چرا کاتلین؟ ۸ دلیل که میتواند برنامهنویسان جاوا را برای تغییر قانع کند (قسمت دوم)
جاوا به چه شکلی در میآمد اگر کسی آن را امروز از نو طراحی میکرد؟ احتمالا چیزی شبیه کاتلین
در قسمت اول این مطلب، در مورد سینتکس و تایپسیستم زبان کاتلین خواندیم. در صورتی که هنوز قسمت اول را نخواندهاید پیشنهاد میشود ابتدا آن را مطالعه کنید.
۳- ایمنی در برابر null
یکی از اهداف طراحی کاتلین، از بین بردن یا کاهش مشکلات ناشی از رفرنسهای null است. کاتلین بیشتر از هر زبان دیگری که من استفاده کردهام، از مشکل بزرگ NullPointerException جلوگیری میکند.
در واقع تایپسیستم کاتلین بین رفرنسهایی که میتوانند مقدار null نگه دارند (رفرنس nullable) و آنها که نمیتوانند، تفاوت قائل میشود و کامپایلر، سازگاری بین رفرنسها موقع انتساب را بررسی میکند. برای مثال تایپ String در کاتلین هرگز نمیتواند null باشد اما متغیری از جنس ?String میتواند null باشد (تفاوت در ? است). در مثال زیر این ایده را میتوان دقیقتر مشاهده کرد.
var s1: String = "abc" // s1 is not nullable var s2: String? = "abc" // s2 is nullable s1 = null // compilation error s1 = s2 // compilation error s2 = s1 // o.k. s2 = null // o.k. println(s1.length) // will never throw NPE println(s2.length) // compilation error ... fun printLength(s : String?) { if (s != null) println(s.length) // o.k. }
فراخوانیهای امن
اگر رفرنس s از نوع nullable باشد و در زمان استفاده از s، کامپایلر نتواند مطمئن شود که s قطعا null نیست، عبارت s.length باعث کامپایل ارور خواهد شد. البته میتوان از s?.length استفاده کرد. در صورت استفاده از این عملگر، اگر s برابر با null نباشد، s.length برمیگردد و اگر null باشد، کلا null برمیگردد. در نهایت تایپ نتیجهی s?.length برابر ?Int خواهد بود و کامپایلر مطمئن میشود که از آن به درستی استفاده شده باشد.
از آنجا که تابع println میتواند مقدار null را هندل کند، صدا کردن تابع println(s?.length) امن است و در صورت وجود، مقدار طول و در غیر این صورت null را چاپ میکند.
عملگر Elvis
بسیار پیش میآید که در برنامهنویسی بخواهیم از یک ویژگی یک متغیر nullable استفاده کنیم، پس ابتدا چک میکنیم که آیا null است یا نه. اگر null نبود که از مقدارش استفاده میکنیم. اما اگر null بود، یک مقدار پیشفرض را به کار میگیریم.
مثال بدون استفاده از Elvis:
val s : String? ... val len : Int = if (s != null) s.length else -1
در کاتلین عملگر «نال یکپارچه» (Null coalescing با علامت :?) وجود دارد که البته بیشتر به نام عملگر Elvis شناخته میشود. سمت چپ عملگر، عبارتیست که میتواند null باشد. اگر عبارت سمت چپ null باشد، به جای خودش، مقدار سمت راست به عنوان مقدار محاسبه شده از مجموعه عملگر و عملوندها برمیگردد. با عملگر Elvis، مثال بالا میتواند به شکل زیر نوشته شود:
val s : String? ... val len = s?.length ?: -1
(یادداشت مترجم: میتوانید از این لینک بیشتر درباره عملگر «نال یکپارچه» بخوانید. همچنین اگر ترجمه بهتری برای Null coalescing سراغ دارید در کامنتها برایمان بنویسید.)
عملگر «!!»
در کاتلین میتوانید از عملگر !! استفاده کنید تا کامپایلر چکهای مربوط به null را انجام ندهد. کد زیر را در نظر بگیرید:
val len = s!!.length
در اینجا به کامپایلر میگوییم که «ببین من مطمئن هستم که اینجا s قطعا null نیست و مسئولیتش را میپذیرم.» در نتیجه این کار باعث خطای کامپایل نمیشود. ولی در عوض، اگر s برابر با null باشد باعث پرتاب استثنای NullPointerException میشود.
۴- توابع و برنامهنویسی تابعی
توابع در کاتلین، میتوانند آبجکتهای سطح بالا باشند، به این معنی که نیازی نیست حتما داخل یک کلاس قرار بگیرند.
همچنین کاتلین امکانات بیشتری در زمینه توابع به برنامهنویسان میدهد که باعث کدهای تمیزتر و گویاتر بدون افت خوانایی میشود. برای مثال میتوانید تایپ خروجی، آکولاد باز و بسته و عبارت return را از تابع حذف کنید و توابع تکخطی داشته باشید. (مشابه عبارتهای لامبدا در جاوا)
برای مثال کد زیر
fun double(x : Int) : Int { return 2*x }
میتواند به شکل زیر نیز نوشته شود:
fun double(x : Int) = 2*x
همچنین کاتلین (مانند سیپلاسپلاس) مقدار پیشفرض برای آرگومانهای تابع را پشتیبانی میکند. در جاوا مرسوم است که یک سازنده، سازندههای دیگری از همان کلاس را صدا کند. مثلا اگر کلاس ArrayList را خودمان بنویسیم، ممکن است چند سازنده به شکل زیر داشته باشیم:
public ArrayList(int initialCapacity) { ... } public ArrayList() { this(10); }
اما در کاتلین به کمک مقدار پیشفرض برای آرگومانهای تابع، به یک سازنده تقلیل میيابد.
constructor(initialCapacity : Int = 10) { ... }
توابع مرتبه بالاتر
کاتلین همچنین از توابع مرتبه بالاتر نیز پشتیبانی میکند. به این معنی که میتوانید توابع را به عنوان آرگومان به توابع دیگر پاس دهید و به عنوان مقدار برگشتی از تابع برگردانید یا هردو. همچنین مثل جاوا، کاتلین از عبارات لامبدا و کلوژرها پشتیبانی میکند، به این ترتیب نیازی نیست تعریف کامل تابع را بنویسیم. حتی بیشتر از آن، اگر لامبدا فقط یک پارامتر ورودی داشته باشد، نیازی به مشخص کردن آن نیست و به شکل ضمنی نام «it» میگیرد.
از قابلیتهای دیگر کاتلین برای پشتیبانی از برنامهنویسی تابعی میتوان به بهینهسازی فراخوانی دم ( tail recursion) و بسیاری از توابع زبانهای برنامهنویسی تابعی برای کار با لیست اشاره کرد.
// assumes that students is a list of Student objects val adultStudents = students.filter { it.age >= 21} .sortedBy { it.lastName }
مثال زیر از این مطلب که توسط مارکین مُسکالا نوشتهشده، استخراج شدهاست.
fun > List.quickSort(): List = if (size < 2) this else { val pivot = first() val (smaller, greater) = drop(1).partition {it <= pivot} smaller.quickSort() + pivot + greater.quickSort() }
در حالات مشخصی، کامپایلر کاتلین از توابع درونخطی (inline) هم پشتیبانی میکند. یعنی کامپایلر فراخوانی تابع را با بدنه تابع جایگزین میکند. درونخطی کردن یک تابع میتواند باعث بهبود کارایی برنامه با از بین بردن سربار فراخوانی و برگشت از تابع بشود اما از طرفی میتواند اندازه کد تولیدشده را افزایش دهد و به همین دلیل باید از این تکنیک آگاهانه استفاده شود.
مناسبترین جا جهت اجرای این تکنیک، برای عبارات لامبدا است. چراکه در این صورت کامپایلر برای هر عبارت لامبدا لازم نیست یک تابع جدید تولید کند.
۵- کلاسهای داده
کلاسی که از ابتدا به منظور نگهداری داده طراحی میشوند، به نام کلاس موجودیت یا کلاس بیزینس یا کلاس داده نامیده میشوند و معمولا در پایگاهداده نگهداری میشوند. در جاوا معمولا این کلاسها متدهای کمی دارند. مثلا متدهای دسترسی فیلدها (getter و setterها) و متدهای استاندارد toString و hashCode و equals و احتمالا clone.
اگرچه محیطهای توسعه مدرن، میتوانند با تولید کد استاندارد برای این متدها در زمان شما صرفهجویی کنند، اما کاتلین نوع خاصی از کلاس با نام «کلاس داده» فراهم میکند که دیگر اصلا نیازی به پیادهسازی این متدها در آن نیست.
برای فهم این بهبود، دو کد زیر را مقایسه کنید. کد اول به زبان جاوا معادل کد دوم به زبان کاتلین است. کد جاوا با متدهایی که eclipse برای ما تولید کرده حدود ۱۸۰ خط (!) بدون کامنت است در حالی که پیادهسازی آن با کاتلین کمتر از ۱۰ خط میشود!
کد جاوا:
public class Student implements Cloneable { private String studentId; private String lastName; private String firstName; private String midInitial; private LocalDate dateOfBirth; private String gender; private String ethnicity; ... // constructor with every field ... // get()/set() methods for each field ... // method toString() ... // methods hashCode() and equals() }
کد کاتلین:
data class Student(var studentId : String, var lastName : String, var firstName : String, var midInitial : String?, var dateOfBirth : LocalDate, var gender : String, var ethnicity : String)
رکوردها در جاوا
طراحان زبان جاوا مشغول اضافه کردن قابلیت Record به جاوا هستند که میتواند امکانات مشابه کلاسهای داده را به برنامهنویسان بدهد.
توضیح مترجم: این قابلیت از نسخه ۱۴ جاوا به شکل نمایشی اضافه شدهاست.
سینتکس آن در جاوا به این شکل خواهد بود.
record Point(int x, int y) { }
۶- توابع و فیلدهای افزودنی
امکانات افزودنی (Extension) به ما این امکان را میدهد که بدون کمک وراثت و بدون تغییر مستقیم تعریف یک کلاس، به آن قابلیتهایی اضافه کنیم.
یک تابع افزودنی به این شکل تعریف میشود که قبل از نام تابع، نام کلاس میآید. مثلا فرض کنید که میخواهیم به کلاس String یک تابع اضافه کنیم. کد زیر یک مثال ساده را نشان میدهد که تابع isLong که یک boolean برمیگرداند را پیادهسازی میکنیم.
fun String.isLong() = this.length > 30 ... // create string str if (str.isLong()) ...
همچنین به شکل مشابه میتوانیم یک فیلد به کلاس فعلی اضافه کنیم. مثلا کلاس دادهای Student را که در بالا نوشتیم به همان شکل در نظر بگیرید. میخواهیم بدون تغییر دادن کد اولیه، فیلدهای fullName و age را به Student اضافه کنیم (مانند کد زیر).
دقت کنید که در پیادهسازی fullName ابتدا چک کردیم فیلد middleInitial که nullable است، مقدارش برابر با null نباشد.
val Student.fullName : String get() { val buffer = StringBuffer(35) buffer.append(lastName) .append(", ") .append(firstName) .append(if (midInitial != null) " $midInitial." else "") return buffer.toString() } val Student.age : Int get() = dateOfBirth.until(LocalDate.now()).getYears() ... // create Student variable student println(student.fullName) println(student.age)
اگرچه این دو فیلد جدید به شکل فیلد extension اضافه شدند، اما همچنین میتوان به شکل فیلد محاسبهای (computed properties) در تعریف کلاس Student نیز آنها را تعریف کرد (به شرط اینکه به کد منبع Student دسترسی داشته باشیم).
فیلد محاسبهای، معادل یک متد ()get در جاواست که پشت آن فیلدی وجود ندارد و محاسبه میشود.
توجه داشته باشید که صدا کردن یک تابع extension و استفاده از فیلدهای extension، در زمان کامپایل توسط کامپایلر پیدا میشود. به این معنی که تابع افزودنی که قرار است صدا شود در زمان کامپایل از روی تایپ متغیر (و نه تایپ واقعی آن شی در زمان اجرا) شناسایی میشود و در آنها چندریختی معنایی ندارد.
منبع: https://www.infoworld.com/article/3396141/why-kotlin-eight-features-that-could-convince-java-developers-to-switch.html
با اندکی تغییر و تلخیص
.
.
.
.
با ما همراه باشید
آدرس کانال تلگرام: JavaCupIR@
آدرس اکانت توییتر: JavaCupIR@
آدرس صفحه اینستاگرام: javacup.ir
صفحه ویرگول: javcup
آدرس گروه لینکدین: Iranian Java Developers
با سلام و ممنون از مقاله خوب شما
یه سوال که دارم سایت به این خوبی و پر محتوا چرا مطالب و مقاله های همیشه از نظر زمانی عقب هس ؟ کاربرها بیشتر با مقاله جدید و دنبال مطالب بروز تر هست
سلام!
ممنون از شما خواننده خوب که با انتقاد سازنده سعی در بهبود جاواکاپ دارید،
از این نظر که مطالبی که ترجمه میشوند و قرار داده میشوند تاریخ انتشارشان جدید نیست حق با شماست.
اما باید این جنبه را نیز در نظر داشت که هدف ما کیفیت بالای مطالب است و نه جدید بودن آنها، چرا که جدیدتر بودن یک مطلب تضمین نمیکند که به موضوع خوب و عمیق پرداخته باشد و برای برنامهنویسان مفید باشد.
همچنین دقت کنید که سایت وجههی خبرگزاری و مطالب جنبه خبری ندارند، بنابراین تمرکز روی مطالب روزهای اخیر نداریم (مگر در مواردی مثل ویژگیهای نسخههای جدید جاوا)