Java8函数式编程阅读

附上原文档

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 背后的概念可以从单一的返回值推广到数据流,这就是响应式编程。响应式编程其实是一种声明式编程方法,它让程序员以自动流动的变化和数据流来编程。


一是由于是看的翻译版的,有些话有点难懂,不过对于我自己来说,学习新的东西就是没那么好懂的
二是,看的很粗糙,感觉自己总是有点赶进度的意思,很多方法也没有自己写代码去测试,看了也容易忘记。慢慢看吧,还是要再看看。

  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值