W3docs

Java: передача по значению vs. по ссылке

Почему Java всегда передаёт по значению, даже при передаче ссылок на объекты, и что это означает на практике.

Java всегда передаёт аргументы по значению. Что бы вы ни передавали в метод — int, String, пользовательский объект или массив — метод получает копию переданного значения. Это значение может быть числом или ссылкой на объект, но оно всё равно остаётся копией.

Это вводит многих в заблуждение, потому что метод может изменять содержимое переданного объекта, и эти изменения видны вызывающему коду. Поэтому кажется, что объект передаётся по ссылке. Но это не так. На самом деле передаётся копия ссылки. В этой главе объясняется, что именно это означает.

На этой странице рассказывается о том, как примитивы, объекты, массивы и строки ведут себя при передаче в метод, предлагается ментальная модель, объясняющая каждый случай, и описываются практические последствия для ваших собственных параметров метода.

Примитивы — всё просто

При передаче примитива его значение копируется в параметр:

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

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

Метод работает со своей собственной переменной n, отдельной от x. Присвоение нового значения n не влияет на x. Здесь все согласны, что это передача по значению.

Аргументы-объекты: копируется ссылка

Для объектных типов переменная содержит не сам объект, а ссылку (адрес, указывающий на объект где-то в памяти). Когда вы передаёте эту переменную в метод, Java копирует ссылку, а не объект:

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

Обе переменные теперь указывают на один и тот же объект Dog. Именно поэтому изменение объекта через параметр видно в точке вызова:

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

Но присвоение новой ссылки параметру не изменяет переменную вызывающего кода:

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

Метод обновил свою собственную копию ссылки. Переменная rex вызывающего кода по-прежнему указывает на исходный объект Dog.

Ментальная модель двух указателей

Когда метод принимает параметр-объект, представьте две стрелки в начале: одна от переменной вызывающего кода, другая от параметра метода — обе указывают на один и тот же объект.

  • Изменение объекта через любую из стрелок затрагивает то, что видит другая стрелка.
  • Перенаправление одной стрелки в другое место не влияет на другую стрелку.

Это единственное правило отвечает на все вопросы «является ли Java передачей по ссылке?».

Массивы подчиняются тому же правилу

Массивы являются объектами, поэтому при передаче массива в метод копируется ссылка, а не содержимое:

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

Метод изменил элемент через свою копию ссылки, и вызывающий код видит изменение, потому что обе ссылки указывают на один и тот же массив.

Но присвоение параметру совершенно нового массива не влияет на вызывающий код:

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

Строки: иммутабельность скрывает проблему

Строки тоже являются объектами, но они иммутабельны — нет метода, который изменял бы содержимое String. Поэтому случай мутации просто невозможен:

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() возвращает новый объект String; присвоение его переменной s обновляет только параметр. Переменная name по-прежнему указывает на исходную строку "ada". Чтобы «изменить» строку, верните новую и позвольте вызывающему коду присвоить её.

Почему это важно

Три практических следствия:

  1. Метод не может «изменить снаружи» примитив или переназначить ссылку вызывающего кода. Если вам нужен такой эффект, возвращайте новое значение: x = doubleIt(x); — или используйте объект-обёртку, который вызывающий код сможет прочитать после вызова.

  2. Метод может изменять общий объект — что иногда именно то, что нужно (заполнение массива, наполнение списка), а иногда является неожиданностью (вызывающий код не ожидает, что его список изменится).

  3. Защитное копирование. Если метод не должен изменять объект вызывающего кода, либо не мутируйте параметр, либо сначала скопируйте его: Arrays.copyOf(xs, xs.length). И наоборот: если вы возвращаете внутренний массив или список, вызывающий код может изменить его через ссылку, если только вы не вернёте копию.

Наглядный пример

java— editable, runs on the server

Что дальше

Вы разобрались, как отдельные аргументы передаются в методы. Иногда заранее неизвестно, сколько аргументов передаст вызывающий код — как в String.format или в max(...), принимающем произвольное количество значений. Для этого существуют varargs.

Практика

Практика
Метод получает параметр-объект и переназначает его: param = new Thing(). Что видит вызывающий код?
Метод получает параметр-объект и переназначает его: param = new Thing(). Что видит вызывающий код?
Was this page helpful?