Spezialisierungen in Java
Übungsfragen
Der Leser sollte für diese Lektion Vorkenntnisse über Typen und Verallgemeinerungen in Java haben, die ausreichen, um die folgenden Übungsfragen zu beantworten.
- Ausdruck "( Object )System.out"
- Welchen Typ hat der Ausdruck "( Object )System.out"? Welchen Typ hat das von diesem Ausdruck referenzierte Objekt?
- Ausdruck "System.out"
- Welchen Typ hat der Ausdruck "System.out"? Welchen Typ hat das von diesem Ausdruck referenzierte Objekt?
Spezialisierungen
Durch eine Spezialisierung (downcast ) wird mit einem Ausdruck eines Typs ein Ausdruck eines Untertyps dieses Typs gebildet. Dazu wird vor den Ausdruck ein Formungsoperator (cast operator ) geschrieben, der einen Untertyp des Typs des Ausdrucks angibt.
Bei der Übersetzung des Programms wird zunächst geprüft, ob der Typ des Formungsoperators tatsächlich ein Untertyp des Typs des folgenden Ausdrucks ist.
Bei einer Ausführung des Programms wird alsdann geprüft, ob der Typ des vom Operanden referenzierten Objektes tatsächlich der angegebene Untertyp (oder ein Untertyp davon) ist.
Der Ausdruck "( java.io.PrintStream )(( Object )System.out )" ist ein Beispiel einer Spezialisierung. Der Operand "(( Object )System.out )" hat den Typ "Object". Der vorangestellte Formungsoperator "( java.io.PrintStream )" spezialisiert diesen Typ dann auf den Untertyp "java.io.PrintStream". Der Übersetzer erkennt dabei, daß es sich hier um einen Untertyp, also um eine Spezialisierung handelt. Ob das von dem Ausdruck "(( Object )System.out )" referenzierte Objekt tatsächlich vom Formungstyp ist wird beim Programmablauf geprüft. Dies trifft tatsächlich zu.
Beispiel einer Spezialisierung
.---------------------------------.
Verallge- | Object |
meinerung |---------------------------------|
^ |---------------------------------| |
| | + hashCode() : int | |
| '---------------------------------' |
| ^ |
| ( Object ) /_\ ( java.io.PrintStream ) |
| | |
| | |
| .---------------------------------. |
| | java.io.PrintStream | V
|---------------------------------| Spezialisierung
|---------------------------------|
| + println() : void |
'---------------------------------'
In dem Programm "ObjectPrinter2" wird der Ausdruck "(( Object ) System.out )" verwendet. Dieser Ausdruck hat den Typ "Object". Er ist der Operand des Formungsoperators "( java.io.PrintStream )". Der Typ "java.io.PrintStream", in dem umgeformt werden soll, ist eine Erweiterung (Spezialisierung) des Typs "Object" des Operanden "(( Object ) System.out )". Daher handelt es sich bei dieser Formung um eine Spezialisierung auf einen Untertyp.
Zur Laufzeit wird bei einer Spezialisierung getestet, ob der Typ des referenzierten Objekts der Typ des Formungsoperators oder ein Untertyp davon ist. Dies trifft bei dem genannten Beispiel zu, denn der Typ des von der Referenz "System.out" referenzierten Objekts ist ja der Typ "java.io.PrintStream", also genau der Typ des Formungsoperators. Die Umformung ist im Falle dieses Beispiels also möglich.
Durch den zuerst angewendeten Umformungsoperator "( Object )" wird aus dem Ausdruck "System.out" vom Typ "java.io.PrintStream" zunächst ein Ausdruck vom Typ "Object", durch den anderen Umformungsoperator "( java.io.PrintStream )" wird dieser verallgemeinerte Typ dann wieder „zurückspezialisiert“.
ObjectPrinter2.java
public class ObjectPrinter2
{ final public static void main( String[] args )
{ (
( java.io.PrintStream ) /* Spezialisierungsoperator */
(( Object )System.out ) /* Ausdruck vom Typ "Object" */
)
/* Der voranstehende Ausdruck hat den Typ "java.io.PrintStream" */
.println( 447 ); }}System.out
447
Es mag vielleicht als unnötiger Umweg erscheinen, wenn ein Ausdruck zuerst verallgemeinert und dann wieder spezialisiert wird, doch ist in der Praxis eine Spezialisierung tatsächlich oft das Rückgängigmachen einer zuvor erfolgten Verallgemeinerung, nur daß die Spezialisierung im Programmtext nicht immer direkt in der Nähe der Verallgemeinerung erscheint.
Die Darstellung "ObjectPrinter2.dis" des vom Java -Übersetzer erzeugte Codes zeigt, daß der Übersetzer eine checkcast -Anweisung erzeugt, die während der Ausführung des Programms prüft, ob das referenzierte Objekt als Objekt des angegebenen Typs interpretiert werden kann, also ob es ein Objekt der Klasse "java.io.PrintStream" oder einer Unterklasse dieser Klasse ist. Das ist hier aufgrund des Programmtexts eigentlich klar, aber es gibt Situationen, in denen dies nicht so offensichtlich ist.
ObjectPrinter2.dis
0 getstatic #2 <Field java.io.PrintStream out>
3 checkcast #3 <Class java.io.PrintStream>
6 sipush 447
9 invokevirtual #4 <Method void println(int)>
12 return
Eine Spezialisierung kann man auch mit der Prüfung vergleichen, ob etwas eine spezielle Fähigkeit besitzt, während eine Verallgemeinerung eher selbstverständlich ist. So ist es klar, daß jeder Nachrichtensprecher auch sprechen kann (Verallgemeinerung); wenn man hingegen jemanden kennenlernt, der sprechen kann, dann ist es nicht klar, ob er auch die speziellen Fähigkeiten eines Nachrichtensprechers besitzt. Dies muß dann erst geprüft werden, bevor die Person als Nachrichtensprecher eingesetzt wird.
Daher geht eine Spezialisierung in einem Java -Programm oft der Verwendung einer speziellen Fähigkeit voraus, die eine bestimmter Ausdruck nicht haben muß, die aber das referenzierte Objekt hat. Durch die Spezialisierung wird geprüft und sichergestellt, daß das referenzierte Objekt diese gewünschte Erweiterung des Basistyps auch wirklich besitzt, und daß der Ausdruck alle Nachrichten des spezielleren Typs auch akzeptiert.
Manche Speicher oder Dienste werden in Form von Ausdrücken verwendet, die den Typ "Object" haben müssen, obwohl in dem speziellen Fall eigentlich bekannt ist, daß sie einen viel spezielleren Typ, wie z.B. "java.io.PrintStream" referenzieren. Dann muß der allgemeine Typ "Object" verwendet werden und die Information über den spezielleren Typ des referenzierten Objekts ist zunächst verloren gegangen. In solchen Fällen wird eine Spezialisierung verwendet, um einen Ausdruck zu erhalten, der wieder den speziellen Typ des referenzierten Objektes hat. Anschließend können dann auch wieder die Erweiterungen dieses Typs benutzt werden, die bei der Verwendung von Ausdrücken des Typs "Object" nicht zur Verfügung stünden.
Eine stillschweigende Spezialisierung (also eine Spezialisierung ohne Formungsoperator) bei der Initialisierung einer Referenzkonstanten ist allerdings nicht möglich. In dem Quellcode "Printer.java" ist der Wert des Ausdrucks "obj" eine Referenz auf ein Objekt des Typs "java.io.PrintStream". Daher sollte eine Initialisierung einer Referenzkonstante dieses Typs mit diesem Wert möglich sein. Doch da der statische Typ der Konstanten "obj" der Typ "Object", ist müßte es dabei zu einer Spezialisierung kommen. Solch eine stillschweigende Spezialisierung ist aber nicht erlaubt.
Printer.java
public class Printer
{ final public static void main( java.lang.String[] args )
{ final Object obj = System.out;
final java.io.PrintStream ps = obj;
System.out.println( obj.hashCode() ); }}Konsole
javac Test.java
Test.java:4: incompatible types
found : java.lang.Object
required: java.io.PrintStream
java.io.PrintStream ps = obj;
^
Ein Name, dessen Typ eine Spezialisierung des Typs eines Ausdrucks ist, kann nicht direkt mit diesem Ausdruck initialisiert werden.
Das heißt aber nicht, daß ein solcher Name nicht indirekt mit einer Referenz auf das Objekt eines Ausdrucks eines Obertyps initialisiert werden darf. Nur muß dann ein Ausdruck verwendet werden, dessen Typ der Typ des Namens (oder ein Untertyp davon) ist. Hierzu kann ein beliebiger Ausdruck mit einer Formung in ein Objekt des Namenstyps verwandelt werden. Stillschweigend wird eine solche Formung in Java aber nie ausgeführt. Im Programm "Printer1.java" wird eine Zuweisung durch eine ausdrückliche Spezialisierung mit dem Formungsoperator "( java.io.PrintStream )" ermöglicht.
Printer1.java
public class Printer1
{ final public static void main( java.lang.String[] args )
{ final Object object = System.out;
final java.io.PrintStream stream =( java.io.PrintStream )object;
stream.println(); }}System.out
In dem Beispielprogramm "Printer1" soll die Operation "println" des Objektes "stream" aufgerufen werden. Das geht aber nur , wenn diese Operation vom Ausdruck akzeptiert wird. Die Versendung "object.println()" wäre also nicht möglich, die Versendung "stream.println()" ist hingegen möglich. Der Ausdruck "object" und der Ausdruck "stream" referenzieren das gleiche Objekt, und dieses Objekt unterstützt auch die Operation "println()". Doch der Typ des Ausdrucks "object" unterstützt die Operation "println()" nicht, der Typ des Ausdrucks "stream" unterstützt sie, so daß die Versendung "stream.println()" möglich ist.
- Und wofür braucht man das?
- Eine typische Anwendung der Spezialisierung ist eine Situation, in der ein Programmteil beispielsweise eine graphische Figur (Obertyp) verarbeitet und bereits weiß, daß es sich bei dieser Form um einen Kreis (eine spezielle Figur) handelt, obwohl der referenzierende Ausdruck "figur" nur den allgemeinen Obertyp "Figur" hat.
- Durch eine Spezialisierung "( Kreis )figur" auf den speziellen Untertyp "Kreis" kann der Programmteil dem Compiler dann versichern, daß dieses Objekt wirklich ein Kreis ist. Anschließend können spezielle Kreisoperationen (wie die Ermittlung des Radius "(( Kreis )figur ).getRadius()") genutzt werden, die für allgemeine Figuren nicht unbedingt verfügbar sein müssen.
- Der Ausdruck "figur.getRadius()" könnte im Vergleich dazu möglicherweise nicht ausgewertet werden können, wenn die Oberklasse "Figur" die Operation "#getRadius()" nicht unterstützt.