Programación

1º DAM/DAW - Curso 2024-2025

User Tools

Site Tools


apuntes:objetos

This is an old revision of the document!


Orientación a Objetos

El lenguaje Java es lo que se conoce como un Lenguaje Orientado a Objetos y, por lo tanto, sigue dicho paradigma. Este paradigma se basa en la idea de que cualquier programa está formado por objetos y que todo puede ser representado como tal. Así, cualquier elemento que forme parte de una aplicación (un usuario, una factura o pedido en un ERP, un coche para una aplicación de gestión de un taller, . . .) se considera que es un objeto de la aplicación con una serie de propiedades y características.

Esta idea de objeto viene acompañada por la definición de su estructura, que es lo que llamamos clase. Basicamente una clase es una estructura de código que define los atributos y características de todos los objetos que pertenecen a un mismo tipo.

Figure 1: Clase / Objeto (fuente:wikibooks.org)

A partir del concepto de objeto, el paradigma propone una serie características que lo definen y que se irán comprendiendo a lo largo de este tema, según se vayan exponiendo los diferentes mecanismos de este paradigma para proporcionarlas:

  • Abstracción: Por el que el programador se \emph{abstrae}, se despreocupa, de los detalles de implementación de cualquier objeto. Los procesos o métodos que se encuentren definidos funcionan por sí solos y no es necesario saber cómo están implementados si sólo necesitamos hacer que se ejecuten.
  • Encapsulamiento: Todas las características o propiedades que pertenezcan a un sólo elemento del programa se pueden crear y encapsular dentro de él, aumentando la cohesión de estos componentes.
  • Polimorfismo: Más adelante veremos cómo diferentes objetos de distintos tipos pueden compartir el mismo nombre de variable, lo que ayudará a la reutilización de código en gran medida.
  • Herencia: La herencia entre clases permitirá que

Creación de una clase

Una clase es una estructura de código que define las características y operaciones que tiene y puede realizar todos los objetos que se creen a partir de ella.

Figure 2: Estructura de una clase (fuente: wikibooks.org
)

La sintaxis para la creación de una clase en Java es la siguiente:

[<modificador_visibilidad>] [<tipo>] class <NombreClase> {
    [<atributos>]
 
    [<constructores>]
 
    [<getters/setters>]
 
    [<métodos>]
}

Donde:

  • <modificador_visibilidad> Se indicará (o no) la visibilidad de la clase con respecto al resto del proyecto u otros proyectos que puedan utilizarla como librería. Por lo general se indicará una visibilidad completa a través de la palabra reserva public.
  • <tipo> Se indicará (o no) el tipo de clase. Por ahora no indicaremos nada y más adelante veremos que opciones podemos especificar aquí para crear diferentes tipos de clases y sus diferentes comportamientos.
  • <NombreClase> El nombre de la clase siguiendo la misma estructura de poner en mayúscula la primera letra de cada palabra que lo formen
  • <atributos> Esta zona se dedica generalmente a la especificación de los atributos (lo veremos más adelante)
  • <constructores> Esta zona se dedica generalmente a la especificación de los constructores (lo veremos más adelante)
  • <getters/setters> Esta zona se dedica generalmente a la especificación de los getters y setteres (lo veremos más adelante)
  • <metodos> Esta zona se dedica generalmente a la especificación de los métodos (lo veremos más adelante)
public class Moto {
 
}

Hay que tener en cuenta que Java exija que al menos exista una clase pública por fichero de código y ésta deberá llamarse como dicho fichero (sin tener en cuenta la extensión del mismo).

Creación de atributos

Los atributos definen las características que tendrán todos los objetos de una misma clase. Son variable declaradas como parte de la clase siguiendo la misma sintaxis que las variables Java, con la diferencia de que es posible indicar un modificador de accesibilidad que definirá a que nivel del proyecto estará accesible dicho atributo:

[<modificador>] <tipo> <nombre>

Hay 4 niveles de accesibilidad

  • No indicar ninguna visibilidad hace que el atributo esté accesible dentro del paquete donde sea definida su clase
  • El modificador public hace que el atributo sea visible para todo el proyecto
  • El modificador protected hace que el atributo sea visible para la propia clase y las que hereden de ésta
  • El modificador private hace qu el atributo sea visible sólo para la propia clase (será el modificador utilizado por defecto)

Los atributos pueden definirse utilizando los tipos primitivos, los tipos clase que Java proporciona o incluso utilizando como tipo cualquier clase definida en el proyecto.

public class Moto {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private LocalDate ultimaRevision;
}

Creación de métodos

