配列
ソートや探索などのアルゴリズムの勉強をしていると必ずと言っていいほど配列が出てきますね。
配列はデータ構造の一つで、実はソートや探索などのアルゴリズムに配列は適しているんです。
配列に適してる・適していないなどはあまり考えたことがなかったです。
システム開発では大量のデータを扱うので、メリット・デメリットを考えて使い分ける必要があります。
なるほど。
Javaで配列を使ったプログラムを書こうとしたとき、配列のオブジェクトを作ったり、例外が発生したりして使うのが苦手なんです。
特にJavaでは配列オブジェクトの作り方もいくつかパターンがありますし、参照型として扱われるので慣れるまで時間が掛かるかもしれませんね。
また、参照型が出てきましたね。参照型は前回勉強したので何となく分かるようになってきました。
配列は参照型として扱われるし、データ構造の一種でもあるなど、いろいろと覚えることが多いですが、整理しながら学習していきましょう。
配列は大量のデータをたった一つの変数で管理できるとても便利な機能です。システム開発では大量のデータを扱うことが多いですから、配列は必須のアイテムとなります。配列は便利な面だけでなく、デメリットもあります。さらに配列は使い方を間違えると例外が簡単に発生します。配列がコンピュータ上でどのように扱われているのか、その仕組みをしっかりと理解するようにしましょう。
参照型の変数とデータの関係
JavaやPythonのようなオブジェクト指向言語では配列は参照型のデータとして扱われます。参照型のデータを変数で扱うときのポイントを簡単に復習しましょう。参照型は「変数の中には直接データが入っているのではなく、データは別の場所に存在し、変数にはそのデータのアドレスが入っている」とう関係でした。
参照型については別の記事で詳しく説明していますので、そちらも参考にしてください。
配列型の変数宣言
参照型の変数とデータの関係を復習したところで次は実際に配列の操作について見ていきましょう。ここでは筆者の好きなJava言語のコードで解説を進めて行きます。
int[] array;
配列の変数を宣言しただけでは肝心の配列自体はまだ作られておらず、単に配列のアドレスを格納できる変数を作っただけであることに注意しましょう。
Javaは静的型付けの強い言語ですから、配列の変数を宣言をする場合、int型やString型など要素の型もきちんと指定する必要があります。基本型でも参照型でも型であれば、あらゆる指定が可能です。
配列オブジェクトの作り方
配列の変数だけ宣言しても、配列のアドレスを入れる箱を作っただけですので、実際に配列を使いたい場合は配列のオブジェクトを生成する必要があります。
では次に配列オブジェクトの生成方法を見ていきましょう。
int[] array = new int[3];
String[] array = new String[3];
右辺の new int[3] や new String[3] は3つの要素を持った配列オブジェクトの生成を表しています。それを左辺の変数に代入していますが、ここで代入されるのは配列そのものではなく配列が存在しているメモリ上のアドレスであることに注意しましょう。
new int[3] は3つの要素を持った配列を作りますが、配列の各要素は 0 で初期化されます。一方、new String[3]は同じく3つの要素を持った配列を作りますが、各要素は参照型の初期値である null で初期化されます。
上記のコード例はいずれも配列に部屋を用意しただけで、部屋の中には仕様で決められた初期値しか入っていません。
配列の要素への代入
配列のオブジェクトを作っただけでは初期値しか入りませんので、配列の各要素への代入の仕方について見ていきましょう。
配列の要素への代入はシンプルです。変数名の後ろに添え字で要素の場所を指定するだけです。
array[1] = 20;
配列の添え字は0から数えますので、array[1]は配列の先頭から2番目の要素を指すことになります。
array[2] = “apple”;
左記の例では配列の先頭から3番目の要素にappleという文字列が代入されます。
初期化子を使った配列の初期化
配列オブジェクトの生成と代入方法を見てきましたが、初期化子(初期化ブロック)を使うと配列の生成と初期化を1行で表現することができます。
int[] array = new int[]{10, 20, 30};
String[] array = new String[]{“apple”, “banana”, “coffee”};
初期化子を使った配列オブジェクトの生成では new XXX[] を省略することもできます。こちらの使い方の方が一般的です。
int[] array = {10, 20, 30};
String[] array = {“apple”, “banana”, “coffee”};
ちなみに初期化子だけで({10, 20, 30}のように)書けるのは変数の宣言時のみです。変数への代入時に同じ書き方をするとコンパイルエラーとなります。代入するときは new XXX[] を省略できませんのでご注意ください。こういった融通の利かなさがJavaの愛しいところだと筆者は思うのですが、どうでしょうか?
String[] array;
array = {“apple”, “banana”, “coffee”}; //コンパイルエラー!
Javaでの配列の扱い
Java言語仕様に依存する内容となりますがJavaでの配列の扱いについて触れておきます。
Javaでは配列は厳密にはクラスではありませんが、配列が持っているClassオブジェクトは他のクラス同様に扱えます。具体的には以下のような扱いになっています。
- 配列のスーパークラスはObjectクラスである
- 配列はCloneableとSerializableインタフェースを実装している
配列は厳密にはクラスではないようですが、ほとんどクラスのように扱えます。参考までにJavaのドキュメントに書かれている内容を引用しておきます。筆者の方で少し変更を加えていますが、ドキュメントの中にコードのサンプルの記載もありましたのでそちらも引用しておきます。
The Java® Language Specification Java SE 22 Edition
https://docs.oracle.com/javase/specs/jls/se22/html/jls-10.html
10.8. Class Objects for Arrays
Every array has an associated Class object, shared with all other arrays with the same component type.
Although an array type is not a class, the Class object of every array acts as if:
・The direct superclass of every array type is Object.
・Every array type implements the interfaces Cloneable and java.io.Serializable.
public class ArraySample {
public static void main(String[] args) {
int[] array = new int[3];
System.out.println(array.getClass().getSuperclass());
for (Class<?> c : array.getClass().getInterfaces())
System.out.println("Superinterface: " + c);
for (Method c : array.getClass().getMethods())
System.out.println("Superinterface: " + c);
}
}
配列の例外
配列はとても便利な機能ですなのですが、使い方を間違えると簡単に実行時の例外が発生します。特に配列の添え字には変数を使用することができるため添え字の範囲外の例外が現場では多く見受けられます。ヌルポー(NullPointerException)と同じくらいよく見かける例外と言えるかもしれません。
典型的な例外についてコード例とともにご紹介します。
ArrayIndexOutOfBoundsException
int[] array = {10, 20, 30};
int num = array[3];
java.lang.ArrayIndexOutOfBoundsException
左記の配列は長さが3ですが、添え字に3を指定すると4番目の部屋をさすため、範囲外となり例外が発生します。配列はfor文と組み合わせて使うことが多く、その場合、添え字に変数を使うため繰り返す条件を間違えると簡単のこの例外が発生します。
NegativeArraySizeException
int[] array = new int[-10];
java.lang.NegativeArraySizeException
このような間違いはほとんど見ることはないと思いますが、配列の大きさの指定に変数を使っているとうっかりミスもあり得るため注意が必要ですね。
ArrayStoreException
public class ArrayException {
public static void main(String[] args) {
B[] b = new B[10];
A[] a = b;
a[0] = new A();
}
}
class A{}
class B extends A{}
java.lang.ArrayStoreException
配列は本来要素の型が異なれば、互換性はないのですが、要素の型が継承関係にあるとき、親クラスの配列に子クラスの配列が代入ができてしまいます(このような性質を共変と呼びます)。左記の例では変数aが参照しているのはB型の配列にも関わらず、5行目でAクラスのオブジェクトを代入しています。この行を実行した瞬間に例外が発生します。かなり特殊な例外だと思いますが、Javaのドキュメントにも明記されていますので、たまにやってしまうミスかもしれませんね。
The Java® Language Specification Java SE 22 Edition
https://docs.oracle.com/javase/specs/jls/se22/html/jls-10.html
10.5. Array Store Exception
For an array whose type is A[]
, where A is a reference type, an assignment to a component of the array is checked at run time to ensure that the value being assigned is assignable to the component.
If the type of the value being assigned is not assignment-compatible with the component type, an ArrayStoreException is thrown.
配列のメリットとデメリット
配列は同じ大きさのデータを連続して並べたデータ構造ですので、データ構造のメリット・デメリットがそのまま配列のメリット・デメリットになります。データ構造上のメリットは同じ大きさのデータが並んでいるため、要素を指す添え字を指定すれば「先頭アドレス + (データの大きさ ✕ 添え字)」の演算で瞬時に目的のデータへアクセス出来ることです。どの要素へも瞬時にアクセスできるため、配列内のデータ同士の交換も素早く行えます。
逆にデメリットとしては配列へのデータの追加や挿入、削除といった操作には向いていないことです。連続的にデータが並んでいるため、追加や挿入、削除が行われるとデータをずらす必要があり、大量のコピーが発生してしまうからです。追加や挿入、削除が頻繁に行われるような処理では連結リストのようなリンク構造が向いています。
データをどのように扱いたいかでデータ構造を選ぶ必要があります。新人プログラマのみなさんには出来るだけ多くのデータ構造を学ぶことをお勧めします。
プログラム的な観点から言うと、一番大きなメリットとしてはたった一つの変数で大量のデータを管理できることに尽きると思います。普通の変数でデータを管理するとデータの数だけ変数を用意する必要があります。データ数が多くなってくると配列を使わないと管理しきれませんよね。ただしデメリットもあります。それは同じ型(または互換性のある型)のデータしか扱えない点です。ただし、これはPythonのような動的型付けであるプログラミング言語ではあまり気にならないデメリットかもしれませんが。
まとめ
配列の主なポイントについてまとめておきます。
- 配列は同じ大きさのデータが連続して並んだデータ構造である
- オブジェクト指向言語では配列はオブジェクトとして扱われる
- オブジェクト指向言語での配列は参照型として扱われる
- データの参照や交換は高速だが、追加・挿入・削除は低速である
- 一つの変数で全データにアクセスできる
配列の仕様はプログラミング言語によって異なりますので、それぞれの言語仕様を確かめて、使いこなせるようになりましょう。
さいごに
配列は参照型ですので変数同士のやり取りはすべて配列のアドレスで行われます(参照渡し)。つまり関数やメソッドの引数で配列を渡すということは配列のアドレスを渡すことになります。参照渡しになるため、元の配列は、関数やメソッド内で共有されることになりますので、別の場所(別の変数)から配列の要素を変更するときには注意が必要です。ライブラリのAPIの中には非破壊の(元の配列を壊さずに操作できる)APIも用意されています。自前で関数やメソッドを作るときはもちろん、ライブラリを使うときも破壊系か非破壊系かを意識するのはプログラマの責任となります。「参照型 = アドレスを扱う」を徹底して頭に叩き込みましょう。
今回は配列の仕組みに重点を置いて見てきましたが、配列には多次元配列という仕組みもあったり、配列を扱うためのライブラリも沢山ありますので、もっと様々な使い方ができます。配列を極めるにはまだまだ学ぶことがありますが、まずは配列の仕組みをきっちりとマスターしましょう。