Java 8 实战笔记

书没看完, 本文正常情况下一周更新一次
个人认为这本书写的不怎样, 中文翻译过来的版本更别说了, 有些地方理解意思全靠猜, 网上也没有勘误表.英文水平过关的还是找找看Java 8的一些其他文档吧.

Java 8 实战

第一章 为什么关心Java 8

  • 行为参数化: 把方法作为参数传递给另一个方法的能力叫做行为参数化.
  • 并行只有在假定代码的多个副本可以独立工作时才可以进行.如果需要写入的是一个共享变量就不可以了.
  • 这两个要点(没有共享的可变数据, 将方法和函数即代码传递给其他方法的能力)是我们常说的函数式编程范式的基石.
  • 顺序处理和并行处理
// 顺序处理
import static java.util.stream.Collectors.toList;
List heavyApples = inventory.stream().filter((Apple a)->a.getWeight() > 150).collect(toList());

// 并行处理
import static java.util.stream.Collectors.toList;
List heavyApples = inventory.parallelStream().filter((Apple a)->a.getWeight() > 150).collect(toList());
  • 函数式接口 就是只定义了一个抽象方法的接口.
  • Java 8中包含了一个容器对象Optional类, 他是一个容器对象, 可以包含也可以不包含一个值.如果你可以一致的使用它就可以避免NPE.

第三章 Lambda表达式

  • Lambda的基本语法
// 两种方式
(parameters) -> expression; // 表达式

(parameters) -> { statement }; // 语句
  • Java 8在接口声明中使用了新的 default 关键字来扩充接口,同时不会破坏现有的代码.
    在类没有对默认方法进行实现的时候, 其主体方法提供默认实现的方法.
  • 函数描述符 函数式接口的抽象方法的签名基本上就是Lambda表达式的签名. 我们将这种抽象方法叫做函数描述符.
  • 原始类型特化
    • 泛型只能绑定到引用类型.
    • 装箱的本质就是把基本类型包裹起来, 并保存到堆里. 因此装箱后的值需要更多地内存, 并需要额外的内存搜索来获取被包裹的原始值.
      Java 8 为函数式接口带来了一个专门的版本, 以便在输入和输出都是基本类型时候的自动装箱和自动拆箱.
  • Java 8中的常用函数式接口
函数式接口 函数描述符 原始类型特化
Predicate T->boolean IntPredicate, LongPredicate, DoublePredicate
Consumer T->void IntConsumer, LongConsumer, DoubleConsumer
Function T->R IntFunction, IntToDoubleFunction, IntToLongFunction, LongFunction, LongToDoubleFunction, LongToIntFunction, DoubleFunction, ToIntFunction, ToDoubleFunction, ToLongFunction
Supplier () -> T BooleanSupplier, IntSupplier, LongSupplier, DoubleSupplier
UnaryOperator T->T IntUnaryOperator, LongOperator, DoubleOperator
BinaryOperator (T, T)->T IntBinaryOperator, LongBinaryOperator, DoubleBinaryOperator
BiPredicate (L,R)->T
BiConsumer (T, U)->void ObjectIntConsumer, ObjectLongConsumer, ObjectDoubleConsumer
BiFunction (T, U)->R ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction
  • 任何函数式接口都不允许抛出受检异常. 如果需要Lambda表达式来抛出异常的话有两种办法:
    • 定义自己的函数式接口, 并申明受检异常
    • 将Lambda包含到一个try/catch块中
// 定义自己的函数式接口
@FunctionalInterface
public interface BufferedReaderProcessor{
    String process(BufferedReader b) throws IOException ;
}
BufferedReaderProcessor p = (BufferedReader b) -> b.readLine();
// 将 Lambda 包含到try/catch 中
Function f = (BufferedReader b) ->{
    try{
        return b.readLine();
    }
    catch(IOException e){
        throw new RuntimeException(e);
    }
};

3.5 类型检查, 类型推断以及限制

3.5.1 类型检查

   Lambda的类型检查是从使用Lambda的上下文推断出来的. Lambda上下文中需要的类型被称为目标类型.

3.5.2 同样的Lambda, 不同的函数式接口

  有了目标类型的概念, 同一个Lambda表达式就可以与不同的函数式接口联系起来, 只要他们的抽象方法签名能够兼容.例如, 以下的两个赋值是有效的:

