JavaRush /Java 博客 /Random-ZH /对象比较:练习
articles
第 15 级

对象比较:练习

已在 Random-ZH 群组中发布
这是专门讨论对象比较的第二篇文章。他们中的第一个讨论了比较的理论基础——它是如何进行的、为什么以及在哪里使用它。在这篇文章中,我们将直接讨论比较数字、对象、特殊情况、微妙之处和非显而易见的点。更准确地说,我们将讨论以下内容:
对象比较:练习 - 1
  • 字符串比较: ' ==' 和equals
  • 方法String.intern
  • 真实原语的比较
  • +0.0-0.0
  • 意义NaN
  • Java 5.0。==通过“ ”生成方法和比较
  • Java 5.0。自动装箱/拆箱:“ ==”、“ >=”和“ <=”用于对象包装器。
  • Java 5.0。枚举元素的比较(类型enum
那么让我们开始吧!

字符串比较: ' ==' 和equals

啊,这些行...最常用的类型之一,这会导致很多问题。原则上,有一篇关于它们的单独文章。在这里我要谈谈比较问题。当然,可以使用 来比较字符串equals。此外,它们必须通过 进行比较equals。然而,有一些微妙之处值得了解。首先,相同的字符串实际上是一个对象。通过运行以下代码可以轻松验证这一点:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
结果将是“相同”。这意味着字符串引用是相等的。这是在编译器级别完成的,显然是为了节省内存。编译器创建该字符串的一个实例,并分配对此实例的引用str1str2但是,这仅适用于在代码中声明为文字的字符串。如果您将各个片段组成一个字符串,则指向它的链接将会不同。确认 - 此示例:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
结果将是“不一样”。您还可以使用复制构造函数创建一个新对象:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
结果也会是“不一样” 因此,有时可以通过引用比较来比较字符串。但最好不要依赖于此。我想谈谈一种非常有趣的方法,它允许您获得所谓的字符串的规范表示 - String.intern。让我们更详细地讨论一下。

String.intern方法

让我们从该类String支持字符串池这一事实开始。类中定义的所有字符串文字(而不仅仅是它们)都会添加到此池中。因此,该方法允许您从该池中获取一个字符串,从 的角度来看,intern该字符串等于现有字符串(调用该方法的字符串) 。如果池中不存在这样的行,则将现有行放置在那里并返回指向该行的链接。因此,即使对两个相等字符串的引用不同(如上面的两个示例),对这些字符串的调用也将返回对同一对象的引用: internequalsintern
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
执行这段代码的结果将是“相同” 我无法确切地说出为什么要这样做。该方法intern是本机的,说实话,我不想陷入 C 代码的荒野。这样做很可能是为了优化内存消耗和性能。无论如何,了解这个实现特性是值得的。让我们继续下一部分。

真实原语的比较

首先,我想问一个问题。很简单。以下总和是多少 – 0.3f + 0.4f?为什么?0.7f?让我们检查:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
因此?喜欢?我也是。对于那些没有完成这个片段的人,我会说结果将是......
f1==f2: false
为什么会发生这种情况?...让我们进行另一个测试:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
请注意转换为double. 这样做是为了输出更多的小数位。结果:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
严格来说,结果是可以预见的。小数部分的表示是使用有限级数2-n来进行的,因此无需讨论任意选择的数字的精确表示。从例子可以看出,表示精度float为小数点后7位。 严格来说,该表示法float 为尾数分配了 24 位。因此,可以使用 float (不考虑程度,因为我们谈论的是精度)表示的最小绝对数是2-24≈6*10-8。正是通过这一步,表示中的值才真正走向 float。既然有量化,那就有误差。 因此得出结论:表示中的数字float只能以一定的精度进行比较。我建议将它们四舍五入到小数点后第六位 (10-6),或者最好检查它们之间差异的 绝对值:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
在这种情况下,结果令人鼓舞:
|f3-f4|<1e-6: true
当然,图片和文字是一模一样的double。唯一的区别是尾数分配了 53 位,因此表示精度为 2-53≈10-16。是的,量化值要小得多,但它是存在的。它可以开一个残酷的玩笑。顺便说一句,在JUnit测试库中,在比较实数的方法中,明确指定了精度。那些。比较方法包含三个参数 - 数量、应该等于什么以及比较的准确性。 顺便说一句,我想提一下以科学格式书写数字(表示程度)相关的微妙之处。问题。10-6怎么写?实践表明,80%以上的答案是——10e-6。同时,正确答案是1e-6!10e-6 就是 10-5!我们在其中一个项目中踩到了这把耙子,出乎意料。他们花了很长时间寻找错误,查看了常数 20 次。没有人对它们的正确性有一丝怀疑,直到有一天,很大程度上是偶然的,常数 10e-3 被打印出来,他们发现了两个小数点后的数字而不是预期的三位。因此,要小心! 让我们继续。

+0.0 和 -0.0

在实数的表示中,最高有效位是有符号的。如果所有其他位均为 0,会发生什么情况?与整数不同,在这种情况下,结果是位于表示范围下限的负数,只有最高有效位设置为 1 的实数也意味着 0,只是带有一个负号。因此,我们有两个零 - +0.0 和 -0.0。一个逻辑问题出现了:这些数字应该被认为是相等的吗?虚拟机正是这样思考的。然而,这是两个不同的数字,因为与它们进行运算的结果会获得不同的值:
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
...结果:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
因此,在某些情况下,将 +0.0 和 -0.0 视为两个不同的数字是有意义的。如果我们有两个对象,其中一个对象的字段为+0.0,另一个对象为-0.0,那么这些对象也可以被视为不相等。问题出现了 - 如果直接与虚拟机进行比较,您如何理解这些数字是不平等的true?答案是这样的。尽管虚拟机认为这些数字是相等的,但它们的表示形式仍然不同。因此,唯一能做的就是比较观点。为了获得它,有方法int Float.floatToIntBits(float)and ,它们分别以和 的long Double.doubleToLongBits(double)形式返回一个位表示(上一个示例的延续): intlong
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
结果将是
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
因此,如果 +0.0 和 -0.0 是不同的数字,那么您应该通过实际变量的位表示来比较它们。我们似乎已经整理出了+0.0和-0.0。然而,-0.0 并不是唯一的惊喜。还有这样的事...

南值

NaN代表Not-a-Number。该值是由于不正确的数学运算而出现的,例如将 0.0 除以 0.0、用无穷大除以无穷大等。该值的特殊之处在于它不等于其自身。那些。:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
...将导致...
x=NaN
x==x: false
当比较对象时,结果会如何呢?如果对象的字段等于NaN,则比较将给出false,即 保证对象被认为是不平等的。尽管从逻辑上讲,我们可能想要相反的结果。使用该方法即可达到所需的结果Float.isNaN(float)true如果参数是 则返回NaN。在这种情况下,我不会依赖于比较位表示,因为 它没有标准化。也许关于原语就足够了。现在让我们继续讨论自 5.0 版以来 Java 中出现的微妙之处。我想谈的第一点是

Java 5.0。==通过“ ”生成方法和比较

设计中有一种模式叫做生产方法。有时它的使用比使用构造函数更有利可图。让我举一个例子。我想我很了解对象外壳Boolean。此类是不可变的,并且只能包含两个值。也就是说,事实上,对于任何需求,只需要两份就足够了。如果你提前创建它们然后简单地返回它们,它会比使用构造函数快得多。有这样一个方法BooleanvalueOf(boolean)。它出现在1.4版本中。Byte类似的生成方法在 5.0 版本中在、CharacterShortInteger类中引入Long。当加载这些类时,将创建与某些范围的原始值相对应的实例数组。这些范围如下:
对象比较:练习 - 2
这意味着使用该方法时,valueOf(...)如果参数落在指定范围内,则始终返回相同的对象。也许这会提高速度。但与此同时,问题的性质也导致了很难追根究底。阅读更多相关内容。 理论上,生成方法valueOf已添加到Float和类中Double。他们的描述说,如果你不需要新的副本,那么最好使用这种方法,因为 它可以提高速度等。等等。然而,在当前(Java 5.0)实现中,在此方法中创建了一个新实例,即 它的使用不能保证提高速度。而且,我很难想象这个方法如何能够加速,因为由于值的连续性,那里无法组织缓存。整数除外。我的意思是,没有小数部分。

Java 5.0。自动装箱/拆箱:“ ==”、“ >=”和“ <=”用于对象包装器。

我怀疑生产方法和实例缓存被添加到整数基元的包装器中以优化操作autoboxing/unboxing。让我提醒你它是什么。如果一个操作必须涉及一个对象,但涉及一个原语,那么这个原语会自动包装在对象包装器中。这autoboxing。反之亦然 - 如果操作必须涉及原语,那么您可以在那里替换对象外壳,并且值将自动从中扩展。这unboxing当然,你必须为这种便利付费。自动转换操作会在一定程度上降低应用程序的速度。不过,这与当前的主题无关,所以我们先离开这个问题。 只要我们处理的是与原语或 shell 明显相关的操作,一切都很好。“ ”操作会发生什么==?假设我们有两个Integer内部具有相同值的对象。他们将如何比较?
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
结果:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
结果:
i1==i2: true
现在这更有趣了!如果autoboxing-e 返回相同的对象!这就是陷阱所在。一旦我们发现返回相同的对象,我们将开始试验,看看情况是否总是如此。我们将检查多少个值?一?十?一百?我们很可能会将每个方向的数量限制在 0 左右。我们到处都平等。似乎一切都很好。然而,回头看一下,这里。您猜到问题是什么了吗?是的,自动装箱期间对象外壳的实例是使用生成方法创建的。下面的测试很好地说明了这一点:
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
结果会是这样的:
number=-129: false
number=-128: true
number=127: true
number=128: false
对于缓存范围 内的值,返回相同的对象,对于缓存范围之外的值,返回不同的对象。因此,如果比较应用程序 shell 中的某个位置而不是基元,则有可能出现最可怕的错误:浮动错误。因为代码很可能还会在不会出现此错误的有限值范围内进行测试。但在实际工作中,它要么出现,要么消失,取决于一些计算的结果。发疯比发现这样的错误更容易。因此,我建议您尽可能避免自动装箱。事实并非如此。让我们记住数学,不超过五年级。设不等式A>=BА<=BA关于和 的关系可以说些什么B?只有一件事——它们是平等的。你同意?我想是的。让我们运行测试:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
结果:
i1>=i2: true
i1<=i2: true
i1==i2: false
这对我来说是最大的奇怪的事情。我完全不明白为什么这个功能会引入到语言中,如果它引入了这样的矛盾。总的来说,我会再次重复 - 如果可以不用autoboxing/unboxing,那么值得充分利用这个机会。我想谈的最后一个主题是…… Java 5.0。枚举元素的比较(enum类型) 大家知道,从5.0版本开始Java就引入了enum这样的类型——枚举。默认情况下,其实例在类的实例声明中包含名称和序列号。因此,当公告顺序发生变化时,数字也会发生变化。然而,正如我在“序列化本身”一文中所说,这不会引起问题。所有枚举元素都存在于单个副本中,这是在虚拟机级别控制的。因此,可以使用链接直接比较它们。* * * 也许这就是今天关于实现对象比较的实际方面的全部内容。也许我错过了什么。一如既往,我期待您的评论!现在,让我先告辞了。感谢大家的关注! 来源链接:比较对象:练习
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION