Java foundations · Lesson 28 About 80 min with practice
Java Class Challenge: An Inventory Model
Bring constructors, private state, instance behavior, and numeric contracts together. Build an inventory item that starts valid, accepts only deliberate stock changes, and reports its current value without hidden or duplicated state.
Translate an item policy into fields, invariants, constructors, and public contracts.
Keep identity and price fixed while controlling changes to private quantity.
Delegate a zero-stock convenience constructor to one validated full constructor.
Implement restock and sell without partial updates or unsafe arithmetic.
Calculate a long stock value from integer-cent prices without storing redundant totals.
Verify boundaries, rejection, independence, queries, and a complete inventory scenario.
Read the challenge as a set of promises
You are modeling one line of inventory: a SKU label, a display name, a unit price in cents, and a current quantity. The class must stay correct when callers try valid changes, invalid changes, repeated requests, or observations between changes. The goal is a trustworthy small model, not a large application.
The exercise's policy is explicit: labels must be non-null and nonblank, surrounding Java-defined whitespace is stripped, unit price is 0..10,000,000 cents, and quantity is 0..1000. A zero price and zero stock are valid. Restock and sell accept only positive amounts that keep quantity within its range; zero is a rejected no-op.
These are fictional teaching limits. The model is single-threaded and in memory: it does not persist stock, process payments, reserve items, track an audit ledger, enforce unique SKUs across a catalog, or coordinate concurrent buyers. A SKU is a label here, not a global identity service. Do not turn a successful console demo into a production-readiness claim.
Choose what stays fixed and what may change
SKU, name, and unit price are private final fields, assigned during construction and not replaceable through this API. Quantity is private but mutable because stock changes over time. The shared maximums are public static final primitives, not a shared stock counter.
The public surface is deliberately small: two constructors, read-only getters, restock, sell, stock-value calculation, and summary. There is no setQuantity method and no public field write. A caller asks for a meaningful action rather than overwriting the accumulated state.
This ownership choice also keeps separate instances independent. Changing pens must not change notebooks. An alias still reaches the same item's public behavior; private fields do not block authorized-by-the-API actions through a copied reference. Encapsulation controls supported code access, not business authorization or thread safety.
JAVA 21 COMPILER
Main.java · Start with one valid item
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · inspect both the returned outcome and the remaining state
PEN-01 | Pens | qty: 12 | value cents: 1800
Choose the owner of each responsibility
Each item owns its quantity and fixed metadata.
The class owns the shared quantity and unit-price limits.
The constructor establishes valid state.
restock and sell protect state during changes.
Queries calculate or report without changing anything.
Main chooses the scenario and prints results.
Establish the invariant once, through both constructors
The full constructor validates SKU, name, price, and quantity in that order. It first stores stripped labels in local variables, checks the numeric ranges, and only then assigns the fields. The private requireLabel helper gives each label a consistent null/blank check and a specific error message.
The three-argument convenience constructor calls this(sku, name, unitPriceCents, 0). It initializes the same object through the full constructor rather than duplicating checks or creating a different temporary item. Under the Java 21 baseline, that delegation is its first statement.
Invalid construction throws IllegalArgumentException rather than returning an invalid item. Normal stock-operation rejection will use false instead; those are different documented contracts. The short try/catch demonstration in the constructors lesson is sufficient to test construction errors here. No method silently clamps an invalid starting value.
JAVA 21 COMPILER
Main.java · A convenience constructor supplies zero stock
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){InventoryItem empty =newInventoryItem("NOTE-01","Notebooks",350);System.out.println(empty.summary());System.out.println("Sell from empty: "+ empty.sell(1));}}classInventoryItem{publicstaticfinalintMAX_QUANTITY=1000;publicstaticfinalintMAX_UNIT_CENTS=10_000_000;privatefinalString sku;privatefinalString name;privatefinalint unitPriceCents;privateint quantity;publicInventoryItem(String sku,String name,int unitPriceCents){this(sku, name, unitPriceCents,0);}publicInventoryItem(String sku,String name,int unitPriceCents,int quantity){String cleanSku =requireLabel(sku,"SKU");String cleanName =requireLabel(name,"Name");if(unitPriceCents <0|| unitPriceCents >MAX_UNIT_CENTS){thrownewIllegalArgumentException("Unit price must be 0..10000000 cents.");}if(quantity <0|| quantity >MAX_QUANTITY){thrownewIllegalArgumentException("Quantity must be 0..1000.");}this.sku = cleanSku;this.name = cleanName;this.unitPriceCents = unitPriceCents;this.quantity = quantity;}privatestaticStringrequireLabel(String value,String label){if(value ==null|| value.isBlank()){thrownewIllegalArgumentException(label +" must not be blank.");}return value.strip();}publicStringgetSku(){return sku;}publicStringgetName(){return name;}publicintgetUnitPriceCents(){return unitPriceCents;}publicintgetQuantity(){return quantity;}publicbooleanrestock(int amount){if(amount <=0|| amount >MAX_QUANTITY- quantity){returnfalse;}
quantity += amount;returntrue;}publicbooleansell(int amount){if(amount <=0|| amount > quantity){returnfalse;}
quantity -= amount;returntrue;}publiclonggetStockValueCents(){return(long) unitPriceCents * quantity;}publicStringsummary(){return sku +" | "+ name +" | qty: "+ quantity
+" | value cents: "+getStockValueCents();}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · inspect both the returned outcome and the remaining state
NOTE-01 | Notebooks | qty: 0 | value cents: 0
Sell from empty: false
Restock against remaining capacity before adding
restock(amount) means add that many units, not replace the current quantity. At twelve units, restocking eight produces twenty. The return value true reports acceptance, while getQuantity reports the state after the action. Keep those meanings separate when predicting output.
Reject nonpositive amounts first. Then compare amount with MAX_QUANTITY minus quantity. The invariant ensures that remaining capacity is in 0..1000, so the subtraction is safe. Only add after acceptance; even Integer.MAX_VALUE can be rejected without evaluating an overflowing quantity-plus-amount sum.
Exactly filling the remaining capacity is valid, so use greater-than for rejection rather than greater-than-or-equal. A full item rejects every further positive restock. There is no partial addition and no attempt to undo a rejected change afterward.
JAVA 21 COMPILER
Main.java · Accept only a positive change within capacity
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · inspect both the returned outcome and the remaining state
Restock eight: true
Fill exactly: true
One too many: false
PEN-01 | Pens | qty: 1000 | value cents: 150000
Trace the exact-fill boundary
After twelve plus eight, quantity is twenty. Remaining capacity is 1000 - 20, or 980. Restocking 980 succeeds; restocking 981 would fail. The inclusive boundary is part of the policy, so a mistaken greater-than-or-equal rejection would incorrectly block a valid exact fill.
Sell only an available positive amount
sell(amount) removes stock. Reject amount <= 0 or amount > quantity before subtraction. Selling exactly the remaining quantity is allowed and produces zero. Trying to sell one more unit afterward must fail with the quantity still at zero.
Negative sales are not a shortcut for restocking. Keeping two positive-amount operations makes each caller's intent visible and prevents a sign mistake from turning a rejected sale into an increase. Use restock when the action is an addition.
The class performs the check and the state change; main prints the outcome and reads the new state. Neither a user-interface precheck nor a caller's earlier getter can replace the method's own validation. In this exercise all execution is single-threaded; the same pattern alone is not a concurrent stock-reservation mechanism.
JAVA 21 COMPILER
Main.java · Selling all remaining stock is valid
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · inspect both the returned outcome and the remaining state
Sell five: true
Sell remaining: true
Sell from zero: false
PEN-01 | Pens | qty: 0 | value cents: 0
A false result is only correct when state is unchanged
A method that subtracts thirteen from twelve and then returns false has already broken the model. Returning a rejection does not repair a mutation. Put every rejecting guard before the assignment and test the resulting quantity, not merely the printed boolean.
Test zero, negative, just-outside, and extreme int requests. Keep both operation names in the test matrix: rejecting a huge restock does not prove a huge negative sale is handled. With private fields and validated construction, the method can rely on a valid current quantity while checking the new request.
Also check identity and fixed metadata after rejection. The original SKU, name, unit price, and derived stock value should still agree with the unchanged quantity. This catches code that accidentally changes another field or leaves a cached total out of sync.
JAVA 21 COMPILER
Main.java · Rejected requests must preserve earlier state
Predict the result, edit Main.java, then run your program.
Create pens and notebooks independently, then sell only pens. Query both items afterward. The notebooks' earlier four units and 1400-cent value must remain unchanged. A private static quantity would still be wrong: privacy does not turn shared class storage into separate instance state.
Two instances can even have the same SKU and name in this exercise, because catalog-wide uniqueness is deliberately outside the item contract. They remain distinct objects with separate quantities. Do not add a global static set or hidden registry just to make this small class look like an entire catalog.
If desk = pens, desk and pens are aliases, not independent stock records. A sale through desk changes what pens sees. Use a second new expression when you mean a different item; do not mistake an alias test for an independence test.
JAVA 21 COMPILER
Main.java · Another item does not share quantity
Predict the result, edit Main.java, then run your program.
Derive stock value in long cents instead of caching it
The stock value is unit price times current quantity. It is a snapshot of this exercise's valuation rule, not revenue, profit, taxes, or a checkout charge. Deriving it on demand means every accepted stock change automatically appears in the next value query without maintaining a second mutable total.
The maximum is 10,000,000 times 1000, or 10,000,000,000 cents, which exceeds int range. Write (long) unitPriceCents * quantity so multiplication happens in long. Assigning an already-overflowed int product into a long would not recover the original value.
The two constructor bounds make that long result safe. Whole cents and a wider type still need explicit range reasoning. A zero-priced item may have positive quantity and zero stock value; never use value == 0 to infer that stock must be empty.
JAVA 21 COMPILER
Main.java · Widen before multiplying a price by stock
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output · inspect both the returned outcome and the remaining state
Value cents: 10000000000
Explain the cast before running the maximum case
Both fields are ints. Without the operand cast, multiplication would happen in int before a long return value received it. The valid maximum product is ten billion cents, so the calculation must widen before multiplying. The constructor bounds then establish why that long product is safe.
Queries report the current model without changing it
summary builds a String using SKU, name, current quantity, and the current calculated value. It does not sell stock, restock, print, or change metadata. Repeated getters and reports must leave every field alone. Java does not enforce read-only behavior merely because a method is called a query.
A saved summary String remains the old report after a later sale. The object has changed, but the earlier returned value is not a live binding to its fields. Call summary again when you want the current state; the example deliberately prints both versions.
Keep all console output in main, where the complete scenario is coordinated. The model should not read keyboard input or format an interactive menu. This separation makes the same constructors and methods easy to exercise from tests or a future caller without importing user-interface responsibilities into the class.
JAVA 21 COMPILER
Main.java · Derive a fresh value without mutating state
Predict the result, edit Main.java, then run your program.
Start with fields, constants, and constructors; test the two creation forms and invalid inputs. Add getters next so you can inspect state through the intended API. Implement one action at a time, then add derived value and summary only after the numeric rules work.
For the final scenario, begin with twelve pens at 150 cents and four notebooks at 350 cents. Restock pens by eight, sell five, reject sixteen, reject zero, reject a restock of 986, then accept 985. Pens finishes at 1000 while notebooks stays at four; their combined stock value is 151400 cents.
Finish with boundary and sequence tests: empty stock, full stock, exact sell-out, exact fill, invalid construction, zero-price items, maximum valuation, aliases, independent objects, and repeated queries. Do not add inheritance just because it comes next in the roadmap. This challenge is complete when its contracts are clear and verified, not when it has the most classes.
Predict the final scenario by hand
Pens begins at twelve; adding eight reaches twenty.
Selling five reaches fifteen.
Sixteen, zero, and a restock of 986 are rejected, preserving fifteen.
Adding 985 exactly fills stock to 1000.
Pens value is 1000 × 150 = 150000 cents; notebooks is 4 × 350 = 1400.
The combined value is 151400 cents, and queries leave both quantities unchanged.
Repair exercise: a rejected sale already changed the stock
This private quantity is still corrupted by its own method. Selling thirteen from twelve returns false but leaves negative stock. Move validation before mutation, reject nonpositive amounts too, and keep the earlier quantity on every rejected request.
JAVA 21 COMPILER
Main.java · Find the partial-update bug
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
The valid Java starter currently prints this invalid model state:
Accepted: false
Quantity: -1
Reveal the explained repair
The wrong version committed the subtraction before deciding it was invalid. The fixed version checks the positive-amount and availability contracts first. Every false path now occurs before assignment. Clamping negative stock to zero would still be wrong because it would silently turn a rejected request into a different accepted-looking state change.
JAVA 21 COMPILER
Main.java · Reject before subtracting
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Expected output after repair
Accepted: false
Quantity: 12
Independent challenge: build the complete inventory model
Build the complete InventoryItem from its written contracts before revealing the solution. Use one public Main and a separate package-private InventoryItem in Main.java so every program runs in the existing website sandbox. Keep the model independent of printing and persistence.
Define shared int constants MAX_QUANTITY = 1000 and MAX_UNIT_CENTS = 10_000_000. Keep sku, name, and unitPriceCents private final, and quantity private and mutable; no static quantity or public setters.
Implement a full public constructor (String sku, String name, int unitPriceCents, int quantity). Check SKU then Name with a private requireLabel helper, rejecting null/blank with <label> must not be blank. and stripping accepted labels. Reject price outside 0..10000000 with Unit price must be 0..10000000 cents.; reject quantity outside 0..1000 with Quantity must be 0..1000. Only assign fields after validation.
Provide the three-argument constructor delegating to zero quantity. Expose read-only getSku, getName, getUnitPriceCents, and getQuantity. Zero price and zero quantity are valid.
Implement public boolean restock(int amount) and sell(int amount). Reject nonpositive amounts and any out-of-range result before mutation, return false without changing state on rejection, and true only when the requested change was applied. Use remaining capacity for restock.
Implement public long getStockValueCents() with widening before multiplication. Implement summary() returning <sku> | <name> | qty: <quantity> | value cents: <value>. Neither queries nor reports may mutate state or print.
Follow the Pens and Notebooks scenario below exactly. Print every outcome, both final reports, the combined long value, object independence, and the repeated-query state check. Then test the documented creation and operation boundaries, not just the sample.
Use pens at 150 cents with twelve units and notebooks at 350 cents with four units. Run the exact request sequence in the starter comments. Expected output:
Restock eight: true
Sell five: true
Oversell sixteen: false
Zero sale: false
Overfill: false
Fill exactly: true
PEN-01 | Pens | qty: 1000 | value cents: 150000
NOTE-01 | Notebooks | qty: 4 | value cents: 1400
Combined value cents: 151400
Distinct objects: true
Queries unchanged: true
JAVA 21 COMPILER
Main.java · Assemble your inventory model
Predict the result, edit Main.java, then run your program.
publicclassMain{publicstaticvoidmain(String[] args){// TODO: create the two items through validated constructors.// TODO: restock 8, sell 5, reject 16 and 0, reject 986, accept 985.// TODO: print both reports, combined value, and identity/query checks.}}classInventoryItem{publicstaticfinalintMAX_QUANTITY=1000;publicstaticfinalintMAX_UNIT_CENTS=10_000_000;// TODO: declare private state and two constructors.// TODO: add read-only access, restock, sell, derived value, and summary.}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Stage 1 hints: initialization and observations
Write the fields and full constructor first. Normalize labels into locals, validate price and quantity, then assign fields using this. Delegate the shorter constructor to zero stock. Add getters and check both valid and rejected construction before implementing actions.
Stage 2 hints: controlled changes
Both methods reject nonpositive amounts. Restock compares against remaining capacity; sell compares against available quantity. Assign only after acceptance, with no public setter and no printing inside the model. Test exact boundaries as well as one beyond them.
Stage 3 hints: derived value and the complete scenario
Calculate value with a cast on an operand before multiplication. Let summary call that query instead of storing another mutable total. Main prints action outcomes, queries both receivers, combines long values, and checks that reading reports leaves state unchanged.
Reveal the complete explained solution
JAVA 21 COMPILER
Main.java · Complete inventory challenge
Predict the result, edit Main.java, then run your program.
Select Run code to compile and execute this program.
Pens moves from twelve to twenty to fifteen. Overselling, zero sale, and exceeding capacity leave fifteen unchanged. Restocking exactly 985 then reaches 1000. Notebooks remains at four. The two long value queries give 150000 and 1400 cents, and no report or getter changes either quantity.
Test changes and boundaries
Write predictions before running variants. Check metadata, outcomes, quantities, and values, not just the last output line. Reset restores the supplied program.
Construct with both overloads. Accept zero and maximum price/quantity. Reject null/blank SKU and name, invalid price, and invalid quantity; check error precedence in SKU, Name, price, quantity order.
Test positive, zero, negative, Integer.MIN_VALUE, and Integer.MAX_VALUE amounts for both actions. Assert the boolean outcome and every observable field after rejection.
Accept exact fill and exact sell-out, then reject one beyond each boundary. Run a sequence rather than recreating an item for every call, so stale state and partial updates cannot hide.
Verify stock value at maximum bounds is 10000000000 cents. Test a zero-price item with positive stock, and repeated reports before and after a sale. A saved String is a snapshot, not a live view.
Use distinct instances with matching labels, plus an alias of one. Confirm receiver-specific changes, unchanged metadata, and read-only queries. Direct quantity writes and calls to the private helper from Main must fail compilation.
Common mistakes and how to diagnose them
Returning false after changing the quantity
Rejection is not an undo operation. Validate before assignment and verify the earlier state remains exactly intact. Do not clamp a failed sale to zero or roll back after potentially unsafe arithmetic.
Allowing negative sales to act as restocks
The API separates two intentions, each taking a positive amount. Negative and zero requests are rejected by both methods so a sign mistake cannot silently invoke a different operation.
Making quantity static or exposing a setter
Stock belongs to one item and changes through the documented actions. Static storage merges independent items, while a blind setter bypasses the invariant even if the field itself is private.
Caching stock value as another mutable field
A cached total can drift after a missed update. Derive it from fixed unit price and current quantity. Cast an operand before multiplication so the maximum result is calculated in long rather than overflowing as int.
Claiming global SKU uniqueness
The item validates a label, not a catalog. Two separate objects may have the same SKU in this exercise. A real uniqueness constraint requires a separate catalog or persistence design and is intentionally outside scope.
Adding persistence, transfers, or inheritance before the contracts work
The challenge is one correct item model plus a deterministic scenario. Keep Main responsible for orchestration and printing, and avoid unrelated architecture that hides missing boundary checks.
Frequently asked questions
Why use separate restock and sell methods?
They make the caller's intent clear and give each method a positive-amount contract. Rejecting negative requests prevents an accidental sign from reversing an operation.
Can zero price or zero stock be valid?
Yes, both are allowed by this exercise. A zero-priced item can have positive stock, so a zero stock value does not prove the quantity is zero.
Why is the stock value a long?
The validated maximum is 10,000,000 cents times 1000 units, or 10,000,000,000 cents. That exceeds int range. Widen before multiplying, and retain the bounds that make the long result safe.
Why does constructor rejection throw while operations return false?
The constructor cannot return a boolean in place of an initialized instance. Its documented invalid inputs throw IllegalArgumentException. Normal restock and sell rejection instead returns false without changing the existing valid item.
Should this challenge use packages or inheritance?
Keep the runnable solution as one Main.java with a separate package-private model. The previous lesson showed how to split classes locally; inheritance comes next. Neither is necessary to prove this item's contracts.
A small model is complete when its contracts hold together
The constructor establishes the invariant; private fields and intentional methods preserve it. Each instance owns its stock, every rejected request preserves earlier state, and value is derived from current quantity with safe arithmetic. Main coordinates a scenario without taking ownership of the model's rules.
Before moving on, explain the difference between a rejected request and a rollback, why a zero-priced item can still have stock, and why the maximum value needs long arithmetic. Next: inheritance and super, where you will learn when an is-a relationship is appropriate and when composition is clearer.
KNOWLEDGE CHECK
Can you defend the inventory model's contracts?
Answer all six questions and read the explanations. Answers are temporary on this page and do not save course progress.
References and related learning
The policy, examples, explanations, and exercises are original SovranCode material. This is a single-threaded teaching simulation, not a production inventory or checkout system. References document the Java 21 rules supporting the implementation.