La clase Object
La clase Object
en Java es la raíz de la jerarquía de herencia, siendo ancestro de todas las demás clases. Si una clase no extiende explícitamente otra, automáticamente se extiende de Object. Aunque tiene pocos métodos, como equals y toString, son vitales porque son heredados por todas las clases. La JVM realiza la promoción automática de tipos cuando se asignan o pasan argumentos de diferentes tipos de referencia, promoviendo el tipo de referencia fuente al tipo de referencia objetivo si este último es un ancestro en la jerarquía de herencia. Esta promoción automática ocurre, por ejemplo, cuando se necesita convertir cualquier tipo de clase al tipo Object debido a su posición en la jerarquía.
El método equals
El método equals
de la clase Object, heredado automáticamente por todas las demás clases en Java, tiene la siguiente interfaz pública:
public boolean equals(Object obj)
Este método permite comparar objetos para determinar si son iguales. Puede ser invocado por cualquier objeto para compararse con otro objeto mediante la llamada:
objectA.equals(objectB)
Esta llamada devuelve un valor booleano (true
o false
). Es importante destacar que no se especifica el tipo de ninguno de los objetos objectA
u objectB
, lo que significa que pueden ser instancias de cualquier clase y no necesariamente de la misma clase.
El método equals
implementa el significado más básico de "igualdad", que se refiere a la identidad de los objetos. Devuelve true
solo si objectA
y objectB
se refieren exactamente al mismo objeto.
Para ilustrar este concepto, consideremos una clase Car
que tiene tres variables de instancia: make
, year
y color
. El método equals
de la clase Car
compara los valores de estas variables de instancia para determinar si dos objetos Car
son iguales. Es importante notar que este método sobrescribe el método equals
heredado de la clase Object
.
Es común implementar el método equals
en las clases definidas por el programador para permitir la comparación de objetos basada en el contenido de sus atributos. Esto se logra comparando los valores de los atributos relevantes en lugar de simplemente comparar las referencias de los objetos.
Es importante recordar que cualquier variable de referencia puede llamar al método equals
, incluso si la clase de la variable de referencia no define su propio método equals
. En tal caso, la JVM busca el método equals
en las clases ancestro, y si no lo encuentra, utiliza el método equals
de la clase Object
. Sin embargo, es recomendable que las clases implementen sus propios métodos equals
para un comportamiento más específico y adecuado a la lógica de la aplicación.
El método toString
El método toString
es una herramienta poderosa en Java para representar objetos como cadenas legibles. Por ejemplo, cuando imprimes un objeto sin invocar explícitamente el método toString
, Java automáticamente llama a este método para generar una representación legible del objeto.
Considera el siguiente fragmento de código:
Object obj = new Object();
Car car = new Car();
System.out.println(obj.toString());
System.out.println(car.toString());
Aquí, cuando invocas toString()
en un objeto Object
, obtienes una cadena que contiene el nombre completo de la clase del objeto y su código hash. Lo mismo ocurre cuando invocas toString()
en un objeto Car
, pero la cadena resultante incluye el nombre de la clase Car
. Esto puede ser útil para depuración y seguimiento, pero generalmente es insuficiente para representar objetos de manera significativa.
Es por eso que a menudo se sobrescribe el método toString()
en las clases personalizadas para proporcionar una representación más legible y útil del objeto. Por ejemplo, en la clase Car
, podrías sobrescribir toString()
para devolver una cadena que contenga información sobre la marca, el año y el color del automóvil.
public String toString() {
return "Make: " + make + ", Year: " + year + ", Color: " + color;
}
Con esta implementación, cuando imprimas un objeto Car
, verás una cadena significativa que describe las características del automóvil, en lugar de solo su nombre de clase y código hash.
Car car = new Car("Honda", 2008, "red");
System.out.println(car.toString());
// Salida: Make: Honda, Year: 2008, Color: red
Al sobrescribir toString()
, puedes personalizar cómo se representa tu objeto como una cadena, lo que puede hacer que tu código sea más legible y fácil de depurar.
Polimorfismo
El polimorfismo es la habilidad de que una llamada a un método particular ejecute varias operaciones en instantes diferentes. Esto ocurre cuando se tiene una variable de referencia que refiere a diferentes tipos de objetos durante el transcurso de la ejecución de un programa. Cuando la variable de referencia llama al método polimórfico, el tipo de objeto de la variable de referencia determina cuál método es llamado esa vez. Muy bien, ¿no? El polimorfismo proporciona programas con bastante poder y versatilidad.
Introducción a la programación con Java, Dean & Dean, Cap. 13, Pag 456
Polimorfismo y vinculación dinámica
El polimorfismo es una de las características fundamentales de la programación orientada a objetos (POO), y se refiere a la capacidad de diferentes tipos de objetos para responder de manera diferente a la misma llamada de método. Esto significa que una variable de referencia puede referirse a diferentes tipos de objetos y, al llamar a un método en esa variable, el comportamiento real del método depende del tipo real del objeto al que se refiere la variable de referencia.
Por ejemplo, considera las clases Dog
y Cat
que tienen un método toString()
sobrescrito para representar el ladrido de un perro y el maullido de un gato, respectivamente. Luego, en un programa como Pets
, puedes tener una variable de referencia de tipo Object
que puede referirse tanto a un objeto Dog
como a un objeto Cat
. Al llamar al método toString()
en esa variable, la JVM decide dinámicamente qué versión del método toString()
(la de Dog
o la de Cat
) debe ejecutarse según el tipo real del objeto al que se refiere la variable de referencia.
Object obj;
if (stdIn.next().equals("d")) {
obj = new Dog();
} else {
obj = new Cat();
}
System.out.println(obj.toString());
Aquí, dependiendo de la elección del usuario ("d" para perro o "c" para gato), la variable obj
se asigna a un objeto Dog
o Cat
. Luego, al llamar a obj.toString()
, la JVM determinará si ejecutar el método toString()
de Dog
o de Cat
según el objeto real al que se refiere obj
.
La vinculación dinámica, también conocida como vinculación tardía, es el mecanismo que permite que esto suceda. La JVM determina qué método se debe llamar justo antes de ejecutar el método en tiempo de ejecución, en función del tipo real del objeto referenciado.
Es importante tener en cuenta que el polimorfismo no se limita solo a los métodos definidos en la clase Object
. Puedes aplicar polimorfismo a cualquier método definido en las clases derivadas, siempre que se cumplan las reglas de sobrescritura de métodos.
Por ejemplo, aunque no puedas llamar directamente a un método específico en una variable de referencia de tipo Object
, puedes usar el operador instanceof
para verificar si un objeto es una instancia de una clase específica y luego realizar operaciones específicas en función de esa verificación.
El operador instanceof
El operador instanceof
es una herramienta útil en la programación orientada a objetos que permite verificar si un objeto es una instancia de una clase específica o de alguna de sus clases derivadas. Esto es útil cuando se trabaja con referencias genéricas y se desea realizar acciones diferentes según el tipo de objeto al que se hace referencia. Por ejemplo, en el contexto del ejemplo de los perros y gatos, podríamos usar instanceof
para determinar si un objeto es un perro o un gato, y realizar acciones específicas en consecuencia, como imprimir "Meneo de la cola" si el objeto es un perro. Esto proporciona una forma simple y directa de manejar diferentes tipos de objetos en un programa.
Asignaciones entre clases en una jerarquía de clases
Este fragmento de código explora la asignación de objetos entre clases en una jerarquía de clases, específicamente cuando se trabaja con polimorfismo. Aquí tienes un resumen de los puntos clave, junto con ejemplos de código para ilustrar cada concepto:
Asignación de objetos entre clases en una jerarquía de clases:
- Es posible asignar un objeto de una subclase a una variable de referencia de una superclase.
- No es posible asignar un objeto de una superclase a una variable de referencia de una subclase.
Person p = new Student(); // Válido Student s = new Person(); // Inválido
Uso de casting para asignaciones inversas:
- Se puede utilizar casting para asignar un objeto de una clase más general a una variable de referencia de una clase más específica.
Student s = (Student) p; // Casting de una referencia Persona a Student
Polimorfismo con el operador*
instanceof
*:**- El operador
instanceof
se utiliza para verificar si un objeto es una instancia de una clase o subclase específica.
if (obj instanceof Dog) { // Hacer algo si obj es una instancia de Dog }
- El operador
Polimorfismo con arreglos:
- Se pueden crear arreglos o ArrayLists de variables de referencia genéricas y asignar diferentes tipos de objetos.
- Luego, se puede recorrer el arreglo y llamar a un método polimórfico para cada elemento.
Employee[] employees = new Employee[100]; employees[0] = new Hourly("Anna", 25.0); employees[1] = new Salaried("Simon", 4800);
Métodos polimórficos y vinculación dinámica:
- Los métodos polimórficos se resuelven en tiempo de ejecución según el tipo real del objeto.
- La JVM utiliza la vinculación dinámica para seleccionar el método adecuado para cada tipo de objeto.
employees[i].printPay(date); // Selecciona el método adecuado según el tipo de empleado
En resumen, el polimorfismo permite escribir código genérico que puede manejar diferentes tipos de objetos de manera uniforme, simplificando el diseño y la implementación del programa. La vinculación dinámica asegura que los métodos correctos se llamen en tiempo de ejecución, lo que facilita el mantenimiento y la extensibilidad del código.
Modificador abstract
abstract
es un modificador que se utiliza en la declaración de clases y métodos en Java. En el contexto de clases, indica que la clase es abstracta, lo que significa que no se puede instanciar directamente, sino que debe ser subclaseada. En el contexto de métodos, indica que el método es abstracto, es decir, no tiene implementación en la clase en la que se declara y debe ser implementado por cualquier subclase no abstracta.
En resumen, abstract
se utiliza para indicar que una clase o método es abstracto, lo que implica que su implementación debe ser proporcionada por clases descendientes no abstractas.
Es ilegal usar private o final con abstracto
- No se puede usar**
private
**con métodos abstractos o sus implementaciones: Un método abstracto define una interfaz pública que debe ser implementada por las clases hijas. Si se declara comoprivate
, no podrá ser accesible desde las clases hijas, lo cual violaría el principio de herencia y polimorfismo. Además, las implementaciones de métodos abstractos en las clases hijas no pueden serprivate
porque necesitan ser accesibles desde fuera de la clase para que puedan ser invocadas polimórficamente. - No se puede usar**
final
**con clases o métodos abstractos: El modificadorfinal
indica que una clase no puede ser subclasificada o que un método no puede ser sobrescrito en las clases hijas. Sin embargo, una clase abstracta está diseñada para ser extendida, y un método abstracto está diseñado para ser implementado por las clases hijas. Por lo tanto, usarfinal
con una clase o método abstracto contradice su propósito fundamental.
Interfaces
Las interfaces ofrecen varios beneficios en el diseño de software:
- Abstracción: Las interfaces permiten definir un conjunto de métodos sin especificar cómo se implementan. Esto permite separar la especificación de la funcionalidad de su implementación concreta, lo que promueve una mayor abstracción y modularidad en el diseño del sistema.
- Estandarización: Las interfaces establecen un contrato que las clases deben seguir al implementarlas. Esto facilita la estandarización de la comunicación entre diferentes partes del sistema, lo que mejora la cohesión y reduce la dependencia entre módulos.
- Flexibilidad: Al permitir que una clase implemente múltiples interfaces, Java permite que las clases compartan comportamientos comunes sin depender de una única cadena de herencia. Esto proporciona una mayor flexibilidad en el diseño y facilita la reutilización de código.
- Polimorfismo: Las interfaces son fundamentales para el polimorfismo en Java. Permite tratar objetos de diferentes clases de manera uniforme a través de una referencia común a una interfaz, lo que facilita la creación de código más genérico y extensible.
- Mantenibilidad: El uso de interfaces hace que el código sea más mantenible y escalable. Como las clases están acopladas a una interfaz en lugar de una implementación concreta, es más fácil cambiar o extender la funcionalidad sin afectar otras partes del sistema.
Algunos de los puntos clasves en el momento de utilizar interfaces son
- Estandarización de la comunicación entre clases: Una interfaz en Java especifica los encabezados de un conjunto de métodos que deben ser implementados por una clase. Sirve como un contrato entre el diseñador del programa y los implementadores, estableciendo una comunicación estandarizada entre diferentes clases. Este uso de interfaces es crucial para proyectos de programación grandes, donde varias clases necesitan interactuar de manera consistente.
- Almacenamiento de constantes universales: Además de especificar métodos, una interfaz puede contener constantes nombradas. Implementar una interfaz proporciona acceso a estas constantes, lo que permite una fácil referencia a un conjunto común de valores en múltiples clases sin la necesidad de prefijos o duplicación de definiciones.
- Implementación de polimorfismos adicionales: Las interfaces también se pueden utilizar para implementar polimorfismos que no encajan en la estructura de herencia existente. Mientras que una clase en Java solo puede heredar de una superclase, puede implementar múltiples interfaces, lo que permite la creación de múltiples jerarquías de herencia y la implementación de múltiples formas de polimorfismo.
- Ejemplo de uso: En el contexto de un sistema de nómina, se muestra cómo las interfaces pueden ser utilizadas para definir comportamientos comunes entre diferentes tipos de empleados, como los empleados asalariados, los empleados por comisión y los empleados asalariados y por comisión. Cada tipo de empleado implementa una interfaz que define un conjunto común de métodos, lo que facilita su tratamiento uniforme en el programa de nómina.
El modificador protected
El modificador de acceso protected
en Java ofrece una accesibilidad intermedia entre public
y private
. Algunos de los usos de protected
son:
- Acceso en la jerarquía de herencia: Los miembros marcados como
protected
pueden ser accedidos desde dentro de la misma clase, desde las clases derivadas (subclases) y también desde otras clases dentro del mismo paquete. Esto es útil cuando se quiere que un miembro sea accesible para las clases derivadas pero no para el público en general. - Extensibilidad: Al marcar un miembro como
protected
, se permite que las clases hijas (subclases) accedan a ese miembro, lo que facilita la extensión y la especialización de la funcionalidad en las clases derivadas. - Reutilización de código: Al permitir que las clases hijas accedan a ciertos miembros, se fomenta la reutilización de código al permitir que estas clases hereden y modifiquen el comportamiento de los miembros
protected
de la clase padre. - Encapsulación controlada: Aunque los miembros
protected
son accesibles desde las clases derivadas, siguen estando encapsulados en el sentido de que su acceso está controlado y restringido a un ámbito específico, lo que ayuda a mantener la integridad de los datos y la coherencia del sistema.
El modificador protected
es útil cuando se quiere proporcionar acceso a ciertos miembros dentro de una jerarquía de herencia de clases, permitiendo la extensibilidad y la reutilización de código mientras se mantiene un cierto grado de encapsulación y control sobre los datos y la funcionalidad.
Tres dimensiones con Java
- Clase*
Employee3
ySalaried3
*:** Se muestra un ejemplo de herencia y polimorfismo en el contexto de las clasesEmployee3
ySalaried3
. La clase abstractaEmployee3
define un método abstractogetPay()
, mientras que la claseSalaried3
extiendeEmployee3
e implementa el métodogetPay()
para calcular el salario de un empleado asalariado, incluyendo la deducción del impuesto. - API de Java para gráficos 3D: Se menciona que el API de Java proporciona clases para dibujar y colorear formas bidimensionales, así como métodos para representar formas tridimensionales sencillas, como un cilindro. Se menciona específicamente los métodos
draw3DRect
yfill3DRect
de la claseGraphics2D
para representar un rectángulo sombreado que parece estar en un plano tridimensional. - Representación de un cilindro sólido: Se describe cómo se puede representar un cilindro sólido en Java considerando cálculos de geometría y trigonometría. Se muestra un ejemplo de código que define una clase
Cylinder
que extiendeJPanel
para dibujar un cilindro tridimensional y se explica cómo se calculan los ángulos de elevación y azimut para representar el cilindro desde diferentes perspectivas. - Uso de interfaces en el API de Java: Se menciona que algunas clases y métodos del API de Java utilizan interfaces como parámetros, lo que permite una mayor flexibilidad y polimorfismo en el código. Por ejemplo, se menciona que los parámetros de entrada para métodos como
append
yfill
son del tipoShape
, una interfaz que puede ser implementada por diferentes tipos de formas.
En resumen
- La clase
Object
es la clase ancestro de todas las demás clases en Java. - Para personalizar el comportamiento de los métodos
equals
ytoString
, es necesario sobrescribirlos en las clases hijas. - En tiempo de compilación, se confirma que una clase de variable de referencia puede manejar las llamadas a métodos de la variable.
- El operador
instanceof
permite verificar si un objeto es una instancia de una clase o de alguna de sus subclases. - Es posible asignar un objeto a una variable de referencia más genérica, pero no al revés sin un tipo de conversión.
- El polimorfismo puede implementarse con arreglos de objetos al declararlos con un ancestro común y sobrescribir los métodos necesarios.
- Una clase puede extender una superclase y/o implementar múltiples interfaces.
- El modificador de acceso
protected
permite el acceso desde clases en el mismo paquete o en subclases. - Las interfaces permiten proporcionar acceso a un conjunto común de constantes y pueden ser implementadas por múltiples clases.
- La sintaxis de un método abstracto incluye la palabra clave
abstract
antes del tipo de retorno y no tiene cuerpo. - Las clases que contienen métodos abstractos deben ser declaradas como clases abstractas.
- No es posible instanciar una clase abstracta.
- El modificador de acceso
protected
permite el acceso desde clases en el mismo paquete o en subclases.