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()));