RizTech Academy logo
RizTech Academy
Object-Oriented JavaLesson 1 of 830 min

Classes, objects and constructors

The arrays lesson ended with a deliberate disaster: three parallel arrays holding names, prices and counts, indexed in lockstep, destroyed the moment anything sorted one of them. That bug is the argument for this module.

A class is how you keep values that belong together from drifting apart.

The problem, stated properly

String[] names   = {"Priya", "Arjun", "Kavita"};
int[] tiffins    = {26, 18, 30};
String[] pincodes = {"411207", "411014", "411207"};

Nothing in that code says names[1] and tiffins[1] describe the same person. The relationship lives in your head. Sort one array, insert into another, or remove an entry from two of the three, and the data is silently wrong — no exception, no warning, just a report that quietly lies.

A class fixes it

public class Subscriber {
    String name;
    int tiffins;
    String pincode;
}

Three fields. The class is a description — a template. To get an actual thing, you create an instance with new:

Subscriber s = new Subscriber();
s.name = "Priya";
s.tiffins = 26;
s.pincode = "411207";

Now the three values cannot be separated. An array of Subscriber sorts as whole people.

Constructors

Setting fields one at a time after construction is a problem: between new and the last assignment the object is half-built, and nothing forces you to finish. A constructor takes the values up front.

public class Subscriber {
    String name;
    int tiffins;
    String pincode;

    Subscriber(String name, int tiffins, String pincode) {
        this.name = name;
        this.tiffins = tiffins;
        this.pincode = pincode;
    }
}
Subscriber s = new Subscriber("Priya", 26, "411207");

A constructor has the class's name and no return type, not even void. If you write none, Java supplies an invisible no-argument one — and the moment you write any constructor, that free one disappears. A class with only the three-argument constructor above has no new Subscriber() any more, which is usually what you want.

this, and the bug you get without it

this refers to the object the method is running on. In the constructor above it is disambiguating: this.name is the field, name is the parameter.

Leave it out and watch:

class Broken {
    String name;

    Broken(String name) {
        name = name;
    }
}

System.out.println("name is [" + new Broken("Arjun").name + "]");
name is [null]

It compiles. It runs. name = name assigns the parameter to itself and never touches the field, which keeps its default of null. IntelliJ flags it; javac does not.

Every constructor parameter that shares a field's name needs this. — and the right habit is to always write this. for fields, so the question never arises.

Several constructors

Overloading lets you offer convenient shorter forms, and this(...) chains one to another so the real work lives in one place:

Subscriber(String name, int tiffins, String pincode) {
    this.name = name;
    this.tiffins = tiffins;
    this.pincode = pincode;
}

Subscriber(String name, String pincode) {
    this(name, 26, pincode);       // the standard monthly plan
}

this(...) must be the first statement in the constructor. One constructor should do the assigning and the rest should delegate to it, or validation added later gets added to one of them and forgotten in the others.

Methods

A method is behaviour that belongs with the data:

int billPaise() {
    return tiffins * PRICE_PER_TIFFIN_PAISE;
}

boolean isInServiceArea() {
    return pincode.startsWith("4112");
}

Inside a method, the fields are simply available — you are already on an object. This is the real difference from a function taking a Subscriber as a parameter: the method is part of the thing.

toString, and a familiar-looking piece of noise

Subscriber s = new Subscriber("Priya Deshmukh", 26, "411207");
System.out.println(s);
Subscriber@799f10e1

