跳转至

Java 8 新特性

1 Lambda表达式

Lambda表达式由参数、箭头和函数主体组成。可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

  • (Apple a1, Apple a2): 参数列表 — 这里它采用了Comparator中compare方法的参数,两个Apple。
  • ->: 箭头, 把参数列表与Lambda主体分开。
  • Lambda主体: 比较两个Apple的重量。表达式就是Lambda的返回值了。

Lambda的基本语法是

(parameters) -> expression
(parameters) -> {statements;} // 注意花括号和分号

函数式接口

函数式接口(funtional interface)就是定义且只定义了⼀个抽象⽅法的接口。函数式接口的抽象⽅法的签名称为函数描述符。函数式接口可以带 有@FunctionalInterface的注解,但不是必须的。常见的函数式接口有Comparable, Runnable, Callable

// Runnable.java
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

⽤函数式接口可以⼲什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象⽅法提供实现,并把整个表达式作为函数式接口的实例 (具体说来,是函数式接口⼀个具体实现的实例):

// 使用lambda
Runnable r1 = () -> System.out.println("hello world!");
// 使用匿名类
Runnable r2 = new Runnable() {
    public void run() {
        System.out.println("Hello world!");
    }
}

public static void process(Runnable r) {
    r.run();
}
// 利用直接传递的Lambda
process(()->System.out.println("hello world!"));

函数描述符

函数式接口的抽象⽅法就是函数描述符。例如,Runnable接口可以看作⼀个什么也不接受什么也不返回(void)的函数的签名,因为它只有⼀个叫作run()的抽象⽅法,这个⽅法什么也不接受,什么也不返回(void)。可以写成()->void来描述函数式接口的签名。

一个例子

资源处理(例如处理⽂件或数据库)是⼀个常见的模式:打开⼀个资源,做⼀些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执⾏处理的那些重要代码。这就是所谓的环绕执⾏(execute around)模式,下图很好的展示了环绕执行模式的特点:

带资源的try语句块,会在结束后释放资源。而核心代码只有 br.readLine()

public static String processFile() throws IOException { 
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 
        return br.readLine(); 
    }
}

假如我们下次需要读取文件前两行呢?我们可能需要复制一下上面的方法。如下:

public static String processFile() throws IOException { 
    try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 
        return br.readLine(); 
    }
}

如果现在需要读取三行,最后一行呢?会造成太多代码冗余了!但是java8后你可以这样:

1.定义一个函数式接口

@FunctionalInterface 
public interface BufferedReaderProcessor { 
    String process(BufferedReader b) throws IOException; 
}

2.定义读取文件的方法

public static String processFile(BufferedReaderProcessor p) 
                                    throws IOException { 
 try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { 
    return p.process(br);
 }}

3.行为参数化-传递lamdba行为表达式

//处理一行:
String oneLine = processFile((BufferedReader br) -> br.readLine()); 
//处理两行:
String twoLines = processFile((BufferedReader br) -> br.readLine() 
            + br.readLine());

常见的函数式接口

Java 8在java.util.function包中引入了几个新的函数式接口。

common_functional_interface

Predicate

Predicate<T>接口定义了一个名叫test的抽象方法,它接受泛型T对象,并返回一个boolean。

以下是源码的一部分:

@FunctionalInterface 
public interface Predicate<T>{ 
    boolean test(T t); 
} 

// filter源码部分
public static <T> List<T> filter(List<T> list, Predicate<T> p) { 
    List<T> results = new ArrayList<>(); 
    for(T s: list){ 
        if(p.test(s)){ 
            results.add(s); 
        } 
    } 
 return results; 
} 

// 实际使用场景-1
Predicate<String> nonEmptyStringPredicate = (String s) -> !s.isEmpty(); 
List<String> nonEmpty = filter(listOfStrings, nonEmptyStringPredicate);
// 实际使用场景-2
List<String> nonEmpty = filter(listOfStrings, (String s) -> !s.isEmpty());

Consumer

Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void)。你如果需要访问类型T的对象,并对其执行某些操作,就可以使用这个接口.

@FunctionalInterface 
public interface Consumer<T>{ 
  void accept(T t); 
} 

public static <T> void forEach(List<T> list, Consumer<T> c){ 
     for(T i: list){ 
         c.accept(i); 
    }
} 

// 实际使用--lambda表达式即为Consumer函数式接口参数
forEach(
    Arrays.asList(1,2,3,4,5), 
    (Integer i) -> System.out.println(i) 
 );
Function

Function<T, R>接口定义了一个叫作apply的方法,它接受一个泛型T的对象,并返回一个泛型R的对象

@FunctionalInterface 
public interface Function<T, R>{ 
    R apply(T t); 
} 
public static <T, R> List<R> map(List<T> list, Function<T, R> f) { 
    List<R> result = new ArrayList<>(); 
    for(T s: list){ 
        result.add(f.apply(s)); 
    } 
 return result; 
} 

// 这里 (String s)相当于T-String  s.length()相当于 R --int
List<Integer> l = map(
        Arrays.asList("lambdas","in","action"), 
        (String s) -> s.length() 
 );

原始类型特化

我们介绍了三个泛型函数式接口:Predicate<T>Consumer<T>Function<T,R>。还有些函数式接口专为某些类型而设计。

如果基础类型也使用这些函数式接口,比如Predicate<Integer>通过自动拆箱装箱是可以实现的,但这在性能方面是要̶出代价的。装箱的本质就是把原来的原始类型包装起来,并保存在堆里。因此,装箱后值需要更多的内存,并需要额外的内存搜索来获取被包装的原始值。

Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。

类型检查推断

当我们第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息。

类型检查

Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。让我们通过一个例子,看看当你使用Lambda表达式时背后发生了什么。

请注意,如果Lambda表达式抛出一个异常,那么抽象方法所声明的throws语句也必须与之匹配

特殊的void匹配规则

如果一个Lambda的主体是一个表达式,就和一个返回void的函数式接口兼容。(当然参数列表必须兼容)

例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是函数Consumer上下文(T -> void)所要求的void:

//Predicate返回了一个boolean 
Predicate<String> p = s -> list.add(s); 
//Consumer返回了一个void 
Consumer<String> b = s -> list.add(s);
类型推断

Java编译器会像下面这样推断Lambda的参数类型:

// 参数a没有显示说明类型
List<Apple> greenApples = filter(inventory, a -> "green".equals(a.getColor()));
Lambda表达式有多个参数,代码可读性的好处就更为明显。例如,你可以这样来创建一个Comparator对象:
// 没有类型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
// 有类型推断
Comparator<Apple> c = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
有时候显式写出类型更易读,有时候去掉它们更易读。没有什么法则说哪种更好;对于如何让代码更易读,程序员必须做出自己的选择。

使用局部变量

我们迄今为止所介绍的所有Lambda表达式都只用到了其主体里面的参数。但Lambda表达式也允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda

下面的Lambda捕获了portNumber变量:

int portNumber = 1337; 
Runnable r = () -> System.out.println(portNumber);

尽管如此,还有一点点小麻烦:关于能对这些局部变量做什么有一些限制。Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但是局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this。) 例如,下面的代码无法编译,因为portNumber变量被赋值两次:

int portNumber = 1337; 
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;
// 错误的,因为portNumber被赋值两次,lambda捕获的局部变量必须是显示final或事实上就是final(即只被赋值一次的)

为什么这样限制

实例变量不需要是final,而局部变量需要是final。最主要的原因是因为:实例变量是存储在堆上的,而局部变量是存储在方法栈上的。而当lambda函数访问局部变量时,该变量可能已经被回收,因此只会捕获一次即只会复制一次局部变量的副本,访问时即访问副本。因此该变量必须保证是final,副本才有效。

使用方法引用

方法引用其实就是为了使代码可读性更高,例如:

// 直接使用lambda
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 使用方法引用
inventory.sort(comparing(Apple::getWeight));

方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,比如(Apple a) -> a.getWeight(),那最好还是方法引用来调用它:Apple::getWeight

当你需要使用方法引用时,目标引用放在分隔符::前,方法的名称放在后面。Apple::getWight 即Apple是目标引用

() -> Thread.currentThread().dumpStack()  ==>    Thread.currentThread()::dumpStack
(str, i) -> str.substring(i)              ==>    String::substring

如何构建方法引用

  • 指向静态方法的方法引用

    (agrs) -> ClassName.staticMethod(args)    ==>    ClassName::staticMethod
    

  • 指向任意类型实例方法的引用

    (exp,args) -> exp.instanceMethod(args)    ==>    ExpClassName::instanceMethod
    

  • 指向现有对象的实例方法的方法引用

    // exp是已有变量
    (args) -> exp.instanceMethod(args)        ==>    exp::insatanceMethod
    

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

