20191211のJavaに関する記事は5件です。

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には同じような機能はありません。

Java
     NumberFormat 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兆
}
Kotlin
    fun 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を利用すると指定した引数分インデントして表示されます。

Java
    String s = "Kotlin";
    System.out.println(s);
    System.out.println(s.indent(2));
    // Kotlin
    //  Kotlin

kotlinの場合は、String.prependIndentを利用し、中に指定した文字でインデントします。
今回は空白2文字を入れたので2文字分インデントされます。

kotlin
    val s = "Kotlin"
    println(s)
    println(s.prependIndent("  "))
    // Kotlin
    //  Kotlin

String.transform

私が調べた限りだとString.transform的なものはKotlinには見つかりませんでした。1

java
    var 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));  // 2000000
kotlin
inline 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行で実現するのは難しそうです。
代わりに関数を用意して実現しました。

Java
    Map.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=3
kotlin
    fun 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です。

Java
    Path 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:24

Kotlinには同様のAPIはなさそうです。
処理を書くのは大変なので、ここは省略します。

String.isEmpty

Java
    String 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)
kotlin
    val s = "";
    val n: String? = null;
    if (s.isEmpty()) {
        println("s is Empty")
    }
    if (n.isNullOrEmpty()) {
        println("n is Null")
    }

    // s is Empty
    // n is Null

Kotlinは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
もう少し変更されそうですが、大きくは変わらないと思います。

Java
    String s = """
           ultra soul
           OCEAN
           """;
    System.out.println(s);
    // ultra soul
    // OCEAN
kotlin
    val s = """
            ultra soul
            OCEAN
            """
    print(s)
    // ultra soul
    // OCEAN

JavaとKotlinでほぼ同じですね。

Java14以降と比較

さてここからはまだ正式にリリースされていませんが、開発中の内容とkotlinを比較してみます。

参考:Amberで検討されているJava構文の変更

Records(JEP 359)

RecordsはJavaのBeanで冗長になっているhashCode equals toString のコードを自動で用意してくれます。
すごくシンプルでいいですね。

Java
    record 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があります。

Kotlin
    data 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))    // true

RecordはScala、kotlin、C#などのデータクラスを参考にしているみたいです。

Sealed Types(JEP 360)

Sealed Typesはクラスの継承を制限するために利用します
これはenum的に利用すると便利です。

Java
    sealed interface HondaCar {};

    public class Demio implements HondaCar {
        public String getName() {
            return "Demio";
        }
    }

    public class Vezel implements HondaCar {
        public String getName() {
            return "Vezel";
        }
    }
kotlin
sealed 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も複数同時に記載可能です。

kotlin
    when (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の動きを調整中っぽい。

Java
    int j = switch (day) {
        case MONDAY  -> 0;
        case TUESDAY -> 1;
        default      -> {
            int k = day.toString().length();
            int result = f(k);
            yield result;
        }
    };
kotlin
    val 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は変数に格納せずにそのまま利用可能です。

kotlin
    val 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/


  1. KotlinでString.transform的な書き方を分かる方がいらっしゃいましたら教えてくださいい。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コンソール上で遊べる重力付き四目並べ

はじめに

今回はコンソール上で重力付き四目並べを遊んでみました!
こんな感じに動きます。
connect.gif

お遊び程度で作ったので判定部分に漏れがあるかもしれませんが、今後はこれをベースに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.java
package 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]で被りますね。

まとめ

コピペすればすぐにでも遊べるはずです!
重力付き四目並べは学生の頃遊んでからプチブームの波があります(笑)
暇つぶしがてら遊んでみてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Resilience4jドキュメントの日本語訳(非公式)を公開しました

Resilience4jとは

Resilience4jは、Javaで実装されたサーキットブレイカーライブラリです。サーキットブレイカーとして有名なNetflixのHystrixは既に開発がメンテナンスモードに入っていて、積極的には開発がされていません。Resilience4jはその代替となるライブラリとして期待されています。

リポジトリ → https://github.com/resilience4j/resilience4j

公式ドキュメント(英語) → https://resilience4j.readme.io

ざっくりと理解したい場合は、僕がPivotal SpringOne Platform 2019でLTしたときのスライドをどうぞ↓

From Hystrix To Resilience4j

ドキュメントの日本語訳

今回僕は、Resilience4j開発者であるRobert Winklerさんから許可をいただき、ドキュメントを日本語訳して公開しました。ライセンスは本家と同様のApache License 2.0になります。全部ではないのですが、主要な部分は翻訳済みです。

日本語訳 → https://github.com/resilience4j-docs-ja/resilience4j-docs-ja/blob/master/README.md

タイトルにある通り、このドキュメントは非公式なものです。すなわち、Resilience4j開発チームとは一切関係がありません。日本語訳に正しくない部分があれば、それは僕を始めとした翻訳メンバーの責任です。

もし翻訳に間違いがあった場合は、GitHubのIssueなどで教えていただければありがたいです。もちろん、翻訳メンバーとして入ってくださることも歓迎します!

この日本語訳が、これからResilience4jを使う人の役に立てば嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Bootでアプリ作成2

前回の続き。
https://qiita.com/YJ2222/items/8c29fc7fc2d886a9b35e

ファイル作成

・必要なファイルを作成する。以下画像と「各ファイルの説明」を参考に作成する。
 1.png

・各ファイルの説明。

  • 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.java
package 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.java
package 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.java
package 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.java
package 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.properties
spring.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.sql

src/main/resources/data.sql
INSERT INTO account (user_id, nickname, password) VALUES (1, 'user1', 'user1+');

src/main/resources/schema.sql
CREATE TABLE IF NOT EXISTS account (
    user_id INT PRIMARY KEY,
    nickname VARCHAR(50),
    password VARCHAR(50)
);

・処理の流れは、コードのコメント欄と以下の画像を参考にすると想像しやすい。
3.png

以上です。これでローカル環境でDBの接続までできました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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」メソッドは、下記の機能を持っています。

  1. 数値("0-9"と".")を示すcharは入力し、内部バッファに保持する
  2. それ以外は無視する
  3. 内部の数値バッファはインスタンス内の文字列で保持する。外部から値を問われたときにはBigDecimalで返す。

テスト実行結果は下図のとおりです。10個分テストされていますね。

コメント 2019-12-11 081737.png

数値が入ることは上記でテストしましたが、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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む