Java学习笔记 String、StringBuffer与StringBuilder(一)

简介: Java学习笔记 String、StringBuffer与StringBuilder(一)

一、String字符串


1.1、认识String类



String:表示为字符串,可以使用字符串字面值与类实例来给该类进行赋值,底层是使用fina char[] value(常量字符数组),并且String类是final常量类,无法被继承,无参构造是创建空的字符串。


//常量类:无法被继承
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    //底层使用char型数组保存字符串
    private final char value[];
    ....
}


特性说明:String代表不可变的字符序列,String对象一旦创建了,对String对象的任何改变都不回影响到原对象,任何更改对象内容的操作都会生成新的对象。


看下String类中的几个更改操作方法:


//合并
public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}
//替换
public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) {
                buf[j] = val[j];
            }
            while (i < len) {
                char c = val[i];
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
            return new String(buf, true);
        }
    }
    return this;
}


可以看到执行这些方法,最后都返回了新的String对象。

这也就是为什么String被称为不可变的字符序列。



1.2、String两种赋值方式(=、new)


首先看几种赋值方式:


public static void main(String[] args) {
    //两种创建字符串方式:
    //方式一:通过在常量池中创建对象返回引用给s
    String s = "changlu";
    //方式二:通过new实例,将引用返回给s1
    String s1 = new String("changlu");
    //+来合并字符串(也是有区别的)
    String s2 = "chang"+"lu";
    String s3 = "" + s;
}



方式一:对于直接赋值字面值的s来说,会在常量池中创建一个字符串并将其引用地址传给s。

方式二:对于使用new创建字符串的,会先判断常量池中是否有此字符串,①若没有,会创建两个对象(常量池及堆),先在常量池中创建一个字符串,将字符串引用传递给堆中对象中的value,接着将堆中对象的引用地址传递给s1;②若有,创建一个对象(堆),首先将常量池中找的该字符串将其引用传递给堆中对象的value,接着再将堆中创建的对象引用传递给s1。

例题情况:由于之前已在常量池中创建了一个字符串"changlu",所以只在堆中创建了一个对象,接着步骤与②一致。


认识下字符串常量池(Constant Pool Table)


字符串常量池出现原因:字符串的分配与其他对象相同,需要消耗高昂的时间与空间,对于字符串的使用是很频繁的,JVM为了提高性能和减少内存的开销引入了字符串常量池,目的是为了共享字符串,相当于给字符串开辟了一个空间,每当创建字符串时会先去常量池中找是否有已经创建相同的字符串,若有直接返回,从而省去了创建的过程(但是有一个遍历查找的过程)。


一旦我们创建字符串时,首先会去检查字符串常量池中是否存在要创建的字符串,若是存在会直接返回该字符串的引用;若不存在才会去创建。

重复赋值区别:


String s = "changlu";//首先在常量池中创建该字符串,再返回引用。(有创建过程)
String s1 = "changlu";//在常量池中发现了已创建的字符串返回引用。(无创建过程)


注意点:对于常量池中的字符串在创建之后无法更改,若是使用"+"字符串(在常量区找合并的字符串是否存在),若不存在,则会在字符串常量区中重新再创建一个字符串。


关于字符串常量区所处的位置:


JDK1.6:字符串常量池在方法区中(具体实现:永久代)。

JDK1.7:字符串常量池在堆中。

JDK1.8:字符串常量池在方法区(具体实现:元空间)。


1.3、字符串赋值的各类情况


我们先看一道题:


public static void main(String[] args) {
    String s = "changlu";
    String s1 = new String("changlu");
    String s2 = "chang" + "lu";
    String s3 = "" + s;
    //判断是否相同
    System.out.println(s == s1);//false
    System.out.println(s == s2);//true
    System.out.println(s == s3);//false
    System.out.println(s1 == s2);//false
    System.out.println(s1 == s3);//false
    System.out.println(s2 == s3);//false
}


我们将所有字符串都进行了逐一比较,对于这种题很容易混淆不清,实际上我们只需要知道他们对应引用的地址是在哪里创建之后,对于这种问题就很轻松了。


首先记几个关键点:


对于使用=直接赋值字符串的(非拼接),一定是使用的常量区的引用。

对于new出来的字符串对象,一定是在堆中的引用。

常量与常量的拼接结果在常量池。(注意常量池不会有相同内容的常量)

常量与变量的拼接结果在堆中。(在堆中新创建一个对象)

分析过程:


String s = "changlu";//在常量区创建字符串对象"changlu",在常量区中地址为0x11,返回引用给s。==>s=0x11
String s1 = new String("changlu");//s1引用地址的对象中的底层数组value引用地址0x11,而s1存放的是堆中创建对象的引用地址为0x22。==>s1=0x22
String s2 = "chang" + "lu";//对于常量与常量拼接,在编译器完成,所以会直接去常量区找拼接后的字符串是否存在,这里是找到将地址0x11引用给s2。==>s2=0x11
String s3 = "" + s;//常量与变量(字符串对象实例)拼接,会在堆中创建新的对象地址为0x36,该对象中的value则会引用常量区的字符串,由于也已经存在了所以将0x11引用给value。==>s3=0x36


对于引用数据类型进行==判断,是比较它们的引用地址,根据上面分析就能够很快得出程序最后的执行结果了。


补充点:


若是常量与常量拼接,在编译期就能够确定了,jvm会直接在此期间就优化如"chang"+"lu"为"changlu",然后直接拿"changlu"去常量池中找,若有直接返回引用地址,没有则在常量区创建并返回引用地址。

若是常量与变量拼接,在编译期无法确定变量的引用地址,如""+s中的s无法确定,那么就不能在编译期优化字符串,只有在程序运行期间动态分配地址并将新地址赋值给栈中的变量。

对于补充点1中的实际验证可以去参考文章2的链接中查看。



1.4、认识intern()方法


intern()方法是String类中方法:


public native String intern();


该方法是一个本地方法,底层调用c++的方法实现。

由于知识量有限,所以直接说结论不往深处探讨:使用该方法会返回指向常量池的地址。


public static void main(String[] args) {
    String str = new String("changlu");
    String str1 = "changlu";
    System.out.println(str == str1);//false
    System.out.println(str1 == str.intern());//true
}


str指向堆中的开辟的地址;

str1指向常量区的地址;

str1.intern()指向堆中实例的value对应的地址(即常量区地址);


1.5、常用方法


字符串与基本数据类型及包装类转换方法


字符串 => 基本数据类型及包装类:Integer包装的parseInt(String s);,其他包装类类似parseXXX(String s);


基本数据类型、包装类 => 字符串:调用不同的String类静态方法String valueOf(int i) 及其他的valueOf()方法


字符数组 => 字符串(String构造器):String(char[]) 和 String(char[],int offset,int length)。


字符串 => 字符数组(String静态方法):public char[] toCharArray(),public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)


字节数组 => 字符串(String构造器):String(byte[])、String(byte[],int offset,int length)


字符串 => 字节数组(String静态方法):public byte[] getBytes() 、public byte[] getBytes(String charsetName)



常见方法


引用的是尚硅谷的课件:








分类总结:长度、指定索引、为空、大小写转换、比较、连接、截取、测试前后缀、是否包含、替换(单个、所有)、正则匹配、拆分字符串。



相关面试题

例1、在使用new String("")创建了几个对象?


通过看博客得知一个比较好的答案,1个或2个。使用new来创建字符串对象时会有如下过程,首先会去检查要创建字符串在字符串常量区中是否存在?


①如果存在,那么只创建一个对象在堆区,接着将在常量区中找到的字符串引用返回给堆中对象的value,再将堆中对象的引用地址返回给栈中的变量。

②如果不存在,那么会创建2个对象(堆与常量区),首先在常量区中创建字符串并将引用传递给堆中对象的value,接着再将堆中对象的引用传递给栈中的常量。


例2:输出下面运行结果


public class StringTest {
    String s = new String("changlu");
    char[] ch = {'h','a','v','a'};
    public void change(String s,char ch[]){
        s = "liner";
        ch[0] = 'j';
    }
    public static void main(String[] args) {
        StringTest str = new StringTest();
        str.change(str.s,str.ch);
        System.out.println(str.s);
        System.out.println(str.ch);
    }
}



说明:本题还是有些迷惑性的,结果是str.s没有改变,str.ch改变了。为啥呢,我们看个图一下子就能懂了。



为什么str.s没有更改?因为方法中只是传递了一个String的字符串,那么给它重新赋值字面值,过程是在常量区创建liner,之后将引用地址给s,并没有涉及到str中s的引用地址指向。

str.s更改了?方法中传递的是地址值0x34,做的操作是0x34地址部分的第一个字符赋值为j,对应str实例中的ch依旧是指向0x34,所以最后结果得出更改了。

那么怎样传递才能让str.s改变值呢?


//方法中参数改为str的实例,这时候传过来更改其中的s变量值才是有效的,因为是对str实例中的属性进行更改操作
public void change1(StringTest str){
    str.s = "liner";
    str.ch[0] = 'j';
}



相关文章
|
3天前
|
安全
String、StringBuuffer、StringBuilder三者的区别
String、StringBuuffer、StringBuilder三者的区别
|
3天前
|
Java
Java String类型转换成Date日期类型
Java String类型转换成Date日期类型
|
3天前
|
Java 索引
Java String应用与开发
Java String应用与开发
12 0
|
5天前
|
存储 安全 Java
聊聊Java中的常用类String
聊聊Java中的常用类String
9 0
|
8天前
|
缓存 安全 Java
【Java基础】String、StringBuffer和StringBuilder三种字符串对比
【Java基础】String、StringBuffer和StringBuilder三种字符串对比
6 0
|
8天前
|
存储 缓存 Java
|
9天前
|
Java API 索引
Java基础&API(2) String、StringBuilder详解
Java基础&API(2) String、StringBuilder详解
|
4月前
|
存储 安全 Java
【JAVA基础】String、StringBuilder和StringBuffer的区别——巨详细
String是不可变的,StringBuilder和StringBuffer是可变的。而StringBuffer是线程安全的,而StringBuilder是非线程安全的。
|
3月前
|
存储 XML 缓存
Java字符串内幕:String、StringBuffer和StringBuilder的奥秘
Java字符串内幕:String、StringBuffer和StringBuilder的奥秘
26 0
|
5月前
|
安全 Java 调度
Java基础面试,String,StringBuffer,StringBuilder区别以及使用场景
* String是final修饰的,不可变,每次操作都会产生新的对象。 * StringBuffer和StringBuilder都是在原对象上进行操作 * StringBuffer是线程安全的,StringBuilder是线程不安全的。 * StringBuffer方法是被synchronized修饰的