Wildcards

0

Mógłby mi ktoś wytłumaczyć dlaczego to się nie kompiluje?

class A {}
class B extends A {}
class C extends B {}

public class Main {

	public static void main(String[] args) {
		A a = new A();
		B b = new B();
		C c = new C();
		
		List<A> list = new ArrayList<>();
		List<B> list2 = new ArrayList<>();
		List<C> list3 = new ArrayList<>();
		
		List<? super C> listA = list;
		List<? super C> listB = list2;
		List<? super C> listC = list3;
	
		listA.add(a); //error
		listA.add(b); //error
		listA.add(c);
		
		listB.add(a); //error
		listB.add(b); //error
		listB.add(c);
		
		listC.add(a); //error
		listC.add(b); //error
		listC.add(c);
	}

Czy ? super C nie oznacza, że do listy mogę dodać wszystko co jest rodzicem C(czyli A i B)?

1

Do List<? super C> możesz wrzucić wartość typu C albo podklasy C. Wyciągnąć możesz tylko Objecta.

Dlaczego do List<? super C> nie można wrzucić wartości typu B? Bo List<? super C> może oznaczać List<C>, a do takiej nie wolno wrzucić B.

Ogólnie te wildcardy są trikowe i ciężko je prosto wytłumaczyć :/

1

Poczytaj o kowariancji i kontrawariancji typów. Spróbuj zrozumieć sygnaturę metody compare z java.util.Comparator.

0

Używasz wildcard'a nie tam, gdzie trzeba. Generalnie służą one do typowania parametrów metod, a nie określania typów zmiennych. Popatrz na przykład:

public class Main {
    public static void processExtends(List<? extends A> list){
        for(A a: list){
            System.out.println(a);
        }
    }

    public static void processSuper(List<? super B> list){
        for(Object a: list){
            System.out.println(a);
        }
    }

    public static void processListA(List<A> list){
        for(A a: list){
            System.out.println(a);
        }
    }

    public static void main(String[] args) {
        List<B> listB = Arrays.asList(new B(1), new B(2));
        List<A> listA = Arrays.asList(new A(1), new A(2));
        List<C> listC = Arrays.asList(new C(1), new C(2));

        processExtends(listA);  //OK listA jest typu List<A> i pasuje do oczekiwanego typu List<A extends A>
        processExtends(listB);  //OK listB jest typu List<B> i pasuje do oczekiwanego typu List<B extends A>
        processExtends(listC);  //OK listC jest typu List<C> i pasuje do oczekiwanego typu List<C extends A>

        processListA(listA);    //OK listA jest typu List<A>
        processListA(listB);    //UPS listB jest typu List<B> a metoda oczekuje List<A>
        processListA(listC);    //UPS listC jest typu List<C> a metoda oczekuje List<A>

        processSuper(listA);    //OK listA jest typu List<A> i pasuje do oczekiwanego typu List<A super B>, bo A jest super klasą dla B
        processSuper(listB);    //OK jak wyżej
        processSuper(listC);    //UPS listC jest typu List<C> a C nie jest super klasą dla B
    }
}

Deklaracja:

List<? super C> listC = new ArrayList<>();

stworzy Ci listę elementów typu C Object i tylko C Object, ale i tak będziesz mógł tam wstawić obiekty z klas pochodnych po C.

2

Ja bym zaczął od kowariancji i kontrawariancji ;) List<A> i List<B> NIE SĄ powiązane w żaden sposób niezależnie od relacji A do B. Np. jeśli masz List<String> to nie da się tego przypisać do zmiennej List<Object> mimo, że mogło by się wydawać że ma to sens, bo każdy String to tez Object.

0

Ok. Chyba rozumiem. Patrzę na to jak na rodzinę.

List<? SUPER C> -> List<Object> or List<A> or List<B> or List<C>
Mogę dodać tylko C(i pochodne), bo tylko C(i pochodne) może być dodane do każdej z tych list.
Mogę pobrać tylko object, bo tylko ref object może wskazywać na wszystkie elementy z poszczególnych list( Object o = new A(), Object o = new B(), ale nie mogę refem a wskazać na object: A a = new Object())

List<? EXTENDS C> -> List<C> or List<D>
Nie mogę dodać nic, bo nie znam dokładnie typu (C or D). Jak dodaję C, to nie pasuje do List<D>.
Teoretycznie mogłaby być możliwość dodania obiektu D, ale pewnie chodzi o to, że zawsze znamy wszystkich możliwych rodziców, ale nigdy nie znamy wszystkich potencjalnych potomków.
Mogę pobrać tylko C, bo jest najbardziej ogólne i tylko C może być refem do wszystkiego z rodziny.

0

Jeszcze inaczej, bo jakoś nie mogę tego w pełni zrozumieć. Mam książkę Java podstawy Horstmanna i parafrazując z niej przykład:

public class Container<E> {
	E element;
	
	public Container(E element) {
		this.element = element;
	}

	public E getElement() {
		return element;
	}

	public void setElement(E element) {
		this.element = element;
	}
}

class Animal{};
class Dog extends Animal{};

I teraz 2 warianty:

1.java<? extends Animal>
2.java<? super Dog>

1.W klasie Container za "< E>" podstawiam "<? extends Animal>" i otrzymuję:

public class Container<? extends Animal> {
	<? extends Animal> element;
	
	public Container(<? extends Animal> element) {
		this.element = element;
	}

	public <? extends Animal> getElement() {
		return element;
	}

	public void setElement(<? extends Animal> element) {
		this.element = element;
	}
}

Wywołania:

Animal a = container.getElement(); //mogę, bo typ zwracany getElement jest albo Animal albo potomek  [public <? extends Animal> getElement() ]
c.setElement(a); // nie mogę, bo nie wiem czy animal czy potomek jest parametrem funkcji [public void setElement(<? extends Animal> element)]
//Ok. Te dwa sa zrozumiałe,ale teraz:
  1. W klasie Container za "< E>" podstawiam <? super Dog> i otrzymuję:
public class Container<? super Dog> {
	<? super Dog> element;
	
	public Container(<? super Dog> element) {
		this.element = element;
	}

	public <? super Dog> getElement() {
		return element;
	}

	public void setElement(<? super Dog> element) {
		this.element = element;
	}
}

Wywołania:

container.getElement(); <- nie mogę, tzn. mogę ale tylko jako przypisanie do object. Object o = <? super Dog>  [public <? super Dog> getElement() ]
c.setElement(new Dog()) <- mogę, ale dlaczego nie c.setElement(new Animal())?
0

Ten kod się w ogóle kompiluje?

0

Weźmy np. taką java.util.List z Javy. Jeśli byłaby ona typem kowariantnym oznaczałoby to, że wszędzie tam gdzie deklarowana jest List<Object> można podstawić List<String>, ponieważ String jest podtypem Object. Niestety tak nie jest by default, do tego służy wildcard z ograniczeniem od góry (upper bounded wildcard), czyli np. List<? extends Object>.

Wtedy mając jakąś metodę foo(List<? extends Object> aList) można zawołać ją z List<String> aList = List.of("ala", "ma", "kota"). Spójrz tutaj: https://docs.oracle.com/javase/tutorial/extra/generics/wildcards.html.

Ciekawostka: w Javie tablice są kowariantne. Przez to, że są również mutowalne możemy dostać ArrayStoreException.

Trudniejszy jest wildcard z ograniczenie z dołu (wildcard with lower bound). W klasie java.util.List mamy metodę:

default void sort(Comparator<? super E> c) { ... }

Czyli przyjmującą dziwny Comparator. Jeśli chcemy posortować List<String>, to okazuje się, że możemy przekazać Comparator<String>, ale również Comparator<Object> - wystarczy przekazać coś, co umie porównać dowolny nadtyp String. Nie może to być komparator dla podtypu, ponieważ mógłby korzystać z właściwości dodawanych przed ten podtyp. Więcej tutaj: https://docs.oracle.com/javase/tutorial/extra/generics/morefun.html

0

Być może źle na to patrzę. Idea <? extends Cośtam> jest taka, żeby uzyskać taki jakby polimorfizm dla genericsów, bo same z siebie nie są w relacji(invariant). Jaka jest właściwie idea <? super Cośtam>?

0

? extends Cośtam jest do kowariancji. ? super Cośtam jest do kontrawariancji.

Dla przykładu sygnatura metody java.util.stream.Stream<T>.map to:

<R> Stream<R> map(Function<? super T,? extends R> mapper)

Jeśli chcę przemapować Stream<A> do Stream<B> mogę to zrobić za pomocą Function<A, B>, Function<nadklasa A, B>, Function<A, podklasa B> czy Function<nadklasa A, podklasa B>. Jeśli mam Function<Object, String> to mogę tej funkcji użyć jako parametru dla Stream.map dla dowolnego Streama właśnie dzięki temu, że w sygnaturze Stream.map użyto ? super T, a Object jest nadklasą wszystkich klas.

link do artykułu na wiki o wariancji

0

Chyba mam już ogólny obraz.

public class Container<E> {
	E element;
	
	public Container(E element) {this.element = element;}
	public E getElement() {	return element;}
	public void setElement(E element) {	this.element = element;	}
}

//Mam Container<Object>, Container<Animal>, Container<Dog>,... i chce je móc przekazywać do jednej metody.

//Wymazanie typów tworzy takie klasy:
//Container<E>
class Container{
	public Object getElement();
	public void setElement(Object o);
}

//Container<Object>
class ContainerZObjectem extends Container{
	Object element;
	
	public Container(Object element) {this.element = element;}
	public Object getElement() {return element;}
	public void setElement(Object element) {this.element = element;}
}

//Container<Animal>
class ContainerZAniamalem extends Container{
	Animal element;
	
	public Container(Animal element) {this.element = element;}
	public Animal getElement() {return element;}
	public void setElement(Animal element) {this.element = element;}
}
//Container<Dog>
class ContainerZDogiem extends Container{
	Dog element;
	
	public Container(Dog element) {this.element = element;}
	public Dog getElement() {return element;}
	public void setElement(Dog element) {this.element = element;}
}

? extends Animal daje możliwe containery:
Container container = new ContainerZAniamalem() or new ContainerZDogiem() or ...;
W zależności od konkretnego containera:
metoda getElement wywoła:
public Animal getElement() or public Dog getElement() or ... -> więc return Animal bo jest supertypem reszty
metoda setElement wywoła:
public void setElement(Animal element) or public void setElement(Dog dog) or ...-> więc nie mogę wstawić nic, bo nie znam samego dołu i nie mogę złapać w jedną grupę wywołań
W ostateczności w tym przypadku container ma takie metody:
public Animal getElement();

? super Dog daje możliwe containery:
Container container2 = new Container() or new ContainerZAniamalem() or new ContainerZDogiem();
W zależności od konkretnego containera:
metoda getElement wywoła:
public Object getElement() or public Animal getElement() or public Dog getElement() -> return Object, bo jest supertypem reszty
metoda setElement wywoła:
public void setElement(Object o) or public void setElement(Animal element) or public void setElement(Dog element) -> żeby z jednej referencji "container2" móc obsłużyć wszystkie sygnatury, muszę pchać doga, bo tylko jego wepche w każdą z tych metod
W ostateczności w tym przypadku container2 ma takie metody:
public Object getElement();
public void setElement(Dog element);

To by się wszystko pokrywało.
Czy taka rozpiska dobrze ukazuje jak wygląda kod już po wymazaniu typów, czyli na poziomie jvm-a? Czy można powiedzieć, ze poszczególne typy po wymazaniu mają wspólny interfejs?

0

Wszystko ładnie pokrywa zasada PECS – Producer Extends Consumer Super

Producent, czyli kolekcja, na której wykonujesz metody typu get musi rozszerzać – extends, ponieważ zapis:

Collection<? extends Car> collection = …
Car car = collection.get();

daje gwarancję, że do zmiennej car nie trafi nic ponad obiekt klasy Car.

Jednocześnie konsument, na którym wywołujesz add

Collection<? super Car> collection = …
collection.add(car);

przetrzymuje dowolne rzeczy, przy czym nie będzie to nic ponad Car.

Na koniec obrazek, który to tłumaczy:

Kowariancja i kontrawariancja

autor: https://stackoverflow.com/users/2707792/andrey-tyukin

0

Czy taka rozpiska dobrze ukazuje jak wygląda kod już po wymazaniu typów, czyli na poziomie jvm-a? Czy można powiedzieć, ze poszczególne typy po wymazaniu mają wspólny interfejs?

Z punktu widzenia JVMa jest tylko jedna klasa - Container bez typów generycznych.

Typy generyczne istnieją tylko na etapie kompilacji, aczkolwiek sygnatury generyczne są wstawiane do plików .class (z bajtkodem) i dostępne przez refleksję. Te sygnatury generyczne odpowiadają temu co jest w kodzie, a nie w czasie wykonania, tzn masz dostępne tylko to co jest w deklaracji klasy (czyli np T extends Animal), a nie to co mogłoby być dostępne w konkretnej instancji klasy (czyli analogicznie np Dog).

Wymazywanie typów odbywa się na etapie kompilacji, czyli to javac wymazuje typy, a java już ich nie dostaje. Poprawne wymazywanie typów w Javie nierozłącznie wiąże się ze wstawieniem rzutowań w odpowiednie miejsca, co automatycznie robi javac podczas wymazywania, stąd np taki kod:

List<String> strings = makeStrings();
String oneString = strings.get(1);

jest wymazywany do czegoś takiego:

List strings = makeStrings();
String oneString = (String) strings.get(1);
0
Wibowit napisał(a):

Z punktu widzenia JVMa jest tylko jedna klasa - Container bez typów generycznych.

Nie jestem pewien czy dobrze to rozumiem.

public static void main(String[] args) {
	Container<Animal> container = new Container<>();
	Container<Dog> container2 = new Container<>();
}

Czy to nie jest tak, że po takim kodzie w jvmi-e będą dwa byty?
1.

class Container{
    Animal element;

    public Container(Animal element) {this.element = element;}
    public Animal getElement() {return element;}
    public void setElement(Animal element) {this.element = element;}
}
class Container{
    Dog element;

    public Container(Dog element) {this.element = element;}
    public Dog getElement() {return element;}
    public void setElement(Dog element) {this.element = element;}
}

Jakiś nie wiem Container#1 i Container#2?

0

Nie. Skompiluj sobie kod za pomocą javac, a potem zdekompiluj pliki .class za pomocą javap -c. Zobaczysz, że w bajtkodzie klasa Container jest tylko jedna. Poza tym w bajtkodzie w C# klasa Container też by była jedna, chociaż wymazywania typów nie ma.

Dla przykładu taki kod:

import java.awt.*;
import java.util.ArrayList;
import java.util.List;

public class ErasureDemo {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        String string = strings.get(1);
        List<Color> colors = new ArrayList<>();
        Color color = colors.get(1);
    }
}

W bajtkodzie wygląda tak:

Compiled from "ErasureDemo.java"
public class erasure.ErasureDemo {
  public erasure.ErasureDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      10: invokeinterface #4,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      15: checkcast     #5                  // class java/lang/String
      18: astore_2
      19: new           #2                  // class java/util/ArrayList
      22: dup
      23: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
      26: astore_3
      27: aload_3
      28: iconst_1
      29: invokeinterface #4,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      34: checkcast     #6                  // class java/awt/Color
      37: astore        4
      39: return
}

Jak widać ArrayLista jest tylko jedna, żadnych typów generycznych nie widać, ale za to są rzutowania (checkcast).

JVM się w ogóle genericsami nie przejmuje, bo dla niego nie istnieją. Nie jest to jednak problemem, gdyż genericsy w Javie i tak są statycznie sprawdzane, tylko na etapie kompilacji do bajtkodu.

0

JVM się w ogóle genericsami nie przejmuje, bo dla niego nie istnieją.

@Wibowit To nie do końca prawda, ponieważ niektóre informacje są dostępne w runtime: https://www.gridshore.nl/2009/10/27/some-notes-on-discovering-your-type-parameter-using-the-reflection-api/

0

Rzeczywiście tak jest. Trochę mi to burzy światpogląd. Wyobrażałem sobie obiekt jako "żywy twór" powstały na skutek ożywienia klasy.

Abstrakcyjny twór (klasa - nie mylić z klasą abstrakcyjną) staje się żywa (obiekt), a parametry metody jako ograniczające wejście dla danych "żywych tworów"(obiektów).

No ale jak na koniec w jvm-ie jest tylko jeden generics i widzę parametr metody java ? extends Animal to trochę smutek. Czy parametry metody są istotne tylko w czasie kompilacji?

0

Rzeczywiście tak jest. Trochę mi to burzy światpogląd. Wyobrażałem sobie obiekt jako "żywy twór" powstały na skutek ożywienia klasy

Nie mam pojecia jak dokladnie dziala jvm ale w gruncie rzeczy wszystko w pewnym momencie musi zejsc do poziomu assemblera. A assembler nawet typow nie zna ;) dla niego istnieja tylko bajty. Obiekty i sygnatury to tylko taka abstrakcja.

0

Jedno drugiego nie wyklucza, tj. programowanie obiektowe („ożywianie klas”) idzie w parze z systemem typów.

0
Charles_Ray napisał(a):

Jedno drugiego nie wyklucza, tj. programowanie obiektowe („ożywianie klas”) idzie w parze z systemem typów.

A idzie w parze w ten sposob, ze chociaz w assemblerze fizycznie mozemy przekazac do funkcji cokolwiek to po przekazaniu takiego cokolwiek mozemy dostac a. smieci b. segfaulta --- ergo lepiej sie tej abstrakcyjnej sygnatury trzymac

0

A rozumiem. Z poziomu jvm-a to wszystko jedno czy metoda jest void doIt(Container<?extends Animal>) czy void doIt(Container<?super Dog>) bo ta część od typowania zostaje wymazana. Ale znaczenie ma, że parametrem tej metody jest Container. Czyli jakby ta brama w postaci metody wpuszcza obiekty containery. Świat uratowany.

0

Genericsy są wymazane (czyli <JakiśTyp>), a zamiast nich w innych miejscach są wrzucane rzutowania (czyli (JakiśTyp) zmienna) i wszystko się zgadza.

1 użytkowników online, w tym zalogowanych: 0, gości: 1