Verfasste Forenbeiträge
-
AutorBeiträge
-
Beispiel (kompiliert nicht):
class A<T> { T t; void f(T x) { x.m(); } } class X { void m() {} } class Y extends X {}Beispielobjekte:
X g = new X(); Y h = new Y();Was gibt es zu beachten?
- Eine generische Klasse gibt es nicht ohne eingesetzten Typ. Es gibt z. B. Objekte der Klassen
A<X>undA<Y>undA<String>, es gibt aber keine Objekte der KlasseA, weil es die “allgemeine” KlasseAnicht gibt. AuchA<T>gibt es nicht, weilTkeine Klasse ist. Der generische TypTkönnte zusätzlich eingeschränkt sein, z. B.class A<T extends X>, dann wäreA<String>nicht mehr möglich, weil fürTdann mindestensX(oder spezieller, alsoYgeht auch) eingesetzt werdem muss.
- Der generische Typ gibt nicht die Vererbungshierarchie vor. Während die Zuweisung von einem
Yan eine Variable vom TypXmöglich wäre (d. h.X var = new Y();geht), daYspezieller ist alsX, gilt das nicht für die KlasseA<...>. Weder die Zuweisung vonA<Y>anA<X>noch umgekehrt wäre möglich (d. h.A<X> var = new A<Y>();ist ein Compiler-Fehler), weil die KlassenA<Y>undA<X>überhaupt nichts miteinander zu tun haben.
- Generische Typen sind nur Platzhalter. Angenommen es existiert eine Variable
A<Y> m, dann gibt es dafür die Methodevoid f(Y x)(siehe Beispiel oben). Der Aufrufm.f(h)wäre möglich,m.f(g)wäre nicht möglich. Für eine VariableA<X> nwären beide Aufrufe möglich. Für eine VariableA<String> owäre keiner der beide Aufrufe möglich.
- Type Erasure: Generische Typen werden nicht als Platzhalter übersetzt, sondern wird beim Kompilieren der allgemeinste Typ verwendet. Vielleicht fragst du dich, warum das Beispiel eigentlich gar nicht kompiliert. Der Grund dafür ist Type Erasure. Der Compiler übersetzt “A” nicht in allen möglichen Formen (
A<X>,A<Y>,A<Integer>, …), weil es dafür ja unendlich viele Möglichkeiten gäbe. Obwohl es die Klasse in unendlich vielen verschiedenen Ausführungen gibt, wird sie nur einmal übersetzt – und zwar alsA<Object>, d. h.Twird durchObjectersetzt. Angenommen wir hätte einA<X> a = new A<X>();und rufen nun die Methodefmit unseremX-Objekt von oben auf, alsoa.f(g), dann wird in derf-Methode jax.m()aufgerufen (wobeix = ggilt). Die Methodem()gibt es aber aus statischer Sicht nicht… Warum? Der statische Typ vonxist innerhalb von der “allgemeinen” Klasse A leiderObject(eben wegen Type Erasure). Wegen Zeile 2 kompiliert das Programm also nicht. Der sog. raw type ist deshalbObject, weilTnicht eingeschränkt ist. Wäre die Definition stattdessenA<T extends X>, so würdeTdurchXstatt Objekt erased werden und alles würde wie erwartet laufen.
Auch bei Attributtypen werden entsprechend ersetzt. Betrachten wir wiederA<X> a = new A<X>();, dann ist der statische Typ vona.thierXund der Aufrufa.t.m()wäre möglich. Benutzen wir das Attribut aber innerhalb von A, dann ist der Typ vontwiederObject… Währenda.t.m()also funktioniert, wäre der Aufruft.m()innerhalb von A nicht möglich (aus dem gleichen Grund weshalb auchx.m()nicht möglich ist).
Type Erasure kann noch komplizierter werden, wenn es bspw. um Überschreibung geht (siehe Altklausur WS 18/19 Wdh.). Nähere Erklärungen dazu mache ich hier nicht, weil das zu kompliziert zu schreiben ist. Man lernt es außerdem besser, indem selbst Beispiele in der IDE macht. Type Erasure haben wir bspw. bei Aufruf 8 in meiner GenericPoly Aufgabe – schau dir das mal an + Erklärung dazu.
Genau. Bei
breakverlässt man dasswitch. Ansonsten (wenn auch nichtcontinueoderreturnkommt) geht es mit dem nächsten Case weiter. Hintergrund ist eigentlich, dass man so mehrere Cases zusammenfassen kann, vgl. Kapitel 5.2 “x == 2 || x == 3“.Im
case 2wirda += 30ausgeführt. Nachdem wir keinbreakhaben geht geht es danach in dencase 3und es wirdbreak l1ausgeführt.Diesen Fall gibt es in genau dieser Form übrigens auch mehrmals in den Skript-Aufgaben (siehe Teilaufgabe i) und l)). Du solltest überprüfen, ob du das dort richtig gemacht hast.
Hier das vollständige Objektdiagramm zur Polymorphie-Aufgabe aus der Wiederholungsklausur 2017/18. Außerdem habe ich den ersten Aufruf nach dem gezeigten Schema gelöst. Attribute kann man dafür im Objektdiagramm ablesen – man muss nur wissen auf welchem Objekt man gerade operiert (also worauf man die Methode aufgerufen hat, wie bei
this). Der zweite Aufruf ist ziemlich ähnlich zum ersten. Ich habe leider keine Zeit, diese Aufgabe detaillierter zu erklären, aber ich hoffe, dir hilft das weiter.
Falls das Bild nicht oder unscharf angezeigt werden sollte, findest du es hier in voller Auflösung.
Wenn du
b.a.a.foo()hättest, dann benötigst du den statischen und dynamischen Typ vonb.a.a, um die Methodefoo()aufzurufen. Dazu gehen wir von links nach rechts:bist statischBund dynamischB, also suchen wir in unseremB-Objekt erstmal ein Attributa, umb.azu bestimmen. Dieses Attributamuss ausB(oder einer Oberklasse vonB) kommen, welches wir hier finden (das ist das einzige a in demB-Objekt).b.aist somit statischB, dynamischC(das obere der beidenCs). Wir suchen also in dem oberenC-Objekt ein Attributa, umb.a.azu bestimmen. Dieses Attributamuss ausB(oder einer Oberklasse vonB) kommen, welches wir hier finden – das ist das AttributaA. DasaC sehen wir nicht, da der statische TypBist. Der statische Typ vonb.a.aist somitB, der dynamischeC(das untere der beiden).- h)
X<A> h = new X<C>();kompiliert nicht, weil der Typ der Variable (X<A>) nichts mit dem Typ des Objekts (X<C>) zu tun hat, d. h. weder erbtX<A>vonX<C>noch umgekehrt. Die Klassen haben genauso wenig miteinander zu tun wieStringundStack– gar nichts. Dass die generischen Typen in einer Vererbungshierarchie stehen ist schön, tut aber nichts zur Sache –X<A>erbt vonObject,X<C>erbt vonObject. - q) class
Q<T> extends X<T> {}kompiliert nicht, weil der Typparameter vonQ(alsoT) nicht eingeschränkt ist, d. h. überTist lediglich bekannt, dass es mindestensObjectsein muss. Die OberklasseXverlangt aber einen generischen Typ, der mindestensAist (<T extends A>). - u)
class U<K extends A> extends X<D> {};definiert zunächst eine Unterklasse vonX<D>, d. h. bspw. es gibt die KlasseU<A>, die vonX<D>erbt; es gibt die KlasseU<B>, die vonX<D>erbt; es gibt die KlasseU<C>, die vonX<D>erbt; und es gibt die KlasseU<D>, die vonX<D>erbt. Dass hier der Typparameter nicht durchgegeben wird ist okay – muss man ja nicht.
Das Problem liegt beiX<B> u = new U<B>();, dennU<B>ist keine Unterklasse vonX<B>, also kompiliert es nicht.U<B>erbt vonX<D>, nicht vonX<B>.
Du hast es genau richtig erklärt. Bei den anderen Teilaufgaben ist es wie bei g). Z. B. bei k) wird versucht, eine Variable vom Typ
Ian eine Variable vom TypCzuzuweisen. Der Compiler sieht nur diese statischen Typen und überprüft nicht, was tatsächlich (dynamisch) gespeichert wird. Eine Variable vom TypIkönnte zwar einC(oderD) speichern (wie es hier der Fall ist), aber theoretisch (aus Compiler-Sicht) auch ein Objekt jedes anderen Typs der mindestensIist (also jede Klasse, dieIimplementiert). Es könnte ja nebenCnoch eine KlasseFgeben, dieIimplementiert. Die Zuweisung funktioniert daher immer nur dann, wenn der (statische) Typ der rechten Seite der Zuweisung mindestens so speziell ist wie der Typ der Variable, an die zugewiesen wird (linke Seite). Also kannst du an eine Variable vom TypCnurCoderDzuweisen. An eine Variable vom TypIkannst duC,DoderIzuweisen. Die rechte Seite muss also den gleichen Typ haben oder einen Typ, der in der Vererbungshierarchie darunter steht (aber nicht darüber)!Der Aufruf 8 kommt ja irgendwann zu
e.g(f). Der statische Typ voneist hier jedoch nichtFsondernE. Grund dafür ist Type Erasure, weil der Aufruf innerhalb vonC<?>stattfindet. Der Compiler weiß nicht, dass wir hier fix inC<F>sind (wir könnten irgendwann ja auch mal inC<D>sein). Der statische Typ muss für einen bestimmten Kontext aber immer fix sein. Deshalb wird füreder allgemeinste mögliche Typ genommen und das istE(wegenC<R extends E>). Dann suchen wirg(F)inE, finden dortg(E)und überschreiben es anschließend inF.Das Attribut bestimmten wir immer über den statischen Typ, also den Typ der Variable, von der wir auf ein Attribut zugreifen bzw. von der Klasse, in der wir uns befinden. Bei
d.eist der statische Typ vondbspw.D(dynamischF), also wollen wir imF-Objekt die Variablee. Dort gibt es zwei, weilFdaseAttribut ausDundAerbt. Wir wählen das ausDgeerbtee, weil der statische Typ vondDist, also besteht nur Zugriff auf die Variablen ausDund Oberklassen vonD.Ich habe jetzt zusätzlich zu den Lösungen noch Diagramme hinzugefügt. Unter den Pfeilen steht außerdem, wegen welchem Statement der jeweilige Pfeil eingezeichnet werden musste. Die Statements sind farbig hinterlegt, sodass du erkennen kannst, durch welches Statement welches Objekt erzeugt wurde. Für ausführlichere Erklärungen habe ich momentan leider zu wenig Zeit, sonst wären die Aufgaben auch im Skript.
Das liegt daran, dass generische Typen nur in einem objektorientierten Kontext benutzt werden können. Erzeugt man beispielsweise ein
Foo<String>Objekt, so wäre klar, dass die MethodemeinenStringerwartet und auch einenStringzurückgibt. Das wäre richtig, wenn die Methode nichtstaticwäre. Statische Methoden müssen aber nicht auf einem Objekt aufgerufen werden. Man ruft sie i. d. R. über den Klassennamen auf, also hier mittelsFoo.m(...). Nun fehlt der Typparameter. Man könnte argumentieren, dass man den Typparameter ja angeben könnte, beispielsweise mittelsFoo<String>.m(...). Das ist richtig, trotzdem funktioniert es bei statischen Methoden nicht. Warum genau das nicht funktioniert kann ich dir nicht sagen – eventuell hat man sich einfach dafür entschieden (weil meiner Meinung nach könnte es theoretisch auch möglich sein).Für dich heißt das, dass du dir einfach merkst, dass man generische Typen nicht in einem statischen Kontext benutzen kann. Möchte man das, so benötigt man eine generische Methode. Man müsste hier also
public static <R> R m(R r)schreiben. Das<R>führt den neuen TypparameterRein (könnte auch anders heißen, hat nichts mit demRausFoo<R>zu tun, dieses R wäre dann überflüssig).Weil es sich um eine private Methode handelt und private Methoden nur innerhalb der Klasse selbst sichtbar sind. Du kannst diese Methode also nur dann benutzen, wenn der Aufruf in der Klasse F selbst stattfindet. Das ist hier nicht der Fall, da alle Aufrufe außerhalb stehen (also nicht in
class F { ... }).Dieser Schritt wird gemacht, um das Zeichen (z. B.
'4') in eine Ziffer (z. B.4) umzuwandeln.Hintergrund:
int c = '4'; System.out.println(c);Dieser Code gibt den Wert 52 aus, weil 52 der ASCII-Wert des Zeichens ‘4’ ist und wir das Zeichen in einer int-Variable speichern. Genauso sieht das auch bei der chars()-Methode aus. Diese gibt dir keinen
CharStream(diese Klasse existiert nicht) sondern einenIntStream. Um aus dem Zeichen ‘4’ (welches im Stream als 52 gepseichert ist) nun die “richtige” Ziffer 4 zu machen, müssen wir das Zeichen ‘0’ subtrahieren. Dazu muss man die ASCII-Werte nicht kennen – es genügt zu wissen, dass die Entfernung zwischen ‘4’ und ‘0’ genau 4 beträgt. Das kannst du aber auch an der ASCII-Tabelle sehen. ‘4’ hat den Wert 52, ‘0’ hat den Wert 48, 52-48 ergibt 4. Genauso wäre das bei Buchstaben. Um aus einem Keinbuchstaben einen Großbuchstaben zu machen schreibt manc-'a'+'A', d. h. man subtrahiert erst das kleine a, um den Offset (Entfernung des Zeichens zum kleinena) zu kennen (bspw. 3, falls c ein'd'ist) und addiert diesen Offset dann zum großenA(z. B.'A'+3=='D').Es geht um den Aufruf
a.f(this):aist dynamisch nichtCsondernA!thisist statischC, weil wir in der Klasse C sind, und dynamisch ebenfalls C, weil beim vorherigen Aufruf (c.f((C)c)) ein C-Objekt vor dem Punkt stand, d. h. wir operieren auf einem C-Objekt.- Nachdem wir also auf einem C-Objekt operieren, handelt es sich bei
aum das Attributades C-Objekts, das ursprünglich durchA a = new A()innerhalb des A-Kontruktors mitnew C(this)erzeugt wurde . Dort wirdthis(also während der Erzeugung von A das A-Objekt selbst) übergeben. Das C-Objekt speichert sich genau dieses A-Objekt in seinem Attributaab (mittelsthis.a = a), d. h. das hier verwendeteahat den dynamischen Typ A (und zwar genau das A-Objekt, das auch in dermain-Methode gespeichert wird).
Daher suchen wir in A eine Methode f(C), finden statisch f(B) und führen diese Methode auch aus.
Nein,
t[i+1] – t[i]könnte negativ sein. Dann würdest du nie in den Fall “> schwank” kommen. Der Teilif (schwank <0) schwank = -schwank;sollte also irgendwie vor dem Vergleich mitschwankpassieren.Außerdem musst du
maxvor der Schleife definieren, sonst kannst du es am Ende nicht zurückgeben (bei dir ist das nur lokal in der Schleife definiert).maxspeichert bei dir auch nicht den richtigen Wert, weil du es einfach immer überschreibst, d. h. die Variable speichert am Ende immer den letzten Tag (bei dem es noch zwei Werte gibt). Du darfstmaxnur dann überschreiben, wenn du auchschwanküberschreibst.Zusatzaufgabe: Polymorphie mit parametrisierten Klassen (Generics)
Die Java-Dateien der Polymorphie-Aufgaben aus dem Crashkurs findest du hier.
Die unkommentierte Lösung zu dieser Aufgabe findest du hier, ein Objektdiagramm hier.public class GenericPoly { static class A<T extends B> { protected T e; public void set(T elem) { e = elem; } void f(B b) { System.out.println("A.f(B)"); } void f(C<E> c) { System.out.println("A.f(C)"); e.f(e); c.f(e); } void g(F f) { System.out.println("A.g(F)"); } } static class B extends A<B> { public B() { set(this); } void f(C<E> c) { System.out.println("B.f(C)"); } void f(F f) { System.out.println("B.f(F)"); this.g(f); } } static class C<R extends E> extends B { public R e; public C(R r) { this.e = r; } void f(B b) { System.out.println("C.f(B)"); } void f(A<C<E>> a) { System.out.println("C.f(A)"); a.f(this); } void g(F f) { System.out.println("C.g(F)"); e.g(f); } } static class D extends A<C<E>> implements E { protected A<B> e; public D() { e = new C<D>(this); } public void f(D d) { System.out.println("D.f(D)"); e.f(super.e); } public void g(F f) { System.out.println("D.g(F)"); } public void g(E e) { System.out.println("D.g(E)"); } } interface E { void g(E e); } static class F extends D { void f(A<C<F>> a) { System.out.println("F.f(A)"); this.f(a.e); } public void f(D d) { System.out.println("F.f(D)"); super.f(this); } } public static void main(String[] args) { A<C<F>> a = new A<>(); C<F> c = new C<>(new F()); a.set(c); D d = c.e; d.f(c); // Aufruf 1 c.f(a.e); // Aufruf 2 d.f(a); // Aufruf 3 d.f(d); // Aufruf 4 c.f(d); // Aufruf 5 d.e.f(c); // Aufruf 6 ((F)d).f(a); // Aufruf 7 c.f((F)d); // Aufruf 8 a.f(((A<C<E>>)d).e); // Aufruf 9 } }
Zusatzaufgabe: Polymorphie mit Attributen
Die Java-Dateien der Polymorphie-Aufgaben aus dem Crashkurs findest du hier.
Die unkommentierte Lösung zu dieser Aufgabe findest du hier, ein Objektdiagramm hier.public class AttributPoly { static class A { protected B a; public A() { a = new C(this); } public A(B b) { set(b); } public void set(B b) { a = b; } void f(B b) { System.out.println("A.f(B)"); a.f(this); } } static class B extends A { public B(B b) { super(b); } public B() { } void f(A a) { System.out.println("B.f(A)"); this.a.f(a); } void f(B b) { System.out.println("B.f(B)"); } } static class C extends B { private A a; public C(A a) { super(null); this.a = a; } void f(A a) { System.out.println("C.f(A)"); } void f(B b) { System.out.println("C.f(B)"); a.f(this); } void f(C c) { System.out.println("C.f(C)"); } } public static void main(String[] args) { B b = new B(); A a = new A(); B c = a.a; c.set(b); b.a.set(c); b.f(a); // Aufruf 1 c.f((C)c); // Aufruf 2 a.f(a); // Aufruf 3 b.a.f(c); // Aufruf 4 a.f(b); // Aufruf 5 ((C)c).a.f(c); // Aufruf 6 ((C) a.a.a.a).a.a.a.f(a); // Aufruf 7 } }
Perfekt 🙂
Wenn der Compiler beim Cast zwischen Book und Medium meckert ist das ein Indiz dafür, dass die Klassen in keiner Vererbungsrelation stehen. Book sollte von Medium erben – fehlt diese Ergänzung bei dir vielleicht?
Ich bin bemüht, so schnell wie möglich auf deine Frage zu antworten, bitte aber um Verständnis, wenn es länger dauert (insb. bei langen Fragen).
- Eine generische Klasse gibt es nicht ohne eingesetzten Typ. Es gibt z. B. Objekte der Klassen
-
AutorBeiträge
