自己制限型は複雑に聞こえるかもしれませんが、実際にはサブクラスが型パラメータを乱用するのを防ぐためのものです。ジェネリッククラス A
があり、任意の型のパラメータを受け入れることができるとします:
class A<T> {
T property;
void setProperty(T t) { property = t; }
T getProperty() { return property; }
}
ここで、A
は任意の型のパラメータを使用できます。たとえば、A<Integer>
や A<String>
のようなものです。もし、B
というクラスがあり、A
を継承したいが、A
の型パラメータを B
に固定したい場合、通常は次のように書きます:
class B extends A<B> {}
このパターンは奇妙な再帰ジェネリックと呼ばれます。要するに、B
は A
を継承し、A
の中で B
を型パラメータとして使用しているということです。
問題はどこにあるのか?#
問題は、この方法では必ずしも B
自体を型パラメータとする必要がないということです。たとえば、次のように書くこともできます:
class C {}
class B extends A<C> {}
これにより、B
は A
を継承していますが、型パラメータに C
を使用しており、私たちが望んでいるルールを完全に破っています。
自己制限型の解決策#
この問題を解決するために、自己制限型を使用します:
class A<T extends A<T>> {}
これにより、T
というジェネリックパラメータは、A<T>
を継承するクラスであることが強制されます。つまり、サブクラスが A
を継承する際には、自分自身を型パラメータとして渡さなければなりません。
したがって、今は:
class C {}
class B extends A<C> {} // コンパイルエラー
上記のコードはコンパイルエラーになります。なぜなら、C
は A<C>
を継承していないからです。一方、次のコードは正当です:
class B extends A<B> {} // コンパイル成功
これにより、B
が A
を継承する際に、型パラメータは必ず B
自体であることが保証されます。
自己制限型の実際の応用:MyBatis-Plus Wrapper#
MyBatis-Plus は人気のある ORM フレームワークであり、Wrapper
はクエリ条件を構築するための一般的なユーティリティクラスです。Wrapper
クラスとそのサブクラス(QueryWrapper
、UpdateWrapper
など)は、自己制限型を大量に使用しており、返り値の型が呼び出し元と一致するようにして、フルエント API の呼び出しを実現しています。
Wrapper
の定義#
簡単な定義として、AbstractWrapper
クラスを見てみましょう:
public abstract class AbstractWrapper<T, R, This extends AbstractWrapper<T, R, This>> {
// いくつかのフィールドとメソッドがあると仮定します
public This eq(R column, Object val) {
// ロジックの実装
return (This) this;
}
public This like(R column, Object val) {
// ロジックの実装
return (This) this;
}
// 他の条件メソッド...
}
この定義では、This
は自己制限型パラメータであり、AbstractWrapper
自体を継承します。これは、AbstractWrapper
を継承するすべてのクラスが、自身をThis
の型パラメータとして渡さなければならないことを意味します。これにより、メソッドチェーンの返り値が具体的なサブクラスの型であることが保証され、基本クラスの型ではないことが保証されます。
QueryWrapper
の実装#
次に、QueryWrapper
の簡単な実装を見てみましょう:
public class QueryWrapper<T> extends AbstractWrapper<T, String, QueryWrapper<T>> {
// ここで追加のクエリ条件を定義できます
}
QueryWrapper
はAbstractWrapper
を継承し、第三のジェネリックパラメータThis
としてQueryWrapper<T>
を渡します。これにより、eq
やlike
などのメソッドを呼び出した場合、返り値の型は引き続きQueryWrapper<T>
であることが保証され、フルエント API の一貫性が確保されます。
サンプルコード#
User
テーブルがあり、QueryWrapper
を使用してクエリ条件を構築したいとします:
QueryWrapper<User> query = new QueryWrapper<>();
query.eq("name", "John")
.like("email", "gmail.com");
// 最終的にSQLクエリを実行
List<User> users = userMapper.selectList(query);
上記のコードでは、eq
やlike
メソッドのそれぞれの返り値はQueryWrapper<User>
オブジェクトであり、他の条件メソッドをチェーンで呼び出すことができます。
自己制限型の役割#
この設計では、自己制限型の役割が非常に明確です:
- 型安全性:間違った型の返り値を防ぐことができます。たとえば、
QueryWrapper
のeq
メソッドがQueryWrapper
を返すことを保証し、親クラスのAbstractWrapper
ではないことを保証します。 - メソッドチェーン:自己制限型の使用により、メソッドチェーンの一貫性が保たれ、コードがより簡潔で直感的になります。
まとめ#
自己制限型は、クラスが継承する際に自分自身を型パラメータとして使用する必要があるようにするテクニックです。これにより、型の渡し間違いを防ぐことができます。このような書き方は見た目が複雑に見えるかもしれませんが、厳密な型の制御が必要な場面で非常に役立ちます。実際の開発では、MyBatis-Plus などのフレームワークが自己制限型を使用することで、よりエレガントで型安全な API 設計を実現しています。
この記事は Mix Space から xLog に同期されています。
オリジナルのリンクは https://me.liuyaowen.club/posts/default/20240821and1 です。