Помилки початківців java-програмістів. Частина 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);
}
}
}
Є два способи виправлення помилки: зробити потрібний метод статичним або створити екземпляр класу. Щоб правильно вибрати потрібний спосіб, запитайте себе: чи використовує метод поля або інші методи класу. Якщо так, то слід створити екземпляр класу та викликати у нього метод, інакше слід зробити метод статичним. Виправлений приклад 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. Іноді студентам доводиться викликати методи суперкласу, але при цьому часто вони забувають використовувати значення, що повертається. Особливо часто це трапляється у тих студентів, які ще не осмислабо методи та їх значення, що повертаються. У прикладі нижче студент хоче вставити результат методу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;
Докладніше про числа з плаваючою точкою можна почитати тут і тут.
-- коментар перекладача -- |
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