ג'אווה 17

Photo by Markus Spiske on Unsplash

שלום לכולם,
הפעם אני רוצה לדבר אתכם על java 17. ב-Tufin עד לאחרונה היינו משתמשים בג'אווה 8, ובזמן האחרון אנחנו עושים את המעבר לג'אווה 17.
תוך כדי המעבר הזה שמתי לב שאני לא כל כך יודע להסביר את ההבדלים בין הגרסאות הללו אז החלטתי לצלול קצת פנימה.

ציר הזמן

ג'אווה 8

אנחנו מתחילים את המסע שלנו בנבכי הג'אווה בשנת 2014, אז יוצאת גרסת ג'אווה 8 לעולם. גרסה שנחשבת כגרסה מאוד משמעותית בעולמות הג'אווה. היא הביאה הרבה חידושים כמו Lambda Expressions, – Stream API

ג'אווה 9

לאחר שלוש שנים וחצי, בספטמבר 2017, אנחנו מקבלים את גרסת ג'אווה 9, שהיא מציגה לעולם את המודולים.
עוד שינוי משמעותי בגרסה הזאת היא ההחלטה של אורקל לעבור לעולם של שני releases בשנה. אחד במרץ ואחד בספטמבר.
המטרה הייתה שכאשר תצא גרסה חדשה, הגרסה הקודמת הופכת להיות פגת תוקף. שזה בעיקרון צעד בכיוון הנכון, אבל לא כל התעשייה מוכנה לעבוד ככה.
ה-corporates של העולם למשל מחפשים יציבות. הם לא רוצים להחליף כל חצי שנה גרסת ג'אווה. ולכן מה שקורה בפועל הוא שכל כמה גרסאות, מגדירים גרסה בתור גרסת LTS. זוהי גרסה שתקבל תמכיה לזמן ארוך (Long Term Support).

ג'אווה 10

משוחררת לעולם במרץ 18, והיא מציגה לעולם את ה-local variable type interface, שבקצרה זה האפשרות לכתוב קוד ככה:

Java
var list = new ArrayList();
var stream = list.stream();

הערה חשובה גרסאות ג'אווה 9 ו-10 נחשבות לגרסאות ביניים, שהייתה להן תמיכה קצרה.

ג'אווה 11

הגרסת LTS הראשונה מאז ג'אווה 8. היא משוחררת בספטמבר 18.
ג'אווה 11 הציגה את ה-Flight Recorder. זהו כלי אשר מאפשר לנו לעשות מעקב אחרי איוונטים אשר קוראים בזמן ריצה.

ג'אווה 17

נדלק קצת קדימה בזמן, עד לג'אווה 17 משתי סיבות.
הראשונה היא שזה נושא הפוסט עצמו, והשנייה היא ש-17 היא הגרסת LTS הבאה בתור.
בין גרסאות 12 עד גרסה 17 יש סה"כ 67 JEP.

איך משנים שפת תכנות

מה זה JEP

שוב פעם הוא התחיל לקלל? (בטח זה מה שעובר לחלקכם בראש).
אז JEP זה ראשי תיבות של JDK Enhancement Proposal. כלומר הצעה לשינוי ב-JDK, שבאה עם פורמט מאוד מסודר.
אוסף כל ה-JEP מהווה בעצם את ה-road map של ה-JDK.

את כל ה-JEP אפשר לחלק לארבע קטגוריות:

  1. דברים שנוספו
  2. דברים שירדו
  3. דברים שנוספו ולא עד הסוף
  4. דברים שירדו אבל לא עד הסוף

דברים שנוספו

