【Java】接口
前言
在Java中,接口和抽象类为我们提供了一种将类的对外接口与实现分离的更加结构化的方法。下面将介绍抽象类,它是普通的类与接口之间的一种中庸之道,接着再介绍接口。
抽象类和抽象方法
当我们仅是希望有一个基类可以提供统一的接口去控制它导出的所有子类,并且该基类没有被实例化的必要时,我们就可以使用抽象类
去创建这个基类。为了使抽象类不被实例化,我们就需要使用某种机制来限制。于是,Java中提供一种叫做抽象方法
的机制(相当于C++中的纯虚函数),这种方法是不完整的:仅有声明而没有方法体。
abstract void fun(); //抽象方法声明语法
抽象类如果包含抽象方法,那么抽象类就是不完整的,试图产生该类的对象的时候,编译器就会抛出错误信息。
所以,我们就将包含抽象方法的类叫做抽象类。当然,如果一个类包含一个或者多个抽象方法,该类就必须被限定为抽象的。并且使用关键字abstract
来限定。
需要注意,Java中的抽象类中除了包含抽象方法也可以包含具体的数据和具体的方法,但是为了程序的清晰度,抽象类中方法最好还是全是抽象方法。如果子类继承自某一个抽象类,并且想创建子类的对象,那么抽象类中的所有抽象方法在子类中都要被实现。否则,子类仍旧是一个抽象类,无法被实例化。
一个抽象类举例:
public abstract Animal{
private String name;
public abstract void eat();
public String toString { return name;}
}
接口
如果说abstract
关键字使得可以在类中创建一个或多个没有方法体的方法给类提供了抽象的概念,那么interface
关键字就使得类的抽象更前一步。
使用interface关键字产生的类是一个完全抽象的类,其中的方法没有任何具体实现。即,只允许创建者确定类中的方法名、参数列表和返回类型,但是没有任何方法体。
一个类如果使用了某个接口那么就必须得实现该接口中规定的所有方法,这倒像是“要干什么事,就必须遵守某种协议”一样。
通常,为使一个方法可以在类间调用,两个类都必须出现在编译时间里,以便Java编译器可以检查以确保方法特殊是兼容的。这个需求导致了一个静态的不可扩展的类环境。在一个系统中不可避免会出现这种状况:函数在类层次中越堆越高以致该机制可以为越来越多的子类可用。
接口的设计避免了这个问题。它们把方法或方法系列的定义从类层次中分开。因为接口是在和类不同的层次中,与类继承层次无关的类实现相同的接口是可行的。这是实现接口的真正原因所在,接口可以使代码之间解除继承限制,降低代码耦合性。
接口的定义和实现
接口的创建同类创建一样,只需将class关键字替换为interface关键字,里面的方法只用声明不用实现即可。接口中也可以包含域,但是这些域都是隐式地为static和final的。要让某个类遵循某个接口(或者是一组接口)就需要使用implements
关键字。
接口的定义
public interface USB{
String name = "USB";
public String getName();
}
访问权限修饰符可以为public也可以不写。接口中的方法不能设置成private的,让使用该接口的类不能够实现该方法,所以接口中方法都是默认地为public。接口中的域是static和final的,所以定义时一定要初始化。
接口的实现
一旦接口被定义,一个或多个类便可以实现该接口。为实现该接口,在定义的类名后使用implement接接口名,然后在类中实现接口定义的方法。
可以使用接口引用指向实现了接口的类对象,就类似于使用基类的引用指向子类对象,于是就可以实现多态功能。
public class Mouse implements USB{
//实现接口中定义的方法
public String getName(){ return "Mouse USB";}
public static void main(String args[]){
USB usb = new Mouse(); //使用接口引用指向实现了接口的类对象
}
}
接口的局部实现
如果一个类不完全实现一个接口中的方法那么该类就必须使用abstract修饰。也就是说,抽象类可以不完全实现接口中的方法。
完全解耦
只要一个方法操纵的是类而非接口,那么你就只能在这个类或其子类上使用这个方法。即只能操纵有继承关系的类。如果你将此方法应用于非此继承结构中的类,那么就会出问题。
若这个方法是接口中的,那么该方法便可以应用在实现了该接口的类对象上,不需要考虑类之间是否有继承性。这样就可以写出复用性更好的代码~
代码说明(参考[1]):
class Processor{
public String name() {
return getClass().getSimpleName();
}
Object process(Object input) {
return input;
}
}
class Upcase extends Processor{
@Override
String process(Object input) {
return ((String)input).toUpperCase();
}
}
class DownCase extends Processor{
@Override
String process(Object input) {
return ((String)input).toLowerCase();
}
}
public class Apply {
//使用基类引用统一控制子类对象
public static void process(Processor p, Object s) {
System.out.println("Using Processor " + p.name());
System.out.println(p.process(s));
}
public static void main(String[] args) {
String s = "This Road Is Long.";
process(new Upcase(), s);
process(new DownCase(), s);
}
}
Apply.process()方法使用基类引用去同一控制对象。
在本例中,创建一个能够根据所传参数对象不同而具有不同行为方法,被称为策略设计模式
。这类方法包含所要执行的算法中不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里,Processor对象就是一个策略,在main()中有两种不同类型的策略应用到了String类型的s对象上。
现在有一组电子滤波器,它们的代码可能适用于Apply.process()方法。
class Waveform{
private static long counter;
private final long id = counter++;
public String toString() { return "Waveform:" + id;}
}
class Filter{
public String name() { return getClass().getSimpleName();}
public Waveform process(Waveform input) { return input;}
}
class LowPass extends Filter{
private double cutoff;
public LowPass(double cutoff) { this.cutoff = cutoff;}
@Override
public Waveform process(Waveform input) { return input;}
}
class HighPass extends Filter{
private double cutoff;
public HighPass(double cutoff) { this.cutoff = cutoff;}
@Override
public Waveform process(Waveform input) { return input;}
}
Filter和Processor具有相同的接口,但是因为Filter不是继承自Processor的,所以不能将Filter应用于Apply.process()方法。Filter不能使用Apply.process()方法的主要原因在于:Apply.process()方法和Processor之间的耦合性过于紧密,导致复用Apply.process()代码时被禁止。
但是,如果将Processor换成是一个接口,那么这些限制便会松动,也就可以复用Apply.process()方法。
public interface Processor{
String name();
Object process(Object input);
}
复用代码的第一种方式就是客户端程序员遵循接口来编写类。
public abstract class StringProcessor implements Processor{
@Override
public String name() {
return getClass().getSimpleName();
}
public abstract String process(Object input);
public static void main(String[] args) {
String s = "This Road is Long.";
Apply.process(new Upcase(), s);
Apply.process(new Downcase(), s);
}
}
class Upcase extends StringProcessor{
@Override
public String process(Object input) { return ((String)input).toUpperCase(); }
}
class Downcase extends StringProcessor{
@Override
public String process(Object input) { return ((String)input).toLowerCase(); }
}
有时候就会遇见无法修改到类,在这种情况下,就可以使用适配器设计模式
。适配器中的代码将接受你所拥有的接口,并产生你所需要的接口。比如,修改电子滤波器使其可以使用Apply.process()。
class FilterAdapter implements Processor{
Filter filter;
public FilterAdapter(Filter filter) {
this.filter = filter;
}
public String name() { return filter.name();}
public Waveform process(Object input) {
return filter.process((Waveform)input);
}
}
public class FilterProcessor {
public static void main(String[] args) {
Waveform w = new Waveform();
Apply.process(new FilterAdapter(new LowPass(1.0)), w);
Apply.process(new FilterAdapter(new HighPass(2.0)), w);
}
}
/*
output:
Using Processor LowPass
Waveform:0
Using Processor HighPass
Waveform:0
*/
在这种使用适配器的方式中,FilterAdapter的构造器接受Filter的所有接口,然后生成需要的Processor接口对象。
将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性。
Java中的多重继承
接口是没有任何具体实现的,即没有任何与接口相关的存储。因此,多个接口便可以组合使用。
使用具体类和多个接口的例子:
interface CanFight{
void fight();
}
interface CanSwim{
void swim();
}
interface CanFly{
void fly();
}
class ActionCharacter{
public void fight(); //与CanFight具有相同的方法特征签名
}
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly{
public void swim(){}
public void fly(){}
}
public class Adventure{
public static void fi(CanFight x){ x.fight(); }
public static void sw(CanSwim x){ x.swim(); }
public static void fl(CanFly x){ x.fly();}
public static void ac(AcionCharacter x){ x.fight();}
public static void main(String args[]){
Hero h = new Hero();
fi(h);
sw(h);
fl(h);
ac(h);
}
}
前面说过一个类要使用一个接口就要实现该接口中的全部方法,但是很明显Hero没有显式实现CanFight中的fight()方法。仔细观察可以发现,Hero继承的具体类ActionCharacter中有实现了的fight()方法。这样,Hero也相当于实现了fight()方法。需要注意,继承的具体类要写在前面。
使用接口的核心原因
上面的例子展示了使用接口的核心原因:
- 为了能够向上转型为多个基类型;
- 第二个原因则是与抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是一个接口。
使用继承来扩展接口
我们可以通过继承一个接口并添加新的方法声明以生成新的接口,或者通过继承多个接口以实现新的接口。这两种方法都是扩展接口的主要方法。
interface Monster{
void menace();
}
//继承并添加新的方法以生成新的接口
interface DangerousMonster extends Monster{
void destroy();
}
//继承多个接口,组合成一个新接口
interface Lethal{
void kill();
}
interface Vampire extends DangerousMonster, Lethal{
void drinkBlood();
}
组合接口时的名字冲突
在前面多重继承中,遇到CanFight和ActionCharacter都有一个相同的方法void fight(),但是这并没有导致什么问题。
但是,如果在组合多个接口时出现两个签名不一样或者返回类型不同的方法时,会不会出现问题呢?
interface I1{ void f(); }
interface I2{ int f(int i);}
interface I3{ int f();}
class C { public int f(){ return 1;}}
class C2 implements I1, I2{
//两个方法重载
public void f(){}
public int f(int i){ return i;}
}
class C3 extends C implements I2{
public int f(int i){ return i;} //重载
}
class C4 extends C implements I3{
//同CanFight和ActionCharacter一样
}
//以下两种方式不行!!!
//class C5 extends C implements I1{}
//interface I4 extends I1, I3{}
重载仅依靠返回类型是无法区分的。在打算组合不同接口中使用相同的方法名通常会造成代码可读性的混乱,这是需要避免的。
接口与工厂
接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式
。与直接调用构造器不同,在工厂对象上调用的是创建方法,为该工厂对象将直接生成接口的某个实现的对象。理论上,通过这种方式,代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。使用工厂模式的一个常见原因便是创建框架。
interface Service{
void method1();
void method2();
}
interface ServiceFactory{
Service getService();
}
class Implementation1 implements Service{
Implementation1(){}
public void method1(){System.out.println("Implementation1 method1");}
public void method2(){System.out.println("Implementation1 method1");}
}
class Implementation1Factory implements ServiceFactory{
public Service getService(){ return new Implementation1(); }
}
class Implementation2 implements Service{
Implementation2(){}
public void method1(){System.out.println("Implementation2 method1");}
public void method2(){System.out.println("Implementation2 method1");}
}
class Implementation2Factory implements ServiceFactory{
public Service getService(){ return new Implementation2(); }
}
public class Factories{
public static void serviceConsumer(ServiceFactory fact){
Service s = fact.getService();
s.method1();
s.method2();
}
public static void main(String args[]){
serviceConsumer(new Implementation1Factory());
serviceConsumer(new Implementation2Factory());
}
}
/*
output:
Implementation1 method1
Implementation1 method1
Implementation2 method1
Implementation2 method1
*/
小结
Java中接口的最大意义就在于对外接口与实现分离,一个接口可以有不同的实现,减少了代码中的耦合性。在本篇博文中还提到了三种设计模式:策略模式、适配器模式以及工厂模式,对三种设计模式介绍地比较简单。在看《Java编程思想》时,也是首次学习,会存在不少疏忽之处,望各位看官指出。最后,在Java 8中,接口是有新的特性的,可以拥有方法实体,但是要声明为default。对接口的介绍暂时到此,以后再继续深入介绍。
参考:
[1] Eckel B. Java编程思想(第四版)[M]. 北京: 机械工业出版社, 2007