Java 接口中的可选方法

根据我的理解,如果你在 Java 中实现了一个接口,那么该接口中指定的方法必须由实现该接口的子类使用。

我注意到,在一些接口(如 Collection 接口)中,有些方法被注释为可选的,但这究竟意味着什么呢?它让我有点吃惊,因为我认为在接口中指定的所有方法都是必需的?

103972 次浏览

为了编译代码,所有的方法都必须实现(除了那些在 Java8 + 中使用 default实现的方法) ,但是实现不必做任何功能上有用的事情。具体来说:

  • 可能为空(空方法)
  • 可能只是抛出一个 UnsupportedOperationException(或类似)

后一种方法通常在集合类中使用——所有方法仍然实现,但是有些方法在运行时调用时可能会抛出异常。

Collection 接口中的可选方法意味着允许该方法的实现引发异常,但无论如何都必须实现异常。按照指定的 在文件里:

一些集合实现对 例如,一些实现禁止 null 元素,有些元素对其元素的类型有限制。 试图添加不合格元素将引发未检查的异常, 通常是 NullPointerException 或 ClassCastException 查询是否存在不合格的元素可能会引发异常,或者 它可能只是返回 false; 一些实现将显示 有些人会表现出后者。更一般地说, 试图对一个不合格的元素进行操作,该元素的完成 不会导致将不合格的元素插入到 集合可能引发异常,也可能成功 此类异常在 此接口的规范。

为了编译接口的实现(非抽象)类——所有方法都必须实现。

然而 ,如果我们认为一个方法的实现是一个简单的异常抛出(就像 Collection接口中的一些方法一样) ,那么在这种情况下,Collection接口是一个异常,而不是一般情况。通常是这样,实现类应该(也将)实现所有方法。

集合中的“可选”意味着实现类不必“实现”它(根据上面的术语) ,它只需抛出 NotSupportedException)。

一个很好的例子——用于不可变集合的 add()方法——具体实现的方法除了抛出 NotSupportedException什么也不做

Collection的情况下,这样做是为了防止混乱的继承树,这将使程序员痛苦-但对于 大部分的情况,这种范例是不建议的,应该避免,如果可能的话。


更新:

从 java 8开始,引入了 默认方法

这意味着,接口可以定义方法-包括它的实现。
这是为了允许向接口添加功能,同时仍然支持不需要新功能的代码片段的向后兼容性。

注意,该方法仍然由声明它的所有类实现,但是使用了接口的定义。

Java 中的接口只是声明了实现类的契约。接口 必须的中的所有方法都可以实现,但是实现类可以将它们保留为未实现的,即空白。举个人为的例子,

interface Foo {
void doSomething();
void doSomethingElse();
}


class MyClass implements Foo {
public void doSomething() {
/* All of my code goes here */
}


public void doSomethingElse() {
// I leave this unimplemented
}
}

现在我没有实现 doSomethingElse(),留给我的子类去实现它,这是可选的。

class SubClass extends MyClass {
@Override
public void doSomethingElse() {
// Here's my implementation.
}
}

但是,如果您讨论的是 Collection 接口,正如其他人所说,它们是一个例外。如果某些方法没有实现,并且您调用了这些方法,那么它们可能会抛出 UnsupportedOperationException异常。

这里的答案似乎有很多令人困惑的地方。

Java 语言要求接口中的每个方法都由该接口的每个实现实现。就这样。说“集合是一个例外”意味着对这里到底发生了什么有一个非常模糊的理解。

重要的是要认识到,对于一个接口,有两个层次的一致性:

  1. Java 语言可以检查的内容。这基本上可以归结为: 每个方法是否都有 一些实现?

  2. 实际履行契约。也就是说,实现是否按照接口中的文档所说的去做?

    编写良好的接口将包括文档,准确地解释从实现中期望得到什么。您的编译器不能为您检查这个。你得看看文件,照他们说的做。如果您没有按照合同的要求去做,那么就 编译器而言,您将拥有一个接口的实现,但它将是一个有缺陷/无效的实现。

在设计 CollectionsAPI 时,Joshua Bloch 决定不再使用非常细粒度的接口来区分不同的集合变体(例如: 可读、可写、随机访问等) ,而是主要使用非常粗糙的接口集 CollectionListSetMap,然后将某些操作记录为“可选”。这是为了避免细粒度接口带来的组合爆炸。来自 Java 集合 API 设计常见问题解答:

为了详细说明这个问题,假设您想要添加 层次结构的可修改性概念。您需要四个新的 接口: 可修改集合,可修改集合,可修改列表,和 以前简单的层次结构现在变得杂乱无章 另外,您还需要一个新的 Iterator 接口,以便与 不包含移除操作的。 现在你可以去掉不支持的操作异常了吗 没有。

考虑数组,它们实现了大多数 List 操作,但是没有实现 删除和添加。它们是“固定大小”列表。如果要捕获 这个概念在层次结构中,你必须添加两个新的接口: VariableSizeList 和 VariableSizeMap VariableSizeCollection 和 VariableSizeSet,因为它们 与可修改集合和可修改集合相同,但您可以 为了保持一致性,你可以选择添加它们。同时,你需要一个新的 各种不支持添加和删除的 ListIterator 行动,以沿着不可修改的名单。现在,我们已经达到10或 十二个接口,加上两个新的迭代器接口,而不是我们的 - 原创四人组,结束了吗?-没有。

考虑日志(如错误日志、审计日志和 可恢复的数据对象) ,它们是自然的仅追加序列, 它支持除了删除和设置之外的所有 List 操作 它们需要一个新的核心接口和一个新的迭代器。

