Inheritance, Type Conversion and Abstract Classes in Java
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
- Confusing
charas tinyint:charis 16-bit unsigned. It cannot hold negative values, andshortcannot hold allcharvalues above . Both need a cast in opposite directions. - Thinking
booleanparticipates in widening:booleanis incompatible with every numeric type; even(int) trueis illegal. - 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 = bwherebisbyte— safe — versusbyte b = x— potentially unsafe. - Arithmetic operands:
char + intmust choose one type for the addition. - Method arguments: passing a
shortto a method expectingintversus 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) andlong(64-bit) are the widest primitives;byteis narrowest numeric;booleanis a separate logical domain.
Sense-check: is exactly the byte range size ; the formula counts exactly.
Pitfalls
- Off-by-one on the upper bound: Signed max is , not , because zero consumes one pattern.
- Treating
charas signed:charrange starts at 0; is not achar.
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:
- The two types are compatible (numeric-to-numeric, or
charto numeric integer). - 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
- Assuming widening keeps exact precision to float/double: It keeps range but not necessarily low-order bits.
- Thinking
char -> shortwidens: It does not;charmaxshortmax , andshortmin is belowcharmin .
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
- 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.
- Silent data loss after casting:
int 257cast tobytebecomes1(256 wrapped), and largeintcast tocharwraps modulo . - Writing
(char)numwith a space confusion: Parentheses must enclose the type, not the value:(char) numis 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
byteorshortreading (e.g., 8-bit sensor) is accumulated into alongcounter ordoubleprice; widening is free. - Graphics and I/O: A computed
intcoordinate or file size must be narrowed tochar/bytefor a compact buffer or wire protocol; the cast documents the risk and forces range checking. - Financial code: Monetary cents in
intare widened todoublefor 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:
- If any operand is
double, the other is promoted todouble, result isdouble. - Else if any operand is
float, the other is promoted tofloat, result isfloat. - Else if any operand is
long, the other is promoted tolong, result islong. - Else both operands are promoted to
int(sobyte,short,charbecomeint), result isint.
Equivalently, the final result type of an expression is the maximum among operand types in the order
So:
- Any
doublepresent → whole expression isdouble. - Else any
float→float. - Else any
long→long. - Else →
int(since byte/char/short have already been widened toint).
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 toint→ result isint(even though values might fit inshort).int + long→intpromoted tolong→ result islong.float + long→longpromoted tofloat→ result isfloat.byte * 2→bytepromoted toint, literal2isint→ result isint; storing back needsb = (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
- Expecting
byte + byteto staybyte: It becomesint. - Forgetting the final cast back:
byte c = a * bis 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
- Storing promoted result back without a cast:
byte b = b * 2fails even though2fits; the product isint. - Integer division surprise after promotion:
i / cwhere both are promoted tointperforms integer division (truncated), even if you later assign todouble; cast one operand todoublefirst 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;
privatemembers 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
- 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.
- 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).
- 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)inRectangle(String color, ...)callsShape(String color)to initializethis.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()insideRectangle.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
- Calling
super(args)outside a constructor's first line: Illegal. - Assuming
superreaches any ancestor: It always means immediate parent; grandparent needs chainedsupercalls level by level (Section 20.7). - 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
}
}
colorbelongs toShapeand is set viathis.color = colorin the one-argument constructor.toString()is the standard object-to-string conversion, overridden to return"Shape of color "concatenated withcolor. It provides ready-made formatting that children can reuse viasuper.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 Shapedeclares inheritance.- Constructor takes three arguments.
super(color)forwards the shared part toShape(String)to initialize the inheritedcolor;lengthandwidthare 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 thensuper.toString()to append the parent's color string. Thesuper.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):
super("red")executesShapeconstructor:this.color = "red".this.length = 4,this.width = 5.toString()builds:"Rectangle of length 4.0 and width 5.0, subclass of Shape of color red"(becausesuper.toString()returns"Shape of color red").getArea()computes , returns asdouble.
Sense-check: matches the driver output discussed later.
Pitfalls
- Placing
super(color)after field assignments: Compile error — it must be first. - Writing
toString()withoutsuper.: You lose the parent's color contribution or create infinite recursion if you calltoString()instead ofsuper.toString(). - 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):
super("blue")→color = "blue".base = 3,height = 6.toString()→"Triangle of base 3.0 and height 6.0, subclass of Shape of color blue".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:
S1constructed with red, 4, 5 →Shapepart holds"red".System.out.println(S1)is equivalent toSystem.out.println(S1.toString()). SinceS1's dynamic type isRectangle,Rectangle.toString()runs. Inside it,super.toString()fetches"Shape of color red"fromShape, so output isRectangle of length 4.0 and width 5.0, subclass of Shape of color red(formatting of doubles may vary).S1.getArea()resolves toRectangle.getArea()→ , prints20.0.S2asTrianglefollows the same path:toStringprints 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
- Thinking
printlnautomatically chains parent toString: It does not; you must explicitly concatenatesuper.toString()inside the child's implementation. - 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
- Typing
class C extends A, Bbecause it reads naturally: Java requires separate interface paths. - Thinking sibling classes share each other's members:
RectangleandTriangledo not inherit from each other; they only share via the common parentShape. - 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(...)orsuper(...). - 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(...)orthis(...).
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:
C.print()entersChild.print(). First statementsuper.print()transfers control toParent.print().Parent.print()first statementsuper.print()transfers toGrandparent.print().Grandparent.print()printsGrandparent's print, then returns.- Control back in
Parent.print()continues toSystem.out.println("Parent's print"). - Control back in
Child.print()continues toSystem.out.println("Child's print").
Output produced:
Grandparent's print
Parent's print
Child's print
The key observation: Child reached Grandparent's code transitively — Child -> 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
- Reading
superas a variable chain: It is notthis.super; you cannot dot a second time. - Forgetting to implement
super.print()at the middle level: ThenChild'ssuper.print()reaches onlyParentand 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,amountdescribe any account. Constructor sets all three from supplied values. setAccount/setNameassign explicitly from parameters tothis.fields.getBalance()simply returns the currentamount— the single read path children will reuse.depositaddsamttoamountand stores back; futuregetBalancesees the new sum.withdrawfirst checksamount < amt. If true, insufficient funds; otherwiseamount -= 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
interestholds the savings rate; example uses . - Constructor takes four parameters. The first three are delegated via
super(accNo, name, amount)toBankAccount'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 fieldinterestis then assigned. addInterestreuses parent methods intentionally: it callsgetBalance()(defined inBankAccount) to read currentamount, 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:
- Construction:
super("111","Ankit",5000)→amount = 5000;interest = 9. getBalance()→ .deposit(1000): .addInterest(): .deposit(540): .withdraw(6000): , so .- 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
- Manipulating
amountdirectly instead of viadeposit/getBalance: You lose the indirection that would later enforce fees or logging. - Forgetting that
interestis a percent, not a fraction: The division by is essential; using directly is equivalent but must not be combined with another division by 100. - Interest integer truncation: With
intarithmetic, is exact, but other rates like with small balances may truncate if done in integers —doublepreserves 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 beyondfreeTransactions = 2;transactionCount` tracks deposits/withdrawals. - Constructor again delegates via
super(accNo, name, amount). deductFeecheckstransactionCount > 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."
depositandwithdraware overridden. In each,transactionCountis incremented first (the operation itself is a counted transaction), then the parent's version is invoked viasuper.deposit(amt)orsuper.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.depositand 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:
- Construct:
amount = 5000, counters zero. deposit(1000):transactionCount→1,super.deposit(1000)→ .withdraw(2000):transactionCount→2,super.withdraw(2000)→ (still free limit, sodeductFeewould be zero if called here).deposit(6000):transactionCount→3, .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:
@Overrideondeposit/withdrawcatches signature typos — omitting it lets a misspelleddepositesilently become an overload rather than an override. - Incrementing after
super: Reversing the order (increment aftersuper.deposit) would still count but obscures that the count belongs to the attempted transaction even ifwithdrawlater fails due to insufficient funds — incrementing first matches the session's logic. - Fee via
withdrawinherits guard: Callingwithdraw(fee)reuses the "insufficient funds" check, so fees are not charged against an empty account without a message — an accidental directamount -= feewould 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 Abecomes 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
- Forgetting to assign in every constructor: If one constructor omits the assignment, that path fails to compile.
- Confusing
finalreference 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 IinFirst: each object assigns once in its constructor.static final int datainA: 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
finalfields 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 notesStringis 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
- Marking a method
finalto "make it faster": Modern JVMs inline regardless; usefinalfor design intent, not performance mythology. - Assuming
final classmakes instances immutable: It only seals the hierarchy; instance fields still mutate unless themselves declaredfinal.
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
@Overrideis 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 objectrefactually 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:
Parent obj = new Child()— left side declared type isParent; right side object type isChild. The assignment is an upcast and is legal because a Child is-a Parent.- At
obj.show(), the question is: whichshowruns? The session states: look to the right-hand side — the actual object type. Sincenew Child()created aChild,Child.show()executes, printingChild'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
- 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.
- Expecting fields to be polymorphic:
Parent p = new Child(); System.out.println(p.field)readsParent's field ifChildhides it. - Calling
super.*to "force" parent dispatch externally: External code cannot bypass dispatch; only code inside the child can usesuper.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 writeABC a = new ABC()— this is not possible ... that's why it cannot be used to create objects." - References are allowed:
ABC refis 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
- Providing a body for an abstract method in the abstract class: Empty braces
{}make it concrete, not abstract, losing the enforcement. - 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
AbstractAdeclaresgetDataandshowDataas 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."DerivedAsupplies both:getDatareads fromScannerintoa;showDataprintsa.- 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:
SavingsAccountdefinesdeductFeewith savings-appropriate logic (perhaps no fee or a different rule) — a definition must exist.CheckingAccountdefinesdeductFeeas 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, usesuper(color)as first line of child constructor, usesuper.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,toStringwithsuper.toString(),getAreaoverridden 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, Bis illegal.
- Constructor and
super.superrules 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 chainedsuper.print()calls.
- BankAccount family is a comprehensive integration question: BankAccount with deposit/withdraw/getBalance, SavingsAccount with interest via
getBalance()*interest/100anddeposit, CheckingAccount with transaction counting, fee formula with free limit and fee , and overridden deposit/withdraw that increment count then delegate viasuper.
- 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()ordraw()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 / 100and 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
finalkeyword enforces production constraints — constant configuration values asfinalvariables, stable core methods sealed asfinalto prevent accidental overriding, and security-sensitive classes such as string or utility classes sealed asfinalto prevent inheritance.
- Real-world: Runtime polymorphism and dynamic dispatch power plug-in architectures and frameworks where a framework holds a
ShapeorBankAccountreference 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
Sections Breakdown
Fixed Java primitive widths fix signed and unsigned ranges via 2^n, establishing directional compatibility.
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.
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.
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.
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.
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.
super(...) or this(...) only as first line of a constructor; Grandparent->Parent->Child print chain uses transitive single super calls; super.super is illegal.
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.
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).
Upcasting (Parent obj = new Child()) plus overridden show/getArea enables runtime polymorphism; dispatch looks at right-hand object type, not declared reference.
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.
Consolidated exam emphasis without mark distribution: conversion/promotion, extends/super, shape/bank traces, inheritance types, super rules, final, upcasting, abstract contracts.
Industry mapping: numeric promotion in sensor/I/O, graphics hierarchies, banking product families, final seals, plug-in via dispatch, abstract team APIs.
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?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.