החלק השני של התרגום של המאמר Java 8 Features – The ULTIMATE Guide . החלק הראשון כאן (ייתכן שהקישור ישתנה).
5. תכונות חדשות בספריות Java 8
Java 8 הוסיפה מחלקות חדשות רבות והרחיבה מחלקות קיימות כדי לתמוך טוב יותר במקבילות מודרנית, תכנות פונקציונלי, תאריך/שעה ועוד.5.1. שיעור אופציונלי
NullPointerException המפורסם הוא ללא ספק הגורם השכיח ביותר לכשלים ביישומי Java. לפני זמן רב, הפרויקט המצוין של גוגל Guava הוצגOptional
כפתרון NullPointerException
, ובכך מנע את זיהום הקוד על ידי בדיקות null, וכתוצאה מכך עידוד כתיבת קוד נקי יותר. מחלקת ה-Guava בהשראת גוגל Optional
היא כעת חלק מ-Java 8. Optional
זה רק מיכל: הוא יכול להכיל ערך או סוג כלשהו Т
, או פשוט להיות null. הוא מספק שיטות שימושיות רבות כך שבדיקות ריק מפורשות אינן מוצדקות עוד. עיין בתיעוד הרשמי למידע מפורט יותר. הבה נסתכל על שתי דוגמאות קטנות לשימוש Optional
: עם ובלי null.
Optional<String> fullName = Optional.ofNullable( null );
System.out.println( "Full Name is set? " + fullName.isPresent() );
System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) );
System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
השיטה isPresent()
מחזירה true אם המופע Optional
מכיל ערך שאינו null ו- false אחרת. השיטה orElseGet()
מכילה מנגנון fallback עבור התוצאה אם Optional
היא מכילה null, ומקבלת פונקציות ליצירת ערך ברירת מחדל. שיטת map () הופכת את הערך הנוכחי Optional
ומחזירה מופע חדש Optional
. השיטה orElse()
דומה ל- orElseGet()
, אבל במקום פונקציה היא לוקחת ערך ברירת מחדל. להלן הפלט של תוכנית זו:
Full Name is set? false
Full Name: [none]
Hey Stranger!
בואו נסתכל במהירות על דוגמה נוספת:
Optional<String> firstName = Optional.of( "Tom" );
System.out.println( "First Name is set? " + firstName.isPresent() );
System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) );
System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );
System.out.println();
התוצאה תהיה כזו:
First Name is set? true
First Name: Tom
Hey Tom!
למידע מפורט יותר, עיין בתיעוד הרשמי .
5.2. זרמים
ה- Stream API החדש שנוסף (java.util.stream
) מציג תכנות בסגנון פונקציונלי אמיתי ב-Java. זוהי ללא ספק התוספת המקיפה ביותר לספריית Java ומאפשרת למפתחי Java להיות יעילים משמעותית וגם מאפשרת להם ליצור קוד יעיל, נקי ותמציתי. ה-API של Stream הופך את עיבוד האוספים להרבה יותר קל (אך לא מוגבל אליהם, כפי שנראה בהמשך). ניקח כדוגמה שיעור פשוט Task
.
public class Streams {
private enum Status {
OPEN, CLOSED
};
private static final class Task {
private final Status status;
private final Integer points;
Task( final Status status, final Integer points ) {
this.status = status;
this.points = points;
}
public Integer getPoints() {
return points;
}
public Status getStatus() {
return status;
}
@Override
public String toString() {
return String.format( "[%s, %d]", status, points );
}
}
}
למשימה יש תחושה מסוימת של נקודות (או פסבדו קשיים) ויכולה להיות OPEN או CLOSE . בואו נציג אוסף קטן של בעיות לשחק איתן.
final Collection<Task> tasks = Arrays.asList(
new Task( Status.OPEN, 5 ),
new Task( Status.OPEN, 13 ),
new Task( Status.CLOSED, 8 )
);
השאלה הראשונה שאנו מתכוונים לברר היא כמה נקודות מכילות משימות OPEN כרגע? לפני Java 8, הפתרון הרגיל לכך היה שימוש באיטרטור foreach
. אבל ב-Java 8, התשובה היא זרמים: רצף של אלמנטים התומכים בפעולות צבירה עוקבות ומקבילות.
// Подсчет общего количества очков всех активных задач с использованием sum()
final long totalPointsOfOpenTasks = tasks
.stream()
.filter( task -> task.getStatus() == Status.OPEN )
.mapToInt( Task::getPoints )
.sum();
System.out.println( "Total points: " + totalPointsOfOpenTasks );
ופלט הקונסולה ייראה כך:
Total points: 18
בואו נראה מה קורה כאן. ראשית, אוסף המשימות מומר לייצוג סטרימינג. לאחר מכן, הפעולה filter
מסננת את כל המשימות בסטטוס סגור . בשלב הבא, הפעולה mapToInt
ממירה זרם Task
s לזרמים Integer
באמצעות שיטה Task::getPoints
לכל מופע Task
. לבסוף, כל הנקודות מסוכמות באמצעות השיטה sum
, המספקת את התוצאה הסופית. לפני המעבר לדוגמאות הבאות, יש כמה הערות לגבי שרשורים שכדאי לזכור (פרטים נוספים כאן ). הפעולות stream
מחולקות לפעולות ביניים וסופיות . פעולות ביניים מחזירות זרם חדש. הם תמיד עצלנים; כאשר הם מבצעים פעולות ביניים כגון filter
, הם אינם מבצעים סינון בפועל, אלא יוצרים זרם חדש, שכאשר הוא הושלם, מכיל את הרכיבים של הזרם המקורי התואמים את הפרדיקט הנתון. פעולות סופיות , כגון forEach
ו- sum
, יכולות לעבור דרך זרם כדי לייצר תוצאה או תופעת לוואי. לאחר השלמת הפעולה הסופית, הזרם נחשב בשימוש ולא ניתן להשתמש בו שוב. כמעט בכל המקרים, פעולות הקצה נוטות להשלים את המעבר שלהן דרך מקור הנתונים הבסיסי. תכונה חשובה נוספת של חוטים היא התמיכה בתהליכים מקבילים מחוץ לקופסה. הבה נסתכל על דוגמה זו, שמוצאת את סכום הציונים של כל הבעיות.
// Calculate total points of all tasks
final double totalPoints = tasks
.stream()
.parallel()
.map( task -> task.getPoints() ) // or map( Task::getPoints )
.reduce( 0, Integer::sum );
System.out.println( "Total points (all tasks): " + totalPoints );
זה דומה מאוד לדוגמא הראשונה, אלא שאנו מנסים לעבד את כל המשימות במקביל ולחשב את התוצאה הסופית באמצעות השיטה reduce
. הנה פלט הקונסולה:
Total points (all tasks): 26.0
לעתים קרובות יש צורך לקבץ אלמנטים לפי קריטריון מסוים. הדוגמה מדגימה כיצד שרשורים יכולים לעזור בכך.
// Группировка задач по их статусу
final Map<Status, List<Task>> map = tasks
.stream()
.collect( Collectors.groupingBy( Task::getStatus ) );
System.out.println( map );
הפלט של המסוף יהיה כדלקמן:
{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}
לסיום עם דוגמאות הבעיות, הבה נחשב את האחוז הכולל (או המשקל) של כל בעיה באוסף על סמך סך הנקודות:
// Подсчет веса каждой задачи (How процент от общего количества очков)
final Collection<String> result = tasks
.stream() // Stream<String>
.mapToInt( Task::getPoints ) // IntStream
.asLongStream() // LongStream
.mapToDouble( points -> points / totalPoints ) // DoubleStream
.boxed() // Stream<Double>
.mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream
.mapToObj( percentage -> percentage + "%" ) // Stream<String>
.collect( Collectors.toList() ); // List<String>
System.out.println( result );
פלט הקונסולה יהיה כך:
[19%, 50%, 30%]
לבסוף, כפי שציינו קודם לכן, ה-API של Stream לא מיועד רק לאוספי Java. פעולת I/O טיפוסית, כגון קריאת קבצי טקסט שורה אחר שורה, היא מועמדת טובה מאוד לשימוש בעיבוד זרם. הנה דוגמה קטנה כדי להוכיח זאת.
final Path path = new File( filename ).toPath();
try( Stream<String> lines = Files.lines( path, StandardCharsets.UTF_8 ) ) {
lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );
}
השיטה onConsole
, שנקראת על שרשור, מחזירה שרשור שווה עם מטפל פרטי נוסף. המטפל הפרטי נקרא כאשר מתודה close()
נקראת על שרשור. ה-Stream API יחד עם lambdas ושיטות התייחסות יחד עם שיטות ברירת מחדל וסטטיות ב-Java 8 הם התשובה לפרדיגמות פיתוח תוכנה מודרניות. למידע מפורט יותר, עיין בתיעוד הרשמי .
5.3. API של תאריך/שעה (JSR 310)
Java 8 מביאה מראה חדש לניהול תאריך וזמן על ידי מתן API חדש לתאריך ושעה (JSR 310) . מניפולציה של תאריך ושעה היא אחת מנקודות הכאב הקשות ביותר עבור מפתחי Java. הסטנדרטjava.util.Date
הבא java.util.Calendar
לא שיפר את המצב באופן כללי (אולי אפילו הפך אותו לבלבל יותר). כך נולדה Joda-Time : אלטרנטיבה מצוינת ל-API של תאריך/שעה עבור Java . ה-Date/Time API החדש ב-Java 8 (JSR 310) מושפע מאוד מג'ודה-טיים ולוקח ממנו את המיטב. החבילה החדשה java.time
מכילה את כל המחלקות לתאריך, שעה, תאריך/שעה, אזורי זמן, משכי זמן ומניפולציה של זמן . עיצוב ה-API לקח את חוסר השינוי ברצינות רבה: שינויים אינם מותרים (לקח קשה שנלמד מ java.util.Calendar
). אם נדרש שינוי, יוחזר מופע חדש של המחלקה המתאימה. בואו נסתכל על הכיתות העיקריות ודוגמאות לשימוש בהן. המחלקה הראשונה Clock
, המספקת גישה לרגע הנוכחי, לתאריך ולשעה באמצעות אזור זמן. Clock
ניתן להשתמש במקום System.currentTimeMillis()
ו TimeZone.getDefault()
.
// Получить системное время How смещение UTC
final Clock clock = Clock.systemUTC();
System.out.println( clock.instant() );
System.out.println( clock.millis() );
פלט מסוף לדוגמה:
2014-04-12T15:19:29.282Z
1397315969360
שיעורים חדשים נוספים שנבחן הם LocaleDate
ו LocalTime
. LocaleDate
מכיל רק את חלק התאריך ללא אזור הזמן במערכת לוח השנה ISO-8601. בהתאם לכך, LocalTime
הוא מכיל רק חלק מקוד הזמן>.
// получить местную date и время время
final LocalDate date = LocalDate.now();
final LocalDate dateFromClock = LocalDate.now( clock );
System.out.println( date );
System.out.println( dateFromClock );
// получить местную date и время время
final LocalTime time = LocalTime.now();
final LocalTime timeFromClock = LocalTime.now( clock );
System.out.println( time );
System.out.println( timeFromClock );
פלט מסוף לדוגמה:
2014-04-12
2014-04-12
11:25:54.568
15:25:54.568
LocalDateTime
משרשרת LocaleDate
ומכילה LocalTime
תאריך ושעה, אך ללא אזור זמן, במערכת לוח השנה ISO-8601. דוגמה פשוטה ניתנת להלן.
// Get the local date/time
final LocalDateTime datetime = LocalDateTime.now();
final LocalDateTime datetimeFromClock = LocalDateTime.now( clock );
System.out.println( datetime );
System.out.println( datetimeFromClock );
פלט מסוף לדוגמה:
2014-04-12T11:37:52.309
2014-04-12T15:37:52.309
במקרה שאתה צריך תאריך/שעה עבור אזור זמן מסוים, ZonedDateTime
. הוא מכיל את התאריך והשעה במערכת לוח השנה ISO-8601. הנה כמה דוגמאות לאזורי זמן שונים.
// Получение даты/времени для временной зоны
final ZonedDateTime zonedDatetime = ZonedDateTime.now();
final ZonedDateTime zonedDatetimeFromClock = ZonedDateTime.now( clock );
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of( "America/Los_Angeles" ) );
System.out.println( zonedDatetime );
System.out.println( zonedDatetimeFromClock );
System.out.println( zonedDatetimeFromZone );
פלט מסוף לדוגמה:
2014-04-12T11:47:01.017-04:00[America/New_York]
2014-04-12T15:47:01.017Z
2014-04-12T08:47:01.017-07:00[America/Los_Angeles]
ולבסוף, בואו נסתכל על הכיתה Duration
: טווח זמן בשניות וננו-שניות. זה הופך את החישוב בין שני תאריכים לפשוט מאוד. בוא נראה איך עושים את זה:
// Получаем разницу между двумя датами
final LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );
final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );
final Duration duration = Duration.between( from, to );
System.out.println( "Duration in days: " + duration.toDays() );
System.out.println( "Duration in hours: " + duration.toHours() );
הדוגמה שלמעלה מחשבת את משך הזמן (בימים ובשעות) בין שני תאריכים, 16 באפריל 2014 ו -16 באפריל 2015 . הנה דוגמה לפלט המסוף:
Duration in days: 365
Duration in hours: 8783
הרושם הכללי של התאריך/שעה החדשים ב-Java 8 הוא מאוד מאוד חיובי. חלקית משום שהשינויים מבוססים על בסיס שנבדק בקרב (Joda-Time), חלקית משום שהפעם הנושא נשקל מחדש ברצינות וקולות המפתחים נשמעו. לפרטים, עיין בתיעוד הרשמי .
5.4. מנוע JavaScript של Nashorn
Java 8 מגיע עם מנוע ה-JavaScript החדש של Nashorn , המאפשר לך לפתח ולהפעיל סוגים מסוימים של יישומי JavaScript ב-JVM. מנוע ה-JavaScript של Nashorn הוא פשוט יישום נוסף של javax.script.ScriptEngine שעוקב אחר אותה מערכת כללים כדי לאפשר אינטראקציה בין Java ו-JavaScript. הנה דוגמה קטנה.ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );
פלט מסוף לדוגמה:
jdk.nashorn.api.scripting.NashornScriptEngine
Result: 2
5.5. בסיס 64
לבסוף, תמיכה בקידוד Base64 מצאה את דרכה לספריית הסטנדרטית של Java עם שחרורו של Java 8. זה מאוד קל לשימוש, הדוגמה מדגימה זאת.package com.javacodegeeks.java8.base64;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class Base64s {
public static void main(String[] args) {
final String text = "Base64 finally in Java 8!";
final String encoded = Base64
.getEncoder()
.encodeToString( text.getBytes( StandardCharsets.UTF_8 ) );
System.out.println( encoded );
final String decoded = new String(
Base64.getDecoder().decode( encoded ),
StandardCharsets.UTF_8 );
System.out.println( decoded );
}
}
פלט המסוף של התוכנית מציג טקסט מקודד ומפוענח כאחד:
QmFzZTY0IGZpbmFsbHkgaW4gSmF2YSA4IQ==
Base64 finally in Java 8!
יש גם שיעורים למקודדים/מפענחים ידידותיים ל-URL, כמו גם למקודדים/מפענחים ידידותיים למיייטים ( Base64.getUrlEncoder()
/ Base64.getUrlDecoder()
, Base64.getMimeEncoder()
/ Base64.getMimeDecoder()
).
5.6. מערכים מקבילים
מהדורת Java 8 מוסיפה שיטות חדשות רבות לעיבוד מערך מקביל. אולי החשוב שבהם הואparallelSort()
, שיכול להאיץ מאוד את המיון במכונות מרובות ליבות. הדוגמה הקטנה להלן מדגימה את משפחת השיטות החדשה ( parallelXxx
) בפעולה.
package com.javacodegeeks.java8.parallel.arrays;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrays {
public static void main( String[] args ) {
long[] arrayOfLong = new long [ 20000 ];
Arrays.parallelSetAll( arrayOfLong,
index -> ThreadLocalRandom.current().nextInt( 1000000 ) );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
Arrays.parallelSort( arrayOfLong );
Arrays.stream( arrayOfLong ).limit( 10 ).forEach(
i -> System.out.print( i + " " ) );
System.out.println();
}
}
פיסת קוד קטנה זו משתמשת בשיטה parallelSetAll()
למילוי מערך ב-20,000 ערכים אקראיים. אחרי זה הוא מוחל parallelSort()
. התוכנית מדפיסה את 10 האלמנטים הראשונים לפני ואחרי המיון כדי להראות שהמערך אכן ממוין. פלט תוכנית לדוגמה עשוי להיראות כך (שים לב שהרכיבים של המערך הם אקראיים).
Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378
Sorted: 39 220 263 268 325 607 655 678 723 793
5.7. מַקבִּילוּת
שיטות חדשות נוספו למחלקהjava.util.concurrent.ConcurrentHashMap
כדי לתמוך בפעולות מצטברות המבוססות על אובייקטי הזרם החדשים שנוספו וביטויי למבדה. כמו כן, נוספו לכיתה שיטות חדשות java.util.concurrent.ForkJoinPool
כדי לתמוך בגיבוש משותף (ראה גם את הקורס החינמי שלנו בנושא ג'אווה במקביל ). מחלקה חדשה java.util.concurrent.locks.StampedLock
נוספה כדי לספק נעילה מבוססת יכולות עם שלושה מצבי גישה לבקרת קריאה/כתיבה (ניתן להתייחס אליה כאלטרנטיבה טובה יותר לזו הלא טובה java.util.concurrent.locks.ReadWriteLock
). כיתות חדשות שנוספו לחבילה java.util.concurrent.atomic
:
- מצבר כפול
- DoubleAdder
- מצבר ארוך
- LongAdder
6. תכונות חדשות בסביבת זמן ריצה של Java (JVM)
האזורPermGen
הוצא לגמלאות והוחלף על ידי Metaspace (JEP 122). אפשרויות JVM -XX:PermSize
והוחלפו -XX:MaxPermSize
ב- -XX:MetaSpaceSize
ובהתאמה -XX:MaxMetaspaceSize
.
GO TO FULL VERSION