- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
其实在Java中,String类被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 都有哪些 特性 吧! 。
为了 让大家 更好 地 理解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: 提供对字符序列进行统1、只读的操作.
从这段源码及其注释中,我们可以得到 下面 这 些 结论:
● String类用final关键字修饰,说明String不可被继承; 。
● String字符串是常量,字符串的值一旦被创建,就不能被改变; 。
● String字符串缓冲区支持可变字符串; 。
● String对象是不可变的,它们是可以被共享的.
在学习了上面的这些核心源码之后,接下来,我们 可以通过一个案例 来实践验证一番,看看String 字符串的内容 到底能不能 改变 。 这里有个 代码案例 , 如 下 图所示:
。
在上述的案例结果中,大家可以看出, s的内容竟然发生了改变 ?!但我们不是一直说String是不可变的吗?这是咋回事? 大家先别急,我们继续往下看.
要想 弄 明白这个问题,我们首先得 知道 一个 知识 点: 引用和值的区别 ! 。
在上面的代码中 , 我们先是创建了一个 "yiyige" 为内容的字符串引用s ,如下图:
。
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" 而已,而其原来的字符串内容,是没有 发生 改变的,如下图所示.
因此,我们在以后的开发中,如果要经常修改字符串的内容,请尽量少用String ! 因为 如果 字符串的指向 经常的 “断开-连接”, 就 会大大降低性能, 我 建议 大家 使用StringBuilder 或 StringBuffer 进行替换 .
我们继续把上面的代码深入地分析一下。 在Java中,因为数组也是对象, 所以value中存储的也只是一个引用,它指向一个真正的数组对象。在执行了String s = “yiyige”; 这句代码之后,真正的内存布局应该是下图这样的 :
。
因为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字符串的内容吧。代码案例如下所示:
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(); }
执行结果如下图所示:
。
从上面的结果中我们可以看到, String字符串的字符数组 , 通过反射进行修改后,字符串的“内容”真的发生了变化! 。
并且我们又利用底层的 java.lang.System#identityHashCode() 方法 (不管是否重写了hashCode方法) , 来获取到了 该字符串 对象的唯一哈希值,该方法获取的hash值与hashCode()方法是一样的.
从结果中, 我们可以看到两个字符串的唯一hash值是一样的, 这就 证明字符串 的 引用地址没有发生改变 .
所以 这就说明, 我们并不是像之前那样创建了一个新的String字符串,而是真的改变了 原有 String的内容 .
这个代码案例进一步 证明了我们上面的结论: String 字符串 的不可变 , 指的 其实 是value 对象 在栈中的引用地址不可变,而不是说常量池中 value里 的数据元素不可变! 简单地说, 就是String字符串的内容其实是 可以改 变的 , 不能改表的是它的对象地址 而已.
。
至此,我们就把今天的面试题分析完了,现在你明白了吗? 最后 我再 来给大家总结一下今天的重点 内容 吧:
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面试常见问题的文章就讲到这里了,如果你想了解更多关于Java中的String类真的不可变吗?java面试常见问题的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
为什么禁用类型像 type t = A of int | B of string * mutable int 虽然允许此类类型: type t = A of int | B of string * i
我正在寻找一种类似结构的数据结构,我可以从中创建多个实例并具有某种类型提示而不是不可变的。 所以我有这样的东西: class ConnectionConfig(NamedTuple): nam
我需要转到引用的结构: class SearchKnot { var isWord : Bool = false var text : String = "" var to
如sec 10.4.3中所述 当控制进入执行时,执行以下步骤 功能对象F(调用者)中包含的功能代码的上下文 提供thisArg,而调用方提供argumentsList: 如
i make a game that start display Activity indicator And activity indicator bottom display UiLable wi
编辑:我在这里不断获得支持。只是为了记录,我认为这不再重要。自从我发布它以来我就不再需要它了。 我想在 Scala 中执行以下操作... def save(srcPath: String, destP
使用可变对象作为 Hashmap 键是一种不好的做法吗?当您尝试使用已修改足以更改其哈希码的键从 HashMap 中检索值时,会发生什么? 例如,给定 class Key { int a; /
如果您在Kotlin中访问List类型的Java值,则将获得(Mutable)List!类型。 例如。: Java代码: public class Example { public stati
我编写了 str 类(内置)的以下扩展,以便执行以下操作:假设我有字符串 "Ciao" ,通过做"Ciao" - "a"我想要的结果是字符串 "Cio" 。这是执行此操作的代码,并且运行良好: cla
使用可变对象作为 Hashmap 键是一种不好的做法吗?当您尝试使用已修改足以更改其哈希码的键从 HashMap 中检索值时,会发生什么? 例如,给定 class Key { int a; /
我正在为我的公司设计一个数据库来管理商业贷款。每笔贷款都可以有担保人,可以是个人或公司,在借款业务失败时作为财务支持。 我有 3 个表:Loan、Person 和 Company,它们存储明显的信息。
我使用二进制序列化从 C# 类中保存 F# 记录。一切正常: F#: type GameState = { LevelStatus : LevelStatus
import javax.swing.JOptionPane; public class HW { public static void main(String[] args) { Strin
使用 flatbuffer mutable 有多少性能损失? 是否“正确”使用 FlatBuffers 来拥有一个应该可编辑的对象/结构(即游戏状态) 在我的示例中,我现在有以下类: class Ga
std::function create_function (args...) { int x = initial_value (args...); return [x] () mut
我需要在 for 循环中找到用户输入的字符。我通常会这样做 如果(句子[i] == 'e') 但是因为在这里,'e' 将是一个单字母字符变量,我不知道如何获取要比较的值。我不能只输入 if (sent
我有一个这样的算法: let seed: Foo = ... let mut stack: Vec = Vec::new(); stack.push(&seed); while let Some(ne
这个问题可能看起来非常基础,但我很难弄清楚如何做。我有一个整数,我需要使用 for 循环来循环整数次。 首先,我尝试了—— fn main() { let number = 10; // An
如果我有以下结构: struct MyStruct { tuple: (i32, i32) }; 以及以下函数: // This will not compile fn function(&mut s
我希望在每个 session 的基础上指定列的默认值。下面的脚本不起作用,但描述了我想如何使用它。我目前使用的是 MySQL 5.5.28,但如果需要可以升级。 CREATE TABLE my_tbl
我是一名优秀的程序员,十分优秀!