Callable c = () -> 42;
PrivilegedAction p = () -> 41;
  • 特殊的void兼容规则
    如果一个Lambda的主体是一个语句表达式, 他就可以和一个返回void的函数描述符兼容(参数列表也要兼容).
// Predicate 返回一个boolean
Predicate p = s -> list.add(s);
// Consumer 返回一个void
Consumer p = s -> list.add(s);
3.5.4 使用局部变量
  • 捕获Lambda
    Lambda表达式可以使用 自由变量 (不是参数, 而是在外层作用域中定义的变量). 他们被称为 捕获Lambda.
    Lambda可以没有限制的捕获实例变量和静态变量, 但是局部变量必须申明为final或者事实上为final. 换句话说, Lambda表达式只能捕获被指派过一次的局部变量.
    • 对局部变量限制的原因:
      • 实例变量都存储在堆中, 局部变量存储在栈上. 如果Lambda在一个线程上可以直接访问局部变量, 可能会在分配该变量的线程将这个变量回收之后再去访问这个变量. 因此, Java在访问自由局部变量的时候访问的是访问他的副本, 而不是原始变量.
      • 这一限制不鼓励使用改变外部变量的典型命令是编程模式.(这种模式很容易阻碍轻易做到的并行)

3.6 方法引用

   方法引用让你可以重复使用现有函数的定义, 并像Lambda一样传递他.

3.6.1 管中窺豹

方法引用可以被看作是仅仅调用特定方法的Lambda的一种快捷写法.当你需要使用方法引用时, 目标引用放在分隔符 :: 前面, 方法名称放到后面.例如: Apple::getWeight

  • 如何构建方法引用
    指向静态方法的引用
    指向任意类型实例方法
    指向现有对象的实例方法的方法引用
    Lambda表达式重构为方法引用, 实例:
// Lambda
(args) -> ClassName. StaticMethod(args);
// 转换为方法引用
ClassName:: StaticMethod

// Lambda
(arg0, test) -> args. instanceMethod(test);
// 转换成方法引用, arg0 的类型是ClassName
ClassName:: instanceMethod

// Lambda
(args) -> expr. instanceMethod(args);
// 转换成方法引用
expr:: instanceMethod

编译器会进行一种Lambda表达式类似的类型检查过程, 来确定对于给定的函数式接口, 这个方法是否有效, 所以需要方法的签名必须和上下文类型匹配.

3.6.2 构造函数引用
// 假设一个构造函数没有参数, 他适合Supplier的签名, 可以写成 
() -> Apple;
// 或者 
Supplier c1 = Apple::new;
Apple a = c1.get();
// 或者
Supplier c2 = () -> new Apple();
Apple a = c2.get();

// 如果构造函数有一个参数, Apple(Integer weight)
Function f1 = Apple::new;
Apple a2 = f1.apply(20);
// 等价于
Function f1 = (weight) -> new Apple(weight);
Apple a2 = f1.apply(20);

3.7 Lambda 和 方法引用实战

第一步: 传递代码
第二步: 使用匿名内部类
第三部: 使用Lambda表达式
第四部: 使用方法引用

// Comparator具有一个叫做comparing的静态辅助方法, 他可以接收一个Function 来提取 Comparable的键值, 并生成一个 Comparator对象.
Comparator c = Comparator.comparing((Apple a) -> a.getWeight());

3.8 复合Lambda表达式的有用方法

// 比较器复合
inventory.sort(comparing(Apple::getWeight));
// 逆序
inventory.sort(comparing(Apple::getWeight)). reversed();
// 比较器链, 两个苹果重量一样时, 进一步使用按照国家排序
inventory.sort(comparing(Apple::getWeight)). reversed().thenComparing(Apple:;getCountry);

// 谓词复合
// 谓词接口包含三个方法: negate, and 和 or
Predicate redApple = (Apple a) -> "red".equals(a.getColor());
// 非红色苹果
Predicate notRedApple = redApple.negate();
// 红色并且重的苹果
Predicate redAndHeavy = readApple.and(a -> a.getWeight() > 150);
// or 操作同 and操作
// !!!! and, or 方式是按照表达式链中的位置, 从左到右确定优先级的. 
// a.or(b).and(c) 可以看做 (a || b) && c

3.8.3 函数复合

