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:
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 IOException
bu 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
}