H. Tam Doan
H. Tam DoanSenior Software Engineer @ Zalo

Jak stworzyć klasę pomocniczą w Kotlinie

Dowiedz się, jak można stworzyć klasę pomocniczą w Kotlinie. Porównaj metody, które można wykorzystać: obiekty oraz metody na poziomie pakietu. Zobacz, jak je potem wykorzystać w Javie.
17.04.20206 min
Jak stworzyć klasę pomocniczą w Kotlinie

TL;DR: możesz utworzyć klasę pomocniczą, umieszczając metody wewnątrz obiektu lub definiując funkcje na poziomie pakietu. Jeśli integrujesz swój kod z kodem Java i potrzebujesz prawdziwych statycznych metod, to możesz je zawsze umieścić w obiekcie i opatrzyć adnotacją @JvmStatic. Jeśli chcesz dowiedzieć się więcej, zapraszamy do artykułu!

Kotlin jest wieloplatformowym, typowanym statycznie językiem. Został on zaprojektowany jako język w pełni kompatybilny z Javą. Rozwój Kotlina jest sponsorowany przez JetBrains i Google za pośrednictwem Fundacji Kotlin. To dobry czas, aby zastanowić się nad jego użyciem w swoim następnym projekcie, ponieważ Google ogłosiło, że jest to główny język rekomendowany do pisania aplikacji na Androida. 

Kiedy po raz pierwszy korzystałem z Kotlina, miałem problem z napisaniem klasy pomocniczej. Zadałem zatem pytanie na Stack Overflow. Zdałem sobie potem sprawę, że istnieje na to wiele sposobów. Zanim jednak przejdziemy do pisania klasy pomocniczej, musimy wyjaśnić kilka podstawowych zagadnień.

Słowo kluczowe static

Metody statyczne są znane programistom Javy. Deklaruje się je za pomocą słowa kluczowego i wywołuje bezpośrednio przez nazwę klasy. Czy możemy zrobić to samo w Kotlinie? Odpowiedź brzmi „tak”, ale w inny sposób.

W przeciwieństwie do Javy, w Kotlinie nie ma niczego, co ma w nazwie „statyczny”.

Jak więc możemy to osiągnąć? Zobaczmy!

Obiekty

W Kotlinie istnieją 3 typy obiektów: Object Declaration, Companion Object oraz Object Expression.

Obiekt — przydatny typ danych w Kotlinie

Object Declaration

Wzorzec Singleton zawsze się przydaje, a Kotlin ułatwia jego deklarację:

object SaberFactory {
    fun makeLightSaber(powers: Int): LightSaber {
        return LightSaber(powers)
    }
}

Oto Object Declaration o nazwie następującej po słowie kluczowym object.

Podobnie jak singleton, istnieje tylko jedna instancja obiektu, który jest tworzony, gdy pierwszy raz potrzebny jest do niego dostęp, a wszystko w sposób w bezpieczny dla wątków.

Aby odnieść się do obiektu, używamy bezpośrednio jego nazwy:

val saber = SaberFactory.makeLightSaber(150)

Companion Object

Companion object łączy się z klasą przez słowo kluczowe companion.

class Saber(val powers: Int) {
    companion object Factory {
        fun makeLightSaber(powers: Int): LightSaber {
            return LightSaber(powers)
        }
    }
}

Companion object może służyć do zadeklarowania, aby metoda była powiązana z klasą, a nie z jej instancjami. 

Companion object to również singleton, więc można uzyskać bezpośredni dostęp do jego metod:

val saber = Saber.makeLightSaber(150)

Różnica między singletonem, a standardową klasą

Największą różnicą jest to, że nie można zainicjalizować obiektu. W rzeczywistości obiekt jest tylko typem danych z pojedynczą instancją, więc jeśli chcemy znaleźć coś podobnego w Javie, byłby to wzorzec Singleton. Być może najlepszym sposobem na pokazanie różnicy jest spojrzenie na zdekompilowany kod Kotlin w formie Java. W Kotlinie deklarujemy obiekt i normalną klasę:

object SaberFactory {
    fun makeLightSaber() { /*...*/ }
}

class SaberFactory {
    fun makeLightSaber() { /*...*/ }
}

Oto ekwiwalent kodu w Javie: singleton oraz standardowa klasa.

// Generated class

public final class SaberFactory {
   public static final SaberFactory INSTANCE = new SaberFactory();

   private SaberFactory() { }

   public final void makeLightSaber() { /*...*/ }
}

public final class SaberFactory {
   public final void makeLightSaber() { /*...*/ }
}

Dlaczego w Kotlinie są obiekty?

Głównym powodem jest to, że Kotlin chciał pozbyć się zmiennych statycznych, pozostawiając nam czysty język obiektowy. Kotlin zniechęca programistów do korzystania z tych pojęć, dając nam zamiast nich obiekty, które są singletonami. Singleton nie jest tak łatwy do napisania w Javie. Jeśli dwa wątki uzyskują dostęp do singletonu jednocześnie, to może to wygenerować dwie instancje obiektu.

Wywoływanie Kotlina z Javy

Kod Kotlina można łatwo wywołać z Javy. Istnieją jednak pewne różnice, które wymagają uwagi.

Kotlin w 100% wymienia się z Javą

Funkcje na poziomie pakietu

Są to wszystkie funkcje zadeklarowane w pliku źródłowym niezawarte w klasie lub obiekcie. Wszystkie funkcje zadeklarowane w pliku app.kt w pakiecie org.example są skompilowane do javowych statycznych metod klasy o nazwie org.example.AppKt (która jest nazwą klasy w PascalCase + Kt). A dzieje się to, aby:

package org.example

fun makeLightSaber() { /*...*/ }

Co zmieni się w:

package org.example

// Generated class
class AppKt {
    public static void makeLightSaber() { /*..*/ }
}

Następnie używamy AppKt w Javie, aby wywołać metodę:

org.example.AppKt.makeLightSaber();


Uwaga:
w przeszłości wszystkie funkcje najwyższego poziomu w tym samym pakiecie stawały się pojedynczą klasą Javy nazwaną po tym właśnie pakiecie. Jednak z jakiegoś powodu, JetBrains postanowił pozbyć się pojedyńczej fasady i zamiast tego użyć nazwy pliku dla wygenerowanej klasy (app.ktAppKt.class). A co jeśli chcemy zmienić nazwę AppKt na inną?

@file:JvmName

Nazwę wygenerowanej klasy Javy można zmienić za pomocą adnotacji @JvmName

@file:JvmName("SaberUtils")

package org.example

fun makeLightSaber() { /*...*/ }


Następnie używamy w Javie SaberUtils do wywołania metody zamiast AppKt:

org.example.SaberUtils.makeLightSaber();


Gdy mamy wiele plików, które generują taką samą nazwę klasy w Javie (ta sama nazwa pakietu, pliku lub to samo @JvmName) to błąd. Jak więc możemy to naprawić?

@file:JvmMultifileClass

Kompilator ma możliwość wygenerowania pojedynczej klasy o określonej nazwie, która będzie zawierać deklaracje ze wszystkich plików, które zawierają tę nazwę. Aby to osiągnąć, użyj adnotacji @JvmMultifileClass we wszystkich plikach.

// lightsaber.kt
@file:JvmName("SaberUtils")
@file:JvmMultifileClass

package org.example

fun makeLightSaber() { /*...*/ }
// darksaber.kt
@file:JvmName("SaberUtils")
@file:JvmMultifileClass

package org.example

fun makeDarkSaber() { /*...*/ }


Możemy następnie wywołać obie metody w Javie za pomocą SaberUtils:

org.example.SaberUtils.makeLightSaber();
org.example.SaberUtils.makeDarkSaber();

@JvmStatic

Jak wspomniałem powyżej, Kotlin reprezentuje funkcje na poziomie pakietu jako metody statyczne. Może jednak również generować metody statyczne dla metod zdefiniowanych w obiektach, jeśli dodasz do nich adnotacje @JvmStatic. Jeśli jej użyjesz, kompilator wygeneruje zarówno metodę statyczną w dołączającej klasie obiektu, jak i metodę instancyjną w samym obiekcie. Na przykład:

object SaberFactory {
    @JvmStatic fun makeStaticSaber() { /*..*/ }
    
    fun makeNonStaticSaber() { /*..*/ }
}

W Javie można używać obu zarówno jako metody statycznej, jak i normalnej (singletona).

SaberFactory.makeStaticSaber(); // works fine
SaberFactory.makeNonStaticSaber(); // error
SaberFactory.INSTANCE.makeNonStaticSaber(); // works, call through the singleton INSTANCE
SaberFactory.INSTANCE.makeStaticSaber(); // works too

Klasy pomocnicze w Kotlinie


Pierwsze podejście

Użyj obiektu. Utwórz metodę w obiekcie:

object SaberUtils {
    fun makeLightSaber(powers: Int): LightSaber {
        return LightSaber(powers)
    }
}

Można wywołać tę metodę z Kotlina, ale przy wywoływaniu z Javy musisz dodać INSTANCE, ponieważ jest to zwykła metoda instancji singletonu, a nie metoda statyczna.

// Call from Kotlin
SaberUtils.makeLightSaber(150)
// Call from Java
SaberUtils.INSTANCE.makeLightSaber(150)


Drugie podejście

Użyj funkcji na poziomie pakietu (bez klasy lub obiektu):

@file:JvmName("SaberUtils")
@file:JvmMultifileClass

fun makeLightSaber(powers: Int): LightSaber {
    return LightSaber(powers)
}

Teraz możesz wywołać SaberUtils.makeLightSaber() z kodu Java. W kodzie Kotlin pozwala to jednak tylko na bezpośrednie użycie metody makeLightSaber() (bez prefiksu SabreUtils), ponieważ nie wiąże się to z żadną klasą.


Co jest lepsze?

Drugie podejście jest w Kotlinie dosyć idiomatyczne. Nie ma właściwie potrzeby umieszczania metod pomocniczych w czymkolwiek. Używanie funkcji zdefiniowanych na poziomie pakietu to nic złego, szczególnie biorąc pod uwagę, że z takich funkcji składa się większość biblioteki standardowej Kotlina.

Jest też kilka powodów, które przemawiają za pierwszym podejściem.

  • Użyj obiektu, aby uniknąć wyświetlania wszystkich sugestii autouzupełniania podczas pisania.
  • Jako programista Javy, zamiast większości metod utils wolałbym Utils.foo() niż foo() zarówno w Javie, jak i w Kotlinie.


Jest jeszcze trzecie rozwiązanie: umieszczenie metod w obiekcie i użycie @JvmStatic na każdej z nich.

Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

<p>Loading...</p>