// Function 接口配备了andThen 和 compose两个默认方法, 他们都会返回Function的一个实例.
// (a +1 ) * 2
Function f = x -> x+1;
Function g = x -> x*2;
Function h = f. andThen(g);
int result = h.apply(1);

Function f = x -> x+1;
Function g = x -> x*2;
Function h = g. compse(f);
int result = h.apply(1);

3.10 小结

  • Lambda表达式可以理解为一种匿名函数, 他没有名称, 但是有参数列表, 函数主体, 返回值类型, 可能还有一个可以抛出的异常列表.
  • 函数式接口就是仅仅只有一个抽象方法的接口.
  • 为了避免装箱操作, 对Predicate, Function 等通用函数式接口的原始类型特化, IntPredicate, IntToLongFunction等.
  • Lambda表达式要代表的类型被称为 目标类型 .

第四章 引入流

4.1 流是什么

流是Java API的新成员, 他允许你以申明的方式处理数据集合(通过查询语句来表达, 而不是临时写一个实现). 此外流可以透明地并行处理, 你无需写任何多线程代码.
filter, map, sorted, collect 等操作是与具体线程无关的高层次构件, 所以他们的内部可以是单线程的, 也可以透明地利用你的多核架构.

4.2 流简介

流的简短定义就是 从支持数据处理的源生成的元素序列.

  • 元素序列: 就像集合一样, 流也提供了一个接口, 可以访问特定元素类型的一组有序值. 集合讲的是数据, 流讲的是数据.
  • 源: 流会使用一个提供数据的源, 例如集合, 数据, 输入/输出源. 在使用有序集合生成源是会保留元素顺序.
  • 数据处理操作: 流的数据处理功能支持类似于数据库的操作, 以及函数式编程中的常见操作, 例如 filter, map, reduce等. 流操作可以顺序执行也可以并行执行.
  • 流水线: 很多流操作本身会返回一个流, 这样多个操作就可以链接起来, 形成一个流水线. 这让我们的一些优化成为可能, 例如延迟和短路.
  • 内部迭代: 与使用迭代器显示迭代不同, 流的迭代操作是在背后进行的

4.3 流与集合

粗略地说, 集合和流之间的差异就在于什么时候计算. 集合是一个内存中的数据结构, 它包含数据结构中目前所有的值–集合中的每个元素都的先算出来才能添加到集合中. 相比之下, 流则是概念上固定的数据结构, 其元素则是按需计算的.

4.3.1 只能遍历一次

和迭代器类似, 流只能遍历一次.

4.3.2 外部迭代和内部迭代

使用Collection接口需要用户去做迭代就叫做外部迭代. Stream库使用内部迭代–他帮你吧迭代做了, 还把得到的流值存在了某个地方, 你只要给出一个函数说要干什么就可以了.
内部迭代可以透明地并行处理, 或者使用更优化的顺序来进行处理. 外部迭代的优化会比较困难, 需要自己处理并行问题了.

4.4 流操作

可以链接起来的操作是 中间操作 , 关闭流的操作是 终端操作 .

4.4.1 中间操作

诸如filter, map等中间操作会返回另外一个流. 这种让多个操作可以连接起来形成一个查询. 重要的是, 除非流水线上出现一个终端操作, 否则不会中间操作不会执行任何处理.
流的优化: 使用短路; 将多个操作合并到同一次遍历中(这种技术叫做循环合并).

4.4.2 终端操作

终端操作会从流的流水线生成结果. 其结果是任何不是流的值.

4.4.3 使用流
  • 使用流一般包含三个操作
    • 一个数据源来执行一个查询
    • 一个中间操作链, 形成一条流的流水线.
    • 一个终端操作, 执行流水线并生成结果.

第五章 使用流

使用内部迭代的话, Stream API可以决定并行执行你的代码, 使用外部迭代就办不到了, 只能使用单一线程迭代.

5.1 筛选和切片

5.1.1 用谓词筛选

Stream的filter操作接收一谓词作为参数, 并返回一个包含所有符合谓词操作的流

5.1.2 筛选各异的元素

流支持一个distinct 操作, 他会返回一个元素各异(根据流生成元素的hashCode和equals方法来实现)的流.

5.1.3 截短流

流支持limit(n)的方法, 该方法会返回一个不超过指定长度的流. 如果流是有序的, 则最多会返回前n个元素. 如果流是无序的那么limit的结果不会以任何顺序排列.

5.1.4 跳过元素

流支持skip(n)方法, 返回一个扔掉前n个元素的流. 如果流中元素不足n个则返回一个空流.

5.2 映射

5.2.1 对流中每一个元素应用函数

流支持map方法, 它将一个函数作为参数. 这个函数会作用到每个元素上, 并将其映射成一个新的元素.

5.2.2 流的扁平化
// 示例: 对于一张单词表, 如何返回一个列表, 列出里面各不相同的字符呢?
// ["hello", "world"]
// 第一个版本的代码可能是这样的, 但是这个结果有个问题, 他返回的是Stream, 而不是Stream
words. Stream(). map(w->w.split("")).distinct().collect(Collectors.toList());

// 想要一个字符流我们可以使用Arrays.stream(), 他接收一个数组并生成一个流.  但是还是有一个问题, 接收了两个数组, 所以最后生成了俩个流, 但是我们想要的是一个字符流
words. Stream(). map(w->w.split("")).map(Arrays::stream).distinct().collect(Collectors.toList());

// 使用flatmap
words. Stream(). map(w->w.split("")).flatmap(Arrays::stream).distinct().collect(Collectors.toList());

简而言之, flatmap让你把一个流中的每个值都转换成另一个流, 再把每个流连接起来成为一个流.

// 测验 5.2: 映射
// (1) 给定一个数字序列, 如何返回一个由每个数字的平方组成的列表呢? 例如 给定[1, 2, 3, 4] 返回 [1, 4, 9, 16]
// (2) 给定两个数字列表, 如何返回所有的数对呢? 例如给定列表[1, 2, 3] 和 [3, 4], 应该返回[(1,3), (1,4), (2,3), (2,4), (3,3), (3,4)]

5.3 查找和匹配

Stream API提供了allMatch, anyMatch, noneMatch, findFirst, findAny这些可以查看数据中某些元素是否匹配一个给定属性.
anyMatch: 检查谓词是否至少匹配一个元素. 返回一个boolean是一个终端操作.
allMatch: 检查谓词是否匹配所有元素.
noneMatch: 检查是否流中没有任何元素符合给定谓词的[]匹配.

// 短路求值
// 有些操作不需要处理整个流就能得到结果. 例如: 假设你需要对一个用and连接起来的大布尔表达式求值. 不管表达式多长, 你只需要找到一个表达式为false, 就可以推断整个表达式将返回false, 所以用不着计算整个表达式, 这就是短路.
5.3.3 查找元素

findAny() 方法返回当前流中的任意元素. 他可以和其他流操作结合使用. 返回Optional.
findFirst() 方法返回流中的第一个元素.

Optional 简介
Optional类是一个容器类, 代表一个值存在或者不存在. findAny()可能什么都没有找到, 返回Optional就不用返回null了.

// 何时使用findAny和 findFirst
// 你可能会想, 为什么同时会有findFirst和findAny呢? 答案是并行, 找到第一个元素在并行上限制太多. 如果你不关心返回的是哪个元素, 请使用findAny, 因为他在使用并行流的时候限制很少.

5.4 归约

归约操作: 将流中的所有元素反腐结合起来, 得到一个值.

5.4.1 元素求和
numbers = [4, 5, 3, 9];
// for 循环求和
int sum = 0;
for(int x : numbers)
{
    sum += x;
}
// 使用reduce 进行归约
int sum = numbers. stream().reduce(0, (x, y)->x+y);
// reduce 接收两个参数, 一个初始值, 一个BinaryOperator来将两个元素结合起来产生一个新值.
// reduce 是如何对一个数字流进行求和的: 首先, 0作为Lambda的第一个参数(x), 从流中获取4 作为第二个参数(y). 0+4得到4, 它生成了新的累积值. 然后再用累积值和流中下一个元素5调用Lambda, 产生新值9.....
// reduce 还有一个重载的变体, 他不接受初始值, 但是结果会返回一个Optional对象.

