воскресенье, 3 марта 2013 г.

Передача объектов в Java – по ссылке или по значению?


В Интернете до сих пор ломают копья в жарких спорах о том, как передаются объекты в качестве параметров в методы в языке Java. Кто-то говорит, что объекты передаются по ссылке, кто-то, что по значению, а кто-то — что передаётся ссылка по значению. Эта статья призвана пролить свет на причину этих споров, и поставить, наконец, в них точку.

Ссылки в Java

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

Пару слов об указателях и их отсутствии в Java. На самом деле, указатели в Java имеются, но они работают на уровне виртуальной машины, и в синтаксисе языка не реализованы. Именно оттуда, с нижнего уровня, в случае обращения по нулевой ссылке генерируется исключение NullPointerException, которое переводится именно как «исключение нулевого указателя».

Передача параметров в метод

Давайте разберёмся, как вообще может передаваться что-либо в метод? Очевидно, что только двумя путями — либо путём передачи фактического значения передаваемой сущности (достигается созданием копии этой сущности внутри метода), либо путём передачи числа, интерпретируемого, как адрес памяти, по которому эта сущность фактически располагается (внутри метода, естественно, тоже создаётся копия этого числа). Первый вариант называется передачей параметра по значению, второй — по адресу (по ссылке, указателю или иной адресной сущности — это уже детали). Всё. В параметре либо сам объект, либо адрес памяти.

Поскольку поведение ссылок в Java не совсем совпадает с поведением классических ссылок, привычных, например, «сишникам», коих, наверное, большинство среди программистов, родилось определение, выносящее новичкам мозг: «передача ссылки по значению» вместо всем понятного «передача параметра по ссылке». В чём же разница? Что этим хотят подчеркнуть искушённые программописатели?

В общих чертах разница между этими понятиями в том, что любая Java-ссылка, в том числе и переданная в метод, ведёт себя не как классическая ссылка в других языках, которую единожды создав, невозможно переопределить, а отчасти как указатель, т.е. как переменная, просто хранящая значение адреса, которое всегда можно поменять. Кроме того, мы знаем, что при передаче параметров в метод создаются копии этих параметров. Так что получается, что если внутри метода мы попытаемся переопределить переданную ссылку, то внешняя ссылка не изменится, а поменяется только значение её копии внутри метода. Всё так же, как и с параметрами, переданными по значению. Вот этот нюанс и хотят подчеркнуть, когда говорят о передаче ссылки по значению.

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

Примеры

Проиллюстрирую сказанное на примерах. Ниже представлен классический пример неудачной попытки переопределить ссылку внутри метода:
public class HelloWorld {

    public static void main(String[] argvc) {
        String test = new String("Hello World!");
        changeIt(test);
        System.out.println("После изменения: " + test);
    }

    static void changeIt(String value) {
        value = new String("Hello!");
    }
}
В первой строке основного метода main мы создали новую строку "Hello World!". Во второй строке мы вызываем метод changeIt, передавая ему эту строку для изменения. Поскольку строки в Java – это объекты, а объекты в Java всегда передаются по ссылке, то строка эта передалась в метод changeIt тоже по ссылке — во внутренней переменной value находится ссылка (в терминологии Java) на строку "Hello World!".

Внутри метода changeIt первой и единственной строкой мы создаём новую строку "Hello!", и думаем, что записываем её адрес во внешнюю ссылку, переданную внутрь метода, ранее содержащую строку "Hello World!". Либо мы можем подумать, что таким образом мы записываем вновь созданный объект строки по адресу, хранящемуся в этой ссылке.

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

Таким образом, вышеприведённый такой код записывает адрес новой строки не во внешнюю ссылку, а в копию этой ссылки, автоматически созданную внутри метода во время передачи параметров. Именно поэтому третья строка основного метода распечатает старое значение "Hello World!"

Тот же пример, но ещё менее очевидный:
public class HelloWorld {

    public static void main(String[] argvc) {
        String test = "Hello World!";
        changeIt(test);
        System.out.println("После изменения : " + test);
    }

    static void changeIt(String value) {
        value = "Hello!";
    }
}
Здесь мы проделаем всё то же самое, но отсутствие явной команды создания нового объекта строки может ввести в заблуждение, что новый объект не создаётся, а редактируется старый. Это было бы так, и код бы работал так, как задумывалось, если бы объекты строк были бы изменяемы (мутабельны [mutable], как любят говорить программисты). Но в Java строки – это константы (неизменяемые объекты [Immutable]). Любым образом пытаясь изменить содержание строки мы фактически создаём новый объект строки по новому адресу памяти. Поэтому, пытаясь записать новое слово "Hello!" в старый объект, мы фактически создаём новый объект строки, адрес которой просто присваиваем копии переданной ссылки, который просто теряется при выходе из этого метода.

Итак, как же правильно называть способ передачи объекта в Java? Называйте его в терминах Java — просто передачей по ссылке. По крайней мере, это не будет вносить путаницы в головы новичков или программистов, у которых Java – первый язык.

4 комментария:

  1. Этот комментарий был удален автором.

    ОтветитьУдалить
  2. Так и не приведено примера как же всё таки правильно передавать строку по ссылке, изменяя ее внутри метода.

    ОтветитьУдалить
    Ответы
    1. Цель статьи - внеси ясность в терминологию. Как правильно изменить строку, переданную по ссылке - навскидку не скажу, надо опять копаться.

      Удалить