45

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

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

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

有什么方法(可能使用反射)可以可靠地检测一个类是否是不可变的?误报(认为它不可变时认为它是不可变的)是不可接受的,但误报(认为它不可变时认为它是可变的)是不可接受的。

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

4

15 回答 15

30

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

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

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

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

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 ;) 其他问题可能是由泛型引起的,您还必须检查它们的类型是否不可变。

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

于 2008-10-15T02:52:21.130 回答
30

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

于 2008-10-15T08:24:14.203 回答
9

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

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

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

于 2008-10-15T02:14:18.087 回答
8

基本上没有。

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

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

于 2008-10-15T02:12:03.893 回答
4

这可能是另一个提示:

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

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

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

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

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

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

于 2008-10-15T03:34:50.043 回答
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,...

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

于 2008-10-15T07:43:18.843 回答
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; }
}

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

于 2008-10-15T02:15:22.840 回答
3

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

于 2008-10-15T16:31:38.650 回答
3

您可以使用来自jcabi-aspects 的@ImmutableAOP 和注释:

@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();
于 2013-02-18T07:02:28.763 回答
2

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

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

interface Immutable {}

class MyImmutable implements Immutable{...}

public void add(Object o) {
  if (!(o instanceof Immutable) && !checkIsImmutableBasePrimitive(o))
    throw new IllegalArgumentException("o is not immutable!");
  ...
}
于 2008-10-15T05:25:43.473 回答
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;
}
于 2011-07-27T11:08:57.347 回答
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 许可下是免费和开源的。

于 2014-05-30T13:01:14.237 回答
0

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

于 2009-03-04T16:27:46.930 回答
0

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

于 2009-03-04T19:21:59.337 回答
0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

于 2015-01-23T14:35:23.327 回答