参照と実体

今回の内容

今回はプログラミングの「参照」という考え方について紹介します。これまではJavaの整数や実数 と配列やインスタンスなどを同じように取り扱っていましたが、コンピューターで処理される際にこれらは全く違う方法で取り扱われます。

変数に整数(int)や実数(double)などを記憶させた場合、変数にはそれらの数値がそのまま記憶されます。たとえば「int a = 10;」と書けば変数aには10という整数が、「double b = 1.5;」と書けば変数bには1.5という実数がそれぞれ記憶されます。これらの変数をプログラムから使用する際には、記憶された値をコピーしてから足し算や引き算などの計算を行います。

対して、配列やインスタンスを変数に記憶させようとした場合、配列やインスタンスそのものは変数に記憶されません。配列やインスタンスはコンピューター上の別の場所に作成されて、その場所への「参照」というものだけが変数に記憶されます。この配列やインスタンスをプログラムから利用するとき、変数に記憶された参照をたどり別の場所に作成された「実体」に対して処理を行います。

図: 参照と実体

整数や実数などとの大きな違いは、変数に記憶された参照をコピーしても実体はコピーされず、プログラム中で同じ実体を共有する点です。メソッドに配列やインスタンスの参照を渡しても同じ実体を共有しているため、起動先でこれらの実体を変更すると、同じ実体を参照する全てに影響が出ることになります。

図: 副作用

今回説明する内容はやや抽象的なものですが、Javaで配列やインスタンスを使う上では非常に重要なものです。将棋や囲碁などのプログラムを例にとると 、これらのボード(将棋盤、碁盤)はプログラム中で共有しながら少しずつ変更していく、といったやり方があると思います。その場合、このボードは様々な個所から使用され、手を進めるたびに変更されていきます。これに参照と実体をうまく使えれば、ボードの実体を1つだけ作成して、それぞれの対局者がボードの参照を持ち、自分の手番ごとに実体を変更していく、といったプログラムの書き方が行えるようになるはずです。

今回の作業

今回の例題、練習問題、課題は以下のパッケージに作成してください。

パッケージの名前
j1.lesson10

クラスの作成方法については、「クラスを作成する」を参照してください。

参照と実体

整数と実数

まずは整数と実数について見てみましょう。下記はどのような動作をするでしょうか。

リスト: ModifyParameter
package j1.lesson10;

import javax.swing.JOptionPane;

public class ModifyParameter {

    public static void main(String[] args) {
        new ModifyParameter().start();
    }

    void start() {
        int variable = 100;
        modify(variable);
        JOptionPane.showMessageDialog(null, variable);
    }

    void modify(int param) {
        param = 200;
    }
}

実行してみると明らかですが、modifyメソッドで仮引数parameterを200に変更しても、startメソッドの変数variableの値は200になりません。

void modify(int param) {
    parameter = 200;
}

これは、起動元の「modify(variable)」の部分でvariableに記憶させた値(100)をコピーしてmodifyメソッドの実引数として渡しているためです。この時点でmodifyメソッドの仮引数であるparamと、起動元の変数variableは別物になっています。

int variable = 100;
modify(variable);

このような動きを説明する場合、「変数と値」の関係を「箱と紙」の比喩 で表す場合があります。変数(または仮引数)とは値が書いてある紙を入れる箱で、変数に記憶させた値を使用する場合には、紙を取り出して内容をコピーした新しい紙を使います。上記の例ではvariableとparamは別の箱(変数)なので、paramの内容を変更してもvariableの内容が変更されることはありません。

図: 値のコピー

上記のように、Javaでは整数や実数などの値を変数にそのまま記憶させています。これらは変数から値を毎回コピーして使用するため、どこかの変数の値を変更しても、他の変数に影響が及ぶことはありません。

配列の構造

次に、配列について先ほどと似たようなプログラムを見てみましょう。

リスト: ModifyArray
package j1.lesson10;

import javax.swing.JOptionPane;

public class ModifyArray {

    public static void main(String[] args) {
        new ModifyArray().start();
    }

    void start() {
        int[] array = new int[3];
        array[0] = 0;
        array[1] = 1;
        array[2] = 2;
        modify(array);
        for (int i = 0; i < array.length; i++) {
            JOptionPane.showMessageDialog(null, i + ": " + array[i]);
        }
    }