Los métodos permiten definir las operaciones que los objetos de una clase pueden ejecutar. Estos métodos podrán ser invocados desde otros métodos o desde el propio método main del proyecto.

[<modificador>] <tipo_retorno> <nombre>([<tipo_parametro> <nombre_parametro>, . . .]) {
 
    [return <valor_retorno>]
}

Donde <tipo_retorno> puede ser cualquier tipo de dato de Java, cualquier clase creada en nuestro proyecto bien la palabra reservada void para indicar que el método no devuelve ningún valor (no tendrá que incluir entonces una instrucción return).

Se pueden crear tantos métodos como sea necesario y en el orden que uno quiera ya que éste no influye en ningún caso. Dentro de una clase métodos definidos antes que otros (más arriba) pueden invocar a métodos definidos después (más abajo).

public class Moto {
    . . .
 
    public void viajar(String origen, String destino) {
        // Calcular la distancia entre origen y destino
        int distancia = . . .
 
        kilometros += distancia;
    }
 
    public void pasarITV() {
        . . .
        ultimaRevision = LocalDate.now();
    }
}

this

El paradigma orientado a objetos permite que coexistan variables locales y atributos con el mismo nombre. Por ello, es posible que a la hora de utilizar una variable local o atributo se dé un conflicto por tener el mismo nombre. Para resolver esa situación existe la palabra reserva this que permite que el programador se refiera explicitamente al atributo de la clase.

Así, si tenemos una variable local kilometros y un atributo también llamado kilometros y queremos referirnos a la segunda, tendremos que referirnos a ella como this.kilometros. En caso de que no haya posibilidad de conflicto en el ámbito en que nos encontremos, no será necesario utilizar this puesto que el compilador no tendrá ningún conflicto a la hora de seleccionar la variable o atributo a la que nos referimos.

Eso si, si en caso de existir conflicto no utilizamos la palabra \verb this para referirnos explicitamente al atributo de la clase, el compilador seleccionará siempre la variable más cercana, que siempre será la local.

public class Moto {
    int kilometros;
 
    public void recorrerDistancia(int kilometros) {
        // Se suma al atributo el valor del parámetro local del método
        this.kilometros += kilometros;
        // No se especifica nada, Java selecciona el parámetro local
        System.out.println("Has recorrido " + kilometros + " Kms");
    }
 
    . . .
}

Constructores

Los constructores permiten inicializar los atributos de un objeto en el momento en que se crea en la aplicación.

Básicamente se trata de un método (sin opción a valor de retorno) que acepta como parámetros una serie de valores que podrán ser tomados en cuenta para la inicialización del objeto en el momento en que se crea a través de la orden new .

public class Moto {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private LocalDate ultimaRevision;
 
    public Moto(String matricula, String marca, String modelo,
                float potencia, int kilometros) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = kilometros;
        this.ultimaRevision = LocalDate.now();
    }
 
    public void viajar(String origen, String destino) {
        // Calcular la distancia entre origen y destino
        int distancia = . . .
 
        kilometros += distancia;
    }
 
    public void pasarITV() {
        . . .
        ultimaRevision = LocalDate.now();
    }
}

Hay que tener en cuenta que, en caso de que el programador no especifique ningún constructor para una clase determinada, Java siempre definirá, implicitamente, lo que se conoce como constructor vacío, que será un constructor sin parámetros que reserva memoria para dicho objeto e inicializa todos sus atributos a sus valores por defecto. De esa manera, un objeto siempre podrá ser instanciado utilizando la orden new y dicho constructor vacío incluso aunque el programador no lo haya implementado de forma explícita.

Utilización de una clase

Los objetos de una clase se crean a través de la instrucción new de la forma que se indica en el siguiente ejemplo. En la fase de creación de un objeto hay que distinguir entre la declaración del objeto (declarar la variable y su tipo) y el momento en que se instancia dicho objeto (cuando se utiliza la orden new ).

Una vez se ha credo el objeto, podremos acceder a sus atributos y métodos (siempre y cuando sean visibles) utilizando el caracter . como separador entre el objeto y el atributo o método que queremos usar.

public class Programa {
 
    public static void main(String args[]) {
        // Se declara e instancia en la misma línea
        Moto moto = new Moto("1234ABC", "Suzuki", "GSF 1250", 98.5);
        . . .
        moto.viajar("Zaragoza", "Madrid");
        moto.pasarITV();
 
        Moto otraMoto;                  // Declaración del objeto
        otraMoto = new Moto(. . .);     // Instanciación del objeto
    }
}

Cómo trabajar con objetos

Cómo se almacena un objeto en memoria

Figure 3: Variable primitiva / Objeto / Referencia a null
// Variable tipo primitivo
int x = 10;
// Objeto declarado (e instanciado)
Vehiculo vehiculo = new Vehiculo();
// Objeto declarado (referencia a null)
Vehiculo vehiculo;

Paso por valor / referencia

public void cambiar(int x) {
  x = 10; 
}
int y = 20; 
cambiar(y); 
¿¿y??
public void cambiar(Vehiculo vehiculo) {
  vehiculo.marca = 'Ferrari'; 
}
 
Vehiculo vehiculo = new Vehiculo('Opel', 'Corsa'); 
cambiar(vehiculo);
¿¿vehiculo.marca??

Referencias a null / NullPointerException

Es uno de los fallos más habituales.

Cuando una variable no está haciendo referencia a ningún objeto no puede accederse a ningún atributo o método de la misma puesto que no está haciendo referencia a ninguna zona de memoria.

Vehiculo vehiculo;
vehiculo.pintar('rojo'); // NullPointerException
Vehiculo vehiculo = new Vehiculo();
...
vehiculo = null;
vehiculo.pintar('verde'); 
// NullPointerException

Sobrecarga de métodos

El paradigma orientado a objetos permite que coexistan varios métodos con el mismo nombre siempre y cuando se diferencien en el número y tipo de parámetros. La sobrecarga es válida también para constructores, por lo que podremos definir tantos constructores como formas diferentes de inicializar un objeto necesitemos.

public class Moto {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private LocalDate ultimaRevision;
 
    public Moto(String matricula, String marca, String modelo,
                float potencia, int kilometros) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = kilometros;
        this.ultimaRevision = LocalDate.now();
    }
 
    public Moto(String matricula, String marca, String modelo, float potencia) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = 0;
        this.ultimaRevision = LocalDate.now();
    }
 
    public void viajar(String origen, String destino) {
        // Calcular la distancia entre origen y destino
        int distancia = . . .
 
        kilometros += distancia;
    }
 
    public void viajar(int distancia) {
        kilometros += distancia;
    }
 
    public void pasarITV() {
        . . .
        ultimaRevision = LocalDate.now();
    }
}

En el siguiente ejemplo podemos ver un ejemplo donde invocamos al mismo método con diferente paso de parámetros. Será Java quién decida a que método invocar realmente en función del número y tipo de éstos.

public class Programa {
    public static void main(String args[]) {
        Moto moto = new Moto("1234ABC", "Suzuki", "GSF 1250", 98.5);
        . . .
        moto.viajar("Zaragoza", "Madrid");
        moto.viajar(100);
    }
}

Los métodos accesores (getters y setters)

Puesto que lo más habitual es hacer que los atributos permanezcan lo más ocultos posible, se hace necesario de algún mecanismo que permita mostrarlos fuera de la implementación de la clase en el caso de que quieran ser leído o escritos desde nuestro proyecto. Para eso existen lo que se conoce como setters y getters. Los primeros permiten acceder a los atributos de una clase para modificarlos, mientras que los segundos permiten acceder a los mismos para leerlos.

Los getters y setters en la práctica no son más que métodos que siguen una notación determinada (getAtributo() para los getters y setAtributo(valor) para los setters que permiten modificar o acceder a los atributos de una forma controlada. Siempre será posible permitir que se pueda acceder a un atributo (getter) pero no para modificarlo.

Por normal general estos métodos se utilizan para fijar o acceder al valor directamente pero podrían ser utilizados para realizar alguna otra tarea adicional si fuera necesario.

public class Moto {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private LocalDate ultimaRevision;
 
    public Moto(String matricula, String marca, String modelo,
                float potencia, int kilometros) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = kilometros;
        this.ultimaRevision = LocalDate.now();
    }
 
    public Moto(String matricula, String marca, String modelo, float potencia) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = 0;
        this.ultimaRevision = LocalDate.now();
    }
 
    public String getMatricula() { return matricula; }
    public void setMatricula(String matricula) { this.matricula = matricula; }
 
    public String getMarca() { return marca; }
    public void setMarca(String marca) { this.marca = marca; }
 
    public String getModelo() { return modelo; }
    public void setModelo(String modelo) { this.modelo = modelo; }
 
    public float getPotencia() { return potencia; }
    public void setPotencia(float potencia) { this.potencia = potencia; }
 
    public int getKilometros() { return kilometros; }
    public void setKilometros(int kilometros) { this.kilometros = kilometros; }
 
    public LocalDate getUltimaRevision() { return ultimaRevision; }
    public void setUltimaRevision(LocalDate ultimaRevision) { 
        this.ultimaRevision = ultimaRevision; 
    }
 
    // Resto de métodos
    . . .
}

Campos y métodos estáticos

Los atributos y métodos estáticos que se definen en una clase como tal permite que se puedan utilizar sin la necesidad de instanciar objetos de dicha clase. Por ello, nunca podrán acceder a atributos o invocar a métodos de la clase que no lo sean.

public class Util {
 
    public static float RELACION_MILLAS_KMS = 1.609;
 
    public int convertirAMillas(int kilometros) {
        return (int) kilometros / RELACION_MILLAS_KMS;
    }
 
    public int convertirAKilometros(int millas) {
        return (int) millas * RELACION_MILLAS_KMS;
    }
}

En el siguiente ejemplo se muestran dos atributos estáticos para la clase Moto.

public class Moto {
 
    public static String combustible = "gasolina";
    public static int numeroRuedas = 2;
 
    . . .
}

El acceso a atributos y métodos estáticos se hace directamente desde la clase, haya o no objetos declarados de la misma.

public class Programa {
    public static void main(String args[]) {
 
        int kilometros = 100;
        int millas = Util.convertirAMillas(kilometros);
 
        System.out.println(kilometros + " kilómetros son " + millas + " millas");
        System.out.println("Las motos usan como combustible " + Moto.combustible);
    }
}

La herencia

La herencia es otra de las características más importantes del paradigma de orientación a objetos. Permite que una clase herede o reutilice todo o parte del código escrito en otra clase, simplemente extendiendo de ella.

El siguiente ejemplo muestra el código que habría que escribir para hacer una aplicación donde se pudiera trabajar con objetos Moto y Coche suponiendo que no existiera el concepto de herencia.

public class Moto {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private boolean carenado;
    private LocalDate ultimaRevision;
 
    public Moto(String matricula, String marca, String modelo,
                float potencia, int kilometros, boolean carenado) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = kilometros;
        this.carenado = carenado;
        this.ultimaRevision = LocalDate.now();
    }
 
    public Moto(String matricula, String marca, String modelo, float potencia,
                boolean carenado) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = 0;
        this.carenado = true;
        this.ultimaRevision = LocalDate.now();
    }
 
    public String getMatricula() { return matricula; }
    public void setMatricula(String matricula) { this.matricula = matricula; }
 
    public String getMarca() { return marca; }
    public void setMarca(String marca) { this.marca = marca; }
 
    public String getModelo() { return modelo; }
    public void setModelo(String modelo) { this.modelo = modelo; }
 
    public float getPotencia() { return potencia; }
    public void setPotencia(float potencia) { this.potencia = potencia; }
 
    public int getKilometros() { return kilometros; }
    public void setKilometros(int kilometros) { this.kilometros = kilometros; }
 
    public boolean getCarenado() { return carenado; }
    public void setCarenado(boolean carenado) { this.carenado = carenado; }
 
    public LocalDate getUltimaRevision() { return ultimaRevision; }
    public void setUltimaRevision(LocalDate ultimaRevision) { 
        this.ultimaRevision = ultimaRevision; 
    }
 
    // Resto de métodos
    . . .
}
public class Coche {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private int numeroPuertas;
    private int capacidadMaletero;
    private LocalDate ultimaRevision;
 
    public Coche(String matricula, String marca, String modelo,
                float potencia, int kilometros, int numeroPuertas,
                int capacidadMaletero) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = kilometros;
        this.numeroPuertas = numeroPuertas;
        this.capacidadMaletero = capacidadMaletero;
        this.ultimaRevision = LocalDate.now();
    }
 
    public String getMatricula() { return matricula; }
    public void setMatricula(String matricula) { this.matricula = matricula; }
 
    public String getMarca() { return marca; }
    public void setMarca(String marca) { this.marca = marca; }
 
    public String getModelo() { return modelo; }
    public void setModelo(String modelo) { this.modelo = modelo; }
 
    public float getPotencia() { return potencia; }
    public void setPotencia(float potencia) { this.potencia = potencia; }
 
    public int getKilometros() { return kilometros; }
    public void setKilometros(int kilometros) { this.kilometros = kilometros; }
 
    public int getNumeroPuertas() { return numeroPuertas; }
    public void setNumeroPuertas(int numeroPuertas) { this.numeroPuertas = numeroPuertas; }
 
    public int getCapacidadMaletero() { return capacidadMaletero; }
    public void setCapacidadMaletero(int capacidadMaletero) { 
        this.capacidadMaletero = capacidadMaletero; 
    }
 
    public LocalDate getUltimaRevision() { return ultimaRevision; }
    public void setUltimaRevision(LocalDate ultimaRevision) { 
        this.ultimaRevision = ultimaRevision; 
    }
 
    // Resto de métodos
    . . .
 
}

Tal y como se puede observar en el ejemplo anterior, es mucha la cantidad de código que se ha repetido. Es muy habitual que varias de las clases de un mismo proyecto tengan grandes similitudes por definir objetos que en la vida real también las tienen. A veces se cree que simplemente copiando y pegando el código sería suficiente para no tener que duplicar ese código, pero incluso en ese caso sería costoso de mantener, puesto que tener varias veces escrito el mismo código en muchas partes del programa es costoso de controlar.

Lo que el paradigma de orientación a objetos propone es que se pueda heredar el código ya escrito en una clase sin necesidad de volver a escribir o pegar. De esa manera, un cambio en la clase original (llamada clase base) se adopta automáticamente y sin hacer nada por las clases que heredan (clases derivadas).

Así, el mismo ejemplo de antes, utilizando la herencia nos quedaría como sigue:

public class Vehiculo {
    private String matricula;
    private String marca;
    private String modelo;
    private float potencia;
    private int kilometros;
    private LocalDate ultimaRevision;
 
    public Vehiculo(String matricula, String marca, String modelo,
                float potencia, int kilometros) {
        this.matricula = matricula;
        this.marca = marca;
        this.modelo = modelo;
        this.potencia = potencia;
        this.kilometros = kilometros;
        this.ultimaRevision = LocalDate.now();
    }
 
    public String getMatricula() { return matricula; }
    public void setMatricula(String matricula) { this.matricula = matricula; }
 
    public String getMarca() { return marca; }
    public void setMarca(String marca) { this.marca = marca; }
 
    public String getModelo() { return modelo; }
    public void setModelo(String modelo) { this.modelo = modelo; }
 
    public float getPotencia() { return potencia; }
    public void setPotencia(float potencia) { this.potencia = potencia; }
 
    public int getKilometros() { return kilometros; }
    public void setKilometros(int kilometros) { this.kilometros = kilometros; }
 
    public LocalDate getUltimaRevision() { return ultimaRevision; }
    public void setUltimaRevision(LocalDate ultimaRevision) { 
        this.ultimaRevision = ultimaRevision; 
    }
 
    // Resto de métodos
    . . .
}
public class Moto extends Vehiculo {
 
    private boolean carenado;
 
    public Moto(String matricula, String marca, String modelo,
                float potencia, int kilometros, boolean carenado) {
        super(matricula, marca, modelo, potencia, kilometros);
        this.carenado = carenado;
    }
 
    public boolean getCarenado() { return carenado; }
    public void setCarenado(boolean carenado) { this.carenado = carenado;
 
    // Resto de métodos
    . . .
}
public class Coche extends Vehiculo {
 
    private int numeroPuertas;
    private int capacidadMaletero;
 
    public Coche(String matricula, String marca, String modelo,
                float potencia, int kilometros, int numeroPuertas,
                int capacidadMaletero) {
        super(matricula, marca, modelo, potencia, kilometros);
        this.numeroPuertas = numeroPuertas;
        this.capacidadMaletero = capacidadMaletero;
    }
 
    public int getNumeroPuertas() { return numeroPuertas; }
    public void setNumeroPuertas(int numeroPuertas) { this.numeroPuertas = numeroPuertas; }
 
    public int getCapacidadMaletero() { return capacidadMaletero; }
    public void setCapacidadMaletero(int capacidadMaletero) { 
        this.capacidadMaletero = capacidadMaletero; 
    }
 
    // Resto de métodos
    . . .
}

Y hay que tener en cuenta que, para este caso, cuantos más tipos de objetos existan derivados de \verb Vehiculo mayor será la reutilización de código y el aprovechamiento de la capacidad de herencia de este paradigma orientado a objetos.

E incluso podremos crear clases derivadas de otras que ya lo son, para crear, en este caso, vehículos todavía más específicos, aprovechando y reutilizando cada vez más las estructuras ya pensadas y definidas:

public class Deportivo extends Coche {
    . . .
    . . .
}
public class Furgoneta extends Coche {
    . . .
    . . .
}
public class Scooter extends Moto {
    . . .
    . . .
}

Limitaciones y características de la herencia en java

En cualquier caso, existen una serie de limitaciones o características a tener en cuenta en cuanto al uso de la herencia en Java:

  • Para heredar de una clase se utiliza la palabra reservada extends
  • La clase que hereda se conoce como clase heredada o clase hija. La clase de la que se hereda se conoce como clase base o clase padre.
  • Cuando una clase A hereda de una clase B, la clase A adquiere todos los atributos y comportamiento (atributos y métodos) de la clase B (aunque quizás no pueda acceder a algunos de ellos por los modificadores de accesibilidad)
  • En Java, sólo se puede heredar de una clase (sin contar la clase Object de la que heredan todas las clases de forma implícita). En Java se idearon las interfaces para suplir en parte esta carencia.
  • No hay límite en cuanto al número de clases que pueden heredar de una clase determinada
  • No hay límite en cuanto al nivel de profundidad en el árbol de herencias entre clases
  • No se puede heredar de una clase si ésta ha sido definida como final (lo que se conoce como una clase final)
  • Cuando una clase Coche hereda de una clase Vehiculo, un objeto de clase Coche se puede considerar ahora de tipo Vehículo pero no al revés.
  • Cuando una clase hereda de otra está obligada a implementar constructores apropiados para invocar a los de la clase base

super

La palabra reservada super se utiliza cuando una clase derivada quiere refererirse a algún componente (atributo o método) de su clase base. Ya vimos, en el caso de los constructores y la herencia de clases, como podemos invocar al constructor ya implementado de la clase base para inicializar los atributos que se declararon alli. De esa manera, estamos sobrescribiendo el método, ampliándolo en este caso. En vez de escribir todo el código de nuevo podemos aprovechar el código ya implementado en la clase base utilizando la palabra reservada super.

También puede emplearse para sobrescribir cualquier método en una clase derivada o bien para utilizar el valor de cualquier atributo que haya sido declarado en la clase base.

Clases abstractas

Podemos definir una clase como abstracta para indicar que no se debe (ni se puede) instanciar objetos de dicha clase. Es uno de los tipos de clase de los que hemos hablado al inicio de esta parte.

public abstract class Vehiculo {
    . . .
    . . .
}

La idea es controlar el uso que se hace de las clases que un programador ha definido para obligar a otros programadores que las puedan emplear en sus desarrollos a continuar con el diseño de clases que el primer programador pensó.

Si pensamos en el ejemplo anterior, es fácil darse cuenta de que nunca nos vamos a encontrar con objetos Vehiculo sino que siempre trabajaremos con algún tipo más específico de esa clase, como Coche o Moto .

public class Programa {
    public static void main(String args[]) {
        // No tiene sentido instancia vehiculos, no existen en el mundo real
        // Al ser definida como clase abstracta, el compilador lo marcará como un error
        Vehiculo vehiculo = new Vehiculo(. . .);
 
        // En el mundo real existen los objetos concretos
        Moto moto = new Moto(. . .);
        Furgoneta furgoneta = new Furgoneta(. . .);
        Scooter scooter = new Scooter(. . .);
 
        . . .
    }
}

Hay que tener en cuenta que también se podrán definir métodos abstractos dentro de una clase abstracta cuando no seamos capaces de definir la implementación de dicho método y queramos dejarlo a elección de los programadores que extiendan de esta clase abstracta. En ese caso, el método se definirá como abstracto y no se tendrá que implementar en la clase genérica o abstracta pero será obligatoria su implementación en cualquier clase concreta de la que se quieran poder definir objetos.

public abstract class Vehiculo {
    . . .
 
    public abstract void hacerRevision();
 
    . . .
}

En el caso de este método abstracto podemos suponer que las operaciones necesarias para hacer cualquier revisión de un tipo concreto de vehículo no son las mismas, por lo que debemos de dejar los detalles de implementación de este método a cada caso concreto (Moto, Coche, . . .). Pero al mismo tiempo, queremos asegurarnos de que todos los vehículos tengan esa operación implementada.

Interfaces

Las interfaces no se consideran clases, aunque en la práctica se parecen mucho a las clases abstractas. Se utilizan para definir simplemente el comportamiento pero sin entrar al detalle de la implementación. De esa manera queda a elección del programador que implementa (que usa) la interface el detalle de implementación de cada uno de los métodos en la clase concreta.

A diferencia de como ocurre con la herencia, no hay límite en el número de interfaces que una clase puede implementar.

public interface VehiculoMotor {
  boolean estaRevisado();
  void hacerRevision();
  void viajar(String origen, String destino);
  void viajar();
}

Así, si una clase implementa (hereda) esta interface, tendrá que implementar obligatoriamente los métodos definidos en ésta. En la práctica, una interface se podría considerar como una clase abstracta donde todos sus métodos han sido definidos también como abstractos.

public class Vehiculo implements VehiculoMotor {
  // Atributos
 
