Java-Lambda篇-第一章:函数接口
前言:
Java 8 迎来了新的时代,其中函数式编程也是新时代带来的新功能,其实函数式编程早已发展多时,但是Java 8 才引入这样的思想,学习Lambda也需要程序员付出额外的成本,当然这些代价换来的是可观的效率提升!以往我们使用的是指令式的编程,程序写的每一步都需要我们去监督,但是函数式编程让我们脱离了这种概念,让我们更加关注业务,更关注"去做什么",而不是"怎么去做"。
我的理解是:面对对象编程中,接口、抽象类是对对象的一种规范,它让对象有更多具备可能性的扩展;函数式编程中,函数式接口实际上是对方法的一种规范扩展,它让方法有了非常多样的可能性。以往我们使用各种重写方式、匿名内部类方式去改变对象的方法执行,都绕不开"面对对象"的特性,有了函数式编程,无需建立、重写对象,而是将方法传递给对象去执行就行了。这里再次强调明确一点:面对对象,只是Java众多编程特性之一,Java 8之后,函数式编程也成为了它一大特性之一。
当Lambda表达式学习熟练后,你还就可以获得新的多线程概念:异步,如同JavaScript中,充满了函数式和异步回调函数。还有RxJava,中文称之为"反应式",Spring中因此新增了反应式Spring,好处就是异步。我们程序员对这些新技术以及带来的更好的编码效果几乎是无法抗拒的,现在我们从最基础的Lambda使用来开始吧。
1.Lambda引用值,而不是变量
java 8 可以引用非final变量,和只给变量赋值过一次的变量。在Lambda表达式中,无法使用非常量,否则会报错!
所以说lambda引用的是值,而不是变量。
例子:多次赋值导致Java不认为它是最终变量
String name = user.getName();
//首次赋值
name = "Jack";
//再次赋值
list.forEach(System.out::print(name))
//错误:local variables referenced from a lambda expression must be final or effectively final
Lambda表达式中引用的局部变量必须是final 或者 可预知final(只赋值过一次),所以你需要显性的使用final或者赋值完毕后再也不会改变这个变量。
因为值引用是存在于栈之中,而栈中的变量是线程私有的,所以它是线程安全的,而对象的变量存在于堆中,栈中存储的是对对象的指针,而堆是线程共享的,所以它是线程不安全的。
Lambda表达式采用引用值的方式来保证每次计算的原子性,它在操作过程中不应被其他线程所影响。因此这也是函数式编程对多线程的一大优势:安全、计算结果不可变。
2.函数接口
应该听过注解 @FunctionalInterface 吧?
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
//Java源码之函数接口: Consumer
没错,他就是函数接口注解,主要是帮助编译器辨别该接口是函数式接口,那么Java 8 带来了那些函数式接口呢?比较重要的接口如下:
接口 |
参数 |
返回类型 |
例子: |
内部方法 |
Predicate<T> |
T |
boolean |
传入lambda表达式的值,返回布尔值,满足返回true |
test(T t) |
Consumer<T> |
T |
void |
传入lambda表达式的值,没有返回值 |
accept(T t) |
Function<T,R> |
T |
R |
传入lambda表达式的值,表示T到R的转化过程 |
apply(T t),andThen<T, V after>,compose<V, R before> |
Supplier<T> |
None |
T |
工厂方法,生产者方法 |
get() |
BiFunction<T,U,R> |
(T,U,R) |
R |
T表示输入类型,U是中转类型,R表示返回结果类型 |
apply(T t),andThen<T, V after>,compose<V, R before> |
现在,你看到这些东西肯定会头疼欲裂,这是什么新语法,还有这些泛型,别着急,之后的会讲解这些魔法的妙用之处,现在你需要对四个常用接口敏感:
1.Predicate<T> , 传入一个函数式,判断是否正确,最后返回的是布尔值;
2.Consumer<T> ,传入一个函数式,对列表中所有的值做一个算法、操作,怎么说都行,但是没有任何的返回值;
3.Function<T,R> , 转化器,例如你将String转化为int的方法写成一个函数,它接受后会帮忙操作。
4.BiFunction<T,U,R>,转化器,它更加难以使用,当你输入T类型时可能需要先转化为U类型,在U类型上进行操作后最终它会产出并转化为R
这三个接口是我们用到最频繁的接口了怎么一看感觉超级简单,但是别着急,我还要介绍特化流:
Java中存在引用类型(例如Integer,Object,List)和基本类型(int,double,byte),Java泛型的实现方式只能绑定引用类型,因此还存在着基本类型特化流,来避免频繁的装箱拆箱带来的性能损耗:
部分特化流:
接口 |
参数 |
返回类型 |
例子: |
内部方法 |
IntPredicate<T> |
T |
boolean |
返回布尔值,传入lambda表达式的值,满足返回true |
test(T t) |
DoublePredicate<T> |
T |
boolean |
返回布尔值,传入lambda表达式的值,满足返回true |
test(T t) |
LongPredicate<T> |
T |
boolean |
返回布尔值,传入lambda表达式的值,满足返回true |
test(T t) |
IntConsumer<T> |
T |
void |
输出一个值 |
accept(T t) |
IntFunction<T,R> |
T |
R |
获得一个值,返回另一个值 |
apply(T t) |
IntToLongFunction<T,R> |
T |
R |
获得一个值,返回另一个值 |
apply(T t) |
... |
|
|
|
|
简单明了,就是因为Java泛型只能泛型对象,而不能泛型基本类型,虽然Java能自动的帮我们拆箱装箱,但它也隐藏了性能上的损耗!
当然这种特化流还有很多,不会一一列举出来。
3.类型判断
某些情况下,用户需要手动指明类型。有时省略可以减少干扰,有时有需要帮助理解代码,
java7允许目标类型推断扩展,可以棱形操作符推断泛型类型
list<String> lists = new list<>();
//java 8 更近一步的类型推断,无需对右侧List设定泛型
Integer atlse = 10;
//设定一个Number对象
Predicate<Integer> atLset5 = x -> x>5;
//编写一个函数式1号
Predicate<Integer> atLset2 = x -> x>2;
//编写一个函数式2号
System.out.println(atLset5.test(atlse));
//true
System.out.println(atLset5.and(atLset2).test(atlse));
//true
在函数式中,函数也可以进行泛型,并且在你引入函数表达式时,它可以反推出你的对象类型,从而保证函数能够正常运作。
你可以将Lambda表达式理解为汽车工厂,每一个函数操作都是工厂中的机械臂,它们在给汽车压壳、喷漆、安装发动机、车轮……最终产出了一辆辆崭新的安全环保绿色能源电池车。
理解函数式接口是Java-Lambda的第一步,此后相信你还会反复来往于此观察函数式接口,理解它们也有助于你理解其他人的Lambda框架作用。
Lambda代码演示:
//处理一串订单(伪代码)
List<Orders> OrdersList = OrderService.getOrders();
OrdersList.stream()
.ship(100)//跳过前100个订单
.filter(Order::getActive) // 获取已经激活的订单
.distinct() // 去除重复项
.limit(50)// 只取得50个订单,满足分页需要
.forEach(System.out::println);//打印到控制台(测试用代码)
//DSL模式,创建一个股票订单
Order order =
forCustomer( "BigBank",
buy( t -> t.quantity( 80 )
.stock( "IBM" )
.on( "NYSE" )
.at( 125.00 )),
sell( t -> t.quantity( 50 )
.stock( "GOOGLE" )
.on( "NASDAQ" )
.at( 125.00 )) );