A two-day course on modern Java, based on Java 25 (LTS).
No Java experience needed. Programming experience in any language helps.
Everything here runs on a plain JDK 25 install β no build tool, no framework, no boilerplate.
Why Java 25?
Java 25 is the current LTS release (September 2025), the successor to Java 21.
It adds compact source files, java.lang.IO, module imports, flexible constructor bodies
and scoped values.
Get a JDK 25 running, then meet JShell β the fastest way to try Java out.
Subsections of Course Introduction
Setup: JDK 25 & Editor
Install the JDK
You need the JDK (compiler + tools), not just a JRE. Pick any OpenJDK build:
Eclipse Temurin, Amazon Corretto or Oracle OpenJDK.
# macOSbrew install --cask temurin@25
# Linuxsudo apt install openjdk-25-jdk
# Windowswinget install EclipseAdoptium.Temurin.25.JDK
# any platform, several versions side by sidesdk install java 25-tem # sdkman.io
Verify:
java --version # openjdk 25 β¦javac --version
No javac?
You installed a JRE, or the JDK’s bin folder is not on your PATH.
Editor
VS Code β install Extension Pack for Java
IntelliJ IDEA Community β set Project SDK and language level to 25
Your first program
Hello.java:
voidmain(){IO.println("Hello, Java 25!");}
java Hello.java # runs directly, no compile step
New in Java 25
Compact source files and instance main methods (JEP 512) drop the class shell,
static, and String[] args. The always-available java.lang.IO class provides
println, print and readln. The classic form still works β see
chapter 7.
Compiling by hand
javac Hello.java # produces Hello.classjava Hello # no file extension!
β Exercises
Install JDK 25 and print java --version.
Write Profile.java printing three lines about yourself.
Compile it with javac. What file appears, and how big is it?
Remove a semicolon on purpose and read the compiler error: file, line, column.
The REPL: JShell
JShell is a ReadβEvalβPrint Loop shipped with the JDK. Use it whenever you
want to try something out without creating a file.
Shift+Tab, then v turns the expression you just typed into a variable declaration.
β Exercises
Compute how many seconds are in a year.
Declare name and print a greeting with it.
Define isEven(int) and test it with five values.
Use Tab completion to list String methods starting with str.
/save your session, /reset, then /open it again.
Day 1: Intro to Java
The language basics: types, methods, collections, logic, control flow, files and a first
HTTP call.
Subsections of Day 1: Intro to Java
Why Java?
Java compiles to bytecode, which runs on the JVM β the same .class file works on
Linux, macOS and Windows. A new release ships every six months, an LTS release every two years.
Version
Year
Highlights
8
2014
lambdas, streams, Optional, java.time
11 LTS
2018
HTTP client
17 LTS
2021
records, sealed classes, text blocks, switch expressions
int[]numbers=newint[5];String[]colors={"red","green","blue"};colors.length// 3 β a field, not a methodcolors[1]// "green"colors[3]// ArrayIndexOutOfBoundsExceptionArrays.sort(numbers);Arrays.toString(numbers);
List<String> means “list of strings” β the compiler keeps everything else out. On the right
side the diamond <> is enough: List<String> names = new ArrayList<>();
Arrow form β no fallthrough, and it produces a value:
varlabel=switch(day){case1,7->"weekend";case2,3,4,5,6->"weekday";default->thrownewIllegalArgumentException("bad day: "+day);};vartext=switch(grade){case1->"excellent";default->{vars="grade "+grade;yields.toUpperCase();// yield inside a block}};
Pattern matching
Stringdescribe(Objecto){returnswitch(o){caseIntegeriwheni>100->"big number: "+i;caseIntegeri->"number: "+i;caseStrings->"text of %d chars".formatted(s.length());caseint[]a->"array of "+a.length;casenull->"nothing";default->"unknown";};}
when adds a guard. Without case null, a null value throws.
for(varname:names){β¦}// for-each: the defaultfor(vari=0;i<5;i++){β¦}// when you need the indexfor(vari=10;i>0;i-=2){β¦}while(rest>1){rest/=2;}do{answer=IO.readln("again? ");}// runs at least oncewhile(!answer.equals("n"));
break and continue
for(varn:numbers){if(n%2!=0)continue;// skipif(n>15)break;// leave the loopIO.println(n);}
Labels exist for nested loops, but extracting a method and return is usually cleaner:
outer:for(β¦){for(β¦){if(β¦)breakouter;}}
Putting it together
voidmain(){varsecret=newjava.util.Random().nextInt(1,101);vartries=0;while(true){varinput=IO.readln("Guess 1-100: ");intguess;try{guess=Integer.parseInt(input.strip());}catch(NumberFormatExceptione){IO.println("Not a number.");continue;}tries++;switch(Integer.compare(guess,secret)){case-1->IO.println("too low");case1->IO.println("too high");default->{IO.println("Got it in %d tries!".formatted(tries));return;}}}}
β Exercises
Print a multiplication table from 1 to 10, aligned with "%4d".
FizzBuzz for 1β100 β once with if/else, once with a switch expression.
Print all primes up to n.
Write String classify(Object o) handling Integer, Double, String, List and null.
Add a Triangle to the shapes above. What does the compiler say about your old switch?
Change the guessing game to stop after 7 wrong guesses.
Compact for scripts and exercises, classic for real projects. The rest of the language is
identical.
Compile and run
java Calculator.java # run a single file directlyjavac Calculator.java # produce Calculator.classjava Calculator
javac -d out $(find src -name "*.java")java -cp out com.example.Calculator
voidmain()throwsException{varzip=IO.readln("ZIP code: ").strip();try(varclient=HttpClient.newHttpClient()){varrequest=HttpRequest.newBuilder(URI.create("https://api.zippopotam.us/us/"+zip)).build();varresponse=client.send(request,HttpResponse.BodyHandlers.ofString());switch(response.statusCode()){case200->IO.println(response.body());case404->IO.println("unknown ZIP code");default->IO.println("unexpected status "+response.statusCode());}}}
Manners
respect rate limits β on 429, back off instead of retrying immediately
always set timeouts
keys go in environment variables: System.getenv("API_TOKEN")
one HttpClient per application, not per request
β Exercises
Call any public API; print the status code and the first 200 characters.
Print all response headers (response.headers().map()).
Fetch three URLs one after another and measure the total time β you will parallelise this
in chapter 15.
Retry up to three times with growing delays when you get a 429.
Read a token from an environment variable and send it as an Authorization header.
What should happen if the variable is missing?
Day 2: Intermediate Java
Streams, object orientation, modules, tooling and concurrency.
Do not use java.util.Date, Calendar or SimpleDateFormat in new code.
Randomness
varr=RandomGenerator.getDefault();r.nextInt(1,7)// a dier.nextDouble()r.nextBoolean()r.ints(5,1,50).sorted().forEach(IO::println);Collections.shuffle(cards);
Record when the object is its data (DTOs, value objects). Class when it has mutable state or
identity β two accounts with the same balance are not the same account.
publicabstractclassVehicle{protectedfinalStringplate;protectedVehicle(Stringplate){this.plate=plate;}publicabstractinttopSpeed();// subclasses must implementpublicStringdescribe(){return"vehicle "+plate;}}publicclassCarextendsVehicle{privatefinalinthp;publicCar(Stringplate,inthp){super(plate);this.hp=hp;}@OverridepublicinttopSpeed(){return50+hp;}@OverridepublicStringdescribe(){returnsuper.describe()+" with "+hp+" hp";}}
abstract β cannot be instantiated
final β class cannot be extended, method cannot be overridden
@Override β optional, but lets the compiler check you
Composition over inheritance
“A car is a vehicle” β inheritance. “A car has an engine” β a field. When unsure, prefer
interfaces plus composition.
Sealed types
sealed fixes the set of subtypes, so the compiler knows every case:
publicsealedinterfaceEventpermitsSignUp,Order,SignOut{}publicrecordSignUp(Stringuser)implementsEvent{}publicrecordOrder(Stringuser,longcents)implementsEvent{}publicrecordSignOut(Stringuser)implementsEvent{}Stringlog(Evente){returnswitch(e){caseSignUp(Stringu)->u+" signed up";caseOrder(Stringu,longc)->"%s ordered %d cents".formatted(u,c);caseSignOut(Stringu)->u+" left";};// no default needed}
sealed interface + record + pattern matching is the modern way to say “a value is one of
these things”.
Checked exceptions describe expected trouble from the outside world; the caller must
handle or declare them. Unchecked ones usually mean a bug.
try{vartext=Files.readString(path);}catch(IOExceptione){IO.println("cannot read: "+e.getMessage());}Stringread(Pathp)throwsIOException{// or pass it upreturnFiles.readString(p);}
try / catch / finally
try{risky();}catch(NumberFormatExceptione){// specific firstβ¦}catch(RuntimeExceptione){// general laterβ¦}finally{// always runs}
publicclassOverdraftExceptionextendsRuntimeException{privatefinallongmissing;publicOverdraftException(longmissing){super("short by %d cents".formatted(missing));this.missing=missing;}publiclongmissing(){returnmissing;}}
Always pass the cause along β otherwise you lose the information you need later:
Objects.requireNonNull(from,"from is required");if(amount<=0)thrownewIllegalArgumentException("amount must be positive: "+amount);
Situation
Exception
invalid argument
IllegalArgumentException
wrong object state
IllegalStateException
argument was null
NullPointerException via requireNonNull
not implemented yet
UnsupportedOperationException
business rule broken
your own type
Common mistakes
// swallowingtry{risky();}catch(Exceptione){}// never// control flowtry{while(true)IO.println(list.get(i++));}catch(IndexOutOfBoundsExceptione){}// use a for-each loop// useless messagesthrownewIllegalArgumentException("error");//saywhatandwhichvalue
Catch where you can actually react β usually high up, not in every helper method.
Cleanup without catching:
try{process();}finally{releaseLock();// exception still propagates}
β Exercises
int divide(int a, int b) throwing a helpful ArithmeticException when b == 0.
Write InvalidInputException and use it in a validator.
A method returning an empty list instead of throwing when a file is missing β when is that
good, when dangerous?
<T> T withRetry(Supplier<T> task, int attempts) β retry on exception, rethrow the last one.
Create a wrapped exception (cause + wrapper) and print both messages.
What does a method return when try has return 1 and finally has return 2? Try it β
then never write it.
Packages, Modules & Libraries
Packages
A package is a namespace and a directory, named after your reversed domain:
In compact source files java.base is imported automatically.
The module system
module-info.java declares what a module needs and exposes:
modulecom.example.shop{requiresjava.net.http;requirestransitivejava.sql;exportscom.example.shop;exportscom.example.shop.api;// com.example.shop.internal stays private, even for public classes}
Worth it for libraries and large applications; unnecessary for small programs.
Standard library map
Package
Contents
java.lang
String, Math, Thread, IO
java.util
collections, Optional, Random
java.util.stream
streams and collectors
java.time
dates and times
java.nio.file
files and paths
java.net.http
HTTP client
java.util.concurrent
threads, executors, locks
java.util.regex
regular expressions
java.math
BigDecimal, BigInteger
External libraries
By hand it is painful:
javac -cp libs/jackson-databind-2.18.2.jar -d out src/*.java
java -cp out:libs/* com.example.Main
Maven is rigid but identical everywhere β a good default. Gradle is more flexible and faster
on big projects, at the price of learning a small language.
Packaging
jar --create --file shop.jar --main-class com.example.shop.Main -C out .
java -jar shop.jar
A trimmed runtime image with only the modules you need:
jlink --add-modules java.base,java.net.http --output runtime --strip-debug
./runtime/bin/java -cp out com.example.shop.Main
Libraries worth knowing
Purpose
Library
JSON
Jackson, Gson
tests
JUnit 5, AssertJ, Mockito
logging
SLF4J + Logback
web
Spring Boot, Quarkus, Javalin, Helidon
database
JDBC, jOOQ, Hibernate
CLI parsing
picocli
β Exercises
Create a Maven project and get a “hello world” running with mvn package.
Move your chapter 7 solutions into a package and fix the imports.
Add Jackson and round-trip a record to JSON and back.
Write a module-info.java. What happens if you leave out a requires?
Try import module java.base; β which imports can you delete?
Build an executable JAR and run it with java -jar.
IO.readln returns null at end of input (Ctrl+D, or redirected files) β check for it.
The classic way:
varscanner=newScanner(System.in);varn=scanner.nextInt();scanner.nextLine();// consume the rest of the line!vartext=scanner.nextLine();
Warning
nextInt() leaves the newline behind, so the next nextLine() returns empty. Safer: read
whole lines and parse them yourself.
intreadInt(Stringprompt,intmin,intmax){while(true){varinput=IO.readln(prompt);if(input==null)thrownewIllegalStateException("input closed");try{varn=Integer.parseInt(input.strip());if(n<min||n>max){IO.println("out of range");continue;}returnn;}catch(NumberFormatExceptione){IO.println("not a number");}}}
Parsing arguments
voidmain(String[]args){Stringfile=null;varverbose=false;varlimit=10;for(vari=0;i<args.length;i++){switch(args[i]){case"-v","--verbose"->verbose=true;case"-n","--limit"->limit=Integer.parseInt(args[++i]);case"-h","--help"->{help();return;}default->{if(args[i].startsWith("-")){IO.println("unknown option");return;}file=args[i];}}}if(file==null){help();return;}run(file,limit,verbose);}voidhelp(){IO.println("""
usage: java Tool.java [options] <file>
-n, --limit N number of results (default 10)
-v, --verbose verbose output
-h, --help this help
""");}
For anything bigger, use picocli:
@Command(name="count",mixinStandardHelpOptions=true,version="1.0")publicclassCountimplementsRunnable{@Parameters(index="0",description="file to read")privatePathfile;@Option(names={"-n","--limit"})privateintlimit=10;@Overridepublicvoidrun(){β¦}publicstaticvoidmain(String[]args){System.exit(newCommandLine(newCount()).execute(args));}}
Virtual threads help waiting work (HTTP, database, files). For computing work use a
pool sized to your CPU cores.
Executors and futures
try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){Future<Integer>future=executor.submit(()->expensive());varresult=future.get();// blocks until ready}vartasks=List.<Callable<String>>of(()->fetch("https://example.test/a"),()->fetch("https://example.test/b"));try(varexecutor=Executors.newVirtualThreadPerTaskExecutor()){for(varf:executor.invokeAll(tasks))IO.println(f.get());}
Structured concurrency (preview in 25)
Subtasks that belong together should live and die together: if one fails, the others are
cancelled.
// run with: java --enable-preview --source 25 File.javaimportjava.util.concurrent.StructuredTaskScope;recordProfile(Stringdetails,Stringorders){}ProfileloadProfile(Stringid)throwsException{try(varscope=StructuredTaskScope.open()){vardetails=scope.fork(()->loadDetails(id));varorders=scope.fork(()->loadOrders(id));scope.join();// waits for both, cancels on failurereturnnewProfile(details.get(),orders.get());}}
No orphaned threads, no forgotten cleanup, readable stack traces.
Scoped values (Java 25)
ScopedValue replaces ThreadLocal for passing context (user, request id, tenant) down the
call stack. Immutable and cleaned up automatically.
finalstaticScopedValue<String>USER=ScopedValue.newInstance();voidhandleRequest(Stringuser){ScopedValue.where(USER,user).run(()->{checkPermissions();writeAuditLog();// sees USER without a parameter});}voidwriteAuditLog(){IO.println("action by "+USER.get());}
Shared state is the hard part
varcounter=newint[1];try(vare=Executors.newVirtualThreadPerTaskExecutor()){for(vari=0;i<1000;i++)e.submit(()->counter[0]++);// broken: not atomic}IO.println(counter[0]);//almostnever1000
Immutable objects (records!) are thread-safe by construction. Less shared mutable state means
less synchronisation β and fewer bugs that only show up in production.
One behaviour per test, and the method name states the rule β not the method being called.
Assertions
assertEquals(expected,actual);assertEquals(3.14,value,0.001);// delta for doublesassertTrue(x);assertFalse(x);assertNull(x);assertNotNull(x);assertArrayEquals(newint[]{1,2},result);assertThrows(IllegalArgumentException.class,()->method());assertDoesNotThrow(()->method());assertAll("person",()->assertEquals("Ann",p.name()),()->assertEquals(30,p.age()));
varp=Path.of("file.txt");Files.readString(p);Files.writeString(p,"content");Files.readAllLines(p);Files.lines(p);// stream, close itFiles.exists(p);Files.size(p);Files.createDirectories(p.getParent());
Your own project β one CLI tool you actually use beats a hundred exercises
Books
Effective Java (Joshua Bloch) β best practices, read it once the basics stick
Java by Comparison β before/after snippets, ideal right after a beginner course
Modern Java in Action β streams, lambdas, functional style
Java Concurrency in Practice β older, but the concepts still hold
Community
Local Java User Groups β most cities have one, talks are usually free
Conference talks from JavaOne, Devoxx and JCon are on YouTube
Stack Overflow β always check the date; a
lot of it describes Java 8
Tip
Check the publication year of anything you read. Java changed a lot since 8 β plenty of
“this is how you do it” answers are now needlessly complicated.
About this course
All code samples are public domain (CC0). Tested with OpenJDK 25. Preview features are marked
and need --enable-preview.