String、Integer 和其他包装类是 HashMap 键的自然候选项,而 String
也是最常用的键,因为 String 是不可变的和最终的,并且覆盖了 equals 和 hashcode() 方法。其他包装类也具有类似的属性。
为了防止用于计算 hashCode() 的字段发生更改,需要不可变性, 因为如果键对象在插入和检索过程中返回不同的 hashCode,则无法从 HashMap 获取对象。
不可变性是最好的,因为它提供了其他优点,例如线程安全,如果您可以通过仅使某些字段成为最终字段来保持哈希码相同,那么您也可以这样做。
由于在从 HashMap 检索值对象期间使用了 equals() 和 hashCode() 方法,因此关键对象必须正确覆盖这些方法并跟踪接触。如果一个不相等的对象返回不同的哈希码,那么冲突的可能性就会减少,从而提高 HashMap 的性能。
虽然 Java 设计者最清楚 String 类是最终类的真正原因,除了 James Gosling 对安全性的暗示之外,我认为以下原因也表明了为什么 String 在 Java 中是最终的或不可变的。
Java 设计师知道 String 将成为所有 Java 应用程序中最常用的数据类型,这就是为什么他们要从一开始就进行优化。朝着这个方向迈出的关键一步是将字符串文字存储在字符串池中。
目标是通过共享它们来减少临时 String 对象,并且为了共享它们,它们必须来自 Immutable 类。不能与彼此未知的两方共享可变对象。让我们举一个假设的例子,其中两个引用变量指向同一个 String 对象:
String s1 = “Java”;
字符串 s2 = “Java”;
现在,如果 s1 将对象从“Java”更改为“C++”,引用变量也会得到值 s2=“C++”,它甚至不知道它。 通过使 String 不可变,使 String 文本的共享成为可能。简而言之,如果不在 Java 中使 String 成为 final 或 Immutable,就无法实现 String 池的关键思想。
Java 在提供每个服务级别的安全环境方面有一个明确的目标,而 String 在整个安全方面至关重要。字符串已被广泛用作许多 java 类的参数,例如用于打开网络连接时,可以将主机和端口作为 String 传递,在 Java 中读取文件时,可以将文件和目录的路径作为 String 传递,对于打开数据库连接,可以将数据库 URL 作为 String 传递。
如果 String 不是不可变的,则用户可能已授予访问系统中特定文件的权限,但在身份验证后,他可以将 PATH 更改为其他内容,这可能会导致严重的安全问题。
同样,在连接到数据库或网络中的任何其他计算机时,更改 String 值可能会带来安全威胁。可变字符串也可能导致 Reflection 中的安全问题,因为参数是字符串。
使 String 成为 final 或 Immutable 的另一个原因是它在类加载机制中被大量使用。由于 String 不是 Immutable,攻击者可以利用这一事实,并且可以将加载标准 Java 类(如 java.io.Reader)的请求更改为恶意类 com.unknown.DataStolenReader。通过保持 String final 和不可变性,我们至少可以确保 JVM 加载了正确的类。
由于并发和多线程是 Java 的关键产品,因此考虑 String 对象的线程安全性非常有意义。由于预期 String 将被广泛使用,因此将其设置为不可变意味着没有外部同步,这意味着涉及在多个线程之间共享 String 的代码要干净得多。
这一单一功能使本已复杂、混乱和容易出错的并发编码变得更加容易。因为 String 是不可变的,我们只是在线程之间共享它,所以它会导致代码更具可读性。
现在,当你使一个类不可变时,你事先知道,这个类一旦创建就不会改变。这保证了许多性能优化(例如缓存)的开放路径。String 本身知道我不会改变,所以 String 缓存了它的哈希码。它甚至可以延迟计算哈希码,一旦创建,只需缓存它。
在一个简单的世界中,当您第一次调用任何 String 对象的 hashCode() 方法时,它会计算哈希代码,并且所有后续对 hashCode() 的调用都会返回已计算的缓存值。
这带来了良好的性能提升,因为 String 在基于 hashtable 和 HashMap 等基于哈希的映射中被大量使用。如果不使哈希码不可变和最终,就不可能缓存哈希码,因为它取决于 String 本身的内容。
除了上述好处之外,您还可以依靠另一个优势,因为 String 在 Java 中是最终的。它是在基于哈希的集合(如 HashMap 和 Hashtable)中用作键的最流行的对象之一。
虽然不可变性不是 HashMap 键的绝对要求,但使用不可变对象作为键比使用可变对象要安全得多,因为如果可变对象的状态在 HashMap 中停留期间发生更改,则不可能将其检索回来,因为它的 equals() 和 hashCode() 方法取决于更改的属性。
如果类是不可变的,则当它存储在基于哈希的集合中时,不会有更改其状态的风险。另一个重要的好处,我已经强调过了,是它的线程安全性。由于 String 是不可变的,因此您可以在线程之间安全地共享它,而无需担心外部同步。它使并发代码更具可读性,更不容易出错。
尽管有所有这些优点,但不可变性也有一些缺点,比如它不是没有成本的。由于 String 是不可变的,因此它会生成大量临时使用和抛出对象,这会给垃圾回收器带来压力。Java 设计师已经考虑过这一点,将 String 文字存储在 pool 中是他们减少 String 垃圾的解决方案。
它确实有帮助,但您必须小心在不使用构造函数的情况下创建 String,例如 new String() 不会从 String 池中选择对象。此外,平均而言,Java 应用程序会产生过多的垃圾。此外,将字符串存储在池中也存在与之相关的隐藏风险。字符串池位于 Java 堆的 PermGen 空间中,与 Java 堆相比,它非常有限。
过多的 String 文字会很快填满这个空间,导致 java.lang.OutOfMemoryError: PermGen Space。值得庆幸的是,Java 语言程序员已经意识到了这个问题,从 Java 7 开始,他们已经将字符串池移动到了正常的堆空间,这比 PermGen 空间大得多。
使 String 成为最终版本还有另一个缺点,因为它限制了其可扩展性。现在,您只是无法扩展 String 以提供更多功能,尽管更一般的情况几乎不需要它,但对于那些想要扩展 java.lang.String 类的人来说,它仍然受到限制。
这就是为什么 String 在 Java 中是 final 或 Immutable 的全部原因。虽然我们不知道确切的原因,因为 Oracle 从未发布过他们在 Java 中使 String 类成为最终类的决定,但这 5 个关于缓存、安全性、并发性和性能的实际原因无疑暗示了为什么 String 类在 Java 中变成了 Final 和不可变。
当然,这是 Java 设计人员的决定,但看起来以上几点有助于他们做出这个决定。由于类似的原因,像 Integer、Long、Double、Float 和 Final这样的包装类也是不可变的。