はじめに
Arrays.sort() と Arrays.toString() はよく使う。でも equals() や copyOf() は、いざ使おうとすると「これで合ってたっけ」となることが多かった。
特に copyOf() まわりは、「コピーしたつもりが元の配列に影響が出た」という経験が一度あって、そのとき表面的に対処しただけで終わっていた気がする。改めてまとめておく。
Arrays クラスとは
java.util.Arrays は、配列操作のためのユーティリティクラスで、すべてのメソッドが static として定義されている。インスタンス化は不要で、クラス名から直接呼び出す。
import java.util.Arrays;
まずこのインポートが必要。java.util パッケージなので自動でインポートされることはない。これを忘れてコンパイルエラーを出したことがある。
sort() — ソートの内部アルゴリズムまで把握していなかった
int[] numbers = {34, 2, 11, 23, 0};
Arrays.sort(numbers);
String[] names = {"Steve", "Alice", "Ema", "Jack"};
Arrays.sort(names);
プリミティブ型の配列にはデュアルピボットクイックソートが使われる。文字列などオブジェクトの配列には Timsort が使われる。どちらも昇順ソートで、降順にしたい場合は別途工夫が必要になる。
「なんとなく動く」でここ数年使ってきたが、内部アルゴリズムが型によって異なることは把握できていなかった。
equals() — == との違い
配列同士を == で比較すると、参照(メモリアドレス)の比較になる。中身が同じでも false が返る。
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = {1, 2, 3, 4, 5};
int[] arr3 = {5, 4, 3, 2, 1};
System.out.println(Arrays.equals(arr1, arr2)); // true
System.out.println(Arrays.equals(arr1, arr3)); // false
Arrays.equals() は要素の値と順序が一致していれば true を返す。順序が異なれば false になる点も重要で、「同じ要素が入っていればいい」という用途には別の手段が必要になる。
toString() — 素で出力すると意味不明な文字列が出る
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers); // [I@d716361
System.out.println(Arrays.toString(numbers)); // [1, 2, 3, 4, 5]
System.out.println() に配列を直接渡すと、データ型とハッシュコードが出力される。[I は int 型の配列を意味し、後ろの16進数はヒープ上のメモリアドレスを表している。
余談だが、この出力を初めて見たとき何のことかまったくわからなかった。デバッグ中に配列の中身を確認したいなら Arrays.toString() を使うのが素直で、これは最初から習慣にしておきたかった。
fill() — 初期値をまとめて設定したいとき
boolean[] flags = new boolean[5];
Arrays.fill(flags, false);
System.out.println(Arrays.toString(flags)); // [false, false, false, false, false]
配列の全要素を同じ値で埋めたいときに使う。デフォルト値に頼らず明示的に初期化したい場面で役立つ。boolean のデフォルトは false なので上の例は動作上は同じだが、「意図して初期化している」ことをコードで示せる点が好ましい。
copyOf() — コピーの挙動を正確に把握していなかった
int[] numbers = {1, 2, 3};
int[] copy = Arrays.copyOf(numbers, numbers.length);
copy[0] = 0;
System.out.println(Arrays.toString(numbers)); // [1, 2, 3]
System.out.println(Arrays.toString(copy)); // [0, 2, 3]
コピーした配列への変更が元の配列に影響しない。これは int などプリミティブ型の場合、値そのものがコピーされるためだ。
オブジェクトの配列(例:String[] やカスタムクラスの配列)の場合、copyOf() は参照をコピーするため、コピー先で参照先オブジェクトの内部状態を変更すると元の配列にも影響が出ることがある。「コピーしたから独立している」と思い込むと意図しない挙動につながる。
※要確認:元ネタでは Arrays.copyOf() を「ディープコピー」と表現しているが、この表現はプリミティブ型に対しては問題ないものの、オブジェクト配列に対しては正確ではない。オブジェクト配列の場合は参照のシャローコピーになるため、状況によって挙動が異なる点に留意してほしい。
長さを指定することで、元と異なるサイズのコピーも作れる。
int[] numbers = {1, 2, 3};
int[] copy1 = Arrays.copyOf(numbers, 1); // [1]
int[] copy2 = Arrays.copyOf(numbers, 10); // [1, 2, 3, 0, 0, 0, 0, 0, 0, 0]
元より短くすると先頭からN個が切り取られ、長くすると残りはデフォルト値(int なら 0)で埋まる。
整理して気づいたこと
Arrays クラスのメソッドは個別には知っていたが、「なぜそう動くか」まで追えていなかった。今回最も印象に残ったのは copyOf() とディープ/シャローコピーの話で、プリミティブ型かオブジェクト型かで挙動が変わる点は、実際にコードを書いて確かめる価値があった。
まだ整理しきれていないのは、Arrays.copyOfRange() や Arrays.deepEquals()(多次元配列の比較)まわり。多次元配列を扱う機会が増えてきたタイミングで改めて手を動かして確認したい。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc