Наследование в Java, часть 1: ключевое слово extends

Java поддерживает повторное использование классов посредством наследования и композиции. Это руководство, состоящее из двух частей, научит вас использовать наследование в ваших программах на Java. В части 1 вы узнаете, как использовать extendsключевое слово для создания дочернего класса из родительского класса, вызывать конструкторы и методы родительского класса и переопределять методы. Во второй части вы java.lang.Objectпознакомитесь с суперклассом Java, от которого наследуются все остальные классы.

Чтобы завершить изучение наследования, обязательно ознакомьтесь с моим советом по Java, объясняющим, когда использовать композицию или наследование. Вы узнаете, почему композиция является важным дополнением к наследованию и как ее использовать для защиты от проблем с инкапсуляцией в ваших программах Java.

загрузить Получить код Загрузите исходный код для примеров приложений из этого руководства. Создано Джеффом Фризеном для JavaWorld.

Наследование Java: два примера

Наследование - это программная конструкция, которую разработчики программного обеспечения используют для установления отношений между категориями. Наследование позволяет нам выводить более конкретные категории из более общих. Более конкретная категория - это своего рода более общая категория. Например, текущий счет - это своего рода счет, на котором вы можете делать депозиты и снимать средства. Точно так же грузовик - это вид транспортного средства, используемого для перевозки крупных предметов.

Наследование может проходить через несколько уровней, что приводит к появлению все более конкретных категорий. В качестве примера на рисунке 1 показаны легковые и грузовые автомобили, унаследованные от транспортного средства; универсал, унаследованный от автомобиля; и мусоровоз, унаследованный от грузовика. Стрелки указывают от более конкретных «дочерних» категорий (ниже) к менее конкретным «родительским» категориям (выше).

Джефф Фризен

Этот пример иллюстрирует одиночное наследование, при котором дочерняя категория наследует состояние и поведение от одной непосредственной родительской категории. Напротив, множественное наследование позволяет дочерней категории наследовать состояние и поведение от двух или более непосредственных родительских категорий. Иерархия на рисунке 2 иллюстрирует множественное наследование.

Джефф Фризен

Категории описываются классами. Java поддерживает одиночное наследование через расширение класса , при котором один класс напрямую наследует доступные поля и методы другого класса, расширяя этот класс. Однако Java не поддерживает множественное наследование через расширение класса.

При просмотре иерархии наследования вы можете легко обнаружить множественное наследование по наличию ромбовидного узора. На рисунке 2 показан этот образец в контексте транспортного средства, наземного транспортного средства, водного транспортного средства и судна на воздушной подушке.

Ключевое слово extends

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известны как базовые классы , родительские классы или суперклассы . Carи SavingsAccountизвестны как производные классы , дочерние классы или подклассы .

Заключительные занятия

Вы можете объявить класс, который не следует расширять; например, по соображениям безопасности. В Java мы используем 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 описан общий класс банковского счета, у которого есть имя и начальная сумма, которые задаются в конструкторе. Кроме того, он позволяет пользователям делать депозиты. (Вы можете снимать средства, внося отрицательные суммы денег, но мы проигнорируем эту возможность.) Обратите внимание, что имя учетной записи должно быть установлено при создании учетной записи.

Представление стоимости валюты

счет копейки. Вы можете предпочесть использовать a 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 , за которым следует список аргументов в скобках.

Когда и куда звонить 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. Эта привычка поможет вам гораздо быстрее обнаруживать ошибки, связанные с перегрузкой.