Ovisnost o tipu u Javi, 2. dio

Razumijevanje kompatibilnosti tipova osnovno je za pisanje dobrih Java programa, ali međusobno djelovanje varijacija između elemenata Java jezika neupućenima se može činiti vrlo akademskim. Ovaj dvodijelni članak namijenjen je programerima koji su spremni riješiti taj izazov! Prvi dio otkrio je kovarijantne i kontravarijantne odnose između jednostavnijih elemenata kao što su tipovi nizova i generički tipovi, kao i poseban element Java jezika, zamjenski znak. Dio 2 istražuje ovisnost o tipu u API-ju Java Collections, u generičkim i lambda izrazima.

Uskočit ćemo odmah, pa ako već niste pročitali 1. dio, preporučujem da započnete tamo.

Primjeri API-ja za kontravarijanciju

Za naš prvi primjer, razmotrite Comparatorverziju java.util.Collections.sort()API-ja Java Collections. Potpis ove metode je:

  void sort(List list, Comparator c) 

sort()Metoda sortira bilo List. Obično je lakše koristiti preopterećenu verziju s potpisom:

 sort(List
    
     ) 
    

U ovom slučaju, extends Comparableizražava da se sort()može pozvati samo ako su potrebni elementi za usporedbu metoda (naime compareTo)definirani u tipu elementa (ili u njegovom supertipu, zahvaljujući :? super T)

 sort(integerList); // Integer implements Comparable sort(customerList); // works only if Customer implements Comparable 

Korištenje generika za usporedbu

Očito je da se popis može sortirati samo ako se njegovi elementi mogu međusobno uspoređivati. Usporedba se vrši jednom metodom compareTokoja pripada sučelju Comparable. Morate implementirati compareTou klasu element.

Međutim, ova vrsta elementa može se sortirati samo na jedan način. Na primjer, možete sortirati a Customerprema njihovom ID-u, ali ne i prema rođendanu ili poštanskom broju. Korištenje Comparatorverzije sort()fleksibilnije je:

 publicstatic  void sort(List list, Comparator c) 

Sada uspoređujemo elemente ne u klasi elementa, već u dodatnom Comparatorobjektu. Ovo generičko sučelje ima jednu objektnu metodu:

 int compare(T o1, T o2); 

Parametri kontravarijanata

Instanciranje objekta više puta omogućuje vam sortiranje objekata koristeći različite kriterije. No trebamo li zaista tako komplicirani Comparatorparametar tipa? U većini slučajeva to Comparatorbi bilo dovoljno. Mogli bismo upotrijebiti njegovu compare()metodu za usporedbu bilo koja dva elementa u Listobjektu, kako slijedi:

