Java7 vs. Java8 Stream

# はじめに
業務でもようやくJava8に移行するような話が出てきているので、復習を兼ねてJava8のStream APIの使い方を幾つかのユースケースごとにまとめてみました。それぞれ、Java7でのコードススタイルとJava8のStreamを用いたコードスタイルでのサンプルコードを記載しています。

# リストの操作
リスト（java.util.List）の操作はStream APIを利用することでより簡潔に記述できるようになります。以降のサンプルコードでは、次の惑星クラスと、

```java

	//惑星
	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）が定義されているものとします。

```java
	//衛星は最大２つまでとします。
	//昔は冥王星も惑星でした。。。
	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:
```java
			for (Planet p : planets) {
				System.out.println(p.getName());
			}
```
#### Java8:
```java
			planets.stream().forEach(p -> System.out.println(p.getName()));
```
StreamのforEachメソッドはすべて要素に指定の処理を適用します。もしくは、Streamではないですが、以下のようにも書けます。

```java		
			planets.forEach(p -> System.out.println(p.getName()));
```
## 条件に一致する要素のみを抽出する
例えば、直径10,000km以上の惑星のみのリストを生成するコードは以下のようになります。

#### Java7:
```java
			List<Planet> filtered = new ArrayList<>();
			for (Planet p : planets) {
				if (p.getDiameter() > 10000) {
					filtered.add(p);
				}
			}
```
#### Java8:

```java
			List<Planet> filtered = planets.stream()
					.filter(p -> p.getDiameter() > 10000)
					.collect(Collectors.toList());
```

抽出して得られるリストは以下の通りです。

```
		[金星,地球,木星,土星,天王星,海王星]
```

## 条件に一致する要素の数を数える
例えば、直径10,000km以上の惑星の数を数えるコードは以下のようになります。

#### Java7:
```java
			long count = 0;
			for (Planet p : planets) {
				if (p.getDiameter() > 10000) {
					count++;
				}
			}
```
#### Java8:

```java
			long count = planets.stream()
					.filter(p -> p.getDiameter() > 10000)
					.count();
```
Streamのfilterメソッドは指定された条件（述語）に一致する要素のみを抽出したStreamを生成します。

## 条件に一致する要素が一つ以上あるか調べる
例えば、直径10,000km以上の惑星が存在するかを調べるコードは以下のようになります。

#### Java7:
```java
			boolean found = false;
			for (Planet p : planets) {
				if (p.getDiameter() > 10000) {
					found = true;
					break;
				}
			}
```
#### Java8:

```java
			boolean found = planets.stream()
					.anyMatch(p -> p.getDiameter() > 10000);
```
StreamのanyMatchメソッドは指定された述語に一致する要素が１つでもあれば ```true``` を返します。

##すべての要素が条件に一致するか調べる
例えば、すべての惑星が直径10,000km以上かを調べるコードは以下のようになります。

#### Java7:
```java
			boolean matchedAll = true;
			for (Planet p : planets) {
				if (!(p.getDiameter() > 10000)) {
					matchedAll = false;
					break;
				}
			}
```
#### Java8:

```java
			boolean matchedAll = planets.stream()
					.allMatch(p -> p.getDiameter() > 10000);		
```
StreamのallMatchは指定された述語にすべての要素が一致する場合のみ ```true``` を返します。

## それぞれの要素を異なる要素に変換（写像）したリストを生成する
例えば、リストの各惑星インスタンスから惑星名（String）のリストを生成するコードは以下のようになります。

#### Java7:
```java
			List<String> mappedList = new ArrayList<>();
			for (Planet p : planets) {
				mappedList.add(p.getName());
			}
```
#### Java8:

```java
			List<String> mappedList = planets.stream()
					.map(p -> p.getName())
					.collect(Collectors.toList());
```
Streamのmapメソッドは、Streamの各要素を別の型の要素にマッピングしたStreamを生成します。上の例では、惑星インスタンス（Planet型）を惑星名（String型）に変換します。

メソッド参照を利用して、以下のようにより簡潔に記述することもできます。

```java
			List<String> mappedList = planets.stream()
					.map(Planet::getName)
					.collect(Collectors.toList());
```

## 各要素を異なる複数要素に変換した結果をマージしたリストを生成する
少しわかりにくいですが、上の例はリストの各要素の1:1での変換であるのに対し、1:Nに変換することです。例えば、各惑星に対応する衛星（複数）をマージして１つのリストとして生成するコードは以下のようになります。

