概述lambda

前言

    java8出来已经很久了,但是令人惊讶的是自己在工作中竟然没有遇到lambda的需求。突然感觉自己已经被时代抛弃,诚惶诚恐!

    看了几篇博客,感觉lambda高深得不行,不过所幸的是,如果你不追究,那倒也不难。

defender方法

    介绍lambda就需要知道函数式接口,函数式接口又会涉及default关键字。所以先从这里开始:

    其实在我看来,推出这个东西完全是属于无奈之举。因为在java8开发时遇到如下问题:

        List<?> list = ...

        list.forEach(...);// Lambda code goes here

    java7中可没有forEach这样的方法,ok,我们需要在list中添加,但是实际上我可能需要在各种容器上都添加,最简单的方法是在公共的接口(比如Iterable)中添加一个forEach方法。但是问题来了,我有那么多的类实现了该接口,当然包括一些第三方库,如果我修改了公共接口,后果就是大家都得跟着改,这是非常难实现的事情,所以就诞生了default关键字。

public  interface  A {
    default  void  foo(){
        System.out.println("Calling A.foo()");
    }
} 
public  class  Clazz  implements A {}

    即使Clazz没有实现foo方法,但是它还是能够调用clazz.foo()。

    那么就会存在问题,如果一个类实现了两个接口,而这两个接口中各自定义了一个同名的default方法,会怎么样?比如

public  interface A {
    default  void  foo(){
        System.out.println("Calling A.foo()");
    }
} 
public  interface B {
    default  void  foo(){
        System.out.println("Calling B.foo()");
    }
} 
public class Clazz implements A, B {}

    上面代码会出现编译失败的提示:

java: class Clazz inherits unrelated defaults for foo() from types A and B

    解决方法就是我们需要手动实现foo方法,排除歧义:

public  class  Clazz  implements A, B {
    public  void foo(){
         //A.super.foo();
         //B.super.foo();
    }
}

    调用A的,或者调用B的,或者干脆都不调用,自己重写。

    实现方式是通过调用 invokespecial 指令进行方法生成(PS : 高阶内容不介绍了)。

函数式接口

    我们知道lambda是一个好工具,那么什么时候能够使用它来做些事情?在使用lambda的时候一定会和函数式接口相关,比如方法中包含函数式接口的参数(类似callback的地方),也可以是实例化一个函数式接口(类似 new 一个interface)。

    函数式接口其实本质上还是一个接口,但是它是一种特殊的接口:SAM类型的接口(Single Abstract Method)。定义了这种类型的接口,使得以其为参数的方法,可以在调用时,使用一个lambda表达式作为参数。从另一个方面说,一旦我们调用某方法,可以传入lambda表达式作为参数,则这个方法的参数类型,必定是一个函数式的接口,这个接口会使用@FunctionalInterface进行修饰(不用也可以)。

  从SAM原则上讲,这个接口中,只能有一个函数需要被实现,但是也可以有如下例外:

    1. 默认方法与静态方法并不影响函数式接口的契约,可以任意使用,即函数式接口中可以有静态方法,一个或者多个静态方法不会影响SAM接口成为函数式接口,并且静态方法可以提供方法实现。可以有 default 修饰的默认方法。

    2. 可以有 Object 中覆盖的方法,也就是 equals,toString,hashcode等方法。

  JDK中以前所有的函数式接口都已经使用 @FunctionalInterface 定义,可以通过查看JDK源码来确认,以下附JDK 8之前已有的函数式接口:

            java.lang.Runnable

            java.util.concurrent.Callable

            java.security.PrivilegedAction

            java.util.Comparator

            java.io.FileFilter

            java.nio.file.PathMatcher 

            java.lang.reflect.InvocationHandler

            java.beans.PropertyChangeListener

            java.awt.event.ActionListener  

            javax.swing.event.ChangeListener

    举例:

//函数式接口
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

//允许使用lambda的方法
void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

//使用
getAllData().forEach(a-> Log.d("sss",a.mArtistName));

    lambda的使用分成三个部分,首先是函数式接口,其实和普通接口一样的定义,另外还需要一个能够使用这个接口的方法,最后使用部分就是调用这个方法。

    其实使用lambda并不简单的是一个语法糖,而是一种新的编程思想,函数式编程。

