Cześć! Na dzisiejszej lekcji będziemy kontynuować naukę rodzajów ogólnych. Tak się składa, że jest to obszerny temat, ale nie da się go uniknąć — jest to niezwykle ważna część języka :) Kiedy studiujesz dokumentację Oracle dotyczącą generyków lub czytasz samouczki online, natkniesz się na terminy typy niereplikowalne i typy podlegające ponownemu użyciu . Typ podlegający ponownej weryfikacji to typ, dla którego informacje są w pełni dostępne w czasie wykonywania. W Javie takie typy obejmują prymitywy, typy surowe i typy nieogólne. W przeciwieństwie do tego typy, których nie można powtórzyć, to typy, których informacje są usuwane i stają się niedostępne w czasie wykonywania. Tak się składa, że są to generyczne —
Zanieczyszczenie sterty może wystąpić w następującej sytuacji:
List<String>
, List<Integer>
, itd.
Przy okazji, czy pamiętasz, co to jest varargs?
Jeśli zapomniałeś, jest to argument o zmiennej długości. Są przydatne w sytuacjach, gdy nie wiemy, ile argumentów można przekazać naszej metodzie. Na przykład, jeśli mamy klasę kalkulatora, która masum
metodę. Metoda sum()
może otrzymać 2 liczby lub 3 lub 5 lub dowolną liczbę. Byłoby bardzo dziwne przeciążać sum()
metodę dla każdej możliwej liczby argumentów. Zamiast tego możemy to zrobić:
public class SimpleCalculator {
public static int sum(int...numbers) {
int result = 0;
for(int i : numbers) {
result += i;
}
return result;
}
public static void main(String[] args) {
System.out.println(sum(1,2,3,4,5));
System.out.println(sum(2,9));
}
}
Wyjście konsoli:
15
11
To pokazuje nam, że istnieje kilka ważnych funkcji podczas używania varargs w połączeniu z rodzajami. Spójrzmy na następujący kod:
import javafx.util.Pair;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
public static void main(String[] args) {
addAll(new ArrayList<String>(), // This is okay
"Leonardo da Vinci",
"Vasco de Gama"
);
// but here we get a warning
addAll(new ArrayList<Pair<String, String>>(),
new Pair<String, String>("Leonardo", "da Vinci"),
new Pair<String, String>("Vasco", "de Gama")
);
}
}
Metoda addAll()
przyjmuje jako dane wejściowe a List<E>
i dowolną liczbę E
obiektów, a następnie dodaje wszystkie te obiekty do listy. W main()
metodzie wywołujemy naszą addAll()
metodę dwukrotnie. W pierwszym przypadku dodajemy dwa zwykłe łańcuchy do pliku List
. Tutaj wszystko jest w porządku. W drugim przypadku dodajemy dwa Pair<String, String>
obiekty do pliku List
. Ale tutaj nieoczekiwanie otrzymujemy ostrzeżenie:
Unchecked generics array creation for varargs parameter
Co to znaczy? Dlaczego otrzymujemy ostrzeżenie i dlaczego jest wzmianka o array
? W końcu nasz kod nie ma array
! Zacznijmy od drugiego przypadku. Ostrzeżenie wspomina o tablicy, ponieważ kompilator konwertuje argument o zmiennej długości (varargs) na tablicę. Innymi słowy, sygnatura naszej addAll()
metody to:
public static <E> void addAll(List<E> list, E... array)
Właściwie wygląda to tak:
public static <E> void addAll(List<E> list, E[] array)
Oznacza to, że w main()
metodzie kompilator konwertuje nasz kod na następujący:
public static void main(String[] args) {
addAll(new ArrayList<String>(),
new String[] {
"Leonardo da Vinci",
"Vasco de Gama"
}
);
addAll(new ArrayList<Pair<String,String>>(),
new Pair<String,String>[] {
new Pair<String,String>("Leonardo","da Vinci"),
new Pair<String,String>("Vasco","de Gama")
}
);
}
Tablica String
jest po prostu w porządku. Ale Pair<String, String>
tablica nie jest. Problem polega na tym, że Pair<String, String>
jest to typ, którego nie da się naprawić. Podczas kompilacji wszystkie informacje o argumentach typu (<String, String>) są usuwane. Tworzenie tablic typu niepodlegającego ponownej weryfikacji jest niedozwolone w Javie . Możesz to zobaczyć, jeśli spróbujesz ręcznie utworzyć tablicę Pair<String, String>
public static void main(String[] args) {
// Compilation error Generic array creation
Pair<String, String>[] array = new Pair<String, String>[10];
}
Powód jest oczywisty: bezpieczeństwo typu. Jak zapewne pamiętasz, podczas tworzenia tablicy zdecydowanie musisz określić, które obiekty (lub prymitywy) będą przechowywane w tablicy.
int array[] = new int[10];
W jednej z naszych poprzednich lekcji szczegółowo zbadaliśmy wymazywanie czcionek. W tym przypadku wymazywanie czcionek powoduje utratę informacji, które obiekty Pair
przechowują <String, String>
w parach. Tworzenie tablicy byłoby niebezpieczne. Używając metod, które zawierają varargi i generyczne, pamiętaj o wymazywaniu typów i o tym, jak to działa. Jeśli masz absolutną pewność co do napisanego kodu i wiesz, że nie spowoduje on żadnych problemów, możesz wyłączyć ostrzeżenia związane z varargs za pomocą adnotacji @SafeVarargs
.
@SafeVarargs
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
Jeśli dodasz tę adnotację do swojej metody, ostrzeżenie, które napotkaliśmy wcześniej, nie pojawi się. Innym problemem, który może wystąpić podczas używania varargs z generykami, jest zanieczyszczenie sterty. 
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<String> polluteHeap() {
List numbers = new ArrayList<Number>();
numbers.add(1);
List<String> strings = numbers;
strings.add("");
return strings;
}
public static void main(String[] args) {
List<String> stringsWithHeapPollution = polluteHeap();
System.out.println(stringsWithHeapPollution.get(0));
}
}
Wyjście konsoli:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Mówiąc prościej, zanieczyszczenie sterty ma miejsce, gdy obiekty typu A
powinny znajdować się na stercie, ale obiekty typu B
trafiają tam z powodu błędów związanych z bezpieczeństwem typów. W naszym przykładzie tak właśnie się dzieje. Najpierw utworzyliśmy zmienną surową i przypisaliśmy jej numbers
ogólną kolekcję ( ). ArrayList<Number>
Następnie dodaliśmy numer 1
do kolekcji.
List<String> strings = numbers;
W tym wierszu kompilator próbował ostrzec nas przed możliwymi błędami, wyświetlając ostrzeżenie „ Niesprawdzone przypisanie... ”, ale zignorowaliśmy to. Otrzymujemy ogólną zmienną typu List<String>
, która wskazuje na ogólną kolekcję typu ArrayList<Number>
. Oczywiście taka sytuacja może prowadzić do kłopotów! I tak się dzieje. Korzystając z naszej nowej zmiennej, dodajemy ciąg znaków do kolekcji. Mamy teraz zanieczyszczenie sterty — dodaliśmy liczbę, a następnie ciąg znaków do sparametryzowanej kolekcji. Kompilator nas ostrzegł, ale zignorowaliśmy jego ostrzeżenie. W rezultacie otrzymujemy ClassCastException
tylko podczas działania programu. Więc co to ma wspólnego z varargs? Używanie varargs z generykami może łatwo doprowadzić do zanieczyszczenia sterty. Oto prosty przykład:
import java.util.Arrays;
import java.util.List;
public class Main {
static void polluteHeap(List<String>... stringsLists) {
Object[] array = stringsLists;
List<Integer> numbersList = Arrays.asList(66,22,44,12);
array[0] = numbersList;
String str = stringsLists[0].get(0);
}
public static void main(String[] args) {
List<String> cars1 = Arrays.asList("Ford", "Fiat", "Kia");
List<String> cars2 = Arrays.asList("Ferrari", "Bugatti", "Zaporozhets");
polluteHeap(cars1, cars2);
}
}
Co tu się dzieje? Ze względu na wymazanie typu, nasz argument o zmiennej długości
List<String>...stringsLists
staje się tablicą list, tj List[]
. obiektów nieznanego typu (nie zapomnij, że varargs podczas kompilacji zamienia się w zwykłą tablicę). Dzięki temu możemy łatwo przypisać go do Object[] array
zmiennej w pierwszym wierszu metody — typ obiektów na naszych listach został wymazany! I teraz mamy Object[]
zmienną, do której możemy dodać wszystko, ponieważ wszystkie obiekty w Javie dziedziczą Object
! Na początku mamy tylko tablicę list ciągów znaków. Ale dzięki wymazywaniu czcionek i użyciu przez nas varargów możemy łatwo dodać listę liczb, co też robimy. W rezultacie zanieczyszczamy stertę, mieszając obiekty różnych typów. Wynik będzie jeszcze inny ClassCastException
, gdy spróbujemy odczytać ciąg znaków z tablicy. Wyjście konsoli:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Takie nieoczekiwane konsekwencje może wywołać użycie varargs, pozornie prostego mechanizmu :) I na tym dzisiejsza lekcja dobiega końca. Nie zapomnij rozwiązać kilku zadań, a jeśli masz czas i energię, przeczytaj dodatkowe lektury. „ Efektywna Java ” sama się nie przeczyta! :) Do następnego razu!
GO TO FULL VERSION