Рубрики
Без рубрики

Является ли Java “передачей по ссылке” или “передачей по значению”?

Этот вопрос часто всплывает в Интернете, и многие ответы основаны на неправильном представлении о том, как Java обрабатывает примитивные и ссылочные типы. В этой статье мы развенчаем это заблуждение.

Автор оригинала: 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

Это, безусловно, означает, что ссылка была передана между вызовом метода и самим методом, поскольку объект, к которому мы хотели получить доступ, действительно был изменен.

Неправильно . То же самое как и в случае с примитивными типами, мы можем пойти дальше и сказать , что после вызова метода существуют две переменные tempemp_1 и emp_2 , на взгляд компилятора.

Разница между примитивом x , который мы использовали раньше, и ссылкой emp , которую мы используем сейчас, заключается в том, что оба emp_1 и emp_2 указывают на один и тот же объект в памяти .

Используя любую из этих двух ссылок, осуществляется доступ к одному и тому же объекту и изменяется одна и та же информация.

Тем не менее, это подводит нас к первоначальному вопросу.

Является ли Java “передачей по ссылке” или “передачей по значению”?

Java передает значение по значению. Примитивные типы передаются по значению, ссылки на объекты передаются по значению.

Java не передает объекты. Он передает ссылки на объекты – поэтому, если кто-нибудь спросит, как Java передает объекты, ответ будет: “это не так”. 1

В случае примитивных типов после передачи им выделяется новое пространство в стеке, и, таким образом, все дальнейшие операции с этой ссылкой связаны с новой ячейкой памяти.

В случае ссылок на объекты после передачи создается новая ссылка , но указывающая на ту же ячейку памяти.

1. По словам Брайана Гетца, архитектора языка Java, работающего над проектами Valhalla и Amber. Вы можете прочитать об этом подробнее здесь .