Является ли Java «передачей по ссылке» или «передачей по значению»? - PullRequest
5963 голосов
/ 03 сентября 2008

Я всегда думал, что Java была передача по ссылке .

Однако я видел пару постов в блоге (например, этот блог ), в которых утверждается, что это не так.

Я не думаю, что понимаю разницу, которую они проводят.

Какое объяснение?

Ответы [ 79 ]

146 голосов
/ 08 сентября 2010

Не могу поверить, что никто еще не упомянул Барбару Лисков. Когда она спроектировала CLU в 1974 году, она столкнулась с той же проблемой терминологии и изобрела термин вызов путем разделения (также известный как вызов путем разделения объектов и вызов через объект ) для этого конкретного случая «вызов по значению, где значение является ссылкой».

109 голосов
/ 12 января 2009

Суть дела в том, что слово ссылка в выражении «передача по ссылке» означает нечто совершенно отличное от обычного значения слова ссылка в Java.

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

80 голосов
/ 21 ноября 2013

В Java все ссылки, поэтому, когда у вас есть что-то вроде: Point pnt1 = new Point(0,0); Java делает следующее:

  1. Создает новый объект Point
  2. Создает новую ссылку на точку и инициализирует эту ссылку на точку (см.) на ранее созданном объекте точки.
  3. Отсюда, через жизнь объекта Point, вы получите доступ к этому объекту через pnt1. ссылка. Таким образом, мы можем сказать, что в Java вы манипулируете объектом через его ссылку.

enter image description here

Java не передает аргументы метода по ссылке; он передает их по значению. Я буду использовать пример с этого сайта :

public static void tricky(Point arg1, Point arg2) {
  arg1.x = 100;
  arg1.y = 100;
  Point temp = arg1;
  arg1 = arg2;
  arg2 = temp;
}
public static void main(String [] args) {
  Point pnt1 = new Point(0,0);
  Point pnt2 = new Point(0,0);
  System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
  System.out.println(" ");
  tricky(pnt1,pnt2);
  System.out.println("X1: " + pnt1.x + " Y1:" + pnt1.y); 
  System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);  
}

Ход программы:

Point pnt1 = new Point(0,0);
Point pnt2 = new Point(0,0);

Создание двух разных объектов Point с двумя разными ссылками. enter image description here

System.out.println("X1: " + pnt1.x + " Y1: " +pnt1.y); 
System.out.println("X2: " + pnt2.x + " Y2: " +pnt2.y);
System.out.println(" ");

Ожидаемый результат будет:

X1: 0     Y1: 0
X2: 0     Y2: 0

В этой строке «проход по значению» входит в игру ...

tricky(pnt1,pnt2);           public void tricky(Point arg1, Point arg2);

Ссылки pnt1 и pnt2 передаются по значению в хитрый метод, что означает, что теперь ваши ссылки pnt1 и pnt2 имеют свои copies с именами arg1 и arg2 .So pnt1 и arg1 указывают на один и тот же объект. (То же самое для pnt2 и arg2) enter image description here

В методе tricky:

 arg1.x = 100;
 arg1.y = 100;

enter image description here

Далее в методе tricky

Point temp = arg1;
arg1 = arg2;
arg2 = temp;

Здесь вы сначала создаете новую ссылку temp Point, которая будет point в том же месте, что и ссылка arg1. Затем вы перемещаете ссылку arg1 в точку в то же место, что и ссылка arg2. Наконец arg2 будет указывать на то же место, что и temp.

enter image description here

Отсюда исчезает область действия метода tricky, и у вас больше нет доступа к ссылкам: arg1, arg2, temp. Но важно отметить, что все, что вы делаете с этими ссылками, когда они «в жизни», будет постоянно влиять на объект, на котором они указывают до.

Итак, после выполнения метода tricky, когда вы возвращаетесь к main, у вас возникает такая ситуация: enter image description here

Итак, теперь полностью выполнение программы будет:

X1: 0         Y1: 0
X2: 0         Y2: 0
X1: 100       Y1: 100
X2: 0         Y2: 0
76 голосов
/ 17 октября 2013

Java всегда передается по значению, а не по ссылке

Прежде всего, нам нужно понять, что такое передача по значению и передача по ссылке.

Передача по значению означает, что вы делаете копию в памяти фактического значения параметра, которое передается. Это копия содержимого фактического параметра .

Передача по ссылке (также называемая передачей по адресу) означает, что копия адреса фактического параметра сохраняется .

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

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeValue(t);
        System.out.println(t.name);
    }

    public void changeValue(Test f) {
        f.name = "changevalue";
    }
}

class Test {
    String name;
}

