Skip to content

面向接口

对比

使用面向接口和面向对象和面向过程实现一样的功能对比

使用面向接口

java
// Ainimal Interface
public interface Animal {
    void eat();
    void sleep();
}

// Dog Class
public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog eating...");
    }
    @Override
    public void sleep() {
        System.out.println("Dog sleeping...");
    }

    // Dog 特有函数
    public void playWithDog() {
        System.out.println("Play with dog...");
    }
}

// Cat Class
public class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("Cat eating...");
    }
    @Override
    public void sleep() {
        System.out.println("Cat sleeping...");
    }

    // Cat 特有函数
    public void playWithCat() {
        System.out.println("Play with cat...");
    }
}

// Play Class
public class Play {
    // Play with animal
    public void playWithXXX(Animal animal) {
        System.out.println("Play starting...");
        animal.eat();
        if (animal instanceof Dog tDog) {
            tDog.playWithDog();
        } else if (animal instanceof Cat tCat) {
            tCat.playWithCat();
        }
        animal.sleep();
    }
}

// Main Class
public static void main(String[] args) {
    Animal dog = new Dog();
    dog.eat(); // Dog eating...
    dog.sleep(); // Dog sleeping...

    Animal cat = new Cat();
    cat.eat(); // Cat eating...
    cat.sleep(); // Cat sleeping...

    Play p = new Play();
    p.playWithXXX(dog);
    /*
    Play start...
    Dog eating...
    Play with Dog...
    Dog sleeping...
    */
    p.playWithXXX(cat);
    /*
    Play start...
    Cat eating...
    Play with Cat...
    Cat sleeping...
    */
}

使用面向对象

java
// Ainimal Class
public class Animal {
    void eat() {
        System.out.println("Animal eating...");
    }
    void sleep() {
        System.out.println("Animal sleeping...");
    }
}

// Dog Class
public class Dog implements Animal {
    @Override
    public void eat() {
        System.out.println("Dog eating...");
    }
    @Override
    public void sleep() {
        System.out.println("Dog sleeping...");
    }

    // 狗独有
    public void playWithDog() {
        System.out.println("Play with dog...");
    }
}

// Cat Class
public class Catimplements Animal {
    @Override
    public void eat() {
        System.out.println("Cat eating...");
    }
    @Override
    public void sleep() {
        System.out.println("Cat sleeping...");
    }

    // 猫独有
    public void playWithCat() {
        System.out.println("Play with cat...");
    }
}

// Play Class
public class Play {

    // 先吃饭->和狗玩->睡觉
    public void playWithDog(Dog dog) {
        System.out.println("Play start...");
        dog.eat();
        dog.playWithDog();
        dog.sleep();
        /*
        Play start...
        Dog eating...
        Play with Dog...
        Dog sleeping...
        */
    }

    // 先吃饭->和狗玩->睡觉
    public void playWithCat(Cat Cat) {
        System.out.println("Play start...");
        Cat.eat();
        Cat.playWithCat();
        Cat.sleep();
    }
}

// Main Class
public static void main(String[] args) {
    Dog dog = new Dog();
    dog.playWithDog();
    /*
    Play start...
    Dog eating...
    Play with Cat...
    Dog sleeping...
    */

    Cat cat = new Cat();
    cat.playWithDog();
    /*
    Play start...
    Cat eating...
    Play with Cat...
    Cat sleeping...
    */
}

面向对象和面向接口对比起来,虽然 main 函数的代码量少了,但是 Play class 里面的代码增加了许多,同时这里面的代码有非常多的重复,除了 PlayWithxxx 方法,都是重复写的,并且它们和 Dog class, Cat class 耦合在一起了。
这里的 PlayWithxxx 主要是为了展示面向对象的缺点

使用面向过程

java
// Dog class
public class Dog {
    public static void eat() {
        System.out.println("Dog eating...");
    }
    public static void sleep() {
        System.out.println("Dog sleep...");
    }
}

// Cat class
public class Cat {
    public static void eat() {
        System.out.println("Cat eating...");
    }
    public static void sleep() {
        System.out.println("Cat sleep...");
    }
}

