Skip to main content

Section 8.8 Overloading Methods

Java allows us to declare several methods with the same name in a single class. To nevertheless distinguish them properly, methods are not only identified by their name, like in C, but also by their signature. Consider the invocation of a method m:

\begin{equation*} a.m(e_1,\dots,e_n)\qquad a:U\quad e_i:U_i,\,1\le i\le n \end{equation*}

Which of the overloaded methods m could be called here? Remember that the exact method that will be called in the execution can in general only be determined at the programs run time because of overridden methods. Which of a group of overriding methods in a class hierarchy is called is decided based on the concrete (dynamic) type of a and not its static type.

A first point of consideration is the number of arguments. For the above call, only method declarations whose signature has \(n\) parameters (their arity is \(n\)) can be considered. Next, Java checks whether the static types of the arguments can be converted to the types of the method parameters. Candidate signatures where this is possible are called matching signatures. Among those, the Java compiler selects the most specific signature. Since overloaded methods are resolved at compile time, only the static types are relevant.

Definition 8.8.1. More Specific Signature.

A signature \((T_1,\dots, T_n)\) is more specific than a signature \((U_1,\dots,U_n)\) if \(T_i\sqsubseteq U_i\) for all \(1\le i\le n\text{.}\) Here, \(T\sqsubseteq U\) holds if \(T\) can be converted to \(U\) according to Figure 8.2.2 or if \(T \subtyperel U\text{.}\)

Example 8.8.2.

  1. \((\mathtt{A}, \mathtt{int}, \mathtt{int})\) is more specific than \((\mathtt{A}, \mathtt{int}, \mathtt{float})\text{.}\)

  2. \((\mathtt{A})\) is more specific than \((\mathtt{B})\) if \(A\subtyperel B\) (as \(\subtyperel\) implies \(\sqsubseteq\)).

The method to be called is now selected from the set of methods whose signature is less specific than (or equally specific as) the signature of the call. Java requires that this set of matching signatures contains one maximally specific signature, whose method is selected.

Definition 8.8.3. Maximally Specific Signature.

Let \(\mathcal S\) be a set of signatures with an arity of \(n\text{.}\) \(s\in\mathcal S\) is a maximally specific signature of \(\mathcal S\) if no \(s'\in\mathcal S\setminus\{s\}\) is more specific than \(s\text{.}\)

Definition 8.8.3 does not exclude the existence of more than one maximally specific signature per set of signatures. In this case, the method call would be ambiguous and the Java compiler would reject the program with an error message.

Example 8.8.4.

Consider the invocation of the method foo in the method call:

class A {
  void foo(int x, int y, float z) {}
  void foo(boolean x, float y) {}
  void foo(int x, float y) {}
  void foo(long x, int y) {}
  void call() {, 2); }

The argument types are \((\mathtt{int}, \mathtt{int})\text{.}\) foo has four declarations with the following signatures:

\begin{equation*} (\mathtt{int}, \mathtt{int}, \mathtt{float}), \quad(\mathtt{boolean}, \mathtt{float}), \quad(\mathtt{int}, \mathtt{float}), \quad(\mathtt{long}, \mathtt{int}) \end{equation*}

The first one does not qualify because its arity does not match. Neither does the second one, as the types are not convertible (int does not convert to boolean). The last two do match, but both are maximally specific. Java therefore cannot find a declaration and reports an error. If we added a method

void foo(int x, int y) {}

its signature would be more specific than \((\mathtt{int}, \mathtt{float})\) and \((\mathtt{long}, \mathtt{int})\text{.}\) Java then could find a unique maximally specific signature.

Remark 8.8.5.

For the sake of simplicity, we ignored variadic arguments (...), boxing/unboxing and type variables here. More details are in Section 15.12.2 of the Java Language Specification [12]. The relation \(\sqsubseteq\) is called Method Invocation Conversion (see Section 5.3 of the language specification).

Subsection 8.8.1 Overloading and overriding

If a method m is overloaded in a class A, we can separately override every overloaded method when we write a class inheriting from A. Strictly speaking, we do not override m, but \(\mathtt{m}(T_1, \dots, T_n)\text{:}\) We only override one of the concrete overloaded methods. The compiler selects the overloaded method statically at compile time, while the dynamic method dispatch at run time only decides which overridden variant of the chosen overloaded method is called.

Example 8.8.6.

Consider the following classes:

public class A {
  public void m(double X)  { System.out.println("A.m(double)"); }
  public void m(boolean x) { System.out.println("A.m(boolean)"); }

public class B extends A {
  public void m(int x)     { System.out.println("B.m(int)"); }
  public void m(boolean x) { System.out.println("B.m(boolean)"); }

To understand which method overrides which, consider the following table:

Table 8.8.7. Overloaded and overridden methods
Class Methods
A A.m(double) A.m(boolean)
B B.m(boolean) B.m(int)

Methods in a row with the same name overload each other. A method in a column overrides the method in the same column of a row above. Hence, here only B.m(boolean) overrides the method A.m(boolean). Consider the following program fragments that call m in different constellations:

  1. A a = new A();

    prints A.m(double). Trivial, since the concrete type is equal to the static type.

  2. A a = new B();

    prints A.m(double). The static type of a is A, but the concrete type is here B. However, B does not override the method A.m(double), but only overloads the methods m(double) and m(boolean) again with the parameter type int. Since the overloaded method is selected by the static type (here: A.m(double)), the method m(int) in the concrete type B is irrelevant.

  3. B b = new B();

    prints B.m(int). The static type is now B and m is overloaded three times in B.