Is there more to an interface than having the correct methods
所以假设我有这个接口:
1 2 3 4 5 6 7 | public interface IBox { public void setSize(int size); public int getSize(); public int getArea(); //...and so on } |
我有一个实现它的类:
1 2 3 4 5 |
如果我想使用接口ibox,我就不能以如下方式实际创建它的实例:
1 2 3 4 |
正确的?所以我必须这样做:
如果这是真的,那么接口的唯一目的就是确保实现接口的类中具有接口所描述的正确方法?或者接口还有其他用途吗?
接口是使代码更加灵活的一种方法。你要做的是:
1 |
然后,稍后,如果您决定使用不同类型的框(可能还有另一个库,使用更好的框),则将代码切换到:
1 | Ibox myBox=new OtherKindOfBox(); |
一旦你习惯了它,你会发现它是一个伟大的(实际上是必要的)工作方式。
另一个原因是,例如,如果您希望创建一个框列表并对每个框执行一些操作,但是您希望该列表包含不同类型的框。在每个盒子上,你可以做:
1 | myBox.close() |
(假设ibox有一个close()方法),即使mybox的实际类会根据您在迭代中所在的框而改变。
接口之所以有用,并不是因为"你以后可以改变主意,使用不同的实现,并且只需要改变对象被创建的地方"。这不是问题。
真正的要点已经在名称中:它们定义了一个接口,任何人都可以实现该接口,以使用在该接口上操作的所有代码。最好的例子是
以同样的方式,您可以编写在知名接口或您定义的接口上操作的代码,其他人可以使用您的代码而不必要求您支持他们的类。
接口的另一个常见用法是回调。例如,java.swing.table.TableCellRenderer,它允许您影响swing表在特定列中显示数据的方式。您实现该接口,将一个实例传递给
我读过的许多用法之一是使用Java中的接口,在没有多重继承的情况下是困难的:
1 2 3 4 5 6 7 | class Animal { void walk() { } .... .... //other methods and finally void chew() { } //concentrate on this } |
现在,想象一下这样一个例子:
1 2 3 4 | class Reptile extends Animal { //reptile specific code here } //not a problem here |
但是,
1 2 3 4 | 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 |
更好的设计是:
1 2 3 4 5 6 | class Animal { void walk() { } .... .... //other methods } |
动物没有chew()方法,而是将其放入一个接口中,如下所示:
1 2 3 | interface Chewable { void chew(); } |
并且让爬行动物类执行这个而不是鸟类(因为鸟类不能咀嚼):
1 | class Reptile extends Animal implements Chewable { } |
如果是鸟,简单地说:
1 | class Bird extends Animal { } |
接口的目的是多态性,即类型替换。例如,给出以下方法:
1 2 3 | public void scale(IBox b, int i) { b.setSize(b.getSize() * i); } |
调用
接口允许静态类型语言支持多态性。面向对象的纯粹主义者会坚持一种语言应该提供继承性、封装性、模块性和多态性,以便成为一种功能齐全的面向对象语言。在动态类型化或鸭式语言中(如SimulalTalk),多态性是微不足道的;然而,在静态类型化语言(如Java或C语言)中,多态性并不是微不足道的(事实上,表面上似乎与强类型的概念不一致)。
让我演示一下:
在动态类型(或duck类型)语言(如smalltalk)中,所有变量都是对对象的引用(无一例外)。因此,在smalltalk中,我可以这样做:
1 2 3 4 5 6 | |anAnimal| anAnimal := Pig new. anAnimal makeNoise. anAnimal := Cow new. anAnimal makeNoise. |
代码:
同样的Java代码看起来像这样(假设鸭子和牛是动物的子类:
1 2 3 4 5 | Animal anAnimal = new Pig(); duck.makeNoise(); anAnimal = new Cow(); cow.makeNoise(); |
这一切都很好,直到我们介绍蔬菜课。蔬菜和动物有一些相同的行为,但不是全部。例如,动物和蔬菜都可以生长,但显然蔬菜不会发出声音,动物也不能收获。
在Smalltalk中,我们可以写下:
1 2 3 4 5 6 7 8 | |aFarmObject| aFarmObject := Cow new. aFarmObject grow. aFarmObject makeNoise. aFarmObject := Corn new. aFarmObject grow. aFarmObject harvest. |
这在smalltalk中非常有效,因为它是duck类型的(如果它像duck一样行走,像duck一样嘎嘎叫-它是duck)。在这种情况下,当消息发送到对象时,在接收者的方法列表上执行查找,如果找到匹配的方法,则调用它。如果不是,则会抛出某种NoSuchMethodError异常——但都是在运行时完成的。
但是在Java中,静态类型的语言,我们可以给我们的变量赋值什么类型呢?玉米需要从蔬菜中继承,以支持生长,但不能从动物中继承,因为它不会发出噪音。牛需要从动物身上继承来支持制造噪音,但不能从蔬菜身上继承,因为它不应该实现收割。看起来我们需要多重继承-从多个类继承的能力。但这是一个相当困难的语言特性,因为所有弹出的边缘情况(当多个并行超类实现相同的方法时会发生什么情况?等)
伴随而来的界面…
如果我们创建动物和蔬菜类,每种实现都可以生长,我们可以声明我们的牛是动物,我们的玉米是蔬菜。我们也可以宣布动物和蔬菜都可以种植。这样我们就可以写这个来发展一切:
1 2 3 4 5 6 7 8 | List<Growable> list = new ArrayList<Growable>(); list.add(new Cow()); list.add(new Corn()); list.add(new Pig()); for(Growable g : list) { g.grow(); } |
它让我们这样做,让动物发出声音:
1 2 3 4 5 6 | List<Animal> list = new ArrayList<Animal>(); list.add(new Cow()); list.add(new Pig()); for(Animal a : list) { a.makeNoise(); } |
Duck类型语言的优势在于,您可以获得非常好的多态性:类提供行为所需的全部工作就是提供方法。只要每个人都表现得很好,并且只发送符合定义方法的消息,那么一切都是好的。缺点是在运行时才捕获下面的错误:
1 2 3 | |aFarmObject| aFarmObject := Corn new. aFarmObject makeNoise. // No compiler error - not checked until runtime. |
静态类型语言提供了更好的"契约式编程",因为它们在编译时会捕获以下两种错误:
1 2 3 | // Compiler error: Corn cannot be cast to Animal. Animal farmObject = new Corn(); farmObject makeNoise(); |
——
1 2 3 | // Compiler error: Animal doesn't have the harvest message. Animal farmObject = new Cow(); farmObject.harvest(); |
所以……总结一下:
接口实现允许您指定对象可以做什么类型的事情(交互),而类继承允许您指定应该如何做(实现)。
接口给了我们许多"真"多态性的好处,而不牺牲编译器类型检查。
通常,接口定义您应该使用的接口(如名称所说;-)。样品
1 2 3 |
现在你的函数
Java中最重要的一点是,可以实现多个接口,但只能扩展一个类!Sample:
1 2 3 |
是可能的
1 2 3 | class Test extends Foo, Bar, Buz { ... } |
不是!
上面的代码也可以是:
你可以做到
1 |
这样,您就可以将这个对象用作IBOX,而不必担心它的实际值是
我认为你理解接口所做的一切,但是你还没有想象出一个接口在什么情况下是有用的。
如果您在一个狭窄的范围内(例如,在一个方法调用中)实例化、使用和释放一个对象,那么一个接口实际上不会添加任何东西。正如您所指出的,具体类是已知的。
当需要在一个地方创建一个对象并将其返回给可能不关心实现细节的调用者时,接口是有用的。让我们将IBox示例更改为一个形状。现在我们可以实现形状,如矩形、圆形、三角形等,对于每个具体类,getArea()和getSize()方法的实现将完全不同。
现在,您可以使用具有多种createShape(params)方法的工厂,这些方法将根据传入的参数返回适当的形状。显然,工厂会知道正在创建的是什么类型的形状,但是调用方不必关心它是圆还是正方形,等等。
现在,假设您有各种操作必须在您的形状上执行。也许您需要按区域对它们进行排序,将它们全部设置为新的大小,然后在UI中显示它们。这些形状都是由工厂创建的,然后可以很容易地传递给分拣机、分级机和显示类。如果将来某个时候你需要添加一个六边形类,你不需要改变任何东西,除了工厂。如果没有接口,添加另一个形状将成为一个非常混乱的过程。
为什么要接口????????
从狗开始。尤其是哈巴狗。
哈巴狗有多种行为:
1 2 3 4 5 6 |
你有一个拉布拉多犬,它也有一系列的行为。
1 2 3 4 5 6 |
我们可以做一些哈巴狗和实验室:
1 2 | Pug pug = new Pug("Spot"); Lab lab = new Lab("Fido"); |
我们可以调用他们的行为:
1 2 3 4 5 | pug.bark() ->"Arf!" lab.bark() ->"Woof!" pug.hasCurlyTail() -> true lab.hasCurlyTail() -> false pug.getName() ->"Spot" |
比如说,我经营一个狗舍,我需要跟踪我住的所有狗。我需要把我的哈巴狗和拉布拉多犬分开存放:
1 2 3 4 5 6 | 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 } } |
但这显然不是最佳选择。如果我也想要养一些贵宾犬,我必须更改我的狗舍定义来添加一组贵宾犬。事实上,我需要为每种狗分别设置一个数组。
洞察:哈巴狗和拉布拉多犬(和贵宾犬)都是狗的类型,它们有相同的行为。也就是说,我们可以说(在本例中)所有的狗都可以吠叫,有一个名字,可能有或没有卷曲的尾巴。我们可以使用一个接口来定义所有的狗都能做什么,但是要实现这些特定的行为,就必须由特定类型的狗来决定。界面上说"这是所有狗都能做的事情",但没有说明每个行为是如何完成的。
1 2 3 4 5 |
然后我稍微修改pug和lab类来实现狗的行为。我们可以说哈巴狗是狗,实验室是狗。
1 2 3 4 5 6 | public class Pug implements Dog { // the rest is the same as before } public class Lab implements Dog { // the rest is the same as before } |
我仍然可以像以前那样实例化哈巴狗和实验室,但现在我也找到了一种新的方法:
1 2 | Dog d1 = new Pug("Spot"); Dog d2 = new Lab("Fido"); |
这就是说,d1不仅是一只狗,它还特别是一只哈巴狗。D2也是一只狗,特别是一个实验室。我们可以调用这些行为,并且它们像以前一样工作:
1 2 3 4 5 | d1.bark() ->"Arf!" d2.bark() ->"Woof!" d1.hasCurlyTail() -> true d2.hasCurlyTail() -> false d1.getName() ->"Spot" |
这是所有额外工作的回报。养狗班变得简单多了。我只需要一个数组和一个adddog方法。这两种方法都可以用于任何一个对象,即实现Dog接口的对象。
1 2 3 4 5 | public class Kennel { Dog[] dogs = new Dog[20]; public void addDog(Dog d) { ... } public void printDogs() { // Display names of all the dogs } } |
以下是如何使用它:
1 2 3 4 5 6 | Kennel k = new Kennel(); Dog d1 = new Pug("Spot"); Dog d2 = new Lab("Fido"); k.addDog(d1); k.addDog(d2); k.printDogs(); |
最后一条语句将显示:斑点菲多
接口使您能够指定一组行为,实现接口的所有类将共享这些行为。因此,我们可以定义变量和集合(如数组),这些变量和集合不必事先知道它们将持有什么类型的特定对象,只需它们将持有实现接口的对象。
别忘了,在以后的日子里,您可以使用一个现有的类,并使它实现
如果接口是可命名的,这就变得更清楚了。例如
1 2 3 4 5 |
等等(命名方案并不总是有效,例如,我不确定
集合框架中有一个很好的接口使用示例。如果编写一个接受
这使得像
the only purpose of interfaces is to make sure that the class which implements an interface has got the correct methods in it as described by an interface? Or is there any other use of interfaces?
我用新的界面来更新答案,这是用Java 8版本介绍的。
从Oracle文档页面的界面摘要:
接口声明可以包含
唯一具有实现的方法是默认方法和静态方法。
接口用途:
关于抽象类和接口之间的区别以及用例与工作示例之间的区别的一些相关SE问题:
接口和抽象类有什么区别?
我应该如何解释接口和抽象类之间的区别?
查看文档页面,了解Java 8中添加的新特性:默认方法和静态方法。
这就是为什么工厂模式和其他创作模式在Java中如此流行的原因。没错,如果没有Java,Java不提供一个非常简单的实例化抽象机制。尽管如此,在方法中没有创建对象的地方,您仍然可以得到抽象,这应该是您的大部分代码。
顺便提一句,我一般鼓励人们不要遵循"irealname"机制来命名接口。这是一个Windows / com的东西,把一只脚放在匈牙利符号的坟墓里,而实际上并不是必需的(Java已经是强类型的,并且拥有接口的整个点是使它们与类类型尽可能大的区别)。
接口的目的是抽象化,或者与实现分离。
如果您在您的程序中引入了一个抽象,那么您就不关心可能的实现。你感兴趣的是它能做什么,而不是怎么做,你用一个EDCOX1 0来表达这个意思。
接口,其中添加到Java中允许多个继承。Java开发人员虽然意识到拥有多重继承是一种"危险"的特性,这就是为什么提出了接口的概念。
多重继承很危险,因为您可能有如下类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
当我们使用
1 2 3 | <wyn> FunckyFigure.GetArea(); </wyn> |
所有的问题都是通过接口来解决的,因为您知道可以扩展接口,而且它们没有类方法……当然,编译器很好,它告诉您是否没有实现一个方法,但我喜欢认为这是一个更有趣的想法的副作用。
如果您有cardboardbox和htmlbox(两者都实现IBox),那么可以将它们传递给接受IBox的任何方法。尽管它们都非常不同并且不完全可交换,但是不关心"打开"或"调整大小"的方法仍然可以使用类(可能是因为它们关心在屏幕上显示某些内容需要多少像素)。
这是我对接口优势的理解。如果我错了就纠正我。假设我们正在开发操作系统,而其他团队正在为某些设备开发驱动程序。所以我们开发了一个接口存储设备。我们有两个由其他开发人员团队提供的IT实现(FDD和HDD)。
然后我们有一个operatingSystem类,它可以通过传递实现了StorageDevice接口的类实例来调用接口方法,如saveData。
这里的优点是我们不关心接口的实现。另一个团队将通过实现存储设备接口来完成这项工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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..."); } } |