Błędy początkujących programistów Java. Część 1
Autor: A.Grasoff™ Link do źródła: Błędy początkujących programistów Java
9. Wywoływanie metod klas niestatycznych z metody main().
Punktem wejścia dowolnego programu Java powinna być metoda statycznamain
:
public static void main(String[] args) {
...
}
Ponieważ ta metoda jest statyczna, nie można z niej wywoływać metod klas niestatycznych. Studenci często o tym zapominają i próbują wywoływać metody bez tworzenia instancji klasy. Ten błąd popełniany jest zazwyczaj już na samym początku szkolenia, kiedy uczniowie piszą małe programy. Zły przykład:
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);
}
}
}
Istnieją 2 sposoby naprawienia błędu: ustaw żądaną metodę jako statyczną lub utwórz instancję klasy. Aby wybrać właściwą metodę, zadaj sobie pytanie, czy metoda wykorzystuje pole, czy inne metody klasowe. Jeśli tak, powinieneś utworzyć instancję klasy i wywołać na niej metodę, w przeciwnym razie powinieneś ustawić metodę statyczną. Poprawiony przykład 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);
}
}
}
Poprawiony przykład 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. Wykorzystanie obiektów klasy String jako parametrów metody.
W Javie klasajava.lang.String
przechowuje dane w postaci ciągów znaków. Jednak ciągi znaków w Javie
- mają charakter trwały (tzn. nie podlegają zmianie),
- są przedmiotami.
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();
}
W powyższym przykładzie uczeń chce zmienić wartość zmiennej lokalnej, test1
przypisując nową wartość parametrowi line
w metodzie appendTodaysDate
. Naturalnie to nie zadziała. Znaczenie line
się zmieni, ale znaczenie test1
pozostanie takie samo. Ten błąd występuje z powodu nieporozumienia, że (1) obiekty Java są zawsze przekazywane przez referencję i (2) ciągi znaków w Javie są niezmienne. Musisz zrozumieć, że obiekty łańcuchowe nigdy nie zmieniają swojej wartości, a wszystkie operacje na ciągach znaków tworzą nowy obiekt. Aby naprawić błąd z powyższego przykładu, musisz albo zwrócić ciąg znaków z metody, albo przekazać obiekt StringBuffer
jako parametr do metody zamiast String
. Poprawiony przykład 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());
}
Poprawiony przykład 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());
}
około. tłumaczenie |
11. Deklaracja konstruktora jako metody
Konstruktory obiektów w Javie wyglądem przypominają zwykłe metody. Jedyną różnicą jest to, że konstruktor nie określa typu zwracanej wartości, a nazwa jest taka sama jak nazwa klasy. Niestety Java pozwala, aby nazwa metody była taka sama jak nazwa klasy. W poniższym przykładzie uczeń chce zainicjować pole klasyVector list
podczas tworzenia klasy. Tak się nie stanie, ponieważ metoda 'IntList'
nie jest konstruktorem. Zły przykład.
public class IntList {
Vector list;
// Выглядит Jak конструктор, но на самом деле это метод
public void IntList() {
list = new Vector();
}
public append(int n) {
list.addElement(new Integer(n));
}
}
Kod zgłosi wyjątek NullPointerException
przy pierwszym dostępie do pola list
. Błąd można łatwo naprawić: wystarczy usunąć wartość zwracaną z nagłówka metody. Poprawiony przykład:
public class IntList {
Vector list;
// Это конструктор
public IntList() {
list = new Vector();
}
public append(int n) {
list.addElement(new Integer(n));
}
}
12. Zapomniałem rzucić obiekt na wymagany typ
Podobnie jak wszystkie inne języki obiektowe, w Javie obiekt można nazwać jego nadklasą. Nazywa się to'upcasting'
i odbywa się automatycznie w Javie. Jeśli jednak zmienna, pole klasy lub wartość zwracana przez metodę zostanie zadeklarowana jako nadklasa, pola i metody podklasy będą niewidoczne. Odnosząc się do nadklasy jako podklasy 'downcasting'
, należy ją samodzielnie zarejestrować (czyli przenieść obiekt do żądanej podklasy). Studenci często zapominają o podklasie obiektu. Dzieje się tak najczęściej podczas używania tablic obiektów i kolekcji z pakietu java.util
(co oznacza Collection Framework ). Poniższy przykład String
umieszcza obiekt w tablicy, a następnie usuwa go z tablicy, aby porównać go z innym ciągiem znaków. Kompilator wykryje błąd i nie skompiluje kodu, dopóki nie zostanie jawnie określone rzutowanie typu. Zły przykład.
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]);
}
Znaczenie rzutowania typów jest dla niektórych trudne. Szczególnie często trudności sprawiają metody dynamiczne. equals
W powyższym przykładzie, gdyby zamiast metody została użyta metoda compareTo
, kompilator nie zgłosiłby błędu, a kod działałby poprawnie, ponieważ zostałaby wywołana metoda equals
klasy String
. Musisz zrozumieć, że łączenie dynamiczne różni się od łączenia downcasting
. Poprawiony przykład:
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. Korzystanie z interfejsów.
Dla wielu uczniów różnica pomiędzy klasami i interfejsami nie jest do końca jasna. Dlatego niektórzy uczniowie próbują implementować interfejsy, używającObserver
słowa kluczowego Runnable
Extends zamiast implements . Aby poprawić błąd, wystarczy poprawić słowo kluczowe na właściwe. Zły przykład:
public class SharkSim extends Runnable {
float length;
...
}
Poprawiony przykład:
public class SharkSim implements Runnable {
float length;
...
}
Powiązany błąd: Niepoprawna kolejność rozszerzania i implementowania bloków . Zgodnie ze specyfikacją Java deklaracje rozszerzeń klas muszą występować przed deklaracjami implementacji interfejsu. Ponadto w przypadku interfejsów słowo kluczowe implements należy zapisać tylko raz; wiele interfejsów oddziela się przecinkami. Kilka bardziej błędnych przykładów:
// Неправильный порядок
public class SharkSim implements Swimmer extends Animal {
float length;
...
}
// ключевое слово implements встречается несколько раз
public class DiverSim implements Swimmer implements Runnable {
int airLeft;
...
}
Poprawione przykłady:
// Правильный порядок
public class SharkSim extends Animal implements Swimmer {
float length;
...
}
// Несколько интерфейсов разделяются запятыми
public class DiverSim implements Swimmer, Runnable {
int airLeft;
...
}
14. Zapomniałem użyć wartości zwracanej przez metodę nadklasy
Java umożliwia wywołanie podobnej metody nadklasy z podklasy za pomocą słowa kluczowego. Czasami uczniowie muszą wywoływać metody nadklasy, ale często zapominają o użyciu wartości zwracanej. Dzieje się tak szczególnie często wśród tych uczniów, którzy nie zrozumieli jeszcze metod i wartości zwracanych przez nie. W poniższym przykładzie uczeń chce wstawić wynik metodytoString()
nadklasy do wyniku toString()
metody podklasy. Nie wykorzystuje jednak wartości zwracanej przez metodę nadklasy. Zły przykład:
public class GraphicalRectangle extends Rectangle {
Color fillColor;
boolean beveled;
...
public String toString() {
super();
return("color=" + fillColor + ", beveled=" + beveled);
}
}
Aby skorygować błąd, zwykle wystarczy przypisać zwracaną wartość do zmiennej lokalnej, a następnie wykorzystać tę zmienną przy obliczaniu wyniku metody podklasy. Poprawiony przykład:
public class GraphicalRectangle extends Rectangle {
Color fillColor;
boolean beveled;
...
public String toString() {
String rectStr = super();
return(rectStr + " - " +
"color=" + fillColor + ", beveled=" + beveled);
}
}
15. Zapomniałem dodać komponenty AWT
AWT wykorzystuje prosty model projektowania GUI: każdy komponent interfejsu musi najpierw zostać utworzony przy użyciu własnego konstruktora, a następnie umieszczony w oknie aplikacji przy użyciuadd()
metody komponentu nadrzędnego. W ten sposób interfejs AWT otrzymuje strukturę hierarchiczną. Uczniowie czasami zapominają o tych 2 krokach. Tworzą komponent, ale zapominają o umieszczeniu go w oknie powiększenia. Nie spowoduje to błędów na etapie kompilacji, komponent po prostu nie pojawi się w oknie aplikacji. Zły przykład.
public class TestFrame extends Frame implements ActionListener {
public Button exit;
public TestFrame() {
super("Test Frame");
exit = new Button("Quit");
}
}
Aby naprawić ten błąd, wystarczy dodać komponenty do ich rodziców. Poniższy przykład pokazuje, jak to zrobić. Należy zauważyć, że często student, który zapomni dodać komponent do okna aplikacji, zapomina również o przypisaniu detektorów zdarzeń do tego komponentu. Poprawiony przykład:
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. Zapomniałem rozpocząć transmisję
Wielowątkowość w Javie jest implementowana przy użyciu platformyjava.lang.Thread
. Cykl życia wątku składa się z 4 etapów: zainicjowania, uruchomienia, zablokowania i zatrzymania. Nowo utworzony wątek jest w stanie zainicjowanym. Aby wprowadzić go w stan działania, musisz wywołać jego funkcję start()
. Czasami uczniowie tworzą wątki, ale zapominają je rozpocząć. Zwykle błąd pojawia się, gdy student nie ma wystarczającej wiedzy na temat programowania równoległego i wielowątkowości. (w przybliżeniu tłumaczę: nie widzę połączenia) Aby naprawić błąd, wystarczy rozpocząć wątek. W poniższym przykładzie uczeń chce stworzyć animację obrazka za pomocą interfejsu Runnable
, ale zapomniał rozpocząć wątek. Zły przykład
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);
}
}
...
}
Poprawiony przykład:
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);
}
}
...
}
Cykl życia wątku oraz relacje między wątkami i klasami implementującymi interfejs Runnable
to bardzo ważna część programowania w Javie i warto się na tym skupić.
18. Użycie zabronionej metody readLine() klasy java.io.DataInputStream
W Javie w wersji 1.0 do odczytania ciągu tekstowego konieczne było użycie metodyreadLine()
klasowej java.io.DataInputStream
. Wersja Java 1.1 dodała cały zestaw klas we/wy, aby zapewnić operacje we/wy dla tekstu: Reader
i Writer
. Zatem od wersji 1.1 do odczytania linijki tekstu należy zastosować metodę readLine()
klasową java.io.BufferedReader
. Uczniowie mogą nie być świadomi tej zmiany, zwłaszcza jeśli uczyli się na podstawie starszych książek. (w przybliżeniu tłumaczenie: właściwie już nieaktualne. Jest mało prawdopodobne, że ktokolwiek będzie teraz uczyć się z książek, które miały 10 lat). Stara metoda readLine()
pozostaje w JDK, ale została uznana za nielegalną, co często dezorientuje uczniów. Musisz zrozumieć, że używanie metody readLine()
klasowej java.io.DataInputStream
nie jest złe, jest po prostu przestarzałe. Musisz użyć klasy BufferedReader
. Zły przykład:
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;
}
}
Poprawiony przykład:
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;
}
}
Istnieją inne zabronione metody w wersjach późniejszych niż 1.0, ale ta jest najczęstsza.
19. Używanie double jako float
Podobnie jak większość innych języków, Java obsługuje operacje na liczbach zmiennoprzecinkowych (liczbach ułamkowych). Java ma 2 typy pierwotne dla liczb zmiennoprzecinkowych:double
dla liczb z 64-bitową precyzją zgodnie ze standardem IEEE i float
dla liczb z 32-bitową precyzją zgodnie ze standardem IEEE. Trudność polega na tym, że przy użyciu liczb dziesiętnych, takich jak 1,75, 12,9e17 lub -0,00003, kompilator przypisuje je do typu double
. Java nie wykonuje rzutowania typów w operacjach, w których może wystąpić utrata precyzji. To rzutowanie typu musi być wykonane przez programistę. Na przykład Java nie pozwoli na przypisanie wartości typu do int
zmiennej typu byte
bez rzutowania typu, jak pokazano w poniższym przykładzie.
byte byteValue1 = 17; /* неправильно! */
byte byteValue2 = (byte)19; /* правильно */
Ponieważ liczby ułamkowe są reprezentowane przez typ double
, a przypisanie double
do zmiennej typu float
może spowodować utratę precyzji, kompilator będzie narzekał na każdą próbę użycia liczb ułamkowych jako float
. Zatem użycie poniższych przypisań zapobiegnie kompilacji klasy.
float realValue1 = -1.7; /* неправильно! */
float realValue2 = (float)(-1.9); /* правильно */
To przypisanie działałoby w C lub C++, ale w Javie jest znacznie bardziej rygorystyczne. Istnieją 3 sposoby na pozbycie się tego błędu. Możesz użyć typu double
zamiast float
. To najprostsze rozwiązanie. Tak naprawdę nie ma sensu używać arytmetyki 32-bitowej zamiast 64-bitowej; różnica w szybkości i tak jest zjadana przez JVM (poza tym w nowoczesnych procesorach wszystkie liczby ułamkowe są konwertowane do formatu procesora 80-bitowego zarejestrować się przed jakąkolwiek operacją). Jedyną zaletą ich stosowania float
jest to, że zajmują mniej pamięci, co jest przydatne podczas pracy z dużą liczbą zmiennych ułamkowych. Możesz użyć modyfikatora typu liczbowego, aby poinformować kompilator, jak przechowywać liczbę. Modyfikator typu float - 'f'
. Zatem kompilator przypisze typ 1.75 do double
, i 1.75f - float
. Na przykład:
float realValue1 = 1.7; /* неправильно! */
float realValue2 = 1.9f; /* правильно */
Możesz użyć jawnego rzutowania typu. Jest to najmniej elegancki sposób, ale jest przydatny podczas konwertowania zmiennej typu double
na typ float
. Przykład:
float realValue1 = 1.7f;
double realValue2 = 1.9;
realValue1 = (float)realValue2;
Więcej o liczbach zmiennoprzecinkowych możesz przeczytać tutaj i tutaj.
-- komentarz tłumacza -- |
GO TO FULL VERSION