LoginSignup
3
1

More than 1 year has passed since last update.

Javaでの擬似自分型(Simulated self type)

Posted at

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は省略します)。
以下のようにSuperClasssetFirstName()メソッドは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のコードは以下のようになっています。
SubClassSuperClass<SubClass>を継承しているので、さきほどのジェネリクスでのTは今回の場合SubClassになります(SubClassSuperClass<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でメソッドチェーンを使うときはもしかしたら使うことになるかもしれないですね。
あまりわかりやすい手法ではないので多用するのは良くないかなとは思います。
共通ライブラリなど、普段開発者が直接いじる機会が少ない箇所に絞るべきかなーというのが素直な感想です。
私自身、この擬似自分型を読んだだけではにいまいち理解しきれなかったので、こうしてまとめることでやっと理解できました。
でも、使い所を選べば非常に有用な手法だとは思うので、ここぞというときに使えるように頭の片隅には置いておきたいですね。

参考文献

Effective Java 第3版

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1