גם כאן אנחנו יכולים לחלק לקטגוריות: לאן נוספו הדברים.

  1. דברים שנוספו ל-JVM, למשל ZGCשזהו Garbage Collector חדש ומאוד מאוד יעיל (מוכן לפרודקשיין מאז ג'אווה 15).
  2. שינויים בשפה – על זה אנחנו נרחיב בהמשך הפוסט
  3. ספריות- מימוש מחדש של Socket API למשל.
  4. כלים – כלים חדשים שנכנסו לשפה, למשל jpackageשנכנס בג'אווה 14 ומאפשר לנו לבנות installers.

דברים שנוספו ולא עד הסוף

כאשר Oracle עברו לשחרר שתי גרסאות בשנה, הם היו צריכים לפתח שיטה איך להכניס שינויים בשפה על מנת לקבל פידבק מהתעשייה.
אז בג'אווה 12 הם הכניסו פיצ'רים חדשים, כשכדי להשתמש בהם צריך להדליק פלאג גם בזמן קומפילציה וגם בזמן ריצה.
בעקבות הפידבק Oracle יכולה להבין מה צריך לשנות ומה לא.

דברים שירדו אבל לא עד הסוף

אלו בעצם פיצ'רים ש-Oracle רוצה להוציא החוצה, אבל היא לא רוצה לעשות את זה בצעד חד מאוד, אז היא מגדירה אותם כ-deprecated. כלומר הם עדיין יעבדו, אבל מכינים את המשתמשים שבגרסאות הבאות זה יכול כבר לא לעובד, ולכן צריך להתחיל למצוא החלפות.
בג'אווה 17, למשל הכריזו על ה-Security Manager כ-Deprecated.

דברים שירדו

אלו דברים שנמחקו רשמית וסופית מהשפה.
למשל בג'אווה 17 מחקו את ה-CMS

שינויים בשפה בג'אווה 17

אבל מה מעניין אותנו המתכנתים בסופו של דבר? לכתוב קוד!
ולכן השינויים בגרסאות ג'אווה שבדרך כלל מעניינות את המפתחים זה השינויים בשפה.
בין גרסאות 12 עד 17 נכנסו שישה JEP שקשורים לשפה

  1. Pattern Matching for instanceof(16)
  2. Records(16)
  3. Sealed Classes(17)
  4. Text Blocks(15)
  5. Switch Expressions(14)
  6. Restore Always Strict Floating Point Semantics(17)
    בסוגריים מופיעות ה-JDK בו הם נכנסו סופית.

Pattern Matching for instanceof

השינוי הזה נכנס תחת JEP394
בואו נסתכל על הקטע קוד הבא:

Java
void printInUpperCase(Object obj) {
    if (obj instanceof String) {
        String s = (String) obj;
        System.out.println(s.toUpperCase());
    }
}

אני בטוח שראיתם קוד כזה בעבר.
קיבלנו איזשהו אובייקט שאני רוצה לוודא שהאובייקט הוא מסוג String, ורק אחרי זה אנחנו ממירים אותו ל-String.
אבל נשאלת השאלה, אנחנו יודעים שזה String מיד אחרי הבדיקה, למה צריך לעשות המרה? למה הקומפיילר לא לומד את זה לבד?
ובכן החל מ-Java 17, הוא אכן יודע:

Java
void printInUpperCase(Object obj) {
    if (obj instanceof String s) {
        System.out.println(s.toUpperCase());
    }
}

יש לנו פה תבנית שאנחנו צריכים לעקוב אחריה:
obj instance of String **s**
ככה אנחנו נותנים לאובייקט שם וממירים אותו בשורה אחת.

ועכשיו נתסכל על זה בצורה יותר כללית:

Java
if (a instanceof Point p) {
    // p is in scope
}
// p is NOT in scope here
if (b instanceof Point p) {
    // new p is in scope
}

עכשיו אנחנו יכולים להרחיב את הביטוי עוד:

Java
if (obj instanceof String s && s.length() > 5) {
    flag = s.contains("jdk");
}

אם הגעתי לצד הימני של הביטוי אנחנו יכולים להיות בטוחים ש-s אכן מוגדר.
אבל את הקוד הבא אנחנו לא יכולים לכתוב:

Java
if (obj instanceof String s || s.length() > 5) { // ERROR!!
    ...
}

כי כאן אנחנו כבר לא יכולים להיות בטוחים ש-s מוגדר. יכול להיות שהצד השמאלי נכשל בבדיקה שלו.

בואו נסתכל על דוגמה יותר מורכבת. הקומפיילר של ג'אווה מוגדר כ-context aware, כלומר הוא מבין את ה-flow של הקוד שלנו:

Java
public void onlyForStrings(Object o) {
    if (!(o instanceof String s)) {
        throw new IllegalArgumentException();
    }
    // s is in scope
    System.out.println(s.trim());
}

במקרה הזה הקומפיילר שלנו הבין ש-s אכן להיות String, כי אחרת היינו עפים על exception.
הקוד הזה יעבוד באותה צורה אם נכתוב string.

השוואות

איפה אנחנו רואים הרבה casting ב-Java? כשאר אנחנו בודקים אם שני אובייקטים הם שווים.
בהרבה מקרים אנחנו רואים קוד כזה:

Java
public final boolean equals(Object o) {
    if (!(o instanceof MyString)) {
        return false;
    }
    MyString s = (MyString) o;
    return this.equalsIgnoreCase(s);
}

עכשיו אנחנו יכולים לרשום את זה ככה:

Java
public final boolean equals(Object o) {
    return (o instanceof MyString ms) && this.equalsIgnoreCase(ms);
}

Records

השינוי הזה נכנס תחת JEP395
אחת התלונות הכי גדולות על Java היא שזוהי שפה שמלאה בטקסים.
הדוגמה הבולטת בנושא היא ה-DTO (Data Transfer Object).
אנחנו בעצם יוצרים class שכל מטרתו בחיים זה להחזיק ולהעביר מידע, או state.
למשל:

Java
class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int x() {
        return x;
    }

    int y()  {
        return y;   
    }

    public boolean equals (Object o) {
        if (!(o instanceof Point)) return false;
        Point other = (Point) o;
        return other.x == x && other.y == y;
    }

    public int hashCode() {
        return Objects.hash(x, y);
    }

    public String toString() {
        return String.format("Point[x=%d, y=%d]", x, y);
    }
}

תראו כמה קוד היינו צריכים לכתוב בשביל דבר כל כך פשוט.
אנחנו צריכים בנאי, ואז אפשרות לקבל את השדות, ואז לבדוק אם שווה, ולהיות מסוגלים לשמור את זה בתוך hash tables למיניהם ולהמיר ל-String.
נורא ארוך ומסורבל.
ומה יקרה אם אני ארצה להוסיף עוד שדה? אני צריך לעדכן את הבנאי, להוסיף מתודה שתחזיר את השדה, ולעדכן את כל שאר המתודות.

נכון, יש דרכים לעקוף את זה יחד עם Lombok, אבל עדיין משהו הפריע לכותבי שפת ג'אווה.
הם רצו לסמן את המחלקות הללו בתור מחלקות שמחזיקות state.

ולכן הכניסו את השינוי הזה, את records.

Java
record Point(int x, int y) {}

בעצם זה דומה ל-Kotlin Data Class.

יש לנו keyword חדש

אז בואו נפרד את השורה הזאת:
יש לנו את record שזו מילת מפתח חדשה בשפה, אשר מחליפה את class.
השם נשאר אותו דבר Point, ויש לנו את ה-Header שבו יש לנו את הרשימה של המרכיבים.

מה שקורה מאחורי הקלעים הוא כזה:

  • עבור כל מרכיב שעובר ב-Header קורים שני דברים
    • נוסף getter עם שם כמו השם של המרכיב (לדוגמה x())
    • שדה שהוא private final
  • נוצר בנאי שהחתימה שלו היא כמו של ה-Header שמחבר בין כל שדה פרטי לערך המתאים לו.
  • מתודות דיפולטיביות של equals ושל hashCode
    • המחלקות זהות רק אם הן מאותו סוג, ויש להם את אותן ערכים במרכיבים שלהם.
  • מתודה של toString, שמחזירה את השדות יחד עם השמות שלהם.

Implicitly Final

==הערה חשובה== המחלקות מסוג records הן Implicitly Final, כלומר שהן לא יכולות לרשת מאף אחד, ואף אחד לא יכול לרשת מהם (יכולות לממש interface), והן לא יכולות להיות אבסטרקטיות.
למה בעצם?
ובכן אם כל ההיגיון שלנו היה שהמחלקות הללו הן מחזיקות state, אז אם אנחנו נירש ממחלקה אחרת, אנחנו בעצם מוסיפים עוד מצב על המחלקה הזאת.
נסתכל על הדוגמה הבאה:

Java
class A {}

class B: A {}

מה יחזיר הקוד הבא:

Java
B b = new B();
return b instanceof A;

יחזיר כמובן true.
וזה מצב שאנחנו מנסים למנוע כאן.

בנאים

כמו שכבר כתבתי למעלה, Java 17 תיצור באופן אוטומטי בנאי דיפולטיבי, אבל זה לא אומר שאנחנו לא יכולים ליצור אחד משלנו:

Java
record Range(int lo, int hi) {
    Range(int lo, int hi) {
        if (lo > hi) {
            throw new IlleagalArgumentException();
        }
    }
}

מה שחשוב לדעת כאן הוא שהחתימה של הבנאי צריכה להיות זהה לחלוטין ל-Header של ה-record.
אבל רגע, איפה ההשמה?
ובכן אם אנחנו נכתוב משהו מהסגנון הזה: this.lo = lo אנחנו נקבל שגיאת קומפילציה.
אנחנו יכולים לעשות ולידציות על הפרמטרים שקיבלנו, אנחנו יכולים לשנות להם את הערך, אבל ההשמה תקרה אוטומטית מאחורי הקלעים.
ה-constructor הזה נקרה Canonical Constructor.
אם אנחנו מוסיפים עוד constructors אחרים, הם יהיו חייבים לקרוא ל-canonical constructor.

Local records

בואו נסתכל על עוד שימוש.
קוד אשר מייצר וצורך הרבה מופעים של record class, ככל הנראה יצטרך סוג של DTO ביניים, שמחזיק מידע לזמן קצר לטובת חישובים כאלה ואחרים.
ולכן יש לנו אפשרות להגדיר Local Record.
למשל בדוגמה הבאה:

Java
List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
    // Local record
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

בדוגמה הזאת אנחנו רוצים למיין את כל הסוחרים שלנו לפי הביצועים שלהם פר חודש.
נשים לב שאנחנו משתמשים במחלקת ביניים בשם MerchantSales על מנת לחשב כמה מכירות יש לכל אחד בחודש.
אז במקום לייצר קלאס מלא לזה בצד, אנחנו יכולים לנצל את ה-record class שהיא גם ככה מאוד פשוט לכתיבה (שורה אחת).

Sealed Classes

אנחנו משתמשים בהורשה ב-OOP.
אבל לפעמים אנחנו רוצים לשלוט במי יכול להרחיב את המחלקה שלנו, או לאיזה ערכים אני יכול לקבל.
לדוגמה, אם אני יוצר מחלקה בשם Planet, אני לא רוצה שהשם שלה יגיע ב-constructor, אני רוצה לשלוח מה האופציות שיהיו.
אז בשביל זה אנחנו משתמשים ב-enum:

Java
enum Planet { MERCURY, VENUS, EARTH }

Planet p = ...
switch (p) {
  case MERCURY: ...
  case VENUS: ...
  case EARTH: ...
}

במקרים של ערכים enum באמת עוזר לנו, אבל יש מקרים שבהם אנחנו רוצים לשלוט בסוג.
לדוגמה:

Java
interface Celestial { ... }
class Planet implements Celestial { ... }
class Star implements Celestial { ... }
class Comet implements Celestial { ... }

אבל כאן אנחנו בעצם פותחים פתח לכל מי שמשתמש ב-package שלי לרשת את אחת מהמחלקות שלי וליצור לו את שלו, ובאמת יש מקרים שבהם אנחנו רוצים למנוע את זה כדי למנוע סיבוכים. אז מה נעשה? ובכן לג'אווה עד עכשיו היה פתרון, שמים final על המחלקות

Java
interface Celestial { ... }
final class Planet implements Celestial { ... }
final class Star implements Celestial { ... }
final class Comet implements Celestial { ... }

אוקי, אבל מה לגבי ה-interface? איך אני אשלוט בו? בעצם עד ל-Java 17 חיברו לי שני עקרונות: הרחבה, גישה וחיברו ביניהם. אם אני רוצה שמשהו יהיה נגיש, הוא בהגדרה גם יהיה ניתן להרחבה. וזו הבעיה ש-`Sealed Class` באה לפתור

Java
package com.example.geometry;

public abstract sealed class Shape
    permits Circle, Rectangle, Square { ... }

גם כאן יש לנו שני keywords חדשים: sealed ו- permits
בעצם מה שהקוד למעלה מגדיר הוא מי יכול לרשת ממני. בדוגמה הזאת רק Circle, Rectangle, Square.
אבל זה לא הכל. יש לנו עוד כללים:

  • כל המחלקות שמותר להן לרשת ממני חייבות להיות קרובות:
    • באותו package
    • באותו module.
  • כל class שמותר לו לרשת חייב לרשת ישירות את ה-sealed class.
    כלומר Circle חייב לעשות extend ל-Shape. אסור לו לעשות extend ל-Rectangle לדוגמה.
  • כל אחד מה-class שמותר לו לרשת חייב להגדיר איך הוא מרחיב את ההורשה:
    • אופציה ראשונה שהוא מגדיר שאף אחד לא יכול לרשת אותו (final)
    • אופציה שניה היא להגדיר את עצמו כ-sealed בפני עצמו
    • להגדיר את עצמו כ non-sealed, כלומר אני לא מגביל את עצמי.

שילוב של records עם sealed classes

ג'אווה 17 בעצם מאפשרת לנו לשלב באופן מעולה בין records לבין sealed classes.
לדוגמה:

Java
package com.demo.expression;

public sealed interface Expr
	permits ContantExpr, PlusExpr, TimesExpr, NegateExpr {...}

public record ConstatnExpr(int i) implements Expr {...}
public record PlusExpr(Expr a, Expr b) implements Expr {...}
public record TimesExpr(Expr a, Expr b) implements Expr {...}
public record NegateExpr(Expr e) implements Expr {...}

במקרה הזה ראש ההיררכיה הוא interface ומתחת יש לנו records שאנחנו מרשים להרחיב
אבל חסר לנו פה משהו? הכלל השלישי של sealed classes, למה הן לא מצהירות מי יכול להרחיב אותן?
ובכן התשובה היא בגלל ש-records הם implictly final כלומר אף אחד לא יכול להרחיב אותן.

Text Blocks

זה אחד הפיצ'רים שבאמת היה מאוד חסר בג'אווה. הוא נכנס ב-JEP 378
כמעט בכל שפה בעולם יש אפשרות לכתוב strings בכמה שורות, ורק בג'אווה היינו צריכים לדחוף את התו \n כל הזמן.
למשל:

Java
String html = "<html>\n"
			  "   <body>\n"
			  "      <p>Hello, world</p>\n"
			  "   </body>\n"
			  "</html>\n";

ועכשיו אנחנו כבר לא צריכים:

Java
String html = """
				<html>
					<body>
						<p> Hello, world</p>
					</body>
				</html>
			""";

חשוב לציין שאין לנו type חדש פה, זה עדיין String לכל דבר ועניין.

לי לא יצא כל כך לכתוב HTML ככה, אבל בהחלט יצא לי לכתוב שאילתות SQL, וזה כבר היה יכול להיות ממש מעצבן עם הצורך לעשות escaping למרכאות:

Java
String query = "Select \"EMP_ID\", \"LAST_NAME\" FROM \"EMPLOYEE_TP\";"

עכשיו אנחנו יכולים לכתוב את זה ככה:

Java
String query = """
Select "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TP";
"""

אז בואו ניכנס לפרטים:
התו הראשון ב-Text Block הוא הדלימיטר """ ואחריו לא יכול לבוא כלום, רק שורה חדשה.
ככה ג'אווה יודעת איך להתחיל לעבוד עם ה-Text Block
אנחנו יכולים להשתמש במרכאות בתוך ה-Text Block ואנחנו יכולים להכניס שורות חדשות.

טיפול ברווחים

החלק המשמעותי הוא איך עושים אינדנטציה ב-Text Block
אם נסתכל על הקוד הבא:

Java
String html = """
................<html>
....................<body>
.......................<p> Hello, world</p>
....................</body>
................</html>
............""";

הנקודות הן רווחים שהם מקריים לחלוטין, כי יכול להיות שב-code style שונה יהיה מספר אחר של רווחים, או אם נשים את זה בתוך בלוק של if.
ולכן ג'אווה יודעת להתעלם מהן. כנ"ל לגבי רווחים עודפים (Trailing spaces)
אבל לפעמים אנחנו כן רוצים שיהיה רווחים בתחילת השורה, אז אנחנו נכניס את זה ככה:

Java
String html = """
				<html>
					<body>
						<p> Hello, world</p>
					</body>
				</html>
.........""";

ואז הקומפיילר יקח רק את מספר הרווחים על לדלימטר האחרון """.

שיטות escaping חדשות

בואו נסכל על הקוד הבא, שאני בטוח שרובכם ראיתם בעבר

הסיבה למה אנחנו מקצרים את האורך של ה-String הזה הוא רק בגלל הקריאות, הרי אין בו שורות חדשות.
אז בשימוש ב-Text Block אנחנו יכולים לבצע את זה בצורה קצת יותר אלגנטית:

Java
String text = """
                Lorem ipsum dolor sit amet, consectetur adipiscing \
                elit, sed do eiusmod tempor incididunt ut labore \
                et dolore magna aliqua.\
                """;

שיפורים ב-Switch

השיפורים הללו נכנסו תחת JEP 361

נתחיל עם דוגמה

Java
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

בעצם ה-switch הגיע דרך שפת C.
יחד עם ה-break הידוע לשמצה (ואי כמה פעמים דיבגתי באג שבו שכחו בטעות להכניס את זה).
לי תמיד הוא הרגיש מאוד low level ולא מודרני.
אז בשעה טובה ב-ג'אווה 17 שיפרו מאוד את הסינטקס:

Java
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

קיבלנו את הסינטקס של החץ כמו בקוטלין, וגם אין לנו fall through שהיינו צריכים לטפל בה באמצעות ה-break כבר לא קיימת.
בנוסף אנחנו יכולים לשים כמה statements ביחד.

Switch Expressions

בלא מעט מקרים, אנחנו רוצים לתת ערך לאיזשהו משתנה בהתאם לערך של משתנה אחר.
כדי לעשות את זה עם switch case היינו צריכים לכתוב משהו כזה:

Java
int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}

עכשיו אנחנו יכולים לכתוב את זה הרבה יותר אלגנטי:

Java
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

בעצם ה-switch הפך להיות ביטוי שמחזיר ערך.
ואם אנחנו רוצים לעשות case עם כמה שורות:

Java
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

הופה, נכנס לנו פה משהו חדש בשם yield
במקור הוא היה אמור להיות ה-break החדש.
אבל אז טענו שזה מבלבל, וגם להשתמש ב-return לא יתאים כי אנחנו לא רוצים לצאת מהפונקציה.
ולכן הביאו את yield שמשתמשים בו במקרה של כמה שורות אז אנחנו נגדיר עם yield איזה ערך צריך לחזור מהמקרה הזה.

Restore Always Strict Floating Point Semantic

על השינוי הזה אני הולך לדלג (אנא סלחו לי) מכמה סיבות:

  1. הפוסט הזה הוא מאוד ארוך גם ככה
  2. זהו לא משהו שהרבה מפתחים הולכים להשתמש בו (סמנטיות של מספרים עשרוניים)

סיכום

אז עברנו פה די הרבה, אבל אני בהחלט מתרגש לעבור לג'אווה 17.
יש פה לא מעט פיצ'רים משמעותיים עבור ג'אווה ועבור הנוחות של המפתחים שכותבים בה.
כרגע לפחות, אני עדיין מעדיף את קוטלין, כיוון שכל הפיצ'רים הללו קיימים שם, והיא נותנת תמיכה בעוד כמה דברים שעדיין חסרים לי בג'אווה, כמו למשל: היכולת להרחיב מחלקות בסיסיות או שימוש בתכנות פונקציונאלי.

השאר תגובה

Scroll to Top