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 :

Java SE 7
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 SE 7

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);
    }
};
Java SE 8

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

  1. Przekazywanie klasy T podczas wywoływania genericMethod :

    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.

Java SE 8
  1. 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();
    }
}


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