    void modify(int[] param) {
        param[1] = 12345;
    }
}

先ほどの例とは異なり、modifyメソッドで仮引数paramの1番目の要素を12345に変更すると、startメソッドで生成した配列の1番目の要素も12345に変更されます。

void modify(int[] param) {
    param [1] = 12345;
}

この動作はJavaの配列の構造を理解しているとわかりやすいと思います。まず、配列は「new <要素型>[要素数]」という命令で作成できます。

int[] array = new int[3];

コンピューターがこの命令を処理すると、コンピューター上には配列の「実体」 が作成されます。今回は「new int[3]」という命令なので、配列の実体には3つの整数を記憶する領域が用意されます。また、変数arrayに作成した実体への「参照」だけを記憶させます。

図: 配列の構造

変数arrayに記憶させた「参照」は、作成した「実体」にたどり着くための情報を持っています。下記のように変数arrayを使用すると、「参照」だけをコピーして起動先のメソッドに渡します。

modify(array);

このため、起動先のメソッドの仮引数には、先ほどの変数arrayに記憶させた参照と同じ「実体」にたどり着くための情報が渡されることになります。つまり、modifyメソッドの中でparam変数を利用して配列を変更すると、先ほど作成した配列の実体が変更されます。

void modify(int[] param) {
    param [1] = 12345;
}

このように、配列は整数や実数と異なり、「参照」と「実体」がコンピューター上に別々に存在します。変数に記憶させるのは参照だけで、変数を使用しても参照だけがコピーされて実体を共有することになります。

図: 配列の変更

以上が配列の構造です。整数や実数は値そのものを変数に記憶させますが、配列は実体と参照が別々に存在して変数には参照だけを記憶させています。そのため、メソッドで配列を操作する場合には「実体は何か」ということを意識するように心がけましょう。

練習: 配列の構造

利用するクラスの名前
ModifyArray

ModifyArrayのmodifyメソッドを書き換えて、変更する配列の要素を、入力ダイアログから整数を入力して選択できるようにしなさい (現在はインデックスが1である要素を書き換えているが、0, 1, 2のどれかを選択する)。

入力された値が0, 1, 2のどれでもない場合には「範囲外の値です」と表示し、再度入力させること。

インスタンスの構造

配列と同様に、クラスのインスタンスも参照と実体が別々に存在する形式です。次のプログラムの動作について考えてみましょう。

リスト: ModifyInstance
package j1.lesson10;

import javax.swing.JOptionPane;

public class ModifyInstance {

    public static void main(String[] args) {
        new ModifyInstance().start();
    }

    void start() {
        Person instance = new Person();
        instance.name = "Bob";
        instance.age = 19;
        modify(instance);
        JOptionPane.showMessageDialog(null, instance.name + "の年齢は" +
            instance.age + "歳です");
    }

    void modify(Person param) {
        param.age = param.age + 1;
    }
}

class Person {
    String name;
    int age;
}

プログラムではまず、「new <クラス名>()」の形式でインスタンスを作成しています。

Person instance = new Person();

このとき、コンピューター上にインスタンスの実体が作成され、変数instanceには作成した実体への参照を記憶させています。

図: クラスインスタンスの構造

modifyメソッドを起動する際に、instanceに記憶させた参照だけをコピーして渡しています。このインスタンスの実体はコピーされずmodifyメソッドの中も共有されるため、modifyメソッドで変更したageフィールドは、起動元の変数instanceから見ても変更されています。

void start() {
    Person instance = new Person();
    ...
    modify(instance);
    ...
}
void modify(Person param) {
    param.age = param.age + 1;
}

これも図に表すと次のようになります。

図: クラスインスタンスの変更

配列とクラスのインスタンスに共通する点として、どちらもnewという命令 でコンピューター上に実体を作成し、プログラムからはそれらの参照を使用しています。1つのプログラムで複数の配列やインスタンスを取り扱いたい場合には、その数だけnewという命令で別々の実体を作ってやる必要があります。

図: 複数回の実体生成

これらの参照と実体という特性をうまく利用すると、1つの実体を様々なメソッドで少しずつ変更していくようなプログラムを簡単に書けるようになります。これはメソッドに配列やインスタンスの参照を渡して起動し、起動先でそれらの実体を操作するようなプログラムです。このように起動元にも影響を与えるようなメソッドを「副作用のあるメソッド」と呼ぶことがあります。「副作用」はあまり前向きな意味で使われませんが、プログラミングにおける副作用も同様で、使い方を間違えるとだれがどこで実体を操作しているかわからないという読みにくいプログラムになってしまいますので、注意が必要です。

練習: インスタンスの構造

利用するクラスの名前
ModifyInstane

ModifyInstanceの下に書いたPersonクラスに新しくフィールド(nicknameなど)を宣言し、それをModifyInstanceのmodifyメソッドで変更するようにしなさい。ただし、modifyメソッドでは入力ダイアログを表示(または疑似乱数を利用)し、その値を使って新しく宣言したフィールドに値を代入するようにすること。

また、変更したフィールドが正しく反映されているか確認するため、メッセージダイアログに表示する内容についても変更すること。

  • ヒント: たとえば、nickname(あだ名)などを増やしてみるとよいと思います。また、元の名前が「Bob」である必要はありませんので、好きに書き換えて下さい。

2次元配列

最後に、発展的な内容として2次元配列について紹介します。前回までに紹介した配列は、インデックス1つで要素を取り出せるという「1次元配列」というものでした。

図: 配列の構造(再掲)

対して2次元配列は要素を取り出すために2つのインデックスが必要な配列です。これは縦と横に変数を敷き詰めた表のようなデータ構造です。

図: 2次元配列(概念)

この2次元配列を利用するプログラムを見てみましょう。

リスト: Show2D
package j1.lesson10;

import javax.swing.JOptionPane;

public class Show2D {

    public static void main(String[] args) {
        new Show2D().start();
    }

    void start() {
        int[][] matrix = new int[2][3];
        matrix[0][0] = 1;
        matrix[0][1] = 2;
        matrix[0][2] = 3;
        matrix[1][0] = 4;
        matrix[1][1] = 5;
        matrix[1][2] = 6;
        for (int i = 0; i < 2; i++) {
            String line = "";
            for (int j = 0; j < 3; j++) {
                int value = matrix[i][j];
                line = line + value + " ";
            }
            JOptionPane.showMessageDialog(null, (i + 1) + "行目は" + line);
        }
    }
}

まず、配列を作成する部分が1次元配列のときと異なっています。1次元配列の変数を宣言する際には、型に「int[]」と"[]"が1回だけ指定していましたが、2次元配列では「int[][]」のように2回繰り返して書きます。また、配列の実体を作成する際にも「new int[2][3]」のように要素の個数を2回に分けて書きます。

int[][] matrix = new int[2][3];

上記の命令が作成する配列は、2×3(2行3列)の大きさを持ちます。この配列を初期化するために、プログラムでは6行にわたって要素に値を代入しています。

matrix[0][0] = 1;
matrix[0][1] = 2;
matrix[0][2] = 3;
matrix[1][0] = 4;
matrix[1][1] = 5;
matrix[1][2] = 6;

配列を作成する際に「new int[2][3]」と指定したので、1つ目の[](1次元目)には0以上2未満のインデックスを、2つ目の[](2次元目)には0以上3未満のインデックスをそれぞれ指定しています。

図: 2×3行列

最後に、繰り返しを使ってこの2次元配列の内容を表示しています。

for (int i = 0; i < 2; i++) {
    String row = "";
    for (int j = 0; j < 3 ; j++) {
        int value = matrix[i][j];
        row = row + value + " ";
    }
    JOptionPane.showMessageDialog(null, (i + 1) + "行目は" + row);
}

外側のfor文(繰り返し変数i)では1次元目の要素数である2を、内側のfor文(繰り返し変数j)では2次元目の要素数である3を指定しています。そしてこれらの内部では「matrix[i][j]」といった形で2次元配列のそれぞれの要素を使用しています。下図のように行ごと列の内容を表示していますので、「1行目は1 2 3」「2行目は4 5 6」というように2回に分けてメッセージダイアログが表示されます。

図: 2×3行列の反復

なお、Javaの2次元配列はもう少し複雑な構造をしています。表形式のデータ構造は論理的な話で、実際には「配列の配列」という形式でコンピューター上に作成されます。

図: 2次元配列の構造

つまり、2×3の配列は、整数型(int)の要素数3つの配列(int[])が2つと、配列型(int[])の要素数が2つの配列(int[][])が1つで構成されています 。「new int[2][3]」という命令はこれら3つの配列を一度に作成し、「配列の配列」の部分への参照を返しています 。

まとめると、2次元配列を利用するには、まず次のような命令で2次元配列の実体を作成します。

<要素の型>[][] <変数の名前> = <要素の型>[<1次元目の要素数>][<2次元目の要素数>];

2次元配列の要素を参照するには、次のようにインデックスを2回指定します。

<変数の名前>[<1次元目のインデックス>][<2次元目のインデックス>]

このような2次元配列の要素を利用する場合には、次のように2重になったfor文が便利です。外側の繰り返し(変数i)で1次元目を反復し、内側の繰り返し(変数j)で2次元目を反復しています。

for (int i = 0; i < <1次元目の要素数>; i++) {
    for (int j = 0; j < <2次元目の要素数>; j++) {
        … <変数の名前>[i][j] …
    }
}

以上が2次元配列の概要です。分化教材に採用した「スプレッドシート」や「キャンバス」はこの2次元配列の考え方を元に作成しています。どちらも2次元の表であり、スプレッドシートは整数や文字列などのデータを、キャンバスにはピクセルごとの色を記憶させています。2次元配列は様々なデータ構造を表す際に非常に便利な道具ですので、使いながら 理解していくのがよいと思います。

練習: 2次元配列

利用するクラスの名前
Show2D

例題Show2Dを書き換えて、さらに列ごとに2次元配列の内容を表示するプログラムを作成しなさい。

先ほどのShow2Dでは次のように、「行ごと」に表示していた。

繰り返し回数 表示する内容
1回目 1行目は1 2 3
2回目 2行目は4 5 6

これを書き換えて、次のようにすること。

繰り返し回数 表示する内容
1回目 1列目は1 4
2回目 2列目は2 5
3回目 3列目は3 6

まとめ

今回は、これまであいまいに説明していた配列やインスタンスの構造について説明しました。Javaには「値」と「参照」という2つの概念があり、整数や実数などは「値」として変数に記憶させていて、配列やインスタンスなどは「参照」として変数に記憶させています。そして配列やインスタンスなどの「実体」はコンピューター上の別の場所に作成され、参照によっていつでも取り出してプログラムから使用できます。

配列の場合、「int[] array = new int[3];」というような命令が処理された際に、配列の実体がコンピューター上に作成されます。ただし、変数arrayに記憶させるものは配列そのものではなく作成した配列の実体への参照です。

図: 配列の構造

インスタンスの場合も同様に、「Person instance = new Person();」というような命令が処理された際に、Personインスタンスの実体がコンピューター上に作成されます。そして変数instanceには作成した実体への参照を記憶させています。

図: クラスインスタンスの構造

メソッドを起動する際に実引数に配列やインスタンスを渡すと、それぞれの参照だけをコピーして渡します。このとき実体についてはコピーせず同じものを使用するため、起動したメソッド内で渡された配列やインスタンスを変更すると、起動元のメソッドで使っていた配列やインスタンスも変更されたことになります。

図: 配列の変更

また、今回は複雑な参照の例として2次元配列についても紹介しました。2次元配列は2つのインデックスを指定して要素を指定する配列で、次のように作成します。

<要素の型>[][] <変数の名前> = <要素の型>[<1次元目の要素数>][<2次元目の要素数>];

2次元配列の要素を参照するには、次のようにインデックスを2回指定します。

<変数の名前>[<1次元目のインデックス>][<2次元目のインデックス>]

これは、次のように2重になった参照と実体を順番にたどっていくような形で処理されます。

図: 2次元配列の構造

このような2次元配列の要素を利用する場合には、次のように2重になったfor文が便利です。外側の繰り返し(変数i)で1次元目を反復し、内側の繰り返し(変数j)で2次元目を反復しています。

for (int i = 0; i < <1次元目の要素数>; i++) {
    for (int j = 0; j < <2次元目の要素数>; j++) {
        … <変数の名前>[i][j] …
    }
}

以上が今回の内容です。今回はプログラムの中でも理解が難しい「参照」とその「実体」について説明しました。これらをうまく使えば、メソッドの中で実体を書き換えたりして、より複雑なプログラムを書けるようになります。

今期のプログラミング入門の内容は以上で終わりです。以降は新しい内容について触れず、復習や発展的なプログラムの作成に時間を使っていきたいと思います。