Ir al contenido

1. El JDK y las herramientas de build

Ejemplo completo: code/java-for-csharp/l01 — cada comando de abajo se ejecuta en CI en los tres sistemas operativos.

.NET Java Función
SDK de .NET JDK (Java Development Kit) compilador, runtime y herramientas en una sola instalación
CLR JVM (HotSpot) ejecuta el código compilado, lo compila con JIT, recolecta la basura
IL en una .dll bytecode en archivos .class, comprimidos en un .jar el resultado de la compilación
Roslyn (csc) javac el compilador
dotnet run app.cs java Hello.java ejecutar un archivo fuente sin proyecto
REPL de C# JShell probar una expresión de forma interactiva
CLI dotnet + .csproj Maven (pom.xml) o Gradle (build.gradle.kts) compilar, probar, empaquetar, resolver dependencias
NuGet Maven Central repositorio público de paquetes

La gran diferencia: el JDK no tiene sistema de proyectos. dotnet build entiende el .csproj; javac solo entiende archivos fuente. Las dependencias, las pruebas y el empaquetado vienen de una herramienta de build aparte, y tanto Maven como Gradle son habituales.

Cada seis meses sale una nueva versión de Java, y una de cada cuatro es una versión con soporte a largo plazo (LTS): Java 21 en 2023, Java 25 en septiembre de 2025. Es el mismo ritmo que las versiones anuales de .NET, con una LTS cada dos años. La versión del lenguaje sigue a la del JDK: no hay <LangVersion>, eliges un JDK y un destino con --release.

OpenJDK es de código abierto y varios proveedores lo compilan: Eclipse Temurin, el Microsoft Build of OpenJDK, Amazon Corretto, los builds de Oracle. Ejecutan el mismo código; se diferencian en sus condiciones de soporte y su calendario de actualizaciones. La CI de este curso usa Temurin; mi máquina ejecuta el build OpenJDK 25 de Oracle.

Ventana de terminal
winget install Microsoft.OpenJDK.25

Abre una terminal nueva para que java esté en el PATH.

$ java --version
openjdk 25 2025-09-16
OpenJDK Runtime Environment (build 25+36-3489)
OpenJDK 64-Bit Server VM (build 25+36-3489, mixed mode, sharing)

Desde Java 25, un archivo fuente puede ser tan corto como un programa C# con instrucciones de nivel superior (JEP 512, archivos fuente compactos):

Hello.java
void main() {
IO.println("Hello, world!");
}
$ java Hello.java
Hello, world!

java Hello.java compila el archivo en memoria y lo ejecuta; no se escribe nada en disco. Un archivo fuente compacto también importa automáticamente todo el módulo java.base, así que List, Map o Files no necesitan ningún import.

La forma clásica, que verás en cualquier base de código existente, declara una clase con un método public static void main(String[] args):

Greeter.java
public class Greeter {
public static void main(String[] args) {
System.out.println("Hello, " + (args.length > 0 ? args[0] : "world") + "!");
}
}

Con las herramientas del JDK, compilar y ejecutar son dos pasos: javac escribe archivos .class, y java ejecuta una clase que encuentra en el class path (-cp), el equivalente en la JVM a la búsqueda de ensamblados.

Ventana de terminal
javac -d out Greeter.java # escribe out\Greeter.class
java -cp out Greeter Ada
Hello, Ada!

Al ejecutar un archivo fuente, un error de compilación lo detiene todo antes de que se ejecute nada:

Oops.java
void main() {
int count = "three";
}
$ java Oops.java
Oops.java:2: error: incompatible types: String cannot be converted to int
int count = "three";
^
1 error
error: compilation failed

Maven se basa en convenciones: pon el código donde lo espera y basta con un pom.xml muy corto.

hello-maven/
├── pom.xml ← como el .csproj
└── src/
├── main/java/com/example/App.java
└── test/java/ ← las pruebas van al lado, no en otro proyecto
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>hello</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
  • groupId:artifactId:version (las coordenadas) identifican el proyecto, como el ID y la versión de un paquete NuGet. groupId suele ser un nombre de dominio invertido.
  • -SNAPSHOT marca una versión todavía en desarrollo, más o menos un sufijo de versión preliminar.
  • maven.compiler.release es la versión de Java para la que se compila, como <TargetFramework>.
  • El nombre de paquete com.example debe coincidir con la carpeta com/example. En C#, los espacios de nombres y las carpetas son independientes; en Java, el compilador y el cargador de clases dependen de esa coincidencia.
$ mvn package
...
[INFO] --- compiler:3.15.0:compile (default-compile) @ hello ---
[INFO] Compiling 1 source file with javac [debug release 25] to target\classes
...
[INFO] --- surefire:3.5.4:test (default-test) @ hello ---
[INFO] No tests to run.
[INFO]
[INFO] --- jar:3.5.0:jar (default-jar) @ hello ---
[INFO] Building jar: …\hello\target\hello-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

mvn package ejecuta un ciclo de vida: cada fase incluye todas las anteriores, y son los plugins los que hacen el trabajo (compiler, surefire para las pruebas, jar). El primer build descarga esos plugins en ~/.m2/repository, el equivalente en Maven a la carpeta global de paquetes de NuGet.

Tarea dotnet Maven Gradle
Compilar dotnet build mvn compile gradle build (o classes)
Ejecutar las pruebas dotnet test mvn test gradle test
Producir el artefacto dotnet publish mvn package gradle jar
Limpiar dotnet clean mvn clean gradle clean
Instalar localmente para otros proyectos feed local de NuGet mvn install gradle publishToMavenLocal (con el plugin maven-publish)
Añadir una dependencia dotnet add package editar pom.xml editar build.gradle.kts

El JAR contiene las clases compiladas, pero no dice cuál arrancar:

$ java -cp target/classes com.example.App
Hello from Maven!
$ java -jar target/hello-1.0-SNAPSHOT.jar
no main manifest attribute, in target\hello-1.0-SNAPSHOT.jar

Un JAR no es un .exe: mientras su manifiesto no indique una Main-Class, java -jar no tiene nada que ejecutar. El ejercicio 2 lo corrige.

gradle init genera un proyecto con un wrapper: scripts gradlew que descargan la versión exacta de Gradle que necesita el proyecto, para que los colaboradores no tengan que instalar Gradle ellos mismos (Maven tiene la misma idea con el Maven Wrapper).

hello-gradle/
├── settings.gradle.kts ← como un .sln: enumera los proyectos
├── gradle/libs.versions.toml ← catálogo de versiones: las versiones de las dependencias en un solo lugar
├── gradlew, gradlew.bat ← el wrapper
└── app/
├── build.gradle.kts ← el script de build, en Kotlin
└── src/main/java/com/example/App.java
// app/build.gradle.kts (generado por Gradle 9.7.1, sin los comentarios)
plugins {
application
}
repositories {
mavenCentral()
}
dependencies {
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation(libs.guava)
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
application {
mainClass = "com.example.App"
}
Ventana de terminal
.\gradlew run
> Task :app:run
Hello World!
BUILD SUCCESSFUL in 2s

Los scripts de build de Gradle son código, lo que los hace potentes y más difíciles de leer; el XML de Maven es declarativo y verboso. Las guías de inicio de Spring ofrecen ambos. Este curso usa Maven: sus convenciones se aplican a casi cualquier proyecto Java que abras.

  • El JDK te da java, javac, jar y jshell; el modelo de proyecto viene de Maven o de Gradle.
  • java File.java ejecuta un solo archivo; desde Java 25 puede ser un archivo compacto con void main().
  • Los paquetes deben coincidir con las carpetas, y src/main/java / src/test/java es la estructura que esperan todas las herramientas.
  • Un JAR necesita una entrada Main-Class en el manifiesto para que java -jar pueda ejecutarlo.
  1. Sin compilarlo antes, ejecuta Greeter.java con tu nombre como argumento. ¿Qué hace el JDK para que esto sea posible?
Solución
$ java Greeter.java Grace
Hello, Grace!

El modo de archivo fuente del lanzador (JEP 330, ampliado a varios archivos por la JEP 458) compila el archivo en memoria y luego ejecuta el método main de la primera clase de nivel superior con los argumentos restantes. No se escribe ningún archivo .class.

  1. Haz que funcione java -jar target/hello-1.0-SNAPSHOT.jar.
Solución

Configura el plugin JAR para que escriba una entrada Main-Class en el manifiesto:

<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.example.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
$ mvn -q package
$ java -jar target/hello-1.0-SNAPSHOT.jar
Hello from Maven!

El META-INF/MANIFEST.MF generado contiene ahora Main-Class: com.example.App. Si la aplicación tuviera dependencias, seguirían faltando en el class path: eso es lo que resuelven los plugins de «fat JAR» (o el empaquetado propio de Spring Boot).

  1. En JShell, calcula -7 % 3 y Math.floorMod(-7, 3). ¿Cuál coincide con el % de C#?
Solución
jshell> -7 % 3
$1 ==> -1
jshell> Math.floorMod(-7, 3)
$2 ==> 2

En los dos lenguajes, % es un resto que toma el signo del dividendo, así que -7 % 3 también vale -1 en C#. Math.floorMod es el módulo matemático, siempre en [0, 3) para un divisor positivo.