מאז שהפלטפורמה של Java אימצה מחזור שחרור של שישה חודשים, עברנו מעבר לשאלות הקבועות כמו "האם Java תמות השנה?" או "האם שווה לעבור לגרסה החדשה?". למרות שחלפו 28 שנה מאז השחרור הראשון שלה, Java ממשיכה לשגשג ונשארת בחירה פופולרית כשפת תכנות ראשית עבור רבים מהפרויקטים החדשים.
Java 17 הייתה אבן דרך משמעותית, אך Java 21 לקחה עכשיו את מקומה של 17 כשחרור התמיכה לטווח הארוך הבא (LTS). זה חיוני למפתחים של Java להישאר מעודכנים לגבי השינויים והתכונות החדשות שגרסה זו מביאה. בהשראת הקולגה שלי דארק, שפרט תכונות של Java 17 במאמר שלו, החלטתי לדון ב-JDK 21 באותו אופן (גם ניתחתי תכונות של Java 23 במאמר מעקב, אז כדאי לבדוק גם את זה).
JDK 21 מורכב מסך של 15 JEPs (הצעות שיפור של JDK). אתה יכול לעיין ברשימה המלאה באתר הרשמי של Java. במאמר זה, אציין מספר JEPs של Java 21 שאני מאמין שהם ראויים לציון במיוחד. כלומר:
בלי עיכובים נוספים, בואו נצלול לקוד ונחקר את העדכונים הללו.
תבניות מחרוזות (תצוגה מקדימה)
תכונת תבניות סתיו עדיין במצב תצוגה מקדימה. כדי להשתמש בה, עליכם להוסיף את הדגל --enable-preview
לארגומנטים של המהדר שלכם. עם זאת, החלטתי להזכיר זאת למרות מעמד התצוגה המקדימה שלה. למה? כי אני מתעצבנת מאוד כל פעם שאני צריכה לכתוב הודעת יומן או משפט SQL שמכילים הרבה ארגומנטים או לפענח איזו מחזיקן יוחלף בארגומנט נתון. ותבניות סתיו מבטיחות לעזור לי (ולכם) עם זה.
כפי שאומרת תיעוד ה-JEP, המטרה של תבניות סתיו היא "לפשט את כתיבת תוכניות Java על ידי הקלה על הבעה של מחרוזות שכוללות ערכים מחושבים בזמן ריצה."
בואו נבדוק אם זה באמת פשוט יותר.
ה"דרך הישנה" הייתה להשתמש בשיטה formatted()
על אובייקט מחרוזת:
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 הציגה היררכיית אוספים חדשה בג'אווה. הסתכלו על התרשים למטה והשוו אותו למה שלמדתם ככל הנראה במהלך שיעורי התכנות שלכם. תבחינו כי נוספו שלושה מבנים חדשים (מודגשים בצבע ירוק).
אוספים מסודרים מציגים API חדש מובנה בג'אווה, אשר משפר את הפעולות על מערכי נתונים מסודרים. API זה מאפשר לא רק גישה נוחה לאלמנטים הראשונים והאחרונים של אוסף, אלא גם מאפשר מעבר יעיל, הוספה במיקומים ספציפיים, ומשיכת תתי-רצפים. השיפורים הללו הופכים פעולות התלויות בסדר האלמנטים לפשוטות ואינטואיטיביות יותר, ומשפרים גם את הביצועים וגם את קריאות הקוד בעת עבודה עם רשימות ומבני נתונים דומים.
זהו הרשימה המלאה של ממשק 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
ותבניות הרשומה, אתרחיב עליהם ביחד. תבניות הרשומה הן תכונה חדשה: הן הוצגו ב-Java 19 (כתצופה). מצד שני, תיבת התאמה של switch
היא סוג של המשכיות של ביטוי ה-instanceof המורחב. היא מביאה לתוך הקוד סינטקס חדש של פקודות switch
אשר מאפשר לך לבטא שאילתות מורכבות המבוססות על נתונים בצורה יותר קלה.
בואו נשכח מיסודות ה-OOP למענה של דוגמה ונפרק את אובייקט העובד באופן ידני (employee
הוא מחלקת POJO).
לפני Java 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
המכוער? טוב, עכשיו אנחנו יכולים, בזכות עוצמת תיבת ההתאמה מ-Java 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
, אנו יכולים גם לשוחח על תכונת תבניות הרשומה. בעת התמדות עם רשומה ב-Java, זה מאפשר לנו לעשות הרבה יותר מאשר עם מחלקת Java סטנדרטית:
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);
}
כפי שהקוד מראה, עם הסינטקס הזה, שדות הרשומה נגישים בקלות. בנוסף, אנו יכולים להוסיף קצת לוגיקה נוספת להצהרות ה-case שלנו:
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
}
למיתוג תהליכים וירטואליים
תכונת ה- Virtual Threads היא כנראה הכי חמה מבין כל ה- Java 21 – או לפחות אחת מהתכונות שפתחי Java חיכו להן הכי הרבה. כפי שאומרת תיעוד ה-JEP (שקישורו במילה הקודמת), אחד מהמטרות של חוטים וירטואליים היה "לאפשר יישומי שרת שנכתבו בסגנון חוט-לפי-בקשה להתרחב עם שימוש אופטימלי כמעט בחומרה". עם זאת, האם זה אומר שעלינו למגר את כל הקוד שלנו שמשתמש ב- java.lang.Thread
?
ראשית, נבחן את הבעיה בגישה שהייתה קיימת לפני Java 21 (בעצם, pretty much מאז השחרור הראשון של Java). אנו יכולים להעריך שחוט אחד של java.lang.Thread
צורך (בהתאם ל-OS ולתצורה) כ-2 עד 8 MB של זיכרון. עם זאת, הדבר החשוב כאן הוא שחוט Java אחד מותאם 1:1 לחוט ליבת מערכת. עבור אפליקציות אינטרנט פשוטות שמשתמשות בגישה של "חוט אחד לכל בקשה", נוכל בקלות לחשב שהמחשב שלנו או "יומת" כאשר התנועה תגדל (לא יוכל להתמודד עם העומס) או שנאלץ לרכוש מכשיר עם זיכרון RAM נוסף, וחיובי ה-AWS שלנו יגדלו כתוצאה מכך.
כמובן, חוטים וירטואליים אינם הדרך היחידה להתמודד עם הבעיה הזו. יש לנו תכנות אסינכרוני (מסגרות כמו WebFlux או API מקורי של Java כמו CompletableFuture
). עם זאת, מסיבה כלשהי – אולי בגלל ה-"API הלא ידידותי" או סף הכניסה הגבוה – הפתרונות הללו אינם פופולריים כל כך.
חוטים וירטואליים אינם מנוהלים או מתוזמנים על ידי מערכת ההפעלה. במקום זאת, תהליך התזמון שלהם מתבצע על ידי ה-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.
הדבר האחרון על חוטים וירטואליים שאני רוצה להראות לכם הוא כיצד הם יכולים לעזור לכם עם פעולות I/O חסומות.
כשהחוט הווירטואלי נתקל בפעולה חסומה, כמו משימות I/O, ה-JVM מנתק אותו ביעילות מהחוט הפיזי הבסיסי (חוט הנושא). ההפרדה הזו היא קריטית מכיוון שהיא פוטרת את חוט הנושא כדי להריץ חוטים וירטואליים אחרים במקום להמתין ללא מעש, מחכה לסיום הפעולה החסומה. כתוצאה מכך, חוט נושא אחד יכול למולטי-פלקס הרבה חוטים וירטואליים, שיכולים להיות במספרים של אלפים או אפילו מיליונים, תלוי בזיכרון הזמין ובטבע המשימות שמתבצעות.
נסה לשחזר את ההתנהגות הזו. כדי לעשות זאת, נאלץ את קוד שלנו להשתמש בלבד בליבה אחת של מעבד, עם רק 2 תהליכי וירטואליים – למטרת בהירות טובה.
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 — אני ממליץ בחום לנתח את רשימת השיפורים המלאה ולחקור את כל התכונות נוספות. לדוגמה, דבר אחד שאני חושב שראוי לשים לב אליו הוא ממשק ה-Vector API, שמאפשר חישובי וקטור על מספר ארכיטקטורות של CPU מובילות — משהו שלא היה אפשרי קודם. כרגע, הוא עדיין בשלב האקובציה/שלב הניסיון (לכן לא דגשתי עליו בפרטים נוספים כאן), אך הוא מחזיק בו פוטנציאל רב לעתיד של Java.
בכלל, התקדמות שעשתה Java בתחומים שונים מעידה על המחויבות המתמדת של הצוות לשיפור יעילות וביצועים ביישומים בעלי דרישה גבוהה.
Source:
https://dzone.com/articles/java-21-features-a-detailed-look