Javaで自分型、つまり自分自身の型を表現する方法について紹介します。
メリットを実感してもらうためにメソッドチェーンを使って説明していきます。
メソッドチェーンというのは
Person person = new Person().setFirstName("太郎").setLastName("日本橋");
のようにSetterのように状態を更新するメソッドの返り値を典型的なSetterのようにvoid
にするのではなく、自分自身を返すことによってメソッドを連鎖させて記述する手法です。
先程のコードはメソッドチェーンを使わないと以下のように書くのではないでしょうか。
Javaのコードなので仕方ないですが、なんだか冗長に感じますね。
Person person = new Person();
person.setFirstName("太郎");
person.setLastName("日本橋");
ただ、Javaの場合、「自分自身」を表す型がないのでメソッドチェーンと継承を組み合わせると少し複雑なことをしないといけません。
それが今回紹介する 擬似自分型(Simulated self type) です。
他の言語ではどうなっているか
TypeScriptだと、メソッドの戻り値として自分自身の型を表せるthis
という型があり、以下のようなコードが書けます。
class SuperClass{
public firstName:string = "";
public lastName: string = "";
public setFirstName(firstName:string): this{
this.firstName = firstName;
return this;
}
public setLastName(lastName: string): this{
this.lastName = lastName;
return this;
}
}
const superClass:SuperClass = new SuperClass().setFirstName("太郎").setLastName("日本橋")
class SubClass extends SuperClass{
public email: string = "";
public setEmail(email: string): this{
this.email = email;
return this;
}
}
const subClass:SubClass = new SubClass().setFirstName("花子").setLastName("丸の内").setEmail("hanako.marunouchi@example.com")
大事な点としては最後の行でnew SubClass().setFirstName("花子")
の戻り値の型がSubClass
になっていることです。
このコードを読むと「何を当たり前のことを。。。」と思うかもしれませんが、Javaにはこのthis
がないので、これから解説していくことになる 擬似自分型(Simulated self type) という少しややこしい手法を使わなくてはならないということを解説していきます。
Javaでの素朴な方法の問題点
Javaの場合どうなるのかというのをみていきます。
まず、メソッドチェーンを普通に実装してみます。
main()
メソッドの中でSuperClass
のオブジェクトを生成してメソッドチェーンで値を設定したあと、フィールドを読み取って標準出力しています。
実行してみると特に問題なく出力されます。
あとで継承を使うのでクラス名はSuperClass
としておきます。
class Main {
public static void main(String[] args) {
SuperClass superClass = new SuperClass().setFirstName("太郎").setLastName("日本橋");
System.out.println("私の名前は%s %sです。".formatted(superClass.getLastName(), superClass.getFirstName()));
}
}
class SuperClass {
private String firstName;
private String lastName;
// Setterが自分自身を返すようにしてメソッドチェーンを実現する
public SuperClass setFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public SuperClass setLastName(String lastName) {
this.lastName = lastName;
return this;
}
// Getterが続く
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
継承してみる
継承してみましょう。親クラスの名前をSuperClass
としているのでサブクラスの名前は当然SubClass
です。
class SubClass extends SuperClass {
// SubClassはメールアドレスも持っている
private String email;
public SubClass setEmail(String email) {
this.email = email;
return this;
}
// Getterが続く
public String getEmail() {
return email;
}
}
このSubClass
に名前、苗字、メールアドレスの順番で設定して、先程のように標準出力してみましょう。
class Main {
public static void main(String[] args) {
SubClass subClass = new SubClass()
.setFirstName("花子")
.setLastName("丸の内")
.setEmail("hanako.marunouchi@example.com");
System.out.println(
"私の名前は%s %sです。宛先はこちら%s。"
.formatted(subClass.getLastName(), subClass.getFirstName(), subClass.getEmail()));
}
}
VSCodeとか使ってるとThe method setEmail(String) is undefined for the type SuperClass
って警告が出ますね。
javac ./Main.java
とコンパイルしてみようとしても以下のようなエラーが出ます。
./Main.java:11: エラー: シンボルを見つけられません
.setEmail("hanako.marunouchi@example.com");
^
シンボル: メソッド setEmail(String)
場所: クラス SuperClass
エラー1個
まあ、setFirstName()
とsetLastName()
の戻り値がSuperクラスなので残念ながら当然です。
聡明な読者は気づいたでしょう。
「setFirstName()
とsetLastName()
をオーバーライドすればいいじゃん!」
class SubClass extends SuperClass {
// SubClassはメールアドレスも持っている
private String email;
public SubClass setEmail(String email) {
this.email = email;
return this;
}
@Override
public SubClass setFirstName(String firstName) {
setFirstName(firstName);
return this;
}
@Override
public SubClass setLastName(String lastName) {
setLastName(lastName);
return this;
}
// Getterが続く
public String getEmail() {
return email;
}
}
今度はコンパイルできましたね。めでたしめでたし!とはいきません。
こんなコードじゃ何のために継承を使ったのか全くわかりませんね。
SuperClass
のフィールドが少ないので、この程度で済んでいますが、フィールドが多かった場合苦行でしかないでしょう。
くそッ!Javaにも自分型があれば、、、と思っているでしょう。
この問題を何とかしてくれるのが 擬似自分型(Simulated self type) です。
解決策としての擬似自分型(Simulated self type)
実装を見てみる
まあ、一旦コードを見てくれ、話はそれからなんですよ。
abstract class SuperClass<T extends SuperClass<T>> {
private String firstName;
private String lastName;
// Setterが自分自身を返すようにしてメソッドチェーンを実現する
public T setFirstName(String firstName) {
this.firstName = firstName;
return self();
}
public T setLastName(String lastName) {
this.lastName = lastName;
return self();
}
// 自分自身を返すようにSubClassに実装してもらう
public abstract T self();
// Getterが続く
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
class SubClass extends SuperClass<SubClass> {
// SubClassはメールアドレスも持っている
private String email;
public SubClass setEmail(String email) {
this.email = email;
return this;
}
@Override
public SubClass self() {
return this;
}
// Getterが続く
public String getEmail() {
return email;
}
}
SuperClass
が抽象クラスになったり、新しくself()
とかいうメソッドを実装しないといけなくなったり、よくわからんジェネリクスがついたりして、よくわかんないですね。
ただ、なんかよくわからんけどコンパイルできます。めでたしめでたし!
今回は実際にめでたいのでこれから説明していきます。
まず、最初に理解して欲しい点として、先程までと違って今回SubClass
がオーバーライドしないといけないのは、自分自身を返すだけのself()
メソッドだけです。
これでSuperClass
のフィールドが膨大だとしてもSubClass
がオーバーライドしないといけないメソッドの数が増えることはありません。
これが擬似自分型を使うメリットです。
どうしてコンパイルが通るの?
次に、どうしてコンパイルが通るのかという解説をします(これからコード中のGetterは省略します)。
以下のようにSuperClass
のsetFirstName()
メソッドはSuperClass<T>
を拡張したクラスのT
クラスを返すことになっています。
abstract class SuperClass<T extends SuperClass<T>> {
private String firstName;
private String lastName;
public T setFirstName(String firstName) {
this.firstName = firstName;
return self();
}
public T setLastName(String lastName) {
this.lastName = lastName;
return self();
}
public abstract T self();
}
そして、SubClass
のコードは以下のようになっています。
SubClass
はSuperClass<SubClass>
を継承しているので、さきほどのジェネリクスでのT
は今回の場合SubClass
になります(SubClass
がSuperClass<SubClass>
を拡張していることは定義より自明でしょう)。
なので、setFirstName()
メソッドはSubClass
を返します。
self()
メソッドは単にthis
を返せばおしまいです。
スッキリしましたね。
class SubClass extends SuperClass<SubClass> {
// SubClassはメールアドレスも持っている
private String email;
public SubClass setEmail(String email) {
this.email = email;
return this;
}
@Override
public SubClass self() {
return this;
}
}
具体例(継承を伴うBuilderパターン)
次に具体例についてみていきます。
実際にJavでWebアプリケーションを作る時はSpringを使用することが多いと思うので、DIコンテナにインスタンスの生成を管理してもらうことになると思うので、自分で書く機会は少ないかと思いますが、Builderパターンでの使用例を見ていきましょう(会社で共通ライブラリとか作るなら使えるかも)。
Builderパターンというと以下のようにメソッドチェーンでBuilderの値を設定していって、最後にbuild()
メソッドでお目当てのクラスを生成する手法です。
Person person = new PersonBuilder().setFirstName("太郎").setLastName("日本橋").build();
では、またSuperClass
のみの場合にどうなるのか見てみましょう。
まずは呼び出し側のコードです。
class Main {
public static void main(String[] args) {
SuperClass superClass = new SuperClass.Builder().setFirstName("太郎").setLastName("日本橋").build();
System.out.println("私の名前は%s %sです。".formatted(superClass.getLastName(),
superClass.getFirstName()));
}
}
ちゃんとBuilderパターンになっていますね。
次はSuperClass
の実装です。
重要な点は、
- staticクラスとして
Builder
が追加されたこと - コンストラクタがプライベートになったこと
-
SuperClass
がSetterを持たないので不変クラスになったこと
あたりです。
class SuperClass {
private String firstName;
private String lastName;
// コンストラクタはプライベート
private SuperClass(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
}
// Getterのみ、Setterは存在しない
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public static class Builder {
private String firstName;
private String lastName;
public Builder setFirstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder setLastName(String lastName) {
this.lastName = lastName;
return this;
}
public SuperClass build() {
SuperClass superClass = new SuperClass(this);
return superClass;
}
}
}
SuperClass
を継承するとうまくいかないのはBuilderパターンの前の解説から明らかでしょう。
うまいこと継承したコードは以下のようになります。
くどくなるので説明はしませんが、先程の話を思い出しながら読めばすらすら読めると思います。
class Main {
public static void main(String[] args) {
SubClass subClass = new SubClass.Builder().setFirstName("花子").setLastName("丸の内").setEmail("hanako.marunouchi@example.com").build();
System.out.println(
"私の名前は%s %sです。宛先はこちら%s。"
.formatted(subClass.getLastName(), subClass.getFirstName(), subClass.getEmail()));
}
}
abstract class SuperClass {
private String firstName;
private String lastName;
// コンストラクタはprotectedになっちゃった
protected SuperClass(Builder<?> builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
}
// Getterのみ、Setterは存在しない
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public static abstract class Builder<T extends Builder<T>> {
private String firstName;
private String lastName;
public T setFirstName(String firstName) {
this.firstName = firstName;
return self();
}
public T setLastName(String lastName) {
this.lastName = lastName;
return self();
}
public abstract SuperClass build();
protected abstract T self();
}
}
class SubClass extends SuperClass {
// SubClassはメールアドレスも持っている
private String email;
// こっちのコンストラクタはプライベート
private SubClass(Builder builder) {
super(builder);
email = builder.email;
}
public static class Builder extends SuperClass.Builder<Builder> {
private String email;
public Builder setEmail(String email) {
this.email = email;
return this;
}
@Override
public SubClass build() {
return new SubClass(this);
}
@Override
protected Builder self() {
return this;
}
}
// Getterのみ、Setterは存在しない
public String getEmail() {
return email;
}
}
標準パッケージでの実装例(enum)
ちなみにOpenJDKのenumも擬似自分型を使って実装されているっぽいです。
GitHubのリンクはこちら
まとめ
Builderパターンで継承を使うときは擬似自分型を使おう!(実はEffective Javaの受け売りです。。。)
まあ、Builderパターンに限らずJavaでメソッドチェーンを使うときはもしかしたら使うことになるかもしれないですね。
あまりわかりやすい手法ではないので多用するのは良くないかなとは思います。
共通ライブラリなど、普段開発者が直接いじる機会が少ない箇所に絞るべきかなーというのが素直な感想です。
私自身、この擬似自分型を読んだだけではにいまいち理解しきれなかったので、こうしてまとめることでやっと理解できました。
でも、使い所を選べば非常に有用な手法だとは思うので、ここぞというときに使えるように頭の片隅には置いておきたいですね。