Kotlin-Java इंटरऑप गाइड

इस दस्तावेज़ में, Java और Kotlin में सार्वजनिक एपीआई बनाने के लिए नियमों का एक सेट दिया गया है. इसका मकसद यह है कि कोड को दूसरी भाषा से इस्तेमाल करने पर, वह स्वाभाविक लगे.

Java (Kotlin के लिए)

कोई हार्ड कीवर्ड नहीं है

Kotlin के किसी भी हार्ड कीवर्ड का इस्तेमाल, तरीकों या फ़ील्ड के नाम के तौर पर न करें. इन्हें Kotlin से कॉल करते समय, बैकटिक का इस्तेमाल करके एस्केप करना ज़रूरी होता है. सॉफ़्ट कीवर्ड, मॉडिफ़ायर कीवर्ड, और खास आइडेंटिफ़ायर इस्तेमाल किए जा सकते हैं.

उदाहरण के लिए, Mockito के when फ़ंक्शन को Kotlin में इस्तेमाल करने के लिए, बैकटिक की ज़रूरत होती है:

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

Any एक्सटेंशन के नामों का इस्तेमाल न करें

जब तक बहुत ज़रूरी न हो, तब तक Any पर एक्सटेंशन फ़ंक्शन के नामों का इस्तेमाल, तरीकों के लिए न करें. इसी तरह, Any पर एक्सटेंशन प्रॉपर्टी के नामों का इस्तेमाल, फ़ील्ड के लिए न करें. सदस्यता वाले तरीकों और फ़ील्ड को हमेशा Any के एक्सटेंशन फ़ंक्शन या प्रॉपर्टी से ज़्यादा प्राथमिकता दी जाएगी. हालांकि, कोड पढ़ते समय यह जानना मुश्किल हो सकता है कि किसे कॉल किया जा रहा है.

शून्य होने की स्थिति के बारे में एनोटेशन

सार्वजनिक एपीआई में हर नॉन-प्रिमिटिव पैरामीटर, रिटर्न, और फ़ील्ड टाइप में, नल होने की संभावना के बारे में एनोटेशन होना चाहिए. एनोटेट नहीं किए गए टाइप को "प्लैटफ़ॉर्म" टाइप के तौर पर समझा जाता है. इनमें शून्य होने की स्थिति के बारे में साफ़ तौर पर नहीं बताया जाता.

डिफ़ॉल्ट रूप से, Kotlin कंपाइलर JSR 305 एनोटेशन को फ़्लैग करता है. हालांकि, वह उन्हें चेतावनियों के साथ फ़्लैग करता है. कंपाइलर को एनोटेशन को गड़बड़ियों के तौर पर मानने के लिए, फ़्लैग भी सेट किया जा सकता है.

LAMBDA फ़ंक्शन के पैरामीटर आखिर में होने चाहिए

एसएएम कन्वर्ज़न के लिए ज़रूरी शर्तें पूरी करने वाले पैरामीटर टाइप, आखिर में होने चाहिए.

उदाहरण के लिए, RxJava 2 के Flowable.create() मेथड के सिग्नेचर को इस तरह से तय किया गया है:

public static <T> Flowable<T> create(
    FlowableOnSubscribe<T> source,
    BackpressureStrategy mode) { /* … */ }

FlowableOnSubscribe, एसएएम कन्वर्ज़न के लिए ज़रूरी शर्तें पूरी करता है. इसलिए, Kotlin से इस तरीके के फ़ंक्शन कॉल इस तरह दिखते हैं:

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

हालांकि, अगर मेथड सिग्नेचर में पैरामीटर को उलट दिया गया है, तो फ़ंक्शन कॉल में ट्रेलिंग-लैम्डा सिंटैक्स का इस्तेमाल किया जा सकता है:

Flowable.create(BackpressureStrategy.LATEST) { /* … */ }

प्रॉपर्टी प्रीफ़िक्स

किसी तरीके को Kotlin में प्रॉपर्टी के तौर पर दिखाने के लिए, "bean"-स्टाइल वाले प्रीफ़िक्स का इस्तेमाल करना ज़रूरी है.

ऐक्सेसर मेथड के लिए, get प्रीफ़िक्स की ज़रूरत होती है. हालांकि, बूलियन वैल्यू दिखाने वाले मेथड के लिए, is प्रीफ़िक्स का इस्तेमाल किया जा सकता है.

public final class User {
  public String getName() { /* … */ }
  public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()

एसोसिएटेड म्यूटेटर मेथड के लिए, set प्रीफ़िक्स की ज़रूरत होती है.

public final class User {
  public String getName() { /* … */ }
  public void setName(String name) { /* … */ }
  public boolean isActive() { /* … */ }
  public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)

अगर आपको प्रॉपर्टी के तौर पर दिखाए गए तरीकों का इस्तेमाल करना है, तो has, set जैसे नॉन-स्टैंडर्ड प्रीफ़िक्स या नॉन-get-प्रीफ़िक्स वाले ऐक्सेसर का इस्तेमाल न करें. नॉन-स्टैंडर्ड प्रीफ़िक्स वाले तरीकों को अब भी फ़ंक्शन के तौर पर कॉल किया जा सकता है. हालांकि, यह इस बात पर निर्भर करता है कि तरीका किस तरह काम करता है.

ऑपरेटर ओवरलोडिंग

ऐसे तरीके के नामों का ध्यान रखें जो कॉल-साइट के खास सिंटैक्स की अनुमति देते हैं. जैसे, Kotlin में ऑपरेटर ओवरलोडिंग. पक्का करें कि छोटे किए गए सिंटैक्स के साथ इस्तेमाल करने के लिए, तरीकों के नाम इस तरह से दिए गए हों कि वे समझ में आएं.

public final class IntBox {
  private final int value;
  public IntBox(int value) {
    this.value = value;
  }
  public IntBox plus(IntBox other) {
    return new IntBox(value + other.value);
  }
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)

Kotlin (Java के लिए)

फ़ाइल का नाम

जब किसी फ़ाइल में टॉप-लेवल के फ़ंक्शन या प्रॉपर्टी शामिल हों, तो उन्हें @file:JvmName("Foo") के साथ एनोटेट ज़रूर करें, ताकि उन्हें एक अच्छा नाम दिया जा सके.

डिफ़ॉल्ट रूप से, MyClass.kt फ़ाइल में टॉप-लेवल के सदस्य, MyClassKt नाम की क्लास में शामिल हो जाएंगे. यह क्लास, देखने में अच्छी नहीं लगती. साथ ही, इससे भाषा की जानकारी भी लीक हो जाती है.

एक से ज़्यादा फ़ाइलों के टॉप-लेवल सदस्यों को एक ही क्लास में शामिल करने के लिए, @file:JvmMultifileClass को जोड़ें.

लैम्ब्डा फ़ंक्शन के आर्ग्युमेंट

Java में तय किए गए सिंगल मेथड इंटरफ़ेस (एसएएम) को Kotlin और Java, दोनों में लागू किया जा सकता है. इसके लिए, लैम्डा सिंटैक्स का इस्तेमाल किया जाता है. यह सिंटैक्स, लागू करने के तरीके को इनलाइन करता है. Kotlin में इस तरह के इंटरफ़ेस को तय करने के लिए कई विकल्प होते हैं. इनमें से हर विकल्प में थोड़ा अंतर होता है.

बेहतर परिभाषा

Java में इस्तेमाल किए जाने वाले हाई-ऑर्डर फ़ंक्शन, ऐसे फ़ंक्शन टाइप नहीं लेने चाहिए जो Unit दिखाते हैं. ऐसा इसलिए, क्योंकि इससे Java कॉल करने वालों को Unit.INSTANCE दिखाना होगा. सिग्नेचर में फ़ंक्शन टाइप को इनलाइन करने के बजाय, फ़ंक्शनल (एसएएम) इंटरफ़ेस का इस्तेमाल करें. इसके अलावा, जब ऐसे इंटरफ़ेस तय करने हों जिन्हें लैम्ब्डा के तौर पर इस्तेमाल किया जाना है, तब सामान्य इंटरफ़ेस के बजाय फ़ंक्शनल (एसएएम) इंटरफ़ेस का इस्तेमाल करें. इससे Kotlin से इनका इस्तेमाल आसानी से किया जा सकेगा.

Kotlin की इस परिभाषा पर ध्यान दें:

fun interface GreeterCallback {
  fun greetName(String name)
}

fun sayHi(greeter: GreeterCallback) = /* … */

Kotlin से शुरू करने पर:

sayHi { println("Hello, $it!") }

Java से शुरू करने पर:

sayHi(name -> System.out.println("Hello, " + name + "!"));

अगर फ़ंक्शन टाइप Unit को वापस नहीं भेजता है, तब भी इसे नाम वाला इंटरफ़ेस बनाना एक अच्छा विकल्प हो सकता है. इससे कॉल करने वाले लोग, इसे नाम वाले क्लास के साथ लागू कर पाएंगे. साथ ही, Kotlin और Java, दोनों में सिर्फ़ लैम्डा का इस्तेमाल नहीं कर पाएंगे.

class MyGreeterCallback : GreeterCallback {
  override fun greetName(name: String) {
    println("Hello, $name!");
  }
}

ऐसे फ़ंक्शन टाइप इस्तेमाल न करें जो Unit दिखाते हैं

Kotlin की इस परिभाषा पर ध्यान दें:

fun sayHi(greeter: (String) -> Unit) = /* … */

इसके लिए, Java कॉलर को Unit.INSTANCE वापस करना होगा:

sayHi(name -> {
  System.out.println("Hello, " + name + "!");
  return Unit.INSTANCE;
});

जब लागू करने का मकसद स्टेट रखना हो, तब फ़ंक्शनल इंटरफ़ेस का इस्तेमाल न करें

जब इंटरफ़ेस को लागू करने का मतलब कोई स्थिति रखना होता है, तो लैम्ब्डा सिंटैक्स का इस्तेमाल करना सही नहीं होता. Comparable एक बेहतरीन उदाहरण है, क्योंकि इसका इस्तेमाल this की तुलना other से करने के लिए किया जाता है. हालांकि, लैम्ब्डा में this नहीं होता. इंटरफ़ेस को fun से प्रीफ़िक्स न करने पर, कॉलर को object : ... सिंटैक्स का इस्तेमाल करना पड़ता है. इससे कॉलर को स्टेट के बारे में जानकारी मिलती है.

Kotlin की इस परिभाषा पर ध्यान दें:

// No "fun" prefix.
interface Counter {
  fun increment()
}

यह Kotlin में लैम्ब्डा सिंटैक्स को रोकता है. इसके लिए, इस लंबे वर्शन की ज़रूरत होती है:

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

Nothing जेनेरिक से बचें

जिस टाइप का जेनरिक पैरामीटर Nothing है उसे Java में रॉ टाइप के तौर पर दिखाया जाता है. Java में रॉ टाइप का इस्तेमाल बहुत कम किया जाता है. इसलिए, इनका इस्तेमाल नहीं करना चाहिए.

दस्तावेज़ से जुड़े अपवाद

जिन फ़ंक्शन में चेक किए गए अपवादों को थ्रो किया जा सकता है उन्हें @Throws के साथ दस्तावेज़ में शामिल किया जाना चाहिए. रंटाइम के दौरान होने वाली गड़बड़ियों के बारे में KDoc में जानकारी दी जानी चाहिए.

ध्यान रखें कि कोई फ़ंक्शन जिन एपीआई को डेलिगेट करता है वे चेक किए गए अपवादों को थ्रो कर सकते हैं. हालांकि, Kotlin में ऐसा नहीं होता.

डिफ़ेंसिव कॉपी

सार्वजनिक एपीआई से, सिर्फ़ पढ़ने के लिए उपलब्ध ऐसे कलेक्शन वापस लाते समय जिन्हें शेयर किया गया है या जिनका मालिकाना हक आपके पास नहीं है, उन्हें ऐसे कंटेनर में रैप करें जिसमें बदलाव नहीं किया जा सकता. इसके अलावा, डिफ़ेंसिव कॉपी भी की जा सकती है. Kotlin में सिर्फ़ पढ़ने के लिए उपलब्ध प्रॉपर्टी को लागू किया जाता है. हालांकि, Java में ऐसा नहीं किया जाता. रैपर या डिफ़ेंसिव कॉपी के बिना, लंबे समय तक चलने वाले कलेक्शन रेफ़रंस को वापस करके, इनवेरिएंट का उल्लंघन किया जा सकता है.

कंपेनियन फ़ंक्शन

कंपैनियन ऑब्जेक्ट में मौजूद सार्वजनिक फ़ंक्शन को @JvmStatic के साथ एनोटेट किया जाना चाहिए, ताकि उन्हें स्टैटिक तरीके के तौर पर दिखाया जा सके.

एनोटेशन के बिना, ये फ़ंक्शन सिर्फ़ स्टैटिक Companion फ़ील्ड पर इंस्टेंस मेथड के तौर पर उपलब्ध होते हैं.

गलत: कोई एनोटेशन नहीं है

class KotlinClass {
    companion object {
        fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.Companion.doWork();
    }
}

सही: @JvmStatic एनोटेशन

class KotlinClass {
    companion object {
        @JvmStatic fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.doWork();
    }
}

कंपैनियन कॉन्स्टेंट

companion object में मौजूद सार्वजनिक और नॉन-const प्रॉपर्टी, स्टैटिक फ़ील्ड के तौर पर दिखने के लिए, @JvmField के साथ एनोटेट की जानी चाहिए.

एनोटेशन के बिना, ये प्रॉपर्टी सिर्फ़ स्टैटिक Companion फ़ील्ड पर, अजीब नाम वाले इंस्टेंस "getters" के तौर पर उपलब्ध होती हैं. @JvmStatic के बजाय @JvmField का इस्तेमाल करने से, "getters" को क्लास के स्टैटिक तरीकों में ले जाया जाता है. हालांकि, यह अब भी गलत है.

गलत: कोई एनोटेशन नहीं है

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
    }
}

गलत: @JvmStatic annotation

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.getBIG_INTEGER_ONE());
    }
}

सही: @JvmField एनोटेशन

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.BIG_INTEGER_ONE);
    }
}

नैचुरल नाम रखना

Kotlin में, Java की तुलना में फ़ंक्शन को कॉल करने के अलग-अलग तरीके होते हैं. इससे फ़ंक्शन के नाम रखने के तरीके में बदलाव हो सकता है. @JvmName का इस्तेमाल करके, ऐसे नाम डिज़ाइन करें जो दोनों भाषाओं के कन्वेंशन के हिसाब से मुहावरेदार लगें या उनके नाम, संबंधित स्टैंडर्ड लाइब्रेरी के नाम से मेल खाएं.

ऐसा अक्सर एक्सटेंशन फ़ंक्शन और एक्सटेंशन प्रॉपर्टी के लिए होता है, क्योंकि रिसीवर टाइप की जगह अलग होती है.

sealed class Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()

@JvmName("ofNullable")
fun <T> T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
    val nullableString: String? = "foo"
    val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
    String nullableString = "Foo";
    Optional<String> optionalString =
          Optionals.ofNullable(nullableString);
}

डिफ़ॉल्ट वैल्यू के लिए फ़ंक्शन ओवरलोड

ऐसे फ़ंक्शन जिनमें पैरामीटर की डिफ़ॉल्ट वैल्यू होती है उनमें @JvmOverloads का इस्तेमाल करना ज़रूरी है. इस एनोटेशन के बिना, किसी भी डिफ़ॉल्ट वैल्यू का इस्तेमाल करके फ़ंक्शन को कॉल नहीं किया जा सकता.

@JvmOverloads का इस्तेमाल करते समय, जनरेट किए गए तरीकों की जांच करें. इससे यह पक्का किया जा सकेगा कि वे सभी सही हैं. अगर ऐसा नहीं होता है, तो जब तक आपको सही नतीजे न मिल जाएं, तब तक इनमें से एक या दोनों रिफ़ैक्टरिंग करें:

  • पैरामीटर के क्रम में बदलाव करें, ताकि डिफ़ॉल्ट वैल्यू वाले पैरामीटर आखिर में दिखें.
  • डिफ़ॉल्ट वैल्यू को मैन्युअल फ़ंक्शन ओवरलोड में ले जाएं.

गलत: नहीं @JvmOverloads

class Greeting {
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Mr.", "Bob");
    }
}

सही: @JvmOverloads annotation.

class Greeting {
    @JvmOverloads
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Bob");
    }
}

लिंट चेक

ज़रूरी शर्तें

  • Android Studio का वर्शन: 3.2 Canary 10 या इसके बाद का वर्शन
  • Android Gradle प्लगिन का वर्शन: 3.2 या इसके बाद का वर्शन

इन जांचों के लिए सहायता उपलब्ध है

अब Android Lint की जांच की जा सकती है. इससे, पहले बताई गई इंटरऑपरेबिलिटी से जुड़ी कुछ समस्याओं का पता लगाने और उन्हें फ़्लैग करने में मदद मिलेगी. सिर्फ़ Java में मौजूद समस्याओं का पता चलता है. हालांकि, Kotlin का इस्तेमाल किया जा सकता है. खास तौर पर, ये जांच की जा सकती हैं:

  • शून्य होने की स्थिति के बारे में जानकारी नहीं है
  • प्रॉपर्टी का ऐक्सेस
  • Kotlin के मुश्किल कीवर्ड का इस्तेमाल न किया गया हो
  • लैम्ब्डा पैरामीटर आखिर में

Android Studio

इन जांचों को चालू करने के लिए, फ़ाइल > प्राथमिकताएं > एडिटर > जांच पर जाएं. इसके बाद, Kotlin Interoperability में जाकर, उन नियमों को चुनें जिन्हें आपको चालू करना है:

पहली इमेज. Android Studio में Kotlin इंटरऑपरेबिलिटी की सेटिंग.

जिन नियमों को चालू करना है उन्हें चुनने के बाद, कोड की जांच (Analyze > Inspect Code…) करने पर नई जांचें शुरू हो जाएंगी

कमांड-लाइन बिल्ड

कमांड-लाइन बिल्ड से इन जांचों को चालू करने के लिए, अपनी build.gradle फ़ाइल में यह लाइन जोड़ें:

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

lintOptions में इस्तेमाल किए जा सकने वाले सभी कॉन्फ़िगरेशन के लिए, Android Gradle DSL रेफ़रंस देखें.

इसके बाद, कमांड लाइन से ./gradlew lint चलाएं.