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” KlasseA
nicht gibt. AuchA<T>
gibt es nicht, weilT
keine Klasse ist. Der generische TypT
könnte zusätzlich eingeschränkt sein, z. B.class A<T extends X>
, dann wäreA<String>
nicht mehr möglich, weil fürT
dann mindestensX
(oder spezieller, alsoY
geht auch) eingesetzt werdem muss.
- Der generische Typ gibt nicht die Vererbungshierarchie vor. Während die Zuweisung von einem
Y
an eine Variable vom TypX
möglich wäre (d. h.X var = new Y();
geht), daY
spezieller 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> n
wären beide Aufrufe möglich. Für eine VariableA<String> o
wä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.T
wird durchObject
ersetzt. Angenommen wir hätte einA<X> a = new A<X>();
und rufen nun die Methodef
mit unseremX
-Objekt von oben auf, alsoa.f(g)
, dann wird in derf
-Methode jax.m()
aufgerufen (wobeix = g
gilt). Die Methodem()
gibt es aber aus statischer Sicht nicht… Warum? Der statische Typ vonx
ist 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
, weilT
nicht eingeschränkt ist. Wäre die Definition stattdessenA<T extends X>
, so würdeT
durchX
statt 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.t
hierX
und der Aufrufa.t.m()
wäre möglich. Benutzen wir das Attribut aber innerhalb von A, dann ist der Typ vont
wiederObject
… 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
break
verlässt man dasswitch
. Ansonsten (wenn auch nichtcontinue
oderreturn
kommt) 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 2
wirda += 30
ausgeführt. Nachdem wir keinbreak
haben geht geht es danach in dencase 3
und es wirdbreak l1
ausgefü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:b
ist statischB
und dynamischB
, also suchen wir in unseremB
-Objekt erstmal ein Attributa
, umb.a
zu bestimmen. Dieses Attributa
muss ausB
(oder einer Oberklasse vonB
) kommen, welches wir hier finden (das ist das einzige a in demB
-Objekt).b.a
ist somit statischB
, dynamischC
(das obere der beidenC
s). Wir suchen also in dem oberenC
-Objekt ein Attributa
, umb.a.a
zu bestimmen. Dieses Attributa
muss ausB
(oder einer Oberklasse vonB
) kommen, welches wir hier finden – das ist das Attributa
A. Dasa
C sehen wir nicht, da der statische TypB
ist. Der statische Typ vonb.a.a
ist 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 wieString
undStack
– 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. überT
ist lediglich bekannt, dass es mindestensObject
sein muss. Die OberklasseX
verlangt aber einen generischen Typ, der mindestensA
ist (<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
I
an eine Variable vom TypC
zuzuweisen. Der Compiler sieht nur diese statischen Typen und überprüft nicht, was tatsächlich (dynamisch) gespeichert wird. Eine Variable vom TypI
könnte zwar einC
(oderD
) speichern (wie es hier der Fall ist), aber theoretisch (aus Compiler-Sicht) auch ein Objekt jedes anderen Typs der mindestensI
ist (also jede Klasse, dieI
implementiert). Es könnte ja nebenC
noch eine KlasseF
geben, dieI
implementiert. 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 TypC
nurC
oderD
zuweisen. An eine Variable vom TypI
kannst duC
,D
oderI
zuweisen. 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 vone
ist hier jedoch nichtF
sondernE
. 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üre
der 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.e
ist der statische Typ vond
bspw.D
(dynamischF
), also wollen wir imF
-Objekt die Variablee
. Dort gibt es zwei, weilF
dase
Attribut ausD
undA
erbt. Wir wählen das ausD
geerbtee
, weil der statische Typ vond
D
ist, also besteht nur Zugriff auf die Variablen ausD
und 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 Methodem
einenString
erwartet und auch einenString
zurückgibt. Das wäre richtig, wenn die Methode nichtstatic
wä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 TypparameterR
ein (könnte auch anders heißen, hat nichts mit demR
ausFoo<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)
:a
ist dynamisch nichtC
sondernA
!this
ist 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
a
um das Attributa
des 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 Attributa
ab (mittelsthis.a = a
), d. h. das hier verwendetea
hat 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 mitschwank
passieren.Außerdem musst du
max
vor der Schleife definieren, sonst kannst du es am Ende nicht zurückgeben (bei dir ist das nur lokal in der Schleife definiert).max
speichert 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 darfstmax
nur 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