في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية كتابة رمز 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
.
يتّضح لنا أن "مستودعنا" هو نسخة فردية مُعلَنة من خلال استخدام الكلمة الرئيسية للعنصر. وتكمن المشكلة في أن لغة Kotlin تُنشئ مثيلًا ثابتًا داخل صفنا، بدلاً من الكشف عنها على أنها خصائص وأساليب ثابتة.
على سبيل المثال، يمكن الإشارة إلى Repository.getNextGuestId()
باستخدام Repository.INSTANCE.getNextGuestId()
، ولكن هناك طريقة أفضل.
يمكننا الحصول على لغة Kotlin لإنشاء أساليب وخصائص ثابتة عن طريق إضافة تعليقات توضيحية إلى الخصائص والأساليب العامة للمستودع باستخدام @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 تضع هذه الدوال &"؛في المستوى الأعلى" أو على مستوى الحزمة داخل فئة يستند اسمها إلى اسم الملف. وفي هذه الحالة، يُطلق على الملف المطابق StringUtilsKt
باسم الملف StringUtils.kt.
يمكننا تغيير جميع مراجع StringUtils
إلى StringUtilsKt
وحلّ هذا الخطأ، ولكن ذلك ليس مثاليًا للأسباب التالية:
- قد تكون هناك مواضع متعددة في الرمز يجب تعديلها.
- فالاسم نفسه غريب.
لذا، بدلاً من إعادة إنشاء رمز جافا، لنحدّث رمز 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")
)
وللأسف، لا يعمل هذا بالطريقة نفسها عند استدعاء الطريقة من جافا.
UseCase.java:
User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
القيم التلقائية غير متاحة في لغة البرمجة Java. ولحلّ هذه المشكلة، عليك الطلب من Kotlin إنشاء أعباء تراكمية لإنشاء أداة الإنشاء باستخدام التعليق التوضيحي @JvmOverloads.
أولاً، يجب إجراء تحديث طفيف على User.kt
.
وقد تم حذف الكلمة الرئيسية constructor
لأنّ الفئة User
تتضمّن مُنشئ طريقة عرض أساسي واحدًا فقط، ولا تتضمّن طريقة الإنشاء أي تعليقات توضيحية. والآن وبما أننا نريد وضع تعليق توضيحي عليه، يجب تضمين الكلمة الرئيسية التي تخص 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:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
تُعد الطريقة البديلة مفيدة على وجه الخصوص للمواقع التي تستخدم إرجاع تلقائي محدد ضمنيًا. مثلاً:
@get:JvmName("isActive")
val active: Boolean
ويسمح ذلك بتغيير اسم الدالة getter&#s8d بدون الحاجة إلى تحديد إرجاع صريح.
وعلى الرغم من هذا التمييز، يمكنك استخدام ما يناسبك. وسيؤدي كلا الإجراءَين إلى إنشاء لغة Kotlin باسم hasSystemAccess()
.
إذا تم التبديل مرة أخرى إلى UseCase.java
، يمكننا التحقق من أن getSystemUsers()
الآن خالية من الأخطاء.
الخطأ التالي موجود في formatUser()
، ولكن إذا كنت تريد قراءة المزيد من المعلومات عن اصطلاح تسمية Kotlin، يمكنك مواصلة القراءة هنا قبل الانتقال إلى الخطوة التالية.
تسمية التقاط الصور
وعندما نكتب لغة Kotlin، فإنه من السهل أن ننسى رمز الكتابة مثل:
val myString = "Logged in as ${user.displayName}")
هو استدعاء دالة للحصول على قيمة displayName
. يمكنك التحقّق من ذلك من خلال الانتقال إلى الأدوات > Kotlin > Show Kotlin Bytecode في القائمة ثم النقر على الزر إلغاء التجميع.
String myString = "Logged in as " + user.getDisplayName();
عندما نريد الوصول إلى هذه العناوين من Java، يجب كتابة اسم إرجاع القيمة بشكل صريح.
في معظم الحالات، يكون اسم 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 إنشاء طرق الحصول على (وضبط قيم للخصائص 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 كاستثناء تم وضع علامة عليه.
ويتم إجراء ذلك من خلال التعليق التوضيحي بلغة Kotlin @Throws
، والذي يساعد في إمكانية التشغيل التفاعلي 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
}