Al terminar la lección anterior tus objetos eran cajas de datos: Libro guarda un título y un ISBN, pero no sabe hacer absolutamente nada con ellos. Toda la inteligencia de BiblioTech sigue encerrada en las doscientas líneas de main, que calcula días de retraso, aplica el tope de la multa y clasifica la gravedad como si los objetos no existieran. Esta lección aporta la mitad que falta: el comportamiento. Un método es una operación con nombre que agrupa un conjunto de instrucciones, recibe datos, hace un trabajo y normalmente devuelve un resultado. Es la unidad de descomposición del código —lo que impide que main crezca sin fin— y, en la POO, es la forma en que un objeto expone lo que sabe hacer. Al acabar, calcularMulta() no será un bloque suelto en medio de un switch: será algo que un Prestamo sabe hacer consigo mismo.
Contenido
- Anatomía de un método
- El valor de retorno y
return - Métodos
void - Paso de parámetros: Java siempre pasa por valor
- Qué implica eso con primitivos
- Qué implica eso con referencias (y por qué confunde a todo el mundo)
- Sobrecarga de métodos
- Métodos
staticfrente a métodos de instancia - La referencia
this - Varargs: número variable de argumentos
- Ámbito de las variables y shadowing
- Buenas prácticas de diseño de métodos
- Recursión: un método que se llama a sí mismo
- Refactorización guiada de BiblioTech
- Errores Comunes y Consejos
- Ejercicios
- Anatomía de un método
Esta es la estructura completa de un método Java:
| Parte | En el ejemplo | Qué significa |
|---|---|---|
| Modificador de acceso | public |
Quién puede llamarlo (03-07) |
| Otros modificadores | (ninguno) | static, final, abstract... |
| Tipo de retorno | double |
Qué tipo de valor devuelve; void si no devuelve nada |
| Nombre | calcularMulta |
Verbo en lowerCamelCase |
| Lista de parámetros | (int diasRetraso) |
Datos de entrada, con tipo y nombre |
| Cuerpo | { return ...; } |
Las instrucciones que se ejecutan |
Al conjunto nombre + tipos de parámetros en orden se le llama firma del método. La firma de este es calcularMulta(int). El tipo de retorno no forma parte de la firma, y eso tendrá consecuencias en el apartado 7.
Vocabulario que conviene fijar para no confundirse nunca más:
- Parámetro: la variable declarada en la definición del método (
int diasRetraso). - Argumento: el valor concreto que se pasa al llamarlo (
calcularMulta(5)pasa el argumento5).
Llamar a un método de instancia se hace siempre a través de un objeto:
- El valor de retorno y
return
returnreturn hace dos cosas a la vez: entrega un valor al llamador y termina inmediatamente la ejecución del método. Lo segundo lo viste en la lección 02-04, aplicado al control de flujo.
public String clasificarGravedad(int diasRetraso) {
if (diasRetraso == 0) {
return "SIN RETRASO"; // sale aqui mismo
}
if (diasRetraso <= UMBRAL_LEVE) {
return "LEVE";
}
return "GRAVE";
}Reglas del compilador que debes conocer:
- El valor devuelto debe ser compatible con el tipo declarado. Un método
doublepuede devolver unint(promoción automática), pero un métodointno puede devolver undoublesin casting explícito. - Todos los caminos deben devolver un valor. Si el compilador encuentra una ruta sin
return, falla con "missing return statement". - El código posterior a un
returnalcanzado siempre es código inalcanzable y no compila.
Este método, por ejemplo, no compila:
public String clasificar(int dias) {
if (dias > 0) {
return "CON RETRASO";
}
// ERROR: si dias <= 0 no hay return
}Basta añadir un else o un return final para arreglarlo. La segunda forma —usar cláusulas de guarda y dejar el caso general al final, sin else— es la que usaste en 02-01 y la que se prefiere en código profesional.
- Métodos
void
voidCuando un método hace algo en lugar de calcular algo, su tipo de retorno es void:
En un método void puedes usar return; a secas para salir antes de tiempo, pero no puedes devolver un valor:
public void devolver() {
if (disponible) {
System.out.println("El libro ya estaba disponible.");
return; // salida anticipada, sin valor
}
disponible = true;
}Un criterio útil de diseño, conocido como separación entre consultas y órdenes: un método o cambia el estado (y devuelve void) o responde una pregunta (y devuelve un valor), pero preferiblemente no ambas cosas. Un método llamado getMulta() que además marque el libro como devuelto sorprendería a cualquiera que lo lea.
- Paso de parámetros: Java siempre pasa por valor
Aquí está uno de los puntos donde más gente se equivoca, incluso con años de experiencia. La regla es esta, y no tiene excepciones:
En Java, los argumentos se pasan siempre por valor. Es decir: al llamar a un método, se copia el valor del argumento en el parámetro.
Lo que despista es qué se está copiando en cada caso:
| Tipo de argumento | Qué se copia | Consecuencia |
|---|---|---|
Primitivo (int, double, boolean...) |
El valor mismo | El método trabaja con una copia independiente |
Referencia (String, Libro, Prestamo...) |
La dirección, no el objeto | El método llega al mismo objeto, pero con su propia flecha |
De ahí las dos consecuencias que hay que memorizar:
- Reasignar el parámetro nunca afecta al llamador (ni con primitivos ni con objetos).
- Mutar el objeto al que apunta el parámetro sí afecta al llamador (porque es el mismo objeto).
Los dos apartados siguientes lo demuestran con código.
- Qué implica eso con primitivos
public static void intentarModificar(int dias) {
dias = 999; // solo cambia la copia local
System.out.println(" Dentro del metodo: " + dias);
}
public static void main(String[] args) {
int diasTranscurridos = 20;
System.out.println("Antes: " + diasTranscurridos);
intentarModificar(diasTranscurridos);
System.out.println("Despues: " + diasTranscurridos);
}Salida:
El parámetro dias es una variable nueva en la pila del método, inicializada con una copia de 20. Cambiarla no toca la variable original. Si un método debe "modificar" un primitivo, la única vía es devolver el nuevo valor:
- Qué implica eso con referencias (y por qué confunde a todo el mundo)
Ahora el caso interesante. Dos métodos aparentemente similares, con resultados opuestos:
public static void mutarLibro(Libro libro) {
libro.titulo = "TITULO CAMBIADO"; // muta el objeto compartido
}
public static void reasignarLibro(Libro libro) {
libro = new Libro(); // reapunta SOLO la flecha local
libro.titulo = "OTRO LIBRO";
}
public static void main(String[] args) {
Libro javaEfectivo = new Libro();
javaEfectivo.titulo = "Java Efectivo";
mutarLibro(javaEfectivo);
System.out.println("Tras mutar: " + javaEfectivo.titulo);
reasignarLibro(javaEfectivo);
System.out.println("Tras reasignar: " + javaEfectivo.titulo);
}Salida:
La segunda llamada no ha hecho nada visible. Veamos por qué, en memoria:
flowchart TB
subgraph P1["Pila de main"]
M["javaEfectivo = 0x1A"]
end
subgraph P2["Pila de reasignarLibro"]
L1["libro = 0x1A (al entrar)"]
L2["libro = 0x9F (tras new)"]
end
subgraph H["Heap"]
O1["0x1A : Libro
titulo = TITULO CAMBIADO"]
O2["0x9F : Libro
titulo = OTRO LIBRO"]
end
M --> O1
L1 -.-> O1
L2 --> O2
Al entrar en reasignarLibro, el parámetro libro recibe una copia de la dirección 0x1A: hay dos flechas apuntando al mismo objeto. Cuando el método hace libro = new Libro(), mueve su propia flecha al nuevo objeto 0x9F. La flecha de main no se ha tocado. Al salir del método, la pila local desaparece y el objeto 0x9F queda inalcanzable.
En mutarLibro, en cambio, no se toca ninguna flecha: se usa el punto para modificar el objeto al que ambas apuntan, y por eso el cambio se ve desde main.
La frase que resume todo: puedes cambiar el contenido del objeto que te pasan, pero no puedes cambiar a qué objeto apunta la variable del llamador.
Un corolario práctico: String es inmutable (módulo 1), así que ningún método puede alterar la cadena que le pasas. Esto no funciona nunca:
public static void ensuciar(String texto) {
texto = texto.toUpperCase(); // crea otra cadena; el llamador no se entera
}
- Sobrecarga de métodos
Sobrecargar es declarar varios métodos con el mismo nombre y distinta lista de parámetros en la misma clase. Java elige cuál llamar según los argumentos.
public class Prestamo {
/** Multa de este prestamo, usando sus propios dias transcurridos. */
public double calcularMulta() {
return calcularMulta(diasTranscurridos);
}
/** Multa que corresponderia a un numero concreto de dias transcurridos. */
public double calcularMulta(int diasTranscurridos) {
int retraso = Math.max(0, diasTranscurridos - DIAS_PRESTAMO);
return Math.min(retraso * TARIFA_DIARIA, MULTA_MAXIMA);
}
/** Multa con una tarifa distinta (por ejemplo, en una campana especial). */
public double calcularMulta(int diasTranscurridos, double tarifa) {
int retraso = Math.max(0, diasTranscurridos - DIAS_PRESTAMO);
return Math.min(retraso * tarifa, MULTA_MAXIMA);
}
}Qué distingue una sobrecarga válida:
| Cambio | ¿Sobrecarga válida? |
|---|---|
| Distinto número de parámetros | Sí |
| Distintos tipos de parámetros | Sí |
| Distinto orden de tipos distintos | Sí ((int, String) frente a (String, int)) |
| Solo distinto nombre de los parámetros | No: error de compilación |
| Solo distinto tipo de retorno | No: el retorno no forma parte de la firma |
Reglas de resolución, que Java aplica en este orden hasta encontrar una coincidencia:
- Coincidencia exacta de tipos.
- Promoción de tipos primitivos (
int→long→float→double). - Autoboxing/unboxing (
int↔Integer, módulo 1). - Varargs (apartado 10), que siempre es la última opción considerada.
public static void registrar(int dias) { System.out.println("version int"); }
public static void registrar(double dias) { System.out.println("version double"); }
public static void registrar(Object dias) { System.out.println("version Object"); }
registrar(5); // "version int" -> coincidencia exacta
registrar(5.0); // "version double" -> coincidencia exacta
registrar(5L); // "version double" -> long promociona a double antes que a ObjectConsejo: la sobrecarga es útil cuando las variantes hacen conceptualmente lo mismo con distintos datos. Si dos métodos con el mismo nombre hacen cosas distintas, ponles nombres distintos.
- Métodos
static frente a métodos de instancia
static frente a métodos de instanciaLa diferencia es la misma que viste con los campos en 03-02:
| Aspecto | Método de instancia | Método static |
|---|---|---|
| Pertenece a | Un objeto concreto | La clase |
| Cómo se llama | prestamo.calcularMulta() |
Prestamo.esRetrasoLeve(5) |
| Puede usar campos de instancia | Sí | No (no hay objeto) |
Puede usar this |
Sí | No |
Puede usar campos static |
Sí | Sí |
| Cuándo usarlo | La operación depende del estado del objeto | La operación solo depende de sus parámetros |
Ejemplos del mundo real que ya conoces: Math.max(a, b) y Integer.parseInt(texto) son static porque no necesitan ningún estado; scanner.nextLine() es de instancia porque depende del estado de ese Scanner. Y main es static porque la JVM debe poder llamarlo antes de que exista ningún objeto.
Aplicado a BiblioTech:
public class Prestamo {
/** De instancia: usa los datos de ESTE prestamo. */
public int calcularDiasRetraso() {
return Math.max(0, diasTranscurridos - DIAS_PRESTAMO);
}
/** Estatico: pura funcion de sus parametros, util sin tener un prestamo. */
public static boolean esRetrasoLeve(int diasRetraso) {
return diasRetraso > 0 && diasRetraso <= UMBRAL_LEVE;
}
}Error clásico de principiante, y probablemente el mensaje del compilador que más veces verás:
public class Prestamo {
public int diasTranscurridos;
public static int mal() {
return diasTranscurridos; // ERROR
}
}La traducción es literal: un método static no está asociado a ningún objeto, así que la pregunta "¿los días transcurridos de quién?" no tiene respuesta.
- La referencia
this
thisDentro de un método de instancia, this es una referencia al objeto sobre el que se llamó el método. Tiene tres usos:
a) Desambiguar cuando un parámetro se llama igual que un campo (el uso más frecuente, y el que verás constantemente en constructores):
public void establecerDiasTranscurridos(int diasTranscurridos) {
this.diasTranscurridos = diasTranscurridos; // campo = parametro
}Sin this, diasTranscurridos = diasTranscurridos; asigna el parámetro a sí mismo y el campo no cambia. El compilador no se queja; el error es silencioso.
b) Llamar a otro método del mismo objeto (opcional, pero a veces más legible):
public String generarResumen() {
return this.clasificarGravedad() + " - " + this.calcularMulta() + " EUR";
}c) Pasar el objeto actual a otro método:
Fuera de esos casos, escribir this. delante de todo es ruido. En Java el estilo dominante es omitirlo salvo cuando sea necesario. En 03-04 aparecerá una cuarta forma, this(...), para encadenar constructores.
- Varargs: número variable de argumentos
Los varargs permiten declarar un parámetro que acepta cero, uno o muchos argumentos del mismo tipo. Se escriben con tres puntos:
public static void registrarEventos(String... eventos) {
System.out.println("Eventos recibidos: " + eventos.length);
for (String evento : eventos) {
System.out.println(" - " + evento);
}
}Llamadas válidas:
registrarEventos(); // 0 eventos
registrarEventos("Prestamo registrado"); // 1
registrarEventos("Devolucion", "Multa aplicada"); // 2Dentro del método, eventos es un array (String[]), con su length y su bucle for-each. Los arrays se estudian a fondo en la lección 05-01; aquí basta con saber que se recorren así.
Dos reglas obligatorias:
- Solo puede haber un parámetro varargs por método.
- Debe ser el último de la lista:
registrar(String empleado, String... eventos)es válido; al revés, no compila.
Ya has usado varargs sin saberlo: System.out.printf(String formato, Object... args) es exactamente esto, y por eso admite cualquier número de valores tras la cadena de formato.
- Ámbito de las variables y shadowing
El ámbito (scope) de una variable es la región de código donde es visible. Resumen:
| Dónde se declara | Desde dónde se ve | Cuánto vive |
|---|---|---|
| Campo de instancia | Todos los métodos de la clase | Lo que viva el objeto |
Campo static |
Todos los métodos, incluidos los static |
Todo el programa |
| Parámetro | Solo dentro de su método | La ejecución del método |
| Variable local | Desde su declaración hasta el } de su bloque |
La ejecución del bloque |
El shadowing ocurre cuando una variable local o un parámetro tiene el mismo nombre que un campo: dentro de ese ámbito, el nombre se refiere a la variable local, y el campo queda "en la sombra".
public class Prestamo {
public int diasTranscurridos = 20; // campo
public void demostrarShadowing(int diasTranscurridos) { // parametro que ensombrece
System.out.println(diasTranscurridos); // el PARAMETRO
System.out.println(this.diasTranscurridos); // el CAMPO
}
}El shadowing no es un error —de hecho es la práctica habitual en constructores y setters, donde el parámetro se llama igual que el campo a propósito—, pero exige recordar this. al asignar.
- Buenas prácticas de diseño de métodos
Estos criterios distinguen un método profesional de uno improvisado:
- Un método, un propósito. Si al describir lo que hace necesitas la palabra "y", probablemente sean dos métodos.
- Cortos. No hay una cifra mágica, pero si un método no cabe en la pantalla, cuesta razonar sobre él. Los doscientos de
mainson el contraejemplo perfecto. - Nombres verbales y precisos.
calcularMulta,registrarDevolucion,estaDisponible. Evitaprocesar,gestionar,hacerodatos. - Convención de nombres booleanos:
esX,estaX,tieneX.if (libro.estaDisponible())se lee como una frase. - Pocos parámetros. Con más de tres o cuatro, plantéate agrupar datos relacionados en un objeto. Es, otra vez, el problema de las variables sueltas.
- Evita los parámetros booleanos.
generarInforme(true)no dice nada al leerlo. ¿Qué estrue?
// Mal: ¿que significa ese false?
imprimirRecibo(prestamo, false);
// Mejor: dos metodos con nombre explicito
imprimirReciboCompleto(prestamo);
imprimirReciboResumido(prestamo);- No mezcles niveles de abstracción. Un método que calcula reglas de negocio no debería además formatear con
printf. Es el tema central de la lección 03-08. - Documenta con Javadoc lo que no sea obvio: qué devuelve, qué espera y qué pasa en los casos límite.
- Recursión: un método que se llama a sí mismo
Un método puede llamarse a sí mismo. A eso se le llama recursión, y toda recursión correcta tiene dos partes: un caso base que la detiene y un caso recursivo que se acerca al caso base.
/** Suma 1 + 2 + ... + n de forma recursiva. */
public static int sumarHasta(int n) {
if (n <= 0) { // caso base: detiene la recursion
return 0;
}
return n + sumarHasta(n - 1); // caso recursivo
}Traza de sumarHasta(4):
sumarHasta(4) = 4 + sumarHasta(3)
= 4 + (3 + sumarHasta(2))
= 4 + (3 + (2 + sumarHasta(1)))
= 4 + (3 + (2 + (1 + sumarHasta(0))))
= 4 + 3 + 2 + 1 + 0 = 10Cada llamada pendiente ocupa un marco en la pila. Si falta el caso base o nunca se alcanza, la pila se agota y obtienes un StackOverflowError, que reconocerás porque la traza repite la misma línea cientos de veces.
En el día a día, la recursión brilla con estructuras que se anidan por naturaleza —árboles, directorios, expresiones— y para el resto suele ser preferible un bucle, que gasta menos memoria. La verás aplicada en la lección 05-09 (búsqueda binaria) y en el módulo 7 (recorrer directorios).
- Refactorización guiada de BiblioTech
Llega el momento de aplicar todo esto al proyecto. Lo haremos en dos pasos, porque así se hace en la vida real: primero extraer, después mudar.
Paso 1: extraer los cálculos de main a métodos static
Este es el código que hoy vive dentro del switch de main:
// --- ANTES: dentro de main, en medio del case "1" ---
int diasRetraso = diasTranscurridos - DIAS_PRESTAMO;
if (diasRetraso < 0) {
diasRetraso = 0;
}
double multa = diasRetraso * TARIFA_DIARIA;
if (multa >= MULTA_MAXIMA) {
multa = MULTA_MAXIMA;
}
String gravedad;
if (diasRetraso == 0) {
gravedad = "SIN RETRASO";
} else if (diasRetraso <= UMBRAL_LEVE) {
gravedad = "LEVE";
} else {
gravedad = "GRAVE";
}Primera mejora: sacarlo a métodos con nombre.
// --- DESPUES (paso 1): metodos static en BiblioTechApp ---
/**
* Calcula los dias de retraso, saturados a cero.
* @param diasTranscurridos dias desde que se realizo el prestamo
* @return 0 si aun esta en plazo; los dias de exceso en caso contrario
*/
private static int calcularDiasRetraso(int diasTranscurridos) {
return Math.max(0, diasTranscurridos - DIAS_PRESTAMO);
}
/**
* Calcula la multa correspondiente a un retraso, aplicando el tope legal.
* @param diasRetraso dias de retraso, nunca negativos
* @return importe en euros, nunca superior a MULTA_MAXIMA
*/
private static double calcularMulta(int diasRetraso) {
return Math.min(diasRetraso * TARIFA_DIARIA, MULTA_MAXIMA);
}
/**
* Clasifica la gravedad de un retraso segun la politica de Nexus Software.
* @param diasRetraso dias de retraso, nunca negativos
* @return "SIN RETRASO", "LEVE" o "GRAVE"
*/
private static String clasificarGravedad(int diasRetraso) {
if (diasRetraso == 0) return "SIN RETRASO";
if (diasRetraso <= UMBRAL_LEVE) return "LEVE";
return "GRAVE";
}Y el case "1" queda así:
int diasRetraso = calcularDiasRetraso(diasTranscurridos);
double multa = calcularMulta(diasRetraso);
String gravedad = clasificarGravedad(diasRetraso);Tres líneas legibles en lugar de veinte. Fíjate en el uso de Math.max y Math.min, que sustituyen los if de saturación: Math.max(0, x) es "nunca por debajo de cero" y Math.min(x, tope) es "nunca por encima del tope". Menos código y menos sitios donde equivocarse.
Paso 2: mudar los métodos a Prestamo
El paso 1 mejora la legibilidad, pero deja las reglas de negocio en la clase de arranque. Y esas reglas no son de la aplicación: son del préstamo. Este es el paso realmente orientado a objetos.
package com.nexussoftware.bibliotech.dominio;
/**
* Registro de un prestamo de un libro a un empleado de Nexus Software.
* Conoce sus propios datos y sabe aplicar sobre ellos las reglas de negocio.
*/
public class Prestamo {
public static final int DIAS_PRESTAMO = 15;
public static final double TARIFA_DIARIA = 0.25;
public static final double MULTA_MAXIMA = 20.0;
public static final int UMBRAL_LEVE = 7;
public static int prestamosCreados = 0;
public Libro libro;
public String empleado;
public int diasTranscurridos;
// ---------- Comportamiento de instancia ----------
/** @return dias de retraso de ESTE prestamo, saturados a cero. */
public int calcularDiasRetraso() {
return Math.max(0, diasTranscurridos - DIAS_PRESTAMO);
}
/** @return multa de ESTE prestamo en euros, con el tope aplicado. */
public double calcularMulta() {
return Math.min(calcularDiasRetraso() * TARIFA_DIARIA, MULTA_MAXIMA);
}
/** @return "SIN RETRASO", "LEVE" o "GRAVE" segun el retraso de ESTE prestamo. */
public String clasificarGravedad() {
int retraso = calcularDiasRetraso();
if (retraso == 0) return "SIN RETRASO";
if (retraso <= UMBRAL_LEVE) return "LEVE";
return "GRAVE";
}
/** @return true si el plazo de 15 dias ya se ha superado. */
public boolean estaVencido() {
return calcularDiasRetraso() > 0;
}
/** @return true si la multa ha alcanzado el tope de MULTA_MAXIMA. */
public boolean tieneMultaTopada() {
return calcularMulta() >= MULTA_MAXIMA;
}
/** @return dias que faltan para vencer; 0 si ya ha vencido. */
public int diasRestantes() {
return Math.max(0, DIAS_PRESTAMO - diasTranscurridos);
}
// ---------- Utilidad estatica ----------
/**
* Indica si un retraso dado se considera leve, sin necesidad de tener
* un prestamo construido. Util para tablas y simulaciones.
*/
public static boolean esRetrasoLeve(int diasRetraso) {
return diasRetraso > 0 && diasRetraso <= UMBRAL_LEVE;
}
}Y ahora, el antes y el después del cálculo en main:
| Antes (módulo 2) | Después (este módulo) |
|---|---|
20 líneas dentro de case "1" |
prestamo.calcularMulta() |
| Las reglas se repiten en la escala de multas | Existen una sola vez, en Prestamo |
Cambiar la tarifa obliga a revisar todo main |
Se cambia una constante |
| Los datos van sueltos por parámetros | Viajan dentro del objeto |
| Solo se puede calcular el préstamo "en curso" | Se puede calcular cualquier préstamo |
Uso desde BiblioTechApp:
Prestamo prestamo = new Prestamo();
prestamo.libro = refactorizacion;
prestamo.empleado = "Marta Ruiz";
prestamo.diasTranscurridos = 20;
Prestamo.prestamosCreados++;
System.out.printf("Libro: %s%n", prestamo.libro.titulo);
System.out.printf("Empleado: %s%n", prestamo.empleado);
System.out.printf("Retraso: %d dias%n", prestamo.calcularDiasRetraso());
System.out.printf("Multa: %.2f EUR%n", prestamo.calcularMulta());
System.out.printf("Gravedad: %s%n", prestamo.clasificarGravedad());
System.out.printf("Vencido: %b%n", prestamo.estaVencido());Salida:
Libro: Refactorización Empleado: Marta Ruiz Retraso: 5 dias Multa: 1,25 EUR Gravedad: LEVE Vencido: true
Este es el momento en que BiblioTech se vuelve orientado a objetos de verdad: main ya no calcula nada, solo pregunta. Y observa que calcularMulta() llama internamente a calcularDiasRetraso(): un método puede apoyarse en otro del mismo objeto, evitando duplicar la lógica de saturación.
Añadamos también comportamiento a Libro, que hasta ahora era pura data:
public class Libro {
public static int librosCreados = 0;
public String titulo;
public String autor;
public String isbn;
public int anioPublicacion;
public boolean disponible;
/** @return true si el ejemplar puede prestarse ahora mismo. */
public boolean estaDisponible() {
return disponible;
}
/** Marca el ejemplar como prestado. Avisa si ya lo estaba. */
public void prestar() {
if (!disponible) {
System.out.println("AVISO: '" + titulo + "' ya estaba prestado.");
return;
}
disponible = false;
}
/** Marca el ejemplar como devuelto. Avisa si ya estaba en la estanteria. */
public void devolver() {
if (disponible) {
System.out.println("AVISO: '" + titulo + "' ya estaba disponible.");
return;
}
disponible = true;
}
/** @return descripcion legible del libro para listados. */
public String describir() {
return titulo + " - " + autor + " (" + anioPublicacion + "), ISBN " + isbn;
}
}Fíjate en prestar() y devolver(): son la razón de ser del encapsulamiento. Con el campo público, cualquiera puede escribir libro.disponible = true; saltándose el aviso; con estos métodos, la operación pasa por un único sitio que puede validar. La lección 03-07 cerrará esa puerta definitivamente.
Errores Comunes y Consejos
- Olvidar
thisen un setter.diasTranscurridos = diasTranscurridos;compila, no hace nada y no avisa. Es de los errores más frustrantes: el IDE lo señala como "asignación a sí mismo", hazle caso. - Creer que Java pasa objetos por referencia. No: pasa la referencia por valor. La diferencia se nota exactamente cuando reasignas el parámetro, como en el apartado 6.
- Intentar usar campos de instancia desde un método
static. El compilador dirá "non-static variable ... cannot be referenced from a static context". Pregúntate si el método debería ser de instancia o si le falta recibir el objeto por parámetro. - Sobrecargar cambiando solo el tipo de retorno. No compila: la firma no incluye el retorno.
- Métodos que hacen dos cosas.
calcularYGuardarMulta()es una señal de alarma en el propio nombre. - Devolver códigos numéricos mágicos. Devolver
-1para indicar "no se pudo calcular" obliga a cada llamador a conocer ese convenio. Mientras no dispongas de excepciones (módulo 6) ni deOptional(10-04), documenta el convenio con Javadoc de forma explícita. - Consejo: usa "extraer método" del IDE. IntelliJ (
Ctrl+Alt+M) y Eclipse (Alt+Shift+M) hacen la refactorización del paso 1 automáticamente y sin errores: seleccionas el bloque, le das nombre y el IDE deduce los parámetros y el retorno. - Consejo: escribe primero la firma y el Javadoc, después el cuerpo. Obliga a decidir qué entra y qué sale antes de perderse en la implementación.
Ejercicios
Ejercicio 1: comportamiento en Empleado
Amplía la clase Empleado de la lección anterior con estos métodos:
registrarPrestamo(): incrementaprestamosAcumuladosen uno. Si el empleado ya está en el límiteMAX_PRESTAMOS_SIMULTANEOS, no lo incrementa y avisa por consola.registrarDevolucion(): decrementaprestamosAcumulados, nunca por debajo de cero.puedeTomarPrestado(): devuelvetruesi aún no ha alcanzado el máximo.getIniciales(): devuelve las iniciales del empleado ("Marta Ruiz" → "M.R.").- Una versión sobrecargada
registrarPrestamo(int cantidad)que registre varios de golpe respetando el límite.
Pruébalos con Marta Ruiz hasta que alcance el límite.
Ejercicio 2: demostrar el paso por valor
Escribe una clase DemoPasoPorValor con cuatro métodos static que reciban, respectivamente: un int, un String, un Libro que se muta y un Libro que se reasigna. Cada uno debe imprimir el valor al entrar y al salir. Desde main, llama a los cuatro e imprime el estado antes y después de cada llamada. Después responde por escrito: ¿por qué el caso del String se comporta como el del int y no como el del Libro mutado?
Ejercicio 3: simulador de escala de multas con varargs
Escribe en Prestamo un método static simularEscala(int... diasTranscurridos) que reciba cualquier número de valores de días transcurridos e imprima una tabla con: días transcurridos, días de retraso, multa sin tope, multa aplicada y gravedad. Debe marcar con <-- la primera fila donde la multa alcance el tope. Reutiliza los métodos existentes y no dupliques ninguna regla de negocio. Pruébalo con simularEscala(5, 15, 20, 30, 60, 95, 100).
Soluciones
Solución 1
package com.nexussoftware.bibliotech.dominio;
public class Empleado {
public static final int MAX_PRESTAMOS_SIMULTANEOS = 3;
public static int empleadosRegistrados = 0;
public String nombre;
public String identificador;
public int prestamosAcumulados;
/** @return true si aun puede llevarse otro libro. */
public boolean puedeTomarPrestado() {
return prestamosAcumulados < MAX_PRESTAMOS_SIMULTANEOS;
}
/** Registra un prestamo si el limite lo permite. */
public void registrarPrestamo() {
if (!puedeTomarPrestado()) {
System.out.printf("AVISO: %s ya tiene %d prestamos (maximo %d).%n",
nombre, prestamosAcumulados, MAX_PRESTAMOS_SIMULTANEOS);
return; // salida anticipada
}
prestamosAcumulados++;
}
/**
* Registra varios prestamos de golpe. Version sobrecargada: misma
* operacion conceptual con distinta entrada.
*/
public void registrarPrestamo(int cantidad) {
for (int i = 0; i < cantidad; i++) {
registrarPrestamo(); // reutiliza la logica y el limite
}
}
/** Registra una devolucion, sin bajar nunca de cero. */
public void registrarDevolucion() {
prestamosAcumulados = Math.max(0, prestamosAcumulados - 1);
}
/** @return iniciales del nombre completo, por ejemplo "M.R." */
public String getIniciales() {
String[] partes = nombre.trim().split(" ");
StringBuilder sb = new StringBuilder(); // patron de la leccion 02-02
for (String parte : partes) {
if (!parte.isEmpty()) {
sb.append(parte.charAt(0)).append('.');
}
}
return sb.toString().toUpperCase();
}
}Prueba:
Empleado marta = new Empleado();
marta.nombre = "Marta Ruiz";
marta.identificador = "EMP-001";
System.out.println(marta.getIniciales()); // M.R.
marta.registrarPrestamo(2); // 2 prestamos
System.out.println(marta.prestamosAcumulados); // 2
System.out.println(marta.puedeTomarPrestado()); // true
marta.registrarPrestamo(); // 3
marta.registrarPrestamo(); // AVISO: limite alcanzado
System.out.println(marta.prestamosAcumulados); // 3
marta.registrarDevolucion();
System.out.println(marta.prestamosAcumulados); // 2Salida:
Claves: registrarPrestamo(int) no duplica la comprobación del límite, delega en la versión sin parámetros; y Math.max(0, ...) en la devolución evita un contador negativo sin necesidad de un if.
Solución 2
package com.nexussoftware.bibliotech;
import com.nexussoftware.bibliotech.dominio.Libro;
public class DemoPasoPorValor {
static void conPrimitivo(int dias) {
System.out.println(" entra: " + dias);
dias = 999;
System.out.println(" sale: " + dias);
}
static void conString(String titulo) {
System.out.println(" entra: " + titulo);
titulo = titulo.toUpperCase(); // crea OTRA cadena; solo cambia la copia local
System.out.println(" sale: " + titulo);
}
static void mutandoObjeto(Libro libro) {
System.out.println(" entra: " + libro.titulo);
libro.titulo = "MUTADO"; // toca el objeto compartido
System.out.println(" sale: " + libro.titulo);
}
static void reasignandoObjeto(Libro libro) {
System.out.println(" entra: " + libro.titulo);
libro = new Libro(); // mueve SOLO la flecha local
libro.titulo = "NUEVO OBJETO";
System.out.println(" sale: " + libro.titulo);
}
public static void main(String[] args) {
int dias = 20;
System.out.println("1) Primitivo. Antes: " + dias);
conPrimitivo(dias);
System.out.println(" Despues: " + dias); // 20
String titulo = "Java Efectivo";
System.out.println("2) String. Antes: " + titulo);
conString(titulo);
System.out.println(" Despues: " + titulo); // Java Efectivo
Libro libro = new Libro();
libro.titulo = "Patrones de Diseño";
System.out.println("3) Objeto mutado. Antes: " + libro.titulo);
mutandoObjeto(libro);
System.out.println(" Despues: " + libro.titulo); // MUTADO
Libro otro = new Libro();
otro.titulo = "Refactorización";
System.out.println("4) Objeto reasignado. Antes: " + otro.titulo);
reasignandoObjeto(otro);
System.out.println(" Despues: " + otro.titulo); // Refactorización
}
}Salida:
1) Primitivo. Antes: 20 entra: 20 sale: 999 Despues: 20 2) String. Antes: Java Efectivo entra: Java Efectivo sale: JAVA EFECTIVO Despues: Java Efectivo 3) Objeto mutado. Antes: Patrones de Diseño entra: Patrones de Diseño sale: MUTADO Despues: MUTADO 4) Objeto reasignado. Antes: Refactorización entra: Refactorización sale: NUEVO OBJETO Despues: Refactorización
Respuesta a la pregunta. El String se comporta como el int porque es inmutable: toUpperCase() no modifica la cadena original, crea una nueva y la devuelve. Al asignarla al parámetro, solo se mueve la flecha local, exactamente igual que en el caso 4. No es que los String se pasen de otro modo —se pasan por valor de la referencia, como cualquier objeto—, es que no existe forma de mutarlos, así que el caso 3 es imposible con String. Esa es una de las grandes ventajas de la inmutabilidad, y la razón por la que en 03-07 verás que Libro es un buen candidato a ser inmutable también.
Solución 3
/**
* Imprime la escala de multas para una serie de valores de dias transcurridos.
* @param diasTranscurridos cero o mas valores a simular
*/
public static void simularEscala(int... diasTranscurridos) {
System.out.println(" ESCALA DE MULTAS DE BIBLIOTECH");
System.out.println(" ==================================================");
System.out.printf(" %8s %9s %11s %11s %-11s%n",
"TRANSC.", "RETRASO", "SIN TOPE", "APLICADA", "GRAVEDAD");
boolean topeYaMarcado = false; // patron bandera (leccion 02-02)
for (int dias : diasTranscurridos) {
// Se reutiliza la MISMA logica del dominio: cero duplicacion de reglas.
int retraso = Math.max(0, dias - DIAS_PRESTAMO);
double sinTope = retraso * TARIFA_DIARIA;
double aplicada = Math.min(sinTope, MULTA_MAXIMA);
String gravedad;
if (retraso == 0) gravedad = "SIN RETRASO";
else if (esRetrasoLeve(retraso)) gravedad = "LEVE";
else gravedad = "GRAVE";
String marca = "";
if (!topeYaMarcado && aplicada >= MULTA_MAXIMA) {
marca = " <--";
topeYaMarcado = true; // una vez levantada, no vuelve a bajar
}
System.out.printf(" %8d %9d %11.2f %11.2f %-11s%s%n",
dias, retraso, sinTope, aplicada, gravedad, marca);
}
}Llamada y salida:
ESCALA DE MULTAS DE BIBLIOTECH
==================================================
TRANSC. RETRASO SIN TOPE APLICADA GRAVEDAD
5 0 0,00 0,00 SIN RETRASO
15 0 0,00 0,00 SIN RETRASO
20 5 1,25 1,25 LEVE
30 15 3,75 3,75 GRAVE
60 45 11,25 11,25 GRAVE
95 80 20,00 20,00 GRAVE <--
100 85 21,25 20,00 GRAVETres detalles que merecen atención. Primero, el método es static porque simula situaciones hipotéticas: no hay ningún préstamo real detrás, solo números. Segundo, reutiliza esRetrasoLeve(int), también static, sin duplicar el umbral. Y tercero, simularEscala() sin argumentos es una llamada perfectamente válida: imprime solo la cabecera, porque un varargs admite cero elementos.
Un detalle a discutir: este método mezcla cálculo y presentación, justo lo que desaconseja el apartado 12. Es aceptable en una utilidad de diagnóstico como esta, pero recuérdalo: en la lección 03-08 verás por qué en el código de negocio esa mezcla se paga cara.
Conclusión
Tus objetos ya saben hacer cosas. Dominas la anatomía completa de un método —modificadores, tipo de retorno, firma, parámetros, cuerpo— y la diferencia entre parámetro y argumento; sabes cuándo un método devuelve valor y cuándo es void, y por qué conviene que no haga ambas cosas. Has entendido la regla que más confusión genera en Java: todo se pasa por valor, incluidos los objetos, cuya referencia se copia; de ahí que mutar el objeto recibido afecte al llamador y reasignar el parámetro no. Sabes sobrecargar métodos y conoces las reglas con que Java elige la versión correcta, distingues métodos static de métodos de instancia y comprendes por qué un static no puede tocar el estado de un objeto que no tiene. Manejas this, los varargs, el ámbito de las variables y el shadowing, y tienes un catálogo de buenas prácticas para escribir métodos que otros puedan leer. Has visto la recursión con su caso base y su riesgo de StackOverflowError.
Y, sobre todo, has hecho la primera gran refactorización de BiblioTech: los cálculos de retraso, multa y gravedad han salido de main, se han convertido en métodos con nombre y después han mudado a la clase Prestamo, que es su dueña legítima. main ya no calcula: pregunta.
Queda un cabo suelto evidente. Tus objetos siguen naciendo vacíos: new Prestamo() produce un préstamo sin libro, sin empleado y con cero días, y hay que rellenarlo campo a campo confiando en no olvidar ninguno. En la lección siguiente, Constructores, cerrarás esa puerta: aprenderás a exigir los datos imprescindibles en el momento de la creación, a sobrecargar y encadenar constructores, a controlar el orden de inicialización y a validar los datos para que ningún objeto de BiblioTech pueda nacer en un estado inválido.
Curso de Programación en Java
Módulo 1: Introducción a Java
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
