概要
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)
// りんごといちごを選んだ場合(※1)
りんご
いちご
// りんごを選び、その他に「パイナップル」と記載した場合(※2)
りんご
パイナップル
// その他のみ選択し、「パイナップル」と記載した場合(※3)
パイナップル
コードサンプル
- Javaのpackageとimportは省略します
- Thymeleafのヘッダーを共通化していますが関係ないので省略します
- 気になる方は Thymeleafでヘッダーフッターを共通化する方法 をご覧ください
/**
* 好きな果物の選択肢
*
* @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());
}
}
<!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>
/**
* 好きな果物選択用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";
}
}
/**
* 値の受け渡しに使うModel
*
* @author tamorieeeen
*
*/
@Getter
@Setter
@NoArgsConstructor
public class FruitModel {
private String[] fruitIds;
private String text;
}
/**
* 好きな果物選択用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)ことで解決したバージョンがこちら。
/**
* 好きな果物選択用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()で取得できる
}
}