C#とJavaの比較(シーシャープとジャバのひかく)の記事では、プログラミング言語C#とJavaの比較について説明する。
言語 オブジェクトの扱い いずれの言語もクラスベースのオブジェクト指向言語であり、その文法はC++に類似しているが、C++との互換性はない。メモリ再利用の手段として、従来の手動で解放する方法ではなくガベージコレクション (GC) を使用する。また、スレッド同期の手段を言語構文に組み込んでいる。
また、いずれの言語もC++のデストラクタに相当する機能を持たないため、ガベージコレクションの管理対象外となるリソース(ファイルやネットワーク接続など)はプログラマが明示的に破棄する処理を記述する必要がある。例外安全に破棄を実行する手段として、try-finallyステートメントのほか、C#ではIDisposableインターフェイスとusingステートメントusing ステートメント - 破棄可能なオブジェクトが正しく使用されるようにする - C# | Microsoft Learn、Javaではとtry-with-resourcestry-with-resources 文 | Oracle Java SE 7 Documentationが用意されている。両言語にはファイナライザという機能も用意されているが、ファイナライザの動作タイミングは決定論的ではなく、プログラマがリソースを破棄し忘れた場合の最終防壁として用意されている手段であり、一般的に使われることはない。Java 9ではファイナライザが非推奨となっている。
Java、.NETいずれもガベージコレクションの実装形態は細かく規定されていないが、多くの実装でコピーGCとマーク&スイープGCを併用した世代別ガベージコレクションが使われている。.NET Frameworkは当初から世代別GCだったが、組み込み環境向けの.NET Compact Frameworkは世代別GCではなくマーク&スイープGCである.NETアプリを軽快にするためのガベージ・コレクション講座(2/4) - @IT。
いずれの言語も強い参照と弱い参照の両方をもつ。Javaでは参照がガベージコレクタによって回収された時に通知を受けるリスナーを登録することができる。これはの性能を考慮したものである。C#にはこれに相当する機能はなく、ファイナライザを使用する方法しかない。その一方、C#は指定したオブジェクトのファイナライザ呼び出しをプログラマが抑止することができる。「世代」の概念をもつガベージコレクションにおいてファイナライザの呼び出しは性能に大きな影響を与えるため、これは非常に有効である。ファイナライザをもつオブジェクトは通常余分な世代が与えられ、回収されるまでに長くかかる。
実行形態と安全性 基本的に実行時コンパイラにより中間表現(.NETの場合は共通中間言語、Javaの場合はJavaバイトコード)を機械語に変換しつつ仮想マシン上で実行させる形態をとっており、通常のプログラムコードの実行は実行環境(ランタイム)によってすべて管理される。.NETではこれをマネージコード(マネージドコード)と呼びマネージド コードとは - .NET | Microsoft Learn、C#のような言語をマネージ言語と呼ぶが、Javaについても同様にマネージ言語と呼ぶことがある。いずれもC言語やC++と比べて安全性が高く、例えばnullのデリファレンスや配列の範囲外アクセスなどが発生しても未定義動作とはならず、ランタイム例外として安全に処理される。
C#は、一部の言語設計者から危険であると指摘されるポインタを使用した演算が制限つきながら利用できる。C#はポインタを使用するメソッド、型あるいはコードブロックをキーワードで修飾することアンセーフ コード、データへのポインター、および関数ポインター - C# reference | Microsoft Learnでこの懸念に対応している。これにより、このコードを利用する者はそれが他の部分に比べて危険であるということを知ることができる。また、このようなコードをコンパイルする際にはコンパイラに対して/unsafeスイッチを指定する必要がある。一般に、unsafeコードが使われるのはアンマネージなAPIやシステムコールとの相互運用が必要なとき、あるいは性能の向上が必要なときのみである。
型システムとデータ型 Javaには大別して参照型(クラス型)とプリミティブ型(基本型)が存在する。参照型はクラスから派生するが、プリミティブ型はスーパークラスを持たない。
一方、C#には大別して参照型、値型、ポインタ型が存在する型 (C# リファレンス) | Microsoft Docs。このうちポインタ型を除き、あらゆる型はすべてSystem.Objectクラスから派生する。クラス (class) は参照型であり、数値型や論理型を含む構造体 (struct) および列挙型 (enum) は抽象クラスSystem.ValueTypeから暗黙的に派生する値型である値型 (C# リファレンス) | Microsoft Docs。
Javaの組み込み型 (built-in type) はプリミティブ型 (primitive data type) と呼ばれるPrimitive Data Types (The Java™ Tutorials > Learning the Java Language > Language Basics)。C#は組み込み型をJavaよりも多く持ち、すべての組み込み型はSystem名前空間に存在する型へのエイリアスであるデータ型 (C# と Java の比較) | Microsoft DocsBuilt-in types table (C# Reference) | Microsoft Docs。組み込み型には以下のようなものがある。 整数型: Javaは符号付き整数のみをサポートし、符号なし整数型が存在しない。C#では符号付き整数と符号なし整数の両方をサポートする。内部表現は2の補数であることが言語仕様で規定されている。 浮動小数点数型: いずれの言語もIEEE 754準拠の32bit単精度浮動小数点数と64bit倍精度浮動小数点数を持つ。 C#ではさらに、10の累乗の形で指数部を表す浮動小数点数であるdecimal型をサポートする。decimal型は値型かつ組み込み型であるが、CLRのプリミティブではない。 文字型: いずれの言語もUTF-16ベースの文字型charを持つ。整数型と相互に変換でき、その値は'\u0000' (0) から'\uFFFF' (65535) の範囲である。 ブーリアン型: いずれの言語もtrue/falseのいずれかを表すブーリアン型を持ち、Javaではboolean、C#ではboolである。 オブジェクト型: C#では組み込み型としてobject型を持つ。Javaのオブジェクト型は組み込み型ではない。 文字列型: C#では組み込み型としてstring型を持つ。Javaの文字列型は組み込み型ではない。 文字列はいずれの言語においても不変 () なオブジェクトとして扱われるが、特殊な構築方法として文字列リテラルを利用することができる。C#ではエスケープ文字を処理しないような逐語的文字列リテラル(文字列: ヒアドキュメントを参照)をサポートする。
C#言語自体にはプリミティブ型という用語は存在しないが、.NET Frameworkの共通言語ランタイム (CLR) では、System.Type.IsPrimitiveプロパティによって、型がCLRプリミティブ型であるかどうかを判定できるType.IsPrimitive Property (System) | Microsoft Docs。.NET言語組み込みの値型は必ずしもCLRプリミティブ型ではないが、CLRプリミティブ型はすべて値型である。
いずれの言語もプリミティブ型(もしくは値型)と参照型との間で変換するためにボックス化 (boxing) とボックス化解除 (unboxing) が可能である。これによってプリミティブ型(もしくは値型)は参照型のサブセットとみなすことができる。値型は仮想メソッドテーブルを持たず、したがってそのままでは多態性を利用できないが、C#においては、ボックス化によって値型で多態性を利用することが可能である(例えばobject型のToString()メソッドをオーバーライドすることができる)。C#では数値リテラルもオブジェクトであり、たとえば42.ToString()のようにint型のリテラルからメソッドを呼び出すことも可能である。Javaではこのような用途のためにプリミティブ型をラップするクラスが別に定義される。すなわち、42.toString()のようなインスタンスメソッド呼び出しでなくのような静的メソッド呼び出しが必要になる。もう一つの相違点として、Javaではジェネリクスにおいてこのような型を多用するため、暗黙的なボックス化解除が可能になっている(C#ではボックス化せずにジェネリクスを利用できる)。このような変換はnullポインタ例外を発生する可能性があるが、Javaではそれがコード上で明白ではない。
C#では、structキーワードによって構造体を定義することができる。構造体にはフィールドやメソッド、プロパティなどの任意のメンバーを定義できる。引数を持たないコンストラクタ(デフォルトコンストラクタ)をプログラマが定義することはできないが、一つ以上の引数を持つパラメータ化されたコンストラクタを定義することはできる。デフォルトコンストラクタはすべてのメンバーを各々の既定値(通例ゼロ相当の値)で初期化する。C# 10.0以降の構造体は、引数のないコンストラクタをユーザー定義することもできるようになったParameterless struct constructors - C# 10.0 draft specifications | Microsoft Learn。また構造体を定義する際、メンバーのメモリレイアウトを属性によって細かく制御することができるため、P/Invokeなどによるネイティブコードとの相互運用にも便利である。なお、構造体は継承元となる基底型を指定することはできず、派生型を定義することもできない。ただし任意のインターフェイスの実装は可能である。Javaには構造体が存在せず、ユーザー定義の値型を作成することはできない。
C#の列挙型は抽象クラスSystem.Enumから暗黙的に派生する値型の一種であり、組み込みの整数型をベースとしている。ベースとなる整数型のどの値も列挙型の値として有効になる(明示的なキャストは必要であるが)。このため、ビットフラグにおいてビットごとのOR演算で列挙型の値を組み合わせることが可能である。ただし構造体と違って任意のインターフェイスを実装することはできない。一方、Javaの列挙型は抽象クラスから暗黙的に派生する参照型である。Javaの列挙型として有効な値は定義においてリストされたものだけである。列挙型の値を組み合わせるためには列挙セットクラスを使用する必要がある。Javaの列挙型では、値によって異なるメソッドの実装が可能である。JavaとC#はいずれも列挙型を文字列に変換することができるが、Javaにおいてはこの変換をカスタマイズすることができる。また、Javaの列挙型は任意のインターフェイスを実装することができる。
プリミティブ型(あるいは値型)は参照型(クラス)とは異なり、インスタンスはヒープ領域ではなくスタックに置かれる。また、(フィールドとして、あるいはボックス化された状態で)クラスの一部になることも、配列の要素になることも可能である。クラスのインスタンスはメモリ上で間接的に参照される必要があるが、プリミティブ型(あるいは値型)はその必要がない。プログラマの視点からは、C#の値型は軽量なクラスとみなせる。しかし、プリミティブ型(あるいは値型)には前述のように多数の制限がある。値型は通常nullの値をとることができないが、.NET Framework 2.0でnull許容型 (System.Nullable) が導入され、C#でも擬似的にnull値を取り得る値型が利用可能となった。
型そのものをコード上で表現するメタデータ型(メタクラス)として、Javaではを、C#ではSystem.Typeを利用する。これらはいずれもリフレクションやイントロスペクションで重要な役割を果たす。
配列 C#およびJavaの配列は参照型であり、第一級オブジェクトである。C#の配列は共通の基本クラスSystem.Arrayから派生するが、Javaの配列はから派生する。代わりに、配列のユーティリティクラスとしてが用意されている。
いずれの言語も配列は共変性 (covariance) を持つ。
System.Console.WriteLine(typeof(int[]).BaseType); // System.Array object[] objs = new string; // 代入可能。 objs = "test"; objs = 10; // System.ArrayTypeMismatchException
System.out.println(int[].class.getSuperclass()); // java.lang.Object Object[] objs = new String; // 代入可能。 objs = "test"; objs = 10; // java.lang.ArrayStoreException
なお、Java 1.5で拡張for文がサポートされた。これはC#のforeach文に相当する。配列を始めとするコレクション型を、イテレータにより走査することができる。
C#は真の多次元配列(矩形配列)をサポートするが、Javaはサポートしない。C#およびJavaはともに「配列の配列」をサポートする(C#ではジャグ配列と呼ばれる)。
内部クラス いずれの言語もネストされた型(入れ子にされた型:クラスや構造体のブロックの中で定義されたクラス・構造体・列挙型・インターフェイス)をサポートする。Javaではインターフェイス内部にクラスや列挙型を定義することもできる。C# 7.xまではインターフェイス内部にクラス・構造体・列挙型・インターフェイスを定義することはできなかったが、C# 8.0以降は定義できるようになったDefault interface methods - C# feature specifications | Microsoft Learn。
Javaでは、ネストされたクラス (nested class) は既定で内部クラス (inner class) となる。内部クラスは外側のクラスのインスタンスを暗黙的にキャプチャすることで、静的メンバ、非静的メンバいずれにもアクセスすることができる。ネストされたクラスがstatic修飾されていた場合は静的メンバのみにアクセスできる。メソッドの内部にクラスを定義することもでき、これはローカルクラス (local class) と呼ばれる。ローカルクラスでは、外側のローカル変数には読み取りアクセスのみできる。また、型の名前を持たないローカルクラス(匿名クラス: anonymous class)を定義し、同時にインスタンス生成をすることもできる。
C#では、ネストされたクラス/構造体から外側のクラス/構造体の非静的メンバにアクセスするためには外側のクラス/構造体のインスタンスへの明示的な参照が必要になる。Javaの内部クラスやローカルクラスに相当する機能は存在しない。代わりに、C# 2.0以降では匿名メソッド (anonymous method) が、C# 3.0以降ではラムダ式が、そしてC# 7.0以降ではローカル関数がサポートされ、外側の変数をキャプチャするクロージャとして利用できる。なお、C# 3.0では限定的なローカルクラスとして、匿名型(読み取り専用プロパティのみを持つ、匿名のクラス型)がサポートされる。
ジェネリクス Javaではジェネリクスは型消去 () によって実装されている。これによってジェネリック型についての情報は実行時には失われ、リフレクションを通してのみ取得できるようになる。.NET 2.0では、ジェネリック型についての情報は完全に保存される。Javaはプリミティブ型に対するジェネリック型は定義できないが、C#では参照型・値型(プリミティブ型を含む)いずれに対してもジェネリック型を定義できる。Javaはその代わりにボックス化した型を使用することができる(List<int>の代わりにList<Integer>など)が、全ての値をヒープに確保し直す必要があるため、パフォーマンスコストが高い。JavaとC#はいずれも、参照型に特殊化されたジェネリック型は、型によらず共通のコードが実行される。しかし、C#において値型に特殊化された場合、CLRは型に最適化されたコードを動的に生成する。.NETにおいては、ジェネリック型に対する型安全性はコンパイル時にチェックされ、CLRにロードされる時に強制される。Javaにおいてはコンパイル時に部分的にチェックされるのみであり、Java VMは実行時にジェネリック型に関する情報を持たないため、キャスト操作を行う必要がある。
ジェネリクスの型制約 C#、Javaともに、ジェネリクスの型制約を持つ。
C#では基底型指定以外に、class(参照型)、class?(null許容またはnull非許容の参照型)、struct(値型)、new()(デフォルトコンストラクタあり)、notnull(null非許容型)、unmanaged(アンマネージド型)の各制約を指定できる。
// C# class ConstraintClass { } class SomeGenericClass<T> where T : ConstraintClass { } class OtherGenericClass<T> where T : new() { } class OtherGenericClass<T> where T : notnull { } class OtherGenericClass<T> where T : unmanaged { }
// Java class ConstraintClass { } class SomeGenericClass<T extends ConstraintClass> { }
ジェネリクスの共変性と反変性 C#、Javaともに、ジェネリクスの共変性と反変性 (covariance and contravariance) を持つ。ただしC#における共変性・反変性のサポートはバージョン4.0以降である。
C#では、型定義側で共変性outあるいは反変性inを指定し、利用側では変性の指定を行わない。なお、C#の値型は不変 (invariant) であり、共変性・反変性は適用されない。 // C# // System.Func<T, TResult> と同様のデリゲートを定義。 // 型定義側で変性の指定を行う。 public delegate TOut MyFunc<in TIn, out TOut>(TIn arg);
public class GenericsTest { public static void Main() { // 利用側では変性の指定を行わない。 MyFunc<object, string> func1 = x => "<" + x + ">"; MyFunc<string, object> func2 = func1; object ret = func2("test"); System.Console.WriteLine(ret); System.Console.WriteLine(ret.GetType()); // System.String
Javaでは、型定義側では変性の指定を行わず、利用側で共変性extendsあるいは反変性superを指定する。 Javaではさらに、上限と下限のいずれも指定しないワイルドカード?による型引数の指定が可能である。以下にJava 8の例を示す。 // Java // java.util.function.Function<T, R> と同様のインターフェイスを定義。 // 型定義側では変性の指定を行わない。 @FunctionalInterface interface MyFunc<TIn, TOut> { TOut apply(TIn arg); }
public class GenericsTest { public static void main(String[] args) { // 利用側で変性の指定を行う。 MyFunc<? super Object, ? extends String> func1 = x -> "<" + x + ">"; MyFunc<? super String, ? extends Object> func2 = func1; Object ret = func2.apply("test"); System.out.println(ret); System.out.println(ret.getClass().getName()); // java.lang.String
// 変性の上下限いずれも指定しないこともできる。 java.util.List<?> list;
型推論 C# 3.0でコンテキストキーワードvarによる限定された型推論が導入された。ローカル変数の宣言時に、型を右辺から推論できる。メソッド引数やフィールドには使えない。また、ラムダ式の戻り値および仮引数は型推論により決定される。ラムダ式の仮引数は型を省略することで型推論されるが、型推論が困難な場合には明示的に型を指定する。
Java 7で導入されたダイヤモンド演算子<>は宣言文の右辺ジェネリクスの型を省略できる程度のものでしかなかったが、Java 10ではC#同様に予約型名varによるローカル変数の型推論が導入された。Java 8で導入されたラムダ式では、仮引数の型を省略することで型推論されるが、さらにJava 11ではラムダ式の仮引数の型にvarを使用して型推論できるようになった。
表記法と特殊な仕様 Java 1.5で、あるクラスの静的メソッド・静的フィールドを短い名前で使用するためにstatic import構文が導入された。これにより、例えばClassAクラスのfoo()メソッドを静的インポートして、無関係のClassBクラスにてClassA.foo()のように型名で修飾することなく、foo()を直接使用できる。C# 6.0でも同様の機能としてstatic usingディレクティブがサポートされた。
C#は静的クラス(Javaの静的内部クラスとは異なる)の構文をもち、これによってクラスは静的メソッドのみを持つことができるようになる。C# 3.0から、型に静的にメソッドを追加するための拡張メソッドが導入されている(Foo型のインスタンスfooについての処理を行うBar()拡張メソッドを定義することで、あたかもFooにインスタンスメソッドが追加されたかのように、foo.Bar()と記述できる)。この機能は、クラスの外部で定義した静的メソッドを、そのクラスのインスタンスメソッドであるかのように見せかけて呼び出せるようにする糖衣構文であり、またターゲットとなるクラスのプライベートメンバにアクセスすることはできないため、カプセル化は維持される。
キーワード キーワード 仕様・使用例 get, set C#では、Javaにおけるアクセサメソッドへの代替としてプロパティを言語構文としてサポートする。 public class Person { private string _name; public string Name { // C# 6.0 以降の式形式プロパティ構文。 get => _name; set => _name = value;
} in, out, ref, ref readonly C#ではメソッド引数をまたはで修飾することで、引数への出力あるいは参照をサポートする。これによって複数の戻り値を得たり、参照渡ししたりするといったことが可能になる。 C# 7.0以降ではさらにメソッドの戻り値を修飾して参照とすることができ、修飾されたローカル変数でその参照を受け取ることができるC# の歴史 / §C# バージョン 7.0 | Microsoft Learn。
C# 7.2以降C# の歴史 / §C# バージョン 7.2 | Microsoft Learnでは、構造体型がマネージド メモリに直接アクセスし、常にスタック割り当てが必要であることを示すために、構造体の型定義を修飾することで「ref構造体()」を宣言することができる。C# 11以降では、さらにref構造体のフィールドに修飾した「refフィールド」を宣言することができる。
C# 7.2以降では、参照渡しではあるもののメソッド内での再代入による割り当て(構造体の場合はフィールドやプロパティの書き換えも含む)を禁止する引数修飾子がサポートされるようになった。引数を参照ローカル変数で受け取ったり参照戻り値で返したりする場合はで修飾するC# 7.2 の新機能 - C# によるプログラミング入門 | ++C++; // 未確認飛行 C。ただしでない構造体の場合は参照渡しとならず、暗黙的なコピーが発生する。C# 12以降では、読み取り専用ではあるものの左辺値のみを想定し、右辺値を想定しないことを示す(リテラルや一時変数のような右辺値を渡すと警告を出す)引数修飾子がサポートされるようになった参照渡し - C# によるプログラミング入門 | ++C++; // 未確認飛行 C。 switch C#ではswitch文で整数型、char型、bool型、列挙型、それらの型のNullable、およびstring型を使うことができる。C#ではフォールスルーを許可しない(caseラベルが連続している場合のみ許可される)。C# 7.0以降では、さらに「型パターン」により型ベースの分岐が可能である。 また、C# 8.0以降C# の歴史 / §C# バージョン 8.0 | Microsoft Learnでは、ステートメントではなく式としてパターンマッチの評価が可能な「switch式」も追加された。
enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday } // C# の switch 式。 var day = Day.Wednesday; int numLetters = day switch { Day.Monday or Day.Friday or Day.Sunday => 6, Day.Tuesday => 7, Day.Thursday or Day.Saturday => 8, Day.Wednesday => 9, _ => throw new InvalidOperationException($"Invalid day: {day}")
Javaでは///型およびそれらのプリミティブラッパークラスを使うことができるが、/型を使うことはできない。Java 1.5では列挙型を、Java 7ではString型を使えるようになったが、C#と違ってラベルにを使うことはできない。Javaではフォールスルーを許可する。
Java 14以降は「式」が追加されたが、C#の式とは構文はやや異なる。従来のラベルと新設の文を使った書き方も可能となっている。
enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY } // Java の switch 式。 var day = Day.WEDNESDAY;