第 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 章 ストリーム#
コレクションはオブジェクトの保存を最適化しました。一方、ストリームはオブジェクトのバッチ処理に関連しています。
私たちが通常言うストリームは、一般的に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
を使用してください。 - 適切なレベルで問題を処理してください。処理方法がわからない限り、例外を捕捉しないでください。