- 投稿日:2019-12-11T22:36:50+09:00
Kotlinと今後のJavaはどっちがいい?
はじめに
本記事は Kotlin Advent Calendar 2019 12日目の記事です。
KotlinはJavaと比較してモダンと言われ続けてきていると思います。
しかしJavaのリリースサイクルが半年に1回となり、今までよりも良い言語になりつつあり、将来的な保守性を考えたときにJavaの方がいいのでは?という意見もあり、言語選定の時にどちらを選択すればいいのかと悩むことがあると思います。
そこで、最新のJavaの動向とKotlinを比較して、どちらがよりモダンかを比較したいというのが今回の内容です。比較する対象はKotlinの最新バージョンとJavaの12以降の機能になります。
Javaについては、バージョン13が2019/9/18にリリースされましたが、今回は開発中のJDK14やそれ以降に入りそうな機能もいくつか比較していければと思います。
JDK11以前の機能については、他に良い記事が沢山あると思うのでそちらを参照ください。もしくは余裕が出たタイミングで追加するかもしれません。JDKの各バージョンで追加された機能については、@nowokay さんの記事がかなり綺麗にまとまっていて分かりやすいので参考にさせていただき、Kotlinと比較していきたいと思います。
ちなみにKotlinはまだ触り始めたばかりで理解が足りていない部分もあると思うので、是非マサカリください。
前提
- Kotlin 1.3.61
- Java OpenJDK 14 Early-Access Build 25
(今回はsdkmanを利用しました)Java12と比較
参考:Java12新機能まとめ
SwitchExpressionsもPreview版が12で入っていますが、JDK14でも修正が続いているので、JDK14のところで説明します。
CompactNumberFormat
NuberFormatがJava12から導入されましたが、Kotlinには同じような機能はありません。
JavaNumberFormat cnf = NumberFormat.getCompactNumberInstance(); System.out.println(cnf.format(10000)); // 1万 System.out.println(cnf.format(10000_0000)); // 1億 System.out.println(cnf.format(10000_0000_0000L)); // 1兆 }Kotlinfun format(number: Long): String { return when { (number >= 10F.pow(12)) -> { floor(number / 10F.pow(12)).toLong().toString() + "兆" } (number >= 10F.pow(8)) -> { floor(number / 10F.pow(8)).toLong().toString() + "億" } (number >= 10F.pow(4)) -> { floor(number / 10F.pow(4)).toLong().toString() + "万" } else -> { number.toString() } } } println(format(10000)) // 1万 println(format(10000_0000)) // 1億 println(format(10000_0000_0000L)) // 1兆3つの単位しかないのでちょっと無理やり作ってみました。
ここはJavaの方が優れてしますね。
使う機会は少なそうですが。String.indent
String.indentを利用すると指定した引数分インデントして表示されます。
JavaString s = "Kotlin"; System.out.println(s); System.out.println(s.indent(2)); // Kotlin // Kotlinkotlinの場合は、String.prependIndentを利用し、中に指定した文字でインデントします。
今回は空白2文字を入れたので2文字分インデントされます。kotlinval s = "Kotlin" println(s) println(s.prependIndent(" ")) // Kotlin // KotlinString.transform
私が調べた限りだとString.transform的なものはKotlinには見つかりませんでした。1
javavar addresses = Map.of("Mike", "Fukuoka", "John", "Tokyo"); var population = Map.of("Tokyo", 30000000, "Fukuoka", 2000000); var name = "Mike"; System.out.println(name.transform(addresses::get).transform(population::get)); // 2000000kotlininline fun <R> String.transform(transform: (String) -> R): R { return transform(this) } fun main() { val addresses = mapOf("Mike" to "Fukuoka", "John" to "Tokyo") val population = mapOf("Tokyo" to 30000000, "Fukuoka" to 2000000) val name = "Mike" println(name.transform(addresses::get)?.transform(population::get)) // 2000000 }@rmakiyama さんにString.transformをJava風にする方法を教えていただきました!ありがとうございます。
Collectors.teeing
これもKotlinだと1行で実現するのは難しそうです。
代わりに関数を用意して実現しました。JavaMap.Entry<String, Long> map = Stream.of("aaa", "", "bbb", "ccc"). filter(Predicate.not(String::isEmpty)). collect(Collectors.teeing( Collectors.joining(","), Collectors.counting(), Map::entry)); System.out.println(map); // aaa,bbb,ccc=3kotlinfun listToMap(list: List<String>): Map<String, Int> { return mutableMapOf(list.joinToString(",") to list.count()) } val list = mutableListOf("aaa", "", "bbb", "ccc") .filter { !it.isBlank() } .toList() println(listToMap(list)) // {aaa,bbb,ccc=3}Files.mismatch
Fileの中身が異なっているかを確認するAPIです。
JavaPath filePath1 = Path.of("./com/example/jdk12/FilesMismatchFile1.txt"); Path filePath2 = Path.of("./com/example/jdk12/FilesMismatchFile2.txt"); long mismatchRow = Files.mismatch(filePath1, filePath2); if (mismatchRow == -1) { System.out.println("file is same"); } else { System.out.println("file is diffarent¥nrow:" + String.valueOf(mismatchRow)); } // ファイルが同じ場合 // file is same // ファイルが異なる場合 // file is diffarent // row:24Kotlinには同様のAPIはなさそうです。
処理を書くのは大変なので、ここは省略します。String.isEmpty
JavaString s = ""; String n = null; if (s.isEmpty()) { System.out.println("s is Empty"); } if (n.isEmpty()) { System.out.println("n is Empty"); } // s is Empty // Exception in thread "main" java.lang.NullPointerException // at com.example.jdk12.StringIsEmpty.main(StringIsEmpty.java:10)kotlinval s = ""; val n: String? = null; if (s.isEmpty()) { println("s is Empty") } if (n.isNullOrEmpty()) { println("n is Null") } // s is Empty // n is NullKotlinは
n.isEmpty()
を利用するとコンパイルエラーが発生します。
Null安全はやっぱりいいですね。Javaでは、Nullの変数に
isEmpty()
を利用してしまうとNPEが出力してしまいます。
また、Optional<String> n = null;
を利用してもコンパイル出来てNPEが発生します。
Javaもライブラリを使わずにNullとEmptyチェックを同時にしてくれるようにならないかな。Java13と比較
参考:https://openjdk.java.net/projects/jdk/13/
SwitchExpressionsはJDK13からもう少し変更されそうなので後ほど比較します。
Text Blocks
https://openjdk.java.net/jeps/368
もう少し変更されそうですが、大きくは変わらないと思います。JavaString s = """ ultra soul OCEAN """; System.out.println(s); // ultra soul // OCEANkotlinval s = """ ultra soul OCEAN """ print(s) // ultra soul // OCEANJavaとKotlinでほぼ同じですね。
Java14以降と比較
さてここからはまだ正式にリリースされていませんが、開発中の内容とkotlinを比較してみます。
Records(JEP 359)
RecordsはJavaのBeanで冗長になっている
hashCode
equals
toString
のコードを自動で用意してくれます。
すごくシンプルでいいですね。Javarecord Point(int x, int y) {} Point point1 = new Point(5, 10); Point point2 = new Point(5, 10); System.out.println(point1.x()); // 5 System.out.println(point1); // Point[x=5, y=10] System.out.println(point1.equals(point2)); // trueそしてkotlinにはdata classがあります。
Kotlindata class Point(val x: Int, val y: Int) val point1 = Point(5, 10) val point2 = Point(5, 10) println(point1.x) // 5 println(point1) // Point(x=5, y=10) println(point1.equals(point2)) // trueRecordはScala、kotlin、C#などのデータクラスを参考にしているみたいです。
Sealed Types(JEP 360)
Sealed Typesはクラスの継承を制限するために利用します
これはenum的に利用すると便利です。Javasealed interface HondaCar {}; public class Demio implements HondaCar { public String getName() { return "Demio"; } } public class Vezel implements HondaCar { public String getName() { return "Vezel"; } }kotlinsealed class HondaCar class Demio: HondaCar() { fun getName():String { return "Demio" } } class Vezel: HondaCar() { fun getName():String { return "Vezel" } }Recordでswitch
ここはJavaのコードがあまり理解出来ていないのですが、イメージを書きます。
(どこかでsealed/recordをswitchで利用する例を見たか聞いたかした気がするのですが、まだissueなどが見つけられていないので、見つけたら詳細URLを貼ります)Java// sealed sealed interface HondaCar permits Demio, Vezel {} record Demio() implements HondaCar {} record Vezel() implements HondaCar {} // use int price = switch(hondaCar) { case Demio(int price) -> "Demio"; case Vezel(int price) -> "Vezel"; // sealedにより選択肢がDemioとVezelしかないのがわかっているので、default文は不要 // default -> throw new IllegalStateException("Error"); };kotlinはすでにswitch文で利用可能です。
kotlin// sealed sealed class HondaCar class Demio: HondaCar() class Vezel: HondaCar() // use val hondaName = when(hondaCar) { is Demio -> "Demio" is Vezel -> "Vezel" // defaultは不要 } println(hondaName)ちなみにkotlinでsealedを利用しない場合は、default(else)が必須になります。
kotlinエラー// interface interface NissanCar class Leaf: NissanCar class Juke: NissanCar // use val nissanCar: NissanCar = Leaf() val nissanName = when(nissanCar) { is Leaf -> "Leaf" is Juke -> "Juke" // elseがないため、以下エラーが出力している // 'when' expression must be exhaustingstive, add necssary 'else' branch } println(nissanName)Switch Expressions (JEP 361)
https://openjdk.java.net/jeps/361
Javaのswitchは元々使いにくいなぁと思っていましたが、jdk12(JEP 325)から検討されていて、色々改善されていそうです。
Kotlinと比較したいだけなので、大きく変わる部分だけを記載しておきます。同じブロックに入る複数のcaseを同時に記載可能
Java// 元々 switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; } // 改善後 switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); }kotlinも複数同時に記載可能です。
kotlinwhen (day) { Day.MONDAY, Day.FRIDAY, Day.SUNDAY -> println(6) Day.TUESDAY -> println(7) Day.THURSDAY, Day.SATURDAY -> println(8) Day.WEDNESDAY -> println(9) }switchを式として利用可能
Sealed Typesでも記載しましたが、switchの結果を式として利用して変数に格納することができるようになりそうです。
現在はfor文などのループの中でswitchを利用したときに、breakやcontinueの動きを調整中っぽい。Javaint j = switch (day) { case MONDAY -> 0; case TUESDAY -> 1; default -> { int k = day.toString().length(); int result = f(k); yield result; } };kotlinval j = when (day) { is Monday -> 0; is Tuesday -> 1; else -> { val k = day.toString().length k } } // kotlinはswitchの中でbreak・continueも対応しています (無駄なロジックになっています) loop@ for (i in 1..100) { when (day) { is Monday -> j = 0; is Tuesday -> j = 1; else -> { j = day.toString().length break@loop } } }Pattern Matching for instanceof (JEP 305)
instanceofで型チェックした後は型が定まっている状態なので、そのまま変数に格納して利用できるようになるのが
Pattern Matching for instanceof
です。Java// 今まで if (o instanceof String) { // 直接oをString型として利用出来ない // System.out.println(o.length()); // 一度String型にキャストしてから利用する必要がある String s = (String)o; System.out.println(s.length()); // 27 } // これから Object o = "Pattern Match of instanceof"; // instanceofと同時に変数に格納できる if (o instanceof String s) { System.out.println(s.length()); // 27 } // switchも利用可能になるらしい。(OpenJDK 14 Early-Access Build 25ではまだ) // https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html switch (o) { case Integer i -> System.out.println(i); case String s -> System.out.println(s.length()); }kotlinは変数に格納せずにそのまま利用可能です。
kotlinval o: Any = "Pattern Match of instanceof" if (o is String) { println(o.length) // 27 } when(o) { is Int -> println("Int") is String -> { val s:String = o println(s) } } // 27まとめ
まずJavaがすごく進化してきていてすごいなと思いました。
以前まではKotlin最高。Javaは残念というイメージだったと思いますが、JDK17に向けてJavaもKotlinなどモダンな言語を取り入れて、あまり差はなくなってきているという印象です。
そしてそんなJavaの新しい構文がすでに取り入れられているkotlinに驚きもありました。Javaとkotlinの言語比較という観点では、KotlinにはNULL安全であったり、関数型が書きやすいなどの利点もまだあり、次のLTSであるJDK17がリリースされる2021年9月時点では、どちらを選択しても良いのではないのかなと思いました。
私がこれから新しく作るプロジェクトではKotlinで書いていくことになったので、今後実際に対応していく中で、気になったことは別途記事にしていきたいと思います。
最後に
Kotlinのアドカレなのに、Javaの色が強くなってしまったのが反省点です。
また、他の方がすでに記事にされている内容を使い回すような表現になってしまって申し訳ありません。
Kotlinと新しいJavaの比較のためには、情報も少ない中でどうしても被ってしまう内容が多かったので、そこはお許しいただければと思います。書きながら思ったのですが、JDK9〜JDK11までも比較した方が良さそうですね。余裕があるときに対応します。
Kotlinも現時点では業務で触っていないため、理解が浅い部分もあると思うので、これから理解が深まったら徐々に修正していきたいと思います。
参考情報
Java12新機能まとめ
Amberで検討されているJava構文の変更https://openjdk.java.net/projects/jdk/13/
https://openjdk.java.net/projects/amber/
KotlinでString.transform的な書き方を分かる方がいらっしゃいましたら教えてくださいい。 ↩
- 投稿日:2019-12-11T20:59:27+09:00
コンソール上で遊べる重力付き四目並べ
はじめに
今回はコンソール上で重力付き四目並べを遊んでみました!
こんな感じに動きます。
お遊び程度で作ったので判定部分に漏れがあるかもしれませんが、今後はこれをベースにCPU対戦を追加など改良を加えていこうと思います。
一応重力付き四目並べについてwiki
https://ja.wikipedia.org/wiki/%E5%9B%9B%E7%9B%AE%E4%B8%A6%E3%81%B9使用環境
・Windows 7
・Java 8
・Eclipseソース紹介
今回のソースを紹介します。
ConnectFour.javapackage connect.main; import java.util.Random; import java.util.Scanner; public class ConnectFour { public static void main(String[] args) { System.out.println("重力付き四目並べ開始"); // 0:player1 1:player2 int player = 0; Random random = new Random(); int turn = 0; int[][] bord = new int[6][7]; int i; // 盤面初期化(-1) for(i = 0; i < 6; i++) { for(int j = 0; j < 7; j++) { bord[i][j] = -1; } } // 0 || 1 player = random.nextInt(2); Scanner scan = new Scanner(System.in); scan.useDelimiter(System.lineSeparator()); while(true) { System.out.println(); printPlayer(player); // コンソールに盤面出力 printBord(bord); // 盤面いっぱいになったら引き分け if(turn >= 42) { System.out.println("引き分け"); break; } // コンソールからの入力とバリデーション System.out.println("番号(0~6)を入力してください。"); String line = scan.next(); if(line.trim().length() == 0 || line.trim().length() > 1) { System.out.println("入力が不正です。"); continue; } if(!Character.isDigit(line.charAt(0))) { System.out.println("入力が不正です。"); continue; } int num = Integer.parseInt(line); if(num < 0 || num > 6) { System.out.println("入力が不正です。"); continue; } for(i = 0; i < 6; i++) { if(bord[i][num] == -1) { bord[i][num] = player; break; } } if(i >= 6) { System.out.println("[ " + num + "]にはこれ以上入れられません。"); continue; } if(isWin(bord, i , num, player)) { System.out.println(); printBord(bord); System.out.println((player == 0 ? "player1[○]":"player2[×]")+"の勝ち!!"); break; } player = (player * -1) + 1; turn++; } scan.close(); System.out.println("終了"); } public static void printPlayer(int player) { switch(player){ case 0: System.out.println("player1[○]の番です。"); break; case 1: System.out.println("player2[×]の番です。"); break; default: System.out.println("エラー"); return; } } public static void printBord(int[][] bord) { for(int j = 0; j < 7; j++) { if(j == 6) { System.out.println("[ " + j + "]"); }else { System.out.print("[ " + j + "]"); } } for(int i = 5; i >= 0; i--) { for(int j = 0; j < 7; j++) { String a = ""; a = (bord[i][j] == 0) ? "[○]" : (bord[i][j] == 1) ? "[×]" : "[ ]"; if(j == 6) { System.out.println(a); }else { System.out.print(a); } } } } public static boolean isWin(int[][] bord, int i, int j, int player) { int yoko = 1;// - int tate = 1;// | int sura = 1;// / int back = 1;// \ int y = i; int x = j; // 横方向チェック(右) while(x < 6) { if(bord[i][x+1] == player) { yoko++; }else { break; } x++; } x = j; // 横方向チェック(左) while(x > 0) { if(bord[i][x-1] == player) { yoko++; }else { break; } x--; } if(yoko >= 4) { return true; } // 縦方向チェック(上) while(y < 5) { if(bord[y+1][j] == player) { tate++; }else { break; } y++; } y = i; // 縦方向チェック(下) while(y > 0) { if(bord[y-1][j] == player) { tate++; }else { break; } y--; } if(tate >= 4) { return true; } y = i; x = j; // /方向チェック(上) while(y < 5 && x < 6) { if(bord[y+1][x+1] == player) { sura++; }else { break; } y++; x++; } y = i; x = j; // /方向チェック(下) while(y > 0 && x > 0) { if(bord[y-1][x-1] == player) { sura++; }else { break; } y--; x--; } if(sura >= 4) { return true; } y = i; x = j; // \方向チェック(上) while(y < 5 && x > 0) { if(bord[y+1][x-1] == player) { back++; }else { break; } y++; x--; } y = i; x = j; // \方向チェック(下) while(y > 0 && x < 6) { if(bord[y-1][x+1] == player) { back++; }else { break; } y--; x++; } if(back >= 4) { return true; } return false; } }ソースはこれで全部です!
全体の流れをざっくり説明すると、
1.player1・2を奇数偶数(0or1)でランダムに先攻を決める。
2.盤面を-1で初期化する。
3.どちらの順番か表示し、盤面を表示する。
4.どこにコマを入れるか番号をコンソールから取得する。
5.入力チェック。
6.盤面に空きがあれば、盤面上にセットする。
7.駒を設置した位置から、縦横ななめの勝利判定をして、条件に当てはまれば終了。
8.playerを切り替え、turnをインクリメントする。
9.3からループ勝利判定について
自分が置いた駒から上下左右、スラッシュ、バックスラッシュ方向に判定する。
※重力付きなので自分の置いた駒の上には駒が存在するはずがないので、上方向はいらないですね
自分が置いた駒が基準となるので変数はそれぞれ初期値1にする(現在位置は現playerが今置いた駒)tate = 1;
yoko = 1;
sura = 1;
back = 1;以下の条件毎にループさせる
①
現在位置から見て↑方向がplayerの駒なら
tate++;
現在位置を↑方向にずらしてループ
違う場合は連続しないのでループ終わり②
現在位置から見て↓方向がplayerの駒なら
tate++;
現在位置を↓方向にずらしてループ
違う場合は連続しないのでループ終わり①と②の結果は
tate = ↑方向の連続駒数 + ↓方向の連続駒数 + 1(初期値);
と同じになる。①、②と同じように←→↙↗↖↘方向に適用させると
yoko = ← + → + 1;
sura = ↙ + ↗ + 1;
back = ↖ + ↘ + 1;
が得られる。どれか一つでも >= 4 の条件に当てはまれば現playerの勝利
注意点は各変数を初期値1にすることと、現在位置から見て次のマスをループ条件で見ることです。
現在位置で判断してしまうと、基準点が重なってしまうからです。(初期値0で最終的に-1すれば基準点でも問題ないですが)
例:[3][3]を基準点とし見ていく
(上方向判定)
現在位置[3][3]がplayerなのでインクリメント
上方向に移り現在位置[4][3]...
(下方向判定)
現在位置[3][3]がplayerなのでインクリメント
下方向に移り現在位置[2][3]...現在位置[3][3]で被りますね。
まとめ
コピペすればすぐにでも遊べるはずです!
重力付き四目並べは学生の頃遊んでからプチブームの波があります(笑)
暇つぶしがてら遊んでみてください!
- 投稿日:2019-12-11T20:23:29+09:00
Resilience4jドキュメントの日本語訳(非公式)を公開しました
Resilience4jとは
Resilience4jは、Javaで実装されたサーキットブレイカーライブラリです。サーキットブレイカーとして有名なNetflixのHystrixは既に開発がメンテナンスモードに入っていて、積極的には開発がされていません。Resilience4jはその代替となるライブラリとして期待されています。
リポジトリ → https://github.com/resilience4j/resilience4j
公式ドキュメント(英語) → https://resilience4j.readme.io
ざっくりと理解したい場合は、僕がPivotal SpringOne Platform 2019でLTしたときのスライドをどうぞ↓
ドキュメントの日本語訳
今回僕は、Resilience4j開発者であるRobert Winklerさんから許可をいただき、ドキュメントを日本語訳して公開しました。ライセンスは本家と同様のApache License 2.0になります。全部ではないのですが、主要な部分は翻訳済みです。
日本語訳 → https://github.com/resilience4j-docs-ja/resilience4j-docs-ja/blob/master/README.md
タイトルにある通り、このドキュメントは非公式なものです。すなわち、Resilience4j開発チームとは一切関係がありません。日本語訳に正しくない部分があれば、それは僕を始めとした翻訳メンバーの責任です。
もし翻訳に間違いがあった場合は、GitHubのIssueなどで教えていただければありがたいです。もちろん、翻訳メンバーとして入ってくださることも歓迎します!
この日本語訳が、これからResilience4jを使う人の役に立てば嬉しいです。
- 投稿日:2019-12-11T13:31:40+09:00
Spring Bootでアプリ作成2
前回の続き。
https://qiita.com/YJ2222/items/8c29fc7fc2d886a9b35eファイル作成
・必要なファイルを作成する。以下画像と「各ファイルの説明」を参考に作成する。
・各ファイルの説明。
- SpringApp1/src/main/java/com.ex1/dao
- UserDAO.java → DBアクセス用のオブジェクト
- SpringApp1/src/main/java/com.ex1/model
- User.java → ユーザ情報のインスタンスを生成するオブジェクト
- UserFindLogic.java → DAOが取得した情報を受け取り、インスタンス化。その後controllerへ渡す。
- SpringApp1/src/main/resources/
- data.sql → schema.sqlが作ったテーブルにデータをinsertするファイル。
- schema.sql → プロジェクト起動時にDBへアクセスしテーブルを作成するファイル。
- templates/userResult.html → postの処理が成功したときにフォワードされるview
作ったファイルにコードを書き込む。
templates/userResult.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"></meta> <title>User</title> </head> <body> <h1>UserResult</h1> <table> <tr> <!-- th:textはthymeleafの機能を使用しgetAttributeできる。 -->> <td>ID:</td><td th:text="${id}"></td> </tr> <tr> <td>ニックネーム:</td><td th:text="${nickname}"></td> </tr> </table> <body> </html>controller/UserController.javapackage com.ex1.Controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import com.ex1.model.User; import com.ex1.model.UserFindLogic; @Controller public class UserController { @Autowired // 依存性の注入。 private UserFindLogic userFindLogic; @GetMapping("/") public String getUser() { return "user"; } // user.htmlからのpostの処理。@RequestParamでnameの値を取得。 @PostMapping("/user/db") public String postDbRequest(@RequestParam("text") String str, Model model) { int id = Integer.parseInt(str); // 文字列変換。 // UserFindLogicをnewしなくても@Autowiredで依存性を注入しているため、User型としてUserFindLogicを実行することができる。 User user = userFindLogic.findUser(id); // UserFindLogic.javaのfindUserメソッドを実行。 // UserFindLogic.javaから戻り値を受け取ったら以下を実行。 model.addAttribute("id", user.getUserId()); // ユーザ情報をsetAttributeする。 model.addAttribute("nickname", user.getNickName()); return "userResult"; // userResult.htmlにフォワード。 } }model/UserFindLogic.javapackage com.ex1.model; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.ex1.dao.UserDAO; @Service // ビジネスロジックに付与するアノテーション。 public class UserFindLogic { @Autowired private UserDAO userDAO; public User findUser(int id) { // controllerからの指示によりメソッドを実行。 Map<String, Object> map = userDAO.findUser(id); // UserDAOのfindUserを実行。 // DAOからuser変数を受け取る。 int userId = (Integer) map.get("user_id"); // 受け取った変数をmapで分解し.getで情報取得。 String nickName = (String) map.get("nickname"); User user = new User(); // userインスタンスを生成(User.java) user.setUserId(userId); // setterを実行。 user.setNickName(nickName); return user; // controllerにuserインスタンスを戻り値として返す。 } }model/User.javapackage com.ex1.model; public class User { private int userId; private String nickName; public void setUserId(int userId) { this.userId = userId; } public void setNickName(String nickName) { this.nickName = nickName; } public int getUserId() { return userId; } public String getNickName() { return nickName; } }dao/UserDAO.javapackage com.ex1.dao; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository // DAOクラスに付与するアノテーション。 public class UserDAO { @Autowired private JdbcTemplate jdbcTemplate; // DB接続できるインスタンス。 public Map<String, Object> findUser(int id) { // UserFindLogic.javaの指示によりメソッドを実行。 // SELECT文を生成 String query = "SELECT " + " * " + "FROM account " + "WHERE user_id=?"; // jdbcTemplateの機能によってDBに対しSQLを実行。 Map<String, Object> user = jdbcTemplate.queryForMap(query, id); return user; // 上記の処理結果をUserFindLogic.javaに戻り値として返す。 } }templates/userResult.html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"></meta> <title>User</title> </head> <body> <h1>UserResult</h1> <table> <tr> <td>ID:</td><td th:text="${id}"></td> </tr> <tr> <td>ニックネーム:</td><td th:text="${nickname}"></td> </tr> </table> <body> </html>src/main/resources/application.propertiesspring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.driver-class-name=org.h2.Driver spring.datasouce.username=sa spring.datasouce.password= spring.datasource.sql-script-encoding=UTF-8 spring.h2.console.enabled=true spring.datasource.initialize=true spring.datasource.schema=classpath:schema.sql spring.datasource.data=classpath:data.sqlsrc/main/resources/data.sqlINSERT INTO account (user_id, nickname, password) VALUES (1, 'user1', 'user1+');src/main/resources/schema.sqlCREATE TABLE IF NOT EXISTS account ( user_id INT PRIMARY KEY, nickname VARCHAR(50), password VARCHAR(50) );・処理の流れは、コードのコメント欄と以下の画像を参考にすると想像しやすい。
以上です。これでローカル環境でDBの接続までできました。
- 投稿日:2019-12-11T08:29:00+09:00
JUnit5のParametrisedTestの@ValueSourceを使う
概要
このエントリは、JUnit5で単体テストを書くとき、パラメータを変えたテストを書くのを楽にしてくれる「ParametrisedTest」の「@ValueSource」を使用する方法を書きます。
Enterprise電卓を作る Advent Calendar 2019の2日目のエントリです。
想定読者
- JUnit4には慣れているけれどもまだJUnit5のお便利機能を使っていない方。
JUnit5のParametrisedTest
JUnit5は、JUnit4を大幅刷新したテストフレームワークです。ばっさり削除された機能もあれば、新たに追加になった機能もあります。
新たに追加になった機能の中に「Parameterized Tests」があります。これは、一つのテストケースの引数に対して、値の種類を様々に変えてパラメータとして渡すことで、簡単にテストが書けるような支援をしてくれるものです。
準備
ドキュメントによれば、Parameterized Testsを実行するには、「junit-jupiter-params」のアーティファクトを利用可能になっている必要があります。
このエントリでは、SpringBootを使っており、テスト用にspring-boot-starter-testを使用していますが、この場合はspring-boot-starter-testを一行書いておくだけでjunit-jupiter-paramsへの依存関係も指定されています。build.gradleに下記のような指定を入れています。
SpringInitializrを使うと、JUnit3やJunit4で書かれたテストを実行することをサポートする「junit-vintage-engine」は使用しない指定も入れてくれました。dependencies { implementation 'org.springframework.boot:spring-boot-starter' testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' }@ValueSourceでテストを書いてみる
JUnitを書くとき、結果の確認には、読みやすさが好きなので「AssertJ」を使っています。build.gradleに下記の一行を追加しています。
testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.14.0'ValueSourceを使ったテストのソースの例を下記に示します。
@ParameterizedTest @ValueSource(chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}) public void push_single_success(char c) { DeskCalculator dc = new DeskCalculator(); dc.pushChar(c); assertThat(dc.getCurrentValue()).isEqualTo(new BigDecimal(new String(new char[]{c}))); }このテストで対象としているDeskCalculatorクラスの「pushChar」メソッドは、下記の機能を持っています。
- 数値("0-9"と".")を示すcharは入力し、内部バッファに保持する
- それ以外は無視する
- 内部の数値バッファはインスタンス内の文字列で保持する。外部から値を問われたときにはBigDecimalで返す。
テスト実行結果は下図のとおりです。10個分テストされていますね。
数値が入ることは上記でテストしましたが、2番目の動作を確認するため、次のテストも書きます。
@ParameterizedTest @ValueSource(chars = {'+', '-', '*', '/', 'A', '!', ' '}) public void push_single_invalid_char_success(char c) { DeskCalculator dc = new DeskCalculator(); dc.pushChar(c); assertThat(dc.getCurrentValue()).isEqualTo(BigDecimal.ZERO); }上記2つの例で挙げたように、@ValueSourceの部分にパラメータを複数渡してあげることで、複数の値に対するテストが実行されます。
おわりに
本エントリでは、JUnit5で追加となった「ParametrisedTest」について取り上げました。
ParametrisedTestは、@ValueSourceのほかにも、複数の引数を渡してあげる機能、CSVから値を読み込む機能なども提供されています。このエントリで使用したコードは、GitHubにタグをつけて格納しています。https://github.com/hrkt/commandline-calculator/releases/tag/0.0.3