JavaRush /Java блог /Random UA /Помилки початківців java-програмістів. Частина 2
articles
15 рівень

Помилки початківців java-програмістів. Частина 2

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