第 6 章 初期化とクリーンアップ#
‘安全でない’プログラミングは、プログラミングコストが高くなる原因の一つです。
そして == 初期化 == と == クリーンアップ == は、‘安全でない’プログラミングを引き起こす二つの要因です。
本章では、Java がコンストラクタを使用してオブジェクトを初期化する方法と、ガベージコレクタに関するいくつかの説明に焦点を当てています。チューリングガイドでは、本章に重点と難点があることが説明されているため、自分の考えをできるだけ詳しく述べるようにします。
コンストラクタによる初期化の保証#
コンストラクタ(コンストラクタ関数)は、Java がオブジェクトを正しく初期化するための重要な手段です。コンストラクタはクラス名と完全に同じで、戻り値の型がないメソッドです。クラス名が完全に同じであることは、コンストラクタが lowerCamelCase の命名規則を持つメソッド名には適用されないことを意味します;戻り値の型がないというのは、戻り値の型がvoid
であるのではなく、戻り値の型が存在しないことを意味します。C++ と比較すると、Java のコンストラクタと初期化は一体です。
ここには古典的な罠があります:コードを書くとき、常にnew
キーワードを使用してコンストラクタを呼び出してオブジェクトを作成するため、コンストラクタの役割はオブジェクトを作成することだと考えがちですが、実際はそうではありません。オブジェクトの作成はJVM
が担当し、JVM
がコンストラクタを呼び出しますが、コンストラクタの役割はオブジェクトの初期化のみです。
メソッドのオーバーロード#
メソッドのオーバーロードのルール:各オーバーロードされたメソッドは、ユニークなパラメータ型リストを持たなければなりません。つまり:
- パラメータの数が異なる。
- パラメータの数が同じで、型が異なる。
- パラメータの数が同じで、型が同じ、順序が異なる。
- 戻り値の型が異なっても、メソッドのオーバーロードにはなりません。
- パラメータの識別子が異なっても、メソッドのオーバーロードにはなりません。
- 修飾子が異なっても、メソッドのオーバーロードにはなりません。
もちろん、渡されるパラメータの型がオーバーロードされたメソッドのパラメータ型リストにない場合、小さい型は昇格されます;char
型はint
型に昇格されます;型が大きすぎるとエラーになります。メソッド呼び出しが複数のメソッドにマッチする場合、一般的にはデータ型が「近い」もの、暗黙の型変換「回数が最も少ない」ものが選択されます。
引数なしのコンストラクタ#
引数なしのコンストラクタの一般的な部分は理解しやすいです:引数のないコンストラクタです。
ただし注意が必要です:コンストラクタを一つでも定義した場合、そのコンストラクタが引数ありであろうとなかろうと、コンパイラは == 自動的に == 引数なしのコンストラクタを作成しません。
例え話:コンストラクタを定義していない場合、コンパイラは「おお、あなたはコンストラクタが必要に違いない、無引数のコンストラクタを追加してあげましょう」と考えます;しかし、すでにコンストラクタを提供している場合、コンパイラは「あなたはすでにコンストラクタを持っている、あなたは自分が何をしているか知っている;もしあなたが自分で無引数のコンストラクタを提供しなかった場合、あなたには理由があるに違いない、もしかしたらそれが不要だからだろう、だから私も追加しません。」
本当に、泣きそう
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
に設定します。
コンストラクタ内部のポリモーフィズムメソッドの動作#
原則:できるだけ少ない操作でオブジェクトを正常な状態にし、必要でない限り、コンストラクタ内で他のメソッドを呼び出さないようにします。
ポリモーフィズムの例として、親クラスのコンストラクタがメソッドを使用し、そのメソッドが同時にサブクラスでオーバーライドされている場合、この状況は非常に危険です:== 初期化されていないサブクラスオブジェクトのメソッドを呼び出してしまう!==
協変戻り値型#
サブクラスがオーバーライドするメソッドの戻り値は、基底クラスメソッドの戻り値のサブタイプであることができます。
継承を使用するための原則#
継承を使用して行動の違いを表現し、フィールドを使用して状態の変化を表現します。