2013-10-24 24 views
0

我正在实现有理数的类,但问题和问题对于复数以及打算用于具有相当数量的应用程序的其他类基本相同在给定的数学对象上执行的计算。在Java中实现数字系统:可变与不可变

在与JRE一起发布的库以及许多第三方库中,数字类是不可变的。这具有“等于”和“散列码”可以按照预期一起可靠地实现的优点。这将使实例可以用作各种集合中的键和值。事实上,实例在整个生命周期中作为集合中的关键值的不变性必须保持在集合上的可靠操作。如果类一旦创建实例时阻止可能会改变哈希码方法依赖的内部状态的操作,而不是由开发人员和代码的后续维护者遵守删除约定在修改其状态之前从集合中获取实例,然后将实例添加回它们必须属于的集合中。然而,如果类设计强制 - 在语言的限制范围内 - 不变性,数学表达式在执行简单的数学运算时就会承担过多的对象分配和随后的垃圾回收。考虑以下作为什么在复杂的计算重复出现的明确示例:

Rational result = new Rational(13L, 989L).divide(new Rational(-250L, 768L)); 

表达包括三个分配 - 其中两个是很快丢弃。为了避免一些开销,类通常预分配常用的“常量”,甚至可以维护一个经常使用的“数字”的散列表。当然,这样的哈希表可能不如简单地分配所有必需的不可变对象,并且依赖于Java编译器和JVM尽可能高效地管理堆,而不仅仅是性能。

另一种方法是创建支持可变实例的类。通过以流畅样式实现类的方法,可以评估在功能上类似于上述的简洁表达式,而不将从“分割”方法返回的第三个对象分配为“结果”。再次,这对于这个表达式并不特别重要。然而,通过对矩阵进行操作来解决复杂的线性代数问题,对于更好地作为可变对象进行处理的数学对象而言,是一种更现实的情况,而不必操作不可变实例。对于有理数的矩阵,一个可变的有理数类似乎更容易被证明。

有了这一切,我有两个相关的问题:

  1. 是否有有关Sun/Oracle的Java编译器,JIT,或JVM这将决定性地推荐了可变类的不可变的理性或复数类东西吗?

  2. 如果不是,在实现可变类时应如何处理“hashcode”?我倾向于通过抛出一个不支持的操作异常来“快速失败”,而不是提供一个易于滥用和不必要的调试会话的实现,或者即使在不可变对象的状态发生变化时仍然健壮的实现,但本质上会将哈希表变成链接名单。

测试代码:

对于那些想知道是否一成不变的数字此事进行计算大致相似于那些我需要实现的时候:

import java.util.Arrays; 

public class MutableOrImmutable 
{ 
    private int[] pseudomatrix = { 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
            0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 
            0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 
            0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 
            0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 
            0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 
            0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 
            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 
            0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 
            0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 
            1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 
            0, 0, 3, 4, 0, 0, 0, 0, 0, 0, 
            0, 0, 0, 0, 5, 5, 0, 0, 0, 0, 
            0, 0, 0, 0, 0, 0, 4, 3, 0, 0, 
            0, 0, 0, 0, 0, 0, 0, 0, 2, 1 }; 

