Skip to main content

Classes & Objects

Java is an object-oriented language. Almost everything revolves around classes and objects. A class is a blueprint; an object is an instance of that blueprint.

Your first class

public class Dog {
// Fields (instance variables)
String name;
int age;

// Method
String bark() {
return name + " says: Woof!";
}
}

Creating and using objects:

public class Main {
public static void main(String[] args) {
Dog dog1 = new Dog();
dog1.name = "Rex";
dog1.age = 3;

Dog dog2 = new Dog();
dog2.name = "Bella";
dog2.age = 5;

System.out.println(dog1.bark());
System.out.println(dog2.bark());
System.out.println(dog1.age);
}
}

Result:

Rex says: Woof!
Bella says: Woof!
3
  • new Dog() creates an object (instance) of the Dog class
  • Each object has its own copy of the fields (name, age)
  • Methods operate on the object's data

Constructors

A constructor initializes an object when it is created. It has the same name as the class and no return type:

public class User {
String name;
int age;

// Constructor
User(String name, int age) {
this.name = name;
this.age = age;
}
}
User user = new User("Ada", 36);
System.out.println(user.name + " is " + user.age);

Result:

Ada is 36

this

Inside a method or constructor, this refers to the current object. It is commonly used when parameter names match field names:

User(String name, int age) {
this.name = name; // this.name = field, name = parameter
this.age = age;
}

Default constructor

If you define no constructors, Java provides a default no-argument constructor. If you define any constructor, the default is no longer generated:

public class Product {
String name;
double price;

Product(String name, double price) {
this.name = name;
this.price = price;
}
}

// Product p = new Product(); // compile error -- no no-arg constructor
Product p = new Product("Laptop", 999.99); // works

Multiple constructors

public class User {
String name;
int age;
String role;

User(String name, int age, String role) {
this.name = name;
this.age = age;
this.role = role;
}

User(String name, int age) {
this(name, age, "user"); // calls the other constructor
}

User(String name) {
this(name, 0, "user");
}
}
User u1 = new User("Ada", 36, "admin");
User u2 = new User("Bob", 25);
User u3 = new User("Charlie");

System.out.println(u1.name + " -- " + u1.role);
System.out.println(u2.name + " -- " + u2.role);
System.out.println(u3.name + " -- " + u3.age);

Result:

Ada -- admin
Bob -- user
Charlie -- 0

this(...) calls another constructor in the same class. It must be the first statement.

Access modifiers

Control who can see and use your fields, methods, and classes:

ModifierClassPackageSubclassWorld
publicYesYesYesYes
protectedYesYesYesNo
(none -- package-private)YesYesNoNo
privateYesNoNoNo
public class BankAccount {
private double balance; // only this class
String accountHolder; // package-private
protected String accountType; // this class + subclasses + same package
public String bankName; // everyone
}

Encapsulation

Encapsulation means hiding internal data and exposing it through methods. The pattern: private fields + public getters/setters.

public class BankAccount {
private String owner;
private double balance;

public BankAccount(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
}

// Getter
public double getBalance() {
return balance;
}

public String getOwner() {
return owner;
}

// Business logic instead of a raw setter
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit must be positive");
}
balance += amount;
}

public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal must be positive");
}
if (amount > balance) {
throw new IllegalArgumentException("Insufficient funds");
}
balance -= amount;
}
}
BankAccount account = new BankAccount("Ada", 1000);
System.out.println(account.getOwner());
System.out.println(account.getBalance());

account.deposit(500);
System.out.println(account.getBalance());

account.withdraw(200);
System.out.println(account.getBalance());

// account.balance = 999999; // compile error -- balance is private

Result:

Ada
1000.0
1500.0
1300.0

Why encapsulation matters:

  • You control how data is modified (validation in deposit/withdraw)
  • You can change internal representation without breaking callers
  • The class enforces its own invariants (balance cannot go negative)

toString

By default, printing an object shows its class name and hash code. Override toString for a useful representation:

public class User {
private String name;
private int age;

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

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
User user = new User("Ada", 36);
System.out.println(user);

Result:

User{name='Ada', age=36}

The @Override annotation tells the compiler you intend to override a method from a parent class. If you misspell the method name, the compiler will flag it.

equals and hashCode

By default, == and .equals() compare object references (whether they are the same object in memory). Override both to compare by content:

import java.util.Objects;

public class User {
private String name;
private int age;

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

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}

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

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
User a = new User("Ada", 36);
User b = new User("Ada", 36);
User c = new User("Bob", 25);

System.out.println(a == b); // false -- different objects
System.out.println(a.equals(b)); // true -- same content
System.out.println(a.equals(c)); // false -- different content

Result:

false
true
false

Rule: if you override equals, you must also override hashCode. Collections like HashMap and HashSet depend on this contract.

static fields and methods

static members belong to the class, not to any instance:

public class Counter {
private static int count = 0; // shared across all instances

private String label;

public Counter(String label) {
this.label = label;
count++;
}

public static int getCount() {
return count;
}

public String getLabel() {
return label;
}
}
Counter a = new Counter("A");
Counter b = new Counter("B");
Counter c = new Counter("C");

System.out.println("Total counters: " + Counter.getCount());
System.out.println(a.getLabel());

Result:

Total counters: 3
A

Common uses of static:

  • Constants: static final double PI = 3.14159;
  • Utility methods: Math.abs(), Integer.parseInt()
  • Factory methods: creating instances with descriptive names

Static vs instance

StaticInstance
Belongs toThe classAn object
Accessed viaClassName.method()object.method()
Can access instance fields?NoYes
Can access static fields?YesYes

Records (Java 16+)

Records are a concise way to create immutable data classes. They auto-generate the constructor, getters, toString, equals, and hashCode:

public record Point(int x, int y) {}

This single line generates the equivalent of:

  • A constructor Point(int x, int y)
  • Accessor methods x() and y() (not getX())
  • toString(): Point[x=3, y=4]
  • equals() and hashCode() based on all fields
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
Point p3 = new Point(0, 0);

System.out.println(p1);
System.out.println(p1.x());
System.out.println(p1.y());
System.out.println(p1.equals(p2));
System.out.println(p1.equals(p3));

Result:

Point[x=3, y=4]
3
4
true
false

Records with validation

public record User(String name, int age) {
// Compact constructor for validation
public User {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be blank");
}
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
}
User valid = new User("Ada", 36);
System.out.println(valid);

try {
User invalid = new User("", 25);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}

Result:

User[name=Ada, age=36]
Error: Name cannot be blank

Records with methods

Records can have methods, static fields, and implement interfaces:

public record Rectangle(double width, double height) {

public double area() {
return width * height;
}

public double perimeter() {
return 2 * (width + height);
}
}
Rectangle r = new Rectangle(5, 3);
System.out.println("Area: " + r.area());
System.out.println("Perimeter: " + r.perimeter());

Result:

Area: 15.0
Perimeter: 16.0

When to use records: whenever you need a simple data carrier -- DTOs, value objects, configuration, return multiple values from a method.

Builder pattern (brief introduction)

For classes with many optional fields, a builder is cleaner than many constructors:

public class HttpRequest {
private final String url;
private final String method;
private final String body;
private final int timeout;

private HttpRequest(Builder builder) {
this.url = builder.url;
this.method = builder.method;
this.body = builder.body;
this.timeout = builder.timeout;
}

@Override
public String toString() {
return method + " " + url + " (timeout=" + timeout + "ms)";
}

static class Builder {
private final String url;
private String method = "GET";
private String body = "";
private int timeout = 5000;

Builder(String url) {
this.url = url;
}

Builder method(String method) {
this.method = method;
return this;
}

Builder body(String body) {
this.body = body;
return this;
}

Builder timeout(int timeout) {
this.timeout = timeout;
return this;
}

HttpRequest build() {
return new HttpRequest(this);
}
}
}
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
.method("POST")
.body("{\"name\": \"Ada\"}")
.timeout(3000)
.build();

System.out.println(request);

Result:

POST https://api.example.com/users (timeout=3000ms)

Organizing files

Java convention:

  • One public class per file
  • Filename matches the class name: User.javapublic class User
  • Package structure matches directory structure
  • Classes in lowercase package names: com.example.app
src/
├── Main.java
├── model/
│ └── User.java
└── util/
└── StringUtils.java

Summary

  • A class is a blueprint; an object is an instance created with new.
  • Constructors initialize objects. this(...) chains to another constructor.
  • Access modifiers control visibility: private → package-private → protectedpublic.
  • Encapsulation: private fields + public methods. Control access, enforce invariants.
  • Override toString for readable output, equals/hashCode for content comparison.
  • static members belong to the class, not instances -- used for shared state and utility methods.
  • Records (Java 16+) auto-generate constructors, accessors, toString, equals, hashCode.
  • The builder pattern handles objects with many optional parameters.

Next up: Inheritance & Interfaces -- extending classes, implementing contracts, and polymorphism.