CONCEPTOS GENERALES
La Programación Orientada a Objetos (POO u OOP) es un paradigma de programación que define los programas en términos de “clases de objetos”, objetos que son entidades que combinan estado (propiedades o datos), comportamiento (procedimientos o métodos) e identidad (propiedad del objeto que lo diferencia del resto).
La programación orientada a objetos expresa un programa como un conjunto de estos objetos, que colaboranentre ellos para realizar tareas. Esto permite hacer los programas y módulos más fáciles de escribir, mantener y reutilizar.
Un objeto contiene toda la información que permite definirlo e identificarlo frente a otros objetos pertenecientes a otras clases (e incluso entre objetos de una misma clase, al poder tener valores bien diferenciados en sus atributos). A su vez, dispone de mecanismos de interacción (los llamados métodos) que favorecen la comunicación entre objetos (de una misma clase o de distintas), y en consecuencia, el cambio de estado en los propios objetos. Esta característica lleva a tratarlos como unidades indivisibles, en las que no se separan (ni deben separarse) información (datos) y procesamiento (métodos).
Las clases son declaraciones o abstracciones de objetos, lo que significa, que una clase es la definición de un objeto. Cuando se programa un objeto y se definen sus características y funcionalidades, realmente se programa una clase.
Clases y Objetos
Creación de objetos
La construcción de un objeto consta de tres
etapas:
Se reserva espacio en memoria para la estructura de
datos que define la clase.
Inicializa los campos de la instancia con los valores
por defecto
Garantiza que cada atributo de una clase tenga un valor
inicial antes de la llamada al constructor
Se aplica sobre la instancia el constructor que se
invoca.
Componentes de un clase
Atributos: Determinan una estructura de almacenamiento para cada objeto de la clase Métodos: Operaciones aplicables a los objetos Único modo de acceder a los atributos.
Ejemplo: En una aplicación bancaria, encontramos objetos “cuenta”. Todos los objetos “cuenta” tienen propiedades comunes: atributos: saldo, titular, ... operaciones: reintegro, ingreso, …
Componentes de un clase
Atributos: Determinan una estructura de almacenamiento para cada objeto de la clase Métodos: Operaciones aplicables a los objetos Único modo de acceder a los atributos.
Ejemplo: En una aplicación bancaria, encontramos objetos “cuenta”. Todos los objetos “cuenta” tienen propiedades comunes: atributos: saldo, titular, ... operaciones: reintegro, ingreso, …
⬇
Definimos una clase CUENTA
Java como todo lenguaje de programación orientado a objetos utiliza los llamados metodos. Acontinuación veremos como se crea un metodo y como se utilizan.
Se podría decir que existen 2 grandes tipos de métodos, el primer tipo de método son métodos que realizan procesos, puedes realizar cualquier operación con ellos, sin embargo el propósito es manipular variables existentes. El segundo tipo de métodos son los que realizan un proceso o calculo, y calculan una variable especifica, un ejemplo podría ser un método para obtener el valor de una multiplicación.
Se podría decir que existen 2 grandes tipos de métodos, el primer tipo de método son métodos que realizan procesos, puedes realizar cualquier operación con ellos, sin embargo el propósito es manipular variables existentes. El segundo tipo de métodos son los que realizan un proceso o calculo, y calculan una variable especifica, un ejemplo podría ser un método para obtener el valor de una multiplicación.
Estructura de un método
Los métodos en java pueden tener parámetros, es decir, que un método puede utilizar variables predefinidas para ser utilizadas en sus procesos, Veamos un ejemplo de como hacer un método en el siguiente ejemplo
En este ejemplo vemos un programa normal en el cual se ejecuta un ciclo while que imprime números del 0 al 7, pero ¿es posible hacerlo utilizando un método?

PASOS PARA CREAR UN MÉTODO EN JAVA.
- System.out.println("Programa de Suma de números iniciando");
- //iniciamos sumando.
- int sumando1=4234;
- System.out.println("Sumando 1: "+sumando1);
- //iniciamos sumando 2.
- int sumando2=64782;
- System.out.println("Sumando 2: "+sumando1)
Parámetros y Argumentos.
Parámetros
Un parámetro representa un valor que el procedimiento espera que se transfiera cuando es llamado. La declaración del procedimiento define sus parámetros.
Cuando se define un procedimiento Function o Sub, se especifica una lista de parámetros entre paréntesis que va inmediatamente después del nombre de procedimiento. Para cada parámetro, se especifica un nombre, un tipo de datos y un mecanismo para pasar argumentos (ByVal (Visual Basic) o ByRef (Visual Basic)). También puede indicar que un parámetro es opcional. Esto significa que el código de llamada no tiene que transferir un valor.
El nombre de cada parámetro actúa como una variable local en el procedimiento. El nombre del parámetro se utiliza del mismo modo que cualquier otra variable.
Argumentos
Un argumento representa el valor que se transfiere a un parámetro del procedimiento cuando se llama al procedimiento. El código de llamada proporciona los argumentos cuando llama al procedimiento.
Cuando se llama al procedimiento Function o Sub, se incluye una lista de argumentos entre paréntesis que van inmediatamente después del nombre del procedimiento. Cada argumento se corresponde con el parámetro situado en la misma posición de la lista.
A diferencia de la definición de parámetros, los argumentos no tienen nombres. Cada argumento es una expresión que puede contener cero o más variables, constantes y literales. El tipo de datos de la expresión evaluada normalmente debe coincidir con el tipo de datos definido para el parámetro correspondiente, y en algún caso, debe poder convertirse al tipo del parámetro.
UN EJEMPLO DE COMO HACERLO.
Retorno de Valores.
Las funciones pueden devolver valores, a través de la sentencia return. También vemos un apunte sobre el ámbito de variables en funciones en Javascript.
Estamos aprendiendo acerca del uso de funciones en Javascript y en estos momentos quizás ya nos hayamos dado cuenta de la gran importancia que tienen para hacer programas más o menos avanzados. En este artículo del Manual de Javascript seguiremos aprendiendo cosas sobre funciones y en concreto que con ellas también se puede devolver valores. Además, veremos algún caso de uso interesante sobre las funciones que nos puede aclarar un poco el ámbito de variables locales y globales.
Devolución de valores en las funciones
Las funciones en Javascript también pueden retornar valores. De hecho, ésta es una de las utilidades más esenciales de las funciones, que debemos conocer, no sólo en Javascript sino en general en cualquier lenguaje de programación. De modo que, al invocar una función, se podrá realizar acciones y ofrecer un valor como salida.
Por ejemplo, una función que calcula el cuadrado de un número tendrá como entrada a ese número y como salida tendrá el valor resultante de hallar el cuadrado de ese número. La entrada de datos en las funciones la vimos anteriormente en el artículo sobre parámetros de las funciones. Ahora tenemos que aprender acerca de la salida.
Veamos un ejemplo de función que calcula la media de dos números. La función recibirá los dos números y retornará el valor de la media.
function media(valor1,valor2){
var resultado
resultado = (valor1 + valor2) / 2
return resultado
}
Para especificar el valor que retornará la función se utiliza la palabra return seguida de el valor que se desea devolver. En este caso se devuelve el contenido de la variable resultado, que contiene la media calculada de los dos números.
Quizás nos preguntemos ahora cómo recibir un dato que devuelve una función. Realmente en el código fuente de nuestros programas podemos invocar a las funciones en el lugar que deseemos. Cuando una función devuelve un valor simplemente se sustituye la llamada a la función por ese valor que devuelve. Así pues, para almacenar un valor de devolución de una función, tenemos que asignar la llamada a esa función como contenido en una variable, y eso lo haríamos con el operador de asignación =.
Para ilustrar esto se puede ver este ejemplo, que llamará a la función media() y guardará el resultado de la media en una variable para luego imprimirla en la página.
var miMedia
miMedia = media(12,8)
document.write (miMedia)
Múltiples return
En realidad en Javascript las funciones sólo pueden devolver un valor, por lo que en principio no podemos hacer funciones que devuelvan dos datos distintos.
Nota: en la práctica nada nos impide que una función devuelva más de un valor, pero como sólo podemos devolver una cosa, tendríamos que meter todos los valores que queremos devolver en una estructura de datos, como por ejemplo un array. No obstante, eso sería un uso más o menos avanzado que no vamos a ver en estos momentos.
Ahora bien, aunque sólo podamos devolver un dato, en una misma función podemos colocar más de un return. Como decimos, sólo vamos a poder retornar una cosa, pero dependiendo de lo que haya sucedido en la función podrá ser de un tipo u otro, con unos datos u otros.
En esta función podemos ver un ejemplo de utilización de múltiples return. Se trata de una función que devuelve un 0 si el parámetro recibido era par y el valor del parámetro si este era impar.
function multipleReturn(numero){
var resto = numero % 2
if (resto == 0)
return 0
else
return numero
}
Para averiguar si un número es par hallamos el resto de la división al dividirlo entre 2. Si el resto es cero es que era par y devolvemos un 0, en caso contrario -el número es impar- devolvemos el parámetro recibido.
Ámbito de las variables en funciones
Dentro de las funciones podemos declarar variables. Sobre este asunto debemos de saber que todas las variables declaradas en una función son locales a esa función, es decir, sólo tendrán validez durante la ejecución de la función.
Nota: Incluso, si lo pensamos, nos podremos dar cuenta que los parámetros son como variables que se declaran en la cabecera de la función y que se inicializan al llamar a la función. Los parámetros también son locales a la función y tendrán validez sólo cuando ésta se está ejecutando.
Podría darse el caso de que podemos declarar variables en funciones que tengan el mismo nombre que una variable global a la página. Entonces, dentro de la función, la variable que tendrá validez es la variable local y fuera de la función tendrá validez la variable global a la página.
En cambio, si no declaramos las variables en las funciones se entenderá por javascript que estamos haciendo referencia a una variable global a la página, de modo que si no está creada la variable la crea, pero siempre global a la página en lugar de local a la función.
Veamos el siguiente código.
function variables_glogales_y_locales(){
var variableLocal = 23
variableGlobal = "qwerty"
}
En este caso variableLocal es una variable que se ha declarado en la función, por lo que será local a la función y sólo tendrá validez durante su ejecución. Por otra parte variableGlobal no se ha llegado a declarar (porque antes de usarla no se ha utilizado la palabra var para declararla). En este caso la variable variableGlobal es global a toda la página y seguirá existiendo aunque la función finalice su ejecución. Además, si antes de llamar a la función existiese la variable variableGlobal, como resultado de la ejecución de esta función, se machacaría un hipotético valor de esa variable y se sustituiría por "qwerty".
Nota: Podemos encontrar más información sobre ámbito de variables en un artículo anterior.
Con esto hemos terminado el tema de las funciones, así que en adelante nos dedicaremos a otros asuntos también interesantes, como son los Arrays en Javascript.
CÓMO CREAR CONSTRUCTORES EN JAVA. EJERCICIOS EJEMPLOS RESUELTOS.
Los constructores de una clase son fragmentos de código que sirven para inicializar un objeto a un estado determinado. Una clase puede carecer de constructor, pero esto no es lo más habitual. Normalmente todas nuestras clases llevarán constructor. En un constructor es frecuente usar un esquema de este tipo:
/* Ejemplo - aprenderaprogramar.com */
public MismoNombreQueLaClase (tipo parámetro1, tipo parámetro2 …, tipo parámetro n ) {
campo1 = valor o parámetro;
campo2 = valor o parámetro;
.
.
.
campo n = valor o parámetro;
}
|
Los constructores tienen el mismo nombre que la clase en la que son definidos y nunca tienen tipo de retorno, ni especificado ni void. Tenemos aquí un aspecto que nos permite diferenciar constructores de métodos: un constructor nunca tiene tipo de retorno mientras que un método siempre lo tiene. Es recomendable que en un constructor se inicialicen todos los atributos de la clase aunque su valor vaya a ser nulo o vacío. Si un atributo se quiere inicializar a cero (valores numéricos) siempre lo declararemos específicamente: nombreAtributo = 0;. Si un atributo se quiere inicializar a contenido nulo (atributos que son objetos) siempre lo declararemos específicamente: nombreAtributo = null;. Si un atributo tipo texto se quiere inicializar vacío siempre lo declararemos específicamente: nombreAtributo = “”;. El motivo para actuar de esta manera es que declarando los atributos como nulos o vacíos, dejamos claro que esa es nuestra decisión como programadores. Si dejamos de incluir uno o varios campos en el constructor puede quedar la duda de si hemos olvidado inicializar ese campo o inducir a pensar que trabajamos con malas prácticas de programación.
La inicialización de campos y variables es un proceso muy importante. Su mala definición es fuente de problemas en el desarrollo de programas. Como regla de buena programación, cuando crees campos o variables, procede de forma inmediata a definir su inicialización.
Un constructor puede:
a) Carecer de parámetros: que no sea necesario pasarle un parámetro o varios al objeto para inicializarse. Un constructor sin parámetros se denomina “constructor general”.
b) Carecer de contenido. Por ejemplo, public Taxi () { } podría ser un constructor, vacío. En general un constructor no estará vacío, pero en algunos casos particulares puede estarlo. Si el constructor carece de contenido los campos se inicializan con valor nulo o, si son tipos definidos en otra clase, como se haya definido en el constructor de la otra clase. Excepto en casos controlados, evitaremos que existan constructores vacíos.
Si un constructor tiene parámetros, el funcionamiento es análogo al que ya hemos visto para métodos. Cuando vayamos a crear el objeto con BlueJ, se nos pedirá además del nombre que va a tener el objeto, el valor o contenido de los parámetros requeridos. Un parámetro con frecuencia sirve para inicializar el objeto como hemos visto, y en ese caso el objeto tendrá el valor pasado como parámetro como atributo “para siempre”, a no ser que lo cambiemos por otra vía como puede ser un método modificador del atributo. No obstante, en algunos casos los parámetros que recibe un constructor no se incorporarán directamente como atributos del objeto sino que servirán para realizar operaciones de diversa índole. Escribe y compila el siguiente código:
/* Ejemplo - aprenderaprogramar.com */
public class Taxi { //El nombre de la clase
private String ciudad; //Ciudad de cada objeto taxi
private String matricula; //Matrícula de cada objeto taxi
private String distrito; //Distrito asignado a cada objeto taxi
private int tipoMotor; //Tipo de motor asignado a cada objeto taxi. 0 = desconocido, 1 = gasolina, 2 = diesel
//Constructor: cuando se cree un objeto taxi se ejecutará el código que incluyamos en el constructor
public Taxi (String valorMatricula, String valorDistrito, int valorTipoMotor) {
ciudad = "México D.F.";
matricula = valorMatricula;
distrito = valorDistrito;
tipoMotor = valorTipoMotor;
} //Cierre del constructor
//Método para obtener la matrícula del objeto taxi
public String getMatricula () { return matricula; } //Cierre del método
//Método para obtener el distrito del objeto taxi
public String getDistrito () { return distrito; } //Cierre del método
//Método para obtener el tipo de motor del objeto taxi
public int getTipoMotor () { return tipoMotor; } //Cierre del método
} //Cierre de la clase
|
Este código es similar al que vimos en epígrafes anteriores. La diferencia radica en que ahora en vez de tener un constructor que establece una forma fija de inicializar el objeto, la inicialización depende de los parámetros que le lleguen al constructor.Además, hemos eliminado los métodos para establecer el valor de los atributos. Ahora éstos solo se pueden consultar mediante los métodos get. Pulsa con botón derecho sobre el icono de la clase y verás como la opción new Taxi incluye ahora los parámetros dentro de los paréntesis. Escoge esta opción y establece unos valores como matrícula “BFG-7452”, distrito “Oeste” y tipo de motor 2. Luego, con el botón derecho sobre el icono del objeto, elige la opción Inspect para ver su estado.
Que un constructor lleve o no parámetros y cuáles tendremos que elegirlo para cada clase que programemos. En nuestro ejemplo hemos decidido que aunque la clase tiene cuatro campos, el constructor lleve solo tres parámetros e inicializar el campo restante con un valor fijo. Un constructor con parámetros es adecuado si tiene poco sentido inicializar los objetos vacíos o siempre con el mismo contenido para uno o varios campos. No obstante, siempre hay posibilidad de darle contenido a los atributos a posteriori si incluimos métodos “setters”. El hacerlo de una forma u otra dependerá del caso concreto al que nos enfrentemos.
El esquema que hemos visto supone que en general vamos a realizar una declaración de campo en cabecera de la clase, por ejemplo String ciudad;, y posteriormente inicializar esa variable en el constructor, por ejemplo ciudad = “México D.F.”;. ¿Sería posible hacer una declaración en cabecera de clase del tipo String ciudad = “México D.F.”;? La respuesta es que sí. El campo quedaría inicializado en cabecera, pero esto en general debe ser considerado una mala práctica de programación y contraproducente dentro de la lógica de la programación orientada a objetos. Por tanto de momento trataremos de evitar incluir código de ese tipo en nuestras clases y procederemos siempre a inicializar en los constructores.
EJERCICIO
Define una clase Bombero considerando los siguientes atributos de clase: nombre (String), apellidos (String), edad (int), casado (boolean), especialista (boolean). Define un constructor que reciba los parámetros necesarios para la inicialización y los métodos para poder establecer y obtener los valores de los atributos. Compila el código para comprobar que no presenta errores, crea un objeto y comprueba que se inicializa correctamente consultando el valor de sus atributos después de haber creado el objeto.
sobrecarga de métodos.
unobrecargado se utiliza para reutilizar el nombre de un método pero con diferentes argumentos (opcional mente un tipo diferente de retorno). Las reglas para sobrecargar un método son las siguientes:
+ Los métodos sobrecargados debe de cambiar la lista de argumentos.
+ Pueden cambiar el tipo de retorno.
+ Pueden cambiar el modificador de acceso.
+ Pueden declarar nuevas o más amplias excepciones.
+ Un método puede ser sobrecargado en la misma clase o en una subclase.
Veamos un método que se desea sobrecargar:
Los siguientes métodos son sobrecargas legales del método cambiarTamano():
public void cambiarTamano(int tamano, String nombre){}
public int cambiarTamano(int tamano, float patron){}
public void cambiarTamano(float patron, String nombre) throws IOException{}
Cómo invocar un método sobrecargado::
Lo que define qué método es el que se va a llamar son los argumentos que se envían al mismo durante la llamada. Si se invoca a un método con un String como argumento, se ejecutará el método que tome un String como argumento, si se manda a llamar al mismo método pero con un float como argumento, se ejecutará el método que tome un float como argumento y así sucesivamente. Si se invoca a un método con un argumento que no es definido en ninguna de las versiones sobrecargadas entonces el compilador arrojará un mensaje de error.
Ejemplo de una clase con un método sobrecargado:
public class Sobrecarga {
public void Numeros(int x, int y){
System.out.println("Método que recibe enteros.");
}
public void Numeros(double x, double y){
System.out.println("Método que recibe flotantes.");
}
public void Numeros(String cadena){
System.out.println("Método que recibe una cadena: "+ cadena);
}
public static void main (String... args){
Sobrecarga s = new Sobrecarga();
int a = 1;
int b = 2;
s.Numeros(a,b);
s.Numeros(3.2,5.7);
s.Numeros("Monillo007");
}
}
Al ejecutar el código anterior obtendremos lo siguiente:
Método que recibe enteros.
Método que recibe flotantes.
Método que recibe una cadena: Monillo007
Al utilizar objetos en lugar de tipos primitivos o cadenas se vuelve más interesante. Veamos lo que sucede cuando se tiene un método sobrecargado que en una de sus versiones toma un Animal como parámetro y en otra un Caballo.
class Animal { }
class Caballo extends Animal{ }
class Animales{
public void MetodoSobrecargado(Animal a){
System.out.println("Método de parámetro Animal...");
}
public void MetodoSobrecargado(Caballo c){
System.out.println("Método de parámetro Caballo...");
}
public static void main(String... args){
Animales as = new Animales();
Animal objetoAnimal = new Animal();
Caballo objetoCaballo = new Caballo();
as.MetodoSobrecargado(objetoAnimal);
as.MetodoSobrecargado(objetoCaballo);
}
}
Al ejecutar el código anterior obtenemos:
Método de parámetro Animal...
Método de parámetro Caballo...
Como era de esperarse, cada objeto manda a llamar al método que le corresponde de acuerdo a su tipo, pero qué pasa si creamos una referencia Animal sobre un objeto Caballo...
Animal objetoAnimalRefCaballo = new Caballo();
as.MetodoSobrecargado(objetoAnimalRefCaballo);
El resultado es:
Método de parámetro Animal...
Aunque en tiempo de ejecución el objeto es un Caballo y no un Animal, la elección de qué método sobrecargado invocar no se realiza dinámicamente en tiempo de ejecución, el tipo de referencia, no el objeto actual, es el que determina qué método es el que se ejecutará.
Reglas de la sobrecarga y sobreescritura de métodos::
Ahora que hemos visto ambas formas de reescribir métodos, revisemos las reglas y diferencias entre ambos tipos de reescritura:
+Argumentos: En un método sobrecargado los argumentos deben de cambiar mientras que en un método sobreescrito NO deben cambiar.
+ El tipo de retorno: En un método sobrecargado el tipo de retorno puede cambiar, en un método sobreescrito NO puede cambiar, excepto por subtipos del tipo declarado originalmente.
+ Excepciones: En un método sobrecargado las excepciones pueden cambiar, en un método sobreescrito pueden reducirse o eliminarse pero NO deben de arrojarse excepciones nuevas o más amplias.
+ Acceso: En un método sobrecargado puede cambiar, en un método sobreescrito el acceso NO debe de hacerse más restrictivo(puede ser menos restrictivo).
+ Al invocar: En un método sobrecargado los argumentos son los que determinan qué método es el que se invocará, en un método sobreescrito el tipo de objeto determina qué método es elegido.
Esto es todo con la sobrecarga de métodos en Java. ¿Alguna duda? deja tu comentario.
+ Los métodos sobrecargados debe de cambiar la lista de argumentos.
+ Pueden cambiar el tipo de retorno.
+ Pueden cambiar el modificador de acceso.
+ Pueden declarar nuevas o más amplias excepciones.
+ Un método puede ser sobrecargado en la misma clase o en una subclase.
Veamos un método que se desea sobrecargar:
public void cambiarTamano(int tamano, String nombre, float patron){ }
Los siguientes métodos son sobrecargas legales del método cambiarTamano():
public void cambiarTamano(int tamano, String nombre){}
public int cambiarTamano(int tamano, float patron){}
public void cambiarTamano(float patron, String nombre) throws IOException{}
Cómo invocar un método sobrecargado::
Lo que define qué método es el que se va a llamar son los argumentos que se envían al mismo durante la llamada. Si se invoca a un método con un String como argumento, se ejecutará el método que tome un String como argumento, si se manda a llamar al mismo método pero con un float como argumento, se ejecutará el método que tome un float como argumento y así sucesivamente. Si se invoca a un método con un argumento que no es definido en ninguna de las versiones sobrecargadas entonces el compilador arrojará un mensaje de error.
Ejemplo de una clase con un método sobrecargado:
public class Sobrecarga {
public void Numeros(int x, int y){
System.out.println("Método que recibe enteros.");
}
public void Numeros(double x, double y){
System.out.println("Método que recibe flotantes.");
}
public void Numeros(String cadena){
System.out.println("Método que recibe una cadena: "+ cadena);
}
public static void main (String... args){
Sobrecarga s = new Sobrecarga();
int a = 1;
int b = 2;
s.Numeros(a,b);
s.Numeros(3.2,5.7);
s.Numeros("Monillo007");
}
}
Al ejecutar el código anterior obtendremos lo siguiente:
Método que recibe enteros.
Método que recibe flotantes.
Método que recibe una cadena: Monillo007
Al utilizar objetos en lugar de tipos primitivos o cadenas se vuelve más interesante. Veamos lo que sucede cuando se tiene un método sobrecargado que en una de sus versiones toma un Animal como parámetro y en otra un Caballo.
class Animal { }
class Caballo extends Animal{ }
class Animales{
public void MetodoSobrecargado(Animal a){
System.out.println("Método de parámetro Animal...");
}
public void MetodoSobrecargado(Caballo c){
System.out.println("Método de parámetro Caballo...");
}
public static void main(String... args){
Animales as = new Animales();
Animal objetoAnimal = new Animal();
Caballo objetoCaballo = new Caballo();
as.MetodoSobrecargado(objetoAnimal);
as.MetodoSobrecargado(objetoCaballo);
}
}
Al ejecutar el código anterior obtenemos:
Método de parámetro Animal...
Método de parámetro Caballo...
Como era de esperarse, cada objeto manda a llamar al método que le corresponde de acuerdo a su tipo, pero qué pasa si creamos una referencia Animal sobre un objeto Caballo...
Animal objetoAnimalRefCaballo = new Caballo();
as.MetodoSobrecargado(objetoAnimalRefCaballo);
El resultado es:
Método de parámetro Animal...
Aunque en tiempo de ejecución el objeto es un Caballo y no un Animal, la elección de qué método sobrecargado invocar no se realiza dinámicamente en tiempo de ejecución, el tipo de referencia, no el objeto actual, es el que determina qué método es el que se ejecutará.
Reglas de la sobrecarga y sobreescritura de métodos::
Ahora que hemos visto ambas formas de reescribir métodos, revisemos las reglas y diferencias entre ambos tipos de reescritura:
+Argumentos: En un método sobrecargado los argumentos deben de cambiar mientras que en un método sobreescrito NO deben cambiar.
+ El tipo de retorno: En un método sobrecargado el tipo de retorno puede cambiar, en un método sobreescrito NO puede cambiar, excepto por subtipos del tipo declarado originalmente.
+ Excepciones: En un método sobrecargado las excepciones pueden cambiar, en un método sobreescrito pueden reducirse o eliminarse pero NO deben de arrojarse excepciones nuevas o más amplias.
+ Acceso: En un método sobrecargado puede cambiar, en un método sobreescrito el acceso NO debe de hacerse más restrictivo(puede ser menos restrictivo).
+ Al invocar: En un método sobrecargado los argumentos son los que determinan qué método es el que se invocará, en un método sobreescrito el tipo de objeto determina qué método es elegido.
Esto es todo con la sobrecarga de métodos en Java. ¿Alguna duda? deja tu comentario.
Modificador de Acceso.
Los modificadores de acceso son palabras clave que se usan para especificar la accesibilidad declarada de un miembro o un tipo. En esta sección se presentan los cuatro modificadores de acceso:
Pueden especificarse los siguientes cinco niveles de accesibilidad con los modificadores de acceso:
public: el acceso no está restringido.protected: el acceso está limitado a la clase contenedora o a los tipos derivados de la clase contenedora.Internal: el acceso está limitado al ensamblado actual.
protected internal: el acceso está limitado al ensamblado actual o a los tipos derivados de la clase contenedora.
private: el acceso está limitado al tipo contenedor.
En esta sección también se presenta lo siguiente:
- Niveles de accesibilidad: usar los cuatro modificadores de acceso para declarar cinco niveles de accesibilidad.
- Dominio de accesibilidad: especifica en qué secciones del programa se puede hacer referencia a dicho miembro.
- Restricciones en el uso de niveles de accesibilidad: un resumen de las restricciones sobre usar niveles de accesibilidad declarados.
- Los modificadores de acceso nos introducen al concepto de encapsulamiento. El encapsulamiento busca de alguna forma controlar el acceso a los datos que conforman un objeto o instancia, de este modo podríamos decir que una clase y por ende sus objetos que hacen uso de modificadores de acceso (especialmente privados) son objetos encapsulados.Los modificadores de acceso permiten dar un nivel de seguridad mayor a nuestras aplicaciones restringiendo el acceso a diferentes atributos, métodos, constructores asegurándonos que el usuario deba seguir una "ruta" especificada por nosotros para acceder a la información.Es muy posible que nuestras aplicaciones vayan a ser usadas por otros programadores o usuarios con cierto nivel de experiencia; haciendo uso de los modificadores de acceso podremos asegurarnos de que un valor no será modificado incorrectamente por parte de otro programador o usuario. Generalmente el acceso a los atributos se consigue por medio de los métodos get y set, pues es estrictamente necesario que los atributos de una clase sean privados.Nota: Siempre se recomienda que los atributos de una clase sean privados y por tanto cada atributo debe tener sus propios métodos get y set para obtener y establecer respectivamente el valor del atributo.Nota 2: Siempre que se use una clase de otro paquete, se debe importar usando import. Cuando dos clases se encuentran en el mismo paquete no es necesario hacer el import pero esto no significa que se pueda acceder a sus componentes directamente.Veamos un poco en detalle cada uno de los modificadores de acceso
Modificador de acceso private
El modificador private en Java es el más restrictivo de todos, básicamente cualquier elemento de una clase que sea privado puede ser accedido únicamente por la misma clase por nada más. Es decir, si por ejemplo, un atributo es privado solo puede ser accedido por lo métodos o constructores de la misma clase. Ninguna otra clase sin importar la relación que tengan podrá tener acceso a ellos.En el ejemplo anterior vemos lo que mencioné al comiendo, tenemos un atributo privado y permitimos el acceso a él únicamente por medio de los métodos de get y set, notemos que estos métodos son públicos y por tanto cualquiera puede accederlos. Lo realmente interesante con los métodos get y set es que nos permiten realizar cualquier operación como por ejemplo llevar una cuenta de la veces que se estableció el valor para el atributo permitiéndonos mantener nuestro sistema sin problemas. También debemos notar que debido a que los métodos get y set son propios de la clase no tienen problemas con acceder al atributo directamente.El modificador por defecto (default)
Java nos da la opción de no usar un modificador de acceso y al no hacerlo, el elemento tendrá un acceso conocido como defaulto acceso por defecto que permite que tanto la propia clase como las clases del mismo paquete accedan a dichos componentes (de aquí la importancia de declararle siempre un paquete a nuestras clases).Modificador de acceso protected
El modificador de acceso protected nos permite acceso a los componentes con dicho modificador desde la misma clase, clases del mismo paquete y clases que hereden de ella (incluso en diferentes paquetes). Veamos:Modificador public
El modificador de acceso public es el más permisivo de todos, básicamente public es lo contrario a private en todos los aspectos (lógicamente), esto quiere decir que si un componente de una clase es public, tendremos acceso a él desde cualquier clase o instancia sin importar el paquete o procedencia de ésta.A continuación y ya para finalizar, pondré una pequeña tabla que resume el funcionamiento de los modificadores de acceso en Java.Mo di fi ca dor La misma cla se Mismo pa que te Sub clase Otro pa que te pri vate Sí No No No de fault Sí Sí No No pro tec ted Sí Sí Sí/No No pu blic Sí Sí Sí Sí Debo aclarar algo en el caso del modificador de acceso protected y el acceso desde suclases. Es un error común pensar que se puede crear un objeto de la clase madre y luego acceder al atributo con acceso protected sin problemas, sin embargo esto no es cierto, puesto que el modificador protected lo que nos permite es acceder al atributo heredado desde el ámbito de la clase hija y no directamente. Sé que esto es un poco confuso así que te invito a ver el video explicativo al final de esta sección para quedar más claros.Eso ha sido todo en esta sección, no te olvides de dejar tus comentarios y de suscribirte a nuestras redes sociales para estar al tanto de las novedades en el sitio.Accede al curso más completo de Java :). Aprende a programar en Java como no te enseñan en el colegio ni la universidad - AQUÍ LES DEJO UN VÍDEO EXPLICATIVO:
Ya mencionamos la diferencia entre variables de instancia, de clase y locales. Consideremos de nuevo la clase Circulo, con dos miembros dato, el radio específico para cada círculo y el número PI que tiene el mismo valor para todos los círculos. La primera es una variable de instancia y la segunda es una variable de clase.
Para indicar que el miembro PI es una variable de clase se le antepone el modificador static. El modificador final indica que es una constante que no se puede modificar, una vez que la variable PI ha sido inicializada.
Definimos también una función miembro denominada calcularArea que devuelva el área del círculo
Para calcular el área de un círculo, creamos un objeto circulo de la clase Circulo dando un valor al radio. Desde este objeto llamamos a la función miembro calcularArea.

Si PI y radio fuesen variables de instancia
Sea una clase denominada Alumno con dos miembros dato, la nota de selectividad, y un miembro estático denominado nota de corte. La nota es un atributo que tiene un valor distinto para cada uno de los alumnos u objetos de la clase Alumno, mientras que la nota de corte es un atributo que tiene el mismo valor para a un conjunto de alumnos. Se define también en dicha clase una función miembro que determine si está (true) o no (false) admitido.
Creamos ahora un array de cuatro alumnos y asignamos a cada uno de ellos una nota.
Un miembro dato estático de una clase se puede acceder desde un objeto de la clase, o mejor, desde la clase misma.
Para indicar que el miembro PI es una variable de clase se le antepone el modificador static. El modificador final indica que es una constante que no se puede modificar, una vez que la variable PI ha sido inicializada.
Definimos también una función miembro denominada calcularArea que devuelva el área del círculo
public class Circulo{
static final double PI=3.1416;
double radio;
public Circulo(double radio){
this.radio=radio;
}
double calcularArea(){
return (PI*radio*radio);
}
}
|
Circulo circulo=new Circulo(2.3);
System.out.println("área: "+circulo.calcularArea());
Veamos ahora las ventajas que supone declarar la constante PI como miembro estático.
Si PI y radio fuesen variables de instancia
public class Circulo{
double PI=3.1416;
double radio;
//....
}
Creamos tres objetos de la clase Circulo, de radios 3.2, 7.0, y 5.4Circulo circulo1=new Circulo(3.2); Circulo circulo2=new Circulo(7.0); Circulo circulo3=new Circulo(5.4);Al crearse cada objeto se reservaría espacio para el dato radio (64 bits), y para el dato PI (otros 64 bits). Véase la sección tipos de datos primitivos. Como vemos en la parte superior de la figura, se desperdicia la memoria del ordenador, guardando tres veces el mismo dato PI.
public class Circulo{
static double PI=3.1416;
double radio;
//....
}
Declarando PI estático (static), la variable PI queda ligada a la clase Circulo, y se reserva espacio en memoria una sóla vez, tal como se indica en la parte inferior de la figura. Si además la variable PI no cambia, es una constante, le podemos anteponer la palabra final.public class Circulo{
static final double PI=3.1416;
double radio;
//....
}
Miembros estáticos
Las variables de clase o miembros estáticos son aquellos a los que se antepone el modificador static. Vamos a comprobar que un miembro dato estático guarda el mismo valor en todos los objetos de dicha clase.Sea una clase denominada Alumno con dos miembros dato, la nota de selectividad, y un miembro estático denominado nota de corte. La nota es un atributo que tiene un valor distinto para cada uno de los alumnos u objetos de la clase Alumno, mientras que la nota de corte es un atributo que tiene el mismo valor para a un conjunto de alumnos. Se define también en dicha clase una función miembro que determine si está (true) o no (false) admitido.
public class Alumno {
double nota;
static double notaCorte=6.0;
public Alumno(double nota) {
this.nota=nota;
}
boolean estaAdmitido(){
return (nota>=notaCorte);
}
}
|
Alumno[] alumnos={new Alumno(5.5), new Alumno(6.3),
new Alumno(7.2), new Alumno(5.0)};
Contamos el número de alumnos que están admitidos int numAdmitidos=0;
for(int i=0; i<alumnos.length; i++){
if (alumnos[i].estaAdmitido()){
numAdmitidos++;
}
}
System.out.println("admitidos "+numAdmitidos);
Accedemos al miembro dato notaCorte desde un objeto de la clase Alumno, para cambiarla a 7.0alumnos[1].notaCorte=7.0;Comprobamos que todos los objetos de la clase Alumno tienen dicho miembro dato estático notaCorte cambiado a 7.0
for(int i=0; i<alumnos.length; i++){
System.out.println("nota de corte "+alumnos[i].notaCorte);
}
El miembro dato notaCorte tiene el modificador static y por tanto está ligado a la clase más que a cada uno de los objetos de dicha clase. Se puede acceder a dicho miembro con la siguiente sintaxisNombre_de_la_clase.miembro_estáticoSi ponemos
Alumno.notaCorte=6.5;
for(int i=0; i<alumnos.length; i++){
System.out.println("nota de corte "+alumnos[i].notaCorte);
}
Veremos que todos los objetos de la clase Alumno habrán cambiado el valor del miembro dato estático notaCorte a 6.5.Un miembro dato estático de una clase se puede acceder desde un objeto de la clase, o mejor, desde la clase misma.
HERENCIA:
¿QUÉ ES LA HERENCIA EN PROGRAMACIÓN ORIENTADA A OBJETOS?
Muchas veces distintos objetos comparten campos y métodos que hacen aproximadamente lo mismo (por ejemplo almacenar y devolver un nombre del ámbito humano con el que se designa al objeto, como el título de un álbum de música, el título de un libro, el título de una película, etc.).
Por ejemplo en un proyecto que utilice objetos Taxi y objetos Autobus podríamos encontrarnos algo así:
Para una aplicación de gestión de una empresa de transporte que tenga entre sus vehículos taxis y autobuses podríamos tener otra clase denominada FlotaCirculante donde tendríamos posibilidad de almacenar ambos tipos de objeto (por ejemplo taxis en un ArrayList y autobuses en otro ArrayList) como reflejo de los vehículos que se encuentran en circulación en una fecha dada. Esas listas conllevarían una gestión para añadir o eliminar vehículos de la flota circulante, modificar datos, etc. resultando que cada una de las listas necesitaría un tratamiento o mantenimiento.
Si nos fijamos en el planteamiento del problema, encontramos lo siguiente:
a) La definición de clases nos permite identificar campos y métodos que son comunes a Taxis y Autobuses. Si implementamos ambas clases tal y como lo venimos haciendo, incurriremos en duplicidad de código. Por ejemplo si el campo matricula es en ambas clases un tipo String, el código para gestionar este campo será idéntico en ambas clases.
b) La definición de clases nos permite identificar campos y métodos que difieren entre una clase y otra. Por ejemplo en la clase Taxi se gestiona información sobre un campo denominado numeroDeLicencia que no existe en la clase Autobus.
c) Conceptualmente podemos imaginar una abstracción que engloba a Taxis y Autobuses: ambos podríamos englobarlos bajo la denominación de “Vehículos”. Un Taxi sería un tipo de Vehiculo y un Autobus otro tipo de Vehiculo.
d) Si la empresa añade otros vehículos como minibuses, tranvías, etc. manteniendo la definición de clases tal y como la veníamos viendo, seguiríamos engrosando la duplicidad de código. Por ejemplo, un minibús también tendría matrícula, potencia… y los métodos asociados.
La duplicidad de código nos implicará problemas de mantenimiento. Por ejemplo inicialmente tenemos una potencia en caballos y posteriormente queremos definirla en kilowatios. O tenemos simplemente que modificar el código de un método que aparece en distintas clases. El tener el código duplicado nos obliga a tener que hacer dos o más modificaciones en sitios distintos. Pueden ser dos modificaciones, tres, cuatro o n modificaciones dependiendo del número de clases que se vieran afectadas, y esto a la larga genera errores al no ser el mantenimiento razonable.
En la clase FlotaCirculante también tendremos seguramente duplicidades: por un lado un ArrayList de taxis y por otro un ArrayList de autobuses, por un lado una operación de adición de taxis y otra operación de adición de autobuses, por un lado una operación para mostrar los elementos de la lista de taxis y otra para los elementos de la lista de autobuses…
¿No sería más razonable, si una propiedad o método va a ser siempre común para varios tipos de objetos, que estuviera localizada en un sitio único del que ambos tipos de objeto “bebieran”? En los lenguajes con orientación a objetos la solución a esta problemática se llama herencia. La herencia es precisamente uno de los puntos clave de este tipo de lenguajes.
La herencia nos permite definir una clase como extensión de otra: de esta manera decimos “la clase 1.1 tiene todas las características de la clase 1 y además sus características particulares”. Todo lo que es común a ambas clases queda comprendido en la clase “superior”, mientras lo que es específico, queda restringido a las clases “inferiores”. En nuestro ejemplo definiríamos una clase denominada Vehiculo, de forma que la clase Taxi tuviera todas las propiedades de la clase Vehiculo, más algunas propiedades y métodos específicos. Lo mismo ocurriría con la clase Autobus y otras que pudieran “heredar” de Vehiculo. Podríamos seguir creando clases con herencia en un número indefinido: tantas como queramos. Si piensas en el API de Java, hay cientos de clases que heredan de clases jerárquicamente superiores como la clase Object. En un proyecto propio, podremos tener varias clases que hereden de una clase común.
Esquema básico de herencia
Otro ejemplo: para la gestión de un centro educativo, podemos definir la clase Persona que comprende los campos y métodos comunes a todas las personas. Luego podremos definir la clase Estudiante, que es extensión de Persona, y comprende los campos y métodos de la clase superior, más algunos específicos. También podrían heredar de Persona la clase Profesor, la clase Director, la clase JefeDeEstudios, la clase PersonalAdministrativo, etc.
La primera aproximación a la herencia en Java la plantearemos para plasmar todo lo que hemos discutido en párrafos anteriores: en vez de definir cada clase por separado con operaciones o campos duplicados en cierta medida, definiremos una clase “padre” que contendrá todas las cosas que tengan en común otras clases. Luego definiremos las otras clases “hijo” como extensión de la clase padre, especificando para ellas únicamente aquello que tienen específico y distinto de la clase padre. Una característica esencial de la herencia es que permite evitar la duplicidad de código: aquello que es común a varias clases se escribe solo una vez (en la clase padre). La duplicidad de información, ya sea código o datos, es algo que por todos los medios hay que tratar de evitar en los sistemas informáticos por ser fuente de errores y de problemas de mantenimiento.
Si nos remitimos al esquema básico de herencia donde hemos representado las clases Vehiculo, Taxi y Autobus, la clase Vehiculo diremos que actúa como clase padre de las clases Taxi y Autobus. Vehiculo recogería todo aquello que tienen o hacen en común taxis y autobuses. Aunque aquello que tienen en común se agrupa, tanto Taxi como Autobus tienen sus campos o métodos específicos.
Que una clase derive de otra en Java se indica mediante la palabra clave “extends”. Por eso muchas veces se usa la expresión “esta clase es extensión de aquella otra”. En los diagramas de clase la herencia se representa con una flecha de punta vacía. El aspecto en BlueJ sería algo así:
Este diagrama refleja que la clase FlotaCirculante usa a la clase Vehiculo, mientras que las clases Taxi y Autobus heredan de la clase Vehiculo (son extensión de la clase Vehiculo). Podemos decir que la clase hijo extiende (hace más extensa) a la clase padre. Una clase de la que derivan otras se denomina clase padre, clase base o superclase. Las clases que heredan se denominan clases derivadas, clases hijos o subclases. A partir de ahora los términos superclase y subclase son los que usaremos con más frecuencia. Una subclase podrá tener un acceso “más o menos directo” a los campos y métodos de la superclase en función de lo que defina el programador, como veremos más adelante. Para referirnos a la herencia también se usa la terminología “es un”. En concreto decimos que un objeto de la subclase es un objeto de la superclase, o más bien en casos concretos, que un Taxi es un Vehiculo, o que un Estudiante es una Persona. Sin embargo, al revés esto no es cierto, es decir, un Vehiculo no es un Taxi, ni una Persona es un Estudiante.
Dos subclases que heredan de una clase no deben tener información duplicada. Esto se consideraría un mal diseño. La información común debe estar en una superclase.
EJERCICIO
Se plantea desarrollar un programa Java que permita la gestión de una empresa agroalimentaria que trabaja con tres tipos de productos: productos frescos, productos refrigerados y productos congelados. Todos los productos llevan alguna información común como fecha de caducidad y número de lote, pero a su vez cada tipo de producto lleva alguna información específica, por ejemplo los productos congelados deben llevar la temperatura de congelación recomendada. Hay tres tipos de productos congelados: congelados por aire, congelados por agua y congelados por nitrógeno.
La empresa gestiona envíos a través de diferentes medios, y un envío puede contener cierto número de productos frescos, refrigerados o congelados. Identificar las 7 clases Java principales que podemos identificar dada la forma de funcionamiento de la empresa. Crear un esquema con las relaciones de herencia y/o uso entre las distintas clases.
Herencia
La herencia es la propiedad por la cual cualquier subclase incluye automáticamente cualquier miembro public o protected de la superclase. La clase hija puede usar estos miembros directamente, sin necesidad de crear una instancia de la superclase.
Para que una clase herede de otra, esta debe usar la palabra reservada extends en su definición: public class HolaMundo extends Saludo {}
Ojo! Java no permite herencia múltiple, una clase solo puede heredar, directamente, de otra. Por lo que solo se permite un clase después de extends.
En Java, la única clase que no tiene padre es java.lang.Object, todas las clases heredan de ella. Las siguientes dos definiciones son iguales, ya que el extends de la segunda lo realiza Java automáticamente en la primera:
public class Zoo { }
public class Zoo extends java.lang.Object { }
Ojo! Si se define una clase como final no se puede heredar de ella.
En el exámen OCA solo aparecerán clases public y default. Estas últimas solo pueden ser accedidas por subclases y clases dentro del mismo paquete.
Si consideramos:
class C extends B {}
La herencia define una relación, C es una clase de B. Lo que significa que C puede ser usada en todos los lugares donde B es usada, C puede sustituir a B.
Complicándolo:
class A {}
class B extends A {}
class C extends B {}
C también puede sustituir a A, ya que B puede hacerlo. Esto también aplica para interfaces.
Constructores y herencia
La primera sentencia de un constructor es una llamada a otro constructor de esa clase usando this()o al constructor de la clase padre usando super(). Usar super() o this() en una línea que no sea la primera, provocará un error de compilación, esto impide que puedas usar ambos a la vez.
super() tendrá argumentos si el constructor de la clase padre los tiene. Es más, Ojo! en este caso, es obligatorio que la clase hija defina un constructor y use super() con argumentos.
Ejemplo:
public class Mamifero {
public Mamifero(int edad) { }
}
public class Elefante extends Mamifero { //No compilará debe definir
} //un constructor
public class Elefante extends Mamifero {
public Elefante() {
super(11)
}
}
Usar super() en una clase que no usa extends en su definición funcionará, ya que llamará al constructor de java.lang.Object.
El constructor de la clase padre siempre es llamado antes que el constructor de la clase hija.
Ejemplo:
class Primate {
public Primate() {
System.out.println("Primate");
}
}
class Mono extends Primate {
public Mono() {
System.out.println("Mono");
}
}
public class Chimpance extends Mono {
public static void main(String[] args) {
new Chimpance();
}
}
La salida será Primate Mono. Es así porqué Java ejecutará super() en la primera línea de cada constructor y esto hará que se ejecute primero el constructor de la clase padre, y así en cadena.
OJo! El constructor de la superclase no se hereda, por tanto, tampoco puede ser sobrescrito.
Llamando a miembros heredados
Una clase hija solo puede usar directamente los miembros public y protected de la clase padre. Pero esto no quiere decir que no pueda acceder a un miembro private, puede acceder indirectamente usando un miembro public o protected.
Ejemplo:
class Fish {
protected int size;
private int age;
public Fish(int age) { this.age = age; }
public int getAge() { return age; }
}
public class Shark extends Fish {
private int numberOfFins = 8;
public Shark(int age) {
super(age);
this.size = 4;
}
public void displaySharkDetails() {
System.out.print("Shark with age: " + getAge());
System.out.print(" and "+ size +" meters long");
System.out.print(" with "+ numberOfFins +" fins");
}
}
Aunque age es private, la clase hija usa el método public para acceder a su valor. También se podría haber accedido con super.getAge() y this.getAge().
Sobreescribiendo métodos
Al heredar los miembros de una clase, se puede dar el caso que se hereden métodos con la misma firma (nombre, número de argumentos, orden, tipo de retorno). Como hemos visto se puede usar super.metodo() para acceder al método de la clase padre.
Ojo! desde Java 1.5 el tipo de retorno puede ser distinto siempre que sea covariante, pero esto no aplica a tipos primitivos.
El compilador realiza las siguientes comprobaciones cuando se sobrescribe un método no private.
- El método de la clase hija tiene que tener la misma firma que el método de la clase padre.
- El método de la clase hija debe ser al menos tan accesible como el método de la clase padre.
- Si el método de la clase padre tiene definida excepción, el método de clase hija puede no tenerla o de tenerla tiene que ser la misma o una subclase de esa. Si el método de la clase padre no tiene excepción, el de la hija no puede tenerla.
- Si el método retorna un valor, debe ser el mismo o covariante (siempre que el original no sea un tipo primitivo).
- Si un método es static en la clase padre también debe serlo en la clase hija. Y si no lo es tampoco puede serlo en la hija.
- No se puede sobrescribir un método marcado como final.
Estas reglas aplican a métodos public o protected, si el método de la clase padre fuera private no sería visible por la clase hija, por tanto, esa clase podría declarar un método igual sin temor a romper ninguna de las reglas anteriores.
Ojo! Si dos métodos tienen el mismo nombre pero diferente firma, los métodos están sobrecargados, no sobrescritos.
Ejemplo: aplicando la regla de covarianza y sobrescritura
class Base {
public Object getValue() { return new Object(); }
}
class Base2 extends Base {
public String getValue() { return "hola"; }
}
public class Test {
public static void main(String[] args) {
Base b = new Base2();
System.out.println(b.getValue());
}
}
Salida: hola
Devuelve hola porque el método que se usa (si no es static) siempre es el que pertenece al objeto, no al tipo de variable. En este caso el tipo de la variable es Base y la referencia al objeto es Base2. Con las variables de instancia sería al revés.
Los métodos static y las variables de clase no son sobrescritos, el acceso es determinado en tiempo de compilación por el tipo de variable, en lugar del tipo de objeto referenciado por la variable, como ocurre con los métodos de instancia.
class A{
private int i = 10;
public void f(){}
public void g(){}
}
class B extends A{
public int i = 20;
public void g(){}
}
public class C{
A b = new B(); //1
}
En //1 se está creando una variable de tipo A que hace referencia a un objeto de tipo B. Debido a esto no podemos hacer b.i porque es private en A. b.f() es legal porque existe un método f() en A. Para acceder a i tendríamos que crear una variable de tipo B: B b = new B();
Ocultando un método
Se puede ocultar un método declarándolo como static y sobrescribiéndolo (en cursiva porque realmente no sobrescribimos, ya hemos dicho que no se puede) en la clase hija.
Al crear una variable del tipo de la subclase se ejecutará el método de la clase padre.
Ejemplo:
public class Marsupial {
public static boolean isBiped() {
return false;
}
public void getMarsupialDescription() {
System.out.println("Marsupial walks on two legs: "+isBiped());
}
}
public class Kangaroo extends Marsupial {
public static boolean isBiped() {
return true;
}
public void getKangarooDescription() {
System.out.println("Kangaroo hops on two legs: "+isBiped());
}
public static void main(String[] args) {
Kangaroo joey = new Kangaroo();
joey.getMarsupialDescription();
joey.getKangarooDescription();
}
}
Salida:
Marsupial walks on two legs: false
Kangaroo hops on two legs: true
En el ejemplo hemos ocultado el método isBiped(), lo cual permite que en tiempo de ejecución se use el método padre. Si quitamos el modificador static la salida sería:
Marsupial walks on two legs: true Kangaroo hops on two legs: true
Ojo! Si además de static el método es marcado como final, no se puede ocultar al igual que no se puede sobrescribir.
Ojo! Java no permite que las variables sean sobrescritas pero sí ocultadas. Siempre se puede acceder a la variable de la clase padre usando super.variable.
Clases Abstractas
Una clase abstracta es aquella definida con la palabra clave abstract y no puede ser instanciada (no puedes usar new con una clase abstracta).
Necesitas una clase abstracta cuando es necesario que la clase padre no sea instanciada y ciertos métodos sean, obligatoriamente, implementados.
Ojo! No es obligatorio que una clase abstracta contenga métodos abstractos, pero un método abstracto solo puede ser definido en una clase abstracta. Tal método no puede ser implementado en la clase padre, solo definido.
Tanto las clases como los métodos abstractos no pueden ser definidos como final o private.
Es obligatorio que la primera subclase de una clase abstracta (clase concreta) implemente todos los métodos abstractos de la clase padre.
Si una clase abstracta hereda de otra clase abstracta, una subclase no abstracta de la primera tiene obligación de implementar todos los métodos abstractos de ambas. Aunque si la clase abstracta hija ya proporciona la implementación de alguno de los métodos, la clase no abstracta que hereda de esta ya no tiene que hacerlo.
Resumiendo las reglas que aplican a clases abstractas:
- Una clase abstracta no puede ser instanciada directamente (new).
- Una clase abstracta puede tener de 0 a n métodos abstractos.
- Una clase abstracta no puede ser ni private ni final.
- Una clase abstracta puede heredar otra clase abstracta.
- La primera subclase no abstracta (clase concreta) debe implementar todos los métodos abstractos heredados.
- Una subclase de una clase no abstracta no puede ser declarada abstracta.
Resumiendo las reglas que aplican a métodos abstractos:
- Un método abstracto solo puede ser definido en una clase abstracta.
- Un método abstracto no puede ser declarado como final o private.
- Un método abstracto no puede ser implementado en la clase abstracta en la que es declarado.
- La implementación de un método abstracto en una subclase, sigue las mismas reglas de sobrescritura ya mencionadas.
Ejemplo:
public abstract class Animal {
public abstract String getName();
}
public abstract class BigCat extends Animal {
public String getName() {
return "BigCat";
}
public abstract void roar();
}
public class Lion extends BigCat {
public void roar() {
System.out.println("The Lion lets out a loud ROAR!");
}
}
Ojo! Como se puede observar solo se usa la palabra abstract en al definición no en la implementación. Como la subclase abstracta BigCat proporciona la implementación del método abstracto getName(), la subclase concreta no está obligada a implementarlo.
Implementando Interfaces
Una interface es una tipo de dato abstracto que define una lista de métodos públicos y abstractos, que cualquier clase que implemente la interface debe implementar.
Una interface se declara usando la palabra reservada interface. Y puede ser implementada por cualquier clase usando la palabra reservada implements. Una clase puede implementar n interfaces.
Reglas a seguir en la creación de una interface:
- Una interface no puede ser instanciada directamente (new).
- Una interface puede tener 0 o n métodos.
- Una interface no puede ser final, private o protected, no compilará.
- Una interface por defecto se asume que es public o default e incluye el modificador abstract.
- Se asume que todos los métodos de una interfaces son public y abstract (no es necesario escribirlo). Por tanto, marcarlos como private, protected o final, dará un error de compilación.
Ejemplo: Las siguientes interfaces son equivalentes
public interface CanFly {
void fly(int speed);
abstract void takeoff();
public abstract double dive();
}
public abstract interface CanFly {
public abstract void fly(int speed);
public abstract void takeoff();
public abstract double dive();
}
El compilador convierte la primera en la segunda en tiempo de ejecución.
Heredando una Interface
Para heredar una interfaces hay que tener en cuenta:
- Una interface no puede heredar una clase, ni implementar otra interface. Solo puede heredar otra interface. Ojo! Una interface puede heredar n interfaces.
- Una clase no puede heredar una interface solo puede implementarla.
- Una clase abstracta que implemente una interface no tiene que implementar los métodos de esta.
- La primera clase concreta que implemente una interface o herede una clase abstracta, que a su ve implementa una interface, debe proporcionar la implementación de todos los métodos abstractos heredados.
Implementando múltiples interfaces
Cuando una clase implementa varias interfaces con el mismo método y la misma firma, esta clase debe proporcionar la implementación del método solo una vez.
Si la firma cambia, mismo nombre pero distintos parámetros, estaríamos ante una sobrecarga del método y habría que implementar un método por interface.
Ojo! Si la firma cambia, de tal forma que todo es igual excepto el tipo de retorno, no compilará.
Ejemplo:
public interface Herbivore {
public int eatPlants();
}
public interface Omnivore {
public void eatPlants();
}
public class Bear implements Herbivore, Omnivore {
public int eatPlants() { // NO COMPILA
System.out.println("Eating plants: 10");
return 10;
}
public void eatPlants() { // NO COMPILA
System.out.println("Eating plants");
}
}
Incluso no compilará si una interface trata de heredar esas dos en conflicto. O una clase abstracta, que no tiene obligación de implementar los métodos, trata de implementarlas.
public interface Supervore extends Herbivore, Omnivore {} // NO COMPILA
public abstract class AbstractBear
implements Herbivore, Omnivore {} // NO COMPILA
Variables de Interface
Puntos a tener en cuenta:
- Se asume que son public, static y final.
- Tienen que ser inicializadas en el momento de su declaración.
- Se usan a modo de constantes, por tanto, se suelen declarar en mayúsculas.
- Se pueden implementar n interface con la misma variable, pero su posterior uso no puede ser ambiguo, hay que identificar la interface del campo a usar.
Métodos default de Interface
Este es un tipo de método introducido con la release Java 8. Un método default es un método definido con la palabra clave default, que solo puede ser declarado dentro de una interface.
Puntos a tener en cuenta:
- Son implementados en la propia interface.
- La clase que implemente la interface no tiene obligación de implementarlo, ya que lo está en la propia interface, pero puede hacerlo y aplicaría esta implementación.
- Estos métodos no son abstractos, como el resto de los métodos de una interface. Sí se asume que son public como el resto.
- Una interface que hereda otra interface con métodos default, puede redefinirlos o incluso quitarles la condición de default, haciendo que para las clases que implementen esta segunda interface sea obligatorio implementar el método.
- Si una clase hereda dos interfaces con métodos default idénticos, no compilará, excepto si esa clase sobreescribe el método.
Ejemplo:
public interface HasFins {
public default int getNumberOfFins() {
return 4;
}
default double getLongestFinLength() { //Se asume public
return 20.0;
}
public default boolean doFinsHaveScales() {
return true;
}
}
public interface SharkFamily extends HasFins {
public default int getNumberOfFins() {
return 8;
}
public double getLongestFinLength();
public boolean doFinsHaveScales() { // NO COMPILA
return false;
}
}
No compila porque está implementando el método sin usar default. El resto es correcto, getNumerOfFins() es sobrescrito y getLongestFinLength() es reemplazado por un método abstracto, para el cual la clase que implemente SharkFamily deberá proporcionar una implementación.
Métodos static de Interface
Java 8 proporciona métodos static dentro de una interface. Estos métodos son definidos con la palabra reservada static y funcionan igual que un método static de una clase. Puntos a tener en cuenta:
- De un método static se asume que es public.
- Un método static se implementa en la propia interface.
- Para referenciar un método static hay que usar el nombre de la interface.
- Si una clase implementa dos interfaces con métodos static idénticos compilará, ya que estos métodos no se heredan y deben ser accedidos usando el nombre de la interface.
Ejemplo:
public interface Hop {
static int getJumpHeight() {
return 8;
}
}
public class Bunny implements Hop {
public void printDetails() {
System.out.println(Hop.getJumpHeight());
}
}
Polimorfismo
Es la propiedad por la que un objeto puede tomar diferentes formas. Gracias a esta propiedad un objeto de una clase puede asignarse a un objeto de su superclase o de una interface. Incluso el casting no es necesario.
A la inversa no compilaría, ya que el objeto de la superclase no tiene scope sobre las variables/métodos de esa subclase.
Ejemplo:
public class Primate {
public boolean hasHair() {
return true;
}
}
public interface HasTail {
public boolean isTailStriped();
}
public class Lemur extends Primate implements HasTail {
public boolean isTailStriped() {
return false;
}
public int age = 10;
public static void main(String[] args) {
Lemur lemur = new Lemur();
System.out.println(lemur.age);
HasTail hasTail = lemur;
System.out.println(hasTail.isTailStriped());
Primate primate = lemur;
System.out.println(primate.hasHair());
}
}
La salida es 10 false true. Se puede observar que solo se ha creado un objeto y este es usado para instanciar la interface y la superclase, esta es la esencia del polimorfismo.
Ejemplo:
class A{
A() { print(); }
void print() { System.out.println("A"); }
}
class B extends A{
int i = 4;
public static void main(String[] args){
A a = new B();
a.print();
}
void print() { System.out.println(i); }
}
Este código devuelve 0 4. El método print() es sobrescrito en la clase B, y debido al polimorfismo el método que se ejecutará será el de la clase a la que haga referencia el objeto. Cuando el objeto de B es creado, primero llama al constructor de la superclase, pero como el objeto hace referencia a B el método ejecutado será el de B, e imprimirá 0 ya que i aún no ha sido inicializada (y toma el valor por defecto de una variable de clase de tipo int).
Objeto vs Referencia
En Java todos los objetos son accedidos por referencia, por tanto, un desarrollador nunca tiene acceso directo al objeto en sí.
El tipo de objeto determina que propiedades existen para el objeto en memoria. El tipo de referencia al objeto determina que métodos/variables son accesibles para dicho objeto.
Casting a Objetos
Estas son las reglas básicas cuando se realiza un casting:
- Realizar un casting de subclase a superclase no requiere un casting explícito.
- Realizar un casting de superclase a subclase sí requiere un casting explícito.
- El compilador no permitirá castear tipos no relacionados.
- Incluso cuando el código compilara, arrojaría una excepción (ClassCastException) en tiempo de ejecución, si el objeto siendo casteado no es una instancia de la clase.
Ejemplo:
public class Bird {}
public class Fish {
public static void main(String[] args) {
Fish fish = new Fish();
Bird bird = (Bird)fish; // No compila
}
}
No compila porque las clases no están relacionadas, para que ese casteo funcionara Fish debería heredar de Bird.
Métodos virtuales
Es la herramienta más importante del polimorfismo. Un método virtual es aquel cuya implementación no es determinada hasta el tiempo de ejecución. De hecho, todo método que no es final, static o private, es considerado un método virtual ya que puede ser sobrescrito.
Ejemplo:
public class Bird {
public String getName() {
return "Unknown";
}
public void displayInformation() {
System.out.println("The bird name is: "+getName());
}
}
public class Peacock extends Bird {
public String getName() {
return "Peacock";
}
public static void main(String[] args) {
Bird bird = new Peacock();
bird.displayInformation();
}
}
La salida de este código es: The bird name is: Peacock
En tiempo de ejecución se sustituye el método getName() de la superclase, con el de la subclase.
Polimorfismo de parámetros
Es la propiedad de pasar como parámetro una instancia de una subclase, estando el parámetro definido con la superclase. El casteo se realiza automáticamente no tiene que definirse.
Ejemplo:
public class Reptile {
public String getName() {
return "Reptile";
}
}
public class Alligator extends Reptile {
public String getName() {
return "Alligator";
}
}
public class Crocodile extends Reptile {
public String getName() {
return "Crocodile";
}
}
public class ZooWorker {
public static void feed(Reptile reptile) {
System.out.println("Feeding reptile "+reptile.getName());
}
public static void main(String[] args) {
feed(new Alligator());
feed(new Crocodile());
feed(new Reptile());
}
}
Salida:
Feeding: Alligator
Feeding: Crocodile
Feeding: Reptile



No hay comentarios.:
Publicar un comentario