第 6 章 初期化とクリーンアップ#
‘不安全’なプログラミングは、プログラミングコストが高くなる原因の一つです。
そして == 初期化 == と == クリーンアップ == は、‘不安全’なプログラミングを引き起こす二つの要因です。
本章では、Java がコンストラクタを使用してオブジェクトを初期化する方法と、ガベージコレクタに関するいくつかの説明に焦点を当てています。チューリングのガイドラインでは、本章には重点と難点があるため、自分の考えをできるだけ詳しく述べるようにしています。
コンストラクタによる初期化の保証#
コンストラクタ(コンストラクタ)は、Java がオブジェクトを正しく初期化するための重要な手段です。コンストラクタはクラス名と完全に同じで、戻り値の型がないメソッドです。クラス名が完全に同じであることは、コンストラクタがメソッド名に lowerCamelCase の命名規則を適用することができないことを意味します;戻り値の型がないとは、戻り値の型がvoid
であるということではなく、戻り値の型が存在しないということです。C++ と比較すると、Java のコンストラクタと初期化は一体です。
ここには古典的な罠があります:コードを書くとき、私たちは常にnew
キーワードを使用してコンストラクタを呼び出しオブジェクトを作成するため、コンストラクタの役割はオブジェクトを作成することだと簡単に考えてしまいますが、実際にはそうではありません。オブジェクトの作成はJVM
によって行われ、JVM
がコンストラクタを呼び出すのです。コンストラクタの役割はオブジェクトの初期化に過ぎません。
メソッドのオーバーロード#
メソッドのオーバーロードのルール:各オーバーロードされたメソッドは、ユニークなパラメータ型リストを持たなければなりません。つまり:
- パラメータの数が異なる。
- パラメータの数が同じで、型が異なる。
- パラメータの数が同じで、型が同じだが順序が異なる。
- 戻り値の型が異なっても、メソッドのオーバーロードにはなりません。
- パラメータの識別子が異なっても、メソッドのオーバーロードにはなりません。
- 修飾子が異なっても、メソッドのオーバーロードにはなりません。
もちろん、渡されるパラメータの型がオーバーロードされたメソッドのパラメータ型リストにない場合、小さい型は昇格されます;char
型はint
型に昇格されます;型が大きすぎるとエラーになります。メソッド呼び出しが複数のメソッドにマッチする場合、一般的にはデータ型が「近い」もの、つまり自動型変換の「回数が最も少ない」ものが選択されます。
引数なしのコンストラクタ#
引数なしのコンストラクタの通常の部分は理解しやすいです:引数のないコンストラクタです。
ただし注意が必要です:すでにコンストラクタを定義している場合、そのコンストラクタが引数ありであっても、コンパイラは == 自動的に == 引数なしのコンストラクタを作成しません。
例え話:コンストラクタを定義していないとき、コンパイラは「おお〜、あなたはコンストラクタが必要に違いない、無引数のコンストラクタを追加してあげましょう」と考えます;しかし、すでにコンストラクタを提供している場合、コンパイラは「あなたはすでにコンストラクタを持っています、あなたは自分が何をしているか知っています;もしあなたが自分で無引数のコンストラクタを提供しなかった場合、あなたには理由があるはずです、もしかしたらそれが不要だからでしょう、だから私は追加しません」と考えます。
本当に、泣きそう
this
キーワード#
this
パラメータは、各メンバーメソッドの暗黙の引数であり、現在のオブジェクトの参照です。これは、ターミナルにその参照を出力することで確認できます。this
を乱用しないでください、これはコードの可読性に影響を与えます。this
は必要な場所にのみ出現すべきです。
コンストラクタ内で、this
の後にパラメータリストがある場合、それは一致するパラメータリストのコンストラクタを呼び出すことを示します。注意すべきは、同時に二つを呼び出すことはできず、コンストラクタの呼び出しはメソッドの最初の部分、つまりメソッド本体の最初の行に出現しなければなりません。
static
静的メソッドにはthis
がありません。これはその静的な本質です:静的メソッドはthis
を暗黙的に渡すことができません。なぜなら、静的メソッドはオブジェクトに依存しないからです。
クリーンアップとガベージコレクション#
リソースのクリーンアップ#
私たちはしばしばnew
キーワードを使用してオブジェクトを作成し、JVM
はガベージコレクタ、つまり GC を通じてオブジェクトのライフサイクル管理を行います。Java
プログラマーは「オブジェクトを一気に作成する」ことができ、そのオブジェクトがいつ破棄されるかを気にする必要はありません。
finalize()
メソッドを使用してメモリ空間の解放を管理します。このメソッドの呼び出しタイミングは GC によって制御されます。しかし、GC がいつ開始されるかは不確定です。したがって、finalize()
メソッドの使用は推奨されません。実際、JDK 9
以降、finalize()
メソッドは@Deprecated
として注釈され、このメソッドは廃止されましたR.I.P, finalize()
現在推奨される方法は、「後処理」メソッドを明示的に定義することです。例えば、Java
のさまざまなストリームのclose()
メソッドです。
ガベージコレクション#
ガベージコレクタGC
は、アイドル時に不定期に参照されていないオブジェクトのメモリ空間を回収するメカニズムです。これにより、私たちはC++
のようにオブジェクトを使用しなくなった後に明示的にデストラクタを実行する必要がなくなります。これにより、プログラマーの作業が軽減され、メモリリークの問題が緩和されます。
なぜストレージの解放がストレージの割り当てに影響を与えるのか#
C
などの言語がヒープ上でスペースを割り当てる前例は、Java
がヒープ上で全ての内容(基本型を除く)を割り当てるコストが非常に高いと感じさせるかもしれません。しかし、実際にはJava
の割り当て方式をコンベヤーベルトのように考えることもできます:スペースを割り当てた後、「ヒープポインタ」は未割り当ての領域に前進します。これはスタックの割り当て方式に非常に似ていますが、いくつかの追加コストが発生することもあります。しかし、これは無制限ではなく、長期的には、オペレーティングシステムがページングスケジューリングを実行し始めることになります —— ディスクの入出力を通じて、物理メモリよりもはるかに大きなプログラムを実行し、物理メモリ空間を効率的に利用しますが、パフォーマンスに大きな影響を与えることもあります。これがGC
が登場する時です。ガベージコレクタはすべての無駄なデータをクリーンアップし、オブジェクトを圧縮し、スペースを回収し、「ヒープポインタ」を開始位置に近い場所に再配置することで、ページフォールトの発生をできるだけ避けます —— 必要なページがメモリにないとき、オペレーティングシステムは中断を生成し、ディスクから必要なページを読み込み、再び中断が発生した場所のコードを実行します。
参照カウント —— 実際には採用されていない方案#
私たちは、ガベージコレクタの実装原理に関する説明を多かれ少なかれ聞いたことがあるかもしれません:各オブジェクトは参照カウンタを保持し、オブジェクトが参照されるたびにカウンタが増加します;参照がスコープを離れるか、null に設定されると、カウンタが減少します。ガベージコレクタはこれらのカウンタが 0 であるかどうかをチェックし、0 であれば参照が指していないことを示し、そのスペースを回収します。参照カウントは通常、ガベージコレクタの動作方式を説明するために使用されますが、実際にこの方案を採用しているJVM
はありません。
ガベージコレクタの回収アルゴリズムは、大まかに以下のように分類できます:
- 停止 - コピー:
- マーク - クリア:
- 世代:
- 適応:
すべての方案は、ヒープ上で生存しているすべてのオブジェクトが、最終的にスタックまたは静的ストレージ内の参照に追跡できるという考え方に従っています。
停止 - コピー方案#
この方案の処理は単純明快です:プログラムを停止し、すべての生存オブジェクト(つまり参照を通じてアクセス可能なオブジェクト)を一つのヒープから別のヒープにコピーします。コピーされなかったオブジェクトはガベージとして回収されます。
この方案には二つの明らかな欠点があります:
- コピーを実現するためには二倍のメモリが必要です。もちろん、この問題には解決策もあります:いくつかの
JVM
実装では、ヒープをブロックに分割し、コピーはブロック間で行われます。 - プログラムが安定している場合、ガベージがほとんど生成されません。しかし、それでも「停止 - コピー」がトリガーされることがあり、これはリソースの無駄です。
マーク - クリア方案#
「停止 - コピー方案」によるリソースの無駄を防ぐために、ガベージが生成されていないことを検出した場合、JVM
は「マーク - クリア」方案に切り替えます:スタックと静的ストレージから始めて、すべての参照が指しているオブジェクトを探索し、見つかったオブジェクトにフラグを設定します。この時点では回収アクションは発生していません。探索が完了した後、回収を実行します。
この方案は通常の場合、「停止 - コピー」方案よりも効率が低いですが、プログラムが安定していてガベージがそれほど多くない場合、この方案の速度は非常に速くなります。
適応#
JVM
は状況に応じて、自動的に適切な方案に切り替えて実行します。
ああ、確かに複雑です。私の Java の境地はまだ GC を研究するレベルには達していないかもしれません。ここで TO-DO を作成し、後で詳細な情報を補完します。
オブジェクト作成のプロセス#
- オブジェクトを初めて作成するか、クラスの静的メンバーまたはメソッドに初めてアクセスする際、Java インタープリタはパスに基づいて対応する
.class
ファイルを検索します。 .class
ファイルがロードされると、ファイル定義順序に従ってすべての静的初期化が開始されます。静的初期化は、初回のロード時にのみ発生します。- ヒープ上に対応するオブジェクトを作成し、十分なストレージスペースを割り当てます。
- そのオブジェクトのすべての基本型をデフォルト値に設定し、参照を
null
に設定します。 - ファイル定義順序に従ってメンバ変数を初期化します。
- コンストラクタを実行します。
可変引数リスト#
function(Object... args)
は省略記号を使用し、コンパイラが自動的に埋め込みます。args
は可変長の配列です。可変引数リストは、引数リストの末尾に配置する必要があります。
JDK 11
局所変数型推論#
JDK 11
では、var
キーワードを使用して変数を宣言でき、コンパイラが自動的に型推論を行います。
第 7 章 隠蔽の実現#
本章の主な核心思想は:変化するものと変わらないものを分離することです。アクセス修飾子を通じて、ライブラリの開発者が顧客プログラマーに対してどれが利用可能で、どれが利用できないかを示すことを許可します。個人的には、アクセス修飾子を理解することが重要だと思います。
- public:任意のクラスがアクセス可能で、実際にはアクセス制限がありません。
- protected:同じパッケージ内の他のクラスがアクセス可能で、異なるパッケージではサブクラスのみがアクセス可能です。
- (デフォルト)何のキーワードも書かない場合、同じパッケージ内の他のクラスがアクセス可能です。
- private:自身のクラス内の他のメンバーにのみ見えます。
第 8 章 再利用#
本章では、Java
言語の二つのコード再利用の方法:コンポジションと継承について主に紹介します。コンポジションは、新しいクラス内に既存のクラスのオブジェクトを作成することを指し、私たちが再利用するのはコードの機能であり、形式ではありません。継承は、既存のクラスの形式を直接コピーし、新しいコードを追加して、既存のコードを汚染しない新しいクラスを作成します。チューリングのガイドでは、クラスのロードのタイミングと順序、継承体系におけるサブクラスと基底クラスの初期化の正しい順序についても詳しく解説しています。
オーバーロードとオーバーライド#
-
メソッドのオーバーロード:同じクラス内、または基底クラスとサブクラス内で、複数のメソッドの名前が同じであることをメソッドのオーバーロードと呼びます。メソッドのオーバーロードの条件は:
-
パラメータの数が異なる。
-
パラメータの数が同じで、データ型が異なる。
-
パラメータの数が同じで、データ型が同じだが順序が異なる。
ただし、3 番目の方法は実際には使用しない方が良いです。
-
-
メソッドのオーバーライド:オーバーライドは、サブクラスが親クラスの許可されたメソッドの実装プロセスを再記述することを指し、戻り値とパラメータは変更できません。つまり、外殻は変わらず、核心をオーバーライドする!重要なのは、メソッドのシグネチャが同じであること、変更できないことです!
継承を慎重に考える#
継承の思想はさまざまな書籍や教材で重視されていますが、だからといって継承をできるだけ使用すべきだというわけではありません。関連する要求がある場合、まずはコンポジションを優先し、慎重に継承を考慮するのが最良です。本書では、コンポジションを選択するか継承を考慮するかを明確に区別する方法を提供しています:「アップキャストを使用する必要がありますか?」。
final
#
final
データ#
基本データ型に対して、final
キーワードはその値を不変にします;参照データ型に対しては、final
はその参照を不変にします。つまり、一度final
で修飾された参照がオブジェクトを指す(初期化される)と、その参照は別のオブジェクトを指すことができなくなります。
空白final
#
原文を引用:final
に対して代入操作が行われるのは、次の二つの場所のみです:
- フィールド定義時に式を使用して代入する。
- 各コンストラクタ内で。
理由:これは、final
フィールドが使用される前に常に初期化されることを保証するためです。
private
メソッドは暗黙のfinal
メソッド#
メソッド内で二つの修飾子を組み合わせても、追加の意味はありません。private
メソッドはアクセスできず、オーバーライドもできません。テストの結果、オーバーライドできるように見えますが、実際には二者には何の関係もありません。private
メソッドはクラスのインターフェースではなく、クラス内に隠されたコードです。したがって、二つのクラス内の同じメソッド名は単に重複しているだけであり、@Override
注釈を使用するとこのエラーを検出できます。
初期化とクラスのロード#
クラスの初期化タイミングは一般的に次の通りです:
クラスの初期化順序は次の通りです:
第 9 章 ポリモーフィズム#
本章ではポリモーフィズムについて詳しく紹介します —— オブジェクト指向の思想のもう一つの基本的な特徴であり、プログラマーが「変化するものと不変のものを分離する」ための重要な技術です。大学でのJava
学習を振り返ると、ポリモーフィズムはカプセル化、継承とともにオブジェクト指向思想の三大特徴としてしばしば言及されます。しかし、現在の多くのフィードバック、特に《On Java》書籍でも言及されているように、必要でない限り、コンポジションを優先し、継承を慎重に考慮するべきです。本章を読んだ後の私のまとめは、継承の利点はポリモーフィズムを利用できることにあります。ポリモーフィズムは非常に素晴らしいものであり、できるだけ利用すべきですが、継承体系があると、後に少しでも継承体系に適さない新しいクラスが現れると、後のリファクタリングや修正が非常に面倒になります。一方、インターフェースはこの面倒な問題を効果的に解決できます。
バインディング#
ポリモーフィズムを使用する際、よくある疑問があります:明らかに基底クラスの参照を渡して、基底クラスのメソッドを呼び出しているのに、なぜサブクラスの参照を渡してもサブクラスのメソッドを呼び出せるのでしょうか?言い換えれば、コンパイラはどのようにしてこの参照の正確な型を知ることができるのでしょうか?実際には、コンパイラはそれを知りませんが、Java
は後期バインディングの言語です。後期バインディングは実行時に発生し、オブジェクトの型を決定します。このメソッド呼び出しメカニズムの存在により、コンパイラが実際には正確な型を知らなくても、正しくメソッドを呼び出すことができます。そして、ポリモーフィズムのメカニズムがあることで、私たちは基底クラスと通信するメソッドを記述するだけで、サブクラスと通信でき、オブジェクトの型を判断する必要がなく、冗長なコードを排除し、コードの再利用を実現できます。
継承とメンバ変数#
継承後、サブクラスが親クラスと同名のメンバ変数を定義した場合、どのように表示されるかは参照によって決まります。例えば;
class Person {
String name = "Person";
public String getName() {
return name;
}
}
class Student extends Person {
String name = "Student";
@Override
public String getName() {
return name;
}
}
public class Demo {
public static void main(String[] args) {
Student student = new Student();
Person person = new Student();
System.out.println(student.name);
System.out.println(person.name);
System.out.println(student.getName());
System.out.println(person.getName());
}
}
出力結果:
これは、メンバ変数にはポリモーフィズムがなく、メソッドのみがポリモーフィズムを持つことを示しています。実際、サブクラスには二つのname
メンバ変数が存在します:Person.name
とStudent.name
が異なるストレージスペースに割り当てられています。親クラスの同名フィールドにアクセスするには、super.name
を明示的に使用する必要があります。したがって、このような混乱を避けるために、一般的にはメンバ変数をprivate
に設定します。
コンストラクタ内部のポリモーフィズムメソッドの動作#
原則:できるだけ少ない操作でオブジェクトを正常な状態にし、必要でない限り、コンストラクタ内で他のメソッドを呼び出さないようにします。
ポリモーフィズムの例として、親クラスのコンストラクタがメソッドを使用しており、そのメソッドがサブクラスでオーバーライドされている場合、この状況は非常に危険です:== 初期化されていないサブクラスオブジェクトのメソッドを呼び出すことになります!==
協変戻り値型#
サブクラスがオーバーライドしたメソッドの戻り値は、基底クラスメソッドの戻り値のサブタイプであることができます。
継承を使用するための原則#
継承は行動の違いを表現し、フィールドは状態の変化を表現するために使用します。