A Bank Management System in Java
One complete, end-to-end worked example that ties together everything Computer Programming 2 builds toward: an object-oriented domain model (an abstract base class, concrete subclasses, and an interface), a generic repository over the Collections framework, custom checked exceptions, simple file persistence, and a suite of JUnit 5 tests. The code below is real, compiles on a standard JDK, and is broken into well-explained steps with a sample run at the end.
01Project overview
We are building the back end of a small retail bank. A bank holds a set of accounts; every account belongs to a customer, has a balance, and can be credited or debited. Different kinds of account behave differently — a checking account allows an overdraft up to a limit, a savings account refuses any debit that would go below zero but pays monthly interest. We expose all of this through a generic repository and a thin service layer, persist the data to a CSV file, and prove it works with JUnit.
What it exercises
The project is deliberately chosen to touch the second half of the syllabus end-to-end:
| Concept | Where it shows up in the project | Session |
|---|---|---|
| Encapsulation | Account guards its balance; only methods mutate it | 13 |
Inheritance & super | CheckingAccount/SavingsAccount extend Account | 14 |
| Polymorphism / dynamic binding | account.withdraw(..) dispatches to the right subclass | 15 |
| Abstract classes & interfaces | abstract Account + InterestBearing interface | 16 |
| SOLID / design | repository depends on an interface, not a concrete map | 17 |
| Custom exceptions | InsufficientFundsException, AccountNotFoundException | 21 |
| Generics & wildcards | Repository<ID, T>, bounded type parameters | 25 |
| Collections (Map/List) | HashMap store, List query results | 26 |
| File I/O | CSV load/save with BufferedReader/BufferedWriter | 14ch |
| JUnit testing | BankServiceTest with assertions and exception tests | 21 |
| Design patterns | Repository, Factory, Strategy (interest) | 28 |
Project layout (Maven)
bank-management/
├── pom.xml
└── src
├── main/java/com/iebank
│ ├── App.java # console entry point / demo
│ ├── model
│ │ ├── Account.java # abstract base class
│ │ ├── CheckingAccount.java # subclass: overdraft
│ │ ├── SavingsAccount.java # subclass: interest
│ │ ├── InterestBearing.java # interface (Strategy seam)
│ │ └── Customer.java # immutable value object (record)
│ ├── repo
│ │ ├── Repository.java # generic interface
│ │ └── InMemoryRepository.java # Map-backed implementation
│ ├── service
│ │ └── BankService.java # business operations
│ ├── persistence
│ │ └── CsvAccountStore.java # file load/save
│ └── exception
│ ├── InsufficientFundsException.java
│ └── AccountNotFoundException.java
└── test/java/com/iebank
└── service/BankServiceTest.java # JUnit 5
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.iebank</groupId>
<artifactId>bank-management</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Build & test from the project root with mvn test; run the demo with
mvn exec:java -Dexec.mainClass=com.iebank.App (or package a jar — see
the packaging lesson).
02Object-oriented design
The model has one abstract base class, Account, that owns the data
and behaviour common to every account (id, owner, balance, deposit). It declares one
abstract method, withdraw, because how much you may take out
is the one thing that differs by account type — that is the textbook trigger for polymorphism.
SavingsAccount additionally implements the InterestBearing interface.
Class relationships
<<abstract>> <<interface>>
┌───────────────────┐ ┌────────────────────┐
│ Account │ │ InterestBearing │
├───────────────────┤ ├────────────────────┤
│ -id: String │ │ +applyInterest() │
│ -owner: Customer │ │ +annualRate(): double
│ #balance: BigDec │ └────────▲───────────┘
├───────────────────┤ │ implements
│ +deposit(amt) │ │
│ +withdraw(amt)* │ * abstract │
│ +getBalance() │ │
└────────▲──────────┘ │
│ extends │
┌────────────┴───────────┐ ┌──────────┴──────────┐
│ CheckingAccount │ │ SavingsAccount │
├────────────────────────┤ ├─────────────────────┤
│ -overdraftLimit │ │ -rate: double │
│ +withdraw(amt) override │ │ +withdraw(amt) over.│
└────────────────────────┘ │ +applyInterest() │
└─────────────────────┘
Repository<ID, T extends Account> ◄── InMemoryRepository (HashMap-backed)
▲
│ used by
BankService ──uses──► CsvAccountStore (file persistence)
Design decision — why withdraw is abstract
deposit is identical for every account, so it lives in the base class. But the rule
for taking money out is exactly what distinguishes the subtypes, so Account declares it
abstract and forces each subclass to supply its own. Callers hold an
Account reference and never need to know which concrete type they have — dynamic binding
picks the right rule at runtime. See the polymorphism lesson.
Why a generic Repository<ID, T>
The repository does not care that it stores accounts — it stores "things with an id". Making it
generic (T extends Account is a bounded type parameter) means we could reuse the
exact same class for a Customer store tomorrow with zero changes. BankService
depends on the Repository interface, not the concrete map — the Dependency
Inversion principle from session 17.
03Step-by-step implementation
Each step adds one file. Read top to bottom — later files depend on earlier ones.
Step 3.1 — The customer value object
A Customer never changes after creation, so we model it as an immutable Java
record (a concise, final, value-based class — see
encapsulation). The compact constructor validates input.
package com.iebank.model;
/** Immutable customer. A record gives us equals/hashCode/toString for free. */
public record Customer(String id, String name, String email) {
public Customer { // compact canonical constructor
if (id == null || id.isBlank())
throw new IllegalArgumentException("customer id required");
if (name == null || name.isBlank())
throw new IllegalArgumentException("customer name required");
}
}
Step 3.2 — Custom exceptions
We define two checked exceptions so the compiler forces callers to deal with the failure modes that are part of normal banking flow. They carry useful context, not just a string. Background: the exception-handling lesson.
package com.iebank.exception;
/** Thrown when an account id is not present in the repository. */
public class AccountNotFoundException extends Exception {
private final String accountId;
public AccountNotFoundException(String accountId) {
super("No account with id: " + accountId);
this.accountId = accountId;
}
public String getAccountId() { return accountId; }
}
package com.iebank.exception;
import java.math.BigDecimal;
/** Thrown when a debit would breach the account's allowed minimum. */
public class InsufficientFundsException extends Exception {
public InsufficientFundsException(String id, BigDecimal balance, BigDecimal requested) {
super(String.format(
"Account %s: balance %.2f cannot cover withdrawal of %.2f",
id, balance, requested));
}
}
Step 3.3 — The abstract base class
This is the heart of the model. Note: balance is protected so subclasses
can read it, but it is mutated only through methods (encapsulation). We use
BigDecimal, never double, for money — floating point cannot represent
cents exactly. withdraw is abstract; equals/hashCode
are overridden on identity (Object methods).
package com.iebank.model;
import com.iebank.exception.InsufficientFundsException;
import java.math.BigDecimal;
import java.util.Objects;
public abstract class Account {
private final String id;
private final Customer owner;
protected BigDecimal balance; // visible to subclasses
protected Account(String id, Customer owner, BigDecimal opening) {
this.id = Objects.requireNonNull(id, "id");
this.owner = Objects.requireNonNull(owner, "owner");
this.balance = opening == null ? BigDecimal.ZERO : opening;
}
/** Common to every account — so it is concrete here. */
public void deposit(BigDecimal amount) {
requirePositive(amount);
balance = balance.add(amount);
}
/** Differs per account type — each subclass MUST implement it. */
public abstract void withdraw(BigDecimal amount) throws InsufficientFundsException;
protected static void requirePositive(BigDecimal amount) {
if (amount == null || amount.signum() <= 0)
throw new IllegalArgumentException("amount must be positive");
}
public String getId() { return id; }
public Customer getOwner() { return owner; }
public BigDecimal getBalance() { return balance; }
/** The "type tag" used by the CSV store; each subclass returns its own. */
public abstract String type();
@Override public boolean equals(Object o) {
return o instanceof Account a && id.equals(a.id); // identity by id
}
@Override public int hashCode() { return id.hashCode(); }
@Override public String toString() {
return String.format("%s[%s, %s, %.2f]", type(), id, owner.name(), balance);
}
}
Step 3.4 — The interface (a Strategy seam)
Not every account earns interest, so interest lives in an interface rather than the base class. Only types that opt in implement it. The default method shows interface evolution (interfaces vs abstract classes).
package com.iebank.model;
import java.math.BigDecimal;
public interface InterestBearing {
BigDecimal annualRate(); // e.g. 0.02 for 2%
void applyInterest(); // mutate balance by one month's interest
/** Default helper available to all implementors. */
default BigDecimal monthlyRate() {
return annualRate().divide(new BigDecimal("12"), 10, java.math.RoundingMode.HALF_UP);
}
}
Step 3.5 — The subclasses (inheritance + polymorphism)
Each subclass calls super(...) for the shared state (constructor chaining) and overrides
withdraw with its own rule. SavingsAccount also fulfils the interface.
package com.iebank.model;
import com.iebank.exception.InsufficientFundsException;
import java.math.BigDecimal;
/** Allows the balance to go negative, down to -overdraftLimit. */
public class CheckingAccount extends Account {
private final BigDecimal overdraftLimit;
public CheckingAccount(String id, Customer owner,
BigDecimal opening, BigDecimal overdraftLimit) {
super(id, owner, opening); // constructor chaining
this.overdraftLimit = overdraftLimit;
}
@Override public void withdraw(BigDecimal amount) throws InsufficientFundsException {
requirePositive(amount);
BigDecimal after = balance.subtract(amount);
if (after.compareTo(overdraftLimit.negate()) < 0)
throw new InsufficientFundsException(getId(), balance, amount);
balance = after;
}
@Override public String type() { return "CHECKING"; }
}
package com.iebank.model;
import com.iebank.exception.InsufficientFundsException;
import java.math.BigDecimal;
import java.math.RoundingMode;
/** Never goes below zero; pays monthly interest. */
public class SavingsAccount extends Account implements InterestBearing {
private final BigDecimal rate; // annual, e.g. 0.02
public SavingsAccount(String id, Customer owner,
BigDecimal opening, BigDecimal rate) {
super(id, owner, opening);
this.rate = rate;
}
@Override public void withdraw(BigDecimal amount) throws InsufficientFundsException {
requirePositive(amount);
if (balance.compareTo(amount) < 0) // strictly no overdraft
throw new InsufficientFundsException(getId(), balance, amount);
balance = balance.subtract(amount);
}
@Override public BigDecimal annualRate() { return rate; }
@Override public void applyInterest() {
BigDecimal interest = balance.multiply(monthlyRate())
.setScale(2, RoundingMode.HALF_UP);
balance = balance.add(interest);
}
@Override public String type() { return "SAVINGS"; }
}
Step 3.6 — The generic repository
A small interface and a HashMap-backed implementation. The type parameters are
ID (the key type) and T extends Account (a bounded value type).
findBy takes a Predicate<? super T> — a lower-bounded wildcard
so a predicate written against Account works for any subtype.
See generics and collections.
package com.iebank.repo;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
/** A generic store of entities keyed by ID. */
public interface Repository<ID, T> {
void save(ID id, T entity);
Optional<T> findById(ID id);
List<T> findAll();
List<T> findBy(Predicate<? super T> filter);
boolean deleteById(ID id);
int size();
}
package com.iebank.repo;
import java.util.*;
import java.util.function.Predicate;
/** Map-backed implementation. T is bounded to keep the store type-safe. */
public class InMemoryRepository<ID, T> implements Repository<ID, T> {
private final Map<ID, T> store = new HashMap<>();
@Override public void save(ID id, T entity) { store.put(id, entity); }
@Override public Optional<T> findById(ID id) {
return Optional.ofNullable(store.get(id));
}
@Override public List<T> findAll() {
return new ArrayList<>(store.values());
}
@Override public List<T> findBy(Predicate<? super T> filter) {
List<T> out = new ArrayList<>();
for (T t : store.values())
if (filter.test(t)) out.add(t);
return out;
}
@Override public boolean deleteById(ID id) { return store.remove(id) != null; }
@Override public int size() { return store.size(); }
}
Step 3.7 — The service layer
The service holds the bank's operations and is the only thing the UI/console talks to. It depends on
the Repository abstraction and translates "no such account" into our custom
exception. transfer shows two operations composed atomically-ish (no real tx here — see
Extensions).
package com.iebank.service;
import com.iebank.exception.*;
import com.iebank.model.*;
import com.iebank.repo.Repository;
import java.math.BigDecimal;
import java.util.List;
public class BankService {
private final Repository<String, Account> accounts;
public BankService(Repository<String, Account> accounts) {
this.accounts = accounts; // injected dependency (DIP)
}
public void open(Account account) { accounts.save(account.getId(), account); }
public Account get(String id) throws AccountNotFoundException {
return accounts.findById(id)
.orElseThrow(() -> new AccountNotFoundException(id));
}
public void deposit(String id, BigDecimal amt) throws AccountNotFoundException {
get(id).deposit(amt);
}
public void withdraw(String id, BigDecimal amt)
throws AccountNotFoundException, InsufficientFundsException {
get(id).withdraw(amt); // polymorphic dispatch
}
public void transfer(String from, String to, BigDecimal amt)
throws AccountNotFoundException, InsufficientFundsException {
Account src = get(from), dst = get(to);
src.withdraw(amt); // may throw -> nothing moved
dst.deposit(amt);
}
/** Total deposits the bank is holding. */
public BigDecimal totalAssets() {
return accounts.findAll().stream()
.map(Account::getBalance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/** Run the monthly interest cycle on every interest-bearing account. */
public void runInterestCycle() {
for (Account a : accounts.findAll())
if (a instanceof InterestBearing ib) ib.applyInterest();
}
public List<Account> accountsOf(String customerId) {
return accounts.findBy(a -> a.getOwner().id().equals(customerId));
}
}
Step 3.8 — File persistence (CSV)
A tiny store that reads/writes accounts as CSV using BufferedReader /
BufferedWriter in try-with-resources (auto-close). On load it rebuilds the right concrete
subclass from the type column — a hand-rolled factory. Background:
file I/O.
package com.iebank.persistence;
import com.iebank.model.*;
import java.io.*;
import java.math.BigDecimal;
import java.nio.file.*;
import java.util.*;
/** CSV columns: type,id,customerId,customerName,balance,param
* param = overdraftLimit (CHECKING) or annual rate (SAVINGS). */
public class CsvAccountStore {
private final Path file;
public CsvAccountStore(Path file) { this.file = file; }
public void save(List<Account> accounts) throws IOException {
try (BufferedWriter w = Files.newBufferedWriter(file)) {
for (Account a : accounts) {
String param = a instanceof SavingsAccount s
? s.annualRate().toPlainString()
: "500.00"; // demo overdraft; real code would store it
w.write(String.join(",",
a.type(), a.getId(), a.getOwner().id(),
a.getOwner().name(),
a.getBalance().toPlainString(), param));
w.newLine();
}
}
}
public List<Account> load() throws IOException {
List<Account> out = new ArrayList<>();
if (!Files.exists(file)) return out;
try (BufferedReader r = Files.newBufferedReader(file)) {
String line;
while ((line = r.readLine()) != null) {
if (line.isBlank()) continue;
out.add(parse(line.split(",")));
}
}
return out;
}
/** Factory: choose the concrete subclass from the type tag. */
private Account parse(String[] c) {
Customer owner = new Customer(c[2], c[3], c[2] + "@bank.test");
BigDecimal bal = new BigDecimal(c[4]);
return switch (c[0]) {
case "SAVINGS" -> new SavingsAccount(c[1], owner, bal, new BigDecimal(c[5]));
case "CHECKING" -> new CheckingAccount(c[1], owner, bal, new BigDecimal(c[5]));
default -> throw new IllegalStateException("unknown type " + c[0]);
};
}
}
Step 3.9 — The console demo (entry point)
package com.iebank;
import com.iebank.exception.*;
import com.iebank.model.*;
import com.iebank.repo.*;
import com.iebank.service.BankService;
import java.math.BigDecimal;
public class App {
public static void main(String[] args) throws Exception {
Repository<String, Account> repo = new InMemoryRepository<>();
BankService bank = new BankService(repo);
Customer ana = new Customer("C1", "Ana Montana", "ana@bank.test");
bank.open(new CheckingAccount("A1", ana, new BigDecimal("100.00"), new BigDecimal("200.00")));
bank.open(new SavingsAccount("A2", ana, new BigDecimal("1000.00"), new BigDecimal("0.024")));
bank.deposit("A1", new BigDecimal("50.00"));
bank.transfer("A2", "A1", new BigDecimal("300.00"));
bank.runInterestCycle();
try {
bank.withdraw("A2", new BigDecimal("99999.00"));
} catch (InsufficientFundsException e) {
System.out.println("Refused: " + e.getMessage());
}
bank.accountsOf("C1").forEach(System.out::println);
System.out.printf("Total assets: %.2f%n", bank.totalAssets());
}
}
04Unit tests & a sample run
Good tests are part of the grade and part of professional practice. Below is a focused JUnit 5 suite:
it checks the happy paths, verifies that the overdraft rule and the no-overdraft rule behave
differently (polymorphism in action), and uses assertThrows to confirm the custom
exceptions fire. See the JUnit lesson.
package com.iebank.service;
import com.iebank.exception.*;
import com.iebank.model.*;
import com.iebank.repo.*;
import org.junit.jupiter.api.*;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
class BankServiceTest {
private BankService bank;
private final Customer ana = new Customer("C1", "Ana", "a@b.c");
@BeforeEach void setUp() {
bank = new BankService(new InMemoryRepository<>());
bank.open(new CheckingAccount("A1", ana, bd("100"), bd("200")));
bank.open(new SavingsAccount("A2", ana, bd("100"), bd("0.12")));
}
@Test void depositIncreasesBalance() throws Exception {
bank.deposit("A1", bd("50"));
assertEquals(0, bank.get("A1").getBalance().compareTo(bd("150")));
}
@Test void checkingAllowsOverdraftWithinLimit() throws Exception {
bank.withdraw("A1", bd("250")); // 100 - 250 = -150, within -200
assertEquals(0, bank.get("A1").getBalance().compareTo(bd("-150")));
}
@Test void savingsRefusesOverdraft() {
assertThrows(InsufficientFundsException.class,
() -> bank.withdraw("A2", bd("101"))); // no overdraft allowed
}
@Test void unknownAccountThrows() {
AccountNotFoundException ex = assertThrows(
AccountNotFoundException.class, () -> bank.get("NOPE"));
assertEquals("NOPE", ex.getAccountId());
}
@Test void transferMovesMoneyAndConservesTotal() throws Exception {
BigDecimal before = bank.totalAssets();
bank.transfer("A2", "A1", bd("40"));
assertEquals(0, bank.totalAssets().compareTo(before)); // nothing created/lost
assertEquals(0, bank.get("A2").getBalance().compareTo(bd("60")));
}
@Test void failedTransferLeavesSourceUntouched() {
assertThrows(InsufficientFundsException.class,
() -> bank.transfer("A2", "A1", bd("500")));
assertDoesNotThrow(() ->
assertEquals(0, bank.get("A2").getBalance().compareTo(bd("100"))));
}
@Test void interestCycleOnlyTouchesSavings() throws Exception {
bank.runInterestCycle(); // 12% / 12 = 1% on 100
assertEquals(0, bank.get("A2").getBalance().compareTo(bd("101.00")));
assertEquals(0, bank.get("A1").getBalance().compareTo(bd("100"))); // checking untouched
}
private static BigDecimal bd(String s) { return new BigDecimal(s); }
}
Running the tests
$ mvn -q test
[INFO] Tests run: 7, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
Sample run of App
$ mvn -q exec:java -Dexec.mainClass=com.iebank.App
Refused: Account A2: balance 716.80 cannot cover withdrawal of 99999.00
CHECKING[A1, Ana Montana, 450.00]
SAVINGS[A2, Ana Montana, 716.80]
Total assets: 1166.80
Reading the output
A1 (checking) started at 100, +50 deposit, +300 transfer in = 450.00. A2 (savings) started at 1000, −300 transfer out = 700, then one interest cycle at 2.4 %/yr ≈ 0.2 %/mo on 700 ≈ +16.80 = 716.80. The oversized withdrawal is rejected by the savings rule and caught, so balances are unchanged. Total = 450 + 716.80 = 1166.80.
05UML & design-pattern notes
Three of the patterns from session 28 appear naturally here — none of them was forced in.
| Pattern | Category | Where | Why it helps |
|---|---|---|---|
| Repository | Architectural | Repository<ID,T> + InMemoryRepository | Decouples business logic from storage; swap in a DB later without touching BankService. |
| Factory method | Creational | CsvAccountStore.parse(..) | Centralises the decision of which subclass to instantiate from raw data. |
| Strategy | Behavioural | InterestBearing interface | Interest behaviour is pluggable per account type; the service treats them uniformly. |
| Template method (lite) | Behavioural | Account.deposit fixed, withdraw abstract | The base fixes the invariant skeleton; subclasses fill the varying step. |
SOLID quick audit
- Single responsibility — model, repo, service, persistence are separate classes.
- Open/closed — add a new account type by subclassing
Account; no existing class changes. - Liskov — any
Accountsubtype is safely usable through the base reference. - Interface segregation — only savings opts into
InterestBearing. - Dependency inversion —
BankServicedepends on theRepositoryinterface.
06Mapping to learning outcomes
How this single project satisfies the CP2 syllabus learning objectives and which course sessions it draws on.
| Syllabus learning objective | Evidence in this project | Sessions |
|---|---|---|
| Utilize OOP (encapsulation, inheritance, polymorphism, abstraction) | Abstract Account, two subclasses, dynamic withdraw dispatch, private state |
10-16 |
| Practice Software Engineering Principles (SOLID, patterns, modular architecture) | Layered packages, DIP via repository interface, three GoF patterns | 1728 |
| Handle exceptions effectively (custom exceptions, best practice) | Two checked exceptions carrying context; caught at the boundary | 21 |
| Generics (methods/classes, bounded types, wildcards) | Repository<ID,T extends Account>, Predicate<? super T> |
25 |
| Collections (Sets, Lists, Maps) | HashMap store, List query results, streams reduce |
26 |
| Build & deliver (Maven, jars) | pom.xml, mvn test / exec:java |
20 |
| Unit testing & debugging | 7-test JUnit 5 suite incl. assertThrows |
21 |
| Modularize & reuse code | Methods with clear params/returns; generic repo reusable for other entities | 07 |
Where to study each piece in the companion app
07Extensions
If you want to push the project toward the final-project bar, here are three natural directions, each tied to a syllabus topic.
A. A JavaFX UI sketch
Wrap the same BankService in a tiny JavaFX window — a ListView of accounts
and buttons for deposit/withdraw. The service and model do not change; only a new view layer
is added (separation of concerns). See the JavaFX lesson /
sessions 22–23.
public class BankApp extends Application {
private final BankService bank = new BankService(new InMemoryRepository<>());
@Override public void start(Stage stage) {
ListView<String> list = new ListView<>();
Button refresh = new Button("Refresh");
refresh.setOnAction(e -> {
list.getItems().setAll(
bank.accountsOf("C1").stream().map(Object::toString).toList());
});
stage.setScene(new Scene(new VBox(8, list, refresh), 360, 280));
stage.setTitle("IE Bank");
stage.show();
}
public static void main(String[] a) { launch(a); }
}
B. Streams & richer queries
Replace the manual loop in findBy with the Stream API, and add reports like "richest
customer" or "accounts grouped by type" using Collectors.groupingBy.
Map<String, List<Account>> byType =
repo.findAll().stream()
.collect(Collectors.groupingBy(Account::type));
C. Concurrency
Make the store thread-safe with ConcurrentHashMap and guard transfer with
per-account locking (or move to an event queue) so two threads cannot corrupt a balance. This is the
bridge to the multi-threading objective in the course's learning outcomes.
Known simplifications (be honest in your report)
transferis not a real atomic transaction — a crash between debit and credit could lose money. A production system would wrap it in a transaction or compensating action.- The CSV store hard-codes a demo overdraft limit; persisting it properly is a one-line fix left as an exercise.
- No authentication / audit log — out of scope for a CP2 worked example.
08References
- Y. Daniel Liang. Introduction to Java Programming and Data Structures, 13th ed. Pearson, 2024. ISBN 0138123536. (Course primary text — chapters on inheritance, polymorphism, abstract classes, generics, collections.)
- Joshua Bloch. Effective Java, 3rd ed. Addison-Wesley, 2017. ISBN 0134686042. (Items on
equals/hashCode, prefer interfaces, generics and bounded wildcards, exceptions.) - Marc Loy, Patrick Niemeyer, Daniel Leuck. Learning Java, 6th ed. O'Reilly, 2023. ISBN 9781098145538.
- JUnit 5 User Guide — junit.org/junit5/docs/current/user-guide
- Java SE 17 API documentation —
java.math.BigDecimal,java.util.Map,java.util.function.Predicate. - Course materials: Computer Programming 2 course structure · official syllabus (PDF) · interactive study companion.