函数式编程

阅读: 评论:0

函数式编程

函数式编程

函数式编程

什么是函数式编程? 在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。

//Lambda表达式
button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) {System.out.println("button clicked");}
});
=>
button.addActionListener(event -> System.out.println("button clicked"));1 传入一个实现某接口的对象不同,我们传入了一段代码块——一个没有名字的函数。 event是参数名,和上面匿名内部类示例中的是同一个参数。-> 将参数和 Lambda 表达式 的主体分开,而主体是用户点击按钮时会运行的一些代码;
2 使用匿名内部类时需要显式地声明参数类型ActionEvent event,而在Lambda表达式中无需指定类型;

1 Lambda表达式种类:

1:Runnable noArguments = () -> System.out.println("Hello World");  2:ActionListener oneArgument = event -> System.out.println("button clicked");3:Runnable multiStatement = () -> {  System.out.print("Hello");System.out.println(" World");};4:BinaryOperator<Long> add = (x, y) -> x + y;5:BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;  
  1. 不包含参数,使用空括号 () 表示没有参数;
  2. Lambda 表达式包含且只包含一个参数,可省略参数的括号;
  3. Lambda 表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号 ({})将代码块括起来;
  4. Lambda 表达式也可以表示包含多个参数的方法;
  5. Lambda 表达式中的参数类型都是由编译器推断得出的。这当然不错, 但有时最好也可以显式声明参数类型;
  • 匿名内部类中使用 final 局部变量,换句话说,Lambda 表达式引用的是值, 而不是变量。
函数接口
例2-8 ActionListener 接口:接受ActionEvent 类型的参数,返回空
public interface ActionListener extends EventListener {
public void actionPerformed(ActionEvent event);
}
ActionListener 只有一个抽象方法:actionPerformed,被用来表示行为:接受一个参数,
返回空。记住,由于actionPerformed 定义在一个接口里,因此abstract 关键字不是必需
的。该接口也继承自一个不具有任何方法的父接口:EventListener。这就是函数接口,接口中单一方法的命名并不重要,只要方法签名和Lambda 表达式的类
型匹配即可。可在函数接口中为参数起一个有意义的名字,增加代码易读性,便于更透彻
地理解参数的用途。接口 参数 返回类型 示例
Predicate<T> T boolean 这张唱片已经发行了吗
Consumer<T> T void 输出一个值
Function<T,R> T R 获得Artist 对象的名字
Supplier<T> None T 工厂方法
UnaryOperator<T> T T 逻辑非( !)
BinaryOperator<T> (T, T) T 求两个数的乘积( *)
  • 函数接口是只有一个抽象方法的接口,用作 Lambda 表达式的类型;
  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda 表达式的常见结构:BinaryOperator add = (x, y) → x + y。
  • 函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型

2 流

allArtists.stream().filter(artist -> artist.isFrom("London"));
  • 这行代码并未做什么实际性的工作,filter只刻画出了 Stream,但没有产生新的集合。
  • 像 filter 这样只描述 Stream,最终不产生新集合的方法叫作惰性求值方法;
  • 而像 count 这样 最终会从 Stream 产生值的方法叫作及早求值方法;
  • 判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是 Stream, 那么是惰性求值; 如果返回值是另一个值或为空,那么就是及早求值
  • Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、peek、 skip、 parallel、 sequential、 unordered
  • Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、count、iterator
  • Short-circuiting: anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
  • “为什么要区分惰性求值和及早求值?”只有在对需要什么样的结果和操作有了更多了解之后,才能更有效率地进行计算。

创建流

  • of:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("A");
  • empty:empty方法返回一个空的顺序Stream,该Stream里面不包含元素项。
  • Collection接口和数组的默认方法:
  • Arrays.stream(ids);

常用的流操作

  1. collect(toList()):collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作。

  2. map:如果有一个函数可以将一种类型的值转换成另外一种类型,map 操作就可以 使用该函数,将一个流中的值转换成一个新的流;

List<String> collected = Stream.of("a", "b", "hello")
.map(string -> UpperCase()) 
.collect(toList());传给map的Lambda 表达式只接受一个String 类型的参数,返回一个新的String。
Function 接口是只包含一个参数的普通函数接口: T->Function->R
  1. filter: 遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter,该模式的核心思想是保留Stream中的一些元素,而过滤掉其他的。
List<String> beginningWithNumbers = Stream.of("a", "1abc", "abc1").filter(value -> isDigit(value.charAt(0))).collect(toList());Function 接口是Predicate : T->Predicate-> boolean      
  1. flatMap: flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4)).flatMap(numbers -> numbers.stream()).collect(toList());
assertEquals(asList(1, 2, 3, 4), together);flatMap 方法的相关函数接口和map 方法的一样,都是Function 接口,只是方法的返回值
限定为Stream 类型罢了
  1. max和min
