GitHub源码: https://github.com/alexandregama/java8-guides-tutorials
GitHub的JDK8 Demo主要包含两点:
- Lambda表达式的使用(几个JDK函数式接口类);
- Stream()的使用;
函数式接口?
@FunctionalInterface
public interface RealFunctionalInterface {
// 唯一的抽象方法
void doSomething();
// 可以存在default修饰的普通方法
default void doAnotherThing() {
System.out.println("default-method");
}
// 可以有static方法
static void doB() {
System.out.println("static-method");
}
}
说明:使用@FunctionalInterface
修饰的接口,仅仅包含一个抽象方法,但是可以有其他的static方法和default方法(可以有方法体);
default方法是JDK8新加入的,让一个接口interface除了static方法,可以有普通的、具有方法体的方法,这个方法可以被继承,因此也可以被重写;
这里测试一下default的继承,顺便再复习一下Java父子之间的继承关系,eg:
class RealFunctionalInterface_class implements TestFunc.RealFunctionalInterface {
@Override
public void doSomething() {
System.out.println("doSomething-class");
}
@Override
public void doAnotherThing() {
System.out.println("default-method-class");
}
// 可"继承-重写-重载"父类接口的default方法
public void doAnotherThing(String s) {
System.out.println("default-method-class");
}
//
static void doB() {
System.out.println("static-method-class");
}
}
测试:
@Test
public void TestSupperClassAndFunctionalInterf() {
RealFunctionalInterface r1 = new RealFunctionalInterface_class();
r1.doSomething();
r1.doAnotherThing();
// r1.doB();// error,父类引用指向子类对象,无法用该引用调用父类static方法(父类的static方法丢失)
// r1.doAnotherThing("SSS");// error,父类引用指向子类对象,该引用无法调用子类有但是父类没有的方法,称为"方法丢失"
RealFunctionalInterface_class r2 = new RealFunctionalInterface_class();
r2.doAnotherThing("SSS");// √
RealFunctionalInterface.doB(); //static方法"与类绑定",不存在"继承static方法",子类也可以写一个static方法doB(),但这不是继承/重写;
RealFunctionalInterface_class.doB();
}
Lambda表达式?
先从接口的使用开始说起,一般的使用情景:classA继承接口IA-实现其抽象方法doA()-使用方法doB(IA a),将classA的对象传入(实现多态);
这么做的目的是什么呢?
一般来说,我们真正需要的不是这个IA/classA类型的a对象,而是classA中doA()方法的具体实现,因此我们关注的不是"对象"而是"方法(函数)",那么能不能将"方法(函数)"传入呢?这么做的好处——不用为了传入一个具体的方法实现而去定义classA,类似的思想有匿名类的使用(eg: new IA(){ @override doA(){...} }),但这种方式还是有很长一串代码,而我们关注的仅仅是doA()方法的内容(称之为"函数"),那么为什么不能将这个"函数"直接传入来取代接口的实例呢?——可以,使用Lambda表达式!
eg:经典的两个接口Runnable和Comparator,分别有run()和compare()方法需要实现,run()方法无返回值-无传入参数,compare()返回int-传入两个String;Lambda表达式写法:
@Override
public void run() {
System.out.println("OK");
}
其Lambda表达式写法: ()->System.out.println("OK");
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
其Lambda表达式—— (s1,s2)->s1.compareTo(s2); // return也可以省略,s1/s2类型可推出来,也可省略;
还有一种情况,Lambda表达式虽然作为方法参数代替了接口对象,但它毕竟不是真正的对象,无法作为句柄(引用)显示的访问接口/类的(default)方法,但是可以把Lambda表达式"赋值"给接口对象;eg:
Comparator<Student> comparator = (s1, s2) -> s1.getName().compareTo(s2.getName());
int result = comparator.compare(mockito, java);
几种JDK自带的常用函数式接口@FunctionalInterface?
这么理解,将函数式接口看做一种"操作"——包括:
(1)是否传入参数/传入几个参数;
(2)操作后,有没有返回结果/返回什么类型的结果;
1.Predicate<T>——传入一个T类对象,返回一个boolean结果;
public boolean test(T t);
可以使用.and(Predicate<T>)/.or(Predicate<T>)连接两个断言对象;
p.negate()返回一个对断言的逻辑取"非"的Predicate对象;
2.Consumer<T>——传入一个T类对象,不返回任何值,相当于把"这个对象给消费了";
public void accept(T t)
有一个.andThen()
方法,连接两个Consumer<T>对象;
3.Supplier<T>——不传入任何参数,返回一个T类型的对象,相当于一个"提供者";
public T get()
只有一个get()方法;
4.Function<T, R>——传入一个T类型参数,返回一个R类型对象,相当于"映射/处理"操作,将T对象处理转换成R对象;
public R apply(T t)
.compose(Function<T> before)
,接另一个Function对象,将T传给前一个Function,返回的值作为当前Function的输入;.andThen(Function<T> after)
刚好与其相反;
其他的还有二元运算...与上面类似;
Stream()流库?
针对集合类(List、Set、Map),更为高效的操作,可以利用CPU的多核性能parallelStream(),这种操作是"惰性"的,即只有需要结果的时候才会去计算,并且不会修改原有的集合类;
Stream操作像一个"生产线"一样,可以组合多个"中间操作",最后一步输出时需要"终端操作",会关闭流;
常用操作分类:
(1) "中间操作,返回流"——过滤filter(Predicate)、排序sorted(Comparator<T>)、映射map(Function<T,R>)、返回指定数量n的元素流limit(long n)、元素去重distinct()、丢弃流的前n个元素skip(long n);
(2) "终止操作,中断流,返回某个值"——匹配Match/noneMatch(Predicate)返回boolean、计数counting()返回流中元素个数、T reduce(T identity, BinaryOperator<T> accumulator)规约,制定初始一个T类对象,将第一个流元素与其操作得到一个T对象,依次遍历整个流,最后只得到一个T对象、R collect(Collector collector)将整个流的所有元素收集起来,放到一个Collector集合里面,如Collector.toList();*也可以接Collectors.groupingBy(Function classifier, Collector downstream)先分组,然后将每组的元素放入容器;
(3) JDK8 之后,对于有返回值的方法,不要再使用nut null判断了,应该使用Optional<T>,它相当于一个容器用来方返回值的,如reduce()的结果,他有get()取值/isPresent()判断非空,有值则consumer操作/orElse()为空则返回其他值,是一个防止NullPointException的漂亮工具;
《Java8 实战》
第 1 章
为什么关心JAVA 8?
改变很多,改变影响很大;简洁代码-Lambda,多核并行-Stream,线程与锁;
- 流处理,将不相关的操作分别交给多个CPU内核并行执行;
- 把代码作为参数传递给方法;
- 并行-共享数据,对于流的并行,需要输入"纯函数",不能多个进程同时访问该共享变量,因此用"代码替换对象";
- 对集合的操作,更多的关注"做什么"而不是给出Iterator遍历集合元素的过程;
- 编程的目的在于操作值(对象),方法/类看做是次要的/辅助的,现在将方法/Lambda也看做"一等公民";
- 方法引用,充分利用已有的方法ckass::method,简化写法;
- Lambda与匿名类,配合方法引用,Lambda更加简洁;
- 集合类-for each "外部迭代",流-forEach()"内部迭代";
- 多线程的问题——变量同步;Stream的设计,包括里面的各个方法在实际的时候就考虑到,让这些操作之间无冲突/可并行;这种思想有点像是"分治"或者"归并";
- 关于接口,如果JDK的某些接口改进,要增加功能,这将会带来大问题——它的子类太多了,每个子类都要修改,解决——允许接口有实例方法,即default方法,与普通方法一样,能被继承/重写/重载;这样的话,增加了已发布接口的新功能而不破坏已有的实现;
- 接上面的问题,这样可能会导致C++父类多继承的典型问题——"菱形继承",给出测试如下,结论:编译器不让一个类继承两个具有"完全一样的"default方法的接口;可以继承继承两个具有"重载关系的"default方法的接口;因为两个父类接口的同名default方法仅返回值不同,不构成"重载关系",因此也是不允许的;以下是测试代码:
package cn.ActionInJ8;
/**
* Created by 11103381 on 2019/8/29.
*/
public class TestDefaultMethod_01 implements IA, IB {
}
interface IA {
default void doSth(){//第1类default方法,与B完全一样
System.out.println("IA");
}
default void doIt() {//第2类default方法,与B是"重载"关系—
System.out.println("do It");
}
default void okay() {//第3类default方法,与B是"非重载"关系——仅仅返回值不同
System.out.println("do It");
}
default void oJbKay() {//第4类default方法,与B是"重载"关系——不仅返回值不同,传参也不同
System.out.println("do It");
}
}
interface IB {
// default void doSth(){//上面继承A、B会编译错误
// System.out.println("IB");
// }
default void doIt(String s) {
System.out.println("do It " + s);
}
// default String okay(){//上面继承A、B会编译错误
// return "do It";
// }
default String oJbKay(String s) {
return "do It,oJbKay";
}
}
第 2 章
行为参数化:定义一种行为,或者叫"功能/操作",以代码块的形式;把这个代码块准备好后,丢给其他程序,这样其他程序需要调用的时候才会调,这个代码块被"推迟执行";
- 不断变化的需求;需求变化,比如说,更小的需求变化,不推荐的做法是:新写一个方法,复制旧方法的大量代码,改其中一个小逻辑;思想:将这些"比方法还小的功能抽象出来",让代码尽可能逻辑清晰-精简代码;那不用新方法实现新需求,该怎么办?——用代码块,即让"行为参数化";
具体操作:写个接口-把这个接口对象作为参数传递到旧方法中-接口里面定义了一个抽象方法-根据不同的需求去写这个接口不同的实现类-让这些实现类的对象作为不同方法的"载体"传入旧方法去实现不同的功能;
-
以上的做法有一点不足——真正需要的不是这个接口对象,而是它的方法实现,这个方法的使用必须依赖这个接口对象,每次使用的时候,需要先定义接口的实现类实现方法;还有一点,这种接口有一个最大的特点,只有一个特定功能,即一个抽象方法!(这个规律可以利用一下-注解)
-
面对上面的问题,第一次改进:不去写这个接口的实现类,而是使用匿名类,这样项目工程就会少很多类;结果发现,需要写的代码并没有减少多少,还有个问题,如果在一个方法内去new匿名类的对象,当方法内部/形参、匿名类的内部、匿名类的实现方法的内部 都有一个重名的变量的时候,对这个变量的操作规则就很复杂(配合this关键字),如下面的测试例子;
-
尝试使用Lambda代替接口对象;eg:Runnable和Comparator,Lambda替换
run()
和compare(T t1,T t2)
;
关于匿名类的有趣的测试:
package cn.ActionInJ8;
import org.junit.Test;
/**
* Created by 11103381 on 2019/8/29.
*/
public class TestAnonymousClass {
int num = 10;//成员变量
@Test
public void printnum(int num) {
System.out.println(num);//输出方法的形参
System.out.println(this.num);//对于成员方法,this指代当前的类的对象,因此这个this.num输出(1)定义的成员变量"Class"
}
@Test
public void doSth() {
int value = 10;//(1)
TestAnonymous ta = new TestAnonymous() {
int value = 9;//(2)
@Override
public void doA() {
int value = 8;//(3)
System.out.println(value);//输出doA()内部的局部变量(3) 8;
System.out.println(this.value);//输出doA所在的匿名类的内部定义的成员变量(2) 9;
this.value++;//允许修改,这里value不是final;
value++;//允许修改,这里value不是final;
}
};
ta.doA();
}
@Test
public void getnum(int num) {//当 在一个方法A内使用匿名类new一个接口对象,并且在匿名内内部(无论是抽象方法内/外)使用该方法A的形参 的时候,这个形参必须是final(隐式),不能被修改;
TestAnonymous ta = new TestAnonymous() {
// int myInt = num++;//error,不能修改形参num;
@Override
public void doA() {
System.out.println(num);
// num++;//error,不能修改形参num;
}
};
}
@Test
public void getnum() {//当 在一个方法A内使用匿名类new一个接口对象,并且在匿名内内部(无论是抽象方法内/外)使用该方法A内部定义的局部变量的时候,这个形参必须是final(隐式),不能被修改;
int num = 10;
TestAnonymous ta = new TestAnonymous() {
// int myInt = num++;//error,不能修改形参num;
@Override
public void doA() {
System.out.println(num);
// num++;//error,不能修改形参num;
}
};
}
}
interface TestAnonymous {
void doA();
}
内部类引用的外部变量是隐式的final修饰
补充:这里再补充一个例子,关于lambda与匿名类的this关键字的含义
Lambda表达式/内部类的this关键字
匿名类的被实现的方法的方法体内的this,指的是这个匿名类的对象;外部类对象this需要显示的指明,即OuterClass.this;而lambda的this指的是外部类对象;
有趣的帖子:Java匿名类遇上final - 简书(图)
1. final遇见内部类
Java中要求如果方法中定义的中类如果引用方法中的局部变量,那要要求局部变量必须要用final修饰(JDK8中已经不需要,但是本质也是和final类似——只读),实例代码如下:
interface Inner{
void method();
}
class Outer{
public Inner createInner(){
final int a = 12;
final Map map = new HashMap();
Inner inner = new Inner(){
public void method(){
int b = a + 1;
System.out.println(" in Inner, b=" + b);
map.put("innerKey", "innerValue");
}
};
System.out.println("in Outer, createInner finish!");
return inner;
}
public static void main(String []args){
Inner inner = new Outer().createInner();
inner.method();
}
}
输出如下:
in Outer, createInner finish!
in Inner, b=13
Note: 上述代码仅仅是展示使用,其中createInner()
方法中的map
变量是存在内存泄漏的,因为外界无法访问他,但是却会被一致持有。关于内存泄漏的问题,通过查看上述代码便后的class文件的内容即可发现。
上述文件编译后,生成了三个文件:
-
Inner.class
-
Outer.class
-
Outer$1.class
打开Outer$1.class可以看到如下内容:
class Outer$1 implements Inner {
Outer$1(Outer this$0, int var2, Map var3) {
this.this$0 = this$0;
this.val$a = var2;
this.val$map = var3;
}
public void method() {
int b = this.val$a + 1;
System.out.println(" in Inner, b=" + b);
this.val$map.put("innerKey", "innerValue");
}
}
可以看到编译后的内容,Inner匿名类拥有另一个带有三个参数的构造方法,
Outer this$0
: 也就是拥有了Outer(外部类)当前对象的一个引用,所以我们Inner的子类中,可以通过Outer.this
访问外部Outer
类的当前实例。var2
: 此处应该为Outer createInner()
方法中的局部变量avar3
: 此处应该为Outer createInner()
方法中的局部变量Map
通过上述编译后的代码,我们大概可以明白为什么匿名类可以访问其外部数据的原因,接下来我们可以讨论一下为什么要对createInner()
中的局部变量a
, map
用final进行修饰。
为了简化表述,以下将Inner匿名类里面的a表述为Inner().a
, 将createInner()
方法中的a表示为 createInner.a
.
通过编译后的代码可以看出来,Inner().a
和createInner.a
不是同一个对象(在内存中不是同一个), 同样的,两个map(值,存在于堆)在内存中也是不同的,但是两个map的都指向了堆上的****同一个****HashMap
对象。理论上我们是可以重新设置Inner().a
和Inner().map
的值的,但是java编译器并不允许这样做, 具体原因我认为可能是如下原因:
在匿名类内部访问外面的变量看起来是一个很正常的需求,而且直观看起来应该是同一个东西。但是在方法调用结束以后局部变量会被销毁(栈里面的内容,也就是
createInner.a
,createInner.map
。如果是同一个东西的话,那么意味着jvm在方法调用结束以后还不能销毁这些局部变量,需要将这些局部变量的生命周期保持到和Inner一样长,这样让jvm的实现起可能会更为复杂(提升这些变量的生命周期)。
所以,为了实现在Inner
中可以访问createInner()
中的a
, map
,同时他们看起来和createInner()
中的一样(一致),并且避免JVM对对象生命周期的管理过于复杂,采用了一个中折中的办法:
- 将被用到的变量作为
Inner
的构造函数参数传入并在Inner
内部设置对应的实例(private)。- 将
createInner().a
,createInner().map
设置为final,并且匿名类类部不可以修改对应实例属性的值,保证一致性。
通过上述的 1中,可以很自然实现在Inner
中很自然的访问createInner
中局部变量的值;由于Inner
中使用的变量实际上和外部函数中的局部变量是不一样的,通过上述2可以保证他们一致(都不允许修改了,肯定一致), 否则开发者在内部修改值,但是却不会影响到外面的局部变量,这会让人困惑(天然看起来应该是一个东西啊,但是却不能一起变化)。
2. 闭包
此处引出了Java对闭包的支持,其实Java目前是支持了闭包的,匿名类就是一个典型的例子。将自由变量(createInner.a
,createInner.map
)封装到Inner中,但是Java的闭包确实有条件的闭包,因为Java只实现了capture-by-value, 只是把局部变量的值copy到了匿名类中, 没有实现capture-by-reference。如果是capture-by-reference的实现方式,可能需要将局部变量提升到对象中(也就是将局部变量的生命周期延长,变为和Inner
类一样长, 那么在createInner()
执行完毕以后,就不会销毁 a
, map
了)。
关于闭包的定义:Ruby之父松本行弘在《代码的未来》一书中解释的最好:闭包就是把函数以及变量包起来,使得变量的生存周期延长
3. 内存泄漏
Java中的闭包并没有真正的实现延长生命周期, 但是间接实现了createInner.map
的生命周期,因为Inner.map
是一个对实际的HashMap()
(位于堆中)对象的引用, 所以在createInner()
方法中创建,但是却不会在该方法执行以后被GC
回收, 该对象的生命周期和其创建Inner
实例一样长。在本例中的代码的内存泄漏就由此而生。也就是说,如果是仅仅使用方法的局部变量(如map),而不是把map赋值给Inner的一个对象作为方法的返回值,则map在方法调用完成后就会被GC回收。
第 3 章
Lambda表达式
-
函数式接口;第2章说过,"让这些接口的实现类的对象作为不同方法的"载体"传入",这种接口有一个最大的特点,只有一个特定功能,即一个抽象方法!现在使用注解来标记这种类型的接口,@FunctionalInterface即函数式接口(编译时会检查,抽象方法超过1个会报错);
-
关于基本类型的"装箱与拆箱",自动装箱会影响性能,如int型到Integer对象会增加所需的内存,并且需要额外的内存搜索来获取被"包装起来"的原始基本类型的值;在函数式接口中的泛型T,如Integer,再使用过程中带来了优化,避免了"自动装箱"来提升性能;
-
Lambda与异常;JDK自带的函数式接口不允许抛出受检查异常(如常见的IOException),而自己定义的@FunctionalInterface可以抛出;IO异常很常见,但又不能抛出,怎么解决呢?
(1) 在方法体,即Lambda的{}内部try/catch,缺点是只能自己catch这个异常,如果自己处理不了这个异常怎么办?;
(2) 优雅的做法——利用泛型,具体的代码示例如下:
package cn.ActionInJ8;
import org.junit.Test;
import java.io.IOException;
import java.util.stream.Stream;
/**
* Created by 11103381 on 2019/8/29.
*/
public class TestFuntionalInterface {
private static void UseInterface(IfTestFuntionalInterface_02 a) {
try {
a.doA();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void func_01() {
UseInterface(() -> { //自定义的函数式接口可以抛出受检查异常
System.out.println("A");
throw new IOException();
});
}
@Test
public void func_02() {
// 传入Consumer
// Stream.of(1,2,3,4,5).forEach(i->{throw new IOException();}); //编译错误,Consumer不能抛出受检查异常,如IOException;
Stream.of(1, 2, 3, 4, 5).forEach(i -> { // 通过编译,可以抛出非检查异常,如RuntimeException;
throw new RuntimeException();
});
}
// 针对上面不能抛出受检查异常的问题,编写一个泛型方法对异常进行包装
static <E extends Exception> void doThrow(Exception e) throws E {
throw (E) e;
}
@Test
public void func_03() {//
Stream.of(1, 2, 3, 4, 5).forEach(i -> {
doThrow(new IOException()); // 编译成功,实现了Consumer抛出受检查异常
});
}
}
interface IfTestFuntionalInterface_01 {
void doA() throws RuntimeException;
default void doB() { }
}
@FunctionalInterface
interface IfTestFuntionalInterface_02 {
void doA() throws IOException;
}
- Lambda表达式可以直接赋值给该函数式接口的引用(就像是一个对象赋值给一个引用),那么Lambda是不是一个Object对象呢?并不是!测试如下:
@Test
public void func_04() {//Lambda是不是Object对象?
IfTestFuntionalInterface_01 IF = () -> System.out.println("A");// 编译成功,Lambda能直接赋值给该函数式接口的引用;
IF.doB();
// Object o1 = ()->{System.out.println("A");}; // 编译失败Lambda不能直接赋值给Object;
// ()->{System.out.println("A");}.doB(); // 编译失败,Lambda不能直接作为句柄访问接口的方法;
}
Lambda访问方法的局部变量;举个例子如下,
Runnable func_05() {
int a = 10;
List<String> l = new ArrayList<>();
l.add("aaa-");
a++;
Runnable r = () -> {
System.out.println("Run!" + (l.add("bbb-")));// 编译成功,因为引用是final,里面的内容可以改;
// System.out.println(a);//这句如果加上去,则编译错误,编译器提示Lambda使用的a为隐式的final;
};
a++;
r.run();
System.out.println(l);//
System.out.println(a);
return r;
}
Δ为什么必须是final的?还有个问题,Lambda访问的是"真正的"方法局部变量a和l吗?
——不是!
因为当这个方法有返回值,如返回这个Runnable r时候,方法执行完,返回Runnable对象,方法的局部变量被回收(局部变量和引用在虚拟机栈上,对象本身放在堆里面,引用指向的对象也因为可达性分析没有root连接的被GC),而这个Runnable作为一个仍然存在的返回值,仍然"持有"a和l,这是矛盾的!
怎么解决?实际上,Runnable作为一个内部类的对象,其Lambda获取的a和l是局部变量的"副本",这样就可以在方法结束后仍然有局部变量指向的对象的引用;这又出现了一个问题!当Lambda内部修改这个副本的时候,不就导致了Lambda内的a和l,与外部局部变量的a和l的值"不一致"吗?(因为一开始的目的是让"Lambda访问外部的局部变量",也就是说二者至少看上去要是"一致的")
怎么解决?让他们都是final的就可以了,都不给修改,这样就"一致"了,看上去"像是Lambda访问了外面的方法内的局部变量","好像延长了局部变量的生命周期";
如果觉得难以理解,记住即可,与匿名类类似,访问的局部变量必须是隐式的final;
方法引用/构造函数引用;——Lambda的语法糖!
利用现有的方法(JDK)自带的,减少代码的复制,进一步精简;
*(1)方法引用
1. 1个参数-对象a作为toString()方法的调用者,可以省略;eg:(Interger a)->a.toString(); 转成 Integer::toString;
2. 1个参数-对象s作为println()的传入参数,可以省略;eg:(String s)->System.out.println(s); 转成System.out::println;
3. 无参数-无参数传入,调用Thread类的方法;eg:()→Thread.currentThread();转成Thread::currentThread;
4. 2个参数,a.doSth(b)的形式,可以省略,要注意顺序;eg:(str,i)→str.subString(i);转成String::subString;
注意:这里仅关注参数的个数和形式,不关注是否有返回值;
(2)构造函数引用
构造函数其实是隐式的static的,把它看作是名字为new的static函数即可;需要注意构造函数的传参个数;eg:
1.无参构造器;
Supplier<Apple> s = Apple::new; Apple apple1 = s.get();//Supplier<T>/T get() 无传入,返回一个T对象;
2.一个参数;
Function<Integer, Apple> f = Apple::new; Apple apple2 = f.apply(10);//Function<T,R>/R get(T) 传入T类对象,返回一个R对象;
3.两个参数;
BiFunction<Integer,String, Apple> bf = Apple::new; Apple apple3 = bf.apply(10,"BigApple");//Function<T1,T2,R>/R get(T1,T2) 传入T1和T2两个对象,返回一个R对象;
构造函数引用的好处:延迟new对象,即Lambda表达式仅仅是一种操作-产生对象的工具,什么时候产生对象取决于我们什么时候需要这个对象(调用上面的get/apply方法);eg:
static Map<String, Function<Double, Fruit_01>> fruitFactory = new HashMap<>();//key-ID,value-产生不同水果的方法;
static {
fruitFactory.put("apple", Apple::new);
fruitFactory.put("Orange", Orange::new);
//...
}
static Fruit_01 getOneFuit_01(String name, double weight) {
return fruitFactory.get(name.toLowerCase()).apply(weight);
}
@Test
public void func_01() {
Fruit_01 apple = getOneFuit_01("APPLE", 10);//来10斤苹果;
}
(3)嵌套使用
@Test
public void func_02() {//嵌套使用方法引用
List<Apple> la = new ArrayList<>(Arrays.asList(new Apple(15), new Apple(10)));
la.sort(Comparator.comparing(Apple::getWeight)); // comparing传入一个Function,对Function的apply的返回值调用compareTo:(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
la.stream().forEach(System.out::print);
}
谓词复合(解释式编程,像是口述告诉程序的流程怎么走);
(1)比较器Comparator 逆序/比较链.reversed()
对比较器"取反";.thenComparing()如果前面的比较得到的两个相等的元素,则按照下一个比较器继续比较;
(2)Predicate<T>
可以使用.and(Predicate<T>)/.or(Predicate<T>)
连接两个断言对象,表示与/或关系;p.negate()
返回一个对断言的逻辑取"非"的Predicate对象;
(3)Consumer<T>.andThen()
方法,连接两个Consumer<T>对象,相当于顺序执行,即传入一个对象T t,对其先后执行Consumer1.accept(t);Consumer2.accept(t')
;
(4)Function<T, R>.compose(Function<T> before)
,接另一个Function对象,将T t传给前一个Function,before.apply(t)返回的值作为当前Function的输入;.andThen(Function<T> after)
刚好与其相反;
其他的还有二元运算...与上面类似;
Stream()流库?
针对集合类(List、Set、Map),更为高效的操作,可以利用CPU的多核性能parallelStream(),这种操作是"惰性"的,即只有需要结果的时候才会去计算,并且不会修改原有的集合类;
Stream操作像一个"生产线"一样,可以组合多个"中间操作",最后一步输出时需要"终端操作",会关闭流;
常用操作分类:
(1) "中间操作,返回流"——过滤filter(Predicate)、排序sorted(Comparator<T>)、映射map(Function<T,R>)、返回指定数量n的元素流limit(long n)、元素去重distinct()、丢弃流的前n个元素skip(long n);
(2) "终止操作,中断流,返回某个值"——匹配Match/noneMatch(Predicate)返回boolean、计数counting()返回流中元素个数、T reduce(T identity, BinaryOperator<T> accumulator)规约,制定初始一个T类对象,将第一个流元素与其操作得到一个T对象,依次遍历整个流,最后只得到一个T对象、R collect(Collector collector)将整个流的所有元素收集起来,放到一个Collector集合里面,如Collector.toList();*也可以接Collectors.groupingBy(Function classifier, Collector downstream)先分组,然后将每组的元素放入容器;
(3) JDK8 之后,对于有返回值的方法,不要再使用nut null判断了,应该使用Optional<T>,它相当于一个容器用来方返回值的,如reduce()的结果,他有get()取值/isPresent()判断非空,有值则consumer操作/orElse()为空则返回其他值,是一个防止NullPointException的漂亮工具;