Re: [Java Spec Report] Precise specification for method resolution needed

Douglas Dunn <[email protected]> Tue, 25 Jan 2005 07:19:03 -0500
Newsgroups gmane.comp.java.spec-report
Message-ID <[email protected]>
--------------020904050406010909010407
Content-Type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 7bit

I spent a lot of time studying overloaded method matching in Java and 
had the advantage of help from Sun's compiler writer. I ripped the 
attached from my book and cleaned it up. Please let me know if this helps.

Regards,
Doug Dunn
President and CEO
developersbookshelf.com


To unsubscribe from this mailing list, send an email to:
[email protected]
 
Yahoo! Groups Links

<*> To visit your group on the web, go to:
    http://groups.yahoo.com/group/java-spec-report/

<*> To unsubscribe from this group, send an email to:
    [email protected]

<*> Your use of Yahoo! Groups is subject to:
    http://docs.yahoo.com/info/terms/
 

--------------020904050406010909010407
Content-Type: text/plain;
 name="ripforjsp.txt"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline;
 filename="ripforjsp.txt"

Overloaded Method Matching
When evaluating a method invocation expression that invokes an overloaded m=
ethod, there exists the possibility of ambiguity. The compiler applies a se=
t of rules referred to as overloaded method matching in the JLS to determin=
e which of the overloaded methods to invoke. Assuming the method invocation=
 expression is not ambiguous, the method invoked is referred to as the most=
 specific method. The same rules apply to constructors, however, which mean=
s the most specific method may actually be the most specific constructor.=20

The 1.4.2 release quietly introduced a subtle change to overloaded method m=
atching. I say =93quietly=94 because the primary Bug Id 4761586 is marked f=
ixed in =93tiger=94 (the code name for the 1.5 release). Furthermore, Bug I=
d 4761586 is not listed in the =93Bugs fixed in J2SE 1.4.2=94 document. The=
 change is simply that the class in which an overloaded method is declared =
(the declaring class) is no longer used in determining the most specific me=
thod. For example=20

class Test extends Superclass {
   public static void main(String[] args) {
      byte b =3D 0;
      new Test().test(b);
   }
   void test(int i) { }
}
class Superclass {
    void test(byte b) { }
}

Under the old overloaded method matching algorithm in pre 1.4.2 releases, a=
ttempting to compile this program would generate the following compiler err=
or:
Test.java:4: reference to test is ambiguous, both method test(byte) in Supe=
rclass and method test(int) in Test match
       new Test().test(b);
       ^
1 error
The test(byte b) and test(int i) method declarations are both applicable. T=
he problem is that the test(byte b) method with the more specific parameter=
 type is declared in a superclass. It is, however, inherited by the Test su=
bclass, so this compiler error makes absolutely no sense to the vast majori=
ty of programmers. As of the 1.4.2 release, this program compiles and print=
s the expected test(byte b).=20

Gilad Bracha has provided a complex mathematical proof that this is a backw=
ards compatible change in the Java Spec Report discussion group at groups.y=
ahoo.com/group/java-spec-report/message/849. To summarize what this Java Sp=
ec Report post says, the change only affects examples such as the one above=
 that did not compile in pre 1.4.2 releases.=20

As a consequence of this change in overloaded method matching, I have divid=
ed this section into two parts. The first is 5.8.1 Choosing The Most Specif=
ic Applicable Method which discusses the rules for overloaded method matchi=
ng in 1.4.2 and future releases. 5.8.2 The Declaring Class of Applicable Me=
thods provides some background on the old overloaded method matching algori=
thm in pre 1.4.2 releases and explains why the declaring class of applicabl=
e methods is no longer relevant. My reason for dividing this section is tha=
t the use of the declaring class significantly muddies the water of overloa=
ded method matching, and no one at Sun has ever adequately explained that l=
anguage design decision. After having researched this problem for years, I =
feel confident in saying that the use of the declaring class in overloaded =
method matching was simply a mistake in the original specification. It ther=
efore has no logical explanation. Readers new to the Java programming langu=
age really should not read 5.8.2 The Declaring Class of Applicable Methods.=
 It is intended for experienced Java programmers who want to fully understa=
