【Java】String类的理解及字符串常量池

2年前 (2022) 程序员胖胖胖虎阿
265 0 0

【Java】String类的理解及字符串常量池博客主页: XIN-XIANG荣
系列专栏:【Java SE】
一句短话: 难在坚持,贵在坚持,成在坚持!

文章目录

  • 一. String类简介
    • 1. 介绍
    • 2. 字符串构造
  • 二. 字符串常量池(StringTable)
    • 1. 思考?
    • 2. 介绍和分析
    • 3. intern方法
  • 三. 面试题:String类中两种对象实例化的区别
  • 四. 字符串的不可变性

一. String类简介

1. 介绍

字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。

Java的String类在lang包里,java.lang.String是java字符串类,包含了字符串的值和实现字符串相关操作的一些方法;java.lang包里面的类都不需要手动导入,是由程序自动导入。

String表示字符串类型,属于引用数据类型, 内部并不存储字符串本身 ;Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。

在String类的实现源码中,String类实例变量如下:

【Java】String类的理解及字符串常量池

public static void main(String[] args) {
        // s1和s2引用的是不同对象 s1和s3引用的是同一对象
        String s1 = new String("hello");
        String s2 = new String("world");
        String s3 = s1;
        System.out.println(s1.length()); 
        // 获取字符串长度---输出5
        System.out.println(s1.isEmpty()); 
        // 如果字符串长度为0,返回true,否则返回false
}

【Java】String类的理解及字符串常量池

字符串存储在字符串常量池中,后文中给出具体的理解与分析。

2. 字符串构造

String类提供的构造方式非常多,常用的就以下三种:

public static void main(String[] args) {
        // 使用常量串构造
        String s1 = "hello bit";
        System.out.println(s1);
    
        // 直接newString对象
        String s2 = new String("hello bit");
        System.out.println(s1);
    
        // 使用字符数组进行构造
        char[] array = {'h','e','l','l','o','b','i','t'};
        String s3 = new String(array);
        System.out.println(s1);
    }

二. 字符串常量池(StringTable)

1. 思考?

通过下面的代码,分析和思考字符串对象的创建和字符串常量池。

public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        String s3 = new String("hello");
        String s4 = new String("hello");
        System.out.println(s1 == s2); // true
        System.out.println(s1 == s3); // false
        System.out.println(s3 == s4); // false
    }

执行及调试结果:

【Java】String类的理解及字符串常量池

思考为什么执行结果中,创建的字符串的地址是一样的,使用new String的时候比较两个对象的地址却不一样,直接使用字符串常量(“ ”)进行赋值的两个对象比较是相同的;

为什么s1和s2引用的是同一个对象,而s3和s4不是呢?

2. 介绍和分析

在Java程序中,类似于:1, 2, 3,3.14,“hello”等字面类型的常量经常频繁使用,为了使程序的运行速度更快、 更节省内存,Java为8种基本数据类型和String类都提供了常量池。

为了节省存储空间以及程序的运行效率,Java中引入了:

  • Class文件常量池:每个.Java源文件编译后生成.Class文件中会保存当前类中的字面常量以及符号信息
  • 运行时常量池:在.Class文件被加载时,.Class文件中的常量池被加载到内存中称为运行时常量池,运行时常量池每个类都有一份
  • 字符串常量池(StringTable) :字符串常量池在JVM中是StringTable类,实际是一个固定大小的HashTable(一种高效用来进行查找的数据结构),不同JDK版本下字符串常量池的位置以及默认大小是不同的:
JDK版本 字符串常量池位置 大小设置
Java6 (方法区)永久代 固定大小:1009
Java7 堆中 可设置,没有大小限制,默认大小:60013
Java8 堆中 可设置,有范围限制,最小是1009

对1中的代码进行分析:

  1. 直接使用字符串常量进行赋值
public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";
        System.out.println(s1 == s2); // true
}

【Java】String类的理解及字符串常量池

当字节码文件加载时,字符常量串“hello”已经创建好了,并保存在字符串常量池中,

当直接使用常量串赋值的时候( String s1 = “hello”;)会优先从字符串常量池找,找到了就将该字符串引用赋值给要赋值的对象(s1和s2);

所以s1和s2内放的都是字符串常量池中“hello”字符串所创建对象的引用,是相同的。

  1. 通过new创建String类对象

【Java】String类的理解及字符串常量池

【Java】String类的理解及字符串常量池

使用new来创建String对象,每次new都会新创建一个对象,每个对象的地址都是唯一的,所以s3和s4的引用是不相同的。

使用常量串创建String类型对象的效率更高,而且更节省空间。用户也可以将创建的 字符串对象通过 intern 方式添加进字符串常量池中。

3. intern方法

intern 是一个native方法(Native方法指:底层使用C++实现的,看不到其实现的源代码);

