Java: передача по ссылке или по значению? Полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: вечная битва "по ссылке" 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 передает ссылки по значению, вы можете избежать многих багов:

  • Не ждите, что метод подменит ваш объект. Если нужно заменить объект в вызывающем коде — возвращайте новый объек

Блоги

Рекомендаций по книгам