תכנות ג'אווה עם ביטויי למבדה

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

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

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

העוצמה של ביטויים למבדה חורגת הרבה מעבר למקרה לשימוש חד פעמי, אך לימוד יישומים שונים של אותה דוגמה אמור להשאיר אותך עם תחושה מוצקה של איך lambdas יועילו לתוכניות Java שלך. במאמר זה אשתמש בדוגמה נפוצה כדי לתאר את הבעיה, ואז אספק פתרונות שנכתבו ב- C ++, Java לפני ביטויי lambda, ו- Java עם ביטויים lambda. שים לב כי רקע חזק במתמטיקה אינו נדרש כדי להבין ולהעריך את הנקודות העיקריות של מאמר זה.

למידה על למבדות

ביטויים למבדה, המכונים גם סגירות, מילולי פונקציה או פשוט למבדה, מתארים קבוצה של תכונות שהוגדרו בבקשת Java Specification Request (JSR) 335. מבוא פחות רשמי / קריא יותר לביטויי למבדה ניתנים בחלק מהגרסה האחרונה של מדריך Java ובכמה מאמרים מאת בריאן גץ, "מצב הלמבה" ו"מצב הלמבה: מהדורת הספריות. " משאבים אלה מתארים את התחביר של ביטויי למבדה ומספקים דוגמאות למקרי שימוש שבהם יש ביטוי למבדה. למידע נוסף על ביטויי lambda ב- Java 8, צפו בכתובת המרכזית הטכנית של מארק ריינהולד ל- JavaOne 2013.

ביטויים למבדה בדוגמה מתמטית

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

  • פונקציה שאנחנו רוצים לשלב.
  • שני מספרים אמיתיים aואת bהמייצגים את נקודות הקצה של מרווח [a,b]בקו המספר האמיתי. (שים לב שהפונקציה שהוזכרה לעיל צריכה להיות רציפה במרווח זה.)
  • מספר שלם זוגי nשמציין מספר אינטרוולים. בבואנו ליישם הכלל של סימפסון אנו מחלקים את המרווח [a,b]לתוך nsubintervals.

כדי לפשט את המצגת, בואו נתמקד בממשק התכנות ולא בפרטי היישום. (למען האמת, אני מקווה כי גישה זו תאפשר לנו לעקוף טענות על הדרך הטובה ביותר או היעילה ביותר כדי ליישם כלל של סימפסון, אשר איננו המוקד של מאמר זה.) נשתמש סוג doubleעבור פרמטרים aו b, ו נשתמש סוג intעבור פרמטר n. הפונקציה שיש לשלב תיקח פרמטר יחיד מהסוג doubleוהחזרת ערך מסוג double.

הורד הורד את דוגמת קוד המקור C ++ עבור מאמר זה. נוצר על ידי ג'ון I. מור עבור JavaWorld

פרמטרים של פונקציות ב- C ++

כדי לספק בסיס להשוואה, נתחיל במפרט C ++. כאשר מעבירים פונקציה כפרמטר ב- C ++, בדרך כלל אני מעדיף לציין את החתימה של פרמטר הפונקציה באמצעות a typedef. רישום 1 מציג קובץ כותרת C ++ בשם simpson.hהמציין הן את typedefפרמטר הפונקציה והן את ממשק התכנות לפונקציה C ++ בשם integrate. גוף הפונקציה של integrateכלול בקובץ קוד מקור C ++ בשם simpson.cpp(לא מוצג) ומספק את ההטמעה של הכלל של סימפסון.

רישום 1. קובץ כותרת C ++ לכלל סימפסון

 #if !defined(SIMPSON_H) #define SIMPSON_H #include  using namespace std; typedef double DoubleFunction(double x); double integrate(DoubleFunction f, double a, double b, int n) throw(invalid_argument); #endif 

השיחות integrateפשוטות ב- C ++. כדוגמה פשוטה, נניח שרצית להשתמש בכלל של סימפסון בכדי לקרב את האינטגרל של פונקציית הסינוס מ- 0π ( PI) באמצעות 30אינטרוולים. (מי השלים חדו"א אני צריך להיות מסוגל לחשב את התשובה בדיוק ללא עזרת מחשבון, מה שהופך מקרה מבחן טוב זה עבור integrateהפונקציה.) בהנחה שאתה לא כללת את הכותרת הנכונה קבצים כגון ו "simpson.h", אתה תהיה מסוגל לתפקוד שיחות integrateכפי שמוצג ברשימה 2.

רישום 2. C ++ קורא לשילוב פונקציות

 double result = integrate(sin, 0, M_PI, 30); 

זה כל מה שיש בזה. ב- C ++ אתה מעביר את פונקציית הסינוס באותה קלות שאתה מעביר את שלושת הפרמטרים האחרים.

דוגמה אחרת

במקום הכלל של סימפסון יכולתי להשתמש באותה קלות בשיטת החיתוך ( aka Algorithm Bisection) לפתרון משוואה של הטופס f (x) = 0 . למעשה, קוד המקור של מאמר זה כולל יישומים פשוטים הן של כלל סימפסון והן של שיטת החיתוך.

הורד הורד את דוגמאות קוד המקור של Java למאמר זה. נוצר על ידי ג'ון I. מור עבור JavaWorld

ג'אווה ללא ביטויים למבדה

עכשיו בואו נסתכל כיצד ניתן לציין את הכלל של סימפסון בג'אווה. לא משנה אם אנו משתמשים בביטויי lambda ובין אם לאו, אנו משתמשים בממשק Java המוצג ברשימה 3 במקום C ++ typedefכדי לציין את החתימה של פרמטר הפונקציה.

רישום 3. ממשק Java לפרמטר הפונקציה

 public interface DoubleFunction { public double f(double x); } 

כדי ליישם את הכלל של סימפסון ב- Java אנו יוצרים מחלקה בשם Simpsonהמכילה שיטה integrate, עם ארבעה פרמטרים דומים למה שעשינו ב- C ++. כמו בהרבה שיטות מתמטיות עצמאיות (ראה, למשל, java.lang.Math), אנו נכין integrateשיטה סטטית. השיטה integrateמוגדרת כדלקמן:

רישום 4. חתימת Java לשילוב השיטה בכיתה סימפסון

 public static double integrate(DoubleFunction df, double a, double b, int n) 

כל מה שעשינו עד כה בג'אווה אינו תלוי אם נשתמש בביטויי למבדה או לא. ההבדל העיקרי בביטויים למבדה הוא באופן שבו אנו מעבירים פרמטרים (באופן ספציפי יותר, כיצד אנו מעבירים את פרמטר הפונקציה) בקריאה לשיטה integrate. ראשית אדגים כיצד הדבר ייעשה בגירסאות Java לפני גרסה 8; כלומר, ללא ביטויים למבדה. כמו בדוגמה C ++, נניח כי אנו רוצים לקרוב את האינטגרל של פונקציית הסינוס מ- 0π ( PI) באמצעות 30רווחי משנה.

שימוש בתבנית המתאם לתפקוד הסינוס

בג'אווה יש לנו יישום של פונקציית הסינוס הזמינה java.lang.Math, אך עם גרסאות ג'אווה לפני Java 8, אין דרך פשוטה וישירה להעביר פונקציית סינוס זו לשיטה integrateבכיתה Simpson. גישה אחת היא להשתמש בתבנית המתאם. במקרה זה נכתוב מחלקת מתאם פשוטה המיישמת את DoubleFunctionהממשק ומתאימה אותו לכינוי פונקציית הסינוס , כפי שמוצג ברשימה 5.

רישום 5. כיתת מתאם לשיטה Math.sin

 import com.softmoore.math.DoubleFunction; public class DoubleFunctionSineAdapter implements DoubleFunction { public double f(double x) { return Math.sin(x); } } 

באמצעות מחלקת מתאם זו אנו יכולים כעת לקרוא integrateלשיטת המחלקה Simpsonכפי שמוצג ברשימה 6.

רישום 6. שימוש בכיתת המתאם כדי לקרוא לשיטה Simpson.integrate

 DoubleFunctionSineAdapter sine = new DoubleFunctionSineAdapter(); double result = Simpson.integrate(sine, 0, Math.PI, 30); 

Let's stop a moment and compare what was required to make the call to integrate in C++ versus what was required in earlier versions of Java. With C++, we simply called integrate, passing in the four parameters. With Java, we had to create a new adapter class and then instantiate this class in order to make the call. If we wanted to integrate several functions, we would need to write an adapter class for each of them.

We could shorten the code needed to call integrate slightly from two Java statements to one by creating the new instance of the adapter class within the call to integrate. Using an anonymous class rather than creating a separate adapter class would be another way to slightly reduce the overall effort, as shown in Listing 7.

Listing 7. Using an anonymous class to call method Simpson.integrate

 DoubleFunction sineAdapter = new DoubleFunction() { public double f(double x) { return Math.sin(x); } }; double result = Simpson.integrate(sineAdapter, 0, Math.PI, 30); 

Without lambda expressions, what you see in Listing 7 is about the least amount of code that you could write in Java to call the integrate method, but it is still much more cumbersome than what was required for C++. I am also not that happy with using anonymous classes, although I have used them a lot in the past. I dislike the syntax and have always considered it to be a clumsy but necessary hack in the Java language.

Java with lambda expressions and functional interfaces

Now let's look at how we could use lambda expressions in Java 8 to simplify the call to integrate in Java. Because the interface DoubleFunction requires the implementation of only a single method it is a candidate for lambda expressions. If we know in advance that we are going to use lambda expressions, we can annotate the interface with @FunctionalInterface, a new annotation for Java 8 that says we have a functional interface. Note that this annotation is not required, but it gives us an extra check that everything is consistent, similar to the @Override annotation in earlier versions of Java.

The syntax of a lambda expression is an argument list enclosed in parentheses, an arrow token (->), and a function body. The body can be either a statement block (enclosed in braces) or a single expression. Listing 8 shows a lambda expression that implements the interface DoubleFunction and is then passed to method integrate.

Listing 8. Using a lambda expression to call method Simpson.integrate

 DoubleFunction sine = (double x) -> Math.sin(x); double result = Simpson.integrate(sine, 0, Math.PI, 30); 

Note that we did not have to write the adapter class or create an instance of an anonymous class. Also note that we could have written the above in a single statement by substituting the lambda expression itself, (double x) -> Math.sin(x), for the parameter sine in the second statement above, eliminating the first statement. Now we are getting much closer to the simple syntax that we had in C++. But wait! There's more!

The name of the functional interface is not part of the lambda expression but can be inferred based on the context. The type double for the parameter of the lambda expression can also be inferred from the context. Finally, if there is only one parameter in the lambda expression, then we can omit the parentheses. Thus we can abbreviate the code to call method integrate to a single line of code, as shown in Listing 9.

Listing 9. An alternate format for lambda expression in call to Simpson.integrate

 double result = Simpson.integrate(x -> Math.sin(x), 0, Math.PI, 30); 

But wait! There's even more!

Method references in Java 8

תכונה קשורה נוספת ב- Java 8 היא דבר הנקרא הפניה לשיטה , המאפשרת לנו להתייחס לשיטה קיימת בשם. ניתן להשתמש בהפניות לשיטה במקום ביטויים למבדה כל עוד הם עומדים בדרישות הממשק הפונקציונלי. כמתואר במשאבים, ישנם מספר סוגים שונים של הפניות לשיטות, כל אחת עם תחביר שונה במקצת. לשיטות סטטיות התחביר הוא Classname::methodName. לכן, באמצעות הפניה לשיטה, אנו יכולים לקרוא integrateלשיטה בג'אווה בפשטות כפי שיכולנו ב- C ++. השווה את שיחת Java 8 המוצגת ברשימה 10 להלן עם שיחת C ++ המקורית המוצגת ברישום 2 לעיל.

רישום 10. באמצעות הפניה לשיטה להתקשר ל- Simpson.integrate

 double result = Simpson.integrate(Math::sin, 0, Math.PI, 30);