Java 8関数インタフェースfunctional interfaceの秘密

135908 ワード

目次 [−]
JDK 8以前に既存の関数インタフェース新しく定義された関数インタフェース関数インタフェースでは複数の抽象メソッドを追加定義できますが、これらの抽象メソッド署名はObjectのpublicメソッドと同じでなければなりません.
宣言例外スタティツクメソッドデフォルトメソッド汎用および継承関係関数インタフェースの交差@FunctionalInterface
関数インタフェース(Functional Interface)は、Java 8の特殊なタイプのインタフェースに対する呼称である.このようなインタフェースは、唯一の抽象的なメソッドのインタフェース(隠されたObjectオブジェクトを除く共通のメソッド)のみを定義するため、最初はSAMタイプのインタフェース(Single Abstract Method)を行う.
なぜインタフェースだけからこのようなインタフェースを定義するのでしょうか.Java Lambdaの実装では、開発グループはLambda式のために特殊なStructural関数タイプを単独で定義したくない.矢印タイプ(arrow type)と呼ばれ、Java既存のタイプシステム(class,interface,methodなど)を採用したいが、構造化された関数タイプを追加すると関数タイプの複雑さが増加し、既存のJavaタイプを破壊し、何千ものJavaクラスライブラリに深刻な影響を及ぼしています.利害を考慮すると,最終的にはSAMインタフェースをLambda式のターゲットタイプとして利用する.
JDKに存在するいくつかのインタフェース自体がRunnableのような関数インタフェースである.JDK 8にはjava.util.functionパケットが追加され、一般的な関数インタフェースが提供される.
関数インタフェースは、特定の関数タイプに対する契約を表す契約です.それが現れる場所では,契約要件に合致する関数が実際に期待される.Lambda式はコンテキストから離れて存在することはできません.明確なターゲットタイプが必要です.このターゲットタイプは関数インタフェースです.
もちろん、Java 8が発表されてもうすぐ1年になりますが、以上の概念についても知っているはずです.この文章では、これらの基礎的なものを紹介するのではなく、関数式インタフェースの概念と応用を深く検討したいと思っています.
JDK 8以前に存在していた関数インタフェース
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
新しく定義された関数インタフェースjava.util.functionには、いくつかのタイプの関数インタフェースと、基本データ型のサブクラスが定義されている.
Predicate — パラメータを入力し、bool結果を返します.方法はboolean test(T t) です.
Consumer — パラメータが入力され、戻り値がなく、純消費されます.方法はvoid accept(T t) である.
Function — パラメータを入力し、結果を返します.方法はR apply(T t) です.
Supplier — パラメータ入力なしで、結果を返します.方法はT get() です.
UnaryOperator — メタオペレータは、Functionを継承し、入力パラメータのタイプと戻りタイプが同じです.
BinaryOperator — バイナリオペレータは、入力された2つのパラメータのタイプと戻りタイプが同じで、BiFunction を継承します.
Java APIの関数インタフェースには、次のように表記されています.
       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
       
       
       
       
java.lang
Interface Runnable
All Known Subinterfaces:
RunnableFuture, RunnableScheduledFuture
All Known Implementing Classes:
AsyncBoxView.ChildState, ForkJoinWorkerThread, FutureTask, RenderableImageProducer, SwingWorker, Thread, TimerTask
Functional Interface:
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.

函数式接口中可以额外定义多个抽象方法,但这些抽象方法签名必须和Object的public方法一样

接口最终有确定的类实现, 而类的最终父类是Object。 因此, 函数式接口可以定义Object的public方法。
如以下的接口依然是函数式接口:

       
       
       
       
1
2
3
4
5
6
7
8
       
       
       
       
@FunctionalInterface
public interface ObjectMethodFunctionalInterface {
void count( int i);
String toString(); //same to Object.toString
int hashCode(); //same to Object.hashCode
boolean equals(Object obj); //same to Object.equals
}
publicタイプ限定の方法はなぜですか?インタフェースで定義されている方法はすべてpublicタイプであるからです.たとえば、次のインタフェースは関数インタフェースではありません.
       
       
       
       
1
2
3
4
5
       
       
       
       
interface WrongObjectMethodFunctionalInterface {
void count( int i);
Object clone(); //Object.clone is protected
}
Object.cloneメソッドはprotectedタイプだからです.
例外の宣言
関数インタフェースの抽象メソッドは宣言できます  (checked exception). ターゲットオブジェクトのこのメソッドを呼び出すにはcatchという例外が必要です.
       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
