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:
Coche es un Vehiculo).Coche tiene un Motor).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:
Todo código ejecutable en Java debe estructurarse dentro 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;
}
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);
}
}
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);
}
}
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.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.
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
Como regla general de diseño orientada a objetos:
private). Esto protege el estado del objeto frente a modificaciones no válidas desde el exterior (por ejemplo, evitar asignaciones como coche1.velocidad = -300000;).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;
}
getAtributo() y el de escritura como setAtributo(). En atributos de tipo booleano se suele emplear isAtributo() para la lectura (ej. isElectrico()).
No. Únicamente debemos crear los métodos accesores que queramos exponer al exterior:
this.marca = marca;, la palabra reservada this hace referencia a la propiedad perteneciente a la instancia actual del objeto.
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
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:
void).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
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);
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.
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));
}
}
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;
}
}
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));
}
}
Calculadora calc = new Calculadora();
IO.println("2 + 4 = " + calc.sumar(2, 4)); // Desaconsejado: debe usarse Calculadora.sumar(2, 4)
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));
}
}
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.
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:
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.
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.
java.lang.Math).final).
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:
Para declarar una enumeración utilizamos la palabra reservada enum. Los valores se escriben habitualmente en mayúsculas:
public enum KiaModel {
NIRO,
XCEED,
SPORTAGE
}
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);
}
}
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();
}
}
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:
restock() y muestra el stock actualizado.sell() y muestra el stock resultante.
Modifica la clase Product para que sus métodos de control de inventario gestionen errores mediante valores de retorno:
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.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.
Cambia la visibilidad de los atributos de Product a private y crea los métodos necesarios para leer y escribir los atributos.
Crea un constructor de Product donde se inicialicen todos sus atributos.
Crea dos productos con el mismo valor en sus atributos y utiliza == para comprobar si son el mismo producto.
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.
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:final): id (long), name (String) y description (String).getters).Product:final): id (long), name (String), price (double), stockQuantity (int) y category (de tipo Category).getters).CategoryDb:categories inicializado con 2 categorías (“Libros” y “Electrónica”).getById(long id), que recorre el array y devuelve la categoría coincidente (o null si no existe).ProductDb:products inicializado con 4 productos asociados a sus respectivas categorías mediante CategoryDb.getById().getById(long id).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).
Añade las siguientes clases al ejercicio anterior:
OrderStatus (enum):PENDING, PROCESSING, SHIPPED, DELIVERED y CANCELLED.description (String) que guarde una frase descriptiva en español (ej. “Pendiente de pago”, “Enviado”, etc.).getDescription().Order:id (long), product (Product), quantity (int) y status (de tipo OrderStatus).id, product y quantity, e inicialice por defecto el estado status en OrderStatus.PENDING.getters) y un setter para modificar el atributo status.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.