【2.1.4 通用函数接口java.util.function.* 返回目录】
为了配合λ表达式的使用,定义了一些作为形参的函数接口。java.util.function包基本覆盖了程序员对函数接口的各种需求。
1.方法的类型签名
函数的类型签名,描述方法的形参列表类型通过本方法处理后,形成返回值类型。以如下格式描述:(形参列表类型) ->返回值类型
从方法的类型签名分析,java.util.function包的核心函数接口有4个。
Ø 函数型T ->R,完成参数类型T向结果类型R的转换。核心函数接口Function
Ø 判断型T ->boolean,核心函数接口Predicate/谓词
Ø 消费型T ->void,核心函数接口Consumer
Ø 供给型void->T,核心函数接口Supplier
其他函数接口与它们类似。各种类型签名可以拓展到二元如(T, U) -> R,三元及更多元的类型签名,难以支持。
由于Java泛型采用擦除技术,Java中不可以用同一个名字定义不同类型或不同数量的参数的泛型类,即无法定义Function<T, R>、Function<T,U, R>、Function<Integer, Integer >等,而必须取不同的类型名字。
2.避免装箱和拆箱
Java泛型仅针对引用类型,如果使用Function<Integer, Integer>,会将代码中的int进行装箱,从而在性能上付出代价。java.util.function包针对基本类型的int、double和long提供支持,当输入或/和输出为基本类型时,可以避免自动装箱的操作。
核心函数接口 | 简化或二元拓展 | 基本类型 |
Function<T, R> ,T ->R 共25个接口 |
| IntFunction<R>,int->R LongFunction<R> DoubleFunction<R> IntToDoubleFunction, int->double IntToLongFunction LongToDoubleFunction, LongToIntFunction, DoubleToIntFunction DoubleToLongFunction ToIntFunction<T>, T->int ToDoubleFunction<T>, ToLongFunction<T> |
BiFunction<T,U,R> ,(T,U) ->R | ToIntBiFunction<T,U>, ToLongBiFunction<T,U>, ToDoubleBiFunction<T,U> | |
UnaryOperator<T>, T->T | IntUnaryOperator, LongUnaryOperator, DoubleUnaryOperator | |
BinaryOperator<T> (T,T) ->T | IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator | |
Predicate<T> T->boolean 共5个接口 |
| IntPredicate, LongPredicate, DoublePredicate |
BiPredicate<L,R> (L,R)->boolean |
| |
Consumer<T>, T->void 共8个接口 |
| IntConsumer, LongConsumer, DoubleConsumer |
BiConsumer<T,U> (T,U)->void | ObjIntConsumer<T>, ObjLongConsumer<T>, ObjDoubleConsumer<T> | |
Supplier<T> ()->T 共5个接口 |
| BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier |
3.自定义函数接口
虽然java.util.function包提供了通用函数接口,在后面的例子中仍然使用了自定义函数接口,主要考虑在演示代码中自定义函数接口比通用函数接口更容易理解。某种程度上,使用通用函数接口,如同定义各种类时,将各种类的方法全部命名为doSth()一样,会降低代码的可读性。
另一方面,通过自定义函数接口,可以掌握通用函数接口的使用,这里就不举例说明通用函数接口的用法了。
4.方法引用
函数接口主要支持λ表达式的使用,例如
ToIntFunction<String>f = s->s.length();
值得注意的是,在λ表达式中调用某个类的方法或构造器的情形,Java 8提供了λ表达式的简化版本,方法引用。
ToIntFunction<String>f = String::length;
方法引用是否会降低代码的可读性,见仁见智。
Consumer<String> c= System.out::println;
IntConsumer c2= System.out::println;
当调用accept(),要注意c需要的参数为泛型指出的String,而c2需要的参数为类名IntConsumer暗示的int。
c.accept("hello");
c2.accept(5);
另外,yqj2065觉得冒号冒号,很丑陋。
注:在我们讨论各种函数接口时,关心类型签名,而函数名applyAsDouble等等不需要知道,yqj2065说过:lambda表达式是省略了名字的函数。通常,通用函数接口中抽象方法的名字如applyAsDouble、apply、test,应用程序员甚至不需要知道,我们使用lambda表达式时就不需要写函数名。
- int –> int封装为IntUnaryOperator。类似的,对于long–> long封装为LongUnaryOperator、double–> double封装为DoubleUnaryOperator。
- 对于参数和返回值为基本类型的一元函数,int –> long封装为IntToLongFunction、int –> double封装为IntToDoubleFunction、long–> int封装为LongToIntFunction、long–> double封装为LongToDoubleFunction、double–> int封装为DoubleToIntFunction、double–> long封装为DoubleToLongFunction。
- 参数为基本类型而返回值为R的一元函数,int –> R封装为IntFunction<R> 、long–> R封装为LongFunction<R>、double–> R封装为DoubleFunction<R>;
- 参数为T而返回值为int等情况,T–> int封装为ToIntFunction<T>、T–> long封装为ToLongFunction<T>、T–> double封装为ToDoubleFunction<T>。
- 更一般的一元函数,T–> R封装为Function<T,R>。如果是T–> T,则特化为UnaryOperator<T>。
public static void unaryFunction(){//一元函数
F y = x-> x + 1;
pln(y.f(2));
IntUnaryOperator y1 = x-> x + 1; //int –> int
pln(y1.applyAsInt(2));
IntToDoubleFunction y2 = x-> x + 1; //int –> double
pln(y2.applyAsDouble(2));
ToIntFunction<String> y3 = s->s.length();//T–> int
pln(y3.applyAsInt("hello"));
IntFunction<String> y4 = i->(i+"ok");//int–> R
pln(y4.apply(5));
Function<String,Integer> y5 = s->s.length();//String–> Integer
pln(y5.apply("hello"));
}
- 一般的二元函数,(T, U) –> R封装为BiFunction<T,U,R>。如果是(T, T)–> T,则特化为BinaryOperator<T>。
- (T, U) –> int封装为ToIntBiFunction<T,U>、(T, U) –> long封装为ToLongBiFunction<T,U>、(T, U) –> double封装为ToDoubleBiFunction<T,U>。
- (int, int) –> int封装为IntBinaryOperator;类似的,(long, long) –> long封装为LongBinaryOperator、(double, double) –> double封装为DoubleBinaryOperator。
- (int, long) –> int这样的组合太多,不提供封装类。
本系列中25个接口。
public static void testFunction(){
principle.callback.lower.DoubleOP<Double,String> f = ( x, y)->{return x +y.length() ;};
double d = f.applyAsDouble(1.0,"yqj2065");
pln(d);
ToDoubleBiFunction<Double,Double> f1 = ( m, n)->{return m +2*n ;};
d = f1.applyAsDouble(1.0,3.0); pln(d);
Function<String,Double> f2 = x->(double)x.length();
d = f2.apply("yqj2065"); pln(d);
BiFunction<String,String,Double> f3 = (x,y)->x.length()+y.length()*1.0;
d = f3.apply("yqj","2065"); pln(d);
BinaryOperator<Double> f4 = (x,y)->x+y;
d = f4.apply(1.0,3.0); pln(d);
DoubleBinaryOperator f5 = (x,y)->x+y;
d = f5.applyAsDouble(1.0,3.0); pln(d);
}
通用函数接口最大的用途,是框架或/和类库的设计。假设下面代码中m()是库函数,应用程序test2()可以视m()为一个高阶函数,它使用函数接口作为形参,而应用程序以lambda表达式作为实参。
public static void m(Predicate<String> p, String t) {
if (p.test(t)) {
System.out.println("ok");
}
}
public static void test2(){
m(s->s.startsWith("a"),"aab");
}
2.Predicate<T>
谓词/Predicate,针对T数据进行测试,返回boolean。lambda表达式的类型签名T→boolean。
上面m()的形参为Predicate<String>,针对String数据进行测试;通常的形参为Predicate<T>或Predicate<? super T>——只读通配符(T 的某个父类型)
下面的例子,说明我们可以针对List<T>抽象出一般性的方法filter——换言之,过滤等函数不是什么了不起的想法,但在Stream中,它是延迟计算的,因而更给力。
public static <T> List<T> filter(List<T> list, Predicate<T> p) {
List<T> results = new ArrayList<>();
/*list.stream().filter((s) -> (p.test(s))).//惰性
forEach((s) -> {
results.add(s);
});*/
for (T s : list) {
if (p.test(s)) {
results.add(s);
}
}
return results;
}
Predicate提供了default方法与或非and、or、 negate()。