附上原文档
Lambda表达式
使用形式
// 方式1:无参,内容一行代码
Runnable helloWorld = () -> System.out.println("hello world");
helloWorld.run();
// 方式2:有一个参数,一行代码
LineHandler lineHandler = x -> System.out.println(x);
lineHandler.handle("hello world 2");
// 方式3: 多行代码
Runnable hello = () -> {
System.out.println("hello");
System.out.println("world 3");
};
hello.run();
// 方式4: 多参
IntBinaryOperator valueProvider = (x, y) -> x + y;
BinaryOperator<Long> add = (x, y) -> x + y;
System.out.println(valueProvider.applyAsInt(1, 3));
System.out.println(add.apply(123L, 1000L));
// 方式5:显示声明参数类型
BiFunction<Integer, Integer, Integer> supplier2 = (Integer x, Integer y) -> x + y;
BinaryOperator<Integer> integerSupplier2 = (Integer x, Integer y) -> x + y;
System.out.println(supplier2.apply(1, 2));
System.out.println(integerSupplier2.apply(1, 1));
其中对于函数的返回类型,例如Runnable 、BiFunction、BinaryOperator等。这些类型我主要是通过idea的快捷指令 .var 自动生成的。
对于带参数的lambda表达式,可以不显示写参数类型,那是因为javac根据程序的上下文进行了推断,例如:
ArrayList<Integer> list = new ArrayList<>();
list.add(1);list.add(5);
// list 中存的是Intger类型,在filter中写的lambda表达式参数e 未声明类型,直接与1比较大小
List<Integer> collect = list.stream().filter(e -> e > 1).collect(Collectors.toList());
// 将过滤后的list打印出来:5
collect.forEach(System.out::println);
// 此处声明的函数,没有在某个语境中执行,但是通过返回类型IntBinaryOperator 进行了控制,所以能识别出来是对两个
IntBinaryOperator valueProvider = (x, y) -> x + y;
lambda表达式引用的是值,而不是变量
在jdk8中 ,要在lambda表达式中使用已有的变量,虽然没有强制要求这个变量使用final修饰,但是编译时会检查这个变量是否会修改,即还是要求这个值只能是一次赋值,不能进行修改,如果非要使用该变量,只能创建一个本地变量,例如:
int i1 = 1;
// i1++;
VoidFunc0 voidFunc0 = () -> {
System.out.println("hello" + i1);
};
voidFunc0.call();
// i1 = 3;
这样是可以正常运行的,如果放开 i1++ 或者 i1 = 3, 则会编译报错
int i1 = 1;
i1++;
int finalI = i1; // 额外创建了一个变量,在lambda表达式中使用这个新变量,其中这个变量默认名称中也包含了final,在名称中也给出了提示,不让修改
VoidFunc0 voidFunc0 = () -> {
System.out.println("hello" + finalI);
};
voidFunc0.call();
函数接口
函数接口是只有一个抽象方法的接口,用作lambda表达式的类型
以上述的BiFunction接口为例:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t, U u) -> after.apply(apply(t, u));
}
}
使用@FunctionalInterface
注解,接口中只有一个apply抽象方法,额外还有一个default方法。<T, U, R>
, 其中
T :表示第一个参数的类型;
U :表示第二个参数的类型;
R :表示返回值的类型。
接口名称中的Bi可以理解为 Binary 两个的意思。
如果自己写函数接口,定义一个接口类,接口使用@FunctionalInterface
注解修饰,只有一个抽象方法,例如:
@FunctionalInterface
public interface MyInterFace<T> {
void print(T t);
}
// 使用
MyInterFace myInterface = (x) -> {
System.out.println(x);
};
myInterface.print("my interface test");
所以,个人理解这个接口函数就只是决定了参数个数,返回类型,具体的实现方法,在写lambda表达式的时候,自己写具体的逻辑,也就是说java提供的接口函数基本都能覆盖到大多数的使用。
流
Stream流是在函数式编程方式在集合类上进行复杂工具的操作。
- 惰性求值方法: 对流进行操作,但是最终不产生新集合的方法,返回值为Stream
- 及早求值方法:最终会从stream产生值的方法,返回值为其他值或空。
System.out.println("test stream start");
ArrayList<Integer> list = new ArrayList<>();
list.add(1);list.add(5);
list.stream().filter(e -> {
System.out.println("filter print :"+e);
return e >= 1;
}).map( e -> e +1);
System.out.println("------");
list.stream().filter(e -> {
System.out.println("filter print :"+e);
return e >= 1;
}).map( e -> e +1).forEach(System.out::println);
System.out.println("------");
System.out.println(list.stream().filter(e -> {
System.out.println("filter print :"+e);
return e >= 1;
}).count());
System.out.println("test stream end");
结果:
test stream start
------
filter print :1
2
filter print :5
6
------
filter print :1
filter print :5
2
test stream end
只执行filter、map未产生新的集合,使用了惰性求值,未执行print语句;而后面使用foreach和count时,使用了及早求值方法,执行了print语句。
而且第二个例子中,根据打印来看,先后执行的 filter, map, foreach,不是所有元素执行完map后,再执行foreach。
常见的流操作
collect(toList())
由stream里的值生成一个列表,是一个及早求值操作。
List<Integer> collect = list.stream().filter(e -> e > 1).collect(Collectors.toList());
map
如果有一个函数可以将一种类型的值转换为另外一种类型,map操作就可以使用该函数,将一个流中的值转换成一个新的流。
List<Integer> collect = list.stream().map( e -> e +1).collect(Collectors.toList());
对流中的每一个元素执行+1操作。
假设要对元素做一些复杂操作,例如:
List<Integer> collect = list.stream().map(e -> { e = e + 1; e = e/2; return e; }).collect(Collectors.toList();
在map中的lambda表达式中将最后的结果 return。
filter
遍历数据并检查其中的元素时,使用filter对元素进行过滤,保留满足条件为true的元素
上面写了几个filter的例子,此处不再重复
或者直接传入一个Predicate的接口函数
Predicate<Integer> tFilter = (x) -> {
return x > 1;
};
List<Integer> list1 = list.stream().filter(tFilter).collect(Collectors.toList());
上述方法类似,可以传入一个符合使用情形的Function。
flatMap
flatMap方法可用Stream替换值,然后将多个stream连接成stream
例如:将多个list合并成一个list:
ArrayList<String> stringList1 = new ArrayList<>();
ArrayList<String> stringList2 = new ArrayList<>();
stringList1.add("a");
stringList1.add("b");
stringList2.add("aa");
stringList2.add("bb");
List<String> flatMapResultList = Stream.of(stringList1, stringList2).flatMap(e -> {
System.out.println(e instanceof ArrayList);
return e.stream();
}).collect(Collectors.toList());
flatMapResultList.forEach(System.out::println);
此处借助Stream.of()创建一个stream流,流中的元素为两个list,在flatMap中的表达式中执行System.out.println(e instanceof ArrayList);
时,打印true,总共打印两次。
flatMap使用与map差不多,都是对元素进行操作,只不过flatMap中返回类型必须是stream流。最终就是做完一系列操作后,合并在一起的集合。
max 和min
查找流中的最大值或最小值,首先要考虑的是使用什么排序指标。所以使用max(), min()方法时,需要传入的就是一个比较器。
System.out.println("min length:"+ stringList2.stream().min(Comparator.comparing(e -> e.length())).get());
System.out.println("max length:"+ stringList2.stream().max(Comparator.comparing(e -> e.length())).get());
System.out.println("min length:"+ stringList2.stream().min((e1, e2) -> e1.length() - e2.length()).get());
System.out.println("min length:"+ stringList2.stream().min(Comparator.comparingInt(String::length)).get());
java8 提供了一个新的静态方法comparing
,使用它可以很方便实现一个比较器,以前的方法就是,要比较两个对象的某个属性的值:(e1, e2) -> e1.length() - e2.length()
, idea提示可以修改为:Comparator.comparingInt(String::length)
,这种就是提供一个取值的方法即可。
看一下comparing源码:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
内部实现还是使用了两个对象进行比较。
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}
comparingInt也是使用两个对象进行比较,只不过明确了是对int值进行比较。而comparing
中未明确使用什么类型的值进行比较,看源码是使用apply的返回值进行比较,<T, U extends Comparable<? super U>>
,要求返回类型继承了Comparable,即要求function的返回值类型可以用于比较的。
通用模式
reduce
实现从一组值中生成一个值。
以累加器来举例:
ArrayList<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
System.out.println(list.stream().reduce((x, y) -> {
System.out.println(x);
return x + y;
}).get());
System.out.println(list.stream()
.reduce(4, (x, y) -> {
System.out.println(x);
return x + y;
}));
Optional<T> reduce(BinaryOperator<T> accumulator);
reduce方法接受的是一个包含两个参数的lambda表达式,第一个参数作为每次计算的result,第二参数为遍历的流中的元素。
整合操作
举例:
问题:找出某张专辑上所有乐队的国籍。
解决步骤:
1、找到专辑上所有的表演者
2、分辨出哪些是乐队
3、找出每个乐队的国籍
4、将找出的国籍加入一个集合
对应的API:
1、album.getMusicians()
2、filter
3、map
4、collect
其中值得注意的是,album.getMusicians()返回的是一个流, 而不是一个list集合,这样后续的操作也不会对原集合产生影响,也能拿到数据。
通过Stream暴露集合的最大优点在于, 它很好地封装了内部实现的数据结构。仅暴露一个Stream接口,用户在实际操作中无论如何使用,都不会影响内部的List或Set。
高阶函数
高阶函数是指接受另外一个函数作为参数,或返回一个函数的函数。
例如,map函数
public<U> Optional<U> map(Function<? super T, ? extends U> mapper)
正确使用Lambda表达式
明确要达成什么转化,而不是说明 如何转化 的另一层含义在于写出的函数没有副作用。(感觉翻译出来,看不懂,个人理解是转化后,要没有副作用)
没有副作用的函数不会该变程序或外界的状态。
类库
基本类型
基本类型有对应的装箱类型。Java的泛型是基于对泛型参数类型的擦除,只有装箱类型才能作为泛型参数,所以只有List<Integer>
没有 List<int>
。
在装箱与拆箱的过程中,需要一些额外的开销,为了减少这些性能开销,Stream类的某些方法对基本类型和装箱类做了区分。
在java8 中,仅对整形、长整型和双浮点数做了特殊处理,因为他们在数值计算中用的最多。
对基本类型做特殊处理的方法在命名上有明确的规范。
如果方法返回类型为基本类型,则在基本类型前加To, 例如ToLongFunction。
如果参数是基本类型,则不加前缀只需要类型名即可, 例如LongFunction。
如果高阶函数使用基本类型,则在操作后加后缀To再加基本类型,如mapToLong。
这些基本类型都有与之对应的Stream,以基本类型名为前缀,如mapToLong()返回的是LongStream, 而这个流是一个特殊处理的流。在这个特殊的流中,map方法的实现方式也不同,他接受一个LongUnaryOperator函数。
IntSummaryStatistics statistics = Stream.of(1).mapToInt(x -> x + 1).summaryStatistics();
System.out.println(statistics.getCount());
System.out.println(statistics.getMax());
statistics.accept(2);
System.out.println(statistics.getCount());
Optional
Optional是为核心类库新设计的一个数据类型,用来替换null值。
Optional对象相当于值的容器,而该值可以通过get方法提取。
使用静态方法of(T) 来创建一个Optional对象,也可以通过empty方法创建一个空对象,ofNullable方法可以将一个null转化为Optional对象。通过isPresent反复来判断对象中是否有值。
当对象为空时,orElse反复提供一个备选值。当备选值计算比较复杂时,可以通过orElseGet方法,传参为Supplier对象(Lambda表达式),当对象真正为空时才会调用。
高级集合类和收集器
方法引用
在lambda表达式中获取对象的某个属性时,经常会写:artist -> artist.getName()
,java8中提供了一个简洁写法,称为方法引用。如:Artist::getName
标准语法为:Classname::methodName
。
构造函数也有同样的缩写:
原:(name, age) -> new User(name, age)
缩写:User::new
还能用这种方式创建数组:String[] ::new
元素顺序
直观上看,流是有序的:流中元素是按照顺序处理的,称为 出现顺序。
当使用sorted方法时会产生顺序。
使用并行流时,forEach方法不能保证元素时按顺序处理的,如果需要保证按顺序处理,应该使用forEachOrdered方法。
使用收集器
前面使用过collect(Collectors.toList())
,生成List。
转化成其他集合
当调用toList或toSet方法时,不需要指定具体的类型。Stream类库在背后自动为你挑选出合适的类型。
当你希望使用一个特定的集合收集值时,可以指定集合的类型。例如可能希望使用TreeSet而不是Set。此时可以使用toCollection,它接受一个函数作为参数,来创建集合。
collect(Collectors.toCollection(TreeSet::new))
转换成值
Collectors的一些其他收集器:
collect(Collectors.averagingInt(x -> x))
; 计算平均值
collect(Collectors.maxBy(Comparator.comparing(x -> x)))
但是该方法,idea提示可以修改为:max(Comparator.comparing(x -> x))
,求最大值
数据分块
collect(Collectors.partitioningBy(x -> x >= 2))
将流分为两块,返回类型为:Map<Boolean, List<Integer>>
返回结果打印出来为:{false=[1], true=[4, 5, 3]}
该收集器只能根据true和false将数据分为2块。
数据分组
collect(Collectors.groupingBy(x -> x % 3))
;
根据lambda表达式的结果分组
返回类型也是map,Map<Integer, List<Integer>>
返回结果打印:{0=[3, 6], 1=[4, 1], 2=[5, 2]}
字符串
例如,需要将一个集合中符合某些条件的元素使用,
拼接成字符串。
可能首先想到的是使用for循环遍历,使用StringBuilder
进行拼接。
也可以使用收集器
Collectors.joining(",", "[", "]"))
joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
返回结果打印:[a,b,c]
组合收集器
例如:使用分组后,计算每个分组的数量:collect(Collectors.groupingBy(x -> x % 3, Collectors.counting()))
返回结果打印:{0=2, 1=2, 2=2}
public static <T, K, A, D>
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream) {
return groupingBy(classifier, HashMap::new, downstream);
}
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}
当使用groupingBy收集器时,未指定第二个参数,则value默认生成的类型为List。
更复杂的组合形式,例如:分组后,提取每个分组元素中的某个属性
collect(Collectors.groupingBy(User:: getAge, Collectors.mapping(User::getName, Collectors.toList())))
.
public static <T, U, A, R>
Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
Collector<? super U, A, R> downstream) {
BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
return neshuw CollectorImpl<>(downstream.supplier(),
(r, t) -> downstreamAccumulator.accept(r, mapper.apply(t)),
downstream.combiner(), downstream.finisher(),
downstream.characteristics());
}
感觉可以一直根据第二个参数为Collector类型来套娃。
数据并行化
下面内容只是描述java8中提升性能的技术。
为什么需要并行化,什么时候会带来性能提升。
并行与并发
并发是两个任务共享时间段,并行则是两个任务在同一时间发生。
数据并行化是指将数据分成块,为每块数据分配单独的处理单元。
并行化流操作
例如,计算一组专辑的曲目总长度。
串行:
albums.stream()
.flatMap(Album::getTracks)
.mapToInt(Track::getlength)
.sum();
并行:
albums.parallelStream()
.flatMap(Album::getTracks)
.mapToInt(Track::getLength)
.sum();
调用parallelStream方法即能并行处理。
但是并不是并行化比串行化运行更快,与运行时的环境有关。
性能
影响并行流性能的主要因素有5个:
- 数据大小
将问题分解之后并行化处理,再将结果合并会带来额外的开销。只有数据足够大、每个数据处理管道花费的时间足够多时,并行化处理才有意义。 - 源数据结构
每个管道的操作都基于一些初始数据源,通常是集合。 - 装箱
处理基本类型比处理装箱类型更快 - 核的数量
极端情况下,只有一个核,因此完全没有必要并行化。 - 单元处理开销
花在流中每个元素身上的时间越长,并行操作带来的性能提升越明显。
在底层,并行流沿用了fork/join框架。
根据性能的好坏,将核心类库提供的通用数据结构分成3组:
- 性能好
ArrayList、数组或IntSteam.range,这些结构支持随机读取,也就是说他们轻易的被任意分解。 - 性能一般
HashSet、TreeSet - 性能差
有些数据结构难于分解,比如,可能要花O(N) 的时间复杂度来分解问题,例如 LinkedList。
并行化数组操作
Java8引入了一些针对数组的并行操作,脱离流框架也可以使用lambda表达式。
方法名 | 操作 |
---|---|
parallelPrefix | 任意给定一个函数,计算数组的和 |
parallelSetAlll | 使用Lambda表达式更新数组元素 |
parallelSort | 并行化数组元素排序 |
这些方法与之前的不太一样:它们改变了传入的数组,而没有创建新的数组。 |
例:计算窗口为3的滑动平均数
Integer[] arr = {2, 2, 3, 4, 5, 6 ,7 ,8 ,9 ,10};
Arrays.parallelPrefix(arr, Integer::sum);
Arrays.stream(arr).forEach(System.out::println);
int n = 3;
int start = n-1;
// 使用 Intstream.range得到包含所需元素下标的流
IntStream.range(start, arr.length)
.mapToDouble(i -> {
int pre = i == start ? 0 : arr[i-n] ;
return (arr[i] - pre) * 1.0 / n;
}).forEach(System.out::println);
运行结果:
2
4
7
11
16
22
29
37
46
56
2.3333333333333335
3.0
4.0
5.0
6.0
7.0
8.0
9.0
测试、调试和重构
同样的东西写两遍
Write Everything Twice, WET
不是所有WET的情况都适合Lambda化。如果有一个整体上大概相似的模式,只是行为上有所不同,就可以试着加入一个Lambda表达式。
设计和架构的原则
如何使用Lambda表达式实现solid原则。
lambda表达式改变了设计模式
命令者模式
策略模式
例如,使用gizp、zip两种方法去实现压缩功能。
使用策略模式,可能这样设计:
定义一个接口类,两个实现类:
public interface CompressionStrategy {
OutputStream compress(OutputStream data) throws IOException;
}
public class GzipCompressionStrategy implements CompressionStrategy {
@Override
public OutputStream compress(OutputStream data) throws IOException {
return new GZIPOutputStream(data);
}
}
public class ZipCompressionStrategy implements CompressionStrategy {
@Override
public OutputStream compress(OutputStream data) throws IOException {
return new ZipOutputStream(data);
}
}
两个实现类中方法的实现就是返回一个流。
封装成策略模式:
public class Compressor {
private final CompressionStrategy strategy;
public Compressor(CompressionStrategy strategy) {
this.strategy = strategy;
}
public void compress(Path inFile, File outFile) throws IOException {
try (OutputStream outStream = new FileOutputStream(outFile)) {
Files.copy(inFile, strategy.compress(outStream));
}
}
}
使用时:
public class MainLambda {
public static void main(String[] args) throws IOException {
Path inFile = new File("").toPath();
File outFile = new File("");
Compressor gzipCompressor = new Compressor(new GzipCompressionStrategy());
gzipCompressor.compress(inFile, outFile);
Compressor zipCompressor = new Compressor(new ZipCompressionStrategy());
zipCompressor.compress(inFile, outFile);
}
}
需要给Compressor传入需要的具体压缩方式,最后通过Compressor对象执行压缩方法。
使用lambda化后,可以直接省略接口实现类:
public class MainLambda {
public static void main(String[] args) throws IOException {
Path inFile = new File("").toPath();
File outFile = new File("");
Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);
gzipCompressor.compress(inFile, outFile);
}
}
未理解,但大受震撼。
将代码复制到idea后,确实编译正常,个人理解:如果一个接口只有一个方法,就可以使用lambda表达式。
观察者模式
在观察者模式中,被观察者持有一个观察者列表,当被观察者的状态发生改变时,会通知观察者。观察者模式被大量应用与MVC的GUI工具中,以此上模型状态发生变化时,自动刷新视图模块,达到两者之间的解耦。
public interface LandingObserver {
public void observeLanding(String name);
}
public class Moon {
private final List<LandingObserver> observers = new ArrayList<>();
public void land(String name) {
for (LandingObserver observer : observers) {
observer.observeLanding(name);
}
}
public void startSpying(LandingObserver observer) {
observers.add(observer);
}
}
public class Aliens implements LandingObserver {
@Override
public void observeLanding(String name) {
if (name.contains("Apollo")) {
System.out.println("They're distracted, lets invade earth!");
}
}
}
public class Nasa implements LandingObserver {
@Override
public void observeLanding(String name) {
if (name.contains("Apollo")) {
System.out.println("We made it!");
}
}
}
使用:
public class OberserverTestMain {
public static void main(String[] args) {
Moon moon = new Moon();
moon.startSpying(new Nasa());
moon.startSpying(new Aliens());
moon.land("An asteroid");
moon.land("Apollo 11");
}
}
lambda化后的使用
public class OberserverTestMain {
public static void main(String[] args) {
Moon moon = new Moon();
moon.startSpying(name -> {
if (name.contains("Apollo"))
System.out.println("We made it!");
});
moon.startSpying(name -> {
if (name.contains("Apollo"))
System.out.println("They're distracted, lets invade earth!");
});
moon.land("An asteroid");
moon.land("Apollo 11");
}
}
使用lambda后,两个接口实现类同样也不需要创建了
此处的lambda表达式与上述不同的地方在于,一个接口方法的实现只是一个流的初始化,并返回了;此处接口方法的实现是内部有简单的逻辑,无返回值。这样也是能使用lambda表达式的。
从某种角度来说,将大量代码塞进一个方法会让可读性变差是决定如何使用Lambda表达式的黄金法则。
模板方法模式
模板方法模式是为这些情况设计的:整体算法的设计是一个抽象类,他有一系列抽象方法,代表算法中可被制定的步骤,同时这个类中包含了一些通用代码。算法的每一个变种由具体的类实现,他们重写了抽象发,提供了相应的实现。
上面的解释有点难懂,根据个人的理解:当要实现一个功能时,假设你将实现步骤划分为了3步,其中第二步,根据不同的情况,可能有两个实现方式,此时可以将第二步设置为抽象方法,具体的实现由实现类来完成。与此同时,如果第1步的实现,在不同的实现类中可能会有变化,在实现类中也可以进行重写。
举例:申请贷款:定义一个抽象类:包含需要实现的方法(对应步骤)。而根据不同的申请人,有不同的实现类。
public abstract class LoanApplication {
public void checkLoanApplication() {
checkIdentity();
checkCreditHistory();
checkIncomeHistory();
reportFindings();
}
protected abstract void checkIdentity() ;
protected abstract void checkIncomeHistory() ;
protected abstract void checkCreditHistory() ;
private void reportFindings() {
}
}
// todo
// 定义不同的实现类,实现抽象方法
使用Lambda表达式和方法引用,进行修改:
public class LoanApplication {
private fnal Criteria identity;
private fnal Criteria creditHistory;
private fnal Criteria incomeHistory;
public LoanApplication(Criteria identity, Criteria creditHistory, Criteria incomeHistory) {
this.identity = identity;
this.creditHistory = creditHistory;
this.incomeHistory = incomeHistory;
}
public void checkLoanApplication() throws ApplicationDenied {
identity.check();
creditHistory.check();
incomeHistory.check();
reportFindings();
}
private void reportFindings() {
对LoanApplication 进行了修改,没有使用一系列的抽象方法,而多了一些属性。每个属性都实现类函数接口Criteria,该接口检查一项标准。
public interface Criteria {
public void check() throws ApplicationDenied;
}
所以现在具体的实现取决于属性的具体实现,所以可以在初始化时,可以传入一个lambda表达式。
文档中写的是:
public class CompanyLoanApplication extends LoanApplication {
public CompanyLoanApplication(Company company) {
super(company::checkIdentity,
company::checkHistoricalDebt,
company::checkProfitAndLoss);
}
}
super中传入的是方法引用,其中company也是一个接口, checkIdentity()等是接口中的方法。
个人理解:由于前面的改造,都是接口中只有一个方法,而此处抽象类中有多个抽象方法,将抽象方法对应的改成一个接口对象,在原抽象方法中,调用接口对象的方法,此时就可以对这个接口进行lambda化了
使用Lambda表达式的领域专用语言
领域专用语言:DSL
直接看这一节,可能开始有点看不懂是在干什么,先看了结论:可以用于设计一个框架,实现代码自动补全功能。并且建议手动创建一个项目,亲自体验。
// todo 手动体验
如果你想深入理解领域专用语言,包括内部领域专用语言和外部领域专用语言,推荐大家
阅读 Martin Fowler 和 Rebecca Parsons 合著的 Domain-Specifc Languages(Addison-Wesley
出版社出版)一书。
使用Lambda表达式的SOLID原则
SOLID: Single responsibility、Open/closed、Liskov substitution、Interface segregation、Dependency inversion。
单一功能原则
程序中的类或方法只能有一个改变的理由。
Lambda表达式在方法级别能更容易实现单一功能原则。
举例:计算一定范围内的质数个数
可能会想到for循环遍历所有数据,然后判断每一个数是不是质数, 若是则结果+1.
所以此处干了两件事:判断是否是质数、计数。
进行重构,对于判断是否是质数单独作为一个方法,返回true/false。
将循环遍历数字,这个迭代过程也应该封装起来。如果遍历的范围很大,我们还可以进行并行操作。
- 使用集合流重构质数计数
public long countPrimes(int upTo) {
return IntStream.range(1, upTo)
.filter(this::isPrime)
.count();
}
public boolean isPrime(int number) {
// System.out.println(number);
return IntStream.range(2, number)
.allMatch(x -> (number % x) != 0);
}
注意,range的两个参数,不会取到第二个参数值:
range(int startInclusive, int endExclusive)
- 并行运行基于集合流的质数计数
public long countPrimes(int upTo) {
return IntStream.range(1, upTo)
.parallel() // 唯一改动的地方
.filter(this::isPrime)
.count();
}
开闭原则
软件应该对扩展开发,对修改闭合。
开闭原则保证已有的类在不修改内部实现的基础上可扩展,避免一处改动影响整个代码。
举例:ThreadLocal类。ThreadLocal有一个特殊的变量,每个线程都有一个该变量的副本与之交互。该类的静态方法withInitial 是一个高阶函数,传入一个负责生成初始值的Lambda表达式。
这符合开闭原则,因为不需要修改ThreadLocal类,就能得到新的行为。
ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(SimpleDateFormat::new);
SimpleDateFormat simpleDateFormat = threadLocal.get()
举例:MetricDataGraph 类用于更新系统中的各个指标,例如:用户花在用户空间、内核空间、输入输出上的时间。
MetricDataGraph 类的公开 API
class MetricDataGraph {
public void updateUserTime(int value);
public void updateSystemTime(int value);
public void updateIoTime(int value);
}
这样的设计意味着每次想添加新的指标,都要修改MetricDataGraph 类。
通过引入抽象可以解决这个问题
class MetricDataGraph {
public void addTimeSeries(TimeSeries values);
}
TimeSeries 为各个时间指标。每个具体指标可以实现TimeSeries 接口,在需要时能直接插入。
依赖反转原则
抽象不应该依赖细节,细节应该依赖抽象。
依赖反转原则的目的是让程序员脱离底层粘合代码,编写上层业务逻辑代码。这就让上层代码依赖于底层细节的抽象,从而可以重用上层代码。
举例:以电子卡片作为输入,使用某种存储机制编写地址簿。
将代码分为3个模块:
- 电子卡片阅读器:解析电子卡片
- 地址簿存储模块:将地址存为文本文件
- 编写模块:从电子卡片中获取有效信息, 并将其写入地址簿
为了具备能在系统中替换组件的灵活性,必须保证编写模块不依赖阅读器或存储模块的实现细节。例如:阅读器要改为解析Twitter账号,存储模块要改为数据库存储。
因此我们引入对阅读信息和输出i西南西的抽象,编写模块的实现以来这种抽象。在运行时传入具体的实现细节,这就是依赖反转原则的工作原理。
个人理解:将底层的实现进行抽象化,上层业务(即此处的编写模块)依赖抽象,实现上层代码的重用(即底层的具体实现改变了,上层代码也不需要改)。
举例:提取文件中的标题,其中标题以冒号(:)结尾。
public List<String> findHeadings(Reader input) throws IOException {
try (BufferedReader reader = new BufferedReader(input)) {
return reader.lines()
.filter(line -> line.endsWith(":"))
.map(line -> line.substring(0, line.length() -1))
.collect(Collectors.toList());
}
}
这串代码中,将文件处理与标题解析混在一起。
我们想要的是提取标题的代码,将文件处理的细节交给另一个方法。可以使用Stream<String>
作为抽象,让代码依赖它,而不是文件。stream 对象更安全,而且不容易被滥用。
重构:
public List<String> findHeadings(Reader input) throws IOException {
return withLinesOf(input, lines -> lines
.filter(line -> line.endsWith(":"))
.map(line -> line.substring(line.length() -1))
.collect(Collectors.toList()), RuntimeException::new);
}
private <T> T withLinesOf(Reader input, Function<Stream<String>, T> handler,
Function<IOException, RuntimeException> error) {
try (final BufferedReader reader = new BufferedReader(input)){
return handler.apply(reader.lines());
} catch (IOException e) {
throw error.apply(e);
}
}
总结下来,高阶函数提供了反转控制,这就是依赖反转的一种形式,可以很容易地和Lambda表达式一起使用。依赖反转原则另外值得注意的一点是待依赖的抽象不必是接口。
使用Lambda表达式编写并发程序
Future
调用Future对象的get方法获取值,它会阻塞当前线程,直到返回值。
@Override
public Album lookupByName(String albumName) {
Future<Credentials> trackLogin = loginTo("track"); // 1
Future<Credentials> artistLogin = loginTo("artist");
try {
Future<List<Track>> tracks = lookupTracks(albumName, trackLogin.get()); // 2
Future<List<Artist>> artists = lookupArtists(albumName, artistLogin.get());
return new Album(albumName, tracks.get(), artists.get()); // 3
}catch (InterruptedException | ExecutionException e) {
throw new AlbumLookupException(e.getCause()); // 4
}
}
在1 处的登录结果,在2处执行get(),需要等到登录结果,拿到结果后,再执行代码2.
此时就会发现代码不是平行执行了,而是串行执行。
我们真正需要的是 不必调用get 方法阻塞当前线程,就能操作Future对象返回的结果。我们需要将Future和回调结合起来使用。
CompletableFuture
上述问题的解决直到就是CompleteFuture, 它结合了 Futur对象和使用回调处理。
public Album lookupByName(String albumName) {
CompletableFuture<List<Artist>> artistLookup
= loginTo("artist")
.thenCompose(artistLogin -> lookupArtists(albumName, artistLogin)); // 1
return loginTo("track")
.thenCompose(trackLogin -> lookupTracks(albumName, trackLogin)) // 2
.thenCombine(artistLookup, (tracks, artists) // 3
-> new Album(albumName, tracks, artists))
.join(); // 4
}
loginTo、lookupArtists 和 lookupTracks 方法均返回 CompletableFuture ,而不是 Future。
原loginTo方法返回的是Future<Credentials>
,此处代码1 中, 使用thenCompose方法将Credentials 对象转化成包含艺术家信息的CompletableFuture 对象。
代码3处引入了新方法:thenCombine, 该方法将一个 CompletableFuture 对象的结果和另外一个 CompletableFuture 对象组合起来。组合曹祖是由用户提供的Lambda表达式完成。
代码4处的join方法的作用与get方法是一样的, 而且它没有使用get方法时的检查异常。
创建CompletableFuture 对象分两部分:创建对象和传给它欠客户代码的值。
有一个工厂方法supplyAsync,用来创建CompletableFuture实例,如:
public static ThreadPoolExecutor executors = new ThreadPoolExecutor(4, 10, 1, TimeUnit.MINUTES, null);
CompletableFuture<String> lookupTrack(String id) {
return CompletableFuture.supplyAsync( () -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return id;
}, executors);
}
上述指定了一个线程池,告诉 CompletableFuture 对象在哪里执行任务。 如果没有提供 Executor, 就会使用相同的 fork/join 线程池并行执行。
当任务执行过程中抛出异常, CompletableFuture 为此提供了 completeExceptionally,用于处理异常情况。该方法可以视为 complete方法的备选项,但不能同时调用 complete 和 completeExceptionally 方法。
completableFuture.completeExceptionally(new Exception("error id :" + id));
此处不详细介绍 CompletableFuture 的用法,简单看下用例:
- 如果想在链的末端执行一些代码而不返回任何值,比如 Consumer 和Runnable, 就框框 thenAccept 和thenRun方法。
- 可使用 thenApply 方法转换 CompletableFuture 对象的值,有点像使用Stream 的map 方法。
- 在 CompletableFuture 对象出现异常时, 可使用 exceptionally 方法恢复,可以将一个函数注册到该方法,返回一个替代值,
- 如果想有一个map, 包含异常情况和正常情况,请使用handle方法。
- 要找出 CompletableFuture 到底出了什么问题,可使用isDone 和 isCompletedExceptionally 方法辅助调查。
响应式编程
CompletableFuture 背后的概念可以从单一的返回值推广到数据流,这就是响应式编程。响应式编程其实是一种声明式编程方法,它让程序员以自动流动的变化和数据流来编程。
一是由于是看的翻译版的,有些话有点难懂,不过对于我自己来说,学习新的东西就是没那么好懂的
二是,看的很粗糙,感觉自己总是有点赶进度的意思,很多方法也没有自己写代码去测试,看了也容易忘记。慢慢看吧,还是要再看看。