    private int[] scalars = { 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; 

    private static final int ITERATIONS = 500; 

    private void testMutablePrimitives() 
    { 
     int[] matrix = Arrays.copyOf(pseudomatrix, pseudomatrix.length); 

     long startTime = System.currentTimeMillis(); 

     for (int iteration = 0 ; iteration < ITERATIONS ; ++iteration) 
     { 
      for (int scalar : scalars) 
      { 
       for (int index = 0 ; index < matrix.length ; ++index) 
       { 
        matrix[ index ] *= scalar; 
       } 
      } 

      for (int scalar : scalars) 
      { 
       for (int index = 0 ; index < matrix.length ; ++index) 
       { 
        matrix[ index ] /= scalar; 
       } 
      } 
     } 

     long stopTime = System.currentTimeMillis(); 
     long elapsedTime = stopTime - startTime; 

     System.out.println("Elapsed time for mutable primitives: " + elapsedTime); 

     assert Arrays.equals(matrix, pseudomatrix) : "The matrices are not equal."; 
    } 

    private void testImmutableIntegers() 
    { 
     // Integers are autoboxed and autounboxed within this method. 

     Integer[] matrix = new Integer[ pseudomatrix.length ]; 

     for (int index = 0 ; index < pseudomatrix.length ; ++index) 
     { 
      matrix[ index ] = pseudomatrix[ index ]; 
     } 

     long startTime = System.currentTimeMillis(); 

     for (int iteration = 0 ; iteration < ITERATIONS ; ++iteration) 
     { 
      for (int scalar : scalars) 
      { 
       for (int index = 0 ; index < matrix.length ; ++index) 
       { 
        matrix[ index ] = matrix[ index ] * scalar; 
       } 
      } 

      for (int scalar : scalars) 
      { 
       for (int index = 0 ; index < matrix.length ; ++index) 
       { 
        matrix[ index ] = matrix[ index ]/scalar; 
       } 
      } 
     } 

     long stopTime = System.currentTimeMillis(); 
     long elapsedTime = stopTime - startTime; 

     System.out.println("Elapsed time for immutable integers: " + elapsedTime); 

     for (int index = 0 ; index < matrix.length ; ++index) 
     { 
      if (matrix[ index ] != pseudomatrix[ index ]) 
      { 
       // When properly implemented, this message should never be printed. 

       System.out.println("The matrices are not equal."); 

       break; 
      } 
     } 
    } 

    private static class PseudoRational 
    { 
     private int value; 

     public PseudoRational(int value) 
     { 
      this.value = value; 
     } 

     public PseudoRational multiply(PseudoRational that) 
     { 
      return new PseudoRational(this.value * that.value); 
     } 

     public PseudoRational divide(PseudoRational that) 
     { 
      return new PseudoRational(this.value/that.value); 
     } 
    } 

    private void testImmutablePseudoRationals() 
    { 
     PseudoRational[] matrix = new PseudoRational[ pseudomatrix.length ]; 

     for (int index = 0 ; index < pseudomatrix.length ; ++index) 
     { 
      matrix[ index ] = new PseudoRational(pseudomatrix[ index ]); 
     } 

     long startTime = System.currentTimeMillis(); 

     for (int iteration = 0 ; iteration < ITERATIONS ; ++iteration) 
     { 
      for (int scalar : scalars) 
      { 
       for (int index = 0 ; index < matrix.length ; ++index) 
       { 
        matrix[ index ] = matrix[ index ].multiply(new PseudoRational(scalar)); 
       } 
      } 

      for (int scalar : scalars) 
      { 
       for (int index = 0 ; index < matrix.length ; ++index) 
       { 
        matrix[ index ] = matrix[ index ].divide(new PseudoRational(scalar)); 
       } 
      } 
     } 

     long stopTime = System.currentTimeMillis(); 
     long elapsedTime = stopTime - startTime; 

     System.out.println("Elapsed time for immutable pseudo-rational numbers: " + elapsedTime); 

     for (int index = 0 ; index < matrix.length ; ++index) 
     { 
      if (matrix[ index ].value != pseudomatrix[ index ]) 
      { 
       // When properly implemented, this message should never be printed. 

       System.out.println("The matrices are not equal."); 

       break; 
      } 
     } 
    } 

    private static class PseudoRationalVariable 
    { 
     private int value; 

     public PseudoRationalVariable(int value) 
     { 
      this.value = value; 
     } 

     public void multiply(PseudoRationalVariable that) 
     { 
      this.value *= that.value; 
     } 

     public void divide(PseudoRationalVariable that) 
     { 
      this.value /= that.value; 
     } 
    } 

    private void testMutablePseudoRationalVariables() 
    { 
     PseudoRationalVariable[] matrix = new PseudoRationalVariable[ pseudomatrix.length ]; 

     for (int index = 0 ; index < pseudomatrix.length ; ++index) 
     { 
      matrix[ index ] = new PseudoRationalVariable(pseudomatrix[ index ]); 
     } 

     long startTime = System.currentTimeMillis(); 

     for (int iteration = 0 ; iteration < ITERATIONS ; ++iteration) 
     { 
      for (int scalar : scalars) 
      { 
       for (PseudoRationalVariable variable : matrix) 
       { 
        variable.multiply(new PseudoRationalVariable(scalar)); 
       } 
      } 

      for (int scalar : scalars) 
      { 
       for (PseudoRationalVariable variable : matrix) 
       { 
        variable.divide(new PseudoRationalVariable(scalar)); 
       } 
      } 
     } 

     long stopTime = System.currentTimeMillis(); 
     long elapsedTime = stopTime - startTime; 

     System.out.println("Elapsed time for mutable pseudo-rational variables: " + elapsedTime); 

     for (int index = 0 ; index < matrix.length ; ++index) 
     { 
      if (matrix[ index ].value != pseudomatrix[ index ]) 
      { 
       // When properly implemented, this message should never be printed. 

       System.out.println("The matrices are not equal."); 

       break; 
      } 
     } 
    } 

    public static void main(String [ ] args) 
    { 
     MutableOrImmutable object = new MutableOrImmutable(); 

     object.testMutablePrimitives(); 
     object.testImmutableIntegers(); 
     object.testImmutablePseudoRationals(); 
     object.testMutablePseudoRationalVariables(); 
    } 
} 

脚注:

可变类与不可变类的核心问题是 - hig溶血素怀疑 - 在Object“散列码”的方法:

hashCode的一般合同是:

