入门lambda表达式(二)

引文

  此次主要介绍Java 8的Stream以及如何与lambda配合使用。Stream做为Java 8的一大亮点,它与java.io包里的InputStream和OutputStream是彻底不一样的概念。Java 8中的Stream是对集合对象功能的加强,它专一于对集合对象进行各类很是便利、高效的聚合操做,或者大批量数据操做。Stream API借助于一样新出现的lambda表达式,极大的提升编程效率和程序可读性。能够说,Stream的出现,彻底改变了处理集合的方式。但愿你们在看完这篇文章后,能抛弃以前对集合用Iterator遍历并完成相关的聚合操做那种笨拙的方式,改用流来处理。
  先用一个例子让你们感觉一下Stream的便捷。假设有这样一个Book类:java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {

    private Integer id;

    private String name;

    private String type;

    private Double price;
}
复制代码

  如今有这样一个业务场景:要发现种类为“计算机”的全部图书,而后返回以价格增序排序好的图书ID集合:
传统方式算法

public class StreamTest {

    public static void main(String[] args) {
        List<Book> bookList = new ArrayList<>();
        bookList.addAll(Arrays.asList(new Book(1, "Java核心技术", "计算机", 90.0),
                new Book(2, "Java编程思想", "计算机", 100.0),
                new Book(3, "浮生六记", "文学", 50.0)));
        List<Book> computerBooks = new ArrayList<>();
        for (Book b : bookList) {
            if (b.getType().equals("计算机")) {
                computerBooks.add(b);
            }
        }
        Collections.sort(computerBooks, new Comparator<Book>() {
            @Override
            public int compare(Book b1, Book b2) {
                return b1.getPrice().compareTo(b2.getPrice());
            }
        });
        List<Integer> bookIds = new ArrayList<>();
        for (Book b : computerBooks) {
            bookIds.add(b.getId());
        }
    }
}
复制代码

使用Stream编程

public class StreamTest {

    public static void main(String[] args) {
        List<Book> bookList = new ArrayList<>();
        bookList.addAll(Arrays.asList(new Book(1, "Java核心技术", "计算机", 90.0),
                new Book(2, "Java编程思想", "计算机", 100.0),
                new Book(3, "浮生六记", "文学", 50.0)));
        List<Integer> bookIds = bookList.stream()
                .filter(b -> b.getType().equals("计算机"))
                .sorted(Comparator.comparing(Book::getPrice))
                .map(Book::getId)
                .collect(Collectors.toList());
    }
}
复制代码

  能够看到,原先繁琐的操做,在使用了Stream后,只用一句话就解决了。那Stream到底是什么呢?数据结构

Stream原理

  Stream不是集合元素或者数据结构,它并不保存数据,而是有关数据的算法和计算的。能够把Stream理解成一个高级版本的Iterator。原始版本的Iterator,用户只能显式地一个一个遍历元素并对其执行某些操做;高级版本的Stream,用户只要给出须要对其包含的元素执行什么操做,好比 “过滤掉长度大于10的字符串”、“获取每一个字符串的首字母”等,Stream会隐式地在内部进行遍历,作出相应的数据转换(ps:也能够把Stream理解成一种处理数据的风格,这种风格将要处理的元素集合看做一种流,流在管道中传输,而且能够在管道的节点上进行处理,好比筛选、排序、聚合等)。
  可能上面说的有些抽象,下面给出一个具体的例子。有这样一段代码:多线程

List<Integer> nums = new ArrayList<>();
nums.addAll(Arrays.asList(1, null, 3, 4, null, 6));
nums.stream().filter(num -> num != null)).count();
复制代码

  上面这段代码的目的是获取一个List中不为null的元素的个数。经过这段代码,咱们来剖析一下Stream的结构: 并发

图片未加载成功
  上图中三个方框,也就是咱们使用Stream的三个基本步骤。红色框中的语句负责建立一个Stream实例;绿色框中的语句对集合元素进行数据转换,每次转换原有Stream对象不改变,返回一个新的Stream对象(能够有屡次转换),这就容许对其操做能够像链条同样排列,变成一个管道;蓝色框中的语句把Stream里包含的内容按照某种算法来汇聚成一个值。再形象一点,就是这样的:
图片未加载成功

  下面,我具体说一下这三个步骤:
  建立Stream有不少方法,你们感兴趣的话能够自行去查。这里我重点说一下如何把一个Collection对象转换成Stream。一般状况下,调用Collection.stream()和Collection.parallelStream()分别产生序列化流(普通流)和并行流。并行和并发的概念,你们应该都清楚。并发是指多线程有竞争关系,在单核的状况下某一时刻只有一个线程运行;而并行是指在多核的状况下同时运行,单核谈并行是无心义的。使用并行方式去遍历时,数据会被分红多个段,其中每个都在不一样的线程中处理,而后将结果一块儿输出。Stream的并行操做依赖于Java 7中引入的Fork/Join框架来拆分任务和加速处理过程。须要注意的是,并行不必定快,尤为在数据量很小的状况下,可能比普通流更慢。只有在大数据量和多核的状况下才考虑并行流。
  流的操做类型分为两种:

  • 中间操做(Intermediate operation):一个流能够后面跟随零个或多个intermediate操做。其目的主要是打开流,作出某种程度的数据映射/过滤,而后返回一个新的流,交给下一个操做使用。这类操做都是惰性化的(lazy),就是说,仅仅调用到这类方法,并无真正开始流的遍历。三个基本步骤中的转换属于中间操做,用于把一个Stream经过某些行为转换成一个新的Stream。
  • 最终操做(Terminal Operation):一个流只能有一个terminal操做,当这个操做执行后,流就被使用“光”了,没法再被操做,因此这一定是流的最后一个操做。Terminal操做的执行,才会真正开始流的遍历,而且会生成一个结果。三个基本步骤中的聚合属于最终操做,用于接受一个元素序列做为输入,反复使用某个合并操做,把序列中的元素合并成一个汇总的结果。好比查找一个数字列表的总和或者最大值,或者把这些数合并成一个List对象。

  须要强调的是,在对一个Stream进行屡次Intermediate操做时,每次都对Stream的每一个元素进行转换,但实质上并无作N(转换次数)次for循环。转换操做都是lazy的,多个转换操做只会在Terminal操做的时候融合起来,一次循环完成。咱们能够这样简单的理解,Stream里有个操做函数的集合,每次转换操做就是把转换函数放入这个集合中,在Terminal操做的时候循环Stream对应的集合,而后对每一个元素执行全部的函数。
  至于有哪些转换和聚合方法呢,你们能够自行查找。推荐一篇文章:ifeve.com/stream/ ,里面用示意图说明了几个典型的转换和聚合方法,很形象。框架

Stream用法示例

map/flatMap

  map的做用是把input Stream的每个元素,映射成output Stream的另一个元素,例如:ide

// 大小写转换
List<String> wordList = Arrays.asList("a", "b", "c");
List<String> newWorldList = wordList.stream()
        .map(String::toUpperCase)
        .collect(Collectors.toList());
复制代码

  map生成的是1:1映射,每一个输入元素,都按照规则转换成为另一个元素。还有一些场景,是一对多映射关系的,这时须要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());
复制代码

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

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);
复制代码

  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);
复制代码

match

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

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

  它们都不是要遍历所有元素才能返回结果。例如allMatch只要一个元素不知足条件,就skip剩下的全部元素,返回false。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);
复制代码

  emmm就先写这几个吧,其实Stream另外一个便捷的地方在于它的一些方法能够直接根据方法名来判断用途。固然,想要掌握Stream的用法,仍是要——多用。
  最后,说几个lambda表达式须要注意的地方:

  • lambda表达式内可使用方法引用(即那个“::”),仅当该方法不修改lambda表达式提供的参数。然而,若对参数有任何修改,则不能使用方法引用,而需键入完整的lambda表达式。
  • lambda内部可使用静态、非静态和局部变量,但只能是final的。这就是说不能在lambda内部修改定义在域外的变量(ps:Java 8对这个限制作了优化,能够不用显式使用final修饰,可是编译器隐式当成final来处理)。
  • 上次说到能够用lambda表达式来代替匿名内部类,但这二者仍是有明显区别的:一是this关键字,匿名类的this关键字指向匿名类,而lambda表达式的this关键字指向包围lambda表达式的类;二是编译方式不一样,Java编译器将lambda表达式编译成类的私有方法。

  初识lambda表达式,可能有的人还会以为很陌生,可是在掌握它的用法以后,必定能感觉到它的强大之处!

相关文章
相关标签/搜索