articles
15 уровень

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

Пост из группы Архив info.javarush.ru
3276 участников

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
Комментарии (4)
  • популярные
  • новые
  • старые
Для того, что бы оставить комментарий вы должны авторизироваться
Alesha 12 уровень, Москва
15 июня, 11:24
А нельзя было примеры разделять Enter'ом, а не сплошным куском вставлять?
Trofa 32 уровень, Одесса
11 октября 2017, 03:03
насчет примера из №14 — вам не удастся вызвать super() в методе поскольку это вызов конструктора родителя. обращение к методам родительского класса, в данном случае производится при помощи ссылки на родителя — super.toString();
snuk 15 уровень, Москва
30 сентября 2017, 19:54
Божечки, ну зачем было добавлять конструктор, переменную класса и заниматься инициализацией через все это добро, когда можно было просто создать экземпляр и через него вызвать метод? (Исправленный пример при вызове нестатического метода в 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);
        }
    }
}
Bushrut 21 уровень
14 апреля 2016, 21:58
Спасибо, весьма познавательно.