#### Java7:
```java
			List<String> sattelites = new ArrayList<>();
			for (Planet p : planets) {
				sattelites.addAll(p.getSatellites());
			}
```
#### Java8:
```java
			List<String> sattelites = planets.stream()
					.flatMap(p -> p.getSatellites().stream())
					.collect(Collectors.toList());	
```
StreamのflatMapメソッドは、各要素から生成されるStreamを１つに統合したStreamを生成します。

Java7,8いずれの結果でも、以下の衛星からなる（フラットな）リストが生成されます。

```
		[月,フォボス,ダイモス,イオ,エウロパ,タイタン,レア,チタニア,オベロン,トリトン,プロテウス]
```

## 各要素を合成して１つの値に集約する
これも少しわかりにくいですが、関数型言語での畳み込み関数のことです。例えば、各惑星の直径の合計を求めるコードは以下のようになります。

#### Java7:
```java		
			int totalDiameter = 0;
			for (Planet p : planets) {
				totalDiameter += p.getDiameter();
			}
```
#### Java8:

```java
			 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:
```java
			List<String> planetWithSattelites = new ArrayList<>();
			for (Planet p : planets) {
				if (p.getSatellites().size() > 0) {
					planetWithSattelites.add(p.getName());
				}
			}
```
#### Java8:

```java
			List<String> planetWithSattelites = planets.stream()
					.filter(p -> p.getSatellites().size() > 0)
					.map(Planet::getName)
					.collect(Collectors.toList());	
```

# マップの操作
Streamではないですが、Java8ではMapの繰り返し処理も簡単にできます。

## すべてのキーと値のペアに何らかの処理を行う
例えば、次のMapのすべてのキーと値を出力するコードは以下のようになります。

```java
		Map<String, String> map = new HashMap<>();
		map.put("1", "one");
		map.put("2", "two");
		map.put("3", "three");		
```

#### Java7:

```java
			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:

```java
			map.forEach((key, value) -> {
				System.out.println(String.format("key=%s, value=%s" ,key, value));
			});		
```


# テキストファイルの操作
次のテキストファイルを題材とします。

```text:/tmp/sample.txt
Java
Scala
C
C#
JavaScript
Groovy
```

## テキストファイルの全ての行に何かの処理を行う
たとえば、テキストファイルの全行を出力するコードは以下のようになります。

#### Java7:

```java
			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:

```java
			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文を利用しています。

以下のように書くこともできます。ただし、この場合は全行をメモリに展開することに注意が必要です。

```java
			for (String line : Files.readAllLines(Paths.get("/tmp/sample.txt"), Charset.forName("utf-8"))) {
				System.out.println(line);
			}
```

## テキストファイルから条件に一致する行を抽出する
たとえば、テキストファイルから"Java"という単語を含む行のみを抽出して出力するコードは以下のようになります。

#### Java7:

```java
			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:

```java
			try (Stream<String> stream = Files.lines(Paths.get("/tmp/sample.txt"))) {
				stream.filter(line -> line.contains("Java")).forEach(System.out::println);
			}
```

出力結果は以下のようになります。

```text
Java
JavaScript
```

# ディレクトリの操作

```/tmp/test``` に以下のディレクトリとファイルが存在するものとします。

```shell-session
$ ls -a /tmp/test
.	..	.dir2	.file2	.file3	dir1	file1
```

## ディレクトリの全てのサブディレクトリとファイルを抽出する
たとえば、ディレクトリ ```/tmp/test``` 配下のすべてのファイルとディレクトリ名を出力するコードは以下のようになります。

#### Java7:

```java
			File dir = Paths.get("/tmp/test").toFile();
			for (File file : dir.listFiles()) {
				System.out.println(file);
			}
```

#### Java8:

```java
			try (Stream<Path> stream = Files.list(Paths.get("/tmp/test"))) {
				stream.forEach(System.out::println);
			}
```

## ディレクトリから条件に一致するファイルのみを抽出する
たとえば、```/tmp/test``` から"."から始まる隠しファイルのみを抽出して出力するコードは以下のようになります。

#### Java7:

```java
			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:

```java
			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);
			}
```

出力結果は以下のようになります。

```text
.file2
.file3
```