Object-Oriented Programming

CSCI-UA.0470-001

NYU, Undergraduate Division, Computer Science Course - Fall 2013

OOP Class Notes for 10/29/13

Method Overloading Overview

  • Consider the following Visitor interface:
    public static interface Visitor<R, E extends Throwable> {
      R visitNumber(Number num) throws E;
      R visitAddition(Addition add) throws E;
      R visitSubtraction(Subtraction sub) throws E;
      R visitMultiplication(Multiplication mul) throws E;
      R visitDivision(Division div) throws E;
    }
  • What is NOT wrong with the Visitor interface
    • It's not a problem that a different method is needed for each node we are visiting (visitNumber, visitAddition, etc.)
    • This is required because they are different entities
    • The only other way is instanceof tests, which are usually bad
  • What IS wrong with the Visitor interface
    • The method names encodes the type of the entity being processed when the parameter does it already
    • A better solution is to name all five methods visit, and make them distinct by using different signatures, i.e., their parameters
    • This is method overloading: using the same name several times, distinguishing between them by parameter number and types
    • We DON'T distinguish by return type because we only know the return type AFTER we found the correct overloaded method at compile time

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