Java 8新特性(二):Stream API

Java
8中的java.util.zip.ZipFile包中提供了stream方法,能够非常容易的获取ZIP压缩包中的条目。在这篇文章中,我会通过一系列的示例来展示我们可以非常快速的遍历ZIP文件中的条目。

澳门新葡亰3522平台游戏 1

注意:为了在这篇博客中做演示,我从GitHub上以ZIP文件的形式下载了我的一个项目,放在了c:/tmp目录下。

本文首发于一书生VOID的博客。
原文链接:Java 8新特性(二):Stream
API

Java7之前的做法

在Java7之前,读取一个ZIP文件中的条目的做法,恩……需要一点点小技巧。当你看到下面的代码的时候,大概就会开始有点讨厌Java了。

public class Zipper {
    public void printEntries(PrintStream stream, String zip)  {
        ZipFile zipFile = null;
        try {
            zipFile = new ZipFile(zip);
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                stream.println(zipEntry.getName());
            }
        } catch (IOException e) {
            // error while opening a ZIP file
        } finally {
            if (zipFile != null) {
                try {
                    zipFile.close();
                } catch (IOException e) {
                    // do something
                }
            }
        }
    }
}

Java 7的做法

多谢有了try-with-resources这样新的try代码块的写法,在Java
7中的代码变得稍微好了一些,但我们还是被“强迫”来使用Enumeration来遍历ZIP压缩包中的条目:

public class Zipper {
    public void printEntries(PrintStream stream, String zip) {
        try (ZipFile zipFile = new ZipFile(zip)) {
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                stream.println(zipEntry.getName());
            }
        } catch (IOException e) {
            // error while opening a ZIP file
        }
    }
}

本篇文章继续介绍Java 8的另一个新特性——Stream API。新增的Stream
API与InputStreamOutputStream是完全不同的概念,Stream
API是对Java中集合操作的增强,可以利用它进行各种过滤、排序、分组、聚合等操作。Stream
API配合Lambda表达式可以加大的提高代码可读性和编码效率,Stream
API也支持并行操作,我们不用再花费很多精力来编写容易出错的多线程代码了,Stream
API已经替我们做好了,并且充分利用多核CPU的优势。借助Stream
API和Lambda,开发人员可以很容易的编写出高性能的并发处理程序。

使用Strean API

真正有意思的是从Java 8开始,Java
8提供在java.util.zip.ZipFile包中提供新的stream方法,能够返回ZIP压缩包中的条目的有序的流,使得Java在处理ZIP压缩包时有了更多的选择。前文提到的读取压缩包的条目的代码可以改写成如下简单的形式:

public class Zipper {
    public void printEntries(PrintStream stream, String zip) {
        try (ZipFile zipFile = new ZipFile(zip)) {
            zipFile.stream()
                    .forEach(stream::println);
        } catch (IOException e) {
            // error while opening a ZIP file
        }
    }
}

如下文所示,有了Stream API,我们有了更多更有趣的方式来处理ZIP文件。

1.Stream API简介

Stream API是Java
8中加入的一套新的API,主要用于处理集合操作,不过它的处理方式与传统的方式不同,称为“数据流处理”。流(Stream)类似于关系数据库的查询操作,是一种声明式操作。比如要从数据库中获取所有年龄大于20岁的用户的名称,并按照用户的创建时间进行排序,用一条SQL语句就可以搞定,不过使用Java程序实现就会显得有些繁琐,这时候可以使用流:

List<String> userNames =
        users.stream()
        .filter(user -> user.getAge() > 20)
        .sorted(comparing(User::getCreationDate))
        .map(User::getUserName)
        .collect(toList());

可以把流跟集合做一个比较。在Java中,集合是一种数据结构,或者说是一种容器,用于存放数据,流不是容器,它不关心数据的存放,只关注如何处理。可以把流当做是Java中的Iterator,不过它比Iterator强大多了。

流与集合另一个区别在于他们的遍历方式,遍历集合通常使用for-each方式,这种方式称为外部迭代,而流使用澳门新葡亰3522平台游戏,内部迭代方式,也就是说它帮你把迭代的工作做了,你只需要给出一个函数来告诉它接下来要干什么:

// 外部迭代
List<String> list = Arrays.asList("A", "B", "C", "D");
for (String str : list) {
    System.out.println(str);
}

// 内部迭代
list.stream().forEach(System.out::println);

在这个大数据的时代,数据变得越来越多样化,很多时候我们会面对海量数据,并对其做一些复杂的操作(比如统计,分组),依照传统的遍历方式(for-each),每次只能处理集合中的一个元素,并且是按顺序处理,这种方法是极其低效的。你也许会想到并行处理,但是编写多线程代码并非易事,很容易出错并且维护困难。不过在Java
8之后,你可以使用Stream API来解决这一问题。

Stream
API将迭代操作封装到了内部,它会自动的选择最优的迭代方式,并且使用并行方式处理时,将集合分成多段,每一段分别使用不同的线程处理,最后将处理结果合并输出。

需要注意的是,流只能遍历一次,遍历结束后,这个流就被关闭掉了。如果要重新遍历,可以从数据源(集合)中重新获取一个流。如果你对一个流遍历两次,就会抛出java.lang.IllegalStateException异常:

List<String> list = Arrays.asList("A", "B", "C", "D");
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.forEach(System.out::println); // 这里会抛出java.lang.IllegalStateException异常,因为流已经被关闭

流通常由三部分构成:
1.
数据源:数据源一般用于流的获取,比如本文开头那个过滤用户的例子中users.stream()方法。
2.
中间处理:中间处理包括对流中元素的一系列处理,如:过滤(filter()),映射(map()),排序(sorted())。
3.
终端处理:终端处理会生成结果,结果可以是任何不是流值,如List<String>;也可以不返回结果,如stream.forEach(System.out::println)就是将结果打印到控制台中,并没有返回。

对ZIP压缩包的内容进行过滤和排序

public void printEntries(PrintStream stream, String zip) {
    try (ZipFile zipFile = new ZipFile(zip)) {
        Predicate<ZipEntry> isFile = ze -> !ze.isDirectory();
        Predicate<ZipEntry> isJava = ze -> ze.getName().matches(".*java");
        Comparator<ZipEntry> bySize = 
                (ze1, ze2) -> Long.valueOf(ze2.getSize() - ze1.getSize()).intValue();
        zipFile.stream()
                .filter(isFile.and(isJava))
                .sorted(bySize)
                .forEach(ze -> print(stream, ze));
    } catch (IOException e) {
        // error while opening a ZIP file
    }
}

private void print(PrintStream stream, ZipEntry zipEntry) {
    stream.println(zipEntry.getName() + ", size = " + zipEntry.getSize());
}

在迭代ZIP压缩包的条目时,我检查了这个条目是否是一个文件并且是否匹配一个给定的字段(为了简单,直接把匹配字段硬编码在代码中了),然后利用一个给定的比较器,对这些条目按照大小进行了排序。

2.创建流

创建流的方式有很多,具体可以划分为以下几种:

为ZIP压缩包创建文件索引

在这个例子中,我把ZIP压缩包中的条目按照文件名的首字母分组,建立形如Map<String, List<ZipEntry>>的索引,预想的结果应该看起来像这样简单:

a = [someFile/starting/with/an/A]
u = [someFile/starting/with/an/U, someOtherFile/starting/with/an/U]

同样,使用Stream API来实现这个功能非常简单:

public void printEntries(PrintStream stream, String zip) {
    try (ZipFile zipFile = new ZipFile(zip)) {
        Predicate<ZipEntry> isFile = ze -> !ze.isDirectory();
        Predicate<ZipEntry> isJava = ze -> ze.getName().matches(".*java");
        Comparator<ZipEntry> bySize =
            (ze1, ze2) -> Long.valueOf(ze2.getSize()).compareTo(Long.valueOf(ze1.getSize()));

        Map<String, List<ZipEntry>> result = zipFile.stream()
                .filter(isFile.and(isJava))
                .sorted(bySize)
                .collect(groupingBy(this::fileIndex));

        result.entrySet().stream().forEach(stream::println);

    } catch (IOException e) {
        // error while opening a ZIP file
    }
}

private String fileIndex(ZipEntry zipEntry) {
    Path path = Paths.get(zipEntry.getName());
    Path fileName = path.getFileName();
    return fileName.toString().substring(0, 1).toLowerCase();
}

2.1 由值创建流

使用静态方法Stream.of()创建流,该方法接收一个变长参数:

Stream<Stream> stream = Stream.of("A", "B", "C", "D");

也可以使用静态方法Stream.empty()创建一个空的流:

Stream<Stream> stream = Stream.empty();

在ZIP压缩包的文件中查找字段

在这最后一个例子中,我在压缩包中的查找所有以.java结尾的且包含@Test字段的文件,这次,我将利用BufferedReader类的lines方法来实现,这个lines方法按行返回文件流。

public void printEntries(PrintStream stream, String zip) {

    try (ZipFile zipFile = new ZipFile(zip)) {
        Predicate<ZipEntry> isFile = ze -> !ze.isDirectory();
        Predicate<ZipEntry> isJava = ze -> ze.getName().matches(".*java");

        List<ZipEntry> result = zipFile.stream()
                .filter(isFile.and(isJava))
                .filter(ze -> containsText(zipFile, ze, "@Test"))
                .collect(Collectors.toList());

        result.forEach(stream::println);

    } catch (IOException e) {
        // error while opening a ZIP file
    }
}

private boolean containsText(ZipFile zipFile, ZipEntry zipEntry, String needle) {
    try (InputStream inputStream = zipFile.getInputStream(zipEntry);
         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {

        Optional<String> found = reader.lines()
                .filter(l -> l.contains(needle))
                .findFirst();

        return found.isPresent();

    } catch (IOException e) {
        return false;
    }
}

2.2 由数组创建流

使用静态方法Arrays.stream()从数组创建一个流,该方法接收一个数组参数:

String[] strs = {"A", "B", "C", "D"};
Stream<Stream> stream = Arrays.stream(strs);

总结

在我看来,Stream
API提供了一个强大的并且相对更容易的方案来解决遍历ZIP压缩包中的条目的问题。

本文中出现的例子只是用来演示说明Stream
API的用法的,都是相对容易的,但我希望你能够喜欢这些例子,并且觉得他对你有用。

2.3 通过文件生成流

使用java.nio.file.Files类中的很多静态方法都可以获取流,比如Files.lines()方法,该方法接收一个java.nio.file.Path对象,返回一个由文件行构成的字符串流:

Stream<String> stream = Files.lines(Paths.get("text.txt"), Charset.defaultCharset());

2.4 通过函数创建流

java.util.stream.Stream中有两个静态方法用于从函数生成流,他们分别是Stream.generate()Stream.iterate()

// iteartor
Stream.iterate(0, n -> n + 2).limit(51).forEach(System.out::println);

// generate
Stream.generate(() -> "Hello Man!").limit(10).forEach(System.out::println);

第一个方法会打印100以内的所有偶数,第二个方法打印10个Hello Man!。值得注意的是,这两个方法生成的流都是无限流,没有固定大小,可以无穷的计算下去,在上面的代码中我们使用了limit()来避免打印无穷个值。

一般来说,iterate()用于生成一系列值,比如生成以当前时间开始之后的10天的日期:

Stream.iterate(LocalDate.now(), date -> date.plusDays(1)).limit(10).forEach(System.out::println);

generate()方法用于生成一些随机数,比如生成10个UUID:

Stream.generate(() -> UUID.randomUUID().toString()).limit(10).forEach(System.out::println);