Dziedziczenie w Javie, część 1: słowo kluczowe extends

Java obsługuje ponowne użycie klas poprzez dziedziczenie i kompozycję. Ten dwuczęściowy samouczek uczy, jak korzystać z dziedziczenia w programach Java. W części 1 dowiesz się, jak używać extendssłowa kluczowego do wyprowadzenia klasy potomnej z klasy nadrzędnej, wywoływać konstruktory i metody klasy nadrzędnej oraz przesłonić metody. W części 2 omówimy java.lang.Object, która jest nadklasą Javy, po której dziedziczy wszystkie inne klasy.

Aby dokończyć naukę o dziedziczeniu, zapoznaj się z moją wskazówką dotyczącą języka Java, wyjaśniającą, kiedy używać kompozycji, a kiedy dziedziczenia. Dowiesz się, dlaczego kompozycja jest ważnym uzupełnieniem dziedziczenia i jak jej używać, aby chronić się przed problemami z hermetyzacją w programach Java.

pobierz Pobierz kod Pobierz kod źródłowy na przykład aplikacje w tym samouczku. Stworzone przez Jeffa Friesena dla JavaWorld.

Dziedziczenie Java: dwa przykłady

Dziedziczenie to konstrukcja programistyczna, której programiści używają do ustanawiania relacji typu is-a między kategoriami. Dziedziczenie pozwala nam wyprowadzić bardziej szczegółowe kategorie z bardziej ogólnych. Kategoria bardziej szczegółowa to rodzaj kategorii bardziej ogólnej. Na przykład konto czekowe to rodzaj konta, na którym można dokonywać wpłat i wypłat. Podobnie ciężarówka to rodzaj pojazdu używanego do przewożenia dużych przedmiotów.

Dziedziczenie może przechodzić przez wiele poziomów, prowadząc do coraz bardziej szczegółowych kategorii. Jako przykład, Rysunek 1 przedstawia dziedziczenie samochodu i ciężarówki po pojeździe; kombi dziedziczenie po samochodzie; i śmieciarka dziedzicząca po ciężarówce. Strzałki wskazują od bardziej szczegółowych kategorii „podrzędnych” (niżej) do mniej szczegółowych kategorii „nadrzędnych” (wyżej).

Jeff Friesen

Ten przykład ilustruje pojedyncze dziedziczenie, w którym kategoria podrzędna dziedziczy stan i zachowania z jednej bezpośredniej kategorii nadrzędnej. Z kolei dziedziczenie wielokrotne umożliwia kategorii podrzędnej dziedziczenie stanu i zachowań z dwóch lub więcej bezpośrednich kategorii nadrzędnych. Hierarchia na rysunku 2 przedstawia dziedziczenie wielokrotne.

Jeff Friesen

Kategorie są opisane klasami. Java obsługuje pojedyncze dziedziczenie poprzez rozszerzenie klasy , w którym jedna klasa bezpośrednio dziedziczy dostępne pola i metody z innej klasy, rozszerzając tę ​​klasę. Jednak Java nie obsługuje wielokrotnego dziedziczenia poprzez rozszerzenie klasy.

Przeglądając hierarchię dziedziczenia, można łatwo wykryć wielokrotne dziedziczenie dzięki obecności wzoru rombu. Rysunek 2 pokazuje ten wzór w kontekście pojazdu, pojazdu lądowego, pojazdu wodnego i poduszkowca.

Słowo kluczowe extends

Java obsługuje rozszerzenie klasy za pomocą extendssłowa kluczowego. Jeśli występuje, extendsokreśla relację rodzic-dziecko między dwiema klasami. Poniżej używam extendsdo ustalenia relacji między klasami Vehiclea Car, a następnie między Accounta SavingsAccount:

Listing 1. Słowo extendskluczowe określa relację rodzic-dziecko

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 }

Słowo extendskluczowe jest określone po nazwie klasy i przed inną nazwą klasy. Nazwa klasy przed extendsidentyfikuje dziecko, a nazwa klasy po extendsokreśla rodzica. Nie można określić wielu nazw klas po, extendsponieważ Java nie obsługuje wielokrotnego dziedziczenia opartego na klasach.

Te przykłady kodyfikacji is-a relacje: Carjest wyspecjalizowane Vehiclei SavingsAccountjest wyspecjalizowane Account. Vehiclei Accountsą znane jako bazowe klas , klasy macierzystych lub nadrzędnych . Cari SavingsAccountsą znane jako klasach pochodnych , klas potomnych lub podklasy .

Zajęcia końcowe

Możesz zadeklarować klasę, która nie powinna być rozszerzona; na przykład ze względów bezpieczeństwa. W Javie używamy finalsłowa kluczowego, aby zapobiec rozszerzaniu niektórych klas. Po prostu poprzedz nagłówek klasy znakiem final, jak w final class Password. Biorąc pod uwagę tę deklarację, kompilator zgłosi błąd, jeśli ktoś spróbuje rozszerzyć Password.

Klasy podrzędne dziedziczą dostępne pola i metody ze swoich klas nadrzędnych i innych elementów nadrzędnych. Jednak nigdy nie dziedziczą konstruktorów. Zamiast tego klasy potomne deklarują własne konstruktory. Ponadto mogą zadeklarować własne pola i metody, aby odróżnić je od rodziców. Rozważ listę 2.

Listing 2. AccountKlasa nadrzędna

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; } }

Listing 2 opisuje ogólną klasę konta bankowego, która ma nazwę i początkową kwotę, które są ustawione w konstruktorze. Pozwala także użytkownikom dokonywać wpłat. (Możesz dokonywać wypłat, wpłacając ujemne kwoty pieniędzy, ale zignorujemy tę możliwość.) Pamiętaj, że nazwa konta musi być ustawiona podczas tworzenia konta.

Reprezentujące wartości walutowe

count of pennies. You might prefer to use a double or a float to store monetary values, but doing that can lead to inaccuracies. For a better solution, consider BigDecimal, which is part of Java's standard class library.

Listing 3 presents a SavingsAccount child class that extends its Account parent class.

Listing 3. A SavingsAccount child class extends its Account parent class

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

The SavingsAccount class is trivial because it doesn't need to declare additional fields or methods. It does, however, declare a constructor that initializes the fields in its Account superclass. Initialization happens when Account's constructor is called via Java's super keyword, followed by a parenthesized argument list.

When and where to call super()

Just as this() must be the first element in a constructor that calls another constructor in the same class, super() must be the first element in a constructor that calls a constructor in its superclass. If you break this rule the compiler will report an error. The compiler will also report an error if it detects a super() call in a method; only ever call super() in a constructor.

Listing 4 further extends Account with a CheckingAccount class.

Listing 4. A CheckingAccount child class extends its Account parent class

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.

Możesz wykryć próbę przeciążenia zamiast przesłonięcia metody w czasie kompilacji, poprzedzając nagłówek metody podklasy @Overrideadnotacją:

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

Określenie @Overrideinformuje kompilator, że dana metoda zastępuje inną metodę. Gdyby zamiast tego ktoś próbował przeciążać metodę, kompilator zgłosiłby błąd. Bez tej adnotacji kompilator nie zgłosiłby błędu, ponieważ przeciążanie metody jest legalne.

Kiedy używać @Override

Wyrób nawyk dodawania przedrostków nadpisywania metod rozszerzeniem @Override. Ten nawyk pomoże Ci znacznie szybciej wykryć błędy związane z przeciążeniem.