【问题标题】:How do I identify immutable objects in Java如何在 Java 中识别不可变对象
【发布时间】:2010-09-17 05:42:10
【问题描述】:

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

我可以做的一件事是检查一些众所周知的不可变类型:

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; }
}

是否有某种方法(可能使用反射)可以可靠地检测一个类是否是不可变的?假阳性(认为它不可变时认为它是不可变的)是不可接受的,但假阴性(认为它不可变时认为它是可变的)是不可接受的。

编辑添加:感谢您提供富有洞察力和有用的答案。正如一些答案所指出的,我忽略了定义我的安全目标。这里的威胁是无知的开发人员——这是一段框架代码,将被大量对线程一无所知并且不会阅读文档的人使用。我不需要防御恶意开发者——任何足够聪明到mutate a String 或执行其他恶作剧的人也足够聪明,知道在这种情况下它是不安全的。代码库的静态分析是一种选择,只要它是自动化的,但不能指望代码审查,因为不能保证每个审查都会有精通线程的审查者。

【问题讨论】:

    标签: java functional-programming immutability


    【解决方案1】:

    基本上没有。

    你可以建立一个巨大的接受类的白名单,但我认为不那么疯狂的方法是在集合的文档中写下这个集合的所有内容必须是不可变的。

    编辑:其他人建议使用不可变注释。这很好,但您也需要文档。否则人们只会想“如果我把这个注释放在我的类上,我可以把它存储在集合中”,然后就会把它扔到任何东西上,不管是不可变类还是可变类。事实上,我会警惕使用不可变注释,以防人们认为注释使他们的类不可变。

    【讨论】:

    • 另外,在 JCIP 书中讨论了像 java.util.Date 这样有效的不可变类,即使它们不是,它们实际上也被用作不可变的
    【解决方案2】:

    在我的公司,我们定义了一个名为@Immutable 的属性。如果你选择将它附加到一个类,这意味着你保证你是不可变的。

    它适用于文档,在您的情况下,它可以用作过滤器。

    当然,您仍然依赖于作者信守不可变的诺言,但由于作者明确添加了注释,这是一个合理的假设。

    【讨论】:

    • 如果你依赖作者信守诺言,它是不可变的,那有什么意义呢?它没有添加任何东西,如果人们认为注释使他们的类不可变,它可能会造成伤害..
    • 这个想法是希望在不久的将来你会有一些静态代码分析 - 可能会集成到你的 IDE 中 - 来检查你的代码并检查你的 @Immutable 注释类是 i> 实际上是不可变的。
    • 在 Jason 的公司(我也在该公司工作),我们还对编写的每一行代码进行代码审查。所以,如果你把@Immutable 属性放在一个不是不可变的类上,你就会被调用。问杰森,我已经做过好几次了。
    • 顺便说一下,@Immutable 属性的想法并非原创,它来自 Brian Goetz 的 Java Concurrency In Practice。
    • 另外,@SCdF,你总是依赖作者来判断一个类在 Java 中是否不可变,即使这样,也有改变任何成员变量的方法。注释可能是您可能称之为最差的最好的。
    【解决方案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; }
    }
    

    然后通过在类上使用反射,检查它是否有注释(我会粘贴代码,但它的样板可以在网上轻松找到)

    【讨论】:

    • 你会如何阻止人们只将注释放在非不可变类上?
    • 你没有阻止他们。如果您不能按照说明使用某物,则不应尝试对其进行编程。
    • @Simon,好的,那么注释的意义何在?难道你不能假设他们足够聪明,可以阅读关于集合的文档,从而知道它只适用于不可变的东西吗?
    • @SCdF,我尝试提供一种具有成本效益的方法,也许他可以使它起作用。说“不,你不能”就等于什么都不说(或更糟)
    • 现在我已经考虑了更多,我不得不部分同意,注释可能会导致混乱和误解。
    【解决方案4】:

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

    接近这一点的唯一方法是:

    • 只允许不可变类型的最终属性(您知道是不可变的原始类型和类),
    • 要求类本身是最终的
    • 要求它们从您提供的基类继承(保证不可变)

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

    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);
    }
    

    更新:正如 cmets 中所建议的,它可以扩展为在超类上递归,而不是检查某个类。还建议在 isValidFieldType 方法中递归使用 isImmutable。这可能可行,我也做了一些测试。但这不是微不足道的。您不能只通过调用 isImmutable 来检查所有字段类型,因为 String 已经未通过此测试(其字段 hash 不是最终的!)。此外,您很容易遇到无休止的递归,导致 StackOverflowErrors ;) 其他问题可能是由泛型引起的,您还必须检查它们的类型是否不可变。

    我认为通过一些工作,这些潜在的问题可能会以某种方式得到解决。但是,你必须先问问自己这是否真的值得(也是性能方面的)。

    【讨论】:

    • 也许“isValidFieldType”可以递归到 isImmutable。 :-)
    • 经过 marcospereira 的修改,这可能非常有用!我们还可以在超类上递归(允许 Object 作为特例),这将消除从共同祖先继承的需要。这很好!
    • 这些都是很好的建议。为了简单和性能,我试图避免在超类和成员上递归(递归可能会变得相当广泛)。但我会将这些建议添加到 anweser 中。
    • 如果您正在检查特定实例的类,恕我直言,它不需要是最终的 - 如果另一个实例是子类,无论如何都会检查它。
    • “没有可靠的方法来检测一个类是否不可变。” - 不是这种情况。也许你的意思是只使用反射?但是使用 BCEL 或 ASM 等库的字节码分析将允许发现一个类是否是可变的。事实上,作为大学作业的一部分,我刚刚完成了其中的 95%。
    【解决方案5】:

    这可能是另一个提示:

    如果类没有设置器,那么它不能被改变,授予它创建的参数要么是“原始”类型,要么本身不可变。

    也不能覆盖任何方法,所有字段都是最终的和私有的,

    我明天会尝试为您编写一些代码,但是 Simon 使用反射的代码看起来不错。

    同时尝试获取 Josh Block 的“Effective Java”一书,它有一个与该主题相关的项目。虽然 is 没有明确说明如何检测不可变类,但它展示了如何创建一个好的类。

    该项目被称为:“支持不变性”

    链接: http://java.sun.com/docs/books/effective/

    【讨论】:

      【解决方案6】:

      就像其他回答者已经说过的那样,恕我直言,没有可靠的方法来确定对象是否真的不可变。

      我只想引入一个接口“不可变”来在追加时进行检查。这作为一个提示,无论出于何种原因,都应该只插入不可变对象。

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

      【讨论】:

        【解决方案7】:

        在我的代码中,我正在创建一个对象集合,这些对象将被各种线程以一种仅在对象不可变时才安全的方式访问。

        不是您问题的直接答案,但请记住,不可变的对象不能自动保证是线程安全的(遗憾的是)。代码需要无副作用才能保证线程安全,而这要困难得多。

        假设你有这个类:

        class Foo {
          final String x;
          final Integer y;
          ...
        
          public bar() {
            Singleton.getInstance().foolAround();
          }
        }
        

        那么foolAround() 方法可能包含一些非线程安全的操作,这会炸毁你的应用程序。并且无法使用反射对此进行测试,因为实际引用只能在方法体中找到,而不是在字段或暴露的接口中。

        除此之外,其他的都是正确的:您可以扫描类的所有声明字段,检查它们中的每一个是否都是最终的并且也是不可变的类,然后就完成了。我不认为方法是最终的。

        另外,在递归检查依赖字段的不变性时要小心,你可能会得到圆圈:

        class A {
          final B b; // might be immutable...
        }
        
        class B {
          final A a; // same so here.
        }
        

        A 类和 B 类是完全不可变的(甚至可能通过一些反射黑客来使用),但幼稚的递归代码将进入一个无限循环,检查 A,然后是 B,然后再检查 A,然后再检查 B,...

        您可以使用不允许循环的“已看到”映射来解决此问题,或者使用一些非常聪明的代码来确定类是不可变的,如果它们的所有依赖项仅取决于它们自己是不可变的,但这将非常复杂...

        【讨论】:

        • 这些都是非常好的警告,我都没有真正考虑过。我怀疑我是否需要防御递归包含的不可变对象(最简单的情况是在字段中包含自身的不可变对象!),但另一种情况有点危险。
        【解决方案8】:

        使用来自Java Concurrency in PracticeImmutable 注释。然后,FindBugs 工具可以帮助检测可变但不应该可变的类。

        【讨论】:

        • 在运行代码时应该如何应用 FindBugs?据我了解,他想在运行时检查是否添加了可变对象?
        • 他可以在构建时使用 Findbugs 来减少错误注释类的数量并在运行时使用注释。
        • 我没有意识到 Findbugs 做了这个分析——太棒了!我查看了文档,似乎它只是验证了带注释的类的所有字段都是最终的(但没有验证它们都包含不可变对象)。
        • 您还可以启用 IntelliJ 以根据这些注释检查您的代码:Tweet from Brian Goetz。以下是作为 Maven 依赖项的注释:link
        【解决方案9】:

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

        【讨论】:

        • 此外,将 class 设为 final absolutely 与它的不变性无关。这只是意味着它不能扩展。
        • 因为从非最终不可变类派生的类可以添加可变属性。
        • 但是你确实知道执行时的真实类型。
        【解决方案10】:

        查看joe-e,Java 功能的实现。

        【讨论】:

          【解决方案11】:

          适用于大部分内置类的方法是 test for instanceof Comparable。对于像 Date 这样不可变的类,在大多数情况下,它们通常被视为不可变的。

          【讨论】:

            【解决方案12】:

            试试这个:

            public static boolean isImmutable(Object object){
                if (object instanceof Number) { // Numbers are immutable
                    if (object instanceof AtomicInteger) {
                        // AtomicIntegers are mutable
                    } else if (object instanceof AtomicLong) {
                        // AtomLongs are mutable
                    } else {
                        return true;
                    }
                } else if (object instanceof String) {  // Strings are immutable
                    return true;
                } else if (object instanceof Character) {   // Characters are immutable
                    return true;
                } else if (object instanceof Class) { // Classes are immutable
                    return true;
                }
            
                Class<?> objClass = object.getClass();
            
                // 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())
                                        || !isImmutable(objFields[i].getType())) {
                                return false;
                        }
                }
            
                // Lets hope we didn't forget something
                return true;
            }
            

            【讨论】:

            • 当对象类有循环时不起作用(例如class MyNode { MyNode parent; public MyNode() {} public MyNode(MyNode parent) { this.parent = parent; } }):你会得到一个StackOverflowError
            • 这不适用于所有课程。例如,BigInteger 扩展了Number,javadoc 声明它是一个不可变类,但BigInteger 类及其方法都不是final,因此可以覆盖该类并且子类可以更改@ 987654328@类行为。
            【解决方案13】:

            你可以使用来自jcabi-aspects的AOP和@Immutable注解:

            @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();
            

            【讨论】:

              【解决方案14】:

              据我所知,没有办法识别 100% 正确的不可变对象。但是,我写了一个库来让你更接近。它对一个类的字节码进行分析以确定它是否是不可变的,并且可以在运行时执行。它是严格的,因此它还允许将已知的不可变类列入白名单。

              您可以查看:www.mutabilitydetector.org

              它允许您在应用程序中编写这样的代码:

              /*
              * Request an analysis of the runtime class, to discover if this
              * instance will be immutable or not.
              */
              AnalysisResult result = analysisSession.resultFor(dottedClassName);
              
              if (result.isImmutable.equals(IMMUTABLE)) {
                  /*
                  * rest safe in the knowledge the class is
                  * immutable, share across threads with joyful abandon
                  */
              } else if (result.isImmutable.equals(NOT_IMMUTABLE)) {
                  /*
                  * be careful here: make defensive copies,
                  * don't publish the reference,
                  * read Java Concurrency In Practice right away!
                  */
              }
              

              它在 Apache 2.0 许可下是免费和开源的。

              【讨论】:

              • 不适用于 Java 11 和 List 用 List.copyOf() 包裹的字段,这使得它不可变。如果没有包含在 Collections.unmodifiableList() 中,则 Mutabilitydetector 确实会失败
              • 我需要为此提出一个问题。识别新的 Java 11 不可变集合构造函数应该不难。
              【解决方案15】:

              我很欣赏和钦佩 Grundlefleck 为他的可变性检测器所做的大量工作,但我认为这有点矫枉过正。您可以编写一个简单但实​​际上非常合适(即 pragmatic)的检测器,如下所示:

              (注意:这是我的评论的副本:https://stackoverflow.com/a/28111150/773113

              首先,您不会只是编写一个确定类是否不可变的方法。相反,您将需要编写一个不变性检测器类,因为它必须保持某种状态。检测器的状态将是它迄今为止检查过的所有类的检测到的不变性。这不仅对性能有用,而且实际上是必要的,因为一个类可能包含循环引用,这会导致简单的不变性检测器陷入无限递归。

              类的不变性有四个可能的值:UnknownMutableImmutableCalculating。您可能希望有一个映射,它将您迄今为止遇到的每个类与一个不变性值相关联。当然,Unknown 实际上并不需要实现,因为它将是尚未在映射中的任何类的隐含状态。

              因此,当您开始检查一个类时,将其与映射中的 Calculating 值相关联,完成后,将 Calculating 替换为 ImmutableMutable

              对于每个类,您只需要检查字段成员,而不需要检查代码。检查字节码的想法是相当错误的。

              首先,你应该检查一个类是否是final的;类的最终确定性不会影响其不变性。相反,一个期望一个不可变参数的方法应该首先调用不可变检测器来断言传递的实际对象的类的不变性。如果参数的类型是 final 类,这个测试可以省略,所以 finality 对性能有好处,但严格来说没有必要。此外,正如您将在下面看到的那样,类型为非最终类的字段将导致声明类被认为是可变的,但这仍然是声明类的问题,而不是非最终类的问题不可变成员类。拥有不可变类的高层次结构是非常好的,其中所有非叶节点当然必须是非最终的。

              你应该检查一个字段是否是私有的;一个类拥有一个公共字段是完全可以的,并且该字段的可见性不会以任何方式、形状或形式影响声明类的不变性。您只需要检查该字段是否为final,其类型是否不可变。

              在检查一个类时,您首先要做的是递归确定其super 类的不变性。如果 super 是可变的,那么根据定义,后代也是可变的。

              那么,您只需要检查类的已声明字段,而不是所有字段。

              如果一个字段不是最终的,那么你的类是可变的。

              如果一个字段是最终的,但该字段的类型是可变的,那么你的类是可变的。 (数组根据定义是可变的。)

              如果一个字段是final,并且该字段的类型是Calculating,则忽略它并继续下一个字段。如果所有字段都是不可变的或Calculating,那么您的类是不可变的。

              如果字段的类型是接口、抽象类或非最终类,那么它被认为是可变的,因为您完全无法控制实际实现可能会做什么。这似乎是一个无法克服的问题,因为这意味着将可修改集合包装在 UnmodifiableCollection 中仍然无法通过不变性测试,但实际上没问题,可以通过以下解决方法来处理。

              某些类可能包含非最终字段,但仍然实际上是不可变的String 类就是一个例子。其他属于这一类的类是包含非最终成员纯粹用于性能监控目的的类(调用计数器等),实现 popsicle immutability 的类(查找),以及包含成员是已知不会引起任何副作用的接口。此外,如果一个类包含真正的可变字段但承诺在计算 hashCode() 和 equals() 时不考虑它们,那么该类在多线程方面当然是不安全的,但它仍然可以被视为为了将其用作地图中的键,它是不可变的。因此,所有这些情况都可以通过以下两种方式之一来处理:

              1. 手动将类(和接口)添加到不变性检测器。如果您知道某个类实际上是不可变的,尽管它的不变性测试失败了,您可以手动向检测器添加一个条目,将其与Immutable 关联。这样,检测器将永远不会尝试检查它是否是不可变的,它只会说“是的,它是”。

              2. 引入@ImmutabilityOverride 注释。您的不变性检测器可以检查字段上是否存在此注释,如果存在,它可能会将字段视为不可变,尽管该字段可能是非最终的或其类型可能是可变的。检测器还可以检查类上是否存在此注释,从而将类视为不可变的,甚至无需检查其字段。

              我希望这对后代有所帮助。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2014-08-24
                • 1970-01-01
                • 2021-05-04
                • 1970-01-01
                • 1970-01-01
                • 2014-09-03
                • 1970-01-01
                • 2012-05-18
                相关资源
                最近更新 更多