构造函数引用
  • 无参构造函数引用 即 () -> T

    Supplier<Apple> = Apple:new
    

  • 一个参数构造函数引用 即 (P) -> T

    Function<Integer, Apple> = Apple::new
    

  • 两个参数的构造函数引用 即 (P1, P2) -> T

    BiFunction<Integer, Integer, Apple> = Apple::new
    

  • 多个构造函数的引用也一样,只是需要自定义函数式接口。接口上下文符合 (P1,P2,P3...) -> T 即可

一个栗子:

List<Integer> weights = Arrays.asList(7, 3, 4, 10); 
// 调用map方法获得一组apple实例的集合
List<Apple> apples = map(weights, Apple::new); 
// 将构造函数引用传递给map方法
public static List<Apple> map(List<Integer> list, Function<Integer, Apple> f){ 
    List<Apple> result = new ArrayList<>(); 
    for(Integer e: list){
        result.add(f.apply(e)); 
    }
    return result; 
}

不将构造函数实例化却能够引用它,这个功能有一些有趣的应用。比如下面的giveMeFruit方法可以获得各种各样不同重量的水果实例:

// 创建一个Map 字符串映射相应的构造函数引用
static Map<String, Function<Integer, Fruit>> map = new HashMap<>(); 
static {
    // apple 匹配Apple的构造函数引用
    map.put("apple", Apple::new); 
    map.put("orange", Orange::new); 
    // etc... 
} 
// 这个方法可以通过输入的 fruit名字 以及构造函数需要的参数,获得相应的实例。
public static Fruit giveMeFruit(String fruit, Integer weight){ 
    return map.get(fruit.toLowerCase()) 
              .apply(weight); 
}

lambda和方法引用实战

用不同的排序策略给一个Apple列表排序,并需要展示如何把一个原始粗暴的解决方法一步步优化。

Java 8的API已经为你提供了一个List可用的sort方法,你不用自己去实现它。那么最困难的部分已经搞定了。但是,如何把排序策略传递给sort方法呢?你看,sort方法的签名是这样的:void sort(Comparator<? super E> c)。而Comparator是函数式接口,可以传递方法。因此我们可以认为sort的行为被参数化了。传递给它的排序策略不同,其行为也会不同。

  • 首先我们的第一个解决办法可能是:

    inventory.sort(new Comparator<Apple>() { 
        public int compare(Apple a1, Apple a2){ 
            return a1.getWeight().compareTo(a2.getWeight()); 
        } 
    });
    

    匿名内部类的方法依旧很啰嗦,因为当我们需要一种新的排序策略时,我们可能需要把上面的代码拷贝一份,但是却只需要改动 return a1.getWeight().compareTo(a2.getWeight()); 这部分核心代码。策略一多便啰嗦极了。

  • ֵ用 Lambda 表达式

上面的例子可以看成是一个接收签名为 (T1,T2) -> int 的方法。

inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));

看起来好多了,因为lambda的类型推断,我们甚至可以1简化成下面这样:

inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));

代码还能更简洁吗?Comparator具有一个叫作comparing的静态辅助方法, 它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象(我们会在第 后面解释为什么接口可以有静态方法)。

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));
}

它可以像下面这样用:

Comparator<Apple> c = Comparator.comparing((a) -> a.getWeight());

现在你可以把代码再改得紧凑一点了:

