Java で「参照」ではなく「値」で渡す理由を誰かが説明してもらえますか?
-
21-08-2019 - |
質問
私は 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 で文字列がどのように動作するかを考えるときにもう 1 つ留意すべきことは、文字列は不変であるということです。これは C# でも同様に機能しますが、それにはいくつかの正当な理由があります。
- 安全:誰も文字列を変更できなければ、誰も文字列にデータを詰め込んでバッファ オーバーフロー エラーを引き起こすことはできません。
- スピード :文字列が不変であることが確実であれば、そのサイズは常に同じであることがわかり、文字列を操作するときにメモリ内のデータ構造を移動する必要はありません。あなた (言語設計者) も、文字列を低速のリンク リストとして実装することについて心配する必要はありません。ただし、これは双方向に当てはまります。+ 演算子だけを使用して文字列を追加すると、メモリの消費量が多くなる可能性があるため、これを高パフォーマンスでメモリ効率の高い方法で行うには StringBuilder オブジェクトを使用する必要があります。
次に、より大きな質問に移ります。なぜオブジェクトがこのように渡されるのでしょうか?Java が文字列を従来のいわゆる「値渡し」として渡す場合、関数に渡す前に文字列全体を実際にコピーする必要があります。それはかなり遅いです。(C のように) 文字列を参照渡しし、それを変更できるようにする場合は、今列挙した問題が発生するでしょう。
私の最初の答えは「なぜそれが起こったのか」であり、「なぜ言語がそのように設計されたのか」ではなかったので、これをもう一度考えてみます。
話を単純化するために、メソッド呼び出しを取り除き、何が起こっているかを別の方法で示します。
String a = "hello";
String b = a;
String b = "howdy"
System.out.print(a) //prints hello
「hello」を出力する最後のステートメントを取得するには、 b メモリ内の同じ「穴」を指す必要があります。 ある (ポインタ) を指します。これは、参照渡しが必要な場合に必要なものです。Java がこの方向に進まないことにした理由はいくつかあります。
ポインタが紛らわしい Java の設計者は、他の言語にあるややこしい点をいくつか削除しようとしました。ポインタは、演算子のオーバーロードと同様に、C/C++ の最も誤解され、不適切に使用されている構成要素の 1 つです。
ポインタはセキュリティリスクです ポインターは悪用されると多くのセキュリティ上の問題を引き起こします。悪意のあるプログラムがメモリのその部分に何かを割り当てると、自分のオブジェクトだと思っていたものが、実際には他人のオブジェクトになります。(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");
}
尋ねとしてあなたの質問は、を実際に(他の人が述べたように)参照渡し、値渡しとしなければならない、または文字列は、は不変であるという事実はありません。
メソッド内で、あなたが実際にローカル変数を作成し、あなたの元の変数(「originalFoo」)と同一の参照を指す(私はその1「localFoo」と呼ぶことにします)。
あなたは「こんにちは」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);
}
は、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 ですべてが値によって渡される理由の 1 つは、Java の言語設計者が言語を簡素化し、すべてを OOP 方式で実行できるようにしたいと考えているためです。
彼らは、参照渡しのファーストクラスのサポートを提供するよりも、オブジェクトを使用して整数スワッパーを設計することを望んでいます。デリゲート (Gosling は関数へのポインターに不快感を感じています。彼はむしろその機能をオブジェクトに詰め込みたいと考えています) と列挙型についても同様です。
これらは、言語を過度に単純化する (すべてがオブジェクトである) ため、ほとんどの言語構造に対するファーストクラスのサポートがありません。参照渡し、デリゲート、列挙型、プロパティなどが思い浮かびます。
あなたはそれがnullで印刷しますか?私は、あなたがfoo変数を初期化するときに、空の文字列を提供するよう、それだけで空白になると思います。
System.out.println(FOO)でfooは依然として古い空の文字列オブジェクトをポイントするようにます。thisDoesntWork方法でFOOの割当はクラスで定義さFOO変数の参照を変更されていません
デイブは、あなたが(まあ、私はあなたが「に持って」いないと思いますが、私はむしろ、あなたがやったと思います)私を許しする必要がなく、その説明は過度に説得力がありません。文字列の値を変更する必要がある人は、いくつかの醜い回避策でそれを行うための方法を見つけるだろうので、セキュリティの向上はかなり最小限です。そして、スピード!あなた自身は(かなり正確に)+と全体のビジネスは非常に高価であることを主張しています。
君たちの残りの部分は、私はそれがそのように動作しますなぜ...方法論の違いを説明するおやめください聞いてるのよ、私はそれがどのように動作するかGETのでご了承ください。
(と私は正直ここでの戦いの任意の並べ替えを捜しているわけではない、ところで、私は、これは合理的な決断だったか表示されません)。
@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";
}
太陽のウェブサイト上で本当に大きなチュートリアルを行います。
あなたは違いスコープ変数をすることができ理解していないようです。 「fooが」あなたのメソッドに対してローカルです。その方法の外に何も何の「foo」というポイントを変更することはできません。あなたの外側のクラスへの静的フィールド - あなたの方法と呼ばれている「fooが」完全に別のフィールドです。
あなたはすべてがあなたのシステムの他のすべてに見えるようにしたいいけないとしてスコープは特に重要です。