140

我被告知 Java 中的 Marker 接口是一个空接口,用于向编译器或 JVM 发出信号,即必须以特殊方式处理实现此接口的类的对象,例如序列化、克隆等。

但最近我了解到它实际上与编译器或 JVM 无关。例如,在Serializable接口的情况下,方法writeObject(Object)ObjectOutputStream检测instanceOf Serializable类是否实现Serializable并相应地抛出NotSerializableException。一切都在代码中处理,这似乎是一种设计模式,所以我认为我们可以定义自己的标记接口。

现在我的疑问:

  1. 上面第一点提到的标记接口的定义是错误的吗?那么我们如何定义一个 Marker 接口呢?

  2. 而不是使用instanceOf运算符,为什么该方法不能类似于writeObject(Serializable)编译时类型检查而不是运行时?

  3. 注释比标记接口好在哪里?

4

10 回答 10

121
  1. 上面第一点提到的标记接口的定义是错误的吗?- 正确的部分是(1)标记接口必须为空,(2)实现它意味着对实现类进行一些特殊处理。不正确的部分是它暗示 JVM 或编译器会以不同的方式处理该类的对象:您正确地观察到 Java 类库的代码将这些对象视为可克隆、可序列化等。它有与编译器或JVM无关。
  2. 而不是使用 instanceOf 运算符,为什么该方法不能像writeObject(Serializable)这样进行编译时类型检查- 这可以让您避免在需要“普通”时使用标记接口的名称污染您的代码Object。例如,如果您创建了一个需要可序列化的类,并且具有对象成员,那么您将被迫Serializable在编译时进行强制转换或创建对象。这是不方便的,因为界面没有任何功能。
  3. 注释如何比标记接口更好?- 它们让您实现将有关类的元数据传达给其消费者的相同目的,而无需为其创建单独的类型。注释也更强大,让程序员将更复杂的信息传递给“使用”它的类。
于 2014-09-15T14:37:45.873 回答
25

由于不可序列化类的子类可以序列化,因此无法强制执行Serializable,但它们的实例可能会向上转换回父类。writeObject因此,持有对不可序列化对象(如Object)的引用并不意味着所引用的实例不能真正序列化。例如在

   Object x = "abc";
   if (x instanceof Serializable) {
   }

父类 ( Object) 不可序列化,将使用其无参数构造函数进行初始化。x, ,引用的值String是可序列化的,条件语句将运行。

于 2014-09-15T14:33:29.113 回答
7

我做了一个简单的演示来解决疑问 1 和 2:

我们将拥有一个由MobilePhone.javaClass 实现的 Movable 接口和一个LandlinePhone.java不实现 Movable 接口的类

我们的标记界面:

package com;

public interface Movable {

}

LandLinePhone.javaMobilePhone.java

 package com;

 class LandLinePhone {
    // more code here
 }
 class MobilePhone implements Movable {
    // more code here
 }

我们的自定义异常类:package com;

public class NotMovableException extends Exception {

private static final long serialVersionUID = 1L;

    @Override
    public String getMessage() {
        return "this object is not movable";
    }
    // more code here
    }

我们的测试课:TestMArkerInterface.java

package com;

public class TestMarkerInterface {

public static void main(String[] args) throws NotMovableException {
    MobilePhone mobilePhone = new MobilePhone();
    LandLinePhone landLinePhone = new LandLinePhone();

    TestMarkerInterface.goTravel(mobilePhone);
    TestMarkerInterface.goTravel(landLinePhone);
}

public static void goTravel(Object o) throws NotMovableException {
    if (!(o instanceof Movable)) {
        System.out.println("you cannot use :" + o.getClass().getName() + "   while travelling");
        throw new NotMovableException();
    }

    System.out.println("you can use :" + o.getClass().getName() + "   while travelling");
}}

现在当我们执行主类时:

you can use :com.MobilePhone while travelling
you cannot use :com.LandLinePhone while travelling
Exception in thread "main" com.NotMovableException: this object is not movable
    at com.TestMarkerInterface.goTravel(TestMarkerInterface.java:22)
    at com.TestMarkerInterface.main(TestMarkerInterface.java:14)

因此,任何实现标记接口的类Movable都将通过测试,否则将显示错误消息。

这是对SerializableCloneableinstanceOf进行操作员检查的方式

于 2018-09-04T12:41:29.513 回答
6

Java 中的标记接口是没有字段或方法的接口。更简单地说,Java 中的空接口称为标记接口。标记接口的示例是SerializableCloneable接口Remote。这些用于向编译器或 JVM 指示一些信息。所以如果 JVM 看到一个类是Serializable,它可以对它做一些特殊的操作。同样,如果 JVM 看到某个类正在实现Cloneable,它可以执行一些操作来支持克隆。RMI 和Remote接口也是如此。简而言之,标记接口向编译器或 JVM 指示信号或命令。

以上内容最初是博客文章的副本,但已针对语法进行了轻微编辑。

于 2014-12-16T04:14:58.553 回答
5

a/ 顾名思义,一个标记接口的存在只是为了通知任何知道它的东西,一个类声明了一些东西。任何东西都可以是Serializable接口的 JDK 类,也可以是您为自定义类编写的任何类。

b/ 如果是标记接口,不应该暗示任何方法的存在——最好在接口中包含隐含的方法。但是,如果您知道为什么 需要它,您可以决定根据需要进行设计

c/ 空接口和不使用值或参数的注解之间几乎没有区别。但不同之处在于:注解可以声明在运行时可访问的键/值列表。

于 2014-09-15T14:44:03.783 回答
4
  1. 它与 JVM 和编译器无关(必然),它与任何对给定标记接口感兴趣并正在测试的代码有关。

  2. 这是一个设计决定,这样做是有充分理由的。请参阅 Audrius Meškauskas 的答案。

  3. 关于这个特定的话题,我认为这不是更好或更坏的问题。标记界面正在做它应该做的事情。

于 2014-09-15T14:33:40.290 回答
3

一个。我一直将它们视为一种设计模式,而不是 JVM-Special 我在几种情况下都使用过这种模式。

C。我相信使用注释来标记某些东西是比使用标记接口更好的解决方案。仅仅因为接口首先旨在定义类型/类的通用接口。它们是阶级层次的一部分。

注释旨在为代码提供元信息,我认为标记是元信息。因此,它们正是针对该用例的。

于 2014-09-15T14:39:55.830 回答
3

标记接口的主要目的是创建特殊类型,其中类型本身没有自己的行为。

public interface MarkerEntity {

}

public boolean save(Object object) throws InvalidEntityFoundException {
   if(!(object instanceof MarkerEntity)) {
       throw new InvalidEntityFoundException("Invalid Entity Found, can't be  saved);
   } 
   return db.save(object);
}

这里的 save 方法确保只保存实现 MarkerEntity 接口的类的对象,对于其他类型抛出 InvalidEntityFoundException。所以这里 MarkerEntity 标记接口定义了一个类型,它为实现它的类添加了特殊行为。

尽管现在也可以使用注释来标记类以进行某些特殊处理,但标记注释是命名模式的替代品,而不是标记接口。

但是标记注释不能完全取代标记接口,因为:标记接口用于定义类型(如上所述),而标记注释则没有。

标记界面注释的来源

于 2015-08-05T16:43:55.227 回答
1

我首先会争辩说 Serializable 和 Cloneable 是标记接口的坏例子。当然,它们是方法的接口,但它们暗示方法,例如writeObject(ObjectOutputStream). (writeObject(ObjectOutputStream)如果您不覆盖它,编译器将为您创建一个方法,并且所有对象都已经拥有clone(),但编译器将再次clone()为您创建一个真正的方法,但需要注意的是。这两种情况都是奇怪的边缘情况,实际上不是好的设计示例。)

标记接口通常用于以下两个目的之一:

1)作为避免过长类型的捷径,这可能发生在许多泛型中。例如,假设你有这个方法签名:

public void doSomething(Foobar<String, Map<String, SomethingElse<Integer, Long>>>) { ... }

打字很混乱,很烦人,更重要的是,很难理解。考虑一下:

public interface Widget extends Foobar<String, Map<String, SomethingElse<Integer, Long>>> { }

然后您的方法如下所示:

public void doSomething(Widget widget) { ... }

它不仅更清晰,而且您现在可以 Javadoc Widget 接口,并且更容易搜索您的 Widget 代码中的所有出现。

2) 标记接口也可以用来解决 Java 缺乏交集类型的问题。使用标记接口,您可以要求某些内容具有两种不同的类型,例如在方法签名中。假设您的应用程序中有一些界面小部件,就像我们上面描述的那样。如果您有一个需要一个小部件的方法,该小部件也恰好让您对其进行迭代(它是人为的,但在这里与我合作),您唯一的好解决方案是创建一个扩展两个接口的标记接口:

public interface IterableWidget extends Iterable<String>, Widget { }

在您的代码中:

public void doSomething(IterableWidget widget) {
    for (String s : widget) { ... }
}
于 2014-09-15T17:47:14.673 回答
0

如果接口不包含任何方法,并且通过实现该接口,如果我们的对象将获得某种能力,则此类接口称为标记接口。

于 2016-05-30T19:30:51.693 回答