该方法的作用是手动将创建的String对象添加到常量池中。

public static void main(String[] args) {
        char[] ch = new char[]{'a', 'b', 'c'};
        String s1 = new String(ch); // s1对象并不在常量池中
        //s1.intern(); 
        //intern调用之后,会将s1对象的引用放入到常量池中
        String s2 = "abc"; // "abc" 在常量池中存在了,s2创建时直接用常量池中"abc"的引用
        System.out.println(s1 == s2);

}

// 输出false
// 将上述方法打开之后,就会输出true

三. 面试题:String类中两种对象实例化的区别

JDK1.8中

  1. String str = “hello”;
    只会开辟一块堆内存空间,保存在字符串常量池中,然后str共享常量池中的String对象
  2. String str = new String(“hello”);
    会开辟两块堆内存空间,字符串"hello"保存在字符串常量池中,然后用常量池中的String对象给新开辟的String对象赋值。
  3. String str = new String(new char[]{‘h’, ‘e’, ‘l’, ‘l’, ‘o’})
    先在堆上创建一个String对象,然后利用copyof将重新开辟数组空间,将参数字符串数组中内容拷贝到String对象中

四. 字符串的不可变性

String是一种不可变对象. 字符串中的内容是不可改变。字符串不可被修改,是因为:

  1. String类在设计时就是不可改变的,String类实现描述中已经说明了

【Java】String类的理解及字符串常量池

【Java】String类的理解及字符串常量池

String类中的字符实际保存在内部维护的value字符数组中,该图还可以看出:

  • String类被final修饰,表明该类不能被继承
  • value被修饰被final修饰,表明value自身的值不能改变,即不能引用其它字符数组,但是其引用空间中的内容可以修改。
  • 字符串真正不能被修改的原因是,存储字符串的value是被private修饰的,只能在String类中使用,但String中并没有提供访问value的公开方法

网上有些人说:字符串不可变是因为其内部保存字符的数组被final修饰了,因此不能改变;这种说法是错误的,不是因为String类自身,或者其内部value被final修饰而不能被修改; final修饰类表明该类不想被继承,final修饰引用类型表明该引用变量不能引用其他对象,但是其引用对象中的内 容是可以修改的。

  1. 所有涉及到可能修改字符串内容的操作都是创建一个新对象,改变的是新对象

比如 replace 方法:

【Java】String类的理解及字符串常量池

注意:

尽量避免直接对String类型对象进行修改,因为String类是不能修改的,所有的修改都会创建新对象,效率非常低下。

public static void main(String[] args) {
        String str = "";
        for (int i = 0; i < 100; i++) {
            str += i;
        }
        System.out.println(str);
}

执行结果:

【Java】String类的理解及字符串常量池

这种方式不推荐使用,因为其效率非常低,中间创建了好多临时对象。

下图是上面代码的汇编,可以看到每一次循环都需要重新创建一个StringBuuilder对象,效率非常低。

【Java】String类的理解及字符串常量池

【Java】String类的理解及字符串常量池

  1. 创建一个StringBuild的对象,假设为temp
  2. 将str对象append(追加)temp之后
  3. 将"world"字符串append(追加)在temp之后.
  4. temp调用其toString方法构造一个新的String对象

【Java】String类的理解及字符串常量池

  1. 将新String对象的引用赋直给str

将上述汇编过程转化为类似代码如下:

public static void main(String[] args) {
        String str = "";
        for (int i = 0; i < 100; i++) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(str);
            stringBuilder.append(i);
            str = stringBuilder.toString();
        }
        System.out.println(str);
}

这里可以将上述代码优化一下进行对比,只创建一次StringBuilder即可:

public static void main8(String[] args) {
        String str = "";
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(str);
        for (int i = 0; i < 100; i++) {
            stringBuilder.append(i);
        }
        System.out.println(stringBuilder);
}

通过下面的代码对比String和StringBuildder、StringBuffer效率上的差异:

ublic static void main(String[] args) {
        long start = System.currentTimeMillis();
        String s = "";
        for(int i = 0; i < 10000; ++i){
            s += i;
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    
        start = System.currentTimeMillis();
        StringBuffer sbf = new StringBuffer("");
        for(int i = 0; i < 10000; ++i){
            sbf.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println(end - start);
    
        start = System.currentTimeMillis();
        StringBuilder sbd = new StringBuilder();
        for(int i = 0; i < 10000; ++i){
            sbd.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println(end - start);
}

执行结果:

【Java】String类的理解及字符串常量池

可以看出在对String类进行修改时,效率是非常慢的,因此:尽量避免对String的直接需要,如果要修改建议尽量 使用StringBuffer或者StringBuilder。

版权声明:程序员胖胖胖虎阿 发表于 2022年9月30日 下午10:32。
转载请注明:【Java】String类的理解及字符串常量池 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...