用Java“编程到接口”总是有意义吗?


9

我已经看到了在这个问题上有关如何实例化从接口实现的类的讨论。就我而言,我正在用Java编写一个很小的程序,该程序使用的实例TreeMap,并且根据那里的每个人的看法,应将其实例化为:

Map<X> map = new TreeMap<X>();

在我的程序中,我正在调用函数map.pollFirstEntry(),该函数未在Map接口中声明(并且在Map接口中也存在其他几个声明)。我设法通过将其转换为一个TreeMap<X>我称之为以下方法的地方来做到这一点:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

我了解上述针对大型程序的初始化准则的优点,但是对于很小的程序(该对象不会传递给其他方法),我认为这是不必要的。尽管如此,我还是在工作应用程序中编写此示例代码,但我不希望我的代码看起来很糟也不混乱。什么是最优雅的解决方案?

编辑:我想指出的是,我对广泛的良好编码实践而不是特定功能的应用更感兴趣TreeMap。正如一些答案已经指出的那样(我已经标记为第一个回答),应该使用更高的抽象级别,而又不会失去功能。


1
需要功能时,请使用TreeMap。这可能是出于特定原因的设计选择,因此它也应该是实现的一部分。

@JordanReiter我应该只添加一个具有相同内容的新问题,还是存在内部交叉发布机制?
jimijazz,2015年


2
“我了解上述针对大型程序的初始化准则的优势”无论程序的大小如何,都必须在各处
Ben Aaronson

Answers:


23

“对接口进行编程”并不意味着“使用尽可能抽象的版本”。在这种情况下,每个人都只会使用Object

这意味着您应该以尽可能低的抽象度定义程序,而又不会失去功能。如果需要,TreeMap则需要使用来定义合同TreeMap


2
TreeMap不是接口,而是实现类。它实现了Map,SortedMap和NavigableMap接口。所描述的方法是NavigableMap界面的一部分。使用TreeMap将防止实现切换到ConcurrentSkipListMap(例如),这是对接口而不是实现进行编码的全部要点。

3
@MichaelT:在这种情况下,我没有看他需要的确切抽象,因此我仅TreeMap作为示例。“编程到接口”不应该被视为字面上的接口或抽象类-实现也可以视为接口。
Jeroen Vannevel 2015年

1
即使实现类的公共接口/方法从技术上来说是“接口”,但它打破了LSP背后的概念,并防止了替换其他子类,这就是为什么要编程public interface而不是“实现的公共方法”的原因。

@JeroenVannevel我同意当接口实际上由类表示时,可以对接口进行编程。但是,我看不到使用会带来什么好处或TreeMapSortedMapNavigableMap
toniedzwiedz

16

如果您仍然想使用界面,则可以使用

NavigableMap <X, Y> map = new TreeMap<X, Y>();

不必总是使用一个接口,但是经常会有一个要点,那就是您希望获得一个更通用的视图,该视图可以让您替换实现(也许用于测试),并且如果将对对象的所有引用都抽象为接口类型。


3

对接口而不是对实现进行编码的目的是避免泄漏实现细节,否则这些细节将限制您的程序。

考虑原始代码版本使用a HashMap并将其公开的情况。

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

这意味着对的任何更改getFoo()都是对API的重大更改,并且会使使用它的人们不满意。如果您要保证的foo只是一张地图,则应返回该地图。

private Map foo = new HashMap();
public Map getFoo() { return foo; }

这使您可以灵活地更改代码内部的工作方式。您意识到foo实际上需要成为一个按特定顺序返回事物的Map。

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

对于其余的代码,这没有任何破坏。

您以后可以在不破坏任何条件的情况下加强班级给予的合同。

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

这深入研究了Liskov替代原理

可替换性是面向对象编程中的一个原则。它指出,在计算机程序中,如果S是T的子类型,则可以用类型S的对象替换类型T的对象(即,类型S的对象可以替换类型T的对象),而无需更改任何期望的对象该程序的属性(正确性,执行的任务等)。

由于NavigableMap是Map的子类型,因此可以在不更改程序的情况下进行此替换。

公开实现类型使您难以在需要进行更改时更改程序的内部工作方式。这是一个痛苦的过程,并且很多时候会创建丑陋的解决方法,这只会在以后给编码器带来更多的痛苦(我正在看您以前的编码器,由于某些原因,该编码器一直在LinkedHashMap和TreeMap之间改组数据–每当我信任我看到svn怪你的名字,我担心)。

仍然希望避免泄漏实现类型。例如,由于某些性能特征,您可能希望实现ConcurrentSkipListMap而不是实现,或者只是喜欢java.util.concurrent.ConcurrentSkipListMap而不是java.util.TreeMap在import语句中实现。



1

这是关于你的沟通意图如何的对象应该被使用。例如,如果您的方法期望Map对象具有可预测的迭代顺序

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

然后,如果您绝对需要告诉上述方法的调用者它也返回Map具有可预测迭代顺序的对象,因为出于某些原因会有这样的期望:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

当然,调用者仍然可以将返回对象本身Map视为这样,但这超出了您的方法的范围:

private Map<String, String> output = processOrderedMap(input);

退后一步

对接口进行编码的一般建议(通常)是适用的,因为通常是由接口提供保证对象应能够执行的任务,也就是合同。许多初学者从这里开始,HashMap<K, V> map = new HashMap<>()建议他们将其声明为Map,因为a所HashMap提供的功能不超过a所能提供的Map。由此,他们将能够(希望地)理解为什么他们的方法应该采用a Map而不是a HashMap,这使他们实现了OOP中的继承功能。

仅引用维基百科条目中与该主题相关的每个人喜欢的原则的一行:

这是一种语义关系,而不仅仅是句法关系,因为它旨在保证层次结构中类型的语义互操作性。

换句话说,使用Map声明并不是因为它在语法上有意义,而是对对象的调用仅应注意它是的类型Map

清洁代码

我发现这也使我有时也可以编写更简洁的代码,尤其是在进行单元测试时。HashMap当我可以轻松地用代替它时,用一个只读测试条目创建一个需要花费多行(不包括使用双括号初始化)Collections.singletonMap()

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.