初心者のための「Kotlin」入門

2017年に「Kotlin」がAndroidアプリの開発言語として正式に採用されたことで、「kotlin」に注目が集まりました。

「Kotlin」は「Java」より、プログラムがシンプルに書けることで、プログラミングの行いやすさも格段に向上します。

それでは、「Kotlin」言語の書き方についてご説明していきたいと思います。

「Kotlin」の実行環境

はじめに、さまざまな「Kotlinの実行環境」についてご説明をしていきたいと思います。

「プログラミングを学び始めた方」には、プログラムの書き方や考え方に慣れるまで、少し時間がかかるかもしれませんが、「Kotlinの実行環境」を構築して、実際に「Kotlin」を書いて実行しながら学んでいきましょう。

「Kotlin」を実行できる環境はいくつかありますが、「Androidアプリ」の開発で利用されている言語であるため、「Android Studio」をインストールして実行する方法が、「Kotlin」の実行環境が整えやすいのではないかと思います。

→「Android Studio」

将来は「Androidアプリを開発したい!」という方は、「Kotlin」を学びながら「Android Studio」の使い方にも慣れていきましょう。

他にも「Kotlin Compiler」を利用する方法もありますが、コマンドを打ってコンパイルを行う形式のため、あまり初心者向きの方法ではありません。

「Kotlin Compiler」を利用してみたい方は、

→「kotlinlang.org」

からダウンロードすることができます。

このページの下部では、「Kotlin」プログラムを書いて実行できる「オンライン実行環境」も用意されています。

他にも「Kotlin」を開発した「JetBrains」社の「IntelliJ IDEA(統合開発環境)」を利用した実行方法もあります。

→「IntelliJ IDEA」

「Android Studio」はこの「IntelliJ IDEA」がベースとなっているため、この2つの開発環境は「画面レイアウト」も非常に酷似しています。

「Kotlin」の構文

ここから「kotlinの構文」についてご紹介していきたいと思います。

目次

  1. エントリーポイント
  2. 変数・定数
  3. データ型
  4. 「null」の取り扱いについて
  5. 演算子
  6. 「Nullable型」関連演算子
  7. 分岐・繰り返し
  8. 配列
  9. コレクション
  10. コレクションの操作
  11. 関数
  12. クラス
  13. コンストラクタ
  14. 継承
  15. 抽象クラス・抽象メソッド
  16. インターフェース
  17. オブジェクト宣言
  18. コンパニオンオブジェクト
  19. データクラス
  20. 列挙型

「Java」と同様に「Kotlin」でも「main」関数からプログラムが実行されます。

fun main( args: Array < String >) {
	//プログラムを記述
}

「関数の作り方」は、後ほどご説明をしていきたいと思いますが、「Java」に比べるとかなり簡潔に「main関数」を書くことができます。

「Android Studio」で「Androidアプリ」のプロジェクトを作成した場合は、起動する「アクティビティ」の「onCreateメソッド」に処理を記述します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //プログラムを記述
    }
}

変数

変数を定義するには、

var 変数名:データ型 = 初期値

のように書きます。例えば、

var age:Int = 23

のようになります。

データ型は省略することもできます。

var age = 23

定数

定数を定義するには、

val 定数名 = 値

のように書きます。例えば、

val age = 0

のようになります。

「Kotlin」のデータ型には、次のようなものがあります。

整数型

Short
16ビット整数
Int
32ビット整数
Long
64ビット整数

浮動小数型

Float
32ビット浮動小数
Double
64ビット浮動小数

文字・文字列型

Char
文字型
String
文字列型

「Java」では、「値が無い」ことを表す「null」を変数に代入することができましたが、「Kotlin」では、「Nullable型」の変数にしないと「null」を変数に代入することができません。

「Nullable型」にするためには、変数の宣言時に指定する「データ型」の末尾に「?」を付加します。

var 変数名:データ型? = 初期値

もし「?」を付けずに「null」を代入してしまうと、

Null can not be a value of a non-null type Int

のようにエラーが発生します。

変数に「null」を代入する場合は、

var num:Int? = null

のように、必ずデータ型の末尾に「?」を付加してください。

「Kotlin」で利用できるさまざまな演算子について見ていきましょう。

プログラムを作る際に「変数と値の比較」を行うことはよくありますが「等しいかどうか?」を判定できる演算子は2つあります。

等価演算子

==
「値」が等しい
===
「参照」が等しい

「Kotlin」での「==」はJavaでの「equalsメソッド」相当の比較を行います。

次に「計算」が行える演算子について見ていきたいと思います。

算術演算子

+
加算(足し算)
-
減算(引き算)
*
乗算(掛け算)
/
除算(割り算)
%
剰余(割り算の余り)

