1. Tablica typu String
Krótko opowiemy o tablicy typu String.
Jak już mówiliśmy, tablica może być dowolnego typu. To znaczy, że można utworzyć tablicę typu String. Oto jak wyglądałby kod, gdybyśmy mieli napisać program, który „wczytuje z klawiatury 10 wierszy i wypisuje je na ekran w odwrotnej kolejności”.
String[] array = new String[10]; // Tworzymy obiekt–tablicę na 10 elementów
for (int i = 0; i < 10; i++) // Pętla od 0 do 9
{
array[i] = console.nextLine(); // Czytamy wiersz z klawiatury i zapisujemy go do komórki tablicy
}
for (int i = 9; i >= 0; i--) // Pętla od 9 do 0
{
System.out.println(array[i]); // Wypisujemy na ekran kolejną komórkę tablicy
}
Kod praktycznie się nie zmienił! Trzeba było tylko przy tworzeniu tablicy zamienić typ int na String. Poza tym wszystko jest dokładnie takie samo!
Wprowadzenie do null
Zagadka — co znajduje się w komórkach każdej z tych tablic tuż po utworzeniu?
int[] numbers = new int[10];
String[] strings = new String[10];
User[] users = new User[10];
Jeśli się nad tym zastanowić, to najprawdopodobniej komórki numbers zawierają 0. Zgadza się.
W takim razie, zgodnie z tą samą logiką, komórki strings powinny być wypełnione pustymi łańcuchami — "". Powiedzmy.
A czym wypełnione są komórki tablicy users? Pustymi obiektami User?
I tu tkwi haczyk: nie dla każdego typu danych istnieje „wartość domyślna”. Dlatego twórcy Javy wymyślili specjalną stałą — null. null to pusta referencja. Gdy tworzona jest zmienna typu obiektowego, jej wartość początkowa to null: referencja do niczego, brak referencji.
String name; // name zawiera null
name = "Alex"; // name zawiera referencję do obiektu/łańcucha "Alex"
name = null; // name zawiera null
Praca z null
Nie wolno wywoływać metod na obiekcie, jeśli referencja ma wartość null — obiektu przecież nie ma! W takim kodzie program zakończy się błędem:
String name; // name zawiera null
String upperName = name.toUpperCase(); // Wystąpi błąd NullPointerException: name == null
Jeśli chodzi o naszą „zagadkę”, odpowiedź jest taka:
int[] numbers = new int[10]; // {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
String[] strings = new String[10]; // {null, null, null, null, null, null, null, null, null, null}
User[] users = new User[10]; // {null, null, null, null, null, null, null, null, null, null}
Tylko typy prymitywne mają „wartość domyślną”. Wszystkie pozostałe typy domyślnie mają wartość null. Wartość początkowa String to nie jest pusty łańcuch. Takie życie.
2. Tablica typu String w pamięci
Rysunek 1. Jak obiekt String jest rozmieszczony w pamięci:

Zwróć uwagę, że tekst łańcucha nie jest przechowywany bezpośrednio w zmiennej: dla niego przydzielany jest osobny blok pamięci. W zmiennej typu String przechowywany jest adres (referencja) do obiektu z tekstem.
Rysunek 2. Jak tablica liczb całkowitych jest rozmieszczona w pamięci:

Ten obrazek także już widzieliście.
Rysunek 3. Jak w pamięci rozmieszczona jest tablica łańcuchów:

Po lewej widzimy zmienną–tablicę typu String[] (przechowuje adres obiektu–tablicy).
Pośrodku — obiekt–tablica typu String.
A po prawej — obiekty–łańcuchy, które przechowują jakieś teksty.
W komórkach obiektu–tablicy typu String nie są przechowywane same łańcuchy (teksty), lecz ich adresy (referencje). Dokładnie tak samo, jak w zmiennych typu String przechowywane są adresy łańcuchów (tekstu).
3. Szybka inicjalizacja tablicy w Javie
Tablice to bardzo przydatna rzecz, więc twórcy Javy postarali się maksymalnie ułatwić pracę z nimi. Pierwsze, co zrobili, to uproszczenie inicjalizacji tablicy, czyli nadawanie jej wartości początkowych.
Bardzo często, oprócz danych, które program skądś odczytuje, potrzebne są mu jeszcze wewnętrzne dane. Na przykład musimy przechowywać w tablicy długości wszystkich miesięcy. Jak może wyglądać ten kod:
int[] months = new int[12];
months[0] = 31; // styczeń
months[1] = 28; // luty
months[2] = 31; // marzec
months[3] = 30; // kwiecień
months[4] = 31; // maj
months[5] = 30; // czerwiec
months[6] = 31; // lipiec
months[7] = 31; // sierpień
months[8] = 30; // wrzesień
months[9] = 31; // październik
months[10] = 30; // listopad
months[11] = 31; // grudzień
Ale można to zapisać krócej — dzięki twórcom Javy:
// długości miesięcy roku
int[] months = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
Wystarczy po prostu wyliczyć po przecinku wszystkie wartości tablicy!
Okazuje się, że kompilator potrafi określić typ kontenera (obiektu–tablicy) na podstawie typu zmiennej–tablicy. A aby określić długość tablicy — po prostu zlicza liczbę elementów zapisanych w nawiasach klamrowych.
Dlatego ten kod można zapisać jeszcze krócej:
// długości miesięcy roku
int[] months = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
Czy to nie jest piękne? 🙂 Taki zapis nazywa się „szybką inicjalizacją tablicy”. Działa, swoją drogą, nie tylko dla typu int...
// nazwy miesięcy roku
String[] months = { "Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec", "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień" };
4. Pętla for-each
Przejście po wszystkich elementach tablicy to tak częsta operacja, że wymyślono do tego specjalną pętlę — for-each. Zapisuje się ją tak:
for (int score : scores)
{
System.out.println("Punkty: " + score);
}
Tutaj pętla sama iteruje po elementach — bez ręcznego indeksu. Nie można jednak zmieniać elementów bezpośrednio (w zmiennej pętli znajduje się tylko kopia wartości), za to bardzo wygodnie jest po prostu „przejść się” po tablicy.
Kompilator przekształci kod pętli for-each w poniższy kod:
for (int i = 0; i < scores.length; i++)
{
int score = scores[i];
System.out.println("Punkty: " + score);
}
Żadnej magii. Teraz widać, dlaczego nie można zmienić elementów tablicy przez zmienną score.
Napiszmy jeszcze przykład: znajdziemy sumę wszystkich punktów.
int sum = 0;
for (int score : scores)
{
sum += score;
}
System.out.println("Suma wszystkich punktów: " + sum);
Kiedy używać której pętli?
- Jeśli potrzebny jest indeks albo trzeba modyfikować elementy — pętla for.
- Jeśli chcesz tylko przejść po wartościach — for-each jest szybsze i czytelniejsze.
5. Zmiana elementów tablicy
A co, jeśli musimy zwiększyć każdą ocenę o 1 punkt (na przykład za dobre zachowanie)? Tutaj potrzebna jest klasyczna pętla for z indeksem, bo tylko tak możemy zmienić wartości w tablicy.
Przykład: zwiększyć każdy element o 1
for (int i = 0; i < grades.length; i++) {
grades[i] = grades[i] + 1;
}
Po wykonaniu tego kodu w tablicy grades będą nowe wartości.
Dlaczego nie można zrobić tego przez for-each?
for (int grade : grades) {
grade = grade + 1; // Nie działa!
}
Ten kod nie zmieni tablicy, ponieważ grade — to kopia wartości z tablicy, a nie referencja do samego elementu.
6. Obliczenia na podstawie tablicy
Tablice często wykorzystuje się do obliczeń: suma, średnia, maksimum, minimum i tak dalej.
Przykład: suma wszystkich elementów tablicy
int sum = 0;
for (int i = 0; i < grades.length; i++) {
sum += grades[i]; // to samo co sum = sum + grades[i];
}
System.out.println("Suma ocen: " + sum);
Przykład: wyszukiwanie wartości maksymalnej
int max = grades[0]; // zaczynamy od pierwszego elementu
for (int i = 1; i < grades.length; i++) {
if (grades[i] > max) {
max = grades[i];
}
}
System.out.println("Maksymalna ocena: " + max);
Przykład: wyszukiwanie wartości minimalnej
int min = grades[0]; // zaczynamy od pierwszego elementu
for (int i = 1; i < grades.length; i++) {
if (grades[i] < min) {
min = grades[i];
}
}
System.out.println("Minimalna ocena: " + min);
Przykład: obliczanie średniej arytmetycznej
int sum = 0;
for (int i = 0; i < grades.length; i++) {
sum += grades[i];
}
double average = (double) sum / grades.length; // koniecznie rzutować na double!
System.out.println("Średnia ocena: " + average);
Zwróć uwagę: aby otrzymać liczbę niecałkowitą, trzeba jawnie rzutować sumę na typ double przed dzieleniem.
7. Zadania praktyczne
Wczytywanie tablicy z klawiatury
Często trzeba wypełnić tablicę wartościami wprowadzanymi przez użytkownika. Do tego używamy klasy Scanner i pętli.
Scanner console = new Scanner(System.in);
int n = 5; // rozmiar tablicy
int[] numbers = new int[n];
System.out.println("Wprowadź " + n + " liczb:");
for (int i = 0; i < n; i++) {
numbers[i] = console.nextInt();
}
System.out.println("Wprowadzono:");
for (int i = 0; i < n; i++) {
System.out.println(numbers[i]);
}
Wypisywanie tablicy w odwrotnej kolejności
Czasem trzeba wypisać elementy tablicy od końca (na przykład, aby sprawdzić, kto wszedł do pokoju jako ostatni).
for (int i = grades.length - 1; i >= 0; i--) {
System.out.println("Ocena nr " + (i + 1) + ": " + grades[i]);
}
8. Typowe błędy przy pracy z tablicami jednowymiarowymi
Błąd nr 1: wyjście poza granice tablicy
Najczęstszy problem — próba odwołania się do nieistniejącego elementu. W Javie prowadzi to do wyjątku ArrayIndexOutOfBoundsException. Pamiętaj: ostatni dozwolony indeks — to arr.length - 1.
int[] arr = new int[5];
System.out.println(arr[5]); // Błąd! Indeksy od 0 do 4.
Błąd nr 2: zapomniano zainicjować tablicę
Zadeklarowano zmienną, ale nie utworzono tablicy:
int[] arr;
arr[0] = 5; // Błąd! Tablica nie została utworzona.
Należy koniecznie utworzyć tablicę za pomocą new:
arr = new int[10];
Błąd nr 3: próba zmiany elementów tablicy przez for-each
W pętli for-each zmienna — to kopia wartości, a nie referencja do elementu:
for (int x : arr) {
x = 100; // Nie zmienia tablicy!
}
Aby zmienić wartości, użyj zwykłej pętli for z indeksem.
Błąd nr 4: nieprawidłowe użycie długości tablicy
Czasem myli się długość tablicy z ostatnim indeksem:
for (int i = 0; i <= arr.length; i++) { // Błąd! Powinno być i < arr.length
// ...
}
Taki kod spowoduje wyjście poza granice.
Błąd nr 5: niejawna konwersja typów
Jeśli tablica jest typu int, nie można bezpośrednio przypisać jej wartości typu double:
int[] arr = new int[3];
arr[0] = 3.14; // Błąd! 3.14 to double.
GO TO FULL VERSION