Kotlin هو جافا الجديد


محمد بغات

السؤال الذي قد يتبادر إلى أذهان مُطوّري الجافا هو "ما الذي عليّ أن أتعلمه الآن؟". هناك مجموعة من اللغات التي تستحق الاعتبار، مثل Clojure, Rust  أو Haskell. ولكن ماذا لو كنت تريد أن تتعلم شيئًا يساعدك على دفع الفواتير وهو فوق ذلك سهل الاستخدام؟ Kotlin قد يكون خيارك الأفضل، وسنحاول في هذه المقالة أن نشرح لماذا.

ما هو Kotlin؟

 لغة برمجة مُطوّرة من قبل JetBrains، والذين كانوا وراء فكرة الـ IDE بالإضافة إلى أشياء أخرى.

  •  بديل بسيط ومرن لجافا
  • تتلاءم جيدًا مع أكواد جافا الموجودة
  • تُترجم لجافا bytecode
  • تعمل على JVM
  • كما تُترجم أيضًا لجافا سكريبت

إن كنت قد قرأت وثائقها فستلاحظ مجموعة من الأمور المهمة:

  • تتيح لك القيام بالكثير من الأشياء بأكواد قليلة
  • تحل الكثير من المشاكل الموجودة في جافا
  • تساعدك على الاستمرار في استخدام النظام البيئي ecosystem المعتاد لجافا
  • تتيح لك برمجة الواجهة الأمامية والخلفية بنفس اللغة
  • 100٪ متوافقة مع الجافا
  • أداؤها جيد مقارنةً بالبدائل (Clojure, Scala)
  • لا تضيف إلّا طبقةً رقيقة من التّعقيد على جافا

يبدو هذا جيدًا، أليس كذلك؟ فيما يلي سنرى بعض الأمثلة لمقارنتها بالجافا.

عناصر القِيَم مقابل بيانات الأصناف Value objects vs data classes

ما تراه هنا هو كائن جافا قديم ((POJO مع الأنماط المعتادة:

public class HexagonValueObject {


    private final int x;
    private final int y;
    private final int z;


    public HexagonValueObject(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }


    public int getX() {
        return x;
    }


    public int getY() {
        return y;
    }


    public int getZ() {
        return z;
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        HexagonValueObject hexagon = (HexagonValueObject) o;
        return getX() == hexagon.getX() &&
                getY() == hexagon.getY() &&
                getZ() == hexagon.getZ();
    }


    @Override
    public int hashCode() {
        return Objects.hash(getX(), getY(), getZ());
    }


    @Override
    public String toString() {
        return "HexagonValueObject{" +
                "x=" + x +
                ", y=" + y +
                ", z=" + z +
                '}';
   }

إنشاء عناصر القِيَم هو عملية مرهقة حتى باستخدام المكتبات مثل Lombok) Lombok يتطلب تثبيت ملحقة في بيئة العمل IDE الخاصة بك من أجل أن يعمل، وهو أمر قد لا يكون ممكنًا في جميع بيئات التطوير، يمكن تجاوز هذا المشكل بأدوات مثل Delombok، ولكنه اختراق أكثر منه حل للمشكلة)، على الأقل IDEA (أو Eclipse) يمنحك بعض المساعدة في توليد الكثير من تلك الوظائف، ولكنّ إضافة حقل ونسيان تعديل الوظيفة equals سيؤدي إلى مفاجآت سيئة. دعونا ننظر الآن إلى الكود المقابل في Kotlin:

data class HexagonDataClass(val x: Int, val y: Int, val z: Int)

مذهل! لقد اختصرنا الكثير من الكتابة بالمقارنة مع نسخة جافا. بيانات الأصناف في Kotlin تعطيك

  • equals + hashCode و
  • toString بالإضافة إلى
  • المُحصّلات والمُعيِّنات (getters and setters). يمكنك أيضًا نسخها، مما يخلق كائنًا جديدًا مع إعادة كتابة بعض حقوله.

حشو سلاسل النصوص String interpolation

التعامل مع سلاسل النصوص في الجافا مرهق. ولكن يمكن تبسيطه باستخدام String.format ومع ذلك ستبقى قبيحة.

public class JavaUser {
    private final String name;
    private final int age;


    public String toHumanReadableFormat() {
        return "JavaUser{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }


    public String toHumanReadableFormatWithStringFormat() {
        return String.format("JavaUser{name='%s', age=%s}", name, age);
    }
}

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

class KotlinUser(val name: String, val age: Int) {


    fun toHumanReadableFormat() = "JavaUser{name='$name', age=$age}"


    fun toHumanReadableFormatWithMethodCall() =
        "JavaUser{name='${name.capitalize()}', age=$age}"
}

دوال التمديد Extension functions

كتابة المُلقّمات decorators في جافا يمكن أن يكون صعبًا، كما أنّها ليست مثالية.
إذا كنت تريد أن تكتب مُلقّمًا، والذي يمكن استخدامه مع جميع الأصناف التي تقدّم List فلا يمكنك ببساطة استخدامه في الملقم خاصتك لأنّه سيحتاج منك أن تقدم الكثير من الوظائف الأخرى، لذلك ستكون مضطرًّا لتمديد AbstractList.

public class ListPresenterDecorator<T> extends AbstractList<T> {

    private List<T> list;

    public ListPresenterDecorator(List<T> list) {
        this.list = list;
    }

    public String present() {
        return list.stream()
                .map(Object::toString)
                .collect(Collectors.joining(", "));
    }

    @Override
    public T get(int index) {
        return list.get(index);
    }

    @Override
    public int size() {
        return list.size();
    }
}

إذا كنت بحاجة إلى تلقيم شيء ما ولكنه لا يوفر أصنافًا أساسية base classes مفيدةً مثل AbstractList أو الصنف final، فأنت لا تستطيع ذلك ببساطة. لكن وظائف التمديد Extension methods يمكن أن تعطيك الحل!

fun <T> List<T>.present() = this.joinToString(", ")

تتصرف هذه الوظيفة كمُلقّم لجميع الأصناف Lists. لو قارنّا هذا ببديله في الجافا فإنّ هذ السطر الصغير أبسط بكثير، والأكثر من ذلك سوف يعمل أيضًا مع الأصناف final. حاول ألّا تسيء استخدامها وحسب.

التحقق من العدمية Null safety

التحقق من القيم المعدومة null ينطوي على الكثير من التعابير المنطقية والأنماط. مع ظهور الجافا، يمكنك أخيرًا التعامل مع هذه المشكلة بواسطة الصنف Optional، لكن ماذا لو كان مرجعOptional معدومًا null؟ ستحصل على NullPointerException، فلا زلنا بعد مرور 20 عام على ظهور جافا لا نستطيع تحديد العنصر الذي كان معدومًا. نأخذ المثال التالي:

public class JavaUser {

    static class Address {
        String city;
    }

    private final String firstName;
    private final String lastName;
    private final List<Address> addresses;

    /**
     * If you want to make sure nothing is `null`
     * you have to check everything.
     */
    public static String getFirstCity(JavaUser user) {
        if(user != null && user.addresses != null && !user.addresses.isEmpty()) {
            for(Address address : user.addresses) {
                if(address.city != null) {
                    return address.city;
                }
            }
        }
        throw new IllegalArgumentException("This User has no cities!");
    }
}

مع Kotlin، لديك العديد من الخيارات. إذا كان مشروعك متداخلًا مع مشاريع جافا يمكنك استخدام عامل التحقق من العدميّة (؟)null safety operator:

data class KotlinUserWithNulls(val firstName: String?,
       // String? means that it is either a String object or a null
                               val lastName: String?,
                               val addresses: List<Address> =  listOf()) {

    data class Address(val city: String?)

    companion object {
        fun fetchFirstCity(user: KotlinUserWithNulls?): String? {
            user?.addresses?.forEach { it.city?.let { return it } }
            return null
        }
    }
}

الكود الموجود بعد ؟ لن يُنفّذ إلا إن كان المعامل الأيسر operand غير معدوم not null. الدالة let تنشئ ارتباطًا محليًّا للكائن المُستدعى بحيث أنّ it سوف تشير إلى it.city. أما إن كان عملك مفصولًا عن الجافا فأقترح الابتعاد عن كل ما له علاقة بـ null

data class KotlinUserWithoutNulls(val firstName: String,
                                  // this parameter can't be null
                                  val lastName: String,
                                  val addresses: List<Address> = listOf()) {

    data class Address(val city: String)

    companion object {
        fun fetchFirstCity(user: KotlinUserWithNulls) =
            user.addresses.first().city
    }
}

إذا لم يكن هناك أي استخدام لـ null (لا وجود لـ ؟) فكل شيء سيصبح أكثر بساطةً.

استنباط النوع Type Inference

Kotlin يدعم استنباط الأنواع، مما يعني أنه قادر على معرفة الأنواع من سياقها. أي مثل العامل الماسي diamond notation في الجافا <>! نأخذ المثال التالي:

public class JavaUser {

    // ...

    private final String firstName;
    private final String lastName;
    private final List<Address> addresses;

    public Address getFirstAddress() {
        Address firstAddress = addresses.get(0);
        return firstAddress;
    }
}

هذا يبدو نفسه تقريبًا في Kotlin:

data class KotlinUser(val firstName: String,
                      val lastName: String,
                      val addresses: List<Address> = listOf()) {

    data class Address(val city: String)

    /**
     * This is the same as in `JavaUser`.
     */
    fun getFirstAddressNoInference(): Address {
        val firstAddress: Address = addresses.first()
        return firstAddress
    }
}

وذلك حتى تدع Kotlin تكتشف أنواع المتغيرات الخاصة بك:

/**
 * Here the type of `firstAddress` is inferred from the context.
 */
fun getFirstAddressInferred(): Address {
    val firstAddress = addresses.first()
    return firstAddress
}

أو حتى الوظائف:

/**
 * Here the return type is inferred. Note that
 * if a method consists of only one statement
 * you can omit the curly braces.
 */
fun getFirstAddress() = addresses.first()

لا داعي للتحقق من الاستثناءات No checked exceptions

ربما رأيت الكود التالي مليون مرة من قبل:

public class JavaLineLoader {

    public List<String> loadLines(String path) {
        List<String> lines  = new ArrayList<>();
        try(BufferedReader br = new BufferedReader(new FileReader(path))) {
            String line;
            while((line = br.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return lines;
    }
}

لأصحاب المدرسة القديمة في (IO in Java). لاحظوا التعليمة try في الكود أعلاه، الأمر نفسه سيُكتب هكذا في Kotlin:

class KotlinLineLoader {
    fun loadLines(path: String) = File(path).readLines()
}

هناك بضعة أشياء ينبغي الانتباه إليها، أولًا: Kotlin ألغى التحقق من الاستثناءات. ثانيًا: Kotlin أضاف استخدام الكائنات من نوع Closeable أي:

Quote

ينفذ [الكتلة block] المعينة على هذا المورد ثم يقوم بإغلاقها بشكل صحيح بغض النظر عن طرح استثناء من عدمه. (وثائق Kotlin)

يمكنك أن تلاحظ أيضًا أنّ دالة التمديد (readLines) أضيفت إلى الصنف File. هذا النمط موجود بكثرة في Kotlin. إذا كنت قد استخدمت Guava أو Apache Commons أو شيء مماثل من قبل، فغالبًا سترى وظائف مشتركة تضاف منها إلى الصنف JDK كدالة تمديد. وهذا قد يكون مفيدًا لصحتك (أعصابك على الأقل).

دعم لامبدا Lambda support

دعونا نلقي نظرة على دعم lambda في الجافا:

public class JavaFilterOperation {

    private List<String> items;

    @FunctionalInterface
    interface FilterOperation {
        Boolean filter(String element);
    }

    private List<String> filterBy(FilterOperation fn) {
        return items.stream()
                .filter(fn::filter) // applying the function
                .collect(Collectors.toList());
    }

    public void doFilter() {
        filterBy((element) -> {
            return element.length() > 0;
            // calling the function with an actual lambda
        });
    }
}

بما أنّه لا توجد قاعدة محددة لكتابة أنواع معاملات الوظائف method parameter types، فسيكون علينا إنشاء واجهة لها بأنفسنا. لاحظ أننا يمكن أن نستخدم Function<String, Boolean> هنا، ولكنها لن تعمل إلّا على الدوال ذات المعامل الواحد! هناك بعض الواجهات في JDK لحل هذه المشكلة، ولكن الكود سيكون مربكًا وغير واضح (مثلًا، ما دور BiFunction هنا؟)، Kotlin يحسّن هذا قليلًا:

class KotlinFilterOperation {

    private val items = listOf<String>()

    fun filterBy(fn: (String) -> Boolean) = items.filter(fn)

    fun doFilter() {
        filterBy(String::isNotEmpty)
        // note the exension function `isNotEmpty` added to `String`!
    }
}

Kotlin يضيف قاعدةً نحويةً من أجل تمرير الدوال كمعاملات: (ParamType1, ...ParamTypeN) -> ReturnType. Kotlin يوفر مراجع للوظائف والحقول، كما يمكنك أيضًا التأشير إلى وظيفة من كائن ملموس! باستخدام المثال أعلاه يمكنني التأشير إلى الوظيفة filterBy من عيّنة instance ملموسة، هكذا:

val reference = KotlinFilterOperation()::filterBy

البرمجة الوظيفيّة Functional programming

البرمجة الوظيفيّة أصبح لها ضجة في الوقت الحاضر. ومع جافا 8، قدمت Oracle مقاربتها لهذا الموضوع: الواجهة البرمجية Stream
(Stream API). وهي تعمل كالتالي:

public class JavaUser {
 
    // ...
    
    public static Set<String> fetchCitiesOfUsers(
                                  List<JavaUser> users) {
        return users.stream()
                .flatMap(user -> user.addresses.stream())
                .map(JavaUser.Address::getCity)
                .collect(Collectors.toSet());
    }
}

المقابل في Kotlin مشابه إلى حد ما، ولكن باختلافات دقيقة:

fun fetchCitiesOfUsers(users: List<KotlinUser>) = users
            .flatMap(KotlinUser::addresses)
            .map(Address::city)
            .toSet()

لا يوجد تحويل صريح للتسلسلات streams بما أنّ كل تجميعات Kotlin تدعمها.  كنتيجة مباشرة لذلك، لن يكون عليك تمرير لامبدا لـ flatMap. تجميع النتيجة يحدث تلقائيًّا (لا حاجة لاستدعاء الوظيفة *Collectors.to). واستخدامنا لـ toSet هنا سببه الوحيد أننا نريد إرجاع عنصر من نوع Set. وإلا فيمكن حذف.toSet()

توافقية جافا وKotlin 

يمكن أن يكون هذا معقدًا لمعظم النّاس، لكن JetBrains تعاملت مع هذا الأمر بالشكل الصحيح:

public class KotlinInterop {

    public void helloJava() {
        System.out.println("Hello from Java!");
    }

    public void helloKotlin() {
        JavaInterop.createInstance().helloKotlin();
    }
}
class JavaInterop {

    fun helloJava() {
        KotlinInterop().helloJava()
    }

    fun helloKotlin() {
        println("Hello from Kotlin!")
    }

    companion object {

        @JvmStatic
        fun createInstance() = JavaInterop()
    }
}

التداخل هنا سلس وسهل. جافا وKotlin يمكن أن يتعايشا معًا في نفس المشروع، كما يقدم Kotlin مجموعة من التعبيرات (مثل @JvmStatic) لتسهيل استدعاء كود Kotlin من الجافا دون أية مشاكل.

قد تكون قد لاحظت من هذه الأمثلة أن Kotlin يستعير الأفكار الجيدة من جافا ويُحسّنها ويحاول أن يختصر عليك الوقت والعمل إلى أدنى حد ممكن. الأنباء الأخيرة حول اعتزام جوجل جعل Kotlin واحدةً من اللغات المعتمدة علىAndroid تدعم هذا التوجه أيضًا.

أشياء لتخبر بها مديرك:

إذا كنت ترغب في إعطاء Kotlin فرصة فإليك بعض النصائح التي ستساعدك عند التفاوض مع مديرك أو زملائك في الفريق:

  • Kotlin خرج من عالم الصناعة، وليس من العالم الأكاديمي. فهو يحل المشاكل التي يواجها المبرمجون اليوم.
  • حر ومفتوح المصدر
  • يأتي ومعه أداة مفيدة لتحويل جافا إلى Kotlin
  • يمكنك مزج Kotlin وجافا دون جهد يُذكر
  • يمكنك استخدام جميع أدوات وأُطُر عمل جافا الموجودة
  • Kotlin مدعوم من قبل أفضل بيئة عمل في السوق (مع نسخة مجانية)
  • من السهل قراءته، فحتى المبرمجون غير المتخصصون في Kotlin يمكنهم مراجعة الأكواد المكتوبة به
  • لا تحتاج إلى رهن مشروعك بـ Kotlin: يمكنك البدء في المرحلة الأولى بكتابة الاختبارات فيه.
  • من غير المرجح أن تتخلى JetBrains عن Kotlin لأنه يُقوّي مبيعاتها
  • Kotlin لديه مجتمع حيوي، كما يمكنك بسهولة أن تساهم في Kotlin وتقترح ميزات جديدة باستخدام KEEP

إذن، هل يستحق Kotlin الضجة المثارة حوله؟ الحكم لك.

ترجمة -وبتصرّف- للمقال Kotlin is the new Java لكاتبه Adam Arold





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن