Until now you have been on the receiving end: reading stack traces and catching what others threw. In this lesson you change roles. throw is the word with which your code declares that it cannot fulfil its contract, and throws is the annotation with which it announces that in its signature.
This change of role is what finally settles two debts you have been carrying since module 3. The first: the constructors of Book, Employee and Loan validate their data by printing console warnings and replacing the invalid values with defaults —"Untitled", year = 0—, which produces objects that exist but are wrong. The second: Catalog.findByReference returns null when it finds nothing, and that null travels through the program until it blows up far from its origin. Both were promised for this module. Time to pay up.
You will also see the technique that separates diagnosable code from code that is not: exception chaining. Wrapping a low-level failure in one from your layer's vocabulary is fine; doing it losing the cause is throwing away the only information that would have solved the case. The difference between the two is one argument in a constructor, and you will see exactly what disappears from the stack trace when it is forgotten.
Contents
throw: throwing explicitly- Unreachable code after a
throw throws: declaring in the signature- What
throwsforces on the caller - Propagation up the chain
- Declaring unchecked exceptions in
throws - Argument validation: the toolbox
Objects.requireNonNulland its variants- Preconditions and failing fast
- Exception chaining: preserving the cause
- What exactly is lost if you do not preserve the cause
- Rethrowing:
throw eversus wrapping - Exception translation between layers
- Java 7's precise rethrow analysis
throwsand method overriding- BiblioTech: validations that throw and searches that do not return
null - Common Mistakes and Tips
- Exercises
throw: throwing explicitly
throw: throwing explicitlyThe syntax is deceptively simple:
Three rules:
- The expression must evaluate to an object that descends from
Throwable. You cannot throw aStringor anint. - The usual thing is to create the object in the
throwitself:throw new IllegalArgumentException("...");. But you can also throw an exception you already have in a variable. - If the expression evaluates to
null, aNullPointerExceptionis thrown instead.throw null;compiles and produces aNullPointerException, not some odd failure.
An example from BiblioTech, now with the right mindset:
package com.nexussoftware.bibliotech.domain;
public class RateCalculator {
private static final double DAILY_RATE = 0.25;
private static final double MAX_FINE = 20.0;
/**
* Calculates the fine for a material returned late.
*
* @param daysLate days late, zero or positive
* @return the amount, capped at MAX_FINE
* @throws IllegalArgumentException if daysLate is negative
*/
public double calculate(int daysLate) {
if (daysLate < 0) {
throw new IllegalArgumentException(
"The days late cannot be negative: " + daysLate);
}
return Math.min(daysLate * DAILY_RATE, MAX_FINE);
}
public static void main(String[] args) {
RateCalculator calc = new RateCalculator();
System.out.println(calc.calculate(4)); // 1.0
System.out.println(calc.calculate(120)); // 20.0 (capped)
System.out.println(calc.calculate(-3)); // <-- IllegalArgumentException
}
}Output:
1.0 20.0 Exception in thread "main" java.lang.IllegalArgumentException: The days late cannot be negative: -3 at com.nexussoftware.bibliotech.domain.RateCalculator.calculate(RateCalculator.java:16) at com.nexussoftware.bibliotech.domain.RateCalculator.main(RateCalculator.java:26)
Compare this behaviour with the module 3 version, which did this:
// OLD VERSION (module 3): fixes the data and carries on
public double calculate(int daysLate) {
if (daysLate < 0) {
System.out.println("WARNING: negative days, using 0");
daysLate = 0; // "fixes" the data
}
return Math.min(daysLate * DAILY_RATE, MAX_FINE);
}The difference is enormous. The old version invents a piece of data and returns 0.0 as if it were a legitimate calculation. The caller receives a perfectly believable number that corresponds to nothing real, and the warning is lost in a console that probably nobody is watching. The new version refuses to invent: it says what it expected, what it received and on which line, and stops the process before propagating false data.
The general principle, which holds for the whole module:
A noisy failure now is better than a silent incorrect result later.
- Unreachable code after a
throw
throwA throw ends the method immediately, just like a return. Any statement after it in the same block is unreachable, and Java detects it at compile time:
public double calculate(int daysLate) {
if (daysLate < 0) {
throw new IllegalArgumentException("Negative: " + daysLate);
// System.out.println("never"); // error: unreachable statement
}
return daysLate * DAILY_RATE;
}That check has a very useful practical consequence: a method whose else ends in a throw does not need a return in that branch, because the compiler knows that path does not reach the end:
public Severity classify(int daysLate) {
if (daysLate < 0) {
throw new IllegalArgumentException("Negative: " + daysLate);
}
if (daysLate == 0) { return Severity.ON_TIME; }
if (daysLate <= MINOR_THRESHOLD) { return Severity.MINOR; }
return Severity.SEVERE;
// No return needed after the initial throw: that path never gets here
}And a case that confuses people: a method declared with a return type that only throws compiles perfectly, with no return:
public Material findRequired(String reference) {
throw new UnsupportedOperationException("Not implemented yet");
// Compiles: the method never returns normally, so no return is missing
}It is the usual idiom for leaving a method unimplemented without breaking compilation. Much better than return null;, which would compile just the same but would smuggle in a null.
throws: declaring in the signature
throws: declaring in the signaturethrows goes in the method signature, after the parameter list, and announces which exceptions it can propagate:
Two almost identical words with opposite roles:
throw |
throws |
|
|---|---|---|
| Where | Inside the method body | In the signature |
| What it does | Throws an exception now | Declares that one may be thrown |
| How many | One, that of the object | Several, separated by commas |
| Syntax | throw new X("..."); |
... method() throws X, Y { |
| Compulsory | Never | Yes, for checked ones that propagate |
Several exceptions are separated by commas:
And the essential point: throws is only compulsory for checked exceptions. If your method throws IllegalArgumentException (unchecked), you do not have to declare anything; the code compiles just the same.
// Compiles without throws: IllegalArgumentException is UNCHECKED
public double calculate(int daysLate) {
if (daysLate < 0) {
throw new IllegalArgumentException("Negative");
}
return daysLate * 0.25;
}
// Does NOT compile without throws: IOException is CHECKED
public String readCatalog(String path) {
throw new java.io.IOException("Not implemented");
// error: unreported exception IOException; must be caught or declared to be thrown
}
- What
throws forces on the caller
throws forces on the callerWhen you call a method that declares a checked exception, the compiler gives you exactly two options. It is the catch or specify rule.
package com.nexussoftware.bibliotech.service;
import java.io.IOException;
public class CallerOptions {
/** Method that declares a checked exception. */
static String readCatalog(String path) throws IOException {
if (!path.endsWith(".txt")) {
throw new IOException("Unsupported format: " + path);
}
return "3 materials";
}
// OPTION A: catch. This method takes responsibility for the failure.
static void optionCatch() {
try {
String content = readCatalog("catalog.csv");
System.out.println(content);
} catch (IOException e) {
System.out.println("The catalogue could not be loaded: " + e.getMessage());
System.out.println("Starting with an empty catalogue.");
}
}
// OPTION B: declare. This method delegates the problem to WHOEVER CALLS IT.
static void optionDeclare() throws IOException {
String content = readCatalog("catalog.csv");
System.out.println(content);
}
// OPTION C (illegal): ignore. DOES NOT COMPILE.
// static void optionIgnore() {
// readCatalog("catalog.csv");
// // error: unreported exception IOException; must be caught or declared
// }
public static void main(String[] args) {
optionCatch();
// main also has to choose: here it catches
try {
optionDeclare();
} catch (IOException e) {
System.out.println("main: " + e.getMessage());
}
}
}How to choose between A and B, which is the real design decision:
| Choose... | When... |
|---|---|
| Catch (A) | This method knows what to do: there is a default value, an alternative or a degradation policy |
| Declare (B) | This method does not know what to do; the decision belongs to someone with more context |
The correct answer is usually B more often than people think. A deep service method almost never has the information needed to decide whether a read failure should abort the application, show a warning or load default values. That decision belongs to a higher layer. Catching for the sake of catching, to "get the error off your back", is the origin of the empty catch.
A shortcut worth knowing: main can declare throws.
It is perfectly legal, and it means "if this fails, let the program die and print the trace". For a learning example or a small tool it is acceptable. For a real application it is a surrender: the end user sees a raw stack trace. In 06-07 you will replace it with a decent error boundary.
- Propagation up the chain
When every method in the chain chooses to declare instead of catch, the exception goes up level by level until the first one that decides to take charge. The throws declaration goes up with it:
package com.nexussoftware.bibliotech.service;
import java.io.IOException;
public class PropagationChain {
// Level 4 (the deepest): this is where the failure originates
static String readFile(String path) throws IOException {
System.out.println(" [4] readFile: trying to open " + path);
throw new IOException("The file does not exist: " + path);
}
// Level 3: does not know what to do, declares and lets it through
static String loadLines(String path) throws IOException {
System.out.println(" [3] loadLines");
return readFile(path);
}
// Level 2: does not know either, declares and lets it through
static int loadCatalog(String path) throws IOException {
System.out.println("[2] loadCatalog");
String content = loadLines(path);
return content.length();
}
// Level 1: HERE there is context to decide. It catches.
public static void main(String[] args) {
System.out.println("[1] main: starting BiblioTech");
try {
int materials = loadCatalog("catalog.txt");
System.out.println("Catalogue loaded: " + materials + " materials");
} catch (IOException e) {
System.out.println("[1] main: there is no previous catalogue (" + e.getMessage() + ")");
System.out.println("[1] main: starting with an empty catalogue. The application continues.");
}
}
}Output:
[1] main: starting BiblioTech [2] loadCatalog [3] loadLines [4] readFile: trying to open catalog.txt [1] main: there is no previous catalogue (The file does not exist: catalog.txt) [1] main: starting with an empty catalogue. The application continues.
The journey, drawn out:
flowchart TB
M["main<br/>try-catch: DECIDES"]
C2["loadCatalog<br/>throws IOException"]
C3["loadLines<br/>throws IOException"]
C4["readFile<br/>throw new IOException"]
M -->|"calls"| C2
C2 -->|"calls"| C3
C3 -->|"calls"| C4
C4 -.->|"the exception goes up"| C3
C3 -.->|"goes up: it only declares"| C2
C2 -.->|"goes up: it only declares"| M
M -.->|"CAUGHT: graceful degradation"| F["Starts with an empty catalogue"]
Notice the distribution of responsibilities, which is the pattern you will apply in BiblioTech: levels 2, 3 and 4 detect and report; only level 1 decides. And that decision —starting with an empty catalogue instead of dying— is only possible in main, because only there is it known that the application can work without a previous catalogue. readFile had no way of knowing.
- Declaring unchecked exceptions in
throws
throwsYou can put an unchecked exception in throws. It is legal but optional, and the compiler ignores it completely: it forces nothing on the caller.
// Legal. It forces nothing on the caller, but it DOCUMENTS.
public double calculate(int daysLate) throws IllegalArgumentException {
if (daysLate < 0) {
throw new IllegalArgumentException("Negative: " + daysLate);
}
return daysLate * DAILY_RATE;
}Is it worth it? There are two schools of thought, and the consensus is fairly clear:
| Position | Argument |
|---|---|
| In favour | The signature documents the complete contract; some IDEs use it to warn you |
| Against (the majority) | It clutters the signature without adding any checking; it gives a false sense of completeness; nobody declares NullPointerException and yet almost any method can throw one |
The recommended practice: document the unchecked ones with @throws in the Javadoc, not in the signature. The Javadoc is the place where you can explain under what condition it is thrown, which is the information that really matters.
/**
* Calculates the fine corresponding to a delay.
*
* @param daysLate days late; must be zero or positive
* @return the amount in euros, capped at {@value #MAX_FINE}
* @throws IllegalArgumentException if {@code daysLate} is negative
*/
public double calculate(int daysLate) {
// ...
}This really is useful: it appears in the generated documentation, the IDE shows it when autocompleting and it explains the condition, not just the type. The signature stays clean.
- Argument validation: the toolbox
We reach the module 3 debt. Java has a standard vocabulary for rejecting invalid input, and using the right term matters: whoever catches will be able to tell one problem from another.
| Exception | When to use it | Example in BiblioTech |
|---|---|---|
NullPointerException |
A required argument is null |
new Loan(ref, null, employee, 1) |
IllegalArgumentException |
The argument is not null but its value is invalid |
publicationYear = 1200; daysLate = -3 |
IndexOutOfBoundsException |
An index is out of range | An invalid position in the reservation queue |
IllegalStateException |
The arguments are valid, but the object is not in a state that allows the operation | Returning a loan that has already been returned |
UnsupportedOperationException |
The operation is not supported by this implementation | add on a read-only catalogue |
ArithmeticException |
An impossible arithmetic condition | Thrown by the JVM on integer division by zero |
The distinction that takes most getting used to is IllegalArgumentException versus IllegalStateException. The rule:
IllegalArgumentException: the problem is in what you passed me. With other arguments, the call would work.IllegalStateException: the problem is in when you asked me. With the same arguments, at another moment, it would work.
Applied to Loan:
package com.nexussoftware.bibliotech.domain;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Loan of a material to an employee.
*
* From this module onwards, the class REJECTS invalid data instead of
* correcting it with console warnings, as it did in module 3.
*/
public class Loan {
public static final int LOAN_DAYS = 15;
private final String reference; // format LN-NNNN
private final Material material;
private final Employee borrower;
private final int startDay;
private int returnDay = -1; // -1 = not yet returned
private final List<Incident> incidents = new ArrayList<>();
public Loan(String reference, Material material, Employee borrower, int startDay) {
// 1. Nulls -> NullPointerException, with Objects.requireNonNull
this.reference = Objects.requireNonNull(reference, "The reference cannot be null");
this.material = Objects.requireNonNull(material, "The material cannot be null");
this.borrower = Objects.requireNonNull(borrower, "The borrower cannot be null");
// 2. Invalid format -> IllegalArgumentException (the argument is wrong)
if (!reference.matches("LN-\\d{4}")) {
throw new IllegalArgumentException(
"The reference must have the format LN-NNNN, and it was: '" + reference + "'");
}
if (startDay < 1) {
throw new IllegalArgumentException(
"The start day must be 1 or later, and it was: " + startDay);
}
// 3. Incompatible state -> IllegalStateException (the argument is fine,
// but the object you pass me is not in a fit condition)
if (!material.isAvailable()) {
throw new IllegalStateException(
"The material " + material.getReference() + " is not available");
}
if (!borrower.canBorrow()) {
throw new IllegalStateException(
"The employee " + borrower.getIdentifier() + " has reached the limit of "
+ Employee.MAX_CONCURRENT_LOANS + " concurrent loans");
}
this.startDay = startDay;
material.lend();
borrower.registerLoan();
}
/**
* Registers the return.
*
* @throws IllegalArgumentException if the day is earlier than the start day
* @throws IllegalStateException if the loan had already been returned
*/
public void registerReturn(int day) {
// IllegalState: the problem is WHEN it is asked, not WHAT is asked
if (isReturned()) {
throw new IllegalStateException(
"The loan " + reference + " was already returned on day " + returnDay);
}
// IllegalArgument: the problem is the argument itself
if (day < startDay) {
throw new IllegalArgumentException(
"The return day (" + day + ") cannot be earlier than the start day ("
+ startDay + ")");
}
this.returnDay = day;
material.returnItem();
}
public boolean isReturned() { return returnDay >= 0; }
public String getReference() { return reference; }
public Material getMaterial() { return material; }
public Employee getBorrower() { return borrower; }
public int getStartDay() { return startDay; }
public int calculateDaysLate(int currentDay) {
int referenceDay = isReturned() ? returnDay : currentDay;
int excess = referenceDay - (startDay + LOAN_DAYS);
return Math.max(0, excess);
}
/** Incident recorded during the loan (static nested class, 04-03). */
public static class Incident {
private final int day;
private final String reason;
private final Severity severity;
public Incident(int day, String reason, Severity severity) {
if (day < 1) {
throw new IllegalArgumentException("Invalid day: " + day);
}
this.day = day;
this.reason = Objects.requireNonNull(reason, "The reason cannot be null");
this.severity = Objects.requireNonNull(severity, "The severity cannot be null");
}
@Override
public String toString() {
return "Day " + day + " [" + severity + "]: " + reason;
}
}
}Compare with the module 3 version:
// OLD VERSION: creates invalid objects and warns nobody
public Loan(String reference, Material material, Employee borrower, int startDay) {
if (reference == null || !reference.matches("LN-\\d{4}")) {
System.out.println("WARNING: invalid reference, using LN-0000");
reference = "LN-0000"; // every bad loan shares a reference!
}
if (startDay < 1) {
System.out.println("WARNING: invalid day, using 1");
startDay = 1;
}
this.reference = reference;
// ...
}The damage of the old version is worse than it looks at first sight: every loan with an invalid reference ends up sharing LN-0000, so the Map<String, Loan> index of the LoanRegistry keeps overwriting them one with another. Loans are lost silently. And the warning was printed hours ago in a console nobody kept.
Objects.requireNonNull and its variants
Objects.requireNonNull and its variantsjava.util.Objects offers the canonical way to reject nulls. It is one line, it returns the value and it throws NullPointerException with your message if it is null:
import java.util.Objects;
// Canonical form: validates and assigns in the same line
this.borrower = Objects.requireNonNull(borrower, "The borrower cannot be null");It is exactly equivalent to this, but in one line and with no noise:
if (borrower == null) {
throw new NullPointerException("The borrower cannot be null");
}
this.borrower = borrower;Its useful variants:
| Method | What it does |
|---|---|
requireNonNull(obj) |
Throws NullPointerException with no message |
requireNonNull(obj, "message") |
With a message. This is the one you should use |
requireNonNull(obj, Supplier<String>) |
Lazy message: only built if it fails |
requireNonNullElse(obj, default) |
Returns default if obj is null; does not throw |
requireNonNullElseGet(obj, Supplier) |
The same, with the default computed lazily |
Objects.isNull(obj) / nonNull(obj) |
Predicates; useful as method references (04-06) |
Objects.equals(a, b) |
Null-tolerant comparison (you saw it in 03-09) |
Objects.requireNonNullElse vs checkIndex |
Objects.checkIndex(i, length) validates indexes and throws IndexOutOfBoundsException |
The Supplier variant deserves a note, because it has a specific reason to exist:
// Bad: the message is built ALWAYS, even when it does not fail
Objects.requireNonNull(material, "Material " + reference
+ " was not found in the catalogue of " + branch + " on day " + day);
// Good: the lambda only runs if material is null
Objects.requireNonNull(material, () -> "Material " + reference
+ " was not found in the catalogue of " + branch + " on day " + day);With the first form, that four-piece concatenation happens on every call, even in the 99.99% that do not fail. With the lambda, only when it is needed. It is the same principle of lazy evaluation you saw in 04-05 with lambdas, and in a heavily called method the difference is measurable.
A frequent debate: NullPointerException or IllegalArgumentException for a null argument? Both are defensible, but the dominant convention in Java —and the one followed by the JDK itself and by Objects.requireNonNull— is NullPointerException. The practical reason: that way an unexpected null always produces the same exception, whether you detect it while validating or it blows up later when used. Be consistent: choose NullPointerException for nulls and IllegalArgumentException for non-null but invalid values.
A warning about validation order. This code has a subtle flaw:
// BAD: reference is used BEFORE checking that it is not null
if (!reference.matches("LN-\\d{4}")) { // NullPointerException if it is null
throw new IllegalArgumentException("Invalid format");
}
this.reference = Objects.requireNonNull(reference, "..."); // never reachedIf reference is null, a NullPointerException fires without your message, from matches. Always validate nulls first, and then the rest.
- Preconditions and failing fast
A precondition is what a method demands in order to fulfil its contract. calculate(int daysLate) demands daysLate >= 0. Loan(...) demands that no argument is null, that the reference has the format LN-NNNN and that the material is available.
Failing fast (fail-fast) is the principle of checking those preconditions at the start of the method, before touching anything, and rejecting immediately if they are not met.
flowchart TB
A["Enters the method"] --> B["VALIDATE preconditions"]
B --> C{"Are they met?"}
C -->|"no"| D["throw: immediate failure,<br/>with a message and the offending data"]
C -->|"yes"| E["Run the logic<br/>without checking again"]
E --> F["Return the result"]
D --> G["The defect is detected<br/>WHERE and WHEN it happens"]
The three advantages, by name:
- The error appears close to its cause. Without validation, a
nullpassed in the constructor blows up twenty minutes later when printing a receipt, with a stack trace pointing at a place that is not to blame. It is exactly the case you analysed in exercise 2 of 06-01. - The rest of the method can trust. Once the preconditions are validated, the logic is written without defensive checks scattered everywhere.
- The object never exists in an invalid state. If the constructor throws, there is no object. There is no way for a
Loanwith no borrower to circulate through the program.
That third point connects directly with the invariants from 03-07. An invariant is a condition that holds throughout the object's life. Without validation in the constructor, the invariant "a loan always has a borrower" is an aspiration. With validation, it is a fact guaranteed by the language.
A convenient idiom when there are many preconditions: private validation methods with descriptive names.
public Book(String reference, String title, String author, int publicationYear, String isbn) {
validateReference(reference);
validateText(title, "title");
validateText(author, "author");
validateYear(publicationYear);
validateIsbn(isbn);
// From here on, everything is valid: the logic stays clean
this.reference = reference;
this.title = title;
this.author = author;
this.publicationYear = publicationYear;
this.isbn = isbn;
}
private static void validateText(String value, String fieldName) {
Objects.requireNonNull(value, "The " + fieldName + " cannot be null");
if (value.isBlank()) {
throw new IllegalArgumentException("The " + fieldName + " cannot be blank");
}
}
private static void validateYear(int year) {
if (year < 1450 || year > 2100) {
throw new IllegalArgumentException(
"The publication year must be between 1450 and 2100, and it was: " + year);
}
}
private static void validateIsbn(String isbn) {
Objects.requireNonNull(isbn, "The ISBN cannot be null");
if (!isbn.matches("97[89]-\\d{10}")) {
throw new IllegalArgumentException(
"ISBN with an invalid format (978-NNNNNNNNNN was expected): " + isbn);
}
}A warning about the scope of this technique: failing fast is for programming errors and for data coming from inside the system. For data typed by a user, you already saw in 06-02 that the correct policy is different: validate and retry with a friendly message, not throw an exception in their face. The border between the two policies is the presentation layer: outside it you throw, inside it you ask.
- Exception chaining: preserving the cause
When you wrap an exception in another, you have to pass the original one as the cause. There are two ways, and one of them is clearly preferable.
Preferred form: the constructor with a cause.
try {
int year = Integer.parseInt(fields[2]);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Malformed catalogue line: " + line, e);
// ^^^
// the cause
}Throwable defines four constructors, and all the standard exceptions offer them:
| Constructor | Use |
|---|---|
X() |
No message, no cause |
X(String message) |
With a message. The most frequent when originating a failure |
X(String message, Throwable cause) |
When wrapping. The one you must use when translating |
X(Throwable cause) |
Message derived from cause.toString(). Not much recommended: you lose the chance to explain |
Alternative form: initCause. It exists for old exceptions that do not offer the constructor with a cause:
IllegalArgumentException failure = new IllegalArgumentException("Malformed line: " + line);
failure.initCause(e); // can only be called ONCE, and only if there was no cause
throw failure;initCause has two limitations that make it awkward: it can only be called once per object, and it fails with IllegalStateException if the cause was already set (even if it was set to null through the two-argument constructor). Use it only when you have no alternative.
A complete example with BiblioTech's three layers:
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.Book;
import com.nexussoftware.bibliotech.domain.Material;
/**
* Demonstrates exception chaining across three layers.
*/
public class CatalogLoader {
/** DATA LAYER: turns a line of text into a Material. */
private Material parseLine(String line) {
String[] fields = line.split(";");
return new Book(fields[0].trim(), fields[1].trim(), fields[2].trim(),
Integer.parseInt(fields[3].trim()), fields[4].trim());
// Can throw: ArrayIndexOutOfBoundsException, NumberFormatException,
// IllegalArgumentException (Book's validations)
}
/** SERVICE LAYER: translates into the domain's vocabulary, PRESERVING the cause. */
public Material loadMaterial(String line, int lineNumber) {
try {
return parseLine(line);
} catch (RuntimeException e) {
throw new IllegalStateException(
"Could not load the material of line " + lineNumber
+ " of the catalogue: '" + line + "'", e); // <-- CAUSE PRESERVED
}
}
public static void main(String[] args) {
CatalogLoader loader = new CatalogLoader();
loader.loadMaterial("BK-0001;Effective Java;Bloch;thousand;978-0000000001", 7);
}
}The resulting stack trace:
Exception in thread "main" java.lang.IllegalStateException: Could not load the material of line 7 of the catalogue: 'BK-0001;Effective Java;Bloch;thousand;978-0000000001' at com.nexussoftware.bibliotech.service.CatalogLoader.loadMaterial(CatalogLoader.java:24) at com.nexussoftware.bibliotech.service.CatalogLoader.main(CatalogLoader.java:32) Caused by: java.lang.NumberFormatException: For input string: "thousand" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) at java.base/java.lang.Integer.parseInt(Integer.java:665) at java.base/java.lang.Integer.parseInt(Integer.java:781) at com.nexussoftware.bibliotech.service.CatalogLoader.parseLine(CatalogLoader.java:16) at com.nexussoftware.bibliotech.service.CatalogLoader.loadMaterial(CatalogLoader.java:22) ... 1 more
This dump contains both complete stories: which business operation failed and with what data (block 1), and what the specific technical failure was and on which line of code (the Caused by:). It is exactly what you need at three in the morning.
- What exactly is lost if you do not preserve the cause
It is worth seeing it with the two dumps side by side, because the difference is underestimated until you have to debug without it.
// VERSION THAT LOSES THE CAUSE
public Material loadMaterialBadly(String line, int lineNumber) {
try {
return parseLine(line);
} catch (RuntimeException e) {
throw new IllegalStateException("Could not load the material of line " + lineNumber);
// ^ the ", e" is missing
}
}Its stack trace:
Exception in thread "main" java.lang.IllegalStateException: Could not load the material of line 7 at com.nexussoftware.bibliotech.service.CatalogLoader.loadMaterialBadly(CatalogLoader.java:30) at com.nexussoftware.bibliotech.service.CatalogLoader.main(CatalogLoader.java:38)
And that is all. Compare what information has disappeared:
| Information | With cause | Without cause |
|---|---|---|
| Which business operation failed | Yes | Yes |
| What technical failure happened | NumberFormatException |
Lost |
| What specific data caused it | "thousand" |
Lost |
| On which line of code it happened | parseLine, line 16 |
Lost |
| The whole stack of the real failure point | Complete | Lost |
With the second dump, a developer has to guess. Was a field missing? Was the year non-numeric? Did the ISBN have an invalid format? Was the title blank? All five possible causes produce exactly the same message. And since the input file probably no longer exists, they cannot even reproduce it.
That information is not recoverable. Once the original exception object is discarded without referencing it, the garbage collector takes it away with its whole stack inside.
A rule with no exceptions: if you catch in order to wrap, always pass the cause. The cost is five characters.
One special case in which not passing the cause is correct: when the original exception contains sensitive information that must not leave that layer —a password in the message of a connection error, for example. In that case, log the original with all its detail in the internal log (06-07) and throw a new one with no cause. It is the only legitimate exception to the rule, and it is deliberate, not an oversight.
- Rethrowing:
throw e versus wrapping
throw e versus wrappingTwo different operations that are sometimes confused:
// A) RETHROW: the same exception, intact
catch (RuntimeException e) {
System.err.println("Context: processing " + reference);
throw e; // same object, same stack, same message
}
// B) WRAP: a NEW exception carrying the original inside
catch (RuntimeException e) {
throw new IllegalStateException("Failed to process " + reference, e);
}Rethrow (throw e) |
Wrap | |
|---|---|---|
| Type the caller sees | The original | The new one |
| Stack trace | Intact: no frames added | New trace, with Caused by: |
| When to use it | You only wanted to add context or log | You need to change the vocabulary or the type |
| Risk | You leak the type of a lower layer | None, if you preserve the cause |
A technical nuance about throw e: it does not reset the stack trace. The trace was captured when the object was built and it is not touched when rethrowing, so the original line of the failure is preserved. If you wanted to reset it —something you almost never want— you would have to call fillInStackTrace() explicitly.
And a warning about rethrowing out of habit:
// Almost always unnecessary
try {
doSomething();
} catch (RuntimeException e) {
throw e; // adds NOTHING: without the try, the result would be identical
}A catch that only rethrows without adding context, without logging and without releasing anything is pure noise. Delete it.
- Exception translation between layers
This is the professional use of chaining, and it connects directly with the abstraction from 03-08.
The principle: a layer must not leak the exceptions of its internal implementation. If Catalog stores materials in a HashMap today and in a database tomorrow, whoever uses it should not find out. But if Catalog lets a SQLException escape, the layer above becomes coupled to the database: it will have catch (SQLException e) everywhere, and the day you switch to files, that code will stop compiling.
flowchart TB
subgraph P["PRESENTATION layer"]
P1["BiblioTechMenu<br/>Sees: domain exceptions<br/>Shows: messages to the user"]
end
subgraph S["SERVICE layer"]
S1["LoanManager, Catalog<br/>Sees: data exceptions<br/>Translates to domain exceptions"]
end
subgraph D["DATA layer"]
D1["Files, database<br/>Throws: IOException, SQLException"]
end
D1 -->|"IOException"| S1
S1 -->|"MaterialNotFoundException<br/>wrapping the cause"| P1
P1 -->|"'Material BK-0001 was not found'"| U["User"]
In code:
package com.nexussoftware.bibliotech.service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.nexussoftware.bibliotech.domain.Material;
public class Catalog {
private final Map<String, Material> indexByReference = new HashMap<>();
/**
* BAD: it leaks the implementation detail.
* The day we move from files to a database, all the code
* calling this method will have to change its catch.
*/
public void loadFromFileBadly(String path) throws IOException {
// ...
}
/**
* GOOD: it translates into the domain's vocabulary, preserving the cause.
* The caller only needs to know that "the catalogue could not be loaded";
* the technical detail is still available in the Caused by: for diagnosis.
*/
public void loadFromFile(String path) {
try {
readPhysically(path); // implementation detail
} catch (IOException e) {
throw new IllegalStateException(
"Could not load the catalogue from '" + path + "'", e);
}
}
private void readPhysically(String path) throws IOException {
throw new IOException("Permission denied: " + path); // simulated; module 7 does it for real
}
}And the rule that governs this technique:
Each layer throws exceptions from the vocabulary of its own level of abstraction, and preserves those of the lower levels as the cause.
It is the same principle professional frameworks apply. Spring turns every SQLException into its DataAccessException hierarchy —with classes such as DuplicateKeyException or DataIntegrityViolationException— precisely so that your business code does not depend on the database engine. In 06-04 you will build the equivalent hierarchy for BiblioTech.
- Java 7's precise rethrow analysis
A brief but useful technical note. Before Java 7, this code did not compile:
public void process() throws IOException, java.sql.SQLException {
try {
operationThatThrowsBoth();
} catch (Exception e) {
log(e);
throw e; // Java 6: error, "unreported exception Exception"
}
}The old compiler reasoned crudely: the declared type of e is Exception, therefore throw e can throw any Exception, therefore the method must declare throws Exception.
Since Java 7, the compiler performs more precise rethrow analysis: it analyses which exceptions the try can really throw and deduces that e can only be IOException or SQLException. The code above compiles unchanged.
The condition for it to work: the catch parameter must not be reassigned. If you reassign it, the compiler falls back to the conservative analysis. That is why it is worth declaring it explicitly final when you depend on this feature:
} catch (final Exception e) { // the final documents the intent
log(e);
throw e; // the compiler deduces IOException | SQLException
}Remember from section 5 of 06-02 that in a multi-catch the parameter is implicitly final, without you having to write it.
throws and method overriding
throws and method overridingAn inheritance rule that connects directly with the polymorphism of 03-05 and 03-06.
An overriding method cannot declare broader checked exceptions than the superclass method.
The reason is the substitution principle: if you have a reference of type Material and call lend(), the compiler only knows how to deal with what Material.lend() declares. If a subclass could throw something more, a checked exception would slip through without anybody declaring it, and the checking system would break.
package com.nexussoftware.bibliotech.domain;
import java.io.IOException;
import java.io.FileNotFoundException;
class OverridingRules {
static class BaseMaterial {
/** Base method: declares IOException. */
public void export(String path) throws IOException {
System.out.println("Exporting " + path);
}
}
// A) Declare the SAME exception: LEGAL
static class BookA extends BaseMaterial {
@Override
public void export(String path) throws IOException { }
}
// B) Declare a SUBCLASS: LEGAL (it is more restrictive, safer)
static class BookB extends BaseMaterial {
@Override
public void export(String path) throws FileNotFoundException { }
}
// C) Declare none: LEGAL (the most restrictive of all)
static class BookC extends BaseMaterial {
@Override
public void export(String path) { }
}
// D) Declare a SUPERCLASS: ILLEGAL
// static class BookD extends BaseMaterial {
// @Override
// public void export(String path) throws Exception { }
// // error: export(String) in BookD cannot override export(String) in BaseMaterial
// // overridden method does not throw Exception
// }
// E) Declare an UNRELATED exception: ILLEGAL if it is checked
// static class BookE extends BaseMaterial {
// @Override
// public void export(String path) throws java.sql.SQLException { }
// // error: overridden method does not throw SQLException
// }
// F) Add any UNCHECKED one: ALWAYS LEGAL (the compiler does not police them)
static class BookF extends BaseMaterial {
@Override
public void export(String path) throws IllegalStateException {
throw new IllegalStateException("The material has no exportable content");
}
}
}Summary of the rule:
| Case | Legal? | Reason |
|---|---|---|
| The same exception | Yes | Identical contract |
| A subclass of the declared one | Yes | More restrictive: never surprises the caller |
| None | Yes | The most restrictive of all |
| A superclass of the declared one | No | Broader: the caller was not expecting it |
| An unrelated checked one | No | Same |
| Any unchecked one | Yes | The compiler never polices them |
Why substitution would break in case D:
BaseMaterial material = new BookD(); // polymorphism (03-06)
try {
material.export("catalog.txt"); // the compiler only sees BaseMaterial.export
} catch (IOException e) {
// Here only IOException can be caught, because it is the only one declared.
// If BookD could throw Exception, a checked one would escape uncaught
// and undeclared: the checked system would be broken.
}A practical corollary: when you design an interface or an abstract class (04-01, 04-02), think hard about which throws you put, because you are fixing the maximum for all present and future implementations. It is one of the weighty reasons for preferring unchecked exceptions in domain interfaces: they do not restrict implementers. It is exactly what Lendable will do in BiblioTech.
- BiblioTech: validations that throw and searches that do not return
null
nullWe close with both debts settled. First, Catalog, which stops lying with null and false:
package com.nexussoftware.bibliotech.service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import com.nexussoftware.bibliotech.domain.Book;
import com.nexussoftware.bibliotech.domain.Material;
/**
* BiblioTech's catalogue, module 6 version.
*
* Changes with respect to module 5:
* - register() no longer returns a mute boolean: it throws stating the REASON.
* - findByReference() no longer returns null: there are two methods with explicit
* contracts, one tolerant and one demanding.
*
* Note: standard exceptions are still used here. In 06-04 they are replaced by
* BiblioTech's own hierarchy, which will also carry the error's data.
*/
public class Catalog {
private final List<Material> materials = new ArrayList<>();
private final Map<String, Material> indexByReference = new HashMap<>();
private final Set<String> registeredIsbns = new HashSet<>();
/**
* Registers a material in the catalogue.
*
* @param material non-null material whose reference is not yet registered
* @throws NullPointerException if the material is null
* @throws IllegalArgumentException if the reference or the ISBN already exist
*/
public void register(Material material) {
Objects.requireNonNull(material, "The material to register cannot be null");
String reference = material.getReference();
if (indexByReference.containsKey(reference)) {
throw new IllegalArgumentException(
"A material with the reference " + reference + " already exists: '"
+ indexByReference.get(reference).getTitle() + "'");
}
if (material instanceof Book book) {
String isbn = book.getIsbn();
if (registeredIsbns.contains(isbn)) {
throw new IllegalArgumentException(
"The ISBN " + isbn + " is already catalogued (reference "
+ reference + ")");
}
registeredIsbns.add(isbn);
}
materials.add(material);
indexByReference.put(reference, material);
}
/**
* Looks for a material REQUIRING that it exists.
*
* It is the version used when absence is an error: lending,
* returning, consulting the card of something the user claims to have.
*
* @throws NoSuchElementException if no material with that reference exists
*/
public Material getByReference(String reference) {
Objects.requireNonNull(reference, "The reference cannot be null");
Material found = indexByReference.get(reference);
if (found == null) {
throw new NoSuchElementException(
"There is no material with the reference '" + reference
+ "'. The catalogue has " + materials.size() + " materials.");
}
return found; // NEVER null: guaranteed by the contract
}
/**
* Looks for a material ALLOWING that it does not exist.
*
* It is the version for when absence is a normal result: checking
* whether a reference is free before registering it.
*
* Returns Optional instead of null: the caller cannot ignore the
* "not found" case by carelessness. Optional is developed in 10-04.
*/
public Optional<Material> findByReference(String reference) {
if (reference == null) {
return Optional.empty();
}
return Optional.ofNullable(indexByReference.get(reference));
}
public boolean exists(String reference) {
return reference != null && indexByReference.containsKey(reference);
}
public int size() {
return materials.size();
}
public List<Material> list() {
return List.copyOf(materials); // immutable copy (03-07)
}
}The key decision is having two methods with two contracts instead of one ambiguous method:
| Method | Contract | When to use it |
|---|---|---|
getByReference |
Never returns null; throws if it does not exist |
Absence is an error: lending, returning |
findByReference |
Returns Optional, never null |
Absence is normal: checking availability |
It is a pattern you will see all over the JDK and all over the frameworks: the get/find, require/optional pairing. What disappears for ever is null as a return value.
And now the joint use, with Employee also validating its invariants:
package com.nexussoftware.bibliotech.domain;
import java.util.Objects;
/** Nexus Software employee entitled to borrow. */
public class Employee {
public static final int MAX_CONCURRENT_LOANS = 3;
private final String name;
private final String identifier; // format EMP-NNN
private int totalLoans;
public Employee(String name, String identifier) {
this.name = Objects.requireNonNull(name, "The name cannot be null");
this.identifier = Objects.requireNonNull(identifier,
"The identifier cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("The name cannot be blank");
}
if (!identifier.matches("EMP-\\d{3}")) {
throw new IllegalArgumentException(
"The identifier must have the format EMP-NNN, and it was: '"
+ identifier + "'");
}
this.totalLoans = 0;
}
public boolean canBorrow() {
return totalLoans < MAX_CONCURRENT_LOANS;
}
/**
* Records one more loan.
*
* @throws IllegalStateException if the limit has already been reached. It is IllegalState
* and not IllegalArgument because the problem is not what is passed
* (there are no arguments), but the STATE of the employee.
*/
public void registerLoan() {
if (!canBorrow()) {
throw new IllegalStateException(
"The employee " + identifier + " (" + name + ") already has "
+ totalLoans + " loans, the maximum is "
+ MAX_CONCURRENT_LOANS);
}
totalLoans++;
}
/**
* Records a return.
*
* @throws IllegalStateException if there are no loans to return.
* This check protects the INVARIANT "totalLoans >= 0",
* whose violation was exactly the root cause of the stack trace
* you analysed in exercise 2 of 06-01.
*/
public void registerReturn() {
if (totalLoans <= 0) {
throw new IllegalStateException(
"The employee " + identifier + " has no loans to return");
}
totalLoans--;
}
public String getName() { return name; }
public String getIdentifier() { return identifier; }
public int getTotalLoans() { return totalLoans; }
@Override
public String toString() {
return identifier + " (" + name + "): " + totalLoans + "/"
+ MAX_CONCURRENT_LOANS + " loans";
}
}And a demonstration of it all together:
package com.nexussoftware.bibliotech.presentation;
import java.util.NoSuchElementException;
import com.nexussoftware.bibliotech.domain.Employee;
import com.nexussoftware.bibliotech.domain.Book;
import com.nexussoftware.bibliotech.service.Catalog;
public class ValidationDemo {
public static void main(String[] args) {
Catalog catalog = new Catalog();
// --- Correct registration ---
catalog.register(new Book("BK-0001", "Effective Java", "Bloch", 2018, "978-0000000001"));
catalog.register(new Book("BK-0002", "Design Patterns", "GoF", 1994, "978-0000000002"));
System.out.println("Catalogue: " + catalog.size() + " materials");
// --- Duplicate reference ---
attempt("duplicate reference", () ->
catalog.register(new Book("BK-0001", "Other", "Other", 2020, "978-0000000009")));
// --- Duplicate ISBN ---
attempt("duplicate ISBN", () ->
catalog.register(new Book("BK-0003", "Copy", "Bloch", 2018, "978-0000000001")));
// --- Null material ---
attempt("null material", () -> catalog.register(null));
// --- Invalid year in Book's constructor ---
attempt("invalid year", () ->
new Book("BK-0004", "Manuscript", "Anonymous", 1200, "978-0000000004"));
// --- Demanding search for something that does not exist ---
attempt("non-existent reference", () -> catalog.getByReference("BK-9999"));
// --- Tolerant search: no exception, no null ---
System.out.println("\nTolerant search for BK-9999: "
+ catalog.findByReference("BK-9999").isPresent());
System.out.println("Tolerant search for BK-0001: "
+ catalog.findByReference("BK-0001")
.map(m -> m.getTitle())
.orElse("(not found)"));
// --- Employee loan limit ---
Employee marta = new Employee("Marta Ruiz", "EMP-001");
for (int i = 1; i <= 3; i++) { marta.registerLoan(); }
System.out.println("\n" + marta);
attempt("fourth loan", marta::registerLoan);
// --- Identifier with an invalid format ---
attempt("invalid identifier", () -> new Employee("Diego Alonso", "diego"));
}
/** Runs an action and shows the failure in a readable way. */
private static void attempt(String description, Runnable action) {
try {
action.run();
System.out.println("[" + description + "] -> did not fail (unexpected)");
} catch (NullPointerException | IllegalArgumentException
| IllegalStateException | NoSuchElementException e) {
System.out.println("[" + description + "] -> "
+ e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}Output:
Catalogue: 2 materials [duplicate reference] -> IllegalArgumentException: A material with the reference BK-0001 already exists: 'Effective Java' [duplicate ISBN] -> IllegalArgumentException: The ISBN 978-0000000001 is already catalogued (reference BK-0003) [null material] -> NullPointerException: The material to register cannot be null [invalid year] -> IllegalArgumentException: The publication year must be between 1450 and 2100, and it was: 1200 [non-existent reference] -> NoSuchElementException: There is no material with the reference 'BK-9999'. The catalogue has 2 materials. Tolerant search for BK-9999: false Tolerant search for BK-0001: Effective Java EMP-001 (Marta Ruiz): 3/3 loans [fourth loan] -> IllegalStateException: The employee EMP-001 (Marta Ruiz) already has 3 loans, the maximum is 3 [invalid identifier] -> IllegalArgumentException: The identifier must have the format EMP-NNN, and it was: 'diego'
Compare with what module 5 produced: seven indistinguishable false and seven indistinguishable null. Now every failure has a name, a reason and the offending data.
One visible annoyance remains in the demonstration: that catch with four standard types joined by |. It is ugly, and it is a symptom. IllegalArgumentException means very different things depending on the case —duplicate reference, duplicate ISBN, year out of range— and whoever catches cannot tell them apart without reading the message, which is free text and fragile. Besides, none of these exceptions carries data: to find out which reference was duplicated you have to parse a string.
The answer is the next lesson.
Common Mistakes and Tips
Confusing throw with throws. throw throws, in the body; throws declares, in the signature. One letter apart, opposite roles.
Silently correcting instead of rejecting. Replacing an invalid year with 0 or an invalid reference with LN-0000 produces objects that exist and are wrong, and in BiblioTech it caused loans to overwrite each other in the index. Reject.
Validating the format before checking for null. reference.matches(...) with reference set to null throws a NullPointerException without your message. Nulls first.
Forgetting the cause when wrapping. It is the most expensive mistake in this module. You lose the type of the real failure, the offending data, the exact line and the whole stack. And it is not recoverable. The cost of avoiding it is five characters: , e.
Using IllegalArgumentException for everything. Distinguish: null → NullPointerException; invalid value → IllegalArgumentException; inappropriate moment → IllegalStateException. Whoever catches will be able to react differently.
Declaring throws Exception in an interface. It fixes the maximum for every implementation and forces callers to catch the broadest possible type. Declare the most specific type, or none.
Putting throws for unchecked exceptions in the signature. Legal, but it adds no checking and it clutters. Document with @throws in the Javadoc, where you can also explain the condition.
main(String[] args) throws Exception in a real application. The end user gets a raw stack trace. Acceptable in an example or an internal tool; in production you need an error boundary (06-07).
Rethrowing without adding anything. A catch that only does throw e; is noise: without the try, the result would be identical. Delete it.
Returning null when something is not found. It delays the failure to a distant point in the program and makes it unrecognisable. Offer two methods: one that throws and one that returns Optional.
Tip: write the message thinking of whoever will read it in six months at three in the morning. What happened, with what data and what was expected. "Year out of range [1450..2100]: 1200" costs the same as "Invalid year".
Tip: do not include sensitive data in the messages. Passwords, tokens, ID numbers. The message will end up in a log, in an automated email and maybe on somebody's screen. In 06-07 there is a specific warning about this.
Tip: extract long validations into named private methods. validateIsbn(isbn) documents better than eight lines of if inside the constructor, and it is reused in the setters.
Tip: Objects.requireNonNull with the message, always. The message-less version throws a NullPointerException that does not say what was null, precisely the information you need.
Exercises
Exercise 1: Book with complete validation
Rewrite BiblioTech's Book class removing the module 3 console warnings entirely and replacing them with exceptions. Requirements:
- Fields:
reference(BK-NNNN),title,author,publicationYear,isbn(978-NNNNNNNNNNor979-...), and theavailablestate. - Nulls →
NullPointerExceptionwithObjects.requireNonNulland a message identifying the field. - Blank text, incorrect formats and a year outside
[1450, 2100]→IllegalArgumentExceptionwith the received value in the message. - Methods
lend()andreturnItem():lend()on an already lent material andreturnItem()on an available one →IllegalStateException. - Extract each validation into a private static method with a descriptive name.
- Add Javadoc with
@throwson the constructor and on the two state methods. - Write a
maindemonstrating each of the validations failing, and one correct construction.
Reflect at the end in a comment: why is lend() on an already lent material an IllegalStateException and not an IllegalArgumentException?
Exercise 2: chaining and loss of information
Write ChainingDemo with two versions of the same service method, loadCardWell and loadCardBadly, which parse a line title;author;year and build a record Card(String title, String author, int year):
- The good version wraps any
RuntimeExceptionin anIllegalStateExceptionpreserving the cause. - The bad version does the same without the cause.
In main:
- Call both with the line
"Effective Java;Bloch;twenty eighteen", catch the exception and show both complete stack traces, one after the other, with a separator. - Write a method
analyse(Throwable t)that walks the chain of causes and reports: the number of links, the class of the root cause, the message of the root cause and the first line of your own code in the root cause. - Apply it to both exceptions and show, in a table printed to the console, what information is available in each case.
- Add a third case with the line
"Only one field"to check that theArrayIndexOutOfBoundsExceptionis preserved too.
Exercise 3: LoanManager with propagation through layers
Write LoanManager in com.nexussoftware.bibliotech.service with the method Loan lend(String materialReference, String employeeId, int day), which orchestrates Catalog, an employee registry and LoanRegistry.
Requirements:
- It catches nothing it does not know how to handle. The exceptions of
CatalogandEmployeego up as they are, except when they need translating. - It validates its own arguments with fail-fast: references neither null nor blank,
day >= 1. - It generates the loan reference with the format
LN-NNNNfrom a counter. - If the material exists but is not available, it throws
IllegalStateExceptionwith a message including the reference and the title. - If the employee has reached the limit, it lets
Employee'sIllegalStateExceptiongo up, but wraps it in anIllegalStateExceptionadding the context of the operation (which material was being lent), preserving the cause. - Add
void returnItem(String loanReference, int day)that throwsNoSuchElementExceptionif the reference does not exist, and letsLoan'sIllegalStateExceptiongo up if it had already been returned.
In main, set up a scenario with the project's three employees and three books, and provoke at least five different failures, showing for each one the class, the message and —if there is one— the cause.
Solutions
Solution 1
package com.nexussoftware.bibliotech.domain;
import java.util.Objects;
/**
* Book in BiblioTech's catalogue.
*
* Module 6 version: it REJECTS invalid data instead of correcting it.
* Every instance that exists satisfies its invariants by construction.
*/
public class Book {
private static final int MIN_YEAR = 1450; // Gutenberg
private static final int MAX_YEAR = 2100;
private final String reference;
private final String title;
private final String author;
private final int publicationYear;
private final String isbn;
private boolean available = true;
/**
* Creates a book validating all its invariants.
*
* @param reference catalogue reference, format BK-NNNN
* @param title non-empty title
* @param author non-empty author
* @param publicationYear year between 1450 and 2100
* @param isbn ISBN-13, format 978-NNNNNNNNNN or 979-NNNNNNNNNN
* @throws NullPointerException if any text argument is null
* @throws IllegalArgumentException if any value fails its format or range
*/
public Book(String reference, String title, String author,
int publicationYear, String isbn) {
// 1. Nulls first: otherwise the matches() below would throw an NPE
// with no useful message.
Objects.requireNonNull(reference, "The reference cannot be null");
Objects.requireNonNull(title, "The title cannot be null");
Objects.requireNonNull(author, "The author cannot be null");
Objects.requireNonNull(isbn, "The ISBN cannot be null");
// 2. Formats and ranges
validateReference(reference);
validateNonEmptyText(title, "title");
validateNonEmptyText(author, "author");
validateYear(publicationYear);
validateIsbn(isbn);
// 3. From here on everything is valid: clean assignment
this.reference = reference;
this.title = title.trim();
this.author = author.trim();
this.publicationYear = publicationYear;
this.isbn = isbn;
}
// ---------- Validations ----------
private static void validateReference(String reference) {
if (!reference.matches("BK-\\d{4}")) {
throw new IllegalArgumentException(
"The reference must have the format BK-NNNN, and it was: '" + reference + "'");
}
}
private static void validateNonEmptyText(String value, String fieldName) {
if (value.isBlank()) {
throw new IllegalArgumentException(
"The field '" + fieldName + "' cannot be empty nor contain only spaces");
}
}
private static void validateYear(int year) {
if (year < MIN_YEAR || year > MAX_YEAR) {
throw new IllegalArgumentException(
"The publication year must be between " + MIN_YEAR + " and " + MAX_YEAR
+ ", and it was: " + year);
}
}
private static void validateIsbn(String isbn) {
if (!isbn.matches("97[89]-\\d{10}")) {
throw new IllegalArgumentException(
"The ISBN must have the format 978-NNNNNNNNNN or 979-NNNNNNNNNN, and it was: '"
+ isbn + "'");
}
}
// ---------- Loan state ----------
/**
* Marks the book as lent.
*
* @throws IllegalStateException if it was already lent
*/
public void lend() {
if (!available) {
throw new IllegalStateException(
"The book " + reference + " ('" + title + "') is already on loan");
}
available = false;
}
/**
* Marks the book as returned.
*
* @throws IllegalStateException if it was not on loan
*/
public void returnItem() {
if (available) {
throw new IllegalStateException(
"The book " + reference + " ('" + title + "') is not on loan");
}
available = true;
}
// ---------- Accessors ----------
public String getReference() { return reference; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
public int getPublicationYear() { return publicationYear; }
public String getIsbn() { return isbn; }
public boolean isAvailable() { return available; }
@Override
public String toString() {
return reference + " - '" + title + "' (" + author + ", " + publicationYear + ") "
+ (available ? "[available]" : "[on loan]");
}
// ---------- Demonstration ----------
public static void main(String[] args) {
System.out.println("=== Correct construction ===");
Book valid = new Book("BK-0001", "Effective Java", "Bloch", 2018, "978-0000000001");
System.out.println(valid);
System.out.println("\n=== Validations that fail ===");
check("null reference",
() -> new Book(null, "T", "A", 2000, "978-0000000001"));
check("null title",
() -> new Book("BK-0002", null, "A", 2000, "978-0000000001"));
check("null isbn",
() -> new Book("BK-0002", "T", "A", 2000, null));
check("badly formatted reference",
() -> new Book("B-1", "T", "A", 2000, "978-0000000001"));
check("blank title",
() -> new Book("BK-0002", " ", "A", 2000, "978-0000000001"));
check("blank author",
() -> new Book("BK-0002", "T", "", 2000, "978-0000000001"));
check("year too old",
() -> new Book("BK-0002", "T", "A", 1200, "978-0000000001"));
check("year in the distant future",
() -> new Book("BK-0002", "T", "A", 3000, "978-0000000001"));
check("badly formatted isbn",
() -> new Book("BK-0002", "T", "A", 2000, "12345"));
System.out.println("\n=== State ===");
valid.lend();
System.out.println(valid);
check("lend twice", valid::lend);
valid.returnItem();
System.out.println(valid);
check("return twice", valid::returnItem);
}
private static void check(String description, Runnable action) {
try {
action.run();
System.out.println(" [" + description + "] did NOT fail (unexpected)");
} catch (RuntimeException e) {
System.out.println(" [" + description + "] "
+ e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
/*
* Why is lend() on an already lent book an IllegalStateException
* and not an IllegalArgumentException?
*
* Because lend() HAS NO ARGUMENTS. There is nothing the caller passed
* wrongly. The problem is the STATE of the object: this very book, at another
* moment (after returning it), would accept exactly the same call
* without a problem.
*
* The rule:
* - IllegalArgumentException: the problem is WHAT YOU PASS ME.
* - IllegalStateException : the problem is WHEN YOU ASK ME.
*
* This distinction is not cosmetic: it lets whoever catches react in
* different ways. Faced with an IllegalArgumentException the input data is
* corrected; faced with an IllegalStateException the state is consulted or
* changed before retrying (for example, putting the material in the reservation queue).
*/Output (abbreviated):
=== Correct construction ===
BK-0001 - 'Effective Java' (Bloch, 2018) [available]
=== Validations that fail ===
[null reference] NullPointerException: The reference cannot be null
[null title] NullPointerException: The title cannot be null
[null isbn] NullPointerException: The ISBN cannot be null
[badly formatted reference] IllegalArgumentException: The reference must have the format BK-NNNN, and it was: 'B-1'
[blank title] IllegalArgumentException: The field 'title' cannot be empty nor contain only spaces
[year too old] IllegalArgumentException: The publication year must be between 1450 and 2100, and it was: 1200
[badly formatted isbn] IllegalArgumentException: The ISBN must have the format 978-NNNNNNNNNN or 979-NNNNNNNNNN, and it was: '12345'
=== State ===
BK-0001 - 'Effective Java' (Bloch, 2018) [on loan]
[lend twice] IllegalStateException: The book BK-0001 ('Effective Java') is already on loan
BK-0001 - 'Effective Java' (Bloch, 2018) [available]
[return twice] IllegalStateException: The book BK-0001 ('Effective Java') is not on loanSolution 2
package com.nexussoftware.bibliotech.presentation;
/**
* Demonstrates, with two stack traces side by side, exactly what information
* is lost when wrapping an exception without preserving the cause.
*/
public class ChainingDemo {
public record Card(String title, String author, int year) { }
// ---------- Data layer ----------
/** Parses a line. Can throw several different RuntimeExceptions. */
private static Card parse(String line) {
String[] fields = line.split(";");
String title = fields[0].trim(); // ArrayIndexOutOfBounds if missing
String author = fields[1].trim(); // likewise
int year = Integer.parseInt(fields[2].trim()); // NumberFormat if not a number
return new Card(title, author, year);
}
// ---------- Service layer: two versions ----------
/** GOOD: wraps preserving the cause. */
public static Card loadCardWell(String line) {
try {
return parse(line);
} catch (RuntimeException e) {
throw new IllegalStateException("Could not load the card for: '" + line + "'", e);
}
}
/** BAD: wraps DISCARDING the cause. */
public static Card loadCardBadly(String line) {
try {
return parse(line);
} catch (RuntimeException e) {
throw new IllegalStateException("Could not load the card for: '" + line + "'");
// ^ , e missing
}
}
// ---------- Analysis of the chain of causes ----------
public record Analysis(int links, String rootClass, String rootMessage, String rootLine) {
public String row(String label) {
return String.format("%-16s | %-9d | %-28s | %-38s | %s",
label, links, rootClass, rootMessage, rootLine);
}
}
public static Analysis analyse(Throwable t) {
int links = 1;
Throwable root = t;
// Walk to the root cause, with anti-circular protection
while (root.getCause() != null && root.getCause() != root && links < 20) {
root = root.getCause();
links++;
}
String rootLine = "(not available)";
for (StackTraceElement f : root.getStackTrace()) {
if (f.getClassName().startsWith("com.nexussoftware")) {
rootLine = f.getMethodName() + ":" + f.getLineNumber();
break;
}
}
String message = (root.getMessage() != null) ? root.getMessage() : "(no message)";
return new Analysis(links, root.getClass().getSimpleName(), message, rootLine);
}
// ---------- Demonstration ----------
public static void main(String[] args) {
String badLine = "Effective Java;Bloch;twenty eighteen";
String shortLine = "Only one field";
System.out.println("############ STACK TRACE WITH CAUSE ############");
IllegalStateException withCause = capture(() -> loadCardWell(badLine));
withCause.printStackTrace(System.out);
System.out.println("\n############ STACK TRACE WITHOUT CAUSE ############");
IllegalStateException withoutCause = capture(() -> loadCardBadly(badLine));
withoutCause.printStackTrace(System.out);
System.out.println("\n############ CASE: MISSING FIELDS ############");
IllegalStateException fields = capture(() -> loadCardWell(shortLine));
fields.printStackTrace(System.out);
System.out.println("\n############ COMPARISON ############");
System.out.printf("%-16s | %-9s | %-28s | %-38s | %s%n",
"VERSION", "LINKS", "ROOT CLASS", "ROOT MESSAGE", "ROOT LINE");
System.out.println("-".repeat(130));
System.out.println(analyse(withCause).row("With cause"));
System.out.println(analyse(withoutCause).row("Without cause"));
System.out.println(analyse(fields).row("Missing fields"));
System.out.println("""
CONCLUSION
----------
With cause: we know what failed (NumberFormatException), with what data
("twenty eighteen") and on exactly which line of code.
Without cause: we only know that "the card could not be loaded". The five
possible causes (a field missing, non-numeric year, empty title,
empty author, year out of range) produce EXACTLY the same
dump. And that information cannot be recovered: the original
exception object was discarded and the collector took it away.
Cost of avoiding it: five characters, ", e".""");
}
/** Runs an action expected to fail and returns the exception. */
private static IllegalStateException capture(Runnable action) {
try {
action.run();
throw new AssertionError("It was expected to fail");
} catch (IllegalStateException e) {
return e;
}
}
}Output (abbreviated):
############ STACK TRACE WITH CAUSE ############ java.lang.IllegalStateException: Could not load the card for: 'Effective Java;Bloch;twenty eighteen' at ...ChainingDemo.loadCardWell(ChainingDemo.java:28) at ...ChainingDemo.lambda$main$0(ChainingDemo.java:78) ... Caused by: java.lang.NumberFormatException: For input string: "twenty eighteen" at java.base/java.lang.Integer.parseInt(Integer.java:665) at ...ChainingDemo.parse(ChainingDemo.java:18) at ...ChainingDemo.loadCardWell(ChainingDemo.java:26) ... 4 more ############ STACK TRACE WITHOUT CAUSE ############ java.lang.IllegalStateException: Could not load the card for: 'Effective Java;Bloch;twenty eighteen' at ...ChainingDemo.loadCardBadly(ChainingDemo.java:38) at ...ChainingDemo.lambda$main$1(ChainingDemo.java:82) ... ############ COMPARISON ############ VERSION | LINKS | ROOT CLASS | ROOT MESSAGE | ROOT LINE -------------------------------------------------------------------------------------------------------------- With cause | 2 | NumberFormatException | For input string: "twenty eighteen" | parse:18 Without cause | 1 | IllegalStateException | Could not load the card for: 'Effe... | loadCardBadly:38 Missing fields | 2 | ArrayIndexOutOfBoundsException| Index 1 out of bounds for length 1 | parse:17
The table sums up the damage: in the version without a cause, the "root class" is the wrapping exception itself and the "root line" points at the line of the throw, which is where the problem was hidden, not where it happened.
Solution 3
package com.nexussoftware.bibliotech.service;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import com.nexussoftware.bibliotech.domain.Employee;
import com.nexussoftware.bibliotech.domain.Book;
import com.nexussoftware.bibliotech.domain.Loan;
/**
* Orchestrates loans and returns between the catalogue, the employees and the registry.
*
* Error policy of this class:
* - Validates its own arguments with fail-fast.
* - Lets exceptions that are already clear go up untouched (material not found).
* - WRAPS, preserving the cause, when it can add useful context
* (which operation was being attempted).
* - Catches NOTHING it does not know how to handle.
*/
public class LoanManager {
private final Catalog catalog;
private final Map<String, Employee> employees = new HashMap<>();
private final Map<String, Loan> loans = new HashMap<>();
private int loanCounter = 0;
public LoanManager(Catalog catalog) {
this.catalog = Objects.requireNonNull(catalog, "The catalogue cannot be null");
}
public void enrol(Employee employee) {
Objects.requireNonNull(employee, "The employee cannot be null");
if (employees.containsKey(employee.getIdentifier())) {
throw new IllegalArgumentException(
"An employee with identifier " + employee.getIdentifier() + " already exists");
}
employees.put(employee.getIdentifier(), employee);
}
/**
* Lends a material to an employee.
*
* @throws NullPointerException if any reference is null
* @throws IllegalArgumentException if any reference is blank or the day is invalid
* @throws NoSuchElementException if the material or the employee do not exist
* @throws IllegalStateException if the material is not available or the employee
* has reached their loan limit
*/
public Loan lend(String materialReference, String employeeId, int day) {
// --- 1. Argument validation: fail fast ---
Objects.requireNonNull(materialReference, "The material reference cannot be null");
Objects.requireNonNull(employeeId, "The employee identifier cannot be null");
if (materialReference.isBlank()) {
throw new IllegalArgumentException("The material reference cannot be blank");
}
if (employeeId.isBlank()) {
throw new IllegalArgumentException("The employee identifier cannot be blank");
}
if (day < 1) {
throw new IllegalArgumentException("The day must be 1 or later, and it was: " + day);
}
// --- 2. Locate. Nothing is caught: getByReference already throws a
// NoSuchElementException with a perfect message. Wrapping it
// would add nothing, so it is left to go up as it is. ---
Book material = (Book) catalog.getByReference(materialReference);
Employee employee = employees.get(employeeId);
if (employee == null) {
throw new NoSuchElementException(
"There is no employee with identifier '" + employeeId
+ "'. Enrolled: " + employees.keySet());
}
// --- 3. Business rules checkable in advance ---
if (!material.isAvailable()) {
throw new IllegalStateException(
"The material " + materialReference + " ('" + material.getTitle()
+ "') is not available");
}
// --- 4. HERE we do wrap: Employee's exception does not know what
// was being lent. We add that context preserving
// the cause, so that the Caused by: keeps the detail. ---
String loanReference = nextReference();
Loan loan;
try {
loan = new Loan(loanReference, material, employee, day);
} catch (IllegalStateException e) {
throw new IllegalStateException(
"Could not lend " + materialReference + " ('" + material.getTitle()
+ "') to " + employeeId + " on day " + day, e);
}
loans.put(loanReference, loan);
return loan;
}
/**
* Registers the return of a loan.
*
* @throws NoSuchElementException if the loan reference does not exist
* @throws IllegalStateException if the loan had already been returned (comes up from Loan)
*/
public void returnItem(String loanReference, int day) {
Objects.requireNonNull(loanReference, "The loan reference cannot be null");
Loan loan = loans.get(loanReference);
if (loan == null) {
throw new NoSuchElementException(
"There is no loan with reference '" + loanReference
+ "'. Registered: " + loans.keySet());
}
// Not caught: if it had already been returned, Loan's IllegalStateException
// already states the reference and the day. Wrapping it would only add noise.
loan.registerReturn(day);
loan.getBorrower().registerReturn();
}
private String nextReference() {
loanCounter++;
return String.format("LN-%04d", loanCounter);
}
public int activeLoans() {
int active = 0;
for (Loan l : loans.values()) {
if (!l.isReturned()) { active++; }
}
return active;
}
// ------------------------------------------------------------------
public static void main(String[] args) {
Catalog catalog = new Catalog();
catalog.register(new Book("BK-0001", "Effective Java", "Bloch", 2018, "978-0000000001"));
catalog.register(new Book("BK-0002", "Design Patterns", "GoF", 1994, "978-0000000002"));
catalog.register(new Book("BK-0003", "Refactoring", "Fowler", 1999, "978-0000000003"));
LoanManager manager = new LoanManager(catalog);
manager.enrol(new Employee("Marta Ruiz", "EMP-001"));
manager.enrol(new Employee("Diego Alonso", "EMP-002"));
manager.enrol(new Employee("Nuria Vidal", "EMP-003"));
System.out.println("=== Correct loans ===");
System.out.println(manager.lend("BK-0001", "EMP-001", 10).getReference());
System.out.println(manager.lend("BK-0002", "EMP-001", 10).getReference());
System.out.println("Active: " + manager.activeLoans());
System.out.println("\n=== Failures ===");
fail("1. Non-existent material", () -> manager.lend("BK-9999", "EMP-001", 12));
fail("2. Non-existent employee", () -> manager.lend("BK-0003", "EMP-999", 12));
fail("3. Material already lent", () -> manager.lend("BK-0001", "EMP-002", 12));
fail("4. Invalid day", () -> manager.lend("BK-0003", "EMP-002", 0));
fail("5. Null reference", () -> manager.lend(null, "EMP-002", 12));
// 6. Loan limit: Marta already has 2, the third goes through and the fourth does not
manager.lend("BK-0003", "EMP-001", 12);
catalog.register(new Book("BK-0004", "Clean Code", "Martin", 2008, "978-0000000004"));
fail("6. Limit exceeded", () -> manager.lend("BK-0004", "EMP-001", 12));
System.out.println("\n=== Returns ===");
manager.returnItem("LN-0001", 30);
System.out.println("LN-0001 returned. Active: " + manager.activeLoans());
fail("7. Return twice", () -> manager.returnItem("LN-0001", 31));
fail("8. Non-existent loan", () -> manager.returnItem("LN-9999", 31));
}
private static void fail(String description, Runnable action) {
try {
action.run();
System.out.println(description + " -> did NOT fail (unexpected)");
} catch (RuntimeException e) {
System.out.println(description + " -> " + e.getClass().getSimpleName()
+ ": " + e.getMessage());
if (e.getCause() != null) {
System.out.println(" Cause: " + e.getCause().getClass().getSimpleName()
+ ": " + e.getCause().getMessage());
}
}
}
}Output:
=== Correct loans ===
LN-0001
LN-0002
Active: 2
=== Failures ===
1. Non-existent material -> NoSuchElementException: There is no material with the reference 'BK-9999'. The catalogue has 3 materials.
2. Non-existent employee -> NoSuchElementException: There is no employee with identifier 'EMP-999'. Enrolled: [EMP-001, EMP-002, EMP-003]
3. Material already lent -> IllegalStateException: The material BK-0001 ('Effective Java') is not available
4. Invalid day -> IllegalArgumentException: The day must be 1 or later, and it was: 0
5. Null reference -> NullPointerException: The material reference cannot be null
6. Limit exceeded -> IllegalStateException: Could not lend BK-0004 ('Clean Code') to EMP-001 on day 12
Cause: IllegalStateException: The employee EMP-001 (Marta Ruiz) already has 3 loans, the maximum is 3
=== Returns ===
LN-0001 returned. Active: 2
7. Return twice -> IllegalStateException: The loan LN-0001 was already returned on day 30
8. Non-existent loan -> NoSuchElementException: There is no loan with reference 'LN-9999'. Registered: [LN-0001, LN-0002, LN-0003]Look at case 6, which is the one illustrating the exercise's central design decision. Employee's exception said "EMP-001 already has 3 loans", correct but incomplete information: it does not say what was being attempted. By wrapping it, the message above supplies the operation (lend BK-0004 to EMP-001 on day 12) and the Caused by: preserves the detail. The two pieces together are a complete diagnosis.
And compare with cases 1 and 3, where it was not wrapped: the messages from Catalog and from the manager itself were already complete, so adding a layer would only have lengthened the stack trace without contributing anything. Wrapping as a matter of course is as bad as never wrapping: you do it when you add context the lower layer could not know.
One pending problem stands out in this code: the catch (IllegalStateException e) in section 4 catches any IllegalStateException coming out of Loan's constructor, both the loan-limit one and the material-not-available one. It cannot tell them apart without reading the message. And that (Book) cast is just as fragile. Both things are solved with the domain's own exceptions, which is the next lesson.
Conclusion
You now know how to signal a failure, not just catch one. You have mastered throw for throwing explicitly —with the certainty that it ends the method instantly and that the compiler rejects subsequent code as unreachable— and throws for declaring in the signature, with the catch or specify rule it imposes on the caller: catch if it knows what to do, declare if the decision belongs to someone with more context. And you have seen that propagation chain working across four levels, where the three deep ones detect and report and only main decides, because only main knows that BiblioTech can start with an empty catalogue.
You know that declaring unchecked exceptions in throws is legal but optional, and that the recommended practice is to document them with @throws in the Javadoc, where you can explain under what condition they are thrown.
You have the standard vocabulary of argument validation and the rule that organises it: null → NullPointerException with Objects.requireNonNull and its message; invalid value → IllegalArgumentException; inappropriate moment → IllegalStateException. With the distinction that is hardest and pays best: the problem is what you pass me versus the problem is when you ask me. You know to validate nulls first, to use the Supplier variant when the message is expensive to build, and to extract long validations into named private methods.
You understand the fail-fast principle: check the preconditions at the start, before touching anything, so that the error appears close to its cause, the rest of the method can trust and —most importantly— the object never comes to exist in an invalid state. It is what turns the invariants of 03-07 from an aspiration into a guarantee.
And you have mastered exception chaining, which is the technique separating a diagnosable system from one that is not. You know that the cause is passed in the two-argument constructor —new IllegalStateException(message, e)— or, in old cases, with initCause. You have seen exactly what disappears when it is forgotten: the type of the real failure, the offending data, the line of code and the whole stack of the original point, information that is never recovered. Five characters, , e, mark the difference between a thirty-second diagnosis and an afternoon of guesswork.
You distinguish rethrowing with throw e —same object, same trace, to add context or log— from wrapping, to change the vocabulary; and you know that wrapping as a matter of course is as bad as never wrapping: you do it when you contribute context the lower layer could not know. You know the translation between layers that stops a layer leaking the exceptions of its internal implementation, the same principle Spring applies when converting SQLException into its own hierarchy. And you have two fine details: Java 7's precise rethrow analysis, which allows throw e from a catch (Exception e) without widening the throws as long as you do not reassign the parameter; and the overriding rule, which stops a subclass declaring broader checked exceptions than the superclass —one more weighty reason to prefer unchecked ones in domain interfaces.
BiblioTech has paid both its debts. The constructors of Book, Employee and Loan no longer print warnings nor invent default values: they reject invalid data, so there are no longer loans sharing the reference LN-0000 nor books published in year 0. Catalog.register throws stating the exact reason instead of an indistinguishable false, and findByReference has disappeared in its old form: in its place there are two methods with explicit contracts, getByReference —which never returns null and throws if it does not find anything— and findByReference, which returns Optional. null as a return value has been eliminated from the catalogue. And LoanManager orchestrates the three pieces wrapping only where it adds context.
But the demonstration code itself has left the next problem in plain sight: that catch with four standard types joined by |, and that catch (IllegalStateException e) that cannot tell "the material is not available" from "the employee has reached their limit" without reading a free-text message. IllegalArgumentException is being used for three different things, none of these exceptions carries data —to find out which reference was duplicated you have to parse a string— and nothing lets you catch "any BiblioTech failure" in one go.
That is the next lesson, Custom Exceptions: why create your own exceptions to name the failure in the domain's language and carry structured data; how they are created by extending Exception or RuntimeException and the criterion for choosing; the four canonical constructors and why it is worth offering them all; how to add fields and accessors —getReference(), getLimit()— so that whoever catches can decide instead of reading text; and the design of a domain hierarchy with a BiblioTechException root and its descendants MaterialNotFoundException, DuplicateReferenceException, LoanLimitExceededException, MaterialNotAvailableException and LoanAlreadyReturnedException, which will let you catch the whole domain in one go. With the naming and message conventions, the criterion for knowing when not to create your own exception, and an advanced note about fillInStackTrace for very frequent control exceptions.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
