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

StackOverflow https://stackoverflow.com/questions/514478

Вопрос

Я довольно новичок в 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 пользователь сможет оперировать вещью, которая передана в метод (и не застрять со значением, установленным overloaded =).

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

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

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

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

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

Чтобы заставить последнюю инструкцию напечатать "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");
}

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

Внутри метода вы фактически создаете локальную переменную (я буду называть ее "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);
}

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

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

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

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

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

Если бы мы провели грубую аналогию с 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 все передается по значению, заключается в том, что разработчики языка хотят упростить язык и сделать все выполняемым в стиле ООП.

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

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

Вы уверены, что он выводит значение null?Я думаю, что он будет просто пустым, так как при инициализации переменной foo вы предоставили пустую строку.

Присвоение foo в методе thisDoesntWork не изменяет ссылку на переменную foo, определенную в классе, поэтому foo в System.out.println(foo) по-прежнему будет указывать на старый пустой объект string.

Дэйв, ты должен простить меня (ну, я думаю, ты не "должен", но я бы предпочел, чтобы ты это сделал), но это объяснение не слишком убедительно.Выигрыш в безопасности довольно минимален, поскольку любой, кому нужно изменить значение строки, найдет способ сделать это с помощью какого-нибудь уродливого обходного пути.А скорость?!Вы сами (совершенно правильно) утверждаете, что весь бизнес с + чрезвычайно дорог.

Остальные из вас, ребята, пожалуйста, поймите, что я ПОНИМАЮ, как это работает, я спрашиваю, ПОЧЕМУ это работает именно так...пожалуйста, перестаньте объяснять разницу между методологиями.

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

@Аксель

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

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

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

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";    
}

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

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

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

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

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

вы сохраняете только ссылку на строку "howdy" в переменной, которая является локальной для метода.Эта локальная переменная (foo) был инициализирован значением вызывающего foo когда метод был вызван, но не имеет ссылки на саму переменную вызывающего объекта.После инициализации:

caller     data     method
------    ------    ------
(foo) -->   ""   <-- (foo)

После выполнения задания в вашем методе:

caller     data     method
------    ------    ------
(foo) -->   ""
          "hello" <-- (foo)

У вас там есть еще какие-то проблемы: String экземпляры являются неизменяемыми (по замыслу, в целях безопасности), поэтому вы не можете изменить их значение.

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

String foo = thisWorks();
System.out.println(foo);//this prints the value assigned to foo in initialization 

public static String thisWorks(){
    return "howdy";
}

Сделайте действительно большой туториал на веб-сайте suns.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top