第 10 章 インターフェース#
本章では、抽象クラスとインターフェースの定義、使用、違いと意義について説明します。この章では「使用と機能実装の分離」という優れたデザインについてさらに詳しく説明し、いくつかのデザインパターンも紹介します。チューリングの導入目標は以下の通りです:
インターフェースは一体何を表しているのか?#
インターフェースは、クラスがどのようなものであり、何ができるかを説明しますが、どのように行うかは含まれていません。実際には、クラス間の「プロトコル」を定義しています。私個人の意見として、インターフェースはより高次の抽象化であると考えています:コード内でいくつかの同じ動作を持つクラスをさらに抽象化することで、コードの再利用と強力な拡張性をもたらしますが、同時に追加の複雑さももたらします。ここから、合理的な設計を維持するためには、適切にカプセル化し、抽象化を合理的なレベルに制御する必要があることがわかります。さもなければ、それは過剰設計です!
インターフェースのデフォルトメソッド#
インターフェースは本来、どのメソッドも実装できません。インターフェースは単に「プロトコル」を表し、サブクラスに実装を任せる必要があります(implements)。しかし、JDK 8
はインターフェースにデフォルトメソッドを提供しました。インターフェースにデフォルトメソッドがある場合、サブクラスはこのメソッドをオーバーライドせずにデフォルト実装として使用できます。一説によると、デフォルトメソッドはコードの互換性と柔軟性を高めます。《On Java》では、これにより既存のインターフェースにメソッドを追加でき、すでにそのインターフェースを使用しているすべてのコードを破壊することなく行えるとしています。デフォルトメソッドはJDK 8
で導入されたストリームの解決策であるべきです。
抽象クラスとインターフェースの違い#
特性 | インターフェース | 抽象クラス |
---|---|---|
組み合わせ | 新しいクラスで複数のインターフェースを組み合わせることができる | 1 つの抽象クラスしか継承できない |
状態(フィールド) | フィールドを含むことはできない(静的フィールドを除くが、オブジェクトの状態はサポートされない) | フィールドを含むことができ、非抽象メソッドはこれらのフィールドを参照できる |
デフォルトメソッドと抽象メソッド | デフォルトメソッドはサブクラスで実装する必要がなく、インターフェース内のメソッドのみを参照できる(フィールドは不可) | 抽象メソッドはサブクラスで実装する必要がある |
コンストラクタ | コンストラクタを持つことはできない | コンストラクタを持つことができる |
アクセス修飾子の制限 | 暗黙の public | protected またはパッケージアクセス権を持つことができる |
使用のガイドライン:「合理的な範囲内で可能な限り抽象化する」。したがって、一般的にはインターフェースを優先し、抽象クラスを使用する必要があるときには自然にわかるでしょう。しかし、必要がない限り、両方とも使用しないでください。後で説明しますが、これは早すぎる最適化です。インターフェースと抽象クラスは、コードをリファクタリングするための強力なツールとして機能します。
完全なデカップリング#
抽象クラスよりもインターフェースの方が好まれることが多いです。なぜなら、呼び出すメソッドに抽象クラスやそのサブクラスを渡すと、基底クラスやそのサブクラスしか使用できず、継承体系から外れることができないからです。一方、インターフェースを使用すると、継承体系の制限を受けずに、より多くの再利用可能なコードを書くことができます。
ストラテジーデザインパターンについて#
まず、ストラテジーデザインパターンに関しては、この記事が非常に良いです。今後、デザインパターンに関連する問題があれば、私も引用します。
ストラテジーデザインパターンは、渡されたパラメータオブジェクトに基づいて異なる動作を示すメソッドを作成することです。これにより、コンテキストはこの戦略がどのように実装されているかを知る必要がなく、私は直接使用できます。今後新しい戦略があっても、コンテキストを変更する必要がなくなり、メソッドがより柔軟で汎用的になり、再利用性も高まります。つまり、「このメソッドは、オブジェクトが私のインターフェースに従っている限り、任意のオブジェクトを操作できます」ということです。これは「変化するものと変わらないものを分ける」というデザインの表れでもあります。
ここにJava
の簡単な実装を書いておきます。後で詳しく説明できます:
import java.util.Random;
enum concreteStrategy {
// 加算
addition,
// 減算
subtraction,
// 乗算
multiplication
}
/**
* ストラテジーインターフェースは、特定のアルゴリズムの異なるバージョン間で共通の操作を宣言します。コンテキストはこのインターフェースを使用して
* 具体的な戦略で定義されたアルゴリズムを呼び出します。
*/
interface Strategy {
/**
* このメソッドは特定の二項演算を実行し、値を返します。
*
* @param num1 演算子 1
* @param num2 演算子 2
* @return double 演算後の結果
* @author BlackFlame33
*/
double execute(int num1, int num2);
}
/**
* 具体的な戦略は、戦略の基本インターフェースに従ってアルゴリズムを実装します。このインターフェースは、コンテキスト内での相互運用性を実現します。
*/
class ConcreteStrategyAdd implements Strategy {
@Override
public double execute(int num1, int num2) {
return num1 + num2;
}
}
class ConcreteStrategySubtract implements Strategy {
@Override
public double execute(int num1, int num2) {
return num1 - num2;
}
}
class ConcreteStrategyMultiply implements Strategy {
@Override
public double execute(int num1, int num2) {
return num1 * num2;
}
}
/**
* コンテキストはクライアントが関心を持つインターフェースを定義します。
*/
class Context {
/**
* コンテキストは特定の戦略オブジェクトへの参照を保持します。コンテキストは戦略の具体的なクラスを知りません。コンテキストは戦略インターフェースを介してすべての戦略と対話する必要があります。
*/
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
/**
* コンテキストは、異なるバージョンのアルゴリズムを自分で実装するのではなく、戦略オブジェクトにいくつかの作業を委任します。
*
* @param num1 演算子 1
* @param num2 演算子 2
* @return double 演算結果
*/
public double executeStrategy(int num1, int num2) {
return strategy.execute(num1, num2);
}
}
/**
* クライアントコードは具体的な戦略を選択し、それをコンテキストに渡します。クライアントは戦略間の違いを知っている必要があり、正しい選択をする必要があります。
*
* @author BlackFlame33
*/
public class ExampleApplication {
public static void main(String[] args) {
// コンテキストオブジェクトを作成します。
Context context = new Context();
// 最初の数を読み取ります。
int num1 = new Random().nextInt(10);
// 最後の数を読み取ります。
int num2 = new Random().nextInt(10);
// アプリケーションは実行すべき具体的な戦略を知っています
concreteStrategy action = randomStrategy();
if (action == concreteStrategy.addition) {
context.setStrategy(new ConcreteStrategyAdd());
}
if (action == concreteStrategy.subtraction) {
context.setStrategy(new ConcreteStrategySubtract());
}
if (action == concreteStrategy.multiplication) {
context.setStrategy(new ConcreteStrategyMultiply());
}
double result = context.executeStrategy(num1, num2);
// 結果を印刷します
System.out.println(num1 + " と " + num2 + " の " + action + " は " + result + " です");
}
private static concreteStrategy randomStrategy() {
return concreteStrategy.values()[new Random().nextInt(concreteStrategy.values().length)];
}
}
インターフェースの組み合わせ時の名前の衝突#
インターフェースを組み合わせるとき、異なるインターフェースで同じメソッド名を使用すると、コードの可読性が低下することがよくあります。(このように命名しないでください。。。)
アダプタインターフェース#
アダプタパターン#
同様に、この記事を見れば良いです。
私の理解では:アダプタは互換性のないオブジェクト間の互換性の層のようなものです。時には、互換性のないコードを修正する権限がない場合があります。このとき、アダプタ内のメソッドを使用して、パラメータを適切な形式に適合させ、封装されたオブジェクト内の 1 つまたは複数のメソッドを呼び出します。
このようにして、インターフェースをパラメータとして受け取るメソッドは、インターフェースのメソッドを実装する限り、ほぼすべてのクラスに適応できます。これにより、インターフェースの力を深く感じることができます。
インターフェースのフィールド#
抽象クラスとインターフェースの違いで、インターフェースには静的フィールドしかないことがわかりました。これらの静的フィールドは、インターフェースの静的ストレージ領域に保存されます。
ネストされたインターフェース#
ファクトリーパターンについて#
ファクトリーパターンは、オブジェクトの作成コードとオブジェクトの使用コードを分離し、間接的な層を作成します。これにより、新しい種類のオブジェクトを追加する際に、新しい種類のクラスを直接追加でき、クライアントコードを変更する必要がなくなります。通常、オブジェクトの作成が複雑な場合に発生します。
詳細については、この記事を参照してください。
インターフェース、使う?#
ガイドライン:「インターフェースよりもクラスを優先して使用する」。クラスから設計を開始し、インターフェースが明らかに必要だと感じた場合は、リファクタリングを行いましょう!
第 11 章 内部クラス#
別のクラス内で定義されたクラスは内部クラスと呼ばれます。
内部クラスに関連する構文の他に、匿名内部クラスは非常に便利であり、習得しておくべきです。
内部クラスから外部クラスへのリンク#
内部クラスは、外部クラスオブジェクトへの参照を暗黙的に保持します。したがって、内部クラスは外部クラスのすべての要素にアクセスする権限を持っています。そのため、内部クラスを作成する際には、内部クラスが外部クラスオブジェクトの参照を保持できるように、まず外部クラスオブジェクトを作成し、その後外部クラスオブジェクトを介して内部クラスを作成する必要があります。
匿名内部クラス#
public Contents contents() {
return new Contents() {
int i = 11;
};
}
このクラスの宣言方法は初めて見ると少し奇妙に見えます:return
を書いているときに、Contents
オブジェクトを返す必要がありますが、ああ、待って、まずContents
のサブクラスを定義しましょう。しかし、クラスが一度だけ使用されるか、非常に少数回しか使用されない場合は、特にそのためにクラスを書く必要はありません。
内部クラスの価値#
内部クラスの価値は、各内部クラスが独立してクラスを継承したりインターフェースを実装したりできることにあります。これにより、多重継承の問題の解決策が改善されます。
内部クラスはクロージャも実現できます。これは、作成されたときのスコープ情報を保持することができます。
第 12 章 コレクション#
オブジェクトの数が固定されており、これらのオブジェクトのライフサイクルが既知である場合、そのようなプログラムは非常に簡単です。
配列は非常に便利ですが、そのサイズは固定されているため、大きな制限をもたらします。通常、必要なオブジェクトの数を正確に知ることはできません。
Iterator
#
イテレータは、任意のタイプのコレクションを前方に遍歴することを実現します。プログラマーは処理しているコレクションのタイプを気にする必要がありません(気にしないと言っても、実際には、渡されたコレクションのタイプを考慮して異なる処理を強いられることはありません)。イテレータはコレクションへのアクセスを統一します。
悪い設計 ——Stack
クラス#
Java
はStack
クラスを提供していますが、このクラスの設計は非常に悪いです(これはおそらく不合理な継承関係に起因しています)。そのため、JDK 6
ではArrayDeque
が追加され、スタックを直接使用する方法が提供されました。
警鐘を鳴らし、継承関係を慎重に考慮してください!
インターフェース指向#
実装指向ではなくインターフェース指向でコードを書くことで、私たちのコードはより多くのオブジェクトタイプに適用できるようになります。
まとめ#
Java
は多くのオブジェクトを保持する方法を提供しています。最も重要な 4 つのクラス:List
、Map
、Set
、Queue
:
Collection
は単一の要素を保存し、Map
はキーと値のペアを保存します;コレクションのサイズは自動的に調整されます;ジェネリクスを使用することで、保存できるタイプを制約でき、取り出すときに強制的な型変換を必要としません;コレクションは参照型データのみを保存でき、基本型データはラッパークラスを介して自動ボクシングされて保存されます。List
は順序付きコレクションの一種です。ある意味で、数値インデックスとオブジェクトを関連付けます。List
の中では、ArrayList
はランダムアクセスにおいて非常に効率的です。一方、LinkedList
は挿入と削除において非常に効率的です。
Map
はオブジェクトを他のオブジェクトに関連付けます。Map
の中では、HashMap
は要素に迅速にアクセスできます;TreeMap
はそのキーを順序付きで保存します;LinkedHashMap
は要素の挿入順序で保存しますが、ハッシュを介して迅速にアクセスします。
Set
では、同じ要素は 1 つしか保存できず、Set
の底層実装は実際にはMap
です。したがって、サブ実装の特性はMap
のサブ実装の特性と非常に似ています。HashSet
は要素に迅速にアクセスできます;TreeSet
は要素を順序付きで保存します;LinkedHashSet
は要素の挿入順序で保存します。
- コレクションクラスの一部はあまり使用されず、設計が不合理なクラスは「レガシークラス」と呼ばれます。
Hashtable
、Vector
、Stack
はすべてスレッドセーフですが、新しいコードでは使用しないでください。
コレクションについてはまだ多くのことがまとめられます。二次ノートではコレクションをテーマに展開できます。
第 13 章 関数型プログラミング#
関数型プログラミング言語は、コードの断片をデータのように簡単に扱います。
ちょっと髪が減るかもしれません
関数型プログラミングは、コードを何らかの方法で他のコードを操作するという考えに基づいています。私たちはゼロからコードを構築するのではなく、既存の信頼できるテスト済みの小さなコード片を組み合わせて新しいコードを作成します。
関数型プログラミングをこう理解することができます:オブジェクト指向プログラミングはデータを抽象化し、関数型プログラミングは動作を抽象化します。
関数型プログラミングでは、すべてのデータが不変である必要があります:一度設定したら、決して変更しません。これは並行プログラミングのシナリオで使用するのに非常に適しています。
lambda
式#
lambda
式の行数は 3 行以内に制御することをお勧めします。3 行を超える場合は、メソッド参照を使用することを検討してください。
関数型インターフェース#
インターフェースが 1 つの抽象メソッドのみを含む場合、このインターフェースは「機能インターフェース」とも呼ばれます。
抽象的な観点から見ると、これはメソッドをパラメータまたは戻り値として扱うことを意味します。しかし、JVM
の実装の観点から見ると、クラスとオブジェクトはJava
の第一級市民であり、メソッドはオブジェクトまたはクラスに依存しており、独立して存在することはできません。したがって、Java
はこれを機能インターフェースに結びつけることを選択しました。
関数型インターフェースを使用する際、名前は重要ではなく、重要なのはパラメータの型と戻り値の型だけです。もちろん、機能インターフェースに関しては、その命名パターンがその役割を迅速に理解するのに役立ちます。例えば:
- オブジェクトのみを処理するインターフェースは、通常
Function
、Consumer
、Predicate
と名付けられます; - 基本型パラメータを 1 つだけ受け取るインターフェースは、通常その基本型を示す最初の部分を使用して命名されます(例:
LongConsumer
、DoubleFunction
、IntPredicate
); - 戻り値の型が基本型のインターフェースは、通常 'To' を使用して示されます(例:
ToLongFunction<T>
やIntToLongFunction
); - パラメータ型と戻り値型が同じインターフェースは、
Operator
を使用して命名されます。UnaryOperator
は 1 つのパラメータを示し、BinaryOperator
は 2 つのパラメータを示します; - 1 つのパラメータを受け取り、
boolean
型を返すインターフェースは、Predicate
を使用して命名されます; - 2 つのパラメータを受け取るインターフェースは、通常
BiXxx
を使用して命名されます(例:BiPredicate
は 2 つのパラメータを受け取り、戻り値の型はboolean
です)。
クロージャ#
内部クラスの章でもクロージャについて説明しました。クロージャは関数オブジェクトまたは匿名関数として機能し、コンテキストデータを保持し、渡したり保存したりできます。Java
では、変数は == 最終的 == に不変である必要があります。
第 14 章 ストリーム#
コレクションはオブジェクトの保存を最適化しました。一方、ストリーム(stream)はオブジェクトのバッチ処理に関係しています。
私たちが通常ストリームと言うとき、一般的にはI/O
ストリームを指しますが、本章で言及するストリームはstream
を指します。ほとんどの場合、オブジェクトをコレクションに保存するのは、それらを処理するためです。したがって、プログラミングの重点がコレクションからストリームに移ることがわかります。ストリームはプログラムをより小さくし、可読性を高め、lambda
式やメソッド参照と組み合わせることで、JDK 8
の魅力を大いに高めました。
宣言型プログラミング#
宣言型プログラミングは、命令型プログラミングとは異なるプログラミングスタイルであり、私たちは「何をするか」(What to do)を宣言し、命令型プログラミングのように「どのようにするか」(How to do)を指示するのではありません。
内部イテレーション#
私たちが通常明示的にfor
ループを使用して遍歴するイテレーションは「外部イテレーション」と呼ばれ、ストリームは「内部イテレーション」を使用します:具体的にどのようにイテレーションが実行されるかはわかりません。しかし、イテレーション方法の制御を緩和することで、何らかの並行メカニズムに委ねることができ、ストリームの内部イテレーションはより効率的です。
遅延評価#
遅延評価は、ストリームが必要なときにのみ評価されることを意味します。「評価される前」にストリームは、呼び出されていないメソッドのように見え、単にロジックが宣言されているだけです。または、ストリームがそのように評価されるモデルとして宣言されている場合、実際に「評価される必要がある」ときにモデルが実行されます。
JDK 8
のストリームサポート#
インターフェースのデフォルトメソッドもJDK 8
で新たに追加され、ストリームの追加と密接に関連しているかもしれません。
ストリームという概念を創造した後、Java
の設計者たちは 1 つの難題に直面しました:ストリームをライブラリに統合する方法、しかし既にライブラリを使用しているコードに影響を与えないようにするには?なぜなら、インターフェースに新しいメソッドを追加すると、そのインターフェースを実装しているすべてのクラスが、新しいメソッドを実装していない場合、破壊されてしまうからです。。。気にしないで、デフォルトメソッドが助けてくれるデフォルトメソッドにより、インターフェースにデフォルト実装を追加でき、サブクラスはこれらのメソッドをオーバーライドする必要がなくなります。
Optional
型#
Optional
型の設計の目的は、ストリーム内で例外が発生して中断されるのを避けることです。ストリームに要素がない場合、特定のストリーム操作を実行するとOptional
オブジェクトが返されます。
reduce(BinaryOperator)
#
引数の命名(acc, i)#
reduce(BinaryOperator)
に渡されるlambda
式の最初の引数は前回の呼び出しの結果であり、2 番目の引数はストリームからの新しい値です。最初の引数はacc
、2 番目の引数はi
と命名するのが最適です。
第 15 章 例外#
Java
の基本哲学は「悪いコードは実行できない」です。
欠陥:例外の欠如#
例外の使用が不適切であると、例外が「飲み込まれる」状況が発生する可能性があります。
例外の説明#
「例外の説明」は縮小できますが、拡大することはできません。これは、クラスの継承過程における規則とは逆です。
Java
の欠点#
メモリのクリーンアップを除いて、他のクリーンアップは自動的には発生せず、クライアントプログラマーに通知し、彼ら自身で処理させる必要があります。
例外を使用するためのガイドライン#
- 可能な限り
try-with-resources
を使用してください。 - 適切なレベルで問題を処理してください。処理方法がわからない限り、例外を捕捉しないでください。