世界を動かす技術を、日本語で。

26年間のJavaの変遷を評価する

概要

Javaの進化を26年にわたり振り返り、主な言語機能やコアライブラリの変更点を評価 UIやVM/GCの改良は対象外、言語仕様とコアAPIに限定 各リリースごとに印象的な機能や影響の大きい追加点をピックアップ 個人的な視点での評価と簡潔なコメント 全体的にJavaは大きく進化したが、設計上の課題や惜しい部分も多い

Javaの主要リリースと機能評価

  • Java 2 (1998)

    • Collections Framework 導入
      • VectorやHashtableから包括的なコレクションAPIへ移行
      • ミュータブル・イミュータブルの区別やAPI設計に課題
      • 長期的には標準採用され続けている実績
      • 評価:4/10
  • Java 1.4 (2002)

    • assertキーワード
      • 新キーワード追加に当時は驚き
      • 本番コードでの利用は稀
      • 評価:3/10
    • 正規表現API
      • Matcherクラスはやや使いづらいが、堅実な実装
      • 評価:9/10
    • NIO(新I/O)
      • ノンブロッキングI/O初登場
      • API設計が複雑・使いにくい
      • 評価:0/10
    • 暗号API(JCE/JSSE)
      • 必要な機能だがエラーが発生しやすく扱いづらい
      • 評価:1/10
  • Java 5 (2004)

    • Generics
      • 型安全性向上・言語の複雑化も招く
      • 導入後の普及度は高い
      • 評価:8/10
    • Annotations
      • 静的解析などの可能性、実際は限定的な用途
      • 評価:5/10
    • Autoboxing
      • プリミティブ型とラッパークラスの自動変換
      • 記述量削減、効率面は据え置き
      • 評価:7/10
    • Enums
      • シンプルな列挙型。高度な型システムには非対応
      • 評価:6/10
    • Varargs
      • 可変長引数で標準APIの冗長性を軽減
      • 評価:8/10
    • for-each文
      • コレクション操作を簡便に
      • Streams登場で一部役割が変化
      • 評価:8/10
    • Static imports
      • DSL用途などで便利、*インポートはやや危険
      • 評価:8/10
    • java.util.concurrent
      • 並行処理APIの決定版。設計の良さが際立つ
      • 評価:10/10
  • Java 7 (2011)

    • switchでのString利用
      • コードスメル感が強い
      • 評価:1/10
    • try-with-resources
      • 例外安全性を大幅向上、現場でも広く利用
      • 評価:10/10
    • ダイヤモンド演算子
      • 型推論による記述量削減
      • 評価:6/10
    • バイナリリテラル・アンダースコア
      • マイナーな記法強化
      • 評価:4/10
    • Path/Filesystem API
      • File APIよりは進化、依然として冗長
      • 評価:3/10
  • Java 8 (2014)

    • Lambdas
      • 導入時は議論多い。匿名関数のデバッグ性に課題
      • 評価:4/10
    • Streams
      • filter/map/reduceの導入、APIの複雑さと学習コスト
      • 並列処理の過剰な期待、リソースリークの温床
      • 評価:1/10
    • Java Time API
      • 従来より大幅改善、複雑さは残る
      • 評価:8/10
  • Java 9 (2017)

    • Modules
      • JRE内部向け機能、アプリ開発者には恩恵薄い
      • 互換性問題を引き起こすことも
      • 評価:-10/10
    • jshell
      • 待望のREPL。導入が遅かった
      • 評価:6/10
  • Java 10 (2018)

    • varによるローカル型推論
      • 好みが分かれるが、記述性向上
      • 評価:9/10
  • Java 11 (2018)

    • 新HTTP Client
      • Apache HttpClient風。やや冗長
      • 評価:6/10
    • TLS 1.3, djb-suite暗号
      • セキュリティ強化
      • 評価:9/10
  • Java 12 (2019)

    • switch式
      • 品質向上のための小規模改善
      • 評価:6/10
  • Java 13 (2019)

    • テキストブロック
      • 複数行文字列。インジェクションリスク増
      • 評価:3/10
  • Java 14 (2020)

    • instanceofのパターンマッチ
      • 明示的キャスト不要に
      • 評価:4/10
    • Records
      • 待望のデータ構造表現
      • 評価:10/10
    • NullPointerException詳細化
      • デバッグ効率向上
      • 評価:8/10
  • Java 15 (2020)

    • Sealed classes
      • 型安全性向上。利用例はまだ少ない
      • 評価:8/10
    • EdDSA署名
      • 暗号機能強化、初期バグあり
      • 評価:8/10
  • Java 16 (2021)

    • Vector(SIMD)API
      • 高速化に期待、まだ開発途中
      • 評価:未定
  • Java 17 (2021)

    • switchのパターンマッチ
      • ADT実現に一歩近づく
      • 評価:7/10
  • Java 18 (2022)

    • デフォルトUTF-8
      • 文字化け問題を大幅解消
      • 評価:10/10
  • Java 19 (2022)

    • Recordパターン
      • ADT表現の完成度向上
      • 評価:9/10
    • 仮想スレッド
      • 非同期・コールバック地獄の解消に期待
      • 評価:未定
  • Java 21 (2023)

    • String templates
      • テキストブロックの課題を解決する設計
      • 初期設計に問題、再設計中
      • 評価:10/10(期待値)

