Lesson 7 of 918 min2 challenges

Classes and Objects: Keeping Related Data Together

Why three parallel arrays are a disaster, and what this means in a constructor.

The problem classes actually solve

Say you need to track 100 students, each with a name, an age, and a score. With what you know so far, you'd write:

String[] names = new String[100];
int[] ages = new int[100];
double[] scores = new double[100];

This works, right up until it doesn't.

The three arrays have to stay perfectly synchronized forever. Remove student number 5 and you have to remove index 5 from all three, in three places. Miss one and every student after position 5 now has someone else's score. Nothing crashes. The data is just wrong, quietly, everywhere.

And here's the deeper issue: the fact that names[5] and scores[5] describe the same human being exists only in your head. There is nothing in the code that says so. The compiler can't help you, because you never told it there was a relationship to protect.

A class fixes exactly this: it lets you tell the compiler that certain pieces of data belong together.

A class is a blueprint; an object is what you build from it

public class Student {
    String name;
    int age;
    double score;
}

That's a blueprint. It says "a thing called a Student has a name, an age, and a score." The blueprint itself isn't a student — you can't set its name, because there's nobody there. You build from it:

Student s1 = new Student();
s1.name = "Maya";
s1.age = 18;
s1.score = 88.5;

Student s2 = new Student();
s2.name = "Sam";

System.out.println(s1.name);    // Maya
System.out.println(s2.name);    // Sam
System.out.println(s2.age);     // 0

new Student() means "build one from the blueprint." What comes out is an object (also called an instance).

s1 and s2 are fully independent. Setting s2.name doesn't touch s1.name. And s2.age prints 0 because we never set it — object fields get the same defaults as array slots: 0 for numbers, false for booleans, null for anything object-shaped.

The wrong mental model: thinking the class holds the data. It doesn't. The class describes the shape; each object holds its own separate copy of those fields. One blueprint, any number of houses.

Now those 100 students are:

List<Student> students = new ArrayList<>();
students.add(s1);
students.add(s2);

One list. Remove a student and their name, age, and score leave together, because they were never apart. The synchronization bug is gone — not fixed, but made impossible to write.

Constructors: fill it in as you build it

Assigning fields one line at a time is tedious and easy to half-finish. A constructor lets you require the values up front.

public class Student {
    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Two rules about constructors, both non-negotiable: the name must match the class name exactly, and there is no return type — not even void. If you write public void Student(...) you haven't made a constructor, you've made an ordinary method that happens to be named Student, and new Student("Maya", 18) won't compile.

Now:

Student s = new Student("Maya", 18);
System.out.println(s.name + " is " + s.age);

Prints Maya is 18.

What this means

this refers to the object currently being built. So this.name is the object's field, and the bare name on the right is the parameter that was passed in.

When the two share a name, you need this to tell them apart. Leave it off and you get this silent disaster:

public Student(String name, int age) {
    name = name;        // assigns the parameter to itself
    age = age;
}

It compiles. It runs. this.name is never touched and stays null for the object's entire life. The crash comes later, somewhere else, when something calls a method on that null — and by then the real cause is nowhere near the stack trace.

Some people sidestep this by naming parameters differently (newName). Most Java code uses this instead, because it's what everyone expects to see.

The free constructor disappears

new Student() worked earlier, before we wrote a constructor. Java hands you a no-argument constructor for free — but only if you don't write one yourself. The moment you add Student(String, int), the free one is gone and new Student() stops compiling.

If you want both, write both. Two constructors with different parameter lists is called overloading:

public Student() {
    this("Unknown", 0);      // calls the other constructor
}

public Student(String name, int age) {
    this.name = name;
    this.age = age;
}

this(...) as a statement means "run the other constructor first." It has to be the first line.

Methods: objects that can do things

A class isn't limited to data. It can hold behavior too.

public class Student {
    String name;
    double score;

    public Student(String name, double score) {
        this.name = name;
        this.score = score;
    }

    public boolean isPassing() {
        return score >= 60;
    }

    public String describe() {
        return name + ": " + score + " (" + (isPassing() ? "pass" : "fail") + ")";
    }
}
Student s = new Student("Maya", 88);
System.out.println(s.describe());

Prints:

Maya: 88.0 (pass)

Two things to notice. 88.0, not 88score is a double, and that's how a double prints. And inside describe(), isPassing() is called with no object in front of it; inside a method, the object is already implied. Same for score. The method lives in the object, so it can reach the object's fields directly.

This is the actual point of object-oriented programming: the logic for deciding whether a student passed now lives with the student's score. It isn't scattered across whatever code happens to be looking at the data.

toString: making println useful

Print an object directly and you get the same unhelpful thing arrays gave you:

System.out.println(s);      // Student@1b6d3586

Type tag plus memory hash. But unlike arrays, you can fix this — write a toString method and println will call it:

@Override
public String toString() {
    return "Student{name='" + name + "', score=" + score + "}";
}

Now:

System.out.println(s);

prints:

Student{name='Maya', score=88.0}

You didn't call toStringprintln did, because every object has one and yours replaced the default.

@Override is a note to the compiler saying "I intend to be replacing an inherited method." It's optional, and you should write it anyway: if you typo toSting, the compiler says "this doesn't override anything" instead of silently creating a brand-new method that nothing will ever call. Free insurance.

Object-oriented programming, in one sentence

Put data and the behavior that operates on that data into the same box.

Encapsulation, inheritance, polymorphism — everything that comes later grows out of that sentence. If you understand why three parallel arrays were a bad idea, you understand the foundation.

Ten-second check

public Student(String name) {
    name = name;
}

What's wrong?

The this. is missing. name = name assigns the parameter to itself and never touches the object's field, which stays null forever. The compiler is perfectly happy with it — you find out at runtime, from a NullPointerException somewhere far away.

Ad slot (AdSense not configured; ads appear here in production)

Hands-on

Reading it isn't knowing it. Writing it is.

0/2 passed

Challenges and grading are completely free, and a wrong answer costs you nothing. Stuck? Hit "Ask the AI": it can see the code you are writing.

Level 01Fill in the blank+10 pts · +15 XP

What is missing from this constructor?

This constructor has a bug — the name field stays null forever. Add the missing piece: ```java public Student(String name) { ____.name = name; } ```

Level 02Multiple choice+10 pts · +15 XP

Class vs object

Which statement is correct?

  • Might be multiple choice. Wrong answers cost nothing, so just try.

Finished? Mark it

Marking it done saves your progress, gives you 20 XP and keeps your streak alive.