جاوا 10 و استنتاج نوع متغیرهای محلی
جاوا ۱۰ نیز پس از مدت کوتاهی از انتشار جاوا ۹ در ۲۰ مارس ۲۰۱۸ به صورت عمومی منتشر شده و کمکم باید خودمان را برای انتشار نسخه مهمتر یعنی جاوا ۱۱ آماده کنیم که قرار است سپتامبر سال آینده معرفی شود.
در نسخه ۱۰ تغییرات مشهود زیادی اضافه نشده و اکثر تغییرات و قابلیتها مربوط به بهینهسازی JDK و موارد اینچنینی بوده. لیستی از این قابلیتها را میتوانید از طریق این لینک مشاهده کنید. اما یکی از این قابلیتها یعنی قابلیت اسنتاج خودکار نوع متغیرهای محلی یا Local-Variable Type Inference است که در JEP 286 مشخصات آن ذکر شده است.
اگر با زبانهایی مثل اسکالا یا پایتون کار کرده باشید این قابلیت برای شما آشنا خواهد بود. همانطور که از اسم آن مشخص است دیگر نیازی به مشخص کردن نوع متغیرهای محلی خود ندارید و کامپایلر میتواند در زمان کامپایل نوع متغیر شما را به صورت خودکار تشخیص دهد.
بگذارید یک مثال ساده را ببینیم:
List<String> names = new ArrayList<>();
تاکنون مجبور بودیم به این شکل متغیرهای خود را تعریف کنیم. از آنجایی که برنامهنویسها معمولا انسانهای تنبلی هستند(!) خیلیها این ایده به ذهنشان رسید که شاید برای یک متد که بدنه آن کاملا معمولی است و با یک نگاه به اسم تابع آن میشود کارکرد آن را متوجه شد نیازی به این همه تایپ کردن نداریم و خود کامپایلر آنقدر باهوش هست که بفهمد ما از چه نوع تایپی استفاده کردهایم.
پس کد بالا را به شکل زیر مینویسیم:
var names = new ArrayList<String>();
کار نگارنده این کد خیلی آسان شده! کافی است از کلمه کلیدی۱ var استفاده کنید و به سادگی مثل قبل کد بزنید.
اما شاید از خودتان بپرسید میتوانستیم از نوع Object یا List استفاده کنیم و اصلا نیازی به var نبود. یعنی:
var names1 = new ArrayList<String>(); names1.add("JavaCUP"); System.out.println("names<"+names1.get(0).getClass().getTypeName()+"> = "+names1); List names2 = new ArrayList<String>(); names2.add("JavaCUP"); names2.add(1); // We don't want this! System.out.println("names<"+names2.get(0).getClass().getTypeName()+"> = "+names2); Object names3 = new ArrayList<String>(); // Is not possible without casting! //names3.add("JavaCUP"); //System.out.println("names<"+names3.get(0).getClass().getTypeName()+"> = "+names3);
همانطور که میبینید استفاده از نوع List رفتار مدنظر ما را عوض میکند و عملا کارکردی مشابه با var را ندارد.
قابلیتهای تکمیل خودکار کد و کمکهای IDE در صورت به روز بودن وجود خواهد داشت و بدون مشکل میتوانید مثل سابق از این کمکها استفاده کنید: (در اینجا از نسخه 2018.1 IntjelliJIDEA استفاده شده)
ممکن است فکر کنید وجود چنین مکانیزمی باعث کند شدن اجرا برنامه شما میشود چون به نظر شبیه کدهای زبانهای داینامیک مثل پایتون یا جاوااسکریپت شده. اما درست مشابه زبان اسکالا (var/val)، سیشارپ (var) یا سی++ (auto) یا بقیه زبانهای با تایپ استاتیک که مکانیزم استنتاج نوع دادهها در زمان کامپایل را دارند، استفاده از این قابلیت بر سرعت اجرای برنامه شما تاثیری ندارد و در نهایت بایتکد تولیدشده تفاوتی با نسخه معمولی آن ندارد.
وجود چنین قابلیتی بحثهای فراوانی پیرامون خوانایی کد ایجاد میکند. آیا استفاده از این قابلیت خوانایی کدها را افزایش میدهد؟ یا منجر به افزایش پیچیدگی کدها و سختتر کردن نگهداری کدها میشود؟
طبیعتا هیچ قانونی وجود ندارد که چه زمانی به صورت صریح نوع دادهها را بیان کنیم و چه زمانی به شکل ضمنی آنها را مشخص کنیم.
خبر خوب یا بد این است که شما نمیتوانید در هرجایی از این قابلیت استفاده کنید و محدودیتهایی وجود دارد. فیلدهای کلاسی، پارامترهای پاس دادهشده به متد یا سازنده، نوع خروجی متد و نوع اکسپشنها در بلوک catch نمیتوانند از این قابلیت استفاده کنند. در واقع بهترین موارد استفاده مثلا در حلقهها و متغیرهای میانی و موارد اینچنینی هستند که آنقدر بدیهی هستند که اصلا نیازی به وجود خوانایی و نگهداری از آنها نیست و فقط میخواهیم زودتر خودمان را از شر نوشتنشان خلاص کنیم و به قسمتهای مهمتر برنامه برسیم. از آنجایی که دامنه استفاده از چنین امکانی محدود هست، خیلی تصمیمگیری سختی نداریم که چه وقت از آن استفاده کنیم و یا چه وقت از آن استفاده نکنیم و صراحت را ترجیح دهیم.
لازم به ذکر است که بدون مقداردهی نمیتوانید از این قابلیت استفاده کنید. یعنی چنین کدی کامپایل نخواهد شد:
//you need initialization! var num; num = 10;
اما به این شکل هیچ موردی وجود ندارد:
//perfectly fine var num = 10; num = 2; System.out.println("num = " + num);
در این مثال در قسمت catch نمیتوانیم از var e استفاده کنیم:
//this is totally okay try (var bufferedReader = new BufferedReader(new FileReader(new File("test.txt")))) { //you cannot use "var e" } catch (var e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
یک نمونه از کاربرد خوب در استفاده از حلقهها:
var names = List.of("Java","Cup"); for(var name:names){ System.out.println(name); }
اما این فقط بخشی از این محدودیتها است. یکی دیگر از محدودیتها هنگامی است که سمت راست عبارت شما عبارت لامبدایی نوشته شده باشد که خود نیاز به استنتاج دارد و نمی توانید در سمت چپ این عبارت از var استفاده کنید. تعدادی از این نوع مثالها:
var x; // ^ //(cannot use 'val' on variable without initializer) var f = () -> { }; // ^ //(lambda expression needs an explicit target-type) var g = null; // ^ //(variable initializer is 'null') var c = l(); // ^ //(inferred type is non denotable) var m = this::l; // ^ //(method reference needs an explicit target-type) var k = { 1 , 2 }; // ^ //(array initializer needs an explicit target-type)
برای مطالعه بیشتر جزئیات حتما این لینک را مطالعه کنید.
۱- جالب است بدانید بسیاری از کلماتی که به عنوان کلمات کلیدی در جاوا ۹ و ۱۰ معرفی شدهاند مثل module یا var واقعا کلمه کلیدی نیستند و در واقع “نام نوع رزروشده” (reserved type name) میباشند. دلیل اینکار حفظ قابلیت backward-compatibility است.
var var = 10; //var is not really a keyword! if it was, this code wouldn't work System.out.println("var = " + var);