Modellierung mit
UML
Loading

B Java

Die bei einer Softwareentwicklung verwendete Programmiersprache hat wesentlichen Einfluss auf die Effektivität der Entwickler sowie die Qualität, Kosten, Weiterentwickelbarkeit und Wartbarkeit des Produkts. Die in dieser Arbeit vorgestellte Methodik UML/P verwendet als Zielsprache Java. Für eine gute Integration mit der Modellierungssprache UML/P werden daher bei der Definition von UML/P soweit wie möglich Elemente aus der Java-Grammatik verwendet. Deshalb wird in diesem Abschnitt die in [GJSB05] definierte Referenzgrammatik für die Sprache Java 6 in EBNF-Form übertragen.

Bei der Verwendung eines der nachfolgend eingeführten Nichtterminale in anderen Kapiteln wird dem Nichtterminal das Subscript der Abbildung, in der es definiert ist, angehängt. So verweist TypeB.2 auf die Definition dieses Nichtterminals in Abbildung B.2.

Auf eine Darstellung der lexikalischen Elemente der Sprache Java wird hier verzichtet und auf [GJSB05] verwiesen. Dort wird die Form der Kommentare, der „white spaces“, des für Java benutzen Unicode-Alphabets und der Schlüsselwörter erklärt. Insbesondere seien die in Abbildung B.1 aufgelisteten Nichtterminale Literal, Identifier und Digits als bekannt voraus gesetzt (siehe auch Anhang A).

Literal
beschreibt die Menge aller Konstanten. Es beinhaltet unter anderem Zahlen und Strings. Beispiele sind 34, 0x3ff oder 'T'.
Identifier
ist die Menge aller Namen, die nicht Schlüsselwörter darstellen. Diese beginnen mit einem Buchstaben oder dem Unterstrich und können Ziffern enthalten. Klassen, Attribute, Methoden und andere Artefakte der Sprache Java können mit solchen Namen benannt werden.
Digits
ist eine nichtleere dezimale Ziffernfolge.
Abbildung B.1: Grundelemente: Literal, Identifier und Digits

Wie aus den eingeführten Nichtterminalen ersichtlich ist, werden die englischen Originalnamen verwendet, damit ein direkter Vergleich mit der in [GJSB05] gegebenen Grammatik möglich ist. Aus den dort gegebenen 124 Nichtterminalen mit relativ einfach strukturierten Produktionen werden 62 Nichtterminale mit komplexeren EBNF-Produktionen komprimiert. Dabei behalten fast alle übernommenen Nichtterminale ihre ursprüngliche Bedeutung. Einige wenige Nichtterminale werden neu eingeführt.

In Abbildung B.2 sind die Produktionen für Typen normale und generische Klassen, qualifizierte Namen und Kompilierungseinheiten (Dateien) enthalten.

CompilationUnit ::= { ⟨Annotation* package Name ; }opt
{ import staticopt Name⟩ { . }opt
; }* TypeDeclaration*
TypeDeclaration ::= ClassDeclaration
| InterfaceDeclaration
| EnumDeclaration
| AnnotationTypeDeclaration
| ;
Name ::= Identifier.1-*
  
BasicType ::= boolean | byte | int |
Type ::= BasicType
| Name⟩ ⟨TypeArgumentsopt
| Type []
| TypeVariable
TypeVariable ::= Identifier | ?
TypeArguments ::= < TypeArgument,1-* >
TypeArgument ::= Type | ?
| ? extends Type&1-*
| ? super Type&1-*
TypeVoid ::= Type | void
Abbildung B.2: Typen, Namen und Dateien

Die Definition von Klassen und Interfaces ist in Abbildung B.3 beschrieben.

ClassDeclaration ::= Modifier* class Identifier
TypeParametersopt
{ extends Type⟩ }opt
{ implements Type,1-* }opt
ClassBody
ClassBody ::= { ClassBodyDeclaration* }
ClassBodyDeclaration
::= FieldDeclaration
| MethodDeclaration
| staticopt Block
| ConstructorDeclaration
| TypeDeclaration
Modifier ::= public | protected | private | static
| final | abstract |
| Annotation
  
InterfaceDeclaration
::= Modifier* interface Identifier
TypeParametersopt
{ extends Type,1-* }opt
{ InterfaceBodyDeclaration* }
InterfaceBodyDeclaration
::= FieldDeclaration
| MethodHeader ;
| TypeDeclaration
  
EnumDeclaration ::= Modifier* enum Identifier
{ implements Type,1-* }opt
EnumBody
EnumBody ::= { EnumConstant,* ,opt
   { ; ClassBodyDeclaration* }opt }
EnumConstant ::= Annotation* Identifier⟩ ⟨Argumentsopt
ClassBodyopt
  
TypeParameters ::= < TypeParameter,1-* >
TypeParameter ::= Identifier⟩ { extends Type&1-* }opt
Abbildung B.3: Klassen und Interfaces

Die in Klassen- und Interface-Deklarationen enthaltenen Attribute, Methoden und Konstruktoren werden in Abbildung B.4 erklärt.

FieldDeclaration ::= Modifier* Type⟩ ⟨VariableDeclarator, 1-* ;
VariableDeclarator ::= Identifier []* { = VariableInitializer⟩ }opt
VariableInitializer ::= Expression
| { VariableInitializer,* ,opt }
  
MethodDeclaration
::= MethodHeader⟩ { ⟨Block | ; }
MethodHeader ::= Modifier* TypeParametersopt TypeVoid⟩ ⟨Identifier
  FormalParameters []* Throws
ConstructorDeclaration
::= ConstructorHeader⟩ ⟨Block
ConstructorHeader
::= Modifier* TypeParametersopt Identifier
  FormalParameters⟩ ⟨Throws
FormalParameters ::= ( FormalParameter,1-*
   { , LastFormalParameter⟩ }opt )
| ( LastFormalParameteropt )
FormalParameter ::= {final | Annotation⟩}* Type⟩ ⟨Identifier []*
LastFormalParameter
::= {final | Annotation⟩}* Type ... Identifier []*
Throws ::= { throws Type,1-* }opt
Abbildung B.4: Attribute, Methoden und Konstruktoren

Die Form der Java-Anweisungen ist in der Abbildung B.5 erklärt. Die hier beschriebene Sprache ist trotz der starken Kompaktifizierung der Grammatik, die durch die Verwendung der EBNF möglich wurde, mit der Originalsprache Java nahezu identisch. Geringfügige Vereinfachungen wurden vorgenommen, die ausschließlich der Erhöhung der Lesbarkeit der Grammatik dienen. So ist bei einer try-Anweisung nicht durch die Grammatik festgelegt, dass wenigstens ein catch oder finally anzugeben ist. Außerdem ist diese kompakte Grammatik bezüglich des bekannten if-then-else-Phänomens mehrdeutig und daher nicht direkt als Vorlage für einen Parser geeignet.

Block ::= { Statement* }
Statement ::= finalopt Type⟩ ⟨VariableDeclarator,1-* ;
| TypeDeclaration
| Block
| ;
| Expression ;
| switch ( Expression ) { SwitchPart* }
| do Statement while ( Expression ) ;
| break Identifieropt ;
| continue Identifieropt ;
| return Expressionopt ;
| assert Expression⟩ { : Expression⟩ }opt ;
| synchronized ( Expression ) Block
| throw Expression ;
| try Block⟩ ⟨CatchClause* { finally Block⟩ }opt
| Identifier : Statement
| if ( Expression ) Statement
{ else Statement⟩ }opt
| while ( Expression ) Statement
| for ( ForInitopt ; Expressionopt ;
Expression,* ) Statement
| for ( Modifieropt Type⟩ ⟨Identifier :
Expression ) Statement
 
SwitchPart ::= { case Expression : | default : }1-*
  BlockStatement*
CatchClause ::= catch ( Type⟩ ⟨Identifier []* ) Block
ForInit ::= Expression,1-*
| finalopt Type⟩ ⟨VariableDeclarator,1-*
Abbildung B.5: Block und Statement

Die Teilsprache zur Beschreibung der Ausdrücke ist in der Abbildung B.6 definiert. Die in Java zur Verfügung stehende Ausdruckssprache ist nicht zuletzt aufgrund der Möglichkeit, innere Klassen zu definieren und zu nutzen sowie Arrays zu initialisieren, relativ komplex. Die Grammatik aus [GJSB05] wurde deshalb zum einen durch eine vollständige Auslagerung der Operator-Prioritäten in der Tabelle B.7 kompaktifiziert. Zum anderen wurden die Produktionen für Primary und Expression umgebaut.

Primary ::= ( Expression )
| Literal
| { ⟨Primary . }opt Identifier⟩ ⟨Argumentsopt
| { ⟨Primary . }opt this Argumentsopt
| { ⟨Primary . }opt super Arguments
| Primary [ Expression ]
| super . Identifier⟩ ⟨Argumentsopt
| new Name []1-* { VariableInitializer,* ,opt }
| new Name⟩ { [ Expression ] }1-* []*
| { ⟨Primary . }opt new Name⟩ ⟨Arguments
ClassBodyopt
| { ⟨Primary | TypeVoid⟩ } . class
PrefixOp ::= ++ | -- | + | - | ~ | !
| ( Type )
PostfixOp ::= ++ | --
Arguments ::= ( Expression,* )
 
Expression ::= PrefixOp* Primary⟩ ⟨PostfixOp*
| Expression⟩ ⟨InfixOp⟩ ⟨Expression
| Expression instanceof Type
| Expression ? Expression : Expression
| LeftHandSide⟩ ⟨AssignmentOperator⟩ ⟨Expression
InfixOp ::= | / | % | + | - | << | >> | >>>
| < | > | <= | >= | == | !=
| & | ^ | | | && | ||
AssignmentOperator
::= = | ⋆= | /= | %= | += | -= | <<= | >>=
| >>= | &= | ^= | |=
LeftHandSide ::= Name
| Primary⟩ { [ Expression ] | . Identifier⟩ }
| super . Identifier
Abbildung B.6: Ausdrücke in Java

Priorität Operator Assoziativität Operand, Bedeutung




13 ++, -- rechts Zahlen
+, -, ~, ! rechts Zahlen, Boolean (!)
(type) rechts Typkonversion (Cast)
12 ⋆, /, % links Zahlen
11 +, - links Zahlen, String (+)
10 <<, >>, >>> links Shifts
9 <, <=, >, >= links Vergleiche
instanceof links Typvergleich
8 ==, != links Vergleiche
7 & links Zahlen, Boolean
6 ^ links Zahlen, Boolean
5 | links Zahlen, Boolean
4 && links Boolesche Logik
3 || links Boolesche Logik
2 ? : rechts Auswahlausdruck
1 =, ⋆=, /=, %= rechts Zuweisung
+=, -=
<<=, >>=, >>>=
&=, ^=, |=
Tabelle B.7: Prioritäten der Infixoperatoren

Annotationen und ihre Definition sind in Abbildung B.8 definiert.

Annotation ::= @ Name
( { ⟨IdentifierB.1 = ElementValue⟩ },* )
| @ Name⟩ { ( ElementValue ) }opt
ElementValue ::= Expression
| Annotation
| { ElementValue,* ,opt }
  
AnnotationTypeDeclaration
::= Modifier* @ interface Identifier
{ AnnotationTypeElementDeclaration* }
AnnotationTypeElementDeclaration
::= TypeDeclaration
| FieldDeclaration
| Modifier* Type⟩ ⟨Identifier ()
   default ElementValue ;
Abbildung B.8: Annotationen in Java

ocl-Anweisung für Zusicherungen

Java bietet eine assert-Anweisung, die es erlaubt in den Code Zusicherungen einzubauen, die vom Laufzeitsystem geprüft werden. Damit ist bereits ein wesentlicher Schritt zur praktischen Verwendung von Invarianten erreicht. In diesem Abschnitt wird eine ergänzende Form der assert-Anweisung vorgeschlagen, die die Verwendung von OCL-Ausdrücken erlaubt, und definierte und benannte OCL-Bedingungen auch durch Referenzierung einbauen kann. Diese Anweisung wird mit dem Schlüsselwort ocl gekennzeichnet. Zusätzlich wird das let-Konstrukt aus der OCL übernommen, um damit lokale Variablen zu definieren, die genau wie in der OCL zur ausschließlichen Verwendung in Invarianten gedacht sind. In Abbildung B.9 werden die Erweiterungen eingeführt, indem das Nichtterminal Statement aus Abbildung B.5 um entsprechende Anweisungen ergänzt wird.

Statement ::=
| let OCLVarDeclaratorC.8 ;
| ocl AssertPredicate⟩ { : OCLExprC.8⟩ }opt ;
AssertPredicate ::= OCLExprC.8
| IdentifierB.1
| IdentifierB.1 ( OCLExprC.8,* )
Abbildung B.9: Erweiterung der Java-Anweisungen

Die hier definierte ocl-Anweisung erlaubt nur die Verwendung von OCL-Ausdrücken, so dass die Abwesenheit von Seiteneffekten garantiert ist. Das erste Argument der ocl-Anweisung ist das zu prüfende boolesche Prädikat. Das optionale zweite Argument wird ausgewertet, wenn das Prädikat falsch ist, und dessen Wert zum Beispiel beim Einsatz in Tests angezeigt.

Während die erste Variante der ocl-Anweisung im ersten Argument die direkte Verwendung eines OCL-Ausdrucks erlaubt, beziehen sich die anderen beiden Formen durch Verwendung eines Namens auf eine OCL-Bedingung, die an anderer Stelle definiert wurde. Damit können OCL-Bedingungen mehrfach verwendet werden.

Eine OCL-Bedingung beginnt mit einer expliziten Definition eines Kontexts in Form einer oder mehrerer Variablen. Wie in Abschnitt 3.1.1 erklärt, wird damit eine Allquantifizierung über die angegebenen Variablen vorgenommen. Um diese Quantifizierung aufzulösen, kann eine explizite Zuordnung der zu prüfenden Objekte auf die Kontext-Variablen vorgenommen werden, indem die OCL-Bedingung als boolesches Prädikat verstanden wird, dem diese Objekte als Argumente übergeben werden. Ist beispielsweise folgende Bedingung definiert:

context Auction a, Person p inv NM:
  p in a.bidder implies
    forall m in a.message : m in p.message

so kann mit ocl NM(a,theo) geprüft werden, ob die Nachrichten einer Auktion an Person theo versandt wurden.

Ist der Kontext der OCL-Bedingung nicht mit context, sondern dem Schlüsselwort import festgelegt, so findet nach Definition keine Allquantifizierung statt, sondern die dabei festgelegten Namen werden direkt aus dem Kontext importiert. Das bedeutet, unter Nutzung der Variation der obigen OCL-Bedingung

import Auction a, Person p inv NM2:
  p in a.bidder implies
    forall m in a.message : m in p.message

kann mit ocl NM2 eine Aussage über die beiden im Java-Kontext definierten Variablen a und p gemacht werden, ohne dass diese Variablen explizit angegeben werden.

Erfahrungsgemäß sind zur Prüfung von Zusicherungen zu einem Zeitpunkt die früheren Belegungen von Attributen oder Zwischenergebnisse früherer Berechnungen notwendig. Diese stehen unter Umständen zum Zeitpunkt der Evaluation einer Zusicherung nicht mehr zur Verfügung und müssen daher vorher explizit zwischengespeichert werden.1 Für die Definition von Zwischenergebnissen, die ausschließlich zur Prüfung von Zusicherungen verwendet werden, ist das let-Konstrukt geeignet. Es führt eine Zwischenvariable ein, die im normalen Java-Code nicht verwendet werden kann und deshalb keinen Effekt auf die Programmausführung haben kann. let-Anweisungen können also wie ocl-Anweisungen im Produktionssystem weggelassen werden.2

Gemäß der Semantik von OCL-Bedingungen und des OCL-let-Konstrukts, werden die während der Auswertung der Argumente dieser Konstrukte auftretenden Exceptions abgefangen. Evaluiert das Argument des ocl-Konstrukts zu einer Exception, so wird dies als Nichterfüllung der Bedingung gewertet. In der let-Anweisung wird jedoch die Variable mit einem Default-Wert wie zum Beispiel null besetzt.


Bernhard Rumpe. Agile Modellierung mit UML. Springer 2012