除了拥有正确的方法之外,接口还有什么

假设我有这样一个接口:

public interface IBox
{
public void setSize(int size);
public int getSize();
public int getArea();
//...and so on
}

我有一个实现它的类:

public class Rectangle implements IBox
{
private int size;
//Methods here
}

如果我想使用接口 IBox,我实际上不能创建它的实例,方法是:

public static void main(String args[])
{
Ibox myBox=new Ibox();
}

所以我必须这么做:

public static void main(String args[])
{
Rectangle myBox=new Rectangle();
}

如果这是真的,那么接口的唯一目的就是确保实现接口的类中包含接口所描述的正确方法?或者接口还有其他用途吗?

168378 次浏览

你可以的

Ibox myBox = new Rectangle();

这样你就可以使用这个对象作为 Ibox,而不用担心它实际上是 Rectangle

接口是使代码更加灵活的一种方法:

Ibox myBox=new Rectangle();

然后,稍后,如果您决定使用不同类型的盒子(也许还有另一个库,有一个更好的盒子) ,您可以将代码切换到:

Ibox myBox=new OtherKindOfBox();

一旦你习惯了它,你会发现它是一个伟大的(实际上是必不可少的)工作方式。

另一个原因是,例如,如果希望创建一个框列表并对每个框执行某些操作,但希望该列表包含不同类型的框。你可以在每个盒子上写:

myBox.close()

(假设 IBox 有 close ()方法) ,即使 myBox 的实际类根据您在迭代中所处的框而变化。

正常情况下,接口定义您应该使用的接口(如名称所示; ——)


public void foo(List l) {
... do something
}

现在你的函数 foo接受 ArrayListLinkedList,... 不仅仅是一种类型。

Java 中最重要的事情是你可以实现多个接口,但是你只能扩展一个类!样本:


class Test extends Foo implements Comparable, Serializable, Formattable {
...
}
是可能的,但是

class Test extends Foo, Bar, Buz {
...
}
不是!

上面的代码也可以是: IBox myBox = new Rectangle();。现在重要的是,myBox 只包含来自 IBox 的方法/字段,而不包含(可能已经存在的)来自 Rectangle的其他方法。

如果有 CardboardBox 和 HtmlBox (它们都实现了 IBox) ,则可以将它们传递给任何接受 IBox 的方法。即使它们是非常不同的并且不能完全互换,那些不关心“打开”或者“调整大小”的方法仍然可以使用你的类(也许是因为它们关心在屏幕上显示东西需要多少像素)。

接口的用途是 多态性,也就是 类型替换类型替换:

public void scale(IBox b, int i) {
b.setSize(b.getSize() * i);
}

在调用 scale方法时,可以提供实现 IBox接口的类型的任何值。换句话说,如果 RectangleSquare都实现了 IBox,那么只要需要 IBox,就可以提供 RectangleSquare

集合框架中是如何使用接口的一个很好的例子。如果你写一个函数接受一个 List,那么用户传入一个 Vector或者 ArrayList或者 HashList或者其他什么并不重要。你也可以把 List传递给任何需要 Collection或者 Iterable接口的函数。

这使得像 Collections.sort(List list)这样的函数成为可能,而不管 List是如何实现的。

一个添加到 java 中允许多重继承的特性。Java 的开发者意识到拥有多重继承是一个“危险”的特性,这就是为什么他们想出了一个接口的想法。

多重继承是危险的,因为你可能有下面这样的类:


class Box{
public int getSize(){
return 0;
}
public int getArea(){
return 1;
}


}


class Triangle{
public int getSize(){
return 1;
}
public int getArea(){
return 0;
}


}


class FunckyFigure extends Box, Triable{
// we do not implement the methods we will used the inherited ones
}

当我们使用


FunckyFigure.GetArea();

所有的问题都是通过接口解决的,因为你知道你可以扩展接口,它们不会有分类方法... ... 当然,编译器是很好的,它会告诉你如果你没有实现一个方法,但我喜欢认为这是一个更有趣的想法的副作用。

