Java exam exercise on classes, collections and custom checked exceptions: a warehouse inventory with validated stock movements, solved and explained.
Updated on
Exam Code Java Object-oriented programming Exam level Tested code

A small company manages the inventory of its warehouse. Write a Java program that reads the initial state of the warehouse and a list of stock movements, applies them one by one and prints the final inventory.
Model the problem with at least these pieces: a class Item with code, name and stock (private fields), providing the methods receive(int) and issue(int); a custom checked exception InvalidMovementException that extends Exception; and a TreeMap<String, Item> indexed by code, so that the final listing comes out sorted alphabetically by code.
Input. The first line contains an integer N, the number of items. N lines follow with three space-separated fields: code, name (a single word) and initial stock (a non-negative integer). Then comes a line with an integer M, the number of movements, and M lines in the format TYPE code quantity, where TYPE is RECEIVE or ISSUE. Blank lines, in any position, must be ignored and do not count as data lines.
Rules. A movement is invalid, and must be rejected without changing the inventory, in the following cases, checked in this order: a type other than RECEIVE or ISSUE (reason unknown type); a code that does not exist in the inventory (no such item); a quantity less than or equal to zero (invalid quantity); an ISSUE that would leave the stock below zero (insufficient stock). Movements are numbered from 1 in order of appearance.
Output. For each rejected movement, in order, one line Movement k: reason. Then the line Inventory:, followed by one line per item, sorted by code, in the format code name stock. Finally the line Applied: a, rejected: r. A rejected movement must never interrupt the processing of the following ones.
Sample input
3
A-1 screws 10
B-2 nuts 5
C-3 buckets 0
4
RECEIVE A-1 5
ISSUE B-2 7
ISSUE C-3 0
ISSUE B-2 5Expected output
Movement 2: insufficient stock
Movement 3: invalid quantity
Inventory:
A-1 screws 15
B-2 nuts 0
C-3 buckets 0
Applied: 2, rejected: 2- Well-encapsulated Item class (private fields, receive and issue methods with validation)
- 0.75
- Custom checked exception InvalidMovementException and its being thrown in every error case
- 0.75
- Correct use of a collection sorted by code (TreeMap) and lookup of items
- 0.75
- Reading the input, catching exceptions without aborting the run, and exact output format
- 0.75
Hints
Hint 1 · Where is the exception thrown and where is it caught?
The validity checks on quantity and stock belong to Item, which throws InvalidMovementException with the reason as its message. The catch happens in the loop of main, inside the try of each movement, so that one error does not break the loop.
Hint 2 · In what order do the checks run?
The order of the rules matters: before looking up the item you must rule out the unknown type, and only after finding it do you validate the quantity. An unknown code with quantity zero must give no such item, not invalid quantity.
Hint 3 · How do you ignore blank lines?
Read all the input lines, discard the ones that are empty after trim(), and work on the resulting list with an index: first N, then N items, then M, then M movements.
Solution
Explained solution
The solution separates responsibilities: Item protects its own state and throws the exception when an operation is not valid; main orchestrates the reading, the counting of applied and rejected movements, and the final printing.
A TreeMap is used because the statement requires listing by sorted code and we also need lookup by key. With a HashMap we would have to sort afterwards.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Main {
static class InvalidMovementException extends Exception {
public InvalidMovementException(String reason) {
super(reason);
}
}
static class Item {
private final String code;
private final String name;
private int stock;
Item(String code, String name, int stock) {
this.code = code;
this.name = name;
this.stock = stock;
}
void receive(int quantity) throws InvalidMovementException {
if (quantity <= 0) {
throw new InvalidMovementException("invalid quantity");
}
stock += quantity;
}
void issue(int quantity) throws InvalidMovementException {
if (quantity <= 0) {
throw new InvalidMovementException("invalid quantity");
}
if (quantity > stock) {
throw new InvalidMovementException("insufficient stock");
}
stock -= quantity;
}
@Override
public String toString() {
return code + " " + name + " " + stock;
}
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
List<String> lines = new ArrayList<>();
String line;
while ((line = br.readLine()) != null) {
if (!line.trim().isEmpty()) {
lines.add(line.trim());
}
}
int pos = 0;
int n = Integer.parseInt(lines.get(pos++));
Map<String, Item> inventory = new TreeMap<>();
for (int i = 0; i < n; i++) {
String[] p = lines.get(pos++).split("\\s+");
inventory.put(p[0], new Item(p[0], p[1], Integer.parseInt(p[2])));
}
int m = Integer.parseInt(lines.get(pos++));
int applied = 0;
int rejected = 0;
StringBuilder errors = new StringBuilder();
for (int k = 1; k <= m; k++) {
String[] p = lines.get(pos++).split("\\s+");
try {
String type = p[0];
if (!type.equals("RECEIVE") && !type.equals("ISSUE")) {
throw new InvalidMovementException("unknown type");
}
Item item = inventory.get(p[1]);
if (item == null) {
throw new InvalidMovementException("no such item");
}
int quantity = Integer.parseInt(p[2]);
if (type.equals("RECEIVE")) {
item.receive(quantity);
} else {
item.issue(quantity);
}
applied++;
} catch (InvalidMovementException e) {
rejected++;
errors.append("Movement ").append(k).append(": ")
.append(e.getMessage()).append("\n");
}
}
StringBuilder out = new StringBuilder();
out.append(errors);
out.append("Inventory:\n");
for (Item item : inventory.values()) {
out.append(item).append("\n");
}
out.append("Applied: ").append(applied)
.append(", rejected: ").append(rejected).append("\n");
System.out.print(out);
}
}The nested class InvalidMovementException extends Exception, so it is checked: the compiler forces us to declare throws on receive and issue and to catch it in main. The reason travels in the message and is retrieved with getMessage().
In Item, the fields are private and only change through receive and issue. Both methods validate the quantity first; issue additionally checks that stock is not exceeded. If they throw the exception, the state is unchanged, because the addition or subtraction happens after the checks.
In main all lines are read and the empty ones discarded, so the index pos advances only over data lines. This handles the blank-line case of the test "Precedence and blank lines", where empty lines appear after N and between movements.
The loop over k applies the order of checks from the statement: first the type, then the lookup in the TreeMap, and finally the quantity, which the item itself validates. Thus, in test 4, RECEIVE X-1 2 gives no such item and RETURN A-1 1 gives unknown type, while RECEIVE A-1 6 is applied and leaves A-1 at 10.
The error messages are accumulated in errors and printed before the inventory, as the format requires. In the example, movement 2 (ISSUE 7 against a stock of 5) is rejected for insufficient stock and movement 3 (quantity 0) for invalid quantity; movements 1 and 4 are applied, and the final count is 2 applied and 2 rejected.
When iterating over inventory.values() the TreeMap hands over the items sorted by code with no extra work. In the test without movements, B-1 was inserted before A-1 and yet the output shows A-1 first.
Test cases
| Case | Input | Expected output | Actual output | Result |
|---|---|---|---|---|
| Example from the statement | 3
A-1 screws 10
B-2 nuts 5
C-3 buckets 0
4
RECEIVE A-1 5
ISSUE B-2 7
ISSUE C-3 0
ISSUE B-2 5 | Movement 2: insufficient stock
Movement 3: invalid quantity
Inventory:
A-1 screws 15
B-2 nuts 0
C-3 buckets 0
Applied: 2, rejected: 2 | Movement 2: insufficient stock
Movement 3: invalid quantity
Inventory:
A-1 screws 15
B-2 nuts 0
C-3 buckets 0
Applied: 2, rejected: 2 | OK |
| No movements and ordering by code | 2
B-1 nails 3
A-1 saws 1
0 | Inventory:
A-1 saws 1
B-1 nails 3
Applied: 0, rejected: 0 | Inventory:
A-1 saws 1
B-1 nails 3
Applied: 0, rejected: 0 | OK |
| Precedence and blank lines | 2
C-3 buckets 2
D-4 funnels 0
5
ISSUE Z-9 1
ISSUE D-4 1
RECEIVE C-3 3
ISSUE C-3 5
ISSUE D-4 2 | Movement 1: no such item
Movement 2: insufficient stock
Movement 5: insufficient stock
Inventory:
C-3 buckets 0
D-4 funnels 0
Applied: 2, rejected: 3 | Movement 1: no such item
Movement 2: insufficient stock
Movement 5: insufficient stock
Inventory:
C-3 buckets 0
D-4 funnels 0
Applied: 2, rejected: 3 | OK |
| Unknown type and unknown code | 1
A-1 batteries 4
3
RECEIVE X-1 2
RETURN A-1 1
RECEIVE A-1 6 | Movement 1: no such item
Movement 2: unknown type
Inventory:
A-1 batteries 10
Applied: 1, rejected: 2 | Movement 1: no such item
Movement 2: unknown type
Inventory:
A-1 batteries 10
Applied: 1, rejected: 2 | OK |
Actual outputs: code compiled with Java 21.0.12.1 (Temurin) and run in an isolated container on 19 October 2026.
Complexity
Reading and filtering the lines is O(L), with L the number of lines. Each insertion and lookup in the TreeMap costs O(log N), so the total processing is O(N log N + M log N). The final traversal of the inventory is O(N).
Memory is O(N + L): the map stores N items and the list keeps the lines read. For exam inputs this is irrelevant, but if the movements numbered in the millions it would be better to process them as a stream without storing every line.
Common mistakes
- Declaring the exception as a
RuntimeExceptionand not catching it, or catching a genericException: the distinction between reasons is lost and real errors such as aNumberFormatExceptionget hidden. - Modifying the stock before validating, so that a rejected movement leaves the inventory altered.
- Using a
HashMapand forgetting to sort the final listing, so the output depends on the internal order of the hash table. - Changing the order of the checks: validating the quantity before looking up the item changes the reason reported for unknown codes with quantity zero.
- Not ignoring blank lines and treating an empty line as a movement, which causes an
ArrayIndexOutOfBoundsException. - Numbering movements from 0 instead of 1, or counting only the applied ones when numbering.
Variants
Add a TRANSFER movement between two items
Add the type TRANSFER source destination quantity. To make it atomic, first validate that both codes exist and that the source has enough stock by calling issue on the source; only if it throws no exception, call receive on the destination. If the destination failed, the issue would have to be reverted, so it is better to check everything before modifying anything.
Replace the checked exception with an unchecked one
If InvalidMovementException extends RuntimeException, the throws clauses on receive and issue disappear, and the catch in main still works. The cost is that the compiler no longer forces callers to deal with the error, so validation depends on the programmer's discipline.
On the syllabus
The content of this exercise (classes with encapsulation, collections and error handling) fits the object-oriented programming courses found in many curricula. The course ISIS1209 APO II at Universidad de los Andes lists the study of class reuse mechanisms such as inheritance and of contract definition such as interfaces among its goals, and the IIC2233 Advanced Programming course at Pontificia Universidad Católica de Chile includes applying object-oriented concepts and fundamental data structures to design and write complex programs in languages such as Java.
Article generated with AI.larebelion



Comentarios
Publicar un comentario