oop Lesson 2 20 min read

Writing Your First Class

Fields, constructors, methods, and the this keyword

Reading: Reges & Stepp: Ch. 8 §8.2

After this lesson, you will be able to:

  • Declare fields (instance variables) with explicit access modifiers
  • Write a constructor that initializes fields using the this keyword
  • Write instance methods that read and modify an object’s data
  • Write a toString() method that returns a human-readable description
  • Create objects and call methods on them from a separate client class
  • Trace through code to determine what this refers to at each point

You Know How to Use Objects. Now Build One.

Last lesson, you learned what objects are: bundles of data and behavior, created from class blueprints with new. You have been using objects all quarter — Scanner, String, ArrayList. But you have never written a class yourself. That changes now.

By the end of this lesson, you will write a complete Student class from scratch: fields to store data, a constructor to initialize it, methods to operate on it, and a toString() to display it. Every class you write for the rest of your CS career follows this same structure.

From CSCD 110: In Python, you never wrote classes in CSCD 110. All your code lived in functions and a main block. Java organizes everything into classes. Where Python uses def __init__(self, name, gpa):, Java uses a constructor. Where Python uses self, Java uses this. The concepts map directly — the syntax is different.


The Three Parts of a Class

Every Java class has three main parts:

  1. Fields (instance variables) — the data each object holds
  2. Constructor — a special method that initializes fields when new is called
  3. Instance methods — methods that operate on the object’s data

Here is the skeleton:

public class Student {
    // 1. Fields
    private String name;
    private double gpa;
    private int id;

    // 2. Constructor
    public Student(final String name, final double gpa, final int id) {
        this.name = name;
        this.gpa = gpa;
        this.id = id;
    }

    // 3. Instance methods
    public String getName() {
        return this.name;
    }

    public double getGpa() {
        return this.gpa;
    }

    public int getId() {
        return this.id;
    }
}

We will build this piece by piece.


Fields: The Data Each Object Holds

Fields are variables declared inside the class but outside any method. Each object gets its own copy of every field. Declare them at the top with explicit access modifiers:

public class Student {
    private String name;
    private double gpa;
    private int id;
}

private means code outside the Student class cannot read or write these fields directly. This is deliberate — we control access through methods. Every field in this course should be declared private unless you have a specific reason not to.

When you create a Student object, that object gets its own name, gpa, and id. Two objects are completely independent:

Student alice = new Student("Alice", 3.8, 1001);
Student bob   = new Student("Bob",   3.2, 1002);
// alice.name is "Alice", bob.name is "Bob" — separate copies
Stack                          Heap
┌─────────────┐              ┌─────────────────────┐
│ alice: ──────┼─────────────►│ name: "Alice"       │
│             │              │ gpa:  3.8           │
│             │              │ id:   1001          │
├─────────────┤              └─────────────────────┘
│ bob:   ──────┼──────┐
│             │      │      ┌─────────────────────┐
└─────────────┘      └─────►│ name: "Bob"         │
                             │ gpa:  3.2           │
                             │ id:   1002          │
                             └─────────────────────┘

Each object on the heap has its own copy of the three fields. Changing alice’s GPA does not affect bob’s.

Common Pitfall: Do not declare fields inside a method or constructor. A variable declared inside a method is a local variable that disappears when the method ends. Fields must be declared directly inside the class body so they persist for the lifetime of the object.

Check Your Understanding

Where should fields be declared in a Java class?


The Constructor: Setting Up a New Object

The constructor is a special method that runs when you write new Student(...). It has two unique properties:

  1. Same name as the classStudent, not createStudent or init
  2. No return type — not void, not Student, nothing at all
public Student(final String name, final double gpa, final int id) {
    this.name = name;
    this.gpa = gpa;
    this.id = id;
}

When you write:

Student alice = new Student("Alice", 3.8, 1001);

Java does this:

  1. Allocates memory on the heap for a new Student object
  2. Calls the constructor, passing "Alice", 3.8, 1001
  3. The constructor sets this.name = "Alice", this.gpa = 3.8, this.id = 1001
  4. Returns a reference to the new object, stored in alice

Notice the final keyword on the constructor parameters. This is Dr. Steiner’s convention: parameters should not be reassigned inside the method. The final keyword makes the compiler enforce this.


The this Keyword

Look at the constructor body again:

public Student(final String name, final double gpa, final int id) {
    this.name = name;
    this.gpa = gpa;
    this.id = id;
}

The parameter is called name. The field is also called name. How does Java know which is which?

this is a reference to the current object — the object being constructed or the object the method was called on. this.name means “the name field of this object.” Plain name (without this) refers to the parameter, because the parameter is the closest declaration in scope.

Without this, the constructor would assign the parameter to itself:

