Частые исключения в Java: причины и как исправить - Вопросы
Всего: 4 вопросов
1. Как прочитать сообщение NullPointerException в Java 15+ и как исправить это исключение?
Как прочитать сообщение NullPointerException в Java 15+ и как исправить это исключение?
С Java 15 сообщение (helpful NullPointerException, JEP 358) называет операцию и выражение, оказавшееся null: Cannot invoke "String.length()" because "name" is null. Для автораспаковки (int n = map.get("x");) там будет because the return value of "java.util.Map.get(Object)" is null. Если класс скомпилирован javac без ключа -g, вместо имени локальной переменной стоит номер слота, например "<local1>"; IDE, Maven и Gradle компилируют с отладочной информацией и показывают настоящее имя. В Java 8 и 11 сообщения нет, остаётся только номер строки в стеке вызовов. Исправляют NPE правкой кода, а не catch: если значение должно быть, инициализируют ссылку; если оно законно может отсутствовать — getOrDefault() вместо get(), константа слева в "Москва".equals(city), Objects.requireNonNullElse(value, default) или явная проверка if (x != null).
2. Почему ClassCastException возникает только во время выполнения и как его избежать?
Почему ClassCastException возникает только во время выполнения и как его избежать?
Компилятор разрешает приведение, если статический тип переменной (Object, суперкласс, интерфейс) в принципе может содержать нужный тип, а реальный класс объекта проверяется только при выполнении. Поэтому Object value = 42; String s = (String) value; компилируется, но падает с сообщением class java.lang.Integer cannot be cast to class java.lang.String: первым указан реальный класс объекта, вторым — тип, к которому приводили. Заведомо несовместимое приведение, например (String) 42, не компилируется вовсе. Исключение бывает и без явного приведения: если в List<String> через raw type List попало число, компилятор сам вставит приведение при чтении элемента, и ошибка проявится там, далеко от причины. Избегают его так: проверяют тип перед приведением — с Java 16 одной конструкцией if (a instanceof Dog dog) { dog.bark(); }, не используют raw types и не игнорируют предупреждения unchecked.
3. Чем IllegalArgumentException отличается от IllegalStateException и какие методы JDK их выбрасывают?
Чем IllegalArgumentException отличается от IllegalStateException и какие методы JDK их выбрасывают?
IllegalArgumentException означает, что метод получил недопустимое значение аргумента: new ArrayList<>(-1) (Illegal Capacity: -1), "ab".repeat(-1), DayOfWeek.valueOf("monday") (No enum constant java.time.DayOfWeek.monday — имя константы сравнивается с учётом регистра). Его подкласс — NumberFormatException. Исправляют, приводя данные к нужному виду до вызова, например valueOf(input.strip().toUpperCase(Locale.ROOT)). IllegalStateException означает, что аргументы в порядке, но объект находится в неподходящем состоянии — ошибка в порядке вызовов: повторная терминальная операция над тем же Stream (stream has already been operated upon or closed), nextLine() у закрытого Scanner (Scanner closed), matcher.group() до успешного find(), iterator.remove() до next(). Исправляют, соблюдая жизненный цикл объекта: например, хранят данные в коллекции и создают новый поток для каждой операции.
4. Почему возникает ConcurrentModificationException и как правильно удалять элементы коллекции при переборе?
Почему возникает ConcurrentModificationException и как правильно удалять элементы коллекции при переборе?
Итератор ArrayList, HashMap и других коллекций из java.util запоминает счётчик изменений (modCount). Если внутри цикла for-each вызвать list.remove() или list.add() у того же списка, следующий iterator.next() заметит расхождение и выбросит ConcurrentModificationException без сообщения — поэтому в стеке указана строка заголовка цикла, а не строка с remove(). Проверка работает «по возможности» (fail-fast): после удаления предпоследнего элемента цикл просто завершается без исключения, а последний элемент остаётся непроверенным, так что отсутствие исключения не доказывает корректность кода. Правильно удалять через list.removeIf(name -> name.startsWith("А")) (Java 8+) или через remove() самого итератора; у HashMap — map.entrySet().removeIf(...). Несмотря на название, ошибка возникает и в одном потоке; для настоящей многопоточности нужны CopyOnWriteArrayList или ConcurrentHashMap.