Upcasting & the final keyword
Why upcasting is always safe, plus final on data, methods, classes, and blank finals.
Upcasting: treating a subtype as its supertype
Upcasting is assigning a subclass reference to a superclass variable — moving *up* the inheritance tree toward the root. It is always safe and needs no cast syntax, because a Circle genuinely *is* a Shape:
Shape s = new Circle(); // upcast: implicit, always safe
Upcasting is what lets one method accept many types. A parameter typed to the supertype works for every subtype — the foundation of polymorphism, which the next lesson explores:
void render(Shape shape) { // accepts Circle, Square, any Shape
shape.draw();
}
After upcasting you can only call members declared on the *static* type (Shape). The object is still a Circle underneath — but to call Circle-specific methods you would have to downcast (covered in 1.6).
final: "this won't change"
final forbids further change, in three places:
- final variable / field — assign once, never reassign. For a *reference*, the reference is frozen but the object it points to can still mutate.
- final method — cannot be overridden by a subclass. Useful to lock behavior others depend on.
- final class — cannot be extended at all (e.g. String, Integer).
Worked example: a blank final
A blank final is a final field declared without an initializer; it must be assigned exactly once, in *every* constructor (or an instance initializer). This lets the value depend on constructor arguments yet stay immutable afterward:
class Sensor {
private final int id; // blank final: not set here
Sensor(int id) {
this.id = id; // assigned exactly once
}
// id = 5; would be a COMPILE ERROR: already final-assigned
}
The compiler guarantees id is set before the constructor returns and refuses any later assignment.