[雑談] 試験の役に立たないJava講座 (13)
ということで引き続きインプレス社の「徹底攻略Java SE 11 Silver問題集」の章立てに沿って、少し雑談していきたいと思います。
以前よりお話ししているとおり問題集の内容に沿ったものではなく抜けているところも多々ありますので、認定資格を取得しようというのであれば問題集を購入して、そちらをしっかり勉強してください。
こちらは章立てに沿って適当に書き散らかしているものですので、試験の役には立ちません。
今回は前回の続きで第8章の関数型インタフェースとラムダ式を考えていきます。
なぜラムダ式なのか
前回、内部クラスと無名クラスについてみてきました。
Javaには関数を渡す機能が提供されていないため、コールバック関数やヘルパー関数が必要となる場面ではそれらのメソッドを定義したインタフェースを実装したオブジェクトをわたし、オブジェクトを受け取った側でインタフェースに定義されたメソッドを呼び出すことでコールバックやヘルパー関数を実現しています。
こういったインタフェースを実装するクラスをトップレベルのクラスとして実装するとクラスファイルが無駄に増えてしまい、パフォーマンスや管理の面であまり好ましくないため、クラスメンバーとしてクラスを定義する内部クラスが導入されています。
さて、無名クラスもクラス定義本体をクラス定義の文法に沿って書かなければいけないため、決まり切ったあまり必要のないコードを書かなければならないという点が問題となり、さらに簡潔に書こうということで導入されたものがラムダ式です。
ラムダ式の前提条件
例えば以下のような無名クラスの実装があるとします。
public class Employee {
private String name;
private LocalDate joined;public static final Comparator<Employee> NAME_COMPARATOR
= new Comparator<>() {
@Override
public int compare(Employee o1, Employee o2) {
return o1.name.compareTo(o2.name);
}
};
// 以下略
}
これは前回の例で挙げたものですが、ここで注目していただきたいのが「Comparator」インタフェースで実装しているメソッドが「compare」1個だけという点です。
そして、メソッド名を省略してもいいのではないかということで導入されたのがラムダ式となります。
ラムダ式
では、上の例をラムダ式で書き直してみたいと思います。
public class Employee {
private String name;
public static final Comparator<Employee> NAME_COMPARATOR= (o1, o2) -> o1.name.compareTo(o2.name);
// 以下略
}
まず、無名関数の最初にあった「new Comparator<>()」ですが、これは変数の型として「Comparator<Employee>」が指定されているので、省略しても問題はありません。
そして実装が必要なメソッドは「compare」だけで、引数の型と戻り値の型もインタフェースで規定されているので、一気に省略して「(o1, o2)」としています。
次の「->」がラムダ式であることを示す記号となり、その後ろに値を返す式、または文が続きます。
今回は1個の式で値を返すことができるので、「o1.name.compareTo(o2.name)」という式が書かれています。
このように実装が必要なメソッドが1個しかないインタフェースを実装するオブジェクトを定義する際に、必要最低限の項目を書くだけにしようというのがラムダ式の考え方になります。
複雑な実装
上の例ではメソッドの戻り値を1個の式で記述できましたが、場合によっては1個の式で記述できない複雑な処理を必要とする場合があります。
もちろん、非常に複雑な処理をラムダ式で記述することは推奨されませんが、そういった場合には式ではなくブロックでラムダ式を定義することができます。
上の例をブロックを用いて記述した例を以下に示します。
public class Employee {
private String name;
public static final Comparator<Employee> NAME_COMPARATOR= (o1, o2) -> {
String s1 = o1.name;
String s2 = o2.name;
return s1.compareTo(s2)
};
// 以下略
}
上の例にあるように普通にブロックとして記述することでラムダ式を構成することが可能です。
ラムダ式の制約事項
ラムダ式だけでなく無名関数でも同じ制約となりますが、ラムダ式はクラス内で定義できるだけではなくメソッド内で定義することもできます。
このため、ラムダ式はオブジェクトのフィールドやメソッドのローカル変数にアクセスできてしまいます。
しかし、ラムダ式によって生成されたオブジェクトのメソッドはラムダ式を定義しているオブジェクトやメソッドの状態や処理と関係なく実行されてしまいます。
場合によってはラムダ式を定義しているオブジェクトが削除された後にラムダ式で生成されたオブジェクトが使用されるかもしれません。
そういった場合に備えてスタティックではない内部クラスのオブジェクトやラムダ式で生成されたオブジェクトは内部クラスやラムダ式を定義しているクラスのオブジェクトの参照を暗黙的に保持します。
このため、内部クラスやラムダ式で生成されたオブジェクトのサイズは通常それほど大きなものではありませんが、それらを定義したクラスのオブジェクトサイズが大きい場合にはメモリ不足の原因となる可能性があります。
また、メソッド内で定義された内部クラスやラムダ式はメソッドのローカル変数にアクセスできますが、ローカル変数はメソッドが終了した時点で失われてしまい、内部クラスやラムダ式で生成されたオブジェクトのメソッドが呼ばれた時点では存在もしていないかもしれません。
このようあ状況に対応できるよう、メソッド内で定義された内部クラスやラムダ式が参照できるローカル変数は定義された後、一切変更されないことが条件となります。
例えば以下のような記述は可能です。
public class Employee {
private String name;
public interface TextGetter {public String getText();
}
public TextGetter getNameTextGetter() {
String text = "name: " + name;
return new TextGetter() {
return text;
};
}
}
上記をラムダ式で書くと以下のようになります。
public class Employee {
private String name;
public interface TextGetter {public String getText();
}
public TextGetter getNameTextGetter() {
String text = "name: " + name;
return () -> text;
}
}
また、ローカル変数は宣言と初期化を分離することができるので以下のような書き方もできます (面倒なので内部クラス表現は省略します)。
public class Employee {
private String name;
public interface TextGetter {public String getText();
}
public TextGetter getNameTextGetter() {
String text;
text = "name: " + name;
return () -> text;
}
}
しかし、以下のような書き方は認められずコンパイルエラーになります。
public class Employee {
private String name;
public interface TextGetter {public String getText();
}
public TextGetter getNameTextGetter() {
String text = "name: ";
text += name;
return () -> text;
}
}
この例は実質的には上二つの例と変わらないのですが、Javaの仕様ではこのように初期化されたローカル変数に対する変更が発生する場合は一律にエラーとしています。
もう一つ気をつけなければならない点として、ラムダ式の形式パラメータに使用する名前はローカル変数名などと重複してはならないという点です。
ざっくりと考えると、ラムダ式の形式パラメータはカッコで囲まれていてブロックになっていないため、スコープが分離されないのでこのような仕様になったのではないかと思われます。
が、どちらにしろコンパイラはラムダ式の形式パラメータを特別扱いしているので、実装上の都合というより仕様の考え方に起因するものなのでしょう。
関数型インタフェース
ところで、ラムダ式で参照される実装が必要なメソッドを1個だけ持つインタフェースを「関数型インタフェース」と呼んでいます。
そして関数型インタフェースが定義するメソッドの引数と戻り値の型や有無、数に基づいて多数のテンプレート的なインタフェースが標準化されています。
ここではそのうちいくつかを見ていきたいと思います。
Function<T,R>インタフェース
Function<T, R>はT型の引数を1個受け取り、R型の戻り値を返すR apply(T t)メソッドを定義しています。
このインタフェースの変種として、引数と戻り値の型が共通なUnaryOperator<T>インタフェース、2個の引数を持つメソッドを定義するBiFunction<T, U, R>インタフェース、さらに2個の引数と戻り値の型が共通なBinaryOperator<T>インタフェースなどが用意されています。
もちろんUnaryOperatorインタフェースやBinaryOperatorインタフェースで演算子を定義することはできませんが、メソッド名がapplyに固定されていることを除けば使いやすいインタフェースだと思います。
Supplierインタフェース
Supplier<T>インタフェースは引数をとらずにT型の戻り値を返すT get()メソッドを定義しています。
Supplierインタフェースを実装するオブジェクトの親オブジェクトの状態や外部状態に基づいて値を返すコールバック関数として利用できるものです。
Consumerインタフェース
Consumer<T>インタフェースはT型の引数をとり戻り値を返さないvoid accept(T t)メソッドを定義しています。
こちらはConsumerインタフェースを実装するオブジェクトの親オブジェクトの状態を設定するコールバック関数などに利用できそうです。
Predicateインタフェース
Predicate<T>インタフェースはT型の引数をとり、boolean型の戻り値を返すboolean test(T t)メソッドを定義しています。
Predicateインタフェースを実装するオブジェクトの親オブジェクトの状態により成否を判定するコールバック関数などに利用できるものです。
これらの標準化されたインタフェースを使用してもいいのですが、メソッド名がメソッドの目的と整合していない場合にはメソッドの目的に併せてメソッド名を定義した独自の関数型インタフェースを作るべきと考えます。
FunctionalInterfaceアノテーション
関数型インタフェースの定義には@FunctionalInterfaceアノテーションを付けることができます。
コンパイラはこのアノテーションがなくても関数型インタフェースの要件、実装しなければならないメソッドが1個だけ存在することを満たしていれば関数型インタフェースとみなすので、ラムダ式で使用することができます。
このアノテーションを付けることでコンパイラや開発環境はFunctionalInterfaceアノテーションがつけられたインタフェースが関数型インタフェースの要件を満たしているかを検査することができるので、アノテーションをつけた方がいいかと思います。
以上、簡単にラムダ式と関数型インタフェースについて考えてみました。
0コメント