mysterious overriding behavior

T

Tony Zuse

Hello everyone,

I always thought the method called depended on the type of the object,
so when I define superclass X and its subclass Y:

class X{
void speak(){
System.out.println("X");
}
}
class Y extends X{
void speak(){
System.out.println("Y");
}
}

Testing yields:

public class Testing{
public static void main(String[] args){
X x=new X();
X xy=new Y();
Y y=new Y();
x.speak(); //prints "X"
xy.speak(); //prints "Y"
y.speak(); //prints "Y"
}
}
.... as expected. However, if I add parameters to the functions:

class X{
void speak(Y y){
System.out.println("X");
}
}
class Y extends X{
void speak(X x){
System.out.println("Y");
}
}

I get a compile time error from xy.speak(x) whereas xy.speak(y) prints
"X", which effectively means java calls the reference-speak and not the
type-speak.

Can anybody explain to me what's happening here?

Thanks, Tony.
 
T

Timothy Bendfelt

In the latter case you are not overriding the method. By narrowing the type
in X::speak(Y y) you eliminated the signature X::speak(X x) and introduced
a similar signature that exists only in Y [Y:speak(X x)].

What I do find odd is that if I override X::speak(Y y) in Y the compiler is
happy and lets you make the call, later if you take out the override *and
compile only the Y class and not the test driver* the VM runs as expected.

Try this in your second case.
class Y extends X{

void speak(X x){
System.out.println("Y");
}

void speak(Y y)
{
System.out.println("YY");

}
}

Once "Testing" compiles against this you can yank out the second method,
compile Y and run the test with the call going to X:speak(Y y);

FYI jdk1.4.1_02 for all.

My head hurts now.
 
M

Mike Schilling

Timothy Bendfelt said:
In the latter case you are not overriding the method. By narrowing the
type
in X::speak(Y y) you eliminated the signature X::speak(X x) and introduced
a similar signature that exists only in Y [Y:speak(X x)].

What I do find odd is that if I override X::speak(Y y) in Y the compiler
is
happy and lets you make the call, later if you take out the override *and
compile only the Y class and not the test driver* the VM runs as expected.

Try this in your second case.
class Y extends X{

void speak(X x){
System.out.println("Y");
}

void speak(Y y)
{
System.out.println("YY");

}
}

Once "Testing" compiles against this you can yank out the second method,
compile Y and run the test with the call going to X:speak(Y y);

FYI jdk1.4.1_02 for all.

My head hurts now.

What woulkd you expect it to do? The call is made at runtime, based on the
information available at runtime.
 
T

Timothy Bendfelt

What would you expect it to do? The call is made at runtime, based on the
information available at runtime.

I'm not versed enough in the JLS to have expected anything. I was just
reacting to subtle difference between what a compiler may allow and what the
runtime executes. On first glance it seemed counter-intuitive to me. Is
javac just being protective here? Would it be legal for a runtime to
generated an exception here? Would it be correct for a compiler to
accept the earlier case that javac rejects? The whole example is sort of
pathological, but interesting.
 
M

Mike Schilling

Timothy Bendfelt said:
I'm not versed enough in the JLS to have expected anything. I was just
reacting to subtle difference between what a compiler may allow and what
the
runtime executes. On first glance it seemed counter-intuitive to me. Is
javac just being protective here? Would it be legal for a runtime to
generated an exception here?

No. The runtime would throw an exception if the method being called
(X:speak(Y y)) weren't found. Since it is found, no exception is generated.
Would it be correct for a compiler to
accept the earlier case that javac rejects?

No. The JLS is quite clear here: the compile-time check is based on the
declared type of the object.
..
The whole example is sort of pathological, but interesting.

I'll go along with that :) It's also instructive, as it helps distingish
what's known at compile time, and thus expected by the generated bytecode
(in this case, that X:speak(Y y) exists) vs. what's not known until runtime
(in this case, whether Y overrides speak(Y y) or not.)
 
T

Tony Zuse

Timothy said:
In the latter case you are not overriding the method. By narrowing the type
in X::speak(Y y) you eliminated the signature X::speak(X x) and introduced
a similar signature that exists only in Y [Y:speak(X x)].
I can see that I am not overriding since the signatures are different.
What I don't see, however, is why xy.speak(x) would not invoke
Y::speak(X x).

"...when the virtual machine invokes an instance method, it selects the
method to invoke based on the actual class of the object..."
(JavaWorld.com).

So shouldn't xy of class Y invoke Y::speak(X x) in any case? Or why is
reference type decisive in this case?
 
T

Timothy Bendfelt

No. The runtime would throw an exception if the method being called
(X:speak(Y y)) weren't found. Since it is found, no exception is generated.


No. The JLS is quite clear here: the compile-time check is based on the
declared type of the object.
.

This is the part I found odd (at first blush). I did not do a good job of
reposting the code from the original question but in the source that javac
rejected Y *does* have the required method-- it inherits it from X. The
compiler rejects this. If you override the method (even just to call
super.speak(y)) the compiler allows it. It seems like with the information
available at compile time the same issue exists even with the method
explicitly placed in the subclass. Could the compiler reject both?


public class Test{
public static void main(String[] args){
Y y=new Y();
y.speak(y);
}
}

class X{
void speak(Y y){
System.out.println("X");
}
}

class Y extends X{

void speak(X x){
System.out.println("Y");
}

//toggle this override on/off with comments
void speak(Y y)
{
super.speak(this);
}
}


I guess I do see the difference somewhat, but I am still left wondering-- if
the compiler is allowed to choose the narrower method in the latter case, why
is it disallowed this in the first case? Either way it gets bound to the
narrower method type.
 
T

Timothy Bendfelt

Timothy said:
In the latter case you are not overriding the method. By narrowing the type
in X::speak(Y y) you eliminated the signature X::speak(X x) and introduced
a similar signature that exists only in Y [Y:speak(X x)].
I can see that I am not overriding since the signatures are different.
What I don't see, however, is why xy.speak(x) would not invoke
Y::speak(X x).

"...when the virtual machine invokes an instance method, it selects the
method to invoke based on the actual class of the object..."
(JavaWorld.com).

So shouldn't xy of class Y invoke Y::speak(X x) in any case? Or why is
reference type decisive in this case?

I see your question. The compiler is helping you on purpose. It does
rely on the reference type and you are calling the method on a type X
which does not have this signature. In this example it does not seem
helpful, but this is desired behavior for type safety and information
hiding and probably a host of things I know nothing about. Also in other
cases the compiler would not be able to know the narrower type. When you
say ((X)y).speak(); you mean it. Similarly when you say X xy = y; you
(usually ;) mean it.
 
L

Lew

Mike said:
I'll go along with that :) It's also instructive, as it helps distingish
what's known at compile time, and thus expected by the generated bytecode
(in this case, that X:speak(Y y) exists) vs. what's not known until runtime
(in this case, whether Y overrides speak(Y y) or not.)

Particularly, the pathology is that superclass X has a method that accepts a
parm of type subclass Y. As a design principle, do not hard-code subtype
knowledge into a parent class.

- Lew
 
L

Lew

Tony said:
I can see that I am not overriding since the signatures are different.
What I don't see, however, is why xy.speak(x) would not invoke
Y::speak(X x).

xy is of type X. At compile time there is no X.speak(X). Variables of type X,
like xy, cannot invoke methods of subtypes. They can only invoke methods of
type X. It is an X method that gets delegated to a Y instance at runtime, but
it is still an X method. In your example there is no X method to call, and
there are no Y overrides of X methods. Therefore you must not "expect" Y to
somehow magically appear in X's behavior.

There is no X.speak(X), so the X-typed xy cannot call it.

This is part of why superclasses should not hard-code knowledge of subclasses.

- Lew
 
T

Tony Zuse

xy is of type X.

But I created it as "X xy =new Y". So isn't it an object of type Y with
a reference of type X? I could even take y of Y and simply attach an X
reference to it ("X xy=y"), thus in no way manipulating the object.
Java would still search X for a method definition.

If you check my first message, you will see that in the no-parameter
case, xy was actually invoking the Y::speak(). So here JVM obviously
recognizes it as a type Y object, no?

It's just that all my sources (JavaWorld, Arnold/Gosling) make this
distinction of actual type and reference type and go on to say "the
actual class of the object , not the type of the reference, governs
which version of the method is called" (A/G, p.69). Only as I
introduce parameters to the methods, this no longer seems to hold true.

I'm sorry about the subclass parameter in the superclass. The original
code involved another class C and its subclass D, but I was trying to
keep my citations short. I don't think it makes a difference to my
question.
 
M

Mike Schilling

It's just that all my sources (JavaWorld, Arnold/Gosling) make this
distinction of actual type and reference type and go on to say "the
actual class of the object , not the type of the reference, governs
which version of the method is called" (A/G, p.69). Only as I
introduce parameters to the methods, this no longer seems to hold true.

This is true when discussing the type of the object on which the method is
being called. For the parameters, on the other hand, all that matters is
their declared type.
 
T

Tony Zuse

This is true when discussing the type of the object on which the method is
being called. For the parameters, on the other hand, all that matters is
their declared type.

But in the case of xy.speak(x) that concerns me, the method is called
on xy. So if xy's type is decisive, then Y::speak(X x) should be
called, just as Y:speak() was called in the earlier example, no?

To me, G/A's argument doesn't seem to hold here.
 
L

Lew

Tony said:
But in the case of xy.speak(x) that concerns me, the method is called
on xy. So if xy's type is decisive, then Y::speak(X x) should be
called, just as Y:speak() was called in the earlier example, no?

To me, G/A's argument doesn't seem to hold here.

You mistake the variable for the object.

The variable xy is of type X. There is no way for that variable to access
methods not of type X. You cannot directly access type Y methods from variable
xy because the variable has no knowledge of that method signature.

Once you cast the "new Y()" up to an X variable, you lost all knowledge of Y
declarations. Only X declarations remain available to the variable. So xy
cannot call speak(X x) because there is no such method.

It can call speak() because X does define such a method. The version of the X
method that runs is the override.

Y.speak( X x ) is not an override. It can only be an override if there is a
superclass method with the same signature, which there is not. Since there is
no corresponding superclass method, the superclass-typed variable cannot call it.

xy's "type is decisive" only on methods that can be reached. So, no.

The declaration of the xy *variable* as type X throws away every expression
not in the definition of X. Method speak(X) cannot be called from the X
*variable*, so it matters not what the run-time *object* type has available.

- Lew
 
T

Timothy Bendfelt

After another look I see that I created a completely new test case. That
is what I found odd and actually I think a bug in javac 1.4.1_02 which is
the jdk that refuses to compile my test case unless the method is
overridden in Y.

Jikes 1.22 javac 1.5 and 1.6 all accept the code. I guess my surprise was
was warranted, albeit completely unrelated to the original post.

Sorry for the red herring.



No. The runtime would throw an exception if the method being called
(X:speak(Y y)) weren't found. Since it is found, no exception is generated.


No. The JLS is quite clear here: the compile-time check is based on the
declared type of the object.
.

This is the part I found odd (at first blush). I did not do a good job of
reposting the code from the original question but in the source that javac
rejected Y *does* have the required method-- it inherits it from X. The
compiler rejects this. If you override the method (even just to call
super.speak(y)) the compiler allows it. It seems like with the information
available at compile time the same issue exists even with the method
explicitly placed in the subclass. Could the compiler reject both?


public class Test{
public static void main(String[] args){
Y y=new Y();
y.speak(y);
}
}

class X{
void speak(Y y){
System.out.println("X");
}
}

class Y extends X{

void speak(X x){
System.out.println("Y");
}

//toggle this override on/off with comments
void speak(Y y)
{
super.speak(this);
}
}


I guess I do see the difference somewhat, but I am still left wondering-- if
the compiler is allowed to choose the narrower method in the latter case, why
is it disallowed this in the first case? Either way it gets bound to the
narrower method type.
 
M

Mike Schilling

Timothy Bendfelt said:
After another look I see that I created a completely new test case. That
is what I found odd and actually I think a bug in javac 1.4.1_02 which is
the jdk that refuses to compile my test case unless the method is
overridden in Y.

Jikes 1.22 javac 1.5 and 1.6 all accept the code. I guess my surprise was
was warranted, albeit completely unrelated to the original post.

Sorry for the red herring.

This sounds interesting. Can you post the precise code that demonstrates
this failure?
 
T

Timothy Bendfelt

This sounds interesting. Can you post the precise code that demonstrates
this failure?

The code is posted in the thread already. The case that I introduced (not
the original posters case) was the case of calling y.speak(y). The 1.4
javac compiler did not like this because Y also defined Y::speak(X). The
specific msg follows.
Error:line (23)reference to speak is ambiguous, both method speak(_foo.Y)
in _foo.X and method speak(_foo.X) in _foo.Y match
<<

The thing I found odd was that if I explicitly define Y::speak(Y) by
overriding it in Y the compiler gets happy again. This is what I found
odd originally.

Here is is the code that 1.4.1_02 javac rejects. Uncommenting the override
makes it happy. 1.5, 1.6 take it either way.


class X{
void speak(Y y){
System.out.println("X");
}
}


class Y extends X{

void speak(X x){
System.out.println("Y");
}

// void speak(Y y)
// {
// super.speak(this);
// System.out.println("YY");
// }
}


public class Test{

public static void main(String[] args){
X x=new X();
X xy=new Y();
Y y=new Y();
xy.speak(y);
y.speak(x);
y.speak(y);//the 1.4 compiler hates this
}
}
 
M

Mike Schilling

Timothy Bendfelt said:
On Fri, 15 Dec 2006, Mike Schilling wrote:

The code is posted in the thread already. The case that I introduced (not
the original posters case) was the case of calling y.speak(y). The 1.4
javac compiler did not like this because Y also defined Y::speak(X). The
specific msg follows.
Error:line (23)reference to speak is ambiguous, both method speak(_foo.Y)
in _foo.X and method speak(_foo.X) in _foo.Y match
<<

The thing I found odd was that if I explicitly define Y::speak(Y) by
overriding it in Y the compiler gets happy again. This is what I found
odd originally.

Here is is the code that 1.4.1_02 javac rejects. Uncommenting the override
makes it happy. 1.5, 1.6 take it either way.


class X{
void speak(Y y){
System.out.println("X");
}
}


class Y extends X{

void speak(X x){
System.out.println("Y");
}

// void speak(Y y)
// {
// super.speak(this);
// System.out.println("YY");
// }
}


public class Test{

public static void main(String[] args){
X x=new X();
X xy=new Y();
Y y=new Y();
xy.speak(y);
y.speak(x);
y.speak(y);//the 1.4 compiler hates this
}
}

OK. This was actually a change to the language definition; you can see the
difference between the description of overload resolution in JLS 2 and JLS
3. Oddly, the change seems to have been made to javac between 1.4.1 and
1.4.2 with no announcement.

To understand it, you need the concept of "more specific". Consider the
following: (note that none of the code here has been compiled or tested, so
typos are inevitable)

class A {

void method(String s) ...
void method(Object o) ...

main() {
method("xxx"); // which is called?
}
}

Obviously, either variant of "method" could be called, because "xxx" is both
an Object and a String. method(String) actually is called, because it has
the most specific parameter: all Strings are Objects, but not all Objects
are Strings.

The rule in 1.4.1 and earlier is that if more than one overload is possible,
to look for one that's the most specific both in all of its arguments and in
the type that defines it. If there is one, call it. If not, the call is
ambiguous. That's why your call is ambiguous:

Y::speak(X) is more specific in the class that defines it
X::speak(Y) is more specific in the parameter type

Now, if you define

Y::speak(Y)

you have a method that's most specific in both, so the compiler likes it.

Starting in 1.4.2, the rule about defining types is eliminated; all that's
checked in the parameter types. That is,

X::speak(Y) is more specific in the parameter type, and that's all that
matters

Thus X::speak(Y) is called.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,774
Messages
2,569,599
Members
45,169
Latest member
ArturoOlne
Top