このポエムの動機
Qiitaのトレンドを見ていたら、Android開発でのKotlinとJavaをくらべている記事が流れてきた。
KotlinはJavaより書きやすいという噂は聞くので、気になって読んでみた。
記事の主張を否定したいわけではないけれど(Androidは知らないし)、Javaについて不正確だったり誤解を与えてしまいそうだったりする内容が散見されて、Kotlin推したさにJavaをディスってるように見えてしまって悲しかった。
というわけで、モヤッとしたことを吐き出しておきたい。
あやふやに覚えていたことを復習する機会になったので、記事には感謝。
※9/30追記
この記事がAIに「元記事に対する反論である」と要約されているのを見てしまったので念のため追記。
表題のとおりこれはポエム。お気持ちを垂れ流したかっただけで、反論したいわけではない。
ファッキンAI(うそうそ、大好き)
コードの簡潔性
1行減って、セッターが=になっただけのように見えたので、申し訳ないけれど40%削減されるとは思えなかった。
ステートメントと式
Javaではifやswitchは文(statement)なので値を返せません。そのため三項演算子に頼ることが多くなります。
Java14からswitchは式として書ける。
enum No = { ONE, TWO };
No val = No.ONE;
String str = switch (val) {
case No.ONE -> "one";
case No.TWO -> "two";
}
余談:When句もプレビューは来てるのでそのうち使えるようになると思われる。
instanceofとsealed-permitsと組み合わせたら便利そう。
2025/10/13追記
When句はJava21で正式採用されている。
コメントで教えていただいた。ありがとうございます!
プログラミングパラダイム
Java
- オブジェクト指向プログラミング一本槍(まあ、それはそれで分かりやすいんですが)
Java8以上であれば、Kotlinで例示されたプログラムはほぼ同様に書ける。
var numbers = Arrays.asList(1,2,3,4,5);
var doubled = numbers.stream().map(it -> it * 2).filter(it -> it > 4);
それで「Javaはオブジェクト指向一本槍」なら、何が理由でKotlinは「ハイブリッド」なのだろうか?
より関数型っぽい手続き型言語がどんなものか想像がつかないので、ぜひ聞きたかった……
余談:関数型=善というわけでもない気がする。
ただ、選べることはいいことのような気はする。
すくなくともStreamを使った処理は普通のループより読みやすくて好きだ。おかげでTailCallも使えるし。
equals と ==, ===
Java
String a = new String("text"); String b = new String("text"); System.out.println(a == b); // false (参照が異なる) System.out.println(a.equals(b)); // true (内容は同じ)
new String("...")って、「極力やるな」って教わる人もいると思う。
なぜなら明示的にnewすると、Stringが持っているメモリ節約術であるStringプールを探すことなく強制的に新しいインスタンスを生成するから。
それさえしなければ、基本的にStringプールにあるオブジェクトを参照するので下記はみんなtrue。
String str1 = "hoge";
String str2 = "hoge";
String str3 = getHoge(); // return "hoge";するメソッド
"hoge" == "hoge";
str1 == str2;
str1 == str3;
str1 == "hoge";
str1 == "h" + "o" + "g" + "e";
StringBuilder.toString()はつど新しいインスタンスを作るし、文字列の組み立てはたいていStringBuilderかStringBufferを使うので、実用的にはequalsを使うのが正解だし、それが直感的でないのには違いないけど。
コレクションの可変性
Kotlinでは読み取り専用(List, Map, Set)と変更可能(MutableList, MutableMap, MutableSet)をコンパイル時に区別。Javaだと実行時まで分からない
変更不可能なリストを変更しようとする機会ってそんなにあるだろうか?
育ちが悪い(Java 6で時を止めたコードばっかり見てる)から分からないのかもしれない。
まあ、たとえば、フィールドがある時点から変更不可能になるような腐った設計は最初から不可能な方が安全だし、早く分かるにこしたことはないのはそれはそう。
だけど、可変のコレクションから不変のコレクションを作り出せるのは、便利な瞬間もあるんじゃないかと思う(だから頻度が気になる)。
遅延初期化
Java
// 手動で実装する必要がある private TextView textView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); textView = findViewById(R.id.text_view); // ここで初期化 }
Javaはたしかにフィールドを宣言する必要があるけど、Kotlinだってメソッドの実装を宣言する必要はあって、宣言はなんならKotlinの方が字数が多い。
それに、コンストラクタで初期化するのは必須ではない。Javaのコンパイラはフィールドの初期化を義務づけていないので、Kotlinのコードと同じように、Javaも初期化用のメソッドを定義することで遅延初期化を実現できる。(getterで初期化するのが古典的手法と思う)
まるでJavaは遅延初期化ができないかのように見せる書き方は、初心者に誤解を与える……。
ちなみに、定数であれば、StableValueを使うことで、メソッド定義すらせずにスレッドセーフに遅延初期化できるようになったらしい。
プレビュー機能だから、まだ業務で使うのは避けたいけど、楽しみ。
データクラスとcopy
org.apache.commons.lang3.SerializableUtilsのcloneを使うとそれらしきことはできる。
標準APIのcloneの造りはちょっと「なんでだよ」とは思う。
record MyRecord(String name, Integer age) implements Serializable {}
MyRecord rec1 = new MyRecord("Alice", 30);
MyRecord rec2 = SerializableUtils.clone(rec1);
経験上は、recordよりLombokでGetterとSetterを省略したBeanの方がよく見る。
BeanのコピーはBeanUtilsがある。apache様様
スマートキャスト
Java
void process(Object obj) { if (obj instanceof String) { String str = (String) obj; // 明示的なキャストが必要 System.out.println(str.length()); } }
明示的なキャストは不要。Java17から、instanceofで判定されたオブジェクトを自動で変数に取れるようになった。
https://docs.oracle.com/javase/jp/17/language/pattern-matching.html
void process(Object obj) {
if (obj instanceof String str) {
System.out.println(str.length());
}
}
拡張関数
便利そうだけど、コード管理上は心配事が増えそうで心配になる。
あると思ってたメソッドが無くて困ったり、各自が似たようなメソッドをちょこちょこ追加してたりし始めないんだろうか。
文字列テンプレートとヒアドキュメント
Java23に入る予定で入らなくなってしまったらしい文字列テンプレートの再プレビュー、いつなんでしょうね……
まとめ
以上、Kotlinの機能うらやましいな~と思うところもあって、興味深くはあったけれども、Javaができることを「できない」と書いたり、普通やらないことをやって面倒くささを強調したり、比較するための情報が足りなかったりするのは、Javaユーザとしてちょっと悲しかった。
いちおう手元でソースを動かしながら記事を書いたけど、有識者のツッコミをお待ちしてます。