Saltearse al contenido

Tema 3: Propiedades básicas de la Orientación a Objetos

Aprende sobre conceptos clave como abstracción, encapsulamiento, modularidad, jerarquía, polimorfismo, tipificación y ligadura dinámica.

1. Modularidad

Propiedad que tiene un sistema que ha sido descompuesto en un conjunto de partes o módulos que sean cohesivos (guardan cierta relación lógica) y débilmente acoplados (minimizan sus dependencias).

Modularidad en Java

Durante este bloque utilizaremos este proyecto de ejemplo creado en el IDE InteliJ. Poco a poco iremos desglosando como funciona Java y la programación Orientada a Objetos.

  • Directory.idea/ directorio del IDE (podemos ignorarlo)
  • Directoryout/ los archivos de compilación (resultado de compilar tu código)
  • Directorysrc/ nuestro código fuente
    • Directorygraficos/
      • Circulo.java
    • Directoryotropaquete/
      • Dibujante.java
  • Directorytest/ nuestros tests (no se verá en este tema)
  • .gitignore
  • Un paquete es a un directorio
    • Es una agrupación lógica de clases.
    • Las clases públicas son solo visibles dentro del paquete.
  • Un fichero a una clase
    • Dentro de un fichero puede haber más de una clase, pero solo una clase pública y esa fichero tiene que llamarse igual que la clase. public class Circulo en Circulo.java.
Circulo.java
package graficos;
public class Circulo {
private int radio;
public int getRadio() { return radio; }
public void setRadio(int radio) {
this.radio = radio;
}
//...
}

Para importar el paquete y poder usar las clases de otro paquete empleamos import graficos.*;.

Dibujante.java
package otropaquete;
import graficos.Circulo;
public class Dibujante {
public static void main(String[] args) {
// Quiero dibujar un círculo del paquete gráficos
Circulo circulo = new Circulo();
}
}

2. Jerarquía

Jerarquía en Java

Esta es la estructura del proyecto que usaremos durante este bloque

  • Directory.idea/
  • Directoryout/
  • Directorysrc/
    • Direccion.java
    • Estudiante.java
    • Persona.java
    • Profesor.java
  • .gitignore

Composición

Cada clase debe ser responsable de la información que contiene. En una clase persona, cada persona tiene almacenada una dirección. Guardada como un objeto Direccion que contiene el método toString() para devolver la información almacenada como un String.

Persona.java
public class Persona {
String nombre;
String apellidos;
String telefono;
String email;
int edad;
Direccion direccion;
public String nombreCompleto() {
return nombre + " " + apellidos;
}
public String toString() {
return nombre + " " + apellidos +
", " + "tlf: " + telefono +
", " + "email: " + email +
", " + edad + " años, " +
direccion.toString();
}
}
Direccion.java
public class Direccion {
String calle;
int numero;
String piso;
String ciudad;
int codigoPostal;
String pais;
public String toString() {
return calle + " " + numero +
" " + piso + ", " +
codigoPostal + " " +
ciudad + ", " + pais;
}
}

Herencia

Herencia en Java

En Java una subclase hereda los métodos y atributos de su clase padre. Por ejemplo tanto un Estudiante como un Profesor son Personas por lo tanto heredan sus características y añaden sus propias.

Estudiante.java
public class Estudiante extends Persona {
Titulacion titulacion;
Asignatura[] asignaturas;
public int calcularMatricula() { /* ... */ }
}
Profesor.java
public class Profesor extends Persona {
String departamento;
String categoria;
String decicacion;
java.util.Date antiguedad;
public int calcularSueldo() { /* ... */ }
}

Podríamos representarlo como:

Cargando diagrama...
Código diagrama
flowchart TB
subgraph Estudiante
subgraph P1[Persona]
subgraph O1[Object]
end
end
end
subgraph Profesor
subgraph P2[Persona]
subgraph O2[Object]
end
end
end

Clases abstractas

El objetivo de las clases abstractas es agrupar características comunes a varias clases, pero es una clases de la que no se pueden crear instancias.

Todos los animales tiene características comunes, pero no hay ningún animal que sea simplemente animal.

Ejemplo de clases abstractas
public abstract class Animal {
private String nombre;
private String codigo;
private Date fechaNacimiento;
public String getNombre() {
return nombre;
}
}
public class Gato extends Animal {
public enum variedadGato {SIAMES, ESFINGE};
}
public class Perro extends Animal {
public enum variedadPerro {PASTOR_ALEMAN, LABRADOR};
}

También podemos usar los métodos abstractos para mantener la consistencia a lo largo de un programa.

Ejemplo de métodos abstractos
public abstract class Figura {
public abstract double area();
public abstract double perimetro();
}
public class Circulo extends Figura {
private double radio;
public double area() {
return Math.PI*radio*radio;
}
public double perimetro() {
return 2*Math.PI*radio;
}
}
class Rectangulo extends Figura {
private double base;
private double altura;
public double area() {
return base*altura;
}
public double perimetro() {
return (base*2)+(altura*2);
}
}

