فراخوانی کد Kotlin از جاوا

در این لبه کد، شما یاد خواهید گرفت که چگونه کد 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
}