C SharpとJavaの比較

C#とJavaの比較（シーシャープとジャバのひかく）の記事では、プログラミング言語C#とJavaの比較について説明する。

 言語 

 オブジェクトの扱い 
いずれの言語もクラスベースのオブジェクト指向言語であり、その文法はC++に類似しているが、C++との互換性はない。メモリ再利用の手段として、従来の手動で解放する方法ではなくガベージコレクション (GC) を使用する。また、スレッド同期の手段を言語構文に組み込んでいる。

また、いずれの言語もC++のデストラクタに相当する機能を持たないため、ガベージコレクションの管理対象外となるリソース（ファイルやネットワーク接続など）はプログラマが明示的に破棄する処理を記述する必要がある。例外安全に破棄を実行する手段として、try-finallyステートメントのほか、C#ではIDisposableインターフェイスとusingステートメント、Javaではとtry-with-resourcesが用意されている。両言語にはファイナライザという機能も用意されているが、ファイナライザの動作タイミングは決定論的ではなく、プログラマがリソースを破棄し忘れた場合の最終防壁として用意されている手段であり、一般的に使われることはない。Java 9ではファイナライザが非推奨となっている。

Java、.NETいずれもガベージコレクションの実装形態は細かく規定されていないが、多くの実装でコピーGCとマーク＆スイープGCを併用した世代別ガベージコレクションが使われている。.NET Frameworkは当初から世代別GCだったが、組み込み環境向けの.NET Compact Frameworkは世代別GCではなくマーク＆スイープGCである。

いずれの言語も強い参照と弱い参照の両方をもつ。Javaでは参照がガベージコレクタによって回収された時に通知を受けるリスナーを登録することができる。これはの性能を考慮したものである。C#にはこれに相当する機能はなく、ファイナライザを使用する方法しかない。その一方、C#は指定したオブジェクトのファイナライザ呼び出しをプログラマが抑止することができる。「世代」の概念をもつガベージコレクションにおいてファイナライザの呼び出しは性能に大きな影響を与えるため、これは非常に有効である。ファイナライザをもつオブジェクトは通常余分な世代が与えられ、回収されるまでに長くかかる。

 実行形態と安全性 
基本的に実行時コンパイラにより中間表現（.NETの場合は共通中間言語、Javaの場合はJavaバイトコード）を機械語に変換しつつ仮想機械上で実行させる形態をとっており、通常のプログラムコードの実行は実行環境（ランタイム）によってすべて管理される。.NETではこれをマネージコード（マネージドコード）と呼び、C#のような言語をマネージ言語と呼ぶが、Javaについても同様にマネージ言語と呼ぶことがある。いずれもC言語やC++と比べて安全性が高く、例えばnullのデリファレンスや配列の範囲外アクセスなどが発生しても未定義動作とはならず、ランタイム例外として安全に処理される。

C#は、一部の言語設計者から危険であると指摘されるポインタを使用した演算が制限つきながら利用できる。C#はポインタを使用するメソッド、型あるいはコードブロックをキーワードで修飾することでこの懸念に対応している。これにより、このコードを利用する者はそれが他の部分に比べて危険であるということを知ることができる。また、このようなコードをコンパイルする際にはコンパイラに対して/unsafeスイッチを指定する必要がある。一般に、unsafeコードが使われるのはアンマネージなAPIやシステムコールとの相互運用が必要なとき、あるいは性能の向上が必要なときのみである。

 型システムとデータ型 
Javaには大別して参照型（クラス型）とプリミティブ型（基本型）が存在する。参照型はクラスから派生するが、プリミティブ型はスーパークラスを持たない。

一方、C#には大別して参照型、値型、ポインタ型が存在する。このうちポインタ型を除き、あらゆる型はすべてSystem.Objectクラスから派生する。クラス (class) は参照型であり、数値型や論理型を含む構造体 (struct) および列挙型 (enum) は抽象クラスSystem.ValueTypeから暗黙的に派生する値型である。

