תכנות מתקדם ב-Java/בנאים

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

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


בנאים בסיסיים ובנאי ברירת המחדל[עריכה]

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

בנאי יכול להיות ריק, כמו זה:


public Constructors() {}

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

public class Constructors {

	public Constructors() {
		for(int i=0; i<10; i++) {
			System.out.println(i);
		}
	}
	
	public static void main(String[] args) {
		new Constructors();
	}

}

אמנם סגנון כזה הוא מסורבל ונהוג שלא להשתמש בו, אבל מותר.

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

public class Constructors {
	
	public static void main(String[] args) {
		Constructors c = new Constructors();
	}

}

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

העמסה של שיטות[עריכה]

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

private void print(String s) {
	System.out.println("Print a string: "+s);
}

private void print(int x) {
	System.out.println("Your number is "+x);
}

אם תגשו לשיטה עם ארגומנט מטיפוס String - תופעל השיטה הראשונה, ואם תגשו עם ארגומנט מטיפוס int - תופעל השיטה השנייה. מספר הארגומנטים אינו חייב להיות זהה, לדוגמה:

private void print(String s, int x) {
	System.out.println("Print a string: "+s+" number "+x);
}

private void print(int x) {
	System.out.println("Your number is "+x);
}

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

private int sameSame(int a) {
	return a;
}

private String sameSame(int b) {
	return String.valueOf(b);
}

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

בנוסף, גם שם המשתנה ששונה (a בעליונה ו b בתחתונה) לא יעזור לנו ועדיין תתרחש שגיאה.

העמסה של בנאים[עריכה]

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

public class Constructors {
	
	private int _x;
	private int _y;
	
	public Constructors() {
		_x = 0;
		_y = 0;
	}
	
	public Constructors(int x) {
		_x = x;
		_y = 0;
	}
	
	public Constructors(int x, int y) {
		_x = x;
		_y = y;
	}

}

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

Constructors c1 = new Constructors();
Constructors c2 = new Constructors(1);
Constructors c3 = new Constructors(1, 2);

בנאים שקוראים לבנאים[עריכה]

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

public class Constructors {
	
	private int[][] _arr;
	
	public Constructors(int x, int y) {
		_arr = new int[x][y];
		for(int i=0; i<x; i++) {
			for(int j=0; j<y; j++) {
				_arr[i][j] = -1;
			}
		}
	}

}

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

המילה this[עריכה]

משמעות המילה this ב-Java היא קריאה של האובייקט לעצמו. למשל: אם השיטה someMethod באובייקט Obj קוראת לשיטה אחרת באובייקט בשם anotherMethod, הפקודה anotherMethod(); היא למעשה this.anotherMethod(); אין צורך להוסיף את המילה this ברוב המקרים, אך לפעמים יהיו מקרים בהם נרצה להשתמש בה בכל זאת (למשל - אם נרצה ששיטה תחזיר את האובייקט עצמו, או שתתייחס לשדה באותו אובייקט). כך, נוכל להתייחס לשדות (instances) בעצמים יותר בקלות, ולא נצטרך לתת להם שם מיוחד, משום שמהדר יודע שבעזרת המילה this אנו פונים לשדה בעצם, ולא לפרמטר או משתנה אחר, למשל.

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

private final static int DEFAULT_X = 10;
private final static int DEFAULT_Y = 10;

public Constructors() {
	this(DEFAULT_X, DEFAULT_Y);
}

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

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

בנאי העתקה[עריכה]

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

	public Constructors(Constructors c) {
	_arr = c._arr;
}

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

public Constructors(Constructors c) {
	_arr = new int[c._arr.length][c._arr[0].length];
	for(int i=0; i<_arr.length; i++) {
		for(int j=0; j<_arr[0].length; j++) {
			_arr[i][j] = c._arr[i][j];
		}
	}
}

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

לסיכום[עריכה]

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


הפרק הקודם:
תיעוד
בנאים
תרגילים
הפרק הבא:
מבני נתונים