【Java】ListIteratorのhasPrevious()が必ずtrueになる罠

# 概要
`ListIterator`を使った際に`hasPrevious()`が常にtrueになってしまいハマったのでメモ。

# 環境
- Java 1.8
- SpringBoot 2.2.1.RELEASE
- Thymeleaf 3.0.11.RELEASE

# ハマった内容
チェックボックスで選択された値を受け取り、Enumに定義されたテキストをメールに出力する際に改行を入れたかった。
`hasNext()`だけでなく`hasPrevious()`も使いたかったので、`ListIterator`を使ったが、要素が1つしかない場合でも`hasPrevious()`が常にtrueで返ってくる。

## こんなシチュエーション
ハマった状況はこんな感じ。

画面上に果物の選択肢があり、好きな果物にチェックを入れて送信する。選択結果をメール本文に書いて送信する。
その際に、
- 各行に改行を入れる(※1)
- その他が選択されている場合は1行空ける(※2)
- ただしその他しか選択されていない場合は改行不要(※3)

```出力したいテキスト.txt
// りんごといちごを選んだ場合(※1)
りんご
いちご

// りんごを選び、その他に「パイナップル」と記載した場合(※2)
りんご

パイナップル

// その他のみ選択し、「パイナップル」と記載した場合(※3)
パイナップル
```

