Skip to main content
Java · lesson 12 of 66
Open the Java page →
Java beginner Lesson 12 of 66

Introduction to Object-Oriented Programming in Java

Start Java OOP with runnable examples: define a class, create objects, write constructors, use this, and see how static and instance members differ.

By SkillByExample · Updated

Object-oriented programming organises a program around objects: values that bundle data (fields) with the code that works on it (methods). A class is the blueprint; new builds objects from it. Every example is a single file run with the JDK 21 source launcher (java File.java).

A class and two objects

public class DogDemo {
    static class Dog {
        String name;                       // field: each Dog has its own
        int age;

        void bark() {                      // method: behaviour
            System.out.println(name + " (" + age + ") says Woof!");
        }
    }

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

        Dog luna = new Dog();
        luna.name = "Luna";

        rex.bark();
        luna.bark();
    }
}
$ java DogDemo.java
Rex (3) says Woof!
Luna (0) says Woof!

rex and luna run the same bark method but print different text, because each object carries its own name and age. luna.age was never set, so it has the default value for an int: 0.

Constructors set up a valid object

Setting fields one by one lets an object exist half-built. A constructor runs as part of new and can insist on the values it needs.

public class Constructors {
    static class Account {
        private final String owner;
        private double balance;

        Account(String owner, double opening) {
            if (opening < 0) throw new IllegalArgumentException("negative opening balance");
            this.owner = owner;
            this.balance = opening;
        }

        Account(String owner) {
            this(owner, 0);                 // delegate to the main constructor
        }

        void deposit(double amount) { balance += amount; }

        String describe() { return owner + ": " + balance; }
    }

    public static void main(String[] args) {
        Account a = new Account("Ada", 100);
        Account b = new Account("Bo");
        a.deposit(50);
        System.out.println(a.describe());
        System.out.println(b.describe());
        new Account("Cy", -5);
    }
}
$ java Constructors.java
Ada: 150.0
Bo: 0.0
Exception in thread "main" java.lang.IllegalArgumentException: negative opening balance
	at Constructors$Account.<init>(Constructors.java:7)
	at Constructors.main(Constructors.java:27)

A constructor has the class’s name and no return type. Overloaded constructors usually delegate with this(...), so validation lives in one place — which is why new Account("Cy", -5) is rejected before an invalid account can exist.

this: the current object

Inside a constructor or method, this refers to the object it is running on. Forget it and a parameter silently shadows the field.

public class ThisDemo {
    static class Point {
        int x, y;

        Point(int x, int y) {
            x = x;                          // assigns the parameter to itself
            this.y = y;                     // assigns the field
        }

        Point moveRight() {
            this.x++;
            return this;                    // return the same object for chaining
        }
    }

    public static void main(String[] args) {
        Point p = new Point(5, 7);
        System.out.println(p.x + "," + p.y);
        p.moveRight().moveRight();
        System.out.println(p.x + "," + p.y);
    }
}
$ java ThisDemo.java
0,7
2,7

x = x compiles but does nothing useful: both names refer to the parameter, so the field x stays 0. Returning this lets calls chain on the same object.

Static vs instance members

An instance field belongs to each object. A static field belongs to the class and is shared by all its objects.

public class Counting {
    static class Ticket {
        static int issued = 0;              // one copy, shared
        final int number;                   // one per object

        Ticket() {
            issued++;
            number = issued;
        }

        static int issuedSoFar() { return issued; }
    }

    public static void main(String[] args) {
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Ticket t3 = new Ticket();
        System.out.println("numbers: " + t1.number + " " + t2.number + " " + t3.number);
        System.out.println("issued: " + Ticket.issuedSoFar());
    }
}
$ java Counting.java
numbers: 1 2 3
issued: 3

Each Ticket remembers its own number, but issued is a single counter on the class. Static methods like issuedSoFar are called on the class name and cannot use this, because there is no object involved.

Variables hold references, not objects

Assigning an object variable copies the reference. Two variables can then name the same object.

public class References {
    static class Box {
        int value;
        Box(int value) { this.value = value; }
    }

    public static void main(String[] args) {
        Box a = new Box(1);
        Box b = a;                          // same object, second name
        Box c = new Box(1);                 // different object, same contents

        b.value = 99;
        System.out.println("a.value = " + a.value);
        System.out.println("a == b ? " + (a == b));
        System.out.println("a == c ? " + (a == c));

        Box empty = null;
        System.out.println(empty.value);
    }
}
$ java References.java
a.value = 99
a == b ? true
a == c ? false
Exception in thread "main" java.lang.NullPointerException: Cannot read field "value" because "<local4>" is null
	at References.main(References.java:18)

Changing the object through b is visible through a. == compares references, so two separate boxes are never == even with equal contents — that is what equals is for. A null reference points at no object, and using it throws NullPointerException. The message names the failing expression; it says <local4> here because the source launcher compiles without local-variable names — compile with javac -g and it says "empty".

toString: how an object describes itself

Printing an object calls its toString(). The inherited version from Object is rarely useful.

public class Printing {
    static class Plain {
        String name = "plain";
    }

    static class Book {
        String title;
        int pages;
        Book(String title, int pages) { this.title = title; this.pages = pages; }

        @Override
        public String toString() { return "Book[" + title + ", " + pages + " pages]"; }
    }

    public static void main(String[] args) {
        String plain = new Plain().toString();
        System.out.println(plain.startsWith("Printing$Plain@"));
        System.out.println(new Book("Dune", 412));
    }
}
$ java Printing.java
true
Book[Dune, 412 pages]

The default prints the class name, @ and a hash code that differs between runs — hence the program only checks its prefix. Overriding toString is the first taste of polymorphism: the println code is the same, but each class decides how it is shown. The four pillars — encapsulation, inheritance, polymorphism and abstraction — each have their own lesson; see the Java OOP guide.

Practice

  1. Give Dog a constructor taking name and age, and a birthday() method that adds one to age. Create rex aged 3, call birthday() twice, and print him.
Solution
public class Birthday {
    static class Dog {
        final String name;
        int age;

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

        void birthday() { age++; }

        @Override
        public String toString() { return name + " is " + age; }
    }

    public static void main(String[] args) {
        Dog rex = new Dog("Rex", 3);
        rex.birthday();
        rex.birthday();
        System.out.println(rex);
    }
}
$ java Birthday.java
Rex is 5

name is final because it never changes after construction; age is not, because birthday changes it.

  1. Fix the Point constructor in ThisDemo so new Point(5, 7) really stores x = 5.
Solution
public class ThisFixed {
    static class Point {
        int x, y;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public static void main(String[] args) {
        Point p = new Point(5, 7);
        System.out.println(p.x + "," + p.y);
    }
}
$ java ThisFixed.java
5,7

this.x names the field; the bare x names the parameter that shadows it.

  1. Add a static count to Dog that tracks how many dogs were created, and print it after creating three.
Solution
public class DogCount {
    static class Dog {
        static int count = 0;
        final String name;

        Dog(String name) {
            this.name = name;
            count++;
        }
    }

    public static void main(String[] args) {
        new Dog("Rex");
        new Dog("Luna");
        new Dog("Max");
        System.out.println("dogs created: " + Dog.count);
    }
}
$ java DogCount.java
dogs created: 3

One shared counter on the class sees every constructor call, whichever object it builds.

  1. Write a Rectangle class with width, height, an area() method and a toString(), and print a list of three rectangles sorted by area.
Solution
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Rectangles {
    static class Rectangle {
        final int width, height;
        Rectangle(int width, int height) { this.width = width; this.height = height; }
        int area() { return width * height; }

        @Override
        public String toString() { return width + "x" + height + "=" + area(); }
    }

    public static void main(String[] args) {
        List<Rectangle> rs = new ArrayList<>(List.of(
            new Rectangle(4, 5), new Rectangle(2, 3), new Rectangle(10, 1)));
        rs.sort(Comparator.comparingInt(Rectangle::area));
        System.out.println(rs);
    }
}
$ java Rectangles.java
[2x3=6, 10x1=10, 4x5=20]

Rectangle::area is a method reference: the comparator asks each object for its own area.

Frequently Asked Questions

What is the difference between a class and an object?
A class is a blueprint that declares fields and methods. An object is one concrete instance built from it with new, holding its own copy of every instance field. One Dog class can produce any number of Dog objects, each with its own name.
Why use object-oriented programming instead of procedural code?
OOP keeps data and the code that changes it together in one class, and lets that class hide its fields behind methods. Large programs become easier to reason about because each object guards its own state instead of any function being able to change any value.
Is everything in Java an object?
Almost. Java's eight primitive types, such as int, double and boolean, are plain values, not objects. Everything else, including String, arrays and every class you write, is an object reached through a reference.
What does the this keyword do in Java?
this is a reference to the object the current method or constructor is running on. It is needed when a parameter has the same name as a field, as in this.name = name, and it can also call another constructor of the same class with this(...).
What happens if I forget to initialise an object reference?
A field of class type defaults to null. Calling a method on it throws NullPointerException, and since Java 14 the message names the exact expression that was null. Local variables are different: the compiler refuses to let you use one before it is assigned.