Classes and objects in Java — theory with students and courses (and the array full of null that made me think Java hated me)
The first time I built a list of students in Java, I wrote this with total confidence:
Student[] group = new Student[30]; System.out.println(group[0].getName());
Java answered with a NullPointerException. I thought new Student[30] created thirty students. What it actually creates is thirty empty slots where you could store students, and every slot is null until you put one in. That difference between “room for an object” and “the actual object” is exactly what we’re going to take apart in this article.
If you’re coming from Python, a lot of this will feel familiar: class, __init__, self… Java has all of it, just with different names and a couple of stricter rules. We’ll use an example that explains itself: students and courses.
Table of Contents
Class and object: the cookie cutter and the cookies
A class is a cookie cutter: it describes what data each thing will have and what it can do. An object is a cookie made with that cutter: one concrete thing, with its own data.
- The
Studentclass says: “every student has an ID, a name and a grade, and can tell you whether they’ve passed”. anaandluisare two objects of that class, each with their own ID, name and grade.
Objects are created with new:
Student ana = new Student("11111111H", "Ana", 7.5);
Student luis = new Student("22222222J", "Luis");
new does three things: it reserves memory for a new object, runs the constructor to give it its initial values, and returns a reference to that object. That reference is what gets stored in the variable ana.
In Python, Student("11111111H", "Ana", 7.5) would be the equivalent call. In Java, new is mandatory: without it, there’s no object.
The complete Student class
Before explaining each piece, here’s the whole class so you have the map in front of you:
public class Student {
// ----- Class attributes (static): one for the WHOLE class -----
public static final double PASS_MARK = 5.0; // shared constant
private static int totalStudents = 0; // how many objects have been created
// ----- Instance attributes: each object has its own -----
private final String id; // never changes: final
private String name;
private double grade;
// Full constructor
public Student(String id, String name, double grade) {
this.id = id; // this.id = the attribute; id = the parameter
this.name = name;
setGrade(grade); // reuse the setter's validation
totalStudents++;
}
// Overloaded constructor: no grade, starts at 0
public Student(String id, String name) {
this(id, name, 0.0); // calls the constructor above
}
// ----- Getters and setters -----
public String getId() { return id; }
public String getName() { return name; }
public double getGrade() { return grade; }
public void setGrade(double grade) {
if (grade < 0 || grade > 10) {
System.out.println("Invalid grade for " + name + ": " + grade);
return; // change nothing
}
this.grade = grade;
}
// ----- Instance methods -----
public boolean hasPassed() {
return grade >= PASS_MARK;
}
// ----- Class method -----
public static int getTotalStudents() {
return totalStudents;
}
@Override
public String toString() {
return name + " (" + id + "): " + grade;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // same reference
if (!(o instanceof Student)) return false; // another type or null
Student other = (Student) o;
return id.equals(other.id); // same ID = same student
}
@Override
public int hashCode() {
return id.hashCode();
}
}
It looks like a lot, but it’s only five ideas: attributes, constructors, encapsulation, static and the toString/equals methods. Let’s go one by one.
Grades here go from 0 to 10, with 5 as the pass mark, as in Spanish universities.
Attributes: each object’s data
Attributes are variables that live inside the object. id, name and grade are instance attributes: each student has their own, and changing Ana’s grade doesn’t touch Luis’s.
If an attribute isn’t initialized, Java gives it a default value: 0 for numbers, false for boolean, and null for objects (including Strings). That’s why a Student created with the second constructor has a grade of 0.0.
final on an attribute means “assigned once and never changed”. It’s perfect for data that identifies the object, like the ID. If you try to change it later, the code won’t even compile.
Constructors and this
The constructor is the special method that runs with new. It has the same name as the class and no return type (not even void).
What is this for?
Look at this line from the constructor:
this.id = id;
There are two variables called id: the parameter the constructor receives and the object’s attribute. Inside the constructor, plain id is the parameter (the closest one). this.id is the attribute of the object being built. this means “this object”: it’s the equivalent of Python’s self, except that in Java you don’t write it as the first parameter.
Writing name = name; without this. It compiles without complaint, but it assigns the parameter to itself: the attribute stays null. It’s one of Java’s quietest bugs. (With id, which is final, at least the compiler warns you that it’s never initialized.)
Constructor overloading
A class can have several constructors, as long as they take different parameters. That’s called overloading. Student‘s second constructor doesn’t repeat any code: it calls the first one with this(id, name, 0.0).
public Student(String id, String name) {
this(id, name, 0.0); // must be the FIRST line
}
That way all the logic (validating the grade, counting students) lives in one place. If it changes tomorrow, you change it once.
If you don’t write any constructor, Java gives you an empty one with no parameters for free. As soon as you write one, that freebie disappears: new Student() would stop compiling.
Encapsulation: private, getters and setters
Why are the attributes private? So that nobody outside can touch them directly. If grade were public, anyone could write luis.grade = 12; and you’d have a student with 12 out of 10.
With private, the only way to change the grade is through setGrade, and that method checks the value before accepting it:
luis.setGrade(12); // Invalid grade for Luis: 12.0 luis.setGrade(4.5); // accepted System.out.println(luis.getName() + " passed? " + luis.hasPassed());
Output:
Invalid grade for Luis: 12.0 Luis passed? false
That’s encapsulation: the object protects its data and decides what can be done with it. Notice too that the constructor calls setGrade instead of this.grade = grade: that way the validation also applies when the student is created.
| Modifier | Who can use it? |
|---|---|
public | Any class |
private | Only the class itself |
| (nothing) | Classes in the same package |
protected | The package and subclasses (you’ll see it in Inheritance) |
A practical rule for FP2: attributes private, methods public. Only create a setter if that data really needs to change; id has no setter because it must never change.
static: what belongs to the class, not the object
So far, every attribute belonged to an object. But some data belongs to the whole class: for example, how many students have been created. That isn’t Ana’s or Luis’s; it belongs to “students” in general. That’s what static is for.
private static int totalStudents = 0; // exists ONLY ONCE public static final double PASS_MARK = 5.0;
totalStudentsis shared: everynew Student(...)increments it, and everyone sees the same value.PASS_MARKis a class constant:static(one for everyone) andfinal(doesn’t change). By convention, in UPPERCASE.
static methods are called with the class name, without needing any object:
System.out.println("Total: " + Student.getTotalStudents()); // Total: 2
System.out.println(Student.PASS_MARK); // 5.0
From a static method you can’t use instance attributes (name, grade) or this. It makes sense: a class method doesn’t know which student you’re talking about. If you try, the error is non-static variable cannot be referenced from a static context. Sound familiar? main is static too.
| Instance | static (class) | |
|---|---|---|
| How many? | One per object | One for the whole class |
| Used with… | ana.getGrade() | Student.getTotalStudents() |
Can it use this? | Yes | No |
| Example | grade, hasPassed() | totalStudents, PASS_MARK |
References: the variable is not the object
This is the hardest idea and the one that prevents the most bugs. A variable of type Student doesn’t contain the student: it contains a reference, a sort of address pointing to where the object is.
Student sameOne = ana; // does NOT copy Ana: points to the same object sameOne.setGrade(9); System.out.println(ana.getGrade()); // 9.0
When you change the grade through sameOne, Ana’s changes, because they’re the same object with two names. If you’ve read the arrays article, it’s exactly what happened with int[] b = a;.
== versus equals
Student copy = new Student("11111111H", "Ana", 9);
System.out.println(ana == copy); // false: two different objects
System.out.println(ana.equals(copy)); // true: same ID
System.out.println(ana == sameOne); // true: same reference
==asks whether two variables point to the same object.equalsasks whether two objects are equal in the sense you define. That’s why we overrode it: for us, two students are the same if they have the same ID.
It’s the same story as with Strings, except now you write the equals yourself.
Overriding equals and forgetting hashCode. With arrays you won’t notice anything, but when you get to collections like HashSet or HashMap, two “equal” students would be treated as different. The rule: if two objects are equals, they must have the same hashCode.
null: the reference that points nowhere
null is an empty reference. Calling a method on it throws a NullPointerException:
Student nobody = null; nobody.getName(); // NullPointerException Student[] group = new Student[3]; System.out.println(group[0]); // null: the slot exists, the student doesn't
That’s my story from the beginning: an array of objects is created full of null, and each student has to be created with its own new.
toString: how an object is printed
If you print an object without toString, Java shows something like Student@1b6d3586: the class name and an internal code. When you override toString, you decide the text:
System.out.println(ana); // Ana (11111111H): 7.5
println calls toString() automatically. The @Override annotation tells the compiler “I’m replacing a method that already exists”; if you misspell the name (tostring, say), it warns you instead of quietly creating a new method that nobody will ever call.
Objects inside objects: the Course class
A course has students. In Java you express that with an attribute that’s an array of objects:
public class Course {
private final String name;
private final Student[] students;
private int numStudents = 0;
public Course(String name, int capacity) {
this.name = name;
this.students = new Student[capacity]; // empty slots: all null
}
public boolean enroll(Student s) {
if (numStudents == students.length) return false; // full
students[numStudents++] = s;
return true;
}
public double averageGrade() {
if (numStudents == 0) return 0;
double sum = 0;
for (int i = 0; i < numStudents; i++) sum += students[i].getGrade();
return sum / numStudents;
}
public int passed() {
int n = 0;
for (int i = 0; i < numStudents; i++) if (students[i].hasPassed()) n++;
return n;
}
public String getName() { return name; }
}
Notice that the loops go up to numStudents, not students.length: the unused slots are null, and calling getGrade() on them would be another NullPointerException. It’s the array + counter pattern you already used in the arrays exercises.
Course fp2 = new Course("FP2", 3);
fp2.enroll(ana);
fp2.enroll(luis);
fp2.enroll(new Student("33333333P", "Marta", 5));
System.out.println(fp2.enroll(new Student("44444444A", "Pablo", 6))); // false: full
System.out.printf("%s: average %.2f, passed %d%n", fp2.getName(), fp2.averageGrade(), fp2.passed());
Output:
false FP2: average 6.17, passed 2
Ana has a 9 (we set it through sameOne), Luis a 4.5 and Marta a 5: an average of 6.17 and two passes, because 5 >= PASS_MARK.
Watch it step by step in Python Tutor
Python Tutor runs your code line by line and draws the variables and objects. Choose Java in the drop-down, paste this program and step through it with Next:
public class Tutor {
static class Student {
String name;
double grade;
Student(String name, double grade) {
this.name = name;
this.grade = grade;
}
}
public static void main(String[] args) {
Student a = new Student("Ana", 7.5);
Student b = a; // b points to the SAME object
Student c = new Student("Ana", 7.5); // new object with the same data
b.grade = 9;
System.out.println(a.grade + " " + c.grade);
System.out.println((a == b) + " " + (a == c));
}
}
Output:
9.0 7.5 true false
What to watch:
- When the first
newruns, an object appears on the right (the heap) with an arrow froma. The variable doesn’t contain the object: it points to it. - Inside the constructor you’ll see a frame with
this: it’s the arrow to the object being built. - After
Student b = a;, no new object appears: just another arrow to the same one. After the secondnew, there are two objects. - When
b.grade = 9runs, the single object thataandbpoint to changes. The onecpoints to keeps 7.5. That’s whya == bistrueanda == cisfalse.
Summary and quick table
| Concept | In Java | If you come from Python |
|---|---|---|
| Template | class Student { … } | class Student: |
| Create an object | new Student(…) | Student(…) |
| Constructor | public Student(…) | def __init__(self, …) |
| The object itself | this (implicit) | self (explicit) |
| Hide data | private | _name convention |
| Class data | static | class attribute |
| Object as text | toString() | __str__ |
| Equality | equals() + hashCode() | __eq__ + __hash__ |
Three ideas to keep:
- The variable isn’t the object: it’s a reference. Assigning copies the arrow, not the data, and
==compares arrows. - **
this.x = x** tells the attribute apart from the parameter; withoutthis, the attribute stays empty. - **
private+ methods protect the object;static** is for what belongs to the whole class.
In the practice article you’ll build three complete programs with classes: a bank account, a library and a lab inventory. And if you want to review the previous topic, there’s regular expressions in Java.
Next step: practiceClasses and objects in Java — practice with a bank, a library and a lab inventory (and the account that belonged to nobody)Did this article help you? Here is how you can support SergioLearns →

4 Comments