除了拥有正确的方法之外,还有更多的接口吗
Posted
技术标签:
【中文标题】除了拥有正确的方法之外,还有更多的接口吗【英文标题】:Is there more to an interface than having the correct methods 【发布时间】:2010-10-05 00:43:38 【问题描述】:假设我有这个界面:
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();
如果这是真的,那么接口的唯一目的是确保实现接口的类具有接口所描述的正确方法?或者接口还有其他用途吗?
【问题讨论】:
请记住,接口并非特定于 Java。所有 OOP 语言都以某种形式拥有它们,但并不总是像 Java 那样明确定义。 从技术上讲,所有强类型 OOP 语言都以某种形式拥有它们。无类型或鸭类型的语言没有类似的概念。 @Jared 你是不是把强类型和静态类型混淆了,而“无类型”和动态类型混淆了? 多态也可以通过接口来实现。查看本页最后一节codenuggets.com/2014/06/20/java-interface ***.com/a/24436493/1286942 【参考方案1】:接口是一种让您的代码更加灵活的方法。你要做的是:
Ibox myBox=new Rectangle();
然后,稍后,如果您决定要使用不同类型的框(也许有另一个库,具有更好类型的框),您将代码切换为:
Ibox myBox=new OtherKindOfBox();
一旦你习惯了它,你会发现这是一种很棒的(实际上是必不可少的)工作方式。
另一个原因是,例如,如果您想创建一个框列表并对每个框执行一些操作,但您希望列表包含不同类型的框。在每个盒子上你可以做:
myBox.close()
(假设 IBox 有一个 close() 方法)即使 myBox 的实际类会根据您在迭代中所处的框而发生变化。
【讨论】:
这个答案中没有什么是 Java 接口 独有的。这同样适用于抽象类,甚至具体类。我希望有一个很好的答案来提及实现多个接口的能力,以及何时/为什么有用。 这是如何被选为答案的?这是对多态性为何有用的简要描述,但正如上面的海报所说,我希望对多个接口有更好的解释,更重要的是,什么时候适合使用接口而不是抽象类。 这与解释接口几乎没有关系,而是与多态的基础知识有关【参考方案2】:使接口有用的原因不是“您可以改变主意并在以后使用不同的实现,而只需更改创建对象的地方”。这不是问题。
真正的意义已经在名称中:他们定义了一个接口,任何人都可以实现该接口以使用在该接口上运行的所有代码。最好的例子是java.util.Collections
,它提供了各种专门在接口上操作的有用方法,例如sort()
或reverse()
for List
。这里的重点是,这段代码现在可用于对实现List
接口的任何 类进行排序或反转——不仅是ArrayList
和LinkedList
,还包括您自己编写的类,这可能会以编写java.util.Collections
的人从未想象过的方式实现。
同样,您可以编写在众所周知的接口或您定义的接口上运行的代码,其他人可以使用您的代码,而无需要求您支持他们的类。
接口的另一个常见用途是回调。例如,java.swing.table.TableCellRenderer,它允许您影响 Swing 表如何显示特定列中的数据。您实现该接口,将实例传递给JTable
,然后在表格呈现期间的某个时间点,您的代码将被调用以执行其工作。
【讨论】:
这是一个很好的答案,我喜欢你从 java 包类中给出的例子...... 我喜欢you can write code that operates on well-known interfaces, or interfaces you define
等一下……使接口有用的不是[使用您喜欢的任何实现的能力],而是[使用您喜欢的任何实现的能力]?不过,提及相反的情况是一个很好的观点。
@Powerslave:paraphrase it more like 使接口有用的不是[在更改实现时只需更改一行即可编写代码的能力],而是[在您需要的地方编写代码的能力]甚至根本不指定实现]。
@MichaelBorgwardt 这听起来好多了。 :) 感谢您的澄清!【参考方案3】:
我读过的许多用途之一是在 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
【讨论】:
@CHEBURASHKA 和糟糕的命名。如果Reptile
“咀嚼”,那么它本身不是“可咀嚼的”。 (有时)命名接口 Whateverable 的约定应该只应用在它完全有意义的地方。将接口命名为Predator
在这里会更合适。
@Powerslave 这是正确的恕我直言,爬行动物“能够咀嚼”/“可咀嚼”。鹰是捕食者,但仍然不能咀嚼……只是挑剔,但“可咀嚼”可以在界面文档中更好地定义。
精湛..解释得很好。谢谢你..!
我不明白。似乎接口是确保您在实现它的每个类中实现相同方法的好方法(例如,因此它可以防止我做出愚蠢的选择,即使用 run() 和 Dog 类的 Bird 类与 runn() - 他们都是一样的)。但是通过注意并使我的类具有相同的方法格式/结构,我不能实现同样的事情吗?真的看起来接口只是确保程序员不会健忘。此外,界面似乎并没有节省我的时间。我仍然需要在实现它的每个类中定义方法。
@AlexG - 我告诉了其中一种用途,伙计 :) 还有更多,我们几乎没有触及表面以简单的方式回答问题!【参考方案4】:
接口的目的是多态性,也就是类型替换。例如,给定以下方法:
public void scale(IBox b, int i)
b.setSize(b.getSize() * i);
当调用scale
方法时,您可以提供任何实现IBox
接口类型的值。换句话说,如果Rectangle
和Square
都实现了IBox
,那么您可以在任何需要IBox
的地方提供Rectangle
或Square
。
【讨论】:
如果我已经可以在 Java 中通过子类化和方法覆盖来实现接口多态性的目的,为什么? 是一样的,只是接口必须省略任何实现。因此,类可以实现多个接口。 嘿,我从来没有说过 Java 有任何概念完整性。类型替换是所有子类型化的目的。 Java 恰好有不止一种子类型化机制,但没有一个特别好。 我也从未说过概念完整性。但让我们继续前进。如果您可以使用您的方法缩放每个 IBox,它不应该是在 IBox 上声明的操作:IBox.scale(int)? 我们不想将 Integer 耦合到 IBox,这就是为什么我们不将其作为 Integer 上的方法。接口上方法的数量取决于它所表达的抽象的一致性和内聚性,而不是实现它的繁琐程度。无论如何,感谢您的回答 Apo。【参考方案5】:接口允许静态类型语言支持多态性。一个面向对象的纯粹主义者会坚持认为一种语言应该提供继承、封装、模块化和多态性,以便成为一种功能齐全的面向对象语言。在动态类型或鸭子类型的语言(如 Smalltalk)中,多态是微不足道的。然而,在静态类型语言(如 Java 或 C#)中,多态性远非微不足道(事实上,从表面上看,它似乎与强类型的概念不一致。)
让我演示一下:
在动态类型(或鸭子类型)语言(如 Smalltalk)中,所有变量都是对对象的引用(不多也不少。)所以,在 Smalltalk 中,我可以这样做:
|anAnimal|
anAnimal := Pig new.
anAnimal makeNoise.
anAnimal := Cow new.
anAnimal makeNoise.
那个代码:
-
声明一个名为 anAnimal 的局部变量(请注意,我们不指定变量的类型 - 所有变量都是对对象的引用,不多也不少。)
创建名为“Pig”的类的新实例
将 Pig 的新实例分配给变量 anAnimal。
向猪发送消息
makeNoise
。
使用奶牛重复整个过程,但将其分配给与猪完全相同的变量。
相同的 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 继承,因为它不应该实现收获。看起来我们需要多重继承 - 从多个类继承的能力。但事实证明这是一个非常困难的语言特性,因为会出现所有边缘情况(当多个并行超类实现相同的方法时会发生什么?等等)
接口随之而来...
如果我们创建 Animal 和 Vegetable 类,每个都实现 Growable,我们可以声明我们的 Cow 是 Animal 而我们的 Corn 是蔬菜。我们还可以声明动物和蔬菜都是可生长的。这让我们写这个来发展一切:
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();
所以....总结一下:
接口实现允许您指定对象可以做哪些事情(交互),类继承可以让您指定事情应该如何做(实现)。
接口为我们提供了“真正的”多态性的许多好处,而不会牺牲编译器类型检查。
【讨论】:
这是我对另一个问题的回答:***.com/questions/379282/…。但是,它们是相关的答案。 所以我想问一下,鸭子类型的语言如何区分 Animal.water() (谨慎的农民过去常说它需要泄漏)和他使用的 Plant.water()给植物浇水。模棱两可是敌人。克服歧义所需的任何冗长都是可以接受的。 是的..ambiguity 是带有鸭子类型语言的游戏名称。在使用鸭子类型语言进行专业工作时,经常会看到名称长度为 50-100 个字符的成员(方法和变量)。 鸭子类型语言的另一个大缺点是无法基于静态分析进行程序化重构 - 尝试向 Smalltalk 图像询问您的 printString 方法的所有调用者的列表...您将获得列表所有 printString 方法的所有调用者...... ...因为 Automobile#printString 的调用者无法以编程方式与 NearEarthOrbit#printString 的调用者区分开来。【参考方案6】:通常接口定义你应该使用的接口(正如名字所说的那样;-))。样本
public void foo(List l)
... do something
现在您的函数foo
接受ArrayList
s、LinkedList
s,...不仅仅是一种类型。
Java 中最重要的是您可以实现多个接口,但您只能扩展一个类!样本:
class Test extends Foo implements Comparable, Serializable, Formattable
...
是可能的,但是
class Test extends Foo, Bar, Buz
...
不是!
您上面的代码也可以是:IBox myBox = new Rectangle();
。现在重要的是,myBox 仅包含来自 IBox 的方法/字段,而不包含来自 Rectangle
的(可能存在的)其他方法。
【讨论】:
'List' 应该是接口成员吗? List是java集合库中的一个接口。 List 是标准 Java 库 (java.sun.com/javase/6/docs/api/java/util/List.html) 中的一个接口。他只是用它来说明他的观点。【参考方案7】:我认为您了解接口所做的一切,但您还没有想象接口有用的情况。
如果您在一个狭窄的范围内(例如,在一个方法调用中)实例化、使用和释放一个对象,那么接口实际上并没有添加任何东西。正如您所指出的,具体类是已知的。
接口有用的地方是当一个对象需要在一个地方创建并返回给可能不关心实现细节的调用者时。让我们将您的 IBox 示例更改为形状。现在我们可以有 Shape 的实现,例如 Rectangle、Circle、Triangle 等,getArea() 和 getSize() 方法的实现对于每个具体类都是完全不同的。
现在您可以使用具有各种 createShape(params) 方法的工厂,该方法将根据传入的参数返回适当的 Shape。显然,工厂将知道正在创建的 Shape 类型,但调用者赢了不必关心是圆形,还是方形,等等。
现在,假设您必须对形状执行各种操作。也许您需要按区域对它们进行排序,将它们全部设置为新大小,然后在 UI 中显示它们。 Shapes 都是由工厂创建的,然后可以很容易地传递给 Sorter、Sizer 和 Display 类。如果您将来需要添加一个六边形类,您无需更改任何内容,只需更改工厂即可。如果没有界面,添加另一个形状会变得非常混乱。
【讨论】:
【参考方案8】:你可以的
Ibox myBox = new Rectangle();
这样您就可以将此对象用作 Ibox,而您并不关心它真的是 Rectangle
。
【讨论】:
也就是说我们可以这样写?! > 矩形 inst = 新矩形 (); @Mr.Hyde 如果你以后想添加Square
,你会遇到问题...... @ 具有相同的方法...当您拥有更大的代码库时,这可能会导致一场噩梦...记住,接口定义了一个模板。【参考方案9】:
为什么是接口??????
从狗开始。特别是哈巴狗。
哈巴狗有各种行为:
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();
最后一条语句将显示: 现货菲多
接口使您能够指定一组行为,实现该接口的所有类将共享这些行为。因此,我们可以定义变量和集合(例如数组),它们不必事先知道它们将持有什么样的特定对象,只需知道它们将持有实现接口的对象。
【讨论】:
@niranjan kurambhatti 我可以让所有类都扩展狗,但为什么还要接口??【参考方案10】:如何使用接口的一个很好的例子是 Collections 框架。如果您编写一个接受List
的函数,那么用户传入Vector
或ArrayList
或HashList
或其他什么都没有关系。您也可以将 List
传递给任何需要 Collection
或 Iterable
接口的函数。
这使得Collections.sort(List list)
之类的功能成为可能,无论List
是如何实现的。
【讨论】:
【参考方案11】:这就是Factory Patterns 和其他创建模式在Java 中如此流行的原因。您是正确的,没有它们,Java 不会提供开箱即用的机制来轻松抽象实例化。尽管如此,在你不在你的方法中创建对象的任何地方你都会得到抽象,这应该是你的大部分代码。
顺便说一句,我通常鼓励人们不要遵循“IRealname”机制来命名接口。这是一个 Windows/COM 的东西,它把一只脚踩在了匈牙利符号的坟墓里,而且真的没有必要(Java 已经是强类型的,拥有接口的全部意义在于让它们与类类型在很大程度上无法区分)。
【讨论】:
您将强类型与静态类型混淆了。【参考方案12】:不要忘记,以后您可以采用 现有的 类,并使其实现 IBox
,然后它将可用于您的所有框识别代码。 p>
如果接口被命名为-able,这就更清楚了。例如
public interface Saveable
....
public interface Printable
....
等等。 (命名方案并不总是有效,例如我不确定Boxable
在这里是否合适)
【讨论】:
【参考方案13】:接口的唯一目的是确保实现接口的类具有接口所描述的正确方法?或者接口还有其他用途吗?
我正在用 java 8 版本引入的界面新功能更新答案。
来自summary of interface 上的 oracle 文档页面:
接口声明可以包含
-
方法签名
默认方法
静态方法
常量定义。
唯一有实现的方法是默认方法和静态方法。
界面使用:
-
定义合同
链接不相关的类具有能力(例如,实现
Serializable
接口的类除了实现该接口外,它们之间可能有也可能没有任何关系
提供可互换实现e.g. strategy pattern
默认方法使您能够向库的接口添加新功能,并确保与为这些接口的旧版本编写的代码的二进制兼容性
使用静态方法组织库中的辅助方法(您可以将特定于接口的静态方法保留在同一接口中,而不是在单独的类中)
关于抽象类和接口之间区别的一些相关SE问题以及带有工作示例的用例:
What is the difference between an interface and abstract class?
How should I have explained the difference between an Interface and an Abstract class?
查看documentation 页面以了解 java 8 中添加的新功能:默认方法和静态方法。
【讨论】:
我删除了 java-8 标记,因为 question 没有询问有关 java-8 的任何内容(实际上早在 java-8 之前就有人问过了)。标签是用来提问的,不是用来回答的。【参考方案14】:接口的目的是抽象,或与实现解耦。
如果您在程序中引入抽象,您就不会关心可能的实现。您对什么它可以做什么而不是如何感兴趣,并且您使用 interface
在 Java 中表达这一点。
【讨论】:
所有结构化编程的目的都是抽象。为什么你会说接口的目的是抽象,因为我可以使用泛型和类组合来实现完全相同的事情? 如果所有结构化编程都是抽象的(您的主张),那么接口就是该抽象中的抽象。【参考方案15】:如果你有 CardboardBox 和 HtmlBox(两者都实现了 IBox),你可以将它们都传递给任何接受 IBox 的方法。即使它们非常不同且不能完全互换,但不关心“打开”或“调整大小”的方法仍然可以使用您的类(可能是因为它们关心在屏幕上显示某些内容需要多少像素)。
【讨论】:
【参考方案16】:将特征添加到 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();
所有的问题都可以通过接口解决,因为你知道你可以扩展接口并且它们不会有分类方法......当然编译器很好,它会告诉你如果你没有实现方法,但我喜欢认为这是一个更有趣的想法的副作用。
【讨论】:
您可能希望在答案中区分多实现继承和多接口继承,否则会令人困惑。【参考方案17】:这是我对接口优势的理解。如果我错了,请纠正我。 想象一下,我们正在开发操作系统,而其他团队正在开发某些设备的驱动程序。 所以我们开发了一个接口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...");
【讨论】:
同样的事情可以用抽象类 StorageDevice 和扩展 StorageDevice 类的 FDD 和 HDD 类来完成。但是如果我们使用抽象类,我们就不能利用多重继承。以上是关于除了拥有正确的方法之外,还有更多的接口吗的主要内容,如果未能解决你的问题,请参考以下文章
除了缩略图方法之外,还有其他方法可以在 Iphone 中截取视频吗?