Java中的String类真的不可变吗?java面试常见问题

简介: Java中的String类真的不可变吗?java面试常见问题

其实在Java中,String类被final修饰,主要是为了保证字符串的不可变性,进而保证了它的安全性。那么final到底是怎么保证字符串安全性的呢?接下来就让我们一起来看看吧。


一. final的作用



1.  final关键词修饰的类不可以被其他类继承,但是该类本身可以继承其他类,通俗地说就是这个类可以有父类,但不能有子类。


final class MyTestClass1 {
    // ...
}


2.  final关键词修饰的方法不可以被覆盖重写,但可以被继承使用。

class MyTestClass2 {
    final void myMethod() {
        // ...
    }
}


3.  final关键词修饰的基本数据类型被称为常量,只能被赋值一次。  

class MyTestClass3 {
    final int number = 100;
}

4.  final关键词修饰的引用数据类型变量,其值为地址值,该地址值不能改变,但该地址对应的数据对象可以被改变(其实这一点就和我们今天要说的内容有关了,在后面我会结合案例跟大家重点解释,大家一定要打起精神仔细学习哦)。


5.  final关键词修饰的成员变量,需要在创建对象前就赋值,否则会报错(即需要在定义时直接赋值)。


综上所述,我们可以知道,final在Java中是一个非常有用的关键字,主要可以提高我们代码的稳定性和可读性。当然,我们今天要讲解的重点是被final修饰的String类,所以接下来我们还是把目光转回到String身上来,看看String都有哪些特性吧!


二. 被final修饰的String类



为了让大家更好地理解String的不可变性,首先我要给各位简要地讲一下String的源码设计。从下面的这段源码中,我们可以搞清楚很多底层的设计思路,接下来就请大家跟着我一起来看看String的核心源码吧。

/**
 * ......其他略......
 *
 * Strings are constant; their values cannot be changed after they
 * are created. String buffers support mutable strings.
 * Because String objects are immutable they can be shared. For example:
 *
 * ......其他略......
 *
 */
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    ......


我先把上面的源码及其注释,给大家作一个简单的解释:

● final:请参考第1小节对final特点的介绍;

● Serializable:用于序列化;

● Comparable<String>:默认的比较器;

● CharSequence: 提供对字符序列进行统一、只读的操作。


从这段源码及其注释中,我们可以得到下面这些结论:

● String类用final关键字修饰,说明String不可被继承;

● String字符串是常量,字符串的值一旦被创建,就不能被改变;

● String字符串缓冲区支持可变字符串;

● String对象是不可变的,它们是可以被共享的。


三. String的不可变性



在学习了上面的这些核心源码之后,接下来,我们可以通过一个案例来实践验证一番,看看String字符串的内容到底能不能改变。这里有个代码案例,如下图所示:

image.png


在上述的案例结果中,大家可以看出,s的内容竟然发生了改变?!但我们不是一直说String是不可变的吗?这是咋回事?大家先别急,我们继续往下看。


要想弄明白这个问题,我们首先得知道一个知识点:引用和值的区别!


在上面的代码中,我们先是创建了一个 "yiyige" 为内容的字符串引用s,如下图:

image.png


s其实先是指向了value对象,而value对象又指向了存储 "y,i,y,i,g,e" 字符的字符数组。但因为value被final修饰,所以value的值不可被更改。因此,上面代码中改变的其实是s的引用指向,而不是改变了String对象的值!


换句话说,上面实例中s的值,其实只是value的引用地址,并不是String的内容本身。当我们执行 s = "yyg" 语句时,Java会创建一个新的字面量对象 "yyg",而原来的 "yiyige" 字面量对象其实依然存在于内存的intern缓存池中。


在这里,String对象的改变,实际上是通过内存地址的“断开-连接”变化来完成的。在这个过程中,原字符串中的内容并没有发生任何的改变。String s = "yiyige" 和 s = "yyg"这两行代码,实质上是开辟了2个内存空间,s只是由原来指向 "yiyige" 变为指向 "yyg" 而已,而其原来的字符串内容,是没有发生改变的,如下图所示。

ea5ab47f001de067a0f8f55bb25f839f.png

因此,我们在以后的开发中,如果要经常修改字符串的内容,请尽量少用String!因为如果字符串的指向经常的“断开-连接”,就会大大降低性能,我建议大家使用StringBuilder 或 StringBuffer 进行替换。


我们继续把上面的代码深入地分析一下。在Java中,因为数组也是对象, 所以value中存储的也只是一个引用,它指向一个真正的数组对象。在执行了String s = “yiyige”; 这句代码之后,真正的内存布局应该是下图这样的:

image.png

因为value是String封装的字符数组,value中所有的字符都属于String这个对象。而由于value是private的,没有提供setValue等公共方法来修改这个value值,所以我们在String类的外部是无法修改value值的,也就是说字符串一旦初始化就不能再被修改。


