JavaとC++の比較

出典: フリー百科事典『ウィキペディア(Wikipedia)』

これはこのページの過去の版です。124.99.156.125 (会話) による 2012年2月28日 (火) 16:06個人設定で未設定ならUTC)時点の版 (→‎意味論)であり、現在の版とは大きく異なる場合があります。

JavaとC++の比較(ジャバとシープラスプラスのひかく)では、JavaC++の比較について説明する。

設計思想

C++とJavaとの違いは、それら言語の歴史から辿ることができる。

C++とJavaは開発の目的が異なるため、両者の方針トレードオフに違いが生じている。

C++ Java
Cとの後方(下位)互換性 Java以外の言語との互換性はない
プログラマを当てにする プログラマを守る
低レベル機能に触れる オブジェクトを通してのみメモリアクセス
簡潔な表現 明確な表現
明示的な型破壊を許可 型安全性
マルチパラダイム手続き型オブジェクト指向関数型総称型 オブジェクト指向、総称型
演算子多重定義 演算子の効果は不変
限られた範囲の標準ライブラリ (GUI、ネットワーク、マルチスレッドを含む)機能豊富で容易に使用できる標準ライブラリ
家電製品・制御系等への組み込みから汎用のアプリケーション イントラネット等のWEBアプリケーション・携帯アプリケーション

これらの方針の違いの主な要因は、C++がC言語との互換性を維持しようとしたため、またC言語の利点(機械語に準ずる高速性など)を一切損なわないようにしたためである。

言語の特徴

文法

  • Java文法はシンプルなLALRパーサによって解析できる文脈自由文法である。C++の構文解析は、それよりも複雑である。例えば、Foo<1>(3);は、Fooが変数であれば比較シーケンスであるが、Fooがクラスのテンプレート名であればオブジェクトを生成する。
  • C++では名前空間レベルの定数、変数、関数が認められている。Javaでは、宣言はクラスインタフェースの中に書かなければならない。
  • C++のconstは、「論理的に読み取り専用データである」ことを明示し、データ型にも適用される。Javaのfinalは、「変数が再び割り当てられない」ことを示す。基本型にとっては(const intfinal intなど)これらは等価だが、クラスでは異なる:
C++ Java
const Rectangle r;
r = anotherRectangle; //誤り
r.x = 5; //'誤り'。 r は Rectangle型の定数
final Rectangle r = new Rectangle();
r = anotherRectangle; // 誤り
r.x = 5; // '正しい', rは依然として同じ長方形を参照している。
  • ただし、C++のconstはJavaのfinalと同様の用途にも用いられる。
Rectangle *const r = new Rectangle;
r = &anotherRectangle; //誤り
r->x = 5; //'正しい', rは依然として同じ長方形を参照している。
  • C++はgoto文をサポートする。Javaはサポートしないが、ラベル付break文ラベル付continue文で、構造上ややgotoライクな機能を提供する。実際には、Javaは、コードを読みやすくするため、構造化制御フローを強要する。
  • C++はJavaが持たないやや低レベルな機能を提供する。C++には、特有のメモリ記憶位置や低レベルオペレーティングシステムコンポーネントを書くために必要なタスクを操るのに役立つポインタがある。同様にして、多くのC++コンパイラはインラインアセンブラをサポートする。Javaでは、そのようなコードは全て外部ライブラリに配置し、Java Native Interfaceを通してアクセスしなければならない。そのため、呼び出しのたびに、大きなオーバーヘッドが発生する。

意味論

  • C++はCとの互換性を保つため組込型の暗黙な型変換をある程度許可しているが、大抵のコンパイラでは警告を出す。また、複合型の暗黙型変換も定義できる。一方、Javaでは暗黙な変換としてネイティブ型の精度が大きくなる型変換(拡張変換)のみを許可する。他の変換は文法的に明示的なキャストを要求する。
    • この影響はJavaとC++双方にある条件式(ifwhileforの脱出条件)にも現れる。条件式の型はbooleanであることが期待されるが、intからbooleanへの精度が狭められる暗黙的型変換(縮小変換)が無いため、if (a = 5)のようなコードは、Javaではコンパイルエラーになる。これは、if (a == 5)ミスタイプに対する対策となるが、if (x)のようなコードをC++からJavaへ移植する際には型変換を明示することになる分、冗長になってしまう。
  • 関数の引数を渡すとき、C++は参照渡し値渡し両方をサポートする。Javaではすべての引数は値渡しであるが、オブジェクト(非プリミティブ変数)の引数は参照になり、これは間接参照が言語に備わっていることを意味する。
  • Javaのプリミティブ型は大きさと値の範囲が指定されている。一方、C++の型は最小限度が定められているものの、正確な大きさは定められておらず、環境によって異なる。また同じコンパイラでもバージョンが違えば型の大きさが異なることもあるし、コンパイラの設定で変更可能な場合もある。C++で値の範囲が指定されていないとは、仮に同じ16ビット整数だとしても、ある環境では2の補数で[-32768, 32767]、別の環境では1の補数で[-32767, 32767]の範囲になるという例があるということである。
    • Javaの文字はUTF-16であるが、C++にはワイド文字(wchar_t型)とマルチバイト文字(char型)の2種類が存在し、その中身はプラットフォームに依存する。規定されているのはcharが1バイトということのみで、それすらも1バイトが8ビット以上であれば良いというものである。例えば、wchar_tがUTF-16UTF-32であったり、charが(日本語環境では)Shift JISEUC-JPUTF-8のいずれかであったりという具合である。なお、双方の言語で文字列を形成できるが、その型には大きな違いがある。
  • C++の浮動小数点数の丸め誤差と精度と演算はプラットフォームに依存する。Javaは異なるプラットフォームでの一貫した結果を保証する高精度浮動小数点モデルを提供しているが、通常は最適な浮動小数点演算性能を得るためにより大雑把な演算モードが使われる。
  • C++ではポインタを使ってメモリアドレスを直接操作できる。Javaはメモリアドレスを直接操作できるポインタを持っていない(オブジェクトの参照と配列参照だけはポインタを持っているが、どちらもメモリアドレスの直接アクセスを許可しない)。C++ではポインタへのポインタを構築できるが、Javaではオブジェクトアクセスにだけ参照を用いる。
  • どちらの言語も配列は固定の長さをもつ。Javaでは配列はファーストクラスオブジェクトであるが、C++ではメモリ空間上のオブジェクトの連続であり、配列自身をオブジェクトのように扱うことは出来ない。先頭要素のポインタをやりとりすることで配列の受け渡しを行う。なお、標準ライブラリに含まれるstd::arrayを用いることで、固定長配列をファーストクラスオブジェクトとして扱うことができる。Javaの配列は長さがあらかじめ決められ、配列の一部分への参照は厄介である。また、配列外アクセスのチェックが強制される。
  • C++ではポインタは関数やメソッド(関数へのポインタまたは関数オブジェクト)を指すことができる。Javaではこれと同等のメカニズムにオブジェクトやインタフェース参照を使用する。
  • C++はプログラマが演算子多重定義を行うことが可能である。Javaでは加算と文字列連結が可能な"+"と"+="が予め用意されているだけである。
  • Javaはリフレクションや任意に新しいコードを動的ロードする機能をサポートする標準APIを持つ。
  • Javaはジェネリック型が存在する。C++はテンプレートが存在する。
  • JavaとC++はいずれも、ネイティブ型(これらも"基本型"または"組込"型として知られる)とユーザ定義型(これも"複合"型として知られる)を区別する。Javaでは、ネイティブ型は値としての意味しかなく、複合型は参照としての意味だけを持つ。C++では、すべての型が値としての意味を持つが、任意の型の参照を作ることが可能である。
  • C++は任意のクラスの多重継承をサポートする。Javaは型の多重継承をサポートするが、実装は単一継承のみをサポートする。Javaでは、クラスは1つのクラスからのみ受け継ぐことができるが、クラスは複数のインタフェースを実装することができる。
  • Javaはインタフェースとクラスを明確に区別している。C++では、純粋仮想関数(抽象メソッドにあたる)を並べたクラスでインタフェースを表現し、複数のインタフェースの実装は多重継承によって模倣される。
  • Javaはマルチスレッドをサポートした標準ライブラリと言語を持つ。synchronized Java予約語はマルチスレッドアプリケーションをサポートするシンプルでセキュアな相互排他ロック(Mutex)を提供するが、synchronized セクションはLIFOオーダーで残されなければならない。一方、C++のライブラリではもっと柔軟なMutexロックメカニズムを提供されている。

