Programowanie ogólne w Javie
Programowanie generyczne w Javie stałosię możliwedzięki tak zwanym rodzajom generycznym od czasu Java 1.5. Termin jest synonimem „typów sparametryzowanych”. Ideą jest wprowadzenie dodatkowych zmiennych dla typów. Te zmienne typu reprezentują nieznane typy w momencie implementacji. Te zmienne typu są zastępowane przez określone typy tylko wtedy, gdy używane są klasy, interfejsy i metody. W ten sposób można zwykle zagwarantować bezpieczne programowanie. Ale nie zawsze.
Koncepcja
Począwszy od wersji 5.0 („Tiger”, opublikowanej w 2004 r.), Język programowania Java udostępnia również typy ogólne , czyli środki składniowe do programowania ogólnego . Pozwala to na sparametryzowanie klas i metod (metod również niezależnych od ich klas) za pomocą typów . Otwiera to pewne podobne możliwości dla języka, które są porównywalne z szablonami w C ++ .
Zasadniczo jednak istnieją znaczne różnice. Podczas Java Parametr typ jest parametryzowana poprzez interfejs , C ++ parametryzacja jest przeprowadzane bezpośrednio za pomocą typu parametru typu siebie. Kod źródłowy szablonu C ++ musi być dostępny dla użytkownika (tj. Gdy wstawiony jest parametr typu), podczas gdy ogólny typ Java może być również opublikowany jako przetłumaczony kod bajtowy. Kompilator tworzy zduplikowany kod docelowy dla różnych specjalnie używanych parametrów typu.
Na przykład funkcja std :: sort w C ++ oferuje możliwość sortowania wszystkich kontenerów, które oferują określone metody (tutaj zwłaszcza begin () i end (), z których każdy zwraca iterator) i których parametry typu implementują 'operator <' (lub jawnie określono inną funkcję porównawczą). Wadą tego systemu jest to, że jest trudniejszy (dla programisty!) Do tłumaczenia. Kompilator nie ma innej opcji niż zastąpienie parametru typu w każdym przypadku wymaganym określonym typem i ponowna kompilacja całego kodu.
W przypadku niewłaściwych parametrów typu i innych problemów, bardzo łatwo mogą pojawić się skomplikowane i niezrozumiałe komunikaty kompilatora, co jest po prostu związane z tym, że nie są znane konkretne wymagania dotyczące parametrów typu. Praca z szablonami C ++ wymaga zatem pełnej dokumentacji wymagań dla parametru typu. Dzięki metaprogramowaniu szablonów większość wymagań (klasa bazowa, dostępność metod, kopiowalność, przypisywalność itp.) Można również odpytywać w specjalnych konstrukcjach, co skutkuje bardziej czytelnymi komunikatami o błędach. Mimo że są zgodne ze standardami, te konstrukcje nie są obsługiwane przez wszystkie kompilatory .
Natomiast ogólne klasy i metody w Javie są świadome ograniczeń dotyczących ich własnych parametrów typu. Aby posortować kolekcję (bez komparatora), elementy, które zawiera, muszą być typu porównywalnego, czyli mieć zaimplementowany ten interfejs. Kompilator musi tylko sprawdzić, czy parametr typu jest podtypem Porównywalny, a tym samym może zapewnić, że kod jest poprawny (tj. Dostępna jest wymagana metoda compareTo). Co więcej, jeden i ten sam kod jest używany dla wszystkich określonych typów i nie jest powielany za każdym razem.
Przydatne przykłady
Program używa jednego do przechowywania ArrayListlisty JButtons.
Do tej pory ArrayList został naprawiony na typie Object :
List list = new ArrayList();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));
for (int i = 0; i < list.size(); i++) {
JButton button = (JButton) list.get(i);
button.setBackground(Color.white);
}
Zwróć uwagę na niezbędną jawną konwersję typu (zwaną również „rzutowaniem”) i związaną z nią niepewność typu. Możesz nieumyślnie ArrayListzapisać obiekt w nie będącym instancją klasy JButton. Informacje o dokładnym rodzaju jest po włożeniu do listy stracony, więc kompilator nie można zapobiec przy starcie, gdy jednoznaczne przekształcenie typ JButtonjednego ClassCastExceptionwystąpienia.
W przypadku typów ogólnych w języku Java można wykonać następujące czynności:
List<JButton> list = new ArrayList<JButton>();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));
for (int i = 0; i < list.size(); i++)
list.get(i).setBackground(Color.white);
Podczas odczytu nie jest już konieczna jawna konwersja typu, podczas zapisywania możliwe jest tylko przechowywanie JButtons na ArrayList liście .
Począwszy od Java7, tworzenie instancji typów ogólnych zostało uproszczone. Pierwszą linię w powyższym przykładzie można zapisać w następujący sposób od wersji Java 7:
List<JButton> list = new ArrayList<>();
Dzięki połączeniu typów ogólnych z rozszerzonymi pętlami for, powyższy przykład może być bardziej zwięzły:
List<JButton> list = new ArrayList<>();
list.add(new JButton("Button 1"));
list.add(new JButton("Button 2"));
list.add(new JButton("Button 3"));
list.add(new JButton("Button 4"));
list.add(new JButton("Button 5"));
for (JButton b: list)
b.setBackground(Color.white);
Poniższy przykładowy kod przedstawia przykład klasy ogólnej, która zawiera dwa obiekty dowolnego typu, ale tego samego typu:
public class DoubleObject<T> {
private T object1;
private T object2;
public DoubleObject(T object1, T object2) {
this.object1 = object1;
this.object2 = object2;
}
public String toString() {
return this.object1 + ", " + this.object2;
}
public static void main(String[] args) {
DoubleObject<String> s = new DoubleObject<>("abc", "def");
DoubleObject<Integer> i = new DoubleObject<>(123, 456);
System.out.println("DoubleObject<String> s=" + s.toString());
System.out.println("DoubleObject<Integer> i=" + i.toString());
}
}
Przypadki wariancji
W Javie można wyróżnić następujące przypadki wariancji. Każdy z nich oferuje całkowicie niezależną elastyczność w radzeniu sobie z typami rodzajowymi i każdy jest całkowicie statycznie bezpieczny.
Niezmienność
W przypadku niezmienności parametr typu jest unikalny. W ten sposób niezmienność zapewnia największą możliwą swobodę podczas używania parametru typu. Na przykład wszystkie akcje są dozwolone dla elementów ArrayList <Integer>, które są również dozwolone w przypadku bezpośredniego używania pojedynczej liczby całkowitej (w tym autoboxing ). Przykład:
List<Integer> list = new ArrayList<Integer>();
// ...
Integer x = list.get(index);
list.get(index).methodeVonInteger();
list.set(index, 98347); // Autoboxing, entspricht Integer.valueOf(98347)
int y = list.get(index); // Auto-Unboxing
Te możliwości są kupowane z niewielką elastycznością w przypisywaniu obiektów samej klasy generycznej. Na przykład niedozwolone jest:
List<Number> list = new ArrayList<Integer>();
mimo że Integer pochodzi od Number. Powodem jest to, że tutaj kompilator nie może już zapewnić, że nie wystąpią żadne błędy typu. Mamy złe doświadczenia z tablicami, które pozwalają na takie przypisanie:
// OK, Integer[] ist abgeleitet von Number[]
Number[] array = new Integer[10];
// ArrayStoreException zur Laufzeit: Double -> Integer sind nicht
// zuweisungskompatibel
array[0] = new Double(5.0);
Kowariancja
Tablice nazywane są kowariantnymi , co oznacza:
lub bardziej ogólnie:
Typ tablicy zachowuje się tak samo, jak parametr typu w odniesieniu do hierarchii dziedziczenia. Kowariancja jest również możliwa w przypadku typów ogólnych, ale tylko z ograniczeniami, aby błędy typu można było wykluczyć w czasie kompilacji.
Potrzebujesz referencji ze składnią ? extends T musi być wyraźnie oznaczony jako kowariantny. T nazywane jest górnym typebound i jest to najbardziej ogólny dozwolony parametr typu.
List<? extends Number> list;
list = new ArrayList<Double>();
list = new ArrayList<Long>();
list = new ArrayList<Integer>();
// Typfehler vom Compiler
list.set(index, myInteger);
// OK aber Warnung vom Compiler: unchecked cast
((List<Integer>) list).set(index, myInteger);
Nie jest możliwe przechowywanie elementów na tych listach, ponieważ, jak opisano powyżej, nie jest to bezpieczne dla typów (wyjątek: można zapisać zero ). Wystąpił błąd w czasie kompilacji. Mówiąc bardziej ogólnie, jest to zadanie
nie dozwolony.
Możliwe jest jednak odczytanie elementów:
Number n = list.get(index); // OK
Integer i = list.get(index); // Typfehler: Es muss sich bei '? extends Number'
// nicht um ein Integer handeln.
Integer j = (Integer) list.get(index); // OK
Przydzial
jest zatem dozwolone, ale nie cesja
Typy generyczne, podobnie jak tablice, oferują zachowanie kowariantne, ale zabraniają wszystkich operacji, które nie są bezpieczne dla typu.
Sprzeczność
Contravariance opisuje zachowanie hierarchii dziedziczenia typu ogólnego w przeciwieństwie do hierarchii jej parametru typu. W przypadku powyższego przykładu oznaczałoby to: lista <Numer> byłaby zgodna z przypisaniem do listy <Podwój>. Odbywa się to w następujący sposób:
List<? super Double> list;
list = new ArrayList<Number>();
list = new ArrayList<Double>();
list = new ArrayList<Object>();
Obiekt, który zachowuje się w sposób kontrawariantny, nie może przyjmować żadnych założeń co do zakresu, w jakim element typu V pochodzi z T, gdzie T jest niższą podstawą typu (w przykładzie ? super Doublejest T Double). Dlatego nie można odczytać powyższych list:
// Fehler: 'list' könnte vom Typ List<Object> sein
Number x = list.get(index);
// Fehler: 'list' könnte List<Object> oder List<Number> sein
Double x = list.get(index);
// Die einzige Ausnahme: Objects sind auf jeden Fall in der Liste
Object x = list.get(index);
Niedozwolone, ponieważ nie są bezpieczne dla typów, więc jest to przypisanie ? super T → (utworzone z Object)
Nietrudno zgadnąć: W zamian element można umieścić na takiej liście:
List<? super Number> list;
list.add(new Double(3.0)); // OK: 'list' hat immer den Typ List<Number>
// oder List<Basisklasse von Number>. Damit
// ist die Zuweisung Double -> T immer erlaubt.
Nieograniczony polimorfizm parametryczny
Wreszcie, leki generyczne oferują całkowicie polimorficzne zachowanie. Nie można tu sformułować żadnego oświadczenia o parametrach typu, ponieważ nie określono limitu w żadnym kierunku. W tym celu zdefiniowano symbol wieloznaczny . Jest reprezentowany przez znak zapytania.
List<?> list;
list = new ArrayList<Integer>();
list = new ArrayList<Object>();
list = new ArrayList<String>();
// ...
Nie można tutaj użyć samego parametru typu, ponieważ żadna instrukcja nie jest możliwa. Dozwolone jest tylko przypisanie T → Obiekt, ponieważ T jest zdecydowanie obiektem. W zamian gwarantujemy, że kod będzie działał ze wszystkimi Ts.
Może to być przydatne, jeśli pracujesz tylko z typem ogólnym:
// Keine Informationen über den Typparameter nötig, kann ''beliebige'' Listen
// aufnehmen.
int readSize(List<?> list) {
return list.size();
}
Aby wyjaśnić, że symbole wieloznaczne są tutaj niepotrzebne i że tak naprawdę nie chodzi o żadną wariancję, podano następującą implementację powyższej funkcji:
<T> int readSize(List<T> list) {
return list.size();
}
linki internetowe
- Ina Brenner: Tutorial on Generics in Java autorstwa autora książki o certyfikacji Java SCJP (niemiecki)
- Klaus Kreft, Angelika Langer: Seria artykułów na temat leków generycznych w Javie w widmie Java (niemiecki)
- Rozdział o ogólnych typach danych z Galileo Openbook Java to również wyspa (niemiecki)