String关键字个人理解

本文深入解析Java中String类的不可变性原理,探讨String、StringBuffer与StringBuilder的区别,以及字符串的创建和存储机制。同时,文章详细解释了字符串常量池的工作原理及其在不同JDK版本中的变化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、String为什么是不可变的

String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
在 Java 8 中,String 内部使用 char 数组存储数据。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    private final byte[] value;

    private final byte coder;
}

value 数组被声明为 privat final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

二、String不可变的好处
  1. 可以缓存 hash 值
    因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
  2. 安全性
    String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。
  3. 线程安全
    String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
三、String, StringBuffer,StringBuilder的区别
  1. 可变与不可变
    String类中使用字符数组保存字符串,如下就是,因为有“final”修饰符,所以可以知道string对象是不可变的。
    private final char value[];
    String 为不可变对象,一旦被创建,就不能修改它的值. . 对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去.
    StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,如下就是,可知这两种对象都是可变的。
    char[] value;
    StringBuffer:是一个可变对象,当对他进行修改的时候不会像String那样重新建立对象 , 它只能通过构造函数来建立, 如: StringBuffer sb = new StringBuffer();
    不能通过赋值符号对他进行付值. , 如 sb = “welcome to here!”;//error
    对象被建立以后,在内存中就会分配内存空间,并初始保存一个null.向StringBuffer中赋值的时候可以通过它的append方法. sb.append(“hello”);

  2. 是否多线程安全
    String中的对象是不可变的,也就可以理解为常量, 显然线程安全 。
    AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
    StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是 线程安全的 。看如下源码:

1   public   synchronized  StringBuffer reverse() {
2       super .reverse();
3       return   this ;
4  }
5  
6   public   int  indexOf(String str) {
7       return  indexOf(str, 0);         //存在 public synchronized int indexOf(String str, int fromIndex) 方法
8  }

StringBuilder并没有对方法进行加同步锁,所以是 非线程安全的 。

  1. StringBuilder与StringBuffer共同点
    StringBuilder与StringBuffer有公共父类AbstractStringBuilder( 抽象类 )。
    抽象类与接口的其中一个区别是:抽象类中可以定义一些子类的公共方法,子类只需要增加新的功能,不需要重复写已经存在的方法;而接口中只是对方法的申明和常量的定义。
    StringBuilder、StringBuffer的方法都会调用AbstractStringBuilder中的公共方法,如super.append(…)。只是StringBuffer会在方法上加synchronized关键字,进行同步。
    最后,如果程序不是多线程的,那么使用StringBuilder效率高于StringBuffer。

效率比较String < StringBuffer < StringBuilder,但是在String S1 =“This is only a”+“simple”+“test”时,String效率最高。

四、字符串的创建和存储机制

在Java语言这种,字符串起着非常重要的作用,字符串的声明和初始化由以下两种情况:

  1. 对于字符串String s1 = new String(“abc”)语句与String s2 = new String(“abc”)语句,存在两种引用对象s1,s2,两个内容相同的字符串对象"abc",他们在内存中的地址是不同的。只要是用到new总会产生新的对象。

  2. 对于String s1 = "abc"和String s2 = "abc"语句,在JVM中存在一个字符串池,其中存有很多String对象,并且可以被共享使用,s1,s2引用同一个常连池中的对象,其中String采用Flyweight设计模式,当创建一个字符串常量时,例如String s=“abc”,会首先在字符串常量池中查找是否存在相同的字符串定义,其判断依据是String对象中的equals方法的返回值。若已经定义,则直接引用其定义,此时不需要创建新的对象;如果没有定义,则首先创建对象,然后把他加入到字符串池中,再将他的引用返回。由于字符串是不可变类,一旦创建好了就不可修改,因此字符串对象可以被共享而且不会引起程序的混乱。

具体而言:

 String s = "abc";    //把abc放在常量区中,在编译时产生。
    String s = "ab"+"c";    //把“ab”+“c”转换成字符串常量“abc”放在常量区中
    String s =new String("anc");    //在运行时把abc放在堆中
例如:
    String s1 = "abc"; //在常量区中存放了一个"abc"的字符串对象
    String s3 = "abc";//s2引用常量区中的对象,因此不会创建新的对象。
    String s4 = new String("abc");//在堆中创建新的对象

为了理解方便,可以吧String s = new String(“abc”)语句的执行分类两个过程;第一个过程就是新建对象的过程,即new String(“abc”);第二个过程就是赋值过程,即String s=。由于第二个过程仅仅创建了一个字符串类型的变量,将一个字符串对象的引用赋值给s,因此这个过程中不会创建新的对象。第一个过程new String(“abc”)调用String类中的构造方法:
public String(String origin) {
//body
}

在调用这个构造函数时,传入一个字符串常量,因此语句new String(“abc”)也就等价于"abc"和new String()两个操作了。若在字符串池中不存在abc,则会创建一个新的字符串常量“abc”,并将其添加到字符串池中;若存在则不创建,然后new String()会在堆中创建一个新的对象,所以s3和s4是指向不同的String对象,地址自然也就不同了。

五、常考知识点
  1. 对于string类型的变量s,赋值语句s=null与s="“是否相同?
    对于赋值语句s=null,其中s是一个字符串类型的引用,它不指向任一个字符串。而赋值语句s=”“中的s是一个字符串类型的引用,它指向另一个字符串(这个字符串的值为”",也就是空字符串),因此两则是不相同的。
  2. 常见的笔试题:
    new String(“abc”)创建了几个对象?
    answer:一个或者两个。如果常量池中原来有字符串"abc",那么只创建一个对象;如果常量池中没有字符串"abc",那么就创建两个对象(这两个对象分别位于字符串常量池和堆中)。
六、Java字符串常量池,运行时常量池,jdk1.7后intern方法的变化

jdk1.7以后, scp从方法区移至了堆区,其实改变的不仅仅是位置

我们看最上面的intern方法的官方注释:

如果常量池中已经存在该字符串,则直接返回常量池中该对象的引用。否则,在常量池中加入该对象,然后 返回引用

这是jdk1.6及之前的,因为当时scp在方法区,也叫非堆区,所以intern的方法,是将该对象加入到常量区中

但是jdk1.7及以后,scp移至了堆区,字符串的创建也在堆区,为了节省开支,intern方法,不再是把该字符串直接加入scp,而是将其地址引用放到scp

例子
1.6下

String s1 = new String("sss111");
s1=s1.intern();
String s2 = "sss111";
System.out.println(s1 == s2);  //true

(若s1=s1.intern();改为s1.intern则为false,由于仅仅做了查询和存放。)

搞清楚这个问题首先要知道intern是做什么的,当我们声明String常量时池往往有以下俩种方式:

直接使用双引号声明出来的String对象会直接存储在常量池中。
如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中

可以在源码中看到(String.intern方法中看到),这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。
例二

public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

打印结果是

  • jdk6 下false false
  • jdk7 下false true

在第一段代码中,先看 s3和s4字符串。String s3 = new String(“1”) + new String(“1”);,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String(“1”)我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。

接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是在 jdk6 在常量池中生成一个 “11” 的对象从而s指向的是堆中1,s2指向常量池中1,因而为false。关键点是 jdk7 中常量池,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的因而为true。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值