如何在Java中识别不可变对象

47

在我的代码中,我正在创建一个对象集合,这些对象将被各种线程以只有当对象不可变时才安全的方式访问。当尝试将新对象插入到我的集合中时,我想测试一下它是否是不可变的(如果不是,我会抛出异常)。

我能做的一件事是检查几种众所周知的不可变类型:

private static final Set<Class> knownImmutables = new HashSet<Class>(Arrays.asList(
        String.class, Byte.class, Short.class, Integer.class, Long.class,
        Float.class, Double.class, Boolean.class, BigInteger.class, BigDecimal.class
));

...

public static boolean isImmutable(Object o) {
    return knownImmutables.contains(o.getClass());
}

这实际上让我完成了90%的工作,但有时我的用户希望创建自己的简单不可变类型:

public class ImmutableRectangle {
    private final int width;
    private final int height;
    public ImmutableRectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
}

有没有某种方法(也许使用反射),我能够可靠地检测一个类是否是不可变的?误报(认为它是不可变的,但实际上不是)是不可接受的,但漏报(认为它是可变的,但实际上不是)是可以的。

编辑补充:感谢提供深入见解和有用答案。正如一些答案所指出的那样,我忽略了定义我的安全目标。这里的威胁是无知的开发者-这是一段将被大量人使用的框架代码,他们对线程几乎一无所知,并且不会阅读文档。我不需要防范恶意开发者-任何聪明到足以 更改字符串 或进行其他花招的人也会聪明到知道在这种情况下不安全。对代码库进行静态分析是一种选择,只要自动化就可以,但不能依赖代码审查,因为不能保证每个审查人员都具备线程方面的知识。


有可靠的方法可以在Java中检测不可变性。然而,如何解决这个问题是一个庞大的主题。我已经回答了另一个非常相似的问题,解释了如何全面地解决这个问题空间。https://dev59.com/IloU5IYBdhLWcg3wzZLw#75043881 - chaotic3quilibrium
在Java中有可靠的方法来检测不可变性。不过,如何去解决这个问题是一个很大的话题。我已经回答了另一个非常相似的问题,解释了你可能如何全面地解决这个问题。https://stackoverflow.com/a/75043881/501113 - undefined
14个回答

32

他应该如何在运行代码时应用FindBugs?据我所知,他想要在运行时检查是否添加了可变对象? - Daniel Hiller
他可以在构建时使用Findbugs来减少错误注释类的数量,并在运行时使用注释。 - ddimitrov
6
我之前没有意识到Findbugs可以进行这种分析,太好了!我查看了文档,它似乎只是验证注释类的所有字段是否为final(但不验证它们是否都包含不可变对象)。 - mcherm
你也可以启用IntelliJ来检查你的代码是否符合这些注释:Brian Goetz的推文。这里是作为Maven依赖项的注释:链接 - Matthias Braun

32

没有可靠的方法来检测一个类是否是不可变的。这是因为有很多种方式可以改变一个类的属性,而你无法通过反射来检测所有这些方式。

唯一接近这个目标的方法是:

  • 只允许使用不可变类型的 final 属性(原始类型和你知道是不可变的类),
  • 要求该类本身是 final 的
  • 要求它们继承自你提供的基类(保证是不可变的)

然后,你可以使用以下代码来检查你拥有的对象是否是不可变的:

static boolean isImmutable(Object obj) {
    Class<?> objClass = obj.getClass();

    // Class of the object must be a direct child class of the required class
    Class<?> superClass = objClass.getSuperclass();
    if (!Immutable.class.equals(superClass)) {
        return false;
    }

    // Class must be final
    if (!Modifier.isFinal(objClass.getModifiers())) {
        return false;
    }

    // Check all fields defined in the class for type and if they are final
    Field[] objFields = objClass.getDeclaredFields();
    for (int i = 0; i < objFields.length; i++) {
        if (!Modifier.isFinal(objFields[i].getModifiers())
                || !isValidFieldType(objFields[i].getType())) {
            return false;
        }
    }

    // Lets hope we didn't forget something
    return true;
}

static boolean isValidFieldType(Class<?> type) {
    // Check for all allowed property types...
    return type.isPrimitive() || String.class.equals(type);
}

更新:根据评论中的建议,可以扩展到对超类进行递归,而不是检查特定类。还建议在isValidFieldType方法中递归使用isImmutable。这可能有效,我也进行了一些测试。但这并不是轻松的事情。您不能仅通过调用isImmutable来检查所有字段类型,因为String已经无法通过此测试(它的字段hash不是final!)。此外,您很容易遇到无限递归,导致StackOverflowErrors ;) 其他问题可能由泛型引起,您还必须检查它们的类型是否为不可变。

我认为通过一些工作,这些潜在问题可能会得到解决。但是,首先您必须问自己是否真的值得这样做(从性能方面考虑)。


也许“isValidFieldType”可以递归到isImmutable。 :-) - marcospereira
这些是很好的建议。为了简单和效率的缘故,我试图避免对超类和成员进行递归(递归可能会变得非常复杂)。但我会把这些建议添加到答案中。 - Simon Lehmann
检测一个类是否是不可变的没有可靠的方法。这并非事实。也许你指的是只使用反射?但是使用像BCEL或ASM这样的库进行字节码分析可以发现一个类是否是可变的。事实上,我刚刚作为大学任务的一部分完成了其中95%的工作。 - Grundlefleck
@Grundlefleck:您能解释一下如何使用字节码分析来实现吗?这在运行时可以轻松完成吗?我觉得这非常有趣,可能会帮助其他想要在未来做此类工作的人。 - Simon Lehmann
为什么认为将属性设置为原始类型有助于不可变性?我可以使用反射来修改它。 - plzdontkillme
显示剩余4条评论

9
在我们的公司中,我们定义了一个名为@Immutable的特性。如果您选择将其附加到一个类上,那么意味着您承诺该类是不可变的。 这对于文档非常有效,在您的情况下,它可以作为一种过滤器使用。 当然,您仍然依赖于作者遵守其关于不可变性的承诺,但由于作者明确添加了注释,因此这是一个合理的假设。

如果你依赖于作者信守承诺,认为它是不可变的,那还有什么意义呢?这并没有增加任何东西,如果人们认为注释使他们的类不可变,那么它可能会造成伤害。 - SCdF
希望不久的将来,您会有一些静态代码分析工具 - 可能集成到您的IDE中 - 来检查您的@Immutable注释类是否真的是不可变的。 - Cem Catikkas
在Jason的公司(我也在那里工作),我们还会对编写的每一行代码进行代码审查。因此,如果你在一个非不可变类上放置@Immutable属性,你会被质疑的。问问Jason吧,我已经做过几次了。 - Brandon DuRette
2
顺便提一下,@Immutable 属性的想法并不是原创的,它来自于 Brian Goetz 的《Java Concurrency In Practice》。 - Brandon DuRette
1
此外,@SCdF,你总是依赖于Java中的作者来确定一个类是否是不可变的,即使在这种情况下,也有改变任何成员变量的方法。注释可能是你所谓的最糟糕的最佳选择。 - Henry B
显示剩余4条评论

8
基本上不行。
你可以建立一个巨大的白名单来接受类,但我认为较少疯狂的方法是在集合的文档中写入所有必须是不可变的内容。
编辑:其他人建议使用不可变注释。这很好,但你也需要文档。否则,人们会认为“如果我将此注释放在我的类上,我就可以将其存储在集合中”,并且会将其放在不可变和可变类上。实际上,我会对具有不可变注释的情况保持谨慎,以防人们认为该注释使他们的类不可变。

1
此外,在JCIP书中还讨论了像java.util.Date这样的有效不可变类,实际上即使它们不是不可变的,也会被用作不可变的。 - ddimitrov

4
这可能是另一个提示:
如果类没有setter,则它不能被改变,前提是它创建时的参数要么是“原始”类型,要么本身不可变。
此外,没有方法可以被覆盖,所有字段都是final和private的。
我明天会为你编写一些代码,但Simon使用反射的代码看起来很不错。
与此同时,请尝试获取Josh Block的《Effective Java》一书的副本,它有一个与此主题相关的条目。虽然它并没有确切地说明如何检测一个不可变类,但它展示了如何创建一个好的类。
该条目称为:“倾向于不可变性”
更新链接:https://www.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997

1
书籍链接已失效。这是最新版本的书籍(我强烈推荐,并且已经阅读了很多遍):https://smile.amazon.com/Effective-Java-Joshua-Bloch/dp/0134685997 - chaotic3quilibrium

4
在我的代码中,我正在创建一个对象集合,这些对象将以一种只有在这些对象是不可变的情况下才安全访问的方式被各个线程所访问。
这并不是你问题的直接答案,但请记住,不可变的对象并不能自动保证线程安全(遗憾的是)。代码需要无副作用才能保证线程安全,这需要更加困难。
假设你有这个类:
class Foo {
  final String x;
  final Integer y;
  ...

  public bar() {
    Singleton.getInstance().foolAround();
  }
}

那么foolAround()方法可能包含一些非线程安全操作,这将导致您的应用程序崩溃。使用反射无法测试此问题,因为实际引用仅在方法体中找到,而不在字段或公开接口中。

除此之外,其他方面都是正确的:您可以扫描类的所有声明字段,检查它们是否都是final和不可变类,然后就完成了。我认为方法被final并不是必需的。

此外,请注意递归检查依赖字段的不可变性,否则可能会出现循环:

class A {
  final B b; // might be immutable...
}

class B {
  final A a; // same so here.
}

类A和B是完全不可变的(甚至可能通过一些反射技巧使用),但是天真递归代码将进入无限循环,检查A,然后B,再到A,以此类推到B...

您可以通过使用“已见”映射禁止循环,或者使用一些非常聪明的代码来解决这个问题,该代码决定如果所有依赖项仅依赖于自身,则认为类是不可变的,但这将非常复杂...


这些都是非常好的警告,我之前都没有真正考虑过。我怀疑我是否需要防范递归包含的不可变对象(最简单的情况就是一个包含自身字段的不可变对象!),但另一个确实有一定的危险性。 - mcherm

3

为什么所有的建议都要求类是final的?如果您使用反射来检查每个对象的类,并且可以编程确定该类是不可变的(不可变,有final字段),那么您就不需要要求该类本身是final。


2
此外,将class设置为final与其不可变性完全没有关系。这只意味着它无法被扩展。 - bendin
因为从非最终不可变类派生的类可能会添加可变属性。 - Ingo
2
但是您在执行时确实知道真正的类型。 - Nicolas Bousquet

3
您可以要求客户添加元数据(注释),并使用反射在运行时进行检查,如下所示:
元数据:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CLASS)
public @interface Immutable{ }

客户端代码:

@Immutable
public class ImmutableRectangle {
    private final int width;
    private final int height;
    public ImmutableRectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
}

然后通过对类使用反射,检查它是否具有注释(我会粘贴代码,但它是样板文件,可以很容易地在网上找到)


你如何阻止人们将该注解放在非不可变类上? - SCdF
你不能阻止他们。如果你无法遵循如何使用某个东西的说明,那么你不应该尝试编程它。 - Simon Lehmann
@Simon,好的,那么注释的意义是什么呢?难道你不能假设他们足够聪明,能够阅读有关集合的文档,并知道它只适用于不可变的事物吗? - SCdF
@SCdF,我尝试提供一种经济实惠的方法,也许他可以使其发挥作用。说“不,你不能”和什么都不说(或最糟糕的是)一样。 - Pablo Fernandez
现在我想了一下,我必须部分同意,拥有注释可能会导致混淆和误解。 - Simon Lehmann
我担心我们的一些开发人员可能无法完全理解这个概念并误用注释。否则,这将是一个绝佳的解决方案。结合验证,它可能会完美地运行。 - mcherm

3

您可以使用AOP和@Immutable注释来自jcabi-aspects

@Immutable
public class Foo {
  private String data;
}
// this line will throw a runtime exception since class Foo
// is actually mutable, despite the annotation
Object object = new Foo();

2

就像其他回答者已经说过的,我认为没有可靠的方法来确定一个对象是否真的是不可变的。

我会引入一个名为“Immutable”的接口,在添加时进行检查。这作为一个提示,只有不可变对象才能被插入,无论你为什么要这样做。

interface Immutable {}

class MyImmutable implements Immutable{...}

public void add(Object o) {
  if (!(o instanceof Immutable) && !checkIsImmutableBasePrimitive(o))
    throw new IllegalArgumentException("o is not immutable!");
  ...
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接