inventory.sort(Comparator.comparing((a) -> a.getWeight());

  • 方法引用

前面解释过,方法引用就是替代那些转发参数的Lambda表达式的语法糖。你可以用方法引 用让你的代码更简洁

inventory.sort(Comparator.comparing(Apple::getWeight);

这就是你的最终解决方案!这比Java 8之前的代码好在哪儿呢?它比较短;它的意 思也很明显,并且代码读起来和问题描述差不多:“对库存进行排序,比较苹果的重量。”

注意: - 这个例子中lambda表达式Apple::getWeight 的返回值是int 因此可以采用 comparingInt方法提高内存利用率。 - lambda表达式的返回值必须实现了Comparable接口,为可比较的元素,才能进行集合排序操作。

复合Lambda表达式

Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的ComparatorFunctionPredicate都提供了允许你进行复合的方法。

这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。

你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这可是违背了函数式接口的定义啊!)窍门在于,提供的允许进行复合操作的方法都是默认方法,也就是说它们不是抽象方法。

比较器复合

我们前面看到,你可以使用静态方法Comparator.comparing,如下所示:Comparator<Apple> c = Comparator.comparing(Apple::getWeight);

  • 逆序

    如果我们需要对之前的排序策略进行逆序怎么办?用不着去建立另一个Comparator的实例。接口有一个默认方法reversed可以使给定的比较器逆序

    inventory.sort(comparing(Apple::getWeight).reversed());
    

  • 比较器链

    前面都很好,但如果发现有两个苹果一样重怎么办?哪个苹果应该排在前面呢?你可能 需要再提供一个Comparator来进一步定义这个比较。比如,在按重量比较两个苹果之后,你可 能想要按原产国排序。thenComparing方法就是做这个用的。

inventory.sort(comparing(Apple::getWeight) 
         .reversed() 
         .thenComparing(Apple::getCountry));
谓词复合

谓词接口包括三个方法:negate、and和or,你可以重用已有的Predicate来创建更复 杂的谓词。比如,你可以使用negate方法来返回一个Predicate的非,比如苹果不是红的:

Predicate<Apple> redApple = (a) -> a.getColor().equals("red");
Predicate<Apple> notRedApple = redApple.negate();

你可能想要把两个Lambda用and方法组合起来,比如一个苹果既是红色的又比较重的:

Predicate<Apple> redAndHeavy = redApple.and((a) -> a.getWeight() > 150);

你可以进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果:

Predicate<Apple> redAndHeavyOrGreen = redApple.and((a) -> a.getWeight() > 150)
                                              .or((a) -> a.getColor().equals("green"));

请注意,and和or方法是按照在表达式链中的位置,从左向右确定优 先级的。因此,a.or(b).and©可以看作(a || b) && c。

函数复合

最后,你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配 了andThen和compose两个默认方法

andThen方法会返回一个函数,它先对输入应用一个给定函数,再对输出应用另一个函数。 比如,假设有一个函数f给数字加1 (x -> x + 1),另一个函数g给数字˱2,你可以将它们组 合成一个函数h,先给数字加1,再给结果乘2:

// g(f(x)) 即:((x+1)*2)
Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.andThen(g); 
int result = h.apply(1); // 返回 4

使用compose方法,先把给定的函数用作compose的参数里面给的那个函 数,然后再把函数本身用于结果。比如在上一个例子里用compose的话,它将意味着f(g(x)), 而andThen则意味着g(f(x)):

// 数学上会写作f(g(x)) 即 ((x*2)+1)  compose组成/构成
Function<Integer, Integer> f = x -> x + 1; 
Function<Integer, Integer> g = x -> x * 2; 
Function<Integer, Integer> h = f.compose(g);
int result = h.apply(1); // 返回3

那么在实际中这有什么用呢?比方说你有一系列工具方法,对用String表示的一份信做文本转换:

public class Letter{ 
    public static String addHeader(String text){ 
        return "From caotinging: " + text; 
    } 
    public static String addFooter(String text){ 
        return text + " Kind regards"; 
    } 
    public static String checkSpelling(String text){ 
        return text.replaceAll("labda", "lambda"); 
    } 
}

现在你可以通过复合这些工具方法来创建各种转型流水线了,比如创建一个流水线:先加上 抬头,然后进行拼写检查,最后加上一个落款:

Function<String, String> addHeader = Letter::addHeader; 
Function<String, String> transformationPipeline 
      = addHeader.andThen(Letter::checkSpelling) 
                 .andThen(Letter::addFooter);

2 流

流是什么

流是允许以声明性方式处理数据集合,并可以透明的并行处理,而无需写任何多线程代码。

下面两段代码都是用来返回低热量的菜肴名称的, 并按照卡路里排序,一个是用Java 7写的,另一个是用Java 8的流写的。比较一下。

java7

// 迭代器筛选卡路里低于400的食物
List<Dish> lowCaloricDishes = new ArrayList<>(); 
for(Dish d: menu){ 
    if(d.getCalories() < 400){ 
        lowCaloricDishes.add(d); 
    } 
} 
// 进行排序
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
    public int compare(Dish d1, Dish d2){ 
        return Integer.compare(d1.getCalories(), d2.getCalories()); 
    } 
}); 
// 获取排序后低热量菜肴的名称
List<String> lowCaloricDishesName = new ArrayList<>(); 
for(Dish d: lowCaloricDishes){ 
    lowCaloricDishesName.add(d.getName()); 
}

