JavaRush /Java блог /Архив info.javarush /Ошибки начинающих java-программистов. Часть 2
articles
15 уровень

Ошибки начинающих java-программистов. Часть 2

Статья из группы Архив info.javarush
Ошибки начинающих java-программистов. Часть 1

9. Вызов нестатичных методов класса из метода main()

Входной точкой любой Java программы должен быть статичный метод main:
Ошибки начинающих java-программистов. Часть 2 - 1

public static void main(String[] args) {
  ...
}
Так как этот метод статичный, нельзя из него вызывать нестатичные методы класса. Об этом часто забывают студенты и пытаются вызывать методы, не создавая экземпляр класса. Эту ошибку обычно допускают в самом начале обучения, когда студенты пишут маленькие программы. Ошибочный пример:

public class DivTest {
    boolean divisible(int x, int y) {
        return (x % y == 0);
    }

    public static void main(String[] args) {
        int v1 = 14;
        int v2 = 9;

        // на следующие строки компилятор выдаст ошибку
        if (divisible(v1, v2)) {
            System.out.println(v1 + " is a multiple of " + v2);
        } else {
            System.out.println(v2 + " does not divide " + v1);
        }
    }
}
Есть 2 способа исправления ошибки: сделать нужный метод статичным или создать экземпляр класса. Чтобы правильно выбрать нужный способ, задайте себе вопрос: использует ли метод поля или другие методы класса. Если да, то следует создать экземпляр класса и вызвать у него метод, иначе следует сделать метод статичным. Исправленный пример 1:

public class DivTest {
    int modulus;

    public DivTest(int m) { 
      modulus = m; 
    }
    
    boolean divisible(int x) {
        return (x % modulus == 0);
    }

    public static void main(String[] args) {
        int v1 = 14;
        int v2 = 9;

        DivTest tester = new DivTest(v2);

        if (tester.divisible(v1) {
            System.out.println(v1 + " is a multiple of " + v2);
        } else {
            System.out.println(v2 + " does not divide " + v1);
        }
    }
}
Исправленный пример 2:

public class DivTest {
    static boolean divisible(int x, int y) {
        return (x % y == 0);
    }

    public static void main(String[] args) {
        int v1 = 14;
        int v2 = 9;

        if (divisible(v1, v2)) {
            System.out.println(v1 + " is a multiple of " + v2);
        } else {
            System.out.println(v2 + " does not divide " + v1);
        }
    }
}

10. Использование объектов класса String как параметров метода.

В Java класс java.lang.String хранит строковые данные. Однако, строки в Java
  1. обладают постоянством (то есть их нельзя изменять),
  2. являются объектами.
Поэтому с ними нельзя обращаться как просто с буфером символов, это неизменяемые объекты. Иногда студенты передают строки, ошибочно расчитывая на то, что строка-объект будет передана как массив символов по ссылке (как в C или C++). Компилятор обычно не считает это ошибкой. Ошибочный пример.

public static void main(String args[]) {
   String test1 = "Today is ";
   appendTodaysDate(test1);
   System.out.println(test1);
}

/* прим. редактора: закомментированный метод должен иметь модификатор
    static (здесь автором допущена ошибка №9)
public void appendTodaysDate(String line) {
    line = line + (new Date()).toString();
}
*/

public static void appendTodaysDate(String line) {
    line = line + (new Date()).toString();
}
В примере выше студент хочет изменить значение локальной переменной test1, присваивая новое значение параметру line в методе appendTodaysDate. Естественно это не сработает. Значение line изменится, но значение test1 останется прежним. Эта ошибка возникает из-за непонимания того, что (1) java объекты всегда передаются по ссылке и (2) строки в Java неизменяемы. Нужно осмыслить, что объекты-строки никогда не изменяют своего значения, а все операции над строками создают новый объект. Чтобы исправить ошибку в примере выше, нужно или возвращать строку из метода, или передавать объект StringBuffer как параметр методу вместо String. Исправленный пример 1:

public static void main(String args[]) {
   String test1 = "Today is ";
   test1 = appendTodaysDate(test1);
   System.out.println(test1);
}

public static String appendTodaysDate(String line) {
    return (line + (new Date()).toString());
}
Исправленный пример 2:

public static void main(String args[]) {
   StringBuffer test1 = new StringBuffer("Today is ");
   appendTodaysDate(test1);
   System.out.println(test1.toString());
}

public static void appendTodaysDate(StringBuffer line) {
    line.append((new Date()).toString());
}

прим. перев.
вообще-то понять в чем ошибка не так просто. Так как объекты передаются по ссылке, то значит line ссылается туда же, куда и test1. А значит создавая новый line, мы создаем новый test1.В неправильном примере все выглядит так, как будто передача String идет по значению, а не по ссылке.

11. Объявление конструктора как метода

Конструкторы объектов в Java внешне похожы на обычные методы. Единственные отличия - у конструктора не указывается тип возвращаемого значения и название совпадает с именем класса. К несчастью, Java допускает задание имени метода, совпадающего с названием класса. В примере ниже, студент хочет проинициализировать поле класса Vector list при создании класса. Этого не произойдет, так как метод 'IntList' - это не конструктор. Ошибочный пример.

public class IntList {
    Vector list;