此外,value变量是final修饰的,也就是说在String类内部,一旦这个值初始化了,value这个变量所引用的地址就不会改变了,即一直引用同一个对象。正是基于这一层,我们才说String对象是不可变的对象。


所以String的不可变,其实是指value在栈中的引用地址不可变,而不是说常量池中value字符数组里的数据元素不可变。也就是说,value所引用的数组对象里的内容,其实是可以发生改变的。


那么我们又如何改变它呢?这就要通过反射来消除String类对象的不可变性啦!


四. String真的不可变吗?



在上述内容中,我们重点给大家解释了String字符串的可变性。现在大家应该已经知道了,String字符串的内容其实是可变的,不可改变的只是String字符串的对象地址。那么我们到底该怎么让String字符串的内容发生改变呢?在上述我们给大家提到了反射,接下来我们就来看看如何通过反射改变String字符串的内容吧。代码案例如下所示:

try {
    String str = "yyg";
    System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str));
    Class stringClass = str.getClass();
    //获取String类中的value属性
    Field field = stringClass.getDeclaredField("value");
    //设置私有成员的可访问性,进行暴力反射
    field.setAccessible(true);
    //获取value数组中的内容
    char[] value = (char[]) field.get(str);
    System.out.println("value=" + Arrays.toString(value));
    value[1] = 'z';
    System.out.println("str=" + str + ", 唯一性hash值=" + System.identityHashCode(str));
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}


执行结果如下图所示:

image.png

从上面的结果中我们可以看到,String字符串的字符数组,通过反射进行修改后,字符串的“内容”真的发生了变化!


并且我们又利用底层的java.lang.System#identityHashCode()方法(不管是否重写了hashCode方法),来获取到了该字符串对象的唯一哈希值,该方法获取的hash值与hashCode()方法是一样的。


从结果中,我们可以看到两个字符串的唯一hash值是一样的,这就证明字符串的引用地址没有发生改变。


所以这就说明,我们并不是像之前那样创建了一个新的String字符串,而是真的改变了原有String的内容。


这个代码案例进一步证明了我们上面的结论:String字符串的不可变,指的其实是value对象在栈中的引用地址不可变,而不是说常量池中value里的数据元素不可变!简单地说,就是String字符串的内容其实是可以改变的,不能改表的是它的对象地址而已。

941dd23c29b6da828a97d8d78e27b681.png


所以这也就是我们上述所说的final的作用之一:final关键词修饰的引用数据类型的变量,其值为地址值,地址值不能改变,但是地址内的数据对象可以被改变!


五. 总结



至此,我们就把今天的面试题分析完了,现在你明白了吗?最后我再来给大家总结一下今天的重点内容吧:


1.  为什么要用final修饰java中的String类呢?

核心:因为它确保了字符串的安全性和可靠性。


2.  java中的String真的不可变吗?

核心:String字符串的内容其实是可变的,但要通过特殊手段进行实现,不可改变的是String字符串对象的地址。


3.  如何消除String类对象的不可变性?

核心:利用反射来消除String类对象的不可变性。


4.  如果想要保证String的不可变要注意哪些?

● 首先,将 String 类声明为 final类型。这意味着String类是不可被继承的,防止程序员通过继承重写String类的某些方法,使得String类出现“可变的”的情况;

● 然后,重要的字符数组value属性,要被private 和 final修饰。它是String的底层数组,用于存贮字符串内容。又因为数组是引用类型,所以只能限制引用不被改变,也就是说数组元素的值是可以改变的,这在上面的案例中已经证明过了;


● 接着,所有修改的方法都返回新的字符串对象,保证修改时不会改变原始对象的引用;

● 最后,不同的字符串对象都可以指向缓存池中的同一个字符串字面量。


当然Java中的String类使用final修饰”这个概念非常重要,因为它确保了字符串的安全性和可靠性。但是我们也要清楚不可改变的只是它的地址,而不是它的内容,它的内容是可以利用反射来改变的!只不过在一般的描述中,大家都会说String内容不可改变,毕竟很多时候是不允许利用反射这种特殊的功能去进行这样的操作的。


来源:Java中的String类真的不可变吗?java面试常见问题 - 可爱的小锋 - 博客园 (cnblogs.com)

相关文章
|
12天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
36 14
|
23天前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
18天前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
39 8
|
17天前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
25 6
|
18天前
|
Java 开发者
在 Java 中,一个类可以实现多个接口吗?
这是 Java 面向对象编程的一个重要特性,它提供了极大的灵活性和扩展性。
35 1
|
22天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
23天前
|
Java Android开发
Eclipse 创建 Java 类
Eclipse 创建 Java 类
24 0
|
3月前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
40 0
java基础(13)String类
|
15天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
2月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
62 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性

热门文章

最新文章