Classes and objects in Java — exercises with products, fractions and a shop (and the static that emptied the whole warehouse)

In my first shop assignment I put static in front of stock without really thinking about it. I’d seen it on main and it looked “more Java”. Everything worked until I added a second product: suddenly the coffee had the same stock as the tea. I’d sell a tea and the coffee would go down. I had turned a value that belonged to each product into a value shared by all of them.

In these exercises you’ll write classes from scratch: decide which attributes belong to each object and which to the class, protect the data with private, write equals and toString, and get several classes working together. And before the solutions, three snippets with a catch (the first one is my static exactly). If you need a refresher first, there’s the theory and the practice for this topic.

How to use this article

  1. Read the statement and the expected output.
  2. Give it at least 15 minutes before opening the hint.
  3. Test the weird cases too: negative values, repeated objects, full arrays.
  4. Compare with the solution and read the reasoning behind each decision.

Each class goes in its own file (Product.java, Fraction.java, Shop.java) plus a Main.java with the tests. The exercises build on each other: the Shop uses the Product from level 1.

Level 1 — Basic: the Product class

Statement. Create the Product class with a name, a price and a stock, all private:

  • The constructor takes all three. If the price or the stock is negative, store 0 instead.
  • getName(), getPrice(), getStock().
  • boolean setPrice(double price): rejects negative prices.
  • boolean sell(int units): only sells if there’s enough stock and the units are positive.
  • void restock(int units) and boolean isSoldOut().
  • toString() with the format Coffee: 3.50 € (10 units).

Tests and expected output:

Product coffee = new Product("Coffee", 3.5, 10);
Product weird = new Product("Error", -2, -5);
System.out.println(coffee);
System.out.println(weird);
System.out.println(coffee.sell(4) + " " + coffee.sell(20) + " " + coffee.setPrice(-1));
coffee.restock(5);
System.out.println(coffee);
Coffee: 3.50 € (10 units)
Error: 0.00 € (0 units)
true false false
Coffee: 3.50 € (11 units)

The name can be final (a product doesn’t change its name). For toString, String.format("%s: %.2f € (%d units)", …).

Level 2 — Intermediate: an immutable Fraction

Statement. Create Fraction with a private final numerator and denominator. Once created, a fraction never changes (it’s immutable):

  • The constructor leaves it simplified (2/4 → 1/2) and with the sign on top (1/−3 → −1/3). If the denominator is 0, it throws an IllegalArgumentException (just throw new IllegalArgumentException("…"); you’ll study exceptions properly later).
  • Fraction add(Fraction o) and Fraction multiply(Fraction o) return a new fraction.
  • equals, hashCode and toString (1/2; if the denominator is 1, just the number).

Tests and expected output:

Fraction a = new Fraction(1, 2), b = new Fraction(2, 4), c = new Fraction(1, -3);
System.out.println(a + " " + b + " " + c);
System.out.println(a.equals(b) + " " + (a == b));
System.out.println(a.add(c) + " " + a.multiply(new Fraction(4, 1)) + " " + new Fraction(6, 3));
System.out.println(a.add(new Fraction(1, 2)));
1/2 1/2 -1/3
true false
1/6 2 2
1

Greatest common divisor with Euclid’s algorithm: while b != 0, r = a % b; a = b; b = r;. If you simplify in the constructor, equals only needs to compare numerator and denominator.

Level 3 — Challenge: a Shop with several products

Statement. Create Shop, which stores products in a fixed-capacity array:

  • Constructor with a name and a capacity.
  • Product find(String name): case-insensitive; null if it isn’t there.
  • boolean add(Product p): fails if the shop is full or there’s already a product with that name.
  • boolean sell(String name, int units).
  • double inventoryValue() (price × stock of everything) and int soldOut().
  • A counter across all shops with the units sold: static int getTotalSales().

Tests and expected output:

Shop downtown = new Shop("Downtown", 3);
Shop beach = new Shop("Beach", 2);
downtown.add(new Product("Coffee", 3.5, 10));
downtown.add(new Product("Tea", 2.0, 5));
System.out.println(downtown.add(new Product("coffee", 9, 1)));   // repeated
downtown.add(new Product("Cookies", 1.8, 2));
System.out.println(downtown.add(new Product("Milk", 1.1, 6)));   // full
beach.add(new Product("Ice cream", 2.5, 8));

System.out.println(downtown.sell("tea", 5) + " " + downtown.sell("Cookies", 3) + " " + beach.sell("Ice cream", 2));
System.out.printf("Inventory %s: %.2f € · sold out: %d%n", downtown.getName(), downtown.inventoryValue(), downtown.soldOut());
System.out.println("Total sales: " + Shop.getTotalSales());
false
false
true false true
Inventory Downtown: 38.60 € · sold out: 1
Total sales: 7

An array of products + a numProducts counter, like the Library from the practice article. The shop’s sell should rely on the product’s sell, not repeat its logic. And think carefully about which attribute is static: only one.

What’s wrong here?

Three snippets that compile. Before reading the explanation, try to predict what they print.

Snippet 1 — The shared stock

class Product {
    private String name;
    private static int stock;      // whose stock is this?

    Product(String name, int stock) {
        this.name = name;
        Product.stock = stock;
    }
    int getStock() { return stock; }
}

Product coffee = new Product("Coffee", 10);
Product tea = new Product("Tea", 3);
System.out.println(coffee.getStock() + " " + tea.getStock());

It prints 3 3. With static, there’s only one stock for the whole class. When the tea is created, that single stock goes from 10 to 3, and the coffee “sees” the same value. That’s my story from the start. The rule: if each object needs its own value, it is not static. static is for things that belong to the whole class: counters, constants, totals.

Using static “because otherwise it doesn’t compile”. It usually happens when calling an instance method from main. The fix isn’t to make everything static, but to create an object and call the method on it.

Snippet 2 — Two equal fractions that aren’t

class Fraction {
    private final int num, den;
    Fraction(int num, int den) { this.num = num; this.den = den; }
}

Fraction a = new Fraction(1, 2);
Fraction b = new Fraction(1, 2);
System.out.println(a.equals(b));

It prints false. If you don’t write equals, the class inherits the one from Object, which does the same as ==: it compares references. a and b are two different objects, so they aren’t “equal”. For two fractions with the same numbers to be equal, you have to override equals (and, with it, hashCode).

Snippet 3 — The getter that opens the door

class Shop {
    private final Product[] products = new Product[3];
    private int numProducts = 0;
    // ...
    Product[] getProducts() { return products; }
}

Product[] list = shop.getProducts();
list[0] = null;
System.out.println(shop.inventoryValue());

It throws a NullPointerException. The array is private, but the getter returns the reference to the same array. Whoever receives it can change its contents and break the shop from outside: the encapsulation leaked out through the getter. The fix is to return a copy: return Arrays.copyOf(products, numProducts);. That way, whatever they do to the copy doesn’t affect the shop.

When a private attribute is an array (or any object that can be modified), ask yourself whether the getter should return a copy. With String you don’t need to, because Strings are immutable.

Solutions

Level 1 — Product

public class Product {
    private final String name;
    private double price;
    private int stock;

    public Product(String name, double price, int stock) {
        this.name = name;
        this.price = price >= 0 ? price : 0;   // never negative
        this.stock = Math.max(0, stock);
    }

    public String getName()  { return name; }
    public double getPrice() { return price; }
    public int getStock()    { return stock; }

    public boolean setPrice(double price) {
        if (price < 0) return false;
        this.price = price;
        return true;
    }

    public boolean sell(int units) {
        if (units <= 0 || units > stock) return false;
        stock -= units;
        return true;
    }

    public void restock(int units) {
        if (units > 0) stock += units;
    }

    public boolean isSoldOut() { return stock == 0; }

    @Override
    public String toString() {
        return String.format("%s: %.2f € (%d units)", name, price, stock);
    }
}

**Why there’s no setStock. Stock should only change when you sell or restock**. A setStock(int) would let anyone bypass those rules (setting it to −5, for example). Setters are written when they make sense, not out of habit.

Why the constructor corrects instead of rejecting. A constructor can’t return false. The options are to correct the value (what we do, Math.max(0, stock)) or to throw an exception, as in Fraction. Correcting is simpler to start with; throwing is more honest when the data can’t be fixed.

**Why units <= 0. Selling 0 or −3 units makes no sense and, with −3, the stock would actually go up**! Validating the “weird” values too is what makes a class robust.

Level 2 — Fraction

public final class Fraction {
    private final int num;
    private final int den;

    public Fraction(int num, int den) {
        if (den == 0) throw new IllegalArgumentException("The denominator can't be 0");
        if (den < 0) { num = -num; den = -den; }    // the sign always goes on top
        int g = gcd(Math.abs(num), den);
        this.num = num / g;
        this.den = den / g;
    }

    private static int gcd(int a, int b) {
        while (b != 0) { int r = a % b; a = b; b = r; }
        return a == 0 ? 1 : a;
    }

    public Fraction add(Fraction o) {
        return new Fraction(num * o.den + o.num * den, den * o.den);
    }

    public Fraction multiply(Fraction o) {
        return new Fraction(num * o.num, den * o.den);
    }

    public double value() { return (double) num / den; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Fraction)) return false;
        Fraction f = (Fraction) o;
        return num == f.num && den == f.den;    // both already simplified
    }

    @Override
    public int hashCode() { return 31 * num + den; }

    @Override
    public String toString() { return den == 1 ? String.valueOf(num) : num + "/" + den; }
}

Why immutable. If add modified this, after a.add(c) the fraction a would no longer be 1/2, and anyone using it would see the change by surprise (the shared references from the theory article). By returning a new fraction, a stays 1/2 forever. It’s what Strings do too.

**Why gcd is private static.** It’s an internal helper (nobody outside needs it) and it doesn’t use any of the object’s attributes: it only works with its parameters. That’s why it can be static.

**Why return a == 0 ? 1 : a.** For the fraction 0/5, the gcd of 0 and 5 is 5, which leaves 0/1 (printed as 0), and that’s correct. gcd would only return 0 if both numbers were 0, which the constructor already prevents; the check is a cheap safeguard against ever dividing by 0 if someone reuses gcd elsewhere.

**Why 31 * num + den.** hashCode must give the same number for fractions that are equals. Since they’re already simplified, combining numerator and denominator is enough. Multiplying by a prime (31) is the usual habit to spread the values out better.

Level 3 — Shop

public class Shop {
    private static int totalSales = 0;          // across ALL shops

    private final String name;
    private final Product[] products;
    private int numProducts = 0;

    public Shop(String name, int capacity) {
        this.name = name;
        this.products = new Product[capacity];
    }

    public Product find(String name) {
        for (int i = 0; i < numProducts; i++) {
            if (products[i].getName().equalsIgnoreCase(name)) return products[i];
        }
        return null;
    }

    public boolean add(Product p) {
        if (numProducts == products.length || find(p.getName()) != null) return false;
        products[numProducts++] = p;
        return true;
    }

    public boolean sell(String name, int units) {
        Product p = find(name);
        if (p == null || !p.sell(units)) return false;
        totalSales += units;
        return true;
    }

    public double inventoryValue() {
        double total = 0;
        for (int i = 0; i < numProducts; i++) total += products[i].getPrice() * products[i].getStock();
        return total;
    }

    public int soldOut() {
        int n = 0;
        for (int i = 0; i < numProducts; i++) if (products[i].isSoldOut()) n++;
        return n;
    }

    public static int getTotalSales() { return totalSales; }

    public String getName() { return name; }
}

**Why totalSales is static and products isn’t. Total sales belong to all** the shops together: that’s why the beach shop’s 2 units add up with downtown’s 5 (7 in total). The products, on the other hand, belong to each shop. It’s snippet 1 in reverse: here static is exactly what we want.

**Why !p.sell(units).** The shop doesn’t check the stock: it asks the product to sell and looks at whether it could. The “don’t sell more than you have” rule lives in Product and isn’t repeated. On top of that, totalSales only goes up if the sale really happened.

**Why equalsIgnoreCase.** So “coffee” and “Coffee” are the same product, both when searching and when preventing duplicates. That’s why the third add fails.

