Diario
Progreso
Sección titulada «Progreso»- Lección 1 — El JDK y las herramientas de build
- Lección 2 — Tipos, igualdad y operadores
- Lección 3 — Clases, records y enums
- Lección 4 — Genéricos y borrado de tipos
- Lección 5 — Excepciones,
nullyOptional - Lección 6 — Lambdas e interfaces funcionales
- Lección 7 — Colecciones y Streams
- Lección 8 — Pattern matching
- Lección 9 — Concurrencia e hilos virtuales
- Lección 10 — Maven y Gradle a fondo
- Lección 11 — Pruebas
- Lección 12 — La biblioteca estándar del día a día
- Lección 13 — La JVM en tiempo de ejecución
- Lección 14 — Anotaciones, reflexión y módulos
2026-09-13 — Lección 12
Sección titulada «2026-09-13 — Lección 12»- Los ejemplos de la lección 12, sus equivalentes en C# y tres fragmentos que no compilan se ejecutan en CI en Windows, Linux y macOS; el proyecto principal tiene ahora 123 pruebas. Todas las llamadas HTTP van a un servidor local, el
HttpServerdel JDK en el lado Java yHttpListeneren el lado C#, así que la salida no depende de la red. - El proyecto C# tenía
InvariantGlobalizationactivado. La lección 12 necesita las zonas horarias IANA y culturas con nombre, así que ICU está ahora activado, yProgram.csfija la cultura actual en la cultura invariable. Las salidas de las lecciones 2 a 9 no cambiaron.
Sorpresas viniendo de C#:
- El
HttpClientde Java no sigue las redirecciones ni tiene timeout de petición salvo que se configuren. Al portar código .NET se obtiene un 302 con el cuerpo vacío, o una llamada que puede esperar para siempre. - Para una hora local que ocurre dos veces,
ZonedDateTimeelige el desfase del horario de verano yTimeZoneInfo.GetUtcOffsetel estándar: una hora de diferencia. YYYYen un patrón de fecha de Java imprimió 2027 para el 31 de diciembre de 2026 conLocale.US, y 2026 conLocale.FRANCE.Files.readStringlanza una excepción ante UTF-8 inválido, dondeFile.ReadAllTextsustituye el byte.- Un primer programa de prueba imprimió
héllo, leído de un archivo UTF-8, comoh�lloen Git Bash en Windows, aunque el archivo era correcto. JEP 400 dejaSystem.outcon la codificación de la consola; por verificar: qué codificación eligió Java en ese terminal.
Lo que hice mal al principio:
- El primer programa de prueba en C# se ejecutó con
InvariantGlobalizationactivado.FindSystemTimeZoneById("Europe/Paris")lanzóTimeZoneNotFoundExceptionen Windows, ynew CultureInfo("fr-FR")lanzó «Only the invariant culture is supported in globalization-invariant mode.» - El primer programa de prueba en Java formateaba los números con la configuración regional por defecto, que es
en_CAen mi máquina y otra distinta en los runners de CI. Los ejemplos pasan ahora unLocaleallí donde la salida depende de uno.
2026-09-13 — Lección 11
Sección titulada «2026-09-13 — Lección 11»- La lección 11 tiene su propio proyecto Maven,
l11, con 24 pruebas. Los mensajes de fallo que cita la lección los produceFailureMessagesTesty se comparan con archivos de texto, así que una actualización de biblioteca que cambie un mensaje hace fallar el build.l11/check.shcubre el resto: los nombres de las pruebas, la advertencia de Mockito sin el agente, el fallo de BlockHound sin su opción de la JVM y los errores de NullAway. - La pregunta abierta de la lección 5 sobre JSpecify y NullAway tiene respuesta en esta lección: NullAway 0.14.1 sobre Error Prone 2.50.0 funciona en JDK 25.
Sorpresas viniendo de C#:
- El conflicto de la lección 10 apareció solo en este proyecto. Mockito 5.23.0 pide Byte Buddy 1.17.7, AssertJ 3.27.7 pide la 1.18.3, y la regla de la definición más cercana de Maven puso Byte Buddy 1.18.3 junto a
byte-buddy-agent1.17.7. Funciona de casualidad, porque el camino de AssertJ es más corto y trae la versión más nueva. - JUnit 6 entrecomilla los valores CSV en los nombres de las pruebas parametrizadas, números incluidos, porque los valores siguen siendo cadenas cuando se construye el nombre.
- Los informes de Surefire ignoran
@DisplayNamesalvo que se configure una opción del reporter, y su salida de consola atribuyó las nueve pruebas dePriceCalculatorTesta su clase anidada. - Con Mockito cargado como agente, la JVM sigue imprimiendo «Sharing is only supported for boot loader classes because bootstrap classpath has been appended». La lección no lo cita; por verificar: si
-Xshare:offes la forma correcta de silenciarlo. - BlockHound 1.0.17 necesita
-XX:+AllowRedefinitionToAddDeleteMethods, una opción de la JVM obsoleta desde JDK 13, e informa deThread.sleepcomojava.lang.Thread.sleepNanos0, el método privado del JDK que instrumenta.
Lo que hice mal al principio:
- La primera ejecución de NullAway falló con «An unknown compilation problem occurred». El error real, un
IllegalAccessErrorsobre la API interna de javac, solo apareció conmvn -e. Error Prone necesita.mvn/jvm.configcon líneas--add-exports. - Primero instalé BlockHound con
BlockHound.install(), como hace su README. En JDK 25 falló con «Could not self-attach to current VM using external process», y con-Djdk.attach.allowAttachSelf=truefalló con «Agent JAR loaded but agent failed to initialize». Solo funcionó-javaagentjunto con la opción. - Mi primer borrador hacía corresponder
MockBehavior.Strictde Moq con los stubs estrictos de Mockito. Comprueban cosas distintas: Moq falla ante una llamada sin setup, Mockito ante un stub que nunca se usa. - Primero escribí que C# señala el unboxing de un nullable con la advertencia CS8629. Convertir un
int?enintsin cast directamente no compila (CS0266); CS8629 es para.Valuesobre unint?posiblemente nulo.
2026-09-13 — Lección 10
Sección titulada «2026-09-13 — Lección 10»- La lección 10 se prueba de otra manera:
l10/check.shejecuta los builds de Maven, Gradle y NuGet, y la CI compara 14 archivos de salida con los que cita la lección. El script tarda unos tres minutos en mi máquina, casi todo en el arranque de Maven y Gradle. - Maven 4: la página de descargas ofrece la 4.0.0-rc-6 como versión preliminar (preview) y la 3.9.x como versión actual, lo que por ahora responde a la pregunta abierta de más abajo.
Sorpresas viniendo de C#:
- Maven puso Commons Lang 3.14.0 en el class path cuando una biblioteca del build necesitaba la 3.20.0, sin ninguna advertencia, y el programa solo falló en tiempo de ejecución con
NoClassDefFoundError. Hace falta-Dverboseincluso para ver que se había pedido otra versión. - Un
implementation("…:3.14.0")directo en Gradle no degrada nada: es un candidato más, y la 3.20.0 sigue ganando. Solostrictlylo fuerza. - NuGet fue el más estricto de los tres: su advertencia de degradación, NU1605, es un error por defecto.
failOnVersionConflict()dejó quecompileJavafuncionara, porque el conflicto solo existe en el class path de ejecución cuando la otra versión llega a través de una dependenciaimplementation.
Lo que hice mal al principio:
- Mi primer build de Gradle compartía sus ajustes con
subprojects { }en el script raíz. La documentación de Gradle lo llama «an improper way to share build logic»; el ejemplo usa ahora un plugin de convenciones enbuildSrc. - La primera ejecución de
mvn installpuso los módulos del ejemplo en mi repositorio local (~/.m2). El script compila ahora en el reactor conpackage, que no necesita instalación, y borré las copias instaladas. dotnet list packageimprime mensajes de restauración cuya redacción depende de lo que ya está restaurado, así que el script de comprobación los filtra.- Mi tabla de resumen decía al principio que Maven no tenía equivalente de
PrivateAssets="all", mientras que la tabla de ámbitos, dos secciones más abajo, lo hacía corresponder con<optional>true</optional>. La tabla de resumen coincide ahora.
2026-09-13 — Lección 9
Sección titulada «2026-09-13 — Lección 9»- El proyecto tiene ahora 113 pruebas: 63 fragmentos rechazados, 26 salidas de ejemplos y 24 soluciones de ejercicios. Los ejemplos de la lección 9 arrancan más de 14.000 hilos virtuales (10.000 tareas que duermen y 4.600 tareas cortas), y el
ExamplesTestcompleto sigue ejecutándose en menos de 3 segundos en mi máquina. - Los ejemplos concurrentes necesitan una salida determinista para poder probarse. Cada uno imprime solo resultados que no dependen de la planificación: totales, listas recolectadas y el orden que imponen los latches.
Sorpresas viniendo de C#:
CompletableFuture.cancel(true)no interrumpe la tarea: el Javadoc dice que el argumento «has no effect in this implementation». El future indica que está cancelado mientras su tarea sigue ejecutándose.- Un
value++con carrera desde 1.000 tareas de 1.000 incrementos imprimió 76.422, luego 855.000 y luego 803.000. La primera ejecución perdió más del 90 % de las actualizaciones, probablemente antes de que el JIT compilara el bucle: por verificar. - Sincronizar sobre un
Integercompila. La única señal de problema es una advertencia de lint, en una categoría que Java 25 llama[identity]. ScopedValueno llega a las tareas de un executor normal; solo lo heredan los hilos bifurcados porStructuredTaskScope, que sigue en preview.AsyncLocalfluye a todas partes.
Lo que hice mal al principio:
- El primer ejemplo
Futurescancelaba una tarea de cinco segundos conCompletableFuture.cancel(true), y el ejemplo tardaba cinco segundos: elclose()del executor esperaba a la tarea que nunca se interrumpió. Eso pasó a formar parte de la lección, con una tarea de un segundo. - La línea «interrupted» del worker y la línea «cancelled» de
mainlas imprimían dos hilos distintos, así que su orden era una carrera. Un segundo latch lo arregla. - En el lado C#, primero leí un
ThreadLocaldentro deTask.Rundespués de unawait. La continuación se ejecuta en un hilo del pool, yTask.Runpodía reutilizar ese mismo hilo, así que la salida no estaba garantizada. La comparación usa ahora unThreaddedicado. - Primero comprobé qué hilo ejecutaba un stream paralelo de un solo elemento. Eso dependía de un detalle de implementación; el ejemplo registra ahora todos los hilos que participan en uno grande.
2026-09-13 — Lecciones 5 a 8
Sección titulada «2026-09-13 — Lecciones 5 a 8»- El lado C# cubre ahora todas las lecciones:
dotnet run -- l05al08imprimen el comportamiento de .NET con el que se compara cada lección. - El proyecto tiene 98 pruebas: 56 fragmentos rechazados, 21 salidas de ejemplos y 21 soluciones de ejercicios.
Sorpresas viniendo de C#:
- Cuando el cierre de un recurso falla después de que haya fallado el cuerpo, Java conserva la excepción del cuerpo y le adjunta la otra como suprimida (suppressed). El
usingde C# pierde la original: el lado C# imprimedispose failed: dben lugar dequery failed on db. throw econserva la traza de pila en Java, porque la traza se captura al crear la excepción. El analizador de C# advierte sobre la misma línea (CA2200).- Dos evaluaciones de
display::onPriceproducen dos objetos distintos, así que eliminar un listener con una referencia a método nueva no hace nada, sin avisar. Los delegados de C# se comparan como iguales. Map.of(…)itera en un orden distinto de un arranque de la JVM a otro: cuatro ejecuciones de la misma línea dieron dos órdenes.- Una expresión
switchsobre una interfaz sellada que omite un subtipo es un error de compilación, mientras que C# solo advierte (CS8509). - Los patrones primitivos (
case byte bsobre unint) siguen en preview en Java 25; javac lo dice y sugiere--enable-preview.
Lo que hice mal al principio:
- La primera captura de la salida esperada del lado C# para la lección 7 incluía una advertencia del analizador, porque
dotnet runrecompiló el proyecto e imprimió la advertencia en la salida estándar. Los archivos esperados se generan ahora condotnet run --no-build, como en la CI. IntSummaryStatistics.toString()yprintf("%.2f")formatean los números con la configuración regional por defecto, así que la salida depende de la máquina:2.200000aquí,2,200000con una configuración regional francesa. La lección 7 imprime ella misma los campos y la lección 8 pasaLocale.ROOT. Los ejemplosEnumsyShapesde la lección 3 siguen usando la configuración regional por defecto: por verificar en una máquina con configuración regional francesa.- Recordaba el error de C# para un brazo de switch dominado como CS8120. Ese código corresponde a las sentencias
switch; una expresión switch informa de CS8510. reduce(UnaryOperator.identity(), (f, g) -> f.andThen(g))no compila:andThendevuelve unaFunction, no unUnaryOperator. El ejercicio 1 de la lección 6 explica por qué.
2026-09-13 — Lecciones 1 a 4
Sección titulada «2026-09-13 — Lecciones 1 a 4»- Herramientas en mi máquina: OpenJDK
25+36, Maven 3.9.16, Gradle 9.7.1 y el SDK de .NET 10 para el lado C#. La CI usa Eclipse Temurin 25 en Windows, Ubuntu y macOS. - El código del curso está en
code/java-for-csharp:- una prueba JUnit compara la salida de cada ejemplo con la lección;
- cada fragmento rechazado se compila mediante la API
javax.toolsy debe producir la clave de diagnóstico de javac que cita la lección (por ejemplocompiler.err.not.exhaustive) — el equivalente Java de los códigosE0xxxde Rust; - las soluciones de los ejercicios son pruebas;
- un pequeño programa .NET 10 imprime el lado C# de cada comparación.
Sorpresas viniendo de C#:
Integer a = 128, b = 128; a == besfalse, pero con 127 estrue. El rango de la caché, de −128 a 127, no es un detalle de implementación: lo exige la JLS.java -jarsobre un JAR de Maven recién construido falla conno main manifest attribute. Nada endotnet buildte prepara para un artefacto que no conoce su propio punto de entrada.- Un
enumde C# acepta(Size)42; un enum de Java no puede contener un valor que no declara. Los enums de Java se parecen mucho más a un conjunto sellado de objetos singleton. - Los mensajes de error de javac a veces son indirectos: crear una clase interna desde un método estático da
non-static variable this cannot be referenced from a static context.
Lo que hice mal al principio:
- En un comentario de código describí
final varcomo «una variable local readonly de C#». C# no tiene variables locales readonly. El comentario ahora lo dice. - Esperaba que
javac -XDrawDiagnosticsy la API del compilador informaran de la misma clave de diagnóstico. No siempre es así: al añadir a unaList<? extends Number>, la línea de comandos informa decompiler.err.cant.apply.symbols, mientras que la API informa de la versión simplificadacompiler.err.prob.found.req. Las pruebas usan la API, así que las lecciones citan lo que ve la API. - Varias lecciones mostraban al principio un extracto de un fragmento rechazado junto a un mensaje de error cuyo número de línea se refería al archivo completo. Ahora las lecciones muestran el archivo completo.
- El signo
‰de un ejemplo aparecía como�en la consola de Windows. El ejemplo ahora escribe «per mille».
Casos prácticos en mis repositorios
Sección titulada «Casos prácticos en mis repositorios»Cada caso aplica una lección a un repositorio público, en un commit fijado.
GuitarAlchemist/ga — el build del plugin de JetBrains (lección 1)
Sección titulada «GuitarAlchemist/ga — el build del plugin de JetBrains (lección 1)»jetbrains-plugin/ en ga@5560b883 es un proyecto Gradle con Kotlin DSL (el propio plugin está escrito en Kotlin). Lo copié e intenté compilarlo en esta máquina.
-
El wrapper está incompleto. Solo está versionado
gradle/wrapper/gradle-wrapper.properties. Faltangradlew,gradlew.batygradle-wrapper.jar, así que./gradlewno existe y cada colaborador necesita tener instalado un Gradle compatible.gradle wrapperregenera los tres archivos, que deberían versionarse. -
Gradle 8.5 no puede ejecutarse sobre JDK 25. Con el Gradle 8.5 fijado y JDK 25,
gradle helpfalla con un mensaje de una sola palabra:* What went wrong:25La traza de pila muestra una
java.lang.IllegalArgumentException: 25lanzada mientras Gradle compila el script de build en Kotlin: el compilador de Kotlin integrado en esa versión de Gradle no conoce Java 25. La solución es o bien un JDK que Gradle 8.5 admita, o bien un Gradle más reciente; la matriz de compatibilidad de Gradle indica qué versión de Gradle funciona con qué Java. -
Actualizar solo Gradle no basta. Con Gradle 9.7.1 el build avanza más y falla en el antiguo plugin
org.jetbrains.intellij1.16.1:class org.jetbrains.intellij.MemoizedProvider overrides final method org.gradle.api.internal.provider.AbstractMinimalProvider.toString()Ljava/lang/String;Ese plugin fue sustituido por el IntelliJ Platform Gradle Plugin 2.x, que usa un DSL distinto. Migrar es un cambio real, no una simple subida de versión.
-
La carpeta de caché de Gradle está versionada. Doce archivos bajo
jetbrains-plugin/.gradle/(archivos de bloqueo, sumas de comprobación,last-build.bin) están en el repositorio, el equivalente en Gradle a versionarobj/. Su sitio es el.gitignore. -
Destino de Java sin toolchain. El script define
sourceCompatibility = "17"en cada tareaJavaCompileen lugar de un bloquejava { toolchain { … } }, así que el JDK que se usa depende de la máquina que ejecuta Gradle — justo la variabilidad que describe la nota sobre toolchains de la lección 1.
Siguiente paso, por verificar: generar el wrapper, migrar al plugin 2.x y comprobar que el plugin sigue cargando en una versión actual de IntelliJ IDEA.
spareilleux/learn — el ejemplo Spring Boot del curso de WSL (lección 1, ejercicio 2)
Sección titulada «spareilleux/learn — el ejemplo Spring Boot del curso de WSL (lección 1, ejercicio 2)»code/wsl-containers/java-reactor-api no tiene ninguna configuración de maven-jar-plugin y, sin embargo, java -jar target/app.jar funciona. Compilarlo muestra por qué:
Main-Class: org.springframework.boot.loader.launch.JarLauncherStart-Class: dev.learn.reactorapi.JavaReactorApiApplicationEl spring-boot-maven-plugin reempaqueta el JAR: la Main-Class del manifiesto es el lanzador de Spring Boot, que lee Start-Class y carga los 62 JAR de dependencias anidados bajo BOOT-INF/lib/ (35 MB en total). Responde a las dos mitades del ejercicio 2 — el punto de entrada que falta y las dependencias que faltan. El elemento <parent> (spring-boot-starter-parent) explica también por qué el POM no indica versiones de plugins ni de dependencias: hace el papel de Directory.Packages.props y de los valores por defecto del SDK. La lección 10 vuelve sobre ello.
Preguntas abiertas
Sección titulada «Preguntas abiertas»- ¿Merece ya la pena enseñar Maven 4? El 2026-09-13 sigue siendo una versión candidata (4.0.0-rc-6); volver a mirarlo cuando la 4.0.0 sea definitiva.
- ¿Cómo se comparan las inspecciones de IntelliJ IDEA con
-Xlint:allde javac para las trampas de las lecciones 2 y 3?