この記事は Florina Muntenescu による Android Developers - Medium の記事 "Don’t argue with default arguments" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
短くて使いやすいデフォルト引数を利用すると、ボイラープレートを書くことなく関数のオーバーロードを実現できます。多くの Kotlin の機能と同じように、この機能も魔法のように便利です。その秘密を知りたいと思いませんか?この記事では、デフォルト引数の内部の仕組みを紹介します。
関数のオーバーロードが必要な場合、同じ関数を複数回実装する代わりに、デフォルト引数を使うことができます。
<!-- Copyright 2019 Google LLC. SPDX-License-Identifier: Apache-2.0 -->// instead of:fun play(toy: Toy){ ... }fun play(){ play(SqueakyToy)} // use default arguments: fun play(toy: Toy = SqueakyToy)fun startPlaying() { play(toy = Stick) play() // toy = SqueakyToy}
<!-- Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 -->
// instead of:
fun play(toy: Toy){ ... }
fun play(){
play(SqueakyToy)
}
// use default arguments:
fun play(toy: Toy = SqueakyToy)
fun startPlaying() {
play(toy = Stick)
play() // toy = SqueakyToy
デフォルト引数はコンストラクタにも適用できます。
<!-- Copyright 2019 Google LLC. SPDX-License-Identifier: Apache-2.0 -->class Doggo( val name: String, val rating: Int = 11)val goodDoggo = Doggo(name = "Tofu")val veryGoodDoggo = Doggo(name = "Tofu", rating = 12)
class Doggo(
val name: String,
val rating: Int = 11
)
val goodDoggo = Doggo(name = "Tofu")
val veryGoodDoggo = Doggo(name = "Tofu", rating = 12)
デフォルトでは、Java はデフォルト値のオーバーロードを認識しません。
<!-- Copyright 2019 Google LLC. SPDX-License-Identifier: Apache-2.0 --> // kotlinfun play(toy: Toy = SqueakyToy) {... }// javaDoggoKt.play(DoggoKt.getSqueakyToy());DoggoKt.play(); // error: Cannot resolve method 'play()'
// kotlin
fun play(toy: Toy = SqueakyToy) {... }
// java
DoggoKt.play(DoggoKt.getSqueakyToy());
DoggoKt.play(); // error: Cannot resolve method 'play()'
コンパイラにオーバーロード メソッドを生成するよう指示するには、Kotlin 関数に @JvmOverloads アノテーションを追加します。
@JvmOverloads
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */@JvmOverloadsfun play(toy: Toy = SqueakyToy) {… }
/* Copyright 2020 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
fun play(toy: Toy = SqueakyToy) {… }
コンパイラが生成した内容を確認するため、逆コンパイルした Java コードを見てみましょう。[Tools] -> [Kotlin] -> [Show Kotlin Bytecode] を選択し、[Decompile] ボタンを押します。
[Tools] -> [Kotlin] -> [Show Kotlin Bytecode]
[Decompile]
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */fun play(toy: Toy = SqueakyToy)...fun startPlaying() { play(toy = Stick) play() // toy = SqueakyToy}// decompiled Java codepublic static final void play(@NotNull Toy toy) { Intrinsics.checkNotNullParameter(toy, "toy");}// $FF: synthetic methodpublic static void play$default(Toy var0, int var1, Object var2) { if ((var1 & 1) != 0) { var0 = SqueakyToy; } play(var0);}public static final void startPlaying() { play(Stick); play$default((Toy)null, 1, (Object)null);}
...
// decompiled Java code
public static final void play(@NotNull Toy toy) {
Intrinsics.checkNotNullParameter(toy, "toy");
// $FF: synthetic method
public static void play$default(Toy var0, int var1, Object var2) {
if ((var1 & 1) != 0) {
var0 = SqueakyToy;
play(var0);
public static final void startPlaying() {
play(Stick);
play$default((Toy)null, 1, (Object)null);
コンパイラが 2 つの関数を生成していることがわかります。
play
Toy
play$default
int
Object
null
play$default の int パラメータの値は、デフォルト引数が渡された引数の数と、そのインデックスに基づいて計算されます。Kotlin コンパイラは、どのパラメータを使って play 関数を呼び出すかを、このパラメータの値に基づいて判断します。
この例の play() の呼び出しでは、インデックス 0 の引数がデフォルト引数を使っています。そのため、int var1 = 2⁰ を使って play$default を呼び出します。
play()
int var1 = 2⁰
これで、play$default の実装は、var0 の値をデフォルト値で置き換えなければならないことを認識できます。
var0
この int パラメータの動作を確認するため、もう少し複雑な例を見てみましょう。play 関数を拡張し、この関数を呼び出す際に doggo と toy をデフォルト引数として使うようにします。
doggo
toy
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ fun play(doggo: Doggo = goodDoggo, doggo2: Doggo = veryGoodDoggo, toy: Toy = SqueakyToy) {...}fun startPlaying() { play2(doggo2 = myDoggo)}
fun play(doggo: Doggo = goodDoggo, doggo2: Doggo = veryGoodDoggo, toy: Toy = SqueakyToy) {...}
play2(doggo2 = myDoggo)
逆コンパイルしたコードはどうなったでしょうか。
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ public static final void play(@NotNull Doggo doggo, @NotNull Doggo doggo2, @NotNull Toy toy) {... }// $FF: synthetic methodpublic static void play$default(Doggo var0, Doggo var1, Toy var2, int var3, Object var4) { if ((var3 & 1) != 0) { var0 = goodDoggo; } if ((var3 & 2) != 0) { var1 = veryGoodDoggo; } if ((var3 & 4) != 0) { var2 = SqueakyToy; } play(var0, var1, var2);}public static final void startPlaying() { play2$default((Doggo)null, myDoggo, (Toy)null, 5, (Object)null); }
public static final void play(@NotNull Doggo doggo, @NotNull Doggo doggo2, @NotNull Toy toy) {
public static void play$default(Doggo var0, Doggo var1, Toy var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var0 = goodDoggo;
if ((var3 & 2) != 0) {
var1 = veryGoodDoggo;
if ((var3 & 4) != 0) {
var2 = SqueakyToy;
play(var0, var1, var2);
play2$default((Doggo)null, myDoggo, (Toy)null, 5, (Object)null);
int パラメータが 5 になりました。この計算の仕組みは次のようになります。0 番目と 2 番目のパラメータでデフォルト引数が使われているので、var3 = 2⁰ + 2² = 5 となります。パラメータは、ビット単位の & 演算を使って次のように評価されます。
var3 = 2⁰ + 2² = 5
var3 & 1 != 0
true
var0 = goodDoggo
var3 & 2 != 0
false
var1
var3 & 4 != 0
var2 = SqueakyToy
コンパイラは、var3 に適用されたビットマスクから、どのパラメータをデフォルト値と置き換えるかを計算できます。
var3
上の例で、Object パラメータの値が常に null になっていたことに気づいたかもしれません。実際に、play$default 関数ではこの値が使われることはありません。このパラメータは、オーバーライドする関数でデフォルト値をサポートするために使用されています。
デフォルト引数がある関数をオーバーライドすると、何が起きるでしょうか。
先ほどの例を次のように変更してみましょう。
Doggo
Open
PlayfulDoggo
PlayfulDoggo.play にデフォルト値を設定しようとしても、次のように表示され、許可されません。An overriding function is not allowed to specify default values for its parameters(オーバーライド関数では、パラメータへのデフォルト値の指定は許可されていません)
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */open class Doggo( val name: String, val rating: Int = 11) { open fun play(toy: Toy = SqueakyToy) {...}}class PlayfulDoggo(val playfulness: Int, name: String, rating: Int) : Doggo(name, rating) { // error: An overriding function is not allowed to specify default values for its parameters override fun play(toy: Toy = Stick) { }}
open class Doggo(
) {
open fun play(toy: Toy = SqueakyToy) {...}
class PlayfulDoggo(val playfulness: Int, name: String, rating: Int) : Doggo(name, rating) {
// error: An overriding function is not allowed to specify default values for its parameters
override fun play(toy: Toy = Stick) { }
override を削除して逆コンパイルしたコードを確認すると、PlayfulDoggo.play() は次のようになっています。
override
PlayfulDoggo.play()
public void play(@NotNull Toy toy) {... }// $FF: synthetic methodpublic static void play$default(Doggo var0, Toy var1, int var2, Object var3) { if (var3 != null) { throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: play"); } else { if ((var2 & 1) != 0) { var1 = DoggoKt.getSqueakyToy(); } var0.play(var1); }}
public void play(@NotNull Toy toy) {... }
public static void play$default(Doggo var0, Toy var1, int var2, Object var3) {
if (var3 != null) {
throw new UnsupportedOperationException("Super calls with default arguments not supported in this target, function: play");
} else {
if ((var2 & 1) != 0) {
var1 = DoggoKt.getSqueakyToy();
var0.play(var1);
これは、デフォルト引数を使った super の呼び出しが将来的にサポートされるという意味なのでしょうか。この点については、成り行きを見守るしかありません。
コンストラクタでは、逆コンパイルした Java コードに 1 つだけ違う点があります。以下をご覧ください。
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */ // kotlin declarationclass Doggo( val name: String, val rating: Int = 11)// decompiled Java codepublic final class Doggo { ... public Doggo(@NotNull String name, int rating) { Intrinsics.checkNotNullParameter(name, "name"); super(); this.name = name; this.rating = rating; } // $FF: synthetic method public Doggo(String var1, int var2, int var3, DefaultConstructorMarker var4) { if ((var3 & 2) != 0) { var2 = 11; } this(var1, var2); }
// kotlin declaration
public final class Doggo {
public Doggo(@NotNull String name, int rating) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.name = name;
this.rating = rating;
public Doggo(String var1, int var2, int var3, DefaultConstructorMarker var4) {
var2 = 11;
this(var1, var2);
コンストラクタでも合成メソッドが作成されていますが、関数で使われていた Object ではなく、DefaultConstructorMarker が null で呼び出されています。
DefaultConstructorMarker
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */// kotlinval goodDoggo = Doggo("Tofu")// decompiled Java codeDoggo goodDoggo = new Doggo("Tofu", 0, 2, (DefaultConstructorMarker)null);
val goodDoggo = Doggo("Tofu")
Doggo goodDoggo = new Doggo("Tofu", 0, 2, (DefaultConstructorMarker)null);
デフォルト引数があるセカンダリ コンストラクタでも、プライマリ コンストラクタと同じように DefaultConstructorMarker を使った別の合成メソッドが生成されます。
/* Copyright 2020 Google LLC. SPDX-License-Identifier: Apache-2.0 */// kotlinclass Doggo( val name: String, val rating: Int = 11) { constructor(name: String, rating: Int, lazy: Boolean = true) }// decompiled Java codepublic final class Doggo { ... public Doggo(@NotNull String name, int rating) { ... } // $FF: synthetic method public Doggo(String var1, int var2, int var3, DefaultConstructorMarker var4) { if ((var3 & 2) != 0) { var2 = 11; } this(var1, var2); } public Doggo(@NotNull String name, int rating, boolean lazy) { ... } // $FF: synthetic method public Doggo(String var1, int var2, boolean var3, int var4, DefaultConstructorMarker var5) { if ((var4 & 4) != 0) { var3 = true; } this(var1, var2, var3); }}
constructor(name: String, rating: Int, lazy: Boolean = true)
public Doggo(@NotNull String name, int rating, boolean lazy) {
public Doggo(String var1, int var2, boolean var3, int var4, DefaultConstructorMarker var5) {
if ((var4 & 4) != 0) {
var3 = true;
this(var1, var2, var3);
シンプルで美しいデフォルト引数を利用すると、パラメータにデフォルト値を設定できるようになるので、メソッドをオーバーロードする際に書かなければならないボイラープレート コードの量が減ります。多くの Kotlin キーワードに言えることですが、生成されるコードを調べてみれば、そこでどのような魔法が使われているかを理解できます。詳しくは、他の Kotlin Vocabulary の投稿を確認してみてください。