Недавно в нашей компании среди разработчиков прошел конкурс на знание подводных камней языка Java. Победители получили почет и призы, все получили много позитива, а также поводов для раздумий и дискуссий. По горячим следам спешим поделиться с хабраобщественостью самыми интересными вопросами, прозвучавшими на викторине. Если вы новичок в Java или профессионал, но хотите освежить в памяти темные стороны языка, добро пожаловать под кат. Там вас ждут 4 каверзных вопроса с ответами, обоснованиями и выводами.


Начнем с баяна классики про упаковку примитивов.
public static void main(String[] args) {
        System.out.println(Byte.valueOf((byte) 48)   == Byte.valueOf((byte) 48));
        System.out.println(Byte.valueOf((byte) 248)  == Byte.valueOf((byte) 248));
        System.out.println(Integer.valueOf(48)       == Integer.valueOf(48));
        System.out.println(Integer.valueOf(248)      == Integer.valueOf(248));
    }

Ответ:
true
true
true
false 


Смахнем пыль с книги под названием Java Language Specification (JLS). Пункт 5.1.7. гарантирует возврат одного и того же объекта при боксинге byte. При боксинге int тоже. Но только для диапазона -128..127. Наша JVM, строго следуя спецификации, в первых трех случаях возвращает один и тот же объект для обеих частей равенства, а в последнем — два разных.

UPD: Как верно заметил senia в комментариях, согласно JLS кеширование int вне этого диапазона -128..127 возможно, но не обязательно.

Мораль: Боксинг — это сложно.
Еще мораль: Чтобы не запутаться, любые объекты надо сравнивать через equals() а не через ==. За исключением случая, когда нам нужно явно определить, что две ссылки указывают на один и тот же экземпляр.


Двигаемся дальше. Простой пример сложной инициализации. Что выведет этот код?
class TrickyClass {
    { value = 10; }
    private int value = 20;
    { value = 30;}
    public int getValue() { return value; }
    public static void main(String[] args) {
       System.out.println(new TrickyClass().getValue());
    }
}

Ответ:
30

Заглянем в пункт 12.5. JLS. Порядок инициализации объекта упрощенно выглядит так:
  1. Выполнение инициализации родительского класса.
  2. Выполнение инициализаторов полей и инициализаторов экземпляров (а странные участки кода в фигурный скобках — это они и есть) по порядку.
  3. Выполнение конструктора класса.


В нашем случае последним выполнится инициализатор { value = 30;}, и именно это значение останется в поле.


Еще один вопрос про инициализацию. Что выведет этот код?
class Base {
   public Base() {
       System.out.println(getName());
   }
   protected String getName() { return "Base";}
}

class Derived extends Base {
   private String name = "Derived";
   @Override
   protected String getName() { return this.name;}
   public static void main(String[] args) {
       new Derived();
   }
}


Ответ
 null 

Согласно уже знакомому нам пункту 12.5. , не предусмотрено специальных правил для перегруженных методов при вызове их из конструктора. В нашем конкретном случае дергается метод дочернего класса Derived, который честно пытается вернуть в родительский конструктор значение поля name. К сожалению, инициализация этого поля еще не произошла, т.к. инициализаторы полей выполняются после вызова родительского конструктора. Поэтому в name лежит законный null.

Мораль: Никогда не вызывайте не-final методы в конструкторах. Но если у вас final класс, то можно.


И на десерт немного сериализации
public class User implements Serializable {

    public final String name;

    public User(String name) { this.name = name; }

    private Object readResolve() { return new User("Darth Vader"); }


    public static void main(String[] args) throws Exception {
        User user = new User("Anakin Skywalker");

        // Serialize user
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        new ObjectOutputStream(outputStream).writeObject(user);

        // Deserialize user
        ByteArrayInputStream inputStreamStream = new ByteArrayInputStream(outputStream.toByteArray());
        User readUser = (User) new ObjectInputStream(inputStreamStream).readObject();

        System.out.println(readUser.name);
    }
}

Ответ:
Darth Vader


Сериализация в Java настолько обширная и темная тема, что сам черт ногу сломит для нее есть отдельная спецификация Java Object Serialization Specification. Пункт 3.7 гласит, что если у десериализуемого объекта объявлен метод readResolve, этот метод будет вызван после чтения объекта из потока. И результат именно этого метода считается результатом десериализации. Каноничный пример использования этой уловки — сериализация\десериализация синглтонов. Использование readResolve вместе с другими специальными методами readObject, writeObject, writeReplace позволяет очень гибко управлять процессом сериализации объектов.

Мораль: К процессу сериализации объектов в Java нужно подходить очень осторожно и предусматривать различные варианты её использования. Если задачу можно решить без механизма сериализации, то лучше так и сделать


В целом, Java — хорошо спроектированный язык. Граблей здесь мало, наступить сложно. Но их знание однажды может сэкономить вам тучу нервов и часов на отладку кода.

Комментарии (15)