Вывод этой программы:

changevalue

Давайте разберемся шаг за шагом:

Test t = new Test();

Как мы все знаем, он создаст объект в куче и вернет эталонное значение обратно в t. Например, предположим, что значение t равно 0x100234 (мы не знаем фактическое внутреннее значение JVM, это всего лишь пример).

first illustration

new PassByValue().changeValue(t);

При передаче ссылки t в функцию она не будет напрямую передавать фактическое значение ссылки теста объекта, но создаст копию t и затем передаст ее функции. Поскольку значение передается по значению , оно передает копию переменной, а не фактическую ссылку на нее. Поскольку мы сказали, что значение t было 0x100234, и t, и f будут иметь одинаковое значение, и, следовательно, они будут указывать на один и тот же объект.

second illustration

Если вы измените что-либо в функции, используя ссылку f, это изменит существующее содержимое объекта. Вот почему мы получили вывод changevalue, который обновляется в функции.

Чтобы понять это более четко, рассмотрим следующий пример:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeRefence(t);
        System.out.println(t.name);
    }

    public void changeRefence(Test f) {
        f = null;
    }
}

class Test {
    String name;
}

Будет ли это NullPointerException? Нет, потому что он передает только копию ссылки. В случае передачи по ссылке он мог выдать NullPointerException, как показано ниже:

third illustration

Надеюсь, это поможет.

68 голосов
/ 13 декабря 2013

При представлении ссылка всегда является значением, независимо от того, какой язык вы используете.

Получив внешний вид коробки, давайте посмотрим на сборку или некоторое низкоуровневое управление памятью. На уровне ЦП ссылка на что-либо немедленно становится значением , если оно записывается в память или в один из регистров ЦП. (Вот почему указатель является хорошим определением. Это значение, которое одновременно имеет цель).

Данные в памяти имеют Местоположение , и в этом месте есть значение (байт, слово, что угодно). В ассемблере у нас есть удобное решение: Name для определенного Location (она же переменная), но при компиляции кода ассемблер просто заменяет Name на указанное местоположение так же, как ваш браузер заменяет доменные имена на IP-адреса.

Вплоть до сути технически невозможно передать ссылку на что-либо на любом языке, не представляя его (когда оно сразу становится значением).

Допустим, у нас есть переменная Foo, ее Местоположение находится на 47-м байте в памяти, а ее Значение равно 5. У нас есть другая переменная Ref2Foo , которая находится в памяти в 223-м байте, и его значение будет 47. Этот Ref2Foo может быть технической переменной, явно не созданной программой. Если вы просто посмотрите на 5 и 47 без какой-либо другой информации, вы увидите только два значения . Если вы используете их в качестве ссылок, чтобы добраться до 5, мы должны путешествовать:

(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223]  -> 47
(Foo)[47]       -> 5

Вот как работают таблицы переходов.

Если мы хотим вызвать метод / функцию / процедуру со значением Foo, существует несколько возможных способов передачи переменной в метод, в зависимости от языка и нескольких режимов вызова его метода:

  1. 5 копируется в один из регистров ЦП (т. Е. EAX).
  2. 5 возвращает PUSHd в стек.
  3. 47 копируется в один из регистров процессора
  4. 47 PUSHd в стек.
  5. 223 копируется в один из регистров ЦП.
  6. 223 возвращает PUSHd в стек.

В каждом случае выше указанного значения - копия существующего значения - было создано, теперь его должен обработать метод получения. Когда вы пишете «Foo» внутри метода, он либо считывается из EAX, либо автоматически разыменовывается , либо дважды разыменовывается, процесс зависит от того, как работает язык и / или от того, что диктует тип Foo. Это скрыто от разработчика, пока она не обходит процесс разыменования. Таким образом, ссылка - это значение , когда оно представлено, потому что ссылка - это значение, которое должно быть обработано (на уровне языка).

Теперь мы передали Foo методу:

  • в случае 1. и 2. если вы измените Foo (Foo = 9), это повлияет только на локальную область, поскольку у вас есть копия значения. Изнутри метода мы даже не можем определить, где в памяти находился оригинальный Foo.
  • в случаях 3. и 4. Если вы используете языковые конструкции по умолчанию и изменяете Foo (Foo = 11), это может изменить Foo глобально (зависит от языка, т. Е. Java или как у Паскаля procedure findMin(x, y, z: integer; var m : integer);). Однако, если язык позволяет обойти процесс разыменования, вы можете изменить 47, скажем на 49. В этот момент, кажется, Foo изменился, если вы прочитали его, потому что вы изменили локальный указатель на него. И если вы захотите изменить этот Foo внутри метода (Foo = 12), вы, вероятно, откажетесь от выполнения программы (иначе, от segfault), потому что вы будете писать в память, отличную от ожидаемой, вы даже можете изменить область, предназначенную для этого. хранение исполняемой программы и ее запись изменят выполняемый код (Foo теперь не в 47). НО значение Foo 47 не изменилось глобально, только значение внутри метода, потому что 47 также было копией метода.
  • в случаях 5. и 6. Если вы измените 223 внутри метода, он создаст тот же хаос, что и в 3. или 4. (указатель, указывающий на неверное значение, которое снова используется в качестве указателя) но это все еще локальная проблема, так как 223 было скопировано . Однако, если вы можете разыменовать Ref2Foo (то есть 223), достичь и изменить указанное значение 47, скажем, на 49, это повлияет на Foo глобально , потому что в в этом случае методы получили копию 223, но ссылка 47 существует только один раз, и изменение ее на 49 приведет к тому, что каждая Ref2Foo двойная разыменование приведет к неправильному значению.

Проницание незначительными деталями, даже языки, которые передают по ссылке, будут передавать значения в функции, но эти функции знают, что они должны использовать это для разыменования. Эта передача-ссылка-как-значение просто скрыта от программиста, потому что она практически бесполезна, а терминология - только передача по ссылке .

Strict передача по значению также бесполезна, это будет означать, что 100-мегабайтный массив должен копироваться каждый раз, когда мы вызываем метод с массивом в качестве аргумента, поэтому Java не может быть строго передана -по-значение. Каждый язык передает ссылку на этот огромный массив (в качестве значения) и использует механизм копирования при записи, если этот массив может быть изменен локально внутри метода, или позволяет методу (как это делает Java) изменять массив глобально (из представление вызывающего абонента) и несколько языков позволяет изменять значение самой ссылки.

Таким образом, вкратце и в собственной терминологии Java, Java это передача по значению , где значение может быть: либо действительное значение , либо значение , которое является представлением ссылки .

58 голосов
/ 08 апреля 2017

Java - это вызов по значению

Как это работает

  • Вы всегда передаете копию битов значения ссылки!

  • Если это тип данных примитива, эти биты содержат значение самого типа данных примитива, поэтому, если мы изменим значение заголовка внутри метода, то это не отразит изменения снаружи.

  • Если это тип данных объекта, например Foo foo = new Foo () , то в этом случае копия адреса объекта проходит как ярлык файла, предположим, у нас есть текстовый файл abc.txt в C: \ desktop и предположим, что мы создаем ярлык для того же файла и помещаем его в C: \ desktop \ abc-shortcut , чтобы при доступе к файл из C: \ desktop \ abc.txt и запишите 'Переполнение стека' и закройте файл, и снова вы откроете файл из ярлыка, после чего напишите '- самый большой онлайн-сообщество для программистов, которые изучат ', тогда общее изменение файла будет ' Stack Overflow - это крупнейшее онлайн-сообщество для программистов, которое изучит ', что означает, что не имеет значения, откуда вы открываете файл, каждый когда мы обращались к одному и тому же файлу, здесь мы можем принять Foo в качестве файла и предположить, что foo хранится в 123hd7h (исходный адрес, такой как C: \ desktop \ abc.txt ) адрес и 234jdid (скопированный адрес li ke C: \ desktop \ abc-shortcut , который фактически содержит оригинальный адрес файла внутри). Так что для лучшего понимания сделайте ярлык файла и почувствуйте ...

52 голосов
/ 12 октября 2012

Нет, ссылка не передается.

Java передается по значению в соответствии со спецификацией языка Java:

Когда вызывается метод или конструктор (§15.12), значения фактических выражений аргумента инициализируют вновь созданные переменные параметра , каждый из объявленного типа, перед выполнением тела метода или конструктора , Идентификатор, который появляется в DeclaratorId, может использоваться как простое имя в теле метода или конструктора для ссылки на формальный параметр .

52 голосов
/ 02 апреля 2009

Насколько я знаю, Java знает только вызов по значению. Это означает, что для примитивных типов данных вы будете работать с копией, а для объектов вы будете работать с копией ссылки на объекты. Однако я думаю, что есть некоторые подводные камни; например, это не будет работать:

public static void swap(StringBuffer s1, StringBuffer s2) {
    StringBuffer temp = s1;
    s1 = s2;
    s2 = temp;
}


public static void main(String[] args) {
    StringBuffer s1 = new StringBuffer("Hello");
    StringBuffer s2 = new StringBuffer("World");
    swap(s1, s2);
    System.out.println(s1);
    System.out.println(s2);
}

