-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
40: Practice Exercise: Building a Class-Based Bank Account System
A few years ago, I was reviewing a pull request for a junior dev who was building a simple ledger system. He had created a BankAccount class, but he made the balance field public because he said it made the unit tests "easier to write" since he could just set the balance directly. Two days later, he came to my desk panicked because a different module in the app was accidentally setting account balances to negative values during a weird edge case in the currency conversion logic. Because the field was public, there was no "gatekeeper" to stop the data from being corrupted.
That is the quintessential lesson in encapsulation. When you're building a system like a bank account, the class shouldn't just be a container for data; it should be the sole authority on how that data is allowed to change. If you let any other class reach in and touch the balance, you've lost control of your business logic.
Shielding the Balance from External Interference
In a real-world Java application, you almost never want your internal state to be public. By marking your balance as private, you're essentially telling the rest of the program, "You can't touch this directly; you have to ask me to do it for you." This is where getters and setters come in, though in a banking system, a generic setBalance() method is actually a dangerous anti-pattern. You don't "set" a balance; you deposit or withdraw money.
public class BankAccount {
private double balance;
private String accountHolder;
public BankAccount(String accountHolder, double initialBalance) {
this.accountHolder = accountHolder;
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
}
By providing a getter but no setter, the balance becomes read-only to the outside world. This is exactly how you prevent the "negative balance bug" I mentioned earlier. The only way the balance changes is through methods we explicitly define.
Encoding Banking Rules into Methods
Now that the data is shielded, we need to create controlled entry points. This is where we bake our business rules directly into the class. For instance, you can't deposit a negative amount of money—that's just a withdrawal in disguise, and it should be handled by a separate method. Similarly, you shouldn't be able to withdraw more than you actually have (unless you're implementing an overdraft feature, but let's keep it simple for now).
I always recommend throwing an error or returning a boolean to indicate if an operation failed. It forces the developer using your class to actually handle the failure instead of just assuming the money moved.
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
System.out.println("Deposited: $" + amount);
} else {
System.out.println("Invalid deposit amount.");
}
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
System.out.println("Withdrew: $" + amount);
return true;
} else {
System.out.println("Insufficient funds or invalid amount.");
return false;
}
}
Orchestrating the Account Logic
Once your BankAccount class is airtight, you need a driver class to put it to the test. This is where you'll instantiate your objects and simulate real-world usage. I like to think of this as the "user story" phase. You aren't just testing if the code runs; you're testing if the behavior matches reality. What happens if a user tries to withdraw $1,000 from a $500 account? Does the system crash, or does it gracefully deny the request?
When you write your main method, try to push the boundaries. Don't just do a successful deposit and withdrawal. Try to break the logic. If you can't find a way to force the balance into a negative state using the public methods, then you've successfully encapsulated your class.
📋 Practical Task
Exercise: The Robust Bank Account Simulator
Your task is to build a fully encapsulated banking system. You will need to create two files: BankAccount.java and BankSystem.java.
Requirements for BankAccount.java:
- Create a private
balance(double) and a privateaccountNumber(String). - Implement a constructor that initializes both fields.
- Create a
deposit(double amount)method that only adds money if the amount is positive. - Create a
withdraw(double amount)method that only deducts money if the amount is positive and doesn't exceed the current balance. It should returntrueif successful andfalseotherwise. - Create a
getBalance()method to allow read-only access to the balance.
Requirements for BankSystem.java:
- In the
mainmethod, instantiate aBankAccountwith an initial balance of $500.00. - Perform a sequence of operations:
- Deposit $200.
- Attempt to withdraw $1,000 (this should fail).
- Withdraw $300.
- Attempt to deposit a negative amount like -$50 (this should fail).
- Print the final balance to the console to verify that only valid transactions were processed.
There are no comments for now.