Master Java class design with constructors, encapsulation using private fields and public accessors, method overloading, static members, and the this keyword to build well-structured objects.
Object-Oriented Programming is not just a feature of Java — it is the organizing principle of the entire language. Every Java program consists of classes and every operation is performed by objects. Understanding how to design good classes is the difference between readable, maintainable enterprise code and tangled, bug-prone spaghetti.
A class is a blueprint: it defines the state (fields) and behavior (methods) that every object of that type will have. An object is a runtime instance of that blueprint, allocated on the heap. The four pillars of OOP — encapsulation, inheritance, polymorphism, and abstraction — all start with getting classes right. In this lesson you will learn how to define classes properly with encapsulation, constructor overloading, static members, and the critical Object methods that every custom class should override.
1// Class declaration
2public class BankAccount {
3 // Fields (instance variables) — describe the state
4 private String accountNumber;
5 private String owner;
6 private double balance; // every instance has its own copy
7 private static int totalAccounts = 0; // class variable — shared
8
9 // Constructor — called with 'new', initializes the object
10 public BankAccount(String accountNumber, String owner, double initialBalance) {
11 // 'this.' disambiguates field from parameter when they have the same name
12 this.accountNumber = accountNumber;
13 this.owner = owner;
14 this.balance = initialBalance;
15 totalAccounts++; // increment shared counter
16 }
17
18 // No-argument constructor (default values)
19 public BankAccount(String accountNumber, String owner) {
20 this(accountNumber, owner, 0.0); // delegates to 3-arg constructor
21 }
22
23 // Getters — read-only access to private state
24 public String getAccountNumber() { return accountNumber; }
25 public String getOwner() { return owner; }
26 public double getBalance() { return balance; }
27
28 // Business method
29 public void deposit(double amount) {
30 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
31 balance += amount;
32 }
33
34 public boolean withdraw(double amount) {
35 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
36 if (amount > balance) return false; // insufficient funds
37 balance -= amount;
38 return true;
39 }
40
41 // Static method — belongs to the class, not an instance
42 public static int getTotalAccounts() { return totalAccounts; }
43}
44
45// Creating objects with 'new'
46BankAccount alice = new BankAccount("ACC-001", "Alice", 1000.0);
47BankAccount bob = new BankAccount("ACC-002", "Bob"); // uses 2-arg constructor
48
49alice.deposit(500.0);
50boolean ok = alice.withdraw(200.0);
51System.out.println(alice.getBalance()); // 1300.0
52System.out.println(BankAccount.getTotalAccounts()); // 2 (static call)1// Access modifiers control visibility
2public class Employee {
3 // private: only accessible within this class
4 private String employeeId;
5 private String name;
6 private double salary;
7 private int vacationDays; // 0-25 range enforced by setter
8
9 // package-private (no modifier): accessible within same package
10 String department;
11
12 // protected: accessible in subclasses and same package
13 protected LocalDate hireDate;
14
15 // public: accessible everywhere
16 public static final int MAX_VACATION_DAYS = 25;
17
18 public Employee(String id, String name, double salary) {
19 this.employeeId = id;
20 this.name = name;
21 setSalary(salary); // use setter for validation
22 this.vacationDays = 0;
23 }
24
25 // Getter: read access
26 public String getEmployeeId() { return employeeId; }
27 public String getName() { return name; }
28 public double getSalary() { return salary; }
29 public int getVacationDays() { return vacationDays; }
30
31 // Setter with validation — this is why fields are private!
32 public void setSalary(double salary) {
33 if (salary < 0) throw new IllegalArgumentException("Salary cannot be negative");
34 this.salary = salary;
35 }
36
37 public void setVacationDays(int days) {
38 if (days < 0 || days > MAX_VACATION_DAYS) {
39 throw new IllegalArgumentException(
40 "Vacation days must be 0-" + MAX_VACATION_DAYS);
41 }
42 this.vacationDays = days;
43 }
44
45 // Business method — combines validation and state change
46 public void giveRaise(double percentIncrease) {
47 if (percentIncrease < 0) throw new IllegalArgumentException("Raise cannot be negative");
48 this.salary *= (1 + percentIncrease / 100);
49 }
50}
51
52// Client code cannot set salary to negative
53Employee emp = new Employee("E001", "Alice", 75000);
54emp.giveRaise(10); // 82500
55// emp.salary = -1000; // COMPILE ERROR: salary is private
56// emp.setSalary(-1000); // IllegalArgumentException at runtime1public class Calculator {
2 // Overloaded methods: same name, different parameter signatures
3 public int add(int a, int b) { return a + b; }
4 public int add(int a, int b, int c) { return a + b + c; }
5 public double add(double a, double b) { return a + b; }
6 public String add(String a, String b) { return a + b; } // concatenation
7
8 // Java resolves overloads at COMPILE TIME based on argument types
9 public static void main(String[] args) {
10 Calculator calc = new Calculator();
11 System.out.println(calc.add(1, 2)); // int version: 3
12 System.out.println(calc.add(1, 2, 3)); // 3-arg version: 6
13 System.out.println(calc.add(1.5, 2.5)); // double version: 4.0
14 System.out.println(calc.add("Hello", " World")); // String: "Hello World"
15 }
16}
17
18// Varargs: variable number of arguments (syntactic sugar for array)
19public class Stats {
20 public double average(double... values) { // zero or more doubles
21 if (values.length == 0) return 0;
22 double sum = 0;
23 for (double v : values) sum += v;
24 return sum / values.length;
25 }
26}
27
28Stats stats = new Stats();
29System.out.println(stats.average()); // 0.0
30System.out.println(stats.average(1, 2, 3)); // 2.0
31System.out.println(stats.average(10, 20, 30, 40)); // 25.01public class Circle {
2 // Static constant — shared by all Circle instances
3 public static final double PI = Math.PI;
4 private static int instanceCount = 0;
5
6 private final double radius;
7
8 public Circle(double radius) {
9 if (radius <= 0) throw new IllegalArgumentException("Radius must be positive");
10 this.radius = radius;
11 instanceCount++;
12 }
13
14 // Instance methods — operate on a specific object's state
15 public double area() { return PI * radius * radius; }
16 public double circumference() { return 2 * PI * radius; }
17 public double getRadius() { return radius; }
18
19 // Static method — no access to 'this' or instance fields
20 public static int getInstanceCount() { return instanceCount; }
21
22 // Static factory method pattern: alternative to constructor
23 public static Circle of(double radius) {
24 return new Circle(radius);
25 }
26
27 public static Circle unit() {
28 return new Circle(1.0); // unit circle
29 }
30}
31
32// Usage
33Circle c1 = Circle.of(5.0);
34Circle c2 = Circle.unit();
35
36System.out.printf("Area of radius 5: %.2f%n", c1.area()); // 78.54
37System.out.printf("Circles created: %d%n", Circle.getInstanceCount()); // 2
38
39// Static initializer block: runs once when class is loaded
40public class Config {
41 private static final Properties props;
42
43 static {
44 props = new Properties();
45 try { props.load(Config.class.getResourceAsStream("/config.properties")); }
46 catch (IOException e) { throw new ExceptionInInitializerError(e); }
47 }
48}1public class Point {
2 private final double x;
3 private final double y;
4
5 public Point(double x, double y) {
6 this.x = x;
7 this.y = y;
8 }
9
10 // toString: used by println, string concatenation, debuggers
11 @Override
12 public String toString() {
13 return String.format("Point(%.2f, %.2f)", x, y);
14 }
15
16 // equals: defines value equality (not reference equality)
17 @Override
18 public boolean equals(Object obj) {
19 if (this == obj) return true; // same reference
20 if (obj == null) return false; // null check
21 if (!(obj instanceof Point)) return false; // type check
22 Point other = (Point) obj;
23 return Double.compare(this.x, other.x) == 0 && // use compare for doubles!
24 Double.compare(this.y, other.y) == 0;
25 }
26
27 // hashCode: MUST be consistent with equals
28 // Two equal objects MUST have the same hash code
29 @Override
30 public int hashCode() {
31 return Objects.hash(x, y);
32 }
33
34 public double distanceTo(Point other) {
35 double dx = this.x - other.x;
36 double dy = this.y - other.y;
37 return Math.sqrt(dx * dx + dy * dy);
38 }
39}
40
41// Records (Java 16+): auto-generate toString, equals, hashCode
42public record ImmutablePoint(double x, double y) {
43 // Compact constructor for validation
44 public ImmutablePoint {
45 if (Double.isNaN(x) || Double.isNaN(y)) {
46 throw new IllegalArgumentException("Coordinates cannot be NaN");
47 }
48 }
49 // toString: Point[x=3.0, y=4.0] — generated automatically
50 // equals: value-based — generated automatically
51 // hashCode: consistent with equals — generated automatically
52}
53
54// Demonstration
55Point a = new Point(3.0, 4.0);
56Point b = new Point(3.0, 4.0);
57System.out.println(a); // Point(3.00, 4.00) — uses toString()
58System.out.println(a.equals(b)); // true — value equality
59
60// Works correctly as HashMap key because equals and hashCode are consistent
61Map<Point, String> labels = new HashMap<>();
62labels.put(new Point(0, 0), "origin");
63System.out.println(labels.get(new Point(0, 0))); // "origin" — works!A complete BankAccount class demonstrating encapsulation, constructor chaining, method overloading, static members, and proper Object method overrides.
1import java.util.ArrayList;
2import java.util.List;
3import java.util.Objects;
4
5/**
6 * BankAccount.java
7 * A well-designed class demonstrating all core OOP concepts:
8 * encapsulation, constructors, static members, overloading, toString/equals/hashCode.
9 */
10public class BankAccount {
11
12 // Class constants
13 public static final double MIN_BALANCE = -500.0; // overdraft limit
14 public static final int MAX_TRANSACTIONS = 100;
15
16 // Class-level state (static)
17 private static int nextAccountNumber = 1000;
18 private static int totalAccounts = 0;
19
20 // Instance state (private for encapsulation)
21 private final String accountId; // final: assigned once, never changed
22 private final String owner;
23 private double balance;
24 private final List<String> transactionHistory;
25
26 // Primary constructor
27 public BankAccount(String owner, double initialBalance) {
28 if (owner == null || owner.isBlank()) {
29 throw new IllegalArgumentException("Owner name cannot be blank");
30 }
31 if (initialBalance < 0) {
32 throw new IllegalArgumentException("Initial balance cannot be negative");
33 }
34 this.owner = owner.trim();
35 this.balance = initialBalance;
36 this.accountId = "ACC-" + (++nextAccountNumber);
37 this.transactionHistory = new ArrayList<>();
38 totalAccounts++;
39
40 recordTransaction("OPEN", initialBalance, initialBalance);
41 }
42
43 // Convenience constructor — delegates with default balance
44 public BankAccount(String owner) {
45 this(owner, 0.0);
46 }
47
48 // Deposit methods — overloaded
49 public void deposit(double amount) {
50 deposit(amount, "Standard deposit");
51 }
52
53 public void deposit(double amount, String memo) {
54 if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
55 double before = balance;
56 balance += amount;
57 recordTransaction("DEPOSIT " + memo, amount, balance);
58 }
59
60 // Withdraw with return value indicating success
61 public boolean withdraw(double amount) {
62 return withdraw(amount, "Standard withdrawal");
63 }
64
65 public boolean withdraw(double amount, String memo) {
66 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
67 if (balance - amount < MIN_BALANCE) return false;
68 balance -= amount;
69 recordTransaction("WITHDRAW " + memo, -amount, balance);
70 return true;
71 }
72
73 // Transfer between accounts — uses this and other instance
74 public boolean transfer(BankAccount destination, double amount) {
75 if (this.withdraw(amount, "Transfer to " + destination.accountId)) {
76 destination.deposit(amount, "Transfer from " + this.accountId);
77 return true;
78 }
79 return false;
80 }
81
82 // Private helper
83 private void recordTransaction(String type, double amount, double newBalance) {
84 if (transactionHistory.size() < MAX_TRANSACTIONS) {
85 transactionHistory.add(String.format("%s: %+.2f (balance: %.2f)",
86 type, amount, newBalance));
87 }
88 }
89
90 // Getters
91 public String getAccountId() { return accountId; }
92 public String getOwner() { return owner; }
93 public double getBalance() { return balance; }
94
95 public List<String> getTransactionHistory() {
96 return List.copyOf(transactionHistory); // defensive copy
97 }
98
99 // Static methods
100 public static int getTotalAccounts() { return totalAccounts; }
101
102 @Override
103 public String toString() {
104 return String.format("BankAccount{id='%s', owner='%s', balance=%.2f}",
105 accountId, owner, balance);
106 }
107
108 @Override
109 public boolean equals(Object obj) {
110 if (this == obj) return true;
111 if (!(obj instanceof BankAccount)) return false;
112 BankAccount other = (BankAccount) obj;
113 return Objects.equals(accountId, other.accountId);
114 }
115
116 @Override
117 public int hashCode() {
118 return Objects.hash(accountId);
119 }
120
121 public static void main(String[] args) {
122 BankAccount alice = new BankAccount("Alice", 1000.0);
123 BankAccount bob = new BankAccount("Bob");
124
125 alice.deposit(500.0, "Paycheck");
126 alice.withdraw(200.0);
127 alice.transfer(bob, 300.0);
128
129 System.out.println(alice);
130 System.out.println(bob);
131 System.out.println("Total accounts: " + BankAccount.getTotalAccounts());
132
133 System.out.println("\nAlice's history:");
134 alice.getTransactionHistory().forEach(System.out::println);
135
136 System.out.println("\nBob's history:");
137 bob.getTransactionHistory().forEach(System.out::println);
138 }
139}This BankAccount class demonstrates every core OOP concept: private fields with validation in setters prevent invalid state, constructor chaining (the no-arg constructor delegates to the primary constructor with 'this()'), method overloading (deposit and withdraw have memo versions), a static counter tracks instances, a transfer method shows two objects interacting, defensive copy in getTransactionHistory() prevents external modification of internal state, and properly implemented equals/hashCode/toString enable value comparison and use in collections. This is the kind of class design you will see and write daily in enterprise Java.
With class design mastered, the next lesson explores inheritance and polymorphism: using extends to create class hierarchies, overriding methods in subclasses, runtime polymorphism through the Liskov Substitution Principle, and the Object class hierarchy. These are the OOP features that make Java codebases extensible.
Your progress is saved in this browser, on this device only.