در این لبه کد، شما یاد خواهید گرفت که چگونه کد Kotlin خود را بنویسید یا تطبیق دهید تا آن را به طور یکپارچه از کد جاوا فراخوانی کنید.
چیزی که یاد خواهید گرفت
- نحوه استفاده از
@JvmField
،@JvmStatic
، و سایر حاشیه نویسی ها. - محدودیتهای دسترسی به برخی ویژگیهای زبان 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
میتوانیم به کاتلین بگوییم که از نام دیگری برای متدهای سطح بسته استفاده کند. بیایید از این حاشیهنویسی برای نامگذاری کلاس 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"))
دلیل آن این است که Kotlin برای توابع، از جمله سازنده ها، اضافه بار ایجاد می کند، اما تنها یک بار اضافه بار برای هر پارامتر با مقدار پیش فرض ایجاد می کند.
بیایید به 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
استفاده کنید.
جواب منفی! به یاد داشته باشید، کاتلین استثناهای تیک دار ندارد، و افزودن @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
}