Может ли кто-нибудь объяснить мне, в чем причина передачи в Java «значения», а не «ссылки»? - PullRequest
8 голосов
/ 05 февраля 2009

Я довольно новичок в Java (пишу другие вещи много лет), и если я что-то упустил (и я счастлив, что я здесь не прав), то это фатальная ошибка ...

String foo = new String();
thisDoesntWork(foo);
System.out.println(foo);//this prints nothing

public static void thisDoesntWork(String foo){
   foo = "howdy";
}

Теперь, мне хорошо известна (довольно плохо сформулированная) концепция, что в java все передается по «значению», а не «ссылкой», но String является объектом и имеет всевозможные навороты, поэтому можно было бы ожидать, что в отличие от int, пользователь сможет работать с вещью, переданной в метод (и не застревать со значением, установленным перегруженным =).

Может кто-нибудь объяснить мне, что было за этим выбором дизайна? Как я уже сказал, я не собираюсь быть здесь, и, возможно, я упускаю что-то очевидное?

Ответы [ 15 ]

11 голосов
/ 05 февраля 2009

Эта напыщенная речь объясняет это лучше, чем я когда-либо мог попытаться

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

9 голосов
/ 05 февраля 2009

Когда вы передаете "foo", вы передаете ссылку в "foo" в качестве значения ThisDoesntWork (). Это означает, что когда вы выполняете присваивание «foo» внутри вашего метода, вы просто устанавливаете ссылку на локальную переменную (foo) в качестве ссылки на вашу новую строку.

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

  • Безопасность : Никто не может заклинить данные в вашей строке и вызвать ошибку переполнения буфера, если никто не сможет ее изменить!
  • Скорость : Если вы можете быть уверены, что ваши строки неизменны, вы знаете, что их размер всегда одинаков, и вам никогда не придется перемещать структуру данных в памяти при манипулировании Это. Вам (разработчику языка) также не нужно беспокоиться о реализации String как медленного связанного списка. Это сокращает оба пути, все же. Добавление строк только с помощью оператора + может быть дорогостоящим в памяти, и вам придется использовать объект StringBuilder, чтобы сделать это высокопроизводительным и эффективным способом памяти.

Теперь на ваш больший вопрос. Почему объекты проходят таким образом? Ну, если бы Java передавала вашу строку как то, что вы обычно называете «по значению», ей пришлось бы фактически скопировать всю строку перед передачей ее в вашу функцию. Это довольно медленно. Если она передаст строку по ссылке и позволит вам изменить ее (как это делает C), у вас возникнут проблемы, которые я только что перечислил.

5 голосов
/ 05 февраля 2009

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

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

String a = "hello";
String b = a;
String b = "howdy"

System.out.print(a) //prints hello

Чтобы получить последний оператор для вывода "привет", b должен указывать на ту же "дыру" в памяти, на которую указывает a (указатель). Это то, что вы хотите, когда вы хотите передать по ссылке. Есть несколько причин, по которым Java решила не идти в этом направлении:

  • Указатели сбивают с толку Разработчики Java пытались убрать некоторые из более запутанных вещей в других языках. Указатели являются одной из наиболее неправильно понятых и неправильно используемых конструкций C / C ++ наряду с перегрузкой операторов.

  • Указатели представляют собой угрозу безопасности Указатели вызывают много проблем безопасности при неправильном использовании. Вредоносная программа присваивает что-то этой части памяти, тогда то, что вы считали своим объектом, на самом деле принадлежит кому-то другому. (Java уже избавилась от самой большой проблемы безопасности, переполнения буфера, с проверенными массивами)

  • Утечка абстракции Когда вы начинаете работать именно с тем, «Что в памяти и где», ваша абстракция становится меньше абстракции. В то время как утечка абстракции почти наверняка проникает в язык, дизайнеры не хотели вносить его напрямую.

  • Объекты - все, что вас волнует В Java все является объектом, а не местом, которое занимает объект. Добавление указателей сделало бы пространство, в котором объект занимает важное место, хотя .......

Вы можете эмулировать то, что вы хотите, создав объект "Hole". Вы можете даже использовать дженерики, чтобы сделать его безопасным. Например:

public class Hole<T> {
   private T objectInHole;

   public void putInHole(T object) {
      this.objectInHole = object;
   }
   public T getOutOfHole() {
      return objectInHole;
   }

   public String toString() {
      return objectInHole.toString();
   }
   .....equals, hashCode, etc.
}


Hole<String> foo = new Hole<String)();
foo.putInHole(new String());
System.out.println(foo); //this prints nothing
thisWorks(foo);
System.out.println(foo);//this prints howdy

public static void thisWorks(Hole<String> foo){
   foo.putInHole("howdy");
}
4 голосов
/ 05 февраля 2009

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

Внутри метода вы фактически создаете локальную переменную (я назову ее "localFoo"), которая указывает на ту же ссылку, что и ваша исходная переменная ("originalFoo").

Когда вы назначаете "howdy" для localFoo, вы не меняете, куда указывает originalFoo.

Если вы сделали что-то вроде:

String a = "";
String b = a;
String b = "howdy"?

Ожидаете ли вы:

System.out.print(a)

распечатать "привет"? Распечатывает "".

Вы не можете изменить то, на что указывает originalFoo, изменив то, на что указывает localFoo. Вы можете изменить объект, на который указывают оба (если он не был неизменным). Например,

List foo = new ArrayList();
System.out.println(foo.size());//this prints 0

thisDoesntWork(foo);
System.out.println(foo.size());//this prints 1

public static void thisDoesntWork(List foo){   
    foo.add(new Object);
}
3 голосов
/ 05 февраля 2009

