Java'dan Kotlin Kodu Çağırma

Bu codelab'de, Kotlin kodunuzu Java kodundan daha okunaklı hale getirmek için nasıl yazacağınızı veya uyarlayacağınızı öğreneceksiniz.

Neler öğreneceksiniz?

  • @JvmField, @JvmStatic ve diğer ek açıklamalardan yararlanma.
  • Java kodundan belirli Kotlin dili özelliklerine erişimle ilgili sınırlamalar.

Bilmeniz gerekenler

Bu codelab, programcılar için yazılmış olup temel Java ve Kotlin bilgisine sahip olduğunu varsayar.

Bu codelab'de, yeni Kotlin kodunu dahil etmek için Java programlama diliyle yazılmış daha büyük bir projenin bir bölümünü taşıma işlemi simüle edilmektedir.

Basit bir şekilde açıklamak gerekirse, mevcut kod tabanını temsil eden UseCase.java adında tek bir .java dosyamız olacak.

Kısa süre önce Java'da yazılmış bazı işlevleri Kotlin dilinde yazılmış yeni bir sürümle değiştirdiğimizi ve entegrasyonu tamamlamamız gerektiğini varsayalım.

Projeyi içe aktar

Projenin kodu, GitHub projesinden klonlanabilir: GitHub

Alternatif olarak, projeyi şu konumda bulunan bir zip arşivinden indirip çıkarabilirsiniz:

Zip'i İndir

IntelliJ IDEA'yı kullanıyorsanız "Projeyi İçe Aktar"ı seçin.

Android Studio kullanıyorsanız "Projeyi içe aktar (Gradle, Eclipse ADT vb.)" seçeneğini belirtin.

UseCase.java uygulamasını açıp gördüğümüz hataları düzeltmeye başlayalım.

Sorunlu ilk işlev: registerGuest

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

Repository.getNextGuestId() ve Repository.addUser(...) hataları aynıdır: "Statik olmayan statik bir bağlamdan erişilemez."

Şimdi Kotlin dosyalarından birine göz atalım. Repository.kt dosyasını açın.

Kod Depomuzun, nesne anahtar kelimesi kullanılarak beyan edilen bir tekilci olduğunu görüyoruz. Sorun, Kotlin'in bunları statik özellikler ve yöntemler olarak göstermek yerine sınıfımızda statik bir örnek oluşturmasıdır.

Örneğin, Repository.INSTANCE.getNextGuestId() kullanılarak Repository.getNextGuestId() için referans verilebilir, ancak daha iyi bir yöntem mevcuttur.

Kotlin'in, @JvmStatic ile Repository'nin herkese açık özelliklerini ve yöntemlerini not ederek statik yöntemler ve özellikler oluşturmasını sağlayabiliriz:

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

IDE'nizi kullanarak kodunuza @JvmStatic ek açıklaması ekleyin.

UseCase.java ürününe geri dönersek Repository ürünündeki özellikler ve yöntemler artık Repository.BACKUP_PATH dışında hatalara neden olmaz. Daha sonra bu konuya geri döneceğiz.

Şimdilik registerGuest() yöntemindeki bir sonraki hatayı düzeltelim.

Aşağıdaki senaryoyu ele alalım: Dize işlemleri için birkaç statik işlev içeren bir StringUtils sınıfımız vardı. Uzantıyı Kotlin biçimine dönüştürdüğümüzde yöntemleri uzantı işlevlerine dönüştürdük. Java'da uzantı işlevi bulunmadığından Kotlin, bu yöntemleri statik işlevler olarak derler.

Maalesef UseCase.java içindeki registerGuest() yöntemini incelediğimizde, tam olarak doğru olmadığını görebiliriz:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Bunun nedeni, Kotlin'in bu "üst düzey" veya paket düzeyindeki işlevleri, adı dosya adına dayalı olan bir sınıfa yerleştirmektir. Bu örnekte, dosyanın adı StringUtils.kt olduğundan ilgili sınıf StringUtilsKt olarak adlandırılmıştır.

StringUtils ile ilgili tüm referanslarımızı StringUtilsKt olarak değiştirebilir ve bu hatayı düzeltebiliriz. Ancak bu, aşağıdaki nedenlerden dolayı ideal değil:

  • Kodumuzda güncellenmesi gereken birçok yer olabilir.
  • Adın kendisi biraz tuhaf.

Bu nedenle, Java kodumuzu yeniden düzenlemek yerine Kotlin kodumuzu bu yöntemler için farklı bir ad kullanacak şekilde güncelleyelim.

StringUtils.Kt uygulamasını açın ve aşağıdaki paket beyanını bulun:

package com.google.example.javafriendlykotlin

Kotlin'e, @file:JvmName ek açıklamasını kullanarak paket düzeyindeki yöntemler için farklı bir ad kullanmasını söyleyebiliriz. Sınıfı StringUtils olarak adlandırmak için bu ek açıklamayı kullanalım.

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Şimdi, UseCase.java sitesine baktığımızda StringUtils.nameToLogin() hatasının çözüldüğünü görüyoruz.

Maalesef bu hata, User için oluşturucuya iletilen parametrelerle ilgili yeni bir hata ile değiştirildi. Sonraki adımda devam edelim ve UseCase.registerGuest() içindeki bu son hatayı düzeltelim.

Kotlin, parametreler için varsayılan değerleri destekler. Bunların init blokunun içine Repository.kt baktıklarında nasıl kullanıldıklarını görebiliriz.

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

"War" kullanıcısı için, User.kt içinde varsayılan bir değer belirtilmiş olduğundan, displayName için değer girmeyi atlayabiliriz.

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

Ne yazık ki yöntem Java'dan çağrılırken aynı şekilde çalışmaz.

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Varsayılan değerler, Java programlama dilinde desteklenmez. Bu sorunu düzeltmek için Kotlin'e @JvmOverloads ek açıklamasının yardımıyla oluşturucumuz için aşırı yük oluşturmasını önerelim.

Önce User.kt üzerinde küçük bir güncelleme yapmamız gerekiyor.

User sınıfı yalnızca tek bir birincil oluşturucu içerdiğinden ve oluşturucu herhangi bir ek açıklama içermediğinden, constructor anahtar kelimesi atlandı. Şimdi, açıklamaya eklemek istediğimize göre constructor anahtar kelimesinin eklenmesi gerekiyor:

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

constructor anahtar kelimesi mevcut olduğunda @JvmOverloads ek açıklamasını ekleyebiliriz:

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

UseCase.java hesabına geri dönersek registerGuest işlevinde başka hata olmadığını görebiliriz.

Sonraki adımda, UseCase.getSystemUsers() içinde user.hasSystemAccess() için yapılan bozuk aramayı düzeltmek istiyoruz. Bunun için bir sonraki adımla devam edin veya hatanın giderilmesi için @JvmOverloads tarafından ne yapıldığı hakkında daha fazla bilgi edinmek için okumaya devam edin.

@JvmOverloads

@JvmOverloads uygulamasının ne yaptığını daha iyi anlamak için UseCase.java ürününde bir test yöntemi oluşturalım:

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

id ve username olmak üzere yalnızca iki parametreyle bir User oluşturabiliriz:

User syrinx = new User(1001, "syrinx");

Ayrıca, groups için varsayılan değeri kullanmaya devam ederken displayName için üçüncü bir parametre ekleyerek de User oluşturabiliriz:

User ione = new User(1002, "ione", "Ione Saldana");

Ancak displayName kodunu atlayıp ek kod yazmadan yalnızca groups için bir değer sağlamak mümkün değildir:

Bu satırı silmek için "//'" ifadesini içeren bir satırı silelim veya başına ekleyelim.

Kotlin'de varsayılan ve varsayılan olmayan parametreleri birleştirmek istiyorsak adlandırılmış parametreleri kullanmamız gerekir.

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

Bunun nedeni, Kotlin'in yapılandırıcılar da dahil olmak üzere işlevler için aşırı yük oluşturma, ancak parametre başına varsayılan bir değerle yalnızca bir aşırı yük oluşturmasıdır.

UseCase.java'yi tekrar ele alıp bir sonraki sorunumuzu ele alalım: UseCase.getSystemUsers() çağrısına user.hasSystemAccess() çağrısı yapılıyor:

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

Bu ilginç bir hata. User sınıfında IDE’nizin otomatik tamamlama özelliğini kullanırsanız hasSystemAccess() öğesinin getHasSystemAccess() olarak yeniden adlandırıldığını fark edersiniz.

Sorunu düzeltmek için Kotlin'in hasSystemAccess adlı val mülkü için farklı bir ad oluşturmasını istiyoruz. Bunun için @JvmName ek açıklamasını kullanabiliriz. User.kt uygulamasına geri dönelim ve bu özelliği nereye uygulamamız gerektiğini görelim.

Ek açıklamayı iki şekilde uygulayabiliriz. Birinci yöntem, doğrudan get() yöntemine uygulamaktır. Örneğin:

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

Bu işlem, açık bir şekilde tanımlanan alıcının imzasını sağlanan adla değiştirmesi için Kotlin sinyaline girer.

Alternatif olarak, aşağıdaki gibi bir get: ön eki kullanarak mülke uygulamak da mümkündür:

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

Alternatif yöntem, varsayılan olarak örtük olarak tanımlanmış bir alıcı kullanan mülkler açısından özellikle faydalıdır. Örneğin:

@get:JvmName("isActive")
val active: Boolean

Bu, alıcı adının açıkça bir alıcı tanımlamak zorunda kalmadan değiştirilmesine olanak tanır.

Bu farklılığa rağmen size en uygun olanı kullanabilirsiniz. İkisi de Kotlin'in hasSystemAccess() adında bir alıcı oluşturmasına neden olur.

UseCase.java hizmetine geri dönersek getSystemUsers() hizmetinin artık hatasız olduğunu doğrulayabiliriz.

Sonraki hata formatUser() dilindedir, ancak Kotlin alıcı adlandırma kuralı hakkında daha fazla bilgi edinmek istiyorsanız bir sonraki adıma geçmeden önce burayı okumaya devam edin.

Alıcı ve Set Adı Adlandırma

Kotlin yazarken, aşağıdaki gibi kod yazmayı unutmak çok kolaydır:

val myString = "Logged in as ${user.displayName}")

displayName değerini almak için bir işlevi çağırıyor. Bunu doğrulamak için menüde Araçlar > Kotlin > Kotlin Bytecode'u Göster seçeneğine gidip Derleme düğmesini tıklayın:

String myString = "Logged in as " + user.getDisplayName();

Bunlara Java'dan erişmek istediğimizde alıcının adını açıkça yazmamız gerekir.

Çoğu durumda, Kotlin mülkleri için alıcıların Java adı yalnızca get ve mülk adıdır (User.getHasSystemAccess() ve User.getDisplayName() ile gördüğümüz gibi). Bunun tek istisnası, adları "&" ile başlayan mülklerdir. Bu örnekte, alıcı için Java adı Kotlin mülkünün adıdır.

Örneğin, User üzerindeki bir mülk şuna benzer:

val isAdmin get() = //...

Java'dan şunlarla erişilebilir:

boolean userIsAnAdmin = user.isAdmin();

Kotlin, @JvmName ek açıklamasını kullanarak ek açıklama yapılan öğe için varsayılan ad yerine belirtilen ada sahip bayt kodu oluşturur.

Oluşturulan adlar her zaman set + mülk adı olan setler için de aynı durum geçerlidir. Örneğin, aşağıdaki sınıfı ele alalım:

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

setRed() adlı satıcıyı updateRed() olarak değiştirirken alıcı adını olduğu gibi bırakalım. Bunun için @set:JvmName sürümünü kullanabiliriz:

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

Java'dan sonra şunları yazabiliriz:

color.updateRed(0.8f);

UseCase.formatUser(), bir User nesnesinin özelliklerinin değerlerini almak için doğrudan alan erişimini kullanır.

Kotlin'de tesisler normal koşullarda alıcı ve ıslatıcı tarafından sergilenmektedir. Bu, val mülklerini içerir.

@JvmField ek açıklamasını kullanarak bu davranışı değiştirebilirsiniz. Bu politika bir sınıftaki mülke uygulandığında, Kotlin alıcı (ve var mülkleri için set) yöntemleri oluşturmayı atlar ve yedekleme alanına doğrudan erişilebilir.

User nesne değiştirilemez olduğundan, mülklerinin her birini alan olarak göstermek isteriz. Bu nedenle her öğeye @JvmField ile ek açıklama ekleriz:

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

UseCase.formatUser() düğmesini yeniden incelersek hataların düzeltildiğini görebiliriz.

@JvmField veya const

Böylece, UseCase.java dosyasında benzer benzer bir hata daha görünür:

Repository.saveAs(Repository.BACKUP_PATH);

Burada otomatik tamamlama özelliğini kullanırsak bir Repository.getBACKUP_PATH() olduğunu görebiliriz. Bu nedenle BACKUP_PATH sayfasındaki ek açıklamayı @JvmStatic yerine @JvmField olarak değiştirmek cazip olabilir.

Haydi deneyelim. Repository.kt uygulamasına geri dönün ve ek açıklamayı güncelleyin:

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

Şimdi UseCase.java sayfasına bakarsak hatanın ortadan kalktığını görürüz ancak BACKUP_PATH sitesinde de bir not vardır:

Kotlin'de const olabilen tek tür temel öğelerdir (int, float ve String gibi). Bu durumda, BACKUP_PATH bir dize olduğundan @JvmField değerine sahip bir val yerine const val kullanarak daha iyi performans elde edebilir ve değere bir alan olarak erişimi koruyabiliriz.

Repository.kt'te değiştirelim:

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

UseCase.java hesabına tekrar baktığımızda yalnızca bir hata kaldığını görebiliriz.

Son hata Exception: 'java.io.IOException' is never thrown in the corresponding try block. diyor

Repository.kt içinde Repository.saveAs koduna baktığımızda, bunun bir istisna yarattığını görürüz. Neler oluyor?

Java, "checked İstisna" kavramına sahiptir. Bu durumlar, kullanıcının dosya adını yanlış yazması veya ağın geçici olarak kullanılamaması gibi kurtarılabilecek istisnalardır. Kontrol edilen bir istisna tespit edildikten sonra geliştirici, kullanıcıya sorunun nasıl düzeltileceğiyle ilgili geri bildirim sağlayabilir.

İşaretlenen istisnalar derleme sırasında kontrol edildiğinden, bunları yöntemin imzasında belirtirsiniz:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

Diğer yandan Kotlin'in istisnaları yoktur ve sorunun nedeni de buradadır.

Bunun çözümü, Kotlin'den IOExceptionbu e-postanın Repository.saveAs() imzasına eklenmesini istemesidir. Böylece JVM bayt kodu bunu bir işaretli istisna olarak içerir.

Bunu Kotlin @Throws ek açıklamasıyla yaparız. Bu, Java/Kotlin birlikte çalışabilirliğine yardımcı olur. Kotlin örneklerinde istisnalar Java'ya benzer, ancak Java'dan farklı olarak Kotlin'de yalnızca işaretlenmemiş istisnalar vardır. Bu nedenle, Java kodunuza bir Kotlin işlevinin istisna yaptığını bildirmek istiyorsanız Kotlin işlevi imzası için @Throws ek açıklamasını Repository.kt file kullanın ve saveAs() ek açıklamasını yeni ek açıklamayı içerecek şekilde güncelleyin:

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

@Throws ek açıklaması kullanıldığında, UseCase.java'teki derleyici hatalarının hepsinin düzeltildiğini görebiliriz. Mükemmel!

Hemen Kotlin'den saveAs() numaralı telefonu ararken try ve catch bloklarını kullanmanız gerekip gerekmediğini merak edebilirsiniz.

Hayır. Kotlin'in istisnaları kontrol etmediğini ve bir yönteme @Throws eklemesinin bu durumu değiştirmediğini unutmayın:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

İlgilenmemiz gereken durumlarda istisnaları yakalamak yararlıdır ancak Kotlin sizi bunların üstesinden gelmeye zorlamaz.

Bu codelab'de, deyimsel Java kodunun yazılmasını da destekleyen Kotlin kodunun nasıl yazılacağıyla ilgili temel bilgileri ele aldık.

Kotlin'in JVM bayt kodunu oluşturma şeklini değiştirmek için ek açıklamaları nasıl kullanabileceğimizi konuştuk. Örneğin:

  • Statik üyeler ve yöntemler oluşturmak için @JvmStatic.
  • Varsayılan değerlere sahip işlevler için aşırı yüklenmiş yöntemler oluşturmak üzere @JvmOverloads.
  • Alıcı ve ayarlayıcıların adını değiştirmek için @JvmName.
  • @JvmField; bir mülkü, alan adı ve ayarlayıcılar yerine doğrudan bir alan olarak göstermek için.
  • İşaretli istisnaları bildirmek için @Throws.

Dosyalarımızın son içeriği şöyledir:

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Repository.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}