接口允许静态类型语言支持多态性。一个面向对象的纯粹主义者会坚持认为,语言应该提供继承、封装、模块化和多态性,这样才能成为功能齐全的面向对象语言。在动态类型语言(如 Smalltalk)中,多态性是微不足道的; 然而,在静态类型语言(如 Java 或 C #)中,多态性远非微不足道(事实上,从表面上看,它似乎与强类型的概念不一致)

让我演示一下:

在动态类型(或鸭类型)语言(如 Smalltalk)中,所有变量都是对对象的引用(不多也不少)所以,在 Smalltalk 中,我可以这样做:

|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.


anAnimal := Cow new.
anAnimal makeNoise.

那个密码:

  1. 声明一个名为 anAnimal 的局部变量(注意,我们没有指定变量的 TYPE ——所有变量都是对对象的引用,不多也不少。)
  2. 创建一个名为“ Pig”的类的新实例
  3. 将那个猪的新实例分配给变量 anAnimal。
  4. 向猪发送消息 makeNoise
  5. 用一头牛重复整个过程,但是把它赋给和猪一样的变量。

同样的 Java 代码应该是这样的(假设 Duck 和 Cow 是 Animal 的子类:

Animal anAnimal = new Pig();
duck.makeNoise();


anAnimal = new Cow();
cow.makeNoise();

在我们介绍蔬菜课之前,一切都很好。蔬菜和动物有一些相同的行为,但不是全部。例如,动物和蔬菜都可以生长,但很明显,蔬菜不会发出噪音,动物不能被收获。

在 Smalltalk 中,我们可以这样写:

|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.


aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.

这在 Smalltalk 中非常有效,因为它是鸭子类型的(如果它像鸭子一样走路,像鸭子一样嘎嘎叫——它就是鸭子)在这种情况下,当向对象发送消息时,将在接收方的方法列表上执行查找,如果找到匹配的方法,则调用该方法。如果没有,就会抛出某种 NoSuchMethodError 异常——但这些都是在运行时完成的。

但是在 Java 这种静态类型语言中,我们可以为变量赋什么类型呢?玉米需要从蔬菜中继承,以支持生长,但不能从动物中继承,因为它不会发出噪音。奶牛需要从 Animal 继承来支持 make  噪音,但是不能從蔬菜继承,因為它不應該實現收割。看起来我们需要 多重继承-从多个类继承的能力。但是,这是一个非常困难的语言特性,因为所有的边界情况都会出现(当多个并行超类实现相同的方法时会发生什么情况?等)

随之而来的是接口。

如果我们使动物和蔬菜类,与每个实现可生长,我们可以声明我们的牛是动物,我们的玉米是蔬菜。我们也可以声明动物和蔬菜都是可生长的。这让我们可以写下这些来种植所有的东西:

List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());


for(Growable g : list) {
g.grow();
}

它让我们这样做,发出动物的声音:

List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
a.makeNoise();
}

Duck 类型语言的优点是您可以获得非常好的多态性: 类提供行为所需要做的全部工作就是提供方法。只要每个人都表现良好,并且只发送匹配已定义方法的消息,那么一切都很好。缺点是直到运行时才能捕捉到下面的错误:

|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.

静态类型语言提供了更好的“契约式编程”,因为它们会在编译时捕获以下两种错误:

// Compiler error: Corn cannot be cast to Animal.
Animal farmObject = new Corn();
farmObject makeNoise();

--

// Compiler error: Animal doesn't have the harvest message.
Animal farmObject = new Cow();
farmObject.harvest();

总结一下:

  1. 接口实现允许您指定对象可以做哪些事情(交互) ,而类继承允许您指定事情应该如何做(实现)。

  2. 接口为我们提供了“真正的”多态性的许多好处,而不牺牲编译器类型检查。

这就是为什么 工厂模式和其他创建模式在 Java 中如此流行的原因。没有它们,Java 就不能提供一种开箱即用的机制,以便于实例化的抽象,这一点是正确的。尽管如此,在 不要在方法中创建对象的任何地方都会得到抽象,这应该是大部分代码。

顺便说一句,我通常鼓励人们不要使用“ IRealname”机制来命名接口。这是一个 Windows/COM 的东西,把一只脚放在匈牙利命名法的坟墓里,并且真的没有必要(Java 已经是强类型的了,接口的全部意义就是让它们在很大程度上与类类型无法区分)。

我认为您理解接口所做的一切,但是您还没有想象接口在哪些情况下是有用的。

如果您在一个狭窄的范围内(例如,在一个方法调用中)实例化、使用和释放一个对象,则 Interface 实际上不会添加任何内容。如你所说,混凝土类是已知的。

在需要创建对象并将其返回给可能不关心实现细节的调用方时,接口非常有用。让我们将 IBox 示例更改为 Shape。现在我们可以实现形状了,比如矩形、圆形、三角形等等,getArea ()和 getSize ()方法的实现对于每个具体的类都是完全不同的。

现在您可以使用带有各种 createShape (params)方法的工厂,这些方法将根据传入的 params 返回适当的 Shape。显然,工厂将知道正在创建什么类型的形状,但是调用者不必关心它是一个圆还是一个正方形,等等。

