Java Classes & Objects
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:
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:
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:
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
| Modifier | Same class | Same package | Subclass | World |
|---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | N |
| (default / package-private) | Y | Y | N | N |
private | Y | N | N | N |
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:
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:
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:
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:
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
- Write a
Rectangleclass with width and height fields, a constructor, andarea()/perimeter()methods. - Add a
square()static factory that returns a Rectangle with equal sides. - Convert your Rectangle to a
recordwith a compact constructor that rejects negative sides. - Override
toString,equals, andhashCodeby hand on a non-record class, then use the IDE generator.