Help us understand the problem. What is going on with this article?

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

CompactNumberFormatが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

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
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.let(addresses::get)?.let(population::get))  // 2000000
}

Kotlinではlet関数を利用することで、String.transformと同じ書き方ができます。

@rmakiyama さん、@anatawa12 さん にKotlinでのString.transformの書き方を教えていただきました!ありがとうございます。

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のcase class、Kotlinのdata class、C#のrecord typesなどを参考にしているみたいです。

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はすでにwhen式で利用可能です。

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/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした