عرض عملي: InsufficientFundsException من البداية إلى النهاية
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
This is a hands-on demo. Over the last five lessons you learned the pieces: throwing, declaring, catching, and writing your own exception. Now you will use them together, in one small program, in the order a working developer does: define the exception, throw it from Account.withdraw, meet the compiler's complaint when a caller does nothing about it, and handle it so the program tells the user what happened and carries on.
This prepares the lab, Bank with rules, where you build the same thing yourself. Follow along in your own project and type the code rather than pasting it. The editor and project steps depend on your setup (IntelliJ IDEA, another IDE, or the command line), so we describe them generally and give exact commands only for the plain javac route.
Why this matters: this pattern, a rule enforced in a domain class, reported through a named exception, and handled at the edge, is the same shape as a real banking or ordering service, only smaller.
This lesson builds on lessons 3 to 5 of this module, on the Account class from the object-oriented modules (BEG-020 to BEG-022), and on encapsulation (BEG-021).
Learning Objectives
By the end of this lesson you will be able to:
- Create a checked business exception with data and a helpful message.
- Enforce a business rule in
Account.withdrawusing that exception, and declare it withthrows. - Explain the compiler error you see when the exception is not handled.
- Handle it in
mainwith a narrowcatchand a message the user can act on. - Confirm that a refused withdrawal leaves the balance unchanged.
- Separate "bug" exceptions (
IllegalArgumentException) from "business" exceptions.
Written Explanation
The plan
We build four small pieces, in this order:
InsufficientFundsException: the named problem, with data.Account: enforces the rule.- A first
mainthat forgets to handle it, so you see the compiler stop you. - A second
mainthat handles it properly.
Keep one rule in mind throughout: checks come before state changes. A withdrawal that is refused must leave the balance exactly as it was.
Step 1: the exception
Create InsufficientFundsException.java in its own file. It extends Exception, so it is checked: a refused withdrawal is a normal, recoverable situation, and the compiler should make every caller decide what to do (lesson 5).
public class InsufficientFundsException extends Exception {
private final double requested;
private final double available;
public InsufficientFundsException(double requested, double available) {
super(String.format("Cannot withdraw %.2f: balance is only %.2f", requested, available));
this.requested = requested;
this.available = available;
}
public double getShortfall() {
return requested - available;
}
}The constructor builds the message from the data, so every place that throws it produces the same helpful text. There is no cause constructor: nothing lower-level triggers this exception, it is raised by our own rule check. (If it ever wraps another exception, add the (String, Throwable) form.) One detail: String.format with %.2f may print a comma instead of a decimal point on a computer set to some regional settings; the numbers stay the same.
Step 2: the rule in Account
public class Account {
private final String owner;
private double balance;
public Account(String owner, double openingBalance) {
if (openingBalance < 0) {
throw new IllegalArgumentException("Opening balance cannot be negative: " + openingBalance);
}
this.owner = owner;
this.balance = openingBalance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive, was " + amount);
}
balance += amount;
}
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal must be positive, was " + amount);
}
if (amount > balance) {
throw new InsufficientFundsException(amount, balance);
}
balance -= amount;
}
public double getBalance() {
return balance;
}
public String getOwner() {
return owner;
}
}Two kinds of failure live here, and they are deliberately different:
- A zero or negative amount is a bug in the caller:
IllegalArgumentException, unchecked, nothrows. - Asking for more than the balance is a business situation:
InsufficientFundsException, checked, declared withthrows.
Both checks are placed before the line balance -= amount;, so no failure can leave the object half-changed.
Step 3: the planned mistake
Now write a main that calls withdraw and does nothing about the exception:
public class Bad {
public static void main(String[] args) {
Account account = new Account("Sara", 100);
account.withdraw(500);
System.out.println("Balance: " + account.getBalance());
}
}Compile it. Whether you use an IDE or the command line, you see the same message. From the command line (javac Bad.java, with the other .java files in the same folder):
Bad.java:4: error: unreported exception InsufficientFundsException; must be caught or declared to be thrown
account.withdraw(500);
^
1 errorAn IDE usually underlines the call in red and offers quick fixes. Read the message, as it tells you exactly what the language wants: caught or declared. The compiler is doing its job: the method's throws clause is a promise, and every caller has to honor it.
There are two ways to make it compile. Declaring throws InsufficientFundsException on main would compile but only pushes the problem to the JVM, which would end the program with a trace. The right fix here is to catch it, because main is where we talk to the user.
Step 4: handle it
public class Main {
public static void main(String[] args) {
Account account = new Account("Sara", 100);
double[] requests = {30, 500, 70, 1};
for (double request : requests) {
try {
account.withdraw(request);
System.out.printf("Withdrew %.2f. Balance: %.2f%n", request, account.getBalance());
} catch (InsufficientFundsException e) {
System.out.println("Refused: " + e.getMessage());
System.out.printf("You need %.2f more. Balance unchanged: %.2f%n",
e.getShortfall(), account.getBalance());
}
}
}
}Real output:
Withdrew 30.00. Balance: 70.00
Refused: Cannot withdraw 500.00: balance is only 70.00
You need 430.00 more. Balance unchanged: 70.00
Withdrew 70.00. Balance: 0.00
Refused: Cannot withdraw 1.00: balance is only 0.00
You need 1.00 more. Balance unchanged: 0.00Examples
Example 1: the exception class on its own (simple)
Compile InsufficientFundsException.java and create one in a throwaway main: new InsufficientFundsException(500, 70). Its getMessage() returns Cannot withdraw 500.00: balance is only 70.00 and getShortfall() returns 430.0. Testing a small piece on its own, before the rest exists, is a good habit. If the message looks wrong here, you find out in ten seconds instead of in the middle of the bank.
Example 2: the compiler stops you (practical)
The Bad class from Step 3 failed with unreported exception InsufficientFundsException; must be caught or declared to be thrown. Fix it three ways and see the difference:
- Catch it in
main, as in Step 4. The program survives and the user gets a message. - Declare it on
mainwiththrows InsufficientFundsException. It compiles, but the exception now travels out ofmain, and the JVM prints a stack trace and ends the program. - Change the exception to unchecked, by extending
RuntimeException. The error disappears, but so does the compiler's reminder. This is the shortcut lesson 2 warned about: choose the type by who can recover, not by what makes the build pass.
For this rule, option 1 is right.
Example 3: the whole run (realistic)
The output of Step 4 shows a request sequence of 30, 500, 70, and 1 against a balance of 100:
- 30 is within the balance, so it succeeds and the balance becomes 70.
- 500 is too much: the exception is thrown before the balance changes, the
catchprints the message and the shortfall, and the loop goes on. - 70 succeeds and empties the account. The earlier refusal did not damage anything.
- 1 is refused with a shortfall of 1.00.
Two ideas to notice. First, the loop survived: a refused request is one iteration's problem, not the program's. Second, the catch block uses the exception's data (getShortfall()) rather than slicing the message string.
You can try the unchecked failure too. Add account.withdraw(-5); to the loop's input array and run: you get an IllegalArgumentException that nothing catches, which ends the program with a trace, because a negative amount is a bug the caller should fix and not a normal outcome. In a real application, the top level would still catch it and report an error.
Backend Insight
This structure is the skeleton of a banking service. The domain class (Account) owns the rule. The rule violation has a name (InsufficientFundsException). And the code at the edge (here main, later a web controller) catches the named exception and converts it to a user-facing answer, such as a "payment declined" response, without any chance of leaving a half-finished withdrawal behind.
When money is involved, "check first, change after" is not just tidiness. Later you will learn about transactions, which make a group of changes succeed or fail together. The habit you practiced here is the single-object version of the same principle, and the career link for this module is rollback rules: frameworks decide whether to undo work based on the kind of exception thrown, which is why choosing checked or unchecked carefully matters.
Common Mistakes
Mistake 1: Changing state before checking
If balance -= amount; came before the check, a refused withdrawal would still reduce the balance. Always validate first.
Mistake 2: Catching Exception in main to make it compile
catch (Exception e) would also catch a bug such as a negative amount and report it as a normal refusal. Catch InsufficientFundsException only.
Mistake 3: Parsing the message instead of using the data
Code that cuts numbers out of e.getMessage() breaks the moment the wording changes. Put facts in fields and read them with getters.
Mistake 4: Messages without numbers
"Insufficient funds" tells the user the rule but not the situation. Include the requested amount and the balance (and never an account password or full card number).
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Why is InsufficientFundsException checked in this demo, while a negative amount throws IllegalArgumentException?
أظهر الحل A1
A1. A refused withdrawal is a normal business outcome the caller can handle, so it is checked and the compiler forces a decision. A negative amount is a mistake in the calling code that should simply be fixed, so it is unchecked.
A2. Which line of withdraw must come last, and why?
أظهر الحل A2
A2. balance -= amount;, the state change, must come after every check so that a failure leaves the balance untouched.
A3. What does the compiler message unreported exception ... must be caught or declared to be thrown ask you to do?
أظهر الحل A3
A3. Either catch the exception with try/catch, or declare it with throws on your own method.
Level B: Application
B1. Add a transfer(Account to, double amount) throws InsufficientFundsException method to Account, which withdraws from this account and deposits into to. Explain why the order of those two calls matters if the withdrawal fails.
أظهر الحل B1
B1.
public void transfer(Account to, double amount) throws InsufficientFundsException {
withdraw(amount);
to.deposit(amount);
}Withdraw first: if it fails, the exception leaves immediately and the other account was never touched. Deposit first and fail on the withdrawal, and the money would exist in both accounts.
B2. In the catch block of Main, the program prints e.getShortfall(). Change the code so the user also sees how much they could withdraw. Which field would you expose, and how?
أظهر الحل B2
B2. Expose the available amount through a getter such as getAvailable() in the exception, then print it: System.out.printf("You can withdraw up to %.2f%n", e.getAvailable());. (The account's getBalance() also works in this program, because the same balance is available there.)
B3. The loop in Main gets {30, 500, 70, 1}. Predict, without running, the balance after each step if the starting balance is 100 and the rule is unchanged.
أظهر الحل B3
B3. After 30: 70. After 500: 70 (refused). After 70: 0. After 1: 0 (refused), matching the real output.
Level C: Challenge
C1. Add a unit-style check in main that proves a refused withdrawal leaves the balance unchanged: record the balance, attempt a too-large withdrawal inside a try/catch, and print OK or BROKEN. Run it.
أظهر الحل C1
C1.
double before = account.getBalance();
try {
account.withdraw(before + 1);
} catch (InsufficientFundsException e) {
System.out.println(account.getBalance() == before ? "OK" : "BROKEN");
}It prints OK, because the check runs before the subtraction. (This exact snippet was not run; a close variant of the whole demo was.)
C2. A teammate changes InsufficientFundsException to extend RuntimeException. List two consequences for the code you wrote, and say whether you would accept the change.
أظهر الحل C2
C2. Consequences: callers are no longer forced to catch it, so Bad.java would compile and then crash at run time; and the throws clause on withdraw becomes optional documentation. I would not accept it without discussion, because the compiler's reminder protects us from forgetting the case. If the team's convention is unchecked, I would accept it and add a note to the method's documentation.
Mini Challenge
Extend the demo in your own project. Add a daily withdrawal limit: a new checked exception DailyLimitExceededException (with the limit and the attempted total as data) thrown by withdraw after the funds check. Update Main so each of the two exceptions has its own catch block and its own user message. Make sure a refused request never changes the balance or the day's total.
Check your work: the new exception has a precise name, helpful message, and data; both catches are narrow; no catch is empty; and a test run shows the balance unchanged after each refusal.
Summary & Next Lesson
Key takeaways
- Define a named, checked exception for a business rule, with data and a message built inside the class.
- Enforce the rule in the domain class, with all checks before any state change, and declare the exception with
throws. - The compiler enforces the contract: callers must catch or declare. Catch where you can talk to the user.
- Use the exception's data, not its message text, when the handler needs the numbers.
- Keep bug-type failures (
IllegalArgumentException) separate from business ones.
Next lesson
The final lesson, "Demo: finding a swallowed exception", looks at the opposite of what we built here: a program where an error was caught and thrown away, so nothing is reported, and how to find and fix it.
Self-check
- Which exception type does
withdrawthrow for a negative amount, and which for an overdraw? - Why must the balance be changed last?
- What does
throws InsufficientFundsExceptiononwithdrawoblige callers to do? - Where is the best place in this program to catch the exception?
- Why read
getShortfall()instead of slicing the message?
أظهر الإجابات
IllegalArgumentExceptionfor the negative amount,InsufficientFundsExceptionfor the overdraw.- So a refused withdrawal leaves the balance exactly as it was.
- Catch it, or declare it themselves.
- In
main, the code that talks to the user. - The data is exact and does not break when the message wording changes.
References
- Oracle, The Java Tutorials, The Catch or Specify Requirement. Fetched. Supports that code which can throw a checked exception must catch it or declare it, and that code which does neither does not compile.
- Oracle, The Java Tutorials, Specifying the Exceptions Thrown by a Method. Fetched. Supports the placement and purpose of the
throwsclause. - All code, compiler output, and program output were produced with JDK 21.0.10 compiled from the command line. The remark about transaction rollback by exception type is general knowledge to be confirmed in a framework's documentation later. The solutions to B1, B2, and C1 were not run.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.