概要
Java で static メソッドだけを持つクラスを実装する、いわゆる Utils クラスパターンを Kotlin で実装したい場合、
一般的には object を使って実装すると言われています。
ただ、それだと static なインスタンスを1つだけ保持し続けることになるので、真の意味で同じ実装にはならないのでは?という疑問が浮かびました。
本当に object でよいのか、調べてみました。
確認
バイトコード -> デコンパイル
Android Studio では Kotlin のコンパイル結果を表示することができます。バイトコードを読めないエンジニアは、そのバイトコードをデコンパイルして Java のコードで見ることもできます。
Android Studio の Find Action で byte と打ち込み、"Kotlin bytecode" を選択すると、バイトコードが出力されます。
バイトコードが読めない場合は、 "Kotlin bytecode" の Decompile ボタンを押してみましょう。
デコンパイルされた Java コードが新しいタブで開きます。
確認するパターン
今回は以下3パターンを試してみます。
- object
- class & companion object
- Top-level function
いずれのパターンでも、URLとして有効な文字列か否かを判定する関数を持つクラスを実装して比較をします。
バイトコード -> デコンパイルの結果を見て、生成されるコードの違いを比較してみます。
1. object
最もポピュラーな、 object に static な関数を持たせるパターンです。
import android.webkit.URLUtil
object Urls {
fun isInvalidUrl(url: String?): Boolean =
url.isNullOrBlank() || (!URLUtil.isHttpUrl(url) && !URLUtil.isHttpsUrl(url))
fun isValidUrl(url: String?): Boolean = !isInvalidUrl(url)
}
Decompiled
Singleton パターンの実装となっており、メリットもデメリットも同パターンと同じです。
import android.webkit.URLUtil;
import kotlin.Metadata;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\bA\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0010\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006J\u0010\u0010\u0007\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006¨\u0006\b"},
d2 = {"Ljp/toastkid/yobidashi/libs/Urls;", "", "()V", "isInvalidUrl", "", "url", "", "isValidUrl", "app"}
)
public final class Urls {
public static final Urls INSTANCE;
public final boolean isInvalidUrl(@Nullable String url) {
CharSequence var2 = (CharSequence)url;
boolean var3 = false;
boolean var4 = false;
return var2 == null || StringsKt.isBlank(var2) || !URLUtil.isHttpUrl(url) && !URLUtil.isHttpsUrl(url);
}
public final boolean isValidUrl(@Nullable String url) {
return !this.isInvalidUrl(url);
}
private Urls() {
}
static {
Urls var0 = new Urls();
INSTANCE = var0;
}
}
2. class & companion object
上記のパターンだと Singleton のオブジェクトを持たないといけなくなるので、それを回避するためにこのようなパターンを考えた同僚がいました。
私は無駄だと思うのですが、一応試してみます。
import android.webkit.URLUtil
class UrlsClass {
companion object Urls {
fun isInvalidUrl(url: String?): Boolean =
url.isNullOrBlank() || (!URLUtil.isHttpUrl(url) && !URLUtil.isHttpsUrl(url))
fun isValidUrl(url: String?): Boolean = !isInvalidUrl(url)
}
}
Decompiled
余計な static クラスが作られてしまっています。やはりあまり意味がないように思えます。
import android.webkit.URLUtil;
import kotlin.Metadata;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\f\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0003\u0018\u0000 \u00032\u00020\u0001:\u0001\u0003B\u0005¢\u0006\u0002\u0010\u0002¨\u0006\u0004"},
d2 = {"Ljp/toastkid/yobidashi/libs/UrlsClass;", "", "()V", "Urls", "app"}
)
public final class UrlsClass {
public static final UrlsClass.Urls Urls = new UrlsClass.Urls((DefaultConstructorMarker)null);
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\b\u0086\u0003\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0010\u0010\u0003\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006J\u0010\u0010\u0007\u001a\u00020\u00042\b\u0010\u0005\u001a\u0004\u0018\u00010\u0006¨\u0006\b"},
d2 = {"Ljp/toastkid/yobidashi/libs/UrlsClass$Urls;", "", "()V", "isInvalidUrl", "", "url", "", "isValidUrl", "app"}
)
public static final class Urls {
public final boolean isInvalidUrl(@Nullable String url) {
CharSequence var2 = (CharSequence)url;
boolean var3 = false;
boolean var4 = false;
return var2 == null || StringsKt.isBlank(var2) || !URLUtil.isHttpUrl(url) && !URLUtil.isHttpsUrl(url);
}
public final boolean isValidUrl(@Nullable String url) {
return !((UrlsClass.Urls)this).isInvalidUrl(url);
}
private Urls() {
}
// $FF: synthetic method
public Urls(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
3. Top-level function
Kotlin はクラスの外にも関数を記述することができます。 Kotlin からはグローバル関数のように使うことになります。
Java からは ファイル名Kt
という名前を使って参照することができます。
import android.webkit.URLUtil
fun isInvalidUrl(url: String?): Boolean =
url.isNullOrBlank() || (!URLUtil.isHttpUrl(url) && !URLUtil.isHttpsUrl(url))
fun isValidUrl(url: String?): Boolean = !isInvalidUrl(url)
Decompiled
このパターンだと、Java でよくやっていた、static な関数だけを持つクラスになるようです。
import android.webkit.URLUtil;
import kotlin.Metadata;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.Nullable;
@Metadata(
mv = {1, 1, 15},
bv = {1, 0, 3},
k = 2,
d1 = {"\u0000\u0010\n\u0000\n\u0002\u0010\u000b\n\u0000\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0010\u0010\u0000\u001a\u00020\u00012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003\u001a\u0010\u0010\u0004\u001a\u00020\u00012\b\u0010\u0002\u001a\u0004\u0018\u00010\u0003¨\u0006\u0005"},
d2 = {"isInvalidUrl", "", "url", "", "isValidUrl", "app"}
)
public final class UrlsTopKt {
public static final boolean isInvalidUrl(@Nullable String url) {
CharSequence var1 = (CharSequence)url;
boolean var2 = false;
boolean var3 = false;
return var1 == null || StringsKt.isBlank(var1) || !URLUtil.isHttpUrl(url) && !URLUtil.isHttpsUrl(url);
}
public static final boolean isValidUrl(@Nullable String url) {
return !isInvalidUrl(url);
}
}
終わりに
Top-level function だと Java の static メソッドを集めたクラスと同じバイトコードが生成されるようでした。
ただ、このパターンだと Kotlin からはグローバル関数のように呼び出さなければいけなくなるので、開発チーム内で合意を取ってから採用した方が良いかと思います。
また、companion object だけを持つ class は余計な static クラスを作り出されるので、あまり使い道がないということも覚えておくと良さそうです。