Gut, ich wurde gebeten, das hier anzusagen.
Das steht auch so im Komsi. Also irgendwo hier kommen sie,
Aufgabenblatt 4, Programmierdiktat. Und ich habe übrigens auch,
vielleicht sage ich das immer noch,
vorab habe ich das wieder vergesse, die Folien von Vorlesung 4,
also heute, die sind auch im Komsi.
Das heißt, wer jetzt da irgendwie online das mitverfolgen will mit
seinen eigenen Folien, der kann da sozusagen mit durchblättern.
Das werde ich auch nächste Woche so machen.
Das heißt also, vorab stehen die Folien zur Verfügung.
Wer das gerne hat,
weil wir hatten eigentlich in den Jahren vorher immer ein Skript,
so wie auch in SE1.
Das klappt dieses Semester noch nicht so gut mit
Muss man mal sehen, wie wir das hinkriegen.
Auf jeden Fall habt ihr ja jetzt alle das oder viele von euch haben das Programmierdiktat mitgemacht.
Und jetzt gibt es einen zweiten Termin. Und wenn ihr schlau seid,
macht ihr das nicht in eurem eigenen regulären Termin,
sondern in einem weiteren anderen Termin.
Und wenn ihr an einem Termin erscheint, an einem beliebigen Termin,
dann holt ihr euch einfach den Abnahmezettel, euren eigenen Abnahmezettel,
bringt ein Lichtbildausweis mit und findet euch dann 30 Minuten nach Beginn vor D017 ein.
Und dann wird jemand euch in einen anderen Raum führen
Das ist das Programmierdiktat. Wir haben festgestellt, dass sehr viele Teilnehmer von SE2
nicht genügend Inhalte von SE1 mitgenommen haben in die Veranstaltung.
Das führt dazu, dass man extrem schmerzhafte Entwurfssitzungen hat,
wo man einfach das Gefühl hat,
dass die Leute gar nichts verstanden haben.
Und diesen Schmerz wollen wir uns und auch euch ersparen,
indem wir eben halt frühzeitig darauf hinweisen,
dass die Kenntnisse für SE2 nicht ausreichen.
Deswegen das Programmierdiktat
Okay, soviel zum technischen Drumherum, zu den Randbedingungen.
Jetzt kommt hier wieder eine richtig technische Vorlesung. Die wird so technisch,
dass wahrscheinlich die einen oder anderen von euch sagen werden,
oha, so das mit der Implementationsvererbung,
das ist ja gar nicht so einfach.
Das ist so eine der schwierigsten Konzepte in Java.
Vielleicht noch nebenläufiger Programmierung, mit Threads,
ist das wirklich schon eine ziemlich harte Nuss.
Und es ist eigentlich vergleichsweise einfach,
weil Java ja
nur einfache Implementationsvererbung hat
Trotzdem ist das, was ich jetzt heute hier erzähle,
also ja, nicht ganz ohne und ich werde es aber versuchen,
sehr nachvollziehbar aufzurollen, wie das mit der Implementationsvererbung in Java funktioniert.
Ich habe letzte Woche ja so ein bisschen erzählt,
dass wir hier unterscheiden zwischen Typvererbung,
zwischen Typ Abstraktion und Implementationsvererbung und heute geht es tatsächlich um Implementationsvererbung.
Also das Anordnen von Klassen mit Methoden in Hierarchien.
Eine Motivation für das, was jetzt hier vorgestellt wird,
Implementationsvererbung ist Wiederverwendung, Wiederverwendung ist eine Ein klassisches Fehlversprechen der Objektorientierung.
Das wurde immer gepredigt, ja, mit Objektorientierung ist die Wiederverwendung ja viel leichter.
Bertron Meier hat das gesagt. Viele haben das immer wieder betont,
dass das mit Objektorientierung ja alles viel besser wäre.
Tatsächlich ist das, würde ich sagen, hat sich als relativ falsch entpuppt.
Wiederverwendung ist was ziemlich Anspruchsvolles. Man hatte früher so die Idee,
ah, wenn wir dann ganz viele Klassen haben,
aus dem ersten Projekt, dann haben wir ja ganz viele Klassen,
die wir vielleicht im zweiten Projekt einfach wiederverwenden können.
Je mehr Klassen wir programmieren,
desto mehr Klassen haben wir zur Verfügung,
um aus dem Vollen
zu schöpfen beim nächsten Mal
Und das hat irgendwie so gar nicht funktioniert. Also das
ist einfach nicht passiert. Wiederverwendung kann man eigentlich so ein
bisschen unterteilen. Das ist einmal so diese klassische Wiederverwendung,
die man aus jeder Programmiersprache kennt, nämlich die Idee,
da ist Code, der macht irgendwas Nützliches und den packe ich in eine Bibliothek.
Und diese Bibliothek wird einfach allen Programmierern zur Verfügung gestellt oder eben halt einem kleinen Kreis,
wenn man eine private Bibliothek hat, wie auch immer.
Aber das Zeug,
was schon programmiert wurde und sich als nützlich rausgestellt hat zur Wiederverwendung,
das wird auch zur Verfügung gestellt,
das nennt man dann eine Bibliothek oder eine Library
Das ist das, was man, wie gesagt,
in jeder Sprache machen kann. Was jetzt sozusagen durch Vererbung reinkommt,
in objektorientierten Sprachen, ist diese Implementationsvererbung, dieses Reuse.
Und das sind so drei große Aspekte,
die jetzt hier heute ausführlich diskutiert werden, Erben zum Anpassen,
Erben zum Vermeiden von Redundanz und Erben zum Vervollständigen.
Und nochmal habe ich eben schon gesagt, Implementations- oder Code-Vererbung,
das ist das Organisieren von ausführbaren Softwarelementen, also Methoden,
die wirklich Code enthalten in Hierarchie.
Wenn man Hierarchien hat, dann kann man prinzipiell auf zwei unterschiedliche Vorgehensweisen unterscheiden.
Einmal eine Generalisierung. Das ist so dieses Bottom Up-Vorgehen. Ich habe mehrere Dinge,
stelle fest, oh, die sind sehr ähnlich,
abstrahiere Gemeinsamkeiten hoch und fasse sozusagen Gemeinsamkeiten in einem super Klasse,
in einer Oberklasse zusammen.
Meistens wird das gemacht, um Redundanz zu vermeiden.
Werden wir gleich Beispiele sehen. Man kann aber auch spezialisieren.
Das heißt, da ist etwas und für einen anderen Kontext
stelle ich fest, es sollte da angepasst werden.
Es ist aber trotzdem die gleiche Klasse,
die ich da dann nur,
sagen wir,
etwas spezialisiere
Das können wir uns noch mal so ein bisschen genauer angucken.
Also in einem neuen Kontext passt eine bestehende Klasse nicht vollständig hinein.
Der alte Kontext ist hier das. Da gibt es irgendwie
eine Klasse C, die in einem ersten Projekt entwickelt wurde.
Und jetzt stellt man fest, oh, in einem neuen
Kontext brauchen wir eine Klasse, die so ähnlich ist.
Aber wir brauchen sie etwas abgewandelt. Jetzt kann man
copy on paste machen, das ist der Standard.
Man kopiert einfach Quelltext aus dem einen Projekt ins andere,
dann sind die auch unabhängig voneinander. Wenn man aber feststellt,
man hätte gerne sagen wir Änderungen,
die an einer der Klassen gemacht werden, zum Beispiel in dieser Klasse,
wenn also Fehler entdeckt werden, dass die da behoben werden,
dass diese Fehlerbehebung sich auch auf neue Klassen auswirkt,
dann kann das witzig sein,
dass man mit dem neuen Kontext wirklich von der alten Klasse erbt.
Also dass die wirklich verbunden bleiben und dann auch die neue Klasse,
die ein Delta ist von der alten Klasse,
von Verbesserungen an der Klasse C profitiert.
Sowas, wenn man so was erkennt, ist das meist
eine 1-zu-1-Beziehung. Also da stellt man fest, oh,
die Klasse passt ganz gut, die beerben wir jetzt mal hier.
Das ist auch etwas, was ich in einem Blue Jay-Projekt
hier dabei habe
Hier ist das mal so angedeutet, ja, also eine Klasse Konto, die so gestrickt ist, wie er das erwartet. Ich zeige die ganz kurz hier,
nicht keine Überraschung, ein privater Saldo,
der wird im Konstrukt da initialisiert, man kann einzahlen,
ja, dann sind hier schon mal Vorbedingungen mit dabei,
also das kennt er jetzt schon,
letzte Woche sind ja Vorbedingungen dran gewesen,
also Requirebetrag größer Null, ja,
man kann da also nicht jetzt irgendwie einen negativen Betrag einzahlen und das wird hier geprüft mit einem Assert,
also wenn der Betrag, der Betrag muss größer Null sein,
das wird zugesichert, wenn das nicht zutrifft,
wird hier eine Acception geworfen mit irgendwie diese Meldung hier.
Dann gibt es ein Auszahlen und es gibt ein Gipsaldo.
So, das habt ihr jetzt tausendmal gesehen, das ist nicht
weiter aufregend. Dann ist aber jetzt hier die Idee,
dass tatsächlich diese Klassik hier der Sparer,
das ist jetzt irgendwie mal irgendwie stellvertretend für benutzenden Code,
also für Klientencode, der benutzt jetzt tatsächlich hier das Konto,
das Sparbuch und nicht das Konto.
Wenn ihr euch erinnert,
letzte Woche, als ich über Polymophie gesprochen habe,
da habe ich sehr viel gezeigt,
ein Klient benutzt eigentlich den Supertyp und will davon abstrahieren,
was für unterschiedliche Subtypen
es gibt
Hier ist es jetzt so,
hier ist tatsächlich jetzt der neue Kontext, das Sparbuch,
in einer Bank wird Software entwickelt, so,
jetzt müssen da Sparbücher gebaut werden, man stellt fest,
oh, Mensch, ein Konto haben wir schon,
da steckt schon ziemlich viel drin und das Sparbuch ist ja nur sozusagen eine Variante davon.
Also lassen wir das erben und in dem Kontext,
wo es jetzt um Sparen geht bei der Sparkasse,
da wird dann hier dieser Code zum Tragen kommen und der benutzt wirklich nur dieses Sparbuch.
Also wirklich hier das Sparbuch und das Sparbuch erweitert dann hier
zum Beispiel das Konto um einen Zinssatz. Ja,
man kann also hier einen Zinssatz als Konstrukterparameter übergeben,
macht ja Sinn, beim Sparen gibt es Zinsen,
heutzutage nicht mehr so viel, aber Früher war das mal so.
Auf einem Girokonto gibt es überhaupt keine Zinsen mehr.
Oder auch einem allgemeinen Konto hier in diesem Fall.
Also hier ist es sozusagen so,
dass der Zinssatz diese Eigenschaft im Sparbuch modelliert ist.
Den kann man sich geben lassen. Und dann ist hier
noch ein berechnet Zinsen, das ist noch nicht implementiert.
Das werden wir uns gleich ansehen. Ja, und
der Sparer sieht eigentlich ganz einfach aus. Der benutzt das
einfach nur. Er zeugt sich tatsächlich hier ein Sparbuch,
zahlt hier irgendwie 1000 Watt auch immer ein,
ruft dann berechne Zinsen auf und dann, wenn hier,
wenn der Saldo größer als 500 ist, dann mach bitte mal auszahlen
500. Der neue Saldo beträgt und dann lässt man sich
den Saal durch ihr ausgeben
Also würde man erwarten, dass da irgendwie rauskommt,
der neue Saldo beträgt dann 500,
beziehungsweise wenn da noch Zinsen dazukommen, halt ein bisschen mehr.
Okay, das ist erstmal so ein Fall,
wo wir jetzt hier so dieses klassische Erben zum Anpassen haben,
also so eine Spezialisierung von einer Klasse für einen neuen Kontext.
Eine andere Geschichte ist das hier, Erben zum Vermeiden von Redundanz.
Das ist tatsächlich etwas, was sehr häufig zum Einsatz kommt,
also was häufig der Grund ist für Implementationsvererbung.
Ein Beispiel hier Air Railist und Linked List,
das sind tatsächlich die Klassen im Collections Framework,
die das Interface List implementieren
Und es gibt dazu eine abstrakte Klasse. Und diese abstrakte Klasse ist nur dazu da,
die Dinge, die in beiden Implementationen gleich sind, an einer Stelle zusammenzufassen.
Das heißt, man möchte vermeiden, dass Code,
der hier und da dupliziert ist, zum Wartungsproblem wird,
weil wenn ich da was daran ändere,
muss ich das ja auch da ändern,
weil das ist ja irgendwie das Gleiche,
sind sozusagen kopierte Methoden, die das Gleiche tun.
Also machen wir es doch so, wir holen die mal
da hoch in eine abstrakte Klasse und können dann mit dieser Klasse Redundanz vermeiden.
Da gibt es auch ein Bluejay-Projekt dazu, habe ich dabei.
Das ist hier Redundanz vermeiden
Das ist etwas, was ihr schon mehr oder weniger gesehen habt.
Ihr erinnert euch möglicherweise an dieses Projekt hier,
das von der letzten Woche, da habe ich so ein bisschen gezeigt,
Kreis Quadrat 3, kennt ihr noch, ja,
die Sache mit die Dingern, die Dingern,
die sich auf einer Leinwand selbst zeichnen.
Und da war es dann so, da habe ich gezeigt,
dass man die einen Interface implementieren lassen kann,
farbig und dann an Umfärbern nur noch Polymorph über farbig zugreift und nichts weiß über die konkreten Unterklassen,
die da drunter hängen.
Und dann haben wir ein zweites Interface gehabt,
bewegbar und dann konnte man eben halt entsprechend auch zeigen,
dass so eine Steuerung möglich ist.
Also hier, das ist so klassisch Polymorphie,
das ist dafür da, um tatsächlich deutlich zu machen,
Ich kann von verschiedenen Unterklassen abstrahieren.
Ich behandle sie unter ihrem Supertyp. Und hier ist es
tatsächlich so, wenn man sich jetzt Kreis anguckt,
dann ist das ein ziemlich langes Ding gewesen.
Hier sieht man das so ein bisschen. Rechts, das ist
eine riesenlange Klasse. Und bei Quadrat ist das genauso.
Auch hier, wenn man jetzt irgendwie sich das hier anzeigen lässt,
dann ist das genauso. Eine riesenlange Klasse. Und das Schlimme war,
wenn ihr euch das angeguckt habt in SA1 damals,
diese Klassen, die meisten Sachen sind sehr ähnlich.
Farbe ändern, bewegen. All diese Dinge sind in allen
drei Klassen gleich implementiert gewesen
Und da habe ich mir mal die Mühe gemacht und habe
mal hier für die Vorlesung das anders gebaut und habe mir halt gesagt,
okay, ich versuche mal, alles das,
was die gemeinsam haben, in eine abstrakte Klasse reinzuziehen.
Und siehe da, da kommt eine Klasse raus,
die ist wieder sehr groß. Diese Klasse handelt alle Gemeinsamkeiten
. Das sind dann so alles alle Methoden drin,
wie sichtbar machen, unsichtbar machen, nach rechts bewegen,
nach links bewegen, dieser ganze Zenova ist jetzt da drin.
Und dadurch sind aber diese Klassen plötzlich sehr klein.
Hier, Kreis, seht ihr sehr überschaubar.
Da ist jetzt eigentlich nur noch drin ein Durchmesser,
naja, ein Kreis braucht halt einen Durchmesser.
Das ist spezifisch für einen Kreis
Oder der Konstruktor, den gucken wir uns dann
noch genauer an. Größe ändern ist hier anders,
weil das eben halt sich auf einen Durchmesser bezieht.
Größe ändern ist also speziell für einen Kreis implementiert und das
sichtbar machen ist natürlich auch das Zeichnen hier.
Elypse 2D, Double muss da irgendwie eine Elypse gezeichnet werden,
wenn es ein Kreis sein soll.
Zum Zeichnen auf der Leinwand. Na gut, und beim
Quadrat ist das genauso. Hier nur der spezielle Code zum
Zeichnen eines Recht-Ex ist da irgendwie speziell alles andere,
ist gemeinsam jetzt in der abstrakten Oberklasse.
Das ist ein Knüller. Also da kann man sofort sehen,
das spart mir Redundanz. Da kann ich wirklich Dinge zusammenfassen
und dann habe ich sie an einer Stelle,
dann muss sie auch nur an einer Stelle ändern
Und das funktioniert alles genau so. Es ist
nicht ganz trivial gewesen, das hier zu machen,
aber da gehe ich dann gleich noch mal ein bisschen darauf ein,
wenn wir so auf Vererbung genauer eingehen.
Okay. Und dann gibt es noch das Erben zum Vervollständigen.
Das ist etwas, was ihr sehr viel vorfinden werdet,
wenn ihr mit Ursprachen programmiert.
Das ist so die Richtung hier von Code Injection.
Das heißt, da sind bestimmte Klassen teilweise implementiert. Da sind explizit
Lücken gelassen und diese Lücken kann man in einer Erbendenklasse befüllen.
So kann man sich das so grob vorstellen,
kommen wir gleich noch genauer drauf.
Das ist so der Weg Richtung Rahmenwerke
, Frameworks
Da gibt es ganz viele, das typische Beispiel hier für
Java Swing. Das ist das Framework für grafische Benutzungsoberflächen,
also für so Dinge, wo man hier so Buttons klicken
kann und Dinge grafisch darstellen lassen kann.
Das kommt ausführlich noch in SE2. Das werdet ihr noch intensiver
sehen. Da kommen wir noch drauf. Okay,
das sind so die drei Hauptmotivationen für Implementationsvererbung.
Und im Folgenden will ich jetzt sehr technisch detailliert einfach mal durchgehen,
wie das funktioniert mit dem Erben von Eigenschaften einer Klasse
Und was das Erste ist,
was ganz wichtig ist und das ist halt auch nicht unbedingt selbstverständlich,
ist, alle Eigenschaften der Oberklasse sind durch Vererbung,
auch Eigenschaften in der Unterklasse.
Das heißt tatsächlich, eine Unterklasse erbt alle Felder der Oberklasse.
Das heißt, ein Exemplar der Unterklasse hat die Felder der
Oberklassen und auch die eigenen Klassen, die eigenen Felder,
die dann in der Unterklasse definiert werden.
Also man kann weitere Felder hinzufügen.
Das werden wir gleich mal so
ein bisschen live sehen
Und dann ist es so, dass die Methoden angepasst werden können.
Ihr wisst, so eine Klasse besteht aus Feldern und Methoden
und die Felder können üblicherweise nicht angepasst werden.
Die können halt nur, da können welche dazukommen.
Aber bei den Methoden, da kann man ein bisschen mehr machen.
Bei den Methoden, klassisch in der Objektorientierung,
gibt es so die Möglichkeit, neue Operationen hinzuzufügen.
Tatsächlich ein Sub, eine Unterklasse hat mehr Operationen, als ihr ihre Oberklasse.
Kann sie haben, also kann Operationen hinzugefügt werden.
Dann können aber auch Operationen redefiniert werden. Das heißt,
eine Operation ist in der Oberklasse definiert und ich kann dann
in der Unterklasse sagen, ja, ich möchte das aber anpassen.
Ich möchte diese Methode Für meine Zwecke etwas spezialisieren.
Das wird dann redefinieren genannt. Da haben wir letzte Woche
schon so ein bisschen drüber gesprochen. Diesmal kommt das
ein bisschen ausführlicher. Wir differentieren das hier tatsächlich in SE2
noch in Überschreiben und Erweitern, werden wir gleich im Detail sehen.
Und es kann auch sein, dass tatsächlich eine Methode überhaupt erst definiert wird.
Das heißt, sie wird zwar deklariert in einer Oberklasse,
hat aber keine Implementation,
es gibt keinen Methodenrumpf und dann wird tatsächlich in einer Unterklasse konkret eine Implementation angeboten dazu.
Das heißt, die Lücke wird befüllt,
das kann man auch definieren nennen.
Gucken wir uns mal das allererste Beispiel an.
Erben zum Hinzufügen. Wir haben hier eine Darstellung dessen,
was auch dem Blue-Jay-Projekt zu sehen ist.
Wir haben ein Konto und wir haben ein Sparbuch.
Zwei Klassen. Die Schnittstelle, das,
was der Klient sieht, sozusagen immer von links aus geguckt.
Der Klient guckt ja immer von links auf das Konto.
Der sieht die Schnittstelle, da gibt es unter anderem einen
Gibsaldo oder ähnliches. Was haben wir jetzt hier gesehen?
Moment, hier ist das Projekt. Also das Konto hat
dann so Schnittstellen wie Einzahlen,
Auszahlen Gibsaldo
Ah, okay, das ist also ganz,
das ist die Schnittstelle, die der Klient auch hier sieht.
Aber diese Operationen, die ein Klient an der Schnittstelle sieht,
sind natürlich implementiert in den Rümpfen der Methoden. Das heißt,
man kann das hier ja begrifflich unterscheiden, die Implementation,
das ist der Rumpf der Methode und das nach außen bekannt geben,
dass es da eine Operation gibt, ist das andere.
Und das Sparbuch erbt jetzt hier und definiert weitere Operationen.
Es hat dann auch einen Gipsaldo, hat es automatisch,
weil es eben halt von Konto erbt,
deswegen hat das Sparbuch auch alle Operationen von da oben,
hat außerdem
auch alle Felder
Und kann neue Dinge hinzufügen, zum Beispiel hier sowas
wie berechnet Zinsen. Also das ist das, was hier
auch in der Implementation passiert. Ich mache mal dieses,
das kann man auch offen lassen, hier das Sparbuch.
Also tatsächlich, man sieht das hier jetzt noch nicht,
aber wenn ich jetzt mal von dieser Klasse Sparbuch hier ein Exemplar erzeuge,
und jetzt lege ich mal hier ein Zinssatzfest von drei Prozent,
goldene Zeiten damals.
So, jetzt habe ich hier ein Exemplar erzeugt und
wir haben gesehen, in Konto gibt es eine Exemplarvariable Saldo
Und ins Sparbuch gibt es eine Exemplar, war ja der Zinssatz.
So, und siehe da, unser Exemplar hat jetzt genau diese beiden Exemplarvariablen.
Ich kann auch ein Exemplar der Klasse Konto erzeugen, zack,
dann habe ich hier ein Konto und das hat natürlich nur einen Saldo.
Aber hier diese Klasse, weil sie davon erbt,
hat automatisch alle Felder auch der Oberklasse.
Jedes Exemplar hat dann alle Felder auch der Oberklasse.
Okay. Diese Vererbungsbeziehung geschieht in Java zwischen Klassen
ausschließlich über Extenz
Also ich muss tatsächlich sagen, Klaas, Sparbuch, Extenskonto.
Das sieht man hier auch im Quelltext. Also hier an dieser Stelle,
Sparbuch, Extenskonto. Das muss da explizit so stehen.
Dann Erbsparbuch aller Operationen und Felder und kann neue hinzufügen.
Also zum Beispiel hier gibt es Hinsatz und berechne Zinsen.
So, und jetzt ist es tatsächlich so, das,
was da jetzt geerbt wurde, das kann auch verwendet werden.
Das heißt also, wenn ein Klient, ein Exemplar,
der klassische Sparbuch erzeugt,
dann haben wir eben gerade gesehen,
dann sind alle Zustandsfelder da
Aber es können auch alle Operationen aufgerufen werden.
Also wenn ich mir das jetzt hier angucke,
die Klasse Sparbuch definiert ja eigentlich nur, gibt es Zinssatz und berechnet Zinsen.
Sonst nichts. Trotzdem kann ein Klient von Sparbuch,
der Sparer, hier aufrufen, auszahlen oder einzahlen,
aber natürlich auch berechnet Zinsen.
Das heißt also, alle Operationen der Oberklassen sind dann natürlich
auch benutzbar für einen Klienten.
Okay? Ja, das ist ja jetzt so ganz schön.
Jetzt wollen wir nochmal den Code hier angucken,
der ja auf der Folie
auch drauf ist
Also, der erzeugt sich hier ins Sparbuch mit drei Prozent Zinsen.
Dann würde man sagen, dann werden hier 1000 Euro eingezahlt und dann werden Zinsen berechnet.
Also bei drei Prozent würde man jetzt erwarten,
dass da irgendwie 30 Euro mehr drauf sind. Dann,
wenn der Saldo größer als 500 ist, werden 500 ausgezahlt,
dann sollte der neue Saldo ja betragen, ungefähr 530 Euro.
Wenn das jetzt hier richtig ausgeführt wird. Okay,
dann gucken wir mal, was der Sparer hier so tut.
Der Sparer, diese Operation, die wir gerade gesehen haben
, ist eine statische Methode. Die kann ich also jetzt
einfach aufrufen, ohne ein Exemplar zu erzeugen.
Das ist ja hier nur einfach, um deutlich zu machen,
wie das mit der Benutzung aussieht.
Der neue Saldo beträgt
Hat nicht geklappt, naja, keine Überraschung,
weil berechnet Zinsen hier einfach noch nicht implementiert waren.
Okay, das mache ich jetzt mal eben schnell.
Ich kann mir mal eben so zuwerfen, wie ich das berechne jetzt hier,
diese Zinsen da auf den, dass das da dazukommt.
Hat mal eben so eine kurze Rechnung, die ich machen kann.
Ah gut, ich mache es eben selber.
Also Saldo ist einfach der alte Saldo. Plus, naja,
irgendwie Mhm. Saldo mal 100 durch Zinssatz. Ist das
irgendwie so einigermaßen nachvollziehbar? Sind es satt? Ja,
also Saldo mal 100, dann durch den Zinssatz,
das fußt jetzt so überhaupt nicht, ne?
Muss man gucken hier, was ich hier sonst so vorhatte.
Ach nee, andersrum, genau. Saldo mal Zinssatz,
ja, also hier, da erstmal, ja,
ihr müsst mir mal protestieren, ja.
Also, Saldo mal Zinssatz. Durch 100, das könnte
besser funktionieren. Also, Saldo mal Zinssatz, sind irgendwie,
wenn da 1.000 Euro drauf sind, mal der Zinssatz,
mal drei sind 3.000, durch 100 sind 30.
Dann kommen da wirklich die 30 Euro drauf.
Juhu, sehr einfache Rechnung. Mir geht es überhaupt nicht
jetzt hier um die Genauigkeit und was weiß ich was,
sondern wir machen das ja hier ganz zahlig.
Okay, also wenn das hier berechnet wird,
dann könnt ihr uns übersetzen.
Aber ich kann es nicht übersetzen.
Warum kann ich das
nicht übersetzen
Wer weiß, was hier das Problem ist? Genau,
der Saldo, den wir hier, wir wissen,
ein Exemplar der Klasse Sparbuch hat, dieses Feld, Saldo.
Das Problem ist, in dem Quelltext hier von Sparbuch
komme ich an dieses Feld nicht ran. Das heißt,
ich bin ein Objekt, ich bin ein Exemplar dieser Klasse,
ich habe dieses Feld, aber man kommt nicht ran.
Das heißt, damit das hier funktioniert,
damit ich sozusagen in der Erbendenklasse an ein Feld der Oberklasse herankomme,
muss ich irgendwie dafür sorgen,
dass da ein Zugang möglich ist
Und die schnellste und schmutzigste Lösung ist, hier zu machen, ein Protected.
Das ist etwas, was jetzt neu eingeführt wird heute hier.
Ich kann nicht nur zwischen Private on Public unterscheiden,
sondern auch noch zwischen Private Public und Protected.
Und nächste Woche kommt da noch ein bisschen mehr dazu.
Aber jetzt erstmal hier protected. Das heißt so viel wie,
eine Erbendeklasse kommt da ran. Klienten kommen da nicht unbedingt ran,
ist protected. Also das ist nicht an der Schnittstelle,
aber ein Erbe kommt jetzt da ran. Das heißt,
wenn ich das jetzt übersetze, hier diese Klasse,
dann hat sich hier erstmal nicht viel geändert.
Aber jetzt kann ich tatsächlich auch Sparbuch übersetzen.
Jetzt kann tatsächlich ein Sparbuch hier auf den Saldo zugegriffen werden.
Also eine Erbendeklasse hat jetzt Zugriff auf diese Felder.
Auf dieses eine Feld. Okay, das ist das,
was hier auch tatsächlich passiert und wir haben das Feld jetzt einfach protected gemacht,
führt, wie gesagt, möglicherweise zu Problemen, komme ich später nochmal drauf zurück.
Was wir jetzt auch gesehen haben, ist, ich habe eben gezeigt,
wenn ich ein Exemplar der Klasse Sparbuch erzeuge, Sparbuch hier,
Zinssatz 3, dann kann ich daran aufrufen,
die Operation, die in Sparbuch definiert sind,
Ich kann die Operation aufrufen, die in Konto definiert sind,
weil die sind ja geerbt.
Aber ich kann auch dir aufrufen, die in Object definiert sind.
Und das ist halt tatsächlich immer so. Bei allen Klassen in Java
kann ich genau diese Sachen hier aufrufen. Also alles das,
was in Object definiert ist, also unter anderem das,
was ihr kennt, Equills, Hashcode und Too String.
Die habt ihr alle schon mal gesehen. Die sind auch
aufrufbar. Und das ist tatsächlich nochmal es wert,
dass wir es mit unserer Terminologie hier betrachten.
Die Klasse Object ist ja auch ein Typ.
Das heißt, der Typ Object definiert Operationen
Die aufgrund dieser Beziehung, dass alle Klassen von Object Arm,
also Subtypen vom Typ Object sind,
hat jeder Typ die Operationen to String, Equals on Hashcoat.
Und dann ist es auch noch so, dass diese Operationen,
die für alle Objekte in einem Java-System definiert sind,
dass da auch tatsächlich Implementationen dazu vorliegen sind,
also wirklich ausimplementierte Methoden, das heißt,
to String kann ich an jedem Objekt aufrufen,
Equals und Hashcoat.
Das heißt, es wird immer eine Standard-Implementation,
die in Object definiert ist in der Klasse Object,
zur Anwendung gebracht.
Es sei denn, ich ändere die und das gucken wir
uns ja jetzt an,
wie das funktioniert
Gut, das ist jetzt genau der nächste Schritt.
Erben und redefinieren. Jetzt wollen wir mal das,
was wir da erben, anpassen. Das kann unterschiedliche Gründe
haben. Hier ist mal so ein relativ einfaches Beispiel.
In der Klasse Konto gibt es eine Operation, mache Jahresabschluss.
Das können wir uns mal angucken. Ich habe hier
das BlueJ-Projekt dazu dabei. Das ist jetzt mehr oder weniger
das Gleiche, wie das, was wir gerade gesehen haben.
Also hier jetzt wieder eine Klasse Konto
Die sieht genauso aus. Nur hier ist das Pferd jetzt schon protected.
Nicht viel anders. Aber in der Klasse Konto gibt es auch,
neben Einzahlen auszahlen soll, da gibt es noch einen Mache-Jahresabschluss.
Und dieses Mache-Jahresabschluss ist irgendwie auf so Konten, hat man das halt öfter mal,
dass so einmal im Jahr so ein Jahresabschluss rübergelaufen wird.
Und da werden jetzt zum Beispiel die Gebühren berechnet.
Habt ihr bestimmt schon mal bei eurer Sparkasse oder was weiß ich wo gesehen,
auf dem Kontoauszug, oh Mensch,
hat mich wieder ein paar Euro gekostet,
dass ich hier mein Konto bei der Sparkasse haben darf.
Also wird hier vom Saldo,
wenn jetzt zum Beispiel hier 10 Euro abgezogen
als Gebühren
Das ist der Jahresabschluss, der für ein Konto definiert wird.
So, das soll jetzt tatsächlich für alle Konten der Fall sein.
Und jetzt ist es so, dass in dem Sparbuch
dieser Jahresabschluss so ein bisschen anders aussehen soll.
Da soll nicht nur die Gebühr berechnet werden für so ein Konto,
sondern es sollen auch die Zinsen berechnet werden. Das heißt,
ich möchte in Macherjahresabschluss im Sparbuch, das möchte ich anpassen,
ich möchte, dass da der Jahresabschluss anders aussieht.
Also kann ich das redefinieren. Das heißt, es bleibt
die gleiche Operation. Das heißt, wenn ich Polymorph
ein Konto habe, Und da mache ich Jahresabschlussaufrufe.
Und es ist ein Sparbuch darunter, zur Laufzeit kann ja
sein, dann wird tatsächlich das Richtige Machejahresabschluss aufgerufen.
Und dieses Machejahresabschluss soll was anderes tun. Das ist hier
in diesem Projekt angedeutet. Hier mache ich Jahresabschluss. Sieht
so aus. Ich berechne die Zinsen für das Sparbuch,
weil ein Sparbuch hat eben halt Zinsen,
biete Zinsen an, also müssen die auch mit berechnet werden.
Gehört zum Jahresabschluss mit dazu. Und es soll aber
auch hier das Gleiche gemacht werden wie in der,
im Konto
Also im Kontofall, also es soll natürlich hier der Saldo entsprechend reduziert werden.
Das heißt, die Jahresgebühr soll auch hier abgezogen werden.
Okay, das heißt, und das ist übrigens nur so
am Rande bemerkt, interessant, wie das so implementiert wird.
Man kann das ja so rummachen oder andersrum,
wenn man jetzt die Zinsen zuerst berechnet hätte,
dann würde der Klient ein bisschen mehr Geld kriegen,
ja, weil, egal, aber das, wie gesagt,
wie das eure Bank macht, müsst ihr dann selber rausfinden.
Auf jeden Fall werden hier zwei Dinge getan.
Also ich möchte in meinem in meinem Sparbuch beide Dinge tun.
Also habe ich hier die Methode redefiniert.
Ich kann also jetzt tatsächlich hier der Sparer,
der macht jetzt hier ein neues Sparbuch,
zahlt irgendwie 1.000 Euro an und mache dann den Jahresabschluss.
Einen Jahresabschluss müssten dann also irgendwie 30 Euro drauf sein und
20 Euro abgezogen werden? Nee, Quatsch. 30 Euro
drauf sein und 10 Euro für die Gebühr abgezogen werden.
Also der neue Saldo müsste irgendwie sowas bei 1020 sein oder so.
Mal gucken, ob das hier stimmt, mal ausführen,
hier die Methode, neuer Saldo 1019. Es wäre 1020,
wenn die Reihenfolge andersrum wäre, aber die ziehen ja erst
ab und berechnen dann die Zinsen.
Also ist das
eine Schurkenbank
Gut, also hier sieht man jetzt,
dass tatsächlich ich das in anderen Jahresabschluss gemacht habe.
Tatsächlich das, was ich redefiniert habe. Ich habe einfach
diese Methode hier, die Macherjahresabschluss, die habe ich hier überschrieben.
Das ist das sogenannte Schlusswort overright. Wir nennen das Redefinieren
. Dieses Overright in Java übersetzen wir mit Redefinieren.
Also ich kann eine Operation der Oberklasse erneut deklarieren und implementieren.
Und in diesem Fall machen wir hier nichts mit der Methode von da oben.
Also schreiben wir hier einfach nur das rein,
was wir da drin machen wollen
Es wird also tatsächlich hier jetzt der richtige Jahresabschluss aufgerufen.
Und dieses Overright schreiben wir da halt vernünftigerweise hin. Da steht auch hier,
also hier, das ist ein Schlüsselwort, das besagt,
hallo, die Operation der Oberklasse, die wird jetzt hier verändert.
Die wird jetzt hier angepasst. Gut,
das ist ja ganz schön, aber wenn man jetzt genau hinguckt,
dann sieht man hier zwei, eine Kleinigkeit, die ziemlich lästig ist.
Hier wird bei Mache Jahresabschluss wird berechnet,
Saldo ist gleich Saldo minus
Und im Sparbuch wird hier der gleiche Code stehen.
Hier steht der gleiche Code, hier werden die Gebühren berechnet.
Das ist ziemlich doof. Und eigentlich wäre es doch schöner,
wenn ich jetzt in diesem Jahresabschluss sagen könnte,
mach mal genau das, was in der Oberklasse beim Jahresabschluss passiert.
Und ich mache mal eine Spezialität dazu. Also dieses Zinsen berechnen,
das gehört halt nur bei einem Sparbuch dazu,
aber alles andere sollte genauso laufen.
Und dann kann man tatsächlich das hier anders hinschreiben.
Ich kann hier hinschreiben. Super,
mache Jahresabschluss
Das heißt, was ich hier jetzt tue, ist,
ich sage, rufe mal genau die Methode der Oberklasse auf.
Und dann wird hier noch zusätzlich was anderes gemacht, berechnet Zinsen.
Das lässt sich so auch übersetzen. Und es macht das Gleiche.
Also der Effekt ist jetzt der Gleiche. Das heißt,
wenn ich jetzt wieder den Sparer hier das ausführen lasse,
dann kommt da wieder ein neuer Saldo 2019 raus.
Also das hat an der Semantik nichts geändert, aber es ist jetzt cleverer,
weil wir nicht denselben Code an zwei Stellen stehen haben.
Wenn ich dann irgendwann mal bei der Gebührenberechnung sich was ändert,
muss ich es nur an einer Stelle ändern.
Das ist das, was wir hier sehen
, erweitern
Das eine war, ich überschreibe, das,
was von oben geerbt ist, komplett.
Ich mache einfach eine ganz neue Implementation für dieselbe Operation.
Oder ich erweitere das nur. Das heißt, ich sage,
das Gleiche soll passieren, was da oben auch passiert.
Plus das, was ich hier irgendwie mit berechnet Zinsen mache.
Das ist hier irgendwie so ein bisschen anders gebaut, egal.
Das heißt, mit diesem Superaufruf kann ich direkt auf die Methode
der Oberklasse zugreifen. Super, super gibt es übrigens nicht.
Man kann das nur in der direkten Oberklasse machen.
So, jetzt geht es zum nächsten Schritt.
Jetzt können innerhalb einer Klassendefinition sich Methoden ja
gegenseitig aufrufen
Ja. Was wir jetzt hier gerade gesehen haben,
ist, was sehen wir das hier?
Sehen wir das hier? Moment. Eine redefinierte Operation.
Da habe ich jetzt ein Beispiel konstruiert,
das ist wirklich ein bisschen an den Haaren herbeigezogen.
Ich glaube, das müsste man jetzt hier sich nochmal angucken.
Hier gibt es noch eine Methode, mache Jahresabschluss,
drucke Jahresbericht und diese Methode drucke Jahresbericht, die ruft Mache Jahresabschluss auf.
So, jetzt ist das interessant. Diese Methode,
wird nicht mehr angefasst. Also in dem Sparbuch habe ich
keinen Druckejahresbericht drin. Das heißt, diese klasse Sparbuch,
wenn ich hier ein Exemplar erzeuge, mal wieder drei Prozent Zinsen,
dann hat die eine Methode gehabt von Konto, einen Druckerjahresbericht.
Hier ist der Druckerjahresbericht. Die wird also einfach geerbt,
die wird nicht redefiniert. In Bluejay ist es übrigens ganz schön,
hier sieht man einen Macherjahresabschluss, da steht hier nochmal deutlich,
das ist redefiniert im Sparbuch.
Das haben wir jetzt ja gerade so gesehen,
dass wir
das gemacht haben
Aber dieses Druckejahresbericht wird hier nicht redefiniert. Der Witz ist aber,
diese Methode, diese Methode hier, diese eine Methode,
Druckejahresbericht, ruft diese Methode, mache Jahresabschluss auf.
Das heißt,
eine Methode ruft eine andere auf und wenn ich diese Methode hier,
diese Methode hier neu implementiere,
dann ruft tatsächlich auch die geerbte Methode jetzt
diesen neuen Code auf
Das ist etwas, was im Kern der Objektorientierung ist.
Also das ist sozusagen eines der Kernkonzepte für,
objektkreierte Programmierung, das alter Code, neuen Code aufruft.
Das heißt also, in diesem Fall hier ist es so,
hier steht, mache Jahresabschluss. Und innerhalb der Klasse Konto
wird natürlich, wenn ich ein Exemplar von Konto erzeuge,
genau das mache Jahresabschluss hier aufgerufen.
Jetzt wird diese Methode, Druckerjahresbericht,
einfach geerbt und wenn dann in der erbenden Klasse,
mache Jahresabschluss redefiniert wird, dann wird auch bei Druckejahresbericht entsprechend hier Das aktualisierte Ding aufgerufen.
Das heißt, wenn ich jetzt hier wieder meinem Sparer das benutzen lasse,
kommt da 1019 raus. Und das heißt eben,
dass tatsächlich, da müssen wir vielleicht ein Exemplar erzählen,
haben wir ja schon hier.
Mein Druckejahresbericht, dann ist da tatsächlich der alte Saldo minus 10,
weil tatsächlich die Gebühr abgezogen wurde.
Und Zinsen gab keine, deswegen konnte da nichts berechnet werden.
Ich müsste da vielleicht jetzt mal was drauf addieren,
um das auch sichtbar zu machen.
Gut, das ist, wie gesagt, ganz besonders.
Das alter Code,
neuen Code aufruft
Normalerweise ist das so, wenn ihr mit Bibliotheken arbeitet,
da steht Alter-Code in einer Bibliothek drin und ihr schreibt neuen
Code und ihr benutzt diesen alten Code und ruft ihn einfach auf.
Hier ist es jetzt so, dass sozusagen das invertiert wird, dieses Prinzip.
Das heißt, alter Code, der schon da ist,
ruft plötzlich durch dieses Redefinieren von Methoden, eure Redefinierten Methoden auf.
Okay, das kann man noch weitertreiben. Das kann man
auch weitertreiben. Eine Oberklasse kann eine Operation deklarieren,
ohne eine Implementation anzugeben
Bei dem Konto können wir uns das jetzt noch mal so,
vielleicht, das ist so eine ganz,
ganz anschauliche Möglichkeit, sich das klarzumachen.
Man kann sich vorstellen, wenn ich bei einem Konto sicherstellen will,
dass ein Auszahlen funktioniert. Das haben wir jetzt hier,
das ist das nächste Projekt. Das ist wieder so ähnlich
wie das, was ihr jetzt schon gesehen habt.
Also wir haben jetzt hier wieder unseren entsprechendes Konto und unser Sparbuch.
Aber hier ist jetzt Konto eine abstrakte Klasse.
Das ist hier mit so einem Stereotype markiert.
Konto ist eine abstrakte Klasse, warum das so ist,
werde ich gleich noch erläutern
Was ich eigentlich erst mal zeigen will, ist,
hier ist es so gebaut beim Auszahlen, gibt es hier eine Vorbedingung.
Require ist Betrag gedeckt. Also ich kann erst auszahlen,
wenn der Betrag, der da ausgezahlt werden soll,
für das Konto gedeckt ist. Und dieses ist Betrag gedeckt,
kann ja bei unterschiedlichen Kontos, Konten sehr unterschiedlich definiert sein.
Bei einem Girokonto kann es einfach so sein,
dass ich bis zum Dispo überziehen kann,
bei einem Sparbuch kann ich meinetwegen nur das abheben,
was wirklich drauf ist.
Also maximal bis zum Saldo. Und ich kann also tatsächlich hier
eine Methode reklarieren, die ist jetzt hier tatsächlich nur deklariert.
Und man sieht hier, da steht einfach nichts.
Da steht einfach nur ein Semikola. Diese Methode wird hier
als abstrakt deklariert. Muss man machen,
sonst kann man dieses mit dem leeren Rumf hier nicht schreiben.
Und diese Methode definiert jetzt eine Operation. Das heißt,
ein Klient, der Klasse Konto, ist Betrag gedeckt aufrufen.
Ein Klient kann das aufrufen. Und es ist auch tatsächlich so,
dass auch andere Methoden in derselben Klasse ist Betrag gedeckt aufrufen können.
Also hier sieht man tatsächlich, Voraussetzungen,
Vorbedingungen für das Auszahlen ist,
dass der Betrag gedeckt ist
Und das Schräge ist jetzt an dieser Stelle, dass hier
nichts implementiert ist. Da ist also einfach nichts gebaut.
Kein Code. Trotzdem kann die Methode benutzt werden.
Und in Sparbuch implementiere ich jetzt, dieses Ist-Betrag gedeckt.
Also hier habe ich jetzt einfach Ist-Betrag gedeckt, implementiert,
wenn der Betrag größer null ist und wenn der Betrag kleiner gleich dem Saldo ist.
Ich kann also maximal bei einem Sparbuch so viel Geld abheben,
wie oft im Sparbuch drauf ist.
Ich kann nicht überziehen bei einem Sparbuch. Und hier sieht man jetzt,
hier muss ich jetzt auch wieder mit meinem Override sagen,
die Methode, Eigentlich die Operation,
die in der Oberklasse deklariert worden ist, die definiere ich jetzt.
Deklarieren heißt einfach nur, lieber Klient, da gibt es eine Operation.
Und hier wird jetzt tatsächlich eine Methode angegeben,
die einen Rumpf enthält, die wirklich etwas tut.
Und somit kann ich jetzt von Sparbuch auch ein Exemplar erzeugen.
Das heißt, ich kann mir jetzt hier ein Sparbuch erzeugen,
drei Prozent Zinsen, kann jetzt hier was einzahlen,
kann also hier einzahlen irgendwie tausend.
So, dann habe ich hier meinen Gibsaldo,
habe ich also tausend drauf auf meinem Sparbuch
Und ich kann jetzt hier fragen, könnte ich jetzt
500 Euro abziehen? Und dann sagt der, ja,
das geht, 500 Euro kannst du abziehen.
Aber ich kann jetzt nicht 1.500 Euro hier abziehen,
dann sagt er, das ist False.
So, und wenn ich jetzt diese Methode kann tatsächlich jetzt
in einer Vorbedingung von einer anderen Methode verwendet werden.
Und das machen wir tatsächlich. Hier sagen wir,
in der Klasse Konto ist Betrag gedeckt, wird tatsächlich aufgerufen.
Das heißt, hier wird eine Methode aufgerufen,
die noch gar nicht implementiert ist
Das ist das, was hier mehr oder weniger auf der Folie
auch beschrieben ist. Das heißt also, ist Betrag gedeckt
, ist hier abstrakt deklariert. Und in dem Moment,
wo ich eine Methode drin habe in einer Klasse, die abstrakt ist,
also sozusagen eine unvollständige Methode, die keinen Rumpf hat,
dann kann ich von einer solchen Klasse keine Exemplare erzeugen.
Das verhindert Java. Java zwingt uns dazu,
sobald eine Methode mindestens eine Methode abstrakt ist,
dass dann auch die ganze Klasse abstrakt sein muss.
Deswegen ist jetzt hier in diesem Beispiel Konto auch abstrakt.
Also tatsächlich Abstract Class Konto. Das muss ich machen,
weil ich eine abstrakte Methode drin habe.
Ich kann also hiervon kein Exemplar erzeugen. Ihr seht hier
, ich kann im Kontextmenü einfach kein Exemplar erzeugen.
Das wäre ja auch komisch, weil wenn ich ein Exemplar
erzeugen könnte, könnte ich ja alle Operationen aufrufen.
Und es ist ja mindestens eine nicht implementiert.
Und von Sparbuch kann ich aber ein Exemplar erzeugen. Okay,
das heißt also hier, die absakte klassische oben deklariert nur diese Methode.
Die klassische Sparbuch definiert dann mit einer konkreten Implementation
das Ding
Und was wir dann uns klar machen müssen,
es gibt dann so einige Eigenschaften von abstrakten Klassen,
habe ich eben schon gesagt, ein Exemplar,
kann ich nicht erzeugen, weil es gibt ja Operationen,
die nicht implementiert sind.
Ja, und deswegen kann ich keine Exemplare erzeugen.
Eine Unterklasse müsste auch abstrakt implementiert sein. Also wenn ich
jetzt in Sparbuch hier nicht, ist Betrag gedeckt, implementieren würde.
Ich kann das mal zeigen, ich kommentiere das mal aus.
Jetzt ist in der Klasse Sparbuch,
auch immer noch ist Betrag gedeckt,
nicht implementiert
Wie ich das versuche zu übersetzen,
dann sagt der Compiler, Sparbuch ist Not Abstract,
and does not overright abstract method ist Betrag gedeckt.
Das heißt, wenn eine abstrakte Methode geerbt wird und keine Implementation dazu angegeben wird,
dann muss die Erbeklasse auch wieder abstrakt sein.
Wollen wir aber ja an dieser Stelle nicht.
Das heißt, wir machen hier einfach wieder das rückgängig.
Das heißt, hier ist jetzt wieder das Overright drin.
Wenn wir das jetzt übersetzen, dann ist das eine konkrete Klasse.
Das heißt, Trakteklassen und konkrete Klassen können wir uns unterscheiden.
Ab Fakteklassen sind die, von denen man keine Exemplare erzeugen darf.
Konkrete Klassen sind die, in denen alle abstrakten Methoden implementiert sind.
Was denn da war, übrigens geht, ist,
ich kann tatsächlich auch eine Klasse abstrakt machen,
obwohl sie keine abstrakte Methode enthält.
Das eine ist die Voraussetzung für das andere,
aber nicht umgekehrt. Es kann durchaus sinnvoll sein,
in einer Klasse alle Methoden zu implementieren und trotzdem zu verhindern,
dass man ein Exemplar davon erzeugt.
Okay, was wir jetzt gesehen haben, ist außerdem,
dass diese abstrakten Methoden aufrufbar sind. Also ich habe hier gesehen,
bei Zahle aus, da ist Betrag gedeckt aufgerufen
Und dieser Aufruf war okay. Und tatsächlich war es dann auch so,
in der Erbendenklasse wird dann an der Stelle natürlich genau die
richtige Methode aufgerufen, nämlich mein neues ist Betrag gedeckt,
was ich in der Erdenklasse definiert habe.
Das ist ziemlich komisch. Also das ist etwas,
wo man wirklich so merkt, jetzt wird es langsam kompliziert.
Irgendwie kann ich in einer Klasse zwei Methoden hinschreiben.
Eine ist abstrakt, die andere ist nicht abstrakt.
Ich kann die abstrakte aus der nicht abstrakten aus aufrufen.
Und das funktioniert auch alles. Und was da genau passiert,
ist tatsächlich ein bisschen, sich zwei mentale Modelle machen, um das nachzuvollziehen.
Ein Modell ist, Erben heißt Quelltext kopieren und neu übersetzen.
Also wenn wir jetzt das Beispiel haben,
in der Klasse Konto haben wir diese Lücke bei diesem Ist-Betrag gedeckt.
Und Erben heißt jetzt erstmal, ich kopiere den ganzen Quelltext.
Das heißt, alle Methoden aus Konto werden in Sparbuch reinkopiert,
könnte man sich so vorstellen, in den Quelltext.
Und überall da, wo ich was redefiniere,
da schmeiße ich einfach das, was ich kopiert habe,
raus und schreibe meine eigene Methode rein.
Funktioniert. Und da, wo ich eine Methode kopiert habe,
die keinen Rumpf hatte, muss ich halt dann
einen Rumpf dazuschreiben
Also dieses Quelltext kopieren und neu übersetzen, funktioniert.
Das heißt, diese Quelltextvererbung, diese Implementationsvererbung ist rein statisch machbar.
Theoretisch. Wenn also immer der Quelltext vorliegt von allen Klassen
, kann man das so machen.
Es gibt eine Sprache, hat die das gemacht.
Aber im Allgemeinen wird es halt mit dynamischem Binden gemacht.
Also diese Aufrufe an eigene Methoden werden auch in Java dynamisch gebunden.
Und das ist wieder so ein ganz komplizierter Fall.
Also wenn ich jetzt mir hier das mal vorstelle,
das ist so etwas, was veranschaulicht,
wie diese Methoden Aufrufe stattfinden
Ich habe also ein Exemplar der Unterklasse,
in diesem Fall Sparbuch und rufe jetzt eine Methode daran auf.
Und zuerst wird geguckt, ob in der Unterklasse
eine Implementation für die Methode da ist.
Zum Beispiel bei Zahle ein, haben wir gerade gesehen,
ist ja nicht in der klassischen Sparbuch implementiert.
Also wird quasi hier in die Oberklasse gegangen und dann wird da geguckt,
gibt es da einen Zahle ein?
Jo, da gibt es einen Zahle ein und in Zahle
ein wird jetzt geprüft, ist Konto gedeckt.
Da ist dieser Aufruf von Ist-Konto gedeckt
Und jetzt geht's los. Jetzt wird sozusagen die Selbstreferenz,
jetzt gibt es hier unten wieder angefangen. Es wird also
bei jedem Aufruf an die eigenen Methoden, wird wieder geguckt.
Es wird wieder bei dem Exemplar der Unterklasse angefangen.
Es wird also wieder in dem Sparbuchexemplar geguckt. In der
Klasse von Sparbuch gibt es da einen, ist Betrag gedeckt.
Ah ja, gibt es. Also wird das entsprechend ausgeführt.
Das heißt also, tatsächlich ist das hier so eine Art Yojo-Effekt.
Also je nachdem, wo die Methode implementiert ist,
wird eben die entsprechende Methode da ausgeführt.
Das ist schon ziemlich kompliziert. Das ist etwas,
was definitiv nicht trivial ist und was eben halt dazu führt,
dass man sich mit Implementationsvererbung sehr, sehr gut ins Knie schießen kann.
Also Code erzeugen kann, der sehr schwer nachvollziehbar ist.
Okay. Gibt es jetzt bis zu diesem Punkt Fragen dazu?
Oder seid ihr jetzt erst mal gesandstrahlt? Da ist eine Frage.
Ich hoffe, das Interface wird liebst, selbst treibt.
Wir haben uns an die Hundebedingungen ignorieren müssen, interpretiert worden.
Also so wie jetzt, hört man das relativ wenig an.
Okay, also die Frage ist,
wofür braucht man eigentlich noch Interfaces,
wenn es doch sowas gibt
wie abstrakte Klassen
Und das ist natürlich eine sehr gute Frage. In dem Sinne,
dass man natürlich alles das, was man mit einem Interface
machen kann, auch mit einer abstrakten Klasse machen könnte.
Man kann sich relativ leicht vorstellen,
statt dass ich nur eine Methode in der Oberklasse leer lasse,
also abstrakt deklariere, könnte ich ja gleich alle Methoden abstrakt machen.
Und dann muss ich in der Erbendenklasse tatsächlich alle Methoden auch implementieren.
Das ist sehr ähnlich zu dem, was mit einem Interface passiert.
Und tatsächlich ist technisch ein Interface nichts weiter
als eine vollständig abstrakte Klasse
Das heißt, wenn ihr euch das genau anguckt,
dann steht implizit bei jeder Operation in einem Interface,
steht davor, es ist implizit public und es ist implizit abstract.
Das kann man da auch dahin schreiben in einem Interface.
Nur es ist eben halt redundant, weil es immer so ist.
In einem Interface ist immer alles abstract und public.
Warum man jetzt sowas wie Interfaces eingeführt hat,
das habe ich ja versucht, in SE1 euch klarzumachen.
Ihr konntet mit Interfaces arbeiten, ihr habt hoffentlich Interfaces ganz gut verstanden,
ohne dass ich diesen ganzen Kram hier erklären musste.
Diese ganze Technik, die da stattfindet, mit abstrakten Methoden,
die dann irgendwie redefiniert werden können und sich überschreiben und nicht überschreiben und so weiter.
Das ist kompliziert. Das, was mit Interfaces passiert,
ist einfach. Das ist einfach,
ich beschreibe eine Schnittstelle abstrakt und die kann unterschiedlich implementiert werden.
Aber sozusagen historisch betrachtet sind Interfaces genau so entstanden.
Also in CPlus zum Beispiel gibt es kein Konzept von Interfaces.
Da würde man ein Interface bauen, in dem man eine
vollständig abstrakte Klasse definiert.
Sonst noch Fragen
Dann würde ich sagen,
machen wir mal eben fünf Minuten Pause
Das ist so.
Das ist so
Musik Musik Musik Musik Musik Musik Musik Musik Musik Musik Musik Okay.
Dann lasst uns weitermachen. Die fünf Minuten sind um.
Schaut euch das in Ruhe nochmal an. Ich glaube,
diese Folie muss man sich einfach in Ruhe nochmal so
ein bisschen auf der Zunge zergehen lassen.
Was ich jetzt nochmal erläutere, ist der erste Einstieg in Entwurfsmuster.
Ja, also dieses Entwurfsmuster hier, Schablonenmethode und Einschubmethode,
Template Method, das ist eines der grundlegendsten Entwurfsmuster,
die wir uns in diesem Semester ansehen.
In SE2 werden wir auch über Entwurfsmuster reden,
über Erfahrungen mit Objekt unter der Software,
die dann in bestimmten musterartigen Beschreibungen festgehalten sind.
Und eines solcher zentralen Muster ist das mit der Schablonenmethode.
Wir haben eben gesehen, diese Methode zahle aus,
hat ja aufgerufen,
ist Konto gedeckt
Da hat also eine konkrete Methode, die tatsächlich implementiert ist,
eine abstrakte Methode aufgerufen. Das ging. So,
die Methode, die aufruft, die wird Schablon-Methode genannt.
Das ist also eine konkrete Methode, die eine oder mehrere
abstrakte Methoden aufruft. Und so eine Schablon-Methode liegt üblicherweise schablon-artig,
ein bestimmtes Verhalten oder ein Algorithmus oder etwas ähnliches fest.
Und die Methode, die man sozusagen freilässt,
die wird auch gerne Einschubmethode genannt, oder Hook Method oder wie auch immer.
Also eine Methode, die explizit abstrakt ist,
damit sie dann in Unterklassen implementiert werden kann
Das heißt, man kann in der Schablonenmethode ein Verhalten festlegen
und kann aber in Unterklassen dann das konkretisieren.
Das heißt, diese Eintypenmethoden werden benutzt für Code Injection.
Also so, wie man sozusagen Code in anderen Code,
der schon da ist, rein injizieren kann.
Das ist die ganze Grundlage für objektorientierte Rahmenwerke. Ein Beispiel
habe ich mal hier so angedeutet, man hat eine abstrakte Klasse Abstract-Titelliste.
Ihr könnt euch das so vorstellen. Ihr wisst noch,
in SE1, da gab es sowas wie Titelliste als Interface
und da gab es zwei implementierende Klassen,
nämlich Linktitelliste und Eric-Titelliste
Und da könnte man sich jetzt vorstellen, dass da auch Gemeinsamkeiten existieren,
die man dann in einer abstrakten Klasse zusammenfassen möchte.
Und hier ist das mal so ein bisschen angedeutet,
also eine Methode Contains, die könnte implementiert werden,
also die Frage, ob ein Element in der Titelliste enthalten ist,
ob ein Titel in der Titelliste enthalten ist,
könnte man beantworten, wenn man andere Operationen entsprechend implementiert hat.
Also hier so ein bisschen Start, Next,
MT-Off-Item sind so quasi so Operationen,
mit denen man so navigieren kann über so
eine Liste
Solange man, wenn es nicht leer ist,
die Liste, dann gehe ich an den Anfang,
auf das erste Element und solange nicht auf dem letzten Element stehe oder hinter dem letzten Element stehe,
prüfe ich das Element an der Stelle gerade,
wo ich gerade stehe.
Und wenn es das nicht ist, dann mache ich nichts
und dann liefere ich halt irgendwann das Ergebnis.
Das heißt, hier diese Methoden müsste eine konkrete Unterklasse implementieren,
aber dann könnte diese Methode sozusagen gemeinsam benutzt werden.
Das ist eine Möglichkeit für so eine oder ein Beispiel für
eine Template-Methode, die dann Einschubmethoden benötigt
So eine abstrakte Oberklasse ist aber dann auch noch für andere Dinge nützlich.
Was wir jetzt noch nicht so diskutiert haben, ist,
wenn ich die abstrakte Oberklasse nicht bottom up baue,
also im Sinne von, ich stelle fest,
zwei Klassen haben ziemlich viel gemeinsam,
also ziehe ich was hoch in die Oberklasse,
sondern ich kann tatsächlich eine abstrakte Klasse vorgeben im Sinne von,
so soll überwiegend die Implementation aussehen und ich lasse so ein paar Dinge frei.
Und ich kann tatsächlich sogar erzwingen,
dass Dinge nicht redefiniert werden können.
Ich kann tatsächlich in der Oberklasse
eine Operation als Final deklarieren
Ja, ich könnte tatsächlich, Gibsaldo zum Beispiel,
in der Kontoklasse, als Final deklarieren,
dann könnte ich in der Klasse Sparbuch diese Operation nicht redefinieren.
Es wäre mir nicht möglich. Ich kann also tatsächlich
sozusagen Verhalten schon oben in der Oberklasse einfrieren,
im Sinne von Final deklarieren und dann verhindern,
dass es in der Unterklasse redefiniert wird.
Das ist möglich, da kann ich also tatsächlich quasi eine Spezifikation geben,
wie Dinge zu sein haben in den Unterklassen.
Ich kann nicht nur einzelne Methoden als Final deklarieren,
sondern auch eine ganze Klasse
Hat eine ganz andere Semantik. Wenn einzelne Methoden-Finals sind,
dann können Erbendeklassen diese Methoden nicht redefinieren. Wenn die ganze
Klasse-Final ist, dann darf es gar keine Unterklassen geben.
Ich kann tatsächlich festlegen, dass es von einer Klasse
keine Unterklassen geben soll. Die Klasse String, die ihr
ja gut kennt in Java, die ist zum Beispiel Final.
Ihr könnt keine eigenen Unterklassen von String definieren.
Das ist aus verschiedenen Gründen ganz sinnvoll,
also auch insbesondere,
wenn man
das effizient machen will
Man das dynamische Binden unterbinden will. Gut,
ein ganz wichtiges Thema, was häufig, sagen wir,
vergessen wird im Zusammenhang mit Vererbung, ist das, was die Konstruktoren angeht.
Ich habe auf irgendeiner Folie vorhin gesagt, geschrieben,
alle Eigenschaften der Oberklasse werden in die Unterklasse geerbt.
Das war eigentlich gelogen. Denn die Konstruktoren, die werden nicht geerbt. Das heißt,
wenn eine Klasse, wir können das hier gleich mal uns mal angucken,
Einen Konstruktor definiert, der einen Parameter erwartet,
dann hat nicht die Erbendeklasse automatisch auch einen solchen Konstruktor,
der einen Parameter erwartet.
Was aber erzwungen wird, und vielleicht erinnert ihr euch noch daran,
was ich immer gesagt habe, was ein Konstruktor ist,
ein Konstruktor in Java, ist ja ein Initialisierer.
Ein Konstruktor erzeugt kein Exemplar, sondern der New-Aufruf,
also die Exemplar-Erzeugung, die erzeugt ein Exemplar und dann sorgt der Konstruktor,
den man damit angeben muss, bei dem Aufruf, dafür,
dass das Exemplar in einen Götter multigen Anfangszustand versetzt wird.
Also ganz wichtig, ein Konstruktor sorgt dafür,
dass ein Objekt in einen gültigen Ausgangszustand versetzt wird.
Da werden unter anderem auch dann die In-Varianten eingesetzt.
Also was Fred letzte Woche erzählt hat, dass es oder vorletzte Woche,
dass es sowas gibt wie In-Varianten, das muss sozusagen,
alles das muss hergestellt werden, dafür sorgt ein Konstruktor.
So, und jetzt ist es so,
wenn diese Initialisierung sinnvoll ist,
also wenn ich zum Beispiel in der Klasse Konto einen Konstruktor
habe und in der Klasse Sparbuch jetzt dafür sorgen will,
dass ja nicht nur die Initialisierung, die in Sparbruch gemacht werden,
vorgenommen werden.
Wir haben hier dieses Beispiel Sparbuch. Hier gibt es
einen Konstruktor. Zinssatz. Das Sparbuch erwartet einen Zinssatz.
Und hier wird eine Initialisierung vorgenommen, nämlich,
dass der Zinssatz wird hier auf einen vernünftigen Wert gesetzt.
Diese Igitalisierung ist sinnvoll, aber es ist natürlich auch sinnvoll,
dass der Konstruktor der Oberklasse aufgerufen wird.
Also wenn wir hier das Beispiel haben,
hier gibt es auch einen Konstruktor.
Und hier, das ist eine Initialisierung.
Und da wird der Saldo
auf Null gesetzt
Jetzt könnte es ja sein, dass wir ganz schlau sind,
dass wir sagen, hier, machen wir mal ein Int-Anfangs-Saldo rein.
Das heißt, ich möchte, dass bei einem Konto,
wenn ein Konto erzeugt wird, dann muss immer ein Anfangs-Saldo angegeben werden.
Und dann wird der Saaldo hier auf diesen Anfangs-Saldo gesetzt.
Das kann ich so übersetzen, gar kein Problem.
Das Problem ist jetzt, dass sich das Sparbuch nicht übersetzen kann.
Wenn ich jetzt das hier versuche zu übersetzen, gibt es einen Fehler.
Hier steht jetzt Constructor, Constructor Konto in Klaas Konto,
cannot be apply to given types, required in found no arguments.
Das Gemeine ist, Ihr müsst im Konstruktor,
immer als erstes dem Konstruktor der Oberklasse aufrufen.
Und wenn ihr das nicht tut, wenn ihr das nicht explizit hinschreibt,
dann fügt Java automatisch einen super Aufruf ein.
Also ich könnte jetzt hier, ich muss jetzt hier tatsächlich einen Aufruf machen,
super. Und ich muss jetzt hier einen Parameter angeben,
weil der Konstruktor der Oberklasse erwartet ja einen Parameter,
nämlich den Anfang Saldo.
So lässt sich das jetzt erstmal übersetzen.
Ich habe also tatsächlich jetzt hier
diesen Aufruf reingesetzt
Was jetzt wahrscheinlich schlau wäre, wäre,
wenn jetzt tatsächlich das Sparbuch selber auch als Konstruktorparameter diesen Anfang Saldo
übergeben bekommt und dann entsprechend diesen Wert dann hier bei dem Superaufruf weitergibt.
Was nur einfach jetzt euch klar sein muss,
wenn ich das jetzt hier wegnehme und ich gehe jetzt nochmal
in die Kontoklasse rein und nehme nochmal diesen Konstrukter hier weg,
diesen Konstrukterparameter.
Also hier machen wir wieder mal eine Null rein.
Jetzt lässt sich
das Sparbuch auch übersetzen
Weil ich hier diesen super Aufwurf reingeschrieben habe.
Diesen super Aufruf kann ich auch rausnehmen, das lässt sich
auch so übersetzen, weil dieser Superaufruf automatisch reingeneriert wird.
Es ist tatsächlich so, also man muss es sich,
das sorgt dafür, dass alle Initialisierungen von den Oberklassen vorgenommen werden.
Java zwingt euch dazu. Und wenn ihr den Superaufruf nicht
selber reinschreibt, dann generiert der Compiler den da rein.
Und der Compiler weiß natürlich nicht, was da so für
Konstruktoren stehen und was da an Parametern übergeben werden muss.
Das heißt also in dem Fall,
wo der Konstruktor der Oberklasse
einen Parameter erwartet
Funktioniert diese automatische Generierung von Java nicht mehr.
Und dann werdet ihr daran erinnert, oh ja,
ich muss ja den Konstrukte der Oberklasse aufrufen.
Das geht häufig schief. Okay, hier ist mal
ein Beispiel, ein weiteres Beispiel dafür. Konstrukturen können sich
auch gegenseitig aufrufen. Hier haben wir eine Klasse Punkt,
die kathesische Punkte in einem, also Punkt in einem kanthesischen Konnatensystem modelliert,
mit einer X- und einer Y-Koordinate.
Und es gibt hier einen Konstrukte, der kriegt zwei Werte
und setzt entsprechend die Kordinaten auf diese Werte.
Das ist also ein zweistelliger Konstruktor
Und es gibt einen parameterlosen Konstruktor, ist ja möglich in Java.
Man kann ja Konstruktoren überladen. Und dann steht hier ein Aufruf mit This
. Und das ist die einzige Ausnahme von der Regel,
dass ihr immer als erstes einen Superaufruf in eurem Konstruktor stehen haben müsst.
Ihr dürft statt eines Superaufrufs auch einen This-Aufruf haben.
Das heißt also, hier dieser Konstruktor ruft diesen Konstruktor auf,
aber hier steht wieder implizit ein Superaufruf drin.
Und da wird jetzt tatsächlich, weil Punkt von keiner anderen Klasse ererbt,
wird dann hier mit dem reingenerierten Superaufruf,
der Konstruktor,
der Klasse Object aufgerufen
Es ist also immer ein super Aufruf da und er steht immer
als erstes da. Wenn wir dann eine Klasse Pixel haben,
dann kann ich hier tatsächlich sagen, ja, super XY,
da hier die beiden Koordinaten und die Farbe wird hier extra gesetzt.
Also ein Pixel ist ein farbiger Punkt in dieser Modellierung.
Aber hier muss tatsächlich ein Konstruktor der Oberklasse aufgerufen werden.
Und das führt zu einer Konstruktorkette. Wenn wir jetzt hier
mal so ein mehrstufiges Beispiel haben, also klasse Konto,
dann das klasse Sparbuch, oder haben wir noch ein weiteres klasse DAX-Sparbuch,
Extenz-Sparbuch, nochmal um was anderes.
Dann ist es tatsächlich so,
wenn ich ein Exemplar von dieser Klasse da unten erzeuge,
dann könnte man ja meinen, dass sozusagen die Konstruktoren von unten nach oben aufgerufen werden.
Tatsächlich ist es genau andersrum. Durch diese Regel, dass
immer zuerst der Superaufruf darstellen muss. Also hier Objekterzeugung,
ein neues DAX-Sparbuch wird erzeugt, New Duck-Sparbuch,
dann findet hier ein Aufruf statt.
Das Exemplar wird erzeugt. Als erstes im Konstruktor,
das Exemplar wird erzeugt,
also wird der Konstruktor ausgeführt
Dem Konstrukt, das erste, was da stattfindet,
ist ein super Aufruf. Der muss da immer stinken.
Entweder steht er da von euch explizit hingeschrieben,
in diesem Fall muss der da stehen,
weil hier ja ein Parameter erwartet wird.
Eine Parameterlose würde also nicht ausreichen. Was steht da
als erstes? Natürlich, in jedem Konstruktor steht als erstes
ein super Aufruf. Also auch hier wieder dieser Aufruf.
Und hier steht auch implizit ein super Aufruf für Object.
So, jetzt geht's los, jetzt werden hier diese Initialisierungen ausgeführt.
Dann, wenn der Konstruktor zu Ende ist,
ist dieser Aufruf beendet, dann werden hier die Initialisierung ausgeführt
Und dann die Initialisierung hier, das heißt,
die Reihenfolge der Initialisierung ist wirklich von oben nach unten.
Also zuerst das, dann das, dann das.
Das ist also tatsächlich gewollt, dass von der Oberklasse
bis zur Unterklasse nacheinander die Konstruktoren ausgeführt werden.
Und das wird in Java immer erzwungen. Kann man nichts
dran ändern. Ja. Das ist jetzt gerade nur die
Konstruktur. Die Frage, die ihr stellt, ist,
ob das nur bei den Konstruktoren so ist oder auch bei Methoden.
Und ganz wichtig, das ist nur bei den Konstruktoren so.
Bei den Konstruktoren muss immer als erstes
ein Superaufruf stehen
Bei den Methoden, die ihr sonst so schreibt,
da könnt ihr ja überschreiben. Das heißt, komplett die
Methode der Oberklasse ignorieren. Oder ihr könnt aus der Methode
die Methode aus der Oberklasse aufrufen, mit super Punkt Methodenname.
Das könnt ihr machen, müsst ihr aber nicht.
Aber bei Konstruktoren ist das ein Zwang in Java.
Das heißt, diese Konstruktorkette, die müsst ihr gut verstanden haben.
Gut. Dann habe ich noch ein weiteres kleines Thema,
was spannend ist. Ich habe immer davon gesprochen,
dass eine Klasse eine Schnittstelle definiert.
Haben wir gesagt, also eine Klasse,
die Public Methoden bilden die Operationen
Die dann an der Schnittstelle der Klasse auftauchen, also für
Klienten aufrufbar sind. Jetzt haben wir eine neue Situation.
Wir haben jetzt Klassen, wir haben Klienten und wir haben Erbendeklassen.
Und eine erbende Klasse steht auch in einem bestimmten Verhältnis zu
ihrer Oberklasse. Und auch da kann man sagen,
dass es sowas geben muss wie eine Schnittstelle. Das heißt,
die Schnittstelle, die eine Klasse ihren Erben gegenüber anbietet,
ist auch eine Form von Schnittstelle.
Die unterscheiden wir einfach. Es gibt einmal die Klientenschnittstelle und
einmal die Erbenschnittstelle
Alles das, was protected ist. Ich habe ja gerade gezeigt,
dass man die Exemplarvariable, diese Saldo-Variable protected machen konnte.
Hier ist so ein anderes Beispiel, also oder ein ähnliches Beispiel.
Hier haben wir eine Klasse Konto und da ist ein Saldo als Protected definiert,
damit man dann in der Klasse Sparbuch direkt auf den Saldo zugreifen kann.
Das hatten wir ja so modelliert vorhin. Also ich möchte
den Saldo irgendwie benutzen können, ich möchte ihn auch auslesen können,
möchte ihn vielleicht auch beschreiben können.
Also wurde der hier protected gemacht und dadurch kann ich hier
darauf zugreifen. Jetzt kann es aber sein,
dass ich mich irgendwann im Laufe meines Projektes umentscheide und sage,
ich will jetzt mein Konto intern nicht mehr den Saldo mit einem Float modellieren Sondern ich mache das jetzt anders.
Ich mache das jetzt ganz zahlig mit Euro und Cent.
Also Vorkomma und Nachkamma-Anteil. Die Schnittstelle von dem Konto ändert sich
dadurch eigentlich nicht. Die Operationen können die gleichen bleiben.
Nur das Feld hat sich geändert. Das Problem ist aber jetzt,
dass diese Klasse, die hier erbt,
gar nicht mehr dieses Feld, dieses Feld unter Strichsaldo zugreifen kann.
Das heißt also, dadurch, dass sich die Oberklasse ändere,
habe ich jetzt plötzlich meine Unterklassen ungültig gemacht.
Die Unterklassen sind nicht mehr übersetzbar. Und weil Unterklassen
häufig ja nach der Oberklasse geschrieben werden,
kann ich gar nicht so genau absehen,
was da alles so an Unterklassen existiert
Das heißt, so eine Änderung ist ziemlich lästig.
Das heißt, wir haben ein Änderungsanfälligen Entwurf. Jede Änderung
hier oben führt dazu, dass alle Erbendenklassen angefasst werden müssen.
Das nennt sich auch Fredgile-Baseclass-Problem. Also dieses Sich-Verlassen,
dass die Erbendeklasse sich verlässt auf Innereien ihrer Oberklasse,
das ist nicht gut. Und wir haben ja eine Strategie,
wir können ja sagen, wir,
genau wie wir das schon bei normalen Klienten gesagt haben,
wir kapseln einfach das,
was da drin steckt und verbergen das hinter an der Schnittstelle,
können wir das auch für die Erbenschnittstelle machen.
Das heißt, wir können tatsächlich Unsere Exemplarvariablen immer private machen
und bieten dann für unsere Erben hier eine Methode an Gibsaldo,
die entsprechend den Saldo zugreifbar macht, für die Abende Klasse.
Also hier kann ich jetzt tatsächlich Gibsaldo benutzen und jetzt kann ich mich aber dabei dazu entschließen,
dieses private Detail, dass ich diesen Saldo nicht mehr mit einem Float,
sondern mit zwei Ins modelliere, das kann ich einfach,
ich implementiere mein eigenes Gibsaldo anders und alle Erbendenklassen sind davon völlig unbetroffen.
Also auch Erben gegenüber sollte man eigentlich so diese Idee im Auge behalten.
Mach die Exemplare ja privat und stelle alle deine Operation,
deine Zugriffe mit Methoden zur Verfügung.
Okay, das ist nur noch mal so ein Aspekt,
dass ihr das noch mal gesehen habt.
Dass man also auch bei der Erbenschnittstelle durchaus schlau vorgehen kann.
Ein kleines weiteres Detail noch, der Operator Instance of in Java.
Der suggeriert ja Die Antwort auf die Frage,
ist dieses ein Exemplar einer bestimmten Klasse? Also,
dass dieser, das Instance auf genau dann True liefert,
wenn eine gegebene Referenz auf ein Exemplar der genannten Klasse verweist.
Müssen wir eigentlich jetzt genauer hingucken. Wenn ich dieses Instance of mache,
ich kann tatsächlich hier mir mal etwas machen,
ich kann hier sagen, einem Sparbuch,
jetzt sag doch mal hier ein Sparbuch und ich kann jetzt in der Direkteingabe Fragen,
Sparbuch, Eins, Instance off, Sparbuch.
Das ist klar, dass das funktioniert.
Da liefert er True. Natürlich ist dieses Exemplar hier,
also die Referenz auf das Exemplar, auf das diese Referenz verweist,
Sparbuch 1, dass das ein Sparbuch ist.
Aber ich kann auch fragen, ist es denn ein Instance
of Konto? Und da kommt auch True raus.
Und es kommt auch True raus, wenn ich frage,
ist es denn ein Instance of Object
Das heißt, und das war ja schon ganz schlau,
wir haben das ja in SE1-Typtest genannt, das ist ein Typtest.
Und tatsächlich hat ein Sparbuch ja den Typ Sparbuch,
den Typ Konto und den Typ Object, hat sie alle.
Weil ein Subtyp hat immer auch die Typen seiner Supertypen.
In diesem Fall ist das also tatsächlich so,
dass dieses Instance-Off nicht die Information liefert,
ist es ein Exemplar dieser Klasse, sondern ist es von dem Typ.
Also hier, das auch nochmal ein bisschen genauer,
also Instance-Off liefert, genau dann Schuh,
wenn die Typzusicherung funktionieren würde,
dieses Jahr
Tatsächlich liefert also jede Referenz in Java, wenn ihr fragt,
ist hier ein Instance of Object, liefert True.
Wenn ihr also irgendwie mal verstecken wollt,
dass ihr immer True liefert,
wenn ihr das mal euren Code irgendwie schlecht leserlich machen wollt,
weil ihr irgendwie als Wartungsprogrammierer euren Job sichern wollt,
dann schreibt einfach Ref Instance of Object, also irgendwelche Referenzen.
Nicht jeder wird sofort verstehen, dass da eigentlich implizit immer True steht,
weil alle Exemplare sind immer vom Typ Object in Java.
Gut, aber wie gesagt, wir haben das als Typtest
ganz richtig eingeführt. So, das war jetzt schon alles
das, was ich zur Implementationsvererbung erzählen wollte
Also, man kann gemeinsames Verhalten verschiedener Klassen in einer Oberklasse zusammenfassen.
Das ist so das Bottom-Up. Ich kann dafür sorgen,
dass eine Unterklasse, Code erweitert, also den Code,
den ich in der Oberklasse angegeben habe, erweitert.
Und dass auch dieser Code angepasst werden kann.
Abstrakte Klassen deklarieren für einige Operationen nur die Schnittstelle,
also nur eine Operation, aber keine Implementation.
Und wenn man Implementationsvererbungen sich genau anguckt,
dann wird tatsächlich diese Unterscheidung zwischen Klienten,
Schinsche und Erben,
Schnittstelle tatsächlich interessant
Man tatsächlich drüber reden an der Stelle. Dann die ganze Java spezifisch,
also das sind allgemeine Aussagen zur Implementationsvererbung in klassischen UEO-Sprachen.
Das hier ist sehr Java-spezifisch. Alle Typen erben die Operation
des typisch Object. Das ist interessant,
weil wenn ihr jetzt euch ein Interface mal vorstellt,
in diesem Beispiel hier, nee, in diesem Beispiel,
hier da unten war dieses Beispiel mit diesen ganzen,
hier ist ein Interface bewegbar.
Ihr könnt über eine Variable vom statischen Typ bewegbar, diese vier Operationen aufrufen.
Klar. Aber ihr könnt über eine Variable dieses Interfaces
von diesem Interface-Typ auch, Equals und Hashcoat aufrufen.
Ja, weil alle Interface-Typen automatisch alle Operationen auch von Object anbieten.
Das ist etwas, was erstmal auch ein bisschen viele Leute erstaunt.
Ja, also das geht tatsächlich. Okay, Konstrukturen,
ganz wichtig werden nicht vererbt. Also eine Erbendeklasse hat nicht die Konstrukturen ihrer Oberklasse,
die Konstrukturen müssen neu geschrieben werden in einer Unterklasse und es
Wir müssen aus den Konstruktoren immer die Konstruktoren der Oberklasse aufgerufen werden.
Das ist jetzt noch mal das, was ich jetzt allgemein erzählt habe zur Vererbung.
Ich habe euch letzte Woche versucht zu sagen, vergesst diese generellen Ist-Einbeziehungen.
Für Klassifizierung ist die Vererbung in Objektnotierten Programmiersprachen eigentlich nicht gedacht.
Das ist ein klassisches Missverständnis. Wofür sie sehr intensiv genutzt werden sollten,
ist für Typ Hierarchien, also diese Ist-Einbeziehung,
also eine Set oder eine Liste ist eine Collection
Das ist Subtyp-Polymorphie. Ich kann etwas über den Supertyp-Polymorph benutzen
und kann davon abstrahieren, was es so für konkrete Untertypen gibt.
Dass die Klientensicht, da gibt es dann Operationen auf
der Typ-Ebene und da wird das Vertragsmodell auch sehr wichtig.
Klassisch auf der Ebene von Schnittstellen werden Verträge vereinbart.
Dass man sagt, hier vor Bedingungen für diese Methode ist,
dass der Parameter nicht kleiner als 100 sein darf oder ähnliches.
Das heißt, hier hat man es primär mit Interfaces zu tun,
aber natürlich nicht nur, weil eine Klasse definiert ja auch einen Typ.
Also es gibt auch tatsächlich Typ-Hierarchien, die durch Klassen entstehen
Dann haben wir Typ-Abstraktionen gesehen. Das ist für die Trennung von Typen und Implementation
. Das kennen wir aus SE1. Ein Typ beschrieben als Interface
und dann zwei implementierende Klassen, die das Ganze umsetzen.
Dann gibt es Subclassing, also tatsächlich das Klassen-Miteinander in Vererbungsbeziehungen stehen.
Dann haben wir abstrakte Klassen kennengelernt.
Und dann auch abstrakte Methoden. Das ist klassisch Code-Wiederverwendung.
Also da stehen dann irgendwelche Methoden miteinander in Vererbungsbeziehungen.
Da will man überwiegend Redundanz vermeiden oder man will Frameworks bauen,
also Dinge so bauen,
dass man dann an geeigneten Stellen
eigenen Code einfügen kann
Okay, dann nochmal die Vererbungskonzepte in der Ursprache.
Kann man verschiedene Dinge tun und was eben halt sehr konsequent ist,
Modellierung von Begriffshierarchien in Subtyphierarchien, in sehr engen Subtyphierarchien,
also da die Bedingungen für Subtypen habt ihr jetzt ja letzte Woche gehört.
Für Trennung von Speziellen Emotionen, für dies und dies,
das habe ich eigentlich alles schon gesagt.
Und auf Implementationsebene, habe ich schon gesagt,
gibt es eigentlich in den meisten modernen Sprachen nur noch Einfachvererbung.
Mehrfache Implementationsvererbung, wenn wir uns die angucken würden,
würden wir hier zwei bis drei
Vorlesungen brauchen
Okay. Und allgemein, Interfaces zur Spezifikation von Clitstellen und Klassen
zur abstrakten und konkreten Implementierung. Gut, dann habe ich
nochmal eine kleine, zum Abschluss, eine kleine Storesi-Frage.
Oder wir können auch noch Fragen klären.
Ich meine, ich könnte auch mal nach Fragen fragen.
Gibt es noch Fragen? Dann habe ich noch eine Frage
an euch. Dann machen wir mal hier Storesi.
Olli sind 4,
richtige ID-Laden.
Das hier ist die Tiny URL,
also falls ihr jetzt da, ich kann auch noch,
auch mal den Barcode zeigen für diejenigen,
die jetzt das nochmal eben kurz hier aktivieren wollen,
die teilnehmen wollen an dieser Abstimmung, müsste eigentlich funktionieren.
Gott, hier steht die Frage,
wie viele unmittelbare Oberklassen kann eine Benutzerdefinierte Klasse in Java haben?
Das ist erstmal so eine Frage zum Warmwerden
jetzt hier
Keiner außer Object, entweder ein oder keine, genau eine oder beliebig viele.
Geht nicht. Wird die Frage nicht angezeigt? Das ist ärgerlich
. Man kommt, ob ich hier im Netz bin.
Doch, eigentlich bin ich im Netz.
Oder? Ich mache mal hier, ich gucke mal,
ob ich hier irgendwie an so
einen Link rankomme
Doch, ich bin im Netz. Also ich habe schon mal
einen Netzzugang. Sturé, sie müsste also eigentlich funktionieren.
13 Stimmen sind auch schon eingetroffen. Gut,
also es scheint immer noch so ein Problem zu sein,
dass die Frage nicht angezeigt wird.
Also wie viele unmittelbare Oberklassen, die wird möglicherweise nicht eingezeigt.
Aber die Antwortmöglichkeiten habt ihr hoffentlich trotzdem.
Habt ihr vier Antwortmöglichkeiten jetzt oder habt ihr noch?
Ihr habt nur zwei. Ach, das ist ja sehr schön.
Das macht das wieder sehr attraktiv hier mit dem Sturé
sie an dieser Stelle. Ich beende das mal eben.
Moment, ich gucke mir das nochmal eben an.
Ah, hier gucken. Ah, speichern und schließen.
Also nochmal, Abstimmung. So, HS,
Olli sind 7, Laden, zack, ups.
Das war jetzt nicht das, was ich wollte.
Also Abstimmung 2000, Vorlesung 4, vor dem 4, ja.
Und die richtige ID, oh, ist das kompliziert.
So, jetzt nochmal gucken, ist die Frage jetzt da?
Immer noch nicht
Ach, die Technik, herrlich. Wer braucht das?
Braucht ihr immer noch? Okay, naja.
Wie viele Antwortmöglichkeiten habt ihr jetzt? Zwei. Es ist
zum Kotzen.
Ah. Gut, macht nichts. Ich stoppe das mal,
wir gucken mal, ob die richtigen Antworten bei den ersten beiden dabei sind.
Ja, also komischerweise habt ihr euch nur für die ersten beiden entschieden.
Ja, und komischerweise ist keine davon richtig.
Großartig, ja, toll, ich bin total begeistert.
Nein, es ist natürlich nur genau eine.
Ja, also eine unmittelbare Oberklasse in Java
Wollten wahrscheinlich alle abstimmen, aber konntet ihr ja nicht.
Gut, hier habe ich eine ganz andere Frage.
Das ist mal etwas konkreter. Hier sind drei Klassen,
die in einem Zusammenhang stehen. Klasse A,
Klasse B, die von A erbt und Klasse
Test, die dann ein Exemplar von B benutzt.
Lest euch das in Ruhe durch? Hier ist dann gleich
die Frage dazu
Also, was kommt da raus,
wenn die Methode Maine hier von Test aufgerufen wird?
Was kommt da raus? B, A, B,
B, A oder lediglich eine der Zeiten?
Wahrscheinlich habt ihr jetzt wieder nur zwei Möglichkeiten zum Antworten.
Da sind schon wieder neun Stimmen eingetroffen.
Wieder nur zwei Möglichkeiten jetzt, ja, ne?
Aber sind jetzt vier Möglichkeiten da?
Zwei, das ist super
Ich habe jetzt von Wolf gehört, dass es
daran liegen könnte, dass ich mit einem Windows-Rechner arbeite.
Und da die Kodierung von Umlauten anders ist als auf anderen Systemen.
Und deswegen kommt der Server damit durcheinander. Das heißt,
ich habe jetzt hier eine Frage mit einem Umlaut drin und
deswegen scheint er die Frage nicht irgendwie auf dem Server darstellen zu können.
Das sind so die klassischen Probleme, mit denen sich Informatiker rumschlagen.
Kodierungsfragen. Gut, also jetzt haben wir hier 35 Antworten.
Ich halte das mal an,
wir gucken mal auf die Verteilung und siehe da,
es sind wieder nur die beiden Möglichkeiten
gewählt worden
Und 75 Prozent haben sich immerhin für AB entschieden, das ist tatsächlich auch die richtige Antwort.
Weil wenn hier ein Exemplar von B erzeugt wird,
dann ist es in Java immer so,
dass der Konstruktor von B aufgerufen wird und im Konstruktor von B steht implizit
ein super Aufruf des Konstruktors von A. Also wird zuerst System Outprint A gemacht,
wenn das zu Ende ist, dann wird hier die Initialisierung vorgenommen,
also B wird ausgegeben und dann ist das Ganze fertig,
das heißt, es kommt A, B raus.
Also immer dran denken, Konstruktoren sind in einer Kette zusammengeführt.
Ja, sehr schön, danke für eure Geduld ohne Storesi
, Schluss für heute,