Pair<K,V> を作る
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 個の値の組」を指す概念です。Entry は Map のエントリ (キー + 値) を指す言葉で、Pair の用途に近いものです。
Pair<K, V>—keyとvalueの 2 つを持つ自作の組Map.Entry<K, V>—HashMapの中で使われている標準のキー値ペアTuple— 任意個の値を持つ組 (Java本体にはなく、Apache Commonsなどのライブラリにある)
図のように、Pair<K, V> は Map.Entry<K, V> と概念的にほぼ同じです。違いは、Map.Entry が Map 専用のインターフェースとして用意されているのに対し、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 を付けたいところです。
ジェネリクスの型パラメータ名は
TEKVRなどの 1 文字 が慣習です。TはType、EはElement(List<E>)、KVはKeyValue(Map<K, V>)、RはResultを表します。意味が伝わる文字を選ぶと読みやすくなります。
使う側は、new Pair<>("Alice", 30) のように ダイヤモンド演算子 で型を省略できます。Pair<String, Integer> p = new Pair<>("Alice", 30); と書けば、p.getKey() は String、p.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)equalshashCodetoStringの自動実装
Java の
RecordはKotlinのdata classやScalaのcase classに相当する仕組みです。「値を入れるだけのシンプルなオブジェクト」を一発で定義できるので、DTO(Data Transfer Object) やPairのような値クラスにぴったりです。
アクセッサのメソッド名は getKey ではなく key になる点だけ気をつけてください。歴史的に JavaBeans の getXxx 慣習があるので、Record の key() value() は最初は違和感がありますが、これは仕様なので慣れるしかありません。
equals と hashCode が自動で実装される
Record のもう 1 つの大きな利点が、equals と hashCode を自動で実装してくれることです。普通のクラスで 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 を作るときは、equals と hashCode を 必ずペアで オーバーライドしないと、HashMap HashSet で正しく動かないので注意してください。
「
equalsだけ書いてhashCodeを書かない」は、Java で最もよく踏むバグの 1 つです。a.equals(b)がtrueのとき、a.hashCode() == b.hashCode()も必ずtrueでなければならない、というのがJavaの規約です。これを破るとHashMapがキーを見失います。
動きを追ってみる
今回の課題では、Pair<String, Integer> を使って 名前:年齢 形式の文字列を返す formatPair メソッドを書きます。実行の流れは次のとおりです。
図のとおり、Pair のコンストラクタで 2 つの値を保持し、getKey getValue で取り出して文字列連結する、というシンプルな流れです。Integer を String に連結するときは Java が自動で toString を呼んでくれるので、明示的な変換は不要です。
よくある間違い
ここで、ジェネリクスを使った自作クラスでよくハマる落とし穴を 3 つ紹介します。
- 型パラメータの順序を逆にする —
Pair<K, V>をnew Pair<Integer, String>(30, "Alice")のように逆に渡してしまうと、getKey()がInteger、getValue()がStringになります。Map<K, V>も同じ順序で、Key → Valueが世界共通の並びだと覚えておきましょう equalsを書かずにHashSetHashMapで使う — 自作PairをSetに入れて重複排除しようとしても、equalsとhashCodeを実装していないと別物として扱われます。値クラスはRecordで書くか、equalsとhashCodeを必ずセットで実装しましょう- フィールドを可変にしてしまう —
keyvalueをfinalにせず、setKeyのような setter を生やしてしまうと、Pairがいつでも変わる箱になってしまいます。HashMapのキーとして使った後に値を書き換えると、ハッシュ値が変わってキーを見失う、という最悪のバグを引き起こします。値クラスは必ず不変(イミュータブル) にしましょう
もう 1 つ補足すると、new Pair<String, Integer>("Alice", 30) のように右辺でも型を書いてしまう古い書き方は、現代の Java では非推奨です。new Pair<>("Alice", 30) の ダイヤモンド演算子 で揃えて、左辺だけに型を書くのが標準スタイルです。
一度作った
Pairの中身を書き換えたくなったら、新しい Pairを作り直すのが正解です。Recordならコンストラクタを呼び直すだけで簡単に「キーだけ差し替えたコピー」を作れます。これは関数型プログラミングの考え方と相性が良いスタイルです。
やってみよう
それでは課題に挑戦してみましょう。やることは次のとおりです。
Solutionクラスの中 (もしくは別ファイル) に、Pair<K, V>クラス相当を定義する (普通のクラスでもRecordでも OK)Solution.formatPair(String name, int age)メソッドの中で、Pair<String, Integer>を新規作成するkey + ":" + valueの形に組み立てて、name:age形式の文字列をreturnする
戻り値の例は次のとおりです。formatPair("Alice", 30) なら "Alice:30"、formatPair("Bob", 25) なら "Bob:25"、formatPair("", 0) なら ":0" です。name が空文字の場合でも、コロンと 0 を出力すれば OK です。
慣れてきたら、Pair を Record 版に書き換えてみたり、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 つができることを確認しましょう。
- Pair の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Pair とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Solutionの中 (内部クラスでも record でも可) に、複数型パラメータ<K, V>を持つPair相当のクラスを定義することformatPair(String name, int age)の中でPair<String, Integer>をnewで生成し、keyとvalueを取り出してname:age形式の文字列を組み立てること- 戻り値の区切り文字は半角コロン (
:) ちょうど 1 つで、前後にスペースを入れないこと
入出力例
test-cases.txt
formatPair("Alice", 30) → "Alice:30"
formatPair("Bob", 25) → "Bob:25"
formatPair("", 0) → ":0"
formatPair("タロウ", 120) → "タロウ:120"