List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream().min(Comparatorparing(track -> Length())).get();
(1), shortestTrack);需要传给它一个Comparator 对象。Java 8 提
供了一个新的静态方法comparing,使用它可以方便地实现一个比较器;
comparing 方法是值得的。实际上这个方法接受一个函数并返回另一个函数;
  1. reduce 操作可以实现从一组值中生成一个值。在上述例子中用到的 count、min和max方 法,因为常用而被纳入标准库中。事实上,这些方法都是 reduce 操作
int count = Stream.of(1, 2, 3).reduce(0, (acc, element) -> acc + element);
assertEquals(6, count);reducer
的类型是第2 章已介绍过的BinaryOperator
  1. 其他
contact:
at(Stream.of(1, 2, 3), Stream.of(4, 5)).forEach(integer -> System.out.print(integer + "  "));distinct:
Stream.of(1,2,3,1,2,3).distinct().forEach(System.out::println);peek:peek方法生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数,并且消费函数优先执行
Stream.of(1, 2, 3, 4, 5).peek(integer -> System.out.println("accept:" + integer)).forEach(System.out::println);skip:skip方法将过滤掉原Stream中的前N个元素,返回剩下的元素所组成的新Stream。如果原Stream的元素个数大于N,将返回原Stream的后(原Stream长度-N)个元素所组成的新Stream;如果原Stream的元素个数小于或等于N,将返回一个空Stream。
Stream.of(1, 2, 3,4,5) 
.skip(2) 
.forEach(System.out::println); 
// 打印结果 
// 3,4,5sorted:sorted方法将对原Stream进行排序,返回一个有序列的新Stream。sorterd有两种变体sorted(),sorted(Comparator),前者将默认使用Object.equals(Object)进行排序,而后者接受一个自定义排序规则函数(Comparator),可按照意愿排序。Stream.of(5, 4, 3, 2, 1).sorted().forEach(System.out::println);// 打印结果// 1,2,3,4,5Stream.of(1, 2, 3, 4, 5).sorted().forEach(System.out::println);// 打印结果// 5, 4, 3, 2, 1count
count方法将返回Stream中元素的个数。allMatch
allMatch操作用于判断Stream中的元素是否全部满足指定条件。如果全部满足条件返回true,否则返回false。anyMatch
anyMatch操作用于判断Stream中的是否有满足指定条件的元素。如果最少有一个满足条件返回true,否则返回false。findAny
findAny操作用于获取含有Stream中的某个元素的Optional,如果Stream为空,则返回一个空的Optional。findFirst
findFirst操作用于获取含有Stream中的第一个元素的Optional,如果Stream为空,则返回一个空的Optional。若Stream并未排序,可能返回含有Stream中任意元素的Optional。limit
limit方法将截取原Stream,截取后Stream的最大长度不能超过指定值N。如果原Stream的元素个数大于N,将截取原Stream的前N个元素;如果原Stream的元素个数小于或等于N,将截取原Stream中的所有元素。noneMatch
noneMatch方法将判断Stream中的所有元素是否满足指定的条件,如果所有元素都不满足条件,返回true;否则,返回false.    

eg1:

找出某张专辑上所有乐队的国籍。艺术家列表里既有个人,也有 乐队。利用一点领域知识,假定一般乐队名以定冠词 The 开头。当然这不是绝对的,但也 差不多。

  1. 找出专辑上的所有表演者。
  2. 分辨出哪些表演者是乐队。
  3. 找出每个乐队的国籍。
  4. 将找出的国籍放入一个集合。
Set<String> origins = Musicians().filter(artist -> Name().startsWith("The")).map(artist -> Nationality()).collect(toSet());

eg2:重构

public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>(); for(Album album : albums) {for (Track track : TrackList()) {if (Length() > 60) {String name = Name();trackNames.add(name);}} }return trackNames;
}public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>(); albums.stream().forEach(album -> {Tracks().forEach(track -> {if (Length() > 60) {String name = Name();trackNames.add(name);}});});return trackNames; 
}public Set<String> findLongTracks(List<Album> albums) {Set<String> trackNames = new HashSet<>();albums.stream().flatMap(album -> Tracks()).filter(track -> Length() > 60).map(track -> Name()).forEach(name -> trackNames.add(name));
return trackNames; }
  • 内部迭代将更多控制权交给了集合类。

  • 和Iterator类似,Stream是一种内部迭代方式。

  • 将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作。

  • 多次调用流操作:用户也可以选择每一步强制对函数求值,而不是将所有的方法调用链接在一起,但是,最好不要如此操作;

    • 代码可读性差,样板代码太多,隐藏了真正的业务逻辑;
    • 效率差,每一步都要对流及早求值,生成新的集合;
    • 代码充斥一堆垃圾变量,它们只用来保存中间结果,除此之外毫无用处
    • 难于自动并行化处理。

练习;