В Java все передаваемые переменные фактически передаются объектами, даже значениями. Все переменные, переданные методу, на самом деле являются копиями исходного значения. В случае вашего строкового примера исходный указатель (на самом деле это ссылка, но во избежание путаницы неправильно используется другое слово) копируется в новую переменную, которая становится параметром метода.

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

Некоторые преимущества включают в себя: - Нет необходимости делать защитные копии. - Threadsafe - не нужно беспокоиться о блокировке на случай, если кто-то еще захочет изменить объект.

2 голосов
/ 05 февраля 2009

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

Это не имеет ничего общего с неизменностью. Точно то же самое произошло бы с изменяемым ссылочным типом.

1 голос
/ 05 февраля 2009

Если бы мы сделали грубую аналогию C и ассемблера:

void Main()
{ 
     // stack memory address of message is 0x8001.  memory address of Hello is 0x0001.  
     string message = "Hello"; 
     // assembly equivalent of: message = "Hello";
     // [0x8001] = 0x0001

     // message's stack memory address
     printf("%d", &message); // 0x8001

     printf("%d", message); // memory pointed to of message(0x8001): 0x0001
     PassStringByValue(message); // pass the pointer pointed to of message.  0x0001, not 0x8001
     printf("%d", message); // memory pointed to of message(0x8001): 0x0001.  still the same

     // message's stack memory address doesn't change
     printf("%d", &message); // 0x8001
}

void PassStringByValue(string foo)
{
    printf("%d", &foo); // &foo contains foo's *stack* address (0x4001)

    // foo(0x4001) contains the memory pointed to of message, 0x0001
    printf("%d", foo);  // 0x0001
    // World is in memory address 0x0002
    foo = "World";  // on foo's memory address (0x4001), change the memory it pointed to, 0x0002
    // assembly equivalent of: foo = "World":
    // [0x4001] = 0x0002

    // print the new memory pointed by foo
    printf("%d", foo); // 0x0002

    // Conclusion: Not in any way 0x8001 was involved in this function.  Hence you cannot change the Main's message value.
    // foo = "World"  is same as [0x4001] = 0x0002

}

void Main()
{
     // stack memory address of message is 0x8001.  memory address of Hello is 0x0001.  
     string message = "Hello"; 
     // assembly equivalent of: message = "Hello";
     // [0x8001] = 0x0001

     // message's stack memory address
     printf("%d", &message); // 0x8001

     printf("%d", message); // memory pointed to of message(0x8001): 0x0001
     PassStringByRef(ref message); // pass the stack memory address of message.  0x8001, not 0x0001
     printf("%d", message); // memory pointed to of message(0x8001): 0x0002. was changed

     // message's stack memory address doesn't change
     printf("%d", &message); // 0x8001
}


void PassStringByRef(ref string foo)
{
    printf("%d", &foo); // &foo contains foo's *stack* address (0x4001)

    // foo(0x4001) contains the address of message(0x8001)
    printf("%d", foo);  // 0x8001
    // World is in memory address 0x0002
    foo = "World"; // on message's memory address (0x8001), change the memory it pointed to, 0x0002
    // assembly equivalent of: foo = "World":
    // [0x8001] = 0x0002;


    // print the new memory pointed to of message
    printf("%d", foo); // 0x0002

    // Conclusion: 0x8001 was involved in this function.  Hence you can change the Main's message value.
    // foo = "World"  is same as [0x8001] = 0x0002

}

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

Они бы предпочли, чтобы вы разработали целочисленный своппер с использованием объектов, а не предоставили бы поддержку первого класса для передачи по ссылке, то же самое для делегата (Гослинг чувствует себя не в своей тарелке с указателем на функцию, он предпочел бы добавить эту функциональность в объекты) и ENUM.

Они чрезмерно упрощают (все является объектом) язык в ущерб отсутствию поддержки первого класса для большинства языковых конструкций, например, на ум приходит передача по ссылке, делегатам, перечислениям, свойствам.

0 голосов
/ 05 февраля 2009

Это потому, что внутри "thisDoesntWork" вы эффективно уничтожаете локальное значение foo. Если вы хотите передать по ссылке таким образом, всегда можете инкапсулировать String внутри другого объекта, скажем, в массиве.

class Test {

    public static void main(String[] args) {
        String [] fooArray = new String[1];
        fooArray[0] = new String("foo");

        System.out.println("main: " + fooArray[0]);
        thisWorks(fooArray);
        System.out.println("main: " + fooArray[0]);
    }

    public static void thisWorks(String [] foo){
        System.out.println("thisWorks: " + foo[0]);
        foo[0] = "howdy";
        System.out.println("thisWorks: " + foo[0]);
    }
}

Результатом будет следующий вывод:

main: foo
thisWorks: foo
thisWorks: howdy
main: howdy
0 голосов
/ 05 февраля 2009

Если вы рассматриваете объект как просто поля в объекте, тогда объекты передаются по ссылке в Java, поскольку метод может изменять поля параметра, а вызывающая сторона может наблюдать за изменением. Однако, если вы также думаете об объекте как об его идентичности, тогда объекты передаются по значению, потому что метод не может изменить идентичность параметра так, как это может наблюдать вызывающая сторона. Поэтому я бы сказал, что Java передается по значению.

0 голосов
/ 05 февраля 2009

Это потому, что он создает локальную переменную внутри метода. что будет простым способом (который, я уверен, сработает) будет:

String foo = new String();    

thisDoesntWork(foo);    
System.out.println(foo); //this prints nothing

public static void thisDoesntWork(String foo) {    
   this.foo = foo; //this makes the local variable go to the main variable    
   foo = "howdy";    
}
...