Skip to content

1. The JDK and build tools

Full example: code/java-for-csharp/l01 — every command below runs in CI on the three OSes.

.NET Java Role
.NET SDK JDK (Java Development Kit) compiler, runtime and tools in one install
CLR JVM (HotSpot) runs the compiled code, JIT-compiles it, collects garbage
IL in a .dll bytecode in .class files, zipped into a .jar the compiled output
Roslyn (csc) javac the compiler
dotnet run app.cs java Hello.java run a source file without a project
C# REPL JShell try an expression interactively
dotnet CLI + .csproj Maven (pom.xml) or Gradle (build.gradle.kts) build, test, package, resolve dependencies
NuGet Maven Central public package repository

The big difference: the JDK has no project system. dotnet build understands .csproj; javac only understands source files. Dependencies, tests and packaging come from a separate build tool, and Maven and Gradle are both common.

A new Java version ships every six months, and every fourth one is a long-term support (LTS) release: Java 21 in 2023, Java 25 in September 2025. It is the same rhythm as .NET’s yearly releases with an LTS every other year. The language version follows the JDK: there is no <LangVersion>, you pick a JDK and a target with --release.

OpenJDK is open source and several vendors build it: Eclipse Temurin, the Microsoft Build of OpenJDK, Amazon Corretto, Oracle’s builds. They run the same code; they differ in support terms and update schedules. This course’s CI uses Temurin; my machine runs Oracle’s OpenJDK 25 build.

Terminal window
winget install Microsoft.OpenJDK.25

Open a new terminal so that java is on the 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)

Since Java 25, a source file can be as short as a C# top-level program (JEP 512, compact source files):

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

java Hello.java compiles the file in memory and runs it; nothing is written to disk. A compact source file also imports the whole java.base module automatically, so List, Map or Files need no import.

The classic form, which you will see in every existing codebase, declares a class with a public static void main(String[] args) method:

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

Compiling and running are two steps with the JDK tools: javac writes .class files, and java runs a class found on the class path (-cp), the JVM’s equivalent of probing for assemblies.

Terminal window
javac -d out Greeter.java # writes out\Greeter.class
java -cp out Greeter Ada
Hello, Ada!

A compile error when running a source file stops before anything runs:

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 is built on convention: put the code where it expects it and a very short pom.xml is enough.

hello-maven/
├── pom.xml ← like the .csproj
└── src/
├── main/java/com/example/App.java
└── test/java/ ← tests live beside, not in another project
<?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 (the coordinates) identify the project, like a NuGet package ID and version. groupId is usually a reversed domain name.
  • -SNAPSHOT marks a version still in development, roughly a prerelease suffix.
  • maven.compiler.release is the Java version to compile for, like <TargetFramework>.
  • The package name com.example must match the folder com/example. In C#, namespaces and folders are independent; in Java, the compiler and class loader rely on the match.
$ 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 runs a lifecycle: each phase includes all the previous ones, and plugins do the work (compiler, surefire for tests, jar). The first build downloads those plugins into ~/.m2/repository, Maven’s equivalent of the NuGet global packages folder.

Task dotnet Maven Gradle
Compile dotnet build mvn compile gradle build (or classes)
Run the tests dotnet test mvn test gradle test
Produce the artefact dotnet publish mvn package gradle jar
Clean dotnet clean mvn clean gradle clean
Install locally for other projects local NuGet feed mvn install gradle publishToMavenLocal (with the maven-publish plugin)
Add a dependency dotnet add package edit pom.xml edit build.gradle.kts

The JAR holds the compiled classes but does not say which one to start:

$ 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

A JAR is not an .exe: unless its manifest names a Main-Class, java -jar has nothing to run. Exercise 2 fixes that.

gradle init generates a project with a wrapper: gradlew scripts that download the exact Gradle version the project needs, so contributors don’t install Gradle themselves (Maven has the same idea with the Maven Wrapper).

hello-gradle/
├── settings.gradle.kts ← like a .sln: lists the projects
├── gradle/libs.versions.toml ← version catalog: dependency versions in one place
├── gradlew, gradlew.bat ← the wrapper
└── app/
├── build.gradle.kts ← the build script, in Kotlin
└── src/main/java/com/example/App.java
// app/build.gradle.kts (generated by Gradle 9.7.1, comments removed)
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"
}
Terminal window
.\gradlew run
> Task :app:run
Hello World!
BUILD SUCCESSFUL in 2s

Gradle build scripts are code, which makes them powerful and harder to read; Maven’s XML is declarative and verbose. Spring’s getting-started guides offer both. This course uses Maven: its conventions carry over to almost every Java project you will open.

  • The JDK gives you java, javac, jar and jshell; the project model comes from Maven or Gradle.
  • java File.java runs a single file; since Java 25 it can be a compact void main() file.
  • Packages must match folders, and src/main/java / src/test/java is the layout every tool expects.
  • A JAR needs a Main-Class manifest entry before java -jar can run it.
  1. Without compiling it first, run Greeter.java with your name as argument. What does the JDK do that makes this possible?
Solution
$ java Greeter.java Grace
Hello, Grace!

The launcher’s source-file mode (JEP 330, extended to several files by JEP 458) compiles the file in memory, then runs the first top-level class’s main method with the remaining arguments. No .class file is written.

  1. Make java -jar target/hello-1.0-SNAPSHOT.jar work.
Solution

Configure the JAR plugin to write a Main-Class entry in the manifest:

<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!

The generated META-INF/MANIFEST.MF now contains Main-Class: com.example.App. If the application had dependencies, they would still be missing from the class path: that is what “fat JAR” plugins (or Spring Boot’s own packaging) solve.

  1. In JShell, compute -7 % 3 and Math.floorMod(-7, 3). Which one matches C#’s %?
Solution
jshell> -7 % 3
$1 ==> -1
jshell> Math.floorMod(-7, 3)
$2 ==> 2

% is a remainder that takes the sign of the dividend in both languages, so -7 % 3 is -1 in C# as well. Math.floorMod is the mathematical modulo, always in [0, 3) for a positive divisor.