Java数组、类、接口多态的内存逻辑模型以及接口和抽象类的区别
Copyright©stonee新博客
在我之前的博客java概述和编译过程中已经大概涉及到了java代码的内存逻辑模型,在博客java多态中也涉及到了类的多态,这次博客 主要是说明接口、通过接口实现的多继承以及接口和抽象类的区别 ,然后借着接口去重新回顾一下数组和类的内存逻辑模型。
1. 接口的语法和特点
- 接口的初衷是方法定义和实现的分离
- 接口的权限必须是public
- 接口的字段都是静态public static final(默认限定,不用显式写出)
- 接口没有Object(0-4)的槽号,即接口 不像类一样 会继承object方法
前两个的原因是因为接口不能new实例对象,所以只能调用静态字段,并且接口即使继承Object的槽号也没有用反而消耗内存空间
- 实现接口的类必须实现接口的 所有非默认方法,为了保证程序的可扩展性,伴随类由此产生(已经被jdk8的default方法所替代)
- 接口的方法成员默认为 public abstract ,即公共的抽象方法,此方法在接口中只能声明不能实现。但是在JDK8之后引入了 public default ,即公共默认方法,此方法在接口中可以实现,这样一来,实现这个接口的程序员只需要为他们真正关心的时间覆盖响应的监听器,默认方法可以调用任何其他方法——摘自Java核心技术卷 因为有了默认方法,就不必使用接口的伴随类了
- 当然,接口中也允许 private 权限修饰符的存在,此方法需要被实现
- 因为有 offset 即相对偏移地址,所以接口是可以进行多继承的
- 类和接口的继承关系分别是:类继承类,类继承接口,接口继承接口,但是接口不能继承类
- 类继承接口需要用到 implements 关键字
- 当类实现接口的时候,需要明确指出是 public 方法
- instanceof 关键字不仅可以观察是某对象的实例,也可以发现实例是否实现了某接口
- 因为 接口的多继承 ,当扩展类继承了基类和接口时,同时基类和接口有相同的实现方法,则基类优先;当扩展类继承了两个接口是,同时两个接口有相同的default方法,则扩展类必须在方法的实现中选择返回某一个接口,如
public int getNumber(){ return interfaceA.getNumber();}
2. 接口和抽象类的不同之处
换句话说,就是既然有抽象类的存在,为什么还要诞生接口,为什么有经验的程序员经常说要面向接口编程?
- 最重要的一点,接口可以实现多继承,多继承可以有效地减少耦合度。试想一下:当一个派生类继承多个接口时,被它所派生的三个接口是 没有联系 的;但当一个派生类派生相同功能的三个基类时,被它所派生的三个基类必定 高度耦合 。
- 接口没有构造方法,并且在JDK8之后里面可以没有抽象方法(即 全为private和default方法 ),但是抽象类中必须有构造方法,里面也必须要有抽象方法。但是要注意,抽象类和接口 都不能实例化
- 接口可以更充分地实现低耦合和可扩展。试想一下:此时一个工厂之前生产过一个产品a,现在要求它再去生产产品b,如果没用接口和类继承的话,需要对产品a进行重构。但是如果之前在类中实现过一个生产产品a方法,此时我们只需要继承下来同时再override就可以了,但是我们需要知道,这是产品a和产品b是基类和派生类的关系,在现实生活中产品a和产品b应该是平行关系。为了满足他们在现实生活中的平行关系,我们不需要覆盖,而是先定义一个接口,然后产品a和产品n都去实现这个接口就ok了。如下程序:
interface product{
void a ();
}
class producta implements product{
public void a(){
System.out.println("a");
}
}
class productb implements product{
public void a(){
System.out.println("b");
}
}
如上程序所示,producta和productb实现了平行关系,但是换做类的话则需要逐个继承,则会增加耦合度。这样不好不好,,,
- 接口中的变量都是公共静态的,抽象类中则不然
- 接口是来源于 自上而下 设计的结果,抽象类是来源于 自下而上 重构的结果
3. 关于简单类和接口继承的内存逻辑模型
interface I {
void func1();
}
class o {
void func(){
System.out.println("you can hit me");
}
}
class c extends o implements I {
public void func1() {
System.out.println("Stonee is so handsome");
}
void func2(){
System.out.println("you can't hit me because the interface don't have me");
}
}public class Test {
public static void main(String [] args){
//inerface
I ic = new c();
ic.func1(); //ok
//a.func2(); //no
((c) ic).func2(); //ok
((c) ic).func(); //ok
//class parents
o oc = new c();
oc.func();
((c) oc).func1();
((c) oc).func2();
//class son
c cc = new c();
cc.func1();
cc.func2();
cc.func();
o oo = new o();
oo.func();
}
}
我们可以发现,a实例只能调用func1方法而调用不了func2方法,这正是由于接口的功劳,因为a是接口类型,而接口中只有func1方法,这样我们就通过接口实现了类和对象的隔离。同时,当类c内部要增加方法或实例时,对于a来说,没有一点影响,这就实现了完全的解耦合。
当我们把上述例子的interface换为class的时候,发现无论是class还是interfac做实例类型,结果竟然完全一样!!! 天呐撸为什么???
看下上面代码的内存逻辑图吧~
自己第一次用wps的流程图画,太鸡儿累了,而且还不是免费QAQ,所以之后我决定用纸和笔画(滑稽.jpg)
整个程序的执行过程就是先找到public class Test并加载该类,然后进入main函数对其他的类进行初始化,如上图的 堆·类 中所示
然后可以看到首先定义了 接口类型 的ic,并压栈,然后指向 堆实例 ,再由堆实例指向相应的 class类
现在我们 转到class c类 中,它分别 实现了接口I,继承了类o 。在他的虚表中,它先找到 接口的抽象方法 以及他们的 偏移量offset ,然后加载 父类0~5 并观察是否override,如果没有override,则方法指向父类o的那个方法。然后再写入自己实现接口的方法和自己定义的方法以及槽号。同时 建立并指向接口虚表 ,接口虚表的槽号由原来实现表的槽号加上偏移量即可得到,同时接口虚表中的方法还会指向虚表中实现的方法
当ic调用func1方法的时候,因为ic是interface类型的,会 先 在interface中找到func1的槽号,在这个程序中该方法的槽号是0,然后因为接口中只有这一个槽号,所以它的偏移地址也是0,故最终槽号为 槽号+偏移地址=0 ,所以JVM在class c的接口虚方法表中找到槽号0的方法func1,然后发现这个方法指向了class虚表中的槽号6的方法实现,然后实现func1方法。从这点我们也可以看出,接口会慢一点
同时,我们也很容易解释为什么 (I)ic.func2() 编译器会报错,因为JVM刚开始在接口中找func2方法就根本找不到
当对ic强制类型转换后,变成 ©ic 之后,因为刚开始是从c类中找func2方法,很显然可以找到,找到之后记录func2的槽号是7(图中写成func了,不好意思),然后再通过实例的指向找到c类对应的槽号7,再找到方法,ok,成功实现方法func2
之后的也是上述过程,请泥萌自己想一下吧(欢迎交流~)
其实上面的过程不完全正确,在JVM加载类的时候是发现一个加载一个的,并不是加载完类之后再运行程序
4. 通过程序来画出内存逻辑模型
一个包含数组,接口,类以及继承的程序:
声明:本程序完全是为了演示接口,类等的内存逻辑模型的demo
另:此程序有点冗余,但确实可以说明继承关系,而且内存逻辑模型是用手画的,只画了一些重要部分,没有上面程序画的详细
package chapter06;
/**
* 关于接口和类的继承以及不规则数组的内存逻辑模型
* @version 1.0 2019-4-8
* @author Stonee(http://www.stonee.club)
*/
public class InterfaceTestCourse {
public static void main(String [] args){
healthPigeon [][] a = new healthPigeon[2][];
a[0] = new healthPigeon[1];
a[1] = new healthPigeon[2];
for (healthPigeon[] e:
a) {
for (healthPigeon es:
e) {
es = new healthPigeon(); //一定要先对数组赋值,不然会指向null,然后报错
System.out.println();
es.eat();
es.move();
es.breathe();
es.feather();
es.fly();
es.fxxk();
System.out.println();
}
}
}
}
// 定义接口
// 定义父接口
interface canEat {
void eat(); //默认修饰符为 public abstract
default void fxxk(){
System.out.println("I like eat");
}
}
interface canMove {
void move();
default void fxxk(){
System.out.println("I like move");
}
}
interface canFly{
void fly(); // 子类已经重载,为什么说此方法未被调用?
}
interface canBreathe{
void breathe();
}// 定义子接口
interface bird extends canBreathe,canFly{
void feather();
}
// 定义类
// 定义父类
class pigeon implements bird{
@Override
public void feather() {
System.out.println("The pigeon have feather");
}
@Override
public void breathe() {
System.out.println("The pigeon can breathe");
}
@Override
public void fly() {
System.out.println("The pigeon can fly");
}
}
// 定义子类
class healthPigeon extends pigeon implements canEat,canMove{
@Override
public void eat() {
System.out.println("The cute Pigeon can eat");
}
@Override
public void move() {
System.out.println("The cute pigeon can move");
}
public void fxxk(){
canEat.fxxk(); //此处必须声明调用接口的哪个默认方法
}
}
有一点需要注意的是:在内存逻辑模型中,接口和接口之间的继承属于契约继承,派生类接口并不会像派生类一样将基类的方法槽号全部继承下来。这也就是说,当派生类接口类型的实例去实现基类接口的方法时,JVM在派生接口中不像派生类一样可以找到槽号,这就需要通过编译器的语法塘进行强制类型转换才行(即将派生类接口类型转换为基类接口)