اگرچه زمان زیادی از ارائه جاوا۱۱ در سپتامبر۲۰۱۸ توسط اوراکل میگذره، ولی چون اوراکل پشتیبانی رسمی از جاوا۸ رو در ژانویه ۲۰۱۹ متوقف کرده و این نسخه، اولین نسخهی LTS (Long Term Support) بعدیه، یعنی با وجود معرفی جاوا 16 در مارس۲۰۲۱، هنوز هم نسخهی LTS جدیدی ارائه نشده، و البته اینکه نرمافزارهای ما هم قراره به جاوا۱۱ مهاجرت کنن، تصمیم گرفتم تفاوتهای جاوا۸ و جاوا۱۱ رو بررسی کنم و اینجا هم برای مطالعه و استفادهی دوستان قرار بدم.
در این نوشته قصد دارم یک نگاه کوتاه به تغییرات JDK برای ما برنامهنویسها داشته باشم و بعد ویژگیهای جدید، ویژگیهای حذف شده و تغییرات در جهت افزایش عملکرد در جاوا۱۱ رو مرور کنم.
خوبه که بدونین جاوا۱۰ آخرین نسخهی مجانی Oracle JDK بود که میتونستیم به صورت تجاری و بدون خرید License ازش استفاده کنیم. البته اوراکل هنوز به ارائهی نسخههایOpen JDK ادامه میده که میتونیم اونها رو بدون پرداخت هزینه استفاده کنیم.
ضمناً علاوه بر اوراکل، ارائه دهندگان Open JDK دیگری هم وجود دارن که میتونیم از نسخههای ارائهشده اونها هم استفاده کنیم. مثل:
AdoptOpenJDK
Amazon Corretto
Azul Zulu
Bck2Brwsr
CACAO
Codename One
DoppioJVM
Eclipse OpenJ9
GraalVM CE
HaikuVM
HotSpot
Jamiga
JamVM
Jelatine JVM
JVM.go
leJOS
Maxine
Multi-OS Engine
RopeVM
uJVM
Jikes RVM (Jikes Research Virtual Machine)
همونطور که از اسمش مشخصه، این متد محتوای رشته رو تکرار میکنه. درواقع این متد یک رشته رو برمیگردونه که برابر concat رشتهایه که قراره n بار(n به عنوان پارامتر ورودی) تکرار بشه؛ ضمناً اگر رشته خالی باشه یا پارامتر ورودی صفر باشه، یک رشتهی خالی رو برمیگردونه:
@Test public void whenRepeatStringTwice_thenGetStringTwice() { String output = "La ".repeat(2) + "Land" is(output).equals("La La Land"); }
این متد یک رشته رو با حذف تمام Whitespaceها از ابتدا و انتهای اون برمیگردونه. این متد زیرمجموعههایی هم با نامهای ()stripLeading و ()stripTrailing داره که به ترتیب Whitespaceها رو از ابتدا و یا از انتهای رشته حذف میکنن.
@Test public void whenStripString_thenReturnStringWithoutWhitespaces() { is("\n\t hello \u2005".strip()).equals("hello"); }
این متد بر اساس متد ()Character.isWhitespace مشخص میکنه که کاراکتر مورد بررسی Whitespace هست یا نه. درواقع این متد تمام Whitespaceهای Unicode رو میشناسه. این مهمترین تفاوت متدهای ()strip و ()trim هست.
این متد اگر رشته خالی باشه یا فقط شامل Whitespace باشه، مقدار true برمیگردونه و در غیراینصورت، مقدار false. درست مثل متد ()strip این متد هم تمام Whitespaceهای Unicode رو میشناسه.
@Test public void whenBlankString_thenReturnTrue() { assertTrue("\n\t\u2005 ".isBlank()); }
این متد یک Stream از خطوط استخراج شده از یک رشته رو برمیگردونه که هر خط در اون رشته توسط کاراکترهای انتهای-خط("n/" یا "r/" و یا "n/r/" ) از باقی خطوط جدا شده:
@Test public void whenMultilineString_thenReturnNonEmptyLineCount() { String multilineStr = "This is\n \n a multiline\n string." long lineCount = multilineStr.lines() .filter(String::isBlank) .count(); is(lineCount).equals(3L); }
در نهایت Stream خروجی، حاوی خطوطیه که به ترتیب در رشته وجود دارن. کاراکترهای انتهای-خط هم از هرخط حذف میشن.
با استفاده از متدهای جدید ()readString و ()writeString از کلاس Files، خوندن و نوشتن رشتهها در fileها آسونتر شده:
Path filePath = Files.writeString(Files.createTempFile(tempDir, "demo", ".txt"), "Sample text"); tring fileContent = Files.readString(filePath); assertThat(fileContent).isEqualTo("Sample text");
به اینترفیس java.util.Collection یک متد جدید با نام ()toArray اضافه شده که می تونه یک IntFunction رو به عنوان generator در ورودی بپذیره. این متد تولید یک آرایه با نوع مناسب رو از یک collection آسون کرده:
List<String> sampleList = Arrays.asList("Java", "\n \n", "Kotlin", " "); List withoutBlanks = sampleList.stream() .filter(not(String::isBlank)) .collect(Collectors.toList()); assertThat(withoutBlanks).containsExactly("Java", "Kotlin");
یک متد static با نام ()not به فانکشنال اینترفیس(FunctionalInterface) Predicate اضافه شده که به کمک اون می تونیم predicateهای موجود رو منفی(negate) کنیم. خب درواقع ما قبل از جاوا۱۱ با همین متد negate() اینکار رو انجام میدادیم، پس اساساً چرا باید این متد جدید بوجود اومده باشه؟!
فرض کنید که یک کلاس Person به شکل زیر داشته باشیم:
public class Person { private static final int ADULT_AGE = 18; private int age; public Person(int age) { this.age = age; } public boolean isAdult() { return age >= ADULT_AGE; } }
و یک لیست به این شکل:
List<Person> people = Arrays.asList( new Person(1), new Person(18), new Person(2) );
و با استفاده از جاوا۸ می خوایم همه افراد بزرگسال رو از لیست بازیابی کنیم. خب این کار به راحتی با قطعه کد زیر قابل انجامه:
people.stream() .filter(Person::isAdult) .collect(Collectors.toList());
حالا اگر بخوایم همه افراد غیربزرگسالرو بازیابی کنیم چی؟ باید predicateمون رو به این ترتیب منفی کنیم:
people.stream() .filter(person -> !person.isAdult()) .collect(Collectors.toList());
اینجا ما دیگه نمیتونیم از method reference استفاده کنیم و خب اینجوری خوانایی کد رو پایین میاریم. یک راه جایگزین هم البته می تونه ساختن یک متد دیگه مثلاً با نام ()isNotAdoult باشه که کد مارو به این شکل تغییر میده:
people.stream() .filter(Person::isNotAdult) .collect(Collectors.toList());
اما ممکنه ما نخوایم متد جدیدی رو به API خودمون که همین حالا در اختیار مشتریه اضافه کنیم. یا ممکنه اصلاً نتونیم اضافه کنیم چون کلاس Person مال ما یا در اختیار ما نیست.
اینجاست که در جاوا۱۱ به جای استفاده از lambda یا اضافه کردن یه متد جدید به کلاس Person میتونیم از متد ()Predicate.not استفاده کنیم:
people.stream() .filter(not(Person::isAdult)) .collect(Collectors.toList());
به بیان بهتر درحالیکه not(isBlank) میتونه خیلی طبیعیتر و قابلفهمتر از ()isBlank.negate باشه، اما مزیت بزرگترش اینه که ما میتونیم از متد ()not با method reference به شکل not(String::isBlank) استفاده کنیم.
(x, y) -> x.process(y)
در جاوا۱۰ (Java SE 10) تعریف نوع ضمنی برای متغیرهای محلی(local variables) ممکن شد. به این ترتیب امکان استفاده از var به عنوان type یک متغیرمحلی به جای type واقعی، به وجود اومد. اونجا کامپایلر، type واقعی روبراساس type مقداردهندهاولیهی(initializer) موجود در سمت راست، استنباط میکرد. مثلا:
var x = new Foo();
for (var x : xs) { ... }
try (var x = ...) { ... } catch …
اما هنوز نمیتونستیم از local variableها در عبارتهای Lambda به این صورت استفاده کنیم:
(var s1, var s2) -> s1 + s2
جاوا۱۱ با پشتیبانی از کد بالا مسأله رو حل کرده وباعث شده استفاده از var در هر دو متغیرمحلی و پارامترهای lambda یکنواخت بشه. مهمترین مزیت این یکنواختی اینه که حالا میتونیم از modifireها، به ویژه annotationها، در متغیرهای محلی و عبارتهای lambda بدون ازدستدادنِ خصوصیتِ خلاصهنویسی، استفاده کنیم:
@Nonnull var x = new Foo(); (@Nonnull var x, @Nullable var y) -> x.process(y)
برای درک بهتر به این مثال نگاه کنین:
List<String> sampleList = Arrays.asList("Java", "Kotlin"); String resultString = sampleList.stream() .map((@NonNull var x) -> x.toUpperCase()) .collect(Collectors.joining(", ")); assertThat(resultString).isEqualTo("JAVA, KOTLIN");
این API جدید از پکیج java.net.http در جاوا۹ معرفی شد، و حالا به یک ویژگی استاندارد در جاوا۱۱ تبدیل شده.
این API که برای بهبود عملکردِ کلیِ ارسالِ درخواست توسط مشتری و دریافتِ پاسخ از سرور طراحی شده، از هر دو استاندارد HTTP/1.1 و HTTP/2 و همچنین از WebSockets پشتیبانی میکنه.
URI uri = URI.create("http://localhost:8080"); HttpClient httpClient = HttpClient.newBuilder() .version(HttpClient.Version.HTTP_2) .connectTimeout(Duration.ofSeconds(20)) .build(); HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(uri) .build(); HttpResponse httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); assertThat(httpResponse.body()).isEqualTo("Hello from the server!");
قطعه کد زیر رو در نظر بگیرین:
public class Outer { private String name = "I am Outer!!" class Inner { public void printName() { System.out.println(name); } } }
اگر این کلاس رو کامپایل کنیم ، دو کلاس Outer و Outer$Inner ایجاد میشن. یعنی از نظر JVM حتی یک کلاس تودرتو هم یک کلاس معمولی با یک نام منحصر به فرده.
اگرچه قانون دسترسی JVM اجازهی دسترسی به اعضای private بین کلاسهای مختلف رو نمیده، اما اگر دو کلاس مثل مثال بالا در اصطلاح nested باشن، کامپایلر این دسترسی رو از طریق ساخت یک bridge method با نام access$000 ایجاد میکنه. کلاسهای بالا بعد از کامپایل به شکل زیر در میان:
public class Outer { private String name = "I am Outer!!" String access$000(){ return name; } } class Outer$Inner { final Outer obj; public void printName() { System.out.println(obj.access$000()); } }
در جاوا 11 ، کامپایلر جاوا هیچ دسترسیای روبه روش bridge ایجاد نمیکنه. بلکه در قانون دسترسی JVM جدید، کنترل دسترسی Nest-Based اجازهی دسترسی private به اعضای Nest رو صادر میکنه. درواقع در جاوا۱۱ متدهای ()getNestHost(), getNestMembers(), isNestmateOf از کلاس java.lang.Class در reflection API برای این کار معرفی شدهن:
یک Nest از کلاسها در جاوا۱۱، هم شامل کلاس بیرونی(اصلی) و هم همهی کلاسهای تودرتو ست. یعنی در اصطلاح، همهی این کلاسها باهم NestMate هستند:
assertThat(Outer.class.isNestmateOf(Outer.Inner.class)).isTrue();
کلاسهای تودرتو ویژگی NestMembers رو دارند، درحالیکه کلاس بیرونی ویژگی NestHost رو داره:
assertThat(Outer.Inner.class.getNestHost()).isEqualTo(Outer.class);
Set<String> nestedMembers = Arrays.stream(Outer.Inner.class.getNestMembers()) .map(Class::getName) .collect(Collectors.toSet()); assertThat(nestedMembers).contains(Outer.class.getName(), Outer.Inner.class.getName());
تغییر اساسی بعدی در این نسخه، اینه که دیگه نیازی به compile فایلهای جاوا با javac نداریم یعنی به جای این کد:
javac HelloWorld.java
java HelloWorld
Hello Java 8!
میتونیم مستقیماً با فرمان java، فایل برنامه رو اجرا کنیم:
java HelloWorld.java
Hello Java 11!
بعضی از ماژولها یا کلاسها در این نسخه حذف یا منسوخ(deprecate) شدهن:
این ماژولها در جاوا۹ deprecate شدن و حالادر جاوا۱۱ بطور کامل حذف شدن. اگر تصمیم دارید به جاوا۱۱ مهاجرت کنید، باید مطمئن بشید که پروژهی شما از هیچیک از بستهها یا ابزارهای زیر استفاده نکرده باشه:
Java API for XML-Based Web Services: (java.xml.ws) (JAX-WS)
Java Architecture for XML Binding: (java.xml.bind) (JAXB)
JavaBeans Activation Framework: (java.activation) (JAF)
Common Annotations: (java.xml.ws.annotation)
Common Object Request Broker Architecture: (java.corba) (CORBA)
JavaTransaction API: (java.transaction) (JTA)
Aggregator module for the six modules above: (java.se.ee )
این دو ماژول هم دیگر به صورت پیشفرض در JDK وجود ندارند. نسخههای جداگانهای از این دو (مجموعه ای ازماژولها) خارج از JKD برای دانلود و استفاده موجود هستن.
ماژول Nashorn هم که یک JavaScript Engin بود، در کنار Pak200 که برای فشردهسازی فایلهای JAR استفاده میشد در جاوا۱۱ deprecate شدن.
برخی امکانات برای افزایش عملکرد JDK معرفی شدهن:
جاوا۱۱ فرمت class-file رو برای پشتیبانی از فرم جدید constant-pool با نام CONSTANT_Dynamic توسعه داده. این فرمت، تولید constantها رو به یک روش bootstrap واگذار کرده که البته بیشتر به کار طراحان زبان و برنامه نویسان کامپایلر میخوره.
جاوا۱۱ رشته و آرایه Intrinsics رو در پردازندههای ARM64 یا AArch64 بهینه کرده. همچنین Intrinsics جدید برای متدهای ()Math.sin() ، Math.cos و ()Match.log پیاده سازی و بهینه شده.
برای ما برنامه نویسها استفاده از توابع Intrinsics مشابه استفاده از باقی متدهاست، اما تابع Intrinsics به روش خاصی توسط کامپایلر کنترل میشه و از کد CPU architecture-specific assembly برای افزایش عملکرد استفاده میکنه.
یک Garbage Collector جدید به نام Epsilon برای استفاده در جاوا۱۱، (البته به صورت آزمایشی) در دسترسه. بهش No-Op (no operations) میگیم چون حافظه رو تخصیص میده اما در واقع هیچ زبالهای رو جمع نمیکنه. بنابراین، Epsilon برای شبیه سازی خطاهای حافظه قابل استفاده ست. بدیهیه که Epsilon برای یک برنامه جاوای معمولی مناسب نیست. اما مواردخاصی وجود دارن که Epsilon میتونه در اونجاها مفید باشه:
برای فعال کردنش هم باید flagهای زیر رو فعال کنید:
-XX:+UnlockExperimentalVMOptions
-XX:+UseEpsilonGC
درواقع JFR یک profiling tool هست که میتونیم ازش برای جمع آوری داده های یک برنامهی درحالِ اجرایِ جاوا استفاده کنیم. این ابزار قبلاً یک محصول تجاری بود و برای استفاده ازش باید پول پرداخت میکردیم که در نسخه فعلی open source شده.
در جاوا۱۱ تغییرات دیگهای هم معرفی شدهن که لازمه مرور بشن:
در این مقاله ، سعی کردم با مطالعهی سایتهای مختلف، برخی از ویژگی های جدید Java 11 رو بررسی کنم. تفاوتهای Oracle JDK و Open JDK را شرح دادم و تغییرات APIها و باقی ویژگیهای مفید development ، performance enhancement ، و deprecated or deleted module روبررسی کردم که امیدوارم مورد استفادهی کسانی که مثل خودم تصمیم به ارتقاء نسخهی JDK دارن قرار بگیره.
خوشحالم می کنین اگر اشتباهات احتمالی و یا کمبودهای مقاله رو بهم اطلاع بدین؛ و ممنون که وقت گذاشتین.