That is the same shape as [I@2f4948e4 from the arrays lesson, and the same cause: every class inherits a toString() from Object that prints the type name and an identity hash, and yours has not replaced it.

@Override
public String toString() {
    return "%s (%s) — %d tiffins".formatted(name, pincode, tiffins);
}
Priya Deshmukh (411207) — 26 tiffins

Write toString on every class that holds data. It costs three lines and it is the difference between a useful log line and Subscriber@799f10e1 at the moment you most need to know what went wrong. The records lesson later in this module generates it for you, which is one of several reasons records exist.

References, and the aliasing trap

A variable of a class type does not hold the object. It holds a reference to it.

Subscription one = new Subscription("Priya", 26);
Subscription two = one;
two.tiffins = 5;

System.out.println(one.tiffins);
System.out.println(one == two);
5
true

There is one object and two names for it. Changing it through two changes what one sees, because they are the same thing. This is not copying — = on a reference type copies the reference, never the object.

This is the same fact behind == on strings and arrays, arriving for the third time. == on any reference type asks "the same object?". By now that should be automatic.

Aliasing is useful — it is how you pass an object to a method without copying it — and it is a bug when you expected a copy. If you need an independent object, make one explicitly.

static: belonging to the class, not the object

public class Subscription {
    static int created = 0;
    static final int PRICE_PER_TIFFIN_PAISE = 8_235;

    String name;
    int tiffins;

    Subscription(String name, int tiffins) {
        this.name = name;
        this.tiffins = tiffins;
        created++;
    }
}

created exists once, shared by every instance. name exists once per instance.

static instance
Exists Once per class Once per object
Accessed as Subscription.created s.name
Can use this No Yes
Created when The class is first loaded new runs

main is static for this reason — it runs before any object exists, as module 1 explained. A static method cannot touch instance fields, because there is no instance to touch.

static final with a capital-letters name is how Java writes a constant, and PRICE_PER_TIFFIN_PAISE in one place beats 8235 scattered through the file.

Do not make everything static. A class of static methods with no state is a namespace, not an object, and if that is what you have you may not need a class at all — a point the interfaces lesson comes back to.

Check your work

Why do three parallel arrays break? Nothing in the code records that names[1] and tiffins[1] belong together. Any operation that reorders or resizes one and not the others silently misaligns them, with no error.

What does name = name; in a constructor do? Assigns the parameter to itself. The field is never written and keeps its default — null for a String. It compiles cleanly. Write this.name = name;.

What happens to the no-argument constructor once you write your own? It disappears. Java only supplies a default constructor for a class that declares none.

Where must this(...) appear? As the first statement of the constructor.

After two = one; two.tiffins = 5;, what is one.tiffins? 5. Both variables reference one object; = copies the reference, not the object.

Why does printing an object give Subscriber@799f10e1? It inherits Object.toString(), which prints the type name and an identity hash. Override toString().

Why can a static method not use this? It belongs to the class and runs without any instance, so there is no object for this to refer to.

Practice 2, the Subscriber class. A reasonable shape:

public class Subscriber {
    static final int PRICE_PER_TIFFIN_PAISE = 8_235;

    String name;
    String pincode;
    int tiffins;

    Subscriber(String name, String pincode, int tiffins) {
        this.name = name;
        this.pincode = pincode;
        this.tiffins = tiffins;
    }

    Subscriber(String name, String pincode) {
        this(name, pincode, 26);
    }

    int billPaise() {
        return tiffins * PRICE_PER_TIFFIN_PAISE;
    }

    @Override
    public String toString() {
        return "%s (%s) - %d tiffins, Rs %d.%02d".formatted(
                name, pincode, tiffins, billPaise() / 100, billPaise() % 100);
    }
}

The fields are left open here because practice 4 needs to write to one from outside. That is exactly what the next lesson closes.

Practice 4, the aliasing surprise. Passing a Subscriber to a method that sets tiffins = 0 changes the caller's object, because the method received a copy of the reference and both point at one object. Java is always pass-by-value — but for reference types the value being passed is a reference. Reassigning the parameter inside the method (s = new Subscriber(...)) does nothing to the caller; mutating what it points at does.

Practice

  1. Reproduce the parallel-array failure. Three arrays, printed as a table. Sort one of them. Print the table again and write down, in one sentence, what the report now claims.

  2. Write Subscriber. Name, pincode, tiffin count. A three-argument constructor and a two-argument one that defaults to 26 tiffins and delegates. A billPaise() method at Rs 82.35 each. A toString. Then build an array of three and print them.

  3. Cause the this bug deliberately. Write a constructor with name = name;, print the field, and confirm you get null with no complaint from the compiler. Then look at what IntelliJ says about that line.

  4. Prove aliasing. Write static void cancel(Subscriber s) that sets s.tiffins = 0. Call it and print the caller's object afterwards. Then add s = new Subscriber("Nobody", "000000") as the last line of the method and confirm the caller does not see that. Explain the difference to yourself — it is the whole of "Java is pass-by-value" in one exercise.

  5. Add a static counter. Count how many Subscriber objects have been created. Print it after making four. Then add a static method resetCount() and notice that it cannot read name — and say why.

  6. Harder — a delivery run. A DeliveryRun holds a date, an area, and an array of Subscriber. Give it totalPaise() and a toString that prints a readable summary. Then write a main that builds two runs for different areas and prints both. Keep every amount in paise. You will rewrite this class twice in this module — once with encapsulation, once as a record — and comparing the three versions is the point.

Next: encapsulation, and the getter that hands a caller the keys to your internals.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship