Typabhängigkeit in Java, Teil 2
Das Verständnis der Typkompatibilität ist für das Schreiben guter Java-Programme von grundlegender Bedeutung, aber das Zusammenspiel von Abweichungen zwischen Java-Sprachelementen kann für Uneingeweihte sehr akademisch erscheinen. Dieser zweiteilige Artikel richtet sich an Softwareentwickler, die bereit sind, sich der Herausforderung zu stellen! Teil 1 enthüllte die kovarianten und kontravarianten Beziehungen zwischen einfacheren Elementen wie Array-Typen und generischen Typen sowie dem speziellen Java-Sprachelement, dem Platzhalter. In Teil 2 wird die Typabhängigkeit in der Java Collections-API, in Generika und in Lambda-Ausdrücken untersucht.
Wir springen gleich ein. Wenn Sie Teil 1 noch nicht gelesen haben, empfehle ich, dort zu beginnen.
API-Beispiele für Kontravarianz
Betrachten Sie für unser erstes Beispiel die Comparator
Version von java.util.Collections.sort()
aus der Java Collections-API. Die Signatur dieser Methode lautet:
void sort(List list, Comparator c)
Die sort()
Methode sortiert beliebige List
. Normalerweise ist es einfacher, die überladene Version mit der Signatur zu verwenden:
sort(List
)
In diesem Fall wird extends Comparable
ausgedrückt, dass das sort()
nur aufgerufen werden darf, wenn die erforderlichen Methodenvergleichselemente (nämlich compareTo)
im Elementtyp (oder in seinem Supertyp) definiert wurden, dank :? super T)
sort(integerList); // Integer implements Comparable sort(customerList); // works only if Customer implements Comparable
Verwendung von Generika zum Vergleich
Offensichtlich ist eine Liste nur sortierbar, wenn ihre Elemente miteinander verglichen werden können. Der Vergleich erfolgt mit der einzelnen Methode compareTo
, die zur Schnittstelle gehört Comparable
. Sie müssen compareTo
in der Elementklasse implementieren .
Diese Art von Element kann jedoch nur in eine Richtung sortiert werden. Beispielsweise können Sie eine Customer
nach ihrer ID sortieren , jedoch nicht nach Geburtstag oder Postleitzahl. Die Verwendung der Comparator
Version von sort()
ist flexibler:
publicstatic void sort(List list, Comparator c)
Jetzt vergleichen wir Elemente nicht in der Klasse des Elements, sondern in einem zusätzlichen Comparator
Objekt. Diese generische Schnittstelle verfügt über eine Objektmethode:
int compare(T o1, T o2);
Kontravariante Parameter
Wenn Sie ein Objekt mehrmals instanziieren, können Sie Objekte nach verschiedenen Kriterien sortieren. Aber brauchen wir wirklich so einen komplizierten Comparator
Typparameter? In den meisten Fällen Comparator
wäre das genug. Wir könnten seine compare()
Methode verwenden, um zwei beliebige Elemente im List
Objekt wie folgt zu vergleichen :
Klasse DateComparator implementiert Comparator {public int compare (Datum d1, Datum d2) {return ...} // vergleicht die beiden Date-Objekte} List dateList = ...; // Liste der Datumsobjekte sort (dateList, new DateComparator ()); // sortiert dateList
Die Verwendung der komplizierteren Version der Methode Collection.sort()
hat uns jedoch für zusätzliche Anwendungsfälle eingerichtet. Der kontravariante Typparameter von Comparable
ermöglicht das Sortieren einer Typliste List
, da java.util.Date
es sich um einen Supertyp handelt von java.sql.Date
:
List sqlList = ... ; sort(sqlList, new DateComparator());
Wenn wir Kontravarianzen in der sort()
Signatur weglassen (nur oder nicht angegeben, unsicher
), lehnt der Compiler die letzte Zeile als Typfehler ab.
Um anzurufen
sort(sqlList, new SqlDateComparator());
Sie müssten eine besonders merkwürdige Klasse schreiben:
class SqlDateComparator extends DateComparator {}
Zusätzliche Methoden
Collections.sort()
ist nicht die einzige Java Collections-API-Methode, die mit einem kontravarianten Parameter ausgestattet ist. Methoden wie addAll()
, binarySearch()
, copy()
, fill()
, und so weiter, können mit ähnlicher Flexibilität verwendet werden.
Collections
Methoden wie max()
und min()
bieten kontravariante Ergebnistypen:
public static
T max( Collection collection) { ... }
Wie Sie hier sehen, kann ein Typparameter angefordert werden, um mehr als eine Bedingung zu erfüllen, indem Sie einfach verwenden &
. Das extends Object
mag überflüssig erscheinen, legt jedoch fest, dass max()
ein Ergebnis vom Typ Object
und nicht von der Zeile Comparable
im Bytecode zurückgegeben wird. (Der Bytecode enthält keine Typparameter.)
Die überladene Version von max()
with Comparator
ist noch lustiger:
public static T max(Collection collection, Comparator comp)
Dies max()
hat sowohl kontravariante als auch kovariante Typparameter. Während die Elemente von Collection
(möglicherweise unterschiedlichen) Subtypen eines bestimmten (nicht explizit angegebenen) Typs sein Comparator
müssen , müssen sie für einen Supertyp desselben Typs instanziiert werden. Der Inferenzalgorithmus des Compilers erfordert viel, um diesen Zwischentyp von einem Aufruf wie diesem zu unterscheiden:
Collection collection = ... ; Comparator comparator = ... ; max(collection, comparator);
Boxed Bindung von Typparametern
Lassen Sie uns als letztes Beispiel für Typabhängigkeit und Varianz in der Java Collections-API die Signatur von sort()
with überdenken Comparable
. Beachten Sie, dass beide extends
und verwendet werden super
, die in Kästchen verpackt sind:
static
void sort(List list) { ... }
In this case, we're not as interested in the compatibility of references as we are in binding the instantiation. This instance of the sort()
method sorts a list
object with elements of a class implementing Comparable
. In most cases, sorting would work without in the method's signature:
sort(dateList); // java.util.Date implements Comparable sort(sqlList); // java.sql.Date implements Comparable
The lower bound of the type parameter allows additional flexibility, however. Comparable
doesn't necessarily need to be implemented in the element class; it's enough to have implemented it in the superclass. For example:
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);
The compiler accepts the last line with
static
void sort(List list) { ... }
and rejects it with
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
|
OK | Error | OK | Cast | Cast | Cast |
Contravariant
|
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
Hier Id
ist ein Supertyp von CustomerNumber
und Person
ist ein Supertyp von Customer
.
Varianz der Methoden
Wir haben über die Varianz der Typen gesprochen. Wenden wir uns nun einem etwas einfacheren Thema zu.