• ,

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

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


Входной точкой любой Java программы должен быть статичный метод main:
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. Иногда студентам приходится вызывать
методы суперкласса, но при этом часто они забывают использовать
возвращаемое значение. Особенно часто это случается у тех студентов,
которые ещ не осмыслили методы и их возвращаемые значения.

В примере ниже студент хочет вставить результат метода 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™
Ссылка на первоисточник: http://www.sql.ru/faq/faq_topic.aspx?fid=566

3 комментария

Bushrut
Спасибо, весьма познавательно.
snuk
  • snuk
  • 0
Божечки, ну зачем было добавлять конструктор, переменную класса и заниматься инициализацией через все это добро, когда можно было просто создать экземпляр и через него вызвать метод? (Исправленный пример при вызове нестатического метода в 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);
        }
    }
}
Trofa
насчет примера из №14 — вам не удастся вызвать super() в методе поскольку это вызов конструктора родителя. обращение к методам родительского класса, в данном случае производится при помощи ссылки на родителя — super.toString();
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.