Javaに対する批判
Javaに対する批判(ジャバにたいするひはん)では、プログラミング言語Javaと、その主たる実装であるJDKやその他の実装に関する批判、およびJavaプラットフォームの性能に対する批判について説明する。Javaプラットフォームの性能に関する、批判以外の内容については「Javaの性能」を参照のこと。
Javaは優れた技術だと評価する人々がいる一方で批判も少なくない。Javaはソフトウェアに関する複雑さを管理する問題に対して革新的な方法を提供するという目標のもとで開発された(ないし、そのように信じられている)。多くの人々2017年10月は、Java技術はこの期待に対して満足できる答えを提供したと評価している。
しかしJavaにも欠点が無いわけではないし、どのようなプログラミング作法にも適応しているというわけでもない。また、どのような環境や要件にも普遍的に適応しているというわけでもない。
ソースコードレベルでのプログラミングパラダイムの追加や記述性の改善を求めて、Java仮想マシン (JVM) 上で動作するScalaやKotlinといった後発言語も出現しているが、JVMベースの互換言語であるがゆえに、JVMに存在する制約もまた残されている。
Javaプログラムを実行する際、すべてのサードパーティーのライブラリはクラスパスに存在する必要がある。これは移植性の障壁となる。なぜならばクラスパスの文法はプラットフォームに依存するからである。例えばWindowsベースのシステムはサブディレクトリを区切るためにバックスラッシュ "Backslash" を使用し、パス区切りにセミコロン ";" を使用している。だが一方、他の多くのプラットフォームではサブディレクトリ区切りにスラッシュ "/" を使用し、パス区切りにコロン ":" を使用している。
各々の.jarや.zipアーカイブはクラスパスにおいて明示的に名前がつけられる必要がある。この抜け道としてアスタリスク(*)で終わるクラスパスを指定することで、そのディレクトリにある.jarや.JARで終わるすべてのファイル名にマッチさせることができる。しかしながら、.zipや.classファイルのようなものはマッチしない。
これらのクラスパス問題は環境変数CLASSPATHを使用せず、サン・マイクロシステムズが推奨する-classpathオプションを使用することで解決する。開発時は、-classpathオプションはバッチファイルやmakeや Apache Ant や統合開発環境を使うことで便利に指定することができる。Javaアプリケーションを利用する実行エンドユーザに対しては、開発者がマニフェストファイルにクラスパスを記述するか、FatJarやOneJarという、複数のjarファイルを一つにまとめるツールを使うことで解決できる。
サン・マイクロシステムズ(サン)のJavaの財産的価値はフリーソフトウェアコミュニティで議論を呼んだ。なぜならばサンのJavaの実装はフリーソフトウェアではなかったため、フリーソフトウェアや主にDebianや$100ラップトップ、Fedora CoreのようなGPL互換ライセンスが要求されるプロジェクトにはサンのJavaを含めることはできなかった。
サンは JavaOne 2006 で、Javaはオープンソースソフトウェアになるだろうと公表した。この声明はSun Softwareの上級副社長Rich Greenによって公表された。「オープンソースソフトウェアにするか否かという問題はない。どのような方法でオープンソースにするかが問題である。つまり我々はこれを実行する。」
2006年7月には、サンの最高技術責任者Robert BrewinはJavaは2007年6月に部分的にオープンソースになるが、全体のプラットフォームが完全なオープンソースになるには時間がかかるだろうとコメントしている。
2006年11月13日、サンは標準版Java実行環境は2007年3月にGPLの下でリリースされる予定であることを公表した。Java実行環境のソースコードはGPLの下で利用可能になる予定である。リチャード・ストールマンによると、これはの終焉を意味するとのことである。マーク・シャトルワースはJavaのオープンソース化についての最初のSunによる発表を「フリーソフトウェアコミュニティにとっての確かなマイルストーン」と評価した。
Javaのライセンスの問題は徐々に解決されつつある。時間が解決していると言える面もあるといえる。今後も新たなライセンスの問題に直面しそうであれば、Java Community Process に提案するよう働きかけてみるという手段もある。フリーソフトウェア財団や Eclipse Foundation など他のオープンソースコミュニティの助けを借りることで問題を解決に向けて進めることができる可能性がある。
Javaはメモリを管理するが、他のリソース(ファイルやJDBCデータベース接続など)は管理しない。これらはC言語およびC++でヒープに動的確保したメモリを明示的に解放する必要があったのと同様、プログラマによって解放されなければならない。
Javaはガベージコレクションによるメモリ管理機能を備える。これによって、完全ではないもののプログラマがメモリリークを起こすようなプログラムを作ってしまう危険性を減らした。Javaではオブジェクトは常にヒープ領域に(JITコンパイラによって最適化されてスタックやレジスタに割り当てられない限り)確保される。ローカル変数はスタックやレジスタに確保される。C++ではオブジェクトをヒープ領域に割り当てるかスタック領域に割り当てるかをプログラマが選択することができたが、Javaではそれが不可能になっている。
オブジェクトはガベージコレクションによって管理されるが、Javaはプログラマにガベージコレクションがいつ起こるかを保証しない。プログラマはガベージコレクションを阻止することができないし、たとえSystem.gc()を用いても任意のオブジェクトを任意のタイミングで解放することはできない。これはプログラミングを簡易にしメモリリークの可能性を軽減するが、決定論的でより効果的なメモリ処理を行うための柔軟性が犠牲になっている。C言語やアセンブリ言語のような低水準言語はこの柔軟性を提供する。
C++などで書かれた多くのプログラムはメモリリークの犠牲になりがちだが、問題はメモリリークだけではない。やデータベース、ネットワーク接続のような他のリソースのリークは特に例外が投げられたときに常に起こりうる。C++ではRAIIと呼ばれるイディオムによっていずれの問題も克服することができるが、Javaプログラマは忘れずにfinally節でリソースを解放する必要があり、Javaが何を解放するかということとプログラマは何を解放しなければならないのかということをきちんと理解する必要がある。
この問題は、メモリを増設する、最大ヒープメモリサイズを拡大するなどで解決できるケースもある。弱い参照 (java/lang/ref/java/lang/ref/java/lang/ref) を用いることで多少の解決策になることはある。だが、JREやJDKのバージョンが特定の古いものであることが要因となっていることもある。最新版のJREやJDKで実行、開発すれば問題発生率を下げることもできる。
finally節の問題は、場合によっては、ライブラリやフレームワークによって解決できることもある。入出力の場合はApache Commons IOを、データベース接続の場合はApache Commons DBUtils、HibernateやApache Cayenneなどのオブジェクト関係マッピングフレームワークを用いることでfinally節のことをあまり多く気にしなくても良いようにする手段がある。Java 7ではtry-with-resources文でこの問題に対処している(例外安全の確保)。
Javaの設計者らは、現在他の言語にあるいくつかの機能(多重継承、演算子多重定義、タプルなど)を実装しないことを決めた。
総称型(ジェネリクス)がJava 5.0に導入されたとき、すでにJavaにはクラスの大規模な枠組みがあった(それらの多くはすでに非推奨となっていた)、そして後方互換性を保つため、総称型の実装方法として既存のクラスを維持することを可能にする(コンパイル時の)型消去(型削除、type erasure)が選ばれた。これは、他の言語と比べると、総称型の導入によって提供される機能を限定してしまう結果になった。
Javaのプリミティブ型はオブジェクトではない。プリミティブ型は値への参照ではなく値そのものをスタック領域に保持している。この設計選択はパフォーマンス上の理由により行われた。このためJavaは純粋なオブジェクト指向言語と見なされていない。またこのことによりリフレクションがより複雑になっている。配列を除き、Javaのコレクションクラスはプリミティブ型を直接扱うことができず、プリミティブラッパークラスへのボックス化が必要となる。この制約はジェネリクスにおいても変わりない。Javaのジェネリクスは型安全性が担保されるだけであり、ジェネリクスを使わない場合と比べてパフォーマンス上のメリットはない。
また、Java 5.0は、コンパイル中に要求された場合にプリミティブ型を対応するプリミティブラッパークラスのオブジェクトに自動変換する機能(オートボクシング)をサポートしているが、オートボクシングではNullPointerExceptionが投げられる可能性がある。オートボクシングは暗黙のうちに起こる(キャストやメソッド呼び出しを伴わない)ため、このNullPointerExceptionという非チェック例外はJavaプログラムのコードに目を通しただけでは明確にはならない恐れがある。
Javaはメソッドを非virtualにする手段を提供しない(オーバーライドを禁止するためにfinal修飾子を使用することで封印することはできる)。これは派生クラスに、同じ名前の関係の無いメソッドを定義させる方法がないことを意味する。これは基底クラスが別の人間によって設計されるときに問題となることがあり、また、新しいバージョンが、派生クラスで同じ名前のメソッドがすでに存在するときに、同じ名前とシグネチャのメソッドを導入することで問題となることがある。これは、どちらのクラスの設計者の意図にも反して、派生クラスのメソッドが基底クラスのメソッドを暗黙のうちにオーバーライドするであろうことを意味する。これらのバージョン問題にある程度適合するためにJava 5.0は@Overrideアノテーションを導入しているが、後方互換性を保つには、それをデフォルトでは強制できない。
Javaは主にオブジェクト指向の単一パラダイム言語である。すべてのデータと処理は必ず何らかのクラス内に記述しなければならないため、必要以上にコードが冗長になってしまうことがある。Java 5.0で追加された静的インポート (static imports) はこれまでのJavaよりも手続きパラダイムによりよく順応する。
JavaではC++でオプションとされていた例外処理の仕様を取り込んだが、この際チェック対象の例外に対応するthrows文を必須とした。例外のチェックは小規模なシステムにとっては役立つが、大規模なシステムについても有益であるかどうかについては統一的な見解には至っていない。特に上位のコードでは下位のコードから発生するエラーを意識したくない(例としては、名前解決例外であるjavax/naming)。名前クラスの作成者は名前解決例外をチェック対象の例外として上位コードに対応を強制するか、コンパイル時のチェックを使わずに下位のコードからの例外を連鎖的に通知するかを選択する必要がある。
Javaでは、内部クラス、ローカルクラス、匿名クラスを使用することで、基本的なクロージャに近い記述が実現できる。しかしこれは完全ではなく、ローカルクラス/匿名クラス外の変数に対する参照を、ローカルクラス/匿名クラスのフィールドとして暗黙キャプチャできるように、final修飾子付きで宣言する必要がある。これは、変数のスコープを抜けたときに削除できるような、様々な寿命に対応したスタックモデルをJVM実装者が選択できるようにするためである。Java 8では実質的にfinalとみなせる変数は明示的にfinal修飾する必要がなくなったが、依然として再代入は許可されない。
また匿名クラスの構文も冗長であり、例えばRunnableインタフェースを要求する場面で、同インタフェースを実装する匿名クラスを定義する場合、単純なコードブロックとして定義することはできず、Runnable.run()メソッドを実装するようなクラスのブロックを定義する必要がある。Java 8ではラムダ式と関数型インタフェースによりこの問題を改善している。
Java 7以前にはC/C++の関数ポインタやC#のデリゲートに相当するメソッド参照の機能がなく、インタフェースで代用するしかなかった。Java 8ではメソッド参照が追加された。
Javaの浮動小数点演算は主にIEEE 754(二進化浮動小数点数演算標準)をベースとしているが、例えばIEEE 754に必須とされる例外フラグや方向指定の丸めなどの、いくつかの機能については"strictfp"修飾子を指定した場合でもサポートされない。Javaの仕様として知られているものはJava自体の問題ではないが、浮動小数点数演算を行う上で避けて通れない問題である。
Java用のクロスプラットフォームなウィジェットツールキットのひとつにSwingがある。Swingを使って書かれたアプリケーションのグラフィカルユーザインタフェース (GUI) のルック・アンド・フィール(look and feel、直訳すると「外観と操作感」)は大抵、各プラットフォームネイティブのアプリケーションのものと異なる。プログラマはネイティブのウィジェットを表示するAWT(ネイティブであるためOSのプラットフォームと同じ見た目を提供する)を使うことを選択することができる。しかしAWTツールキットは、高度なウィジェットのラッピングを必要とし、かつ様々なサポートされたプラットフォームで移植性を犠牲にしない高度なGUIプログラミングには向いていない。そして、SwingとAWTにはとりわけ高水準ウィジェットにおいてAPIが大きく異なる。
Swingツールキットは全てJavaで実装されている。Swingツールキットはネイティブアプリケーションとは異なるルックアンドフィールを持つという問題がある。一方でSwingツールキットのウィジェットはネイティブなツールキットの機能に限定されないという利点がある。この利点は全てのプラットフォームで利用できることが保証される最も基本的な描画機構を使ってウィジェットを再実装していることによる。不幸にも、(2006年8月の時点で)Java実行環境 (JRE) のデフォルトのインストールでは、Swingデフォルトの埋め込みMetal (メタル) ルックアンドフィールの代わりに、システムの「ネイティブ」なルックアンドフィールを使わなかった。プログラマがjavax/swingを使ってシステムネイティブなルックアンドフィールを設定しなかった場合、ユーザーは外観がネイティブアプリケーションのそれとは非常に異なるアプリケーションに遭遇するだろう。 macOSの配布元に限って含まれるApple自身のJava実行環境の最適化バージョンは、デフォルトで「デフォルト」をセットし、"Aqua" のルックアンドフィールを実装し、Macintosh上のSwingアプリケーションは、ネイティブなソフトウェアに似た外観を提供している。macOSの環境でさえ、プログラマはそのアプリケーションがAquaのように見えることを確実とするために、若干の余分の作業をまだしなければならない(例えば、メニューバーをWindowsのように各アプリケーションウィンドウ内部に表示するのではなく、macOSのメニューバー内部に表示させるために、システムプロパティをセットする必要がある)。
この問題は、開発者がJavaのバージョンを Java SE 6以降にアップグレードすることで解決する。Java SE 6になってからJavaのデスクトップまわり、GUI環境は一新され、開発効率は高まっているため、以前のバージョンよりも開発の手間はかからなくなっている。
Javaプログラムのパフォーマンスに関して一般論を述べることは不可能である。なぜなら、実行時の性能は、言語自身が持つ固有の特性よりも、JavaコンパイラやJava仮想マシンの品質に非常に大きく影響されるからである。Javaバイトコードの実行方法は、仮想マシンによって実行時に翻訳して実行する方法と、ロード時もしくは実行時に機械語にコンパイルしてハードウェアによって直接的に実行する方法がある。前者の翻訳実行する方法は機械語の実行よりも遅く、後者のロード時もしくは実行時にコンパイルして実行する方法は、最初のコンパイル時にパフォーマンスが犠牲になる。これはJavaにかぎらず、.NET Frameworkなど他の仮想マシンベースのプログラミング環境においても同様である。
コンピュータハードウェア性能の向上と、Javaのバージョンアップに伴う最適化技術の進歩によって、Javaプログラムの初回起動時のオーバーヘッドなどは誤差の範囲内になりつつあるが、ヴィルトの法則が示すように、総じてソフトウェアの複雑化やシステム要求の上昇にハードウェアの性能向上が追い付いていないのが現状である。C/C++ではJavaよりも細やかな最適化を手動で施すことができるが、プログラマが手作業で施した最適化よりも、JITコンパイルによる自動最適化のほうが性能面で上回るケースもすでに存在する。C/C++でJavaの実行時最適化技術を真似することは困難である。
Javaだけに限ったことではないが、パフォーマンス上の障壁となる言語仕様や言語仕様の欠落がいくつか存在する。それは例えば配列境界チェックや実行時型チェック、非仮想メソッドの欠如などである。ただしコードの安全性と性能はトレードオフの関係にあるため、チェック機構が一概に悪とは言えない。また、コンパイル時の最適化やJVMの実装次第で解消できるものもある。そのほか、Javaは構造体(ユーザー定義の値型)および矩形配列(真の多次元配列)を持たず、それぞれクラス(参照型)およびジャグ配列(配列の配列)などで代替する必要がある。また、Javaではメソッドから複数の値を返すためにはクラスや配列といった参照型を使用する以外の方法がない。これらの制約によってJavaのプログラムコードは、他の言語で書かれたコードよりも頻繁にヒープを使用しなければならなくなっている。
不要オブジェクトの自動回収を行うガベージコレクションは明示的なメモリ解放に比べそのオーバーヘッドは大きいが、ガベージコレクタの実装やアプリケーションでのオブジェクトの利用状況によってその影響はまちまちに変化する。多くのJava仮想マシンは世代別ガベージコレクションの採用によって動的メモリ管理を高速化しているため、多くのアプリケーションでは高い性能を示している。
一般に、プログラミング言語とその実行方法は、直交するのが普通で、コンパイラだったり、インタプリタだったり、バイトコード方式だったりする実装があっても構わないはずである。しかし、Javaには、実装上制限があり、あまりに自由度がない。
ジャストインタイムコンパイル(JITコンパイル)とネイティブコンパイルの性能差はほとんど無いとされるが、よく議論の種にもなる。JITコンパイルには時間が掛かるため、短時間で起動終了することが求められるアプリケーションや、起動と終了を頻繁に繰り返すアプリケーション、またプログラム内容が巨大なアプリケーションでは問題となる。しかし、一旦ネイティブコードに変換すれば数値計算においてもネイティブコンパイルと同等の性能を示す。
Javaはメソッド呼び出しの明示的なインライン化をサポートしないものの、多くのJITコンパイラではバイトコード読み込み時にインライン展開を行い、また実行時のプロファイリングを利用してその効率をより高めている。HotSpot仮想マシンが採用している動的再コンパイルでは、実行時でしか取得できない情報を利用することで、多くのプログラミング言語が採用する静的コンパイル方式を超える性能を得る場合もある。
Javaはセキュリティと移植性を重視して設計されたので、Javaではマシンアーキテクチャとアドレス空間への直接アクセスをサポートしていない。スキャナ(デジタルカメラ、オーディオレコーダ、ビデオキャプチャ)ないし実質的に直接メモリ空間の制御(一般的に、ドライバインストールを必要とするハードウェアやそれらの部品)を必要とする特定の一部のハードウェアを動かすことはJavaでは容易に実現できないことを意味する。この問題の実例はJavaのバージョン1.0で見られた。様々なプリンタドライバへのインタフェースコードがこの初期のJava実行環境に含まれず、プリンタへのアクセスが可能でなかった。
ハードウェアに直にアクセスするクライアントサイドまたはサーバシステムは、ネイティブコードとJavaライブラリを橋渡しするJava Native Interface (JNI) を使って、C/C++およびアセンブリ言語とJavaを組み合わせる方法を取る。また、ハードウェアアクセスを担うネイティブコードとJavaとの通信をファイルやデータベース、共有メモリやソケットを介して行う方法もあるが、理想的なやり方ではない。
JNIを使うことで、プラットフォーム依存性、潜在的なデッドロック、メモリリーク、場合によっては性能の著しい劣化などの問題が発生しうる。また、2つの異なるコードベースをメンテナンスするために必要となるコードの複雑性は、言うまでもない。しかし、それは、例えば.NET Frameworkの共通言語ランタイムにおけるP/Invokeのように、他の仮想マシン環境と共通する事例である。
現在では今のところ、Javaはまだまだハードウェア開発、デバイスドライバ開発には適していない点が多い。この問題について、JavaでUSBドライバ開発ができる Java Communication API という技術がすでにある。他にも、Jini対応機器を端末に接続すると、サーバから自動的にJava製ドライバを端末にダウンロードしてその機器を使うことができる技術Jiniというものが考えられている。なおこのJiniは、Java Native Interfaceを意味するJNIとは別物である。
Javaは、Java仮想マシン (JVM) の上部で動くバイトコード言語である。異なるプラットフォームで実行できる能力と言語の互換性は、最終的にはJVM実装の安定性とJVMのバージョンに依存している。Javaは非常に様々なシステム上で動くとうたわれているが、最新のJVM(とJRE)はWindows、Linux、Solaris対応だけ活発に更新されている状況である。HP-UX(Java for HP-UXなど)とIBM (for MVS、AIX、OS/400) は、それぞれのプラットフォームファミリー独自の実装を提供するが、必ずしも最新のサン・マイクロシステムズのリリースを反映していない。他のプラットフォームにおけるJVM実装も、たいてい今後も続くが、時々一般の実装例よりも何年か何ヶ月か遅れるので、互換性問題が生じる。具体的な例を示すと、Java 2 Platform Standard Edition 1.5 (J2SE 1.5) をサン・マイクロシステムズがリリースしたのは2004年9月30日だが、Mac OS X用J2SE 1.5をAppleが公開したのは2005年3月であり、5か月余りの差が生じている。
"Write once, run anywhere"(WORA、一度書けばどこでも動く)という言葉があるとおり、Javaの目標の一つにプラットフォーム非依存があげられる。JavaコンパイラはJava仮想マシン用の中間言語(Javaバイトコード)を生成する。コンパイルされたJavaのプログラムは、Java仮想マシンを実行環境として動作する。この仮想マシンがハードウェア間の差異を吸収することで、プラットフォーム非依存を実現している。ただし、現時点では一部にプラットフォーム依存の部分があり、完全なプラットフォーム非依存ではない。
また、マルチプラットフォームにするということは、一部のプラットフォームにしかない独自の機能はJavaから使えないことを意味する。
例えばWindows用のマルチメディアAPIであるDirectXや、3DグラフィックスAPIであるDirect3DはJavaから直接呼び出すことはできない。そのため、橋渡しをするための拡張APIが提供されている。
またJavaではバージョン間の前方互換性・後方互換性の問題が議論の対象になっている。Javaではバージョン間の互換性をある程度の水準まで達成している。しかし、バージョンの異なる実行環境の取り扱いには課題が残っている。例えばJ2SE 1.4実行環境用に書かれたプログラムは、実行環境にJ2SE 1.3を想定すると明示的に指定してコンパイルしなければJ2SE 1.3実行環境では動かず、利用するライブラリがJ2SE 1.4以降から追加されたものである場合にはJ2SE 1.3実行環境での実行を諦めなければならない。J2SE 1.3に対する後方互換性は、2世代先であるJ2SE 5.0まで保証されている。J2SE 1.3以降のJavaプログラムでは前方互換性は保証されないが、Java実行環境 (JRE) の自動アップデート機能によって仮想マシンを最新バージョンにアップデートすれば解決できる。JDK 1.1、J2SE 1.2 時代のJavaプログラムは、2019年3月となっては古いため、後方互換性問題に引っかかる可能性がある。例えば、古いプログラムが新しいバージョンのJDK/JREで廃止されたAPIを使用している場合に問題となる。その場合は、そのJavaプログラム開発者に最新版のJavaコンパイラでもコンパイルが通るように修正してもらうしかない。
この問題も、サン・マイクロシステムズのJavaコーディング規約をJavaプログラマが守っていればほぼ起きることがなく、Javaソースコード上のimport宣言や新しく加わったJavaキーワード(enumやassert)と重複するものが無ければほぼ心配することもなくなる。とくに、多くの場合において、これらの問題はコーディング規約だけでなく、統合開発環境やCheckStyle、FindBugsなど各種ツールなどによって解決できるケースがある。Javaプログラマは、日頃からCheckStyleやFindBugsを使ってプログラミングしていれば、後方互換性の問題につき当たる可能性は下がる。しかし、Javaアプレットなどのように、方針転換による大規模な機能廃止もありうる。JDK/JREにはサポート期間が定められており、古いJDK/JREを永久に使い続けることはできないため、廃止予定となったAPIが完全廃止される前に代替技術に移行するなど、最新のJava技術動向に追随していく必要がある。