HIRO Tracks

山根正大です。Fintech業界エンジニアが日々学んだ知識を発信します。

【Kolin】Java民から見た時のKotlinのギャップまとめ

本記事の概要

社内プロジェクトでServer Side Kotlinを使ってコードを書くことになりました。 メンバーには自分含めJava民が多いです。Kotlinへの移行がちょっとでもスムーズになるように、Java民から見た「Kotlin特有の書き方だなぁ」ってところをまとめました。

目次

Kotlin慣れに良い本・サイト

[公式]イディオム集

https://kotlinlang.org/docs/java-to-kotlin-idioms-strings.html

[公式]コレクションタイプ

https://kotlinlang.org/docs/java-to-kotlin-collections-guide.html#operations-on-any-collection-type

リファレンス

https://dogwood008.github.io/kotlin-web-site-ja/docs/reference/basic-types.html

(公式)SpringBootとH2DB使ってAPIつくる

https://kotlinlang.org/docs/jvm-spring-boot-restful.html#add-database-support

(Qiita)Kotlinの基本的な言語仕様を学習したまとめ

https://qiita.com/kerobot/items/ac8fa69955f9d3414f81

また、山根は以下の内容をこの本で勉強しました。 基本的な言語仕様から始まり、Kotlin + SpringBoot + H2/MyBatis + Vue.js使って簡単なアプリケーションを作るところまでがハンズオンで学べます。

変数定義周り

以下、山根がKotlinキャッチアップ時に"Javaと違うな..."って感じたところを記載していきます。

Null Safety

変数がデフォルトで非NULL型。これがデカい。

当たり前ですが、非NULL型の記事にnullを代入しようとすると、コンパイルの時点でエラーになります。 これにより皆さんの嫌いなNullPointerExceptionが起きづらくなります。

var a: String = "hogehoge"
a = null //コンパイルエラー Null can not be a value of a non-null type String

var b: Int = "hogehoge"
b = null //コンパイルエラー Null can not be a value of a non-null type Int

型定義に"?"を付与することで、NULL許容型も作れます。

var a: String? = "hogehoge"
a = null //コンパイルOK

var b: Int? = "hogehoge"
b = null //コンパイルOK

定義した変数を呼ぶとき

NULL許容で変数定義した場合は、下記の2つのパターンでメソッドを呼ぶ。 後者は非推奨なので実質セーフコール演算子がベターです。

セーフコール演算子

値がNULLでない場合のみメソッドが呼ばれる。

var a: String? = "hogehoge"
println(a?.length) // 8

var b: String? = null
println(b?.length) // null lengthメソッドは呼ばれない

非NULL表明演算子

一度NULL許容で定義したものを、NULL非許容な型に変更できる。

val nullable: String? = getNullable()
val nonnull: String = nullable!!

またメソッド呼び出しと一緒に使うと、NULLだろうが何だろうがメソッドを呼ぶようになります。 当然nullとなっている変数に対してメソッドが呼ばれるとNullPointerExceptionが出ますので、注意して扱うようにしてください。

var a: String? = "hogehoge"
println(a!!.length) // 8

var b: String? = null
println(b!!.length) // ぬるぽ

変数定義時にval(再代入不可能) OR var(再代入可能)を必ず指定する

  • val...再代入不可
  • var...再代入可。

val側はJavaでいうfinal定義みたいなもんです。 特に理由がない限りvalを使って、変数のスコープを小さく書くのがベターですね。

var a: String = "hogehoge"
a = ”piyo” //OK

val b: String= "hogehoge"
b = ”piyo” //コンパイルエラー。 "Val cannot be reassined"(再代入すんな)

型推論可能

明示的に型を指定しなくても、推論してくれます。

val a = "hogehoge"
a.startsWith("h") // startWithはStringクラスが持つメソッド。 コンパイルエラーにならない

lateinit var

lateinitはインスタンス生成時にまだ初期化したくない変数を定義するときに使います。

lateinit var hoge: String //初期化しなくてもコンパイルが通る

クラス定義周り

今までは変数を定義する時の話でしたが、以下はクラスを定義する際の話です。

CompanionObject

インスタンス生成せずにメソッド/変数を呼びたい時、static定義ではなく、 代わりにCompanionObjectなるものを用いる

class Hoge {
//クラス内で定義
    companion object {
        fun fuga(str : String) {
            println("$str")
        }
    }
}

Hoge.fuga("Kotlin")

変数定義についても同様 クラス内での変数定義の話

データクラス

class定義時にdata classとすると、以下のメソッドを自動で実装してくれます。

  • equals
  • hashCode
  • copy
  • toString()
    • Object(key1=value1, key2=value2)の文字列を返す
  • componentN

Javaで言うところのlombok.@Dataが言語のデフォルトになったイメージです。

import java.time.LocalDate
data class Book {
    var id: Long? = null,
    var title: String? = null,
    var author: String? = null,
)
val book = Book(1, "bocchan", "soseki")
println(book.toString()) //Book(id=null, title="bocchan", author="soseki")

可視性

デフォルト/final

Kotlinでは、クラスはデフォルトで継承不可(Javaでいうfinalクラス状態)になっています。 明示的にコードで表現したい場合にはfinalをつけることも可能です。

class User(val name: String, val age: Int) //継承不可
final class User(val name: String, val age: Int) //継承不可

open

他ファイルでもそのクラスを継承したい場合はopenをつけます。

open class User(val name: String, val age: Int) //継承可

seald

同じファイルに定義されているクラスからのみ継承を許す (=別ファイルに定義されたクラスから継承不可)

seald class User(val name: String, val age: Int) //「別ファイルに定義されたクラスから」継承不可

getter/setter定義不要

  class User1 {
    var name: String = ""
  }

var変数に対しては、getter/setterメソッドの定義が不要です。また、呼び出しも=だけで代用できます。

  val user = User1()
  user.name = "Tanaka" // user.setName()とか書かなくて良い
  println(user.name) //Tanaka

valで定義するとgetterのみ生成されます。(再代入不可なので当然っちゃ当然)

getter/setterを拡張することもできる

  class User2 {
    lateinit var name: String
    // isValidNameプロパティに対するget()関数の処理を書き換える
    val isValidName: Boolean
      get() = name != ""
  }

上記のようにgetterメソッドを拡張することもできます。

  val user = User2()
  user.name = "Tanaka" 
  println(user.isValidName) //True

また、

  class User3 {
    var name: String = ""
      //空文字の場合はデフォルト値として"Kotlin"を返す拡張
      set(value) {
        if (value == "") {
          field = "Kotlin"
        } else {
          field = value
        }
      }
  }

上記のようにsetterメソッドを拡張することもできます。

  val user = User3()
  user.name = "" //空文字を登録しているようで、拡張setterで実質"Kotlin"を代入している
  println(user.name) //"Kotlin"

コンストラクタ

プライマリコンストラクタ

class Person constructor(
  val firstName: String, 
  val lastName: String, 
  var isEmployed: Boolean = true
)

コンストラクタに注釈・可視性修飾子がない場合は、constructorを省略できる

class Person(
  val firstName: String, 
  val lastName: String, 
  var isEmployed: Boolean = true
)

コンストラクタに注釈・可視性修飾子がある場合は、constructor必須

class Person public consutructor (
  val firstName: String, 
  val lastName: String, 
  var isEmployed: Boolean = true
)

セカンダリコンストラクタ

class Person (private val name: String, private val age: Int) {
    // thisはプライマリコンストラクタを指す
    constructor(name: String) : this(name, 20)
}

話が若干逸れるのですが、Kotlinのコンストラクタ周りについて調べている時、kotlinでSingletonパターン書こうとしたらどうなるんだろうな...と思ったりしました。 結果、objectで生成する方法が有力らしいです。

インスタンス生成周り

クラスを定義した後、クラスをインスタンス化して利用する時のお話です。

List,Mapのインスタンス生成(ミュータブル or 読み取り専用)

  val mutableList = mutableListOf(1, 2, 3) //読み取り可能 + add可能
  val List = listOf(1, 2, 3)  //読み取り可能 addは不可...★
  val mutableSet = mutableSetOf(1, 2, 3) //読み取り可能 + add可能
  val set = setOf(1, 2, 3)  //読み取り可能 addは不可...★

★なお、List,Setは"読み取り専用"なだけで、イミュータブルではない。 一度定義した後でも、下記のように参照先を書き換えると変更可能。

    val mutableList = mutableListOf(1, 2, 3)
    val list: List<Int> = mutableList //Listインターフェース。 読み取り専用。この時点では(1,2,3)
    println(list) //1,2,3

    mutableList.add(9)
    println(mutableList) //1,2,3,9
    println(list) //1,2,3,9