Programación

1º DAM - Curso 2021-2022

User Tools

Site Tools


apuntes:objetos

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

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 {
    . . .
    . . .
}

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

  • Una clase sólo puede heredar de una clase
  • No hay límite en el número de clases que pueden derivar de otra
  • No se puede heredar de una clase si ésta ha sido definida como final (lo que se conoce como una clase final)
  • Cuando se hereda de una clase, se hereda todo el código de ella, aunque los modificadores de visibilidad no permitan acceder a algunos atributos o métodos de la clase base directamente

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 {
 
    public boolean estaRevisado();
    public void hacerRevision();
    public void viajar(String origen, String destino);
    public 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 {
    . . .
}
. . .
. . .

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

Ejemplos


© 2019 Santiago Faci

apuntes/objetos.txt · Last modified: 2019/02/16 11:33 by Santiago Faci