תכונות של Java 8 עם דוגמאות

Java 8 יוצא ב-18 במרץ 2014. זה זמן רב אבל עדיין פרוייקטים רבים פועלים על Java 8. זה קורה משום שהוא היה גרסה ראשית עם המון תכונות חדשות. בואו נסתכל על כל התכונות המרגשות והעיקריות של Java 8 עם קוד דוגמא.

סקירה מהירה של תכונות Java 8

כמה מהתכונות החשובות של Java 8 הן;

  1. forEach() שיטה בממשק Iterable
  2. שיטות ברירת מחדל וסטטיות בממשקים
  3. ממשקי פונקציונליות וביטויי Lambda
  4. API של Java Stream לפעולות מספר גדולות על מבני נתונים
  5. API של זמן ב-Java
  6. שיפורים ב-API של אוסף
  7. שיפורים ב-API של תכנות רציף
  8. שיפורים ב-IO של Java

בואו נסתכל בצורה קצרה על אלה תכונות של Java 8. אספק קטעי קוד עבור הבנת התכונות בצורה פשוטה.

1. forEach() שיטה בממשק Iterable

בכל פעם שאנו צריכים לעבור דרך אוסף, אנו צריכים ליצור Iterator שמטרתו היא לעבור במעגל, ואז יש לנו לוגיקת עסקים בלולאה עבור כל אחד מהאיברים באוסף. עשוי להתקבל ConcurrentModificationException אם האיטרטור לא משמש כראוי.

Java 8 הציגה את שיטת forEach בממשק java.lang.Iterable כך שבעת כתיבת הקוד אנו מתמקדים בלוגיקת עסקים. שיטת forEach מקבלת אובייקט java.util.function.Consumer כארגומנט, ולכן היא עוזרת לנו להפריד את הלוגיקה של העסקים במיקום נפרד שנוכל להשתמש בו מחדש. בואו נראה את שימוש ב- forEach עם דוגמה פשוטה.

package com.journaldev.java8.foreach;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;

public class Java8ForEachExample {

	public static void main(String[] args) {
		
		//יצירת אוסף דוגמא
		List<Integer> myList = new ArrayList<Integer>();
		for(int i=0; i<10; i++) myList.add(i);
		
		//עובר על ידי Iterator
		Iterator<Integer> it = myList.iterator();
		while(it.hasNext()){
			Integer i = it.next();
			System.out.println("Iterator Value::"+i);
		}
		
		//עובר דרך שיטת forEach של Iterable עם מחלקת אנונימית
		myList.forEach(new Consumer<Integer>() {

			public void accept(Integer t) {
				System.out.println("forEach anonymous class Value::"+t);
			}

		});
		
		//עובר עם מימוש ממשק Consumer
		MyConsumer action = new MyConsumer();
		myList.forEach(action);
		
	}

}

//מימוש Consumer שניתן לשימוש מחדש
class MyConsumer implements Consumer<Integer>{

	public void accept(Integer t) {
		System.out.println("Consumer impl Value::"+t);
	}
}

מספר השורות עשוי להגדיל אך forEach עוזרת בהפרדת הלוגיקה לעבודת העסקים ולוגיקת העגלה במקום נפרד, תורמת לקוד נקי ולהפרדת תחומים נקיים יותר.

2. שיטות ברירת מחדל וסטטיות בממשקים

אם תקראו את פרטי השיטה forEach בזהירות, תגלו שהיא מוגדרת בממשק Iterable, אך אנו יודעים שממשקים לא יכולים לכלול גוף של שיטה. החידוש מ-Java 8 הוא שממשקים משופרים לכלול שיטה עם מימוש. אנו יכולים להשתמש במילת המפתח default ובמילת המפתח static כדי ליצור ממשקים עם מימוש של שיטה. מימוש השיטה forEach בממשק Iterable הוא:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

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

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

package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface1 {

	void method1(String str);
	
	default void log(String str){
		System.out.println("I1 logging::"+str);
	}
	
	static void print(String str){
		System.out.println("Printing "+str);
	}
	
	//ניסיון לדרוס שיטת Object נותן שגיאה בזמן קומפילציה כי
	//"שיטה ברירת מחדל לא יכולה לדרוס שיטה מ-java.lang.Object"
	
//	default String toString(){
//		return "i1";
//	}
	
}
package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface2 {

	void method2();
	
	default void log(String str){
		System.out.println("I2 logging::"+str);
	}

}

שים לב ששני הממשקים יש להם שיטה משותפת log() עם הלוגיקה של המימוש.

package com.journaldev.java8.defaultmethod;

public class MyClass implements Interface1, Interface2 {

	@Override
	public void method2() {
	}

	@Override
	public void method1(String str) {
	}

	//MyClass לא תהיה מהדר בלעדי ללא מימוש log() שלה
	@Override
	public void log(String str){
		System.out.println("MyClass logging::"+str);
		Interface1.print("abc");
	}
	
}

כפי שניתן לראות שמקום Interface1 יש מימוש של שיטת סטטית שמשמשת במימוש של MyClass.log(). Java 8 משתמשת במידות default ו־static באופן רב ב-API Collection ושיטות ברירת המחדל מתווספות כדי שהקוד שלנו יישאר תואם לאחור.

אם כיתה כלשהי בהיררכיה מכילה שיטה עם אותה חתימה, אז שיטות ברירת המחדל הופכות ללא רלוונטיות. Object הוא המחלקה הבסיסית, כך שאם יש לנו שיטות ברירת מחדל כמו equals(), hashCode() בממשק, הן תהפוך ללא רלוונטיות. לכן למען בהירות טובה יותר, לא מותר לממש מתודות ברירת המחדל של Object בממשקים.

לפרטים מלאים על שינויי הממשק ב-Java 8, נא לקרוא את שינויי הממשק ב-Java 8.

3. ממשקים פונקציונליים וביטויי למבדר

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

@FunctionalInterface היא מתן למניעת הוספת מתודות מופשטות במקרים של ממשקים פונקציונליים. ניתן לחשוב על זה כמו על ההערה @Override וזה נחשב לפרקטיקה מומלצת להשתמש בו. ממשק ה- java.lang.Runnable עם מתודה מופשטת אחת בשם run() הוא דוגמה נהדרת לממשק פונקציונלי.

אחד היתרונות המרכזיים של הממשק הפונקציונלי הוא האפשרות להשתמש בביטויי lambda כדי ליצור אותם. אנו יכולים ליצור ממשק עם כיתה אנונימית אך הקוד נראה מפוחד.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

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

Runnable r1 = () -> {
			System.out.println("My Runnable");
		};

אם יש לך אחת במימוש המתודה, אין צורך בסוגריים מסולסלים גם. לדוגמה, ממשק1 של המחלקה אנונימית יכול להיות מופעל בעזרת lambda כך:

Interface1 i1 = (s) -> System.out.println(s);
		
i1.method1("abc");

אז לימבדה expressions הם דרך ליצירת מחלקות אנונימיות של ממשקי פונקציונל בקלות. אין יתרונות זמן ריצה בשימוש ב־lambda expressions, אז אני אשתמש בהם בזהירות מאחר ואין לי בעיה לכתוב כמה שורות קוד נוספות.

A new package java.util.function has been added with bunch of functional interfaces to provide target types for lambda expressions and method references. Lambda expressions are a huge topic, I will write a separate article on that in the future.

ניתן לקרוא את המדריך המלא ב־Java 8 Lambda Expressions Tutorial.

4. ממשק Stream API של Java לפעולות מסוג המון על מובנים

A new java.util.stream has been added in Java 8 to perform filter/map/reduce like operations with the collection. Stream API will allow sequential as well as parallel execution. This is one of the best features for me because I work a lot with Collections and usually with Big Data, we need to filter out them based on some conditions.

ממשק האוסף הוא מורחב עם שיטות ברירת מחדל של stream() ושל parallelStream() לקבלת הזרם לביצועים רצופים ופרלליים. בואו נצפה בשימושם עם דוגמה פשוטה.

package com.journaldev.java8.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {

	public static void main(String[] args) {
		
		List<Integer> myList = new ArrayList<>();
		for(int i=0; i<100; i++) myList.add(i);
		
		//זרם רצוף
		Stream<Integer> sequentialStream = myList.stream();
		
		//זרם פרללי
		Stream<Integer> parallelStream = myList.parallelStream();
		
		//שימוש ב־lambda עם Stream API, דוגמת סינון
		Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
		//שימוש ב־lambda בתוך forEach
		highNums.forEach(p -> System.out.println("High Nums parallel="+p));
		
		Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
		highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));

	}

}

אם תריצו את קטע הקוד הנ"ל, תקבלו פלט כמו זה:

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

שימו לב שערכי העיבוד הפרלליים אינם בסדר, לכן עיבוד פרללי יהיה מאוד מועיל בעבודה עם אוספים גדולים.

לכסות הכל על גבי Stream API אינו אפשרי בפוסט זה, תוכל לקרוא הכל על Stream API ב- Java 8 Stream API Example Tutorial.

5. Java Time API

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

כרקע ראשון ב-API החדש של זמן ב-Java, אני יכול להרגיש שזה יהיה מאוד קל לשימוש. יש כמה תת-חבילות: java.time.format שמספקת קלאסים להדפסה ולפירוק של תאריכים וזמנים, ו-java.time.zone שמספקת תמיכה באזורי זמן ובחוקיהם.

ה-API החדש לזמן מעדיף את האינומים על פני הקבועים השלמים לחודשים ולימים בשבוע. אחד הקלאסים השימושיים הוא DateTimeFormatter להמרת אובייקטי DateTime למחרוזות. למדריך מלא, תעבור ל- Java Date Time API Example Tutorial.

6. Collection API improvements

ישנן כבר את שיטת forEach() וממשק ה-Stream API לאוספים. כמה שיטות חדשות הוספו ל-API של Collection:

  • Iterator שיטת ברירת מחדל forEachRemaining(Consumer action) לביצוע הפעולה הנתונה עבור כל האיברים הנותרים עד שכל האיברים עוברים עיבוד או שהפעולה זורקת חריגה.
  • Collection שיטת ברירת מחדל removeIf(Predicate filter) להסרת כל האיברים באוסף זה שמקיימים את התנאי הנתון.
  • Collection שיטת spliterator() המחזירה מופע של Spliterator שניתן להשתמש בו כדי לעבור על האיברים בצורה רציפה או מקבוצה.
  • Map replaceAll(), compute(), merge() שיטות.
  • שיפור בביצועים עבור מחלקת HashMap עם התנגשויות בין מפות מפתחות.

7. שיפורי API תחרותיות

כמה שיפורי API תחרותיים חשובים הם:

  • ConcurrentHashMap שיטות compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() ו- search().
  • CompletableFuture שיכול להושלם באופן פורמלי (בהגדרת ערך ומצב שלו).
  • Executors שיטת newWorkStealingPool() ליצירת בריכת תהליכים מסוג WorkStealing שמשתמשת בכל התהליכים הזמינים כרמת פרלליזם שלו.

8. שיפורים ב- Java IO

כמה מהשיפורים ב-IO שידועים לי הם:

  • Files.list(Path dir) שמחזיר זרם שנמלא באופן עצלני, שבו האיברים הם הרשומות בתיקייה.
  • Files.lines(Path path) שקורא את כל השורות מקובץ כזרם.
  • Files.find() שמחזיר זרם שנמלא באופן עצלני עם נתיבים על ידי חיפוש אחר קבצים בעץ קבצים שמשתמש בקובץ התחלה נתון.
  • BufferedReader.lines() שמחזיר זרם שהאיברים בו הם שורות שנקראו מ- BufferedReader זה.

שיפורי ממשק ה- Core API של Java 8 השונים

כמה שיפורים שונים ב-API שעשויים להיות שימושיים הם:

  1. פעולה סטטית של ThreadLocal באמצעות השיטה withInitial(Supplier supplier) ליצירת מופעים בקלות.
  2. הממשק Comparator הוארך עם הרבה שיטות ברירת מחדל וסטטיות לסדר טבעי, סדר הפוך, וכו '.
  3. שיטות min(), max() ו- sum() במחלקות העטיפה Integer, Long ו- Double.
  4. השיטות logicalAnd(), logicalOr() ו- logicalXor() במחלקת Boolean.
  5. שיטת stream() במחלקת ZipFile כדי לקבל Stream מסודר על פריטי קובץ ה-ZIP. הפריטים מופיעים ב-Stream בסדר שבו הם מופיעים בפנימיה של קובץ ה-ZIP.
  6. כמה שיטות עזר במחלקת Math.
  7. נוסף פקודת jjs להפעלת מנוע Nashorn.
  8. נוסף פקודת jdeps לניתוח של קבצי המחלקות
  9. נסרק JDBC-ODBC Bridge.
  10. הוסרה זיכרון המרחב PermGen

זהו כל התכונות של Java 8 עם דוגמאות. אם פספסתי תכונות חשובות של Java 8, אנא הודיעו לי דרך תגובות.

Source:
https://www.digitalocean.com/community/tutorials/java-8-features-with-examples