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?</p>
AInside the constructor
BInside any instance method
CInside the class body but outside any method or constructor
DBefore the class declaration
Fields must be declared inside the class but outside all methods and constructors. Variables declared inside a method are local variables that only exist while the method runs. Fields persist for the lifetime of the object.
--- ## 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 class** --- `Student`, not `createStudent` or `init` 2. **No return type** --- not `void`, not `Student`, nothing at all ```java public Student(final String name, final double gpa, final int id) { this.name = name; this.gpa = gpa; this.id = id; } ``` When you write: ```java 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: ```java 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: ```java // 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?</p>
AIt works the same way
BThe field stays at its default value (null for String, 0 for int)
CIt throws a NullPointerException
DIt causes a compilation error
Without this, both name references on that line refer to the parameter. The assignment name = name; assigns the parameter to itself, which does nothing. The field this.name is never set and stays at its default value (null for String). This compiles without error, which makes the bug hard to catch.
--- ## 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: ```java 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: ```java 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: ```java public String toString() { return this.name + " (ID: " + this.id + ", GPA: " + this.gpa + ")"; } ``` Without `toString()`: ```java Student alice = new Student("Alice", 3.8, 1001); System.out.println(alice); // Student@6d06d69c (memory address — useless) ``` With `toString()`: ```java 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: ```java 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: ```java 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 ```java // 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 ```java // 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 ```java // 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;
}</p>
AThe final keyword is wrong
BIt has a return type (void), so Java treats it as a regular method, not a constructor
CIt should use name instead of this.name
DNothing is wrong
Constructors must not have a return type. Adding void turns it into a regular method named Student. Java will not call it when you write new Student("Alice"). Instead, it will look for a real constructor (one without a return type) and use the default no-argument constructor if none exists, leaving all fields at their default values.
--- ## `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: ```java 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.