java中用new创建一个对象的过程解析

java中用new创建一个对象的过程解析


对于用new 创建一个对象,我们需要弄清楚它的过程:
引用和创建一个对象的格式是:

类名 变量名;

变量名=new 类名(参数列表);

比如 Vehicle veh1=new Vehicle();

这个语句具体的执行过程是:

1.右边的“new vheicle"是以vehicle类为模板,在堆空间里创建一个vehicle类对象(也简称vehicle对象)。

2.末尾的()意味着,在对象创建后,立即调用vehicle类的构造函数,对刚生成的对象进行初始化。构造函数肯定是有的,如果没有创建,java会补上一个默认的构造函数。

3.左边的'Vehicle veh1'创建了一个vehicle类引用变量

4.“=”操作符使对象引用指向刚创建的Vehicle对象。


将上面的语句分为两个步骤:

Vechicle veh1;

veh1=new  Vechicle;

 这样写,就比较清楚了,有两个实体:一是对象引用变量,一是对象本身。在堆空间里创建的实体,与在栈空间里创建的实体不同。尽管它们也是确确实实存在的实体,但是似乎很难准确的“抓”住它。


/*
SubClass sub = new SubClass();
这句话到底做了什么事情呢?
1.javac编译.java源文件形成.class字节码文件;
2.new SubClass()对象时,先检查有没有父类,有父类,类加载器(ClassLoader)先将父类的Class文件读入内存,创建一个java.lang.Class对象,然后加载子类,类加载器将子类的Class文件读入内存,创建一个java.lang.Class对象;
3.先初始化父类的静态属性,再初始化父类的静态代码块;
4.再初始化子类的静态属性,再初始化子类的静态代码;
5.在堆内存中分配内存空间,分配内存地址,此时是因为父类的特有属性才在堆内存中为父类对象分配空间。
6.初始化父类的特有属性。
7.初始化父类的构造代码块。
8.初始化父类对象相应的构造方法。
9.在堆内存中分配内存空间,分配内存地址,此时是因为子类的特有属性才在堆内存中为子类对象分配空间的。
10.初始化子类的特有属性。
11.初始化子类的构造代码块。
12.初始化子类相应的构造方法。
13.将子类的内存地址赋值给栈中的引用对象。
*/
package com.zhangyike.staticExcise;

// 静态变量

    public static String staticField = "父类--静态变量";

    public String field = "父类--普通变量";

    // 静态块
    static {
        System.out.println(staticField);
        System.out.println("父类--静态块");
    }

    // 初始化块
    {
        System.out.println(field);
        System.out.println("父类--普通块");
    }

    // 构造器
    public ParentClass() {
        System.out.println("父类--构造器");
    }
}

public class SubClass extends ParentClass {

    // 静态变量
    public static String sstaticField = "子类--静态变量";
    // 变量
    public String sField = "子类--变量";

    // 静态块
    static {
        System.out.println(sstaticField);
        System.out.println("子类--静态块");
    }

    // 初始化块
    {
        System.out.println(sField);
        System.out.println("子类--普通块");
    }

    // 构造器
    public SubClass() {
        System.out.println("子类--构造器");
    }

    public static void main(String[] args) {

        System.out.println("顺序:" + "第一次new SubClass");
        SubClass sub = new SubClass();
        System.out.println("顺序:" + "第二次new SubClass");
        new SubClass();
    }
}

程序执行的结果为:
父类–静态变量
父类–静态块
子类–静态变量
子类–静态块
顺序:第一次new SubClass
父类–普通变量
父类–普通块
父类–构造器
子类–变量
子类–普通块
子类–构造器
顺序:第二次new SubClass
父类–普通变量
父类–普通块
父类–构造器
子类–变量
子类–普通块
子类–构造器

====================================================================================================================================


本文大部分内容来自于IBM的博文多态在 Java 和 C++ 编程语言中的实现比较 。这里写一遍主要是加深自己的理解,方便以后查看,加入了一些自己的见解及行文组织,不是出于商业目的,如若需要下线,请告知。

结论

基于基类的调用和基于接口的调用,从性能上来讲,基于基类的调用性能更高 。因为invokevirtual是基于偏移量的方式来查找方法的,而invokeinterface是基于搜索的。

概述

多态是面向对象程序设计的重要特性。多态允许基类的引用指向派生类的对象,而在具体访问时实现方法的动态绑定。
java对方法动态绑定的实现方法主要基于方法表,但是这里分两种调用方式invokevirtual和invokeinterface,即类引用调用和接口引用调用。类引用调用只需要修改方法表的指针就可以实现动态绑定(具有相同签名的方法,在父类、子类的方法表中具有相同的索引号),而接口引用调用需要扫描整个方法表才能实现动态绑定(因为,一个类可以实现多个接口,另外一个类可能只实现一个接口,无法具有相同的索引号。这句如果没有看懂,继续往下看,会有例子。写到这里,感觉自己看书时,有的时候也会不理解,看不懂,思考一段时间,还是不明白,做个标记,继续阅读吧,然后回头再看,可能就豁然开朗。)。
类引用调用的大致过程为:java编译器将java源代码编译成class文件,在编译过程中,会根据静态类型将调用的符号引用写到class文件中。在执行时,JVM根据class文件找到调用方法的符号引用,然后在静态类型的方法表中找到偏移量,然后根据this指针确定对象的实际类型,使用实际类型的方法表,偏移量跟静态类型中方法表的偏移量一样,如果在实际类型的方法表中找到该方法,则直接调用,否则,按照继承关系从下往上搜索。
下面对上面的描述做具体的分析讨论。

JVM的运行时结构

这里写图片描述
从上图可以看出,当程序运行时,需要某个类时,类载入子系统会将相应的class文件载入到JVM中,并在内部建立该类的类型信息,这个类型信息其实就是class文件在JVM中存储的一种数据结构,他包含着java类定义的所有信息,包括方法代码,类变量、成员变量、以及本博文要重点讨论的方法表<喎�"/kf/ware/vc/" target="_blank" class="keylink">vc3Ryb25nPqGj1eK49sDg0M3Qxc+ivs205rSi1Nq3vbeox/ihozxiciAvPg0K16LS4qOs1eK49re9t6jH+NbQtcTA4NDN0MXPorj61Nq20dbQtOa3xbXEY2xhc3O21M/zyseyu82stcSho9Tat723qMf41tCjrNXiuPZjbGFzc7XEwODQzdDFz6LWu9PQzqjSu7XEyrXA/aOoy/nS1MrHuPe49s/fs8y5ss/ttcTE2rTmx/jT8qOpo6y2+NTattHW0L/J0tTT0LbguPa4w2NsYXNzttTP86Gjv8nS1M2ouf220dbQtcRjbGFzc7bUz/O3w87Ktb23vbeox/jW0MDg0M3Qxc+ioaO+zc/x1NpqYXZht7TJ5Lv61sbEx9H5o6zNqLn9Y2xhc3O21M/zv8nS1LfDzsq1vbjDwOC1xMv509DQxc+i0rvR+aGjPGJyIC8+DQq3vbeose3Kx8q1z9a2r8ystffTw7XEusvQxKGjt723qLHttOa3xdTat723qMf41tC1xMDg0M3Qxc+i1tCho7e9t6ix7dbQtOa3xdPQuMPA4Lao0uW1xMv509C3vbeovLDWuM/yt723qLT6wuu1xNa41euho9Xi0Km3vbeo1tCw/MCotNO4uMDgvMyz0LXEy/nT0Le9t6jS1Lyw19TJ7dbY0LSjqG92ZXJyaWRlo6m1xLe9t6ihozwvcD4NCjxoMiBpZD0="类引用调用invokevirtual">类引用调用invokevirtual

代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<code class = "hljs java" > package org.fan.learn.methodTable;
 
/**
  * Created by fan on 2016/3/30.
  */
public class ClassReference {
     static class Person {
         @Override
         public String toString(){
             return "I'm a person." ;
         }
         public void eat(){
             System.out.println( "Person eat" );
         }
         public void speak(){
             System.out.println( "Person speak" );
         }
 
     }
 
     static class Boy extends Person{
         @Override
         public String toString(){
             return "I'm a boy" ;
         }
         @Override
         public void speak(){
             System.out.println( "Boy speak" );
         }
         public void fight(){
             System.out.println( "Boy fight" );
         }
     }
 
     static class Girl extends Person{
         @Override
         public String toString(){
             return "I'm a girl" ;
         }
         @Override
         public void speak(){
             System.out.println( "Girl speak" );
         }
         public void sing(){
             System.out.println( "Girl sing" );
         }
     }
 
     public static void main(String[] args) {
         Person boy = new Boy();
         Person girl = new Girl();
         System.out.println(boy);
         boy.eat();
         boy.speak();
         //boy.fight();
         System.out.println(girl);
         girl.eat();
         girl.speak();
         //girl.sing();
     }
}
</code>

注意,boy.fight();girl.sing(); 这两个是有问题的,在IDEA中会提示“Cannot resolve method ‘fight()’”。因为,方法的调用是有静态类型检查的,而boy和girl的静态类型都是Person类型的,在Person中没有fight方法和sing方法。因此,会报错。
执行结果如下:
这里写图片描述
从上图可以看到,boy.eat()girl.eat() 调用产生的输出都是”Person eat”,因为Boy和Girl中没有override 父类的eat方法。
字节码指令:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<code class = "hljs scss" > public static void main(java.lang.String[]);
   Code:
    Stack= 2 , Locals= 3 , Args_size= 1
    0 :   new     # 2 ; //class ClassReference$Boy
    3 :   dup
    4 :   invokespecial   # 3 ; //Method ClassReference$Boy."<init>":()V
    7 :   astore_1
    8 :   new     # 4 ; //class ClassReference$Girl
    11 :  dup
    12 :  invokespecial   # 5 ; //Method ClassReference$Girl."<init>":()V
    15 :  astore_2
    16 :  getstatic       # 6 ; //Field java/lang/System.out:Ljava/io/PrintStream;
    19 :  aload_1
    20 :  invokevirtual   # 7 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    23 :  aload_1
    24 :  invokevirtual   # 8 ; //Method ClassReference$Person.eat:()V
    27 :  aload_1
    28 :  invokevirtual   # 9 ; //Method ClassReference$Person.speak:()V
    31 :  getstatic       # 6 ; //Field java/lang/System.out:Ljava/io/PrintStream;
    34 :  aload_2
    35 :  invokevirtual   # 7 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    38 :  aload_2
    39 :  invokevirtual   # 8 ; //Method ClassReference$Person.eat:()V
    42 :  aload_2
    43 :  invokevirtual   # 9 ; //Method ClassReference$Person.speak:()V
    46 return </init></init></code>

其中所有的invokevirtual调用的都是Person类中的方法。

下面看看java对象的内存模型:
这里写图片描述
从上图可以清楚地看到调用方法的指针指向。而且可以看出相同签名的方法在方法表中的偏移量是一样的。这个偏移量只是说Boy方法表中的继承自Object类的方法、继承自Person类的方法的偏移量与Person类中的相同方法的偏移量是一样的,与Girl是没有任何关系的。

下面再看看调用过程,以girl.speak() 方法的调用为例。在我的字节码中,这条指令对应43: invokevirtual #9; //Method ClassReference$Person.speak:()V ,为了便于使用IBM的图,这里采用跟IBM一致的符号引用:invokevirtual #12; 。调用过程图如下所示:
这里写图片描述
(1)在常量池中找到方法调用的符号引用
(2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
(3)根据this指针确定方法接收者(girl)的实际类型
(4)根据对象的实际类型得到该实际类型对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用;如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。

接口引用调用invokeinterface

代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<code class = "hljs axapta" > package org.fan.learn.methodTable;
 
/**
  * Created by fan on 2016/3/29.
  */
public class InterfaceReference {
     interface IDance {
         void dance();
     }
 
     static class Person {
         @Override
         public String toString() {
             return "I'm a person" ;
         }
         public void speak() {
             System.out.println( "Person speak" );
         }
         public void eat() {
             System.out.println( "Person eat" );
         }
     }
 
     static class Dancer extends Person implements IDance {
         @Override
         public String toString() {
             return "I'm a Dancer" ;
         }
         @Override
         public void speak() {
             System.out.println( "Dancer speak" );
         }
         public void dance() {
             System.out.println( "Dancer dance" );
         }
     }
 
     static class Snake implements IDance {
         @Override
         public String toString() {
             return "I'm a Snake" ;
         }
         public void dance() {
             System.out.println( "Snake dance" );
         }
     }
 
     public static void main(String[] args) {
         IDance dancer = new Dancer();
         System.out.println(dancer);
         dancer.dance();
         //dancer.speak();
         //dancer.eat();
         IDance snake = new Snake();
         System.out.println(snake);
         snake.dance();
     }
}
</code>

上面的代码中dancer.speak(); dancer.eat(); 这两句同样不能调用。
执行结果如下所示:
这里写图片描述
其字节码指令如下所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<code class = "hljs scss" > public static void main(java.lang.String[]);
   Code:
    Stack= 2 , Locals= 3 , Args_size= 1
    0 :   new     # 2 ; //class InterfaceReference$Dancer
    3 :   dup
    4 :   invokespecial   # 3 ; //Method InterfaceReference$Dancer."<init>":()V
    7 :   astore_1
    8 :   getstatic       # 4 ; //Field java/lang/System.out:Ljava/io/PrintStream;
    11 :  aload_1
    12 :  invokevirtual   # 5 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    15 :  aload_1
    16 :  invokeinterface # 6 1 ; //InterfaceMethod InterfaceReference$IDance.dance:()V
    21 new     # 7 ; //class InterfaceReference$Snake
    24 :  dup
    25 :  invokespecial   # 8 ; //Method InterfaceReference$Snake."<init>":()V
    28 :  astore_2
    29 :  getstatic       # 4 ; //Field java/lang/System.out:Ljava/io/PrintStream;
    32 :  aload_2
    33 :  invokevirtual   # 5 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    36 :  aload_2
    37 :  invokeinterface # 6 1 ; //InterfaceMethod InterfaceReference$IDance.dance:()V
    42 return </init></init></code>

从上面的字节码指令可以看到,dancer.dance();snake.dance(); 的字节码指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
为什么invokeinterface指令会有两个参数呢?

对象的内存模型如下所示:
这里写图片描述
从上图可以看到IDance接口中的方法dance()在Dancer类的方法表中的偏移量跟在Snake类的方法表中的偏移量是不一样的,因此无法仅根据偏移量来进行方法的调用。(这句话在理解时,要注意,只是为了强调invokeinterface在查找方法时不再是基于偏移量来实现的,而是基于搜索的方式。)应该这么说,dance方法在IDance方法表(如果有的话)中的偏移量与在Dancer方法表中的偏移量是不一样的。
因此,要在Dancer的方法表中找到dance方法,必须搜索Dancer的整个方法表。

下面写一个,如果Dancer中没有重写(override)toString方法,会发生什么?
代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<code class = "hljs axapta" > package org.fan.learn.methodTable;
 
/**
  * Created by fan on 2016/3/29.
  */
public class InterfaceReference {
     interface IDance {
         void dance();
     }
 
     static class Person {
         @Override
         public String toString() {
             return "I'm a person" ;
         }
         public void speak() {
             System.out.println( "Person speak" );
         }
         public void eat() {
             System.out.println( "Person eat" );
         }
     }
 
     static class Dancer extends Person implements IDance {
//        @Override
//        public String toString() {
//            return "I'm a Dancer";
//        }
         @Override
         public void speak() {
             System.out.println( "Dancer speak" );
         }
         public void dance() {
             System.out.println( "Dancer dance" );
         }
     }
 
     static class Snake implements IDance {
         @Override
         public String toString() {
             return "I'm a Snake" ;
         }
         public void dance() {
             System.out.println( "Snake dance" );
         }
     }
 
     public static void main(String[] args) {
         IDance dancer = new Dancer();
         System.out.println(dancer);
         dancer.dance();
         //dancer.speak();
         //dancer.eat();
         IDance snake = new Snake();
         System.out.println(snake);
         snake.dance();
     }
}
</code>

执行结果如下:
这里写图片描述
可以看到System.out.println(dancer); 调用的是Person的toString方法。
内存模型如下所示:
这里写图片描述

结束语

这篇博文讨论了invokevirtual和invokeinterface的内部实现的区别,以及override的实现原理。下一步,打算讨论下invokevirtual的具体实现细节,如:如何实现符号引用到直接引用的转换的?可能会看下OpenJDK底层的C++实现。

====================================================================================================================================

首页 > 程序开发 > 软件开发 > Java > 正文
java方法调用之重载、重写的调用原理(一)
2016-03-30 09:33:18      0 个评论   来源:程序员  
收藏   我要投稿

前一段时间看了《深入理解JVM》第三部分虚拟机执行子系统的内容,看到了重载与重写在JVM层面的调用原理(详见8.3 方法调用一节),但是没有写成博客总结一下,这里讨论讨论。在讨论过程中,难免会涉及到字节码指令 相关的内容。

结论

1.重载(overload)方法
对重载方法的调用主要看静态类型,静态类型是什么类型,就调用什么类型的参数方法。
2.重写(override)方法
对重写方法的调用主要看实际类型。实际类型如果实现了该方法则直接调用该方法,如果没有实现,则在继承关系中从低到高搜索有无实现。
3.
java文件的编译过程中不存在传统编译的连接过程,一切方法调用在class文件中存放的只是符号引用,而不是方法在实际运行时内存布局中的入口地址。

基本概念

1.静态类型与实际类型,方法接收者

?
1
2
<code class = "hljs fix" >Human man = new Man();
man.foo();</code>

上面这条语句中,man的静态类型为Human,实际类型为Man。所谓方法接收者,就是指将要执行foo()方法的所有者(在多态中,有可能是父类Human的对象,也可能是子类Man的对象)。
2.字节码的方法调用指令
(1)invokestatic:调用静态方法
(2)invokespecial:调用实例构造器方法,私有方法和父类方法。
(3)invokevirtual:调用所有的虚方法。
(4)invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
(5)invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
前2条指令(invokestatic, invokespecial),在类加载时就能把符号引用解析为直接引用,符合这个条件的有静态方法、实例构造器方法、私有方法、父类方法这4类,这4类方法叫非虚方法。
非虚方法除了上面静态方法、实例构造器方法、私有方法、父类方法这4种方法之外,还包括final方法。虽然final方法使用invokevirtual指令来调用,但是final方法无法被覆盖,没有其他版本,无需对方法接收者进行多态选择,或者说多态选择的结果是唯一的。

重载overload

上面说的静态类型和动态类型都是可以变化的。静态类型发生变化(强制类型转换)时,对于编译器是可知的,即编译器知道对象的最终静态类型。而实际类型变化(对象指向了其他对象)时,编译器是不可知的,只有在运行时才可知。

?
1
2
3
4
5
6
<code class = "hljs cs" > //静态类型变化
sr.sayHello((Man) man);
sr.sayHello((Woman) man);
//实际类型变化
Human man = new Man();
man = new Woman();</code>

重载只涉及静态类型的选择。
测试代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<code class = "hljs axapta" > /**
  * Created by fan on 2016/3/28.
  */
public class StaticDispatcher {
 
     static class Human {}
     static class Man extends Human {}
     static class Woman extends Human {}
 
     public void sayHello(Human human) {
         System.out.println( "Hello guy!" );
     }
 
     public void sayHello(Man man) {
         System.out.println( "Hello man!" );
     }
 
     public void sayHello(Woman woman) {
         System.out.println( "Hello woman!" );
     }
 
     public static void main(String[] args) {
         StaticDispatcher staticDispatcher = new StaticDispatcher();
         Human man = new Man();
         Human woman = new Woman();
         staticDispatcher.sayHello(man);
         staticDispatcher.sayHello(woman);
         staticDispatcher.sayHello((Man)man);
         staticDispatcher.sayHello((Woman)man);
     }
}</code>

先看看执行结果:
这里写图片描述

由此可见,当静态类型发生变化时,会调用相应类型的方法。但是,当将Man强制类型转换成Woman时,没有编译错误,却有运行时异常。“classCastException”类映射异常。
看看字节码指令:
javap -verbZ喎�"/kf/ware/vc/" target="_blank" class="keylink">vc2UgLWMgU3RhdGljRGlzcGF0Y2hlcjwvcD4NCjxwcmUgY2xhc3M9"brush:java;">public static void main(java.lang.String[]); Code: Stack=2, Locals=4, Args_size=1 0: new #7; //class StaticDispatcher 3: dup 4: invokespecial #8; //Method "":()V 7: astore_1 8: new #9; //class StaticDispatcher$Man 11: dup 12: invokespecial #10; //Method StaticDispatcher$Man."":()V 15: astore_2 16: new #11; //class StaticDispatcher$Woman 19: dup 20: invokespecial #12; //Method StaticDispatcher$Woman."":()V 23: astore_3 24: aload_1 25: aload_2 26: invokevirtual #13; //Method sayHello:(LStaticDispatcher$Human;)V 29: aload_1 30: aload_3 31: invokevirtual #13; //Method sayHello:(LStaticDispatcher$Human;)V 34: aload_1 35: aload_2 36: checkcast #9; //class StaticDispatcher$Man 39: invokevirtual #14; //Method sayHello:(LStaticDispatcher$Man;)V 42: aload_1 43: aload_2 44: checkcast #11; //class StaticDispatcher$Woman 47: invokevirtual #15; //Method sayHello:(LStaticDispatcher$Woman;)V 50: return

看到,在强制类型转换时,会有指令checkCast的调用,而且invokevirtual指令的调用方法也发生了变化39: invokevirtual #14; //Method sayHello:(LStaticDispatcher$Man;)V
虚拟机(准确说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。
对于字面量类型,编译器会自动进行类型转换。转换的顺序为:
char-int-long-float-long-Character-Serializable-Object
转换成Character是因为发生了自动装箱,转换成Serializable是因为Character实现了Serializable接口。

重写override

测试代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<code class = "hljs scala" > /**
  * Created by fan on 2016/3/29.
  */
public class DynamicDispatcher {
 
     static abstract class Human {
         protected abstract void sayHello();
     }
 
     static class Man extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Man say hello" );
         }
     }
 
     static class Woman extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Woman say hello" );
         }
     }
 
     public static void main(String[] args) {
         Human man = new Man();
         Human woman = new Woman();
         man.sayHello();
         woman.sayHello();
         man = new Woman();
         man.sayHello();
     }
 
}</code>

执行结果:
这里写图片描述

看下字节码指令:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<code class = "hljs scss" > public static void main(java.lang.String[]);
   Code:
    Stack= 2 , Locals= 3 , Args_size= 1
    0 :   new     # 2 ; //class DynamicDispatcher$Man
    3 :   dup
    4 :   invokespecial   # 3 ; //Method DynamicDispatcher$Man."<init>":()V
    7 :   astore_1
    8 :   new     # 4 ; //class DynamicDispatcher$Woman
    11 :  dup
    12 :  invokespecial   # 5 ; //Method DynamicDispatcher$Woman."<init>":()V
    15 :  astore_2
    16 :  aload_1
    17 :  invokevirtual   # 6 ; //Method DynamicDispatcher$Human.sayHello:()V
    20 :  aload_2
    21 :  invokevirtual   # 6 ; //Method DynamicDispatcher$Human.sayHello:()V
    24 new     # 4 ; //class DynamicDispatcher$Woman
    27 :  dup
    28 :  invokespecial   # 5 ; //Method DynamicDispatcher$Woman."<init>":()V
    31 :  astore_1
    32 :  aload_1
    33 :  invokevirtual   # 6 ; //Method DynamicDispatcher$Human.sayHello:()V
    36 return </init></init></init></code>

从字节码中可以看到,他们调用的都是相同的方法invokevirtual #6; //Method DynamicDispatcher$Human.sayHello:()V ,但是执行的结果却显示调用了不同的方法。因为,在编译阶段,编译器只知道对象的静态类型,而不知道实际类型,所以在class文件中只能确定要调用父类的方法。但是在执行时却会判断对象的实际类型。如果实际类型实现这个方法,则直接调用,如果没有实现,则按照继承关系从下往上一次检索,只要检索到就调用,如果始终没有检索到,则抛异常(难道能编译通过)。

(1)测试代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<code class = "hljs axapta" > /**
  * Created by fan on 2016/3/29.
  */
public class Test {
 
     static class Human {
         protected void sayHello() {
             System.out.println( "Human say hello" );
         }
         protected void sayHehe() {
             System.out.println( "Human say hehe" );
         }
     }
 
     static class Man extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Man say hello" );
         }
 
//        protected void sayHehe() {
//            System.out.println("Man say hehe");
//        }
     }
 
     static class Woman extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Woman say hello" );
         }
 
//        protected void sayHehe() {
//            System.out.println("Woman say hehe");
//        }
     }
 
     public static void main(String[] args) {
         Human man = new Man();
         man.sayHehe();
     }
 
}</code>

测试结果如下:
这里写图片描述
字节码指令:

?
1
2
3
4
5
6
7
8
9
10
<code class = "hljs scss" > public static void main(java.lang.String[]);
   Code:
    Stack= 2 , Locals= 2 , Args_size= 1
    0 :   new     # 2 ; //class Test$Man
    3 :   dup
    4 :   invokespecial   # 3 ; //Method Test$Man."<init>":()V
    7 :   astore_1
    8 :   aload_1
    9 :   invokevirtual   # 4 ; //Method Test$Human.sayHehe:()V
    12 return </init></code>

字节码指令与上面代码的字节码指令没有本质区别。

(2)测试代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<code class = "hljs axapta" > /**
  * Created by fan on 2016/3/29.
  */
public class Test {
 
     static class Human {
         protected void sayHello() {
         }
     }
 
     static class Man extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Man say hello" );
         }
 
         protected void sayHehe() {
             System.out.println( "Man say hehe" );
         }
     }
 
     static class Woman extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Woman say hello" );
         }
 
         protected void sayHehe() {
             System.out.println( "Woman say hehe" );
         }
     }
 
     public static void main(String[] args) {
         Human man = new Man();
         man.sayHehe();
     }
 
}</code>

编译时报错:
这里写图片描述

这个例子说明了:Java编译器是基于静态类型进行检查的。

修改上面错误代码,如下所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<code class = "hljs axapta" > /**
  * Created by fan on 2016/3/29.
  */
public class Test {
 
     static class Human {
         protected void sayHello() {
             System.out.println( "Human say hello" );
         }
//        protected void sayHehe() {
//            System.out.println("Human say hehe");
//        }
     }
 
     static class Man extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Man say hello" );
         }
 
         protected void sayHehe() {
             System.out.println( "Man say hehe" );
         }
     }
 
     static class Woman extends Human {
 
         @Override
         protected void sayHello() {
             System.out.println( "Woman say hello" );
         }
 
         protected void sayHehe() {
             System.out.println( "Woman say hehe" );
         }
     }
 
     public static void main(String[] args) {
         Man man = new Man();
         man.sayHehe();
     }
 
}</code>

注意在Main方法中,改成了Man man = new Man();
执行结果如下所示:
这里写图片描述
字节码指令如下所示:

?
1
2
3
4
5
6
7
8
9
10
<code class = "hljs scss" > public static void main(java.lang.String[]);
   Code:
    Stack= 2 , Locals= 2 , Args_size= 1
    0 :   new     # 2 ; //class Test$Man
    3 :   dup
    4 :   invokespecial   # 3 ; //Method Test$Man."<init>":()V
    7 :   astore_1
    8 :   aload_1
    9 :   invokevirtual   # 4 ; //Method Test$Man.sayHehe:()V
    12 return </init></code>

注意上面的字节码指令invokevirtual #4; //Method Test$Man.sayHehe:()V

结束语

本文讨论了一下重载与重写的基本原理,查看了相关的字节码指令,下篇博文 java方法调用之单分派与多分派(二)讨论下单分派与多分派。

====================================================================================================================================


URL(uniform resource location) : 统一资源定位符,用来作为互联网上各种资源的标识符,可理解为 身份证号

URL的基本结构: 协议名称 :// 服务器所在域名或者IP地址 : 端口号/ 所要访问的文件路径

注意点:浏览器为了保证安全性,设定了跨域保护策略, 即窗口之间的通信必须满足使用相同协议, 相同域或者子域, 相同端口, 因此深入理解URL各组成部分的含义有助于我们判断两个窗口之间是否能互相通信。

IP(Internet Protocol) :互联网中设备间进行通信都要遵从的一种协议,它规定了每台设备都要有且唯一的IP地址,用来标识自己在互联网中的地址。格式通常为XXX.XXX.XXX.XXX,不同网段下IP地址的范围也不同。如有兴趣者,请自行百度。

域名(Domain Name) :由于IP协议规定的纯数字IP地址在日常中难以记忆,因此人们便产生使用更加常见,好记的字符标识设备的地址,域名应运而生。一个域名就是一个更加容易记忆的目标主机的地址标识符。例如:百度的域名就为www.baidu.com,实际对应的IP地址为119.75.217.109

DNS(Domain Name System): 互联网中实际定位设备时还是使用IP地址来定位,因此产生了DNS,一种专门用来将域名转换为IP地址的协议,提供该协议服务的服务器就叫DNS服务器。

HTTP(HyberText Transfer Protocol) : 超文本传输协议,万维网中传输超文本都要遵从的一个协议,可以理解为HTML文件的传输就是靠它。

解答部分

地址解析阶段

用户在浏览器端输入URL后,浏览器先做第一件事情就是找到目标域名的IP地址,大致经过以下几个阶段:

1.查询 浏览器端的DNS缓存 中是否有目标域名的相关信息。
2.查询 本机的host文件 中是否有目标域名的信息。
3.查询本地的 路由器中的DNS缓存 中是否有目标域名的信息。
4.查询 ISP(互联网服务提供商,例如电信,移动)中的DNS服务器 中是否有目标域名的信息。
5.查询 根域名服务器 是否有目标域名的信息,如果没有,则传至 子域名服务器 进行查询, 以此递推
6.查询到目标IP地址后,则开始建立 TCP 三次握手 ,与目标服务器建立连接。
7.通过 HTTP 协议向目标主机发送请求。

请求处理及响应阶段

1.服务器端接受到请求后,根据路由将url中的地址进行重定向到服务器程序上的目标文件。

2.此处涉及到后台的MVC架构,大致如下:
URL中的文件地址部分经过服务器上的路由程序重定向到对应的控制器(controller)对象,控制器对象根据URL中指定的操作执行相关的逻辑并调用目标数据的模型(Model)对象,模型对象与数据库交互完成目标操作后,控制器将模型中反馈的数据填充到视图中。

3.视图部分(通常是HTML页面)作为HTTP响应发送到浏览器端。

视图解析阶段

1.浏览器开始解析目标HTML文件,执行流的顺序为自上而下。
2.HTML解析器将HTML结构转换为基础的DOM(文档对象模型),构建DOM树完成后,触发DomContendLoaded事件。
3.CSS解析器将CSS解析为CSSOM(层叠样式表对象模型),一棵仅含有样式信息的树。
4.CSSOM和DOM开始合并构成渲染树,每个节点开始包含具体的样式信息。
5.计算渲染树中个各个节点的位置信息,即布局阶段。
6.将布局后的渲染树显示到界面上。



作者:饥人谷_丁天奇
链接:http://www.jianshu.com/p/1517f0e88d66
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/bieleyang/article/details/78050405