Kiedy używać metod ogólnych, a kiedy używać symboli wieloznacznych?

122

Czytam o metodach ogólnych z OracleDocGenericMethod . Jestem dość zdezorientowany porównaniem, kiedy mówi, kiedy użyć symboli wieloznacznych, a kiedy metod ogólnych. Cytat z dokumentu.

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

Mogliśmy zamiast tego użyć metod ogólnych:

interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

[…] To mówi nam, że argument typu jest używany do polimorfizmu; jego jedynym skutkiem jest zezwolenie na użycie różnych rzeczywistych typów argumentów w różnych miejscach wywołań. W takim przypadku należy używać symboli wieloznacznych. Symbole wieloznaczne są przeznaczone do obsługi elastycznych podtypów, co próbujemy tutaj wyrazić.

Czy nie sądzimy, że dzikie karty (Collection<? extends E> c);są również wspierane przez pewien rodzaj polimorfizmu? Dlaczego więc użycie metody ogólnej nie jest dobre w tym?

Kontynuując, stwierdza:

Metody ogólne pozwalają na użycie parametrów typu do wyrażenia zależności między typami co najmniej jednego argumentu metody i / lub jej zwracanego typu. Jeśli nie ma takiej zależności, nie należy używać metody ogólnej.

Co to znaczy?

Przedstawili przykład

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

[…]

Moglibyśmy napisać podpis dla tej metody w inny sposób, bez używania symboli wieloznacznych:

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

Dokument zniechęca do drugiej deklaracji i promuje użycie pierwszej składni? Jaka jest różnica między pierwszą a drugą deklaracją? Wydaje się, że obaj robią to samo?

Czy ktoś może oświetlić ten obszar.

benz
źródło

Odpowiedzi:

173

Są pewne miejsca, w których symbole wieloznaczne i parametry typu robią to samo. Ale są też miejsca, w których trzeba używać parametrów typu.

  1. Jeśli chcesz wymusić jakąś relację na różnych typach argumentów metod, nie możesz tego zrobić za pomocą symboli wieloznacznych, musisz użyć parametrów typu.

Biorąc twoją metodę jako przykład, załóżmy, że chcesz się upewnić, że lista srci destprzekazana do copy()metody powinna być tego samego sparametryzowanego typu, możesz to zrobić z parametrami typu, takimi jak:

public static <T extends Number> void copy(List<T> dest, List<T> src)

Tutaj są zapewnione, że oba desti srcmają taki sam typ dla parametryczne List. Tak więc kopiowanie elementów z srcdo jest bezpieczne dest.

Ale jeśli przejdziesz do zmiany metody na użycie symbolu wieloznacznego:

public static void copy(List<? extends Number> dest, List<? extends Number> src)

nie będzie działać zgodnie z oczekiwaniami. W drugim przypadku możesz przejść List<Integer>i List<Float>jako desti src. Dlatego przenoszenie elementów z srcdo destnie byłoby już bezpieczne. Jeśli nie potrzebujesz tego rodzaju relacji, możesz w ogóle nie używać parametrów typu.

Inne różnice między używaniem symboli wieloznacznych i parametrów typu to:

  • Jeśli masz tylko jeden sparametryzowany argument typu, możesz użyć symbolu wieloznacznego, chociaż parametr typu również będzie działał.
  • Parametry typu obsługują wiele granic, symbole wieloznaczne nie.
  • Symbole wieloznaczne obsługują zarówno górną, jak i dolną granicę, parametry typu obsługują tylko górne granice. Tak więc, jeśli chcesz zdefiniować metodę, która przyjmuje Listtyp Integerlub jego superklasę, możesz zrobić:

    public void print(List<? super Integer> list)  // OK

    ale nie możesz użyć parametru typu:

     public <T super Integer> void print(List<T> list)  // Won't compile

Bibliografia:

Rohit Jain
źródło
1
To dziwna odpowiedź. Nie wyjaśnia, dlaczego ?w ogóle musisz używać . Mógłbyś przepisać to jako `public static <T1 extends Number, T2 extends Number> void copy (List <T1> dest, List <T2> src) iw tym przypadku stało się oczywiste, co się dzieje.
kan
@kan. Cóż, to prawdziwy problem. Możesz użyć parametru typu, aby wymusić ten sam typ, ale nie możesz tego zrobić za pomocą symboli wieloznacznych. Używanie dwóch różnych typów dla parametru typu to inna sprawa.
Rohit Jain
1
@benz. Nie można zdefiniować dolnych granic w Listparametrze using. List<T super Integer>nie jest poprawny i nie skompiluje się.
Rohit Jain
2
@benz. Nie ma za co :) Gorąco zachęcam do skorzystania z linku, który zamieściłem na końcu. To najlepsze źródło informacji na temat leków generycznych, jakie możesz dostać.
Rohit Jain
3
@ jorgen.ringen <T extends X & Y>-> wiele granic.
Rohit Jain
12

Rozważmy następujący przykład z The Java Programming by James Gosling 4th wydanie poniżej, w którym chcemy scalić 2 SinglyLinkQueue:

public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

Obie powyższe metody mają taką samą funkcjonalność. Więc co jest lepsze? Odpowiedź jest druga. Własnymi słowami autora:

„Ogólna zasada polega na używaniu symboli wieloznacznych, gdy jest to możliwe, ponieważ kod zawierający symbole wieloznaczne jest ogólnie bardziej czytelny niż kod z wieloma parametrami typu. Decydując, czy potrzebujesz zmiennej typu, zadaj sobie pytanie, czy ta zmienna typu jest używana do powiązania dwóch lub więcej parametrów, lub aby powiązać typ parametru z typem zwracanym. Jeśli odpowiedź brzmi nie, wystarczy użyć symbolu wieloznacznego. "

Uwaga: W książce podana jest tylko druga metoda, a nazwa parametru to S zamiast „T”. W książce nie ma pierwszej metody.

chammu
źródło
Głosowałem za cytatem z książki, jest to bezpośrednie i zwięzłe
Kurapika
9

W pierwszym pytaniu: oznacza to, że jeśli istnieje związek między typem parametru a typem zwracanym przez metodę, użyj generycznego.

Na przykład:

public <T> T giveMeMaximum(Collection<T> items);
public <T> Collection<T> applyFilter(Collection<T> items);

Tutaj wyodrębniasz część T zgodnie z określonymi kryteriami. Jeśli T to Longtwoje metody powrócą Longi Collection<Long>; rzeczywisty typ zwracany jest zależny od typu parametru, dlatego użyteczne i zalecane jest używanie typów ogólnych.

Jeśli tak nie jest, możesz użyć typów symboli wieloznacznych:

public int count(Collection<?> items);
public boolean containsDuplicate(Collection<?> items);

W tym przykładzie, niezależnie od typu elementów w kolekcjach, będą zwracane typy inti boolean.

W twoich przykładach:

interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

te dwie funkcje zwrócą wartość logiczną, niezależnie od typu elementów w kolekcjach. W drugim przypadku jest ograniczony do wystąpień podklasy E.

Drugie Pytanie:

class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

Ten pierwszy kod umożliwia przekazanie heterogenicznego List<? extends T> srcparametru jako parametru. Ta lista może zawierać wiele elementów różnych klas, o ile wszystkie rozszerzają klasę bazową T.

gdybyś miał:

interface Fruit{}

i

class Apple implements Fruit{}
class Pear implements Fruit{}
class Tomato implements Fruit{}

mógłbyś

List<? extends Fruit> basket = new ArrayList<? extends Fruit>();
basket.add(new Apple());
basket.add(new Pear());
basket.add(new Tomato());
List<Fruit> fridge = new ArrayList<Fruit>(); 

Collections.copy(fridge, basket);// works 

Z drugiej strony

class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

ograniczenie List<S> srcdo jednej konkretnej klasy S, która jest podklasą T. Lista może zawierać tylko elementy jednej klasy (w tym przypadku S) i żadnej innej klasy, nawet jeśli implementują również T. Nie byłbyś w stanie użyć mojego poprzedniego przykładu, ale możesz zrobić:

List<Apple> basket = new ArrayList<Apple>();
basket.add(new Apple());
basket.add(new Apple());
basket.add(new Apple());
List<Fruit> fridge = new ArrayList<Fruit>();

Collections.copy(fridge, basket); /* works since the basket is defined as a List of apples and not a list of some fruits. */
le-doude
źródło
1
List<? extends Fruit> basket = new ArrayList<? extends Fruit>();Nie jest prawidłową składnią. Musisz utworzyć wystąpienie ArrayList bez ograniczeń.
Arnold Pistorius
Nie można dodać jabłka do koszyka w powyższym przykładzie, ponieważ koszyk może być listą gruszek. Niepoprawny przykład AFAIK. I nie kompiluje się również.
Khanna111
1
@ArnoldPistorius To mnie zdezorientowało. Sprawdziłem dokumentację API ArrayList i ma podpisany konstruktor ArrayList(Collection<? extends E> c). Czy możesz mi wyjaśnić, dlaczego to powiedziałeś?
Kurapika,
@Kurapika Czy to możliwe, że używałem starej wersji Java? Komentarz został opublikowany prawie 3 lata temu.
Arnold Pistorius
2

Metoda symboli wieloznacznych jest również ogólna - możesz ją nazwać z pewnym zakresem typów.

<T>Składnia określa nazwę zmiennej typu. Jeśli zmienna typu ma jakiekolwiek zastosowanie (np. W implementacji metody lub jako ograniczenie dla innego typu), to warto nadać jej nazwę, w przeciwnym razie można by użyć jej ?jako zmiennej anonimowej. Wygląda więc na skrót.

Co więcej, ?składni nie da się uniknąć podczas deklarowania pola:

class NumberContainer
{
 Set<? extends Number> numbers;
}
kan
źródło
3
Czy to nie ma być komentarz?
Buhake Sindi
@BuhakeSindi Przepraszamy, co jest niejasne? Dlaczego -1? Myślę, że to odpowiada na pytanie.
kan
2

Spróbuję po kolei odpowiedzieć na Twoje pytanie.

Czy nie sądzimy, że dzikie karty (Collection<? extends E> c);są również wspierane przez pewien rodzaj polimorfizmu?

Nie. Powodem jest to, że powiązany symbol wieloznaczny nie ma zdefiniowanego typu parametru. To jest nieznane. Wszystko, co „wie”, to to, że „zamknięcie” jest pewnego typu E(niezależnie od definicji). Dlatego nie może zweryfikować i uzasadnić, czy podana wartość jest zgodna z typem ograniczonym.

Tak więc nie ma sensu mieć zachowań polimorficznych na symbolach wieloznacznych.

Dokument zniechęca do drugiej deklaracji i promuje użycie pierwszej składni? Jaka jest różnica między pierwszą a drugą deklaracją? Wydaje się, że obaj robią to samo?

Pierwsza opcja jest lepsza w tym przypadku, ponieważ Tjest zawsze ograniczona i na sourcepewno będzie miała wartości (niewiadomych), które podklasy T.

Więc załóżmy, że chcesz skopiować całą listę numerów, pierwszą opcją będzie

Collections.copy(List<Number> dest, List<? extends Number> src);

srcW istocie, można przyjąć List<Double>, List<Float>itp, jak istnieje górna granica do parametryzowane rodzaj znaleźć w dest.

Druga opcja zmusi cię do wiązania Sdla każdego typu, który chcesz skopiować, tak jak to

//For double 
Collections.copy(List<Number> dest, List<Double> src); //Double extends Number.

//For int
Collections.copy(List<Number> dest, List<Integer> src); //Integer extends Number.

As Sjest typem sparametryzowanym, który wymaga powiązania.

Mam nadzieję, że to pomoże.

Buhake Sindi
źródło
Czy możesz wyjaśnić, co masz na myśli w swoim ostatnim akapicie
benz
Ta, w której jest druga opcja, zmusi cię do związania jednej ...... czy możesz to rozwinąć
benz
<S extends T>stwierdza, że Sjest to typ sparametryzowany, który jest podklasą klasy T, więc wymaga sparametryzowanego typu (bez symbolu wieloznacznego), który jest podklasą klasy T.
Buhake Sindi
2

Jeszcze jedna różnica, która nie została tutaj wymieniona.

static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
    for (T o : a) {
        c.add(o); // correct
    }
}

Ale poniższe spowoduje błąd czasu kompilacji.

static <T> void fromArrayToCollection(T[] a, Collection<?> c) {
    for (T o : a) {
        c.add(o); // compile time error
    }
}
Vivek Kumar
źródło
0

O ile rozumiem, istnieje tylko jeden przypadek użycia, w którym symbol wieloznaczny jest ściśle potrzebny (tj. Może wyrazić coś, czego nie można wyrazić za pomocą jawnych parametrów typu). To wtedy musisz określić dolną granicę.

Oprócz tego symbole wieloznaczne służą do pisania bardziej zwięzłego kodu, jak opisano w poniższych stwierdzeniach w wymienionym dokumencie:

Metody ogólne pozwalają na użycie parametrów typu do wyrażenia zależności między typami co najmniej jednego argumentu metody i / lub jej zwracanego typu. Jeśli nie ma takiej zależności, nie należy używać metody ogólnej.

[…]

Używanie symboli wieloznacznych jest jaśniejsze i bardziej zwięzłe niż deklarowanie jawnych parametrów typu i dlatego powinno być preferowane, gdy tylko jest to możliwe.

[…]

Symbole wieloznaczne mają również tę zaletę, że mogą być używane poza sygnaturami metod, jako typy pól, zmienne lokalne i tablice.

Martin Maletinsky
źródło
0

Głównie -> Symbole wieloznaczne wymuszają typy ogólne na poziomie parametru / argumentu metody innej niż generyczna. Uwaga. Można to również domyślnie wykonać w genericMethod, ale tutaj zamiast? możemy użyć samego T.

pakiet generics;

public class DemoWildCard {


    public static void main(String[] args) {
        DemoWildCard obj = new DemoWildCard();

        obj.display(new Person<Integer>());
        obj.display(new Person<String>());

    }

    void display(Person<?> person) {
        //allows person of Integer,String or anything
        //This cannnot be done if we use T, because in that case we have to make this method itself generic
        System.out.println(person);
    }

}

class Person<T>{

}

SO Wildcard ma swoje specyficzne zastosowania, takie jak ten.

Arasn
źródło