総括と主観的な所感

  • Javaは 大規模な言語進化 を遂げてきたが、 設計上の一貫性やAPIの使いやすさ には依然課題
  • 強力で便利な機能もあれば、 複雑化や冗長性 を招いたものも多い
  • コアライブラリ言語機能 の進化は、現場のニーズや開発者の声を反映しつつある
  • 今後も シンプルさとパワフルさの両立 に期待

Hackerたちの意見

アノテーションの影響を過小評価してると思う。個人的には、アノテーションがアプリケーションを暗黙的に結びつける使い方はあまり好きじゃないけど、その影響は認めざるを得ないね。ポジティブな使い方と極端にネガティブな使い方があるから、5/10が妥当かも。これらの機能の多くは、他の言語で実証された後に採用されたものだし。Javaはかなり慎重なアプローチを取ったから、もっと洗練されたデザインになると思ってたのに、ストリームみたいなものは以前の開発より劣ってる結果になっちゃった。ほんと残念。今のJavaは、美しさや魅力が全くないフランケンシュタインの怪物みたいだね。

スプリングの方向性について冗談を言ってたんだけど、アプリがボイラープレートのメインメソッドと何十行かのアノテーションだけになるんじゃないかって。実際にそれを目の当たりにしたことがあって、db2からrabbitmqに更新を送るアプリがあったんだけど、アプリはアノテーションによる設定だけで、通常のスプリングのメインメソッド以外はJavaコードが全くなかったんだ。

本当に不思議だよ。どうして他の人たちがもっと良いものを作っているのに、わざわざ劣ったものをデザインするのか理解できない。理由は何だろう?言語全体との一貫性?でも、それってそんなに重要なのかな。特定のコンパイラの部分に手をつけたくないだけなのかな?

Javaが実証済みの機能を使ってるとはあまり感じないな。例えば、チェック例外を使ってるけど、あれは実証済みの機能には見えない。C++はチェックなしの例外を使ってるし、ほとんどの人気言語もそうだ。Javaはチェック例外を選んだけど、今では開発者にほとんど無視されてる。これは完全に失敗だと思う。ストリームもいい例だね。コレクションのための関数型APIを作るのはかなり簡単なのに、彼らは簡単な並列化のためにストリームをデザインすることにした。これが非常に複雑な実装につながって、めちゃくちゃ難しくなった。今までこの機能の使い道に出会ったことがないし。だから、非常に稀な機能のためにデザインをめちゃくちゃ複雑にしちゃった。モジュール…笑。グリーンスレッドがどう機能するか見てみよう。ほとんどの言語はもっとシンプルなasync/awaitアプローチを採用してるし、グリーンスレッドを実装してる言語はごく少数だね。

それはちょっと恐ろしいね。フレームワークの設計者たちが、言語がアプリのロジックを表現するには力不足だと感じて、自分たちのカスタムな動作を上乗せしたってことだから。明確なコードは、誰が見ても読みやすく理解できるように努力すべきだし、受け入れられるコードは、技術について少し知っていて、IDEのサポートがあれば理解できるべきだよ。ゴミコードってのは、実際に動かさないと理解できないコードのことだね。メタデータに基づいてフレームワークのロジックを使って、ものをつなぎ合わせてるから。

OpenJDKはそんな風には考えてないよ。彼らは、自分たちが提供する機能は他の言語からの影響を受けているにしても、せいぜい軽いものであると思ってる。傲慢からではなく、実用的な観点からね。他の言語の機能をそのままJavaにコピー&ペーストするのは、フランケンシュタインを作るようなものだから。例えば、Javaはラムダ構文があって、ラムダが必ずコンパイル時に何らかの「関数型」として解釈されなければならないという点で、ちょっとユニークなんだ。関数型とは、正確に1つの未実装メソッドを定義するインターフェースのこと。JVM上で動くScalaを含むほとんどの言語は、ラムダを関数として記述する型階層を作っていて、Scalaの場合はコンパイル時に自動的に「ボックス」や「キャスト」することもある。つまり、Javaのアプローチは独特なんだ(私の知る限りでは)。当時、他の言語のように機能を実装する代替提案もあったけど(BGGA提案)、それは拒否された。君のコメントの核心的な問題はこれだね。「洗練された」と「エレガント」とは何かを定義してみて。簡単そうに聞こえるけど、言語の機能は非常に異なるメロディに合わせて踊ろうとしていて、一人の人にとっての「エレガンス」は、別の人にとっての「フランケンシュタインの怪物」なんだ。同じことは「美しさ」や「魅力」にも当てはまるけど、もし大雑把に言うなら、ほとんどの人が「魅力的な」言語については大まかな共通理解を持っていると思う。そういう言葉に当てはまるメインストリームの長期的に人気のある言語は知らないし、それは本質的なものだと思う。言語がメインストリームであるためには、非常に安定している必要がある。単に言語Xでクールな新しいおもちゃを作っているだけではなく、多くの金や目が関わるプロダクションコードを書いているから、そのソフトウェアが動き続けることに本当に依存しているなら、安定性が必要不可欠なんだ。安定性があると、手枷が付くことになる。『非推奨』を使うのは極めて控えめにしなければならないし、基本的には使わないべきだよ。それには後続の影響がある。新しい機能をテストすることもできないから。今のところ、from future import ...のようなシステムの上で本当に繁栄している言語は見たことがない。それには理由がある。エコシステム全体が未来の機能を採用して、もしそれが壊れたら同じような頭痛を引き起こすか、そうでなければ、そういう機能を使わないかおもちゃのようなものにしか使わないから、導入から得られる経験はほとんどない。言い換えれば、Javaがフランケンシュタインなら、JavaScript、C#、Python、Ruby、Scalaも同じだよ。そうならざるを得ない。私は、特にそれを防ぐことに100%焦点を当てた言語のコアデザイン原則を見てみたい。これまで見たことのない、言語自体のバージョン管理に関する極端なアプローチだね。どんなものかはわからないけど、ここで見たい努力をした言語は思い出せない。このために必要なことのほんの一部を挙げると、* 言語自体がバージョン管理されていて、すべての以前のバージョンが言語仕様の一部として維持され、将来のコンパイラによってもメンテナンスされ続けること。少なくとも長い間、できれば永遠に。 * すべてのソースファイルは、どのバージョンの言語を使っているかを示す必要がある。 * コアライブラリもバージョン管理されていて、別々に。新しいバージョンは古い言語バージョンに対して書かれるか、古い言語バージョンのソースから使える。 * システムのコンパイラやツールは、基本的に「プロジェクト」レベルの粒度で動作する。個々のソースファイルをコンパイルすることはできない。できる場合は、その行為によって一時的な無名プロジェクトが暗黙的に示されることが仕様で説明されている。 * すべてのバージョンには移行ツールが付属し、言語バージョンX用に書かれたソースを自動的に言語バージョンX+1に「更新」し、問題を引き起こす可能性がほぼゼロのものを自動的に適用し、自動更新ができない非推奨の使用法を明示的に修正するようにプログラマーを導く。 * 言語は本質的に「ファサード」をサポートしていて、バージョンYのライブラリがバージョンX(XはYより古い)のAPIを、Yのデータ構造を使って公開できるようにする。これにより、バージョンXのコードベースとバージョンYのコードベースの間で相互運用が可能になる。その言語は、「エレガント」、「シンプル」、「メインストリーム」、「真剣なプロジェクトに適している」、「実際に良い」という、他では不可能な仕事を管理できるかもしれない。

その通り。著者はたぶんSpringや、他の依存性注入フレームワークに触れたことがないんじゃないかな。アノテーションを使うことで、全く違う方法で物事を進められて、たくさんのボイラープレートを省けるんだ。アノテーションには少なくとも9/10をあげるよ。(このレベルの知識があると、記事の他の部分には興味を失っちゃうけどね。)

評価がめちゃくちゃだね。Jshellが6/10?基準は何?

わからないけど、魔法みたいな機能をあまり使わないプロジェクトで働いたことがあって、正直言ってそれは悪化したと思う。設定クラスと@Inject Config config;があるアプリの代わりに、巨大な*Configクラスがあったんだ。それぞれに@Bean public fooProducer(FooConfig config, BazProvider provider, BarProvider barProvider, SoapClient soapClient) {...}みたいなメソッドがたくさんあった。どうやって作られたか知りたい?クラスのコンストラクタの使用例を探してみて。魔法の@Injectや@Autowiredのアノテーションは、それよりも悪くないと思うんだ。

リストは全体的にかなり単純化してるけど、アノテーションについてはその通りだね。必須の個人的エピソード:私はJavaの専門家じゃないけど、99年からJavaに関わってて、数年前に厳密にJavaのチームに移動したんだ。紹介の際に、私のパーティートリックを見せようと思って、一日で擬似「wolf3d」を実装したんだ。いつものように、Javaの開発者たちはJavaでグラフィックスやユーザー入力ができることにちょっと感心してた。今ではそれがほとんどの人にとって非常に珍しいからね。承認をもらった代わりに、彼らにSpringの一日深掘りをお願いしたんだ。結局、基本的なHello Worldプロジェクトが提示されたんだけど、それはほとんどが… 空のクラスだった。文字通り、class Foo {} EOF! もちろん、これらの空のクラスにはクラス宣言の上に少なくとも5行のアノテーションがあったし、最終的にはフレームワークを正しい方向に押しやったけど、マジで、私はその週ずっと気持ち悪かった。

ごめん、嫌わないでね(疲れてて、他にやることがないんだ) https://files.catbox.moe/ge4el3.png

いや、これめっちゃいいね!

モジュールに対する嫌悪感、なんでそんなに強いの?このスレッドの皆がほぼ一様に嫌ってるみたいだけど、理由がわからない。

これを作ってくれて本当にありがとう!

ありがとう!これ、元の投稿に入れるべきだよ。

著者はJavaのアサーションを軽視してると思う。私はこの機能が好きで、Javaがうまくやった数少ない機能の一つだと思ってる。構文がとても表現力豊かで、失敗したときに意味のある例外を簡単に生成できるのがいいね。それに、言語に不変チェックを追加する標準的な方法を提供してくれるのも便利で、プロダクションでは削除できるけど、テストやデバッグのときには実行できる(-daと-eaの違い)。if文を使って似たようなことはできるけど、そうするとビジネスロジックと不変チェックを区別するのが難しくなるだろうし、各自が独自のトグルを実装することになりそう。

彼がアサーションはプロダクションコードに見られないと言ったのにはかなり驚いた。ほんとにそうなの?私はあまりJavaを書かないけど、Cのコードではアサーションを(プロダクションコードで)常に使ってるよ。関数に2、3個のアサーションが含まれるのは珍しくないし。

これをキーワードにすることの利点は、標準関数にするのと比べて何なの?

コレクションに対して厳しすぎると思う。置き換えたものがどれだけひどかったかも考慮しないと。> Java Time: 前のものよりはずっと良いけど、正直このAPIをあまり使ったことがないから、本当にどれくらい良いのか判断できない。前のバージョンがどれだけひどかったかは、過小評価できないよ。とはいえ、私は今でもjoda timeを使ってるけどね。

「前のバージョン [のJava時間] がどれだけひどかったかを過小評価するのは難しい。」 元々のJava Timeクラスは、Javaにとって最後の瞬間の追加だった可能性が高い。明らかにC言語のtime.hを直接コピーしたものだ。Javaチームがこんな会話をしていたように感じる。「やばい、1ヶ月後にJava 1.0を出荷するけど、時間関数を含めるのを忘れた!」 「ああ、どうしよう!何かしなきゃ!」 「そうだ、Cのtime.hを移植しよう!」

私の中では、Javaは1.5でコレクションとジェネリクスが追加されてから本当に使いやすくなった。コレクションがなかった時は、本当に面倒だった。でも、追加された後も、コレクションから取り出すときには、元の型にキャストしなきゃいけなかった。それもまた大変だった。ジェネリクスが「正しく」実装されていないという議論は知ってるし、問題にも直面した。でも、彼らがあることに本当に感謝してる。

JVMでのストリームのやり方はもっといい方法があると思うけど、Scalaみたいな素晴らしい例もあるし、実装がどんなに不完全でも、ストリームは本当にプラスの要素だから、言語にストリームがないなんて考えられないよ。Goを書くときはストリームAPIが恋しいな。

例外に関する批判には完全に同意するよ。ストリームの中で例外が必要になると、めちゃくちゃになる。全体的に君に同意する。例外がないよりは、少し冗長でもずっと良い。古いループを小さなストリームで整理するのが好きだし、同じことをもっとコンパクトで読みやすく表現できる。彼が言ってる並列の利点が実際に使われているのを見たことがないのも正しい。

ああ、Java。私が愛せなかった言語。オブジェクト指向の「キャンプ」時代にプログラミングを始めたんだ:Eiffel、Smalltalk、CLOS、C++など。95年頃から98年頃までのJavaは、巨大なバックドラフトのようだった。他のすべてのものから空気を完全に吸い取ってしまった。プログラミング言語のためのWSJの全面広告を覚えている人いる?誰もそれが本当に何なのか知らなかった頃のやつ。だから、私のJavaに対する最初の印象は感情的で非合理的だったし、「もちろんJavaは動くよ、だって新しいことは何もないから」ってコメントに強化されていた。— James Goslingだけど、これが都市伝説かもしれないとずっと疑っていた。「Javaは、C++の構文のエレガンスとSmalltalkのスピードを兼ね備えている」— Kent BeckかJan Steinman。「20年後、私たちはまだJavaについて話しているだろう。それはコンピュータプログラミングへの貢献のためではなく、言語をマーケティングする方法の実証として」— ?? 今日、私はJavaで少しコードが書けるけど(だって、GPTや友達がいるから! :))、Kotlinを使うことに決めて、まあまあ満足してる。これに関して興味深いのは、プログラマーが使う実際の計算モデルを変えた変更を分解することだね。文法的な糖衣やライブラリの洗練とは別に。「こういう重い足跡を持つ言語」は、実際に結果を計算する方法と同じくらい、実行時ライブラリやフレームワークに関するものでもあるから。

今日はちょっとJavaでコーディングできるよ(だって、ほら、GPTと仲間たちがいるからね!! :) )GPT大好き。ほんと素晴らしいツールだよ。ChatGPTが登場する前は、医療の経験なんて全然なかったんだ。GPTと仲間たちのおかげで、今は医者になったよ。自分のクリニックも開業したんだ。

Doug Leaのjava.util.concurrent(ここで10/10の評価を受けた)のクールなところは、そのデザインがPythonのconcurrent.futuresパッケージにも影響を与えたことだね。これはPEP 3148[1](「理由」の下)で明示的に認められていて、2009年に遡るPEPなんだ。 [1]: https://peps.python.org/pep-3148/

