Engineering/Backend

How Does Java Decide Which Method to Call?

Isaac S. Lee 2026. 7. 30. 03:40

Java allows several methods to share the same name.

A class can provide different methods for different parameter types, and a subclass can replace an inherited method with its own implementation. The first feature is called overloading. The second is called overriding.

They are often introduced with two simple definitions:

  • Overloading means using the same method name with different parameter lists.
  • Overriding means redefining a parent class method in a subclass.

Those definitions are correct, but they do not fully explain how Java decides which method actually runs.

Consider the following code:

Object message = "hello";
printer.print(message);

The object stored in message is a String, yet Java may call print(Object) instead of print(String).

Now consider another example:

Animal animal = new Dog();
animal.speak();

The variable is declared as Animal, but Java calls the implementation in Dog.

These results may appear contradictory. In reality, they come from two different decisions made at two different times.

Java method invocation is easier to understand as a two-stage process.

First, the compiler determines which method signature the call refers to. Overloading is resolved during this stage.

Then, if the selected method is an overridable instance method, the JVM determines which class implementation should execute based on the actual object at runtime. Overriding operates during this stage.

Once these two decisions are separated, overloading and overriding stop looking like two similar language features. They become two distinct mechanisms that work together to support Java’s type system and polymorphism.


Overloading and overriding are resolved at different times

Let us begin with the central distinction.

Overloading

Overloading allows multiple methods to have the same name as long as their parameter lists differ.

public class Printer {

    public void print(String value) {
        System.out.println("String: " + value);
    }

    public void print(int value) {
        System.out.println("int: " + value);
    }
}

The compiler selects the appropriate overload before the program runs.

Printer printer = new Printer();

printer.print("hello");
printer.print(10);

The first call is compiled as a call to print(String). The second becomes a call to print(int).

Overriding

Overriding occurs when a subclass provides a new implementation of an inherited instance method with the same signature.

class Animal {

    public void speak() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {

    @Override
    public void speak() {
        System.out.println("Bark");
    }
}

The following variable is declared as Animal:

Animal animal = new Dog();
animal.speak();

But the output is:

Bark

The compiler checks whether Animal has an accessible speak() method. At runtime, the JVM sees that the actual object is a Dog and executes Dog.speak().

The essential difference can be summarized as follows:

QuestionOverloadingOverriding

When is it resolved? Compile time Runtime
What determines the result? Compile-time argument types Runtime receiver type
What is selected? A method signature An implementation of that signature
Main purpose Support several input forms Support subtype-specific behavior

A variable has both a compile-time type and a runtime type

Consider this declaration:

Animal animal = new Dog();

Two types are involved.

The compile-time type of the variable is Animal.

Compile-time type: Animal

The runtime type of the object is Dog.

Runtime type: Dog

The compile-time type determines what the compiler allows:

  • which fields can be accessed
  • which methods can be called
  • which overload candidates are visible
  • which method signature is selected

The runtime type matters when Java dispatches an overridden instance method.

animal.speak();

The compiler accepts the call because Animal declares speak().

When the program runs, the JVM invokes Dog.speak() because the object is a Dog.

However, methods that exist only in Dog are not directly available through an Animal reference.

class Dog extends Animal {

    @Override
    public void speak() {
        System.out.println("Bark");
    }

    public void fetch() {
        System.out.println("Fetch");
    }
}
Animal animal = new Dog();

animal.speak(); // valid
animal.fetch(); // compilation error

The object may be a Dog, but the compiler checks method availability through the declared type Animal.

A type check and cast can expose the subtype-specific method:

if (animal instanceof Dog dog) {
    dog.fetch();
}

If this kind of cast appears throughout an application, however, it may indicate that the abstraction or responsibility boundaries need to be reconsidered.


Overloaded methods are distinguished by their parameter lists

Overloaded methods must differ in their parameter types, count, or order.

The following declarations are valid overloads:

public void send(String message) {
}

public void send(String message, int retryCount) {
}

public void send(byte[] payload) {
}

Changing the order of parameter types also creates a different signature:

public void register(String name, int age) {
}

public void register(int age, String name) {
}

Parameter names do not distinguish methods.

public void save(String name) {
}

public void save(String value) {
}

These declarations have the same signature and cannot coexist.

For ordinary Java methods, the signature is primarily based on the method name and parameter types.

The return type is not enough to create an overload.

public int find() {
    return 1;
}

public String find() {
    return "one";
}

This code does not compile.

A caller is allowed to ignore the return value:

find();

The compiler would have no way to decide which method the call refers to.

A throws clause also does not create a different overload.

public void load() throws IOException {
}

public void load() throws SQLException {
}

These methods still have the same signature.


Overload resolution uses declared types, not runtime object types

Consider a printer with two overloads:

class Printer {

    void print(Object value) {
        System.out.println("Object");
    }

    void print(String value) {
        System.out.println("String");
    }
}

When a String variable is passed, the result is straightforward:

String message = "hello";

new Printer().print(message);

Output:

String

The compile-time type of message is String, so the compiler selects print(String).

Now store the same object in an Object variable:

Object message = "hello";

new Printer().print(message);

The runtime object is still a String, but the output is:

Object

The compiler does not inspect the future runtime object when resolving an overload.

It uses the type of the expression that appears in the source code.

Declared type: Object
Runtime type: String

Type used for overload resolution: Object

The selected method is therefore print(Object).

This behavior is a direct consequence of Java being a statically typed language. The compiler must determine the target signature before the program begins running.


Overloading and overriding can both affect one call

Overloading and overriding are not limited to separate examples. Both mechanisms can participate in a single method call.

class MessagePrinter {

    void print(Object value) {
        System.out.println(
                "MessagePrinter Object"
        );
    }

    void print(String value) {
        System.out.println(
                "MessagePrinter String"
        );
    }
}

class LoggingPrinter extends MessagePrinter {

    @Override
    void print(Object value) {
        System.out.println(
                "LoggingPrinter Object"
        );
    }

    @Override
    void print(String value) {
        System.out.println(
                "LoggingPrinter String"
        );
    }
}

Now consider:

MessagePrinter printer =
        new LoggingPrinter();

Object message = "hello";

printer.print(message);

The compiler resolves the overload first.

The compile-time type of printer is MessagePrinter, and the compile-time type of message is Object.

The selected signature is therefore:

void print(Object value)

At runtime, Java looks at the actual receiver object.

The object is a LoggingPrinter, and that class overrides print(Object).

The final method executed is:

LoggingPrinter.print(Object)

Output:

LoggingPrinter Object

The process is:

1. Compile time
   The argument type is Object.
   Select print(Object).

2. Runtime
   The receiver is a LoggingPrinter.
   Execute LoggingPrinter.print(Object).

LoggingPrinter.print(String) is not reconsidered at runtime.

Runtime dispatch does not restart overload resolution using the argument’s actual object type. It only chooses an implementation for the signature that the compiler already selected.


Adding an overload in a subclass does not make it visible through the parent type

The difference becomes especially clear when a subclass adds a new overload without overriding the inherited method.

class Parent {

    void process(Object value) {
        System.out.println("Parent Object");
    }
}

class Child extends Parent {

    void process(String value) {
        System.out.println("Child String");
    }
}

Child.process(String) does not override Parent.process(Object).

The parameter types differ, so it is a new overload.

With a Child reference, the more specific overload is available:

Child child = new Child();
child.process("hello");

Output:

Child String

Now use a parent-typed reference:

Parent parent = new Child();
parent.process("hello");

The compiler searches the methods available through Parent.

Parent declares only:

process(Object)

The compiler therefore selects process(Object).

Because Child does not override that signature, the inherited parent implementation runs:

Parent Object

The fact that the runtime object is a Child does not make every method declared by Child visible through a Parentreference.

Dynamic dispatch applies only to an already selected, matching method signature.


The compiler chooses the most specific applicable overload

When several overloads can accept an argument, the compiler attempts to select the most specific applicable method.

class Calculator {

    void calculate(int value) {
        System.out.println("int");
    }

    void calculate(long value) {
        System.out.println("long");
    }

