ירושה בג'אווה, חלק 1: מילת המפתח המרחיבה

Java תומכת בשימוש חוזר בשיעור באמצעות ירושה והרכב. מדריך שני חלקים זה מלמד כיצד להשתמש בירושה בתוכניות Java שלך. בחלק 1 תלמד כיצד להשתמש extendsבמילת המפתח להפקת כיתת ילדים מכיתת הורים, להפעיל קונסטרוקציות ושיטות כיתת הורים ולדרוס שיטות. בחלק 2 תסייר java.lang.Object, שהוא מעמד העל של ג'אווה שממנו יורש כל מחלקה אחרת.

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

הורד קבל את הקוד הורד את קוד המקור למשל יישומים במדריך זה. נוצר על ידי ג'ף פרייזן עבור JavaWorld.

ירושת ג'אווה: שתי דוגמאות

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

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

ג'ף פריזן

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

ג'ף פריזן

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

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

מילת המפתח מרחיבה

Java תומכת בהרחבת כיתות באמצעות extendsמילת המפתח. כאשר הוא קיים, extendsמציין קשר הורה-ילד בין שתי כיתות. להלן אני משתמש extendsכדי ליצור מערכת יחסים בין כיתות Vehicleו Car, ולאחר מכן בין Accountלבין SavingsAccount:

רישום 1. extendsמילת המפתח מציינת יחסי הורה וילד

class Vehicle { // member declarations } class Car extends Vehicle { // inherit accessible members from Vehicle // provide own member declarations } class Account { // member declarations } class SavingsAccount extends Account { // inherit accessible members from Account // provide own member declarations }

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

דוגמאות אלה מקודדים משמשת בתור יחסים: Carהוא התמחה Vehicleו SavingsAccountהוא מתמחה Account. Vehicleו Accountידוע כמו מחלקות בסיס , כיתות הורה , או superclasses . Carו SavingsAccountידועים כמו במחלקות הנגזרות , כיתות הילד , או subclasses .

שיעורים אחרונים

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

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

רישום 2. Accountכיתת הורים

class Account { private String name; private long amount; Account(String name, long amount) { this.name = name; setAmount(amount); } void deposit(long amount) { this.amount += amount; } String getName() { return name; } long getAmount() { return amount; } void setAmount(long amount) { this.amount = amount; } }

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

ייצוג ערכי מטבע

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

רישום 3 מציג SavingsAccountכיתת ילדים המרחיבה את Accountכיתת ההורים שלה .

רישום 3. SavingsAccountכיתת ילדים מרחיבה את Accountכיתת ההורים שלה

class SavingsAccount extends Account { SavingsAccount(long amount) { super("savings", amount); } }

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

מתי ואיפה לקרוא סופר ()

כמו this()שחייב להיות האלמנט הראשון בבנאי שקורא לבנאי אחר באותה כיתה, super()חייב להיות האלמנט הראשון בבנאי שקורא לבנאי בכיתת העל שלו. אם תעבור על כלל זה המהדר ידווח על שגיאה. המהדר ידווח גם על שגיאה אם ​​הוא מזהה super()שיחה בשיטה; רק להזמין super()קונסטרוקטור.

רישום 4 מתארך עוד יותר Accountעם CheckingAccountכיתה.

רישום 4. CheckingAccountכיתת ילדים מרחיבה את Accountכיתת ההורים שלה

class CheckingAccount extends Account { CheckingAccount(long amount) { super("checking", amount); } void withdraw(long amount) { setAmount(getAmount() - amount); } }

CheckingAccountהוא קצת יותר מהותי מאשר SavingsAccountבגלל שהוא מכריז על withdraw()שיטה. שימו לב לשיחות של שיטה זו setAmount()ו getAmount(), אשר CheckingAccountיורשת מ Account. אינך יכול לגשת ישירות amountלשדה Accountמכיוון ששדה זה הוכרז private(ראה רישום 2).

סופר () והבנאי ללא ויכוח

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

דוגמה להיררכיה כיתתית

I've created an AccountDemo application class that lets you try out the Account class hierarchy. First take a look at AccountDemo's source code.

Listing 5. AccountDemo demonstrates the account class hierarchy

class AccountDemo { public static void main(String[] args) { SavingsAccount sa = new SavingsAccount(10000); System.out.println("account name: " + sa.getName()); System.out.println("initial amount: " + sa.getAmount()); sa.deposit(5000); System.out.println("new amount after deposit: " + sa.getAmount()); CheckingAccount ca = new CheckingAccount(20000); System.out.println("account name: " + ca.getName()); System.out.println("initial amount: " + ca.getAmount()); ca.deposit(6000); System.out.println("new amount after deposit: " + ca.getAmount()); ca.withdraw(3000); System.out.println("new amount after withdrawal: " + ca.getAmount()); } }

The main() method in Listing 5 first demonstrates SavingsAccount, then CheckingAccount. Assuming Account.java, SavingsAccount.java, CheckingAccount.java, and AccountDemo.java source files are in the same directory, execute either of the following commands to compile all of these source files:

javac AccountDemo.java javac *.java

Execute the following command to run the application:

java AccountDemo

You should observe the following output:

account name: savings initial amount: 10000 new amount after deposit: 15000 account name: checking initial amount: 20000 new amount after deposit: 26000 new amount after withdrawal: 23000

Method overriding (and method overloading)

A subclass can override (replace) an inherited method so that the subclass's version of the method is called instead. An overriding method must specify the same name, parameter list, and return type as the method being overridden. To demonstrate, I've declared a print() method in the Vehicle class below.

Listing 6. Declaring a print() method to be overridden

class Vehicle { private String make; private String model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } void print() { System.out.println("Make: " + make + ", Model: " + model + ", Year: " + year); } }

Next, I override print() in the Truck class.

Listing 7. Overriding print() in a Truck subclass

class Truck extends Vehicle { private double tonnage; Truck(String make, String model, int year, double tonnage) { super(make, model, year); this.tonnage = tonnage; } double getTonnage() { return tonnage; } void print() { super.print(); System.out.println("Tonnage: " + tonnage); } }

Truck's print() method has the same name, return type, and parameter list as Vehicle's print() method. Note, too, that Truck's print() method first calls Vehicle's print() method by prefixing super. to the method name. It's often a good idea to execute the superclass logic first and then execute the subclass logic.

Calling superclass methods from subclass methods

In order to call a superclass method from the overriding subclass method, prefix the method's name with the reserved word super and the member access operator. Otherwise you will end up recursively calling the subclass's overriding method. In some cases a subclass will mask non-private superclass fields by declaring same-named fields. You can use super and the member access operator to access the non-private superclass fields.

To complete this example, I've excerpted a VehicleDemo class's main() method:

Truck truck = new Truck("Ford", "F150", 2008, 0.5); System.out.println("Make = " + truck.getMake()); System.out.println("Model = " + truck.getModel()); System.out.println("Year = " + truck.getYear()); System.out.println("Tonnage = " + truck.getTonnage()); truck.print();

The final line, truck.print();, calls truck's print() method. This method first calls Vehicle's print() to output the truck's make, model, and year; then it outputs the truck's tonnage. This portion of the output is shown below:

Make: Ford, Model: F150, Year: 2008 Tonnage: 0.5

Use final to block method overriding

Occasionally you might need to declare a method that should not be overridden, for security or another reason. You can use the final keyword for this purpose. To prevent overriding, simply prefix a method header with final, as in final String getMake(). The compiler will then report an error if anyone attempts to override this method in a subclass.

Method overloading vs overriding

Suppose you replaced the print() method in Listing 7 with the one below:

void print(String owner) { System.out.print("Owner: " + owner); super.print(); }

The modified Truck class now has two print() methods: the preceding explicitly-declared method and the method inherited from Vehicle. The void print(String owner) method doesn't override Vehicle's print() method. Instead, it overloads it.

אתה יכול לזהות ניסיון להעמיס במקום לעקוף שיטה בזמן הקומפילציה על ידי קידומת כותרת השיטה של ​​מחלקה משנה עם @Overrideההערה:

@Override void print(String owner) { System.out.print("Owner: " + owner); super.print(); }

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

מתי להשתמש ב- @Override

לפתח את ההרגל של קידומת שיטות עיקריות עם @Override. הרגל זה יעזור לך לזהות טעויות עומס מוקדם הרבה יותר.