Typabhängigkeit in Java, Teil 1

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 Artikel richtet sich an Softwareentwickler, die bereit sind, sich der Herausforderung zu stellen! Teil 1 enthüllt die kovarianten und kontravarianten Beziehungen zwischen einfacheren Elementen wie Array-Typen und generischen Typen sowie dem speziellen Java-Sprachelement, dem Platzhalter. Teil 2 untersucht die Typabhängigkeit und -varianz in allgemeinen API-Beispielen und in Lambda-Ausdrücken.

download Quellcode herunterladen Holen Sie sich den Quellcode für diesen Artikel "Typabhängigkeit in Java, Teil 1". Erstellt für JavaWorld von Dr. Andreas Solymosi.

Konzepte und Terminologie

Bevor wir uns mit den Beziehungen zwischen Kovarianz und Kontravarianz zwischen verschiedenen Java-Sprachelementen befassen, stellen wir sicher, dass wir einen gemeinsamen konzeptionellen Rahmen haben.

Kompatibilität

Bei der objektorientierten Programmierung bezieht sich Kompatibilität auf eine gerichtete Beziehung zwischen Typen, wie in Abbildung 1 dargestellt.

Andreas Solymosi

Wir sagen, dass zwei Typen in Java kompatibel sind , wenn es möglich ist, Daten zwischen Variablen der Typen zu übertragen. Die Datenübertragung ist möglich, wenn der Compiler sie akzeptiert, und erfolgt durch Zuweisung oder Parameterübergabe. Zum Beispiel shortist kompatibel mit, intweil die Zuordnung intVariable = shortVariable;möglich ist. Ist booleanaber nicht kompatibel mit, intda die Zuordnung intVariable = booleanVariable;nicht möglich ist; Der Compiler akzeptiert es nicht.

Da Kompatibilität eine gerichtete Beziehung ist, ist sie manchmal kompatibel, aber nicht kompatibel oder nicht auf die gleiche Weise. Wir werden dies weiter sehen, wenn wir die explizite oder implizite Kompatibilität diskutieren.T1T2T2T1

Entscheidend ist, dass die Kompatibilität zwischen Referenztypen nur innerhalb einer Typhierarchie möglich ist. Alle Klassentypen sind beispielsweise kompatibel mit Object, da alle Klassen implizit von erben Object. Integerist jedoch nicht kompatibel Float, da Floates sich nicht um eine Oberklasse von handelt Integer. Integerist kompatibel mit Number, weil Numberes sich um eine (abstrakte) Oberklasse von handelt Integer. Da sie sich in derselben Typhierarchie befinden, akzeptiert der Compiler die Zuweisung numberReference = integerReference;.

Wir sprechen von impliziter oder expliziter Kompatibilität, je nachdem, ob Kompatibilität explizit markiert werden muss oder nicht. Zum Beispiel ist short implizit kompatibel mit int(wie oben gezeigt), aber nicht umgekehrt: Die Zuordnung shortVariable = intVariable;ist nicht möglich. Short ist jedoch explizit kompatibel mit int, da die Zuordnung shortVariable = (short)intVariable;möglich ist. Hier müssen wir die Kompatibilität durch Gießen markieren , auch als Typkonvertierung bekannt.

Ebenso unter Referenztypen: integerReference = numberReference;ist nicht akzeptabel, integerReference = (Integer) numberReference;würde nur akzeptiert. Daher Integerist implizit kompatibel mit, Numberaber Numbernur explizit kompatibel mit Integer.

Abhängigkeit

Ein Typ kann von anderen Typen abhängen. Beispielsweise int[]hängt der Array-Typ vom primitiven Typ ab int. Ebenso ist der generische Typ ArrayListvom Typ abhängig Customer. Methoden können auch typabhängig sein, abhängig von den Typen ihrer Parameter. Zum Beispiel die Methode void increment(Integer i); hängt vom Typ ab Integer. Einige Methoden (wie einige generische Typen) hängen von mehr als einem Typ ab, z. B. Methoden mit mehr als einem Parameter.

Kovarianz und Kontravarianz

Kovarianz und Kontravarianz bestimmen die Kompatibilität basierend auf den Typen. In beiden Fällen ist Varianz eine gerichtete Beziehung. Kovarianz kann als „anders in der gleichen Richtung“ , übersetzt werden oder mit-unterschiedlichen , während Kontra Mitteln „anders in der entgegengesetzten Richtung“ , oder gegen unterschiedliche . Kovariante und kontravariante Typen sind nicht gleich, aber es besteht eine Korrelation zwischen ihnen. Die Namen implizieren die Richtung der Korrelation.

So Kovarianz bedeutet , dass die Vereinbarkeit von zwei Arten impliziert die Kompatibilität der verschiedenen Arten von ihnen abhängig. Bei gegebener Typkompatibilität wird angenommen, dass abhängige Typen kovariant sind, wie in Abbildung 2 dargestellt.

Andreas Solymosi

Die Kompatibilität von to impliziert die Kompatibilität von ) to ). Der abhängige Typ heißt kovariant ; oder genauer gesagt, ) ist kovariant zu ).T1T2A(T1A(T2A(T)A(T1A(T2

Ein weiteres Beispiel: Da die Zuweisung numberArray = integerArray;möglich ist (zumindest in Java), sind die Array-Typen Integer[]und Number[]kovariant. So können wir sagen , dass Integer[]ist implizit covariant zu Number[]. Und während das Gegenteil ist nicht wahr - die Zuordnung integerArray = numberArray;nicht möglich ist - die Zuordnung mit Typ - Casting ( integerArray = (Integer[])numberArray;) ist möglich; deshalb, sagen wir, Number[]ist explizit kovariant zu Integer[].

Zusammenfassend: Integerist implizit kompatibel mit Number, ist daher Integer[]implizit kovariant mit Number[]und Number[]ist explizit kovariant mit Integer[]. Abbildung 3 zeigt.

Andreas Solymosi

Generell können wir sagen, dass Array-Typen in Java kovariant sind. Wir werden uns später in diesem Artikel Beispiele für Kovarianz zwischen generischen Typen ansehen.

Kontravarianz

Kontravarianz ist wie Kovarianz eine gerichtete Beziehung. Während Kovarianz mit-anders bedeutet, bedeutet Kontravarianz gegen-anders . Wie ich bereits erwähnt habe, drücken die Namen die Richtung der Korrelation aus . Es ist auch wichtig zu beachten, dass Varianz kein Attribut von Typen im Allgemeinen ist, sondern nur von abhängigen Typen (wie Arrays und generischen Typen sowie von Methoden, die ich in Teil 2 diskutieren werde).

Ein abhängiger Typ wie A(T)wird als kontravariant bezeichnet, wenn die Kompatibilität von to die Kompatibilität von ) to ) impliziert . Abbildung 4 zeigt.T1T2A(T2A(T1

Andreas Solymosi

Ein Sprachelement (Typ oder Methode) in A(T)Abhängigkeit von Tist kovarianten wenn die Kompatibilität auf die Kompatibilität impliziert ) auf ). Wenn die Kompatibilität der auf die Kompatibilität impliziert ) auf ), wird der Typ ist kontra . Wenn die Kompatibilität von zwischen keine Kompatibilität zwischen ) und ) impliziert , ist dies unveränderlich .T1T2A(T1A(T2T1T2A(T2A(T1A(T)T1T2A(T1A(T2A(T)

Array-Typen in Java sind nicht implizit kontravariant , können jedoch genau wie generische Typen explizit kontravariant sein . Ich werde später in diesem Artikel einige Beispiele anbieten.

Typabhängige Elemente: Methoden und Typen

In Java sind Methoden, Array-Typen und generische (parametrisierte) Typen die typabhängigen Elemente. Methoden sind abhängig von der Art ihrer Parameter. Ein Array-Typ T[]ist abhängig von den Typen seiner Elemente T. Ein generischer Typ Gist abhängig von seinem Typparameter T. Abbildung 5 zeigt.

Andreas Solymosi

Meistens konzentriert sich dieser Artikel auf die Typkompatibilität, obwohl ich gegen Ende von Teil 2 auf die Kompatibilität zwischen Methoden eingehen werde.

Implizite und explizite Typkompatibilität

Earlier, you saw the type T1 being implicitly (or explicitly) compatible to T2. This is only true if the assignment of a variable of type T1 to a variable of type T2 is allowed without (or with) tagging. Type casting is the most frequent way to tag explicit compatibility:

 variableOfTypeT2 = variableOfTypeT1; // implicit compatible variableOfTypeT2 = (T2)variableOfTypeT1; // explicit compatible 

For example, int is implicitly compatible to long and explicitly compatible to short:

 int intVariable = 5; long longVariable = intVariable; // implicit compatible short shortVariable = (short)intVariable; // explicit compatible 

Implicit and explicit compatibility exists not only in assignments, but also in passing parameters from a method call to a method definition and back. Together with input parameters, this means also passing a function result, which you would do as an output parameter.

Note that boolean isn't compatible to any other type, nor can a primitive and a reference type ever be compatible.

Method parameters

We say, a method reads input parameters and writes output parameters. Parameters of primitive types are always input parameters. A return value of a function is always an output parameter. Parameters of reference types can be both: if the method changes the reference (or a primitive parameter), the change remains within the method (meaning it isn't visible outside the method after the call--this is known as call by value). If the method changes the referred object, however, the change remains after being returned from the method--this is known as call by reference.

A (reference) subtype is implicitly compatible to its supertype, and a supertype is explicitly compatible to its subtype. This means that reference types are compatible only within their hierarchy branch--upward implicitly and downward explicitly:

 referenceOfSuperType = referenceOfSubType; // implicit compatible referenceOfSubType = (SubType)referenceOfSuperType; // explicit compatible 

The Java compiler typically allows implicit compatibility for an assignment only if there is no danger of losing information at runtime between the different types. (Note, however, that this rule isn't valid for losing precision, such as in an assignment from int to float.) For example, int is implicitly compatible to long because a long variable holds every int value. In contrast, a short variable does not hold any int values; thus, only explicit compatibility is allowed between these elements.

Andreas Solymosi

Beachten Sie, dass die implizite Kompatibilität in Abbildung 6 davon ausgeht, dass die Beziehung transitiv ist : shortkompatibel mit long.

Ähnlich wie in Abbildung 6 ist es immer möglich, einer Referenz eines Subtyps inteine Referenz eines Supertyps zuzuweisen . Beachten Sie jedoch, dass dieselbe Zuweisung in die andere Richtung eine ClassCastExceptionauslösen kann, sodass der Java-Compiler dies nur mit Typumwandlung zulässt.

Kovarianz und Kontravarianz für Array-Typen

In Java sind einige Array-Typen kovariant und / oder kontravariant. Im Fall der Kovarianz bedeutet dies, dass wenn Tkompatibel mit U, dann T[]auch kompatibel mit U[]. Im Falle einer Kontravarianz bedeutet dies, dass dies U[]kompatibel ist mit T[]. Arrays primitiver Typen sind in Java unveränderlich:

 longArray = intArray; // type error shortArray = (short[])intArray; // type error 

Arrays von Referenztypen sind implizit kovariant und explizit kontravariant , jedoch:

 SuperType[] superArray; SubType[] subArray; ... superArray = subArray; // implicit covariant subArray = (SubType[])superArray; // explicit contravariant 
Andreas Solymosi

Abbildung 7. Implizite Kovarianz für Arrays

Praktisch bedeutet dies, dass eine Zuweisung von Array-Komponenten ArrayStoreExceptionzur Laufzeit ausgelöst werden kann. Wenn eine Array-Referenz SuperTypeauf ein Array-Objekt verweist SubTypeund eine seiner Komponenten einem SuperTypeObjekt zugewiesen ist , gilt Folgendes:

 superArray[1] = new SuperType(); // throws ArrayStoreException 

This is sometimes called the covariance problem. The true problem is not so much the exception (which could be avoided with programming discipline), but that the virtual machine must check every assignment in an array element at runtime. This puts Java at an efficiency disadvantage against languages without covariance (where a compatible assignment for array references is prohibited) or languages like Scala, where covariance can be switched off.

An example for covariance

In a simple example, the array reference is of type Object[] but the array object and the elements are of different classes:

 Object[] objectArray; // array reference objectArray = new String[3]; // array object; compatible assignment objectArray[0] = new Integer(5); // throws ArrayStoreException 

Aufgrund der Kovarianz kann der Compiler die Richtigkeit der letzten Zuordnung zu den Array-Elementen nicht überprüfen - die JVM führt dies mit erheblichem Aufwand durch. Der Compiler kann jedoch die Kosten optimieren, wenn die Typkompatibilität zwischen Array-Typen nicht verwendet wird.

Andreas Solymosi

Denken Sie daran, dass in Java für eine Referenzvariable eines Typs, die auf ein Objekt seines Supertyps verweist, verboten ist: Die Pfeile in Abbildung 8 dürfen nicht nach oben gerichtet sein.

Abweichungen und Platzhalter in generischen Typen

Generische (parametrisierte) Typen sind in Java implizit invariant , was bedeutet, dass verschiedene Instanziierungen eines generischen Typs nicht miteinander kompatibel sind. Selbst Typguss führt nicht zu Kompatibilität:

 Generic superGeneric; Generic subGeneric; subGeneric = (Generic)superGeneric; // type error superGeneric = (Generic)subGeneric; // type error 

Die Typfehler treten jedoch auf subGeneric.getClass() == superGeneric.getClass(). Das Problem ist, dass die Methode den Rohtyp getClass()bestimmt. Aus diesem Grund gehört ein Typparameter nicht zur Signatur einer Methode. Somit sind die beiden Methodendeklarationen

 void method(Generic p); void method(Generic p); 

darf nicht zusammen in einer Schnittstellendefinition (oder einer abstrakten Klasse) vorkommen.