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와 달리 사용자는 메서드에 전달된 항목에 대해 작업을 수행할 수 있습니다(그리고 오버로드된 =에 의해 설정된 값에 얽매이지 않습니다).

이 디자인 선택의 이유가 무엇인지 누군가 나에게 설명해 줄 수 있습니까?내가 말했듯이, 나는 바로 여기에 있을 생각이 없습니다. 아마도 뭔가 분명한 것을 놓치고 있는 것 아닐까요?

도움이 되었습니까?

해결책

이 폭언 내가 시도 할 수있는 것보다 더 잘 설명합니다.

Java에서는 프리미티브의 가치가 전달됩니다. 그러나 객체는 참조별로 전달되지 않습니다. 올바른 진술은 객체 참조가 값으로 전달됩니다.

다른 팁

"foo"를 전달하면 참조 ThisDoesntWork()에 대한 값으로 "foo"를 지정합니다.즉, 메서드 내에서 "foo"에 할당할 때 지역 변수(foo)의 참조를 새 문자열에 대한 참조로 설정하는 것뿐입니다.

Java에서 문자열이 어떻게 작동하는지 생각할 때 염두에 두어야 할 또 다른 사항은 문자열이 불변이라는 것입니다.이는 C#에서도 동일한 방식으로 작동하며 몇 가지 좋은 이유가 있습니다.

  • 보안:누구도 문자열에 데이터를 집어넣을 수 없으며, 누구도 이를 수정할 수 없다면 버퍼 오버플로 오류가 발생할 수 없습니다!
  • 속도 :문자열이 변경 불가능하다고 확신할 수 있다면 문자열의 크기가 항상 동일하며 조작할 때 메모리에서 데이터 구조를 이동할 필요가 없다는 것을 알 수 있습니다.당신(언어 디자이너)은 문자열을 느린 연결 목록으로 구현하는 것에 대해 걱정할 필요도 없습니다.그러나 이것은 두 가지 방법 모두를 잘라냅니다.+ 연산자를 사용하여 문자열을 추가하는 것은 메모리 측면에서 비용이 많이 들 수 있으므로 고성능, 메모리 효율적인 방식으로 이 작업을 수행하려면 StringBuilder 개체를 사용해야 합니다.

이제 더 큰 질문에 들어갑니다.객체가 이런 식으로 전달되는 이유는 무엇입니까?글쎄, Java가 전통적으로 "값 기준"이라고 부르는 문자열을 전달한 경우 함수에 전달하기 전에 실제로 전체 문자열을 복사해야 합니다.꽤 느립니다.문자열을 참조로 전달하고 이를 변경하도록 허용하면(C처럼) 방금 나열한 문제가 발생하게 됩니다.

나의 원래 대답은 "왜 그런 일이 일어 났는지"였고 "왜 언어가 설계 되었는가?"

물건을 단순화하기 위해 방법 호출을 제거하고 다른 방식으로 무슨 일이 일어나고 있는지 보여줍니다.

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

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

마지막 진술이 "Hello"를 인쇄하려면 메모리에서 동일한 "구멍"을 가리켜 야합니다. (포인터)를 가리 킵니다. 이것은 참조별로 패스를 원할 때 원하는 것입니다. Java 가이 방향을 가리지 않기로 결정한 몇 가지 이유가 있습니다.

  • 포인터는 혼란 스럽습니다 Java의 디자이너들은 다른 언어에 대한 더 혼란스러운 것들을 제거하려고했습니다. 포인터는 작업자 과부하와 함께 C/C ++의 가장 오해되고 부적절하게 사용되는 구성 중 하나입니다.

  • 포인터는 보안 위험입니다 포인터는 오용 할 때 많은 보안 문제를 일으 킵니다. 악의적 인 프로그램은 기억의 해당 부분에 무언가를 할당하고, 당신이 당신의 대상이라고 생각한 것은 실제로 다른 사람의 것입니다. (Java는 이미 가장 큰 보안 문제, 버퍼 오버플로를 제거하여 확인되었습니다)

  • 추상화 누출 "기억 속에있는 것과 어디에"를 다루기 시작하면 정확히 당신의 추상화는 추상화가 덜됩니다. 추상화 누출은 거의 확실하게 언어로 들어 오지만 디자이너는 직접 굽기를 원하지 않았습니다.

  • 물체는 당신이 신경 쓰는 모든 것입니다 Java에서는 모든 것이 객체가 차지하는 공간이 아닌 물체입니다. 포인터를 추가하면 공간이 객체를 차지하는 공간이 중요합니다.

"구멍"객체를 만들어 원하는 것을 모방 할 수 있습니다. 제네릭을 사용하여 유형을 안전하게 만들 수도 있습니다. 예를 들어:

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

질문에 따라 귀하의 질문 실제로 가치를 통과하거나, 참조로 통과하거나, 줄이 불변이라는 사실과 관련이 없습니다. (다른 사람들이 언급 한 바와 같이).

이 메소드 내부에서는 실제로 원래 변수 ( "OriginalFoo")와 동일한 참조를 가리키는 로컬 변수 ( "LocalFoo"라고 부르겠습니다)를 만듭니다.

"Howdy"를 LocalFoo에 할당하면 OriginalFoo가 가리키는 곳에서는 변경하지 않습니다.

당신이 같은 일을했다면 :

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

당신은 기대 하시겠습니까 :

System.out.print(a)

"Howdy"를 인쇄하려면? 인쇄 "".

LocalFoo가 가리키는 것을 변경하여 OriginalFoo가 가리키는 것을 변경할 수 없습니다. 둘 다 가리키는 객체를 수정할 수 있습니다 (불변이 아닌 경우). 예를 들어,

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에서 통과 된 모든 변수는 실제로 가치가있는 객체에 의해 전달됩니다. 방법으로 전달 된 모든 변수는 실제로 원래 값의 사본입니다. 문자열 예제의 경우 원래 포인터 (실제로 참조 - 그러나 혼란을 피하기 위해 다른 단어를 사용하지 않음)는 메소드의 매개 변수가되는 새 변수로 복사됩니다.

모든 것이 참조 된 경우 고통이 될 것입니다. 하나는 진짜 고통 일 것입니다. 모두 가치 유형 등에 불변성을 사용하면 프로그램이 무한히 단순하고 확장 가능하다는 것을 알고 있습니다.

일부 혜택은 다음과 같습니다 .- 방어 사본을 만들 필요가 없습니다. - ThreadSafe- 다른 사람이 객체를 변경하려는 경우를 대비하여 잠금에 대해 걱정할 필요가 없습니다.

문제는 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의 가치에 의해 전달되는 한 가지 이유는 언어 디자이너 사람들이 언어를 단순화하고 모든 것을 OOP 방식으로 수행하기를 원합니다.

오히려 그들은 객체를 사용하여 정수 스와퍼를 설계하여 대의원 통과에 대한 첫 번째 클래스 지원을 제공합니다 (Gosling은 기능에 대한 포인터가있는 느낌이 든다.

그들은 대부분의 언어 구성에 대한 일류 지원을받지 못한다는 사실, 예를 들어, 참조, 대표, 열거, 재산이 떠오른다는 것은 언어를 과도하게 단순화합니다 (모든 것이 대상입니다).

당신은 그것이 널 인쇄된다고 확신합니까? 빈 문자열을 제공 한 Foo 변수를 초기화했을 때 비어있을 것이라고 생각합니다.

이 doesntwork 메소드에서 foo를 할당하면 클래스에 정의 된 foo 변수의 참조를 변경하지 않으므로 system.out.println (foo)의 foo가 여전히 이전 빈 문자열 객체를 가리 킵니다.

데이브, 당신은 나를 용서해야합니다 (음, 당신은 "할 필요가 없지만 오히려 당신이했던 것 같아요) 그러나 그 설명은 지나치게 설득력이 없습니다. 문자열 값을 변경 해야하는 사람이라면 누구나 추악한 해결 방법으로이를 수행 할 수있는 방법을 찾을 수 있기 때문에 보안 이익은 상당히 최소화됩니다. 그리고 속도?! 당신은 (매우 정확하게) +와의 전체 비즈니스가 매우 비싸다고 주장합니다.

나머지 여러분, 내가 어떻게 작동하는지 이해하십시오. 왜 그런 식으로 작동하는지 묻습니다 ... 방법론의 차이점을 설명하지 마십시오.

(그리고 나는 솔직히 여기서 어떤 종류의 싸움도 찾고 있지 않습니다. BTW, 나는 이것이 어떻게 합리적인 결정인지 모르겠습니다).

@axelle

메이트는 가치별로 통과하는 것과 참조의 차이를 정말로 알고 있습니까?

Java에서는 참조조차 가치별로 전달됩니다. 객체에 대한 참조를 전달하면 두 번째 변수에서 참조 포인터의 사본을 얻습니다. Tahts 두 번째 변수가 첫 번째에 영향을 미치지 않고 변경 될 수있는 이유.

메소드 내부에 로컬 변수를 생성하기 때문입니다. 쉬운 방법은 다음과 같습니다.

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에서 참조로 전달됩니다. 그러나 객체를 신원으로 생각하면 신뢰할 수있는 방식으로 메소드가 매개 변수의 ID를 변경할 수 없기 때문에 객체는 값으로 전달됩니다. 그래서 나는 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 인스턴스는 불변 (설계, 보안을위한)이므로 그 값을 수정할 수 없습니다.

당신의 방법이 당신의 문자열에 초기 값을 제공하기 위해 당신의 방법을 정말로 원한다면 어느 그 삶의 시간, 그 문제에 대한 시간) 반품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