Javaは今,最も使われているプログラミング言語の一つである。1995年に発表されて以来,「C++の欠点を取り除いた優れたプログラミング言語」という評価を受けてきた。だが広く使われるに連れ,だんだんその欠点も見えてきている。

 Javaの欠点と言われるのは次の三つだ。(1)オブジェクトではないデータ型があること,(2)一つの表現でいろいろなクラスに当てはまるような記述法がないこと,(3)二つ以上のクラスの実装を継承できないこと,である。プログラミング言語のあるべき姿は人によって違うため,これらは欠点ではないという見方もある。ただプログラマの混乱を招いたり,ソース・コードの可読性やプログラムの保守性を下げる要因になっている面はある。

オブジェクトでないデータ型がある
表1●Javaのプリミティブ型。
真か偽を示すboolean型と文字,数値型がある

 一つ目は,オブジェクト指向言語だと言いながら,オブジェクトではないデータ型が存在することである。

 「オブジェクト指向」の定義はいろいろあるが,いずれにしても最も基本的な概念は,データと手続きをまとめた「オブジェクト」でシステムを表現するというものだ。Javaの世界でも,クラスという鋳型を使ってオブジェクトを作り,そのメソッドを呼び出すことでプログラムを組み立てる。

 ところがその中に,オブジェクトでないものが混ざっているのだ。プリミティブ型,または基本データ型と呼ばれるものである(表1[拡大表示])。文字を扱うcharや,真偽を示すboolean,int,floatなどの数値型がそれに当たる。

プリミティブ型はメモリ管理の仕組みが違う

図1●Javaのメモリ管理の仕組み。
ローカル変数を格納するスタック領域と,オブジェクトのデータを格納するヒープ領域がある。スタック領域にはオブジェクトを参照するための情報が入っている

 プリミティブ型とオブジェクト型は,メモリ管理の方法が違う。Javaの仮想マシンが管理するメモリ領域には,スタック領域とヒープ領域がある(図1[拡大表示])。スタック領域には,ローカル変数のデータを置く。出てきた順に,ローカル変数のデータを積み上げていく。変数の有効範囲を抜けると,このデータはすぐに解放される。

 一方のヒープ領域は,オブジェクトの実体を格納する領域である。オブジェクト型の変数を作ると,まずスタックにその場所が用意される。次にnew演算子でそこに新しいオブジェクトを作ると,オブジェクトそのもののデータがヒープ領域に作られる。そして,ヒープ上のオブジェクトの位置が,オブジェクト型の変数のデータとしてスタック領域に書き込まれるのである。これはオブジェクトを参照するための情報なので,オブジェクト型の変数は「参照型」と呼ばれる。

 これに対し実際のデータ自体がスタック領域に書き込まれるのがプリミティブ型である。このようなメモリ管理をする型を「値型」という。

 参照型の変数が変更されると,ヒープ領域にある実際のオブジェクトのデータを参照し,データを書き換える。例えばメソッドの引数にオブジェクト型の変数を記述したとき,メソッドに渡されるのはその場所の情報である。だからメソッド内で加えられた変更は呼び出し元のオブジェクトにも反映される。一方,値型の変数はその値が渡される。メソッドの内部で変更されても,それは元の変数には反映されない。

オブジェクトが持つ機能を使えない

リスト1●JavaのVectorクラスに数値のデータを格納するプログラム。
Integerクラスを生成して数値をラップしている

 プリミティブ型とオブジェクト型はメモリ管理方法が違うので,両者のデータを統一したクラス・ライブラリは作れない。例えばオブジェクト型のデータだけなら,任意のデータを入れるクラス・ライブラリを構築できる。

 その一例が,可変長の配列クラスだ。これは,java.util.Vectorというクラスとして作られている。任意のオブジェクトを配列に追加したり取得したり,削除したりできる。これは引数に,任意のオブジェクトを指定できる。しかし,プリミティブ型のデータはオブジェクトではないのでそのままでは入れられない。

 そこでJavaには,プリミティブ型に相当するクラスもある。例えばint型の変数なら,java.lang.Integerクラスを使う。これを新しく生成して数値を格納し,Vectorに追加すればよい(リスト1[拡大表示])。

 しかしちょっと考えればわかるように,この方法はあまりスマートなやり方ではない。よけいなコードが入り,見てごちゃごちゃした感じがする。さらに,メモリ空間を無駄に使うことも気になる。元々の値とは別に,新たなオブジェクトのメモリを確保し直さねばならない。こういった見かけ上の問題に加え,本質的な問題もある。データの同一性を保てないことだ。オブジェクト型として保持する値と,プリミティブ型として保持する値はまったく別のものだ。プリミティブ型の値を変更しても,元々のint型のデータには反映されない。

C#がBoxingで解決するのは一部だけ

 これはJava固有の問題ではない。例えばJavaに似ている言語として知られるC#も同じ問題を抱えている。C#の場合,この問題をBoxingという仕組みで一部解決している。しかし解決するのは,余分なコードを書かなくてもいいという部分だけだ。メモリの問題と同一性の問題は残り続ける。

 C#でも,intやdouble,charなどのデータ型はオブジェクトとしては扱われない。Javaのプリミティブ型と同じく,値型の変数である。

 C#では,この値をオブジェクトに代入できる。リスト2[拡大表示]に具体的なコードを示した。Object型の変数に,int型の値を代入している。ここでBoxingが行われ,基本型のデータを暗黙のうちにオブジェクト型へと変換しているのだ。ヒープ領域にメモリを確保し,そこに数値データを格納する(図2[拡大表示])。Object型の変数は,それを参照する。

リスト2●Boxingが実行されるC#のコード。
Objectに直接数値を代入できる。このコードを実行すると,0と1が出力される。つまり変数aとoの同一性はない
 
図2●C#のBoxingの仕組み。
スタック上にあるint型の構造体をBoxingする時は,ヒープ上にオブジェクトを暗黙のうちに作成する。このため,当初の値との整合性は保たれない

 このBoxingの仕組みを使って,C#でJavaのVectorに値を格納するのと似たようなコードを書いてみた(リスト3[拡大表示])。ArrayListクラスは引数にObject型の変数をとるが,そこにintの変数を直接指定できるためコードはすっきりする。

 ただし,余計なメモリの消費と値の同一性の問題は解決しない。単に,オブジェクトへの変換を「自動化」しているだけだからだ(図3[拡大表示])。

リスト3●リスト1と同じ動作をするC#のコード。
Boxingの仕組みがあるため,ArrayListに直接数値を追加できる
 
図3●JavaとC#で,int型の変数をオブジェクトに変換する仕組み。
内部的な処理は基本的に同じだが,変換を暗黙のうちに行うのがC#の特徴だ

実用を考えれば利点とも言える

 こう見てくると,なぜ最新の言語であるJavaやC#がこんな問題を残しているのか疑問が出てくる。実は性能を重視したからなのだ。

 プリミティブ型のデータは,プログラミングをする上で一番よく使うことになるので,それを素早く処理できるプリミティブ型で扱うと性能が高まる。オブジェクト型のデータはオブジェクトを生成したり,位置情報を使ってヒープ領域のデータを参照しにいくのにもオーバヘッドが生じる。もう一つの問題はヒープ領域だ。すべてオブジェクト型だったら,例えば簡単なforループを実行しただけで,ヒープ領域に大量のオブジェクトを作ることになる。ヒープ領域の消費速度は急激に上がり,頻繁にガベージ・コレクションが発生するためやはり性能が低下する。

 JavaやC#は性能を考えて,プリミティブ型のデータを作った。このため「純粋な」オブジェクト指向言語とは言えなくなったのだ。実用性を考えれば妥当な線と言えるのではないだろうか。

(大森 敏行、八木 玲子)