Pair<K,V> を作る

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

Pair<K,V> を作る とは

複数の型パラメータを持つジェネリッククラス Pair<K,V> を題材に、HashMap<K,V> と同じ仕組みでキーと値の組を表現する方法と、Record を使った最新の書き方を学ぼう。

Pair<K, V> で「2 つの値を 1 セット」を表現する

前のレッスンまでで、ArrayList<E>Box<T> のように 型パラメータが 1 つだけ のジェネリッククラスを扱ってきました。ここで一歩踏み込んで、HashMap<K, V> のように 型パラメータを 2 つ 持つクラスを自分で書いてみます。題材は 名前と年齢 単語と意味 商品 ID と在庫数 のような「キーと値の組」を表す Pair<K, V> クラスです。

Pair は Java の標準ライブラリには (javafx.util.Pair を除いて) 用意されていないクラスですが、業務コードでも教科書でも頻繁に「2 つの値をまとめて返したい」という場面が出てくるので、自分で書けるようになると一気に表現の幅が広がります。

Pair<String, Integer> という型は、「文字列のキーと整数の値を持つペア」を意味します。これは Map.Entry<String, Integer> とよく似た構造で、実は HashMap の内部で各 エントリ を表すのにも同じ仕組みが使われています。本章ではこの Pair を入り口に、ジェネリクスでの 複数型パラメータ の使い方と、Java 14 以降で書ける Record による超短い定義を一気に押さえていきましょう。

Pair / Entry / Tuple の関係を整理する

似た用語が並ぶので、最初に頭の中を整理します。Pair は要素 2 つの組、Tuple (タプル) はもっと一般的に「N 個の値の組」を指す概念です。EntryMap のエントリ (キー + 値) を指す言葉で、Pair の用途に近いものです。

  • Pair<K, V>keyvalue の 2 つを持つ自作の組
  • Map.Entry<K, V>HashMap の中で使われている標準のキー値ペア
  • Tuple — 任意個の値を持つ組 (Java 本体にはなく、Apache Commons などのライブラリにある)
diagram (will load when visible)

図のように、Pair<K, V>Map.Entry<K, V> と概念的にほぼ同じです。違いは、Map.EntryMap 専用のインターフェースとして用意されているのに対し、Pairどこでも使える汎用的な組 として書ける、という点です。

型パラメータ <K, V> の宣言

複数型パラメータの書き方は、1 個のときと考え方は同じです。クラス名の右側にカンマ区切りで並べるだけです。HashMap<K, V>K V という名前はそのまま借りるのが慣習で、Key Value の頭文字を意味します。

Java

public class Pair<K, V> { private final K key; private final V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } @Override public String toString() { return key + ":" + value; } }

ポイントは次のとおりです。フィールドを final にしておくと、Pair が生成された後にキーや値が書き換えられない 不変オブジェクト になります。スレッド間で共有しても安全になり、バグも減るので、Pair のような値クラスではぜひ final を付けたいところです。

ジェネリクスの型パラメータ名は T E K V R などの 1 文字 が慣習です。TTypeEElement (List<E>)、K VKey Value (Map<K, V>)、RResult を表します。意味が伝わる文字を選ぶと読みやすくなります。

使う側は、new Pair<>("Alice", 30) のように ダイヤモンド演算子 で型を省略できます。Pair<String, Integer> p = new Pair<>("Alice", 30); と書けば、p.getKey()Stringp.getValue()Integer になります。

Record で書くと 1 行で済む

上のコードを見ると、Pair クラスは「フィールド + コンストラクタ + getter + toString」のほぼお決まりの形ばかりで、書くのが面倒です。Java 14 で preview 導入され、Java 16 で正式機能になった Record を使うと、これが たった 1 行 で書けます。

Java

public record PairRecord<K, V>(K key, V value) { @Override public String toString() { return key + ":" + value; } }

record というキーワードと、その後ろの (K key, V value) だけで、コンパイラが自動で次を生成してくれます。

  • private final K key; private final V value; のフィールド
  • public PairRecord(K key, V value) のコンストラクタ (正準コンストラクタ)
  • key() value() のアクセッサ (getKey ではなく key)
  • equals hashCode toString の自動実装

Java の RecordKotlindata classScalacase class に相当する仕組みです。「値を入れるだけのシンプルなオブジェクト」を一発で定義できるので、DTO (Data Transfer Object) や Pair のような値クラスにぴったりです。

アクセッサのメソッド名は getKey ではなく key になる点だけ気をつけてください。歴史的に JavaBeansgetXxx 慣習があるので、Recordkey() value() は最初は違和感がありますが、これは仕様なので慣れるしかありません。

equalshashCode が自動で実装される

Record のもう 1 つの大きな利点が、equalshashCode を自動で実装してくれることです。普通のクラスで Pair を作ると、何もしない限り equals は「同じインスタンスかどうか」しか判定しません。

Java

Pair<String, Integer> a = new Pair<>("Alice", 30); Pair<String, Integer> b = new Pair<>("Alice", 30); System.out.println(a.equals(b)); // false (オブジェクトとして別物)

