String、StringBuilder、StringBuffer三姐妹

在这里插入图片描述
字符串不仅是程序设计中最常见的行为之一,在JavaWeb中更是如此

三姐妹之一——不可变的String

在Java中是没有内置的字符串类型,所以标准Java类库提供了一个String类,只要是双引号(“ab”)括起来的字符串都是一个String实例

String e = ""; // 空串
String str = "hello";

查看Java类库中的String源码,Java8内部是使用char[]数组来存储数据的

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];//用char数组存储数据
}

我们注意到String类是被final修饰的,所以String不可以被继承
而在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}

不过,无论是 Java 8 还是 Java 9,用来存储数据的 char 或者 byte 数组 value 都一直是被声明为 final 的,这意味着 value 数组初始化之后就不能再引用其它数组了。并且 String 内部没有改变 value 数组的方法,因此我们就说 String 是不可变的。
所谓不可变,就如同数字 3 永远是数字 3 —样,字符串 “hello” 永远包含字符 h、e、1、1 和 o 的代码单元序列, 不能修改其中的任何一个字符。当然, 可以修改字符串变量 str, 让它引用另外一个字符串, 这就如同可以将存放 3 的数值变量改成存放 4 一样。
举个例子:

String str="abc";
String s=str.toUpperCase();
print(s);//s="ABC"

上述的代码看起来好像把abc变成了大写,改变了字符串,但是进入 toUpperCase 的源码我们发现,这个看起来会修改 String 值的方法,实际上最后是创建了一个全新的 String 对象,而最初的 String 对象则丝毫未动。
在这里插入图片描述
空串和null
空串 “” 很好理解,就是长度为 0 的字符串。可以调用以下代码检查一个字符串是否为空:

//1
if(str.length() == 0){
    // todo
}
//2
if(str.equals("")){
	// todo
}

空串是一个 Java 对象, 有自己的串长度( 0 ) 和内容(空),也就是 value 数组为空。
String 变量还可以存放一个特殊的值, 名为 null,这表示目前没有任何对象与该引用变量关联。要检查一个字符串是否为 null,可如下判断:

if(str == null){
    // todo
}

有时要检查一个字符串既不是 null 也不为空串,这种情况下就需要使用以下条件:

if(str != null && str.length() != 0){
    // todo
}

这段代码看起来很简单!但是有坑,这里的条件执行顺序是不可以改变的,我们必须首先检查 str 是否为 null,因为如果在一个 null 值上调用方法,编译器会报错。

字符串拼接

上面既然说到 String 是不可变的,我们来看段代码,为什么这里的字符串 a 却发生了改变?

String a = "hello";
String b = "world";
a = a + b; // a = "helloworld"

我们上面调用的toUpperCase方法是创建了一个新的String对象存放转化后哦的String,那么字符串拼接是否也是如此?
答:实际上,在使用 + 进行字符串拼接的时候,JVM 是初始化了一个 StringBuilder 来进行拼接的。相当于编译后的代码如下:

String a = "hello";
String b = "world";
StringBuilder builder = new StringBuilder();
builder.append(a);
builder.append(b);
a = builder.toString();

我们看一下toString的源码:
在这里插入图片描述
原来也是和toUpperCase一样创建了一个新的String对象,而不是在旧字符串的内容上做更改,相当于把旧字符串的引用指向的新的String对象。这也就是字符串 a 发生变化的原因。
另外还有一个特点,我们使用一个字符串和另一个非字符串对象拼接时,会自动将其转化成字符串对象,所有Java对象都可以转换成字符串

int age = 13;
String rating = "PG" + age; // rating = "PG13"

那么又有一个问题,我们使用空字符串和null拼接的时候会得到什么?
答:“null”
为什么呢?因为上面提到字符串拼接其实是新建一个StringBuilder对象,然后将拼接的字符串append进去

String str = null;
str = str + "";
StringBuilder builder = new StringBuilder();
builder.append(str);
builder.append("");
str = builder.toString();

append(null)会发生什么?我们来看一下源码
在这里插入图片描述
append(null)会调用appendNull方法返回一个“null”字符串。

检测两个字符串是否相等

1.可以使用equals方法

String str = "hello";
System.out.println("hello".equals(str)); // true

equals方法是Object类的一个方法,Object类是所有类的父类。了解equals之前,我们回顾一下运算符“==”的两种运用情况
对于基本数据类型来说, == 比较的是值是否相同;
对于引用数据类型来说, == 比较的是内存地址是否相同。

String str1 = new String("hello"); 
String str2 = new String("hello");
System.out.println(str1 == str2); // false

上述的代码,我们可以看到str1和str2都调用new String()创建了两个字符串对象,以 String str1 = new String(“hello”); 为例,new 出来的对象存放在堆内存中,用一个引用 str1 来指向这个对象的地址,而这个对象的引用 str1 存放在栈内存中。str1 和 str2 是两个不同的对象,地址不同,因此 == 比较的结果也就为 false。(引用类型==比较的是地址)
在这里插入图片描述
而实际上,Object 类中的原始 equals 方法内部调用的还是运算符 ==,判断的是两个对象是否具有相同的引用(地址),和 == 的效果是一样的:
在这里插入图片描述
换句话说,如果我们新建的类没有重写继承自Object类的equals方法,那么调用equals方法和使用 == 效果是一样的
而String类重写了equals方法,代码如下
在这里插入图片描述
String 重写的 equals 方法比较的是对象的内容,而非地址。
那么总结一下equals的两种使用情况:
1.如果没有重写Object的equals方法,等价于通过 == 比较这两个对象(比较的是地址)。
2.如果新建的类重写了equals方法,例如String类的equals方法就是比较对象的内容是否相等,当然你也可以不这么做

举个例子:

String a = new String("ab"); // a 为一个字符串引用
String b = new String("ab"); // b 为另一个字符串引用,这俩对象的内容一样

if (a.equals(b)) // true
    System.out.println("aEQb");
if (a == b) // false,不是同一个对象,地址不同
    System.out.println("a==b");

字符串常量池

字符串 String 既然作为 Java 中的一个类,那么它和其他的对象分配一样,需要耗费高昂的时间与空间代价,作为最基础最常用的数据类型,大量频繁的创建字符串,将会极大程度的影响程序的性能。为此,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化:
1.为字符串开辟了一个字符串常量池 String Pool,可以理解为缓存区(存放在Java堆内存中)
2.创建字符串常量时,首先检查字符串常量池中是否存在该字符串
3.若字符串常量池中存在该字符串,则直接返回该引用实例,无需重新实例化;若不存在,则实例化该字符串并放入池中。
举个例子:

String str1 = "hello";
String str2 = "hello";

System.out.printl("str1 == str2" : str1 == str2 ) //true 

有人肯定疑惑,== 比较的不是地址吗,为什么str1又等于str2了?注意了,我们上面的代码是调用了new来创建了一个新的对象,而这里是让str1,str2引用的常量而不是对象
String str1 = “hello”;, 编译器首先会在栈中创建一个变量名为 str1 的引用,然后在字符串常量池中查找有没有值为 “hello” 的引用,如果没找到,就在字符串常量池中开辟一个地址存放 “hello” 这个字符串,然后将引用 str1 指向 “hello”。然后执行String str2 = “hello”;这时候字符串常量池已经存在执行str1创建的hello字符串了,所以str2和str1的引用是一样的
在这里插入图片描述
字符串常量池的位置再jdk1.7发生了改变

JDK 1.7 之前,字符串常量池存在于常量存储(Constant storage)中
JDK 1.7 之后,字符串常量池存在于堆内存(Heap)中。

在这里插入图片描述
另外我们也可以使用 String 的 intern() 方法在运行过程中手动的将字符串添加到 String Pool 中。具体过程是这样的:
当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串的值相等,那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。

String str1 = new String("hello"); 
String str3 = str1.intern();
String str4 = str1.intern();
System.out.println(str3 == str4); // true

对于str3来说,str1.intern() 会先在 String Pool 中查看是否已经存在一个字符串和 str1 的值相等,发现没有(new是在堆内存创建一个字符串对象,不是常量池!),于是在 String Pool 中添加了一个新的值和 str1 相等的字符串,并返回这个新字符串的引用。对于str4,str1.intern()还是在字符串常量池中查找是否有一个字符串和str1的值相等,找到了str3引用,于是直接返回这个字符串的引用。所以str3 == str4
在这里插入图片描述
总结:

String str = “i” 的方式,java 虚拟机会自动将其分配到常量池中;

String str = new String(“i”) 则会被分到堆内存中。可通过 intern 方法手动加入常量池

new String(“hello”) 创建了几个字符串对象
下面这行代码到底创建了几个字符串对象?仅仅只在堆中创建了一个?

String str1 = new String("hello"); 

显示不是,对于 str1 来说,new String(“hello”) 分两步走:
1.首先,hello属于字符串常量,所以编译的时候会先在字符串常量池中查找是否有值为“hello”的引用,如果没有,就要在字符串常量池中分配内存创建一个字符串对象指向这个 “hello” 字符串字面量;
2.然后,使用new方式又会在堆中创建一个字符串对象
因此,使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “hello” 字符串对象)。

在这里插入图片描述

双生子——StringBuilder和StringBuffer

字符串拼接
有些时候, 需要由较短的字符串构建字符串, 例如, 按键或来自文件中的单词。采用字符串拼接的方式达到此目的效率比较低。由于 String 类的对象内容不可改变,所以每当进行字符串拼接时,总是会在内存中创建一个新的对象。既耗时, 又浪费空间。例如:

String s = "Hello";
s += "World";

上述的代码其实产生了三个字符串即 “Hello”、“World” 和"HelloWorld"。“Hello” 和 “World” 作为字符串常量会在 String Pool 中被创建,而拼接操作 + 会 new 一个对象用来存放 “HelloWorld”。
使用 StringBuilder/ StringBuffer 类就可以避免这个问题的发生,毕竟 String 的 + 操作底层都是由 StringBuilder 实现的。StringBuilder 和 StringBuffer 拥有相同的父类:
在这里插入图片描述
但是StringBuilder不是线程安全的,在多线程的情况下会出现数据不一致的情况,而StringBuffer是线程安全的,这是因为在StringBuffer类里面的常用方法都使用了synchronized关键字来进行同步,所以是线程安全的(但是加锁系统开销大)。而 StringBuilder 并没有。这也是运行速度 StringBuilder 大于 StringBuffer 的原因了。因此,如果在单线程下,优先考虑使用 StringBuilder。
StringBuiler/StringBuffer 不能像 String 那样直接用字符串赋值,所以也不能那样初始化。它需要通过构造方法来初始化。首先, 构建一个空的字符串构建器:

StringBuilder builder = new StringBuilder();

每次需要添加一部分内容时,调用append方法即可

char ch = 'a';
builder.append(ch);

String str = "ert"
builder.append(str);

需要构建字符串时就调用toString方法即可

String mystr = builder.toString(); // aert

String、StringBuilder、StringBuffer比较

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值