はじめに
業務でもようやくJava8に移行するような話が出てきているので、復習を兼ねてJava8のStream APIの使い方を幾つかのユースケースごとにまとめてみました。それぞれ、Java7でのコードススタイルとJava8のStreamを用いたコードスタイルでのサンプルコードを記載しています。
リストの操作
リスト(java.util.List)の操作はStream APIを利用することでより簡潔に記述できるようになります。以降のサンプルコードでは、次の惑星クラスと、
//惑星
public static class Planet {
private final String name;
private final int diameter;
private final String[] satellites;
public Planet(String name, int diameter, String... satelites) {
this.name = name;
this.diameter = diameter;
this.satellites = satelites;
}
//名前
public String getName() {
return name;
}
//直径(km)
public int getDiameter() {
return diameter;
}
//衛星のリスト
public List<String> getSatellites() {
return Arrays.asList(satellites);
}
}
この惑星クラスを要素とする、下のリスト(planets)が定義されているものとします。
//衛星は最大2つまでとします。
//昔は冥王星も惑星でした。。。
List<Planet> planets = Arrays.asList(
new Planet("水星", 4879),
new Planet("金星", 12103),
new Planet("地球", 12756, "月"),
new Planet("火星", 6794, "フォボス", "ダイモス"),
new Planet("木星", 142984, "イオ", "エウロパ"),
new Planet("土星", 120536, "タイタン", "レア"),
new Planet("天王星", 51118, "チタニア", "オベロン"),
new Planet("海王星", 49572, "トリトン", "プロテウス")
);
すべての要素に何かの処理を行う
例えば、すべての惑星の名前を出力するコードは以下のようになります。
Java7:
for (Planet p : planets) {
System.out.println(p.getName());
}
Java8:
planets.stream().forEach(p -> System.out.println(p.getName()));
StreamのforEachメソッドはすべて要素に指定の処理を適用します。もしくは、Streamではないですが、以下のようにも書けます。
planets.forEach(p -> System.out.println(p.getName()));
条件に一致する要素のみを抽出する
例えば、直径10,000km以上の惑星のみのリストを生成するコードは以下のようになります。
Java7:
List<Planet> filtered = new ArrayList<>();
for (Planet p : planets) {
if (p.getDiameter() > 10000) {
filtered.add(p);
}
}
Java8:
List<Planet> filtered = planets.stream()
.filter(p -> p.getDiameter() > 10000)
.collect(Collectors.toList());
抽出して得られるリストは以下の通りです。
[金星,地球,木星,土星,天王星,海王星]
条件に一致する要素の数を数える
例えば、直径10,000km以上の惑星の数を数えるコードは以下のようになります。
Java7:
long count = 0;
for (Planet p : planets) {
if (p.getDiameter() > 10000) {
count++;
}
}
Java8:
long count = planets.stream()
.filter(p -> p.getDiameter() > 10000)
.count();
Streamのfilterメソッドは指定された条件(述語)に一致する要素のみを抽出したStreamを生成します。
条件に一致する要素が一つ以上あるか調べる
例えば、直径10,000km以上の惑星が存在するかを調べるコードは以下のようになります。
Java7:
boolean found = false;
for (Planet p : planets) {
if (p.getDiameter() > 10000) {
found = true;
break;
}
}
Java8:
boolean found = planets.stream()
.anyMatch(p -> p.getDiameter() > 10000);
StreamのanyMatchメソッドは指定された述語に一致する要素が1つでもあれば true
を返します。
すべての要素が条件に一致するか調べる
例えば、すべての惑星が直径10,000km以上かを調べるコードは以下のようになります。
Java7:
boolean matchedAll = true;
for (Planet p : planets) {
if (!(p.getDiameter() > 10000)) {
matchedAll = false;
break;
}
}
Java8:
boolean matchedAll = planets.stream()
.allMatch(p -> p.getDiameter() > 10000);
StreamのallMatchは指定された述語にすべての要素が一致する場合のみ true
を返します。
それぞれの要素を異なる要素に変換(写像)したリストを生成する
例えば、リストの各惑星インスタンスから惑星名(String)のリストを生成するコードは以下のようになります。
Java7:
List<String> mappedList = new ArrayList<>();
for (Planet p : planets) {
mappedList.add(p.getName());
}
Java8:
List<String> mappedList = planets.stream()
.map(p -> p.getName())
.collect(Collectors.toList());
Streamのmapメソッドは、Streamの各要素を別の型の要素にマッピングしたStreamを生成します。上の例では、惑星インスタンス(Planet型)を惑星名(String型)に変換します。
メソッド参照を利用して、以下のようにより簡潔に記述することもできます。
List<String> mappedList = planets.stream()
.map(Planet::getName)
.collect(Collectors.toList());
各要素を異なる複数要素に変換した結果をマージしたリストを生成する
少しわかりにくいですが、上の例はリストの各要素の1:1での変換であるのに対し、1:Nに変換することです。例えば、各惑星に対応する衛星(複数)をマージして1つのリストとして生成するコードは以下のようになります。
Java7:
List<String> sattelites = new ArrayList<>();
for (Planet p : planets) {
sattelites.addAll(p.getSatellites());
}
Java8:
List<String> sattelites = planets.stream()
.flatMap(p -> p.getSatellites().stream())
.collect(Collectors.toList());
StreamのflatMapメソッドは、各要素から生成されるStreamを1つに統合したStreamを生成します。
Java7,8いずれの結果でも、以下の衛星からなる(フラットな)リストが生成されます。
[月,フォボス,ダイモス,イオ,エウロパ,タイタン,レア,チタニア,オベロン,トリトン,プロテウス]
各要素を合成して1つの値に集約する
これも少しわかりにくいですが、関数型言語での畳み込み関数のことです。例えば、各惑星の直径の合計を求めるコードは以下のようになります。
Java7:
int totalDiameter = 0;
for (Planet p : planets) {
totalDiameter += p.getDiameter();
}
Java8:
int totalDiameter = planets.stream()
.map(p -> p.getDiameter())
.reduce((accum, p) -> accum + p)
.get();
Streamのreduceメソッドが関数型言語での畳み込み関数(fold関数)に対応します。Streamでは左からの畳み込み関数しかありません。reduceメソッドは、結果が存在しない場合があるため(たとえば空のStreamの場合など)、Optional型を返すことになっています。そのため、最後のgetメソッドで値を取得する必要があります。
リストをフィルタリングしてマッピングして簡約する
いわゆる、map-reduceパターンです。例えば、衛星を持つ惑星を抽出(filter)し、その惑星を惑星名に変換(map)し、惑星名のリストを生成(reduce)するコードは以下のようになります。
Java7:
List<String> planetWithSattelites = new ArrayList<>();
for (Planet p : planets) {
if (p.getSatellites().size() > 0) {
planetWithSattelites.add(p.getName());
}
}
Java8:
List<String> planetWithSattelites = planets.stream()
.filter(p -> p.getSatellites().size() > 0)
.map(Planet::getName)
.collect(Collectors.toList());
マップの操作
Streamではないですが、Java8ではMapの繰り返し処理も簡単にできます。
すべてのキーと値のペアに何らかの処理を行う
例えば、次のMapのすべてのキーと値を出力するコードは以下のようになります。
Map<String, String> map = new HashMap<>();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
Java7:
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
System.out.println(String.format("key=%s, value=%s" ,key, value));
}
Java8:
map.forEach((key, value) -> {
System.out.println(String.format("key=%s, value=%s" ,key, value));
});
テキストファイルの操作
次のテキストファイルを題材とします。
Java
Scala
C
C#
JavaScript
Groovy
テキストファイルの全ての行に何かの処理を行う
たとえば、テキストファイルの全行を出力するコードは以下のようになります。
Java7:
try (BufferedReader br = Files.newBufferedReader(Paths.get("/tmp/sample.txt"), Charset.forName("utf-8"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
Java8:
try (Stream<String> stream = Files.lines(Paths.get("/tmp/sample.txt"), Charset.forName("utf-8"))) {
stream.forEach(line -> System.out.println(line));
}
Fileから生成したStreamの場合は、明示的にStreamをクローズする必要があります。よって、上のようにtry-with-resources文を利用しています。
以下のように書くこともできます。ただし、この場合は全行をメモリに展開することに注意が必要です。
for (String line : Files.readAllLines(Paths.get("/tmp/sample.txt"), Charset.forName("utf-8"))) {
System.out.println(line);
}
テキストファイルから条件に一致する行を抽出する
たとえば、テキストファイルから"Java"という単語を含む行のみを抽出して出力するコードは以下のようになります。
Java7:
try (BufferedReader br = Files.newBufferedReader(Paths.get("/tmp/sample.txt"), Charset.forName("utf-8"))) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains("Java")) {
System.out.println(line);
}
}
}
Java8:
try (Stream<String> stream = Files.lines(Paths.get("/tmp/sample.txt"))) {
stream.filter(line -> line.contains("Java")).forEach(System.out::println);
}
出力結果は以下のようになります。
Java
JavaScript
ディレクトリの操作
/tmp/test
に以下のディレクトリとファイルが存在するものとします。
$ ls -a /tmp/test
. .. .dir2 .file2 .file3 dir1 file1
ディレクトリの全てのサブディレクトリとファイルを抽出する
たとえば、ディレクトリ /tmp/test
配下のすべてのファイルとディレクトリ名を出力するコードは以下のようになります。
Java7:
File dir = Paths.get("/tmp/test").toFile();
for (File file : dir.listFiles()) {
System.out.println(file);
}
Java8:
try (Stream<Path> stream = Files.list(Paths.get("/tmp/test"))) {
stream.forEach(System.out::println);
}
ディレクトリから条件に一致するファイルのみを抽出する
たとえば、/tmp/test
から"."から始まる隠しファイルのみを抽出して出力するコードは以下のようになります。
Java7:
File dir = Paths.get("/tmp/test").toFile();
for (File file : dir.listFiles()) {
if (!file.isFile()) {
continue;
}
if (file.getName().startsWith(".")) {
System.out.println(file.getName());
}
}
Java8:
try (Stream<Path> stream = Files.list(Paths.get("/tmp/test"))) {
stream
.filter(path -> path.toFile().isFile())
.map(path -> path.getFileName().toString())
.filter(name -> name.startsWith("."))
.forEach(System.out::println);
}
出力結果は以下のようになります。
.file2
.file3