負け犬プログラマーの歩み

負け組だった人間が今一度PGとして人生の飛躍を模索するも加齢と共に閉ざされる未来に直面しているブログ。

なぜJavaはC#と比べて駄目なのか

Javaは決して悪い言語ではない。

C++からポインターの「*」やアロー演算子の「->」とかスコープ演算子の「::」とか気持ち悪いものを廃止・整理して、比較的読み易いシンタックスになったと思う。1995年当時から見れば、十分に出来の良い言語だったと思われる。

でも後発のC#でコーディングする機会が増えてきたら、如何にJavaが駄目(というか保守的な)言語かってのもまた同時に痛感してしまう。2005年リリースの2.0の時点で既にJavaをほぼ完全に上回っていると思うのに、その後ラムダ式LINQ・拡張メソッドなど数多くの新機能が加わった現行C#とは最早比べるまでもないと思う。

以下は根拠。

■注(2014年2月18日)
このエントリーは殴り書きに等しい状態で放置してましたが、最近は思わぬところで読まれ始めたりしたので、ちょっと加筆修正しました。

①そもそも純粋なオブジェクト指向言語ではない。

Javaで実際に用いる型の大半はプリミティブ型だけど、このプリミティブ型はJavaのルートオブジェクトであるjava.lang.Objectを継承しているオブジェクトではない。その代償として、メンバやメソッドを一切を持てず、int型の変数を文字列にするのにインスタンスObject.toString()がないから、クラスメソッドInteger.toString()を使う必要がある。後述する型消去型のジェネリクスとの相性も最悪、というかそもそも使えない。
RubyScalaみたいな純粋なオブジェクト指向型言語のデザインを採らず、敢えて中核的な型に「オブジェクトではないもの」を残したのは、パフォーマンスの観点からと言われている。すなわちオブジェクトは、ヒープ領域にメモリを確保するし、負荷の大きいガベージコレクション処理を要してしまうからだと。
一方で、プリミティブ型に該当するC#のデータ型は、もちろんスタック領域に作られる構造体であり他のクラスが継承することはできないけど、System.ValueType(この親はSystem.Object)を継承しているオブジェクトであり、基礎的なメソッドやメンバーは備わっているので、パフォーマンスとオブジェクト指向を見事に両立させたデザインとなっている。

※ただし、仮にJavaのプリミティブ型がC#のデータ型よりも高速に機能するのであれば、この仕様は正当化されると思う。ベンチマークもないからなんとも言えんけど。

2015/1追記:簡単な計測をしてみたところ、同じぐらいかむしろC#の方が速い模様。
JavaとC#のパフォーマンスを比較する。 - 負け犬プログラマーの歩み

②多重継承でなくインターフェイスを採用したのに、ミックスインがない。

多重継承は色々と問題が有る(らしいけどC++の経験がないので知らない)。だから継承できるクラスは1つのみだけど、その代わり「これを実装したものは外部から見れば全て同一と見なせる」って感じのインターフェイスという仕様を採用している。ただしインターフェイスは、デフォルトの実装を持たないので、インターフェイスを実装したクラス達が共通の処理を持ちたくても、基本的にコピペするか委譲させるしかない。また、部分的に実装するという選択もできない。その欠点を補うものとして他の言語はミックスインやトレイト、@optionalを採用し、C#も拡張メソッドという形でこの欠点を克服したのだが、Javaは近日登場予定のSE8でやっとインターフェイスのデフォルト実装を導入するまでずっとこの問題を抱え込んでしまっていた。

③関数が第一級オブジェクトではない。

これは①の完全なオブジェクト指向型言語ではない理由の一つでもある。とにかくJavaは関数を引数として渡すことができない。やるとしたら文字列で関数名を渡してリフレクションするしかない。Javaでよく無名クラスを使わざる得ない最大の理由がここであるけど、無名クラスを使うとなると、今度はクロージャーがなかったりするんで、無名クラスに値を渡したり逆に値を受け取るために、finalをつけた変数をわざわざ用意することになる。

※これもJava SE8で変わるのかもしれないが最早Javaには興味がないのでよく仕様は見ていない。

ジェネリクスがあまり役に立たない。

Javaジェネリクスは型消去法で実装されている。要はコンパイルするときに、全てObject型に入れてパラメーターに与えた型でキャストするコードに置き換えてしまうやり方だが、①で述べたようにJavaで用いる型の大半はオブジェクトではないプリミティブ型なので、ジェネリクスはそのままでは使えず、わざわざIntegerなどのラッパークラスに対するボクシングの必要性が出てくる。
加えて、そもそもJavaジェネリクスは単純な型のミスマッチを検出できることしか利点がないように思う。あるコレクションに対して間違って別の型のオブジェクトを格納してしまうという凡ミスがビルト段階で分かる程度の話であり、名前から分かるようにジェネリクスの本来の意義である「総称的なプログラミング」とは程遠い使用用途になっている。C++のテンプレートから由来した仕様なんだろうけど、いっそのことジェネリクスという名称を用いるより、型の安全性の確保が主目的であれば、タイプセーフティとか何とかでいいだろう。
これに比べると、C#ジェネリクスはもちろん大抵の場合はコレクションの型の安全性を確保する場合に使われるだけかもしれないが、コンパイル後にも動的に型パラメーターを取得することが可能になっている。

⑤プロパティがない。

JavaC#の比較として良く「プロパティの有無が~」と指摘されるが、個人的には、オブジェクト指向言語としては、obj.Hoge = "foo"の方がobj.setHoge("foo")よりも幾らか直感的で分かりやすいシンタッタスであるとか、 Javaのコードは頻繁に末尾が...)));となって汚い程度の不満があるだけで、そこまで致命的な問題とは思わない。
そもそもC#のプロパティもコンパイルすればset_Xみたいなメソッドとして定義されるというのに、コンパイル前まではメソッド扱いされないのでデリゲートで指定することができないなど個人的に不満があるからだ(ラムダ式で回避できた記憶もあるが、これが原因でC#でもわざわざJava式のアクセッサーを書いた事もある)。
JavaもSetter/GetterはIDEで自動生成させるのが通例であるのなら、そこまでプロパティの有無でユーザの生産性が落ちるとは思えないし、それがJave SE7で導入寸前までいって見送られた理由なんじゃないかなとは思う。

⑥インデクサーがない。

C#の場合はコレクションや独自クラスには[]で中身にアクセスできるのに、Javaの場合はlist.get()になってしまう。またC#は独自クラスにインデクサーを定義して、[]でアクセスさせることができる。もちろんステートメントの末尾が頻繁に)));になるぐらいのデメリットしかないが、オブジェクト指向言語としてより直感的なインターフェイスを提供しているのはC#だ。

型推論が超限定的。

そりゃあ一部の言語の本格的な型推論に比べて、C#varは単に型が明らかなものを省略可能にしているので、型推論と呼ぶには若干の抵抗もあるかもしれないが、いちいちローカル変数に長ったらしい型を書かずに済むのは非常に助かるわけで、個人的にはC#3.0で導入された新機能のうち最も嬉しいものだった。
これに比べて、Java型推論List list = new ArrayList<>();のように、右辺の型パラメーターを<>に省略できる程度に限定されている。「<>がダイヤモンドに見えるからダイヤモンド演算子」とか呼んでいるらしいが、これがSE7の目玉仕様の1つだった時点でJavaがどんなに駄目な言語だったかを物語る。
もちろん、C#型推論はローカル変数限定となっているので(多分必ずしも代入を必要としないから?)、メンバー変数にコレクション等を指定する場合はJava型推論の方が優れているとは言えるが、どちらにしろ限定的である。

⑧いまだに名前付き引数や省略可能(オプション)引数がない。

厳密に言うと可変長引数を使えば省略可能にはできる。でも使用者にインデックスの意味を覚えさせるようなメソッドを設計して外部に公開するのはアンチパターンなので、結局はJavaはひたすらオーバーロードメソッドを復数定義し、その中でnullだった場合のデフォルト値を自分で設定するとか、名前付き引数がないから仕方なくオリジナルの引数用クラスを作るなどを迫られる。
コンパイラなんて書ける気がしない言語設計書の素人が言うのもなんだが、この2つに関しては基本的に糖衣構文なので、パーサを少しいじるだけで実装できるように思えるが、C#Rubyもそういえば実装がだいぶ遅れた経緯もあるので、きっと難しいのかもしれない。そもそもSun(Oracle)は基本的に保守的なので、オーバーロードがある限りはポジショナル引数のみの仕様にしたいのかもしれない。

メソッドがデフォルトでオーバーライド可能であり、オーバーライドを示すアノテーションが任意になっている。

Javaは「オーバーライド禁止の場合はfinalをつけて、オーバーライドする場合は@Overrideアノテーションを任意でつけられる」という仕様になっているが、C#の「オーバーライド可能な関数にはvirtualをつけて、オーバーライドする場合はoverrideキーワードをつける」という仕様に明らかに劣ると思う。とりあえず、手動でfinalを書くのはだるい。
Javaは1.6からはインターフェイスメソッドにも@Overrideをつけられるように変更した事は一見良い判断とは思うのだが、任意でよければC#だってインターフェイスの型名とメソッド名も記載し、明示的にインターフェイスを実装することもできるので、どの道Javaの仕様に優位点はない。
なお、Javaはこれだけオーバーロードに依存しているのだから、VB.NETではキーワードとなっているoverloadアノテーションなりで導入した方がいいとは思うのだが、C#にも実は存在しないので、これはおあいこ。

⑩チェック例外という概念がある。

昔からチェック例外については賛否両論があるので一概にJavaが悪いとは言えない。でもやはりJavaを書いてると、API使うときにコンパイルが通らないからとりあえずcatchブロックは用意したけどその中身が空っぽってことがよくあると思う。また、スレッドを使う場合は必ずInterruptedExceptionをキャッチしないといけないのは煩雑過ぎる。

partialがない。

partialはうまく使えば可読性を上げるし、ASP.NETでは自明で機械的な変数宣言の類はVisual Studioが自動生成したpartialクラス内でやってくれるのでソースコードも綺麗になったりする。しかし、Javaにはそんな便利なものはないので、例えばAndroidアプリを作っていると、クラスの上の方はいつも大量のView関連のメンバ変数を手動で定義しないといけない。

演算子オーバーロードがない。

個人的にC#でもRubyでもこれまで一度も演算子オーバーロードしたことないが、数理計算をしたい場合などには問題かもしれないし、また既に標準ライブラリの時点でこの欠落が影響している部分もある。例えば、C#は文字列同士の比較は普通に==演算子でいけるのだが、Javaの場合はStringがオーバーライドしたequal()を使わないといけない。==の「メモリでの同じ場所を指してるかどうかを判定する」という挙動を文字列という特殊なオブジェクトに対して変えることができない。

⑬デストラクタがない

C#のデストラクタもログを出力する時にしか使ったことはないけど、あえてJavaがデストラクタを採用しなかった理由もまた分からない。

2014/06/24
すいません、ファイナライザは有りました。

プリプロセッサがない。

C言語プリプロセッサだらけのコードは読んでてげんなりするが、それでも#ifすら使わない人でも流石に#Regionぐらいは大半のC#/VBエンジニアは使うだろう。でもJavaにはプリプロセッサはない。

■2014/06/24追加

⑮1つのファイルにpublicなクラスを1つしか含められない。

ファイル名とクラス名が一目瞭然なJavaの方が分かりやすさに寄与していると一瞬思うかもしれないが、それでも細かいクラスを1つの名前空間の元に含めて管理できるC#の方がやはり分かり易い。

⑯文字列の扱いが不便。

Javaは前述の==演算子で文字列を比較できないだけでなく、単に文字列に変数を埋め込むだけのString.format()は奇怪な構文を必要とし、逐語的文字列リテラルもない。