10 самых распространённых ошибок, которые делают новички в Java

Original author: Mikhail Selivanov
  • Translation
  • Tutorial
Здравствуйте, меня зовут Александр Акбашев, я Lead QA Engineer в проекте Skyforge. А также по совместительству ассистент tully в Технопарке на курсе «Углубленное программирование на Java». Наш курс идет во втором семестре Технопарка, и мы получаем студентов, прошедших курсы по C++ и Python. Поэтому я давно хотел подготовить материал, посвященный самым распространенным ошибкам новичков в Java. К сожалению, написать такую статью я так и не собрался. К счастью, такую статью написал наш соотечественник — Михаил Селиванов, правда, на английском. Ниже представлен перевод данной статьи с небольшими комментариями. По всем замечаниям, связанным с переводом, прошу писать в личные сообщения.



Изначально язык Java создавался для интерактивного телевидения, однако со временем стал использоваться везде, где только можно. Его разработчики руководствовались принципами объектно-ориентированного программирования, отказавшись от излишней сложности, свойственной тем же С и С++. Платформонезависимость виртуальной машины Java сформировала в своё время новый подход к программированию. Добавьте к этому плавную кривую обучения и лозунг «Напиши однажды, запускай везде», что почти всегда соответствует истине. Но всё-таки ошибки до сих пор встречаются, и здесь я хотел бы разобрать наиболее распространённые из них.

Ошибка первая: пренебрегают существующими библиотеками


Для Java написано несметное количество библиотек, но новички зачастую не пользуются всем этим богатством. Прежде чем изобретать велосипед, лучше сначала изучите имеющиеся наработки по интересующему вопросу. Многие библиотеки годами доводились разработчиками до совершенства, и вы можете пользоваться ими совершенно бесплатно. В качестве примеров можно привести библиотеки для логирования logback и Log4j, сетевые библиотеки Netty и Akka. А некоторые разработки, вроде Joda-Time, среди программистов стали стандартом де-факто.

На эту тему хочу рассказать о своём опыте работы над одним из проектов. Та часть кода, которая отвечала за экранирование HTML-символов, была написана мной с нуля. Несколько лет всё работало без сбоев. Но однажды пользовательский запрос спровоцировал бесконечный цикл. Сервис перестал отвечать, и пользователь попытался снова ввести те же данные. В конце концов, все процессорные ресурсы сервера, выделенные для этого приложения, оказались заняты этим бесконечным циклом. И если бы автор этого наивного инструмента для замены символов воспользовался одной из хорошо известных библиотек, HtmlEscapers или Google Guava, вероятно, этого досадного происшествия не произошло. Даже если бы в библиотеке была какая-то скрытая ошибка, то наверняка она была бы обнаружена и исправлена сообществом разработчиков раньше, чем проявилась бы на моём проекте. Это характерно для большинства наиболее популярных библиотек.

Ошибка вторая: не используют ключевое слово break в конструкции Switch-Case


Подобные ошибки могут сильно сбивать с толку. Бывает, что их даже не обнаруживают, и код попадает в продакшен. С одной стороны, неудачное выполнение операторов switch часто бывает полезным. Но если так не было задумано изначально, отсутствие ключевого слова break может привести к катастрофическим результатам. Если в нижеприведённом примере опустить break в case 0, то программа после Zero выведет One, поскольку поток выполнения команд пройдёт через все switch, пока не встретит break.

public static void switchCasePrimer() {
    	int caseIndex = 0;
    	switch (caseIndex) {
        	case 0:
            	System.out.println("Zero");
        	case 1:
            	System.out.println("One");
            	break;
        	case 2:
            	System.out.println("Two");
            	break;
        	default:
            	System.out.println("Default");
    	}
}

Чаще всего целесообразно использовать полиморфизм для выделения частей кода со специфическим поведением в отдельные классы. А подобные ошибки можно искать с помощью статических анализаторов кода, например, FindBugs или PMD.

Ошибка третья: забывают освобождать ресурсы


Каждый раз после того, как программа открывает файл или устанавливает сетевое соединение, нужно освобождать использовавшиеся ресурсы. То же самое относится и к ситуациям, когда при оперировании ресурсами возникали какие-либо исключения. Кто-то может возразить, что у FileInputStream есть финализатор, вызывающий метод close() для сборки мусора. Но мы не можем знать точно, когда запустится цикл сборки, поэтому есть риск, что входной поток может занять ресурсы на неопределённый период времени. Специально для таких случаев в Java 7 есть очень полезный и аккуратный оператор try-with-resources:

private static void printFileJava7() throws IOException {
    try(FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while(data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

Этот оператор можно применять с любыми объектами, относящимися к интерфейсу AutoClosable. Тогда вам не придётся беспокоиться об освобождении ресурсов, это будет происходить автоматически после выполнения оператора.

Ошибка четвёртая: утечки памяти


В Java применяется автоматическое управление памятью, позволяющее не заниматься ручным выделением и освобождением. Но это вовсе не означает, что разработчикам можно вообще не интересоваться, как приложения используют память. Увы, но всё же здесь могут возникать проблемы. До тех пор, пока программа удерживает ссылки на объекты, которые больше не нужны, память не освобождается. Таким образом, это можно назвать утечкой памяти. Причины бывают разные, и наиболее частой из них является как раз наличие большого количества ссылок на объекты. Ведь пока есть ссылка, сборщик мусора не может удалить этот объект из кучи. Например, вы описали класс со статическим полем, содержащим коллекцию объектов, при этом создалась ссылка. Если вы забыли обнулить это поле после того, как коллекция стала не нужна, то и ссылка никуда не делась. Такие статические поля считаются корнями для сборщика мусора и не собираются им.

Другой частой причиной возникновения утечек является наличие циклических ссылок. В этом случае сборщик просто не может решить, нужны ли ещё объекты, перекрёстно ссылающиеся друг на друга. Утечки также могут возникать в стеке при использовании JNI (Java Native Interface). Например:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
	BigDecimal number = numbers.peekLast();
   	if (number != null && number.remainder(divisor).byteValue() == 0) {
     	System.out.println("Number: " + number);
		System.out.println("Deque size: " + numbers.size());
	}
}, 10, 10, TimeUnit.MILLISECONDS);

	scheduledExecutorService.scheduleAtFixedRate(() -> {
		numbers.add(new BigDecimal(System.currentTimeMillis()));
	}, 10, 10, TimeUnit.MILLISECONDS);

try {
	scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
	e.printStackTrace();
}

Здесь создаётся два задания. Одно из них берёт последнее число из двусторонней очереди numbers и выводит его значение и размер очереди, если число кратно 51. Второе задание помещает число в очередь. Для обоих заданий установлено фиксированное расписание, итерации происходят с интервалом в 10 миллисекунд. Если запустить этот код, то размер очереди будет увеличиваться бесконечно. В конце концов это приведёт к тому, что очередь заполнит всю доступную память кучи. Чтобы этого не допустить, но при этом сохранить семантику кода, для извлечения чисел из очереди можно использовать другой метод: pollLast. Он возвращает элемент и удаляет его из очереди, в то время как peekLast только возвращает.

Если хотите узнать побольше об утечках памяти, то можете изучить посвящённую этому статью.

Примечание переводчика: на самом деле, в Java решена проблема циклических ссылок, т.к. современные алгоритмы сборки мусора учитывают достижимость ссылок из корневых узлов. Если объекты, содержащие ссылки друг на друга, не достижимы от корня, они будут считаться мусором. Об алгоритмах работы сборщика мусора можно почитать в Java Platform Performance: Strategies and Tactics.

Ошибка пятая: чрезмерное количество мусора




Такое случается, когда программа создаёт большое количество объектов, использующихся в течение очень непродолжительного времени. При этом сборщик мусора безостановочно убирает из памяти ненужные объекты, что приводит к сильному падению производительности. Простой пример:

String oneMillionHello = "";
for (int i = 0; i < 1000000; i++) {
    oneMillionHello = oneMillionHello + "Hello!";
}
System.out.println(oneMillionHello.substring(0, 6));

В Java строковые переменные являются неизменяемыми. Здесь при каждой итерации создаётся новая переменная, и для адресации нужно использовать изменяемый StringBuilder:

StringBuilder oneMillionHelloSB = new StringBuilder();
    for (int i = 0; i < 1000000; i++) {
        oneMillionHelloSB.append("Hello!");
    }
System.out.println(oneMillionHelloSB.toString().substring(0, 6));

Если в первом варианте на выполнение кода уходит немало времени, то во втором производительность уже гораздо выше.

Ошибка шестая: использование без необходимости нулевых указателей


Старайтесь избегать применения null. Например, возвращать пустые массивы или коллекции лучше методами, чем null, поскольку это позволит предотвратить появление NullPointerException. Ниже представлен пример метода, обрабатывающего коллекцию, полученную из другого метода:

List<String> accountIds = person.getAccountIds();
for (String accountId : accountIds) {
    processAccount(accountId);
}

Если getAccountIds() возвращает null, когда у person нет account, то возникнет NullPointerException. Чтобы этого не произошло, необходимо делать проверку на null. А если вместо null возвращается пустой список, то проблема с NullPointerException не возникает. К тому же код без null-проверок получается чище.

В разных ситуациях можно по-разному избегать использования null. Например, использовать класс Optional, который может быть как пустым объектом, так и обёрткой (wrap) для какого-либо значения:

Optional<String> optionalString = Optional.ofNullable(nullableString);
if(optionalString.isPresent()) {
    System.out.println(optionalString.get());
}

В Java 8 используется более лаконичный подход:

Optional<String> optionalString = Optional.ofNullable(nullableString);
optionalString.ifPresent(System.out::println);

Optional появился в восьмой версии Java, но в функциональном программировании он использовался ещё задолго до этого. Например, в Google Guava для ранних версий Java.

Ошибка седьмая: игнорирование исключений


Зачастую начинающие разработчики никак не обрабатывают исключения. Однако не стоит пренебрегать этой работой. Исключения бросаются не просто так, и в большинстве случаев нужно разбираться с причинами. Не игнорируйте подобные события. Если нужно, бросьте их заново, чтобы посмотреть сообщение об ошибке, или залогируйте. В крайнем случае, нужно хотя бы обосновать для других разработчиков причину, по которой вы не разбирались с исключением.

selfie = person.shootASelfie();
try {
    selfie.show();
} catch (NullPointerException e) {
    // Может, человек-невидимка. Да какая разница?
}

Лучше всего обозначить незначительность исключения с помощью сообщения в переменной:

try { selfie.delete(); } catch (NullPointerException unimportant) {  }

Примечание переводчика: Практика не устаёт доказывать, что не бывает неважных исключений. Если исключение хочется проигнорировать, то нужно добавлять какие-то дополнительные проверки, чтобы либо не вызывать исключение в принципе, либо игнорировать исключение сверхточечно. Иначе вас ожидают долгие часы дебага в поисках ошибки, которую так легко было написать в лог. Также нужно помнить, что создание исключения — операция не бесплатная. Как минимум, нужно собрать коллстэк, а для этого нужно приостановиться на safepoint. И это всё занимает время...

Ошибка восьмая: ConcurrentModificationException


Это исключение возникает, когда коллекция модифицируется во время итерирования любыми методами, за исключением средств самого итератора. Например, у нас есть список головных уборов, и мы хотим убрать из него все шапки-ушанки:

List<IHat> hats = new ArrayList<>();
hats.add(new Ushanka()); // that one has ear flaps
hats.add(new Fedora());
hats.add(new Sombrero());
for (IHat hat : hats) {
    if (hat.hasEarFlaps()) {
        hats.remove(hat);
    }
}



При выполнении этого кода вылезет ConcurrentModificationException, поскольку код модифицирует коллекцию во время итерирования. То же самое исключение возникнет, если один из нескольких тредов, работающих с одним списком, попытается модифицировать коллекцию, пока другие треды итерируют её. Одновременное модифицирование коллекции является частым явлением при многопоточности, но в этом случае нужно применять соответствующие инструменты, вроде блокировок синхронизации (synchronization lock), специальных коллекций, адаптированных для одновременной модификации и т.д.

В случае с одним тредом эта проблема решается немного иначе.

Собрать объекты и удалить их в другом цикле

На ум сразу приходит решение собрать ушанки и удалить их во время следующего цикла. Но тогда придётся создать новую коллекцию для хранения шапок, приготовленных для удаления.

List<IHat> hatsToRemove = new LinkedList<>();
for (IHat hat : hats) {
    if (hat.hasEarFlaps()) {
        hatsToRemove.add(hat);
    }
}
for (IHat hat : hatsToRemove) {
    hats.remove(hat);
}

Использовать метод Iterator.remove

Это более лаконичный способ, при котором не нужно создавать новую коллекцию:

Iterator<IHat> hatIterator = hats.iterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.remove();
    }
}

Использовать методы ListIterator

Когда модифицированная коллекция реализует интерфейс List, целесообразно использовать итератор списка (list iterator). Итераторы, реализующие интерфейс ListIterator, поддерживают как операции удаления, так и добавления и присвоения. ListIterator реализует интерфейс Iterator, так что наш пример будет выглядеть почти так же, как и метод удаления Iterator. Разница заключается в типе итератора шапок и его получении с помощью метода listIterator(). Нижеприведённый фрагмент демонстрирует, как можно заменить каждую ушанку на сомбреро с помощью методов ListIterator.remove и ListIterator.add:

IHat sombrero = new Sombrero();
ListIterator<IHat> hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.remove();
        hatIterator.add(sombrero);
    }
}

С помощью ListIterator вызовы методов удаления и добавления могут быть заменены одним вызовом:

IHat sombrero = new Sombrero();
ListIterator<IHat> hatIterator = hats.listIterator();
while (hatIterator.hasNext()) {
    IHat hat = hatIterator.next();
    if (hat.hasEarFlaps()) {
        hatIterator.set(sombrero); // set instead of remove and add
    }
}

Используя поточные методы, представленные в Java 8, можно трансформировать коллекцию в поток, а потом отфильтровать его по каким-либо критериям. Вот пример того, как поточный API может помочь в фильтрации шапок без появления ConcurrentModificationException:

hats = hats.stream().filter((hat -> !hat.hasEarFlaps()))
        .collect(Collectors.toCollection(ArrayList::new));

Метод Collectors.toCollection создаёт новый ArrayList с отфильтрованными шапками. Если критериям удовлетворяет большое количество объектов, то это может быть проблемой, поскольку ArrayList получается довольно большим. Так что пользуйтесь этим способом с осторожностью.

Можно поступить другим образом — использовать метод List.removeIf, представленный в Java 8. Это самый короткий вариант:

hats.removeIf(IHat::hasEarFlaps);

И всё. На внутреннем уровне этот метод задействуется Iterator.remove.

Использовать специализированные коллекции

Если бы в самом начале мы решили использовать CopyOnWriteArrayList вместо ArrayList, то проблем бы вообще не было, потому что CopyOnWriteArrayList использует методы модифицирования (присвоения, добавления и удаления), которые не меняют базовый массив (backing array) коллекции. Вместо этого создаётся новая, модифицированная версия. Благодаря этому можно одновременно итерировать и модифицировать исходную версию коллекции без опасения получить ConcurrentModificationException. Недостаток у этого способа очевиден — приходится генерировать новую коллекцию для каждой модификации.

Существуют коллекции, настроенные для разных случаев, например, CopyOnWriteSet и ConcurrentHashMap.

Другой возможной ошибкой, связанной с ConcurrentModificationException, является создание потока из коллекции, а потом модифицирование базовой коллекции (backing collection) во время итерирования потока. Избегайте этого. Ниже приведён пример неправильного обращения с потоком:

List<IHat> filteredHats = hats.stream().peek(hat -> {
    if (hat.hasEarFlaps()) {
        hats.remove(hat);
    }
}).collect(Collectors.toCollection(ArrayList::new));

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

Ошибка девятая: нарушение контрактов


