제이온

[Effective Java] アイテム1. コンストラクタの代わりに静的ファクトリメソッドを検討する

作成: 2024-04-27

作成: 2024-04-27 00:45

概要

クラスのインスタンスを取得する伝統的な手段は、publicコンストラクターです。


```javascript public class Member {

}

public enum MemberStatus {

}


一般的には、publicコンストラクターだけでも十分ですが、コンストラクター以外に、静的ファクトリメソッド(static factory method)を提供すると、ユーザー側から意図した通りにインスタンスを作成しやすくなる場合があります。


静的ファクトリメソッドの代表的な例として、BooleanのvalueOf()メソッドがあります。


```javascript public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }


上記のメソッドは、基本型であるboolean値を受け取って、Booleanオブジェクトに変換して返しています。


静的ファクトリメソッドの利点

名前を持つことができる。

コンストラクターに渡すパラメータとコンストラクター自体だけでは、返されるオブジェクトの特性を正しく説明できない場合があります。例えば、上記のMemberクラスのメイン コンストラクター(name、age、hobby、memberStatus)だけでは、どのような特性を持つMemberなのかを把握するのが難しいです。


また、1つのシグネチャでは1つのコンストラクターしか作れませんが、静的ファクトリメソッドは名前を持つことができるため、1つのシグネチャで複数の静的ファクトリメソッドを作成し、インスタンスを返却できます。


```javascript public class Member {

}


このように、コンストラクターでMemberStatusを区別するよりも、同じシグネチャを持つ複数の静的ファクトリメソッドを作成することで、ユーザーは混乱することなく、特定のスキルを持つMemberインスタンスを作成できるようになります。

JDKで定義されているライブラリを見ると、BigIntegerの静的ファクトリメソッドであるprobablePrime()があります。


```javascript public static BigInteger probablePrime(int bitLength, Random rnd) { if (bitLength < 2) throw new ArithmeticException("bitLength < 2");

}


BigIntegerの通常のコンストラクターと、静的ファクトリメソッドであるprobablePrime()を比較すると、値が素数である BigIntegerを返す、という文は、後者の方が説明が明確です。


呼び出されるたびに新しいインスタンスを生成する必要がない。

```javascript public static Boolean valueOf(boolean b) { return (b ? Boolean.TRUE : Boolean.FALSE); }


BooleanのvalueOf()メソッドは、インスタンスを事前にキャッシュしておいて、返却していることがわかります。このような特性は、生成コストの高いオブジェクトが頻繁に要求される状況であれば、パフォーマンスを大幅に向上させる可能性があり、フライウェイトパターンと類似した手法と見なすことができます。


繰り返し要求に対して同じオブジェクトを返すというように、静的ファクトリメソッド方式を使用するクラスは、インスタンスのライフサイクルを制御できるため、インスタンス制御クラスと呼ばれます。インスタンスを制御することで、シングルトン クラスを作成したり、インスタンス化不可クラスを作成したりできます。また、不変値クラスで、同じインスタンスが1つしかないことを保証できます。

インスタンス制御はフライウェイトパターンの基礎であり、列挙型はインスタンスが1つだけ作成されることを保証します。


マインクラフトで木を植える必要があります。もし木オブジェクト1つ1つを新しく生成すると、メモリオーバーフローが発生する可能性があります。

そのため、上記のように赤い木と緑色の木オブジェクトを保存しておき、位置だけを変更して返せばよいでしょう。もちろん、色は2色以外にも増える可能性があるので、Mapなどのデータ構造に色別に木を保存しておけば効率的でしょう。


```javascript public class Tree {

}

public class TreeFactory { // HashMapデータ構造を利用して作成された木を管理します。 public static final Map<String, Tree> treeMap = new HashMap<>();


}

