W3docs

Java: Übergabe per Wert vs. per Referenz

Warum Java immer pass-by-value ist, auch beim Übergeben von Objektreferenzen, und was das in der Praxis bedeutet.

Java ist pass-by-value. Immer. Was auch immer du an eine Methode übergibst — ein int, einen String, ein eigenes Objekt, ein Array — die Methode erhält eine Kopie des übergebenen Werts. Dieser Wert kann eine Zahl sein oder eine Referenz auf ein Objekt, aber es ist trotzdem eine Kopie.

Das verwirrt viele, weil eine Methode den Inhalt eines übergebenen Objekts tatsächlich verändern kann, und diese Änderung ist für den Aufrufer sichtbar. Es fühlt sich also an, als ob das Objekt per Referenz übergeben wurde. Wurde es aber nicht. Was übergeben wurde, ist eine Kopie der Referenz. Dieses Kapitel zeigt genau, was das bedeutet.

Diese Seite erklärt, wie sich Primitives, Objekte, Arrays und Strings jeweils verhalten, wenn sie an eine Methode übergeben werden, das mentale Modell, das jeden Fall erklärt, und die praktischen Konsequenzen für deine eigenen Methodenparameter.

Primitives sind einfach

Das Übergeben eines Primitives kopiert seinen Wert in den Parameter:

public static void doubleIt(int n) {
  n = n * 2;
}

int x = 5;
doubleIt(x);
System.out.println(x);    // 5

Die Methode hat ihr eigenes n, getrennt von x. Das Neusetzen von n hat keinen Effekt auf x. Darüber sind sich alle einig: Das ist pass-by-value.

Objektargumente: die Referenz wird kopiert

Bei Objekttypen enthält die Variable nicht das Objekt selbst — sie enthält eine Referenz (eine Adresse, die auf das Objekt irgendwo im Speicher zeigt). Wenn du diese Variable an eine Methode übergibst, kopiert Java die Referenz, nicht das Objekt:

Caller's variable        →   [ref to Dog A]
                                  |
                                  v
                              { Dog A: name="Rex" }
                                  ^
                                  |
Method's parameter       →   [ref to Dog A]

Beide Variablen zeigen jetzt auf dasselbe Dog-Objekt. Deshalb ist das Mutieren des Objekts über den Parameter an der Aufrufstelle sichtbar:

public static void rename(Dog d) {
  d.setName("Buddy");           // mutates the shared object
}

Dog rex = new Dog("Rex");
rename(rex);
System.out.println(rex.getName());   // Buddy

Aber das Zuweisen einer neuen Referenz an den Parameter ändert die Variable des Aufrufers nicht:

public static void replace(Dog d) {
  d = new Dog("Buddy");         // parameter now points at a new Dog
}

Dog rex = new Dog("Rex");
replace(rex);
System.out.println(rex.getName());   // Rex — unchanged

Die Methode hat ihre eigene Kopie der Referenz aktualisiert. Das rex des Aufrufers zeigt weiterhin auf das ursprüngliche Dog.

Das Zwei-Zeiger-Modell

Wenn eine Methode einen Objektparameter erhält, stelle dir zu Beginn zwei Pfeile vor: einen von der Variable des Aufrufers, einen vom Parameter der Methode, beide zeigen auf dasselbe Objekt.

  • Das Mutieren des Objekts über einen der Pfeile ändert, was der andere Pfeil sieht.
  • Das Umlenken eines Pfeils auf ein anderes Ziel hat keinen Effekt auf den anderen Pfeil.

Diese eine Regel beantwortet jede „Ist Java pass-by-reference?"-Frage.

Arrays folgen derselben Regel

Arrays sind Objekte, also kopiert das Übergeben eines Arrays an eine Methode die Referenz, nicht den Inhalt:

public static void zeroFirst(int[] xs) {
  xs[0] = 0;                    // mutates the shared array
}

int[] data = {1, 2, 3};
zeroFirst(data);
System.out.println(data[0]);    // 0

Die Methode hat ein Element über ihre Kopie der Referenz geändert, und der Aufrufer sieht die Änderung, weil beide Referenzen auf dasselbe Array zeigen.

Aber das Zuweisen eines brandneuen Arrays an den Parameter hat keinen Effekt auf den Aufrufer:

public static void resetArray(int[] xs) {
  xs = new int[]{0, 0, 0};      // parameter only
}

int[] data = {1, 2, 3};
resetArray(data);
System.out.println(data[0]);    // 1 — unchanged

Strings: Unveränderlichkeit verbirgt das Problem

Strings sind ebenfalls Objekte, aber sie sind unveränderlich — es gibt keine Methode, die den Inhalt eines String verändert. Daher kann der Mutationsfall nicht auftreten:

public static void uppercase(String s) {
  s = s.toUpperCase();          // creates a new String
}

String name = "ada";
uppercase(name);
System.out.println(name);       // ada — unchanged

s.toUpperCase() gibt einen neuen String zurück; das Zuweisen an s aktualisiert nur den Parameter. name zeigt weiterhin auf das ursprüngliche "ada". Um einen String zu „ändern", gib den neuen zurück und lass den Aufrufer ihn zuweisen.

Warum das wichtig ist

Drei praktische Konsequenzen:

  1. Eine Methode kann ein Primitive nicht „herausschreiben" und die Referenz des Aufrufers nicht neu setzen. Wenn du diesen Effekt brauchst, gib den neuen Wert zurück: x = doubleIt(x); oder verwende ein Wrapper-Objekt, das der Aufrufer nach dem Aufruf auslesen kann.

  2. Eine Methode kann ein gemeinsames Objekt mutieren — was manchmal gewünscht ist (ein Array füllen, eine Liste befüllen) und manchmal eine Überraschung (Aufrufer erwarten nicht, dass sich ihre Liste ändert).

  3. Defensive Kopien. Wenn eine Methode das Objekt des Aufrufers nicht verändern soll, mutiere den Parameter entweder gar nicht, oder kopiere ihn zuerst: Arrays.copyOf(xs, xs.length). Umgekehrt gilt: Wenn du ein internes Array oder eine Liste zurückgibst, können Aufrufer es über die Referenz mutieren, es sei denn, du gibst eine Kopie zurück.

Ein durchgearbeitetes Beispiel

java— editable, runs on the server

Was kommt als Nächstes

Du verstehst jetzt, wie einzelne Argumente in Methoden fließen. Manchmal weißt du im Voraus nicht, wie viele Argumente der Aufrufer übergeben wird — denk an String.format oder ein max(...), das beliebig viele Werte annimmt. Genau dafür sind varargs da.

Übungen

Übung
Eine Methode erhält einen Objektparameter und weist ihn neu zu: param = new Thing(). Was sieht der Aufrufer?
Eine Methode erhält einen Objektparameter und weist ihn neu zu: param = new Thing(). Was sieht der Aufrufer?
Was this page helpful?