Herencia en Java, Parte 1: La palabra clave extiende

Java admite la reutilización de clases a través de la herencia y la composición. Este tutorial de dos partes le enseña cómo usar la herencia en sus programas Java. En la Parte 1, aprenderá cómo usar la extendspalabra clave para derivar una clase secundaria de una clase principal, invocar constructores y métodos de clase principal y anular métodos. En la Parte 2, hará un recorrido java.lang.Object, que es la superclase de Java de la que heredan todas las demás clases.

Para completar su aprendizaje sobre la herencia, asegúrese de consultar mi consejo de Java que explica cuándo usar la composición frente a la herencia. Aprenderá por qué la composición es un complemento importante de la herencia y cómo usarla para protegerse contra problemas de encapsulación en sus programas Java.

descargar Obtener el código Descargar el código fuente, por ejemplo, las aplicaciones de este tutorial. Creado por Jeff Friesen para JavaWorld.

Herencia de Java: dos ejemplos

La herencia es una construcción de programación que los desarrolladores de software utilizan para establecer relaciones is-a entre categorías. La herencia nos permite derivar categorías más específicas de otras más genéricas. La categoría más específica es una especie de categoría más genérica. Por ejemplo, una cuenta corriente es un tipo de cuenta en la que puede realizar depósitos y retiros. Del mismo modo, un camión es un tipo de vehículo que se utiliza para transportar artículos grandes.

La herencia puede descender a través de múltiples niveles, dando lugar a categorías cada vez más específicas. Como ejemplo, la Figura 1 muestra un automóvil y un camión heredando del vehículo; camioneta heredada del automóvil; y camión de basura heredado de camión. Las flechas apuntan desde categorías "secundarias" más específicas (más abajo) a categorías "principales" menos específicas (más arriba).

Jeff Friesen

Este ejemplo ilustra la herencia única en la que una categoría secundaria hereda el estado y los comportamientos de una categoría principal inmediata. Por el contrario, la herencia múltiple permite que una categoría secundaria herede el estado y los comportamientos de dos o más categorías principales inmediatas. La jerarquía de la Figura 2 ilustra la herencia múltiple.

Jeff Friesen

Las categorías se describen por clases. Java admite la herencia única a través de la extensión de clase , en la que una clase hereda directamente los campos y métodos accesibles de otra clase al extender esa clase. Sin embargo, Java no admite la herencia múltiple mediante la extensión de clases.

Al ver una jerarquía de herencia, puede detectar fácilmente una herencia múltiple mediante la presencia de un patrón de diamante. La Figura 2 muestra este patrón en el contexto de vehículo, vehículo terrestre, vehículo acuático y aerodeslizador.

La palabra clave extiende

Java admite la extensión de clase a través de la extendspalabra clave. Cuando está presente, extendsespecifica una relación padre-hijo entre dos clases. A continuación utilizo extendspara establecer una relación entre clases Vehicley Car, y luego entre Accounty SavingsAccount:

Listado 1. La extendspalabra clave especifica una relación padre-hijo

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 }

La extendspalabra clave se especifica después del nombre de la clase y antes de otro nombre de clase. El nombre de la clase anterior extendsidentifica al niño y el nombre de la clase posterior extendsidentifica al padre. Es imposible especificar varios nombres de clases después extendsporque Java no admite la herencia múltiple basada en clases.

Estos ejemplos codifican relaciones is-a: Cares un especializado Vehicley SavingsAccountes un especializado Account. Vehicley Accountse conocen como clases base , clases padre o superclases . Cary SavingsAccountse conocen como clases derivadas , clases secundarias o subclases .

Clases finales

Puede declarar una clase que no debería ampliarse; por ejemplo, por razones de seguridad. En Java, usamos la finalpalabra clave para evitar que se extiendan algunas clases. Simplemente prefija un encabezado de clase con final, como en final class Password. Dada esta declaración, el compilador informará un error si alguien intenta extender Password.

Las clases secundarias heredan los campos y métodos accesibles de sus clases principales y de otros ancestros. Sin embargo, nunca heredan constructores. En cambio, las clases secundarias declaran sus propios constructores. Además, pueden declarar sus propios campos y métodos para diferenciarlos de sus padres. Considere el Listado 2.

Listado 2. Una Accountclase principal

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

El Listado 2 describe una clase de cuenta bancaria genérica que tiene un nombre y una cantidad inicial, que se establecen en el constructor. Además, permite a los usuarios realizar depósitos. (Puede hacer retiros depositando cantidades negativas de dinero, pero ignoraremos esta posibilidad). Tenga en cuenta que el nombre de la cuenta debe establecerse cuando se crea una cuenta.

Representando valores monetarios

cuenta de centavos. Es posible que prefiera usar a doubleo a floatpara almacenar valores monetarios, pero hacerlo puede generar inexactitudes. Para una mejor solución, considere BigDecimal, que es parte de la biblioteca de clases estándar de Java.

El Listado 3 presenta una SavingsAccountclase secundaria que amplía su Accountclase principal.

Listado 3. Una SavingsAccountclase secundaria extiende su Accountclase principal

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

La SavingsAccountclase es trivial porque no necesita declarar campos o métodos adicionales. Sin embargo, declara un constructor que inicializa los campos en su Accountsuperclase. La inicialización ocurre cuando Accountse llama al constructor de mediante la superpalabra clave de Java , seguido de una lista de argumentos entre paréntesis.

Cuándo y dónde llamar a super ()

Así como this()debe ser el primer elemento en un constructor que llama a otro constructor en la misma clase, super()debe ser el primer elemento en un constructor que llama a un constructor en su superclase. Si rompe esta regla, el compilador informará un error. El compilador también informará de un error si detecta una super()llamada en un método; solo llame super()a un constructor.

El Listado 4 se extiende aún más Accountcon una CheckingAccountclase.

Listado 4. Una CheckingAccountclase secundaria extiende su Accountclase principal

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.

Puede detectar un intento de sobrecarga en lugar de anular un método en el momento de la compilación al anteponer el encabezado del método de una subclase con la @Overrideanotación:

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

Especificar @Overridele dice al compilador que el método dado anula otro método. Si alguien intentara sobrecargar el método, el compilador reportaría un error. Sin esta anotación, el compilador no informaría de un error porque la sobrecarga de métodos es legal.

Cuándo usar @Override

Desarrolle el hábito de anteponer métodos primarios con @Override. Este hábito le ayudará a detectar errores de sobrecarga mucho antes.