多次元配列

多次元配列ですが、二次元配列は何とか使えるのですが、三次元以上になると上手く使えません。

業務では四次元や五次元などの配列が使われることもあります。どんな次元の配列が出てきても読み書きできるようになっておかないと困りますよね。
二次元配列は使えて、三次元以上になる使えなくなるのはなぜですか?

一次元配列は直線、二次元配列は縦横に並んだExcelの表と考えればイメージしやすいのですが、三次元は立体なのでもう頭の中がこんがらがってイメージできなくなるからだと思います。

う~ん、確かに我々の世界では一次元は直線、二次元は面、三次元は立体なのですが、配列が実際にコンピュータ上でそのように管理されていると思ってます?

え?違うんですか?
そもそもどうやってデータが並んでいるかなんて気にしていませんでした。

メモリは1バイトずつ0番地から順番にアドレスが割り当てられているので、多次元配列でもデータは直線にしか並んでいません。

言われてみれば確かに。「次元」という言葉に引っ張られ過ぎていたのですね。

多次元配列は配列の中に配列が入っている入れ子構造ですので、イメージするならマトリョーシカ人形の方が近いですね。と言っても実際には入れ子になっているわけではなく、配列は参照型ですので、配列の中に入っているのは配列のアドレスの方です。ただ、コードを読み書きするときはマトリョーシカのように入れ子になっているとイメージすれば問題ありません。

マトリョーシカ人形ですか。やっぱりややこしいことには変わりないですね。

多次元配列は一見ややこしいように思えますが、一次元配列と変数の関係さえ分かっていればいいので、実は全然難しくないんですよ。

配列は多次元で扱うことができてとても便利なのですが、コンピュータがメモリ上でどのように多次元配列を管理しているかをイメージできないと、扱うのは難しいです。入門書レベルの参考書では二次元配列までは取り扱ってくれる書籍が多いのですが、三次元以上を扱っている書籍はあまり見たことがありません。それゆえ参考書等でしか学んでいない新人プログラマにとって多次元配列はまさにつまずきポイントの一つだと思います。多次元配列を攻略する方法はいたって簡単で一次元配列の仕組みをしっかりと理解しているかどうかです。今回もJava言語をベースに解説していきます。それでは詳しく見ていきましょう。
一次元配列のオブジェクトと変数の関係
オブジェクト指向言語では配列はオブジェクトとして扱われますので、配列の変数の型は参照型となります。参照型の変数はデータ自体が変数に格納されるのではなく、データのアドレスを格納するという少しややこしい管理方法になります。また配列はたくさんの変数が順番に並んだ形のデータ構造で、一つ一つの部屋(配列の要素)には変数名は付いてはいないため、配列の変数名と添え字(インデックス)を使って各要素にアクセスします。一つ一つの配列の要素はいわば一つの変数に相当します。変数に基本型と参照型があるように、配列の要素にも基本型と参照型の二種類がありますので、コードを読むときは要素の型が何型かをきちんと確認するようにしましょう。配列の要素の型が基本型の場合と参照型の場合のメモリ上のイメージ図を掲載しておきます。


配列は参照型ですので、変数の中には配列オブジェクトのアドレスが代入されていることを再確認しましょう。
配列は変数が順番に並んでいるとイメージするととても分かりやすいです。変数に基本型と参照型があるように配列の要素の型にも基本型と参照型があります。
要素の型が参照型の場合、要素に入っている値は代入されているオブジェクトのアドレスであることに注意しましょう。
参照型や配列の基本が理解出来ているかがいまいち不安という方は以下のブログをご参照ください。


二次元配列
多次元配列は二次元以上の配列のことで「配列の配列」と表現されることが多いです。「配列の配列」とは「配列の各要素の中に配列が入っている」という意味ですので、多次元配列は配列同士の入れ子構造のことを言います。ここで注意が必要なのは「配列の各要素の中に配列が入っている」と表現していますが、配列は参照型ですので、実際に配列の各要素の中に入っているのは配列のアドレスになります。配列は入れ子構造ですのでマトリョーシカ人形のようにイメージすればいいのですが、実際には配列のアドレスが入っていることを常に頭の片隅に置いてコードを読み書きするようにしましょう。
言葉で説明しても分かりにくいと思いますので、3×3の二次元配列のメモリ上のイメージ図を示しておきます。

変数が直接参照している配列は一次元目の配列となります。
※上図の例では100番地にある配列を参照しています。
一次元配列の各要素から参照されているのが二次元目の配列となります。
※上図の例では1000番地、2000番地、3000番地にある配列を参照しています。
目的のデータ(この図ではint型のデータ)は二次元目の配列の各要素に格納されています。
この図の例では表で言えば縦3✕横3のデータを作っているのですが、「縦3」を一次元目の配列が作っていて、「横3」を二次元目の配列が作っていることになります。ポイントは配列オブジェクトは全部で4つ作られていることです。
オブジェクト指向言語では一つの配列は一つのオブジェクトとして管理されています。例えば今回の二次元配列を例に取ると、二次元目の配列は3つのオブジェクト(1000番地、2000番地、3000番地に作られたオブジェクト)として存在し、それぞれの配列は独立した個別のオブジェクトとして管理されています。さらに配列の長さは配列オブジェクトごとに自由に指定できますので、多次元配列での配列の大きさは一定でなくてもいいということになります。このように配列の大きさがバラバラで指定できる多次元配列を「ジャグ配列」とも呼びます。
配列の長さがバラバラな二次元配列の例を以下に示します。このような管理が出来るのは配列一つ一つが独立したオブジェクトとして作ることが出来るからです。

上図を例にとって配列の長さと要素の取得方法について見ていきます。
コード | コードの意味 | 格納されている値 |
---|---|---|
array | 100番地の配列を参照 | 100 |
array[0] | 1000番地の配列を参照 | 1000 |
array[1] | 2000番地の配列を参照 | 2000 |
array[2] | 3000番地の配列を参照 | 3000 |
array.length | 100番地の配列の長さ | 3 |
array[0].length | 1000番地の配列の長さ | 3 |
array[1].length | 2000番地の配列の長さ | 4 |
array[2].length | 3000番地の配列の長さ | 2 |
コード | 値 | コード | 値 | コード | 値 |
---|---|---|---|---|---|
array[0][0] | 10 | array[1][0] | 20 | array[2][0] | 30 |
array[0][1] | 11 | array[1][1] | 21 | array[2][1] | 31 |
array[0][2] | 12 | array[1][2] | 22 | – | – |
– | – | array[1][3] | 23 | – | – |
今回の配列と同じになるようなコード例を掲載しておきます。
public class TwoDimensionalArray {
public static void main(String[] args) {
//初期化子を使った配列の生成
int[][] array = {
{10, 11, 12},
{20, 21, 22, 23},
{30, 31}
};
//for文を使った各要素の取得例
for(int i = 0; i < array.length; i++) { //一次元目の配列のループ
for(int j = 0; j < array[i].length; j++) { //二次元目の配列のループ
System.out.print(array[i][j] + " "); //各要素を取得し表示
}
System.out.println(""); //改行
}
}
}
三次元配列
二次元配列の仕組みが分かれば三次元配列はさらに一階層ネスト(入れ子)が深くなっただけですので、もう簡単にイメージできますよね。三次元配列のメモリ上のイメージ図を示しておきます。自分のイメージと合っているでしょうか?

変数arrが直接参照している配列は一次元目の配列、一次元目の配列の各要素から参照されているのが二次元目の配列、二次元目の配列の各要素から参照されているのが三次元目の配列、といった構造になっています。
最終的なデータを取得するためには変数名の後ろに一次元目の要素の添え字、次に二次元目の要素の添え字、最後に三次元目の要素の添え字を指定して各データを取得します。
例)arr[0][0][0]と書くと100が取得できます。
変数と一次元目の配列の関係、一次元目の配列の各要素とそこから参照される二次元目の配列との関係、二次元目の配列の各要素とそこから参照される三次元目の配列との関係はそれぞれ一次元配列と変数の関係と全く同じ構図になっています。これが前述した一次元配列と変数の関係さえ理解すれば多次元配列は簡単に理解出来ると言った所以になります。
この図と同じになるようなコード例を掲載しておきます。
初期化子を使って一気に配列を生成しているコードと一次元配列を基に二次元、三次元と順番に配列を作っていくコードの2例をご紹介します。
public class ThreeDimensionalArray1 {
public static void main(String[] args) {
//初期化子を使った配列の生成
int[][][] array = {
{
{100, 101, 102},
{110, 111, 112},
{120, 121, 122},
},
{
{200, 201, 202},
{210, 211, 212},
{220, 221, 222},
},
{
{300, 301, 302},
{310, 311, 312},
{320, 321, 322},
}
};
//for文を使った各要素の取得例
for(int i = 0; i < array.length; i++) { //一次元目の配列のループ
for(int j = 0; j < array[i].length; j++) { //二次元目の配列のループ
for(int k = 0; k < array[i][j].length; k++) { //三次元目の配列のループ
System.out.print(array[i][j][k] + " "); //各要素を取得し表示
}
System.out.println(""); //改行
}
System.out.println(""); //改行
}
}
}
public class ThreeDimensionalArray2 {
public static void main(String[] args) {
//一次元配列の生成
int[] array0_0 = {100, 101, 102};
int[] array0_1 = {110, 111, 112};
int[] array0_2 = {120, 121, 122};
int[] array1_0 = {200, 201, 202};
int[] array1_1 = {210, 211, 212};
int[] array1_2 = {220, 221, 222};
int[] array2_0 = {300, 301, 302};
int[] array2_1 = {310, 311, 312};
int[] array2_2 = {320, 321, 322};
//二次元配列に代入
int[][] dArray0 = new int[3][3];
dArray0[0] = array0_0;
dArray0[1] = array0_1;
dArray0[2] = array0_2;
int[][] dArray1 = new int[3][3];
dArray1[0] = array1_0;
dArray1[1] = array1_1;
dArray1[2] = array1_2;
int[][] dArray2 = new int[3][3];
dArray2[0] = array2_0;
dArray2[1] = array2_1;
dArray2[2] = array2_2;
//三次元配列に代入
int[][][] array = new int[3][3][3];
array[0] = dArray0;
array[1] = dArray1;
array[2] = dArray2;
//for文を使った各要素の取得例
for(int i = 0; i < array.length; i++) { //一次元目の配列のループ
for(int j = 0; j < array[i].length; j++) { //二次元目の配列のループ
for(int k = 0; k < array[i][j].length; k++) { //三次元目の配列のループ
System.out.print(array[i][j][k] + " "); //各要素を取得し表示
}
System.out.println(""); //改行
}
System.out.println(""); //改行
}
}
}
多次元配列
一次元配列に配列を代入すると二次元配列、二次元配列にさらに配列を代入すると三次元配列、この繰り返しとなっているのが多次元配列となります。多次元配列を正しくイメージ出来るようになったでしょうか?
直線や表、立体などのイメージとは全然異なるものだというのがお分かりいただけたと思います。
多次元配列の仕組みが分かったところで、実際に多次元配列を使ったプログラムを書いてみましょう。
ある都市の全小学校の全校生の五教科分の得点を配列を使って管理してください。
一次元配列・・・五教科分の点数の管理
二次元配列・・・クラス単位での管理
三次元配列・・・学年単位での管理
四次元配列・・・学校単位での管理
五次元配列・・・都市単位での管理
今回は都市における複数の学校単位での管理となるため五次元配列で管理できそうですね。
すべてのデータを用意すると膨大な量になりますので、サンプルコードとして二人分のデータを学校単位で管理する内容とします。
public class MultidimensionalArray1 {
public static void main(String[] args) {
//一人分の五教科のデータの管理(一次元配列)
int[] taro = {82, 93, 74, 100, 96}; //太郎くんの五教科分のデータ
int[] hanako = {91, 78, 82, 89, 98}; //花子さんの五教科分のデータ
//クラス単位でのデータの管理(二次元配列)
int[][] classA = {taro, hanako};
//学年単位でのデータの管理(三次元配列)
int[][][] grade1 = {classA};
//学校単位でのデータの管理(四次元配列)
int[][][][] school1 = {grade1};
//都市単位でのデータの管理(五次元配列)
int[][][][][] city = {school1};
//太郎くんの成績の表示
for (int i = 0; i < city.length; i++) { //学校の数だけループを回す
for (int j = 0; j < city[i].length; j++) { //学年の数だけループを回す
for (int k = 0; k < city[i][j].length; k++) { //クラスの数だけループを回す
for (int l = 0; l < city[i][j][k].length; l++) { //生徒の数だけループを回す
for (int m = 0; m < city[i][j][k][l].length; m++) { //教科の数だけループを回す
System.out.print(city[i][j][k][l][m] + " "); //各教科の点数表示
}
System.out.println(); //改行
}
}
}
}
}
}
Javaには拡張forループという機能がありますので、拡張forループを使ったサンプルコードも掲載しておきます。拡張forループを使うと、あたかもマトリョーシカ人形のように外側の配列から順番にぱかっと開けて中の配列を取り出している、といった感覚を実感できると思います。
public class MultidimensionalArray2 {
public static void main(String[] args) {
//一人分の五教科のデータの管理(一次元配列)
int[] taro = {82, 93, 74, 100, 96}; //太郎くんの五教科分のデータ
int[] hanako = {91, 78, 82, 89, 98}; //花子さんの五教科分のデータ
//クラス単位でのデータの管理(二次元配列)
int[][] classA = {taro, hanako};
//学年単位でのデータの管理(三次元配列)
int[][][] grade1 = {classA};
//学校単位でのデータの管理(四次元配列)
int[][][][] school1 = {grade1};
//都市単位でのデータの管理(五次元配列)
int[][][][][] city = {school1};
//拡張forループを使った例
for (int[][][][] school : city) { //五次元配列から四次元配列を取り出す
for (int[][][] grade : school) { //四次元配列から三次元配列を取り出す
for (int[][] klass : grade) { //三次元配列から二次元配列を取り出す
for (int[]person : klass) { //二次元配列から一次元配列を取り出す
for (int subject : person) { //一次元配列から一教科ごとの点数を取り出す
System.out.print(subject + " "); //各教科の点数表示
}
System.out.println(); //改行
}
}
}
}
}
}
まとめ
多次元配列の重要なポイントをまとめておきます。
- 多次元配列は配列の中に配列が入っている入れ子の構造を持った配列である
- 配列の各要素に入っているのは配列そのものではなく、配列のアドレスである
- 多次元配列はメモリがある限りいくつでも入れ子にできる
- 多次元配列の各配列の長さ(要素数)は一定でなくてもよい(ジャグ配列)
さいごに
多次元配列は配列の入れ子構造ですので、一次元配列と変数の関係さえ分かっていれば、あとはそれが入れ子になっていると考えればいいだけです。ところが初心者用の入門書などを見ていると二次元配列は表、三次元配列は立体のようなイメージで図解されて説明されていることもあります。我々の日常での「次元」のイメージを使って説明しているので、イメージしやすい面はあるのですが(特に二次元配列は表で考えた方が分かりやすいです)、それがかえって多次元配列を理解しにくいものにしていると思います。なぜなら四次元以上の配列が頭の中でイメージできなくなってしまうからです。「次元」という言葉のイメージに引っ張られずに「入れ子」というイメージをしっかりと持ちましょう。
ただし入れ子になっているといってもそれも単なるイメージで、配列は参照型ですので、配列の要素の中に入っているのは、代入されている配列のアドレスになります。ですので配列を扱うときは常に「参照渡し」となることには注意しましょう。
業務ではどんな多次元配列が出てくるか分かりませんので、どんな多次元配列が出てきても即座に読めるようにしておかないと仕事になりません。そのためには多次元配列をコンピュータがメモリ上でどのように管理しているかを理解する必要があります。苦手だった多次元配列が得意になるよう、このブログが役立つのであれば執筆者冥利に尽きます。
今回も最後まで読んでいただいてありがとうございました。