// 计算最大值
Optional<Integer> max = numbers. stream().reduce(Integer::max);
// 计算最小值
Optional<Integer> min = numbers. stream().reduce(Integer::min);
// 归约方法的优势和并行化
// 相比于逐步迭代求和, 使用reduce的好处在于, 这里的迭代被内部迭代 抽象掉了, 这让内部实现得以选择并行执行reduce操作.
// 流操作: 无状态和有状态
// 无状态: 诸如filter, map等操作会从输入流中获取每一个元素, 并在输出流中得到0个或一个结果. 这些操作一般是无状态的. 他们没有内部状态.
// 有状态: 诸如sort, distinct等操作一开始都和filter, map差不多, 都是接收一个流, 在生成一个流, 但是有一个关键区别. 从流中排序和删除重复项目时都需要知道先前的历史. 这些操作被称为有状态操作.

5.5 付诸实践

(1) 找出2011年发生的所有交易, 并按照交易额排序(从低到高).
(2) 交易员都在哪些不同的城市工作过.
(3) 查找所有来自于剑桥的交易员, 并按姓名排序
(4) 返回所有交易员的姓名字符串, 按字母顺序排序
(5) 有没有交易原始在木兰工作的
(6) 打印生活在剑桥的交易员的所有交易额
(7) 所有交易中, 最高的交易额是多少
(8) 找到交易额最小的交易

Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");

List<Transaction> transactions = Arrays.asList(
        new Transaction(brian, 2011, 300),
        new Transaction(raoul, 2012, 1000),
        new Transaction(raoul, 2011, 400),
        new Transaction(mario, 2012, 710),
        new Transaction(mario, 2012, 700),
        new Transaction(alan, 2012, 950)
);

// Trader和Transaction类的定义:
public class Trader {
    private String name;
    private String city;

    public Trader(String n, String c){
        this.name = n;
        this.city = c;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    @Override
    public String toString() {
        return "Trader{" +"name='" + name + '\'' + ", city='" + city + '\'' + '}';
    }
}

public class Transaction {
    private Trader trader;
    private Integer year;
    private Integer value;

    public Transaction(Trader trader, Integer year, Integer value) {
        this.trader = trader;
        this.year = year;
        this.value = value;
    }

    public Trader getTrader() {
        return trader;
    }

    public void setTrader(Trader trader) {
        this.trader = trader;
    }

    public Integer getYear() {
        return year;
    }

    public void setYear(Integer year) {
        this.year = year;
    }

    public Integer getValue() {
        return value;
    }

    public void setValue(Integer value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Transaction{" + "trader=" + trader + ", year=" + year + ", value=" + value + '}';
    }
}
//  (4) 返回所有交易员的姓名字符串, 按字母顺序排序
String traderStr =
                transactions.stream()
                        // 提取所有交易员姓名,生成一个 Strings 构成的 Stream
                        .map(transaction -> transaction.getTrader().getName())
                        // 只选择不相同的姓名
                        .distinct()
                        // 对姓名按字母顺序排序
                        .sorted()
                        // 逐个拼接每个名字,得到一个将所有名字连接起来的 String
                        .reduce("", (n1, n2) -> n1 + " " + n2);
// 这里最后字符串连接的时候效率不高, 他每次迭代都要建立一个新的String对象. 建议使用Collectors.joining(), 他的内部是使用StringBuilder实现的.
String traderStr = 
transactions.stream().map(transaction -> 
transaction.getTrader().getName()).distinct() .sorted().collect(Collectors.joining());

5.6 数值流


int calories = menu.stream()
                    .map(Dish::getCalories)
                    .reduce(0, Integer::sum);
// 这段代码是有问题的, 他暗含了一个自动装箱的过程
5.6.1 原始类型的特化

Java 8 引入了三个原始类型特化流接口来解决自动装箱的问题, IntStream, DoubleStream, LongStream, 分别将流中的元素特化为int, long, double, 从而避免了暗含的装箱成本.

// 上面的代码可以改写为下面这种
int calories = menu.stream() .mapToInt(Dish::getCalories).sum();
// 这里如果流是空的, 返回值就是0. IntStream 还有其他方法 例如max, min, average等.

// 如果需要将原始流转换为一般流, 使用boxed则可以实现
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
5.6.2 数值范围

Java 8 引入了两个可以用于IntStream和 LongStream的静态方法生成范围数值, range和rangeClosed. 这两个方法都是第一个参数接收初始值, 第二个参数接收结束值. range不包含结束值, rangeClosed 包含结束值.

你可能感兴趣的:(Java,Java,8,lambda)