การสืบทอดใน Java ตอนที่ 1: คีย์เวิร์ดขยาย

Java สนับสนุนการใช้คลาสซ้ำผ่านการสืบทอดและองค์ประกอบ บทช่วยสอนสองส่วนนี้จะสอนวิธีใช้การสืบทอดในโปรแกรม Java ของคุณ ในส่วนที่ 1 คุณจะได้เรียนรู้วิธีใช้extendsคีย์เวิร์ดเพื่อรับคลาสย่อยจากคลาสพาเรนต์เรียกใช้คอนสตรัคเตอร์และเมธอดคลาสพาเรนต์และแทนที่เมธอด ในส่วนที่ 2 คุณจะทัวร์ชมjava.lang.Objectซึ่งเป็นซูเปอร์คลาสของ Java ที่คลาสอื่น ๆ ทุกคลาสสืบทอดมา

หากต้องการเรียนรู้เกี่ยวกับการสืบทอดให้สมบูรณ์อย่าลืมดูเคล็ดลับ Java ของฉันที่อธิบายว่าเมื่อใดควรใช้องค์ประกอบเทียบกับการสืบทอด คุณจะได้เรียนรู้ว่าเหตุใดองค์ประกอบจึงเป็นส่วนเสริมที่สำคัญในการสืบทอดและวิธีใช้เพื่อป้องกันปัญหาเกี่ยวกับการห่อหุ้มในโปรแกรม Java ของคุณ

ดาวน์โหลดรับโค้ดดาวน์โหลดซอร์สโค้ดสำหรับแอปพลิเคชันตัวอย่างในบทช่วยสอนนี้ สร้างโดย Jeff Friesen สำหรับ JavaWorld

การสืบทอด Java: สองตัวอย่าง

การสืบทอดเป็นโครงสร้างการเขียนโปรแกรมที่นักพัฒนาซอฟต์แวร์ใช้เพื่อสร้างความสัมพันธ์ระหว่างหมวดหมู่ การสืบทอดช่วยให้เราได้รับหมวดหมู่ที่เฉพาะเจาะจงมากขึ้นจากหมวดหมู่ทั่วไปมากขึ้น หมวดหมู่มากขึ้นเฉพาะเป็นชนิดของหมวดหมู่อื่น ๆ ทั่วไป ตัวอย่างเช่นบัญชีเงินฝากเป็นบัญชีประเภทหนึ่งที่คุณสามารถฝากและถอนได้ ในทำนองเดียวกันรถบรรทุกเป็นยานพาหนะชนิดหนึ่งที่ใช้สำหรับลากสิ่งของขนาดใหญ่

การสืบทอดสามารถสืบเชื้อสายมาได้หลายระดับซึ่งนำไปสู่หมวดหมู่ที่เฉพาะเจาะจงมากขึ้นเรื่อย ๆ ดังตัวอย่างรูปที่ 1 แสดงรถยนต์และรถบรรทุกที่สืบทอดมาจากยานพาหนะ สเตชั่นแวกอนที่สืบทอดมาจากรถยนต์ และรถบรรทุกขยะที่สืบทอดมาจากรถบรรทุก ลูกศรชี้จากหมวดหมู่ "ลูก" ที่เฉพาะเจาะจงมากขึ้น (ลดระดับลง) ไปยังหมวดหมู่ "หลัก" ที่เฉพาะเจาะจงน้อยกว่า (สูงกว่า)

Jeff Friesen

ตัวอย่างนี้แสดงให้เห็นถึงการสืบทอดเพียงครั้งเดียวซึ่งหมวดหมู่ย่อยจะสืบทอดสถานะและพฤติกรรมจากหมวดหมู่หลักในทันที ในทางตรงกันข้ามการสืบทอดหลายรายการทำให้หมวดหมู่ย่อยสามารถสืบทอดสถานะและพฤติกรรมจากหมวดหมู่หลักสองประเภทขึ้นไปได้ทันที ลำดับชั้นในรูปที่ 2 แสดงการสืบทอดหลายรายการ

Jeff Friesen

หมวดหมู่จะอธิบายตามชั้นเรียน Java สนับสนุนการสืบทอดเดียวผ่านส่วนขยายคลาสซึ่งคลาสหนึ่งจะสืบทอดฟิลด์และวิธีการที่สามารถเข้าถึงได้โดยตรงจากคลาสอื่นโดยการขยายคลาสนั้น อย่างไรก็ตาม 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เนื่องจาก Java ไม่รองรับการสืบทอดหลายคลาสตามคลาส

ตัวอย่างเหล่านี้ประมวลเป็นแบบความสัมพันธ์: Carเป็นผู้เชี่ยวชาญVehicleและSavingsAccountเป็นAccountผู้เชี่ยวชาญ VehicleและAccountเป็นที่รู้จักกันเป็นฐานเรียน , คลาสแม่หรือsuperclasses CarและSavingsAccountเป็นที่รู้จักกันเป็นชั้นเรียนมา , เด็กเรียนหรือsubclasses

ชั้นเรียนสุดท้าย

คุณอาจประกาศคลาสที่ไม่ควรขยาย เช่นเหตุผลด้านความปลอดภัย ใน Java เราใช้finalคีย์เวิร์ดเพื่อป้องกันไม่ให้มีการขยายคลาสบางคลาส เพียงแค่คำนำหน้าส่วนหัวที่มีระดับเช่นเดียวกับในfinal ได้รับการประกาศนี้คอมไพเลอร์จะรายงานข้อผิดพลาดหากพยายามให้คนที่จะขยายfinal class PasswordPassword

คลาสย่อยจะสืบทอดฟิลด์และวิธีการที่สามารถเข้าถึงได้จากคลาสพาเรนต์และบรรพบุรุษอื่น ๆ พวกเขาไม่เคยสืบทอดผู้สร้างอย่างไรก็ตาม แต่ชั้นเรียนย่อยจะประกาศผู้สร้างของตนเองแทน นอกจากนี้ยังสามารถประกาศสาขาและวิธีการของตนเองเพื่อแยกความแตกต่างจากพ่อแม่ พิจารณารายชื่อ 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 อธิบายคลาสบัญชีธนาคารทั่วไปที่มีชื่อและจำนวนเงินเริ่มต้นซึ่งทั้งสองตั้งค่าไว้ในตัวสร้าง นอกจากนี้ยังช่วยให้ผู้ใช้ทำการฝากเงิน (คุณสามารถถอนเงินได้โดยการฝากเงินจำนวนติดลบ แต่เราจะเพิกเฉยต่อความเป็นไปได้นี้) โปรดทราบว่าต้องตั้งชื่อบัญชีเมื่อสร้างบัญชี

แทนค่าสกุลเงิน

จำนวนเพนนี คุณอาจต้องการใช้ a doubleหรือfloatเพื่อจัดเก็บมูลค่าทางการเงิน แต่การทำเช่นนั้นอาจทำให้เกิดความไม่ถูกต้องได้ สำหรับวิธีแก้ปัญหาที่ดีกว่าให้พิจารณาBigDecimalซึ่งเป็นส่วนหนึ่งของไลบรารีคลาสมาตรฐานของ Java

รายการ 3 แสดงSavingsAccountคลาสย่อยที่ขยายAccountคลาสพาเรนต์

รายการ 3. SavingsAccountคลาสย่อยขยายAccountคลาสพาเรนต์

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

SavingsAccountระดับเป็นที่น่ารำคาญเพราะมันไม่จำเป็นต้องประกาศเขตข้อมูลเพิ่มเติมหรือวิธีการ อย่างไรก็ตามจะประกาศตัวสร้างที่เริ่มต้นฟิลด์ในAccountคลาสระดับสูง การเริ่มต้นเกิดขึ้นเมื่อตัวAccountสร้างถูกเรียกผ่านsuperคีย์เวิร์ดของ Java ตามด้วยรายการอาร์กิวเมนต์ในวงเล็บ

โทรหา super () เมื่อใดและที่ไหน

เช่นเดียวกับที่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 is a little more substantial than SavingsAccount because it declares a withdraw() method. Notice this method's calls to setAmount() and getAmount(), which CheckingAccount inherits from Account. You cannot directly access the amount field in Account because this field is declared private (see Listing 2).

super() and the no-argument constructor

If super() is not specified in a subclass constructor, and if the superclass doesn't declare a no-argument constructor, then the compiler will report an error. This is because the subclass constructor must call a no-argument superclass constructor when super() isn't present.

Class hierarchy example

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. นิสัยนี้จะช่วยให้คุณตรวจจับความผิดพลาดมากเกินไปได้เร็วขึ้น