Skip to main content
Object Oriented Design, Analysis and Programming

Inheritance, Type Conversion and Abstract Classes in Java

Published: 2026-08-19
Level: postgraduate
Audience: Postgraduate students in Object Oriented Design, Analysis and Programming

Prerequisite Knowledge

This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.

Previously Covered in This Subject

  • Inheritance — Core Idea, Superclass and Subclass — covered in Lecture 1: Object-Oriented Analysis and Design
  • Types of Inheritance in Java — covered in Lecture 1: Object-Oriented Analysis and Design
  • Runtime Polymorphism and Dynamic Method Dispatch (Upcasting) — covered in Lecture 1: Object-Oriented Analysis and Design
  • Abstract Classes and Abstract Methods — Enforcing Hierarchy Symmetry — covered in Lecture 11: GRASP Patterns and Design Principles — Polymorphism, Indirection, Fabrication and Protected Variations
  • The final Keyword — Variables, Methods, Classes, Blank and Static Blank Finals — covered in Lecture 17: Constructors, Static Members, this, final and Software Development Life Cycle

# Inheritance, Type Conversion and Abstract Classes in Java

20.1 Primitive Types and Type Compatibility

20.1.1 The Java Primitive Family and Its Sizes

Hook: Why can a tiny byte value fit safely inside an int, but an int value may break when forced into a byte? The answer starts with how many bits each primitive type owns.

Java defines eight primitive types — the built-in data types the language itself supplies, not created from classes. The lecture focuses on the seven numeric and logical primitives boolean, byte, short, char, int, long, float, double. Each has a fixed storage width, a fixed range derived from that width, and a language-defined default value assigned to fields before explicit initialization.

Intuition — size is capacity: Think of each type as a box with a fixed number of binary slots. More slots mean more distinct patterns, so a larger numeric range. The range gap between two boxes is exactly why compatibility questions arise — you are asking whether the value patterns of one box all fit inside the other.

A concrete everyday mapping: a 8-slot locker (byte) can label distinct items, while a 32-slot warehouse (int) can label items. Every locker label fits in the warehouse, but most warehouse labels cannot return to the locker.

Where the analogy breaks: real boxes can overflow physically; Java types instead silently wrap or refuse at compile time rather than spilling.

Formalize — widths, patterns, and signed range

Primitive type — predefined type built into the language; storage width — number of bits reserved per value; range — interval of representable values implied by .

The eight types and their widths as presented:

Type Width Kind Default (field)
boolean 1 bit (conceptually; JVM detail varies) logical false
byte 8 bits signed integer 0
short 16 bits signed integer 0
char 16 bits unsigned integer (character) '\u0000'
int 32 bits signed integer 0
long 64 bits signed integer 0L
float 32 bits IEEE 754 single 0.0f
double 64 bits IEEE 754 double 0.0d

For a signed -bit two's complement integer, the number of patterns and the range are

For an unsigned -bit type such as Java char,

Applying this:

Derived consistently: size determines pattern count , which determines range, which determines whether one type can safely hold another's values. Boolean is special — only two values true/false (1-bit logical), not compatible with numeric types; char is unsigned unlike the signed integer family, which alters compatibility direction.

Preserved verbatim flavor: "if it is about one bit, so it can be true or false if it is about eight bits so it is about minus 128 to 127 — it is about like two to the power eight" expresses exactly the growth.

Visual intuition: Picture a number line per type. byte is a short segment centered at zero from to . short extends 256 times wider. char is a segment from to sitting entirely on the non-negative side, overlapping part of short and int but extending beyond short's negative side. int covers a line more than 65,000 times wider than short; long and double extend further by orders of magnitude. The takeaway: the line gets wider with , and moving to a wider type always covers the narrower segment — the geometric reason widening is safe.

Scope: The range formulas assume two's complement signed integers and IEEE 754 floats, which Java guarantees regardless of platform. Range alone decides storage capacity, not semantic meaning — char and short share width but not signedness, so neither is a subtype of the other. Assumption: These widths are fixed by the Java Language Specification; unlike C/C++, they do not vary by machine. Code that assumes this fixed mapping is portable.

Pitfalls

  1. Confusing char as tiny int: char is 16-bit unsigned. It cannot hold negative values, and short cannot hold all char values above . Both need a cast in opposite directions.
  2. Thinking boolean participates in widening: boolean is incompatible with every numeric type; even (int) true is illegal.
  3. Forgetting default values: Local variables have no default — they must be assigned before use — while fields do get the table defaults. Mixing these contexts causes "variable might not have been initialized" errors.

Recap + Bridge: Primitive widths fix ranges via , and range containment decides compatibility. With that foundation, the next section formalizes the two directional rules that follow: widening (small to large, automatic) and narrowing (large to small, explicit cast).

The width table and directional range idea recur directly in industry code whenever sensor or file data uses compact types but computation accumulates in int/long/double.

20.1.2 Why Type Compatibility Matters

Type compatibility asks one directional question: given source type A and target type B, can every value of A be stored in B without loss? If yes, the assignment B b = a is safe; if not, information could be lost and Java requires explicit action.

This question surfaces in three concrete places:

  • Assignment: int x = b where b is byte — safe — versus byte b = x — potentially unsafe.
  • Arithmetic operands: char + int must choose one type for the addition.
  • Method arguments: passing a short to a method expecting int versus the reverse.

The lecture frames it as a compass: compatibility has a direction. short -> int may succeed while int -> short fails, even for the same pair of types. That directional asymmetry is the seed for widening, narrowing, and promotion rules.

Scope: Compatibility here is about value range, not inheritance or polymorphism (which govern objects). Primitive compatibility is decided solely by width and signedness.

Real-world tie: APIs often return compact types (byte from an I/O buffer, short from audio samples) but callers store results in int/long. Understanding directional compatibility tells you when the assignment is free and when you must insert a checked cast.

20.1.3 Mathematical View of Range

Formalize — the -bit formulas derived step by step

Let be the bit width. Each bit is 0 or 1, so by the multiplication principle the number of distinct bit patterns is

For signed two's complement, one pattern encodes zero, half the remaining patterns encode negatives and slightly fewer encode positives, yielding

For unsigned char, all patterns encode non-negatives:

Step-by-step for byte ():

For char as unsigned 16-bit, character codes align with integer codes: 'C' maps to and, for illustration, 'X' maps to via the same ASCII/Unicode code point table. When a char is widened to int, the numeric code point is preserved: int code = 'C' yields .

Worked example — how big is each type?

  • byte: patterns → values through .
  • short: patterns → through .
  • char: patterns → through ; 'A' is , 'C' is , 'X' is .
  • int: patterns → through .
  • Width ordering by : , so double (64-bit) and long (64-bit) are the widest primitives; byte is narrowest numeric; boolean is a separate logical domain.

Sense-check: is exactly the byte range size ; the formula counts exactly.

Pitfalls

  1. Off-by-one on the upper bound: Signed max is , not , because zero consumes one pattern.
  2. Treating char as signed: char range starts at 0; is not a char.

Recap + Bridge: With range as (signed) or (char), double is widest and byte narrowest. The next concept turns this size order into executable rules: widening versus narrowing.

20.2 Type Conversion: Widening and Narrowing

20.2.1 Definitions — Automatic Conversion Versus Explicit Casting

Hook: Why does int x = b (where b is byte) compile silently, while byte b = x refuses to compile without you adding (byte)?

Type conversion occurs when a value of type A is assigned to a variable of type B with . Java decides between two outcomes based on directional compatibility.

Automatic type conversion (widening) — Java converts itself when the destination type can hold every value of the source type. No syntax needed beyond the assignment.

Type casting (explicit conversion, narrowing) — Java refuses the automatic conversion and demands the programmer write the target type in parentheses, (targetType) value, to acknowledge possible loss.

Formalize — the two conditions for widening

Widening happens exactly when:

  1. The two types are compatible (numeric-to-numeric, or char to numeric integer).
  2. The destination width is source width along the widening chain.

If both hold, target = source is automatic. Otherwise casting is required. Notice boolean fails condition 1 with every numeric type, so it never widens.

Verbatim intuition preserved: "when the value of one data type is assigned to another, the two types might not be compatible ... if the data types are compatible then Java will perform conversion automatically which is known as automatic type conversion ... if not we need to cast them explicitly."

Scope note: This section covers single assignment conversion. Expressions with multiple operands follow separate promotion rules (Section 20.3), where the "widest wins" logic extends stepwise.

20.2.2 Widening — Small to Large Is Safe

