KotlinとScalaの比較
1. 序章
このチュートリアルでは、Java以外のJVMで人気のあるプログラミング言語の2つであるScalaとKotlinの主な特徴と機能を調べます。
その過程で、それらが互いにどのように比較され、どのような状況でJavaよりも言語として適しているかについても理解します。
2. 主な特徴
まず、ScalaとKotlinの主な特徴を見ていきましょう。 これは、チュートリアルの残りの部分でのディスカッションのコースを確立するのに役立ちます。
2.1. Scala
Scalaの設計は、2001年にMartinOderskyによってEPFLで始まりました。 の名前自体は、「スケーラブル」と「言語」という単語のかばん語です。 当時、Javaは急速に進歩しましたが、下位互換性を維持する必要があるため、非常に制約がありました。 これにより、FunctionalNetsと呼ばれる結合計算のオブジェクト指向バージョンに基づくアカデミックプロジェクトFunnelが生まれました。
ただし、Funnelの使用はあまり実用的ではなく、最終的にScalaの開発につながりました。 Scalaは、より優れたJavaを作成するように設計されており、Javaインフラストラクチャ、JVM、およびそのライブラリに接続されています。 要約すると、Scalaはオブジェクト指向プログラミングと関数型プログラミングの両方をサポートする汎用プログラミング言語です。
ScalaソースコードはJavaバイトコードにコンパイルされるため、任意のJVMで実行できます。 また、Javaとの完全な相互運用性を提供するため、ScalaからJavaを参照したり、その逆を簡単に行うことができます。 Scala は、Javaにはないいくつかの機能を追加します。 これらは、 Scheme 、 Standard ML 、Haskellなどの他のいくつかの言語に触発されています。
2.2. Kotlin
Kotlinは比較的新しいプログラミング言語であり、2011年にJetBrainsによって開始されました。 JetBrainsは、人気のあるIDEであるIntelliJIDEAを開発したのと同じ会社です。 Kotlinは、Scalaと非常によく似た設計目標を持っていますが、Scala の欠点を改善することを目標としていました。特に、Scalaのコンパイル時間が遅いことです。 にちなんで名付けられました
本質的に、Kotlinはマルチパラダイムのオブジェクト指向プログラミング言語であり、Javaよりも優れた言語になることを目指しています。 ただし、Javaとの完全な相互運用性を維持しているため、Java開発者コミュニティ間で段階的に採用することができます。 この原因を解決するために、Goggleは2019年に、KotlinをAndroidプラットフォームの優先言語として発表しました。
Kotlin は、マルチプラットフォームプログラミングもサポートしています。これは、その主要な利点の1つであることが証明される可能性があります。 プライマリターゲットプラットフォームとしてJVMを使用して開始しましたが、JSやネイティブプラットフォームなどの他のプラットフォーム用にKotlinソースを非常に簡単にコンパイルできるようになりました。 Kotlin自体は非常に軽量な言語であり、拡張ライブラリを介したシリアル化や同時実行などの高度な機能のほとんどを提供します。
3. 型システム
型システムは、あらゆるプログラミング言語の特徴の1つです。 基本的には、変数、関数、式などのさまざまな言語構造に型を割り当てるための一連のルールです。 他の目的の中でも、プログラムのタイプエラーを回避するのに役立ちます。 大まかに言えば、型システムは、型チェックがコンパイル時または実行時にそれぞれ発生するかどうかに応じて、静的または動的になります。
さらに、プログラミング言語の型システムの重要な側面の1つは、パラメーター化された型の作成のサポートです。 基本的に、これにより、型をパラメーターとして受け入れるクラスや関数などの汎用構造を定義できます。 コンパイル時のより強力な型チェックをサポートすることは別として、それは私たちが一般的なアルゴリズムを書くのを助けます。
また、より複雑なタイプ間のサブタイピングがそれらのコンポーネント間のサブタイピングとどのように関連しているかを理解することも重要です。 この相関関係を分散として知っています。 コンポーネント間のサブタイピング関係は、それぞれの複合型で無視、保持、または逆にすることができます。 これにより、それぞれ不変、共変性、反変性の関係が生じます。
3.1. Scala型システム
ScalaはJavaのような静的に型付けされた言語ですが、最も洗練された包括的な型システムの1つを備えています。 Javaの欠点を改善する一方で、型システムで関数型プログラミング要素とオブジェクト指向プログラミングを組み合わせています。
Scalaには統一されたタイプ階層があり、Anyはすべてのタイプのスーパータイプです。 これには、値型を表すAnyValと参照型を表すAnyRefの2つの直接サブクラスがあります。
さらに、すべての参照タイプのサブタイプである Null と、すべてのタイプのサブタイプであるNothingがあります。
型パラメーター化を使用して、Scalaで汎用クラスを作成することもできます。 例を見てみましょう:
class Garage[+A]
ここでのGarageクラスはパラメーターとして型を受け取るため、任意の型でインスタンスを作成できます。
class Vehicle
class Car extends Vehicle
class Bike extends Vehicle
var myCarGarage = new Garage[Car]
var myBikeGarage = new Garage[Bike]
ただし、Scalaの汎用型のサブタイプはデフォルトで不変です。 簡単に言うと、次のステートメントは直感的ですが違法です。
val myGarage: Garage[Vehicle] = new Garage[Car] // Illegal statement
val myGarage: Garage[Car] = new Garage[Vehicle] // Illegal statement
Scala は、ジェネリッククラスの型パラメーターの分散アノテーションをサポートして、共変または反変にすることができます。
分散アノテーションを使用して汎用クラスを共変にする方法を見てみましょう。
class Garage[+A]
var myGarage: Garage[Vehicle] = new Garage[Car] // This is legal now
var myGarage: Garage[Car] = new Garage[Vehicle] // This is still illegal
同様に、別の分散アノテーションを使用して、この汎用クラスを反変にすることができます。
class Garage[-A]
var myGarage: Garage[Vehicle] = new Garage[Car] // This is still illegal
var myGarage: Garage[Car] = new Garage[Vehicle] // This is legal now
汎用クラスを定義するときに分散アノテーションを追加するため、この分散スタイルは宣言サイトの分散として認識されます。
さらに、型の境界を使用して、Scalaの型パラメーターを制約することができます。 境界は、型変数の具体的な値を制限します。
class Garage[T <: Vehicle] { }
class Garage[T >: Car] { }
ここでわかるように、タイプ境界は、上限タイプ境界または下限タイプ境界のいずれかです。 型の上限は、型変数TがVehicleのサブタイプになるように制限します。 逆に、型の下限は、型変数TがCarのスーパータイプになるように制限します。
3.2. Kotlin型システム
Kotlinは静的型付け言語でもありますが、Scalaに比べてはるかに単純な型システムを備えています。 Scalaと同様に、Kotlinにも統一型システムがあります。 ただし、Kotlinはnull許容型と非null許容型を明確に区別します。 null許容型または非null許容型として任意の型を定義できます。 Kotlinには、すべてのnull許容型のスーパータイプを表す Any と、すべてのnull許容型のスーパータイプを表す Any?があります。
null許容型は、対応するnull許容型のスーパータイプであることに注意してください。 さらに、Kotlinには特別なタイプとして Unit があり、Javaのvoidと同等のタイプがないことを表しています。 Scalaと同様に、Kotlinには Nothing があり、これは非終了を表します。
ScalaやJavaと同様に、パラメータとして型をとるKotlinの汎用クラスを定義できます:
class Garage<T>
これで、さまざまなタイプパラメータを使用してGarageクラスをインスタンス化できます。
open class Vehicle
class Car: Vehicle()
class Bike: Vehicle()
val myCarGarage = Garage<Car>()
val myBikeGarage = Garage<Bike>()
Scalaと同様に、genericクラスはKotlinではデフォルトで不変です。 ただし、Kotlinは、共変および反変のジェネリック型を定義するための宣言サイト分散アノテーションも提供します。
//Covariant Garage
class Garage<out T>
var myGarage: Garage<Vehicle> = Garage<Car>() //This is legal
var myGarage: Garage<Car> = Garage<Vehicle>() //This is illegal
//Contravariant Garage
class Garage<in T>
var myGarage: Garage<Vehicle> = Garage<Car>() //This is illegal
var myGarage: Garage<Car> = Garage<Vehicle>() //This is legal
ただし、Kotlin は、タイププロジェクションによる使用サイトの差異もサポートしています。 タイプパラメータに宣言サイトアノテーションを適用できるとは限りません。
class Garage<in T> {
fun park(t: T): T {return t}
}
ここで、型パラメーター T はプロデューサーとコンシューマーの両方として表示されるため、共変または反変にすることはできません。 ただし、使用場所の差異により、型安全性を得ることができます。
fun transfer(from: Garage<out Vehicle>, to: Garage<Vehicle>) { }
fun main() {
transfer(Garage<Car>(), Garage<Vehicle>()) // This is legal due to type projection
}
タイプ射影とは、基本的にの関数パラメーターが制限付きまたは射影型であることを意味します。
Kotlinは、型引数についてはわからないが、型セーフな方法で使用したい場合に備えて、スタープロジェクションも提供しています。
fun transfer(from: Garage<*>, to: Garage<Vehicle>) { }
fun main() {
transfer(Garage<Car>(), Garage<Vehicle>()) // This is legal due to star projection
}
最後に、Kotlinはが取ることができる値を制限するために、型パラメーターの上限の定義もサポートしています。
open class Vehicle
interface Electric
// Single Upper bound
class Garage<T: Vehicle> { }
// Multiple Upper bounds
class Garage<T> where T: Vehicle, T: Electric { }
ここで、ご覧のとおり、typeパラメーターに1つ以上の上限を定義できます。
4. オブジェクト指向プログラミングのサポート
オブジェクト指向プログラミング(OOP)は、オブジェクトを中心にソフトウェア設計を編成するプログラミングパラダイムです。 オブジェクトは基本的に、メソッドのプロパティと動作の形式でデータを含むタイプです。 オブジェクトの使用は、カプセル化、抽象化、継承、ポリモーフィズムなどの特定の原則によってさらに制限されます。
Javaは、当初から主にクラスベースのオブジェクト指向プログラミング言語でした。 オブジェクト指向プログラミングのすべての基本原則を基本的にサポートしています。 Javaのバックグラウンドから来たScalaとKotlinが、さらに優れたサポートを提供すると考えるのは当然のことです。
より成熟した言語であるScalaは、多くの追加機能をサポートしています。 暗黙の変換に基づく暗黙のクラスやパラメーターのように、それらのいくつかはさらに混乱します。 暗黙的な変換により、Scalaコンパイラーはいくつかの条件に基づいてあるタイプを別のタイプに自動的に変換できます。 ただし、これらの高度なトピックはスキップします。
4.1. ScalaでのOOPサポート
Javaと同様に、 Scalaはクラスベースのオブジェクト指向プログラミング言語であり、クラスはオブジェクトを作成するための青写真として機能します。 Scalaクラスには、メソッド、値、変数、タイプ、オブジェクト、特性、クラスなどのメンバーを含めることができます。
class Car(var make: String, var model: String) {
val description = s"($make, $model)"
}
プライマリコンストラクターに初期化コードを含めることはできません。 ただし、初期化コードはクラス本体のどこにでも表示できます。 プライマリコンストラクターとは別に、Scalaで任意の数の補助コンストラクターを定義することもできますが、それらはプライマリコンストラクターを呼び出す必要があります。
class Car(var make: String, var model: String) {
val description = s"($make, $model)"
def this(make: String) = {
this(make, "Unknown")
}
}
クラスのメンバーは、デフォルトでScalaで公開されています。 ただし、アクセス修飾子を使用して、クラス外からの可視性を制限できます。
class Car(var make: String, var model: String) {
private val _description = s"($make, $model)"
def description = _description
}
クラスの単一のインスタンスが必要な場合もありますが、その場合は、そのクラスを定義する必要はありません。 Scalaはこれのためにシングルトンオブジェクトを提供します:
object Logger {
def info(message: String): Unit = println(s"INFO: $message")
}
クラスと同じ名前のオブジェクトは、クラスのコンパニオンオブジェクトと呼ばれます。 これは、Javaクラスで静的メンバーを定義することに似ています。 コンパニオンオブジェクトは、そのコンパニオンクラスのプライベートメンバーにアクセスでき、その逆も可能です。
class Car(var make: String, var model: String) {
private val _description = s"($make, $model)"
import Car._
log(_description)
}
object Car {
private def log(message: String) = Logger.info(message)
}
抽象クラスまたは特性を使用してScalaで抽象化を作成できます。 特性は、Javaのインターフェースに類似しています。 どちらにも、抽象メソッドと非抽象メソッドを含めることができます。
abstract class Vehicle {
def accelerate: Unit
def decelerate: Unit
}
trait Electric {
def recharge: Unit
}
trait Insured {
def renew: Unit
}
ただし、抽象クラスとは異なり、クラス構成でミックスインを使用すると、特性を使用して多重継承を実現できます。
class Car extends Vehicle with Electric with Insured {
def accelerate = log("Accelerating")
def decelerate = log("decelerating")
def recharge = log("Recharging")
def renew = log("Renewing")
}
抽象クラスまたはscalaのトレイトは、他の抽象クラスまたはトレイトを拡張できます。
ここで、 Car は、ElectricとInsuredの両方の特性を混在させる必要はありません。 ただし、ある特性を別の特性と混合する必要があることを宣言する方法があります。
trait Electric {
this: Insured =>
def recharge: Unit
}
これはScalaではセルフタイプと呼ばれ、Electricの特性とInsuredの特性を混合するように強制されます。
4.2. KotlinでのOOPサポート
Kotlin は、クラスベースのオブジェクト指向プログラミングもサポートしています。 オプションのプライマリコンストラクターを使用してクラスを作成できますが、は初期化コードを初期化ブロックにのみ追加できます。
class Car(var make: String, var model: String) {
val description = "($make, $model)"
init {
print(description)
}
}
Kotlinでセカンダリコンストラクターを定義することもできますが、プライマリコンストラクターに委任する必要があります。
class Car(val make: String, val model: String) {
val description = "($make, $model)"
constructor(make:String): this(make, "Unknown") {
print(description)
}
}
Kotlinのデフォルトの可視性修飾子はpublicですが、 private 、 protected 、またはinternalに変更することもできます。 プロパティのゲッターとセッターはKotlinで自動的に生成され、ゲッターは対応するプロパティと同じ可視性を持ちます。
class Car(val make: String, val model: String) {
private val description = "($make, $model)"
}
クラスはデフォルトでKotlinのfinalであり、これは継承できないことを意味します。 ただし、クラスに継承のために開いていると注釈を付けることができます。
open class Vehicle
class Bike: Vehicle()
class Car: Vehicle()
Scalaと同様に、Kotlinでシングルトンオブジェクトを宣言するオプションがあります。 シングルトンオブジェクトもスーパータイプを持つことができます:
object Logger {
fun info(message: String) {println("INFO: $message")}
}
クラス内で宣言されたオブジェクトをコンパニオンとしてマークできます。 これにより、クラス名を修飾子として使用して、コンパニオンオブジェクトのメンバーを呼び出すことができます。
class Bike: Vehicle(){
override fun accelerate() {log("Accelerating")}
override fun decelerate() {log("Decelerating")}
companion object {
private fun log(message: String) {Logger.info(message)}
}
}
Kotlinには、クラスを継承せずに新しい機能でクラスを拡張するオプションも用意されています。 これは、拡張機能を介して可能です。
fun Bike.start() {
Logger.info("Starting")
}
val bike:Bike = Bike()
bike.start()
JavaやScalaと同様に、抽象クラスとインターフェースを使用してKotlinで抽象化を作成できます。 インターフェースは、状態を格納できないため、抽象クラスとは少し異なります。
abstract class Vehicle {
abstract fun accelerate()
abstract fun decelerate()
}
interface Electric {
fun recharge()
}
interface Insured {
fun renew()
}
Scalaの場合と同様に、クラスは単一のクラスまたは抽象クラスからのみ継承できますが、複数のインターフェースから継承できます。
class Bike: Vehicle(), Electric, Insured {
override fun accelerate() {println("Accelerating")}
override fun decelerate() {println("Decelerating")}
override fun recharge() {println("Recharge")}
override fun renew() {println("Renew")}
}
Kotlinの機能インターフェースは、単一の抽象メソッドでインターフェースをマークできます。 利点として、ラムダ式を使用してインスタンス化できます。
fun interface Insured {
fun renew()
}
val insured = Insured({println("Renewing")})
最後に、Kotlin は、継承の代替として委任をネイティブでサポートしています。
class BaseInsured: Insured {
override fun renew(){Logger.info("Renewing")}
}
class Bus(insured: Insured) : Vehicle(), Insured by insured {
override fun accelerate() {Logger.info("Accelerating")}
override fun decelerate() {Logger.info("Decelerating")}
}
val bus:Bus = Bus(BaseInsured())
ここで、クラス Bus は、すべてのパブリックメンバー Insured を、それ自体を実装するのではなく、BaseInsuredのオブジェクトに委任しています。
5. 関数型プログラミングのサポート
関数型プログラミングは基本的に宣言型プログラミングパラダイムであり、関数を適用して構成することによってプログラムを構築します。 関数とは、値を他の値にマップする式のツリーを意味します。 関数型プログラミングでは、関数を第一級市民として扱います。 したがって、他のデータ型と同じように扱うことができるはずです。
前に見たように、Javaはオブジェクト指向言語として始まり、関数型プログラミングをネイティブでサポートすることはありませんでした。 Javaで関数型プログラミングを行うことは常に可能でしたが、それは確かに簡単ではありませんでした。 ただし、Java 8以降、Java8を簡単にする多くの機能があります。 当然、関数型プログラミングのサポートは、ScalaとKotlinではるかに優れているはずです。
関数型プログラミングには、純粋関数、高階関数、カリー化、反復、モナドなどの多くの手法が含まれています。 通常、これらの多くは、ネイティブでサポートされていない場合でも、任意の汎用プログラミング言語で実装できます。 ただし、ここでは、ScalaまたはKotlinである程度のネイティブサポートを備えた手法に限定して説明します。
5.1. Scalaでの関数型プログラミングのサポート
そもそも、関数はScala の第一級市民であり、多くの関数型プログラミングイディオムを扱いやすくしています。 例を見てみましょう:
val myFunction = (x:Int) => x*2
myfunction(2) // 4
ここでは、他のデータ型と同じように関数を作成できます。
高階関数は、他の関数をパラメーターとして受け取るか、結果として関数を返す関数です。 これは関数型プログラミングの便利なテクニックです。
def handle(number: Int, function: Int => Int): Int = function(number)
handle(4, myFunction) // 8
ここでは、別の関数をパラメーターとして受け取る高階関数を定義しました。
また、Scala を使用すると、関数を別の関数内にネストできます。 これには、関数型プログラミングで役立つアプリケーションがいくつかあります。 入れ子関数を使用してn番目のフィボナッチ数を取得する方法を見てみましょう。
def fibonacci(x: Int): Int = {
def next(x: Int, y: Int, current: Int, target: Int): Int = {
if (current == target) y
else next(y, x + y, current + 1, target)
}
if(x==1) 0 else if(x==2) 1 else next(0, 1, 2, x)
}
<code class="language-kotlin">println(fibonacci(10)) // 34
Scalaはまた、メソッドが複数のパラメーターリストを取得できるようにします。これは、関数型プログラミングではカリー化としても知られている手法です。 この機能のユースケースの1つは、部分適用です。
def weight(gravity: Double) = (mass: Double) => mass*gravity;
val weightOnEarth = weight(9.81)
println(weight(9.81)(80.0))
println(weightOnEarth(80.0))
ここでは、惑星の重量を計算するために2つのパラメーターを受け取る関数を定義しました。 ただし、部分適用を使用して別の関数を生成する単一のパラメーターを渡すことができます。
関数型プログラミングのもう1つの重要な原則は、不変性です。 簡単に言えば、不変型を作成した後でその状態を変更することはできません。 Scala は、ケースクラスの形式で不変データをモデル化するためのネイティブサポートを備えています。
case class Planet(gravity: Double)
val earth = Planet(9.81)
earth.gravity = 10 //Illegal, can you imagine what might happen if gravity changes!
ケースクラスのパラメーターはデフォルトで値です。つまり、再割り当てすることはできません。
関数型プログラミングのもう1つの便利な手法は、関数が独自のコード内から自分自身を呼び出す再帰です。 再帰呼び出しが発生するに応じて、ヘッド再帰または末尾再帰のいずれかになります。
現在、末尾再帰は、末尾呼び出し最適化(TCO)と呼ばれるメモリ最適化手法から大きな恩恵を受けることができます。
import scala.annotation.tailrec
def factorial(x: Int): Int = {
@tailrec
def next(x: Int, result: Int): Int = {
if(x == 1) result
else next(x-1, result*x)
}
next(x, 1)
}
Scala は、コンパイル時の末尾呼び出しの最適化を実行し、実際には再帰呼び出しを単一の呼び出しに置き換えます。
5.2. Kotlinでの関数型プログラミングのサポート
Scalaと同様に、Kotlin も関数を第一級市民として扱います。他のデータ型と同じように、関数を作成して使用できます。
val myFunction = {x: Int -> x*2}
myFunction(2) // 4
Kotlin は、関数を表す一連の関数タイプと、それらを使用するラムダ式などの特殊な言語構造によってこれを実現します。
この関数を使用して高階関数を作成する方法を見てみましょう。
fun handle(number: Int, function: (Int) -> Int): Int = function(number)
handle(4, myFunction) // 8
通常、各関数はオブジェクトであり、クロージャをさらにキャプチャする可能性があるため、メモリ割り当てに関する実行時のオーバーヘッドは高くなります。 ただし、ラムダ式をインライン化することで、このオーバーヘッドを排除できます。
inline fun handle(number: Int, function: (Int) -> Int): Int = function(number)
inline 修飾子は、Kotlinコンパイラーに関数と呼び出しサイトで渡されるラムダをインライン化するように指示します。
Scalaと同様に、Kotlin では、関数を別の関数内にネストすることもできます。
fun fibonacci(x: Int): Int {
fun next(x: Int, y: Int, current: Int, target: Int): Int {
return if (current == target) y
else next(y, x + y, current + 1, target)
}
return if (x == 1) 0 else if (x == 2) 1 else next(0, 1, 2, x)
}
println(fibonacci(10)) // 34
Kotlinでのカレー関数の定義も、Scalaで行ったものと非常によく似ています。
fun weight(gravity: Double) = {mass: Double -> mass*gravity}
val weightOnEarth = weight(9.81)
println(weight(9.81)(80.0))
println(weightOnEarth(80.0))
データのみを保持するクラスの場合、Kotlinはデータクラスを作成するオプションを提供します。 これを使用する利点は、ボイラープレートコードがはるかに少ないことです。 さらに、データクラスのプロパティを値として宣言して、データを不変にすることができます:
data class Planet(val gravity: Double)
val earth = Planet(9.81)
earth.gravity = 10 // Illegal assignment
最後に、Kotlin は、末尾再帰の末尾呼び出しの最適化もサポートしています。 関数に特別な修飾子をマークする必要があります。
fun factorial(x: Int): Int {
tailrec fun next(x: Int, result: Int): Int {
return if (x == 1) result
else next(x - 1, result * x)
}
return next(x, 1)
}
Scalaと同様に、Kotlinはコンパイル時の最適化を実行し、再帰を高速で効率的なループベースのコードに置き換えます。
6. その他の言語機能
すでに説明した機能とは別に、説明する価値のある追加機能がいくつかあります。 これは、ScalaとKotlinの両方をJavaとは明確に区別するため、特に重要です。
6.1. ヌルセーフティ
Javaでは、nullと呼ばれる特別な値に任意の参照型を割り当てることができます。 これは、型に値がないことを示していますが、はJavaで多くの批判の源となっています。 一般に、 Tony Hoare によるプログラミングでのnull参照の発明は、「10億ドルの間違い」として説明されてきました。
Scalaは、すべての変数を初期化するように強制することでこれを改善しようとします:
val x: String = "Hello World!" // Legal statement
val x: String // Illegal statement, we must initialize
val x: String = _ // Legal statement, initializes with default
val x: String = null // Legal statement
変数をnullで初期化することもできますが、少なくともそれは意図的なものです。
Scalaは、特定のタイプの単一要素または要素なしのキャリアとして使用できるOptionクラスも提供します。 メソッドがnull値を返す可能性がある場合は、Optionを使用することをお勧めします。
def findCar(model: String): Option[Car] = {
val cars = Map("Ford" -> new Car("Ford"))
cars.get(model)
}
このメソッドを呼び出すと、 Some(Car)またはNoneのいずれかを受け取りますが、nullを受け取ることはありません。 これにより、強力なパターンマッチングを使用して、オプションの値を分離することもできます。
Kotlinはnull値をはるかに防御的に処理し、エラーの余地はほとんどありません。 デフォルトでは、変数が値nullを取ることはできません。
val x: String = "Hello World!" // Legal statement
val x: String // Illegal statement, we must initialize
val x: String = null // Illegal statement
ただし、は変数をnull として宣言できますが、前に見たように、Kotlinは型システムレベルでそれらを分離します。
val x: String? = null // This is legal statement
ただし、このは、これらのnull許容レシーバーを実際に使用する方法にいくつかの制限を課します。
println(x.length) // Illegal use of a nullable receiver
安全な呼び出しまたはnull以外のアサートされた呼び出しのいずれかで使用する必要があります。
println(x?.length) // Safe call, will not result into null pointer exception
println(x!!.length) // Non-null asserted call, will result into null pointer exception
これでもエラーの余地はありますが、非常に明確で明確になります。
6.2. パターンマッチング
パターンマッチングは、特定のトークンシーケンスをチェックして、パターンの構成要素が存在するかどうかを確認するプロセスです。 値をその部分に分解するなど、あらゆるプログラミング言語でいくつかの用途があります。 私たちはしばしば正規表現の助けを借りてそれを達成します。
Scalaは非常に成熟したパターンマッチングをサポートしています。 は、Javaのswitch式の強力なバリアントであるmatch式で頻繁に使用します。
def dayOfWeek(x: Int): String = x match {
case 1 => "Monday"
case 2 => "Tuesday"
case _ => "other"
}
ただし、ケースクラスと一緒に使用すると、パターンマッチングの真の力を実現できます。
abstract class Order
case class MailOrder(name: String, address: String) extends Order
case class WebOrder(name: String, email: String, phone: String) extends Order
def generateResponse(order: Order): String = {
order match {
case MailOrder(name, address) => s"Thank you $name, you will receive confirmation through post at: $address"
case WebOrder(name, email, _) => s"Thank you $name, you will receive confirmation through email at: $email"
}
}
ここでは、パターンマッチングを使用してタイプを識別し、値の破棄を実行できます。
パターンマッチングのサポートは、Scalaと比較してKotlinではそれほど洗練されていません。 Kotlinのwhen-blockを使用して、同様のことを実現できます。
fun dayOfWeek(x: Int): String {
return when (x) {
1 -> "Monday"
2 -> "Tuesday"
else -> "other"
}
}
もちろん、 when-blockはさまざまな方法で使用できますが、完全なパターンマッチングの代わりにはなりません。
6.3. 例外処理
一般に、例外処理とは、プログラミングの異常な状態に適切に対応するプロセスを指します。 Javaは、チェックされた例外またはチェックされていない例外のいずれかを定義する機能を提供します。 前者はコンパイル時にチェックされますが、後者はチェックされません。 ただし、チェックされた例外の存在は、かなり長い間Javaで批判を集めてきました。
Scalaは、未チェックの例外を宣言して使用するメカニズムを提供します。 ただし、Javaとは異なり、Scalaは例外をチェックしていません。 Scalaでチェックされた例外をどのように使用できるか見てみましょう:
def divide(x: Double, y: Double): Double = {
if (y == 0) throw IllegalArgumentException
x / y
}
def performDivision(x: Double, y: Double) = {
try {
divide(x, y)
} catch {
case ex: IllegalArgumentException =>
println(ex.getMessage)
} finally {
println("Performing cleanup")
}
}
これはかなり標準的ですが、 catch ブロックで使用できるパターンマッチングを除いて、単一のブロックで例外を処理できます。
同様に、 Kotlinでも未チェックの例外を使用できますが、チェック済みの例外はありません。 それが提供する機能は、パターンマッチングを除いて、Scalaとほとんど同じです。
fun divide(x: Double, y: Double): Double {
if (y == 0.0) throw IllegalArgumentException("Illegal argument")
return x / y
}
fun performDivision(x: Double, y: Double) {
try {
divide(x, y)
} catch (ex: IllegalArgumentException) {
println(ex.message)
} finally {
println("Performing cleanup")
}
}
さらに、Kotlinの式としてthrowsまたはtry-catchブロックを使用することもできます。
7. 結論
要約すると、このチュートリアルでは、Scalaプログラミング言語とKotlinプログラミング言語の機能比較を行いました。 これは、それらの型システム、オブジェクト指向プログラミングをサポートする機能、および関数型プログラミングをサポートする機能の比較をカバーしました。