public class Main { public static void main(String[] args) { Scanner scanner = new Scanner(System.in);

}


シングルトンパターンとの違い

シングルトンパターンでは、Treeクラスに木を1つしか作成できません。そのため、シングルトンパターンを使用する場合は、作成された唯一のオブジェクトの色を変更する必要があります。つまり、シングルトンパターンは、種類に関係なく、1つしか持つことができません。


使用例

JavaのString Constant Poolでは、フライウェイトパターンが使用されています。


返却型のサブタイプオブジェクトを返却できる。

ArraysユーティリティクラスのasList()メソッドを使用したことがあるなら、この利点について理解できるでしょう。


```javascript public static List asList(T... a) [ return new ArrayList<>(a); }


Listのサブ実装であるArrayListで値をラップして返しますが、ユーザーはこれらの実装を認識する必要はありません。つまり、返されるオブジェクトのクラスを自由に選択できる柔軟性により、開発者は実装を公開せずに実装を返すことができるため、APIを小さく維持できます。


Javaインターフェースの静的メソッドに関する話

Java 8以前は、インターフェースに静的メソッドを宣言できなかったので、名前が「Type」であるインターフェースを返す静的メソッドが必要な場合は、「Types」というインスタンス化不可のコンパニオン クラスを作成し、その中にメソッドを定義していました。


代表的な例として、JCFが提供する45個のユーティリティ実装がありますが、これらの実装のほとんどは、単一のコンパニオン クラスであるjava.util.Collectionsに静的ファクトリメソッドによって取得されるようにしていました。特に、これらの実装の中には、publicではなく、静的ファクトリメソッドでのみインスタンスを作成できる実装も存在します。(この実装は当然、継承できません。)


また、45個の実装を公開していないため、APIを大幅に小さくすることができました。


```javascript // インターフェースとコンパニオンクラスの例 List empty = Collections.emptyList();


しかし、Java 8からはインターフェースに直接静的メソッドを追加できるようになったため、コンパニオン クラスを別途定義する必要はありません。


入力パラメータに応じて、毎回異なるクラスのオブジェクトを返却できる。

単にサブタイプを返すだけでなく、パラメータの値に応じて異なるサブタイプを返却できます。例えば、点数に応じてMemberStatusを異ならせて返したい場合は、以下のように静的ファクトリメソッドを作成し、その中に比較ロジックを組み込めばよいでしょう。


```javascript public enum MemberStatus {

}

@DisplayName("MemberStatusテスト") class MemberStatusTest {

}


静的ファクトリメソッドを作成する時点で、返却するオブジェクトのクラスが存在しなくてもよい。

上記の文でオブジェクトのクラスは、私たちが作成するクラスファイルそのものです。

ちなみに、Class<?>は、クラスローダーがクラスをロードしたときにヒープ領域に割り当てられるClassオブジェクトを意味します。このClassオブジェクトは、私たちが作成したクラスのさまざまなメタデータを保持しています。


```javascript package algorithm.dataStructure;

public abstract class StaticFactoryMethodType {

}


上記のコードを見ると、インターフェース実装の位置からClassオブジェクトを生成し、リフレクション技術を使用して実際の実装を初期化していることがわかります。このとき静的ファクトリメソッドを作成する時点では、StaticFactoryMethodTypeChildクラスは存在しなくてもよいのです。


もし、静的ファクトリメソッドを使用する時点でalgorithm.dataStructure.StaticFactoryMethodTypeChildパスについて、実装が存在しない場合はエラーが発生しますが、静的ファクトリメソッドを作成する時点では問題はないため、柔軟性が高いと言えるでしょう。


```javascript public interface Test {

}

public class Main {

}


リフレクションを使用しなくても、同じ柔軟性を手に入れることができます。Testの静的ファクトリメソッドであるcreate()を見ると、実装が存在しなくても、作成時点では問題が発生しません。もちろん、実際に使用するときはNPEが発生するので、後で実装を返却する必要があります。


このような柔軟性は、サービスプロバイダーフレームワークを作成する基礎となり、代表的な例としてJDBCがあります。JDBCサービスプロバイダーフレームワークのプロバイダーはサービスの実装であり、この実装をクライアントに提供する役割をフレームワークが制御することで、クライアントを実装から分離します。(DIP)


  • サービスプロバイダーフレームワークのコンポーネント
    • サービスインターフェース
      • 実装の動作を定義する
      • JDBCのConnection
    • プロバイダー登録API
      • プロバイダーが実装を登録する
      • JDBCのDriverManager.registerDriver()
    • サービスアクセスAPI
      • クライアントがサービスのインスタンスを取得するために使用し、条件を指定しない場合は、デフォルトの実装 またはサポートされている実装を順番に返す。
      • 静的ファクトリメソッドに相当する
      • JDBCのDriverManager.getConnection()
    • (オプション) サービスプロバイダーインターフェース
      • これがなければ、各実装をインスタンス化するときにリフレクションを使用する必要がある。
      • JDBCのDriver


サービスプロバイダーフレームワークパターンには、さまざまなバリエーションがあり、ブリッジパターン、依存性注入フレームワークなどがあります。


典型的 JDBCの例

```javascript Class.forName("oracle.jdbc.driver.OracleDriver"); Connection connection = null; connection = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:ORA92", "root", "root");

// 各種Statementを使用したsqlロジック


一般的にJDBCは、上記のように記述します。Class.forName()を使用して、Driverの実装の1つであるOracleDriverを登録し、DriverManager.getConnection()を使用して、Connectionの実装の1つであるOracleDriver用のConnectionを取得します。


ここで、Connectionはサービスインターフェース、DriverManager.getConnection()はサービスアクセスAPI、Driverはサービスプロバイダーインターフェースであることがわかります。しかし、プロバイダー登録APIであるDriverManager.registerDriver()は使用されていません。それでも、Class.forName()だけで、Driverの実装であるOracleDriverを登録できます。なぜこれが可能なのでしょうか?


Class.forName()の動作原理

このメソッドは、物理的なクラスファイル名を引数に渡すと、JVMにこのクラスをロードするように要求します。すると、クラスローダーは、クラスのメタデータをメソッド領域に格納する一方、Classオブジェクトをヒープ領域に割り当てます。また、クラスのロードが完了すると、staticフィールドとstaticブロックが初期化され、このときプロバイダー登録APIが使用されます。


```javascript public class OracleDriver implements Driver {

}


実際、OracleDriverを見てみると、staticブロックの中でDriverManager.registerDriver()を使用して、Driverの実装であるOracleDriverを登録していることがわかります。


DriverManagerクラスの分析

```javascript public class DriverManager {

}


DriverManagerクラスは、実際にははるかに複雑ですが、重要な部分だけを抜き出して簡単に言うと、上記のようになります。説明したように、registerDriver()をOracleDriverのstaticブロックで呼び出してOracleDriverを登録し、getConnection()を呼び出すことで、ユーザーはConnectionの実装を取得できます。


ユーザーアクセスAPIであるgetConnetion()を詳しく見ると、DriverインターフェースからConnectionを取得していることがわかります。もしサービス提供インターフェースであるDriverがなければ、目的のConnection実装を返すために、Class.forName()のようなリフレクションを使用できます。このときConnection実装は、静的ファクトリ作成時点では存在しなくてもよいのです。


代わりに、Driverインターフェースを使用し、動的にDriverの実装を登録した後、このDriverに対応するConnection実装を簡単に取得できます。


ちなみに、DriverManagerのgetConnection()メソッドの実際のJDKコードを分析してみたのですが、あまり興味がない場合は、飛ばしても問題ありません。


```javascript @CallerSensitive public static Connection getConnection(String url, String user, String password) throws SQLException { java.util.Properties info = new java.util.Properties();

}


まず、public staticメソッドであるgetConnection()が呼び出され、url、Properties、CallerClassがprivate staticメソッドであるgetConnection()の引数に渡されます。このとき、Reflection.getCallerClass()は、このpublic staticメソッドであるgetConnection()を呼び出したクラスを取得する役割を担います。もしCarクラスがgetConnection()を呼び出した場合、Reflection.getCallerClass()によってClassオブジェクトを取得できます。


```javascript private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; synchronized(DriverManager.class) { if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); } }

}


callerCLはクラスローダーオブジェクトであり、callerまたは現在のスレッドのクラスローダーによって作成されます。その後、現在のアプリケーションに登録されているDriverのリストであるregisteredDriversから、aDriverを1つずつ取り出します。そして、このDriverがisDriverAllowed()によってtrueと判定された場合、そのDriverでConnectionオブジェクトを取得し、それを返します。isDriverAllowed()は、callerにaDriverが存在するかどうかを確認する役割を担います。


JDBCフレームワークの利点

JDBCフレームワークのポイントは、Driver、Connectionインターフェースとそのインターフェースを実際に実装する実装クラスが完全に分離して提供されることです。インターフェースを使用してフレームを作成し、そのフレームに合わせてそれぞれ実装クラスを作成すれば、非常に柔軟性が高いという利点があります。


そのため、他のDBMSが登場しても、そのベンダーはDriverとConnectionインターフェースを実装して提供すれば、Javaを使用する開発者が他のDBMSドライバと同一のAPIを使用できるようになります。


静的ファクトリメソッドの欠点

継承する際に、publicまたはprotectedコンストラクターが必要となるため、静的ファクトリメソッドのみを提供すると、サブクラスを作成できません。


しかし、この制約は、継承よりもコンポジションを誘導し、不変型を作成するためにこの制約を守る必要があるという点で、むしろ利点となる可能性があります。


静的ファクトリメソッドは、プログラマーが見つけにくい。

コンストラクターのようにAPIの説明に明確に示されていないため、開発者はAPIドキュメントをきちんと書いておいたり、メソッド名に広く知られている規約に従って付けたりすることで、問題を軽減する必要があります。


静的ファクトリメソッドの命名方法

  • from
    • パラメータを1つ受け取り、そのタイプのインスタンスを返す
    • Date date = Date.from(instant);
  • of
    • 複数のパラメータを受け取り、適切なタイプのインスタンスを返す
    • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf
    • fromとofの詳細なバージョン
    • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instanceまたはgetInstance
    • パラメータで指定したインスタンスを返す。ただし、同じインスタンスであることは保証されない。
    • StackWalker luke = StackWalker.getInstance(options);
  • createまたはnewInstance
    • instanceまたはgetInstanceと同じだが、毎回新しいインスタンスを生成して返されることが保証される。
    • Object newArray = Array.newInstance(classObject, arraylen);
  • getType
    • getInstanceと同じだが、生成するクラスではなく、別のクラスにファクトリメソッドを定義する場合に使用します。
    • FileStore fs = Files.getFileStore(path);
  • newType
    • newInstanceと同じだが、生成するクラスではなく、別のクラスにファクトリメソッドを定義する場合に使用します。
    • BufferedReader br = Files.newBufferedReader(path);
  • type
    • getTypeとnewTypeの簡潔なバージョン
    • List<Complaint> litany = Collections.list(legacyLitany);


まとめ

静的ファクトリメソッドとpublicコンストラクターは、それぞれ用途が異なりますので、適切に使用しましょう。


出典

コメント0