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 extends A>,Cell super A>を使って、パラメータに使えるクラスの範囲に制約をかける。
一般的に 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アプリケーションにおけるセキュアコーディング
は省略。
最近のコメント