Okay, dann lasst uns anfangen. Herzlich willkommen zur vierten Vorlesung,
SE2. Ich habe letzte Woche einen Einstieg gegeben in die Vererbung,
in das Thema Vererbung, ein relativ großes und wichtiges Thema für objektantierte Programmiersprachen.
Und im letzten Jahr haben wir vornehmlich über Typhierarchien unterhalten.
Das heißt, wir haben uns damit beschäftigt,
wie man Typen in Hierarchien anordnen kann.
Das klingt abstrakt. Konkret haben wir dann gesehen,
dass das sehr nützlich ist, wenn ein Klient von Vielgestaltigkeit abstrahieren will,
also unabhängig von verschiedenen Ausprägungen, verschiedene Dinge gleich behandeln will.
Heute gucken wir uns jetzt die Implementationsvererbung an.
Das ist quasi die andere große Schiene, wenn man sich
mit Vererbung beschäftigt. Es gibt, sagen wir mal,
mit Vererbung relativ viele Konzepte. Wir konzentrieren uns hier auf
die Wahrheit
den wichtigsten
Das Thema ist relativ ausufernd. Das ist nur die Übersicht,
das ist jetzt für die Vorstellung wenig hilfreich.
Eine der Hauptmotivationen für Implementationsvererbung ist die sogenannte Wiederverwendung.
Und Wiederverwendung ist so eines der, ja,
wie soll man sagen, eines der gemeinsten Märchen der Objektorientierung.
Also lange Zeit hat man immer geglaubt, ja,
das ist ja ganz toll, wenn man dann objektorientiert programmiert,
dann hat man ganz viele Klassen und wenn man das nächste Projekt macht,
dann kann man ganz viele Klassen,
weil die ja so allgemeingültig sind,
im nächsten Projekt wiederverwenden
Das hat sich nicht so bewahrheitet. Wiederverwendung ist etwas,
was man sehr bewusst planen muss. Trotzdem ist es natürlich
total sinnvoll, möglichst viel wiederzuverwenden. Das heißt,
es ist schon sinnvoll, in einer Sprache Mechanismen anzubieten,
mit denen man Redundanz vermeiden kann.
Die einfachste Form, um Redundanz zu vermeiden, ist,
dass man Sachen, die man immer wieder braucht,
systematisch in Softwarebibliotheken abzulegen.
Das kennt ihr.
JavaYoutill ist so eine Softwarebibliothek für Java
Das ist dann einfach so mal als Klient, benutzt man die.
Die sind da, man kann sie irgendwie in seinen eigenen Quelltext einbinden.
In professionellen Projekten gibt es dann JAR-Files mit irgendwelchen Bibliotheken von Drittanbietern,
die benutzt werden können. Das ist offensichtlich so,
dass man da Code, den andere schon entwickelt haben, wiederverwendet.
Die, die wir uns jetzt hier genauer ansehen wollen,
ist die Interpretation in der Objektorientierung. Die schließen sich nicht aus.
Die Objektorientierung verzichtet nicht auf Bibliotheken, das wäre ja Quatsch.
Aber es gibt eben halt so diese etwas andere Ausprägung,
diese Idee von Reuse. Also, dass man tatsächlich,
wie es hier unten auch steht, Ausführbare Softwareeinheiten,
Klassen in Hierarchien organisiert und dadurch ja bestimmte Einsparungseffekte erzielen kann.
Also vor allen Dingen auch Redundanzen vermeiden kann. Dabei gibt es
tatsächlich irgendwie zwei Herangehensweisen, wie man das machen kann.
Die eine ist so die Sichtweise, wir haben verschiedene Abstraktionen,
verschiedene Typen, verschiedene Objektklassen und stellen fest,
die haben was gemeinsam.
Und dann stellen wir irgendwann fest, wir könnten das doch vielleicht zusammenfassen.
Wir könnten also irgendwie Gemeinsamkeiten in einer Oberklasse zusammenführen.
Also das ist eher so ein Bottom-Up,
so eine Bottom-Absichtweise
Die ist auch relativ häufig tatsächlich anzutreffen, dass man also feststellt,
Dinge sind sehr ähnlich und wir abstrahieren sie hoch in Oberklassen.
Also das eher so dem Vermeiden von Redundanz gedient.
Und die andere Idee ist, dass man etwas,
was da ist, spezialisiert,
also sozusagen in der Vererbungshierarchie nach unten geht und sagt,
hier ist schon irgendeine nützliche Abstraktion und die passe ich jetzt für meine Zwecke speziell an.
Und das drücken wir uns nochmal ein bisschen im Detail an.
Hier ist so das erste Beispiel Erben zum Anpassen.
Wir haben irgendeinen Kontext, in dem
eine Klasse C
entwickelt worden ist
Wir kommen in einen neuen Kontext, ein anderes Projekt.
Es kann ganz unterschiedlich motiviert sein. Und da stellt man fest,
Mensch, wir haben doch schon mal was Ähnliches gemacht.
Dann nehme ich doch einfach die Klasse von daher.
Die passt zwar nicht ganz, aber mit vielleicht
so ein bisschen Anpassungen kann ich die Klasse dann benutzen.
Und dann wird hier möglicherweise tatsächlich geerbt. Das heißt,
hier wird eine Klasse beerbt und für spezielle neue Kontexte angepasst.
Das Beispiel, was ich jetzt natürlich wieder dabei habe,
ist das Übliche, das ist jetzt ein Beispiel,
das ich auch wieder durch die Fotos so ein bisschen durchziehen werde.
Ich hoffe, ihr seid es nicht müde oder ihr
seid es nicht leid
Nach wie vor ein extrem anschauliches Beispiel. Ihr kennt die
Fachlichkeit mit den Konten und so weiter. Das heißt,
wir können uns auf die technischen Randdetails konzentrieren. Wie gesagt,
ihr werdet in den Übungen ja noch genügend Gelegenheit haben,
auch noch mit umfangreicheren Klassen- und Typhierarchien umzugehen.
Das ist das übliche Beispiel. Und was ihr hier schon seht,
ist eine spezielle, ein anderer Fokus. Wir hatten,
wir haben hier sozusagen die Klasse Konto,
das sei jetzt mal die Klasse C aus der Folie und
ich habe hier die Klasse Sparbuch und das ist da hier
die Klasse C-Strich
Das heißt, irgendwie habe ich schon irgendwann mal eine Klasse
Konto entwickelt, die wird irgendwie benutzt, keine Ahnung.
Und dann stelle ich fest, oh, jetzt geht es irgendwie
um den Anwendungsbereich sparen in einer Bank, nehmen wir mal an.
So, jetzt müsste also auch ein Sparbuch definiert werden.
So, dann nimmt man also, stellt man fest,
aha, ziemlich viel von einem Sparbuch,
ist eigentlich schon in der Klasse Konto drin.
Also baue ich doch mal dieses Sparbuch als eine Unterklasse von Konto.
Und der Witz ist tatsächlich,
dass der Sparer oder der Klient, dass diese Spare,
diese klassische Spare ist hier nur erst mal so als irgendein archetypischer Klient aufgeführt.
Da steckt ein bisschen Beispiel Code drin,
der benutzt tatsächlich nur das Sparbuch.
Man sieht hier
keine Benutzbeziehung zum Konto
Das ist etwas anders, als wir es letzte Woche hatten.
Wenn wir euch das überlegt, wir hatten hier,
ja, ich zeige es vielleicht erstmal,
ich zeige erstmal vielleicht dieses kleine Beispiel,
dann kommen wir gleich bei der nächsten Folie auf.
Das ähnliche System. Hier haben wir das Konto.
Ist das einigermaßen lesbar, der Fontgroß genug,
sagt mal ganz kurz, könnt ihr das da hinten einigermaßen lesen,
soll ich den Font größer machen?
Bisschen größer, okay
Versuchen wir es mal. Ja, es ist sicherlich ein
bisschen besser zu lesen. Okay, also das ist das,
was ihr schon mehrfach gesehen habt, klasse Konto mit einer privaten Variablen
Exemplarvariablen Saldo, unterstrich Saldo. Im Konstruktor wird der Saldo initialisiert,
man kann etwas einzahlen. Und was wir hier auch schon sehen,
ist, wir haben tatsächlich sowas wie eine Vorbedingung.
Also wenn ich hier einen Betrag einzahlen will,
dann ist erstmal eine vernünftige Vorbedingung ein intelligenter Klient,
sollte einen Betrag größer null übergeben.
Das sagt hier der Dienstleister, wenn man mir was einzahlen will,
der muss der Betrag größer null sein,
weil Int lässt ja auch negative Zahlen zu
Oder negative Werte. Also in diesem Fall hier eine Vorbedingung,
die dann wird dann auch hier überprüft, wie wir es kennen mit dem Asserd.
Und dann wird hier tatsächlich was eingezahlt. Beim Auszahlen ist
es ganz ähnlich, der Betrag muss größer Null sein.
Auch das wird geprüft mit einem Asserd an dieser Stelle.
Aber außerdem ist es noch so, dass hier steht,
der Betrag, der ausgezahlt werden soll,
der muss kleiner gleich dem Saldo sein.
Das macht nun irgendwie, ja, vermutlich ganz gut Sinn,
an der Stelle zu sagen, naja,
ich darf hier nicht mehr abheben, als drauf ist.
Das ist auch ganz interessant, wenn wir uns dann
gleich nochmal im Zusammenhang mit Vererbung ansehen.
Ich hatte es ja letzte Woche schon gesagt,
auch das Vertragsmodell im Zusammenhang Vererbung ist ja so ein bisschen speziell.
Da hatte ich gesagt, dass Vorbedingungen sich nicht verschärfen dürfen.
Wenn ich jetzt hier in der Oberklasse und damit auch in dem Typ da oben,
in diesem super Typ, eine Vorbedingung formuliere,
dass der Betrag kleiner gleich dem Saldo sein muss.
Also hier tatsächlich Betrag, kleiner gleich Gibsaldo.
Dann ist das eine ziemlich harte Einschränkung. Wir können uns
ja auch vorstellen, dass es sowas gibt wie Girokonten.
Da darf man mehr abheben, als auf dem Konto
drauf ist. Man darf ja in den Dispo reingehen.
Man darf ja so ein Konto überziehen,
so ein Girokonto
Bei einem Sparkonto geht das nicht. Bei einem Girokonto kann das passieren. Das heißt,
bei einem Girokonto,
wenn wir später dann nicht nur Sparbuch von dem Konto ableiten,
sondern auch noch Girokonto, da könnte man dann die Vorbedingungen aufweichen und sagen,
es reicht, wenn der Saldo nicht kleiner gleich dem Saal,
wenn der Betrag, der abgehoben wird,
nicht nur kleiner gleich dem Saldo ist,
er kann auch ein bisschen größer als der Saal sein.
Bis zum Dispo. Kann ein bisschen komplizierter sein.
Da ist also das tatsächlich aufgeweicht. Das heißt, die
Vorbedingung wird abgeschwächt. Das ist okay. Also Vorbedingungen dürfen
entlang der Hierarchie geschwächt werden. Von daher also kein Problem,
dass hier so in die Oberklasse reinzuschreiben, in den Supertyp dieses Vertragsmodell hier.
Ja, dann wird hier das einfach abgehoben und dann gibt es hier noch Gibsalo.
Also total unspektakulär dieses Konto. Dann haben wir hier Sparbuch
und da sieht man schon, da ist auch relativ wenig drin.
Irgendwie definiert Sparbuch einen Zinssatz. Der wird irgendwie im Konstruktor
gesetzt als Parameter und dann kann man sich in den Zinssatz
rausgeben lassen und man kann da irgendwie die Zinsen berechnen lassen.
Hier steht momentan noch kein Quelltext, das machen wir gleich.
Okay, und man sieht hier so ein bisschen den Klienten,
das ist auch relativ einfach, so ein Klientencode einfach eine Benutzung von so einem Sparbuch.
Er zeugt sich ein Sparbuch mit Zinsen von drei Prozent,
dann zahlt er irgendwie 1.000 Euro ein,
berechnet Zinsen und dann steht hier Gibsaldo,
wenn der Saldo größer als 500 ist,
dann zahle ich mich mal 500 aus.
Das heißt, hier ist der Klient wirklich intelligent und prüft das,
ob der Saldo auch hoch genug ist.
Also verhält sich der Klient hier sehr schlau. Dem Vertrag
angemessen. Ja, also das ist hier relativ unspektakulär.
Und wie gesagt, das ist die Situation der Klientencode
benutzt die abgeleitete Klasse,
also die Unterklasse
Jetzt zum Vergleich. Nochmal hier dieses Projekt von letzter Woche.
Das war relativ groß, aber ihr erinnert euch möglicherweise noch.
Wir hatten wir unsere bekannten Klassenkreis, Quadrat und Dreieck,
die sich auf einer Leinwand zeichnen.
Da hatten wir ein Interface farbig und alle drei haben das
implementiert, dann ein Interface bewegbar.
Das hätten auch alle drei implementieren können. Wir haben es
nur mit Dreieck gemacht. Und was man hier sieht,
ist, hier sind die Klienten, also auf die es jetzt ankommt,
über die wir reden,
hier der Unfärber benutzt den Supertyp
Polymorph, er will gar nicht wissen, ob es ein Kreis,
ein Quadrator und ein Dreieck ist, sondern er benutzt Polymorph diesen Typ.
Genauso die Steuerung benutzt dieses Interface hier und es ist egal,
was dahinter steckt. Da hat die Benutzung oben,
das ist diese Polymorphie-Idee, möglichst unabhängig von vielförmigkeit alle Dinge
gleich behandeln auf eine bestimmte Art und Weise.
Und hier ist es eben genau anders. Hier haben wir
jetzt sozusagen den Fokus auf das, was da unten passiert.
Wir gucken uns also sehr genau an, was passiert eigentlich
in Sparbuch. Und da hast du so ein Detail,
das eben halt Sparbuch,
dann auch Dinge von Konto erbt
Okay. Dann haben wir diese Idee,
was ich eben auch schon angedeutet habe, dass dieses Vermeiden von Redundanz,
ich möchte also Gemeinsamkeiten von verschiedenen Klassen in eine Oberklasse zusammenfassen.
Das ist typischerweise eine 1-zu-N-Beziehung. Ich habe also irgendwie,
das ist ein Beispiel aus dem Java Collections Framework.
Ihr kennt Railist, ihr kennt Linklist, ihr habt die schon zehnmal benutzt.
Die haben große Teile ihrer Implementation, ist sehr ähnlich.
Und dann haben die Framework-Designer gesagt, naja,
dann ziehen wir das doch zusammen in eine Klasse Abstract List.
Das heißt, die wird einfach Redundanz vermieden.
Ein sehr viel Konkreteres Beispiel, das ihr kennt,
ist dieses hier. Auch da sieht man jetzt wieder,
wir haben jetzt überhaupt keinen Klienten an Abstract Figure,
so eine abstrakte Klasse, da gibt es selten wirklich Klienten davon.
Es geht also gar nicht um Polymorphie. Hier geht es
tatsächlich um diese Vermeidung von Redundanz. Ihr erinnert euch,
das Kreis, Quadrat und Dreieck,
wenn ihr mal reingeguckt habt in die Klassen damals,
dass die weite Teile identisch hatten, das Bewegen des Farbeändern,
der ganze Zenober, das war alles in allen drei Klassen mehrfach da.
Das war natürlich blöd. Das ist jetzt hier,
Ich habe das mal einfach mal testweise gemacht.
Das ist nicht ganz so einfach, wie man das denken sollte.
Aber ich habe tatsächlich sehr viel in diese Klasse reinschieben können,
sodass diese Klassen total klein sind.
Ich sage das jetzt mal, hier sieht man,
diese Klasse ist ziemlich groß. Ihr seht hier,
das ist, ihr müsst das nicht alles verstehen,
ich will euch nicht schwindelig machen, aber ihr seht,
das ist ziemlich viel.
Das war vorher alles auch in den Klassen da unten drin.
Und jetzt ist aber Kreis total klein geworden. Jetzt sagt man
nur noch, hier Kreis, Extense, Abstract Figure.
Dann gibt es irgendwie bei einem Kreis als Spezialität
einen Durchmesser, den muss man wieder zu definieren.
Dann wird hier irgendwie die Initialisierung mit einem super Aufruf gemacht,
gucken wir uns gleich noch genauer an
Dann gibt es hier Größe ändern, weil das ist ja was andere,
das ist ja bei einem Kreis irgendwie speziell,
also ist das hier so implementiert?
Und dann gibt es noch das Zeichnen,
wo dann hier tatsächlich eine Elypse gezeichnet wird.
Das ist alles, was speziell für einen Kreis gemacht werden muss.
Alles andere bewegen und so weiter, Farbe ändern,
alles einfach in der Oberklasse drin.
Das ist hier genauso, Quadrat ist genauso übersichtlich,
Größe ändern, zeichnen und genauso auch beim Dreieck.
Also, das ist, das schreit förmlich danach,
Gemeinsamkeiten von mehreren Klassen in eine abstrakte Oberklasse zusammenzufassen.
Ob die abstrakt sein muss, können wir noch darüber diskutieren,
aber ihr seht schon mal,
dafür ist das
sehr nützlich
Und es ist tatsächlich so, dass diese drei Klassen,
all das, was hier drin steckt,
einfach erben und das genauso haben, als wenn sie es selbst definiert hätten.
Okay, das ist nur nochmal zur Veranschaulichung. Und dann gibt es auch noch,
sagen wir, diese allgemeine Idee von,
ich habe etwas sehr Nützliches, das ist aber auch schon dafür gedacht,
dass es irgendwie in bestimmten Kontexten wiederverwendet wird.
Und ich an diesem Stück Code, an dieser Klasse,
was auch immer, sehe ich so ein paar Stellen vor,
an denen dann jemand, der diese Klasse beerbt,
Code reinschieben kann.
Also das, was an bestimmten Stellen
notwendig wird
Grafische Benutzungsoberflächen funktionieren sehr stark so.
Wir werden das in den nächsten Wochen uns noch ansehen.
Ihr werdet auch in den Übungen da intensiv mit in Kontakt kommen,
wie man eine Google baut in Java mit Swing.
Und da ist es eben tatsächlich so,
dass man dann eigentlich in Erbendenklassen Dinge hinzufügt,
die dann in der Oberklasse auch schon vorgesehen waren dafür.
Auch das sehen wir uns ein bisschen genauer an,
was da technisch notwendig ist, damit das funktioniert.
Okay, was ich jetzt machen will,
ist tatsächlich relativ technisch diese Mechanismen, die Java anbietet,
durchzugehen und dabei zu erläutern,
wofür sie denn da sind
Das ist so ein bisschen, ja,
so nach und nach werde ich die Dinge so einführen.
Ich habe hier das mal dargestellt, wenn ich so eine Situation habe,
eine Klasse erbt, eine Unterklasse erbt von einer Oberklasse,
dann ist es erst mal so,
dass alle Eigenschaften der Oberklasse auch Eigenschaften der Unterklasse sind.
Das muss man sich erst mal klar machen. Ja,
also ich habe eine Erbendeklasse und die kriegt erst mal alles,
was die Oberklasse hat.
Und diese Bestandteile bilden wirklich einen festen Bestandteil der Unterklasse.
Die Felder werden vollständig übernommen und auch meistens unverändert.
Es gibt Programmiersprachen, da gibt es Möglichkeiten,
dass die Felder sich ändern,
so bestimmte automatische Anpassungen anhand des Typs
Das ist in Java nicht möglich. Das heißt,
in Java könnt ihr davon auskönnen, alle die Felder,
die in einer Oberklasse sind, sind auch in einer Unterklasse definiert.
Und natürlich kann eine Unterklasse weitere Felder hinzufügen,
das sehen wir uns gleich an. Das ist also einmal
die Exemplarvarianen oder die Felder. Und dann die Methoden,
das ist ja die zweite wichtige Einheit bei einer Klasse,
die können spezialisiert werden. Und was wir uns ansehen werden,
dass wir Operationen hinzufügen, das ist relativ häufig so,
dass in einer Unterklasse neue Operationen hinzugefügt werden.
Wir können aber auch die, die wir erben,
können wir redefinieren.
Wir können die also anpassen
Da gibt es zwei Möglichkeiten, man kann die überschreiben oder erweitern.
Und es kann sogar sein,
dass man erst eine Implementation angibt für eine Operation,
die in einer Oberklasse lediglich deklariert wurde.
Auch das sehen wir uns genauer an. Wir machen das
Schritt für Schritt. Anhand dieses Beispiels und sehen uns zuerst an,
wie das ist, wenn man erbt und etwas hinzufügt.
Wir haben das eben schon gesehen in diesem Mini-Projekt.
Konto hat so Operationen wie Gibsalde. Und was ihr hier
auch wieder seht, ist so diese Unterscheidung von Operationen und Methoden.
Das hier links, das ist immer die Schnittstelle,
also der Klient guckt immer so von links da drauf.
Das ist die Schnittstelle
Und da sieht man die Operationen an der Schnittstelle.
Also Gibsaldo ist zum Beispiel eine Operation von einem Konto.
Und es gibt dazu aber auch eine Methode,
also eine Implementation. Jetzt haben wir dann ein Sparbuch,
dieses Sparbuch erbt von der Oberklasse, hat dann automatisch auch dieses Gibsaldo,
haben wir gerade gesagt, und kann aber auch was hinzufügen.
Zum Beispiel berechnet Zinsen, weil bei einem Sparbuch irgendwie Zinsen
eine Rolle spielen. Also erstmal alles das Erben,
was da oben schon da ist und man kann was hinzufügen.
So, das passiert einfach nur, indem man Extense schreibt.
Also hier steht tatsächlich, klar, das Sparbuch,
Extense Konto, Damit beerbt eine Klasse eine andere.
Da ist es ganz zentral. Das muss man erst mal
machen, um überhaupt Vererbung zwischen Klassen zu erreichen.
Sparbuch, in diesem Fall hier, Sparbuch erbt dann alle Operationen und Felder.
Und Sparbuch ergänzt hier zwei neue. Also wir sehen hier irgendwie ein Zahlen,
Auszahlen gibt Saldo und dann gibt es Hinsatz,
berechnet Zinsen, die kommen neu hinzu. So,
und das können wir uns jetzt hier mal live angucken,
weil hier steht es ja auch auf der Folie,
wird ein Exemplar, der klassische Sparbuch, erzeugt,
enthält dieses alle Zustandsfelder von Sparbuch und Konto.
Das heißt, ich sehe hier in Sparbuch nur eine Exemplarvariable.
Ich erzeuge mir ein Exemplar von Sparbuch mit einem Zinssatz von drei Prozent.
Das waren noch Zeiten. Und kann jetzt hier tatsächlich sehen,
dass ich, erst mal kann ich mir jetzt mit dem Objektinspektor das Objekt ansehen.
Und ich sehe, dieses Exemplar enthält einmal den Zinssatz,
den das Sparbuch selbst definiert und eine weitere Exemplare ja besaldo,
die nämlich einfach da ist, weil Sparbuch von Konto erbt.
Und Konto hat ja auch hier da oben diese Exemplarvariable.
Also das erst mal ganz offensichtlich, eine Unterkasse hat automatisch
alle Zustandsfelder ihrer Oberklassen und ihrer eigenen. Ja, okay,
und dann haben wir hier auf der Folie die Situation,
dass wir einen Klienten haben und dieser Klient kann alles aufrufen,
was Sparbuch anbietet, aber auch alles, was Konto anbietet,
weil Sparbuch das automatisch mit übernimmt.
Das heißt, das scheint ganz selbstverständlich,
aber man muss es eben halt noch mal deutlich sagen,
an dieser Stelle hier,
wenn ein Klient sich ein Sparbuch erzeugt,
dann kann er
aufrufen einzahlen
Berechnet Zinsen. Und das eine ist eine Operation aus Konto,
das andere ist eine Operation aus Sparbuch.
Aus Konto, auszahlen und Ritaldo, alles aus Konto.
Das kann ich ganz normal an dem Sparbuch aufrufen.
Wie gesagt, der Klient benutzt das Sparbuch,
hat mit dem Konto eigentlich weiter nichts zu tun.
Aber irgendwie muss natürlich bekannt sein, das Sparbuch dieser Operation miterbt.
Okay, das können wir uns auch nochmal hier angucken.
Das heißt, hier sehe ich jetzt meinen Klienten und
er kann diese Operation hier alle wirklich aufrufen.
Einzahlen, berechnet Zinsen. Das können wir jetzt
mal ausführen
Können wir mal den Sparer hier mal aufrufen, benutzen konnten,
ist eine statische Methode. Der erzeugt sich jetzt hier also ein Konto,
zahlt 1000 Euro ein und hebt eine 500 ab.
Wobei, wenn wir nochmal genau hingucken,
was der Sparer hier macht in seinem Quelltext,
also er erzeugt ein Sparbuch mit drei Prozent Zinsen,
zahlt 1000, einer kommt, steht hier berechnet Zinsen.
Und ja, irgendwie passiert da nichts. Berechnet Zinsen
macht das gar nicht. Sonst würden wir ja irgendwie da
jetzt vielleicht 30 Euro mehr drauf haben bei drei Prozent.
Weil wir hier dann sagen, gib Saldo,
wenn der größer 500 ist, dann wurden 500 ausgezahlt und
dann wurde hier der Saldo ausgegeben, 500.
Ich hätte jetzt erwartet,
dass da irgendwie 35 drauf sind oder sowas
Okay, ja, aber das ist ja auch keine Überraschung,
weil wir haben ja hier berechnet Zinsen noch gar nicht implementiert.
Okay, mag mir mal jemand eben zurufen, wie ich das Ding implementieren kann?
Also ich will ja irgendwie jetzt den Zinsen berechnen,
das heißt, ich würde irgendwie erwarten,
dass irgendwie dieser Zinssatz draufgerechnet wird auf den bestehenden Saldo.
Hat dir mal mal eine Idee, wie man das hinschreiben kann?
Also, nein. Saldo ist gleich,
Saldo mal, mal Klammer auf,
Sind herausgekommen. Eins plus Zinssatz.
Und dann? Geteilt durch 100, so, oder?
Oh, der Ball. Okay. 100,0, okay,
okay, okay, ja. Also ihr seht schon,
man muss ja hier so ein bisschen jetzt aufpassen.
Das ist ja ganz zahlig. Also Int ist hier
ein Zinssatz. So kann man das möglicherweise machen.
Ich überlege gerade Zinssatz, wenn man da sagt,
jetzt 100,0, dann wird es ein Float-Ausdruck.
Moment,
ein Punkt
100,0, dann wird da hier wirklich 3 durch 100 gerechnet,
dann wird also irgendwie eine 0,0 und dann kommt da 1 plus,
das wird dann auch und das wird dann multipliziert und das wird dann wieder,
das wird nicht klappen, vermute ich mal,
weil dann hier in dem Int etwas gespeichert wird,
was eine höhere Präzision hat.
Gut, ich mache mal einen anderen Vorschlag,
wir sagen einfach, ist gleich Saldo.
Ah, Moment, Saldo ist gleich Saldo,
der alte Saldo plus,
der Saldo mal,
der Zinssatz durch
Das dürfte meiner Meinung nach funktionieren. Also ich sage,
Saldo ist irgendwie 1.000 Euro, mal den Zinssatz,
das ist irgendwie dann 3.000 durch 100, dann kriegt man,
also wenn man erst multipliziert und dann dividiert,
kriegt man davon.
Die Genauigkeit ist jetzt gar nicht wichtig. Es geht ja
nur darum, dass wir das jetzt hier berechnen wollen.
Okay, super, können wir mal übersetzen.
Ah, da fehlt noch irgendwo.
Klammer zu fehlt.
Eine zu viel
Ja, jetzt passt. Ja. Okay, super mal übersetzen.
Oha. Saldo has Private Access in Konto. Ah. Okay,
also wir versuchen hier gerade, auf diese Exemplarvariable Saldo zuzugreifen.
Die ist aber dummerweise hier gar nicht definiert in Sparbuch,
sondern die ist ja ein Konto definiert.
Und die ist da, wenn wir uns das genauer angucken,
Private. Private heißt in Java tatsächlich komplett privat.
Das heißt, auch eine erbende Klasse kommt da nicht ran
Das ist interessant. Das ist nicht in allen Sprachen
so. Es gibt Sprachen, da ist es so,
wenn jemand erbt, keiner auf alles zugreifen von der Oberklasse.
In Eifel ist das zum Beispiel so. In Java
ist das hier so, dass wenn das private ist,
selbst die Unterklasse da nicht rankommt.
Ich mache jetzt mal folgendes, ich schreibe da mal protected hin.
Protected heißt, eine Erbendeklasse kann darauf zugreifen. Ein Klient
nicht. Wir wollen ja mit dem Private wollten wir bisher erreichen,
dass ein Klient da nicht drankommt, an diese Zustandsvariablen.
Jetzt machen wir sozusagen ein bisschen auf und sagen,
na gut,
Erbendeklassen dürfen das
Wenn ich das jetzt übersetze, dann müsste es wieder funktionieren.
Seht ihr, jetzt könnt ihr es übersetzen.
Jetzt kann tatsächlich Sparbuch auf dieses Feld zugreifen.
Das ist soweit ganz schön. Das ist also hier genau,
was wir gesehen haben. Private Eigenschaften werden zwar mitvererbt,
sie sind aber nicht zugreifbar, muss man wirklich aufpassen.
Also ein Exemplar der Unterklasse hat dieses Feld.
Es darf nur nicht darauf zugreifen. Interessant. Damit das
Feld Saldur also zugreifbar ist, machen wir es protected.
Es ist dann für Erbende Klassenzugreifer, aber nicht für normale
Klienten, also für normale Benutzer der Klasse.
Und Sparbuch erweitert dann die geerbten Felder um das Feld Zinssatz.
Also das ist einfach, was wir schon gesehen haben
Zinssatz wird hier einfach dazugefügt. Okay,
ich habe hier so ein kleines Zeichen rangemacht,
weil man aufpassen muss mit diesem Protected-Machen von Exemplar-Variablen.
Das werden wir uns gleich nochmal im zweiten Teil der Frisur ansehen,
wie man das vielleicht noch ein bisschen besser machen kann.
Dass man also auch tatsächlich dem Erben gegenüber eine Schnittstelle definieren kann.
Aber jetzt funktioniert das erstmal so und das ist wunderbar.
So, was wir jetzt auch gesehen haben,
also wenn ich jetzt hier mir tatsächlich nochmal angucke,
ich habe hier mein Sparbuch, mach mir nochmal hier so ein Exemplar, So.
Und ich kann ja jetzt hier sehen,
erstmal habe ich hier berechnet, Zinssatz gibt es hinsatz.
Das sind die Operationen, die Sparbuch definiert. Dann habe ich
hier geerbt von Konto. Da sehe ich also die Sachen,
die ich auch wirklich von Konto auf, wo kann,
die kann ich ja hier dran aufrufen.
Ich sehe aber auch hier geerbt von Object. Das ist
auch ganz wichtig. Ich kann also tatsächlich alles das,
was Object definiert, an jedem Exemplar jeder Klasse aufrufen.
Also diese Sachen, Hashcode, die anderen Sachen müssen wir uns nicht angucken.
Notify Wait, das hat alles was mit Synchronisation zu tun.
Aber Equals und Hashcode und das Two String dürfte ja auch
irgendwo sein. Das sind die, die ihr kennt.
Die kann man
immer aufrufen
Das ist auch genau das, was hier steht.
Jede Klasse, die nicht explizit durch eine Extenzklausel von einer
anderen Klasse erbt, beerbt implizit die Klasse Object.
Also wenn ich ja jetzt die Klasse Konto mir ansehe,
die Klasse Konto erbt implizit von Object.
Und Object ist damit direkt oder indirekt Oberklasse aller anderen Klassen.
Der Typ Object, definitiv Operationen,
die aufgrund dieser impliziten Fallausbildung jeder Typ hat,
also to string e-could Hashcode, kann ich wirklich als Klient an jedem Exemplar aufrufen.
Und die Klasse Object implementiert diese Operation auch durch entsprechende Methoden.
Also das to string ist da irgendwie implementiert,
das E-coulds und das Hashcode auch
Und man kann das aber redefinieren. Das habt ihr auch
schon gemacht. Ihr habt zum Beispiel das Equills in SE1
redefiniert, wenn ihr für eine Klasse festlegen wolltet,
wann zwei Exemplare der Klasse gleich sind.
Das konnte man sozusagen ändern. Das werden wir uns jetzt ansehen,
wie das geht. Also man kann nicht nur einfach das,
was da ist, da was dazu packen,
sondern ich kann tatsächlich das, was ich erbe, sogar modifizieren.
Also redefinieren nennen wir das. Das heißt,
ich habe hier sowas wie meinen Gibsaldo, die Schnittstelle und
meinen Machejahresabschluss.
Das ist jetzt
das nächste Projekt
Ich mache mal hier das andere Projekt zu. Dieses hier,
das können wir mal zumachen, das brauchen wir nicht mehr.
Ich öffne jetzt das nächste Projekt. Das ist Erben und Redefinieren
. Und das können wir zumachen. Das sieht wieder mehr
oder weniger genauso aus. Ich habe hier das Konto.
Hier ist jetzt der Saldo Protected. Und alles andere ist
mehr oder weniger gleich geblieben. Einzahlen, auszahlen ist gleich geblieben.
Gib Saldo. Und hier gibt es aber noch zwei Operationen.
Nämlich mache Jahresabschluss und Druckejahresbericht. Wir gucken uns zuerst nur
, mache Jahresabschluss an
Das ist so bei einem Konto, da muss halt irgendwann gesagt werden,
so, jetzt berechnen wir zum Beispiel mal die Gebühren.
Hier, die Implementation ist einfach,
da werden von dem Saldo einfach 10 Euro abgezogen.
Dieses Machi-Jahresabschluss kann natürlich in einem Sparbuch anders aussehen,
als bei einem allgemeinen Konto.
Deswegen ist hier ein Sparbuch, hier Sparbuch Extens Konto.
Hier ist übrigens jetzt der Zinssatz auch protected.
Einfach mal so gemacht. Und hier sieht man jetzt,
mache Jahresabschluss, sieht hier ein bisschen anders aus.
Mache Jahresabschluss, macht hier auch dieses Gebühren abziehen,
aber außerdem werden die
auch die Zinsen berechnet
Macht ja Sinn, dass man irgendwie zum Jahresabschluss sagt,
so, jetzt, wenn da die Zinsen draufgeschlagen auf das Konto.
Das heißt, an dieser Stelle, und das ist auch,
was hier steht, da ich redefiniere das, was ich geerbt habe.
Das englische Schlüsselwort ist hier overright,
das ist also eine etwas unglückliche Benennung,
wie ihr gleich sehen werdet,
weil wir das allgemein erfassen unter Re definieren und dann gibt es zwei verschiedene Ausprägungen.
Eine Ausprägung haben wir gerade gesehen, ich überschreibe das komplett.
Ich deklariere einfach die Operation erneut, habe also hier einfach
das Mariahesabschluss neu hingeschrieben in der Unterklasse und Ich mache da irgendwie was Eigenes.
Und wenn dann tatsächlich ein Exemplar von Sparbuch erzeugt wird,
dann wird dieser Jahresabschluss auch wirklich ausgeführt.
Wenn nicht ein Exemplar von Konto erzeuge,
dann wird natürlich das ausgeführt, was da oben steht.
Das können wir uns angucken, indem wir das einfach mal machen.
Also ich mache mal hier ein Konto, zahle da mal
irgendwie 101. So. Und dann sage ich jetzt hier,
mache Jahresabschluss und dann habe ich hier bei Gibsaldo 90 drauf
Es wurden halt zehn abgezogen. Hier beim Sparbuch ist es so,
ich hoffe mal, dass der Zinssatz hier berechnet wird.
Ja, hier wird der Zinssatz berechnet. Das heißt,
wenn ich jetzt hier den mache, Jahresabschluss aufrufe,
dann sollte hoffentlich nicht nur die Gebühr abgezogen werden,
sondern auch die Zinsen sollten berechnet werden.
Also, ich erzeuge mir ein Sparbuch, drei Prozent Zinsen.
Ich zahle mal was ein, einzahlen, 100 Euro,
zack, wenn ich jetzt aufrufe, mache Jahresabschluss,
dann sollte da irgendwie 93 rauskommen.
So, mal gucken,
gib Saldo
92. Interessant. Drei Prozent Zinsen. Okay.
Noch mal gucken. Ah, diese Bank ist schlau.
Die zieht zuerst die Gebühr ab und berechnet dann die Zinsen.
Man könnte auch erst die Zinsen berechnen und dann die 10-Euro-Gebühr abziehen.
Das sind so diese kleinen Details, die führen dann hier dazu,
dass man eben halt ein bisschen weniger Geld hat.
Also das ist nur so am Rande,
da gibt es tatsächlich dann verschiedene Arten und Weisen,
wie man das machen kann.
Aber das erklärt hier, warum es hier
nur 92 sind
Okay, das ist soweit okay. Und man sieht tatsächlich,
an dem Sparbuch habe ich diese Operation umredefiniert.
Ich kann alles noch genauso aufrufen,
aber die redefinierte Operation hat ein anderes Verhalten an dieser Stelle.
So, was ich jetzt auch wieder sehe, ist,
wenn ich mir das hier angucke,
ich habe hier Saldos gleich Saldo-Gebühren und hier unten schreibe ich den gleichen Code wieder hin.
Das ist eigentlich eine klassische Situation, wo man sagt,
eigentlich möchte ich hier unten nicht überschreiben,
also das komplett wegschmeißen, was da oben passiert,
sondern das, was da oben passiert,
ist schon in Ordnung.
Und ich möchte etwas zusätzlich machen
Das heißt, erstens möchte ich diese Redundanz vermeiden,
dass hier derselbe Code zweimal steht in den beiden Nutzen.
Und zum anderen möchte ich deutlich machen,
eigentlich mache ich das, was da oben passiert,
möchte ich, da möchte ich nur was hinzufügen.
Und das ist das, was wir hier erweitern nennen bei uns.
Das heißt also, ich redefiniere das, indem ich sage,
hier in meinem Machejahresabschluss schreibe ich hin, super,
mache Jahresabschluss, dann wird wirklich die Machejahresabschluss von oben aufgerufen.
Und dann werden hier die Zinsen berechnet.
Das ist sozusagen spezifisch und hier steht aber einfach,
mache das, was sowieso da oben auch gemacht wird.
In dem Moment erweitere ich das,
was in der Oberklasse passiert
Das können wir mal eben live machen hier. Sparbuch. Ich sage,
hier, mache Jahresabschluss und statt das nochmal hinzuschreiben, schreibe ich hierhin.
Super, mache Jahresabschluss. Ich übersetze das, lässt sich
übersetzen, wunderbar. Ich übersetze hier auch den Klienten.
Ich mache mir nochmal so ein Sparbuch. Wieder drei Prozent Zinsen. Und wieder,
weil die Reihenfolge jetzt ja so ist,
wir können es ja mal, die jetzt wieder,
wenn ich jetzt sage, ich zahle mal irgendwie was ein,
100 Euro, dann dürfte er da wieder das Gleiche rauskommen.
Das ist also der gleiche Effekt, nur etwas anders implementiert.
Also wenn ich jetzt hier sage, mache Jahresabschluss und mir
dann den Saldo anzeigen lasse, dann habe ich wieder die 92. Okay.
Das heißt, ich habe tatsächlich hier das Gleiche gemacht,
nur etwas intelligenter, indem ich Kotduplizierung vermieden habe.
Es gibt übrigens nur die Möglichkeit, dass bei der direkten Oberklasse aufzurufen,
also ich kann nicht irgendwie sagen, ich möchte jetzt irgendwie den Jahresabschluss,
wenn es da drüber noch eine Klasse gäbe,
von da oben aufrufen, also so ein super,
super Aufruf oder so.
Das geht halt nicht
Man kann immer nur von seiner direkten Oberklasse die Methode mit dem Superaufruf aufrufen.
Okay. Das ist soweit alles ganz okay.
Ihr habt gesehen, man kann das ein bisschen anpassen.
Ich kann also tatsächlich in meiner erbenden Klasse Dinge so ein bisschen ändern,
weil ich die ein bisschen anders haben will in meiner Klasse.
Jetzt gibt es aber eine Situation, die ganz spannend ist,
nämlich dass Operationen sicher innerhalb einer Klasse gegenseitig aufrufen können.
Ja, also hier ganz generisch, Operation 1 kann ja Operation 2 aufrufen.
Und wenn ich jetzt Operation 2 in der Unterklasse redefiniere,
dann wird tatsächlich aus Operation 1 in der Unterklasse
auch das redefinierte Ding aufgerufen
Das ist extrem ungewöhnlich. Das ist eine der Kernkonzepte von Objektorientierung,
dass man tatsächlich an dieser Stelle sagen kann, alter Code,
also diese Operation hier, diese Methode eigentlich,
im Rumpf dieser Methode wird eine andere Operation aufgerufen.
Und wenn ich diese redefiniere,
dann wird tatsächlich quasi diese Operation, diese Methode mit angepasst,
weil die ja plötzlich dann eine andere Methode aufruft.
Das können wir uns anhand eines Beispiels ansehen. Ich habe hier
auch, das ist wirklich langsam an den Haaren herbeigezogen,
eine Operation,
Boah. Also irgendwie soll da, was da in dem Jahresbericht passiert,
es wird vor einigen Jahresabschluss gemacht und dann anschließend werden diese Ergebnisse irgendwie geeignet gedruckt.
Das habe ich hier mal mitgemacht, dass ich hier tatsächlich gesagt habe,
Moment, ich zeige erst mal, wie das in Konto ist.
Im Konto gibt es, mache Jahresabschluss, das kennt ihr,
Gebühren abziehen und dann gibt es hier Drucke,
Jahresbericht und da wird irgendwie tatsächlich aufgerufen,
mache Jahresabschluss und dann System Outprinting, aktueller Saldo.
Also eigentlich müsste hier To-Do, steht hier ja auch,
ein bisschen was Vernünftiges passieren,
es ist eigentlich ziemlich komisch,
dass ein Konto selbst
was druckt
Also wirklich an den Haaren herbeigezogen. Aber nur,
dass ihr seht, diese Operation basiert auf Machejahresabschluss.
Ja, und hier wird also dieses Machejahresabschluss aufgerufen.
Und wenn ich jetzt ein Exemplar von Sparbuch erzeuge,
wird aus, obwohl, und jetzt muss man sehen,
Druckejahresbericht wird nicht geändert. Ja, also in Sparbuch,
redefiniere ich hier mit dem Overright und der neuen Definition von Machejahresabschluss.
Nur ich mache Jahresabschluss neu. Und wenn ich mir tatsächlich jetzt hier
so ein Sparbuch erzeuge, ich habe ja hier schon eins,
dann kann ich ja hier aufrufen, geerbt von Konto,
Druckejahresbericht.
Das kann ich
einfach machen
Und auch hier sieht man dann wieder,
wenn ich mir ein Konto erzeuge, 100 Euro einzahle.
Und dann sage Druckejahresbericht, dann kommt er raus, aktueller Saldo
90. Diese Druckejahresbericht wird hier einfach runtergeerbt, kann das also
in Sparbuch auch aufrufen. Wir müssen mal ganz kurz gucken,
wie der Saldo hier aussieht. Der ist bei 92,
damit wir es gleich haben, zahle ich nochmal eben was ein.
Ich zahle mal hier 8 Euro ein, zack.
Also hier Saldo jetzt bei 100. Wenn ich jetzt bei dem Sparbuch sage,
mache den Jahres, nee Quatsch, Drucker den Jahresbericht,
das Ding habe ich einfach geerbt, unverändert.
Dann wird aus dieser Links tatsächlich, das mache ich Jahresabschluss,
mit aufgerufen und zwar das Geänderte aus dem Sparbuch.
Das heißt, hier kommt 92 raus. Es passiert wirklich
was anderes. Das ist schon ganz schön,
da muss man schon ganz schön aufpassen.
Das ist tatsächlich eine richtig eigene Qualität, dass da,
wie das hier so schön steht, alter Code, neuen Code aufruft.
Das ist eigentlich ungewöhnlich, wenn ihr normalerweise mit Bibliotheken arbeitet,
dann habt ihr einen neuen Code,
ihr schreibt einen neuen Code und ihr benutzt alten Code in Bibliotheken.
Ihr ruft mit neuem Code, alten Code auf.
Das wird hier umgedreht in der Objektorientierung, dass dann auch es vorkommen kann,
als alter Code etwas aufruft, das wird geändert.
Und dann ruft automatisch dann der alte Code,
diese geänderte Operation auf. Das ist schon speziell.
Okay, das können wir noch ein bisschen weiter treiben.
Das können wir noch weiter treiben. Es ist tatsächlich möglich,
eine Operation zu deklarieren, ohne sie zu definieren.
Das sind Begriffe, die relativ verbreitet sind im Programmiersprachenumfeld.
Kann man sich also durchaus merken, zum Beispiel bei CPlus
plus. Bei C ist das auch eine ähnliche Terminologie.
Deklarieren heißt einfach nur, es gibt eine Operation
Ist Betrag gedeckt. Die wird einfach gesagt, die gibt
es. Also Konto sagt, es gibt eine Operation,
ist Betrag gedeckt. Ist ja total schlau,
so um die Sache mit dem Auszahlen,
vielleicht ein bisschen geschickter zu formulieren, könnte man ja sagen,
okay, ich kann mir bei einem Konto was auszahlen und die Vorbedingung ist,
ist der Betrag gedeckt.
Und wie das ist, Betrag gedeckt, implementiert ist,
kann dann die jeweilige Unterklasse entscheiden. Das ist also die,
die ich sage, hier ist diese Operation,
ist Betrag gedeckt, aber ich implementiere sie gar nicht,
weil man kann die nicht irgendwie für Konto irgendwie sinnvoll implementieren.
Aber in einer konkreten Klasse kann man dann das Ist-Betrag gedeckt
wirklich implementieren und damit definiert man, wie diese Operation umgesetzt sein soll.
Das heißt, man gibt eine Methode an mit einem Rumpf,
die wirklich was macht und füllt damit diese Lücke,
die hier mit Absicht gelassen wurde.
Das können wir uns auch noch mal angucken. Ich habe da zufällig
ein Projekt dabei. Die Sache mit den abstrakten Methoden,
das ist eigentlich wieder das gleiche Projekt, was ihr kennt,
ein bisschen anders.
Man sieht hier, der Sparer ist gleich geblieben,
kann ich gleich sagen. Das Sparbuch, gucken wir uns
gleich an. Das Konto sieht etwas anders aus.
Das Konto hat nämlich Folgendes. Erstmal steht hier ominös
ein Appstract
Ich erkläre gleich, warum. Es gibt wieder den Konstruktor,
es gibt wieder das Einzahlen und jetzt gibt es hier,
eine Methode ist Betrag gedeckt.
Eigentlich ist es eine Operation. In dem Moment,
wo wir gar keinen Rumpf da stehen haben,
immer wenn etwas keine Rumpf hat,
das kennt ihr aus den Interfaces auch schon.
In den Interfaces sagen wir, da stehen Operationen.
Auch diese abstrakte Methode ist eigentlich eine Deklaration einer Operation.
Also hier wird gesagt, es gibt einen,
ist Betrag gedeckt. Aber ich implementiere es nicht.
Interessant ist, dass ich tatsächlich dieses Ist-Betrag-Gedeck hier aufrufen kann
Das heißt, ich habe hier eine konkrete Methode, die enthält Quelltext.
Und unter anderem kann ich hier diese Operation aufrufen.
Obwohl sie hier gar nicht implementiert ist, da steht gar nichts.
Das ist auch spannend. Ja, also das ist tatsächlich
das, was hier auf der Folie steht.
Ich kann Methoden deklarieren und ich kann die tatsächlich auch aufrufen.
Das ist auch mehr oder mehr das, was hier auf der Folie steht.
Die Klasse kontodeklariert in Operation ist Betrag gedeckt, die sie nicht implementiert.
Ob das gedeckt ist, kann man gar nicht entscheiden.
Also sollten nur konkrete Unterklassen wie Sparbuch oder Girokonto
das sinnvoll ausimplementieren
Das gucken wir uns mal an. Ich habe also hier
Sparbuch jetzt. Und da sieht man tatsächlich,
ist Betrag gedeckt, ist hier implementiert.
Hier ist tatsächlich jetzt konkreter Quelltext angegeben. Das heißt,
die Klasse Sparbuch gibt eine Methode an, also definiert tatsächlich,
wie die Operation ist Betrag gedeckt für diese Klasse definiert sein soll.
Also hier steht irgendwie, Betrag muss größer null sein.
Und der Betrag sollte kleiner gleich dem Saldo sein.
Wunderbar. Macht ja Sinn. Bei einem Girokonto könnte ich
mir jetzt vorstellen, dass das da ein bisschen anders implementiert
ist mit der Möglichkeit zum Überziehen.
Aha,
soweit so gut
Was das hier bedeutet an dieser Stelle, ja,
das bedeutet eigentlich nicht sehr viel.
Also man kann es hier in der Ausführung gar nicht groß sehen.
Das einzige, was man vielleicht mal zeigen könnte,
ist, wen ich jetzt ein Sparbuch erzeuge,
mal wieder mit einem Zinssatz von drei Prozent.
Und jetzt sage, auszahlen, ich habe nichts drauf,
0, 0 Euro.
Jetzt sage ich hier irgendwie 10 Euro auszahlen,
dann kracht das hier
Tatsächlich hier Asserge schlägt fehl, also ist Betrag gedeckt,
wird offensichtlich tatsächlich aufgerufen an dieser Stelle.
Und das ist das, der Aufruf von Ist-Betrag
gedeckt in der Klasse Konto, der hier aufgerufen wird.
Interessant. Da steht ja auch der Aufruf. Und hier
ist dann diese Verletzung dieser Vorbedingungen. Es knallt tatsächlich,
also es gibt hier eine Acception, das ist ja das,
was wir wollen, wenn eine Vorbedingung verletzt ist,
das ist ein echter Fehler, dann sollte man hart was tun.
Sollte man nicht so geschehen lassen. Ich habe ja hier
mich als Klient jetzt explizit blöd verhalten.
Gucken wir uns mal an, was das jetzt heißt für abstrakte Klassen.
Also wenn ich eine Methode, wenn ich eine Operation nur deklarieren will,
dann kennzeichne ich sie als Upstract und gebe keinen Rumpf an.
Ganz wichtig ist in Java, wenn eine Methode in einer Klasse Abstract ist,
dann muss die ganze Klasse auch als Abstract deklariert werden.
Das ist zwangsläufig. Also in dem Moment darf ich nämlich
von dieser Klasse Konto gar keine Exemplare mehr erzeugen.
Wenn ihr euch das hier anguckt,
ich kann mir von Konto kein Exemplar erzeugen,
bietet mir Bluegeot auch gar nicht an,
weil es einfach nicht geht.
Weil das Ding ist ja eigentlich unvollständig.
Da ist ja eine Lücke. Da ist ja diese
eine Methode
nicht implementiert
Okay, das ist ganz wichtig, diese Klasse ist abstract,
abstract oder abstrakt. Und sobald eine abstrakte Methode ist,
muss ich das auch schreiben. Hier nochmal ein bisschen dargestellt,
also Methoden, dann kann man Abstract deklarieren.
Und das klassische Sparbuch definiert das. So kann ich also
hier tatsächlich das machen. Eine abstrakte Klasse kann Operation deklarieren,
die lediglich in ihrer Schnittstelle festgelegt sind.
Keine Implementation da. Also das ist genau das,
was ich gerade sagte. Wenn ich jetzt ein Exemplar einer
solchen Klasse erzeugen würde, würde ja irgendwas schiefgehen.
Also darf man das nicht von abstrakten Klassen?
Aber eine Unterklasse, da darf ich natürlich ein Exemplar erzeugen,
wenn die diese Operation implementiert
Also, wenn jetzt Sparbuch, in diesem Fall,
diese abstrakte Methode nicht implementieren würde, nicht definieren würde,
dann müsste Sparbuch selbst auch abstrakt deklariert werden.
Dann könnte ich auch kein Exemplar von Sparbuch erzeugen.
Waldsparbuch, aber tatsächlich alles, was offen gelassen ist,
ausfüllt, kann Sparbuch eine konkrete Klasse sein.
Das nennen wir konkrete Klassen. Ja,
und deswegen kann ich auch ein Exemplar davon erzeugen,
weil sozusagen durch die Sprachdefinition garantiert ist,
dass da was passieren kann, wenn man das aufruft.
Übrigens nur ein Detail, in vielen Sprachen ist es so,
man darf eine Klasse nur dann abstrakt machen,
wenn mindestens eine abstrakte Methode drin ist.
In Java darf ich tatsächlich eine ganze Klasse als abstrakt deklarieren,
auch wenn keine einzige Methode
App Strack ist
Das ist durchaus sinnvoll, weil man vielleicht einfach tatsächlich die ganzen Operationen,
die man ausimplementiert hat, in einer Erbendenklasse nur noch wiederverwenden will.
Okay, also abstrakte Methoden sind aufrufbar,
das haben wir jetzt auch schon, naja,
uns noch nicht so genau angeguckt, aber das haben wir gesehen,
also in dem Zahle aus wird das IS-Betrag gedeckt aufgerufen und tatsächlich wird das da auch,
an dieser Stelle haben wir ja gesehen, aufgerufen.
Das ist immer wieder so ein bisschen komisch,
sich das klarzumachen, wie wird da entschieden,
was da aufgerufen wird?
Das ist ja nicht
so ganz einfach
Also wie kann das überhaupt funktionieren, dass irgendwie da so,
an verschiedenen Stellen Sachen aufgerufen werden und mal das Redefinierte und mal nicht.
Und das ist relativ schnell verwirrend, gerade bei größeren Hierarchien von Klassen.
Und es gibt ja so zwei mentale Modelle, die man
sich irgendwie zur Erleichterung und zur Veranschaulichung klar machen kann.
Das eine ist, Erben heißt Quelltext kopieren und neu übersetzen. Ihr könnt euch vorstellen,
ich habe die Klasse Sparbuch und ich habe das klasse Konto und wenn Klasse Sparbuch sagt,
ich erbe von Konto, dann könnte ich mir vorstellen,
dass alles das,
was im Konto steht,
einfach kopiert wird in den Quelltext von Sparbuch
und dann wird das übersetzt
Das funktioniert ganz gut, weil dann sehe ich zum Beispiel,
wenn ich hier eine Operation leer lasse,
dann fülle ich diese Lücke einfach auf.
Oder wenn ich da eine Operation habe, die wird dann zwar runterkopiert,
aber dann wird sie ja überschrieben durch das,
was ich da wirklich konkret reinschreibe.
Also dieses Redefinieren kann man sich auch durchaus vorstellen.
Es gibt tatsächlich Sprachen, die das genauso machen. Technisch,
die sagen einfach, naja, das ist ein Kopieren von Quelltexten.
Das funktioniert wunderbar. Es ist eine relativ statische Angelegenheit.
Es passiert tatsächlich auf dem Quelltext. Also Sather macht zum Beispiel das,
es gibt so Brömischmann, die das wirklich so machen.
In Java ist es anders, in Java ist das
sogenannte Jojo-Modell umgesetzt
Diese Selbstaufrufe werden dynamisch gebunden. Das heißt,
wenn da ein Operationsaufruf steht in der Klasse,
der eigenen Klasse, also einer Methode der eigenen Klasse,
dann wird die dynamisch gebunden.
Also tatsächlich dafür gesorgt, dass dann zur Laufzeit jeweils die
richtige Methode aufgerufen wird. Und die Suche nach einer geeigneten
Implementation beginnt dann immer in der Klasse des aktuellen Objektes.
Das ist hier nochmal so ein bisschen veranschaulicht.
Also wir haben hier eine Unterklasse Sparbuch.
Und wir haben eine Oberklasse Konto. Jetzt erzeugen wir uns
ein Exemplar der Klasse Sparbuch
Das haben wir eben gesehen. Und ich rufe dann an
dieser Klasse etwas auf. Ich habe zum Beispiel an der
Klasse Sparbuch aufgerufen, auszahlen. Jetzt wird zuerst in dieser,
also das ist wirklich so die Veranschaulichung, wie diese Methodensuche funktioniert.
Ich gucke zuerst in der Unterklasse, gibt es da eine
Operation, eine Methode für die Operation auszahlen?
Nö, hier haben wir ja keine Auszahlen implementiert.
Also gehe ich in der Vererbungshierarchie nach oben,
gucke da, gibt es da einen Auszahlen?
Ja, hier gibt es eine Auszahlen. Okay,
in dem Auszahlen wird jetzt aufgerufen, Dieses ist Konto gedeckt
oder ist Saalo gedeckt oder irgendwie was war das?
Irgendwie ist Betrag gedeckt. Das,
und jetzt geht es wieder genau los,
jetzt wird wieder hier runtergegangen zu dieser Disreferenz,
wir haben immer noch unsere Referenz,
wir haben ein Exemplar der Klasse Sparbuch und jetzt geht der nächste Methoden auf und geht wieder hier los.
Jetzt wird wieder hier geguckt, ist hier ein,
ist Betrag gedeckt, wenn ja, dann wird das genommen.
Es wird also immer, das am weitesten unten stehende,
die am weitesten unten stehende Methode genommen.
Es wird also immer wieder auch bei Aufrufen aus beliebig Oberklassen da oben,
wird immer wieder bei einem dynamisch gebundenen Aufruf hier unten wieder
angefangen und dann wird wieder von unten da oben gesucht
Deswegen dieses rauf und runter gehen, deswegen hier Jojo-Jo-Effekt.
Das ist also tatsächlich wie Operationen auch innerhalb einer Klasse dynamisch
gebunden werden. Das nennt sich also auch spätgebundene Selbstreferenz.
Und das ist alles wieder letztendlich dynamisches Binden, wie wir
es schon letzte Woche auch bei der Polymorphie kennengelernt haben.
Okay. Ich hoffe, das ist einigermaßen klar geworden.
Gibt es dazu erstmal noch Fragen? Ich denke,
das müsst ihr euch dann auch nochmal so ein bisschen vielleicht
in Ruhe ansehen im Video und auch in den Übungen ein bisschen was zu programmieren.
Aber im Großen und Ganzen ist das jetzt erstmal das,
was ich zu diesem Thema hier sagen will, so redefinieren.
Wir werden uns gleich nochmal so ein paar konkretere Beispiele ansehen.
Gibt es denn erstmal noch Fragen zu dem,
was ich bis jetzt gesagt habe? Keine, okay.
Dann fünf Minuten Pause. Okay. Diese Geschichte,
dass Methoden sich gegenseitig aufrufen, Und dass dann Teile dieser Methoden abstrakt sein können.
Das ist etwas, was sehr zentral ist in der objektnotierten Programmierung.
Das ist tatsächlich abstrahiert worden im sogenannten Schablonenmuster.
Wir kommen jetzt schon zu einem ersten Beispiel eines sogenannten Entwurfsmusters.
Die werden wir uns dann auch noch in den nächsten Wochen
ein bisschen genauer ansehen. Ein Muster ist diese Schablonenmuster.
Und da hat man eigentlich den Zusammenhang zwischen einer aufrufenden Methode.
Die ist konkret, sondern hat einen Rumpf mit mehreren Aufrufen.
Und möglicherweise mehreren abstrakten Methoden, die dann aufgerufen werden
Das eine ist die Schablonmethode, das ist eine konkrete Methode.
Die legt irgendwie einen Ablauf, einen Algorithmus oder irgendetwas, ein Verhalten fest.
Und diese Einschubmethoden, das sind diese abstrakten Methoden,
die sind sozusagen bewusst offen gelassen, in einer Oberklasse,
damit man dann in Unterklassen da das gewünschte konkrete Verhalten einfügen kann.
Die Unterklasse schiebt dann, injiziert quasi konkretes Verhalten in diese Klasse hinein.
Also Code Injection wird das auch genannt. Und das ist etwas,
was sehr zentral ist für Objekte und jete Rahmenwerke.
Hier ist mal ein Beispiel, das ist ein bisschen anders
als im Skript
Habe das einfach mal angepasst, damit es ein bisschen realistischer ist.
Auch mehr Bezug hat zu dem, was ihr kennt.
Ihr erinnert euch noch an die Titelliste.
Wir hatten ein Interface-Titelliste und wir hatten dann zwei Klassen,
nämlich Linktitelliste und Ray-Titelliste, die das implementiert haben.
Und jetzt könnte man irgendwie mal sagen, naja, man kann sich vorstellen,
dass es auch wieder Gemeinsamkeiten geben kann zwischen diesen beiden Implementationen und dass man die zusammenfassen könnte in einer abstrakten Oberklasse.
Das wäre so eine Upstract-Titelliste. Und was man hier sieht,
man kann sich dann solche Operationen vorstellen,
wie man kann einen Titel am Ende anfügen, man kann,
und das ist jetzt so eine Idee von so einer Art,
eines Cursors, das ist wirklich technisch nicht ganz sauber,
wie das hier gemacht ist, aber zur Veranschaulichung ganz nützlich.
Man kann also irgendwie sagen,
positioniere mal am Anfang der Liste, steig,
dann geh mal einen weiter, dann kann ich irgendwie fragen,
ob die Liste leer ist, dann kann ich fragen,
ob ich sozusagen am Ende der Liste bin mit meinem Cursor und ich kann mir das konkrete Element geben lassen, wo der Cursor gerade steht.
Also ich habe sozusagen so eine implizite Position,
wenn man auch oft einen Cursor und diese Operation deklariere ich einfach nur,
dann sage ich, die soll es geben und dann kann ich tatsächlich dieser Klasse hier auf Basis von diesen Operationen,
die einfach nur deklariert sind, gefordert sind, einen Contains implementieren.
Also sowas wie eine lineare Suche. Das heißt also,
ich kann irgendwie fragen, ist ein bestimmter Titel enthalten.
Also das erste Mal, wenn das Ding nicht leer ist,
dann fange ich mal an, setze mich auf den Anfang,
solange ich nicht am Ende der Liste bin und das Item,
aktuelle Item, mit dem ich mit dem Cursor gerade stehe,
nicht der Titel ist, mache ich einfach next.
Also blicke mich sozusagen linear durch die Liste durch und
am Ende sage ich halt, wenn ich nicht,
am Ende mir dieses Ende hinaus bin,
dann habe ich das Ding gefunden,
ansonsten nicht
Das ist tatsächlich interessant, dass man das hier komplett implementieren kann,
also richtig ein Algorithmus, komplett implementieren kann,
so was wie lineare Suche, unabhängig davon,
wie es konkret implementiert ist,
ob es eine Etatital-Liste ist oder eine Array-Liste.
Kann man beides damit implementieren. Das ist also so ein Beispiel
und dann ist tatsächlich dieses Contains hier die Schablonenmethode und das sind die ganzen Einschubmethoden.
Okay, das ist nur so ein Beispiel. Wenn man das jetzt auch zu Ende denkt,
sozusagen in der Oberklasse wird irgendetwas definiert und man kann in der Unterklasse,
wenn man will, das übernehmen, man kann es aber auch alles ändern, man kann ja alles redefinieren.
Jetzt möchte man aber vielleicht Basisklasse als Oberklasse festlegen,
dass bestimmte Dinge einfach so bleiben sollen, dass man also garantiert bekommt,
dass wenn eine Unterklasse die beerbt, diese Oberklasse,
dass dann Teile des Verhaltens festgelegt sind und dass man die nicht ändern kann.
Und dafür gibt es tatsächlich einen Mechanismus in Lava und das
ist die Möglichkeit, eine Operation als Final zu deklarieren.
Das heißt also, ich kann tatsächlich sowas sagen wie Gibsaldo
zum Beispiel bei dem Konto, das soll Final sein,
dann kann ich in einer Erbendenklasse Gibsaldo nicht redefinieren.
Dann muss ich einfach genau das nehmen,
was ich da kriege
Das heißt, auf die Art und Weise kann ich Teile des Verhaltens festschreiben,
wenn ich das denn will in der Basisklasse.
Man kann auch eine ganze Klasse als Final deklarieren,
dann darf man überhaupt keine Unterklassen definieren. Das gibt es tatsächlich,
die Klasse String in Java ist so ein Beispiel.
Ihr dürft keine Unterklassen von String definieren, weil die Klasse
komplett Final ist. Also Final bei Operationen,
nur die Operation ist Final, kann nicht redefiniert werden,
ganze Klasse, dann sind alle Operationen sozusagen nicht redefinierbar,
weil ich gar keine Unterklasse bilden kann.
Das ist ganz spannend, weil man tatsächlich da ja
ein Verhalten spezifizieren kann. Man kann also relativ viel Semantik
festlegen und tatsächlich auch garantieren für die Unterklassen
Sehr nützlich. Jetzt kommen wir zum nächsten
technischen Thema und das sind die Konstruktoren in Java.
Ihr habt Konstruktoren bisher kennengelernt als Initialisierer. Das heißt,
in einem Konstruktor wird ein Exemplar, ein frischer Toilette-Exemplar initialisiert.
Ihr habt hoffentlich inzwischen es abgelegt zu glauben,
dass ein Konstruktor ein Objekt erzeugt.
In einem Konstruktor wird das bereits erzeugte Objekt lediglich initialisiert.
Also ein Konstruktor ist eigentlich ein Initialisierer,
ein Konstruktor Aufruf erzeugt ein Exemplar
Das ist jetzt ein bisschen spannend. Wenn ihr euch überlegt,
wir hatten diese Exemplarvariable in Konto mit dem Saldo.
Wir haben ja dieses Beispiel hier mit der,
dieses hier, ich habe hier mein Konto,
da habe ich eine Exemplarvariable Saldo und nehmen wir mal an,
die wäre jetzt nicht protect, sondern die wäre private.
Und in Sparbuch habe ich dann auch vielleicht das Ding private gehabt,
die haben wir eben gesehen, dass es nicht funktioniert,
wir werden dann gleich sehen, wie man es anders machen kann,
aber das wäre jetzt, das ist hier tatsächlich private.
Wenn ich jetzt garantieren will. Also normalerweise gilt ein Konstruktor,
macht ein Konstruktor ja, sorgt dafür,
dass das Objekt in einen vernünftigen Anfangszustand kommt.
Insbesondere für das Vertragsmodell. Da gelten alle in Varianten und von da
ab jeder Operationsaufruf führt wieder von einem konsistenten Zustand in den nächsten konsistenten Zustand.
Das heißt, man möchte durch den Konstruktor garantiert bekommen,
dass das Objekt einen vernünftigen Anfangszustand hat.
Das kann ich aber für die privaten Exemplarvarianten einer Oberklasse überhaupt nicht garantieren,
weil da komme ich ja gar nicht ran.
Das heißt, ich möchte eigentlich dafür sorgen,
dass die Initialisierung von der Oberklasse auch gemacht wird
Und weil das so sinnvoll ist, haben die das
in Java einfach fest definiert. Die haben gesagt,
es muss immer aus einem Konstruktor einer Unterklasse der Konstruktor der Oberklasse aufgerufen werden.
Das ist zwingend so. Man kann das explizit machen,
man kann das wirklich als Programmierer explizit anstoßen, dass da ein Superaufruf passiert.
Das heißt, wenn ihr euch das hier anseht,
das Sparbuch, und hier steht ein Konstruktor,
dann steht da eigentlich implizit
dieser Aufruf.
Parameterlos. Der Compiler generiert das da rein, wenn es
da nicht steht. Wenn ich das jetzt übersetze,
dann lässt sich das wunderbar übersetzen. Ich kann es nur
hinschreiben. Wenn ich es nicht hinschreibe, steht es implizit nahe.
Der Compiler generiert es da hinein. Das ist ganz wichtig,
dass euch das klar ist, weil ihr möglicherweise,
wenn ihr mal mit Vererbung mehr zu tun habt,
euch wundern werdet, warum manchmal Dinge nicht mehr funktionieren.
Es ist nämlich dann problematisch,
dass der Compiler da einfach einen parameterlosen Aufruf reingeneriert,
wenn ihr nicht selber einen hinschreibt,
wenn die Oberklassiker
keinen parameterlosen Konstruktor hat
Es ist ja in diesem Beispiel so,
dass ich hier jetzt diesen Superaufruf explizit hingeschrieben habe,
weil es da oben einem parameterlosen Konstruktor gibt.
Also dann gucken wir uns nochmal eben ganz kurz hier das Konto an.
Da ist der parameterlose Konstruktor. Wenn da kein Konstruktor stünde,
auch wieder eine Default-Regelung in Java, würde ein Konstruktor generiert.
Gut, ich mache mal Folgendes hier in Anfang Saldo.
Anfang Saldo. Und dann sage ich hier alle,
Saldo ist dann eben halt nicht null,
sondern es ist eben halt
dieser Anfangssaldo
Lässt sich, glaube ich, übersetzen.
Ich kann also einfach in einem Konto sagen, naja,
man kann halt bei der Erzeugung eines Kontos,
muss man einen anfangs Soldo angeben.
Das ist jetzt tatsächlich zwingend. Also man kommt da nicht
drum rum. Also ich kann nur mit diesem einen Konstrukt
da hier Exemplare dieser Klasse erzeugen.
So, jetzt versuche ich hier Sparbuch zu übersetzen.
Ich sage jetzt hier mal übersetzen. Und das kracht hier,
weil natürlich hier ein Parameter oder so ein Aufruf steht.
Ich nehme die jetzt mal weg, weil ich denke,
naja, ich habe ja auch irgendwie einen Fehler gemacht.
Aber auch das lässt sich
jetzt nicht übersetzen
Constructor, Konstructor, Konto in Klaas Konto,
cannot be apply to given types, required in, found no arguments.
Also das ist gemein. Der Compiler generiert hier diesen parameterlosen Aufruf hinein,
weil da keiner steht. Parameter ist aber falsch, Parameter los,
ist falsch, weil ich muss ja tatsächlich irgendeinen Anfangssalo angeben.
Das heißt, was ich normalerweise jetzt hier machen würde,
ist bei einem Sparbuch, würde ich vielleicht dann sagen,
naja, da muss ich halt hier hinschreiben,
super null oder ich lasse mir selber wieder in meinem Konstruktor
den Anfangssalo übergeben und kann dann hier tatsächlich diesen weiter übergeben an den Dings her.
Das ist ganz wichtig und das passiert relativ häufig,
sobald ihr mal einen Konstruktor irgendwie selbst definiert bei eurer Klasse,
aufpassen, wenn dann Erbendeklassen keinen super Aufruf haben,
der Compiler generiert einen Parameterlosen Aufruf, das funktioniert nicht,
weil ihr ja Parameter erwartet.
Also das ist dieser sehr zentrale Zusammenhang in Java.
Okay, das ist ganz hübsch. Das führt aber auch
zu einer Konstruktorkette, die werden wir uns gleich ansehen.
Vielleicht nochmal ein Beispiel. Ich kann zum Beispiel mir eigene Acception-Klassen definieren,
also es gibt ja die Klasse Acception in Java definiert.
Jetzt kann ich sagen, ich möchte eine eigene Exception-Klasse definieren,
nicht mal Range-Acception, also ich habe irgendwie eine Bereichsverletzung,
die ich vielleicht irgendwie melden will.
Und dann ist hier tatsächlich, kann man das hinschreiben,
der Konstruktor von Range-Acception ruft, den parameterlosen Konstruktor auf.
Aber Exception hat, man kann ja auch mehrere Konstruktoren haben,
also offensichtlich hat Acception einen parameterlosen Konstruktor,
aber auch einen Konstruktor mit einem String als Parameter.
Das kann ich dann auch hier einfach durchreichen,
man sieht hier String als Parameter,
wird hier bei der Range-Acception reingegeben und der wird einfach weitergereicht
Es ist immer zwingend, dass da
irgendein Superaufruf stehen muss oder der Compiler macht einen rein.
Und man kann dann hier zum Beispiel auch sowas bauen,
wie bei meiner speziellen Acception-Klasse, kann ich sagen,
na, es ist eine Range-Acception.
Ich weiß jetzt irgendwie, es ist eine Bereichsverletzung.
Ich möchte jetzt den Index, der da falsch ist,
mit übergeben, damit das dann der Fehlerdiagnose verfügbar ist.
Also stecke ich den hier mit rein in den Konstruktor und
dann wird hier tatsächlich einfach auch dieser String-Parameter zusammengebastelt mit dem Index da drin.
Auch das kann man so machen. So,
aber ein wichtiger Punkt ist eben halt, immer noch nicht,
die Kette. Hier ist noch ein weiteres Beispiel für Konstrukturen
und Vererbung
Wir haben eine Klasse, Punkt. Die besteht aus
einer X- und einer Y-Koordinate. Definiert also Punkte in,
sagen wir mal, im kathesischen Koordinatensystem. Und dann ist
es hier so, es gibt ein Default-Konstruktor ohne Parameter.
Und es gibt einen zweiten Konstrukter mit zwei Parametern.
Und was dann häufig der Fall ist,
dass man so etwas schreibt wie hier,
dass man mit This den eigenen Konstruktor aufruft.
Das ist die einzige Ausnahme von der Regel,
dass in einem Konstruktor als erstes
immer ein super Aufruf stehen muss
Das ist die einzige Ausnahme, man darf auch Biss hinschreiben.
Man darf also einen eigenen Konstruktor aufrufen. Es muss immer
aus einem Konstruktor, muss immer irgendein anderer Konstruktor aufgerufen werden.
Entweder der Superklasse oder ein eigener. Und dann ist es irgendwie transitiv klar,
dass mindestens einer von den Konstruktoren in meiner Klasse tatsächlich einen
super Aufruf haben muss, weil sonst wird es nicht funktionieren.
Also hier ist es dann auch wieder so, dass hier implizit ein Superaufruf steht.
Von dem parameterlosen Aufruf, weil wir haben ja nichts hingeschrieben,
also wird das wohl der Parameterlose sein.
Das heißt also, Punkt erbt anscheinend hier von Object,
also wird da der parameterlose Konstruktor von Object aufgerufen
Und dann sieht man hier, kann eine Klasse das dann wieder erweitern,
hat eine weitere Eigenschaft, definitiv die Farbe von einem Punkt.
Also ein Pixel ist irgendwie ein farbiger Punkt, kann man sagen.
Immer dieser Zusammenhang ist ein, muss ja funktionieren.
Und dann sagt man hier, naja,
man hat halt diese Parameter wieder und dann kann man tatsächlich
den Superaufruf machen mit den beiden Koordinatenparametern und setzt die Farbe hier für diese abgeleitete Klasse.
So, und worauf ich jetzt schon seit zwei Folgen hinaus
will, das ist nämlich das hier.
Sowas kann natürlich auch mehrstufig auftreten. Und das ist tatsächlich,
muss man sich klar machen, in welcher Reihenfolge diese Konstruktoren ausgeführt werden.
Und es ist irgendwie kontraintuitiv. Man denkt irgendwie,
naja, ich erzeuge mir ein Exemplar von diesem DAX-Sparbuch hier.
Ich habe also wieder die Hierarchie, die ihr kennt.
Klasse Konto, klasse Sparbuch. Und jetzt habe ich aber
hier, klasse DAX-Sparbuch, erweitert nochmal das Sparbuch.
Und hier kann man auch einen Zinssatz angeben.
So, hier muss tatsächlich ein Zinssatz angegeben werden.
Also muss ich hier auch den Superaufruf mit dem Zinssatz hinschreiben.
Ja, und man würde jetzt denken,
irgendwie zuerst wird das von der Unterklasse gemacht,
dann von der da drüber und dann von der da drüber.
Es ist genau anders. Ich erzeuge,
ich spiele das mir mal durch,
erzeuge so ein DAX-Sparbuch,
also ein Exemplar dieser Klasse
Was passiert da? Naja, als erstes wird hier der Superaufruf gemacht.
Das ist immer das Erste, was in einem Konstruktor stehen muss.
Was steht da oben? Naja, als erstes ein Superaufruf.
Das heißt also hier wieder ein super Aufruf.
Und eigentlich da jetzt noch wieder ein Superaufruf nach Object.
Und erst wenn der Konstruktor von Object fertig ist,
dann wird hier das hier ausgeführt.
Also Saldo wird auf null gesetzt, dann wird der Zinssatz
auf Z gesetzt und dann wird hier noch die weitere Initialisierung da unten gemacht.
Das heißt also, eigentlich ist die Reihenfolge 1,
2, 3. Und wenn man es genau nimmt,
eigentlich noch null da oben beim Object.
Aber da passiert ja nichts, weil ja Object keine Exemplarvariablen deklariert,
da kann also nicht viel im Zustand passieren.
Aber das muss man sich klar machen, dass durch dieses Constructor-Chaining,
also diese Konstruktorenkette, garantiert ist, dass immer alle Konstruktoren ausgeführt werden.
Und zwar genau in der Reihenfolge zuerst ganz oben,
dann da drunter und so weiter, bis zur abgeleiteten Klasse.
Dann gibt es da noch so ganz viel gelinische Randeffekte,
die will ich hier nicht groß thematisieren.
Es ist relativ schwierig, wenn die aus einem Konstruktor
eine Exemplarmethode aufruft, die dynamisch gebunden wird.
Das könnt ihr euch mal in Ruhe überlegen, warum das schwierig sein kann.
Das will ich hier nicht vertiefen. Ich habe es einfach
inzwischen raus der Verlosung rausgenommen, Ja, schon ein Randfall ist.
Aber man muss da eben auch wieder sehr aufpassen,
dass man das nicht falsch macht.
Worauf ich jetzt noch eingehen will, ist das Thema Klienten- und Erbenschnittstelle.
Ich habe schon drüber gesprochen. Es ist durchaus sinnvoll,
eine Klasse und eine Unterklasse dazu.
Auch da eine Art Klientenverhältnis zu sehen. Das heißt,
es gibt einmal die Erbendeklasse als spezieller Klient. Es gibt normale Klienten,
die einfach nur die Schnittstelle benutzen in einer Klasse.
Alles, was Public Visits ist an der Klientenschnittstelle,
also für beliebige Klienten aufrufbar, was protected ist,
Ist erst mal nur für Erben zugreifbar.
Das haben wir auch gesehen. Wir haben ja gesehen,
dass zum Beispiel hier Saldo und Zinssatz protected waren.
Deswegen konnten wir hier tatsächlich darauf zugreifen. Alles,
was Private ist, ist nur in der deklarierenden Klasse selbst sichtbar.
Das habe ich auch schon gesagt. Das gilt sowohl für
Exemplarvariabel als auch für Methoden. Also eine Methode,
die privat ist, wird tatsächlich auch nicht dynamisch gebunden.
Das heißt, wirklich statisch gebunden in Java,
weil es kann niemand in einer Erbendenklasse das Ding überschreiben.
Man kommt gar nicht ran, man kann es nicht aufrufen.
Also weiß man auch da oben schon,
da weiß der Compiler,
es wird
genau diese Methode aufgerufen
Okay, und diese Idee, dass man auch dem Erben gegenüber
eine Schnittstelle einhalten sollte, um sich Freiheiten offen zu halten,
das versuche ich jetzt mal mit diesem Beispiel hier zu illustrieren.
Ich habe also ein klasse Konto und die hat den Saldo als Float implementiert.
Ob das sinnvoll ist, kann man drum streiten. Jedenfalls hat man dann das klasse Sparbuch,
die ist davon abgeleitet und hat hier dieses Ist gedeckt und sagt halt hier Return Saldo größer,
B. Das heißt,
in der anderen Klasse kann ich auf unterstrich Saldo direkt zugreifen,
obwohl es ja in der Oberklasse definiert ist,
aber weil es protected ist,
komme ich da halt ran
So, jetzt mache ich aber einen Rededesign.
Also irgendwie ändere ich eine Entscheidung hier bei der Klasse.
Ich sage einfach, ich speichere intern den Saldo nicht
als Float, sondern in Form von zwei Ins.
Also ein In für Euro und ein In für Cent.
Kann man so machen? Dem Klienten gegenüber dem normalen Klienten
gegenüber ändert sich eigentlich nix. Man kann tatsächlich die Schnittstelle
genauso beibehalten. Das ist ja das Schöne von dieser Idee,
dass man Dinge privat macht, die diese Entscheidung,
die man da privat hält, die kann dich jederzeit ändern,
ohne dass es einen Klient irgendwie wehtut.
Das ist sehr nützlich
Das passiert aber jetzt hier nicht ganz.
Also weil ich mir jetzt sagen muss, naja,
wenn ich jetzt hier tatsächlich das Saldo gar nicht mehr habe,
ich habe also tatsächlich, ich ändere eine Basisklasse,
das ist in der Literatur auch als Freddial-Based-Class-Problem bekannt.
Ich ändere diese Basisklasse und eine Erbendeklasse geht kaputt,
funktioniert nicht mehr. Es gibt unterstrich Saldo nicht mehr,
weil ich es hier in diesem neuen Design irgendwie anders gemacht habe.
Das ist also eine sehr enge Kopplung zwischen diesen beiden Klassen.
So, das heißt also, das wird änderungsanfälliger.
Ich habe nicht mehr die Freiheiten,
wirklich eine Oberklasse wirklich zu ändern,
ohne dass ich da irgendwie dann darauf achten muss,
dass auch alle Unterklassen damit klarkommen
Was wir stattdessen machen könnten, ist folgendes. Ich habe
meinen Saldo tatsächlich doch privat deklariert. Dann gibt es eine Gibsaldo.
Und dann kann ich tatsächlich hier in meinem Ist gedeckt, dieses Gibsaldo-Aufrufen.
Und ich kann dann tatsächlich diese Entwurfsentscheidung, diese interne Entwurfsentscheidung ändern.
Ich kann also einfach sagen, na gut,
ich will jetzt nicht mehr den Saldo im Float halten,
sondern in zwei Ins.
Dann ist hier einfach das Gibsaldo anders implementiert,
dem Klienten gegenüber sieht es genauso aus.
Und hier kann ich tatsächlich das Ding unverändert lassen
Also auch diese Grundidee, auch einem Erben,
über sollte man gewisse Geheimnisse haben und Geheimnisse heißt halt,
ich habe ein paar Dinge privat,
die wirklich nur hier in meiner Klasse so definiert sind und wenn ich die ändere,
diese Entscheidung, wird sich niemand dran stören können.
Das ist also die Grundidee von dem Geheimnisprinzip. Ich werde
das auch nochmal in der Entwurfsvorlesung, die noch kommt,
nochmal thematisieren. Aber hier seht ihr sozusagen ein erstes,
ein erstes Beispiel dafür, dass das nützlich sein kann.
Okay, also wir sollten das immer versuchen,
solche Kapselungen zu machen.
Noch ein technischer Aspekt
Instance Off. Ihr kennt diesen Operator Instance Off.
Ihr habt ihn schon ein paar Mal gesehen,
ihr habt ihn auch, glaube ich, schon in SE1 benutzt.
Wir haben ihn aber auch in SE2 jetzt schon anfangs ein bisschen benutzt.
Liefert True immer dann, wenn eine gegebene Referenz auf ein Exemplar der genannten Klasse verweist.
Das war das, was wir in SE1 gesagt haben. Also zum Beispiel,
ich habe irgendeine Referenz, eine Referenz war ja,
wir hält eine Referenz, wenn ich jetzt frage,
Ref Instance of Object, dann haben wir gesagt,
naja, dann das Liefert True genau dann,
wenn das ein Exemplar der Klasse Object ist.
Ist aber nicht ganz richtig,
weil wir haben ja jetzt inzwischen gelernt,
es gibt ja
auch Unterklassen
Eigentlich ist es, was hier getestet wird, ist,
als ob es ein Exemplar des genannten Typs oder eines seiner Subtypen ist.
Das heißt, jedes beliebige Exemplar, also auch zum Beispiel
ein Exemplar von Sparbuch, liefert auf die Frage, hey,
bist du eine Instance of Object, liefert da True?
Weil jedes Objekt ja von Object erbt. Das heißt,
das ist kein Test, ob es ein Exemplar in
der Klasse ist und das ist ein Typtest.
Und das ist das Schöne, wir haben das ja schon damals
Typtest genannt. Und wir wissen jetzt mit diesen Subtypen,
die wir ja auch kennengelernt haben, dass das durchaus heißen kann,
wenn ich zum Beispiel bei einem Sparbuchfrage.
Bist du vom Typ, bist du ein Instance of Konto?
Dann würde man beim Sparbuch sagen, ja, ich bin auch ein Konto.
Oder also auch True liefern. Gut. Ja,
das ist jetzt, das ist noch nicht das Ende, langsam.
Das ist erst mal das, was ich zur Implementationsvererbung sagen wollte.
Wir haben jetzt gesehen,
wie man ein gemeinsames Verhalten verschiedener Klassen in einer Oberklasse zusammenfassen kann,
wie man auch dieses Verhalten garantieren kann,
eben man Sachen Final macht.
Ich kann auch einfach neue Operationen hinzufügen.
Ich kann das also erweitern. Ich kann auch Dinge
redefinieren,
also anpassen
Abstrakte Klassen können tatsächlich einige Operationen nur deklarieren,
ohne Implementation festzulegen. Und wenn wir über Implementationsvererbung reden,
dann ist auch diese Unterscheidung zwischen Klientenschnittscheller und Erbenschnittstelle überhaupt erst interessant.
Also erst wenn man auf dieser Ebene über das Redefinieren von Methoden redet,
wird das wirklich spannend, dass man diese Unterscheidung machen kann.
Alle Typen, Klassen und Interfaces in Java sind ja Typen,
erben die Operationen des Typs Object.
Alle Klassen erben auch die Methoden der Klasse Object.
Das ist übrigens interessant
Ein Interface, irgendein beliebiges Interface. Wenn ihr
eine Variable von einem Interface definiert, sowas wie bewegbar,
was wir gerade als Beispiel hatten, in diesem Polymorphie-Projekt.
Ich kann über eine Variable vom Typ bewegbar alle Operationen von Upject aufrufen.
Das ist auch etwas, was viele erstmal überrascht. Man denkt ja irgendwie,
inbewegbar steht vielleicht jetzt nur die Operation drin, bewege nach oben,
bewege nach unten, bewege nach links, bewege nach rechts.
Und diese nur diese vier Operationen darf ich aufrufen. Nein,
wenn ich eine Variable von diesem Typ bewegbar habe,
darf ich auch Equals, Hashcode und To String aufrufen.
Weil tatsächlich alle Typen, Nicht nur die Klassen,
auch die Interfaces, alle Operationen des Typs, Object Armen.
Konstruktoren werden nicht vererbt. Das habe ich eben nicht so deutlich gesagt.
Ich habe gesagt, wenn eine klasse Sparbuch von einem Klassekonto er hat,
dann erbt sie automatisch alles, was die Klasse Konto hat.
Alles, was in Ponto ist, ist dann auch in Sparbuch.
Ist eigentlich gelogen oder nicht ganz richtig, weil Konstruktoren werden nicht geerbt.
Das heißt, wenn ich da in dem Konto,
haben wir es ja gesehen,
wenn ich im Konto ein Konstrukteparameter definiere für den Anfang Saldo,
dann habe ich nicht automatisch auch einen Konstruktor in Sparbuch,
der genau einen Parameter erwartet
Also meine Konstrukturen in jeder Klasse neu schreiben. Und muss dann
in denen dafür sorgen, dass dann diese Superkonstruktoren aufgerufen werden.
Okay, was man jetzt nochmal schön in der Zusammenhangfassung sehen kann,
ist, was wir jetzt diskutiert haben. Was ihr kanntet,
war, dass hier Typ-Abstraktion aus SE1, also wir haben ein Interface-List,
Titelliste zum Beispiel, und ich habe zwei verschiedene Implementationen,
die aber eigentlich sich weitgehend an die Semantik von List halten.
Also das ist einfach, das sind zwei verschiedene Implementationen desselben Konzeptes derselben Idee.
Dann haben wir uns jetzt in der letzten Woche angeguckt,
es gibt sowas wie Typ-Hierarchien, dann haben wir festgestellt,
zum Beispiel Set on List, stehen in einer Typhierarchie mit Collection.
Das heißt, Collections sind eine Verallgemeinerung von so Sammlungen wie Set oder List.
Und auf der Ebene von Typhierarchien haben wir dann so Dinge diskutiert
wie Vertragsmodell. Wenn hier eine Vorbedingung definiert ist,
dann darf ich die hier nicht abschwächen.
Und ich darf auch hier keine Nachbedingungen abschwächen.
Also ich darf keine Vorbedingungen verschärfen und keine Nachbedingungen abschwächen.
Ich kann hier Ergebnis, also Ergebnistypen von Methoden,
Co-Variante anpassen.
Das haben wir gesehen
All das findet eigentlich so auf Typ-Ebene statt. Und ich
habe natürlich ganz stark hier auf Typ-Ebene diese Idee von Polymorphie.
Also ich möchte wirklich meistens etwas von dem Supertyp haben,
also irgendwie, ich habe einen statischen Variable von diesem Supertyp
und habe dann zur Laufzeit irgendwelche Exemplare von Untertypen da drunter hängen.
Was wir uns heute angesehen haben, ist diese Idee hier,
die auch im Collections-Framework so implementiert ist.
Ich habe zwei Listen-Implementationen, die haben weite Teile sehr ähnlich implementiert,
also gibt es eine abstrakte Oberklasse Abstractice, die dann relativ viel zusammenfasst.
Das haben wir eben gesehen bei diesem Figurenbeispiel,
aber man hat auch gesehen,
dass wirklich relativ viel Code kann man
gemeinsam haben
Das ist eine Extenzbeziehung in Java, also zwischen Interfaces, ja, ist es eine Extenzbeziehung.
Das ist die Implementsbeziehung hier und das ist auch eine Extenzbeziehung,
also das ist eine Klasse und das ist eine Klasse,
also Beziehung, Vererbungsbeziehung zwischen Klassen, Extense in Java.
Was wir hier jetzt auch gesehen haben,
ist auf Ebene von Code-Wiederverwendung, Reuse,
ich kann hier Methoden redefinieren,
ich kann abstrakte Methoden implementieren, ich muss darauf achten,
dass ich immer den Konstruktor der Oberklasse aufrufe,
das sind alles so Aspekte,
die beschäftigen sich hier mit dieser Idee von Code-Wiederverwendung
Das hier oben habe ich mehr oder weniger gedisst.
Ich habe also gesagt, das vergesst das, das ist zu allgemein.
Also allgemeine ist eine Beziehung, sind nicht das,
was wir mit Vererbung modellieren wollen in Programmiersprachen.
Das sind üblicherweise sehr, sehr eingeschränkte Beziehungen. Das heißt,
wir wollen tatsächlich, dass immer ein Subtyp überall da einsetzbar ist,
wo ein Supertyp erwartet wird.
Damit das funktioniert, müssen bestimmte Regeln eingehalten werden bei der Signatur,
bei den Verträgen und so weiter.
Das einfach nochmal jetzt so als Gesamtüberblick. Allgemein haben wir
jetzt gesehen, was man mit Werbung alles machen kann.
Das steht im Skript
Und auch hier noch mal so ein bisschen das zusammengefasst.
Gibt es noch Fragen jetzt zum Thema Vererbung?
Es war heute relativ viel technisches Zeug. Da ist eine
Frage. Erst die Obersteller ausgeschüttet, reden die danach?
So möchte ich auch mal heraus. Tut mir leid.
Der Herr ganz gut. Also die Frage ist,
was ist, wenn ich tatsächlich mehrere Klassen habe,
die in einer Vererbungshierarchie stehen?
Also wir haben ja, nehmen wir mal
dieses Beispiel hier
Das hier, ja. Ich habe hier also tatsächlich mehrere Konstruktoren.
Wir wissen, dass die sich erstmal alle aufrufen müssen. Das heißt,
dass zuerst der ausgeführt wird, wenn jetzt in diesem Konstruktor,
das war das, was der Student gefragt hat,
wenn jetzt hier eine Methode aufgerufen wird,
die hier unten redefiniert wurde,
dann hat man ein Problem.
Nehmen wir mal an, ich würde jetzt hier eine Methode aufrufen,
eine Operation aufrufen, die tatsächlich dynamisch gebunden ist.
Und in der Redefinition hier unten
würde auf den Zinssatz zugegriffen
Dann haben wir die Situation, dass hier oben diese Operation
aufgerufen wird, bevor der Zinssatz überhaupt richtig initialisiert ist.
Das ist tatsächlich problematisch. Ich möchte nicht,
dass hier an dieser Stelle ein dynamischer Aufruf steht einer Exemplarmethode,
die dann hier weiter unten redefiniert wurde,
weil diese Redefinition hier unten möglicherweise von Dingen Gebrauch macht,
die noch gar nicht richtig initialisiert sind.
Also aus einem Konstruktor als Grundregel sollte man
keine dynamisch gebundenen eigenen Operationen aufrufen.
Im Konstruktor
höchstens private Methoden aufrufen
Weil die werden sowieso statisch gebunden. Aber dynamisch gebundene Sachen sind schwierig,
weil potenziell kann ja eine Erbendeklasse die redefinieren,
in der Redefinition von einem Zustand ausgehen,
der noch gar nicht eingesetzt wurde.
Also erst hier unten, nach diesem Konstruktor ist wirklich
die Gesamtinitialisierung abgeschlossen. Also alle Konstrukturen müssen ausgeführt sein.
Erst dann kann man sagen, alle Inhalte sind installiert.
Und das Objekt ist einem vernünftigen Ausgangszustand.
Sonst würden irgendwelche Methoden in einem Zwischenzustand agieren.
Beantwortet das die Frage? Ja. Sonst noch eine Frage.
Das ist nicht der Fall. Es ist Viertel vor,
wir machen mal pünktlich Schluss.
Vielen Dank