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

1. הקדמה

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

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

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

2. לכידת למבדות

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

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

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

3. משתנים מקומיים בלכידת למבדות

פשוט שים, זה לא יהיה קומפילציה:

מדרגת ספק (int התחלה) {return () -> start ++; }

הַתחָלָה הוא משתנה מקומי, ואנחנו מנסים לשנות אותו בתוך ביטוי למבדה.

הסיבה הבסיסית שזה לא יאסוף היא שהממבה הוא לכידת הערך של הַתחָלָה, כלומר להכין עותק ממנו. אילוץ המשתנה להיות סופי נמנע מלתת את הרושם כי תוספת הַתחָלָה בתוך הלמדה באמת יכול לשנות את הַתחָלָה פרמטר השיטה.

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

3.1. סוגיות במקביל

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

מה עלינו לעשות כאן:

חלל ציבורי localVariableMultithreading () {ריצה בוליאנית = נכון; executor.execute (() -> {while (run) {// do operation}}); לרוץ = שקר; }

אמנם זה נראה תמים, אך יש לו את הבעיה הערמומית של "נראות". נזכיר כי כל שרשור מקבל ערימה משלו, וכך כיצד אנו מבטיחים כי שלנו בזמן לוּלָאָה רואה השינוי ל- לָרוּץ משתנה בערימה השנייה? התשובה בהקשרים אחרים יכולה להיות שימוש מסונכרן בלוקים או נָדִיף מילת מפתח.

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

4. משתנים סטטיים או משתנים בלכידת למבדות

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

אנחנו יכולים להכין את הדוגמה הראשונה שלנו להרכיב רק על ידי המרת שלנו הַתחָלָה משתנה למשתנה מופע:

התחלה פרטית int = 0; מדרגת ספקים () {return () -> התחל ++; }

אבל למה אנחנו יכולים לשנות את הערך של הַתחָלָה פה?

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

אנו יכולים לתקן את הדוגמה השנייה שלנו על ידי ביצוע אותו הדבר:

ריצה בוליאנית נדיפה פרטית = נכון; מופע חלל ציבוריVariableMultithreading () {executor.execute (() -> {while (run) {// do operation}}); לרוץ = שקר; }

ה לָרוּץ משתנה גלוי כעת למבדה גם כאשר הוא מבוצע בשרשור אחר מאז שהוספנו את נָדִיף מילת מפתח.

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

5. הימנע מדרך לעקיפת הבעיה

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

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

workaroundSingleThread () int [] מחזיק = int int [] {2}; סכומי IntStream = IntStream .of (1, 2, 3) .map (val -> val + holder [0]); מחזיק [0] = 0; סכומי החזר. sum (); }

יכולנו לחשוב שהזרם מסכם 2 לכל ערך, אבל זה למעשה מסכם 0 מכיוון שזהו הערך העדכני ביותר הזמין בעת ​​ביצוע הלמדה.

בואו נתקדם צעד נוסף ונבצע את הסכום בשרשור אחר:

דרכים לעקיפת הריק הציבוריMultithreading () {int [] holder = int int [] {2}; Runnable runnable = () -> System.out.println (IntStream .of (1, 2, 3) .map (val -> val + holder [0]) .sum ()); שרשור חדש (ניתן להריצה) .התחל (); // לדמות עיבוד כלשהו לנסות {Thread.sleep (אקראי חדש (). nextInt (3) * 1000L); } לתפוס (InterruptedException e) {לזרוק RuntimeException (e) חדש; } מחזיק [0] = 0; }

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

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

6. מסקנה

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

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


$config[zx-auto] not found$config[zx-overlay] not found