那么不可更改的集合呢,与不可更改的集合相比呢? (即,不能被客户端 AND 更改的集合将永远不会 许多人认为这是最 重要的区别,因为它允许多个线程 并发访问集合而不需要同步。 将此支持添加到类型层次结构中还需要四个 接口。

现在我们有了大约20个接口和5个迭代器 几乎可以肯定,在实践中仍然会出现收藏 不能完全适合于任何接口。例如, Map 返回的集合视图是自然的只删除集合。 此外,还有一些集合将拒绝 基于它们的价值,所以我们还没有抛弃运行时 例外。

说到底,我们觉得这是一个完美的工程 通过提供一个非常小的集合来回避整个问题 可以引发运行时异常的核心接口。

当 CollectionsAPI 中的方法被记录为“可选操作”时,这并不意味着你可以把方法实现留在实现中,也不意味着你可以使用一个空的方法主体(首先,许多方法需要返回一个结果)。相反,它意味着有效的实现选择(仍然符合约定的选择)是抛出 UnsupportedOperationException

注意,因为 UnsupportedOperationExceptionRuntimeException,所以就编译器而言,您可以从任何方法实现抛出它。例如,您可以从 Collection.size()的实现中抛出它。然而,这样的实现将违反合同,因为 Collection.size()的文档没有说明这是允许的。

旁白: Java 的 CollectionsAPI 所使用的方法有些争议(不过,现在可能比最初引入时要少)。在一个完美的世界中,接口 没有有可选的操作,而细粒度的接口将被替代使用。问题是 Java 既不支持推断的结构类型,也不支持交叉类型,这就是为什么尝试用“正确的方式”做事情最终会在集合的情况下变得极其笨拙。

如果我们通读 grepCode 中的 AbstractCollection.java代码,它是所有集合实现的祖先类,它将帮助我们理解可选方法的含义。下面是 AbstractCollection 类中 add (e)方法的代码。根据 收藏品接口,add (e)方法是可选的

public boolean  add(E e) {


throw new UnsupportedOperationException();
}

可选方法意味着它已经在祖先类中实现,并且在调用时抛出 Unsupport tedOperationException。如果我们想让我们的集合可修改,那么我们应该覆盖集合接口中的 可以选择方法。

事实上,我的灵感来自于 SurfaceView.Callback2。我认为这是正式的方式

public class Foo {
public interface Callback {
public void requiredMethod1();
public void requiredMethod2();
}


public interface CallbackExtended extends Callback {
public void optionalMethod1();
public void optionalMethod2();
}


private Callback mCallback;
}

如果您的类不需要实现可选方法,只需“实现 Callback”。 如果您的类需要实现可选方法,只需“实现 CallbackExtended”。

抱歉说脏话。

尽管它没有回答 OP 的问题,但是值得注意的是,从 Java8开始,向接口 实际上是可行的添加默认方法。放置在接口的方法签名中的 default关键字将导致类拥有重写方法的选项,但不要求重写方法。

我正在寻找一种实现回调接口的方法,所以实现可选方法是必要的,因为我不想为每个回调实现每个方法。

因此,我没有使用接口,而是使用了一个具有空实现的类,比如:

public class MyCallBack{
public void didResponseCameBack(String response){}
}

可以像这样设置成员变量 CallBack,

c.setCallBack(new MyCallBack() {
public void didResponseCameBack(String response) {
//your implementation here
}
});

那就这么说吧。

if(mMyCallBack != null) {
mMyCallBack.didResponseCameBack(response);
}

这样,您就不必担心实现每个回调的每个方法,而只需覆盖您需要的方法。

这个话题已经被提到... 是的。.但是想想,缺少一个答案。我说的是接口的“默认方法”。 例如,假设您有一个用于关闭任何东西(比如析构函数或其他东西)的类。假设它应该有3个方法。我们将它们称为“ doFirst ()”、“ doLast ()”和“ onClose ()”。

所以我们说我们希望任何类型的对象至少实现“ onClose ()”,但是其他对象是可选的。

您可以通过使用接口的“默认方法”来实现这一点。我知道,在大多数情况下,这会否定接口的原因,但是如果你正在设计一个框架,这可能是有用的。

所以如果你想用这种方式来实现它,它会看起来如下

public interface Closer {
default void doFirst() {
System.out.print("first ... ");
}
void onClose();
default void doLast() {
System.out.println("and finally!");
}
}

例如,如果你在一个名为“ Test”的类中实现了它,那么编译器就可以完全满足以下要求:

public class TestCloser implements Closer {
@Override
public void onClose() {
System.out.print("closing ... ");
}
}

输出:

first ... closing ... and finally!

或者

public class TestCloser implements Closer {
@Override
public void onClose() {
System.out.print("closing ... ");
}


@Override
public void doLast() {
System.out.println("done!");
}
}

输出:

first ... closing ... done!

所有的组合都是可能的。任何带有“默认”的东西都可以实现,但是不能实现,但是任何没有默认的东西都必须实现。

希望我现在的回答不是完全错误的。

祝大家今天愉快!

[ edit 1] : 请注意: 这只能在 Java8中使用。

在 Java8和更高版本中,这个问题的答案仍然有效,但是现在更加微妙了。

首先,这些来自公认答案的陈述仍然是正确的:

  • 接口意味着在契约中指定它们的隐式行为(实现类必须遵守的行为规则声明,以便被认为是有效的)
  • 契约(规则)和实现(规则的程序化编码)之间是有区别的
  • 在接口中指定的方法必须总是被实现(在某些时候)

那么,Java8中新的细微差别是什么呢?当谈到 「可供选择的方法」时,下列任何一项现在都适用:

1. 一种方法,其实现在契约上是可选的

“第三条语句”说抽象接口方法必须始终实现,这在 Java8 + 中仍然成立。然而,正如在 Java集合框架中一样,在合同中可以将一些抽象接口方法描述为“可选的”。

在这种情况下,实现接口的作者可以选择不实现该方法。然而,编译器将坚持使用一个实现,因此作者将这段代码用于特定实现类中不需要的任何可选方法:

public SomeReturnType optionalInterfaceMethodA(...) {
throw new UnsupportedOperationException();
}

在 Java7和更早的版本中,这实际上是唯一一种“可选方法”,也就是说,一个方法如果没有实现,就会抛出一个 Unsupport tedOperationException。这种行为必须由接口契约(例如,Java集合框架的可选接口方法)指定。

2. 重新实现是可选的默认方法

Java8引入了 默认方法的概念。这些方法的实现可以由接口定义本身提供,也可以由接口定义本身提供。通常只有当方法主体可以使用其他接口方法(即“原语”)编写时,以及当 this可以表示“其类实现了该接口的对象”时,才可能提供默认方法

默认方法必须实现接口的约定(就像任何其他接口方法实现一样)。因此,在实现类中指定接口方法的实现由作者自行决定(只要行为适合他或她的目的)。

在这种新的环境下,Java集合框架 ABc0被重写为:

public interface List<E> {
:
:
default public boolean add(E element) {
throw new UnsupportedOperationException();
}
:
:
}

这样,如果实现类没有提供它自己的新行为,那么“可选”方法 add()具有抛出 Unsupport tedOperationException 的默认行为,这正是您希望发生的,并且符合 List 的契约。如果作者编写的类不允许将新元素添加到 List 实现中,则 add()的实现是可选的,因为默认行为正是所需的。

在这种情况下,上面的“第三条语句”仍然成立,因为该方法已经在接口本身中实现。

3. 返回 Optional结果的方法

最后一种新的可选方法是返回 Optional的方法。Optional类提供了一种更加面向对象的方法来处理 null结果。

在一种流畅的编程风格中,比如在使用新的 Java Streams API 编码时常见的那种,任何时候的 null 结果都会导致程序崩溃,出现 NullPointerException 异常。Optional类提供了一种将 null 结果返回到客户机代码的机制,这种方式使连贯样式不会导致客户机代码崩溃。

Oracle 的 Java 集合教程:

为了使核心集合接口的数量可管理,Java 平台不为每种集合类型的每个变体提供单独的接口。(这样的变体可能包括不可变的、固定大小的和仅附加的。)相反,每个接口中的修改操作被指定为可选的ーー给定的实现可能选择不支持所有操作。如果调用不受支持的操作,集合将引发 不支持的操作异常。实现负责记录它们支持的可选操作。Java 平台的所有通用实现都支持所有可选操作。

我想对接口中的“可选”方法做一些澄清,记住这篇文章已经有10年的历史了。

正如前面已经提到的(@amit 是最著名的一个) ,从 Java8开始,接口可以具有 default方法。这是解决这个问题的一种方法,因为实现类不需要实现默认方法(因此您可以从实现的角度调用“可选”)。但是,实现类仍然可以调用这个方法,因为接口正在定义它。因此,实际上,这不是一个可选的行为,因为类已经包含了接口强加的(默认)行为。我有一个更好的选择,稍后我会提到。

我要说的第二点是,在绝大多数情况下,实施“什么都不做”的方法是一个糟糕的想法。“什么都不做”的方法会引入副作用,大多数时候弊大于利,而且有可能,你永远不会意识到这些副作用。作为一个例子,我想用 JUnit 测试来说明这一点。如果您创建了一个包含大量“什么也不做”测试方法的测试类,框架将执行这些方法,并将它们标记为已通过,而实际上并没有执行任何测试。这是相当危险的副作用。将此场景扩展到您可能创建的包含“不做任何事”方法的公共库。客户机可能不知道这些空实现,并且可能调用这些方法,而不知道它们什么都不做。我可以举出更多的例子,但这不是我回答的重点。总之,实现一个“什么都不做”的方法并不是一个“可选”行为的例子。实现该方法的事实已经证明了它不是可选的(“不做任何事”的行为与不存在的行为是不一样的)。

不管您使用的是什么版本的 Java,这个问题的最佳解决方案是遵循 SOLID 原则中的“我”: < em > 接口隔离原则 。使用 ISP,您可以在单独的接口中隔离可选和必需的方法。假设您需要创建一个 Calculator并打算使用一个 CalculatorOperations接口。但是这个界面的操作比你需要的多,因为你的计算器只需要使用四个基本的数学运算(加、减、乘、除)。ISP 声称 不应该强迫任何客户端依赖于它不使用的方法。因此,处理这个问题的最佳方法是将 CalculatorOperations接口分解为两个或多个接口,例如 BasicOperationsAdvancedOperations。一旦你通过使用不同的接口从可选函数中分离出所需的接口,你可以让你的类实现所需的接口,或者更好的是,在需要的时候使用依赖注入(DI)注入所需的行为(甚至删除它)。例如,使用 DI 可以让你通过简单地点击一个按钮(即小算盘)来显示和隐藏操作。

我知道计算器操作接口只是一个简单的例子,但关键是 ISP 是使用 Java 中的接口明确区分必需操作和可选操作的最佳方式。