デストラクタの特徴とは?C#・Javaでの使い方も解説!

- PG
- コンストラクタなら聞いたことがありますが、デストラクタとは何でしょうか……?
- PM
- デストラクタはC#でメモリの管理を行う際に使うメソッドですね。注意点もあるので、ここで使い方を覚えておきましょう!
この記事でわかること
C#のデストラクタとは
はじめに、デストラクタについて知識を入れておきましょう。デストラクタ(ファイナライザ)とは、ガベージコレクターによってインスタンスが破棄されるときに呼び出されるメソッドです。
【知識】
ガベージコレクター…プログラムが動的に確保したメモリ領域を解放する機能「ガベージコレクション」を行うもの。
デストラクタとコンストラクタとの違い
デストラクタと同様にインスタンスのリソース管理にかかわる機能として、コンストラクタというものがあります。このコンストラクタは、デストラクタとは違い、インスタンスが生成されるときに呼ばれるメソッドです。基本的にはクラス内パラメータの既定値設定等で使われますが、2回以上インスタンスを生成できないクラス(シングルトンクラス)の実現等にも利用されます。
デストラクタの特徴7つ
ここまでデストラクタの役割について紹介してきましたが、実際に使用する際は通常のメソッドとは異なる性質を持つため注意が必要です。ここでは、デストラクタの特徴を7つ紹介します。
1:実行の指定はできない
デストラクタの呼び出しタイミングをプログラマが制御することはできません。デストラクタがいつ呼ばれるかは、ガベージコレクター次第になります。ただし、少なくとも該当インスタンスがアプリケーション内で使用される可能性がある(参照元が存在する)間は実行されません。つまり、デストラクタの実行タイミングは、ガベージコレクターが該当インスタンスを終了処理が可能なオブジェクトであると判断したときとなります。
2:1つのデストラクタにつき1つのクラスで使う
デストラクタは1つのクラスで1つのみ使用できます。1つのクラスにデストラクタを多重定義することはできません。
なお、コンストラクタは1つのクラスに対して多重定義ができます。
3:使用できるのはクラスのみ
デストラクタはクラスでのみ使用ができます。構造体では定義することも使用することもできません。
なお、コンストラクタは構造体でも定義および使用ができます。
4:Java ではファイナライザと呼ばれる
Javaでは全クラスの継承元(基底クラス)であるObjectクラスのfinalizeメソッドがデストラクタの役割を担い、ファイナライザと呼ばれます。C#のデストラクタ同様にガベージコレクターにより使用されないと判断されたときに実行されます。
5:引数や戻り値を持たない
前述の通り、デストラクタはアプリケーションが呼び出すものではなくシステム側のガベージコレクターが呼び出すものですので、引数と戻り値を持たせることはできません。また、修飾子も持ちません。
なお、コンストラクタは引数を持つものを定義できます。戻り値は持ちません。
6:頭にチルダを付ける決まりがある
デストラクタの名前はクラス名の頭にチルダを付与したものになります。例えば、「Test」というクラスでデストラクタを定義する場合は「~Test()」とすることになります。
なお、コンストラクタはクラス名と同名になります。
7:自分で定義しない場合は自動で生成される
デストラクタは必ず明示的な定義が必要なものではありません。特に処理が必要なければ宣言は不要です。定義しない場合でデストラクタが呼ばれた場合は、暗黙的に継承チェーンの全インスタンスにあるデストラクタを呼び出すものとして解釈されます。
以上、デストラクタの特徴を7つ紹介しました。とはいえ、あまりピンと来ないと思いますので、さっそく次項でサンプルプログラムを紹介します。
C#のデストラクタの使用例
ここでは、実際にデストラクタを使用し、その役割について解説していきたいと思います。デストラクタは以下のように、クラス名の前に「~」を付けて定義します。
1 2 3 4 5 6 7 8 |
class SampleClass { ~SampleClass() // デストラクタ { // 処理 } } |
では、デストラクタを呼んでみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
using System; namespace FinalizeSample { class Finalizer { static void Main() { Console.WriteLine("1"); // コンストラクタの呼び出し Program program = new Program(); Console.WriteLine("2"); // programインスタンスは利用されなくなる program = null; Console.WriteLine("3"); Console.WriteLine("4"); Console.WriteLine("5"); Console.ReadLine(); } } class Program { // コンストラクタの定義 public Program() { Console.WriteLine("コンストラクタが呼ばれたよ"); } ~Program() { Console.WriteLine("デストラクタが呼ばれたよ"); } } } |
そして、こちらが実行結果です。
上画像の通り、この場合ではデストラクタは呼び出されないことが分かります。これは前述のデストラクタは「いつ呼び出されるか分からない」という特徴によるものです。そのため、アンマネージリソースで使用するのは控えるようにしましょう。
Javaでのデストラクタの使い方
Javaでデストラクタ(ファイナライザ)を使用する場合は以下のようにfinalizeメソッドを実装します。
1 2 3 4 5 6 7 |
public class SampleClass { public void finalize() { // 処理 } } |
デストラクタを呼び出す
では、デストラクタを呼んでみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package finalizeSample; public class Finalizer { public static void main(String[] args) { System.out.println("1"); // コンストラクタの呼び出し Program program = new Program(); System.out.println("2"); // programインスタンスは利用されなくなる program = null; System.out.println("3"); System.out.println("4"); System.out.println("5"); System.gc(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package finalizeSample; public class Program { // コンストラクタの定義 public Program() { System.out.println("コンストラクタが呼ばれたよ"); } public void finalize() { System.out.println("ファイナライザが呼ばれたよ"); } } |
ただし、このプログラムの実行結果もC#のときと全く同じとなります。これはJavaでもC#と同じく、デストラクタは「いつ呼び出されるか分からない」ためになります。
try-with-resources文を利用したリソース管理
一般的にプログラミングでは確保したリソースはfreeやdeleteといった関数・演算子を使って解放します。しかし、この方法は解放処理が意図せず複数回呼ばれ、メモリ領域が壊れてしまう(二重解放となる)危険性があります。
そこでJavaでは、特定のリソースをスコープ内でしか使用しないことを明示的に示す書き方として、try-with-resources文があります。これを利用することで、リソースを確実に閉じつつも複数回解放してしまう危険性を防げます。では、実際に例を見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package finalizeSample; import java.io.IOException; public class Finalizer2 { public static void main(String[] args) { System.out.println("1"); // コンストラクタの呼び出し try (Program2 program = new Program2()) { System.out.println("2"); } catch (IOException e) { // エラー・例外処理 } // programインスタンスは利用されなくなる System.out.println("3"); System.out.println("4"); System.out.println("5"); System.gc(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package finalizeSample; import java.io.Closeable; import java.io.IOException; public class Program2 implements Closeable { // コンストラクタの定義 public Program2() { System.out.println("コンストラクタが呼ばれたよ"); } public void finalize() { System.out.println("ファイナライザが呼ばれたよ"); } @Override public void close() throws IOException { System.out.println("クローズ処理が呼ばれたよ"); } } |
そして、実行結果は以下のようになります。
1 2 3 4 5 6 7 8 |
1 コンストラクタが呼ばれたよ 2 クローズ処理が呼ばれたよ 3 4 5 ファイナライザが呼ばれたよ |
スコープを抜けた段階でクローズ処理が呼ばれていることが分かります。ただし、この時点でデストラクタは呼ばれないことに注意が必要です。デストラクタが呼ばれるタイミングはあくまでシステムのガベージコレクターに依存します。(例ではSystem.gcメソッドを利用して、アプリケーション側からガベージコレクターを実行させています)
デストラクタを使用するときの注意点
アプリケーションが外部リソースを使用する場合、デストラクタを明示的に定義し、そこでリソースの開放処理を記述する必要があります。その記述がない場合、インスタンスの生成と破棄を繰り返すうちに外部リソースを使用し続けてメモリ解放が起きない、いわゆるメモリリーク等のリソース問題の原因となる可能性があります。
- PG
- あれ、デストラクタが呼ばれていませんね……。
- PM
- 先ほど説明したとおり、デストラクタは「いつ呼び出されるか分からない」という特徴があります。アンマネージリソースで使用するのは控えるようにしましょう。
デストラクタは確実にインスタンスを破棄してくれるメソッド
今回はデストラクタの概要から使い方までを解説していきました。デストラクタは確実にインスタンスを破棄してくれるメソッドです。しかし、「いつ実行されるか分からない」というデメリットもあるため、Disposeメソッドを呼び出すusingと一緒に使用するようにしてください。