استدعاء رمز Kotlin من Java

في هذا الدرس العملي، ستتعرّف على كيفية كتابة رمز Kotlin أو تعديله ليكون قابلاً للاستدعاء بسلاسة أكبر من رمز Java.

ما ستتعرّف عليه

  • كيفية استخدام @JvmField و@JvmStatic والتعليقات التوضيحية الأخرى
  • قيود على الوصول إلى بعض ميزات لغة Kotlin من رمز Java

المعلومات التي يجب معرفتها

تمت كتابة هذا الدرس التطبيقي حول الترميز للمبرمجين، ويفترض أن لديك معرفة أساسية بلغتَي Java وKotlin.

يحاكي هذا الدرس التطبيقي حول الترميز عملية نقل جزء من مشروع أكبر مكتوب بلغة البرمجة Java، وذلك لدمج رمز Kotlin جديد.

لتسهيل الأمور، سيكون لدينا ملف .java واحد باسم UseCase.java، والذي سيمثّل قاعدة الرموز الحالية.

لنفترض أنّنا استبدلنا بعض الوظائف المكتوبة في الأصل بلغة Java بإصدار جديد مكتوب بلغة Kotlin، وعلينا إنهاء عملية الدمج.

استيراد المشروع

يمكن استنساخ رمز المشروع من مشروع GitHub هنا: GitHub

بدلاً من ذلك، يمكنك تنزيل المشروع واستخراجه من أرشيف مضغوط على الرابط التالي:

تنزيل ملف Zip

إذا كنت تستخدم IntelliJ IDEA، اختَر "استيراد المشروع".

إذا كنت تستخدم "استوديو Android"، اختَر "استيراد مشروع (Gradle وEclipse ADT وغير ذلك)".

لنفتح 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(...) هي نفسها: "لا يمكن الوصول إلى غير الثابت من سياق ثابت".

لنلقِ الآن نظرة على أحد ملفات Kotlin. افتح الملف Repository.kt.

نرى أنّ مستودعنا هو كائن فردي يتم تعريفه باستخدام الكلمة الرئيسية object. المشكلة هي أنّ Kotlin تنشئ مثيلاً ثابتًا داخل الفئة بدلاً من عرضه كسمات وطرق ثابتة.

على سبيل المثال، يمكن الإشارة إلى Repository.getNextGuestId() باستخدام Repository.INSTANCE.getNextGuestId()، ولكن هناك طريقة أفضل.

يمكننا جعل Kotlin تنشئ طرقًا وسمات ثابتة من خلال إضافة التعليقات التوضيحية إلى السمات والطرق العامة في Repository باستخدام @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، حوّلنا الطرق إلى دوال إضافية. لا تتضمّن Java دوال إضافة، لذا يجمّع Kotlin هذه الطرق كدوال ثابتة.

للأسف، إذا نظرنا إلى طريقة registerGuest() داخل UseCase.java، يمكننا أن نرى أنّ هناك خطأ:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

والسبب هو أنّ Kotlin تضع هذه الدوال "ذات المستوى الأعلى" أو دوال مستوى الحزمة داخل فئة يستند اسمها إلى اسم الملف. في هذه الحالة، وبما أنّ اسم الملف هو StringUtils.kt، يكون اسم الفئة المقابلة هو StringUtilsKt.

يمكننا تغيير جميع مراجع StringUtils إلى StringUtilsKt وإصلاح هذا الخطأ، ولكن هذا ليس الحلّ الأمثل للأسباب التالية:

  • قد يكون هناك العديد من المواضع في الرمز التي يجب تعديلها.
  • الاسم نفسه غير مألوف.

بدلاً من إعادة تصميم رمز Java، لنعدّل رمز Kotlin لاستخدام اسم مختلف لهذه الطرق.

افتح StringUtils.Kt وابحث عن تعريف الحزمة التالي:

package com.google.example.javafriendlykotlin

يمكننا إخبار Kotlin باستخدام اسم مختلف لطُرق مستوى الحزمة من خلال استخدام التعليق التوضيحي @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")
)

لا يمكن تنفيذ ذلك بالطريقة نفسها عند استدعاء الطريقة من Java.

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

لا تتوافق القيم التلقائية مع لغة البرمجة Java. لحلّ هذه المشكلة، سنطلب من 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");

يمكننا أيضًا إنشاء User من خلال تضمين مَعلمة ثالثة لـ displayName مع الاستمرار في استخدام القيمة التلقائية لـ 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

يشير ذلك إلى Kotlin لتغيير توقيع الدالة get المحدّدة بشكل صريح إلى الاسم المقدَّم.

بدلاً من ذلك، يمكن تطبيقها على السمة باستخدام البادئة get: على النحو التالي:

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

تكون الطريقة البديلة مفيدة بشكل خاص للسمات التي تستخدم دالة جلب تلقائية محددة ضمنيًا. على سبيل المثال:

@get:JvmName("isActive")
val active: Boolean

يتيح ذلك تغيير اسم الدالة بدون الحاجة إلى تحديد دالة getter بشكل صريح.

على الرغم من هذا التمييز، يمكنك استخدام أيّ منهما يناسبك أكثر. سيؤدي كل منهما إلى إنشاء دالة getter في Kotlin بالاسم hasSystemAccess().

إذا عدنا إلى UseCase.java، يمكننا التأكّد من أنّ getSystemUsers() لم يعُد يتضمّن أي أخطاء.

يحدث الخطأ التالي في formatUser()، ولكن إذا أردت الاطّلاع على مزيد من المعلومات حول اصطلاح تسمية دوال الحصول في Kotlin، يمكنك مواصلة القراءة هنا قبل الانتقال إلى الخطوة التالية.

تسمية دوال Getter وSetter

عند كتابة رمز Kotlin، من السهل أن ننسى أنّ كتابة رمز مثل:

val myString = "Logged in as ${user.displayName}")

يتم في الواقع استدعاء دالة للحصول على قيمة displayName. يمكننا التحقّق من ذلك من خلال الانتقال إلى الأدوات > Kotlin > عرض الرمز الثانوي لـ Kotlin في القائمة ثم النقر على الزر تفكيك:

String myString = "Logged in as " + user.getDisplayName();

عندما نريد الوصول إلى هذه القيم من Java، علينا كتابة اسم الدالة get بشكل صريح.

في معظم الحالات، يكون اسم Java الخاص بوظائف الحصول على قيم خصائص Kotlin هو get + اسم الخاصية، كما رأينا في User.getHasSystemAccess() وUser.getDisplayName(). الاستثناء الوحيد هو المواقع التي تبدأ أسماؤها بـ "is". في هذه الحالة، يكون اسم Java الخاص بدالة الجلب هو اسم السمة في Kotlin.

على سبيل المثال، موقع إلكتروني على User مثل:

val isAdmin get() = //...

يمكن الوصول إليه من Java باستخدام:

boolean userIsAnAdmin = user.isAdmin();

باستخدام التعليق التوضيحي @JvmName، تنشئ لغة Kotlin رمزًا بايتًا يتضمّن الاسم المحدّد للعنصر الذي يتمّ وضع التعليق التوضيحي عليه، بدلاً من الاسم التلقائي.

وينطبق الأمر نفسه على دوال الضبط، التي تكون أسماؤها التي يتم إنشاؤها دائمًا 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
}

من Java، سنتمكّن بعد ذلك من كتابة ما يلي:

color.updateRed(0.8f);

تستخدم UseCase.formatUser() إذن الوصول المباشر إلى الحقل للحصول على قيم خصائص عنصر User.

في Kotlin، يتم عادةً عرض السمات من خلال دوال الجلب والضبط. ويشمل ذلك مواقع val.

يمكن تغيير هذا السلوك باستخدام التعليق التوضيحي @JvmField. عند تطبيق ذلك على سمة في فئة، سيتخطّى Kotlin إنشاء طرق getter (وsetter لسمات 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، نرى أنّه يعرض استثناءً. ما هي المشكلة؟

تتضمّن Java مفهوم "الاستثناء الذي تم التحقّق منه". هذه هي الاستثناءات التي يمكن استردادها، مثل أن يخطئ المستخدم في كتابة اسم ملف، أو أن تكون الشبكة غير متاحة مؤقتًا. بعد رصد استثناء تم التحقّق منه، يمكن للمطوّر تقديم ملاحظات للمستخدم حول كيفية حل المشكلة.

بما أنّه يتم التحقّق من الاستثناءات التي تم التحقّق منها في وقت الترجمة البرمجية، عليك الإفصاح عنها في توقيع الطريقة:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

في المقابل، لا تتضمّن لغة Kotlin استثناءات تم التحقّق منها، وهذا هو سبب المشكلة هنا.

الحلّ هو أن تطلب من Kotlin إضافة IOException التي من المحتمل أن يتم طرحها إلى توقيع Repository.saveAs()، حتى يتضمّن رمز JVM البايت ذلك كاستثناء تم التحقّق منه.

ونفعل ذلك باستخدام التعليق التوضيحي @Throws في Kotlin، ما يساعد في إمكانية التشغيل التفاعلي بين Java وKotlin. في Kotlin، تتشابه الاستثناءات مع Java، ولكن على عكس Java، لا تتضمّن Kotlin سوى استثناءات غير خاضعة للتحقّق. لذلك، إذا أردت إعلام رمز Java بأنّ دالة 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. رائع!

قد تتساءل عمّا إذا كان عليك استخدام كتلتَي try وcatch عند استدعاء saveAs() من Kotlin الآن.

لا. تذكَّر أنّ لغة Kotlin لا تتضمّن استثناءات يتم التحقّق منها، وأنّ إضافة @Throws إلى إحدى الطرق لا يغيّر ذلك:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

يظل من المفيد رصد الاستثناءات عندما يمكن التعامل معها، ولكن لا يفرض عليك Kotlin التعامل معها.

في هذا الدرس التطبيقي حول الترميز، تناولنا الأساسيات المتعلقة بكيفية كتابة رمز Kotlin البرمجي الذي يتيح أيضًا كتابة رمز Java البرمجي الاصطلاحي.

تحدثنا عن كيفية استخدام التعليقات التوضيحية لتغيير طريقة إنشاء 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
}