4
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PLISEAdvent Calendar 2019

Day 24

ジェネリクスと関数型インターフェースを活用した便利メソッドの作り方

Posted at

便利メソッドの作成

多人数でチームを組んで開発を行っていると共通で行いたい処理などがでてきます。
例えば文字列操作や日付操作などは色々なシーンで使われますが、その際には便利なライブラリを利用したり、便利なメソッドが集まったクラスなどを作成したりするかと思います。

特定のクラスに対する処理であればある程度簡単に作れてしまいますが、多人数で開発する場合には様々なクラスに対して共通の処理を行いたいといった要望がでてきます。

その際に利用すると便利なものが「ジェネリクス」や「関数型インターフェース」といったものです。
この2つについてはなんとなく知っていても実際にどう使っていいかわかりにくという人もいるかと思います。

そこで今回は実際に便利メソッドを作る工程を追いながら実際にどのように使うのかを紹介してみたいと思います。
※ 作りながら雰囲気をつかんでもらうため「ジェネリクス」や「関数型インターフェース」の紹介は軽めです

ジェネリクス (総称型) とは

データ型の定義に対してパラメータのように渡すことで、同じような構造をもつプログラムを複数のデータ型に対応できるようにしたものです。

例えば JDK1.4 までは List を取り扱う時には型指定がなく Object としてなんでも入るようになっていました。

List list = new ArrayList();
list.add("string");
list.add(Integer.valueOf(100));

型が制限されておらず文字だけを入れたい場合はプログラムを作る人が気を付けて作るしかありませんでした。
ジェネリクスが出てからは型を指定できるようになり以下のように書けるようになりました。

List<String> list = new ArrayList<>();
list.add("string1");
list.add("string2");

Java のAPI仕様を見るとそれぞれ List<E>ArrayList<E> と記載されています。
この E の部分がジェネリクスといわれ、同じ構造のプログラムに対して任意の型を設定できるようになっています。

関数型インターフェースとは

関数型インターフェースを簡単に表現すると Java 8 から導入された「メソッド参照」や「ラムダ式」を代入できるインターフェースのことです。

例えば Java で良く作る getter, setter を関数型インターフェースで表すと以下のようになります。

メソッド インターフェース メソッド
getter Supplier<T> T get​()
setter Consumer<T> accept(T t)

Supplier は引数が無く任意の型を返すインターフェースです。
同じような仲間に、任意ではなくプリミティブを返す IntSupplier, BooleanSupplier なども存在します。

Consumer は引数を受け取り戻り値がないインターフェースです。
同じような仲間に、任意ではなくプリミティブを渡す IntConsumer, DoubleConsumer なども存在します。

単体テストのための便利メソッドを作ってみる

ジェネリクスや関数型インターフェースを使ったことない人にとっては上記のような説明だけでは難しいかと思います。
そこで実際に便利メソッドを作りながら簡単に使い方を解説していきます。

準備

まずは生徒クラスを作成したとします。
生徒クラスは学籍番号、生徒名、年齢を持ち setter, getter が定義されているとします。

public class Student {
    /** 学籍番号 */
    private String code;

    /** 生徒名 */
    private String name;
    
    /** 年齢 */
    private int age;

    //... setter, getter 略 ...
}

単体テストのデータ準備のため、テストデータとして3人の生徒を作りListに詰めます。

final Student student1 = new Student();
student1.setCode("S01001");
student1.setName("山田太郎");
student1.setAge(20);

final Student student2 = new Student();
student1.setCode("S02001");
student1.setName("山田次郎");
student1.setAge(19);

final Student student3 = new Student();
student1.setCode("S03001");
student1.setName("山田三郎");
student1.setAge(18);

final List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
students.add(student3);

3人分であればまだ見れなくないですが、これが10人、20人と増えてくると大変なことになります。

List の作成を簡易化するメソッド作成

まずは List の作成を簡易化するメソッドを作成してみました。

public <T> List<T> createInstanceList(Supplier<T> supplier, int size) {
    return IntStream.range(0, size)
            .mapToObj(i -> supplier.get())
            .collect(toList());
}

このプログラムを少しずつ解説していきます。

public <T> List<T> createInstanceList(Supplier<T> supplier, int size) 

まずはメソッドの宣言についですが、 <T> とジェネリクスが定義されています。
ジェネリクスを使っているため任意のクラスに対して使えるメソッドとなりました。

戻り型は List<T> となっているため任意の型の List が返却されます。

第一引数は Supplier<T> が宣言されているため、ジェネリクスの紹介にもあった通り任意の型を返すためだけのインターフェースを受け取れます。

第荷引数は int で List のサイズを指定できるようにしています。

IntStream.range(0, size)

次に返却する値ですが、まず IntStream.range(0, size) でストリームを作成しています。
range を使用しているため 0 から size - 1 まで繰り返します。
size が 3 の場合 0, 1, 2 という数値が繰り返されます。

.mapToObj(i -> supplier.get())

先ほど IntStream から 0, 1, 2 が渡されてきますがそれを無視して supplier.get() を使用しています。
これは引数で渡されてきた関数インターフェースの取得メソッドを呼び結果を返しています。

.collect(toList());

最後に数値の繰り返し回数 (例では3回) 関数型インタフェースから受け取った値を List に詰めて返します。

List 簡易化メソッドを適用

説明が難しい部分があるため実際にこのメソッドを使うとどう変わるか最初のコードを変更してみます。

final List<Student> students = createInstanceList(Student::new, 3);
students.get(0).setCode("S01001");
students.get(0).setName("山田太郎");
students.get(0).setAge(20);
students.get(1).setCode("S02001");
students.get(1).setName("山田次郎");
students.get(1).setAge(19);
students.get(2).setCode("S03001");
students.get(2).setName("山田三郎");
students.get(2).setAge(18);

生徒のインスタンス作成とListへのセットが無くなりました。

final List<Student> students = createInstanceList(Student::new, 3);

最初のこの行で List の作成と内部のインスタンス作成を同時にやっています。
Student::newコンストラクター参照 といわれ、new Student() の結果を返す関数型インターフェース を返します。

コンストラクタ参照が Supplier として扱われ、先程作成したメソッドの第一引数としてわたせるようになります。
先程のメソッドにあった supplier.get() で new Student() の結果が返ることになります。

List 内のオブジェクトに値をセットするメソッド作成

先程のメソッドを作成しコードは短くなりましたが値をセットするところがまだ煩雑です。
そのため以下のようなコードを書いてみます。

public <T, U> void setValues(List<T> obj, BiConsumer<T, U> biConsumer, U... values) {
    for (int i = 0; i < obj.size(); i++) {
        biConsumer.accept(obj.get(i), values[i]);
    }
}

※ 解説用のため例外処理は記載していません

このプログラムも少しずつ解説していきます。

public <T, U> void setValues(List<T> obj, BiConsumer<T, U> biConsumer, U... values)

まずはメソッドの宣言についですが、<T, U> とジェネリクスが定義されています。
最初のメソッドとは違い2つの任意の型を取り扱います。

戻り型は void のため値を返却しません。

第一引数は List obj となっているため値をセットしたい任意の型のリストを渡します。

第二引数は BiConsumer biConsumer となっています。
こちらは後に解説します。

第三引数は U... values となっており可変引数を渡せるようになっています。
この引数で List 内のオブジェクトに対してセットしたい値を可変でしていできます。

値セット用メソッドを適用

こちらも実際にこのメソッドを使うとどう変わるかコードを変更してみます。

final List<Student> students = createInstanceList(Student::new, 3);
setValues(students, Student::setCode, "S01001", "S02001", "S03001");
setValues(students, Student::setName, "山田太郎", "山田次郎", "山田三郎");
setValues(students, Student::setAge, 20, 19, 18);

かなりコードが短くなり見通しが良くなりました。

第一引数に 値をセットしたい 生徒のリスト をセットしています。

第二引数に setter のメソッド参照 を渡しています。
このメソッド参照が先程作成したメソッドの BiConsumer<T, U> として渡されて、 biConsumer.accept(...) で呼び出されます。

第三引数以降は 可変引数 のため、セットしたい値を並べます。

クラスメソッド参照とインスタンスメソッド参照

ここで一つ疑問があります。
setter メソッドは引数が1つで戻り値がないため通常は Consumer<T> を使うはずです。
ではなぜ今回 BiConsumer という2つの引数をとる関数型インターフェースを使うかを簡単に解説します。

例えば引数を Conumer<T> を使用した場合以下のようなプログラムを作成することができます。

public <T> void setValue(Consumer<T> consumer, T value) {
    consumer.accept(value);
}

使い方はこうです。

Student student = new Student();
setValue(student::setCode, "S01001");

BiConsumer を使った時との違いは以下です。

インターフェース メソッド参照
BiConsumer<T, U> Student::setCode
Consumer<T> student::setCode

BiConsumer を使った時にはクラスのメソッド参照、Consumerを使った時には インスタンスのメソッド参照 を使っています。

今回 List 内のインスタンスに対して値をセットしたいため、List 内部のインスタンスごとにメソッド参照を渡すのは意味がありません。
そこで クラスのメソッド参照 を渡して List の中にあるインスタンス毎に setter を呼ぶようにしています。

まとめ

今回作成したメソッドは以下2つです。

public <T> List<T> createInstanceList(Supplier<T> supplier, int size) {
    return IntStream.range(0, size)
            .mapToObj(i -> supplier.get())
            .collect(toList());
}

public <T, U> void setValues(List<T> obj, BiConsumer<T, U> biConsumer, U... values) {
    for (int i = 0; i < obj.size(); i++) {
        biConsumer.accept(obj.get(i), values[i]);
    }
}

私は普段たくさんの学生と一緒に開発の仕事を行っています。
スキルの違いも大きいためできる限り同じようなプログラムを書けるようになったり、開発効率をあげるためにも便利なメソッドの作成は必要不可欠です。

簡単な便利メソッドはすぐに作れますが、できるだけ汎用的なものを作ろうとすればするほど、今回紹介したジェネリクスや関数型インターフェース等を知っているとより便利なものが作れるようになります。

4
8
0

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
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?