java中关于String的知识整理

1、JVM相关知识
JVM的体系结构图:
在这里插入图片描述
Java栈(线程私有数据区):

    每个Java虚拟机线程都有自己的Java虚拟机栈,Java虚拟机栈用来存放栈帧,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

Java堆(线程共享数据区):

   在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配。

方法区(线程共享数据区):

   方法区在虚拟机启动的时候被创建,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造函数和普通方法的字节码内容、还包括在类、实例、接口初始化时用到的特殊方法。在JDK8之前永久代是方法区的一种实现,而JDK8元空间替代了永久代,永久代被移除,也可以理解为元空间是方法区的一种实现。

常量池(线程共享数据区):

    常量池常被分为两大类:静态常量池和运行时常量池。

    静态常量池也就是Class文件中的常量池,存在于Class文件中。

    运行时常量池(Runtime Constant Pool)是方法区的一部分,存放一些运行时常量数据。

重点了解下字符串常量池

    字符串常量池存在运行时常量池之中(在JDK7之前存在运行时常量池之中,在JDK7已经将其转移到堆中)。

    字符串常量池的存在使JVM提高了性能和减少了内存开销。

    使用字符串常量池,每当我们使用字面量(String s=”1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈中)。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。

    使用字符串常量池,每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中复制该对象的副本,然后将堆中对象的地址赋值给引用s。

2、String源码分析


public final class String
 
    implements java.io.Serializable, Comparable<String>, CharSequence
 
{
 
    /** The value is used for character storage. */
 
    private final char value[];
 
 
 
    /** The offset is the first index of the storage that is used. */
 
    private final int offset;
 
 
 
    /** The count is the number of characters in the String. */
 
    private final int count;
 
 
 
    /** Cache the hash code for the string */
 
    private int hash; // Default to 0
 
 
 
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
 
    private static final long serialVersionUID = -6849794470754667710L;
 
 
 
    ........
 
}

首先我们来看看String类,String类是用final修饰的,这意味着String不能被继承,而且所有的成员方法都默认为final方法。

接下来看看String类实现的接口:

   java.io.Serializable:这个序列化接口仅用于标识序列化的语意。

   Comparable<String>:这个compareTo(T 0)接口用于对两个实例化对象比较大小。

   CharSequence:这个接口是一个只读的字符序列。包括length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了改接口。

最后看看String的成员属性:

    value[] :char数组用于储存String的内容。

    offset :存储的第一个索引。

    count :字符串中的字符数。

    hash :String实例化的hashcode的一个缓存,String的哈希码被频繁使用,将其缓存起来,每次使用就没必要再次去计算,这也是一种性能优化的手段。这也是String被设计为不可变的原因之一。

3、关于equals和==
(1)对于==,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean ),则直接比较其存储的"值"是否相等;如果作用于引用类型的变量(String),则比较的是所指向的对象的地址(即是否指向同一个对象)。

(2)equals方法是基类Object中的方法,因此对于所有的继承于Object的类都会有该方法。在Object类中,equals方法是用来比较两个对象的引用是否相等。

(3)对于equals方法,注意:equals方法不能作用于基本数据类型的变量。如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;而String类对equals方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如Double,Date,Integer等,都对equals方法进行了重写用来比较指向的对象所存储的内容是否相等。
例子:

        String s1 = "abc";
        String s2 = "abc";
        System.out.println(s1 == s2);                     
        System.out.println(s1.equals(s2)); 

输出结果:true,true。
说明:s1和s2都指向字符串常量池中的“abc”对象,故s1与s2地址相同(==比较地址是否相同,比较是否为同一个对象)。字符串重写了equals方法,同一个对象返回true,内容相同,也返回true。

        String s3= new String("abc");
        String s4 = "abc";
        System.out.println(s3 == s4);                     
        System.out.println(s3.equals(s4));

输出结果:false,true。
说明:s3指向堆中的“abc”对象。s4指向常量池中的对象。s3与s4不指向同一个对象,故第一个返回false。s3与s4内容相同,输出结果true。

        String s5 = "a"+"b"+"c";
        String s6="abc";
        System.out.println(s5== s6);                     
        System.out.println(s5.equals(s6));

输出结果:true,true。
说明:s5在编译期间会优化为 s5=”abc”。此时和上面的s1和s2代码逻辑一样。输出结果和s1 s2相同。

        String s7 = "ab";
        String s8 = "abc";
        String s9 = s7+"c";

        System.out.println(s9 == s8);
        System.out.println(s9.equals(s8));

输出结果:false,true。
说明:s8指向字符串常量池中的“abc”对象,s9语句会在编译期间,优化为stringBuild(s7).append(“c”)。也就是s9最终指向stringBuild.toString()方法返回的“abc”对象(在堆上)。故s9!=s8;内容相同,所以equals返回true。

        String s10 = "wenwei";
        final String s11 = "wen";
        String s12 = s11+"wei";
        System.out.println(s10==s12);

输出结果:true。
说明:s10指向常量池中的“wenwei”对象。由于s11字符串被final修饰,也就是不可改变常量,s12语句在编译的时候,会优化成s12=”wen”+”wei”。此时s10和s12和s5 s6指向逻辑相同。

        String s13 = new String("zhang");
        s13.intern();
        String s12 = "zhang";
        System.out.println(s13 == s12);

