JavaとC++の比較
プログラミング言語の比較 |
---|
比較全般 |
基本文法 |
文字列演算 |
文字列関数 |
|
評価戦略 |
|
CとC++の互換性 |
CとPascalの比較 |
C++とJavaの比較 |
C#とJavaの比較 |
C#とVisual Basic .NETの比較 |
JavaとC++の比較(ジャバとシープラスプラスのひかく)では、JavaとC++の比較について説明する。
設計思想
C++とJavaとの違いは、それら言語の歴史から辿ることができる。
- C++はC言語、手続き型プログラミング言語に抽象データ型を実現し、静的型付けオブジェクト指向プログラミングを書きやすくするために作られ、C言語を継承し機械語の効率的な実行のための設計を維持しようとしている。
- Javaは当初、組み込みシステム上でネットワークコンピューティングに対応(support)するために作られた。Javaは移植性があり、セキュアであり、マルチスレッド対応であり、分散であり、そしてC++よりも単純(simple)になるように設計された。Javaの文法はCプログラマに馴染みやすいものが選ばれたが、Cとの直接的な互換性は維持されていない。
C++とJavaは開発の目的が異なるため、両者の方針とトレードオフに違いが生じている。
C++ Java Cとの後方(下位)互換性 Java以外の言語との互換性はない プログラマを当てにする プログラマを守る 低レベル機能に触れる オブジェクトを通してのみメモリアクセス 簡潔な表現 明確な表現 明示的な型破壊を許可 型安全性 マルチパラダイム(手続き型、オブジェクト指向、関数型、総称型) オブジェクト指向、総称型 演算子多重定義 演算子の効果は不変 限られた範囲の標準ライブラリ (GUI、ネットワーク、マルチスレッドを含む)機能豊富で容易に使用できる標準ライブラリ 組み込み,パーソナルコンピュータ 基幹業務システム(Webフロントエンド,Webバックエンド),携帯端末
これらの方針の違いの主な要因は、C++がC言語との互換性を維持しようとしたため、またC言語の利点(機械語に準ずる高速性など)を一切損なわないようにしたためである。
言語の特徴
文法
- Java文法はシンプルなLALRパーサによって解析できる文脈自由文法である。C++の構文解析は、それよりも複雑である。例えば、
Foo<1>(3);
は、Fooが変数であれば比較シーケンスであるが、Fooがクラスのテンプレート名であればオブジェクトを生成する。 - C++では名前空間レベルの定数、変数、関数が認められている。Javaでは、宣言はクラスやインタフェースの中に書かなければならない。
- C++の
const
は、「論理的に読み取り専用データである」ことを明示し、データ型にも適用される。Javaのfinal
は、「変数が再び割り当てられない」ことを示す。基本型にとっては(const int
とfinal 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では暗黙な変換としてネイティブ型の精度が大きくなる型変換(拡張変換)のみを許可する。他の変換は文法的に明示的なキャストを要求する。
- 関数の引数を渡すとき、C++は参照渡しと値渡し両方をサポートする。Javaではすべての引数は値渡しであるが、オブジェクト(非プリミティブ変数)の引数は参照になり、これは間接参照が言語に備わっていることを意味する。
- Javaのプリミティブ型は大きさと値の範囲が指定されている。一方、C++の型は最小限度が定められているものの、正確な大きさは定められておらず、環境によって異なる。また同じコンパイラでもバージョンが違えば型の大きさが異なることもあるし、コンパイラの設定で変更可能な場合もある。C++で値の範囲が指定されていないとは、仮に同じ16ビット整数だとしても、ある環境では2の補数で[-32768, 32767]、別の環境では1の補数で[-32767, 32767]の範囲になるという例があるということである。
- 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::vector と
java.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に特化したコードを生成したり、高い分岐予測精度を実現したりできる(ホットスポット)。
一般的に、Javaは、メモリ確保やファイルI/Oのような演算においてはC++より性能がよいが、算術演算や三角関数計算ではC++の方が優れた徴候を示す。[2] 数値演算について述べると、Javaは新しいバージョンで大きく進歩しているものの、浮動小数点数を様々なプラットフォームで再現するためのオーバーヘッド等により、未だにC++やFortranより遅い。[3]
脚注
- ^ "Performance of Java versus C++" by J.P. Lewis and Ulrich Neuman, USC, Jan. 2003 (updated 2004)
- ^ "Microbenchmarking C++, C# and Java" by Thomas Bruckschlegel, Dr. Dobbs, June 17, 2005
- ^ "Java and Numerical Computing" by Ronald F. Boisvert, José Moreira, Michael Philippsen and Roldan Pozo, NIST, Dec 2000
関連項目
外部リンク
- Object Oriented Memory Management: Java vs. C++
- How Java Differs from C — excerpt from Java in a Nutshell by David Flanagan
- Java vs. C++ resource management comparison - Comprehensive paper with examples
- Why Java Will Always Be Slower than C++ - "Java is high performance. By high performance we mean adequate. By adequate we mean slow." - Mr. Bunny