String型キーをやめてマーカーインタフェースを導入する

# はじめに

Javaのライブラリでは、キーやプロパティ名など**オブジェクトを識別するための値**（以下「キー」とします。）としてString型のオブジェクトを用いるものをよく目にします。例えば、プロパティ・リストの導入に利用する`java.util.Properties`クラスであったり、プロパティの変更の通知などイベントの送信に利用する`java.beans.PropertyChangeSupport`クラスであったり、~~古いだの衰退しているだのと言われJavaFxと常に比較される~~Swingのレイアウトマネージャーの一つである`java.awt.CardLayout`クラスであったり・・・。

String型を用いる手法では、**その値に誤字脱字を含んでいたり**、**実装の変更に伴いキーの名前が変更したりする**と、オブジェクトを正しく参照できずに不具合が生じることがあります。

```Java:キー名の誤字による値の取得失敗例
Map<String, String> entries = new HashMap<>() {{
  put("hoge", "ほげ");
  put("huga", "ふが");
  put("piyo", "ぴよ");
}};
System.out.println(entries.get("hoge")); // 出力：ほげ
System.out.println(entries.get("hige")); // 出力：null（取得失敗）
```

```Java:実装の変更に伴う値の取得失敗例
Map<String, String> entries = new HashMap<>() {{
  put("foo", "ふー");  // 実装の変更
  put("huga", "ふが");
  put("bar", "ばー");  // 実装の変更
}};
System.out.println(entries.get("hoge")); // 出力：null（取得失敗）
System.out.println(entries.get("huga")); // 出力：ふが
```

このような危険性は、マーカーインタフェース[^マーカーインタフェース]を導入することで除去することができます。今回は先程紹介したCardLayoutクラスの利用を例に、説明したいと思います。

# 導入前

次に折りたたんであるコードAによりレイアウトされる画面を考えます。

<details><summary>コードA</summary><div>

```Java:
public class App {

    public static void main(String[] args) {
        new JFrame("Sample") {{
            setSize(350, 300);
            setLocationRelativeTo(null);
            setDefaultCloseOperation(EXIT_ON_CLOSE);
            setVisible(true);
            // CardLayoutの生成
            CardLayout cards = new CardLayout();
            // フレーム中央
            JLabel[] labels = {
                new JLabel("Card 1"),
                new JLabel("Card 2"),
                new JLabel("Card 3"),
                new JLabel("Card 4")
            };
            Font fnt = new Font("メイリオ", Font.PLAIN, 40);
            JPanel center = new JPanel();
            center.setLayout(cards);
            for (int i = 0; i < labels.length; i++) {
                labels[i].setFont(fnt);
                labels[i].setVerticalAlignment(JLabel.CENTER);
                labels[i].setHorizontalAlignment(JLabel.CENTER);
                center.add(labels[i], "Card" + (i + 1));
            }
            // フレーム下部
            JButton[] buttons = {
                new JButton("Card 1"),
                new JButton("Card 2"),
                new JButton("Card 3"),
                new JButton("Card 4")
            };
            JPanel south = new JPanel();
            for (int i = 0; i < buttons.length; i++) {
                int l_i = i;
                buttons[i].addActionListener(e -> cards.show(center, "Card" + (l_i + 1)));
                south.add(buttons[i]);
            }
            // フレームへのパネルの追加
            add(center, BorderLayout.CENTER);
            add(south, BorderLayout.SOUTH);
        }};
    }
}
```
</div></details>

このコードを実行し各ボタンをクリックすると、次のように4つのラベルを切り替えて表示することができます。

![ok.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726479/828a30bd-db6b-1996-6da4-70051489ea0b.gif)

ここで、コードA内にある

```Java:
buttons[i].addActionListener(e -> cards.show(center, "Card" + (l_i + 1)));
```

の`"Card"`を**`"Cerd"`**と間違えた瞬間、次のように切り替えは正常に動作しなくなります。

![ng.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726479/e82e66e9-8840-d7ae-be48-179c5c5a20fd.gif)

これは、第2引数に4つのラベルの識別子を指定したaddメソッドによりフレーム中央のパネルに紐付けられた各ラベルを、誤字のせいでshowメソッドにより参照できなくなったためです。

```Java:
center.add(labels[i], "Card" + (i + 1));

buttons[i].addActionListener(e -> cards.show(center, "Cerd" + (l_i + 1))); // "Card"じゃない！
```

コンパイルエラーにならないので、自分で気がつくか誰かに教えてもらわない限り、その不具合の原因がわからずじまいとなってしまってもおかしくはありません。

# 導入

ということで、マーカーインタフェースを導入します。

マーカーインタフェースを導入するだけでは意味がないので、列挙体を用意してキーを定義し、その列挙体にマーカーインタフェースを実装させます。

```Java:
// マーカーインタフェース
interface CardKey {}
// マーカーインタフェースを実装した列挙体
enum Card implements CardKey {
    CARD1, CARD2, CARD3, CARD4;
}
```

また、先程登場したaddメソッドやshowメソッドでは定義したキーをそのまま引数に指定することができないため、次に折りたたんであるユーティリティクラスを定義し、キーと切り替えて表示するコンポーネントを紐付けられるようにします。

<details><summary>ユーティリティクラス</summary><div>

```Java:
class CardLayoutUtil {
    private CardLayout cards = new CardLayout();
    private JPanel panel = new JPanel();

    private CardLayoutUtil(JPanel panel) {
        this.panel = panel;
        cards = (CardLayout) panel.getLayout();
    }
    
    /**
     * 引数にとるパネルに基づき、このクラスのインスタンスを生成する。
     *
     * @param panel インスタンスに紐付けるパネル
     * @return このクラスのインスタンス
     * @throws NullPointerException 引数がnullである場合
     * @throws IllegalArgumentException 引数にとるパネルにCardLayoutが設定されていない場合
     */
    static CardLayoutUtil of(JPanel panel) {
        Objects.requireNonNull(panel);
        if (!(panel.getLayout() instanceof CardLayout))
            throw new IllegalArgumentException("引数にとるパネルにCardLayoutが設定されていません。");
        return new CardLayoutUtil(panel);
    }

    /**
     * インスタンスに紐付けているパネルに、コンポーネントを登録する。
     *
     * @param comp 登録するコンポーネント
     * @param key コンポーネントに紐付けるキー
     * @throws NullPointerException いずれかの引数がnullである場合
     */
    void addCard(Component comp, CardKey key) {
        Objects.requireNonNull(comp);
        Objects.requireNonNull(key);
        panel.add(comp, key.toString());
    }

    /**
     * 引数にとるキーと紐付くコンポーネントを表示する。
     *
     * @param key コンポーネントに紐付けているキー
     * @throws NullPointerException 引数がnullである場合
     */
    void showCard(CardKey key) {
        Objects.requireNonNull(key);
        cards.show(panel, key.toString());
    }
}
```
</div></details>

ユーティリティクラスと紐付けるパネルのレイアウトマネージャがCardLayoutでなかったり、各メソッドの引数にとるキーやコンポーネントがnullであると期待する動作は実現できないので、例外処理を施しています。

このユーティリティクラスのオブジェクトを生成し、ついでにループで回せるように各キーの配列も用意しておきます。

```Java:
// CardLayoutUtilオブジェクトの生成
CardLayoutUtil util = CardLayoutUtil.of(center);
Card[] cardKeys = Card.class.getEnumConstants();
```

あとは、コードA内にある

```Java:
center.add(labels[i], "Card" + (i + 1));

buttons[i].addActionListener(e -> cards.show(center, "Cerd" + (l_i + 1))); // "Card"じゃない！
```

の部分を

