מדריך להשלמת העתיד

1. הקדמה

מדריך זה הוא מדריך לפונקציונליות ולמקרי השימוש של העתיד מחלקה שהוצגה כשיפור API 8 של Java 8 Concurrency API.

2. חישוב אסינכרוני בג'אווה

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

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

ג'אווה 8 הציגה את העתיד מעמד. ביחד איתי עתיד ממשק, זה גם יישם את שלב השלמה מִמְשָׁק. ממשק זה מגדיר את החוזה לשלב חישוב אסינכרוני אותו נוכל לשלב עם שלבים אחרים.

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

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

3. שימוש העתיד כפשוטה עתיד

קודם כל, ה העתיד הכיתה מיישמת את עתיד ממשק, כדי שנוכל השתמש בו כ- עתיד יישום, אך עם לוגיקת השלמה נוספת.

לדוגמה, אנו יכולים ליצור מופע של מחלקה זו עם קונסטרוקטור ללא ארגון לייצג תוצאה עתידית כלשהי, למסור אותה לצרכנים ולהשלים אותה בזמן כלשהו בעתיד באמצעות לְהַשְׁלִים שיטה. הצרכנים רשאים להשתמש ב- לקבל שיטה לחסימת השרשור הנוכחי עד לתוצאה זו.

בדוגמה שלהלן, יש לנו שיטה שיוצרת a העתיד למשל, ואז מסובב חישוב כלשהו בשרשור אחר ומחזיר את עתיד מיד.

כאשר החישוב נעשה, השיטה משלימה את ה- עתיד על ידי מתן התוצאה ל לְהַשְׁלִים שיטה:

עתיד ציבורי calcAsync () זורק InterruptedException {CompletableFuture completeableFuture = new CompletableFuture (); Executors.newCachedThreadPool (). Submit (() -> {Thread.sleep (500); completeableFuture.complete ("שלום"); להחזיר null;}); החזרה הושלמה העתיד; }

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

שים לב ש ה calcAsync שיטה מחזירה א עתיד למשל.

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

שימו לב גם כי לקבל שיטה זורקת כמה חריגים בדוקים, כלומר ExecutionException (עוטף חריג שהתרחש במהלך חישוב) ו- חריג מופרע (יוצא מן הכלל המסמן שרשור שמבצע שיטה הופרע):

Future completeableFuture = calcAsync (); // ... תוצאת מחרוזת = completeFuture.get (); assertEquals ("שלום", תוצאה);

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

העתיד completeableFuture = CompletableFuture.completedFuture ("שלום"); // ... תוצאת מחרוזת = completeableFuture.get (); assertEquals ("שלום", תוצאה);

כתרחיש חלופי, אולי נרצה לבטל ביצוע א עתיד.

4. העתיד עם לוגיקה חישובית מקופלת

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

שיטות סטטיות runAsync ו supplyAsync אפשר לנו ליצור a העתיד מופע מתוך ניתן לרוץ ו ספק סוגים פונקציונליים בהתאמה.

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

ה ניתן לרוץ ממשק הוא אותו ממשק ישן שמשמש בשרשורים והוא אינו מאפשר להחזיר ערך.

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

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

CompletableFuture future = CompletableFuture.supplyAsync (() -> "שלום"); // ... assertEquals ("שלום", future.get ());

5. עיבוד תוצאות של חישובים אסינכרוניים

הדרך הגנרית ביותר לעבד את התוצאה של חישוב היא להזין אותה לפונקציה. ה לאחר מכן החל השיטה עושה בדיוק את זה; זה מקבל א פוּנקצִיָה למשל, משתמש בו לעיבוד התוצאה ומחזיר a עתיד המחזיקה בערך שהוחזר על ידי פונקציה:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "שלום"); CompletableFuture future = completeableFuture .thenApply (s -> s + "World"); assertEquals ("שלום עולם", future.get ());

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

יש שיטה למקרה השימוש הזה ב- העתיד. ה ואז קבל השיטה מקבלת א צרכן ומעביר אותה תוצאת החישוב. ואז הגמר future.get () שיחה מחזירה מופע של בָּטֵל סוּג:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "שלום"); CompletableFuture future = completeableFuture .thenAccept (s -> System.out.println ("חישוב הוחזר:" + s)); future.get ();

לבסוף, אם איננו זקוקים לערך החישוב ולא נרצה להחזיר ערך כלשהו בסוף השרשרת, נוכל להעביר ניתן לרוץ למבדה ל ואז הפעל שיטה. בדוגמה הבאה, אנו פשוט מדפיסים שורה במסוף לאחר קריאה ל- future.get ():

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "שלום"); CompletableFuture future = completeFuture .thenRun (() -> System.out.println ("חישוב הסתיים.")); future.get ();