1 编写一个求和函数, 计算流中所有数之和。例如,int addUp(Stream<Integer> numbers);
public static int addUp(Stream<Integer> numbers){duce(0,(acc,x)->acc+x);
}2编写一个函数,接受艺术家列表作为参数,返回一个字符串列表,其中包含艺术家的姓名和国籍
public static List<string> getNameAndOrigin(List<Aritist> aritists){return aritists.stream().flatMap(x-> Stream.Name(), x.getNation())).List());
}3 编写一个函数,接受专辑列表作为参数,返回一个由最多包含3 首歌曲的专辑组成的列表
public static List<Album> getAlbumsWithMostThreeTracks(List<Album> albums){albums.stream().filter(x-> x.getTracks(),size()<=3)。collect(toList());
}2. 迭代。修改如下代码,将外部迭代转换成内部迭代:
int totalMembers = 0;
for (Artist artist : artists) {Stream<Artist> members = Members();totalMembers += unt();
}artists.stream().flatMap(x-&Members()).count();3 计算一个字符串中小写字母的个数(提示:参阅String 对象的chars 方法)
public static int countLowercaseLetters(string str){return str.chars().filter(x->x.isLowerCase).count();
}4 在一个字符串列表中,找出包含最多小写字母的字符串。对于空列表,返回Optional
<String> 对象
public static Optional<String> mostLowercaseString(List<String> strings) {return strings.stream().max(ComparatorparingInt(StringExercises::countLowercaseLetters));}1. 只用reduce 和Lambda 表达式写出实现Stream 上的map 操作的代码,如果不想返回
Stream,可以返回一个List。
public class MapUsingReduce {public static <I, O> List<O> map(Stream<I> stream, Function<I, O> mapper) {duce(new ArrayList<O>(), (acc, x) -> {// We are copying data from acc to new list instance. It is very inefficient,// but contract duce method requires that accumulator function does// not mutate its arguments.// llect method could be used to implement more efficient mutable reduction,// but this exercise asks to use reduce method.List<O> newAcc = new ArrayList<>(acc);newAcc.add(mapper.apply(x));return newAcc;}, (List<O> left, List<O> right) -> {// We are copying left to new list to avoid mutating it. List<O> newLeft = new ArrayList<>(left);newLeft.addAll(right);return newLeft;});}}2. 只用reduce 和Lambda 表达式写出实现Stream 上的filter 操作的代码,如果不想返回
Stream,可以返回一个List。    
/*** Advanced Exercises Question 2*/
public class FilterUsingReduce {public static <I> List<I> filter(Stream<I> stream, Predicate<I> predicate) {List<I> initial = new ArrayList<>();duce(initial,(List<I> acc, I x) -> {if (st(x)) {// We are copying data from acc to new list instance. It is very inefficient,// but contract duce method requires that accumulator function does// not mutate its arguments.// llect method could be used to implement more efficient mutable reduction,// but this exercise asks to use reduce method explicitly.List<I> newAcc = new ArrayList<>(acc);newAcc.add(x);return newAcc;} else {return acc;}},FilterUsingReduce::combineLists);}private static <I> List<I> combineLists(List<I> left, List<I> right) {// We are copying left to new list to avoid mutating it. List<I> newLeft = new ArrayList<>(left);newLeft.addAll(right);return newLeft;}}

4 类库

//代码中使用Lambda表达式
Logger logger = new Logger(); 
if (logger.isDebugEnabled()) {logger.debug("Look at this: " + expensiveOperation());
}=>
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());
public void debug(Supplier<String> message) { if (isDebugEnabled()) {());}
}
  • int 和 Integer—— 前者是基本类型,后者是装箱类型,由于装箱类型是对象,因此在内存中存在额外开销。将基本类型转换为装箱类型,称为装箱,反之则称为拆箱,两者都需要额外的计算开销。 对于需要大量数值运算的算法来说,装箱和拆箱的计算开销,以及装箱类型占用的额外内存,会明显减缓程序的运行速度。为了减小这些性能开销,Stream 类的某些方法对基本类型和装箱类型做了区分,仅对整型、 长整型和双浮点型做了特殊处理,对基本类型做特殊处理的方法在命名上有明确的规范;

  • 如果方法返回类型为基本类型,则 在基本类型前加 To,如· ToLongFunction。

  • 如果参数是基本类型,则不加前缀只 需类型名即可,如 LongFunction。

  • 如果高阶函数使用基本类型,则在操作后加 后缀 To 再加基本类型,如 mapToLong。

  • 总而言之,Lambda 表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循 如下规则:

    • 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
    • 如果有多个可能的目标类型,由最具体的类型推导得出;
    • 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。

FunctionalInterface

  • 该注释会强制javac 检查一个接口是否符合函数接口的标准
  • 为了提高Stream 对象可操作性而引入的各种新接口,都需要有Lambda 表达式可以实现它。它们存在的意义在于将代码块作为数据打包起来

多重继承

  • 接口允许多重继承,因此有可能碰到两个接口包含签名相同的默认方法的情况。
比如接口 Carriage 和 Jukebox 都有一个默认方法 rock,虽然各有各的用途。类 MusicalCarriage 同时实现了接口 Jukebox。
public class MusicalCarriage implements Carriage, Jukebox { }编译器会报错
public class MusicalCarriage implements Carriage, Jukebox {@Overridepublic String rock() {return k(); }
}
javac 并不明确应该继承哪个接口中的方法,因此编译器会报错:class Musical Carriage
inherits unrelated defaults for rock() from types Carriage and Jukebox。当然,在类中实现rock 方法就能解决这个问题,

三定律

  1. 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义 的方法。
  2. 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法, 那么子类中定义的方法胜出。
  3. 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明 为抽象方法。
  • 接口和抽象类之间还是存在明显的区别。接口允许多重继承,却没有成员变量;抽象类可以继承成员变量,却不能多重继承。

  • Optional

  1. Optional 是为核心类库新设计的一个数据类型,用来替换null 值,人们常常使用 null 值表示值不存在,Optional 对象能更好地表达这个概念,使用null代表值不存在的最大问题在于NullPointerException;
  2. 使用 Optional 对象有两个目的:
    首先,Optional 对象鼓励程序员适时检查 变量是否为空,以避免代码缺陷;
    其次,它将一个类的 API 中可能为空的值文档化,这比 阅读实现代码要简单很多。
  3. 调用get()方法前,先使用 isPresent 检查 Optional 对象是否有值。使用orElse方法则更简洁,当Optional对象为空时,该方法提供了一个备选值
  • 使用为基本类型定制的Lambda表达式和Stream,如IntStream可以显著提升系统性能。
  • 默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀。
  • 在一个值可能为空的建模情况下,使用Optional对象能替代使用null值。

5 高级集合类和收集器

方法引用

artist -> Name()    
Artist::getName  
标准语法为 Classname::methodName
(name, nationality) -> new Artist(name, nationality)     
Artist::new

元素顺序

  • 流是有序的,因为流中的元素都是按顺序处理的。这种顺序称为出现顺序。出现顺序的定义依赖于数据源和对流的操作。
  • 有序集合中创建一个流时,流中的元素就按出现顺序排列;
List<Integer> numbers = asList(1, 2, 3, 4);
List<Integer> sameOrder = numbers.stream().collect(toList());
assertEquals(numbers, sameOrder);
  • 如果集合本身就是无序的,由此生成的流也是无序的
Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream().collect(toList());
// 该断言有时会失败
assertEquals(asList(4, 3, 2, 1), sameOrder);
  • 大多数操作都是在有序流上效率更高,比如 filter、map 和 reduce 等。这会带来一些意想不到的结果,比如使用并行流时,forEach 方法不能保证元素是按顺序处理的。如果需要保证按顺序处理,应该使用forEachOrdered方法。

收集器:collect(toList())

  • 转换成其他集合
toSet 和 toCollection
llect(toCollection(TreeSet::new));
  • 转换成值
//找出成员最多的乐队
public Optional<Artist> biggestGroup(Stream<Artist> artists) { Function<Artist,Long> getCount = artist -> Members().count(); llect(maxBy(comparing(getCount)));
}
  • 数据分块
partitioningBy,它接受一个流,并将其分成两部分;
将艺术家组成的流分成乐队和独唱歌手两部分;
public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) { llect(partitioningBy(artist -> artist.isSolo()));
}
使用方法引用代替 Lambda 表达式:llect(partitioningBy(Artist::isSolo));
  • 数据分组
llect(groupingBy(album -> MainMusician()));
  • 字符串
期望的输出 为:"[George Harrison, John Lennon, Paul McCartney, Ringo Starr, The Beatles]"
String result = artists.stream().map(Artist::getName).collect(Collectors.joining(", ", "[", "]"));
  • 组合收集器
使用收集器计算每个艺术家的专辑数
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) { llect(groupingBy(album -> MainMusician(),counting()));
}
  • 重构和定制收集器

要点回顾

  • 方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName。
  • 收集器可用来计算流的最终值,是reduce方法的模拟。
  • Java 8 提供了收集多种容器类型的方式,同时允许用户自定义收集器。

练习

使用Map 的computeIfAbsent 方法高效计算斐波那契数列。这里的“高效”是指避免将那
些较小的序列重复计算多次。

package com.insightfullogic.java8.answers.chapter5;import java.util.HashMap;
import java.util.Map;public class Fibonacci {private final Map<Integer,Long> cache;public Fibonacci() {cache = new HashMap<>();cache.put(0, 0L);cache.put(1, 1L);}public long fibonacci(int x) {return cacheputeIfAbsent(x, n -> fibonacci(n-1) + fibonacci(n-2));}}

用一个定制的收集器实现upingBy 方法,不需要提供一个下游收集器,只需实现一个最简单的即可。别看JDK 的源码,这是作弊!提示:可从下面这行代码开始:
public class GroupingBy<T, K> implements Collector<T, Map<K, List>, Map<K,List>>

package com.insightfullogic.java8.answers.chapter5;import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;public class GroupingBy<T, K> implements Collector<T, Map<K, List<T>>, Map<K, List<T>>> {private final static Set<Characteristics> characteristics = new HashSet<>();static {characteristics.add(Characteristics.IDENTITY_FINISH);}private final Function<? super T, ? extends K> classifier;public GroupingBy(Function<? super T, ? extends K> classifier) {this.classifier = classifier;}@Overridepublic Supplier<Map<K, List<T>>> supplier() {return HashMap::new;}@Overridepublic BiConsumer<Map<K, List<T>>, T> accumulator() {return (map, element) -> {K key = classifier.apply(element);List<T> elements = mapputeIfAbsent(key, k -> new ArrayList<>());elements.add(element);};}@Overridepublic BinaryOperator<Map<K, List<T>>> combiner() {return (left, right) -> {right.forEach((key, value) -> {(key, value, (leftValue, rightValue) -> {leftValue.addAll(rightValue);return leftValue;});});return left;};}@Overridepublic Function<Map<K, List<T>>, Map<K, List<T>>> finisher() {return map -> map;}@Overridepublic Set<Characteristics> characteristics() {return characteristics;}}

假设一个元素为单词的流,计算每个单词出现的次数。假设输入如下,则返回值为一个形如[John → 3, Paul → 2, George → 1] 的Map:Stream names = Stream.of(“John”, “Paul”, “George”, “John”,
“Paul”, “John”);


import com.amples.chapter1.Artist;import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;import static java.util.Comparatorparing;public class LongestName {private static Comparator<Artist> byNameLength = comparing(artist -> Name().length());public static Artist byReduce(List<Artist> artists) {return artists.stream().reduce((acc, artist) -> {return (byNameLengthpare(acc, artist) >= 0) ? acc : artist;}).orElseThrow(RuntimeException::new);}public static Artist byCollecting(List<Artist> artists) {return artists.stream().collect(Collectors.maxBy(byNameLength)).orElseThrow(RuntimeException::new);}}

6 数据并行化

  • 并行化是指为缩短任务执行时间,将一个任务分解成几部分,然后并行执行。这和顺序执行的任务量是一样的,区别就像用更多的马来拉车,花费的时间自然减少了。实际上,和顺序执行相比,并行化执行任务时,CPU 承载的工作量更大。
  • 数据并行化。数据并行化是指将数据分成块,为每块数据分配单独的处理单元。还是拿马拉车那个例子打比方,就像从车里取出一些货物,放到另一辆车上,两辆马车都沿着同样的路径到达目的地。当需要在大量数据上执行同样的操作时,数据并行化很管用。它将问题分解为可在多块数据上求解的形式,然后对每块数据执行运算,最后将各数据块上得到的结果汇总,从而获得最终答案
  • 任务并行化和数据并行化做比较,在任务并行化中,线程不同,工作各异。

并行化流操作

  • 如果已经有一个 Stream 对象,调用它的 parallel 方法就能让其拥有并行操作的能力。如果想从一个集合类创建一个流,调用 parallelStream 就能立即获得一个拥有并行能力的流。

  • 在一个四核电脑上,如果有10 张专辑,串行化代码的速度是并行化代码速度的8 倍;如果将专辑数量增至100 张,串行化和并行化速度相当;如果将专辑数量增值10 000 张,则并行化代码的速度是串行化代码速度的2.5 倍。

  • 之前调用 reduce 方法,初始值可以为任意值,为了让其在并行化时能工作正常,初值必须 为组合函数的恒等值。拿恒等值和其他值做 reduce 操作时,其他值保持不变。

  • 比如,使用 reduce操作求和,组合函数为(acc, element) -> acc + element,则其初值必须为0,因 为任何数字加 0,值不变。乘法的话初始值就是1;

  • reduce 操作的另一个限制是组合操作必须符合结合律。这意味着只要序列的值不变,组 合操作的顺序不重要。

性能

  • 影响并行流性能的主要因素有 5 个

    • 数据大小:只有数据足够大、每个数据处理管道花费的时间足够多 时,并行化处理才有意义
    • 源数据结构:将不同的数据源分割相对容易, 这里的开销影响了在管道中并行处理数据时到底能带来多少性能上的提升
    • 装箱 处理基本类型比处理装箱类型要快
    • 核的数量
    • 单元处理开销:花在流中 每个元素身上的时间越长,并行操作带来的性能提升越明显
  • 我们可以根据性能的好坏,将核心类库提供的通用数据结构分成以下3 组。

    • 性能好:ArrayList、数组或IntStream.range,这些数据结构支持随机读取,也就是说它们能轻,而易举地被任意分解。
    • 性能一般:HashSet、TreeSet,这些数据结构不易公平地被分解,但是大多数时候分解是可能的。
    • 性能差:有些数据结构难于分解,比如,可能要花O(N) 的时间复杂度来分解问题。其中包括LinkedList,对半分解太难了。还有Streams.iterate 和BufferedReader.lines,它们长度未知,因此很难预测该在哪里分解。
    • 如果能避开有状态,选用无状态操作,就能获得更好的并行性能。无状态操作包括map、filter 和flatMap,有状态操作包括sorted、distinct 和limit。

并行化数组操作

* parallelPrefix,任意给定一个函数,计算数组的和
* parallelSetAll 使用 Lambda 表达式更新数组元素 Arrays.parallelSetAll(values, i -> i);它们改变了传入的数组,而没有创建一个新的数组。
* parallelSort
  • 数据并行化是把工作拆分,同时在多核CPU上执行的方式。
    如果使用流编写代码,可通过调用parallel或者parallelStream方法实现数据并行化操作。
    影响性能的五要素是:数据大小、源数据结构、值是否装箱、可用的CPU核数量,以及处理每个元素所花的时间。
eg1:
range.map(x -> x * x).sum();
range.parallel().map(x->x*x).sum();
  • 修复缺陷:数字相乘,然后再将所得结果乘以 5
return linkedListOfNumbers.stream().reduce(5, (acc, x) -> x * acc);
5 * numbers.parallelStream().reduce(1, (acc, x) -> x * acc);

要点回顾

  • 数据并行化是把• 工作拆分,同时在多核 CPU上执行的方式。
  • 如果使用流编写代码,可通过调用 parallel 或者 parallelStream 方法实现数据并行化操作。
  • 影响性能的五要素是:数据大小、源数据结构、值是否装箱、可用的 CPU 核数量,以及处理每个元素所花的时间。

练习

例6-11 中的代码把列表中的数字相乘,然后再将所得结果乘以5。顺序执行这段程序没有问题,但并行执行时有一个缺陷,使用流并行化执行该段代码,并修复缺陷。

例6-11 把列表中的数字相乘,然后再将所得结果乘以5,该实现有一个缺陷

public static int multiplyThrough(List<Integer> linkedListOfNumbers) {return linkedListOfNumbers.stream().reduce(5, (acc, x) -> x * acc);
}
public static int multiplyThrough(List<Integer> numbers) {return 5 * numbers.parallelStream().reduce(1, (acc, x) -> x * acc);
}

7 测试、调试、重构

  • 重构、测试驱动开发(TDD)和持续集成(CI)

种反模式通过传入代码即数据的方式

Logger logger = new Logger();
if (logger.isDebugEnabled()) {
logger.debug("Look at this: " + expensiveOperation());
}Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());

孤独的覆盖

  • ThreadLocal 能创建一个工厂,为每个线程最多只产生一个值;
  • 可以为工厂方法withInitial 传入一个Supplier 对象的实例来创建对象
ThreadLocal<Album> thisAlbum = new ThreadLocal<Album> () {
@Override protected Album initialValue() {
return database.lookupCurrentAlbum();
}
};ThreadLocal<Album> thisAlbum
= ThreadLocal.withInitial(() -> database.lookupCurrentAlbum());

DRY

public long countRunningTime() {
long count = 0;
for (Album album : albums) {for (Track track : TrackList()) {count += Length();}
}
return count;
}public long countMusicians() {
long count = 0;
for (Album album : albums) {count += MusicianList().size();
}
return count;
}public long countTracks() {
long count = 0;
for (Album album : albums) {count += TrackList().size();
}
return count;
}=》public long countRunningTime() {
return albums.stream().mapToLong(album -> Tracks().mapToLong(track -> Length()).sum()).sum();
}public long countMusicians() {
return albums.stream().mapToLong(album -> Musicians().count()).sum();
}public long countTracks() {
return albums.stream().mapToLong(album -> Tracks().count()).sum();
}=> public long countFeature(ToLongFunction<Album> function) {return albums.stream().mapToLong(function).sum();
}public long countTracks() {return countFeature(album -> Tracks().count());
}public long countRunningTime() {return countFeature(album -> Tracks().mapToLong(track -> Length()).sum());
}public long countMusicians() {return countFeature(album -> Musicians().count());
}

Lambda表达式的单元测试

  • 方法引用。任何Lambda 表达式都能被改写为普通方法,然后使用方法引用直接引用。
public static List<String> elementFirstToUppercase(List<String> words) {return words.stream().map(Testing::firstToUppercase).collect(Collectors.<String>toList());
}public static String firstToUppercase(String value) { 􀁮char firstChar = UpperCase(value.charAt(0));return firstChar + value.substring(1);
}
把处理字符串的的逻辑抽取成一个方法后,就可以测试该方法,把所有的边界情况都覆盖
到。新的测试用例如例7-13 所示。例7-13 测试单独的方法
@Test
public void twoLetterStringConvertedToUppercase() {String input = "ab";String result = Testing.firstToUppercase(input);assertEquals("Ab", result);
}
  • 惰性求值和调试
  • 日志和打印消息:peak 流有一个方法让你能查看每个值,同时能继续操作流。这就是 peek 方法。记录日志这是 peek 方法的用途之一。为了像调试循环那样一步一步跟踪,可在 peek 方法中加入断点,这样就能逐个调试流中的元素了。
  • 此时,peek 方法可知包含一个空的方法体,只要能设置断点就行。有一些调试器不允许在 空的方法体中设置断点,此时,我将值简单地映射为其本身,这样就有地方设置断点了, 虽然这样做不够完美,但只要能工作就行
List<String> s = Arrays.asList("12a3", "2s3", "@b3", "3d2", "6d6");
List<String> strings = s.stream().map(String::toUpperCase).peek(nation -> System.out.println("Found nationality: " + nation)).List());可以断点调试;
  • 重构遗留代码时考虑如何使用Lambda表达式,有一些通用的模式。如果想要对复杂一点的Lambda表达式编写单元测试,将其抽取成一个常规的方法。peek方法能记录中间值,在调试时非常有用。

要点回顾

  • 重构遗留代码时考虑如何使用Lambda 表达式,有一些通用的模式。
  • 如果想要对复杂一点的 Lambda表达式编写单元测试,将其抽取成一个常规的方法。
  • peek 方法能记录中间值,在调试时非常有用。

8 设计和架构的原则

  • Lambda表达式对设计模型的改变;

命令者模式

  • 命令者是一个对象,它封装了调用另一个方法的所有细节,命令者模式使用该对象,可以
    编写出根据运行期条件,顺序调用方法的一般化代码;
  • 命令接收者:实际执行任务
  • 命令者:封装了所有调用命令执行者的信息
  • 发起者:控制一个或多个命令的顺序和执行。
  • 客户端:创建具体的命令者实例
命令接收者
public interface Editor {public void save();public void open();public void close();
}我们需要一个统一的接口来概括这些不同的操作,我将这个接口叫作Action,它代表了一个操作。所有的命令都要实现该接口
public interface Action {public void perform();
}现在让每个操作都实现该接口,这些类要做的只是在Action 接口中调用Editor 类中的一个方法
保存操作代理给Editor 方法public class Save implements Action {private final Editor editor;public Save(Editor editor) {this.editor = editor;}@Overridepublic void perform() {editor.save();}
}public class Macro {private final List<Action> actions;public Macro() {actions = new ArrayList<>();}public void record(Action action) {actions.add(action);}public void run() {actions.forEach(Action::perform);}
}Macro macro = new Macro();
d(new Open(editor));
d(new Save(editor));
d(new Close(editor));
macro.run();使用Lambda 表达式构建宏
Macro macro = new Macro();
d(() -> editor.open());
d(() -> editor.save());
d(() -> editor.close());
macro.run();使用方法引用构建宏
Macro macro = new Macro();
d(editor::open);
d(editor::save);
d(editor::close);
macro.run();在核心Java 中,已经有一个和Action 接口结构一致的函数接口——Runnable。
我们可以在实现上述宏程序中直接使用该接口

策略模式

  • 策略模式能在运行时改变软件的算法行为。如何实现策略模式根据你的情况而定,但其主
    要思想是定义一个通用的问题,使用不同的算法来实现,然后将这些算法都封装在一个统
    一接口的背后。
文件压缩就是一个很好的例子。我们提供给用户各种压缩文件的方式,可以使用zip 算法,也可以使用gzip 算;定义压缩数据的策略接口
public interface CompressionStrategy {public OutputStream compress(OutputStream data) throws IOException;
}有两个类实现了该接口,分别代表gzip 和ZIP 算法
public class GzipCompressionStrategy implements CompressionStrategy {@Overridepublic OutputStream compress(OutputStream data) throws IOException {return new GZIPOutputStream(data);}
}public class ZipCompressionStrategy implements CompressionStrategy {@Overridepublic OutputStream compress(OutputStream data) throws IOException {return new ZipOutputStream(data);}
}Compressor 类,有一个compress方法,读入文件,压缩后输出。它的构造函数有一个CompressionStrategy 参数,调用代码可以在运行期使用该参数决定使用哪种压缩策略;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)) {py(inFile, strategypress(outStream));}}
}Compressor gzipCompressor = new Compressor(new GzipCompressionStrategy());
gzipCompressorpress(inFile, outFile);Compressor zipCompressor = new Compressor(new ZipCompressionStrategy());
zipCompressorpress(inFile, outFile);==》
Compressor gzipCompressor = new Compressor(GZIPOutputStream::new);
gzipCompressorpress(inFile, outFile);Compressor zipCompressor = new Compressor(ZipOutputStream::new);
zipCompressorpress(inFile, outFile);

