はじめに
本稿では、ジェネリックを利用してコンパイル時の動的処理およびクライアント関連の型安全性(type-safety)を実現する方法について解説します。一般的に、サブクラス化を行う際の最も重要な側面は、クラス固有の機能を実現するために、いかにして同じメソッドパラメータを使ってオーバーライドを実現するかということです。場合によっては、クラス固有のパラメータが必要になることもあるかもしれません。さらに、オーバーライドメソッドが、こうしたクラス固有パラメータのスーパークラスであるパラメータを使用する場合も考えられます。このようなメソッドの例としては、パブリックAPIを通じて公開され、具象実装クラス内でオーバーライドされるメソッドが挙げられます。
シナリオ
本稿で取り上げるのは非常によくあるシナリオで、多くの人が過去に直面したことがあり、同じような方法で解決してきたのではないかと思います。ここでは、単純なレンタカーシステムの例を使ってジェネリックの利用方法を説明します。
今回の例では、メインのレンタルサービスクラスをRentVehicleManagerとしますが、このクラスはインターフェイスにしても抽象クラスにしてもかまいません。RentCarManagerとRentBikeManagerはRentVehicleManagerのサブクラスで、RentVehicleManagerのメソッドをそれぞれの機能に合わせてオーバーライドします。このシナリオの実用的な実装ソリューションとしては、次のようなものが考えられます。
- 一連のManagerクラスの作成にFactoryパターンを使用する
- パラメータの共通階層を作り、メソッド内で厳密なチェックを行う
- ジェネリックを利用した共通階層を使用する
方法1は、関連クラスのインスタンス生成という点では優れたソリューションですが、オブジェクトパラメータの一般化は実現できません。
方法2のソリューションは、たとえば図2のようになります。この場合、個々のオーバーライドメソッドは、そのクラスにふさわしい型の値が引数に渡されているかどうかをメソッドの冒頭で厳密にチェックしなければなりません。
この場合のインターフェイスは、たとえば次のようになります。
package com.sumithp.codeguru.nongeneric.vehicle;
import com.sumithp.codeguru.vehicle.domain.Vehicle;
public interface RentVehicleMgr {
public void rentOut(Vehicle vehicle);
public void checkIn(Vehicle vehicle);
public void diagnose(Vehicle vehicle);
public void repair(Vehicle vehicle);
}
方法2の場合、バイクレンタルに関する実装はたとえば次のようになります。
package com.sumithp.codeguru.nongeneric.vehicle;
import com.sumithp.codeguru.vehicle.domain.Vehicle;
public class RentBikeMgrImpl implements RentVehicleMgr {
// If we don't use Vehicle as the parameter here, the clients
// will not be able to use a generalized interface to call our
// methods.
public void rentOut(Vehicle vehicle) {
// if (vehicle instanceof bike)
// Renting Out Related DB Operations
}
public void checkIn(Vehicle vehicle) {
// if (vehicle instanceof bike)
// Vehicle Check In Related DB Operations
}
public void diagnose(Vehicle vehicle) {
// if (vehicle instanceof bike)
// Self Diagnose functionality of a vehicle
// Print diagnosis
}
public void repair(Vehicle vehicle) {
// if (vehicle instanceof bike)
// Perform pre-defined repair
// Print repair details
}
}
方法2の場合、コンパイル時のクライアント側の使い方は次のようになるでしょう。
package com.sumithp.codeguru.nongeneric.vehicle.client;
import com.sumithp.codeguru.nongeneric.vehicle.RentBikeMgrImpl;
import com.sumithp.codeguru.nongeneric.vehicle.RentCarMgrImpl;
import com.sumithp.codeguru.nongeneric.vehicle.RentVehicleMgr;
import com.sumithp.codeguru.vehicle.domain.Bike;
import com.sumithp.codeguru.vehicle.domain.Car;
import com.sumithp.codeguru.vehicle.domain.Vehicle;
public class RentNonGenericVehicleClient {
public void rentBike() {
// You want only one interface to handle all rentals
RentVehicleMgr rentVehicleMgr;
rentVehicleMgr = new RentBikeMgrImpl();
Vehicle vehicle = new Bike(104,"TWO",true,150);
rentVehicleMgr.rentOut(vehicle);
/*
* Client can as well do this
*
* Vehicle vehicle = new Car(104,"FOUR",true,"PETROL");
* rentVehicleMgr.rentOut(vehicle);
*
* If there are no instanceof checks, this bombs!
*
*/
}
public void rentCar() {
// You want only one interface to handle all rentals
RentVehicleMgr rentVehicleMgr;
rentVehicleMgr = new RentCarMgrImpl();
Vehicle vehicle = new Car(104,"FOUR",true,"PETROL");
rentVehicleMgr.rentOut(vehicle);
/*
* Client can do the same as shown for rentBike()
*
* Vehicle vehicle = new Bike(104,"TWO",true,150);
* rentVehicleMgr.rentOut(vehicle);
*
* If there are no instanceof checks, this bombs too!
*
*/
}
}
方法3は、最も完成度が高く、有効なソリューションです。必要なメソッド群は1つのクラスで公開され、各メソッドはジェネリック変数を通じてそれぞれの実装を提供するため、一貫性に優れています。それでは、このソリューションについて詳しく見ていきましょう。
Javaジェネリックを使った共通階層
ジェネリックの大きな長所は、その「型消去(Type Erasure)」という性質です。これはつまり、コンパイル時チェックのみが行われ、その後はジェネリック変数が消去され、実行時の検証は行われないということを意味します。一方、これには短所もあり、サードパーティ製のコードと併用するときに安全性が保証されないなどの問題があります(内部コードでも、ジェネリックを使用しないコードであれば同様の問題が起こります)。
このソリューションを実現するには、方法2に簡単な変更を加え、階層内の各クラスに新しいジェネリック変数を追加します。トップレベルでは、RentVehicleManagerの宣言を、Vehicleを拡張(extends)する型のジェネリック変数を使用するように書き換えます。次に例を示します。
package com.sumithp.codeguru.generic.vehicle;
import com.sumithp.codeguru.vehicle.domain.Vehicle;
public interface RentVehicleMgr< T extends Vehicle > {
public void rentOut(T vehicle);
public void checkIn(T vehicle);
public void diagnose(T vehicle);
public void repair(T vehicle);
}
さらに、RentVehicleManagerを継承するクラス(つまりRentBikeManagerとRentCarManager)の宣言を、それぞれにふさわしい型のジェネリック変数を使用するように書き換えます。たとえばカーレンタルに関する実装は次のようになります。
package com.sumithp.codeguru.generic.vehicle;
import com.sumithp.codeguru.vehicle.domain.Car;
public class RentCarMgrImpl implements RentVehicleMgr< Car > {
// Can use Car as parameter here, as well as allow
// clients to have a generalized interface
public void rentOut(Car car) {
// Renting Out Related DB Operations
}
public void checkIn(Car car) {
// Vehicle Check In Related DB Operations
}
public void diagnose(Car car) {
// Self Diagnose functionality of a vehicle
// Print diagnosis
}
public void repair(Car car) {
// Perform pre-defined repair
// Print repair details
}
}
これはつまり、クライアント側から特定の種類のManagerクラスのメソッドを呼び出すときは、目的クラスに対応する型のオブジェクトを渡さなければならないということです。これにより、Manager実装クラスはメソッド内で厳密なチェックを行わずに済むようになります。また、こうしたサブクラスのメソッドを修正するときに、instanceofチェックを含める必要はなくなります。
これでクライアントコードは一層すっきりし、安全になります。一般的な実装は次のようになります。
package com.sumithp.codeguru.generic.vehicle.client;
import com.sumithp.codeguru.generic.vehicle.RentBikeMgrImpl;
import com.sumithp.codeguru.generic.vehicle.RentCarMgrImpl;
import com.sumithp.codeguru.generic.vehicle.RentVehicleMgr;
import com.sumithp.codeguru.vehicle.domain.Bike;
import com.sumithp.codeguru.vehicle.domain.Car;
public class RentGenericVehicleClient {
public void rentBike() {
// You want only one interface to handle all rentals
RentVehicleMgr< Bike > rentVehicleMgr;
rentVehicleMgr = new RentBikeMgrImpl();
Bike bike = new Bike(104,"TWO",true,150);
rentVehicleMgr.rentOut(bike);
/*
* Client cannot do this:
*
* Vehicle vehicle = new Car(104,"FOUR",true,"PETROL");
* rentVehicleMgr.rentOut(vehicle);
*
* Even if there are no instanceof checks, all is well!
* Client is absolutely clear on what he needs to do.
*
*/
}
public void rentCar() {
// You want only one interface to handle all rentals
RentVehicleMgr< Car > rentVehicleMgr;
rentVehicleMgr = new RentCarMgrImpl();
Car car = new Car(104,"FOUR",true,"PETROL");
rentVehicleMgr.rentOut(car);
/*
* Client cannot do the following as shown for rentBike():
*
* Vehicle vehicle = new Bike(104,"TWO",true,150);
* rentVehicleMgr.rentOut(vehicle);
*
* Even if there are no instanceof checks, all is well!
* Client is absolutely clear on what he needs to do.
*
*/
}
}
このような設計にすると、保守やコーディングがしやすくなるだけでなく、実装についての理解もしやすくなります。この設計方法は、サービスや機能の実装側クラスだけでなく、サービスのコンシューマ側クラスにとってもメリットがあります。
まとめ
本稿で紹介したジェネリックの使い方は動的継承に役立ちますが、これはコンパイル時の動的処理を実現するだけです。前にも述べたとおり、実行時にはジェネリック変数はクラスから削除されます。
4年以上にわたりJava/J2EEテクノロジを使ったアプリケーションの設計/開発に携わる。現在はSymantec India(インド、プネー)の開発者として勤務。SRSIT(インド、バンガロール)にて情報工学の学士号を取得。Sun Certified Java Programmerの資格を持つ。