Javaの組み込み型 (built-in type) はプリミティブ型 (primitive data type) と呼ばれる。C#は組み込み型をJavaよりも多く持ち、すべての組み込み型はSystem名前空間に存在する型へのエイリアスである。組み込み型には以下のようなものがある。
 整数型: Javaは符号付き整数のみをサポートし、符号なし整数型が存在しない。C#では符号付き整数と符号なし整数の両方をサポートする。内部表現は2の補数であることが言語仕様で規定されている。
 浮動小数点数型: いずれの言語もIEEE 754準拠の32bit単精度浮動小数点数と64bit倍精度浮動小数点数を持つ。 
 C#ではさらに、10の累乗の形で指数部を表す浮動小数点数であるdecimal型をサポートする。decimal型は値型かつ組み込み型であるが、CLRのプリミティブではない。
 文字型: いずれの言語もUTF-16ベースの文字型charを持つ。整数型と相互に変換でき、その値は'\u0000' から'\uFFFF' の範囲である。
 ブーリアン型: いずれの言語もtrue/falseのいずれかを表すブーリアン型を持ち、Javaではboolean、C#ではboolである。
 オブジェクト型: C#では組み込み型としてobject型を持つ。Javaのオブジェクト型は組み込み型ではない。
 文字列型: C#では組み込み型としてstring型を持つ。Javaの文字列型は組み込み型ではない。
 文字列はいずれの言語においても不変 () なオブジェクトとして扱われるが、特殊な構築方法として文字列リテラルを利用することができる。C#ではエスケープ文字を処理しないような逐語的文字列リテラル（文字列: ヒアドキュメントを参照）をサポートする。

C#言語自体にはプリミティブ型という用語は存在しないが、.NET Frameworkの共通言語ランタイム (CLR) では、System.Type.IsPrimitiveプロパティによって、型がCLRプリミティブ型であるかどうかを判定できる。.NET言語組み込みの値型は必ずしもCLRプリミティブ型ではないが、CLRプリミティブ型はすべて値型である。

いずれの言語もプリミティブ型（もしくは値型）と参照型との間で変換するためにボックス化 (boxing) とボックス化解除 (unboxing) が可能である。これによってプリミティブ型（もしくは値型）は参照型のサブセットとみなすことができる。値型は仮想メソッドテーブルを持たず、したがってそのままでは多態性を利用できないが、C#においては、ボックス化によって値型で多態性を利用することが可能である（例えばobject型のToString()メソッドをオーバーライドすることができる）。C#では数値リテラルもオブジェクトであり、たとえば42.ToString()のようにint型のリテラルからメソッドを呼び出すことも可能である。Javaではこのような用途のためにプリミティブ型をラップするクラスが別に定義される。すなわち、42.toString()のようなインスタンスメソッド呼び出しでなくのような静的メソッド呼び出しが必要になる。もう一つの相違点として、Javaではジェネリクスにおいてこのような型を多用するため、暗黙的なボックス化解除が可能になっている（C#ではボックス化せずにジェネリクスを利用できる）。このような変換はnullポインタ例外を発生する可能性があるが、Javaではそれがコード上で明白ではない。

C#では、structキーワードによって構造体を定義することができる。構造体にはフィールドやメソッド、プロパティなどの任意のメンバーを定義できる。引数を持たないコンストラクタ（デフォルトコンストラクタ）をプログラマが定義することはできないが、一つ以上の引数を持つパラメータ化されたコンストラクタを定義することはできる。デフォルトコンストラクタはすべてのメンバーを各々の既定値（通例ゼロ相当の値）で初期化する。C# 10.0以降の構造体は、引数のないコンストラクタをユーザー定義することもできるようになった。また構造体を定義する際、メンバーのメモリレイアウトを属性によって細かく制御することができるため、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以降は定義できるようになった。

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# 7.2以降では、構造体型がマネージド メモリに直接アクセスし、常にスタック割り当てが必要であることを示すために、構造体の型定義を修飾することで「ref構造体（）」を宣言することができる。C# 11以降では、さらにref構造体のフィールドに修飾した「refフィールド」を宣言することができる。

C# 7.2以降では、参照渡しではあるもののメソッド内での再代入による割り当て（構造体の場合はフィールドやプロパティの書き換えも含む）を禁止する引数修飾子がサポートされるようになった。引数を参照ローカル変数で受け取ったり参照戻り値で返したりする場合はで修飾する。ただしでない構造体の場合は参照渡しとならず、暗黙的なコピーが発生する。C# 12以降では、読み取り専用ではあるものの左辺値のみを想定し、右辺値を想定しないことを示す（リテラルや一時変数のような右辺値を渡すと警告を出す）引数修飾子がサポートされるようになった。 switch C#ではswitch文で整数型、char型、bool型、列挙型、それらの型のNullable、およびstring型を使うことができる。C#ではフォールスルーを許可しない（caseラベルが連続している場合のみ許可される）。C# 7.0以降では、さらに「型パターン」により型ベースの分岐が可能である。
また、C# 8.0以降では、ステートメントではなく式としてパターンマッチの評価が可能な「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;
int numLetters = switch (day) {
 case MONDAY, FRIDAY, SUNDAY -> 6;
 case TUESDAY -> 7;
 case THURSDAY, SATURDAY -> 8;
 case WEDNESDAY -> 9;
 default -> throw new IllegalStateException("Invalid day: " + day);
}; strictfp Java SE 1.2からJava SE 16までは、異なるプラットフォーム間で浮動小数点数の演算結果が同じになるよう保証するキーワードを利用できた。 checked, unchecked C#では、checkedブロック内（あるいは式単体）では実行時に整数型の算術オーバーフローがチェックされる。 using, try C#では、usingステートメントによって、作成されたオブジェクトがブロックを抜ける際に確実に破棄されるよう強制することができる。C# 8.0以降はブロックの明示的な記述を必要としないusing宣言もサポートする。
// "test.txt"というファイルを作成し、文字列を書き込み、（例え例外が発生しても）確実に閉じる。
using (StreamWriter file = new StreamWriter("test.txt")) {
 file.Write("test");
}
Java 7では、try-with-resources構文で同様のリソース解放に対応している。 goto C#ではgoto文がサポートされる。これは便利な場面もあるが、通常はより構造化されたフロー制御方法が推奨される。C#ではswitchステートメントにおいてgotoキーワードを使うことで、異なるcaseラベルに移ることができる。
Console.Write("Color is ");
switch (color) {
 case Color.Blue: Console.Write("blue"); break;
 case Color.DarkBlue: Console.Write("dark "); goto case Color.Blue;
 case Color.LightBlue: Console.Write("light "); goto case Color.Blue;
 // ...
 default: Console.Write("unknown"); break;
}
Console.WriteLine();
Javaではgoto文はサポートされない。ただしgotoは予約されており、識別子として利用できない。代わりに制限されたgotoとして、ラベル付きbreak文/continue文を使うことができる。これにより多重ループの脱出などを簡潔に記述できる。 yield C# 2.0以降ではコンテキストキーワードyieldが用意されており、yield return文とyield break文を使用してイテレータ（コルーチン）を記述できる。これらの文を含むメソッド（イテレータ）の戻り値はIEnumerableインターフェイス、またはIEnumeratorインターフェイスでなければならない。
また、イテレータは、内部的に状態を持つステートマシンとしてコンパイルされ、値を部分的に返却する列挙子を提供するように振る舞う。
// 指定された個数の乱数を生成する
public IEnumerable<int> RandomValueGenerator(int count) {
 var r = new Random();
 for (int index = 0; index < count; ++index) {
 yield return r.Next();
 }
}
.NET Core 3.0およびC# 8.0以降では、IAsyncEnumerableインターフェイスを返す非同期メソッドの中で文を使用でき、文によって非同期ストリームを処理できる。

Javaには言語構文レベルでのコルーチンのサポートはない。前述のように、Java 14で追加された文は、式における糖衣構文で使用されるものにとどまっている。 async, await C# 5.0以降ではコンテキストキーワード/を用いて非同期処理を同期処理風に記述することが可能である。 unsafe, fixed C#ではキーワードを用いることで、ポインタや関数ポインタ、固定サイズバッファを利用可能なコンテキストを宣言することができる。コンテキスト内ではステートメントを使うことで、ガベージコレクタのメモリコンパクションによる変数の再配置を抑制し、オブジェクトのアドレスを固定（ピン留め）してポインタを取得することもできる。

 イベント処理 
Javaでは、Observer パターンの記述を容易にするために匿名クラスという糖衣構文が用意されている。匿名クラス構文により、クラス本体の定義とインスタンスの生成を同時に行なうことができ、また定義と生成がひとつの式として扱えるため、インターフェイス実装クラスのインスタンスを一度限りしか生成しない場面、例えば特定のインターフェイス実装（あるいは特定のスーパークラスのメソッドのオーバーライド）を要求するイベントハンドラーの設定時などによく用いられる。Java 8ではラムダ式がサポートされ、匿名クラスの代わりに使うこともできる。Java 7まではC#のデリゲートに相当するものは存在しなかったが、Java 8では類似機能としてメソッド参照がサポートされるようになった。

C#ではデリゲート (delegate) 型をはじめ、イベント処理をサポートするための機能が広範囲に渡って言語レベルでサポートされている。デリゲート型はメソッドへの型安全な参照であり、複数のデリゲートを結合してマルチキャスティングすることもできる。デリゲートを結合／分離するための+, -演算子、イベントハンドラーをカプセル化するためのイベント構文 (event, add/remove) や、イベントハンドラーを登録／削除するための+=, -=演算子が用意されている。デリゲートは共変性と反変性 () をサポートし、完全なクロージャとしての性質をもつ匿名関数（匿名メソッドおよびラムダ式）を作成することができる。

 非同期処理 
JavaおよびC#はともに標準ライブラリでスレッドをサポートしている。OSのスレッド（カーネルレベルのスレッド）に対する薄い抽象化を提供するクラスとして、Javaには、C# (.NET) にはSystem.Threading.Threadが用意されている。そのほか、セマフォなどの同期オブジェクトやアトミック演算なども標準ライブラリでサポートされている。

言語組み込みの同期構文として、Javaにはブロックおよびメソッドが用意されている。C#にはステートメントが用意されている。

C# 5.0以降は/コンテキストキーワードによる非同期メソッド構文がサポートされた。このasync/await構文はFuture パターンをさらに発展させたもので、イテレータおよび.NET 4以降で追加されたタスク並列ライブラリ (Task Parallel Library; TPL) を実行基盤とする糖衣構文であり、非同期タスクの完了待機と実行結果の取得、そして後続処理（継続）をあたかも同期メソッドのように記述することができる。このasync/await構文はMicrosoft Visual Studio 2012と同時期にリリースされたVisual Basic .NET 11にも搭載された。その後、さまざまな言語で同様の構文がサポートされるようになった。

Java 1.5以降はによるライブラリベースのFutureパターンをサポートするものの、async/await構文はサポートされていない。

 数値処理 
数学・科学計算・金融分野のアプリケーション開発に十分対応するための言語仕様が、それぞれに存在する。

Java SE 1.2からJava SE 16までは、IEEE 754に準拠した厳密な浮動小数点計算を強制するためのキーワードが存在した。これによってあらゆるプラットフォームで必ず同じ値が結果として得られることを保証することができた。1990年代後半の当時、この機能が必要とされた背景は、もともとIntelのx86アーキテクチャにおけるx87浮動小数点命令セットの残念な特性に起因していたが、x64アーキテクチャではスカラー浮動小数点演算にもSSE/SSE2命令を使用するようになったため、厳密な浮動小数点セマンティクスを余分なオーバーヘッドなしに自然にサポートできるようになった。Java SE 17以降は一貫して厳密な浮動小数点セマンティクスが必須となり、意味を失ったの機能は廃止された。詳細はの記事も参照のこと。C#にはこれに相当する機能はない。

単精度浮動小数点数floatや倍精度浮動小数点数doubleといった二進表現の浮動小数点数は、多くの環境でCPU命令によるハードウェアサポートがあるため高速な処理に適しているが、「0.1」のような十進数の値を正確に表現することができないため、丸め誤差が生じてしまう。金融分野のアプリケーションソフトウェアでは、厳密な十進表現のサポートは必須である。C#には、十進数の厳密な浮動小数点計算に適した128ビットの組み込み型が存在する。この型はSystem.Decimal構造体のエイリアスであり、速度性能および表現可能な値の範囲はやに劣るものの、有効桁数が28桁と大きく、有効桁数の範囲内であれば十進数データの誤差が存在しないため、二進浮動小数点表現に存在する問題の多くが解決される。Javaでは、このような用途のためにが導入されている。また、Java 1.1では巨大な整数を扱うためのクラス型としてが導入されている。BigDecimalとBigIntegerは最大で約21億桁程度までの数を任意精度で表現できる。C#においては、.NET Framework 4よりSystem.Numerics.BigInteger型が導入されている。

Javaでは、BigDecimalや複素数型といったライブラリ定義の型をプリミティブ型と同じレベルで使用することは不可能である。また、Javaではユーザー定義型はすべて参照型扱いとなり、メモリブロックの実体はヒープに確保される。一方、C#は次のような機能をサポートする。
 演算子多重定義やインデクサ。
 暗黙的または明示的な型変換。C++のキャスト演算子オーバーロードに似た、ユーザー定義の型変換を定義できる。これにより、例えばプリミティブ数値型同士の変換と類似の機能を提供できる。
 値型と値型に対するジェネリック型。値型はスタックに確保される。これは実行時の性能面に影響を及ぼす。
これらに加え、C#では数値処理アプリケーションのために、コード中の特定領域において整数型の算術演算および変換に対する算術オーバーフローの実行時チェックの有効・無効を制御するchecked/uncheckedキーワードを使用できる。

 演算子多重定義 
Javaは言語仕様をシンプルに保つため、また乱用による難読化を防ぐため、ユーザー定義の演算子オーバーロードをサポートしていない。一方でコードの書きやすさおよび直感性は犠牲になっている。
var myList = new java.util.ArrayList<Integer>(java.util.Collections.nCopies(10, 0));
myList.set(4, -100);
var gravityTable = new java.util.HashMap<String, Double>();
gravityTable.put("Mercury", 0.377);
gravityTable.put("Venus", 0.9);
gravityTable.put("Earth", 1.0);
double gravityOnVenus = gravityTable.get("Venus");

var b1 = new java.math.BigInteger(Long.toString(Long.MAX_VALUE));
var b2 = new java.math.BigInteger(Integer.toString);
var b3 = b1.add(b2);

C#は表記法の多くの点でJavaよりも多機能である。演算子多重定義やユーザー定義キャストなど、それらの多くはC++プログラマによって既に親しまれているものである。

C#はインデクサ（C++のoperator[]に相当）をサポートする。インデクサの定義構文はプロパティと似ているが、this[]という名前をもち、一つ以上の引数（インデックス）を持つ点が異なる。インデクサを定義する際、インデックスには任意の型を使用することができる。
var myList = new System.Collections.Generic.List<int>(new int);
myList = -100;
var gravityTable = new System.Collections.Generic.Dictionary<string, double>();
gravityTable["Mercury"] = 0.377;
gravityTable["Venus"] = 0.9;
gravityTable["Earth"] = 1.0;
double gravityOnVenus = gravityTable["Venus"];

C#は（論理的一貫性を保つための制限はあるものの）ユーザー定義の演算子オーバーロードをサポートしており、注意深く使用すれば簡潔で可読性の高いコードを記述することができる。

var b1 = new System.Numerics.BigInteger(long.MaxValue);
var b2 = new System.Numerics.BigInteger;
var b3 = b1 + b2;

また、C#では明示的メンバ実装 (Explicit Member Implementation) が可能である。これによって、インターフェイスメソッドの実装とクラス自身のメソッドの実装とを分離することができ、また同じシグネチャ（名前および引数の数と型の順序）を持つメソッドが異なるインターフェイスにそれぞれ存在した場合に、インターフェイス名を指定して区別することでそれらの実装を別々に行うことができる。

 メソッド 
C#のメソッドはデフォルトで非仮想（非）であり、仮想メソッドにする必要がある場合は明示的にvirtualと宣言しなければならない。Javaのメソッドは常に仮想 (C#でvirtual指定された状態) であり、非仮想にすることはできないが、final修飾子を指定することでオーバーライドを禁止することはできる。

Javaでは、メソッドを非仮想的にする方法はない。これは、派生クラスが同名の無関係なメソッドを再定義することが不可能であることを意味する。これは基底クラスが別のプログラマによって書かれており、バージョン更新の際に、派生クラスに既に存在していたものと同じシグネチャのメソッドが追加されてしまった場合に問題になる。Javaでは、この場合どちらのプログラマの意図とも異なり、派生クラスのメソッドは暗黙的に基底クラスのオーバーライドになってしまう（基底クラスでfinal宣言されていた場合はコンパイルエラーになってしまう）。

これらのバージョン更新の問題を部分的に解決するため、Java SE 5.0ではアノテーションが導入された。これにより、基底クラスが同一シグネチャのメソッドを持ち、それが派生クラスで正しくオーバーライドされていることを保証することはできる。しかし後方互換性維持のため、この指定は必須ではない。従ってIDEやツールなしに上記のように思いがけずオーバーライドしてしまうような問題を防ぐことはできない。

C#では、派生クラスで仮想メソッドをオーバーライドする際には、明示的にその旨を宣言する必要がある。メソッドが基底クラスのオーバーライドである場合、override修飾子が指定されていなければならない。また、オーバーライドではなく同名の無関係なメソッドを派生クラスで再定義することができるが、コンパイラが警告を発する。コンパイラ警告を抑制したい場合、new修飾子を指定しなければならない。

 メソッド種別 C# Java 非仮想 (指定なし) 仮想 virtual (指定なし) 抽象 abstract abstract オーバーライド override @Override 継承先でのオーバーライド禁止 (指定なし：非仮想) final オーバーライド かつ 継承先でのオーバーライド禁止 sealed override @Override final 隠蔽 new 

 プリプロセッサ 
JavaはC/C++でしばしば問題を引き起こしていたプリプロセッサを採用しなかった。一方でC#は限定的にプリプロセッサディレクティブを使用可能である。

 条件付きコンパイル 