12
13
14
       
       
       
       
public class FunctionalInterfaceWithException {
public static void main(String[] args) {
InterfaceWithException target = i -> {};
try {
target.apply( 10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@FunctionalInterface
interface InterfaceWithException {
void apply( int i) throws Exception;
}

これは、以前のインタフェース/メソッド呼び出しと同じです.
ただし、Lambda式で例外が放出され、ターゲットインタフェースの抽象関数がこのチェック可能性を宣言していない場合、このインタフェースはこのlambda式のターゲットタイプとして使用できません.
       
       
       
       
1
2
3
4
5
6
7
8
9
10
       
       
       
       
public class FunctionalInterfaceWithException {
public static void main(String[] args) {
InterfaceWithException target = i -> { throw new Exception();};
}
}
@FunctionalInterface
interface InterfaceWithException {
void apply( int i);
}

上面的例子中不能编译, 因为lambda表达式要求的目标类型和InterfaceWithException不同。 InterfaceWithException的函数没有声明异常。

静态方法

函数式接口中除了那个抽象方法外还可以包含静态方法。
Java 8以前的规范中接口中不允许定义静态方法。 静态方法只能在类中定义。 Java 8中可以定义静态方法。

1个或者多个静态方法不会影响SAM接口成为函数式接口。
下面的例子中FunctionalInterfaceWithStaticMethod包含一个SAM: apply,还有一个静态方法sum。 它依然是函数式接口。

       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
       
       
       
       
@FunctionalInterface
interface FunctionalInterfaceWithStaticMethod {
static int sum( int[] array) {
return Arrays.stream(array).reduce((a, b) -> a+b).getAsInt();
}
void apply();
}
public class StaticMethodFunctionalInterface {
public static void main(String[] args) {
int sum = FunctionalInterfaceWithStaticMethod.sum( new int[]{ 1, 2, 3, 4, 5});
FunctionalInterfaceWithStaticMethod f = () -> {};
}
}

默认方法

Java 8中允许接口实现方法, 而不是简单的声明, 这些方法叫做默认方法,使用特殊的关键字default
因为默认方法是不是抽象方法,所以不影响我们判断一个接口是否是函数式接口。

       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
       
       
       
       
@FunctionalInterface
interface InterfaceWithDefaultMethod {
void apply( Object obj);
default void say(String name) {
System.out.println( "hello " + name);
}
}
class FunctionalInterfaceWithDefaultMethod {
public static void main(String[] args) {
InterfaceWithDefaultMethod i = (o) -> {};
i.apply( null);
i.say( "default method");
}
}

InterfaceWithDefaultMethod仍然是一个函数式接口。

泛型及继承关系

接口可以继承接口。 如果父接口是一个函数接口, 那么子接口也可能是一个函数式接口。 判断标准依据下面的条件:

对于接口I, 假定M是接口成员里的所有抽象方法的继承(包括继承于父接口的方法), 除去具有和Object的public的实例方法签名的方法, 那么我们可以依据下面的条件判断一个接口是否是函数式接口, 这样可以更精确的定义函数式接口。
如果存在一个一个方法m, 满足:

  • m的签名(subsignature)是M中每一个方法签名的子签名(signature)
  • m的返回值类型是M中的每一个方法的返回值类型的替代类型(return-type-substitutable)
    那么I就是一个函数式接口。

看几个例子。
1)

       
       
       
       
1
2
3
       
       
       
       
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}

インタフェースZは、XYインタフェースのmメソッドを継承している.この2つのメソッドの署名は同じであり、戻り値も同じであるため、Zには唯一の抽象メソッドint m(Iterable arg);があり、関数インタフェースとして機能することができる.
2)
       
       
       
       
1
2
3
       
       
       
       
interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}

メソッド署名Y.m 一致署名はX.mであり、戻り値もreturn-type-substitutableである.したがって、Zは関数インタフェースであり、関数タイプはIterable m(Iterable arg)である.
3)
       
       
       
       
1
2
3
       
       
       
       
interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}

コンパイルエラー.すべてのメソッドのサブ署名であるメソッドの署名は1つもありません.
4)
       
       
       
       
1
2
3
       
       
       
       
interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class> c); }
interface Z extends X, Y {}

Compiler error:No method has a subsignature of all abstract methodsコンパイルエラー、すべてのメソッドのサブ署名に署名する方法はありません
5)
       
       
       
       
1
2
3
       
       
       
       
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}

Compiler error:no method is return type substitutableコンパイルエラー、戻り値タイプが異なります.
6)
       
       
       
       
1
2
3
       
       
       
       
interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}

Compiler error:different signatures,same erasureコンパイルエラー
7)
       
       
       
       
1
2
3
       
       
       
       
interface Foo { void m(String arg); }
interface Bar<T> { void m(T arg); }
interface FooBar<T> extends Foo, Bar<T> {}

関数インタフェースではなく、2つのメソッドのタイプパラメータが異なります.
8)
       
       
       
       
1
2
3
4
5
       
       
       
       
interface X { void m() throws IOException; }
interface Y { void m() throws EOFException; }
interface Z { void m() throws ClassNotFoundException; }
interface XY extends X, Y {}
interface XYZ extends X, Y, Z {}
X.m,Y.m,Z.mメソッド署名は同じで,戻り値タイプはvoidであるが,異常リストは異なる.  EOFExceptionIOExceptionのサブクラスである.この場合、XYXYZはいずれも関数インタフェースですが、関数タイプは異なります.//XY has function type ()->void throws EOFException//XYZ has function type ()->void (throws nothing)
9)
       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
       
       
       
       
interface A {
List foo( List arg) throws IOException, SQLTransientException;
}
interface B {
List foo( List arg) throws EOFException, SQLException, TimeoutException;
}
interface C {
List foo( List arg) throws Exception;
}
interface D extends A, B {}
interface E extends A, B, C {}

//D has function type (List)->List throws EOFException, SQLTransientException//E has function type (List)->List throws EOFException, SQLTransientException
10)
       
       
       
       
1
2
3
4
5
6
7
       
       
       
       
interface G1 {
Exception> Object m() throws E;
}
interface G2 {
Exception> String m() throws Exception;
}
interface G extends G1, G2 {}

//G has function type ()->String throws F
関数インタフェースの交差
1)
       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
12
       
       
       
       
public class Z {
public static void main(String[] args) {
Object o = (I & J) () -> {};
}
}
interface I {
void foo();
}
interface J {
void foo();
}

I和J方法的交集依然符合函数式接口的定义。 上述代码可以用JDK中的javac编译通过但是Eclipse报错,这是Eclipse的一个bug。

2)

       
       
       
       
1
2
3
4
5
6
7
8
9
10
11
12
       
       
       
       
public class Z {
public static void main(String[] args) {
Object o = (I & J) () -> {};
}
}
interface I {
void foo();
}
interface J {
void foo();
}

上述代码Eclipse不会报错但是javac无法编译,javac认为 (I & J)不是一个函数式接口。 看起来javac工作正常, Eclipse处理这样的case还有问题。

@FunctionalInterface

Java 不会强制要求你使用@FunctionalInterface注解来标记你的接口是函数式接口, 然而,作为API作者, 你可能倾向使用@FunctionalInterface指明特定的接口为函数式接口, 这只是一个设计上的考虑, 可以让用户很明显的知道一个接口是函数式接口。

       
       
       
       
1
2
3
4
       
       
       
       
@FunctionalInterface
public interface SimpleFuncInterface {
public void doWork();
}

関数式ではないインタフェースで@FunctionalInterfaceタグを使用すると、どうなりますか?コンパイル中にエラーが発生しました.
       
       
       
       
1
2
3
4
5
       
       
       
       
error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
I is not a functional interface
multiple non-overriding abstract methods found in interface I