配列 (1)

一次元配列

配列とは

配列は複数のデータを扱うときに便利なデータ構造であり、同じ型のデータを一列に並べたものである。例えば、6個の整数型データ3, 5, 7, 9, 11, 13を一列に並べて扱うときは、

int[] a = {3, 5, 7, 9, 11, 13};

という宣言をすればよい。int[]int型の配列 を表す。その次の a が int 型の配列であるという宣言である (a がその配列の名前になる)。"=" 以下でその配列に初期値を与えている。配列 a の先頭の要素 (それは a[0] で表される) に値 3 を入れ、その次の要素 a[1] に値 5 を入れ、a[2] に 7、a[3] に 9、a[4] に 11、a[5] に 13 を入れることになる。配列 a の長さ (要素の数) は 6 になる。この配列 a に対して

System.out.println(a[4]);

を実行すれば 11 が出力される。

変数と配列のメタファ(比喩)

これまでに出てきた変数、配列、値を「箱」と「紙」を使ったモデルで説明する。

これまでに出てきた変数

第02週で「変数は何かを入れておく箱のようなものである」という説明をした。


上の図では、int 型の変数 x と double 型の変数 y を箱にたとえている。この箱には何らかの値が一つだけ入るが、ここではその値を紙にたとえる。すると、「変数に値を代入する」という動作は以下のような図で表せる。


上の図では int 型の変数 x に 100 と書いてある紙を格納し、double 型の変数 y に 3.14 と書いてある紙を格納している。この際、既に x や y に紙が入っていたら古いものは捨てる。ここで重要なのは、変数への代入は紙に書いてある値を書き換えるのではなく、紙を置き換えるということである。ここで出てくる「紙」は全て何らかの値が書き込まれており、値を書き換えることはできない。

「変数の値を参照する」という動作を同様に考えると、「箱から紙を取り出して新しい紙にコピーする」という作業に等しくなる。


上の図では、変数 r と 変数 pi から値を参照し、それらを掛け合わせた結果を変数 s に代入している。

変数という箱は、宣言された時点ではまだ空である (紙が何も入っていない)。そのため、次のようなプログラムはエラーになる。

...
int x;
System.out.println("x の値は " + x);
...

上の例では変数 x の値を参照しようとしているが、変数 x にはまだ紙が入っていないため、これはコンパイルエラーになる。

配列

変数 = 箱, 値 = 紙 というメタファを使うと、配列もきれいに表すことができる。


まず、

int[] a = {100, 200, 300}

という文を書くと、右辺の {100, 200, 300} という式に対して int 型の3つの箱 [0], [1], [2] が作られ、それにIDが割り振られる (IDは3つの箱が置いてある場所を示すものである)。さらに、3つの箱には順に 100, 200, 300 と書かれた紙が格納される (値が代入される)。その後、そのIDが書かれた紙が左辺の a という箱に格納される (変数 a に代入される)。

配列の各要素を参照したり、各要素に代入したりする手続も同様に表せる。


上の図では、配列 a の 1 番目の要素と 2 番目の要素を足し合わせて、その結果を 0 番目の要素に格納している。ここで「配列 a の~」という表現を使っているが、実際は「箱 a の中の紙に書いてあるIDが指し示す配列の~」という意味である。つまり、配列を格納できる変数は「何枚も紙を入れることができる箱」というわけではない。単に「たくさんの箱が置いてある場所を指し示す紙を入れることができる箱」であり、その箱に入る紙は一枚 (IDが書かれた紙) だけである。

配列の生成

初期化付き生成

これまでの例でも出てきたが、配列を作成する方法のひとつとして、各要素の初期値を決めて作成する方法がある。

int[] a = {10, 20, 30, 40};

上の例では、変数 a に「長さ4で初期値が順に10, 20, 30, 40の配列」を格納している。

これは、次の形の省略形である。

int[] a = new int[]{10, 20, 30, 40};

この、「new int[]」というのを省略できるのは、配列を格納する変数を作成したと同時に初期化を行う際のみである。つまり、以下のような省略はコンパイルエラーとなる。

int[] a;
a = {10, 20, 30, 40};

一般に、以下のような式で初期化を同時に行う配列を生成できる。

new 型の名前[] { 初期値[0]を表す式, 初期値[1]を表す式, 初期値[2]を表す式, ... }

double型の値を格納できる配列を作成する場合は、以下のようにすればよい。

double[] b = new double[]{1.0, 2.0};

ただしこれは宣言と同時に代入を行っているので、「new double[]」の部分を省略できる。

double[] b = {1.0, 2.0};

ところで、初期値の部分に「{ 初期値[0]を表す式, 初期値[1]を表す式, 初期値[2]を表す式, ... }」と書いた。ここにある通り、初期値は定数でなくてもかまわない。

double[] c = { Math.sqrt(2.0), Math.sqrt(2.0) * 2, Math.sqrt(3.0) };

上記のような式を含めても問題ない。

要素数を指定して生成

初期化付き生成では、値 (初期値) を並べることによって、その個数の長さを持つ配列を生成していた。初期値を与えずに配列を生成する場合は、次のように、配列の長さだけ指定すれば良い。

int[] a = new int[10];

上の例では、10 個の int の値を格納できる配列を作成し、そのIDを変数 a に格納している。それぞれの要素の初期値は 0 になる。

一般に、以下のような式で要素数を指定して配列を生成できる。

new 型の名前[要素数を表す式]

double型の値を格納できる、7個の要素を持つ配列を生成する場合は、以下のようにすればよい。

double[] b = new double[7];

また、要素数は定数ではなく計算式やメソッドの呼び出しの結果などを使用してもよい。ただし、int 型の値で指定してやる必要がある。

BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("配列の大きさを入力:");
int size = Integer.parseInt(reader.readLine());
double[] c = new double[size];

上記の例では、配列の大きさをコンソールに入力させ、その要素数をもつ double 型の配列を作成する。

配列の大きさは、0 以上である必要がある。

配列要素の参照

配列の各要素を参照したり、配列の要素に値を代入する場合は、次のようにする。

int[] a = {1, 2, 3};
a[0] = 10;
int b = a[2];

上の例のように、配列を表す変数 (または式) のあとに [] で、何番目の要素を指定するか書いてやればよい。この要素を指定する部分には定数だけではなく計算式などを使えるため、以下のように書くこともできる。

int[] a = {1, 2, 3};
int b = 1;
int c = a[b];

このように書くと、a[1] (配列 a の 2 番目の要素) に格納されている値が c に代入される。同様にして、次のようにも書ける。

int[] a = {1, 2, 3};
int c = a[a[0]];

これは、 a[a[0]] => a[1] => 2 と解釈され、結果として c に 2 が代入されることになる。

配列の後ろにつけられる [数値] を添え字またはインデックス と呼ぶことが多い。

配列長の取得

配列には、要素を格納するための箱だけでなく、配列の長さという情報も入っている。

int[] a = {1, 2, 3};

上記の配列の長さは 3 であるが、その情報は a.length として得られる。配列 a の最後の要素(今の場合 a[2])は a[a.length-1] である。どんな配列 h についても、その最後の要素は h[h.length-1] である。

この情報を用いると、「配列の要素を順番に参照する」などのプログラムが簡単に書ける。

int[] a = {1, 2, 3};
for (int i = 0; i < a.length; i++) {
  System.out.println(a[i]);
}

これは擬似コードで書くと、以下のように表せる。

a = {1, 2, 3}
for i を 0 から (配列 a の長さ - 1) まで
  print a[i]

つまり、配列の全ての要素を 0 番目から順に表示するようなプログラムである。上に示したプログラムを実際に実行したとすると、以下のような結果が得られるはずである。

1
2
3

配列のコピー

配列の要素をコピーする場合、以下のようにすると問題が発生することがある。

int[] a = {100, 200, 300};
int[] b = a;

これは先ほどの箱と紙のメタファで表現すると、以下の図のようになる。


これでは、a と b が指し示す配列の実体 (ID=750) が同じものになってしまっている。つまり、b の各要素に加えられた変更は a の各要素にも反映してしまうことになる。プログラムをわざわざこのようなつくりにする場合は構わないが、あまりそういう場面はない。

そこで、以下の図のように、配列 b 用に新たに箱とIDを用意して、配列 a の各要素をコピーしてやる必要がある。


これならば、a と b が指し示す配列の実体は別のものとなる。

配列の型

これまでに int 型と double 型を学んできたが、今回で配列を学んだことにより型が2種類増える。つまり、「int配列型 (int[])」と「double配列型 (double[])」である。

これらはメソッドの各引数の型や戻り値の型としても使用することができる。

public static void main(String[] args) { 
  int[] a = createArray();
  ...
}

public static int[] createArray() {
  return new int[]{10, 20, 30, 40};
}

public static void printArray(int[] array) {
  for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
  }
}

これまでに int[] を「int型の配列」と呼んでいたが、Javaを使う上では「int配列型」という認識をしたほうが楽である。Javaは型という考え方が非常に強いため、「int型の配列」という考え方では解釈が難しい場面がいくつかある。

引数の値渡しと参照渡し

メソッド呼び出しでは、実引数の値が仮引数に代入されてから、メソッドの本体が実行される。このようなメソッド呼び出しの方法は一般に値呼び出し (call by value) と呼ばれる。引数の型が整数型、実数型のときは、それらの型のデータが仮引数に代入される。実引数がそれらの型の変数であるときで、メソッドの本体の実行中に仮引数に代入が行われたとしても、実引数の値は変わらない。

たとえば


でプリントされる値は10であり、7ではない。これは呼び出し先の仮引数 (右側の箱) に 7 という値を持つ新しい紙を格納していて、決して紙に書いてある値を書き換えることではないからである。この規則によって、呼び出し側の実引数 (左側の箱) の中身が変わってしまうことは無い。

しかし、引数が配列(名)であると事情が異なる。配列名の箱には配列の実体を指し示すIDが入っているから、その ID (の値) が仮引数 (の配列名の箱) に代入される。したがって、仮引数の配列と実引数の配列は実体が同じになる。このようなメソッド呼び出しの方法は一般に参照呼び出し (call by reference) と呼ばれる。この場合は、仮引数の配列の要素への代入は実引数の配列の要素への代入になる。


どちらにしろ、呼び出し側の実引数 (左側の箱) の中身は変化していない。変化したのは、実引数にあるIDが指し示す配列の要素であって、ID自体は変更されていない。

次のプログラムは、渡された配列の各要素の値を2乗するメソッドを含む。

package j1.lesson11;

public class MapSquare {

    public static void main(String[] args) {
        double[] array = {1.0, 2.0, 3.0};
        printArray(array);
        square(array);
        printArray(array);
        square(array);
        printArray(array);
        square(array);
        printArray(array);
    }
    
    // 与えられた配列の各要素を2乗する
    public static void square(double[] a) {
        for (int i = 0; i < a.length; i++) {
            a[i] = a[i] * a[i];
        }
    }
    
    // 与えられた配列を表示する
    public static void printArray(double[] a) {
        for (int i = 0; i < a.length; i++) {
            System.out.print(a[i] + " ");
        }
        System.out.println("");
    }
}

このように、メソッドを呼び出すとその呼び出し元に何らかの影響を与える場合、そのメソッドは「副作用のあるメソッド」と呼ばれる。square(double[]) メソッドの副作用は、引数に与えられた配列の中身を変更してしまう副作用を持つ。

逆に、以下のような例は副作用の無いメソッドである。

// 与えられた配列の各要素を2乗した、新しい配列を返す
public static double[] square(double[] a) {
    double[] b = new double[a.length];
    for (int i = 0; i < a.length; i++) {
        b[i] = a[i] * a[i];
    }
    return b;
}

上の例では配列を変更するのではなく、新たに配列を生成してそれを変更している。これによって、実引数として与えられた配列の実体に影響が及ぶことはない。ただし、これを使用する場合、mainメソッドを次のように変えてやる必要がある。

public static void main(String[] args) {
    double[] array = {1.0, 2.0, 3.0};
    printArray(array);
    array = square(array);
    printArray(array);
    array = square(array);
    printArray(array);
    array = square(array);
    printArray(array);
}