lambda表达式

语法规则

1.Lambda表达式的形式化表示如下所示

Parameters -> an expression 

2.如果Lambda表达式中要执行多个语句块,需要将多个语句块以{}进行包装,如果有返回值,需要显示指定return语句,如下所示:

Parameters -> {expressions;};

3.如果Lambda表达式不需要参数,可以使用一个空括号表示,如下示例所示

() -> {for (int i = 0; i < 1000; i++) doSomething();};

4.Java是一个强类型的语言,因此参数必须要有类型,如果编译器能够推测出Lambda表达式的参数类型,则不需要我们显示的进行指定,如下所示,在Java中推测Lambda表达式的参数类型与推测泛型类型的方法基本类似,至于Java是如何处理泛型的,此处略去

String []datas = new String[] {"peng","zhao","li"};
Arrays.sort(datas,(String v1, String v2) -> Integer.compare(v1.length(), v2.length()));

上述代码中 显示指定了参数类型Stirng,其实不指定,如下代码所示,也是可以的,因为编译器会根据Lambda表达式对应的函数式接口Comparator<String>进行自动推断

String []datas = new String[] {"peng","zhao","li"};;
Arrays.sort(datas,(v1, v2) -> Integer.compare(v1.length(), v2.length()));

5.如果Lambda表达式只有一个参数,并且参数的类型是可以由编译器推断出来的,则可以如下所示使用Lambda表达式,即可以省略参数的类型及括号

Stream.of(datas).forEach(param -> {System.out.println(param.length());});

6.Lambda表达式的返回类型,无需指定,编译器会自行推断,说是自行推断

7.Lambda表达式的参数可以使用修饰符及注解,如final、@NonNull等

    Ok lambda表达式讲完了。什么?这就讲完了?实际上是的,如果只是lambda表达式,就只有这些内容,但是本文还没有结束,因为java8的大部分修改都是相关联的,是对函数式编程的扩展。

方法引用

    在学习lambda表达式之后,我们通常使用lambda表达式来创建匿名方法。然而,有时候我们仅仅是调用了一个已存在的方法。如下:

Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));

    在Java8中,我们可以直接通过方法引用来简写lambda表达式中已经存在的方法。

Arrays.sort(stringsArray, String::compareToIgnoreCase);

    这种特性就叫做方法引用(Method Reference)。

    方法引用的标准形式是:类名::方法名。(注意:只需要写方法名,不需要写括号)有以下四种形式的方法引用:

类型示例
引用静态方法ContainingClass::staticMethodName
引用某个对象的实例方法containingObject::instanceMethodName
引用某个类型的任意对象的实例方法ContainingType::methodName
引用构造方法ClassName::new

通过demo来学习,假设存在如下类

public class Person {

    LocalDate birthday;

    public static int compareByAge(Person a,Person b){
        return a.birthday.compareTo(b.birthday);
    }

}

引用静态方法 

Person [] persons = new Person[10];

//使用匿名类
Arrays.sort(persons, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.birthday.compareTo(o2.birthday);
            }
 });

//使用lambda表达式
Arrays.sort(persons, (o1, o2) -> o1.birthday.compareTo(o2.birthday));

//使用lambda表达式和类的静态方法
Arrays.sort(persons, (o1, o2) -> Person.compareByAge(o1,o2));

//使用方法引用
//引用的是类的静态方法
Arrays.sort(persons, Person::compareByAge);

引用对象的实例方法

class ComparisonProvider{
            public int compareByAge(Person a,Person b){
                return a.getBirthday().compareTo(b.getBirthday());
            }
        }

ComparisonProvider provider = new ComparisonProvider();

//使用lambda表达式
//对象的实例方法
Arrays.sort(persons,(a,b)->provider.compareByAge(a,b));

//使用方法引用
//引用的是对象的实例方法
Arrays.sort(persons, provider::compareByAge);

引用某个类型的任意对象的实例方法

String[] stringsArray = {"Hello","World"};

//使用lambda表达式和类型对象的实例方法
Arrays.sort(stringsArray,(s1,s2)->s1.compareToIgnoreCase(s2));

//使用方法引用
//引用的是类型对象的实例方法
Arrays.sort(stringsArray, String::compareToIgnoreCase);

引用构造方法

public static <T, SOURCE extends Collection<T>, DEST extends Collection<T>>
    DEST transferElements(SOURCE sourceColletions, Supplier<DEST> colltionFactory) {
        DEST result = colltionFactory.get();
        for (T t : sourceColletions) {
            result.add(t);
        }
        return result;
    }
...
 
final List<Person> personList = Arrays.asList(persons);

//使用lambda表达式
Set<Person> personSet = transferElements(personList,()-> new HashSet<>());

//使用方法引用
//引用的是构造方法
Set<Person> personSet2 = transferElements(personList, HashSet::new); 

 

Stream

    流,对于程序员来说这应该不是一个默认的词汇。Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

流的构成

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。

图 1. 流管道 (Stream Pipeline) 的构成

图 1.  流管道 (Stream Pipeline) 的构成

有多种方式生成 Stream Source:

  • 从 Collection 和数组
    • Collection.stream()
    • Collection.parallelStream()
    • Arrays.stream(T array) or Stream.of()
    从 BufferedReader
    • java.io.BufferedReader.lines()
  • 静态工厂
  • java.util.stream.IntStream.range()
  • java.nio.file.Files.walk()
  • 自己构建
    • java.util.Spliterator
    其它
    • Random.ints()
    • BitSet.stream()
    • Pattern.splitAsStream(java.lang.CharSequence)
    • JarFile.stream()

流的操作类型分为两种:

  • Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。

还有一种操作被称为 short-circuiting。用以指:

  • 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。
  • 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。

当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。

清单 3. 一个流操作的示例

int sum = widgets.stream()

.filter(w -> w.getColor() == RED)

 .mapToInt(w -> w.getWeight())

 .sum();

stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

流的使用详解

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。

流的构造与转换

下面提供最常见的几种构造 Stream 的样例。

清单 4. 构造流的几种常见方法

// 1. Individual values

Stream stream = Stream.of("a", "b", "c");

// 2. Arrays

String [] strArray = new String[] {"a", "b", "c"};

stream = Stream.of(strArray);

stream = Arrays.stream(strArray);

// 3. Collections

List<String> list = Arrays.asList(strArray);

stream = list.stream();

需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:

IntStream、LongStream、DoubleStream。当然我们也可以用 Stream<Integer>、Stream<Long> >、Stream<Double>,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。

Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

清单 5. 数值流的构造

IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);

IntStream.range(1, 3).forEach(System.out::println);

IntStream.rangeClosed(1, 3).forEach(System.out::println);

清单 6. 流转换为其它数据结构

// 1. Array

String[] strArray1 = stream.toArray(String[]::new);

// 2. Collection

List<String> list1 = stream.collect(Collectors.toList());

List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));

Set set1 = stream.collect(Collectors.toSet());

Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));

// 3. String

String str = stream.collect(Collectors.joining()).toString();

一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。

流的操作

接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。

  • Intermediate:

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

  • Terminal:

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

  • Short-circuiting:

anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

我们下面看一下 Stream 的比较典型用法。

map/flatMap

我们先来看 map。如果你熟悉 scala 这类函数式语言,对这个方法应该很了解,它的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。

清单 7. 转换大写

List<String> output = wordList.stream().

map(String::toUpperCase).

collect(Collectors.toList());

这段代码把所有的单词转换为大写。

清单 8. 平方数

List<Integer> nums = Arrays.asList(1, 2, 3, 4);

List<Integer> squareNums = nums.stream().

map(n -> n * n).

collect(Collectors.toList());

这段代码生成一个整数 list 的平方数 {1, 4, 9, 16}。

从上面例子可以看出,map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。

清单 9. 一对多

Stream<List<Integer>> inputStream = Stream.of(

 Arrays.asList(1),

 Arrays.asList(2, 3),

 Arrays.asList(4, 5, 6)

 );

Stream<Integer> outputStream = inputStream.

flatMap((childList) -> childList.stream());

flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

filter

filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新 Stream。

清单 10. 留下偶数

Integer[] sixNums = {1, 2, 3, 4, 5, 6};

Integer[] evens =

Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

经过条件“被 2 整除”的 filter,剩下的数字为 {2, 4, 6}。

清单 11. 把单词挑出来

List<String> output = reader.lines().

 flatMap(line -> Stream.of(line.split(REGEXP))).

 filter(word -> word.length() > 0).

 collect(Collectors.toList());

这段代码首先把每行的单词用 flatMap 整理到新的 Stream,然后保留长度不为 0 的,就是整篇文章中的全部单词了。

forEach

forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

清单 12. 打印姓名(forEach 和 pre-java8 的对比)

// Java 8

roster.stream()

 .filter(p -> p.getGender() == Person.Sex.MALE)

 .forEach(p -> System.out.println(p.getName()));

// Pre-Java 8

for (Person p : roster) {

 if (p.getGender() == Person.Sex.MALE) {

 System.out.println(p.getName());

 }

}

对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的:

stream.forEach(element -> doOneThing(element));

stream.forEach(element -> doAnotherThing(element));

相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。如下是出现在该 api javadoc 上的一个示例。

清单 13. peek 对每个元素执行操作并返回一个新的 Stream

Stream.of("one", "two", "three", "four")

 .filter(e -> e.length() > 3)

 .peek(e -> System.out.println("Filtered value: " + e))

 .map(String::toUpperCase)

 .peek(e -> System.out.println("Mapped value: " + e))

 .collect(Collectors.toList());

forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。

findFirst

这是一个 termimal 兼 short-circuiting 操作,它总是返回 Stream 的第一个元素,或者空。

这里比较重点的是它的返回值类型:Optional。这也是一个模仿 Scala 语言中的概念,作为一个容器,它可能含有某值,或者不包含。使用它的目的是尽可能避免 NullPointerException。

清单 14. Optional 的两个用例

String strA = " abcd ", strB = null;

print(strA);

print("");

print(strB);

getLength(strA);

getLength("");

getLength(strB);

public static void print(String text) {

 // Java 8

 Optional.ofNullable(text).ifPresent(System.out::println);

 // Pre-Java 8

 if (text != null) {

 System.out.println(text);

 }

 }

public static int getLength(String text) {

 // Java 8

return Optional.ofNullable(text).map(String::length).orElse(-1);

 // Pre-Java 8

// return if (text != null) ? text.length() : -1;

 };

在更复杂的 if (xx != null) 的情况中,使用 Optional 代码的可读性更好,而且它提供的是编译时检查,能极大的降低 NPE 这种 Runtime Exception 对程序的影响,或者迫使程序员更早的在编码阶段处理空值问题,而不是留到运行时再发现和调试。

Stream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等。

reduce

这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于

Integer sum = integers.reduce(0, (a, b) -> a+b); 或

Integer sum = integers.reduce(0, Integer::sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。

清单 15. reduce 的用例

// 字符串连接,concat = "ABCD"

String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);

// 求最小值,minValue = -3.0

double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); 

// 求和,sumValue = 10, 有起始值

int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

// 求和,sumValue = 10, 无起始值

sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();

// 过滤,字符串连接,concat = "ace"

concat = Stream.of("a", "B", "c", "D", "e", "F").

 filter(x -> x.compareTo("Z") > 0).

 reduce("", String::concat);

上面代码例如第一个示例的 reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是 Optional,请留意这个区别。

limit/skip

limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素(它是由一个叫 subStream 的方法改名而来)。

清单 16. limit 和 skip 对运行次数的影响

public void testLimitAndSkip() {

 List<Person> persons = new ArrayList();

 for (int i = 1; i <= 10000; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<String> personList2 = persons.stream().

map(Person::getName).limit(10).skip(3).collect(Collectors.toList());

 System.out.println(personList2);

}

private class Person {

 public int no;

 private String name;

 public Person (int no, String name) {

 this.no = no;

 this.name = name;

 }

 public String getName() {

 System.out.println(name);

 return name;

 }

}


这是一个有 10,000 个元素的 Stream,但在 short-circuiting 操作 limit 和 skip 的作用下,管道中 map 操作指定的 getName() 方法的执行次数为 limit 所限定的 10 次,而最终返回结果在跳过前 3 个元素后只有后面 7 个返回。

name1

name2

name3

name4

name5

name6

name7

name8

name9

name10

[name4, name5, name6, name7, name8, name9, name10]

 


有一种情况是 limit/skip 无法达到 short-circuiting 目的的,就是把它们放在 Stream 的排序操作后,原因跟 sorted 这个 intermediate 操作有关:此时系统并不知道 Stream 排序后的次序如何,所以 sorted 中的操作看上去就像完全没有被 limit 或者 skip 一样。

清单 17. limit 和 skip 对 sorted 后的运行次数无影响

List<Person> persons = new ArrayList();

 for (int i = 1; i <= 5; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<Person> personList2 = persons.stream().sorted((p1, p2) ->

p1.getName().compareTo(p2.getName())).limit(2).collect(Collectors.toList());

System.out.println(personList2);

 

上面的示例对清单 13 做了微调,首先对 5 个元素的 Stream 排序,然后进行 limit 操作。输出结果为:

name2

name1

name3

name2

name4

name3

name5

name4

[stream.StreamDW$Person@816f27d, stream.StreamDW$Person@87aac27]


最后有一点需要注意的是,对一个 parallel 的 Steam 管道来说,如果其元素是有序的,那么 limit 操作的成本会比较大,因为它的返回对象必须是前 n 个也有一样次序的元素。取而代之的策略是取消元素间的次序,或者不要用 parallel Stream。即虽然最后的返回元素数量是 2,但整个管道中的 sorted 表达式执行次数没有像前面例子相应减少。

sorted

对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。我们对清单 14 进行优化:

清单 18. 优化:排序前进行 limit 和 skip

List<Person> persons = new ArrayList();

 for (int i = 1; i <= 5; i++) {

 Person person = new Person(i, "name" + i);

 persons.add(person);

 }

List<Person> personList2 = persons.stream().limit(2).sorted((p1, p2) -> p1.getName().compareTo(p2.getName())).collect(Collectors.toList());

System.out.println(personList2);

 

结果会简单很多:

name2

name1

[stream.StreamDW$Person@6ce253f1, stream.StreamDW$Person@53d8d10a]

当然,这种优化是有 business logic 上的局限性的:即不要求排序后再取值。

min/max/distinct

min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n)。同时它们作为特殊的 reduce 方法被独立出来也是因为求最大最小值是很常见的操作。

清单 19. 找出最长一行的长度

BufferedReader br = new BufferedReader(new FileReader("c:\\SUService.log"));

int longest = br.lines().

 mapToInt(String::length).

 max().

 getAsInt();

br.close();

System.out.println(longest);

下面的例子则使用 distinct 来找出不重复的单词。

清单 20. 找出全文的单词,转小写,并排序

List<String> words = br.lines().

 flatMap(line -> Stream.of(line.split(" "))).

 filter(word -> word.length() > 0).

 map(String::toLowerCase).

 distinct().

 sorted().

 collect(Collectors.toList());

br.close();

System.out.println(words);

Match

Stream 有三个 match 方法,从语义上说:

  • allMatch:Stream 中全部元素符合传入的 predicate,返回 true
  • anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
  • noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true

它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。对清单 13 中的 Person 类稍做修改,加入一个 age 属性和 getAge 方法。

清单 21. 使用 Match

List<Person> persons = new ArrayList();

persons.add(new Person(1, "name" + 1, 10));

persons.add(new Person(2, "name" + 2, 21));

persons.add(new Person(3, "name" + 3, 34));

persons.add(new Person(4, "name" + 4, 6));

persons.add(new Person(5, "name" + 5, 55));

boolean isAllAdult = persons.stream().

 allMatch(p -> p.getAge() > 18);

System.out.println("All are adult? " + isAllAdult);

boolean isThereAnyChild = persons.stream().

 anyMatch(p -> p.getAge() < 12);

System.out.println("Any child? " + isThereAnyChild);

输出结果:

All are adult? false

Any child? true

进阶:自己生成流

Stream.generate

通过实现 Supplier 接口,你可以自己来控制流的生成。这种情形通常用于随机数、常量的 Stream,或者需要前后元素间维持着某种状态信息的 Stream。把 Supplier 实例传递给 Stream.generate() 生成的 Stream,默认是串行(相对 parallel 而言)但无序的(相对 ordered 而言)。由于它是无限的,在管道中,必须利用 limit 之类的操作限制 Stream 大小。

清单 22. 生成 10 个随机整数

Random seed = new Random();

Supplier<Integer> random = seed::nextInt;

Stream.generate(random).limit(10).forEach(System.out::println);

//Another way

IntStream.generate(() -> (int) (System.nanoTime() % 100)).

limit(10).forEach(System.out::println);

Stream.generate() 还接受自己实现的 Supplier。例如在构造海量测试数据的时候,用某种自动的规则给每一个变量赋值;或者依据公式计算 Stream 的每个元素值。这些都是维持状态信息的情形。

清单 23. 自实现 Supplier

Stream.generate(new PersonSupplier()).

limit(10).

forEach(p -> System.out.println(p.getName() + ", " + p.getAge()));

private class PersonSupplier implements Supplier<Person> {

 private int index = 0;

 private Random random = new Random();

 @Override

 public Person get() {

 return new Person(index++, "StormTestUser" + index, random.nextInt(100));

 }

}

输出结果:

StormTestUser1, 9

StormTestUser2, 12

StormTestUser3, 88

StormTestUser4, 51

StormTestUser5, 22

StormTestUser6, 28

StormTestUser7, 81

StormTestUser8, 51

StormTestUser9, 4

StormTestUser10, 76

Stream.iterate

iterate 跟 reduce 操作很像,接受一个种子值,和一个 UnaryOperator(例如 f)。然后种子值成为 Stream 的第一个元素,f(seed) 为第二个,f(f(seed)) 第三个,以此类推。

清单 24. 生成一个等差数列

Stream.iterate(0, n -> n + 3).limit(10). forEach(x -> System.out.print(x + " "));.

输出结果:

0 3 6 9 12 15 18 21 24 27

与 Stream.generate 相仿,在 iterate 时候管道必须有 limit 这样的操作来限制 Stream 大小。

进阶:用 Collectors 来进行 reduction 操作

java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把 Stream 元素进行归组。

groupingBy/partitioningBy

清单 25. 按照年龄归组

Map<Integer, List<Person>> personGroups = Stream.generate(new PersonSupplier()).

 limit(100).

 collect(Collectors.groupingBy(Person::getAge));

Iterator it = personGroups.entrySet().iterator();

while (it.hasNext()) {

 Map.Entry<Integer, List<Person>> persons = (Map.Entry) it.next();

 System.out.println("Age " + persons.getKey() + " = " + persons.getValue().size());

}

上面的 code,首先生成 100 人的信息,然后按照年龄归组,相同年龄的人放到同一个 list 中,可以看到如下的输出:

Age 0 = 2

Age 1 = 2

Age 5 = 2

Age 8 = 1

Age 9 = 1

Age 11 = 2

……

清单 26. 按照未成年人和成年人归组

Map<Boolean, List<Person>> children = Stream.generate(new PersonSupplier()).

 limit(100).

 collect(Collectors.partitioningBy(p -> p.getAge() < 18));

System.out.println("Children number: " + children.get(true).size());

System.out.println("Adult number: " + children.get(false).size());

输出结果:

Children number: 23

Adult number: 77

在使用条件“年龄小于 18”进行分组后可以看到,不到 18 岁的未成年人是一组,成年人是另外一组。partitioningBy 其实是一种特殊的 groupingBy,它依照条件测试的是否两种结果来构造返回的数据结构,get(true) 和 get(false) 能即为全部的元素对象。

 

java.util.function

    java.util.function 提供了一些已经定义好的函数式接口,可以自己看下包的内容,主要作用是,当你需要使用函数式编程的时候,减少自定义的函数式接口,直接用系统提供的。

 

引用

 本文引用了如下博文

http://www.cnblogs.com/JohnTsai/p/5806194.html

http://www.importnew.com/16436.html

http://www.cnblogs.com/WJ5888/p/4618465.html

https://www.ibm.com/developerworks/cn/java/j-lo-java8streamapi/

 

    

转载于:https://my.oschina.net/zzxzzg/blog/870546

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值