💨

Kotlinの基本概念だけを学習してみた

2024/06/23に公開

はじめに

業務で使用することはない Kotlin ですが、Better Java と言われているので学習してみました。

Null 安全

Kotlin では、変数に null 値を代入することが明示的に制御されています。これにより、NullPointerException(NPE)の発生を防ぎます。

fun main() {
    var a: String = "abc"
    // a = null // コンパイルエラー
    var b: String? = "abc"
    b = null // OK
}

コンパイル時の警告

main.kt:2:9: warning: variable 'a' is never used
    var a: String = "abc"
        ^
main.kt:4:9: warning: variable 'b' is assigned but never accessed
    var b: String? = "abc"
        ^
main.kt:5:5: warning: the value 'null' assigned to 'var b: String? defined in main' is never used
    b = null // OK
    ^

この例では、a は非 null 型の変数として宣言されていますが、b は nullable 型の変数として宣言されています。そのため、a に null を代入しようとするとコンパイルエラーが発生しますが、b には null を代入することができます。また、未使用の変数や未アクセスの変数についての警告も表示されます。

非 null 型の変数に null を代入した場合

コンパイルエラー

main.kt:3:9: error: null can not be a value of a non-null type String
    a = null // コンパイルエラー
        ^

Null 安全のメリット

  1. NullPointerException の防止: Null 安全により、実行時に発生する NullPointerException をコンパイル時に防ぐことができます。
  2. コードの明確化: 変数の型に null 許容かどうかを明示することで、コードの意図が明確になります。
  3. バグの減少: Null に関連するバグを減少させ、信頼性の高いコードを書くことができます。

スマートキャスト

Kotlin では、型チェックの後にその変数を自動的にキャストする「スマートキャスト」という機能があります。

fun demo(x: Any) {
    if (x is String) {
        println(x.length) // x は自動的に String 型としてキャストされる
    }
}

fun main() {
    demo("Hello, Kotlin")
}

この例では、x が String 型であるかどうかをチェックした後、x は自動的に String 型としてキャストされ、そのプロパティやメソッドにアクセスすることができます。

スマートキャストのメリット

  1. コードの簡潔化: 型キャストのコードを明示的に書く必要がなく、コードが簡潔になります。
  2. 型安全性の向上: 型チェックとキャストが一貫して行われるため、型エラーを防ぐことができます。
  3. 可読性の向上: 条件によって型が明確にわかるため、コードの可読性が向上します。

データクラス

Kotlin では、データを保持するための特別なクラスである「データクラス」を簡単に定義することができます。データクラスは、自動的に equals(), hashCode(), toString() などのメソッドを生成します。

data class User(val name: String, val age: Int)

fun main() {
    val user1 = User("Alice", 25)
    val user2 = User("Bob", 30)
    println(user1) // User(name=Alice, age=25)
    println(user2) // User(name=Bob, age=30)
}

この例では、User というデータクラスを定義し、そのインスタンスを作成しています。toString() メソッドが自動的に生成されているため、user1 を出力するとその内容が表示されます。

データクラスのメリット

  1. ボイラープレートコードの削減: 自動生成されるメソッドにより、手動でのメソッド定義が不要になります。
  2. 可読性の向上: データクラスを使用することで、クラスの主目的がデータ保持であることが明確になります。
  3. 一貫性の確保: equals(), hashCode(), toString() が一貫して生成されるため、バグのリスクが減ります。

拡張関数

Kotlin では、既存のクラスに対して新しいメソッドを追加することができる「拡張関数」という機能があります。

fun String.lastChar(): Char = this.get(this.length - 1)

fun main() {
    val str = "Kotlin"
    println(str.lastChar()) // n
}

この例では、String クラスに対して lastChar という拡張関数を追加しています。この関数を使うと、文字列の最後の文字を取得することができます。

拡張関数のメリット

  1. 既存コードの再利用: クラスのソースコードを変更せずに新しい機能を追加できます。
  2. 可読性の向上: クラスのメソッドのように使用できるため、コードが直感的に理解しやすくなります。
  3. 柔軟性の向上: 必要に応じてクラスに新しいメソッドを追加できるため、コードの柔軟性が増します。

Sealed クラス

Kotlin には「Sealed クラス」という概念があります。Sealed クラスは、特定のクラスのサブクラスを限定するために使用されます。これにより、条件分岐で exhaustiveness(網羅性)を保証できます。

sealed class Result
data class Success(val data: String) : Result()
data class Failure(val error: String) : Result()

fun handleResult(result: Result) {
    when (result) {
        is Success -> println("Success with data: ${result.data}")
        is Failure -> println("Failure with error: ${result.error}")
    }
}

fun main() {
    val success = Success("Data loaded successfully")
    val failure = Failure("Failed to load data")
    handleResult(success)
    handleResult(failure)
}

この例では、Result という Sealed クラスを定義し、そのサブクラスとして Success と Failure を定義しています。handleResult 関数では、when 式を使って Result の各サブクラスに応じた処理を行います。

エラー発生版

sealed class Result
data class Success(val data: String) : Result()
data class Failure(val error: String) : Result()

fun handleResult(result: Result) {
    when (result) {
        is Success -> println("Success with data: ${result.data}")
        // is Failure -> println("Failure with error: ${result.error}") // コメントアウトして網羅性を欠如させる
    }
}

fun main() {
    val success = Success("Data loaded successfully")
    val failure = Failure("Failed to load data")
    handleResult(success)
    handleResult(failure)
}

コンパイルエラー

このコードは、when 式で全てのサブクラスをチェックしていないため、コンパイルエラーが発生します。

main.kt:6:11: error: 'when' expression must be exhaustive, add necessary 'is Failure' branch or 'else' branch instead
    when (result) {
          ^

この例では、Result という Sealed クラスを定義し、そのサブクラスとして Success と Failure を定義しています。しかし、handleResult 関数の when 式で Failure の分岐がコメントアウトされているため、網羅性が欠如し、コンパイルエラーが発生します。

Sealed クラスのメリット

  1. 条件分岐の網羅性保証: Sealed クラスを使用することで、when 式で全てのサブクラスをチェックすることが保証され、コンパイル時に検出できます。
  2. コードの明確化: サブクラスの種類が限定されるため、コードの意図が明確になります。
  3. 型安全性の向上: 全てのサブクラスがコンパイル時に判定されるため、型安全性が向上します。

Delegation

Kotlin には「Delegation」という概念があります。これは、クラスがインターフェースを実装する際に、その実装を他のオブジェクトに委譲することができる機能です。

interface Printer {
    fun print()
}

class PrinterImpl(val message: String) : Printer {
    override fun print() {
        println(message)
    }
}

class MyPrinter(printer: Printer) : Printer by printer

fun main() {
    val printer = PrinterImpl("Hello, Kotlin!")
    val myPrinter = MyPrinter(printer)
    myPrinter.print() // Hello, Kotlin!
}

この例では、Printer インターフェースを実装する PrinterImpl クラスがあり、MyPrinter クラスは Printer インターフェースの実装を PrinterImpl に委譲しています。

Delegation のメリット

  1. コードの再利用: 共通の機能を持つオブジェクトに処理を委譲することで、コードの再利用性が向上します。
  2. シンプルな設計: クラスの設計をシンプルに保ちつつ、機能を拡張できます。
  3. 柔軟性の向上: インターフェースの実装を容易に変更でき、柔軟性が向上します。

レシーバー

Kotlin には「レシーバー」という概念があります。レシーバーを使うことで、特定のクラスやオブジェクトに対して関数やプロパティを追加することができます。

fun String.firstChar(): Char {
    return this[0]
}

fun main() {
    val str = "Kotlin"
    println(str.firstChar()) // K
}

この例では、String クラスに対して firstChar という拡張関数を追加しています。この関数を使うと、文字列の最初の文字を取得することができます。

レシーバーのメリット

  1. 既存クラスの拡張: クラスのソースコードを変更せずに新しいメソッドを追加できます。
  2. コードの可読性向上: クラスのメソッドのように使用できるため、コードが直感的に理解しやすくなります。
  3. 柔軟性の向上: 必要に応じてクラスに新しいメソッドを追加できるため、コードの柔軟性が増します。

Destructuring Declarations

Kotlin には「破棄可能な変数」という概念があります。これにより、オブジェクトのプロパティを簡単に複数の変数に分解することができます。

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 25)
    val (name, age) = person
    println("Name: $name, Age: $age") // Name: Alice, Age: 25
}

この例では、Person データクラスのインスタンス person を name と age という 2 つの変数に分解しています。

Destructuring Declarations のメリット

  1. コードの簡潔化: オブジェクトのプロパティを簡単に変数に分解できるため、コードが簡潔になります。
  2. 可読性の向上: 必要なプロパティを個別に変数として扱うことで、コードの可読性が向上します。
  3. 柔軟なデータ操作: データクラスやコレクションの要素を簡単に分解して操作できます。

範囲(Range)

Kotlin には「範囲」という概念があります。範囲を使うことで、連続する値の集合を簡単に扱うことができます。

fun main() {
    val range = 1..5
    for (i in range) {
        println(i) // 1, 2, 3, 4, 5
    }

    if (3 in range) {
        println("3 is in the range")
    }
}

この例では、1 から 5 までの範囲を定義し、その範囲内の数値を出力しています。また、3 が範囲内に含まれているかどうかをチェックしています。

範囲のメリット

  1. コードの簡潔化: 範囲を使うことで、連続する値の集合を簡単に定義できます。
  2. 柔軟な条件チェック: 範囲内に特定の値が含まれているかどうかを簡単にチェックできます。
  3. 可読性の向上: 直感的な範囲指定により、コードの可読性が向上します。

まとめ

Java の良くないところを取り除いた JVM 系言語って感じで非常に良いなと感じました。Java の資産をそのまま利用することもできるので、必要な分だけを Kotlin に移行できることが分かりました。

Discussion