はじめに
本記事は 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には同じような機能はありません。
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兆
}
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を利用すると指定した引数分インデントして表示されます。
String s = "Kotlin";
System.out.println(s);
System.out.println(s.indent(2));
// Kotlin
// Kotlin
Kotlinの場合は、String.prependIndentを利用し、中に指定した文字でインデントします。
今回は空白2文字を入れたので2文字分インデントされます。
val s = "Kotlin"
println(s)
println(s.prependIndent(" "))
// Kotlin
// Kotlin
String.transform
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
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行で実現するのは難しそうです。
代わりに関数を用意して実現しました。
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
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です。
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
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)
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
もう少し変更されそうですが、大きくは変わらないと思います。
String s = """
ultra soul
OCEAN
""";
System.out.println(s);
// ultra soul
// OCEAN
val s = """
ultra soul
OCEAN
"""
print(s)
// ultra soul
// OCEAN
JavaとKotlinでほぼ同じですね。
Java14以降と比較
さてここからはまだ正式にリリースされていませんが、開発中の内容とKotlinを比較してみます。
Records(JEP 359)
RecordsはJavaのBeanで冗長になっているhashCode
equals
toString
などのコードを自動で用意してくれます。
すごくシンプルでいいですね。
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があります。
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的に利用すると便利です。
sealed interface HondaCar {};
public class Shuttle implements HondaCar {
public String getName() {
return "Shuttle";
}
}
public class Vezel implements HondaCar {
public String getName() {
return "Vezel";
}
}
sealed class HondaCar
class Shuttle: HondaCar() {
fun getName():String { return "Shuttle" }
}
class Vezel: HondaCar() {
fun getName():String { return "Vezel" }
}
Recordでswitch
ここはJavaのコードがあまり理解出来ていないのですが、イメージを書きます。
JEP360参考
// sealed
sealed interface HondaCar permits Shuttle, Vezel {}
record Shuttle() implements HondaCar {}
record Vezel() implements HondaCar {}
// use
int price = switch(hondaCar) {
case Shuttle() -> "Shuttle";
case Vezel() -> "Vezel";
// sealedにより選択肢がDemioとVezelしかないのがわかっているので、default文は不要
// default -> throw new IllegalStateException("Error");
};
Kotlinはすでにwhen式で利用可能です。
// sealed
sealed class HondaCar
class Shuttle: HondaCar()
class Vezel: HondaCar()
// use
val hondaName = when(hondaCar) {
is Shuttle -> "Shuttle"
is Vezel -> "Vezel"
// defaultは不要
}
println(hondaName)
ちなみにKotlinでsealedを利用しない場合は、default(else)が必須になります。
// 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を同時に記載可能
// 元々
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も複数同時に記載可能です。
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の結果を式として利用して変数に格納することができるようになりました。
int j = switch (day) {
case MONDAY -> 0;
case TUESDAY -> 1;
default -> {
int k = day.toString().length();
int result = f(k);
yield result;
}
};
val j = when (day) {
is Monday -> 0;
is Tuesday -> 1;
else -> {
val k = day.toString().length
k
}
}
Pattern Matching for instanceof (JEP 305)
instanceofで型チェックした後は型が定まっている状態なので、そのまま変数に格納して利用できるようになるのがPattern Matching for instanceof
です。
// 今まで
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は変数に格納せずにそのまま利用可能です。
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/