Java8 学习

Java8 新特性

Java并没有没落,人们很快就会发现这一点 本教程将对java8新增的特性进行简明的较少,使得大家对于java8新特性有一个比较直观的印象。

接口中定义默认方法与静态方法

java在jdk1.8前仅允许在接口中使用抽象方法和静态常量定义。Java8将允许在接口中定义默认方法和静态方法。接口中的默认方法主要是用于解决jdk的向下兼容问题。如Collection接口中添加了新的stream方法,则jdk1.7之前的代码是无法在1.8平台上编译通过,故提供了接口中的默认方法。
###接口中的默认方法 Java 8 允许我们使用default关键字,为接口声明添加非抽象的方法实现。这个特性又被称为扩展方法。该方法在函数接口或者是非函数接口都可以实现。

public interface DefaultInterface {
    default void Print() {
        System.out.println("hello world");
    }
}
public interface Formula {
    String TEXT = "text";
    double calculate(int a);
    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

####默认方法继承

重写与普通方法的大致一致。多重继承时,使用增强的super

####接口中的静态方法 该方法主要针对类库的开发人员,便于工具类(接口)的设计和开发。

public interface Formula { 
    static void print() { 
        System.out.println("hello, world"); 
    } 
}

###函数接口(Functional Interfaces) 本节作为lambda表达式的基础部分,内容相对较为简单。Runnable接口、Callable接口、Comparator接口, 和Java中定义的其它大量接口——在Java 8中我们称为函数式接口:它们是只需要实现一个方法去满足需求的接口。
每一个lambda都能够通过一个特定的函数式接口,与一个给定的类型进行匹配。同时,任意只包含一个抽象方法的接口,我们都可以用来做成lambda表达式。为了让你定义的接口满足要求,你应当在接口前加上@FunctionalInterface 标注。编译器会注意到这个标注,如果你的接口中定义了第二个抽象方法的话,编译器会抛出异常。

@FunctionalInterface
public interface Converter<F, T> {
    T convert(F from);
}
 
Converter<Integer, String> converter = from -> String.valueOf(from);
System.out.println(converter.convert(33));

Java中重要的函数接口

函数名| 参数| 返回类型| 示例| 对应Guava| 备注 ----|--- Predicate<T>| T |boolean |吃了没? |Predicate<T>| 断言,判断表达式是否为真 Consumer<T>| T |void ||| Function<T,R>| T |R |String 转int| Function<T,R> | Supplier<T> |无| T |工厂方法 || UnaryOperator<T>| T |T| 逻辑非(!)| | BinaOperator<T>| (T,T)| T| 求两个数的乘积 ||

###方法引用 方法引用语法格式有以下三种: objectName::instanceMethod ClassName::staticMethod ClassName::instanceMethod 对于函数式接口的代码块,还有更简单的实现方式,具体如下

####静态方法引用

Converter<Integer, String> methodReference = String::valueOf;
        System.out.println(methodReference.convert(21));

####非静态方法引用

class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
 
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

####构造函数引用 对于构造函数(构造函数本质上也是一种静态方法),此处的用法比较精妙。且看示例:

public class Person {
    String firstName;
    String lastName;
 
    public Person() {}
 
    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}
 
interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}
 
PersonFactory personFactory = Person::new;
Person person = personFactory.create("黄", "大仙");

上面展示的代码中,我们首先创建了一个Person类,和该类的一个工厂接口用于创建Person对象。最终通过构造函数引用来把所有东西拼到一起,而不是像以前一样,通过手动实现一个工厂来这么做。
我们通过Person::new来创建一个Person类构造函数的引用。Java编译器会自动地选择合适的构造函数(person(string, string))来匹配PersonFactory.create函数的签名,并选择正确的构造函数形式。

PersonFatory为一个“函数接口”, 且Person::new 返回的为一个函数。具体请看上节内容。

##lambda表达式 ###认识lambda表达式

维基百科对lambda表达式的解释:没找到

简单的说lambda表达式:一段带有输入参数的可执行语句块。

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  //.............
  return statmentM;
}   
//无参数的lambda表达式   
Runnable noArgument = () -> System.out.print("hello, world");
ActionListener oneArgument = event -> System.out.print("button click");
Runnable multiStatement = () -> {
      System.out.print("hello ");
      System.out.print("world");
};
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

注意:1. 代码块和普通方法遵循的规则一致, 可以使用返回或异常进行退出。

####体验lambda表达式

举个栗子:将list中的内容由小写变为大写

List<String> names = new ArrayList<>();
names.add("huang");
names.add("chuancong");
List<String> lowercaseNames = new ArrayList<>();
for (String name : names) {
  lowercaseNames.add(name.toLowerCase());
}

List<String> names = new ArrayList<>();
names.add("huang");
names.add("chuancong");
List<String> lowercaseNames = FluentIterable.from(names).transform(new Function<String, String>() {
  @Override
  public String apply(String name) {
    return name.toLowerCase();
  }
}).toList();

List<String> names = new ArrayList<>();
names.add("huang");
names.add("chuancong");
List<String> lowercaseNames = names.stream().map((String name) -> {return name.toLowerCase();}).collect(Collectors.toList());

###Lambda表达式作用域 ####访问本地变量 对于JDK1.7匿名内部类而言,如果要使用外部的变量,则该变量需要显式的定义为final类型。

final String name = "黄传聪";
Button button = new Button();
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("hi: " + name);
    }
});
Java 1.8之后,该final修饰符可以省略,但是、but java8虽然放松了限制,但是该变量还是“既成事实上必须是final”。
String name = "黄传聪";
name += "黄传聪";
button.addActionListener(e -> System.out.println("hi: " + name));

该代码会编译失败, 总而言之,java1.8虽然放松了 限制,但是在编译时仍要保证该变量为final类型。因此,lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。

访问属性和静态变量

对于静态变量和属性的访问方式同匿名内部类一致。


class Lambda4 {
    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

###访问接口的默认方法 接口的默认方法为java1.8新加的属性,具体可参看第一小节。接口中默认方法,可以被其实现类、或匿名内部类进行调用。但是,Lambda表达式不能使用。

Lambda表达式中的this

#####类型推断 Lambda表达式中的类型推断,实际上是java7中就引入的目标类型推断的扩充。Java7中提供了<>操作符,使得javac可根据该操作符推断出泛型参数的类型。如:

Map<String, String> map = new HashMap<String, String>();
Map<String, String> map = new HashMap<>();
 
private void useMap(Map<String, String> map);
useMap(new HashMap<>);

lambda表达式可省略所有的参数类型,javac可以根据上下文推断参数类型,但是,也有推断不出的情景,且看下文方法重载。

方法重载

方法的重载可能会导致lambda表达类型推断失败。 对于推导过程:

  • 如果只有一个可能的目标类型,由相应的函数接口李德参数类型推导出具体类型;
  • 如果有多个可能的目标类型。由最具体的类型推导出;
  • 如果有多个可能的目标类型,且最具体的类型不明确,则需认为指定。
  • 发生场景:Lambda表达式作为参数进行传递时..
@FunctionalInterface
public interface IntPredicate {
    public boolean test(int value);
}
 
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}
 
public boolean predicate(Predicate<Integer> predicate) {
    return predicate.test(1);
}

public boolean predicate(IntPredicate predicate) {
    return predicate.test(1);
}
application.predicate(a -> a > 1);

该段代码在Intellij 中提示:
输入图片说明
表示,推断系统无法推断lambda表达式的具体类型,因为该表达式对于两个方法的参数都契合。

Lambda表达式与匿名内部类之间的区别请参考

##Stream stream流 API是java8中最大的一次改动,我们将从 什么是流,流的作用及流的实战等方面进行相关的讨论。

A sequence of elements supporting sequential and parallel aggregate operations.

针对java给出的定义我们进行简单的解释: Stream是元素的集合,类似于Iterator;可以通过顺序和并行的方式对流进行汇聚操作。
大家可以把Stream当成一个高级版本的Iterator。原始版本的Iterator,用户只能一个一个的遍历元素并对其执行某些操作;高级版本的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于10的字符串”、“获取每个字符串的首字母”等,具体这些操作如何应用到每个元素上,就给Stream就好了!
举个栗子:

List<Integer> integerList =new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(null);
integerList.stream().filter(num -> num != null).count();

该例子的左右:统计integerList中元素不为null的值得数量。
输入图片说明

对于该例子的拆解,可以发现使用stream流可以分为以下三个步骤:

创建Stream;

转换Stream,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换);
对Stream进行聚合(Reduce)操作,获取想要的结果;
输入图片说明

创建stream

常见的创建stream的方法有两种
通过Stream接口的静态工厂方法;
通过Collection接口的默认方法把一个Collection对象转换成Stream
通过Stream接口的静态工厂方法创建
通过该接口中提供的静态方法 of()进行创建
其源码如下,该方法存在两个重载方法。

public static<T> Stream<T> of(T t) {
    return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

@SafeVarargs
@SuppressWarnings("varargs") // Creating a stream from an array is safe
public static<T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}
使用该方法创建Stream的示例如下:
Stream<Integer> integerStream = Stream.of(1);
 Stream<String> stringStream = Stream.of("1", "2");

使用generator方法创建流

Returns an infinite sequential unordered stream where each element is generated by the provided Supplier
该方法返回一个无限长度的stream, 该方法适合生成 固定元素或随机元素的流。如:
Stream.generate(new Supplier<String>() {
    @Override
    public String get() {
        return "1";
    }
});
 
Stream.generate(new Supplier<Double>() {
    @Override
    public Double get() {
        return Math.random();
    }
});
Stream.generate(() -> Math.random());
Stream.generate(Math::random);

三条语句的作用都是一样的,这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Stream的limit()方法来用。

使用iterator生成stream

也是生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环

Returns an infinite sequential ordered Stream produced by iterative application of a function f to an initial element seed, producing a Stream consisting of seed, f(seed), f(f(seed)), etc. The first element (position 0) in the Stream will be the provided seed. For n > 0, the element at position n, will be the result of applying the function f to the element at position n - 1.

没看懂,不讲了

使用 Collection默认方法创建

该种方法应该是使用比较频繁的方法,流大多数的使用场景在于对容器中得数据的处理。

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

示例:

List<Integer> integerList =new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(null);
integerList.stream();

转换Stream

这一节是纯抄的的,抄的,抄的 转换Stream其实就是把一个Stream通过某些行为转换成一个新的Stream。Stream接口中定义了几个常用的转换方法,下面我们挑选几个常用的转换方法来解释。

distinct

对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;
distinct方法示意图(以下所有的示意图都要感谢RxJava项目的doc中的图片给予的灵感, 如果示意图表达的有错误和不准确的地方,请直接联系我。):
输入图片说明

filter:

对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素; filter方法示意图: 输入图片说明 #####map: 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble, 主要用于解决自动装箱、拆箱的开销。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的 Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗; map方法示意图: 输入图片说明

flatMap:

和map类似,可以将其看作为列表的合并操作
flatMap方法示意图:
输入图片说明
示例代码:

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());
outputStream.forEach(System.out::println);
//outputStream.forEach(t -> System.out.println(t));

运行结果为:
输入图片说明
起作用:相当于将多个容器对应的流合并到同一个流中。

#####peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;
peek方法示意图:
输入图片说明
#####limit: 对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;
limit方法示意图:
输入图片说明

skip:

返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

输入图片说明
#####在一起,在一起!

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).
   			distinct().mapToInt(num -> num * 2).
               peek(System.out::println).skip(2).limit(4).sum());

这段代码演示了上面介绍的所有转换方法(除了flatMap),简单解释一下这段代码的含义:给定一个Integer类型的List,获取其对应的Stream对象,然后进行过滤掉null,再去重,再每个元素乘以2,再每个元素被消费的时候打印自身,在跳过前两个元素,最后去前四个元素进行加和运算(解释一大堆,很像废话,因为基本看了方法名就知道要做什么了。这个就是声明式编程的一大好处!)。大家可以参考上面对于每个方法的解释,看看最终的输出是什么。
######性能问题

有些细心的同学可能会有这样的疑问:在对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在汇聚操作(见下节)的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在汇聚操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。

##Reduce 该操作是对Stream流进行的最后一步操作。

A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list. The streams classes have multiple forms of general reduction operations, called reduce() and collect(), as well as multiple specialized reduction forms such as sum(), max(), or count().
简单翻译一下:汇聚操作(也称为折叠)接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。

下面会分两部分来介绍汇聚操作:

  1. 可变汇聚:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;
  2. 其他汇聚:除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;

可变汇聚

可变汇聚对应的只有一个方法:collect,正如其名字显示的,它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。先看一下最通用的collect方法的定义(还有其他override方法):

<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

先来看看这三个参数的含义:Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。看晕了?来段代码!

List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
       List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
               collect(() -> new ArrayList<Integer>(),
                       (list, item) -> list.add(item),
                       (list1, list2) -> list1.addAll(list2));

上面这段代码就是对一个元素是Integer类型的List,先过滤掉全部的null,然后把剩下的元素收集到一个新的List中。进一步看一下collect方法的三个参数,都是lambda形式的函数(上面的代码可以使用方法引用来简化,留给读者自己去思考)。 第一个函数生成一个新的ArrayList实例;
第二个函数接受两个参数,第一个是前面生成的ArrayList对象,二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。第二个函数被反复调用直到原stream的元素被消费完毕;
第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;
但是上面的collect方法调用也有点太复杂了,没关系!我们来看一下collect方法另外一个override的版本,其依赖Collector

1	<R, A> R collect(Collector<? super T, A, R> collector);

这样清爽多了!少年,还有好消息,Java8还给我们提供了Collector的工具类–Collectors,其中已经定义了一些静态工厂方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中。这样的静态方法还有很多,这里就不一一介绍了,大家可以直接去看JavaDoc。下面看看使用Collectors对于代码的简化:

List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).
                collect(Collectors.toList());
JavaDoc示例:

     // Accumulate names into a List
     List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());

     // Accumulate names into a TreeSet
     Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));

     // Convert elements to strings and concatenate them, separated by commas
     String joined = things.stream()
                           .map(Object::toString)
                           .collect(Collectors.joining(", "));

     // Compute sum of salaries of employee
     int total = employees.stream()
                          .collect(Collectors.summingInt(Employee::getSalary)));

     // Group employees by department
     Map<Department, List<Employee>> byDept
         = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment));

     // Compute sum of salaries by department
     Map<Department, Integer> totalByDept
         = employees.stream()
                    .collect(Collectors.groupingBy(Employee::getDepartment,
                                                   Collectors.summingInt(Employee::getSalary)));

     // Partition students into passing and failing
     Map<Boolean, List<Student>> passingFailing =
         students.stream()
                 .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

其他汇聚

除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch; – reduce方法:reduce方法非常的通用,后面介绍的count,sum等都可以使用其实现。reduce方法有三个override的方法,本文介绍两个最常用的,最后一个留给读者自己学习。先来看reduce方法的第一种形式,其方法定义如下:

Optional<T> reduce(BinaryOperator<T> accumulator);

接受一个BinaryOperator类型的参数,在使用的时候我们可以用lambda表达式来。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce((sum, item) -> sum + item).get());

可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional,这是Java8防止出现NPE的一种可行方法,后面的文章会详细介绍,这里就简单的认为是一个容器,其中可能会包含0个或者1个对象。 这个过程可视化的结果如图: 输入图片说明 reduce方法还有一个很常用的变种:
T reduce(T identity, BinaryOperator<T> accumulator);

这个定义上上面已经介绍过的基本一致,不同的是:它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值。下面直接给出例子,就不再做说明了。

List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().reduce(0, (sum, item) -> sum + item));

– count方法:获取Stream中元素的个数。比较简单,这里就直接给出例子,不做解释了。

 List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println("ints sum is:" + ints.stream().count());
– 搜索相关

– allMatch:是不是Stream中的所有元素都满足给定的匹配条件
– anyMatch:Stream中是否存在任何一个元素满足匹配条件
– findFirst: 返回Stream中的第一个元素,如果Stream为空,返回空Optional
– noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件
– max和min:使用给定的比较器(Operator),返回Stream中的最大|最小值
下面给出allMatch和max的例子,剩下的方法读者当成练习。

List<Integer&gt; ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(ints.stream().allMatch(item -> item < 100));
ints.stream().max((o1, o2) -&gt; o1.compareTo(o2)).ifPresent(System.out::println);

Stream 小结

Stream到此告一段落,简单对Stream进行简单的总结:
上文中讲述的Stream都是顺序操作,当然Stream同样支持并行操作,具体可自己查阅 ;
Stream的使用,包括三个环节,同时有几个比较重要的状态需要了解一下。转换Stream,可以认为是Stream的中间状态,jdk描述如下:

Intermediate operations return a new stream. They are always lazy; executing an intermediate operation such as filter() does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.

正如上文中所言,该状态的Stream为了效率起见,并不会立即执行,只有执行到终态时才触发。处于该状态的操作主要为上文中提到的Stream的转换操作。

###参考文献:
http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html
http://winterbe.com/posts/2014/03/16/java-8-tutorial/
http://ifeve.com/java-8-tutorial-2/
http://www.importnew.com/10360.html
http://ifeve.com/lambda/
http://ifeve.com/stream/
http://ifeve.com/java8/#more-21384
http://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

猜你喜欢

转载自my.oschina.net/u/1011659/blog/874071