Java8函数之旅(四) --四大函数接口

前言

  Java8中函数接口有不少,大概有几十个吧,具体到底是多少我也数不清,因此一开始看的时候感受一脸懵逼,不过其实根本没那么复杂,毕竟不该该也不必把一个东西设计的很复杂。java

几个单词

  在学习了解以前,但愿你们能记住几个单词,掌握这几个单词,什么3,40个官方的函数接口都是小问题了,不信的话接着往下看啦。ok,那这几个单词呢分别是supplier 提供者,consumer 消费者,function 函数,operation 运算符,binary 二元(就是数学里二元一次方程那个二元,表明2个的意思),双重的安全

四大基础函数接口

  函数接口,你能够理解为对一段行为的抽象,简单点说能够在方法就是将一段行为做为参数进行传递,这个行为呢,能够是一段代码,也能够是一个方法,那你能够想象在java8以前要将一段方法做为参数传递只能经过匿名内部类来实现,并且代码很难看,也很长,函数接口就是对匿名内部类的优化。
  虽然类库中的基本函数接口特别多,但其实整体能够分红四类,就好像阿拉伯数字是无限多的,但总共就10个基本数字同样,理解了这4个,其余的就都明白了。闭包

Functio<t,r>接口

   function,顾名思义,函数的意思,这里的函数是指数学上的函数哦,你也能够说是严格函数语言中的函数,例如haskell里的,他接受一个参数,返回一个值,永远都是这样,是一个恒定的,状态不可改变的方法。其实想讲函数这个完全将明白能够再开一篇博客了,因此这里不详细的说了。
   上面说到,函数接口是对行为的抽象,所以我方便你们理解,就用java中的方法做例子。app

   Fcuntion接口是对接受一个T类型参数,返回R类型的结果的方法的抽象,经过调用apply方法执行内容。函数

public class Operation{

/* 
    下面这个方法接受一个int类型参数a,返回a+1,符合我上面说的接受一个参数,返回一个值
    因此呢这个方法就符合Function接口的定义,那要怎么用呢,继续看例子 
*/
public static final int addOne(int a){
    return a+1;
}

/* 
    该方法第二个参数接受一个function类型的行为,而后调用apply,对a执行这段行为
*/
public static int oper(int a, Function<Integer,Integer> action){
    return action.apply(a);
}

/* 下面调用这个oper方法,将addOne方法做为参数传递 */
pulic static void main(String[] args){
    int x = 1;
    int y = oper(x,x -> addOne(x));//这里能够换成方法引用的写法 int y = oper(x,Operation::addOne)
    System.out.printf("x= %d, y = %d", x, y); // 打印结果 x=1, y=2
    
    /* 固然你也可使用lambda表达式来表示这段行为,只要保证一个参数,一个返回值就能匹配 */
     y = oper(x, x -> x + 3 ); // y = 4
     y = oper(x, x -> x * 3 ); // y = 3    
}

}

 

这里的箭头指向的位置就是形参,能够看到第二个箭头的Lambda表达式指向了Funtion接口学习

Consumer 接口

Consumer 接口翻译过来就是消费者,顾名思义,该接口对应的方法类型为接收一个参数,没有返回值,能够通俗的理解成将这个参数'消费掉了',通常来讲使用Consumer接口每每伴随着一些指望状态的改变或者事件的发生,例如最典型的forEach就是使用的Consumer接口,虽然没有任何的返回值,可是却向控制台输出了语句。
Consumer 使用accept对参数执行行为优化

    public static void main(String[] args) {
        Consumer<String> printString = s -> System.out.println(s);
        printString.accept("helloWorld!");
        //控制台输出 helloWorld!
    }

Supplier 接口

Supplier 接口翻译过来就是提供者,和上面的消费者相反,该接口对应的方法类型为不接受参数,可是提供一个返回值,通俗的理解为这种接口是无私的奉献者,不只不要参数,还返回一个值,使用get()方法得到这个返回值spa

        Supplier<String> getInstance = () -> "HelloWorld!";
        System.out.println(getInstance.get());
        // 控偶值台输出 HelloWorld

Predicate 接口

predicate<t,boolean> 谓语接口,顾名思义,中文中的‘是’与‘不是’是中文语法的谓语,一样的该接口对应的方法为接收一个参数,返回一个Boolean类型值,多用于判断与过滤,固然你能够把他理解成特殊的Funcation<t,r>,可是为了便于区分语义,仍是单独的划了一个接口,使用test()方法执行这段行为线程

 public static void main(String[] args) {
     Predicate<Integer> predOdd = integer -> integer % 2 == 1;
     System.out.println(predOdd.test(5));
     //控制台输出 5
 }

其余的接口

介绍完正面这四种最基本的接口,剩余的接口就能够很容易的理解了,java8中定义了几十种的函数接口,可是剩下的接口都是上面这几种接口的变种,大多为限制参数类型,数量,下面举几个例子。翻译

类型限制接口

  • 参数类型,例如IntPredicate,LongPredicate, DoublePredicate,这几个接口,都是在基于Predicate接口的,不一样的就是他们的泛型类型分别变成了Integer,Long,Double,IntConsumer,LongConsumer, DoubleConsumer好比这几个,对应的就是Consumer<Integer>,Consumer<Long>,Consumer<Double>,其他的是同样的道理,就再也不举例子了
  • 返回值类型,和上面相似,只是命名的规则上多了一个To,例如IntToDoubleFunction,IntToLongFunction, 很明显就是对应的Funtion<Integer,Double> 与Fcuntion<Integer,Long>,其他同理,另外须要注意的是,参数限制与返回值限制的命名惟一不一样就是To,简单来讲,前面不带To的都是参数类型限制,带To的是返回值类型限制,对于没有参数的函数接口,那显而易见只多是对返回值做限制。例如LongFunction<R>就至关于Function<Long,R> 而多了一个To的ToLongFunction<T>就至关于Function<T,Long>,也就是对返回值类型做了限制。

数量限制接口

  • 有些接口须要接受两名参数,此类接口的全部名字前面都是附加上Bi,是Binary的缩写,开头也介绍过这个单词了,是二元的意思,例如BiPredicate,BiFcuntion等等,而因为java没有多返回值的设定,因此Bi指的都是参数为两个

Operator接口

  • 此类接口只有2个分别是UnaryOperator<T> 一元操做符接口,与BinaryOperator<T>二元操做符接口,这类接口属于Function接口的简写,他们只有一个泛型参数,意思是Funtion的参数与返回值类型相同,通常多用于操做计算,计算 a + b的BiFcuntion若是限制条件为Integer的话 每每要这么写BiFunction<Integer,Integer,Integer> 前2个泛型表明参数,最后一个表明返回值,看起来彷佛是有点繁重了,这个时候就能够用BinaryOperator<Integer>来代替了。

下面是各类类型的接口的示意图,相信只要真正理解了,其实问题并不大

关于lambda的限制

Java8中的lambda表达式,并非彻底闭包,lambda表达式对值封闭,不对变量封闭。简单点来讲就是局部变量在lambda表达式中若是要使用,必须是声明final类型或者是隐式的final例如

int num = 123;
Consumer<Integer> print = () -> System.out.println(num);

就是能够的,虽然num没有被声明为final,但从总体来看,他和final类型的变量的表现是一致的,可若是是这样的代码

int num = 123;
num ++;
Consumer<Integer> print = () -> System.out.println(num);

则没法经过编译器,这就是对值封闭(也就是栈上的变量封闭)
若是上文中的num是实例变量或者是静态变量就没有这个限制。
看到这里,天然而然就会有疑问为何会这样?或者说为何要这么设计。理由有不少,例如函数的不变性,线程安全等等等,这里我给一个简单的说明

  • 为何局部变量会有限制而静态变量和全局变量就没有限制,由于局部变量是保存在栈上的,而众所周知,栈上的变量都隐式的表现了它们仅限于它们所在的线程,而静态变量与实例变量是保存在静态区与堆中的,而这两块区域是线程共享的,因此访问并无问题。
  • 如今咱们假设若是lambda表达式能够局部变量的状况,实例变量存储在堆中,局部变量存储在栈上,而lambda表达式是在另一个线程中使用的,那么在访问局部变量的时候,由于线程不共享,所以lambda可能会在分配该变量的线程将这个变量收回以后,去访问该变量。因此说,Java在访问自由局部变量时,其实是在访问它的副本,而不是访问原始变量。若是局部变量仅仅赋值一次那就没有什么区别了。
  • 严格保证这种限制会让你的代码变得无比安全,若是你学习或了解过一些经典的函数式语言的话,就会知道不变性的重要性,这也是为何stream流能够十分方便的改为并行流的重要缘由之一。

总结

本篇介绍了四大函数接口和他们引伸出的各种接口,终点是对不一样种类行为的封装致使了设计出不一样的函数接口,另外在使用函数接口或者lambda表达式的时候,要注意lambda对值封闭这个特性。

相关文章
相关标签/搜索