Classes and Objects: The Foundation of OOP

Duration: 45 min

Classes and Objects: The Foundation of OOP

Duration: 45 min

Introduction

Object-oriented programming organizes code around objects and classes. A class is a blueprint; an object is an instance of that blueprint. Classes encapsulate data (fields) and behavior (methods) together, making code modular, reusable, and maintainable. Understanding class anatomy—fields, methods, constructors, access modifiers—is fundamental to professional Java development.

This module covers class structure, object creation, the this keyword, static members, and access modifiers. You'll learn how to design classes effectively and why these patterns matter in real applications.

Class Anatomy

A Java class contains fields (state) and methods (behavior). Fields store data; methods operate on that data.

public class BankAccount {
    // Fields (instance variables) - unique to each object
    private String accountNumber;
    private double balance;
    private String accountHolder;
    
    // Constructor - called when creating new object
    public BankAccount(String accountNumber, String accountHolder) {
        this.accountNumber = accountNumber;
        this.accountHolder = accountHolder;
        this.balance = 0.0;
    }
    
    // Methods - behavior
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: $" + amount);
        }
    }
    
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrew: $" + amount);
        } else {
            System.out.println("Invalid withdrawal amount");
        }
    }
    
    public double getBalance() {
        return balance;
    }
    
    public void printStatement() {
        System.out.println("Account: " + accountNumber);
        System.out.println("Holder: " + accountHolder);
        System.out.println("Balance: $" + balance);
    }
}

public class BankDemo { public static void main(String[] args) { // Create object - constructor called BankAccount alice = new BankAccount("123456", "Alice"); // Call methods alice.deposit(1000); alice.withdraw(250); alice.printStatement(); // Each object is independent BankAccount bob = new BankAccount("789012", "Bob"); bob.deposit(5000); bob.printStatement(); } }

Each object has its own copy of instance variables. Methods operate on the object's data. Fields are typically private; methods are typically public. This separation is fundamental to encapsulation.

Constructors

Constructors initialize objects. Java provides default (no-arg) and parameterized constructors. You can overload constructors—multiple versions with different parameters.

public class Car {
    private String make;
    private String model;
    private int year;
    private double price;
    
    // Constructor 1: Full initialization
    public Car(String make, String model, int year, double price) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.price = price;
    }
    
    // Constructor 2: Minimal initialization
    public Car(String make, String model) {
        this.make = make;
        this.model = model;
        this.year = 2024;
        this.price = 0.0;
    }
    
    // Constructor 3: Default values
    public Car() {
        this.make = "Unknown";
        this.model = "Unknown";
        this.year = 2024;
        this.price = 0.0;
    }
    
    public void displayInfo() {
        System.out.println(year + " " + make + " " + model + " - $" + price);
    }
}

public class CarDemo { public static void main(String[] args) { Car c1 = new Car("Toyota", "Camry", 2023, 25000); c1.displayInfo(); Car c2 = new Car("Honda", "Civic"); c2.displayInfo(); Car c3 = new Car(); c3.displayInfo(); } }

Java automatically provides a no-arg constructor if you don't define any. If you define parameterized constructors, the no-arg disappears unless you explicitly provide it. This is a common source of bugs—always provide a no-arg constructor when you define parameterized ones.

The This Keyword

this refers to the current object. Use it to disambiguate when local variables shadow field names, or to call other constructors.

public class Person {
    private String name;
    private int age;
    private String email;
    
    // Constructor using 'this' to refer to fields
    public Person(String name, int age) {
        this.name = name;  // 'this.name' refers to field, 'name' is parameter
        this.age = age;
    }
    
    // Method using 'this' explicitly (usually optional)
    public void printInfo() {
        System.out.println("Name: " + this.name);
        System.out.println("Age: " + this.age);
    }
    
    // Setter methods
    public void setEmail(String email) {
        this.email = email;
    }
    
    public String getEmail() {
        return this.email;
    }
    
    public void celebrateBirthday() {
        this.age++;  // 'this' is optional here but can improve clarity
    }
}

public class PersonDemo { public static void main(String[] args) { Person p = new Person("Alice", 25); p.printInfo(); p.setEmail("alice@example.com"); p.celebrateBirthday(); System.out.println("Email: " + p.getEmail()); } }

Use this when parameter names match field names. It makes code intent clear—you're explicitly working with the object's state.

Static Members

Static fields and methods belong to the class, not instances. There's one static field shared by all objects; each object gets its own instance fields.

public class Counter {
    private static int totalCount = 0;  // Shared by all instances
    private int instanceId;
    
    public Counter() {
        totalCount++;  // Increment class-level counter
        instanceId = totalCount;  // Assign unique ID
    }
    
    public int getId() {
        return instanceId;
    }
    
    // Static method - belongs to class, not instance
    public static int getTotalCount() {
        return totalCount;  // Can only access static members
    }
    
    public void displayInfo() {
        System.out.println("Instance #" + instanceId + ", Total created: " + totalCount);
    }
}

public class CounterDemo { public static void main(String[] args) { System.out.println("Total: " + Counter.getTotalCount()); // 0 Counter c1 = new Counter(); c1.displayInfo(); // Instance #1, Total: 1 Counter c2 = new Counter(); c2.displayInfo(); // Instance #2, Total: 2 Counter c3 = new Counter(); c3.displayInfo(); // Instance #3, Total: 3 System.out.println("Total: " + Counter.getTotalCount()); // 3 } }

Static methods cannot access instance fields (only static ones). Call static methods with ClassName.methodName(), not on object instances. Static fields are useful for shared state: counters, configuration, caching.

Access Modifiers

Access modifiers control visibility: public (everywhere), protected (same package + subclasses), package-private (same package only), private (same class only).

public class AccessDemo {
    public String publicField = "Accessible everywhere";
    protected String protectedField = "Accessible in same package and subclasses";
    String packagePrivateField = "Accessible only in same package (default)";
    private String privateField = "Accessible only in this class";
    
    public void publicMethod() {
        System.out.println("I'm public");
    }
    
    protected void protectedMethod() {
        System.out.println("I'm protected");
    }
    
    void packagePrivateMethod() {
        System.out.println("I'm package-private");
    }
    
    private void privateMethod() {
        System.out.println("I'm private");
    }
    
    public void demonstrateAccess() {
        // All members accessible from within class
        System.out.println(privateField);
        privateMethod();
    }
}

public class Student { // Best practices private String studentId; // Private field private double gpa; public Student(String studentId, double gpa) { this.studentId = studentId; this.gpa = gpa; } // Public getter public String getStudentId() { return studentId; } // Public getter public double getGpa() { return gpa; } // Public setter with validation public void setGpa(double newGpa) { if (newGpa >= 0.0 && newGpa <= 4.0) { this.gpa = newGpa; } } }

Best practices: make fields private, provide public getters/setters for controlled access. This allows validation and prevents external code from setting invalid values.

Field Initialization

Fields can be initialized at declaration, in constructor, or in initializer blocks.

public class Initialization {
    // Field initialization at declaration
    private String name = "Unknown";
    private int count = 0;
    private java.util.Date created = new java.util.Date();
    
    // Instance initializer block (runs before constructor)
    {
        System.out.println("Initializer block running");
    }
    
    public Initialization() {
        System.out.println("Constructor running");
    }
    
    public Initialization(String name) {
        this.name = name;  // Overwrites initialized value
    }
}

Order of execution: field initializers, instance initializer blocks, constructor. Initializers provide defaults; constructors override with specific values.

Object Lifecycle and Memory Management

Object Creation and Garbage Collection

public class ObjectLifecycle {
    private String name;
    
    public ObjectLifecycle(String name) {
        this.name = name;
        System.out.println(name + " created");
    }
    
    // Called by garbage collector
    @Override
    protected void finalize() throws Throwable {
        System.out.println(name + " being garbage collected");
        super.finalize();
    }
    
    public static void main(String[] args) {
        ObjectLifecycle obj1 = new ObjectLifecycle("obj1");
        ObjectLifecycle obj2 = new ObjectLifecycle("obj2");
        
        // obj1 is no longer referenced
        obj1 = null;  // Eligible for garbage collection
        
        // Suggest garbage collection (not guaranteed to run)
        System.gc();
        
        // obj2 still exists
    }
}

Reference vs Object

public class ReferenceVsObject {
    static class Box {
        int value;
        Box(int value) { this.value = value; }
    }
    
    public static void main(String[] args) {
        Box b1 = new Box(10);
        Box b2 = b1;  // Both reference same object
        
        b2.value = 20;
        System.out.println(b1.value);  // 20 - same object!
        
        b2 = new Box(30);  // b2 now references different object
        System.out.println(b1.value);  // Still 20 - different objects
    }
}

Method Design and Best Practices

Meaningful Method Names

public class BankAccount {
    private double balance;
    
    // Good: describes what it does
    public void deposit(double amount) {
        balance += amount;
    }
    
    public void withdraw(double amount) throws Exception {
        if (amount > balance) {
            throw new Exception("Insufficient funds");
        }
        balance -= amount;
    }
    
    public double getBalance() {
        return balance;
    }
    
    // Bad: unclear names
    public void process(double x) {
        balance += x;
    }
    
    public double get() {
        return balance;
    }
}

Method Parameter Design

public class ParameterDesign {
    // Avoid too many parameters (limit to 3-4)
    public void goodMethod(String name, int age) {
        // Clear purpose
    }
    
    // Too many parameters
    public void badMethod(String name, int age, String address, 
                         String phone, String email, String job) {
        // Hard to call and maintain
    }
    
    // Solution: Use objects to group related data
    public class UserInfo {
        String name, address, phone, email, job;
        int age;
    }
    
    public void betterMethod(UserInfo user) {
        // Cleaner and more extensible
    }
}

Static Context and Class Variables

public class ConfigManager {
    private static final String VERSION = "1.0";
    private static int instanceCount = 0;
    private static java.util.List instances = new java.util.ArrayList<>();
    
    private String name;
    
    public ConfigManager(String name) {
        this.name = name;
        instanceCount++;
        instances.add(this);
    }
    
    public static String getVersion() {
        return VERSION;  // Can access static members
    }
    
    public static int getInstanceCount() {
        return instanceCount;
    }
    
    public static java.util.List getAllInstances() {
        return new java.util.ArrayList<>(instances);
    }
    
    // Static methods cannot access instance members
    public static void printStatistics() {
        System.out.println("Version: " + VERSION);
        System.out.println("Instance count: " + instanceCount);
        // System.out.println(name);  // ERROR - name is instance variable
    }
}

Real-World Class Design

Bank Account System

public class AdvancedBankAccount {
    private static final double MINIMUM_BALANCE = 100.0;
    private static int nextAccountNumber = 1000;
    
    private int accountNumber;
    private String accountHolder;
    private double balance;
    private java.util.List transactionHistory;
    
    public AdvancedBankAccount(String holder, double initialDeposit) {
        if (initialDeposit < MINIMUM_BALANCE) {
            throw new IllegalArgumentException("Initial deposit must be at least $" + MINIMUM_BALANCE);
        }
        this.accountNumber = nextAccountNumber++;
        this.accountHolder = holder;
        this.balance = initialDeposit;
        this.transactionHistory = new java.util.ArrayList<>();
        this.transactionHistory.add("Account opened with deposit: $" + initialDeposit);
    }
    
    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
        balance += amount;
        transactionHistory.add("Deposit: $" + amount);
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Withdrawal must be positive");
        if (balance - amount < MINIMUM_BALANCE) {
            throw new IllegalStateException("Cannot withdraw - would fall below minimum balance");
        }
        balance -= amount;
        transactionHistory.add("Withdrawal: $" + amount);
    }
    
    public double getBalance() {
        return balance;
    }
    
    public void printStatement() {
        System.out.println("========== Account Statement ==========");
        System.out.println("Account Number: " + accountNumber);
        System.out.println("Holder: " + accountHolder);
        System.out.println("Balance: $" + balance);
        System.out.println("Transaction History:");
        for (String transaction : transactionHistory) {
            System.out.println("  - " + transaction);
        }
    }
}

Common Design Patterns in Classes

Builder Pattern

public class PersonBuilder {
    static class Person {
        private String firstName;
        private String lastName;
        private int age;
        private String email;
        private String phone;
        
        // Private constructor - use builder instead
        private Person(Builder builder) {
            this.firstName = builder.firstName;
            this.lastName = builder.lastName;
            this.age = builder.age;
            this.email = builder.email;
            this.phone = builder.phone;
        }
        
        @Override
        public String toString() {
            return firstName + " " + lastName + " (" + age + ") - " + email;
        }
    }
    
    static class Builder {
        private String firstName;
        private String lastName;
        private int age;
        private String email;
        private String phone;
        
        public Builder firstName(String name) {
            this.firstName = name;
            return this;
        }
        
        public Builder lastName(String name) {
            this.lastName = name;
            return this;
        }
        
        public Builder age(int age) {
            this.age = age;
            return this;
        }
        
        public Builder email(String email) {
            this.email = email;
            return this;
        }
        
        public Builder phone(String phone) {
            this.phone = phone;
            return this;
        }
        
        public Person build() {
            return new Person(this);
        }
    }
    
    public static void main(String[] args) {
        Person person = new PersonBuilder.Builder()
            .firstName("John")
            .lastName("Doe")
            .age(30)
            .email("john@example.com")
            .phone("555-1234")
            .build();
        System.out.println(person);
    }
}

Key Takeaways

1. Classes combine data (fields) and behavior (methods) 2. Constructors initialize objects; overload them for flexibility 3. Use this when parameter names match field names 4. Static members are shared by all instances 5. Access modifiers enforce encapsulation: private fields, public methods 6. Getters/setters provide controlled access to fields 7. Design classes around cohesive responsibility 8. Use meaningful names for methods and parameters 9. Avoid too many constructor parameters—use objects to group data 10. Object lifecycle involves creation, use, and garbage collection

Quiz

Question 1: What happens if you don't define any constructor?

  • A) Compilation error
  • B) Java provides a default no-arg constructor ✓
  • C) Object creation fails at runtime
  • D) You must create objects differently

Question 2: What does this.name refer to?

  • A) The method parameter named name
  • B) The field in current object ✓
  • C) A local variable
  • D) A static field

Question 3: Can a static method access instance fields?

  • A) Yes, always
  • B) Yes, if you use 'this'
  • C) No, it can only access static members ✓
  • D) It depends on the field's access modifier

Question 4: How do you call a static method?

  • A) object.method()
  • B) ClassName.method()
  • C) Both A and B
  • D) Neither works

Question 5: What's the access scope of a private field?

  • A) Only within the same class ✓
  • B) Within same class and package
  • C) Available everywhere
  • D) Only to subclasses

Question 6: Why use getters and setters?

  • A) They're required by Java
  • B) To provide validation and control ✓
  • C) They make code faster
  • D) They reduce memory usage