    // Выглядит как конструктор, но на самом деле это метод
    public void IntList() {
        list = new Vector();
    }

    public append(int n) {
        list.addElement(new Integer(n));
    }
}
Код выдаст исключение NullPointerException при первом же ображении к полю list. Ошибку легко исправить: нужно просто убрать возвращаемое значение из заголовка метода. Исправленный пример:

public class IntList {
    Vector list;

    // Это конструктор
    public IntList() {
        list = new Vector();
    }

    public append(int n) {
        list.addElement(new Integer(n));
    }
}

12. Забыл привести объект к нужному типу

Как и во всех других объектно-ориентированных языках, в Java можно обращаться к объекту как к его суперклассу. Это называется 'upcasting', он выполняется в Java автоматически. Однако, если переменная, поле класса или возвращаемое значение метода объявлено как суперкласс, поля и методы подкласса будут невидимы. Обращение к суперклассу как к подклассу называется 'downcasting', его нужно прописывать самостоятельно (то есть привести объект к нужному подклассу). Студенты часто забывают о приведении оъекта к подклассу. Чаще всего это случается при использовании массивов объектов Object и коллекций из пакета java.util (имеется ввиду Collection Framework). В примере ниже объект String заносится в массив, а затем извлекается из массива для сравнения с другой строкой. Компилятор обнаружит ошибку и не станет компилировать код, пока не будет явно указано приведение типов. Ошибочный пример.

Object arr[] = new Object[10];
arr[0] = "m"; 
arr[1] = new Character('m');

String arg = args[0];
if (arr[0].compareTo(arg) < 0) {
    System.out.println(arg + " comes before " + arr[0]);
}
Смысл приведения типов для некоторых оказывается затруднительным. Особенно часто затруднения вызывают динамические методы. В примере выше, если бы использовался метод equals вместо compareTo, компилятор бы не выдал ошибку, и код бы правильно отработал, так как вызвался бы метод equals именно класса String. Нужно понять, что динамическое связывание отличается от downcasting. Исправленный пример:

Object arr[] = new Object[10];
arr[0] = "m"; 
arr[1] = new Character('m');

String arg = args[0];
if ( ((String) arr[0]).compareTo(arg) < 0) {
    System.out.println(arg + " comes before " + arr[0]);
}

13. Использование интерфейсов.

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

public class SharkSim extends Runnable {
    float length;
    ...
}
Исправленный пример:

public class SharkSim implements Runnable {
    float length;
    ...
}
Связанная с этим ошибка: неправильный порядок блоков extends и implements. Согласно спецификации Java, объявление о расширении класса должно идти перед объявлениями о реализации интерфейсов. Также, для интерфейсов ключевое слово implements нужно писать всего 1 раз, несколько интерфейсов разделяются запятыми. Еще ряд ошибочных примеров:

// Неправильный порядок
public class SharkSim implements Swimmer extends Animal {
    float length;
    ...
}

// ключевое слово implements встречается несколько раз
public class DiverSim implements Swimmer implements Runnable {
    int airLeft;
    ...
}
Исправленные примеры:

// Правильный порядок
public class SharkSim extends Animal implements Swimmer {
    float length;
    ...
}

// Несколько интерфейсов разделяются запятыми
public class DiverSim implements Swimmer, Runnable {
    int airLeft;
    ...
}

14. Забыл использовать значение, возвращаемое методом суперкласса

Java позволяет вызывать из подкласса аналогичный метод суперкласса с помощью ключевого слова keyword. Иногда студентам приходится вызывать методы суперкласса, но при этом часто они забывают использовать возвращаемое значение. Особенно часто это случается у тех студентов, которые ещe не осмыслили методы и их возвращаемые значения. В примере ниже студент хочет вставить результат метода toString() суперкласса в результат метода toString() подкласса. При этом он не использует возвращаемое значение метода суперкласса. Ошибочный пример:

public class GraphicalRectangle extends Rectangle {
      Color fillColor;
      boolean beveled;
      ...
      public String toString() {
          super();
          return("color=" + fillColor + ", beveled=" + beveled);
      }
}
Для исправления ошибки обычно достаточно присвоить возвращаемое значение локальной переменной, и затем использовать эту переменную при вычислении результата метода подкласса. Исправленный пример:

public class GraphicalRectangle extends Rectangle {
      Color fillColor;
      boolean beveled;
      ...
      public String toString() {
          String rectStr = super();
          return(rectStr + " - " +
         "color=" + fillColor + ", beveled=" + beveled);
      }
}

15. Забыл добавить AWT компоненты

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

public class TestFrame extends Frame implements ActionListener {
    public Button exit;

    public TestFrame() {
        super("Test Frame");
        exit = new Button("Quit");
    }
}
Чтобы исправить эту ошибку, необходимо просто добавить компоненты к своим родителям. Пример ниже показывает, как это сделать. Необходимо заметить, что часто студент, забывший добавить компонент в окно приложения, также забывает назначить слушателей событий для этого компонента. Исправленный пример:

public class TestFrame extends Frame implements ActionListener {
    public Button exit;

    public TestFrame() {
        super("Test Frame");

        exit = new Button("Quit");

        Panel controlPanel = new Panel();
        controlPanel.add(exit);

        add("Center", controlPanel);

        exit.addActionListener(this);
    }

    public void actionPerformed(ActionEvent e) {
        System.exit(0);
    }
}

17. Забыл запустить поток

Многопоточность в Java реализуется с помощью класса java.lang.Thread. Жизненный цикл потока состоит из 4х этапов: проинициализирован, запущен, заблокирован и остановлен. ТОлько что созданный поток находится в проинициализированном состоянии. Чтобы перевести его в запущенное состояние, необходимо вызвать его метод start(). Иногда студенты создают потоки, но забывают запустить их. Обычно ошибка возникает при недостаточных знаниях студента о параллельном программировании и многопоточности. (прим. перев.: не вижу связи) Чтобы исправить ошибку, необходимо просто запустить поток. В примере ниже, студент хочет создать анимацию картинки используя интерфейс Runnable, но он забыл запустить поток. Ошибочный пример

public class AnimCanvas extends Canvas implements Runnable {
        protected Thread myThread;
        public AnimCanvas() {
                myThread = new Thread(this);
        }

        // метод run() не будет вызван,
        // потому что поток не запущен.
        public void run() {
                for(int n = 0; n < 10000; n++) {
                   try { 
                     Thread.sleep(100); 
                   } catch (InterruptedException e) { }
                   
                   animateStep(n);
                }       
        }
        ...
}
Исправленный пример:

public class AnimCanvas extends Canvas implements Runnable {
        static final int LIMIT = 10000;
        protected Thread myThread;

        public AnimCanvas() {
                myThread = new Thread(this);
                myThread.start();
        }

        public void run() {
                for(int n = 0; n < LIMIT; n++) {
                        try { 
                          Thread.sleep(100); 
                        } catch (InterruptedException e) { }

                        animateStep(n);
                }
        }
        ...
}
Жизненный цикл потока и связь потоков и классов, реализующих интерфейс Runnable — это очень важная часть программирования на Java, и не будет лишним заострить свое внимание на этом.

18. Использование запрещенного метода readLine() класса java.io.DataInputStream

В Java версии 1.0 для считывания строки текста необходимо было использовать метод readLine() класса java.io.DataInputStream. В Java версии 1.1 был добавлен целый набор классов для ввода-вывода, обеспечивающий операции ввода-вывода для текста: классы Reader и Writer. Таким образом с версии 1.1 для чтения строки текста надо использовать метод readLine() класса java.io.BufferedReader. Студенты могут не знать об этом изменении, особенно если они обучались по старым книгам. (прим. перев. вообще-то уже не актуально. вряд ли кто-то станет сейчас учиться по книгам 10-летней давности). Старый метод readLine() оставлен в JDK, но объявлен как запрещенный, что часто смущает студентов. Необходимо понять, что использование метода readLine() класса java.io.DataInputStream не является неправильным, оно просто устарело. Необходимо использовать класс BufferedReader. Ошибочный пример:

public class LineReader {
    private DataInputStream dis;

    public LineReader(InputStream is) {
        dis = new DataInputStream(is);
    }

    public String getLine() { 
        String ret = null;

        try {
          ret = dis.readLine();  // Неправильно! Запрещено.
        } catch (IOException ie) { }

        return ret;
    }
}
Исправленный пример:

public class LineReader {
    private BufferedReader br;

    public LineReader(InputStream is) {
        br = new BufferedReader(new InputStreamReader(is));
    }

    public String getLine() { 
        String ret = null;

        try {
          ret = br.readLine(); 
        } catch (IOException ie) { }

        return ret;
    }
}
Есть и другие запрещенные методы в версиях, более поздних чем 1.0, но этот встречается чаще всего.

19. Использование типа double как float

Как и в большинстве других языков, в Java поддерживаются операции над числами с плавающей точкой (дробными числами). В Java есть 2 типа-примитива для чисел с плавающей точкой: double для чисел с 64-битной точностью по стандарту IEEE, и float, для чисел с 32-битной точностью по стандарту IEEE. Трудность заключается в использовании десятичных чисел, таких как 1.75, 12.9e17 или -0.00003 — компилятор присваивает им тип double. Java не производит приведение типов в операциях, в которых может произойти потеря точности. Такое приведение типов должен осуществлять программист. Например, Java не позволит присвоить значение типа int переменной типа byte без приведения типов, как показано в примере ниже.

byte byteValue1 = 17; /* неправильно! */
byte byteValue2 = (byte)19; /* правильно */
Так как дробные числа представлены типом double, и присваивание double переменной типа float может привести к потере точности, компилятор пожалуется на любую попытку использовать дробные числа как float. Так что использование присваиваний, приведенных ниже, не даст классу откомпилироваться.

float realValue1 = -1.7;          /* неправильно! */
float realValue2 = (float)(-1.9); /* правильно */
Это присваивание сработало бы в C или C++, для Java все гораздо строже. Есть 3 способа избавиться от этой ошибки. Можно использовать тип double вместо типа float. Это наиболее простое решение. На самом деле нет особого смысла использовать 32-битную арифметику вместо 64-битной, разницу в скорости все равно скушает JVM (к тому же в современных процессорах все дробные числа приводятся к формату 80-битного регистра процессора перед любой операцией). Единственный плюс использования float — это то, что они занимают меньше памяти, что бывает полезно при работе с большим числом дробных переменых. Можно использовать модификатор для обозначения типа числа, чтобы сообщить компилятору как хранить число. Модификатор для типа float - 'f'. Таким образом, компилятор присвоит числу 1.75 тип double, а 1.75f - float. Например:

float realValue1 = 1.7;    /* неправильно! */
float realValue2 = 1.9f;   /* правильно */
Можно использовать явное приведение типов. Это наименее элегантный способ, но он полезен при конвертации переменной типа double в тип float. Пример:

float realValue1 = 1.7f; 
double realValue2 = 1.9;
realValue1 = (float)realValue2;
Подробнее о числах с плавающей точкой можно почитать здесь и здесь.

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

Автор: А.Грасоff™ Ссылка на первоисточник: Ошибки начинающих java-программистов
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
snuk Уровень 15
30 сентября 2017
Божечки, ну зачем было добавлять конструктор, переменную класса и заниматься инициализацией через все это добро, когда можно было просто создать экземпляр и через него вызвать метод? (Исправленный пример при вызове нестатического метода в main)

public class DivTest {
    int modulus;

    public DivTest(int m) { 
      modulus = m; 
    }
    
    boolean divisible(int x) {
        return (x % modulus == 0);
    }

    public static void main(String[] args) {
        int v1 = 14;
        int v2 = 9;

        DivTest tester = new DivTest(v2);

        if (tester.divisible(v1) {
            System.out.println(v1 + " is a multiple of " + v2);
        } else {
            System.out.println(v2 + " does not divide " + v1);
        }
    }
}