  // Constructores
 
  // Getters y Setters
 
  public boolean estaRevisado() {
    // Implementación propia de los vehículos
    . . .
  }
 
  public void hacerRevision() {
    // Implementación propia de los vehículos
    . . .
  }
 
  public void viajar(String ciudadOrigen, String ciudadDestino) {
    // Implementación propia de los vehículos
    . . .
  }
 
  public void viajar() {
    // Implementación propia de los vehículos
    . . .
  }
}
public class Embarcación implements VehiculoMotor {
  // Atributos
 
  // Constructores
 
  // Getters y Setters
 
  public boolean estaRevisado() {
    // Implementación propia de las embarcaciones
    . . .
  }
 
  public void hacerRevision() {
    // Implementación propia de las embarcaciones
    . . .
  }
 
  public void viajar(String puertoOrigen, String puertoDestino) {
    // Implementación propia de las embarcaciones
    . . .
  }
 
  public void viajar() {
    // Implementación propia de las embarcaciones
    . . .
  }
}
public class Lancha extends Embarcacion {
  . . .
}
 
public class Moto extends Vehiculo {
  . . .
}
 
public class Coche extends Vehiculo {
  . . .
}
. . .
. . .

Métodos por defecto

  • A partir de Java 8 se permiten crear métodos por defecto en interfaces
  • Son métodos en los que hay que proporcionar una implementación, que será la utilizada por defecto cuando la clase que implementa la interface no implemente dicho método en esa clase
  • De esa manera, ya no es obligatorio que la clase que implementa dicha interface, implemente el código
  • El caso de uso sería aquellas interfaces a las que se les añade algún método nuevo y ya están siendo implementadas. Eso provoca un fallo en todas aquellas clases que la implementan. De esta manera podemos evitar esos errores puesto que tomarían la implementación del método por defecto
  • Puede utilizarse también como implementación genérica para aquellos casos en los que no interese una implementación específica
  • En una misma interface puede haber más de un método por defecto
  • Si una clase implementa dos interfaces con el mismo método por defecto, deberá implementarlo
public interface MyInterface {
  default void aMethod() {
    // Hacer algo
  } 
}

Métodos estáticos

  • Al igual que los métodos por defecto, tienen que tener implementación
  • La clase que implementa una interface con métodos estáticos puede sobrescribir dichos métodos
  • Cumple el mismo propósito que un método estático en una clase
public interface MyInterface {
  static void aMethod() {
    // Hacer algo
  } 
}

Métodos privados

  • A partir de Java 9 se introduce este nuevo concepto que permite definir métodos privados en las interfaces
  • Se pueden definir para su uso dentro de la propia interface (puede servir de apoyo a otros métodos públicos)
public interface MyInterface {
  private void aMethod() {
    // Hacer algo
  } 
}

Creación y utilización de interfaces

Se pueden declarar variables utilizando como tipo el interfaz (al igual que ocurre con las clases abstractas) pero nunca será posible instanciar objetos ya que habrá que hacerlo utilizando siempre una clase concreta.

public class Programa {
    public static void main(String args[] {
        Moto unaMoto = new Moto(. . .);
        Lancha unaLancha = new Lancha(. . .);
 
        Mecanico mecanico = new Mecanico(. . .);
        mecanico.revisar(unaMoto);
        mecanico.revisar(unaLancha);
        System.out.println("Vehiculos revisados");
    }
}
 
public class Mecanico extends Trabajador {
    . . .
 
    public void revisar(VehiculoMotor vehiculoMotor) {
        . . .
 
        vehiculo.revisar();
        if (vehiculoMotor.estaRevisado()) {
            . . .
            . . .
        }
 
        // Si hay que acceder a atributos/métodos específicos
        if (vehiculoMotor instanceof Moto) {
            Moto moto = (Moto) vehiculoMotor;
            // Se puede acceder a la implementación específica del objeto
        }
        else if (vehiculoMotor instanceof Lancha) {
            Lancha lancha = (Lancha) vehiculoMotor;
            // Se puede acceder a la implementación específica del objeto
        }
        . . .
        else {
            System.out.println("Tipo de vehículo desconocido");
        }
    }
 
    . . .
}

En el ejemplo anterior se puede observar como se ha definido dentro de la clase Mecanico un método que aceptaba como parámetros objetos cuyo tipo coincidía con el interfaz. Es una manera genérica de definir un método que admita como parámetro cualquier tipo de objeto que implemente dicho interfaz (en vez de tener que definir uno para cada tipo concreto de clase). Así, podemos definir operaciones comunes a objetos que comparten parte de su especificación e implementación.

Más adelante, si queremos realizar alguna operación para un tipo concreto de objeto, podemos utilizar la orden instanceof para conocer de qué tipo concreto es un objeto y, realizando un cast de tipos, acceder a su implementación concreta y trabajar con ella.

Clases anidadas

Una clase anidada es una clase que se define dentro de otra para mantener un nivel mayor de encapsulación. Es útil cuando una clase es la única que hace uso de otra. En ese caso es posible definir la segunda clase dentro de la primera para mantener los niveles de encapsulación y abstracción.

public class Moto extends Vehiculo {
 
    . . .
    private Carenado carenado;
    . . .
 
    class Carenado {
        String material;
        float peso;
 
        public Carenado(String material, float peso) {
            this.material = material;
            this.peso = peso;
        }
 
        . . .
        . . .
    }
}

Número variable de parámetros en métodos

Java permite definir métodos con paso variable de parámetros de forma que podremos invocar al mismo método pasándole tantos parámetros como queramos, siempre y cuando éstos sean del mismo tipo. En el cuerpo del método los parámetros se reciben como un array del tipo que se haya especificado en su declaración. Esto permite, por ejemplo, saber si se ha pasado algún parámetro comprobando la longitud de dicho array.

<modificador> <tipo_retorno> <nombre>(<tipo_parametro>... <nombre_parametro>) {
}
public class Vehiculo {
 
    . . .
 
    public void viajar(String... ciudades) {
        if (ciudades.length < 2) {
            System.out.println("Se deben de especificar al menos dos ciudades");
            return;
        }
 
        System.out.println("Ha pasado por las siguientes ciudades:");
 
        for (String ciudad : ciudades) {
            System.out.println(ciudad);
            int kms = . . . // Calcular distancia entre ciudades
            kilometros += kms;
        }
 
        System.out.println("Fin del viaje");
 
        . . .
    }
 
    . . .
}
 
public class Programa {
    public static void main(String args[]) {
        Moto moto = new Moto(. . .);
        // Podemos especificar un número variable de parámetros
        moto.viajar("Zaragoza", "Madrid", "Vigo");
        moto.viajar("Huesca", "Teruel"); 
    }
}

Ejercicios

  1. Desarrolla una clase CuentaBancaria, que posea los siguientes atributos y métodos:
    1. Saldo
    2. Número de cuenta
    3. Interés
    4. Titular
    5. Entidad
    6. Ingresar: Permitirá ingresar una cantidad de dinero en la cuenta
    7. Ingresar: Permitirá ingresar una cantidad de dinero en la cuenta siempre y cuando el saldo de la misma esté por debajo de una cantidad que se pasará como parámetro
    8. Implementa algún constructor
    9. Implementa los getters y setters necesarios
    10. Añade una clase principal desde la que puedas crear objetos y mostrar la información de los mismos.
    11. Invoca también a alguno de los métodos que realizan cambios sobre el estado del objeto para visualizar dichos cambios.
  2. Implementa una clase Java que permita definir la estructura para un eletrodoméstico. Elige hasta 5 atributos de diferentes tipos e implementa los atributos, al menos un constructor, getters y setters y algún método de utilidad para dicha clase.
  3. Implementa una clase CuentaBancaria para definir una clase que será usada para la nueva web de una entidad bancaria.
    1. Tendrá que tener los siguientes atributos: Número de cuenta, titular, saldo, interes.
    2. El número de cuenta no se podrá modificar
    3. Debe ser posible realizar las operaciones de ingresar, retirar dinero y otra operación para calcular los intereses generados en un mes determinado
  4. Implementa una clase Trabajador para definir a los trabajadores de una nueva aplicación que quiere desarrollar una empresa para gestionar a su plantilla:
    1. De cada trabajador se almacenará el nombre, apellidos, dni, email, fecha de nacimiento, salario
    2. Se tiene que poder crear un trabajador inicializando todos los atributos y también indicando sólo nombre y apellidos, ya que habitualmente habrá quién visite la empresa días determinados y necesite una acreditación temporal
    3. Hay que tener en cuenta que nombre, apellidos y dni nunca se podrán modificar
    4. Realizar las operaciones necesarias para poder incrementar el salario de un trabajador incrementándolo en una cantidad determinada y también indicando el tanto por ciento de subida que se le aplicará

© 2019-2025 Santiago Faci

apuntes/objetos.1685902291.txt.gz · Last modified: 2023/06/04 18:11 by Santiago Faci