型変数 ~Type variables~
クラス、インターフェース、メソッド、コンストラクタの本体で使用される変数の型定義を、呼び出す側で指定することを可能にするのが型変数です。
型変数を使用して定義されたクラス
import java.util.List;
public class SearchResult<T>{
private List<T> results;
public SearchResult(List<T> results){
this.results = results;
}
public List<T> getResults(){
return results;
}
}
呼び出し側
import java.util.Arrays;
import java.util.List;
class ClientOfResultClass {
public static void main(String[] args){
//String型のListを宣言し、SearchResultの型変数にStringを指定する。
List<String> words = Arrays.asList("Java","Generics");
SearchResult<String> wordSearch = new SearchResult<>(words);
//getResultメソッドを呼び出す。
String firstWord = wordSearch.getResults().get(0);
System.out.println("単語検索の結果:" + firstWord);
//Userクラス型のListを宣言し、SearchResultの型変数にUserを指定する。
List<User> user = Arrays.asList(new User("田中"), new User("佐藤"));
SearchResult<User> userSearch = new SearchResult<>(user);
User firstUser = userSearch.getResults().get(0);
System.out.println("ユーザー検索の結果:" + firstUser.getName());
}
}
class User {
private String name;
public User(String name) { this.name = name; }
public String getName() {return name;}
}
実行結果
/*
単語検索の結果:Java
ユーザー検索の結果:田中
*/
このように型変数を使用することでクラス内の変数型を柔軟にでき、効率的な開発を行えます。
型変数の定義パターン
| パターン | 構文 | 意味 | 呼べるメソッドの範囲 |
|---|---|---|---|
| 境界なし | <T> |
Object の子型として扱われる。 | Object クラスのメソッドのみ (toString() 等) |
| クラス境界 | <T extends C> |
TはクラスCまたはそのサブクラスである必要がある | Cおよびその親クラスの公開メソッド |
| インターフェース境界 | <T extends I> |
TはインターフェースIを実装している必要がある。 | Iで定義された抽象メソッドやデフォルトメソッド |
境界なし
- メリット
- 再利用性の高さ
- データとして保持したいとき、右から左へ受け渡すときに有効 ( 例: List, Optional, Result )
- アルゴリズムの分離
- 型に依存しない不変のプロセスを共通化できる。
- 具体例: リストの要素をシャッフルする、2つの値を入れ替えるなど。
public class DeliveryBox <T> {
private final T content;
private final String destination;
private Status status = Status.PREPARING;
public DeliveryBox(T content, String destination) {
this.content = content;
this.destination = destination;
}
public T getContent() {
return content;
}
public String getDestination() {
return destination;
}
public Status getStatus() {
return status;
}
public void prepare() {
this.status = Status.PREPARING;
System.out.println("--- 準備中 ---");
System.out.println("送り先: " + destination);
System.out.println("荷物内容: " + content);
System.out.println("------------------");
}
public void ship() {
this.status = Status.SHIPPED;
System.out.println("--- 配送中 ---");
System.out.println("送り先: " + destination);
System.out.println("荷物内容: " + content);
System.out.println("------------------");
}
public void delivered() {
this.status = Status.DELIVERED;
System.out.println("--- 完了 ---");
System.out.println(content + " を " + destination + " に配達しました。");
}
}
class DeliverySystemMain{
public static void main(String[] args) {
DeliveryBox<Melon> melonBox = new DeliveryBox<>(new Melon("北海道"),"東京都新宿区");
DeliveryBox<Computer> computerBox = new DeliveryBox<>(new Computer("MacBook Pro"),"大阪府北区");
melonBox.prepare();
computerBox.prepare();
melonBox.ship();
melonBox.delivered();
computerBox.ship();
}
class Melon{
private final String origin;
public Melon(String origin) { this.origin = origin;}
@Override public String toString() {return origin + "産の高級メロン";}
}
class Computer {
private final String model;
public Computer(String model) { this.model = model;}
@Override public String toString() {return model + "モデルのコンピュータ";}
}
--- 準備中 ---
送り先: 東京都新宿区
荷物内容: 北海道産の高級メロン
------------------
--- 準備中 ---
送り先: 大阪府北区
荷物内容: MacBook Proモデルのコンピュータ
------------------
--- 配送中 ---
送り先: 東京都新宿区
荷物内容: 北海道産の高級メロン
------------------
--- 完了 ---
北海道産の高級メロン を 東京都新宿区 に配達しました。
--- 配送中 ---
送り先: 大阪府北区
荷物内容: MacBook Proモデルのコンピュータ
------------------
--- 完了 ---
MacBook Proモデルのコンピュータ を 大阪府北区 に配達しました。
- デメリット
- 「Object」としか扱えない
- 境界を設けずにクラスの型変数を定義した場合、コンパイラはTを全オブジェクトの共通の親であるObjectクラスとして扱う。
- Objectが持つメソッドのみ使用可能で、型変数で指定したクラス固有のメソッドを呼び出せない。
さきほどのサンプルコードで追加要件として、それぞれの商品の状態が良好かを出荷前に確認しなければいけなくなった。
class Melon{
private final String origin;
public Melon(String origin) { this.origin = origin;}
@Override public String toString() {return origin + "産の高級メロン";}
}
class Computer {
private final String model;
public Computer(String model) { this.model = model;}
@Override public String toString() {return model + "モデルのコンピュータ";}
}
public void prepare() {
//content.testHardware(); Error Symbol not found
//content.checkExpiration(); Error Symbol not found
this.status = Status.PREPARING;
System.out.println("--- 準備中 ---");
System.out.println("送り先: " + destination);
System.out.println("荷物内容: " + content);
System.out.println("------------------");
}
このように、境界を設けない設計では配送準備のルーチンの中で、自動的に賞味期限や動作チェックを行うといった、ドメインモデルそれぞれの能力に依存した共通処理が書けない。
インターフェース境界
今回のケースのように、Melon、Computerのそれぞれで出荷前に商品の状態を確認できるといった能力は同じでも、実際の処理内容が異なる場合は、ドメインモデルに対してインターフェースを用いて実装を強制し、DeliveryBoxでインターフェースによる境界を設ける実装が推奨される。
public interface products {
void checkCondition();
}
public class DeliveryBox<T extends products> {
private final T content;
private final String destination;
private Status status = Status.PREPARING;
public DeliveryBox(T content, String destination) {
this.content = content;
this.destination = destination;
}
public T getContent() {
return content;
}
public String getDestination() {
return destination;
}
public Status getStatus() {
return status;
}
public void prepare() {
//配送処理内で状態確認
content.checkCondition();
this.status = Status.PREPARING;
System.out.println("--- 準備中 ---");
System.out.println("送り先: " + destination);
System.out.println("荷物内容: " + content);
System.out.println("------------------");
}
public void ship() {
this.status = Status.SHIPPED;
System.out.println("--- 配送中 ---");
System.out.println("送り先: " + destination);
System.out.println("荷物内容: " + content);
System.out.println("------------------");
}
public void delivered() {
this.status = Status.DELIVERED;
System.out.println("--- 完了 ---");
System.out.println(content + " を " + destination + " に配達しました。");
}
}
class Melon implements products{
private final String origin;
private final boolean isExpired = false;
public Melon(String origin) { this.origin = origin;}
@Override public String toString() {return origin + "産の高級メロン";}
public void checkCondition() {
if (isExpired) {
System.out.println("警告:賞味期限が切れています。");
} else {
System.out.println("賞味期限内であることを確認しました。");
}
}
}
class Computer implements products {
private final String model;
private boolean powerOn = false;
public Computer(String model) { this.model = model;}
@Override public String toString() {return model + "モデルのコンピュータ";}
/**
* 家電固有のメソッド:通電・初期不良の確認
*/
public void checkCondition() {
this.powerOn = true;
System.out.println(model + " の通電確認を完了しました。正常に動作します。");
}
}
クラス境界
インターフェースが 「なにができるかを」を定義するのに対して、クラスは何者であるか、と共通資産(フィールド、メソッド)を扱います。
配送システムの規模が大きくなり、食品と家電を更に細かくカテゴライズする必要性が出てきたとします。
例:食品 → 生鮮食品、冷凍食品
電化製品 → パソコン、家電製品
配送システムで食品、電化製品がそれぞれ出荷前に状態確認を行う必要があり、かつ生鮮食品やパソコンに独自のフィールドやメソッドを持たせたいときにクラスによる継承を用います。
- 食品の親クラス
package foods;
import java.time.LocalDate;
public class Food {
private final String name;
private final LocalDate expiryDate;
public Food(String name, LocalDate expiryDate) {
this.name = name;
this.expiryDate = expiryDate;
}
public String getName() {
return name;
}
public LocalDate getExpiryDate() {
return expiryDate;
}
public boolean isExpired() {
return expiryDate.isBefore(LocalDate.now());
}
}
- Foodを継承したサブクラス
package foods;
import java.time.LocalDate;
public class FreshFood extends Food {
private final String origin;
private final boolean requiresRefrigeration;
public FreshFood(String name, LocalDate expiryDate, String origin, boolean requiresRefrigeration) {
super(name, expiryDate);
this.origin = origin;
this.requiresRefrigeration = requiresRefrigeration;
}
//FreshFood独自のメソッド
public void checkFreshness() {
System.out.println("産地: " + origin);
System.out.println("要冷蔵: " + requiresRefrigeration);
System.out.println(isExpired() ? "賞味期限切れです" : "まだ新鮮です");
}
}
package foods;
import java.time.LocalDate;
public class FrozenFood extends Food {
private final int storageTemperature;
private final int frozenDays;
public FrozenFood(String name, LocalDate expiryDate, int storageTemperature, int frozenDays) {
super(name, expiryDate);
this.storageTemperature = storageTemperature;
this.frozenDays = frozenDays;
}
//FrozenFood独自のメソッド
public void checkFrozenCondition() {
System.out.println("保存温度: " + storageTemperature + "℃");
System.out.println("冷凍日数: " + frozenDays + "日");
System.out.println(isExpired() ? "賞味期限切れです" : "まだ新鮮です");
}
}
// Food、またはFoodを継承したクラスの検査を行うクラス
package Foods;
package foods;
public class FoodBox<T extends Food> {
private final T content;
public FoodBox(T content) {
this.content = content;
}
public T getContent() {
return content;
}
// 状態を検査するメソッド
public void inspect() {
System.out.println("=== Food Box Inspection ===");
System.out.println("内容物: " + content.getName());
System.out.println(content.isExpired() ? "賞味期限切れです" : "まだ新鮮です");
}
}
import foods.Food;
import foods.FoodBox;
import foods.FreshFood;
import foods.FrozenFood;
import java.time.LocalDate;
public class DeliverySystemMain {
public static void main(String[] args) {
LocalDate expiry = LocalDate.of(2030, 12, 31);
Food tuna = new Food("Tuna", expiry);
FreshFood salmon = new FreshFood("Salmon", expiry, "北海道", true);
FrozenFood iceCandy = new FrozenFood("Ice Candy", expiry, -10, 30);
// Food を境界にしているので、Food の子クラスもまとめて扱える
FoodBox<Food> foodBox = new FoodBox<>(tuna);
FoodBox<FreshFood> freshFoodBox = new FoodBox<>(salmon);
FoodBox<FrozenFood> frozenFoodBox = new FoodBox<>(iceCandy);
System.out.println("=== Food の状態確認 ===");
foodBox.inspect();
freshFoodBox.inspect();
frozenFoodBox.inspect();
System.out.println();
System.out.println("=== 派生クラス固有の確認 ===");
salmon.checkFreshness();
iceCandy.checkFrozenCondition();
}
}
=== Food の状態確認 ===
=== Food Box Inspection ===
内容物: Tuna
まだ新鮮です
=== Food Box Inspection ===
内容物: Salmon
まだ新鮮です
=== Food Box Inspection ===
内容物: Ice Candy
まだ新鮮です
=== 派生クラス固有の確認 ===
産地: 北海道
要冷蔵: true
まだ新鮮です
保存温度: -10℃
冷凍日数: 30日
まだ新鮮です
複合境界
前章では、クラス継承を用いることで「食品(Food)」という実体に、生鮮食品や冷凍食品といった具体的な属性を持たせる設計を学びました。しかし、実際の配送現場では「何者であるか(クラス)」という情報だけでなく、「配送可能か(インターフェース)」という状態も同時に満たしていなければなりません。
例えば、検品フローと配送フローが独立していると、状態確認が済んでいない商品が誤って発送されるリスクがあります。そこで、Javaのジェネリクスにおける**複合境界(&)**を活用し、「特定のクラスを継承しており、かつ特定のインターフェースを実装していること」を型レベルで強制する設計に改良します。
- 商品の基盤となるクラス
package types;
public abstract class Product {
protected String name;
public String getName(){
return name;
}
public abstract void printInfo();
}
- 配送前の状態確認ができることを強制するインターフェース
package types;
public interface Deliverable {
//ElectronicsとFoodクラスそれぞれに状態確認の振る舞いを強制するため
void checkCondition();
}
- Productを継承し、deliverableを実装した食品カテゴリの基底クラス
package Foods;
import types.Deliverable;
import java.time.LocalDate;
import types.Product;
public class Food extends Product implements Deliverable{
LocalDate expiryDate;
public Food(String name, LocalDate expiryDate) {
this.name = name;
this.expiryDate = expiryDate;
}
public void checkExpiration(){
System.out.println("賞味期限: " + expiryDate);
}
@Override
public void checkCondition() {
checkExpiration();
System.out.println(name + " is ready for delivery");
}
@Override
public void printInfo() {
System.out.println("Food: " + name + ", Expiry: " + expiryDate);
}
}
- Foodを継承し、保存温度を属性に持つ独自クラス
package Foods;
import java.time.LocalDate;
public class FrozenFood extends Food {
private final int storageTemperature;
public FrozenFood(String name, LocalDate expiryDate, int storageTemperature) {
super(name, expiryDate);
this.storageTemperature = storageTemperature;
}
public void checkTemperature(){
System.out.println("保存温度: " + storageTemperature + "℃");
}
@Override
public void checkCondition(){
super.checkCondition();
checkTemperature();
}
@Override
public void printInfo(){
System.out.println("冷凍食品: " + getName());
}
}
- Productを継承し、Deliverableを実装した家電製品カテゴリの基底クラス
import types.Deliverable;
import types.Product;
public class Electronics extends Product implements Deliverable {
private final int powerConsumptionW;
protected Electronics(String name, int powerConsumptionW) {
this.name = name;
this.powerConsumptionW = powerConsumptionW;
}
public void checkPowerSpec(){
System.out.println("消費電力: " + powerConsumptionW + "W");
}
// implements Deliverable
@Override
public void checkCondition() {
checkPowerSpec();
System.out.println(name + " is ready for delivery");
}
// overrides Product
@Override
public void printInfo() {
System.out.println("家電: " + name);
}
}
- productを継承し、Deliverableを実装したクラスのみ受け入れ、状態確認と配送手順を管理するクラス
import types.Deliverable;
import types.Product;
public class DeliveryBox<T extends Product & Deliverable> {
private final T content;
private final String destination;
private boolean packed;
public DeliveryBox(T content, String destination){
this.content = content;
this.destination = destination;
}
public void pack(){
packed = true;
System.out.println("梱包しました。" + content.getName());
}
public void prepare(){
if(!packed){
throw new IllegalStateException("梱包されていません。");
}
content.checkCondition();
System.out.println("送り先: " + destination);
}
public void ship(){
System.out.println(content.getName() + "を送ります。");
}
}
- 配送フロー全体の実行クラス
import Foods.Food;
import Foods.FrozenFood;
import java.time.LocalDate;
public class DeliverySystemMain {
public static void main(String[] args) {
// 配送対象となる商品の期限を用意する
LocalDate expiry = LocalDate.of(2030, 12, 31);
// 食品カテゴリの具体クラスを生成する
Food tuna = new Food("Tuna", expiry);
// 冷凍食品カテゴリの具体クラスを生成する
FrozenFood iceCandy = new FrozenFood("Ice Candy", expiry, -10);
// 家電カテゴリの具体クラスを生成する
Electronics laptop = new Electronics("Laptop", 20);
// 商品の状態を確認するための箱を生成する
// DeliveryBox は Product と Deliverable を同時に満たす型しか受け取れない
DeliveryBox<Food> foodBox = new DeliveryBox<>(tuna, "東京都新宿区");
DeliveryBox<FrozenFood> frozenFoodBox = new DeliveryBox<>(iceCandy, "神奈川県横浜市");
DeliveryBox<Electronics> electronicsBox = new DeliveryBox<>(laptop, "大阪府北区");
System.out.println("=== 配送前処理 ===");
// 先に梱包しないと検品へ進めない
foodBox.pack();
frozenFoodBox.pack();
electronicsBox.pack();
// 配送前の条件確認を実行する
foodBox.prepare();
frozenFoodBox.prepare();
electronicsBox.prepare();
System.out.println("=== 出荷 ===");
// 出荷処理を実行する
foodBox.ship();
frozenFoodBox.ship();
electronicsBox.ship();
}
}
以上のように、& による複合境界を用いることで、ドメインモデルとしての商品であることと、配送前の状態確認が可能であることを同時に表現できた。 その結果、配送フローにおける検品漏れを防ぎつつ、配送対象に求められる条件を型で明確に示すことができる。
=== 配送前処理 ===
梱包しました。Tuna
梱包しました。Ice Candy
梱包しました。Laptop
賞味期限: 2030-12-31
Tuna is ready for delivery
送り先: 東京都新宿区
賞味期限: 2030-12-31
Ice Candy is ready for delivery
保存温度: -10℃
送り先: 神奈川県横浜市
消費電力: 20W
Laptop is ready for delivery
送り先: 大阪府北区
=== 出荷 ===
Tunaを送ります。
Ice Candyを送ります。
Laptopを送ります。
クラス境界定義時の注意点
・複合境界を設ける際のパラメータは複数指定できるが、Javaの単一継承の言語であるという特性上、複数のクラスを指定するとコンパイルエラーになる。
class sumpleA{...}
class sumpleB{...}
public interface InterfaceA
//Ok
public class Box<T extends sumpleA & interfaceA>
//No
public class Box<T extends sumpleA & sumpleB> {}
・ "型が違う同じ" ジェネリクスインターフェース同士を用いて型変数を定義できない
public class Box <T extends Listable<String> & Listable<Integer> >{
・サブタイプの衝突禁止
interface Converter<T> {
void convert(T data);
}
// 文字列用コンバーター
class StringConverter implements Converter<String> {
public void convert(String data) {...}
}
// 数値用コンバーター
class IntConverter implements Converter<Integer> {
public void convert(Integer data) {...}
}
public class MultiProcessor<T extends StringConverter & IntConverter> {
// ...
}
・クラスは先頭にしか書けない
class sumpleA{...}
public interface Interface****{...}
//↓Ok
public class Box<T extends sumpleA & InterfaceB***> {}
//↓No
public class Box<T extends InterfaceB & sumpleA>
型パラメータにプリミティブ型(int, doubleなど)を渡すことはできない。
ワイルドカード
型パラメータには '参照型' の他にも 'ワイルドカード'を指定することが可能です。、何を渡すかは決まっていないがとりあえずList<>として扱いたいときなどに有効です。
static void printCollection(List<?> lists){...}
- 実用例
複合境界の章では、配送ロジックを扱うDelieryBoxクラスを下記のように定義することで Productクラスを継承し、かつ、Deliverableインターフェースを実装したクラスのみを扱うように制約を設けました。
public class DeliveryBox<T extends Product & Deliverable>
ワイルドカードも同様に特定のクラス、インターフェースを指定して制約を設けることができます。
- 上限境界 (? extends B): B またはそのサブタイプなら何でもOK
- 下限境界 (? super B): B またはそのスーパータイプ(親クラス)なら何でもOK。
public class DistributionCenter {
// Food / FrozenFood / Electronics のような派生型をまとめて読み取る
public void summarizeProducts(List<? extends Product> products){
int totalPrice = 0;
System.out.println("----------we applied this order----------");
for(Product product : products){
System.out.println(product.getName() + " : " + product.getPrice());
totalPrice += product.getPrice();
}
System.out.println("Total price: " + totalPrice);
}
// Product かつ Deliverable を満たす商品だけを、箱に詰めて返す
public void prepareAll(List<? extends Deliverable> items){
System.out.println("----------checkCondition----------");
for(Deliverable item : items){
item.checkCondition();
System.out.println();
}
}
// products には「Product を受け取れる入れ物」を渡す必要がある。
// List<? super Product> なら Product そのもの、またはその親型の List を受け取れるため、
// // そのため、Food や FrozenFood のような Product の子クラスも安全に register できる。z
public void registerProducts(List<? super Product> products, List<? extends Product> incomingProducts){
System.out.println("----------registerProducts----------");
for(Product incomingProduct : incomingProducts){
products.add(incomingProduct);
System.out.println(incomingProduct.getName() + " is registered");
}
}
}
public class DeliverySystemMain {
public static void main(String[] args) {
// 配送対象となる商品の期限を用意する
LocalDate expiry = LocalDate.of(2030, 12, 31);
Food tuna = new Food("Tuna",1000, expiry );
FrozenFood iceCandy = new FrozenFood("Ice Candy",300 , expiry,-10);
Electronics laptop = new Electronics("Laptop", 200000,20);
DistributionCenter distributionCenter = new DistributionCenter();
// Productの派生クラスをまとめて渡して、価格の集計を行う
distributionCenter.summarizeProducts(List.of(tuna, iceCandy,laptop));
// Deliverableインターフェースを実装したクラスのみを渡して、出荷可能か確認する
distributionCenter.prepareAll(List.of(tuna,iceCandy,laptop));
DeliveryBox<Food> foodBox = new DeliveryBox<>(tuna,"東京都板橋区小豆沢");
DeliveryBox<FrozenFood> frozenFoodBox = new DeliveryBox<>(iceCandy,"東京都板橋区小豆沢");
DeliveryBox<Electronics> electronicsBox = new DeliveryBox<>(laptop,"東京都板橋区小豆沢");
List<Product> products = new ArrayList<>();
distributionCenter.registerProducts(products, List.of(tuna, iceCandy, laptop));
System.out.println("----------packing----------");
foodBox.pack();
frozenFoodBox.pack();
electronicsBox.pack();
System.out.println("----------shipped----------");
foodBox.ship();
frozenFoodBox.ship();
electronicsBox.ship();
}
}
ざっくりまとめ:PECSの法則
ワイルドカードを使い分けるコツは、そのリストをメソッド内で 「どう扱うか」 です。
-
Producer-Extends: データを 出す(get) ためのリストなら
? extends T- 例:
summarizeProducts(商品から価格を取り出す)
- 例:
-
Consumer-Super: データを 入れる(add) ためのリストなら
? super T- 例:
registerProducts(リストに商品を追加する)
- 例:
これを意識するだけで、ジェネリクスのエラーに悩まされる回数は激減します!
*** 型消去 ~Type Erasure
コンパイル後、バイトコードの世界ではジェネリクスは削除されます。
- Before
DeliveryBox<Food> foodBox = new DeliveryBox<>(tuna, "板橋区");
Food myFood = foodBox.getContent(); // 普通に受け取れる
- After
// 型消去により、DeliveryBoxの中身を返すメソッドは「Product」を返すようになっている
DeliveryBox foodBox = new DeliveryBox(tuna, "板橋区");
// コンパイラが (Food) というキャストを「こっそり」挿入している!
Food myFood = (Food) foodBox.getContent();
Java 5でジェネリクスが導入されたとき、それ以前の「ジェネリクスがない時代のJava(Java 1.4以前)」で作られたプログラムとも一緒に動かせる必要がありました(後方互換性)。
もし実行時にも List という情報が厳密に残ってしまうと、古い List と混ぜて使えなくなってしまいます。そのため、「コンパイル時に型チェックを完璧に済ませ、実行時には古い Java と同じ形(Objectのリストなど)に変換する」という手法が取られました。
- 消去された方はどこへ行く?
のような境界のない型は Object に置き換わりますが、今回の DeliveryBox のように境界がある場合は、一番左側の親クラス(ここでは Product)に置き換わります。だからこそ、実行時にも Product としての振る舞いが保証されるのです。
まとめ
JLSという1次情報から学ぶことで、言語の裏側にある仕組みがクリアになり、エンジニアとしての「安心感」を得ることができました。言語仕様を読み解く楽しさを感じつつ、今後は黒本での資格勉強と並行してJLSのサマリーを確認するスタイルが、最も効率的で理解が深まると確信しています。まだ黒本には着手していませんが、仕様という「地図」を先に手に入れたことで、今後の演習も迷わず進められそうです。
サンプルコードはこちら