ワイルドカード (?, extends, super)
CoddyのJavaジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 57/87。
境界付き型パラメータはジェネリクスを特定の型に制限しますが、既存のジェネリック型を扱っていて、正確な型パラメータがわからない、あるいは気にする必要がない場合はどうすればよいでしょうか?ここでワイルドカードの出番です。
非境界ワイルドカード ? は、未知の型を表します。これは、任意のパラメータ化された型を受け入れたい場合に便利です:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printList(new ArrayList<String>()); // 動作します
printList(new ArrayList<Integer>()); // 動作します上限付きワイルドカード ? extends Type は、指定された型またはそのサブクラスのいずれかを受け入れます。ジェネリック構造から読み取る必要がある場合に使用します:
public static double sumNumbers(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
sumNumbers(Arrays.asList(1, 2, 3)); // Integerのリスト
sumNumbers(Arrays.asList(1.5, 2.5)); // Doubleのリスト下限付きワイルドカード ? super Type は、指定された型またはそのスーパークラスのいずれかを受け入れます。ジェネリック構造に書き込む必要がある場合に使用します:
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
List<Number> numbers = new ArrayList<>();
addIntegers(numbers); // 動作します - NumberはIntegerのスーパークラスです役立つガイドライン:読み取りのみを行う(プロデューサー)場合は extends を使用し、書き込みのみを行う(コンシューマー)場合は super を使用します。これは PECS 原則(Producer Extends, Consumer Super)として知られています。
チャレンジ
簡単3つのすべての種類のワイルドカードを実演する動物園の給餌システムを構築しましょう!動物と食べ物の階層を作成し、非境界、上限境界、下限境界のワイルドカードを使用して、さまざまな給餌シナリオを処理するユーティリティメソッドを作成します。
コードを4つのファイルに分けて構成します:
Food.java: 食べ物のためのシンプルなクラス階層を作成します。まず、プライベートフィールドname(String)、それを初期化するコンストラクタ、およびgetName()メソッドを持つベースクラスFoodを作成します。次に、同じファイル内に2つのサブクラスを作成します:Meatは Food を継承し(コンストラクタはsuper(name)を呼び出す)、Vegetableは Food を継承します(同じパターン)。Animal.java: プライベートフィールドspecies(String)、コンストラクタ、およびgetSpecies()メソッドを持つベースクラスAnimalを作成します。2つのサブクラスCarnivoreとHerbivoreを作成し、それぞれ Animal を継承して親コンストラクタを呼び出します。ZooFeeder.java: ここでワイルドカードが真価を発揮します!3つの static メソッドを持つユーティリティクラスを作成します:printInventory(List<?> items)- 非境界ワイルドカードを使用して、任意のリストを出力します。ループ処理を行い、toString()を使用して各項目を1行に1つずつ出力します。calculateTotalFood(List<? extends Food> foods)- 上限境界ワイルドカードを使用して、任意の Food 型のリストから読み取ります。項目の数をintとして返します。これはプロデューサー(生産者)からの読み取りを実演します。addMeatToStock(List<? super Meat> stock, String meatName)- 下限境界ワイルドカードを使用して、Meat を保持できるリストに書き込みます。指定された名前で新しいMeatオブジェクトを作成し、それを stock に追加します。これはコンシューマー(消費者)への書き込みを実演します。Main.java: 動物園の給餌システムを完成させましょう!肉の名前 (String) と野菜の名前 (String) の2つの入力を受け取ります。まず、
ArrayList<Food>を作成し、肉の名前を持つMeatオブジェクトと、野菜の名前を持つVegetableオブジェクトを追加します。Food inventory:と出力してから、このリストを引数にprintInventoryを呼び出します。次に、空行を出力し、
calculateTotalFoodを使用してTotal food items: [count]と出力します。最後に、
meatStockという名前の新しいArrayList<Food>を作成します。空行を出力し、次にAdding to meat stock...と出力します。このリストと文字列"Beef"を指定してaddMeatToStockを呼び出します。その後、Meat stock after adding:と出力し、meat stock に対してprintInventoryを呼び出します。
肉の名前と野菜の名前の2つの入力を順番に受け取ります。
出力が正しく機能するように、Food クラスで toString() をオーバーライドして食べ物の名前を返すようにしてください。各ワイルドカードの型がどのように異なる目的を果たすかに注目してください:単に Object として読み取る必要がある場合の柔軟性を最大にする ?、特定の型を読み取る場合の ? extends、そしてコレクションに書き込む場合の ? super です!
チートシート
Javaは、正確な型パラメータが不明または無関係な場合にジェネリック型を扱うための3種類のワイルドカードを提供しています。
非限定ワイルドカード (?)
未知の型を表し、任意のパラメータ化された型を受け入れます:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printList(new ArrayList<String>()); // 動作します
printList(new ArrayList<Integer>()); // 動作します上限境界ワイルドカード (? extends Type)
指定された型またはそのサブクラスのいずれかを受け入れます。ジェネリック構造からデータを読み取る必要がある場合に使用します:
public static double sumNumbers(List<? extends Number> numbers) {
double sum = 0;
for (Number n : numbers) {
sum += n.doubleValue();
}
return sum;
}
sumNumbers(Arrays.asList(1, 2, 3)); // Integerのリスト
sumNumbers(Arrays.asList(1.5, 2.5)); // Doubleのリスト下限境界ワイルドカード (? super Type)
指定された型またはそのスーパークラスのいずれかを受け入れます。ジェネリック構造にデータを書き込む必要がある場合に使用します:
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
List<Number> numbers = new ArrayList<>();
addIntegers(numbers); // 動作します - NumberはIntegerのスーパークラスですPECS原則
Producer Extends, Consumer Super: 読み取りのみを行う場合(プロデューサー)はextendsを使用し、書き込みのみを行う場合(コンシューマー)はsuperを使用します。
自分で試してみよう
import java.util.Scanner;
import java.util.ArrayList;
import java.util.List;
class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String meatName = scanner.nextLine();
String vegetableName = scanner.nextLine();
// TODO: ArrayList<Food> を作成し、Meat と Vegetable オブジェクトを追加する
// TODO: "Food inventory:" を出力し、その後 printInventory を呼び出す
// TODO: 空行を出力し、次に calculateTotalFood を使用して "Total food items: [count]" を出力する
// TODO: meatStock という名前の新しい ArrayList<Food> を作成する
// TODO: 空行を出力し、次に "Adding to meat stock..." を出力する
// TODO: meatStock と "Beef" を引数に指定して addMeatToStock を呼び出す
// TODO: "Meat stock after adding:" を出力し、その後 meatStock に対して printInventory を呼び出す
}
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
4継承
継承の基本 (extends)super キーワードメソッドのオーバーライド (@Override)コンストラクタチェーンObject クラス単一継承と多段階継承なぜ多重継承ができないのかまとめ:従業員の階層構造