输出结果:false。
说明:intern(),方法在jdk1.7中执行逻辑是:先检查常量池是否有改字符串对象;如果没有则将s13在堆中的对象引用,赋值给常量池中的变量,并返回本引用,如果存在则直接返回引用。s13代码会在类加载解析阶段,在常量池中生成“zhang”对象,s13.intern()方法,检查常量池存在,不会将s13堆中的对象引用赋值到常量池中;s12语句,指向的是常量池中的对象,s13指向堆中新建的对象。故输出结果为false。


        String s14 = new String("1") + new String("1");
        s14.intern();
        String s15 = "11";
        System.out.println(s14 == s15);

输出结果:true。
说明:s14语句会在堆中产生一个“11”对象,s14.intern()语句,会将对堆中的“11”对象引用记录的常量池中(jdk1.7以后)。s15语句指向后,s15的值即为常量池中引用的值,也指向堆中的“11”对象。故输输出值为true。

代码优劣
//代码片段1
        String result1="";
        for(int i =0;i<100;i++){
            result1+=i;
        }
        System.out.println(result1);

//代码片段2
        StringBuilder sb = new StringBuilder();
        for(int i =0;i<100;i++){

            sb.append(i);
        }
        System.out.println(sb.toString());

说明:
片1中的result+=i语句编译器编译会优化为,reuslt =newStringBuilder(result).append(i).toString()。
这样片段1代码执行100次循环,会创建100个StringBuilder对象,和100个result对象(toString()方法)。代码片段2执行100次循环,就创建了1个StringBuilder和1个String对象。故代码片段2内存占用更少,性能最佳。注意,在java中字符串的+操作,如果连接的不是常量字符串或final修饰的字符变量,编译后都会编译成StringBuilder().append()的形式。
4、String被设计成不可变和不能被继承的原因

    String是不可变和不能被继承的(final修饰),这样设计的原因主要是为了设计考虑、效率和安全性。

字符串常量池的需要:

    只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段。

String对象缓存HashCode:

    上面解析String类的源码的时候已经提到了HashCode。Java中的String对象的哈希码被频繁地使用,字符串的不可变性保证了hash码的唯一性。

安全性

    首先String被许多Java类用来当参数,如果字符串可变,那么会引起各种严重错误和安全漏洞。

    再者String作为核心类,很多的内部方法的实现都是本地调用的,即调用操作系统本地API,其和操作系统交流频繁,假如这个类被继承重写的话,难免会是操作系统造成巨大的隐患。

    最后字符串的不可变性使得同一字符串实例被多个线程共享,所以保障了多线程的安全性。而且类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载。

5、关于String的常见面试题解析

 public static void testString1(){
        //此句代码会产生两个对象,一个在编译类加载(解析)阶段产生,一个在运行时产生。
        String s1= new String("weiwei");
        //________________________________________  

        //先在常量池创建对象
        String s2 = "zhang";
        //创建一个对象
        String s3 = new String("zhang");
        //________________________________________

        //创建一个对象
        String s4 = "a"+"b"+"c";
        //________________________________________

        String s5 = "helloworld";
        final String s6 = "hello";
        //没有创建对象
        String s7 = s6+"world";

        //________________________________________ 

        String s8 = "how are you";
        String s9 = "how are";
        //创建一个对象
        String s10 = s7+"you";

    }

(1)执行s1语句的时候,会产生两个对象,第一个对象是在类加载的解析阶段,会将字符串“weiwei”在常量池中创建(首先在常量池查找,此处假设其他地方的代码中没有此字符串常量),第二个对象时在代码运行期间,在堆上创建内容为“weiwei”的对象。
(2)执行s2,s3语句,首先s2语句,在加载阶段和s1语句加载相同,常量池不存在,会在常量池中创建对象。s3语句,在类加载解析阶段,由于s2语句已经确保在常量池中存在“zhang”对象,故不会产生对象,在运行期间,new关键字,会在堆上产生一个对象。故s3语句只会生成一个对象。
(3)执行s4语句,首先java文件编译的时候,会将s4语句优化为 String s4 = “abc”;故s4语句,会在类的加载解析阶段,创建一个“abc”对象存放在常量池中,在运行期间,不会创建新的对象,而是将常量池“abc”的引用,赋值给s4。 
(4)执行s5,s6,s7语句,首先s5语句,同s4语句相同,会在常量池中产生一个对象。s6,s7语句在编译期间会进行优化,s6直接优化成字符串字面常量,s7会优化成s7=”hello”+”world”也即是 s7=”helloworld”;s7语句不会新建对象。
(5)执行s8,s9,s10,s8和s9同s2语句执行逻辑相同。s10在编译期间,会将+号编译成StringBuild.append(),在装载阶段“you”会在字符串常量池产生一个对象;由于StringBuild会产生一个对象,故s10语句会产生两个对象。

参考文章:
https://blog.csdn.net/qq_34490018/article/details/82110578
https://blog.csdn.net/qq_22494029/article/details/79306182

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值