    void calculate(double value) {
        System.out.println("double");
    }
}

An integer literal has type int by default:

new Calculator().calculate(10);

Output:

int

A literal with the L suffix has type long:

new Calculator().calculate(10L);

Output:

long

Suppose the int overload is removed:

class Calculator {

    void calculate(long value) {
        System.out.println("long");
    }

    void calculate(double value) {
        System.out.println("double");
    }
}

Calling the method with an int value selects long:

new Calculator().calculate(10);

Output:

long

Both long and double can represent the argument through primitive widening, but long is the more specific applicable choice in this case.


Primitive widening is considered before boxing

Autoboxing can make overload resolution less intuitive.

class Processor {

    void process(long value) {
        System.out.println("long");
    }

    void process(Integer value) {
        System.out.println("Integer");
    }
}

Which method is selected here?

new Processor().process(10);

Output:

long

The literal 10 has type int.

Java can widen int to long without boxing. It can also box the value into Integer, but overload resolution prefers an applicable method that does not require boxing.

A useful simplified model is:

exact or directly applicable match
→ primitive or reference widening
→ boxing or unboxing where permitted
→ variable-arity invocation

The complete language rules are more detailed than this outline. The practical lesson is that mixing primitive, wrapper, and variable-argument overloads can make an API difficult to predict.


Variable-argument methods are considered later

Consider a normal single-argument method and a variable-argument method:

class Logger {

    void log(String message) {
        System.out.println("single");
    }

    void log(String... messages) {
        System.out.println("varargs");
    }
}

Passing one string selects the fixed-arity method:

new Logger().log("hello");

Output:

single

Passing several strings requires the variable-argument method:

new Logger().log(
        "first",
        "second"
);

Output:

varargs

Variable arguments are useful, but combining them with related reference-type overloads can create ambiguous calls.

For example:

new Logger().log(null);

This call is ambiguous.

null can be passed as a String, but it can also be passed as a String[], which is the actual parameter type of String....

Because String and String[] are unrelated reference types, neither overload is more specific for this call.


null can make overload resolution ambiguous

Consider two overloads with unrelated reference types:

class Formatter {

    void format(String value) {
        System.out.println("String");
    }

    void format(Integer value) {
        System.out.println("Integer");
    }
}

This call does not compile:

new Formatter().format(null);

null can be assigned to both String and Integer.

Neither type is a subtype of the other, so the compiler cannot determine which overload is more specific.

A cast can force the intended choice:

new Formatter().format(
        (String) null
);

However, if callers frequently need explicit casts to select an overload, the API may be too ambiguous.

The situation differs when one parameter type is more specific than the other:

void format(Object value) {
}

void format(String value) {
}

For:

format(null);

the compiler selects format(String) because String is more specific than Object.


Generic type arguments cannot always distinguish overloads

These methods appear to accept different parameter types:

void process(List<String> values) {
}

void process(List<Integer> values) {
}

But the code does not compile.

Java generics use type erasure. After erasure, both signatures effectively become:

process(List)

The methods therefore have the same erased signature.

A clearer alternative is to use distinct names:

void processNames(
        List<String> names
) {
}

void processScores(
        List<Integer> scores
) {
}

If the operation is genuinely the same for every element type, a generic method may be appropriate:

<T> void process(List<T> values) {
}

Overloading methods only by generic type arguments is not possible when their erased signatures are identical.


Overriding provides a different implementation of the same contract

Overriding allows a subtype to provide its own implementation of an inherited operation.

class NotificationSender {

    public void send(String message) {
        System.out.println(
                "Sending notification: "
                        + message
        );
    }
}
class EmailSender
        extends NotificationSender {

    @Override
    public void send(String message) {
        System.out.println(
                "Sending email: "
                        + message
        );
    }
}

Code using the parent type does not need to know the exact subtype:

public void notifyUser(
        NotificationSender sender,
        String message
) {
    sender.send(message);
}
notifyUser(
        new EmailSender(),
        "Order completed"
);

notifyUser() depends only on the send() contract declared by NotificationSender.

At runtime, the actual object supplies the implementation.

A new subtype can participate without changing the calling method:

class SmsSender
        extends NotificationSender {

    @Override
    public void send(String message) {
        System.out.println(
                "Sending SMS: "
                        + message
        );
    }
}
notifyUser(
        new SmsSender(),
        "Order completed"
);

This is runtime polymorphism: the same invocation can produce different behavior depending on the object supplied.


An overriding method must match the inherited signature

The following method correctly overrides its parent:

class Parent {

