Статьи

Каковы Плохие Особенности Java?

обзор

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

Это может быть подкреплено реальным опытом, когда устранение использования функции улучшило код. Иногда это происходит из-за того, что разработчики не знали, как правильно использовать функцию, или эта функция по своей природе подвержена ошибкам (в зависимости от того, нравится вам это или нет).

Смущает, когда меняется мода или ваша команда, и эта функция становится прекрасной или даже предпочтительная методология.

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

Проверенные исключения

I am often surprised at the degree that developers don’t like to think about error handling.  New developers don’t even like to read error messages. It’s hard work, and they complain the application crashed, «it’s not working». They have no idea why the exception was thrown when often the error message and stack dump tell them exactly what went wrong if they could only see the clues. When I write out stack traces for tracing purposes, many just see the log shaped like a crash when there was no error.   Reading error messages is a skill and at first it can be overwhelming.

Similarly, handling exceptions in a useful manner is too often avoided.  I have no idea what to do with this exception, I would rather either log the exception and pretend it didn’t happen or just blow up and let the operations people or to the GUI user, who have the least ability to deal the error.

Many experienced developers hate checked exceptions as a result.  However, the more I hear this, the more I am glad Java has checked exception as I am convinced they really will find it too easy ignore the exceptions and just let the application die if they are not annoyed by them.

Checked exceptions can be overused of course.  The question should be when throwing a checked exception; do I want to annoy the developer calling the code by forcing them to think a little bit about error handling? If the answer is yes, throw a checked exception.

IMHO, it is a failing of the lambda design that it doesn’t handle checked exception transparently. i.e. as a natural block of code would by throwing out any unhandled exception as it does for unchecked exceptions and errors. However, given the history of lambdas and functional programming, where they don’t like side effect at all, let alone short cut error handling, it is not surprising.

Вы можете обойти ограничение лямбд, перебросив проверенное исключение, как если бы оно было непроверенным. Это работает, потому что JVM не имеет понятия о проверенных исключениях, это проверка времени компиляции, как обобщенные. Мой предпочтительный метод — использовать Unsafe.rethrowException, но есть 3 других способа сделать это. Thread.currentThread (). Stop (e) больше не работает в Java 8, несмотря на то, что это всегда было безопасно.

Был ли Thread.currentThread (). Stop (e) небезопасным?


Метод Thread.stop (Throwable) был небезопасным, когда он мог заставить 
другой  поток вызывать исключение в случайной части кода. Это может быть проверенное исключение в части кода, которая этого не ожидала, или исключение, которое перехватывается в некоторых частях потока, но не в других, оставляя вас без понятия, что он будет делать.  

Однако главная причина, по которой он был небезопасен, заключается в том, что он мог оставить атомарные операции синхронизированными в заблокированном разделе кода в несогласованном состоянии, повреждая память тонкими и непроверяемыми способами.

Чтобы добавить к путанице, трассировка стека Throwable не совпадает с трассировкой стека потока, где фактически было сгенерировано исключение.

Но как насчет Thread.currentThread (). Stop (e)?
Это запускает текущий поток, чтобы выдать исключение в текущей строке. Это не хуже, чем просто с помощью исключения throw, вы выполняете операцию, которую не может проверить компилятор. Проблема в том, что компилятор не всегда знает, что вы делаете, и действительно ли он безопасен или нет. Для дженериков это классифицируется как «непроверенное приведение», которое является предупреждением, которое можно отключить с помощью аннотации. Java не поддерживает операции такого же типа с проверенным исключением, и вы в конечном итоге используете хаки или, что еще хуже, скрываете истинное проверенное исключение как исключение времени выполнения, а это означает, что есть небольшая надежда, что вызывающая сторона обработает его правильно.

Использование статики плохо?


Это новое «правило» для меня.
Я понимаю, откуда оно исходит, но есть больше исключений из этого правила, чем где оно должно применяться. Давайте сначала рассмотрим все контексты, в которых может использоваться перегруженное значение статики.
  1. статические изменяемые поля
  2. статическое неизменяемое поле (конечные примитивные или конечные поля, указывающие на объекты, которые не были изменены)
  3. статические методы.
  4. статические классы (которые не имеют неявной ссылки на внешний экземпляр)
  5. статический инициализатор блоков.

Я бы согласился с тем, что использование статических изменяемых полей может быть либо ошибкой новичка, либо чем-то, чего следует избегать, если это вообще возможно.
Если вы видите, что статические поля изменяются в конструкторе, это почти наверняка ошибка. (Даже если нет, я бы избегал этого). Я считаю, что это является причиной того, что оператор избегает статических изменений.

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

Короче говоря, статические это хорошо, а изменяемые статические поля являются исключением, а не правилом.

Синглтоны плохие?


Проблемы с синглетонами идут с двух сторон.
Они фактически являются глобально изменяемым состоянием, что затрудняет их обслуживание или инкапсуляцию, например, в модульном тесте, и они поддерживают автоматическое подключение. т.е. любой компонент может получить к нему доступ, что делает ваши зависимости неясными и сложными в управлении. По этим причинам некоторые разработчики ненавидят их.

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

Если вы исключите глобальное состояние и компоненты самостоятельного подключения, у вас останутся Singletons, которые являются неизменяемыми и передаются через внедрение зависимостей, и в этом случае они могут работать действительно элегантно.
Обычный шаблон, который я использую для реализации стратегий, — это использование перечисления с одним экземпляром, который реализует интерфейс.
   
enum MyComparator реализует Comparator {
       INSTANCE;
       public int compare(MyObject o1, MyObject o2) {
           // something a bit too complicated to put in a lambda
       }
    }

Этот экземпляр может быть передан как реализация Comparator через внедрение зависимостей и без изменяемого состояния может безопасно использоваться в потоках и модульных тестах.

Могу ли я получить библиотеку или фреймворк, чтобы сделать это очень просто для меня?


Библиотеки и фреймворки могут сэкономить вам много времени и усилий, заставляя ваш собственный код делать то, что уже работает где-то еще.

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

Тем не менее, я видел (на Stackoverflow) разработчиков так много, чтобы не использовать собственный код даже для тривиальных примеров.
Им кажется, что если они используют библиотеку, это должно быть лучше, чем все, что они написали. Проблема с этим, это предполагает; добавление библиотек не требует больших затрат, у вас действительно хорошее понимание библиотеки, и вам никогда не придется учиться писать код, которому можно доверять.

Некоторые разработчики используют фреймворки, чтобы понять, что на самом деле является методологией.
Часто разработчики используют каркас для внедрения зависимостей, когда на самом деле вы можете просто сделать это в простой Java, но они либо не доверяют себе или своей команде делать это.

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

Использование двойного за деньги плохо?


Использование дробных чисел без учета округления даст вам неожиданные результаты.
С положительной стороны, для двойной, как правило, явно неправильно, как 10.99999999999998 вместо 11.

Некоторые считают, что BigDecimal является решением.
Однако проблема в том, что BigDecimal имеет свои собственные ошибки, гораздо сложнее проверять / читать / записывать, но хуже всего может выглядеть правильно, когда это не так. Возьми этот пример
    
двойной d = 1,0 / 3 * 3 + 0,01;
    BigDecimal bd1 = BigDecimal.valueOf(1.0)
            .divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(3))
            .add(BigDecimal.valueOf(0.01))
            .setScale(2, BigDecimal.ROUND_HALF_UP);
    BigDecimal bd2 = BigDecimal.valueOf(1.0)
            .divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(3)
            .add(BigDecimal.valueOf(0.01)))
            .setScale(2, BigDecimal.ROUND_HALF_UP);
    System.out.println("d: " + d);
    System.out.println("bd1: " + bd1);
    System.out.println("bd2: " + bd2);

Это дает три разных результата.
На вид, какой из них дает правильный результат? Можете ли вы сказать разницу между bd1 и bd2?

Это печатает
d: 1.01
bd1: 1.00
bd2: 0.99

Вы видите по выходным данным, что не так?
На самом деле ответ должен быть 1.01.

Другой недостаток BigDecimal — это то, что равно и сравнивают, чтобы не вести себя одинаково. Равное () может быть ложным, когда CompareTo () возвращает 0. т. е. в BigDecimal 1.0 равняется 1,00 — ложно, поскольку шкалы различаются.

Проблема, с которой я столкнулся в BigDecimal, заключается в том, что вы получаете код, который часто труднее понять и который дает неправильные результаты, которые выглядят так, как будто они могут быть правильными. BigDecimal значительно медленнее и производит много мусора. (Это улучшается в каждой версии Java 8). Существуют ситуации, когда BigDecimal является лучшим решением, но это не дано, как некоторые протестуют.

Если BigDecimal не является отличной альтернативой, есть ли другие? Часто int и long используются с фиксированной точностью, например, целое число центов вместо доли в долларах. Это имеет некоторые проблемы, вы должны помнить, где находится десятичное место. Если Java поддерживает типы значений, возможно, имеет смысл использовать их как обертки для денег и обеспечить вам большую безопасность, но контроль, прояснение и производительность при работе с примитивами целых чисел.

Использование нулевых значений


Для разработчиков, плохо знакомых с Java, получение повторного исключения NullPointerException является трудоемким процессом.
Действительно ли мне нужно создавать новый экземпляр каждого объекта, каждого элемента массива в Java? Другие языки не требуют этого, поскольку это часто делается с помощью встроенных структур данных. (Что-то, что рассматривается для Java)

Даже опытным Java-разработчикам трудно иметь дело с нулевыми значениями, и они считают большой ошибкой иметь нулевое значение в языке.
ИМХО Проблема в том, что замены часто намного хуже. такие как объекты NULL, которые не являются NPE, но, возможно, должны были быть инициализированы чем-то другим. В Java 8, Optional является хорошим дополнением, которое делает обработку не-результата более понятной. Я думаю, что это полезно для тех, кто борется с NullPointerException, так как это заставляет вас думать, что результата может не быть вообще. Это не решает проблему неинициализированных полей.  

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

Общий вопрос:
как я должен был знать, что переменная была нулевой? Это неправильный путь в моей голове. Должно быть, почему предполагается, что оно не может быть нулевым? Если вы не можете ответить на этот вопрос, вы должны предположить, что он может быть нулевым, и NPE не должно вызывать удивления, если вы не проверите это.

Можно утверждать, что Java могла бы использовать более синтаксический сахар для создания кода, который обрабатывает ноль чище, такого как оператор Элвиса, но я думаю, что проблема заключается в том, что разработчики недостаточно думают о нулевых значениях.
Например, вы проверяете, что переменная enum равна нулю, прежде чем включить ее? (Я думаю, что должен быть случай NULL: в переключателе, но нет или провалиться по умолчанию: но это не так)

Насколько важно быстро писать код?


Java не является кратким языком, и без IDE для написания половины кода для вас было бы очень больно писать esp, если бы вы потратили целый день на написание кода.

Но это то, что разработчики делают весь день, не так ли?
На самом деле, они этого не делают. Разработчики не тратят большую часть своего времени на написание кода, они тратят от 90% (для нового кода) до 99% (для унаследованного кода) на 
понимание проблемы .

Вы могли бы сказать;
Я пишу 1000 строк кода весь день и позже и переписываю код (часто делая его короче), и через некоторое время я исправил код. Однако, хотя код все еще свеж в вашем уме, если бы вы писали только Код, который вам нужен в конце (или вы делаете это из распечатки), и вы делите его на общее время, потраченное на проект, от начала до конца, вы, вероятно, обнаружите, что на самом деле это было менее 100 строк кода в день Возможно менее 10 строк в день.

Так что же вы действительно делали за это время, если не писали готовый продукт?
Было понятно, что требуется конечным пользователям и что необходимо для реализации решения.

Кто-то однажды сказал мне;
не имеет значения, насколько быстро, насколько велико, как глубоко или сколько вырыли ям, если вы копаете их не в том месте.

Вывод


Я слышал мнения начинающих разработчиков о том, что вы не должны / я не могу представить, почему вы / должны быть уволены, если вы используете X, вы должны использовать только Y. Я считаю, что такие утверждения редко бывают точными на 100%.
Часто есть или крайние случаи, и иногда очень распространенные случаи, когда такое утверждение вводит в заблуждение или просто неверно.

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