现在,想象一下你有各种各样的操作要对你的形状执行。也许您需要按区域对它们进行排序,将它们全部设置为新的大小,然后在 UI 中显示它们。形状都是由工厂创建的,然后可以很容易地传递给分类器、 Sizer 和 Display 类。如果将来需要添加一个六边形类,则只需更改工厂即可。如果没有接口,添加另一个形状会变得非常麻烦。

使接口有用的是 没有这样一个事实: “您可以改变想法,稍后使用不同的实现,只需要更改创建对象的一个位置”。这不是问题。

真正的要点已经在名称中了: 它们定义了一个 接口,任何人都可以实现它来使用在该接口上操作的所有代码。最好的例子是 java.util.Collections,它提供了各种专门在接口上操作的有用方法,例如 sort()reverse()对于 List。这里的要点是,这段代码现在可以用来排序或反向实现 List接口的 任何类——不仅仅是 ArrayListLinkedList,还有你自己编写的类,这些类的实现方式可能是编写 java.util.Collections的人从未想到过的。

同样,您可以编写在已知接口或您定义的接口上运行的代码,其他人也可以使用您的代码,而不必要求您支持他们的类。

接口的另一个常见用途是回调。例如,Table. TableCellRenderer,它允许您影响 Swing 表在特定列中显示数据的方式。您实现该接口,将一个实例传递给 JTable,并且在表呈现期间的某个时刻,您的代码将被调用来完成它的工作。

不要忘记,在以后的日期,您可以采取一个 存在类,并使其实现 IBox,然后它将成为可用的所有框感知代码。

如果接口被命名为 有能力,例如,这就会更清楚一些。

public interface Saveable {
....


public interface Printable {
....

等等(命名方案并不总是有效,例如,我不确定 Boxable在这里是否合适)

接口的用途是 抽象,即与实现解耦。

如果您在程序中引入了抽象,那么您就不会关心可能的实现。您感兴趣的是它能做的 什么而不是 怎么做,并且您使用 interface在 Java 中表示它。

我读过的许多用法之一是,在 Java 中没有使用多重继承的接口是很困难的:

class Animal
{
void walk() { }
....
.... //other methods and finally
void chew() { } //concentrate on this
}

现在,想象一下这样一个例子:

class Reptile extends Animal
{
//reptile specific code here
} //not a problem here

但是,

class Bird extends Animal
{
...... //other Bird specific code
} //now Birds cannot chew so this would a problem in the sense Bird classes can also call chew() method which is unwanted

更好的设计应该是:

class Animal
{
void walk() { }
....
.... //other methods
}

Animal 没有咀嚼()方法,而是被放在一个接口中,如下所示:

interface Chewable {
void chew();
}

并让爬行类实现这一点,而不是鸟类(因为鸟类不能咀嚼) :

class Reptile extends Animal implements Chewable { }

以及鸟类的情况:

class Bird extends Animal { }

接口的唯一目的是确保实现接口的类中包含接口所描述的正确方法?或者接口还有其他用途吗?

我正在用新的接口特性更新答案,这些特性已经与 Java 8版本一起引入。

从甲骨文文档页面的 接口总结接口总结:

接口声明可以包含

  1. 方法签名
  2. 默认方法
  3. 静态方法
  4. 不变的定义。

具有实现的唯一方法是默认方法和静态方法。

使用接口 :

  1. 定义 合约
  2. 链接不相关的类 有一种能力(例如,实现 Serializable接口的类除了实现该接口之外,它们之间可能有也可能没有任何关系
  3. 提供 可以互换实现
  4. 默认方法 允许您向库的接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性
  5. 使用静态方法组织库中的 helper 方法 (可以将特定于接口的静态方法保存在同一接口中,而不是单独的类中)

关于 抽象类接口之间差异的一些相关 SE 问题,以及带有工作实例的用例:

接口和抽象类的区别是什么?

我应该如何解释接口和抽象类之间的区别?

看看 文件页面,了解在 java 8中添加的新特性: 默认方法和静态方法

为什么要用接口?

首先是一只狗,特别是 哈巴狗

哈巴狗有各种各样的行为:

public class Pug {
private String name;
public Pug(String n) { name = n; }
public String getName() { return name; }
public String bark() { return  "Arf!"; }
public boolean hasCurlyTail() { return true; } }

你有一只拉布拉多,它也有一系列的行为。

public class Lab {
private String name;
public Lab(String n) { name = n; }
public String getName() { return name; }
public String bark() { return "Woof!"; }
public boolean hasCurlyTail() { return false; } }

我们可以做些哈巴狗和拉布拉多犬:

Pug pug = new Pug("Spot");
Lab lab = new Lab("Fido");

我们可以引用他们的行为:

pug.bark() -> "Arf!"
lab.bark() -> "Woof!"
pug.hasCurlyTail() -> true
lab.hasCurlyTail() -> false
pug.getName() -> "Spot"

假设我经营一家狗舍,我需要跟踪所有我收容的狗。I 需要把我的哈巴狗和拉布拉多犬分开存放:

public class Kennel {
Pug[] pugs = new Pug[10];
Lab[] labs = new Lab[10];
public void addPug(Pug p) { ... }
public void addLab(Lab l) { ... }
public void printDogs() { // Display names of all the dogs } }

但这显然不是最佳选择。如果 我想养几只狮子狗也是这样,我必须更改 Kennel 定义以添加一个贵宾犬数组。事实上,我需要一个单独的数组为每种狗。

洞察力: 哈巴狗和拉布拉多犬(以及贵宾犬)都是狗的类型,它们有着相同的行为。也就是说,我们可以说(为了这个例子的目的) ,所有的狗可以吠叫,有一个名字,并可能或可能没有一个卷曲的尾巴。我们可以使用一个接口来定义所有狗可以做什么,但是让特定类型的狗来实现这些特定的行为。界面上写着“这是所有狗都能做的事情”,但没有说明每个行为是如何做的。

public interface Dog
{
public String bark();
public String getName();
public boolean hasCurlyTail(); }

然后稍微修改 Pug 和 Lab 类以实现 Dog 行为。我们可以说 Pug 是狗,Lab 是狗。

public class Pug implements Dog {
// the rest is the same as before }


public class Lab implements Dog {
// the rest is the same as before
}

我仍然可以像以前那样实例化 Pug 和 Labs,但是现在我也得到了一种新的方法:

Dog d1 = new Pug("Spot");
Dog d2 = new Lab("Fido");

这上面说 d1不仅是一只狗,还是一只哈巴狗。D2也是一只狗,特别是一只拉布拉多犬。 我们可以调用这些行为,它们像以前一样工作:

d1.bark() -> "Arf!"
d2.bark() -> "Woof!"
d1.hasCurlyTail() -> true
d2.hasCurlyTail() -> false
d1.getName() -> "Spot"

这就是所有额外工作的回报。Kennel 类变得更加简单。我只需要一个数组和一个 addDog 方法。两者都可以处理任何实现 Dog 接口的对象。

public class Kennel {
Dog[] dogs = new Dog[20];
public void addDog(Dog d) { ... }
public void printDogs() {
// Display names of all the dogs } }

以下是使用方法:

Kennel k = new Kennel();
Dog d1 = new Pug("Spot");
Dog d2 = new Lab("Fido");
k.addDog(d1);
k.addDog(d2);
k.printDogs();

最后一项声明将显示: 斑点狗

接口使您能够指定一组行为,所有实现该接口的类都将共享这些行为。因此,我们可以定义变量和集合(比如数组) ,这些变量和集合不需要事先知道它们将包含哪种特定的对象,只需要知道它们将包含实现接口的对象。

以下是我对界面优势的理解。如果我错了请纠正我。 想象一下,我们正在开发操作系统,而其他团队正在为一些设备开发驱动程序。 因此,我们开发了一个接口 StorageDevice。我们有其他开发团队提供的两个实现(FDD 和 HDD)。

然后我们有一个 OperatingSystem 类,它可以通过传递一个实现了 StorageDevice 接口的类的实例来调用诸如 saveData 之类的接口方法。

这里的优点是我们不关心接口的实现。另一个团队将通过实现 StorageDevice 接口来完成这项工作。

package mypack;


interface StorageDevice {
void saveData (String data);
}




class FDD implements StorageDevice {
public void saveData (String data) {
System.out.println("Save to floppy drive! Data: "+data);
}
}


class HDD implements StorageDevice {
public void saveData (String data) {
System.out.println("Save to hard disk drive! Data: "+data);
}
}


class OperatingSystem {
public String name;
StorageDevice[] devices;
public OperatingSystem(String name, StorageDevice[] devices) {


this.name = name;
this.devices = devices.clone();


System.out.println("Running OS " + this.name);
System.out.println("List with storage devices available:");
for (StorageDevice s: devices) {
System.out.println(s);
}


}


public void saveSomeDataToStorageDevice (StorageDevice storage, String data) {
storage.saveData(data);
}
}


public class Main {


public static void main(String[] args) {


StorageDevice fdd0 = new FDD();
StorageDevice hdd0 = new HDD();
StorageDevice[] devs = {fdd0, hdd0};
OperatingSystem os = new OperatingSystem("Linux", devs);
os.saveSomeDataToStorageDevice(fdd0, "blah, blah, blah...");
}
}