一方、Record 版なら フィールドの値が同じなら等しい と自動判定されます。

Java

PairRecord<String, Integer> a = new PairRecord<>("Alice", 30); PairRecord<String, Integer> b = new PairRecord<>("Alice", 30); System.out.println(a.equals(b)); // true System.out.println(a.hashCode() == b.hashCode()); // true

この仕組みのおかげで、HashMap のキーとして PairRecord を使ったり、Set<PairRecord<String, Integer>> で重複排除したりが、追加コードなしでうまく動きます。一方、普通のクラスで Pair を作るときは、equalshashCode必ずペアで オーバーライドしないと、HashMap HashSet で正しく動かないので注意してください。

equals だけ書いて hashCode を書かない」は、Java で最もよく踏むバグの 1 つです。a.equals(b)true のとき、a.hashCode() == b.hashCode() も必ず true でなければならない、というのが Java の規約です。これを破ると HashMap がキーを見失います。

動きを追ってみる

今回の課題では、Pair<String, Integer> を使って 名前:年齢 形式の文字列を返す formatPair メソッドを書きます。実行の流れは次のとおりです。

diagram (will load when visible)

図のとおり、Pair のコンストラクタで 2 つの値を保持し、getKey getValue で取り出して文字列連結する、というシンプルな流れです。IntegerString に連結するときは Java が自動で toString を呼んでくれるので、明示的な変換は不要です。

よくある間違い

ここで、ジェネリクスを使った自作クラスでよくハマる落とし穴を 3 つ紹介します。

  • 型パラメータの順序を逆にするPair<K, V>new Pair<Integer, String>(30, "Alice") のように逆に渡してしまうと、getKey()IntegergetValue()String になります。Map<K, V> も同じ順序で、Key → Value が世界共通の並びだと覚えておきましょう
  • equals を書かずに HashSet HashMap で使う — 自作 PairSet に入れて重複排除しようとしても、equalshashCode を実装していないと別物として扱われます。値クラスは Record で書くか、equalshashCode を必ずセットで実装しましょう
  • フィールドを可変にしてしまうkey valuefinal にせず、setKey のような setter を生やしてしまうと、Pair がいつでも変わる箱になってしまいます。HashMap のキーとして使った後に値を書き換えると、ハッシュ値が変わってキーを見失う、という最悪のバグを引き起こします。値クラスは必ず 不変 (イミュータブル) にしましょう

もう 1 つ補足すると、new Pair<String, Integer>("Alice", 30) のように右辺でも型を書いてしまう古い書き方は、現代の Java では非推奨です。new Pair<>("Alice", 30)ダイヤモンド演算子 で揃えて、左辺だけに型を書くのが標準スタイルです。

一度作った Pair の中身を書き換えたくなったら、新しい Pair を作り直すのが正解です。Record ならコンストラクタを呼び直すだけで簡単に「キーだけ差し替えたコピー」を作れます。これは関数型プログラミングの考え方と相性が良いスタイルです。

やってみよう

それでは課題に挑戦してみましょう。やることは次のとおりです。

  1. Solution クラスの中 (もしくは別ファイル) に、Pair<K, V> クラス相当を定義する (普通のクラスでも Record でも OK)
  2. Solution.formatPair(String name, int age) メソッドの中で、Pair<String, Integer> を新規作成する
  3. key + ":" + value の形に組み立てて、name:age 形式の文字列を return する

戻り値の例は次のとおりです。formatPair("Alice", 30) なら "Alice:30"formatPair("Bob", 25) なら "Bob:25"formatPair("", 0) なら ":0" です。name が空文字の場合でも、コロンと 0 を出力すれば OK です。

慣れてきたら、PairRecord 版に書き換えてみたり、Pair<String, String>"red""赤" のような単語と訳語のペアを表現してみたり、Pair をフィールドに持つ別のクラスを作ってみると、複数型パラメータの威力がより実感できます。次のレッスンでは、この Pair を使って 境界付き型パラメータ (extends Comparable) に踏み込んでいきます。

よくある質問

Q. 中級の内容は実務でどれくらい使いますか?

A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。

Q. Stream と for ループはどっちで書くべき?

A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。

Q. 次のステップでは何を学ぶべきですか?

A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。

次のレッスン

次は ジェネリクス まとめクイズ で、ジェネリクス まとめクイズ を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. Pair の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. Pair とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Solution の中 (内部クラスでも record でも可) に、複数型パラメータ <K, V> を持つ Pair 相当のクラスを定義すること
  2. formatPair(String name, int age) の中で Pair<String, Integer>new で生成し、keyvalue を取り出して name:age 形式の文字列を組み立てること
  3. 戻り値の区切り文字は半角コロン (:) ちょうど 1 つで、前後にスペースを入れないこと

入出力例

test-cases.txt

formatPair("Alice", 30)"Alice:30" formatPair("Bob", 25)"Bob:25" formatPair("", 0)":0" formatPair("タロウ", 120)"タロウ:120"

ヒント

main.java
main.java
学習モード

メモ

Pair<K,V> を作る

⌘S で保存