## コードサンプル
- Javaのpackageとimportは省略します
- Thymeleafのヘッダーを共通化していますが関係ないので省略します
 - 気になる方は [Thymeleafでヘッダーフッターを共通化する方法](https://qiita.com/tamorieeeen/items/4bc818e18f4ff8bdf0c2) をご覧ください

```FruitType.java
/**
 * 好きな果物の選択肢
 *
 * @author tamorieeeen
 *
 */
@Getter
@AllArgsConstructor
public enum FruitType {

    APPLE(1, "りんご", "りんご"),
    ORANGE(2, "みかん", "みかん"),
    STRAWBERRY(3, "いちご", "いちご"),
    BANANA(4, "バナナ", "バナナ"),
    PEACH(5, "もも", "もも"),
    OTHER(6, "その他", "");

    private final int id;
    private final String name;
    private final String output;

    /**
     * 該当のFruitTypeを取得
     */
    public static FruitType getFruitType(int id) {

        return Stream.of(values())
                .filter(v -> v.id == id)
                .findFirst()
                .orElse(null);
    }

    /**
     * 一覧を取得
     */
    public static List<FruitType> getFruitTypes() {

        return Stream.of(values()).collect(Collectors.toList());
    }
}
```
```fruit.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="common :: meta_header('sample',~{::link},~{::script},~{::meta})">
</head>
<body>
    <h1>好きな果物を選んでください</h1>
    <p th:if="${sent}">送信が完了しました。</p>
    <p>選択肢にない場合は「その他」に入力してください。</p>
    <form th:action="@{/selectfruit}" method="post" th:object="${selection}">
        <table>
            <tr th:each="f:${fruits}">
                <td colspan="2">
                    <label><input type="checkbox" th:value="${f.id}" th:field="*{fruitIds}" />[[${f.name}]]</label>
                    <textarea th:if="${#strings.isEmpty(f.output)}" rows="5" cols="50" th:field="*{text}"></textarea>
                </td>
            </tr>
        </table>
        <button type="submit">送信する</button>
    </form>
</body>
</html>
```
```FruitController.java
/**
 * 好きな果物選択用Controller
 *
 * @author tamorieeeen
 *
 */
@Controller
public class FruitController {

    @Autowired
    private FruitService fruitService;

    /**
     * 好きな果物選択画面
     */
    @GetMapping("/selectfruit")
    public String selectFruit(Model model) {

        model.addAttribute("selection", new FruitModel());
        model.addAttribute("fruits", FruitType.getFruitTypes());

        return "fruit";
    }

    /**
     * 選択結果をメールする
     */
    @PostMapping("/selectfruit")
    public String selectFruitComplete(
            @ModelAttribute("selection") FruitModel selection,
            RedirectAttributes redirect) {

        fruitService.sendFruitMail(selection);

        redirect.addFlashAttribute("sent", true);

        return "redirect:/selectfruit";
    }
}
```
```FruitModel.java
/**
 * 値の受け渡しに使うModel
 * 
 * @author tamorieeeen
 *
 */
@Getter
@Setter
@NoArgsConstructor
public class FruitModel {

    private String[] fruitIds;
    private String text;
}
```
```FruitService.java
/**
 * 好きな果物選択用Service
 * 
 * @author tamorieeeen
 *
 */
@Service
public class FruitService {
    /**
     * 選択結果をメールする
     */
    public void sendFruitMail(FruitModel selection) {

        StringBuilder builder = new StringBuilder();
        
        for (ListIterator<String> itr = Arrays.asList(
                selection.getFruitIds()).listIterator(); itr.hasNext();) {

            FruitType type = FruitType.getFruitType(
                Integer.parseInt(itr.next()));

            if (type.equals(FruitType.OTHER)) {
                // その他で他の果物と併用の場合は改行を追加する
                if (itr.hasPrevious()) {
                    builder.append("\n");
                }
                builder.append(selection.getText());
            } else {
                builder.append(type.getOutput());
            }
            // 次の行がある場合だけ改行を入れる
            if (itr.hasNext()) {
                builder.append("\n");
            }
        }

        // メール送信(省略)
        // 組み立てた文面はbuilder.toString()で取得できる
    }
}
```
要素が1つしかなければ`itr.hasPrevious()`はfalseで返ってくることを想定していたが、この書き方だと`itr.hasPrevious()`が常にtrueで返ってくる。

## `itr.hasPrevious()`が常にtrueな原因
「`itr.next()`を呼ぶとiteratorの位置が移動するから」が原因だった。

`itr.next()`を呼ぶと、次の要素を取り出してiteratorの位置が進む。したがって取り出される要素は次の要素だが、その時点でiteratorが1つ進むので、たった今取り出した要素は前の要素と化す。
したがって、`itr.next()`を呼んだ後に`itr.hasPrevious()`を呼んだ場合、たった今取り出した要素の有無を判定することになるので、当然trueが返ってくる。

※この辺りのiteratorの動きは、[参考](#参考)の記事の図解が非常にわかりやすいです

そのため、私が想定している動きにするためには、`itr.next()`を呼ぶ前に`itr.hasPrevious()`を呼ばなければならない。
しかし今回の場合は`itr.next()`を呼んで`FruitType.OTHER`かどうかを判定してから`itr.hasPrevious()`の結果を使いたい。

それを踏まえて、`itr.next()`を呼ぶ前に`itr.hasPrevious()`の状態を取得しておく(※1)ことで解決したバージョンがこちら。

```FruitService.java
/**
 * 好きな果物選択用Service
 * 
 * @author tamorieeeen
 *
 */
@Service
public class FruitService {
    /**
     * 選択結果をメールする
     */
    public void sendFruitMail(FruitModel selection) {

        StringBuilder builder = new StringBuilder();
        
        for (ListIterator<String> itr = Arrays.asList(
                selection.getFruitIds()).listIterator(); itr.hasNext();) {

            boolean hasPrevious = itr.hasPrevious(); // ※1
            FruitType type = FruitType.getFruitType(
                Integer.parseInt(itr.next()));

            if (type.equals(FruitType.OTHER)) {
                // その他で他の果物と併用の場合は改行を追加する
                if (hasPrevious) {
                    builder.append("\n");
                }
                builder.append(selection.getText());
            } else {
                builder.append(type.getOutput());
            }
            // 次の行がある場合だけ改行を入れる
            if (itr.hasNext()) {
                builder.append("\n");
            }
        }

        // メール送信(省略)
        // 組み立てた文面はbuilder.toString()で取得できる
    }
}
```

# 参考
- [JavaのListIteratorのnext()とprevious()が同じ値を指す話](https://mem-archive.com/2018/05/12/post-494/)