12-面向对象5(多态)

看了视频,讲师就只说“编译看左边,运行看右边”,讲的跟玄学似的;我可不能那么肤浅!于是看了很多博客,现摘下来做个整合,整我一头汗,图书馆还不开空调,热死我了

JVM

  • Java源代码被编译器编译成class文件(不是底层操作系统可以直接执行的二进制指令)。因此,我们需要一种平台可以解释class文件并运行它。而做到这一点的正是JVM

  • 实际上,JVM是一种解释执行class文件的规范技术,各个提供商都可以根据规范,在不同的底层平台上实现不同的JVM

  • JVM实现的基本结构图

  • 运行时数据区

    当JVM运行一个程序时,需要在内存存储许多东西。比如字节码、程序创建的对象、传递的方法参数、返回值、局部变量等等。JVM会把这些东西都组织到几个"运行时数据区"中便于管理

    • 【栈】栈中的数据是线程私有的,一个线程是无法访问另一个线程的栈的数据
    • 【方法区】


    • 【堆】

多态引入

  • 【抽象理解】同一个对象,不同时刻表现出来的不同状态
    • 继承允许将对象视为他自己本身的类型或其基类型来加以处理。既然允许将多种类型(从同一基类导出的)视为同一类型来处理,那么同一份代码也就可以毫无差别的运行在这些不同类型之上了
    • 多态机制使具有不同内部结构的对象可以共享相同的外部接口
  • 作用:消除类型之间的耦合关系
  • 体现:父类引用指向子类对象
  • 使用前提
    • 要有继承关系
    • 要有方法重写
    • 父类引用指向子类对象

对象的转型

  • 向上转型upcasting
    • 由于对象既可以当作它自己本身的类型使用,也可以当作它的基类类型使用,编译器是允许的;这个转换可以自动进行
    • 因此将某个对象的引用是其基类类型的引用的行为称为"向上转型",通俗来讲就是"父类引用指向子类对象"
  • 向下转型downcasting(造型)
    • 如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,必须进行造型(强制类型转换),才能够通过编译时的检查 // 强制类型转换要求双目必须有子父类关系,否则编译报错
    • 错误的类型转换,就算躲过编译器检查,运行时也会引发java.lang.ClassCastException;可以使用 instanceof 来测试一个对象的类型
  • obj instanceof T
    • obj 为一个对象,T 表示一个类或者一个接口,当 obj 为 T 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false
    • 无继承关系的引用类型间的转换是非法的!所以,obj 所属的类与类T必须是子类和父类的关系,否则instanceof编译报错!

对象的类型

  • 编译时类型
    • 编译时类型由声明该变量时使用的类型决定
    • 一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问在子类中添加的属性和方法
    • 属性也是在编译时确定的,编译时引用声明为Person类型,没有Son特有成员变量,因而编译错误
    • 方法同理,只能调用父类中的方法,不能调用子类特有方法,否则也会编译报错
  • 运行时类型
    • 运行时类型由实际赋给该变量的对象决定
    • 若编译时类型和运行时类型不一致,就出现了对象的多态性

绑定

将 [一个方法调用] 与 [一个方法主体] 关联起来,被称为"绑定"

静态绑定

若这个绑定时发生在程序执行之前(如:由编译器或链接程序实现的),则称为"前期绑定"/"静态绑定"

  • 静态绑定过程

  • 如果一个方法有static、private、final修饰或者是构造方法,那就都是"前期绑定"
  • 所有的静态方法都是"前期绑定",因为静态方法可以通过类名进行访问,而不会用到引用的对象的实际类型信息,因此在编译时就可以通过类型信息确定是哪一个具体的方法
    • 这也就揭示了为什么静态方法不能重写了,因为重写的目的是为了实现多态
    • private方法 默认是 final类型
    • 构造方法其实是一种特殊的 static 方法
  • 成员变量也属于"前期绑定"
    • 调用到的成员变量为父类的属性!
    • 也就是说运行时(动态)绑定针对的范畴只是 [对象的方法]
  • 总结调用一个方法的过程(摘自Java核心技术卷1)
    • 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数 x 声明为 Father 类的对象。需要注意的是:有可能存在多个名字为 f,但参数类型不一样的方法。例如,可能存在方法f(int)和方法f(String)。编译器将会一一列举所有Father类中名为f的方法和其超类中访问属性为 public 且名为 f 的方法(超类的私有方法不可访问)。至此,编译器已获得所有可能被调用的候选方法
    • 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在一个与提供类型完全匹配,就选择这个方法。这个过程被称为重载解析。例如:对于调用x.f("Hello")来说,编译器将挑选出f(String),而不是f(int)。由于允许类型转换(int可以转换成double,Manager可以转换成Employee,Circle可以转换成Object,等等)所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。至此,编译器已获得需要调用的方法名字和参数类型。
  • Java中 [重载方法] 的选择是"静态绑定"
    • 也就是一个方法的参数选择是静态绑定的
    • 如调用了一个重载的方法,在 编译时 根据参数列表就可以确定方法,并且如果这个方法是非静态的,那么具体调用的是父类的方法还是子类的方法还需要通过"动态绑定"来确定
    • 当程序运行时,并且采用"动态绑定"调用方法时,JVM一定会调用与x所引用对象的实际类型最合适的那个类的方法

动态绑定

这个绑定发生在程序运行之中,根据对象的具体类型进行绑定的,那么这种绑定称为"动态绑定"

  • 动态绑定是基于对象的实际类型而非对象的引用类型!(摘自Java编程思想)

  • 再提【方法区】

    • 每次调用方法都要进行搜索,时间开销相当。因此会在JVM加载类的同时,在方法区中会为每个类存放很多信息。而存放的类的信息中有一个数据结构叫【方法表】。它以 {数组} 的形式记录了当前类及其所有超类的可见方法字节码在内存中的直接地址。这样一来,在真正调用方法的时候,JVM仅查找这个表就行了
    • 子类方法表中继承了父类的方法
    • 相同的方法(相同的方法签名:方法名和参数列表相同。返回值不是签名的一部分,但在覆盖时要保证返回类型的兼容性,即:允许子类将覆盖方法的返回类型定义为原返回类型的子类型)在所有类的方法表中的索引相同
    • 如果调用 "super.f()",编译器将对隐式参数超类的方法表进行搜索
  • 动态绑定过程

    • 【常量池解析】根据 [测试类常量池中第15个常量表] 中记录的 "方法f1信息的符号引用" 来查找引用father所在对应类Father,继而找到该类对应的方法表,然后将找到的Father方法表中 "f1方法对应的索引项11" 再放置回 [测试类的常量池中的第15个常量表] 中 ,这个过程是用 Father方法表中的索引项 来代替 常量池中的符号引用
    • 在编译阶段,最佳方法名依赖于这个父类引用;而决定方法是哪个类的版本,这通过由JVM推断出这个对象的运行时类型来完成
      • 换言之,在编译阶段,该被调用的方法就已经确定好了(如上:父类方法表第11个方法)。而具体调用谁的方法实现,这还要看具体引用所指向的对象是谁 → 该对象所属类的方法表中 同样是第11个方法数据的指针 所指向的方法字节码所在的存储空间,那里存放的才是真正要被执行的方法体
      • 体现在代码上,就是:能够被调用的只有父类中声明的方法,子类特有方法不能通过父类引用来调用
  • 对于如下的调用方法,JVM是如何定位的呢?

    • 问题就在于Father类型中并没有方法签名为f1(char)的方法呀。但打印结果显示JVM调用了Father类型中的f1(int)方法,并没有调用Son类型中的f1(char)方法
    • 根据上面详细阐述的调用过程,首先可以明确的是:JVM首先是根据对象father声明的类型Father来解析常量池的(解析:用Father方法表中的索引项来代替常量池中的符号引用)。如果Father中没有匹配到"合适"的方法,就无法进行常量池解析,这程序在编译阶段就通过不了。那既然通过了,就说明还是"合适"~
    • 那什么叫"合适"的方法呢?
      • 方法签名完全一样的方法自然是合适的。但如果方法中的参数类型在声明的类型中并不能找到呢?比如上面的代码调用father.f1(),Father类型并没有f1(char)的方法签名
      • 实际上,JVM会找到一种"凑合"的办法,就是通过 [参数的自动转型] 来找到"合适"的办法。比如char可以通过自动转型成int,那么Father类就可以匹配到这个方法了

小结:overload / overwrite

code

向上转型 → 编译时类型 & 运行时类型 → 动态绑定

class Animal {
	protected void eat() {
		System.out.println("animal eat food");
	}
}

class Cat extends Animal {
    protected void eat() {
		System.out.println("cat eat fish");
	}
}

class Dog extends Animal {
	public void eat() {
		System.out.println("Dog eat bone");
	}

}

class Sheep extends Animal  {
	public void eat() {
		System.out.println("Sheep eat grass");
	}
}

// 多态是编译时行为还是运行时行为?
public class Test {
	public static Animal getInstance(int key) {
		switch (key) {
		case 0:
			return new Cat ();
		case 1:
			return new Dog ();
		default:
			return new Sheep ();
		}
	}

	public static void main(String[] args) {
		int key = new Random().nextInt(3);
		System.out.println(key);
		Animal animal = getInstance(key);
		animal.eat();
	}
}
public class Test2 {
    public static void main(String[] args) {
        Base base = new Sub();
        base.add(1, 2, 3); // "sub_1"; 可变参数和数组不构成[重载]! → 它俩是[重写]!
        Sub s = (Sub)base;
        s.add(1,2,3); // "sub_2"
    }
}

class Base {
    public void add(int a, int... arr) {
        System.out.println("base");
    }
}

class Sub extends Base {
    public void add(int a, int[] arr) {
        System.out.println("sub_1");
    }

    public void add(int a, int b, int c) {
        System.out.println("sub_2");
    }
}

原文地址:https://www.cnblogs.com/liujiaqi1101/p/13045289.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


jinfo 命令可以用来查看 Java 进程运行的 JVM 参数,命令如下:[root@admin ~]# jinfo --helpUsage: jinfo [option] <pid> (to connect to running process) jinfo [option] <executable <core> (to connect to a core file) jinfo [option] [serve
原文链接:https://www.cnblogs.com/niejunlei/p/5987611.htmlJava Virtual Machine Stacks,线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),由于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行就对应着栈帧在虚拟机栈中的入栈,出栈...
java 语言, 开发者不能直接控制程序运行内存, 对象的创建都是由类加载器一步步解析, 执行与生成与内存区域中的; 并且jvm有自己的垃圾回收器对内存区域管理, 回收; 但是我们已经可以通过一些工具来在程序运行时查看对应的jvm内存使用情况, 帮助更好的分析与优化我们的代码;jps查看系统中有哪些java进程jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息
1.jvm的简单抽象模型:  2.类加载机制     双亲委派模型是为了防止jdk核心类库被篡改,如果需要打破可以重写Classloader.loadClass方法。r 双亲委派模型:一个类加载器收到一个类的加载请求,他会先判断自身是否已存在该类,如果不存在上抛给上一级类加载器ClassLoad
堆外内存JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Netty,广泛使用了堆外内存,但是这部分的内存并不归JVM管理,GC算法并不会对它们进行回收,所以在使用堆外内存时,要格外小心,防止内存一直得不到释放,造成线上故障。堆外内存的申请和释放JDK的ByteBuffe
1.springboot和tomcat2.springcloud的请求如何通过网关鉴权?3.springmvc启动时组件的加载顺序?4.mybatis如何同时更新三条记录5.hibernate实现级联更新6.一个web程序应用程序启动时的加载流程7.如何向www.baidu.com地址发出请求时,并获取相应?8.???9.谈谈你对tcp/iptelnetudp协
堆设置-Xms256M:初始堆大小256M,默认为物理内存的1/64-Xmx1024M:最大堆大小1024M,默认为物理内存的1/4,等于与-XX:MaxHeapSize=64M-Xmn64M:年轻代大小为64M(JDK1.4后支持),相当于同时设置NewSize和MaxNewSize为64M-XX:NewSize=64M:初始年轻代大小-XX:MaxNewSize=256M:最大年轻代大小(默认
一.概述收集算法(JVM之垃圾回收-垃圾收集算法)是内存回收的抽象策略,垃圾收集器就是内存回收的具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。就像没有最好的算法一样,垃圾收集器
Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象,如下图所示: 在Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为三个区域:Eden、S0、S1。 这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包
JVM深入理解JVM(4)——如何优化JavaGC「译」 PostedbyCrowonAugust21,2017本文翻译自SangminLee发表在Cubrid上的”BecomeaJavaGCExpert”系列文章的第三篇《HowtoTuneJavaGarbageCollection》,本文的作者是韩国人,写在JDK1.8发布之前,虽然有些地
 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Scala等)程序员在提升开
运行时数据区  线程独有本地方法栈、虚拟机栈、程序计数器这些与线程对应的数据区会随着线程开始和结束创建和销毁  整体公有元数据区(又称方法区)、堆区会随着虚拟机启动而创建,随着虚拟机退出而销毁 
java整个堆大小设置:Xmx和Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。永久代PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍年轻代Xmx的设置为老年代存活对象的1-1.5倍老年代的内存大小设置为老年代存活对象的2-3倍BTW: Sun官方建议年轻代
栈顶缓存(Top-of-StackCashing)技术基于栈式架构得虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存读写次数 由于操作数是存储在内存重的,因此频繁地执行内存读/写操作必然影响速度。 综上
自用。同样的代码在不同的平台生成的机器码是不一样的,为什么java代码生成的字节码文件,能在不同的平台运行?因为不同版本的jdk里面的虚拟机会屏蔽不同操作系统在底层硬件与指令上的区别。栈:线程栈,局部变量存放栈内存区域。线程(分配一个栈)运行分配栈将局部变量放入内存。怎么放:栈
jconsole监控:1.java启动命令加上参数java-Djava.rmi.server.hostname=172.16.17.247-Dcom.sun.management.jmxremote-Dcom.sun.management.jmxremote.port=2099-Dcom.sun.management.jmxremote.authenticate=false-Dcom.sun.management.jmxremote.ssl=false -XX:+Unlock
类加载器分类publicclassStackStruTest{publicstaticvoidmain(String[]args){//对用户自定义个类来说:默认使用系统类加载器进行加载-----AppClassLoaderClassLoaderclassLoader=StackStruTest.class.getClassLoader();System.out.p
堆体系结构一个JVM实例只存在一个堆内存,堆内存的大小是可调节的。类加载器读取类文件后,需要把类、方法、常量、变量放在堆内存中,保存所有引用类型的真实信息,以方便执行器指向,堆内存分为三个部分:年轻代、老年代、永久代。Java7之前,堆内存在逻辑上分为:年轻代、老年代、永久代。物
JVM深入理解JVM(5)——虚拟机类加载机制 PostedbyCrowonAugust21,2017在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机中,而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机中会发生什么变化?本文将逐步解答这
保存(持久化)对象及其状态到内存或者磁盘Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在