观察者模式

  • 在观察者模式中,被观察者持有一个观察者列表。当被观察者的状态发生改变,会通知观察者。观察者模式被大量应用于基于MVC 的GUI 工具中,以此让模型状态发生变化时,自动刷新视图模块,达
    到二者之间的解耦。
观察的对象是月球! NASA 和外星人都对登陆
到月球上的东西感兴趣,都希望可以记录这些信息。NASA 希望确保阿波罗号上的航天员
成功登月;外星人则希望在NASA 注意力分散之时进犯地球。定义观察者的API, 这里我将观察者称作LandingObserver。它只有一个observeLanding 方法,当有东西登陆到月球上时会调用该方法
public interface LandingObserver {public void observeLanding(String name);
}被观察者是月球Moon,它持有一组LandingObserver 实例,有东西着陆时会通知这些观察
者,还可以增加新的LandingObserver 实例观测Moon 对象
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);}
}我们有两个具体的类实现了LandingObserver 接口,分别代表外星人和NASA检测着陆情况
public class Aliens implements LandingObserver {@Overridepublic void observeLanding(String name) {if (ains("Apollo")) {System.out.println("They're distracted, lets invade earth!");}}
}public class Nasa implements LandingObserver {@Overridepublic void observeLanding(String name) {if (ains("Apollo")) {System.out.println("We made it!");}}
}Moon moon = new Moon();
moon.startSpying(new Nasa());
moon.startSpying(new Aliens());
moon.land("An asteroid");
moon.land("Apollo 11");=》 Moon moon = new Moon();
moon.startSpying(name -> {if (ains("Apollo"))System.out.println("We made it!");
});
moon.startSpying(name -> {if (ains("Apollo"))System.out.println("They're distracted, lets invade earth!");
});
moon.land("An asteroid");
moon.land("Apollo 11");
  • 将大量代码塞进一个方法会让可读性变差是决定如何使用Lambda 表达式的黄金法则。之所以不在这里过分强调,是因为这也是编写一般方法时的黄金法则!

