8

私は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 とは異なり、ユーザーはメソッドに渡されたものを操作できます (オーバーロードされた = によって設定された値に固執することはありません)。

このデザインの選択の背後にある理由を誰か説明してもらえますか? 私が言ったように、私はここにいるつもりはありません。おそらく、明らかな何かが欠けているのでしょうか?

4

15 に答える 15

11

この暴言は、私がこれまでに試みることさえできなかったよりもうまく説明しています。

Java では、プリミティブは値によって渡されます。ただし、オブジェクトは参照渡しではありません。正しいステートメントは、オブジェクト参照は値によって渡されます。

于 2009-02-05T03:59:21.907 に答える
9

「foo」を渡すと、「foo」への参照が値として This DoesntWork() に渡されます。つまり、メソッド内で「foo」への代入を行う場合、ローカル変数 (foo) の参照を新しい文字列への参照に設定しているだけです。

Java で文字列がどのように動作するかを考えるときに留意すべきもう 1 つのことは、文字列は不変であるということです。C# でも同じように機能しますが、いくつかの正当な理由があります。

  • セキュリティ: 誰も文字列にデータを詰め込み、誰も変更できない場合、バッファ オーバーフロー エラーを引き起こすことはできません!
  • 速度: 文字列が不変であると確信できる場合、そのサイズは常に同じであり、データ構造を操作するときにメモリ内でデータ構造を移動する必要がないことがわかります。あなた (言語設計者) も、String を遅いリンクリストとして実装することについて心配する必要はありません。ただし、これは両方の方法をカットします。+ 演算子だけを使用して文字列を追加すると、メモリの負荷が高くなる可能性があり、高性能でメモリ効率の高い方法でこれを行うには、StringBuilder オブジェクトを使用する必要があります。

次に、より大きな質問に進みます。オブジェクトがこのように渡されるのはなぜですか? Java が文字列を伝統的に「値渡し」と呼んでいたものとして渡した場合、関数に渡す前に文字列全体を実際にコピーする必要があります。それはかなり遅いです。参照によって文字列を渡し、それを変更できるようにする場合 (C のように)、先ほど挙げた問題が発生します。

于 2009-02-05T03:55:44.737 に答える
5

私の最初の答えは「なぜそれが起こったのか」であり、「なぜ言語がそのように設計されたのか」ではなかったので、これをもう一度やり直します。

簡単にするために、メソッド呼び出しを取り除き、別の方法で何が起こっているかを示します。

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

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

"hello" を出力する最後のステートメントを取得するには、bは、 aが指しているメモリ内の同じ "穴" (ポインター) を指している必要があります。これは、参照渡しが必要な場合に必要なものです。Java がこの方向に進まないことにした理由はいくつかあります。

  • ポインターは紛らわしいJava の設計者は、他の言語のややこしい部分をいくつか取り除こうとしました。ポインターは、C/C++ の最も誤解され、不適切に使用される構造の 1 つであり、演算子のオーバーロードも同様です。

  • ポインタはセキュリティ上のリスクポインタは、誤用されると多くのセキュリティ上の問題を引き起こします。悪意のあるプログラムがメモリのその部分に何かを割り当てると、自分のオブジェクトだと思っていたものが実際には他の誰かのものになります。(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");
}
于 2009-02-05T14:38:39.273 に答える
4

あなたの質問は、値渡し、参照渡し、または文字列が不変であるという事実とは関係ありません(他の人が述べているように)。

メソッド内で、元の変数 ("originalFoo") と同じ参照を指すローカル変数 (これを "localFoo" と呼びます) を実際に作成します。

"howdy" を localFoo に割り当てても、originalFoo が指している場所は変更されません。

次のようなことをした場合:

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

あなたは期待しますか:

System.out.print(a)

「こんにちは」を印刷するには?"" と出力されます。

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);
}
于 2009-02-05T04:13:05.263 に答える
3

Java では、渡されるすべての変数は、実際には値オブジェクトによっても渡されます。メソッドに渡されるすべての変数は、実際には元の値のコピーです。文字列の例の場合、元のポインター(実際には参照ですが、混乱を避けるために別の単語を使用しないでください)が新しい変数にコピーされ、メソッドのパラメーターになります。

すべてが参照であるとしたら、それは苦痛です。あちこちにプライベートコピーを作成する必要があり、これは間違いなく本当に面倒です. 値の型などに不変性を使用すると、プログラムが無限に単純でスケーラブルになることは誰もが知っています。

いくつかの利点は次のとおりです。 - 防御コピーを作成する必要はありません。- スレッドセーフ - 他の誰かがオブジェクトを変更したい場合に備えて、ロックについて心配する必要はありません。

于 2009-02-05T04:07:09.940 に答える
2

問題は、Java 参照型をインスタンス化していることです。次に、その参照型を静的メソッドに渡し、それをローカル スコープの変数に再割り当てします。

不変性とは何の関係もありません。可変参照型の場合もまったく同じことが起こります。

于 2009-02-05T04:06:40.923 に答える
1

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 ですべてが値渡しされる理由の 1 つは、Java の言語設計者が言語を単純化し、すべてを OOP 方式で処理したいと考えていることです。

彼らは、参照渡しのファーストクラスサポートを提供するよりも、オブジェクトを使用して整数スワッパーを設計してもらいたいと考えています。デリゲート(ゴスリングは関数へのポインターにうんざりしていると感じています。彼はむしろその機能をオブジェクトに詰め込みたいと思っています)と列挙型。

それらは言語を単純化しすぎて (すべてがオブジェクトである)、ほとんどの言語構造 (参照渡し、デリゲート、列挙型、プロパティなど) に対するファースト クラスのサポートがないという欠点があります。

于 2009-02-05T06:05:16.363 に答える
0

これは、メソッド内にローカル変数を作成するためです。簡単な方法は次のとおりです(うまくいくと確信しています)。

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";    
}
于 2009-02-05T09:11:05.443 に答える
0

nullを出力してもよろしいですか?空の文字列を提供した foo 変数を初期化したときと同じように、空白になると思います。

thisDOSntWork メソッドでの foo の割り当ては、クラスで定義された foo 変数の参照を変更しないため、 System.out.println(foo) の foo は引き続き古い空の文字列オブジェクトを指します。

于 2009-02-05T03:59:34.137 に答える
0

デイブ、あなたは私を許しなければなりません(まあ、あなたは「する必要はない」と思いますが、私はあなたがしたほうがいいと思います)が、その説明はあまり説得力がありません. 文字列の値を変更する必要がある人は、醜い回避策でそれを行う方法を見つけるため、セキュリティの向上はごくわずかです。そしてスピード?! あなた自身(かなり正確に)、+ を使用したビジネス全体が非常に高価であると主張しています。

残りの皆さん、私はそれがどのように機能するかを理解していることを理解してください.なぜそれがそのように機能するのかを尋ねています.方法論の違いを説明するのはやめてください.

(正直なところ、私はここで何らかの戦いを求めているわけではありません。ところで、これがどのように合理的な決定だったのかわかりません)。

于 2009-02-05T04:13:12.440 に答える
0

オブジェクトをオブジェクト内の単なるフィールドと考えると、メソッドはパラメーターのフィールドを変更でき、呼び出し元は変更を監視できるため、Java ではオブジェクトは参照によって渡されます。ただし、オブジェクトを ID と考えている場合、メソッドは呼び出し元が観察できる方法でパラメーターの ID を変更できないため、オブジェクトは値によって渡されます。したがって、Java は値渡しであると言えます。

于 2009-02-05T09:36:03.800 に答える
0

これは、「this DoesntWork」内で、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
于 2009-02-05T16:41:00.087 に答える
-1

参照型の引数は、オブジェクト自体への参照として渡されます (オブジェクトを参照する他の変数への参照ではありません)。渡されたオブジェクトのメソッドを呼び出すことができます。ただし、コードサンプルでは:

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";
}
于 2009-02-05T04:06:11.017 に答える
-2

suns の Web サイトで、非常に大きなチュートリアルを行ってください。

スコープ変数の違いを理解していないようです。「foo」はメソッドに対してローカルです。そのメソッド以外では、「foo」が指すものも変更できません。メソッドに参照されている「foo」は、完全に異なるフィールドです。これは、囲んでいるクラスの静的フィールドです。

システム内の他のすべてのものにすべてが見えるようにしたくないため、スコープは特に重要です。

于 2009-02-05T03:56:55.077 に答える