Szukaj…


Wprowadzenie

W tym temacie opisano niektóre typowe błędy popełniane przez początkujących w Javie.

Obejmuje to wszelkie typowe błędy w używaniu języka Java lub zrozumieniu środowiska wykonawczego.

Błędy związane z określonymi interfejsami API można opisać w tematach charakterystycznych dla tych interfejsów API. Ciągi znaków są specjalnym przypadkiem; są one objęte specyfikacją języka Java. Szczegóły inne niż typowe błędy można opisać w tym temacie dotyczącym ciągów .

Pitfall: użycie == do porównania prymitywnych obiektów owijania, takich jak liczba całkowita

(To pułapka stosuje się jednakowo do wszystkich typów pierwotnych otoki, ale będziemy ją zilustrować na Integer i int .)

Podczas pracy z obiektami Integer kuszące jest użycie == do porównywania wartości, ponieważ właśnie to zrobiłbyś z wartościami int . W niektórych przypadkach wydaje się, że to zadziała:

Integer int1_1 = Integer.valueOf("1");
Integer int1_2 = Integer.valueOf(1);

System.out.println("int1_1 == int1_2: " + (int1_1 == int1_2));          // true
System.out.println("int1_1 equals int1_2: " + int1_1.equals(int1_2));   // true

Tutaj stworzyliśmy dwa obiekty Integer o wartości 1 i porównujemy je (w tym przypadku stworzyliśmy jeden z ciągu String a drugi z literału int . Istnieją inne alternatywy). Ponadto obserwujemy, że obie metody porównania ( == i equals ) dają true .

To zachowanie zmienia się, gdy wybieramy różne wartości:

Integer int2_1 = Integer.valueOf("1000");
Integer int2_2 = Integer.valueOf(1000);

System.out.println("int2_1 == int2_2: " + (int2_1 == int2_2));          // false
System.out.println("int2_1 equals int2_2: " + int2_1.equals(int2_2));   // true

W takim przypadku tylko porównanie equals daje prawidłowy wynik.

Przyczyną tej różnicy w zachowaniu jest to, że JVM utrzymuje pamięć podręczną obiektów Integer dla zakresu od -128 do 127. (Górną wartość można zastąpić właściwością systemową „java.lang.Integer.IntegerCache.high” lub Argument JVM „-XX: AutoBoxCacheMax = rozmiar”). W przypadku wartości z tego zakresu Integer.valueOf() zwróci buforowaną wartość zamiast tworzyć nową.

Zatem w pierwszym przykładzie wywołania Integer.valueOf(1) i Integer.valueOf("1") zwracały tę samą buforowaną instancję Integer . Natomiast w drugim przykładzie Integer.valueOf(1000) i Integer.valueOf("1000") utworzyły i zwróciły nowe obiekty Integer .

Operator == dla typów referencyjnych testuje równość referencyjną (tj. Ten sam obiekt). Dlatego w pierwszym przykładzie int1_1 == int1_2 jest true ponieważ odwołania są takie same. W drugim przykładzie int2_1 == int2_2 jest fałszem, ponieważ odwołania są różne.

Pitfall: zapomnienie o wolnych zasobach

Za każdym razem, gdy program otwiera zasób, taki jak plik lub połączenie sieciowe, ważne jest, aby zwolnić zasób po zakończeniu jego używania. Podobną ostrożność należy zachować, jeśli podczas operacji na takich zasobach zostanie zgłoszony wyjątek. Można argumentować, że FileInputStream ma finalizator, który wywołuje metodę close() w przypadku zdarzenia FileInputStream ; Ponieważ jednak nie jesteśmy pewni, kiedy rozpocznie się cykl wyrzucania elementów bezużytecznych, strumień wejściowy może zużywać zasoby komputera przez czas nieokreślony. Zasób musi zostać zamknięty w finally sekcji bloku try-catch:

Java SE 7
private static void printFileJava6() throws IOException {
    FileInputStream input;
    try {
        input = new FileInputStream("file.txt");
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    } finally {
        if (input != null) {
            input.close();
        }
    }
}

Od wersji Java 7, szczególnie w tym przypadku, wprowadzono w Javie 7 bardzo przydatną i zgrabną instrukcję, zwaną try-with-resources:

Java SE 7
private static void printFileJava7() throws IOException {
    try (FileInputStream input = new FileInputStream("file.txt")) {
        int data = input.read();
        while (data != -1){
            System.out.print((char) data);
            data = input.read();
        }
    }
}

Instrukcja try-with-resources może być używana z dowolnym obiektem, który implementuje interfejs Closeable lub AutoCloseable . Zapewnia to zamknięcie każdego zasobu do końca instrukcji. Różnica między tymi dwoma interfejsami polega na tym, że metoda close() Closeable zgłasza Closeable IOException który należy w jakiś sposób obsłużyć.

W przypadkach, w których zasób został już otwarty, ale po użyciu powinien zostać bezpiecznie zamknięty, można przypisać go do zmiennej lokalnej wewnątrz try-with-resources

Java SE 7
private static void printFileJava7(InputStream extResource) throws IOException {
    try (InputStream input = extResource) {
        ... //access resource
    }
}

Lokalna zmienna zasobów utworzona w konstruktorze try-with-resources jest faktycznie ostateczna.

Pitfall: wycieki pamięci

Java automatycznie zarządza pamięcią. Nie musisz ręcznie zwalniać pamięci. Pamięć obiektu na stercie może zostać zwolniona przez śmieciarz, gdy obiekt nie jest już osiągalny przez aktywny wątek.

Możesz jednak zapobiec uwolnieniu pamięci, umożliwiając dostęp do obiektów, które nie są już potrzebne. Niezależnie od tego, czy nazywasz to przeciekaniem pamięci, czy pakowaniem pamięci, rezultat jest taki sam - niepotrzebny wzrost przydzielonej pamięci.

Wycieki pamięci w Javie mogą się zdarzyć na różne sposoby, ale najczęstszym powodem są wieczne odwołania do obiektów, ponieważ śmieciarz nie może usunąć obiektów ze sterty, dopóki są jeszcze odniesienia do nich.

Pola statyczne

Można utworzyć takie odwołanie, definiując klasę za pomocą pola static zawierającego pewną kolekcję obiektów, i zapominając o ustawieniu tego pola static na null gdy kolekcja nie jest już potrzebna. pola static są uważane za pierwiastki GC i nigdy nie są gromadzone. Innym problemem są wycieki w pamięci nie-sterty, gdy używany jest JNI .

Przeciek Classloader

Zdecydowanie najbardziej podstępnym rodzajem wycieku pamięci jest wyciek modułu ładującego . Classloader zawiera odniesienie do każdej załadowanej klasy, a każda klasa zawiera odniesienie do swojego loadloadera. Każdy obiekt zawiera również odwołanie do swojej klasy. Dlatego jeśli nawet pojedynczy obiekt klasy załadowany przez moduł ładujący klasy nie jest śmieciem, nie można zebrać ani jednej klasy załadowanej przez moduł ładujący klasy. Ponieważ każda klasa odnosi się również do jej pól statycznych, nie można ich również gromadzić.

Wyciek kumulacji Przykład wycieku kumulacji może wyglądać następująco:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    BigDecimal number = numbers.peekLast();
    if (number != null && number.remainder(divisor).byteValue() == 0) {
        System.out.println("Number: " + number);
        System.out.println("Deque size: " + numbers.size());
    }
}, 10, 10, TimeUnit.MILLISECONDS);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);

try {
    scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Ten przykład tworzy dwa zaplanowane zadania. Pierwsze zadanie pobiera ostatnią liczbę z liczby wywoływanej nazywanej numbers , a jeśli liczba jest podzielna przez 51, wypisuje liczbę i wielkość logiki. Drugie zadanie umieszcza liczby w deque. Oba zadania są zaplanowane ze stałą szybkością i działają co 10 ms.

Jeśli kod zostanie wykonany, zobaczysz, że rozmiar deque stale rośnie. Spowoduje to, że deque zostanie wypełnione obiektami, które zajmują całą dostępną pamięć sterty.

Aby temu zapobiec przy jednoczesnym zachowaniu semantyki tego programu, możemy użyć innej metody pobierania liczb z deque: pollLast . W przeciwieństwie do metody peekLast , pollLast zwraca element i usuwa go z deque, podczas gdy peekLast zwraca tylko ostatni element.

Pitfall: użycie == do porównania ciągów

Częstym błędem dla początkujących w Javie jest użycie operatora == do sprawdzenia, czy dwa ciągi są równe. Na przykład:

public class Hello {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0] == "hello") {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Powyższy program ma przetestować pierwszy argument wiersza poleceń i wydrukować różne komunikaty, gdy nie jest to słowo „cześć”. Ale problem polega na tym, że to nie zadziała. Ten program wyświetli „Czy dzisiaj czujesz się zrzędliwy?” bez względu na to, jaki jest pierwszy argument wiersza poleceń.

W tym szczególnym przypadku String „hello” jest umieszczany w puli ciągów, podczas gdy String argumentów [0] znajduje się na stercie. Oznacza to, że istnieją dwa obiekty reprezentujące ten sam literał, każdy z odnośnikiem. Ponieważ == testuje referencje, a nie rzeczywistą równość, porównanie zazwyczaj daje fałszywe informacje. Nie oznacza to, że zawsze tak będzie.

Gdy używasz == do testowania ciągów, tak naprawdę testujesz, jeśli dwa obiekty String są tym samym obiektem Java. Niestety nie to oznacza równość łańcuchów w Javie. W rzeczywistości poprawnym sposobem testowania ciągów jest użycie metody equals(Object) . W przypadku pary ciągów zwykle chcemy sprawdzić, czy składają się z tych samych znaków w tej samej kolejności.

public class Hello2 {
    public static void main(String[] args) {
        if (args.length > 0) {
            if (args[0].equals("hello")) {
                System.out.println("Hello back to you");
            } else {
                System.out.println("Are you feeling grumpy today?");
            }
        }
    }
}

Ale tak naprawdę jest gorzej. Problemem jest to, że == dadzą oczekiwany odpowiedź w pewnych okolicznościach. Na przykład

public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        if (s1 == s2) {
            System.out.println("same");
        } else {
            System.out.println("different");
        }
    }
}

Co ciekawe, to wypisuje „to samo”, nawet jeśli testujemy łańcuchy w niewłaściwy sposób. Dlaczego? Ponieważ specyfikacja języka Java (rozdział 3.10.5: Literały łańcuchowe) stanowi, że dwa łańcuchy >> literały łańcuchowe << składające się z tych samych znaków będą faktycznie reprezentowane przez ten sam obiekt Java. Dlatego test == da prawdę dla równych literałów. (Literały łańcuchowe są „internowane” i dodawane do wspólnej „puli łańcuchowej” po załadowaniu kodu, ale tak naprawdę jest to szczegół implementacji.)

Aby dodać zamieszanie, specyfikacja języka Java określa również, że jeśli masz wyrażenie stałe w czasie kompilacji, które łączy dwa literały łańcuchowe, jest to równoważne pojedynczemu literałowi. A zatem:

    public class Test1 {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        String s3 = " mum";
        if (s1 == s2) {
            System.out.println("1. same");
        } else {
            System.out.println("1. different");
        }
        if (s1 + s3 == "hello mum") {
            System.out.println("2. same");
        } else {
            System.out.println("2. different");
        }
    }
}

Spowoduje to wyświetlenie „1. tego samego” i „2. innego”. W pierwszym przypadku wyrażenie + jest oceniane podczas kompilacji i porównujemy jeden obiekt String z samym sobą. W drugim przypadku jest on oceniany w czasie wykonywania i porównujemy dwa różne obiekty String

Podsumowując, użycie == do testowania ciągów w Javie jest prawie zawsze niepoprawne, ale nie ma gwarancji, że poda złą odpowiedź.

Pitfall: testowanie pliku przed próbą jego otwarcia.

Niektóre osoby zalecają, aby przed próbą otwarcia pliku zastosować różne testy do pliku, aby zapewnić lepszą diagnostykę lub uniknąć wyjątków. Na przykład ta metoda próbuje sprawdzić, czy path odpowiada czytelnemu plikowi:

public static File getValidatedFile(String path) throws IOException {
    File f = new File(path);
    if (!f.exists()) throw new IOException("Error: not found: " + path);
    if (!f.isFile()) throw new IOException("Error: Is a directory: " + path);
    if (!f.canRead()) throw new IOException("Error: cannot read file: " + path);
    return f;
}

Możesz użyć powyższej metody w ten sposób:

File f = null;
try {
    f = getValidatedFile("somefile");
} catch (IOException ex) {
    System.err.println(ex.getMessage());
    return;
}
try (InputStream is = new FileInputStream(file)) {
    // Read data etc.
}

Pierwszy problem dotyczy podpisu dla FileInputStream(File) ponieważ kompilator nadal będzie nalegał na przechwycenie IOException tutaj lub dalej na stosie.

Drugi problem polega na tym, że kontrole wykonywane przez getValidatedFile nie gwarantują powodzenia FileInputStream .

  • Warunki wyścigu: inny wątek lub osobny proces może zmienić nazwę pliku, usunąć plik lub usunąć dostęp do odczytu po powrocie getValidatedFile . Doprowadziłoby to do „zwykłego” IOException bez niestandardowego komunikatu.

  • Istnieją przypadki skrajne, które nie są objęte tymi testami. Na przykład w systemie z SELinux w trybie „wymuszania” próba odczytania pliku może się nie powieść, mimo że canRead() zwraca true .

Trzeci problem polega na tym, że testy są nieefektywne. Na przykład, exists , isFile i canRead połączenia będą każdy zrobić syscall wykonać wymaganą czek. Następnie wykonuje się kolejne wywołanie systemowe, aby otworzyć plik, który powtarza te same kontrole za kulisami.

Krótko mówiąc, metody takie jak getValidatedFile są błędne. Lepiej po prostu spróbować otworzyć plik i obsłużyć wyjątek:

try (InputStream is = new FileInputStream("somefile")) {
    // Read data etc.
} catch (IOException ex) {
    System.err.println("IO Error processing 'somefile': " + ex.getMessage());
    return;
}

Jeśli chcesz rozróżnić błędy We / Wy zgłaszane podczas otwierania i czytania, możesz użyć zagnieżdżonego try / catch. Jeśli chcesz uzyskać lepszą diagnostykę dla otwartych awarii, możesz wykonać kontrole exists , isFile i canRead w canRead obsługi.

Pitfall: myślenie o zmiennych jako przedmiotach

Żadna zmienna Java nie reprezentuje obiektu.

String foo;   // NOT AN OBJECT

Żadna tablica Java również nie zawiera obiektów.

String bar[] = new String[100];  // No member is an object.

Jeśli błędnie pomyślisz o zmiennych jako obiektach, zaskoczy Cię faktyczne zachowanie języka Java.

  • W przypadku zmiennych Java, które mają typ pierwotny (np. int lub float ), zmienna przechowuje kopię wartości. Wszystkie kopie pierwotnej wartości są nierozróżnialne; tzn. istnieje tylko jedna wartość int dla liczby jeden. Wartości pierwotne nie są obiektami i nie zachowują się jak obiekty.

  • W przypadku zmiennych Java, które mają typ odwołania (klasa lub tablica), zmienna zawiera odwołanie. Wszystkie kopie referencji są nie do odróżnienia. Odwołania mogą wskazywać na obiekty lub mogą być null co oznacza, że nie wskazują na żaden obiekt. Nie są jednak obiektami i nie zachowują się jak obiekty.

Zmienne nie są obiektami w obu przypadkach i nie zawierają obiektów w obu przypadkach. Mogą zawierać odniesienia do obiektów , ale to mówi coś innego.

Przykładowa klasa

Poniższe przykłady wykorzystują tę klasę, która reprezentuje punkt w przestrzeni 2D.

public final class MutableLocation {
   public int x;
   public int y;

   public MutableLocation(int x, int y) {
       this.x = x;
       this.y = y;
   }

   public boolean equals(Object other) {
       if (!(other instanceof MutableLocation) {
           return false;
       }
       MutableLocation that = (MutableLocation) other;
       return this.x == that.x && this.y == that.y;
   }
}

Wystąpieniem tej klasy jest obiekt, który posiada dwa pola x i y , które mają typu int .

Możemy mieć wiele instancji klasy MutableLocation . Niektóre będą reprezentować te same lokalizacje w przestrzeni 2D; czyli odpowiednie wartości x i y będą pasować. Inne będą reprezentować różne lokalizacje.

Wiele zmiennych może wskazywać na ten sam obiekt

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

Powyżej zadeklarowaliśmy trzy zmienne here , there i elsewhere które mogą zawierać odniesienia do obiektów MutableLocation .

Jeśli (niepoprawnie) myślisz o tych zmiennych jako obiektach, prawdopodobnie błędnie zinterpretujesz stwierdzenia:

