Index

Classes and Objects

Java Object-Oriented Design

2.1 Defining Classes and Creating Objects

In Java, everything begins with classes and objects. These two concepts are at the heart of object-oriented programming (OOP). In this section, you’ll learn how to define your own classes and create objects from them—laying the foundation for building modular, reusable, and maintainable software.

What Is a Class?

A class is a blueprint for creating objects. It defines the structure (fields or attributes) and behavior (methods or functions) that its instances will have. Think of a class as a template—like architectural drawings for a house.

Syntax of a Class Declaration

public class Car {
    // Fields (attributes)
    String model;
    int year;

    // Method (behavior)
    void startEngine() {
        System.out.println("Engine started.");
    }
}

In this example:

This class doesn’t actually do anything until we create objects from it.

What Is an Object?

An object is a specific instance of a class. When you create an object, you’re telling Java: “Make me one of these, with memory allocated for its fields.”

Each object has its own copies of the class’s fields, but it shares the behavior (methods) defined in the class.

Creating Objects in Java

To create an object in Java, you use the new keyword followed by a call to the class's constructor (a special method we'll cover in the next section).

Car myCar = new Car();      // Object 1
Car yourCar = new Car();    // Object 2

You can then assign values to the fields and call methods:

myCar.model = "Toyota Corolla";
myCar.year = 2020;

yourCar.model = "Honda Civic";
yourCar.year = 2022;

myCar.startEngine();  // Output: Engine started.
yourCar.startEngine();  // Output: Engine started.

Each object maintains its own state (model and year), even though they share the same structure and behavior.

Behind the Scenes: Memory and Referencing

When you write:

Car myCar = new Car();

here’s what happens under the hood:

  1. new Car() allocates memory in the heap for the new object.
  2. The object is initialized (default values are assigned).
  3. A reference to that memory location is returned and stored in myCar.

It’s important to understand that myCar is not the object itself—it’s a reference (like a pointer) to the object in memory. You can have multiple references to the same object:

Car anotherCar = myCar;
anotherCar.model = "Tesla Model 3";

System.out.println(myCar.model);  // Output: Tesla Model 3

Since both myCar and anotherCar point to the same object in memory, changes made through one reference affect the other.

A Complete Example

Let’s tie it all together with a complete, simple program:

public class Car {
    String model;
    int year;

    void displayInfo() {
        System.out.println("Model: " + model + ", Year: " + year);
    }
}

public class Main {
    public static void main(String[] args) {
        Car car1 = new Car();
        car1.model = "Ford Mustang";
        car1.year = 2023;

        Car car2 = new Car();
        car2.model = "Chevrolet Camaro";
        car2.year = 2021;

        car1.displayInfo();  // Output: Model: Ford Mustang, Year: 2023
        car2.displayInfo();  // Output: Model: Chevrolet Camaro, Year: 2021
    }
}

Summary

Understanding the difference between class as blueprint and object as instance is critical for writing effective Java programs. In the next section, you’ll dive deeper into fields and methods—the components that give objects their unique identity and purpose.

Index

2.2 Fields and Methods

In object-oriented programming, fields and methods are the core building blocks of a class. Fields define the data or state that an object holds, while methods define the behaviors or actions that the object can perform. Together, they form the structure and functionality of Java classes.

Understanding how to declare and use fields and methods is essential for writing well-structured and reusable code. This section covers the fundamentals of fields and methods in Java, including access control, method parameters, and variable scope.

Fields: Representing Object State

Fields, also known as instance variables, are declared inside a class but outside any method. Each object created from a class gets its own copy of the class’s fields. Fields hold the data that represents the state of the object.

Declaring Fields

public class Person {
    // Fields (instance variables)
    public String name;
    private int age;
}

In this example:

It’s a best practice to make fields private and access them via public methods (getters and setters) to preserve encapsulation.

Methods: Representing Object Behavior

Methods define what an object can do. A method is a block of code that performs a specific task, and it can operate on the fields of the class.

Declaring Methods

public class Person {
    private String name;

    // Method with no return value
    public void greet() {
        System.out.println("Hello, my name is " + name);
    }

    // Method with parameters and a return type
    public void setName(String newName) {
        name = newName;
    }

    public String getName() {
        return name;
    }
}

Here:

Access Modifiers

Java uses access modifiers to control the visibility of class members (fields and methods):

Encapsulation encourages using private fields and providing controlled access via public methods.

Instance Variables vs Local Variables

It’s important to distinguish between:

Example:

public class Calculator {
    private int lastResult; // Instance variable

    public int add(int a, int b) {
        int sum = a + b;     // Local variable
        lastResult = sum;    // Store in instance variable
        return sum;
    }
}

Here, sum is a local variable inside the add() method. It exists temporarily during method execution. lastResult is an instance variable, maintaining a persistent state across method calls.

Method Invocation and Parameter Passing

Once an object is created, its methods can be invoked using dot notation:

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("Alice");
        person.greet(); // Output: Hello, my name is Alice
    }
}

Here, setName("Alice") passes a parameter ("Alice") to the method, which is then used to update the internal state (name) of the object.

Java uses pass-by-value for parameter passing:

Example:

public class Demo {
    public void updateValue(int x) {
        x = 10; // Does not affect original variable
    }

    public void updatePersonName(Person p) {
        p.setName("Updated"); // Affects original object
    }
}

Calling updateValue() won’t change the original int, but updatePersonName() will update the actual Person object's name, since the object reference is passed.

Click to view full runnable Code

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

    // Method with no return value
    public void greet() {
        System.out.println("Hello, my name is " + name);
    }

    // Setter for name
    public void setName(String newName) {
        name = newName;
    }

    // Getter for name
    public String getName() {
        return name;
    }

    // Setter for age
    public void setAge(int newAge) {
        age = newAge;
    }

    // Getter for age
    public int getAge() {
        return age;
    }
}

public class Calculator {
    private int lastResult; // Instance variable

    public int add(int a, int b) {
        int sum = a + b;     // Local variable
        lastResult = sum;    // Store in instance variable
        return sum;
    }

    public int getLastResult() {
        return lastResult;
    }
}

public class Demo {
    public void updateValue(int x) {
        x = 10; // Does not affect original variable
    }

    public void updatePersonName(Person p) {
        p.setName("Updated"); // Affects original object
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("Alice");
        person.setAge(30);
        person.greet(); // Output: Hello, my name is Alice

        Calculator calc = new Calculator();
        int result = calc.add(5, 7);
        System.out.println("Sum: " + result);           // Sum: 12
        System.out.println("Last Result: " + calc.getLastResult()); // Last Result: 12

        Demo demo = new Demo();
        int original = 5;
        demo.updateValue(original);
        System.out.println("Original after updateValue: " + original); // 5

        demo.updatePersonName(person);
        System.out.println("Person's name after updatePersonName: " + person.getName()); // Updated
    }
}

Summary

Understanding and using fields and methods correctly is crucial for designing meaningful and maintainable Java classes. In the next section, you’ll learn about the this keyword and how it helps distinguish between instance fields and parameters.

Index

2.3 The this Keyword

In Java, the keyword this is a special reference variable that refers to the current object—the instance of the class in which it is used. It is especially useful when differentiating between instance variables (fields) and parameters or local variables with the same name. It also allows a method to return the current object, which can be useful for method chaining and fluent APIs.

Disambiguating Fields from Parameters

A common use case for this arises in constructors or setter methods where the parameter name is the same as the field name.

Example: Without this

public class Student {
    String name;

    public void setName(String name) {
        name = name; // This assigns the parameter to itself, not the field!
    }
}

In the above example, name = name does not do what we expect—it just reassigns the method parameter to itself. The field name remains unchanged.

Corrected: Using this

public class Student {
    String name;

    public void setName(String name) {
        this.name = name; // this.name refers to the field, name is the parameter
    }
}

Here, this.name clearly refers to the instance variable, while name refers to the method parameter. This eliminates ambiguity and ensures the field is correctly updated.

Returning the Current Object

The this keyword can also be used to return the current object from a method. This technique is often used to enable method chaining—calling multiple methods on the same object in a single statement.

Example:

public class Book {
    private String title;

    public Book setTitle(String title) {
        this.title = title;
        return this;
    }

    public void printTitle() {
        System.out.println("Title: " + title);
    }
}

public class Main {
    public static void main(String[] args) {
        new Book()
            .setTitle("Clean Code")
            .printTitle();
    }
}

In this example, setTitle() returns this, allowing printTitle() to be chained directly.

Common Mistakes Avoided with this

Using this is not always required, but it is essential whenever you need to clarify that you're referring to the current object’s field or when enabling fluent programming styles.

Summary

The this keyword is a simple yet powerful tool that helps you:

As your classes grow in complexity, understanding and using this properly will help make your code clearer and more maintainable.

Index

2.4 Constructors and Constructor Overloading

In Java, a constructor is a special kind of method used to initialize objects when they are created. Unlike regular methods, constructors have the same name as the class and do not have a return type (not even void). Every time you use the new keyword to create an object, a constructor is called to set up the object’s initial state.

Understanding how constructors work—and how to write flexible, reusable ones through overloading—is essential to building robust and readable Java classes.

What Is a Constructor?

A constructor is executed once when an object is created. It typically sets default or user-defined values for the object's fields.

Syntax:

public class Person {
    String name;
    int age;

    // Constructor
    public Person() {
        name = "Unknown";
        age = 0;
    }
}

You can now create a Person object like this:

Person p1 = new Person(); // name = "Unknown", age = 0

Default Constructor

If no constructor is explicitly defined, Java automatically provides a default constructor with no parameters, which does nothing but allow the object to be instantiated.

However, if any constructor is defined, Java does not generate a default one. You must explicitly create it if needed.

public class Animal {
    String species;

    // No explicit constructor → Java provides a default one.
}

But if you write:

public class Animal {
    String species;

    public Animal(String s) {
        species = s;
    }
}

You must now explicitly add a no-argument constructor if you want to create an object like new Animal().

Parameterized Constructors

A parameterized constructor allows you to pass initial values when creating an object.

public class Car {
    String model;
    int year;

    public Car(String m, int y) {
        model = m;
        year = y;
    }
}

Now you can create a car object like this:

Car car1 = new Car("Toyota", 2020);

Parameterized constructors make your code more expressive and reduce the need to call separate setter methods after object creation.

Constructor Overloading

Java allows you to define multiple constructors in the same class, as long as they differ in the number or types of parameters. This is called constructor overloading, and it lets you create objects in different ways depending on available data.

Example:

public class Book {
    String title;
    String author;

    // Default constructor
    public Book() {
        this("Unknown", "Unknown");
    }

    // Constructor with one parameter
    public Book(String title) {
        this(title, "Unknown");
    }

    // Constructor with two parameters
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public void printInfo() {
        System.out.println("Title: " + title + ", Author: " + author);
    }
}

Usage:

Book b1 = new Book();
Book b2 = new Book("1984");
Book b3 = new Book("Brave New World", "Aldous Huxley");

b1.printInfo(); // Output: Title: Unknown, Author: Unknown  
b2.printInfo(); // Output: Title: 1984, Author: Unknown  
b3.printInfo(); // Output: Title: Brave New World, Author: Aldous Huxley
Click to view full runnable Code

public class Book {
    String title;
    String author;

    // Default constructor
    public Book() {
        this("Unknown", "Unknown");
    }

    // Constructor with one parameter
    public Book(String title) {
        this(title, "Unknown");
    }

    // Constructor with two parameters
    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public void printInfo() {
        System.out.println("Title: " + title + ", Author: " + author);
    }
}

public class Main {
    public static void main(String[] args) {
        Book b1 = new Book();
        Book b2 = new Book("1984");
        Book b3 = new Book("Brave New World", "Aldous Huxley");

        b1.printInfo(); // Output: Title: Unknown, Author: Unknown  
        b2.printInfo(); // Output: Title: 1984, Author: Unknown  
        b3.printInfo(); // Output: Title: Brave New World, Author: Aldous Huxley  
    }
}

Constructor Chaining with this()

In the Book example above, one constructor calls another using the this() keyword. This is called constructor chaining and helps reduce code duplication.

Rules of constructor chaining:

Best Practices

Summary

Constructors are essential for initializing Java objects. Java supports both default and parameterized constructors, and lets you overload them for flexibility. Constructor chaining using this() makes your code cleaner and reduces repetition.

As you design more complex classes, mastering constructor overloading and chaining will help ensure objects are created in a consistent, predictable way. In the next section, you’ll explore how Java manages the lifecycle and scope of objects in memory.

Index

2.5 Object Lifecycle and Scope

Every object in Java goes through a lifecycle: it is created, used, and eventually removed from memory when no longer needed. Understanding this lifecycle—and the different scopes that variables can have—is essential for writing efficient, bug-free code.

Object Creation and Garbage Collection

Objects in Java are created using the new keyword. This allocates memory on the heap, where dynamic memory resides:

Car myCar = new Car();

The reference myCar points to the memory location of the Car object. Java manages memory using an automatic garbage collector (GC), which periodically reclaims memory used by objects that are no longer reachable.

For example:

Car tempCar = new Car();
tempCar = null; // The object is now eligible for garbage collection

You do not need to (and cannot) manually delete objects in Java. However, understanding when objects become unreachable is important to avoid memory leaks in long-running programs.

Variable Scope: Local, Instance, and Class-Level

Local Variables

Declared inside methods, constructors, or blocks. They exist only during the method's execution.

public void drive() {
    int speed = 60; // local variable
    System.out.println("Speed: " + speed);
}

Instance Variables

Defined in a class but outside any method. They belong to each object.

public class Car {
    String model; // instance variable
}

Class-Level Variables (Static)

Declared with the static keyword. They belong to the class itself, not any instance.

public class Car {
    static int numberOfCars; // class-level variable
}

Scope in Practice

public class Example {
    int instanceVar = 10;
    static int staticVar = 20;

    public void methodScope() {
        int localVar = 30;
        System.out.println(instanceVar); // Allowed
        System.out.println(staticVar);   // Allowed
        System.out.println(localVar);    // Allowed
    }

    public void anotherMethod() {
        // System.out.println(localVar); // Error: not visible here
    }
}

This illustrates that local variables are not accessible outside their defining method, while instance and static variables are accessible throughout the class.

Finalization and Memory Management

Java used to provide a finalize() method, which was called by the garbage collector before reclaiming an object. However, it’s now deprecated and not recommended due to unpredictability and performance concerns.

Instead, use try-with-resources or implement AutoCloseable for releasing non-memory resources (like file handles or network connections).

Summary

Java simplifies memory management through automatic garbage collection, ensuring objects are cleaned up when no longer in use. Understanding variable scope—local, instance, and class-level—helps you write clear, maintainable code and avoid common pitfalls such as unintended variable reuse or memory retention.

In later chapters, you’ll learn how this knowledge supports encapsulation, modularity, and efficient resource use in object-oriented design.

Index