C SharpとJavaの比較
プログラミング言語の比較 |
---|
比較全般 |
基本文法 |
文字列演算 |
文字列関数 |
|
評価戦略 |
|
CとC++の互換性 |
CとPascalの比較 |
C++とJavaの比較 |
C#とJavaの比較 |
C#とVisual Basic .NETの比較 |
言語
オブジェクトの扱い
いずれの言語もC++のオブジェクト指向言語であり、その文法はC++に類似しているが、C++との互換性はない。メモリ再利用の手段として、従来の手動で解放する方法ではなくガベージコレクションを使用する。また、スレッド同期の手段を言語構文に組み込んでいる。
いずれの言語も強い参照と弱い参照の両方をもつ。Javaでは参照がガベージコレクタによって回収された時に通知を受けるリスナーを登録することができる。これはWeakHashMap
のパフォーマンスを考慮したものである。C#にはこれに相当する機能はなく、ファイナライザ(Javaにも存在する)を使用する方法しかない。その一方、C#は指定したオブジェクトのファイナライザ呼び出しをプログラマが抑止することができる。「世代」の概念をもつガベージコレクション(Javaと.NETいずれにも当てはまる)においてファイナライザの呼び出しはパフォーマンスに大きな影響を与えるため、またファイナライザはプログラマがオブジェクトを破棄しなかった場合のフェイルセーフであるため、これは非常に有効である。ファイナライザをもつオブジェクトは通常余分な世代が与えられ、回収されるまでに長くかかる。
C#は、一部の言語設計者から危険であるとされるポインタが制限つきながら利用できる。C#はポインタを使用するコードブロックあるいはメソッドをunsafe
キーワードで修飾することでこの懸念に対応している。これにより、このコードを利用する者はそれが他の部分に比べて危険であるということを知ることができる。コンパイラは、このようなコードをコンパイルする際には/unsafeスイッチを指定する必要がある。一般に、unsafeコードが使われるのはアンマネージAPIやシステムコール(これは本来「危険」なものである)との相互運用が必要な時、あるいはパフォーマンスの向上が必要な時のみである。
データ型
いずれの言語もプリミティブ型(C#では値型と呼ばれる)の概念をもつ。C#はそのような型をJavaよりも多くもち、符号つき整数だけでなく符号なし整数もサポートされる。さらに、十進の浮動小数を扱うためのdecimal
型をサポートする。Javaには符号なし整数が存在しない。文字列はいずれの言語においても不変(immutable)なオブジェクトとして扱われるが、特殊な構築方法として文字列リテラルを利用することができる。C#ではエスケープ文字を処理しないような文字列リテラル(verbatim文字列)をサポートする。
いずれの言語もプリミティブ型とオブジェクト型の間で変換するためにボックス化とボックス化解除が可能である。これによってプリミティブ型はオブジェクト型のサブセットとみなすことができる。C#においては、これによってプリミティブ型がメソッドをもつことが可能である(例えばobject
型のToString()
メソッドをオーバーライドすることができる)。Javaではこのような用途のためにプリミティブ型をラップするクラスが別に定義される。すなわち、42.ToString()
のようなインスタンス呼び出しでなくInteger.toString(42)
のような静的呼び出しが必要になる。もう一つの相違点として、Javaではジェネリックスにおいてこのような型を多用するため、暗黙的なボックス化解除が可能になっている(C#ではキャストが必要である)。このような変換はnullポインタ例外を発生する可能性があるが、Javaにおいてはそれがコード上で明白ではない。
C#では、struct
キーワードによって値型を定義することができる。プログラマの視点からは、これは軽量なクラスとみなせる。値型は通常のクラスと異なり、そして組み込みのプリミティブ型と同様、ヒープではなくスタックに置かれる。また、クラスの一部になることもでき(フィールドとして、あるいはボックス化された状態で)、配列の要素になることもできる。通常のクラスではメモリ上で間接的に参照される必要があるが、これらはその必要がない。しかし、値型には多数の制限がある。値型はnullの値をとることができなく、また初期化なしで配列の要素となる必要があることから、常に暗黙のデフォルトコンストラクタが定義される(これはメモリ領域をゼロで初期化する)。プログラマは一つ以上の引数をもつコンストラクタしか定義することができない。また、これは値型は仮想メソッドテーブルを持たないということを意味し、そのため継承関係をもつことができない(インターフェースの実装は可能である)。
C#の列挙型は組み込みの整数型をベースとしている。ベースとなる整数型のどの値も列挙型の値として有効になる(明示的なキャストは必要であるが)。このため、ビットフラグにおいてビットごとのOR演算で列挙型の値を組み合わせることが可能である。一方、Javaの列挙型はオブジェクトである。Javaの列挙型として有効な値は定義においてリストされたものだけである。列挙型の値を組み合わせるためには列挙セットクラスを使用する必要がある。Javaの列挙型では、値によって異なるメソッドの実装が可能である。JavaとC#はいずれも列挙型を文字列に変換することができるが、Javaにおいてはこの変換をカスタマイズすることができる。
配列
配列やコレクション型は、イテレータによるforeach
構文をはじめ、いずれの言語の構文においても重視されている。いずれの言語も配列に相当するArray
クラスをもつ。C#は真の多次元配列をサポートするが、Javaでは「配列の配列」として表現する必要がある(これはC#ではジャグ配列と呼ばれる)。ジャグ配列では参照の展開が次元の分だけ必要なのに対し、多次元配列ではそれが一回で済むため、これはパフォーマンスの向上に寄与する。またジャグ配列では各次元ごとに初期化のために明示的なループが必要なのに対し、多次元配列では一回のnew
演算子の使用で領域が確保できる点もパフォーマンスに影響する。
内部クラス
いずれの言語も内部クラス(クラスの内部に定義されたクラス)を定義できる。Javaでは、内部クラスからは外側のクラスの静的メンバ、非静的メンバいずれにもアクセスすることができる(内部クラスがstatic
として定義されていた場合は静的メンバのみ)。メソッドの内部にローカルクラスを定義することもでき、ローカル変数には読み込みアクセスのみすることができる。また、匿名ローカルクラスによってそのクラスのメソッドをオーバーライドしたクラスのインスタンスを作成することができる。
C#では、外部クラスの非静的メンバにアクセスするためには外部クラスインスタンスへの明示的な参照が必要になる。ローカルクラスは存在せず、その代わり、ローカル変数やメンバにアクセスすることのできる匿名デリゲートがサポートされる。
ジェネリクス
Javaではジェネリクスは型消去(type erasure)によって実装されている。これによってジェネリック型についての情報は実行時には失われ、リフレクションを通してのみ取得できるようになる。.NET 2.0では、ジェネリック型についての情報は完全に保存される。Javaはプリミティブ型に対するジェネリック型は定義できないが、C#では参照型・値型(プリミティブ型を含む)いずれに対してもジェネリック型を定義できる。Javaはその代わりにボックス化した型を使用することができる(List<int>
の代わりにList<Integer>
など)が、全ての値をヒープに確保し直す必要があるため、パフォーマンスコストが高い。JavaとC#はいずれも、参照型に特殊化されたジェネリック型は、型によらず共通のコードが実行される。しかし、C#において値型に特殊化された場合、CLRは型に最適化されたコードを動的に生成する。.NETにおいては、ジェネリック型に対する型安全性はコンパイル時にチェックされ、CLRにロードされる時に強制される。Javaにおいてはコンパイル時に部分的にチェックされるのみであり、Java VMは実行時にジェネリック型に関する情報を持たないため、キャスト操作を行う必要がある。
表記法と特殊な仕様
Javaはあるクラスの静的メソッド・静的フィールドを短い名前で使用するために静的インポート構文をもつ(別のクラスのfoo()
メソッドを静的インポートしてfoo(bar)
と記述できる)。C#は静的クラス(Javaの静的内部クラスとは異なる)の構文をもち、これによってクラスは静的メソッドのみを持つことができるようになる。C# 3.0では型に静的にメソッドを追加するための拡張メソッドが導入される予定である(foo
についての処理を行うbar()
拡張メソッドを追加し、foo.bar()
と記述できる)。
キーワード
キーワード | 仕様・使用例 |
---|---|
get、set | C#では、Javaにおけるアクセサメソッドへの代替としてプロパティを言語構文としてサポートする。 |
out、ref | C#では引数への出力あるいは参照をサポートする。これによって複数の返り値を得たり、値型を参照渡ししたりといったことが可能になる。 |
switch | C#では、Javaと異なりswitch構文でstring 型やlong 型を扱うことができる。
|
strictfp | Javaでは、異なるプラットフォーム間で実数演算結果が同じになるよう保証するstrictfpキーワードを利用できる。 |
checked、unchecked | C#では、checked ブロック内(あるいは式単体)では実行時に数値オーバーフローがチェックされる。
|
using | C#では、using キーワードによって、作成されたオブジェクトがブロックを抜ける際に確実に破棄されるよう強制することができる。
// "test.txt"というファイルを作成し、文字列を書き込み、(例え例外が発生しても)確実に閉じる。 using (StreamWriter file = new StreamWriter("test.txt")) { file.Write("test"); } |
goto | C#はgotoキーワードがサポートされる。これは便利な場面もあるが、通常はより構造化されたフロー制御方法が推奨される。C#において主にgoto キーワードが使われるのはswitch ステートメントにおいて異なるcase ラベルに移る時である。C#とJavaはいずれもbreak やcontinue の使用に制限があり、goto が必要になる場合がある。
switch(color) { case Color.Blue: Console.WriteLine("Color is blue"); break; case Color.DarkBlue: Console.WriteLine("Color is dark"); goto case Color.Blue; // ... } |
イベント処理
Javaでは、プログラマがObserverパターンを記述するために匿名内部クラスという糖衣構文が用意されている。これにより、コードのある点でクラス本体の定義とインスタンスの作成を同時に行うことができる。これはオブザーバを作成するためによく用いられる。
C#ではデリゲート型をはじめ、イベント処理をサポートするための機能が広範囲に渡って言語レベルでサポートされている。デリゲート型はメソッドへの型安全な参照であり、複数のものを結合してマルチキャスティングすることもできる。これらをサポートするため、イベントを定義するための構文や、イベントハンドラを登録・登録解除・結合をするための演算子が存在する。デリゲートは共変性と反変性(en:covariance and contravariance)をサポートし、完全なクロージャとしての性質をもつ匿名メソッドを作成することができる。
クロージャはJava SE 7の新しい機能として提案されている[1]。C#のデリゲートと同様、このクロージャはスコープ内の全ローカル変数に読み・書き両方のアクセスができる予定である(匿名内部クラスからはfinal
属性をもつ変数を読むことができるのみであった)。
数値処理
数学・金融分野のアプリケーションを十分にサポートするため、いくつかの言語仕様が存在する。Javaでは浮動小数点計算を強制するためにstrictfp
キーワードが存在する。これによってあらゆるプラットフォームで必ず同じ値が結果として得られることを保証することができる。C#にはこれに相当する機能はないが、厳密な浮動小数点計算のためにdecimal
型が存在する。これによって二進浮動小数点表現(float
、double
)に存在した問題が解決される。これら二進表現は十進数を正確に表現することができないため、丸め誤差が生じてしまっていた。金融分野のアプリケーションでは厳密な十進表現は必須である。Javaでは、このような用途のためにJava 5.0からBigDecimal
が導入された。BigDecimal
とBigInteger
は数を任意精度で表現できる。.NET Frameworkには現在このようなクラスは存在しないが、第三者による実装が存在し、また将来のバージョンでは導入される予定である[2]。
Javaでは、BigDecimal
や複素数型といったライブラリ定義の型をプリミティブ型と同じレベルで使用することは不可能である。一方、C#は次のような機能をサポートする。
- 演算子オーバーロードやインデクサ。
- 暗黙的または明示的な型変換。
int
がlong
に暗黙に変換できるのと同様の変換を定義できる。 - 値型と値型に対するジェネリック型。Javaではユーザ定義型はすべてヒープに確保される。これはパフォーマンスの点で大きなデメリットになる。
これらに加え、C#では数値処理アプリケーションのために、コード中のある領域の数値オーバーフローの実行時チェックを有効・無効にするためにchecked
・unchecked
キーワードが使用できる。また、「配列の配列」よりも高速に処理できる多次元配列もサポートする。
演算子オーバーロード
C#は表記法の多くの点でJavaよりも優れている。演算子オーバーロードやユーザ定義キャストなど、それらの多くはC++プログラマによって既に親しまれているものである。また、明示的メンバ実装(Explicit Member Implementation)が可能である。これによって、インターフェースメソッドの実装とクラス自身のメソッドの実装とを分離することができ、また同じ名前とシグネチャをもつメソッドが異なるインターフェースに存在した場合に、それらの実装を別々に行うことができる。
C#はインデクサ(C++のoperator[]
に相当)や、引数つきのget
/set
プロパティをサポートする。インデクサはthis[]
という名前をもち、一つ以上の引数(インデックス)をもつプロパティである。インデックスはあらゆる型をとることができる。
myList[4] = 5; string name = xmlNode.Attributes["name"]; orders = customerMap[theCustomer];
Javaは、乱用を防ぐため演算子オーバーロードをサポートせず、言語仕様をシンプルにしている。C#は論理的一貫性を保つための制限はあるものの演算子オーバーロードをサポートしており、注意深く使用すれば簡潔で可読性の高いコードを記述することができる。
メソッド
C#ではメソッドはデフォルトで非仮想的であり、必要ならば明示的にvirtual
と宣言しなければならない。Javaでは、非finalメソッドはデフォルトで仮想的である。仮想的であるということは、最も下位でオーバーライドされたメソッドが常に呼び出されるということである。しかし、仮想メソッドの呼び出しは通常インライン化できなく、また仮想メソッドテーブルを通した間接的な呼び出しが必要であるため、パフォーマンスコストが高い。Sunによるリファレンス実装を含む一部のJVM実装では、最もよく呼ばれる仮想メソッドをインライン化することによって実行時のオーバーヘッドを軽減している。
Javaでは、メソッドを非仮想的にする方法はない(final
修飾子でオーバーライドを禁止することはできる)。これは、派生クラスが同名の無関係なメソッドを再定義することが不可能であること意味する。これは基底クラスが別のプログラマによって書かれており、バージョン更新の際に、派生クラスに既に存在していたものと同じ名前・シグネチャのメソッドが追加されてしまった場合に問題になる。Javaでは、この場合どちらのプログラマの意図とも異なり、派生クラスのメソッドは暗黙的に基底クラスのオーバーライドになってしまう。このバージョン更新の問題を避けるため、C#では派生クラスで仮想メソッドをオーバーライドする際には明示的にそのように宣言する必要がある。メソッドが基底クラスのオーバーライドである場合、override
修飾子が指定されていなければならない。また、オーバーライドではなく無関係のメソッドを再定義したい場合、new
修飾子を指定しなければならない。
これらのバージョン更新の問題を部分的に解決するため、Java 5.0では@Override
アノテーションが導入された。しかし後方互換性のため、この指定は必須ではない。従って上記のように思いがけずオーバーライドしてしまうような問題を防ぐことはできない。しかし、基底クラスが同じシグネチャのメソッドをもち、正しくオーバーロードされていることを保証することはできる。
条件コンパイル
Javaとは異なり、C#ではプリプロセッサディレクティブを用いた条件コンパイルが実装されている。また、指定されたコンパイル定数が定義されている時のみ呼び出されるようConditional
属性を指定することができる。この方法によってDEBUG
定数が定義されている時のみ評価されるアサート(表明)機能(Debug.Assert()
)が提供されている。Java 1.4からは実行時に有効・無効が切り替えられるアサート機能が言語仕様として導入されている。