リソース管理

  • Javaは自動ガベージコレクションを必要とする。C++のメモリ管理は普通、手動で行われるか、スマートポインタを通して行われる。C++でガーベッジコレクションを実装することも不可能ではないが、標準規格で要求されているわけではなく、実際には滅多に使われない。また、C++ではオブジェクトを再配置できないが、一般にオブジェクトの再配置が可能であれば、明示に解放するスタイルより空間と時間効率が良くなることが知られている。
    • C++でのスマートポインタは、主に参照カウントが用いられる。その場合、循環参照の注意が必要という欠点がある。
  • メモリ割り当ては、C++では任意に可能だが、Javaはオブジェクトインスタンス化を通してのみ可能である。Javaでは、バイト配列を作ることで任意のブロック割り当てを再現できる。もっとも、Javaの配列もオブジェクトである。
  • JavaとC++はリソース管理に異なる手法を用いる。Javaは主にガベージコレクションに頼り、メモリの再利用だけができ、他のリソースは最後まで回収されないかもしれない。だが、C++は主にRAII (Resource Acquisition Is Initialization)というイディオムに頼る。これは2つの言語間の以下のような様々な違いに現れている。
    • C++では、複合型も組込型同様スタックに割り当てられ、スコープから外れると共に破棄される。Javaでは、複合型は常にヒープに割り当てられ、ガーベッジコレクタによって回収される(これはあくまで概念上の話で、実装上はエスケープ解析最適化でスタックにオブジェクトを割り当てる仮想マシンもある)。
    • C++はデストラクタを持っているが、Javaはファイナライザを持っている。双方はオブジェクトの解放時に優先的に呼び出されるが、それらは重大性が異する。
      • C++オブジェクトのデストラクタは、オブジェクトが解放されるときに必ず呼び出される(ようにコンパイラは実装しなければならない)。例外が投げられたときでも、スタック上のオブジェクトは、スタックの巻き戻し(アンワインド)に伴ってデストラクタが呼ばれる。また、デストラクタは定義さえすれば、利用する側に特別な記述は不要である。このことを利用して確実なリソースの解放を行うのが広義のRAIIである。
      • Javaでは、オブジェクト解放はガーベッジコレクタによって暗黙のうちに解放される。Javaオブジェクトのファイナライザは、最後にアクセスされた後と、実際に解放される前に、ときどき非同期に呼び出されるが、決して何も起こらないかも知れない。非常に僅かしかないオブジェクトはファイナライザを要求する。ファイナライザは解放状態を優先するオブジェクトの多少のクリーンナップを保証しなければならないオブジェクトによって要求されるだけである。—だいたいは、JVM外のリソースへ放出される。Javaでは安全な同期によるリソース解放は、try/finally文を構築して明示的に行われなければならない。
    • C++では、時としてdangling pointer(破棄されたオブジェクトを参照するポインタ)が存在してしまう。dangling pointerを間接参照するときは、基本的にプログラムのバグである。Javaでは、ガーベッジコレクタは参照されているオブジェクトは解放しない。
    • C++では初期化されていないプリミティブなオブジェクトを持つことが可能である。Javaでは、初期化が強制される。
    • C++では、領域を割り当てられたが到達不能であるオブジェクトが発生してしまうことがある。到達不能オブジェクトとは、それへの到達可能な参照が全く存在しないオブジェクトのことである。到達不能オブジェクトは解放(破棄)することができず、メモリリークを引き起こす。それとは対照的に、Javaではオブジェクトは、それがユーザプログラムによって到達不可能になるまでにガーベッジコレクタによって解放される。(注: 異なる到達可能性の強さを考慮に入れた、Javaのガーベッジコレクタとともに働く、弱い参照がサポートされている。) 。
    • Javaは簡単に非メモリリソースを漏らすが、一方でC++では、前述のRAIIによって上手にプログラムを書けば漏らしにくくなっている。

ライブラリ

JavaはC++と比べ標準ライブラリの提供する範囲が広い。標準C++ライブラリ文字列コンテナIOストリームのような比較的一般的な目的のコンポーネントだけを提供する。Java標準ライブラリネットワーキンググラフィカルユーザインタフェースXML処理、ロギングデータベースアクセス、暗号化やそのほか様々な領域のコンポーネントを含む。このような機能は、C++ではサードパーティ製のライブラリやOS固有のAPIによって実現されていることが多いが、どんな環境でも用意されているとは限らない。

C++はCとの後方互換性を持つため、(多くのオペレーティングシステムAPIのような)Cライブラリも直接使用できる。Javaでは、そのような環境固有のライブラリで提供される機能の多くが、クロスプラットフォームでリッチな標準ライブラリで提供される。その一方で、Javaからネイティブなオペレーティングシステムやハードウェア機能に直接アクセスするには、Java Native Interfaceを使用する必要がある。

ランタイム

  • C++は通常、機械語に直接コンパイルされてから、オペレーティングシステムおよびCPUによって直接実行される。Javaは通常バイトコードにコンパイルされてからJava仮想マシン (JVM) がインタプリタでバイトコードを解釈するか、またはJITがバイトコードをマシンコードにコンパイルしつつ実行される。理論上、動的再コンパイルはどの言語でも使うことができる(しかしJavaのほうが向いている)が、現在のところ、どちらの言語でも動的に再コンパイルされることは稀である。
  • 強制によらない表現力のため、C++の多くのエラー要因(範囲外チェックされない配列アクセス、未使用ポインタ、型の不一致など)はコンパイル時または実行時の不適当なオーバーヘッド無しに信頼できるチェックを行えない。このため、低レベルバッファオーバフローページフォールトセグメンテーションフォルトを導いてしまう。標準やサードパーティのライブラリがそのようなエラーを避けることを助ける高水準な(動的配列リストマップのような)抽象概念を提供している。一方、Javaではそのようなエラーは単純に起こすことも、JVMに検出されることも無く、例外によってアプリケーションに報告される。
  • Javaは、配列アクセスの境界チェックを行い、そして領域外にアクセスすることが判明したときに明確な振る舞い(例外の送出)を要求する。これにより、一般に実行が低速になる代わりに不安定さの源が除去される。ただし、コンパイラ解析で不必要な境界チェックが消去される場合もある。C++はネイティブな配列の配列外アクセスの振る舞いを要求しないため、通常は境界チェックしないのが一般的である。ただしstd::vectorのようなC++標準ライブラリでは、atメンバ関数の使用という形で、境界チェック付きアクセスを任意に選択できる。要約すると、Javaの配列は「常に安全で、厳しく強いられる、可能な限り高速」だがC++のネイティブ配列は「常に高速、完全に強制されない、潜在的に危険」ということである。

その他

  • JavaとC++では多くのソースファイルでコードを分割するために異なる方法を使用する。Javaはすべてのプログラム定義でファイル名とパスが影響するパッケージシステムを使用する。Javaでは、コンパイラは実行可能クラスファイルをインポートする。C++はソースファイル間で宣言を分割するヘッダファイルソースコード包含システムおよび名前空間を使用する。(参考 importとincludeの比較。)
  • コンパイルされたJavaバイトコードファイルは通常、C++コンパイラの出力する機械語のコードファイルよりも小さい。第一に、Javaバイトコードは通常、ネイティブな機械語よりもコンパクトである。第二に、C++のテンプレートやマクロが類似コードの重複を発生させやすいことが挙げられる。第三に、Javaは常に標準ライブラリを動的リンクするため、標準ライブラリのコードを出力に含まないということが挙げられる。反面、Javaバイトコードを翻訳実行する環境(JVMやJIT)が要求される。
  • C++コンパイラは、Javaにはない、言葉通りのプリプロセッシング(前処理)の段階があることが特徴的である。これを用いるために、Javaユーザの中には、ビルドプロセスにプリプロセッサを付加する者がいる。
  • 双方の言語は、配列が固定サイズである。Javaでは、配列はファーストクラスオブジェクトであるが、C++では配列はベースとなるオブジェクトの連続した領域であり、最初の要素と随意的な配列の長さをポインタを使って参照しているに過ぎない。Javaでは、配列は境界チェックされ、長さもわかっているが、C++では配列を連続した領域として扱うだけである。C++とJava双方は、リサイズ、サイズ保存できるコンテナクラス(それぞれ、std::vectorjava.util.Vectorまたはjava.util.ArrayList)を提供している。
  • Javaの除算と剰余演算子は0を切り捨てるよう正しく定義されている。C++は、これらの演算子が0を切り捨てるか「マイナス無限大に切り捨てる」かを明確に指定しない。 Javaでは、 -3 / 2は常に-1となる。一方、C++ではプラットフォームに依存し、-1を返すかも知れないし-2を返すかも知れない。C99はJavaと同じように除算を定義している。双方の言語はすべてのaとb (b != 0)で(a/b)*b + (a%b) == aを保証する。C++(ならびにC99より前のC)にこの保証がない理由は、仮にCPUの除算命令がこのような定義でなかったとしても、C++の除算を直接CPUの除算命令にコンパイルできるようにするためである。ただし、(以前の)CやC++でもdiv関数を用いればC++でも常に-3 / 2の商として-1という結果を得られる。

パフォーマンス

このセクションでは、WindowsやLinuxのような一般的なOSでのC++とJava相互の演算パフォーマンスを比較する。

Javaの初期バージョンは、C++のような静的コンパイルされる言語に比べ著しく性能が低かった。これは、C++ではソースコードはハードウェアが直接に解釈できる機械語にコンパイルされるのに対し、Javaでは共通の(ハードウェアに依存しない)仮想機械語であるJavaバイトコードにコンパイルされ、それをJava仮想マシンがインタプリタ的に実行していたからである。例として、

Java/C++構文 C++が生成したコード Java生成したコード
vector[i]++;
mov edx,[ebp+4h]
mov eax,[ebp+1Ch]
inc dword ptr [edx+eax*4]
aload_1
iload_2
dup2
iaload
iconst_1
iadd
iastore

のように、ソースコードレベルでは同様な命令でも、Javaバイトコードの方がネイティブな機械語よりも長くなる傾向にある(C++によるコードでは、3行目でロード,インクリメント,保存が1命令で行われており、短く済んでいる)。

しかし、Javaは長期稼働するサーバやデスクトップのためにジャストインタイム(JIT) コンパイラテクノロジを発展させ、それがC++との性能差を縮めるであろうと言われている。JITコンパイルとは、Javaバイトコードをインタプリタ的に実行するのではなく、実行時にネイティブな機械語にコンパイルしてから実行する方式である。

以下は、JavaはC++よりも高速であるという研究[1]の主張の一部である。

  • CおよびC++では、「なんでも指せる」というポインタの性質が最適化を難しくしている。(ただしこの問題はC99のrestrictキーワードによって回避できる。)
  • Javaでは新たに確保されたメモリがガベージコレクションによって物理的に連続した領域に集められるので、アクセスする際にCPUキャッシュのミスヒットが起こりにくい(CPUキャッシュについてはキャッシュメモリを参照)。
  • 実行時コンパイルはそれがどのプロセッサの上で実行されるか、どのコードを実行するかが解っているため、各CPUに特化したコードを生成したり、高い分岐予測精度を実現したりできる(ホットスポット)。

一般的に、Javaは、メモリ確保やファイルI/Oのような演算においてはC++より性能がよいが、算術演算や三角関数計算ではC++の方が優れた徴候を示す。[2] 数値演算について述べると、Javaは新しいバージョンで大きく進歩しているものの、浮動小数点数を様々なプラットフォームで再現するためのオーバーヘッド等により、未だにC++やFortranより遅い。[3]

脚注

  1. ^ "Performance of Java versus C++" by J.P. Lewis and Ulrich Neuman, USC, Jan. 2003 (updated 2004)
  2. ^ "Microbenchmarking C++, C# and Java" by Thomas Bruckschlegel, Dr. Dobbs, June 17, 2005
  3. ^ "Java and Numerical Computing" by Ronald F. Boisvert, José Moreira, Michael Philippsen and Roldan Pozo, NIST, Dec 2000

関連項目

外部リンク