  1. Skopiuj lokalizację „[1, 2]” here
  2. Kopiowanie położenie „[1, 2]”, aby there
  3. Skopiuj lokalizację „[1, 2]” w elsewhere

Z tego prawdopodobnie wnioskujesz, że mamy trzy niezależne obiekty w trzech zmiennych. W rzeczywistości istnieją tylko dwa obiekty utworzone przez powyższe. Zmienne here i there faktycznie odnoszą się do tego samego obiektu.

Możemy to zademonstrować. Zakładając zmienne deklaracje jak wyżej:

System.out.println("BEFORE: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);
here.x = 42;
System.out.println("AFTER: here.x is " + here.x + ", there.x is " + there.x +
                   "elsewhere.x is " + elsewhere.x);

Spowoduje to wyświetlenie następujących danych:

BEFORE: here.x is 1, there.x is 1, elsewhere.x is 1
AFTER: here.x is 42, there.x is 42, elsewhere.x is 1

Przypisaliśmy nową wartość here.x i to zmieniło wartość, którą widzimy za there.x pośrednictwem. there.x Odnoszą się do tego samego obiektu. Ale wartość, którą widzimy za pośrednictwem pliku elsewhere.x , nie uległa zmianie, więc elsewhere musi odnosić się do innego obiektu.

Gdyby zmienna była obiektem, przypisanie tutaj. here.x = 42 nie zmieniłoby się there.x . there.x

Operator równości NIE sprawdza, czy dwa obiekty są równe

Zastosowanie operatora równości ( == ) do testów wartości odniesienia, jeśli wartości odnoszą się do tego samego obiektu. To nie sprawdza, czy dwa (różne) obiekty są „równe” w intuicyjnym sensie.

 MutableLocation here = new MutableLocation(1, 2);
 MutableLocation there = here;
 MutableLocation elsewhere = new MutableLocation(1, 2);

 if (here == there) {
     System.out.println("here is there");
 }
 if (here == elsewhere) {
     System.out.println("here is elsewhere");
 }

Spowoduje to wydrukowanie „tutaj jest tam”, ale nie zostanie wydrukowane „tutaj jest gdzie indziej”. (Odniesienia here i elsewhere dotyczą dwóch różnych obiektów.)

Natomiast jeśli equals(Object) metodę equals(Object) , którą zaimplementowaliśmy powyżej, sprawdzimy, czy dwie instancje MutableLocation mają równe położenie.

 if (here.equals(there)) {
     System.out.println("here equals there");
 }
 if (here.equals(elsewhere)) {
     System.out.println("here equals elsewhere");
 }

Spowoduje to wydrukowanie obu wiadomości. W szczególności here.equals(elsewhere) zwraca true ponieważ kryteria semantyczne, które wybraliśmy dla równości dwóch obiektów MutableLocation , zostały spełnione.

Wywołania metod wcale nie przekazują obiektów

Wywołania metod Java używają przekazywania o wartości 1 do przekazywania argumentów i zwracania wyniku.

Kiedy przekazujesz wartość referencyjną do metody, faktycznie przekazujesz referencję do obiektu według wartości , co oznacza, że tworzy kopię referencji do obiektu.

Dopóki oba odwołania do obiektów nadal wskazują ten sam obiekt, możesz modyfikować ten obiekt z dowolnego z nich, a to powoduje pewne zamieszanie.

Jednak nie przekazujesz obiektu przez odniesienie 2 . Różnica polega na tym, że jeśli kopia odwołania do obiektu zostanie zmodyfikowana, aby wskazywała na inny obiekt, oryginalne odwołanie do obiektu będzie nadal wskazywać na oryginalny obiekt.

void f(MutableLocation foo) {  
    foo = new MutableLocation(3, 4);   // Point local foo at a different object.
}

void g() {
    MutableLocation foo = MutableLocation(1, 2);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 1".
}

Nie przekazujesz też kopii obiektu.

void f(MutableLocation foo) {  
    foo.x = 42;
}

void g() {
    MutableLocation foo = new MutableLocation(0, 0);
    f(foo);
    System.out.println("foo.x is " + foo.x); // Prints "foo.x is 42"
}

1 - W językach takich jak Python i Ruby termin „przekazuj przez udostępnianie” jest preferowany w odniesieniu do „przekazywania przez wartość” obiektu / odwołania.

2 - Termin „przekazać przez odniesienie” lub „wywołanie przez odniesienie” ma bardzo konkretne znaczenie w terminologii języka programowania. W efekcie oznacza to, że przekazujesz adres zmiennej lub elementu tablicy , więc gdy wywoływana metoda przypisuje nową wartość do argumentu formalnego, zmienia wartość w oryginalnej zmiennej. Java nie obsługuje tego. Pełniejszy opis różnych mechanizmów przekazywania parametrów znajduje się na stronie https://en.wikipedia.org/wiki/Evaluation_strategy .

Pitfall: połączenie zadania i skutków ubocznych

Czasami widzimy pytania Java StackOverflow (oraz pytania C lub C ++), które zadają coś takiego:

i += a[i++] + b[i--];

ocenia na ... dla niektórych znanych stanów początkowych i , a i b .

Ogólnie rzecz biorąc:

  • w przypadku Javy odpowiedź jest zawsze określona 1 , ale nieoczywista i często trudna do zrozumienia
  • w przypadku C i C ++ odpowiedź jest często nieokreślona.

Takie przykłady są często używane na egzaminach lub rozmowach kwalifikacyjnych jako próba sprawdzenia, czy student lub rozmówca rozumie, jak naprawdę działa ocena wyrażeń w języku programowania Java. Jest to prawdopodobnie uzasadnione jako „test wiedzy”, ale to nie znaczy, że powinieneś to zrobić w prawdziwym programie.

Aby to zilustrować, w pozornie prostych pytaniach pojawił się kilka razy w pytaniach StackOverflow (takich jak ten ). W niektórych przypadkach wydaje się, że jest to prawdziwy błąd w czyimś kodzie.

int a = 1;
a = a++;
System.out.println(a);    // What does this print.

Większość programistów (w tym ekspertów Java) szybko czytających te stwierdzenia powiedziałoby, że wypisuje 2 . W rzeczywistości daje 1 . Aby uzyskać szczegółowe wyjaśnienie przyczyny, przeczytaj tę odpowiedź .

Jednak prawdziwą różnicą od tego i podobnych przykładów jest to, że każda instrukcja Java, która zarówno przypisuje tę samą zmienną, jak i skutki uboczne, będzie co najwyżej trudna do zrozumienia, aw najgorszym wręcz myląca. Powinieneś unikać pisania takiego kodu.


1 - potencjalne problemy modulo z modelem pamięci Java, jeśli zmienne lub obiekty są widoczne dla innych wątków.

Pitfall: Nie rozumiem, że String jest niezmienną klasą

Nowi programiści Java często zapominają lub nie rozumieją w pełni, że klasa Java String jest niezmienna. Prowadzi to do problemów takich jak ten w poniższym przykładzie:

public class Shout {
    public static void main(String[] args) {
        for (String s : args) {
            s.toUpperCase();
            System.out.print(s);
            System.out.print(" ");
        }
        System.out.println();
    }
}

Powyższy kod ma wypisywać argumenty wiersza poleceń dużymi literami. Niestety to nie działa, przypadek argumentów nie ulega zmianie. Problemem jest to stwierdzenie:

s.toUpperCase();

Można by pomyśleć, że wywołanie toUpperCase() spowoduje zamianę s na ciąg wielkich liter. Tak nie jest. Nie może! Obiekty String są niezmienne. Nie można ich zmienić.

W rzeczywistości metoda toUpperCase() zwraca obiekt String który jest wersją String , która jest wywoływana wielkimi literami. Prawdopodobnie będzie to nowy obiekt String , ale jeśli s było już pisane wielkimi literami, wynikiem może być istniejący ciąg.

Aby więc skutecznie korzystać z tej metody, musisz użyć obiektu zwróconego przez wywołanie metody; na przykład:

s = s.toUpperCase();

W rzeczywistości reguła „ciągi nigdy się nie zmieniają” dotyczy wszystkich metod String . Jeśli to pamiętasz, możesz uniknąć całej kategorii błędów początkujących.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow