در این لبه کد، یاد خواهید گرفت که چگونه کد Kotlin خود را بنویسید یا تطبیق دهید تا آن را به طور یکپارچه از کد جاوا فراخوانی کنید.
چیزی که یاد خواهید گرفت
- نحوه استفاده از
@JvmField
،@JvmStatic
، و سایر حاشیه نویسی ها. - محدودیتهای دسترسی به برخی ویژگیهای زبان Kotlin از کد جاوا.
آنچه شما باید از قبل بدانید
این کد لبه برای برنامه نویسان نوشته شده است و دانش پایه جاوا و کاتلین را فرض می کند.
این آزمایشگاه کد مهاجرت بخشی از یک پروژه بزرگتر که با زبان برنامه نویسی جاوا نوشته شده است را شبیه سازی می کند تا کدهای جدید Kotlin را در خود جای دهد.
برای ساده کردن کارها، ما یک فایل .java
به نام UseCase.java
خواهیم داشت که نشان دهنده پایگاه کد موجود است.
ما تصور می کنیم که برخی از قابلیت هایی که در ابتدا در جاوا نوشته شده بود را با نسخه جدیدی که در Kotlin نوشته شده بود جایگزین کردیم و باید یکپارچه سازی آن را به پایان برسانیم.
پروژه را وارد کنید
کد پروژه را می توان از پروژه GitHub در اینجا کلون کرد: GitHub
همچنین، میتوانید پروژه را از یک آرشیو فشرده که در اینجا یافت میشود دانلود و استخراج کنید:
اگر از IntelliJ IDEA استفاده می کنید، "Import Project" را انتخاب کنید.
اگر از Android Studio استفاده می کنید، "Import project (Gradle, Eclipse ADT, etc.)" را انتخاب کنید.
بیایید UseCase.java
باز کنیم و از طریق خطاهایی که می بینیم شروع به کار کنیم.
اولین تابعی که مشکل دارد registerGuest
است:
public static User registerGuest(String name) {
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
Repository.addUser(guest);
return guest;
}
خطاهای Repository.getNextGuestId()
و Repository.addUser(...)
یکسان هستند: "Non-static را نمی توان از یک زمینه ثابت دسترسی داشت."
حال بیایید نگاهی به یکی از فایل های کاتلین بیندازیم. فایل Repository.kt
را باز کنید.
می بینیم که Repository ما یک تک تن است که با استفاده از کلمه کلیدی شی اعلان می شود. مشکل این است که کاتلین در حال تولید یک نمونه ایستا در داخل کلاس ما است، نه اینکه آنها را به عنوان ویژگی ها و روش های ایستا نشان دهد.
به عنوان مثال، Repository.getNextGuestId()
می توان با استفاده از Repository.INSTANCE.getNextGuestId()
ارجاع داد، اما راه بهتری وجود دارد.
میتوانیم کاتلین را برای تولید متدها و ویژگیهای استاتیک با حاشیهنویسی ویژگیها و متدهای عمومی مخزن با @JvmStatic
دریافت کنیم:
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)
}
}
حاشیه نویسی @JvmStatic را با استفاده از IDE خود به کد خود اضافه کنید.
اگر به UseCase.java
برگردیم، ویژگیها و روشهای موجود در Repository
، به جز Repository.BACKUP_PATH
، دیگر باعث خطا نمیشوند. بعداً به آن باز خواهیم گشت.
در حال حاضر، بیایید خطای بعدی را در متد registerGuest()
برطرف کنیم.
بیایید سناریوی زیر را در نظر بگیریم: ما یک کلاس StringUtils
با چندین تابع ثابت برای عملیات رشته داشتیم. وقتی آن را به Kotlin تبدیل کردیم، متدها را به توابع افزونه تبدیل کردیم. جاوا توابع پسوندی ندارد، بنابراین کاتلین این روش ها را به عنوان توابع استاتیک کامپایل می کند.
متأسفانه، اگر به متد registerGuest()
در داخل UseCase.java
نگاه کنیم، می بینیم که چیزی کاملاً درست نیست:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
دلیل آن این است که Kotlin این توابع "سطح بالا" یا بسته را در داخل کلاسی قرار می دهد که نام آن بر اساس نام فایل است. در این حالت، چون فایل StringUtils.kt نام دارد، کلاس مربوطه StringUtilsKt
نام دارد.
ما میتوانیم تمام مراجع StringUtils
خود را به StringUtilsKt
تغییر دهیم و این خطا را برطرف کنیم، اما این ایدهآل نیست زیرا:
- ممکن است مکان های زیادی در کد ما وجود داشته باشد که باید به روز شوند.
- نام خود ناخوشایند است.
بنابراین به جای اینکه کد جاوا خود را تغییر دهیم، بیایید کد Kotlin خود را به روز کنیم تا از نام دیگری برای این روش ها استفاده کنیم.
StringUtils.Kt
را باز کنید و اعلان بسته زیر را پیدا کنید:
package com.google.example.javafriendlykotlin
با استفاده از حاشیهنویسی @file:JvmName
میتوانیم به Kotlin بگوییم که از نام دیگری برای متدهای سطح بسته استفاده کند. بیایید از این حاشیهنویسی برای نامگذاری کلاس StringUtils
استفاده کنیم.
@file:JvmName("StringUtils")
package com.google.example.javafriendlykotlin
اکنون، اگر به UseCase.java
نگاه کنیم، می بینیم که خطای StringUtils.nameToLogin()
برطرف شده است.
متأسفانه، این خطا با یک خطای جدید در مورد پارامترهای ارسال شده به سازنده برای User
جایگزین شد. بیایید به مرحله بعدی ادامه دهیم و این آخرین خطا را در UseCase.registerGuest()
برطرف کنیم.
Kotlin از مقادیر پیش فرض پارامترها پشتیبانی می کند. با نگاه کردن به داخل بلوک init
Repository.kt
می توانیم نحوه استفاده از آنها را ببینیم.
Repository.kt:
_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
میتوانیم ببینیم که برای کاربر "warlow"، میتوانیم از قرار دادن یک مقدار برای displayName
صرفنظر کنیم زیرا یک مقدار پیشفرض برای آن در User.kt
مشخص شده است.
User.kt:
data class User(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
متأسفانه هنگام فراخوانی متد از جاوا، این کار یکسان نیست.
UseCase.java:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
مقادیر پیشفرض در زبان برنامهنویسی جاوا پشتیبانی نمیشوند. برای رفع این مشکل، اجازه دهید به Kotlin بگوییم که با کمک حاشیه نویسی @JvmOverloads برای سازنده ما اضافه بار تولید کند.
ابتدا باید بهروزرسانی مختصری در User.kt
انجام دهیم.
از آنجایی که کلاس User
فقط یک سازنده اصلی دارد و سازنده هیچ توضیحی را شامل نمی شود، کلمه کلیدی constructor
حذف شده است. با این حال، اکنون که میخواهیم آن را حاشیهنویسی کنیم، کلمه کلیدی constructor
باید گنجانده شود:
data class User constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
با وجود کلمه کلیدی constructor
، می توانیم حاشیه نویسی @JvmOverloads
را اضافه کنیم:
data class User @JvmOverloads constructor(
val id: Int,
val username: String,
val displayName: String = username.toTitleCase(),
val groups: List<String> = listOf("guest")
)
اگر به UseCase.java
برگردیم، می بینیم که هیچ خطایی در تابع registerGuest
وجود ندارد!
گام بعدی ما رفع تماس شکسته به user.hasSystemAccess()
در UseCase.getSystemUsers()
است. برای آن به مرحله بعدی ادامه دهید یا به خواندن ادامه دهید تا به آنچه که @JvmOverloads
برای رفع خطا انجام داده است عمیقتر کنید.
@JvmOverloads
برای درک بهتر کاری که @JvmOverloads
انجام می دهد، بیایید یک روش تست در UseCase.java
ایجاد کنیم:
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);
}
ما می توانیم یک User
تنها با دو پارامتر، id
و username
بسازیم:
User syrinx = new User(1001, "syrinx");
همچنین میتوانیم با اضافه کردن پارامتر سوم برای displayName
یک User
بسازیم، در حالی که همچنان از مقدار پیشفرض برای groups
استفاده میکنیم:
User ione = new User(1002, "ione", "Ione Saldana");
اما نمی توان از displayName
رد شد و فقط یک مقدار برای groups
بدون نوشتن کد اضافی ارائه کرد:
پس بیایید آن خط را حذف کنیم یا با '//' پیشگفتار کنیم تا نظر بدهیم.
در Kotlin، اگر بخواهیم پارامترهای پیش فرض و غیر پیش فرض را با هم ترکیب کنیم، باید از پارامترهای نامگذاری شده استفاده کنیم.
// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))
دلیل آن این است که کاتلین برای توابع، از جمله سازنده ها، اضافه بار ایجاد می کند، اما تنها یک بار اضافه بار برای هر پارامتر با مقدار پیش فرض ایجاد می کند.
بیایید به UseCase.java
نگاه کنیم و مشکل بعدی خود را حل کنیم: فراخوانی به user.hasSystemAccess()
در متد UseCase.getSystemUsers()
:
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;
}
این یک خطای جالب است! اگر از ویژگی تکمیل خودکار IDE خود در کلاس User
استفاده کنید، متوجه خواهید شد که نام hasSystemAccess()
به getHasSystemAccess()
تغییر یافته است.
برای رفع مشکل، میخواهیم Kotlin نام دیگری برای ویژگی val
hasSystemAccess
ایجاد کند. برای این کار می توانیم از حاشیه نویسی @JvmName
استفاده کنیم. بیایید به User.kt
برگردیم و ببینیم کجا باید آن را اعمال کنیم.
دو روش وجود دارد که می توانیم حاشیه نویسی را اعمال کنیم. اولین مورد این است که آن را مستقیماً روی متد get()
اعمال کنید، مانند این:
val hasSystemAccess
@JvmName("hasSystemAccess")
get() = "sys" in groups
این به کاتلین سیگنال می دهد تا امضای گیرنده مشخص شده را به نام ارائه شده تغییر دهد.
از طرف دیگر، می توان آن را با استفاده از یک پیشوند get:
به این ویژگی اعمال کرد:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
روش جایگزین به ویژه برای خواصی که از یک گیرنده پیش فرض و به طور ضمنی تعریف شده استفاده می کنند مفید است. به عنوان مثال:
@get:JvmName("isActive")
val active: Boolean
این اجازه می دهد تا نام گیرنده بدون نیاز به تعریف صریح یک گیرنده تغییر کند.
با وجود این تمایز، می توانید از هر کدام که برایتان بهتر است استفاده کنید. هر دو باعث می شوند کاتلین یک گیرنده با نام hasSystemAccess()
ایجاد کند.
اگر به UseCase.java
برگردیم، می توانیم تأیید کنیم که getSystemUsers()
اکنون بدون خطا است!
خطای بعدی در formatUser()
است، اما اگر میخواهید درباره قرارداد نامگذاری Kotlin getter بیشتر بخوانید، قبل از رفتن به مرحله بعدی به خواندن اینجا ادامه دهید.
نامگذاری گیرنده و تنظیم کننده
هنگامی که ما در حال نوشتن Kotlin هستیم، به راحتی میتوان نوشتن کدهایی مانند:
val myString = "Logged in as ${user.displayName}")
در واقع یک تابع را برای بدست آوردن مقدار displayName
فراخوانی می کند. با رفتن به Tools > Kotlin > Show Kotlin Bytecode در منو و سپس کلیک کردن روی دکمه Decompile میتوانیم این موضوع را تأیید کنیم:
String myString = "Logged in as " + user.getDisplayName();
وقتی میخواهیم از جاوا به اینها دسترسی پیدا کنیم، باید به صراحت نام گیرنده را بنویسیم.
در اکثر موارد، نام جاوای دریافتکنندهها برای ویژگیهای Kotlin به سادگی get
+ نام ویژگی است، همانطور که با User.getHasSystemAccess()
و User.getDisplayName()
دیدیم. تنها استثنا، خواصی است که نام آنها با "is" شروع می شود. در این مورد، نام جاوا برای دریافت کننده، نام ویژگی Kotlin است.
به عنوان مثال، یک ویژگی در User
مانند:
val isAdmin get() = //...
از جاوا با:
boolean userIsAnAdmin = user.isAdmin();
با استفاده از حاشیهنویسی @JvmName
، کاتلین بایت کدی را تولید میکند که به جای نام پیشفرض، نام مشخصشده را برای مورد در حال حاشیهنویسی دارد.
این کار برای تنظیمکنندهها که نامهای تولید شده آنها همیشه set
+ نام خصوصیت است، یکسان است. به عنوان مثال، کلاس زیر را انتخاب کنید:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
بیایید تصور کنیم که میخواهیم نام تنظیمکننده را از setRed()
به updateRed()
تغییر دهیم، در حالی که گیرندگان را به حال خود رها کنیم. برای انجام این کار می توانیم از نسخه @set:JvmName
استفاده کنیم:
class Color {
@set:JvmName("updateRed")
var red = 0f
@set:JvmName("updateGreen")
var green = 0f
@set:JvmName("updateBlue")
var blue = 0f
}
از جاوا، ما می توانیم بنویسیم:
color.updateRed(0.8f);
UseCase.formatUser()
از دسترسی مستقیم فیلد برای بدست آوردن مقادیر خصوصیات یک شی User
استفاده می کند.
در کاتلین، خواص معمولاً از طریق گترها و ستترها در معرض دید قرار می گیرند. این شامل خواص val
می شود.
تغییر این رفتار با استفاده از حاشیه نویسی @JvmField
امکان پذیر است. هنگامی که این مورد بر روی یک ویژگی در یک کلاس اعمال می شود، کاتلین از تولید متدهای دریافت کننده (و تنظیم کننده برای ویژگی های var
) صرفنظر می کند و می توان به فیلد پشتیبان مستقیماً دسترسی داشت.
از آنجایی که اشیاء User
تغییر ناپذیر هستند، ما می خواهیم هر یک از ویژگی های آنها را به عنوان فیلد در معرض نمایش بگذاریم، و بنابراین هر یک از آنها را با @JvmField
حاشیه نویسی می کنیم:
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()
نگاه کنیم، می بینیم که خطاها برطرف شده اند!
@JvmField یا const
با آن، یک خطای مشابه دیگر در فایل UseCase.java
وجود دارد:
Repository.saveAs(Repository.BACKUP_PATH);
اگر در اینجا از تکمیل خودکار استفاده کنیم، میتوانیم ببینیم که یک Repository.getBACKUP_PATH()
وجود دارد، و بنابراین ممکن است تغییر حاشیهنویسی در BACKUP_PATH
از @JvmStatic
به @JvmField
وسوسهانگیز باشد.
بیایید این را امتحان کنیم. به Repository.kt
برگردید و حاشیه نویسی را به روز کنید:
object Repository {
@JvmField
val BACKUP_PATH = "/backup/user.repo"
اگر اکنون به UseCase.java
نگاه کنیم، می بینیم که خطا برطرف شده است، اما یک یادداشت نیز در BACKUP_PATH
وجود دارد:
در Kotlin، تنها انواعی که می توانند const
باشند، اولیه هستند، مانند int
، float
و String
. در این مورد، چون BACKUP_PATH
یک رشته است، میتوانیم با استفاده از const val
به جای val
که با @JvmField
حاشیهنویسی شده است، عملکرد بهتری داشته باشیم، در حالی که توانایی دسترسی به مقدار را به عنوان یک فیلد حفظ میکنیم.
بیایید آن را اکنون در Repository.kt تغییر دهیم:
object Repository {
const val BACKUP_PATH = "/backup/user.repo"
اگر به UseCase.java
برگردیم، می بینیم که تنها یک خطا باقی مانده است.
خطای نهایی می گوید Exception: 'java.io.IOException' is never thrown in the corresponding try block.
اگر به کد Repository.saveAs
در Repository.kt
نگاه کنیم، می بینیم که یک استثنا ایجاد می کند. چه خبر است؟
جاوا مفهوم "استثنای بررسی شده" را دارد. اینها استثناهایی هستند که می توانند از آنها بازیابی شوند، مانند اشتباه تایپ نام فایل توسط کاربر، یا در دسترس نبودن موقت شبکه. پس از مشخص شدن یک استثنای علامتگذاری شده، توسعهدهنده میتواند در مورد نحوه رفع مشکل به کاربر بازخورد ارائه کند.
از آنجایی که استثناهای بررسی شده در زمان کامپایل بررسی می شوند، آنها را در امضای متد اعلام می کنید:
public void openFile(File file) throws FileNotFoundException {
// ...
}
از طرف دیگر، کاتلین استثناهای بررسی شده ای ندارد و این همان چیزی است که باعث ایجاد مشکل در اینجا می شود.
راه حل این است که از Kotlin بخواهیم IOException
که به طور بالقوه به امضای Repository.saveAs()
پرتاب می شود، اضافه کند، به طوری که بایت کد JVM آن را به عنوان یک استثناء بررسی شده شامل شود.
ما این کار را با حاشیه نویسی Kotlin @Throws
انجام می دهیم که به قابلیت همکاری جاوا/کوتلین کمک می کند. در کاتلین، استثناها شبیه جاوا عمل می کنند، اما برخلاف جاوا، کاتلین فقط دارای استثناهای بدون علامت است. بنابراین اگر میخواهید به کد جاوا خود اطلاع دهید که یک تابع Kotlin یک استثنا ایجاد میکند، باید از حاشیهنویسی @Throws به امضای تابع Kotlin استفاده کنید و به Repository.kt file
بروید و saveAs()
برای اضافه کردن حاشیهنویسی جدید بهروزرسانی کنید:
@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
، می توانیم ببینیم که تمام خطاهای کامپایلر در UseCase.java
رفع شده اند! هورا!
ممکن است تعجب کنید که آیا اکنون هنگام فراخوانی saveAs()
از Kotlin باید از بلوکهای try
and catch
استفاده کنید.
نه! به یاد داشته باشید، Kotlin استثناهای علامتدار ندارد و افزودن @Throws
به یک متد، این را تغییر نمیدهد:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
هنوز هم مفید است که استثناها را زمانی که می توان آنها را مدیریت کرد، اما Kotlin شما را مجبور به رسیدگی به آنها نمی کند.
در این نرمافزار، اصول اولیه نحوه نوشتن کد Kotlin را که از نوشتن کد اصطلاحی جاوا نیز پشتیبانی میکند، توضیح دادیم.
ما در مورد اینکه چگونه می توانیم از حاشیه نویسی برای تغییر روش تولید بایت کد JVM کاتلین استفاده کنیم، صحبت کردیم، مانند:
-
@JvmStatic
برای تولید اعضا و متدهای ثابت. -
@JvmOverloads
برای تولید متدهای بارگذاری شده برای توابعی که مقادیر پیش فرض دارند. -
@JvmName
برای تغییر نام گیرنده ها و تنظیم کننده ها. -
@JvmField
برای نمایش یک ویژگی مستقیماً به عنوان یک فیلد، نه از طریق گیرنده و تنظیم کننده. -
@Throws
برای اعلام استثناهای علامتدار.
محتوای نهایی فایل های ما عبارتند از:
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
}