منذ اعتماد منصة جافا دورة إصدار تستمر لمدة ستة أشهر، لقد تجاوزنا الأسئلة الدائمة مثل “هل ستموت جافا هذا العام؟” أو “هل يستحق الترقية إلى الإصدار الجديد؟”. على الرغم من مرور 28 عامًا على الإصدار الأول لها، تستمر جافا في الازدهار وتظل خيارًا شائعًا كلغة برمجة رئيسية للعديد من المشاريع الجديدة.
كانت جافا 17 نقطة مهمة، ولكن جافا 21 احتلت الآن مكان 17 كإصدار الدعم طويل المدى (LTS) التالي. من الضروري على المطورين في جافا البقاء على اطلاع على التغييرات والميزات الجديدة التي يجلبها هذا الإصدار. مستوحاة من زميلي داريك، الذي قدم ميزات جافا 17 في مقاله، قررت مناقشة JDK 21 بنفس الأسلوب (لقد قمت أيضًا بتحليل ميزات جافا 23 في قطعة تتبعية، لذا تحقق منها أيضًا).
يتألف JDK 21 من مجموعة من 15 JEPs (مقتراحات تعزيز JDK). يمكنك مراجعة القائمة الكاملة على الموقع الرسمي لجافا. في هذه المقالة، سأسلط الضوء على عدة JEPs في جافا 21 أعتقد أنها تستحق الاهتمام بشكل خاص. وتتضمن:
دون تأخير، دعونا نستكشف الكود ونستكشف هذه التحديثات.
قوالب السلاسل (معاينة)
ميزة قوالب السلاسل لا تزال في وضع المعاينة. لاستخدامها، يجب عليك إضافة العلم --enable-preview
إلى وسيط المترجم الخاص بك. ومع ذلك، قررت ذكرها على الرغم من وضعها كمعاينة. لماذا؟ لأنني أصبحت مستاءً جدًا في كل مرة يجب علي كتابة رسالة سجل أو عبارة SQL تحتوي على العديد من الوسائط أو فك شيفرة أي بدل سيتم استبداله بوسيط معين. وتعد قوالب السلاسل وعدت لمساعدتي (وأنت) في ذلك.
كما يقول وثائق JEP، الغرض من قوالب السلاسل هو “تبسيط كتابة برامج Java من خلال جعلها سهلة للتعبير عن سلاسل تتضمن قيم تحسب في وقت التشغيل.”
دعونا نتحقق ما إذا كانت الأمور حقًا أبسط.
الطريقة “القديمة” ستكون باستخدام طريقة formatted()
على كائن String:
var msg = "Log message param1: %s, pram2: %s".formatted(p1, p2);
الآن، مع StringTemplate.Processor
(STR)، تبدو الأمور على النحو التالي:
var interpolated = STR."Log message param1: \{p1}, param2: \{p2}";
مع نص قصير مثل الذي ذكرناه أعلاه، قد لا يكون الربح ظاهرًا كثيرًا – ولكن صدقني، عندما يتعلق الأمر بكتل نص كبيرة (JSONs، عبارات SQL، الخ)، فإن المعلمات المسماة ستساعدك كثيرًا.
مجموعات متسلسلة
لقد قدمت جافا 21 تسلسلًا جديدًا لتسلسل جافا. انظر إلى الرسم البياني أدناه وقارنه بما ربما تعلمته خلال دروس البرمجة الخاصة بك. ستلاحظ أنه تم إضافة ثلاث هياكل جديدة (تميزت باللون الأخضر).
تقدم مجموعات السلسلة تحسينًا جديدًا في واجهة برمجة التطبيقات جافا المدمجة، مما يعزز العمليات على مجموعات البيانات المرتبة. تسمح هذه الواجهة ليس فقط بالوصول المريح إلى عناصر المجموعة الأولى والأخيرة ولكنها تمكن أيضًا من عبور فعال، وإدراج في مواقع محددة، واسترداد متتاليات فرعية. تجعل هذه التحسينات العمليات التي تعتمد على ترتيب العناصر أكثر بساطة وأكثر تفهمًا، مما يحسن الأداء وقراءة الشفرة عند العمل مع القوائم والهياكل البيانية المماثلة.
هذا هو القائمة الكاملة لواجهة SequencedCollection
:
public interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
default void addFirst(E e) {
throw new UnsupportedOperationException();
}
default void addLast(E e) {
throw new UnsupportedOperationException();
}
default E getFirst() {
return this.iterator().next();
}
default E getLast() {
return this.reversed().iterator().next();
}
default E removeFirst() {
var it = this.iterator();
E e = it.next();
it.remove();
return e;
}
default E removeLast() {
var it = this.reversed().iterator();
E e = it.next();
it.remove();
return e;
}
}
لذا، الآن، بدلاً من:
var first = myList.stream().findFirst().get();
var anotherFirst = myList.get(0);
var last = myList.get(myList.size() - 1);
يمكننا فقط كتابة:
var first = sequencedCollection.getFirst();
var last = sequencedCollection.getLast();
var reversed = sequencedCollection.reversed();
هذا تغيير بسيط، ولكن في رأيي الشخصي، إنه ميزة مريحة وقابلة للاستخدام.
تطابق الأنماط وأنماط السجل
بسبب التشابه في مطابقة النمط لswitch
ونماذج السجل، سأصفهما معًا. تعتبر نماذج السجل ميزة جديدة: تم تقديمها في جافا 19 (كمعاينة). من ناحية أخرى، مطابقة النمط لswitch
هي نوعًا من استمرار لتعبير extended instanceof
. إنها تقدم بناء جديدة لبيانات switch
التي تتيح لك التعبير بسهولة عن استعلامات موجهة للبيانات المعقدة.
لننسى أساسيات البرمجة الشيئية من أجل هذا المثال ونقوم بتفكيك كائن الموظف يدويًا (employee
هو فئة POJO).
قبل جافا 21، كان يبدو هكذا:
if (employee instanceof Manager e) {
System.out.printf("I’m dealing with manager of %s department%n", e.department);
} else if (employee instanceof Engineer e) {
System.out.printf("I’m dealing with %s engineer.%n", e.speciality);
} else {
throw new IllegalStateException("Unexpected value: " + employee);
}
ماذا لو استطعنا التخلص من instanceof
القبيح؟ حسنًا، الآن يمكننا ذلك، بفضل قوة مطابقة النمط من جافا 21:
switch (employee) {
case Manager m -> printf("Manager of %s department%n", m.department);
case Engineer e -> printf("I%s engineer.%n", e.speciality);
default -> throw new IllegalStateException("Unexpected value: " + employee);
}
أثناء الحديث عن بيان switch
، يمكننا أيضًا مناقشة ميزة نماذج السجل. عند التعامل مع سجل جافا، يسمح لنا بفعل أكثر بكثير مما يمكن فعله مع فئة جافا القياسية:
switch (shape) { // shape is a record
case Rectangle(int a, int b) -> System.out.printf("Area of rectangle [%d, %d] is: %d.%n", a, b, shape.calculateArea());
case Square(int a) -> System.out.printf("Area of square [%d] is: %d.%n", a, shape.calculateArea());
default -> throw new IllegalStateException("Unexpected value: " + shape);
}
كما يوضح الكود، من خلال تلك الصيغة، يمكن الوصول بسهولة إلى حقول السجل. علاوة على ذلك، يمكننا وضع بعض المنطق الإضافي لعبارات الحالة:
switch (shape) {
case Rectangle(int a, int b) when a < 0 || b < 0 -> System.out.printf("Incorrect values for rectangle [%d, %d].%n", a, b);
case Square(int a) when a < 0 -> System.out.printf("Incorrect values for square [%d].%n", a);
default -> System.out.println("Created shape is correct.%n");
}
يمكننا استخدام صيغة مماثلة لعبارات if
. أيضًا، في المثال أدناه، يمكننا أن نرى أن نماذج السجل تعمل أيضًا للسجلات المتداخلة:
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
//sth
}
المواضيع الافتراضية
تعتبر ميزة الخيوط الافتراضية من بين الأكثر إثارة في جافا 21 – أو على الأقل واحدة من أكثر الميزات التي انتظرها مطورو جافا. كما تقول وثائق JEP (المربوطة في الجملة السابقة)، كان أحد أهداف الخيوط الافتراضية هو “تمكين تطبيقات الخادم المكتوبة بأسلوب خيط لكل طلب من التوسع مع استخدام قريب من المثالية للعتاد”. ومع ذلك، هل يعني هذا أننا يجب أن ننتقل بكودنا بالكامل الذي يستخدم java.lang.Thread
؟
أولاً، دعونا نفحص المشكلة مع الأسلوب الذي كان موجودًا قبل جافا 21 (في الواقع، منذ الإصدار الأول لجافا تقريبًا). يمكننا تقدير أن خيطًا واحدًا من java.lang.Thread
يستهلك (اعتمادًا على نظام التشغيل والتكوين) حوالي 2 إلى 8 ميغابايت من الذاكرة. ومع ذلك، الشيء المهم هنا هو أن خيط جافا واحد يتم تعيينه 1:1 إلى خيط نواة. بالنسبة لتطبيقات الويب البسيطة التي تستخدم نهج “خيط واحد لكل طلب”، يمكننا بسهولة حساب أنه إما أن يتم “قتل” جهازنا عندما تزداد حركة المرور (لن يتمكن من تحمل الحمل) أو سنضطر إلى شراء جهاز بذاكرة وصول عشوائي أكبر، وستزداد فواتير AWS لدينا نتيجة لذلك.
بالطبع، ليست الخيوط الافتراضية هي الطريقة الوحيدة للتعامل مع هذه المشكلة. لدينا البرمجة غير المتزامنة (أطر العمل مثل WebFlux أو واجهة برمجة التطبيقات الأصلية لجافا مثل CompletableFuture
). ومع ذلك، لسبب ما – ربما بسبب “واجهة برمجة التطبيقات غير الصديقة” أو العتبة العالية للدخول – فإن هذه الحلول ليست شائعة جدًا.
الخيوط الافتراضية لا تخضع لرقابة أو جدولة من قبل نظام التشغيل. بدلاً من ذلك، يتم التعامل مع جدولة هذه الخيوط من قبل JVM. بينما يجب تنفيذ المهام الحقيقية في خيط منصة، يستخدم JVM ما يُسمى بخيوط الحاملة – وهي في الأساس خيوط منصة – “لتحمل” أي خيط افتراضي عندما يحين وقت تنفيذها. تم تصميم الخيوط الافتراضية لتكون خفيفة الوزن وتستخدم ذاكرة أقل بكثير من خيوط المنصة القياسية.
يوضح الرسم البياني أدناه كيف ترتبط الخيوط الافتراضية بخيوط المنصة ونظام التشغيل:
لذا، لرؤية كيفية استخدام الخيوط الافتراضية بواسطة خيوط المنصة، دعنا نشغل كودًا يبدأ (1 + عدد وحدات المعالجة المركزية التي يمتلكها الجهاز، في حالتي 8 نوى) خيوط افتراضية.
var numberOfCores = 8; //
final ThreadFactory factory = Thread.ofVirtual().name("vt-", 0).factory();
try (var executor = Executors.newThreadPerTaskExecutor(factory)) {
IntStream.range(0, numberOfCores + 1)
.forEach(i -> executor.submit(() -> {
var thread = Thread.currentThread();
System.out.println(STR."[\{thread}] VT number: \{i}");
try {
sleep(Duration.ofSeconds(1L));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}));
}
تبدو المخرجات كما يلي:
[VirtualThread[#29,vt-6]/runnable@ForkJoinPool-1-worker-7] VT number: 6
[VirtualThread[#26,vt-4]/runnable@ForkJoinPool-1-worker-5] VT number: 4
[VirtualThread[#30,vt-7]/runnable@ForkJoinPool-1-worker-8] VT number: 7
[VirtualThread[#24,vt-2]/runnable@ForkJoinPool-1-worker-3] VT number: 2
[VirtualThread[#23,vt-1]/runnable@ForkJoinPool-1-worker-2] VT number: 1
[VirtualThread[#27,vt-5]/runnable@ForkJoinPool-1-worker-6] VT number: 5
[VirtualThread[#31,vt-8]/runnable@ForkJoinPool-1-worker-6] VT number: 8
[VirtualThread[#25,vt-3]/runnable@ForkJoinPool-1-worker-4] VT number: 3
[VirtualThread[#21,vt-0]/runnable@ForkJoinPool-1-worker-1] VT number: 0
لذا، ForkJonPool-1-worker-X
خيوط المنصة هي خيوطنا الحاملة التي تدير خيوطنا الافتراضية. نلاحظ أن الخيوط الافتراضية رقم 5 و 8 تستخدم نفس خيط الحامل رقم 6.
آخر شيء عن الخيوط الافتراضية أريد أن أظهره لك هو كيف يمكن أن تساعدك في عمليات الإدخال/الإخراج المحجوزة.
عندما تواجه خيط افتراضي عملية محجوزة، مثل مهام الإدخال/الإخراج، يقوم JVM بفصلها بكفاءة عن خيط المنصة الفعلي (خيط الحامل). هذا الفصل حاسم لأنه يحرر خيط الحامل لتشغيل خيوط افتراضية أخرى بدلاً من أن يكون عاطلاً، في انتظار إكمال العملية المحجوزة. ونتيجة لذلك، يمكن أن يقوم خيط حامل واحد بتعدد مهام العديد من الخيوط الافتراضية، التي قد تصل أعدادها إلى الآلاف أو حتى الملايين، اعتمادًا على الذاكرة المتاحة وطبيعة المهام المنفذة.
دعونا نحاول محاكاة هذا السلوك. للقيام بذلك، سنجبر كودنا على استخدام نواة واحدة فقط من وحدة المعالجة المركزية، مع وجود خيطين افتراضيين فقط – من أجل وضوح أفضل.
System.setProperty("jdk.virtualThreadScheduler.parallelism", "1");
System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1");
System.setProperty("jdk.virtualThreadScheduler.minRunnable", "1");
الخيط 1:
Thread v1 = Thread.ofVirtual().name("long-running-thread").start(
() -> {
var thread = Thread.currentThread();
while (true) {
try {
Thread.sleep(250L);
System.out.println(STR."[\{thread}] - Handling http request ....");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
);
الخيط 2:
Thread v2 = Thread.ofVirtual().name("entertainment-thread").start(
() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var thread = Thread.currentThread();
System.out.println(STR."[\{thread}] - Executing when 'http-thread' hit 'sleep' function");
}
);
التنفيذ:
v1.join(); v2.join();
النتيجة:
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#23,entertainment-thread]/runnable@ForkJoinPool-1-worker-1] - Executing when 'http-thread' hit 'sleep' function
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
نلاحظ أن كلا الخيطين الافتراضيين (long-running-thread
وentertainment-thread
) يتم التعامل معهما بواسطة خيط منصة واحد فقط، وهو ForkJoinPool-1-worker-1
.
لتلخيص الأمر، هذا النموذج يمكن تطبيقات Java من تحقيق مستويات عالية من التزامن وقابلية التوسع مع تكاليف أقل بكثير من نماذج الخيوط التقليدية، حيث يتطابق كل خيط مباشرة مع خيط نظام تشغيل واحد. من الجدير بالذكر أن الخيوط الافتراضية موضوع شاسع، وما وصفته هو مجرد جزء صغير. أشجعكم بشدة على معرفة المزيد حول الجدولة، والخيوط المثبتة، والداخلية للخيوط الافتراضية.
الملخص: مستقبل لغة البرمجة Java
الميزات الموصوفة أعلاه هي ما أعتبره الأكثر أهمية في Java 21. معظمها ليست رائدة كما أن بعض الأشياء المقدمة في JDK 17، لكنها لا تزال مفيدة جدًا، وتعد تغييرات جيدة لتحسين جودة الحياة (QOL).
ومع ذلك، لا ينبغي أن تتجاهل تحسينات JDK 21 الأخرى أيضًا — أنا أشجعك بشدة على تحليل القائمة الكاملة واستكشاف جميع الميزات أكثر. على سبيل المثال، أحد الأشياء التي أعتبرها جديرة بالذكر بشكل خاص هو واجهة برمجة التطبيقات الخاصة بالمتجهات، التي تسمح بإجراء حسابات متجهية على بعض بنى المعالجة المركزية المدعومة — وهو أمر لم يكن ممكنًا من قبل. حاليًا، لا تزال في حالة الحضانة/المرحلة التجريبية (لهذا السبب لم أقم بتسليط الضوء عليها بمزيد من التفاصيل هنا)، لكنها تحمل وعدًا كبيرًا لمستقبل Java.
بشكل عام، تشير التقدمات التي حققتها Java في مجالات مختلفة إلى التزام الفريق المستمر بتحسين الكفاءة والأداء في التطبيقات ذات الطلب العالي.
Source:
https://dzone.com/articles/java-21-features-a-detailed-look