Поиск…


Вступление

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

замечания

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

Pitfall - игнорирование видимости метода

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

Вы должны обратить внимание на то, какие методы вы публикуете. Публичные методы в приложении - это видимый API приложения. Это должно быть как можно меньше и компактнее, особенно если вы пишете библиотеку многократного использования (см. Также принцип SOLID ). Важно также учитывать наглядность всех методов и использовать только защищенный или пакетный доступ, если это необходимо.

Когда вы объявляете методы, которые должны быть закрытыми как общедоступные, вы раскрываете внутренние детали реализации этого класса.

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

Pitfall - Отсутствие «перерыва» в случае «переключения»

Эти проблемы с Java могут быть очень смущающими и иногда оставаться неоткрытыми до тех пор, пока они не будут запущены в производство. Эффективное поведение в операторах switch часто полезно; однако отсутствие ключевого слова «break», когда такое поведение нежелательно, может привести к катастрофическим результатам. Если вы забыли поставить «break» в «case 0» в приведенном ниже примере кода, программа напишет «Zero», а затем «One», так как поток управления внутри здесь будет проходить весь оператор «switch» до тех пор, пока он достигает «перерыва». Например:

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");
        }
}

В большинстве случаев чище решение было бы использовать интерфейсы и перемещать код с определенным поведением в отдельные реализации ( состав над наследованием )

Если оператор switch неизбежен, рекомендуется документировать «ожидаемые» всплытия, если они происходят. Таким образом, вы показываете коллегам-разработчикам, что знаете о недостающем разрыве и что это ожидаемое поведение.

