Java 入門

Java の値渡し

本章では、JavaにおけるPass-by-Valueの詳細を深掘りし、それが異なるデータ型にどのような影響を与え、プログラムの挙動をどのように左右するのかを解説します。

1. Pass-by-Valueを深く理解する

Javaでは、すべてのパラメータはメソッドに対してPass-by-Value(値渡し)で渡されます。これは、パラメータを使用してメソッドを呼び出す際、システムがそのパラメータ値の「コピー」を作成し、そのコピーをメソッドに渡すことを意味します。その後、メソッドはオリジナルの変数そのものではなく、このコピーに対して操作を実行します。

この違いを理解することは極めて重要です。なぜなら、メソッド内部で行われた変更が、メソッド外部のオリジナル変数に影響を与えるかどうかを決定づけるからです。

2. Primitive Type(プリミティブ型)

intdoublebooleanなどのPrimitive Type(プリミティブ型)をメソッドに渡す際、メソッドは該当の変数に格納されている実際の数値のコピーを受け取ります。メソッド内部でそのパラメータに対して行われたいかなる変更も、メソッド外部のオリジナル変数には一切影響を与えません。

public class PassByValuePrimitive {
    public static void main(String[] args) {
        int x = 10;
        System.out.println("modifyValue 呼び出し前: x = " + x); // 出力: 10
        modifyValue(x);
        System.out.println("modifyValue 呼び出し後: x = " + x);  // 出力: 10
    }

    public static void modifyValue(int num) {
        num = 20;
        System.out.println("modifyValue 内部: num = " + num);      // 出力: 20
    }
}

この例では、xは10に初期化されています。modifyValueメソッドは数値10のコピーを受け取り、それをローカル変数numにアサインした後、numを20に変更します。しかし、mainメソッド内のオリジナル変数xは変更されません。modifyValueが操作したのはあくまでxの数値のコピーであるため、xの値は10のまま維持されます。

3. Reference Type(リファレンス型・オブジェクト)

Reference Type(オブジェクト)をメソッドに渡す際、渡されるのはリファレンス(参照)のコピーです。オブジェクト自体のコピーではありません。これは、オリジナルのリファレンスと、コピーされたリファレンスが、メモリ上で全く同じオブジェクトを指し示していることを意味します。

したがって、メソッドがオブジェクトのステートを変更した場合(例えば、オブジェクトの特定のフィールドの値を変更した場合)、両方のリファレンスが同じエンティティを指しているため、それらの変更はオリジナルオブジェクトに反映されます。ただし、メソッドがパラメータのリファレンスを再アサインし、完全に異なる新しいオブジェクトを指すようにした場合、オリジナルのリファレンスは変更されず、初期のオブジェクトを指し続けます。

class Dog {
    String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public class PassByValueReference {
    public static void main(String[] args) {
        Dog myDog = new Dog("バディ");
        System.out.println("changeName 呼び出し前: " + myDog.getName()); // 出力: バディ
        changeName(myDog);
        System.out.println("changeName 呼び出し後: " + myDog.getName());  // 出力: マックス

        Dog yourDog = new Dog("ベラ");
        System.out.println("replaceDog 呼び出し前: " + yourDog.getName()); // 出力: ベラ
        replaceDog(yourDog);
        System.out.println("replaceDog 呼び出し後: " + yourDog.getName());  // 出力: ベラ
    }

    public static void changeName(Dog dog) {
        dog.setName("マックス");
        System.out.println("changeName 内部: " + dog.getName());      // 出力: マックス
    }

    public static void replaceDog(Dog dog) {
        dog = new Dog("チャーリー"); // リファレンスの再アサイン
        System.out.println("replaceDog 内部: " + dog.getName());      // 出力: チャーリー
    }
}

changeNameの例では、changeNameメソッドはmyDogオブジェクトを指すリファレンスのコピーを受け取ります。その後、2つのリファレンスが共通して指し示しているオブジェクト上でsetNameメソッドが呼び出されます。その結果、mainメソッドでmyDog.getName()を呼び出すと、変更が反映されます。

replaceDogの例では、replaceDogメソッドはyourDogオブジェクトを指すリファレンスのコピーを受け取ります。しかしメソッド内部で、dogリファレンスは「チャーリー」という名前の新しいDogオブジェクトを指すように再アサインされます。この再アサインはmainメソッド内のオリジナルyourDogリファレンスには影響を与えず、それは引き続き元の「ベラ」オブジェクトを指し続けます。

4. Stringオブジェクト:特殊なケース

JavaにおけるStringオブジェクトはイミュータブル(Immutable:不変)です。これは、一度作成されるとそのステートを変更できないことを意味します。Stringをメソッドに渡し、それを変更しようと試みた場合、実際には新しいStringオブジェクトを作成していることになります。オリジナルのStringは元の状態を保ちます。

public class PassByValueString {
    public static void main(String[] args) {
        String text = "Hello";
        System.out.println("modifyString 呼び出し前: " + text); // 出力: Hello
        modifyString(text);
        System.out.println("modifyString 呼び出し後: " + text);  // 出力: Hello
    }

    public static void modifyString(String str) {
        str = str + " World";  // 新しい String オブジェクトを作成
        System.out.println("modifyString 内部: " + str);      // 出力: Hello World
    }
}

この例において、modifyStringメソッドは表面上textの文字列を変更しているように見えます。しかし文字列はイミュータブルであるため、str = str + " World";というコードは、実のところ完全に新しいStringオブジェクト "Hello World" を作成し、そのリファレンスをローカル変数strにアサインしています。mainメソッドのオリジナルtext変数は、依然として元の "Hello" 文字列を指し続けています。

5. 総合的な実践演習

JavaにおけるPass-by-Valueの理解を定着させるために、さらに実践的な例を探求してみましょう。

5.1 サンプル1:Array(配列)の変更

Array(配列)はReference Type(リファレンス型)に属するため、Pass-by-Valueがそれにどう影響するかを理解することは非常に重要です。

public class PassByValueArray {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.print("modifyArray 呼び出し前: ");
        printArray(numbers); // 出力: 1 2 3

        modifyArray(numbers);

        System.out.print("modifyArray 呼び出し後: ");
        printArray(numbers);  // 出力: 10 2 3
    }

    public static void modifyArray(int[] arr) {
        arr[0] = 10; // インデックス 0 のエレメントを変更
        System.out.print("modifyArray 内部: ");
        printArray(arr);      // 出力: 10 2 3
    }

    public static void printArray(int[] arr) {
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();
    }
}

このケースでは、modifyArrayメソッドはnumbers配列を指すリファレンスのコピーを受け取ります。arr[0] = 10;を実行すると、main内のnumbersmodifyArray内のarrが同じ配列オブジェクトを指しているため、メモリ上の実際の配列が変更されます。

5.2 サンプル2:カスタムオブジェクトの操作

再度Dogクラスを振り返り、機能を拡張してPass-by-Valueのデモンストレーションを深めましょう。

class Dog {
    private String name;
    private int age;

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Dog{name='" + name + "', age=" + age + '}';
    }
}

public class PassByValueDog {
    public static void main(String[] args) {
        Dog myDog = new Dog("バディ", 3);
        System.out.println("celebrateBirthday 呼び出し前: " + myDog); // 出力: Dog{name='バディ', age=3}
        celebrateBirthday(myDog);
        System.out.println("celebrateBirthday 呼び出し後: " + myDog);  // 出力: Dog{name='バディ', age=4}

        Dog yourDog = new Dog("ベラ", 5);
        System.out.println("changeDog 呼び出し前: " + yourDog); // 出力: Dog{name='ベラ', age=5}
        changeDog(yourDog, new Dog("チャーリー", 2));
        System.out.println("changeDog 呼び出し後: " + yourDog);  // 出力: Dog{name='ベラ', age=5}
    }

    public static void celebrateBirthday(Dog dog) {
        dog.setAge(dog.getAge() + 1);
        System.out.println("celebrateBirthday 内部: " + dog);      // 出力: Dog{name='バディ', age=4}
    }

    public static void changeDog(Dog originalDog, Dog newDog) {
        originalDog = newDog; // リファレンスの再アサイン
        System.out.println("changeDog 内部: " + originalDog);      // 出力: Dog{name='チャーリー', age=2}
    }
}

ここでは、celebrateBirthdayはメソッドがリファレンスのコピーを受け取り、2つのリファレンスが同一のオブジェクトを指しているため、Dogオブジェクトの年齢の変更に成功しています。しかし、changeDogoriginalDogリファレンスを新しいDogオブジェクトに再アサインしようと試みていますが、この再アサインはメソッド内部のローカルなリファレンスであるoriginalDogにのみ影響を与え、mainメソッド内のyourDogリファレンスには何ら影響を与えていません。