1. Problemas del enfoque antiguo
«Classpath party»: cuando todos están en la misma cocina
Antes de la aparición de los módulos en Java (en la versión 9), todo el código de la aplicación, las bibliotecas y las dependencias se amontonaban en un único gran montón — el classpath. De facto no había fronteras entre bibliotecas: cualquier clase que entrara en el classpath podía ser encontrada y utilizada por cualquiera.
Surgían conflictos de nombres: dos bibliotecas contienen una clase con el mismo nombre completo, por ejemplo com.example.Util — se elegía «alguna» y te enterabas del problema por el comportamiento extraño en tiempo de ejecución.
Un clásico del género — el infierno de dependencias (dependency hell): una biblioteca requiere un logger versión 1.2, otra — 1.3, y en el classpath acaban ambas. La JVM no sabe cuál cargar y te topas con un enigmático NoSuchMethodError.
Además: aunque una clase esté declarada como public pero destinada a uso interno de la biblioteca, otro código puede usarla igualmente — y romper algo en el proceso. Los proyectos grandes se convertían en un campo minado y el mantenimiento — en una aventura.
2. ¿Qué es un módulo en Java?
Módulo: como una caja con agujeros
En Java 9 apareció el sistema de módulos (JPMS). Un módulo es una caja con clases y paquetes en la que tú abres «agujeros»: indicas explícitamente lo que expones hacia fuera (exports) y lo que escondes dentro. Y sí: incluso una clase public no es visible para otros módulos si su paquete no está exportado.
Definición formal
Un módulo es una unidad lógica de partición del código que agrupa paquetes y clases relacionados. Cada módulo declara explícitamente:
- qué exporta — lo que pone a disposición de otros módulos (exports);
- y qué importa — de qué otros módulos depende (requires).
Propiedades clave del módulo:
- Fronteras explícitas. Está claramente definido qué es accesible desde fuera y qué no.
- Dependencias explícitas. Un módulo no «ve» otro módulo sin un requires explícito.
- Aislamiento. Los detalles internos pueden ocultarse por completo del mundo exterior.
3. Ventajas de la modularidad
Mejor encapsulación
Apareció un nuevo nivel de visibilidad — a nivel de módulo. Incluso si una clase es public, pero su paquete no se exporta, solo es visible dentro del módulo. Las tripas de la biblioteca ya no «se cuelan» hacia fuera.
Descripción explícita de dependencias
Las dependencias necesarias se enumeran en module-info.java. El compilador y la IDE avisarán con antelación si olvidaste indicar un módulo del que dependes.
Mayor seguridad y fiabilidad
Restringir el acceso a las API internas dificulta la interferencia accidental o intencionada. Esto es crítico para bibliotecas grandes y módulos de plataforma.
Posibilidad de crear una JRE «recortada»
Con jlink puedes construir un runtime mínimo solo con los módulos necesarios. En la nube y en escenarios integrados (embedded) esto ahorra espacio en disco y memoria.
Bonus: arranque más rápido y menor tamaño
No se cargan todos los módulos, sino solo los realmente utilizados — las aplicaciones arrancan más rápido y consumen menos recursos.
4. Dónde se utilizan los módulos
En la biblioteca estándar de Java
Desde Java 9, la propia plataforma es modular. Ejemplos:
- java.base — módulo base (obligatorio para cualquier programa).
- java.sql — trabajo con bases de datos.
- java.xml — trabajo con XML.
Si no utilizas XML, el módulo java.xml ni siquiera formará parte de tu runtime.
En aplicaciones y bibliotecas grandes
Imprescindible para sistemas corporativos, donde decenas de equipos desarrollan partes de un monorepo. La modularidad reduce conflictos y facilita el mantenimiento.
En tus propios proyectos
Incluso en un proyecto personal, los módulos ayudan a entrenar el pensamiento arquitectónico y a mantener el código en orden, evitando el «espagueti».
5. Resumen breve de la sintaxis: module-info.java
Lo más importante: el archivo module-info.java
El archivo module-info.java se encuentra en la raíz de las fuentes del módulo y declara sus límites y dependencias.
module my.awesome.module {
exports com.example.api; // Paquete exportado
requires java.sql; // Dependencia de un módulo estándar
}
Palabras clave principales:
- module <nombre> — declara un módulo.
- exports <paquete> — hace que el paquete sea accesible para otros módulos.
- requires <módulo> — declara una dependencia de otro módulo.
Ejemplo mínimo:
module com.myproject.core {
exports com.myproject.core.api;
}
Ejemplo con dependencia:
module com.myproject.app {
requires com.myproject.core;
requires java.sql;
}
Capacidades adicionales (breve): para reflexión está opens, para servicios — uses y provides ... with ... (en detalle — en lecciones avanzadas).
6. Detalles útiles
Los módulos son como «pasaportes» para las clases. Antes, cualquier clase con el visado public podía «viajar» por la aplicación. Ahora también hace falta el «pasaporte» modular — la exportación del paquete (exports). Sin él, la clase se queda «en casa».
Dependency hell es un término real. Si has visto ClassNotFoundException: com.google.common.base.Strings, ya has estado allí. Los módulos pretenden ahuyentar a esos «demonios» con fronteras y dependencias estrictas.
Toda Java es ahora modular. Aunque no escribas tus propios módulos, la plataforma ya está dividida en decenas. Prueba el comando:
java --list-modules
Cómo afecta al desarrollo
Gestionas explícitamente la visibilidad del código, y los paquetes internos ya no pasan a estar disponibles para todos «por accidente». La IDE y el compilador detectan los errores antes: si olvidas declarar una dependencia, el proyecto no se construirá. El mantenimiento de sistemas grandes se simplifica: es más fácil entender quién depende de quién y qué se romperá con los cambios.
7. Errores típicos al pasar a módulos
Error n.º 1: olvidaste exportar el paquete, pero la clase es public. Declaraste la clase como public, pero no exportaste su paquete — otros módulos no podrán usarla. El compilador dirá: «El paquete no está exportado por el módulo».
Error n.º 2: no declaraste la dependencia mediante requires. Estás usando un tipo de otro módulo, pero olvidaste añadir requires en module-info.java. Resultado — error de compilación: «el módulo no puede encontrar la clase necesaria».
Error n.º 3: nombres de módulos duplicados. En un proyecto grande, dos módulos recibieron accidentalmente el mismo nombre. La JVM no lo tolera — renómbralos y sigue una convención de nombres uniforme.
Error n.º 4: olvidaste los módulos estándar. Por ejemplo, usas JDBC pero no añadiste requires java.sql;. En Java 8 «se veía de todos modos», pero en 9+ — no.
Error n.º 5: intentar utilizar una clase interna de otro módulo. Si un paquete no se exporta, incluso una clase public sigue siendo invisible desde fuera. Exporta el paquete o mueve el API a una capa «pública».
GO TO FULL VERSION