Stream でインデックスと要素を扱いたい

## 目次
[1. はじめに](#1-はじめに)
[2. Java の for 文](#2-java-の-for-文)
[3. guava](#3-guava)
[4. 実装](#4-実装)
[5. まとめ](#5-まとめ)
[参考文献](#参考文献)

## 1. はじめに
### Java 100 本ノック、55 本目の応用。
業務で Java を使うので、[Java 100 本ノック](https://github.com/JustSystems/java-100practices)を使い学習をしています。[55 本目](https://github.com/JustSystems/java-100practices/tree/master/contents/055)の問題は、ファイルを読み 1 行の文字数を合計して出力するプログラムを実装するというものですが、応用として読んだ行に行番号をつけて別ファイルに書くプログラムを実装することにしました。

### Python の for 文では簡単にインデックスを扱える。
Java では、[`Files`](https://docs.oracle.com/javase/jp/8/docs/api/java/nio/file/Files.html) や [`BufferedReader`](https://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedReader.html) を使えばファイルの中身を `String` の `List` や `Stream` で取得できます。なので、for 文や `Stream` でインデックスと要素を取得すれば簡単に実装できるなと思いました。Python では簡単にできるので。
```python:sample.py
list = ['a', 'b', 'c']

for idx, element in enumerate(list):
    print(f'{idx} {element}')
```
```console:出力
0 a
1 b
2 c
```
でも、Java ではこんな便利機能はすぐに見つからなかった......

## 2. Java でインデックスと要素を扱う
### 拡張 for 文
```java:Sample.java
import java.util.List;

public class Sample {
    public static void main(String[] args) throws Exception {
        List<String> list = List.of("a", "b", "c");
        
        int idx = 0;
        
        for (String element : list) {
            System.out.println(String.format("%s %s", idx, element));
            idx++;
        }
    }
}
```
まあできるけど、idx のスコープがいけてないよね。

### 普通の for 文
```java:Sample.java
import java.util.List;

public class Sample {
    public static void main(String[] args) throws Exception {
        List<String> list = List.of("a", "b", "c");

        for (int idx = 0; idx < list.size(); idx++) {
            String element = list.get(idx);
            System.out.println(String.format("%s %s", idx, element));
        }
    }
}
```
古臭い。
### IntStream
```java:Sample.java
import java.util.List;
import java.util.stream.IntStream;

public class Sample {
    public static void main(String[] args) throws Exception {
        List<String> list = List.of("a", "b", "c");

        IntStream
                .range(0, list.size())
                .mapToObj(i -> String.format("%s %s", i, list.get(i)))
                .forEach(System.out::println);
    }
}
```
まだ許せるが何をやっているかわかりにくい。`List` を操作してる感がない。そもそも `BufferReader` を使いたいから `List` は NG だった。
```java:Sample.java
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class Sample {
    public static void main(String[] args) throws Exception {
        Stream<String> stream = Stream.of("a", "b", "c");
        List<String> list = stream.toList();

        IntStream
                .range(0, list.size())
                .mapToObj(i -> String.format("%s %s", i, list.get(i)))
                .forEach(System.out::println);
    }
}
```
まったくいけてない......

## 3. guava
コア API だけでは厳しそうだということで調べてみると、[guava](https://central.sonatype.com/artifact/com.google.guava/guava) という Google が開発しているライブラリを見つけました。[`Streams.mapWithIndex`](https://www.javadoc.io/doc/com.google.guava/guava/21.0/com/google/common/collect/Streams.html#mapWithIndex-java.util.stream.Stream-com.google.common.collect.Streams.FunctionWithIndex-) 使えば良さそう。
```java:Sample.java
import com.google.common.collect.Streams;

import java.util.stream.Stream;

public class Sample {
    public static void main(String[] args) throws Exception {
        Stream<String> stream = Stream.of("a", "b", "c");

        Streams.mapWithIndex(
                        stream,
                        (element, idx) -> String.format("%s %s", idx, element)
                )
                .forEach(System.out::println);
    }
}
```
python の `enumerate` と全く同じ要領で使えて感動。

## 4. 実装
本題の実装。テキストファイルを 1 行ずつ読み、行番号をつけて新規ファイルに出力するプログラムです。
```java:ExAnswer055.java
import com.google.common.collect.Streams;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 * 応用。
 */
public class ExAnswer055 {
    /**
     * メインメソッド。
     *
     * @param args コマンドライン引数
     */
    public static void main(String[] args) {
        Path input = Path.of("./src/main/resources/MyAnswer055.txt");
        Path output = Path.of("./src/main/resources/exoutput055.txt");

        try (
                BufferedReader reader = Files.newBufferedReader(
                        input,
                        StandardCharsets.UTF_8
                );
                BufferedWriter writer = Files.newBufferedWriter(
                        output,
                        StandardCharsets.UTF_8
                )
        ) {
            Streams.mapWithIndex(
                            reader.lines(),
                            (str, idx) -> String.format("%s %s%n", idx + 1, str)
                    )
                    .forEach(i -> write(writer, i));
        } catch (IOException | UncheckedIOException e) {
            e.printStackTrace();
        }
    }

    private static void write(BufferedWriter writer, String line) {
        try {
            writer.write(line);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}
```
シンプルでいい感じに実装できました。プライベートメソッドの `write` は、`writer.write(line)` で `IOException` が出るので、非チェック例外にして throw するために作りました。`Stream` 内で try-catch はしたくないので。

## 5. まとめ
Python でデータ処理する時は `enumerate` を頻繁に使っていたので、Java でも標準であってもいいと思うんですけどね。まあでもこんな感じでコア API 以外も自分で調べて使いこなせてきたのでヨシ!

## 参考文献
- Stream の中間操作で index を扱う, Junpei Katayama (Zenn), https://zenn.dev/junpei_katayama/articles/b8a549f104a0a0, 2023-09-25 確認
- 【Java入門】要素数をカウントする方法（ストリームAPI）, kitanote, https://kita-note.com/java-stream-count, 2023-09-25 確認
- guava, maven central repository, https://central.sonatype.com/artifact/com.google.guava/guava, 2023-09-25 確認
- Streams.mapWithIndex, com.google.guava, https://www.javadoc.io/doc/com.google.guava/guava/21.0/com/google/common/collect/Streams.html#mapWithIndex-java.util.stream.Stream-com.google.common.collect.Streams.FunctionWithIndex-, 2023-09-25 確認