Передача параметров по значению или по ссылке в Java

При переходе на Java после изучения других си-подобных (и не только) языков программирования возникает вопрос о способах передачи аргументов в методы в Java. Плюс это довольно распространённый теоретический вопрос, который, кроме всего прочего, при неправильном понимании может привести появлению логических ошибок в коде.

Передача в методы аргументов примитивных типов


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

public static void main(String[] args) { int arg = 1; testArgPass(arg); System.out.println(arg);}
private static void testArgPass(int param) { param = 0;}
Данный код выведет в консоль 1, так как внутри метода testArgPass хоть и менялось значение переменной param, на исходное значение переменной arg из метода main это не оказало никакого влияния.
И действительно, при вызове метода testArgPass машина скопировала значение arg, то есть 1, и присвоила эту копию (ещё одну единицу) переменной param вызываемого метода. Дальше с этой переменной param может происходить всё что угодно, на arg это не повлияет никак.

Передача в методы аргументов ссылочных типов


С ссылочными типами ситуация аналогичная, передаются значения ссылок. Однако, именно в этом месте часто происходит непонимание важного нюанса, который сильно путает начинающих программистов.
Значение ссылочной переменной является ссылка, то есть число, которое представляет собой адрес в памяти машины, по которому располагается какой-то объект. Например:
Object obj = new Object();
Может показаться, что мы имеем дело с двумя сущностями, переменной obj и фактическим объектом в куче. Так работают примитивы: имя переменной + значение. Но в этом примере есть три сущности:
  • имя переменной (obj);
  • адрес в памяти машины (например, 4b67cf4d, или другое число в зависимости от машины)
  • реальный объект в памяти

obj -> 4b67cf4d -> new Object()
Так вот значением переменной obj является адрес (ссылка), то есть 4b67cf4d (это, на самом деле хэшкод, но представим, что это адрес), а не объект. Именно этот адрес будет скопирован и передан в метод в качестве аргумента.
Обычно объяснение в духе: "В Java аргументы всегда передаются по значению, но так как значением ссылочной переменной является ссылка, то она и передаётся, а это выглядит так, будто значение передалось по ссылке" воспринимается тяжело и мало помогает (хотя оно в принципе верное). Поэтому стоит помнить трёхкомпонентную структуру переменной объектного типа (или переменной-массива).

Изменение ссылки внутри другого метода


Здесь повторяется история с примитивами. Например:
public static void main(String[] args) {
String one = "one"; String two = "two"; System.out.println(one);}
private static void testArgPass(String param1, String param2) { param1 = param2;}
Переменная one ссылается на объект "one". Но мы уже знаем, что ей это в некотором смысле неважно, она просто имеет значение, например, 4b67cf4d а по этому адресу уже лежит объект. Именно это значение 4b67cf4d и будет скопировано в param1 при вызове метода testArgPass.
Дальше, что бы ни происходило в методе testArgPass повлиять на значение one (равное 4b67cf4d) это уже не может никак. Манипуляции типа param1 = param2 используются в тестах, просто чтобы запутать отвечающего. Она запишет в param1 другой адрес, например, 7ea987ac, но к one это уже не будет иметь никакого отношения.

Изменение состояния объекта внутри другого метода


Всё вышесказанное не означает, что передача ссылочной переменной в метод не может иметь побочных (пусть даже желательных) эффектов. Ведь внутри метода можно изменить состояние объекта. Тогда внешние переменные, разумеется, продолжат ссылаться на те же объекты, но сами объекты могут оказаться "не теми, что раньше". Например:
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello"); sbProcessor(sb); System.out.println(sb);}
private static void sbProcessor(StringBuilder param) { param.delete(0, param.length()); param.append("bye");}
В методе main мы создаём объект StringBuilder, в котором хранится строка "hello". Таково его состояние: "hello". Далее мы передаём ссылку на этот объект в метод sbProcessor. В этом методе с самой ссылкой никто ничего не пытается делать, никто её не меняет, ничего ничему не присваивает. Но. В нём меняется состояние объекта: удаляются все символы, которые он содержит, и в него помещается строка "bye".
Таким образом после вызова sbProcessor объект, на который ссылается sb из main пребывает в другом состоянии. Теперь его состояние: "bye". И именно в этом состоянии он и будет передан в System.out.println.