3. そのほかのnull
最後に、比較的最近誕生した言語であるSwift [1]
のnil
とKotlin [2]のnull
について、
Java、C#と比較しながら考える。
また、C++のstd::optional
についても簡単に紹介する。
Swift 5
Swiftのnil
は安全である。まず、ローカル変数は宣言時に初期化を強制する。
つまり、未初期化の変数の心配は不要だ。
それから、
JavaのOptional<T>
同様、
選択型のOptional<T>
[3] 型があり、
そのインスタンスは不変オブジェクトである。
ただし、
Javaとは異なり、
SwiftではT?
という記法でも記述できる。
T?
はOptional<T>
のシンタックスシュガーでしかなく、 Javaでの参照型の値をjava.util.Optional<T>
でラップしたもの、 あるいはC#での値型の値をSystem.Nullable<T>
でラップしたものと同様に考えてよい。
ラップした値にアクセスする方法は複数用意されている。
無条件アンラップ
Optional<T>
型の式に強制アンラップ演算子を適用(式に!
を後置する)すると、
強制的に型T
の値をアンラップする。
ただし、値が存在しないと、ランタイムエラーとなる。
Swiftの
!
後置演算子は、 Javaのjava.util.Optional<T>
のメソッドget()
、 C#のSystem.Nullable<T>
のプロパティValue
に相当する操作を表す記法である。
選択連鎖
Optional<T>
型の式に選択連鎖演算子を適用(式に?
を後置)して、
ラップされた型T
のインスタンスのメソッド、プロパティのアクセス、
または添え字アクセスを実行する。
値が存在しない場合、何もアクセスされず、式はnil
となる。
メソッドの戻り値の型、プロパティの型、
または添え字アクセスの型がU
ならば、式の型はU?
になる。
戻り値がVoid
型のメソッドは()
、すなわち空のタプル、を返すと定義されている†1。
そのため、選択連鎖演算子で戻り値がVoid
型のメソッドを呼び出すと、
式の型はVoid?
、すなわち()?
になる。
さらに、プロパティに値をセットする式は、
戻り値がVoid
型のセッターメソッドを呼び出す式と同等なので、
選択連鎖演算子でプロパティに値を設定する式の型も()?
となる。
†1
Void
は()
の型エイリアス(type alias)である。
したがって、選択連鎖演算子のアクセスの結果をnil
と比較して、
アクセスできたかどうかを調べることができる。
公式ドキュメントで例証されているコードを次に引用する:
// printNumberOfRooms()は戻り値がVoid型のメソッド
if john.residence?.printNumberOfRooms() != nil {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
// プロパティaddressに値をセットする
if (john.residence?.address = someAddress) != nil {
print("It was possible to set the address.")
} else {
print("It was not possible to set the address.")
}
nil
合体演算子
nil合体演算子(??
)はOptional<T>
型の式に値が存在しないときのデフォルト値を提供する演算子である。例えば、expr1
が選択型のときexpr1 ?? expr2
は、
expr1
の値が存在すればその値に、存在しなければ式expr2
の値になる式である。
選択束縛
Optional<T>
のオブジェクトにラップされている値が存在するときに、
それを別の変数に取り出すフロー制御の記法が用意されている:
if let
guard let
switch
if let
はC#のis
パターンマッチングに似た感じで使用できる:
if let value = maybeNil {
// maybeNilがラップする値が存在した場合: このスコープでvalueはその値となる
...
} else {
// maybeNilがラップする値が存在しない場合
...
}
ところが選択型の値が複数あるときは、次のようにコードのネストが深くなりやすい†2:
if let value1 = maybeNil1 {
if let value2 = maybeNil2 {
if let value3 = maybeNil3 {
// value1, value2, value3を使うコードが続く
...
†2 ちなみにC#の
is
パターンマッチングも同様の傾向にあるが、 C#では変数のスコープが異なるので、if
の条件を反転させてSwiftのguard let
のように使うこともできる。 しかし、そのような使用例を積極的に紹介しないところをみると、 Microsoftは Early Exit [4] の考え方があまり好きではないのだろう。 実際、 C#のis
パターンマッチングの解説 [5] には、 次のような記載がある:The samples in this topic use the recommended construct where a pattern match
is
expression definitely assigns the match variable in thetrue
branch of theif
statement. You could reverse the logic by sayingif (!(shape is Square s))
and the variables
would be definitely assigned only in thefalse
branch. While this is valid C#, it is not recommended because it is more confusing to follow the logic.
これを解消するのがguard let
である。
次のように、必要な値のいずれかが存在しなかった時点でreturn
するような構造にコードを書ける:
guard let value1 = maybeNil1 else {
return
}
guard let value2 = maybeNil2 else {
return
}
guard let value3 = maybeNil3 else {
return
}
// value1, value2, value3を使うコードが続く
...
もちろん、return
以外のフロー制御も可能で、
例えば、ループ内であればbreak
やcontinue
を用いることもできる。
なお、guard
は必ずlet
と組み合わせて使う必要があるわけではない。
guard
ステートメントの条件式で代入された定数や変数は、
そのguard
ステートメントを含むスコープが閉じるまで有効なので、
nil
のチェック以外のガードでEarly Exitする際でも役に立つ。
最後にswitch
を用いる束縛だが、次のように
case let
の後に定数名と?
を指定する:
func printValue(_ maybeString: String?) {
switch maybeString {
case let value?:
// maybeStringは値が存在し、valueに代入済み
print("value: \(value)")
break
default:
// maybeStringはnil
print("no value")
break
}
}
printValue("foo")
printValue(nil)
出力は次のようになる:
value: foo
no value
また、
switch
にタプルを用いることで、複数の値を同時にチェックすることもできる:
func printValues(_ maybeInt1: Int?, _ maybeInt2: Int?) {
switch (maybeInt1, maybeInt2) {
case let (value1?, value2?):
print("values: (\(value1), \(value2))")
break
default:
print("one of the values is nil.")
break
}
}
printValues(2, 3)
printValues(4, nil)
printValues(nil, nil)
出力は次のようになる:
values: (2, 3)
one of the values is nil.
one of the values is nil.
標準ライブラリにおける選択型の整合性
Swiftの選択型は、JavaのOptional<T>
やC#のnull許容型と比べて明らかに優れている。
標準ライブラリの基本的な機能として最初から選択型が組み込まれているからだ。
例えば、Dictionary<K, V>
の添え字アクセス(subscript
)の戻り値はV?
型であるし、
Array<E>
のfirst
の戻り値はE?
型である。
理解を深めるため、
Sequence
プロトコルのcompactMap
について言及しておきたい。
compactMap
は引数に「シーケンスの要素を型T?
の値に変換する」クロージャをとり、
型T
の要素を含むシーケンスを生成する。
つまり、compactMap
はその引数のクロージャでシーケンスの要素を選択型のオブジェクトに変換後、
値をラップしていないものを除き、
さらに値をアンラップして取り出すことで、
型T
の要素だけのシーケンスを生成する。
この操作はnil
の除去と、型T?
からT
への変換の両方を含む。
重要なのは、変換後のシーケンスがnil
を含まない、
ということが静的解析的にはっきりすることだ。
対照的に、要素が型T?
のシーケンスにfilter
を適用してnil
の要素を除いても、
コンパイラは生成したシーケンスを要素が型T?
のものとみなしてしまう。
これをC#のLINQを用いて説明してみよう。
次のコードは参照型の要素のリスト、
ただし要素がnull
である可能性があるリスト、
を受け取り、
null
を含まないリストを生成して返す、つもりである:
public static IEnumerable<T> WhereNonNull<T>(this IEnumerable<T?> list)
where T : class
{
var newList = list.Where(e => e is {});
...
このようにWhere
を使って、
null
を含むlist
から、
null
を含まないnewList
を作ることができる。
一見すると、
これでIEnumerable<T>
のnewList
が手に入り、目的を達成したように思える。
しかし、実際にはnewList
の型はIEnumerable<T?>
である。
つまり、元々のlist
も生成したnewList
も要素の型は同じT?
である。
したがって、静的解析ではnewList
がnull
を含む可能性があるとみなしてしまう。
なお、C#でnull
の除去と、型T?
からT
の変換は、
次のようにOfType
メソッドを使うことでも実現できる:
public static IEnumerable<T> WhereNonNull<T>(this IEnumerable<T?> list)
where T : class
{
var newList = list.OfType<T>();
...
これを利用して、SwiftのcompactMap
をC#で模倣すると、次のようになる:
public static IEnumerable<U> CompactReferenceMap<T, U>(
this IEnumerable<T> list,
Func<T, U?> transform)
where U : class
{
return list.Select(e => transform(e))
.OfType<U>();
}
また、Javaで模倣すると次のようになる†3:
private static <T, U> List<U> compactMap(
List<T> list,
Function<T, Optional<U>> transform) {
return list.stream()
.map(e -> transform.apply(e))
.flatMap(o -> o.stream())
.collect(Collectors.toList());
}
†3 Java 9で
Optional
クラスに追加されたstream()
メソッドを使った。 APIリファレンスにも同様の説明がある。
Kotlin 1.3
Kotlinのnull
も、Swiftのnil
と同じように安全だ。
公式のリファレンスでnull安全性
[6]
についてまとめられているので、
その全貌についてはそちらを参照してほしい。
Swiftと大きく違う点は、T?
が選択型ではなく、null許容型であることだ。
null許容型は、C# 8のnull許容参照型と同様に、フェイク型である。
つまり、null許容型はコンパイラの静的解析によって実現されている。
Kotlinを発明したJetBrains社は、JavaのIDEであるIntelliJ IDEAの開発元でもある。
Java 11のパートで説明したように、
IntelliJ IDEAのコンパイラは@NotNull
/@Nullable
アノテーションをヒントにして、
データフロー解析でnullチェックが適切かどうか検証できる。
だから、その技術をKotlinとそのコンパイラに用いたのも不思議ではない。
プリミティブ型のnull許容型
ただし、プリミティブ型の値については注意が必要である。
例えば、
公式のドキュメントにも記載されているように、
Int?
型の値はボクシングされたInt
オブジェクトとなる。
これはJavaで、@Nullable Integer
は可能だが@Nullable int
は不可、
というのと同じである。
次のように、ボクシングされたプリミティブ型の値では、
値の等価性(equality)は保たれるが、
オブジェクトの同一性(identity)が保たれることは保証されない:
val a: Int = 10000
val boxedA: Int? = a
val anotherBoxedA: Int? = a
// Prints 'true'
println(boxedA == anotherBoxedA)
// Prints 'false'
println(boxedA === anotherBoxedA)
null許容型の演算子
デジャブ感があるが、null許容型に対して、次の演算子が用意されている:
.?
(safe call operator)?:
(Elvis operator)!!
(not-null assertion operator)
それぞれ、C#/Swiftの.?
演算子、??
演算子、!
後置演算子と同様の意味をもつ。
詳細については公式リファレンスを参照してもらうことにして、
興味深い点についてだけ紹介しておく。
.?
演算子はSwift同様に左辺値に適用できる。
また、.?
演算子とlet
関数を次のように組み合わせる†4ことで、
JavaのOptional<T>
のifPresent(Consumer)
や
map(Function)
と同様な処理を実現できる:
val item: String? = ...
item?.let { println(it) }
val length = item?.let { it.length }
†4 実際には
let
に限らず、 公式ドキュメントのスコープ関数にあるrun
、apply
、also
なども組み合わせることができる。
?:
演算子の右の項には、式の代わりにreturn
またはthrow
を指定できる。
[6] から例証を引用して次に示す:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
...
null許容型と集合
Array
クラスとIterable
インターフェースには、
SwiftのcompactMap
に相当する、
mapNotNull
メソッドがある。
さらに、
要素がnull許容型のコレクションから値の存在する要素だけを取り出すfilterNotNull
メソッドも用意されている。
Swiftと同様、 このようなAPIの存在が、 最初からnull許容型が存在していた言語の優れている点である。
プラットフォーム型
Javaとの相互運用性(interoperability)はKotlinの重要な特徴のひとつである。
しかし、
Kotlinから見ればJavaの参照型はすべてnull許容型なので、
何の対策も無しにJavaのAPIを利用することはnull安全性に脅威をもたらすことになる。
つまり、JavaのAPIを呼び出し、戻り値をすべてnull許容型として扱うと、
エラーまみれになり、!!
をひたすら追加することになる。
そうしているうちに、
本当に修正が必要なエラーは埋もれてしまい、
null安全性は崩壊する。
Kotlinの設計者は賢いので、
Javaからやって来る値を扱うために、
プラットフォーム型 [7] という特別な型を用意した。
といっても、それは銀の弾丸ではなく、
コンパイル時にnullに関するデータフロー解析をオフにするだけの型、
つまり暗黙に!!
演算子が適用されている型である。
これにより、Javaからのインスタンスのnullチェックを怠れば、
実行時にNPEがスローされる、というだけの問題になる。
[6] から引用した例証を次に示す:
// listはnull非許容型(コンストラクタの結果)
val list = ArrayList<String>()
list.add("Item")
// sizeはnull非許容型(プリミティブ型)
val size = list.size
// itemはプラットフォーム型(Javaのオブジェクト)
val item = list[0]
// コンパイルは成功するが、実行時にitemがnullなら例外をスロー
item.substring(1)
// 何の問題もない
val nullable: String? = item
// コンパイルは成功するが、実行時に失敗するかもしれない
val notNull: String = item
このように、Javaからの値がすべてプラットフォーム型になるわけではなく、
コンストラクタの結果やプリミティブ型の値などnull
でないことが自明なものはnull非許容型になる。
プラットフォーム型の値は速やかにnull許容型、
またはnull
でないことが明白ならnull非許容型、
の変数に代入して扱うべきだろう。
プラットフォーム型は記述のための記法をもたない。
しかし、コンパイラがエラーなどで型の説明をするための表記法だけは用意されていて、
「T
またはT?
」という意味のプラットフォーム型をT!
として表示する。
表記例を [6] から次に引用しておく:
(Mutable)Collection<T>!
Array<(out) T>!
前者は「要素の型がT
の、可変または不変のJavaの集合」の参照またはnull
、
後者は「要素の型がT
またはT
のサブタイプであるJavaの配列」の参照またはnull
を表す。
なお、Kotlinのコンパイラは、
Javaのパートで説明したnull
にまつわるアノテーションを理解するので、
Kotlinから参照するJavaのAPIを@NotNull
と@Nullable
でアノテーションしておけば、
Javaのオブジェクトがプラットフォーム型になることを回避できる。
C++17
C++はC++11でnullptr
キーワード、C++17でstd::optional
クラスが導入された。
std::optional
は、
JavaのOptional
と同様の課題を解決するためのものだ。
C/C++では、配列はオブジェクトではない。したがって、
Javaのパートで説明した「null
の代わりに長さ0または1の配列を返す」ようなことをモノマネすることはできない。
もちろん、理屈の上では配列の代わりにstd::vector
などで同様なことを実現できるものの、
C++を使う動機の多くは、そのようなオーバーヘッドを許容しないため、だからだ。
C++の標準はstd::optional
の実装が(値を格納するために)動的にメモリを割り当てることを禁止†5している。
パフォーマンスを理由に選択型の採用を拒絶する輩への対策は、
標準化委員会がやっておいてくれた。
†5 誤解しないように念のため補足しておく。
std::optional<T>
型のオブジェクトは、 生成された時点で型T
の値を格納するためのメモリ領域を事前に確保しておく。 だから、std::optional<T>
型のオブジェクトに型T
の値を格納するときに、 動的なメモリ割り当ては発生しない、という意味である。 典型的な実装は、長さsizeof(T)
のバイト配列を確保しておいて、 プレイスメントnew
でそこに値を格納する。 そして、このことから分かるように、JavaやSwiftとは異なり、T
の派生型の値を格納することはできない。
オブジェクトの生成
値をもつstd::optional<int>
型のオブジェクトの宣言の例を示す:
std::optional<int> v1(123);
std::optional<int> v2 {{123}};
std::optional<int> v3 = 123;
auto v4 = std::optional<int>(123);
auto v5 = std::make_optional<int>(123);
どれでも同じ結果になる。同様に、値をもたない宣言の例を示す:
std::optional<int> n1;
std::optional<int> n2 {};
std::optional<int> n3 = std::nullopt;
auto n4 = std::optional<int>();
同じく、どれも同じ結果になる。
値の有無の確認と取り出し
他の言語の選択型、null許容型に比べると、
C++17のstd::optional
でできることは少ない。
std::optional
には、
JavaのOptional
のifPresent(Consumer)
メソッド、
map(Function)
メソッドのような、
ラムダ式を受け取る操作がない。
そもそも、
C++は現在のところ標準ライブラリにリスト内包表記(list comprehension)[8]
のAPIがない。
だから、そのようなものがstd::optional
だけにあったところで、
使い勝手が劇的に良くなることはないだろう。
ただし、次のようなプロポーザルが出ているので、 将来的には他の言語でできることができるくらいに機能が追加されるかもしれない:
std::optional
の値の有無は、bool
型の値を返すhas_value()
メンバ関数で取得できる。
しかし、この関数を使う必要はない。
std::optional
はoperator bool
(bool
型への暗黙的な型変換)を定義しているので、
次のようにインスタンスをif
などの条件式にそのまま指定できる:
std::optional<int> maybeInt = ...;
if (maybeInt) {
// maybeInt.has_value()がtrue、すなわち値が存在する場合
...
} else {
// maybeInt.has_value()がfalse、すなわち値が存在しない場合
...
}
値の取り出しには、operator *
もしくはvalue()
メンバ関数を使う。
これらの結果が異なるのは、値が存在しない場合だけである。
その場合、前者は未定義の振る舞い、
後者は例外std::bad_optional_access
のスローとなる。
値が存在する場合は、operator ->
を使って値のメンバにアクセスすることもできる:
std::optional<std::string> maybeString = ...;
if (maybeString) {
// auto &s = *maybeString;
// auto size = s.size();
// と書くのと、次の行は同じ:
auto size = maybeString->size();
...
}
ただし、operator *
同様、値が存在しないときは未定義の振る舞いとなる。
value_or(T)
メンバ関数は、値が存在するときはその値、そうでなければ引数の値を返す:
std::optional<std::string> maybeString = ...;
auto s = maybeString.value_or(defaultValue);
遅延初期化
興味深いことに、
JavaのOptional
、C#のNullable
、SwiftのOptional
とは異なり、
C++のstd::optional
のインスタンスは不変オブジェクトではない。
値の有り、無しの状態を変更すること、
そして値有りの状態のままで値を別のものに変更することもできる。
値無しから値有りへの変更を用いて、
遅延初期化(lazy initialization)を実現できる
(#7「Immutable Object」の遅延評価、
#12「Javaのメモリモデル」の遅延初期化を参照)。
std::optional<T>
のオブジェクトを、 値無しから値有りに状態を変える、または値を別のものに変更するには、emplace(...)
メンバ関数を呼び出す、operator =
でT
型のオブジェクトを代入する、 または値有りのstd::optional
オブジェクトを代入する、 などの操作がある。 また逆に、値有りから値無しに状態を変えるにはreset()
メンバ関数を呼び出す、operator =
でstd::nullopt
を代入する、 などの操作がある。
遅延初期化の例として、計算式を表す文字列をコンストラクタで受け取り、
getValue()
で評価した値を返すクラスCalculator
を考えてみよう。
次のような使用例を想定する:
int main() {
Calculator c("(8 * 7 + 6) / 4");
std::cout << c.getValue() << std::endl;
}
計算式の評価を初回のgetValue()
の呼び出しまで遅延するクラスCalculator
の実装例を次に示す:
class Calculator {
public:
Calculator(std::string expr) : expr(expr) {
}
int getValue() {
if (!value) {
value.emplace(evalExpr());
}
return *value;
}
private:
std::string expr;
std::optional<int> value;
// exprを評価して返す
int evalExpr() {
return ...
}
};
もう少し実用的な例を示す。
クラスFoo
が、クラスBar
型のメンバbar
を持ちたいが、
Bar
クラスにはデフォルトコンストラクタが無い場合を考える。
ただし、Foo
のコンストラクタではbar
を初期化できず、
bar
を遅延初期化せざる得ないとする。
C++17より前では、次のようにstd::unique_ptr
を使って解決することができる:
class Foo {
public:
Foo() {
...
}
void initialize() {
bar = std::make_unique<Bar>(...);
}
private:
std::unique_ptr<Bar> bar;
};
しかし、次のようにstd::optional
を使えば、
動的なメモリ割り当てを用いずに、遅延初期化を実現できる:
class Foo {
public:
Foo() {
...
}
void initialize() {
bar.emplace(...);
}
private:
std::optional<Bar> bar;
};
未定義の振る舞いから例外のスローへ
C++でこのstd::optional
を使う最大の魅力は例外std::bad_optional_access
のスローである。
例えば、あるAPIがstd::optional
を使わず、nullptr
を返すとしよう。
戻り値がnullptr
なのにnullチェックを忘れてアクセスしたら、
未定義の振る舞いになる。
そうではなく、そのAPIがstd::optional
を返すのであれば、
値の有無をチェックせずにvalue
でアクセスしても、
所詮例外のスローで済む。この違いは大きい。
しかしながら、nullptr
またはNULL
を返す過去の資産があるため、
これから新規に作成するAPIだけにstd::optional
を用いても、
焼け石に水なのだろう(長い時間をかけて変わっていく可能性もあるだろうけど...)。
C++ Core Guidelinesのgsl::not_null
クラス
標準ライブラリの話ではないが、
C++ Core Guidelines [9]
のgsl::not_null
クラスを非nullのポインタを扱うための仕掛けとして使うことができる。
以降ではMicrosoft実装のGuidelines Support Library(GSL)[10]
を取り扱う。
スマートポインタの場合と異なり、
T
を何かのポインタ型(すなわちU *
)とするか、
もしくはスマートポインタ型†6とするようにgsl::not_null<T>
は設計されている。
例えば、非nullのconst char *
型の引数をとる関数を次のように記述することができる:
std::size_t length(gsl::not_null<const char *> s)
{
return std::strlen(s);
}
同様に、非nullのstd::shared_ptr<U>
型であれば、
gsl::not_null<std::shared_ptr<U>>
と記述できる。
†6 スマートポインタ型といっても、標準ライブラリでは
std::shared_ptr
またはstd::unique_ptr
しか使えず、std::weak_ptr
は使えない。というのは、T
はその値とnullptr
が比較可能(T
型の値v
に対して式v != nullptr
の評価が可能)でなければならないが、std::weak_ptr
はその条件を満たさないからである。 その他にも、T
に対して単項演算子*
が適用可能である、 などの条件がある。もちろん、そうした条件を満たせば適当なクラスをT
に指定できる。
T
型の値からgsl::not_null<T>
を構築できるので、次のように関数length
を呼び出すことができる:
auto n = length("hello");
しかし、次のようにnullポインタ定数を引数とする呼び出しはコンパイルエラーとなる:
auto n = length(nullptr);
実行時、T
型の値からgsl::not_null<T>
を構築する際、
その値がnullと等しければstd::terminate()
を呼び出して終了する。
したがって、T
がU *
(または「std::shared_ptr<U>
」など)であれば、
構築後はそのポインタが非nullであることが確実になる。
構築時に値が非nullであっても、取得するときには値がnullになり得るような、 自作のクラスを
T
に指定する場合は注意が必要である。gsl::not_null<T>
から値を取得しようとすると、T
の単項演算子*
で「ポインタをたどって」値を取得する前に、 そのT
型の値とnullptr
との比較(つまりnullチェック)を実行する。 そして、それがnullptr
とイコールであれば、 構築時同様に、std::terminate()
を呼び出して終了する。
構築後はgsl::not_null<T>
のget()
関数でT
型の値(U
のポインタ型)を取得できるほか、
スマートポインタと同様、単項演算子*
と->
でのアクセスも可能である。また、
gsl::not_null<T>
からT
(null非許容からnull許容)への暗黙の型変換が許されているので、
上の例のstd::strlen()
の呼び出しのように、
T
型を期待しているところにgsl::not_null<T>
型の値を指定してよい。
gsl::not_null<T>
を用いることで、
大昔によく見かけた、次のような関数の先頭での引数のnullチェック:
void foo(const void *p)
{
assert(p != nullptr);
...
}
は多くの場合、不要になる。
ただし、++s
などのポインタ演算や、
s[1]
などのような添え字アクセスは許されていないので、
必要であれば別のT
型の変数に代入してから操作するしかない。
ポインタは(参照のように)単一のオブジェクトを指すべき、という思想のようだ。
References
-
Apple, Swift Standard Library, Numbers and Basic Values, Optional
-
Apple, The Swift Programming Language, Language Guide, Control Flow
-
Kotlin Foundation, Kotlin Programming Language, Language Guide, Null Safety
-
Kotlin Foundation, Kotlin Programming Language, Language Guide, Calling Java code from Kotlin