klasa DateComparator implementira Comparator {public int compare (Date d1, Date d2) {return ...} // uspoređuje dva Date objekta} Popis dateList = ...; // Popis sortiranja objekata datuma (dateList, novi DateComparator ()); // sortira dateList

Collection.sort()Međutim, koristeći kompliciraniju verziju metode, postavili smo se za dodatne slučajeve upotrebe. Parametar kontravarijantnog tipa Comparableomogućuje sortiranje popisa vrsta List, jer java.util.Dateje supertip od java.sql.Date:

 List sqlList = ... ; sort(sqlList, new DateComparator()); 

Ako izostavimo kontravarijanciju u sort()potpisu (koristeći samo ili neodređenu, nesigurnu ), tada prevodilac odbacuje posljednji redak kao pogrešku u tipu.

Kako bi nazvao

 sort(sqlList, new SqlDateComparator()); 

morali biste napisati dodatni tečaj bez obilježja:

 class SqlDateComparator extends DateComparator {} 

Dodatne metode

Collections.sort()nije jedina metoda API-ja Java Collections opremljena kontravarijantnim parametrom. Metoda kao što su addAll(), binarySearch(), copy(), fill(), i tako dalje, može se koristiti sa sličnim fleksibilnosti.

Collectionsmetode poput max()i min()nude kontravarijantne vrste rezultata:

 public static 
    
      T max( Collection collection) { ... } 
    

Kao što vidite ovdje, od parametra tipa može se zatražiti da zadovolji više od jednog uvjeta, samo pomoću &. extends ObjectMogu se pojaviti suvišno, ali je propisano da max()vraća rezultat tipa Object, a ne redom Comparableu bytecode. (U bajtkodu nema parametara tipa.)

Preopterećeni verzija max()sa Comparatorjoš smješnije:

 public static  T max(Collection collection, Comparator comp) 

Ovo max()ima i parametre kontravarijantnog i kovarijantnog tipa. Iako elementi mustra Collectionmoraju biti (moguće različitih) podtipova određenog (ne izričito datog) tipa, Comparatormoraju biti instancirani za nadtip istog tipa. Mnogo je potrebno od algoritma zaključivanja prevoditelja, kako bi se razlikovao ovaj među-tip od poziva poput ovog:

 Collection collection = ... ; Comparator comparator = ... ; max(collection, comparator); 

Povezivanje parametara tipa u kutiji

Kao naš posljednji primjer ovisnosti o tipu i varijance u API-ju Java Collections, preispitajmo potpis sort()sa Comparable. Imajte na umu da koristi oboje extendsi super, koji su uokvireni:

 static 
    
      void sort(List list) { ... } 
    

U ovom slučaju, nismo toliko zainteresirani za kompatibilnost referenci koliko za vezivanje instancije. Ova instanca sort()metode sortira listobjekt s elementima izvedbe klase Comparable. U većini slučajeva sortiranje bi funkcioniralo bez potpisa u metodi:

 sort(dateList); // java.util.Date implements Comparable sort(sqlList); // java.sql.Date implements Comparable 

Donja granica parametra tipa omogućuje dodatnu fleksibilnost. Comparablene mora nužno biti implementiran u klasu elemenata; dovoljno je to implementirati u superklasu. Na primjer:

 class SuperClass implements Comparable { public int compareTo(SuperClass s) { ... } } class SubClass extends SuperClass {} // without overloading of compareTo() List superList = ...; sort(superList); List subList = ...; sort(subList); 

Prevoditelj prihvaća posljednji redak s

 static 
    
      void sort(List list) { ... } 
    

i odbija ga s

static 
    
      void sort(List list) { ... } 
    

The reason for this rejection is that the type SubClass (which the compiler would determine from the type List in the parameter subList) is not suitable as a type parameter for T extends Comparable. The type SubClass doesn't implement Comparable; it only implements Comparable. The two elements are not compatible due to the lack of implicit covariance, although SubClass is compatible to SuperClass.

On the other hand, if we use , the compiler doesn't expect SubClass to implement Comparable; it's enough if SuperClass does it. It's enough because the method compareTo() is inherited from SuperClass and can be called for SubClass objects: expresses this, effecting contravariance.

Contravariant accessing variables of a type parameter

The upper or the lower bound applies only to type parameter of instantiations referred by a covariant or contravariant reference. In the case of Generic covariantReference; and Generic contravariantReference;, we can create and refer objects of different Generic instantiations.

Different rules are valid for the parameter and result type of a method (such as for input and output parameter types of a generic type). An arbitrary object compatible to SubType can be passed as parameter of the method write(), as defined above.

 contravariantReference.write(new SubType()); // OK contravariantReference.write(new SubSubType()); // OK too contravariantReference.write(new SuperType()); // type error ((Generic)contravariantReference).write( new SuperType()); // OK 

Because of contravariance, it's possible to pass a parameter to write(). This is in contrast to the covariant (also unbounded) wildcard type.

The situation doesn't change for the result type by binding: read() still delivers a result of type ?, compatible only to Object:

 Object o = contravariantReference.read(); SubType st = contravariantReference.read(); // type error 

The last line produces an error, even though we've declared a contravariantReference of type Generic.

The result type is compatible to another type only after the reference type has been explicitly converted:

 SuperSuperType sst = ((Generic)contravariantReference).read(); sst = (SuperSuperType)contravariantReference.read(); // unsafer alternative 

Examples in the previous listings show that reading or writing access to a variable of type parameter behaves the same way, regardless of whether it happens over a method (read and write) or directly (data in the examples).

Reading and writing to variables of type parameter

Table 1 shows that reading into an Object variable is always possible, because every class and the wildcard are compatible to Object. Writing an Object is possible only over a contravariant reference after appropriate casting, because Object is not compatible to the wildcard. Reading without casting into an unfitting variable is possible with a covariant reference. Writing is possible with a contravariant reference.

Table 1. Reading and writing access to variables of type parameter

reading

(input)

read

Object

write

Object

read

supertype   

write

supertype   

read

subtype    

write

subtype    

Wildcard

?

 OK  Error  Cast  Cast  Cast  Cast

Covariant

?extends

 OK  Error  OK  Cast  Cast  Cast

Contravariant

?super

 OK  Cast  Cast  Cast  Cast  OK

The rows in Table 1 refer to the sort of reference, and the columns to the type of data to be accessed. The headings of "supertype" and "subtype" indicate the wildcard bounds. The entry "cast" means the reference must be casted. An instance of "OK" in the last four columns refers to the typical cases for covariance and contravariance.

See the end of this article for a systematic test program for the table, with detailed explanations.

Creating objects

On the one hand, you cannot create objects of the wildcard type, because they are abstract. On the other hand, you can create array objects only of an unbounded wildcard type. You cannot create objects of other generic instantiations, however.

 Generic[] genericArray = new Generic[20]; // type error Generic[] wildcardArray = new Generic[20]; // OK genericArray = (Generic[])wildcardArray; // unchecked conversion genericArray[0] = new Generic(); genericArray[0] = new Generic(); // type error wildcardArray[0] = new Generic(); // OK 

Because of the covariance of arrays, the wildcard array type Generic[] is the supertype of the array type of all instantiations; therefore the assignment in the last line of the above code is possible.

Within a generic class, we cannot create objects of the type parameter. For example, in the constructor of an ArrayList implementation, the array object must be of type Object[] upon creation. We can then convert it to the array type of the type parameter:

 class MyArrayList implements List { private final E[] content; MyArrayList(int size) { content = new E[size]; // type error content = (E[])new Object[size]; // workaround } ... } 

For a safer workaround, pass the Class value of the actual type parameter to the constructor:

 content = (E[])java.lang.reflect.Array.newInstance(myClass, size); 

Multiple type parameters

A generic type can have more than one type parameter. Type parameters don't change the behavior of covariance and contravariance, and multiple type parameters can occur together, as shown below:

 class G {} G reference; reference = new G(); // without variance reference = new G(); // with co- and contravariance 

The generic interface java.util.Map is frequently used as an example for multiple type parameters. The interface has two type parameters, one for key and one for value. It's useful to associate objects with keys, for example so that we can more easily find them. A telephone book is an example of a Map object using multiple type parameters: the subscriber's name is the key, the phone number is the value.

The interface's implementation java.util.HashMap has a constructor for converting an arbitrary Map object into an association table:

 public HashMap(Map m) ... 

Because of covariance, the type parameter of the parameter object in this case does not have to correspond with the exact type parameter classes K and V. Instead, it can be adapted through covariance:

 Map customers; ... contacts = new HashMap(customers); // covariant 

Ovdje Idje supertip CustomerNumberi Personje supertip Customer.

Varijacija metoda

Razgovarali smo o varijantama vrsta; sada se okrenimo nešto lakšoj temi.