עד 50% הנחה
לקורסי בוקר במתכונת BOOTCAMP!
4 חודשים ונכנסתם להייטק כמפתחים מן השורה
אל תחמיצו את מועדי הפתיחה הקרובים:
מסלול RT Embedded Linux | 10/09 |
מסלול Cyber | 10/09 |
מסלול Computer Vision | 10/09 |
מסלול Machine Learning | 15/09 |
מסלול Full Stack | 28/10 |
עודכן לאחרונה: 1 נובמבר, 2021
JAVA היא שפת מאוד פופולרית בימים אלו בתעשיית ההייטק. כמו בשנים עברו גם כיום יש דרישה מאוד גבוהה למפתחי JAVA מיומנים. כמובן שלצד דרישה גבוהה לעובדים ומשכורות מפנקות על התפקידים הטובים בחברות מובילות ובין לאומיות יש מטבע הדברים תחרות מאוד גדולה. אז איך מתבלטים מעל שאר המתמודדים? אחת האפשרויות העיקריות היא פשוט לכתוב קוד בצורה נכונה, לכתוב קוד נקי ויעיל אשר יעשה את עבודתו בצורה הטובה ביותר וגם יאפשר תחזוקה שוטפת ואפשרות לגורמים נוספים לבצע שיפורים ושינויים.
איך עושים זאת נכון? הכנו עבורכם מאמר עם מספר דגשים חשובים בכדי לכתוב בצורה נכונה, בהצלחה:)
מחרוזות בג'אווה הם Immutables. משמעות הדבר הוא שמרגע שנוצר אובייקט מחרוזת עם מידע מסוים (התווים שמתארים את הטקסט שלו), המידע הזה לא משתנה לעולם. לא ניתן לעדכן את התווים או להוסיף או להסיר תווים מהמחרוזת. כל פונקציה או פעולה שמבצעת שינוי על מחרוזת בעצם יוצרת מחרוזת חדשה עם השינויים.
משמעות הדבר היא שבכל פעם שאנחנו מפעילים פעולה על מחרוזת אחת כדי לקבל מחרוזת שנייה, המחרוזת הראשונה מועתקת עם שינויים בתור המחרוזת השנייה ובזיכרון יהיו שני אובייקטים מחרוזת. אם למחרוזת הישנה אין יותר הפניות (references) אז אוסף הזבל (ה-garbage collector) ידאג לפנות אותה מהזיכרון.
דבר זה אומר שפעולות מרובות על מחרוזות יכולות לפגוע ביעלות של התכנית מפני שכל פעם מדובר בתהליך של העתקה של המידע המעודכן ובפינוי של מהזיכרון של המידע הישן.
נתבונן בקטע הקוד הבא:
String text = "ABC"; text += "123"; text += "XYZ";
הפעולות שנעשות בזיכרון עבור תכנית זאת היא כדלהלן:
1) בזיכרון עבור "ABC", "123", ו- "XYZ" יוקצו בד"כ לזיכרון בתהליך שנקרא interning
2) בשורה הראשונה text מקבל הפנייה ל-"ABC"
3) בשורה השנייה נוצר אובייקט מחרוזת חדש בזיכרון שמעתיק את התוכן של "ABC" ושל -"123" (העתקה של 6 תווים) ומתקבלת
המחרוזת "ABC123".
4) בשורה השלישית נוצר אובייקט מחרוזת חדש בזיכרון שמעתיק את התוכן של "ABC123" ושל "XYZ" (העתקה של 9 תווים)
5) כיוון שהמחרוזת "ABC123" נותרת ללא הפניה, ה-garbage collector ינקה אותה מהזיכרון כאשר יבצע סקירה.
ז"א שאפילו בקוד תמים כמו זה, פעולה שאמורה ליצור מחרוזת בת 9 תווים, התבצעה העתקה של 15 תווים ופינוי בזיכרון של
אובייקט אחר לפחות.
דבר זה מחמיר כאשר השרשור צריך להתבצע מספר רב יותר של פעמים.
Scanner in = new Scanner(System.in); String s = ""; for (int i=0; i<100; i++) { s += in.nextLine(); } System.out.println("result:") System.out println(s)
בקוד שלעיל נקלט מהמשתמש 100 שורות של טקסט שמשורשרות ביחד למחרוזת במשתנה s. אפילו אם בכל קליטה מדובר במחרוזת של תו בודד, תתבצע סך של !100 (מאה עצרת) העתקות (בפעם הראשונה יועתק תו אחד, בפעם השנייה יועתקו שני תווים ואז שלוש וכולי עד מאה)
הפתרון המוצע בג'אווה הוא להשתמש באובייקט אחר לניהול מחרוזות שנקרא StringBuilder. אובייקט זה יכול לשמור תווים כמו מחרוזת, אבל בניגוד למחרוזות קלאסיות, StringBuilder הוא Mutable ובנוי באופן שנועד לאפשר שינויים יעילים של מחרוזות.
נתבונן בתוכנית מעודכנת
Scanner in = new Scanner(System.in); StringBuilder sb = StringBuilder(); for (int i=0; i<100; i++) { sb.append(in.nextLine()); } System.out.println("result:") System.out println(s.toString())
הודות ל-StringBuilder נוכל לצמצם את מספר ההעתקות למינימום, ראשית התווים שנקלטים מועתקים למחרוזת שנבנית.
לאחר מכן בשורה האחרונה אנחנו יוצרים מחרוזת קלאסית שמעתיקה את המידע שלה מה-StringBuilder כך שעבור N תווים נבצע 2N העתקות (בניגוד ל-N עצרת העתקות) שימו לב שבחישוב זה לא כללנו את המחרוזות הנקלטות עצמן (עוד N העתקות של תווים לסך של 3N)
בנוסף ל-StringBuilder קיימת לנו מחלקה נוספת בשם StringBuffer שמבצעת את אותן הפעולות, אבל StringBuffer בנויה להיות thread safe, ז"א שהקוד שלה מתבצע בפחות יעילות אבל בתמורה מספק הגנה מפני גישה מקבילה. אז לרוב נעדיף לעבוד עם StringBuilder אך אם התוכנית שלנו תצטרך לעבוד עם מספר threads במקביל, נצטרך לעבוד עם StringBuffer.
יש לציין שלמרות האמור לעיל, אין בעיה "לפרק" מחרוזת למספר שורות ע"י שימוש בפעולת השרשור. במקרים כאלה הקומפיילר יהיה חכם מספיק להבין שמדובר במחרוזת אחת ארוכה ולא לבצע שרשור של המחרוזות. זאת אומרת שקטעי הקוד הבאים שקולים זה לזה:
String s = "ABC123XYZ";
String s = "ABC" + "123" + "XYZ";
כאשר נרצה להשתמש בערכים בסיסיים כגון מספרים או ערכי true ו-false נעדיף להשתמש בטיפוסים פרימיטיביים (int, short ,long ,float double, boolean ,char) טיפוסים אלה אינם אובייקטים וכתוצאה מכך תופסים פחות מקום באופן משמעותי (בית אחד עד 8 בתים עבור פרימיטיביים לעומת 16 בתים או יותר עבור אובייקטים). כמו-כן הגישה למידע נעשה בצורה ישירה יותר כיוון שפרימיטיביים נשמרים על גב המחסנית לעומת אובייקטים בג'אווה שנשמרים תמיד על ה-heap והגישה אליהם היא באמצעות הפנייה.
כאשר נרצה לכתוב קוד שבודק את ערכו של ביטוי בודד נעדיף להשתמש ב-switch case לעומת if else.
למשל נתבונן בקטע הקוד הבא:
Scanner in = new Scanner(System.in); int x = in.nextInt(); if (x == 1) System.out.println("Option 1"); else if (x == 2) System.out.println("Option 2"); else if (x == 3) System.out.println("Option 3"); else if (x == 4) System.out.println("Option 4");
כדי להגיע לאפשרות 4, המחשב צריך לבצע 4 תנאים אילו היה מדובר ב-100 אפשרויות המחשב היה צריך לבצע 100 תנאים עבור התנאי ה-100 ו - 99 תנאים עבור התנאי ה-99 וכו'. ז"א שהסיבוכיות היא (O(n.
לעומת זאת
Scanner in = new Scanner(System.in); int x = in.nextInt(); Switch(x){ case 1: System.out.println("Option 1"); case 2: System.out.println("Option 2"); case 3: System.out.println("Option 3"); case 4: System.out.println("Option 4"); }
במקרה זה, Java מייצרת טבלת קפיצה (lookupswitch או tableswitch) שלוקחת את הערך המושווה ומחפשת אותו בטבלה (תהליך בסיבוכיות של (1)O עבור tableswitch ובסיבוכיות של (O(log n עבור lookupswitch) ובהתאם אליו מואצת לאיזה אזור בקוד יש לקפוץ ואז מבצעת קפיצה נוספת.
יש לציין שבמקרים מסוימים של תנאים פשוטים או קצרים יחסית, התוספת בפעולות של switch דווקא פוגעת ביעילות של הקוד לכן בתנאים קצרים יחסית נעדיף להשתמש ב-if else.
בנוסף יש לציין שכמעט תמיד, נעדיף להתמקד בכתיבת קוד קריא ונח לתחזוקה ולייעל אותו במידת הצורך. ובכל מקרה יש לבדוק האם באמת נעשה כאן שיפור על ידי מדידה של הזמן שנדרש לביצוע הפעולות לפני ואחרי השינוי (benchmarking)