// WRONG: assigns parameter to itself, field stays null!
public Student(final String name, final double gpa, final int id) {
    name = name;   // parameter = parameter (does nothing useful)
    gpa = gpa;     // parameter = parameter
    id = id;       // parameter = parameter
}

After this broken constructor, this.name is null, this.gpa is 0.0, and this.id is 0 — the default values for uninitialized fields.

The Trick: When a parameter has the same name as a field, use this.fieldName to refer to the field. This is the standard Java convention. Some programmers use prefixes like _name or theName for parameters to avoid the ambiguity, but this is the cleanest and most common approach.

Check Your Understanding

What happens if you write name = name; instead of this.name = name; in the constructor?


Instance Methods: Operating on Object Data

Instance methods are methods that belong to an object. They can read and modify the object’s fields. Unlike the static methods you have written so far, instance methods do not use the static keyword:

public class Student {
    private String name;
    private double gpa;
    private int id;

    public Student(final String name, final double gpa, final int id) {
        this.name = name;
        this.gpa = gpa;
        this.id = id;
    }

    // Getter: returns a field value
    public String getName() {
        return this.name;
    }

    public double getGpa() {
        return this.gpa;
    }

    public int getId() {
        return this.id;
    }

    // Setter: modifies a field with validation
    public void setGpa(final double newGpa) {
        if (newGpa >= 0.0 && newGpa <= 4.0) {
            this.gpa = newGpa;
        } else {
            throw new IllegalArgumentException("GPA must be between 0.0 and 4.0");
        }
    }

    // Behavior: uses fields to compute a result
    public boolean isHonorStudent() {
        return this.gpa >= 3.5;
    }
}

When you call a method on an object, this refers to that specific object:

Student alice = new Student("Alice", 3.8, 1001);
Student bob   = new Student("Bob",   3.2, 1002);

alice.isHonorStudent();  // this = alice → checks alice's gpa (3.8) → true
bob.isHonorStudent();    // this = bob   → checks bob's gpa (3.2)   → false

The method code is written once, but each call operates on a different object’s data. That is the power of instance methods.

Getter and Setter Naming Conventions

Java has a standard naming convention for methods that read and write fields:

Pattern Name Example
Read a field getFieldName() getName(), getGpa()
Write a field setFieldName(value) setGpa(3.5)
Read a boolean isFieldName() isHonorStudent()

Getters return the field value. Setters accept a new value and can validate it before storing — the setGpa method above rejects GPAs outside the valid range. This is the first step toward encapsulation, which you will study in depth next lesson.


The toString() Method

Every class should have a toString() method. It returns a String description of the object and is called automatically when you print an object or concatenate it with a string:

public String toString() {
    return this.name + " (ID: " + this.id + ", GPA: " + this.gpa + ")";
}

Without toString():

Student alice = new Student("Alice", 3.8, 1001);
System.out.println(alice);  // Student@6d06d69c (memory address — useless)

With toString():

System.out.println(alice);  // Alice (ID: 1001, GPA: 3.8)

Java calls toString() automatically in these situations:

  • System.out.println(alice) — prints alice.toString()
  • "Student: " + alice — calls alice.toString() for concatenation

Key Insight: toString() is not just a convenience. It is essential for debugging. When something goes wrong and you print an object to inspect it, toString() is what gives you readable output instead of a memory address.


Complete Example: The Student Class

Here is the full Student class with all the pieces assembled:

public class Student {
    // Fields
    private String name;
    private double gpa;
    private int id;

    // Constructor
    public Student(final String name, final double gpa, final int id) {
        if (gpa < 0.0 || gpa > 4.0) {
            throw new IllegalArgumentException("GPA must be between 0.0 and 4.0");
        }
        this.name = name;
        this.gpa = gpa;
        this.id = id;
    }

    // Getters
    public String getName() {
        return this.name;
    }

    public double getGpa() {
        return this.gpa;
    }

    public int getId() {
        return this.id;
    }

    // Setter with validation
    public void setGpa(final double newGpa) {
        if (newGpa < 0.0 || newGpa > 4.0) {
            throw new IllegalArgumentException("GPA must be between 0.0 and 4.0");
        }
        this.gpa = newGpa;
    }

    // Behavior
    public boolean isHonorStudent() {
        return this.gpa >= 3.5;
    }

    // toString
    public String toString() {
        return this.name + " (ID: " + this.id + ", GPA: " + this.gpa + ")";
    }
}

And a client class that uses it:

public class StudentClient {
    public static void main(final String[] args) {
        // Create students
        Student alice   = new Student("Alice",   3.8, 1001);
        Student bob     = new Student("Bob",     3.2, 1002);
        Student charlie = new Student("Charlie", 3.5, 1003);

        // Print using toString()
        System.out.println(alice);    // Alice (ID: 1001, GPA: 3.8)
        System.out.println(bob);      // Bob (ID: 1002, GPA: 3.2)
        System.out.println(charlie);  // Charlie (ID: 1003, GPA: 3.5)

        // Use getter methods
        System.out.println(alice.getName() + "'s GPA: " + alice.getGpa());

        // Check honor status
        System.out.println("Alice is honor student? " + alice.isHonorStudent());  // true
        System.out.println("Bob is honor student? " + bob.isHonorStudent());      // false

        // Update GPA with validation
        bob.setGpa(3.6);
        System.out.println("Bob's updated GPA: " + bob.getGpa());  // 3.6
        System.out.println("Bob is honor student? " + bob.isHonorStudent());  // true

        // Store in an array
        Student[] roster = {alice, bob, charlie};
        for (int i = 0; i < roster.length; i++) {
            System.out.println(roster[i]);
        }
    }
}

Sample output:

Alice (ID: 1001, GPA: 3.8)
Bob (ID: 1002, GPA: 3.2)
Charlie (ID: 1003, GPA: 3.5)
Alice's GPA: 3.8
Alice is honor student? true
Bob is honor student? false
Bob's updated GPA: 3.6
Bob is honor student? true
Alice (ID: 1001, GPA: 3.8)
Bob (ID: 1002, GPA: 3.6)
Charlie (ID: 1003, GPA: 3.5)

Notice how the Student class and the StudentClient class live in separate files. The Student class defines the blueprint. The StudentClient class uses it. This separation is a core OOP principle: the class that defines a type should be separate from the code that uses it.


Common Mistakes

1. Forgetting this in the Constructor

// WRONG: assigns parameter to itself
public Student(final String name, final double gpa, final int id) {
    name = name;  // field stays null!
}

// CORRECT: assigns parameter to field
public Student(final String name, final double gpa, final int id) {
    this.name = name;
}

2. Declaring Fields Inside the Constructor

// WRONG: creates local variables that vanish after the constructor
public Student(final String name, final double gpa, final int id) {
    String this.name = name;   // compilation error
}

// CORRECT: fields are declared at the class level
public class Student {
    private String name;  // field declaration here

    public Student(final String name, final double gpa, final int id) {
        this.name = name;  // assignment here
    }
}

3. Adding a Return Type to the Constructor

// WRONG: this is a regular method named "Student", NOT a constructor
public void Student(final String name, final double gpa, final int id) {
    this.name = name;
}

// CORRECT: constructors have no return type
public Student(final String name, final double gpa, final int id) {
    this.name = name;
}

If you accidentally add void, Java treats it as a regular method. The real constructor (the default no-argument one) runs instead, leaving all fields at their default values.

Check Your Understanding

What is wrong with this constructor?
public void Student(final String name) {
    this.name = name;
}


static Methods vs. Instance Methods

You have been writing static methods all quarter. Instance methods look similar but behave differently:

  static method Instance method
Keyword public static void doThing() public void doThing()
Belongs to The class itself A specific object
Can access Only parameters and local variables Fields, this, parameters, local variables
Called with ClassName.method() object.method()
Example Math.sqrt(25) alice.getGpa()

static methods do not belong to any object. They cannot use this and cannot access instance fields. Instance methods belong to a specific object and can access that object’s fields through this.

Your main method is static — it runs before any objects exist. From main, you create objects and call their instance methods:

public static void main(final String[] args) {
    Student alice = new Student("Alice", 3.8, 1001);  // create object
    System.out.println(alice.getGpa());                // call instance method
}

Quick Reference

Part Syntax Purpose
Field private Type name; Data that each object holds
Constructor public ClassName(params) { ... } Initializes fields when new is called
this this.fieldName Refers to the current object’s field
Getter public Type getField() { return this.field; } Returns a field value
Setter public void setField(Type val) { this.field = val; } Modifies a field (with validation)
toString() public String toString() { ... } Returns a readable string description
Instance method public Type methodName() { ... } Operates on the object’s data

Summary

A Java class has three parts: fields (the data), a constructor (initialization), and instance methods (behavior). Fields are declared private at the class level. The constructor has the same name as the class, no return type, and uses this to assign parameters to fields. Instance methods access the object’s fields through this and are called on specific objects with dot notation.

The this keyword refers to the current object. In a constructor, this is the object being created. In an instance method, this is the object the method was called on. Without this, a parameter with the same name as a field shadows the field, and the assignment does nothing.

Every class should have a toString() method. Without it, printing an object gives you a memory address. With it, you get a readable description. Java calls toString() automatically when you print an object or concatenate it with a string.

Next lesson: Encapsulation and Access Control — why fields should be private, how getters and setters protect your data, and the principle of information hiding.