「Nullable型」を扱う際に必要な演算子を利用することで、「安全にnullを扱う」ことができます。

「?」演算子

「null」を格納した変数の値を取り出す場合は、「?」演算子を利用して値を取り出すことで、「null」が入っていてもエラーを出さず「null」を返すことができます。

例えば、

var weather:String? = null
print(weather.length)

のように書くと、

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

のようなエラーが発生してしまいます。

そこで、

var weather:String? = null
print(weather?.length)

のように「String」のデータ型の末尾に「?」を書くと、

null

と表示され、エラーは出ないようになります。

「!!」演算子

「!!演算子」は強制的に「nullではないデータ型」に変換することができます。

しかし、「null」などの変換することができない値が格納されているとエラーが発生してしまいます。

例えば、

var num:Int? = null
print(num!!)

のように書いてしまうと、

Exception in thread "main" kotlin.KotlinNullPointerException

のようにエラーが発生しますので、必ず「nullではないこと」が保証された状態で利用する必要があります。

「条件」によって実行するプログラムを切り替えることができる「分岐構文」と、「条件」を満たしている間は処理を繰り返す「繰り返し構文」について見ていきたいと思います。

分岐(if)

「if式」を使うと、条件を満たしている場合だけ処理を行うことができます。

if( 条件1 ){
    //条件1を満たしている時の処理
} else if( 条件2 ){
    //条件2を満たしている時の処理
} else {
    //条件1・2のどちらも満たさなかった時の処理
}

例えば、

val num = 7
if( num < 5 ){
    print("low")
} else if( num  < 8 ){
    print("high")
} else {
    print("very high")
}

のようになります。

このプログラムを実行すると、「else if( num < 8 )」の条件に合致し、「high」と表示されます。

Javaでは、「if」は「文」ですが、Kotlinでは、「式」のため、「式の評価結果を返す」ことができます。

val num = 11
var result = if( num > 10 ){
    "high"
} else {
    "low"
}
println(result)

それぞれの条件の実行処理に「返したい値」を記述すると「if式」の「返り値」として「result変数」に値が代入されます。

上記のプログラムでは、「result変数」に「high」の文字列が返されます。

分岐(when)

「when式」では、「指定した変数・定数」がある「値」と同じかどうかを調べることができます。

when(変数・定数){
    比較する値1->{
        //「比較する値1」と同じ場合の処理
    }
    比較する値2->{
        //「比較する値2」と同じ場合の処理
    }
    比較する値3->{
        //「比較する値3」と同じ場合の処理
    } else -> {
    	//「比較する値1・2・3」と合致しない場合の処理
    }
}

例えば、

val num = 5

var result = when(num){
    1->{
    	"numは「1」です。"
    }
    2->{
        "numは「2」です。"
    }
    3->{
        "numは「3」です。"
    } else -> {
        "numは「1・2・3」ではありません。"
    }
}
println(result)

のように書くことができ、この場合は、『numは「1・2・3」ではありません。』と出力されます。

繰り返し(for)

「for」の書式は、

for( 変数 in オブジェクト){
	//繰り返す処理
}

のように書きますが、「オブジェクト」には「配列」などのデータを指定します。

「配列」を例にすると、

var numbers = arrayOf(1,2,3,4,5,6,7,8,9,10);

for ( num in numbers ){
    println(num)
}

のように書くことができます。

出力結果は、

1
2
3
4
5
6
7
8
9
10

のようになります。

「繰り返し回数」を指定したい場合は「範囲演算子」を利用します。

例えば、「1~10」を出力したい場合は、

for( i in 1..10){
    println(i)
}

のように書きます。

「配列」の利用方法についてご説明していきたいと思いますが、「Kotlin」では、さまざまな「配列の作り方」があります。

配列の作り方1

配列は「Arrayクラス」のインスタンスですので、まず初めに「コンストラクタ」を利用した配列の作り方について見ていきましょう。

書式は、

var 変数名 = Array<データ型>(要素数,初期化処理);

のようになります。配列を作ってみると、

var numbers = Array<Int>(10,{it});

初期化処理には「ラムダ式」を指定していますが、最後の引数が「ラムダ式」の場合は引数の外側に出すことができます。

var numbers = Array<Int>(10){it};

「it」には配列の要素番号と同じ値が入っています。

配列の作り方2

配列に格納する「値」を直接指定して格納したい場合は、「arrayOfメソッド」の引数に作成したい「値」を指定します。

var numbers = arrayOf(1,2,3,4,5,6,7,8,9,10);

配列の「値」を全て「null」で作成したい場合は、「arrayOfNullsメソッド」の引数に作成したい「要素数」を指定します。

var numbers:Array<Int?> = arrayOfNulls(5);

配列の値の取り出し方1

配列の変数名の横にブラケットを書き、その中に配列の要素番号を指定します。

