OOP Class Notes for 10/29/13
Method Overloading Overview
Implementation Details
- As stated above, method overloading means using the same name several times, distinguishing between them by parameter number and types
- We DON'T distinguish by return type because selecting the correct overloaded method requires analyzing the number and types of the arguments to pick the right method
- Overloaded methods have separate slots in the vtable because the compiler treats them as totally different. The sameness of their names is a convenience for us as programmers, and does not change the execution model of our language
Method Overloading Example
- Consider the following class:
public class Overloaded {
public static class A { public String toString() { return "A"; } }
public static class B extends A { public String toString() { return "B"; } }
public static class C extends B { public String toString() { return "C"; } }
public void m() { System.out.println("m() : ---"); }
public void m(byte b) { System.out.println("m(byte) : " + b); }
public void m(short s) { System.out.println("m(short) : " + s); }
public void m(int i) { System.out.println("m(int) : " + i); }
public void m(long l) { System.out.println("m(long) : " + l); }
public void m(Integer i) { System.out.println("m(Integer) : " + i); }
public void m(Object o) { System.out.println("m(Object) : " + o); }
public void m(String s) { System.out.println("m(String) : " + s); }
public void m(A a) { System.out.println("m(A) : " + a); }
public void m(B b) { System.out.println("m(B) : " + b); }
public void m(A a1, A a2) { System.out.println("m(A,A) : "+ a1 +", "+ a2);}
public void m(A a1, B b2) { System.out.println("m(A,B) : "+ a1 +", "+ b2);}
public void m(B b1, A a2) { System.out.println("m(B,A) : "+ b1 +", "+ a2);}
public void m(C c1, C c2) { System.out.println("m(C,C) : "+ c1 +", "+ c2);}
public static void main(String[] args) {
Overloaded o = new Overloaded();
byte n1 = 1, n2 = 2;
A a = new A();
B b = new B();
C c = new C();
o.m();
o.m(n1);
o.m(n1 + n2);
o.m(new Object());
o.m(new Exception());
o.m("String");
o.m(a);
o.m(b);
o.m(c);
o.m(a, a);
o.m((A)b, b);
o.m(c, c);
}
}
Calling m
with no parameters or with one byte
as a parameter behaves exactly as expected, dispatching to the obvious method
With the sum of two bytes as one parameter, we call m(int)
because in Java the sum of two bytes has type int
This is because of the JVM design: Java instructions are only 8
bits wide, so there is not enough space to have separate arithmetic operations for bytes, shorts, ints, longs, floats, and doubles
With only 256 total instructions, byte addition was considered
not important enough for its own instruction. So the JVM architecture affects the language design.
Calling m
with an Object
, an Exception
, or a String
also behaves just as expected
In particular, if we comment out the
method m(String)
, passing a String
will
call m(Object)
. If we uncomment it, then we call m(String)
Making a method static does not change this: whether a method
is virtual or not is orthogonal to the question of which overloaded method to invoke
Now with inheritance:
calling m(a)
, m(b)
, m(c)
is
resolved
to m(A)
, m(B)
, m(B)
,
respectively, because we invoke the most specific match. That is,
we have C extends B extends A
but we only have
distinct m
methods for A
and B
The same logic holds for m(a,a)
and m(c,c)
, which calls m(A,A)
and m(C,C)
because there is a direct match
Tricky Points
- For
m(b,b)
it is less clear because we only have m(A,B)
and m(B,A)
and neither is more specific than the other (intuitively; see a more formal notion of a method's generality below)
- The code does not compile because the compiler does not like the ambiguity in the method call
- We can resolve the ambiguity with explicit casting of one of the arguments to an
A
, and now our code compiles
- Since
B extends A
, the cast is an upcast and
always safe, i.e., requires no runtime checking. The cast is
only used for resolution of overloading
Rules for Resolution of Overloading
- From the Java language specification: first determine the class (or interface, but we don't implement that in our translation), then find methods that are applicable and accessible
- Now we need a symbol table to track statically declared variable types so that we can decide which methods are applicable (same number of arguments as paramters, each of correct type or can be statically cast to correct type)
- xtc.util.SymbolTable already exists to help us do this
- We also need to make sure that we only use accessible methods: do not attempt to access private methods from a subclass or a protected method from outside the hierarchy or a package private method from outside the package
- If more than one method declaration is both accessible and applicable, choose the most specific method
- One method declaration is more specific than another if any invocation handled by the first can be passed on to the other without a compiler error
- So to return to our example from
Overloaded
,
neither m(A, B)
or m(B, A)
is more
specific than the other. This is why we encountered an
ambiguity when attempting to call m(b, b)
- Finally, we need to make sure that the chosen method is
appropriate: for example, calling an instance method in a static context will not compile
- There is also a concept of generality which applies to primitives, although they lie outside the class hierarchy: primitive types are "more specific" than reference types
int
goes to long
before being
auto-boxed to Integer
, which
extends Object
, for example (NOTE: the translator does not need to support auto-boxing)
- these widening, shortening, and coercion rules are enumerated in the Java language specification
Implications for our Translators
- If there are overloaded methods, we need to do some name mangling for our vtable because the method name is no longer unique
- If we overload a method in a subclass, the vtable for the superclass may also have a mangled name, so we need some consistent discipline
- However, we do have the ability to read the entirety of the code before beginning translation -- highly useful
- Since method overloading is a major addition to our translators for the final project, there will be extensive testing of name mangling
- Name mangling is necessary since members of the vtable are
function pointers. Function pointers can't be distinguished by types of parameters like methods can be
- For example, in
Overloaded
we have 14 methods named m
, but in our vtable there is no concept of "slot overloading" so each of these 14 methods need a different name, just like you can't declare an int n
and a double n
in the same scope
- There will be many test cases, mostly testing major features like vtables, method chaining, and method overloading rather than obscure features
- Conversions between types of numbers (int, short, long, float, double, etc.) is also something we need to handle in our translator
Conceptual Recap
- From a language design perspective, we want to keep concepts like static, private, overloading orthogonal
- From a programmer's perspective, we want overloading to achieve greater conciseness, but don't want to change runtime behavior of our code
- We get a compile-time error if overloading resolution is ambiguous, as we saw above with
m(b, b)
- We also get a compile-time error if we resolve to an
instance method in a static context