论坛首页 Java版

java的动态绑定和方法重载

浏览 3959 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
最后更新时间:2005-07-08
父类Father:

[code:1]public class Father {

       public Father() {
        super();
       
    }
    public void shout(Object word){
        System.out.println("object father shout:"+word);
    }

}[/code:1]

子类Son:

[code:1]public class Son extends Father{


    public Son() {
        super();
       
    }
    public void shout(String word){
        System.out.println("string son shout:"+word);
    } 
}[/code:1]

ok,我们来看一下测试:

[code:1]public class TestOverload {

  
    public static void main(String[] args) {
        Son son=new Son();
        Father father=son;
       
        son.shout("hello");
        father.shout("hello");
    }
}[/code:1]

结果:

[code:1]string son shout:hello
object father shout:hello[/code:1]

测试中father son是一个人,喊出来的结果却不一样

问题的关键在于,java动态绑定的实现机制。在绑定的过程中,确实考虑了静态类型而不是简单的只看对象实际类型,否则不会出现这样的结果。

缺少资料,我只能初步猜测动态绑定时先到静态类型里匹配方法,找不到再去实际类型里匹配。

在这里,father.shout()会匹配到Father的方法,这里存在参数向上转型。

这个问题是在看visitor模式时想到的,把阎博士的那本书反复看了好多遍,觉得对于单分派多分派还是没有讲清楚,自己动手试了一下,就碰到这个问题,我也不知道该怎么解释了还望高手解惑

我想,这里有这样几个问题:

1 动态绑定机制,即多态。对象的方法调用会绑定到对象实际类型的方法 这个问题实际上体现了java是单分派的语言。
2 方法(函数)的匹配方式或过程,对于这个我不太明白,如果知道一些细节应该就可以解释上面的问题了。
3 方法中参数的匹配方式。java是单分派语言,对于参数不会进行分派,也就是不会根据参数的实际类型来绑定方法,而是根据静态类型绑定。但这里还有一种情况,就是向上转型,这与分派没有关系,但是容易使人混淆。总之,java对方法参数不会根据实际类型来分派但会存在向上转型。

实际上,我们可以针对以上3个问题都写出测试。
1不用说了,大家应该都很熟悉。
3中的向上转型也常见,反而是根据静态类型绑定我不怎么见到。(指的是对某方法重载,而2个方法只有一个参数不同,这2个参数又有继承关系,比如String和object)不知道我说清楚了没有就像上面那个例子中的2个shout方法,放到一个类中声明,然后调用时传String或object(静态类型)绑定的方法是不同的
2就是上面写的那个测试提出的问题。我想不太清楚,这个例子故意把3个问题揉到一起了

以前刚学java的时候,死死记着:可以使用父类的地方一定可以使用子类,而多态会保证根据实际类型调用正确的方法。就这么形成了习惯,以为在哪都是这样,还真不太适应参数的静态绑定,唉,不求甚解阿

基础不扎实,胡言乱语了一些,大家来讨论讨论
   
最后更新时间:2005-07-08
这种重载挺特殊啊~没想到为什么~

这个是应该和参数有关系吧,希望明白的能解释一下定位方法的具体的细节:)

PS:java的reference是不是只是存了地址信息啊~我总感觉它好像是能存储其它一些有用信息的,比如引用类型什么的,这样在判断的时候方便啊,在网上找了半天引用的结构也没找到:(,这是我的猜测,希望大家能指正。
   
0 请登录后投票
最后更新时间:2005-07-08
参数类型不一样,就是两个完全不同的方法。

这是一个编译期的选择问题。
调用哪个方法签名(method signature,包括方法名,参数列表),在编译期就已经确定了。
father 没有 shout(string) 方法,编译器自然选择 shout(object)。

假设father 有2个方法。
shout(object) {print "object"};
shout(string) {print "string"};

那么

String a = "a";
father.shout(a);  //  string

Object a = "a";
father.shout(a); // object

String[] a = null;
father.shout(a); // object

BigDecimal a = null;
father.shout(a); // object
   
0 请登录后投票
最后更新时间:2005-07-08
我对“编译期就确定了”理解起来模糊。因为我觉得编译期不能完全确定调用哪个方法签名,如果完全确定了,多态就不存在了。我觉得这里是不是存在一个顺序问题?就是先静态后动态。当然一般不会区分得那么清楚,但要解释我这个特殊的例子就要区分清楚一点。
在这个例子里,对象就一个,是son,而我将它的声明类型分别作为father、son时,却调用了不同的方法,正是这一点令我疑惑。当然这还是函数签名搞了鬼,所以问题的实质还是匹配函数的过程。我以为是有一个先后顺序问题的。先匹配静态的,到father类里匹配,即使找不到编译期的工作也算完了,剩下的是动态绑定的事情了。因为我们可以把father类的shout方法去掉,这时2次调用就都会匹配到son的shout上面。这就让我感觉匹配了father的只是优先选择了它,而不是只能选择它。
我对jvm较底层的工作方式不了解,有错误之处请指正。
   
0 请登录后投票
最后更新时间:2005-07-08
这还涉及不到jvm底层,主要是一些编译原理方面的体会。
你可以设想一下,如果你实现javac,如何编译那个程序?如何根据那些语句,生成jvm指令?

关于这类问题,可以使用 javap 察看编译后的jvm伪指令。
javap -verbose TestOverload > 1.txt

[code:1]
Compiled from "TestOverload.java"
public class TestOverload extends java.lang.Object
  SourceFile: "TestOverload.java"
  minor version: 0
  major version: 0
  Constant pool:
const #1 = Asciz TestOverload;
const #2 = class #1; //  TestOverload
const #3 = Asciz java/lang/Object;
const #4 = class #3; //  Object
const #5 = Asciz <init>;
const #6 = Asciz ()V;
const #7 = Asciz Code;
const #8 = NameAndType #5:#6;//  "<init>":()V
const #9 = Method #4.#8; //  java/lang/Object."<init>":()V
const #10 = Asciz LineNumberTable;
const #11 = Asciz LocalVariableTable;
const #12 = Asciz this;
const #13 = Asciz LTestOverload;;
const #14 = Asciz main;
const #15 = Asciz ([Ljava/lang/String;)V;
const #16 = Asciz Son;
const #17 = class #16; //  Son
const #18 = Method #17.#8; //  Son."<init>":()V
const #19 = Asciz hello;
const #20 = String #19; //  hello
const #21 = Asciz shout;
const #22 = Asciz (Ljava/lang/String;)V;
const #23 = NameAndType #21:#22;//  shout:(Ljava/lang/String;)V
const #24 = Method #17.#23; //  Son.shout:(Ljava/lang/String;)V
const #25 = Asciz Father;
const #26 = class #25; //  Father
const #27 = Asciz (Ljava/lang/Object;)V;
const #28 = NameAndType #21:#27;//  shout:(Ljava/lang/Object;)V
const #29 = Method #26.#28; //  Father.shout:(Ljava/lang/Object;)V
const #30 = Asciz args;
const #31 = Asciz [Ljava/lang/String;;
const #32 = Asciz son;
const #33 = Asciz LSon;;
const #34 = Asciz father;
const #35 = Asciz LFather;;
const #36 = Asciz SourceFile;
const #37 = Asciz TestOverload.java;

{
public TestOverload();
  Code:
   Stack=1, Locals=1, Args_size=1
   0: aload_0
   1: invokespecial #9; //Method java/lang/Object."<init>":()V
   4: return
  LineNumberTable:
   line 2: 0
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      5      0    this       LTestOverload;

public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=3, Args_size=1
   0: new #17; //class Son
   3: dup
   4: invokespecial #18; //Method Son."<init>":()V
   7: astore_1
   8: aload_1
   9: astore_2
   10: aload_1
   11: ldc #20; //String hello
   13: invokevirtual #24; //Method Son.shout:(Ljava/lang/String;)V
   16: aload_2
   17: ldc #20; //String hello
   19: invokevirtual #29; //Method Father.shout:(Ljava/lang/Object;)V
   22: return
  LineNumberTable:
   line 5: 0
   line 6: 8
   line 8: 10
   line 9: 16
   line 10: 22
  LocalVariableTable:
   Start  Length  Slot  Name   Signature
   0      23      0    args       [Ljava/lang/String;
   8      15      1    son       LSon;
   10      13      2    father       LFather;

}
[/code:1]

注意看main函数的编译结果

[code:1]
public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=3, Args_size=1
   0: new #17; //class Son
   3: dup
   4: invokespecial #18; //Method Son."<init>":()V
   7: astore_1
   8: aload_1
   9: astore_2
   10: aload_1
   11: ldc #20; //String hello
   13: invokevirtual #24; //Method Son.shout:(Ljava/lang/String;)V
   16: aload_2
   17: ldc #20; //String hello
   19: invokevirtual #29; //Method Father.shout:(Ljava/lang/Object;)V
   22: return
[/code:1]

可以看到,编译期已经确定了 method signature。注意方法签名里面的参数类型。

   13: invokevirtual #24; //Method Son.shout:(Ljava/lang/String;)V

   19: invokevirtual #29; //Method Father.shout:(Ljava/lang/Object;)V

另外,  多态,是在 invokevirtual 这个指令里面实现的。
   
0 请登录后投票
最后更新时间:2005-07-09
http://java.sun.com/docs/books/jls/third_edition/html/classes.html#8.4.9

引用

8.4.9 Overloading
If two methods of a class (whether both declared in the same class, or both inherited by a class, or one declared and one inherited) have the same name but signatures that are not override-equivalent, then the method name is said to be overloaded. This fact causes no difficulty and never of itself results in a compile-time error. There is no required relationship between the return types or between the throws clauses of two methods with the same name, unless their signatures are override-equivalent.
Methods are overridden on a signature-by-signature basis.


If, for example, a class declares two public methods with the same name, and a subclass overrides one of them, the subclass still inherits the other method.

When a method is invoked (§15.12), the number of actual arguments (and any explicit type arguments) and the compile-time types of the arguments are used, at compile time, to determine the signature of the method that will be invoked (§15.12.2). If the method that is to be invoked is an instance method, the actual method to be invoked will be determined at run time, using dynamic method lookup (§15.12.4).


http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#292575

引用

15.12.2 Compile-Time Step 2: Determine Method Signature
The second step searches the type determined in the previous step for member methods. This step uses the name of the method and the types of the argument expressions to locate methods that are both accessible and applicable, that is, declarations that can be correctly invoked on the given arguments. There may be more than one such method, in which case the most specific one is chosen. The descriptor (signature plus return type) of the most specific method is one used at run time to perform the method dispatch.
A method is applicable if it is either applicable by subtyping (§15.12.2.2), applicable by method invocation conversion (§15.12.2.3), or it is an applicable variable arity method (§15.12.2.4).

The process of determining applicability begins by determining the potentially applicable methods (§15.12.2.1). The remainder of the process is split into three phases.
   
0 请登录后投票
最后更新时间:2005-07-10
建议:

(1) 尽量避免 overload 重载。
(2) 如果overload 重载了,尽量避免这样的容易产生歧义的、让编译器难以决定的代码。比如,

son.shout(null);

你在为难编译器。因为这里shout(Object), shout(String) 都是可选的。
编译器不太清楚你要做什么,它只能大致猜测,替你选择shout(String) ,而不是shout(Object).


要这么写

Object o = null;
son.shout(o);

String o = null;
son.shout(o);

这样的写法,编译器清楚地知道,你需要调用哪个方法。

http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#15.12.2.10
引用

15.12.2.10 Example: Overloading Ambiguity
Consider the example:
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 = new ColoredPoint();
                test(cp, cp);                                                                                   // compile-time error
        }
}
This example produces an error at compile time. The problem is that there are two declarations of test that are applicable and accessible, and neither is more specific than the other. Therefore, the method invocation is ambiguous.
If a third definition of test were added:
       static void test(ColoredPoint p, ColoredPoint q) {
                System.out.println("(ColoredPoint, ColoredPoint)");
        }
then it would be more specific than the other two, and the method invocation would no longer be ambiguous.
   
0 请登录后投票
最后更新时间:2005-07-09
buaawhl 写道
建议:

(1) 尽量避免 overload 重载。
(2) 如果overload 重载了,尽量避免这样的容易产生歧义的、让编译器难以决定的代码。比如,

son.shout(null);

你在为难编译器。因为这里shout(Object), shout(String) 都是可选的。
编译器不太清楚你要做什么,它只能大致猜测,替你选择shout(String) ,而不是shout(Object).


要这么写

Object o = null;
son.shout(o);

String o = null;
son.shout(o);

这样的写法,编译器清楚地知道,你需要调用哪个方法。


这还不到编译器那里吧,这在JVM那里就应该已经被认为是两个不同的方法签名了。
他们的参数的型别是不一样的。
   
0 请登录后投票
最后更新时间:2005-07-09
firebody 写道


这还不到编译器那里吧,这在JVM那里就应该已经被认为是两个不同的方法签名了。
他们的参数的型别是不一样的。


应该先编译在执行啊~编译的时候不会和JVM打交道吧,应该到了执行class的时候才是JVM该上场的时间吧~
   
0 请登录后投票
最后更新时间:2005-07-09
引用
另外, 多态,是在 invokevirtual 这个指令里面实现的。


对这句话我的理解是:在编译期间能够确定的方法调用就直接写到我们要执行的class文件中去了(这应该也和编译器的设计相关吧,会涉及到编译器的优化等问题),等到执行的时候就直接调用invokevirtual方法即可,而不能直接确定的方法,需要在运行时刻才能够确定的方法我们就会在运行时检查相关的状态,然后进行对应的调用。

而我们使用javap 所看的指令只是编译好的确定的这一部分(也就是.class文件),也就算是静态的编码,而实际在运行中的编码我们看不到,比如说能够在运行时期随时察看该程序对堆的使用情况等。
   
0 请登录后投票
论坛首页 Java版

跳转论坛:
JavaEye推荐