  • 每当它是一个Java应用程序的执行过程中调用同一个对象上不止一次, hashCode方法必须始终返回相同的整数,前提是在修改对象的等于比较时不使用任何信息。该整数不需要从应用程序的一次执行到同一应用程序的另一次执行保持一致。

  • 如果两个对象根据equals(Object)方法相等,则对两个对象中的每个对象调用hashCode方法必须产生相同的整数结果。

  • 根据equals(java.lang.Object)方法,如果两个对象不相等,则不要求对这两个对象中的每一个调用hashCode方法都必须产生不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同的整数结果可能会提高哈希表的性能。

但是,一旦一个物体被添加到用于确定依赖于来自其内部状态导出的其哈希码值的集合“平等,”它不再是正确散列到收集时,它的状态变化。是的,程序员的负担是为了确保可变对象没有被不正确地存储在集合中,但是维护程序员的负担更重,除非不恰当地使用可变类不会首先被阻止。这就是为什么我认为对可变对象的“hashcode”正确的“回答”是总是抛出一个UnsupportedOperationException,同时仍然执行“equals”来确定对象的相等性 - 想想你想要比较的矩阵是否相等,但不会想到添加到Set。然而,可能有人认为抛出异常违反了上述“合同”,并带有其自身的可怕后果。在这种情况下,尽管执行的性质很差,但将可变类的所有实例散列为相同的值可能是维持合同的“正确”方法。返回一个常量值 - 可能是通过散列类名生成的 - 通过抛出异常推荐?

+1

'hashCode '和'equals'在可变类中实现没有问题。可变类的问题是当你在可能依赖于这些方法的'Collection'实例中使用时。 – SJuan76

+1

在你确实有数据表明分配对你来说是一个瓶颈之前,绝对要使用不可变类型。分配很便宜。 –

+0

如果你让它们变为可变的,你会遇到很大的麻烦,因为Java中的Number是不可变的。你可以在一个可变对象上实现'hashcode'和'equals'并且很好地解决这个问题,但是问题是其他人可能没有那么小心,并且遇到非常棘手的调试情况。就像路易斯提到的那样,你是否确定这是瓶颈的实际来源? –

回答

0

目前,我正在用不可变对象实现有理数。这允许在我需要执行的计算中经常发生ZERO和ONE对象的大量重用。然而,使用有理数字元素实现的Matrix类是可变的 - 甚至在内部使用null来表示“虚拟”零。随着更迫切需要无缝处理“小”有理数和任意精度“大”有理数,现在可以接受的是不变的实现,直到我有时间来描述我可用于此目的的问题库以便确定无论是可变对象还是更大的一组“常见”不可变对象都将赢得一天。

当然,如果我最终需要实现“equals”来测试Matrix的平等性,那么当可能需要该方法的可能性极小时,我将回到与Matrix“hashcode”相同的问题。这又把我带回到“散列码” - 也许“等于”的相当无用的投诉 - 应该永远不会是java.lang.Object合同的一部分...

0

你已经写了:“数学表达式成为执行甚至简单的数学运算时,背负着过多的对象分配和后续的垃圾收集。” 和“表达式包含三个分配 - 其中两个快速丢弃”。

现代垃圾收集实际用于分配的这种格局优化,让你的(隐含的)假设分配和后续的垃圾收集是昂贵的,是错误的。

例如,看到这个白色的纸: http://www.oracle.com/technetwork/java/whitepaper-135217.html#garbage 在“代际复制集”,它指出:

” ......首先,由于新的对象被连续堆叠状的方式在分配对象幼儿园的分配变得非常快,因为它只涉及更新单个指针并执行一次托儿所溢出检查;其次,当幼儿园溢出时,幼儿园中的大部分对象已经死亡,使得垃圾收集器可以简单地将其他地方的少数幸存物移出,并避免为托儿所的死物进行填海工作。“因此,我建议你真正的问题的答案是你应该使用不可变对象,因为感知成本根本不是真正的成本,但是感知的好处(例如简单性,代码可读性)是真正的好处。

+0

感谢您的参考。一般来说,我相信你有一个有效的观点。但是,考虑一个简单的大矩阵乘以标量的情况。矩阵的每个单元格都将被更新。在Java中,很少有人会实现不可变矩阵,每个矩阵运算后需要分配一个新矩阵,然后丢弃受操作影响的矩阵。实际上,大多数矩阵库提供了双精度,浮点精度和甚至整数矩阵,这些矩阵更新内部数组,不需要分配或垃圾回收。但是有理性或复杂的数字...... – Ned

+0

在某些情况下,问题不仅仅是垃圾收集的成本,在某些情况下还包括复制对象的未更改部分的成本。例如,增加一个可变的2048位整数在大多数情况下只需要更新较低的字,并且很少需要更新超过两个或三个,但增加一个不可变的2048位整数可能需要构造一个新的256字节对象。 – supercat

0

一个可能有用的模式是为“可读”事物定义抽象类型或接口,然后使其具有可变和不可变形式。如果基类型或接口类型包含AsMutable,AsNewMutableAsImmutable方法,并且这些方法可以以适当的方式在衍生对象中重写,则此模式可能会特别好。这样的方法可以在需要时实现可变性的好处,同时也可以获得使用不可变类型的好处。如果代码想要坚持一个值但不改变它,如果它使用可变类型,则必须使用“防御性复制”,但如果它接收到“可读”的东西,则可以使用AsImmutable。如果事情碰巧是可变的,它会复制,但如果它是不可变的,则不需要复制。顺便说一句,如果设计一个不可变的类型,除了引用一个包含acutal数据的大对象之外的字段相对较少,并且如果类型的事情经常被比较为相等,那么可能会有所帮助每种类型都拥有唯一的序列号以及对最早实例(如果有的话)的引用(如果已知它们相等)(或者,如果没有已知的旧实例存在,则为null)。当比较两个实例的相等性时,确定已知最匹配的实例(递归检查最早的已知实例,直到它为空)。如果两个实例已知匹配相同的实例,则它们是相等的。如果不是,但是结果是平等的,那么无论哪个“老年人”更年轻,都应该把另一个看作是一个与之相当的老年人。这种方法会产生加速的比较,就像国际会议一样,但不需要使用单独的实习字典,也不需要散列值。