Interfaces

Las interfaces són similares a las clases abstractas. Se declaran con la palabra reservada interface y contienen solo las cabeceras de los métodos.

Por defecto en las interfaces:

  • Los métodos son públicos y abstractos.
  • Los atributos públicos, estáticos y finales. Es decir, constantes de clase.

Rescatando el ejemplo anterior de las figuras, estas dos implementaciones són equivalentes:

Figura.java
public abstract class Figura {
public abstract double area();
public abstract double perimetro();
}
public interface Figura {
double area();
double perimetro();
}

Una utilidad de las interfaces era la herencia múltiple:

Herencia múltiple
interface Monstruo {
void amenaza();
}
interface MonstruoPeligroso extends Monstruo {
void destruye();
}
interface Letal {
void mata();
}
interface Vampiro extends MonstruoPeligroso, Letal {
void bebeSangre();
}
class Virus implements Monstruo, Letal {
public void amenaza() {
// Contagio del virus
}
public void mata() {
// Enfermedad del virus
}
}
Cargando diagrama...
Código diagrama
flowchart BT
Vampiro --> Letal & MonstruoP[Monstruo Peligroso]
MonstruoP --> Monstruo
Virus -.-> Letal & Monstruo

Herencia vs. composición

Herencia vs. composición

Composición ganó la batalla pero como en Java hay que mantener la retrocompatibilidad simepre, pues nadie tocó nada y se siguen pudiendo usar las dos.

Por ejemplo, en la API de Java, Stack fué definido como un Vector. Pero ArrayList es la implementación moderna de Vector. Ahora ya no se puede cambiar porqué romperíamos la retrocompatibilidad de Java.

Por lo tanto la composición es la clara vencedora, ya que una implementación de métodos abstractos es mucho mejor que heredar forzadamente todos los métodos de la clase a heredar.

3. Polimorfismo

Capacidad de un objeto de pertenecer a más de una clase y de una función de ser aplicada sobre parámetros de distintas clases.

Polimorfismo como un molde de helados

Coacción o coerción

Coacción en Java

Operación semántica por la cual se convierte un argumento al tipo esperado por una función para evitar que se produzca un error de tipos.

Ejemplo de coacción
public class Coaccion {
public static double multiplicaPorDos(double i) {
return i*2;
}
public static void main(String[] args) {
System.out.println(Coaccion.multiplicaPorDos(5.5)); // Imprime 11.0
System.out.println(Coaccion.multiplicaPorDos(5.0)); // Imprime 10.0
System.out.println(Coaccion.multiplicaPorDos(5)); // Imprime 10.0
}
}

Sobrecarga y sobrescritura

Metáfora de la Sobrecarga

Capacidad de un objeto de pertenecer a más de una clase y de una función de ser aplicada sobre parámetros de distintas clases.

Sobrecarga de Constructores
public class Caja {
private int lado; // Lado de la caja
private int valor; // Valor interno
// Constructores
public Caja() { this(0, 10); }
public Caja(int valor) { this(valor, 10); }
public Caja(Caja c) { this(c.valor, c.lado); }
public Caja(int valor, int lado) {
this.valor = valor;
this.lado = lado;
}
}
Sobrecarga de Métodos
public class Sobrecarga {
public void metodoX (String s) {
System.out.println("Cadena " + s );
}
public void metodoX (int i) {
System.out.println("Entero " + i);
}
public void metodoX (int i, int j) {
System.out.println("Enteros " + i + " y " + j);
}
public void metodoX (int i, String c) {
System.out.println("Entero " + i + " y cadena " + c);
}
}

Debemos diferenciar la sobrecarga de la sobrescritura:

Sobreescritura del método display
class Animal {
public void display() {
System.out.println("I am an animal");
}
}
class Dog extends Animal {
public void display() {
System.out.println("I am a dog");
}
}
class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.display(); // I am a dog
}
}

Polimorfismo de inclusión

Polimorfismo que ocurre a través de las relaciones de herencia y mediante el cual una instancia de una subclase es también un instancia de sus superclases. Es el polimorfismo típico de la POO.

Ejemplo del polimorfismo de inclusión
public static void metodo(Animal a) { ... }
// ...
Animal a1 = new Perro(); // Podemos asignar un Perro a un Animal
Animal a2 = new Gato(); // Podemos asignar un Gato a un Animal
Perro p1 = new Perro();
// ...
metodo(p1); // Podemos pasar por parámetro un Perro si se pide un Animal
// ...
Perro p2 = new Animal(); // Error de compilación un Animal no es un Perro

Utilidades del polimorfismo de inclusión:

  • Métodos de inclusión: En Java y otros lenguajes Orientados a Objetos el polimorfismo de inclusión es utilizado para creación de métodos genéricos.
    • En Java cualquier objeto es un “hijo” de la superclase Object, con lo cual hereda sus métodos.
  • Colecciones genéricas: Podemos crear colecciones genéricas para cualquier tipo de objeto.
    • Como ArrayList, que permitan crear listas de cualquier tipo.
  • Clase envoltorio: Cuando se necesite que un tipo primitivo (Ej: int) se comporte como un objeto se utilizan clases envoltorio para guardar los valores.
    • Esto se hace de manera automática en un proceso llamado autoboxing y autounboxing.

Polimorfismo Parámetro (Genericidad)

Consiste en que una clase tiene uno, o varios, parámetros genéricos definidos. En la instanciación de dicha clase se especificará qué valores concretos tienen dichos parámetros genéricos.

CajaGenerica.java
public class CajaGenerica<T> {
private T valor;
public void setValor (T valor) {
this.valor = valor;
}
public T getValor () {
return valor;
}
public CajaGenerica(T valor) {
this.valor = valor;
}
}

Esto nos permite tener una instancia de un tipo específico de una clase genérica. Si tenemos una lista de perros, será SOLO de perros.

public static void main(String[] args) {
Perro p1 = new Perro("Snoopy");
CajaGenerica<Perro> c1 = new CajaGenerica<Perro>(p1);
}

Hasta que no es declarado de T lo único que sabemos de el es que es un Object. La forma de limitar los objetos aceptados a una clase es con extends.

public class CajaNumerica<T extends Number> { /* ... */ }

Esto no siempre será lo optimo. Ya que habrá métodos en los que queramos que un T sea diferente a otro T. Para esto usamos los comodines: ?.

Si, además, queremos limitarlo a una clase concreta pero que puedan ser clases diferentes entre si:

CajaNumerica.java
public class CajaNumerica<T extends Number> {
4 collapsed lines
private T valor;
public void setValor(T valor) { this.valor = valor; }
public T getValor() { return valor; }
public CajaNumerica(T valor) { this.valor = valor; }
boolean absEqual(CajaNumerica<? extends Number> ob) { // Comodín
if(Math.abs(valor.doubleValue()) == Math.abs(ob.valor.doubleValue()))
return true;
else return false;
}
public static void main(String[] args) {
CajaNumerica<Integer> n1 = new CajaNumerica<>(5); // Caja de Enteros
CajaNumerica<Double> n2 = new CajaNumerica<>(5.0); // Caja de Dobles
n1.absEqual(n2); // ¡¡ FUNCIONA !!
}
}

4. Tipificación

Un tipo es una caracterización precisa de las propiedades estructurales y de comportamiento que comparten una serie de entidades. Java tiene un tipado estático y fuerte.

Tipado en Java

Ejemplo del tipado de Java
class Animal { /* El metodo "ladra" no est´a definido en Animal */ }
class Perro extends Animal {
public void ladra() { System.out.println("Guau"); }
public static void main(String[] args) {
Animal unAnimal = new Perro();
unAnimal.ladra();
Perro unPerro = new Perro();
unPerro.ladra();
}
}
Cargando diagrama...
Código diagrama
---
title: Representación gráfica
---
flowchart TB
subgraph Animal
subgraph Perro
subgraph Object
end
end
end
subgraph 1[Perro]
subgraph 2[Animal]
subgraph 3[Object]
end
end
end
Compilador[Compilador: ¿ladras?]
Compilador -. NO .-> Animal
Compilador -. SI .-> 1

Esto es conocido como el principio del tipado del pato (Duck Typing): La comprobación de tipos se encarga de comprobar que el objeto en cuestión tiene los aspectos deseados (camina, nada, grazna) y no de qué tipo de objeto se trata.

Java DuckTyping

5. Ligadura Dinámica

Proceso que se encarga de ligar o relacionar la llamada a un método (o mensaje) con el código que se ejecuta finalmente.

Ejemplo de la ligadura dinámica
abstract class Animal {
public void hazRuido() { System.out.println("No hago ruido"); }
}
class Perro extends Animal {
public void hazRuido() { ladra(); }
public void ladra() { System.out.println("Guau"); }
}
class Pez extends Animal { /* no redefine hazRuido() */ }
// ...
Animal a1 = new Perro();
Animal a2 = new Pez();
a1.hazRuido();
a2.hazRuido();
// ...
Salida por Pantalla
Guau
No hago ruido
Otro ejemplo de la ligadura dinámica
class Padre {
public static void metodoEstatico()
{ System.out.println("Padre");}
}
class Hijo extends Padre {
public static void metodoEstatico()
{ System.out.println("Hijo"); }
}
class Estaticos {
public static void main(String[] args) {
Padre p = new Hijo();
p.metodoEstatico();
}
}
Salida por Pantalla
.
Padre

Para evitar estos podemos usar:

  • Clases Finales: Las clases finales limitan la posible extensibilidad del código y deben por lo general evitarse salvo situaciones puntuales.

  • Métodos Finales: Los métodos finales también limitan algo esencial en la orientación a objetos como es la sobrescritura de métodos.

Pablo Portas López © 2025 licensed under CC BY 4.0