- 投稿日:2021-01-21T23:22:21+09:00
Java基礎 シリアライズとは
前回、ファイルオブジェクトを生成し文字を書き込んだり読み込んだりする処理を書きました。
今回は、プログラマーが作ったクラスで生成したオブジェクトを入出力する方法について書きたいと思います。シリアライズ
生成したインスタンスをバイト列に変換したり(シリアライズ)、またそのバイト列をインスタンスに戻したり(デシリアライズ)する仕組みです。
この仕組みによって、入出力ストリームを使用してファイルに保存したりネットワークで伝送したりできます。
シリアライズは直列化とも呼ばれます。オブジェクトをシリアライズ可能にする
通常のクラスをシリアライズ可能にするには、java.io.Serializableインターフェースを実装する必要があります。このインターフェースはマーカーインターフェースのため、オーバーライドすべきメソッドは持ちません。
下のプログラムは、PersonクラスがSerializableインターフェースを実装し、シリアライズ可能なオブジェクトにしています。
Person.javaimport java.io.*; class Person implements Serializable { private static final long serialVersionUID = 1L; private String name = ""; private int age = 0; private transient String language = "Japanese"; public Person(String name, int age) { this.name = name; this.age = age; } public String getInfo() { return name + ":" + age; } public String getLang() { return language; } }入出力するためにストリームを生成する
シリアライズ可能なオブジェクトの入出力は、ObjectOutputStreamクラスやObjectOutputStreamクラスを使用します。これらのストリームを使って、インスタンスを外部に保存したり、外部から復元したりできるようになります。
以下のプログラムは、前述のPersonクラスで生成したインスタンスを、ストリーム経由でファイルに保存、そこから復元をしています。
Main.javapublic class Main { public static void main(String[] args) { try (ObjectOutputStream outObject = new ObjectOutputStream(new FileOutputStream("object.txt")); ObjectInputStream inObject = new ObjectInputStream(new FileInputStream("object.txt"));) { outObject.writeObject(new Person("Taro",22)); Person exP = (Person)inObject.readObject(); System.out.println(exP.getInfo()); System.out.println(exP.getLang()); } catch (IOException | ClassNotFoundException e) { } } }「object.txt」というパスを持ったFileOutputStream、FileInputStreamを生成し、それらをもとにObjectOutputStream、ObjectInputStreamを生成します。
そして、writeObject()メソッドで指定したオブジェクト(直接"太郎、22歳"というPersonを生成)を、ObjectOutputStreamを経由して「object.txt」に書き込んでいます。
さらに、readObject()メソッドを使用して、「object.txt」に保存されたインスタンスを復元しPersonクラスの変数に格納しています。readObject()メソッドの戻り値はObject型であるため、exPに代入する前にPersonクラス型へキャストしないといけません。
※readObject()メソッドはClassNotFindExceptionをスローする可能性があるため、それに対応した例外処理を書く必要があります。
ちなみに、上記のプログラムを実行すると、以下の結果が出力されます。
Taro:22 null2行目のnullが出力される理由としては、後述の「transient」キーワードを参照ください。
シリアライズ対象外の変数
Serializableインターフェースを実装したクラスでも、以下の変数はシリアライズできません。
・transientで指定された変数
・static変数
・Serializableインターフェースを実装していないクラス型の変数transientはデシリアライズ時にnullの状態で復元されます。先ほどのプログラムで、nullが返されたのもこれが理由です。
またstatic変数は、static領域に値を持つため、復元時はそこの値が使われます。シリアライズの継承
シリアライズ可能なクラスのサブクラスは「implements Serializable」を書かなくても暗黙的にシリアライズ可能です。
また、スーパークラスがSerializableインタフェースを実装していない状態で、サブクラスがSerializableインタフェースを実装している場合、デシリアライズの際にスーパークラスのコンストラクタが呼び出されてインスタンス化されます。
シリアルバージョンID
Personクラスのフィールドに、「serialVersionUID」という定数を宣言しています。
この定数はシリアルバージョンIDというもので、復元前後でクラスが変更されていないか識別するために使います。(eclipseではこれを書かないと警告が出ます。)クラス修正時にシリアルバージョンIDを修正することで、修正前に保存したインスタンスを修正後のクラスで復元しようとすると例外(InvalidClassException)が発生します。
これによって、矛盾した状態での復元がなされた時、それを知らせることができます。
<参考>
書籍
・スッキリわかるJava入門 実践編 第2版
・オラクル認定資格教科書 Javaプログラマ Gold SE 8
ホームページ
・マコトのおもちゃ箱 ~ぼへぼへ自営業者の技術メモ~
http://piyopiyocs.blog115.fc2.com/blog-entry-205.html
- 投稿日:2021-01-21T22:22:43+09:00
Java の UUID は素晴らしい!
ユニークのキーを生成したいと思って、この記事を見つけました。
https://qiita.com/kawasima/items/6b0f47a60c9cb5ffb5c4
まさにこの記事の通りです。素晴らしい。
結論から言うと、UUID version4 が良さそうです。
UUID 以前から知ってましたが、バージョンまであるのは知りませんでした。じゅあ、どうやってバージョンを調べるの?と調べたら、この記事
http://xawa99.blogspot.com/2013/07/UUID-Version.html
xxxxxxxx-xxxx-Vxxx-xxxx-xxxxxxxxxxxx
なるほど!Java 8 で検証してみました。
import java.util.UUID; public class App { public static void main(String[] args) { for (int i = 0; i < 10; i++) { UUID uuid = UUID.randomUUID(); System.out.println(uuid); } } }結果
3e817ae3-c770-4e43-afc3-4076e7bf1917 8622a167-d6e0-49db-bf7c-879251d7706d 89edf330-7421-4c71-8066-ced5d3d6669e 8a63ab75-de69-45c2-ab7c-9fd788e21a2d 40fd3de0-b8f6-43a5-bdcb-a6dcb44cabf9 f1be93b0-93e4-44f9-bbec-867502e6c9d9 c26ed1d8-2b79-4dbf-b088-ee7e80752018 5591cacd-3d02-4397-908b-37cedab6f760 8fde8770-7776-4171-9b9f-22fde1db5ab5 3c0b6030-64e4-4c73-a1d7-f56eaa6ecfa0java 8 は既にバージョン4ですね!
以上!
- 投稿日:2021-01-21T21:45:25+09:00
【Spring】@SessionAttributesでセッションオブジェクトでオブジェクトを持ち回りたいのに・・・
No primary or single public constructor found for interface java.util.List
このエラーに5時間位かかりましたね。意味わからんかったー
結局セッションオブジェクトの扱い方がわかっていなくて、コントローラーのメソッドの引数に@ModelAttributeなしでList<Integer> ListInt,
みたいに渡してて、これがアカン買ったようです。大まかなイメージ
- セッションに置きたい名前を準備
- その名前にオブジェクトを結びつける
- model.addAttributeでセッションに保存する
- メソッドの引数に@ModelAttributeをつけてセッションから取得
実装する場所
コントローラーの上に
@SessionAttributes( value="〇〇")
〇〇
にはオブジェクトの名前(属性名)コントローラーの中の一番上に@ModelAttributeでオブジェクトを準備
@ModelAttribute( value = "〇〇" ) public △△型 setUp〇〇(){ return new △△(); }セッションのコンストラクタ?
これで、〇〇という名前の△△型のオブジェクトが準備されるメソッドの引数に@ModelAttributeでセッションから取得
public method( @ModelAttribute("〇〇") △△型 〇〇, Model model ){modelも必要でした
メソッド内で
model.addAttribute("〇〇", 〇〇)ここでセッションの中の"〇〇"という名前でオブジェクトを保存することができます
別のコントローラーでは@ModelAttribute無しでも持ち回れたんだけど・・・
基本がわかっていないからこういうことになるんですけど、まだまだ駆け出しなのでうまく行ったものを正解だと思って同じようにやってるんだけど、ちょっと変わってくるとエラーが出て「さっきはできてたやん!!」って叫びたくなりませんか??!!
まあ勉強にはなるんですけど、、、、
- 投稿日:2021-01-21T21:13:09+09:00
KotlinでMapとListをfor文で操作しよう!
Key:Valueが1:1のMapをListにする
// 1:1 val map = mapOf("key1" to "val1", "key2" to "val2", "key3" to "val3") // mapのすべてのvalueが格納される val valueList = ArrayList(map.values) var list: MutableList<String> list = ArrayList() list.addAll(valueList)1:1のMapを条件分岐でListに格納する
val map = mapOf("key1" to "val1", "key2" to "val2", "key3" to "val3") val valueList = ArrayList(map.values) // key1のvalueがarray変数に格納される("val1") var araay = map.get("key1").toString() // itemは0からはじまる for (item in valueList.indices){ // val1がvalueListにある場合はListに追加 if (araay.contains(valueList[item])){ list.add(valueList[item]) } }Mapのvalueを配列にしてネストさせる(1:n)
// 1:n val mapArray = mapOf("keyX" to arrayOf("valA","valB", "valC").map { num -> num }, "keyY" to arrayOf("valD","valE").map { num -> num }) val valueArrayList = ArrayList(mapArray.values)1:nのMapを条件分岐でListに格納する
// 1:n val mapArray = mapOf("keyX" to arrayOf("valA","valB", "valC").map { num -> num }, "keyY" to arrayOf("valD","valE").map { num -> num }) val valueArrayList = ArrayList(mapArray.values) var araay = mapArray.get("keyX").toString() for (item in valueArrayList.indices) { // ネストしている配列の要素数だけループする for (i in valueArrayList[item].indices) { if (araay.contains(valueArrayList[item].get(i))) { list.add(valueArrayList[item].get(i)) } } }
- 投稿日:2021-01-21T20:24:59+09:00
Java メモ
列挙で比較する時など
Arrays.asList({A,B,C}).containsを使う
Optional.isPresent()はなるべく使わず
Optional.map ~ orElse orElseThrow
する
- 投稿日:2021-01-21T15:45:11+09:00
初心者が1から始めるJava開発-Vol.3- クラス、メソッド編
今回はJavaの最も重要な概念であるクラス、メソッドについて説明していきます。
私もまだまだプログラマーとしては発展途上で、こういう概念の話は言語化がむずい。
分かりにくい概念の話が多いですが、実装を経て「あーそういう事か」と分かればOKです。もちろん、Hands On形式で実際にコードも書いていきますよ!
Javaの初心者向けおすすめ本
スッキリわかるJava入門 第3版 (スッキリシリーズ)
クラス
オブジェクト指向の言語にはおおむねあると思っていますが、
Javaでは、ファイル1つのことを指す場合が多いです。Javaでは
XXXX.javaというファイルを作った ⇔ XXXXというクラスを作った
という同値関係が、おおむね成立すると考えてもらってよいです。※Pythonなどの場合は1ファイルの中に複数クラスを定義できたりします。
逆にFortranやCobolなどの言語はオブジェクト指向ではないので、
1ファイルにすべての処理を順に記載するといったこともあります。クラスという概念は、1つの処理を複数のクラスに分割して、各クラスに処理を委譲するという事であり、
プログラムを簡単にかつ効率的に開発するうえで重要な考え方です。オブジェクト指向
オブジェクト指向とは、簡単に言うと、「処理とデータを同時に部品として扱う」ようなことと考えてください。
料理で例える
カレーを作るという処理を考えたときに
- カレーを作れと支持する人
- 野菜を洗う人
- 野菜を切る人
- 肉を切る人
- 煮込む人
- 盛り付ける人
のようなステップ(人)によって実現します。
家で作るときは一人でやると思いますが、
プログラムの世界では、分業させた方がきれいで読みやすく保守しやすくなる方が多いので分けました。
この時「処理とデータを同時に部品として扱う」に従えば
- 野菜を洗う人
では
クラス:「野菜を洗う人」
データ:「野菜」
処理:「洗う」となります。
- 「野菜」はJavaではフィールドというもので表現します。(必ずフィールドが必要なわけではありません)
- 「洗う」はJavaではメソッドというもので表現します。
これによって、
クラス内に、フィールドとメソッドを定義することで「野菜を洗う人」というオブジェクトを定義するためのクラスが実装できるわけです。※たとえが悪いと思うので後出の実装部分で実際のJavaで詳しく見ていきましょう。
ちなみに、フィールドは例では使っていませんが。。メソッド
Javaのメソッドは前出の通り、処理を表現するものです。
メソッドを使うメリットとして
- 処理を使いまわすことが可能で、コードが見やすく、書きやすい
- 修正箇所が限定的になる
例を見た方が良いので後出の実装部分で実際のJavaで詳しく見ていきましょう。
プログラム実装
それでは、今回は、渡した数字に対して、四則演算をして、集計をするようなプログラムを書いていきます。
仕様
1、2つの数字、a,bに対して、四則演算をしていく(今回は足し算(+2)と掛け算(×2)だけにします) 2、四則演算した2つの数字を最後に足し算して合計値を表示する。それでは書いていきます。
まずはCalculationMainクラスを作成しましょう。
パッケージはどこでも構いません。クラスの作り方を忘れた場合はVol.2を参照してください
package calculation; public class CalculationMain { }今回はsrc/main/javaの下にcalculationパッケージを作成し、そこにクラスを作成しました。
次に2つの数字a,bを定義していきます。
package calculation; public class CalculationMain { public static void main(String[] args) { int a = 1; int b = 2; } }お決まりのpublic static void メイン あーぐすを実装します。これもメソッドです。
その中にa,bの2つのint(数値)型を定義します。今回は1,2でいきます。続いて、四則演算をしていきますが、ここで役割を分けるために四則演算専用クラスを作っていきます。
私と同じようにしている場合はcalculationパッケージの下にserviceパッケージを作成し、その下にCalculationクラスを作成します
- calculationを選択し右クリックから New > other 選択します
- packageと入力し Java > packageを選択しNextを押下します。
- Nameにcalculationはすでに入っていると思うのでcalculation.serviceとします。
- Finishで作成完了です。
すると、calculationのしたにserviceというフォルダらしきものができたことがわかると思います。
このように見えていない場合は、package explorerのところにある3点アイコンみたいなやつを選択して以下の画像のようにHierarchicalを選択すればOKです。ただここに関しては個人の好き好みがあると思うのでどちらでもOKです。
ではクラスを作りましょう。serviceパッケージの下に以下のようなクラスができたかと思います。
package calculation.service; public class Calculation { }足し算のメソッド
ここに四則演算をするためのメソッドを作っていきます。
まずは足し算用のメソッド
package calculation.service; public class Calculation { public int add(int num) { } }新出の知識を抑えてください。
メソッドは
public [返り値の型] メソッド名前([引数の型 引数名,..])で記述します。
[返り値の型]:voidは返り値なし、それ以外は指定に従って値を返します。
[引数の型 引数名,..]:型と引数の名前を指定します。複数指定が可能です。ここで、今add(int num)の部分に赤線が出ていると思います。
マウスを持っていくと、なぜ赤線が出ているか理解できます。そのとおり、返り値にvoid以外を指定するとreturnつまり値を返す記述が必要になります。
選択肢の中にあるAdd return statumentを選択してください。すると、勝手にreturn num;が書かれたと思います。
package calculation.service; public class Calculation { public int add(int num) { return num; } }続いて足し算(+2)なのでもうadd2という名前にしてしまいましょう。
そして、引数のnumに2を加算して返すように書くと
package calculation.service; public class Calculation { public int add2(int num) { return num + 2; } }これで2を足すメソッドができました。
掛け算のメソッド
次に掛け算のメソッドを書いていきます。
名前はmultiply2としましょう。
package calculation.service; public class Calculation { public int add2(int num) { return num + 2; } public int multiply2(int num) { return num; } }知ってる人はいますかね?掛け算は*を使うので以下で完成です。
package calculation.service; public class Calculation { public int add2(int num) { return num + 2; } public int multiply2(int num) { return num * 2; } }最後に、仕様2の集計するメソッドを作ります。名前はaggregateとかで。
package calculation.service; public class Calculation { public int add2(int num) { return num + 2; } public int multiple2(int num) { return num * 2; } public int aggregate(int a, int b) { return a + b; } }もう余裕ですよね?
2つの値を足しすので、引数には2つの値を指定します。
それでは最後にオブジェクト指向らしさを出す作業、CalculationMainのa,bに対し、このメソッドを追加って最後まで実装していきます。
CalculationMainを開いてください。
まずCalculationクラスの中のメソッドを使う場合、外部クラスで直接呼べないので、呼ぶために以下のように書きます。
package calculation; import calculation.service.Calculation; public class CalculationMain { public static void main(String[] args) { int a = 1; int b = 2; Calculation calc = new Calculation(); } }new を使って書いています。現場では「Newする」と言ったりもしますが、
Calculationクラスの形を持ったオブジェクトをインスタンス化するというのがたぶん正しい表現です。
要するに初期化して使えるようにするみたいなイメージです。前出の料理の例でいえば
- 辛口のカレーを作る
- 甘口のカレーを作る
それぞれでNewが必要になる可能性があります。
(同じ鍋を使ったら甘口なのに中辛になっちゃうから一回鍋を洗うか、新しい鍋を使う的なイメージ)フィールドがなくメソッドだけのクラスをNewする時には改めてNewしないで使うことができる場合も多いです。
ここら編のインスタンス化の詳細は参考書籍に譲ります。(直近影響はないと思うので、いつかちゃんと勉強して整理するつもり)
そして、a,bそれぞれに仕様通りの演算をします。
package calculation; import calculation.service.Calculation; public class CalculationMain { public static void main(String[] args) { int a = 1; int b = 2; Calculation calc = new Calculation(); int aAdd2=calc.add2(a); int bAdd2=calc.add2(b); } }まず、a,bにそれぞれ2を足します。
Calculation型のcalcというオブジェクトのadd2メソッドを使う場合はこのように書きます。同様に掛け算と、集計もやってみます。
package calculation; import calculation.service.Calculation; public class CalculationMain { public static void main(String[] args) { int a = 1; int b = 2; Calculation calc = new Calculation(); int aAdd2 = calc.add2(a); int bAdd2 = calc.add2(b); int aMultiply2 = calc.multiple2(aAdd2); int bMultiply2 = calc.multiple2(bAdd2); int result = calc.aggregate(aMultiply2, bMultiply2); } }こんな感じで書けると思います。
最後にこんっソールに結果を表示したいので
java
System.out.println(result);
を入れて完成です。
package calculation; import calculation.service.Calculation; public class CalculationMain { public static void main(String[] args) { int a = 1; int b = 2; Calculation calc = new Calculation(); int aAdd2 = calc.add2(a); int bAdd2 = calc.add2(b); int aMultiply2 = calc.multiple2(aAdd2); int bMultiply2 = calc.multiple2(bAdd2); int result = calc.aggregate(aMultiply2, bMultiply2); System.out.println(result); } }結果はどうなるか予想しておきましょう。
a = 1 , b = 2 (a + 2)* 2 = 6 (b + 2)* 2 = 8 result = (a + 2)* 2 + (b + 2)* 2 より ∴ result = 14では実行してみてください。実行方法は前回Vold.2と同様です。
確かに14になりましたよね?
a,bの値を変えて試してみてください。
最後に
今回学んでほしかったのは
- CalculationMainクラスが、「処理を実行する役割」
- Calculationクラスが、「計算を実行する役割」
であること
- add2メソッドが計算の中でも2を加算する役割
- multiply2メソッドが計算の中でも2を掛け算する役割
- aggregateメソッドが計算の中でも2つの数字を足す役割
というように細かく処理の担当を分けたりすることで、
例えば、
「今、最初に2足してるけど、3にしといて」という仕様変更が入っても
メソッドに変えておけば1か所直せばいいのです。メソッドになってないと2か所直す必要があります。今回のコードは簡単ですが、もっと大規模になってくると、仕様変更で膨大な修正をしなければならないみたいなことも起きてくるので、
- 適切にメソッドを切り出す(出来るだけ小さくする)
- クラスやメソッドの役割を明確にしておくということを学んでもらえていればいいかなと思います。
- 投稿日:2021-01-21T15:03:25+09:00
~今さらながらJavaで関数型プログラミングを学習してみた~(第2部)
「関数型プログラミングとJava8での取り組みと追加API説明」を中心テーマとした第1部はすでに投稿されています。
第1部へのリンクはじめに
第1部では関数型プログラミング自体の概説と、Java8で新たに取り入れられたAPI+そのサンプルを中心に紹介しました。
本第2部では「実際Javaシステム開発においてどの程度関数型プログラミングを取り込むべきなのか?」を中心テーマとして少し現実的なサンプルも交えて検討していきたいと思います。
あわせて関数型プログラミングの説明でよく用いられている「パイプライン、高階関数、カリー化」という用語についてもJavaベースでの説明を行っていきます。
なお、内容には主観にもとづく部分や、個人的指向が入ったプログラムスタイルになっている場合がありますので了承お願い致します。
関数プログラミング特有の語句については第1部の後尾部分に理解した範囲での簡単な説明を記述していますので参考にして下さい。関数型プログラミングにおける代表的手法
中心テーマに入る前に、まず関数型プログラミング説明においてよく用いられている代表的手法についてJavaベースで説明しておきたいと思います。
代表的手法としてパイプライン、高階階数、カリー化(部分適用)を対象としました。(1) パイプライン
Javaでの関数型プログラミングにおけるパイプライン処理とは一般的にはStreamAPIでのパイプライン処理を意味します。
このパイプライン処理はストリームパイプラインとも呼ばれています。
ストリームパイプラインは中間操作(0個以上)と終端操作を連結して構成した処理となっています。
以下のサンプルではListをstream化し、その後filter(条件に一致したデータを抽出)処理やsort処理を行って、終端処理として結果戻り値であるList作成を作成したり、各要素の出力を行っています。
StreamAPIではこれらの一連の処理を014、016、018のようにパイプライン形式で1行に記述することが可能になっています。
この方法はJavaにおける関数型プログラミングスタイルでは一般的によく使用されます。PipelineTest.java001 package functiontest.unit; 002 import java.util.Arrays; 003 import java.util.Comparator; 004 import java.util.List; 005 import java.util.stream.Collectors; /** * Streamパイプラインテストクラス */ 006 public class PipelineTest { 007 public static void main(String[] args) { 008 List<Food> foods = Arrays.asList(new Food[] {new Food("sibuya", "ラーメン", 1700), new Food("sibuya", "チャーハン", 800), 009 new Food("sibuya", "餃子", 500), new Food("sinjuku", "スパゲティ", 1000), new Food("sinjuku", "オムライス", 900), 010 new Food("sinjuku", "ステーキ", 2500)}); 011 execute(foods); 012 } /** *パイプラインテスト実行 */ 013 public static void execute(List<Food> foods) { //フィルター処理→リスト化 014 List<Food> filteredList = foods.stream().filter(fobj -> fobj.getStore().startsWith("sib")).collect(Collectors.toList()); 015 System.out.println(filteredList); //ソート処理→リスト化 016 List<Food> sorttedList = foods.stream().sorted(Comparator.comparing(Food::getPrice)).collect(Collectors.toList()); 017 System.out.println(sorttedList); //フィルター処理+ソート処理→要素出力処理 018 foods.stream().filter(fobj -> fobj.getPrice() >= 1000).sorted(Comparator.comparing(Food::getName)).forEach(System.out::println); 019 } 020 }Food.java/** *Food情報格納クラス */ 001 package functiontest.unit; 002 public class Food { 003 String store; 004 String name; 005 int price; 006 Food( String store, String name, int stock){ 007 this.store = store; 008 this.name=name; 009 this.price = stock; 010 } 011 public String getStore() { 012 return store; 013 } 014 public String getName() { 015 return name; 016 } 017 public int getPrice() { 018 return price; 019 } 020 }(2) 高階関数
高階関数とは他の関数を引数あるいは結果として返すことができる関数(メソッド)のことです。
Java8ではmap, flatMap, filter, reduce, forEach, anyMatch, allMatch関数などが標準で用意されています。
引数で設定される関数の処理内容についてはラムダ式を用いて実装・記述することができます。
また、自作関数インターフェイスを作成すれば高階関数を自作することもできます。
高階関数の目的・メリットは関数の一部処理を引数(関数)で渡すことによって特定処理(引数(関数)以外の部分)を共通化して整理することができるという点です。
つまり、大枠の処理を関数として共通化しておき、具体的な一部処理を引数(関数)で渡すということができるようになります。
カリー化もこの高階関数を利用して実現しています。
以下に6つの高階関数使用サンプル(最後のサンプルは対象外)を提示して概説致します。
・pickupAndCountメソッド(015~027):指定した範囲条件にマッチした値の件数を算出しています。
016~024が命令型スタイルでのプログラミングで、025~026が関数型スタイルでのプログラミングとなっています。
026でfilter高階関数を使用してラムダ式で条件を指定しています。
・reduceSumメソッド(028~032):配列要素の合計値を算出します。
030でreduce高階関数を使用して合計値を算出しています。
・calSquareメソッド(033~038):二乗値を算出します。
035で二乗値算出式を定義して標準Functionインターフェイスに設定しています。
037のmapメソッドではこの標準Functionインターフェイスを引数として渡しています。
・calFiveTimesUsingInterfaceメソッド(039~042):標準Functionインターフェイスを使用して5倍値を算出しています。
040でgetFiveTimesメソッドを使用した5倍値算出式を定義して標準Functionインターフェイスに設定しています。
041でこのFunctionインターフェイスのapplyメソッドで5倍値を算出しています。
・calFiveTimesUsingLambdaメソッド(043~052):ラムダ式を使用して5倍値を算出しています。
044~046でexecuteメソッドに渡す引数としてラムダ式(5倍算出式)を定義しています。
049~052でexecuteメソッド内容を定義し引数の標準Functionインターフェイスでのapplyメソッドを実行しています。
・calIntegerAndCompareメソッド(053~069):2倍と5倍の値を比較し大きい方の値を抽出しています。
054&055で2つのメソッドを使用した2倍&5倍値算出式を定義して標準Functionインターフェイスに設定しています。
056でcalAndCompareメソッドを実行して2倍&5倍値の比較結果を取得しています。
059~063でcalAndCompareメソッドの処理内容を定義しています。
064~069でgetDoubleメソッド及びgetFiveTimesメソッドの具体的処理内容を定義しています。
・concatThreeStringメソッド(070~077):自作関数インターフェイスを使用して3つの文字列を結合しています。
071で3文字列結合する自作関数インターフェイス(メソッド名はconcatString)を定義しています。
072でconcatStringメソッド3文字列結合を実行しています。HigherOrderFunctionTest.java001 package functiontest.unit; 002 import java.util.Arrays; 003 import java.util.List; 004 import java.util.function.Function; /** * 高階関数テストクラス */ 005 public class HigherOrderFunctionTest { 006 public static void main(String[] args) { 007 pickupAndCount(); 008 reduceSum(); 009 calSquare(); 010 calFiveTimesUsingInterface(2); 011 calFiveTimesUsingLambda(1); 012 calIntegerAndCompare(3); 013 concatThreeString(); 014 } /** * 高階関数(filter)を使用したリスト内における数値範囲条件にマッチした件数算出 */ 015 private static void pickupAndCount() { 016 List<Integer> list = Arrays.asList(new Integer[] {38, 20, 40, 32, 36}); 017 System.out.println("命令型スタイルでのプログラミング"); 018 int count = 0; 019 for (int num : list) { 020 if (num == 20 || (num >= 30 && num <= 39)) { 021 count++; 022 } 023 } 024 System.out.println("命令型count:" + count); // 025 System.out.println("高階関数を使用したプログラミングスタイル(StreamAPI)"); 026 System.out.println("関数型スタイルcount:" + list.stream().filter(n -> n == 20 || (n >= 30 && n <= 39)).count()); 027 } /** * 高階関数(reduce)を使用した配列要素合計値算出 */ 028 public static void reduceSum() { 029 List<Integer> nums = Arrays.asList(new Integer[] {0, 1, 2, 3, 4}); 030 int sum = nums.stream().reduce(0, (t, x) -> t + x); 031 System.out.println("関数型スタイルsum:" + sum); 032 } /** * 高階関数(map)を使用した二乗値算出 */ 033 public static void calSquare(){ 034 List<Integer> nums = Arrays.asList(new Integer[]{0, 1, 2, 3, 4}); //Functionインターフェイスを定義 035 Function<Integer, Integer> square = (x) -> { return x * x; }; //StreamAPIのmapを使用して二乗算出 036 System.out.println("関数型スタイルsquare:"); 037 nums.stream().map(square).forEach(System.out::println); 038 } /** * Functionインターフェイスを使用した5倍値算出 */ 039 public static void calFiveTimesUsingInterface(Integer value) { 040 Function<Integer, Integer> fivetimes = (x) -> { return getFiveTimes(x);}; 041 System.out.println("関数型スタイルfivetimes_interface:" + fivetimes.apply(value)); 042 } /** * 高階関数(execute)を使用した5倍値算出 */ 043 public static void calFiveTimesUsingLambda(Integer value) { 044 Integer fivetimes = execute(value, t -> { 045 return getFiveTimes(value); 046 }); 047 System.out.println("関数型スタイルfivetimes_lambda:" + fivetimes); 048 } /** * 引数の関数インターフェイス(ラムダ式形式で定義)を実行するメソッド */ 049 public static <R> R execute(Integer value, Function<Integer, R> fnc) { 050 R rtnval = fnc.apply(value); 051 return rtnval; 052 } /** * 高階関数(calAndCompare)を使用した2倍と5倍の値を比較し大きい値を抽出 */ 053 public static void calIntegerAndCompare(Integer value) { 054 Function<Integer, Integer> times1 = (x) -> { return getDouble(x);}; 055 Function<Integer, Integer> times2 = (x) -> { return getFiveTimes(x);}; 056 int rtnval = calAndCompare(value, times1, times2); 057 System.out.println("関数型スタイル(値比較)rtnval:" + rtnval); 058 } /** * 引数のFunctionインターフェイスを実行し値を比較するメソッド */ 059 public static Integer calAndCompare(Integer value, Function<Integer, Integer> fnc1, Function<Integer, Integer> fnc2) { 060 int rtnval1 = fnc1.apply(value); 061 int rtnval2 = fnc2.apply(value); 062 return Math.max(rtnval1, rtnval2); 063 } /** * 具体的な処理を実行するメソッド(値を2倍する) */ 064 public static Integer getDouble(Integer value) { 065 return value * 2; 066 } /** * 具体的な処理を実行するメソッド(値を5倍する) */ 067 public static Integer getFiveTimes(Integer value) { 068 return value * 5; 069 } /** * 自作インターフェースを使用した3つの文字列を結合 */ 070 public static void concatThreeString() { //IOriginalFuncの定義と実行 071 IOriginalInterface originalInterface = (str1, str2, str3) -> {return str1+str2+str3;}; 072 System.out.println(originalInterface.concatString("AAA", "BBB", "CCC")); 073 } //自作の関数型インターフェース 074 @FunctionalInterface 075 public interface IOriginalInterface { // String三つを引数に取る関数を定義 076 public String concatString(String str1, String str2, String str3); 077 } 078 }(3) カリー化
関数型プログラミングにおいて理解しにくい単語の1つが「カリー化」ですが、基本だけでも理解すればそれほど難しくはありません。
カリー化とは複数の引数を受け取って処理する関数を、最初の引数を受け取って処理し、「残りの引数を受け取って処理する関数」を返す関数に変換することをいいます。
簡単簡略に言えば、関数を引数ごとに分解・分割して、それらを連続(ネスト)して処理することを「カリー化して処理すること」となります。
具体的に記述すると、引数3個の関数f(t, u, v) 関数があるとすると、この関数にt引数だけ渡して処理し、結果として引数uとvを持つg(u, v) 関数を返すこととなります。
これによって単引数を持つ関数の連続(ネスト)によって複数引数の関数を表現・記述することが可能になります。
ちなみに、純粋関数型言語の実質的標準であるHaskellにおいては原則複数引数の関数というものは定義できず、引数は1つとなっているようです。
ただ形式的(糖衣構文)には複数引数の設定は可能になっているようでこの場合には自動的にカリー化されて単引数関数を用いて処理されるようです。
(筆者はHaskell未使用なので確定的な記述はできません。)
ではなぜこのようなカリー化を行うかということですが、「カリー化」の目的は「部分適用」や「関数合成」を行う(やりやすくする)ためとなっています。
(ただ部分適用にカリー化が必須という訳ではありません。)
つまり、関数型プログラミングで関数の引数を1つにすることを原則・基本とするのは、「部分適用」・「関数合成」等の関数型プログラミング特有(限定という意味ではありません。)の処理・方法のためとも言えます。
「関数合成」は文字通り関数を合成して新しい関数を作るものです。
「部分適用」とは複数引数の関数の一部を固定化して引数を減らした関数を作ることを言い、これにより複数引数関数における不必要な処理について省略することが可能となります。
どちらもJava8以降で対応することが可能で、「関数合成」はFunctionインターフェイスでの2メソッドcomposeとandThenを利用することとなります。
ただ、開発システムのどのような処理・機能に対して、どのようにこの特有の処理・方法を適用していくかについては、純粋関数型プログラミング範疇に入り本記事の範疇を超えるのでここでは割愛としますので、他参考書を参照して下さい。
なお、以下ではJavaにおいて比較的「関数合成」よりはどちらかというと利用可能性があると思われる「カリー化」+「部分適用」についてのサンプルと説明を記述します。
・mainメソッド(001~040):カリー化及び部分適用テストのメインメソッド。
007~009:1つの引数数値が1桁の場合1を返し、それ以外は-1を返す(カリー化をしていない)。
010~012:2つの引数が0以上でs<eの場合1を返し、それ以外は-1を返す(カリー化をしていない)。
013~018:2つの引数が0以上でs<eの場合1を返し、それ以外は-1を返す(カリー化して部分適用)。
013で010で定義したITwoParameter(BiFunction)インターフェイスをカリー化しています。
046~050でカリー化を行うcurryingメソッドを定義しています。
014で1つ目の引数を固定してIOneParameter(Function)インターフェイスを取得し、これを使用して015そして017で2つ目の引数を設定して比較結果を求めています。
019~022:013~018と同じ処理内容ですが一時的なインターフェイスを取得せずに実行しています。
023~030:3つの引数が0以上でs<e<vの場合1を返し、それ以外は-1を返す。
023~025でカリー化を行い、026で2つの引数を固定してIOneParameter(Function)インターフェイスを取得しています。
027そして029で3つ目の引数を設定して比較結果を求めています。
031~034:023~030と同じ処理内容ですが一時的なインターフェイスを取得せずに実行しています。
035~040:2引数ケースと同じ処理を命令型複数引数メソッドで処理した場合とメンバー変数を使用した場合(部分適用に類似する)を記述。
041~056:引数1,2,3個の関数インターフェイスを定義。
057~059:メンバー変数の設定メソッド。
060~069:引数が1つ及び2つの命令型比較メソッド。CurryingTest.java001 package functiontest.unit; 002 import java.util.function.BiFunction; 003 import java.util.function.Function; /** * カリー化テストクラス */ 004 public final class CurryingTest { 005 private static Integer sint; 006 public static void main(String[] args) { //☆☆引数が1つのケース //引数数値が1桁の場合1を返し、それ以外は-1を返す(カリー化をしていないケース) 007 IOneParameter<Integer, Boolean> p1exe = (s) -> {if (-1 < s && 10 > s) {return true;} else {return false;}}; 008 Boolean result = p1exe.apply(3); 009 System.out.println("IOneParameter_result:" + result); //☆☆引数が2つのケース //引数が0以上でs<eの場合1を返し、それ以外は-1を返す(カリー化をしていないケース) 010 ITwoParameter<Integer, Integer, Boolean> p2exe = (s, e) -> {if (-1 < s && -1 < e && s < e) {return true;} else {return false;}}; 011 Boolean cmpResult1 = p2exe.apply(1, 2); 012 System.out.println("TwoParameter_cmpResult1:" + cmpResult1); //カリー化を実施し部分適用をしたケース(2ケース) 013 IOneParameter<Integer, IOneParameter<Integer, Boolean>> firstP2exe = p2exe.currying(); 014 IOneParameter<Integer, Boolean> secondP2exe = firstP2exe.compare(2); 015 Boolean cmpResult2a = secondP2exe.compare(1); 016 System.out.println("TwoParameter_cmpResult2a:" + cmpResult2a); 017 Boolean cmpResult2b = secondP2exe.compare(3); 018 System.out.println("ITwoParameter_cmpResult2b:" + cmpResult2b); //カリー化を実施しインターフェイスを取得せずに実行したケース 019 IOneParameter<Integer, IOneParameter<Integer, Boolean>> curryingP2exe = s -> e -> { 020 if (-1 < s && -1 < e && s < e) {return true;} else {return false;}}; 021 Boolean cmpResult2c = curryingP2exe.compare(1).compare(2); 022 System.out.println("CurryingTwoParameter_cmpResult2c:" + cmpResult2c); //☆☆引数が3つのケース //引数が0以上でs<e<vの場合1を返し、それ以外は-1を返す(カリー化をしないケース) 023 IThreeParameter<Integer, Integer, Integer, Boolean> p3exe = (s, e, v) -> { 024 if (-1 < s && -1 < e && -1 < v && s < e && e < v) {return true;} else {return false;}}; //カリー化を実施し部分適用をしたケース(2ケース) 025 IOneParameter<Integer, IOneParameter<Integer, IOneParameter<Integer, Boolean>>> firstP3exe = p3exe.currying(); 026 IOneParameter<Integer, Boolean> secondP3exe = firstP3exe.compare(1).compare(2); 027 Boolean cmpResult3a = secondP3exe.compare(1); 028 System.out.println("ThreeParameter_cmpResult3a:" + cmpResult3a); 029 Boolean cmpResult3b = secondP3exe.compare(3); 030 System.out.println("IThreeParameter_cmpResult3b:" + cmpResult3b); //カリー化を実施しインターフェイスを取得せずに実行したケース 031 IOneParameter<Integer, IOneParameter<Integer, IOneParameter<Integer, Boolean>>> curryingP3exe = s -> e -> v -> { 032 if (-1 < s && -1 < e && -1 < v && s < e && e < v) {return true;} else {return false;}}; 033 Boolean cmpResult3c = curryingP3exe.compare(1).compare(2).compare(3); 034 System.out.println("CurryingThreeParameter_cmpResult4:" + cmpResult3c); //☆☆引数が2つのケース //命令型(オブジェクト指向)タイプで実行したケース 035 Boolean cmpResult4a = compareByObject (2, 1); 036 setSint(2); 037 Boolean cmpResult4b = compareByObject (3); 038 System.out.println("ObjectTwoParameter_cmpResult4a:" + cmpResult4a); 039 System.out.println("ObjectTwoParameter_cmpResult4b:" + cmpResult4b); 040 } /** * 引数が1つのFunction型インターフェイス(compareメソッド定義) */ 041 interface IOneParameter<T, R> extends Function<T, R> { 042 default R compare(T t) { 043 return apply(t); 044 } 045 } /** * 引数が2つのBiFunction型インターフェイス(curryingメソッドでカリー化定義) */ 046 interface ITwoParameter<T, U, R> extends BiFunction<T, U, R> { 047 default IOneParameter<T, IOneParameter<U, R>> currying() { 048 return p1 -> p2 -> apply(p1, p2); 049 } 050 } /** * 引数が3つの関数型インターフェイス(curryingメソッドでカリー化定義) */ 051 interface IThreeParameter<T, U, V, R> { 052 R apply(T t, U u, V v); 053 default IOneParameter<T, IOneParameter<U, IOneParameter<V, R>>> currying() { 054 return p1 -> p2 -> p3 -> apply(p1, p2, p3); 055 } 056 } /** * sintメンバー変数の設定 */ 057 public static void setSint(Integer sint) { 058 CurryingTest.sint = sint; 059 } /** * 引数が1つの命令型(オブジェクト指向)タイプでの比較 */ 060 public static Boolean compareByObject (Integer e) { 061 return compareByObject (CurryingTest.sint, e); 062 } /** * 引数が2つの命令型(オブジェクト指向)タイプでの比較 */ 063 private static Boolean compareByObject (Integer s, Integer e) { 064 if (-1 < s && -1 < e && e > s) { 065 return true; 066 } else { 067 return false; 068 } 069 } 070 }オブジェクト指向(命令型)と関数型プログラミングの比較・相違
次に、Javaオブジェクト指向(命令型)と関数型プログラミングの基本的な相違を簡単に説明してみたいと思います。
オブジェクト指向は基本的にデータと操作(関数)を一体化(クラス化)してその独立性をできるだけ高めシステムの堅牢性を向上させるという方法です。
クラスが完全に独立できればクラス単位という視点でみれば副作用排除ができるということになります(正確に言えば違いますが)。
しかし、一般的にはそのようなケースはまれで、そもそもクラス(概念)間でメッセージをやり取りするというオブジェクト指向本来の指向とは異なるため、全面的な副作用排除は難しいと言えます。
関数型プログラミングは関数(操作)単位での副作用排除(参照透過性の保証)を目指し、関数(操作)間で共有する変数をなくし、関数(操作)内での代入文も無くし、目的・出力(このデータを用いてこの出力を得る)を示して、それを実現する手続きはシステム、ライブラリやユーティリティ等に任せていくという方法であり、データと操作の一体化という指向ではなく、逆にどちらかというとデータと関数(操作)は分離していく傾向にあります。
つまりオブジェクト指向と関数型プログラミングはベースとしては相反に近い関係にあるとも言えることになり、オブジェクト指向ベースによるシステム開発に関数型プログラミングを全面的に近い形で取り入れることはほぼ困難であり、あくまで部分的・表層的な取り入れということになります。現実的サンプルを使用したオブジェクト指向プログラミング→関数型プログラミング例
ではどの程度関数型プログラミングを取り入れることができるか、あるいは取り入れるべきなのかという事が焦点となります。
このことを考察・推察するために世の中で一般的に提示されている、「関数型プログラミングにすればこんなにコーディング量は少なくなります」、というようなごく簡単なサンプルではなく、少しだけ現実的なサンプルをオブジェクト指向(命令型)と関数型プログラミングの両方で実装して比較することにしました。
なおJava関数型プログラミングでは純粋に副作用を排除することは困難なので、
(1)メソッド外の変数を使用・代入しない(ただしコンストラクタは除外)。
(2)for文を使用しない。
(3)メソッド内でのローカル変数の代入は可能とするが極力少なくする。
という条件で作成してできるだけ副作用を少なくしています。
この現実的なサンプルではパイプラインや高階関数が複数使用されています。
現実的なサンプルは単純な商品在庫管理でメインクラス、在庫管理クラス、商品グループ別合計数及び平均値算出用ユーティリティクラスの3クラスで構成されています。
機能としては商品登録、在庫数更新(入庫/出庫)、商品グループ別合計数及び平均値算出があります。
"Object"と付加したクラスがオブジェクト指向(命令型)で、"Function"と付加したクラスが関数型プログラミングとなります。
StockObjectSampleMain:オブジェクト指向(命令型) 商品在庫管理メインクラス
StockObjectSample:オブジェクト指向(命令型) 商品在庫管理クラス
UtilityObjectSample:オブジェクト指向(命令型) 商品在庫管理用ユーティリティ
StockFunctionSampleMain:関数型プログラミング 商品在庫管理メインクラス
StockFunctionSample:関数型プログラミング 商品在庫管理クラス
UtilityFunctionSample:関数型プログラミング 商品在庫管理用ユーティリティ
処理内容はAタイプ商品の登録→在庫数チェック→入庫、Bタイプ商品の登録→在庫数チェック→入庫、全商品在庫数出力、グループ別合計数及び平均値出力としています。
グループ別合計数及び平均値算出はユーティリティクラスで算出しています。
以下5機能(メソッド)については処理形式が類似しているので標準関数インターフェイスを各々定義してexecuteFunctionメソッド(高階関数)を使用して関数引渡しで処理しています。
標準関数インターフェイスはStockFunctionSample.javaの038~076、executeFunctionメソッドは091~104で確認できます。
標準関数インターフェイスはコンストラクタで設定しています。
・商品確認:指定商品が登録されているか確認:checkGoodsメソッド
・在庫追加:入庫:stockメソッド
・在庫減少:出庫:shipメソッド
・在庫確認:指定数の在庫があるかの確認:checkStockメソッド
・商品・在庫追加:指定商品を登録し在庫数を設定:addGoods+stockメソッド
UtilityFunctionSample.javaではグループ別合計数及び平均値出力をStreamAPI(filter、flatmap、reduce、forEachメソッド使用)を用いて算出しています。StockObjectSampleMain.java001 package functiontest.stock; /** * オブジェクト指向(命令型):商品在庫管理メインクラス * 在庫情報:商品名、在庫数、商品説明の3要素で構成 * 商品名:"Goods"+商品グループ名(1文字)+連番で構成 */ 002 public class StockObjectSampleMain { 003 private static final String[] goodsNameA = {"GoodsA1", "GoodsA2", "GoodsA3"}; 004 private static final String[] goodsNameB = {"GoodsB1", "GoodsB2", "GoodsB3"}; 005 private static final int[] goodsStockA = {1, 2, 3}; 006 private static final int[] goodsStockB = {1, 2, 3}; 007 private static final String[] goodsDescriptionA = {"商品A1", "商品A2", "商品A3"}; 008 private static final String[] goodsDescriptionB = {"商品B1", "商品B2", "商品B3"}; 009 public static void main(String[] args) { 010 System.out.println("開始"); //初期化 011 StockObjectSample ss = initialize(); //在庫追加 012 ss.stock(goodsNameA[0], goodsStockA[0]); 013 ss.stock(goodsNameA[1], goodsStockA[1]); 014 ss.stock(goodsNameA[2], goodsStockA[2]); //商品確認 015 System.out.println("商品確認(GoodsA1):" + ss.checkGoods(goodsNameA[0])); //在庫確認 016 System.out.println("在庫確認(GoodsA2):" + ss.checkStock(goodsNameA[1], 2)); //在庫数確認 017 System.out.println("在庫数確認(GoodsA3):" + ss.getStock(goodsNameA[2])); //商品・在庫追加 018 update(goodsNameB[0], goodsStockB[0], goodsDescriptionB[0]).addGoods(); 019 update(goodsNameB[1], goodsStockB[1], goodsDescriptionB[1]).addGoods(); 020 update(goodsNameB[2], goodsStockB[2], goodsDescriptionB[2]).addGoods(); 021 update(goodsNameB[0], goodsStockB[0]).stock(); 022 update(goodsNameB[1], goodsStockB[1]).stock(); 023 update(goodsNameB[2], goodsStockB[2]).stock(); //在庫リスト出力 024 ss.outputStock(); //グループ別在庫数出力 025 ss.outputStockByGroup(); 026 System.out.println("終了"); 027 } /** * 初期化 */ 028 private static StockObjectSample initialize() { 029 StockObjectSample ss = new StockObjectSample(); 030 ss.addGoods(goodsNameA[0], 0, goodsDescriptionA[0]); 031 ss.addGoods(goodsNameA[1], 0, goodsDescriptionA[1]); 032 ss.addGoods(goodsNameA[2], 0, goodsDescriptionA[2]); 033 return ss; 034 } /** * 商品在庫情報の更新 */ 035 private static StockObjectSample update(String name, int num, String des) { 036 StockObjectSample ss = new StockObjectSample(name, num, des); 037 return ss; 038 } /** * 商品在庫情報の更新 */ 039 private static StockObjectSample update(String name, int num) { 040 StockObjectSample ss = new StockObjectSample(name, num); 041 return ss; 042 } 043 }StockObjectSample.java001 package functiontest.stock; 002 import java.util.ArrayList; 003 import java.util.List; 004 import functiontest.utility.UtilityObjectSample; /** * オブジェクト指向(命令型):商品在庫管理クラス * 在庫情報:商品名、在庫数、商品説明の3要素で構成 * 商品名:"Goods"+商品グループ名(1文字)+連番で構成 */ 005 public class StockObjectSample { 006 private static List<String> goodsNameList = new ArrayList<String>(); 007 private static List<Integer> numberStockList = new ArrayList<Integer>(); 008 private static List<String> descriptionList = new ArrayList<String>(); 009 private String goodsName; 010 private Integer numberStock; 011 private String description; /** * コンストラクタ */ 012 public StockObjectSample() { 013 numberStock = 0; 014 } /** * コンストラクタ */ 015 public StockObjectSample(String name, int num, String des) { 016 goodsName = name; 017 numberStock = num; 018 description = des; 019 } /** * コンストラクタ */ 020 public StockObjectSample(String name, int num) { 021 goodsName = name; 022 numberStock = num; 023 } /** * 商品登録 */ 024 public void addGoods() { 025 addGoods(goodsName, numberStock, description); 026 } /** * 商品登録 */ 027 public void addGoods(String name, int num, String des) { 028 if (-1 < checkAdditionalGoods(name)) { 029 goodsNameList.add(name); 030 numberStockList.add(num); 031 setDescription(des); 032 } 033 } /** * 入庫 */ 034 public int stock() { 035 return stock(goodsName, numberStock); 036 } /** * 入庫 */ 037 public int stock(String name, int num) { 038 int index = checkGoods(name); 039 int sval = -1; 040 if (-1 < index) { 041 sval = numberStockList.get(index).intValue() + num; 042 numberStockList.set(index, sval); 043 } 044 return sval; 045 } /** * 出庫 */ 046 public void ship() { 047 ship(goodsName, numberStock); 048 } /** * 出庫 */ 049 public int ship(String name, int num) { 050 int index = checkGoods(name); 051 int sval = -1; 052 if (-1 < index) { 053 sval = numberStockList.get(index).intValue() - num; 054 numberStockList.set(index, sval); 055 } 056 return sval; 057 } /** * 在庫数チェック */ 058 public boolean checkStock() { 059 return checkStock(goodsName, numberStock); 060 } /** * 在庫数チェック */ 061 public boolean checkStock(String name, int num) { 062 boolean result = true; 063 int index = checkGoods(name); 064 if (-1 < index) { 065 if (num > numberStockList.get(index).intValue()) { 066 result = false; 067 } 068 } 069 return result; 070 } /** * 在庫数取得 */ 071 public int getStock() { 072 return getStock(goodsName); 073 } /** * 在庫数取得 */ 074 public int getStock(String name) { 075 int result = 0; 076 int index = checkGoods(name); 077 if (-1 < index) { 078 result = numberStockList.get(index).intValue(); 079 } 080 return result; 081 } /** * 在庫数クリア */ 082 public void clearStock() { 083 clearStock(goodsName); 084 } /** * 在庫数クリア */ 085 public void clearStock(String name) { 086 int index = checkGoods(name); 087 if (-1 < index) { 088 numberStockList.set(index, 0); 089 } 090 } /** * 在庫情報出力 */ 091 public void outputStock() { 092 StringBuilder sb = new StringBuilder(); 093 for (int i = 0; i < goodsNameList.size(); i++) { 094 if (0 != i) { 095 sb.append("\n"); 096 } 097 sb.append("商品名:"); 098 sb.append(goodsNameList.get(i)); 099 sb.append(" 在庫数:"); 100 sb.append(numberStockList.get(i)); 101 sb.append(" 説明:"); 102 sb.append(descriptionList.get(i)); 103 } 104 outputMessage(sb.toString()); 105 } /** * グループ別在庫数出力 */ 106 public void outputStockByGroup() { 107 List<List<String>> groupStockList = UtilityObjectSample.getStockOfGroup(goodsNameList, 5, 6, numberStockList); 108 outputMessage("商品グループ名, 合計数, 平均数"); 109 for (int i = 0; i <groupStockList.size(); i++) { 110 outputMessage(groupStockList.get(i).get(0) + ", " + groupStockList.get(i).get(1) + ", " + groupStockList.get(i).get(2) + ", "); 111 } 112 } /** * 追加商品チェック */ 113 public int checkAdditionalGoods(String name) { 114 if (0 > checkGoodsName(name)) { 115 return -1; 116 } 117 if (goodsNameList.contains(name)) { 118 outputMessage("既に商品名は登録されています。"); 119 return -1; 120 } 121 return 0; 122 } /** * 商品チェック */ 123 public int checkGoods(String name) { 124 if (0 > checkGoodsName(name)) { 125 return -1; 126 } 127 int index = goodsNameList.indexOf(name); 128 if (0 > index) { 129 outputMessage(name + "は登録されていません。"); 130 return -1; 131 } 132 return index; 133 } /** * 商品名チェック */ 134 public int checkGoodsName(String name) { 135 if (null == goodsNameList) { 136 outputMessage("商品名が全く登録されていません。"); 137 return -1; 138 } 139 if (null == name || "".equals(name)) { 140 outputMessage("商品名が指定されていません。"); 141 return -1; 142 } 143 return 0; 144 } /** * メッセージ出力 */ 145 private void outputMessage(String mes) { 146 System.out.println(mes); 147 } /** * 説明を設定 */ 148 public void setDescription(String des) { 149 if (null == des) { 150 des = ""; 151 } 152 descriptionList.add(des); 153 } 154 }UtilityObjectSample.java001 package functiontest.utility; 002 import java.util.ArrayList; 003 import java.util.Collections; 004 import java.util.List; /** * オブジェクト指向(命令型) 商品在庫管理用ユーティリティ */ 005 public class UtilityObjectSample { /** * グループ別合計数及び平均値算出 */ 006 public static List<List<String>> getStockOfGroup(List<String> goodsData, int sgroup, int egroup, List<Integer> stockList) { 007 List<String> list = new ArrayList<String>(); 008 for (String name : goodsData) { 009 String grp = name.substring(sgroup, egroup); 010 if (!list.contains(grp)) { 011 list.add(grp); 012 } 013 } 014 Collections.sort(list); 015 List<List<String>> groupStockList = new ArrayList<List<String>>(); 016 for (String gname : list) { 017 List<String> itemList = new ArrayList<String>(); 018 int count = 0; 019 float totalCount = 0; 020 for (int i = 0; i < goodsData.size(); i++) { 021 if (-1 < goodsData.get(i).indexOf(gname)) { 022 ++count; 023 totalCount = totalCount + stockList.get(i); 024 } 025 } 026 itemList.add(gname); 027 itemList.add(String.valueOf(totalCount)); 028 itemList.add(String.valueOf(totalCount / count)); 029 groupStockList.add(itemList); 030 } 031 return groupStockList; 032 } 033 }StockFunctionSampleMain.java001 package functiontest.stock; /** * 関数型プログラミング:商品在庫管理メインクラス * 在庫情報:商品名、在庫数、商品説明の3要素で構成 * 商品名:"Goods"+商品グループ名(1文字)+連番で構成 */ 002 public class StockFunctionSampleMain { 003 private static final String[] goodsNameA = {"GoodsA1", "GoodsA2", "GoodsA3"}; 004 private static final String[] goodsNameB = {"GoodsB1", "GoodsB2", "GoodsB3"}; 005 private static final int[] goodsStockA = {1, 2, 3}; 006 private static final int[] goodsStockB = {1, 2, 3}; 007 private static final String[] goodsDescriptionA = {"商品A1", "商品A2", "商品A3"}; 008 private static final String[] goodsDescriptionB = {"商品B1", "商品B2", "商品B3"}; 009 public static void main(String[] args) { 010 System.out.println("開始"); //初期化 011 StockFunctionSample ss = initialize(); //在庫追加 012 ss.executeFunction(goodsNameA[0], goodsStockA[0], ss.getICheckGoods(), ss.getIStock()); 013 ss.executeFunction(goodsNameA[1], goodsStockA[1], ss.getICheckGoods(), ss.getIStock()); 014 ss.executeFunction(goodsNameA[2], goodsStockA[2], ss.getICheckGoods(), ss.getIStock()); //商品確認 015 System.out.println("商品確認(GoodsA1):" + ss.executeFunction(goodsNameA[0], -1, ss.getICheckGoods(), null)); //在庫確認 016 System.out.println("在庫確認(GoodsA2):" + ss.executeFunction(goodsNameA[1], 2, ss.getICheckGoods(), ss.getICheckStock())); //在庫数確認 017 System.out.println("在庫数確認(GoodsA3):" + ss.getStock(goodsNameA[2], ss.getGoodsNameList())); //商品・在庫追加 018 StockFunctionSample ss1 = update(goodsNameB[0], goodsStockB[0], goodsDescriptionB[0]); 019 StockFunctionSample ss2 = update(goodsNameB[1], goodsStockB[1], goodsDescriptionB[1]); 020 StockFunctionSample ss3 = update(goodsNameB[2], goodsStockB[2], goodsDescriptionB[2]); 021 ss1.addGoods(null, 0,null); 022 ss2.addGoods(null, 0,null); 023 ss3.addGoods(null, 0,null); 024 update(goodsNameB[0], goodsStockB[0]).executeFunction(goodsNameB[0], goodsStockB[0], ss1.getICheckGoods(), ss1.getIStock()); 025 update(goodsNameB[1], goodsStockB[1]).executeFunction(goodsNameB[1], goodsStockB[1], ss2.getICheckGoods(), ss2.getIStock()); 026 update(goodsNameB[2], goodsStockB[2]).executeFunction(goodsNameB[2], goodsStockB[2], ss3.getICheckGoods(), ss3.getIStock()); //在庫リスト出力 027 ss.outputStock(ss.getGoodsNameList(), ss.getNumberStockList(), ss.getDescriptionList()); //グループ別在庫数出力 028 ss.outputStockByGroup(ss.getGoodsNameList(), ss.getNumberStockList()); 029 System.out.println("終了"); 030 } /** * 初期化 */ 031 private static StockFunctionSample initialize() { 032 StockFunctionSample ss = new StockFunctionSample(); 033 ss.addGoods(goodsNameA[0], 0, goodsDescriptionA[0]); 034 ss.addGoods(goodsNameA[1], 0, goodsDescriptionA[1]); 035 ss.addGoods(goodsNameA[2], 0, goodsDescriptionA[2]); // Printer.printBasicObject("check01", ss.getGoodsNameList()); 036 return ss; 037 } /** * 商品在庫情報の更新 */ 038 private static StockFunctionSample update(String name, int num, String des) { 039 StockFunctionSample ss = new StockFunctionSample(name, num, des); 040 return ss; 041 } /** * 商品在庫情報の更新 */ 042 private static StockFunctionSample update(String name, int num) { 043 StockFunctionSample ss = new StockFunctionSample(name, num); 044 return ss; 045 } 046 }StockFunctionSample.java001 package functiontest.stock; 002 import java.util.ArrayList; 003 import java.util.List; 004 import java.util.function.BiFunction; 005 import java.util.function.Consumer; 006 import java.util.function.Function; 007 import java.util.stream.Collectors; 008 import java.util.stream.IntStream; 009 import functiontest.utility.UtilityFunctionSample; /** * 関数型プログラミング:商品在庫管理クラス * 在庫情報:商品名、在庫数、商品説明の3要素で構成 * 商品名:"Goods"+商品グループ名(1文字)+連番で構成 */ 010 public class StockFunctionSample { 011 private static List<String> goodsNameList = new ArrayList<String>(); 012 private static List<Integer> numberStockList = new ArrayList<Integer>(); 013 private static List<String> descriptionList = new ArrayList<String>(); 014 private GoodsInterface IAdditionalGoods; 015 private Function<String, Integer> ICheckGoods; 016 private BiFunction<Integer, Integer, Integer> IStock; 017 private BiFunction<Integer, Integer, Integer> IShip; 018 private BiFunction<Integer, Integer, Integer> ICheckStock; 019 private Function<Integer, Integer> IGetNumberStock; 020 private Consumer<Integer> IClearStock; /** * コンストラクタ */ 021 public StockFunctionSample() { 022 implementsInterface(null, 0, null, goodsNameList, numberStockList, descriptionList); 023 } /** * コンストラクタ */ 024 public StockFunctionSample(String name, int num, String des) { 025 implementsInterface(name, num, des, goodsNameList, numberStockList, descriptionList); 026 } /** * コンストラクタ */ 027 public StockFunctionSample(String name, int num) { 028 implementsInterface(name, num, null, goodsNameList, numberStockList, descriptionList); 029 } /** * 関数インターフェイスの実装 */ 030 private void implementsInterface(String goodsName, Integer numberStock, String description, List<String> goodsNameList, List<Integer> numberStockList, List<String> descriptionList) { 031 implementsGoodsInterface(goodsName, numberStock, description, goodsNameList, numberStockList, descriptionList); 032 implementsStockInterface(goodsNameList, numberStockList, descriptionList); 033 } //商品登録用自作関数型インターフェース 034 @FunctionalInterface 035 private interface GoodsInterface { 036 public void addGoods(String name, int num, String des); 037 } //商品登録用インターフェースの実装 038 private void implementsGoodsInterface(String goodsName, Integer numberStock, String description, List<String> goodsNameList, List<Integer> numberStockList, List<String> descriptionList) { 039 IAdditionalGoods = (x, y, z) -> { 040 if (null != x) { 041 if (-1 < checkAdditionalGoods(x, goodsNameList)) { 042 addGoodsData(x, y, z, goodsNameList, numberStockList, descriptionList); 043 } 044 } else { 045 if (-1 < checkAdditionalGoods(goodsName, goodsNameList)) { 046 addGoodsData(goodsName, numberStock, description, goodsNameList, numberStockList, descriptionList); 047 } 048 } 049 }; 050 } //在庫数関連インターフェースの実装 051 private void implementsStockInterface(List<String> goodsNameList, List<Integer> numberStockList, List<String> descriptionList) { 052 ICheckGoods = (x) -> { return checkGoods(x, goodsNameList);}; 053 IStock = (x, y) -> { 054 int sval = numberStockList.get(x).intValue() + y; 055 numberStockList.set(x, sval); 056 return sval; 057 }; 058 IShip = (x, y) -> { 059 int sval = numberStockList.get(x).intValue() - y; 060 numberStockList.set(x, sval); 061 return sval; 062 }; 063 ICheckStock = (x, y) -> { 064 int result = 0; 065 if (y > numberStockList.get(x).intValue()) { 066 result = -1; 067 } 068 return result; 069 }; 070 IGetNumberStock = (x) -> { 071 return numberStockList.get(x).intValue(); 072 }; 073 IClearStock = (x) -> { 074 numberStockList.set(x, 0); 075 }; 076 } /** * 商品登録 */ 077 private void addGoodsData(String name, int num, String des, List<String> goodsNameList, List<Integer> numberStockList, List<String> descriptionList) { 078 goodsNameList.add(name); 079 numberStockList.add(num); 080 setDescription(des, descriptionList); 081 } /** * 商品説明設定 */ 082 private void setDescription(String des, List<String> descriptionList) { 083 if (null == des) { 084 des = ""; 085 } 086 descriptionList.add(des); 087 } /** * 商品登録 */ 088 public void addGoods(String name, int num, String des) { 089 IAdditionalGoods.addGoods(name, num, des); 090 } /** * 指定関数を実行するメソッド */ 091 public int executeFunction(String name, Integer value, Function<String, Integer> ckfnc, BiFunction<Integer, Integer, Integer> dofnc) { 092 int result = -1; 093 if (null != ckfnc) { 094 if (null != name) { 095 result = ckfnc.apply(name); 096 } 097 } 098 if (-1 < result && null != dofnc) { 099 if (-1 < value) { 100 result = dofnc.apply(result, value); 101 } 102 } 103 return result; 104 } /** * 在庫数取得 */ 105 public int getStock(String name, List<String> goodsNameList) { 106 int result = 0; 107 int index = checkGoods(name, goodsNameList); 108 if (-1 < index) { 109 result = IGetNumberStock.apply(index); 110 } 111 return result; 112 } /** * 在庫数クリア */ 113 public void clearStock(String name, List<String> goodsNameList) { 114 int index = checkGoods(name, goodsNameList); 115 if (-1 < index) { 116 IClearStock.accept(index); 117 } 118 } /** * 在庫情報出力 */ 119 public void outputStock(List<String> goodsNameList, List<Integer> numberStockList, List<String> descriptionList) { 120 List<String> list = IntStream.range(0, goodsNameList.size()) 121 .mapToObj(i -> { 122 StringBuilder sbc = new StringBuilder(); 123 sbc.append("商品名:"); 124 sbc.append(goodsNameList.get(i)); 125 sbc.append(" 在庫数:"); 126 sbc.append(numberStockList.get(i)); 127 sbc.append(" 説明:"); 128 sbc.append(descriptionList.get(i)); 129 return sbc.toString(); 130 }) 131 .collect(Collectors.toList()); 132 outputMessage(list); 133 } /** * グループ別在庫数出力 */ 134 public void outputStockByGroup(List<String> goodsNameList, List<Integer> numberStockList) { 135 List<List<String>> groupStockList = UtilityFunctionSample.getStockOfGroup(goodsNameList, 5, 6, numberStockList, 0, 0); 136 outputMessage("商品グループ名, 合計数, 平均数"); 137 List<String> list = IntStream.range(0, groupStockList.get(0).size()) 138 .mapToObj(i -> { 139 StringBuilder sbc = new StringBuilder(); 140 sbc.append(groupStockList.get(0).get(i)); 141 sbc.append( ", "); 142 sbc.append(groupStockList.get(1).get(i)); 143 sbc.append( ", "); 144 sbc.append(groupStockList.get(2).get(i)); 145 sbc.append( ", "); 146 return sbc.toString(); 147 }) 148 .collect(Collectors.toList()); 149 outputMessage(list); 150 } /** * 追加商品チェック */ 151 public int checkAdditionalGoods(String name, List<String> goodsNameList) { 152 if (0 > checkGoodsName(name, goodsNameList)) { 153 return -1; 154 } 155 if (null != name && goodsNameList.contains(name)) { 156 outputMessage("既に商品名は登録されています。 " + name); 157 return -1; 158 } 159 return 0; 160 } /** * 商品チェック */ 161 public int checkGoods(String name, List<String> goodsNameList) { 162 if (0 > checkGoodsName(name, goodsNameList)) { 163 return -1; 164 } 165 int index = goodsNameList.indexOf(name); 166 if (0 > index) { 167 outputMessage(name + "は登録されていません。"); 168 return -1; 169 } 170 return index; 171 } /** * 商品名チェック */ 172 public int checkGoodsName(String name, List<String> goodsNameList) { 173 if (null == goodsNameList) { 174 outputMessage("商品名が全く登録されていません。 " + name); 175 return -1; 176 } 177 if (null == name || "".equals(name)) { 178 outputMessage("商品名が指定されていません。 " + name); 179 return -1; 180 } 181 return 0; 182 } /** * メッセージ出力 */ 183 public void outputMessage(String mes) { 184 System.out.println(mes); 185 } /** * メッセージ出力 */ 186 private void outputMessage(List<String> list) { 187 list.stream().forEach(s -> {outputMessage(s);}); 188 } 189 public Function<String, Integer> getICheckGoods() { 190 return ICheckGoods; 191 } 192 public BiFunction<Integer, Integer, Integer> getIStock() { 193 return IStock; 194 } 195 public BiFunction<Integer, Integer, Integer> getIShip() { 196 return IShip; 197 } 198 public BiFunction<Integer, Integer, Integer> getICheckStock() { 199 return ICheckStock; 200 } 201 public List<String> getGoodsNameList() { 202 return goodsNameList; 203 } 204 public List<Integer> getNumberStockList() { 205 return numberStockList; 206 } 207 public List<String> getDescriptionList() { 208 return descriptionList; 209 } 210 }UtilityFunctionSample.java001 package functiontest.utility; 002 import java.util.ArrayList; 003 import java.util.Collections; 004 import java.util.List; 005 import java.util.stream.Collectors; 006 import java.util.stream.Stream; /** * 関数型プログラミング 商品在庫管理用ユーティリティ */ 007 public class UtilityFunctionSample { /** * グループ別合計数及び平均値算出 */ 008 public static List<List<String>> getStockOfGroup(List<String> goodsData, int sgroup, int egroup, List<Integer> stockList, int snumeric, int enumeric) { //グループ区分け及びソート 009 List<String> groupList = new ArrayList<String>(); 010 goodsData.stream().filter(vf -> {if(!groupList.contains(vf.substring(sgroup, egroup))) {return true;}return false;}).forEach(ve -> { 011 groupList.add(ve.substring(sgroup, egroup)); 012 }); 013 Collections.sort(groupList); //グループ別合計数及び平均値算出 014 Stream<String> groupStock = groupList.stream().flatMap(gname -> { //gnameグループ名と一致する在庫数リストを作成 015 List<Integer> countList = new ArrayList<Integer>(); 016 goodsData.stream().filter(vf -> {if (-1 < vf.indexOf(gname)) {return true;}return false;}).forEach(ve -> { 017 if (null == stockList) { 018 countList.add(new Integer(ve.substring(snumeric, enumeric))); 019 } else { 020 countList.add(stockList.get(goodsData.indexOf(ve))); 021 } 022 }); //グループ別合計数 023 float totalCount = countList.stream().reduce((i, t) -> i + t).get(); 024 Stream<String> gelement = Stream.of("0"+gname); 025 gelement = Stream.concat(gelement, Stream.of("1"+String.valueOf(totalCount))); //グループ別平均値 026 gelement = Stream.concat(gelement, Stream.of("2"+String.valueOf(totalCount / countList.size()))); 027 return gelement; 028 }); //groupStock先頭文字 0:グループ名/1:合計数/2:平均値 029 List<String> glist = groupStock.collect(Collectors.toList()); 030 List<String> nameList = createGroupList(glist.stream(), "0", 0, 1); 031 List<String> totList = createGroupList(glist.stream(), "1", 0, 1); 032 List<String> aveList = createGroupList(glist.stream(), "2", 0, 1); 033 List<List<String>> groupStockList = new ArrayList<List<String>>(); 034 groupStockList.add(nameList); 035 groupStockList.add(totList); 036 groupStockList.add(aveList); 037 return groupStockList; 038 } /** * Stream<<String>から指定文字範囲が指定キーに一致するデータ(2文字目以降)をリスト化して戻す */ 039 public static List<String> createGroupList(Stream<String> strm, String key, int sindx, int eindx) { 040 List<String> list = new ArrayList<String>(); 041 strm.filter(s -> {if (key.equals(s.substring(sindx, eindx))) {return true;}return false;}).forEach(s -> { 042 list.add(s.substring(1)); 043 }); 044 return list; 045 } 046 }現実的サンプルでの考察・推測
まずは、後述のまとめを含め現実的サンプルでの考察・推測をするための前提を以下2項目とします。
・オブジェクト指向ベースのシステム開発で、純粋な関数型プログラミングの知識が少ないメンバーがほとんどである。
・StreamAPIの利用に関する知識は保有している。
Javaで関数型プログラミングを取り入れる目的としては、
(1)副作用を少なくし品質をあげる。
(2)コーディング量を少なくする。
が一般的に挙げられています。
(1)については確かに表層的・部分的には副作用を減少させることが可能ではありますが、上述で述べているとおり本質的な対応は困難ですし、無理矢理適用すればメソッド引数がかなり多くなりプログラミング上好ましくない状態となり、データと処理操作が乖離していきベースとなるオブジェクト指向から遠ざかっていくこととなります。
今回の現実的サンプルでもメソッドの引数はかなり増加していてプログラムの見通しが悪くなり無駄も発生しています。
(2)のコーディング量については、公開サイトでよく見られる数行のサンプルでは少なるように思えますが、一般的にはforループ内は少し複雑な処理となったり、インデックス使用となることが比較的多いので余計な対応が必要となったりして結局それ程の効果はないと推測され、今回の現実的サンプルでも少なくはなっていない。
また、streamAPIを多用すればstream化や逆stream化処理での処理時間が遅くなることも想定されます。まとめ
現実的サンプルでの考察・推測をふまえて、最終的にJavaでは関数型プログラミングをどのように扱えばよいのかをまとめたいと思います。
あくまで個人的な意見・対応方針となりますが、
(a)関数型プログラミング主体でシステム開発したい場合はHaskell等の純粋関数型言語を使用しJavaでは行わない。
(b)システム開発設計はきちんとオブジェクト指向で行う。
(c)あえて副作用をなくすため(参照透過性保証)にオブジェクト指向に反する設計やプログラミングは行わない。
(d)無理して関数型プログラミングに関係した手法(自作高階関数、カリー化など)を取り込まない。
(e)Javaで標準として用意されている関数プログラミング用APIをフィットする範囲で利用する範囲にとどめてプログラミングを行う。
(f)コレクションfor文をなくすために毎回あえてstreamAPIを使用することはしない。
(g)for文内が単純でstreamAPIにフィットする処理ケース以外はfor文をそのまま使用した方が見通しも良く、処理速度劣化も無い(並列ストリーム使用時を除く)。
(h)静的ユーティリティ用メソッドはオブジェクトのメソッドよりも多少ではあるが副作用排除はしやすいのではと思われる。
以上のまとめをざっくり言えば、Javaを用いてオブジェクト指向で開発する範囲においては、表層的・部分的な副作用回避のために関数型プログラミングを多用する意義はそれほど大きくはなく、コーディング量の減少効果も全体的にみればそれほど期待はできないと思われる。
ただ、この関数型プログラミング効果は開発対象や開発メンバーに左右されるので、よく調査しベンチマークの上どの程度取り入れるべきかを決定することが妥当である事はいうまでもない。
という内容が今回の活動での結論・総括となりました。最後に
以上です。最後までお読み頂き有難うございました。
- 投稿日:2021-01-21T14:27:01+09:00
Java Silver SE11 継承とオーバーライドの間違えやすい部分
Java Silver SE11の問題を解いていて継承とオーバーライドに関する問題を間違えるので備忘録として残す。
大事なのは何の型で宣言されて、何のインスタンスを生成しているか。
メソッドはどのようなアクセス修飾子が付与されているか。public class Main { public static void main(String[] args) throws Exception { //case A Super superClass = new Sub(); //case B // Super superClass = new Super(); Sub subClass = new Sub(); //code 1 // subClass = superClass; //code 2 // subClass = (Sub)superClass; //code 3 // superClass = subClass; superClass.print(); subClass.print(); } } class Super{ //case C privateで修飾 //case D staticで修飾 void print(){ System.out.println("Super"); } } class Sub extends Super{ //case D staticで修飾 void print(){ System.out.println("Sub"); } }それぞれの実行結果がどうなるか。
case Aの場合
- code 1の時
サブクラス型の変数subClassにスーパークラス型の変数superClassを代入しているためコンパイルエラー
- code 2の時
スーパークラス型の変数superClassはSubクラスのインスタンス生成しているため、ダウンキャストされて代入される。実行結果はオーバーライドされたメソッドが優先されるので「Sub Sub」
- code 3の時
スーパークラス型の変数superClassにサブクラス型の変数subClassを代入しているため自動的にアップキャストされ、実行結果も同様にオーバーライドされたメソッドが優先されて「Sub Sub」
case Bの場合
- code 1の時
case Aの場合と同様にコンパイルエラー
- code 2の時
明示的にキャストしているためコンパイルエラーは起こらない。しかし、スーパークラス型の変数superClassはSuperクラスのインスタンスを生成しており、サブクラスの差分を持たないため、ClassCastExceptionがスローされる(実行時エラーが起きる)
- code 3の時
case Aの場合と同様にアップキャストされ、実行結果は「Sub Sub」
case Cの場合
- スーパークラスであるSuperクラスのprintメソッドがprivateで修飾されると、オーバーライドされているとみなされず、生成されているインスタンスに関わらず、変数の型のprintメソッドが実行される。
case Dの場合
- 両方のprintメソッドがstaticで修飾された場合、変数の型のprintメソッドが実行される。
- staticで修飾する場合は両方のメソッドを修飾しないとコンパイルエラーになる。
- 投稿日:2021-01-21T12:39:29+09:00
DynamoDB Enhanced Client にて、なぜかインデックステーブルに対してアクセスしてしまう
エラー内容
エラー文は以下の通り。
java.lang.IllegalArgumentException: A sort key value was supplied for an index that does not support one. Index: $PRIMARY_INDEX結論
属性に対するゲッター・セッターの定義がおかしい。(敗北N回目、、、)
対処
適切なゲッター・セッターを設定しましょう。
具体的には以下の2点をチェック。
- メソッド名
- 引数の型
発生状況
筆者の場合は以下のような形で発生しました。
@DynamoDbBean @Setter public class Foo { private String name; private String date; @DynamoDbAttribute("name") public String getName() { return name; } @DynamoDbAttribute("date") public String getDate() { return date; } public setDate(Date date) { this.date = new SimpleDateFormat("yyyy-MM-dd").format(date); } }原因は
Foo#setDate(Date)
ですね。
テーブルのメタデータにはゲッターとセッターを設定する必要があります。
@DynamoDbAttribute
アノテーションで設定する場合は、メソッドの名称と引数の型を、フィールドと合わせる必要があります。
フィールドdate
はString型
で定義されていますが、セッターではDate型
を受け取ってしまっています。
Lombockの@Setter
アノテーションで生成されるセッターとはシグネチャが異なるので共存可能であり、Foo#setDate(String)
も定義されています。
しかし、テーブルのメタデータとしてはFoo#setDate(Date)
が拾われてしまうようです。
そこからなぜか、インデックステーブルへアクセスしようとし、エラーが発生してしまうようですね。
- 投稿日:2021-01-21T02:35:54+09:00
Selenium マウスホバーしてからクリック
Seleniumを使用してDOM要素をマウスホバーしてからクリックする方法について明記します。
Javaimport org.openqa.selenium.interactions.Actions; public static void autoHoverClick(WebDriver driver, WebElement element){ Actions act = new Actions(driver); act.moveToElement(element).perform(); // 指定したDOM要素をホバーする(hover) element.click(); // DOM要素をクリック }
- 投稿日:2021-01-21T02:35:54+09:00
Selenium DOM要素をホバー&クリックする方法
Seleniumを使用してDOM要素をホバーしてからクリックする方法について明記します。
SeleniumはWebサイトやWebページの閲覧を自動化することができます。
今回は、抽出したDOM要素をホバーしてからクリックする方法について明記します。
要素をホバーすることでプログラム上、マウスカーソルがDOM要素に重なっている状態
を表現します。Javaimport org.openqa.selenium.interactions.Actions; public static void autoHoverClick(WebDriver driver, WebElement element){ Actions act = new Actions(driver); act.moveToElement(element).perform(); // 指定したDOM要素をホバーする(hover) element.click(); // DOM要素をクリック }
- 投稿日:2021-01-21T00:28:13+09:00
たったこれだけ!コードフォーマット未適用を防ぐ方法
はじめに
「コードフォーマットが適用されておりません。」
こんな指摘をした人、された人。いるのではないでしょうか?
今回はコードフォーマットをCIツールで実行し、開発者はフォーマッターの実行をしないような仕組みを紹介します。この記事を書こうと思った動機
お客様指定のコードスタイルのある規約があるプロダクトにおいて以下の問題に直面し、結果的に工数が増大したため。
ケース1. ローカルで「コードフォーマッター」が適用されずにPull Requestを出すケース
ケース2. すり抜けコードをベースに修正し、コードフォーマッターを適用したら、改修箇所以外の差分が抽出された
【コードフォーマッターが適用されないわけ】
- 開発環境構築手順書・開発フローにコードフォーマッターの記載されていない。(曖昧なドキュメント)
- 開発メンバー間でのフローの引き継ぎもない(開発メンバーが流動的)
折角コードフォーマッターの設定ファイルを用意しても、運用されていなければ意味がありません。
ドキュメントをしっかり書いたとしても、プロジェクトメンバー全員の開発環境設定が同じであることを確認するにも限度があります。そのため、同じような悩みを持っているコードオーナの方向けにCIツール側でコードフォーマッターを適用する方法について紹介しようと思いました。CIツールでコードフォーマッターを実行する
解決アイディア : 開発環境に依存するのでは無く、フォーマッタはレビュー実施前に自動的に適用される方式とする。
前提
CIツールとコードフォーマッターを連携するために以下の要件を満たす必要があります。
- バージョン管理ツールへのPushやPull Request(Merge Request)を検出する仕組み
- 以下に対応したコードフォーマッターが存在すること
- コマンドラインから実行可能で且つ、スタンドアロンで動作可能なツールであること
- CIツールに対応したツールであること
後述の方式は上記の要件を満たしたツールを選定しておりますが、もし存在しない( 特に [1.] ) は自作することもご検討ください。
コードフォーマット自動適用例
今回のサンプルは以下の仕様のもとで作成するもとのする。
項目 内容 プログラミング言語 Java コードスタイル Google Java Style Guide に準拠 ツール google-java-format CIツールで自動的にコードフォーマッターを実行する
CI用コード(GitHub Action)
ポイント:on pull_reqestにて取得されたブランチはPull Requestしたブランチとは別ブランチのため、checkout時にPull Requestを行ったブランチを明示的して取得する必要があります。
name: Reformat Java source code on: pull_request: jobs: format: runs-on: ubuntu-latest steps: # Pull Requestされたブランチをチェックアウト(★refがポイント) - uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} # Java のセットアップ - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 # Gitの設定 - name: Git settings run: | # Git リポジトリの設定 git config --local user.name "ユーザ名" git config --local user.email "E-Mailアドレス" # コードフォーマットの実行 - name: Code format run: | wget -q -O google-java-format.jar \ https://github.com/google/google-java-format/releases/download/google-java-format-1.9/google-java-format-1.9-all-deps.jar java -jar google-java-format.jar -replace $(git ls-files src/**/*.java) # 変更確認 - name: Check for modified files id: git-check run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi) # 変更されていた場合、コミット・プッシュを行う - name: Push if: steps.git-check.outputs.modified == 'true' run: | git commit -am "Automated :Reformat Java source code." git push実行結果
PullRequst実施、変更時にコードフォーマッターが適用された新しいコミットが追加される
サンプルリポジトリ
上記は以下のリポジトリで実現されております。(ブランチ: feature/format-action)
https://github.com/yukiusa/github-action-auto-java-format-sample/tree/feature/format-actionさいごに
いかがだったでしょうか?
今回はCIツールを利用し「開発環境に依存せずルールを遵守させる仕組み」について紹介しました。
本取り組みを実施することで
- 開発環境の作成の手間を減らす
- チェックの工数を削減する
- レビューア・レビューイの負荷を減らす
などの効果が期待できるかと思います。
(※コードスタイルがお粗末であれば意味をなさないため、コードスタイルのガイドライン作りは慎重に行う必要があります)今後の業務の改善案の一つとしてお役立ていただければ幸いでございます。
最後まで読んでいただきましてありがとうございました。
- 投稿日:2021-01-21T00:28:13+09:00
たったこれだけ!チーム開発でコードフォーマット未適用を防ぐ方法
はじめに
「コードフォーマットが適用されておりません。」
こんな指摘をした人、された人。いるのではないでしょうか?
今回はコードフォーマットをCIツールで実行し、開発者はフォーマッターの実行をしないような仕組みを紹介します。この記事を書こうと思った動機
お客様指定のコードスタイルのある規約があるプロダクトにおいて以下の問題に直面し、結果的に工数が増大したため。
ケース1. ローカルで「コードフォーマッター」が適用されずにPull Requestを出すケース
ケース2. すり抜けコードをベースに修正し、コードフォーマッターを適用したら、改修箇所以外の差分が抽出された
【コードフォーマッターが適用されないわけ】
- 開発環境構築手順書・開発フローにコードフォーマッターの記載されていない。(曖昧なドキュメント)
- 開発メンバー間でのフローの引き継ぎもない(開発メンバーが流動的)
折角コードフォーマッターの設定ファイルを用意しても、運用されていなければ意味がありません。
ドキュメントをしっかり書いたとしても、プロジェクトメンバー全員の開発環境設定が同じであることを確認するにも限度があります。そのため、同じような悩みを持っているコードオーナの方向けにCIツール側でコードフォーマッターを適用する方法について紹介しようと思いました。CIツールでコードフォーマッターを実行する
解決アイディア : 開発環境に依存するのでは無く、フォーマッタはレビュー実施前に自動的に適用される方式とする。
前提
CIツールとコードフォーマッターを連携するために以下の要件を満たす必要があります。
- バージョン管理ツールへのPushやPull Request(Merge Request)を検出する仕組み
- 以下に対応したコードフォーマッターが存在すること
- コマンドラインから実行可能で且つ、スタンドアロンで動作可能なツールであること
- CIツールに対応したツールであること
後述の方式は上記の要件を満たしたツールを選定しておりますが、もし存在しない( 特に [1.] ) は自作することもご検討ください。
コードフォーマット自動適用例
今回のサンプルは以下の仕様のもとで作成するものとする。
項目 内容 プログラミング言語 Java コードスタイル Google Java Style Guide に準拠 ツール google-java-format CIツールで自動的にコードフォーマッターを実行する
CI用コード(GitHub Action)
ポイント:on pull_reqestにて取得されたブランチはPull Requestしたブランチとは別ブランチのため、checkout時にPull Requestを行ったブランチを明示的して取得する必要があります。
name: Reformat Java source code on: pull_request: jobs: format: runs-on: ubuntu-latest steps: # Pull Requestされたブランチをチェックアウト(★refがポイント) - uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} # Java のセットアップ - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 # Gitの設定 - name: Git settings run: | # Git リポジトリの設定 git config --local user.name "ユーザ名" git config --local user.email "E-Mailアドレス" # コードフォーマットの実行 - name: Code format run: | wget -q -O google-java-format.jar \ https://github.com/google/google-java-format/releases/download/google-java-format-1.9/google-java-format-1.9-all-deps.jar java -jar google-java-format.jar -replace $(git ls-files src/**/*.java) # 変更確認 - name: Check for modified files id: git-check run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi) # 変更されていた場合、コミット・プッシュを行う - name: Push if: steps.git-check.outputs.modified == 'true' run: | git commit -am "Automated :Reformat Java source code." git push実行結果
PullRequst実施、変更時にコードフォーマッターが適用された新しいコミットが追加される
サンプルリポジトリ
上記は以下のリポジトリで実現されております。(ブランチ: feature/format-action)
https://github.com/yukiusa/github-action-auto-java-format-sample/tree/feature/format-actionさいごに
いかがだったでしょうか?
今回はCIツールを利用し「開発環境に依存せずルールを遵守させる仕組み」について紹介しました。
本取り組みを実施することで
- 開発環境の作成の手間を減らす
- チェックの工数を削減する
- レビューア・レビューイの負荷を減らす
などの効果が期待できるかと思います。
(※コードスタイルがお粗末であれば意味をなさないため、コードスタイルのガイドライン作りは慎重に行う必要があります)今後の業務の改善案の一つとしてお役立ていただければ幸いでございます。
最後まで読んでいただきましてありがとうございました。
- 投稿日:2021-01-21T00:28:13+09:00
たったこれだけ!チーム開発でコードフォーマット忘れを防ぐ方法
はじめに
「コードフォーマットが適用されておりません。」
こんな指摘をした人、された人。いるのではないでしょうか?
今回はコードフォーマットをCIツールで実行し、開発者はフォーマッターの実行をしないような仕組みを紹介します。この記事を書こうと思った動機
お客様指定のコードスタイルのある規約があるプロダクトにおいて以下の問題に直面し、結果的に工数が増大したため。
ケース1. ローカルで「コードフォーマッター」が適用されずにPull Requestを出すケース
ケース2. すり抜けコードをベースに修正し、コードフォーマッターを適用したら、改修箇所以外の差分が抽出された
【コードフォーマッターが適用されないわけ】
- 開発環境構築手順書・開発フローにコードフォーマッターの記載されていない。(曖昧なドキュメント)
- 開発メンバー間でのフローの引き継ぎもない(開発メンバーが流動的)
折角コードフォーマッターの設定ファイルを用意しても、運用されていなければ意味がありません。
ドキュメントをしっかり書いたとしても、プロジェクトメンバー全員の開発環境設定が同じであることを確認するにも限度があります。そのため、同じような悩みを持っているコードオーナの方向けにCIツール側でコードフォーマッターを適用する方法について紹介しようと思いました。CIツールでコードフォーマッターを実行する
解決アイディア : 開発環境に依存するのでは無く、フォーマッタはレビュー実施前に自動的に適用される方式とする。
前提
CIツールとコードフォーマッターを連携するために以下の要件を満たす必要があります。
- バージョン管理ツールへのPushやPull Request(Merge Request)を検出する仕組み
- 以下に対応したコードフォーマッターが存在すること
- コマンドラインから実行可能で且つ、スタンドアロンで動作可能なツールであること
- CIツールに対応したツールであること
後述の方式は上記の要件を満たしたツールを選定しておりますが、もし存在しない( 特に [1.] ) は自作することもご検討ください。
コードフォーマット自動適用例
今回のサンプルは以下の仕様のもとで作成するものとする。
項目 内容 プログラミング言語 Java コードスタイル Google Java Style Guide に準拠 ツール google-java-format CIツールで自動的にコードフォーマッターを実行する
CI用コード(GitHub Action)
コードの前提
以下のScriptはGitHub MarketplaceでVerifiedとなっているプラグインのみ利用して実現しております。
(※手早く実装したい場合はMarketplaceにあるプラグインをどんどん活用しましょう)name: Reformat Java source code on: pull_request: jobs: format: runs-on: ubuntu-latest steps: # Pull Requestされたブランチをチェックアウト - uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} # Java のセットアップ - name: Set up JDK 11 uses: actions/setup-java@v1 with: java-version: 11 # Gitの設定 - name: Git settings run: | # Git リポジトリの設定 git config --local user.name "ユーザ名" git config --local user.email "E-Mailアドレス" # コードフォーマットの実行(※1) - name: Code format run: | wget -q -O google-java-format.jar \ https://github.com/google/google-java-format/releases/download/google-java-format-1.9/google-java-format-1.9-all-deps.jar # 以下の例ではすべての変更に対してフォーマッタを加えております。 # プルリクエスト時の変更を取る場合はgit logで変更差分のみ取得してフォーマッタをかけましょう。(※2) java -jar google-java-format.jar -replace $(git ls-files src/**/*.java) # 変更確認 - name: Check for modified files id: git-check run: echo ::set-output name=modified::$(if git diff-index --quiet HEAD --; then echo "false"; else echo "true"; fi) # 変更されていた場合、コミット・プッシュを行う(※3) - name: Push if: steps.git-check.outputs.modified == 'true' run: | git commit -am "Automated :Reformat Java source code." git push※1 … google-java-format
※2 … get-all-changed-files
※3 … Git Auto Commit実行結果
PullRequst実施、変更時にコードフォーマッターが適用された新しいコミットが追加される
サンプルリポジトリ
上記は以下のリポジトリで実現されております。(ブランチ: feature/format-action)
https://github.com/yukiusa/github-action-auto-java-format-sample/tree/feature/format-actionさいごに
いかがだったでしょうか?
今回はCIツールを利用し「開発環境に依存せずルールを遵守させる仕組み」について紹介しました。
本取り組みを実施することで
- 開発環境の作成の手間を減らす
- チェックの工数を削減する
- レビューア・レビューイの負荷を減らす
などの効果が期待できるかと思います。
今後の業務の改善案の一つとしてお役立ていただければ幸いでございます。最後まで読んでいただきましてありがとうございました。
改訂履歴
@naminodarie 様からの指摘一部修正。ご指摘ありがとうございました。