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.
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:
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.
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).
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
public
hace que el atributo sea visible para todo el proyectoprotected
hace que el atributo sea visible para la propia clase y las que hereden de éstaprivate
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; }
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(); } }
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"); } . . . }
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.
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 } }
// Variable tipo primitivo int x = 10;
// Objeto declarado (e instanciado) Vehiculo vehiculo = new Vehiculo();
// Objeto declarado (referencia a null) Vehiculo vehiculo;
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??
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
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); } }
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 . . . }
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 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:
extends
Object
de la que heredan todas las clases de forma implícita). En Java se idearon las interfaces para suplir en parte esta carencia.final
(lo que se conoce como una clase final)Coche
hereda de una clase Vehiculo
, un objeto de clase Coche
se puede considerar ahora de tipo Vehículo pero no al revés.
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.
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.
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 { . . . } . . . . . .
public interface MyInterface { default void aMethod() { // Hacer algo } }
public interface MyInterface { static void aMethod() { // Hacer algo } }
public interface MyInterface { private void aMethod() { // Hacer algo } }
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.
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; } . . . . . . } }
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"); } }
© 2019-2024 Santiago Faci