跳到主要内容

楔子

1.1 、多态的含义

在java里,多态是同一个行为具有不同表现形式或形态的能力,即对象多种表现形式的体现,就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。

如下图所示:使用手机扫描二维码支付时,二维码并不知道客户是通过何种方式进行支付,只有通过二维码后才能判断是走哪种支付方式执行对应流程。

// 支付抽象类或者接口
public class Pay {
public String pay() {
System.out.println("do nothing!")
return "success"
}
}

// 支付宝支付
public class AliPay extends Pay {
@Override
public String pay() {
System.out.println("支付宝pay");
return "success";
}
}

// 微信支付
public class WeixinPay extends Pay {
@Override
public String pay() {
System.out.println("微信Pay");
return "success";
}
}

// 银联支付
public class YinlianPay extends Pay {
@Override
public String pay() {
System.out.println("银联支付");
return "success";
}
}

// 测试支付
public static void main(String[] args) {
// 测试支付宝支付多态应用
Pay pay = new AliPay();
pay.pay();

// 测试微信支付多态应用
pay = new WeixinPay();
pay.pay();

// 测试银联支付多态应用
pay = new YinlianPay();
pay.pay();
}

// 输出结果如下:
支付宝pay
微信Pay
银联支付

多态存在的三个必要条件:

  • 继承
  • 重写
  • 父类引用指向子类对象

比如:

Pay pay = new AliPay();

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

1.2、抽象类与接口

这样实现当然是可行的,但其实有一个小小的问题,就是Pay类当中的pay方法多余了。因为我们使用的只会是它的子类,并不会用到Pay这个父类。所以我们没必要实现父类Pay中的pay方法,做一个标记,表示有这么一个方法**,子类实现的时候需要实现它就可以了。

这就是抽象类和抽象方法的来源,我们可以把Pay做成一个抽象类,声明pay是一个抽象方法。抽象类是不能直接创建实例的,只能创建子类的实例,并且抽象方法也不用实现,只需要标记好参数和返回就行了。具体的实现都在子类当中进行。说白了抽象方法就是一个标记,告诉编译器凡是继承了这个类的子类必须要实现抽象方法,父类当中的方法不能调用。那抽象类就是含有抽象方法的类。

我们写出Pay变成抽象类之后的代码:

public abstract class Pay {
abstract public String pay();
}

很简单,因为我们只需要定义方法的参数就可以了,不需要实现方法的功能,方法的功能在子类当中实现。由于我们标记了pay这个方法是一个抽象方法,凡是继承了Pay的子类都必须要实现这个方法,否则一定会报错。

抽象类其实是一个擦边球,我们可以在抽象类中定义抽象的方法也就是只声明不实现,也可以在抽象类中实现具体的方法。在抽象类当中非抽象的方法,子类的实例是可以直接调用的,和子类调用父类的普通方法一样。但假如我们不需要父类实现方法,我们提出提取出来的父类中的所有方法都是抽象的呢?针对这一种情况,Java当中还有一个概念叫做接口,也就是interface,本质上来说interface就是抽象类,只不过是只有抽象方法的抽象类。

所以刚才的Pay通过接口实现如下:

interface Pay {
String pay();
}

把Pay变成了interface之后,子类的实现没什么太大的差别,只不过将extends关键字换成了implements。另外,子类只能继承一个抽象类,但是可以实现多个接口。早先的Java版本当中,interface只能够定义方法和常量,在Java8以后的版本当中,我们也可以在接口当中实现一些默认方法和静态方法。

接口的好处是很明显的,我们可以用接口的实例来调用所有实现了这个接口的类。也就是说接口和它的实现是一种要宽泛许多的继承关系,大大增加了灵活性。

以上虽然全是Java的内容,但是讲的其实是面向对象的内容,如果没有学过Java的小伙伴可能看起来稍稍有一点点吃力,但总体来说问题不大,没必要细扣当中的语法细节,get到核心精髓就可以了。

1.3、Go中的接口实现

Golang当中也有接口,但是它的理念和使用方法和Java稍稍有所不同,它们的使用场景以及实现的目的是类似的,本质上都是为了抽象。通过接口提取出了一些方法,所有继承了这个接口的类都必然带有这些方法,那么我们通过接口获取这些类的实例就可以使用了,大大增加了灵活性。

但是Java当中的接口有一个很大的问题就是侵入性Golang当中的接口解决了这个问题,也就是说它完全拿掉了原本弱化的继承关系,只要接口中定义的方法能对应的上,那么就可以认为这个类实现了这个接口。

我们先来创建一个interface,当然也是通过type关键字:

type Pay interface {
pay() string
}

我们定义了一个Pay的接口,当中声明了一个pay函数。也就是说只要是拥有这个函数的结构体就可以用这个接口来接收

type AliPay struct{}

type WeixinPay struct{}

type YinlianPay struct{}

func (a AliPay) pay() {
fmt.Println("支付宝pay")
}

func (w WeixinPay) pay() {
fmt.Println("微信pay")
}

func (y YinlianPay) pay() {
fmt.Println("银联pay")
}

之后,我们尝试使用这个接口来接收各种结构体的对象,然后调用它们的pay方法:

func main() {
var p Pay
p = AliPay{}
p.pay()
p = WeixinPay{}
p.pay()
p = YinlianPay{}
p.pay()
}

出来的结果是一样的!

golang中的接口设计非常出色,因为它解耦了接口和实现类之间的联系,使得进一步增加了我们编码的灵活度,解决了供需关系颠倒的问题。但是世上没有绝对的好坏,golang中的接口在方便了我们编码的同时也带来了一些问题,比如说由于没了接口和实现类的强绑定,其实也一定程度上增加了开发和维护的成本。

总体来说这是一个仁者见仁的改动,有些写惯了Java的同学可能会觉得没有必要,这是过度解绑,有些人之前深受其害的同学可能觉得这个进步非常关键。但不论你怎么看,这都不影响我们学习它,毕竟学习本身是不带立场的

提示

接口本身就是一种规范,能让大家在一个框架下开发,比如张三新进入部门,开发一个新的支付功能,在接口的限制下,开发就会会规范很多。就像USB接口一样,定义统一接口,无论外部实现的是音响还是硬盘,必须都按定义好的数据格式开发。