Referenzeffekte in Java
Diese Lektion behandelt in ihrem zweiten Teil die Frage der Übergabe von Referenzargumenten an Parameter. Im ersten Teil wird dieses Thema zunächst mit der Erklärung allgemeiner Eigenschaften von Referenzen vorbereitet.
In Java können Werte von Variablen oder Ausdrücken neben den primitiven Werten immer nur Referenzen auf Objekte oder die Nullreferenz sein und nie Objekte selber. Dadurch ergeben sich einige Effekte, die in dieser Lektion beschrieben werden sollen.
Änderungen von Variablen
In der Methode "main" der Klasse "BindingModification" wird die Bindung der Inhalte der lokalen Variablen "s" verändert. Sie enthält zuerst eine Referenz auf einen Textspeicher mit dem Text "a" und später dann eine Referenz auf einen Textspeicher mit dem Text "b". So kommt die Ausgabe "ab" mit zwei verschiedenen Zeichen zustande, obwohl zweimal der gleiche Bezeichner als Argument der Ausgabeoperation angegeben ist.
BindingModification.javapublic class BindingModification
{ public static void main( final java.lang.String[] args )
{ final java.lang.StringBuilder a = new java.lang.StringBuilder( "a" );
final java.lang.StringBuilder b = new java.lang.StringBuilder( "b" );
java.lang.StringBuilder s;
s = a; java.lang.System.out.print ( s );
s = b; java.lang.System.out.println( s ); }}System.outab
Die Diagramme veranschaulichen die Situation nach den beiden Zuweisungen. Es ist die dargestellte Änderung der Referenz in einer Variablen, die durch den Variablenmodifizierer "final" verhindert worden wäre, der deswegen in dem Beispielprogramm fehlt.
In den Abbildungen dieser Lektion stellen die Rechtecke Variablen dar, eine Zahl oder ein Doppelkreuz "#" symbolisiert eine Referenz, und ein Kreis steht für ein Objekt.
Situation nach "s=a"Quelltextmodell : Laufzeitmodell _____
: .-' '-.
: .' '.
: / \
: .---. ; ;
s =======================>| 0 ------------>0| "a" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
: '-._____.-'
: Objekt
: _____
: .-' '-.
: .' '.
: / \
: ; ;
: 1| "b" |
: ; ;
: \ /
: '. .'
: '-._____.-'
: Objekt
:Situation nach "s=b"Quelltextmodell : Laufzeitmodell _____
: .-' '-.
: .' '.
Bezeichner : / \
: .---. ; ;
s =======================>| 1 | 0| "a" |
Bindung : '-|-' ; ;
: Vari|able \ /
: | '. .'
: | '-._____.-'
: | Objekt
: | _____
: | .-' '-.
: | .' '.
: | / \
: | ; ;
: '------------->1| "b" |
: Referenz ; ;
: \ /
: '. .'
: '-._____.-'
: Objekt
:
Änderungen von Objekten
Auch in der Methode "main" der Klasse "StateModification" wird durch zweifache Ausgabe des lokalen Bezeichners "s" Text »ab« ausgegeben, aber hier geschieht dies auf eine ganz andere Weise. Die Variable »s« enthält die ganze Zeit lang dieselbe Referenz, verweist also auf dasselbe Objekt. Zwischen den beiden Ausgabeoperationen wird hier nicht der Inhalt der Variablen »s«, sondern der Zustand des Objektes verändert, der von diesem Inhalte referenziert wird.
StateModification.javapublic class StateModification
{ public static void main( final java.lang.String[] args )
{ final java.lang.StringBuilder s = new java.lang.StringBuilder( "-" );
s.setCharAt( 0, 'a' ); java.lang.System.out.print ( s );
s.setCharAt( 0, 'b' ); java.lang.System.out.println( s ); }}System.outab
Die Abbildungen stellen die Situation bei den beiden Ausgabeoperationen bildlich dar. Man erkennt, daß zwar der Zustand des von der Referenz in der Variablen referenzierten Objekts verändert wird, aber sich die Variable selber nicht verändert. Sie zeigt immer auf dasselbe Objekt. Daher konnte die Variable in diesem Programm sogar mit dem Variablenmodifizierer »final« deklariert werden. (Man vergleiche diese Situation mit der Situation, die in dem vorigen Abschnitt „Änderungen von Variablen “ beschrieben wurde.)
Situation nach "s.setCharAt( 0, 'a' )"Quelltextmodell : Laufzeitmodell _____
: .-' '-.
: .' '.
: / \
: .---. ; ;
s =======================>| # ------------->| "a" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
: '-._____.-'
: Objekt
:Situation nach "s.setCharAt( 0, 'b' )"Quelltextmodell : Laufzeitmodell _____
: .-' '-.
: .' '.
: / \
: .---. ; ;
s =======================>| # ------------->| "b" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
: '-._____.-'
: Objekt
:
Aliaseffekte
In der Methode "main" der Klasse "Aliasing" wird eine Referenz auf einen Textspeicher einer Variablen "s" und gleichzeitig auch einer anderen Variablen "t" zugewiesen. Da beide Referenzen dasselbe Objekt referenzieren, läßt dieses sich dann in dem Gültigkeitsbereich dieser beiden Bezeichner mit jedem der beiden Bezeichner ansprechen. So wird das Objekt dann unter Verwendung des Bezeichners "t" verändert, aber unter Verwendung des Bezeichners "s" ausgelesen.
Aliasing.javapublic class Aliasing
{ public static void main( final java.lang.String[] args )
{ final java.lang.StringBuilder s = new java.lang.StringBuilder( "-" );
final java.lang.StringBuilder t = s;
t.setCharAt( 0, 'a' ); java.lang.System.out.print ( s );
t.setCharAt( 0, 'b' ); java.lang.System.out.println( s ); }}System.outab
Das Verwirrende daran kann es sein, daß man bei oberflächlicher Betrachtung vermeint, daß die Variable »s« zwischen den beiden Ausgaben nicht verändert wird und daher die beiden Ausgaben gleich sein müßten. Dieses Beispiel illustriert Anweisungen mit einer versteckten Wirkung, die beim Programmieren manchmal zu beobachten sind: Eine Veränderung eines Zustandes erfolgt durch eine Maßnahme, der man dies zunächst nicht ansieht. Hier hat die Anweisung »t.setCharAt( 0, 'b' );« die versteckte Wirkung der „Veränderung von s“. Die Wirkung ist versteckt, da der Name »s« in der Anweisung selber nicht vorkommt. Solche versteckten Wirkungen lassen sich nicht immer ganz vermeiden, aber sie können Ursachen für fehlerhafte Interpretationen von Programmen sein und damit zu Fehlern führen.
Solche Aliassituationen in einem Block wird man selten bewußt herstellen, weil sie die Situation nur unnötig verkomplizieren. Aliassituationen können aber unbeabsichtigt auftreten, etwa wenn ein Methode zwei Referenzen verarbeiten soll, und beispielsweise von dem einen referenzierten Objekt in das andere kopieren soll. Es könnte dabei aber auch einmal vorkommen, daß beide Referenzen dasselbe Objekt referenzieren. Die Methode sollte sich dann auch in diesem Fall so wie beabsichtigt (wie gewünscht) verhalten. Der Programmierer muß also stets auch an die Möglichkeit denken, daß zwei auf verschiedenen Wegen erhaltene Referenzen einander gleich sein könnten.
Die beiden Abbildungen zum Aliaseffekt machen die Situation noch einmal deutlich.
Situation nach "t.setCharAt( 0, 'a' )"Quelltextmodell : Laufzeitmodell
:
: .---.
t =======================>| # -----------------------.
Bezeichner Bindung : '---' Referenz |
: Variable |
: __V__
: .-' '-.
: .' '.
: / \
: .---. ; ;
s =======================>| # ------------->| "a" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
: '-._____.-'
: ObjektSituation nach "t.setCharAt( 0, 'b' )"Quelltextmodell : Laufzeitmodell
:
: .---.
t =======================>| # -----------------------.
Bezeichner Bindung : '---' Referenz |
: Variable |
: __V__
: .-' '-.
: .' '.
: / \
: .---. ; ;
s =======================>| # ------------->| "b" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
: '-._____.-'
: Objekt
Referenzvariablen und Referenzwerte (Referenzen)
Referenzvariablen und Referenzwerte werden oft nicht deutlich unterschieden, indem beide einfach nur als „Referenzen“ bezeichnet werden. Eine „Referenz“ ist aber eigentlich ein Referenzwert und keine Referenzvariable.
Ein Referenzwert —oder, kurz, eine Referenz —ist eine Angabe, die es ermöglicht, ein bestimmtes Objekt zu erreichen (oder es ist die Nullreferenz).
Eine Referenzvariable ist eine Speicher, der zu einem Zeitpunkt genau einen Referenzwert enthalten kann, aber im Laufe der Zeit kann sich der enthaltene Referenzwert auch verändern.
Den Unterschied zwischen Referenzvariablen und Referenzwerten kann man jetzt verstehen, wenn man daran denkt, daß im Abschnitt „Änderungen von Variablen “ dieselbe Referenzvariable zwei verschiedene Referenzwerte enthielt (zu zwei verschiedenen Zeitpunkten). Umgekehrt findet man in dem letzten Abschnitt „Aliaseffekte “ die Situation dargestellt, daß zwei verschiedene Referenzvariablen denselben Referenzwert enthalten. (Zum Verständnis dieses Unterschiedes betrachte man noch einmal die Schaubilder dieser beiden Abschnitte.)
Referenzargumente
In Java werden Argumente eines Aufrufs immer als Wert an die angewendete Methode übergeben. (Das bedeutet, daß der Parameter an der Wert des jeweiligen Argumentausdrucks gebunden wird.) Dies gilt sowohl für Argumentwerte primitiver Typen (wie des Typs "int") als auch für Argumentwerte von Referenztypen, also beispielsweise, wenn ein Bezeichner als Argument verwendet wird, dessen Typ eine Klasse ist.
Eine Variable enthält einen Wert eines primitiven Typs oder eine Referenz, aber nie ein Objekt selber. Daher werden also stets primitive Werte oder Referenzen, aber nie Objekte übergegeben. (Primitive Werte und Referenzwerte werden manchmal zusammen auch als „Skalare“ bezeichnet.)
Nach der Anweisung "java.lang.StringBuilder va = new java.lang.StringBuilder();" enthält die Variable "va" beispielsweise kein Objekt, sondern eine Referenz auf ein Objekt.
Der Bezeichner "va" hat den Typ "java.lang.StringBuilder" und ist an eine Variable gebunden, die eine Referenz auf ein Objekt der Klasse "java.lang.StringBuilder" enthalten kann—diese Variable enthält tatsächlich eine Referenz auf ein soeben erzeugtes Objekt der Klasse "java.lang.StringBuilder".
Ein Referenzvariablenbezeichner bezieht sich auf ein Objekt:
:
Quelltextmodell : Laufzeitmodell
:
:
:
: Objekt
: _____
: .-' '-.
: .' '.
: / \
: .---. ; ;
va ======================>| # ------------->| |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
: '-._____.-'
:
:
Typ: : Typ:
java.lang.StringBuilder : java.lang.StringBuilder
:
:
:
:
Änderung eines Objekts
In der Übersetzungseinheit "RefArg.java" wird in der Methode "main" ein Objekt der Klasse "java.lang.StringBuilder" erzeugt. Die Methode "modify" kann dann mit der Variablen "va" aufgerufen werden, die eine Referenz auf dieses Objekt enthält. Da die Methode "modify" über diese Referenz auf das Objekt zugreifen kann, ist sie auch in der Lage, es zu verändern. Das liegt aber nicht daran, daß etwa eine Referenz auf die Variable "va" übergeben wurde: Es wurde vielmehr auch in diesem Fall der Wert der Variablen "va" übergeben, nur daß dieser Wert eben eine Referenz auf ein Objekt ist.
RefArg.javapublic class RefArg
{
public static void show( final java.lang.StringBuilder vc )
{ System.out.println( vc ); }
public static void modify( final java.lang.StringBuilder vb )
{ vb.setCharAt( 0, 'b' ); }
public static void main( final String[] args )
{ final java.lang.StringBuilder va =
new java.lang.StringBuilder( "a" );
show( va ); modify( va ); show( va ); }}System.outa
b
Die zweite Ausgabe des von der Variablen "va" referenzierten Objektes zeigt, daß dieses Objekt von der Methode "modify" verändert werden konnte und, daß diese Veränderung auch in der aufrufenden Methode "main" beobachtbar ist.
Die Variable "va" enthält eine Referenz auf das erzeugte Objekt und die Methode "modify" erhält eine Kopie dieses Wertes, also eine Kopie dieser Referenz auf das erzeugte Objekt, aber keine Kopie dieses Objektes. Bei Eintritt in den Rumpf der Methode "modify" haben die Variable "va" und die Variable "vb" nun denselben Wert, sie referenzieren beide dasselbe Objekt "O".
Zwei Referenzen auf dasselbe ObjektQuelltextmodell : Laufzeitmodell
:
Gültigkeitsbereich "main" :
: .---.
va =======================>| # -----------------------.
Bezeicher Bindung : '---' Referenz |
: Variable |
..............................: __V__
: .-' '-.
: .' '.
Gültigkeitsbereich "modify" : / \
: .---. ; ;
vb =======================>| # ------------->| "b" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: vb.setCharAt( 0, 'b' ) '. .'
: -------------------------> '-._____.-'
: Operation Objekt
Nachbildung der Übergabe von Objektwerten (Call-by-Object-Value )
Wenn Änderungen an einem Objekt nicht möglich sein sollen, so kann entweder eine Kopie des Objekts (ein Clone ) übergeben werden oder ein unveränderliches Objekt verwendet werden.
Nachbildung der Variablenübergabe (Call-by-Reference )
Die Übergabe einer Variablen, so daß deren Inhalt verändert werden kann, kann auf verschiedenen Wegen nachgebildet werden, etwas durch Übergabe einer Reihung. Dabei entspricht die Reihung der Variablen, und dem Inhalt der Variablen entspricht der Inhalt der ersten Komponente der Reihung.
Main.javapublic class Main
{ static void method( final java.lang.String[] variable )
{ variable[ 0 ]= "beta"; } public static void main( final java.lang.String[] commandLineArguments )
{ final java.lang.String[] variable = new java.lang.String[]{ "alpha" };
method( variable );
java.lang.System.out.println( variable[ 0 ] ); }}
Änderung trotz des Schlüsselwortes "final"
Man beachte, daß auch das Schlüsselwort "final" in der Parameterdeklaration "final java.lang.StringBuilder vb" die Veränderung des von der Variablen "vb" referenzierten Objekts nicht untersagt. Dieses "final" untersagt nur Veränderungen des Inhalts der Variablen "vb", also das Schreiben eines neuen Referenzwertes in die Variable "vb", nicht jedoch Veränderungen des von der enthaltenen Referenz referenzierten Objektes. (Um solche Veränderungen zu verhindern, kann eine Schnittstelle [oder Klasse] verwendet werden, die keine Methoden enthält, deren Aufruf solche Veränderungen vornehmen kann.)
Änderung einer Referenzvariablen
Wird der Inhalt der Variablen "vb" in der Methode "modify", z.B. durch "vb = new java.lang.StringBuilder();" jedoch so verändert, daß er ein anderes Objekt referenziert, dann wird das von der Variablen "va" in der aufrufenden Methode "main" referenzierte Objekt durch anschließende Zugriffe auf das von dem Parameter "vb" referenzierte Objekt natürlich auch nicht mehr verändert.
Die Zuweisung eines Wertes an den Parameter "vb" in der Methode "modify" verändert also nicht das bisher von dem Parameter "vb" referenzierte Objekt "Objekt 0".
RefArg1.javapublic class RefArg1
{
public static void show( final java.lang.StringBuilder vc )
{ System.out.println( vc ); }
public static void modifyParameter( java.lang.StringBuilder vb )
{ vb = /* vb erhaelt eine Referenz auf ein neues Objekt 1 */
new java.lang.StringBuilder( "a" ); /* Objekt 1 wird erzeugt */
vb.setCharAt( 0, 'b' ); /* Stelle 0, aendert Objekt 1 */ }
public static void main ( final String[] args )
{ final java.lang.StringBuilder va =
new java.lang.StringBuilder( "a" ); /* Objekt 0 wird erzeugt */
show( va ); modifyParameter( va ); show( va ); }}System.outa
a
Wenn die Kontrolle die mit dem Kommentar "/* Stelle 0, aendert Objekt 1 */" markierte Zeile erreicht, dann ist der Parameter "vb" an ein anderes Objekt "Objekt 1" gebunden worden, und die Zugriffe auf dieses Objekt können das erste Objekt "Objekt 0" dann natürlich nicht mehr verändern.
Die beiden Variablen referenzieren an Stelle 0 verschiedene ObjekteQuelltextmodell : Laufzeitmodell _____
: .-' '-.
Gültigkeitsbereich "main" : .' '.
: / \
: .---. ; ;
va =======================>| 0 ------------>0| "a" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: '. .'
..............................: '-._____.-'
: Objekt
: _____
: .-' '-.
am Ende von "modifyParameter" : .' '.
: / \
: .---. ; ;
vb =======================>| 1 ------------>1| "b" |
Bezeichner Bindung : '---' Referenz ; ;
: Variable \ /
: vb.setCharAt( 0, 'b' ) '. .'
: -------------------------> '-._____.-'
: Operation Objekt
Der Parameter "vb" wird in der Methodendeklaration verändert, indem ihm eine neue Referenz zugewiesen wird. So etwas ist selten sinnvoll, da ein Parameter die Werte der Argumente enthalten sollte und diese Voraussetzung nicht mehr erfüllt wäre, wenn er verändert werden würde. Daher sollten Parameter in der Regel mit "final" deklariert werden, das hat auch den Vorteil, daß der Übersetzer dann bei einer möglicherweise nicht den Absichten des Autors entsprechenden Zuweisung (wie in der Methode "modifyParameter") eine Fehlermeldung anzeigte, die den Programmierer auf die fragwürdige Zuweisung aufmerksam machte.
modifyParameter [Methodendeklaration]public static void modifyParameter( java.lang.StringBuilder vb )
{
vb = /* vb erhaelt eine Referenz auf ein neues Objekt 1 *
new java.lang.StringBuilder( "a" ); /* Objekt 1 wird erzeugt */
vb.setCharAt( 0, 'b' ); /* Stelle 0, aendert Objekt 1 */
}
Das hier für den Parameter "vb" Gesagte gilt entsprechend auch für andere Typen von Variablenbezeichnern.
Nullreferenzen
Bei Ausdrücken mit »null« ist es offensichtlich, daß eine Nullreferenz verwendet wird. Bei Parametern ist dies jedoch auch immer möglich, ohne daß es so offensichtlich ist.
Für jede Operation sollte spezifiziert sein, ob eine Nullreferenz als Argument zulässig ist und welches Verhalten die Operation dann haben soll. Ist eine Nullreferenz nicht zulässig, dann muß der Aufrufer einer Operation diese unbedingt vermeiden. Ist sie zulässig, dann muß die Operation sicherstellen, daß sie auch bei Übergabe einer Nullreferenz das spezifizierte Verhalten zeigt.
Quellen zum Thema
- Parameter passing in Java —by reference or by value?
- http://www.yoda.arachsys.com/java/passing.html
- Pass-by-value semantics in Java applications
- http://www-128.ibm.com/developerworks/java/library/j-passbyval/
- Pass-by-value semantics in Java applications
- http://www-128.ibm.com/developerworks/library/j-praxis/pr1.html
- Fun with pointers
- http://cslibrary.stanford.edu/104/