Java Language
Generics
Szukaj…
Wprowadzenie
Generics to funkcja ogólnego programowania, która rozszerza system typów Java, aby umożliwić typowi lub metodzie działanie na obiektach różnego typu, zapewniając jednocześnie bezpieczeństwo typu kompilacji. W szczególności struktura kolekcji Java obsługuje funkcje ogólne, aby określić typ obiektów przechowywanych w instancji kolekcji.
Składnia
- class ArrayList <E> {} // klasa ogólna z parametrem typu E
- klasa HashMap <K, V> {} // klasa ogólna z dwoma parametrami typu K i V
- <E> void print (element E) {} // ogólna metoda z parametrem typu E
- ArrayList <String> names; // deklaracja klasy ogólnej
- Obiekty ArrayList <?>; // deklaracja klasy ogólnej z nieznanym parametrem typu
- new ArrayList <String> () // tworzenie instancji klasy ogólnej
- nowa instancja ArrayList <> () // z wnioskiem typu „diamond” (Java 7 lub nowsza)
Uwagi
Generyczne są zaimplementowane w Javie poprzez usuwanie typu, co oznacza, że podczas działania informacje o typie określone w wystąpieniu klasy ogólnej nie są dostępne. Na przykład instrukcja List<String> names = new ArrayList<>();
tworzy obiekt listy, z którego nie można odzyskać typu elementu String
w czasie wykonywania. Jeśli jednak lista jest przechowywana w polu typu List<String>
lub przekazana do parametru metody / konstruktora tego samego typu lub zwrócona z metody tego typu zwracanego, wówczas pełne informacje o typie można odzyskać w czasie wykonywania poprzez interfejs API Java Reflection.
Oznacza to również, że podczas rzutowania na typ ogólny (np. (List<String>) list
) rzutowanie jest rzutem niezaznaczonym . Ponieważ parametr <String>
został usunięty, JVM nie może sprawdzić, czy rzutowanie z List<?>
Na List<String>
jest poprawne; JVM widzi tylko rzutowanie dla List
do List
w czasie wykonywania.
Tworzenie klasy ogólnej
Ogólne pozwalają klasom, interfejsom i metodom przyjmować inne klasy i interfejsy jako parametry typu.
W tym przykładzie użyto klasy ogólnej Param
do pobrania parametru jednego typu T
, ograniczonego nawiasami kątowymi ( <>
):
public class Param<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Aby utworzyć instancję tej klasy, podaj argument typu zamiast T
Na przykład liczba Integer
:
Param<Integer> integerParam = new Param<Integer>();
Argumentem typu może być dowolny typ odwołania, w tym tablice i inne typy ogólne:
Param<String[]> stringArrayParam;
Param<int[][]> int2dArrayParam;
Param<Param<Object>> objectNestedParam;
W Javie SE 7 i nowszych argument typu można zastąpić pustym zestawem argumentów typu ( <>
) o nazwie diament :
Param<Integer> integerParam = new Param<>();
W przeciwieństwie do innych identyfikatorów parametry typu nie mają żadnych ograniczeń nazewnictwa. Jednak ich nazwy są zwykle pierwszą literą ich celu dużymi literami. (Dotyczy to nawet oficjalnych JavaDocs.)
Przykłady obejmują T
dla „typu” , E
dla „elementu” i K
/ V
dla „klucza” / „wartości” .
Rozszerzanie klasy ogólnej
public abstract class AbstractParam<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
AbstractParam
to klasa abstrakcyjna zadeklarowana za pomocą parametru typu T
Podczas rozszerzania tej klasy ten parametr typu można zastąpić argumentem typu zapisanym wewnątrz <>
lub parametr typu może pozostać niezmieniony. W pierwszym i drugim przykładzie poniżej String
i liczba Integer
zastępują parametr type. W trzecim przykładzie parametr type pozostaje niezmieniony. Czwarty przykład w ogóle nie używa ogólnych, więc jest podobny do tego, czy klasa ma parametr Object
. Kompilator będzie ostrzegał przed AbstractParam
typem ObjectParam
, ale skompiluje klasę ObjectParam
. Piąty przykład ma 2 parametry typu (patrz „parametry wielu typów” poniżej), wybierając drugi parametr jako parametr typu przekazywany do nadklasy.
public class Email extends AbstractParam<String> {
// ...
}
public class Age extends AbstractParam<Integer> {
// ...
}
public class Height<T> extends AbstractParam<T> {
// ...
}
public class ObjectParam extends AbstractParam {
// ...
}
public class MultiParam<T, E> extends AbstractParam<E> {
// ...
}
Oto użycie:
Email email = new Email();
email.setValue("[email protected]");
String retrievedEmail = email.getValue();
Age age = new Age();
age.setValue(25);
Integer retrievedAge = age.getValue();
int autounboxedAge = age.getValue();
Height<Integer> heightInInt = new Height<>();
heightInInt.setValue(125);
Height<Float> heightInFloat = new Height<>();
heightInFloat.setValue(120.3f);
MultiParam<String, Double> multiParam = new MultiParam<>();
multiParam.setValue(3.3);
Zauważ, że w klasie Email
metoda T getValue()
działa tak, jakby miała podpis String getValue()
, a void setValue(T)
działa tak, jakby została zadeklarowana jako void setValue(String)
.
Możliwe jest także tworzenie instancji z anonimową klasą wewnętrzną z pustymi nawiasami klamrowymi ( {}
):
AbstractParam<Double> height = new AbstractParam<Double>(){};
height.setValue(198.6);
Pamiętaj, że użycie diamentu z anonimowymi klasami wewnętrznymi jest niedozwolone.
Parametry wielu typów
Java zapewnia możliwość użycia więcej niż jednego parametru typu w ogólnej klasie lub interfejsie. W klasie lub interfejsie można zastosować wiele parametrów typu, umieszczając listę typów oddzieloną przecinkami między nawiasami kątowymi. Przykład:
public class MultiGenericParam<T, S> {
private T firstParam;
private S secondParam;
public MultiGenericParam(T firstParam, S secondParam) {
this.firstParam = firstParam;
this.secondParam = secondParam;
}
public T getFirstParam() {
return firstParam;
}
public void setFirstParam(T firstParam) {
this.firstParam = firstParam;
}
public S getSecondParam() {
return secondParam;
}
public void setSecondParam(S secondParam) {
this.secondParam = secondParam;
}
}
Użycie można wykonać w następujący sposób:
MultiGenericParam<String, String> aParam = new MultiGenericParam<String, String>("value1", "value2");
MultiGenericParam<Integer, Double> dayOfWeekDegrees = new MultiGenericParam<Integer, Double>(1, 2.6);
Deklaracja metody ogólnej
Metody mogą również mieć ogólne parametry typu.
public class Example {
// The type parameter T is scoped to the method
// and is independent of type parameters of other methods.
public <T> List<T> makeList(T t1, T t2) {
List<T> result = new ArrayList<T>();
result.add(t1);
result.add(t2);
return result;
}
public void usage() {
List<String> listString = makeList("Jeff", "Atwood");
List<Integer> listInteger = makeList(1, 2);
}
}
Zauważ, że nie musimy przekazywać argumentu typu rzeczywistego do metody ogólnej. Kompilator określa dla nas argument typu na podstawie typu docelowego (np. Zmiennej, do której przypisujemy wynik) lub na podstawie typów rzeczywistych argumentów. Generalnie wywnioskuje najbardziej specyficzny argument typu, który sprawi, że wywołanie będzie prawidłowe.
Czasami, choć rzadko, konieczne może być zastąpienie wnioskowania tego typu jawnymi argumentami typu:
void usage() {
consumeObjects(this.<Object>makeList("Jeff", "Atwood").stream());
}
void consumeObjects(Stream<Object> stream) { ... }
Jest to konieczne w tym przykładzie, ponieważ kompilator nie może „patrzeć w przyszłość”, aby zobaczyć, że Object
jest pożądany dla T
po wywołaniu stream()
a w przeciwnym razie makeList
String
oparciu o argumenty makeList
. Należy zauważyć, że język Java nie obsługuje pomijając klasę lub obiekt, w którym metoda jest wywoływana ( this
w powyższym przykładzie), gdy argumenty typu są wyraźnie przewidziane.
Diament
Java 7 wprowadziła Diamond 1, aby usunąć płytę kotła wokół instancji klasy ogólnej. Za pomocą Java 7+ możesz pisać:
List<String> list = new LinkedList<>();
Tam, gdzie musiałeś pisać w poprzednich wersjach:
List<String> list = new LinkedList<String>();
Jedno ograniczenie dotyczy klas anonimowych , w których nadal należy podać parametr type w instancji:
// This will compile:
Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
// But this will not:
Comparator<String> caseInsensitiveComparator = new Comparator<>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
Chociaż używanie diamentu z Anonimowymi klasami wewnętrznymi nie jest obsługiwane w Javie 7 i 8, zostanie ono uwzględnione jako nowa funkcja w Javie 9 .
Notatka:
1 - Niektórzy nazywają użycie <>
„ operatorem diamentowym”. To jest niepoprawne. Diament nie zachowuje się jak operator i nie jest opisany ani wymieniony nigdzie w JLS ani (oficjalnych) samouczkach Java jako operator. Rzeczywiście, <>
nie jest nawet wyraźnym tokenem Java. Raczej jest to <
token, po którym następuje >
token, i dozwolone jest (choć zły styl) umieszczanie białych znaków lub komentarzy między nimi. JLS i samouczki konsekwentnie określają <>
jako „diament”, a zatem jest to właściwy termin na to określenie.
Wymaganie wielu górnych granic („rozszerza A i B”)
Możesz wymagać typu ogólnego, aby rozszerzyć wiele górnych granic.
Przykład: chcemy posortować listę liczb, ale Number
nie implementuje Comparable
.
public <T extends Number & Comparable<T>> void sortNumbers( List<T> n ) {
Collections.sort( n );
}
W tym przykładzie T
musi rozszerzyć Number
i zaimplementować Comparable<T>
który powinien pasować do wszystkich „normalnych” wbudowanych implementacji liczb, takich jak Integer
lub BigDecimal
ale nie pasuje do bardziej egzotycznych, takich jak Striped64
.
Ponieważ wielokrotne dziedziczenie nie jest dozwolone, możesz użyć maksymalnie jednej klasy jako powiązania i musi to być pierwsza z wymienionych. Na przykład <T extends Comparable<T> & Number>
nie jest dozwolone, ponieważ Porównywalny jest interfejsem, a nie klasą.
Tworzenie ograniczonej klasy ogólnej
Można ograniczyć prawidłowe typy używane w klasie ogólnej , ograniczając ten typ w definicji klasy. Biorąc pod uwagę następującą prostą hierarchię typów:
public abstract class Animal {
public abstract String getSound();
}
public class Cat extends Animal {
public String getSound() {
return "Meow";
}
}
public class Dog extends Animal {
public String getSound() {
return "Woof";
}
}
Bez ograniczeń ogólnych nie możemy stworzyć klasy kontenerowej, która będzie zarówno ogólna, jak i wie, że każdy element jest zwierzęciem:
public class AnimalContainer<T> {
private Collection<T> col;
public AnimalContainer() {
col = new ArrayList<T>();
}
public void add(T t) {
col.add(t);
}
public void printAllSounds() {
for (T t : col) {
// Illegal, type T doesn't have makeSound()
// it is used as an java.lang.Object here
System.out.println(t.makeSound());
}
}
}
Z ogólną definicją klasy jest to teraz możliwe.
public class BoundedAnimalContainer<T extends Animal> { // Note bound here.
private Collection<T> col;
public BoundedAnimalContainer() {
col = new ArrayList<T>();
}
public void add(T t) {
col.add(t);
}
public void printAllSounds() {
for (T t : col) {
// Now works because T is extending Animal
System.out.println(t.makeSound());
}
}
}
Ogranicza to również prawidłowe instancje typu ogólnego:
// Legal
AnimalContainer<Cat> a = new AnimalContainer<Cat>();
// Legal
AnimalContainer<String> a = new AnimalContainer<String>();
// Legal because Cat extends Animal
BoundedAnimalContainer<Cat> b = new BoundedAnimalContainer<Cat>();
// Illegal because String doesn't extends Animal
BoundedAnimalContainer<String> b = new BoundedAnimalContainer<String>();
Decydujesz pomiędzy „T”, „? super T` i `? rozszerza T`
Składnia symboli ogólnych ograniczonych przez język Java reprezentujących nieznany typ przez ?
jest:
? extends T
oznacza górną granicę znaku wieloznacznego. Nieznany typ reprezentuje typ, który musi być podtypem T lub samym typem T.? super T
oznacza symbol wieloznaczny z dolną granicą. Nieznany typ reprezentuje typ, który musi być nadtypem T lub samym typem T.
Zasadniczo powinieneś używać
-
? extends T
jeśli potrzebujesz tylko dostępu „do odczytu” („wejście”) -
? super T
jeśli potrzebujesz dostępu „do zapisu” („wyjście”) -
T
jeśli potrzebujesz obu („modyfikuj”)
Używanie extends
lub super
jest zwykle lepsze, ponieważ sprawia, że twój kod jest bardziej elastyczny (jak w: zezwalanie na użycie podtypów i nadtypów), jak zobaczysz poniżej.
class Shoe {}
class IPhone {}
interface Fruit {}
class Apple implements Fruit {}
class Banana implements Fruit {}
class GrannySmith extends Apple {}
public class FruitHelper {
public void eatAll(Collection<? extends Fruit> fruits) {}
public void addApple(Collection<? super Apple> apples) {}
}
Kompilator będzie mógł teraz wykryć pewne nieprawidłowe użycie:
public class GenericsTest {
public static void main(String[] args){
FruitHelper fruitHelper = new FruitHelper() ;
List<Fruit> fruits = new ArrayList<Fruit>();
fruits.add(new Apple()); // Allowed, as Apple is a Fruit
fruits.add(new Banana()); // Allowed, as Banana is a Fruit
fruitHelper.addApple(fruits); // Allowed, as "Fruit super Apple"
fruitHelper.eatAll(fruits); // Allowed
Collection<Banana> bananas = new ArrayList<>();
bananas.add(new Banana()); // Allowed
//fruitHelper.addApple(bananas); // Compile error: may only contain Bananas!
fruitHelper.eatAll(bananas); // Allowed, as all Bananas are Fruits
Collection<Apple> apples = new ArrayList<>();
fruitHelper.addApple(apples); // Allowed
apples.add(new GrannySmith()); // Allowed, as this is an Apple
fruitHelper.eatAll(apples); // Allowed, as all Apples are Fruits.
Collection<GrannySmith> grannySmithApples = new ArrayList<>();
fruitHelper.addApple(grannySmithApples); //Compile error: Not allowed.
// GrannySmith is not a supertype of Apple
apples.add(new GrannySmith()); //Still allowed, GrannySmith is an Apple
fruitHelper.eatAll(grannySmithApples);//Still allowed, GrannySmith is a Fruit
Collection<Object> objects = new ArrayList<>();
fruitHelper.addApple(objects); // Allowed, as Object super Apple
objects.add(new Shoe()); // Not a fruit
objects.add(new IPhone()); // Not a fruit
//fruitHelper.eatAll(objects); // Compile error: may contain a Shoe, too!
}
Wybierając odpowiedni T
? super T
czy ? extends T
jest konieczne, aby umożliwić użycie z podtypami. Kompilator może następnie zapewnić bezpieczeństwo typu; nie powinno być potrzeby przesyłania (co nie jest bezpieczne dla typu i może powodować błędy programowania), jeśli używasz ich poprawnie.
Jeśli nie jest to łatwe do zrozumienia, pamiętaj o regule PECS :
P roducer xtends zastosowania "E" i C onsumer zastosowań uper "S".
(Producent ma tylko dostęp do zapisu, a konsument ma tylko dostęp do odczytu)
Korzyści z ogólnej klasy i interfejsu
Kod korzystający z generycznych ma wiele zalet w porównaniu z kodem innym niż ogólny. Poniżej znajdują się główne zalety
Silniejsze kontrole typu w czasie kompilacji
Kompilator Java stosuje silne sprawdzanie typu do kodu generycznego i generuje błędy, jeśli kod narusza bezpieczeństwo typu. Naprawianie błędów czasu kompilacji jest łatwiejsze niż naprawianie błędów środowiska wykonawczego, które mogą być trudne do znalezienia.
Eliminacja obsad
Poniższy fragment kodu bez elementów ogólnych wymaga rzutowania:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
Po ponownym napisaniu w celu użycia ogólnych , kod nie wymaga rzutowania:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // no cast
Umożliwianie programistom implementacji ogólnych algorytmów
Używając ogólnych, programiści mogą implementować ogólne algorytmy, które działają na kolekcjach różnych typów, można je dostosowywać, są bezpieczne i łatwiejsze do odczytania.
Wiązanie parametru ogólnego z więcej niż 1 typem
Parametry ogólne można również powiązać z więcej niż jednym typem za pomocą T extends Type1 & Type2 & ...
składnię T extends Type1 & Type2 & ...
Załóżmy, że chcesz utworzyć klasę, której typ ogólny powinien implementować zarówno Flushable
jak i Closeable
, możesz pisać
class ExampleClass<T extends Flushable & Closeable> {
}
Teraz ExampleClass
akceptuje tylko parametry ogólne, typy, które implementują zarówno Flushable
jak i Closeable
.
ExampleClass<BufferedWriter> arg1; // Works because BufferedWriter implements both Flushable and Closeable
ExampleClass<Console> arg4; // Does NOT work because Console only implements Flushable
ExampleClass<ZipFile> arg5; // Does NOT work because ZipFile only implements Closeable
ExampleClass<Flushable> arg2; // Does NOT work because Closeable bound is not satisfied.
ExampleClass<Closeable> arg3; // Does NOT work because Flushable bound is not satisfied.
Metody klasy mogą wnioskować o argumentach typu ogólnego jako Closeable
lub Flushable
.
class ExampleClass<T extends Flushable & Closeable> {
/* Assign it to a valid type as you want. */
public void test (T param) {
Flushable arg1 = param; // Works
Closeable arg2 = param; // Works too.
}
/* You can even invoke the methods of any valid type directly. */
public void test2 (T param) {
param.flush(); // Method of Flushable called on T and works fine.
param.close(); // Method of Closeable called on T and works fine too.
}
}
Uwaga:
Nie można powiązać parametru ogólnego z żadnym z typów za pomocą klauzuli OR ( |
). Obsługiwana jest tylko klauzula AND ( &
). Typ ogólny może obejmować tylko jedną klasę i wiele interfejsów. Klasa musi być umieszczona na początku listy.
Tworzenie wystąpienia typu ogólnego
Z powodu usunięcia typu następujące elementy nie będą działać:
public <T> void genericMethod() {
T t = new T(); // Can not instantiate the type T.
}
Typ T
zostaje skasowany. Ponieważ w środowisku wykonawczym JVM nie wie, co pierwotnie było T
, nie wie, który konstruktor ma zostać wywołany.
Obejścia
Przekazywanie klasy
T
podczas wywoływaniagenericMethod
:public <T> void genericMethod(Class<T> cls) { try { T t = cls.newInstance(); } catch (InstantiationException | IllegalAccessException e) { System.err.println("Could not instantiate: " + cls.getName()); } }
genericMethod(String.class);
Który zgłasza wyjątki, ponieważ nie ma sposobu, aby dowiedzieć się, czy przekazana klasa ma dostępny domyślny konstruktor.
Przekazywanie odwołania do konstruktora
T
:public <T> void genericMethod(Supplier<T> cons) { T t = cons.get(); }
genericMethod(String::new);
Odwoływanie się do zadeklarowanego typu ogólnego w ramach własnej deklaracji
Jak zabierasz się do korzystania z instancji (ewentualnie dalszego) odziedziczonego typu ogólnego w deklaracji metody w deklarowanym typie ogólnym? Jest to jeden z problemów, z którym będziesz musiał się zmierzyć, gdy zagłębisz się nieco w generyczne, ale wciąż dość powszechny.
Załóżmy, że mamy DataSeries<T>
(tutaj interfejs), który definiuje ogólną serię danych zawierającą wartości typu T
Uciążliwa jest bezpośrednia praca z tym typem, gdy chcemy wykonać wiele operacji, np. Z podwójnymi wartościami, dlatego definiujemy DoubleSeries extends DataSeries<Double>
. Załóżmy teraz, że oryginalny DataSeries<T>
ma metodę add(values)
która dodaje kolejną serię o tej samej długości i zwraca nową. W jaki sposób DoubleSeries
typ values
i typ zwracanej wartości w DoubleSeries
zamiast DataSeries<Double>
w naszej klasie pochodnej?
Problem można rozwiązać, dodając ogólny parametr typu odwołujący się i rozszerzający deklarowany typ (stosowany tutaj do interfejsu, ale to samo oznacza klasy):
public interface DataSeries<T, DS extends DataSeries<T, DS>> {
DS add(DS values);
List<T> data();
}
Tutaj T
reprezentuje typ danych, który przechowuje seria, np. Double
i DS
sama seria. Dziedziczony typ (lub typy) można teraz łatwo wdrożyć, zastępując wyżej wspomniany parametr odpowiednim typem pochodnym, uzyskując w ten sposób konkretną Double
definicję formy:
public interface DoubleSeries extends DataSeries<Double, DoubleSeries> {
static DoubleSeries instance(Collection<Double> data) {
return new DoubleSeriesImpl(data);
}
}
W tej chwili nawet IDE zaimplementuje powyższy interfejs z poprawnymi typami, które po odrobinie zawartości mogą wyglądać następująco:
class DoubleSeriesImpl implements DoubleSeries {
private final List<Double> data;
DoubleSeriesImpl(Collection<Double> data) {
this.data = new ArrayList<>(data);
}
@Override
public DoubleSeries add(DoubleSeries values) {
List<Double> incoming = values != null ? values.data() : null;
if (incoming == null || incoming.size() != data.size()) {
throw new IllegalArgumentException("bad series");
}
List<Double> newdata = new ArrayList<>(data.size());
for (int i = 0; i < data.size(); i++) {
newdata.add(this.data.get(i) + incoming.get(i)); // beware autoboxing
}
return DoubleSeries.instance(newdata);
}
@Override
public List<Double> data() {
return Collections.unmodifiableList(data);
}
}
Jak widać metoda add
jest zadeklarowana jako DoubleSeries add(DoubleSeries values)
i kompilator jest szczęśliwy.
W razie potrzeby wzór można zagnieżdżać.
Zastosowanie instanceof z Generics
Użycie ogólnych do zdefiniowania typu w instanceof
Rozważ następującą klasę ogólną Example
zadeklarowany za pomocą parametru formalnego <T>
:
class Example<T> {
public boolean isTypeAString(String s) {
return s instanceof T; // Compilation error, cannot use T as class type here
}
}
To zawsze powoduje błąd kompilacji, ponieważ gdy tylko kompilator skompiluje źródło Java do kodu bajtowego Java , stosuje proces znany jako kasowanie typu , który konwertuje cały kod ogólny na kod nieogólny, uniemożliwiając rozróżnienie typów T w czasie wykonywania. Typ użyty w instanceof
musi być możliwy do ponownego sprawdzenia , co oznacza, że wszystkie informacje o typie muszą być dostępne w czasie wykonywania, a zwykle nie dotyczy to typów ogólnych.
Poniższa klasa przedstawia, jak wyglądają dwie różne klasy Example
, Example<String>
i Example<Number>
, po usunięciu generycznych typów według skasowania typu :
class Example { // formal parameter is gone
public boolean isTypeAString(String s) {
return s instanceof Object; // Both <String> and <Number> are now Object
}
}
Ponieważ typy zniknęły, JVM nie może wiedzieć, który typ to T
Wyjątek od poprzedniej reguły
Zawsze możesz użyć niezwiązanego znaku wieloznacznego (?) Do określenia typu w instanceof
w następujący sposób:
public boolean isAList(Object obj) {
return obj instanceof List<?>;
}
Może to być przydatne do oceny, czy obj
jest List
czy nie:
System.out.println(isAList("foo")); // prints false
System.out.println(isAList(new ArrayList<String>()); // prints true
System.out.println(isAList(new ArrayList<Float>()); // prints true
W rzeczywistości niezwiązany symbol wieloznaczny jest uważany za typ podlegający zwrotowi.
Używanie ogólnej instancji z instanceof
Druga strona medalu polega na tym, że użycie instancji t
of T
z instanceof
jest legalne, jak pokazano w poniższym przykładzie:
class Example<T> {
public boolean isTypeAString(T t) {
return t instanceof String; // No compilation error this time
}
}
ponieważ po usunięciu typu klasa będzie wyglądać następująco:
class Example { // formal parameter is gone
public boolean isTypeAString(Object t) {
return t instanceof String; // No compilation error this time
}
}
Ponieważ nawet jeśli usunięcie typu i tak się zdarzy, teraz JVM może rozróżniać różne typy w pamięci, nawet jeśli używają tego samego typu odwołania ( Object
), jak pokazano w poniższym fragmencie:
Object obj1 = new String("foo"); // reference type Object, object type String
Object obj2 = new Integer(11); // reference type Object, object type Integer
System.out.println(obj1 instanceof String); // true
System.out.println(obj2 instanceof String); // false, it's an Integer, not a String
Różne sposoby implementacji interfejsu ogólnego (lub rozszerzenia klasy ogólnej)
Załóżmy, że zadeklarowano następujący ogólny interfejs:
public interface MyGenericInterface<T> {
public void foo(T t);
}
Poniżej wymieniono możliwe sposoby jego wdrożenia.
Nietypowa implementacja klasy z określonym typem
Wybierz konkretny typ, aby zastąpić parametr typu formalnego <T>
MyGenericClass
i zaimplementuj go, jak w poniższym przykładzie:
public class NonGenericClass implements MyGenericInterface<String> {
public void foo(String t) { } // type T has been replaced by String
}
Ta klasa zajmuje się tylko String
, a to oznacza, że za pomocą MyGenericInterface
z różnymi parametrami (np Integer
, Object
itd.) Nie zostanie skompilowany, jak ilustruje to poniższy fragment kodu:
NonGenericClass myClass = new NonGenericClass();
myClass.foo("foo_string"); // OK, legal
myClass.foo(11); // NOT OK, does not compile
myClass.foo(new Object()); // NOT OK, does not compile
Implementacja klasy ogólnej
Zadeklaruj inny ogólny interfejs za pomocą parametru typu formalnego <T>
który implementuje MyGenericInterface
, w następujący sposób:
public class MyGenericSubclass<T> implements MyGenericInterface<T> {
public void foo(T t) { } // type T is still the same
// other methods...
}
Zauważ, że mógł zostać użyty inny parametr typu formalnego, jak następuje:
public class MyGenericSubclass<U> implements MyGenericInterface<U> { // equivalent to the previous declaration
public void foo(U t) { }
// other methods...
}
Implementacja klasy typu surowego
Zadeklaruj nie-ogólną klasę, która implementuje MyGenericInteface
jako typ surowy (w ogóle nie używając ogólnego), w następujący sposób:
public class MyGenericSubclass implements MyGenericInterface {
public void foo(Object t) { } // type T has been replaced by Object
// other possible methods
}
Ta metoda nie jest zalecana, ponieważ nie jest w 100% bezpieczna w środowisku wykonawczym, ponieważ łączy w sobie surowy typ (podklasy) z generycznymi (interfejsu) i jest również myląca. Nowoczesne kompilatory Java wygenerują ostrzeżenie przy tego rodzaju implementacji, jednak kod - ze względu na kompatybilność ze starszą JVM (1.4 lub wcześniejszą) - zostanie skompilowany.
Wszystkie powyższe sposoby są również dozwolone, gdy używa się klasy ogólnej jako nadtypu zamiast interfejsu ogólnego.
Używanie Generics do automatycznego rzutowania
W przypadku generycznych możliwe jest zwrócenie wszystkiego, czego oczekuje dzwoniący:
private Map<String, Object> data;
public <T> T get(String key) {
return (T) data.get(key);
}
Metoda zostanie skompilowana z ostrzeżeniem. Kod jest w rzeczywistości bezpieczniejszy niż się wydaje, ponieważ środowisko wykonawcze Java wykona rzutowanie, gdy go użyjesz:
Bar bar = foo.get("bar");
Jest mniej bezpieczny, gdy używasz typów ogólnych:
List<Bar> bars = foo.get("bars");
W tym przypadku rzutowanie będzie działać, gdy zwracanym typem jest dowolna List
(tzn. ClassCastException
List<String>
nie wyzwalałoby ClassCastException
; w końcu można go uzyskać, gdy elementy zostaną usunięte z listy).
Aby obejść ten problem, możesz utworzyć interfejs API, który używa wpisywanych klawiszy:
public final static Key<List<Bar>> BARS = new Key<>("BARS");
wraz z tą metodą put()
:
public <T> T put(Key<T> key, T value);
Przy takim podejściu nie można umieścić niewłaściwego typu na mapie, więc wynik zawsze będzie poprawny (chyba że przypadkowo utworzysz dwa klucze o tej samej nazwie, ale różnych typach).
Związane z:
Uzyskaj klasę spełniającą ogólny parametr w czasie wykonywania
Wiele niezwiązanych parametrów ogólnych, takich jak te stosowane w metodzie statycznej, nie można odzyskać w czasie wykonywania (patrz Inne wątki dotyczące usuwania ). Istnieje jednak wspólna strategia dostępu do typu spełniającego ogólny parametr w klasie w czasie wykonywania. Pozwala to na ogólny kod, który zależy od dostępu do typu bez konieczności podawania informacji o typie wątku przy każdym wywołaniu.
tło
Ogólną parametryzację klasy można sprawdzić, tworząc anonimową klasę wewnętrzną. Ta klasa przechwytuje informacje o typie. Zasadniczo mechanizm ten nazywany jest tokenami supertypowymi, opisanymi szczegółowo w poście na blogu Neala Gaftera .
Realizacje
Trzy popularne implementacje w Javie to:
Przykładowe użycie
public class DataService<MODEL_TYPE> {
private final DataDao dataDao = new DataDao();
private final Class<MODEL_TYPE> type = (Class<MODEL_TYPE>) new TypeToken<MODEL_TYPE>
(getClass()){}.getRawType();
public List<MODEL_TYPE> getAll() {
return dataDao.getAllOfType(type);
}
}
// the subclass definitively binds the parameterization to User
// for all instances of this class, so that information can be
// recovered at runtime
public class UserService extends DataService<User> {}
public class Main {
public static void main(String[] args) {
UserService service = new UserService();
List<User> users = service.getAll();
}
}