JavaRush /בלוג Java /Random-HE /מדריך Java 8. 1 חלק.
ramhead
רָמָה

מדריך Java 8. 1 חלק.

פורסם בקבוצה

"ג'אווה עדיין חיה - ואנשים מתחילים להבין את זה".

ברוכים הבאים למבוא שלי ל-Java 8. מדריך זה יעביר אתכם צעד אחר צעד דרך כל התכונות החדשות של השפה. באמצעות דוגמאות קוד קצרות ופשוטות, תלמד כיצד להשתמש בשיטות ברירת המחדל של ממשק , ביטויי למבדה , שיטות התייחסות והערות הניתנות לחזרה . עד סוף המאמר, תכירו את השינויים האחרונים בממשקי API כגון זרמים, ממשקי פונקציות, הרחבות שיוך ו-Date API החדש. בלי קירות של טקסט משעמם - רק חבורה של קטעי קוד עם הערות. תהנה!

שיטות ברירת מחדל עבור ממשקים

Java 8 מאפשרת לנו להוסיף שיטות לא מופשטות המיושמות בממשק באמצעות שימוש במילת המפתח המוגדרת כברירת מחדל . תכונה זו ידועה גם בשם שיטות הרחבה . הנה הדוגמה הראשונה שלנו: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } בנוסף לשיטה המופשטת calculate , ממשק הפורמולה מגדיר גם שיטת ברירת מחדל sqrt . מחלקות המיישמות את ממשק הנוסחה מיישמות רק את שיטת החישוב המופשטת . ניתן להשתמש בשיטת ברירת המחדל של sqrt ישירות מהקופסה. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 אובייקט הנוסחה מיושם כאובייקט אנונימי. הקוד די מרשים: 6 שורות קוד פשוט לחשב sqrt(a * 100) . כפי שנראה בסעיף הבא, יש דרך אטרקטיבית יותר ליישם אובייקטים של שיטה בודדת ב-Java 8.

ביטויי למדה

נתחיל עם דוגמה פשוטה כיצד למיין מערך של מחרוזות בגרסאות מוקדמות של Java: שיטת העזר הסטטיסטית Collections.sort לוקחת רשימה ו-Comparator כדי למיין את האלמנטים של הרשימה הנתונה. מה שקורה לעתים קרובות הוא שאתה יוצר השוואות אנונימיות ומעביר אותם לשיטות מיון. במקום ליצור אובייקטים אנונימיים כל הזמן, Java 8 נותן לך את היכולת להשתמש בהרבה פחות תחביר, ביטויי למבדה : כפי שאתה יכול לראות, הקוד הרבה יותר קצר וקל יותר לקריאה. אבל כאן זה נהיה עוד יותר קצר: בשיטה של ​​שורה אחת, אתה יכול להיפטר מהסוגרים המתולתלים {} ומילת המפתח החזרה . אבל כאן הקוד מתקצר עוד יותר: מהדר ג'אווה מודע לסוגי הפרמטרים, כך שאתה יכול גם להשאיר אותם בחוץ. עכשיו בואו נצלול לעומק כיצד ניתן להשתמש בביטויי למבדה בחיים האמיתיים. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

ממשקים פונקציונליים

כיצד ביטויי למבדה מתאימים למערכת מסוג Java? כל למבדה מתאימה לסוג נתון המוגדר על ידי ממשק. והממשק הפונקציונלי כביכול חייב להכיל בדיוק שיטה מופשטת אחת מוצהרת. כל ביטוי למבדה מסוג נתון יתאים לשיטה מופשטת זו. מכיוון ששיטות ברירת המחדל אינן שיטות מופשטות, אתה חופשי להוסיף שיטות ברירת מחדל לממשק הפונקציונלי שלך. אנו יכולים להשתמש בממשק שרירותי כביטוי למבדה, בתנאי שהממשק מכיל רק שיטה מופשטת אחת. כדי להבטיח שהממשק שלך עומד בתנאים אלה, עליך להוסיף את ההערה @FunctionalInterface . המהדר יקבל מידע על ידי ביאור זה שהממשק חייב להכיל רק שיטה אחת, ואם הוא יתקל בשיטה מופשטת שנייה בממשק זה, הוא יזרוק שגיאה. דוגמה: זכור שקוד זה יהיה חוקי גם אם ההערה @FunctionalInterface לא הוכרזה. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

הפניות לשיטות ובנאים

ניתן לפשט עוד יותר את הדוגמה שלמעלה על ידי שימוש בהפניה לשיטה סטטיסטית: Java 8 מאפשרת לך להעביר הפניות לשיטות ולבנאים באמצעות :: סמלי מילת המפתח . הדוגמה לעיל מראה כיצד ניתן להשתמש בשיטות סטטיסטיות. אבל אנחנו יכולים גם להתייחס לשיטות על אובייקטים: בואו נסתכל כיצד השימוש ב-: עובד עבור בנאים. ראשית, הבה נגדיר דוגמה עם בנאים שונים: לאחר מכן, נגדיר את ממשק המפעל של PersonFactory ליצירת אובייקטי אדם חדשים : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } במקום ליישם את המפעל באופן ידני, אנו קושרים הכל יחד באמצעות הפניה של קונסטרוקטור: אנו יוצרים הפניה לבנאי של המחלקה Person באמצעות Person::new . מהדר Java יקרא אוטומטית לבנאי המתאים על ידי השוואת החתימה של הבנאים עם החתימה של שיטת PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

אזור למדה

ארגון גישה למשתני היקף חיצוני מביטויי למבדה דומה לגישה מאובייקט אנונימי. אתה יכול לגשת למשתנים סופיים מההיקף המקומי, כמו גם לשדות מופעים ומשתנים מצטברים.
גישה למשתנים מקומיים
אנו יכולים לקרוא משתנה מקומי עם השינוי הסופי מתוך ההיקף של ביטוי למבדה: אבל בניגוד לאובייקטים אנונימיים, אין צורך להכריז על משתנים כסופיים כדי להיות נגישים מביטוי למבדה . גם הקוד הזה נכון: עם זאת, המשתנה num חייב להישאר בלתי ניתן לשינוי, כלומר. להיות סופי מרומז עבור הידור קוד. הקוד הבא לא יקמפל: שינויים ב-num בתוך ביטוי למבדה גם אינם מותרים. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
גישה לשדות מופע ומשתנים סטטיסטיים
בניגוד למשתנים מקומיים, אנו יכולים לקרוא ולשנות שדות מופעים ומשתנים סטטיסטיים בתוך ביטויי למבדה. אנו מכירים את ההתנהגות הזו מחפצים אנונימיים. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
גישה לשיטות ברירת מחדל של ממשקים
זוכרים את הדוגמה עם מופע הנוסחה מהסעיף הראשון? ממשק הפורמולה מגדיר שיטת ברירת מחדל sqrt שניתן לגשת אליה מכל מופע של נוסחה , כולל אובייקטים אנונימיים. זה לא עובד עם ביטויי למבדה. לא ניתן לגשת לשיטות ברירת מחדל בתוך ביטויי lambda. הקוד הבא אינו קומפילציה: Formula formula = (a) -> sqrt( a * 100);

ממשקים פונקציונליים מובנים

ה-API של JDK 1.8 מכיל ממשקים פונקציונליים מובנים רבים. חלקם מוכרים היטב מגירסאות קודמות של Java. למשל Comparator או Runnable . ממשקים אלה מורחבים כך שיכללו תמיכת lambda באמצעות ההערה @FunctionalInterface . אבל ה-API של Java 8 מלא גם בממשקים פונקציונליים חדשים שיקלו עליכם את החיים. חלק מהממשקים הללו מוכרים היטב מספריית הגויאבה של גוגל . גם אם אתה מכיר את הספרייה הזו, כדאי שתסתכל מקרוב על אופן הרחבה של ממשקים אלה, עם כמה שיטות הרחבה שימושיות.
פרדיקטים
פרדיקטים הם פונקציות בוליאניות עם ארגומנט אחד. הממשק מכיל שיטות ברירת מחדל שונות ליצירת ביטויים לוגיים מורכבים (ואו, שלילה) באמצעות פרדיקטים Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
פונקציות
פונקציות לוקחות ארגומנט אחד ומפיקות תוצאה. ניתן להשתמש בשיטות ברירת מחדל כדי לשלב מספר פונקציות יחד לשרשרת אחת (חיבור, ולאחר מכן). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
ספקים
הספקים מחזירים תוצאה (מופע) מסוג זה או אחר. בניגוד לפונקציות, ספקים לא לוקחים ארגומנטים. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
צרכנים
צרכנים מייצגים שיטות ממשק עם ארגומנט יחיד. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
משווים
משווים מוכרים לנו מגרסאות קודמות של Java. Java 8 מאפשר לך להוסיף שיטות ברירת מחדל שונות לממשקים. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
אופציונליים
ממשק האופציונלי אינו פונקציונלי, אך הוא כלי עזר נהדר למניעת NullPointerException . זוהי נקודה חשובה לסעיף הבא, אז בואו נסתכל במהירות על איך הממשק הזה עובד. הממשק האופציונלי הוא מיכל פשוט לערכים שיכולים להיות null או לא null. תארו לעצמכם ששיטה יכולה להחזיר ערך או כלום. ב-Java 8, במקום להחזיר null , אתה מחזיר מופע אופציונלי . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

זרם

java.util.Stream הוא רצף של אלמנטים שעליהם מתבצעת פעולה אחת או רבות. כל פעולת זרם היא ביניים או מסוף. פעולות טרמינל מחזירות תוצאה מסוג מסוים, בעוד שפעולות ביניים מחזירות את אובייקט הזרם עצמו, ומאפשרות ליצור שרשרת קריאות למתודה. Stream הוא ממשק, כמו java.util.Collection עבור רשימות וסטים (מפות אינן נתמכות).כל פעולת Stream יכולה להתבצע ברצף או במקביל. בואו נסתכל על איך פועל הזרם. ראשית, ניצור קוד לדוגמה בצורה של רשימת מחרוזות: אוספים ב-Java 8 משופרים כך שתוכל ליצור זרמים די פשוט על ידי קריאה ל- Collection.stream() או Collection.parallelStream() . הסעיף הבא יסביר את פעולות הזרם החשובות והפשוטות ביותר. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
לְסַנֵן
מסנן מקבל פרדיקטים לסינון כל רכיבי הזרם. פעולה זו היא ביניים, מה שמאפשר לנו לקרוא לפעולות זרם אחרות (למשל forEach) על התוצאה המתקבלת (המסוננת). ForEach מקבל פעולה שתתבצע על כל רכיב של הזרם שכבר מסונן. ForEach היא פעולה מסוף. יתר על כן, התקשרות לפעולות אחרות היא בלתי אפשרית. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
מְמוּיָן
ממוין היא פעולת ביניים שמחזירה ייצוג ממוין של זרם. האלמנטים ממוינים בסדר הנכון אלא אם תציין את ה-Comparator שלך . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" זכור שמיון יוצר ייצוג ממוין של זרם מבלי להשפיע על האוסף עצמו. הסדר של רכיבי stringCollection נותר ללא שינוי: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
מַפָּה
פעולת מפת הביניים ממירה כל אלמנט לאובייקט אחר באמצעות הפונקציה המתקבלת. הדוגמה הבאה ממירה כל מחרוזת למחרוזת רישיות. אבל אתה יכול גם להשתמש במפה כדי להמיר כל אובייקט לסוג אחר. סוג אובייקטי הזרם המתקבלים תלוי בסוג הפונקציה שאתה מעביר למיפוי. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
התאמה
ניתן להשתמש בפעולות התאמה שונות כדי לבדוק את האמת של פרדיקט מסוים ביחס הזרם. כל פעולות ההתאמה הן סופניות ומחזירות תוצאה בוליאנית. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
לספור
ספירה היא פעולת טרמינל שמחזירה את מספר רכיבי הזרם כארוכה . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
לְהַפחִית
זוהי פעולת טרמינל המקצרת את רכיבי הזרימה באמצעות הפונקציה שעברה. התוצאה תהיה אופציונלי המכיל את הערך המקוצר. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

זרמים מקבילים

כפי שהוזכר לעיל, זרמים יכולים להיות עוקבים או מקבילים. פעולות זרם רציף מבוצעות על חוט טורי, בעוד שפעולות זרם מקביל מבוצעות על מספר חוטים מקבילים. הדוגמה הבאה מדגימה כיצד להגדיל בקלות את הביצועים באמצעות זרם מקביל. ראשית, בואו ניצור רשימה גדולה של אלמנטים ייחודיים: כעת נקבע את הזמן המושקע במיון הזרם של האוסף הזה. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
זרם סדרתי
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
זרם מקביל
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms כפי שאתה יכול לראות, שני השברים כמעט זהים, אבל המיון המקביל מהיר יותר ב-50%. כל מה שאתה צריך הוא לשנות את stream() ל- parallelStream() .

מַפָּה

כפי שכבר צוין, מפות אינן תומכות בזרמים. במקום זאת, מפה החלה לתמוך בשיטות חדשות ושימושיות לפתרון בעיות נפוצות. הקוד למעלה צריך להיות אינטואיטיבי: putIfAbsent מזהיר אותנו מלכתוב בדיקות null נוספות. forEach מקבל פונקציה לביצוע עבור כל אחד מערכי המפה. דוגמה זו מראה כיצד מתבצעות פעולות על ערכי מפה באמצעות פונקציות: לאחר מכן, נלמד כיצד להסיר ערך עבור מפתח נתון רק אם הוא ממופה לערך נתון: שיטה טובה נוספת: מיזוג ערכי מפה הוא די קל: מיזוג או שתכניס את המפתח/הערך למפה , אם אין ערך עבור המפתח הנתון, או שתקרא פונקציית המיזוג, שתשנה את הערך של הערך הקיים. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION