Автор оригинала: David Landup.
Вступление
Этот вопрос часто возникает как в Интернете, так и когда кто-то хочет проверить ваши знания о том, как Java обрабатывает переменные:
Использует ли Java “передачу по ссылке” или “передачу по значению” при передаче аргументов методам?
Это кажется простым вопросом (так оно и есть), но многие люди ошибаются, говоря:
Объекты передаются по ссылке, а примитивные типы передаются по значению.
Правильным утверждением было бы:
Ссылки на объекты передаются по значению, как и примитивные типы . Таким образом, Java во всех случаях передается по значению, а не по ссылке.
Это может показаться неинтуитивным для некоторых, так как обычно на лекциях демонстрируется разница между примером, подобным этому:
public static void main(String[] args) { int x = 0; incrementNumber(x); System.out.println(x); } public static void incrementNumber(int x) { x += 1; }
и такой пример, как этот:
public static void main(String[] args) { Number x = new Number(0); incrementNumber(x); System.out.println(x); } public static void incrementNumber(Number x) { x.value += 1; } public class Number { int value; // Constructor, getters and setters }
Первый пример будет напечатан:
0
В то время как второй пример будет напечатан:
1
Причина этой разницы часто понимается как “передача по значению” (первый пример, передается скопированное значение x
, и любая операция с копией не отразится на исходном значении) и “передача по ссылке” (второй пример, ссылка передается, и при изменении она отражает исходный объект).
В последующих разделах мы объясним почему это неверно .
Как Java Обрабатывает Переменные
Давайте обновим информацию о том, как Java обрабатывает переменные, поскольку это ключ к пониманию неправильного понимания. Заблуждение основано на истинных фактах, но немного искажено.
Примитивные типы
Java-это статически типизированный язык. Это требует, чтобы мы сначала объявили переменную, затем инициализировали ее, и только тогда мы сможем ее использовать:
// Declaring a variable and initializing it with the value 5 int i = 5; // Declaring a variable and initializing it with a value of false boolean isAbsent = false;
Вы можете разделить процесс объявления и инициализации:
// Declaration int i; boolean isAbsent; // Initialization i = 5; isAbsent = false;
Но если вы попытаетесь использовать неинициализированную переменную:
public static void printNumber() { int i; System.out.println(i); i = 5; System.out.println(i); }
Вас встречает ошибка:
Main.java:10: error: variable i might not have been initialized System.out.println(i);
Для локальных примитивных типов, таких как i
, нет значений по умолчанию . Хотя, если вы определяете глобальные переменные, такие как i
в этом примере:
static int i; public static void printNumber() { System.out.println(i); i = 5; System.out.println(i); }
Запустив это, вы увидите следующий вывод:
0 5
Переменная i
была выведена в виде 0
, хотя это еще не было назначено.
Каждый тип примитива имеет значение по умолчанию, если оно определено как глобальная переменная, и обычно это будут 0
для числовых типов и false
для логических значений.
В Java существует 8 примитивных типов:
байт
: Диапазоны от-128
чтобы127
включительно, 8-разрядное целое число со знакомкороткие
: Диапазоны от-32,768
чтобы32,767
включительно, 16-разрядное целое число со знакомint
: Диапазон от-2,147,483,648
чтобы2,147,483,647
включительно, 32-разрядное целое число со знакомдлинный
: Колеблется от -2 31 чтобы 2 31 -1 , включительно, 64-разрядное целое число со знакомfloat
: 32-разрядная точность с одинарной точностью IEEE 754 целое число с плавающей запятой с 6-7 значащими цифрамиdouble
: Двойная точность, 64-разрядное целое число IEEE 754 с плавающей запятой, с 15 значащими цифрамилогическое
: Двоичные значения,истина
илиложь
символ
: Колеблется от0
чтобы65,536
включающее 16-разрядное целое число без знака, представляющее символ Юникода
Передача Примитивных Типов
Когда мы передаем примитивные типы в качестве аргументов метода, они передаются по значению. Или, скорее, их значение копируется, а затем передается методу.
Давайте вернемся к первому примеру и разберем его:
public static void main(String[] args) { int x = 0; incrementNumber(x); System.out.println(x); } public static void incrementNumber(int x) { x += 1; }
Когда мы объявляем и инициализируем int;
, мы сказали Java, чтобы в стеке оставалось 4-байтовое пространство для хранения int
. int
не обязательно заполнять все 4 байта ( Целое число.MAX_VALUE
), но будут доступны все 4 байта.
На это место в памяти затем ссылается компилятор, когда вы хотите использовать целое число x
. Имя переменной x
– это то, что мы используем для доступа к ячейке памяти в стеке. Компилятор имеет свои собственные внутренние ссылки на эти местоположения.
Как только мы передали x
методу increment Number ()
, и компилятор достигает подписи метода с параметром int x
, он создает новую ячейку/пространство памяти в стеке.
Имя переменной, которое мы используем, x
, имеет мало значения для компилятора. Мы даже можем зайти так далеко, что скажем, что int x
, который мы объявили в методе main ()
, является x_1
, а int x
, который мы объявили в сигнатуре метода, является x_2
.
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
Затем мы увеличили значение целого числа x_2
в методе, а затем напечатали x_1
. Естественно, значение, сохраненное в ячейке памяти для x_1
, выводится на печать, и мы видим следующее:
0
Вот визуализация кода:
В заключение компилятор делает ссылку на расположение примитивных переменных в памяти.
Стек существует для каждого запущенного нами потока и используется для статического выделения памяти простых переменных, а также ссылок на объекты в куче (Подробнее о куче в последующих разделах).
Скорее всего, это то, что вы уже знали, и то, что знают все, кто ответил первоначальным неверным утверждением. Где кроется самое большое заблуждение, так это в следующем типе данных.
Ссылочные типы
Тип, используемый для передачи данных, – это ссылочный тип .
Когда мы объявляем и создаем/инициализируем объекты (аналогичные примитивным типам), для них создается ссылка – опять же, очень похожая на примитивные типы:
// Declaration and Instantiation/initialization Object obj = new Object();
Опять же, мы также можем разделить этот процесс на части:
// Declaration Object obj; // Instantiation/initialization obj = new Object();
Примечание: Существует разница между созданием экземпляра и инициализацией . Создание экземпляра относится к созданию объекта и назначению ему места в памяти. Инициализация относится к заполнению полей этого объекта через конструктор после его создания.
Как только мы закончим с объявлением, переменная obj
будет ссылкой на новый
объект в памяти. Этот объект хранится в куче – в отличие от примитивных типов, которые хранятся в стеке .
Всякий раз, когда создается объект, он помещается в кучу. Сборщик мусора очищает эту кучу от объектов, которые потеряли свои ссылки, и удаляет их, так как мы больше не можем до них добраться.
Значение по умолчанию для объектов после объявления равно null
. Не существует типа, который null
является экземпляром
, и он не принадлежит ни к какому типу или набору. Если ссылке не присвоено значение, например obj
, ссылка будет указывать на null
.
Допустим, у нас есть такой класс, как Сотрудник
:
public class Employee { String name; String surname; }
И создайте экземпляр класса как:
Employee emp = new Employee(); emp.name = new String("David"); emp.surname = new String("Landup");
Вот что происходит на заднем плане:
Ссылка emp
указывает на объект в пространстве кучи. Этот объект содержит ссылки на два объекта String
, которые содержат значения David
и Land up
.
Каждый раз, когда используется ключевое слово new
, создается новый объект.
Передача Ссылок На Объекты
Давайте посмотрим, что происходит, когда мы передаем объект в качестве аргумента метода:
public static void main(String[] args) { Employee emp = new Employee(); emp.salary = 1000; incrementSalary(emp); System.out.println(emp.salary); } public static void incrementSalary(Employee emp) { emp.salary += 100; }
Мы передали нашу emp
ссылку на метод увеличение заработной платы()
. Метод обращается к полю int
объекта и увеличивает его на 100
. В конце концов, нас встречают:
1100
Это, безусловно, означает, что ссылка была передана между вызовом метода и самим методом, поскольку объект, к которому мы хотели получить доступ, действительно был изменен.
Неправильно . То же самое как и в случае с примитивными типами, мы можем пойти дальше и сказать , что после вызова метода существуют две переменные temp
– emp_1
и emp_2
, на взгляд компилятора.
Разница между примитивом x
, который мы использовали раньше, и ссылкой emp
, которую мы используем сейчас, заключается в том, что оба emp_1
и emp_2
указывают на один и тот же объект в памяти .
Используя любую из этих двух ссылок, осуществляется доступ к одному и тому же объекту и изменяется одна и та же информация.
Тем не менее, это подводит нас к первоначальному вопросу.
Является ли Java “передачей по ссылке” или “передачей по значению”?
Java передает значение по значению. Примитивные типы передаются по значению, ссылки на объекты передаются по значению.
Java не передает объекты. Он передает ссылки на объекты – поэтому, если кто-нибудь спросит, как Java передает объекты, ответ будет: “это не так”. 1
В случае примитивных типов после передачи им выделяется новое пространство в стеке, и, таким образом, все дальнейшие операции с этой ссылкой связаны с новой ячейкой памяти.
В случае ссылок на объекты после передачи создается новая ссылка , но указывающая на ту же ячейку памяти.
1. По словам Брайана Гетца, архитектора языка Java, работающего над проектами Valhalla и Amber. Вы можете прочитать об этом подробнее здесь .