天天看点

java8——函数式接口

函数式接口

什么是函数式接口?

  函数式接口,@FunctionalInterface,简称FI,简单的说,FI就是指仅含有一个抽象方法的接口,以@Functionalnterface标注,这里的抽象方法指的是该接口自己特有的抽象方法,而不包含它从其上级继承过来的抽象方法,例如:

@FunctionalInterface
public interface ApplePredicate {
    boolean test(Apple apple);
}
           

函数接口有哪些?

Java 7 中已经存在的函数式接口:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.security.PrivilegedAction
  • java.util.Comparator
  • java.io.FileFilter
  • java.beans.PropertyChangeListener

除此之外,Java 8中增加了一个新的包:java.util.function,它里面包含了常用的函数式接口,例如:

- Predicate < T > ——接收 T 并返回 boolean

- Consumer< T >——接收 T,不返回值

- Function< T, R >——接收 T,返回 R

- Supplier< T >——提供 T 对象(例如工厂),不接收值

- UnaryOperator< T >——接收 T 对象,返回 T

- BinaryOperator< T >——接收两个 T,返回 T

除了上面的这些基本的函数式接口,我们还提供了一些针对原始类型(Primitive type)的特化(Specialization)函数式接口,例如 IntSupplier 和 LongBinaryOperator。(我们只为 int、long 和 double 提供了特化函数式接口,如果需要使用其它原始类型则需要进行类型转换)同样的我们也提供了一些针对多个参数的函数式接口,例如 BiFunction

如何定义自己的函数式接口?

在上面的ApplePredicate接口中可以看到我们使用了注解@FunctionalInterface来声明这是一个函数式接口,但在实际开发过程中我们并不需要通过注解来声明,编译器会自动根据接口自行判断该接口是否是一个函数式接口(判断过程并非简单的对接口方法计数:一个接口可能冗余的定义了一个 Object 已经提供的方法,比如 toString(),或者定义了静态方法或默认方法,这些都不属于函数式接口方法的范畴),使用注解的好处在于显示声明该接口为函数式接口,防止其他开发人员往该接口添加其他方法。

接口介绍

java.util.function.* 接口中定义了多种不同的函数接口,这里首先介绍三个泛型函数式接口:Predicate,Consumer,Function

@FunctionalInterface
    public interface Predicate<T>{
        boolean test(T t);
    }
           

java.util.function.Predicate 定义了一个test方法接受一个泛型返回一个布尔型

@FunctionalInterface
public interface Consumer<T>{
    void accept(T t);   
}
           

java.util.function.Consumer 定义了一个accept方法接受一个泛型没有返回类型,如:

public static <T> void forEach(List<T> list, Consumer<T> c) {
for (T i : list) {
c.accept(i);
}
}

public static void main(String arg[]) {
forEach(Arrays.asList(,,,,,), integer -> {if (integer %  == ) System.out.println(integer);});
}
           
@FunctionalInterface   
     public interface Function<T, R>{
        R apply(T t);
    }
           

java.util.function.Function< T, R >定义了一个apply方法接受一个泛型T返回一个泛型R,如:

public static void main(String arg[]) {
     List<Integer> l = map(Arrays.asList("lambdas", "in", "action"), s -> s.length());
}

public static <T, R> List<R> map (List<T> list, Function<T, R> f) {
     List<R> result = new ArrayList<R>();
     for (T s : list) {
     result.add(f.apply(s));
     }
     return result;
}
           

原始类型特化

Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer<T>中的T)只能绑定到引用类型。这是由泛型内部的实现造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的机制。这个机制叫做装箱(boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫做拆箱(unboxing)。Java还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱是自动完成的。比如,这就是为什么下面的代码是有效的:
           
List<Integer> list = new ArrayList<>();
    for (int i = ; i < ; i++){
        list.add(i);
    }
           
但是这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包装起来,并保存在队里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取包装的原始值。
Java8为我们前面所说的函数式接口带来了一个专门的版本,一遍在输入和输出都是原始类型时避免自动装箱的操作。比如,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate<Integer>就会把参数1000装箱到一个Integer对象中:
           
public interface IntPredicate{
    boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i %  == ;
evenNumbers.test();//无装箱
Predicate<Integer> oddNumbers = (Integer i) -> i %  == ;
oddNumbers.test();//装箱
true     
           

下表展示了Java 8中的常用函数式接口,

函数接口 函数描述符 原始类型特化
Predicate< T > T->boolean IntPredicate,LongPredicate,DoublePredicate
Consumer< T > T->void IntConsumer,LongConsumer, DoubleConsumer
Function< T,R > T->R IntFunction< R >,
IntToDoubleFunction,
IntToLongFunction,
LongFunction< R >,
LongToDoubleFunction,
LongToIntFunction,
DoubleFunction< R >,
ToIntFunction< T >,
ToDoubleFunction,
ToLongFunction
Supplier< T > ()->T BooleanSupplier,IntSupplier, LongSupplier,DoubleSupplier
UnaryOperator T->T IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator
BinaryOperator (T,T)->T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate (L,R)->boolean
BiConsumer (T,U)->void ObjIntConsumer, ObjLongConsumer, ObjDoubleConsumer
BiFunction (T,U)->R ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction

下表展示了Lambdas及函数式接口的例子

使用案例 Lambda的例子 对应的函数式接口
布尔表达式 (List< String > list) -> list.isEmpty() Predicate< List< String > >
创建对象 () -> new Apple(10) Supplier< Apple >
消费一个对象 (Apple a) -> System.out.println(a.getWeight()) Consumer< Apple >
从一个对象中选择/提取 (String s) -> s.length() Function< String, Integer >或 ToIntFunction< String >
合并两个值 (int a, int b) -> a * b IntBinaryOperator
比较两个对象 (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()) Comparator< Apple >或 BiFunction< Apple, Apple, Integer > 或 ToIntBiFunction< Apple, Apple >

之前讲到了双冒号目标引用方法,这里我们来看一些例子:

lambda 等效的方法引用
(Apple a) -> a.getWeight() Apple::getWeight
() -> Thread.currentThread().dumpStack() Thread.currentThread()::dumpStack
(str, i) -> str.substring(i) String::substring
(String s) -> System.out.println(s) System.out::println

如何构建方法引用

方法引用只是Lamdba单一方法的语法糖。

方法引用主要有三类。

(1) 指向静态方法的方法引用(例如Integer的parseInt方法,写Integer::parseInt)。

(2) 指 向 任 意 类 型 实 例 方 法 的 方 法 引 用 ( 例 如 String 的 length 方 法 , 写 作String::length)。

(3) 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)。