Javaではプリプロセッサがサポートされないため、コンパイル時の分岐は不可能となっている。

一方、C#ではプリプロセッサディレクティブを用いた条件付きコンパイル (条件コンパイル、) が実装されている。また、指定されたコンパイル定数が定義されている時のみ呼び出されるようConditional属性 (System.Diagnostics.ConditionalAttribute) をメソッドに指定することができる。この方法によって、DEBUG定数が定義されている時のみ評価される表明（アサート）機能 (System.Diagnostics.Debug.Assert()メソッド) が提供されている。

Java 1.4からは実行時に有効・無効を切り替えられるassert文が言語仕様として導入されている。

 名前空間とソースファイル 
C#の名前空間はC++のそれと類似している。Javaのパッケージとは異なり、名前空間はソースファイルの物理的位置とは無関係である。Javaではパッケージ構造とソースファイルの位置が必ずしも一致している必要はないものの、デフォルトではそのような振る舞いをする。

Javaではアクセスレベルがpublicのクラス名はソースファイル名 (*.java) と一致していなければならない。つまり1つのソースファイルにpublicクラスは1つだけしか定義できない。デフォルトのアクセスレベルのクラスであれば、1つのソースファイルに複数のクラスをまとめて記述することができる。

C#ではそのような制約はなく、クラス名と無関係の1つのソースファイル (*.cs) に任意のクラスを複数記述することができる。C# 2.0からはpartialキーワードによってクラスの定義を複数のファイルに分けて記述することが可能になった。

 例外処理 
Javaは非チェック例外 () に加えてチェック済み例外 () をサポートする。C#では非チェック例外のみである。チェック済み例外は、プログラマがメソッドから発生し得るものを全て宣言し、捕捉する必要がある。

全てのエラーが処理されることを保証できるため、チェック済み例外は非常に便利だとする者もいる。一方、C#の設計者であるアンダース・ヘルスバーグのように、Javaのチェック済み例外はある程度実験的な仕様であり、小さなプログラムでの例を除いては実装する価値を見出せなかった、とする者もいる。一つの批判として、チェック済み例外はプログラマが空のcatchブロックを記述するのを促進し、catch ( e) {}のような危険なコードを増やす結果になってしまったというものがある。また別の批判として、メソッドの実装に変更を加えた結果新しいチェック済み例外が発生するようになる可能性があり、これによって契約が破壊されてしまう、というものがある。これは限られた例外のみが宣言されたインターフェイスを実装するメソッドや、メソッドの内部実装が変更された場合に起こり得る。中には、このような予期しない例外が発生することを見越し、あらゆる型の例外が発生し得る、と宣言 (throws Exception) するプログラマもいる。これはチェック済み例外の利点を無にしている。しかしながら、いくつかの場面では例外連鎖 ()、すなわち捕捉した例外を別の例外でラップして投げ直す、という手法が適用できる。例えば、ファイルにアクセスするコードがデータベースにアクセスするよう変更された場合、呼び出し側は内部で何が行われているかを知る必要がないため、が捕捉された場合でもとして投げ直すことが可能である。

try-finallyステートメントにおいても両者は異なる。finallyはたとえtryブロック内でthrowやreturnが実行された場合でも必ず実行される。これは、try内とfinally内で異なる値がreturnされた場合に予期しない振る舞いを生じることがある。C#ではfinallyブロック内ではreturnやbreakといった文の実行を禁止している。

 低レベルコード 
C/C++などで書かれたネイティブコード資産の再利用や、オペレーティングシステムあるいはハードウェアへのローレベルなアクセスを可能とするため、JavaおよびC#はともにネイティブ相互運用のための機能を提供している。

Java Native Interface (JNI) ではJavaコード内で非Javaコードをメソッドとして呼び出すことができる。しかしながら、JNIは呼び出されるコードのインターフェイス（シグネチャ）に制限がある。このため、Javaと既存のネイティブコード資産との間に余分なレイヤーが必要になることがよくある。このレイヤーはJavaではない言語で書かれる必要があり、CやC++がよく用いられる。JNIを利用することで、逆にC/C++側からJavaのクラスライブラリにアクセスすることも可能である。

.NETのプラットフォーム呼び出し（、P/Invoke）はC#からアンマネージコードの呼び出しを可能にする。プログラマはメタデータを通して、メソッドの引数や戻り値がどのように橋渡し（マーシャリング）されるかを完全に制御することができる。このため、既存コードのインターフェイスがC言語形式関数でありさえすれば、余分なレイヤーは必要にならない。P/Invokeは（Win32やPOSIXなどの）手続き型APIにはほぼ完全にアクセスすることができるが、C++クラスライブラリへの直接的なアクセスはきわめて困難である。そのほか、C++/CLI言語を介することで、C#とC/C++間の相互運用を行なうことも可能である。またCOM相互運用により、コード資産を相互に利用することも可能である。

C#は通常の型チェックなどのCLRの安全のための機能を無効にし、ポインタ変数を利用することができる。この時、プログラマはコードをキーワードでマークする必要がある。JNI、P/Invoke、unsafeコードはどれも同様に「危険な」機能であり、セキュリティホールやアプリケーションの不安定性につながる恐れがある。unsafeコードがP/InvokeやJNIに対して優れている点は、アンマネージコードを呼び出すことなくC#の機能内でタスクを完結できる点である。unsafeコードを含むアセンブリはコンパイル時にそのように明示的に指定する必要がある。これにより、実行環境は危険な可能性があるコードであるということを実行する前に知ることができる。

 統合言語クエリ 
C#には、統合言語クエリの為の拡張されたキーワード群が存在し、ソースコード上でSQL構文に似たクエリを直接記述することができる。このキーワード群は構文糖として機能し、決められた拡張メソッド呼び出しに展開される。
// 数列から偶数を抜き出して表示する
var inputs = new[] { 1, 5, 2, 3, 4, 7, 11, 10, 6 };
#if true
// LINQクエリ構文
var results =
 from inputValue in inputs
 where (inputValue % 2) == 0
 select inputValue;
#else
// LINQメソッド構文
var results = inputs.
 Where(inputValue => (inputValue % 2) == 0).
 Select(inputValue => inputValue);
#endif
foreach (var resultValue in results) {
 Console.WriteLine(resultValue);
}
統合言語クエリの拡張メソッド群は、コレクションに対するさまざまな集合演算が実装されており、このような演算をロジックで記述することなく、簡単に操作することができる。
また、ラムダ式から式ツリー（Expressionクラス）のインスタンスを生成することができるため、この機能を使用してデータベースへのクエリ発行・収集を、タイプセーフ性を失うことなく直接的に行うことができる（LINQ to SQL、LINQ to Entitiesなど）。

Javaでは、このようなクエリ構文を直接サポートしない。Java 8ではC#のLINQメソッド構文に近い記述が可能となるStream APIとラムダ式がサポートされた。

 命名規則 
.NETでは、外部に公開される識別子のうち、名前空間、型、メソッド、定数フィールドなどの命名にはパスカルケース（アッパーキャメルケース）が使われ、引数の命名にはキャメルケース（ローワーキャメルケース）が使われる。C#コードの命名規則もこのガイドラインに準ずるが、ローカル定数の命名にはパスカルケースが、ローカル変数の命名にはキャメルケースが使われる。

Javaでは、パッケージの命名には小文字の英数字（アンダースコアは含めるべきではない）、型の命名にはアッパーキャメルケース、メソッドの命名にはローワーキャメルケース、定数フィールドの命名にはアッパースネークケース、変数の命名にはローワーキャメルケースが使われる。

詳細は各言語のガイドラインを参照のこと。これらの命名規則は、後述の姉妹言語と相互運用する際にも重要な要素となるため、特に外部に公開される識別子に関してはガイドラインを遵守する必要がある。

 字下げスタイル 
言語の本質とは無関係だが、公式リファレンスなどのコード例における字下げスタイルとして、C#ではBSD/オールマンスタイル、Javaではサン・マイクロシステムズが1995年（最終改訂は1999年4月20日）に発表した公式のコーディング規約「Code Conventions for the Java Programming Language」に従うことが多い。統合開発環境の自動フォーマッターは通例これらのスタイルに沿っている。

// C#
class SomeClass
{
 public static double SomeMethod(double x)
 {
 if (double.IsNaN(x) || double.IsInfinity(x) || x < 0.0)
 {
 throw new System.ArgumentException();
 }
 else
 {
 return System.Math.Sqrt(x);
 }
 }
}

// Java
class SomeClass {
 public static double someMethod(double x) {
 if (Double.isNaN(x) || Double.isInfinite(x) || x < 0.0) {
 throw new IllegalArgumentException();
 } else {
 return Math.sqrt(x);
 }
 }
}

ただし両言語とも構文規則はフリーフォーマットであり、基本的に空白や改行の位置に左右されない。本記事では構文的および機能的な比較のしやすさと省スペース化の観点から、C#のコード例に関してもJavaと同じ字下げスタイルとしている。

 実装 

 JVMとCLR 
Javaはまったく異なる多くのオペレーティングシステム間で実行できる。またパーソナル・コンピュータに限らず、高度な計算処理や制御を必要とする家電製品や、Blu-ray Discのインタラクティブ技術にもBD-Jとして使用されている。このように数多くのJava仮想マシン (Java VM, JVM) 実装が存在する。

C#および.NETテクノロジー自体はクロスプラットフォーム指向であったものの、当初はWindows専用の実装しか存在しなかった。.NET Frameworkはマイクロソフトによる最初の.NETの実装であり、共通言語ランタイム (CLR) はマイクロソフトによる共通言語基盤 (CLI) の実装である。のちに他のプラットフォーム向けに実装された有名なものとしてMono/Xamarinがあり、多くのオペレーティングシステム向けの.NET開発が可能となった。ただし、マイクロソフトによる実装と比較して未実装部分が多く、利用できるライブラリに大きく制限があった。マイクロソフトによるモバイル／組み込み環境向け実装としては.NET Compact Frameworkがあり、Xbox 360などに搭載されていた。

その後、.NET Frameworkとは別に、マイクロソフトおよび.NET Foundationによるクロスプラットフォームなオープンソースソフトウェア (OSS) としての標準実装である「.NET Core」が公開された。なお、.NET CoreのCLR実装はCoreCLRと呼ばれていた。.NET Coreはバージョン5.0で「.NET」と改称された。.NET Frameworkの機能的なバージョンアップは4.8で終了しているが、Windowsへのバンドルおよびメンテナンスは続けられている。

 標準 
両言語の構文（文法）、プログラミングインターフェイス、バイナリ形式（実行ファイル形式）、実行環境などは様々な機関によって管理されている。

C#はEcmaとISO、JISによって定義されている。標準化の対象は言語構文、基本クラスライブラリ、アセンブリ形式、実行環境（共通言語基盤: CLI）など多岐に渡る。下位層フレームワークの上に新しく実装された上位層ライブラリの多くはこの標準には含まれない（Windows Forms、ASP.NET、ADO.NETなど）。

現在のところ、Javaのどの部分も第三者の標準化団体によって標準化されていない。Javaの商標、ソースコードやその他の素材に関してはオラクル（旧サン・マイクロシステムズ）が無制限の独占的な権利を保持しているが、オラクル（サン）はJava Community Process (JCP) と呼ばれるプロセスに参加し、当事者たちがJavaに関連する技術（言語、SDKからAPIに至るまで）に対する変更を専門家団体や諮問会議を通して提案することを許可している。JCP内の規定では、Javaに対する新しい仕様や変更はオラクル（サン）による承認が必要であるとされている。JCPは営利寄与者に対しては会費が必要としているが、非営利寄与者や個人は無料で参加できる。JavaのAPIセットにはいくつかのエディションがあり、標準エディションのJava SE、エンタープライズ向けエディションのJakarta EE（旧Java EE）、モバイル／組み込み環境向けエディションのJava MEが存在する。

 姉妹言語 
C#およびJavaには、それぞれの実行環境を用いて動作する姉妹言語が存在し、それぞれ.NET言語およびJVM言語と呼ばれている。姉妹言語は他の既存言語からの移行のしやすさや、記述能力および生産性の向上、あるいは新たなプログラミングパラダイムを導入するなどの目的で開発されたものであり、異なる言語間でユーザー定義のクラス型などを再利用する相互運用も可能である。

各々の代表的な姉妹言語を列挙する。

 .NET言語
 Visual Basic .NET (VB.NET)
 C++/CLI
 F#
 IronPython

C#は登場当初、C#.NETと呼ばれており、VB.NETもまた.NET Frameworkに対応する言語として同時期に登場し、当初はほぼ同等の言語機能を持っていたが、C#と比べてバージョンアップおよび新機能の追加は遅れることが多かった。2023年には、VB.NETには今後新しい言語機能は追加されないことが発表された。

Visual Studio 2005から利用可能になったC++/CLIはC++マネージ拡張の後継言語で、C#やVB.NETほど統合開発環境との親和性は高くないが、マネージコードとネイティブコード両方を混在して記述することができる唯一の言語である。

汎用プログラミング言語ではないが、コマンドラインシェル向けに特化したDSLとして、PowerShellが存在する。もともとは.NET Framework上に実装された「Windows PowerShell」だったが、クロスプラットフォームな.NET Coreに対応したPowerShell Coreとして再実装され、さらにPowerShellと改称された。

 JVM言語
 Scala
 Kotlin
 Groovy