繰り返し処理 というのは、どのプログラミング言語でも非常に重要で最もよく実行する処理の一つです。
繰り返し処理には、配列やコレクション(ListやMapなど)という データが数珠つなぎで保存されているものを使用することが多いのですが、ここでは配列(array)について基本を学習しましょう。
今回の学習内容
配列について
一つの変数に複数の値を集めたもの
値である文字列や数値などを「あるカテゴリー」で複数集めたものを格納するデータ構造のことを配列といいます。
配列には インデックス番号 という配列の中身を参照するための番号が先頭からふられます。
インデックス番号は 0 から始まります。
以下の簡単な例をみてください。
配列の定義方法その1
動物の名前を集めた配列「ぞう、ぱんだ、かめ、れっさーぱんだ、くじら、うさぎ」を配列として定義する場合は以下のようになります。
// 文字列型の配列animalsを定義して初期化する
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
System.out.println(animals[3]); // くじら
文字列が格納される配列animals
を定義して、値を代入(初期化)しています。
値は左側から順にインデックス番号0から値がふられるので、animals[3]
は “くじら” が出力されます。
配列の定義方法その2
// int型の配列をサイズ5で定義する
int test[] = new int[5];
test[0] = 500;
test[1] = 400;
test[2] = 300;
test[3] = 200;
test[4] = 100;
test[5] = 0; // エラー: java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
test
配列を 5 つの サイズで定義しました。ですので、このtest
配列は6つ以上のデータを持つことができないことになります。
定義後に、インデックス番号の 0 から順に適当な値を代入していき、
インデックス番号が 5 の箇所に値を代入しようとした時に、「ArrayIndexOutOfBoundsException
」という例外(エラー)が発生します。
配列のサイズを取得する
配列のサイズを取得する際は length
を使用します。
animals
配列のサイズの場合は、ドット記法を用いて animals.length
になります。
また、配列の最後の要素を取得する際も length
を使用します。
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
System.out.println(animals.length); // 5
//配列の最後の要素を取得する
System.out.println(animals[animals.length - 1]); // うさぎ
配列の要素を入れ替える
配列内の要素を入れ替えるには一工夫必要です。
例えば、インデックス番号 1 と 2 の値を入れ替える場合だと以下の順序になります。
- 1の要素を一時的な変数(
temp
)に要素を退避させる - その間に 2 の要素を 1 に上書きする
- 最後に
temp
の値を 2 に上書きする
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
System.out.println(animals[1] + ", " + animals[2]); // かめ, れっさーぱんだ
// インデックス番号1と2の要素を入れ替える
String temp = animals[1];
animals[1] = animals[2];
animals[2] = temp;
System.out.println(animals[1] + ", " + animals[2]); // れっさーぱんだ, かめ
- 配列は、要素の入れ替えが得意ではありません。
配列をコピーする
シャローコピー(Shallow Copy)
シャローコピーとは、ただただ変数のみをコピーすることをいいます。
変数をコピーするだけなので値はコピーされません。
例えば、a[]
を b[]
にシャローコピーした後に、b[1]
の要素を変更すると a[1]
の要素も同様に変更されています。
String a[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
System.out.println(a[1]); // かめ
// シャローコピー
String b[] = a;
// コピー先の要素を変更
b[1] = "らいおん";
// a[1] と b[1] の値を比較
System.out.println(a[1].equals(b[1])); // true
- 普通の手段でコピーを行うとシャローコピーになります。
ディープコピー(Deep Copy)
ディープコピーは、値そのものをまるっとコピーします。
メモリに値が複製されるため、大げさに例えると 1KB(キロバイト)の容量のあるデータをディープコピーすると両方で2KB(キロバイト)必要ということになります。
まるっとコピーされるため、どちらの値が変更されても双方で作用しません。
String a[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
// ディープコピー
String b[] = Arrays.copyOf(a, a.length);
// コピー先の要素を変更
b[1] = "らいおん";
// a[1] と b[1] を出力
System.out.println(a[1]); // かめ
System.out.println((b[1])); // らいおん
ここでは Arrays.copyOf
メソッドを使用して配列をコピーしていますが、2つ目の引数はコピーする配列のサイズを指定しています。
この Arrays.copyOf
メソッドを使用するためには、Arraysパッケージをファイルにインポート(import)する必要があるため、クラスファイルの一番上に import java.util.Arrays;
の記述が必要です。
また、別の方法で System.arraycopy(...)
[Java 11 Doc]を使用する方法があります。
- ディープコピーは容量が2倍になるということを意識してください。
配列のサイズを増やす
配列のサイズを変更する手段はJavaにはありません。
もし、どうしても配列のサイズを変更しなければならない処理が発生した場合は、上記でも使用したメソッドを使用することでサイズを変更した新しい配列を作成できます。
Arrays.copyOf
メソッドを使用するSystem.arraycopy
メソッドを使用する
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
System.out.println(animals.length); // 5
// もとのanimalsのサイズに +1 した配列を新しく作成する
String sizeChanged[] = Arrays.copyOf(animals, animals.length + 1);
System.out.println(sizeChanged.length); // 6
- 配列は、サイズ変更が得意ではありません。
配列は要素の入れ替えやサイズの変更処理があまり得意ではありません。
しかし、インデックス番号を使用してどこの要素にでもすぐアクセスできるというメリットがあります。
要素の入れ替えやサイズ変更がなく、要素を様々なインデックス番号(ランダムなど)で取得する機会の多いデータを扱う場合は、配列を使用しましょう。
逆に、要素の入れ替え処理やサイズ変更が必要で、要素をインデックス番号順に順次アクセスする機会が多いのであれば、Collection(コレクション)インタフェースを実装したクラスを使用してください。
繰り返し処理について
配列やコレクションなどの要素を順に取り出して処理を行う場合の処理を「ループ処理」または「繰り返し処理」といいます。
以下は、繰り返し処理を使用する少しの例です。
- 参加者の平均年齢を取得する
- テストの合計点を取得する
- 点数の順位を計算する
- 最も数の多い(低い)要素を取得する
- 特定の要素の出現率を計算する
ここからは主に繰り返し処理で使用する、for、for-each、while を学習します。
forを使用した繰り返し処理
for ( カウンター ; 終了条件 ; イテレーター ) {
繰り返し処理
}
for文 には次のような機能があります。
- カウンター: ループの最初に、初期化する値を宣言します。
- 終了条件: ループが終了する 条件 を設定します。
- イテレーター: ループごとの最後に行われる処理です。主に カウンター で宣言した値を増加(インクリメント)させます。
つまり、カウンター で設定した値を イテレーター で増加させて 終了条件 に達するとループを終了する、というような処理になります。
ループが何回行われるか予め分かっている場合 や 配列の反復処理 などでは for
文 を使用します。
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
for (int i = 0; i < animals.length; i++) {
System.out.println(animals[i]);
}
これは配列animals
の中身を順次に出力するループ処理です。
-
カウンタで宣言される変数
i
は indexの略でループ処理にて慣習的に使用されます。
-
ネストされた
for
の中では 変数i
が使用できないため、i
の次のj
が使用され、その更に下のループにはk
が使用されます。
breakを使用してループを離脱する
もし「れっさーぱんだ」のインデックス番号を知りたい場合、「れっさーぱんだ」以降の「くじら」と「うさぎ」の処理は不要になります。
終了条件を満たさない場合でも、break
を使用することでループを終了することができます。
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
int redPandaIndexNo = -1;
for (int i = 0; i < animals.length; i++) {
if(animals[i].equals("れっさーぱんだ")) {
// 一致したので結果を代入して処理を抜ける
redPandaIndexNo = i;
break;
}
}
if(redPandaIndexNo >= 0) {
System.out.println("れっさーぱんだのインデックス番号は" + redPandaIndexNo + "番です。");
} else {
System.out.println("れっさーぱんだは見つかりませんでした。");
}
continueを使用してループ内の処理を省略する
ループ内の処理を中断し、次のループに移行したい場合は continue
を使用します。
次の例では、「れっさーぱんだ」以外であれば出力するという処理を行っています。
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
for (int i = 0; i < animals.length; i++) {
if(animals[i].equals("れっさーぱんだ")) {
// 次のループに移行する
continue;
}
System.out.println(animals[i]);
}
for-earchを使用した繰り返し処理
配列の要素を順次に処理したい場合であれば for-earch 文を使用することができます。
for ( 型 変数名 : コレクション ) {
繰り返し処理
}
コロン(:
)より左で定義する変数には、ループ毎に配列(コレクション)の 要素で初期化されます。
以下の例だと、最初のループでは「ぞう」が 変数val
に代入されており、次のループでは「かめ」、その次は「れっさーぱんだ」、と配列の先頭の要素から順次代入されます。
String animals[] = {"ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ"};
for(String val : animals) {
System.out.println(val);
}
for文と異なり簡潔にループを表現できますが、注意点もあります。
インデックス番号を使用する場合はfor文を使用する
以下は for文を使用して「れっさーぱんだ」のインデックス番号を出力する例です。
for (int i = 0; i < animals.length; i++) {
if(animals[i].equals("れっさーぱんだ")) {
System.out.println("れっさーぱんだのインデックス番号は" + i + "番です。");
break;
}
}
これを for-earch文で表現するとこうなります。
int i = 0;
for(String val : animals) {
if(animals[i].equals("れっさーぱんだ")) {
System.out.println("れっさーぱんだのインデックス番号は" + i + "番です。");
break;
}
i++;
}
for-earch文だとコード行が増える
for-earch文の場合、カウンタをブロック({}
)の外側で定義する必要があり、カウンタも作成する必要があります。すると、for文に比べコード行が増えてしまい、誤ってカウンタを削除してしまったりするようなミスが発生する恐れがあります。
for-earch文だとカウンタがローカル変数になってしまう
for文では、for内でカウンタを宣言できますが、for-earch文ではブロッグ({}
)の外側でカウンタを定義する必要があります。
もし for-earch文の後に、for文を記述した場合、カウンタの変数 i
はローカル変数で既に宣言されているため使用できなくなります。
int i = 0; // ローカル変数で宣言している
for(String val : animals) {
}
for (int i = 0; i < animals.length; i++) {
// ^ 変数名の重複とみなされ宣言できない
}
- インデックス番号を使用しない配列やコレクションのループ処理に適しています。
-
カウンタを使用する必要がある場合は
for
文を使いましょう。
-
for
と同様にbreak
、continue
が使用できます。
whileを使用した繰り返し処理
上限が定められていない繰り返し処理では、while文を使用します。
例えば、「0以上100未満の数値がランダムで生成される処理の中で、特定の数値が出現するのは何番目か」という結果を得たい場合は、while文が力を発揮します。
「何番目」に出現するか分からないので 繰り返し上限を定める for文やコレクションが必要な for-earch文は使用できません。
while ( 条件式 ) {
繰り返し処理
}
while文には、boolean型で返されるループの継続条件を記す必要があります。
次の例は、ランダムで0以上100未満の数値が出力される場合に、0 は何番目に出現したかという結果を出力します。
// ランダムで数値 0 が何回目に出現するか
int count = 0;
int randomValue = -1;
// whileの条件式には「0でなければ継続する」を以下のように設定する
while(randomValue != 0) {
Random random = new Random();
// 0以上100未満のランダム値を randomValue に代入する
randomValue = random.nextInt(100);
// 回数をインクリメントする
count++;
}
System.out.println("0以上100未満が生成される乱数で 0 は" + count + "番目に出現しました。");
ランダム値を生成するためには Randomパッケージ を使用する必要があります。
ファイルの上部(クラス宣言の上)に、import java.util.Random;
を記述して Randomパッケージを読み込むよう記述します。
条件式が真のときはループを継続するため、while (true) {}
とすると永久ループになってしまいます。また、論理的に到達不可能な条件を設定した場合も永久ループになります。
永久ループは文字通り、プログラムが永久的にwhile文の中をループしてしまうというバグです。
必ず永久ループを発生させないような条件式を設定してください。
-
while
を使用する前にfor
またはfor-earch
で代替することができないか考えましょう。
- 使用する場合は、永久ループに気をつけましょう。
-
for
と同様にbreak
、continue
が使用できます。
二元配列(おまけ)
配列の要素にさらに配列を設定することができます。
配列の中に配列が入っているので二次元配列と呼ばれます。
例えば、「将棋」や「オセロ」などの縦横のマスがあるボードゲームをプログラムする場合は二次元配列を使用します。
以下のコードは、二次元配列の定義の仕方と要素の呼び出し方です。
// 二次元配列
int board[][] = {
{1, 2, 3},
{4, 5, 6},
{8, 8, 9}
};
System.out.println(board[1][2]); // 6
二次元配列の要素すべてを出力する場合は、以下のように for文を ネストして使用します。
// 二次元配列の要素をインデックス番号の昇順に出力する
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board.length; j++) {
System.out.println(board[i][j]);
}
}
課題
String animals[] = { "ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ" };
配列animals
の 最初のインデックス番号を出力するプログラムを作成してください。
String animals[] = { "ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ" };
配列animals
の 最後のインデックス番号を出力するプログラムを作成してください。
String animals[] = { "ぞう", "かめ", "れっさーぱんだ", "くじら", "うさぎ" };
配列animals
の インデックス番号 1 と インデックス番号 3 の要素を入れ替えてください。
また、入れ替えが正常に行われていることを出力して確認するプログラムを作成してください。
int numbers[] = new int[]{4, 3, 5, 1, 6, 9, 16, 73, 23, 44};
for文を使用して 数値 9 の要素のインデックス番号を出力するプログラムを作成してください。
int numbers[] = new int[]{4, 3, 5, 1, 6, 9, 16, 73, 23, 44};
for文を利用してすべての要素を -1 に初期化するプログラムを作成してください。
int numbers[] = new int[]{4, 3, 5, 1, 6, 9, 16, 73, 23, 44};
for-earch文を使用して、すべての要素の数値を足し算した結果を出力するプログラムを作成してください。
Random random = new Random();
// 0以上100未満のランダム値を 変数randomValue に代入する
randomValue = random.nextInt(100);
while文を使用して、乱数が代入されるrandomValue
に、23 と 77 がそれぞれ何番目に出現したか結果を出力するプログラムを作成してください。