はじめに
JavaのObject
クラスはすべてのクラスのスーパークラスで非常に重要なクラスですが、それ自体をnew
することはあまりないのではないでしょうか?
この記事ではnew Object()
が有意義となる二つのコード例を紹介します。
メソッド内にメソッドを定義する
以下は「Javaで数学の組み合わせ(Combination)を実装する - Qiita」のコメントに記載されたコード例です。文字列の配列data
からk
個取る組み合わせを列挙して返すメソッドです。
static List<String[]> combination(String[] data, int k) {
List<String[]> result = new ArrayList<String[]>();
combination(data, 0, new String[k], 0, result);
return result;
}
static void combination(String[] data, int di, String[] comb, int ci, List<String[]> result) {
if (ci == comb.length) {
result.add(comb.clone());
return;
}
for ( ; di <= data.length - (comb.length - ci); di++) {
comb[ci] = data[di];
combination(data, di + 1, comb, ci + 1, result);
}
}
2つのメソッドから構成されていて、外部から直接呼び出されるのは2引数のcombination
で、5引数のcombination
は内部的に利用されています。5引数のcombination
は自分自身を再起呼び出ししていますが、5引数の内、3引数(data
, comb
, result
)は全く同じ値を引数として指定しています。
もしJavaがメソッドを入れ子にして定義できるのであれば、再起呼び出し時に同じ引数を記述しなくても済むはずです。残念ながらJavaではメソッドを入れ子にすることができません。
そこでnew Object()
を使ってこれを書き直してみます。
static List<String[]> combination(String[] data, int k) {
int length = data.length;
List<String[]> result = new ArrayList<String[]>();
String[] comb = new String[k];
new Object() {
void combination(int di, int ci) {
if (ci == k) {
result.add(comb.clone());
return;
}
for (; di <= length - (k - ci); di++) {
comb[ci] = data[di];
combination(di + 1, ci + 1);
}
}
}.combination(0, 0);
return result;
}
5引数のcombination
はnew Object() {}
の内側に記述して固定の3引数(data
, comb
, result
)は削除しました。
このコードのnew Object() {}
は無名のインナークラスを定義して、同時にそのクラスのインスタンスを作成します。new Object() {}
の後の.combination(0, 0)
はインナークラス内で定義されているcombination(int di, int ci)
を呼び出します。
固定の3変数(data
, comb
, result
)はインナークラスの中から参照可能なので、呼び出しごとに引数で渡してやる必要はなくなります。
new Object()
で無駄なインスタンスを作っていますが、1個作るだけなのでオーバーヘッドは小さいです。再起呼び出し時の引数が減るので、スタックの消費量は減ります。
一時的なデータ保管のためのクラス
以下のようなクラスがあったとします。
public class Person {
private final String name;
private final String age;
public Person(String name, String age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public String getAge() {
return this.age;
}
}
さらに以下のようなPerson
のリストがあったとします。
List<Person> list = List.of(
new Person("Yamada", "18"),
new Person("Ichikawa", "72"),
new Person("Sato", "39"),
new Person("Tanaka", "9"));
これをage
で昇順にソートすることを考えてみます。age
は文字列なので整数としてソートするためにはこのように記述する必要があります。
List<Person> result = list.stream()
.sorted(Comparator.comparingInt(p -> Integer.parseInt(p.getAge())))
.collect(Collectors.toList());
これはこれでうまくいきますが、値を比較する都度Integer.parseInt(p.getAge())
が2回ずつ実行されてしまうので、あまり効率がよくありません。高速なソートのアルゴリズムでもInteger.parseInt
は $ 2n \log n $ 回実行されてしまいます。
Map.Entry
Person
のインスタンスとage
を整数化したものをペアにすれば、この問題は解決します。こういう時、一時的な格納場所としてよく利用されるのがMap.Entry
クラスです。
List<Person> result = list.stream()
.map(p -> Map.entry(Integer.parseInt(p.getAge()), p))
.sorted(Comparator.comparingInt(Entry::getKey))
.map(Entry::getValue)
.collect(Collectors.toList());
整数のage
の計算は1回で済みますが、Map.Entry
が使われている所が直観的にわかりにくいです。
Records
Java14ではデータ保管のための不変クラスを簡単に定義できるRecordsという機能が追加されています。これを使うとこうなります。
record PersonWithAge(Person p, int age) {
PersonWithAge(Person p) {
this(p, Integer.parseInt(p.getAge()));
}
}
List<Person> result = list.stream()
.map(PersonWithAge::new)
.sorted(Comparator.comparingInt(PersonWithAge::age))
.map(PersonWithAge::p)
.collect(Collectors.toList());
わかりやすくなりましたが、一時保管用のクラスを定義していて少しおおげさな感じもします。
new Object()
new Object()
を使うとこうなります。
List<Person> result = list.stream()
.map(p -> new Object() {
int intAge = Integer.parseInt(p.getAge());
Person person = p;
})
.sorted(Comparator.comparingInt(obj -> obj.intAge))
.map(obj -> obj.person)
.collect(Collectors.toList());
new Object() {...}
はこの式の中だけで有効な一時的保管場所です。無名のクラスなのですが、この式の中ではintAge
とperson
というフィールドを持った一人前のクラスであると認識されています。
まとめ
いかがだったでしょうか?Object
クラスにもいろいろな使い方あることがわかっていただけたと思います。トリッキーな感じがするのであまりおすすめはできませんが、私はこういう書き方ができると知ってからけっこう使っています。