Many learners can repeat that a class is a blueprint and inheritance is an is-a relationship, yet still struggle to tell which object owns a value or which method runs through a parent-typed reference. One Java example settles both. Two objects start from the same odometer reading of 12000, one of them declared as the parent type but built as a subclass, and the program ends by printing 1600 and 1220. Reaching those two numbers means separating class from object, object from reference, and declared type from runtime type. Java is one of the tracks inside the Coding & Skill Development Courses category.
Build the OOP mental model before reading syntax
A class is a programmer-defined type grouping state, construction rules and behaviour. An object is a runtime instance with its own state. A reference is the value used to reach an object, not the object itself. In Vehicle van = new Vehicle(12000), Vehicle is the class and declared reference type, van is the reference, and the object begins with odometerKm = 12000.
Encapsulation controls access to state through a type's boundary. Inheritance derives a more specific type from an existing type. Polymorphism lets code use one parent contract with different runtime implementations. These tools do not automatically make software reusable or maintainable; the design still does that.
Procedural decomposition instead organises work mainly around procedures and data passed between them. For that adjacent model, revise functions, arrays and pointers in C Programming for Teaching CS Exams.
Classes share a definition, objects keep independent state
Our base class Vehicle stores a protected integer named odometerKm. Its constructor assigns the supplied value. Its drive(int addedKm) method adds distance to that object's odometer, while serviceCost() returns (odometerKm / 1000) * 100. That formula is a teaching device, not a real service quotation.
Now create Vehicle van = new Vehicle(12000). A second object, built by new ElectricCar(12000, 80), is reached through the reference taxi. Both objects use code defined by Vehicle, but updating one object's odometerKm does not update the other's field. The class supplies the common definition; each object supplies its own field values.
Identity is also different from value. Two objects can both begin with odometerKm = 12000 and still be distinct objects. Java learners can continue with String Handling in Java to study references, equality, immutability and pooled strings, where those distinctions matter most.
Inheritance adds a specific type without duplicating the parent
class ElectricCar extends Vehicle adds a private integer named batteryPercent. Its constructor receives km and battery, calls super(km) first, then assigns batteryPercent = battery. It inherits drive and overrides serviceCost() with 500 + (odometerKm / 2000) * 80.
In this model, every ElectricCar is a Vehicle, so Vehicle taxi = new ElectricCar(12000, 80) is valid. The reverse does not follow. This also separates inheritance from composition: a car has a battery, but a battery is not a kind of car.
In Java, the protected odometerKm field is available to the subclass here. If the parent field were private, the subclass could reach it only through an inherited accessor method, never by naming the field. Java constructors run as a chain during object construction, but they are not inherited as ordinary methods.

Work the complete object trace and calculate both outputs
The full program follows. Every method body is visible, so each state change can be followed line by line.
class Vehicle {
protected int odometerKm;
Vehicle(int odometerKm) {
this.odometerKm = odometerKm;
}
void drive(int addedKm) {
odometerKm += addedKm;
}
int serviceCost() {
return (odometerKm / 1000) * 100;
}
}
class ElectricCar extends Vehicle {
private int batteryPercent;
ElectricCar(int km, int battery) {
super(km);
batteryPercent = battery;
}
@Override
int serviceCost() {
return 500 + (odometerKm / 2000) * 80;
}
}
public class OopTrace {
public static void main(String[] args) {
Vehicle van = new Vehicle(12000);
Vehicle taxi = new ElectricCar(12000, 80);
van.drive(4000);
taxi.drive(6000);
System.out.println(van.serviceCost());
System.out.println(taxi.serviceCost());
}
}Trace van first. It starts at 12000, then drive(4000) gives 12000 + 4000 = 16000. The separate ElectricCar object reached through taxi starts at 12000 with batteryPercent = 80. Its inherited drive(6000) changes only its inherited odometer field: 12000 + 6000 = 18000.
Now calculate each call. Because all operands are integers, Java performs integer division here; both divisions are exact for these values. van.serviceCost() uses Vehicle.serviceCost():
(16000 / 1000) * 100 = 16 * 100 = 1600
taxi.serviceCost() dispatches to the override:
500 + (18000 / 2000) * 80 = 500 + 9 * 80 = 500 + 720 = 1220
The program therefore prints 1600 and then 1220.
Reference type controls access, runtime type controls overridden dispatch
The declaration Vehicle taxi = new ElectricCar(12000, 80) raises two different questions. At compile time, the declared type Vehicle determines which member names can be requested through taxi. Therefore taxi.drive(6000) and taxi.serviceCost() are valid, but direct access to the private batteryPercent field is not. At runtime, the actual ElectricCar object determines which overridden serviceCost() body runs.
The two methods behave differently for a clear reason. ElectricCar supplies no new drive, so the inherited Vehicle.drive body changes the object's inherited odometer field. It does supply a new serviceCost, so Java's dynamic dispatch selects the override.
Overriding supplies subtype behaviour for a matching inherited signature; overloading only selects among different parameter lists, so it is not the mechanism behind taxi.serviceCost().

Fix the traps that produce wrong OOP traces
Trap | Why it fails | Correction |
|---|---|---|
Treating the reference as the object |
| Draw reference and object as two boxes, and change fields only in the object box |
Assuming | Each object holds its own copy, so | Keep one field column per object: 16000 for |
Assuming every parent member is reachable | Access modifiers still apply, so | Check the modifier and the declared type before calling a member |
Expecting constructors to be inherited |
| Read the chain as construction order, not as an inherited method |
Reading |
| Assign upwards freely; the other direction needs a cast and a runtime check |
Calling an overload an override | A different parameter list creates a new method, with no dispatch decision | Match name and parameter list exactly, as |
Resolving the call from the declared type | The declared type decides what may be called, never which body runs | Resolve overridden calls from the runtime object: 1220, not 1800 |
After both calls, van.odometerKm = 16000, not 18000. The taxi object has odometerKm = 18000 and batteryPercent = 80. It prints 1220, not 1800. The base formula gives (18000 / 1000) * 100 = 18 * 100 = 1800, but overriding selects the subtype body.
Do not use inheritance when the subtype cannot replace the parent. Use is-a for inheritance and has-a for composition, then test whether parent-typed code can accept the subtype.
How exam-style questions test classes, objects and inheritance
Practice questions ask you to identify class, object and reference, trace constructor order, calculate final fields, test an is-a assignment, separate allowed access from runtime dispatch, predict output after an override, or reject a false claim about private members and constructors.
From the example: van reaches a Vehicle; taxi is declared Vehicle but reaches an ElectricCar; taxi.drive(6000) is legal because drive belongs to the parent contract; and taxi.serviceCost() returns 1220 because the runtime object overrides it.
For rough work, box every reference declaration, write the runtime class beside each new, record fields separately for each object, update state call by call, then resolve every overridden method from the runtime object.
OOP in one minute: recap and next step
A class defines a type, an object owns state, and a reference reaches it. Inheritance expresses is-a, while overriding lets a subtype supply runtime behaviour. Here, independent objects finish at 16000 and 18000, and print 1600 and 1220.
Redraw the diagrams. Then change taxi's starting odometer to 14000: its final state is 14000 + 6000 = 20000, and its cost is 500 + (20000 / 2000) * 80 = 500 + 10 * 80 = 1300. Continue with the Java course for structured study, then use DSA using Java to apply Java in problem solving.




