Polymorphism, casting, and access control
Inheritance, interfaces and polymorphism12 min readLesson 6 of 6
6.1 One call, several behaviors
Polymorphism lets one variable type refer to objects of several subclasses, with each object running its own version of a method. The call is written once; the object decides which method body runs.
This works when the method is declared in the parent and overridden in the children. The compiler checks the call against the parent type, and the runtime uses the object's class. The mechanism is dynamic dispatch.
Polymorphism keeps loops and methods open to new subclasses. A method that accepts Payment works with a card payment or a class added next month.
public class Main {
public static void main(String[] args) {
Payment[] payments = { new CardPayment(), new CashPayment() };
for (Payment payment : payments) {
payment.pay(250);
}
}
}
class Payment {
void pay(int amount) { System.out.println("Paying " + amount); }
}
class CardPayment extends Payment {
@Override
void pay(int amount) { System.out.println("Card " + amount); }
}
class CashPayment extends Payment {
@Override
void pay(int amount) { System.out.println("Cash " + amount); }
}Output
Card 250 Cash 250
6.1.1 Upcasting and downcasting
Assigning a subclass object to a parent variable is an upcast, and it happens automatically. The variable sees the parent's members. Assigning back to the subclass type is a downcast and needs an explicit cast.
A downcast is checked at runtime. If the object is not an instance of the target type, the JVM throws ClassCastException. Check with instanceof before casting when the object type is uncertain.
Java 16 and later support a pattern form: if (payment instanceof CardPayment card) { card.refund(); }. The variable card is already cast and scoped to the true branch.
public class Main {
public static void main(String[] args) {
Payment payment = new CardPayment();
if (payment instanceof CardPayment card) {
card.refund();
}
Payment cash = new CashPayment();
System.out.println(cash instanceof CardPayment);
}
}
class Payment { }
class CardPayment extends Payment {
void refund() { System.out.println("Refunded"); }
}
class CashPayment extends Payment { }Output
Refunded false
6.1.2 Access modifiers
private limits a member to its own class. No modifier, often called package-private, limits it to the package. protected also allows subclasses in other packages. public allows access from anywhere.
Fields usually start private, with public methods for the operations that should be allowed. Methods that form the class's API are public; helpers stay private.
protected is useful for a member that subclasses need to build on. It also widens the API to every subclass, so use it when inheritance is part of the design.
public class Main {
public static void main(String[] args) {
Account account = new Account();
account.deposit(50);
System.out.println(account.type);
System.out.println(account.getBalance());
}
}
class Account {
private int balance;
protected String type = "Savings";
void deposit(int amount) { balance += amount; }
int getBalance() { return balance; }
}Output
Savings 50
6.1.3 Static and instance
A static field belongs to the class. One copy exists, and it is read through the class name. A static method also belongs to the class and runs without an object. An instance member belongs to each object, and it is called through a reference.
A static method cannot use this and cannot read instance fields directly. It can accept an object as a parameter and use its public methods. main is static because the launcher calls it before any object exists.
Every object shares the same static field. A counter of created objects is a common example. An instance field is separate for each object, even when the class has a static field too.
public class Main {
public static void main(String[] args) {
Counter first = new Counter();
Counter second = new Counter();
first.increment();
second.increment();
System.out.println(first.value);
System.out.println(Counter.created);
}
}
class Counter {
static int created;
int value;
Counter() { created++; }
void increment() { value++; }
}Output
1 2
6.1.4 Encapsulation in practice
Encapsulation keeps fields private and exposes operations that preserve the class's rules. A setter can reject an impossible value; a getter can return a copy or a derived value.
Validation in one place is easier to trust than checks repeated at every call site. The class can change its internal representation later while the public methods keep the same meaning.
Not every field needs a getter and setter. Expose the operations the outside world needs, and keep the rest internal. A class with a small public surface is easier to maintain.
public class Main {
public static void main(String[] args) {
Thermostat thermostat = new Thermostat();
thermostat.setCelsius(24);
thermostat.setCelsius(-50);
System.out.println(thermostat.getCelsius());
}
}
class Thermostat {
private int celsius;
void setCelsius(int value) {
if (value >= 10 && value <= 40) {
celsius = value;
}
}
int getCelsius() { return celsius; }
}Output
24
6.1.5 Practice
- Create a
Paymentparent withpay(int)and two subclasses that override it. Call both through a parent array. - Upcast a card payment to
Payment, then useinstanceofwith a pattern to callrefund(). - Create a class with a private field, a package-private method, and a protected field. Explain which other classes can use each.
- Add a static counter to a class and print it after creating three objects.
- Add validation to a setter and confirm that an invalid value leaves the field unchanged.
For step 3, write the answer as a table with one row per modifier and one column for each caller: same class, same package, subclass, anywhere.
