C# vs Java:20年の歩みと思想の違いを技術的に整理する
はじめに
JavaエンジニアがC#(ASP.NET Core)を学ぶと、「似ているようで根本的に違う」と感じる瞬間がいくつかある。async/await、yield return、IAsyncEnumerable、タプル——これらをJavaで実現しようとすると、ライブラリが必要だったり、冗長な実装が必要だったりする。
本記事では、C#とJavaの設計思想の違いがどこから来るのかを、両言語の歴史的な歩みと具体的なコードで整理する。
1. 出発点の違い
| Java | C# | |
|---|---|---|
| 登場 | 1995年(Sun Microsystems) | 2000年(Microsoft) |
| 設計者 | James Gosling | Anders Hejlsberg |
| 前身 | Oak(組込み向け言語) | Delphi / J++ |
| コア思想 | Write Once, Run Anywhere | 開発者生産性の最大化 |
| 実行環境 | JVM | CLR(Common Language Runtime) |
HejlsbergはTurboPascal・Delphiの設計者でもあり、C#は「Javaの反省を踏まえて設計し直した言語」という側面が強い。
2. Javaの機能歩み(Java 1.0 〜 Java 25)
Java 1.0〜1.4(1995〜2002):基礎の確立
- オブジェクト指向、ガベージコレクション、例外処理
-
java.util(Collections Framework) - スレッド(
Thread/Runnable) - JDBC(データベース接続の抽象化)
この時代はC#が存在しなかった。Javaが「エンタープライズの標準」として普及。
Java 5(2004):近代化の第一歩
// ジェネリクス
List<String> names = new ArrayList<>();
// 拡張for文
for (String name : names) { ... }
// アノテーション
@Override
public String toString() { ... }
// enum
enum Day { MONDAY, TUESDAY, WEDNESDAY }
// オートボクシング
Integer x = 42; // int → Integer 自動変換
C#との比較: C# 2.0(2005)でジェネリクス導入。ほぼ同時期だが、C#のジェネリクスはランタイムレベルで実装されており、JavaのType Erasure(実行時に型情報が消える)との設計上の違いがある。
Java 8(2014):最大の転換点
// ラムダ式
Runnable r = () -> System.out.println("Hello");
// Stream API(java.util.stream)
List<String> result = names.stream()
.filter(n -> n.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
// Optional
Optional<String> opt = Optional.ofNullable(getValue());
opt.ifPresent(System.out::println);
// インターフェースのdefaultメソッド
interface Greeter {
default void greet() { System.out.println("Hello"); }
}
C#との比較: C#はLINQ(C# 3.0、2007年)で7年先行。JavaのStreamはLINQに近いが、collect(Collectors.toList()) の冗長さは今も残る。C#では .ToList()。
Java 9〜11(2017〜2018):モジュールと型推論
// var(Java 10〜)
var list = new ArrayList<String>(); // 型推論
// モジュールシステム(Java 9)
module com.example.app {
requires com.example.core;
exports com.example.api;
}
// String新メソッド(Java 11)
" hello ".strip(); // trim()のUnicode対応版
"".isBlank();
"a\nb\nc".lines().count(); // 3
Java 11がLTSとして広く採用される。
Java 14〜16(2020〜2021):表現力の向上
// Switch式(Java 14)
int numLetters = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
};
// yield(Switch式の中だけで使う)
int result = switch (x) {
case 1 -> 10;
default -> {
int v = x * 2;
yield v; // C#の yield return とは別物!
}
};
// Record(Java 16):イミュータブルなデータクラス
record Point(int x, int y) {}
// equals, hashCode, toString, getter が自動生成
// テキストブロック(Java 15)
String json = """
{
"name": "田中",
"age": 25
}
""";
// instanceof パターンマッチング(Java 16)
if (obj instanceof String s) {
System.out.println(s.length()); // キャスト不要
}
Java 17(2021):LTS、封印クラス
// sealed class:継承できるクラスを制限
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
// パターンマッチングとの組み合わせ(Java 21で完成)
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
Java 21(2023):Virtual Threads で革命
// Virtual Threads:OSスレッドを消費しない軽量スレッド
// ブロッキングI/OをしてもOSスレッドをブロックしない
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
executor.submit(() -> {
// ブロッキングでDBアクセスしても高スループット
var data = database.query("SELECT ...");
process(data);
});
}
}
これはC#のasync/awaitとは逆のアプローチ。
- C#:「非同期で書く」→ コンパイラがステートマシンに変換
- Java:「同期で書く」→ ランタイムが軽量スレッドで高スループットを実現
Java 25(2025):LTS最新版
- パターンマッチングのさらなる強化(プリミティブ型パターン)
- Structured Concurrency(構造化並行性)正式化
- Scoped Values正式化
- Project Valhalla(値オブジェクト)が近づく
3. C#の機能歩みと対比
C# 3.0(2007):LINQ — Javaより7年早く関数型
// LINQ:クエリ構文
var result = from n in names
where n.StartsWith("A")
select n.ToUpper();
// メソッド構文(Javaのstream APIに近い)
var result = names
.Where(n => n.StartsWith("A"))
.Select(n => n.ToUpper())
.ToList(); // Java: .collect(Collectors.toList())
C# 5.0(2012):async/await — Javaとの最大の分岐点
// C#:言語レベルで非同期を抽象化
public async Task<string> FetchDataAsync()
{
var response = await httpClient.GetStringAsync(url);
return response;
}
// Java:ライブラリ(CompletableFuture)で対応
public CompletableFuture<String> fetchDataAsync() {
return CompletableFuture.supplyAsync(() -> {
// ここでは同期的なHTTPコールが必要
return httpClient.get(url); // ブロッキング
});
}
C#のawaitはコンパイラがステートマシンに変換する言語機能。Javaのコールバックチェーンより遥かに自然に書ける。
C# 8.0(2019):IAsyncEnumerable + yield return
// 非同期ストリーム:言語レベルで実装
public async IAsyncEnumerable<int> ReadChunkedAsync()
{
int index = 0;
while (index < 100)
{
int[] chunk = await GetNextChunkAsync(index);
if (chunk.Length == 0) yield break;
foreach (var item in chunk)
yield return item; // 状態を保持したまま一時停止
index++;
}
}
// 呼び出し側
await foreach (var item in ReadChunkedAsync())
{
Console.WriteLine(item);
}
// Javaで同等のことをする場合:Project Reactorが必要
Flux<Integer> readChunked() {
return Flux.generate(
() -> 0,
(index, sink) -> {
int[] chunk = getNextChunk(index);
if (chunk.length == 0) sink.complete();
else for (int item : chunk) sink.next(item);
return index + 1;
}
);
}
C# タプル vs Java Record
// C#:匿名タプル(名前不要)
(bool isValid, string message) Validate(string input)
=> (true, "OK");
var result = Validate("test");
Console.WriteLine(result.isValid); // true
Console.WriteLine(result.message); // OK
// Java:Recordは「名前が必須」
record ValidationResult(boolean isValid, String message) {}
ValidationResult validate(String input) {
return new ValidationResult(true, "OK");
}
// 一時的な複数戻り値のためにクラス定義が必要
4. DI(依存性の注入)の比較
Spring Boot vs ASP.NET Core
// Spring Boot:アノテーションスキャンで自動検出
@Service
public class OrderService {
@Autowired
private OrderRepository repository;
}
@Repository
public class OrderRepositoryImpl implements OrderRepository { ... }
// ASP.NET Core:Program.cs に明示的に登録
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<OrderService>();
// コンストラクタインジェクション(アノテーション不要)
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
=> _repository = repository;
}
思想の違い:
- Spring:「コンテナが全部見つけてくれる」(暗黙的・魔法感あり)
- ASP.NET Core:「何が登録されているか一目でわかる」(明示的・透明性)
5. ORM の比較
| Java | C# | |
|---|---|---|
| フルORM | JPA / Hibernate | Entity Framework Core |
| マイクロORM | MyBatis | Dapper |
| マイグレーション | Flyway / Liquibase | EF Core Migrations |
| LINQ相当 | JPQL / Criteria API | LINQ to Entities |
// EF Core:LINQでSQLなしにクエリ
var orders = await context.Orders
.Where(o => o.Status == "Active")
.Include(o => o.Items)
.ToListAsync();
// JPA:JPQLで文字列クエリ
List<Order> orders = em.createQuery(
"SELECT o FROM Order o WHERE o.status = :status", Order.class)
.setParameter("status", "Active")
.getResultList();
6. 非同期モデルの設計思想対比
C# の思想:「複雑さを言語が隠す」
async/await → コンパイラがステートマシン生成
yield return → コンパイラがIEnumerator生成
IAsyncEnumerable → 両方を組み合わせる
開発者は「同期的に見えるコード」を書く
Java の思想:
〜Java 20:「ライブラリで解決(Reactor、RxJava)」
Java 21〜:「ランタイムで解決(Virtual Threads)」
Reactor → push型の非同期ストリーム
VirtualThreads → 同期的に書いて高スループット
7. テストフレームワークの比較
| 機能 | Java | C# |
|---|---|---|
| 単体テスト | JUnit 5 | xUnit / NUnit / MSTest |
| モック | Mockito | Moq |
| アサーション | AssertJ | FluentAssertions |
| Spring統合 | @SpringBootTest | WebApplicationFactory |
// JUnit 5 + Mockito
@Test
void shouldReturnOrder() {
when(repository.findById(1L)).thenReturn(Optional.of(order));
var result = service.getOrder(1L);
assertThat(result).isPresent();
}
// xUnit + Moq
[Fact]
public async Task ShouldReturnOrder()
{
_mockRepo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(order);
var result = await _service.GetOrderAsync(1);
Assert.NotNull(result);
}
8. まとめ:思想の違いの本質
┌─────────────────────────────────────────────┐
│ Java │
│ 「安定性」「後方互換」が最優先 │
│ 重要機能 → まずライブラリで解決 │
│ 標準化は慎重に、10年越しもある │
│ Java 21〜 Virtual Threadsで逆転の発想 │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ C# │
│ 「開発者生産性」が最優先 │
│ 重要機能 → 言語・コンパイラに組み込む │
│ async/await、yield、LINQ全部言語機能 │
│ .NETと密に統合、一貫したAPI │
└─────────────────────────────────────────────┘
どちらを選ぶか
| ユースケース | 推奨 |
|---|---|
| エンタープライズJava資産の継続 | Java + Spring |
| 新規Webアプリ(Azure利用) | C# + ASP.NET Core |
| マイクロサービス(コンテナ) | どちらも可 |
| 高スループットAPI | C#(gRPC、SignalR) または Java 21 Virtual Threads |
| 関数型プログラミング重視 | Kotlin(JVM)または F#(.NET) |