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

时间:2008-10-15 01:55:01

标签: java functional-programming immutability

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

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

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或执行其他恶作剧的人都会足够聪明,知道在这种情况下它不安全。代码库的静态分析是一种选择,只要它是自动化的,但代码审查不能被依赖,因为不能保证每次审查都会有精通线程的审阅者。

15 个答案:

答案 0 :(得分:29)

没有可靠的方法来检测类是否是不可变的。这是因为有很多方法可以改变类的属性,并且无法通过反射检测所有属性。

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

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

然后,如果您拥有的对象是不可变的,则可以使用以下代码进行检查:

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不是最终的!)。此外,您很容易遇到无休止的递归,导致 StackOverflowErrors ;)其他问题可能是由泛型引起的,您还必须检查其类型的不可变性。

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

答案 1 :(得分:29)

使用Immutable中的Java Concurrency in Practice注释。然后,工具FindBugs可以帮助检测可变但不应该是的类。

答案 2 :(得分:9)

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

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

当然,你仍然依赖于作者保持他的不可变性,但是由于作者明确添加了注释,这是一个合理的假设。

答案 3 :(得分:8)

基本上没有。

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

编辑:其他人建议使用不可变注释。这很好,但您也需要文档。否则人们会认为“如果我把这个注释放在我的课堂上,我可以将它存储在集合中”,并且只会把它放在任何东西,不可变和可变的类上。事实上,我会担心有一个不可变的注释,以防人们认为注释使他们的类不可变。

答案 4 :(得分:4)

这可能是另一个提示:

如果该类没有setter,则它不能被变异,授予它所创建的参数是“原始”类型或者本身不可变。

也没有方法可以覆盖,所有字段都是final和private,

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

同时尝试获取Josh Block的“Effective Java”一书的副本,它有一个与此主题相关的项目。虽然不能确定如何检测一个不可改变的类,但它显示了如何创建一个好的类。

该项目被称为:“赞成不变性”

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

答案 5 :(得分:4)

  

在我的代码中,我正在创建一个对象集合,这些对象将以各种线程的方式访问,只有在对象是不可变的情况下才是安全的。

不能直接回答您的问题,但请记住,不可变的对象不会自动保证是线程安全的(遗憾的是)。代码需要是副作用免费的线程安全,这是非常困难的。

假设你有这个课程:

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,...,

你可以用一个不允许循环的“看到”地图来修复它,或者用一些非常聪明的代码来决定类是不可变的,如果它们的所有依赖都是不可变的,只取决于它们自己,但那将会非常复杂...... / p>

答案 6 :(得分: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; }
}

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

答案 7 :(得分:3)

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

答案 8 :(得分:3)

您可以使用@Immutable中的AOP和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();

答案 9 :(得分: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!");
  ...
}

答案 10 :(得分:1)

试试这个:

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

答案 11 :(得分:1)

据我所知,没有办法识别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许可下的免费开源。

答案 12 :(得分:0)

查看joe-e,这是java的功能实现。

答案 13 :(得分:0)

适用于高比例内置类的东西是testof instanceof Comparable。对于像Date一样不可变的类,在大多数情况下它们通常被视为不可变的。

答案 14 :(得分:0)

我很欣赏和欣赏Grundlefleck在他的可变性检测器中所做的工作量,但我认为这有点过分。您可以编写一个简单但实​​际上非常充足的(即实用)检测器,如下所示:

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

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

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

因此,当您开始检查某个类时,会将其与地图中的Calculating值相关联,完成后,将Calculating替换为Immutable或{{ 1}}。

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

首先,你应该检查课程是否是最终的;一个阶级的终结性不会影响其不变性。相反,一个期望不可变参数的方法首先应该调用不变性检测器来断言传递的实际对象的类的不变性。如果参数的类型是最终类,则可以省略该测试,因此最终性有利于性能,但严格来说不是必需的。此外,正如您将进一步了解的那样,一个类型为非final类的字段将导致声明类被认为是可变的,但仍然是,这是一个声明类的问题,而不是问题。非最终不可变成员类。拥有一个不可变类的高层次结构是完全没有问题的,其中所有非叶子节点当然必须是非最终的。

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

在检查课程时,首先要做的是递归以确定其Mutable课程的不变性。如果super是可变的,那么后代也是可变的。

然后,您只需要检查该类的声明的字段,而不是所有字段。

如果某个字段是非最终字段,则您的类是可变的。

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

如果某个字段是最终字段,并且该字段的类型为super,则忽略该字段并继续执行下一个字段。如果所有字段都是不可变的或Calculating,那么您的类是不可变的。

如果字段的类型是接口,抽象类或非最终类,那么它将被视为可变,因为您完全无法控制实际实现可能执行的操作。这似乎是一个不可逾越的问题,因为这意味着将一个可修改的集合包装在Calculating内仍然会使不变性测试失败,但它实际上很好,并且可以通过以下解决方法来处理。

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

  1. 手动将类(和接口)添加到不变性检测器。如果您知道某个类实际上是不可变的,尽管它的不变性测试失败,您可以手动向检测器添加一个条目,将其与String相关联。通过这种方式,探测器永远不会尝试检查它是否是不可变的,它总是只是说'是的,它是。&#39;

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

  3. 我希望这有助于后代。