Javaによるプログラミングに関する世界観2C(ガーベッジコレクションと例外)です。オブジェクト指向も参照のこと。
Javaでは、自分でメモリを解放しなくても、ガーベッジコレクションによって参照がなくなったオブジェクトを自動で破棄してくれる。
また、ファイルオープンのエラー処理などは例外が行ってくれる。
ガーベッジコレクションと例外があるおかげで、C/C++でプログラミングを行うのとは比べ物にならないぐらい楽で安全にプログラミングができる。
その代わり、スピードや効率は遅いし、いつどのような状況でGCが行われるのかを熟知しておく必要がある。
C言語(配列とポインタ)とプログラミング作法(ポインタとメモリ管理)も参照のこと。
ガーベッジコレクションとは、Javaの参照されなくなったオブジェクトを回収し、空きメモリとして再利用する機構のこと。
オブジェクトのインスタンス変数が、どこからも参照されなくなった場合、すなわち変数名としてのとっかかりが消えて「幽霊状態」になった場合、Java仮想マシンはそれをガーベッジ(ゴミ)とみなす。
Java仮想マシンの使用するシステムの空きメモリが不足してきた時に、このガーベッジを掃除し、使用されていたメモリとデータを解放・消去して、空きメモリとして再利用する。
たとえば、
HogeObject hoge = new HogeObject(); hoge = null;
とした場合、一行目で作ったHogeObjectは二行目でhogeにnullを代入した時点で、参照がどこからもなくなり、ガーベッジとみなされる。
注意点として、少しでも参照が残っているオブジェクトはガーベッジとはみなされない。また、空きメモリが足りなくならない限り、ガーベッジコレクション(GC)は起きない。
プログラムの中で、強制的に手動でGCを起こすためには、java.lang.System.gc()メソッドを実行する。java.lang.*パッケージは何もしなくてもインポートされているので、単にSystem.gc()と書けばよい。
GCが起きた時に行いたい処理がある場合、finalize()メソッドに書くことができる。
(以上はJava言語プログラミングレッスン 第3版(下) オブジェクト指向を始めようを参考に執筆しました。)
Javaにはガーベッジコレクションがあります。しかしながら、GCにおいても、「いつガーベッジコレクタによってオブジェクトが破棄されるのか」という、オブジェクトの寿命を把握し、管理することはとても重要です。
C/C++ではmalloc()やfree()で行っていたため、メモリ解放をし忘れる危険性はありましたが、自分でどのタイミングでメモリ解放を行うのか、きちんと知った上でプログラミングを行えました。
これに対して、GCを行う言語では、「いつGCがメモリを解放するのか」を常に意識し、間違えないようにプログラミングを行わなければいけません。
GCによって、「プログラムの内部で自分で取得したものは必ず自分で破棄する」という伝統的なプログラミングスタイルは一変しましたが、逆にGCのタイミングを知らなければならないため、一部では「malloc()とfree()を返してほしい」といった意見すらあるのです。
これに対して、Rustのような新しいプログラミング言語では、時代の情勢に反してGCがありません。オブジェクトの寿命をきちんと厳密に正しく管理することで、GCが必要なくなるのです。
Javaのようなガーベッジコレクションのある言語の何がいいか、それはメモリリークの心配をする必要がないということです。
メモリリークとは、「メモリを使いすぎ状態になること」です。メモリをmalloc()などで動的に取得しておいて、free()などで解放するのをし忘れてしまい、必要のないメモリを確保し続けて、結果システムのリソースを使い果たしてシステム全体に影響がでるようなバグのことです。
C/C++でも、きちんとメモリの解放をすれば、メモリリークは起きません。ですが、大規模な並列処理プログラムになってくると、「このポインタは解放してよかったのか、それともしてはならないのか」ということが、コードを書いているプログラマにとっても、実際に動いているプログラムにとっても、分からなくなってしまうのです。
このような時、参照カウンタなどを使ったガーベッジコレクションは、どこからも参照のなくなったデータを自動的に破棄してくれます。プログラマが自分で解放するのを忘れても、メモリを使いすぎ状態になることがないのです。
もちろん、デメリットもあります。ガーベッジコレクションはパフォーマンスを低下させます。自分で手動でメモリを確保・破棄するC++のプログラムは、ある意味でパフォーマンス最優先のところがあり、たとえばたくさんのメモリを動的に確保する画像処理アプリケーションなどでは、GCを使わないほうがプログラムのスピードは速くなります。
ですが、スピードがそんなに必要のないプログラムで、複雑かつ大規模なプログラムであったり、あるいは参照を管理することが人間の手では不可能な高度な並列処理プログラムだったりすると、C++のように「自ら取得したものは自ら解放する」という言語では、プログラムを記述することができなくなってしまいます。
要するに、スピードを重視するか、それとも安全性を重視するかということです。なので、絶対に安全性が必要で、スピードはある程度あればいいようなエンタープライズなアプリケーションであった場合、C++よりもJavaを使ったほうが適切である場合があります。たとえば、OS(そもそもOSはCのような低水準言語でしか開発できない)やWebブラウザや画像処理プログラムならC/C++で開発すべきでしょうが、金融機関のバッチ処理プログラムなどはJavaで開発すべきでしょう。
例外。try構文の途中でエラーが出た時は、自動的にcatch文がそれを「捕捉」する。
try { ... } catch ( ... ) { ... }
後日注記:例外を吐くにはthrowを使う。独自の例外クラスを作るにはExceptionクラスを継承する。その場でtry-catchせずにもっと上流の呼び出し側で捕捉する場合はメソッド名にthrowsをつける。
後日注記:たとえば、例外をthrowで吐いた時、throwsをメソッド名につけることで、そのメソッドを使うユーザがメソッドを呼び出した時に例外をtry-catchすることができる。逆に言えばthrowsがつけられているメソッドを使う時には必ずその例外をtry-catchする必要がある(あるいはさらに上流にthrowsするか)。
Javaの例外は、C++やほかのオブジェクト指向言語にもある機能であり、使いやすく見えますが、意外ときちんと実装するのは面倒くさいです。
特に、自らの独自の例外を作る際に、その例外クラスを実現するためのソースファイルをわざわざ作らなければなりませんし、その例外を扱うためのさまざまな場所に適切に記述をしなければなりません。
ですが、Javaの良さというのは、そうした「面倒だけどもきちんと正しく記述すればきちんと動いてくれる」ということです。
C言語のエラーチェックは、数値の比較を使ったものであり、何も知らない人が見ると、そのエラーチェックの意味を理解することができません。きちんとエラーチェックをしなかった場合、もし想定外の動作をしても対応できず、簡単にバグが生まれます。
C++にも例外はありますが、Javaほどきちんと設計されたものではなく、リーナス・トーバルズなどは「C++の例外は壊れている」と言います。
Javaの例外処理は、きちんとやるのは面倒くさいですが、きちんと書くときちんと動きます。面倒くさいと言えど、関数やAPIごとに異なるC言語のエラーチェックよりは使いやすいものであると言えるでしょう。
詳しくは以下の書籍が参考になります。