Tema 1 - Clases y objetos
Programción orientada a objetos
La programación orientada a objetos (POO) es un paradigma de programación basado en la creación y manipulación de objetos, que son instancias de clases. Su objetivo es modelar problemas del mundo real mediante entidades que combinan datos (atributos) y comportamientos (métodos).
La POO permite organizar el código de manera modular y reutilizable, favoreciendo el mantenimiento y la escalabilidad de los programas. Se basa en cuatro principios fundamentales: abstracción, encapsulamiento, herencia y polimorfismo, que facilitan la estructuración del software en componentes independientes pero interconectados.
Este paradigma es ampliamente utilizado en lenguajes como Java, Kotlin, C#, C++ y Python, entre otros, y es fundamental en el desarrollo de software moderno.
Los pilares de la POO
La POO, como ya hemos dicho antes, se basa en cuatro características básicas, las cuales son abstracción, encapsulamiento, herencia y polimorfismo:
-
Abstracción: Consiste en representar los elementos esenciales de un objeto y ocultar los detalles innecesarios. Permite modelar conceptos del mundo real en clases y objetos sin preocuparse por la implementación interna.
-
Encapsulamiento: Es el principio de ocultar los datos internos de un objeto y exponer solo lo necesario a través de métodos públicos. Se logra con modificadores de acceso (privado, protegido, público) y evita la manipulación indebida del estado del objeto. Esto ofrece seguridad y facilita el mantenimiento del código.
-
Herencia: Permite que una clase (subclase) herede atributos y métodos de otra (superclase). Facilita la reutilización de código y la creación de jerarquías de clases, fomentando la modularización y la extensibilidad.
-
Polimorfismo: Permite que un mismo método o función pueda tener diferentes comportamientos según el contexto. Puede manifestarse mediante sobrecarga (métodos con el mismo nombre pero diferentes parámetros) o sobrescritura (una subclase redefine un método de la superclase).
Transición hacia la POO
Antes de que la programación orientada a objetos se popularizara, se utilizaban otros paradigmas como la programación estructurada y la programación modular. Estos enfoques siguen siendo válidos y se combinan con la POO en muchos proyectos de software.
Programación estructurada
Es un paradigma anterior a la programación orientada a objetos que se basa en la división del código en bloques lógicos y secuencias de control bien definidas, evitando el uso excesivo de saltos incontrolados (goto).
Se apoya en tres estructuras fundamentales: secuencia (ejecución ordenada de instrucciones), selección (condicionales como if o switch) y repetición (bucles como for o while). Su objetivo es mejorar la legibilidad, mantenibilidad y depuración del código.
Programación modular
Es un enfoque que busca dividir un programa en módulos independientes y reutilizables, donde cada módulo representa una parte específica de la funcionalidad del software. Estos módulos pueden ser funciones, procedimientos o archivos independientes, permitiendo la organización del código de manera más clara y escalable.
La programación orientada a objetos puede considerarse una evolución de este paradigma, ya que las clases y objetos funcionan como módulos que encapsulan datos y comportamiento. (ya nos encontraremos con Maven o Gradle, no os preocupéis JAJAJA)
Clase
Una clase es una plantilla o un modelo que define la estructura y el comportamiento de un conjunto de objetos. Es como un plano arquitectónico que especifica qué atributos (datos/variables) y qué métodos (acciones/funciones) tendrá cada objeto basado en esa clase.
Por ejemplo, en Java:
class Coche { String marca; int velocidad;
void acelerar() { velocidad += 10; }
void frenar() { velocidad -= 10; }}Aquí, la clase Coche define que todos los coches tendrán una marca y una velocidad, además de un método para acelerar y otro frenar.
El punto de entrada en un programa en Java es el método main, cuya firma obligatoria es:
public static void main(String[] args) { // Código del programa}La JVM (Java Virtual Machine) busca este método para iniciar la ejecución del programa. Debe ser public para que sea accesible desde fuera de la clase, static para que pueda ejecutarse sin necesidad de instanciar la clase y void porque no devuelve un valor.
Método
Un método es una función definida dentro de una clase que representa el comportamiento de un objeto. Permite que los objetos realicen acciones y manipulen sus atributos.
Por ejemplo, en Java:
class Coche { int velocidad = 0;
void acelerar() { velocidad += 10; }}Aquí, acelerar() es un método que incrementa la velocidad del coche.
Métodos estáticos
Un método estático es un método que pertenece a la clase, no a sus instancias. Se declara con la palabra clave static y puede ser llamado sin necesidad de crear un objeto de la clase.
Características de los métodos estáticos:
- Se pueden llamar usando el nombre de la clase:
Clase.metodoEstatico(). - No pueden acceder a atributos o métodos no estáticos (de instancia) directamente.
- Son útiles para funciones auxiliares o de utilidad, como cálculos matemáticos o conversión de datos.
Ejemplo en Java:
class Utilidad { static void mensaje() { System.out.println("Este es un método estático"); }}
public class Main { public static void main(String[] args) { Utilidad.mensaje(); // Llamada sin instanciar un objeto }}Salida:
Este es un método estáticoUn ejemplo típico es el método Math.sqrt(), que pertenece a la clase Math y no requiere instancias.
Sobrecarga de métodos
La sobrecarga de métodos (method overloading) es una técnica que permite definir varios métodos con el mismo nombre dentro de una clase, pero con diferentes parámetros (cantidad o tipo). Esto mejora la legibilidad y flexibilidad del código.
Ejemplo en Java:
class Coche { void acelerar() { System.out.println("El coche acelera."); }
void acelerar(int cantidad) { System.out.println("El coche acelera " + cantidad + " km/h."); }}Aquí, el método acelerar() tiene dos versiones: una sin parámetros y otra que recibe un entero. El compilador elige cuál ejecutar según los argumentos proporcionados a la hora de llamar a la función.
El método toString()
El método toString() es un método especial que se utiliza para representar un objeto como una cadena de texto. Por defecto, devuelve una representación basada en la dirección de memoria del objeto, pero puede ser sobrescrito para proporcionar una representación más significativa.
En Java, toString() se usa comúnmente para imprimir objetos de manera legible en lugar de mostrar sus direcciones de memoria. Otros lenguajes orientados a objetos, como Kotlin, ya incluyen una implementación predeterminada de este método en ciertas clases especiales, como las data classes.
En versiones más recientes de Java, existen los record, que son clases inmutables con implementaciones automáticas de los métodos toString(), equals() y hashCode(). Sin embargo, si se necesitan setters, estos deben definirse manualmente.
Objeto
Un objeto es una instancia de una clase, es decir, una entidad concreta creada a partir de la plantilla que define la clase.
Podríamos definirlo por tres elementos fundamentales:
-
Estado: Representa los datos o atributos del objeto. Define sus características y puede cambiar a lo largo del tiempo.
-
Comportamiento: Son las acciones o métodos que el objeto puede realizar. Definen cómo interactúa con otros objetos y cómo modifica su estado.
-
Identidad: Es lo que distingue a un objeto de otro, incluso si tienen el mismo estado y comportamiento. Cada objeto ocupa una ubicación única en memoria, por lo tanto, son completamente distintos e independientes.
Un objeto se crea a partir de una clase mediante el operador new y puede acceder a sus atributos y métodos.
Por ejemplo:
Coche miCoche = new Coche();miCoche.marca = "Toyota";miCoche.acelerar();Aquí, miCoche es un objeto de la clase Coche con su propio estado y comportamiento.
En resumen, la clase es la definición, mientras que el objeto es la instancia concreta que existe en memoria y puede interactuar con otros objetos.
Almacenamiento en memoria
Los objetos generalmente se almacenan en el heap, un área de memoria dinámica que permite la creación y destrucción de instancias en tiempo de ejecución. Sin embargo, en algunos lenguajes o contextos, pueden existir optimizaciones como la asignación en el stack si el objeto tiene un tiempo de vida corto y es determinista.
En Java, por ejemplo, todos los objetos se almacenan en el heap, mientras que las variables locales y la lista de referencias se almacenan en el stack.
No todos los lenguajes manejan la memoria de la misma manera. En lenguajes como Java y Python, la memoria es gestionada automáticamente mediante un sistema de recolección de basura (garbage collection), que libera los objetos que ya no están en uso (tanto de forma directa como indirecta).
En cambio, en lenguajes como C++, la gestión de memoria es manual y requiere que el programador libere los recursos explícitamente con delete
para evitar fugas de memoria (memory leaks).
En Java, todos los parámetros se pasan por valor, lo que significa que cuando se pasa un objeto a un método, se copia la referencia al objeto, pero no el objeto en sí.
Por lo tanto:
- Los cambios en los atributos del objeto dentro del método afectan al objeto original, porque la referencia copiada sigue apuntando al mismo objeto en memoria.
- Si se cambia la referencia dentro del método (asignando un nuevo objeto), el objeto original no se ve afectado, ya que solo se modifica la copia de la referencia.
Caso 1: Modificación de atributos (afecta al objeto original)
class Punto { int x;
Punto(int x) { this.x = x; }}
public class Main { static void modificarPunto(Punto p) { p.x = 100; // Se modifica el atributo del mismo objeto }
public static void main(String[] args) { Punto punto = new Punto(5); modificarPunto(punto); System.out.println(punto.x); // Imprime 100 }}Aquí, p es una copia de la referencia, pero sigue apuntando al mismo objeto en memoria, por lo que punto.x cambia.
Caso 2: Reasignación de la referencia (no afecta al objeto original)
static void cambiarReferencia(Punto p) { p = new Punto(200); // Se cambia la referencia, pero solo en la copia local}
public static void main(String[] args) { Punto punto = new Punto(5); cambiarReferencia(punto); System.out.println(punto.x); // Imprime 5}Aquí, dentro de cambiarReferencia(), la variable p apunta a un nuevo objeto, pero eso no afecta a punto en main(), ya que solo se modificó la copia de la referencia.
En conclusión:
- Java no pasa objetos por referencia, sino que pasa una copia de la referencia por valor.
- Modificar los atributos del objeto dentro del método afecta al original, porque la referencia copiada sigue apuntando al mismo objeto.
- Asignar un nuevo objeto dentro del método no afecta al original, porque solo se cambia la copia de la referencia, no la original.
Modificadores de acceso e instanciación
¿Qué es static y para qué sirve?
La palabra clave static indica que un atributo o método pertenece a la clase y no a sus instancias. Esto significa que se puede acceder a él sin necesidad de crear un objeto.
Por ejemplo:
class Util { static void mensaje() { System.out.println("Método estático"); }}
Util.mensaje(); // Se llama sin instanciar un objeto, es decir, no se usa `new`¿Sólo se usa en main?
No, static se puede aplicar a:
- Métodos estáticos, como los de utilidad en
Math:Math.sqrt(4). - Atributos estáticos, que son compartidos por todas las instancias de una clase.
- Bloques
static, ejecutados una sola vez cuando la clase se carga en memoria.
static combinado con final
Cuando se combina static final, se crea una constante de clase, cuyo valor no puede cambiar y es compartido por todas las instancias.
Ejemplo:
class Config { static final double PI = 3.1416;}Aquí, PI es una constante accesible sin instanciar Config y su valor es inmutable.