**Why the loops go up to numProducts.** The unused positions are null: looping up to products.length would throw a NullPointerException as soon as the shop wasn’t full.

Watch it step by step in Python Tutor

Let’s watch snippet 1 go wrong live. Open Python Tutor, choose Java, paste this code and step through it:

public class Tutor {
    static class Product {
        private String name;
        private static int stock;      // whose stock is this?

        Product(String name, int stock) {
            this.name = name;
            Product.stock = stock;
        }
        int getStock() { return stock; }
    }

    public static void main(String[] args) {
        Product coffee = new Product("Coffee", 10);
        Product tea = new Product("Tea", 3);
        System.out.println(coffee.getStock() + " " + tea.getStock());
    }
}

Output:

3 3

What to watch:

  1. Notice that stock doesn’t appear inside the objects: Python Tutor shows it separately, as something that belongs to the class. Each object only has its name.
  2. When the coffee is created, stock becomes 10. When the tea is created, that same stock becomes 3.
  3. Both calls to getStock() read the same value: that’s why you get 3 3.
  4. Remove static (and change Product.stock to this.stock) and run it again: now each object has its own stock inside and the output is 10 3.

Cheat sheet: classes and objects in Java

The essentials in 30 seconds

  • A class is the template; an object is created with new, and the variable holds a reference to it.
  • Attributes private, methods public. Getters to read; setters only if it makes sense to change the data, and validating it.
  • this.x = x in the constructor: without this, the attribute stays empty.
  • static = one for the whole class (counters, constants, totals). If each object needs its own value, it is not static.
  • == compares references; equals compares whatever you decide. If you override equals, override hashCode too.

Syntax

I want to…CodeResult
Declare a classpublic class Product { … }template
Protected attributeprivate double price;only reachable from the class
Attribute that never changesprivate final String name;assigned once
Constructorpublic Product(String n, double p) { this.name = n; … }runs with new
Call another constructorthis(name, 0); (first line)reuses code
Create an objectProduct p = new Product("Coffee", 3.5, 10);reference to a new object
Getter / settergetPrice() / setPrice(double)controlled read / write
Boolean getterisLent()is… convention
Class attributeprivate static int counter;one for everyone
Constantpublic static final double VAT = 0.21;shared and immutable
Class methodShop.getTotalSales()no object needed
Object as text@Override public String toString()used by println
Equality@Override public boolean equals(Object o) + hashCode()a.equals(b)
Array of objectsnew Product[3]3 slots holding null

Templates from the exercises

A method that validates and reports (Product):

public boolean sell(int units) {
    if (units <= 0 || units > stock) return false;
    stock -= units;
    return true;
}

coffee.sell(4) → true · coffee.sell(20) → false

equals for an immutable object (Fraction):

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Fraction)) return false;
    Fraction f = (Fraction) o;
    return num == f.num && den == f.den;
}

new Fraction(1, 2).equals(new Fraction(2, 4)) → true

Array of objects + counter (Shop):

private final Product[] products = new Product[capacity];
private int numProducts = 0;
// ...
for (int i = 0; i < numProducts; i++) { … products[i] … }

Example inventory → 38.60 € · total sales → 7

Most repeated errors

SymptomLikely cause
An attribute is null even though you passed it to the constructorMissing this.: name = name;
All objects share the same valueAttribute marked static by mistake
non-static variable cannot be referenced from a static contextYou’re using an instance attribute from main or another static method: create an object
a.equals(b) is false with equal dataYou haven’t overridden equals (it uses Object‘s, which compares references)
NullPointerException when looping over an array of objectsYou loop up to length instead of the counter, or never new-ed each object
Someone outside changes your private arrayThe getter returns the reference: return a copy with Arrays.copyOf
new MyClass() stops compilingYou wrote a constructor with parameters: the empty one is gone
Product@1b6d3586 gets printedMissing toString()

Before you submit, ask yourself

  1. Are all attributes private, and does every setter validate its input?
  2. Is every static really something that belongs to the whole class and not to each object?
  3. Do my constructors use this. and reuse code with this(...)?
  4. If I compare objects, have I overridden equals and hashCode?
Keep goingAll Java (en) articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *