Java8 实战读书笔记(1~3章)
Java8 实战 读书笔记
第一章 为什么要关心Java 8
要点
- 流处理
- 用行为参数化把代码传递给方法
- 并行与共享可变数据
- 外部迭代(需要手动调用for-each去一个个迭代)
- 内部迭代(迭代在库内进行,不需要手动调用for-each)
- Java 8 Stream API 解决了集合处理时的套路和晦涩,以及难以利用多核。
- 使用默认方法,在实现类没有实现方法是提供方法内容
第二章 通过行为参数化传递代码
概述
- 这章主要介绍由于不断变更需求带来工作量,以及使用行为参数化的方式来解决这种场景下的需求
- 通过行为参数化以及匿名类的配合使用使代码变得灵活继而引出lambda表达式的出现
关键词
- 谓词 :标准库定义了一类可以"定制操作"的算法函数,这类函数接收一个参数,这个参数与以往我们所认识的参数不同,它是一个可调用的对象,其返回结果是一个能作为条件的值,这个函数就是谓词,换句话说,就是一个返回boolean值的函数,如下代码块所示,这个test抽象函数就是一个谓词。
public interface ApplePredicate{
boolean test (Apple apple);
}
- 行为参数化 让方法接受多种行为作为参数(其方法定义该多种行为的接口)作为参数,并在内部使用,来完成不同的行为。
- 匿名类 匿名类的使用其实是为了方便行为参数化,为了实现定义多种行为的接口不那么繁琐,即不去建那么多的实现类,在调用时,直接通过匿名类的形式实现接口,并做不同的行为。但通过匿名类的使用,使代码变得很啰嗦并没有那么好维护以及易读性也很差。
- lambda lambda表达式就是对以上现象的优化。
第三章
要点
- lambda管中窥豹
- 在哪里以及如何使用lambda
- 环绕执行模式
- 函数式接口类型推断
- 方法引用
- lambda复合
管中窥豹
可以把lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
- 匿名 我们说匿名,是因为它不像普通方法那样有一个明确的名称:写得少而想的多。
- 函数 我们说它是函数,是因为lambda函数不像方法一样属于某一个特定的类(我的理解是:可以在其他类去直接调用),但是和方法一样,有参数列表、函数主体等。
- 传递 lambda表达式可以作为参数传递给方法或者储存在变量中。
- 简洁 无需像匿名类那样写很多模板代码。
构成
- 参数列表 ()、(参数1,...)
- 箭头 ->
- lambda主体 表达式就是lambda的返回值了。
举例
- () -> {} 这个lambda没有参数,并返回void
- () -> "success" 这个lambda没有参数,并返回String作为表达式。
- () -> {return "success";} 这个lambda没有参数,并返回String(利用显式返回语句)
上面的例子可以看出,lambda主体 有两种,一种为表达式,一种为显式返回语句 ,显示返回语句一般用于大于一条语句的代码块,并用大括号包围,其实就是一般方法体内的代码块,写法和代码块相同,所以一旦使用大括号就要按正常写法编码。
在哪里可以使用lambda
函数式接口
函数式接口就是只定义一个抽象方法的接口。
注意:
如果接口有很多默认方法(default声明),但是只要接口只定义一个抽象方法,那这个接口还是一个函数式接口。
函数式接口的使用
lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式(其实就是方法体内的语句)作为函数式接口的实例。
函数描述符
函数式接口的抽象方法叫做函数描述符,抽象方法的签名基本上就是lambda表达式的签名。
签名示例:
()-> void
表示参数为空,返回为voidboolean test(Apple apple);
这个为函数式接口的抽象方法,那么签名便为(Apple apple) -> boolean。
注意:
lambda表达式的签名需要和函数式接口的抽象方法一样。
使用方法
public void process(Runnable r){
r.run();
}
该process方法以Runable为参数类型,方法体内调用了Runnable.run的抽象方法,总结就是,process方法使用Runnable.run()方法做最终操作,该方法没有返回值,如果有返回值就是Runnable.run()方法处理后的返回值。由于Runnable为函数式接口,run方法为抽象方法。所以调用process方法只需要像这样写:
process(() -> System.out.println("This is awesmoe!!");
从这里可以体会到什么???
如果方法入参使用了函数式接口作为参数,那么方法体里面如果使用该函数式接口调用相应的抽象方法,那么就可以在调用process方法式,在原本属于函数式接口参数的位置,直接用抽象方法实现即可,这个实现需要以lambda表达式书写,即参数列表、箭头、lambda主体。
所以回头看看函数式接口的定义,只有一个抽象方法的接口,为什么只有一个抽象方法,如果这个runnable接口的抽象方法有多种的话,按照上述的写法还能合理吗?
@FunctionalInterface
如果你想写一个函数式接口,建议加上这个注解,如果不符合函数式接口的规范的话,编译器会提示错误。这个注解不是必要的,但是很有用有没有,一是提示你自己,而是提示其他程序员。
实践
@FunctionalInterface
public interface UserService {
void testDemo();
default String sayHello(FoodService r) {
return r.sayHello();
}
}
@FunctionalInterface
public interface FoodService {
String sayHello();
}
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
public class LogbackServiceImplTest {
@Before
public void setUp() throws Exception {
System.out.println("测试开始。。。。");
}
@After
public void tearDown() throws Exception {
System.out.println("测试结束。。。。");
}
public String execute(UserService userService, FoodService foodService) {
userService.testDemo();
return userService.sayHello(foodService);
}
@Test
public void test() {
String execute = this.execute(() -> {
System.out.println("this is my demo!");
}, () -> {
return "this is my second demo!";
});
System.out.println(execute);
}
}
定义了两个函数式接口,UserService、FoodService,其中UserService还定义了一个默认方法,这个测试用例是我当时对函数式接口的一些理解而做的一些验证。
Java API中的几个函数式接口
Predicate
抽象方法:boolean test(T t);
使用场景: 当你的方法中涉及到一个布尔类型的判断条件,而且这个判断条件是动态化或者说是多样性的,那么你完全可以使用这个接口,在你的方法入参里加上一个Predicate类型的参数即可。
示例:
public List filter(List list,Predicate p){
List results = new ArrayList<>();
for(T s : list){
if(p.test(s)){
results.add(s);
}
}
return results;
}
这个示例侧重点是,对于实体s过滤条件的多样化,如果有该种场景,只需对该多样化的行为进行抽象,并将定义的函数式接口放到入参里面即可,所以上面的例子调用方式如下:
@Test
public void test() {
List strings = Arrays.asList("1", "2", "", "3", "4");
List list = this.filter(strings, (String s) -> !s.isEmpty());
System.out.println(list.toString());
}
注意:
其实从调用来看只需关注函数式接口具体实现那一部分,所以上述的filter方法内部也可以做一些环绕执行的方式。Predicate函数式接口没有指定类型,用的是泛型,所以在编写lambda表达式时,参数列表需要指定类型,如果有的函数式接口已经指定类型,那么(String s)中的String可以忽略,直接一个s -> !s.isEmpty()即可。
Consumer
抽象方法: void accept(T t);
使用场景: 对于多样化行为如果没有返回值,便可以使用该函数式接口。
示例:
public void forEach(List list, Consumer c){
for(T i: list){
c.accept(i);
}
}
调用:
@Test
public void test() {
List integers = Arrays.asList(1, 2, 3, 4);
this.forEach(integers, (Integer i) -> System.out.println(i));
}
Function
抽象方法: R apply(T t);
使用场景: 如果你的方法是一个入参为一种类型,返回值为另一种类型,那么这个函数式接口完全满足你的需求。
示例:
public List map(List list, Function f){
List result = new ArrayList<>();
for(T s : list){
result.add(f.apply(s));
}
return result;
}
调用:
@Test
public void test() {
List strings = Arrays.asList("lambdas", "in", "action");
List list = this.map(strings, (String s) -> s.length());
System.out.println(list.toString());
}
Supplier
抽象方法: T get();
使用场景: 如果你的方法是一个入参为空,返回值为另一种类型,可以使用该函数式接口
示例:
一般新建一个实体用的就是Supplier,如 Student::new,这里返回的是一个Supplier
BiFunction
抽象方法: R apply(T t, U u);
使用场景: 入参为两种类型,返回值为第三种类型。
示例:
上述五个函数式接口应该涵盖了我们平时用到方法类型,返回布尔值、返空回值、返回指定类型等方法,所以说很有用。
原始类型特化
Java类型要么使用引用类型(如Byte、Integer、Object、List),要么是原始类型(如byte、double、int、char)。但是泛型只能绑定到引用类型。这是泛型内部实现方式造成的。因此涉及到两个操作:
- 装箱 在Java里有一个将原始类型转换为对应的引用类型的机制,这个机制成为装箱。
- 拆箱 将引用类型转换为对应原始类型叫做拆箱。
示例
List list = new ArrayList<>();
for(int i = 300;i<400;i++){
list.add(i);
}
i放进到list中就涉及到装箱操作,装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门版本,以便在输入和输出都是原始类型时避免自动装箱的操作。
函数式接口 | 函数描述符 | 原始类似型特化 |
---|---|---|
Predicate |
T -> boolean | IntPredicate、LongPredicate、DoublePredicate |
Consumer |
T -> void | IntConsumer、LongConsumer、DoubleConsumer |
Funcation |
T -> R | IntFunction |
IntToDoubleFunction(入参为int类型,返回值为double类型) | ||
IntToLongFunction(入参为int类型,返回值为long类型) | ||
LongFunction |
||
LongToDoubleFunction(入参为long类型,返回值为double类型) | ||
LongToIntFunction(入参为long类型,返回值为int类型) | ||
DoubleFunction |
||
ToIntFunction |
||
ToDoubleFunction |
||
ToLongFunction |
||
Supplier |
() -> T | BooleanSupplier,IntSupplier,LongSupplier,DoubleSupplier |
UnaryOperator |
T -> T | IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator |
BinaryOperator | (T,T) -> T | IntbinaryOperator,LongBinaryOperator,DoubleBinaryOperator |
BiPredicate |
||
BiConsumer(T,U) -> void |
| |
BiFunction |
(T,U) -> R | ToIntBiFunction |
ToLongBiFunction |
||
ToDoubleBiFunction |
任何函数式接口都不允许抛出受检异常。
类型检查、类型推断以及限制
类型检查
lambda表达式的类型是通过lambda的上下文推断出来的。
- 上下文 在一个方法入参中加入lambda表达式,那么lambda表达式的类型可以通过该方法定义时多指定的函数式接口来确定(这个函数式接口为该表达式的目标类型),或者有一个变量接收一个lambda表达式,那么表达式的类型由那个变量确定。
检查顺序
- 首先你需要找到该方法的声明。
- 第二步,确定书写lambda表达式在该方法上的参数位置
- 第三步,确认该参数位置是否声明的是函数式接口
- 第四步,确定该函数式接口的抽象方法(函数描述符),接受一个什么值(或者为空),返回一个什么值。
- 第五步,该方法的实际参数,即声明为函数式接口的参数的书写必须符合该函数式接口的定义。
以上就是检查顺序,有赋值上下文、方法调用上下文、类型转换上下文这三种方式可以获取目标类型。
使用局部变量
**自由局部变量**:不是函数式接口抽象方法参数,而是在外层作用域中定义的变量,它们被称为 **捕获lambda**,lambda没有限制捕获实例变量(我认为是在lambda表达式内创建的新的实例,而不是一个可变的引用)和静态变量。但是局部变量必须显示声明为final或事实上是final。原因如下:
- 实例变量都存储在堆中,而局部变量则保存在栈上。
- 访问自由局部变量时相当于访问副本,而不是真实的原始变量
- 如果lambda是在一个线程里使用,另一个分配自由变量的线程将这个变量收回,这个时候lambda再去访问这个自由变量会出问题,便是线程不安全。
所以如果这个自由变量一旦被赋值后便不再变化那么就能保证副本和真实原始变量一样。
其他特性:
- 局部变量是保存在栈上,隐式表示它们仅限于所在的线程里,而堆是可以多线程访问的(多线程共享的)。
闭包: 是一个函数实例(是一种能被调用的对象,它保存了创建它的作用域的信息),且它可以无限制地访问该函数的非本地变量。Java并不能显式的支持闭包,一般是通过非静态内部类支持。
方法引用
方法引用可以被看作仅仅调用特定方法的lambda的一种快捷写法。
基本思想:如果一个lambda代表的只是"直接调用这个方法",而没有其他操作,那最好还是用名称来调用它,而不是去描述如何调用它。显示地指明方法的名称,代码的可读性会更好。
组成:类名::方法名
如何构建方法引用
- 指向静态方法的方法引用(类方法)(有static关键字的方法)。
- 指向任意类型的实例方法的方法引用(无static关键字的方法)。
- 指向现有对象的实例方法的方法引用。(这个现有对象是指已经存在的局部变量(一个对象)或者该lambda所在方法的类对象,lambda调用了该对象的方法)
示例1:
@Test
public void test() {
TestBean testBean = new TestBean();
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
List list1 = list.stream().map(testBean::getDouble).collect(Collectors.toList());
list1.stream().forEach(System.out::println);
}
public class TestBean{
public int getDouble(int a) {
return a*2;
}
}
代码中的testBean便是存在于lambda表达式外的局部变量,该局部变量的类中含有一个实例方法,那testBean::getDouble之前的lambda表达式应该是:(a)-> testBean.getDouble(a),这个a是声明list中的单个对象。
示例2
@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
public class LogbackServiceImplTest {
@Test
public void test() {
List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
List list1 = list.stream().map(this::getDouble2).collect(Collectors.toList());
list1.stream().forEach(System.out::println);
}
public int getDouble2(int a) {
return a*2;
}
}
代码中test方法中使用this::getDouble方法引用,原lambda为(a) -> this.getDouble2(a),引用的是该类中的另外一个实例方法。
接下来看一下lambda表达式到方法引用的转换:
lambda | 方法引用 |
---|---|
(args) -> ClassName.staticMethod(args) | ClassName::staticMethod |
(arg0,rest) -> arg0.instanceMethod(rest) | ClassName::instanceMethod |
(args) -> expr.instanceMethod(args) | expr::instanceMethod |
第二个转换中,arg0是ClassName类型的。
示例为:
List str = Arrays.asList("a","b","A","B");
str.sort((s1,s2)->s1.compareToIgnoreCase(s2));
//向下变成
str.sort(String::compareToIgnoreCase);
复合lambda表达式的有用方法
复合lambda表达式实现的核心是default声明的默认方法实现。
比较复合器
- 逆序
示例:
@Test
public void test() {
List list = Arrays.asList("1", "2", "3", "4", "5");
list.sort(Comparator.reverseOrder());
System.out.println(list.toString());
}
- 比较器链
通过thenComparing实现:
示例:
@Test
public void test() {
List list = Arrays.asList(new TestBean2(5, "e"), new TestBean2(2, "d"), new TestBean2(2, "c"), new TestBean2(4, "b"), new TestBean2(1, "a"));
list.sort(Comparator.comparing(TestBean2::getAge).thenComparing(TestBean2::getName));
list.stream().map(TestBean2::toString).forEach(System.out::println);
}
public class TestBean2{
/**
* 年龄
*/
private int age;
/**
* 姓名
*/
private String name;
public TestBean2(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestBean2{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
如果第一个比较项出现相同值,可添加第二个比较项继续比较,如年龄为2的有两个,那么就继续比较name。
谓词复合
谓词接口包括三个方法:
- negate (非)
- and (与)
- or (或)
示例:
@Test
public void test() {
List list = Arrays.asList(new TestBean2(5, "e"), new TestBean2(2, "d"), new TestBean2(2, "c"), new TestBean2(4, "b"), new TestBean2(1, "a"));
//创建testBean的age大于1的谓词。
Predicate bean1 = bean -> bean.getAge() > 1;
//创建testBean的name不为"d"的谓词
Predicate bean2 = bean01 -> bean01.getName().equals("d");
Predicate negateBean = bean2.negate();
//将两个谓词进行与运算得到最终谓词
Predicate testBean2Predicate = bean1.and(negateBean);
List list1 = list.stream().filter(testBean2Predicate).collect(Collectors.toList());
list1.stream().map(TestBean2::toString).forEach(System.out::println);
}
public class TestBean2{
/**
* 年龄
*/
private int age;
/**
* 姓名
*/
private String name;
public TestBean2(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "TestBean2{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
上面的例子介绍了部分谓词接口方法的使用方式,这和流的声明式编码相吻合,先声明谓词为哪些条件,最终将各个条件谓词汇总为一个放到filter方法中,在stream中看起来比较简洁,和显式编码比起来我是没有看出来有太明显的优势,只能说各有千秋吧。注意以上接口皆返回Predicate接口,用于结果为Boolean的地方。
函数复合
- andThen
- compose
以上两个方法是将Function接口所代表的lambda表达式复合起来,它们都会返回Function的一个实例。
示例:
分别有两个函数:
- Function
f = x -> x + 1; - Function
g = x -> x * 2;
分别进行如下计算:
a. Function
b. Function
a 数学上会写作g(f(x)),意思就是,f函数所得到的结果会作为g函数的参数。相当于函数流水线,一个函数运行完,放到另一个函数运行。
b 数学上表示f(g(x)),compose表示构成的意思,f.compose()表示f的构成,这样理解也还可以吧。。,g的函数运行结果会作为f的参数。