javaSE复习整理(仗剑走天涯)
面向对象
多态
查看字节码指令
javap -verbose 名称.class
或
javap -v 名称.class
main字节码
mian方法
public int plus(int i,int j){
return i+j;
}
public static void main(String[] args) {
DuoTai duoTai = new DuoTai();
System.out.println(duoTai.plus(2,4));
}
main字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
// 创建一个对象,并将其引用值压入栈顶
0: new #2 // class com/ydlclass/Computer
// 复制栈顶数值并将复制值压入栈顶
3: dup
// 很明显这是在调用构造器,编译之后还是符号引用,就是方法的字符串形式的名字,
// 加载之后,我们就可以解析成对应的方法的调用地址了
// 因为一旦类加载到内存的方法区,这个方法就有了真实的调用地址了
4: invokespecial #3 // Method "<init>":()V
// 将栈顶引用型数值存入第二个本地变量
7: astore_1
// 获取指定类的静态域,并将其值压入栈顶
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将第二个引用类型本地变量推送至栈顶
11: aload_1
// 将 int 型 2 推送至栈顶
12: iconst_2
// 将 int 型 4 推送至栈顶
13: iconst_4
// 调用实例方法,调用的过程是在内存进行的,只有当字节码被加载进入内存才有具体的地址
14: invokevirtual #5 // Method plus:(II)I
// 以下部分是粘贴过来的plus方法的,此时会创建新的栈帧
// 单独这个方法的指令入口在编译的时候是不可知的,但是加载到内存就可知了
// 其实,这个调用的不一定是这个方法,只是为了演示
-------------------------
// 将第二个 int 型本地变量推送至栈顶
0: iload_1
// 将第三个 int 型本地变量推送至栈顶
1: iload_2
// 将栈顶两 int 型数值相加并将结果压入栈顶
2: iadd
3: ireturn
-------------------------
17: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
20: return
LineNumberTable:
line 10: 0
line 11: 8
line 12: 20
LocalVariableTable:
// 这里的Signature就是一个引用的静态类型,这里早有记录
Start Length Slot Name Signature
0 21 0 args [Ljava/lang/String;
8 13 1 computer Lcom/ydlclass/Computer;
}
flags
:访问权限
code
: 代码区域
stack
操作数栈的深度
locals
局部变量的个数
args_size
参数的个数
LocalVariableTable
:本地变量表
invokespecial用来调用构造方法,invokestatic用来调用静态方法,invokeinterface用来调用接口方法,invokespecial用来调用实例方法(虚方法)
被invokestatic、invokeinterface 和 invokespecial指令调用的方法,一定能在解析阶段(加载完成后或第一次使用)确定唯一的调用版本,比如静态方法,私有方法,和实例构造器、被final修饰的方法。调用会在类加载的时候就能顺序解析成直接引用,这类方法叫非虚方法,反之都是虚方法,这里边有个特例,就是final修饰的方法也是被invokevirtual 调用
重载方法的调用过程
我们在调用一个虚方法的时候,jvm会在适当的时候帮我们选择合适的方法版本,有的时候在编译期、有时是在运行时,这个方法版本的选择过程我们可以称之为【方法分派】
public class Friend {
}
public class BoyFriend extends Friend {
}
public class GirlFriend extends Friend{
}
public class Overload {
public void relation(Friend friend){
System.out.println("朋友身份");
}
public void relation(BoyFriend boyFriend){
System.out.println("男朋友身份");
}
public void relation(GirlFriend girlFriend){
System.out.println("女朋友身份");
}
public static void main(String[] args) {
Overload overload = new Overload();
Friend friend = new Friend();
overload.relation(friend);
Friend boyFriend = new BoyFriend();
overload.relation(boyFriend);
Friend girlFriend = new GirlFriend();
overload.relation(girlFriend);
}
}
运行结果
朋友身份
朋友身份
朋友身份
虚拟机在选择重载方式时,是通过【静态类型】决定的而不是动态类型。由于静态类型编译时就可知,事实上虚拟在编译期就已经知道选择哪一个重载方法,并且把这个方法的符号引用写在了invokevirtual的指令中,所有依赖【静态类型】决定方法执行版本的的分派动作称之为静态分派
第一步是静态分派的过程,jvm从Friend类的多个重载方法中选择了 Friend::relation(Friend friend) 这个方法,并且生成指令 invokevirtual Friend::relation(Friend friend)
第二步是动态分派的过程,是根据运行时类型确定具体调用谁的 relation(Friend friend) 方法,因为运行时类型是Friend,所以最终的方法选择是 Friend::relation(Friend friend)
重载和重写
重载只是选择了调用方法的版本。
重写是具体明确了调用谁的方法。
多态 可以理解为调用不同接口 出现不同状态
对象转型
向上转型:子类对象转为父类,向上转型不需要显示的转化
向上转型会丢失子类独有的特性
Friend friend = new BoyFriend();
向下转型:父类对象转为子类,向下转型需要强制转化
向下转型可能会出现错误,需要谨慎。
GirlFriend girlFriend = (GirlFriend) friend2;
Friend friend = new BoyFriend();
if(friend instanceof BoyFriend){
BoyFriend boyFriend = (BoyFriend) friend;
boyFriend.fatherMethod();
boyFriend.pet();
}
Friend friend2 = new GirlFriend();
if(friend2 instanceof GirlFriend){
GirlFriend girlFriend = (GirlFriend) friend2;
girlFriend.fatherMethod();
girlFriend.flighty();
}
抽象类和接口
抽象类
去掉方法体,加一个abstract关键字就是一个抽象方法,如果一个类里有抽象方法,在类的申明上必须也要加上abstract,变成一个抽象类。抽象方法没有方法体,所以不能直接调用,也正是因为抽象方法没有方法体,所以我们不能直接构造一个抽象类
抽象类中除了拥有抽象方法,也可以拥有普通方法
创建子类时,父类依然会被创建,抽象类只有在构建子类的时候才会被构建出实例
如果一个类被final修饰则失去这种能力
抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public
抽象类不能直接实例化,需要依靠子类采用向上转型的方式处理
抽象类必须有子类,使用extends继承,一个子类只能继承一个抽象类
子类(如果不是抽象类)则必须覆写抽象类之中的全部抽象方法(如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。)
接口
关键字:interface
接口是多实现的,一个类可以实现多个接口,但是只能继承一个类。接口之间也可以相互继承
接口是设计 实现是搬砖
抽象类是模板式的设计,而接口是契约式设计
继承和实现
继承其实有一种 我是什么什么的意思 就像 小狗是动物 女朋友是朋友 男朋友是朋友 男人是人 女人是人
实现有一种就是我可以干什么 像人可以吃 喝 玩 乐 飞机可以滑行 飞
设计模式
设计模式是人们为软件开发中相同表征的问题,抽象出的可重复利用的解决方案
面向对象设计模式
开闭原则
对扩展开发、对修改关闭
可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了
里氏代换原则
继承必须确保超类所拥有的性质在子类中仍然成立
子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法
依赖倒转原则
要面向接口编程,不要面向实现编程
每个类尽量提供接口或抽象类,或者两者都具备
变量的声明类型尽量是接口或者是抽象类
任何类都不应该从具体类派生
使用继承时尽量遵循里氏替换原则
接口隔离原则
将庞大的接口拆分成更小的和更具体的接口
使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合
迪米特法则(最少知道原则)
只与你的直接朋友交谈,不跟“陌生人”说话 其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性
合成复用原则
原则是尽量使用合成/聚合的方式,而不是使用继承。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范
单一原则
一个类只做一件事
模板方法设计模式
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类【行为型模式】
public abstract class MoBan {
public abstract void start();
public abstract void work();
public abstract void rest();
public abstract void close();
public void life(){
start();
work();
rest();
work();
close();
}
}
public class Teacher extends MoBan{
@Override
public void start() {
System.out.println("开始上班");
}
@Override
public void work() {
System.out.println("开始工作");
}
@Override
public void rest() {
System.out.println("开始休息");
}
@Override
public void close() {
System.out.println("开始下班");
}
public static void main(String[] args) {
MoBan teacher = new Teacher();
teacher.life();
}
}
开始上班
开始工作
开始休息
开始工作
开始下班
优点
它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展
它在父类中提取了公共的部分代码,便于【代码复用】
部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则
缺点
对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度
父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度
由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