Бывает, что для правильной работы кода из стандартной библиотеки или от какого-то вендора нужно соблюдать определённые правила. Например, контракт hashCode и equals гарантирует работу набора коллекций из фреймворка коллекций Java, а также других классов, использующих методы hashCode и equals. Несоблюдение контракта не всегда приводит к исключениям или прерыванию компиляции. Тут всё несколько сложнее, иногда это может повлиять на работу приложения так, что вы не заметите ничего подозрительного. Ошибочный код может попасть в продакшен и привести к неприятным последствиям. Например, стать причиной глючности UI, неправильных отчётов данных, низкой производительности, потери данных и т.д. К счастью, такое случается редко. Тот же вышеупомянутый контракт hashCode и equals используется в коллекциях, основанных на хэшинге и сравнении объектов, вроде HashMap и HashSet. Проще говоря, контракт содержит два условия:
  • Если два объекта эквивалентны, то их коды тоже должны быть эквивалентны.
  • Если даже два объекта имеют одинаковые хэш-коды, то они могут и не быть эквивалентны.

Нарушение первого правила приводит к проблемам при попытке извлечения объектов из hashmap.

public static class Boat {
    private String name;

    Boat(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Boat boat = (Boat) o;

        return !(name != null ? !name.equals(boat.name) : boat.name != null);
    }

    @Override
    public int hashCode() {
        return (int) (Math.random() * 5000);
    }
}

Как видите, класс Boat содержит переопределённые методы hashCode и equals. Но контракт всё равно был нарушен, потому что hashCode возвращает каждый раз случайные значения для одного и того же объекта. Скорее всего, лодка под названием Enterprise так и не будет найдена в массиве хэшей, несмотря на то, что она была ранее добавлена:

public static void main(String[] args) {
    Set<Boat> boats = new HashSet<>();
    boats.add(new Boat("Enterprise"));

    System.out.printf("We have a boat named 'Enterprise' : %b\n", boats.contains(new Boat("Enterprise")));
}

Другой пример относится к методу finalize. Вот что говорится о его функционале в официальной документации Java:
Основной контракт finalize заключается в том, что он вызывается тогда и если, когда виртуальная машина определяет, что больше нет никаких причин, по которым данный объект должен быть доступен какому-либо треду (ещё не умершему). Исключением может быть результат завершения какого-то другого объекта или класса, который готов быть завершённым. Метод finalize может осуществлять любое действие, в том числе снова делать объект доступным для других тредов. Но обычно finalize используется для действий по очистке до того, как объект необратимо удаляется. Например, этот метод для объекта, представляющего собой соединение input/output, может явным образом осуществить I/O транзакции для разрыва соединения до того, как объект будет необратимо удалён.

Не надо использовать метод finalize для освобождения ресурсов наподобие обработчиков файлов, потому что неизвестно, когда он может быть вызван. Это может произойти во время работы сборщика мусора. В результате продолжительность его работы непредсказуемо затянется.

Ошибка десятая: использование сырых типов (raw type) вместо параметризованных


Согласно спецификации Java, сырой тип является либо непараметризованным, либо нестатическим членом класса R, который не унаследован от суперкласса или суперинтерфейса R. До появления в Java обобщённых типов, альтернатив сырым типам не существовало. Обобщённое программирование стало поддерживаться с версии 1.5, и это стало очень важным шагом в развитии языка. Однако ради совместимости не удалось избавиться от такого недостатка, как потенциальная возможность нарушения системы классов.

List listOfNumbers = new ArrayList();
listOfNumbers.add(10);
listOfNumbers.add("Twenty");
listOfNumbers.forEach(n -> System.out.println((int) n * 2));

Здесь список номеров представлен в виде сырого ArrayList. Поскольку его тип не задан, мы можем добавить в него любой объект. Но в последней строке в int забрасываются элементы, удваиваются и выводятся. Этот код откомпилируется без ошибок, но если его запустить, то выскочит исключение на этапе выполнения (runtime exception), потому что мы пытаемся записать строчную переменную в числовую. Очевидно, что если мы скроем от системы типов необходимую информацию, то она не убережёт нас от написания ошибочного кода. Поэтому старайтесь определять типы объектов, которые собираетесь хранить в коллекции:

List<Integer> listOfNumbers = new ArrayList<>();

listOfNumbers.add(10);
listOfNumbers.add("Twenty");

listOfNumbers.forEach(n -> System.out.println((int) n * 2));

От первоначального варианта этот пример отличается строкой, в которой задаётся коллекция:

List<Integer> listOfNumbers = new ArrayList<>();

Этот вариант не откомпилируется, поскольку мы пытаемся добавить строковую переменную в коллекцию, которая может хранить только числовые. Компилятор выдаст ошибку и укажет на строку, в которой мы пытаемся добавить в список строковую Twenty. Так что всегда старайтесь параметризировать обобщённые типы. В этом случае компилятор сможет всё проверить, и шансы появления runtime exception из-за противоречий в системе типов будут сведены к минимуму.

Заключение


Многие моменты в разработке ПО на платформе Java упрощены, благодаря разделению на сложную Java Virtual Machine и сам язык. Однако широкие возможности, вроде автоматического управления памятью или приличных OOP-инструментов, не исключают вероятности возникновения проблем. Советы здесь универсальны: регулярно практикуйтесь, изучайте библиотеки, читайте документацию. И не забывайте о статических анализаторах кода, они могут указать на имеющиеся баги и подсказать, на что стоит обратить внимание.
Mail.ru Group
Building the Internet

Comments 85

    +16
    <troll>
    Ошибка 0: Использование Java вместо Scala
    </troll>
      +4
      s/Scala/Clojure/ ;)
        +11
        Тут же про начинающих речь. С тем, количеством фичей, которые есть в Скале и список будет не из 10, а из 100 пунктов, а первой ошибкой будет — не разобрался в sbt и покончил с собой.
          +3
          Чтобы с sbt разобраться нужно посмотреть выступление Даниэля Вестхайда с ScalaDays 2013? 2014? — он очень классно за час все объясняет.
          www.parleys.com/talk/learn-you-sbt-fun-profit

          После этого мои волосы стали мягкими и шелковистыми, а билды — простыми и надежными
            +2
            И вас устраивает количество мусора в проекте? Разные build.sbt, pligins.sbt и отдельная папка project? Уж лучше лаконичный Gradle с одним скриптом build.gradle, чем этот вынос мозга.
              +2
              У меня, например, gradle не подружился с идеей. На каждый чих она его запускает, что даёт жуткие тормоза. Но, может быть, это проблема интеграции с IDE.
                +1
                Поставьте галочку offline mode в настройках грэдла в идее — может дело в репозитории.
                  0
                  С maven и sbt (отнюдь не в offline-mode) таких проблем не было. Сейчас проектов на gradle на поэкспериментировать нет под рукой.
                +2
                В смысле? У меня есть 4 файла:
                — build.sbt — описание всех проектов и подпроектов. Чаще всего делается с нуля для каждого.
                — project/Dependencies.scala — я так привык задавать разные зависимости и делать package sets
                — project/plugins.sbt — подключение плагинов. Мигрирует из проекта в проект
                — settings.sbt — настройки scalariform, revolver, packaging, web plugin etc
                Иногда еще таски дополнительные делаю, но там зависит.

                Где там мусор? Когда в проекте 300 файлов исходников, 4 файла которые управляют всем билдом, все аккуратно разнесено по нужным файликам, каждый из которых не более 20 строк.

                Если писать не hello-world, то билд-система должна быть достаточно мощной, но и простой для начинающих. В sbt, соглашусь, классовый переход между простотой и мощностью чуток затянут, не спорю, но для понимания достаточно один раз посмотреть видео выше.
                  0
                  Это много раскиданных файлов вместо одного скрипта на проект в корне.
                  0
                  Опять же, зачем мне учить еще один язык, вдобавок, не добавляющий никаких новых идиом, а скорее конфузящий. Как правильно перевести confusing?
                    0
                    Groovy намного ближе к Java, чем Scala. Использовать для Java проектов SBT я бы не стал. При этом в Gradle отличная поддержка всех трёх языков: Java/Groovy/Scala.
                      0
                      Я думаю, что речь тут о том, что когда Scala уже выучена — зачем учить ещё и Groovy, который кроме как для конфигурирование Gradle всё равно больше нигде смысла использовать нет в случае проекта на Scala
                      0
                      «Смущающий»
                        +1
                        Сбивающий с толку, скорее.
              +10
              По поводу пятой ошибки: сдается мне, в приведенном примере замедляет работу программы не столько обилие мусора, сколько квадратичная сложность алгоритма
                +9
                Еще пара распространенных ошибок:
                — использование не thread safe классов в многопоточных приложениях. Сколько людей пыталось сэкономить на
                private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd"); 
                

                и обнаруживали потом странные даты.
                — Классика жанра: январь месяц в Calendar имеет нулевой номер, лечится или JodaTime или LocalDate в Java 8, где месяцы как положено пронумерованы от 1 до 12.
                  0
                  Еще можно лечить ThreadLocal, помнится мы так делали.
                    0
                    Насчёт SimpleDateFormat ошибка была в том, что FindBugs не использовали :D Он же, кстати, неплохо находит третью, пятую и девятую ошибки и позволяет решить много головняков с NPE. Восьмую ошибку в ряде случаев находит fb-contrib (плагин к FindBugs, хотя он много мусора выдаёт).
                      +3
                      Могу ещё посоветовать полезный maven plugin и ant task forbiddenapis: code.google.com/p/forbidden-apis/

                      Из коробки поддерживает следующее:
                      The JAR file contains the following signatures and can be used in Maven or Ant using . All signatures are versioned against the specified JDK version:

                      jdk-unsafe-*: Signatures of «unsafe» methods that use default charset, default locale, or default timezone. For server applications it is very stupid to call those methods, as the results will definitely not what the user wants (for Java * = 1.5, 1.6, 1.7, 1.8; Maven automatically adds the compile Java version)
                      jdk-deprecated-*: This disallows all deprecated methods from the JDK (for Java * = 1.5, 1.6, 1.7, 1.8; Maven automatically adds the compile Java version)
                      jdk-system-out: On server-side applications or libraries used by other programs, printing to System.out or System.err is discouraged and should be avoided (any java version, no specific version)
                      commons-io-unsafe-*: If your application uses the famous Apache Common-IO library, this adds signatures of all methods that depend on default charset (for versions * = 1.0, 1.1, 1.2, 1.3, 1.4, 2.0, 2.1, 2.2, 2.3, 2.4)
                    +1
                    Другой частой причиной возникновения утечек является наличие циклических ссылок. В этом случае сборщик просто не может решить, нужны ли ещё объекты, перекрёстно ссылающиеся друг на друга.

                    GC спокойно справляется с этим, т.к. ищет из «корней» все достижимые объекты. Если некий набор перекрестно ссылающихся объектов недостижим, то он будет спокойно очищен сборщиком мусора.
                      0
                      Это если не замкнуть через какой-нибудь threadlocal и/или classloader =)
                        0
                        Это одни из «корней», т.е. GC и не должен очищать объекты доступные по ссылкам из thread local/classloader.
                        Тут скорее возможность получить OOM, если бесконтрольно класть что-то в thread local или перезагружать классы/класслоадеры.
                          0
                          Когда вы загружаете приложение, используя свой кусок иерархии класслоадеров, это уже далеко не корень. Простой пример — servlet container или osgi.
                          0
                          Можно поподробнее?
                          Про threadlocal я примерно вкурсе, а вот что можно сделать с classloader нет. Поделитесь

                          P.S. не static threadlocal зло)
                            0
                            Смоделирую кейс, который приходилось лечить и попробую написать на днях статью. Для комментария это слишком монструозно получится.
                          +1
                          Кажется, я упомянул об этом в примечаниях переводчика :)
                            0
                            Прошу прощения, читал «по диагонали», не заметил примечание.
                          +16
                          По-моему сюда нужно вписать использование == для String, Integer и т.д без понимания как это работает.
                            +1
                            Да, это тоже классика.
                              0
                              Думал, это будет первый пункт в списке.
                                +2
                                Самое ужасное при сравнении для новичков в java это то, что сравнение Integer корректно работает для чисел меньше 127 по модулю. Так что при написании каких- нибудь лабораторных работ они могут даже не столкнуться с такой проблемой.
                                  0
                                  Если быть точнее до -128… +127
                                    +3
                                    Давайте и я позанудствую: от -128 до значения опции -XX:AutoBoxCacheMax (по умолчанию 127).
                                      0
                                      И я позанудствую: от -128 до -Djava.lang.Integer.IntegerCache.high.
                                    0
                                    Ещё можно вспомнить про то, что числа, начинающиеся с нуля превращаются в восьмеричные.
                                    +3
                                    В Scala, кстати, сравнение == это то же самое, что equals, а вот сравнение по ссылке делается через eq.
                                      +5
                                      Я пришёл в java из c# и js, и просто охренел когда обнаружил такое поведение. Оно меня до сих пор просто убивает.
                                        +1
                                        Зато оно логично =)
                                          +9
                                          Да в жопу такую логику. Это неудобно.
                                            +1
                                            Ну явное использование Integer вообще подозрительно и в подавляющем большинстве случаев ненужно. Поэтому проблема остаётся только со String.
                                              –1
                                              А мне вот нужно.
                                              К примеру сервер должен возвращать матрицу чисел, в некоторых ячейках могут быть null, тогда и появляется Double, Integer.
                                                +4
                                                null в матрице? Опишите какую задачу вы решаете?

                                                Что касается поведения, то по-моему equals является вариантом более лучшим, чем == хотя бы потому, что можно переопределить его логику. Ну и равенство ссылок тоже как-то надо проверять. Java придумывали крайне умные люди и они не зря так сделали. В ней нету не одной необъяснимой конструкции.
                                                  +3
                                                  Получение среза данных из куба по mdx запросу.

                                                  В c# есть переопределение операторов, и это более удобно — не надо делать проверки на null, как с equals.

                                                  А так получается, что оператор == в java ну нафик не нужен.
                                                    +3
                                                    Если нужно специальное значение в double, можно NaN'ами добить. Сэкономите большое количество памяти на самом деле. Кстати, если нужно много специальных значений, можно завести неканонические NaN'ы и читать их через Double.doubleToRawLongBits.
                                                      0
                                                      А вот за это спасибо :) Попробую.
                                                      0
                                                      Возникает логичный вопрос, если с# так хорош, то зачем вы на java перешли?
                                                        +1
                                                        Хороший вопрос: ответ простой — на java есть одна хорошая open-source библиотека для работы с кубами и mdx — mondrian.
                                                        На C# ничего подобного не было, да и вообще где-либо.
                                                  0
                                                  А что делать, если генерики не могут быть параметризованы примитивными типами?
                                                    +2
                                                    Использовать специальные коллекции, конечно. Их сейчас куча наплодилось. Почитайте, например, этот обзор. Или вот. Выигрыш и по памяти, и по скорости иногда в 10 раз.
                                          +9
                                          По моему, первая ошибка новичка в Java — не читать книги по Java :) Т.к. в них большая часть ошибок разобрана.
                                          Да и судя по опыту, это не только ошибка новичка, но и более опытных Java программистов. :(
                                            +1
                                            К сожалению, и не только Java.
                                              0
                                              Кстати, какие лучшие книги по Java рекомендуете?
                                              Классика, т.с., библии?
                                                +1
                                                Для старта, мне кажется, нет ничего лучше:
                                                Core Java Volume I--Fundamentals (9th Edition) (Core Series) by Cay S. Horstmann
                                                Core Java, Volume II--Advanced Features (9th Edition) (Core Series) by Cay S. Horstmann
                                                  +3
                                                  См. toster.ru, таких вопросов штук 20, не преувеличение.
                                                –1
                                                Ошибка новичка №0:
                                                Читать переводную литературу и статьи.

                                                P.S. Местами предложения и термины лучше было не переводить вовсе.
                                                  +1
                                                  StringBuilder oneMillionHelloSB = new StringBuilder();
                                                      for (int i = 0; i < 1000000; i++) {
                                                          oneMillionHelloSB.append("Hello!");
                                                      }
                                                  System.out.println(oneMillionHelloSB.toString().substring(0, 6));
                                                  

                                                  Кстати, если вы формируете огромную строку в StringBuilder'е и способны предугадать её длину (или хотя бы оценить сверху), полезно передать её параметром в конструктор. В данном случае передача в конструктор 1000000*"Hello!".length() заметно ускоряет работу:

                                                  Benchmark        Mode  Cnt   Score   Error  Units
                                                  SbBench.control  avgt   30  14.139 ± 0.231  ms/op // это как в статье
                                                  SbBench.fixedSB  avgt   30   9.991 ± 0.042  ms/op // это с параметром
                                                  

                                                  Вы выигрываете на том, что не надо перевыделять массив и копировать из старого.
                                                    –2
                                                    что мертвому припарки.
                                                      0
                                                      Поясните вашу фразеологию.
                                                        0
                                                        Избавились от кучи выделений памяти, а ускорились всего в полтора раза. Меня не впечатляет.
                                                          +7
                                                          А можно с другой стороны посмотреть: изменили всего одну строчку и ускорились ажно в полтора раза :-)

                                                          Если вам очень часто нужен такой алгоритм для большого количества строчек (непонятно зачем, конечно) и хочется его ещё ускорить, можно применить стратегию разделяй и властвуй:

                                                          private static void timesClever0(StringBuilder sb, int n, String s) {
                                                              if(n == 0) return;
                                                              if(n % 2 == 0) {
                                                                  timesClever0(sb, n/2, s);
                                                                  sb.append(sb);
                                                              } else {
                                                                  timesClever0(sb, n-1, s);
                                                                  sb.append(s);
                                                              }
                                                          }
                                                          
                                                          private static String timesClever(int n, String s) {
                                                              if(n == 0) return "";
                                                              StringBuilder sb = new StringBuilder(n*s.length());
                                                              timesClever0(sb, n, s);
                                                              return sb.toString();
                                                          }
                                                          

                                                          Результаты JMH (сейчас я за другой машиной, с предыдущими не надо сравнивать):

                                                          Benchmark                Mode  Cnt   Score   Error  Units
                                                          SbBench.builderClever    avgt   30   9.442 ± 0,449  ms/op
                                                          SbBench.builderLength    avgt   30  15.443 ± 1,251  ms/op
                                                          SbBench.builderNoLength  avgt   30  22.329 ± 0,761  ms/op
                                                          

                                                          Полный код бенчмарка здесь.
                                                      +1
                                                      По-моему это очевидно и без бенчмарков. Аналогично и для коллекций: указание примерного размера при создании ArrayList / HashSet / HashMap etc. дает ощутимый выигрыш в производительности.
                                                        +2
                                                        «Примерного» — неподходящее слово. Если размер близок, но чуть меньше, то память всё равно будет выделяться дважды. И насчёт «очевидно и без бенчмарков» — с этим осторожнее. В JIT-компиляторе столько магии, что всё надо реально проверять — и ассемблер смотреть, и профилировать. В некоторых тестах, например, ""+num (что эквивалентно new StringBuilder("").append(num).toString()) работает быстрее, чем Integer.toString(num). Как это получается, я до сих пор не понял.
                                                      –8
                                                      А еще многие не умеют играть в «try/catch»:
                                                      try{
                                                       //...
                                                      }catch(){
                                                      }finally{
                                                        try{
                                                         a.close();//закрыли поток
                                                        }catch(){
                                                         //..
                                                        }
                                                      }

                                                      Просто и эффективно.
                                                        +3
                                                        Конструкция в общем полезная, но сейчас есть try_with_resources и надобность в ней отпала.
                                                          0
                                                          Да, вы правы, с 1.7 ввели.
                                                        0
                                                        по поводу 6. сразу дисклеймер — разговор про яву только.

                                                        во-первых, не надо использовать Optional в яве. не оправдано это.
                                                        во-вторых, возврат пустых коллекций приведет к тому, что придется научиться копировать их перед использованием. Поскольку Collections.emptyList не позволит их модифицировать, и это может сказаться в неожиданных местах.
                                                          0
                                                          Почему же не оправдано?
                                                            +1
                                                            ну, у меня только из опыта есть аргументы.

                                                            0. Это инструмент. Соответственно пользоваться им надо от необходимости. Я не вижу экономии букв, если просто ввести Optional.
                                                            1. Получается миксинг из подходов null и Optional. Потому что без третьих библиотек не обойтись, а там Optional не бывает. Даже если привык и нравится использовать Optional — все равно в середине перфекционизма может возникнуть работа с null.

                                                            2. И крайний аргумент. Самый идиотский, из-за которого работа с библиотеками становится пыткой — это type erasure. В случае c Optional придется потратить уйму времени на работу с orm или биндингами параметров в методах spring webmvc.
                                                            В результате получиться ситуация, что в контроллерах — null, в DAO — null. Ну а посередине, где получится, гордясь от знаний функциональности, будем впихивать Optional.

                                                            Я не против нового, сам первый встаю под новые знамена. Но случай Optional в яве — мне кажется сомнительным и не оправдывающим себя.
                                                              –1
                                                              Ну тогда может и от lambda, stream api, новых concurrent collections и CompletableFuture и т.д откажимся?
                                                              Вы просто иронизируете, ничего страшного в миксе нету. Просто нужно качественно описание ваших интерфейсов)
                                                                +1
                                                                не откажемся. потому что это интересно и полезно. в случае Optional — я не вижу полезности, а вот грабли — вижу.

                                                                Я же не о преимуществах сферического коня в вакууме спорю. Поэтому можно провести эксперимент — взять пару проектов покрупнее и посмотреть насколько распространены Optional.
                                                                  +1
                                                                  Тут я, кстати, соглашусь: Optional мне кажется ересью в мире Java. Просто кидайте исключение вместо того, чтобы возвращать null. В большинстве случаев бизнес-логики это оптимальный вариант. Остальные перечисленные вами новшества Java 8 классные, их можно и нужно использовать.
                                                                    0
                                                                    Ну вы же наверное знаете, что исключение дорогостоящая операция)
                                                                      +2
                                                                      Ещё я знаю, что преждевременная оптимизация — корень зла ;-)
                                                                  0
                                                                  Понял вас. В принципе, я согласен со всеми доводами, они все входят в мой внутренний список причин, по которым использование Java в принципе не оправдано)
                                                                +3
                                                                А кто вам сказал, что коллекцию, которую вернул метод, можно модифицировать? Если это требуется, должно быть в контракте метода прописано. Но зачастую это дополнительные ненужные расходы ресурсов. Так всегда можно вернуть ImmutableList (Guava), который устроен экономнее. Для одного значения можно вернуть singletonList. Наконец, часто эффективнее вернуть ссылку на внутреннюю структуру, обернув её в unmodifiableList для безопасности. Если хотите модифицировать, просто оберните результат метода в new ArrayList<>(...).
                                                                  +1
                                                                  Вы предлагаете всегда (при необходимости изменений) оборачивать в новый ArrayList. Это интересный метод, не всегда эффективный. И не всегда возможный. Например, если изменяемая коллекция передается по ссылке и при этом нет возможности установить возвращаемое значение.

                                                                  Я просто не вижу особо смысла пытаться применить одинаковый подход «с сегодняшнего дня везде возвращаем пустой read-only список», потому что так в переводной статье написано. Каждое решение и его использование должно быть обосновано. Выносить это в must have — я не поддерживаю.

                                                                  «А кто вам сказал, что коллекцию, которую вернул метод, можно модифицировать?»

                                                                  Это есть в контракте Collection. И у ImmutableList он тоже есть кстати. И там ещё пара отдельных исключений возникает — например, поддерживает ли он null в коллекции и может ли он содержать «вот эти» конкретные типы. А мы спорим только об одном — это несправедливо по отношению к бедному Collection.
                                                                    +1
                                                                    Необходимость модификации коллекции, которую вам вернули, исключительно редка. Если контракт метода подразумевает, что коллекция возвращается по ссылке и может быть изменена пользователями снаружи, это отдельный случай. Но и в таком случае смысла нет возвращать null, ведь тогда вы тем более не сможете модифицировать коллекцию! Надо завести в вашей переменной пустой ArrayList и его и возвращать. Отмечу, что это в большинстве случаев суровое нарушение инкапсуляции (если речь идёт о публичном методе), по сути ничем не лучше публичного поля: клиенты могут бесконтрольно менять внутреннее состояние объекта.
                                                                      0
                                                                      возращать new ArrayList — это нормально.

                                                                      Я же изначально имел в виду другие грабли: в статье написано «пустой список». И этот подход в ловких руках с использованием Collections.emptyList() может дать неожиданный результат, о котором новичкам тоже надо знать.
                                                                      0
                                                                      Это есть в контракте Collection.
                                                                      А можно ссылочку на этот контракт? А то вообще странно — в самой java.util.Collections есть метод — emptyList(). И наверное не зря же его придумали. Да и используется (сейчас посмотрел в IDEA в нашем проекте) в 14 случаях в JDK и в 170 случаях во внешних либах (Guava, Mockito, Apache Common) и singletoneList используется в 10 случаях в JDK и в 70 в библиотеках. Так что вполне рабочий метод — использовать unmodifiable коллекции. Хотя да, если часто изменять возвращаемые из методов коллекции, то может быть сюрприз, когда это сделать не удастся. Но это уже пользователи методов виноваты :)
                                                                        0
                                                                        Ну так контрактом является сам интерфейс Collection. У него есть методы, которые изменяют коллекцию.
                                                                        То есть, если вы принимаете на вход Collection, то непонятно, что с ним можно сделать, если не вызвать add().
                                                                        Наличие исключений дает ситуацию, что иногда можно изменять, иногда нет. Например список лимитированного размера — он вроде add поддерживает, но до определенного момента.

                                                                        Тонкость emptyList() в том что он возвращает не только empty, но и read-only list. И это может ввести в заблуждение. И приводит к появлению виноватых пользователей.
                                                                          +1
                                                                          Интересный вопрос — вообще наличие метода говорит о том, что можно добавлять. Но если прочитать JavaDoc метода (java.util.Collection#add(E e)) и там ясно написано, что поведение метода зависит от реализации:
                                                                          Throws:
                                                                          UnsupportedOperationException — if the add operation is not supported by this collection

                                                                          Т.к. с одной стороны метод конечно есть, но в JavaDoc чётко написано — что он может не работать. Так что нужно читать JavaDoc.
                                                                  +2
                                                                  Например, возвращать пустые массивы или коллекции лучше методами, чем null

                                                                  вы точно это хотели сказать?

                                                                  Это исключение возникает, когда коллекция модифицируется во время итерирования любыми методами, за исключением средств самого итератора.

                                                                  не только во время итерирования
                                                                    0
                                                                    String oneMillionHello = "";
                                                                    for (int i = 0; i < 1000000; i++) {
                                                                        oneMillionHello = oneMillionHello + "Hello!";
                                                                    }
                                                                    System.out.println(oneMillionHello.substring(0, 6));
                                                                    


                                                                    Был глубоко убеждён, что компилятор умеет такое оптимизировать и уже достаточно давно
                                                                      –6
                                                                      Ошибка №0: выбор Java за иконку кофе в логотипе =)

                                                                      Only users with full accounts can post comments. Log in, please.