Programación

1º DAM/DAW - Curso 2023-2024

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

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) {
            // Se puede acceder a la implementación específica del objeto
            moto . . .
        }
        else if (vehiculoMotor instanceof Lancha lancha) {
            // Se puede acceder a la implementación específica del objeto
            lancha . . .
        }
        . . .
        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á
  5. Se quieren diseñar ahora las clases para un videojuego de estrategia al estilo Civilization. En él los usuarios podrán crear sus propios personajes y construir una ciudad que irá evolucionando para ganar a las de los demás jugadores. De cada jugador se quiere almacenar su nick, nombre, apellidos, email y tiempo de juego (en segundos). De cada personaje se quiere almacenar su nombre, la época a la que pertenece, el nivel y el dinero conseguido hasta el momento. Hay queUna operación que permita añadir kilometros al coche tener en cuenta que cada usuario puede tener un personaje y debe quedar registrado cuál es. Como cada personaje puede crear una ciudad (y debe quedar asi registrado) se guardará, de cada una, el nombre, el nivel, número de habitantes y el tipo de ciudad (aldea, pueblo o ciudad).
    1. Se espera que los jugadores puedan realizar las siguientes operaciones:
      1. Una operación para añadir tiempo de juego (Se ejecutará cada vez que termine una partida para incrementar el tiempo empleado)
    2. De los personajes se espera que puedan realizar las siguientes operacones:
      1. Una operación que permita crear una ciudad indicando el nombre de ésta. El nivel y número de habitantes inicial será 0, y el tipo de ciudad será aldea. La ciudad deberá de quedar vinculada al personaje.
    3. Para las ciudades se necesitarán las siguientes operaciones
      1. Una operación que permita incrementar el nivel de la ciudad. En esta operación habrá que tener en cuenta que cuando el nivel supere el valor 10 la
    4. Para todos los casos, implementar las clases, atributos, constructores necesarios y las operaciones solicitadas
  6. El propietario de un taller te ha encargado que desarrolles una aplicación para la gestión de su trabajo. Te encuentras en el paso en que debes diseñar e implementar las clases necesarias para desarrollar dicha aplicación. En el taller arreglan Coches y Motos y trabajan varios mecánicos. De los coches quieren almacenar el número de bastidor, la matrı́cula, modelo, color, kilometraje y la capacidad del maletero. De las motos quieren guardar también número de bastidor, matrı́cula, modelo, color y kilometraje, y además quieren saber si una moto tiene o no carenado y el peso de la misma. En ambos casos, para motos y coches se querrá almacenar si están o no reparados. Además, de los mecánicos quieren almacenar el nombre, apellidos, email, especialidad (Coche o Moto) y el número de reparaciones que han hecho desde que trabajan alli. Además, siempre se guardará el vehı́culo en el que está trabajando cada mecánico.
    1. En cuanto a las operaciones de los coches, se esperan las siguientes:
      1. Una operación que permita añadir kilometros al coche
      2. Una operación para pintar el coche de otro color
    2. De las motos se necesitarán estas operaciones:
      1. Una operación para eliminar el carenado de la moto (hay que tener en cuenta que el peso disminuirá en 2 kg)
      2. Una operación para pintar la moto de otro color
    3. Para el caso de los mecánicos, las operaciones serán las siguientes
      1. Una operación que permita reparar cualquier vehı́culo, que marcará el vehı́culo como reparado
      2. Una operación que permita marcar que un vehı́culo entra al taller, marcándolo como sin reparar, para que quede constancia que hay que trabajar en él
      3. De alguna forma tendremos que poder asignar un vehı́culo al mecánico, de forma que cuando éste lo repare deje de estarlo
    4. Implementar las clases necesarias con sus atributos, los constructores, getters y setters, y las operaciones indicadas
  7. Ahora te piden diseñar el sistema de clases para una biblioteca. Sólo se necesita almacenar la información relativa a Socios, Libros y CDs de música.
    1. Puede haber varios tipos de libros (novelas y tebeos) o cds. De todos ellos se quiere almacenar el isbn, titulo, autor y el año de edición. Es posible que en el momento de dar de alta cualquier elemento no se disponga de la información del autor (habrá que tenerlo en cuenta).
    2. De las novelas además se guardará un resumen del argumento de unas 100 palabras y, en ocasiones, el número de páginas
    3. De los cds de música se querrá almacenar también el número de canciones y la discográfica que lo comercializó.
    4. De los tebeos se quiere guardar un resumen y el personaje principal.
    5. De los socios guardaremos el número de socio, nombre, apellidos y un email. No siempre se podrá disponer del email por lo que será un dato opcional. Además se quiere guardar en todo momento que libro ha pedido prestado cada usuario, teniendo en cuenta que sólo puede coger un libro al mismo tiempo.
    6. Hay que tener en cuenta que también existe un tipo especial de socio (VIP) que podrá alquilar libros durante más tiempo previo pago con tarjeta de crédito, por lo que guardaremos el número de la misma.
    7. Se te piden realizar lo siguiente:
      1. Implementar las clases necesarias para la aplicación
      2. Implementar los constructores que permitan trabajar con las clases de la forma que se indica
      3. Implementar operaciones para que los socios puedan alquilar y devolver. Habrá que controlar que no puedan alquilar nada si todavı́a no han devuelto lo que tenı́an alquilado. También hay que tener en cuenta que sólo permitimos el alquiler de cds de música si el socio ha facilitado su dirección de correo electrónico
  8. Te piden diseñar las clases que permitirán definir el modelo de datos para una aplicación que permita gestionar toda la información de Parque Zoológico. Hay que tener en cuenta que puede haber cualquier tipo de animal pero que todos, al fin y al cabo, necesitan los mismos cuidado y tienen el mismo comportamiento, aunque a su manera, que se quiere monitorizar para velar por su bienestar:
    1. En cuanto a los cuidados, tiene que ser posible: limpiarlos, darles de comer, cambiarlos de estancia y curar cualquier enfermedad que puedan tener
    2. En cuanto a su comportamiento, todo ellos: comerán, dormirán, se moverán, volarán (los que puedan), pondrán huevos o darán a luz nuevas crías de alguna forma, . . . .
    3. Debes diseñar, además, el modelo de clases pensando que en cualquier momento puede ser incorporado al Zoo animales de nuevas razas. Intenta definir dicho modelo permitiendo que sea fácil adaptable a esta nueva situación.
    4. Define:
      1. Una cuantas clases para una variedad de animales
      2. Constructores
      3. Gettes y Setters necesarios
      4. Una clase principal donde probar el funcionamiento de todo el modelo diseñado
  9. La cadena de supermercados CodeAndCokeMarket te ha contratado para que desarrolles la aplicación que necesitan para gestionar al personal, sus locales y las ventas.
    1. Hay que tener en cuenta que, en cada local, hay un encargado, unos cuantos reponedores y personal que trabaja en la línea de cajas. Puesto que a veces hay picos de trabajo, a pesar de que existan definidos esos tres perfiles 3 perfiles, todos deben estar preparados para desempeñar cualquier de las funciones, incluso el propio encargado.
    2. En cuanto a los locales, los hay de tres tipos: grandes superficies a las afueras de la ciudad, pequeños locales en el centro y también algunos centros que abren 24 horas. La diferencia entre los tres tipos de locales son el tamaño, la variedad de productos que tienen, el horario y el número de personas que trabajan en ellos.
    3. En cuanto a los clientes, puesto que quieren potenciar la fidelidad de los mismos, siempre se les solicita si quieren hacerse socios de TokioMarket para entregarles una tarjeta de cliente y asi optar a descuentos y promociones varias. En función del gasto mensual que hagan hay 3 perfiles: Socio Bronce, Plata y Oro. La diferencia entre los 3 es que optan a mayores descuentos (el porcentaje que se aplica a los productos). También es cierto que, en función del tipo de socio, pueden tener descuentos en unos u otros productos y además, a los Oro, se les invita a eventos exclusivos.
    4. Sobre los productos, hay varias categorías principales: Hogar, Ocio y Trabajo, pero cada producto puede pertenecer a varias categorias al mismo tiempo
    5. Define:
      1. Clases para definir todo el problema
      2. Constructores, Gettes y Setters para todos
      3. Una clase principal donde probar el funcionamiento de todo el modelo diseñado

© 2019-2024 Santiago Faci

apuntes/objetos.txt · Last modified: 2023/06/04 21:03 by Santiago Faci