4
2

More than 3 years have passed since last update.

new Object() {}で一時的クラスを作成する

Last updated at Posted at 2020-07-09

はじめに

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引数のcombinationnew 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() {...}はこの式の中だけで有効な一時的保管場所です。無名のクラスなのですが、この式の中ではintAgepersonというフィールドを持った一人前のクラスであると認識されています。

まとめ

いかがだったでしょうか?Objectクラスにもいろいろな使い方あることがわかっていただけたと思います。トリッキーな感じがするのであまりおすすめはできませんが、私はこういう書き方ができると知ってからけっこう使っています。

4
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2