====== 03.1 - POO I: Diseño de Clases, Encapsulación y Organización ====== La **Programación Orientada a Objetos** (**POO**) es un paradigma donde se organiza el código en unidades denominadas **clases**, las cuales contienen **propiedades** (o **atributos**) y **métodos**. Al ser Java un lenguaje orientado a objetos, hemos utilizado este paradigma desde la primera línea de código: public class HolaMundo { static void main() { IO.println("Hola mundo"); } } Definimos una clase mediante la palabra reservada ''class''. La idea es **encapsular** (proteger y aislar) el código de nuestra aplicación en clases que se comuniquen entre sí mediante métodos. Las características más importantes de la POO son: * **Abstracción**: Identifica las características y comportamientos esenciales de una entidad del mundo real omitiendo los detalles irrelevantes. La herramienta principal para la abstracción es la clase. * **Encapsulación**: Mecanismo para restringir el acceso directo a los datos internos de un objeto. Protege la información frente a modificaciones indebidas. * **Modularidad**: Divide el programa en módulos o clases independientes almacenadas en archivos separados. Permite modificar un componente sin alterar el resto. * **Jerarquía**: Clasifica y relaciona clases mediante dos estructuras principales: * **Generalización / Especialización (Herencia - "Es un"):** Permite crear clases derivadas a partir de una clase base (ej. ''Coche'' es un ''Vehiculo''). * **Composición / Agregación ("Tiene un" / "Es parte de"):** Permite agrupar objetos dentro de otro (ej. un ''Coche'' tiene un ''Motor''). * **Polimorfismo**: Capacidad de objetos de distintas clases derivadas de responder de forma diferente ante una misma llamada a un método. ===== 1. Clases y Objetos ===== Una **clase** es la plantilla o prototipo abstracto que define las propiedades y métodos comunes. Un **objeto** es una instancia concreta creada a partir de esa plantilla. Las propiedades fundamentales de un objeto son: * **Identidad**: Elemento que distingue de forma única a un objeto de cualquier otro (ej. su dirección de memoria o una clave primaria), aunque compartan los mismos datos. * **Estado**: Conjunto de valores almacenados en sus atributos en un momento determinado. * **Comportamiento**: Conjunto de acciones (métodos) que el objeto puede ejecutar o recibir. ==== 1.1 Definición e instanciación de clases ==== Todo código ejecutable en Java debe estructurarse dentro de una clase. === Creación de una clase === Para definir una entidad (por ejemplo, un vehículo), creamos un archivo ''.java'' con la estructura base: public class Coche { String marca; String modelo; String color; int numeroBastidor; int velocidad = 0; } === Acceso a propiedades y necesidad de instanciación === Intentar acceder directamente a los atributos de una clase sin crear una instancia genera un error de compilación: public class Main { static void main() { // ERROR: Marca no es accesible de forma estática // IO.println(Coche.marca); } } Para utilizar una clase debemos declarar la variable e **instanciar el objeto** con el operador ''new'': public class Main { static void main() { // Instanciación del objeto Coche coche1 = new Coche(); // Asignación y lectura de atributos coche1.marca = "Kia"; IO.println("La marca de mi coche es " + coche1.marca); } } ==== 1.2 Métodos de clase ==== Los **métodos** son las funciones definidas dentro de una clase que operan sobre sus atributos o realizan acciones específicas. public class Coche { String marca; String modelo; String color; int numeroBastidor; int velocidad = 0; public void acelerar(int aumento) { velocidad += aumento; } } Invocación del método sobre el objeto instanciado desde la clase principal: public class Main { static void main() { Coche coche1 = new Coche(); IO.println("Velocidad inicial: " + coche1.velocidad); coche1.acelerar(20); IO.println("Velocidad tras acelerar: " + coche1.velocidad); } } En Java **todo se pasa por valor**. Sin embargo, el comportamiento percibido varía radicalmente dependiendo de si pasamos un tipo de dato primitivo o un objeto:\\ \\ * **Tipos primitivos (''int'', ''double'', ''boolean'', etc.):** Se pasa una **copia del valor**. Cualquier modificación dentro del método afecta únicamente a la variable local del método y **no modifica** la variable original. * **Objetos (Tipos por referencia):** Lo que se pasa por valor es **la copia de la referencia** (la dirección de memoria) que apunta al objeto. Por tanto, tanto la variable original como el parámetro del método apuntan al **mismo objeto en memoria**. **Consecuencia clave:** Si un método modifica el estado interno de un objeto recibido como parámetro (por ejemplo, llamando a sus métodos **setter** o modificando sus atributos), **los cambios se reflejarán en el objeto original fuera del método**. ===== 2. Visibilidad ===== Hasta ahora, la mayoría de nuestros métodos, propiedades y clases tenían asociada la palabra reservada ''public'' en su declaración. Esto hace referencia a los **modificadores de acceso** (o visibilidad) de los elementos. En la POO existen habitualmente 3 tipos de visibilidad: * ''public'': El elemento es accesible desde cualquier clase. * ''private'': El elemento es accesible solo desde la propia clase. * ''protected'': El elemento es accesible desde la propia clase y sus clases derivadas (herencia). En Java, existe un cuarto tipo de visibilidad, además de un matiz específico para ''protected'': * ''protected'' (en Java): Accesible desde la propia clase, sus clases derivadas y también desde cualquier otra clase definida en el mismo paquete (''package''). * ''package-private'' (por defecto): Si no se especifica ningún modificador de acceso, el elemento es accesible únicamente por las clases definidas en el mismo paquete. Por ejemplo, modificamos los atributos de la clase ''Coche'' haciéndolos privados: public class Coche { private String marca; private String modelo; private String color; private int numeroBastidor; private int velocidad = 0; public void acelerar(int aumento) { velocidad += aumento; } } Si intentamos acceder directamente a un atributo privado desde la clase ''Main'': public class Main { static void main() { Coche coche1 = new Coche(); // ERROR de compilación: el atributo 'marca' es privado coche1.marca = "Kia"; IO.println("La marca de mi coche es " + coche1.marca); } } El compilador mostrará el siguiente mensaje de error indicando la falta de visibilidad: The field Coche.marca is not visible Si cambiamos la visibilidad de los atributos a pública (''public''): public class Coche { public String marca; public String modelo; public String color; public int numeroBastidor; public int velocidad = 0; public void acelerar(int aumento) { velocidad += aumento; } } El acceso directo desde otra clase vuelve a estar permitido: public class Main { static void main() { Coche coche1 = new Coche(); coche1.marca = "Kia"; IO.println("La marca de mi coche es " + coche1.marca); } } La marca de mi coche es Kia ==== 2.1 Criterios de diseño para la visibilidad ==== Como regla general de diseño orientada a objetos: * **Atributos:** Deben ser **privados** (''private''). Esto protege el estado del objeto frente a modificaciones no válidas desde el exterior (por ejemplo, evitar asignaciones como ''coche1.velocidad = -300000;''). * **Métodos públicos:** Conforman la interfaz externa de la clase (las operaciones que el resto del programa puede solicitar al objeto). * **Métodos privados:** Funciones auxiliares internas que no deben exponerse fuera de la clase. ===== 3. Getters y Setters ===== Dado que los atributos deben ser privados, para consultar o modificar sus valores desde fuera de la clase empleamos métodos accesores: **getters** (métodos de lectura) y **setters** (métodos de escritura). Estos métodos permiten controlar el acceso a la información y aplicar validaciones antes de modificar el estado interno de un objeto. La estructura base de un método de lectura y de escritura es la siguiente: public String getMarca() { return marca; } public void setMarca(String marca) { this.marca = marca; } Por convención en Java, el acceso de lectura se nombra como ''getAtributo()'' y el de escritura como ''setAtributo()''. En atributos de tipo booleano se suele emplear ''isAtributo()'' para la lectura (ej. ''isElectrico()''). **¿Es necesario definir getters y setters para todos los atributos?** No. Únicamente debemos crear los métodos accesores que queramos exponer al exterior: * **Clases inmutables:** Si el estado de un objeto no debe cambiar una vez instanciado, se definen solo los //getters// y se omiten los //setters//. * **Atributos internos:** Si una propiedad solo sirve para la lógica interna de la clase, no debe llevar ni //getter// ni //setter//, manteniéndose completamente aislada del resto del programa. En la instrucción ''this.marca = marca;'', la palabra reservada **this** hace referencia a la propiedad perteneciente a la instancia actual del objeto. ===== 4. Constructores ===== Un **constructor** es un método especial que se invoca automáticamente al instanciar un objeto mediante el operador ''new''. Su función principal es inicializar los atributos de la instancia. Considera la clase ''Coche'' con atributos privados e incialización por defecto: public class Coche { private String marca; private String modelo; private String color; private int numeroBastidor; private int velocidad; public String getMarca() { return marca; } } Al instanciarla desde la clase principal: public class Main { static void main() { Coche coche1 = new Coche(); IO.println("La marca del coche es " + coche1.getMarca()); } } La marca del coche es null El valor **null** es un literal especial que indica la ausencia de valor o que una variable de tipo objeto no apunta a ninguna instancia en memoria. ==== 4.1 Definición de constructores ==== Para asegurar que un objeto nazca con un estado válido, definimos un constructor con los datos iniciales necesarios. Reglas de los constructores en Java: * Su nombre debe coincidir exactamente con el nombre de la clase. * No especifica ningún tipo de retorno (ni siquiera ''void''). * Se invoca automáticamente solo durante la instanciación con ''new''. public class Coche { private String marca; private String modelo; private String color; private int numeroBastidor; private int velocidad; public Coche(String marca) { IO.println("Creando un objeto de la clase Coche"); this.marca = marca; } public String getMarca() { return marca; } } Si el constructor exige argumentos, es obligatorio proporcionarlos al invocar ''new'': public class Main { static void main() { Coche coche1 = new Coche("Kia"); IO.println("La marca del coche es " + coche1.getMarca()); } } Creando un objeto de la clase Coche La marca del coche es Kia Si no declaras explícitamente ningún constructor en tu clase, Java crea automáticamente un **constructor por defecto** sin parámetros. Sin embargo, en cuanto defines manualmente al menos un constructor con parámetros, el constructor explícito por defecto deja de existir salvo que lo declares explícitamente mediante [[clase:daw:prog:1eval:funciones#sobrecarga_de_metodos|sobrecarga de métodos]]. ===== 5. La palabra reservada this ===== En la instrucción ''this.marca = marca;'': * **''marca''**: Hace referencia al parámetro local recibido en el constructor o método. * **''this.marca''**: Hace referencia al atributo de la instancia actual de la clase. El identificador ''this'' es una referencia implícita al objeto sobre el cual se está ejecutando el código. public class Coche { private String marca; // Resuelve la ambigüedad cuando el parámetro y el atributo comparten nombre public Coche(String marca) { this.marca = marca; } } También permite llamar a otros métodos dentro de la misma clase: this.acelerar(30); ===== 6. Métodos y atributos estáticos ===== Para ejecutar un método estándar de una clase es necesario instanciar previamente un objeto de la misma. Intentar invocar un método de instancia directamente sobre la clase provoca un error de compilación. Sin embargo, Java proporciona los **miembros estáticos** (definidos mediante la palabra reservada ''static''), los cuales pertenecen a la clase en sí y no a un objeto concreto, permitiendo su uso sin necesidad de instanciar la clase. El punto de entrada principal de una aplicación Java (''main()'') utiliza el modificador ''static'' precisamente por esta razón: para que la Máquina Virtual de Java (JVM) pueda ejecutarlo sin necesidad de instanciar previamente la clase principal. Si definimos un método de instancia dentro de la clase principal e intentamos invocarlo directamente desde un contexto estático como ''main()'', el compilador generará un error: public class Main { private int sumar(int numero1, int numero2) { return numero1 + numero2; } static void main() { int suma; // ERROR de compilación: no se puede usar 'this' o métodos de instancia desde un contexto estático suma = this.sumar(3, 5); IO.println("La suma es " + suma); } } El mismo problema sucede al intentar usar métodos de instancia de una clase externa sin instanciarla con el operador ''new'': public class Calculadora { public int sumar(int numero1, int numero2) { return numero1 + numero2; } public int restar(int numero1, int numero2) { return numero1 - numero2; } public int multiplicar(int numero1, int numero2) { return numero1 * numero2; } public int dividir(int numero1, int numero2) { return numero1 / numero2; } } public class Main { static void main() { Calculadora calc; // ERROR: La variable local 'calc' no ha sido inicializada con 'new' IO.println("3 + 5 = " + calc.sumar(3, 5)); } } ==== 6.1 Definición de métodos estáticos ==== Para poder invocar los métodos directamente a través del nombre de la clase, añadimos el modificador ''static'' en sus declaraciones: public class Calculadora { public static int sumar(int numero1, int numero2) { return numero1 + numero2; } public static int restar(int numero1, int numero2) { return numero1 - numero2; } public static int multiplicar(int numero1, int numero2) { return numero1 * numero2; } public static int dividir(int numero1, int numero2) { return numero1 / numero2; } } ==== 6.2 Invocación de métodos estáticos ==== Desde la clase principal o cualquier otro punto del programa, podemos ejecutar los métodos estáticos utilizando la sintaxis ''NombreClase.nombreMetodo()'': public class Main { static void main() { IO.println("3 + 5 = " + Calculadora.sumar(3, 5)); } } Aunque es técnicamente posible invocar un método estático a través de una instancia concreta, es una mala práctica y se desaconseja: Calculadora calc = new Calculadora(); IO.println("2 + 4 = " + calc.sumar(2, 4)); // Desaconsejado: debe usarse Calculadora.sumar(2, 4) ==== 6.3 Acceso a propiedades de clase desde contextos estáticos ==== Si intentamos almacenar el resultado de una operación en una propiedad de la clase desde un método estático: public class Calculadora { private int resultado; public static int sumar(int numero1, int numero2) { // ERROR: No se puede acceder a atributos de instancia desde un contexto estático this.resultado = numero1 + numero2; return this.resultado; } } Un método estático no está asociado a ninguna instancia en particular, por lo que no existe la referencia ''this'' ni se puede acceder directamente a variables de instancia. Para solucionar esto, la propiedad también debe ser declarada como estática (''private static''): public class Calculadora { private static int resultado; public static int sumar(int numero1, int numero2) { resultado = numero1 + numero2; return resultado; } public static int restar(int numero1, int numero2) { resultado = numero1 - numero2; return resultado; } public static int multiplicar(int numero1, int numero2) { resultado = numero1 * numero2; return resultado; } public static int dividir(int numero1, int numero2) { resultado = numero1 / numero2; return resultado; } } public class Main { static void main() { IO.println("3 + 5 = " + Calculadora.sumar(3, 5)); } } En un contexto estático **no se puede utilizar la referencia ''this''**. Si existe ambigüedad de nombres entre un parámetro y una propiedad estática, se puede calificar la propiedad indicando el nombre de la clase: ''Calculadora.resultado''. ==== 6.4 Comportamiento en memoria de las propiedades estáticas ==== Las propiedades estáticas son **variables de clase**, lo que significa que existe una única copia almacenada en memoria compartida por todas las instancias de dicha clase. Observa la diferencia entre un atributo de instancia y uno estático: === Atributo de instancia === public class Contador { private int cont = 0; public Contador() { cont++; IO.println(cont); } } public class Main { static void main() { Contador cont1 = new Contador(); Contador cont2 = new Contador(); Contador cont3 = new Contador(); } } 1 1 1 Cada objeto instanciado crea su propio espacio reservado para la variable ''cont''. === Atributo estático === public class Contador { private static int cont = 0; public Contador() { cont++; IO.println(cont); } } 1 2 3 Al ser estática, la variable ''cont'' pertenece a la clase y es compartida por todos los objetos creados. **¿Cuándo utilizar miembros estáticos?** * **Clases de utilidad / Helpers:** Agrupación de funciones matemáticas o de conversión sin estado (ej. la clase estándar ''java.lang.Math''). * **Constantes de aplicación:** Valores globales compartidos (combinado con ''final''). * **Patrones de diseño:** Implementaciones como **Singleton** o factorías (**Factory**). ===== 7. Enumerados ===== Los **enumerados** (''enum'') fueron introducidos en Java 5 para definir un conjunto fijo de constantes con tipo seguro. A nivel interno, los enumerados son clases especiales que extienden implícitamente de la clase abstracta ''java.lang.Enum''. Debido a su naturaleza: * No pueden extender de ninguna otra clase (no admiten herencia múltiple de clases). * Sus constructores son siempre **privados** o con visibilidad de paquete. * Garantizan que solo existirán las instancias explícitamente declaradas. ==== 7.1 Creación de enumerados ==== Para declarar una enumeración utilizamos la palabra reservada ''enum''. Los valores se escriben habitualmente en mayúsculas: public enum KiaModel { NIRO, XCEED, SPORTAGE } ==== 7.2 Uso de enumerados en clases ==== Utilizar un enumerado nos permite limitar las opciones de un atributo a los valores predefinidos en el ''enum'', evitando errores tipográficos propios de las cadenas de texto: public class Kia { private KiaModel model; public Kia(KiaModel model) { this.model = model; } public KiaModel getModel() { return model; } @Override public String toString() { return "Kia " + this.getModel(); } } Para instanciar objetos pasando valores del enumerado: public class App { static void main() { Kia niro = new Kia(KiaModel.NIRO); Kia sportage = new Kia(KiaModel.SPORTAGE); Kia xceed = new Kia(KiaModel.XCEED); IO.println(niro); IO.println(sportage); IO.println(xceed); } } ==== 7.3 Enumerados con atributos y constructores ==== Un ''enum'' en Java puede tener propiedades, métodos y constructores. Esto es útil para asociar información adicional a cada constante: public enum KiaModel { NIRO("1.6 GDi HEV 103kW"), XCEED("1.6 TGDi 112kW"), SPORTAGE("1.0 T-GDi 88kW"); private String engine; // El constructor de un enum SIEMPRE es privado private KiaModel(String engine) { this.engine = engine; } public String getEngine() { return this.engine; } } Podemos consultar los métodos del ''enum'' directamente desde el objeto contenedor: public class Kia { private KiaModel model; public Kia(KiaModel model) { this.model = model; } public KiaModel getModel() { return model; } @Override public String toString() { return "Kia " + this.getModel() + " - Motor: " + this.getModel().getEngine(); } } ===== Ejercicios ===== === Ejercicio 1a === Crea una clase llamada ''Product'' con los siguientes atributos públicos: * ''name'' (''String'') * ''price'' (''double'') * ''stockQuantity'' (''int'') Añade a la clase los siguientes métodos: * ''restock(int quantity)'': Incrementa el valor del atributo ''stockQuantity''. * ''sell(int quantity)'': Reduce el valor del atributo ''stockQuantity''. En la clase principal (''Main''), realiza las siguientes acciones: - Instancia un objeto de la clase **Product**.\\ \\ - Asigna valores a sus atributos de forma directa.\\ \\ - Muestra por pantalla sus datos iniciales.\\ \\ - Modifica su stock llamando al método ''restock()'' y muestra el stock actualizado.\\ \\ - Vende una cantidad válida llamando a ''sell()'' y muestra el stock resultante.\\ \\ - Simula una venta superior al stock disponible para observar cómo responde el método sin validación previa.\\ \\ === Ejercicio 1b === Modifica la clase ''Product'' para que sus métodos de control de inventario gestionen errores mediante valores de retorno: - Método ''restock(int quantity)'': Si la cantidad ingresada es negativa (''quantity < 0''), no debe modificar el stock y devolverá $-1$ como código de error. Si es una cantidad válida, incrementa el stock y devuelve el nuevo valor de ''stockQuantity''.\\ \\ - Método ''sell(int quantity)'': Comprueba si hay suficiente stock disponible. Si hay suficiente stock, reduce la cantidad, devuelve la cifra exacta de unidades vendidas (''quantity''). Si no hay suficiente stock, no modifica la propiedad y devuelve $-1$ para indicar que la operación ha fallado. - En la clase principal (''Main''), realiza operaciones de venta y añadir stock válidas e inválidas, evaluando el resultado devuelto por los métodos para mostrar los mensajes informativos correspondientes al usuario. === Ejercicio 1c === Cambia la visibilidad de los atributos de ''Product'' a ''private'' y crea los métodos necesarios para leer y escribir los atributos. === Ejercicio 1d === Crea un constructor de ''Product'' donde se inicialicen todos sus atributos. === Ejercicio 1e === Crea dos productos con el mismo valor en sus atributos y utiliza ''=='' para comprobar si son el mismo producto. === Ejercicio 1f === Crea el método ''boolean isSameProduct(Product other)'' en la clase ''Product'' que compruebe si sus atributos son iguales. Usa el método recién creado para comparar los productos del ejercicio anterior. === Ejercicio 2a === Evoluciona el diseño del sistema introduciendo categorías y simulando un repositorio de datos en memoria (base de datos estática) para consultar información mediante identificadores únicos (IDs): * ''Category'': * Atributos privados e inmutables (''final''): ''id'' (''long''), ''name'' (''String'') y ''description'' (''String''). * Constructor completo y métodos accesores (''getters''). * ''Product'': * Atributos privados e inmutables (''final''): ''id'' (''long''), ''name'' (''String''), ''price'' (''double''), ''stockQuantity'' (''int'') y ''category'' (de tipo **Category**). * Constructor completo y métodos accesores (''getters''). * ''CategoryDb'': * Define un array estático privado ''categories'' inicializado con 2 categorías ("Libros" y "Electrónica"). * Implementa el método estático ''getById(long id)'', que recorre el array y devuelve la categoría coincidente (o ''null'' si no existe). * ''ProductDb'': * Define un array estático privado ''products'' inicializado con 4 productos asociados a sus respectivas categorías mediante ''CategoryDb.getById()''. * Implementa el método estático ''getById(long id)''. * Implementa el método estático ''getByCategoryId(long categoryId)'', el cual debe devolver un nuevo array ''Product[]'' conteniendo exclusivamente los productos pertenecientes a esa categoría. En la clase principal (''Main''), prueba la recuperación individual de un producto y una categoría por su ID, e imprime el listado de productos devuelto al filtrar por la categoría de "Electrónica" (ID 2). === Ejercicio 2b === Añade las siguientes clases al ejercicio anterior: * ''OrderStatus'' (''enum''): * Define las constantes: ''PENDING'', ''PROCESSING'', ''SHIPPED'', ''DELIVERED'' y ''CANCELLED''. * Añade un atributo privado e inmutable ''description'' (''String'') que guarde una frase descriptiva en español (ej. "Pendiente de pago", "Enviado", etc.). * Define el constructor del enumerado y el método accesor ''getDescription()''. * ''Order'': * Atributos privados: ''id'' (''long''), ''product'' (''Product''), ''quantity'' (''int'') y ''status'' (de tipo **OrderStatus**). * Constructor que reciba ''id'', ''product'' y ''quantity'', e inicialice por defecto el estado ''status'' en ''OrderStatus.PENDING''. * Métodos accesores (''getters'') y un ''setter'' para modificar el atributo ''status''. * Método ''calculateTotal()'' que devuelva el precio total (multiplicando el precio del producto por la cantidad). En la clase principal (''Main''), crea un pedido, muestra su estado inicial utilizando el método ''getDescription()'' del enumerado, simula el avance del envío cambiando su estado y evalúa la situación del pedido mediante una estructura de control ''switch''.