配列変数名[要素番号]

例えば、

numbers[3]

のように書きます。

配列の値の取り出し方2

「配列」の実体は、「Arrayクラス」のオブジェクトですので、「Arrayクラス」の「getメソッド」でも値を取り出すことができます。

配列変数名.get(要素番号)

例えば、

numbers.get(3)

のようになります。

配列の要素数の取得

配列の「要素数」は、「Arrayクラス」の「size」プロパティから取得することができます。

配列変数名.size

例として、

numbers.size

のようになります。

配列の値の設定方法1

「値」を設定する場合は、

配列変数名[要素番号] = 設定値

のように書くことができます。例えば、

numbers[3] = 22

のようになります。

配列の値の設定方法2

「Arrayクラス」の「setメソッド」で値を設定することもできます。

配列変数名.set(要素番号, 設定値)

のように書いていきます。例えば、

numbers.set(3, 22)

と書くこともできます。

「リスト」が利用できる「コレクション」の機能について見ていきたいと思います。

「Kotlin」の「コレクションタイプ」には、

  1. List
  2. Set
  3. Map

の3つがあります。

List

「List」は「データの順序」が保証されているため、格納した順番に値を取り出すことができます。

「List」インスタンスを作るには、「listOfメソッド」または「mutableListOfメソッド」を利用します。

「List」インスタンスの生成

listOfメソッド
読み取り専用
var 変数名 = listOf(値1,値2,値3・・・)
mutableListOfメソッド
追加可能
var 変数名 = mutableListOf(値1,値2,値3・・・)

Set

「Set」は重複した値を持つことができず、また「データの順序」も保証されません。

「Set」インスタンスを作るには、「setOfメソッド」または「mutableSetOfメソッド」を利用します。

「Set」インスタンスの生成

setOfメソッド
読み取り専用
var 変数名 = setOf(値1,値2,値3・・・)
mutableSetOfメソッド
追加可能
var 変数名 = mutableSetOf(値1,値2,値3・・・)

Map

「Map」は「キーと値」をペアで持つことができます。

「Map」インスタンスを作るには、「mapOfメソッド」または「mapListOfメソッド」を利用します。

「Map」インスタンスの生成

mapOfメソッド
読み取り専用
var 変数名 = mapOf(値1,値2,値3・・・)
mutableMapOfメソッド
追加可能
var 変数名 = mutableMapOf(キー1 to 値1, キー2 to 値2, キー3 to 値3・・・)

「List・Set・Map」には、これらを操作するための「メソッド・プロパティ」がありますので、ご説明していきたいと思います。

これから紹介する「メソッド・プロパティ」では、下記のような「Mutable」な「List・Set・Map」を操作していきます。

var fruitsList = mutableListOf("orange","melon","grape")
var fruitsSet = mutableSetOf("orange","melon","grape")
var fruitsMap = mutableMapOf("One" to "orange", "Two" to "melon", "Three" to "grape")

size

「格納されているデータ数」を取得できるのが、「sizeプロパティ」です。

fruitsList.size

とすると、「データ数」である「3」を取得することができます。

contains(List・Setのみ)

このメソッドは、「List・Set」のみ利用することができ、「指定した値が含まれているか?」を調べることができます。

値が含まれていれば「true」、含まれていなければ「false」が返ってきます。

fruitsList.contains("orange")

containsKey(Mapのみ)

「Map」の中に「指定したKeyの値が含まれているか?」を調べることができます。

キーが含まれていれば「true」、含まれていなければ「false」が返ります。

fruitsMap.containsKey("One")

containsValue(Mapのみ)

「Map」の中に「指定した値が含まれているか?」を調べることができます。

値が含まれていれば「true」、含まれていなければ「false」が返ります。

fruitsMap.containsValue("Orange")

containsAll(List・Setのみ)

「List・Set」の中に、「指定したList・Setの内容が全て含まれているか?」を調べることができます。

全て含まれていれば「true」、そうで無ければ「false」が返ります。

var compareList = listOf("orange", "grape")
fruitsList.containsAll(compareList)

上記の場合は、「compareList」の「orange」「grape」の値が「fruitsList」の中に存在しているため「true」となります。

get(List・Mapのみ)

「get」は、「List・Map」の値を取得するためのメソッドです。

引数に「取得したい要素番号」を指定しますが、「Set」には「順序」という概念が無いため、「要素番号」を指定して取得することはできません。

fruitsList.get(1)

indexOf(List・Setのみ)

「indexOf」は「指定した値」が含まれている場合に、最初に出現する「コレクション内での位置(要素番号)」を調べることができます。

「返り値」は「位置(要素番号)」が返ってきますが、「値」が無ければ「-1」が返ってきます。

fruitsList.indexOf("orange")

lastIndexOf(List・Setのみ)

「lastIndexOf」は「指定した値」が含まれている場合に、最後に出現する「コレクション内での位置(要素番号)」を調べることができます。

「返り値」は「位置(要素番号)」が返ってきますが、「値」が無ければ「-1」が返ってきます。

fruitsList.lastIndexOf("orange")

isEmpty(List・Set・Map)

「コレクション」内が「空(データが0)」かどうかを調べることができます。

「空」であれば「true」、そうでなければ「false」が返ります。

var fruitsList = listOf()
print(fruitsList.isEmpty()) //true

indices(List・Setのみ)

「コレクション」の範囲を取得できるのが「indices」プロパティです。

例えば、

print(fruitsList.indices)

のように実行すると、

0..2

のように出力されます。

lastIndex(Listのみ)

最後の「要素番号」を取得できるのが「lastIndex」プロパティです。

fruitsList.lastIndex

iterator(List・Set・Map)

このメソッドを実行すると、「イテレータ」を返り値として受け取ることができます。

fruitsList.iterator()

listIterator(Listのみ)

このメソッドを実行すると、「リストイテレータ」を返り値として受け取ることができます。

fruitsList.listIterator()

subList(Listのみ)

「開始位置」と「終了位置」を指定して「List」の中の「指定範囲」のデータを取得することができます。

開始位置は「0」から始まり、「終了位置」の1つ前までの値を取得することができます。

例えば、

//順序:                           0        1       2
var fruitsList = mutableListOf("orange","melon","grape")
print(fruitsList.subList(0,2))

出力結果は、

[orange, melon]

のように出力されます。

「終了番号」に「2」を指定すると「2番目」の「grape」は含まれず、1つ前の「melon」までが取得されます。

all(List・Set・Map)

「List・Set・Map」内の全てのデータが、指定した条件を満たしていれば「true」、そうでなければ「false」が返ります。

var numbers = listOf(1,2,3,4,5)
print(numbers.all{it < 6})

「List」内の値は全て「6未満」の場合のため、このプログラムの返り値は「false」となります。

any(List・Set・Map)

「List・Set・Map」内のどれか1つのデータが指定した条件を満たしていれば「true」、そうでなければ「false」が返ります。

var numbers = listOf(1,2,3,4,5)
print(numbers.any){it > 3})

「List」内の「4・5」の値は「3以上」で条件を満たしているため、このプログラムの出力は「true」になります。

asIterable(List・Set・Map)

「コレクション」でないオブジェクトを「Iterable」に変換します。

例えば、

var numbers = arrayOf(1,2,3,4,5)
print(numbers.javaClass.kotlin)

を実行すると、

class kotlin.Array

と表示されますが、「asIterable」を適用し、

var numbers = arrayOf(1,2,3,4,5).asIterable()
print(numbers.javaClass.kotlin)

を実行すると、

class kotlin.collections.ArraysKt___ArraysKt$asIterable$$inlined$Iterable$1

のように配列が「コレクション」に変換されています。

asSequence

「コレクション」ではないオブジェクトを「Sequence」に変換します。

var numbers = arrayOf(100,200,300,400,500).asSequence()

「asIterable」と同様に戻り値には変換したオブジェクトが返ってきます。

asReserved(Listのみ)

「asReserved」は、Listのデータを逆順に入れ替えることができます。

返り値には、「逆順」にしたリストが返ってきます。

var numbers = listOf(100,200,300,400,500)
numbers.forEach({ datum ->
    println(datum)
})

を実行すると、

100
200
300
400
500

のように表示されます

この「List」に対して、

var numbers_rec = numbers.asReversed()
numbers_rec.forEach({ datum ->
    println(datum)
})

を実行すると、

500
400
300
200
100

のように、「List」のデータを「逆順」にしたデータを得ることができます。

他にもたくさんのメソッドが用意されていますので、さまざまなメソッドの使い方について学んでみてください。

関数定義

Kotlinの関数の定義方法は、

fun 関数名(引数:引数のデータ型):戻り値のデータ型{
    //関数の処理
}

のようになります。

例えば、「長方形」の面積を求める関数は、

fun getRectangleArea(sideLength1: Int, sideLength2: Int):Int{
    return sideLength1 * sideLength2
}

のようになります。関数を実行する場合は、

print(getRectangleArea(10,20))

のようになり、このプログラムを実行すると「200」が出力されます。

関数の代入的宣言

処理が「1つ」しかない場合は、

fun getRectangleArea(sideLength1: Int, sideLength2: Int) = sideLength1 * sideLength2

のように書くこともできます。

関数の「戻り値」が無い場合

戻り値が無い関数は、

fun displayGreetFromString(message: String): Unit {
    println(message)
}

のように「戻り値」の部分に「Unit」を指定しますが、「Unit」は省略することもできます。

関数の引数の「デフォルト値」

関数の「引数」には「デフォルト値」を指定することができます。例えば、

fun add(num1:Int = 1, num2:Int = 1, num3:Int =1) = num1 + num2 + num3
println(add())

のように書くと「3」と出力され、引数に指定が無い場合は、引数の「デフォルト値(1)」がそれぞれの引数に設定されます。

引数名の指定

関数を呼び出す際に、「引数名」を指定して呼び出すことができます。「add」関数を「引数名」を付加して呼び出すと、

println(add(num2=10))

のようになります。

「num1」と「num3」は、引数を指定しない場合、デフォルト値の「1」が設定されますので、出力結果は、「2」となります。

関数の複数の戻り値

「Kotlinの関数」は、「複数の値」を戻り値として返すことができます。値を2つ返す場合は「Pair」を利用します。

戻り値に「Pair<戻り値1のデータ型,戻り値2のデータ型>」のように記述します。

長方形の「面積」と「辺の長さ」を返す関数を作ってみると、

fun getRectangleArea(sideLength1: Int, sideLength2: Int):Pair<Int, Int>{
    return Pair(sideLength1 * sideLength2,sideLength1*2 + sideLength2*2)
}

var (area, length) = getRectangleArea(7,12)
println("area=" + area)
println("length=" + length)

のようになります。このプログラムを実行すると、

area=84
length=38

のように出力されます。

値を3つ返すこともでき、戻り値のデータ型に「Triple<戻り値1のデータ型,戻り値2のデータ型,戻り値3のデータ型>」のように記述します。

「5%」「8%」「10%」の税込み金額を計算するプログラムを作ると、

fun getAmountIncludingTax(price: Int):Triple<Double, Double, Double>{
	return Triple(price*1.05, price*1.08, price*1.1)
}
				
var (five_per, eight_per, ten_per) = getAmountIncludingTax(100)
				
println("5%=" + five_per)
println("8%=" + eight_per)
println("10%=" + ten_per)

のようになります。このプログラムの出力結果は、

5%=105.0
8%=108.0
10%=110.00000000000001

のようになります。

「Kotlin」はJavaと同様に「オブジェクト指向言語」ですので、「クラス」を作ることができます。

「Kotlin特有の仕組み」などもありますのでご説明していきたいと思います。

「クラス」の定義

クラスは下記の様に定義します。

class クラス名(){
    //クラスの実装内容
    //プロパティ
    //メソッド
}

例えば、

class Shop() {
    var name = "sample shop"
    var productType = "fruits"
    var maxProductsCount = 100

    override fun toString(): String {
        return "店舗名=" + name + " 取り扱い商品:" + productType + " 商品最大数:" + maxProductsCount
    }
}

ように作成します。

「toString」メソッドは、Javaと同様に「オブジェクトの文字列表現」を定義することができます。

インスタンスの作成

クラスから「インスタンス」を作成する場合は、

クラス名()

と書きますが、先ほどのクラスのインスタンスを作成するプログラムを作成してみると、

val shop_1 = Shop()
println(shop_1)

のようになり、出力結果は、

店舗名=sample shop 取り扱い商品:fruits 商品最大数:100

のようになります。

「ゲッター」と「セッター」の定義

「ゲッター」と「セッター」を定義するには、

var プロパティ名 = 初期値
    get() {
        return field
    }
    set(value) {
        field =  value
    }

のように定義します。

「Kotlin」のプロパティが直接値を保持しているのでは無くて、「バッキングフィールド」と呼ばれる「データ保存フィールド」に値を保存しています。

「field」はこの「バッキングフィールド」を表しています。

バッキングフィールドの「イメージ」は下図のようになります。

バッキングフィールド

「バッキングフィールド」へのアクセスは「ゲッター・セッター」を通じて行います。

例えば、

class Sample(){
    var datum = 0
        get() {
            return field
        }
        set(value) {
            field =  value
        }
}

var sample = Sample()

//ゲッターの実行
println(sample.datum)

//セッターの実行
sample.datum = 20

のようになり、「インスタンス変数名.プロパティ名」と書くと、プロパティを介して「バッキングフィールド」にアクセスができます。

セッターの中では「値のチェック」なども行えるため、「不正な値」は、「バッキングフィールド」へ代入されないよう制御することができます。

例えば、

set(value) {
    if( value < 0){
        field =  0
    } else {
        field = value
    }
}

「ゲッター」と「セッター」は省略することもできますが、その場合は、「プロパティ」を「バッキングフィールド」と同様に扱うことができます。

アクセス修飾子

クラスのアクセス修飾子は次のようになります。

クラスのアクセス修飾子

public
全てのクラスからアクセス可能
protected
サブクラスからアクセス可能
internal
同じモジュールからアクセス可能
private
クラス外からのアクセス付加

private set

「値を外部のクラスから変更されたくない」場合は、「private set」キーワードを付けると、「クラス内からしか変更できない」プロパティを作ることもできます。

例えば、

class Sample(){
    var datum = 0
        private set
}

のように「private set」キーワードを付けます。この状態でもし、

var sample = Sample()
sample.datum = -5

のように「セッター」を書いてしまうと、

Cannot assign to 'datum': the setter is private in 'Sample'

のようにエラーが発生し、クラスの外部からは値が変更できないようになります。

「ゲッター」は利用できますので、クラスの外部からでも「プロパティの値」を取得することは可能です。

「Kotlin」の「コンストラクタ」には、2つの種類があり、

プライマリーコンストラクタ
クラスの中で1つのみ作れるコンストラクタ(クラス宣言部に作成)
セカンダリーコンストラクタ
複数作れるコンストラクタ(クラスのプロパティ・メソッド宣言部に作成)

のように使い分けていきます。

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

クラスの宣言部に「プライマリーコンストラクタ」を定義することができます。

ただし、この部分に定義するのは「コンストラクタの引数」のみになります。

実際のコンストラクタの処理は「init」ブロックに記述していきます。

class クラス名 constructor(コンストラクタの引数 ){
    //プロパティの定義

    init{
        //コンストラクタの定義
    }
}

実際にコンストラクタを追加したクラスを作成すると、

class Food constructor(name:String, price:Int ){
    var name:String
    var price:Int
						
    init{
        this.name = name
        this.price = price
    }
}

のようになります。

クラス宣言部に「コンストラクタ」が含まれているのが特徴的な記述となっています。

今回作成したプログラムでは、「コンストラクタの引数」に指定した値をプロパティに設定する内容となっていますが、このような場合は、

class Food constructor(var name:String, var price:Int ){
    //プロパティ
    //メソッド
}

のように書くことができ、「コンストラクタの引数」に「var」キーワードを付けると、「init」ブロックは不要になります。

また、アクセス修飾子やアノテーションが無い場合は、「constructor」の記述も省略できるため、

class Food(var name:String, var price:Int ){
    //プロパティ
    //メソッド
}

のように書くこともできます。

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

2つ目以降のコンストラクタには「セカンダリコンストラクタ」を作ることができます。

「セカンダリコンストラクタ」の書式は、

constructor(コンストラクタ引数) プライマリーコンストラクタの呼び出し

上記のように、「セカンダリコンストラクタ」の中で「プライマリーコンストラクタ」を呼び出す必要があります。

先ほどの「Foodクラス」でプログラムを作ると、

class Food(var name: String, var price: Int) {
    constructor(name: String):this(name, 0)
}

のようになり、「:this」のキーワードを利用すると、このクラスの「別のコンストラクタ」を呼び出すことができます。

「継承」は、「親クラス」の「プロパティ・メソッド」が「子クラス」でも使えるようになる機能ですが、「継承」の書式は、

class クラス名 : 親クラス名

のように、「クラス名」の右側に「:」を書き、その後に「親クラス名」を書きます。

プログラムを作成してみると、

open class Parent() {
    var parentName: String = "Parent"
    open fun displayName(){
        println("クラス名は${this.parentName}です。")
    }
}

class Child : Parent {
    var childName: String = "Child"
    constructor():super()
    override fun displayName(){
        println("親のクラス名は「${this.parentName}」このクラスのクラス名は「${this.parentName}」です。" )
    }
}

のようになります。

「継承元」となるクラスは「open」指定しないと、

This type is final, so it cannot be inherited from

のようなエラーが発生しますので、「親クラス」となるクラスには「open」を指定しましょう。

また「継承先のクラス」で「オーバーライド」するメソッドにも「open」が必要になります。

var child = Child()
println(child.displayName())

のように「子クラス」のインスタンスを作成し、「親クラス」のメソッドをオーバーライドし、「displayName」メソッドを実行すると、

親のクラス名は「Parent」このクラスのクラス名は「Parent」です。

と表示されます。

「子クラス」のコンストラクタの

super()

の部分は、「子クラス」から「親クラスのコンストラクタ」を呼び出しています。

「kotlin」の継承は、「単一(単純)継承」ですので、「親クラス」は1つしか持つことができません。

継承

「継承先のクラス」にメソッドの実装を強制することができるのが「抽象メソッド」です。

「抽象メソッド」は、メソッド名の前に「abstract」キーワードを付加します。

abstract fun メソッド名(引数)

のように作成します。

「抽象メソッド」を持つクラスは、「抽象クラス」にする必要がありますので、クラス名の前に「abstract」を付ける必要があります。

「抽象クラス」と「抽象メソッド」を作成すると、

abstract class AbstractSample(){
    abstract fun sampleMethod()
}

のようになります。

「抽象メソッドの内容」は「継承先のクラス」で決めるため、「メソッドの実装内容」は書かないようにします。

「継承する先」のクラスの実装は下記のようになります。

class Sample : AbstractSample(){
    override fun sampleMethod(){
        println("sample")
    }
}

「AbstractSample」クラスの「sampleMethod」メソッドの内容を継承先の「Sample」クラスでオーバーライドしています。

もし「sampleMethod」メソッドをオーバーライドしなければ、

Class 'Sample' is not abstract and does not implement abstract base class member public abstract fun sampleMethod(): Unit defined in main.AbstractSample

のようにエラーが発生します。

そのため、継承先のクラスでも「抽象メソッド」を実装しないならば、そのクラスも「抽象クラス」にする必要があります。

「インターフェース」は、「1つのクラスに複数継承できる抽象クラス」のように振舞います。

「クラス」は「親クラス」を1つしか持つことができない(単純継承)のため、1つのクラスに複数のクラスの「抽象メソッド」を実装することができません。

しかし「インターフェース」は1つのクラスに複数のインターフェースを「適用(実装)」することができます。

インターフェース

インターフェイスの定義は、

interface インターフェース名{
    //インターフェースの内容(抽象メソッド・抽象プロパティ)
}

のように書きます。例えば、

interface SampleIf1{
    fun smpIfMethod1()
}
interface SampleIf2{
    fun smpIfMethod2()
}

のように「インターフェース」を作成します。

インターフェースを実装する場合は、

class クラス名 : インターフェース名1, インターフェース名2

のように書きます。

そして、このインターフェースを実装するクラスは、

class Sample() : SampleIf1, SampleIf2{
    override fun smpIfMethod1() {
        println("call smpIfMethod1")
    }
	
    override fun smpIfMethod2() {
        println("call smpIfMethod1")
    }
}

のように作成します。

もし、インターフェースに定義されている抽象メソッドを実装し忘れると、

Class 'Sample' is not abstract and does not implement abstract member public abstract fun smpIfMethod1(): Unit defined in SampleIf1

のようなエラーが発生してしまいます。

機能は「抽象クラス」と似ていますが、1つのクラスに「複数のインターフェース」を実装できることが特徴となっています。

インターフェースに「プロパティ」を持たせることもできます。

interface SampleIf{
    var num:Int
}

のように、インターフェースに「プロパティ」を定義し、実装先のクラスは、

class Sample : SampleIf {
    override var num:Int = 0
}

のように、インターフェースの「プロパティ」をオーバーライドして実装します。

もし、「プロパティ」の実装を忘れてしまうと、

Class 'Sample' is not abstract and does not implement abstract member public abstract var num: Int defined in SampleIf

のようにエラーが発生します。

「インターフェース」は、「コンストラクタ」は定義できず、「バッキングフィールド」へもアクセスができません。

「アクセス修飾子」は「public」のみ利用することができます。

「唯一のインスタンス」を作りたい場合には、「オブジェクト宣言」を利用します。

「オブジェクト宣言」は、

object オブジェクト名 {
    //プロパティ
    //メソッド
}

のようになります。例えば、

object Sample {
    var name: String = "Sample"
    var description: String = " Sample Object"
	
    fun displayObject(){
        println("name=${name} description=${description}")
    }
}

「クラス」の内容に該当する部分を定義し、すぐに「オブジェクト」を作成しています。

「オブジェクト」を利用する場合は、

オブジェクト名.メンバ

と書いて、「プロパティ」や「メソッド」を利用することができます。

「メソッド」を実行するプログラムを書いてみると、

Sample.displayObject()

のようになります。

このプログラムの実行結果は、

name=Sample description= Sample Object

となります。

オブジェクトを直接作成しているため、「コンストラクタ」はありませんが、下記のように「他のクラス」を継承してオブジェクトを作ることはできます。

object オブジェクト名 : 親クラス名 { ・・・}

「コンパニオンオブジェクト」は「クラス」の中に作成するオブジェクトです。

class クラス名 {
    companion object {
        //プロパティ
        //メソッド
    }
}

のように「コンパニオンオブジェクト」を定義します。

実際に作成すると、

class BaseClass {
    companion object {
        var num = 1
        fun displayName(){
            println("This class is BaseClass")
        }
    }
}

のようになります。

「コンパニオンオブジェクト」を利用するには、

クラス名.コンパニオンオブジェクトのメソッド名
クラス名.コンパニオンオブジェクトのプロパティ名

のようにプログラムを書いていきます。

例えば、

BaseClass.displayName()
println(BaseClass.num)

のようにプログラムを作成すると、

This class is BaseClass
1

のように出力されます。

「データ」を扱う専門のクラスは「データクラス」として定義することができます。

「class」の前に「data」キーワードを付加するだけなので、定義は非常に簡単です。

data class クラス名(引数){
    //定義内容(プロパティ・メソッド)	
}

「通常のクラス」と「データクラス」の違いは、

  • equals
  • hashCode
  • toString
  • component
  • copy

のメソッドが用意されているかの違いとメソッドの挙動の違いです。

equals

「equals」メソッドの違いを確認するために、下記のようなクラスを作成しました。

//「通常のクラス」の定義
class NormalData(var id:Int, var contents:String)
	
//「データクラス」の定義
data class Data(var id:Int, var contents:String)

//「通常のクラス」のインスタンスを作成
var ndata_1 = NormalData(1,"data")
var ndata_2 = NormalData(1,"data")

//「データクラス」のインスタンスを作成
var data_1 = Data(1,"data")
var data_2 = Data(1,"data")

そして、

println("通常のクラスの結果:" + ndata_1.equals(ndata_2))
println("データクラスの結果:" + data_1.equals(data_2))

を実行してみると、

通常のクラスの結果:false
データクラスの結果:true

のように異なる結果となります。

「通常のクラス」では、「異なるインスタンス」を比較しているため、「false」となります。

「データクラス」では、「プライマリーコンストラクタの引数」が同値かを比較しているため、「true」となります。

hashCode

「通常のクラス」では、「インスタンス」を元にハッシュコードを出力しますが、「データクラス」では「プロパティの値」を元にハッシュコードを出力します。

println("通常のクラスの結果(ndata_1):" + ndata_1.hashCode())
println("通常のクラスの結果(ndata_2):" + ndata_2.hashCode())
println("データクラスの結果(data_1):" + data_1.hashCode())
println("データクラスの結果(data_2):" + data_2.hashCode())

のようにプログラムを実行してみると、

通常のクラスの結果(ndata_1):500977346
通常のクラスの結果(ndata_2):20132171
データクラスの結果(data_1):3076041
データクラスの結果(data_2):3076041

のようになり、データクラスでは、全く同じプロパティの値を持つインスタンスは、ハッシュコードは同じ値になります。

toString

「toString」は「オブジェクトの文字列表現」を返します。

「通常のクラス」では、オブジェクトを表す値を表示しますが、「データクラス」では、「プロパティ値」を表示します。

println("通常のクラスの結果(ndata_1):" + ndata_1.toString())
println("通常のクラスの結果(ndata_2):" + ndata_2.toString())
println("データクラスの結果(data_1):" + data_1.toString())
println("データクラスの結果(data_2):" + data_2.toString())

のようなプログラムを作成して実行すると、

通常のクラスの結果(ndata_1):TestKt$main$NormalData@1ddc4ec2
通常のクラスの結果(ndata_2):TestKt$main$NormalData@133314b
データクラスの結果(data_1):Data(id=1, contents=data)
データクラスの結果(data_2):Data(id=1, contents=data)

のように出力されます。

component

「データクラス」のプロパティを取得できる「component」メソッドですが、

//1つ目のプロパティの取得
component1()

//2つ目のプロパティの取得
component2()

のように、「component」の末尾に取得したいプロパティの番号を指定して取得します。

println(data_1.component1())
println(data_1.component2())

のようにプログラムを作成すると、

1
data

のように出力されます。

Copy

「インスタンスの複製」が作成できるのが「Copy」メソッドです。

var copy_data_1 = data_1.copy()

copy_data_1.id = 2
copy_data_1.contents = "data2"

println(data_1)
println(copy_data_1)

のようにプログラムを作成して実行してみると、

Data(id=1, contents=data)
Data(id=2, contents=data2)

のように出力され、別のインスタンスとなっているのが確認できます。

「データクラス」は、「プライマリーコンストラクタ」に必ず1つ以上引数を持つことが必要となり、「var」または「val」を付加する必要があります。

また、

  • open
  • inner
  • abstract

は利用できません。

「列挙型」の定義の方法は、下記のようになります。

enum class 列挙型名 {
    定数1,
    定数2,
    定数3
}

プログラムを作ってみると、

enum class Test {
    DATA1,
    DATA2,
    DATA3
}

のようになります。

列挙型の値を利用する時は、

列挙型名.値

と書きます。


ここまで「Kotlin」の構文について見てきましたが、「kotlin」には他にも、

  • ジェネリック
  • 拡張関数
  • ネストクラス

などの応用的な使い方もあります。

少しずつ、「kotlinの基礎」を習得していき、プログラムの書き方が身についてきたら、応用的な構文についても学んでみてはいかがでしょうか。

HOMEへ