模板方法模式

  • 模板方法模式是为这些情况设计的:整体算法的设计是一个抽象类,它有一系列抽象方
    法,代表算法中可被定制的步骤,同时这个类中包含了一些通用代码。算法的每一个变种
    由具体的类实现,它们重写了抽象方法,提供了相应的实现。
假设我们是一家银行,需要对公众、公司和职员放贷。放贷程序大体一致——验明身份、信用记录和收入记录;抽象类LoanApplication 来控制算法结构,该类包含一些贷款调查结果报告的通用代码public abstract class LoanApplication {public void checkLoanApplication() throws ApplicationDenied {checkIdentity();checkCreditHistory();checkIncomeHistory();reportFindings();}
protected abstract void checkIdentity() throws ApplicationDenied;
protected abstract void checkIncomeHistory() throws ApplicationDenied;
protected abstract void checkCreditHistory() throws ApplicationDenied;
private void reportFindings(){}
}
根据不同的申请人,有不同的类:CompanyLoanApplication、PersonalLoanApplication 和EmployeeLoanApplication==> 模板方法模式真正要做的是将一组方法调用按一定顺序组织起来。如果用函数接口表
示函数,用Lambda 表达式或者方法引用实现这些接口,相比使用继承构建算法,就会得到
极大的灵活性。让我们看看如何使用这种方式实现LoanApplication 算法
public class LoanApplication {private final Criteria identity;private final Criteria creditHistory;private final Criteria incomeHistory;public LoanApplication(Criteria identity,Criteria creditHistory,Criteria incomeHistory) {	this.identity = ditHistory = creditHistory;this.incomeHistory = incomeHistory;
}public void checkLoanApplication() throws ApplicationDenied {identity.check();creditHistory.check();incomeHistory.check();reportFindings();
}
private void reportFindings() {}这里没有使用一系列的抽象方法, 而是多出一些属性:identity、creditHistory 和incomeHistory。每一个属性都实现了函数接口Criteria,该接口检查一项标准,如果不达标就抛出一个问题域里的异常。我们也可以选择从check 方法返回一个类来表示成功或失败,但是沿用异常更加符合先前的实现public interface Criteria {public void check() throws ApplicationDenied;
}

DSL领域专用语言

使用Lambda表达式的SOLID原则

  • Single responsibility、Open/closed、Liskov substitution、Interface segregation 和 Dependency inversion 指导你开发出易于维护和扩展的代码
单一功能原则
  • 程序中的类或方法只能有一个改变的理由:单一功能原则不止于此:一个类不仅要功能单一,而且还需将功能封装好。换句话说,如果我想改变输出格式,那么只需改动负责输出的类,而不必关心负责制表的类。
开闭原则
  • 软件应该对扩展开放,对修改闭合:
  • “不可变性”一词有两种解释:观测不可变性和实现不可变性。观测不可变性是指在其他对象看来,该类是不可变的;实现不可变性是指对象本身不可变。实现不可变性意味着观测不可变性,反之则不一定成立。
  • 我们说不可变对象实现了开闭原则,是因为它们的内部状态无法改变,可以安全地为其增加新的方法。新增加的方法无法改变对象的内部状态,因此对修改是闭合的;但它们又增加了新的行为,因此对扩展是开放的。
依赖反转原则
  • 抽象不应依赖细节,细节应该依赖抽象
  • 依赖反转原则的目的是让程序员脱离底层粘合代码,编写上层业务逻辑代码,这就让上层代码依赖于底层细节的抽象,从而可以重用上层代码。这种模块化和重用方式是双向的: 既可以替换不同的细节重用上层代码,也可以替换不同的业务逻辑重用细节的实现。

要点回顾

  • Lambda 表达式能让很多现有设计模式更简单、可读性更强,尤其是命令者模式。
  • 在 Java 8 中,创建领域专用语言有更多的灵活性。
  • 在 Java 8 中,有应用 SOLID 原则的新机会。

9 使用Lambda表达式编写并发程序

  • 使用基于Lambda表达式的回调,很容易实现事件驱动架构。
  • CompletableFuture代表了IOU,使用Lambda表达式能方便地组合、合并。
  • Observable继承了CompletableFuture的概念,用来处理数据流。

每章的练习答案:

本文发布于:2024-02-02 11:48:55,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170684573643596.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:函数
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23