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