Вступление
Этот вопрос часто возникает как в Интернете, так и когда кто-то хочет проверить ваши знания о том, как 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
передается, и любая операция
с копией не отражается на исходном значении) и "передача по значению"
-reference " (второй пример, передается ссылка, и при изменении она
отражает исходный объект).
В следующих разделах мы объясним, почему это неверно .
Как 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 примитивных типов:
byte
: Диапазон от-128
до127
включительно, 8-битное целое число со знакомshort
: диапазон от-32,768
до32,767
включительно, 16-битное целое число со знакомint
: варьируется от-2,147,483,648
147 483 648 до2,147,483,647
включительно, 32-разрядное целое число со знаком.long
: варьируется от -2 ^31^ до 2 ^31^ -1 включительно, 64-битное целое число со знакомfloat
: 32-битное целое число IEEE 754 с плавающей запятой одинарной точности с 6-7 значащими цифрами.double
: 64-битное целое число с плавающей запятой IEEE 754 двойной точности с 15 значащими цифрами.boolean
: двоичные значения,true
илиfalse
char
: диапазон от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 x = 0;
, Мы говорили Java ,
чтобы сохранить 4-байтовое пространство в стеке для int
, чтобы быть
сохранен в. int
не должен заполнить все 4 байта ( Integer.MAX_VALUE
), но все 4 байта будет доступен.
На это место в памяти затем обращается компилятор, когда вы хотите
использовать целое число x
. Имя x
- это то, что мы используем для
доступа к ячейке памяти в стеке. У компилятора есть собственные
внутренние ссылки на эти места.
После того, как мы передали x
incrementNumber()
и компилятор
достигает сигнатуры метода с параметром int x
он создает новую ячейку
/ пространство памяти в стеке.
Используемое нами имя переменной x
имеет большого значения для
компилятора. Мы можем даже пойти дальше и сказать, что int x
мы
объявили в методе main()
x_1
а int x
мы объявили в сигнатуре
метода, равен x_2
.
Затем мы увеличили значение целого числа x_2
в методе и затем
распечатали x_1
. Естественно, печатается значение, хранящееся в
ячейке памяти для x_1
и мы видим следующее:
0
Вот визуализация кода:
{.ezlazyload}
В заключение компилятор делает ссылку на место в памяти примитивных переменных.
Стек существует для каждого потока, который мы выполняем, и он используется для статического распределения памяти простых переменных, а также ссылок на объекты в куче (подробнее о куче в следующих разделах).
Вероятно, это то, что вы уже знали, и то, что знают все, кто ответил с первоначальным неверным утверждением. Самое большое заблуждение заключается в следующем типе данных.
Типы ссылок
Тип, используемый для передачи данных, - это ссылочный тип .
Когда мы объявляем и создаем экземпляры / инициализируем объекты (аналогично примитивным типам), на них создается ссылка - опять же, очень похоже на примитивные типы:
// Declaration and Instantiation/initialization
Object obj = new Object();
Опять же, мы также можем разделить этот процесс:
// Declaration
Object obj;
// Instantiation/initialization
obj = new Object();
Примечание: существует разница между созданием экземпляра и инициализацией . Создание экземпляра относится к созданию объекта и назначению ему места в памяти. Инициализация относится к заполнению полей этого объекта через конструктор после его создания.
Когда мы закончим с объявлением, переменная obj
станет ссылкой на
new
объект в памяти. Этот объект хранится в куче - в отличие от
примитивных типов, которые хранятся в стеке .
Всякий раз, когда объект создается, он помещается в кучу. Сборщик мусора очищает эту кучу от объектов, которые потеряли свои ссылки, и удаляет их, поскольку мы больше не можем до них добраться.
Значение по умолчанию для объектов после объявления - null
. Не
существует типа, который является instanceof
null
и не принадлежит
ни к какому типу или набору. Если ссылке не присвоено никакого значения,
например obj
, ссылка будет указывать на null
.
Допустим, у нас есть такой класс, как Employee
:
public class Employee {
String name;
String surname;
}
И создайте экземпляр класса как:
Employee emp = new Employee();
emp.name = new String("David");
emp.surname = new String("Landup");
Вот что происходит в фоновом режиме:
{.ezlazyload}
emp
указывает на объект в пространстве кучи. Этот объект содержит
ссылки на два String
которые содержат значения David
и Landup
.
Каждый раз, когда используется 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
на метод incrementSalary()
. Метод обращается к
int salary
объекта и увеличивает его на 100
. В итоге нас встречают:
1100
Это, безусловно, означает, что ссылка была передана между вызовом метода и самим методом, поскольку объект, к которому мы хотели получить доступ, действительно был изменен.
Неправильно . Так же , как с примитивными типами, мы можем идти
вперед и сказать , что есть два emp
переменных после того , как метод
был назван - emp_1
и emp_2
, в глаза компилятора.
Разница между примитивом x
мы использовали раньше, и emp
которую мы
используем сейчас, заключается в том, что и emp_1
и emp_2
указывают
на один и тот же объект в памяти .
Используя любую из этих двух ссылок, осуществляется доступ к одному и тому же объекту и изменяется одна и та же информация.
{.ezlazyload}
При этом это подводит нас к первоначальному вопросу.
Является ли Java «передачей по ссылке» или «передачей по значению»?
Java проходит по значению. Примитивные типы передаются по значению, ссылки на объекты передаются по значению.
Java не передает объекты. Он передает ссылки на объекты - поэтому, если кто-нибудь спросит, как Java передает объекты, ответ будет: «Нет». ^1^
В случае примитивных типов после передачи им выделяется новое пространство в стеке, и, таким образом, все дальнейшие операции с этой ссылкой связываются с новой ячейкой памяти.
В случае объектных ссылок после передачи создается новая ссылка , но указывающая на то же место в памяти.
[1. По словам Брайана Гетца, архитектора языка Java, работающего над проектами Valhalla и Amber. Вы можете прочитать об этом здесь .]{.small}