Java特性
封装,继承,多态
谈到Java语言,就会自然的想到:面向对象,符合人们的思维模式。同时,它自身的特性更是被广泛的应用。三大特性:封装、继承、多态。
三大特性
► 概念理解:
1 封装
主要是用来 隐藏细节,保证安全性
2 继承
主要是用来子父类之间的继承关系,利于代码规范以及扩展
3 多态
是以继承为基础,体现的是 类与类之间的关系
多态,多态,从字面上理解就是:多种表现形态
(向上转型、向下转型)
很多人也许都有这样的意识,谈到多态,自然而然的脱口而出:重载和重写。在这里,就不纠结多态的具体含义。因为多态从其自身而言,分为了 编译时多态以及运行时多态。在这里,具体的来谈谈重载和重写。
1 重载剖析
2 重写剖析
前景引入
自己的下载的英雄联盟8个G,放到了自己定义的磁盘上了,要运行起来这游戏,而自己的内存却只有8个G。运行游戏的时候,是怎么吃我的内存的呢?一次性全部吃完 or 一点点的慢慢吃?
在Java世界里,程序的运行,必然是经过了类的加载机制:加载,验证,解析,准备,初始化,使用,卸载。其中加载的这一过程,就是Java文件编译为了.class文件。这个时候会问一下自己,这个编译的时候会发生什么事情呢?这就涉及到Javac的知识了,这里不做深究。这里只想说,如玩英雄联盟一样,这里的程序是怎样运行的?多态的两种定义:编译时多态,运行时多态到底是怎么一个区分法。重载和重写到底是属于哪一种呢?
重载 (理论以及抽象理解)
一个类里,方法名相同,参数类型以及个数不同即为重载。
1 在一个类里 模拟重载 三个同名方法 但参数不同
2 这个类,没有main方法,但是调用 javac Main类里面的main方法对这个HelloWorld进行编译
3 编译结束之后 生成.class文件
从生成的.class文件 我们可以看到
《1》 生成了都知道的 默认构造方法
默认构造方法:方法名与类名一致,没有返回值
public HelloWorld() {
}
《2》 同时还有三个 同名但参数不一样的方法
public void printTest() {
System.out.println("无参方法:说好不哭");
}
public void printTest(String var1, String var2) {
System.out.println("参数str: " + var1 + " 参数string: "
+ var2 + " 说好不哭");
}
public void printTest(int var1, String var2) {
System.out.println("参数a: " + var1 + " 参数string: "
+ var2 + " 说好不哭");
}
重载字节码 剖析
字节码剖析class文件
从字节码中可以看到:
默认构造方法:
public HelloWorld() {
}
字节码:
public HelloWorld();
descriptor: ()V
无参方法
public void printTest() {
System.out.println("无参方法:说好不哭");
}
字节码:
public void printTest();
descriptor: ()V
有参方法 String var1, String var2
public void printTest(String var1, String var2) {
System.out.println("参数str: " + var1 + " 参数string: " +
var2 + " 说好不哭");
}
字节码:
public void printTest(java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;)V
有参方法
public void printTest(int var1, String var2) {
System.out.println("参数a: " + var1 + " 参数string: " +
var2 + " 说好不哭");
}
字节码:
public void printTest(int, java.lang.String);
descriptor: (ILjava/lang/String;)V
综上,在生成的字节码中,每个方法下面都有一个 descriptor
其实这个 descriptor 就是作为 JVM 识别的方法描述符
同一方法名 printTest()
产生的不同 方法描述符
descriptor: ()V
descriptor: (Ljava/lang/String;Ljava/lang/String;)V
descriptor: (ILjava/lang/String;)V
重载 why? 作用 以及总结
所以,为啥要有重载呢?
Java中方法的重载为 :方法名相同,参数不同的方法。
这样设计的好处?
因为 参数的有无以及参数类型的不确定性,但都是针对于同一方法名来说的。意思就是说,比如上方的,同一方法名 printTest(),都是这一动作行为的方法,但是呢?参数类型不一样。像这样的,我们就可以用重载来完成
由上述的 字节码层面看到的:
同一方法名 printTest()
产生的不同 方法描述符
所以,自己就再来自己总结一下,重载 重载 字面上看,无非就是 重新加载喽,既然是加载,是在由 java文件编译为class文件,此时虽然没有被调用,没有被运行,但是实际上 已经产生了 作为该方法的描述符descriptor,有了这个,其实在后续的调用以及运行中,JVM是根据这方法的描述符,去判断调用以及运行这同名方法中的具体其中的哪一个。
再来看看 这个类中有main方法,执行这个类里面的方法的时候
HelloWorld helloWorld = new HelloWorld();
helloWorld.printTest();//调用无参方法
helloWorld.printTest("杰伦","信");//调用string string 方法
helloWorld.printTest(1,"信");//调用int stirng 方法
在编译的过程中,已经产生了该方法的描述符
在调用过程中,人为显现的调用(传参 or 不传参)
为什么java中重载是以方法名和参数作为方法的描述符的?
因为 在方法调用执行中 有时候 注重执行过程 而不 注重返回值,
比如构造方法,有一个默认的构造方法。但是想对属性做一下赋值,可以重载对属性操作。
所以 用方法名和方法的参数作为方法的描述符
综上 总结
Java中的方法的重载 在编译时期就已经产生了 该方法的描述符(通过字节码可知,每个方法都有自己的描述符)
在执行被调用的的时候,根据方法的描述符和传入的参数选择方法执行
------------------------------- 编译时多态
所以
重载 是 编译时多态
说完了 重载 接下来 说说 重写
重写(理论介绍 以及 抽象理解)
跟重载一样,这里自己也来说说重写,重写,重写,重新写呗,
重写Override:在Java中,
1 通过继承 extends 子父类的继承 并且只能是 单继承
可以在子类中 Override 父类中的方法
此时是重写父类中已经有的方法
2 通过实现 implements 实现接口
可以在该类中 Override 接口中定义的方法
此时是重写具体的实现方式
区分开来了以上的两点,这里谈的重写,是第一种子父类继承的重写
重载 是强调的是 同一个类里面 方法名相同 参数的类型以及个数不同
那么 重写呢?(继承的重写)
强调的是 不同的类之间的 继承 关系,重写该继承过来的方法
1 三个不同的类,模拟重写
其中 Child 继承了Parent
2 编译后,生成了三个class文件
从生成的.class文件 我们可以看到
1 HelloWorld 类
public class HelloWorld {
public HelloWorld() {
}
}
2 Parent 类
class Parent {
Parent() {
}
public void testSay(String var1) {
System.out.println("父亲说:说好不哭");
}
3 Child 类 (继承了Parent )
class Child extends Parent {
Child() {
}
public void testSay(String var1) {
System.out.println("孩子说:说好不哭");
}
}
重写 字节码
从字节码文件 我们可以看到
1 HelloWorld
public HelloWorld();
descriptor: ()V
2 Parent
public void testSay(java.lang.String);
descriptor: (Ljava/lang/String;)V
3 Child(继承了Parent)
public void testSay(java.lang.String);
descriptor: (Ljava/lang/String;)V
同样都会有 descriptor
这个时候,是否会问下:重写也是编译时多态?
其实 这里的 descriptor 并不是判断依据
由上面的可以知道,重载 是编译时多态,是由于在同一个类中,同一的方法名 不同的参数 在编译的时候,就已经确定好了,在调用的时候,通过传递参数就可以具体的指定为 是哪一个方法了。
那么,重写呢?
重写 现在知道的是,在不同的类中,子父类,同样的方法名,子类可以继承后,方法名以及参数不改,修改的是 方法体里面的内容。那么在调用的时候,是否可以知道其具体的是哪一个方法呢?
猜想:如果知道是 具体调用的是哪一个方法,那么就是编译时多态
否则,就为 运行时多态
既然是 子父类继承,那么父类永远只有一个,但是孩子可以是多个
//这里模拟的是 父类的引用 指向到底是哪一个子类的对象
Parent parent = h.testWay();
//然后 子类对象的方法打印
parent.testSay("杰伦");
打印结果剖析:
1 由编译时多态(重载)的结论。在调用的时候,根据参数的不同,就已经知道具体的是要调用的是哪一个方法了
2 那么在重写的时候,这个时候调用的时候,由于是子父类继承,方法名以及参数是一样的,这里的关注点是,在调用的时候,是否就已经知道调用的是具体的哪一个方法
3 打印的结果:
多次运行main方法
《1》孩子3说:说好不哭
《2》孩子1说:说好不哭
《3》孩子2说:说好不哭
《4》孩子1说:说好不哭
通过以上的打印结果,运行了 四次 main方法
每次打印的结果都不一样,这个时候能说,重写是 编译时多态吗?因为 编译时多态,不管运行多少次,打印的结果都是一样的
所以:
重写(继承关系的重写) 是 运行时多态,因为在编译的时候,确定不好。
重写 why? 作用 以及总结
综上,重写(继承关系的重写) 是 运行时多态。
//这里模拟的是 父类的引用 指向到底是哪一个子类的对象
Parent parent = h.testWay();
所以 运行时多态 : 父类的引用 指向了 子类的对象
同时 在撸过的代码中
List<> list = new ArrayList<>();
应该说 都遇到过这种写法
撸过ArrayList源码都知道,ArrayList 实现了List接口
所以 接口的引用 指向了 实现该接口的类的对象
这里为啥不是
List<> list = new List<>();
或者
ArrayList<> list = new ArrayList<>();
因为这两种 都是 实例化当前类,是具体的。
设计模式中 "代码尽量依赖于抽象,不依赖于具体"
面向接口,依赖抽象,才是符合思维模式,代码可以方便替换。
再来谈谈 关于重写(继承关系的重写)注意的地方:
1 父类的引用指向子类的对象
至于会打印出来什么?
理解了 父类的引用 指向了 子类的对象
就可以大声的喊出来,打印的会是什么了
2 父类的引用指向子类的对象,但是调用的方法是子类的,父类没有
这个时候,会问 是否可以编译通过,以及会打印什么
其实,理解了 父类的引用 指向了 子类的对象
就可以大声的说出来了
当 父类的引用 指向了 子类的对象 时,调用的方法 一定要满足 子父类二者中的方法都要有
向上转型 向下转型
说到这个 其实就是形态状态的改变
向上转型 (子类 继承了 父类 ,有了 父类里面的所有方法,子类还可以 定义自己的方法 所以 子类的作用方法范围 必然比 父类大)
所以 这里的 向上,指的是 范围的意思
既然是 向上(范围变大了) 所以 就是 父类引用 ---指向--》 子类对象
Parent p = new Child();
//这就叫 upcasting (向上转型)
// 现在 父类引用p 引用指向一个Child 子类对象
那相应的
向下转型
既然是 向下(范围变小了 必须要强转) 所以 就是 子类引用 ---指向---》子类对象的父类引用
Child c= (Child )p;
// 这就叫 downcasting (向下转型)
// 现在p 还是指向 Child 子类对象
注意:
子类的引用 不能指向 父类对象
不管怎样,通过这样的分析
就可以明确的了解到了
1 多态 ---- 编译时多态 运行时多态
2 向上 向下转型
在其他地方,有的也会有说 :多态 是运行时 父类引用指向子类对象。是类与类的关系,而重载 是在一个类中的。所以 重载不是多态。甚至还有 “晚绑定”,“早绑定”。
在这里呢,多态这个概念 每个人的理解不一样,但是,对于原理一定要掌握
所以 Polymorphic 梦里花落知多少,知多知少,原理最重要~
Hey~
我们又见面啦~
你还好吗?
2019.09.22
喜欢记得来一个