Какие тонкости и нюансы Java часто упускают из виду?

Ответ

1. String Pool и Immutability строк Строковые литералы кэшируются в пуле строк (String Pool) для экономии памяти. Оператор new создает новый объект в куче.

String a = "hello"; // Может быть взята из пула
String b = "hello"; // Ссылается на тот же объект из пула, что и 'a'
String c = new String("hello"); // Создается новый объект String
System.out.println(a == b); // true (одинаковые ссылки)
System.out.println(a == c); // false (разные объекты)
System.out.println(a.equals(c)); // true (одинаковое содержимое)

2. Кэширование значений в классах-обертках (Integer Cache) JVM кэширует объекты Integer для значений от -128 до 127.

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // true (объект из кэша)

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false (созданы новые объекты)
// Сравнивайте значения через .equals()

3. Переопределение (Override) vs Перегрузка (Overload)

  • @Override: Аннотация для метода, который переопределяет метод суперкласса/интерфейса. Проверяет совместимость сигнатур во время компиляции.
  • Перегрузка: Создание метода с тем же именем, но другими параметрами в том же классе.

4. Try-with-Resources (AutoCloseable) Конструкция автоматически закрывает ресурсы, реализующие AutoCloseable, даже при возникновении исключения.

try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
    System.out.println(br.readLine());
} // fis.close() и br.close() вызываются автоматически здесь

5. Ленивая инициализация статических полей (Initialization-on-demand holder idiom) Класс-холдер загружается только при первом обращении к его статическому полю.

public class Singleton {
    private Singleton() {}

    private static class Holder {
        static final Singleton INSTANCE = new Singleton(); // Инициализация здесь
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE; // Загрузка класса Holder и создание INSTANCE
    }
}

Ответ 18+ 🔞

А, ну вот, опять эти ваши Java-заморочки, блядь. Сидишь, бывает, на собеседовании, а тебе такой: "Расскажи про String Pool". И ты такой, блядь, вспоминаешь, что это не бассейн для строк, а какая-то ёбнутая экономия памяти, пиздец.

Смотри, тут всё просто, как три копейки, но мозг выносит. Возьмём строки.

String a = "hello";
String b = "hello";
String c = new String("hello");

Вот a и b — это, типа, два алкаша у одного ларька. Оба кричат "hello" одному и тому же продавцу, то есть ссылаются на один и тот же объект в этом самом пуле, String Pool'е, блядь. Поэтому a == b даст true. А этот выёбок c, который через new создался — это уже новый, отдельный алкаш, который пришёл со своим "hello", но из другого подъезда. Он в памяти новый объект, поэтому a == c — это false, хоть он и кричит то же самое. А вот a.equals(c)true, потому что equals смотрит не на рожу, а на то, что кричат, на содержимое, блядь.

Дальше, про эти ваши обёртки, Integer. Тут JVM, такая хитрая жопа, решила сэкономить и закэшировала маленькие циферки от -128 до 127.

Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // true

Всё ок, оба ссылаются на один закэшированный объект, как близнецы-братья. Но как только вылезаешь за границу — пиши пропало!

Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false

Всё, кэш кончился, создаются два новых, отдельных объекта. И тут уже == не работает, надо equals использовать, а то на ровном месте багу наловишь, в рот меня чих-пых!

Теперь про @Override и перегрузку. Это две большие разницы, ёпта!

  • @Override — это когда ты в наследнике говоришь: "Батя, твой метод — говно, я сейчас перепишу по-своему". И аннотация @Override стоит как охранник, который проверяет, а точно ли у отца был такой метод, а то вдруг ты опечатался и пишешь какую-то хуйню. Компилятор сразу в морду даст.
  • Перегрузка (Overload) — это когда в одном классе у тебя несколько методов с одним именем, но разными параметрами. Типа, "позвать Петю", "позвать Петю с бутылкой", "позвать Петю с бутылкой и закуской". Имя одно — callPetya, а сигнатуры разные. Вот и вся магия, блядь.

А вот try-with-resources — это вообще песня, ебать мои старые костыли! Раньше надо было в finally блоке эти ваши FileInputStream и BufferedReader закрывать, руки отпадали. А теперь?

try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
    System.out.println(br.readLine());
}

Засунул ресурсы в скобки после try — и спи спокойно. Они сами закроются, даже если посередине чтения исключение выскочит. Реализуют они интерфейс AutoCloseable, и всё, красота. Не жизнь, а малина.

Ну и на десерт — моя любимая ленивая инициализация синглтона, она же Initialization-on-demand holder idiom. Выглядит страшно, но гениально просто.

public class Singleton {
    private Singleton() {} // Конструктор спрятали, чтоб не выёбывались

    private static class Holder {
        static final Singleton INSTANCE = new Singleton(); // Вот где создаётся!
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE; // А вот когда запросили — тогда и загрузился класс Holder
    }
}

Суть в чём, блядь? Класс Holder загрузится в память только тогда, когда кто-то впервые вызовет getInstance(). И только тогда создастся этот самый INSTANCE. До этого — ни хуя не происходит. И потокобезопасно, и лениво, и красиво. Ёперный театр, вот это я понимаю — паттерн!

Вот так вот, коротко и без воды, а то некоторые лекции раздувают, что заслушаешься, блядь.