20210305のJavaに関する記事は2件です。

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

コードサンプル

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

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Java・マイクラMod開発]自作アイテムを追加はできたがアイテム名とテクスチャが反映されない。

やっていること

MineCraftのMod作成(主に新アイテムの追加)
[Minecraft JE 1.12.2(-1.13.x)] MOD作成のすすめ(チュートリアル) Part01 : アイテムを追加するまでを参考にしました。

発生している問題

追加したtest_itemと定義しているアイテムに名前とテクスチャを適応させたいのですが、resourceというファイルに入れているアイテム名やテクスチャが一切反映されません。
実際にrunしてみるとアイテム名はitem.test_item.nameとなり、テクスチャがない場合に出る例の黒と紫のブロック状の表示になっていました。

該当のソースコードとディレクトリ構造

※実際のコードが入っているファイル以外は省略しています。必要なものがあればお手数ですがお教えください。
MoreSwordMod/src/main
  ├ java/moresword/tutorial
   │   ├ MoreSwordMod.java
   │   └ MoreSwordItems.java
   └ resources/assets/tsukineko_moresword
       ├ lang
       │  ├ en_us.lang
       │  └ ja_jp.lang
       ├ models/item
       │  └ test_item.json
       └ textures/items
          └ test_items.png ※ 現在はテクスチャとして使用していません。
package com.moresword.tutorial;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
@Mod(modid=MoreSwordMod.MOD_ID, name=MoreSwordMod.MOD_NAME, version=MoreSwordMod.VERSION)
public class MoreSwordMod {
    public static final String MOD_ID = "tsukineko_moresword";
    public static final String MOD_NAME = "MoreSwordMod";
    public static final String VERSION = "1.0";
    //アイテム・ブロック・ディメンション登録用
    @Mod.EventHandler
    public void preInit(FMLPreInitializationEvent event){
        MoreSwordItems.init();
        MoreSwordItems.register();
    }
    //レシピ・バイオーム・エンティティー登録用
    @Mod.EventHandler
    public void init(FMLInitializationEvent event){
    }
    //既存レシピの変更や他Modとの連携性登録用
    @Mod.EventHandler
    public void postInit(FMLPostInitializationEvent event){
    }
}
package com.moresword.tutorial;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.block.model.ModelResourceLocation;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.Item;
import net.minecraftforge.fml.common.registry.ForgeRegistries;
public class MoreSwordItems {
    public static Item test_item;
    public static void init(){
        test_item = new Item()
            .setRegistryName(MoreSwordMod.MOD_ID, "test_item")
            .setUnlocalizedName("test_item")
            .setCreativeTab(CreativeTabs.MATERIALS)
            .setMaxStackSize(64);
    }
    public static void register(){
        ForgeRegistries.ITEMS.register(test_item);
    }
    public static void registerRenders(){
        registerRender(test_item, 0);
    }
    private static void registerRender(Item item, int meta){
        Minecraft.getMinecraft().getRenderItem().getItemModelMesher().register(
            item, meta, new ModelResourceLocation(item.getRegistryName(), "Inventory")
        );
    }
}

コードについて

基本的には上で取り上げた動画のコードのアイテム名など固有の部分以外丸パクリで、中身自体はほとんど理解していません。

試したこと

調べた結果、Intellij IDEAを使っていると、resourcesが読み込まれない?みたいな問題があるという風に書いてありました。
具体的な原因は、IntellijだとModelResourceLocation(MoreSwordItems.java末尾四行目)で、mainのclassがある同じディレクトリ内しかresourocesを探さない?かららしいです(?)
そこでbuild.gradl末尾に下記コードを突っ込むと読み込むようになるらしいのでやってみました。

copy {
delete {
delete "$buildDir/classes/java/main"
}
into "$buildDir/classes/java/main"
from sourceSets.main.resources
}

理論上はModelResourceLocationがresouroesを探してくれる場所に、毎回resourocesをコピーして作ってくれることで、見つけれるようにするらしいです。
効果はありませんでした。

補足情報(FW/ツールのバージョンなど)

OS: Win10
Editor: Intellij IDEA Community Edition 2020
MineCraftVer: 1.12.2
質問者のスキルレベル: Javaほぼ無知・Pythonがメイン言語

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む