Java Classes & Objects

INTERMEDIATE ~8 min read Tutorial

A class is a blueprint for objects. It defines what fields an object will hold, what methods it will respond to, and what its invariants are. An object is a concrete instance of a class — an actual thing in memory that you can create with new and call methods on.

This tutorial is the foundation of object-oriented Java. We cover fields, constructors, methods, the this keyword, access modifiers, and the modern record form for simple data-carrying classes. Everything else in OOP — inheritance, polymorphism, encapsulation — builds on the syntax introduced here.

1. Anatomy of a Class

A class declaration has a name, an optional list of fields, optional constructors, and optional methods:

java
public class Person {
    class=class="tok-str">"tok-cmt">// class="tok-num">1. fields - state of the object
    private String name;
    private int age;

    class=class="tok-str">"tok-cmt">// class="tok-num">2. constructor - sets up the initial state
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    class=class="tok-str">"tok-cmt">// class="tok-num">3. methods - behaviour
    public String describe() {
        return name + " is " + age + " years old";
    }
}

2. Creating Objects with new

The new keyword allocates memory for a new object, runs the constructor, and returns a reference to it:

java
class=class="tok-str">"tok-cmt">// create a new Person object
Person alice = new Person("Alice", class="tok-num">30);
Person bob   = new Person("Bob",   class="tok-num">25);

System.out.println(alice.describe());   class=class="tok-str">"tok-cmt">// Alice is class="tok-num">30 years old
System.out.println(bob.describe());     class=class="tok-str">"tok-cmt">// Bob is class="tok-num">25 years old

class=class="tok-str">"tok-cmt">// alice and bob are independent objects with their own state
alice.age = class="tok-num">31;   class=class="tok-str">"tok-cmt">// if age is public
class=class="tok-str">"tok-cmt">// bob.age is still class="tok-num">25

The constructor is a special method whose name matches the class name and has no return type. Its job is to set the new object's fields to a valid initial state.

3. The this Keyword

this refers to the current object. Use it to disambiguate parameter names that shadow fields, to call another constructor, or to pass yourself to another method:

java
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;   class=class="tok-str">"tok-cmt">// this.name is the field, name is the param
        this.age  = age;
    }

    class=class="tok-str">"tok-cmt">// constructor chaining - one constructor calls another
    public Person(String name) {
        this(name, class="tok-num">0);   class=class="tok-str">"tok-cmt">// calls the (String, int) constructor
    }

    public Person withName(String name) {
        this.name = name;   class=class="tok-str">"tok-cmt">// mutate this object
        return this;         class=class="tok-str">"tok-cmt">// return self for method chaining
    }
}

Always name constructor parameters the same as the fields they set, and use this.x = x. The alternative of this.x = otherName reads worse and confuses readers.

4. Access Modifiers

ModifierSame classSame packageSubclassWorld
publicYYYY
protectedYYYN
(default / package-private)YYNN
privateYNNN

The general rule: fields are private, constructors and the public API are public, and helper methods are private. Use protected rarely — it is a wide-open door for subclasses to mess with internals.

5. Getters and Setters (Encapsulation)

Encapsulation means hiding internal state behind methods. Fields stay private; the outside world reads and writes through methods you control:

java
public class Temperature {
    private double celsius;

    public Temperature(double celsius) {
        if (celsius < -class="tok-num">273.15)
            throw new IllegalArgumentException("below absolute zero");
        this.celsius = celsius;
    }

    class=class="tok-str">"tok-cmt">// getters
    public double getCelsius()    { return celsius; }
    public double getFahrenheit() { return celsius * class="tok-num">9.0 / class="tok-num">5.0 + class="tok-num">32; }

    class=class="tok-str">"tok-cmt">// setter with validation
    public void setCelsius(double celsius) {
        if (celsius < -class="tok-num">273.15)
            throw new IllegalArgumentException("below absolute zero");
        this.celsius = celsius;
    }
}

The benefit is that you can later change the internal representation (a double temperature to int milliKelvin, say) without breaking any caller. Getters and setters are not boilerplate for their own sake; they are the seam between the public contract and the private implementation.

6. Static Members

A static field is shared by all instances of the class. A static method belongs to the class and does not have access to this:

java
public class Counter {
    class=class="tok-str">"tok-cmt">// static field - shared by all instances
    private static int nextId = class="tok-num">1000;

    class=class="tok-str">"tok-cmt">// instance fields - per object
    private final int id;
    private int count;

    public Counter() {
        this.id = nextId++;   class=class="tok-str">"tok-cmt">// assign then increment the shared counter
    }

    public int id()    { return id; }
    public int count() { return count; }

    public void inc()  { count++; }

    class=class="tok-str">"tok-cmt">// static method - call as Counter.totalIdsAssigned()
    public static int peekNextId() { return nextId; }
}

Counter a = new Counter();
Counter b = new Counter();
System.out.println(a.id());   class=class="tok-str">"tok-cmt">// class="tok-num">1000
System.out.println(b.id());   class=class="tok-str">"tok-cmt">// class="tok-num">1001
System.out.println(Counter.peekNextId());   class=class="tok-str">"tok-cmt">// class="tok-num">1002

7. Records (Java 16+)

When a class is just a bag of immutable fields with identity-based equality, the modern alternative is a record. The compiler generates the constructor, getters, equals, hashCode, and toString for you:

java
class=class="tok-str">"tok-cmt">// classic style
public final class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) { this.x = x; this.y = y; }
    public double x() { return x; }
    public double y() { return y; }
    class=class="tok-str">"tok-cmt">// + equals, hashCode, toString - lots of boilerplate
}

class=class="tok-str">"tok-cmt">// the same thing as a record (one line)
public record Point(double x, double y) {}

class=class="tok-str">"tok-cmt">// compact constructor for validation
public record Age(int years) {
    public Age {
        if (years < class="tok-num">0 || years > class="tok-num">150)
            throw new IllegalArgumentException("invalid age");
    }
}

class=class="tok-str">"tok-cmt">// usage
Point p = new Point(class="tok-num">3.0, class="tok-num">4.0);
System.out.println(p.x());          class=class="tok-str">"tok-cmt">// class="tok-num">3.0
System.out.println(p);              class=class="tok-str">"tok-cmt">// Point[x=class="tok-num">3.0, y=class="tok-num">4.0]
Point q = new Point(class="tok-num">3.0, class="tok-num">4.0);
System.out.println(p.equals(q));    class=class="tok-str">"tok-cmt">// true

Records are perfect for DTOs, value objects, return types that bundle multiple values, and any case where you would otherwise write boilerplate. The fields are accessed by name (p.name()), not by a getter (p.getName()) — the convention is different by design.

8. toString, equals, hashCode

Every class inherits these three methods from Object. Override them when you want custom behaviour:

java
public class Person {
    private final String name;
    private final int age;

    public Person(String name, int age) { this.name = name; this.age = age; }

    @Override
    public String toString() {
        return "Person[name=" + name + ", age=" + age + "]";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person p)) return false;
        return age == p.age && name.equals(p.name);
    }

    @Override
    public int hashCode() {
        return java.util.Objects.hash(name, age);
    }
}

The contract: if two objects are .equals(), they must have the same hashCode. The reverse is not required, but hash-based collections perform poorly when many unequal objects share a hashcode. IDEs generate all three correctly with one keystroke.

Exercises

  1. Write a Rectangle class with width and height fields, a constructor, and area() / perimeter() methods.
  2. Add a square() static factory that returns a Rectangle with equal sides.
  3. Convert your Rectangle to a record with a compact constructor that rejects negative sides.
  4. Override toString, equals, and hashCode by hand on a non-record class, then use the IDE generator.