JavaとC++の比較
JavaとC++の比較(ジャバとシープラスプラスのひかく)の記事では、JavaとC++の比較について説明する。
C++とJavaとの違いは、それら言語の歴史から辿ることができる。
C++はC言語の派生規格であり、手続き型プログラミング言語にクラス(抽象データ型)を導入し、静的型付けオブジェクト指向プログラミングを実現するために開発された。C言語の設計思想を維持・継承し、C言語の利点(機械語やアセンブラに準ずる高速性やハードウェア操作性など)を一切損なわないようにしているため、他のオブジェクト指向言語に比べてコードの実行効率や柔軟性を重視している反面、安全性は犠牲になっている。
Javaは当初、組み込みシステム上でネットワークコンピューティングに対応 (support) するために開発された。Javaは移植性があり、セキュアであり、マルチスレッド対応であり、分散であり、そしてC++よりも単純 (simple) になるように設計された。Javaの文法はC/C++プログラマに馴染みやすいものが選ばれたが、C/C++との直接的な互換性は維持されていない。
C++とJavaは開発の目的が異なるため、両者の方針とトレードオフに違いが生じている。
C言語とC++は相互運用性が確保されており、C++からCのライブラリ(関数群)を直接利用することが可能になっている。なお、2018年現在のC/C++最新規格はそれぞれC11およびC++17だが、双方に独自の追加仕様が重ねられており、C言語に対する完全な上位互換性はなくなっている。ただし、互いの差異を埋めて互換性を向上するための機能追加もなされている。
また、JavaはC/C++との互換性はないが、ScalaやKotlin、Groovyといった後発のJava仮想マシン (JVM) ベースのABI互換言語との間で相互運用が可能になっている。
Java文法はシンプルなLALRパーサによって解析できる文脈自由文法である。C++の構文解析は、それよりも複雑である。例えば、Foo<1>(3);は、Fooが変数であれば比較シーケンスであるが、Fooがクラスのテンプレート名であればオブジェクトを生成する。
C++では名前空間レベルの定数、変数、関数が認められている。Javaでは、宣言はクラスやインタフェースの中に書かなければならない。
C++のconstは、「論理的に読み取り専用データである」ことを示す。Javaのfinalは、「変数が再び割り当てられない」ことを示す。const intとfinal intなど、基本型にとってはこれらは概ね等価であるが、C++では変数および定数の宣言時初期化もしくは定義時初期化が必要であるのに対し、Javaでは初期化が一回だけ実行されるという条件が満たされればよい。また、C++のポインタ自身に対するconst修飾は、Javaの参照に対するfinal修飾と同じ意味を持つ。
C++11では、コンパイル時に値が決まる定数式 (constant expression) すなわちリテラルを表すconstexpr修飾子をサポートするようになった。Javaでは通例static finalフィールドで定数を定義する。
C++においてconst修飾されたメンバ関数は、関数内でメンバ変数を変更することができなくなる。Javaには相当機能はない。
Javaにおいてfinal修飾されたメソッドは派生クラスでオーバーライドできなくなる。また、final修飾されたクラスは派生クラスを定義できなくなる。なお、C++11ではJavaのようにメンバ関数のオーバーライドや派生クラスの定義を禁止するfinal修飾子が追加された。
C++はgoto文をサポートする。Javaはサポートしないが、ラベル付break文とラベル付continue文で、構造上ややgotoライクな機能を提供する。実際には、Javaは、コードを読みやすくするため、構造化制御フローを強要する。
C++はJavaが持たないやや低レベルな機能を提供する。C++には、特有のメモリ記憶位置や低レベルオペレーティングシステムコンポーネントを書くために必要なタスクを操るのに役立つポインタがある。同様にして、多くのC++コンパイラはインラインアセンブラをサポートする。Javaでは、そのようなコードは全て外部ライブラリに配置し、Java Native Interfaceを通してアクセスしなければならない。そのため、呼び出しのたびに、大きなオーバーヘッドが発生する。
C++はCとの互換性を保つため組込型の暗黙な型変換をある程度許可しているが、大抵のコンパイラでは警告を出す。また、複合型の暗黙型変換も定義できる。一方、Javaでは暗黙な変換としてネイティブ型の精度が大きくなる型変換(拡張変換)のみを許可する。他の変換は文法的に明示的なキャストを要求する。
この影響はJavaとC++双方にある条件式(if、whileとforの脱出条件)にも現れる。条件式の型は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]の範囲になるという例があるということである。C++11ではサイズと内部表現が規定された整数型(std::int32_tなど)がオプションとして追加された。
Javaの文字型charは、内部表現がUTF-16であることが規定されている。文字列を表現するクラスとして、java.lang.Stringが用意されている。一方、C++の文字型にはcharとwchar_t(ワイド文字)の2種類が存在し、それぞれの文字列クラスとしてstd::stringおよびstd::wstringが定義されているが、文字型の幅(ビット数)や内部表現はプラットフォームに依存する。規定されているのはcharのサイズが1バイト (sizeof(char) == 1) ということのみである。さらに、1バイトのビット数が規定されておらず、8ビット以上であればよい、という制約しかない。例えば、charがシングルバイト文字であったとしても、ASCIIであるとはかぎらないし、マルチバイト文字であったとしても、日本語環境でShift_JISあるいはEUC-JPが使われたり、中国語環境でGB 2312あるいはBig5が使われたり、もしくはUTF-8であったりしてもかまわない。また、wchar_tの内部表現は、UTF-16もしくはUTF-32であってもかまわないし、あるいはUnicodeと無関係の内部表現であってもかまわない。C++11ではUTF-16/UTF-32専用の文字型char16_t/char32_tと文字列クラスstd::u16string/std::u32stringが追加された。
Javaの文字列リテラルはString型のオブジェクトである。C++の文字列リテラルはヌル終端文字列をぴったり格納する固定長配列型である。
C++の浮動小数点数の丸め誤差と精度と演算はプラットフォームに依存する。Javaは異なるプラットフォームでの一貫した結果を保証する高精度浮動小数点モデルを提供しているが、通常は最適な浮動小数点演算性能を得るためにより大雑把な演算モードが使われる。
C++ではポインタを使ってメモリアドレスを直接操作できる。Javaはメモリアドレスを直接操作できるポインタを持っていない(オブジェクトの参照と配列参照だけはポインタを持っているが、どちらもメモリアドレスの直接アクセスを許可しない)。C++ではポインタへのポインタを定義できるが、Javaではオブジェクトアクセスにだけ参照を用いる。
どちらの言語も配列は固定の長さをもつ。C++の配列はメモリ空間上のオブジェクトの連続であり、配列自身をオブジェクトのように扱うことはできない(第一級オブジェクトではない)。通例、C言語のように配列先頭要素へのポインタを用いて配列の受け渡しを行うが、C++ではサイズの指定された固定長配列への参照を定義することもできる。配列範囲外アクセスはチェックされない。なお、構造体あるいはクラスでラップするか、C++11で追加された標準ライブラリに含まれるstd::arrayを用いることで、固定長配列を第一級オブジェクトとして扱うことができる。Javaの配列は第一級オブジェクトであるが、配列の一部分のみを参照するサブ配列は定義できず、インデックス範囲を別途指定する必要がある。また、配列範囲外アクセスのチェックが強制される。
C++では関数へのポインタ(あるいは関数への参照)によって、関数あるいはメンバ関数を間接参照することができる。また、ポインタによって関数オブジェクトを指す方法もある。Java 7以前では代替方法としてインタフェースを利用するしかなかったが、Java 8ではメソッド参照の機能が追加された。
C++ではプログラマが演算子を多重定義できる(利用者定義演算子)。Javaは演算子多重定義をサポートしない。文字列型java.lang.Stringに対してのみ、文字列連結が可能な演算子+と+=が予め用意されているだけである。
Javaはリフレクションや任意に新しいコードを動的ロードする機能をサポートする標準APIを持つ。
Javaは1.5以降でジェネリクスをサポートする。C++はテンプレートによりジェネリックプログラミングをサポートする。
JavaとC++はいずれも、ネイティブ型(これらも"基本型"または"組込型"として知られる)とユーザー定義型(これも"複合型"として知られる)を区別する。Javaでは、ネイティブ型は値としての意味しかなく、複合型は参照としての意味だけを持つ。C++では、すべての型が値としての意味を持つが、任意の型の参照を作ることが可能である。
C++は任意のクラスの多重継承をサポートする。また、状態の多重継承にまつわる問題を回避するための機構として、仮想継承をサポートする。Javaは型の多重継承をインタフェースによりサポートするが、実装は単一継承のみをサポートする。Javaでは、サブクラスはただ1つのスーパークラスからのみ派生することができるが、クラスは複数のインタフェースを実装することができる。なお、Java 8ではインタフェースのデフォルトメソッドにより、実装の多重継承をサポートするようになったが、状態の多重継承は依然としてサポートしない。
Javaはインタフェースとクラスを明確に区別している。C++では、純粋仮想関数(抽象メソッドにあたる)を並べたクラスでインタフェースを表現し、複数のインタフェースの実装は多重継承によって模倣される。
Javaはマルチスレッドをサポートした標準ライブラリと言語機能を持つ。synchronized Java予約語はマルチスレッドアプリケーションをサポートするシンプルでセキュアな相互排他ロック(Mutex)を提供するが、synchronized セクションはLIFOオーダーで残されなければならない。Java 1.5では並行プログラミングのためのライブラリが強化され、java.util.concurrent.Semaphoreクラスをはじめとする、柔軟なMutexロックメカニズムを提供するための明示的な同期オブジェクトなどもサポートするようになった。一方、C++では長らくスレッドが標準化されていなかったため、プラットフォームごとのスレッド機能を使うか、Boost C++ライブラリなどを利用する必要があったが、C++11でスレッドが標準化された。
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では非メモリリソースをリークしないように注意深く解放コードを逐一書く必要がある(ただしJava 7のtry-with-resources文により、ある程度緩和されている)。一方で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およびC++11はJavaと同じように除算の仕様を定義しており、すべてのaとb (b != 0) で(a/b)*b + (a%b) == aを保証する。古いC/C++規格にこの保証がない理由は、仮にCPUの除算命令がこのような定義でなかったとしても、C/C++の除算を直接CPUの除算命令にコンパイルできるようにするためである。ただし、古いC/C++でも標準ライブラリのdiv()関数 (/<cstdlib>) を用いれば常に-3 / 2の商として-1という結果を得られる。
このセクションでは、WindowsやLinuxのような一般的なOSでのC++とJava相互の演算パフォーマンスを比較する。
Javaの初期バージョンは、C++のような静的コンパイルされる言語に比べ著しく性能が低かった。これは、C++ではソースコードはハードウェアが直接に解釈できる機械語にコンパイルされるのに対し、Javaでは共通の(ハードウェアに依存しない)仮想機械語であるJavaバイトコードにコンパイルされ、それをJava仮想マシンがインタプリタ的に実行していたからである。例として、
のように、ソースコードレベルでは同様な命令でも、Javaバイトコードの方がネイティブな機械語よりも長くなる傾向にある (C++によるコードでは、3行目でロード、インクリメント、保存が1命令で行われており、短く済んでいる)。
しかし、Javaは長期稼働するサーバやデスクトップのためにジャストインタイム(JIT) コンパイラテクノロジを発展させ、それがC++との性能差を縮めるであろうと言われている。JITコンパイルとは、Javaバイトコードをインタプリタ的に実行するのではなく、実行時にネイティブな機械語にコンパイルしてから実行する方式である。
以下は、JavaはC++よりも高速であるという研究の主張の一部である。
CおよびC++では、「なんでも指せる」というポインタの性質が最適化を難しくしている(ただしこの問題はC99のrestrictキーワードによって回避できる)。
Javaでは新たに確保されたメモリがガベージコレクションによって物理的に連続した領域に集められるので、アクセスする際にキャッシュミスが起こりにくい。
実行時コンパイルはそれがどのプロセッサの上で実行されるか、どのコードを実行するかが解っているため、各CPUに特化したコードを生成したり、高い分岐予測精度を実現したりできる(ホットスポット)。
一般的に、Javaは、メモリ確保やファイルI/Oのような演算においてはC++より性能がよいが、算術演算や三角関数計算ではC++の方が優れた徴候を示す。 数値演算について述べると、Javaは新しいバージョンで大きく進歩しているものの、浮動小数点数を様々なプラットフォームで再現するためのオーバーヘッド等により、未だにC++やFortranより遅い。