Недавно в нашей компании среди разработчиков прошел конкурс на знание подводных камней языка 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. Порядок инициализации объекта упрощенно выглядит так:
- Выполнение инициализации родительского класса.
- Выполнение инициализаторов полей и инициализаторов экземпляров (а странные участки кода в фигурный скобках — это они и есть) по порядку.
- Выполнение конструктора класса.
В нашем случае последним выполнится инициализатор { 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 — хорошо спроектированный язык. Граблей здесь мало, наступить сложно. Но их знание однажды может сэкономить вам тучу нервов и часов на отладку кода.
Автор: Rambler&Co