@takumi19910112 (やま)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

値渡しと参照渡しについて具体的なイメージをもち納得したい

解決したいこと

オブジェクト指向言語における値渡しと参照渡しについて違いを明確にし、具体的なイメージをもちたいです。

発生している問題・エラー

私は今、Javaの勉強をしているのですが、値渡しと参照渡しという言葉のところで具体的なイメージが持てず、つまづいています。
参考にしている書籍によると、
値渡しとは「メソッドを呼び出すときに引数として通常の変数を指定した場合、メソッドに渡されるのは変数ではなく、変数に入っている値である。このように、値そのものが渡される呼び出しを値渡しという」
また、参照渡しとは「引数としてアドレスを渡すことを参照渡しという」と書かれています。
値渡しが変数の値そのものをメソッドに渡し、参照渡しはアドレス(番地)をメソッドに渡すことの違いがある、というところまでわかりました。ですが、なぜメソッドへの変数の渡し方にこのような違いがあるのか?それぞれのメリットやデメリットは何か?具体的な使用例としてどのようなものが考えられるのか?がわからず、困り感を感じています。
どなたかご教授いただけませんでしょうか。

自分で試したこと

java
public static void main(String[] args){
    int[] array = {1, 2, 3};
}

上記のように配列の場合は、配列型変数arrayには配列の値そのものではなく、配列のアドレスが代入されていることまでは勉強しました。ここらへんと関連性があるのでしょうか?

0 likes

6Answer

「値型、参照型」と「値渡し、参照渡し」はまったく別の話です。
非常によくあることなんですが、メソッド引数以外のことに言及していたら前者の話が入り込んでいる可能性が高いので注意してください。
「値渡し、参照渡し」に関してはその実体がヒープにあるかスタックにあるかなんて意識するのはまったく無意味です。
たいていの言語で引数の受け渡しはスタックを使って行われるのでそういう話題にも飛びやすいですが、肝心なのはパラメータ変数に何が入っているか(何を指しているか)であって受け渡しにスタックが使われることを意識する必要はないです。

4Like

Comments

  1. @takumi19910112

    Questioner

    ありがとうございます。
    「値型、参照型」と「値渡し、参照渡し」が混同していました。
    ヒープかスタックかを意識するのではなく、変数に何が入っているかが重要なのですね。
    勉強になりました。

まず、「参照渡し」「値渡し」という言葉は、言語や文脈等によって意味合いが異なることが多く、
うかつにつかうと痛い目を見るので気を付けた方が良いです。

うかつにその単語を出すと、たとえば
・Javaには「参照渡し」はない
・⇔いや、○○という動作を(便宜的に)「参照渡し」といってるだけだ。
・Javaにあるのは「参照(値)の値渡し」でしょ?
・Javaには「参照(値)の値渡し」もない、あるのは値渡しだけだ。
・⇔いや、ある。△△という動作は「参照(値)の値渡し」だ。著名な人もこの意味で使っている
・C言語には本来の意味での「値渡し」「参照渡し」があるよね?
・はあ?何言ってんの?C言語には「参照渡し」なんてないよ。それをいうなら「ポインタ渡し」だろ?
・お前さあ「ポインタ型の値渡し」と「参照渡し」の区別ついてる?

等々といった論争が続くことになります。

したがって、ここでは立ち入りません。
人によっては、質問者さんが引用した教科書の記述は間違ってる、というかもしれませんが、それもここでは立ち入りません。

上っ面の知識で武装して言葉尻をとらえて論破することは生産的とはいえません。

まず大切なのは質問者さんが使おうとしている Java での基本的な原理や挙動を理解することだと思います。

このあたりは下記のページが参考になると思うので、ぜひ一度読んでください。

Java に参照渡しは存在しない。値渡しだけが存在する。
ttps://yoshitaro-yoyo.hatenablog.com/entry/only_value_passing_in_java

「プリミティブ型」と「参照型」はメモリ管理の仕組みが異なる【Java】
ttps://yoshitaro-yoyo.hatenablog.com/entry/memory_management_in_java

上記が難しければ、Java における変数の管理について図でわかりやすくまとめてある下記を一度読んでからもう一度読んでみて下さい。
ttps://freelance-jak.com/technology/java/1175/
 
上記でも言及されていますが、関係するリファレンスの記述は、ここら辺になりますので、余裕があれば読んでおくことをおすすめします(英語ですが翻訳ソフト等を使えばよいでしょう)


メリット・デメリット

・では、質問者さんが使っている文脈での「"値渡し"」「"参照渡し"」風の動作のメリット・デメリットについて簡単に触れます。


注意:
以下独自用語であるという意味を強調するため二重引用符で囲んだうえ「風の動作」という接尾辞をつけています。
この、「"参照渡し"風の動作」「"値渡し風"の動作」という用語自体、本質問・回答内でしか通用しないものであり、外部に持ち出すべきではありません。


"参照渡し"風の動作
public class Main {
    public static void main(String[] args) throws Exception {
        int[] array = {1, 2, 3};
        methodA(array);
        System.out.println(array[0]);  // 11が出力されます
    }
    
    public static void methodA(int[] num){
        num[0] += 10;
    }
}

メリット:参照型の変数を渡す際、ポインタ(アドレス)のコピーだけで済むので、コストが小さい。(動作が早くメモリ消費も少ない)

  • デメリット:渡し先での挙動に注意する必要あり(渡し元のデータを書き換える場合がある)
"値渡し"風の動作
public class Main {
    public static void main(String[] args) throws Exception {
        int value = 1;
        methodB(value);
        System.out.println(value);  // 1が出力されます
    }
    
    public static void methodB(int num){
        num += 10;
    }
}
  • メリット:値そのものがコピーされるので、書き換えを心配?しなくてよい
  • デメリット:
    ・メソッド間で直感的な意味での値の共有ができない。
    ・値のコピーにコストがかかる(ただしJavaでは、質問者さんの文脈での「"値渡し"」風の動作になるのは、基本的にプリミティブ型の変数を渡すときだけなので、実際のコストは問題になりにくい。[ミュータブル・イミュータブルの挙動違いについてはここでは深入りしません])

ただ、実際上記のようにメリットデメリットというくくりで論じるのはあまり適切ではないかなとも思います。

というのは、Java では基本的に、プリミティブ型の変数を渡す場合は、"値渡し"風の動作となり、参照型の変数(その変数が指し示している先がオブジェクト[=クラスのインスタンス、配列]等であるもの)を渡す場合は、"参照渡し"風の動作となり、実質的に選択できないからです。[ミュータブル・イミュータブルの違いによる、渡した後での挙動の違いはここでは深入りしません]

たとえば、参照型の変数を渡したときに"値渡し"風の動作をさせたいのであれば、コピー用のメソッドを使うか、自分で中身をコピーする処理を書く必要があります。

(また、同じ参照型でも、ミュータブルなオブジェクトとイミュータブルなオブジェクトでも、渡した後の処理によっては挙動が異なってくる場合がありますので、そこもおさえておいた方が良いと思います。

この辺は下記がわかりやすいと思います。
ttps://medium-company.com/java-プリミティブ型-参照型/


なぜメソッドへの変数の渡し方にこのような違いがあるのか?

歴史的な経緯として、C言語のような歴史の長い言語の一部(古いものだけではなく、たとえば最近の言語だとGolangなども含む)は、ポインタ(のようなもの)を使って"値渡し風"の動作と"参照渡し"風の動作を選択できます。

どちらかといえば、"値渡し風"の動作が既定の動作です。

しかし、巨大なサイズの構造体(オブジェクトのようなもの)に対して"値渡し風"の動作を行うと、都度コピーが必要になり遅くなりメモリを圧迫するというデメリットがあります。
また、一般的に、巨大なサイズの構造体に対して"値渡し風"の動作を行う必要性のある場面も少ない、という事情があったと考えられます。(なので構造体に対してはポインタを使って"参照渡し"風の動作をさせることが多い)

一方、単純な値(プリミティブ型のようなもの)については、コピーのコストが少ないです。また、仮に単純な値に対してまで"参照渡し"風の動作を行うと、意図せず渡し元の値を書き換えてしまうリスクがある(=設計上、単純な値は渡し元を書き換えたくない・書き換える必要がない場面が多い)と考えられます。

なので、Java では、プリミティブ型は"値渡し"風、参照型は"参照渡し"風の動作になったのではないか、と思われます。

(あくまで推測です)

3Like

前置きですが、Javaには「参照渡しは無い」「他の言語のそれと異なる」という話があります。私はJavaは詳しくなく、細かい言い回しなどあれこれ言うのも違うので深入りせずに回答させていただきます。

値渡しと参照渡しのメリット/デメリットですが、「共有」と「コピーのコスト」がポイントになります。
値渡しはコピーして渡すので、一方が値を変更しても相互に影響はしませんが、コピーのコストは物が大きくなれば増えます。
参照渡しは共有することになるので、一方が値を変更すると相互に影響しますが、コピーのコストは場所を渡すだけなので小さくなります。
共有がメリットになるかデメリットになるかは状況次第です。

Javaにちなんで「お茶」を例にすると、値渡しは「もう一杯お茶を淹れて渡す」です。一方がお茶を飲んでももう一方のお茶は減りません。参照渡しだと「自分のお茶を共有する」です。一方がお茶を飲むと減ります。
どちらの渡し方が良いかは要求によって異なります。例えば、「どんなお茶を飲んでいるのか見たいだけ」なら共有してもいいかもしれません。「飲みたい」というならもう一杯淹れるのがいいかもしれませんね。

1Like

値渡しの

  1. メリット  親の環境が関数の結果に影響を受けない。
  2. デメリット 巨大なオブジェクトを真面目にdeep copyすると処理時間が掛かる。

参照渡しの

  1. メリット  グローバル変数を使う必要がなく、高速である。
  2. デメリット 関数が親pg専用関数になってしまう。

プリミティブ型はdeep copy、shallow copyの概念はありません。

プリミティブ型の値をcopyして引数として渡すのが「値渡し」、C言語で言うポインタで引数として渡すのが「参照渡し」です。(親と共有)

プリミティブ型以外(オブジェクト型)の変数は「値渡し」しても結果、「参照渡し」になってしまうことです。

それは、値渡しでコピーしても深い階層まで、deep copyされません。「参照渡し風」と言われます。(shallow copyの様に)

これはjava言語の仕様なのか?欠陥なのか?私は言及しません。

1Like

ちなみに、C言語の構造体やPHPの配列は値型なので、複数の要素をコピーして渡します。
ただし、PHPの配列は、内部的に参照を渡しておいて要素に代入(要素変更)されたときにコピー実施するようになっていて、参照するだけの場合は速度低下しないように工夫されてます。
PHPは参照渡しもできるので、配列をコピーせずに共有することも可能です。

また、Pythonでは int, float, bool などもすべて参照型です。
値型か参照型かは言語によって異なるということですね。

1Like

Comments

  1. @takumi19910112

    Questioner

    ありがとうございます。
    言語によってどの型に分類されるか異なるのですね。
    他の方々からのアドバイスも含めて非常に参考になりましたし、さらに勉強をする必要があると強く感じました。

解答していただいた方々、丁寧な説明ありがとうございます。
また、わかりやすい参考資料もありがとうございます。
理解が足りないかもしれませんが、

まず、厳密な意味での「参照渡し」は存在せず、「参照渡しのような挙動」がみられる、と理解しました。

値渡しとは、
D6D204B6-D14F-4AB8-83FC-7EEE9F97EBD3.jpeg
スタック領域と呼ばれる場所に値そのものが保存され、

参照渡しのような挙動とは、
37FDD38F-FCCB-40D8-A5E8-846FEA2A2303.jpeg
11AFBFC3-7DC1-4A47-917D-FEA7047B5523.jpeg

そのため、プリミティブ型ではなく配列のような方では、参照渡しのような挙動となり、

main.java
public class Main {
    public static void printArray(int[] array){
        array[0] = 100;
    }
    public static void main(String[] args){
        int[] array = {1, 2, 3};
        printArray(array);
        for(int i = 0; i < array.length; i++){
            System.out.println(array[i]);
        }
        // 100 2 3 と出力される。
        // メソッド呼び出しの際にコピーされるのは、配列の内容{1, 2, 3}でなく配列の先頭要素のアドレス。
        // mainメソッド内の変数arrayとprintArrayメソッドの引数arrayはどちらも同じアドレスを指す。
    }
}

となることがわかりました。うまく言語化できず、まだ全て理解しきれていないのですが、なんとなくのイメージはできた様な気がします。

0Like

Your answer might help someone💌