Топ ошибок начинающих Java разработчиков: продвинутый уровень

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Топ ошибок начинающих Java разработчиков: продвинутый уровень

Многие разработчики, освоив основы Java, переходят к написанию более сложного кода, но продолжают совершать типичные ошибки, которые снижают производительность и надежность приложений. В этой статье мы разберем топ ошибок на продвинутом уровне, которые встречаются у Java-разработчиков, и покажем, как их избежать. Статья будет полезна тем, кто уже знаком с синтаксисом языка, но хочет углубить свои знания.

1. Неправильное использование Optional

Класс Optional был введен в Java 8 для борьбы с NullPointerException. Однако многие разработчики используют его неправильно, превращая элегантное решение в источник новых проблем.

Ошибка: Использование Optional.get() без проверки

Одна из самых частых ошибок — вызов метода get() без предварительной проверки isPresent(). Это приводит к исключению NoSuchElementException.

// Неправильно
Optional<String> name = Optional.ofNullable(getName());
String result = name.get(); // Исключение, если name == null

// Правильно
String result = name.orElse("Default");
// Или с обработкой
name.ifPresent(System.out::println);

Ошибка: Использование Optional в полях класса

Optional не предназначен для использования в качестве полей класса или параметров методов. Это нарушает сериализацию и добавляет лишнюю сложность.

// Неправильно
public class User {
    private Optional<String> email; // Не сериализуется
}

// Правильно
public class User {
    private String email; // null означает отсутствие
}

Ошибка: Цепочки Optional.orElse() с дорогими вычислениями

Метод orElse() вычисляет аргумент всегда, даже если значение присутствует. Используйте orElseGet() для ленивых вычислений.

// Неправильно — expensiveCall() выполняется всегда
String result = optional.orElse(expensiveCall());

// Правильно — expensiveCall() выполняется только при отсутствии значения
String result = optional.orElseGet(() -> expensiveCall());

2. Утечки памяти и неправильная работа с коллекциями

Продвинутые разработчики часто сталкиваются с утечками памяти, которые сложно обнаружить. Рассмотрим основные причины.

Ошибка: Забыли переопределить equals() и hashCode()

При использовании объектов в HashMap или HashSet без корректных equals() и hashCode() объекты не удаляются из коллекции, что приводит к утечкам.

public class Key {
    private int id;
    
    // Забыли переопределить equals() и hashCode()
    // Теперь ключи считаются разными, даже если id совпадает
}

Map<Key, String> map = new HashMap<>();
map.put(new Key(1), "value");
map.get(new Key(1)); // null — утечка памяти

Ошибка: Хранение больших объектов в static коллекциях

Статические коллекции живут до конца работы приложения. Если в них добавлять данные без контроля, это приведет к OutOfMemoryError.

// Плохая практика
public class CacheManager {
    private static List<byte[]> cache = new ArrayList<>();
    
    public static void addData(byte[] data) {
        cache.add(data); // Растет бесконечно
    }
}

// Решение: используйте WeakHashMap или ограничьте размер
private static Map<String, byte[]> cache = new WeakHashMap<>();

Ошибка: Закрытие ресурсов через finalize()

Метод finalize() устарел и ненадежен. Используйте try-with-resources для автоматического закрытия.

// Неправильно
public class Resource {
    @Override
    protected void finalize() throws Throwable {
        // Закрытие ресурса — может не вызваться
    }
}

// Правильно
try (InputStream is = new FileInputStream("file.txt")) {
    // Работа с ресурсом
} // Автоматически закрывается

3. Проблемы с многопоточностью и синхронизацией

Многопоточность — одна из самых сложных тем в Java. Даже опытные разработчики совершают ошибки, которые приводят к race condition и deadlock.

Ошибка: Использование synchronized на строковых литералах

Строковые литералы интернируются, поэтому блокировка на них может привести к неожиданным блокировкам между разными частями программы.

// Неправильно — "LOCK" — интернированная строка
synchronized ("LOCK") {
    // Другие потоки тоже могут использовать "LOCK"
}

// Правильно — используйте отдельный объект
private final Object lock = new Object();
synchronized (lock) {
    // Безопасно
}

Ошибка: Неправильное использование volatile

volatile гарантирует видимость, но не атомарность. Для инкремента используйте AtomicInteger.

// Неправильно — два потока могут прочитать одно значение
private volatile int count = 0;
public void increment() { count++; } // Не атомарно

// Правильно
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }

Ошибка: Забыли обработать InterruptedException

Многие разработчики просто подавляют исключение, что может привести к зависанию потоков.

// Неправильно — подавление исключения
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Игнорируем — плохо
}

// Правильно — восстанавливаем флаг прерывания
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // Восстанавливаем флаг
    // Или выходим из потока
    return;
}

4. Ошибки при работе с Generics и Stream API

С появлением Java 8 многие разработчики начали активно использовать Stream API, но часто делают это неправильно, снижая производительность.

Ошибка: Использование raw types вместо generics

Raw types (например, List без параметра) отключают проверку типов на этапе компиляции и могут привести к ClassCastException.

// Неправильно — raw type
List list = new ArrayList();
list.add("String");
list.add(123); // Компилятор не ругается

// Правильно
List<String> list = new ArrayList<>();
list.add("String");
// list.add(123); // Ошиб                

Блоги

Рекомендаций по книгам