1. The JDK and build tools
Full example: code/java-for-csharp/l01 — every command below runs in CI on the three OSes.
The pieces
Section titled “The pieces”| .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.
Versions and distributions
Section titled “Versions and distributions”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.
Installing a JDK
Section titled “Installing a JDK”winget install Microsoft.OpenJDK.25Open a new terminal so that java is on the PATH.
sudo apt install openjdk-25-jdk # Ubuntu 24.04 and laterOr, for any distribution and to switch between versions, SDKMAN!:
curl -s "https://get.sdkman.io" | bashsdk install java 25.0.4-tembrew install openjdk@25sudo ln -sfn "$(brew --prefix)/opt/openjdk@25/libexec/openjdk.jdk" /Library/Java/JavaVirtualMachines/openjdk-25.jdkThe formula is keg-only: the second line registers it so that /usr/bin/java finds it. SDKMAN! (see the Linux tab) works on macOS too.
$ java --versionopenjdk 25 2025-09-16OpenJDK Runtime Environment (build 25+36-3489)OpenJDK 64-Bit Server VM (build 25+36-3489, mixed mode, sharing)Running a single file
Section titled “Running a single file”Since Java 25, a source file can be as short as a C# top-level program (JEP 512, compact source files):
void main() { IO.println("Hello, world!");}$ java Hello.javaHello, 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:
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.
javac -d out Greeter.java # writes out\Greeter.classjava -cp out Greeter Adajavac -d out Greeter.java # writes out/Greeter.classjava -cp out Greeter Adajavac -d out Greeter.java # writes out/Greeter.classjava -cp out Greeter AdaHello, Ada!A compile error when running a source file stops before anything runs:
void main() { int count = "three";}$ java Oops.javaOops.java:2: error: incompatible types: String cannot be converted to int int count = "three"; ^1 errorerror: compilation failedA Maven project
Section titled “A Maven project”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.groupIdis usually a reversed domain name.-SNAPSHOTmarks a version still in development, roughly a prerelease suffix.maven.compiler.releaseis the Java version to compile for, like<TargetFramework>.- The package name
com.examplemust match the foldercom/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 SUCCESSmvn 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.AppHello from Maven!$ java -jar target/hello-1.0-SNAPSHOT.jarno main manifest attribute, in target\hello-1.0-SNAPSHOT.jarA JAR is not an .exe: unless its manifest names a Main-Class, java -jar has nothing to run. Exercise 2 fixes that.
A Gradle project
Section titled “A Gradle project”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"}.\gradlew run./gradlew run./gradlew run> Task :app:runHello World!
BUILD SUCCESSFUL in 2sGradle 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.
Key takeaways
Section titled “Key takeaways”- The JDK gives you
java,javac,jarandjshell; the project model comes from Maven or Gradle. java File.javaruns a single file; since Java 25 it can be a compactvoid main()file.- Packages must match folders, and
src/main/java/src/test/javais the layout every tool expects. - A JAR needs a
Main-Classmanifest entry beforejava -jarcan run it.
Exercises
Section titled “Exercises”- Without compiling it first, run
Greeter.javawith your name as argument. What does the JDK do that makes this possible?
Solution
$ java Greeter.java GraceHello, 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.
- Make
java -jar target/hello-1.0-SNAPSHOT.jarwork.
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.jarHello 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.
- In JShell, compute
-7 % 3andMath.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.