6. שילוב של עתיד

החלק הכי טוב של העתיד ה- API הוא ה- יכולת לשלב העתיד מקרים בשרשרת שלבי חישוב.

התוצאה של השרשור הזה היא עצמה א העתיד שמאפשר עוד שרשור ושילוב. גישה זו נמצאת בכל מקום בשפות פונקציונליות ולעתים קרובות מכנים אותה דפוס עיצוב מונאדי.

בדוגמה הבאה אנו משתמשים ב- ואז חבר שיטה לשרשרת שתיים עתיד ברצף.

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

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "שלום"). ואז Compose (s -> CompletableFuture.supplyAsync (() -> s + "עולם")); assertEquals ("שלום עולם", completeableFuture.get ());

ה ואז חבר שיטה, יחד עם לאחר מכן החל, ליישם אבני יסוד בסיסיות בתבנית המונאדית. הם קשורים קשר הדוק ל מַפָּה ו flatMap שיטות של זרם ו אופציונאלי שיעורים זמינים גם ב- Java 8.

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

אם אנחנו רוצים לבצע שני עצמאיים עתיד ולעשות משהו עם התוצאות שלהם, אנחנו יכולים להשתמש ב- ואז שלב שיטה המקבלת א עתיד ו פוּנקצִיָה עם שני טיעונים לעיבוד שתי התוצאות:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "שלום") .thenCombine (CompletableFuture.supplyAsync (() -> "עולם"), (s1, s2) -> s1 + s2)); assertEquals ("שלום עולם", completeableFuture.get ());

מקרה פשוט יותר הוא כאשר אנו רוצים לעשות משהו עם שניים עתיד'תוצאות, אך אינן צריכות להעביר ערך כלשהו כתוצאה מטה עתיד שַׁרשֶׁרֶת. ה ואז קבל שניהם השיטה היא שם כדי לעזור:

CompletableFuture future = CompletableFuture.supplyAsync (() -> "שלום") .thenAcceptBoth (CompletableFuture.supplyAsync (() -> "עולם"), (s1, s2) -> System.out.println (s1 + s2));

7. ההבדל בין אז החל () ו ואז לכתוב ()

בסעיפים הקודמים הראינו דוגמאות לגבי אז החל () ו ואז לכתוב (). שני ממשקי ה- API עוזרים לשרשר שונים העתיד שיחות, אך השימוש בשתי הפונקציות הללו שונה.

7.1. אז החל ()

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

אז שיטה זו שימושית כשאנחנו רוצים לשנות את התוצאה של a העתיד שִׂיחָה:

CompletableFuture finalResult = מחשב (). ואז החל (s-> s + 1);

7.2. ואז לכתוב ()

ה ואז לכתוב () שיטה דומה ל- אז החל () בכך ששניהם מחזירים שלב השלמה חדש. למרות זאת, ואז לכתוב () משתמש בשלב הקודם כטיעון. זה ישתטח ויחזיר א עתיד עם התוצאה באופן ישיר, ולא עתיד מקונן כפי שצפינו ב אז החל ():

CompletableFuture computeAnother (Integer i) {להחזיר CompletableFuture.supplyAsync (() -> 10 + i); } CompletableFuture finalResult = לחשב (). ואז לחבר (זה :: computeAnother);

אז אם הרעיון הוא לשרשר העתיד שיטות אז עדיף להשתמש ואז לכתוב ().

כמו כן, שים לב שההבדל בין שתי השיטות הללו מקביל להבדל בין מַפָּה() ו flatMap ().

8. ריצה מרובה עתיד במקביל

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

ה CompletableFuture.allOf השיטה הסטטית מאפשרת לחכות להשלמת כל עתיד מסופק כ- var-arg:

CompletableFuture future1 = CompletableFuture.supplyAsync (() -> "שלום"); CompletableFuture future2 = CompletableFuture.supplyAsync (() -> "יפה"); CompletableFuture future3 = CompletableFuture.supplyAsync (() -> "עולם"); CompletableFuture combinedFuture = CompletableFuture.allOf (עתיד 1, עתיד 2, עתיד 3); // ... combinedFuture.get (); assertTrue (future1.isDone ()); assertTrue (future2.isDone ()); assertTrue (future3.isDone ());

שימו לב שסוג ההחזרה של ה- CompletableFuture.allOf () הוא העתיד. המגבלה של שיטה זו היא שהיא אינה מחזירה את התוצאות המשולבות של כולם עתיד. במקום זאת, עלינו להשיג תוצאות ידנית עתיד. לְמַרְבֶּה הַמַזָל, CompletableFuture.join () שיטה ו- Java 8 Streams API עושה את זה פשוט:

