面向接口
对比
使用面向接口和面向对象和面向过程实现一样的功能对比
使用面向接口
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...
*/
}看起来代码量最少,因为结构简单,数量少,如果有的动物越来越多,重复的代码也会越来越多,并且无法出现两只狗,例如一只旺财,一直小白,如果希望出现两只狗,只能新建代码,十分繁琐。
总结
面向接口:实现复杂,解耦效果好,代码复用高,模块清晰,需要抽象设计,前期设计比较困难
面向对象:实现复杂,解耦效果中,代码复用中,模块清晰,同时时间久了,可能会出现无数个类继承的情况,旧的代码几乎无法修改
面向过程:实现简单,解耦效果差,代码无复用,无语言层面模块划分
面向接口是实现原理
- 编译(Compile): 编译器会再编译时保证类型安全
- 运行(Run): 动态绑定 (Dynamic Binding) 和 虚方法表 (Virtual Method Table, vtable) 实现调用正确的方法
编译
java
Animal dog = new Dog();- 编译器看到
Animal dog时,首先会关注Animal,也就是 dog 的类型声明- 编译器会根据
Animal类型寻找到类的定义javapublic interface Animal { void eat(); void sleep(); } - 根据
Animal的定义,定制一个方法表T用于静态类型检查,编译结束后就会消失
- 编译器会根据
- 有了 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...- 当程序运行到
eat()和sleep()方法时,程序需要决定运行 Dog 里面的方法还是 Cat 里面的方法- 这里就会通过 动态绑定(Dynamic Binding) 或 后期绑定 (Late Binding) 实现
在dog变量里,包含了两部分信息- 静态类型:Animal
- 动态类型:Dog
- 虚方法表 (Virtual Method Table, vtable)
当一个类(比如 Dog)被加载到内存中时,系统会为这个类创建一个叫做“虚方法表”的结构。这个表本质上是一个函数指针数组- 表的每一项都指向一个具体的方法实现
- 如果一个类实现了某个接口,或者重写了父类的方法,vtable中对应的条目就会指向这个类自己的方法实现
- 对于实现了同一个接口
Animal的所有类,它们的 vtable 中,eat()和sleep()方法会处在相同的索引位置。这是由编译器保证的 - 当创建一个对象
new Dog()时,这个对象在内存中除了包含自己的数据(如name),还会包含一个隐藏的指针 vpointer。这个指针就指向Dog类的 vtable
- 这里就会通过 动态绑定(Dynamic Binding) 或 后期绑定 (Late Binding) 实现
- 程序会通过
dog变量找到找到堆中的对象,它本质是Dog类型 - 从刚刚找到的对象的头部中,找到 vpointer 指针,通过这个指针找到
Dog类的 vtable - 找到
Dog类的 vtable 之后,读取eat()和sleep()的索引,这里的索引是编译时就确定了的 - 通过
eat()和sleep()的索引以及Dog类的 vtable ,可以直接找到Dog类的eat()和sleep()的实现机器码 - 运行被找到的机器码
动态绑定的这些操作,大部分都是在运行时进行,相比于静态绑定,性能会稍微慢一些,因为它多了几次内存访问(解引用指针)的操作
但是这里的开销还是非常小的,和后续代码维护相比起来,还是可以忽略的