// Main class
public static void main(String[] args) {
    Dog.eat();
    Dog.sleep();
    /*
    Dog eating...
    Dog sleeping...
    */

    Cat.eat();
    Cat.sleep();
    /*
    Cat eating...
    Cat sleeping...
    */
}

看起来代码量最少,因为结构简单,数量少,如果有的动物越来越多,重复的代码也会越来越多,并且无法出现两只狗,例如一只旺财,一直小白,如果希望出现两只狗,只能新建代码,十分繁琐。

总结

面向接口:实现复杂,解耦效果好,代码复用高,模块清晰,需要抽象设计,前期设计比较困难
面向对象:实现复杂,解耦效果中,代码复用中,模块清晰,同时时间久了,可能会出现无数个类继承的情况,旧的代码几乎无法修改
面向过程:实现简单,解耦效果差,代码无复用,无语言层面模块划分

面向接口是实现原理

  1. 编译(Compile): 编译器会再编译时保证类型安全
  2. 运行(Run): 动态绑定 (Dynamic Binding) 和 虚方法表 (Virtual Method Table, vtable) 实现调用正确的方法

编译

java
    Animal dog = new Dog();
  1. 编译器看到 Animal dog 时,首先会关注 Animal ,也就是 dog 的类型声明
    1. 编译器会根据 Animal 类型寻找到类的定义
      java
      public interface Animal {
          void eat();
          void sleep();
      }
    2. 根据 Animal 的定义,定制一个方法表T用于静态类型检查,编译结束后就会消失
  2. 有了 T ,编译器就会拿着 T 检查代码
    java
    Animal dog = new Dog();
    dog.eat(); // 编译通过
    dog.sleep(); // 编译通过
    
    dog.play(); // 编译不通过
    假如 dog 有一个特有的方法 play() ,一样会出现报错,因为 dog 的类型是 Animal ,编译器还是会使用 T 来进行检查

运行

java
    Animal dog = new Dog();
    dog.eat(); // Dog eating...
    dog.sleep(); // Dog sleeping...
  1. 当程序运行到 eat()sleep() 方法时,程序需要决定运行 Dog 里面的方法还是 Cat 里面的方法
    1. 这里就会通过 动态绑定(Dynamic Binding)后期绑定 (Late Binding) 实现
      dog 变量里,包含了两部分信息
      • 静态类型:Animal
      • 动态类型:Dog
    2. 虚方法表 (Virtual Method Table, vtable)
      当一个类(比如 Dog)被加载到内存中时,系统会为这个类创建一个叫做“虚方法表”的结构。这个表本质上是一个函数指针数组
      • 表的每一项都指向一个具体的方法实现
      • 如果一个类实现了某个接口,或者重写了父类的方法,vtable中对应的条目就会指向这个类自己的方法实现
    3. 对于实现了同一个接口 Animal 的所有类,它们的 vtable 中,eat()sleep() 方法会处在相同的索引位置。这是由编译器保证的
    4. 当创建一个对象 new Dog() 时,这个对象在内存中除了包含自己的数据(如 name),还会包含一个隐藏的指针 vpointer。这个指针就指向 Dog 类的 vtable
  2. 程序会通过 dog 变量找到找到堆中的对象,它本质是 Dog 类型
  3. 从刚刚找到的对象的头部中,找到 vpointer 指针,通过这个指针找到 Dog 类的 vtable
  4. 找到 Dog 类的 vtable 之后,读取 eat()sleep() 的索引,这里的索引是编译时就确定了的
  5. 通过 eat()sleep() 的索引以及 Dog 类的 vtable ,可以直接找到 Dog 类的 eat()sleep() 的实现机器码
  6. 运行被找到的机器码

动态绑定的这些操作,大部分都是在运行时进行,相比于静态绑定,性能会稍微慢一些,因为它多了几次内存访问(解引用指针)的操作
但是这里的开销还是非常小的,和后续代码维护相比起来,还是可以忽略的