350 likes | 449 Views
Grundlagen der Informatik I Thema 18: Typkonvertierungen und Generizität. Prof. Dr. Max Mühlhäuser Dr. Guido Rößling. Typkonvertierung. Mit statischer Typisierung gibt es viele Kontexte, in denen Werte eines bestimmten Typs erwartet werden.
E N D
Grundlagen der Informatik IThema 18: Typkonvertierungen und Generizität Prof. Dr. Max Mühlhäuser Dr. Guido Rößling
Typkonvertierung • Mit statischer Typisierung gibt es viele Kontexte, in denen Werte eines bestimmten Typs erwartet werden. • In “a = expression” erwarten wir, dass expression den gleichen Typ wie a hat • In “a + b” erwarten wir, dass a und b entweder beide ganze Zahlen, Fließkommazahlen oder Zeichenketten sind. • In “f(a,b)” erwarten wir, dass die Typen der Argumente sich mit denen der formalen Parameter decken.
Arten der Typkonvertierung in Java • Identitätskonvertierung – von einem Typ auf denselben Typ • Es ist z.B. ok, eine redundante Konvertierung einzufügen • Verbreiterung primitiver Typen • z.B. byte nach int (ohne Informationsverlust) • z.B. int nach double (möglicher Informationsverlust) • Verengung primitiver Typen • z.B. int nach byte (höhere Bits wegwerfen) • z.B. float nach int
Arten der Typkonvertierung in Java • Verbreiterung von Referenztypen (Verallgemeinerung) • Gegeben Referenztypen A, B, dann kann A zu B verbreitert werden genau dann wenn A ein Subtyp vonB ist. • Diese Konvertierung geschieht immer während der Kompilierung und kann keine Laufzeitfehler auslösen. • Verengung von Referenztypen (Konkretisierung) • Gegeben Referenztypen A, B, dann kann A verengt werden zu B genau dann wenn B ein Subtyp von A ist. • Diese Konvertierung muss zur Laufzeit überprüft werden und löst möglicherweise einen Laufzeitfehler aus.
Arten der Typkonvertierung in Java • String Konvertierung • Jeder Typ kann zu String konvertiert werden • Implizit wird die toString()-Methode (aus java.lang.Object) benutzt • Das haben Sie z.B. bei System.out.println(myObject)schon genutzt • Boxing und Unboxing • Von byte zu Byte, int zu Integer etc. und umgekehrt • Ungeprüfte Konvertierung • Konvertierungen die zu einem Fehler führen können, ergeben Warnung zur Kompilierzeit • z.B. Konvertierung eines “raw type” zu einem parametrisierten Typ
Konvertierungskontexte • Zuweisungskonvertierung:v = expr • Konvertiere den Typ von expr zum Typ von v. • Nur für Konvertierungen ohne mögliche Laufzeitfehler • Methodenaufrufkonvertierung:expr.M(expr‘) • konvertiert die Typen der Argumente. • Nur für Konvertierungen ohne mögliche Laufzeitfehler • Cast-Konvertierung:(T)expr • Konvertiert den Typ von exprzu T • Kann auch die Verengungskonvertierung benutzen! • Numerische Konvertierung • Konvertiert die Operanden eines numerischen Operators in einen gemeinsamen Typen, so dass die Operation ausgeführt werden kann • erlaubt Identitäts-, Verbreiterungs- und Unboxing Konvertierung • 4 + 2.0 6.0
Typ-Verengung • Verbreiterung verliert statische Typinformation // p bezieht sich auf Circle, Verbreiterung OKGraphicObject p = new Circle(5, 12, 4); Circle c1 = p; // compile-Fehler — kann nicht verengen • Die Information kann zur Laufzeit mit Tests und Casts wieder zurückgeholt werden kann if (p instanceof Circle) { // Laufzeittest c1 = (Circle) p; // Explizite Verengung} // zur Laufzeit OK
Nochmals Collections… • Eine Collection sollte zu mehr als einem Typ passen • Eine Implementierung langt für verschiedene Zwecke • Das allgemeine Verhalten der Collection hängt nicht vom Elementtyp ab • Zusätzlich wollen wir einen spezifischenElementtypgarantiert haben • Angenommen, wir nutzen eine Collection nur für Person Instanzen • Dann wollen wir auch Person Objekte ohne Verendung von Objecterhalten können!
Nochmals Collections… List myIntList = newLinkedList(); // 1 myIntList.add(new Integer(0)); // 2 Integer x = (Integer)myIntList.get(0); // 3 • Der Cast in Zeile ist nervig aber erforderlich • Java garantiert nur, dass das Ergebnis zum Typ Object passt • Es kann also vom Typ Integer sein – oder eben auch nicht! • Daher kriegen wir einen Typfehler ohne den Cast • Zudem wurde der Cast nicht mit instanceofabgesichert • Wir könnten uns auch irren und etwas anderes erhalten List myIntList = newLinkedList(); // 1 myIntList.add("Hello World"); // 2’ Integer x = (Integer)myIntList.get(0); // 3 • Nun erhalten wir eine ClassCastException zur Laufzeit!
Angabe der Absichten mit Generics • Wir müssen Java mehr über unsere Absichten sagen • “Diese Liste will nur Elemente haben, die zu Integer passen” • Dafür müssen wir…: • Die Deklaration der Liste anpassen: List<Integer> • Das Anlegen der Liste anpassen: newLinkedList<Integer> • Vorteile: • Wir können auf den Cast in Zeile 3 verzichten • Typen, die nicht zu Integer passen, können nicht eingefügt oder abgefragt werden List<Integer> myIntList = newLinkedList<Integer>(); // 1 myIntList.add(new Integer(0)); // 2 Integer x = myIntList.get(0); // 3 myIntList.add("Hello World"); //compile-Fehler
Was hat sich geändert? • myIntList ist nun eine List<Integer> • Nicht „eine“ Liste, sondern eine Liste von Integer-Objekten • Wir müssen Ergebnisse also nicht mehr nach Integercasten • Wir können keine anderen Typen mehr speichern • Darum kümmern sich der Java Compiler (und Eclipse) • myIntList.add("Hello World"); • The method add(Integer)in the type List<Integer> is not applicable for the arguments (String)
Und was bringt uns das? • Auf den ersten Blick vielleicht nicht viel • Die Deklaration und Erzeugung der Liste sind länger • Wir können nur auf den Cast verzichten • Es gibt aber einen großen Unterschied! • Nun kann der Compiler die Typkorrektheit überprüfen • Die Deklaration von myIntList gibt den Typ an • Der Compiler stellt sicher, dass alle Zugriffe zum Typ passen • Was ist hier anders als bei einem Cast? • Mit einem Cast sagt der Programmierer “der Typ sollte hier zum Cast zu Typ X passen” • Das sollte immer mit instanceofabgesichert werden! • Generische Deklaration sagt “das passt immer zu Typ X“
Nochmals Listen List und Iterator werden in java.util wie folgt definiert: public interface List<E> { void add(E x); Iterator<E> iterator(); } publicinterface Iterator<E> { E next(); boolean hasNext(); void remove(); } • Das kennen wir im Wesentlichen schon (siehe T15) • Außer dem <E> und E • Damit wird ein formaler Typparameterdeklariert • E kann dann in der Klasse als Typ genutzt werden
List<Integer> und IntegerList • Wir nutzen List<Integer>, aber es gibt nur List<E>? • E ist ein formaler Typparameter • Ähnlich zu den formalen Parametern in Methodensignaturen • E wird in allen Aufrufen durch Integer ersetzt • Intuitiv kann man sich das so vorstellen: publicinterfaceIntegerList { voidadd(Integer x); IntegerIteratoriterator(); } • Das kann das Verständnis erleichtern • Aber es ist auch irreführend • Es gibt nur eine List Klasse, nicht “eine pro Typ”
Generics und Subtypen • Warum ist der folgende Code nicht korrekt? • Zeile 1 ist sicherlich korrekt • LinkedList ist ein Untertyp von List • Die formalen Parameter (beide String) „passen“ • Passt List<String> in Zeile 2 zu List<Object>? • List ist auf beiden Seiten die gleiche Basisklasse • String ist ein Erbe von Object • Die intuitive Antwort ist also „ja” List<String> ls = newLinkedList<String>(); // 1 List<Object> lo = ls; // 2 lo.add(newObject()); // 3 String s = ls.get(0); // 4 Dieser Code wird vom Übersetzer so nicht akzeptiert!
Generics und Subtypen • Zunächst schauen wir uns den weiteren Code an: List<String> ls = newLinkedList<String>(); // 1 List<Object> lo = ls; // 2 lo.add(newObject()); // 3 String s = ls.get(0); // 4 Dieser Code wird vom Übersetzer so nicht akzeptiert! • Zeile 2 legt ein Alias der Object und String List an • Wir können ein Object in List<Object> einfügen (Zeile 3) • Aber wir fügen es gleichzeitig auch in lsein! • ls ist deklariert als List<String>… • Aber lswürde nun auch andere Objekttypen enthalten • Der Compiler erkennt das nicht, wenn Zeile 2 korrekt ist • Daher wird der Compiler Line 2 zurückweisen • “type mismatch: cannotconvertfromList<String>toList<Object>”
Generics, Subtypen und unsere Intuition • Sei Y eine Unterklasse von (Klasse oder Interface) X • Gsei eine generische Typdeklaration (z.B. List<E>) • G<Y>ist kein Subtyp von G<X> • Warum fällt uns das so schwer zu glauben? • Wir unterstellen, dass sich die Collection nicht ändert • Das ist natürlich oft eine falsche Annahme! • Angenommen, das Studierendensekretariat gibt eine Liste der Studenten an das Einwohnermeldeamt • Ein Student ist eine Person • Übergabe von List<Student> für List<Person>wirkt OK • Aber nur, wenn eine Kopie übergeben wird, keine Referenz! • Das Einwohnermeldeamt fügt eine Person (aber nicht Student) ein • Die gemeinsame (!) Studentenliste ist nun korrumpiert
Generics, Subtypen und Intuition Collection<Vehicle> Collection<Car> Punktweise Subtypen, sicher Set<Vehicle> Set<Car> Kovariante Subtypen nicht sicher
Behandlung von Subtypen: Wildcards • Wir wollen alle Elemente einer Collection ausgeben • Ohne Generics funktioniert das in etwa wie folgt: • Jetzt passen wir das eben schnell an Generics an: • Aber diese Methode akzeptiert nurCollection<Object> • Sie akzeptiert keineList<Person>, sondern nur List<Object> • Zur Erinnerung: List<Person> ist kein Subtypvon List<Object> voidprintCollection(Collection c) { for (Objectelement: c) System.out.println(element); } void printCollection(Collection<Object> c) { for (Object element: c) System.out.println(element); }
Behandlung von Subtypen: Wildcards • Was müssen wir nun tun? • Wir brauchen einen gemeinsamen Obertypfür alle Collection-Arten • Das ist die “Collection von unbekannten Elementtypen” • In Java-Notation: Collection<?> (hier: List<?>) • Nun können wir den Code anpassen: • Die Elemente der Liste werden wie Objectbehandelt • Da java.lang.Object die Superklasse aller Typen ist • Daher passt jeder Typ – auch der „unbekannte Typ ?“ - zu Object voidprintCollection(Collection<?> c) { for (Objectelement: c) System.out.println(element); }
The unbekannte Typ „<?>“ • Der „Unbekannte Typ“ <?> wirkt sehr hilfreich • Wir können endlich auf die Elemente zugreifen! • Falls nötig, können wir sie auf einen konkreten Typ casten • Natürlich nur wenn der Cast gültig ist! • Wie steht es mit dem folgenden Code? • c hat den “unbekannten” Elementtyp “?” • Der einzufügende Typ muss zu “?” passen • Der Compiler kennt den tatsächlichen Typ von “?” nicht • Daher ist unklar, ob Object dazu passt • Man darf kein Object einfügen in eine List<String>! • Es kann nur der Wert null eingefügt werden Collection<?> c = new ArrayList<String>(); c.add(newObject()); Dieser Code wird vom Übersetzer so nicht akzeptiert!
Eine generische Zeichenanwendung • Wir betrachten folgendes Modell für ein Zeichentool: abstractclassShape { abstractvoiddraw(Canvas c); } class Circle extends Shape { privateint x, y, radius; voiddraw(Canvas c) { // ... } } classRectangleextends Shape { privateint x, y, width, height; voiddraw(Canvas c) { // ... } } classCanvas { voiddraw(Shape s) { s.draw(this); } }
Eine generische Zeichenanwendung • Jede Form erweitert Shape und kann sich zeichnen • Wir werden meist mehr als eine Form zeichnen! • Also speichern wir die Elemente in einer List<Shapes> • Wir passen den Code von Canvasentsprechend an: • Das funktioniert gut für jede List<Shape> • Und wenn wir eine List<Circle> zeichnen wollen? • Circle ist ein Subtyp von Shape • Aber List<Circle> ist kein Subtyp vonList<Shape> • Der Aufruf drawAll(List<Circle>) führt zu einem Compile-Fehler voiddrawAll(List<Shape> shapes) { for(Shape s: shapes) { s.draw(this); } }
Eine generische Zeichenanwendung • drawAll passt an sich zu allen Shape-Subtypen • drawAll sollte mit List<Circle>, List<Shape>, List<Rectangle>, funktionieren…! • Der Einsatz von “?” bringt uns nicht weiter • Der unbekannte Typ ? hat keine Methode “draw(Canvas)” • Der Cast nach Shape ist möglich aber gefährlich • Wir müssen die Wildcard beschränken (“bound”) • Sie sollte alle Listen von Subtypen von Shape akzeptieren • In Java-Notation: voiddrawAll(List<? extends Shape> shapes) { for(Shape s: shapes) { s.draw(this); } }
Beschränkte Wildcards • Es gibt einen kleinen aber wichtigen Unterschied • List<Shape> akzeptiert nur genau List<Shape> • List<?extends Shape> akzeptiert Listen von allen Subtypen von Shape, inklusive Shape selbst • List<Shape> • List<Circle> • List<Rectangle> • … • “? extends X” bedeutet: • Wir kennen den exakten Type nicht ( “?”) • Aber wir wissen, dass der Typ zu X konform sein muss • X ist die „obere Schranke“ der Wildcard
Beschränkte Wildcards • Warum stimmt am folgenden Code nicht? • Wir können kein Rechteck in List<? extends Shape> einfügen • Wir kennen den exakten Typ der Elemente in shapes nicht • Wir wissen zwar, dass es ein Subtyp von Shape ist • Aber der Typ passt nicht unbedingt zu Rectangle • Der Aufruf könnte lauten addRectangle(List<Circle>); • Bei unbekanntem Typ können wir nur null einfügen • Einerseits schlecht… • Dafür gibt es keine Typprobleme zur Laufzeit! • Java geht hier den (typ-)sicheren Weg. voidaddRectangle(List<? extends Shape> shapes) { shapes.add(0, newRectangle()); } Dieser Code wird vom Übersetzer so nicht akzeptiert!
Schreiben von generischen Methoden • Wie kopiert man alle Arrayelemente in eine Collection? • Wir können nicht Collection<Object> als Parameter nutzen • Aber Collection<?> wird auch nicht funktionieren! • Der Typ ist unbekannt, also passt jeder konkrete Typ potentiell nicht static void copyToCollection(Object[] array, Collection<?> c) { for (Object o: array) { c.add(o); } } Dieser Code wird vom Übersetzer so nicht akzeptiert!
Schreiben von generischen Methoden • Dazu schreiben wir eine generische Methode: • Das <T> besagt, dass dies eine generische Methode ist • T wird vor dem Rückgabewert eingeführt • T dient als formaler Parametertyp • Der Java Compiler nutzt das für Zugangskontrolle • Der Typ des Feldes und der Collection müssen konform sein • Es gilt der allgemeinste konforme Typ • Meist langt die Nutzung von Wildcards • Wildcards werden bevorzugt – klarer und knapper static <T> voidcopyToCollection(T[]array, Collection<T> c) { for (T o: array) { c.add(o); } }
Abfrage des Typs • Abfrage des Basistyps eines generischen Typs: • Aber wir erhalten nicht den genauen generischen Typ • Es gibt nur eine Klasse List (siehe Folie 15) • Die compile-Zeit Information über den formalen Parameter ist zur Laufzeit nicht verfügbar • Wir können auch getClass() nutzen: • Die Ausgabe ist wieder true – getClass() liefert java.util.List List<Circle> circles = new ArrayList<Circle>(); List<Rectangle> rects = new ArrayList<Rectangle>(); System.out.println(circlesinstanceof List); // true System.out.println(circlesinstanceof List<Circle>); // Fehler System.out.println(circles.getClass() == rects.getClass());
Untere Schranken • Wir wollen eine Datensenke implementieren • Diese leert eine Collection und gibt das letzte Element zurück • Was ist hier das generische Typargument <T>? • Es gibt keinen gültigen Typ <T>, da die Typen nicht konform sind • Der Aufruf in der letzten Zeile ist damit illegal interface Sink<T> { voidflush(T t); } static<T> T flushAll(Collection<T> c, Sink <T> sink) { T last = null; for (T t: c) { last = t; sink.flush(last); } return last; } Sink<Object> sink; Collection<String> collection; String lastString = flushAll(collection, sink); Dieser Code wird vom Übersetzer so nicht akzeptiert!
Untere Schranken • Das scheint einfach zu beheben zu sein…: • Jetzt funktioniert der Aufruf! • Aber: T wird nun auf Object abgebildet • Weil das der Elementtyp des Sinks ist • Daher scheitert die Zuweisung des Ergebnisses an String • Wir benötigen “unbekannt, aber Oberklasse von T” • In Java-Notation: <? super T> static <T> T flushAll(Collection<? extends T> c, Sink<T> sink) { static <T> T flushAll(Collection<T> c, Sink<? super T> sink) {
Implementierungsbeispiel interface Sink<T> { voidflush(T t); } classConcreteSink<T>implementsSink<T> { publicvoidflush(T t) { System.err.println("Flushing " + t + ", type: " +t.getClass().getName()); } } static<T> T flushAll(Collection<T> c, Sink <? super T> sink) { T last = null; for(T t: c) { last = t; sink.flush(last); } returnlast; } Sink<Object> sink = newConcreteSink<Object>(); Collection<String> cs = Arrays.asList("a","bb2","cdf"); System.err.println(flushAll(cs, sink)); Bitte denken Sie sich den jeweiligen Klassenkontext dazu!
Statische Typisierung - Zusammenfassung • Statische Typsysteme sind Gegenstand sehr aktiver Forschung • Java-ähnliche Typsysteme sind begrenzt, aber im Allgemeinen können Typsysteme sehr mächtig und ausdrucksstark sein • Aber auch sehr kompliziert • Manche Programmierer sehen statische Typsysteme als eine Begrenzung ihrer Freiheit ("ich weiß, was ich tue") • Andere Programmierer denken, dass statische Typsysteme nicht nur viele Fehler erkennen, sondern auch eine gute Struktur im Code erzwingen ("erst denken, dann schreiben") • Die Diskussion ist noch nicht beendet. • Sie sollten eine fundierteMeinung darüber haben!
Weiterführende Literatur • Die folgenden Materialien bieten (viel) mehr zu Generics: • Java Tutorial on Generics (byGiladBracha): http://java.sun.com/docs/books/tutorial/extra/generics/ • GiladBracha, “Generics in the Java Programming Language” • Diente als Hauptvorlage für diese Folien • http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf • Maurice Naftalin, Philip Wadler: “Java GenericsandCollections”, O’Reilly, 2006