3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Java の Utils クラスパターンを Kotlin で実装する

Posted at

概要

Java で static メソッドだけを持つクラスを実装する、いわゆる Utils クラスパターンを Kotlin で実装したい場合、
一般的には object を使って実装すると言われています。
ただ、それだと static なインスタンスを1つだけ保持し続けることになるので、真の意味で同じ実装にはならないのでは?という疑問が浮かびました。
本当に object でよいのか、調べてみました。


確認

バイトコード -> デコンパイル

Android Studio では Kotlin のコンパイル結果を表示することができます。バイトコードを読めないエンジニアは、そのバイトコードをデコンパイルして Java のコードで見ることもできます。

Android Studio の Find Action で byte と打ち込み、"Kotlin bytecode" を選択すると、バイトコードが出力されます。

スクリーンショット (7).png

バイトコードが読めない場合は、 "Kotlin bytecode" の Decompile ボタンを押してみましょう。

20200401_2.png

デコンパイルされた Java コードが新しいタブで開きます。

20200401_3.png

確認するパターン

今回は以下3パターンを試してみます。

  1. object
  2. class & companion object
  3. 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 クラスを作り出されるので、あまり使い道がないということも覚えておくと良さそうです。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?