自学内容网 自学内容网

编程语言历史:聊聊Java最失败的设计:“==”

聊聊Java最失败的设计:==

Java以其强大的生态系统、丰富的库支持以及跨平台的能力而闻名。但是Java有许多设计在今天看来已经不合时宜,其中最失败的设计非操作符==莫属。我们今天一起探讨==操作符的来龙去脉,失败在哪,为什么会失败的问题。

首先,我们先了解==操作符的基本概念。在Java中,==操作符主要用于比较两个变量或表达式是否相等。对于基本数据类型(如int、char等),它直接比较值是否相同。当涉及到对象时,==操作符比较的是两个对象的引用是否指向内存中的同一个位置,而不是这两个对象的内容是否相等。

字符串比较中的陷阱

一个典型的例子就是字符串的比较。假设你有两个字符串,如果和其他语言一样,习惯性地使用==来判断它们是否相等。但这种做法往往会带来错误的结果。比如下面这段代码:

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // 输出 true
System.out.println(a == c); // 输出 false

即使abc都包含了相同的字符序列“hello”,但ab是通过字符串字面量定义的,因此它们共享同一个内存位置,所以a == b返回true。而c是通过new关键字创建的新对象,虽然它的内容与ab相同,但由于它位于不同的内存位置,因此a == c返回false

正确的做法应该是使用equals()方法来比较字符串的内容:

System.out.println(a.equals(c)); // 输出 true

性能优化和==结合后的坑

当两个Integer对象的值在-128到127之间时,Java为了节省内存会重用这些对象,所以在这个范围内,使用==来比较也会返回true。但是,当数值超出这个范围时,即使数值相同,由于每个对象都是独立创建的,==比较也会返回false

Integer a = 100; // 自动装箱
Integer b = 100;
System.out.println(a == b); // true, 因为在-128至127之间的值会被重用

Integer c = 1000; // 自动装箱
Integer d = 1000;
System.out.println(c == d); // false, 每个对象都在堆上单独创建

在这个例子中,ab都表示数字100,但它们是否相等取决于它们的值是否在-128到127之间。如果是的话,a == b会返回true,因为Java对这个区间内的整数进行了缓存。然而,如果值超出了这个范围,即使它们的值相同,==也会返回false

最佳实践

为了避免这些问题,我们一般遵循的一些最佳实践包括:

  1. 使用equals()方法:当你想比较两个对象的内容是否相等时,应该使用equals()方法,而不是==
  2. 注意自动装箱的细节:对于包装类对象,要特别留意自动装箱的机制,特别是在进行对象引用比较的时候。
  3. 保持意图明确:编写代码时要尽量清晰地表达你的意图,比如使用Objects.equals()方法来处理可能为null的对象,使代码更安全、更易读。
  4. 编写单元测试:编写单元测试以验证逻辑,确保在各种情况下都能正确地比较对象。

JVM中其他语言纷纷改进了==的语义

Kotlin:智能的==比较

Kotlin是近年来非常受欢迎的一种现代编程语言,它直接运行在JVM上,并且可以与Java无缝集成。Kotlin的设计者们意识到了==操作符在Java中的局限性,并对其进行了改进。在Kotlin中,==操作符默认是比较对象的内容,而不是引用。因此,当你使用==来比较两个字符串或其他对象时,它实际上调用的是equals()方法来进行比较,而不是简单的引用比较。而Kotlin给予===严格相等的含义,这个设计与Java的==相似,但又不完全一样。

例如,在Kotlin中:

val a: String = "hello"
val b: String = "hello"
val c = "hello"

println(a == b) // 输出 true
println(a == c) // 输出 true

这里的ab是通过不同的方式初始化的,但是它们的内容是一样的,因此a == b返回true。同样,ac也是内容相同的不同对象,所以a == c同样返回true

Groovy:灵活的==比较

Groovy是另一种在JVM上运行的动态语言,它也提供了一个更加灵活的==操作符实现。在Groovy中,默认情况下==操作符的行为类似于Java,但是Groovy允许用户通过元编程技术来自定义对象的比较行为。或者覆盖对象的equals()方法,因为当使用==比较对象时,Groovy会调用equals()方法来确定两个对象是否相等。如果需要改变默认的行为,可以在类中覆盖equals()方法。
这使得开发者可以根据自己的需求来定制==操作符的行为,使其更加符合业务逻辑的需求。

Scala:多重比较机制

Scala是一种兼具面向对象和函数式编程特性的语言,它在JVM上运行并且提供了多种比较机制。Scala中,默认情况下==操作符用于比较对象的内容,与Kotlin类似。此外,Scala还提供了eq方法来专门比较对象的引用。

例如:

val a: String = "hello"
val b: String = "hello"
val c = new String("hello")

println(a == b) // 输出 true
println(a == c) // 输出 false
println(a eq b) // 输出 true
println(a eq c) // 输出 false

在这里,==用于比较内容,而eq用于比较引用,这样可以更加清晰地区分两种不同的比较方式。

当时为什么Java当时要这样设计?

Java作为一种面向对象的编程语言,自诞生之初就旨在提供一种统一的、跨平台的编程模型。在Java的设计初期,很多决策都是基于当时的技术环境和对未来发展的预期。对于==操作符的设计,其初衷是为了提供一种快速且直观的方式来比较对象的引用。这种设计背后有几个重要的考虑因素:

受C/C++影响

Java的设计受到C/C++的影响较大,这两种语言中的==操作符都用来比较值或引用,而不是内容。继承这一设计可以让熟悉C/C++的开发者更容易过渡到Java。这也是Java这样设计的直接原因
但C++与Java完全不同,C++开发者经常需要处理引用和值的区别,而且C++有强大的运算符重载能力,没有自动装拆箱,很好地规避了这个设计带来的缺陷。

性能和观念影响

在Java语言最初设计的那个年代,计算资源相对有限,性能优化尤为重要。直接比较对象引用的速度远远快于比较对象的内容。这是Java设计者鼓励开发者多用==来提高性能。而将比较内容的运算符设计为===

明确语义一致性

Java的设计者希望为程序员提供明确的语义边界。如果==操作符默认比较对象的内容,那么程序员需要时刻警惕哪些地方需要比较引用,哪些地方需要比较内容。这种混淆会导致更多的错误。因此,Java选择将==固定为引用比较,并推荐使用equals()方法来比较内容,这种区分有助于减少歧义。

Java==设计回顾

虽然Java在==操作符的设计上有其历史背景和技术考量,但这一设计在某些情况下显得不再那么理想。现代编程更注重避免误用,代码更符合人的直觉,因此Java的这个设计原则已经不符合现代要求了。

总结

通过观察JVM上的其他语言对==操作符的改进,我们可以看到,许多现代语言倾向于让==默认比较对象的内容,而不是引用。这样的设计更加符合开发者的直觉,减少了因误解==操作符而引入的bug。同时,这些语言也提供了额外的方法来比较对象的引用,使得开发者可以在需要的时候进行精确控制。

总结来说,==操作符的设计在Java中确实存在不合理之处。因此,在学习和使用Java时,深刻理解其设计的缘由和缺陷,可以有效地帮助我们在实践中规避许多问题,解答许多疑惑,进而更好地掌握Java并写出更加健壮和易于维护的代码。


原文地址:https://blog.csdn.net/hebhljdx/article/details/142629880

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!