```Java:
util.addCard(labels[i], cardKeys[i]);

buttons[i].addActionListener(e -> util.showCard(cardKeys[l_i]));
```

と置き換えれば、誤字や実装の変更によって先程のような不具合が発生することを心配する必要はなくなります。

# 注意点

今回の手法は、キー（列挙子）の文字列表現を`toString()`によって取得したものを用いることで実現しています。したがって、**同じキーを別のコンポーネントに紐付けて使いまわしていたり**、**別々の列挙体間で同じ名前の列挙子を定義していたりする**と、うまく動作しません[^toString]。

```Java:列挙体間で同じ名前の列挙子を定義している例
enum CardsA implements CardKey {
    CARD1, CARD2, CARD3, CARD4;
}
enum CardsB implements CardKey {
    CARD1, CARD2, CARD3, CARD4;
}
```

<details><summary>正常に動作しないコード例</summary><div>

```Java:
// フレーム中央
JLabel[] labelsA = {
    new JLabel("Card 1"),
    new JLabel("Card 2"),
    new JLabel("Card 3"),
    new JLabel("Card 4")
};
JLabel[] labelsB = {
    new JLabel("Card 5"),
    new JLabel("Card 6"),
    new JLabel("Card 7"),
    new JLabel("Card 8")
};
... // 省略
// CardLayoutUtilオブジェクトの生成
CardLayoutUtil util = CardLayoutUtil.of(center);
CardsA[] cardKeysA = CardsA.class.getEnumConstants();
CardsB[] cardKeysB = CardsB.class.getEnumConstants();
for (int i = 0; i < labelsA.length; i++) {
    ... // 省略
    util.addCard(labelsA[i], cardKeysA[i]);
}
for (int i = 0; i < labelsB.length; i++) {
    ... // 省略
    util.addCard(labelsB[i], cardKeysB[i]);
}
// フレーム下部
JButton[] buttonsA = {
    new JButton("Card 1"),
    new JButton("Card 2"),
    new JButton("Card 3"),
    new JButton("Card 4")
};
JButton[] buttonsB = {
    new JButton("Card 5"),
    new JButton("Card 6"),
    new JButton("Card 7"),
    new JButton("Card 8")
};
JPanel south = new JPanel();
for (int i = 0; i < buttonsA.length; i++) {
    ... // 省略
    south.add(buttonsA[i]);
}
for (int i = 0; i < buttonsB.length; i++) {
    ... // 省略
    south.add(buttonsB[i]);
}
```
</div></details>

![ng2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726479/eef780a3-5952-5440-3879-4d085a490254.gif)

ただ、これはキーの識別を列挙子名だけで行っているゆえに起きる現象であるため、ユーティリティクラス内の

```Java:
cards.show(panel, key.toString());

panel.add(comp, key.toString());
```

の部分を

```Java:
cards.show(panel, key.getClass().getName() + "." + key.toString());

panel.add(comp, key.getClass().getName() + "." + key.toString());
```

と書き換えることで、キーの識別が**列挙体の完全修飾クラス名[^完全修飾クラス名]＋列挙子名**で行われるようになり、正常に動作するようになります。

![ok2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/726479/57d1903a-865b-e4fe-26ac-b2918c3e004e.gif)

また、キーとコンポーネントの数が合わなかったりするとループを回しているときに`ArrayIndexOutOfBoundsException`を吐いたりもします。その辺はユーティリティクラスをもう少しゴニョゴニョすれば改善できるかもしれません。

[^マーカーインタフェース]: マーカーインタフェースとは、中身に何も実装していないインタフェースのことです。実装するクラスに意味付けを行うときによく用いられます。

[^toString]: `toString()`をオーバーライドさせてすべて同一の文字列表現が返されるようにしていても、キーが判別できなくなるため駄目です。

[^完全修飾クラス名]: インポート文で使用されるパッケージ名を含むクラス名のことです。