Analogy — pouring glasses (professor's own): Pouring water from a small glass into a large glass never spills — the large glass has room for everything in the small one. That is widening. Pouring the other way risks overflow unless you explicitly accept the spill.

Formalize — the widening chain

Java's widening order by width is

with char sitting alongside short at 16 bits but with its own signedness rule: char -> int -> long -> float -> double widens, but char -> short and short -> char both require casts because neither range contains the other. byte -> short -> int chains automatically.

Formally, if along this chain, then

is a widening automatic conversion. No (targetType) prefix is needed.

Worked example — short to int widens automatically

Given short A and int B:

short A = 32000;
int B = A; // automatic widening, no cast

Check: short range is , int range is . Every short value lies inside the int interval, so the assignment is safe.

Additional widenings that follow the same check:

  • int -> long: patterns fit inside .
  • long -> float -> double: width increases; integer-to-float is widening in width but may lose integer precision (see Pitfalls).

All are automatic with no cast.

Trace with values: If A = 120, then B becomes 120 exactly, type int. No information is lost; equality holds numerically.

Scope: Widening is safe for range containment, but int -> float and long -> double can lose low-bit precision because float/double trade integer precision for range. The conversion is exact only when the integer value fits in the floating-point mantissa.

Visual: Imagine bars ordered byte < short < int < long < float < double. An arrow left-to-right is green (automatic); the reverse arrow is red (needs cast). char sits beside short at the same height but offset due to unsignedness, requiring a cast sideways.

Pitfalls

  1. Assuming widening keeps exact precision to float/double: It keeps range but not necessarily low-order bits.
  2. Thinking char -> short widens: It does not; char max short max , and short min is below char min .

Recap + Bridge: Widening follows the left-to-right chain and is automatic. The opposite direction — large to small — is narrowing and requires you to write the cast explicitly.

20.2.3 Narrowing — Large to Small Requires a Cast

When , a value may not fit, so Java refuses the assignment and demands an explicit cast to show intent to accept possible loss.

Formalize — explicit cast syntax

Write the desired type in parentheses directly before the value:

char CH = (char) num;
byte b = (byte) i;

Semantics: the conversion forces the value into the target width; high-order bits are discarded (integers) or precision is reduced (floats). The cast is the programmer's acknowledgment: "I accept truncation/overflow risk."

Syntax preserved: "we need to put one character like the data type in which you want your value to be converted ... in a bracket and then this entire converted value can be assigned to the character."

Worked example — char and int in both directions, fully traced

Widening (safe, automatic):

char CH = 'C';
int x = CH; // automatic — char widens to int

Step: 'C' has code point . Since char ( to ) is contained in int, Java converts CH to its integer code and stores in x. No cast.

The reverse (narrowing) — compile error, then fix:

int num = 88;
char CH = num; // ERROR: incompatible types: possible lossy conversion from int to char

Why the error: int range vastly exceeds char range; num could be or at the type level. The compiler checks types, not the specific literal value.

Explicit narrowing fix:

int num = 88;
char CH = (char) num; // explicit cast — now legal

Trace: num is . Casting to char interprets as code point for 'X' (65 is 'A', 66 is 'B', so 88 is 'X'). Storage succeeds; CH now holds 'X'.

Second illustration — byte and int:

byte b = 10;
int k = b;      // widening, automatic
byte b2 = (byte) k; // narrowing, cast required — if k were 1000, b2 would wrap

If k = 1000 (binary 000...11 11101000), casting to 8-bit byte keeps only low 8 bits (11101000), yielding after signed reinterpretation — concrete truncation.

Visual: Two containers: narrow byte (256 slots) and wide int (four billion slots). Placing the narrow contents into the wide container always fits. Shoving the wide contents into the narrow one needs you to clamp lid and accept that excess sticks out unless you deliberately trim.

Scope: Narrowing applies to explicit assignment conversions. In expressions, narrowing never happens implicitly; promotion only moves to wider types. Placing an int expression result into a byte variable requires a cast even if the expression's value fits.

Pitfalls

  1. Thinking "88 fits so no cast needed": The compiler's rule is type-based, not value-based, for non-constant variables. Even provably fitting values need the cast.
  2. Silent data loss after casting: int 257 cast to byte becomes 1 (256 wrapped), and large int cast to char wraps modulo .
  3. Writing (char)num with a space confusion: Parentheses must enclose the type, not the value: (char) num is correct; char(num) is not Java.

Recap + Bridge: Large-to-small is narrowing and needs (targetType) value. That explicit-cast idea scales to expressions: when many types appear together, Java promotes everything toward the widest type present — the subject of Section 20.3.

20.2.4 Student Questions and Answers

Q: When should we use log base 10 versus natural log in such numeric conversions or type promotions? How many decimal places should we keep on the exam?

A: The widening/promotion chain does not involve logarithms — the choice of type is decided by the widest operand's width, not by log bases. For numeric answers, keep exact computed values and round only as the question instructs; do not arbitrarily round intermediate steps because rounding errors compound.

Q: Why does the compiler complain "possible lossy conversion" even when we know the specific int value 88 would fit in a char?

A: The compiler enforces the type rule, not a value-specific exception for general variables. Any int variable could hold a value outside char range, so char c = someInt is treated as potentially lossy and needs (char). (Constant expression edge cases exist but are not relied on here — the principle is to write the explicit cast.)

20.2.5 Industry Applications

  • Sensor and collection: A byte or short reading (e.g., 8-bit sensor) is accumulated into a long counter or double price; widening is free.
  • Graphics and I/O: A computed int coordinate or file size must be narrowed to char/byte for a compact buffer or wire protocol; the cast documents the risk and forces range checking.
  • Financial code: Monetary cents in int are widened to double for rate calculations, then explicitly narrowed back with rounding when storing to storage.

Exam note: Memorize the chain byte -> short -> int -> long -> float -> double, the cast syntax (type)value, and the compiler message "possible lossy conversion." Be ready to label any assignment as widening (automatic) or narrowing (cast required) and to trace char <-> int examples with exact code-point numbers like 67 for 'C' and 88 for 'X'.

20.3 Type Promotion in Expressions

20.3.1 The Problem With Multiple Operands

Hook: byte a = 40, b = 50; What type is a * b? Surprisingly, it is int — and that promotion is what saves the product from overflowing the byte range.

Type conversion (Section 20.2) handles one assignment target = source. Type promotion handles one expression containing multiple operands of different types, such as

where each letter may have a different width. During evaluation, every intermediate value needs a type wide enough that the narrower operands do not overflow before the final storage.

Formalize — why promotion is necessary

Suppose byte a = 50 and byte b = 40. Each is , but exceeds any byte value. If arithmetic stayed in byte, it would wrap incorrectly. Java therefore promotes narrower operands before the operation.

Verbatim preserved: "type promotion is something which is related to having multiple operands in a single statement ... by evaluating expressions, the intermediate value may exceed the range of operands and hence the expression will be promoted ... if one operand is long, float or double the whole expression is promoted to long, float or double."

Analogy: Combining ingredients measured in grams and kilograms — you convert the grams to kilograms before mixing, so the combined weight is expressed in the larger unit and nothing is lost to unit mismatch.

20.3.2 The Promotion Rule

Formalize — the complete rule as Java defines it (Java Language Specification style)

Java applies these rules to every binary numeric promotion, stepwise:

  1. If any operand is double, the other is promoted to double, result is double.
  2. Else if any operand is float, the other is promoted to float, result is float.
  3. Else if any operand is long, the other is promoted to long, result is long.
  4. Else both operands are promoted to int (so byte, short, char become int), result is int.

Equivalently, the final result type of an expression is the maximum among operand types in the order

So:

  • Any double present → whole expression is double.
  • Else any floatfloat.
  • Else any longlong.
  • Else → int (since byte/char/short have already been widened to int).

This is why mixing int and double yields double, and mixing byte and short yields int, not short.

Worked check — small cases

  • byte + short → both promote to int → result is int (even though values might fit in short).
  • int + longint promoted to long → result is long.
  • float + longlong promoted to float → result is float.
  • byte * 2byte promoted to int, literal 2 is int → result is int; storing back needs b = (byte)(b*2).

These mirror the rule table above; stepwise promotion explains each.

Scope: The "first promote to int" step means there is no expression result type of byte, short, or char. If you need a byte result, you must cast the final int back explicitly.

Visual: Draw a ladder byte → short/char → int → long → float → double. Each binary +, -, *, /, % moves the lower operand up to the higher rung before computing, and the result stays on that higher rung.

Pitfalls

  1. Expecting byte + byte to stay byte: It becomes int.
  2. Forgetting the final cast back: byte c = a * b is a compile error without (byte).

20.3.3 Worked Example — Mixed byte, char, short, int, float, double

The session works with all six categories simultaneously: byte b, char c, short s, int i, float f, double d. Expression examined is conceptually (f * b) + (i / c) - (d * s) stored into double result.

Complete promotion trace with real numbers

Assume concrete values: (byte), (char), (short), (int), (float), (double). Track types exactly.

Therefore the declaration must be

double result = (f * b) + (i / c) - (d * s); // all promoted to double

No programmer casts were written — Java inserted the promotions. The narrative emphasis is exactly: at each bracket the smaller type converts to the bigger type as needed, and ultimately the result lives in the largest type present, double.

Exactly as stated for the lecture's six-type mix: byte with float becomes float; char with int becomes int; short with double becomes double; combining those three intermediates — float, int, double — the final result is double, stored in a double variable.

Symbolically the general propagation is

If c were 'C' () or 'X' (), the integer-promotion value in i/c changes accordingly but the type flow is unchanged.

Scope: Promotion happens per binary operator, left to right as grouped by parentheses and precedence, not by scanning the whole expression at once. The "max type present" shortcut gives the same final answer because promotion is transitive.

Correctness tie: This is the companion-docs "Automatic Type Promotion in Expressions" rule from Schildt (T6): byte/short/char first to int, then long, then float, then double. Our trace matches that standard form exactly.

20.3.4 Why This Matters for Correctness

Without promotion, computed in byte would wrap (since 127 is max) before any widening to int could rescue it. Promotion ensures the multiplication itself is performed in int, so intermediate values are not prematurely truncated.

Similarly, accumulating a byte sensor stream into an int accumulator remains accurate only because each addition is promoted before the sum is formed.

Pitfalls

  1. Storing promoted result back without a cast: byte b = b * 2 fails even though 2 fits; the product is int.
  2. Integer division surprise after promotion: i / c where both are promoted to int performs integer division (truncated), even if you later assign to double; cast one operand to double first if fractional division is needed.

Recap + Bridge: Promotion is the expression-level companion to widening: the widest type wins, with byte/short/char first climbing to int. A single double in the mix pulls the whole expression to double. This numeric tower built, the lecture pivots to object-level reuse: inheritance, where a child acquires a parent's members. Exam note: Expect questions asking for the final type and value of a mixed expression, or a stepwise trace through sub-expressions. Always list the promotion at each operator and state where the final double storage is required.

20.4 Inheritance — Core Idea, Superclass and Subclass

20.4.1 What Inheritance Means

Hook: If ten classes all need a color field and a getArea() method, must you copy the same code ten times?

Inheritance is the mechanism by which a new class acquires the properties — data members and methods — of an existing class. The existing class is the parent class, also called superclass or base class. The new class is the child class, also called subclass or derived class. The keyword is extends.

Formalize — what the child gets

  • A child class declares its own data members and methods plus it can access the data members and methods of its parent class (subject to access control; private members are inherited in structure but not directly accessible).

As phrased in the session: "we are going to inherit the properties of the parent class ... the child class can have its own data members and functions plus it can access the data members and functions of the parent class — this is what the concept of inheritance is."

That yields code reuse: define color once in Shape; every Rectangle, Triangle, Circle child reuses it instead of duplicating the field and its handling.

Example skeleton:

class Super { int a; void foo() {} }
class Sub extends Super { int b; void bar() { a = 5; foo(); } }

Sub has a, foo() from Super plus its own b, bar(). Set-theoretic view: subclass objects form a subset — every Manager is an Employee, but not every Employee is a Manager.

Intuition — family, not cloning: Think of inheritance as a family recipe book. The parent book contains base recipes (fields and methods). Each child's book starts with a copy of the parent pages, then adds new pages and may annotate selected recipes with their own version. The parent book itself remains unchanged and usable on its own.

Where it breaks: unlike a photocopy, inheritance creates a live link — changing the parent class affects all children at compile time.

20.4.2 The Three Categories of Parent Methods Seen From the Child

Viewed from the derived class, parent methods fall into three groups that require different handling:

The three categories

  1. Constructors — named after the class, no return type. A child constructor that needs to initialize inherited fields must call a parent constructor via super(arguments). This must be the first statement.
  1. Overridden methods — same name and same type signature (same parameter count and types, and compatible return) exists in both parent and child. When the child's version wants to invoke the parent's version, it must qualify with super: super.methodName(args).
  1. Remaining / normal methods — defined in the parent but not overridden in the child. These are available directly by name from child code or via a child object, with no special syntax.

Verbatim preserved: "first category is constructors ... using the super keyword we can call the constructor of the base class ... second category is for overridden functions ... using super keyword followed by the name of the function ... third category is for the remaining or normal functions that we can directly call using the name of the function."

Scope example: If BankAccount defines getBalance() and SavingsAccount does not override it, SavingsAccount inherits it and calls getBalance() directly. If it does override deposit(), the overridden version must use super.deposit(...) to reach the parent's implementation.

20.4.3 The super Keyword — Two Distinct Uses

Formalize — one keyword, two syntactic roles

  • Constructor call: super(args) — must appear as the very first line of the child constructor. It delegates construction of the inherited part. Example: super(color) in Rectangle(String color, ...) calls Shape(String color) to initialize this.color.
  • Method qualifier: super.methodName(args) — placed inside any child method. It explicitly routes the call to the parent's implementation, bypassing the child's override. Example: super.toString() inside Rectangle.toString().

Mixing them fails: writing super(color) in an ordinary method or after other statements produces a compile-time error. Omitting super where overriding exists calls the child's version, not the parent's.

Mini illustration — both forms side by side

class Parent { Parent(int x) {} void show() { } }
class Child extends Parent {
    Child(int x, int y) {
        super(x); // constructor role — first line, delegates x to Parent
        // initialize y locally
    }
    @Override void show() {
        super.show(); // method role — dotted, calls Parent's show
        System.out.println("Child addition");
    }
}

First super constructs the parent part; second super reuses parent behavior.

Pitfalls

  1. Calling super(args) outside a constructor's first line: Illegal.
  2. Assuming super reaches any ancestor: It always means immediate parent; grandparent needs chained super calls level by level (Section 20.7).
  3. Confusing overriding signature: Parameter types must match exactly; otherwise it is overloading, not overriding, and super. will not help.

20.4.4 Design Aspects of Extending

The session raises the design questions that the coming examples answer: How many ways can we inherit? (Single, hierarchical, multilevel vs. forbidden multiple — Section 20.6.) How many ways can we override? Can we prevent overriding or inheritance when a stable contract is needed? (Use final — Section 20.9.) How does overriding interact with runtime dispatch via upcasting? (Section 20.10.) And when should a contract be declared but not defined, forcing every child to define it? (Use abstract — Section 20.11.)

Bank and shape families are the concrete arenas where each choice is exercised.

Recap + Bridge: Inheritance lets a child reuse a parent via extends, with three parent-method categories and two distinct super forms. To see those rules live, the next section builds a complete Shape → Rectangle/Triangle family.

20.5 Worked Example: Shape, Rectangle and Triangle

20.5.1 The Parent Class — Shape

Hook: Can a "generic shape" compute its own area? No — but it can declare the promise to compute an area, so every concrete shape knows what it must provide.

Formalize — the parent that declares shared state and a stub operation

Shape is the common abstraction. Data member: color of type String. Constructor initializes it; two methods illustrate the two inheritance patterns — a useful concrete method and a stub meant to be overridden.

class Shape {
    String color;
    Shape(String color) {
        this.color = color;
    }
    String toString() {
        return "Shape of color " + color;
    }
    double getArea() {
        // generic dimensions unknown — stub
        System.out.println("Error: shape unknown cannot compute area");
        return 0; // or error signal
    }
}
  • color belongs to Shape and is set via this.color = color in the one-argument constructor.
  • toString() is the standard object-to-string conversion, overridden to return "Shape of color " concatenated with color. It provides ready-made formatting that children can reuse via super.toString().
  • getArea() deliberately prints an error because a bare shape has no dimensions — rectangle needs length/width, circle needs radius. The method exists so the name and signature are available for every child to override with a real formula. This "declare in parent, define in each child" is a reusable design pattern and foreshadows the abstract-method solution.

Scope: Printing the error and returning 0 is a stub, not a computation. It signals insufficient data rather than computing anything. Production code would later replace this pattern with an abstract method (Section 20.11).

Visual: Shape sits at top with a box labeled color. Downward arrows to Rectangle and Triangle each add their own boxes (length/width vs base/height), but both remain connected upward to the shared color box.

Exam note: toString vs getArea here is the canonical "same signature in parent and child" case leading to overriding — be ready to identify which methods are overridden versus inherited.

20.5.2 The First Child Class — Rectangle

Formalize — inheriting via extends, initializing via super, overriding both methods

class Rectangle extends Shape {
    double length;
    double width;
    Rectangle(String color, double L, double W) {
        super(color); // parent constructor — must be first line
        this.length = L;
        this.width = W;
    }
    @Override
    String toString() {
        return "Rectangle of length " + length + " and width " + width
               + ", subclass of " + super.toString();
    }
    @Override
    double getArea() {
        return length * width;
    }
}
  • extends Shape declares inheritance.
  • Constructor takes three arguments. super(color) forwards the shared part to Shape(String) to initialize the inherited color; length and width are then set locally. Verbatim preserved: "we are using the super keyword within constructor of Rectangle class ... passing color as an argument ... it will call the constructor of the Shape class where the value of color will be initialized."
  • toString() is overridden. The body concatenates rectangle-specific data then super.toString() to append the parent's color string. The super. qualifier is essential — without it, toString() would call itself recursively via polymorphism.

Returning area:

returned as double.

Verbatim preserved: "get area is returning ... multiplying length and width and returning the resulting value from this function in the form of a double ... it is an overridden method because in the Shape class getArea was present but we were printing an error message there."

Worked trace — Rectangle with concrete values

Create Rectangle("red", 4, 5):

  1. super("red") executes Shape constructor: this.color = "red".
  2. this.length = 4, this.width = 5.
  3. toString() builds: "Rectangle of length 4.0 and width 5.0, subclass of Shape of color red" (because super.toString() returns "Shape of color red").
  4. getArea() computes , returns as double.

Sense-check: matches the driver output discussed later.

Pitfalls

  1. Placing super(color) after field assignments: Compile error — it must be first.
  2. Writing toString() without super.: You lose the parent's color contribution or create infinite recursion if you call toString() instead of super.toString().
  3. Omitting @Override: Code compiles but typos in signature become silent overloads; the annotation turns mismatches into compile errors (good practice).

20.5.3 The Second Child Class — Triangle

The structure mirrors Rectangle exactly, swapping dimensions and formula.

Parallel child with different formula

class Triangle extends Shape {
    double base;
    double height;
    Triangle(String color, double B, double H) {
        super(color);
        this.base = B;
        this.height = H;
    }
    @Override
    String toString() {
        return "Triangle of base " + base + " and height " + height
               + ", subclass of " + super.toString();
    }
    @Override
    double getArea() {
        return 0.5 * base * height;
    }
}

Area implemented as

Same reusable pattern: super(color) for construction, super.toString() for string building, distinct getArea() logic. The lecture highlights the symmetry: both children share the parent protocol but compute area differently.

Worked trace — Triangle with concrete values

Create Triangle("blue", 3, 6):

  1. super("blue")color = "blue".
  2. base = 3, height = 6.
  3. toString()"Triangle of base 3.0 and height 6.0, subclass of Shape of color blue".
  4. getArea().

Symmetry with Rectangle's 20.0 illustrates polymorphism: same getArea() name, shape-specific arithmetic.

Scope: Neither Rectangle nor Triangle redefines color — they inherit it. Redefining a field with the same name would hide rather than override, a different mechanism.

20.5.4 The Driver — How Dispatch Chooses the Right Method

Full driver with line-by-line dispatch explanation

Rectangle S1 = new Rectangle("red", 4, 5);
System.out.println(S1);           // implicitly calls S1.toString()
System.out.println(S1.getArea()); // 20.0

Triangle S2 = new Triangle("blue", 3, 6);
System.out.println(S2);           // calls Triangle's toString()
System.out.println(S2.getArea()); // 9.0

Trace:

  1. S1 constructed with red, 4, 5 → Shape part holds "red".
  2. System.out.println(S1) is equivalent to System.out.println(S1.toString()). Since S1's dynamic type is Rectangle, Rectangle.toString() runs. Inside it, super.toString() fetches "Shape of color red" from Shape, so output is Rectangle of length 4.0 and width 5.0, subclass of Shape of color red (formatting of doubles may vary).
  3. S1.getArea() resolves to Rectangle.getArea(), prints 20.0.
  4. S2 as Triangle follows the same path: toString prints base/height plus subclass-of-shape color; getArea() returns .

Clarifying exchange preserved: calling toString on a Rectangle by itself runs only the child's version. It reaches Shape.toString only if the child's body explicitly writes super.toString(). That dotted-super call is the deliberate routing — the choice is made by code inside the child's method.

Similarly, omitting super.toString() inside Triangle keeps execution in Triangle; writing it routes to Shape.

Design aside: Generic parent with stub/error behavior plus specific meaningful overrides in concrete children is a reusable family pattern appearing in graphics, CAD, and banking hierarchies.

Pitfalls

  1. Thinking println automatically chains parent toString: It does not; you must explicitly concatenate super.toString() inside the child's implementation.
  2. Confusing field inheritance with method overriding: Methods override; fields hide — different dispatch.

20.5.5 Student Questions and Answers

Q: If we create a Rectangle and call toString, does it automatically also call Shape's toString? Do we need super.toString()?

A: Calling toString on a Rectangle runs only Rectangle's toString. The run transfers to Shape's toString only if inside Rectangle's toString you explicitly write super.toString(). That dotted call is the intentional routing to the parent version. Triangle is identical — super.toString() inside Triangle.toString is what reaches Shape.

Why the doubt is plausible: It is natural to imagine that overriding somehow chains automatically; Java requires you to state the delegation explicitly, which preserves control over what parent behavior to include and where to place it in the string.

Q: Is the error message in Shape's getArea a stub? Why not leave it empty?

A: The parent's getArea exists so the signature is available everywhere and children have something to override. An empty body would compile but give no diagnostic. Printing "shape unknown cannot compute area" makes clear that Shape alone lacks dimensions. Children then replace the stub with real mathematics: length * width for rectangle and for triangle.

Replacement note: Later the lecture refines this stub pattern into the stronger abstract contract (Section 20.11), where the parent declares but does not define the method, and the compiler forces every concrete child to define it.

Recap + Bridge: Shape shows stub versus useful concrete methods, while Rectangle and Triangle show super(color) for construction and super.toString() for selective reuse. The driver shows dispatch choosing the child's version. Next, the lecture classifies how such parent-child links can be arranged across Java.

20.6 Types of Inheritance in Java

Hook: If a class can have one parent, why not two? Java deliberately draws a line here — and that line changes your design choices.

Java supports only single-parent inheritance per class, but that single link can be composed into three allowed shapes built from one rule: class Child extends Parent. The lecture walks each shape with a minimal skeleton and a diagram description.

20.6.1 Single Inheritance

Formalize — one parent, one child

class A { /* parent */ }
class B extends A { /* child */ }

B inherits directly from A. Diagram: A at top, single arrow down to B. This is single-level inheritance — exactly one extends step. It is the building block for all larger hierarchies.

When to use: Two classes with a clear is-a relationship and no need for siblings, e.g., BankAccount → SavingsAccount if checking were not present.

20.6.2 Hierarchical Inheritance

Formalize — one parent, multiple children

class X { /* parent */ }
class A extends X {}
class B extends X {}
class C extends X {}

A, B, and C all extend the same parent X. Diagram: X at top, three downward arrows to A, B, C. Each child inherits the shared state of X plus its own specialization.

Canonical domain example already seen: Shape as X, with Rectangle, Triangle, Circle as A, B, C. Each sibling shares color logic via super(color) but computes getArea differently. The same shape also describes the bank family before abstraction: BankAccount as X, SavingsAccount/CheckingAccount as A/B.

Industry note: GUI toolkits use this shape heavily — Component as X, Button, TextField, Canvas as siblings.

20.6.3 Multilevel Inheritance

Formalize — chaining extends over levels

class A { /* grandparent */ }
class B extends A {} // child of A, parent of C
class C extends B {} // inherits B, transitively A

B is a child with respect to A but a parent with respect to C. The chain A -> B -> C models layered specialization. C transitively inherits members of A via B.

This shape is the testbed for the grandparent-access question (Section 20.7): can C reach A's methods directly? Answer: only transitively via B's super, not via super.super.

Design intuition: Each level adds one layer of refinement — e.g., Account → BankAccount → SavingsAccount — where the middle layer may add bank-level logic and the leaf adds product rules.

20.6.4 Multiple Inheritance — Not Supported Directly in Java

Formalize — one child, two parents (forbidden for classes)

class A {}
class B {}
class C extends A, B {} // compile error — illegal in Java

As stated: "a class in Java cannot have more than one parent class." Attempting class C extends A, B fails to compile. The diagnostic phrase preserved: attempting it "will start complaining ... will give you error because this is something which is not allowed in Java."

Why the restriction: The "diamond problem" — if A and B both define foo() with different bodies, C would have two competing implementations with no clear rule for super.foo(). Java avoids this ambiguity for classes; interfaces with default methods have explicit resolution rules, but class multiple inheritance is disallowed entirely.

Companion alignment: Both T6 Chapter 8 and T2 Chapter 6 emphasize: "Java does not support the inheritance of multiple superclasses into a single subclass."

What to use instead: For shared capabilities across branches, use interfaces (implements) or composition ("has-a") rather than extra extends parents.

Scope: The ban is on extends with multiple classes. A class can implement multiple interfaces and extend one class. That interface mechanism is a different hierarchy.

Quick classification drill

  • class Manager extends Employee → single inheritance.
  • Shape → Rectangle, Shape → Triangle → hierarchical.
  • Shape → Rectangle → FilledRectangle → multilevel (hypothetical third level).
  • class D extends A, B → multiple — illegal; compiler rejects before any object is created.

Check: For each declaration, count comma-separated parents after extends. More than one name means illegal for classes.

Pitfalls

  1. Typing class C extends A, B because it reads naturally: Java requires separate interface paths.
  2. Thinking sibling classes share each other's members: Rectangle and Triangle do not inherit from each other; they only share via the common parent Shape.
  3. Confusing multilevel with hierarchical: Multilevel is a vertical chain; hierarchical is a horizontal fan.

20.6.5 Student Questions and Answers

Q: Can we inherit more than one parent class in a child class?

A: No. A Java class can extend at most one class directly. Writing class C extends A, B is illegal and produces a compile-time error. That arrangement would be multiple inheritance and is not supported for classes. The design alternatives are interfaces and composition.

Why the doubt is plausible: Conceptually a real-world entity (e.g., a teaching assistant who is both Student and Employee) has two roles; modelling that with two parents feels natural. Java channels such cases into interface roles or aggregation rather than class multiple inheritance to keep method dispatch unambiguous.

Recap + Bridge: Single, hierarchical, and multilevel are the allowed extends shapes; multiple parents per class is forbidden and fails at compile time. The multilevel shape raises the immediate next question: how do constructors and super calls travel up that chain?

20.7 Constructor Invocation Rules and the Grandparent Access Question

20.7.1 Can We Call a Parent or Grandparent Constructor Directly?

Hook: You wrote super(color) correctly in a constructor — but the moment you try the same super line inside a regular method, the compiler stops you. Why?

Four related questions sharpen object construction in hierarchies:

  • Can we call the parent constructor directly?
  • Can we call the grandparent constructor directly?
  • Can we call any constructor of any class from anywhere?
  • In a multilevel chain, can we reach the grandparent's method via super.super.method()?

Formalize — constructor invocation is tightly restricted

  • Constructors are invoked only via this(...) or super(...).
  • That invocation, when present, must be the very first statement of another constructor.
  • No other location — not an ordinary method, not the middle of a constructor — may contain super(...) or this(...).

Attempting otherwise yields a compile-time error. This is a language rule, not a convention.

Concrete illustration with Shape/Rectangle: inside Rectangle(String color, ...), the line super(color) is legal because it is first in that constructor and delegates color to Shape(String). Writing super(color) inside a separate void init() method of Rectangle, outside any constructor, fails.

The same restriction blocks jumping directly to a grandparent constructor. You can only call the immediate parent's constructor; that parent in turn calls its parent. Construction therefore proceeds top-down implicitly.

Verbatim preserved: "you cannot call a constructor from a method — the only place from which you can invoke constructors using this or super is the first line of another constructor. If you try to invoke constructors explicitly elsewhere, a compile time error will be generated."

Companion support: T6 Chapter 8 notes super() must be the first statement and, if omitted, Java inserts super() (the no-arg parent constructor) automatically.

Scope: This rule is about super(...) as a constructor call (with parentheses containing arguments and standing alone as a statement). super.method() as a dotted method call follows the different rule "inside any method, to call the overridden parent version."

Legal vs illegal constructor calls

class Shape { Shape(String c) { color = c; } }
class Rectangle extends Shape {
    Rectangle(String c, double L, double W) {
        super(c);        // legal — first line of this constructor
        this.length = L;
    }
    void reset(String c) {
        // super(c);     // illegal — not in a constructor's first line
    }
}

Only the first super(c) compiles. The second, if uncommented, produces "call to super must be first statement in constructor" (or "not in a constructor").

20.7.2 Worked Multilevel Example — Grandparent, Parent, Child Print

To test super chaining for ordinary methods, a three-level hierarchy is built where each level overrides the same print() method and deliberately calls super.print().

Complete trace with explicit control flow

class Grandparent {
    void print() { System.out.println("Grandparent's print"); }
}
class Parent extends Grandparent {
    @Override void print() {
        super.print(); // calls Grandparent's print
        System.out.println("Parent's print");
    }
}
class Child extends Parent {
    @Override void print() {
        super.print(); // calls Parent's print, which in turn calls Grandparent's
        System.out.println("Child's print");
    }
}
Child C = new Child();
C.print();

Execution order, traced line by line:

  1. C.print() enters Child.print(). First statement super.print() transfers control to Parent.print().
  2. Parent.print() first statement super.print() transfers to Grandparent.print().
  3. Grandparent.print() prints Grandparent's print, then returns.
  4. Control back in Parent.print() continues to System.out.println("Parent's print").
  5. Control back in Child.print() continues to System.out.println("Child's print").

Output produced:

Grandparent's print
Parent's print
Child's print

The key observation: Child reached Grandparent's code transitivelyChild -> Parent -> Grandparent via two single super hops. No super.super was needed.

Assumption: Each class correctly uses super.print() (not this.print() or print()). Calling this.print() from Parent would recurse to Parent.print itself, not to Grandparent.

20.7.3 The Illegal super.super Attempt

Formalize — super.super is illegal syntax

Attempting to jump two levels directly:

super.super.print(); // illegal — will not compile

was tried and correctly fails. The session states explicitly: "if I try to write something like super.super.print() ... so that using the first super I can refer to print of the parent and using another super I can refer to print of grandparent ... this is something which is not going to work — your program will start throwing error and it is not going to compile."

Correct pattern is the level-by-level chain shown above: Child calls super.print() to Parent; Parent calls super.print() to Grandparent. Invoking C.print() where C is Child therefore reaches Grandparent transitively, not directly.

Why illegal: super.super would require super to evaluate to a value-like reference that itself has a super, but super is not an expression — it is a keyword tied to the immediate parent of the current class. The language intentionally keeps super single-step to preserve encapsulation.

Pitfalls

  1. Reading super as a variable chain: It is not this.super; you cannot dot a second time.
  2. Forgetting to implement super.print() at the middle level: Then Child's super.print() reaches only Parent and stops; grandparent logic is silently skipped.

20.7.4 Student Questions and Answers

Q: Can we write super.super.print() from Child to jump two levels?

A: No. super.super.print() is illegal and does not compile. super always means the immediate parent. Child should call super.print() to reach Parent, and Parent should call super.print() to reach Grandparent. The chained single-step calls propagate top-down and produce the same observable order Grandparent → Parent → Child.

Why the doubt is plausible: In multilevel hierarchies it feels natural to want a "grandparent" direct line; Java funnels that intent through the parent, so the parent retains control over whether and when to forward.

Q: Can we call a parent constructor from an ordinary method instead of from a constructor?

A: No. super(...) (or this(...)) can appear only as the first line of another constructor. Calling a constructor from a regular method or from any other position is a compile-time error. Constructors are not methods and cannot be invoked like them.

Why the doubt is plausible: Constructors look like methods with class names, but they are initialization contracts tied to object creation order, so the language locks their call sites.

Recap + Bridge: super(...) is constructor-only and first-line-only; super.method() is anywhere but reaches only one level. Grandparent access is therefore transitive, not direct. That call discipline prepares the richer bank hierarchy where both constructor super and method super appear together. Exam note: Be ready for output-prediction with print chains and for compile-error spotting with super.super or super(...) placed incorrectly.

20.8 BankAccount Family: SavingsAccount and CheckingAccount With Overriding

20.8.1 The Parent Class — BankAccount

A banking domain makes the reuse versus specialization trade-off tangible. Parent BankAccount holds the common account state and generic operations; children add product-specific rules and reuse parent logic instead of duplicating it.

Formalize — parent with shared fields and balance mechanics

class BankAccount {
    String accountNumber;
    String name;
    double amount; // or float in some slides

    BankAccount(String accNo, String name, double amount) {
        this.accountNumber = accNo;
        this.name = name;
        this.amount = amount;
    }
    void setAccount(String accNo) { this.accountNumber = accNo; }
    void setName(String name) { this.name = name; }
    double getBalance() { return amount; }

    void deposit(double amt) {
        amount = amount + amt; // addition
    }
    void withdraw(double amt) {
        if (amount < amt) {
            System.out.println("Insufficient funds");
        } else {
            amount = amount - amt; // subtraction
        }
    }
}

Step-by-step, preserved verbatim logic:

  • Fields accountNumber, name, amount describe any account. Constructor sets all three from supplied values.
  • setAccount/setName assign explicitly from parameters to this. fields.
  • getBalance() simply returns the current amount — the single read path children will reuse.
  • deposit adds amt to amount and stores back; future getBalance sees the new sum.
  • withdraw first checks amount < amt. If true, insufficient funds; otherwise amount -= amt. Conditional and subtraction are exactly "if current value of amount is less than amount that you are requesting to withdraw, insufficient funds ... otherwise amount has to be deducted, subtraction operation is to be performed."

Invariants established: amount is the single source of truth; every balance change flows through deposit/withdraw, so counting or fee logic built atop them inherits correctness.

Analogy: BankAccount is the bank's filing cabinet drawer — every account has a label (accountNumber), holder name, and cash pile (amount). Common tools deposit, withdraw, getBalance are the drawer's shared cash-register operations; product drawers (savings, checking) add their own tools rather than building a second register.

Scope: This class is shown concrete at first, so direct new BankAccount(...) would compile. The lecture later strengthens it to abstract when child-specific contracts need enforcement (Section 20.11).

20.8.2 The First Child — SavingsAccount and addInterest

Formalize — adding a rate and reusing parent balance operations

class SavingsAccount extends BankAccount {
    double interest; // e.g., 9 percent

    SavingsAccount(String accNo, String name, double amount, double interest) {
        super(accNo, name, amount); // forward first three args to parent
        this.interest = interest;
    }
    void addInterest() {
        double intAmt = getBalance() * interest / 100.0;
        deposit(intAmt);
    }
}
  • Extra field interest holds the savings rate; example uses .
  • Constructor takes four parameters. The first three are delegated via super(accNo, name, amount) to BankAccount's constructor, avoiding repetition. Verbatim benefit: "using the super keyword you are not required to rewrite the code ... it will automatically initialize the values of account, name and amount as per the constructor of BankAccount. Otherwise I need to write the code again." Local field interest is then assigned.
  • addInterest reuses parent methods intentionally: it calls getBalance() (defined in BankAccount) to read current amount, computes

then calls deposit(intAmt) (parent's deposit) to add that interest back into amount. No direct amount += ... in the child is needed; tested parent logic does the work.

Visual: SavingsAccount adds one shelf (interest) to the parent drawer and one procedure addInterest that reads the cash pile through the parent's viewer (getBalance) and returns proceeds through the parent's deposit slot.

Worked driver — numeric trace fully recomputed

SavingsAccount SA = new SavingsAccount("111", "Ankit", 5000, 9);
System.out.println(SA.getBalance()); // 5000
SA.deposit(1000);                    // 5000 + 1000 = 6000
System.out.println(SA.getBalance()); // 6000
SA.addInterest();                    // interest = 6000 * 9 / 100 = 540; 6000 + 540 = 6540
System.out.println(SA.getBalance()); // 6540
SA.withdraw(6000);                   // 6540 - 6000 = 540
System.out.println(SA.getBalance()); // 540

Computation, step by step:

  1. Construction: super("111","Ankit",5000)amount = 5000; interest = 9.
  2. getBalance().
  3. deposit(1000): .
  4. addInterest(): . deposit(540): .
  5. withdraw(6000): , so .
  6. Final getBalance().

Reconciliation of spoken discrepancy: The session voices the final balance as in one moment, but the arithmetic from the stated inputs yields . The reconciled correct value is ; the utterance is a minor spoken slip. No formula change is needed — the interest formula and steps above are internally consistent.

Sense-check: of is indeed about one-tenth minus a bit ( minus ), so is plausible.

Pitfalls

  1. Manipulating amount directly instead of via deposit/getBalance: You lose the indirection that would later enforce fees or logging.
  2. Forgetting that interest is a percent, not a fraction: The division by is essential; using directly is equivalent but must not be combined with another division by 100.
  3. Interest integer truncation: With int arithmetic, is exact, but other rates like with small balances may truncate if done in integers — double preserves fractional paise.

20.8.3 The Second Child — CheckingAccount, Fees and Overridden Deposit/Withdraw

CheckingAccount introduces transaction-fee business rules: a free-service allowance, then a per-transaction charge. The implementation overrides deposit/withdraw to count transactions and adds a fee-deduction helper.

Formalize — fields, delegation, and overriding pattern

class CheckingAccount extends BankAccount {
    double transactionFee = 25;
    int freeTransactions = 2; // first two transactions free
    int transactionCount = 0;

    CheckingAccount(String accNo, String name, double amount) {
        super(accNo, name, amount);
    }
    void deductFee() {
        if (transactionCount > freeTransactions) {
            double fee = (transactionCount - freeTransactions) * transactionFee;
            withdraw(fee); // deduct from amount
        }
    }
    @Override
    void deposit(double amt) {
        transactionCount++; // deposit counts as a transaction
        super.deposit(amt); // add via parent's deposit
    }
    @Override
    void withdraw(double amt) {
        transactionCount++; // withdraw counts as a transaction
        super.withdraw(amt); // deduct via parent's withdraw
    }
}
  • Fields encode the rule: transactionFee = 25\) rupees per extra transaction beyond freeTransactions = 2; transactionCount` tracks deposits/withdrawals.
  • Constructor again delegates via super(accNo, name, amount).
  • deductFee checks transactionCount > freeTransactions. If true, computes

With transactions and free, . It then calls withdraw(fee) to deduct from amount. Verbatim preserved: "number of transaction count minus free number of transactions and multiplied by the transaction fee which is defined here as 25 — so 25 rupees per extra transaction should be charged."

  • deposit and withdraw are overridden. In each, transactionCount is incremented first (the operation itself is a counted transaction), then the parent's version is invoked via super.deposit(amt) or super.withdraw(amt). That delegation handles the actual addition/subtraction with the insufficient-funds check. Verbatim preserved: "deposit itself is a transaction so transaction count is incremented and then we are calling super method ... super.deposit and amount we are passing ... it should be added and this addition should be reflected to current amount."

Worked driver — three transactions plus fee

CheckingAccount CA = new CheckingAccount("112", "Ankit", 5000);
System.out.println(CA.getBalance()); // 5000
CA.deposit(1000);  // transaction 1, balance 6000
CA.withdraw(2000); // transaction 2, balance 4000, still within free limit
CA.deposit(6000);  // transaction 3, balance 10000, exceeds free limit by 1
System.out.println(CA.getBalance()); // 10000
CA.deductFee();    // fee 25 deducted, amount = 9975
System.out.println(CA.getBalance()); // 9975

Line-by-line type/flow:

  1. Construct: amount = 5000, counters zero.
  2. deposit(1000): transactionCount →1, super.deposit(1000).
  3. withdraw(2000): transactionCount →2, super.withdraw(2000) (still free limit, so deductFee would be zero if called here).
  4. deposit(6000): transactionCount →3, .
  5. deductFee(): true → withdraw(25).

Trace matches the spoken walk: "5000 plus 1000 minus 2000 plus 6000 — so again I am going to get 10000. Now if I go for deducting the fees because of the third transaction, 25 rupees ... after deducting the transaction fees of 25 rupees, this is the amount that should be reflected ..."

Code reuse highlighted: "we no need to write the code again and again, we just need to call the method of the parent class accordingly."

Collective summary: Through savings and checking, we see superclasses, subclasses, overloaded/overridden methods, constructor calls via super, and overridden method delegation via super.methodName — all reducing duplicated lines while encoding distinct business rules.

Scope & Pitfalls

  • Override annotation: @Override on deposit/withdraw catches signature typos — omitting it lets a misspelled deposite silently become an overload rather than an override.
  • Incrementing after super: Reversing the order (increment after super.deposit) would still count but obscures that the count belongs to the attempted transaction even if withdraw later fails due to insufficient funds — incrementing first matches the session's logic.
  • Fee via withdraw inherits guard: Calling withdraw(fee) reuses the "insufficient funds" check, so fees are not charged against an empty account without a message — an accidental direct amount -= fee would bypass that guard.

20.8.4 Student Questions and Answers

Q: Why does addInterest call getBalance() instead of reading the amount field directly? And why call deposit for interest?

A: getBalance() and deposit encapsulate correct access to amount. Calling them reuses tested parent logic: getBalance() reads amount consistently, and deposit adds to it and preserves invariants (such as future logging or fee hooks). addInterest computes getBalance() * interest / 100 then deposit adds that result, which automatically updates amount through the canonical path rather than fragmenting balance manipulation across children.

Why the doubt is plausible: The field amount is visible in these protected-like examples, so direct field math seems shorter. The encapsulation route is preferred because it centralizes every balance mutation through deposit/withdraw, reducing bug surface when rules change.

Q: In CheckingAccount, why does each override increment transactionCount before delegating with super?

A: Each customer-visible deposit or withdraw is a counted transaction. Incrementing first ensures deductFee later sees the correct count that includes the current operation. Delegation via super.deposit or super.withdraw then handles the underlying cash movement with insufficient-funds checking. Counting before delegation also counts an attempted withdraw even if it later prints "Insufficient funds" — the attempt itself was a service interaction.

Why the doubt is plausible: One might think to count after successful completion; the session's ordering counts every invocation, which matches "per interaction" fee policies more naturally than "per successful funds movement."

Recap + Bridge: Savings reuses the parent read/write path for interest; checking wraps it with counting and a fee hook, both via super. The same bank family soon exposes a design tension: deductFee was only in CheckingAccount, so a uniform BankAccount reference cannot call it — the problem that final and then abstract solve in expanding layers.

20.9 The final Keyword — Variables, Methods, Classes, Blank and Static Blank Finals

20.9.1 Meaning According to Target

Hook: How do you tell the compiler "no one may ever change this value, override this behavior, or extend this class again" — and still choose the value at construction time?

The final keyword is a seal whose meaning depends on what it marks. One keyword, three distinct restrictions.

Formalize — target-dependent semantics

  • Before a variable (final int x): makes it a constant. Once assigned, its value cannot be reassigned. The single-assignment rule for fields is detailed in the following subsections.
  • Before a method (final void getArea()): prevents overriding. No child class may provide a new definition with the same signature. Useful for security or invariant-critical methods.
  • Before a class (final class A): prevents inheritance entirely. class B extends A becomes a compile error. The class hierarchy is sealed at that node.

Verbatim preserved: "whenever we are using final keyword in Java, if I am using it with respect to variable, it means I am talking about making the variable constant ... if it is final keyword if I am talking about method so it prevents method overriding ... if I am putting final before the name of a class so I cannot even inherit this class in any of the coming classes."

Intuition — sealing analogy (professor's own): Think of final as a wax seal. Sealing a variable locks its value; sealing a method locks its behavior; sealing a class locks its family — no new children may claim it. Blank finals add nuance: the seal is placed early, but the value is chosen once at construction or class loading, then frozen.

20.9.2 Blank Final Variables

A blank final variable is a final variable not initialized at declaration. It may be assigned exactly once — preferably within the constructor — and then never reassigned.

Formalize — the one-time assignment rule

class First {
    final int I; // blank final — declared but not yet assigned
    First() { I = 10; } // first and only assignment — legal
    void demo() {
        // I = 20; // ERROR — cannot reassign final
    }
}

In main:

final int I = 5; // local final — assigned once at declaration
System.out.println(I); // 5
// I = 6; // ERROR — second assignment disallowed

Narrative preserved: "variable is initialized once ... if you try to initialize the value again it will not take and it will start throwing error because this is only one time where we can initialize the value of a final variable."

A variant where the blank final I is never assigned in any constructor also fails — the compiler requires definite assignment exactly once on every construction path. For locals, the same rule applies: assign once before use, never again.

Worked lifecycle — blank final through two constructions

class First {
    final int I;
    First() { I = 10; }
    First(int v) { I = v; } // each constructor assigns once
}
First a = new First();    // a.I is 10
First b = new First(99);  // b.I is 99
// b.I = 100; // error regardless of instance

Each object's I is fixed at construction. Sense-check: two objects can hold different final values (10 vs 99), but neither object's I changes after construction.

Pitfalls

  1. Forgetting to assign in every constructor: If one constructor omits the assignment, that path fails to compile.
  2. Confusing final reference with immutable object: final StringBuilder sb = new StringBuilder() fixes the reference, not the object's contents — sb.append(...) is still allowed; sb = new StringBuilder() is not.

20.9.3 Static Blank Final Variables

Formalize — class-level one-time initialization

A static variable is a class variable shared across all instances and accessible via the class name. A static blank final variable is static final but not initialized at declaration; it can be initialized only in a static block — the special block executed when the class is loaded, before any instance exists.

class A {
    static final int data; // static blank final — declaration without value
    static {
        data = 50; // initialization in static block — legal, runs once at class load
    }
}

After the static block runs, A.data always yields and cannot be reassigned. Verbatim preserved: "static final int data — that's a static blank final variable because it is declared with final keyword and it is a static variable. So this is a static block like data = 50 ... you can use it throughout in your class as a static variable because it is initialized in the static block ... its value cannot be modified."

Lifecycle note: The static block executes once when the class loader first loads A, not per object. Therefore data is a true class constant whose value may be computed (e.g., reading a config) but is then frozen.

Access pattern

System.out.println(A.data); // 50 — no instance needed
// A.data = 60; // ERROR

If the static block were omitted and no inline initializer provided, A.data would be "not initialized" and the class would fail to compile.

Contrast with instance blank final:

  • final int I in First: each object assigns once in its constructor.
  • static final int data in A: the class assigns once in its static block, shared by all.

20.9.4 Design Intuition

When to use each seal:

  • Variable: Configuration constants, keys, mathematical constants, or account numbers that must not mutate post-construction. Use final fields to make mutation a compile error rather than a runtime bug.
  • Method: Sensitive invariants such as security checks, identity, or core area logic you do not want children to silently change.
  • Class: Utility classes (String, Math-like), security-sensitive types, or designs where inheritance would break contracts (T2 notes String is final for this reason).
  • Blank vs assigned: Choose blank finals when the correct constant differs by construction path (e.g., account created with different limits per branch) but still must be frozen after that choice.

Companion note: T6 and T2 both note final classes cannot be extended and final methods cannot be overridden — our treatment matches those references exactly; finally (exception handling) is unrelated.

Pitfalls

  1. Marking a method final to "make it faster": Modern JVMs inline regardless; use final for design intent, not performance mythology.
  2. Assuming final class makes instances immutable: It only seals the hierarchy; instance fields still mutate unless themselves declared final.

Recap + Bridge: final seals a value, a behavior, or a whole family, with blank and static-blank variants allowing one deferred-but-frozen assignment. With sealing understood, the next hierarchy property that depends on it is runtime choice: which overridden method actually executes when types differ between reference and object.

20.10 Runtime Polymorphism and Dynamic Method Dispatch (Upcasting)

20.10.1 Definitions — Polymorphism, Overriding, Dynamic Dispatch

Hook: Two shapes both answer getArea(), but rectangle multiplies and triangle halves. How can the same call name do two different calculations without an if-else?

Polymorphism means "one name, multiple forms." The session's concrete tie is method overriding: a method such as getArea or show exists in the parent with one implementation and in each child with its own — same name and signature, different bodies.

Dynamic method dispatch is the mechanism by which a call to an overridden method is resolved at runtime rather than at compile time — the runtime looks at the actual object's type, not the reference's declared type.

Formalize — the three linked ideas

  • Method overriding — child redefines a parent method with identical name and parameter types (and compatible return). Annotation @Override is good practice to signal intent.
  • Polymorphism — the single name (show, getArea) names multiple implementations across the hierarchy.
  • Dynamic dispatch — the call ref.overriddenMethod() is bound at execution time to the method belonging to the object ref actually points to.

Verbatim preserved: "dynamic method dispatch is the mechanism by which call to an overridden method is resolved at runtime rather than compile time ... we have a getArea method which is present in the parent class and which is present in the child class as well — during execution it is going to be decided at the runtime which class getArea is to be executed." Also: "method overriding is one of the ways in which Java supports runtime polymorphism ... dynamic method dispatch."

20.10.2 Upcasting — Reference Variable of Superclass Referring to Child Object

Formalize — the enabler of runtime choice

Upcasting is the pattern where a superclass reference variable refers to a child object:

Parent obj = new Child(); // left: declared Parent, right: created Child

Determination of which overridden method runs is based on the object being referred to (right side), not the declared reference type (left side). The same reference can be reassigned over time to different concrete children and each call dispatches to the current child's method.

This is the foundation for extensible code: a framework holds a Shape reference, plugins supply new Shape children, and the framework's shape.getArea() always invokes the correct plugin without recompilation.

Relation to Section 20.5: Shape s = new Rectangle("red",4,5); s.getArea() should invoke Rectangle.getArea() at runtime; reassigning s = new Triangle("blue",3,6) should then invoke Triangle.getArea().

Analogy: The reference is a remote control labeled "Shape"; the actual object is the device plugged in (rectangle or triangle box). Pressing getArea on the remote executes the device's own program, not the label's.

20.10.3 Worked Example — Parent and Child share show

Complete trace — upcasting with show()

class Parent {
    void show() { System.out.println("Parent's show"); }
}
class Child extends Parent {
    @Override
    void show() { System.out.println("Child's show"); }
}

Parent obj = new Child(); // upcasting — reference Parent, object Child
obj.show(); // calls Child's show

Narrated trace, emphasizing the highlighted line:

  1. Parent obj = new Child() — left side declared type is Parent; right side object type is Child. The assignment is an upcast and is legal because a Child is-a Parent.
  2. At obj.show(), the question is: which show runs? The session states: look to the right-hand side — the actual object type. Since new Child() created a Child, Child.show() executes, printing Child's show.

Verbatim preserved: "I have an object ... created for the parent class ... but on the right side for creating this object I have taken the reference of the child class ... this is something that is known as upcasting ... when I am calling obj.show ... which show method is to be referred ... ultimately what is going to do it will take you to the child class ... because whenever this kind of situation occurs you just need to look at the right hand side — the reference to which a particular object is initialized."

Same principle for area:

Shape s = new Rectangle("red", 4, 5);
System.out.println(s.getArea()); // Rectangle.getArea() → 20.0 at runtime
s = new Triangle("blue", 3, 6);
System.out.println(s.getArea()); // Triangle.getArea() → 9.0 at runtime

One name getArea, two forms selected by the live object type.

Scope: Only overridden instance methods dispatch dynamically. static methods hide (not override) and are resolved by declared type; final methods cannot be overridden at all, so dispatch is moot; fields are not polymorphic — they follow declared type.

20.10.4 Why Runtime Versus Compile Time Matters

If dispatch were compile-time (static), obj.show() would always call Parent.show() due to declared type Parent, even when the object is a Child. Every new child would require new if-else branches.

Runtime dispatch removes that fragility. A single variable Shape s or BankAccount ba can hold heterogeneous children over its lifetime; each call s.getArea() or ba.deductFee() executes the child's correct logic without the caller knowing the child's class at compile time. This powers plug-in architectures, collections of heterogeneous shapes, and the abstract-contract pattern in Section 20.11.

Companion alignment: T2 Chapter 6 notes that existing libraries can call methods on new subclasses without recompiling while maintaining a clean abstract interface — exactly this runtime dispatch benefit.

Pitfalls

  1. Thinking the left type decides: It controls what methods you may call (compile-time visibility), but the right type decides which implementation runs for overridden methods.
  2. Expecting fields to be polymorphic: Parent p = new Child(); System.out.println(p.field) reads Parent's field if Child hides it.
  3. Calling super.* to "force" parent dispatch externally: External code cannot bypass dispatch; only code inside the child can use super.method() to call the parent version.

20.10.5 Student Questions and Answers

Q: How do we know which overridden method runs in Parent obj = new Child(); obj.show();?

A: Look at the object creation on the right side of =. new Child() means the actual object is a Child, so Child.show() runs, even though the variable's declared type is Parent. That choice at execution time — not at compile time — is dynamic method dispatch.

Why the doubt is plausible: Declared type dominance is intuitive from static type systems, but Java's overridden instance methods deliberately defer to the live object's type to enable polymorphism.

Recap + Bridge: One name (show/getArea) has multiple forms; upcasting (Parent ref = new Child()) plus runtime dispatch selects the form matching the actual object. With that selection rule clear, the next section asks: how can a hierarchy force every child to supply its own form?

20.11 Abstract Classes and Abstract Methods — Enforcing Hierarchy Symmetry

20.11.1 What Is an Abstract Class

Hook: What if you want a shared blueprint that every team member must follow, but you never want anyone to build a bare blueprint directly?

An abstract class is a restricted class that cannot be instantiated directly. Placing the abstract keyword before the class declaration signals this:

abstract class ABC { /* members */ }

Formalize — instantiation versus referencing

  • Direct creation is forbidden: new ABC() is illegal. As phrased: "we cannot write ABC a = new ABC() — this is not possible ... that's why it cannot be used to create objects."
  • References are allowed: ABC ref is legal, and may be assigned a concrete child object: ABC ref = new XYZ() — this is the same upcasting seen in Section 20.10.

So ABC ref = new XYZ() pairs an abstract-type reference (compile-time contract) with a concrete object (runtime behavior). Dynamic dispatch then calls XYZ's methods through the ABC-typed variable.

Verbatim preserved: "a restricted class that cannot be used to create objects — we can have references of abstract class type ... it has become a restricted class — I cannot create an object of the ABC class because it is an abstract class."

Scope: An abstract class may still have data members, constructors (default and parameterized), concrete methods, and static members. "Abstract" does not mean "empty" — it means at least one contract is deferred to children.

20.11.2 What Is an Abstract Method

A method declared abstract has no body and no statements; it states only a signature:

abstract class ABC {
    abstract void getData(); // declared, not defined — note no braces
}
class XYZ extends ABC {
    @Override
    void getData() { /* must define — body required here */ }
}

Formalize — the compulsory-override contract

  • An abstract method is a declaration without definition.
  • Any concrete (non-abstract) child that extends the abstract class is compelled to override every inherited abstract method with a body. If it does not, the child itself becomes abstract and cannot be instantiated.

Properties as stated: "if any method is declared as abstract, it is compulsory to override it ... these methods does not contain any statements — they only enforces symmetry hierarchy ... if I have public abstract void getData ... in XYZ where I am extending ABC, it becomes a necessity to define this method."

Terminology note: the session interchanges "symmetry" and "structure" to mean a consistent interface contract — every child honoring the same method set.

Companion nuance: T6 Chapter 8 and T2 Chapter 6 agree — abstract methods have no implementation; the hierarchy's symmetry (uniform interface) is enforced by the compiler turning missing overrides into errors.

Pitfalls

  1. Providing a body for an abstract method in the abstract class: Empty braces {} make it concrete, not abstract, losing the enforcement.
  2. Leaving a child without an override: The child silently remains abstract; attempting new Child() then fails with "class is abstract; cannot be instantiated."

20.11.3 Worked Abstract Example — AbstractA and DerivedA

This concrete abstract family introduces interactive input via Scanner.

abstract class AbstractA {
    // constructors still exist even though the class is abstract
    AbstractA() {} // default constructor
    AbstractA(int a) { /* parameterized — e.g., this.a = a */ }
    abstract void getData();
    abstract void showData();
}

class DerivedA extends AbstractA {
    int a;
    DerivedA() {}
    DerivedA(int a) { this.a = a; }
    @Override
    void getData() {
        // read from user via Scanner and store in a
        Scanner sc = new Scanner(System.in);
        a = sc.nextInt();
    }
    @Override
    void showData() {
        System.out.println(a);
    }
}

Trace — contracts and upcasting together

  • AbstractA declares getData and showData as abstract with no bodies. Any concrete child must supply both; otherwise the child remains abstract and cannot be instantiated — "it becomes compulsory because they are abstract methods."
  • DerivedA supplies both: getData reads from Scanner into a; showData prints a.
  • The abstract class still provides data members, constructors (default and parameterized), and potentially concrete helper methods.

Main with upcasting:

AbstractA x = new DerivedA(); // reference abstract, object concrete — allowed
AbstractA y = new DerivedA(5);
x.getData();  // runtime dispatches to DerivedA.getData() → reads Scanner input
y.showData(); // prints y's a (e.g., 5 if constructed with 5, or last Scanner value)
// AbstractA y2 = new AbstractA(); // illegal — cannot instantiate abstract class

Verbatim preserved: "this is how an object of the abstract class is created taking a reference of the DerivedA class — because we cannot take ... new AbstractA() ... not possible ... but we can take a reference of some other class for creating an object of the abstract class."

Sense-check: AbstractA's constructors are reachable: new DerivedA(5) calls DerivedA(5) which, if it contains super() or implicitly does, invokes AbstractA's default constructor to set up the abstract-class portion before the child part. Abstraction does not remove object-construction responsibility.

Scope: Scanner is java.util.Scanner — shown as a named reference. It reads typed input from System.in; nextInt() advances the scanner token and parses the next integer.

20.11.4 Team Structure Story — Why Abstraction Matters

Analogy — managerial enforcement (professor's own story): Imagine leading a team of ten developers. You want every module to implement a core set of functions without which the system cannot proceed. You publish an abstract class Main with those functions declared abstract and distribute it.

Every member's class must then extend Main. The compiler compels each class to provide definitions for every abstract method — otherwise the code does not compile. This enforces uniform structure (symmetry) across the whole codebase and converts missing-implementation bugs from runtime surprises into immediate compile-time errors.

Verbatim preserved: "if you are working with your team ... as a manager ... you are providing a structure to your team ... 10 members ... you want all members to implement at least some of the important functions ... you can create an abstract class ... within main you can take a number of abstract methods ... provide this abstract class to your team ... it becomes compulsory for them to implement all of these methods ... this is something like through abstract class you can enforce the structure, you can enforce the symmetry that you are looking for in your entire program collectively — that is the biggest advantage of having an abstract class."

Where the analogy breaks: Real teams can negotiate exceptions; abstract methods leave no room for "optional" — the enforcement is absolute unless the child also declares itself abstract.

Domain link: This pattern underlies framework APIs (e.g., servlet skeletons, test frameworks, persistence templates) where the framework supplies the workflow and the abstract methods are the variation points every application must fill.

20.11.5 BankAccount Revisited — Empty Method Versus Abstract Method Solution

The bank hierarchy exposes the concrete design problem that abstract solves elegantly.

Problem — uniform reference cannot see child-only method

In a driver with a uniform BankAccount reference:

BankAccount ba = new CheckingAccount(...); // or SavingsAccount
ba.getBalance();
ba.deposit(1000);
ba.withdraw(2000);
ba.deposit(6000);
double bal = ba.getBalance();
ba.deductFee(); // desired call — fee logic differs per product

But deductFee was originally defined only in CheckingAccount, not in BankAccount. The call ba.deductFee() through a BankAccount-typed reference therefore produces a compile error — BankAccount has no such method visible on that reference, even though the runtime object does.

Two solutions were contrasted:

Solution 1 — Empty method in parent (discarded as meaningless).

class BankAccount {
    void deductFee() {} // empty — compiles but does nothing
}

Now ba.deductFee() compiles, but it is a no-op for any child that forgets to override — fees are silently skipped. Judged as: "this is a way to avoid the error, but logically it is meaningless because we are not writing anything ... we are just creating an empty method ... so that's a meaningless solution."

Solution 2 — Abstract method in abstract parent (preferred).

abstract class BankAccount {
    String accountNumber;
    String name;
    double amount;
    BankAccount(String accNo, String name, double amount) { ... }
    void setAccount(String accNo) { ... }
    void setName(String name) { ... }
    double getBalance() { return amount; }
    void deposit(double amt) { amount += amt; }
    void withdraw(double amt) {
        if (amount < amt) System.out.println("Insufficient funds");
        else amount -= amt;
    }
    abstract void deductFee(); // declared only — every concrete child must define
}

Every concrete child must now define deductFee. Omitting it keeps the child abstract and prevents accidental silent no-ops; the missing definition becomes a compile error — the desired enforcement.

Compliance:

  • SavingsAccount defines deductFee with savings-appropriate logic (perhaps no fee or a different rule) — a definition must exist.
  • CheckingAccount defines deductFee as before:

After this change the driver compiles and dispatches polymorphically:

BankAccount ba = new CheckingAccount("123", "Ankit", 5000);
ba.deposit(1000);
ba.withdraw(2000);
ba.deposit(6000);
ba.deductFee(); // runtime routes to CheckingAccount.deductFee

BankAccount ba2 = new SavingsAccount("111", "Ankit", 5000, 9);
ba2.deductFee(); // routes to SavingsAccount.deductFee

In both, ba is declared BankAccount but the executed deductFee is the child's matching method at runtime. As summarized: "what we can see that an abstract class can have its own data members, an abstract class can have its constructor and an abstract class can have its own methods like setAccount, setName, getBalance, deposit, withdraw — and an abstract class can have an abstract method as well, which is compulsory to implement in the child classes ... once we have made this class as an abstract class ... I need to take a reference of the derived class ... so if I am writing BankAccount ba = new CheckingAccount(...) so it will take a reference of the checking account and it will call the deduct fee of the checking account ... if I replace it with SavingsAccount so it will take me to deduct fee of SavingsAccount."

This unites inheritance, overriding, upcasting, dynamic dispatch, and abstraction: the abstract parent declares the contract, concrete children fulfill it, and a uniform parent reference invokes the correct child behavior at runtime.

Scope: Making BankAccount abstract means direct new BankAccount(...) is now illegal. Only new SavingsAccount / new CheckingAccount via an abstract-typed parent reference is allowed — exactly the intended restriction.

Decision guide — when to use which fix

Situation Use empty method? Use abstract method?
Some children legitimately need no fee logic Tempting, but fragile — forgetting to override is silent Preferred: child implements explicit "do nothing" with a comment and the compiler still checks presence
Every child must have a meaningful fee rule Never — no enforcement Yes — enforcement by language

Conclusion preserved from the session: the empty-method workaround compiles but enforces nothing; the abstract contract is the structurally sound solution.

Quick compliance check: Remove deductFee from SavingsAccount after the abstract parent change — compilation fails with "SavingsAccount is not abstract and does not override abstract method deductFee()". Re-adding the method restores compilation.

20.11.6 Student Questions and Answers

Q: Why not just leave deductFee empty in BankAccount instead of making it abstract?

A: An empty method compiles but enforces nothing — a child that silently inherits the no-op skips fee handling with no diagnostic, and the bug surfaces only at runtime as missing charges. Declaring deductFee abstract forces every concrete child to supply a real definition; omitting it becomes a compile-time error, guaranteeing the contract is honored. That enforcement is the structural advantage the session identifies.

Why the doubt is plausible: An empty parent implementation seems like the smallest change that fixes compilation. The abstract route seems heavier until you consider a team or product family where silent no-ops cause financial correctness bugs — catching them at compile time justifies the stricter declaration.

Recap + Bridge: Abstraction splits "declare" from "define": the abstract parent cannot be instantiated but prescribes methods; each concrete child must define them; upcasting plus dynamic dispatch then selects the child's definition at runtime. Applied to the bank family, this lifts the ba.deductFee() driver from a compile error to a polymorphic, compiler-checked product rule — the closing synthesis of inheritance, overriding, upcasting, dispatch, and contract enforcement for this lecture.

Exam Guidance Summary

No explicit mark distribution, question pattern or "this will not be on the exam" statement was voiced in this session. No textbook chapter numbers were cited. The following guidance is inferred from emphasis signals in the content:

  • Type conversion versus promotion is a core exam topic — understand widening chain , explicit cast syntax , and the compiler error for lossy conversion without a cast. Trace mixed-type expression promotion to the widest type.
  • Inheritance fundamentals are heavily emphasized — define parent/super/base versus child/sub/derived, use extends, use super(color) as first line of child constructor, use super.method() to call overridden parent methods, distinguish normal methods from overridden ones.
  • Be ready to write and trace Shape–Rectangle–Triangle code: color via super, toString with super.toString(), getArea overridden as and , and driver dispatch showing which method runs.
  • Types of inheritance taxonomy: single, hierarchical, multilevel are valid in Java; multiple inheritance for classes is not allowed — class C extends A, B is illegal.
  • Constructor and super.super rules are flagged as likely theory or output-prediction questions: calling a constructor outside another constructor's first line is illegal; super.super.print() is illegal — achieve grandparent reach via chained super.print() calls.
  • BankAccount family is a comprehensive integration question: BankAccount with deposit/withdraw/getBalance, SavingsAccount with interest via getBalance()*interest/100 and deposit, CheckingAccount with transaction counting, fee formula with free limit and fee , and overridden deposit/withdraw that increment count then delegate via super.
  • final keyword distinctions: variable constant versus method non-overridable versus class non-inheritable; blank final assigned once in constructor; static blank final assigned once in static block.
  • Runtime polymorphism and dynamic method dispatch via upcasting such as Parent obj = new Child(); obj.show(); resolves to Child's method at runtime — look to the right-hand object type.
  • Abstract class versus abstract method: abstract class cannot be instantiated but can have references, data members, constructors and concrete methods; abstract method has no body and must be overridden in concrete children; use abstract contract to solve the ba.deductFee() driver problem versus empty method workaround.

Exam note: The session closes with a 7–8 minute break mid-way and high repetition of core definitions, indicating these topics are considered essential. Present any numeric computation or table filling in full, keep intermediate steps exact as computed, and state assumptions if needed.

Key Industry Applications

  • Real-world: Mixed numeric APIs rely on widening and promotion — for example a byte sensor reading accumulated in a long counter or a float factor combined with a double price yields a double result; explicit casting is needed when storing a computed int into a char pixel buffer or protocol field.
  • Real-world: Inheritance hierarchies such as Shape with Rectangle and Triangle mirror domain modeling in graphics frameworks, game engines and CAD systems where a common parent declares getArea() or draw() and each concrete shape overrides with its own formula.
  • Real-world: BankAccount with SavingsAccount and CheckingAccount models real banking product families — interest accrual in savings via amount * rate / 100 and transaction-fee handling in checking with free-operation thresholds and per-operation charges are direct business rules implemented through overridden deposit/withdraw and fee deduction.
  • Real-world: The final keyword enforces production constraints — constant configuration values as final variables, stable core methods sealed as final to prevent accidental overriding, and security-sensitive classes such as string or utility classes sealed as final to prevent inheritance.
  • Real-world: Runtime polymorphism and dynamic dispatch power plug-in architectures and frameworks where a framework holds a Shape or BankAccount reference but executes the correct concrete behavior at runtime without recompiling callers.
  • Real-world: Abstract classes structure team development at scale — a lead publishes an abstract API with abstract methods that every team module must implement, guaranteeing structural symmetry across dozens of components and turning missing implementations into immediate compile errors rather than runtime failures.

OODAP Lecture 20 notes · Inheritance, Type Conversion and Abstract Classes in Java

Object Oriented Design, Analysis and Programming Architecture· postgraduate· 2026-08-19

Sections Breakdown

120.1 Primitive Types and Type Compatibility

Fixed Java primitive widths fix signed and unsigned ranges via 2^n, establishing directional compatibility.

220.2 Type Conversion: Widening and Narrowing

Widening small-to-large is automatic along byte->short->int->long->float->double; large-to-small needs explicit (type) cast, illustrated with char-int and byte-int.

320.3 Type Promotion in Expressions

In multi-operand expressions byte/short/char first promote to int, then to long/float/double; the widest type wins, yielding double for mixed byte-char-short-int-float-double.

420.4 Inheritance — Core Idea, Superclass and Subclass

Inheritance via extends lets a child acquire parent fields/methods in three categories (constructors via super(args), overridden via super.method, and normal direct) with two super roles.

520.5 Worked Example: Shape, Rectangle and Triangle

Shape defines color via super(color) and stub getArea; Rectangle and Triangle extend it, reuse super.toString, and override getArea as length*width and 0.5*base*height.

620.6 Types of Inheritance in Java

Single, hierarchical, and multilevel inheritance are valid via single-parent extends; multiple inheritance (class C extends A,B) is illegal and rejected at compile time.

720.7 Constructor Invocation Rules and the Grandparent Access Question

super(...) or this(...) only as first line of a constructor; Grandparent->Parent->Child print chain uses transitive single super calls; super.super is illegal.

820.8 BankAccount Family: SavingsAccount and CheckingAccount With Overriding

BankAccount provides getBalance/deposit/withdraw; SavingsAccount adds interest via getBalance*interest/100 and deposit; CheckingAccount overrides deposit/withdraw to count and deducts fee (count-free)*25.

920.9 The final Keyword — Variables, Methods, Classes, Blank and Static Blank Finals

final seals value (variable), behavior (method no override), or family (class no extend); blank final assigned once in constructor, static blank final once in static block (e.g., 50).

1020.10 Runtime Polymorphism and Dynamic Method Dispatch (Upcasting)

Upcasting (Parent obj = new Child()) plus overridden show/getArea enables runtime polymorphism; dispatch looks at right-hand object type, not declared reference.

1120.11 Abstract Classes and Abstract Methods — Enforcing Hierarchy Symmetry

Abstract class cannot be new but can be referenced; abstract method declares without body and compels concrete children to define it; BankAccount abstract deductFee enforces contract versus empty method.

12Exam Guidance Summary

Consolidated exam emphasis without mark distribution: conversion/promotion, extends/super, shape/bank traces, inheritance types, super rules, final, upcasting, abstract contracts.

13Key Industry Applications

Industry mapping: numeric promotion in sensor/I/O, graphics hierarchies, banking product families, final seals, plug-in via dispatch, abstract team APIs.

Postgraduate students in Object Oriented Design, Analysis and Programming

Exam Revision Notes

Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.

Primitive Types and Type Compatibility

Must-know: Each primitive has fixed width and range [ -2^{n-1}, 2^{n-1}-1 ] signed or [0,2^{n}-1] for char; size decides compatibility direction.

⚠️ Top pitfall: Treating char as signed or thinking boolean participates in numeric promotion.

Self-check: Why can byte widen to int automatically but int cannot narrow to byte without a cast?

Connects to: 20.2, 20.3

Type Conversion: Widening and Narrowing

Must-know: Widening is automatic when dest width >= source width; narrowing needs (type) cast and may truncate; compiler errors on possible lossy conversion check types not values.

⚠️ Top pitfall: Thinking a fitting literal value removes the need for a narrowing cast, or assuming char->short widens.

Self-check: Is char CH = num where num is int legal without a cast? Why?

Connects to: 20.1, 20.3

Type Promotion in Expressions

Must-know: All byte/short/char -> int first; if any operand is double then double, else float -> float, else long -> long, else int.

⚠️ Top pitfall: Expecting byte+byte to stay byte and forgetting byte b = b*2 needs a cast.

Self-check: What is the type of (f*b)+(i/c)-(d*s) with byte, char, short, int, float, double and why?

Connects to: 20.2, 20.4

Inheritance — Core Idea, Superclass and Subclass

Must-know: Child has own members plus accessible parent members; three parent-method categories and two super forms (constructor first-line vs dotted method).

⚠️ Top pitfall: Placing super(args) anywhere but first line of a constructor, or assuming super reaches grandparent.

Self-check: What are the three parent-method categories from the child viewpoint and how is each accessed?

Connects to: 20.5, 20.6

Worked Example: Shape, Rectangle and Triangle

Must-know: extends + super(color) for inherited color, super.toString for reuse, getArea overridden per child with distinct formulas, driver dispatch to child version.

⚠️ Top pitfall: Omitting super.toString and thinking parent toString chains automatically, or placing super(color) not first.

Self-check: What does Rectangle S1 = new Rectangle(red,4,5) println produce via super.toString?

Connects to: 20.4, 20.7

Types of Inheritance in Java

Must-know: Only one class after extends; three allowed shapes vs forbidden multiple; alternatives are interfaces/composition.

⚠️ Top pitfall: Writing class C extends A, B or thinking siblings share members.

Self-check: Which of single/hierarchical/multilevel/multiple are allowed for Java classes and which line is illegal?

Connects to: 20.5, 20.7

Constructor Invocation Rules and the Grandparent Access Question

Must-know: Constructor super calls only inside constructor first line; super.method only reaches immediate parent; grandparent via chained super calls not super.super.

⚠️ Top pitfall: Calling super from a normal method or writing super.super.print().

Self-check: What prints for Child C = new Child(); C.print() with Grandparent->Parent->Child each calling super.print()?

Connects to: 20.5, 20.8

BankAccount Family: SavingsAccount and CheckingAccount With Overriding

Must-know: Savings: super(accNo,name,amount) + addInterest via getBalance/deposit; Checking: overridden deposit/withdraw increment then super, deductFee = (count-free)*25.

⚠️ Top pitfall: Manipulating amount directly instead of via super methods, or incrementing count after delegation.

Self-check: Trace SavingsAccount 5000+1000 interest 9 percent withdraw 6000 and CheckingAccount 3 transactions fee.

Connects to: 20.5, 20.11

The final Keyword — Variables, Methods, Classes, Blank and Static Blank Finals

Must-know: final variable constant, final method cannot be overridden, final class cannot be extended; blank final once in constructor, static blank final once in static block.

⚠️ Top pitfall: Confusing final reference with immutable object, or expecting final class to imply immutable instances.

Self-check: Where may a blank final be assigned and where may a static blank final be assigned?

Connects to: 20.6, 20.11

Runtime Polymorphism and Dynamic Method Dispatch (Upcasting)

Must-know: Polymorphism = one name multiple forms via overriding; upcasting enables dynamic dispatch; obj.show() runs Child version because object is Child.

⚠️ Top pitfall: Thinking left declared type decides which overridden implementation runs, or expecting fields to be polymorphic.

Self-check: Parent obj = new Child(); obj.show() — which show runs and why?

Connects to: 20.5, 20.11

Abstract Classes and Abstract Methods — Enforcing Hierarchy Symmetry

Must-know: Abstract class: no new but references allowed; abstract method: no body must be overridden; abstract BankAccount with abstract deductFee solves ba.deductFee driver.

⚠️ Top pitfall: Giving abstract method an empty body {} or leaving a child without override and trying to instantiate it.

Self-check: Why is abstract deductFee preferred over empty deductFee in BankAccount?

Connects to: 20.8, 20.10

Was this lecture useful?

Loading comments…
🤖

BitsNotes AI Assistant

Subject Notes Assistant

Configure AI Chat

Choose how to access the chatbot
Have your own API key?

Switch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.

🔑 Enter API key above to fetch live models from provider, or enter model name manually.
OpenAI-Compatible API Support

Choose any provider preset (Gemini, DeepSeek, Kimi, GLM, MiniMax, Qwen, OpenAI, Groq, Ollama, etc.) or enter a custom endpoint URL.

Security & Privacy First

Your API key is sent directly from your browser to your specified provider. BitsNotes servers never store or see your key.