Javaにある"値渡しと参照渡しの境界みたいな部分"の話

TL;DR

Twitterベースのプログラミング初心者向け勉強会で、ちょっと雑というか、きちんと説明しないと後々、誤解が出そうな話題があったので、纏めます。

何の話かというと「プログラミングにおける関数の使い方で、値渡しと、参照渡しの違い」みたいな部分。

Javaはすべて値渡しです:はい、究極的には正しいです。ただし、落とし穴になりがちな部分を知らないと事故ります。

Javaは複写できる型は値渡し、複写できない型は参照渡し:そんな風に見えるけど違います


もうちょっと詳しく

テストしてみてください

こんなコードを想定してください。さて、実行結果はどうなるでしょうか。

import java.util.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        Integer i = 1;
        sub(i);
        System.out.println(i.toString());
    }
    
    private static void sub(Integer i)
    {
        i = 10;
        return;
    }
}

出力されるのは

1

です。典型的な「関数に値渡しをした」挙動ですね。

では次に、こんなコードだとどうなるでしょうか。

import java.util.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        List<String> li = new ArrayList<>();
        li.add("1番目");
        
        sub(li);
        
        for(String s : li)
        {
            System.out.println(s);
        }
    }
    
    private static void sub(List<String> li)
    {
        li.add("2番目");
    }
}

出力結果は

1番目
2番目

はい。参照渡しに見える挙動が起きています。

これで「List<String>は複製できないから参照渡しなんだ!」と思ったら、ドツボにはまります。

次のケース。これはどうでしょうか?

import java.util.*;

public class Main
{
    public static void main(String[] args) throws Exception
    {
        List<String> li = new ArrayList<>();
        li.add("1番目");
        
        sub(li);
        
        for(String s : li)
        {
            System.out.println(s);
        }
    }
    
    private static void sub(List<String> li)
    {
        li = new ArrayList<>();
        li.add("2番目");
    }
}

もしsub関数に、List<String>が本当に参照渡しされているなら、subの中で

li = new ArrayList<>();
li.add("2番目");

しているので、「2番目」だけが出力されないとおかしいです。

ですが、実行結果は、

1番目

はい。THE・値渡し みたいな挙動です。つーか値渡しですね。


どうしてこうなるの

「値をコピーできる型は、値渡ししてる」
まず、これはそのままの理解でOKです。つまり、値をメモリ上の別の場所にそのまま複写して、それを関数の引数にしてる(渡してる)わけです。

「値をコピーできない型は、値のあるメモリ上の場所を、値渡ししてる」
ここがこの話の肝です。専門用語それくらい知っとけでポインタというやつです。場所といっても、実際には数値(使えるメモリ領域の中でn番目の場所、的なニュアンス)です。
つまりInteger等の数値型のように、自由に複写できます。ゆえに、「場所の情報を複写して渡す」ことが可能になります。

そして受け取った関数側で、どう処理されるかといえば、

  • 直接的な操作は、複写されたメモリ上の値をそのまま参照しに行くので、呼び出し元から渡されたオブジェクトに影響する(上のサンプルの例でいえば追加される)

    • つまり「参照渡し」されたようなまぎらわしい動作をする

  • 内容そのものを書き換える操作は、複写された「どこの場所か」の値が指定している場所にある変数の情報を、ざっくり書き換える。でもそれは「複写された」場所の情報なので、呼び出し元には影響しない。

    • つまり「値渡し」されたのと同じ動作をする

というからくりです。OK?

わからなかったら、わかるまでコード書いて、動かし倒して理解してください。習うより慣れろ。


それはそれとして苦言

そもそも、この記事を書いたきっかけは、冒頭に書いた、プログラミング勉強会関連なのです。が。

今回書いた部分って、割とJava特有で、ほかの言語ではあまり似たような動作は見かけないと思います。
実際、オブジェクト型だけ参照渡しになるような言語も普通にあるので、混乱することもあるでしょう。

そういうこともあるので、特にプログラミング初心者は、特定の言語に依存しない、プログラミングの一般論みたいな解説をベースに学んでしまうと、「その言語特有の挙動」に振り回されることがあります。

でもまあ、これって、本来は「特定の言語に特化した教材」を使うことで、100%とはいかなくても、かなり解消できるのです。
言語特有の部分に注釈をいれることもできるし、そもそも一般論に偏る必要がないので。


という前提を加味して、あえて言います。

カスであると。

間違えました。あえて言います。

一般論で教えるなら、個別の言語を学んでる人を、それぞれの言語に特化した部分でフォローできる体制を作らないと、逆に躓く原因になるよ。と。

やるなとは言いませんが、無責任なやり方では、初心者を崖から突き落とすだけにしかなりません。
というか、目的と手段を混同したり、目的の正しさで間違った手段を正当化してると、技術者はどんどん信用を失います。よ?


この記事が気に入ったらサポートをしてみませんか?