Java/שיטות

מתוך ויקיספר, אוסף הספרים והמדריכים החופשי

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

מבנה[עריכה]

כל שיטה בנוייה מכמה חלקים:

  • שם השיטה: זהו השם שמזהה את השיטה. הכללים לגבי שמות השיטות דומים לכללים המקבילים הנוגעים למשתנים. כדאי מאוד לתת לשיטות שם שמתאר את תפקידן בקצרה, כדי להקל על הבנת הקוד בעתיד. ב-Java נהוג לתת לשיטות שמות שמתחילים באות קטנה. אם שם השיטה מורכב מכמה מילים, האות הראשונה בכל מילה תהייה גדולה. דוגמה: printAllLines. באנגלית מכונה צורת כתיבה זו Camel Case (זאת, מפני שהאותיות הגדולות הנמצאות באמצע המילה יוצרות קו מתאר שמזכיר את גבו של הגמל (camel), כאשר האותיות הגדולות הן הדבשות).
  • משתני קלט: שיטות יכולות (אך אינן חייבות) לקבל קלט המורכב מאוסף של משתנים ואובייקטים.
  • משתנה פלט: שיטות יכולות (אך גם כאן, אינן חייבות) להחזיר ערך יחיד כלשהו. הבנה של אופן השימוש בשיטות תסביר מדוע קיימת המגבלה הזאת, של הערך היחיד, ומהן השיטות לעקוף אותה. ההחזרה מתבצעת באמצעות המילה השמורה return.
  • תוכן השיטה עצמו: אוסף הפעולות אותן מבצעת השיטה. האוסף הזה יכול להיות ארוך מאוד או קצר מאוד - גם שיטה ריקה היא חוקית! עם זאת, ראוי להימנע מכתיבת שיטות ארוכות מאוד, שעשויות להקשות על ההבנה.

שימוש[עריכה]

נציג כאן את אופן השימוש בשיטות באמצעות כמה דוגמאות. נתחיל בדוגמה לשיטה פשוטה מאוד:

public static void printHello() {
	System.out.println("Hello");
}

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

public class FirstMethod {

	public static void printHello() {
		System.out.println("Hello");
	}

	public static void main(String[] args) {
		printHello();
	}
}

כדי לקרוא לשיטה, כתבנו printHello();‎, ב-main והשיטה בוצעה. איננו חייבים לקרוא לשיטה דווקא מה-main: אפשר לקרוא לשיטה מכל שיטה אחרת, ואפילו מאותה השיטה עצמה (מה יקרה אם נוסיף את השורה printHello();‎ לשיטה printHello? נסו!). נתעכב כעת בצורה מפורטת יותר על השורה הראשונה של השיטה: תחילת השורה במילה public, שמצהירה על כך שהשיטה היא פומבית. זה עניין שנוגע לתכנות מתקדם יותר, אך בקצרה ניתן לומר שבג'אווה קיימת מערכת של הרשאות, והמילה public מאפשרת לכל אחד לגשת לשיטה זו. המילה הבאה היא static, שנוגעת לתכנות מונחה עצמים. גם לגביה, נוותר על ההסבר. המילה הבאה מעניינת אותנו הרבה יותר: המילה void היא הצהרה לגבי משתני הפלט של השיטה: המילה void, שפירושה המילולי הוא "ריק", אומרת למחשב שהשיטה לא מחזירה שום ערך שהוא. אם השיטה הייתה מחזירה ערך, את המילה void היה מחליף סוג המשתנה שהשיטה מיועדת להחזיר.

נעבור כעת לשיטה מעט מורכבת יותר, המשתמשת גם במשתני קלט ופלט:

public static int power(int x, int n) {
	if(n<=0)
            return 1;
	int num=1;
	for(int i=0; i<n; i++)
		num = num*x;
	return num;
}

זוהי שיטה שמטרתה לחשב חזקה: היא מקבלת שני מספרים שלמים ומחזירה את תוצאת ההעלאה בחזקה של המספר הראשון בשני. נעיר כי למען הנוחות, תוצאת העלאה בכל חזקה שלילית (או 0) נקבעה להיות 1, והשיטה מקבלת מספרים שלמים בלבד. נתבונן על השינויים בשיטה ביחס לשיטה הקודמת: השינויים הראשונים נמצאים בשורה הראשונה: את מקום המילה void החליפה המילה int, וזאת מכיוון שהשיטה הזו מחזירה ערך, והוא מטיפוס int. שינוי (משמעותי) אחר מהווים המשתנים int x ו-int n שהופיעו בתוך הסוגריים, שבשיטה הקודמת היו ריקות. משתנים אלה הם המשתנים שהשיטה מקבלת - הם מוגדרים בתוך השיטה וערכם הוא הערך שהוצב כאשר שיטה אחרת קראה לשיטה הזו. השינוי המשמעותי האחרון היא המילה return. כאשר מילה זו מופיעה - פעולת השיטה מסתיימת, והערך שנכתב אחרי המילה יוחזר. שימו לב - הערך שמוחזר חייב להיות מאותו הטיפוס שקבענו! לדוגמה, אם בשיטה power, שהוגדרה להחזיר טיפוס int, ננסה להחזיר מספר מטיפוס double - ניתקל בשגיאה. כמו שאפשר לראות, בעזרת return ניתן להחזיר משתנה או קבוע - אין הבדל, ובתנאי שהטיפוס מתאים. גם בשיטה שלא מחזירה ערך (void) אפשר להשתמש ב-return בלי כל ערך (return;‎), וזה יביא לסיום מיידי של פעולת השיטה - אמצעי שיכול להיות שימושי.

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

public class AnotherMethod {

	public static int power(int x, int n) {
		if(n<=0)
                    return 1;

		int num=1;
		for(int i=0; i<n; i++)
			num = num*x;
		return num;
	}

	public static void main(String[] args) {
		int i=2, j=5;
		int solution = power(i, j);
		System.out.println(i+"^"+j+"="+solution);
	}
}

באותה מידה שיכולנו לכתוב solution=32; או solution=i^j; הצבנו בו את ערך השיטה בצורה solution=power(i,j); והמשתנה solution קיבל את הערך אותו החזירה השיטה power עבור הערכים שהיו במשתנים i ו-j. נסו להריץ את התוכנית כמה פעמים, ובכל פעם שנו את הערכים במשתנים i ו-j - שימו לב כיצד משתנה הפלט בעקבות שינוי הערכים. זכרו שקיימים שני סוגים של שיטות, סוג אחד שמבצע פעולה כלשהי (בדרך כלל הדפסה) ובהכרזתו תהיה המילה void, והסוג השני שיחזיר ערך כלשהו ובהכרזתו תהיה המילה שמתאימה לסוג הערך שיחזיר (int, string, double וכו').

תחום הגדרה[עריכה]

נתחיל בניסוי. העתיקו את התוכנית הבאה:

public class Testing {

	public static void manipulate(int a, int b) {
		a=3;
		b=4;
	}

	public static void main(String[] args) {
		int a=1, b=2;
		manipulate(a, b);
		System.out.println("A is "+a+", B is "+b);
	}
}

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

A is 3, B is 4


למעשה הפלט היה

A is 1, B is 2


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

תחום ההגדרה של משתנים מקומיים[עריכה]

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

משתנים מקומיים וקריאה לשיטות[עריכה]

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

שיטת עבודה זו מכונה Pass by Value (בניגוד ל-Pass by Reference).

משתנים גלובליים[עריכה]

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

קבועים[עריכה]

קבועים הם משתנים גלובליים שאת ערכם מקבעים (בדרך כלל) בתחילת התוכנית באמצעות המילה השמורה final. לדוגמה, אם נרצה להגדיר קבוע מסוג int נכתוב final static int a=5; בתחילה עשויים הקבועים להיראות מיותרים - מדוע שמישהו ירצה משתנה שכזה? למעשה, מדובר בכלי שימושי וחשוב:

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

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

כדאי לדעת:

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

נציג כאן תוכנית פשוטה המשתמשת בקבועים:

import java.util.Scanner;

public class Testing {

	// Maximum pyramid's height
	final static int MAX_HEIGHT = 10;
	// Pyramid's print shape
	final static char SHAPE='#';
	
	/*
	 * Prints a pyramid with a given height. If height is illegal - quits
	 * with an error message.
	 */
	public static void printTheShape(int height) {
		// Check legality of height
		if(height>MAX_HEIGHT || height<1) {
			System.out.println("Illegal height!");
			return;
		}
		// Draws the pyramid
		for(int i=1;i<=height; i++) {
			for(int j=0; j<i; j++) 
			     	System.out.print(SHAPE);    
			System.out.println();
		}
	}

	/*
	 * Main method
	 */
	public static void main(String[] args) {
		Scanner s = new Scanner(System.in);
		System.out.print("Enter pyramid's height: ");
		int h = s.nextInt();
		printTheShape(h);
	}
}

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

החזרה של משתנים מרובים[עריכה]

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

  • החזרה של מערך. המערך הוא אמנם עצם אחד, אבל מכיל בתוכו איברים רבים. נראה דוגמה לכך:
public class Methods {

	// Array size
	public static final int N=10;
	
	/*
	 * Create and return a new array
	 */
	public static int[] returnArr(int n) {
		int[] newArr = new int[n];
		for(int i=0; i<n; i++)
			newArr[i] = i;
		return newArr;
	}
	
	/*
	 * Prints an integer array
	 */
	public static void printArr(int[] arr) {
		for(int i=0; i<arr.length; i++)
			System.out.print(arr[i]+" ");
		System.out.println();
	}
	
	/*
	 * Main method 
	 */
	public static void main(String[] args) {
		int[] myArr = returnArr(N);
		printArr(myArr);
	}
}
  • שינוי ערכי מערך. מערך, בהיותו אובייקט, שומר על הערכים שבתוכו גם במעבר בין שיטות. ניתן להשתמש בשיטה זו כדי להשתמש במערך כמתווך, שיכיל את הערכים אותם אנחנו רוצים להעביר, וזאת בלי להשתמש כלל בפקודה return (או להשתמש בה כדי להחזיר נתון נוסף). נראה דוגמה:
public class Methods {

	// Array size
	public static final int N=10;
	
	/*
	 * Change array values
	 */
	public static void changeArr(int[] arr) {
		for(int i=0; i<arr.length; i++)
			arr[i] = arr[i]+1;
	}
	
	/*
	 * Prints an integer array
	 */
	public static void printArr(int[] arr) {
		for(int i=0; i<arr.length; i++)
			System.out.print(arr[i]+" ");
		System.out.println();
	}
	
	/*
	 * Main method 
	 */
	public static void main(String[] args) {
		int[] myArr = new int[N];
		for(int i=0; i<N; i++)
			myArr[i] = i;
		System.out.print("Before changing: ");
		printArr(myArr);
		changeArr(myArr);
		System.out.print("After changing: ");
		printArr(myArr);
	}
}

עקרונות מנחים[עריכה]

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


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