Each overloaded method must have a unique list of argument types.

This means that there are the following possible cases:

  1. argument count are different
void foo(int a) {}
void foo(int a, String b) {}
  1. argument types are different
void foo(int a) {}
void foo(String b) {}
  1. argument types are different, even if inheritance
void foo(List a) {}
void foo(ArrayList a) {}
  1. return types are different
void foo(int a) {}
int foo(int b) { return 0; }

NO!! This will have a compiler error.

When overloaded methods are invoked, compiler choose the best matched method by using the declared types of passing arguments.

So:

public static void main(String[] args) {
    List a = new ArrayList();
    o.foo(a) // This will invoke the "void foo(List a) {}" method
}

because overloading is a static binding behavior and only occurs in compile time.

When considering which overloaded method will be invoked, you should always regard yourself as the compiler.

When it comes to the primitives, please see the following example:


class Demotion {
    void f1(char x) {
        System.out.println("f1(ch<wbr />ar)");
    }

    void f1(byte x) {
        System.out.println("f1(byte)");
    }

    void f1(short x) {
        System.out.println("f1(short)");
    }

    void f1(int x) {
        System.out.println("f1(int)");
    }

    void f1(long x) {
        System.out.println("f1(long)");
    }

    void f1(float x) {
        System.out.println("f1(float)");
    }

    void f1(double x) {
        System.out.println("f1(double)");
    }

    void f2(char x) {
        System.out.println("f2(char)");
    }

    void f2(byte x) {
        System.out.println("f2(byte)");
    }

    void f2(short x) {
        System.out.println("f2(short)");
    }

    void f2(int x) {
        System.out.println("f2(int)");
    }

    void f2(long x) {
        System.out.println("f2(long)");
    }

    void f2(float x) {
        System.out.println("f2(float)");
    }

    void f3(char x) {
        System.out.println("f3(char)");
    }

    void f3(byte x) {
        System.out.println("f3(byte)");
    }

    void f3(short x) {
        System.out.println("f3(short)");
    }

    void f3(int x) {
        System.out.println("f3(int)");
    }

    void f3(long x) {
       System.out.println("f3(long)");
    }

    void f4(char x) {
        System.out.println("f4(char)");
    }

    void f4(byte x) {
        System.out.println("f4(byte)");
    }

    void f4(short x) {
       System.out.println("f4(short)");
    }

    void f4(int x) {
        System.out.println("f4(int)");
    }

    void f5(char x) {
        System.out.println("f5(char)");
    }

    void f5(byte x) {
        System.out.println("f5(byte)");
    }

    void f5(short x) {
        System.out.println("f5(short)");
    }

    void f6(char x) {
        System.out.println("f6(char)");
    }

    void f6(byte x) {
        System.out.println("f6(byte)");
    }

    void f7(char x) {
        System.out.println("f7(char)");
    }

    void testDouble() {
        double x = 0;
        System.out.println("double argument:");
        f1(x);
        f2((float) x);
        f3((long) x);
        f4((int) x);
        f5((short) x);
        f6((byte) x);
        f7((char) x);
    }

    public static void main(String[] args) {
        Demotion p = new Demotion();
        p.testDouble();
    }
}

Output is:

double argument:

f1(double)

f2(float)

f3(long)

f4(int)

f5(short)

f6(byte)

f7(char)

From the above example, you can see the same rule: the compiler decide method binding in compile time and it always chooses the best and most possible method. When it is possible, it need casting a type to another. So when some wider types like double, float cast to some narrower types like int, short etc, the precision and width of number will be narrowed. If it cannot do this, an error message will be issued.