什么是接口
接口是对类的一种抽象描述,准确的说是对类的行为(方法)的抽象描述。所有实现了某个接口的类都应该实现这个接口所描述行为。通过接口,我们可以了解实现了接口的类能够对外界提供哪些方法和行为。
如何使用接口
定义接口
接口通过interface
关键字来定义,例如,现在需要定义一个接口来描述游泳这个行为,假设游泳有摆动法游泳,滑动法游泳,喷射法游泳三种方式。那么我们可以定义如下ISwim
接口如下。
public interface ISwim {
String SWIM_BY_SWING = "swim by swing";
String SWIM_BY_SLIDE = "swim by slide";
String SWIM_BY_JET = "swim by jet";
void swim();
}
通过ISwim
接口,我们可以看到,在接口中只定义了方法(描述),但是并没有提供方法的具体实现。同时,方法没有访问修饰符(public,private,protected)来限定。此外,我们还在接口中定义了变量,如SWIM_BY_SWING,SWIM_BY_SLIDE,SWIM_BY_JET
。在接口中定义的方法和变量有如下特点。
接口中的变量会被隐式地指定为public static final类型(且只能是public static final类型,用其他类型修饰编译器会报错)
接口中的方法会被隐式地指定为public abstract类型(在Java8以前,方法只能是public abstract类型,在Java8以后,允许在接口中定默认方法和static方法)
基于上面两个特点,我们可以得出下面的ISwim
接口和上面的接口是完全相同的。
public interface ISwim {
public static final String SWIM_BY_SWING = "swim by swing"; //摆动法游泳
public static final String SWIM_BY_SLIDE = "swim by slide"; //滑动法游泳
public static final SWIM_BY_JET = "swim by jet"; //喷射法游泳
public abstract void swim();
}
实现接口
由于接口只提供了方法的抽象描述,没有提供方法的具体定义,所以单纯的接口没有意义的,因为我们不能像创建对象一样直接new 一个接口对象。必须有实体类来实现接口才有意义,而实现接口的本质就是把接口中抽象的方法描述变成具体的方法实现。实现一个接口时要使用implements
关键字。一个接口可以被多个类实现,实现了接口的类具有接口所描述的特征。下面定义了三个类分别实现ISwim
接口。
public class Fish implements ISwim{
@Override
public void swim() {
System.out.println(ISwim.SWIM_BY_SWING);
}
}
public class Frog implements ISwim{
@Override
public void swim() {
System.out.println(ISwim.SWIM_BY_SLIDE);
}
}
public class Octopus implements ISwim{
@Override
public void swim() {
System.out.println(ISwim.SWIM_BY_JET);
}
}
实现接口和继承基类有点类似,但是实现接口时,接口中定义的方法必须声明为public的,例如上面三个类中的swim
方法。这是因为接口中的方法默认是public的。如果用其他访问修饰符来改变方法的可见性,编译器会报错。
一个接口可以被多个类实现,同时一个类可以实现多个接口,实现多个接口时,所有接口都放到implements
关键字后面,接口之间通过逗号来分割,语法如下。
class A implements Interface1,Interface2,Interface3...{
}
例如,定义一个接口,描述在陆地行走这个行为。
public interface IWalk {
void walk();
}
因为青蛙(Frog)是两栖动物,既可以在水中游泳,也可以在陆地上行走,所以青蛙同时具有游泳和行走两种行为,那么我们可以让Frog
类同时实现ISwim
和IWalk
接口。
public class Frog implements ISwim, IWalk {
@Override
public void swim() {
System.out.println(ISwim.SWIM_BY_SLIDE);
}
@Override
public void walk() {
System.out.println("walk by jump");
}
}
一个类可以实现多个接口,这是一个很有用的特性,它是Java中实现多继承的一种重要方式。因为在Java中,一个类只能有一个基类,只能单继承,而接口的存在,让Java拥有了多重继承的能力。
在上面的例子中,除了实现多个接口,也可以通过继承的方式将多个接口组合成一个接口来扩展接口的能力。例如,可以定义一个接口来描述两栖动物。
public interface IAmphibians extends ISwim{
void walk();
}
这样IAmphibians
接口同时描述了swim
和walk
两种能力,接下来让Frog
类实现IAmphibians
接口。
public class Frog implements IAmphibians {
@Override
public void swim() {
System.out.println(ISwim.SWIM_BY_SLIDE);
}
@Override
public void walk() {
System.out.println("walk by jump");
}
}
使用接口
如果一个类实现了一个接口,那么这个类可以向上转型为接口类型,就像可以转型为基类一样。虽然我们没有办法直接new一个接口类型的对象,但是我们却可以把实现了接口的类向上转型为接口的类型。基于这个特性,在代码中可以使用接口类型来代替具体的类,这样做可以提供更高的抽象性,将定义和实现分离,保证代码的扩展性。例如,我们现在需要一个方法来显示一个动物是如何游泳的,定义方法如下。
public class TestSwim {
public static void testSwim(ISwim swim) {
swim.swim();
}
public static void main(String[] args) {
testSwim(new Fish());
testSwim(new Frog());
testSwim(new Octopus());
}
}
通过将接口类型ISwim
作为testSwim
方法的参数,我们避免了testSwim
方法和具体的类型绑定。这样可以把方法和具体的类实现解耦,后面任何实现了ISwim
接口的类都可以使用这个方法。便于代码的复用和扩展。
在接口中定义静态方法
在Java8之前,接口中只能存在方法的描述,不能定义任何方法的实体,并且方法默认是public abstract类型。所以在Java8之前,如果有一些通用的方法,很有可能会放在工具类里边。但是Java8以后,允许在接口中定义静态方法。这样,可以将一些静态的工具方法直接定义在接口中,而不再需要一个单独的工具类。例如,可以把TestSwim
类中的静态方法testSwim
移动到接口ISwim
中。
public interface ISwim {
String SWIM_BY_SWING = "swim by swing";
String SWIM_BY_SLIDE = "swim by slide";
String SWIM_BY_JET = "swim by jet";
void swim();
static void testSwim(ISwim swim) {
swim.swim();
}
}
这样,就可以直接在其他类中通过ISwim.testSwim
的方式来调用testSwim
方法。需要注意的是,在Java8之前,在接口中定义静态方法编译器是会直接报错的。
在接口中定义默认方法
在Java8后,除了可以在接口中定义静态方法外,还可以在接口中定义默认方法。默认方法通过default
关键字来标识,默认方法不仅提供了方法描述,还提供了方法的具体实现。例如。
public interface Collection {
int size();
default boolean isEmpty() {
return size()==0;
}
}
在Collection
接口中,为isEmpty
方法提供了一个默认实现。这样在实现Collection
接口的时候,我们可以只关注size
方法的实现,而不需要关心isEmpty
方法。
public class CollectionImpl implements Collection{
@Override
public int size() {
return 1;
}
}
当然,在CollectionImpl
中也可以重写isEmpty
方法。同时,通过Collection
接口可以看到在默认方法中,还可以调用接口中的其他方法。
那为什么要在接口中增加这个能力呢?在接口中定义默认方法主要有两个用途:
- 当接口中有多个方法时,默认方法可以让实现接口的类只关注他们要实现的方法,而对其他方法则不必关心。
- 当接口后续拓展时,通过把新增的方法定义为默认方法,可以避免修改之前已经实现了该接口的类,保证向前兼容。
第一点很好理解,关于第二点,以Collection
接口为例,假设最开始Collection
接口定义如下。
public interface Collection {
int size();
}
然后,有一个类CollectionImpl
实现了Collection
接口。
public class CollectionImpl implements Collection{
@Override
public int size() {
return 1;
}
}
现在,如果想在Collection
接口中增加一个isEmpty
方法,那么我们就需要修改CollectionImpl
类,让CollectionImpl
类实现isEmpty
方法,如果代码中有很多类都实现了Collection
接口,我们就需要修改很多地方,这违反了面向对象设计的开闭原则。但是,如果isEmpty
方法是默认方法,那么Collection
接口的修改对先前已经实现了Collection
接口的类就是无感的。
以上,就是接口的一些基本特性和使用方法。