在这段代码中,你用了一个“垃圾变量”lowCaloricDishes。它唯一的作用就是作为一次 性的中间容器。在Java 8中,实现的细节被放在它本该归属的库里了。

java8

import static java.util.Comparator.comparing; 
import static java.util.stream.Collectors.toList; 

List<String> lowCaloricDishesName = 
            menu.stream() 
                .filter(d -> d.getCalories() < 400) 
                .sorted(comparing(Dish::getCalories))
                .map(Dish::getName) 
                .collect(toList());

为了利用多核架构并行执行这段代码,你只需要把stream()换成parallelStream():

List<String> lowCaloricDishesName = 
                menu.parallelStream() 
                    .filter(d -> d.getCalories() < 400) 
                    .sorted(comparing(Dishes::getCalories)) 
                    .map(Dish::getName) 
                    .collect(toList());

你可能会想,在调用parallelStream方法的时候到底发生了什么。用了多少个线程?对性 能有多大提升?后面会详细讨论这些问题。现在,你可以看出,从软件工程师的角度来看,新 的方法有几个显而易见的好处。

  • 代码是以声明式的方式写的:说明想要完成什么,而不是说明如何实现一个操作(利用循环和if条件等控制流语句)。
  • 轻松应对变化的需求:你很容易再创建一个代码版本,利用 Lambda表达式来筛选高卡路里的菜肴,而用不着去复制粘贴代码。
  • 你可以把几个基础操作链接起来,来表达复杂的数据处理流水线(在filter后面接上 sorted、map和collect操作),同时保持代码清晰可读。filter的结果 被传给了sorted方法,再传给map方法,最后传给collect方法。

别浪费太多时间了。一起来拥抱接下来介绍的强大的流吧!

总结一下,Java 8中的Stream API可以让你写出这样的代码: - 声明式——更简洁,更易读 - 可复合——更灵活 - 可并行——性能更好

我们会使用这样一个例子:一个menu,它只是一张菜单:

List<Dish> menu = Arrays.asList( 
 new Dish("pork", false, 800, Dish.Type.MEAT), 
 new Dish("beef", false, 700, Dish.Type.MEAT), 
 new Dish("chicken", false, 400, Dish.Type.MEAT), 
 new Dish("french fries", true, 530, Dish.Type.OTHER), 
 new Dish("rice", true, 350, Dish.Type.OTHER), 
 new Dish("season fruit", true, 120, Dish.Type.OTHER), 
 new Dish("pizza", true, 550, Dish.Type.OTHER), 
 new Dish("prawns", false, 300, Dish.Type.FISH), 
 new Dish("salmon", false, 450, Dish.Type.FISH) );
Dish类的定义是:
public class Dish { 
     private final String name; 
     private final boolean vegetarian; // 素
     private final int calories; 
     private final Type type; 

     public Dish(String name, boolean vegetarian, int calories, Type type) { 
         this.name = name; 
         this.vegetarian = vegetarian; 
         this.calories = calories; 
         this.type = type; 
     } 
     public String getName() { 
        return name; 
     } 
     public boolean isVegetarian() { 
        return vegetarian; 
     } 
     public int getCalories() { 
        return calories; 
     } 
     public Type getType() { 
        return type; 
     } 
     @Override 
     public String toString() { 
        return name; 
     }
     public enum Type { MEAT, FISH, OTHER } 
}

流简介

简短的定义就是“从支持数据处理操作的源生成的元素序列”。让 我们一步步分析这个定义。

  • 元素序列——就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序 值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元 素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如你前面见到的 filter、sorted和map。集合讲的是数据,流讲的是计算。我们会在后面详细解 释这个思想。
  • ——流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集 合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作——流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中 的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执 行,也可并行执行。

此外,流操作有两个重要的特点。 - 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大 的流水线。这让一些优化成为可能,如延迟和短路。流水线的操作可以 看作对数据源进行数据库式查询。 - 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

让我们来看一段能够体现所有这些概念的代码:

import static java.util.stream.Collectors.toList; 
List<String> threeHighCaloricDishNames = 
                    menu.stream()  // 从菜单集合中获取流-建立流水线
                        .filter(d -> d.getCalories() > 300) // 首先筛选卡路里高于300的食物
                        .map(Dish::getName) // 获取菜名
                        .limit(3) // 截取前三个
                        .collect(toList()); // 组合成新的列表
System.out.println(threeHighCaloricDishNames);

在本例中,我们先是对menu调用stream方法,由菜单得到一个流。数据源是菜单列表, 它给流提供一个元素序列。接下来,对流应用一系列数据处理操作:filter、map、limit 和collect。除了collect之外,所有这些操作都会返回另一个流,这样它们就可以接成一条流水线,于是就可以看作对源的一个查询。最后,collect操作开始处理流水线,并返回结果(它 和别的操作不一样,因为它返回的不是流,在这里是一个List)。在调用collect之前,没有任 何结果产生,实际上根本就没有从menu里选择元素。你可以这么理解:链中的方法调用都在排 队等待,直到调用collect。

过程如下所示:

流与集合

我们先来打个直观的比方吧。比如说存在DVD里的电影,这就是一个集合(也许是字节,也 许是帧,这个无所谓),因为它包含了整个数据结构。

现在再来想想在互联网上通过视频流看同 样的电影。现在这是一个流(字节流或帧流)。流媒体播放器只要提前下载用户观看位置的 那几帧就可以了,这样不用等到流中大部分值计算出来,你就可以显示流的开始部分了(想想观 看直播足球赛)。

特别要注意,视频播放器可能没有将整个流作为集合,保存所需要的内存缓冲 区——而且要是非得等到最后一帧出现才能开始看,那等待的时间就太长了。

简单地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构, 它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。(你可 以往集合里加东西或者删减东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素 都得先算出来才能成为集合的一部分。)

相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计 算的。 这对编程有很大的好处。在后面,我们将展示利用流构建一个质数流(2, 3, 5, 7, 11, …)有 多简单,尽管质数有无穷多个。这个思想就是用户仅仅从流中提取需要的值,而这些值——在用 户看不见的地方——只会按需生成。从另一个角度来说,流就 像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱 动,甚至是实时制造)。

与此相反,集合则是急切创建的(供应商驱动:先把̱库装满,再开始卖,就像那些昙花一现 的圣诞新玩意儿一样)。以质数为例,要是想创建一个包含所有质数的集合,那这个程序算起 来就没完没了了,因为总有新的质数要算,然后把它加到集合里面。当然这个集合是永远也创建 不完的,消费者这辈子都见不着了。

只能遍历一次

请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。 你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集 合之类的可重复的源,如果是I/O通道就没戏了)。

List<String> title = Arrays.asList("Java8", "In", "Action"); 
Stream<String> s = title.stream(); 
s.forEach(System.out::println);
// 下面这句代码会抛出异常 java.lang.IllegalStateException:提示流已被消费 
s.forEach(System.out::println);

所以要记得,流只能被消费一次!

内部迭代和外部迭代

使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反, Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出 一个函数说要干什么就可以了

// 集合:使用for-each循环外部迭代
List<String> names = new ArrayList<>(); 
for(Dish d: menu){ 
    names.add(d.getName()); 
}

请注意,for-each还隐藏了迭代中的一些复杂性。for-each结构是一个语法糖,它背后的 东西用Iterator对象表达出来更要丑陋得多。

// 集合:用背后的迭代器做外部迭代
List<String> names = new ArrayList<>(); 
Iterator<String> iterator = menu.iterator(); 
while(iterator.hasNext()) { 
    Dish d = iterator.next(); 
    names.add(d.getName()); 
}
// 流:内部迭代,将得到的操作流根据提供的函数进行操作
List<String> names = menu.stream() 
            .map(Dish::getName)
            .collect(toList());

举个例子说明:比如你希望你两岁的女儿把地上的玩具收起来

外部迭代:

你:“我们把玩具收进盒子里,地上还有玩具吗?”
小孩:“有,球。”
你:“好,把球放进盒子里,还有吗?”
小孩:“有,娃娃。”
你:“好,把娃娃放进盒子里,还有吗?”
小孩:“有,水枪。”
你:“好,把水枪放进盒子里,还有吗?”
小孩:“没有了”
你:“好。我们收好了”

内部迭代:

// 你只需要告诉小孩,把地上的玩具放进盒子里
你:”我们把地上的玩具都收进盒子里“
小孩:“好”

// 小孩可以选择一只手拿球,一只手拿娃娃,一起放进盒子里,也可以先把近一点的水枪放进盒子里,效率更高

总结就是:内部迭代时,项目可以透明地并行处理,或者用更优化的顺 序进行处理。要是用Java过去的那种外部迭代方法,这些优化都是很困难的。 并且一旦通过写for-each而选择了外部迭代,那你基 本上就要自己管理所有的并行问题了(自己管理实际上意味着“某个良辰吉日我们会把它并行化” 或“开始了关于任务和synchronized的漫长而艰苦的斗争”)

流操作

stream定义了很多操作,分为两类:

List<String> names = menu.stream() 
                .filter(d -> d.getCalories() > 300)
                .map(Dish::getName) 
                .limit(3) 
                .collect(toList());

你可以看到两类操作: 1.filter、map和limit可以连成一条流水线; 2.collect触发流水线执行并关闭它;

可以连接起来的流操作称为中间操作,关闭流的操作称为终端操作。

中间操作

诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查 询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。 这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

为了搞清楚流水线中到底发生了什么,我们把代码改一改,让每个Lambda都打印出当前处 理的菜肴(就像很多演示和调试技巧一样,这种编程风格要是放在生产代码里那就吓死人了,但 是学习的时候却可以直接看清楚求值的顺序):

List<String> names = 
    menu.stream() 
        .filter(d -> { 
            System.out.println("filtering" + d.getName()); 
            return d.getCalories() > 300; 
        }) 
        .map(d -> { 
            System.out.println("mapping" + d.getName()); 
            return d.getName(); 
        }) 
        .limit(3) 
        .collect(toList()); 

System.out.println(names);

此时打印的结果如下:

filtering pork 
mapping pork 
filtering beef 
mapping beef 
filtering chicken 
mapping chicken 
[pork, beef, chicken]

可以很清楚的看出来,程序并不是顺序执行的,filter、map是并行处理的。这种技术称之为循环合并。 有好几种优化利用了流的延迟性质。第一,尽管很多菜肴热量都高于300卡路里, 但只选出了前三个!这是因为limit操作以及一种称为短路的技巧。(后面会介绍)

终端操作

终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚 至void

例如,在下面的流水线中,forEach是一个返回void的终端操作,它会对源中的每道 菜应用一个Lambda。把System.out.println传递给forEach,并要求它打印出由menu生成的 流中的每一个Dish:

menu.stream().forEach(System.out::println);

总而言之,流的使用一般包括三件事:

  1. 一个数据源(如集合)来执行一个查询;
  2. 一个中间操作链,形成一条流的流水线;
  3. 一个中间操作,执行流水线,并能生成结果。

下面列出了已经遇到的流操作,不涵盖全部

以上都是书中所述,本人抱着实践出真知的态度亲自试了一下stream和for循环的性能比较,就在源码中的chap4中的streamBasic、结果大跌眼镜。在难以置信的情况下 查阅了相关资料,发现了这个:Follow-up: How fast are the Java 8 Streams? 这个:Java 8 Stream的性能到底如何? 这个:Java performance tutorial – How fast are the Java 8 streams? 但是,很多关于集合线程安全性方面的考虑,Stream已经帮我们做了,如果在多线程场景下,java7的写法再加上一堆的同步锁等操作。结果究竟如何也不得而知。

3 使用Optional

如果对象不存在,那么调用对象的方法便会抛出NullPointerException异常。为了避免NullPointerException异常,通常需要在必要的地方添加null的检查。

public String getCarInsuranceName(Person person) {
    if (person != null) {
        Car car = pserson.getCar();
        if (car != null) {
            Insurance insurance = car.getInsurance();
            if (insurance != null) {
                return insurance.getName();
            }
    }
}

但很显然,这种嵌套的if语句块增加了代码缩进的层数,不具备扩展性,也牺牲了代码的可读性。总结来说,使用null会带来种种问题;

Scala语言中的Option[T]既可以包含类型为T的变量,也可以不包含该变量。但要使用它,必须显示地调用Option类型的available操作,检查该变量是否有值,而这其实也是一种变相的null检查。

Java 8中引入了新的类java.util.Optional<T>,用来封装T类型的值的类。

  • 通过静态工厂方法Optional.empty声明一个空的Optional
    • Optional<Car> optCar = Optional.empty();
  • 通过静态工厂方法Optional.of创建一个Optional对象
    • Optional<Car> optCar = Optional.of(car);
  • 可接收null的Optional
    • Optional<Car> optCar = Optional.ofNullable(car);


  1. https://github.com/caotinging/Java8Action