159

所以可以说我有这个界面:

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();
}

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

4

17 回答 17

142

接口是使您的代码更加灵活的一种方式。你要做的是:

Ibox myBox=new Rectangle();

然后,稍后,如果您决定要使用不同类型的框(可能有另一个库,具有更好类型的框),您将代码切换为:

Ibox myBox=new OtherKindOfBox();

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

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

myBox.close()

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

于 2009-02-02T21:12:59.933 回答
125

使接口有用的不是“您可以改变主意并在以后使用不同的实现,而只需要更改创建对象的地方”这一事实。那不是问题。

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

同样,您可以编写在众所周知的接口或您定义的接口上运行的代码,其他人可以使用您的代码,而无需要求您支持他们的类。

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

于 2009-02-02T22:03:38.083 回答
120

我读过的众多用途之一是在 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 没有chew() 方法,而是放在一个接口中:

interface Chewable {
void chew();
}

并让 Reptile 类实现这个而不是 Birds(因为 Birds 不能咀嚼):

class Reptile extends Animal implements Chewable { } 

和鸟类简单的情况:

class Bird extends Animal { }
于 2013-06-20T17:46:18.930 回答
48

接口的目的是多态性,也就是类型替换。例如,给定以下方法:

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

调用该scale方法时,您可以提供任何实现IBox接口的类型的值。换句话说,如果RectangleSquare两者都实现IBox,您可以在任何预期的地方提供 aRectangle或 a 。SquareIBox

于 2009-02-02T21:14:48.057 回答
33

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

让我演示一下:

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

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

anAnimal := Cow new.
anAnimal makeNoise.

该代码:

  1. 声明一个名为 anAnimal 的局部变量(请注意,我们不指定变量的类型 - 所有变量都是对对象的引用,不多也不少。)
  2. 创建名为“Pig”的类的新实例
  3. 将 Pig 的新实例分配给变量 anAnimal。
  4. 将消息发送makeNoise给猪。
  5. 使用牛重复整个过程,但将其分配给与 Pig 相同的变量。

相同的 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 这种静态类型语言中,我们可以为变量分配什么类型?玉米需要从蔬菜继承,以支持生长,但不能从动物继承,因为它不会发出噪音。Cow 需要从 Animal 继承来支持 makeNoise,但不能从 Vegetable 继承,因为它不应该实现收获。看起来我们需要多重继承——从多个类继承的能力。但事实证明这是一个非常困难的语言特性,因为会出现所有边缘情况(当多个并行超类实现相同的方法时会发生什么?等等)

随之而来的接口...

如果我们创建动物和蔬菜类,每个实现 Growable,我们可以声明我们的牛是动物,我们的玉米是蔬菜。我们还可以声明动物和蔬菜都是可生长的。这让我们写这个来增长一切:

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();
}

鸭子类型语言的优点是你可以获得非常好的多态性:一个类提供行为所要做的就是提供方法。只要每个人都表现得很好,并且只发送与定义的方法匹配的消息,一切都很好。缺点是直到运行时才捕获到下面的错误类型:

|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. 接口为我们提供了“真正的”多态性的许多好处,而不会牺牲编译器类型检查。

于 2009-02-02T21:27:32.477 回答
9

通常,接口定义了您应该使用的接口(顾名思义;-))。样本


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

现在您的函数foo接受ArrayLists, LinkedLists, ... 不仅是一种类型。

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


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

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

您上面的代码也可能是:IBox myBox = new Rectangle();. 现在重要的是,myBox 仅包含来自 IBox 的方法/字段,而不包含来自Rectangle.

于 2009-02-02T21:14:07.700 回答
6

你可以做

Ibox myBox = new Rectangle();

这样您就可以将此对象用作 Ibox 并且您并不关心它真的是Rectangle.

于 2009-02-02T21:11:03.510 回答
6

我认为您了解接口所做的一切,但您还没有想象接口有用的情况。

如果您在一个狭窄的范围内(例如,在一个方法调用中)实例化、使用和释放一个对象,那么接口实际上并没有添加任何东西。正如您所指出的,具体类是已知的。

接口有用的地方是当一个对象需要在一个地方创建并返回给可能不关心实现细节的调用者时。让我们将您的 IBox 示例更改为形状。现在我们可以有 Shape 的实现,例如 Rectangle、Circle、Triangle 等,getArea() 和 getSize() 方法的实现对于每个具体类都是完全不同的。

现在您可以使用具有各种 createShape(params) 方法的工厂,该方法将根据传入的参数返回适当的 Shape。显然,工厂将知道正在创建的 Shape 类型,但调用者不会知道关心它是圆形,还是方形,等等。

现在,假设您必须对形状执行各种操作。也许您需要按区域对它们进行排序,将它们全部设置为新大小,然后在 UI 中显示它们。Shapes 都是由工厂创建的,然后可以很容易地传递给 Sorter、Sizer 和 Display 类。如果您将来需要添加一个六边形类,您无需更改任何内容,只需更改工厂即可。如果没有界面,添加另一个形状会变得非常混乱。

于 2009-02-02T21:58:50.090 回答
6

为什么是界面??????

它从一只狗开始。特别是哈巴狗

哈巴狗有多种行为:

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"

假设我经营一个狗窝,我需要跟踪我所饲养的所有狗。我需要将我的哈巴狗和拉布拉多犬存放在单独的数组中

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 } }

但这显然不是最优的。如果我也想养一些贵宾犬,我必须更改我的犬舍定义以添加一组贵宾犬。事实上,我需要为每种狗创建一个单独的数组。

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

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

然后我稍微改变 Pug 和 Lab 类来实现 Dog 行为。我们可以说哈巴狗是狗,实验室是狗。

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

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

我仍然可以像以前一样实例化 Pugs 和 Labs,但现在我也有了一种新的方法:

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

这表示 d1 不仅是一只狗,它特别是一只哈巴狗。d2 也是 Dog,特别是 Lab。我们可以调用这些行为,它们像以前一样工作:

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();

最后一条语句将显示:Spot Fido

接口使您能够指定一组行为,实现该接口的所有类将共享这些行为。因此,我们可以定义变量和集合(例如数组),它们不必事先知道它们将保存什么样的特定对象,只需知道它们将保存实现接口的对象。

于 2017-05-15T06:27:09.520 回答
3

Collections 框架是一个很好的接口使用示例。如果您编写一个接受 a 的函数List,那么用户传入 aVector或 anArrayList或 aHashList或其他什么都没有关系。你也可以将它传递List给任何需要一个CollectionIterable接口的函数。

这使得类似的功能成为Collections.sort(List list)可能,无论它是如何List实现的。

于 2009-02-02T21:14:57.947 回答
3

这就是为什么工厂模式和其他创建模式在 Java 中如此流行的原因。你是对的,没有它们,Java 没有提供开箱即用的机制来轻松抽象实例化。尽管如此,在您没有在方法中创建对象的任何地方都会获得抽象,这应该是您的大部分代码。

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

于 2009-02-02T21:48:18.800 回答
3

不要忘记,在以后你可以获取一个现有的类,并让它实现IBox,然后它就会对你所有的盒子感知代码可用。

如果接口被命名为-able ,这会变得更清楚一些。例如

public interface Saveable {
....

public interface Printable {
....

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

于 2009-02-03T11:10:35.857 回答
3

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

我正在使用java 8版本引入的界面新功能更新答案。

从关于接口摘要的oracle 文档页面:

接口声明可以包含

  1. 方法签名
  2. 默认方法
  3. 静态方法
  4. 常量定义。

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

接口用途

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

关于抽象类接口之间的区别以及带有工作示例的用例的一些相关 SE 问题:

接口和抽象类有什么区别?

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

查看文档页面以了解 java 8 中添加的新功能:默认方法和静态方法

于 2016-03-04T21:16:07.453 回答
2

接口的目的是抽象,或与实现解耦。

如果你在你的程序中引入一个抽象,你就不会关心可能的实现。您对它能做什么而不是如何感兴趣,并且您使用 aninterface在 Java 中表达这一点。

于 2009-02-04T14:54:30.397 回答
1

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

于 2009-02-02T21:14:26.200 回答
1

将功能添加到 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(); 

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

于 2009-02-02T21:17:17.570 回答
0

以下是我对接口优势的理解。如果我错了,请纠正我。想象一下,我们正在开发操作系统,而其他团队正在开发某些设备的驱动程序。所以我们开发了一个接口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...");    
    }
}
于 2018-06-22T08:05:25.570 回答