Введение: вечная битва "по ссылке" vs "по значению"
Если вы хоть раз писали на Java и передавали массив или объект в метод, а потом удивлялись, почему исходные данные изменились — вы не одиноки. Этот вопрос мучает новичков (и не только) с самого появления языка. На Stack Overflow он набрал миллионы просмотров, а споры в комментариях не утихают годами.
Сегодня мы раз и навсегда разберемся: Java — это "pass-by-reference" или "pass-by-value"? Спойлер: ответ однозначен, но дьявол, как всегда, в деталях. Мы не просто дадим сухое определение, а покажем на реальных примерах, как работает передача аргументов в Java, и почему многие путаются.
Что говорят создатели Java?
Официальная документация и сам Джеймс Гослинг (создатель Java) не оставляют места для споров: Java всегда передает аргументы по значению (pass-by-value). Никаких исключений. Ни для примитивов, ни для объектов.
Но тогда почему код меняет объекты? Почему List или массив, переданный в метод, изменяется? Давайте разбираться.
Pass-by-value: что это вообще значит?
Для начала — четкое определение. Pass-by-value означает, что при вызове метода создается копия значения переменной, и именно эта копия передается в метод. Оригинальная переменная (в вызывающем коде) остается нетронутой.
Pass-by-reference — это когда методу передается ссылка на оригинальную переменную, и метод может напрямую менять её значение (например, подменять объект). В Java такого нет.
Теперь — к примерам.
Пример 1: примитивные типы (int, double, boolean)
С примитивами всё очевидно. Давайте напишем простой код:
public class Main {
public static void main(String[] args) {
int x = 10;
changePrimitive(x);
System.out.println("После метода x = " + x); // 10
}
public static void changePrimitive(int value) {
value = 20; // меняем копию
}
}
Вывод:
После метода x = 10
Всё логично: мы передали копию числа 10, внутри метода изменили копию на 20, но оригинал остался как был. Это чистый pass-by-value.
Пример 2: объекты (строки, списки, свои классы)
А теперь самое интересное. Смотрите:
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
changeStringBuilder(sb);
System.out.println("После метода: " + sb); // Hello World!
}
public static void changeStringBuilder(StringBuilder builder) {
builder.append(" World!"); // меняем содержимое объекта
}
}
Вывод:
После метода: Hello World!
«Ага! — скажете вы. — Объект изменился! Значит, это pass-by-reference!»
Нет. Это ловушка, в которую попадаются 90% новичков. Давайте разберем, что на самом деле произошло.
Что хранится в переменной sb?
В Java переменная объектного типа (например, StringBuilder, List, String) не хранит сам объект. Она хранит ссылку (адрес) на объект в памяти. Сам объект лежит где-то в куче (heap).
Когда вы передаете sb в метод, вы передаете копию этой ссылки. То есть копию адреса. И оригинал, и копия указывают на один и тот же объект. Поэтому, когда вы через копию ссылки меняете содержимое объекта, эти изменения видны и через оригинальную ссылку.
Но! Если вы попытаетесь присвоить новый объект параметру внутри метода, оригинальная ссылка не изменится. Докажем:
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
changeReference(sb);
System.out.println("После метода: " + sb); // Hello, а не Hi!
}
public static void changeReference(StringBuilder builder) {
builder = new StringBuilder("Hi"); // меняем копию ссылки
}
}
Вывод:
После метода: Hello
Вот оно! Если бы Java была pass-by-reference, то после вызова changeReference переменная sb указывала бы на новый объект "Hi". Но этого не произошло. Мы изменили только копию ссылки, оригинал остался прежним.
Пример 3: массивы — та же история
Массивы в Java — это тоже объекты. Работают точно так же:
public class Main {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
changeArray(arr);
System.out.println(arr[0]); // 99
}
public static void changeArray(int[] array) {
array[0] = 99; // меняем содержимое через копию ссылки
}
}
Вывод:
99
Но если попробовать присвоить новый массив:
public static void changeArray(int[] array) {
array = new int[]{100, 200, 300}; // копия ссылки перестала указывать на старый массив
}
Оригинал не изменится.
Почему путаница так распространена?
Есть несколько причин:
- Терминология C++. В C++ есть настоящий pass-by-reference (через
&). Программисты, пришедшие из C++, ожидают такого же поведения в Java. - Смешение понятий. Многие путают «ссылку на объект» (reference type) и «передачу по ссылке» (call-by-reference). Это разные вещи.
- Поведение объектов. Когда объект меняется внутри метода, это выглядит как pass-by-reference, хотя на самом деле это pass-by-value для ссылки.
- Неверные учебники. К сожалению, даже в некоторых книгах пишут «объекты передаются по ссылке». Это неверно.
Как запомнить раз и навсегда?
Используйте простую мантру:
Java всегда передает копию того, что лежит в переменной. Если переменная хранит примитив (например, 5) — передается копия числа. Если переменная хранит ссылку (адрес объекта) — передается копия этого адреса.
Или еще проще: Java — pass-by-value. Всегда. Точка.
Практический совет: как не допустить ошибок
Зная, что Java передает ссылки по значению, вы можете избежать многих багов:
- Не ждите, что метод подменит ваш объект. Если нужно заменить объект в вызывающем коде — возвращайте новый объек