Это заполнит Hello World, а не World Hello, потому что в функции подкачки вы используете copys, которые не влияют на ссылки в основном. Но если ваши объекты не являются неизменяемыми, вы можете изменить их, например:

public static void appendWorld(StringBuffer s1) {
    s1.append(" World");
}

public static void main(String[] args) {
    StringBuffer s = new StringBuffer("Hello");
    appendWorld(s);
    System.out.println(s);
}

Это заполнит Hello World в командной строке. Если вы измените StringBuffer на String, он выдаст только Hello, потому что String неизменен. Например:

public static void appendWorld(String s){
    s = s+" World";
}

public static void main(String[] args) {
    String s = new String("Hello");
    appendWorld(s);
    System.out.println(s);
}

Однако вы можете создать оболочку для String, например, такую, которая позволила бы использовать ее со строками:

class StringWrapper {
    public String value;

    public StringWrapper(String value) {
        this.value = value;
    }
}

public static void appendWorld(StringWrapper s){
    s.value = s.value +" World";
}

public static void main(String[] args) {
    StringWrapper s = new StringWrapper("Hello");
    appendWorld(s);
    System.out.println(s.value);
}

edit: я считаю, что это также причина для использования StringBuffer, когда дело доходит до «добавления» двух строк, потому что вы можете модифицировать исходный объект, чего нельзя сделать с неизменными объектами, такими как String.

48 голосов
/ 13 мая 2015

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

/ **

Передать по значению

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

* /

Пример 1:

public class PassByValueString {
    public static void main(String[] args) {
        new PassByValueString().caller();
    }

    public void caller() {
        String value = "Nikhil";
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

Результат

output : output
value : Nikhil
valueflag : false

Пример 2:

/ ** * * Передать по значению * * /

public class PassByValueNewString {
    public static void main(String[] args) {
        new PassByValueNewString().caller();
    }

    public void caller() {
        String value = new String("Nikhil");
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

Результат

output : output
value : Nikhil
valueflag : false

Пример 3:

/ ** Это «Pass By Value» имеет ощущение «Pass By Reference»

Некоторые люди говорят, что примитивные типы и «String» - это «проход по значению» и объекты «передаются по ссылке».

Но из этого примера мы можем понять, что это фактически передача только по значению, имея в виду, что здесь мы передаем ссылку в качестве значения. т.е. ссылка передается по значению. Вот почему могут измениться, и все же это справедливо после локальной области. Но мы не можем изменить фактическую ссылку за пределы исходной области. что это означает, продемонстрировано на следующем примере PassByValueObjectCase2.

* /

public class PassByValueObjectCase1 {

    private class Student {
        int id;
        String name;
        public Student() {
        }
        public Student(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        new PassByValueObjectCase1().caller();
    }

    public void caller() {
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student);
    }

    public String method(Student student) {
        student.setName("Anand");
        return "output";
    }
}

Результат

output : output
student : Student [id=10, name=Anand]

Пример 4:

/ **

В дополнение к тому, что было упомянуто в примере 3 (PassByValueObjectCase1.java), мы не можем изменить фактическую ссылку за пределы исходной области.

Примечание: я не вставляю код для private class Student. Определение класса для Student такое же, как в примере 3.

* /

public class PassByValueObjectCase2 {

    public static void main(String[] args) {
        new PassByValueObjectCase2().caller();
    }

    public void caller() {
        // student has the actual reference to a Student object created
        // can we change this actual reference outside the local scope? Let's see
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student); // Will it print Nikhil or Anand?
    }

    public String method(Student student) {
        student = new Student(20, "Anand");
        return "output";
    }

}

Результат

output : output
student : Student [id=10, name=Nikhil]
46 голосов
/ 08 марта 2009

Вы не можете передавать по ссылке в Java, и один из очевидных способов - это когда вы хотите вернуть более одного значения из вызова метода. Рассмотрим следующий фрагмент кода на C ++:

void getValues(int& arg1, int& arg2) {
    arg1 = 1;
    arg2 = 2;
}
void caller() {
    int x;
    int y;
    getValues(x, y);
    cout << "Result: " << x << " " << y << endl;
}

Иногда вы хотите использовать тот же шаблон в Java, но вы не можете; по крайней мере, не напрямую. Вместо этого вы можете сделать что-то вроде этого:

void getValues(int[] arg1, int[] arg2) {
    arg1[0] = 1;
    arg2[0] = 2;
}
void caller() {
    int[] x = new int[1];
    int[] y = new int[1];
    getValues(x, y);
    System.out.println("Result: " + x[0] + " " + y[0]);
}

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

...