מחרוזת משולבת = Stream.of (future1, future2, future3) .map (CompletableFuture :: join) .collect (Collectors.joining ("")); assertEquals ("שלום עולם יפה", משולב);

ה CompletableFuture.join () שיטה דומה ל- לקבל שיטה, אך היא מביאה חריג לא מסומן למקרה עתיד לא מסתיים כרגיל. זה מאפשר להשתמש בו כהפניה לשיטה ב- Stream.map () שיטה.

9. טיפול בשגיאות

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

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

בדוגמה הבאה אנו משתמשים ב- ידית שיטה לספק ערך ברירת מחדל כאשר החישוב האסינכרוני של ברכה הסתיים עם שגיאה מכיוון שלא סופק שם:

שם מחרוזת = null; // ... CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> {if (name == null) {זרוק RuntimeException חדש ("שגיאת חישוב!");} החזיר "שלום," + שם;})}). ידית ((s, t) -> s! = null? s: "שלום, זר!"); assertEquals ("שלום, זר!", completeFuture.get ());

בתור תרחיש חלופי, נניח שאנחנו רוצים להשלים את ה- עתיד עם ערך, כמו בדוגמה הראשונה, אך יש להם גם את היכולת להשלים אותו למעט יוצא מן הכלל. ה להשלים באופן יוצא מן הכלל השיטה מיועדת בדיוק לזה. ה completeableFuture.get () השיטה בדוגמה הבאה זורקת ExecutionException עם חריגת זמן ריצה כגורם שלה:

CompletableFuture completeableFuture = חדש CompletableFuture (); // ... completeableFuture.completeExceptionally (RuntimeException חדש ("החישוב נכשל!"); // ... completeableFuture.get (); // ExecutionException

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

10. שיטות אסינכרון

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

השיטות ללא אסינכרון postfix מריצים את שלב הביצוע הבא באמצעות שרשור קריאה. לעומת זאת, ה אסינכרון שיטה ללא מוציא להורג טיעון מריץ שלב באמצעות המשותף מזלג / הצטרף יישום הבריכה של מוציא להורג לגישה באמצעות ה- ForkJoinPool.commonPool () שיטה. סוף - סוף, ה אסינכרון שיטה עם מוציא להורג טיעון מריץ שלב באמצעות ה- עבר מוציא להורג.

להלן דוגמה שונה המעבדת את התוצאה של חישוב עם פוּנקצִיָה למשל. ההבדל הגלוי היחיד הוא ואז החל Async שיטה, אך מתחת למכסה המנוע היישום של פונקציה עטוף ל- a ForkJoinTask למשל (למידע נוסף על מזלג / הצטרף מסגרת, עיין במאמר "מדריך למזלג / הצטרף למסגרת ב- Java"). זה מאפשר לנו להקביל את החישוב שלנו עוד יותר ולהשתמש במשאבי המערכת בצורה יעילה יותר:

CompletableFuture completeableFuture = CompletableFuture.supplyAsync (() -> "שלום"); CompletableFuture future = completeableFuture .thenApplyAsync (s -> s + "עולם"); assertEquals ("שלום עולם", future.get ());

11. JDK 9 העתיד ממשק API

Java 9 משפר את העתיד ממשק API עם השינויים הבאים:

  • נוספו שיטות מפעל חדשות
  • תמיכה בעיכובים ובפסקי זמן
  • תמיכה משופרת בסיווג משנה

ו- API חדשים של מופעים חדשים:

  • ברירת מחדל של המוציא לפועלExecutor ()
  • CompletableFuture newIncompleteFuture ()
  • העתק העתיד ()
  • CompletionStage minimalCompletionStage ()
  • השלמה העתיד הושלם Async (ספק הספק, מוציא להורג)
  • השלמה העתיד הושלם Async (ספק הספק)
  • CompleteFuture או Timeout (פסק זמן ארוך, יחידת TimeUnit)
  • CompleteTuture completeOnTimeout (ערך T, פסק זמן ארוך, יחידת TimeUnit)

כעת יש לנו כמה שיטות שימוש סטטיות:

  • מוציא לפועל (עיכוב ארוך, יחידת TimeUnit, מוציא לפועל)
  • ביצוע עיכוב ביצוע (עיכוב ארוך, יחידת TimeUnit)
  • השלמה שלב הושלם שלב (ערך U)
  • CompletionStage failedStage (ניתן לזרוק לשעבר)
  • CompleteFuture failedFuture (לשעבר לזריקה)

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

  • orTimeout ()
  • completeOnTimeout ()

הנה המאמר המפורט לקריאה נוספת: שיפורי Java 9 CompletableFuture API.

12. מסקנה

במאמר זה תיארנו את השיטות ומקרי השימוש האופייניים של העתיד מעמד.

קוד המקור של המאמר זמין באתר GitHub.