这次让我们来看看如何使用Java 8 Streams(流)来实现一张数据透视表。通常情况下,原始数据本身一般并不容易被人们所读懂,因此我们需要进行一些数据聚合操作来辨别出原始数据中的各种规律模式。而数据透视表是这样一种工具,它运用聚合的方法来展示出各种可视的图形和图表。 在过往的文章中,我们曾展示过如何使用Java 8 Streams进行类似SQL的效果对原始数据进行切割与分解。而的文章建立在那些例子之上,如果你觉得这些对你有些难度的话,我建议你先通过如下链接浏览那两篇文章。 https://dzone.com/articles/java-streams-groupingby-examples https://dzone.com/articles/using-java-collectors 如果你不喜欢使用这种“原始数据”的方法来创建一张数据透视表,而选择使用Excel来实现的话,我为你提供了另外的备选方案,请参考如下链接: http://www.novixys.com/blog/excel-pivot-table-using-apache-poi/ 将CSV数据表示为POJO 我们用如下的POJO(简单的Java对象,Plain Ordinary Java Object)来表示棒球运动员及其工资。 public class Player { private int year; private String teamID; private String lgID; private String playerID; private int salary; // defined getters and setters here } 它的数据来自于一个简单的CSV文件,该文件内既没有引用的字段,也没有多行的字段,而且单个字段中还不存在着逗号。我们可以使用一个简单的正则表达式模式来解析该CSV文件,并将数据加载到一个列表之中。其数据看起来如下所示,它一共有大约26428行: yearID,teamID,lgID,playerID,salary 1985,ATL,NL,barkele01,870000 1985,ATL,NL,bedrost01,550000 1985,ATL,NL,benedbr01,545000 1985,ATL,NL,campri01,633333 1985,ATL,NL,ceronri01,625000 ... 我们用类似于如下代码的Streams加载CSV的数据: Pattern pattern = Pattern.compile(","); try (BufferedReader in = new BufferedReader(new FileReader(filename));){ List<Player> players = in .lines() .skip(1) .map(line -> { String[] arr = pattern.split(line); return new Player(Integer.parseInt(arr[0]), arr[1], arr[2], arr[3], Integer.parseInt(arr[4])); }) .collect(Collectors.toList()); } 定义数据透视表列的类 我们使用下面的类来定义数据透视表的各个列容器的类。这些列是用于对数据进行分组的。如果你使用的是SQL的话,这些列将出现在“GROUP BY”的语句中。 public class YearTeam { public int year; public String teamID; public YearTeam(int year,String teamID) { this.year = year; this.teamID = teamID; } @Override public boolean equals(Object other) { if ( other == null ) return false; if ( this == other ) return true; if ( other instanceof YearTeam ) { YearTeam yt = (YearTeam)other; if ( year == yt.year && teamID.equals(yt.teamID) ) return true; } return false; } @Override public int hashCode() { int hash = 1; hash = hash * 17 + year; hash = hash * 31 + teamID.hashCode(); return hash; } @Override public String toString() { StringBuilder sbuf = new StringBuilder(); sbuf.append('[').append(year).append(", ").append(teamID) .append(']'); return sbuf.toString(); } } 仅仅是为了方便起见,这些字段被定义为了“public(公有)”访问属性。而对于你自己的应用程序,你完全可以将其设置为“private(私有)”属性,并根据需要添加getter和/或setter。 这个类覆盖重写了equals()和hashCode(),因为它将在存储一张Map(映射表)时被作为key(键)使用。 用Streams分组数据 我们在此读取该CSV的数据,为每一行创建一个POJO,并用上面所定义的透视表列的类来对数据进行分组。 Map<YearTeam,List<Player>> grouped = in .lines() .skip(1) .map(line -> { String[] arr = pattern.split(line); return new Player(Integer.parseInt(arr[0]), arr[1], arr[2], arr[3], Integer.parseInt(arr[4])); }) .collect(Collectors.groupingBy(x-> new YearTeam(x.getYear(), x.getTeamID()))); 至此,这些数据已被正确地收集到了在一张Map(映射表)中,并按指定的列项进行了分组。 将数据透视表打印成CSV 让我们将数据透视表里的数据打印成一个CSV文件,以便我们可以将其加载到Excel中用以比较。在打印数据的时候,我们用到了求和函数summingLong()。Java 8 Streams同样也提供了averagingLong()函数让你求出平均值。如果你需要一次性地快速获取的话,那么summarizingLong()能够将所有信息呈现到你的面前,请尽情享用吧! CSV列的头部标题 我们使用teamID的各个数值来作为列的头部标题。对它们的收集和打印操作,如下所示。我们这里使用TreeSet来对其进行字母顺序的排列。 Set<String> teams = grouped .keySet() .stream() .map(x -> x.teamID) .collect(Collectors.toCollection(TreeSet::new)); System.out.print(','); teams.stream().forEach(t -> System.out.print(t + ",")); System.out.println(); 打印数据 这样创建并打印出来一张完整的数据透视表。对于团队每年的总和,我们从运动员列表中提取出来,并执行一个加总和打印的操作。 Set<Integer> years = grouped .keySet() .stream() .map(x -> x.year) .collect(Collectors.toSet()); years .stream() .forEach(y -> { System.out.print(y + ","); teams.stream().forEach(t -> { YearTeam yt = new YearTeam(y, t); List<Player> players = grouped.get(yt); if ( players != null ) { long total = players .stream() .collect(Collectors.summingLong(Player::getSalary)); System.out.print(total); } System.out.print(','); }); System.out.println(); }); 比较来自Excel的输出 我们将该CSV文件加载到Excel中并输出如下数据: 用Excel自带的数据透视表功能进行比较,可见这些数据是相同的。(如下所示,由于某种原因,列“MON”出现在了Excel的前端。也许这是诸多神奇“特性”中的一种吧。反正各个数值都是相同的。) 朋友们,这是使用Java自带的简单Collections(类集)来创建数据透视表的一种方法。你可以通过它去发现更多酷炫的用途! 总结 数据透视表的确是一种十分有用的数据汇总工具。大多数的数据分析软件,包括Excel都有用到它。在此,我们学会了如何使用Java 8 Streams来创建出相同的数据结构。同时,我们也用到了分组和加总来实现该功能。