Solved Java exercise on classes and objects: encapsulation, validation and a prepaid wallet with balance in cents, with tests and common mistakes.
Updated on
Exam Code Java Object-oriented programming Basic level Tested code

Many universities use Java in their first courses to introduce object-oriented programming: it forces you to declare types and to write everything inside classes, which helps you tell apart the state of an object from the operations it allows. This exercise applies exactly that idea to a small case: a prepaid electronic wallet.
Design the class Wallet. Each object represents one owner's wallet and stores its balance in cents (an integer, never a double). The maximum allowed balance is 15000 cents (150.00 dollars) and the balance can never be negative. The class must offer at least a constructor that receives the owner's name, a method topUp(int cents), a method pay(int cents) and a method to read the balance. The fields must be private.
The rules of each operation are checked in this order. First, if the amount is less than or equal to zero, the operation is INVALID. Then, for a top-up, if the resulting balance would exceed 15000 cents, the operation is LIMIT. For a payment, if the amount is greater than the available balance, the operation is INSUFFICIENT. In any other case the operation is accepted (OK) and changes the balance. A rejected operation does not change the balance.
The main program reads from standard input. The first line contains an integer n, the number of operations. The next n lines have the format OWNER OPERATION AMOUNT, where OWNER is a single word without spaces, OPERATION is TOPUP or PAY and AMOUNT is an integer number of cents. An owner who appears for the first time starts with a wallet whose balance is 0, and each owner has their own independent wallet.
For each operation, print one line with the result (OK, INVALID, LIMIT or INSUFFICIENT), the owner, and the wallet balance after the operation, written in dollars with two decimals and a decimal point (for example 12.50). If n is 0, print nothing. Format hint: separate the three fields with a single space.
Sample input
6
alice TOPUP 2000
alice PAY 750
bob TOPUP 500
alice PAY 1250
bob PAY 600
alice TOPUP 3Expected output
OK alice 20.00
OK alice 12.50
OK bob 5.00
OK alice 0.00
INSUFFICIENT bob 5.00
OK alice 0.03- Wallet class with private fields, a constructor and consistent accessor methods
- 0.75
- Methods topUp and pay, with the validation and limit rules applied in the correct order
- 0.75
- Handling several independent objects (one per owner) with a Map
- 0.5
- Reading the input and exact output format, including the balance with two decimals
- 0.5
Hints
Hint 1 · What must each object store, and what must the outside world not be able to touch?
Each Wallet needs its owner and its balance in cents. Declare both private and let only topUp and pay change the balance. That way the rules (balance never negative, maximum of 15000) live in a single place and nobody can bypass them.
Hint 2 · How do you represent that an operation is accepted or rejected?
Make topUp and pay return a String with the result (OK, INVALID, LIMIT or INSUFFICIENT). Check the non-positive amount first and then the rule specific to each operation; modify the balance only in the OK case.
Hint 3 · How do you keep one wallet per owner, and print dollars without using double?
Use a Map<String, Wallet> and create the wallet the first time the owner appears (computeIfAbsent or a check with containsKey). To print, divide the balance by 100 and take the remainder: String.format("%d.%02d", balance / 100, balance % 100). This avoids floating-point rounding errors.
Solution
Explained solution
The solution separates two responsibilities. The Wallet class protects its state and applies the business rules; the main method only reads lines, finds the owner's wallet and writes the result. This split is exactly what a first exercise on classes and objects is meant to illustrate.
The balance is stored as an int in cents. With integers every comparison is exact, and the limit of 15000 can be tested without fear of rounding errors.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class Main {
static class Wallet {
private static final int MAXIMUM = 15000;
private final String owner;
private int balance;
Wallet(String owner) {
this.owner = owner;
this.balance = 0;
}
String getOwner() {
return owner;
}
int getBalance() {
return balance;
}
String topUp(int cents) {
if (cents <= 0) {
return "INVALID";
}
if (balance + cents > MAXIMUM) {
return "LIMIT";
}
balance += cents;
return "OK";
}
String pay(int cents) {
if (cents <= 0) {
return "INVALID";
}
if (cents > balance) {
return "INSUFFICIENT";
}
balance -= cents;
return "OK";
}
String balanceInDollars() {
return String.format("%d.%02d", balance / 100, balance % 100);
}
}
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String first = in.readLine();
if (first == null || first.isBlank()) {
return;
}
int n = Integer.parseInt(first.trim());
Map<String, Wallet> wallets = new HashMap<>();
StringBuilder output = new StringBuilder();
for (int i = 0; i < n; i++) {
String line = in.readLine();
if (line == null) {
break;
}
String[] parts = line.trim().split("\\s+");
String owner = parts[0];
String operation = parts[1];
int amount = Integer.parseInt(parts[2]);
Wallet w = wallets.computeIfAbsent(owner, Wallet::new);
String result;
if (operation.equals("TOPUP")) {
result = w.topUp(amount);
} else {
result = w.pay(amount);
}
output.append(result).append(' ')
.append(w.getOwner()).append(' ')
.append(w.balanceInDollars()).append('\n');
}
System.out.print(output);
}
}The Wallet class. The fields owner and balance are private and the constant MAXIMUM is 15000. The constructor sets the balance to 0. Outside the class there is no way to write to balance: only topUp and pay modify it, and both validate before touching it. That is encapsulation in practice: the invariant "0 ≤ balance ≤ 15000" holds because a single class guards it.
Order of the checks. In topUp the code first tests cents <= 0 and then balance + cents > MAXIMUM; in pay, the non-positive amount and then cents > balance. The order matters: a negative amount must yield INVALID regardless of any other rule. Only the final, accepted branch executes balance += cents or balance -= cents, so a rejected operation leaves the balance untouched.
The main method. The HashMap associates each name with its object. computeIfAbsent(owner, Wallet::new) creates the wallet with balance 0 the first time and returns the existing one afterwards, so each owner has a distinct, independent object. Each line is split on whitespace and the result is accumulated in a StringBuilder to be printed at the end. If n is 0 the loop never runs and nothing is written.
Balance format. balanceInDollars uses balance / 100 and balance % 100 with %02d, so 50 cents is written 0.50 and 15000 is written 150.00. Since the value never goes through a double, there is no rounding to explain surprising results.
Test cases. The first test, the one in the statement, mixes top-ups and payments of two owners and checks that their balances do not get mixed: bob's payment of 600 is rejected with INSUFFICIENT while alice is unaffected. The second exercises the rejection reasons (zero amount, negative amount and a payment larger than the balance). The third, "Exact balance limits", tops up to exactly 150.00, which is accepted, and then confirms that one more cent is rejected with LIMIT without altering the balance. The fourth covers input with 0 operations, which produces no output, and the fifth a single operation by a new owner.
Test cases
| Case | Input | Expected output | Actual output | Result |
|---|---|---|---|---|
| Statement example | 6
alice TOPUP 2000
alice PAY 750
bob TOPUP 500
alice PAY 1250
bob PAY 600
alice TOPUP 3 | OK alice 20.00
OK alice 12.50
OK bob 5.00
OK alice 0.00
INSUFFICIENT bob 5.00
OK alice 0.03 | OK alice 20.00
OK alice 12.50
OK bob 5.00
OK alice 0.00
INSUFFICIENT bob 5.00
OK alice 0.03 | OK |
| Invalid and insufficient amounts | 5
mary TOPUP 0
mary TOPUP -300
mary PAY 100
mary TOPUP 1000
mary PAY 1001 | INVALID mary 0.00
INVALID mary 0.00
INSUFFICIENT mary 0.00
OK mary 10.00
INSUFFICIENT mary 10.00 | INVALID mary 0.00
INVALID mary 0.00
INSUFFICIENT mary 0.00
OK mary 10.00
INSUFFICIENT mary 10.00 | OK |
| Exact balance limits | 4
jon TOPUP 14000
jon TOPUP 1000
jon TOPUP 1
jon PAY 15000 | OK jon 140.00
OK jon 150.00
LIMIT jon 150.00
OK jon 0.00 | OK jon 140.00
OK jon 150.00
LIMIT jon 150.00
OK jon 0.00 | OK |
| No operations | 0 | (empty) | (empty) | OK |
| A single operation | 1
zoe TOPUP 99 | OK zoe 0.99 | OK zoe 0.99 | OK |
Actual outputs: code compiled with Java 21.0.12.1 (Temurin) and run in an isolated container on 21 October 2026.
Complexity
Each operation costs O(1): one lookup in the HashMap (expected constant time), a couple of comparisons and one integer addition or subtraction. For n operations the total time is O(n), plus the cost of reading and formatting the lines.
Memory is O(o), where o is the number of distinct owners, because one Wallet object is stored per owner. The output is accumulated in a StringBuilder, which holds O(n) characters; this is perfectly reasonable for an exam, and printing line by line would have the same asymptotic time cost.
Common mistakes
- Declaring
balanceaspublic, or with no modifier, and changing it frommain: encapsulation is broken and the rules stop being centralised, which is exactly what the exercise wants to assess. - Storing the balance as a
doublein dollars: 0.1 + 0.2 is not 0.3 in floating point, and comparisons against the limit of 150 can fail. With integer cents the problem disappears. - Changing the balance before validating and undoing it afterwards, or accepting the operation and checking later: a rejected operation must leave the balance exactly as it was.
- Getting the limit check wrong: using
>=instead of>rejects the top-up that leaves the balance at exactly 15000, which must be accepted (the "Exact balance limits" test catches this). - Using a single shared
Walletobject for all owners, or creating a new one on every line: in the first case balances get mixed, in the second the previous balance is lost. - Printing the balance with
System.out.println(balance / 100.0): it shows12.5instead of12.50and does not follow the requested format.
Variants
Add a transfer between two owners
Add an operation TRANSFER source target amount. The source wallet must be able to pay and the target wallet to receive, and the operation must be atomic: if either half fails, neither balance changes. A simple way is to check first with the same rules (positive amount, amount <= balance of the source and balance + amount <= MAXIMUM of the target) and only then call pay and topUp. Paying before checking the target would force you to give the money back if the target rejects it.
Replace the result strings with an enum
Instead of returning String, define enum Result { OK, INVALID, LIMIT, INSUFFICIENT } and make topUp and pay return that type. The compiler prevents returning a non-existent value, and printing only needs result.name(). It is a typical example of how Java's type system prevents errors that, with strings, would only show up at run time. For more on contracts between types, see the exercise on interfaces and default methods.
On the syllabus
Designing classes with encapsulated state that communicate through methods is part of the syllabus of object-oriented courses. The programme of IIC2233 Advanced Programming at the Pontificia Universidad Católica de Chile includes applying object-oriented concepts to write programs in languages such as Java and C#, and the course ISIS1209 at the Universidad de los Andes (Colombia) studies class-reuse mechanisms, such as inheritance, and contract-definition mechanisms, such as interfaces. If you want to continue from this exercise, the exercise on inheritance, constructors and super is the natural next step.
For its part, the syllabus of Algoritmos y Programación II (75.41) at the Universidad de Buenos Aires covers dynamic memory management through pointers and references, an idea that in Java translates into understanding that a variable of type Wallet holds a reference to an object, not the object itself.
Article generated with AI.larebelion · Reviewed by Kernel


