Orientación a Objetos (Object Orientation) I
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:
- Una clase está constituida por su nombre y por atributos que la definen. Por llamarlo de alguna manera, esto es la información de la clase en tiempo real.
- A la par, debemos definir acciones que pueden definir esta clase. Un ejemplo sería la clase cliente, que contendría los siguientes atributos: Nombre Completo, Identificación, Fecha de Nacimiento, Posición Social... Y como acciones a realizar o métodos podría contener los siguientes: Modificar el nombre Completo, leer el nombre Completo, contratar un crédito en un banco, solicitar la baja del mismo, etc.
- Una instancia de una clase (objeto), es el efecto de darle vida a la representacion de una clase; podemos ver la clase como una plantilla y cada resultado de usar la plantilla para obtener una creación a partir de ella es un objeto o instancia de la clase, con la particularidad en el mundo informático de que el objeto vive en la memoria RAM (Puede ser persistido en otros medios a través de serialización, bases de datos orientadas a Objetos, mapear sus propiedades a tablas en bases de datos relacionales, etc...).
- Se define estado al valor contenido por los atributos de la instancia de una clase en un momento determinado del tiempo.
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:
- El programa banco crea una sucursal con sede en Madrid.
- El programa banco crea una sucursal con sede en Barcelona.
- La sucursal en Madrid crea un empleado con nombre José.
- El empleado José de la sucursal de Madrid atiende a un nuevo cliente, llamado Juan, y lo da de alta.
- El cliente Juan de la sucursal de Madrid contrata un nuevo crédito en la misma por valor de 2 millones de euros.
Vemos entonces que las clases no están aisladas, si no que se comunican entre sí mediante el paso de mensajes.
Concretando
- Un programa informático tiene como finalidad representar y gestionar información.
- Un programa informático puede ser un programa de orientación a objetos.
- En un programa de orientación a objetos está definido por un conjunto de clases.
- Las clases están definidas por un conjunto de atributos, que representan la información, y un conjunto de acciones entre las mismas, que permiten modificar la información.
- Las clases pueden crear instancias reales.
Los temas que cubre en sí el examen son:
- Encapsulamiento
- Herencia (IS-A, Has-A)
- Polimorfismo
- Overriding (Sobreescritura de métodos) y Overloading (Sobrecarga de métodos), pero en vista a que el examen es en ingles mantengamos los anglicismos.
- Casting
- Interfaces
- Tipos de Retorno en Métodos Constructores e Instanciación
- Variables y métodos estáticos
- Coupling (Acoplamiento) y Cohesión
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
- Si entre las preguntas aparece una clase en la cual debes evaluar si cumple o no la notación de JavaBean revisa que los campos no estén declarados como public, que tengan sus respectivos métodos de acceso.
- Si un setter recibe un parametro con el mismo nombre de la propiedad debe obligatoriamente utilizar la palabra this, de lo contrario la variable que viene como parámetro se estará asignando el valor a sí mismo.
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:
- clone()
- equals (Object o)
- finalize()
- getClass()
- hashCode()
- notify
- notifyAll()
- toString()
- wait()
- wait(long l)
- wait(long l, int i)
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:
- Al existir clases que heredan de Car ésta recibe la denominación de clase base
- Truck y Bus al extender de Car heredan todas las propiedades (year, model) y todos los métodos (setXXX, getXXX y accelerate) y además tienen sus propios métodos.
¿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:
- En las líneas 1 y 2 declaramos 2 objetos de tipo Bus, uno declarado como tal, y otro declarado del tipo Car, que es la clase base.
- En la línea 3 hacemos uso del método accelerate, lo cual es correcto ya que el método está en la clase base por lo cual el objeto bus puede hacer uso de él.
- En la línea 4, el objeto bus hacemos uso del método getPassengers el cual es propio de la clase Bus
- En la línea 5 ocurre lo mismo que en la línea 4, con la diferencia que el objeto que intenta hacer uso del método es bus2 el cual está declarado como tipo Car... ¡NO! Eso es imposible, la clase Car desconoce totalmente del método getPassengers... Pero si bus2 es una instancia de Bus... ¿Por qué no es posible? Tal como lo dijimos anteriormente, la instancia de bus2 como tipo Bus es creada en tiempo de ejecución, no en tiempo de compilación; para el compilador de Java, bus2 es un objeto de tipo Car, y sólo le da acceso a los métodos declarados por lo tanto al tratar de compilar lanzaría el siguiente error:
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:
- Tenemos 2 clases: Car y Truck, donde Truck hereda de Car, y tenemos la clase CleanManager que se encarga de invocar el método clean; lo importante a notar es que el método recibe un objeto de tipo Car, e invoca al método clean().
- En el método main vemos que al hacer uso de CleanManager lo podemos utilizar enviándole una instancia de Car o una instancia de Truck, como vemos el compilador lo acepta sin problemas porque Truck es un (IS-A) Car, y puede hacerse automáticamente el casting llamado también AutoBoxing.
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:
- En las líneas 14 y 15 se declaran 2 instancias de Horse y Animal respectivamente
- En la línea 16 invocamos al método doSound del objeto a que es instancia de Animal, como vemos se ejecuta el método de la clase base mostrando el mensaje “No definido”
- En la línea 17 hacemos uso del polimorfismo para asignar en a la referencia al objeto h de tipo Horse, estamos haciendo uso del Autoboxing ya que h es una referencia de una clase que hereda de la clase de la cual a es referencia, por lo tanto el compilador lo maneja perfectamente
- En la línea 18 ejecutamos el método doSound de h, como Horse sobreescribe el método de la clase base el resultado es “Caballo relincha”
- En la línea 19 ejecutamos el método doSound de a, pero ahora nos muestra el mensaje “Caballo relincha” a diferencia de lo sucedido en la línea 16, porque ahora a está direccionado a una referencia de tipo Horse.
- En la clase base tenemos la variable years declarada como pública y así mismo en Horse se declara la misma variable por lo tanto le está haciendo un Shadowing u ocultando la variable de la clase base.
- En la línea 20 imprimimos el valor de years y efectivamente el resultado es el esperado: 20 que es lo que está definido en Horse
- ¡¡En la línea 21 se imprime 10!!! ¿Como es ésto posible si la variable a es una referencia de Horse? ¿Que pasó con el polimorfismo? Aquí entra la siguiente regla:
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.
- Battery IS-A Battery
- Phone HAS-A Battery
- SmartPhone IS-A Phone
- SmartPhone HAS-A Battery
- SmartPhone IS-A Phone y HAS-A Battery
Argumentos que pueden ser errados al expresarlos pueden ser:
- Phone IS-A SmartPhone: Falso, no necesariamente ya que la relación de herencia no se aplica a la inversa, Phone no sabe de lo que SmartPhone es capaz en su implementación
- Phone IS-A Battery: Falso, la clase Phone tiene una propiedad de tipo Battery, lo cual lo convierte en una relación HAS-A.
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.
Reader Comments (5)
Podrias revisar los ejemplos,
01.public abstract class Animal {
02.public int years=10;
03.public abstract void doSound();
04.}
05.
06.public class Horse extends Animal {
07.public int years=20;
08.@Override
09.public void doSound() {
10.System.out.println("Caballo relincha");
11.}
12.
13.public static void main(String[] args) {
14.Horse h = new Horse();
15.Animal a = new Animal();
16.a.doSound();
17.a = h;
18.h.doSound();
19.a.doSound();
20.System.out.println(h.years);
21.System.out.println(a.years);
22.}
23.}
en la linea 15 quieres instanciar una clase abstracta, y eso no creo que este permitido
Asi es, efectivamente; ya esta corregido el código! Muchas gracias
Gracias por compartir...Aparte del error que habia, y que le puede pasar a cualquiera, la explicacion esta excelente...muy instructiva y facil de asimilar...
Muy buen artículo.
En el ejemplo del acceso a las variables públicas creo que falta el System.out.println("No definido");
public void doSound(){};
Muchas gracias.
Te agradezco mucho por el artículo. :D
Sería bueno actualizar el pdf con las últimas modificaciones que realizaste en el post
Saludos. Muy buen trabajo.