    void execute(String command) {
    }
}

class Child extends Parent {

    @Override
    void execute(String command) {
    }
}

Changing the parameter type creates a different overload instead:

class Child extends Parent {

    void execute(Object command) {
    }
}

Child.execute(Object) does not replace Parent.execute(String).

They are separate methods.

Child child = new Child();

child.execute("run");

Because execute(String) is more specific for a String argument, the inherited parent method may be selected rather than the newly declared execute(Object).

If the developer intended to override the parent method, the result is incorrect.

This is why @Override should be used consistently.

@Override
void execute(Object command) {
}

The compiler rejects this declaration because no matching inherited method exists.

@Override is not merely documentation. It asks the compiler to verify that the intended overriding relationship is real.


An overriding method may return a more specific type

An overriding method may use the same return type as the parent method or a subtype of that return type.

This is called a covariant return type.

class AnimalFactory {

    Animal create() {
        return new Animal();
    }
}
class DogFactory
        extends AnimalFactory {

    @Override
    Dog create() {
        return new Dog();
    }
}

Because Dog is an Animal, the child method still satisfies the parent contract.

Code expecting an Animal can safely receive a Dog.

The reverse is not allowed.

If the parent promises a Dog, the child cannot broaden the result to a general Animal, because existing callers may rely on receiving a Dog.


An overriding method cannot reduce accessibility

A subclass may preserve or widen the accessibility of an inherited method.

class Parent {

    protected void run() {
    }
}

The subclass may make it public:

class Child extends Parent {

    @Override
    public void run() {
    }
}

But it cannot make the method less accessible:

class Child extends Parent {

    @Override
    private void run() {
    }
}

This does not compile.

Code written against the parent type is already allowed to call the inherited method under the parent’s access rules. A subtype cannot break that contract by making the operation less accessible.


Checked exceptions cannot be broadened when overriding

Suppose a parent method declares a checked exception:

class FileLoader {

    void load() throws IOException {
    }
}

A child method may declare the same exception or a more specific subtype:

class TextFileLoader
        extends FileLoader {

    @Override
    void load()
            throws FileNotFoundException {
    }
}

FileNotFoundException is a subtype of IOException, so callers prepared to handle IOException remain valid.

The child cannot declare a broader checked exception:

class InvalidLoader
        extends FileLoader {

    @Override
    void load() throws Exception {
    }
}

Code using FileLoader was written under the assumption that handling IOException was sufficient. Allowing the subtype to throw any checked Exception would break that expectation.

Unchecked exceptions do not follow the same declaration restriction, but an overriding implementation should still respect the behavioral expectations of the parent contract.


Static methods are hidden, not overridden

Static methods belong to classes rather than individual objects.

They are therefore not dynamically dispatched based on the runtime type of the receiver.

class Parent {

    static void identify() {
        System.out.println("Parent");
    }
}

class Child extends Parent {

    static void identify() {
        System.out.println("Child");
    }
}

Now consider:

Parent value = new Child();
value.identify();

Output:

Parent

The call is resolved using the compile-time type of value, which is Parent.

The method in Child does not override the parent method. It hides it.

Static methods should normally be invoked through the class name:

Parent.identify();
Child.identify();

Calling them through an instance is legal, but it misleadingly suggests that the object’s runtime type might affect the result.


Fields are hidden rather than overridden

Field access also depends on the compile-time type of the reference.

class Parent {

    String name = "Parent";
}

class Child extends Parent {

    String name = "Child";
}
Parent value = new Child();

System.out.println(value.name);

Output:

Parent

The selected field is Parent.name because value is declared as Parent.

Instance methods behave differently:

class Parent {

    String name = "Parent";

    String getName() {
        return name;
    }
}

class Child extends Parent {

    String name = "Child";

    @Override
    String getName() {
        return name;
    }
}
Parent value = new Child();

System.out.println(value.name);
System.out.println(value.getName());

Output:

Parent
Child

The field access is resolved through the compile-time type.

The instance method is dynamically dispatched to the runtime type.

Declaring fields with the same name in parent and child classes usually makes code harder to understand. Field hiding is best avoided unless there is a compelling reason.


Private methods are not overridden

A private method is not inherited by subclasses.

class Parent {

    private void run() {
        System.out.println("Parent");
    }
}

A subclass may declare a method with the same name and parameters:

class Child extends Parent {

    private void run() {
        System.out.println("Child");
    }
}

These are unrelated methods.

Child.run() does not override Parent.run() because the parent method is not visible or inherited in the child.

This property can be useful when constructor logic must call an internal helper that subclasses must not replace.


Final methods cannot be overridden

A parent class can prevent further overriding by declaring a method final.

class Payment {

    public final void validate() {
        System.out.println(
                "Common validation"
        );
    }
}

A subclass cannot replace that behavior:

class CardPayment extends Payment {

    @Override
    public void validate() {
        // compilation error
    }
}

final can protect behavior that must remain consistent throughout a hierarchy.

It should still be used deliberately. Marking every method final can make a design unnecessarily rigid. The class should distinguish between behavior that subclasses are expected to customize and rules that must remain under the parent’s control.


Constructors are not overridden

Constructors are tied to the class that declares them and are not inherited.

class Parent {

    Parent(String name) {
    }
}
class Child extends Parent {

    Child(String name) {
        super(name);
    }
}

Although the parameter lists are similar, Child(String) does not override Parent(String).

The child constructor initializes a new Child object and delegates to the parent constructor to initialize the parent-defined portion of that same object.

Constructor selection is based on the class and arguments written in the new expression:

new Child("name");

This selects Child(String) at compile time.

Constructors are not dynamically dispatched after object creation like ordinary overridable instance methods.


Calling an overridable method from a constructor is dangerous

The previous article on object initialization examined a case like this:

class Parent {

    Parent() {
        printState();
    }

    void printState() {
        System.out.println("Parent");
    }
}

class Child extends Parent {

    private String message =
            "Child is ready";

    @Override
    void printState() {
        System.out.println(message);
    }
}

Now create a Child:

new Child();

Output:

null

Even while the parent constructor is running, the actual object is a Child. Dynamic dispatch therefore calls Child.printState().

However, the child’s field initializer has not yet executed.

Create Child object
message = null

→ run Parent constructor
→ call printState()
→ dispatch to Child.printState()
→ print null

→ initialize Child fields
message = "Child is ready"

Dynamic dispatch applies even while an object is still being constructed.

For this reason, constructors should generally avoid calling instance methods that subclasses can override. A parent constructor cannot know which uninitialized child fields an overriding method may access.


Overriding is more than printing different messages

Introductory examples often demonstrate overriding by printing different text.

In real applications, overriding allows implementations to satisfy the same contract in different ways.

public interface PaymentProcessor {

    PaymentResult process(
            PaymentRequest request
    );
}

A card implementation may call a card authorization service:

public final class CardPaymentProcessor
        implements PaymentProcessor {

    @Override
    public PaymentResult process(
            PaymentRequest request
    ) {
        // Process a card payment
        return PaymentResult.success();
    }
}

A bank transfer implementation may follow a different workflow:

public final class BankTransferProcessor
        implements PaymentProcessor {

    @Override
    public PaymentResult process(
            PaymentRequest request
    ) {
        // Process a bank transfer
        return PaymentResult.success();
    }
}

The application service depends on the contract:

public final class PaymentService {

    private final PaymentProcessor
            paymentProcessor;

    public PaymentService(
            PaymentProcessor paymentProcessor
    ) {
        this.paymentProcessor =
                paymentProcessor;
    }