switch(caseIndex) {
    [...]
    case 2:
        System.out.println("Two");
        // fallthrough
    default:
        System.out.println("Default");

Pitfall - Пункты с запятой и отсутствующие фигурные скобки

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

if (feeling == HAPPY)
    System.out.println("Smile");
else
    System.out.println("Frown");

они случайно пишут это:

if (feeling == HAPPY);
    System.out.println("Smile");
else
    System.out.println("Frown");

и недоумевают, когда компилятор Java сообщает им, что else неуместно. Компилятор Java с интерпретированием выше:

if (feeling == HAPPY)
    /*empty statement*/ ;
System.out.println("Smile");   // This is unconditional
else                           // This is misplaced.  A statement cannot
                               // start with 'else'
System.out.println("Frown");

В других случаях ошибки компиляции не будут, но код не будет делать то, что намеревается программист. Например:

for (int i = 0; i < 5; i++);
    System.out.println("Hello");

только печатает «Привет» один раз. Опять же паразитная точка с запятой означает, что тело цикла for представляет собой пустой оператор. Это означает, что следующий вызов println является безусловным.

Другой вариант:

for (int i = 0; i < 5; i++);
    System.out.println("The number is " + i);

Это даст ошибку «Не могу найти символ» для i . Наличие ложной точки с запятой означает, что вызов println пытается использовать i вне его области видимости.

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

  1. Точка с запятой в Java не является «синтаксическим шумом». Наличие или отсутствие точки с запятой может изменить смысл вашей программы. Не добавляйте их в конце каждой строки.

  2. Не доверяйте отступу вашего кода. В языке Java лишние пробелы в начале строки игнорируются компилятором.

  3. Используйте автоматический индентор. Все IDE и многие простые текстовые редакторы понимают, как правильно отступать код Java.

  4. Это самый важный урок. Следуйте последним рекомендациям стиля Java и поместите фигурные скобки вокруг операторов «then» и «else» и оператора body цикла. Открытая скобка ( { ) не должна быть на новой строке.

Если программист следовал правилам стиля, то пример if с неуместными точками с запятой выглядел бы так:

if (feeling == HAPPY); {
    System.out.println("Smile");
} else {
    System.out.println("Frown");
}

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

if (feeling == HAPPY); {
                           System.out.println("Smile");
                       } else {
                           System.out.println("Frown");
                       }

который должен выделиться как неудачный даже для новичков.

Pitfall - Оставляя брекеты: проблемы с «болтающимися, если» и «болтающимися»

В последней версии руководства по стилю Java Java указано, что операторы «then» и «else» в операторе if всегда должны быть заключены в «фигурные скобки» или «фигурные скобки». Аналогичные правила применяются к телам различных операторов цикла.

if (a) {           // <- open brace
    doSomething();
    doSomeMore();
}                  // <- close brace

Синтаксис языка Java на самом деле не требуется. В самом деле, если «то» часть оператора if является единственным выражением, то законно оставить фигурные скобки

if (a)
    doSomething();

или даже

if (a) doSomething();

Однако есть опасность игнорировать правила стиля Java и оставлять фигурные скобки. В частности, вы значительно увеличиваете риск того, что код с ошибочным отступом будет неверно истолкован.

Проблема «болтаться»:

Рассмотрим пример кода сверху, переписанный без брекетов.

if (a)
   doSomething();
   doSomeMore();

Этот код, кажется, говорит, что вызовы doSomething и doSomeMore будут возникать тогда и только тогда, когда a true . Фактически, код имеет неправильный отступ. Спецификация языка Java, что doSomeMore() представляет собой отдельный оператор, следующий за оператором if . Правильный отступ выглядит следующим образом:

if (a)
   doSomething();
doSomeMore();

Проблема «болтаться еще»

Вторая проблема возникает, когда мы добавляем else к миксу. Рассмотрим следующий пример с отсутствующими фигурными скобками.

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
else
   doZ();

Вышеприведенный код говорит, что doZ будет вызываться, когда a является false . Фактически, отступ неверен еще раз. Правильный отступ для кода:

if (a)
   if (b)
      doX();
   else if (c)
      doY(); 
   else
      doZ();

Если код был написан в соответствии с правилами стиля Java, это выглядело бы так:

if (a) {
   if (b) {
      doX();
   } else if (c) {
      doY(); 
   } else {
      doZ();
   }
}

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

if (a) {                         if (a) {
   if (b) {                          if (b) {
      doX();                            doX();
   } else if (c) {                   } else if (c) {
      doY();                            doY();
} else {                         } else {
   doZ();                            doZ();
}                                    }
}                                }

Но в обоих случаях ошибочный код «выглядит неправильно» для глаз опытного Java-программиста.

Pitfall - перегрузка вместо переопределения

Рассмотрим следующий пример:

public final class Person {
    private final String firstName;
    private final String lastName;
   
    public Person(String firstName, String lastName) {
        this.firstName = (firstName == null) ? "" : firstName;
        this.lastName = (lastName == null) ? "" : lastName;
    }

    public boolean equals(String other) {
        if (!(other instanceof Person)) {
            return false;
        }
        Person p = (Person) other;
        return firstName.equals(p.firstName) &&
                lastName.equals(p.lastName);
    }

    public int hashcode() {
        return firstName.hashCode() + 31 * lastName.hashCode();
    }
}

Этот код не будет вести себя так, как ожидалось. Проблема в том, что методы equals и hashcode для Person не переопределяют стандартные методы, определенные Object .

  • Метод equals имеет неправильную подпись. Он должен быть объявлен как equals(Object) не equals(String) .
  • Метод hashcode имеет неправильное имя. Это должен быть hashCode() (обратите внимание на капитал C ).

Эти ошибки означают, что мы объявили случайные перегрузки, и они не будут использоваться, если Person используется в полиморфном контексте.

Однако есть простой способ справиться с этим (начиная с Java 5). Используйте аннотацию @Override всякий раз, когда вы планируете переопределить ваш метод:

Java SE 5
public final class Person {
    ...

    @Override
    public boolean equals(String other) {
        ....
    }

    @Override
    public hashcode() {
        ....
    }
}

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

Pitfall - Октальные литералы

Рассмотрим следующий фрагмент кода:

// Print the sum of the numbers 1 to 10
int count = 0;
for (int i = 1; i < 010; i++) {    // Mistake here ....
    count = count + i;
}
System.out.println("The sum of 1 to 10 is " + count);

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

Причина в том, что целочисленный литерал, начинающийся с нуля ('0'), интерпретируется компилятором Java как восьмеричный литерал, а не десятичный литерал, как вы могли ожидать. Таким образом, 010 - это восьмеричное число 10, которое равно 8 десятичным.

Pitfall - объявление классов с теми же именами, что и стандартные классы

Иногда программисты, которые не знакомы с Java, ошибаются в определении класса с именем, которое совпадает с широко используемым классом. Например:

package com.example;

/**
 * My string utilities
 */
public class String {
    ....
}

Затем они задаются вопросом, почему возникают неожиданные ошибки. Например:

package com.example;

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

Если вы скомпилируете и затем попытаетесь запустить указанные выше классы, вы получите сообщение об ошибке:

$ javac com/example/*.java
$ java com.example.Test
Error: Main method not found in class test.Test, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Кто-то, смотрящий на код для класса Test , увидит объявление main и посмотрит на его подпись и задается вопросом, о чем жалуется команда java . Но на самом деле, команда java говорит правду.

Когда мы объявляем версию String в том же пакете, что и Test , эта версия имеет приоритет перед автоматическим импортом java.lang.String . Таким образом, подпись метода Test.main самом деле

void main(com.example.String[] args) 

вместо

void main(java.lang.String[] args)

и команда java не будет распознавать это как метод точки входа.

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

Pitfall - использование '==' для проверки логического

Иногда новый программист Java будет писать такой код:

public void check(boolean ok) {
    if (ok == true) {           // Note 'ok == true'
        System.out.println("It is OK");
    }
}

Опытный программист заметил бы это как неуклюжий и хотел бы переписать его как:

public void check(boolean ok) {
    if (ok) {
       System.out.println("It is OK");
    }
}

Тем не менее, с ошибкой ok == true чем просто неуклюжесть. Рассмотрим этот вариант:

public void check(boolean ok) {
    if (ok = true) {           // Oooops!
        System.out.println("It is OK");
    }
}

Здесь программист ошибся == as = ... и теперь код имеет тонкую ошибку. Выражение x = true безоговорочно присваивает true x а затем оценивается как true . Другими словами, теперь метод check будет печатать «Все в порядке» независимо от параметра.

Урок здесь состоит в том, чтобы избавиться от привычки использовать == false и == true . В дополнение к тому, чтобы быть многословным, они делают ваше кодирование более склонным к ошибкам.


Примечание. Возможная альтернатива ok == true которая позволяет избежать ошибок, заключается в использовании условий Yoda ; т.е. поставить литерал в левой части реляционного оператора, как в true == ok . Это работает, но большинство программистов, вероятно, согласятся с тем, что условия Yoda выглядят странно. Конечно, ok (или !ok ) является более кратким и более естественным.

Pitfall - импорт подстановок может сделать ваш код хрупким

Рассмотрим следующий частичный пример:

import com.example.somelib.*;
import com.acme.otherlib.*;

public class Test {
    private Context x = new Context();   // from com.example.somelib
    ...
}

Предположим, что когда вы впервые разработали код против версии 1.0 somelib и версии 1.0 otherlib . Затем в какой-то более поздний момент вам нужно обновить свои зависимости до более поздних версий, и вы решите использовать версию otherlib версии 2.0. Также предположим, что одно из изменений, которые они сделали для otherlib между 1.0 и 2.0, заключалось в том, чтобы добавить класс Context .

Теперь, когда вы перекомпилируете Test , вы получите ошибку компиляции, сообщающую вам, что Context является неоднозначным импортом.

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

Проблема здесь в подстановочных импортах. С одной стороны, использование подстановочных знаков может сделать ваши классы на несколько строк короче. С другой стороны:

  • Совместимые с обновлением изменения в других частях вашей кодовой базы, стандартных библиотек Java или сторонних библиотек могут привести к ошибкам компиляции.

  • Читаемость страдает. Если вы не используете IDE, выяснить, какой из подстановочных импортов тянет в названный класс, может быть сложно.

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

Pitfall: использование 'assert' для проверки аргумента или пользователя

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

Простой ответ заключается в том, что он не подходит.

Лучшие альтернативы включают:

  • Выбрасывание исключения IllegalArgumentException с использованием настраиваемого кода.
  • Использование методов Preconditions доступных в библиотеке Google Guava.
  • Используя Validate методы , доступные в библиотеке Apache Commons Lang3.

Это то, что предлагает Java Language Specification (JLS 14.10, для Java 8) по этому вопросу:

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

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

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

Вторая проблема с использованием утверждений для проверки аргументов заключается в том, что ошибочные аргументы должны приводить к соответствующему исключению во время выполнения (например, IllegalArgumentException , ArrayIndexOutOfBoundsException или NullPointerException ). Ошибка утверждения не приведет к соответствующему исключению. Опять же, не запрещено использовать утверждения для проверки аргументов в публичных методах, но это, как правило, неуместно. Предполагается, что AssertionError никогда не будет поймано, но это можно сделать, поэтому правила для операторов try должны обрабатывать утверждения, появляющиеся в блоке try так же, как и текущее обращение с инструкциями throw.

Pitfall авто-распаковки нулевых объектов в примитивы

public class Foobar {
    public static void main(String[] args) {

        // example: 
        Boolean ignore = null;
        if (ignore == false) {
            System.out.println("Do not ignore!");
        }
    }
}

Ловушка здесь заключается в том, что null сравнивается с false . Поскольку мы сравниваем примитивное boolean с Boolean , Java пытается распаковать Boolean Object в примитивный эквивалент, готовый для сравнения. Однако, поскольку это значение равно null , NullPointerException .

Java неспособна сравнивать примитивные типы с null значениями, что вызывает NullPointerException во время выполнения. Рассмотрим примитивный случай условия false == null ; это создало бы ошибку времени компиляции incomparable types: int and <null> .



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow