Motivation: Objekte und die Orientierung daran
Grundlage der objektorientierten Sichtweise ist buchstäblich die Orientierung an den Objekten. Diese Objekte bilden reale oder gedachte Gegenstände (auch: Konzepte oder Entitäten) der Realität (oder was wir dafür halten) ab.
Grundlegend neu ist an dieser Sichtweise nichts, der Mensch wendet sie seit jeher und beginnend mit frühester Kindheit spontan und intuitiv an, dies haben jüngste pädagogische Experimente bestätigt (vgl. Frankfurter Allgemeine Zeitung, 18. Aug. 1999, Nr. 190, S. N3).

Neu hingegen der Transfer dieser "natürlichen" Weltsicht auf die Technik, konkret als Paradigma auf den Programmierprozess.

back to top   1 Das objektorientierte Paradigma

 

Grundbegriffe

Das objektorientierte (Abk. OO) Paradigma bildet keine geschlossene theoretisch fundierte Basis (wie z.B. die Relationentheorie für relationale Datenbanken), sondern lediglich eine -- noch dazu unscharfe -- Zusammenfassung einzelner Prinzipien.

Diese Prinzipien sind üblicherweise mit den Begriffen

belegt. Jedoch wird diese Aufzählung je nach Autor um weitere Begriffe ergänzt oder einige der angegebenen ganz weggelassen. Darüber hinaus werden diese Schlagworte der Objektorientierung teilweise unterschiedlichst definiert.
Im Rahmen der Vorlesung sind jedoch die im folgenden gegebenen Definitionen bindend.

Exkurs: Der Begriff des Paradigmas(siehe gesondertes Handout)

back to top   2 Historie objektorientierter Programmiersprachen

 

Die Idee der Objektorientierung ist mehr als 25 Jahre alt. Ebenso die ersten Ansätze objektorientierte Programmiersprachen (Abk. OOP) zu entwickeln.

Geschichtliche Entwicklung objektorientierter Programmiersprachen

Trotz des "Entwicklungsbalasts" (nicht zu vergessen das immer drohende second system syndom) ist es mit Java gelungen eine kleine (d.h. an Grundkonzepten überschaubare) aber dennoch mächtige Sprache zu gestalten.

back to top   3 Elemente des objektorienterten Paradigmas, ihre graphische Beschreibung und Umsetzung mit Java

 

Das OO-Paradigma liefert ein Rahmenwerk der Philosophie, jedoch keinen Anhaltspunkt der technischen Umsetzung.
Es ist jedoch naheliegend eine OOP, die konkrete Manifestation des Paradigmas, vollständig neu zu definieren ohne auf bestehende Realisierungen anderer Paradigmen aufzusetzen (wie dies bei C++ geschehen ist), um eine "rein" objektorientierte Sprache zu erhalten.
Gemeinsam ist allen OOPs jedoch das zugrundeliegende Paradigma, d.h. die sprachunabhängigen Basiskonzepte. Zur besseren -- und vor allem OOP unabhängigen -- Beschreibung objektorientierter Sachverhalte sind im laufe der Zeit sog. Beschreibungssprachen entstanden. Sie geben objektorientierte Konzepte sprachneutral wieder. Eine besondere Bedeutung kommt den graphischen Beschreibungssprachen zu, das sie die gewünschte Abstraktion von der konkreten Syntax um Übersichtlichkeit und somit leichte Erfaßbarkeit ergänzen. Wir werden im folgenden die durch die Object Management Group (Abk. OMG>) standardisierte graphische Beschreibungssprache Unified Modeling Language (Abk. UML) betrachten.

3.1 Visualisierung objektorientierter Sachverhalte mit der Unified Modeling Language

Während es bereits 25 Jahre Publikationen zur objektorientierten Programmierung gibt, existieren erst seit den frühen 90er Jahren Bücher zu objektorientierten Analyse- und Designmethoden.
Zu ihnen gehörigen die von Booch, Coad und Yourdan, Rumbaugh et al., Wirfs-Brock und Johnson, Shlaer und Mellor, Martin und Odell, Henderson-Sellers sowie Firesmith. Viele der Methoden sind nicht das Ergebnis einer gezielten Entwicklung, sondern vielmehr die abstrahierte Essenz praktisch durchgeführter OO Projekte, vornehmlich in der US-Industrie. Als Beispiel hierfür sei die im Rahmen konkreter Projekte bei General-Electric entwickelte Object Modeling Technique (Abk. OMT) angeführt.

OO Modellierungsmethodenevolution

Die Graphik zeigt einige objektorientierte Modellierungsmethoden und ihre Entstehung.

Deutlich tritt sowohl der Aspekt der Integration bestehender Methoden in die UML als auch die Absorption mit ihr konkurrierender Ansätze wie der Object Modeling Language (Abk. OML) hervor.

Hinweis:
zur Begriffunterscheidung Methode vs. Notation:
Eine Methode [grch. methodos "Weg"] bezeichnet eine Vorgehensweise, d.h. einen dynamischen Prozess, der unter bestimmten Eingabedaten zu einem Resultat führt. Im vorliegenden Fall bildet die zu lösende Fragestellung (das Problem) die Eingangsdaten, die unter Anwendung eines konkreten Prozesses (eben der Modellierungsmethode) in eine Modellierungssprache abgebildet werden.
Eine Notation ist eine graphische Beschreibungssprache zur Darstellung bestimmter (in unserem Falle: objektorientierter) Sachverhalte.

Die voneinander unabhängig entwickelten, am Markt sehr stark konkurrierenden, Methoden und Notationen von Booch und Rumbaugh setzen sich Anfang der 90er Jahre als die beliebtesten (gemessen am tatsächlichen Verwendungsgrad in industriellen Projekten) durch.
Die zweite Generation der OO-Entwurfsmethoden ist sehr stark vom Versuch geprägt, gute Ansätze, die sich als quasi-Standard etabliert hatten, in Konkurrenzmethoden zu integrieren.
Während Rumbaughs OMT sehr stark an die klassischen strukturierten Methoden zur statischen Datenmodellierung bis hin zum Design relationaler Datenbankstrukturen angelehnt ist und den Analyseprozess vielfältig unterstützt, deckt die Booch-Methode (Object Oriented Design (Abk. OOD) gennant) die Bereiche kommerzieller, technischer und auch zeitkritischer Anwendungen, insbesondere die Zielsprachenimplementierung, ab. Bemerkenswert zur Terminologie ist der Bezug der eigentlichen Tätigkeitsgattung -- dem Prozess des objektorientierten Designs -- auf die konkrete Methode, die einen klaren Abstraktionsbruch darstellt.
Inhaltlich grenzen sich beide Methoden weder von der Begrifflichkeit her, noch von der zugrundeliegenden Analyse- und Designmethode signifikant voneinander ab. Jedoch führten die hauptsächlich graphischen Unterschiede (die Notation) zu einem wahren "Methodenstreit" -- sogar von einem method war war zu lesen.

Auslöser des Versuchs die verschiedenen, ohnehin zunehmend konfluenten, Noationen zu einer einzigen zu vereinigen war ein Request for Proposals der Object Managment Group. Hierbei bot sich erstmals eine Gelegenheit für einen objektorientierten Analyse- und Designstandard, der eine einheitliche Begriffs- und Notationsbasis schafft.
Die Entwicklung der -- späteren -- Unified Modeling Language begann 1995 nach dem Wechsel von Jim Rumbaugh zur Rational Software Corp., bei der bereits Grady Booch seine Wirkungsstädte hatte, mit dem Versuch der Autoren ihre beiden Methoden und Notationen zu vereinigen. Hauptfokus hierbei war das methodische Vorgehen innerhalb der Analyse- und Designphasen. Eine Draftversion der (Grand) Unified Method Version 0.8, die sich auf die rein statische Modellierung beschränkte, wurde im Oktober 1995 der Öffentlichkeit vorgestellt. Auf der OOPSLA-Konferenz '95 wurde plakativ das offizielle Ende des Methodenstreits bekanntgegeben.
Im Spätherbst des selben Jahres wurden die beiden in ihren Bemühungen, nach der Übernahme der Firma Objectory durch Rational, um Ivar Jacobson ergänzt. Dies eröffnete die Möglichkeit, die entwickelte Modellierungssprache um dynamische Elemente zur Modellierung von Abläufen zu erweitern. Insbesondere sollten die durch Jacobson beschriebenen Use-Cases in die gemeinsame Vorgehensweise und Notation integriert werden. Das Autorenteam wird seitdem in der Fachwelt als die drei Amigos bezeichnet.

Angesichts der Schwierigkeiten bei der Vereinigung der verschiedenen Entwicklungsmethoden wurde diese -- zugunsten der Vereinheitlichung der graphischen Elemente -- auf unbestimmte Zeit verschoben. Äußerlich zeigt sich dies in der Umbenennung der Unified Method in Unified Modeling Language.
Erklärtes Ziel bleibt jedoch die einheitlich definierte Semantik aller graphischen Elemente.

Evolution der Unified Modeling Language

Zu Anfang der Vereinigungsbemühungen wurden durch die Autoren vier Anwendungsziele für die im Entstehen begriffene UML postuliert:

Hierfür umfaßt die UML drei Grundelemente:

Das Ergebnis, die Modellierungssprache, soll u.a. folgenden Anforderungen genügen:

  1. Leicht benutzbare, ausdrucksstarke Modellierungssprache für den Austausch hochwertiger Modelle
  2. Ansatzpunkte für semantikkonforme Erweiterungen der Basiskonzepte (z.B. Stereotypen)
  3. Sprach- und Entwicklungsprozessunabhängigkeit
  4. Formale Basis zum Verständnis der Modellierungssprache
  5. Unterstützung des OO-Werkzeugmarktes
  6. Unterstützung für High-Level Entwicklungskonzepte wie Frameworks, Patterns und komponentenbasierte Entwicklung
  7. Integration der besten bestehenden Ansätze (sog. best practices)

Hierdurch wird deutlich, das die UML für ein weit breiteres Anwendungsspektrum der Systembeschreibung entwickelt und ausgelegt wurde, als sie im Rahmen dieser Vorlesung Anwendung finden wird.
Unser Hauptaugenmerk liegt auf der implementierungsnahen Verwendung zur Dokumentation des programmiersprachlichen Java-Codes.

3.2 Objektorientierte Konzepte in der Programmiersprache Java

Der Java-Quellcode setzt sich aus verschiedenen Elementen zusammen:

Aus diesen Elementen wird die Grammatik der Sprache Java definiert, welcher ein Quell-Programm genügen muß (d.h. es muß als syntaktisch korrekt anerkannt werden) um übersetzt und ausgeführt werden zu können.
Die Programmiersprache selbst wird -- wie in der Logik üblich -- als Objektsprache bezüglich einer Metasprache, welche die syntaktischen Gegebenheiten formal beschreibt, aufgefaßt. Die Metasprache selbst ist wieder eine Sprache. Als Notation wird eine Erweiterung der bekannten Backus-Naur-Notation (Abk. EBNF) verwendet.

Die Metasprache sei folgendermaßen definiert:

Metasymbol
Beispiel
Bedeutung
:=
x := y
ist definiert durch
(im Beispiel: y ist definiert durch x)
|
a|b
Selektion, Auswahl aus gleichwertigen Alternativen
(im Beispiel: a oder b)
[]
[x]
Optionalität
(im Beispiel:x kann null- oder einmal auftreten)
{}
{x}
Menge
(im Beispiel: x kann beliebig oft auftreten, auch null-mal)
"
"x"
Schlüsselwort
(im Beispiel: x muss im Source-Code wie angegeben (auch zwischen Groß- und Kleinschreibung wird unterschieden) auftreten)
()
(a|b)
Klammerung dient der Zusammenfassung von Teilausdrücken

Hinweis:
Die Zeichen [] sind sowohl Bestandteil der Meta- als auch der Objektsprache. Bei möglicherweise verwirrender Verwendung wird gesondert darauf hingewiesen.

3.2.1 Das Konzept der Klasse

Semantik:
Eine Klasse bildet die Beschreibung einer Menge von Objekten, die sich dieselben Attribute, Operationen, Methoden, Beziehungen und Semantik1 teilen.
Eine Klasse definiert die Datenstruktur eines Objekts, jedoch existieren auch Klassen von denen keine Objekte erzeugt werden können -- sog. abstrakte Klassen; sie dienen hauptsächlich als Strukturierungsmittel zur Komplexitätsreduktion im Zusammenhang mit Vererbung.
Synonym: Typ (nicht vollständig deckungsgleich, jedoch oft synonym verwendet)

Graphische Notation in UML:

Darstellung einer Klasse in UML

Darstellung einer Klasse in UML.
Der Klassenname wird im Fettdruck wiedergegeben.
Es hat sich eingebürgert Klassennamen in üblicher Schreibweise, d.h. beginnend mit einem Großbuchstaben, anzugeben.

Darstellung einer abstrakten Klasse in UML

Abstrakte Klassen werden analog der "normalen" dargestellt.
Der Klassenname wird im kursiven Fettdruck dargestellt. Alternativ kann auch die im vorhergehenden beschriebene Notation verwandt werden, jedoch ergänzt um die Bezeichnung abstract in geschweiften Klammern unterhalb des Klassennamens. Diese Variante eignet sich für handschriftliche Zeichnungen und in Umgebungen, die keine Kursivschreibung unterstützen.

Vollständige Darstellung einer Klasse in UML incl. Platz für Attribute und Operationen

Unterhalb des Rechteckes für den Klassnnamen können optional zwei weitere platziert werden. Hierbei dient das erste zur Aufnahme der Attribute und das zweite für die Spezifikation der Operationen.

Java-Syntax:
Die Klassendefinition wird durch das Schlüsselwort class eingeleitet. Daran anschließend wird der Klassenname angegeben; jede Klasse trägt einen systemweit eindeutigen Namen.
Der ClassModifier legt bestimmte Charakteristika der Klasse fest. Zunächst ob es sich um eine instanziierbare Klasse handelt, d.h. ob Objekte von ihr erzeugt werden können, oder andernfalls um eine abstrakte Klasse durch Angabe des entsprechenden Schlüsselwortes. public definiert eine Klasse als allgemein sichtbar, auch für andere Programmeinheiten. Die Festlegung final wird im Bezug auf Vererbung interessant, dort legt sie fest, das es sich bei der so deklarierten Klasse um das Ende einer Vererungshierarchie (sog. Blattknoten) handelt. Defaultmäßig gilt eine Klasse als public. Ebenfalls vererbungsbezogen ist die optionale Angabe der Superklasse.
Die Interfacedeklaration spezifiert welche Schnittstellen durch die Klasse implementiert werden.
ClassDeclaration := [ClassModifier] "class" ClassName [SuperDeclaration] [InterfaceDeclaration] Body
ClassModifier := ("abstract" | "final" | "public")
ClassName := Identifier
SuperDeclaration := "extends" TypeName
TypeName := Identifier
InterfaceDeclaration := "implements" InterfaceName {","InterfaceName}
InterfaceName := Identifier
Body := ... Anm.: Für den Body-Teil soll hier keine explizite Definition gegeben werden
Identifier := JavaIdentifierStart {JavaIdentifierPart}
JavaIdentifierStart := ("A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M"|"N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z"|
"a"|"b"|"c"|"d"|"e"|"f"|"g"|"h"|"i"|"j"|"k"|"l"|"m"|"n"|"o"|"p"|"q"|"r"|"s"|"t"|"u"|"v"|"w"|"x"|"y"|"z"|"_"|"$")
JavaIdentifierpart := (JavaIdentifierStart | Digit)
Digit := ("0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9"|"0")

Wichtig:
Java ist Case-Sensitiv, dies bedeutet Groß- und Kleinschreibung wird unterschieden. Dies kann insbesondere bei Identifiern zu Verwirrung durch vermeintliche Doppeldeutigkeit führen, weshalb eine durchgängige Benennungskonvention angeraten ist.

Beispiele:
public class TestClass1 ...
abstract class Person ...
class TestClass2 extends TestClass1 ...
final class TestClass3 implementes TestInterface ...

3.2.2 Das Konzept des Objekts

Semantik:
Ein Objekt ist die konkrete Ausprägung einer Klasse. Eine Klasse dient gleichsam wie eine Schablone zur Objekterzeugung. Für alle Attribute mit Minimalmultiplizitätät 1 enthält jedes Objekt dieser Klasse ständig einen Wert.
Synonyme: Instanz (Fehlübersetzung des engl. instance), Ausprägung

Graphische Darstellung in UML:

Verschiedene äquivalente Darstellung eines Objekts in UML

Objekte werden ähnlich den Klassen dargestellt. Sie sind ebenfalls durch beschriftete Rechtecke symbolisiert. Jedoch tragen sie statt des Klassennamens die unterstrichene Bezeichnung der konkreten Ausprägung. Zur besseren Unterscheidung kann ein Doppelpunkt vor den Objektnamen gesetzt werden. Diese Variante kürzt die Vollspezifikation, bestehend aus Objektname und Klassenname getrennt durch Doppelpunkt, ab. Auch hierbei werden alle Bestandteile unterstrichen dargestellt.

Darstellung eines Objekts mit der zugehörigen Wertbelegung und der erzeugenden Klasse in UML

Zur Visualisierung der tatsächlichen Wertebelegung der konkreten Ausprägung wird ebenfalls auf den aus der Klassendarstellung bekannten Mechanismus zurückgegriffen. Die gesetzten Werte sind nach einem Gleichheitszeichen direkt hinter dem betreffenden Attribut notiert.
Zwischen den Objekten und der erzeugenden Klasse kann eine gerichtete Kante (graphisch durch einen unterbrochenen Pfeil dargestellt) mit der Beschriftung instance of eingeschlossen in spitze Winkelklammern ("instance of" ) -- ein UML Stereotyp der die dargestellte Beziehung konkretisiert -- angetragen werden.

Java-Syntax:
Die Objekterzeugung geschieht dynamisch im Java-Programmablauf und besitzt im Allgemeinen folgende Gestalt:
DataType UserDefiniedName = "new" DataType"();"
UsedDefiniedName := Identifier

Hinweis:
Der DataType wurde bereits bei der Diskussion des Attributs definiert.
Auch hier gilt: sowohl das = als auch die runden Klammern sind Bestandteil der Objekt- nicht der Metasprache.

3.2.3 Das Konzept des Attributs

Semantik:
Ein Attribut ist ein benannter Speicherplatz (engl. slot) in einer Klasse, das eine Menge von gültigen Wertbelegungen innerhalb der Objekte beschreibt.
Die Mengen gültiger Wertbelegungen werden als Typ des Attributs bezeichnet und im Modell dargestellt.
Im Normallfall, d.h. sofern nicht anders festgelegt, kann der konkrete Wert eines Attributs sowohl von Objekt zu Objekt als auch über die Zeit variieren. Da niemals zwei Objekte bezüglich eines Attributs denselben Speicherplatz referenzieren wirken sich Wertänderungen innerhalb eines Attributs nicht auf andere Attribute des selben Typs aus. Ist dies explizit gewünscht, d.h. alle Objekt besitzen zwar ein eigenes Attribut, jedoch teilen sie sich den zugrundeliegenden Speicherplatz (sog. Klassenattribut) so muss dies explizit definiert werden.
Ebenso muss explizit spezifiert werden, dass ein Attribut nach der Initialisierung als unveränderliche Konstante behandelt werden soll.
Jedes Attribut kann zusätzlich über eine Sichtbarkeitsspezifikation zugriffsbeschränkt werden.
Abgeleitete Attribute (engl. derived Attributes) stellen strenggenommen nicht zwingend "echte" Attribute, hinsichtlich des zugeordneten Speicherplatzes, dar. Üblicherweise werden sie dynamisch aus vorhandenen Daten zur Laufzeit durch Methoden berechnet. Jedoch kann ihr aus anderer Information ableitbarer Informationsgehalt (z.B. wegen dessen Berechnungsaufwand) auch redundant gehalten werden. In diesem Falle ist für die notwendige Synchronisation von Ausgangsdaten und abgeleiteten Daten zusätzlicher Aufwand vorzusehen.
Synonym: Membervariable.

Graphische Darstellung in UML:

Darstellung von Attributen in UML

Jedes Attribut wird im zweiten Rechteck des Klassensymbols als gesonderte Zeile dargestellt. Jedes Attribut erhält einen für diese Klasse eindeutigen Namen, d.h. innerhalb einer Klasse existieren keine zwei Attribute mit gleichem Namen.
Jedes Attribut erhält zwingend einen Typ. Dieser wird nach dem Klassennamen -- abgetrennt durch einen Doppelpunkt -- spezifiziert.
Vor dem Attributnamen kann in eckigen Klammen die sog. Multiplizität eingetragen werden. Sie bezeichnet die Vielfachheit des Attributs. Konkret bedeutet dies, ein Attribut kann mehrere Ausprägungen eines Typs beinhalten (sog. mengenwertige Attribute). Die beiden Intervallgrenzen können beliebige positiv ganzzahlige Werte annehmen jedoch unter folgenden Einschränkungen:

Zusätzlich kann optional noch ein Vorgabewert spezifiert werden, mit dem das Attribut zum Objekterzeugungszeitpunkt automatisch durch das System belegt wird (die sog. initial value). Generell gilt: direkt nach Objekterzeugung sind Attribute zwar definiert (d.h. der notwendige Speicherplatz ist vorgesehen), aber noch mit einem Wert versehen (initialisiert). Ein Zugriff vor Wertsetzung resultiert in einem Fehler.
Die Sichtbarkeit (engl. visiblity), die symbolhaft vor dem Attributnamen notiert ist legt fest wie Programmeinheiten ausserhalb der aktuellen Klasse auf dieses Attribut zugreifen dürfen. Hierbei legt private (im Zeichen: -) fest dass nur Objekte dieser Klasse auf das Attribut zugreifen dürfen. Analog definiert public (im Zeichen: +) eine Zugriffsmöglichkeit für alle beliebigen Objekte. protected (#) hingegen bewirkt wirkt im Zusammenhang mit Vererbung ein Verhalten wie private in Bezug auf beliebige andere Objekte. Jedoch ist dieses Attribut in allen Subklassen der definierenden Klasse, sowie über typkorrekte Referenzen sicht- und zugreifbar. Ist nichts spezifiziert so gilt public als Vorgabewert.
Ein Attribut kann auch vom Typ Object sein, dies soll (zunächst) als die Möglichkeit definiert werden, das ein Attribut auch ein ganzes Objekt als Wert beinhalten kann.
Klassenattribute werden durch Unterstreichung ausgezeichnet. Konstante Attribute erhalten den Eigenschaftswertfrozen in geschweiften Klammern nachgestellt.
Aus Gründen der besseren Übersichtlichkeit ist es allgemein üblich, Attributenamen mit Kleinbuchstaben zu beginnen. Darüber hinaus werden boole'sche Attribute oftmals mit einem "is..." eingeleitet.
Abgeleitete Attribute werden durch einen einleitenden Schrägstrich (/) gekennzeichnet. Zusätzlich sollte die Berechnungsformel als Eigenschaftswert (in geschweiften Klammern) angegeben werden.

Java-Syntax:
Attribute werden im Body der Klassendeklaration angegeben.
Für Konstanten wird der FieldModifier auf final gesetzt, d.h. die zwingend ebenfalls spezifierte Vorgabewert wird bei Erzeugung gesetzt und ist danach während der gesamten Laufzeit nicht mehr veränderbar.
"Normale", d.h. üblicherweise verwandte Definitionen, verzichten ganz auf den ChangeableType somit wird transient als Vorgabewert gesetzt, und das Attribut als jederzeit veränderbar deklariert.
Die Angabe von volatile gewinnt erst im Zusammenspiel mehrerer gleichzeitig aktiver Programmteile (sog. Threads) an Bedeutung; hierbei legt er fest das auf eine Variable gleichzeitig (d.h. unsynchronisiert) von mehreren Threads schreibend zugegriffen werden kann.
Mit static wird der Compiler angewiesen allen Objekten die dieses Attribut besitzen denselben Speicherplatz für dieses Attribut zuzuweisen -- es handelt sich somit um ein Klassenattribut.
Abgeleitete Attribute können nur direkt in ein Java-Attribut umgesetzt werden, wenn eine bewußte Entscheidung für die redundante Speicherung getroffen wurde. Andernfalls werden sie üblicherweise als Methoden implementiert.

AttributeSpecification := [VisibilityModifier] [FieldModifier] DataType AttributeName [ArrayDeclaration] [Initialization]
VisibilityModifier := ("public" | "protected" | "private")
FieldModifier := [StaticType] [ChangeableType] [VolatileType]
StaticType := "static"
ChangeableType := (final | transient)
VolatileType := volatile
(static | final | transient | volatile)
ArrayDeclaration := []
AttributeName := Identifier
DataType := ("boolean" | "char" | "byte" | "short" | "int" | "long" | "float" | "double" | Object | ...)
Initialization := "=" Value
Value := ...Anm.: Für die Wertvorgabe soll hier keine explizite Definition gegeben werden, sie erfolgt gemäß der durch den DataType definierten Typeinschränkung
Als Datentyp sind ferner alle durch den Anwender definierten Klassen zulässig.

Hinweis:
Bei der ArrayDeclaration handelt es sich um die Zeichenkette [] nicht um eine leere optionale Angabe. [] sind hierbei Bestandteil der Objekt- nicht der Metasprache. Ebenso das = als Teil des Metasymbols :=.
Value ist ein beliebiger -- gemäß Typeinschränkung -- gültiger Vorgabewert.

Beispiel:
Folgendes Java-Äquivalent entspricht dem oben dargestellten UML-Klassendiagramm:


   public abstract class TestKlasse
   {
      boolean testAttrib1 = false;
      protected int testAtttrib2 =42;
      private char testAttrib3;
      short testArray[];
      final double pi = 3.14;
      static int noOfTestKlasse;
   }

Hinweis:
Entgegen der Reihenfolge in der UML-Darstellung wird in Java zuerst der Datentyp, und dann der Attributname wiedergegeben. Die Multiplizität eines Attributs wird über sog. Vektoren (auch engl.: Arrays) dargestellt.

Die Programmiersprache Java bietet folgende primiven Datentypen standardmäßig an:

Primitiver Typ
Größe in Bit
Wertebereich
Wrapper-Typ
boolean
1
-
Boolean
char
16
Unicode 0 - Unicode 216-1
Character
byte
8
(-128) - (+127)
Byte
short
16
(-215)-(+215-1)
Short
int
32
(-231)-(+231-1)
Integer
long
64
(-263)-(263-1)
Long
float
32
IEEE754
Float
double
64
IEEE754
Double
void
-
-
Void

Der explizite Nicht-Typ void steht naturgemäß nicht zur Definition von Attributen zur Verfügung. Ergänzend sei noch darauf hingewiesen, das Java keine vorzeichenlosen Typen unterstützt.
Die Wrapper-Typen kapseln die Primitivtypen in entsprechende Objekte, d.h. jede Ausprägung eines Wrapper-Typs ist ein Objekt; zur näheren Diskussion der Unterscheidung Objekte vs. Werte sei auf den Abschnitt über Objekte verwiesen.

Vertiefung: Implementierungsmöglichkeiten abgeleiteter Attribute

UML-Darstellung abgeleiteter Attribute

Das UML-Klassendiagramm zeigt eine Klasse mit einem abgeleiteten Attribut a2 dessen Information aus dem Attribut a derselben Klasse berechenbar ist. Die Berechnungsformel zur Gewinnung von a2 ist als Zusicherung in geschweiften Klammern angegeben. Zusaetzlich sind -- gemäß unserer Konvention -- je eine Get- und eine Set-Methode zum Setzen bzw. Auslesen des jeweiligen Attributwertes angegeben.
Zur Java-Umsetzung bieten sich folgende Alternativen an:
a) Umsetzung des abgeleiteten Attributs in ein "normales" Attribut. Zusätzlich muss die Berechnungsformel jedesmal beim ändernden Zugriff auf das unabhängige Attribut (in unserem Beispiel a) ausgeführt werden.
b) Keine Umsetzung des abgeleiteten Attributs, sondern lediglich Ausprogrammierung der angegebenen Zugriffsmethoden. Die Zugriffsmethoden implementieren hierzu die angegebene Formel im Falle des Auslesens, bzw. die Invertierung im Falle des schreibenden Zugriffs. Für den Nutzer dieser Klasse bleibt die öffentliche Schnittstelle (repräsentiert durch die public-Methoden) unverändert.

Umsetzung als "echtes" Attribut


   public class TestClass
   {
      private int a;
      private int a2;

      public int get_a()
      {
         return a;
      }//end get_a()
      public void set_a(int newValue)
      {
         a = newValue;
         a2 = 2*a;//update derived attribute
      }//end set_a()
      public int get_a2()
      {
         return a2;
      }//end get_a2()
      public void set_a2(int newValue)
      {
         a2 = newValue;
         a = a2/2;
      }//end set_a()
   }//end class TestClass

Zur Beachtung: In allen Methoden, die schreibend auf das abgeleitete Attribut, oder dessen Ausgangsattribute zugreifen muss zusaetzlicher Aufwand fuer die Konsistenzerhaltung vorgesehen werden. Konkret muss in allen Set-Methoden der beteiligten unabhaengigen Attribute die Berechnungsformel entsprechend der Vorgabe im UML-Klassendiagramm umgesetzt werden. Während in der schreibenden Methode des abhängigen Attributs die Invertierung der angegebenen Berechnungsformel implementiert werden muss (besser: müßte).
Hinweis: Es ist nicht immer möglich die Invertierung der Berechnungsformel zur Gewinnung des abgeleiteten Attributs eindeutig anzugeben. Beispiel: c=a2+b2 (mit a,b,c ganze Zahlen) so ist zwar c eindeutig aus a und b, jedoch nicht a und b aus Angabe eines c berechenbar (konkretes Zahlenbeispiel: c=8 mit a=b=2, jedoch auch: a=-2, b=2 oder a=2, b=-2 oder a=b=-2).

Im Allgemeinen ist es sinnvoller, und weniger fehlerträchtig, auf die Umwandlung in ein Attribut zu verzichten. Gemäß unserer Konvention sind alle Attribute für Dritte nicht direkt zugreifbar, d.h. sie sind mit dem Sichtbarkeitstyp private oder protected definiert. Alle Zugriffe auf die internen Attribute einer Klasse werden über spezielle Schreib- und Lese-Operationen abgewickelt.
Deshalb kann auf die Reservierung eines gesonderten Speicherplatzes für abgeleitete Attribute verzichtet werden, da ihre Information auch jederzeit aus der vorhandenen dynamisch bereitgestellt werden kann.

Umsetzung als reine Methodenimplementierung


   public class TestClass
   {
      private int a;

      public int get_a()
      {
return a; }//end get_a() public void set_a(int newValue) { a = newValue; }//end set_a() public int get_a2() { return 2*a;//calculate value of a2 dynamically }//end get_a2() public void set_a2(int newValue) { a2 = newValue; a = a2/2; }//end set_a() }//end class TestClass

Zur Beachtung: Auf eine Set-Methode für abgeleitete Attribute wie a2 sollte im Allgemeinen verzichtet werden, da die notwendige Umkehrung der Berechnungsformel nicht immer eindeutig existiert. Konkret tut sie dies dann und nur dann, wenn die Berechnungsformel eine bijektive Abbildung zwischen Eingangswerten und berechnetem Wert ist.

Als Ergebnis bleibt festzuhalten: Für den jeweiligen Anwendungskontext muss die Häufigkeit der lesenden genüber der schreibenden Zugriffe auf die abgeleiteten und unabhängigen Attribute betrachtet werden.
Werden die unabhängigen Eingangswerte des abgeleiteten Attributs häufig verändert, und das abgeleitete Attibut ist in ein "echtes" Attribut überführt, so resultieren hieraus auch rechenzeit-konsumierende Änderungen am abgeleiteten Attribut. Jedoch erfolgend lesende Zugriffe auf das abgeleitete Attribut ohne Zeitverlust. Diese Variante eignet sich besonders für häufiges Auslesen des abgeleiteten Attributs bei vorzugsweise seltener Wertänderung.
Wird das abgeleitete Attribut hingegen nur vergleichsweise selten ausgelesen, so kann sein Wert auch -- bei vernachlässigbarem Zeitverlust -- jedesmal neu berechnet werden. Dieses Verfahren zeichnet sich durch seine Speicherplatzökonomie aus. Zusätzlich muss keinerlei Aufwand für die Aktualisierung des abhängigen Attributs bei Wertänderung der Eingangswerte vorgesehen werden.

3.2.4 Das Konzept der (Objekt)-Identität

Identität ist eine Eigenschaft, die ein Objekt von allen anderen unterscheidet. Jedoch ist diese Eigenschaft nicht in Form eines Attributs kodifiziert.
Dieser Begriff wird häufig verwechselt mit der Adressierbarkeit, Gleichheit der Attribute oder einem eindeutigen Namen oder Schlüssel.
Solche Schlüssel sind jedoch nur bedingt geeignet, um ein Objekt zu identifizieren, denn die Identität eines Objektes hängt i.A. nicht von seinen Attributen ab. Namen können wechseln; verschiedene Objekte können (zumindest temporär) gleichnamig sein.
Um dem Problem der mangelnden Eignung von Attributwerten -- oder Kombinationen daraus -- zur eindeutigen Identifizierung grundsätzlich aus dem Weg zu gehen, werden künstliche Schlüssel (sog. Surrogat-Schlüssel) verwendet, die keinen inhaltlichen Bezug zu den Eigenschaften der Objekte haben und ihren Wert über die Zeit nicht verändern.
Typischerweise ist dieser Objektidentifikator (engl. object identifier (Abk. OID)) transparent durch das System generiert und für den Anwender weder sicht- noch zugreifbar.

Java-Syntax:
In Java wird die OID durch das System generiert und kontrolliert. Es besteht weder die Notwendigkeit noch die Möglichkeit eines Nutzereingriffs.

Beispiel:


   public class OIDTest
   {
      public static void main(String[] argv)
      {
         TestClass testInstance1 = new TestClass();
         TestClass testInstance2 = new TestClass();

         if (testInstance1 == testInstance2)
         {
            System.out.println("Instances are equal");
         }
         else
         {
            System.out.println("Instances are unequal");
         } //end if

         Integer FirstInteger = new Integer(5);
         Integer SecondInteger = new Integer(5);
         if (FirstInteger == SecondInteger)
         {
         System.out.println("Instances are equal");
         }
         else
         {
         System.out.println("Instances are unequal");
         }//end if

         System.out.println("compareTo returns: "+ FirstInteger.compareTo(SecondInteger) );
      } //end main()
   } //end class OIDTest

   class TestClass
   {
      int testAttrib = 42;
   } //end class TestClass
   

Resultat/Ausgabe:


   Instances are unequal
   Instances are unequal
   compareTo returns: 0
   equals returns: true

Die beiden Objekte vom Typ TestClass sind, trotz identischer Wertinitialisierung, ungleich.
Analoges gilt für die beiden Integer-Zahlen die aus dem Wrapper-Type um int erzeugt wurden. Obwohl es zunächst intuitiv verwunderlich ist, sind die beiden Objekte - obwohl mit gleichem internen Wert versehen - unterschiedlich. Die auf der Klasse Integer definierte Operation equals liefert jedoch, ebenso wie compareTo die erwartete Interpretation der Wertgleichheit

Objekte vs. Werte:

Objekt
Wert
besitzt Identität
keine Identität
Ausprägungen müssen explizit erzeugt werden
Ausprägungen durch System zur Verfügung gestellt
aufwendigeres Handling bei Primitivoperationen
einfacheres Handling bei Priimitivoperationen
referenzierbar
nicht referenzierbar
beliebig komplex
inhärent skalar

3.2.5 Die Konzepte Nachricht, Operation und Methode

Semantik: Die notwendige inter- und intra-Objekt Kommunikation findet zur Laufzeit mittels sog. Nachrichten (engl. messages) statt, die unter Objekten ausgetauscht werden.
Konkret wird dieses Denkmodell durch den Aufruf von Operationen auf dem jeweiligen Objekt realisiert. D.h. ein Objekt tritt in Interaktion mit einem anderen (aber auch sich selbst) über den Aufruf der Operation.
Eine Methode implementiert eine Operation und stellt dadurch das dynamische Verhalten der Klasse zur Verfügung. Operationen werden durch Angabe ihrer Signatur im unteren Rechteck der Klassendarstellung spezifiziert. Eine Signatur setzt sich aus dem Namen der Operation sowie deren Ein- und Ausgangsparametern (auch: Rückgabewerte) zusammen.
Analog der Attribute besitzt eine Operation eine Sichtbarkeit derselben Definition und Semantik. Ebenso kann eine Operation als Klassenoperation definiert werden. In Anlehnung an die Semantik des statischen Klassenattributs ist diese Operation auf der Klasse selbst -- ohne gleichzeitige Existenz eines zugehörigen Objekts ausführbar.
Synonym: Service.

Prinzipiell kann die Signatur einer Operation beliebig festgelegt werden, mit zwei Ausnahmen: dem Konstruktor und dem Destruktor.
Definition: Ein Konstruktor ist eine Operation, die automatisch zum Erzeugungszeitpunkt eines Objekts von dieser Klasse aufgerufen wird.
Definition: Ein Destruktur ist eine Operation, die automatisch zum Zerstörungszeitpunkt eines Objekts aufgerufen wird.
Anmerkung: Existiert kein expliziter Konstruktor bzw. Destruktor, so wird ein Defaultkonstruktor bzw. -destruktor aufgerufen der im wesentlichen die (erwarteten) Aufgaben (d.h, Speicherplatzzuweisung bei Objekterzeugung, bzw. dessen Freigabe bei Zerstörung) übernimmt.

Graphische Darstellung in UML:

Darstellung von Operationen in UML

Operationen werden im unteren Rechteck der Klassendefinition dargestellt. Für die Symbolisierung der Sichtbarkeit gelten dieselben syntaktischen Vorschriften wie bei Attributen. Im Anschluß an das Sichtbarkeits-Symbol wird die Operation eindeutig benannt. Eingabeparameter werden eingeleitet durch ihren Datentyp in Klammern dargestellt. Der optionale Rückgabewert wird nach einem Doppelpunkt lediglich durch seinen Typ spezifiziert. Aufgrund der Limitierung der Rückgabeparameter auf genau eins ist es nicht möglich mehrere Rückgaben nach dem Doppelpunkt zu spezifizieren. Für diesen Zweck können die Eingangsparameter entsprechend modifiziert werden (dies spiegelt auch das übliche Verfahren bei der programmiersprachlichen Umsetzung wieder). Jedem Eingabeparameter kann wahlweise ein in, out oder die Mischform inout vorangestellt werden; abhängig davon ob er Parameter in der Operation nur gelesen, nur geschrieben oder auf Basis der Eingabe verändert zurückgegeben wird.
Konstruktoren und Destruktoren werden graphisch wie "normale" Operationen behandelt.
Klassenoperationen werden analog der Klassenattribute durch Unterstreichung kenntlich gemacht.

Java-Syntax:
Der Konstruktor ist zwingend eine Operation vom Namen der Klasse; d.h. Signatur: Klassenname(...) ohne(!) (expliziten) Rückgabewert. Ein expliziter Konstruktoraufruf auf einem erzeugten Objekt ist nicht möglich.
Hinweis: Konstruktoren haben keinen expliziten Rückgabewert (auch nicht void!) sondern geben implizit per definitionem das erzeugte Objekt zurück.
Konstruktoren sollten -- sofern die Objekterzeugung erlaubt ist -- vom Sichtbarkeitstyp public oder protected sein. Im Falle von private könnte niemals ein Objekt erzeugt werden, ausser durch ein bereits existierendes Objekt (Widerspruch!) -- deshalb lehnt der Java-Compiler die Übersetzung bei privaten Konstruktoren ab. Ist eine Klasse Kindklasse, so wird vor der Ausführung des eigentlichen Kindklassenkonstruktors der Konstruktur der Elternklasse ausgeführt. Allgemein gilt folgende Ausführungsreihenfolge bei der Objekterzeugung:
1. Aufruf des Konstruktors der Elternklasse
2. Initialisierung der Attribute mittels Defaultwerten
3. Ausführung des Konstruktors

Der Destruktur bildet das Analogon zum Konstruktor am Ende des Objektlebenszyklus. Wird ein Objekt zerstört, d.h. der zugeordnete Speicherplatz freigegeben, kann eine eigens definierte Methode ausgeführt werden.
Ebenso wie der kaskadierte Konstruktoraufruf wird auch der Destrukturaufruf zuerst auf etwaigen Elternklassen, und anschließend auf der zu zerstörenden Klasse selbst ausgeführt. Die (zwingende) Syntax eines Destrukturs lautet: public void finalize(). Üblicherweise werden Destruktoren durch mindestens folgende Methode implementiert: super.finalize(). Hierdurch wird sichergestellt, dass nicht nur das aktuelle Objekt, sondern auch die Elternklasseninstanzen ordnungsgemäß gelöscht werden.

Java erlaubt ferner das Überschreiben einer Methode in Subklassen explizit durch Angabe des Schlüsselwortes final vor der Operationsdeklaration zu verbieten. Analoges gilt für die Angabe des selben Schlüsselwortes innerhalb der Klassendeklaration für die ganze Klasse.
OperationDeclaration := ["final"] ReturnValue OperationName FormalParameterList
OperationName := Identifier
FormalParameterList := ( [Parameter]{","Parameter} )
Parameter := DataType ParameterName
ParameterName := Identifier

Hinweis:
Die öffnende und schließende runde Klammer im Ausdruck FormalParameters ist als Teil der Java-Syntax Bestandteil der Objektsprache und nicht der Metasprache.


Beispiel (Umsetzung des UML-Klassendiagramms in Java):


   class TestClass {
      public boolean get_testAttrib1();
      public boolean set_testAttrib1(boolean Value2Set);
      public static float get_pi();
      private someInternalStruff(int MyInt, char MyChar);
      CalcX(int j, int j, double x);
      }
   


Beispiel: (KonstruktorTest.java)


   public class KonstructorTest
   {
      public static void main(String[] argv)
      {
         System.out.println("create instance of TestClass1");
         TestClass1 myTestClass1Instance = new TestClass1();
         System.out.println("create instance of TestClass2");
         TestClass2 myTestClass2Instance = new TestClass2();
         System.out.println("create instance of TestClass3");
         TestClass3 myTestClass3Instance = new TestClass3();

         myTestClass1Instance = null; //destroy object
         System.out.println("calling garbage collector manually");
         System.gc();
      } //end main()
   } //end class KonstructorTest

   class TestClass1
   {
      public TestClass1()
      {
         System.out.println("TestClass1's constructor called");
      } //end TestClass1()

      public void finalize()
      {
         System.out.println("TestClass1's destructor called");
      } //end finalize()
   } //end class TestClass1

   class TestClass2
   {
      protected TestClass2()
      {
         System.out.println("TestClass2's constructor called");
      } //end TestClass2()
   } //end class TestClass2

   class TestClass3 extends TestClass1
   {
      public TestClass3()
      {
         System.out.println("TestClass3's constructor called");
      } //end TestClass3()
   } //end class TestClass3()
   

Ausgabe des Programms:


create instance of TestClass1
TestClass1's constructor called
create instance of TestClass2
TestClass2's constructor called
create instance of TestClass3
TestClass1's constructor called
TestClass3's constructor called
garbage collector manually invoked
TestClass1's destructor called
   

Anmerkung: Der manuelle Aufruf der garbage collectors ist in diesem Beispiel zwingend erforderlich, wir werden die Thematik noch ausführlicher bei der Betrachtung der Speicherverwaltung behandeln.

Interaktion zwischen Objekten:

Graphische Darstellung in UML:
Der dynamische Botschaftenaustausch zwischen Laufzeitobjekten kann durch den UML-Diagrammtyp der Sequenzdiagramme (engl. sequence diagram) abgebildet werden.
Objekte werden durch unterbrochene senkrechte Linien dargestellt. Darüber wird das Symbol der interagierenden Komponente (typischerweise ein Objekt) angetragen.
Botschaften werden als horizontale Pfeile zwischen den Objekt-Linien dargestellt. Auf ihnen wird die Nachricht und ihre evtl. erfolgte Antwort, in der Form nachricht(argument) notiert.
Die Überlagerung der gestrichelten Lebenslinien durch breite, nichtausgefüllte senkrechte Balken, symbolisiert den Steuerfokus. Er gibt an, welche Objekte zum betrachteten Zeitpunkt aktiv sind.

UML Sequenzdiagramm

Call by Reference vs. Call by Value -- Referenztypen in Java

Die Übergabe der Paramterwerte in Methoden erfolgt in Java durch Wertübergabe (engl. call by value). Das heißt, dass Werte von Parametervariablen in einer Methode Kopien der vom Aufrufer angegebenen Werte sind.
Daraus folgt: eine Methode kann die übergebenen Werte intern zwar lokal verändern, diese Änderung wird jedoch nicht an die wertliefernde (aufrufende) Umgebung propagiert.
Um ändernden Zugriff aus der Methode in die aufrufende Umgebung zu erhalten muss der Paramet als Referenztyp übergeben werden.

Beispiel:


   class calculator
   {
      void addInt(int sum1, int sum2, int result)
      {
         result = sum1+sum2;
      } //end addInt()
   } //end class calculator

Ein Aufruf der Operation könnte wie folgt erfolgen:


   int result = 0; //declaration and initialization
   result = addInt(5,5, result); //calls the operation
   

Welchen Wert nimmt result an?
Gemäß dem vorher dargestellten wird als Ergebnis nicht -- wie gewünscht -- der Übergabeparameter result mit dem Additionsresultat überschrieben, sondern der Initialwert 0 beibehalten.
Das Ergebnis wird zwar operationsintern Berechnet, aber nach Austritt aus der Methode wieder verworfen.

Um übergebene Parameter in der Methode verändern zu können, und sie verändert zurückzugeben muss der formale Parameter als Referenz übergeben werden -- diese Mimik wird entsprechend als call by reference bezeichnet.
Beispiel:


   public class CallByRef
   {
      public static void main(String[] arg)
      {
         UserType myType = new UserType();
         myType.setContent(1);
         System.out.println("before: "+myType.getContent());
         Calculator myCalculator = new Calculator();
         myCalculator.makeDouble(myType);
         System.out.println("after: "+myType.getContent());
      } //end main()
   }//end class CallByRef

   class Calculator
   {
      public void makeDouble(UserType inValue)
      {
         inValue.setContent(inValue.getContent() * 2);
      } //end makeDouble()
   } //end class Calculator

   class UserType
   {
      private int content;

      public void setContent(int value2set)
      {
         content = value2set;
      } //end setContent()
      public int getContent()
      {
         return content;
      } //end getContent
   } //end class UserType

Ausgabe des Programms:
before: 1
after: 2

3.2.6 Paketierung

Semantik:
Die Paktierung -- das Zusammenfassen von Klassen zu Paketen (engl. package) ist streng genommen kein Element des objektorientierten Paradigmas, sondern eine Weiterentwicklung der verschiedenen im Bereich der klassischen strukturierten Programmiersprachen existierenden Konzepte zur Modularisierung.
Nach unserer Definition ist ein Paket eine Sammlung von Klassen. Durch die Bildung höher aggregierter Einheiten wird ein Gesamtmodell in kleinere, überschaubarere Einheiten zergliedert.
Einen weiteren wichtigen Gesichtspunkt bilden die unwillkürlich in größeren Projekten auftretenden Namensgleichheiten einzelner Klassen und die daraus resultierenden Konflikte. Jedes Paket bildet einen eigenen Namensraum, d.h. eine Einheit abgeschlossener Benennungen. Technisch gesehen wird zur eindeutigen Identifikation - und Ermittlung etwaiger Namenskonflikte - neben dem Klassennamen auch die Benennung des umgebenden (d.h. beinhaltenden) Paketes (und selbstverständlich auch die Namen der dieses Paket umgebenden Pakete ...) herangezogen. Die hieraus entstehenden Pfadausdrücke charakterisieren eine beliebige Klasse vollständig durch Explizierung ihrer Lokalisation in der Pakethierarchie. Z.B. Eine Klasse KlasseA die in einem Paket Paket1 definiert ist das wiederum in einem Paket Paket2 enthalten ist, wird als Paket2.Paket1.KlasseA bezeichnet.
Jede Klasse ist in maximal einem Paket definiert (das Heimatpaket), kann aber aus beliebigen Paketen referenziert werden. Ist eine Klasse in keinem Paket enthalten (d.h. enthält die zugehörige Quellcodedatei keine Paketdeklaration) so wird sie automatisch einem unbenannten Paket zugeordnet.
Systemseitig wird die Pakethierarchie durch eine entsprechende Dateisystemskataloghierarchie verwirklicht.
Um im Internet eindeutige Paketnamen zu erhalten hat es sich eingebürgert (ohne zwingenden syntaktischen Hintergrund) ähnlich den URLs vorzugehen, jedoch mit dem Unterschied diese von rechts nach links zu interpretieren. So würde ein Paket, das in der FH Augsburg erstellt worden ist beispielsweise folgendermassen identifiziert: de.fh-augsburg.informatik.vorlesung.java.mm1.testPackage.

Graphische Darstellung in UML:

Pakete und ihre Beziehungen in UML

Packages werden in der UML als stilisierte Hängeordner dargestellt.
Der angedeutete Reiter am oberen Rand nimmt den eindeutigen Namen des Paketes auf.
Zwischen Packages können folgende vordefinierte Abhängigkeitsbeziehungen erklä fwerden. Neben der Referenzbeziehung (UML-Stereotype access) existiert auch noch die Importbeziehung (import, welche das unabhängige Paket (=Spitze der gerichteten Abhängigkeitsbeziehung) vollständig in das abhängige (=Ausgang der Abhängigkeitsbeziehung) aufnimmt.

In einem Paket enthaltene Klassen und weitere Pakete können innerhalb des unteren Rechtecks dargestellt werden.

Java-Syntax:
Pro Java-Quellcodedatei kann maximal ein Paket definiert werden. Ein Paket kann Klassen und Schnittstellendeklarationen enthalten. Die Paketdefinition muß zwingend in der ersten Codezeile (vorausgehende Kommentarzeilen werden ggf. ignoriert) der Quelltextdatei stehen; d.h. vor jeglicher Klassen oder Schnittstellendefinition. Die Paketdefinition gilt für alle nachfolgenden Java-Deklarationen; d.h. alle nach einer Packagedeklaration angeschriebenen Klassen und Schnittstellen gehören dem spezifizierten Paket an.
Der Import von Klassen aus einem Paket geschieht mit dem import Schlüsselwort, auf das der vollständige Name der Klasse, bzw. ein Stern (*) für alle Klassen dieses Pakets, folgt.

Sollen in einem Package andere Pakete importiert werden, so wird zunächst das Paket deklariert, dann die gewünschten Importe vorgenommen.
In einem Paket deklarierte Elemente sind ausserhalb dieses Pakets nur sicht- und zugreifbar, wenn sie mit der Sichtbarkeitseinschränkung public gekennzeichnet sind.

PackageStatement := [PackageDeclaration] [PackageImport] PackageDeclaration := "package" PackageName
PackageName := Identifier {"."Identifier}

PackageImport := "import" PackageReference
PackageReference := Identifier {"."IdentifierOrJoker}
IdentifierOrJoker := Identifier | "*"

Alle nach einem Package-Statement folgenden Definitionen werden als diesem Package zugehörig interpretiert; d.h. ein Package kann zwar explizit geöffnet, aber nicht geschlossen, werden!
Standardmässig import das Java-Laufzeitsystem drei Pakete: das leere Defaultpackage, das Paket java.lang (deshalb sind auch die darin definierten Wrapper-Typen und einfachen komplexen Typen (wie String) ohne expliziten Import ansprechbar), sowie das Paket dem die Hauptklasse zugeordnet ist (dieses ist evtl. auch namenlos).
Hinweis: Die einfache Textausgabemethode System.out.println ist nicht in einem Paket definiert, sondern System ist eine Klasse, in der out eine Klassenvariable ist, deren Typ die Methode println unterstützt.

Werden die Klassen eines Pakets in verschiedene Quellcodedateien aufgeteilt, so müssen alle Sourcecodedateien des Pakets gemeinsam übersetzt werden. Dies geschieht durch die Übergabe mehrerer .java Dateien an den Java-Compiler. Beispiel: javac klasse1.java klasse2.java.
Zur Erinnerung: Sollen mehrere Klassen eines Pakets von außen zugreifbar sein, so ist die Aufteilung in mehrere Quellcodedateien unvermeidlich, da Java je Datei nur genau eine als public deklarierte Klasse zulässt!

Beispiel:
Definition eines Paketes P1 innerhalb eines Paketes P2 package P2.P1;

Import der Klasse C1 aus dem Paket P1 das in Paket P2 enthalten ist:
import P2.P1.C1;
Import aller Klassen des Paketes P3:
import P3.*;

3.2.7 Das Konzept des Polymorphismus

Semantik:
Unter Polymorphismus (grch.: Vielgestaltigkeit) wird (für uns) die Möglichkeit verstanden gleichname Elemente (=Objekte, Klassen, Variablen, Operationen, Attribute) in verschiedenen Gültigkeitesbereichen definieren zu können.
Unter Gültigkeitsbereich wollen wir (zunächst) die direkt umfassende Programmeinheit verstehen.
Beispielsweise ist für ein Attribut die direkt umfassende Einheit die enthaltende Klasse, ebenso für die auf dieser Klasse definierten Operationen. Für die Klassen selbst bildet das Paket in dem die Klasse definiert (nicht importiert!) ist die umgebende Einheit. Innerhalb einer Pakethierchie können weitere Pakete definiert sein, hierbei bildet jedes Paket einen eigenen Gültigkeitsbereich.

Die Umsetzung des Polymorphiegedankens in der Programmiersprache Java sorgt für die Möglichkeit gleichname Elemente in verschiedenen Gültigkeitsbereichen definieren und unterscheiden zu können.
Hinweis: Im Falle der Operationen gilt -- wie bekannt -- nicht die Bennennung der Operation allein als identifizierendes Merkmal, sondern die gesammte Signatur.

3.2.8 Das Konzept der Vererbung

Semantik:
Vererbung (engl. Inheritance ist ein Konzept, das es erlaubt auf der Basis vorhanderer Information weitere zu spezifizieren. Zumeist wird zwischen einer Oberklasse (auch: Obertyp, Supertyp, Superklasse, Elternklasse) und einer Unterklasse (analog auch: Untertyp, Subtyp, Subklasse, Kindklasse) eine Beziehung definiert. Diese Vererbungsbeziehung genannte Relation legt die Weitergabe der Superklassenstruktur (Attribute und Operationen) an die Subklasse fest.
Diese Mimik der Strukturpropagierung wird als Vererbung bezeichnet. Die Subklasse erbt von der Superklasse.
Erbt eine Klasse von mehreren Klassen (d.h. sie besitzt mehrere Superklassen) ist dies mit dem Begriff Mehrfachvererbung (engl. multiple Inheritance) belegt. Andernfalls (genau eine Superklasse) wird der Begriff einfache Vererbung gebraucht.
Jedes Attribut ist ebenso wie jede Operation der Superklasse automatisch durch die Vererbungsbeziehung in der Subklasse definiert. Dies bedeutet die in der Superklasse implementierte Methode kann polymorph auch als dynamisches Verhalten der Subklassen auftreten.
Wird in einer Subklasse explizit ein Attribut gleichen Namens oder eine Operation gleicher Signatur spezifiziert, so wird die ererbte Definition überschrieben (engl. overriding).

Graphische Darstellung in UML:

Verschiedene Vererbungsbeziehungen und Beispiele dazu

In UML wird die Vererbungsbeziehung zwischen Klassen als gerichtete, mit einem hohlen Pfeil versehene, Kante ausgehend von der erbenden Klasse (der Subklasse) hin zur vererbenden Klasse (der Superklasse) dargestellt.
Optional kann noch der Grund der Vererbung -- der sog. Diskriminator als Eigenschaftswert (in geschweiften Klammern) an die Kante angetragen werden. Diese Mimik ist rechts in der Abbildung dargestellt (die abstrakte Klasse Person vererbt an die gemäß Geschlecht gebildeten Subklassen Mann und Frau.
Per Konvention werden in UML nur direkt in der Klasse definierte Strukturkomponenten, d.h. keine ererbten -- es sei denn sie sind überschrieben --, dargestellt.
Das Klassenmodell unten rechts illustriert eine typische Situation in der Mehrfachvererbung auftritt.

Probleme bei Mehrfachvererbung:
Deadly Diamond of Death in UML dargestellt
Die Graphik illustriert eine typische Problemsituation die bei Mehrfachvererbung (früher oder später) unweigerlich auftritt: den sog. deadly diamond of death (auch: diamond inheritance). Das Phänomän ist dadurch gekennzeichnet, das die Quelle ererbter Charakteristika (d.h. die Attribut- oder Operationsvererbende Superklasse) nicht eindeutig identifiziert werden kann. Im Beispiel würde die Klasse C4 neben dem dort definierten Attribut a4 auch das von C2 ererbte Attribut a2 und das von C3 geerbte a3 enthalten.
C2 und C3 ihrerseits erben von C1 das Attribut a1. Hieraus resultiert das geschilderte Problem. Eigentlich sollte C4 als Subklasse sowohl von C2 als auch C3 die dort definiert und dorthin vererbten Attribute erben. Dies führt zu einer Doppeldefinition von a1 das sowohl über den Vererbungspfad C1->C2->C4 als auch C1->C3->C4 ererbt wird.
Hierzu müßte das objektorientierte Paradigma jedoch mehrere namens- und typgleiche Attribute (identische Einträge im UML-Attributbereich der UML Klassendarstellung) zulassen -- was es jedoch nicht tut!

Aufgrund dieser Problematik unterstützt die Programmiersprache Java keine Mehrfachvererbung!

Java-Syntax:
Wird eine Operation in einer erbenden Klasse überschrieben, wird nicht mehr standardmäßig auf die signaturgleiche Operation der Elternklasse zugegriffen, ist dies Verhalten jedoch explizit gewünscht, so kann dies mit dem Schlüsselwort super erzwungen werden. Beispiel: super.x() ruft in einer Operation x die gleichnnamige der Superklasse auf. Im Falle des Aufrufs des Superklassenkonstruktor vor dem der aktuellen Klasse muss dieser Aufruf zwingend als erste Anweisung in einer überschriebenen Methode angegeben werden

Deklarationssyntax: Das Konzept der Klasse.
Die dort dargestellte optionale SuperDeclaration erlaubt die Angabe maximal einer Elternklasse zu jeder anwenderdefinierten Klasse.
Das Rumpfstatement stellt sich wie folgt dar: class Klassenname extends Elternklassenname

In Java ist jede Klasse die über keine explizite Elternklasse verfügt implizt von der Klasse java.lang.Object abgeleitet. Dies stellt sicher, dass jedes Anwendungsobjekt nach java.lang.Object umgewandelt werden kann.
Ist eine explizite Elternklasse gegeben, so überschreibt diese die Vorgabevererbung.
Der Java-Compiler setzt diese Mimik während der Bytecodeerzeugung um. Dies bedeutet: nicht das Laufzeitsystem nimmt als Elternklasse aller nicht abgeleiteten Klassen java.lang.Object an, sondern der Compiler verändert den übergebenen Source-Code vor der Übersetzung entsprechend. Hierdurch wird der Implementierungsaufwand der Virtuellen Maschine verringert, da Teile des Standardverhaltens in die explizite Sprachebene verlagert werden.

Beispiel:
Leicht nachzuprüfen ist die Vorgabevererbung für Klassen ohne explizit spezifizierte Elternklasse mit dem (aus Kapitel I bekannten) Java-Diassembler javap.
Aufruf: javap HelloWorld
Der Diassembler liefert für das HelloWorld-Programm folgende Ausgabe:


   Compiled from HelloWorld.java
   public class HelloWorld extends java.lang.Object {
   public HelloWorld();
   public static void main(java.lang.String[]);
   }
   

Anmerkung: Die Spezifikation der Übergabeparameter zeigt die Deklaration des String als Element des Pakets java.lang welches standardmässig importiert wird. Darüberhinaus aus der Definition auch ersichtlich, dass der gewählte Name des Übergabeparametes (in unseren Beispielen zumeist args oder argv keine Rolle spielt. Zur Signaturbildung wird lediglich der Operationsname und die Typen der Übergabe- und Rückgabeparameter herangezogen.

Neben der impliziten Typumwandlung durch das Laufzeitsystem bei Übergabe einer Subtypinstanz an eine Programmeinheit (Operation oder Attribut) die eine Supertypinstanz erwartet kann die erforderliche Umwandlung auch durch den Programmmierer erfolgen.
Hierfür stellt Java ein explizites Verfahren zur Verfügung.

Generell gibt zu beachten, dass nur Typen, die in einer Vererbungshierarchie miteinander verbunden sind (unabhängig ob eine Sub-Super- oder Super-Sub-Beziehung) vorliegt ineinander konvertiert werden können.
Als sichere Umwandlung gilt die, gemäß LSP immer mögliche, Typerweiterung (engl. widening oder casting up). Hierbei wird ein Objekt einer Subklasse anstatt einem der Superklasse plaziert; diese Umwandlung ist immer zulässig und bedarf keiner näheren Prüfung.
Problematischer verhält es sich hingegen im umgekehrten Falle; dem Versuch eine Superklassen-Ausprägung in eine spezialisiertere Subklassen-Ausprägung zu überführen. Hierbei fehlen u. U. wichtige Informationen (konkret: die Erweiterung der Subklasse gegenüber der Superklasse).

Java-Syntax:
Die Anweisung:
"("DestinationClass")" SourceObject
Wandelt ein Objekt (lies: ein Objekt beliebigen Typs), sofern gemäß der Typeinschränkung möglich, in eine Ausprägung der DestinationClass um. Im Falle der Unmöglichkeit wird eine ClassCastException ausgelöst.
Ist eine Umwandlung generell nicht möglich (siehe folgendes Beispiel) so verweigert der Java-Compiler bereits die Übersetzung.

Beispiel:
Die Übersetzung des folgenden Quellcodes wird augrund der offensichtlichen Unmöglichkeit der Typkonversion durch den Compiler abgelehnt:


   public class CastTest
   {
      public static void main(String[] argv)
      {
         Person hans = new Person();
         Building house;

         house = hans; 

      } //end main()
   } //end class CastTest

   class Person
   {
   }//end class Person

   class Building
   {
   } //end class Building
   

Vor der Typumwandlung sollte stets die Klassenzugehörigkeit eines Objekts (bzw. einer Referenz auf ein Objekt) ermittelt werden,

Zur Ermittlung der Klassenzugehörigkeit eines Objekts bietet die Programmiersprache Java den instanceOf-Operator an.

Java-Syntax:
object "instanceof" class
Wobei object der Namen eines definierten Objekts, und class der Namen einer existierenden Klasse ist.
Die Operation liefert als Result den Wahrheitswert true wenn zu prüfende Objekt der bezeichneten Klasse, oder einer der Superklassen angehört.
Implizit wird somit die Möglichkeit einer Typumwandlung geprüft. Dies ist auch die häufigste Anwendung des InstanceOf-Operators.

Beispiel: (Quellcodedatei: InstanceOfTest.java)


   public class InstanceOfTest
   {
      public static void main(String[] argv)
      {
         Person aPerson;
         Man hans;
         hans = new Man();

         aPerson = hans;

         System.out.print("hans...");
         checkManhood(hans);

         System.out.print("hans assigned to a person variable...");
         checkManhood(aPerson);

         aPerson = (Person) hans;
         System.out.print("hans converted into a person and afterwards assigned to a person variable...");
         checkManhood(aPerson);
      } //end main()

      public static void checkManhood(Person anyone)
      {
         if (anyone instanceof Man)
         {
            System.out.println("is a man!");
         } //if
         else
         {
            System.out.println("isn't a man!");
         } //else
      } //end checkManhood
   } //end class InstanceOfTest

   abstract class Person
   {
   } //end class Person

   class Man extends Person
   {
   } //end class Man

   class Woman  extends Person
   {
   } //end class Woman

Beispiel: (InheritanceTest.java)


   public class InheritanceTest
   {
      public static void main(String[] argv)
      {
         ClassA mySuperInstance = new ClassA();ClassB mySubInstance = new ClassB();

         mySuperInstance.sayHello();
         mySubInstance.sayHello();
      } //end main()
   } //end class InheritanceTest

   class ClassA
   {
      public void sayHello()
      {
         System.out.println("Hello from ClassA");
      } //end sayHello()
   } //end class ClassA

   class ClassB extends ClassA
   {
      //nothing!
   }
   

Resultat/Ausgabe:
Der Aufruf der ererbten Operation sayHello führt transparent die in der (vererbenden) Elternkasse implementierte Methode gleichen Namens aus.

Vertiefung: Das Liskov'sche Substitutionsprinzip (Abk. LSP)

Zwar bietet Java, wie auch andere verfügbare objektorientierte Programmiersprachen, die Möglichkeit der Vererbung, jedoch ist seitens der Programmiersprache keine weitergehende Einschränkung zu deren Einsatz definiert. Konkret bedeutet dies: auch die unsaubere Vererbung mit dem Ziel der -- bequemen -- Schreibaufwandsreduktion ist technisch möglich.
Gewünscht ist jedoch eine griffige Formulierung zur (richtigen) Bildung von Subtypen; diese liefert das von Barbara Liskov 1988[2] postulierte Substitutionsprinzip:

Definition (nach Liskov): Wenn für jedes Objekt o1 eines Typs S ein Objekt o2 eines Typs T existiert, so dass für alle Programme P das Verhalten von P nach Substitution von o1 durch o2 unverändert ist, dann ist S ein Untertyp von T.
Alternativ (und einfacher): Dann (und nur dann) ist eine Klasse eine (sinnvolle) Subklasse einer anderen, wenn an jeder Stelle im Programm, an der eine Ausprägung der Superklasse erwartet wird, ein Objekt der Subklasse stehen kann (siehe Beispiel).

Beispiel (Dateiname: LSPTest.java)


   package Liskov1;
   public class LSPTest
   {
      public static void main (String[] argv)
      {
         Woman marion;
         Person aPerson;

         System.out.println("creating woman instance");
         marion = new Woman();

         System.out.println("creating a woman and assigning it to an attribute of type person");
         aPerson = new Woman();

         marion.printAge(); //inherited method called!
         LSPTest programInstance = new LSPTest();
         programInstance.printPersonsAge(marion); //type Person is explected, but Woman is delivered!
      } //end main()

      public void printPersonsAge(Person agedPerson)
      {
         agedPerson.printAge();
      } //end printPersonsAge()
   } //end class LSPTest

   abstract class Person
   {
      protected int age;

      Person()
      {
         System.out.println("Person created!");
      } //end constructor Person

      public void printAge()
      {
         System.out.println(age);
      } //end printAge()
   } //end class Person

   class Woman extends Person
   {
      int noChildren;

      Woman()
      {
         System.out.println("Woman created!");
      } //end constructor Woman
   } //end class Woman
   

Ausgabe des Programms:
creating woman instance
Person created!
Woman created!
creating a woman and assigning it to an attribute of type person
Person created!
Woman created!
0
0

Zunächst wird transparent, ohne Zutun des Programmierers, durch das Laufzeitsystem während der (konkret: davor) Erzeugung eines Objekts der Klasse Woman ein Objekt der Elternklasse Person erzeugt (vgl. Ausgaben der beiden Konstruktoren).
Analog verläuft die Erzeugung eines weiteren Woman-Objekts, das jedoch einem Attribut vom Typ Person zugewiesen wird. In Verwirklichung des LSP wird auch hier die spezialisierte Ausprägung der von Person abgeleiteten Klasse akzeptiert.
Anschließend wird in der Mainmethode die von Person auf Woman vererbte Operation printAge aufgerufen. Als Resultat wird die erste Null ausgegeben.
Die Verwirklichung des Liskov'schen Substitutionsprinzips in Java (d.h. die Unterstellung seiner Semantik bei der Typbildung) wird an den folgenden Codezeilen deutlich: Die Operation printPersonsAge, welche auf der Hauptklasse LSPTest definiert ist. Sie erwartet als Übergabeparameter (agedPerson genannt) ein Objekt der Klasse (Synonym: vom Typ) Person. Jedoch wird der Operation im Beispielprogramm ein Objekt der spezialisierten Subklasse Woman übergeben. Dennoch wird dieses Objekt als (gemäß der Liskov'schen Typeinschränkung) gültig angesehen.

3.2.9 Das Konzept der Beziehung

Semantik:
Eine Beziehung beschreibt als Relation zwischen Klassen die gemeinsame Semantik und Struktur einer Menge von Objektverbindungen.
Synonym: Assoziation.

Graphische Darstellung in UML:

Darstellung einer bidirektionalen Assoziation mit UML

Die Abbildung zeigt die Visualisierung einer Beziehung zwischen Klassen. Auch bisher haben wir (implizit) mit Beziehungen operiert. So besitzt jedes Attribut eine Beziehung zur beinhaltenden Klasse und umgekehrt jede Klasse zu den gekapselten Attributen.
Neu ist die in der Graphik dargestellte Beziehung auf Klassenebene, die konkret zur Laufzeit zwischen zwei Objekten der beteiligten Klassen (oder deren Subklassen) eingegengen wird.
Strukturell besteht eine Beziehung aus folgenden Komponenten:

Für die Multiplizitäten gelten dieselben syntaktischen Regeln wie bei der bekannten Anwendung auf Attributebene.

Für unsere weitere Anwendung spielt jedoch dieser ungerichtete bidirektionale Beziehungstyp keine grosse Rolle. Vielmehr werden wir uns auf die programmiersprachliche Umsetzung gerichteter unidirektionaler Beziehungen spezialiseren.
Die Darstellung in UML erfolgt leicht modifiziert gegenüber der bekannten für bidirektionale Beziehungen:

Darstellung gerichteter Beziehungen

Wie die Abbildung zeigt ist es ohne weiteres möglich eine bidirektionale Beziehung -- ohne Informationsverlust -- in zwei unidirektionale zu überführen.
Um die Lesrichtung auszudrücken wird am Ende der Beziehungskante ein offener Pfeil notiert. Die Regeln zur Angabe der Rolle und Multiplizität bleiben unverändert.

Java-Syntax:
Die Programmiersprache Java bietet kein explizites Sprachkonstrukt zur Definition einer Beziehung zwischen Objekten.
Jedoch kann die notwendige Semantik mit der Objekt-Referenz nachgebildet werden. Hierbei ist lediglich eine Orientierung an den spezifizierten Multiplizitäten notwendig:
0..1 Die Beziehung wird durch genau ein Referenzattribut vom Typ der Zielklasse dargestellt. Dieses Attribut kann in einigen Objekten leer sein.
1..1 Die Beziehung wird durch genau ein Referenzattribut vom Typ der Zielklasse dargestellt. Durch geeignete Operationen ist sicherzustellen, dass die Menge niemals leer ist. Dieses Attribut darf in keinem Objekt leer sein. Dies ist durch geeignete konsistenzgarantierende Operationen sicherzustellen.
1..* Die Beziehung wird durch genau ein mengenwertiges Attribut dargestellt. Der Typ dieses Attributs sollte als Vector (siehe java.util) festgelegt werden, da die mehrfache Definition identischer Beziehungen zwischen identischen Objekten selten sinnvoll ist.
0..* Die Beziehung wird durch genau ein mengenwertiges Attribut dargestellt. Diese Menge kann leer sein. Der Typ dieses Attributs sollte als Vector (siehe java.util) festgelegt werden, da die mehrfache Definition identischer Beziehungen zwischen identischen Objekten selten sinnvoll ist.
m..n Die Beziehung wird durch genau ein mengenwertiges Attribut dargestellt. Eine geeignete Operation muss sicherstellen, das immer mindestens m aber höchstens n Elemente in der Menge enthalten sind.

UML-Diagramm einer 0..*-Beziehung:

UML-Diagramm einer konditionell multiplen Beziehung

Implementierung einer 0..* Beziehung:
(Datei UrsprungsklasseBez1.java)


import java.util.Vector;
//0..* Beziehung

public class UrsprungsklasseBez1
{
   private Vector rolle1_beziehung1;

   public boolean rolle1_beziehung1_aufbauen(Zielklasse neuesZielDerBeziehung)
   {
      if (rolle1_beziehung1 == null)
      { //die rolle1_beziehung1 exisitiert noch nicht
         rolle1_beziehung1 = new Vector();
      } //end if
      if (rolle1_beziehung1.add(neuesZielDerBeziehung) == false)
      {
         System.out.println("Zu diesem Objekt kann keine Beziehung aufgebaut werden.");
         return false;
      } //if
      else
      {
         return true;
      } //else
   } //end rolle1_beziehung1_aufbauen()

   public boolean rolle1_beziehung1_entfernen(Zielklasse zuEntfernendesZiel)
   {
      if (rolle1_beziehung1.remove(zuEntfernendesZiel) == false)
      {
         System.out.println("Die Beziehung zu diesem Objekt kann nich entfernt werden.");
         return false;
      } //if
      else
      {
         return true;
      } //else
   } //end rolle1_beziehung1_entfernen()
}//end class UrsprungsklasseBez1

UML-Diagramm einer 1..*-Beziehung:

UML-Diagramm einer multiplen Beziehung

Implementierung einer 1..* Beziehung:


import java.util.Vector;
//1..* Beziehung

public class UrsprungsklasseBez2
{
   private Vector rolle2_beziehung2;

   UrsprungsklasseBez2(Zielklasse ZielderBeziehung)
   {
      rolle2_beziehung2 = new Vector();
      rolle2_beziehung2_aufbauen(ZielderBeziehung);
   } //end constructor UrsprungsklasseBez2

   public boolean rolle2_beziehung2_aufbauen(Zielklasse neuesZielDerBeziehung)
   {
      if (rolle2_beziehung2.add(neuesZielDerBeziehung) == false)
      {
         System.out.println("Die Beziehung kann zu diesem Objekt nicht hergestllt werden.");
         return false;
      } //if
      else
      {
         return true;
      } //else
   } //end rolle2_beziehung2_aufbauen()

   public boolean rolle2_beziehung2_entfernen(Zielklasse zuEntfernendesZiel)
   {
      if (rolle2_beziehung2.remove(zuEntfernendesZiel) == false)
      {
         System.out.println("Die Beziehung zu diesem Objekt kann nicht entfernt werden.");
         return false;
      } //if
      else
      {
         return true;
      } //else
   } //end rolle2_beziehung2()

} //end class UrsprungsklasseBez2

UML-Diagramm einer 0..1-Beziehung:

UML-Diagramm einer konditionellen Beziehung

Implementierung einer 0..1 Beziehung:


import java.util.Vector;
//0..1 Beziehung:

public class UrsprungsklasseBez3
{

   private Zielklasse rolle3_beziehung3 = null;//die Ursprungsklasse hat die rolle3 in Beziehung beziehung3 zur Zielklasse

      public boolean rolle3_beziehung3_aufbauen(Zielklasse neuesZielDerBeziehung)
      {
         if (rolle3_beziehung3 == null)
         {
            rolle3_beziehung3 = neuesZielDerBeziehung;
            return true;
         } //if
         else
         {
            System.out.println("In einer 1..1 Beziehung kann nicht mehr als eine Beziehung aufgebaut werden");
            return false;
         } //else
      } //end rolle3_beziehung3_aufbauen()

      public boolean rolle3_beziehung3_entfernen(Zielklasse zuEntfernendesZiel)
      {
      if (rolle3_beziehung3 == zuEntfernendesZiel)
         {//zuEntfernendesZiel  muss das Ziel der Beziehung sein
            rolle3_beziehung3 = null; //Beziehung entfernen
            return true;
         } //if
         else
         {
            System.out.println("Da keine Beziehung zu Diesen Objekt existiert, kann Sie nicht entfernt werden.");
            return false;
         } //else
   } //end rolle3_beziehung3_entfernen()
}//end class UrsprungsklasseBez3

UML-Diagramm einer 1..1-Beziehung:

UML-Diagramm einer 1..1-Beziehung

Implementierung einer 1..1 Beziehung:


import java.util.Vector;
//1..1 Beziehung:

public class UrsprungsklasseBez4
{
   private Zielklasse rolle4_beziehung4 = null;
   //die Ursprungsklasse spielt die rolle4 in der Beziehung beziehung4 zur Zielklasse

   public void UrsprungsklasseBez4(Zielklasse ZielderBeziehung)
   {
      if (ZielederBeziehung != null)
      {
         rolle4_beziehung4 = ZielderBeziehung;
      } //end if
      else
      {
         System.out.println("Eine Beziehung muss bei der Anlage eines Objekts angegeben werden");
      } //end else
   }//end Constructor UrsprungsklasseBez2

   public void rolle4_beziehung4_tauschen(Zielklasse zuTauschendesZiel)
   {
      rolle4_beziehung4 = zuTauschendesZiel;
   } //end rolle4_beziehung4_tauschen()
}//end class UrsprungsklasseBez4
UML-Diagramm einer n..m-Beziehung

Implementierung einer m..n Beziehung (Hier der Fall einer 2..5 Beziehung)


import java.util.Vector;
//2..5 Beziehung (Spezialfall von m..n)

public class UrsprungsklasseBez5
{
   private Vector rolle5_beziehung5;

   UrsprungsklasseBez5(Vector ZielederBeziehung)
   {
      if (ZielederBeziehung.size() >=2 && ZielederBeziehung.size() <=5)
      {
         rolle5_beziehung5 = new Vector(ZielederBeziehung);
      } //if
      else
      {
         System.out.println("UrsprungsklasseBez5 kann über die Beziehung5 nicht mit weniger als zwei oder mehr als fünf Objekten der Klasse Zielklasse verbunden sein");
      } //else
   } //end constructor UrsprungsklasseBez5

   public boolean rolle5_beziehung5_aufbauen(Zielklasse neuesZielDerBeziehung)
   {
      if (rolle5_beziehung5.size() == 5)
      {
         //die Maximalmultiplizität ist erreicht
         System.out.println("Es können nicht mehr als 5 Beziehungen aufgebaut werden.");
         return false;
      } //if
      else
      {
         return (rolle5_beziehung5.add(neuesZielDerBeziehung));
      } //else
   } //end rolle5_beziehung5_aufbauen()

   public boolean beziehung4_entfernen(Zielklasse zuEntfernendesZiel)
   {
      if (rolle5_beziehung5.size() == 2)
      {
         //das Minimalmultiplizität ist erreicht
         System.out.println("Die Beziehung zu diesem Objekt kann nicht entfernt werden.");
         return false;
      } //if
      else
      {
         if (rolle5_beziehung5.remove(zuEntfernendesZiel) != true)
         {
            System.out.println("Da keine Beziehung zu Diesen Objekt existiert, kann Sie nicht entfernt werden.");
            return false;
         } //end if
         else
         {
            return true;
         } //end else
      } //else
   } //end beziehung5_aufbauen()
} //end class UrsprungsklasseBez5

Die Zielklasse als Bezugspunkt aller dargestellten Beziehungsbeispiele


public class Zielklasse
{
} //end class Zielklasse

Die Abbildung faßt nochmals die Implementierungsableitung aus dem UML-Klassendiagramm zusammen:

Übersicht der Java-Implementierungsschritte eines UML-Klassendiagramms

3.2.10 Das Konzept der Schnittstelle

Motivation:
Mit abstrakten Klassen wurde eine Möglichkeit zur vom konkreten Anwendungsbegriff abstrahierten Definition von Attributen und Operationen vorgestellt. Auch ist die Implementierung von Operationen durch Methoden bereits auf Ebene der abstrakten Klassen möglich.
Untersuchungen existierender Sprachen haben gezeigt, dass die Anwendung abstrakter Klassen im wesentlichen in zwei Bereiche zerfällt: Einerseits zur Ausfaktorisierung gemeinsamer Attribute verschiedener Klassen in eine Superklasse. Andererseits zur Spezifikation möglichen Verhaltens (ausgedrückt durch Operationen) unabhängig von der konkreten Anwendungsklasse.

Semantik:
Eine Schnittstelle beschreibt einen Teil der extern sichtbaren Verhaltens einer Klasse.

Graphische Notation in UML:
Schnittstellen können als "klassische" Klasse, um den Stereotyp interface erweitert, dargestellt werden.

Darstellung der API-Standardklasse String mit den durch sie implementierten Schnittstellen

Java-Syntax:
Im Gegensatz zur Vererbungsbeziehung zwischen Klassen erlaubt Java bei Schnittstellen Mehrfachvererbung, d.h. eine Klasse kann beliebig viele Schnittstellen implementieren. Das im Abschnitt 2.2.8 diskutierte Problem des mehrfachen Erbens tritt hierbei nicht zu Tage, da eine Schnittstelle nur Operationssignaturen definiert. Somit sind ererbte Signaturen entweder identisch (d.h. Namens- und Typgleichheit hinsichtlich der Übergabe- und Rückgabeparameter) oder verschieden und damit unterscheidbar.
Durch die implements-Klausel in der Klassendeklaration erklärt die Klasse ihre Bereitschaft die angegebenen Schnittstellen vollständig zu implementieren; dies wird durch den Java-Compiler geprüft.
Die Syntax wurde bereits bei der Darstellung der Klassendeklaration in Punkt 2.2.1 dargestellt.
Alle Operationen einer Schnittstelle sind implizit abstrakt, weil in der Schnittstelle keine Methoden für die deklarierten Operationen angegeben werden müssen. Implementiert eine Klasse nicht alle durch die Schnittstelle zur Verfügung gestellten Operationen, so muss die Klasse als abstrakt deklariert sein; andernfalls meldet der Compiler bei der Übersetzung einen Fehler.
Die Implementierung der in einer Schnittstelle definierten Operationen durch Methoden einer Klasse schließt die vollständige Übernahme der in der Schnittstelle definierten Signaturen ein. Insbesondere kann die implementierende Klasse die Signatur nicht verengen, z.B. den Sichtbarkeitstyp auf eine restriktivere Variante setzen, oder Datentypen einzuschränken.
Als Besonderheit, und wesentliches Unterscheidungsmerkmal zu den "üblichen" Klassen, können die Operationen einer Schnittstelle nicht als static, final, private oder protected deklariert werden. Dies würde einen zu grossen Eingriff in die Struktur der diese Schnittstelle implementierenden Klasse bedeuten. Im Falle von static wäre dies ein klarer Widerspruch zur Intention der Schnittstellenbildung. Da als static deklarierte Operationen immer klassenbezogen und damit konkret statt abstrakt sind und eine Schnittstelle per definitionem ausschließlich abstrakte Methoden enthalten kann.
Datenfelder einer Schnittstelle hingegen sind immer klassenbezogen (static) und endgültig (final). Sie dienen der Definition vom beim Aufruf von Methoden verwendeten Konstanten.

Beispiel:
Definition einer Schnittstelle:


   interface Comparable
   {
      int YES = 1;
      int NO = 0;

      public int compareTo(Object o);
   }//end interface Comparable

Erklärung der Implementierungsabsicht durch eine Klasse:
class String implements java.io.Serializable, Compareable;

1Semantik [grch.: semantikos "bezeichnend"] Bezeichnung für die wissenschaftliche Beschäftigung mit der Bedeutung sprachlicher Ausdrücke bzw. von Zeichen im allgemeinen.

2 Barbara Liskov: Data Abstraction and Hierarchy, SIGPLAN Notices, Vol. 23, No. 5, May, 1988.




separator line
Service provided by Mario Jeckle
Generated: 2004-06-07T12:31:56+01:00
Feedback Feedback       SiteMap SiteMap
This page's original location This page's original location: http://www.jeckle.de/vorlesung/sei/kII.html
RDF metadata describing this page RDF description for this page