LoginSignup
1
0

More than 3 years have passed since last update.

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

Posted at

概要

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)
パイナップル

コードサンプル

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()で取得できる
    }
}

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0