Writing Your First Class
Fields, constructors, methods, and the this keyword
After this lesson, you will be able to:
- Declare fields (instance variables) with explicit access modifiers
- Write a constructor that initializes fields using the
thiskeyword - 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
thisrefers 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 usesself, Java usesthis. 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
newis 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?
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:
- Same name as the class —
Student, notcreateStudentorinit - No return type — not
void, notStudent, 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:
- Allocates memory on the heap for a new
Studentobject - Calls the constructor, passing
"Alice",3.8,1001 - The constructor sets
this.name = "Alice",this.gpa = 3.8,this.id = 1001 - 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.fieldNameto refer to the field. This is the standard Java convention. Some programmers use prefixes like_nameortheNamefor parameters to avoid the ambiguity, butthisis the cleanest and most common approach.
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)— printsalice.toString()"Student: " + alice— callsalice.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.
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.