Subclases y Herencia

Los métodos y variables que posee un objeto definen la clase a la cual pertenece. Por ejemplo, todos los objetos de la clase A poseen los métodos Set, Incx y Print y las variables x e y. En cambio los objetos de la clase Eslabon poseen el método Encadenar y las variables next y a.

Una variable de tipo Eslabon no puede contener una referencia a un objeto de la clase A.

Eslabon e= new A(); // error de tipos
Puede existir una clase B de objetos que poseen todos los métodos y todas las variables de A, pero además poseen otros métodos y/o variables que no poseen los objetos de A. En ese caso se dice que B es una subclase de A.
Los objetos de la clase B también pertenecen a la clase A.
El principio es que todo el código que se haya escrito para objetos de la clase A también funcionará con objetos de la clase B.

Una subclase se define mediante:

class B extends A
{
  // variables que B agrega a A
  int z;
  // Métodos que B agrega a A
  // Observe que B también posee x
  void Incz() { z= z+x; }
}
Se dice que la clase B hereda todas las variables y métodos de A. También se dice que B se deriva de A o que A es la clase base para B.

La jerarquía de clases permite apreciar fácilmente qué clases son subclases de otras.

Observe que todos los objetos pertenecen a la clase Object.

Consideraciones importantes al usar subclases:


El operador instanceof

Se puede consultar si un objeto pertenece a una clase mediante:
Object obj;
...
if (obj instanceof A)
  // obj pertenece a la clase A
  A a=(A)obj; // Ok, nunca hay error
Los objetos de la clase B también son instancias de la clase A:
Object obj= new B();
if (obj instanceof A) // true
  A a= (A)obj;        // Ok

El constructor en una subclase

Los constructores no se heredan:
class A
{
  ...
  A(int ix, int iy){ ... };
}

class B extends A
{
  ...
}

B b= new B(1,2); // error, ningún
                 // constructor calza
El constructor de la clase base se puede invocar con super:
class B extends A
{
  ...
  B(int ix, int iy)
  {
    super(ix, iy);
    z= 0;
  }
  B(int ix, int iy, int iz)
  {
    super(ix, iy);
    z= iz;
  }
  B(B b)
  {
    z= b.z; // x=y=?
    super(b.x, b.y); // error, super debe ser
  }                  // la primera instrucción
}
La invocación del constructor de A siempre debe ser la primera instrucción del constructor de B. El principio es que en B las componentes de la clase base (A) deben inicializarse antes que las componentes que se agregan en la clase B.


Redefinición de Métodos

Un problema que tiene la clase B que heredó de A es que el método Print sólo imprime los campos x e y:
B b= new B(1, 2, 3);
b.Print();  // 1 2   >8^(
Al declarar una clase B derivada de A, aparte de agregar campos y métodos, también se pueden redefinir métodos. Por ejemplo, para B se puede redefinir el método Print:
class B extends A
{
  ...
  void Print() // Redefinición
  { System.out.println(x+" "+y+" "+z); }
}
B b= new B(1, 2, 3);
b.Print(); // 1 2 3    8^)
El número y tipo de los parámetros del método redefinido debe coincidir exactamente con los del método original.

Observe que el método Print para la clase A no cambia:

A a= new A(1, 2);
a.Print(); // 1 2

Enlace dinámico

¿Qué método se invoca en el siguiente caso?

A a= new B(1, 2, 3);
a.Print(); // ?
El tipo estático de la variable a es la clase A en donde Print sólo imprime x e y, por lo que una posible respuesta es que se invoca el método definido en la clase A.

Sin embargo, la respuesta correcta es que se invoca el método definido para el tipo dinámico de la variable a. Es decir la clase más específica a la cual pertenece el objeto referenciado por la variable a. Esta clase es B. Por lo tanto se invoca el Print definido para la clase B y la salida será:

    1 2 3
Esta forma de enlazar el nombre de un método con el código que se ejecutará para un objeto determinado se denomina enlace dinámico, porque el método que finalmente se invocará en general sólo se conoce durante la ejecución y no durante la compilación.


Clases y Métodos Abstractos

Una clase abstracta es una clase que se introduce sólo para que se deriven nuevas clases de ella, no para que se creen objetos con su nombre. Del mismo modo, un método abstracto es un método que se introduce para que sea redefinido en una clase derivada. Por ejemplo

abstract class GraphObj
{
  int x, y; // La posición central
  GraphObj(int ix, int iy)
  { x= ix; y= iy; } // constructor
  void Move(int dx, int dy)
  { x+= dx; y+= dy; }
  abstract void Paint(Graphics g);
  // Paint es abstracto
}
Esta clase no se puede usar para crear un objeto, por lo que lo siguiente es un error:
GraphObj gf= new GraphObj(10,20);
  // error
La idea es que sólo se pueden crear objetos de clases derivadas de la clase anterior:
class Line extends GraphObj
{
  // x e y se heredan
  int ix, iy;
  GraphObj(int aix, int aiy,
           int afx, int afy)
  {
    super((aix+afx)/2, (aiy+afy)/2);
    ix= aix; iy= aiy;
  }
  void Paint(Graphics g)
  { g.DrawLine(xi,yi,x+(x-xi),y+(y-yi)); }
  // Move se hereda de GraphObj
}

// Ahora sí!
Line line= new Line(0,0, 10,20);
El principio es que se use varias veces la clase abstracta para definir varias otras clases que poseen un conjunto común de métodos: Paint y Move.
// Una caja
class Box extends GraphObj
{
  int height, width;
  Box(int lx, int ly, int hx, int hy)
  {
    super( ... ); // Ejercicio
    ...
  }
  void Paint(Graphics g)
  {
    ... // Ejercicio
  }
}

Redefinición parcial de métodos

Supongamos que ahora se desea introducir una caja con color. Este objeto gráfico es similar a un caja sola. Por lo tanto derivamos la caja con color a partir de una caja simple. Conservamos casi todo, pero tenemos que redefinir Paint. Aún así podemos reutilizar el Paint de Box:

class ColorBox extends Box
{
  int color;
  // El mismo constructor
  ColorBox(int lx, int ly,
           int hx, int hy,
           int acolor)
  {
    super(lx, hx, ly, hy);
    color= acolor;
  }
  void Paint() // Redefinición
  {
    int savecolor= g.currColor();
    g.setColor(color);
    super.Paint(); // el Paint de Box
    g.setColor(savecolor);
  }
}
Al redefinir un método, se puede invocar el método de la clase base usando super con sus respectivos argumentos.


Clases, Métodos y Campos finales

Una clase final es una clase que no se puede derivar. Es decir no posee subclases.
final class B extends A
{
  ...
}
class C extends B // error B es final
{
  ...
}
Un método final es un método que no se puede redefinir en una subclase de la clase en donde se definió. Por ejemplo el método Move asociado a un objeto gráfico se puede declarar final en la clase GraphObj mediante:
final void Move(int dx, int dy)
{ x+= dx; y+= dy; }
De esta forma este método no podrá ser redefinido posteriormente en las clases Line, Box o ColorBox.

Una campo final es una variable a la que no se puede asignar un valor. La variable se inicializa con un valor durante su declaración, pero luego no puede cambiar. Cumple el papel de las constante de otros lenguajes, pero observe que en Java se trata de constantes dinámica cuyo valor se calcula en ejecución.


Variables y métodos de la clase

Los campos que se definen en una clase se denominan variables de instancia porque se encuentran en los objetos. En Java también se pueden definir variables de la clase. Este tipo de variables se encuentra en la clase y no en los objetos. Las variables de clase se definen usando el atributo static:
class A
{
  int iv;
  static int cv;

  void Inc()
  {
    iv++; cv++;
    System.out.println(iv+" "+cv);
  }

  // Constructor para los objetos
  A() { iv=0; }
  // Inicializador de la clase
  static { cv=0; }
}

A a1= new A();
A a2= new A();
a1.Inc(); // 1 1
a2.Inc(); // 1 2
a1.Inc(); // 2 3
a1.Inc y a2.Inc incrementan las misma variable cv.

Un método static de la clase es un método que sólo accesa variables de la clase. Se definen usando el atributo static:

  static void Inc2() { cv++; }
  static void Inc3()
  { iv++; } // error iv es de un objeto
  static void Inc4()
  {
    Inc();  // error Inc necesita un obj.
    Inc2(); // Ok, porque Inc2 es static
  }