Java

2023/10/14

パッケージ原則とクラス原則の違いは何なのか

パッケージ原則とクラス原則の違いについて考える時があった。
直感的なラフなメモ。

【参考】
パッケージ設計の原則を本で学ぶ - Qiita

イラストで理解するSOLID原則 - Qiita

パッケージ原則を見ると、パッケージの凝集度3つ、結合度3つの観点に分けられる。
その内容は、クラス原則であるSOLID原則をパッケージ版に拡張したように思える。

実際、単一責任の原則はパッケージの凝集度に関連するし、開放閉鎖原則はパッケージ版も同様だ。
インターフェイス分離の原則やLiskocの置換原則は、パッケージの結合度に関連してくる。

ただし、その違いはある。
1点目は、パッケージやコンポーネントの観点では、リリースできる単位が再利用できる観点と同一になる原則が最も重要になる。
理由は当たり前で、リリースして他ユーザが使えるようにするからには、他の人が再利用できる観点と同一にならざるを得ないから。

リリースモジュールは単独で動くものである一方、クラスは単独では動作できず、複数のクラスを結合して初めて一つの画面や帳票、バッチなどの機能を形成する。
この違いは大きく、再利用の観点も他者に利用してもらえる観点と、他者に読んで保守してもらえる観点の違いになる。

もう1点は、パッケージとクラスを保守する人の単位が異なる点だ。
クラスを修正する人は基本的には1人であり、コードのレビューアも関わるだろうが、1人が責任を負う。
その修正履歴はコミット履歴に残る。

一方、パッケージを保守する単位はチームや部署の単位になる。
普通は、リリースモジュールの単位はサブシステムであるから、システムごとに担当する部署があるだろう。
大規模な基幹系システムを持つ企業であれば、数多くの部署ごとに担当するシステムが異なるだろう。
つまり、サブシステムというリリースモジュールを保守するのは複数人であり、アプリ層だけでなくフロント層、インフラ層など数多くのメンバーが関わって初めて一つのシステムが動く。

すると、システム単位に開発チームが存在するので、コンウェイの法則に従うことになるだろう。
つまり、アーキテクチャ(システム)は組織に従うわけだ。
この特徴により、サブシステム間が部署間の壁となり、IFとなるので、IF連携は認識齟齬が起きやすいリスクが発生しやすい。
部署ごとにシステムが分かれることにより、システムは局所最適化されやすくなる。

そんなことを考えると、パッケージ原則とクラス原則は同じような観点が多い一方、クラス担当者やシステム担当チームの違いを認識することも必要だろうと思う。

| | コメント (0)

2023/09/30

パッケージ設計の原則の意義は変化しているのか

パッケージ設計の原則についてちょっと議論する場があり、色々考えるものがあった。
考えたことをラフなメモ書き。

【参考】
パッケージ設計の原則を本で学ぶ - Qiita

パッケージ設計6つの原則~ポイントは関連性/依存性/抽象度 | プレイン・プログラム

コンポーネントに関する6つの原則 - Qiita

【1】パッケージ原則6個の復習。

1. パッケージ再利用等価原則 Release Reuse Equivalency Principle
* 再利用の粒度はリリースの粒度と同じ
2. 共通閉鎖原則 Common Closure Principle
* クラスは共に変化し、共に存在する
3. 共通再利用原則 Coomon Reuse Principle
* 共に再利用されないクラスを同じグループに入れるべきではない
4. 非循環依存関係原則 Acyclic Depenedencies Principle
* パッケージ間の依存関係は循環してはいけない
5. 安定依存関係原則 Stable Dependencies Principle
* 安定している方向に依存する
6. 安定抽象原則 Stable Abstracttions Principle
* 安定したパッケージは抽象的であるべきだ

【2】パッケージ再利用等価原則が最も基本の原則と考える。

ちょっと昔のJavaシステム開発であれば、リリースモジュールwar/earがリリースの単位であり再利用の単位になる。
サブシステムごとにwar/earをビルドして検証環境でテストした後、オンプレの複数台のサーバー環境ごとに、複数のAPサーバーへwarを手動でデプロイしていた。
ロードバランサーからデプロイするサーバーを切り離して、1つずつwarファイルをデプロイして起動確認し、確認OKならば外部通信できるように設定していた。
つまり、そういう面倒な手動のリリース作業があった。

オンプレのサーバ環境にwar/earファイルをデプロイする単位は普通はサブシステム単位なので、そういう粒度で再利用しやすくする。
その場合、war/earファイルはできるだけサイズは小さい方がAPサーバ再起動時間も短くなるし、リリース作業も短くなるので、リリース作業ミスの確率も減らせる。

特に最近はAWSのようなクラウド環境では、サーバ環境そのものを使い捨てみたいにコンテナから自動生成するので、コンテナ(リリースする単位)のサイズが小さいほどサービスの再起動時間が短くなり、サービス停止時間も短縮化でき、顧客満足度も高くなる。
JavaGoldを取得した時に、モジュールの話で、モジュールサイズをできるだけ小さくしたい要望がある理由がそこにあると聞いて納得したことがあった。

クラウド上の開発がJavaに与えた影響は何なのか: プログラマの思索

Javaのモジュールシステムは複雑性をより増している: プログラマの思索

Javaのモジュールシステムの考え方をまとめてみた: プログラマの思索

【2】ただし、リリースモジュールの保守性や分割についてトレードオフがある点は理解できる。

実際、サブシステム単位にリリースモジュールwar/earをビルドする場合が多いので、普通はリリースモジュールのファイルサイズは非常に大きくなりがちだ。
なぜなら、リリースモジュールには、Apacheの共通ライブラリ、会社特有の共通ライブラリなどのJarという共通コンポーネントを多数含んでいるからだ。
同様に、RailsのようなWebアプリでも、bundlerの中には共通ライブラリGemを多数含んでいる場合が多い。

すると、リリースモジュールのファイルサイズを小さくしたくなる。
簡単に思いつくのは、リリースモジュールから他のサブシステムと共通で使う共通ライブラリは別出しして、デプロイする時は共通ライブラリは再リリースしなくて良いようにしたい。
APサーバ上に共通ライブラリを別で配置して事前ロードしておいたり、別APサーバ上に共通ライブラリを配置するケースも考えられるだろう。

メリットは、リリースモジュールのうち共通ライブラリは既にAPサーバにデプロイされているので再リリースは不要であり、リリースモジュールのサイズを小さくできる。
その分、ビルド作業時間、リリース作業時間を短縮でき、リリース作業ミスのリスクも減らせるだろう。

一方、デメリットは、共通ライブラリに手を加えた場合、既にデプロイ済みのサブシステムのwar/earファイルに影響が発生してしまう。
デグレがないか事前確認が必要だし、共通ライブラリがサブシステムのAPサーバとは別APサーバにデプロイされていて、共通ライブラリが複数のサブシステムから呼び出されているならば、複数のサブシステムに影響が発生してしまう。
共通ライブラリのリリース作業中にAPサーバを停止する事態が発生すれば、呼び出し側の複数のサブシステムで業務停止してしまうリスクが発生する。

また、共通ライブラリにサブシステムA向けのAPIを追加したり改修して、他のサブシステムB向けのAPIは触らない場合であっても、共通ライブラリをリリースする時にサブシステムAもBにも影響が発生してしまう。

だから、一般には、サブシステムごとに共通ライブラリを含んでリリースモジュールをビルドする場合が多いと思う。
すると、たとえば、サブシステムAの共通ライブラリXのバージョンは1.1、サブシステムBの共通ライブラリXのバージョンは1.2、みたいにコンポーネントのバージョンがサブシステムごとに違ってくる場合も発生するだろう。

つまり、サブシステムで利用する共通ライブラリのバージョン管理、構成管理が重要になってくる。
この仕組みがMavenであり、Railsならbundlerなのだろうと思う。
ライブラリやコンポーネントの構成管理というソフトウェアの複雑性をビルド管理の仕組みで補っているわけだ。

【3】では、昨今のアジャイル開発、DevOps、クラウドなどにより、パッケージ設計の原則の意義は変化しているのか?

メタな観点ではパッケージ設計の原則の意義は変わらない。
リリースモジュールは再利用できる単位であることは変わらないし、モジュールの分割方針やデプロイ方針も基本は変わらない。
しかし、具体的なリリース手順や開発プロセスは影響を受けていると思う。

例えば、SaaSビジネスでは、リリース作業時間は極力短くしたい。
リリース作業時間は広義の意味では、顧客に機能を提供するリードタイムと同じ。
BtoCのSaaSビジネスならば、機能改善の要件定義からリリースまでのリードタイムを短縮化する事は売上に直結する。
ちょっとした機能改善を即座にリリースできれば、顧客満足度も上がり、ユーザ数増加が売上につながるから。

また、特に昨今はクラウドでサーバーごとの仮想化するなどして丸ごとコンテナ化し、コンテナを使い捨てみたいにいくらでもデプロイできるから、リリース作業時間はできるだけ短くしたい。

これは、DevOpsの考え方と非常に相性が良いと思う。
DevOpsで開発チームがシステム運用と一体化したプロセスになるし自然にアジャイル開発になるはず。
つまり、アプリ開発者はアプリも開発するし、クラウド上でインフラ基盤もサーバー基盤もコンテナをプログラム化して自動配置できるようにすれば、開発も運用も一体化できるはず。

そして、リリース作業時間と言うKPIを開発チームが毎回計測し監視すれば、改善すべきか評価できるはず。
リリース作業時間、ビルド時間、デプロイ時間などはアジャイル開発の主要なメトリクスの一部と捉えられるだろう。
システム停止やデータ移行の時間も含めてリリース作業時間に3日間かかっていたのを、1時間で終わらせたり、わずか5分で終わらせれば、その分システム停止時間も短くでき、顧客の業務や顧客の操作時間への影響を減らせる。

そんなことを考えると、アジャイル開発やDevOpsという考え方は、サーバの仮想化やクラウドの技術のおかげで進化している部分も大きいのだろうと思う。
こんなことは既に当たり前の考え方と思うけれど、アプリ層の設計技法もインフラ基盤の仮想化技術に相当影響を受けているのではないか、と思う。

| | コメント (0)

2023/02/01

ChatGPTにEclipseでEclEmmaとJaCoCoからカバレッジを出力する方法を聞いた

15年以上前にEclipseでdjunitでカバレッジを出力していろいろ試していた。

dJUnitでカバレッジテスト: プログラマの思索

しかし、今のEclipseではdjunitはもう使えなくなっていた。
ChatGPTに聞いたら、代わりにEclipseでEclEmmaとJaCoCoからカバレッジを出力する方法がわかったのでメモ。

【参考】
EclEmma - Java Code Coverage for Eclipse

EclEmma - JaCoCo Java Code Coverage Library

EclEmma - Source Code Annotation

ブログエンジン(5) Maven+JaCoCoでカバレッジ計測 - afnf.net

WildFly+jacocoでintegration-testのカバレッジ取得 - Qiita

カバレッジレポートの出し方について書く ・ Issue #10 ・ syobochim/doc

JaCoCo - Command Line Interface

複雑度と単体テストケース数の相関関係: プログラマの思索

【前提条件】
Eclipseの実行メニューに「カバレッジ>Junit」が表示される前提とする。
表示できていれば、EclipseにEclEmma プラグインがインストールされている。
最新版のEclipseであれば既にインストール済み。

Eclipseにカバレッジ・ビューを表示するように設定するには、ウィンドウ>ビューの表示>その他 から、java>カバレッジを 選択して画面下部に表示するように設定する。

JaCoCo のZipファイルをダウンロードしておく。

EclEmma - JaCoCo Java Code Coverage Library

【EclipseにJacocoを設定する方法】
この手順書は、ChatGPTに質問して作成した。

EclipseにJacocoを設定する方法は次のようになります。

Eclipseで、JaCoCoを使用するプロジェクトを選択します。

[Run] -> [Run Configurations...]を選択し、「JUnit」に対して新しい設定を作成します。

「Arguments」タブで、「VM arguments」欄に「-javaagent:path/to/jacocoagent.jar=destfile=path/to/jacoco.exec」を追加します。

path/to/jacocoagent.jarは、JaCoCoがインストールされている場所に置き換えてください。
path/to/jacoco.execは、JaCoCoのエグゼキューションファイルを出力する場所を指定します。
「Apply」をクリックし、「Run」をクリックして、テストを実行します。

実行が終了したら、「Coverage」ビューを開き、カバレッジ結果を確認します。

注意: 上記の設定は、JUnit4を使用している場合の設定方法です。JUnit5を使用する場合は適切なアダプタを使用する必要があります。

【EclipseとJacocoを使って、カバレッジのレポートをHTMLで出力する方法】

Eclipseで、JaCoCoを使用するプロジェクトを選択します。

[Run] -> [Run Configurations...]を選択し、「JUnit」に対して新しい設定を作成します。

「Arguments」タブで、「VM arguments」欄に「-javaagent:path/to/jacocoagent.jar=destfile=path/to/jacoco.exec」を追加します。

path/to/jacocoagent.jarは、JaCoCoがインストールされている場所に置き換えてください。
path/to/jacoco.execは、JaCoCoのエグゼキューションファイルを出力する場所を指定します。
「Apply」をクリックし、「Run」をクリックして、テストを実行します。

実行が終了したら、JaCoCoのアクションツールを使用して、jacoco.execからHTMLレポートを生成します。

JaCoCoのアクションツールは、Javaコマンドラインから次のように実行できます。

java -jar path/to/jacococli.jar report path/to/jacoco.exec --classfiles path/to/classfiles --sourcefiles path/to/sourcefiles --html path/to/report

path/to/jacococli.jarは、JaCoCoのアクションツールを含むjarファイルへのパスです。
path/to/jacoco.execは、JaCoCoが生成したエグゼキューションファイルへのパスです。
path/to/classfilesは、JaCoCoを適用するクラスファイルへのパスです。
path/to/sourcefilesは、JUnit対象ソースコードへのパスです。
path/to/reportは、HTMLレポートを生成する場所を指定します。index.htmlを開けば出力結果が表示されます。

注意: 上記の設定は、JUnit4を使用している場合の設定方法です。JUnit5を使用する場合は適切なアダプタを使用する必要があります。

JaCoCo - Command Line Interface

【感想】
ChatGPTに明確な質問を書けば、的確に回答してくれる。
手順は必ず回答があるから、AIが強いだろうと思う。

WildFly+jacocoでintegration-testのカバレッジ取得 - Qiitaによれば、Missed InstructionsがC0 (命令網羅率) でMissed BranchesがC1 (条件網羅率) を表しているとのこと。
ドリルダウンすると、カバレッジを通過したパスは、
緑:実行された命令。
赤:実行されなかった命令。
黄:分岐において一部のみ実行された命令。
で表示してくれる。

カバレッジ結果のHTMLを開くと、C0/C1カバレッジをいい感じで出力してくれるのでありがたい。

Jacoco_report1

Jacoco_report2

Jacoco_report3

| | コメント (0)

2022/12/25

JavaGold SE11の感想

JavaGold SE11を無事に取得できた。
試験勉強を通じて、有意義な経験を得られたと思う。
ラフなメモ書き。
間違っていたら後で直す。

【参考】
Java歴13年がJava Gold(SE 11)を受けてみた - yucatio@システムエンジニア

JavaGoldSE8に受かったので(前編) - Qiita

JavaGoldSE8に受かったので(中編) - Qiita

JavaGoldSE8に受かったので(後編) - Qiita

JavaSilverの感想~Javaはオブジェクト指向と関数型言語の2つの性格を持つ: プログラマの思索

Javaはなぜ関数型言語になろうとしているのか: プログラマの思索

【1】受験のきっかけ
コロナ禍がずっと続いて、リモートワークになりプライベートでも対面の付き合いがほとんどなくなって、余った時間を有効活用したい。
過去にJavaで開発していた頃はせいぜいJava5くらいで、それ以降のバージョンは完全に理解できていなかったので、ラムダ式等をきちんと習得したい。
15年以上前にSunのJava試験を受けて落ちたのでリベンジしたい。

【2】勉強方法
オラクル認定資格教科書 Javaプログラマ Gold SE11(試験番号1Z0-816)」(通称、紫本)と『徹底攻略Java SE 11 Gold問題集[1Z0-816]対応』(通称、黒本)を最終的に5回転以上回した。
黒本だけでは理解できず、紫本を5回転以上回して慣れる必要があった。
なお『徹底攻略Java SE 11 Gold問題集[1Z0-816]対応』の「総仕上げ問題」は実際の試験問題に似ているのでやるべき。

【3】JavaSilverは「Javaはオブジェクト指向である」ことを設計でもコーディングでも理解できれば合格できる。
しかし、JavaGoldは「Javaは関数型言語である」こと、特にラムダ式を使って設計でもコーディングでも理解する必要があった。
頻出論点は、ストリームAPIと関数型インターフェイス、モジュールシステムだったと思う。
僕には正直難しかった。
理由は、オブジェクト指向プログラミングはUMLによるモデリングを通じて経験していたが、関数型言語プログラミングのお作法もその概念も理解していなかったから。
「OptionalはMaybeモナド、ストリームAPIはMapReduceでありリストモナド、入出力APIはIOモナド」ということを最終的に理解する必要があるのではと思った。

【4】 第1章 クラスとインタフェース

なぜ内部クラス(inner class)が必要なのか?
2つのクラスソースファルを1つにまとめる必要がある時に使う。

内部クラス→static内部クラス→ローカルクラス→匿名クラス→ラムダ式 の順に昇華されていく。
よって、ラムダ式の発端は内部クラスにある。

インナークラスのインスタンス化には、エンクロージングクラスのインスタンス化が必要である。
つまり、無意味なインスタンス化のロジックが発生する。
そこで、インナークラスをstaticインナークラスにすれば解決できる。
staticインナークラスならば、エンクロージングクラスのインスタンス化無しで、インナークラスのインスタンス化ができる。
staticインナークラスにすれば、staticなインナークラスのフィールドやメソッドをエンクロージングクラスでそのまま参照できる。

staticなインナークラスでは、フィールドやメソッドの参照はコンパイルエラーになる。
一方、staticでないインナークラスでは、フィールドやメソッドを参照できる。

Effective Javaでは、「内部クラスにはstaticを付けるべき」というプラクティスがある。

内部クラスにstaticを付けると、内部クラスから外部クラス(エンクロージングクラスとも言う)のフィールド変数にアクセスできなくなる。
つまり、スコープが小さくなる。

ローカルクラスから参照するローカル変数は、実質finalが必須。
実質finalでなければコンパイルエラーになる。
ローカルクラスから参照するローカル変数を、ローカルクラスの後で変更するとコンパイルエラーになる。
この仕様は関数型言語のイミュータブルな性質に似ているので、そのような仕様にJavaが合わせたのではないか。

一方、RubyやPythonのクロージャでは、ローカル変数を変更しても問題なく動く。
Rubyのブロックはメソッドによる手続きブロックとは異なって、ブロックの外側で定義されたローカル変数をブロック内で参照・変更できるという性質を有する。ただブロック内で定義された変数はその外側で参照できない。

無名クラスは、メソッド内に無名のインナークラスを定義するので、ラムダ式。と実質同じ。
Effective Javaでは、「無名クラスよりもラムダを選ぶ」プラクティスがある。
無名クラスは冗長な記述であり、ラムダ式の方が簡潔に書けるからだ。

匿名クラスにコンストラクタは定義できない。
しかしインスタンス初期化子を使えば、似たような処理を行うことが出来る。

インターフェイスのdefault/privateメソッドは、Scalaのtrait, Rubyのmoduleと同じ。
ただし、インターフェイスを継承した2階層下のクラスや2階層下のインターフェイスでは使えない。
インターフェイスのstaticメソッドは、継承直下の子クラスのみ利用できる。
privateメソッドはdefaultメソッドから呼び出されることを前提としている。
インターフェイスは型の提供が目的であり、メソッドの実装は基本は実装クラスが提供する。
インターフェイスのstaticメソッドとラムダ式のおかげで、Factroyクラスが不要になった。

なぜJavaのインターフェイスがのstatic/default/privateメソッドを必要とするのか、理由が分からなかった。
たぶん、Scalaのtrait, Rubyのmoduleを真似て、共通ロジックをわざわざインスタンス化して利用する面倒な手続きを減らしたい意図があるのだろう。

Enumはシングルトンクラス。
Effective Javaでは、Enumに独自に新規メソッドを実装するプラクティスがある。

Javaのenum型はシングルトンクラスみたいだ: プログラマの思索

Effective Java 第3版 第6章enumとアノテーション - Qiitaでは、int型をEnu型で書くべき、というプラクティスは理解できる。
しかし、「拡張可能なenumをインタフェースで模倣する」プラクティスのように、通貨クラスに「+」「-」のようなメソッドを独自で実装するという発想はなかった。
こういう考え方がドメイン駆動設計の値オブジェクトの実装に役立つのだろう。

【5】 第2章 関数型インタフェースとラムダ式

Javaのラムダ式は、関数型インフェーエイスの宣言とインスタンスの生成を同時に行う文法とみなす。
つまり、ラムダ式=関数型インターフェイス(抽象メソッド1個だけ)+インスタンス生成。

ラムダ式を使えば、Factroyクラスは不要になる。
つまり、DIを実現するために、Factoryパターンを使い、その実装にラムダ式を使う。
ラムダ式を使えば、Strategyパターンを短く書ける。
ラムダ式をメソッドチェーンで書けば、Builderパターンを短く書ける。

Javaのラムダ式のローカル変数は、スコープ外では実質finalとして扱われる。
ローカル変数の値を更新するとコンパイルエラーになる。
この性質は関数型言語の特徴に似ていると思う。

@FunctionalInterfaceを付けたインターフェイスは、自動的に関数型インターフェースとして使用できるが、抽象メソッドは1個だけ限る。
他言語では関数1個で定義できるが、Javaは必ず関数をclassで囲む必要があるので、関数型IFで定義する必要がある。

Javaの関数型インターフェースの抽象メソッドで注意すべき点は、java.lang.Objectのメソッドはカウントされない。
たとえば、public abstract String toString()があれば、抽象メソッドにカウントしない。

ラムダ式を使うと何が嬉しいのか?
1.抽象メソッドを実装したクラスを最小のコード量で実装できる
2.抽象メソッドの実装と、メソッドを使うところを一つにできる

関数型インターフェイスには1つしか抽象メソッドがないので、戻り値・引数の型と順番を、関数型インターフェイスの型からJavaコンパイラが推測できる。この仕組みが型推論なわけだ。
だから、Haskellのような関数型言語はコンパイラを作りやすいという理由は、型推論が強力な特徴があるからだろう。

通常パターン;
* Function, XXXFunction :apply()
* => 戻り値:R
* Supplier:get()
* => 戻り値:T
* Consumer, XXXConsumer :accept()
* => 戻り値:void
* Predicate, XXXPredicate :test()
* => 戻り値:boolean

特殊パターン:
* ToXXXFuntion:applyAsXXX()
* ToIntFunctionならapplyAsInt()のような感じ。
* XXXSupplierの形だけ注意! => getAsXXX()
* IntSupplier:getAsInt()
* DoubleSupplier:getAsDouble()
* LongSupplier::getAsLong()

UnaryOperator、BinaryOperator=引数と戻り値の型が同じ。
引数と戻り値の型が同じなので、Gnericsは1つで良い。

【6】 第3章 並列処理

並行処理(concurrent):処理を切り替えて同時に動いているように見せる。Threadクラスと同じ。
並列処理(parallel):複数のコアで同時に処理を行う。ストリームAPIでparallelメソッドを使う。

Executor によって処理されるタスクの状態遷移図
created(タスク生成) 
→ submitted(キューにタスクを登録) 
→ started(タスクのrun実行)
→ completed(タスクの終了)

Thread.run()がキューに登録されて、Thread.start()で、run()が実行される。
キューに登録されたRunnable.run()が実行される。

なぜ、並列処理はラムダ式や匿名クラスを使うのか?
スレッドのタスクは、関数型インターフェイスRunnnableのラムダ式で実装されるから。

Future=スレッドを生成したメソッドが、新しく作ったスレッドの結果を保持する。
submitの戻り値=Future => Future.get()で取得できる。
list.add(services.submit( XX -> XX))を実行できる。
一方、executeの戻り値=無し。

Runnable.run()はvoidなので、何も返さないから、Future.get()はNULLを返す。
Callable.run()は戻り値があるので、Future.get()は結果を返す。

Future fut = ex.submit( () -> {処理}, 0); は、第2引数0を返す。

Callableで定義したタスクで例外が発生した時、ExecutionExceptionでキャッチする。

CyclicBarrier=スレッドを待機させる。
java.util.concurrent.CyclicBarrierクラスを利用すると、複数のスレッドが特定のポイントまで到達するのを待機させることができる。
CyclicBarrier=複数のスレッドが特定のポイントまで到達するのを待機できるようにする同期化支援機能を持つスレッドクラス。
スレッドN本目を通過すると、await()の待機は解除される。

* バリアー:複数スレッドの待ち合わせポイント
* バリアーアクション:待ち合わせ後に実行される処理

マルチスレッドで扱うクラスのフィールドにvolatile修飾子を付けると、キャッシュされなくなる。
これにより並列処理時にどちらかがキャッシュを読み込むことによる不整合をなくせる。

【7】 第4章 ストリームAPI

ストリームAPIとは何なのか?
一言で言えば、JavaのMapReduce用APIと思えばいい。
大量データをリストで引数として設置して、Mapで処理させて並列処理できるようにばらして、並列処理の結果をreduceで1つにまとめて戻り値に返す。

実際の問題では引っ掛けパターンが多い。

ラムダ式のローカル変数は実質finalなのに、2回代入している
ただし、ローカル変数が配列ならポインタ参照なので、2回代入はOK。
たとえば、ArrayListやint[]の場合。

ラムダ式を{}で書いたのに「;」なし。

ラムダ式の中で、varの引数はコンパイルOK。
つまり、ラムダ式内の処理の引数は型推論される。
ただし、varはメソッド引数やメンバ変数に使うとコンパイルエラーになる。

Stream<型>のようなGnericsで引っ掛けるパターンが多い。

* map(T -> R)とflatMap(T -> Stream)
* mapToInt()とmapToObject()
* mapToInt(ToIntFunction)とmap(T, R)
* pararell(Stream --> Stream)とpararellStream(Collection --> Stream)
* reduce(BinaryOperator --> Optional)とreduce(DoubleBinaryOperator -->DoubleStream)

終端操作を2回実行すると実行時に例外が発生する。
例:anyMatch, count, reduce, forEach, collect

中間操作だけで終わると、値は返却されない
例:peek

終端操作countだけ実行しても出力されない。System.out.println(count)が別途必要。

List.stream().pararell() <=> List.pararellStream()は同じ。

boxed()は、XXStream -> Streamへ変換する。
逆の操作は、Stream --> mapToInt(IntToIntFunction)--> IntStream。

reduce().orElse()でint型、double型を返す。

Javaのsumはreduceで置き換えられる: プログラマの思索

Javaのreduceの使い方は2種類ある: プログラマの思索

モナドとは「メソッド内の副作用の存在を戻り値の型で表現する」ためのデザインパターン。
「関数に副作用がある」ことを「戻り値の型」で表現している。
Genericsを使って、戻り値の型を増やしている。

なぜ関数型IFではGenericsが頻繁に使われるのか?
理由は、関数の副作用を戻り値の型で表現するために、Genericsを使って戻り値の方の個数を増やしているからと考える。

OptionalはMaybeモナド。
Optionalの語源:値がないかもしれない => オプションの値を作りましょう。

StreamはListモナド。
MapReduceの戻り値はOptionalを返す。
それにより、Nullの戻り値はなく、ヌルポを防げる。

IOはIOモナド。
入出力処理における副作用は、戻り値の型をGenericsで表現する。

StreamAPIはメソッドチェーンで書かれるので、処理の途中でどのように戻り値の型が変換されて遷移するのかわかりにくい。
そこで、StreamAPIはバラして考えるとよい。
Eclipseの「ローカル変数の抽出」を使って、メソッドチェーンをばらすといい。

【8】 第5章 入出力

java.ioパッケージは古い機能。
たとえば、Fileクラスは、ディレクトリやファイルへのパスを扱うだけで、ファイル自身を表すのではない。

java.nioパッケージは新しい機能なので、痒い所に手が届くようになっている。
nio.Filesクラスは新しい入出力API => 同じパスに同じファイルがあれば作成時に例外を発生させて検知してくれる。
NoSuchFileException - ファイルが存在しない場合
DirectoryNotEmptyException - ファイルがディレクトリで、ディレクトリが空でないために削除できなかった場合

FileクラスとFilesクラスの違いは覚える。
Fileクラス提供のisXXXメソッドは、引数を取りません。
* 例:File.isDirectory() : boolean
* 例:File.isFile() : boolean

Fileクラス提供のメソッドは、renameTo(dest)以外はほとんど引数を取りません。
* 例:dirFile.mkdir()
* 例:dirFile.mkdirs()
* 例:file.renameTo?(File dest)
* 例:dirFile.listFiles()
* 例:file.getAbsolutePath()
* 例:file.toPath()
* => new File("ファイル名").toPath()の形式でPathオブジェクトを生成する

Filesクラス提供のメソッドは、全て引数を取ります
これ覚えとくだけで事前にコンパイルエラーかわかるようになった。
* 例:Files.isDirectory(Path dirPath)
* 例:Files.deleteIfExists(Path path) =>ファイルやディレクトリが存在している場合だけ削除する
* 例:Files.list(Path dirpath): Stream =>ディレクトリ内の全てのパスを表示
* 例:Files.find(開始パス、深さ、BiPredicateオブジェクト、オプション): Stream
* 例:Files.isSameFile(path1 : Path, path2 : Path) : boolean : path1とpath2が同じか否か
* Files.move?(Path source, Path target, CopyOption... options)
* Files.copy?(Path source, Path target, CopyOption... options)

