接口
Java包含一个接口概念。 Java接口有点像类,但是Java接口只能包含方法签名和字段。 Java接口不能包含方法的实现,只能包含方法的签名(名称,参数和异常)。
你可以使用接口作为实现多态的一种方式。本文稍后会提到多态。
接口例子
下面是一个简单的接口示例:
public interface MyInterface {
public String hello = "Hello";
public void sayHello();
}
如你所见,使用关键字interface来声明一个接口。Java接口可以被声明为public或者package作用域(没有访问修饰符)。
上面的接口示例包含一个变量和一个方法。变量可以直接从接口访问,如下所示:
System.out.println(MyInterface.hello);
正如你所看到的,访问接口中的变量与访问类中的静态变量的方式非常相似。
但是,接口中的方法需要在某个类中实现,然后才能访问它。下一节将解释如何完成这个工作。
实现接口
在真正使用接口之前,必须在某个Java类中实现该接口。下面是一个实现MyInterface接口的类:
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
注意上面类声明中implements MyInterface部分。这是向Java编译器发出信号,MyInterfaceImpl类实现了MyInterface接口。
实现接口的类必须实现接口中声明的所有方法。这些方法必须具有与接口中声明的完全相同的签名(名称+参数)。该类不需要实现(声明)接口的变量。
接口实例
一旦Java类实现了Java接口,就可以使用该类的一个实例作为该接口的一个实例。示例如下:
MyInterface myInterface = new MyInterfaceImpl();
myInterface.sayHello();
注意,上面创建的对象是MyInterfaceImpl类型,但是变量被声明为MyInterface接口类型。在Java中允许这样做,因为MyInterfaceImpl类实现了MyInterface接口。然后可以引用MyInterfaceImpl类的实例作为MyInterface接口的实例。
你不能自己创建Java接口的实例。你必须始终创建一个实现该接口的类的实例,并将该实例作为该接口的一个实例。
实现多个接口
一个Java类可以实现多个Java接口。在这种情况下,类必须实现所有接口中声明的所有方法。示例如下:
public class MyInterfaceImpl
implements MyInterface, MyOtherInterface {
public void sayHello() {
System.out.println("Hello");
}
public void sayGoodbye() {
System.out.println("Goodbye");
}
}
这个类实现了名为MyInterface和MyOtherInterface的两个接口。在implements关键字后面跟着需要实现的接口名称,接口与接口间用逗号分隔。
如果接口与实现类不在同一个包中,则还需要导入接口。例如:
import com.jenkov.package1.MyInterface;
import com.jenkov.package2.MyOtherInterface;
public class MyInterfaceImpl implements MyInterface, MyOtherInterface {
...
}
下面是上面实现的两个接口:
public interface MyInterface {
public void sayHello();
}
public interface MyOtherInterface {
public void sayGoodbye();
}
正如你看到的那样,每个接口都包含一个方法。这些方法都在MyInterfaceImpl类中实现。
方法签名重叠
如果一个Java类实现了多个Java接口,而这些接口中的某些方法可能包含具有相同签名(名称+参数)。由于Java类只能在一个具有给定签名的方法,这可能会导致一些问题。
Java规范没有给出任何解决这个问题的方法。在这种情况下,由你决定该怎么做。
接口变量
Java接口可以同时包含变量和常量。通常情况下将变量放置在接口中是没有意义的。在某些情况下,在接口中定义常量是有意义的。特别是如果那些常量需要被实现接口的类使用时,例如这些常量用在计算中,或者作为接口中某些方法的参数。不过,我建议你尽量避免在Java接口中放置变量。
即使在变量声明中省略public关键字,接口中的所有变量也是公共的。
接口方法
Java接口可以包含一个或多个方法声明。如前所述,接口不能为这些方法指定任何实现,这是方法由实现该接口的类来实现。
即使在方法声明中省略public关键字,接口中的所有方法也是公共的。
接口默认方法
在Java 8之前,Java接口不能包含方法的实现,只能包含方法签名。但是,当API需要添加一个方法到其某个接口时,会导致一些问题。如果API只是将方法添加到所需的接口,则实现该接口的所有类都必须实现这个新方法。如果所有的实现类都位于API中,那就好了。但是,如果某些实现类位于API(使用该API的代码)的客户端代码中,则该代码会中断。
让我用一个例子来说明这一点。看看这个接口,想象它是许多应用程序内部使用的开源API:
public interface ResourceLoader {
Resource load(String resourcePath);
}
现在设想一个项目使用这个API,并已经实现了ResourceLoader接口,如下所示:
public class FileLoader implements ResourceLoader {
public Resource load(String resourcePath) {
// 在这里实现该方法
// 然后返回
}
}
如果API的开发人员想要向ResourceLoader接口添加一个方法,那么当该项目升级到新版本的API时,FileLoader类将被中断。
为了缓解这个Java接口的升级进化问题,Java 8在Java接口中增加了接口默认方法的概念。接口默认方法可以包含该方法的默认实现。实现了该接口但不包含默认接口实现的类将自动获得默认的方法实现。
你可以使用default关键字在接口中将方法标记为默认方法。以下是将一个默认方法添加到ResourceLoader接口的示例:
public interface ResourceLoader {
Resource load(String resourcePath);
default Resource load(Path resourcePath) {
//在一个Resource对象中,
//提供默认实现来加载
//来自Path的资源并返回内容
}
}
这个例子添加了默认的方法load(Path)。
一个类可以通过显式实现该方法来重写默认方法的实现,就像通常在实现Java接口时一样。类中的任何实现优先于接口默认方法实现。
接口和继承
一个Java接口可以继承自另一个Java接口,就像类可以继承其他类一样。你可以使用extends关键字指定继承。这里是一个简单的接口继承的例子:
public interface MySuperInterface {
public void saiHello();
}
public interface MySubInterface extends MySuperInterface {
public void sayGoodbye();
}
MySubInterface接口扩展了MySuperInterface接口。这意味着,MySubInterface继承了MySuperInterface的所有字段和方法。这意味着,如果一个类实现了MySubInterface,那么这个类必须实现MySubInterface和MySuperInterface中定义的所有方法。
如果你需要的话,你也可以在子接口中定义和父接口完全一样的方法签名。
与类不同,接口实际上可以继承多个父接口。你可以通过列出所有要继承的接口的名称来指定,用逗号分隔。实现该接口的类必须实现接口及其父接口中的所有方法。
以下是继承多个接口的示例:
public interface MySubInterface extends
SuperInterface1, SuperInterface2 {
public void sayItAll();
}
当实现多个接口时,如果多个父接口具有相同签名(名称+参数)的方法时,没有规定如何处理这种情况。
继承和默认方法
接口默认方法增加了接口继承规则的复杂性。尽管一个类通常可以实现多个接口,即使接口包含具有相同签名的方法,但如果这些方法中的一个或多个是默认方法,则这是不可能的。换句话说,如果两个接口包含相同的方法签名(名称+参数),并且其中一个接口声明此方法为默认方法,则类不能自动实现这两个接口。
如果接口从多个接口扩展(继承),并且其中一个或多个接口包含具有相同签名的方法,并且其中一个父接口将重叠方法声明为默认方法,则情况与上面相同。
在上述两种情况下,Java编译器都要求该类明确地实现导致问题的方法。类中的实现优先于任何默认实现。
接口和多态
Java接口是实现多态的一种方式。基本上,多态意味着一个类实例(一个对象)可以像使用不同的类型一样使用。这里,一个类型表示一个类或一个接口。
看看这个简单的类图:
在同一个应用程序中两个并行的类层次结构。
上面的类是所有模型的一部分,代表不同类型的车辆和驾驶员,它们都具有字段和方法。
现在想象一下,你需要将这些对象存储在数据库中,并将它们序列化为XML,JSON或其他格式。你希望简单地实现一些方法,该方法可以在每个Car,Truck或Vehicle对象上使用。 这些方法包括store()方法、serializeToXML()方法和serializeToJSON()方法。
如果直接在对象上直接实现这个功能可能会导致一个凌乱的类层次结构。试想这就是你想要实现的操作。
在上面的图表中,你会把这三种方法放在哪里,所有的类都可以访问吗?
解决这个问题的一种方法是为Vehicle和Driver类创建一个通用的超类,它具有存储和序列化方法。但是,这会导致概念上的混乱。类层次结构将不再模拟车辆和驾驶员,而且还与应用程序中使用的存储和序列化机制相关联。
一个更好的解决方案是创建一些存储和序列化方法的接口,并让这些类实现这些接口。示例如下:
public interface Storable {
public void store();
}
public interface Serializable {
public void serializeToXML(Writer writer);
public void serializeToJSON(Writer writer);
}
当每个类实现这两个接口及其方法时,可以通过将对象转换为接口类型的实例来访问这些接口的方法。只要你知道它实现了什么接口,你就不需要知道给定的对象是什么类。这里是一个例子:
Car car = new Car();
Storable storable = (Storable) car;
storable.store();
Serializable serializable = (Serializable) car;
serializable.serializeToXML (new FileWriter("car.xml"));
serializable.serializeToJSON(new FileWriter("car.json"));
正如你现在可以想象的那样,接口提供了一种方式来实现类中的交叉切割功能,这种方式比继承更加清晰。
下一篇: 接口 vs 抽象类