Polymorphism in Java is the ability of one method call to run different code depending on the object or the arguments it gets. The most common form is a variable of a supertype, such as Ride, that holds a subclass object, such as CarRide, and calls the subclass version of a method at runtime.
We use polymorphism to write code once against a general type and let each class bring its own behavior. A ride-hailing app can compute fares for cars and bikes in one loop, and the loop never checks which kind of ride it has.
The following example stores two different rides in variables of the same type and calls the same method on both.
class Ride {
int fare(int km) {
return 200 + km * 100; // cents
}
}
class CarRide extends Ride {
@Override
int fare(int km) {
return 300 + km * 150;
}
}
class BikeRide extends Ride {
@Override
int fare(int km) {
return 100 + km * 50;
}
}
Ride first = new CarRide();
Ride second = new BikeRide();
int carFare = first.fare(10); // 1800
int bikeFare = second.fare(10); // 600
Both variables have the type Ride, yet each call runs the method of the real object. From here, we go through the two kinds of polymorphism in Java, the overload rules the compiler uses, what happens at runtime, the members that are not polymorphic, and pattern matching over sealed types as a modern alternative to overriding.
1. Compile-Time and Runtime Polymorphism
Java has two kinds of polymorphism, and the difference is when the method gets chosen. With overloading, the compiler picks one of several methods with the same name by looking at the argument types. With overriding, the JVM picks the method at runtime by looking at the class of the object. The runtime form is also called virtual method invocation.

| Compile-time polymorphism | Runtime polymorphism | |
|---|---|---|
| Java feature | Method overloading | Method overriding |
| Decided by | The compiler, from the declared argument types | The JVM, from the class of the object |
| Also called | Static binding, early binding | Dynamic binding, dynamic dispatch |
| Needs inheritance | No, overloads live in one class | Yes, a subclass or an implementing class |
| Signature | Same name, different parameter lists | Same name and same parameter list |
Both kinds rely on the same idea that one name stands for several implementations. The overloading vs overriding article compares the rules in more detail, including return types and exceptions.
2. Overloading and How the Compiler Picks a Method
Overloading means declaring several methods with the same name and different parameter lists in one class. A receipt printer for the ride app can accept an amount in cents, an amount plus a currency, or a list of amounts, and every caller uses the same method name.
static String receipt(int cents) {
return receipt(cents, "USD");
}
static String receipt(int cents, String currency) {
return currency + " " + cents / 100 + "." + String.format("%02d", cents % 100);
}
static String receipt(List<Integer> items) {
int total = 0;
for (int item : items) {
total += item;
}
return receipt(total);
}
String single = receipt(1850); // "USD 18.50"
String euros = receipt(600, "EUR"); // "EUR 6.00"
String trip = receipt(List.of(1800, 250)); // "USD 20.50"
When more than one overload could accept the arguments, the compiler searches in three phases defined in JLS 15.12.2. It first looks for a match without boxing or varargs, which allows widening such as int to long. If none fits, it allows boxing and unboxing, and only after that does it consider varargs methods.
static String kind(long value) {
return "long";
}
static String kind(Integer value) {
return "Integer";
}
static String kind(Object value) {
return "Object";
}
String widened = kind(7); // "long"
String boxed = kind(Integer.valueOf(7)); // "Integer"
Object declaredAsObject = 7;
String byStaticType = kind(declaredAsObject); // "Object"
Overload selection uses the declared type of the argument, not the class of the object, so kind(declaredAsObject) picks kind(Object) even though the value is an Integer. The plain int 7 prefers widening to long over boxing to Integer, because widening happens in the first phase.
3. Overriding and Dynamic Dispatch
Overriding means that a subclass declares a method with the same name and parameter list as a method it inherits. When code calls the method through a supertype variable, the JVM looks up the class of the object at runtime and runs the most specific version, as described in JLS 15.12.4.4. The Java inheritance guide covers how subclasses get these methods in the first place.
The ride-hailing app computes the total of a shared trip by asking each ride for its fare. When the company adds scooters next quarter, the team writes a ScooterRide class with its own fare(), and the total calculation stays untouched.
class ScooterRide extends Ride {
@Override
int fare(int km) {
return 150 + km * 80;
}
}
List<Ride> rides = List.of(new CarRide(), new BikeRide(), new ScooterRide(), new Ride());
int total = rides.stream().mapToInt(ride -> ride.fare(10)).sum(); // 4550
An overriding method has to follow a few rules, and the @Override annotation makes the compiler check them.
- The parameter list must be the same. A different parameter list creates an overload instead, which is the most common bug that @Override catches.
- The return type must be the same or a subtype, which is called a covariant return type.
- The access cannot be weaker, so a public method cannot become protected in the subclass.
- The method cannot throw broader checked exceptions than the method it overrides.
- Methods that are static, private or final cannot be overridden.
4. Polymorphism With Interfaces and Lambdas
Runtime polymorphism does not need a class hierarchy. Any interface gives us the same dispatch, and a functional interface lets us pass the behavior as a lambda. Price rules in the ride app, such as surge pricing at night, fit this shape well because each rule is a single calculation.
interface PriceRule {
int apply(int cents);
}
PriceRule surge = cents -> cents * 3 / 2;
PriceRule airportFee = cents -> cents + 500;
int night = surge.apply(1800); // 2700
int airport = airportFee.apply(1800); // 2300
Each lambda is an object of a class the JVM generates, and the call rule.apply() dispatches to it like any overridden method. A list of PriceRule objects can be applied in a loop, which is the strategy pattern without extra classes.
5. Fields and Static Methods Are Not Polymorphic
Only instance methods take part in dynamic dispatch. A field with the same name in a subclass hides the parent field, and a static method with the same signature hides the parent method. In both cases, Java picks the member from the declared type of the variable, not from the object.
class Fleet {
String name = "fleet";
static String category() {
return "general";
}
String label() {
return "fleet label";
}
}
class TaxiFleet extends Fleet {
String name = "taxi";
static String category() {
return "taxi";
}
@Override
String label() {
return "taxi label";
}
}
Fleet fleet = new TaxiFleet();
String fieldValue = fleet.name; // "fleet"
String staticValue = fleet.category(); // "general"
String methodValue = fleet.label(); // "taxi label"
Only label() follows the object. Calling a static method through an instance compiles, but IDEs flag it because it misleads readers, so we write Fleet.category() instead. The method hiding article explains static method hiding in more depth, and the rule for fields is in JLS 15.11.1.
6. Pattern Matching Over Sealed Types
Since Java 21, a switch can match on types (JEP 441). Combined with a sealed interface and records, it gives us a second way to run different code per type. Here the logic lives in one method instead of being spread across the classes.
sealed interface Discount permits PercentOff, FlatOff, NoDiscount {
}
record PercentOff(int percent) implements Discount {
}
record FlatOff(int cents) implements Discount {
}
record NoDiscount() implements Discount {
}
static int discounted(int cents, Discount discount) {
return switch (discount) {
case PercentOff p -> cents - cents * p.percent() / 100;
case FlatOff f -> Math.max(0, cents - f.cents());
case NoDiscount n -> cents;
};
}
int tenPercent = discounted(1800, new PercentOff(10)); // 1620
int flat = discounted(1800, new FlatOff(500)); // 1300
int capped = discounted(1800, new FlatOff(5000)); // 0
The two styles suit different situations. Overriding works best when new types appear often and each type owns its behavior, like new ride types. A switch over a sealed type works best when the set of types is fixed and new operations appear often, like discounts that billing and reporting each treat differently. The compiler rejects the switch if a permitted type is missing, so adding a fourth discount record shows every place to update.
7. Polymorphism in Java FAQs
A call that runs a different method than the developer expected is where these questions start, in code reviews as well as in interviews.
7.1. Can we override static methods in Java?
No. A static method with the same signature in a subclass hides the parent method, and the call is resolved from the declared type at compile time, as the Fleet example in section 5 shows.
7.2. Does Java support operator overloading?
Not for our own types. The language overloads a few operators itself, such as + for numbers and for String concatenation, but we cannot define operators for our classes. Classes such as BigDecimal use methods like add() instead.
7.3. Can private or final methods be overridden?
No. A private method is not inherited, so a subclass method with the same name is a new method. A final method cannot be overridden, and the compiler reports an error if a subclass tries.
7.4. Is method overloading a form of polymorphism?
Yes, it is called ad hoc or compile-time polymorphism. Many developers mean only overriding when they say polymorphism, because overriding is the form that changes behavior at runtime.
7.5. Are constructors polymorphic?
No. Constructors are not inherited, so they cannot be overridden. They can be overloaded within a class, and a subclass constructor calls one of the parent constructors with super(…).
8. Conclusion
Polymorphism lets one call run different code. Overloading is resolved by the compiler from declared argument types, and overriding is resolved by the JVM from the class of the object. Fields and static methods are never dispatched at runtime, and neither are private methods.
In practice, we use overriding and interfaces when new types keep arriving, and a switch over sealed records when the types are fixed and the operations keep changing. Both approaches remove the long if–instanceof chains that older code used, and both are part of the OOP principles in Java.
9. References
- Java Tutorials, Polymorphism
- JLS 25, Compile-Time Step 2: Determine Method Signature
- JLS 25, Locate Method to Invoke
- JLS 25, Inheritance, Overriding, and Hiding
- JEP 441, Pattern Matching for switch
Happy Learning !!
class Animal { int id=10; public void makeNoise() { System.out.println("Some sound"); } } class Dog extends Animal{ int id=20; public void makeNoise() { System.out.println("Bark"); } } class Cat extends Animal{ int id=30; public void makeNoise() { System.out.println("Meawoo"); } } public class OverridingClassConcepts { public static void main(String[] args) { // TODO Auto-generated method stub Animal a1 = new Cat(); a1.makeNoise(); //Prints Meowoo System.out.println(a1.id); // prints 10 // reference type is Animal and object Cat class so its called Cat class method but why its calling Animal class variable.. // why its printing Animal class variable and Catclass method. Animal a2 = new Dog(); a2.makeNoise(); //Prints Bark System.out.println(a2.id); // print 10 //reference type is Animal and object Dog class so its called Dog class method but why its calling Animal class variable.. // why its printing Animal class variable and Dog class method. } }I understand its not completely related to polymorphism concept but please solve my problem
I think you have not gone through the Inheritance concept properly. If you go through the point 3-Accessing inherited parent class members.
“java fields cannot be overridden”. Having same name field will hide the field from parent class – while accessing via child class. Attribute accessed will be decided based on the class of reference type.
But in the case of methods access its opposite to field access. Method access uses the type of actual object created in Run Time.
Hi Pankaj,
To add few things
change the return type or access specifier/modifier in method overloading is not allowed and leads to compiler time error i hope but the same works in method overriding some time called as covariant return type.
overriding with different return type and access specifer
Object Class
protected native Object clone() throws CloneNotSupportedException{
}
User Class
public User clone(){
}
Hi,
Overriding a static method is some time also refer as masking or hiding
can u please explain more about the above points ?
if you write main method in public class Car from there call main method of mini which is non public.????
always it should call the method which object is created right ?? but why here the parent method object is called..???
Consider the below example
public class Parent {
public static void method(){
System.out.println(“parent method is called”);
}
/**
* @param args
*/
public static void main(String[] args) {
Parent p=new Child();
p.method();
}
}
public class Child extends Parent {
public static void method(){
System.out.println(“parent method is called”);
}
}
Output is : parent method is called.
According to above explanations, it should call Child method right..?? why is it calling parent method..??
Sorry , i donno how to edit the comment in the above code…
a small correction…in the above code..
public class Child extends Parent {
public static void method(){
System.out.println(“child method is called”);
}
}
The reason is that static methods are always called for reference type, and not from actual instance. If you look at eclipse warning (“a yellow triangle”), it also hints “The static method method() from the type Parent should be accessed in a static way”.
Also Saurabh pointed out that “static method are not available for method overriding.It is part of class rather than object. But you can always overload Static method.”
Actually there is no difference between below both statements:
1) p.method();
2) Parent.method(); //Both are same
public class Parent
{
public static void method()
{
System.out.println("parent method is called");
}
public void method1()
{
System.out.println("parent method1 is called");
}
public static void main(String[] args)
{
Parent p = new Child();
p.method();
Parent.method();
p.method1(); //A call to non-static method will call method from actual instance
}
}
public class Child extends Parent
{
public static void method()
{
System.out.println("child method is called");
}
public void method1()
{
System.out.println("child method1 is called");
}
}
Let me know if there are more confusions.
Lokesh,
Wondering why someone will create Animal class as per your example. if Amimal class has no use at all. better it should be interface then a Class to force concreat class to implement
or the same method of base class gives different output would have been better example.
I respectfully disagree. As a general rule, you should create classes for nouns and interfaces for verbs i.e. actions. So, actually Animal should be a class with default behavior which all animals will have e.g. that all can move, make noise, eat and many things like that. There is no need to override every behavior for all animals. It will be code duplication. Each animal can override it’s specific behavior only. With interface it is not possible.
Interface which can make sense are Walkable, Swimmable or Fyable which a group of animals can implement based on needs. But still they waill have all default behavior from Animal.class.
Hi Lokesh,
Why JVM call always Overloaded method if i pass null ?
class MyClass {
public void test(Object o) {
System.out.println(“From() method “);
}
public void test(String o) {
System.out.println(“From Overloaded test() method “);
}
}
public class Demo {
public static void main(String[] args) {
MyClass m = new MyClass();
m.test(null);
}
}
O/P= From Overloaded test() method
While resolving the overloaded method to call, JVM tries to find method with most specialized type. NULL can be referred from spring as well as object type variables. But, String is more specialized. So, JVM chooses to call “public void test(String o)”.
I will recommend reading following thread: https://stackoverflow.com/questions/13041042/using-null-in-overloaded-methods-in-java
Hi Praveen,
More fun happens when you also add this
public void test(Integer o) {
System.out.println(“From() method “);
}
Why method overloading happens at compile time only not at run time?please reply
At compile time, JVM generates the bytecode for a program i.e. your class. This bytecode is essentially a sequence of operations. These operations are in low level languages and are executed in sequence.
In case overloading, JVM is able to and it determines which method call it needs to make for a certain operation, so it generates the bytecode equivalent to method call in compile time itself.
In case of overriding, it is unable to determine this so it generates code with certain conditional keywords which determine the actual method call when bytecode runs on machine.
I Guess Nitya’s question is “Why method overloading happens at run time only why not at compile time “?
Compiler dose not know which method is called at compilation time, because Objects are created at the time of Running program. So JVM knows based on the object creation, which method has to call at the time of running the program..
NO. Nitya is right. Overloading is resolved at compile time itself. Overriding is resolved on runtime because actual instance is resolved in runtime time only. Remember, in java super class reference can point to instances of child classes also. Actual instance is resolved at runtime only.