在我最近的博客文章Arrays.hashCode()与 DZone联合版本的评论中提出了一个有趣的问题。 Objects.hash() “。 该评论的作者建立了一些示例,这些示例与我的博客文章中使用的示例相似,并且显示出与我看到的结果不同的结果。 感谢评论作者抽出宝贵的时间来发表这篇文章,因为它带来了Java的细微差别,我认为这很值得写博客。
评论作者显示了以下有效的Java语句:
int[] arr = new int[]{1,2,3,4};
System.out.println(Arrays.hashCode(arr));
System.out.println(Objects.hash(1,2,3,4));
System.out.println(Arrays.hashCode(new Integer[]{new Integer(1),new Integer(2),new Integer(3),new Integer(4)}));
System.out.println(Objects.hash(new Integer(1),new Integer(2),new Integer(3),new Integer(4)));
该评论的作者提到,对于所有四个语句,运行刚显示的代码的结果都完全相同。 这与我的示例不同,在示例中,在原始int值数组上调用Arrays.hashCode(int [])的结果与在同一原始int
值数组上调用Objects.hash(Object…)的结果不同。
对原始反馈评论的一个答复准确地指出,不能保证在不同JVM上生成的哈希码是相同的。 实际上, Object.hashCode()方法的Javadoc注释指出(我强调了 ):
- 只要在Java应用程序执行期间在同一对象上多次调用它,hashCode方法就必须一致地返回相同的整数,前提是不修改该对象的equals比较中使用的信息。 从一个应用程序的执行到同一应用程序的另一执行,此整数不必保持一致。
- 如果根据equals(Object)方法,两个对象相等,则在两个对象中的每个对象上调用hashCode方法必须产生相同的整数结果。
陈述了所有这些内容之后,为整数计算的哈希码通常在每次运行之间都是一致的。 原始评论者示例的输出都具有完全相同的值也很有趣。 尽管我可能不希望这些值与示例的值相匹配,但是令人惊讶的是,评论者提供的所有示例都具有相同的答案。
反馈注释中提供的示例与我的示例之间的区别在于注释者的示例如何为原始int
值数组调用Objects.hash(Object...)
与我的示例如何调用Objects.hash(Object...)
用于原始int
值的数组。 在我的示例中,我将相同的本地数组传递给所有方法调用。 该注释者的示例将原始int
值的显式数组传递给Arrays.hashCode(int[])
,但将各个int
元素传递给Objects.hash(Object...)
而不是将数组传递给后一个方法。 当我向注释者的示例集中添加另一个示例,该示例确实将原始int
值数组传递给Objects.hash(Object...)
方法时,我得到的生成的哈希码与所有其他哈希码不同。 接下来显示该增强的代码。
final int[] arr = new int[]{1,2,3,4};
out.println("Arrays.hashCode(int[]): " + Arrays.hashCode(arr));
out.println("Objects.hash(int, int, int, int): " + Objects.hash(1,2,3,4));
out.println("Objects.hash(int[]): " + Objects.hash(arr));
out.println("Objects.hashCode(Object): " + Objects.hashCode(arr));
out.println("int[].hashCode(): " + arr.hashCode());
out.println("Arrays.hashCode(Int, Int, Int, Int): " + Arrays.hashCode(new Integer[]{1,2,3,4}));
out.println("Objects.hash(Int, Int, Int, Int): " + Objects.hash(1,2,3,4));
运行注释器提供的代码的经过调整和增强的版本会导致输出(带有我添加的示例突出显示):
Arrays.hashCode(int[]): 955331
Objects.hash(int, int, int, int): 955331
Objects.hash(int[]): 897913763
Objects.hashCode(Object): 897913732
int[].hashCode(): 897913732
Arrays.hashCode(Int, Int, Int, Int): 955331
Objects.hash(Int, Int, Int, Int): 955331
将输出与生成它的代码进行比较,可以看出,当将int
值数组的元素传递给Arrays.hashCode(int[])
,它与Objects.hash(Object...)
生成相同的哈希码值Objects.hash(Object...)
方法作为单个元素。 但是,我们还可以看到,当完整地传递原始int
值的数组(作为单个数组而不是作为数组的单个元素)时, Objects.hash(Object...)
方法生成了完全不同的哈希码。 我添加的其他两个示例(突出显示)是通过直接在数组上调用.hashCode()
或通过Objects.hashCode获得等效结果来显示原始int
值数组上的“直接”哈希码。 (对象) 。 [这并非巧合, Objects.hash(Object...)
为原始int
值数组生成的哈希码比为原始int
值数组生成的“直接”哈希码正好大31。 ]
所有这些都指向这里的真正问题:通常最好不要将原语数组传递给接受可变参数 (通告省略号 )的方法。 SonarSource规则浏览器 ( Java )在RSPEC-3878中提供了有关此内容的更多详细信息。 与规则描述特别相关的是与歧义有关的问题:“数组应该是一个对象还是对象的集合?”
刚刚提出的问题的答案是,当将原始int
值数组传递给接受方法Objects.hash(Object...)
的变量参数时, 整个数组将被视为单个 Object
。 相反,当将引用对象的数组(例如Integer
)传递给相同的方法时,它将其视为与数组中的元素传递给它的对象数量相同。 下一个代码清单和相关输出证明了这一点。
package dustin.examples.hashcodes;import static java.lang.System.out;/*** Demonstrates the difference in handling of arrays by methods that* accept variable arguments (ellipsis) when the arrays have primitive* elements and when arrays have reference object elements.*/
public class ArraysDemos
{private static void printEllipsisContents(final Object ... objects){out.println("==> Ellipsis Object... - Variable Arguments (" + objects.length + " elements): " + objects.getClass() + " - " + objects);}private static void printArrayContents(final Object[] objects){out.println("==> Array Object[] - Variable Arguments (" + objects.length + " elements): " + objects.getClass() + " - " + objects);}private static void printArrayContents(final int[] integers){out.println("==> Array int[] - Variable Arguments (" + integers.length + " elements): " + integers.getClass() + " - " + integers);}public static void main(final String[] arguments){final int[] primitiveIntegers = ArraysCreator.createArrayOfInts();final Integer[] referenceIntegers = ArraysCreator.createArrayOfIntegers();out.println("\nint[]");printEllipsisContents(primitiveIntegers);printArrayContents(primitiveIntegers);out.println("\nInteger[]");printEllipsisContents(referenceIntegers);printArrayContents(referenceIntegers);}
}
int[]
==> Ellipsis Object... - Variable Arguments (1 elements): class [Ljava.lang.Object; - [Ljava.lang.Object;@2752f6e2
==> Array int[] - Variable Arguments (10 elements): class [I - [I@1cd072a9Integer[]
==> Ellipsis Object... - Variable Arguments (10 elements): class [Ljava.lang.Integer; - [Ljava.lang.Integer;@7c75222b
==> Array Object[] - Variable Arguments (10 elements): class [Ljava.lang.Integer; - [Ljava.lang.Integer;@7c75222b
刚刚显示的示例代码和相关的输出表明,期望变量参数的方法将传递给它的原始值数组视为单个元素数组 。 另一方面,相同的方法将传递给具有参考对象类型的数组的数组视为具有相同元素数的数组。
考虑到这一点,请返回哈希码生成示例,由Objects.hash(Object...)
为原始int
值数组生成的哈希码与由Arrays.hashCode(int[])
生成的哈希码不同。 类似地,我们现在可以解释为什么对象引用数组导致相同的哈希码,而不管调用了哪种方法。
前面我提到过,由Objects.hash(Object)
生成的哈希码比整个数组的“直接”哈希码高31并非巧合。 这并不奇怪,因为Objects.hash(Object...)
的OpenJDK实现将Arrays.hashCode(Object[])
Objects.hash(Object...)
委托给Arrays.hashCode(Object[])
,该数组使用素数乘以31 ,并乘以计算出的哈希码中的每个元素。 考虑到上述观察,由Objects.hash(Object...)
为原始int
值数组提供的哈希码值似乎正是该方法的实现将导致我们期望的结果:整个数组的直接哈希值加上31个质数。 当该哈希码方法仅循环一个元素时(传递给需要可变参数的方法的基元数组就是这种情况),其计算本质上是31 * 1 + <directHashValueOfOverallArray>
。
值得注意的是,即使参考对象数组的哈希码计算得出的结果与将元素传递给接受可变参数的方法时的结果相同,还是最好避免将参考对象数组传递给这样的对象。方法。 当发生这种情况时, javac
编译器会提供此警告:“警告:对最后一个参数使用不精确参数类型的varargs方法的非varargs调用”,并添加了有关解决此问题的潜在方法的这些有用的细节:“为varargs调用广播到对象” “广播到Object []以进行非可变参数调用并禁止显示此警告”。 当然,对于JDK 8和更高版本,在将数组提供给需要可变参数的方法之前,以多种其他方式处理数组是相当简单的。
我在原始帖子 (及其DZone联合版本 )中添加了最后一段,以尝试快速解决此问题,但是我已使用此帖子来更详细地表达此信息。 此处总结的经验教训可以概括为“对原始数组使用适当的重载Arrays.hashCode
方法,而不是使用Objects.hash(Object...)
”和“对数组数组使用Favor Arrays.hashCode(Object[])
”。引用类型,而不是使用Objects.hash(Object...)
。” 如果调用的方法“看到”的元素数量无论如何都是重要的,则更通用的准则是要警惕将原始值数组传递给需要Object
类型变量参数的方法,并且要警惕传递引用数组指向期望可变参数的方法的对象,以避免编译器警告和模棱两可的警告。
翻译自: https://www.javacodegeeks.com/2018/09/java-subtlety-with-arrays-of-primitives-and-variable-arguments.html