接口


接口和内部类为我们提供了一种将接口与实现分离的更加结构化的方法。

接口和抽象类

继承中的基类的方法往往是“哑”(dummy)方法。若要调用这些方法,就会出现一些错误。这是因为基类的目的是为它的所有导出类创建一个通用接口。建立通用接口的唯一理由是,不同的子类可以用不同的方式表示此接口。通用接口建立起一种基本形式,以此表示所有导出类的共同部分。另一种说法是将基类称作抽象基类或简称抽象类 。

如果我们只有一个像动物(Animal)这样的抽象类,那么该类的对象几乎没有任何意义。我们创建抽象类是希望通过这个通用接口操纵一系列类。因此,Animal只是表示了一个接口,没有具体的实现内容;因此,创建一个Animal对象没有什么意义,并且我们可能还想阻止使用者这样做。通过让Animal中的所有方都产生错误,就可以实现这个目的。但是这样做会将错误信息延迟到运行时才获得,并且需要在客户端进行可靠、详尽的测试。所以最好是在编译时捕获这些问题。

为此,Java提供一个叫做抽象方法的机制,这种方法是不完整的,仅有声明而没有方法体。下面是抽象方法声明所采用的语法:

abstract  void f();

包含抽象方法的类叫做抽象类。如果一个类包含一个或多个抽象方法,该类必须被限定为抽象的。(否则编译器就会报错。)如果一个抽象类不完整,那么当我们试图产生该类的对象时,编译器会怎样处理呢?由于为抽象类创建对象是不安全的,所以我们会从编译器那里得到一条出错消息。这样,编译器会确保抽象类的纯粹性,我们不必担心会误用它。

如果从一个抽象类继承,并想创建该新类的对象,那么就必须为基类中的所有抽象方法提供方法定义。如果不这样做(可以选择不做),那么导出类便也是抽象类,且编译器将会强制我们用abstract关键字来限定这个类。

我们也可能会创建一个没有任何抽象方法的抽象类。考虑这种情况:如果有一个类,让其包含任何abstract方法都显得没有实际意义,而且我们也想要阻止产生这个类的任何对象,那么这时这样做就很有用了。

abstract class Animal{
	private int id = 1;
	public abstract void play();
	public abstract void play(String name);
	public String f(){
		return "Animal.f()";
	}
}

abstract class Dog2 extends Animal{
	abstract void Dog2_f();
}

class Dog extends Dog2{
	@Override
	public void play() {
		System.out.println("Dog playing");
	}
	@Override
	public void play(String name) {
		System.out.println("Dog playing"+name);
	}
	@Override
	public void Dog2_f() {
		System.out.println("Dog.Dog2_f()");
	}
}

创建抽象类和抽象方法非常有用,因为它们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样来使用它们。抽象类还是很有用的重构工具,因为它们使得我们可以很容易地将公共方法沿着继求层次结构向上移动。

接口

interface关键字使抽象的概念更向前迈进了一步。abstract关键字允许人们在类中创建一个或多个没有任何定义的方法——提供了接口部分,但是没有提供任何相应的具体实现,这些实现是由此类的继承者创建的。interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体实现。它允许创建者确定方法名、参数列表和返回类型,但是没有任何方法体。接口只提供了形式,而未提供任何具体实现。

一个接口表示:“所有实现了该特定接口的类看起来都像这样”。因此,任何使用某特定接口的代码都知道可以调用该接口的哪些方法,而且仅需知道这些。因此,接口被用来建立类与类之间的协议。

但是,interface不仅仅是一个极度抽象的类,因为它允许人们通过创建一个能够被向上转型为多种基类的类型,来实现某种类似多重继变种的特性。

要想创建一个接口,需要用interface关键字来替代class关键字。就像类一样,可以在interface关键字前面添加public关键字(但仅限于该接口在与其同名的文件中被定义)。如果不添加public关键字,则它只具有包访问权限,这样它就只能在同一个包内可用。接口也可以包含域,但是这些域隐式地是static和final的。

要让一个类遵循某个特定接口(或者是一组接口),需要使用implements关键字,它表示:“interface只是它的外貌,但是现在我要声明它是如何工作的。”除此之外,它看起来还很像继承。例:

public interface Animal {
	void play();
	int i = 1;
}
public class Dog implements Animal {

	@Override
	public void play() {
		System.out.println("playing"+Animal.i);
	}
	public static void main(String[] args) {
		Dog d = new Dog();
		d.play();
		Animal.i = 2;//Animal.i cannot be assigned
	}
	//输出:Dog playing1
}

可以选择在接口中显式地将方法声明为public的,但即使你不这么做,它们也是public的。因此,当要实现一个接口时,在接口中被定义的方法必须被定义为是public的;否则,它们将只能得到默认的包访问权限,这样在方法被继承的过程中,其可访问权限就被降低了,这是Java编译器所不允许的。要注意的是,在接口中的每一个方法确实都只是一个声明,这是编译器所允许的在接口中唯一能够存在的事物。

完全解耦

只要一个方法操作的是类而非接口,那么你就只能使用这个类及其子类。如果你想要将这个方法应用于不在此继承结构中的某个类,那么你就会触霉头了。接口可以在很大程度上放宽这种限制,因此它使得我们可以编写可复用性更好的代码。例:

class Processor{
	public String name(){
		return getClass().getSimpleName();
	}
	public Object process(Object obj){
		return obj;
	}
}
class Upcase extends Processor{
	public String process(Object obj){
		return ((String)obj).toUpperCase();
	}
}
class Downcase extends Processor{
	public String process(Object obj){
		return ((String)obj).toLowerCase();
	}
}
class Apply{
	public static void process(Processor p,Object obj){
		System.out.println("Using Precessor:"+p.name()+","+p.process(obj));
	}

	public static void main(String[] args){
		String s = "AbCd";
		process(new Upcase(),s);
		process(new Downcase(),s);
	}
	//输出:
	//Using Precessor:Upcase,ABCD
	//Using Precessor:Downcase,abcd
}

Apply.process()方法可以接受任何类型的Processor,并将其应用到一个Object对象上,然后打印结果。像上例这样,创建一个能够根据所传递的参数对象的不同而具有不同行为的方法,被称为策略设计模式。这类方法包含所要执行的算法中固定不变的部分,而“策略”包含变化的部分。策略就是传递进去的参数对象,它包含要执行的代码。这里,Processor对象就是一个策略,在main()中可以看到有二种不同类型的策略应用到了string类型的对象s上。

但是,如果Processor是一个接口,那么这些限制就会变得松动,使得你可以复用结构该接口的Apply.process()。

public interface Precessor_i{
	String name;
	Object process(Object obj);
}
public class Apply{
	public static void process(Precessor p,Object obj){
		System.out.println("Using Precessor:"+p.name()+","+p.process(obj));
	}
}

但是,你经常碰到的情况是你无法修改你想要使用的类。在某些情况下,可以使用 适配器设计模式。 适配器中的代码将接受你所拥有的接口,并产生你所需要的接口,就像下面这样:

class PrecessorAdapter implements Precessor_i{
	Precessor_i p;
	public PrecessorAdapter(Precessor_i p){
		this.p = p;
	}
	
	public String name(){
		return p.name();
	}
	public Object process(Object obj){
		return p.process(obj);
	}
	
	
	public static void main(String[] args){
		Upcase u = new Upcase();
		Apply.precess(new PrecessorAdapter(u),"AbCd");
	}
	//输出:ABCD
}

在这种使用适配器的方式中,PrecessorAdapter的构造器接受你所拥有的接口Precessor_i,然后生成具有你所需要的Precessor_i接口的对象。你可能还注意到了,在PrecessorAdapter类中用到了代理。

将接口从具体实现中解耦使得接口可以应用于多种不同的具体实现,因此代码也就更具可复用性。

java中的多重继承

接口不仅仅只是一种更纯粹形式的抽象类,它的目标比这要高。因为接口是根本没有任何具体实现的——也就是说,没有任何与接口相关的存储;因此也就无法阻止多个接口的组合。这一点是很有价值的,因为你有时需要去表示“一个x是一个a和一个b以及一个c”。因此,我们可以通过组合多个接口来结局这个问题,但是只有一个类可以有具体实现。

在导出类中,不强制要求必须有一个是抽象的或“具体的”(没有任何抽象方法的)基类。如果要从一个非接口的类继承,那么只能从一个类去继承。其余的基元素都必须是接口。需要将所有的接口名都置于implements关键字之后,用逗号将它们一一隔开。可以继承任意多个接口,并可以向上转型为每个接口,因为每一个接口都是一个独立类型。下面的例子展示了一个具体类组合数个接口之后产生了一个新类:

interface CanFight{
	void fight();
}
interface CanSwim{
	void swim();
}
class ActionCharacter{
	public void fight();
}
public class Hero extends ActionCharacter implements CanFight,CanSwim{
	public void swim(){}
	
	public static void f1(CanFight c){c.fight();}
	public static void f2(CanSwim c){c.swim();}
	
	public static void main(String[] args){
		Hero h = new Hero();
		f1(h);
		f2(h);
	}
}

可以看到,Hero组合了具体类ActionCharacter和接口CanFight和CanSwim。当通过这种方式将一个具体类和多个接口组合到一起时,这个具体类必须放在前面,后面跟着的才是接口(否则编译器会报错)。注意,CanFight接口与ActionCharacter类,的fight()方法的特征签名是一样的,而且在Hero中并没有提供fight()的定义。可以扩展接口,但是得到的只是另一个接口。当想要创建对象时,所有的定义文首先必须都存在。即使Hero没有显式地提供fight()的定义,其定义也因ActionCharacter而随之而来,这样就使得创建Hero对象成为了可能。

一定要记住,前面的例子所展示的就是使用接口的核心原因:为了能够向上转型为多个基类型(以及由此而带来的灵活性)。然而,使用接口的第二个原因却是与使用抽象基类相同:防止客户端程序员创建该类的对象,并确保这仅仅是建立一个接口。这就带来了一个问题:我们应该使用接口还是抽象类?如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。事实上,如果知道某事物应该成为一个基类,那么第一选择应该是使它成为一个接口。

通过继承来扩展接口

通过继承,可以很容易地在接口中添加新的方法声明,还可以通过继承在新接口中组合数个接口,这两种情况都可以获得新的接口,例:

interface Monster{
	void menace();
}

interface DangerousMonster extends Monster{
	void destroy();
}
class DragonZilla implements DangerousMonster{
	public void menace(){}
	public void destroy(){}
}

适配接口

接口最吸引人的原因之一就是允许同一个接口具有多个不同的具体实现。在简单的情况中,它的体现形式通常是一个接受接口类型的方法,而该接口的实现和向该方法传递的对象则取决于方法的使用者。因此,接口的一种常见用法就是前面提到的策略设计模式,此时你编写一个执行某些操作的方法,而该方法将接受一个同样是你指定的接口。你主要就是要声明:“你可以用任何你想要的对象来调用我的方法,只要你的对象遵循我的接口。”这使得你的方法更加灵活、通用,并更具可复用性。就像下面这样:

interface Animal{
	void play();
}
class Dog implements Animal{
	public void play(){
		System.out.println("Dog play.");
	}
}
class Cat implements Animal{
	public void play(){
		System.out.println("Cat play.");
	}
}
public Test{
	static void f(Animal a){
		a.play();
	}
	
	public static void main(String[] args){
		f(new Dog());
		f(new Cat());
	}
	//输出:
	//Dog play.
	//Cat play.
}

接口中的域

因为放入接口中的任何域都自动是static和final的,所以接口就成为了一种很便捷的用来创建常量组的工具。在接口中定义的域不能是“空final”,但是可以被非常量表达式初始化。既然域是static的,它们就可以在类第一次被加载时初始化

嵌套接口

接口可以嵌套在类或其他接口中,这揭示了许多非常有趣的特性。

class A{
	public interface B{
		void f();
	}
	public interface C{
		void f();
	}
	private interface D{
		void f();
	}
	public class Dimpl implements D{
		public void f(){}
	}
	
	public D getD(){return new Dimpl();}
	private D dref;
	public void receiveD(D d){
		dref = d;
		dref.f();
	}
}

interface E{
	void g();
	interface G{
		void f();
	}
}
class Test{
	public static void main(String[] args){
		A a = new A();
		//Can't access a.getD();
		//A.D ad = a.getD();
		A a2 = new A();
		a2.receiveD(a.getD());
	}
}

在类中嵌套接口的语法是相当显而易见的,就像非嵌套接口一样,可以拥有public和“包访问”两种可视性。作为一种新添加的方式,接口也可以被实现为private的,就像在A.D中所看到的(相同的语法既适用于嵌套接口,也适用于嵌套类)。那么private的嵌套接口能带来的好处是:它可以强制该接口中的方法定义不要添加任何类型信息(也就是说,不允许向上转型)。

getD()方法使我们陷入了一个进退两难的境地,这个问题与private接口相关:它是一个返回对private接口的引用的public方法。对这个方法的返回值能做些什么呢?在main()中,可以看到尝试使用返回值的行为都失败了。只有一种方式可成功,那就是将返回值交给有权使用它的对象。在本例中,是另一个A通过receiveD()方法来实现的。

接口E说明接口彼此之间也可以嵌套。然而,作用于接口的各种规则,特别是所有的接口元素都必须是public的,在此都会被严格执行。因此,嵌套在另一个接口中的接口自动就是public的,而不能声明为private的。

特别要注意的是,当实现某个接口时,并不需要实现嵌套在其内部的任何接口。而且,private接口不能在定义它的类之外被实现。添加这些特性的最初原因可能是出于对严格的语法一致性的考虑。一旦你了解了某种特性,就总能够找到它的用武之地。

接口与工厂

接口是实现多重继承的途径,而生成遵循某个接口的对象的典型方式就是工厂方法设计模式。这与直接调用构造器不同,我们在工厂对象上调用的是创建方法,而该工厂对象将生成接口的某个实现的对象。理论上,通过这种方式,我们的代码将完全与接口的实现分离,这就使得我们可以透明地将某个实现替换为另一个实现。下面的实例展示了工厂方法的结构:

interface Service{
	void f();
}
interface ServiceFactory{
	Service getService(): 
}
class Impl1 implements Service{
	public void f(){System.out.println("Impl1");}
}
class Impl1Factory implements ServiceFactory{
	public Service getService(){
		return new Impl1();
	}
}
class Impl2 implements Service{
	public void f(){System.out.println("Impl2");}
}
class Impl2Factory implements ServiceFactory{
	public Service getService(){
		return new Impl2();
	}
}
public class Test{
	public static void serviceCosumer(Impl1Factory fact){
		Service s = fact.getService();
		s.f();
	}
	public static void main(String[] args){
		serviceCosumer(new Impl1Factory());
		serviceCosumer(new Impl2Factory());
	}
	//输出:
	//Impl1
	//Impl2
}

如果不是用工厂方法,你的代码就必须在某处指定将要创建的Service的确切类型,以便调用合适的构造器 。

总结

“确定接口是理想选择,因而应该总是选择接口而不是具体的类。”这其实是一种引诱。当然,,对于创建类,几乎在任何时刻,都可以替代为创建一个接口和一个工厂。

许多人都掉进了这种诱惑的陷阱,只要有可能就去创建接口和工厂。这种逻辑看起来好像是因为需要使用不同的具体实现,因此总是应该添加这种抽象性。这实际上已经变成了一种草率的设计优化。

任何抽象性都应该是应真正的需求而产生的。当必需时,你应该重构接口而不是到处添加额外级别的间接性,并由此带来的额外的复杂性。这种额外的复杂性非常显著,如果你让某人去处理这种复杂性,只是因为你意识到由于以防万一而添加了新接口,而没有其他更有说服力的原因,那么好吧,如果我碰上了这种事,那么就会质疑此人所作的所有设计了。

恰当的原则应该是优先选择类而不是接口。从类开始,如果接口的必需性变得非常明确,那么就进行重构。接口是一种重要的工具,但是它们容易被滥用。


  1. 本文来源《Java编程思想(第四版)》

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值