nd the rationale for this change in overloaded method matching.




Choosing The Most Specific Applicable Method
Before discussing the rules for overloaded method matching, it is necessary=
 to define the term applicable method. It is defined in the JLS as follows.

A method declaration is applicable to a method invocation if and only if bo=
th of the following are true:=20
The number of parameters in the method declaration equals the number of arg=
ument expressions in the method invocation.
The type of each actual argument can be converted by method invocation conv=
ersion 										to the type of the corresponding parameter.

There is actually a gross omission in this specification; applicable method=
s necessarily have the same name as is found in the method invocation or cl=
ass instance creation expression. Thus, if there is more than one applicabl=
e method, the implication is that the method (or constructor) is overloaded=
. This section discusses the rules Java compilers use to resolve a method i=
nvocation or class instance creation expression that has more than one appl=
icable method or constructor.

The =93by method invocation conversion=94 in the last bulleted item simply =
means that the type conversion must be safe (or, more precisely, either an =
identity or widening conversion as shown in Table 5.7 Allowable Conversions=
 in a Given Context on page 655). The following example is repeated from 5.=
7.2 Method Invocation Conversion Context.=20

class Test {
   public static void main(String[] args) {
       test(0);
   }
   static void test(byte b) { }
}
Attempting to compile this program generates the following compiler error:
Test.java:3: test(byte) in Test cannot be applied to (int)
       test(0);
       ^
1 error

The test(byte b) method in Test =93cannot be applied to (int)=94 because in=
t to byte is a narrowing (read unsafe) type conversion, which is not allowe=
d in the method invocation conversion context.
If there is more than one applicable method, the compiler must choose the m=
ost specific method. It is precisely here where the conceptual difficulty i=
n understanding overloaded method matching arises. The types of the actual =
arguments in a method invocation or class instance creation expression dete=
rmine the applicable method(s) or constructor(s), but they are not used in =
overloaded method matching. I cannot overemphasize this point. Instead of u=
sing the types of the actual arguments, overloaded method matching in the J=
ava programming language compares formal parameter types in the list of app=
licable methods. Even though the actual argument types are not used, the re=
sulting conversions from actual argument types to the corresponding formal =
parameter types of the most specific method will always be appropriate for =
the method invocation conversion context (i.e. either an identity or wideni=
ng conversion) because the definition of applicable methods. If you underst=
and this one point, then overloaded method matching in the 1.4.2 release an=
d beyond is very simple.

---------------------------------------------------------------------------=
----------------
The type(s) of the actual argument expression(s) are not used in overloaded=
 method matching.
---------------------------------------------------------------------------=
----------------

The rule for choosing the most specific applicable method is very simple. E=
ach parameter in the applicable method signature must be narrower than the =
same parameter in all of the other applicable methods. That is precisely wh=
at makes it the most specific method. For example, consider the overloaded =
println methods in the PrintStream and PrintWriter classes. Given a byte ar=
gument, all four of the overloaded println methods in Figure 5.11 are appli=
cable. This is just another way of saying that a byte type argument can be =
converted to an int, long, float, or double (all of which are widening conv=
ersions) in the method invocation conversion context. The println(int) meth=
od is chosen because int is a narrower primitive type than long, float, or =
double. It is that simple (most of the time).




More than One Applicable Method

As stated above, if the applicable methods have more than one parameter, ea=
ch parameter in the most specific applicable method must be narrower than t=
he same parameter in all of the other applicable methods. If there is no on=
e applicable method in which all of the parameters are the narrowest, then =
the method invocation or class instance creation expression is ambiguous. T=
he following example of ambiguity is from the JLS.

class Point { int x, y; }

class ColoredPoint extends Point { int color; }

class Test {
   static void test(ColoredPoint p, Point q) {
      System.out.println("(ColoredPoint, Point)");
   }
   static void test(Point p, ColoredPoint q) {
      System.out.println("(Point, ColoredPoint)");
   }
   public static void main(String[] args) {
      ColoredPoint cp =3D new ColoredPoint();
      test(cp, cp);    // compile-time error
   }
}

Attempting to compile this program generates the following compiler error:
Test.java:13: reference to test is ambiguous, both method test(ColoredPoint=
,Point) in Test
 and method test(Point,ColoredPoint) in Test match
      test(cp, cp);                      // compile-time error
      ^
1 error

In test(ColoredPoint p, Point q) only the first parameter is narrower. Like=
wise, in test(Point p, ColoredPoint q) only the second parameter is narrowe=
r. The method invocation test(ColoredPoint, Colored-Point) in main is there=
fore ambiguous. Note that the javac compiler refers to the applicable metho=
ds in this example as matching methods. This is simply because the rules of=
 overloaded method matching have been applied.=20

Using null as an argument expression is a special case that 15.12.2.2 Choos=
e the Most Specific Method of the JLS does not directly address. I believe =
it should because the resulting behavior is not always intuitive. The Evalu=
ation of Bug Id 4366660 is interesting in this regard. It includes the foll=
owing comment.

I admit that this behavior may be confusing due to the fact that the litera=
l null belongs to all reference types (i.e., the null type is convertible t=
o any reference type). In the more common case, where the argument expressi=
on belongs to a named class or interface type, choosing the most specific m=
ethod generally results in the intuitively expected behavior. [emphasis add=
ed]
This is precisely what makes the use of null as an argument expression a sp=
ecial case. A method with any reference type in the corresponding parameter=
 is applicable. Consider the following example.

class Test {
   public Test(Object o) {
       System.err.println("null is an Object");
   }
   public Test(String s) {
       System.err.println("null is a String");
   }
   public static void main(String[] args) {
       new Test(null);
   }
}

Executing this program prints null is a String because String is narrower t=
han Object. If you find yourself asking why null is a String instead of an =
Object and thinking of this example as arbitrary, then you have fallen into=
 the trap of considering the type of the argument expression. As stated abo=
ve, it is only relevant to overloaded method matching to the extend that it=
 is used to compile the list of applicable methods. The only thing that mat=
ters is that String is narrower than Object. In fact, in examples of overlo=
aded method matching such as this in which there is only one formal paramet=
er, the applicable method with an Object type parameter is never chosen.
As a consequence of the fact that the declaring class is no longer part of =
the overloaded method matching algorithm, it is generally no longer possibl=
e for a method invocation or class instance creation expression that has on=
ly one argument to be ambiguous. It is definitely not possible when the par=
ameter type of the applicable methods is primitive. In the two exceptions t=
o this rule of which I am aware, the parameter types of the applicable meth=
ods are reference types:

Using null as an argument expression
Multiple inheritance in interface hierarchies

What both of these examples have in common is that the formal parameter typ=
es of the applicable methods may be unrelated class or interface types.=20

When null is used as an argument, method invocation or class instance creat=
ion expressions only compile if the parameter types of all the applicable m=
ethods (or constructors) are related. If an unrelated type is thrown into t=
he mix, an ambiguous method error is always generated. For example:

class Test {
   public Test(Object o) {
       System.err.println("null is an Object");
   }
   public Test(String s) {
       System.err.println("null is a String");
   }
   public Test(StringBuffer sb) {
       System.err.println("null is a StringBuffer");
   }
   public static void main(String[] args) {
       new Test(null);
   }
}
Attempting to compile this program generates the following compiler error:
Test.java:12: reference to Test is ambiguous, both method Test(java.lang.St=
ring) in Test and method Test(java.lang.StringBuffer) in Test match
       new Test(null);
       ^
