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';
}



相关文章
|
11天前
|
存储 JavaScript Java
Java 中的 String Pool 简介
本文介绍了 Java 中 String 对象及其存储机制 String Pool 的基本概念,包括字符串引用、构造方法中的内存分配、字符串文字与对象的区别、手工引用、垃圾清理、性能优化,以及 Java 9 中的压缩字符串特性。文章详细解析了 String 对象的初始化、内存使用及优化方法,帮助开发者更好地理解和使用 Java 中的字符串。
Java 中的 String Pool 简介
|
16天前
|
缓存 安全 Java
java 为什么 String 在 java 中是不可变的?
本文探讨了Java中String为何设计为不可变类型,从字符串池的高效利用、哈希码缓存、支持其他对象的安全使用、增强安全性以及线程安全等方面阐述了不可变性的优势。文中还通过具体代码示例解释了这些优点的实际应用。
java 为什么 String 在 java 中是不可变的?
|
2月前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第14天】Java零基础教学篇,手把手实践教学!
123 65
|
28天前
|
JSON Java 关系型数据库
Java更新数据库报错:Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.
在Java中,使用mybatis-plus更新实体类对象到mysql,其中一个字段对应数据库中json数据类型,更新时报错:Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.
41 4
Java更新数据库报错:Data truncation: Cannot create a JSON value from a string with CHARACTER SET 'binary'.
|
11天前
|
存储 Java
Java 11 的String是如何优化存储的?
本文介绍了Java中字符串存储优化的原理和实现。通过判断字符串是否全为拉丁字符,使用`byte`代替`char`存储,以节省空间。具体实现涉及`compress`和`toBytes`方法,前者用于尝试压缩字符串,后者则按常规方式存储。代码示例展示了如何根据配置决定使用哪种存储方式。
|
26天前
|
Java
在Java中如何将基本数据类型转换为String
在Java中,可使用多种方法将基本数据类型(如int、char等)转换为String:1. 使用String.valueOf()方法;2. 利用+运算符与空字符串连接;3. 对于数字类型,也可使用Integer.toString()等特定类型的方法。这些方法简单高效,适用于不同场景。
54 7
|
28天前
|
安全
String、StringBuffer、StringBuilder的区别
String 由 char[] 数组构成,使用了 final 修饰,对 String 进行改变时每次都会新生成一个 String 对象,然后把指针指向新的引用对象。 StringBuffer可变并且线程安全;有一定缓冲区容量,字符串大小没超过容量,不会重新分配新的容量,适合多线程操作字符串; StringBuiler可变并且线程不安全。速度比StringBuffer更快,适合单线程操作字符串。 操作少量字符数据用 String;单线程操作大量数据用 StringBuilder;多线程操作大量数据用 StringBuffer
|
2月前
|
存储 SQL 安全
Java零基础-StringBuilder类详解
【10月更文挑战第12天】Java零基础教学篇,手把手实践教学!
32 5
|
2月前
|
Java 测试技术 开发者
Java零基础-indexOf(String str)详解!
【10月更文挑战第13天】Java零基础教学篇,手把手实践教学!
57 1
|
1月前
|
Java 数据库连接 API
Spring 框架的介绍(Java EE 学习笔记02)
Spring是一个由Rod Johnson开发的轻量级Java SE/EE一站式开源框架,旨在解决Java EE应用中的多种问题。它采用非侵入式设计,通过IoC和AOP技术简化了Java应用的开发流程,降低了组件间的耦合度,支持事务管理和多种框架的无缝集成,极大提升了开发效率和代码质量。Spring 5引入了响应式编程等新特性,进一步增强了框架的功能性和灵活性。
46 0
下一篇
DataWorks