Ошибки начинающих java-программистов. Часть 1
![Ошибки начинающих java-программистов. Часть 2 - 1]()
Автор: А.Грасоff™
Ссылка на первоисточник: Ошибки начинающих java-программистов
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
- обладают постоянством (то есть их нельзя изменять),
- являются объектами.
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());
}
прим. перев. |
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;
Подробнее о числах с плавающей точкой можно почитать здесь и здесь.
-- комментарий переводчика -- |
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