    public PaymentResult pay(
            PaymentRequest request
    ) {
        return paymentProcessor
                .process(request);
    }
}

PaymentService does not need to check whether the implementation is for cards, transfers, or another payment type.

It calls the contract method, and the injected object supplies the actual behavior.

This can be easier to extend than repeatedly growing a conditional:

if (type == CARD) {
    // card payment
} else if (type == BANK_TRANSFER) {
    // bank transfer
} else if (type == MOBILE) {
    // mobile payment
}

Conditionals are not inherently wrong. But when behaviors change independently and represent separate responsibilities, polymorphic implementations can create clearer boundaries.


Overloading is useful when one operation supports several input forms

Overloading works well when several methods represent the same conceptual operation.

public class MessageSender {

    public void send(String message) {
        send(message, 1);
    }

    public void send(
            String message,
            int retryCount
    ) {
        // Perform the actual send
    }
}

Both methods represent message delivery.

The shorter overload supplies a default retry count, while the other gives the caller more control.

Standard library APIs often use the same pattern:

Duration.ofSeconds(seconds);
Duration.ofSeconds(
        seconds,
        nanoAdjustment
);

The overloads belong together because they represent one operation with different levels of detail.

Using the same name for unrelated work is much less helpful:

void process(String filePath) {
    // process a file
}

void process(int userId) {
    // delete a user
}

The parameter types differ, but the operations do not share a meaningful concept.

Clearer names communicate the intent better:

void processFile(String filePath) {
}

void deleteUser(int userId) {
}

Overloading should provide several natural ways to perform the same operation, not merely reduce the number of method names.


Too many overloads make an API difficult to predict

Java can resolve an overload set like this:

void convert(int value) {
}

void convert(long value) {
}

void convert(Integer value) {
}

void convert(Number value) {
}

void convert(Object value) {
}

void convert(int... values) {
}

But a caller may need to think about:

  • exact type matches
  • primitive widening
  • boxing
  • reference widening
  • variable arguments
  • the type of null
  • explicit casts

An API is not automatically clear merely because the compiler can resolve it.

Particularly risky combinations include:

  • primitive and wrapper overloads
  • several unrelated reference types
  • arrays and variable arguments
  • multiple nullable reference parameters
  • unrelated operations sharing one name

When the selected method is not obvious from the call site, distinct method names may be the better design.


An overriding implementation must preserve the parent contract

A subclass may replace a method body, but it should not destroy the expectations established by the parent type.

Consider:

class Account {

    public void withdraw(long amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException();
        }

        // Normal withdrawal behavior
    }
}

A subclass could technically override the method like this:

class BrokenAccount extends Account {

    @Override
    public void withdraw(long amount) {
        throw new UnsupportedOperationException();
    }
}

But code written against Account expects withdrawal to be a supported operation:

void pay(
        Account account,
        long amount
) {
    account.withdraw(amount);
}

Passing a BrokenAccount violates that expectation.

Correct overriding is not only about matching the signature. The subtype should remain usable wherever the parent type is expected.

Before using inheritance for code reuse, it is worth asking whether the types genuinely share the same behavioral contract. If they do not, composition may be a better design.


Use @Override consistently

An overriding method should almost always be annotated with @Override.

@Override
public void send(String message) {
}

Consider this mistake:

class Parent {

    void send(String message) {
    }
}
class Child extends Parent {

    void send(Object message) {
    }
}

The developer may believe the method was overridden. In reality, the child added an overload.

Adding @Override exposes the mistake immediately:

@Override
void send(Object message) {
    // compilation error
}

The annotation also protects against:

  • misspelled method names
  • accidentally changed parameter types
  • refactoring in a superclass
  • incorrect assumptions about inherited methods

It documents the relationship for readers and asks the compiler to verify it.


Tracing several calls step by step

Consider the following hierarchy:

class Parent {

    void show(Object value) {
        System.out.println(
                "Parent Object"
        );
    }

    void show(String value) {
        System.out.println(
                "Parent String"
        );
    }
}

class Child extends Parent {

    @Override
    void show(Object value) {
        System.out.println(
                "Child Object"
        );
    }

    @Override
    void show(String value) {
        System.out.println(
                "Child String"
        );
    }
}

Now declare:

Parent target = new Child();

Object first = "hello";
String second = "hello";

First call

target.show(first);

The compile-time type of first is Object.

The compiler selects show(Object).

The runtime object referenced by target is a Child, so Child.show(Object) executes.

Child Object

Second call

target.show(second);

The compile-time type of second is String.

The compiler selects show(String).

The receiver is still a Child, so Child.show(String) executes.

Child String

Third call

target.show((Object) second);

The cast changes the compile-time type of the argument expression to Object.

The compiler selects show(Object).

Runtime dispatch then selects Child.show(Object).

Child Object

The cast does not transform the underlying String object into an Object. Every Java object is already an Object.

It changes the type through which the compiler interprets the expression.

A reliable way to analyze a method call is:

1. Determine the compile-time type of each argument.
2. Select the overload signature.
3. Determine the runtime type of the receiver.
4. Execute the overriding implementation of that signature.

Practice problems

Problem 1

class Printer {

    void print(Object value) {
        System.out.println("Object");
    }

    void print(String value) {
        System.out.println("String");
    }
}

Object value = "Java";

new Printer().print(value);

What is printed?

Problem 2

class Parent {

    void run() {
        System.out.println("Parent");
    }
}

class Child extends Parent {

    @Override
    void run() {
        System.out.println("Child");
    }
}

Parent value = new Child();
value.run();

What is printed?

Problem 3

class Parent {

    void process(Object value) {
        System.out.println(
                "Parent Object"
        );
    }
}

class Child extends Parent {

    void process(String value) {
        System.out.println(
                "Child String"
        );
    }
}

Parent value = new Child();
value.process("hello");

What is printed?

Problem 4

class NumberPrinter {

    void print(long value) {
        System.out.println("long");
    }

    void print(Integer value) {
        System.out.println("Integer");
    }
}

new NumberPrinter().print(10);

What is printed?


Answers

Problem 1

Object

The runtime object is a String, but the compile-time type of value is Object.

Overload resolution therefore selects print(Object).

Problem 2

Child

The compiler confirms that Parent has a run() method.

At runtime, the receiver is a Child, so the overriding implementation executes.

Problem 3

Parent Object

The compile-time type of value is Parent.

The only applicable method visible through that type is Parent.process(Object).

Child.process(String) is a separate overload, not an override of process(Object).

Problem 4

long

The literal 10 has type int.

Primitive widening from int to long is preferred over boxing the value into Integer.


Understanding method selection makes Java polymorphism clearer

Overloading and overriding both involve methods with the same name, but they solve different problems.

Overloading allows the compiler to choose among several method signatures based on the static type information available at the call site.

The runtime object does not change that overload choice.

Overriding allows the actual receiver object to choose the implementation of an already selected instance method.

That is why these two calls behave differently:

Object message = "hello";
printer.print(message);

Here, the declared type of message determines the overload.

Animal animal = new Dog();
animal.speak();

Here, the actual Dog object determines the overriding implementation.

When a method call looks confusing, there is no need to reason about everything at once.

First, think like the compiler. Look at the declared receiver type and the compile-time argument types. Determine which method signature is selected.

Then think about runtime dispatch. If the selected method is an overridable instance method, look at the actual receiver object and determine which implementation it provides.

This same model explains several other Java behaviors:

  • why an overridden method may run during a parent constructor
  • why static methods do not follow the runtime object
  • why field access and method invocation can produce different results
  • why interface-typed references can use many implementations
  • why adding a new overload in a subclass does not replace a parent method

Good overloading provides several clear ways to perform the same conceptual operation.

Good overriding preserves the parent contract while allowing each subtype to provide behavior appropriate to the object it represents.

When overload sets become difficult to predict or overriding breaks the assumptions of the parent type, the code may be syntactically valid while still being poorly designed.

Understanding how Java selects a method is therefore not only useful for solving output questions.

It is the foundation for understanding how compile-time types, runtime objects, contracts, and polymorphism work together in real Java applications.

References