Sun Certified Java Programmer-Buenas Practicas

179
Sun Certified Java Programmer 6, CX-310-065 - Parte 1: Declaraciones y Controles de Acceso Este es el primer post dedicado a la certificación SCJP 6 (CX-310-065) . Definiremos algunos conceptos básicos del lenguaje Java así como sus reglas más básicas de funcionamiento. ¿Por qué es importante esto? Bueno esto es de vital importancia ya que en todo lenguaje de programación, y tecnología en general, es importante para entender sus funciones avanzadas, comenzar entendiendo las partes básicas, así como definir los términos que se usan, para que todos entendamos de lo que estamos hablando. Pues comencemos con el primer tema con un repaso de los conceptos básicos que debemos tener presentes, a modo de recordatorio. DECLARACIONES Y CONTROLES DE ACCESO Conceptos básicos: Recordemos que Java es un lenguaje orientado a objetos, así que comencemos con algunas definiciones de este paradigma: Clase: Una clase es una especie de plantilla que describe el estado y comportamiento que los objetos de su tipo soportan. Las clases son una forma de encapsular datos y las operaciones relacionadas con esos datos. Son la estructura fundamental del paradigma orientado a objetos. 1

Transcript of Sun Certified Java Programmer-Buenas Practicas

Page 1: Sun Certified Java Programmer-Buenas Practicas

Sun Certified Java Programmer 6, CX-310-065 - Parte 1: Declaraciones y Controles de Acceso

Este es el primer post dedicado a la certificación SCJP 6 (CX-310-065). Definiremos algunos conceptos básicos del lenguaje Java así como sus reglas más básicas de funcionamiento.

¿Por qué es importante esto? Bueno esto es de vital importancia ya que en todo lenguaje de programación, y tecnología en general, es importante para entender sus funciones avanzadas, comenzar entendiendo las partes básicas, así como definir los términos que se usan, para que todos entendamos de lo que estamos hablando.

Pues comencemos con el primer tema con un repaso de los conceptos básicos que debemos tener presentes, a modo de recordatorio.

DECLARACIONES Y CONTROLES DE ACCESO

Conceptos básicos:

Recordemos que Java es un lenguaje orientado a objetos, así que comencemos con algunas definiciones de este paradigma:

Clase: Una clase es una especie de plantilla que describe el estado y comportamiento que los objetos de su tipo soportan. Las clases son una forma de encapsular datos y las operaciones relacionadas con esos datos. Son la estructura fundamental del paradigma orientado a objetos. Objeto: Son las instancias de una clase. Tiene su propio estado, y el acceso a todos los comportamientos u operaciones definidas por su clase. Estado: Los valores asignados a las "variables de instancia" de un objeto constituyen el estado del objeto. Comportamiento (Métodos): Es donde la lógica de la clase es almacenada, donde los algoritmos son ejecutados y los datos son manipulados.

1

Page 2: Sun Certified Java Programmer-Buenas Practicas

Cuando comenzamos a trabajar con el paradigma orientado a objetos algunas veces es difícil entender la diferencia entre clase y objeto. Usando una analogía, podríamos decir que, en vez de ser programadores somos cocineros y estamos haciendo galletas, la clase sería el molde para las galletas y los objetos serían las galletas que se hagan con ese molde.

Identificadores y Palabras Reservadas:

Los nombres de las clases, variables y métodos (en general los nombres de todo lo que podemos declarar) son llamados identificadores. Los programadores de Java (y Sun) han creado convenciones para nombrar los métodos, variables y clases. Estos métodos serán mencionados más adelante en este mismo post.

Como todos los lenguajes de programación Java cuenta con palabras reservadas, tenemos que recordar que estas palabras reservadasno deben ser usadas como identificadores.

Herencia:

El centro de Java y de la programación orientada a objetos. Con la herencia podemos definir que una clase sea reusada en otras clases, podemos definir una clase general (súper-clase) y sub-clases que heredan de esta. La súper-clase no sabe nada de la existencia de las clases que heredan de ella pero todas las sub-clases que la heredan deben declarar explícitamente la relación de herencia que existe. Una sub-clase obtiene automáticamente acceso a las variables y métodos definidos en la súper-clase y también es libre de sobre escribir los métodos (hablaremos sobre sobre-escritura de métodos en un post posterior).

Interfaces:

El compañero perfecto de la herencia es el uso de las interfaces. Las interfaces son clases 100% abstractas que definen los métodos que una subclase debe implementar, pero no como debe hacerlo. Esto nos permite definir el comportamiento que esperamos que tenga una clase, sin decir exactamente cómo debe hacerlo. Gracias a las interfaces podemos lograr uno de los objetivos más buscados dentro

2

Page 3: Sun Certified Java Programmer-Buenas Practicas

del diseño orientado a objetos: bajo acoplamiento (del cual hablaremos en los siguientes posts).Identificadores y JavaBeans

En java existen reglas y acuerdos para denominar los diferentes tipos de datos y objetos existentes y creados por nosotros mismos a continuación explicamos brevemente cada uno de estos conceptos:

Identificador legal: Como dijimos: un identificador es el nombre que se le da a un elemento en Java (ya sea una clase, interface, variable, enumeración, etc). Para que un identificador sea legal se utilizan una serie de reglas explicadas posteriormente. Si un identificador no sigue las reglas necesarias para sea correcto o legal este arrojara un error. Para que el compilador detecte si un identificador es legal o no, utiliza las reglas establecidas para su correcto funcionamiento. Hablaremos de estas reglas en un momento. Convenciones de código Java de Sun: Son recomendaciones de Sun para nombrar a las clases, variables y métodos. Estándar de Nombres JavaBeans: Los requerimientos de nombres de la especificación JavaBeans.

Ahora pasaremos a hablar detalladamente de cada uno de estos:

Identificador legal:

Técnicamente un identificador legal debe estar compuesto por solo caracteres Unicode, números, símbolos de dólar ($) y caracteres de conexión (como el guion bajo "_").

Los identificadores legales deben cumplir con los siguientes requisitos:

Un identificador debe empezar con una letra, símbolo de dólar ($) o caracteres de conexión (_), nunca debe empezar con un número.

Después del primer carácter él identificador puede contener cualquier combinación de letras, símbolo de dólar ($), caracteres de conexión (_) o números.

No hay un límite de caracteres que un identificador pueda tener. No se pueden usar una palabra reservada como identificador.

3

Page 4: Sun Certified Java Programmer-Buenas Practicas

Los identificadores son "Case Sensitive", es decir, una clase "Carro" no es igual que una clase "cARRO".

*Nota: Usaremos el IDE Netbeans para ayudarnos a aclarar algunos ejemplos. Este IDE está actualmente en su versión 6.9, pero usaremos la versión 6.8 con lo cual no debería haber ningún problema.

Ejemplo de identificadores legales:

Como podemos observar, el IDE no marca ningún error, con lo cual deducimos que las declaraciones son correctas aunque algo extrañas.

Ejemplo de identificadores no legales:

Como podemos observar, esta vez el IDE si marca error ya que obviamente no estamos siguiendo las reglas antes mencionadas para que un identificador sea legal.

4

Page 5: Sun Certified Java Programmer-Buenas Practicas

Las palabras reservadas de Java son 50 (hasta la versión 1.6) y las mencionamos en la siguiente tabla. Recordamos que no podemos llamar a un identificador como uno de estas palabras reservadas.

abstract

boolean break byte case catch

char class const continue default

do

double else extends

final finally

float

for goto if implements

import instanceof

int interface

long native new package

private

protected

public

return short static

strictfp

super switch

synchronized

this throw

throws transient

try void volatile

while

assert enum

Cabe mencionar que aunque todas las palabras de la tabla anterior están reservadas no todas se usan en las versiones actuales de Java, y están "apartadas" por si en algún momento se les da un uso.

Convenciones de código Java de Sun:

Sun ha creado un conjunto de normas o convenciones o recomendaciones de codificación de Java, y se publicaron las normas en un documento titulado ingeniosamente "Convenciones de código Java", que se puede encontrar en la página de convenciones de código de Oracle.Aquí están las normas de nomenclatura que Sun recomienda:

Clases e interfaces: La primera letra debe ser mayúscula, si varias

5

Page 6: Sun Certified Java Programmer-Buenas Practicas

palabras se unen para formar un nombre cada palabra que lo conforma debe empezar con la primera letra mayúscula a esto se le llama "Camel Case", por ejemplo: 

Perro Cuenta SistemaDeAdministracionFinanciero

Las interfaces son típicamente adjetivos como:

Runnable Serializable Imprimible

Métodos: la primera letra debe ser minúscula y después se puede aplicar la regla Camel Case, típicamente son pares verbo-sustantivo, por ejemplo:

calculaPeso getBalance obtenConexion setNombreCliente

Variables: Similar a los métodos, Sun recomienda nombres cortos, significativos y fáciles de recordar.

nombreCompleto porcentajeInteres miString

Constantes: Las variables constantes son marcadas como "static" y "final". Estas se deben nombrarse con todas sus letras mayúsculas y separando cada palabra con guion bajo (_), por ejemplo:

ESTE_VALOR_NUNCA_CAMBIARA VARIABLE_ESTATICA

6

Page 7: Sun Certified Java Programmer-Buenas Practicas

Aunque no es necesario seguir estas convenciones para que nuestro código compile, el hacerlo nos ayudará cuando realicemos proyectos en equipo o que otros entiendan más fácilmente nuestro código.

Ahora hablaremos de los estándares JavaBeans que son acuerdos para nombrar a las clases, métodos "set" (establecer un valor) y "get" (obtener un valor), etc. Que tampoco estamos obligados a seguirlas pero es una buena práctica para nosotros y para tener más claro nuestro código.

Estándares JavaBeans

Los JavaBean son clases que contienen propiedades, estas propiedades son nombradas variables de instancia y por lo regular, aunque deberíamos tratar de que fuera siempre, las marcamos como privadas, la única forma de acceder a estas propiedades desde fuera de la clase es a través de métodos, los métodos que se utilizan para establecer valor a estas variables son llamados métodos "setters" y los métodos para obtener el valor son llamados "getters".

Sun establece unas reglas para nombrar a las propiedades, así como a sus métodos "setter" y "getter". Muchos frameworks se basan en que nuestro código sigue estas reglas para poder funcionar correctamente y, como toda regla, si no las seguimos corremos el riesgo de que algo no funcione como lo esperamos. 

Reglas para nombrar a las propiedades: Si la propiedad no es de tipo booleano, el prefijo para un método getter es "get", por ejemplo para una variable "nombre" su respectivo método getter será "getNombre" (aquí también se aplica la regla de Camel Case para separar cada palabra). También hay que tener en cuenta que no hay necesidad de que el nombre que sigue después del prefijo "get" sea igual al nombre de la propiedad, el nombre de la propiedad es irrelevante para el método "get" o "set".

Si la propiedad es un booleano el método getter para esta propiedad puede llevar el prefijo "get" o "is", para una propiedad booleana llamada "estado" su método get válido puede ser "getEstado" o "isEstado".

7

Page 8: Sun Certified Java Programmer-Buenas Practicas

El prefijo para el método setter de una propiedad es "set", por ejemplo "setNombre" para un atributo "nombre".

Como dije anteriormente en las propiedades también se aplica la regla camel case, es decir, después del prefijo "set", "get" o "is" la primera letra seguida es una letra mayúscula. El método "setter" debe ser marcado público con un valor de retorno void (vacio) y con un argumento o parámetro que represente al tipo de la propiedad, por ejemplo: "public void setNombre(String nombre){ … }" para una propiedad "nombre" del tipo String.

Los métodos "get" también deben ser marcados públicos con un valor de retorno que represente al tipo de la propiedad y/o con el tipo de argumento o parámetro del método set para esa propiedad, por ejemplo: "public String getNombre(){ return nombre; }" para una propiedad "nombre" del tipo String".

Estas reglas se extieden para proporcionar relgas especiales para los eventos que nuestro sistema pueda lanzar (usualmente en aplicaciones stand alone o de escritorio)

Un evento constituye un método para que una clase notifique a los usuarios de un objeto que algo interesante sucede al objeto, como, por ejemplo, que se ha hecho clic en un control de una interfaz gráfica de usuario. Los eventos JavaBean soportan especificaciones, que permiten a los componentes notificar a otros cuando algo sucede. El modelo de eventos se utiliza a menudo en aplicaciones de interfaz gráfica de usuario (GUI) cuando un evento como un clic del ratón es de multidifusión a muchos otros objetos que pueden tener cosas que hacer cuando se produce el clic del ratón. Los objetos que reciben la información que se produjo un evento se llaman "listeners" (oyentes). Se necesita saber que los métodos que se utilizan para añadir o eliminar los listeners de un evento también debe seguir las normas de nomenclatura JavaBean:

Reglas para nombrar a los listeners: Los nombres de los métodos listeners usados para "registrar" un listener con un evento debe usar el prefijo "add" seguido del tipo de listener, por ejemplo: "addActionListener".

8

Page 9: Sun Certified Java Programmer-Buenas Practicas

Los nombres de los métodos usados para "eliminar" un listener debe usar el prefijo "remove", seguido del tipo de listener.

El tipo de listener que se registra o elimina debe ser pasado como argumento al método. Los listeners deben terminar con la palabra "Listener".

Ejemplo de métodos JavaBeans validos:

public void setMyValue(int v) public int getMyValue() public boolean isMyStatus() public void addMyListener(MyListener m) public void removeMyListener(MyListener m)

Ejemplo de métodos JavaBeans invalidos:

void setNombreCliente(String s) // debe ser publico public void modificarMiValor(int v) // no puede usar 'modificar' public void addXListener(MyListener m) // No coinciden los tipos

Declaraciones de clases:Cuando escribimos código Java escribimos clases o interfaces, dentro de estas escribimos las variables y métodos. La forma en la que declaremos las clases, variables y métodos tiene implicaciones en el comportamiento de nuestro código, por ejemplo, cuando declaramos una clase como public (publico) esta clase puede ser accedida o vista desde cualquier clase dentro de nuestra aplicación.

Reglas de declaración de archivos de código fuente:

Sólo debe haber una clase declarada pública dentro de un archivo de código. Los comentarios pueden ir en cualquier línea del código.

9

Page 10: Sun Certified Java Programmer-Buenas Practicas

Si tenemos una clase declarada publica en un archivo, el nombre del archivo debe ser el mismo de la clase pública, por ejemplo, para la clase "public class Empleado" el archivo debe tener el nombre "Empleado.java".

Si la clase forma parte de un paquete, la declaración de este paquete debe ir primero antes que la declaración de la clase. Si no se manejan paquetes y se importan clases, con la palabra reservada "import" (importaciones) estos "import" debe declararse antes que la clase. Si no se tienen ni paquetes ni importaciones la declaración de la clase debe aparecer en la primera línea (aunque pueden ir comentarios antes de esta). Las importaciones y los paquetes se aplican para todas las clases declaradas en un archivo, no hay forma de que declares múltiples clases en un archivo y pertenezcan a diferentes paquetes o usen diferentes importaciones. Un archivo puede tener más de una clase no pública.

Declaración de clases y modificadores

Los modificadores se dividen en dos categorías:

Modificadores de acceso: public, private, protected.

Modificadores de no acceso: algunos como final, static, abstract, etc.

Primero debemos de tener en mente que con los modificadores de acceso podemos establecer si una clase puede ser accedida o no por un determinado recurso. Existen 4 niveles de acceso pero 3 modificadores de acceso. El cuarto nivel de acceso de control es el usado por defecto, que se establece cuando no declaramos ningún modificador de acceso a nuestra clase, es decir, todas las clases, variables y métodos cuentan con un control de acceso. Los cuatro niveles de acceso funcionan para las variables y métodos perouna clase solo puede ser declarada pública o por defecto.

10

Page 11: Sun Certified Java Programmer-Buenas Practicas

Acceso a las clases:

Cuando decimos que una clase "A" tiene acceso a una clase "B", esto significa 3 cosas:

Que la clase "A" puede crear una instancia de la clase "B".

La clase "A" extiende de la clase "B", es decir es una sub clase de la clase "B".

La clase "A" puede acceder a ciertos métodos y variables de la clase "B", dependiendo de las controles de acceso que tengas estos en la clase "B".

Acceso por defecto o default:

Una clase con acceso por defecto es la que no tiene ningún modificador de acceso en su declaración. Pensemos en el acceso por defecto como un nivel de acceso de paquete, es decir, una clase con acceso por defecto solo puede ser accedida desde otra clase en el mismo paquete. Por ejemplo si tenemos dos clases "A" y "B", que están en diferentes paquetes y ambas tienen el nivel acceso por defecto (es decir, no tienen declarado ningún modificador de acceso), la clase "B" no puede crear una instancia de la clase "A" (ya que no puede verla), y la clase "A" ni siquiera sabe que la clase "B" existe (ya que tampoco puede verla). Por ejemplo, si tenemos las siguientes clases:

package autos;

class Toyota{ }

Y en un segundo archivo tenemos:

package aviones;

import autos.Toyota;

public class Boing extend Toyota { }

Como podemos, ver la clase "Toyota" y la clase "Boing" se encuentran en diferentes paquetes. La importación en la parte superior de clase "Boing" está tratando de importar a la clase "Toyota", la

11

Page 12: Sun Certified Java Programmer-Buenas Practicas

clase "Toyota" compila bien, pero cuando tratamos de compilar la clase "Boing" tendremos un error como este:

Can't access class autos.Toyota Class or interface must be public, in same package, or an accessible member class. import autos.Toyota;

La clase "Boing" no puede acceder a la clase "Toyota", ya que esta clase esta con el nivel de acceso por defecto y se encuentran en diferentes paquetes. Para lograr nuestro cometido podemos realizar 2 cosas: incluir a las dos clases en el mismo paquete o declarar a la clase "Toyota" como pública.

Acceso público:

Una clase declarada como pública puede ser accedida desde cualquier lugar de nuestra aplicación. No debemos olvidar que para poder acceder a una clase publica desde otro paquete tenemos que importar esta clase pública.

Otros modificadores o modificadores de no acceso (Non Access Modifiers):

Los modificadores de no acceso (non access modifiers) sirven para un propósito distinto al de los modificadores de acceso, sirven para lograr diferentes funcionalidades, en resumen y para mencionar algunos explicaremos primero brevemente algunos de los modificadores de no acceso para dar una idea un poco más claro de estos:

El modificador static se aplica a los métodos de clase y las variables para convertirlos en métodos o variables de clases, y no de instancia. El modificador final se aplica a las clases, métodos y variables para evitar que sean extendidos, sobreescritos o cambiados (en resumen modificados de alguna forma). El modificador abstract para crear clases y métodos abstractos.

12

Page 13: Sun Certified Java Programmer-Buenas Practicas

Hemos mencionado solo algunos de los modificadores de no acceso, mas adelante abarcaremos en más detalle todos estos modificadores. Como podemos ver algunos modificadores solo se pueden aplicar a métodos y clases otros a clases, métodos y variables, todo esto lo veremos a continuación. 

Una clase puede tener sólo los siguientes modificadores de no acceso: 

final abstract strictfp

Estos modificadores son adicionales a los modificadores de acceso y podemos usarlos en combinación con estos. Podemos, por ejemplo, declarar una clase como "public final". Sin embargo no siempre podemos combinar estos los modificadores de no acceso, como por ejemplo nunca debemos declarar una clase "abstract final", ya veremos porque más adelante.Clases declaradas con el modificador ‘final’:

Una clase declarada con el modificador "final" significa que no se pueden crear subclases a partir de ella. Un ejemplo de una clase declarada "final" es cuando queramos tener la absoluta garantía que los métodos y variables de esta clase no puedan ser sobre escritos por otra clase.

Una de las ventajas de tener clases no "final" es este escenario: imaginen que encontramos un problema con un método en una clase que estamos utilizando, pero no tenemos el código fuente, así que no podemos modificar el código fuente para mejorar el método. Pero se puede extender la clase y reemplazar el método en su nueva subclase, y sustituir en todas partes la superclase original. Si la clase es "final", sin embargo, entonces estamos perdidos.

Modificaremos nuestro ejemplo anterior: 

package autos;

13

Page 14: Sun Certified Java Programmer-Buenas Practicas

public final class Toyota{ }

Si ahora intentamos extender la clase "Toyota", que está marcada como "final", con la siguiente clase que tenemos en un segundo archivo: 

package aviones;

import autos.Toyota;

public class Boing extend Toyota { }

Obtendremos un error como el siguiente

Can't subclass final classes: class autos.Toyota class Boing extends Toyota{ 1 error

Tenemos este error ya que la clase "Toyota" tiene el modificador "final" (no puede tener subclases) y aun así la clase "Boing" está tratando de extenderla.

Clases abstractas:

Una clase abstracta nunca puede ser instanciada (osea que no podemos crear instancias de ella usando el operador "new"), su único propósito, misión en la vida, su razón de ser, es ser extendida. Podemos usar una clase abstracta, por ejemplo, si tenemos una clase "Carro" que tiene los métodos que deben ser implementados en todos los tipos de carros que extienden de él.

Un ejemplo de clase abstracta es la siguiente:

abstract class Carro { private double precio; private String modelo; private String anio; public abstract void irRapido();

14

Page 15: Sun Certified Java Programmer-Buenas Practicas

public abstract void irLento(); // aquí va codigo adicional, importante y serio }

En donde hemos declarado que la clase "Carro" es una clase "abstract", además tiene dos métodos que hemos marcado también como "abstract": "irRapido" e "irLento".

El ejemplo anterior compila bien, pero si intentamos instanciar esta clase abstracta de la siguiente forma:

Carro x = new Carro();

Obtendremos el siguiente error:

AnotherClass.java:7: class Car is an abstract class. It can't be instantiated.Carro x = new Carro();1 error

Notemos que los métodos abstractos declarados terminan en punto seguido (;) en lugar de llaves ({}).

Si un método es declarado como abstracto, tanto el método como la clase deben ser declarados abstractos, o sea que una clase debe ser abstracta si por lo menos uno de sus métodos es abstracto. Si cambiamos los métodos de abstracto a no abstracto no olvidemos de cambiar el punto y como al final por un par de llaves.

Declaración de interfaces:Cuando declaramos una interface es como si declaráramos un contrato qué debe de hacer la clase sin decir como lo hará. Si, por ejemplo, declaramos una interface "Rebotable" con sus métodos "rebotar" y "setFactorRebote", la clase que implemente esta interface tiene la obligación de implementar los métodos "rebotar" y "setFactorRebote". 

Una interface puede ser implementada por cualquier clase. Podemos

15

Page 16: Sun Certified Java Programmer-Buenas Practicas

tener dos clases diferentes que extiendan de clases diferentes y que no estén relacionadas de ninguna forma, pero al decir que estas clases implementan la interfaz "Rebotable" queremos decir que estas dos clases deben de implementar los métodos "rebotar" y "setFactorRebote".

Como dijimos al inicio: una interfaces es una clase 100% abstracta como mostramos en el siguiente ejemplo:

Lo que nosotros declaramos:

interface Rebotable{ void rebote(); void setFactorRebote(int fr);}

Lo que el compilador ve: 

interface Rebotable{ public abstract void rebote(); public abstract void setFactorRebote(int fr);}

Lo que la clase que implementa la interface debe hacer

public class Pelota implements Rebotable{ public void rebote(){}; public void setFactorRebote(int fr){};}

Mientras una clase abstracta puede definir métodos abstractos (que deben ser implementados en la clase que utiliza esta clase abstracta) y métodos no abstractos, una interface solo puede definir métodos abstractos.

Como todas las cosas que vimos anteriormente, las interfaces también

16

Page 17: Sun Certified Java Programmer-Buenas Practicas

cuentan con reglas especificas para su correcta implementación:

Todos los métodos de una interfaces son implícitamente "public" y "abstract" (públicos y abstractos), es decir, no es necesario escribir explícitamente estos dos modificadores, pero el método sigue siendo público y abstracto. Todas las variables definidas en una interface deben ser públicas, estáticas y finales ("public", "static", "final"), en otras palabras, las interfaces solo declaran constantes, no variables de instancia. Los métodos de una interface NO deben ser estáticos ("static").

Ya que los métodos de una interface son abstractos, no pueden ser marcados con los modificadores "final", "strictfp" y "native".

Una interface puede extender a una o más interfaces. Una interface no puede implementar a otras interfaces. Una interfaz debe ser declarada con la palabra reservada "interface".

Escribir una interface con el modificador "abstract" resulta redundante ya que al igual que sus métodos una interface es implícitamente abstracta lo indiquemos o no, por ejemplo:

public interface Rebotable{}

public abstract interface Rebotable{}

Las dos declaraciones anteriores son legales.

Al igual que las declaraciones de las interfaces, los métodos de las mismas no tienen necesidad de ser declarados como abstractos, ya que también resulta redundante porque estos son implícitamente abstractos y públicos, por ejemplo:

void rebote();

public abstract void rebote();

Estas dos declaraciones también son legales.

17

Page 18: Sun Certified Java Programmer-Buenas Practicas

Ejemplo de declaraciones legales:

void rebote(); public void rebote(); public abstract rebote(); abstract public rebote();

Ejemplo de declaraciones no legales:

final void rebote();

static void rebote();

private void rebote();

protected void rebote();

Declarando constantes en una interfaz:

Una interface permite declarar constantes, la clase que implemente esta interfaz tiene acceso a las variables de la interfaz implementada como si se tratara de una relación de herencia. Hay que recordar una regla para declarar constantes: estas deben de ser públicas, estáticas y finales ("public", "static" y "final"), pero no debemos preocuparnos de escribir explícitamente estos modificadores ya que, al igual que las clases y los métodos, son implícitamente públicos y abstractos, las variables de una interfaz son implícitamente públicos, estáticos y finales, lo escribamos o no.

Pero hay que tener en cuenta lo siguiente: una constante una vez que se le es asignado un valor, este valor no puede ser modificado nunca, por ejemplo:

public interface Rebotable{

18

Page 19: Sun Certified Java Programmer-Buenas Practicas

int NUMERO_CONSTANTE = 5;}

En otro archivo

public class Pelota implements Rebotable{ public void modificaNumeroConstante() { NUMERO_CONSTANTE = 8; }}

Como dijimos anteriormente una constante una vez asignado el valor no puede cambiar, es decir NUMERO_CONSTANTE = 8; no compilará ya que al declararlo en la interfaz "Rebotable" se convierte en una constante y esta no puede ser modificada como se está tratando de hacer en la clase "Pelota".

Declaración de miembros de clase:

Anteriormente vimos las reglas necesarias y a tener en cuenta para poder declarar correctamente una clase. Ahora veremos lo que se debe hacer para declarar correctamente una variable y los métodos de una clase.

Modificadores de acceso:

Como los métodos y las variables son accedidos de la misma forma, hablaremos de los dos en esta sección.

Recordemos que cuando declaramos una clase solo podemos usar dos de los cuatro niveles de acceso (public y por defecto), con los miembros de clase (métodos y variables) podemos usar los 4 niveles de acceso:

public protected private

19

Page 20: Sun Certified Java Programmer-Buenas Practicas

Por defecto

Recordemos que el acceso por defecto es cuando no se escribe explícitamente un modificador de acceso, o sea, no ponemos nada.

Podemos acceder a los miembros de una clase desde otra clase de dos maneras:

La primera consiste en usar el operador punto (.) para invocar a un método o una variable, por ejemplo:

public class Auto{ public void muevete() { System.out.println("Me estoy moviendo!!!"); }}

class Perro{ public void usandoMueveteDeAuto() { Auto auto = new Auto();//podemos hacer esto porque tenemos acceso a la clase "Auto" System.out.println("El perro dice" + auto.muevete());//podemos acceder al método muevete porque este método es publico }}

La segunda forma de acceder a los miembros de una clase es a través de la herencia, por ejemplo:

public class Auto{ public void muevete() {

20

Page 21: Sun Certified Java Programmer-Buenas Practicas

System.out.println("Me estoy moviendo!!!"); }}

class Perro extends Auto{ public void usandoMueveteDeAuto() { System.out.println("El perro dice" + this.muevete()); //podemos hacer esto por que heredamos los métodos publicos Auto auto = new Auto();//podemos hacer esto porque tenemos acceso a la clase Auto

System.out.println("El perro vuelve a decir" + auto.muevete());//podemos acceder al método muevete porque este método es publico }}

Acceso público:

Cuando una variable o método es declarado con el nivel de acceso público quiere decir que este puede ser accedido desde cualquier clase, independientemente del paquete en que esté (asumiendo que la clase a la cual pertenece es visible). Como se muestra en la siguiente imagen:

21

Page 22: Sun Certified Java Programmer-Buenas Practicas

También podemos imaginar que la clase "AmoDelPerro" se encuentra en un diferente paquete que las 2 clases anteriores y no tendría ningún problema, siempre y cuando la clase como el método a la cual hace referencia sean públicos (haciendo las importaciones necesarias).

Acceso Privado:

Los miembros de una clase marcados como privados no pueden ser accedidos desde ninguna otra clase que no sea la clase en la cual fue declarado, por ejemplo:

22

Page 23: Sun Certified Java Programmer-Buenas Practicas

package transporteTerrestre;

public class Auto{ private void muevete() { //podemos poner cualquier código aquí, pero solo la clase "Auto" la conocerá System.out.println("Me estoy moviendo!!!"); }}

Y en otro archivo declaramos:

package transporteAereo;

import transporteTerrestre.Auto

public class Avion{ public void vuela() { //Accedemos a la clase Auto, porque esta es publica Auto auto = new Auto();

System.out.println("Primero tiene que moverte" + auto.muevete()); //Error de compilación }}

Si tratamos de ejecutar este código obtendremos un error como el siguiente:

Cannot find Symbol Symbol: method muevete();

Esto sucede ya que el compilador interpreta como si el método

23

Page 24: Sun Certified Java Programmer-Buenas Practicas

"muevete()" no existiera, ya que un método declarado como privado no existe para ninguna clase excepto para su propia clase.

Algo similar sucede con la herencia: si un método en la súper clase es declarado como privado este método no puede ser heredado por la sub-clase; sin embargo, podemos declarar un método con la misma firma en la subclase, no importa cómo se vea, este método no está sobre escribiendo al de la súper clase (la sobre escritura depende de la herencia) es simplemente un método que tiene el mismo nombre que el de la súper clase.

package transporteTerrestre;

public class Auto{ private void muevete() { //podemos poner cualquier código aquí, pero solo la clase //Auto la conocerá System.out.println("Me estoy moviendo!!!"); }}

Ahora el método "muevete" se limita a la clase "Auto" incluso en el mismo paquete:

package transporteTerrestre;

public class Toyota extends Auto //Toyota y Auto se encuentran en el mismo paquete y la súper clase Auto es publica { private void hazAlgo() { System.out.println(muevete()); //Error de compilación }}

24

Page 25: Sun Certified Java Programmer-Buenas Practicas

Esto se ve más claramente en la siguiente imagen:

25

Page 26: Sun Certified Java Programmer-Buenas Practicas

Miembros protegidos y por defecto:

Los niveles de acceso protegidos y por defecto son muy parecidos pero tienen una gran diferencia. El miembro de una clase que tiene el nivel de acceso por defecto puede ser accedido por cualquier clase sólo dentro del mismo paquete, los miembros con nivel de acceso protegido ("protected") también pueden ser accedidos desde otro paquete pero solamente por medio de la herencia.

Veamos un ejemplo con las siguientes dos clases que se encuentran en diferente paquete.

package ejemplo;

public class Auto{ void muevete() { System.out.println("Me estoy moviendo"); }}

package acceso; //Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto{ public static void main (String[] args) { Auto auto = new Auto(); auto.muevete(); }}

Como podemos apreciar, la clase "Auto" se encuentra en un paquete llamado "ejemplo" y tiene su método llamado "muevete" que esta

26

Page 27: Sun Certified Java Programmer-Buenas Practicas

con el nivel de acceso por defecto (no está declarado explícitamente alguno modificador de acceso), después tenemos la clase "AccesandoAuto" el cual está en un paquete diferente llamado "acceso", como dijimos anteriormente: debemos pensar en el nivel de acceso por defecto como un acceso a nivel de paquete y como el método "muevete" se encuentre en un paquete diferente al tratar de ejecutarlo obtendremos el siguiente error:

ERRORmuevete() is not public in ejemplo.Auto; cannot be accessed from outside package

Los niveles de acceso por defecto y protegidos solo difieren cuando hablamos de herencia (sub clases). Si usamos el nivel de acceso "protected" (protegido) para declarar a un miembro de clase, este miembro de clase puede ser accedido desde cualquier subclase no importando si la súper clase y la sub clase se encuentren en diferente paquete, los miembros de la súper clase siempre son visibles por las sub clases. En contraste con el nivel de acceso por defecto una subclase no puede acceder a los miembros de la súper clase a menos que estén en el mismo paquete, el nivel de acceso por defecto no tiene ninguna consideración para con la herencia.

Para resumir: cuando pienses en acceso por defecto piensa en un acceso a nivel de paquete (sin excepciones) y cuando pienses en el nivel de acceso protegido piensa en un nivel de acceso por paquete y también de padre a hijo cuando hablamos de diferentes paquetes, para aclarar otro punto, si la subclase intenta acceder a la súper clase con el miembro protegido a través de una instancia de la súper clase esto no es posible, aclarando por última vez: Solo es a través de la herencia.

Detalles de acceso a miembros de clase protegido:

package ejemplo;public class Auto{ protected int numero = 5;}

27

Page 28: Sun Certified Java Programmer-Buenas Practicas

El código anterior declara una variable llamada "numero" con nivel de acceso protegido, como dijimos anteriormente esta variable solo puede ser accedido por clases dentro del paquete "ejemplo" y por clases fuera de ese paquete sólo a través de la herencia.

Ahora creemos otra clase en otro paquete:

package acceso;//Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto extends Auto{ public void metodo () { System.out.println("El valor de numero es: " + numero) }}

El ejemplo anterior compila bien ya que estamos accediendo a un miembro de clase protegido en diferente paquete pero a través de la herencia, pero ¿qué sucede si intentamos acceder al miembro de la subclase a través de una instancia de la súper clase?

package acceso;//Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto extends Auto{

28

Page 29: Sun Certified Java Programmer-Buenas Practicas

public void metodo() { System.out.println("El valor de numero: " + numero); Auto auto = new Auto(); System.out.println("El valor de numero: " + auto.numero); // Error de compilación }}

En código anterior tenemos un error en la línea:

System.out.println("El valor de numero es: " + auto.numero);

El error es el siguiente: 

Uncompilable source code - numero has protected access in ejemplo.Auto at acceso. AccesandoAuto.metodo

Ya que a pesar de estar en una clase que hereda a la súper clase con miembro de clase protegido, y espero decirlo por última vez: SOLO ES VISIBLE A TRAVES DE LA HERENCIA.

Detalles de miembros de clase con nivel de acceso por defecto:

Empezaremos con el nivel de acceso por defecto en la herencia, en el siguiente ejemplo modificaremos nuestra variable y le pondremos nivel de acceso por defecto, es decir, no le pondremos explícitamente un modificador de acceso.

package ejemplo;

public class Auto{ int numero = 5;

29

Page 30: Sun Certified Java Programmer-Buenas Practicas

}

¿Qué sucede si la clase padre y la clase hijo se encuentran en diferente paquete y en la clase padre tenemos una variable con nivel de acceso por defecto?

package acceso; //Diferente paquete

import ejemplo.Auto;

public class AccesandoAuto extends Auto{ public void metodo() { System.out.println("El valor de numero es: " + numero); // Error de compilación Auto auto = new Auto(); System.out.println("El valor de numero es: " + auto.numero); // Error de compilación }}

El error es el siguiente

numero is not public in ejemplo.Auto; cannot be accessed from outside package at acceso. AccesandoAuto.metodo

En estos dos casos obtendremos el error descrito anteriormente ya que las variables de clase declaradas con el nivel de acceso por defecto solo son visibles dentro de las clases que se encuentren en el mismo paquete, incluso si hacemos uso de la herencia.

Variables locales y modificadoras de acceso

Antes de todo debemos aclarar que se llaman variables locales a las variables declaradas dentro de un método. Estas variables locales NUNCA deben ser declaradas con algún modificador de acceso, por ejemplo, algo como esto es incorrecto:

30

Page 31: Sun Certified Java Programmer-Buenas Practicas

package ejemplo;

public class Auto{ public void metodo() { private int numero; }}

El código anterior nos daría el siguiente error

Uncompilable source code - illegal start of expression at ejemplo.Auto.metodo

Para resumir lo visto hasta el momento vamos a mostrar una tabla donde se explica los niveles de acceso:

Visibilidad Public Protectec Default Private

Desde la misma clase Si Si Si Si

Desde cualquier clase dentro del mismo paquete

Si Si Si No

Desde una subclase en el mismo paquete

Si Si Si No

Desde una subclase desde otro paquete

SiSi (sólo por herencia)

No No

Desde una clase distinta fuera del paquete

Si No No No

Miembros de clase con modificadores de no acceso:

Primero veremos este tipo de modificadores en los métodos después veremos estos mismos pero aplicados a variables de instancia.

31

Page 32: Sun Certified Java Programmer-Buenas Practicas

Métodos marcados con el modificador final:

Los métodos marcados con este modificador no pueden ser sobre escritos por una subclase, muchos métodos de la API de Java no puedes ser sobre escritos ya que están marcados con el modificador "final", como ejemplo veamos esta clase:

public class SuperClase { public final void metodo() { System.out.println("Esto es un metodo"); }}

No hay ningún problema en extender una clase, ya que no está marcada con el modificador final, pero un método marcado con este modificador no puede ser sobre escrito.

public class SubClase extends SuperClase{ public void metodo()// Tratamos de sobre escribir el metodo final de la super clase { System.out.println("Esto es un metodo"); }}

Al intentar ejecutar este código obtendremos un error como este:

The method void metodo () declared in classSubClass cannot override the final method of the same signature declared in class SuperClass.Final methods cannot be overridden.

32

Page 33: Sun Certified Java Programmer-Buenas Practicas

public void metodo () { }1 error

Como dijimos anteriormente: los métodos marcados con el modificador "final" no pueden ser sobre escritos.

Argumentos marcados con el modificador final

Los argumentos de los métodos son los que aparecen entre los dos paréntesis de un método, por ejemplo: 

public void metodo(String argumento){}

Y un método con declarado con múltiples argumentos se ve de la siguiente manera:

public void metodo(String argumentoUno, int argumentoDos){}

Los argumentos de un método son esencialmente lo mismo que las variables locales, nosotros podemos marcar a los argumentos de un método con el modificador "final", por ejemplo:

public void metodo(String argumentoUno, final int argumentoDos){}

En el ejemplo anterior el argumento de nombre "argumentoDos" está marcado con el modificador "final", lo que quiere decir que este valor no puede ser cambiado dentro del método, en otras palabras, el valor no puede ser otro que el valor que se le paso al método al momento de ser llamado.

Métodos abstractos

Un método abstracto, como aclaramos anteriormente, es un método que debe ser declarado pero no implementado y si recordamos lo dicho anteriormente un método abstracto no tiene llaves (que es

33

Page 34: Sun Certified Java Programmer-Buenas Practicas

donde se coloca la lógica del método), simplemente termina en punto y coma.

Declarando un método como abstracto obligamos a las subclases que heredan de la clase abstracta a la cual pertenece a implementar estos métodos. Por ejemplo si tenemos un método llamado "muevete" en la súper clase "Auto", las sub clases que extiendan a esta clase están obligadas a implementar el método "muevete" en ellas.

También debemos recordar que si un método es definido como abstracto la clase a la cual pertenece también tiene que ser abstracta, es decir, el código siguiente es incorrecto

public class SuperClase // La clase también debería ser marcada como abstracta{ public abstract void metodo(); // metodo abstracto}

El código anterior nos arroja el siguiente error:  SuperClase is not abstract and does not override abstract method metodo.

También debemos recordar que una clase abstracta puede contar con métodos no abstractos (de los normalitos, llamados concretos) y abstractos, por ejemplo el siguiente código es correcto: public abstract class SuperClase { public abstract void metodo(); // metodo abstracto public void otroMetodo(){ } // metodo no abstracto}

No olvidemos que los métodos no abstractos deben llevar llaves.

Ahora prestemos atención a una cosa, si tenemos una clase abstracta que a su vez hereda de otra clase abstracta, esta clase abstracta hija

34

Page 35: Sun Certified Java Programmer-Buenas Practicas

no tiene la necesidad de implementar los métodos abstractos de la clase abstracta padre, pero en algún momento tendremos que declarar una clase concreta y esta clase concreta tendrá que implementar TODOS los métodos abstractos en el árbol de herencia que no han sido implementados entre las clases abstractas, mejor veamos esto en un ejemplo: public abstract class Auto { private String tipo; public abstract void acelera(); // Metodo abstracto public String getTipo() // Metodo no abstracto { return tipo; }}

public abstract class Carro extends Auto { public abstract void acelera(); // Sigue abstracto public void hazAlgo() { // logica del metodo }}

public class Toyota extends Carro { public void acelera () { // logica del metodo }}

La clase "Toyota" hereda los métodos no abstractos "hazAlgo" y "getTipo" del árbol de herencia y tiene la necesidad de implementar el método "acelera" de la súper clase "Auto" ya que este nunca fue

35

Page 36: Sun Certified Java Programmer-Buenas Practicas

implementado en la clase "Carro" por lo que sigue declarada abstracta en esta clase. Si tenemos métodos abstractos que han sido implementados en las subclases abstractas no tenemos la necesidad de implementar estos métodos, por ejemplo:

public abstract class Auto { private String tipo; public abstract void acelera(); // Metodo abstracto public String getTipo() // Metodo no abstracto { return tipo; } public abstract void frena();// Metodo abstracto}

public abstract class Carro extends Auto { public abstract void acelera(); // Sigue abstracto

public void hazAlgo() // Metodo no abstracto { // logica del metodo }

public void frena()//implementando método abstracto { // logica del metodo }

public abstract void atropella();//método abstracto}

public class Toyota extends Carro { public void acelera() //implementación obligatoria

36

Page 37: Sun Certified Java Programmer-Buenas Practicas

{ // logica del metodo }

public void atropella()//implementación obligatoria { // logica del metodo } public void frena()// implementación opcional { // logica del metodo }}

Como podemos observar, la clase abstracta "Auto" cuenta con dos métodos abstractos ("acelera" y "frena"). La clase abstracta "Carro", que extiende de "Auto", cuenta con un método abstracto propio ("atropella"), además podemos observar que implementa un método abstracto de la súper clase abstracta (el método "frena"), por lo cual la clase concreta "Toyota" tiene la obligación de implementar los métodos abstractos que no han sido implementados en el árbol de herencia (el método abstracto "acelera" en la clase "Auto" y el método abstracto "atropella" en la clase "Carro").

Para aclarar otro punto, el método que implementa un método abstracto heredado tiene que ser exactamente el mismo, veamos esto en otro ejemplo:

public abstract class Carro { public abstract void acelera(); // metodo abstracto}

public class Toyota extends Carro { public void acelera(String mensaje) {

37

Page 38: Sun Certified Java Programmer-Buenas Practicas

// logica del metodo }}

Aunque parezca que el método "acelera" en la clase "Toyota" está implementando el método abstracto de la súper clase "Carro", esto no es así, lo que está haciendo el método de la clase "Toyota" es sobrecargar el método, lo cual no cumple con las reglas necesarias para que se considere que el método "acelera" en la clase "Toyota" está implementando el método abstracto de la súper clase "Carro".

Para terminar debemos aclarar que un método abstracto no puede ser marcado como "final" o "private" ya que obviamente, como dijimos anteriormente, un método abstracto está destinado a ser implementado por la sub clase que lo herede, y marcarlo como "final" significa que no puede ser sobre escrito y "private" que no puede ser visto por otra clase, lo cual también impide una posible implementación 

También debemos aclarar que un método abstracto no puede ser marcado como "static", a continuación presentamos declaraciones no legales de métodos abstractos:

private abstract void acelera();

final abstract void acelera();

abstract static void acelera();

Al ejecutar el código anteriormente escrito nos dará el siguiente error de compilación:

Illegal combination of modifiers: abstract and static

Métodos sincronizados

Un método marcado con el modificador "synchronized" quiere decir que este método solo puede ser accedido por un hilo (Thread) a la

38

Page 39: Sun Certified Java Programmer-Buenas Practicas

vez, solamente los métodos pueden ser marcados como sincronizados, no variables, no clases SOLO MÉTODOS, un método sincronizado se declara de la siguiente manera:public synchronized void acelera();

También debemos tener en cuenta que un método marcado como sincronizado puede tener cualquiera de los 4 niveles de acceso 

Métodos nativos:

Sólo los métodos pueden ser marcados como nativos ("native"), un método nativo termina en punto y coma como los métodos abstractos indicando que la implementación de dicho método es omitida. Los métodos nativos NO son implementados usando código Java, sino que hacen uso de algún otro lenguaje que genera código "nativo" para el dispositivo en el que se ejecutará nuestra aplicación, como C o C++.

Métodos scrictpf

Con "strictfp", se puede predecir cómo se comportarán los puntos flotantes, independientemente de la plataforma subyacente de laJVM que se está ejecutando. La desventaja es que si la plataforma subyacente es capaz de soportar una mayor precisión, un método "strictfp" no será capaz de tomar ventaja de ello.

Bueno, como dijimos anteriormente: una clase puede ser marcada con este modificador ("scrictpf") y esto también funciona con los métodos, no hay necesidad de marcar la clase con "scrictfp" para que declaremos métodos con este modificador, también debemos recordar que una variable nunca debe ser marcada con este modificador.

Métodos con una lista variable de argumentos (var-args)

Desde la versión 5.0 de Java se permite crear métodos que pueden tener una cantidad variable de argumentos con un mecanismo llamado "var-args", esto es muy útil cuando tenemos métodos que reciben como una cantidad variable o grande de parámetros de un tipo especifico, con var-args solo es necesario definir el tipo de nombre y el nombre del parámetros para que esta pueda recibir una cantidad

39

Page 40: Sun Certified Java Programmer-Buenas Practicas

variable de argumentos del tipo declarado, también sigue un conjunto de reglas que se explicaran a continuación:Primero vamos a aclarar la diferencia entre argumentos y parámetros:

Argumentos: son las cosas que especificamos entre los paréntesis cuando invocamos a un método, por ejemplo:

mensaje("El mensaje es: ", "Hola mundo");//invocando al //método mensaje, "El mensaje es:" y "Hola mundo" son argumentos

Parámetros: Las cosas que ponemos en un método al declararlo, que determina qué es lo que el método debe recibir cuando sea invocado, por ejemplo

mensaje(String palabraUno, String palabraDos){}//especificamos que el método recibirá 2 Strings cuando sea invocado: palabraUno y palabraDos

Ahora vamos a establecer las reglas para poder declarar correctamente un parámetro var–args:

Cuando declaremos un parámetro var-args debemos especificar el tipo de argumento(s) que el método debe recibir, este tipo puede ser un tipo primitivo o un objeto. Para declarar correctamente un var-args primero debemos especificar el tipo que este será seguido de tres puntos (...), un espacio y el nombre que llevará esta lista de parámetros en su conjunto. Es legal tener otros parámetros en un método que usa un var-args.

El var-args siempre debe ir como parámetro final y solamente se puede declarar un var-args en un método.

Al declarar un var-args decimos que podemos pasar cero o más argumentos del tipo declarado al invocar al método.

Ejemplos de declaraciones correctas de var-args:

40

Page 41: Sun Certified Java Programmer-Buenas Practicas

public void mensaje(String... mensajes){};

public void mensaje2(int numeroMensajes, String... mensajes){};

public void numeroAutos(Auto... autos){};

Ejemplos de declaraciones incorrectas de var-args: public void mensaje(String mensajes...){};

public void mensaje2(String... mensajes, int numeroMensajes){};

public void mensaje3(String... mensajes, int... numeroMensajes){};

Las declaraciones anteriores son incorrectas porque no siguen las reglas especificadas anteriormente como que los tres puntos seguidos (...) van entre el tipo de dato y el nombre que llevará (primer caso), que los var-args siempre van como parámetro final (segundo caso) y que sólo se puede declarar un var-args en un método, uno y sólo uno (tercer caso).

También es posible usar var-args donde normalmente usamos arreglos (ya que se trabajan de la misma forma) siempre y cuando se sigan las reglas indicadas anteriormente. Por ejemplo, normalmente declaramos nuestro método "main" de la siguiente forma:

public static void main(String[] args){ //codigo}

Usando var-args podemos declararlo de la siguiente forma:

41

Page 42: Sun Certified Java Programmer-Buenas Practicas

public static void main(String... args){ //codigo}

La cual también es una declaráción válida.

Declaración de constructores

Cada vez que creamos un nuevo objeto, usando el operador "new", el constructor del objeto es invocado. Todas las clases tienen un constructor, independientemente de que lo declaremos o no. Si no declaramos explícitamente un constructor para una clase, el compilador construirá uno por defecto, veamos un ejemplo:

public class Carro { public Carro(){} // este es un constructor public void Carro(){} // este es un método con un nombre no muy adecuado, pero legal}

Reglas y diferencias de los constructores (con los métodos):

La primera diferencia que debemos tener en cuenta entre los constructores y los métodos es que los constructores nunca, nunca retornan un valor. Los constructores pueden ser declarados con cualquiera de los niveles de acceso. Un constructor puede tener argumentos, incluido los var-args.

La gran regla de un constructor nos dice que este siempre debe tener el mismo nombre de la clase en la que es declarada. Los constructores no pueden ser declarados como "static", "abstract" o "final".

Los constructores no se heredan.

42

Page 43: Sun Certified Java Programmer-Buenas Practicas

Ejemplo de declaraciones legales e ilegales de constructores:

Public class Auto{ //Constructores legales Auto(){}

private Auto(int numero){}

Auto(int numero){}

public Auto(int numero, String... tipos){}

//Constructores ilegales void Auto(){} // esto es un método

Auto2(){} //No es un método ni un constructor

static Auto(){} //No puede ser static

final Auto(int numero){} // No puede ser final

abstract Auto(int numero){} // No puede ser abstract

Auto(String... tipos, int numero){} //mala sintaxis var-args }

Declaración de variables:

Las variables son valores modificables, es decir, son nombres que representan un valor de cierto tipo y el valor asociado al nombre se puede variar dependiendo de cada caso.

En Java contamos con dos tipos de variables:

Variables primitivas: Una variable primitiva puede ser uno de los siguientes ocho tipos: "int", "char", "boolean", "byte", "short", "double", "float" y "long". Una vez que declaras una variable de

43

Page 44: Sun Certified Java Programmer-Buenas Practicas

tipo primitivo, podemos cambiar el valor, más no el tipo, pero podemos asignarle valores de tipos equivalentes haciendo un cast, por ejemplo a un "int" podemos asignarle un valor "long" haciendo el cast apropiado:

int i = (int)1000L;

Variables de referencia: Es usada para hacer referencia a un objeto, una variable de referencia es declarada para que sea de un determinado tipo y este nunca puede ser cambiado. Puede ser usado para hacer referencia a cualquier objeto del tipo declarado o de un sub tipo del tipo que se declaro con un tipo compatible o equivalente a él, por ejemplo, tenemos una clase "Perro" que extiende de la clase "Animal" y podemos hacer esto.

Animal animal = new Perro();

Declarando primitivos y rango de valores primitivos:

Los valores primitivos pueden ser declarados como variables de clase, parámetros de método, variables de instancia o variables locales.

Podemos declarar más de una variable del mismo tipo en una sola línea, por ejemplo:

int numero;

int numeroUno, numeroDos, numeroTres;

Es importante saber que para los tipos enteros la secuencia de menor a mayor es "byte", "short", "int", "long", y que los "double" son más grandes que los de tipo "float".

Los seis tipos de números en Java se componen de un número determinado de bytes de 8-bits cada uno, y son números reales, lo que significa que puede ser negativo o positivo. El bit más a la izquierda (la cifra más significativa) se utiliza para representar el signo,

44

Page 45: Sun Certified Java Programmer-Buenas Practicas

donde 1significa negativo y 0 significa positivo, como se muestra en la siguiente figura: 

La figura anterior nos muestra que de los 8 bits que se usan para representar un tipo de dato "byte" uno de estos bits se usan para representar el signo, y 7 para representar el valor de la variable. Lo que nos dice que un byte puede tener 27 posibles valores. Esto sumado al signo nos dice que el rango de valores de un byte es de -27 a 27-1 (este último -1 ocurre porque el 0 se considera dentro de los números positivos).

Digámoslo de otra forma para que quede más claro ya que el concepto en si es un poco extraño. El mínimo valor positivo que puede tener un byte es (representado en bits) 00000000, donde el primer 0 representa el signo (en este caso positivo). El máximo valor positivo delbyte sería 01111111 donde, nuevamente el primer 0 representa el signo positivo. Por lo tanto tenemos que el rago positivo va desde00000000 hasta 01111111, o sea (traduciendolo a decimal) desde 0 hasta 127. Si brincaramos al siguiente número binario (10000000) ya estariamos colocando el último bit en 1 lo que lo convertiria en un número negativo (en este caso -128)

De la misma forma. El mínimo valor negativo que puede tener un byte es 10000000, donde el primer 1 representa el signo (en este caso negativo). Y el máximo valor negativo que puede tener es 11111111. O sea, va desde -128 hasta -1. 

O sea que un byte va desde -128 hasta 127.¿Cómo es esto posible? Pues esto es posible porque esto trabaja usando complemento a dos.

La misma explicación funciona para el resto de los tipos primitivos numéricos.

45

Page 46: Sun Certified Java Programmer-Buenas Practicas

En la siguiente tabla mostramos los rangos de los números primitivos existentes en java y su respectivo equivalente en bits, bytes y su rango mínimo y máximo:

Tipo Bits Bytes Rango Inferior Rango Superior

byte 8 1 -27 (-128) 27-1 (127)

short 16 2 -215 (-32768) 215-1 (32767)

int 32 4 -231 (-2147483648) 231-1 (2147483647)

long 64 8-263 (-9223372036854775808)

263-1 (9223372036854775807)

float 32 4 n/a n/a

double 64 8 n/a n/a

El rango de los números de punto flotante es dificil de determinar (debido a que la cantidad de cifras después del punto depende de la plataforma).

Para los tipos booleanos no hay un rango, solo pueden ser true o false. La cantidad de bits que se usan para representar estos valores depende de la implementación de la máquina virtual, pero para efectos del exemen esto no es importante.

El tipo char contiene un solo caracter unicode de 16 bits sin signo, o sea que sus valores van desde 0 hasta 65535 (a diferencia de losshort ya que estos últimos usan un bit para el signo).

Variables de instancia:

Las variables de instancia son declaradas dentro de una clase pero fuera de algún método y son inicializadas solo cuando la clase es instanciada. Las variables de instancia son los campos que diferencian a un objeto de otro, por ejemplo veamos los campos (variables de instancia) de nuestra clase "Auto":

46

Page 47: Sun Certified Java Programmer-Buenas Practicas

public class Auto{ // definiendo campos (variables de instancia) para Auto private String color; private String marca; }

El código anterior nos dice que cada vez que instanciamos la clase "Auto" este tendrá su propio "color" y su propia "marca", debemos tener en cuenta que "campo", "variable de instancia", "propiedad" o "atributo" significan lo mismo.

A continuación mostramos las reglas que debemos seguir para declarar correctamente estas variables de instancia:

Se puede usar cualquiera de los 4 niveles de acceso Puede ser marcado con "final"

Puede ser marcado con "transient"

No puede ser marcado con "abstract"

No puede ser marcado con "synchronized"

No puede ser marcado con "strictfp"

No puede ser macado con "native"

No puede ser marcado con "static" porque de esa manera seria una variable de clase

Hasta el momento hemos aprendido que como declarar y que modificadores de acceso deben tener las clases, los métodos, variables locales y las variables no locales (de instancia) para que sea correcta su declaración, a continuación mostramos una tabla en la que resumimos todo lo aprendido hasta el momento:

47

Page 48: Sun Certified Java Programmer-Buenas Practicas

CLASESVARIABLES LOCALES

VARIABLES NO LOCALES

MÉTODOS

public

final

abstract

final

final

static

transient

volatile

protected

private

public

final

static

abstract

native

syncrhronized

strictfp

public

private

protected

A continuación vamos a hablar más detalladamente sobre las variables locales:

Variables locales:

Como dijimos anteriormente las variables locales son las variables que se declaran dentro de un método. El ciclo de vida de estas variables empieza dentro del método y termina cuando el método ha sido completado.

Las variables locales siempre se encuentran en el "stack" (la sección de memoria que mantiene los valores de las variables para métodos particulares), no en el "heap" (la sección de memoria en donde se crean los objetos). Aunque el valor de una variable podría ser pasado a, digamos, otro método que almacene ese valor en una variable de instancia la variable en sí, vive solo dentro del ámbito del método. Solo no olviden que mientras la variable local se encuentra en el stack, si

48

Page 49: Sun Certified Java Programmer-Buenas Practicas

la variable es una referencia a un objeto, el objeto mismo seguirá siendo creado en el heap, no existe una cosa como un objeto en el stack, solo una variable en el stack. Algunas veces escucharemos a programadores usar el término "objeto local" pero lo que en realidad quieren decir es "una variable de referencia declarada localmente" Así que si escuchan a un programador usar esa expresión, sabrán que tan solo es flojo como para decir la frase en una forma técnicamente precisa.

En las declaraciones de las variables locales no se pueden utilizar la mayoría de los modificadores que se pueden aplicar a las variables de instancia, como "public" (o los modificadores de acceso de otro tipo), "transient", "volatile", "abstract", o "static", sino como hemos visto en la tabla mostrada anteriormente, las variables locales pueden ser marcadas con el modificador "final", por ejemplo veamos un ejemplo de variables locales:

public class Ejemplo{ public void unMetodo() { int numero = 5; }}

Normalmente deberíamos inicializar una variable local en la misma línea en que la declaramos, aunque también podríamos inicializarla más adelante en el método. Lo que siempre debemos de recordar es que una variable local debe ser inicializada antes de ser usada. Si no hacemos esto, el compilador rechazara y marcara como error cuando intentemos usar una variable local sin antes haberle asignado un valor, ya que, a diferencia de una variable de instancia, la variable local no tiene un valor por defecto.Al compilar un método que utiliza una variable con un valor sin inicializar obtendremos el siguiente error que nos dice que la variable en cuestión no ha sido inicializada:

variable nombreVariable might not have been initialized

49

Page 50: Sun Certified Java Programmer-Buenas Practicas

Debemos recordar que una variable local no puede ser usada o referenciada fuera del método en el que se declaró, por ejemplo:

public class Ejemplo{ public void unMetodo() { int numero = 5; } public void otroMetodo(int otroNumero) { numero = otroNumero;//Error, numero no puede ser usado fuera //del método unMetodo() }}

Al compilar el código anterior obtendremos el siguiente error ya que el compilador no conoce a esa variable local en otro lugar que no fuese dentro del método donde fue declarado, el error es el siguiente:

cannot find symbol

Una variable local puede llevar el mismo nombre que una variable de instancia, a esto se le conoce como ensombrecimiento (shadowing), por ejemplo:

public class Ejemplo{ int numero = 8;

public void unMetodo() { int numero = 5; System.out.println(numero); }

public void otroMetodo()

50

Page 51: Sun Certified Java Programmer-Buenas Practicas

{ System.out.println(numero); }

public static void main(String[] args) { new Ejemplo().unMetodo() //variable local new Ejemplo().otroMetodo() //variable de instancia } }

Si ejecutamos el código anterior la salida correspondiente es:

58

Ahora veamos qué ocurriría si hacemos esto:

public class Ejemplo{ int numero = 8; public void unMetodo(int numero) { numero = numero; // que numero es igual a q numero? }}

Para no tener este tipo de problema cuando declaramos el mismo nombre de la variable a la variable local (que ocurre por ejemplo típicamente en los métodos "setter") debemos usar la palabra reservada "this", también conocido como el apuntador "this" o la referencia "this". La palabra reservada "this" siempre hace

51

Page 52: Sun Certified Java Programmer-Buenas Practicas

referencia al objeto que se está ejecutando y en este caso hace referencia a la variable de instancia, por ejemplo:

public class Ejemplo{ int numero = 8; public void unMetodo(int numero) { this.numero = numero; // this.numero hace referencia a la //variable de instancia }}

Declaración de arreglos:

En Java, los arreglos son objetos que guardan múltiples variables del mismo tipo, o variables que son todas las subclases del mismo tipo. Los arreglos pueden contener ya sea primitivos o referencias de objetos, un arreglo siempre será un objeto, incluso si el arreglo fuera contenedor de elementos primitivos. En otras palabras, no hay tal cosa como un arreglo primitivo, pero podemos hacer un arreglo de elementos primitivos.

Declarando un arreglo de elementos primitivos:

int[] arreglo;// los corchetes antes del nombre (recomendado)

int arreglo[];// los corchetes después del nombre (legal pero menos legible)

Declarando un arreglo de referencias de objetos:

String []arreglo; String arreglo[];

52

Page 53: Sun Certified Java Programmer-Buenas Practicas

También podemos declarar arreglos multi-dimensionales, que en realidad son arreglos de arreglos y se declaran de la siguiente manera:

String []arreglo[];

String [][][]arreglo;

Nuca debemos indicar el tamaño del arreglo en la declaración de este, se debe declarar cuando se realiza la instanciación del arreglo con el operador new, por ejemplo:

String []arreglo = new String[10];String []arreglo2;arreglo2 = new String[10];

El siguiente ejemplo nos arrojara un error ya que estamos asignando el tamaño del arreglo en la declaración de este:

String [10]arreglo;

Si intentamos cómpilar un código con un arreglo declarado de la forma anterior obtendremos el siguiente error:

Arreglos.java:5: not a statement String [10]arreglo; ^Arreglos.java:5: ';' expected String [10]arreglo; ^2 errors

Variables marcadas con el modificador "final"

Cuando declaramos una variable con el modificador final, garantizamos que esta variable no pueda ser reinicializada (cambiar su valor) una vez que su valor ha sido asignado explícitamente (explícitamente, no por defecto). Para los tipos primitivos esto significa que el valor nunca podrá ser cambiado por ejemplo, si asignamos una

53

Page 54: Sun Certified Java Programmer-Buenas Practicas

variable llamada "PI" como double y marcamos con "final" asignándole un valor de "3.1416", este valor nunca podrá ser cambiado. Cuando marcamos una variable de referencia a un objeto con el modificadorfinal, quiere decir que esta variable no podrá ser asignada para referir a otro objeto diferente, los datos del objeto puede ser modificados pero el objeto nunca podrá ser cambiado, a continuación pondremos unos ejemplos que explican cómo se comporta el identificadorfinal en diferentes situaciones:

Variables marcadas con el modificador "transient"

Cuando marcamos una variable con el modificador "transient" le estamos diciendo a la maquina virtual de java (JVM) que ignore esta variable cuando se intenta serializar el objeto que lo contiene. La serialización es una de las mejores características de Java que permite almacenar un objeto, escribiendo su estado (o sea el valor de sus variables de instancia) en un tipo especial de stream de I/O o sea en un flujo de lectura y escritura. Con la serialización, podemos

54

Page 55: Sun Certified Java Programmer-Buenas Practicas

guardar un objeto en un archivo, o enviarlo a través de un cable para deserializarlo al otro lado, en otra JVM.

Variables con el modificador "volatile"Por el momento solo necesitamos saber que al igual que "transient" solo se aplican a variables de instancia.

Variables y métodos estáticos:El modificador estático ("static") es usado para crear variables y métodos que existen independientemente de cualquier instancia creada para la clase. Todos los miembros estáticos existen antes de haya una nueva instancia de una clase, y habrá un solo ejemplar de un miembro estático, independientemente del número de instancias de esa clase. En otras palabras, todas las instancias de una clase comparten el mismo valor para cualquier variable dada estática.

Podemos marcar como estático:

Las variables de instancia Los métodos Bloques de inicialización Una clase anidada dentro de otra clase, pero no dentro de un método.

Declarando Enums:

Desde la versión 5.0 Java nos permite poder restringir una variable a unos cuantos valores predefinidos por nosotros, estos valores se obtienen a partir de una lista enumerada de valores declarados, se logra esto a través de los ENUMS.Si por ejemplo queremos restringir el tamaño de una persona con solo 3 valores podemos realizarlo de la siguiente manera:

enum EstaturaPersona {ENANO, NORMAL, ALTO}

Con el código anterior estamos restringiendo que las variables de tipo "EstaturaPersona" sólo pueden tener 3 valores porsibles: "ENANO", "NORMAL", "ALTO".

55

Page 56: Sun Certified Java Programmer-Buenas Practicas

Si deseamos asignarle un valor a la variable lo hacemos de la siguiente manera:

EstaturaPersona ep = EstaturaPersona.ENANO;

No es necesario que declaremos los valores con mayúsculas, pero si recordamos las convenciones de Sun nos dicen q las constantes debe ser escritas con mayúsculas.

Las enum pueden ser declaradas separadas de la clase, o como un miembro de clase, sin embargo, no debe ser declarada dentro de un método, por ejemplo

enum EstaturaPersona {ENANO, NORMAL, ALTO} // No puede ser protected o private

class Persona{ EstaturaPersona estatura;}

public class PersonaTest{ public static void main(String[] args) { Persona persona = new Persona(); persona.estatura = EstaturaPersona.ENANO;// enum fuera de clase }}

El punto clave a recordar es que una enum que no está encerrado en una clase se puede declarar sólo con el modificador público y por defecto, al igual que una clase no interna. Ejemplo de declarar una enumeración dentro de una clase:

class Persona{

56

Page 57: Sun Certified Java Programmer-Buenas Practicas

enum EstaturaPersona {ENANO, NORMAL, ALTO} EstaturaPersona estatura;}

public class PersonaTest{ public static void main(String[] args) { Persona persona = new Persona(); persona.estatura = Persona.EstaturaPersona.ENANO;// requiere //nombre de clase }}

Debemos recordar que las enumeraciones pueden ser declaradas dentro de su propia clase, o encerrado en otra clase, y que la sintaxis para acceder a los miembros de una enumeración depende del lugar donde fue declarada la enumeración, el siguiente ejemplo es incorrecto:

public class PersonaTest{ public static void main(String[] args) { enum EstaturaPersona {ENANO, NORMAL, ALTO}//No declarar en un método Persona persona = new Persona(); persona.estatura = Persona.EstaturaPersona.ENANO; }}

Es importante saber que el punto y coma al final de la declaración de un enum (;) es opcional: 

class Persona{

57

Page 58: Sun Certified Java Programmer-Buenas Practicas

enum EstaturaPersona {ENANO, NORMAL, ALTO};//<-- Punto y coma opcional}

public class PersonaTest{ public static void main(String[] args) { Persona persona = new Persona(); persona.estatura = Persona.EstaturaPersona.ENANO;// requiere //nombre de clase }}

También debemos recordar que un enum no es un String o un int, cada enumeración (valor) de "EstaturaPersona" es una instancia de "EstaturaPersona", es decir "ENANO" es del tipo "EstaturaPersona". Debemos pensar en los enums como un tipo de clase. El enum de ejemplo se puede ver conceptualmente de la siguiente manera:

public class EstaturaPersona{ public static final EstaturaPersona ENANO = new EstaturaPersona("ENANO", 0); public static final EstaturaPersona NORMAL = new EstaturaPersona("NORMAL", 1); public static final EstaturaPersona ALTO = new EstaturaPersona("ALTO", 2);

public EstaturaPersona(String nombre, int posicion){}}

Como dijimos anteriormente pensemos que cada valor del enum "EstaturaPersona" es del tipo "EstaturaPersona" y son

58

Page 59: Sun Certified Java Programmer-Buenas Practicas

representadas con "static" y "final" porque así nos dice Java que debemos declarar a las constantes. Además cada enum conoce suposición, es decir, el orden en que fueron declarados.

Declarando constructores, métodos y variables dentro de un enum

Como dijimos anteriormente: un enum es algo así como un tipo especial de clase, y podemos hacer más que sólo declarar una lista de variables. Veremos que podemos declarar constructores, métodos, variables y algo realmente extraño llamado cuerpo de un elemento específico (constant specific class body).

Para entender mejor esto veamos un ejemplo. Pensemos que tenemos una bebida que se vende en tres presentaciones: PERSONAL, MEDIANA y FAMILIAR, y que la bebida personal tiene como contenido 1 litro de bebida, la mediana 3 litro y la familiar 5 litros

Podemos tratar estos valores e implementarlo de muchas maneras, pero lo manera más simple es tratar estos valores (PERSONA, MEDIANA y FAMILIAR) como objetos y que cada uno de estos tengan sus propias variables de instancia. Esto lo logramos asignando estos valores en el momento que inicializamos el enum y pasando un valor al constructor del enum, como mostramos a continuación en el siguiente código:

enum BebidaPresentacion{ //1, 3 y 5 son pasados al constructor, de la siguiente manera PERSONAL(1), MEDIANA(3), FAMILIAR(5);

BebidaPresentacion (int litros)// Constructor { this.litros = litros; }

private int litros;

public getLitros()

59

Page 60: Sun Certified Java Programmer-Buenas Practicas

{ return litros; }}

class Bebida{ BebidaPresentacion presentacion;

public static void main(String[] args) { Bebida bebida1 = new Bebida(); bebida1.presentacion = BebidaPresentacion.PERSONAL;

Bebida bebida2 = new Bebida(); Bebida2.presentacion = BebidaPresentacion.FAMILIAR; System.out.println(bebida1.presentacion.getLitros()); //1 System.out.println("********************"); for(BebidaPresentacion present : BebidaPresentacion.values()) { System.out.println(present + " " + present.getLitros()); } }}

El código anterior imprimirá como salida:

1********************PERSONAL 1MEDIANA 3FAMILIAR 5

60

Page 61: Sun Certified Java Programmer-Buenas Practicas

Como todo lo que hemos visto hasta ahora, los enums también tienen unas reglas que debemos seguir para su correcto uso:

NUNCA debemos invocar al constructor del enum directamente. El constructor del enum se invoca automáticamente, con los argumentos que se definen después del valor constante. Por ejemplo, PERSONAL(1) invoca al constructor de "BebidaPresentacion" que toma un "int", pasando el "int" "1" al constructor. Podemos definir más de un argumento al constructor, y podemos sobrecargar los constructores de un enum, tal como se puede sobrecargar un constructor de una clase normal. Para inicializar una "BebidaPresentacion" tanto con la cantidad de litros como el color del envase, se pasan dos argumentos al constructor como PERSONAL(1, "Azul"), lo que significa que tiene un constructor que recibe tanto un int y un String.

Aquí termina el primer post relacionado a conceptos básicos de Java, hemos aprendido sobre cómo declarar correctamente los identificadores, las convenciones JavaBeans, denominaciones estándar, cómo declarar correctamente clases, métodos y variables. Hemos visto como se declaran las interfaces, métodos y clases abstractos, los modificadores de acceso y los de no acceso, los var-args, arreglos y finalmente los enums, en el siguiente post hablaremos sobre conceptos de Orientación a Objetos en Java.

Si tienen alguna duda comentario o sugerencia no duden en colocarlo en la sección correspondiente y con todo gusto Alan o yo contestaremos lo más pronto posible.

61

Page 62: Sun Certified Java Programmer-Buenas Practicas

Sun Certified Java Programmer 6, CX-310-065 - Parte 2:

Orientación a Objetos

En este segundo post para la certificación de Java 6 hablaremos sobre conceptos de la orientación a objetos, que abarca temas como la herencia, el polimorfismo, la cohesión, bajo acoplamiento (loose coupling), etc.

Empezaremos hablando sobre la encapsulación:

ENCAPSULACIÓN

La encapsulación es un mecanismo que nos permite que, aunque nuestras clases utilicen muchas variables y métodos para su correcto funcionamiento, no todas sean visibles desde el exterior. O sea que solo exponemos lo que los clientes necesitan para poder hacer uso de los objetos de nuestra clase.

Para entender mejor este concepto imaginemos el siguiente escenario:

Tenemos la siguiente clase:

public class Examen{ public String pregunta1;

public static void main(String... args) { Examen examen = new Examen();

62

Page 63: Sun Certified Java Programmer-Buenas Practicas

examen.pregunta1 = "¿Que es Java?"; // Legal pero no recomendable }}

El ejemplo anterior compilará y se ejecutará de forma correcta. Sin embargo no es recomendable que los atributos de la clase (las variables de instancia) estén expuestas de forma que los clientes puedan leer y escribir sus valores directamente. De esta forma cualquiera podría colocar el valor que quisiera en la variable "pregunta1", aún valores que nuestra aplicación no puede o no está preparada para manejar.

Ahora hagamos una pregunta: ¿Cómo poder cambiar la clase cuando alguien cambia el valor de "pregunta1" por un valor incorrecto? La única forma es volver a la clase e implementar un método de sólo escritura (un método setter: "setPregunta1(String pregunta1)") y ocultar la variable "pregunta1" estableciéndola como privada, pero de esta manera al no establecerla desde un principio con los métodos "set" o "get" todas las personas que han utilizado este código anteriormente y de la manera en la que estaba implementada se encontraran perdidas.

La capacidad de realizar cambios en el código de aplicación sin romper el código de otras personas que utilizan este código es un beneficio clave de la encapsulación. Ocultando los detalles de la implementación a través de métodos de acceso nos brinda la ventaja de poder rehacer el código dentro de los métodos sin forzar a las demás personas a cambios, ya que ellas usan dichos métodos de acceso.

Con todo esto obtenemos las dos promesas/beneficios de la programación orientada a objetos: Flexibilidad y Mantenimiento, pero como vemos estos dos beneficios no llegan solos, nosotros tenemos que implementar nuestro código de manera que brinde y soporte estos beneficios.

Estas son algunas recomendaciones para lograr la encapsulación:

63

Page 64: Sun Certified Java Programmer-Buenas Practicas

Mantener las variables de instancia protegidas (mayormente con el modificador de acceso private). Mantener publicos los métodos de acceso a las variables de instancia (modificador de acceso public), así forzamos a llamar a las variables de instancia y a la implementación del mismo a través de estos métodos en lugar de ir directamente por las variables de instancia. Para los métodos de acceso se recomienda usar las reglas de las convenciones de nombres de JavaBean:set<nombrePropiedad> y get<nombrePropiedad>,de las cuales se habló en el post anterior.

A continuación mostramos un ejemplo más práctico de lo que nos estamos refiriendo:

La clase "A" no puede acceder a las variables de instancia de la Clase "B" sin antes ir a los métodos de acceso ("set" y "get") de dichas variables, las variables de instancia son marcadas privadas y los métodos de acceso son públicos. Debemos tener presente que el código de estos métodos no solo se utiliza para devolver o establecer los valores de las variables de instancia, también se puede establecer lógica o implementación de muchas más cosas o reglas que queramos definir, por ejemplo:

public void setTamanio(int tamanio){ this.tamanio = tamanio * 0.10;

64

Page 65: Sun Certified Java Programmer-Buenas Practicas

this.color = "negro";}El encapsulamiento es uno de los mecanismos que nos proporciona la orientación a objetos para poder darle una funcionalidad rica a nuestras clases sin que las personas que las utilicen sepan los detalles exactos de cómo está implementada dicha funcionalidad. Sin embargo, este no es el único mecanismo proporcionado por la orientación a objetos. A continuación veremos otro de ellos. La herencia.

HERENCIA, ES–UN, TIENE–UN (IS–A, HAS-A)

En Java, la herencia se encuentra en todos. Es seguro decir que en Java casi nada se puede hacer sin hacer uso de la herencia. Para dar un ejemplo a continuación usaremos del operador "instanceof" (por ahora no ahondaremos mucho en la explicación del uso de este operador ya que se tocará más adelante con mayor detalle, sólo cabe recordar ahora que este operador devuelve un "true" si la variable puesta al principio es del tipo de la variable con la que se está comparando):

public class Test{ public static void main(String... args) { Test t1 = new Test(); Test t2 = new Test(); if(!t1.equals(t2)) { System.out.println("No son iguales") } if(t1 instanceof Object) { System.out.println("t1 es un objeto"); } }}

65

Page 66: Sun Certified Java Programmer-Buenas Practicas

¿De dónde saca "t1" el método "equals"? Si no hemos implementado ningún método dentro de la clase con ese nombre, ¿o sí? Por otro lado se pregunta si "t1" es una instancia de la clase "Object" y de ser así, la condición if será exitosa.

¿Cómo puede ser "t1" del tipo "Object" si solo lo declaramos que sea del tipo de la clase "Test"? Esto ocurre porque todas las clases en java (las que ya están escritas, las que escribimos y las que escribiremos) son una subclase de la clase "Object" (excepto por supuesto la clase "Object" misma) y siempre tendrán métodos como "equals", "clone", "notify", "wait" y otros más. Siempre que creamos una clase, esta hereda todos los métodos de la clase "Object".

El método "equals" por ejemplo: Los creadores de Java asumieron correctamente que nosotros comúnmente desearíamos comparar instancias de las clases para comprobar la igualdad; si la clase "Object" no tuviera un método "equals" tendríamos que implementar nosotros mismos un método para este propósito.

También debemos recordar que las 2 razones más importantes para el uso de la herencia son:

Reutilización de código Uso del polimorfismo

Empecemos con la reutilización. Un enfoque de diseño común es crear una versión de una clase bastante genérica y después crear subclases muy especializadas que hereden de esta, por ejemplo:

public class Animal{ public void muevete() { System.out.println("Me estoy moviendo");

66

Page 67: Sun Certified Java Programmer-Buenas Practicas

}}

public class Perro extends Animal{ public void ladra() { System.out.println("Estoy ladrando"); }}

public class PruebaAnimal{ public static void main(String... args) { Perro perro = new Perro(); perro.muevete(); perro.ladra(); }}

La salida del código anterior seria:

Me estoy moviendoEstoy ladrando

Como podemos ver, la clase "Perro" está heredando el método "muevete" de la clase "Animal" y que también tiene su propio método agregado, en este caso es el método "ladra". Aquí se está haciendo uso de la reusabilidad al utilizar un método genérico de una clase padre que en este caso es el método "muevete", con esto podemos crear diferentes tipos de animales y todos van a poder utilizar el método "muevete" sin necesidad de implementarlo ellos mismos.

El segundo objetivo de la herencia es poder acceder a las clases polimórficamente. Imaginemos este escenario: digamos que tenemos una clase llamada "Entrenador" que quiere recorrer todos los diferentes tipos de animal e invocar al método "muévete" en cada uno

67

Page 68: Sun Certified Java Programmer-Buenas Practicas

de ellos, al momento de escribir la clase "Entrenador" no sabemos cuántas clases de "Animal" podría haber y seguro no vamos a querer cambiar el código sólo porque a alguien se le ocurrió crear un nuevo tipo de Animal.

Lo bonito del polimorfismo es que podemos tratar cualquier tipo de "Animal" como un "Animal", en otras palabras podemos decir lo siguiente: No me importa qué tipo de Animal se pueda crear, siempre y cuando extienda (herede) de Animal todos van a poder moverse (método "muevete").

Ahora miremos esto con un ejemplo:

public class Animal{ public void muevete() { System.out.println("Me estoy moviendo"); }}

public class Perro extends Animal{ public void ladra() { System.out.println("Estoy ladrando"); }}

public class Gato extends Animal{ public void ronronea() { System.out.println("Estoy ronroneando"); }}

Ahora imaginemos una clase llamada "Entrenador" que tiene un método que tiene como argumento un "Animal", esto significa que

68

Page 69: Sun Certified Java Programmer-Buenas Practicas

puede tomar cualquier tipo de "Animal", cualquier tipo de animal puede ser pasado a un método con un argumento del tipo "Animal", ejemplo:

public class Entrenador{ public static void main(String... args) { Gato gato = new Gato(); Perro perro = new Perro(); mueveteAnimal(gato); mueveteAnimal(perro); }

public static void mueveteAnimal(Animal animal) { animal.muevete(); } }

La salida del código anterior es:

Me estoy moviendoMe estoy moviendo

El método "mueveteAnimal" está declarando un "Animal" como argumento pero se le puede pasar cualquier sub-clase o sub-tipo de esta clase "Animal", este método podría invocar a cualquier método dentro de la clase "Animal". Lo que si debemos de tener en cuenta es que sólo podemos llamar a los métodos declarados por "Animal", los métodos declarados dentro de las subclases de "Animal" son dependiente del tipo declarado, esto significa que no podríamos llamar

69

Page 70: Sun Certified Java Programmer-Buenas Practicas

al método "ladra" incluso si el "Animal" que se está pasando es del tipo "Perro".

RELACIONES IS – A, HAS– A

IS – A

En Orientación a objetos el concepto de "IS-A" (es-un) esta basado en la herencia de una clase ("extends") o en la implementación de una interface ("implements"). IS-A es una forma de decir "Esta cosa es del tipo de esta cosa" (o estos dos tipos pueden ser equivalentes), por ejemplo un "Perro" es del tipo "Animal", en OO nosotros podemos decir: "Perro IS-A Animal", "Lechuga IS-A Vegetal", en java podemos expresar esta relación IS-A por medio de las palabras reservadas "extends" (para la herencia de clases) y de "implements" (para la implementación de interfaces), veamos un ejemplo:

public class Carro{ //cualquier código aquí}

public class Toyota extends Carro{ /*Toyota está heredando de carro, no olvidemos que Toyota hereda los miembros de Carro incluido métodos y variables*/}

"Carro" también es un vehículo así que se podría implementar un árbol de herencia de la siguiente manera:

public class Vehiculo{...}public class Carro extends Vehiculo{...}public class Toyota extends Carro{...}

En términos de OO podríamos decir lo siguiente:

70

Page 71: Sun Certified Java Programmer-Buenas Practicas

Vehículo es la súper clase de Carro

Carro es la subclase de Vehículo

Carro es la súper clase de Toyota

Toyota es la subclase de Carro

Toyota hereda de Carro y de Vehiculo

Toyota deriva de Carro

Carro deriva de Vehículo

Toyota es subtipo de Carro y Vehiculo

Retornando a la relación IS-A, lo siguiente es correcto:Toyota extends Carro significa Toyota IS-A CarroCarro extends Vehiculo significa Carro IS-A Vehiculo

Ahora recordemos que al principio usamos el operador instanceof, bueno, si la expresión "Toyota instanceof Carro" es verdadera, entonces es lo mismo que decir "Toyota IS-A Carro", la expresión "Toyota instanceof Vehiculo" también es verdadera aunque explícitamente no dice esto ya que "Toyota" extiende de "Carro", pero por otro lado "Carro" si extiende de "Vehiculo" a esto se le llama herencia indirecta ya que una clase puede ser hijo, nieto, bisnieto, etc. de otra clase, una clase puede extender o heredar de otra directa o indirectamente ya que en el árbol de herencia pueden haber clases intermedias.

HAS–A

La relación HAS-A esta basada en el uso en lugar de la herencia, por ejemplo "A HAS-A B" si la clase "A" tiene una referencia a una instancia de la clase "B", por ejemplo:

public class Animal{ }

public class Gato extends Animal{ CajaDeArena miCajaDeArena;}

71

Page 72: Sun Certified Java Programmer-Buenas Practicas

En el código anterior la clase "Gato" tiene una referencia una variable de instancia del tipo "CajaDeArena", entonces podemos decir "Gato HAS-A CajaDeArena", en otra palabras, "Gato" tiene una referencia a una "CajaDeArena", la clase "Gato" puede tener un método llamado "llenarCaja(Arena miArena)", de esta manera los usuarios de la clase "Gato" no podrán saber nunca que cuando se invoca al método "llenarCaja" este método delega toda la responsabilidad a la clase "CajaDeArena" llamando a su método "llenarCaja", veamos esto con un ejemplo:

public class Gato extends Animal{ private CajaDeArena miCajaDeArena; public void llenarCaja(Arena miArena) { miCajaDeArena.llenarCaja(miArena); /*delegando comportamiento al objeto CajaDeArena */ }}

public class CajaDeArena{ public void llenarCaja(Arena miArena) { System.out.println("Llenando la caja de arena"); }}

En OO muchas veces no queremos que la gente se preocupe por cual clase u objeto está haciendo realmente el trabajo, los usuarios de la clase "Gato" hacen llamado del método "llenarCaja" pero estos no saben si la misma clase hace el trabajo o no, sin embargo a ellos les parece que la propia clase "Gato" lo hace, no tienen ni idea de que

72

Page 73: Sun Certified Java Programmer-Buenas Practicas

existe algo como una clase llamada "CajaDeArena" que es quien realmente hace el trabajo.

Toda la explicación anterior nos servirá para entender mejor otro de los conceptos de la programación orientada a objetos, uno de los más útiles si sabemos cómo utilizarlo correctamente: el polimorfismo.

POLIMORFISMO

Cualquier clase que pase la prueba de tener la relación "IS-A" puede ser considerada polimórfica, todos los objetos en Java son polimórficos ya que pasan la prueba de la relación IS-A, tanto para su propio tipo como para con la clase "Object". Debemos recordar la única forma de a un objeto es a través de una variable de referencia, hay algunas cosas claves que debemos recordar de las variables de referencia:

Una variable de referencia puede ser sólo de un tipo y una vez declarado este tipo nunca podrá cambiar (aunque el objeto al que hace referencia puede cambiar). Una referencia es una variable, por lo cual su valor puede ser reasignada a otros objetos a menos que esta sea declarada como final.

Un tipo de variable de referencia determina los métodos que se pueden invocar en el objeto que esta variable está referenciando. Una variable de referencia puede referirse a cualquier objeto del mismo tipo que el que está declarando, o puede referirse a cualquier subtipo del tipo declarado. Una variable de referencia puede ser declarado como un tipo de clase o un tipo de interfaz. Si la variable se declara como un tipo de interfaz, se puede hacer referencia a cualquier objeto de cualquier clase que implementa la interfaz.

Anteriormente creamos una clase "Animal" que era extendida por dos clases, "Perro" y "Gato", ahora imaginemos que tenemos una clase llamada "Gaviota", después queremos hacer que algunos tipos de Animal vuelen o se puedan elevar en el aire y otros no como el caso de "Gaviota" que si puede elevar, por el contrario "Perro" y "Gato" no pueden hacerlo, para esto podríamos crear una clase

73

Page 74: Sun Certified Java Programmer-Buenas Practicas

llamada "Elevable" con un método "elevar" y hacer que unos tipos de Animales puedan elevarse y otros no, pero también queremos que todos los tipos de Animal se muevan que es lo que nos permite el método "muevete" de la clase "Animal", pero esto no funcionaría ya que Java solo soporta la herencia simple, esto significa que una sub clase sólo puede tener una clase padre, es decir lo siguiente es incorrecto:

public class Gaviota extends Animal, Elevable // NO!!{ //Cualquier codigo aquí}

Una clase NO puede extender de más que de sólo una clase, ante estos casos la solución sería crearse una interface llamada "Elevable" y solo las sub-clases de "Animal" que puedan volar implementen esta interfaz, dicha interfaz quedaría de la siguiente manera:

public interface Elevable{ public void volar();}

public class Animal{ public void muevete() { System.out.println("Me estoymoviendo"); }}

A continuación mostramos la clase "Gaviota" que implementa esta interfaz:

public class Gaviota extends Animal implements Elevable{

74

Page 75: Sun Certified Java Programmer-Buenas Practicas

public void volar() { System.out.println("¡Estoy volando!"); }}

Ahora tenemos a la clase "Gaviota" que pasa la prueba de la relación IS-A tanto para la clase "Animal" como para la interfaz "Elevable", esto significa que una "Gaviota" puede ser tratada polimórficamente como una de estas cuatro cosas en un momento dado, dependiendo del tipo declarado de la variable de referencia:

Como un "Object" (ya que todas las clases heredan de la clase Object).

Como un "Animal" (ya que está extendiendo de la clase Animal).

Como una "Gaviota" (ya que es lo que es).

Como un "Elevable" (ya que implementa de la interface Elevable).

Las siguientes declaraciones son legales:

Gaviota gaviota = new Gaviota();Object object = gaviota;Animal animal = gaviota;Elevable elevable = gaviota;

Aquí solo hay un objeto Gaviota pero cuatro diferentes variables de referencia, hagamos una pregunta: ¿Cuál de las cuatro variables de referencia puede llamar al método "muevete"? Recuerden que las llamadas a métodos permitidos por el compilador se basan únicamente en el tipo declarado de la referencia, con independencia del tipo de objeto. Así que buscando en los cuatro tipos de referencia de nuevo, "Object", "Gaviota", "Animal" y "Elevable" ¿cuál de estos cuatro tipos puede llamar al método "muevete()"? La respuesta es la siguiente: El objeto "animal" como el objeto "gaviota" son

75

Page 76: Sun Certified Java Programmer-Buenas Practicas

conocidos por el compilador para poder llamar o invocar al método "muevete".

¿Qué métodos pueden ser invocados cuando el objeto "gaviota" está utilizando la referencia a la inteface "Elevable"? Sólo al método "volar()".

Un beneficio es que cualquier clase desde cualquier lugar en el árbol de herencia puede invocar a la interface "Elevable", que sucedería si tenemos un método que tiene como argumento declarado con tipo "Elevable", podrías pasar una instancia de objeto del tipo "gaviota" y cualquier otra instancia de clase que implemente a la interface "Elevable", podría usar el parámetro del tipo "Elevable" para invocar al método "volar()" pero no al método "muevete()" o cualquier otro objeto que se sabe que el compilador conocer basado en el tipo de referencia.

Otra cosa que debemos saber es que si bien el compilador solo conoce al tipo de referencia declarado, la JVM en tiempo de ejecución sabe lo que el objeto realmente es, y eso significa que incluso si el objeto "gaviota" hace una llamada al método "muevete" usando la variable de referencia "animal", si el objeto "gaviota" sobre-escribe el método "muevete", la JVM invocara a esa versión del método "muevete". La JVM mira al objeto real que se encuentra al otro extremo de la referencia, puede ver que se ha sobre escrito el método del tipo de variable de referencia declarado e invoca al método del objeto de la clase actual.

Siempre se puede hacer referencia a un objeto con un tipo de referencia variable más general (una superclase o interfaz), pero en tiempo de ejecución, las únicas cosas que son seleccionadas dinámicamente basándose en el objeto real (en lugar de tipo de referencia) son métodos de instancia (no los métodos estáticos, no las variables), sólo los métodos de instancia sobre-escritos se invocan de forma dinámica en función del tipo de objeto real.

Ejemplo:

public class Animal{

76

Page 77: Sun Certified Java Programmer-Buenas Practicas

public void muevete() { System.out.println("Me estoy moviendo"); }

public static void respirar() //Método estático { System.out.println("Estoy respirando"); }}

public class Gaviota extends Animal { public void muevete() { System.out.println("La gaviota se mueve"); }

public static void respirar() //Método estático { System.out.println("La gaviota está respirando"); }}

public class Prueba{ public static void main(String... args) { Animal a = new Gaviota(); a.muevete(); a.respirar(); }}

Salida:

La gaviota se mueve Estoy respirando

77

Page 78: Sun Certified Java Programmer-Buenas Practicas

El segundo resultado que vemos se trata de la invocación de un método estático. El comportamiento que vemos pasa porque cuando el compilador ve que se está invocando un método estático, cambia la referencia al objeto por el tipo de la clase, o sea que al final queda de la siguiente manera:

Animal a = new Gaviota();Animal.respirar();

En el primer caso el método que se invoca es el de "Gaviota" y no el de "Animal", ya que el método "muevete" no es estático sino un método de instancia; por eso dice se dice que sólo los métodos de instancia sobrecargados se invocan de forma dinámica en función del tipo de objeto real.

SOBRECARGA Y SOBRE ESCRITURA

Sobre escritura de métodosEn cualquier momento que se tenga un método que hereda de una superclase, se tendrá la oportunidad de realizar una sobre-escritura de métodos, a menos que el método este marcado con la palabra reservada final. El principal beneficio de la sobre-escritura de métodos es que se puede definir un comportamiento especial para un método de una subclase. En el siguiente ejemplo veremos cómo la clase "Gaviota" sobre-escribe el método "muevete" de la clase "Animal":

public class Animal{ public void muevete() { System.out.println("Me estoy moviendo"); }}

public class Gaviota extends Animal { public void muevete()

78

Page 79: Sun Certified Java Programmer-Buenas Practicas

{ System.out.println("La gaviota se mueve"); }}

Para métodos abstractos que se heredan desde una superclase, no se tiene otra opción más que sobre escribir-dichos métodos. Se deben implementar los métodos a menos que la subclase que los herede también este marcada como abstracta. Los métodos abstractos deben ser implementados por una subclase concreta, esto quiere decir que la subclase concreta sobre-escribe el método abstracto de la superclase. Entonces debemos pensar que los métodos abstractos son métodos que forzosamente deben ser sobre-escritos.

El creador de la clase "Animal" podría haber decidido que, a efectos de polimorfismo, todos los subtipos de Animal deben implementar el método "moverse", de una manera única y especifica. Polimórficamente, cuando alguien tiene una referencia de Animal que no refiere a una instancia de Animal, sino a una instancia de una subclase de Animal, la persona que llama debe ser capaz de invocar al método "muevete" en la referencia a "Animal", pero el objeto en tiempo de ejecución real ejecutará su propio y especifico método "muevete". Marcando el método "muevete" como abstracto es la forma que el programador de la clase "Animal" dice a todos los desarrolladores de las demás subclases: "No tiene ningún sentido para el nuevo subtipo utilizar el método genérico "muevete", por lo que tú debes implementar tu propio método "muevete". A continuación mostraremos un ejemplo de clases no abstractas:

public class Prueba{ public static void main(String... args) { Animal a = new Animal(); Animal b = new Gaviota(); //Referencia a Animal, pero objeto Gaviota a.muevete(); b.muevete();

79

Page 80: Sun Certified Java Programmer-Buenas Practicas

}}

public class Animal{ public void muevete() { System.out.println("Me estoy moviendo"); }}

public class Gaviota extends Animal { public void muevete() { System.out.println("La gaviota se mueve"); }

public void volar() { System.out.println("La gaviota vuela"); }}

En el código anterior la clase "Prueba" utiliza una referencia a "Animal" para invocar un método en el objeto "Gaviota", hay que recordar que el compilador solo permitirá que se invoquen métodos que se encuentran en la clase "Animal", cuando usas una referencia a un "Animal". Por lo tanto, el siguiente código no es legal:

Animal c = new Gaviota();c.volar(); // No puedes invocar a "volar()", Animal no tiene un método volar

Para reiterar: el compilar solo mira el tipo de referencia y no el tipo de instancia en tiempo de ejecución. El polimorfismo nos permite tener referencias a súper tipos o a tipos más abstractos (incluyendo las interfaces) para referir a uno de estos subtipos (incluyendo implementación de interfaces).

80

Page 81: Sun Certified Java Programmer-Buenas Practicas

El método que sobre-escribe no debe tener un modificador de acceso más restringido que el del método a ser sobre-escrito. Por ejemplo no se puede sobre-escribir un método marcado con el modificador de acceso public y cambiarlo a protected. Pensemos en los siguiente: si la clase "Animal" tiene un método "comer()" marcado "public" y alguien tiene una referencia de "Animal", es decir, una referencia declarada del tipo "Animal", se asume que es seguro llamar al método "comer()" en la referencia de "Animal", independientemente de la instancia actual que la referencia a "Animal" está invocando. Si una subclase cambia el modificador de acceso del método sobre-escrito, entonces cuando la JVM invoque la verdadera versión del objeto "Gaviota" en lugar de la versión del tipo de referencia "Animal", el programa perecerá, a continuación un ejemplo:

public class Prueba{ public static void main(String... args) { Animal a = new Animal(); Animal b = new Gaviota(); // Referencia a Animal, pero objeto Gaviota a.muevete(); b.muevete(); }}

public class Animal{ public void muevete() { System.out.println("Me estoy moviendo"); }}

public class Gaviota extends Animal {

81

Page 82: Sun Certified Java Programmer-Buenas Practicas

private void muevete() //Marcado como privado { System.out.println("La gaviota se mueve"); }}

Si el código anterior compilara (cosa que no hará) obtendríamos el siguiente error en tiempo de ejecución:

Animal b = new Gaviota(); //Referencia a Animal, pero objeto Gaviotaa.muevete(); //Ocurre una crisis en tiempo de ejecución

La variable "b" es del tipo "Animal", el cual tiene un método público llamado "muevete", pero hay que recordar que en tiempo de ejecución, Java utiliza la invocación de métodos virtuales para seleccionar dinámicamente la versión real del método que se ejecutará, basado en la del tipo del objeto en tiempo de ejecución. Una referencia de "Animal" puede referir siempre a una instancia de "Gaviota" porque "Gaviota" IS-A "Animal" ("Gaviota" ES UN "Animal"). Lo que hace posible que una instancia de una superclase referencie a una instancia de la subclase es que la subclase es capaz de hacer todo lo que la superclase puede hacer (ya que recibe el comportamiento de esta por herencia). Esto quiere decir que cualquiera con una referencia a "Gaviota" usando una instancia deAnimal (Animal a = new Gaviota();) es libre de llamar a todos los métodos accesibles de "Animal", no importa si "Gaviota" sobre-escribe los métodos de "Animal" o simplemente los hereda. Un método-sobre escrito debe cumplir con el contrato de la superclase. 

Las reglas para sobre escribir un método son las siguientes:

La lista de argumentos debe ser exactamente igual (del mismo tipo y en el mismo orden) que el método a sobre-escribir. Si esta lista no coincide en realidad lo que se está haciendo es una sobrecarga del método (que puede que no sea nuestra intención). El tipo de retorno del método sobre escrito debe ser el mismo o un subtipo del declarado en el método de la súper clase.

82

Page 83: Sun Certified Java Programmer-Buenas Practicas

El modificador de acceso no debe ser más restrictivo que del método a sobre-escribir. Por ejemplo, si en la clase base tenemos un método "public" NO podemos sobre-escribirlo poniéndole un modificador "protected".

El modificador de acceso puede ser menos restrictivo que el del método a sobre-escribir. Por ejemplo, si en la clase base tenemos un método "protected" SI podemos sobre-escribirlo poniéndole un modificador "public".

Los métodos de instancia solo pueden ser sobre-escritos si estos son heredados por la subclase. Una subclase dentro del mismo paquete que su superclase puede sobre escribir cualquier método de la súper clase que NO esté marcado comoprivate o final. Una subclase en diferente paquete puede sobre-escribir solo los métodos no finales marcados public oprotected (los métodos protected son heredados por la subclase).

Los métodos sobre-escritos no deben lanzar excepciones marcadas (en las que sean necesarias un try catch) que sean nuevas o más amplias que aquellas declaradas en el método que sobre-escribe. Por ejemplo un método que declara un "FileNotFoundException" no puede ser sobre-escrito por un método que declara un "SQLException", "Exception", "IOException" o cualquier otra excepción a menos que esta sea una subclase de "FileNotFoundException".

Los métodos sobre-escritos pueden lanzar excepciones más específicas (excepciones que extiendan de la excepción que se está declarando) o lanzar menos excepciones. Solo porque el método a sobre-escribir puede lanzar excepciones no quiere decir que el método sobre-escrito lanzara estas mismas excepciones, un método sobre-escrito no tiene que declarar una excepción que nunca lanzará. Independientemente de que el método a sobre-escribir las declare. No se puede sobre-escribir un método marcado con final.

No se puede sobre-escribir un método marcado con static.

Si un método no puede ser heredado entonces no puede ser sobre-escrito. Hay que recordar que la sobre-escritura implica que se está re-implementando un método que está heredanddo. Por ejemplo el siguiente código no es legal:

83

Page 84: Sun Certified Java Programmer-Buenas Practicas

public class Prueba{ public static void main(String... args) { Gaviota a = new Gaviota(); a.muevete(); //No es legal porque Gaviota no hereda muevete(), muevete() es declarado private en Animal } }

public class Animal{ private void muevete() { System.out.println("Me estoy moviendo"); }}

public class Gaviota extends Animal {}

Invocando a la versión de la superclase de un método sobre escrito

Tal vez queramos tomar ventaja de una parte del código en la versión de la súper clase de un método y aun así sobre-escribirlo para proveer algún comportamiento especifico adicional. Esto es como decir: "Ejecuta la versión de la superclase de un método, después vuelve y termina con mi código para hacer un comportamiento adicional en el método de la subclase", esto es fácil de hacer utilizando la palabra reservada super como en el ejemplo siguiente:

public class Prueba{ public static void main(String... args) { Gaviota a = new Gaviota(); a.muevete(); }

84

Page 85: Sun Certified Java Programmer-Buenas Practicas

}

public class Animal{ public void muevete() { System.out.println("Me estoy moviendo"); }}

public class Gaviota extends Animal { public void muevete() { //podemos agregar alguna cosa mas super.muevete(); //invoca al código de la superclase (Animal) }}

Usar la palabra reservada "super" para invocar a un método sobre-escrito sólo se aplica para métodos de instancia, hay que recordar quelos métodos marcados con static no pueden ser sobre escritos.

Sobrecarga de métodosLa sobrecarga de métodos permite usar el mismo nombre de un método en una clase, pero con diferentes argumentos y, opcionalmente, un tipo de retorno diferente, el código asume la carga de hacer frente a diferentes tipos de argumentos en lugar de obligar a la persona que llama a hacer las conversiones antes de invocar el método. Las reglas a seguir para la sobrecarga son simples:

Para sobrecargar métodos se debe cambiar la lista de argumentos. Para sobrecargar métodos se puede cambiar el tipo de datos de retorno. Para sobrecargar métodos se puede cambiar el modificador de acceso.

85

Page 86: Sun Certified Java Programmer-Buenas Practicas

Para sobrecargar métodos se pueden declarar excepciones que sean nuevas o más amplias. El método puede ser sobrecargado en la misma clase o en una subclase. Si la clase "A" define un método "hacerAlgo(int i)", la subclase "B" puede definir un método llamado "hacerAlgo(String s)", sin sobre-escribir el método "hacerAlgo(int i)" de la súper clase que toma como argumento un entero. Dos métodos con el mismo nombre pero en diferentes clases pueden ser consideradas sobrecargados, si la sub-clase hereda una versión del método y luego declara otra versión sobrecargada en la definición de la clase.

Formas legales de sobrecargar un método

El método que queremos sobrecargar es el siguiente:

public void unMetodo(String str, int t, double d){}

Los siguientes métodos son legales para sobrecargar el método anterior:

public void unMetodo(String str, int t){}

public int unMetodo(String str, double d){}

public void unMetodo(String str, int t)throws IOException{}

Invocando a métodos sobrecargadosCuando un método es invocado, más de un método con el mismo nombre puede existir para el tipo de objeto que estamos invocando. Por ejemplo la clase "Gaviota" puede tener tres métodos con el mismo nombre pero con diferente lista de argumentos, estos serán métodos sobrecargados. Definir cuál de los diferentes métodos se desea invocar dependerá de la lista de argumentos que contenga. Si estamos invocando a un método que tiene un "String" como argumento, el método que tiene un "String" como argumento será llamado. A continuación veremos un ejemplo:

86

Page 87: Sun Certified Java Programmer-Buenas Practicas

public class UnaClase{ public int unMetodo(int x, int y) { return x + y; }

public double unMetodo(double x, double y) { return x + y; }}

public class Prueba{ public static void main(String... args) { UnaClase a = new UnaClase(); int x = 9; int y = 5; int resultado = a.unMetodo(x, y); //¿Qué versión de "unMetodo" es invocado?

double otroResultado = a.unMetodo(5.2, 3.6); //¿Qué versión es invocada? }}

En el primer caso se llama a la primera versión del método "unMetodo(x,y)" ya que se está pasando a dos enteros y en el segundo caso se llama a la versión del método "unMetodo" que recibe dos argumentos del tipo double.

La invocación de métodos sobrecargados que reciben objetos en lugar de tipos primitivos es un poco más interesante. Si tenemos un método sobrecargado, que en un método toma un objeto del tipo "Animal" y otro en el que toma un objeto del tipo "Gaviota" (subclase de "Animal"). Si pasamos un objeto del tipo Gaviota cuando invocamos

87

Page 88: Sun Certified Java Programmer-Buenas Practicas

al método, invocaremos a la versión sobrecargada que toma una "Gaviota". O al menos eso parece a primera vista:

class Animal{}

class Gaviota extends Animal{}

public class UsaAnimales{ public void muevete(Animal a) { System.out.println("Estas en la versión de Animal"); }

public void muevete(Gaviota g) { System.out.println("Estas en la versión de Gaviota"); } public static void main(String... args) { UsaAnimales ua = new UsaAnimales(); Animal a = new Animal(); Gaviora g = new Gaviota(): ua.muevete(a); ua.muevete(g); }}

La salida es la que esperamos:

Estas en la versión de AnimalEstas en la versión de Gaviota

¿Pero qué sucede si usamos una referencia de "Animal" para un objeto "Gaviota"?

Animal animalReferenciaGaviota = new Gaviota();

88

Page 89: Sun Certified Java Programmer-Buenas Practicas

ua.muevete(animalReferenciaGaviota);

¿Cuál de las versiones del método "muévete" es invocada?, podríamos pensar que la respuesta es: "El que toma una "Gaviota", ya que es un objeto del tipo "Gaviota" el que en tiempo de ejecución está siendo pasado al método". Pero no es así como funciona. El código anterior imprime lo siguiente:

Estas en la versión de Animal

El objeto en tiempo de ejecución es una "Gaviota" y no un "Animal". La elección de cuál método sobrecargado se deberá llamar no es decidido dinámicamente en tiempo de ejecución. Sólo hay que recordar que el tipo de referencia, no el tipo de objeto, determina qué método sobrecargado es llamado.

Para resumir lo que hemos visto hasta ahora de sobrecarga y sobre-escritura: cuál versión de un método sobre-escrito es llamado (en otras palabas, desde cuál clase en el árbol de herencia) es decidido en tiempo de ejecución por el tipo de objeto; pero cuál versión del método sobrecargado se llamará está basado en el tipo de referencia del argumento que se pasa en tiempo de compilación. Si invocamos un método pasando una referencia de "Animal" para un objeto "Gaviota", el compilador solo sabe que está recibiendo un "Animal", por lo que elige la versión sobrecargada que toma un "Animal". No importa que en tiempo de ejecución realmente se pase una "Gaviota".

Polimorfismo en métodos sobrecargados y sobre-escritos

¿Cómo trabaja el polimorfismo en métodos sobrecargados? Si pasamos una referencia de "Animal", el método que toma un "Animal" será llamado, incluso si el objeto actual pasado es una "Gaviota". Una vez que la "Gaviota" disfrazada de "Animal" entra en el método, sin embargo, el objeto "Gaviota" sigue siendo una "Gaviota" a pesar de ser pasado a un método que recibe un "Animal". Entonces es verdad que el polimorfismo no determina que versión del método sobrecargado será llamado. El polimorfismo entra

89

Page 90: Sun Certified Java Programmer-Buenas Practicas

en juego cuando se decide cuál versión de un método sobre-escrito es llamado. Pero algunas veces un método puede ser ambos, sobre-escrito y sobrecargado. Imaginemos lo siguiente:

public class Animal{ public void comer() { System.out.println("El Animal genérico está comiendo"); }}

public class Gaviota extends Animal{ public void comer() { System.out.println("La gaviota está comiendo"); }

public void comer(String s) { System.out.println("La gaviota está comiendo esto: " + s); }}

Noten que la clase "Gaviota" tiene sobrecargado y sobre-escrito el método "comer" (el método sobrecargado es el que recibe unString y el sobre-escrito el que no recibe nada).

La siguiente tabla muestra cuál de las versiones del método "comer" será llamado dependiendo de cómo sea invocado:

90

Page 91: Sun Certified Java Programmer-Buenas Practicas

Código de Invocación al método

Resultado

Animal a = new Animal();a.comer();

"el Animal genérico está comiendo"

Gaviota g = new Gaviota();g.comer();

"La gaviota está comiendo"

Animal ag = new Gaviota();ag.comer();

"La gaviota está comiendo"Polimorfismo trabajando: El objeto actual ("Gaviota"), no el tipo de referencia ("Animal"), es usado para determinar cuál versión del método "comer" es llamado.

Gaviota gp = new Gaviota();gp.comer("peces")

"La gaviota está comiendo esto: peces"El método sobre cargado "comer(String s)" es llamado.

Animal a2 = new Animal();a2.comer("manzanas");

¡¡Error de compilación!!El compilador mira la clase Animal y ve que no tiene un método comer que reciba un String.

Animal ag2 = new Gaviota();ag2.comer("peras");

¡¡Error de compilación!!El compilador ve solo la referencia y sabe que "Animal" no tiene un método "comer" que reciba unString. Al compilador no le importa que el objeto actual pueda ser una "Gaviota" en tiempo de ejecución.

Diferencias entre métodos sobre cargados y sobre escritos:

91

Page 92: Sun Certified Java Programmer-Buenas Practicas

Método sobrecargado Método sobre-escrito

Argumento(s) Debe cambiar. No debe cambiar.

Tipo de retorno

Puede cambiar.No puede cambiar, excepto para retornos covariantes.

Excepciones Puede cambiar.

Pueden no declarar una excepción del método que sobre-escribe, o declarar una subclase de esta excepción. No debe lanzar nuevas excepciones o más "amplias" que aquellas declaradas en el método que sobre-escribe.

Nivel de Acceso

Puede cambiar.No pueden tener un nivel de acceso más restrictivo, pero si uno igual o menos restrictivo.

Invocación

El tipo de referencia determina cuál método sobrecargado es seleccionado, basado en el tipo de argumentos declarados. Sucede en tiempo de compilación.

El método real que se invoca es todavía una invocación de método virtual que siempre sucede en tiempo de ejecución, pero el compilador ya sabe la firma del método que se invoca. El tipo de objeto determina cuál método es seleccionado. Sucede en tiempo de ejecución.

CASTING DE VARIABLES DE REFERENCIA

Hasta el momento hemos visto como es posible y común el uso de variables de referencia genéricas para referirse a tipos de objetos más específicos, eso es el corazón del polimorfismo, como ejemplo veamos la siguiente línea de código:

Animal animal = new Gaviota();

¿Pero qué sucede si queremos usar una variable de referencia

92

Page 93: Sun Certified Java Programmer-Buenas Practicas

"Animal" para invocar un método que solo la clase "Perro" tiene? Sabemos que nos estamos refiriendo a un Perro y queremos hacer una cosa específica de Perro.

En la siguiente línea de código tendremos un arreglo de Animales y cada vez que encontremos una Perro en el arreglo, haremos algo especial de Perro. Asumamos por el momento que todo el código escrito a continuación esté correcto, solo que no estamos seguros de la línea de código que invocara al método "hacerRuido".

public class Animal{ void hacerRuido() { System.out.println("Ruido Generico"); }}

public class Perro extends Animal{ void hacerRuido() { System.out.println("Ladrando"); }

void hacerseElMuerto() { System.out.println("Muriendo"); }}

public class Prueba{ public static void main(String... args) { Animal[] a = {new Animal(), new Perro(), new Animal()};

for(Animal animal: a)

93

Page 94: Sun Certified Java Programmer-Buenas Practicas

{ animal.hacerRuido();

if(animal instanceof Perro) { animal.hacerseElMuerto(); //Se intenta invocar un metodo de Perro? } } }}

Cuando compilemos el código anterior, el compilador nos enviara un mensaje como:

Cannot find Symbol

El compilador nos está diciendo: "Oye, la clase "Animal" no tiene un método "hacerseElMuerto"".

Ahora modifiquemos el bloque de la condicional if:

if(animal instanceof Perro){ Perro perro = (Perro) animal; // Casteando la variable de referencia perro.hacerseElMuerto();}

El nuevo y mejorado bloque de código contiene un "cast", el cual en algunos casos es llamado down casting, porque estamos bajando en el árbol de herencia a una categoría mas especifica. 

Ahora el compilador no nos dará problemas, casteamos la variable "animal" a un tipo "Perro". El compilador nos está diciendo lo siguiente: "Sabemos que nos estamos refiriendo a un objeto del tipo Perro, está bien hacer una nueva variable de referencia de tipoPerro, para referirnos a este objeto", en este caso estamos bien,

94

Page 95: Sun Certified Java Programmer-Buenas Practicas

porque antes de intentar el "cast" hicimos una prueba deinstanceof para asegurarnos.

Es importante saber que el compilador confía en nosotros cuando hacemos un down casting aun cuando pudiéramos hacer algo como lo siguiente:

public class Animal{}

public class Perro extends Animal{}

public class Prueba{ public static void main(String... args) { Animal animal = new Animal(); Perro perro = (Perro)animal; //Compila pero fallara después }}

Este código compilara correctamente pero cuando intentemos ejecutarlo vamos a obtener una excepción como la siguiente:

java.lang.ClassCastException

¿Por qué no podemos confiar en el compilador para que nos ayude en esto? ¿No puede ver que animal es del tipo "Animal"? Todo lo que el compilador puede hacer es comprobar que los dos tipos pertenezcan al mismo árbol de herencia, por lo que dependiendo de lo que el código puede hacer antes del down casting, es posible de que "animal" sea del tipo "Perro". El compilador puede permitir cosas que posiblemente puedan funcionar en tiempo de ejecución. Sin embargo si el compilador sabe con certeza que el cast no puede funcionar, entonces la compilación fallara. El siguiente bloque de código NO compilará:

Animal animal = new Animal();Perro p = (Perro) animal;

95

Page 96: Sun Certified Java Programmer-Buenas Practicas

String s = (String) animal; //animal nunca puede ser un String

En este caso obtendremos un error como el siguiente:

inconvertible types

A diferencia del down-casting, el up-casting (convertir a un tipo más general en el árbol de herencia) trabaja implícitamente porque cuando estmos haciendo up-casting estamos implícitamente restringiendo el numero de métodos que podemos invocar, lo que impide que más adelante podamos invocar a un método más especifico, por ejemplo:

class Animal{}

class Perro extends Animal{}

class Prueba{ public static void main(String … args) { Perro p = new Perro(); Animal a1 = p; //upCasting, se puede sin conversión explicita Animal a2 = (Animal)p; // upCasting, se puede con conversión explicita }}

Ambos up-castings anteriores compilaran y se ejecutaran sin excepción, porque un "Perro" ES-UN "Animal", lo que significa que lo que cualquier "Animal" pueda hacer, el "Perro" lo hará. Un "Perro" puede hacer más, pero en este punto, cualquiera con una referencia de "Animal" puede llamar con seguridad a los métodos de "Animal" en una instancia de "Perro". Los métodos de "Animal" pueden haber sido sobre-escritos en la clase "Perro", pero todo lo que importa ahora es saber que un "Perro" puede hacer todo lo que

96

Page 97: Sun Certified Java Programmer-Buenas Practicas

un "Animal" puede hacer. El compilador y la JVM saben esto también, entonces el up-casting implícito es siempre legar para la asignación de un objeto de un subtipo para una referencia a su súper tipo o interface. Si "Perro" implementa la interface "Mascota", y "Mascota" define el método "seAmigable()", un "Perro" puede implícitamente hacer casting a una "Mascota", pero el único método de "Perro" que se podrá invocar es "seAmigable()", el cual "Perro" fue forzado a implementar porque "Perro" implementó a la interface "Mascota".

Una cosa más, si "Perro" implementa a la interface "Mascota", y si "Sabueso" extiende a "Perro", pero "Sabueso" no declara que este implementando a "Mascota", "Sabueso" sigue siendo una "Mascota", "Sabueso" es una "Mascota" simplemente porque este está extendiendo a "Perro". La clase "Sabueso" puede siempre sobre-escribir cualquier método que este heredando desde "Perro", incluyendo los métodos que "Perro" implementa para cumplir con el contrato de la interfaz.

Por último, si "Sabueso" declara que implementa a "Mascota", es solamente para que los que busquen en "Sabueso" puedan ver fácilmente que "Sabueso" ES-UNA "Mascota", sin tener que mirar a la superclase de "Sabueso". "Sabueso" no tiene la necesidad de implementar el método "seAmigable()" si la clase "Perro" (súper clase de "Sabueso") ya se ha ocupado de eso. En otras palabras, si "Sabueso" ES-UN "Perro", y "Perro" ES-UNA "Mascota", entonces "Sabueso" ES-UNA "Mascota", y ya ha cumplido con los métodos de "Mascota" para la aplicación del método "seAmigable()", ya que hereda el método "seAmigable()". El compilador es suficientemente inteligente para decir: "Sé que Sabueso es un Perro, pero está bien para que sea más obvio".

Así que no hay que dejarse engañar por el código que muestra una clase concreta que declara que implementa una interfaz, pero no implementa el método de la interface, antes de poder decir si el código es legal, debemos mirar cuál es la superclase de la clase que esta implementado esta interface. Si alguna clase en el árbol de herencia ya ha implementado métodos concretos y ha declarado que ella (la superclase) implementa la interfaz, entonces la subclase no tiene

97

Page 98: Sun Certified Java Programmer-Buenas Practicas

ninguna obligación de volver a implementar (sobre-escribir) los métodos.

IMPLEMENTANDO UNA INTERFACE

Cuando implementamos una interface estamos aceptando el contrato definido por la interfaz. Eso quiere decir que estamos obligados a implementar todos los métodos definidos por la interfaz y que cualquiera que conozca los métodos de la interfaz (no como lo implementa pero si como se llaman y que retornan) puede invocar estos métodos en una clase que implementa la interfaz.

Por ejemplo si creamos una clase que implemente a la interfaz "Runnable" (para que el código pueda ejecutarse en otro un hilo), debemos implementar el método "public void run()", de lo contrario el hilo verá que no tenemos implementado el método "run" de la interface "Runnable" y provocará un error. Afortunadamente, Java impide que esta crisis se produzca mediante la ejecución de un proceso de comprobación, en el compilador, de cualquier clase que pretende implementar una interfaz.

Si la clase está implementando una interface, deberíamos tener una implementación para cada método de la interface, con unas pocas excepciones que veremos en un momento.

Por ejemplo, imaginemos que tenemos la interfaz "Rebotable", con dos métodos "rebotar()" y "setFactorRebote", la siguiente clase compilará:

public class Pelota implements Rebotable //palabra reservada implements{ public void rebotar(){} public void setFactorRebote(int fr){}}El contrato garantiza que una clase tenga todos los métodos de una interface pero no garantiza que tenga una buena o correcta implementación en el cuerpo del método. El compilador nunca se fijará que haya algo entre las llaves del método, nunca dirá que es un

98

Page 99: Sun Certified Java Programmer-Buenas Practicas

método y que debería hacer algo, solo se fija en que tenga los métodos que se describen en la interface, nada más.

Las clases que implementan una interface deben de seguir las mismas reglas para una clase que extiende a una clase abstracta.

Las reglas para que una clase NO abstracta implemente correctamente una interface son las siguientes:

Proveer implementación para todos los métodos declarados en la interface. Seguir todas las reglas para una sobre-escritura legal (overrides). No declarar excepciones, en la implementación del método, que no estén en el método definido en la interface. Se pueden declarar subclases de las declaradas por el método de la interfaz. Mantener el mismo nombre del método de la interface y el mismo tipo de dato de retorno (o un subtipo).

Una clase que implementa una interface puede ser abstracta. Por ejemplo:

abstract class Pelota implements Rebotable{}

¿Notan que algo falta?, pues no estamos implementando los métodos de la interfaz "Rebotable", y no tenemos ningún error. Si la clase que implementa una interface es una clase abstracta esta puede simplemente pasar la implementación de los métodos a la primera clase concreta que se implemente. 

Por ejemplo si tenemos la clase "PelotaPlaya" y esta extiende de la clase "Pelota", entonces la clase "PelotaPlaya" debe de implementar todos los métodos de la interfaz "Rebotable":

public class PelotaPlaya extends Pelota{ /*A pesar que no se dice en la declaración anterior (no hay implements a Rebotable)

99

Page 100: Sun Certified Java Programmer-Buenas Practicas

PelotaPlaya tiene que implementar a la interface Rebotable ya que la super clase abstracta de PelotaPlaya (Pelota) implementa a Rebotable */ public void rebotar(){} public void setFactorRebote(int fr){} /*Si la clase Pelota hubiera declarado algun método abstracto, entonces ese método debería estar implementado aquí también*/}

A menos que la implementación sea de una clase abstracta, la implementación debe tener todos los métodos definidos por la interface.

Hay dos reglas más que debemos saber:

1. Una clase puede implementar más de una interface, por ejemplo:

public class Pelota implements Rebotable, Runnable, Serializable{}Podemos extender solo a una clase, pero implementar a muchas interfaces. Pero recuerden que la sub-clasificación (extends) define quién y qué es nuestra clase, mientras que la implementación (implements) define una función que puede desempeñar o un sombrero que nuestra clase puede usar, a pesar de lo diferente que podría ser de la otra clase que implemente la misma interface (pero de un árbol de herencia diferente). Por ejemplo: "Persona extends SerHumano", pero una "Persona" también puede ser (implements) "Programador", "Empleado", "Pariente".

2. Una interface puede también extender a otra interface, pero nunca implementar nada, por ejemplo:

100

Page 101: Sun Certified Java Programmer-Buenas Practicas

public interface Rebotable extends Movible{}

¿Qué significa esto? La primera clase concreta que implemente "Rebotable" debe implementar todos los métodos de esta interface, pero también debe implementar todos los métodos de la interface "Movible". La sub-interface simplemente está agregando más requerimientos al contrato de la super-interface.

Una interface puede extender a más de una interface, pero sabemos que cuando hablamos de clases esto es ilegal, por ejemplo:

public class Programador extends Empleado, Geek{} //Esto es ilegal!!

Como mencionamos anteriormente una clase no permite extender a más de una clase en Java, una interface, sin embargo, puede implementar a más de una interface, por ejemplo:

public interface Rebotable extends Movible, Inflable //OK{ void rebotar(); void setFactorRebote(int fr);}

interface Movible{ void muevete();}

interface Inflable{ void inflate();}

En el próximo ejemplo, "Pelota" requiere implementar la interface "Rebotable", pero también debe implementar todos los métodos de las interfaces que "Rebotable" ha extendido, incluyendo cualquier interface que aquellas interfaces extienden, y así sucesivamente hasta llegar a la parte superior de la pila. "Pelota" debe tener lo siguiente:

101

Page 102: Sun Certified Java Programmer-Buenas Practicas

public class Pelota implements Rebotable{ public void rebotar(){} //implementando metodos de la public void setFactorRebote(int fr){} //interface Rebotable

public void muevete(){} //implementando de Movible

public void inflate(){} //implementando de Inflable}

Si la clase "Pelota" no implementa cualquiera de los métodos de "Rebotable", "Movible" o "Inflable", el compilador encontrará errores, al menos que "Pelota" sea una clase abstracta. En el caso de que "Pelota" sea una clase abstracta esta puede implementar todos, algunos o ningún método de las interfaces, puede dejar la implementación a una sub clase concreta de "Pelota", por ejemplo:

abstract class Pelota implements Rebotable{ public void rebotar(){} //define comportamiento de la public void setFactorRebote(int fr){} //interface Rebotable

/*no implementa el resto de los métodos, deja el resto para una sub-clase concreta*/}

class PelotaFutbol extends Pelota{ /*implementando métodos que pelota no hizo*/ public void muevete(){} //implementando de Movible

102

Page 103: Sun Certified Java Programmer-Buenas Practicas

public void inflate(){} //implementando de Inflable

/*PelotaFutbol puede elegir sobre escribir el método rebotar implementado en pelota*/ public void rebotar(){} }

Comparando ejemplos de abstracto y concreto de extender e implementar:

Ya que "PelotaFutbol" es la primera clase concreta que Implementa "Rebotable", esta debe implementar todos los métodos de la interface "Rebotable", excepto aquellos definidos en la clase abstracta "Pelota". Ya que "Pelota" no provee implementación de los métodos de la interface "Rebotable", es necesario que "PelotaFutbol" implemente todos aquellos métodos.

103

Page 104: Sun Certified Java Programmer-Buenas Practicas

TIPOS CORRECTOS DE RETORNO

El objetivo de esta sección es cubrir dos aspectos de los tipos de retorno:

1. Qué se puede declarar como un tipo de retorno, y 2. Qué se puede retornar como un valor.

Qué se puede y no se puede declarar es muy sencillo, pero esto depende si estamos sobre-escribiendo un método, heredado o simplemente declarando un método nuevo (el cual incluye sobrecarga de métodos). Daremos solo una pequeña mirada a las diferencias entre las reglas para los tipos de retorno para métodos sobre cargados y sobre-escritos (overloaded y overriding).

Declaración de tipos de retorno

Veremos qué está permitido para declarar como un tipo de retorno, lo cual depende primero en que si estamos sobre-escribiendo, sobrecargando o declarando un nuevo método.

Tipos de retorno en métodos sobre cargados

Recuerden que un método sobrecargado no es más que una forma para la reutilización del mismo nombre de un método pero con diferentes argumentos. Un método sobrecargado es un método completamente diferente de cualquier otro método con el mismo nombre. Si heredamos un método pero lo sobrecargamos este en una subclase, este no está sujeto a las restricciones de sobre-escritura, lo cual implica que podemos declarar cualquier tipo de retorno. Lo que no podemos hacer es cambiar solo el tipo de retorno. Para sobre cargar un método solo debemos cambiar la lista de argumentos, por ejemplo:

public class Animal{ void muevete(){};}

104

Page 105: Sun Certified Java Programmer-Buenas Practicas

public class Perro extends Animal{ String muevete(int numeroPasos) { return null; }}

Noten que el método de "Animal" tiene un tipo de retorno diferente que el método de "Perro". Esto está bien. En el momento que se ha cambiado la lista de argumentos, hemos sobrecargado el método, entonces el tipo de retorno no tiene que coincidir con el del método de la súper clase, pero por ejemplo, lo siguiente no está permitido: 

public class Animal{ void muevete(){};}

public class Perro extends Animal{ String muevete() //Error, no se puede cambiar solo el tipo de retorno { return null; }}

Sobre escritura y tipos de retorno, retornos covariantesCuando una clase quiere cambiar la implementación del método de un método heredado (sobre-escrito), la subclase debe definir un método que debe coincidir exactamente con el método heredado. O, a partir de Java 5, se le permite cambiar el tipo de retorno en el método sobre-escrito siempre y cuando el nuevo tipo de retorno es un subtipo del tipo de retorno declarado en el método a sobre-escribir (superclase).

class Alpha

105

Page 106: Sun Certified Java Programmer-Buenas Practicas

{ Alpha hacerAlgo(char c) { return new Alpha(); }}

class Beta extends Alpha{ Beta hacerAlgo(char c) //sobre escritura legal en Java 1.5 { return new Beta(); }}En Java 5 este código compilara sin problemas, pero si intentamos compilar este código con Java 1.4, mostrará el siguiente error:

attempting to use incompatible return type

Otras reglas aplicadas a la sobre-escritura, incluyen aquellas para modificadores de acceso y declaración de excepciones.

Retornando un valorTenemos que recordar solo 6 reglas para retornar un valor:

1. Podemos retornar null a los métodos que tengan como tipo de retorno un objeto, ejemplo:

public Button hacerAlgo{ return null;}

2. Podemos declarar un arreglo como tipo de retorno sin problemas, ejemplo:

public String[] go(){

106

Page 107: Sun Certified Java Programmer-Buenas Practicas

return new String[] {"Alan", "Alex", "Diego", "y Peke", "Henry", "Cesar", "Pedroww"};}

3. En un método con un tipo de retorno primitivo, podemos retornar cualquier valor o variable que pueda ser implícitamente convertido al tipo de retorno declarado, ejemplo:

public int go(){ char c = ‘c’; return c; //char es compatible con int}

4. En un método con un tipo de retorno primitivo, podemos retornar cualquier valor o variable que pueda ser explícitamente convertido al tipo de retorno declarado, ejemplo:

public int go(){ float f = 32.5f; return (int) f}

5. No debemos retornar nada desde un método que tiene como tipo de retorno "void", ejemplo:

public void go(){ return "Esto es todo"; //Error, no es legal}

6. En un método con un tipo de retorno con referencia a un objeto, podemos retornar cualquier tipo de objeto que pueda ser implícitamente convertido al tipo de retorno declarado, ejemplo:

107

Page 108: Sun Certified Java Programmer-Buenas Practicas

public Animal getAnimal(){ return new Perro(); //Asumiendo que Perro extiende de Animal}

public Object getObject(){ int[] nums = {1, 2, 3}; return nums; //retorna un arreglo de enteros, el cual sigue siendo un //objeto}

public interface Rebotable{}

public class Pelota implements Rebotable{}

public class TestPelota{ //Método con una interface como tipo de retorno public Rebotable getRebotable() { return new Pelota(); //Retorna un implementador de la interface }}

Y con eso terminamos todo lo concerniente con las reglas para el retorno de tipos de valor. Ahora veremos algunos detalles interesantes sobre constructores e instanciación de objetos.

CONSTRUCTORES E INSTANCIACIÓN

Los objetos son construidos. No podemos crear un nuevo objeto sin invocar a un constructor. No podemos crear un nuevo objeto sin invocar al constructor del objeto actual y a los constructores de sus superclases. 

108

Page 109: Sun Certified Java Programmer-Buenas Practicas

Los constructores son código que se ejecuta cuando usamos la palabra reservada "new". También pueden ser bloques de inicialización que se ejecutan al escribir "new" (son como constructores pero un poco distintos), pero vamos a cubrir estos (bloques de inicialización), y sus homólogos de la inicialización estática, más adelante. Vamos a ver cómo se codifican los constructores y como trabajan en tiempo de ejecución.

Constructores Básicos

Todas las clases, incluyendo las clases abstractas, deben tener un constructor, pero solo porque una clase deba de tener un constructor no quiere decir que necesitamos escribirlo explícitamente. Un constructor es algo como esto:

public class Rectangulo{ Rectangulo(){} //Constructor para la clase Rectangulo}

Un constructor no retorna ningún tipo de valor. Hay dos cosas que siempre debemos recordar acerca de los constructores: 

1. 1. No retornan ningún tipo de valor y

2. 2. Llevan el mismo nombre que la clase.

Típicamente los constructores son utilizados para inicializar el estado de las variables de instancia, por ejemplo:

public class Rectangulo{ private int ancho; private int altura;

public Rectangulo (int ancho, int altura) { this.ancho = ancho; this.altura = altura; }

109

Page 110: Sun Certified Java Programmer-Buenas Practicas

}

En el caso anterior, la clase "Rectangulo" no tienen un constructor sin argumentos, lo que significa que el siguiente código fallará en tiempo de compilación:

Rectangulo r = new Rectangulo(); //No compila, no coinciden el constructor

Pero el siguiente código, compilará correctamente: 

Rectangulo r = new Rectangulo(4,2); //No hay problema, los argumentos coinciden con el constructor

Es muy común (y deseable) tener un constructor sin argumentos, independientemente del número de constructores sobrecargados que nuestra clase pueda tener (sí, los constructores pueden ser sobrecargados). De vez en cuando se tiene una clase en la que no tiene sentido crear una instancia sin necesidad de suministrar información al constructor. Un "java.awt.Color", por ejemplo, no puede ser llamado mediante una llamada a un constructor sin argumentos, porque es como decirle a la JVM: "Hazme un nuevo color, y no me importa qué tipo de color sea este... tu decide", ¿realmente creen que la JVM pueda tomar decisiones de ese tipo?

Encadenamiento de constructors

Sabemos que los constructores son invocados en tiempo de ejecución cuando usamos la palabra reservada "new" de la siguiente manera:

Perro p = new Perro();

Pero ¿qué es lo que realmente sucede cuando escribimos "new Rectangulo()"?

Asumamos que "Perro" extiende (hereda) de "Animal" y "Animal" extiende de "Object"

110

Page 111: Sun Certified Java Programmer-Buenas Practicas

1. 1. El constructor de "Perro" es invocado. Cada constructor invoca al constructor de su superclase con una llamada implícita a "super()", a menos que el constructor invoque a un constructor sobrecargado de la misma clase, pero esto lo veremos más adelante.2. 2. El constructor de "Animal" es invocado ("Animal" es la superclase de "Perro")

3. 3. El constructor de "Object" es invocado ("Object" es la última superclase de todas las clases, entonces "Animal" extiende de "Object" a pesar de que no escribimos explícitamente "extends Object" en la declaración de la clase "Animal". Esto es Implícito), en este punto estamos en el cima de la pila.4. 4. Se asignan los valores explícitos a las variables de instancia. Nos referimos a las variables que declaramos como "int x = 24", donde 24 es el valor explicito (en comparación con el valor por defecto) de la variable de instancia.5. 5. El constructor de Object se completa.

6. 6. Se asignan los valores explícitos a las variables de instancia de "Animal" (si tuviera).

7. 7. El constructor de "Animal" completa.

8. 8. Se asignan los valores explícitos a las variables de instancia de "Perro" (si tuviera).

9. 9. El constructor de "Perro" completa.

A continuación mostramos como trabajan los constructores en la pila de llamadas:

1. 4.- Object()

2. 3.- Animal() llamada a super()

3. 2.- Perro() llamada a super()

4. 1.- main() llamada a new Perro()

Reglas para los constructores:A continuación mostramos lo que debemos recordar acerca de los constructores:

111

Page 112: Sun Certified Java Programmer-Buenas Practicas

Un constructor puede tener cualquier modificador de acceso, incluso "private" (un constructor privado quiere decir que solo el código dentro de la clase misma puede instanciar un objeto de ese tipo, así que si la clase con constructor privado quiere permitir que una clase pueda instanciarla, la clase debe proveer un método estático o variable que permitan acceder a una instancia creada dentro de la clase). El constructor debe tener el mismo nombre que la clase. El constructor no debe retornar ningún tipo de valor. Es legal (pero tonto) tener un método con el mismo nombre de la clase, pero esto no hace que sea un constructor. Si ven que tiene un tipo de retorno, entonces es un método en vez de un constructor. Podemos tener ambos, un método y un constructor con el mismo nombre (el nombre de la clase) en una misma clase y esto no es problema para Java.

Si no escribimos ningún constructor dentro de la clase, un constructor por defecto será automáticamente generado por el compilador. El constructor por defecto SIEMPRE es un constructor sin argumentos. Si deseamos tener un constructor sin argumentos y ya hemos escrito algún otro constructor dentro de la clase, el compilador NO proporcionara un constructor sin argumentos (o cualquier otro constructor). Es decir si hemos escrito algún constructor con argumentos no vamos a tener un constructor sin argumentos a menos que nosotros mismos lo declaremos. Todos los constructores tienen como primera declaración ya sea una llamada a un constructor sobre cargado "this()" o una llamada al constructor de la superclase "super()", aunque recuerden que esta llamada puede ser insertada por el compilador. Si escribimos un constructor (en lugar de confiar en el constructor por defecto generado por el compilador), y no escribimos una llamada a this() o a super(), el compilador insertará una llamada sin argumentos a super() por nosotros, como una primera declaración en el constructor. Una llamada a super() puede ser una llamada sin argumentos o podemos incluir argumentos en él.

112

Page 113: Sun Certified Java Programmer-Buenas Practicas

Un constructor sin argumentos no es necesariamente el constructor por defecto, aunque el constructor por defecto siempre es uno sin argumentos. El constructor por defecto es el único que el compilador provee, pero podemos insertar nuestro propio constructor sin argumentos. No podemos hacer una llamada a un método de instancia o acceder a una variable de instancia hasta que se ejecute el constructor super.

Solo variables y métodos estáticos pueden ser accedidos como parte de la llamada a super() o this(). Por ejemplo: super(Animal.NOMBRE) está bien ya que NOMBRE está declarada como variable estática. Las clases abstractas tienen constructores, y estos constructores son siempre llamados cuando una clase concreta es instanciada. Las interfaces no tienen constructores, las interfaces no son parte del árbol de herencia de un objeto. La única forma de que un constructor pueda ser invocado es dentro de otro constructor, en decir, no podemos invocar a un constructor de la siguiente manera:

public class Perro{ public Perro(){} //Contructor public void hacerAlgo() { Perro(); //llamada al constructor. Error!! }}

Determinar si un constructor por defecto será creadoEl siguiente código muestra a la clase "Perro" con dos constructores:

public class Perro{ public Perro(){} public Perro(String nombre){}}

113

Page 114: Sun Certified Java Programmer-Buenas Practicas

¿El compilador insertará un constructor por defecto para la clase anterior? ¡¡NO!!

¿Y para la siguiente modificación en la clase?

public class Perro{ public Perro(String nombre){}}

¿Ahora el compilador insertará un constructor por defecto? ¡¡NO!!

¿Y qué hay de esta clase?

public class Perro{ }

El compilador SI generará un constructor por defecto para la clase anterior, porque la clase no tiene ningún constructor definido. Y ahora ¿Qué hay de esta siguiente clase?:

public class Perro{ void Perro(){}}

Podría parecer que el compilador no crea un constructor ya que ya hay un constructor en la clase "Perro". ¿Pero realmente hay un constructor? Revisemos nuevamente la clase anterior, pues eso no es un constructor, es solo un método que tiene el mismo nombre que la clase. Recordemos que el tipo de retorno es un claro indicador de que eso es un método y no un constructor.

¿Cómo se sabe con seguridad que un constructor por defecto será creado?

114

Page 115: Sun Certified Java Programmer-Buenas Practicas

Porque no declaramos NINGUN constructor en nuestra clase.

¿Cómo se verá el constructor por defecto creado por el compilador?

El constructor por defecto tiene el mismo modificador de acceso que la clase. El constructor por defecto no tiene argumentos. El constructor por defecto incluye una llamada sin argumentos al súper constructor (super()).

¿Qué sucede si el súper constructor tiene argumentos?

Los constructores pueden tener argumentos así como los métodos, si tratamos de invocar a un método que toma por ejemplo un int y no le pasamos nada al método, el compilador se comportará de la siguiente manera: 

class Ejemplo{ void tomaUnInt(int valor){}}

class Test{ public static void main(String … args) { Ejemplo e = new Ejemplo(); e.tomaUnInt(); //Se está tratando de invocar al metodo "tomaUnInt()" sin argumentos }}

El compilador nos dirá que estamos tratando de invocar a "tomaUnInt()" sin pasarle ningún un int. El compilador, según la versión de la JVM, responderá más o menos de la siguiente manera:

115

Page 116: Sun Certified Java Programmer-Buenas Practicas

Test.java:7: tomaUnInt(int) in Ejemplo cannot be applied to ()e. tomaUnInt();

Esto quiere decir que debemos pasar los mismos valores o variables que el método acepta y en el mismo orden en el que esté declarado en el método, a lo que queremos llegar es que este mecanismo funciona exactamente igual en los constructores.

A continuación mostramos el código que genera el compilador con respecto a los constructores:

Código de clase (lo que nosotros escribimos)

Código de constructor generado por el compilador

class Perro { }

class Perro { Perro() { super(); } }

class Perro { Perro(){} }

class Perro { Perro() { super(); } }

public class Perro { }

public class Perro { public Perro() { super(); } }

116

Page 117: Sun Certified Java Programmer-Buenas Practicas

Código de clase (lo que nosotros escribimos)

Código de constructor generado por el compilador

class Perro { Perro(String nombre){} }

class Perro { Perro(String nombre) { super(); } }

class Perro { Perro(String nombre) { super(); } }

Nada, el compilador no necesita insertar nada

class Perro { void Perro(){} }

class Perro { void Perro(){} Perro() { super(); } }

("void Perro()", es un método no un constructor)

Si el súper constructor (el constructor de su inmediata súper clase o

117

Page 118: Sun Certified Java Programmer-Buenas Practicas

clase padre) tiene argumentos, debemos escribir en la llamada asuper() los argumentos adecuados.

Un punto crucial: si nuestra superclase no tiene un constructor sin argumentos, debemos escribir un constructor en la clase (subclase), porque necesitamos un lugar donde insertar, en la llamada a super, los argumentos adecuados.

El siguiente código da un ejemplo del problema:

class Animal{ Animal(String nombre){}}

class Perro extends Animal{ Perro() { super(); //He aquí el problema }}

El compilador nos arrojará algo como esto:

Perro.java:7: cannot resolve symbolsymbol : constructor Animal ()location: class Animal super(); // Problema! ^

Otra forma de explicar es la siguiente: Si nuestra superclase no tiene un constructor sin argumentos entonces la subclase no será capaz de usar el constructor por defecto que te provee el compilador, es decir, el compilador puede solo insertar una llamada a "super()" sin argumentos, no será capaz de compilar algo como esto:

class Animal

118

Page 119: Sun Certified Java Programmer-Buenas Practicas

{ Animal(String nombre){} }

class Perro extends Animal{}

El compilador nos arrojará algo como esto:

Animal.java:4: cannot resolve symbolsymbol : constructor Animal () location: class Animalclass Perro extends Animal { }^

El compilador explícitamente está haciendo el código que se muestra a continuación, donde vamos a proveer a "Perro" el mismo constructor que el compilador le proveerá:

class Animal{ Animal(String nombre){}}

class Perro extends Animal{ //el constructor a continuación es idéntico al que el compilador proveerá Perro() { super(); //¡¡Se invoca al constructor sin argumentos de "Animal" el cual no existe!! }}

Una última cosa que debemos recordar es que los constructores no se heredan, estos no son métodos los cuales si se heredan. Estos no

119

Page 120: Sun Certified Java Programmer-Buenas Practicas

pueden ser sobre-escritos pero, como hemos visto a lo largo de esta parte de constructores, estos si se pueden sobrecargar. 

Sobrecarga de constructoresSobrecargar un constructor quiere decir que escribimos diferentes versiones del constructor, cada uno tiene diferente número de argumentos, como por ejemplo:

class Perro{ Perro(){} Perro(String nombre){}}

La clase "Perro" anterior muestra dos constructores sobrecargados, uno de ellos toma un String como argumento y el otro no tiene argumentos; este es exactamente igual al constructor que el compilador provee, pero recordemos que una vez que nosotros escribamos un constructor en la clase, como por ejemplo el que toma el String, el compilador ya no nos proveerá un constructor por defecto. Si queremos un constructor sin argumentos para sobrecargar el que tiene argumentos nosotros mismo tendremos que escribirlo como en el ejemplo anterior.

Sobrecargando un constructor proveemos diferentes maneras de que se pueda instanciar un objeto de nuestra clase, por ejemplo si sabemos el nombre del Perro, podemos pasarselo a un constructor de "Perro" que toma una cadena. Pero si no sabemos el nombre podemos llamar al constructor sin argumentos para que nos provea un nombre por defecto, a continuación mostramos un ejemplo de lo que estamos hablando:

1. public class Perro {2. String nombre;3. Perro (String nombre){4. this.nombre = nombre;5. }6. 7. Perro(){

120

Page 121: Sun Certified Java Programmer-Buenas Practicas

8. this(tomaNombreAleatorio());9. }10. 11. static String tomaNombreAleatorio(){12. int x = (int) (Math.random() * 5);13. String nombre = new String[]{"fido", "tango", "aguao", "rex" , "lassy"}[x];14. return nombre;15. }16. 17. public static void main(String … args){18. Perro a = new Perro();19. System.out.println(a.nombre);20. Perro b = new Perro("flafy");21. System.out.println(b.nombre);22. }23. }

Ejecutando el código unas cuantas veces tendremos un resultado como el siguiente:

% java Perro tango flafy% java Perro lassy flafy% java Perro rex flafy% java Perro aguaoflafyA continuación mostramos la pila de llamadas para la invocación del constructor cuando un constructor es sobrecargado.

4. Object

3. Perro(String nombre) llamada a super()

121

Page 122: Sun Certified Java Programmer-Buenas Practicas

2. Perro() llamada a this(tomaNombreAleatorio())

1. main() llamada a new Perro()

Ahora vamos a describir el código desde la parte superior:

Línea 2: declaramos una variable de instancia nombre de tipo String

Línea 3 – 5: El constructor toma un String y lo asigna a la variable de instancia nombre

Línea 7: Aquí viene lo interesante. Asumiendo que cada Animal necesita de un nombre, pero la persona que lo invoca no siempre debe saber que nombre será (código de llamada), entonces nosotros establecemos un nombre aleatorio. El constructor sin argumentos genera un nombre aleatorio invocando al método "tomaNombreAleatorio()".

Línea 8: El constructor sin argumentos invoca a su propio constructor sobrecargado que toma un String llamandolo de la misma manera que sería llamado si se hace una nueva instancia del objeto, pasándole un String para el nombre. La invocación al constructor sobrecargado se hace mediante la palabra reservada "this", pero la utiliza como si fuese un nombre de método, this(). Entonces en la línea 8 simplemente se está llamando al constructor sin parámetros que está en la línea 3, pasándole un nombre aleatorio en lugar nosotros establecer el nombre.

Línea 11: Notemos que el método "tomaNombreAleatorio()" está marcado como estático (static), eso es porque no podemos invocar a un método de instancia (en otras palabras, no estático) o acceder a una variable de instancia hasta después de que el super constructor haya sido ejecutado, y hasta que el súper constructor sea invocado desde el constructor en la línea 3, en lugar que de la línea 7, la línea 8 puede usar solo un método estático para generar un nombre. Si nosotros quisiéramos especificar algún nombre en específico en vez de que se genere aleatoriamente, por ejemplo, "Thor", entonces en la línea 8 simplemente pondríamos this("Thor") en lugar de llamar al método "tomaNombreAleatorio()" para generar un nombre aleatorio.

122

Page 123: Sun Certified Java Programmer-Buenas Practicas

Línea 12: Esto no tiene nada que ver con el constructor pero es bueno aprenderlo, esto genera un numero entero aleatorio entre 0 y 4; Línea 13: Estamos creando un nuevo String, pero queremos que este String sea seleccionado aleatoriamente desde una lista, entonces necesitamos hacer esto. Para explicarlo con mejor detalle, en esta sola línea de código hacemos lo siguiente:

1. 1. Declaramos una variable del tipo String.

2. 2. Creamos un arreglo de Strings (anónimo ya que no asignamos el arreglo)3. 3. Obtenemos el String en index [x] (x contiene el numero aleatorio generado en la línea 12) del recientemente creado arreglo de Strings.

4. 4. Asignamos el String obtenido del arreglo a la variable de instancia "nombre". Esto hubiera sido más fácil de leer si hubiéramos escrito esto:

String[] lista = {"fido", "tango", "aguao", "rex" , "lassy"}; String nombre = lista[x];

Línea 18: Invocamos al constructor sin argumentos (generando un nombre aleatorio que después será enviado al constructor que recibe un String).

Línea 20: Invocamos al constructor sobrecargado que toma un String que representa el nombre.

El punto cable en el código antes escrito está en la línea 8, en lugar de llamar a super(), estamos llamando a "this()", y "this()" siempre significa llamar a otro constructor en la misma clase. ¿Pero qué sucede al momento de llamar a "this()"? Tarde o temprano el constructor "super()" será llamado, una llamada a "this()" solo significa que estamos retrasando lo inevitable, ya que algún constructor en algún lugar debe hacer una llamada a "super()".

123

Page 124: Sun Certified Java Programmer-Buenas Practicas

La regla clave: la primera línea de un constructor debe ser una llamada a "super()" o una llamada a "this()".

Sin excepciones. Si el constructor "A()" tiene una llamada a "this()", el compilador sabe que el constructor "A()" no será e que invoque a "super()".

La regla anterior significa que un constructor nunca podrá tener ambas llamadas, es decir no podrá llamar a la vez a "this()" y a "super()". Porque alguna de estas llamadas debe ser la primera sentencia en un constructor, no podemos usar ambas en un mismo constructor. El compilador tampoco insertará una llamada a "super()" si el constructor tiene una llamada a "this()".

¿Qué sucederá si nosotros tratamos de compilar el siguiente código?

class A{ A() { this("test"); }

A(String s) { this(); }}

El compilador puede que no capte el problema (esto depende del compilador). Este asume que sabemos lo que estamos haciendo. ¿Captan el problema? Sabemos que el súper constructor siempre debe de ser llamado, entonces ¿Dónde podría ir la llamada a "super()"? Recordemos que el compilador no inserta ningún constructor por defecto si nosotros ya insertamos uno o más constructores en nuestra clase, y cuando el compilador no inserta un constructor por defecto todavía puede insertar una llamada a super() en algún constructor que no tenga explícitamente una llamada a super(), a menos que, el constructor ya tenga una llamada

124

Page 125: Sun Certified Java Programmer-Buenas Practicas

a this() y recordemos que un constructor no puede tener ambas llamadas (this() y super()). Entonces en el código anterior ¿Dónde va la llamada a super()? Si los únicos dos constructores en la clase tienen una llamada a this(), en efecto nosotros tenemos el mismo problema que tendríamos si escribimos los siguientes métodos: 

public void ir(){ hacerAlgo();}

public void hacerAlgo(){ ir();}

¿Ahora pueden ver el problema? Podemos ver que el stack explota. Ya que se llamaran uno a otro indefinidamente hasta que la maquina o la JVM estalle (esperamos que lo segundo suceda primero… ¿o lo primero?). Regresando al ejemplo de los constructores podemos decir que dos constructores sobrecargados y ambos con una llamada a this() son dos constructores llamándose uno a otro una y otra y otra vez, resultando en:

% java AException in thread "main" java.lang.StackOverflowError

El beneficio de tener constructores sobrecargados es que ofrecen formas flexibles de poder instanciar un objeto de nuestra clase. El beneficio de que un constructor invoque a otro constructor sobrecargado es evitar duplicación de código, en el ejemplo anterior no hubo algún otro código para establecer el nombre, pero imaginen que después de la línea 4 aún hay muchas cosas que podríamos hacer.

125

Page 126: Sun Certified Java Programmer-Buenas Practicas

VARIABLES Y MÉTODOS ESTÁTICOS

El modificador "static" tiene un impacto tan profundo en el comportamiento de un método o una variable que debemos tratando como un concepto totalmente separado de los demás modificadores. Para entender la manera en que un miembro estático trabaja, primero veremos las razones por las cuales utilizaríamos alguno. 

Imaginen que tenemos una clase de utilidad la cual siempre se ejecuta de la misma manera, su única función es devolver, digamos, un número al azar. No nos importa en cuál instancia de la clase se ejecuta el método, ya que siempre se comporta de la misma manera. En otras palabras, el comportamiento del método no tiene ninguna dependencia en el estado (valores de variable de instancia) de un objeto. Entonces ¿Porque es necesario un objeto cuando el método nunca será instanciado? ¿Por qué no solo pedimos a la clase en si misma que ejecute el método?

Ahora imaginemos otro escenario: supongamos que queremos mantener un contador corriendo para todas las instancias de una clase en particular. ¿En dónde incluiremos la variable? No va a trabajar si la incluimos como variable de instancia dentro de las clases en las cuales se quiere hacer el seguimiento, ya que el contador siempre se iniciara a un nuevo valor por defecto cada que nosotros creamos una nueva instancia.

La respuesta a los dos escenarios anteriores (el método de utilidad que se ejecuta siempre de la misma manera, y el contador para mantener el total de instancias actualizado) es utilizar el modificador static.

Las variables y métodos marcados con static pertenecen a la clase y no a una instancia en particular. Podemos usar un método o variable static sin tener instancias de esa clase, solo necesitamos que la clase esté disponible para invocar un método static o acceder a una variable static.

Una variable estática de una clase será compartida por todas las instancias de esa clase, sólo hay una copia.

126

Page 127: Sun Certified Java Programmer-Buenas Practicas

El siguiente código utiliza una variable static que será utilizada como contador:

class Oveja{ static int contadorOvejas = 0; //Declaramos e inicializamos la variable estática public Oveja() { contadorOveja += 1; //Modificamos el valor en el constructor }

public static void main(String … args) { new Oveja(); new Oveja(); new Oveja();

System.out.println("El contador de ovejas está ahora en: " + contadorOveja); }}

En el código anterior, la variable estática "contadorOveja" es establecida en cero cuando la clase "Oveja" es cargada por primera vez por la JVM, antes de que cualquier instancia de "Oveja" sea creada (en realidad no se necesita inicializar la variable estática en cero, las variable estáticas obtienen los mismos valores por defecto que las variables de instancia obtienen). Cada vez que una instancia de la clase "Oveja" es creada el constructor de "Oveja" es ejecutado y la variable "contadorOveja" es incrementada. Cuando este código es ejecutado, tres instancias de "Oveja" son creadas en el "main()", y el resultado es el siguiente:

El contador de ovejas está ahora en: 3

127

Page 128: Sun Certified Java Programmer-Buenas Practicas

Ahora imaginemos que sucedería si la variable "contadorOveja" fuera una variable de instancia (es decir, una variable no estática):

class Oveja{ int contadorOvejas = 0; //Declaramos e inicializamos la variable de instancia public Oveja() { contadorOveja += 1; //Modificamos el valor en el constructor }

public static void main(String... args) { new Oveja(); new Oveja(); new Oveja();

System.out.println("El contador de ovejas está ahora en: " + contadorOveja); }}

Cuando este código es ejecutado, este puede todavía crear tres instancias de la clase "Oveja" en el "main()", pero el resultado es un error de compilación. No podemos compilar este código, mucho menos ejecutarlo, porque obtenemos el siguiente error:

Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - non-static variable contadorOveja cannot be referenced from a static contextSystem.out.println("El contador de ovejas está ahora en: " + contadorOveja);^

128

Page 129: Sun Certified Java Programmer-Buenas Practicas

La JVM no sabe a cuál objeto "contadorOveja" de "Oveja" estamos intentando de acceder. El problema está en que el método "main()" en sí mismo es un método estático. Un método estático no puede acceder a una variable no estática (variable de instancia) porque hay que recordar que para acceder a una variable estática no se necesita una instancia de la clase, ya que la variable pertenece a la clase misma.

Eso no quiere decir que no hay instancias de la clase con vida en la heap, si las hay, pero el método estático no sabe nada de ellas.

Lo mismo se aplica a los métodos de instancia, un método estático no pueden invocar directamente un método no estático.

Piensen que static=clase, no-estático=instancia. Haciendo que el método llamado por la JVM ("main()") sea un método estático laJVM no tiene que crear una instancia de la clase sólo para iniciar la ejecución de código.

Accediendo a métodos y variables estáticos

Puesto que no es necesario tener una instancia para invocar un método estático o acceder a una variable estática, entonces ¿cómo invocar o utilizar un miembro estático? ¿Cuál es la sintaxis? Sabemos que con un método de instancia regular, se utiliza el operador punto "." en una referencia, por ejemplo:

class Oveja{ static int contadorOvejas = 0; //Declaramos e inicializamos la variable estática public Oveja() { contadorOveja += 1; //Modificamos el valor en el constructor }

public static void main(String... args)

129

Page 130: Sun Certified Java Programmer-Buenas Practicas

{ new Oveja(); new Oveja(); new Oveja(); System.out.println("El contador de ovejas está en: " + Oveja.contadorOveja); }}

Pero para que sea realmente confuso, el lenguaje Java también permite el uso de una variable de referencia de objeto para acceder a un miembro estático:

Oveja ov = new Oveja(); ov.contadorOveja; //Accediendo a la variable estática contadorOveja usando o

En el código anterior, hemos instanciado una "Oveja" asignando "new Oveja" a la variable de referencia "ov", y después usamos la referencia "ov" para invocar al método estático. Pero a pesar de que se está utilizando una instancia específica para acceder al método estático, las reglas no han cambiado. Esto no es más que un truco de sintaxis que nos permite utilizar una variable de referencia a objeto (pero no el objeto que se refiere) para llegar a un método o variable estática, pero el miembro estático sigue ignorando la instancia utiliza para invocar al miembro estático. En el ejemplo de la Oveja, el compilador sabe que la variable de referencia "ov" es del tipo de Oveja, por lo que el método estático de la clase Oveja se ejecuta sin conocimiento o interés para la instancia de la Oveja del otro extremo de la referencia "ov". En otras palabras al compilador le importa solo que la variable de referencia "ov" sea del tipo Oveja.

El compilador en realidad hace una transformación a la llamada que acabamos de hacer. Cuando ve que estamos tratando de invocar a un miembro estático, reemplaza la variable de instancia que estamos usando por la clase en la que está dicho miembro. Esto quiere decir que reemplazara esta llamada:

Oveja ov = new Oveja();

130

Page 131: Sun Certified Java Programmer-Buenas Practicas

ov.contadorOveja;

Por esta otra:

Oveja ov = new Oveja(); Oveja.contadorOveja;

Así que como podemos ver, finalmente quedamos con una llamada al miembro estático, a través de la clase a la que pertenece dicho miembro.

La siguiente imagen describe el efecto del modificador static en métodos y variables:

131

Page 132: Sun Certified Java Programmer-Buenas Practicas

Finalmente, recuerden que los métodos estáticos no pueden ser sobrescritos. Pero esto no significa que no puedan ser redefinidos en una subclase. Redefinir y sobrescribir no son la misma cosa. A continuación mostramos un ejemplo sobre redefinir (no sobrescribir) un método marcado como static:

class Animal{ static void hacerAlgo() { System.out.println("a"); }

132

Page 133: Sun Certified Java Programmer-Buenas Practicas

}

class Perro extends Animal{ static void hacerAlgo() { System.out.println("b"); //Esto es redefinir, no sobrescribir }

public static void main(String... args) { Animal[] a = {new Animal(), new Perro(), new Animal()};

for(int i = 0; i < a.length; i++ ) { a[i].hacerAlgo(); //Invoca al método estático } }}

Ejecutando el código anterior se produce la siguiente salida:

a a a

Recuerden, la sintaxis "a[i].hacerAlgo()" es solo un atajo (un truco de sintaxis). El compilador lo sustituye con algo como "Animal.hacerAlgo()", como vimos hace un momento.

COHESIÓN Y ACOPLAMIENTO (COUPLING AND COHESION):

Estos dos temas, la cohesión y el acoplamiento, tienen que ver con la calidad de un diseño orientado a objetos. En general un buen diseño pide un acoplamiento bajo y evita un acoplamiento estrecho y

133

Page 134: Sun Certified Java Programmer-Buenas Practicas

un buen diseño orientado a objetos pide una alta cohesión y evita la baja cohesión. Como con la mayoría de las discusiones sobre diseño de orientación a objetos, las metas de una aplicación son:

Facilidad de creación. Facilidad de mantenimiento. Facilidad de mejora.

Acoplamiento

Vamos a empezar haciendo un intento sobre la definición de acoplamiento. Acoplamiento es el grado en el cual una clase sabe acerca de otra clase. Si el único conocimiento que la clase "A" tiene acerca de la clase "B", es lo que la clase "B" ha puesto de manifiesto a través de su interface, se dice que la clase "A" y "B" están débilmente acopladas, lo cual es algo bueno. Si por otro lado, la clase "A" se basa en una parte de la clase "B", que no es parte de la interface de la clase "B", entonces el acoplamiento entre estas dos clases es más estrecho, y esto no es algo bueno. En otras palabras, si la clase "A" sabe más de lo que debería de la forma en que se implementó "B", entonces "A" y "B" están estrechamente acopladas.

Usando este segundo escenario, imaginemos lo que sucede cuando la clase "B" sea mejorada. Es muy posible que el desarrollador que mejore a "B" no tenga conocimiento acerca de "A" ¿Y porque debería de tenerlo? El desarrollador de la clase "B" debe pensar (y es correcto) que todas las mejoras que no quiebren o rompan la interfaz de la clase deben ser seguras, por lo que podría cambiar alguna parte que no tenga que ver con la interface en la clase. Si las dos clases están estrechamente acopladas, este cambio provocaría que la clase "A" se quiebre.

Veamos un ejemplo obvio de acoplamiento fuerte, que ha sido posible gracias a una pobre encapsulación:

class Impuestos{ float ratio;

134

Page 135: Sun Certified Java Programmer-Buenas Practicas

float calculaImpuestoVentaPeru() { RatioImpuestoVentas riv = new RatioImpuestoVentas(); ratio = riv.ratioVentas; /*Mal, esto debería ser una llamada a un método get para obtener la variable, por ejemplo: ratio = riv.getRatioVentas("PE"); */ }}

class RatiosImpuestoVentas{ public float ratioVentas; //Debería ser privado public float ajusteRatioVentas; //Debería ser privado public float getRatioVentas(String pais) { ratioVentas = new Impuestos().calculaImpuestoVentaPeru(); //Mal otra vez hacer basado en los cálculos de la región. return ajusteRatioVentas; }}

Todas las aplicaciones orientadas a objetos que no son triviales son una mezcla de muchas clases e interfaces trabajando juntas. Idealmente, todas las interacciones entre los objetos en un sistema orientado a objetos deben utilizar sus APIs, en otras palabras, los contratos de las clases de los objetos respectivos. Teóricamente, si todas las clases en una aplicación tienen bien diseñada sus APIs, entonces debería ser posible para todas las interacciones entre las clases el uso de las APIs de forma exclusiva. Como hemos comentado anteriormente en este capítulo, un aspecto de buen diseño de una

135

Page 136: Sun Certified Java Programmer-Buenas Practicas

clase y el diseño de la API es que las clases deben estar bien encapsuladas.

Cohesión

Mientras que el acoplamiento tiene que ver con cómo las clases interactúan con las otras clases, la cohesión es acerca de cómo una simple clase es diseñada. El termino cohesión es usado para indicar el grado para el cual una clase tiene un propósito simple bien enfocado . Hay que mantener en mente que la cohesión es un concepto subjetivo al igual que el acoplamiento. Cuanto más enfocada sea la clase en una sola tarea, mayor su cohesión, lo cual es bueno. El beneficio clave de la alta cohesión, es que estas clases son mucho más fáciles de mantener (y menos frecuentes a ser cambiadas) que las clases con baja cohesión. Otro beneficio de la alta cohesión es que las clases con un propósito bien enfocado tienden a ser más reutilizables que otras clases. Veamos un ejemplo:

class ReportePresupuesto{ void conectarBaseDatos(){} void generarReportePresupuesto(){} void guardarArchivo(){} void imprimir(){}}

Ahora imaginen que su jefe llega y dice, "Eh, ¿sabes de la aplicación de contabilidad que estamos trabajando? Los clientes decidieron que también van a querer generar un informe de proyección de ingresos, y que quieren hacer algunos informes de inventario también".

También les dice que se aseguren de que todos estos informes les permitan elegir una base de datos, seleccione una impresora, y guardar los informes generados a ficheros de datos ... ¡Ouch!

En lugar de poner todo el código de impresión en una clase de informe, probablemente hubiera sido mejor usar el siguiente diseño desde el principio:

class ReportePresupuesto

136

Page 137: Sun Certified Java Programmer-Buenas Practicas

{ Opciones getOpcionesReporte() {} void generarReportePresupuesto(Opciones o){} }

class ConexionBaseDatos{ ConexionBD getConexion(){}}

class Impresion{ OpcionesImpresion getOpcionesImpresion(){}}

class AlmacenArchivos{ OpcionesGuardado getOpcionesGuardado(){}}

Este diseño es mucho más cohesivo. En lugar de una clase que hace todo, hemos roto el sistema en cuatro clases principales, cada uno con un rol muy específico (cohesión).

Como hemos construido estas clases especializadas, reutilizables, y va a ser mucho más fácil escribir un nuevo informe, dado que ya tenemos la clase de conexión de bases de datos, la clase de impresión, y la clase para guardar archivos, y eso significa pueden ser reutilizados por otras clases pueden desear imprimir un informe.

Este es el fin del segundo tutorial, donde hemos aprendido un poco sobre algunos de los conceptos más importantes de la orientación a objetos que son necesarios para el examen de certificación, y para nuestra vida como programadores en general. El la próxima entrega seguiremos aprendiendo más temas importantes para nosotros como programadores Java y en particular para los que deseen certificarse como programadores Java.

Muchas gracias a Alan Cabrera Avanto, de Trujillo Perú por este tutorial.

137

Page 138: Sun Certified Java Programmer-Buenas Practicas

Saludos.

138