Files.walk(), Files.find()は引数を覚えるのが重要。
Filesクラスのwalk()は再帰的にパス情報を取ってくる。
Files.walk(開始パス、深さ、オプション):サブディレクトリまで展開したパスを再帰的に表示
指定された開始ファイルをルートとするファイル・ツリーを参照することで Pathが遅延移入されるStreamを返します。

Filesクラスのfind()は再帰的にパス情報を処理して、判定条件に合致したファイルだけを探す。
Filesクラスのfind()の引数の順番は覚えたほうが良い。
Files.find(開始パス、深さ、BiPredicateオブジェクト、オプション):サブディレクトリまで展開したパスを再帰的に処理して必要なフィルのみ表示
指定された開始ファイルをルートとするファイル・ツリー内でファイルを検索することで Pathが遅延設定されるStreamを返します。

XXのようにStreamがついてなければCharacterストリーム。
XXStreamのようにStreamがついていればByteストリーム。

getDefault()は、FileSystemsクラス, Localeクラスにある。
SystemDefault()はZoneIdクラスにある。

Serializableのtransient修飾子は、シリアライズ対象から外す。
デシリアライズ後はnullになる。
static変数は、シリアライズ対象外になる。
∵static変数はグローバル変数なので値は保持される。

【9】 第6章 JDBCによるデータベース連携

Statement ◇--PreparedStatement, CallableStatement

Statement 単純な実行計画を行いたい時(select * from ... を 1 回だけ など)
PreparedStatement 複数回に渡る実行計画を行いたい時, ? のパラメーター解析を使いたい時
CallableStatement ストアドプロシージャを実行

Statementは静的なSQLを扱い、PreparedStatementはプレースホルダを使った動的SQLを扱えます。
基本は、Statementクラスを用いずにjava.sql.PreparedStatementクラスを使用する。
∵SQLインジェクション対策になる。
∵名前の通り、SQLがDBにキャッシュされるため、繰り返し同じSQL文を発行する場合に処理速度が速くなる。
PreparedStatementはDBMSが理解できるようにSQL文をあらかじめコンパイルするから。

まあ、今ならベタなPreparedStatementで実装することはなく、フレームワークで書くのが普通だろう。

【10】 第7章 汎用とコレクション

Genericsの使い道は2種類しか思いつかない。
ListやMapの型安全。
関数型IFの引数、戻り値の型定義。

Cell と Cellは全く別のクラスになる。
Genericsを使う時に、初心者が間違えやすいらしく、僕も最初はハマった。

Cellにすれば、パラメータに全てのクラスを扱える。
しかし、Cellではパラメータにクラスの制約がないのは不便。
そこで、Cell,Cellを使って、パラメータに使えるクラスの範囲に制約をかける。

一般的に PECS という呪文が存在し、上限付き境界ワイルドカード型をProducerと呼び、下限付き境界ワイルドカード型をConsumerと呼ぶことがあるらしい。
Producer - 値の生成専門
Consumer - 値の受取専門

PECS(Producer extends and Consumer super)とは、Javaのジェネリックスプログラミングにおいて、ジェネリッククラスのメソッドに柔軟性を持たせるための原則である。基本は以下の通り。
メソッドが値を取得するコレクション(Producer)は型にextendsをつける
メソッドで値を設定するコレクション(Consumer)は型にsuperをつける

Javaジェネリクス:PECSって何? - Qiita

java - What is PECS (Producer Extends Consumer Super)? - Stack Overflow

JavaジェネリックスのPECS原則、extendsとsuperの勘所 -- ぺけみさお

Genericsの型推論は、ダイヤモンド演算子<>を記述すると、下記が推論される。
* 変数への代入 =>例: var a = new ArrayList<>();
* メソッドの戻り値 =>例: return new ArrayList<>();
* メソッド呼び出しの引数 =>例: execute(new ArrayList<>());

ComparableとComparatorの違いも注意。

ComparableとComparatorの違いは何か: プログラマの思索

【11】 第8章 アノテーション
【12】 第9章 例外とアサーション
【13】 第10章 ローカライズ
は省略。

【14】 第11章 モジュール・システム

JavaのモジュールシステムでSPIとDIを実現するやり方: プログラマの思索

Javaのモジュールシステムの考え方をまとめてみた: プログラマの思索

Javaのモジュールシステムは複雑性をより増している: プログラマの思索

【15】 第12章 Java SEアプリケーションにおけるセキュアコーディング
は省略。

| | コメント (0)

2022/11/20

ComparableとComparatorの違いは何か

ComparableとComparatorの違いを混同していた。
自分用のラフなメモ書き。

【参考】
Java:任意の順番でのソート ? サイゼントの技術ブログ

compareToでJavaのソートは自由自在! 一からお伝えします

Java - オブジェクトの比較(==、equals、Comparable、Comparator)

Java Comparatorメモ(Hishidama's Java Comparator Memo)


結論は、
java.lang.Comparableは、Arrays.sort(Object[]), Collections.sort(List)で使う。
ソートを行いたいクラスにComparatorを明示的に実装する場合に使われる。

java.util.Comparatorは、ラムダ式やstream.sorted()で使う。
Comparatorは、ソートしたいクラスからソートのロジック部分を分離したいときに使われる。

違いは、Comparatorインターフェイスではなく、Comparatorはオブジェクトとして作成する。

class Book implements Comparable{} は、
処理の中で自分のクラス(Book)のソート順を定義します。
comparaTo()をオーバーライドして定義します。

//java.lang.Comparableは、自分のクラスと比較してソートする
public class Book implements Comparable{
// タイトル
private String title;
// 値段
private int price;
// 発行日
private LocalDate dateOfIssue;
@Override
public int compareTo(Book book){
return title.compareTo(book.title);
}
}
Collections.sort(bookList);

つまり、Comparableを格納しているListをソートする際は、Collections.sort(List)/reverse(List)を使うのが基本。
AA.compareTo(BB) => java.lang.Comparable を連想する。

たとえば、String#compareTo(String)、Integer#compareTo(Integer)になる。
Comparable.compareToのコツは、判断結果は小さい・等しい・大きいの3種類だけ。
値や文字列が等しいか、どちらが大きいかを判断するだけ。

オブジェクトや文字列や数値のListやSetを直接ソートしたい場合は、Comparableを継承して、compareToメソッドを実装する。
その方が楽。

一方、class BookTitleComparator(ソート用クラス) implements Comparator{} は、
処理の中で何かのクラス(ソートさせたいクラス)のソート順を定義します。
compare()をオーバーライドして定義します。

//java.util.Comparatorは、任意のクラスをソートできる
public class BookTitleComparator implements Comparator {
@Override
public int compare(Book book1, Book book2){
return book1.getTitle().compareTo(book2.getTitle());
}
}
bookList.sort(new BookTitleComparator());
または
bookList.sort((book1, book2) -> book1.getTitle().compareTo(book2.getTitle()));

ストリームAPIでは、基本はComparatorを使う場合が多い。
なぜなら、ソート処理にラムダ式を頻繁に使うから。

こんなことを調べてみると、ComparableはComparatorよりも古いAPIなのだろう。
Java8以後でラムダ式が生まれたので、Comparatorはその後に定義されたのだろう。

実際、java.lang.Comparableなので、import文は不要。
一方、java.util.Comparatorなので、必ずimport文は必要になる。

| | コメント (0)

Javaのsumはreduceで置き換えられる

JavaのストリームAPIとして、sum()とreduce()をよく混同していた。
JDKのsum()の定義を読んだら、Javaのsumはreduceで置き換えられると分かった。
以下は自分用のラフなメモ書き。

IntStream (Java SE 11 & JDK 11 )

Javaのreduceの使い方は2種類ある: プログラマの思索

Java Optionalメモ(Hishidama's Java8 Optional Memo)

一般に、sumはこんな感じで使われる。
int total = items.stream().mapToInt(Item::getPrice).sum();

sumはIntStreamだけで定義されているのに、Streamでも使えると混乱していた。
だから、map()の後に使えるのか、mapToInt()の後に使えるのか混乱していた。
理由は、数値配列型のStreamでしか使えないからだろう。

もう一つは、sumの戻り値がなぜOptional型ではないのか?
averageの戻り値はOptionalDoubleだし、他の統計処理メソッドもOptionalが多いのに、なぜsumだけがint型が許されるのか?

(引用開始)
sum
int sum()
このストリーム内の要素の合計を返します。 これはリダクションの特殊な場合であり、次と同等になります。

return reduce(0, Integer::sum);

これは終端操作です。

戻り値:
このストリームの要素の合計
(引用終了)

上記の定義を読むと、sumは、 return reduce(0, Integer::sum); と同じなので、必ず初期値0が設定されているので、NULLになる可能性はなく、Optional型はなくてもいい。
sumの戻り値の型はIntegerが正しいが、オートボクシングされてint型もコンパイルOKになる。

つまり、
int total = items.stream().mapToInt(Item::getPrice).reduce(0, Integer::sum);

int total = items.stream().mapToInt(Item::getPrice).sum();
は同じ。

sumをreduceで書き換える時、こんな感じでもOK。

int total = items.stream().map(Item::getPrice).reduce((x,y) -> x+y).orElse(0);
int total = items.stream().map(Item::getPrice).reduce((x,y) -> x+y).get();
int total = items.stream().map(Item::getPrice).reduce(0, (x,y) -> x+y);

reduce(op)の戻り値はOptionalになる。
だから、Optional演算子orElse, getが必要。

一方、reduce(0, op)の戻り値はIntegerになる。
だから、戻り値はオートボクシングされてint型もOK。

IntStream.mapToInt(ToIntFunction).sum()で覚える。
Stream.map(Function,Optional>).reduce(bo)で覚える。

ちょっとややこしいのは、averageの場合、戻り値=OptionalDoubleになる。
なぜならば、DoubleStreamでaverageが定義されているから。
Optionalではない点に注意。

DoubleStream (Java SE 11 & JDK 11 )

こんな例かな。
double avg = items.stream().mapToInt(Item::getPrice).average().getAsDouble();
double avg = items.stream().mapToInt(Item::getPrice).average().orElse(0.0);

OptionalDoubleをOptionalへ変換できるか?という記事もあった。

Convert OptionalDouble to Optional - Stack Overflow

下記の記事が分かりやすい。
JavaのOptionalは、ScalaのOptionクラスに相当する。
Optional型で扱えば、空かどうかの判定を自動判定してくれるメリットがある。

Java Optionalメモ(Hishidama's Java8 Optional Memo)

「OptionalとOptionalInt等の相互変換は出来ないようだ。(継承関係も無いし、変換に使えそうなメソッドも無い)」とのことなので、OptionalとOptionalは明確に異なる。
たぶん、IntStreamの場合とStreamの場合で異なるのだろう。

| | コメント (0)

2022/11/14

JavaのモジュールシステムでSPIとDIを実現するやり方

JavaGold黒本を読んでいたら、JavaのモジュールシステムでSPIとDIを実現するやり方をやっと理解できた。
理解できたことをラフなメモ。

【参考】
Javaのモジュールシステムの考え方をまとめてみた: プログラマの思索

クラウド上の開発がJavaに与えた影響は何なのか: プログラマの思索

Javaのモジュールシステムは複雑性をより増している: プログラマの思索

Java 9のモジュール機能「サービス(SPI)」と既存ライブラリの共存 (1/2)|CodeZine(コードジン)

DI(依存性の注入)とは依存性を注入するということである、、? - Qiita

module-info.javaに下記のような記述がある。
Sampleモジュールはどんな構造を持っているのか?

module Sample {
exports test;
uses test.Hello;
}

Sampleモジュールは、testパッケージを公開しており、test.Helloインターフェイスの実装クラスを利用する。
つまり、Sample単体ではコンパイルできるけれど、実装クラスがセットでなければ動作しない。

SPI(Service Provider Interface)を実装したHelloモジュールがない場合、コンパイルエラーにならない。
実際、ServiceLoader.load()は空のServiceLoaderインスタンスを返却するだけでスキップされる。
こんなプログラムになるだろう。

ServiceLoader loader = ServiceLoader.load(Hello.class);
Iterator iter = loader.iterator();
while (iter.hasNext()) {
Hello obj = iter.next();
System.out.println(obj.sayHello());
}

つまり、META-INF/services/helloというテキストファイルから読み込む時に、実装クラスが書いてなければ、ループ処理は空回りするだけ。

では、test.Helloインターフェイスの実装クラスtest.impl.HelloImplを持つHelloモジュールはどんな構造を持つのか?
たぶんこんな構造だろう。

module Hello {
exports test.impl;
requires Sample;
provides test.Hello with test.impl.HelloImpl;
}

Helloモジュールは、test.implパッケージを公開して、Helloの実装クラスHelloImplを提供する。
この時、HelloモジュールがSampleモジュールを呼び出し、Sampleへの依存関係を持つ点が重要。
なぜならば、test.Helloを利用するには、定義されているSampleモジュールをImportする必要があるから。

すると、SampleとHelloは依存性の注入(DI:Dependency Injection)を実現していることになる。
なぜならば、SPIの定義より、クライアント側Sampleはライブラリ側Helloの実装内容を利用しているのに、プログラムの依存関係としては、HelloからSampleへ逆向きの依存関係を持っているからだ。

つまり、普通は、Sample(利用側) --> Hello(提供側) になるはずだが、Sample(利用側) <-- Hello(提供側) なので、依存関係が逆転している。

まとめると、
Sample --> testパッケージ --> test.Helloインターフェイス
Hello --> Sample --> test.Helloインターフェイス
Hello --> test.impl.HelloImplクラス
という依存関係を持つ。

DIが実現されていることによってどんなメリットがあるのか?

まず、test.Helloインターフェイスを実装した具象クラスは、Sampleモジュールのモジュールパスに含める必要はない。
なぜならば、SPIの実装は後から提供されるので、実装に依存する必要はないからだ。
つまり、最初にIFだけ定義して公開しておけば、SPIの実装側ライブラリは後から実装すればいい。
IFという約束事だけ定義して、具体的な中身は後から開発できるので、開発スケジュールを調整しやすく、仕様変更にも柔軟に対応できる。

次に、test.Helloインターフェイスを実装するモジュールは、Sampleモジュールを再コンパイルする必要はない。
なぜならば、SPIの拡張ポイントとなるインターフェイスは変わらない限り、実装クラスに依存しないからだ。
つまり、SPIを利用する側のSampleモジュールと、実装クラスを提供するHelloは独立してコンパイルできる。

CやJavaなどでは、リプレース時にリコンパイルが必要だったりして、開発環境を準備したり、リリースするときは割と面倒だが、リコンパイルが不要であれば、過去にコンパイルしたモジュールをそのまま利用すればいい。

このモジュールシステムのサンプルから、僕はJDBCドライバの実装を連想した。
JDBCドライバでは、JDBCドライバのIFは既に定義されているが、各RDBごとにJDBCドライバの実装は異なる。
つまり、ユーザ側はJDBCドライバのIFを使ってプログラムを組めば良く、各RDBの実装の違いを意識する必要はない。
後から、利用するRDBのJDBCドライバを組み込めばいい。

2000年代にDIのアイデアが出た時はすごい設計技法のようなイメージだったが、2020年代の今となっては枯れた技術の一つにすぎない。
しかし、こういう依存関係を逆転する技術を利用して、プログラムの複雑性を手懐けようとする設計手法は、今後もその必要性は変わらないと思う。

| | コメント (0)

2022/11/13

Javaのreduceの使い方は2種類ある

Javaのreduceの戻り値が異なるのに戸惑っていた。
Javaのreduceの使い方は2種類ある。
ラフなメモ。

【参考】
Java:Streamのreduceの使用方法 | GWT Center

Stream BinaryOperator(Java Platform SE 8 )


Javaのreduceは、Rubyのreduceみたいに、累積する処理みたいなもの。
MapRedcueの間柄のように、map関数とreduce関数はペアで考える。

reduceの引数はBinaryOperator型になるのは分かる。
合計値のように、2つの引数を累積させる処理を行って、1つの戻り値を返すだけ。

しかし、reduce(BinaryOperator)とreduce(初期値, BinaryOperator)は戻り値の型が異なるのに混同していた。
結論は、初期値を設定していなければ、戻り値が空の場合があるのでOptionalでラッピングして返す。
Optionalであれば、NullPointerExceptionのリスクを無くせるメリットがある。

reduceの引数に初期値を設定していれば、戻り値がなくても初期値をそのまま返せばいい。
つまり、戻り値はプリミティブ型でも問題ない。

Java:Streamのreduceの使用方法 | GWT Centerの説明がわかりやすかった。
以下引用している。

【パターン1】Optional reduce(BinaryOperator accumulator)
戻り値がない場合があるので、Optionalで返す。

// 何らかの要素があったか
boolean foundAny = false;
// 最終結果
T result = null;
for (T element : このストリームの要素を列挙する) {
if (!foundAny) {
// 最初の要素の場合
foundAny = true;
result = element;
} else {
// 二番目以降の要素の場合
result = accumulator.apply(result, element);
}
}
// 結果があればそれを返す。なければ空を返す
return foundAny ? Optional.of(result) : Optional.empty();

【パターン2】T reduce(T identity, BinaryOperator accumulator)

初期値が与えられているので「値が存在しない」ということは無く、戻り値はOptionalではない。

// 初期値
T result = identity;
for (T element : このストリームの要素を列挙する) {
// 初期値に加算していく
result = accumulator.apply(result, element);
}
return result;

IntStream (Java SE 11 & JDK 11 )には、reduceの考え方が書かれている。
これは参考になる。

int reduce​(int identity, IntBinaryOperator op)
指定された単位元の値と結合的な累積関数を使ってこのストリームの要素に対してリダクションを実行し、リデュースされた値を返します。 これは、次の操作に相当します。

int result = identity;
for (int element : this stream)
result = accumulator.applyAsInt(result, element)
return result;

つまり、reduceは何らかのアルゴリズムに添って累積させる処理を行っている。
その累積処理は抽象化されているので、アルゴリズムにラムダ式を当てはめれば汎用的に使えるわけだ。

| | コメント (0)

2022/10/21

Javaのモジュールシステムの考え方をまとめてみた

Javaのモジュールシステムの理解が深まったのでメモ。
Java初心者のラフなメモ書き。

【1】モジュールシステムはなぜJavaで必要なのか?

異なるJarであっても、同一パッケージ名が衝突する問題があった。
モジュールは、パッケージを区別するための仕組み。
パッケージはクラスを包み込み、モジュールはパッケージを包み込む。

Javaはオブジェクト指向言語なので、機能追加したい場合、開放閉鎖原則に従って、既存クラスは修正せず新規クラスを追加する。
Rubyのオープンクラスみたいなもの。
すると、クラスがどんどん増えるので、パッケージでクラスを分類しようとする。
そして、パッケージをまとめたJarを配布して、開発者に利用してもらうようにする。

しかし、Jarファイルもどんどん増えてしまって、異なるJarなのに同一パッケージで衝突する場合がある。
Mavenでこういう依存ライブラリのJarを管理するけれど、名前衝突が多くなるのだろう。

【2】なぜ、無名モジュール、自動モジュールは必要なのか?

Java9以後はモジュールシステムを使う必要があるが、以前のJara8のJarファイルはモジュールに対応していない。
Java8のJarファイルを利用できるような環境としてモジュールが導入された。

基本は、Classpathにある無名モジュールが一般的だが、modulepathにないので、名前付きモジュールから無名モジュールを読み込めない。
そこで、modulepathにJarファイルを置いて、自動モジュールに変更して、名前付きモジュールから自動モジュールを読み込めるようにした。

実際の現場を見ると、Java8までで止まっているWebシステムは割と多いように感じる。
たぶん、Java9以後のモジュールシステムに対応するように、移植するのは難しい場面があるからだろう。

【3】無名モジュール、自動モジュールとは何か?

無名モジュールはJava9以前のJarで、Classpathにある。
以前のコマンドみたいに、java -cp **.jarみたいに使う。

なお、無名モジュールのJarはclasspathにあるので、名前付きモジュールから読み込めない。

自動モジュールはJava9以前のJarで、ModulePathにある。
java --module-path **.jar とか、java -p **.jarみたいに使う。

名前付きモジュール--> 自動モジュール、自動モジュール --> 名前付きモジュールの両方で読み込める。
なぜならば、modulepathに名前付きモジュールも自動モジュールも両方配置されているから。

なお、無名モジュールも自動モジュールも、パッケージのクラスは全て公開されているから、モジュールシステムのように公開の制限はできない。

無名モジュールはJDK9以前のライブラリ。
module-info.javaもMETA-INF/MANIFEST.MFもない。
ClasspathにJarを配置していると、java --module-path **.jarが使えない。

自動モジュールは、META-INF/MANIFEST.MFにAutomatic-Module-Name属性が指定されている or jarファイル。
JDK9以後は、java -p **.jarで実行する。

自動モジュールはできるだけMETA-INF/MANIFEST.MFにAutomatic-Module-Name属性を指定する。
なぜならば、Jarファイル名は書き換えられやすいので間違いやすい。
META-INF/MANIFEST.MFに記述すれば、Jarファイル名が書き換えられても呼び出されるJarは同じになるので安全。

【4】jlinkはなぜ必要なのか?

専用のランタイム用アプリを作りたい。
JDK9以後は、JREがないから。

さらに、クラウドのサーバーレス環境でアプリを実行する時、コンパイルしたアプリのサイズを小さくできる。
クラウドのリソースを抑えられるし、アプリの起動時間(ロード時間)も短くできるメリットが出てくる。
つまり、AWS lambdaみたいに、イベント駆動で多数のプロセスを並行起動するような場面では、アプリサイズが小さいほど性能も良くなるし、デプロイもやりやすい。

たぶん、クラウドの開発に特化するようにJavaも進化してきているのだろう。

クラウド上の開発がJavaに与えた影響は何なのか: プログラマの思索

【5】jdeps --jdkinternal はなぜ必要なのか?

JDKの内部APIでクラスレベルの依存関係を検索するためのコマンド。
古いJava8のJarファイルがどんなJDKライブラリを使っているか調べるために使う。
Java9以後は公開されていないJDKライブラリは排除していくべきという考え方。

Java9のモジュールへ移行する時や、jlinkコマンドで小さい専用ランタイムアプリを作りたい時に利用できるだろう。

【6】なぜServiceLoaderは必要なのか?

ServiceLoaderは、Java9以前で依存性注入(DI)を実現する方法に過ぎない。
たとえば、Sampleインターフェイスだけ公開して、SampleImpl実装クラスは呼び出さないように実装したい。
META-INF/servicesに、実装クラスを記載したテキストファイルを作って、ServiceLoaderが読み込んで動的に切り替える仕組みにしただけ。

Java9のモジュールシステムでは、ServiceLoaderを利用する場合、module-info.javaにprovides~with~でIFを書いて公開する。

「公開IFの提供」をmodule-info.javaで宣言する => provides 公開IF with 実装クラス(SPIを実装)
「公開IFの利用」をmodule-info.javaで宣言する => uses 公開IF名(SPIとして使う)

ServiceLoaderはJDBCドライバの実装にも使われているので、わりと一般的。

【7】Javaのモジュールシステムは、Javaの進化でどのような意義を持つのか?

モジュールシステムは、Javaをクラウド開発に適用したり、デプロイしたモジュールやライブラリの移植性を高めるために必要なのだろう。
しかし、Javaは過去の遺産が多すぎるために、どんどん仕様が複雑になってきていると思う。

実際、Jarの依存性のエラーメッセージの種類が多くなっているために、問題解決するのは大変だろうと想像する。

Javaのモジュールシステムは複雑性をより増している: プログラマの思索

一方、Javaはオブジェクト指向言語だけでなく、関数型言語の一面も持つ。
実際、OptionalやStreamなどのモナドのAPIはまさに関数型言語そのものだ。

Javaが関数型言語の特徴を持つようになった理由の背後には、クラウド上で多数のプロセスを並行で動かす時に、MapReduceの仕組みがあれば、負荷分散をより活用できるメリットを活かしたいからだろう。

Javaはオブジェクト指向言語ではなく関数型言語だった~「[増補改訂]関数プログラミング実践入門」はお勧めの本だ: プログラマの思索

Javaはなぜ関数型言語になろうとしているのか: プログラマの思索

JavaSilverの感想~Javaはオブジェクト指向と関数型言語の2つの性格を持つ: プログラマの思索

そんなことを考えると、20年前にUMLやJavaを使って、オブジェクト指向設計に熱狂していた時代はもう古くなっている。
そういう価値観はもはや終わったように思える。
そして、時代は更に進化しているわけだ。

一方、その進化はどんどんソフトウェアの複雑性を増やす。
ソフトウェアの本質は複雑性にあるが、その複雑性をどのように手懐けてコントロールすべきなのか?
今も昔もソフトウェア開発はこの問題から逃れられない。

| | コメント (0)

2022/10/16

クラウド上の開発がJavaに与えた影響は何なのか

クラウド上の開発がJavaに与えた影響は何なのだろうか。
考えがまとまっていないがラフなメモ。
自分はAWSも知らないし、Go言語も知らないので、間違っていたら後で直す。

【参考】
Javaがサーバレスに至るまでの道のり

元JavaエンジニアがGoに感じた「表現力の低さ」と「開発生産性」の話 - DMM inside

Ruby->Go->Scalaという習得順序がエンジニアの爆速の成長に最適である理由 - Qiita

Javaがサーバレスに至るまでの道のりの記事を読むと、いかに軽量のWebアプリケーションを実現するか、に注力しているように思える。

従来のWebアプリケーション開発では、PerlのCGIのようなプロセス指向ではなく、JavaのServletのようなマルチスレッドの方が無駄にリソースを食わず、効率が良いと歌われていた。

しかし、サーバーレス環境では、AWS Lambdaのようにアプリをキックするような仕組みなので、アプリの処理時間だけでなく、アプリのロード時間も短いほど優位だからだ。
サーバーレス環境では、アプリを常駐させて、マルチプロセスで動く。
そして、処理が終わり、無駄に多くなったプロセスはすぐに破棄される。

つまり、クラウドでは、DockerやK8sのようなコンテナのおかげで、アプリ環境を使い捨てにできるメリットがある。
アプリも、負荷分散に応じて必要なだけ常駐プロセスを増やして、不要になれば捨てればいい。

すると、アプリのビルド、デプロイの時間も短い方がいい。
さらに、「起動時間がレスポンスタイムに含まれる」ために、アプリの起動時間も短くできれば、サーバーレス環境ではレスポンスタイムをもっと短くできて、使い勝手が良くなる。

そういう発想がJavaにどんな影響を与えているのか?
クラウド上の開発がJavaに与えた影響は何なのか?

たまたま、Javaのモジュールシステムの話の中で、jlinkコマンドがなぜ必要なのか?という話があった。
JDK9以後は、JREが配布されないので、Javaファイルを実行できない。
そこでjlinkで実行ファイルを作る必要があるという話だったが、さらにもう一つ興味深い話があった。

Java Module System 入門 その1 - YouTube

Java Module System 入門 その2 - YouTube

jlinkコマンドを使うとランタイム専用アプリを作れるが、必要なJDKライブラリだけを読み込んで、Jarファイルを作成する。
すると、jlinkで作られたランタイム専用アプリのサイズは、従来のランタイム専用アプリのサイズよりもかなり小さくなる。
不要なライブラリを除いているからだ。
実際、デモ画面では200Mから30Mまで小さくできた例があった。

つまり、jlinkコマンドを使えば、クラウドのサーバーレス環境でアプリを実行する時、コンパイルしたアプリのサイズを小さくできる。
その分、クラウドのリソースを抑えられるし、アプリの起動時間(ロード時間)も短くできるメリットが出てくる。

すなわち、jlinkコマンドはクラウド上で配布するアプリに適合するように作られたものなのだろう。
Javaもクラウドに特化したアーキテクチャ設計を取り込んでいるのだろう。

そんな話を聞くと、サーバーレス環境のアプリでは、ランタイム専用アプリはバイナリファイルでサイズが小さいほど優位性が高いことになる。
マイクロサービスの実装言語はGoが主流であると聞くが、その理由は、Goで簡単にバイナリファイルが作れて、さらにそのサイズが小さいので、性能的にも優れている点があるのではないだろうか?
そして、Javaもそういう流れの設計手法を取り入れるために、jlinkコマンドのようなアーキテクチャが必要になってきたのではないだろうか?

ここで、クラウド上のシステム設計では、ドメインごとにアプリを細分化したマイクロサービスアーキテクチャが流行していて一般的になってきている。
マイクロサービスアーキテクチャが必要な理由は、普通のWebシステムでもクライアントからのアクセスがブラウザだけでなく、スマホだったりタブレットだったり、色んな端末に対応する必要があるので、公開APIを通じて制御できるよに、色々分割したほうが楽になる点があるだろう。
また、逆コンウェイ戦略のように、システム開発しやすいチーム組織の観点で設計すると、小さなコンポーネントの集まりになるようにシステムを分割して、マイクロサービスアーキテクチャで連携できるようにした方がよい点もあるだろう。

マイクロサービスアーキテクチャのそんな特徴を活かすには、おそらく、Go言語やjlinkコマンドのような実装言語やビルド方法も必要になってくるのだろう。

そんなことを考えると、クラウド上のシステム開発では、フロントWebシステムをRubyやJava、Pythonで作ったとしても、AWS lambdaやマイクロサービスの部分はGo言語やjlinkを使う、といったように、複数のプログラミング言語を適材適所に当てはめて開発する必要性があるのではないか。
つまり、クラウド上の開発は、単にコンテナ部分のアーキテクチャを使いこなすだけでなく、複数のプログラミング言語を適材適所で使い分けるノウハウも必要になってくるのだろう。

この辺りは妄想に近いかもしれないので間違っているかもしれない。
ただ、クラウドというアーキテクチャは、Webリソースをふんだんに使える富豪プログラミングを可能にしたからこそ、それに特化した設計手法がどんどん現れているのだろうと思う。

この発想が正しいのか、これから検証していく。

| | コメント (0)