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:
- Fields (instance variables) — the data each object holds
- Constructor — a special method that initializes fields when
new is called
- 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.
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.
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.
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.