1 error

It is impossible for String to be narrower than StringBuffer or vice versa =
because these are unrelated types.

In an interface type hierarchy involving multiple inheritance, it is also p=
ossible for the applicable methods to be unrelated. For example:

class Test {
   public static void main(String[] args) {
      C c =3D new C() {
         public void f(A a) { }
         public void f(B b) { }
      };
      c.f(c);
   }
}
interface A {}
interface B {}
interface C extends A, B {
   void f(A a);
   void f(B b);
}

Attempting to compile this program generates essentially the same error mes=
sage because interfaces A and B are unrelated; neither extends the other.=20

Note that autoboxing, unboxing, and varargs in the 1.5 =93Tiger=94 release =
will further complicate overloaded method matching. I will not address this=
 issue until both the beta version of the compiler and a draft copy of the =
Third Edition of the JLS is released.





The Declaring Class of Applicable Methods
Prior to the 1.4.2 release, overloaded method matching was much more compli=
cated. Not only did each of the method parameters in the applicable method =
have to be the narrowest, but so did the declaring class (which has been re=
ferred to as the most specific declaring class by at least one software eng=
ineer at Sun). Here is a slightly modified example I came across in a JavaR=
anch forum:

public class Test extends Superclass {
   public static void main(String[] args) {
      new Test().m('x');
   }
   float m(float f) {
     System.out.println("float m(float f)");
        return f;
   }
}
class Superclass {
   char m(char c) {
      System.out.println("char m(char c)");
      return c;
   }
}

Attempting to compile this program in a pre 1.4.2 release generates the fol=
lowing compiler error:
Test.java:7: reference to m is ambiguous, both method m(char) in Superclass=
 and method m(float) in Test match
      new Test().m('x');
      ^
1 error

As of the 1.4.2 release this code compiles. Programmers do not think of cha=
r as being a numeric data type, which makes this example particularly count=
erintuitive.
The question that everyone would like answered is why did it not compile in=
 previous releases? Here is the same example with the method signatures in =
the Test and Superclass reversed:

public class Test extends Superclass {
   public static void main(String[] args) {
      new Test().m('x');
   }
   char m(char c) {
      System.out.println("char m(char c)");
      return c;
   }
}
class Superclass {
   float m(float f) {
     System.out.println("float m(float f)");
        return f;
   }
}

This example compiles in all releases. Why the difference? This is very con=
fusing for application programmers because in both cases m(char c) and m(fl=
oat f) are members of the Test; so why are they treated differently based o=
n their declaring class? The remainder of this section attempts to answer t=
hat question and then explains why the language designers have reversed thi=
s decision.
There has never been an official explanation why the class in which an appl=
icable method is declared was ever part of the overloaded method matching a=
lgorithm. In fact, the Evaluation of Bug Id 4814557 includes the following =
sentence.

There isn't a very good reason why the specification is the way it is with =
respect to this issue.
Not even Neal Gafter knows the precise reason why, and he is the software e=
ngineer responsible for the development of the javac compiler. As stated ab=
ove, I am confident in saying that this was simply a mistake in the origina=
l JLS. It therefore has no logical explanation.=20

The reason overloaded method matching in pre-1.4.2 releases had to be fixed=
 is that requires client programmers know the declaring class of overloaded=
 methods. This =93exposes the structure of the type hierarchy to clients=94=
 In other words, it breaks encapsulation (in the worst kind of way). The pr=
oblem with the example at the top of this section is that the most specific=
 method is in Superclass, which is not allowed under the old rules.

There are two ways to fix this problem. If the client programmer is aware o=
f the problem of using the declaring class in the overloaded method matchin=
g algorithm (which excludes a great many Java programmers), then he or she =
can assign the reference to the superclass type in which the overloaded met=
hod is declared or else use a cast. For example:

public class Test extends Superclass {
   public static void main(String[] args) {
      Test test =3D new Test();
      ((Superclass)test).m('x');=20
      Superclass temp =3D test;=20
      temp.m('x');
   }
   float m(float f) {
     System.out.println("float m(float f)");
        return f;
   }
}
class Superclass {
   char m(char c) {
      System.out.println("char m(char c)");
      return c;
   }
}

The Test program now compiles in all releases and prints the expected char =
m(char c) twice. This baffles application programmers, however, because it =
appears as if m(char c) is not in fact a member of Test. The more common fi=
x is to add a m(char c) method to Test and then to use message forwarding t=
o invoke the same method in Superclass. For example:
Using the declaring class of applicable methods in overloaded method matchi=
ng breaks encapsulation.

public class Test extends Superclass {
   public static void main(String[] args) {
      new Test().m('x');
   }
   char m(char c) {
      return super.m(c);
   }
   float m(float f) {
     System.out.println("float m(float f)");
        return f;
   }
}
class Superclass {
   char m(char c) {
      System.out.println("char m(char c)");
      return c;
   }
}

This amounts to an ugly workaround for a problem that should not exist. Fro=
m a API design standpoint, it is highly undesirable because there is no app=
arent reason for redeclaring the m(char c) method in the subclass. Even mor=
e troubling, this is the only exception to the rule that inherited members =
are exactly the same as members declared in a class or interface. There sho=
uld be no exceptions to that rule.

That this not a theoretical problem can be seen in Bug Ids 4109924 and 4114=
065. Here is the description from the latter:

Graphics2D declares drawString(String, float, float). This overloads Graphi=
cs drawString(String, int, int). But because of the method invocation rules=
, trying to call drawString (String, int, int) on an instance of Graphics2D=
 is declared ambiguous by the compiler.
The intent of this method is to add the capability to specify position with=
 float precision to the drawString method, not to require that clients alwa=
ys explicitly use float precision when using Graphics-2D. Since the other o=
verloads of drawString in Graphics2D are not ambiguous and will promote int=
 to float gracefully, this particular overload stands out as unusually diff=
icult to use.

In fact prior to the 1.3 release, the drawString(String, float, float) in  =
Graphics2D would have to be called as follows.

((Graphics)g2).drawString((String) vector.get(i), 1,
                          (yy +=3D strheight));
This is a modified example from Bug Id 4109924. The problem was solved by r=
edeclaring drawString(String, int, int) in the Graphics2D subclass in the 1=
.3 release.
As stated above, the use of the declaring class in overloaded method matchi=
ng will be dropped as of the 1.4.2 FCS release. This is not a cosmetic chan=
ge to the specification; programs that did not compile prior to the introdu=
ction of the change will now compile. In other words, the language itself h=
as changed, not just the specification. It is a backwards compatible change=
 because it only affects code that previously would not compile. A complex =
mathematical proof attributed to Gilad Bracha that this change is backwards=
 compatible can be found at groups.yahoo.com/group/java-spec-report/message=
/849.
Finally, note that the declaring class of applicable methods was never used=
 when choosing the most specific class method. For example:=20

class Test extends Superclass {
   public static void main(String[] args) {
      Test.print(0);
   }
}
class Baseclass {=20
    public static void print(int i) {
       System.out.println(i);
    }
}
class Superclass extends Baseclass {
    public static void print(long l) {
       System.out.println(l);
    }
}

This is the same example that generated an ambiguous method error at the be=
ginning of this section, only the static modifier has been added to the met=
hod declarations. This program, however, compiles. That is a problem only i=
n that the relevant specification makes no distinction whatsoever between i=
nstance and class methods. The 1.4.2 FCS release will fix this problem beca=
use the declaring class of applicable methods will no longer be part of the=
 overloaded method matching algorithm. In other words, this =93bug=94 will =
become normal behavior (as it should be for class methods).
--------------020904050406010909010407--