だから、Pythonのconcurrent.futureパッケージを使うときにJavaを思い出すんだ。

機能を振り返るのはいいけど、Javaの歴史って機能やプログラマーの人気だけじゃないんだよね。(1) それは最初の破壊的なエンタープライズビジネスモデルだった。彼らは、労働コストを下げるために、誰でもJavaプログラマーになれるように無料でアクセスできるようにしたけど、エンタープライズ(や組み込み、ブラウザ)のVMやコンテナには料金を取った。これは、しっかり根付いていたMicrosoftやIBMを下回るためだった。(IBMはすぐに高級IDEを捨てて、無料のEclipseをサポートし始めた。これにより、Borlandや他のIDEメーカーが自分たちのライブラリやプログラミングモデルを結びつけていた競争が壊れた。)(2) インタープリタ言語として、Javaは良いJITがなければ実用的ではなかった。Borlandのは最初のもので(JDK 1.1.7で)、その後すぐにUCSBの教授であるUrs HolzleがHotSpotコンパイラを作り、世代を超えたパフォーマンス向上をもたらした。VMとJITのおかげで、様々な世代のハードウェアを活用し、桁違いの改善を実現し、ソフトウェアをあらゆる製品に組み込むことができた。ハードウェアとソフトウェアを分離することで、顧客を苦しめていた縦の統合が減った(これがSun Microsystemsにも悪影響を及ぼした)。ちなみに、Urs Holzleはその後Googleの社員#8になり、データセンターで大量の並列オフ・ザ・シェルフハードウェアを使うことに貢献した。彼がGoogleの夢を実現させたんだ。

元々のビジネスプランは、Javaをネイティブに実行するCPUを売ることだったんだ。それは速くなるはずだった。そのアイデアはひどく失敗した。

Joshua BlochのJava2のコレクションに関する仕事は、私にとって本当に重要だった。プログラミングを始めたばかりで、名前付けが難しいのは分かっていたから、リファレンスマニュアル以上にシソーラスを使ってた。彼の仕事は、私にとってAPI設計の基礎を築いたんだ。私たちが当たり前だと思っていること、そしてしばしば些細なこととして見過ごしてしまうこと。例えば、putというメソッドを持つコレクションタイプがあるとする。2つの引数を取る - 挿入したいオブジェクトと、挿入するインデックス。どちらの引数を先にするべき?インデックスはオプションにできる?デフォルト値は何にする?関数は何かを返すの?挿入が成功したかどうかを示すブール値?それともオブジェクトが挿入されたインデックス?もし後者なら、エラーをどうやって示す?これらは一見些細なことだけど、彼と彼のチームはそのライブラリに1年以上取り組んで、プレゼンテーションでしっかりドキュメント化していた。彼のJavaパズラーも忘れちゃいけない、あれは本当に素晴らしい。

私の考えでは、Javaの機能についてネガティブになりやすいのは、それが設計された人じゃないからだと思う。例えば、モジュールシステムの主な「顧客」はJDKそのものだし、NIO/2の主な顧客はNettyみたいな低レベルのライブラリなんだ。現代のJava言語の進化に関する哲学に興味がある人には、ブライアン・ゲッツの「Growing the Java Language」ってトークを超おすすめするよ。タイトルに惑わされないで、これはJavaだけの話じゃなくて、ソフトウェアデザインについてなんだ。[1]: https://www.youtube.com/watch?v=Gz7Or9C0TpM

そうだね、モジュールはエンドユーザー向けじゃない、少なくともほとんどはね。

モジュールは大きく壊れたから-10だね。

例えば、モジュールシステムの主な「顧客」はJDKそのものだ そうそう、TFAに書いてあったけど、「一般的なアドバイスは、モジュールは(あるべき)JREの内部詳細であり、アプリケーションコードでは無視されるべきだ」と。だから、なんで「主な顧客」でない人たちにそれを見せる必要があるの?