Bu codelab'de, Kotlin kodunuzu Java kodundan daha sorunsuz bir şekilde çağrılabilir hale getirmek için nasıl yazacağınızı veya uyarlayacağınızı öğreneceksiniz.
Neler öğreneceksiniz?
@JvmField
,@JvmStatic
ve diğer ek açıklamaları kullanma- Java kodundan belirli Kotlin dili özelliklerine erişmeyle ilgili sınırlamalar.
Önceden bilmeniz gerekenler
Bu kod laboratuvarı, programcılar için yazılmıştır ve temel Java ile Kotlin bilgisi gerektirir.
Bu codelab, Java programlama diliyle yazılmış daha büyük bir projenin bir kısmını yeni Kotlin kodunu içerecek şekilde taşımayı simüle eder.
İşleri basitleştirmek için mevcut kod tabanını temsil eden UseCase.java
adlı tek bir .java
dosyamız olacak.
Başlangıçta Java ile yazılmış bazı işlevleri Kotlin ile yazılmış yeni bir sürümle değiştirdiğimizi ve entegrasyonu tamamlamamız gerektiğini varsayalım.
Projeyi içe aktarma
Projenin kodu, GitHub projesinden klonlanabilir: GitHub
Alternatif olarak, projeyi buradan bulabileceğiniz bir ZIP arşivinden indirip çıkarabilirsiniz:
IntelliJ IDEA kullanıyorsanız "Import Project"i (Projeyi İçe Aktar) seçin.
Android Studio kullanıyorsanız "Projeyi içe aktar (Gradle, Eclipse ADT vb.)" seçeneğini belirleyin.
UseCase.java
dosyasını açıp gördüğümüz hataları düzeltmeye başlayalım.
Sorun olan 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;
}
Hem Repository.getNextGuestId()
hem de Repository.addUser(...)
için aynı hata gösteriliyor: "Non-static cannot be accessed from a static context." ("Statik olmayan bir bağlamdan statik bağlama erişilemiyor.")
Şimdi Kotlin dosyalarından birine göz atalım. Dosyayı açın Repository.kt
.
Veri havuzumuzun, object anahtar kelimesi kullanılarak tekil olarak tanımlandığını görüyoruz. Sorun, Kotlin'in bunları statik özellikler ve yöntemler olarak kullanıma sunmak yerine sınıfımızda statik bir örnek oluşturmasıdır.
Örneğin, Repository.getNextGuestId()
, Repository.INSTANCE.getNextGuestId()
kullanılarak referans verilebilir ancak daha iyi bir yol vardır.
Deponun herkese açık özelliklerini ve yöntemlerini @JvmStatic
ile açıklama ekleyerek Kotlin'in 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ını ekleyin.
UseCase.java
'ya geri dönersek Repository.BACKUP_PATH
hariç Repository
'deki özellikler ve yöntemler artık hataya neden olmaz. Bu konuya daha sonra tekrar değineceğiz.
Şimdilik registerGuest()
yöntemindeki bir sonraki hatayı düzeltelim.
Şu senaryoyu ele alalım: Dize işlemleri için çeşitli statik işlevlere sahip bir StringUtils
sınıfımız var. Kotlin'e dönüştürdüğümüzde yöntemleri uzantı işlevlerine dönüştürdük. Java'da uzantı işlevleri olmadığından Kotlin bu yöntemleri statik işlevler olarak derler.
Maalesef UseCase.java
içindeki registerGuest()
yöntemine baktığımızda bir şeylerin doğru olmadığını görüyoruz:
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ı bir sınıfın içine yerleştirmesidir. Bu durumda, dosyanın adı StringUtils.kt olduğundan ilgili sınıfın adı StringUtilsKt
olur.
StringUtils
ile ilgili tüm referanslarımızı StringUtilsKt
olarak değiştirip bu hatayı düzeltebiliriz ancak bu çözüm şu nedenlerle ideal değildir:
- Kodumuzda güncellenmesi gereken birçok yer olabilir.
- Adın kendisi garip.
Bu nedenle, Java kodumuzu yeniden düzenlemek yerine Kotlin kodumuzu güncelleyerek bu yöntemler için farklı bir ad kullanalım.
StringUtils.Kt
dosyasını açın ve aşağıdaki paket bildirimini bulun:
package com.google.example.javafriendlykotlin
@file:JvmName
ek açıklamasını kullanarak Kotlin'e paket düzeyindeki yöntemler için farklı bir ad kullanmasını söyleyebiliriz. Sınıfı StringUtils
olarak adlandırmak için bu açıklamayı kullanalım.
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
Şimdi UseCase.java
'ya geri dönersek StringUtils.nameToLogin()
ile ilgili hatanın çözüldüğünü görebiliriz.
Maalesef bu hata, User
için oluşturucuya iletilen parametrelerle ilgili yeni bir hatayla değiştirildi. Bir sonraki adıma geçelim ve UseCase.registerGuest()
içindeki bu son hatayı düzeltelim.
Kotlin, parametreler için varsayılan değerleri destekler. Bu özelliklerin nasıl kullanıldığını init
Repository.kt
bloğunun içine bakarak görebiliriz.
Repository.kt:
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
"warlow" kullanıcısı için displayName
alanına değer girmenin atlanabileceğini görüyoruz. Bunun nedeni, User.kt
içinde bu alan için varsayılan bir değer belirtilmiş olmasıdır.
User.kt:
data class User(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
Maalesef bu durum, yöntem Java'dan çağrıldığında 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 @JvmOverloads ek açıklaması yardımıyla Kotlin'e oluşturucumuz için aşırı yüklemeler oluşturmasını söyleyelim.
Öncelikle User.kt
uygulamasında küçük bir güncelleme yapmamız gerekiyor.
User
sınıfında yalnızca tek bir birincil oluşturucu olduğundan ve oluşturucu herhangi bir ek açıklama içermediğinden constructor
anahtar kelimesi çıkarılmıştı. Ancak şimdi bu öğeye açıklama eklemek istediğimiz için constructor
anahtar kelimesi eklenmelidir:
data class User constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
constructor
anahtar kelimesi mevcutken @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
'ya geri döndüğümüzde registerGuest
işlevinde başka hata olmadığını görüyoruz.
Bir sonraki adımımız, user.hasSystemAccess()
içinde bozuk olan UseCase.getSystemUsers()
işlem çağrısını düzeltmektir. Bunun için sonraki adıma geçin veya @JvmOverloads
'ın hatayı düzeltmek için neler yaptığını daha ayrıntılı öğrenmek üzere okumaya devam edin.
@JvmOverloads
@JvmOverloads
işlevini daha iyi anlamak için UseCase.java
içinde 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);
}
Yalnızca iki parametreyle (id
ve username
) User
oluşturabiliriz:
User syrinx = new User(1001, "syrinx");
User
için üçüncü bir parametre ekleyerek displayName
oluşturabiliriz. Bu durumda groups
için varsayılan değer kullanılmaya devam eder:
User ione = new User(1002, "ione", "Ione Saldana");
Ancak displayName
atlanıp ek kod yazmadan yalnızca groups
için değer sağlamak mümkün değildir:
Bu nedenle, bu satırı silelim veya yorum satırı yapmak için 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 oluşturucular da dahil olmak üzere işlevler için aşırı yüklemeler oluşturması ancak varsayılan değeri olan parametre başına yalnızca bir aşırı yükleme oluşturmasıdır.
UseCase.java
'ya tekrar bakalım ve bir sonraki sorunumuzu ele alalım: UseCase.getSystemUsers()
yönteminde user.hasSystemAccess()
çağrısı:
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. IDE'nizin otomatik tamamlama özelliğini User
sınıfında kullanırsanız hasSystemAccess()
sınıfının adının getHasSystemAccess()
olarak değiştirildiğini görürsünüz.
Sorunu düzeltmek için Kotlin'in val
özelliği hasSystemAccess
için farklı bir ad oluşturmasını istiyoruz. Bunu yapmak için @JvmName
ek açıklamasını kullanabiliriz. Şimdi User.kt
'ya geri dönelim ve bu işlevi nerede kullanmamız gerektiğini görelim.
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, Kotlin'e açıkça tanımlanmış alıcının imzasını sağlanan adla değiştirmesini bildirir.
Alternatif olarak, get:
gibi bir önek kullanarak özelliği mülke uygulayabilirsiniz:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
Alternatif yöntem, özellikle varsayılan ve örtülü olarak tanımlanmış bir alıcı kullanan özellikler için yararlıdır. Örneğin:
@get:JvmName("isActive")
val active: Boolean
Bu sayede, getter'ı açıkça tanımlamaya gerek kalmadan getter'ın adı değiştirilebilir.
Bu ayrıma rağmen, hangisi size daha iyi geliyorsa onu kullanabilirsiniz. Her ikisi de Kotlin'in hasSystemAccess()
adında bir alıcı oluşturmasına neden olur.
UseCase.java
'ya geri dönersek getSystemUsers()
'nin artık hatasız olduğunu doğrulayabiliriz.
Bir sonraki hata formatUser()
içinde yer alıyor. Ancak Kotlin getter adlandırma kuralı hakkında daha fazla bilgi edinmek istiyorsanız bir sonraki adıma geçmeden önce buradan okumaya devam edin.
Getter ve Setter Adlandırma
Kotlin yazarken aşağıdaki gibi kodlar yazmanın:
val myString = "Logged in as ${user.displayName}")
displayName
değerini almak için aslında bir işlevi çağırır. Menüde Tools > Kotlin > Show Kotlin Bytecode'a (Araçlar > Kotlin > Kotlin Bytecode'u Göster) gidip Decompile (Derlemeyi Kaldır) düğmesini tıklayarak bunu doğrulayabiliriz:
String myString = "Logged in as " + user.getDisplayName();
Bunlara Java'dan erişmek istediğimizde getter'ın adını açıkça yazmamız gerekir.
Çoğu durumda, Kotlin özelliklerinin alıcılarının Java adı, User.getHasSystemAccess()
ve User.getDisplayName()
'de gördüğümüz gibi yalnızca get
+ özellik adıdır. Bunun tek istisnası, adları "is" ile başlayan mülklerdir. Bu durumda, getter'ın Java adı, Kotlin özelliğinin adıdır.
Örneğin, User
üzerindeki bir mülk:
val isAdmin get() = //...
Java'dan şu şekilde erişilir:
boolean userIsAnAdmin = user.isAdmin();
Kotlin, @JvmName
ek açıklamasını kullanarak ek açıklama eklenen öğe için varsayılan ad yerine belirtilen ada sahip bir bayt kodu oluşturur.
Bu durum, oluşturulan adları her zaman set
+ özellik adı olan ayarlayıcılar için de geçerlidir. Örneğin, aşağıdaki sınıfı ele alalım:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
Getter'ları değiştirmeden setter adını setRed()
olarak değiştirmek istediğimizi varsayalım.updateRed()
Bunu yapmak 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'da şu kodu yazabiliriz:
color.updateRed(0.8f);
UseCase.formatUser()
, User
nesnesinin özelliklerinin değerlerini almak için doğrudan alan erişimini kullanır.
Kotlin'de özellikler normalde getter'lar ve setter'lar aracılığıyla kullanıma sunulur. val
özellikleri bu kapsamdadır.
Bu davranışı @JvmField
ek açıklamasını kullanarak değiştirebilirsiniz. Bu, bir sınıftaki bir özelliğe uygulandığında Kotlin, getter (ve var
özellikleri için setter) yöntemleri oluşturmayı atlar ve destek alanı doğrudan erişilebilir.
User
nesneleri değişmez olduğundan her bir özelliğini alan olarak kullanıma sunmak istiyoruz. Bu nedenle, her birini @JvmField
ile açıklama olarak ekleyeceğiz:
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
}
Şimdi UseCase.formatUser()
'ya tekrar baktığımızda hataların düzeltildiğini görüyoruz.
@JvmField veya const
Bununla birlikte, UseCase.java
dosyasında benzer görünümlü başka bir hata var:
Repository.saveAs(Repository.BACKUP_PATH);
Burada otomatik tamamlama özelliğini kullanırsak Repository.getBACKUP_PATH()
olduğunu görürüz. Bu nedenle, BACKUP_PATH
üzerindeki açıklamayı @JvmStatic
yerine @JvmField
olarak değiştirmek isteyebilirsiniz.
Şimdi bunu deneyelim. Repository.kt
planına geri dönün ve notu güncelleyin:
object Repository {
@JvmField
val BACKUP_PATH = "/backup/user.repo"
Şimdi UseCase.java
adresine bakarsak hatanın ortadan kalktığını ancak BACKUP_PATH
ile ilgili bir not olduğunu görürüz:
Kotlin'de yalnızca int
, float
ve String
gibi temel türler const
olabilir. Bu durumda, BACKUP_PATH
bir dize olduğundan, değeri alan olarak erişme özelliğini korurken @JvmField
ile açıklama eklenmiş bir val
yerine const val
kullanarak daha iyi performans elde edebiliriz.
Şimdi Repository.kt dosyasında bunu değiştirelim:
object Repository {
const val BACKUP_PATH = "/backup/user.repo"
UseCase.java
'ya tekrar baktığımızda yalnızca bir hata kaldığını görüyoruz.
Son hata mesajında Exception: 'java.io.IOException' is never thrown in the corresponding try block.
yazıyor.
Repository.kt
içindeki Repository.saveAs
koduna baktığımızda bir istisna oluşturduğunu görüyoruz. Neler oluyor?
Java'da "işaretli istisna" kavramı vardır. Bunlar, kullanıcının dosya adını yanlış yazması veya ağın geçici olarak kullanılamaması gibi kurtarılabilecek istisnalardır. Kontrollü bir istisna yakalandıktan sonra geliştirici, sorunun nasıl düzeltileceği konusunda kullanıcıya geri bildirimde bulunabilir.
Kontrollü istisnalar derleme zamanında kontrol edildiğinden bunları yöntemin imzasında bildirirsiniz:
public void openFile(File file) throws FileNotFoundException {
// ...
}
Ancak Kotlin'de kontrol edilen istisnalar yoktur ve bu durum burada soruna neden olmaktadır.
Çözüm olarak, Kotlin'den IOException
öğesini Repository.saveAs()
imzasına eklemesini isteyebilirsiniz. Böylece JVM bayt kodu, bunu kontrol edilen bir istisna olarak içerir.
Bunu, Java/Kotlin birlikte çalışabilirliğine yardımcı olan Kotlin @Throws
ek açıklamasıyla yapıyoruz. Kotlin'de istisnalar Java'ya benzer şekilde davranır ancak Java'dan farklı olarak Kotlin'de yalnızca kontrol edilmemiş istisnalar bulunur. Bu nedenle, Kotlin işlevinin bir istisna oluşturduğunu Java kodunuza bildirmek istiyorsanız Kotlin işlev imzasına @Throws ek açıklamasını kullanmanız gerekir. Repository.kt file
'a geçin ve saveAs()
'ı 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ıyla birlikte, UseCase.java
içindeki tüm derleyici hatalarının düzeltildiğini görebiliriz. Yaşasın!
Artık Kotlin'den saveAs()
işlevini çağırırken try
ve catch
bloklarını kullanmanız gerekip gerekmediğini merak ediyor olabilirsiniz.
Hayır. Kotlin'de kontrol edilen istisnalar olmadığını ve bir yönteme @Throws
eklemenin bunu değiştirmediğini unutmayın:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
İşlenebilecek istisnaları yakalamak yine de faydalıdır ancak Kotlin, bunları işlemenizi zorunlu kılmaz.
Bu kod laboratuvarında, deyimsel Java kodu yazmayı da destekleyen Kotlin kodu yazmayla 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ğimizden bahsettik. Örneğin:
@JvmStatic
statik üyeler ve yöntemler oluşturmak için kullanılır.- Varsayılan değerlere sahip işlevler için aşırı yüklenmiş yöntemler oluşturmak üzere
@JvmOverloads
. @JvmName
, alıcıların ve ayarlayıcıların adını değiştirmek için kullanılır.- Bir özelliği, getter ve setter'lar aracılığıyla değil doğrudan alan olarak kullanıma sunmak için
@JvmField
. @Throws
tuşlarına basarak kontrol edilen istisnaları bildirin.
Dosyalarımızın son içerikleri şunlardır:
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
}