Orientación a Objetos (Object Orientation) I
lunes, septiembre 26, 2011 at 10:28PM
gishac in certificacion, certificacion, java, java, ocjp, ocjp, ocp, ocp, orientacion a objetos, scjp, scjp

Uno de los capítulos que cubre primordialmente el examen de certificación es el conocimiento sobre orientación a objetos aplicadas a Java; digo primordial ya que básicamente EN TODAS las preguntas del examen estos conceptos deben estar tan claros que pueden asegurarte en un 60% el éxito en el examen.

Este es uno de los capítulos más extensos de la guía de estudio base (El libro de Kathy Sierra y Bert Bates). Así que si estás un poco fuera de forma en éste tópico es un buen momento para googlear, revisar código viejo, y seguir leyendo los posts de Javahispano :)

Introducción

Empezamos por un concepto básico en Java. El lenguaje Java es un lenguaje de programación orientado a objetos, esto es, su finalidad pasa por representar la realidad modelando ésta en clases con atributos que representan las propiedades o características y métodos que representan comportamiento.

Un objeto representa una entidad real en un modelo, esto es, puede ser un auto, una cuenta bancaria, una persona... Cada objeto puede definir instancias diferentes, esto es, en un programa, cuyo cometido es gestionar un banco, tendremos diferentes objetos que tienen relaciones entre sí, por ejemplo: clientes, cuentas bancarias, préstamos adquiridos, hipotecas, sucursales bancarias, empleados del banco y categorías... Podemos tener un cliente llamado Juan y otro cliente diferente llamado Antonio, podemos tener una sucursal en Madrid y otra en Barcelona.... Es decir todo la representacion real de un modelo de un proceso representado a través del lenguaje de programación.

El programa mencionado anteriormente definiría un conjunto de clases que establecen relaciones entre sí y se pasan mensajes entre sí para comunicarse, esto es, una sucursal bancaria puede registrar nuevos clientes, los cuales, a su vez, pueden contratar préstamos, hipotecas, etc.

Entrando más en detalle, podemos ver lo siguiente:

Por otra parte, vemos que un programa es un conjunto de clases, que a la par definen instancias concretas cuyo contenido va variando con el tiempo de ejecución del programa mediante la ejecución de métodos. Además, vemos que una clase puede tener relaciones con otras clases y comunicarse con ellas a través de métodos. Veámoslo en el siguiente ejemplo:

En un programa que representa a un banco, definimos un conjunto de clases: sucursales, empleados y clientes, que a su vez, pueden crear instancias reales que representan a entidades concretas, las cuales interactúan entre sí.

Veámoslo en una secuencia en el tiempo:

Vemos entonces que las clases no están aisladas, si no que se comunican entre sí mediante el paso de mensajes.

Concretando

Los temas que cubre en sí el examen son:

Encapsulamiento

No hay mucho que decir en ésta sección más que se deben seguir los patrones y buenas prácticas de orientación a objetos manteniendo los atributos de una clase como privados o protegidos y exponerlos al mundo a través de los getters y setters utilizando la notación de javabeans para el nombrado de los métodos. ¿Las ventajas? No encontrarte con sorpresas de que se cambien valores de un objeto de otra manera que no sea los métodos destinados para aquello... o sino imagina un mundo paralelo en el cual declaras una propiedad como public y tienes un cálculo dentro del setter... es lanzar una moneda al azar sin saber que se va a cumplir el comportamiento que esperas de la fórmula del setter ya que se tiene libre acceso a modificar le valor.

Aquí un ejemplo de una clase con una declaración correcta:

private class Car
{
    private String model;
    private int year;
    private boolean automatic;
   
    public String getModel() { return this.model; } //La palabra get o set va antes del nombre de la propiedad y comienza con una letra mayúscula (Camel Case)
    public void setModel(String model) { this.model = model; } //El nombre del parámetro suele ser el mismo que la propiedad (Shadow de variables), y se identifica a la propiedad usando la palabra reservada this
    public int getYear() { return this.year; }
    public void setYear(int year) { this.year = year; }
    public boolean isAutomatic { return this.automatic; } // Para las variables de tipo boolean se utiliza el prefijo is o has, aunque no es incorrecto usar get; pero lo recomendado es usar alguno de los 2 primeros.
    public void setAutomatic(boolean automatic) { this.automatic = automatic; }
}

Tips de Examen

Herencia

La herencia está en todas partes, en el más mínimo programa en Java. Heredar de una clase significa adquirir todas sus propiedades y comportamiento. Para heredar de una clase se utiliza la palabra reservada extends. Ahora veamos el siguiente código:

public class Herencia{
   public static void main(String[] args){
       Herencia obj = new Herencia();
       System.out.println(obj.toString());
   }
}

Y... ¿dónde está la herencia? Pues, partiendo la premisa de que todo en Java es un objeto y notando que el objeto que declaramos (obj) tiene un método toString() que no lo tenemos declarado... sí, así es lo está heredando de la clase base Object, que es la clase de la cual heredan todos las clases en Java y les da los siguientes métodos:

No es objetivo de ésta revisión entrar al detalle de lo que hace cada uno de éstos métodos ya que los más importantes y que entran en el examen se revisan en otros posts correspondientes a la sección donde se utilizan. (Collections y Threading para que se emocionen xD )

Bueno, ahora veamos un ejemplo de herencia de clases (además de la herencia por default de Object)

class Car {
   protected int year;
   protected String model;
   public String getModel() {
       return model;
   }
   
   public void setModel(String model) {
       this.model = model;
   }

   public int getYear() {
       return year;
   }

   public void setYear(int year) {
       this.year = year;
   }

  public void accelerate(int value){
    //aumentar la velocidad
  }

}

class Truck extends Car{
   private double capacity;

   public double getCapacity() {
       return capacity;
   }

   public void setCapacity(double capacity) {
       this.capacity = capacity;
   }
}

class Bus extends Car{
   private int passengers;

   public int getPassengers() {
       return passengers;
   }

   public void setPassengers(int passengers) {
       this.passengers = passengers;
   }

}

Listo, hemos definido 2 clases que heredan de Car (Truck y Bus son definitivamente un Car), y además de eso implementan un comportamiento particular, así que veamos lo que hemos logrado:

¿Que se obtiene de ésto? Pues claramente una de los 2 principales objetivos/ventajas de la herencia: Reutilizar código... ¿Cómo? Pues digamos que tenemos una cantidad considerable de implementaciones de autos, podemos abstraer todos las propiedades en común que tienen todos, así mismo todos los métodos y colocarlos en una clase base (en este caso Car), para que luego cualquier clase que tenga características particulares las pueda implementar.

Bus b = new Bus();
b.accelerate(15);

Como vemos, en la clase Bus tenemos acceso al método accelerate el cual no está implementado directamente en si mismo, sino en su clase base Car. Ahora, ¿qué sucede si el Bus tiene caja de cambios automática y el método de acelerar tiene un comportamiento diferente al de su clase base? La clase Bus tiene toda la capacidad para sobreescribir el comportamiento de los métodos de su clase base (si y sólo si el método de la clase base no está marcado como final):

class Bus extends Car{
   private int passengers;

   public int getPassengers() {
       return passengers;
   }

   public void setPassengers(int passengers) {
       this.passengers = passengers;
   }

   @Override
   public void accelerate(int value){
       //aceleracion con caja de cambios automática
   }
}

¿Qué se puede notar además de que la declaración del método es idéntica (el mismo tipo de dato de retorno, el mismo nombre, y los mismos parámetros)? La anotación @Override (que no es obligatoria ya que si no se la coloca sólo genera un warning en el compilador, y además no aparece como parte de las preguntas del examen a la fecha); así que de ésta manera a pesar de que Car define un comportamiento específico para acelerar (digamos que con caja de cambios manual), Bus está en todo su derecho de implementar su propio procedimiento para acelerar haciendo un Override (Sobreescritura) al método de la clase base.

Otro de los objetivos/ventajas de la programación orientada a objetos es el Polimorfismo (Múltiples formas) la cual nos da la capacidad de que varias clases derivadas de una clase base utilicen un mismo método de una manera diferente (exactamente lo que acabamos de revisar en el párrafo anterior), ahora veamos un poco más de ésta aplicabilidad no sólo en la sobreescritura de métodos sino en como se manejan las instancias a nivel de código.

Car baseCar = new Car();
Truck truck = new Truck();
Bus bus = new Bus();
Car truck2 = new Truck();
Car bus2 = new Bus();

En el código mostrado hemos declarado 5 instancias de diferentes objetos, los 3 primeros es la típica declaración, una instancia de su respectiva clase, pero si vemos los 2 últimos ejemplos vemos una particularidad, la cual es que declaramos el objeto de tipo Car, pero al darle vida lo referenciamos como una instancia de Truck y de Bus respectivamente. Esto es 100% válido ya que un Bus es un Car (relación IS-A => Bus Is-A Car) y de igual manera Truck; prácticamente detrás de la declaración sería finalmente algo así:

Truck truck2 = new Truck(); // La instancia de truck2 es de tipo Truck

Regresando a la declaración inicial, la referencia en memoria a la cual el objeto truck2 apunta va a ser una referencia de tipo Truck, a pesar de que su declaración sea de tipo Car; y sí, el compilador de Java y cualquier IDE que utilicemos para desarrollar van a tratar a bus2 y a truck2 como si fueran de tipo Car, será en el tiempo de ejecución (runtime), que la máquina virtual de Java va a poder identificar que la instancia es de otro tipo diferente al de la clase base. Veamos ahora que consecuencias trae ésto a nivel de tiempo de compilación:

Bus bus = new Bus(); 
Car bus2 = new Bus(); 
bus2.accelerate(25); 
System.out.println(bus.getPassengers()); 
System.out.println(bus2.getPassengers());  

El análisis línea por línea:

               cannot find symbol symbol:
               method getPassengers()
               location: variable bus2......................

Entonces, ¿cuál es la ventaja de tener esa capacidad de polimorfismo si no puedo usar los métodos de la clase derivada? Pues aún estas salvo, aquí entra el concepto de Casting o Boxing, el cual permite convertir un tipo de dato a otro (siempre y cuando sea posible), entonces para hacer el casting o la conversión de la referencia de bus2 y hacer uso de sus características de Bus en tiempo de compilación lo hacemos de la siguiente manera:

Bus bus = new Bus();
Car bus2 = new Bus(); 
bus2.accelerate(25); 
Bus bus2real = (Bus) bus2;
System.out.println(bus.getPassengers()); 
System.out.println(bus2real.getPassengers());

En la línea 4 ocurre la magia del casting, aquí le decimos al compilador de Java, ey yo se que bus2 es una referencia de tipo Bus, lo puedes convertir y asignar a la variable bus2real de tipo Bus, y así tenemos acceso ahora a todos los métodos de tipo bus. Java nos permite también una manera más corta de escribir el código anterior con el mismo resultado, pero es un poco menos legible si tiene muchos castings anidados; nos ahorramos la línea 4 y la línea 6 se la puede escribir de la siguiente manera:

System.out.println( ((Bus)bus2).getPassengers());

Hacemos el casting en la misma línea, y al cerrar el paréntesis el compilador entiende que el objeto resultante es del tipo Bus y nos da acceso a toda la funcionalidad de ésta clase. Bueno, ¿y cuál es el uso que le podemos dar al polimorfismo? Combinemos el overriding de métodos y los efectos del casting para el siguiente ejemplo.

El escenario planteado, es que tenemos un objeto que se encarga de la limpieza de los vehículos, así que imaginemos el universo de autos, y cada uno con sus características propias para mantenerse en perfecto estado, ¿se imaginan la clase que controla la limpieza? Tendría que tener todas y cada una de las implementaciones de como manejar la limpieza de cada vehículo... sería un código infernal mas o menos así:

class CleanManager{
     public void clean(Car car) {
          if(car.getModel().equals(“Mitsubishi”))
               cleanMitsubishi(car);
          else if(car.getModel().equals(“Toyota”))
               cleanToyota(car);
     }
}

No es una buena práctica tener tantos if’s anidados o inclusive al mismo nivel, por eso se utiliza el polimorfismo para abstraer el comportamiento en un tipo base pero que realmente varía su comportamiento dependendiendo del tipo de instancia que está referenciada.

Analicemos, la clase CleanManager se encarga de la administración y la gestión de la limpieza, pero antes de eso preguntémonos, ¿quién realmente sabe el proceso que debe seguir para llevar el cuidado del auto? Claro el auto mismo, así que con el ejemplo planteado veamos como el polimorfismo y el casting se aplican.

class Car {
     public void clean(){
          System.out.println("Limpieza estandar de vehiculo lista!");
     }
}

class Truck extends Car{
     public void clean(){
          System.out.println("Limpieza especializada de camiones lista!");
     }
}

class CleanManager{
     public void doClean(Car car){
          car.clean();
}

public static void main(String[] args) {
     CleanManager cm = new CleanManager(); 
     cm.doClean(new Car()); 
     cm.doClean(new Truck());
     }
}

El análisis:

El resultado de la ejecución del programa sería:

     Limpieza estandar de vehiculo lista!
     Limpieza especializada de camiones lista!

Aquí está la magia del polimorfismo, como vemos el metodo doClean de la clase CleanManager invoca al metodo clean de la referencia de Car que recibe, ¿entonces por qué los mensajes son diferentes? La respuesta es porque las instancias son diferentes:

En la línea 20 se envía una instancia de Car, mientras que en la línea 21 es una instancia de Truck, por lo tanto al momento de invocar al método clean() se llamará al de la referencia correspondiente. En el examen es muy común éste tipo de preguntas donde el juego de que método se llama es una constante, ya que lo combinan con casi todos los otros temas del examen.

Ejemplo:

public class Animal {
     public int years=10;
     public void doSound(){};
}

public class Horse extends Animal {
     public int years=20;
     @Override
     public void doSound() {
          System.out.println("Caballo relincha");
     }

public static void main(String[] args) {
     Horse h = new Horse(); 
     Animal a = new Animal();
     a.doSound();
     a = h; 
     h.doSound(); 
     a.doSound(); 
     System.out.println(h.years); 
     System.out.println(a.years); 
    }
}

Basado en los conceptos de polimorfismo que hemos revisado la salida en consola de ésta ejecución es:

     No definido
     Caballo relincha
     Caballo relincha
     20
     10

¿Que sucedió aquí? Analicemos:

La invocación de métodos polimórficos se aplica sólo a métodos de instancia. Lo único que en tiempo de ejecución va a ser dinámicamente seleccionado basado en la referencia del objeto son los métodos de instancia, NO las variables ni los métodos estáticos. Así que hay que estar muy atentos ya que aunque se aplica el Shadowing para las variables y la sobrecarga incluso para los métodos estáticos, se invocará de acuerdo al tipo del objeto, mas no dependiendo de la referencia.

public class Animal {
     public static void doSound(){
          System.out.println("No definido");
     }
}

public class Horse extends Animal {
     public static void doSound() {
          System.out.println("Caballo relincha");
     }
     public static void main(String[] args) {
          Animal a1 = new Animal(); 
          Animal a2 = new Horse(); 
          a1.doSound(); 
          a2.doSound(); 
          new Horse().doSound(); 
     }
}

Si entedemos la regla explicada anteriormente comprenderemos que el resultado de la ejecución de éste programa es el siguiente:

     No definido
     No definido
     Caballo relincha

¿Nadie dijo que el OCJP era fácil verdad? Aquí a pesar de que Horse sobreescribe el método estático doSound, en la línea 14 que esperamos que el resultado sea obvio por el polimorfismo debemos recordar que éste comportamiento dinámico aplica para métodos de instancia, no para métodos estáticos.

Hemos visto como a un objeto de una clase base le podemos asignar una referencia de una clase hija que hereda, pero, ¿ y si queremos hacer lo contrario? Tomando en cuenta las clases del ejemplo anterior, que sucede con los siguientes ejemplos?

public class Horse extends Animal {
     public static void main(String[] args) {
          Animal a1 = new Animal(); 
          System.out.println("Animal instanciado"); 
          Animal a2 = new Horse(); 
          System.out.println("Animal instanciado como Horse"); 
          Horse h2 = (Horse) a2; 
          System.out.println("Casting de Animal a Horse OK?"); 
          Horse h1 = (Horse) a1; 
          System.out.println("Casting 2 de Animal a Horse OK?"); 
     }
}

En el ejemplo que se presenta tratamos de hacer lo contrario a lo que hemos visto hasta ahora de casting (casting de subclase a clase base), estamos tratando de asignar a una subclase la referencia de una clase base utilizando casting o boxing, el resultado de la ejecución de éste programa sería:

     Animal instanciado
     Animal instanciado como Horse
     Casting de Animal a Horse OK?
     Exception in thread "main" java.lang.ClassCastException: Animal cannot be cast to Horse at Horse.main(Horse.java:20)

Como vemos la ejecución llega hasta la línea 9 donde se cae por una excepción en tiempo de ejecución (RuntimeException), ya que la referencia de a1 es de tipo Animal y bajo NINGUNA circunstancia va a poder ser convertida al tipo Horse. Ok, pero, ¿¿y cómo es posible entonces el casting de la línea 7??? Aquí es posible ya que a2 aunque es de tipo animal está instanciada como de tipo Horse en la línea 5. Hay que estar muy atento en el examen a éste tipo de preguntas ya que el resultado de la ejecución va a ser una Runtime Exception si se trata de hacer casting de una clase base hacia un tipo heredado si la instancia no es del mismo tipo; y además que el casting no es automático, el compilador lo exige.

El último tema a revisar en éste post se encuentra a nivel semántico en el examen, y es las relaciones entre objetos (Relaciones HAS-A), que también se lo nombra también como composición de objetos, y es importante conocerlo ya que hay que mezclarlo con los otros conceptos vistos previamente.

public class Battery { }

public class Phone {
     protected Battery battery;
     public Battery getBattery() { return battery; }
     public void setBattery(Battery battery) { this.battery = battery; }
}

public class SmartPhone extends Phone { }

Tenemos 3 clases, y vemos que Phone tiene una propiedad del tipo Battery (Phone HAS-A Battery / Phone tiene una Battery), mientras que SmartPhone simplemente hereda de la clase base Phone. Si hacemos una especie de conjugación del modelo de clases que tenemos podríamos expresarlo de las siguientes maneras:

*Debido a que los términos IS-A y HAS-A son utilizados como expresión los usaremos sin traducción.

Argumentos que pueden ser errados al expresarlos pueden ser:

En algunas preguntas del examen encontrarás que te plantean el enunciado IS-A, HAS-A y debes seleccionar cual es la implementación que aplica a nivel de clases lo que se expresa como oración, por ejemplo:

Un circulo es una figura geométrica que tiene radio y diámetro, las cuales a su vez son medibles. La implementación sería:

class FiguraGeometrica { }

class Medible{ private double longitud; }

class Radio extends Medible{ }

class Diametro extends Medible{ }

class Circulo extends FiguraGeometrica{
     private Radio radio;
     private Diametro diametro;
}

Por ahora es todo en éste post, con respecto a lo que falta de éste capítulo siguen posts más detallados sobre: Overriding y Overloading de métodos Casting de variables de referencia Tipos de retorno válidos Constructores e instanciación Métodos y propiedades estáticas Acoplamiento y Cohesión.

Article originally appeared on javaHispano (http://www.javahispano.org/).
See website for complete article licensing information.