JavaseLearn21-String和StringBuffer
JavaseLearn21-String&StringBuffer&StringBuilder
一、String
1. String字符串的存储原理
创建以下几个对象:
String s1 = "abc";
String s2 = "abc";
String s3 = "abc" + "def";
String s4 = new String("fff");
String s5 = new String("fff");
以上对象在JVM内存图中表示为:
//s1:在方法区常量池中创建一个"abc"
String s1 = "abc";
//s2:不会创建新对象,直接指向已经存在的"abc"
String s2 = "abc";
//s3:在常量池中创建两个对象,一个是"def",一个是"abcdef", "abc"由于s1已经创建过了,直接拿来使用
String s3 = "abc" + "def";
//s4:创建两个对象,一个是位于方法区常量池中的"fff",一个是位于堆内存中的String对象,存储的是常量池中"fff"的内存地址
String s4 = new String("fff");
//s5:只在堆内存中创建一个对象,存放的是s4已经在方法区常量池中创建了的"fff"内存地址
String s5 = new String("fff");
1.1为什么说String是不可变的呢?
String底层是一个被final修饰的byte[]数组。因为数组一旦创建,长度不可变,而且被final修饰的引用无法指向新的对象,所以说String是不可变的。
2. String字符串的比较运算
创建下列字符串对象:
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
内存图:
2.1通过"=="进行比较
通过“==”比较的是String字符串对象的内存地址
//s1 == s2:
//s1和s2存储的都是常量池中"abc"的内存地址,
//以下语句相当于:0x1111 == 0x1111 结果为:true
System.out.println("s1 == s2:" + (s1 == s2));
//s3 == s4:
//s3和s4存储的是各自在堆中的对象的内存地址,
//以下语句相当于:0x4444 == 0x6843 结果为:false
System.out.println("s3 == s4:" + (s3 == s4));
//s1 == s3:
//s1存储的都是常量池中"abc"的内存地址,s4存储的是在堆中的对象的内存地址,
//以下语句相当于:0x1111 == 0x4444 结果为:false
System.out.println("s1 == s3:" + (s1 == s3));
//"abc" == s1:
//这里相当于拿常量池中"abc"的内存地址与s1中存储的常量池中"abc"的内存地址相比较,
//以下语句相当于:0x1111 == 0x1111 结果为:true
System.out.println("\"abc\" == s1:" + ("abc" == s1));
//"abc" == s3:
//这里相当于拿常量池中"abc"的内存地址与s3中存储的在堆中对象的内存地址相比较,
//以下语句相当于:0x1111 == 0x4444 结果为:false
System.out.println("\"abc\" == s3:" + ("abc" == s3));
运行结果:
s1 == s2:true
s3 == s4:false
s1 == s3:false
"abc" == s1:true
"abc" == s3:false
2.2通过"equals()"进行比较
String类已经重写过equals()方法,使得可以比较String对象中的值
System.out.println("s1.equals(s2):" + (s1.equals(s2)));
System.out.println("s3.equals(s4):" + (s3.equals(s4)));
System.out.println("s1.equals(s3):" + (s1.equals(s3)));
//"abc"是一个String字符串对象,故可以调用equals方法
System.out.println("\"abc\".equals(s1):" + ("abc".equals(s1)));
//建议使用"abc".equals(s3)这种形式,可以避免空指针异常
System.out.println("\"abc\".equals(s3):" + ("abc".equals(s3)));
因为所有引用都指向常量池中的"abc",故都是true
运行结果:
s1.equals(s2):true
s3.equals(s4):true
s1.equals(s3):true
"abc".equals(s1):true
"abc".equals(s3):true
3. String类的常用方法
3.1 String类常用的构造方法
3.1.1 String s = "";
最常用
String s1 = "Hello";
//输出字符串对象的话,不会输出对象内存地址,输出的是字符串本身,因为String类已经重写了toString()
System.out.println("s1:" + s1);
s1:Hello
3.1.2 String s = new String("");
String s2 = new String("Hello");
System.out.println("s2:" + s2);
s2:Hello
3.1.3 String s = new String(byte数组);
//97->"a",98->"b",99->"c"
byte[] bytes = {97,98,99};
/*
String(字节数组)
将byte数组转换成字符串
*/
String s3 = new String(bytes);
System.out.println("s3:" + s3);
s3:abc
3.1.4 String s = new String(byte数组,起始下标,长度);
byte[] bytes = {97,98,99};
/*
String(byte数组,数组元素下标的起始位置,长度)
将byte数组里的一部分转换成字符串
*/
String s4 = new String(bytes,1,2);
System.out.println("s4:" + s4);
s4:bc
3.1.5 String s = new String(char数组);
char[] chars = {'字','符','串'};
/*
String(char数组)
将char数组转换成字符串
*/
String s5 = new String(chars);
System.out.println("s5:" + s5);
s5:字符串
3.1.6 String s = new String(char数组,起始下标,长度);
char[] chars = {'字','符','串'};
/*
String(char数组,起始下标,长度)
将char数组里的一部分转换成字符串
*/
String s6 = new String(chars,1,2);
System.out.println("s6:" + s6);
s6:符串
3.2 String类常用的实例方法
3.2.1 charAt
char charAt(int index)
返回字符串指定索引处的字符。
String s1 = "abc";
System.out.println(s1.charAt(1));
b
3.2.2 compareTo
int compareTo(String anotherString)
按字典顺序比较两个字符串。
换句话说就是将两个字符串中的每个字符按顺序挨个按各自的ASCII码进行相减。
如果前面的字符串等于后面的字符串,那么比较结果就为0,
如果前面的字符串大于后面的字符串,那么比较结果就为正数,
如果前面的字符串小于后面的字符串,那么比较结果就为负数。
System.out.println("字符a的ASCII码为:" + (int)'a');
System.out.println("字符A的ASCII码为:" + (int)'A');
System.out.println("字符c的ASCII码为:" + (int)'c');
System.out.println("字符d的ASCII码为:" + (int)'d');
System.out.println("字符i的ASCII码为:" + (int)'i');
//两个字符串中每个字符都相等,ASCII码也一样,故相减结果为0
System.out.println("abc".compareTo("abc"));//0
//拿前面的字符串中第一个字符'A'和后面的字符串中第一个字符'a'比较,如果分出胜负,后面的字符就不比了。
//'A'的ASCII码为65,'a'的ASCII码为97。
//拿65减去97,得-32。
System.out.println("Abc".compareTo("abc"));//-32
System.out.println("abc".compareTo("abd"));//-1
System.out.println("abd".compareTo("abc"));//1
System.out.println("iab".compareTo("abi"));//8
结果:
字符a的ASCII码为:97
字符A的ASCII码为:65
字符d的ASCII码为:99
字符e的ASCII码为:100
字符i的ASCII码为:105
0
-32
-1
1
8
3.2.3 compareToIgnoreCase
int compareToIgnoreCase(String str)
按字典顺序比较两个字符串,忽略字符大小写。
System.out.println("Abc".compareToIgnoreCase("abc"));
结果:
0
3.2.4 contains
boolean contains(CharSequence s)
判断前面字符串中是否包含后面的字符串,如果包含,返回true,否则返回false.
System.out.println("abcdef".contains("def"));
System.out.println("abcdef".contains("woc"));
true
false
3.2.5 startsWith&endsWith
boolean endsWith(String suffix)
判断当前字符串是否以指定的字符串结尾,是为true,不是为false。
System.out.println("HelloWorld.java".endsWith(".java"));
System.out.println("HelloWorld.java".endsWith(".c"));
System.out.println("abcdef".endsWith("ef"));
true
false
true
boolean startsWith(String prefix)
判断当前字符串是否以指定的字符串开头,是为true,不是为false。
System.out.println("http://www.baidu.com".startsWith("http://"));
System.out.println("http://www.baidu.com".startsWith("https://"));
true
false
3.2.6 equals
boolean equals(Object anObject)
将当前字符串与其他字符串进行比较。相同为true,不同为false
String s1 = new String("abc");
System.out.println("abc".equals(s1));
System.out.println("Abc".equals(s1));
true
false
3.2.7 equalsIgnoreCase
boolean equalsIgnoreCase(String anotherString)
将当前字符串与其他 String比较,忽略字符串中字符的大小写。
System.out.println("Abc".equalsIgnoreCase("abc"));
true
3.2.8 getBytes
byte[] getBytes()
将字符串转换成字节数组
byte[] bytes = "abc".getBytes();
int i = bytes.length - 1;
while (i >= 0) {
System.out.println(bytes[i]);
i--;
}
99
98
97
3.2.9 isEmpty
boolean isEmpty()
判断某个字符串是否为空,为空返回true,不为空返回false
String s2 = "";
String s3 = "a";
System.out.println(s2.isEmpty());
System.out.println(s3.isEmpty());
true
false
3.2.10 length()
int length()
返回字符串的长度
String s4 = "abc";
System.out.println(s4.length());
3
面试题:判断字符串长度和数组长度
String s4 = "abc";
System.out.println(s4.length());
String[] strings = {"a","b","c"};
System.out.println(strings.length);
判断字符串长度使用length()方法。
判断数组长度使用length属性。
3.2.11 indexOf
int indexOf(int ch)
返回指定字符在当前字符串内第一次出现处的下标。
System.out.println("abcdef".indexOf(97));
0
int indexOf(String str)
返回指定字符串在当前字符串内第一次出现处的下标。
System.out.println("abcdef".indexOf("def"));
3
int lastIndexOf(String str)
返回指定字符串在当前字符串内最后一次出现处的下标。
System.out.println("javaHellojava".lastIndexOf("java"));
9
3.2.13 valueOf
static String valueOf(boolean b)
将非字符串的元素转换成字符串。
String类里唯一的静态方法
int i = 100;
String str1 = String.valueOf(i);
System.out.println(str1);
100
当使用valueof转换一个对象时,底层会自动调用toString方法
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public class StringDemo04 {
public static <Char, Chat> void main(String[] args) {
String str2 = String.valueOf(new Cat("加菲猫"));
System.out.println(str2);
}
}
class Cat {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
加菲猫
由此我们可以查看println打印的底层源码:
参数为int类型数字时:
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
public void print(int i) {
write(String.valueOf(i));
}
参数为对象时:
public void println(Object x) {
String s = String.valueOf(x);
synchronized (this) {
print(s);
newLine();
}
}
可见,println方法底层调用了valueof方法,进而调用了toString方法。
所以说,System.out.println();输出任何数据时,都先转换成字符串,再打印。
3.2.14 substring
String substring(int beginIndex)
截取一个新的字符串,从下标beginIndex开始
String str1 = "abcdefg";
String str2 = str1.substring(2);
cdefg
String substring(int beginIndex, int endIndex)
返回一个新字符串,它是该字符串的一个子字符串。该字符串从指定的下标beginIndex开始,到下标endIndex-1处结束。前闭后开。
//前闭后开
System.out.println(str2);
String str3 = str1.substring(2,5);
System.out.println(str3);
cde
3.2.15 toCharArray
char[] toCharArray()
将此字符串转换为新的字符数组。
String str1 = "abcde";
char[] arr = str1.toCharArray();
for (char c : arr) {
System.out.println(c);
}
a
b
c
d
e
3.2.16 replace
String replace(CharSequence target, CharSequence replacement)
替换指定字符串
String的父接口就是CharSequence
String newStr1 = "HelloWorld.txt".replace("txt","java");
System.out.println(newStr1);
String newStr2 = "时/分/秒".replace("/",":");
System.out.println(newStr2);
HelloWorld.java
时:分:秒
3.2.17 toLowerCase&toUpperCase
String toLowerCase()
将字符串转换成小写
String str1 = "asdfGHJK";
System.out.println(str1.toLowerCase());
asdfghjk
String toUpperCase()
将字符串转换成大写
String str1 = "asdfGHJK";
System.out.println(str1.toUpperCase());
ASDFGHJK
3.2.18 split
String[] split(String regex)
将字符串从指定字符串处拆分成一个字符串数组
String[] str1 = "2021-07-05".split("-");
for (String s : str1) {
System.out.println(s);
}
String[] str2 = "id=007&userName=zhangsan&passWord=12345".split("&");
for (String s : str2) {
String[] str3 = s.split("=");
for (String s1 : str3) {
System.out.println(s1);
}
}
2021
07
05
id
007
userName
zhangsan
passWord
12345
3.2.20 trim
String trim()
去除字符串前后空白
System.out.println(" Hello World.java ".trim());
Hello World.java
二、StringBuffer和StringBuilder
思考:我们在实际开发中,如果需要进行字符串的频繁拼接,会有什么问题?
因为java中String字符串是不可变的,所以每一次拼接都会产生新的字符串。
String str1 = "a";
str1 = str1 + "b";
这两句就能导致在方法区常量池里创建三个对象:"a"、"b"、"ab",且"a"、"b"不会消失,会变成未使用对象。
String str1 = "";
for (int i = 0; i < 100; i++) {
str1 += i;
System.out.println(str1);
}
以上代码块中,每循环一次,就会进行一次字符串的拼接,在常量池里就会生成新的未使用对象。当拼接次数过多时,会给常量池造成很大的负荷。
所以我们在进行频繁的字符串拼接时,为了避免这种情况,要使用到StringBuffer或StringBuilder。
1. StringBuffer
1.1 StringBuffer概述
与String类不同的是,StringBuffer类的对象能够被多次修改,且不会产生新的未使用对象。
StringBuffer底层实际上是一个byte[]数组,未被final修饰。
往StringBuffer中放字符串,实际上是放到byte[]数组里了。
StringBuffer的初始化容量是16。
StringBuffer是线程安全的。
1.2 StringBuffer的追加
1.2.1 StringBuffer的追加需要调用append()方法
//创建一个初始化容量为16的byte[]数组(字符串缓存区对象)
StringBuffer sb1 = new StringBuffer();
sb1.append("a");
sb1.append("b");
sb1.append("c");
sb1.append(100);
//append方法在进行追加时,如果byte数组满了,会自动扩容
sb1.append(true);
System.out.println(sb1);
String s = "abc";
abc100true
1.2.2 为什么说StringBuffer的追加不会产生新的未使用对象
String底层是一个byte[]数组,被final修饰。
-
数组一旦创建长度不可变。
-
被final修饰的引用一旦指向某个对象后,不可再指向其他对象。
-
String进行拼接操作时,底层数组引用无法指向新的数组,无法进行扩容操作,只能创建新的字符串对象。
-
而旧的字符串对象不会被垃圾回收器回收,会造成空间上的浪费。
StringBuffer底层也是一个byte[]数组,但未被final修饰。
- StringBuffer进行拼接操作时,底层数组引用可以指向新的数组。如果容量满了,可以进行扩容操作。
- 当扩容完毕时,旧的数组可以被垃圾回收器回收,故不会产生新的未使用对象。
1.2.3 append的底层源码
以StringBuffer append(String str) 为例。
/**
* StringBuffer中append方法
*/
@Override
//由synchronized修饰,线程不可并发,线程是安全的,但效率低
public synchronized StringBuffer append(String str) {
toStringCache = null;
//调用父类中的append方法
super.append(str);
return this;
}
/**
* 父类AbstractStringBuilder中的append方法
*/
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//调用确保容量的方法
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
/**
* 确保容量方法
*/
private void ensureCapacityInternal(int minimumCapacity) {
// 如果容量满了,则进行扩容
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
/**
* 扩容方法
*/
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
//调用Arrays类里的拷贝方法,将旧的数组和需要扩容的长度newCapacity传入
value = Arrays.copyOf(value, newCapacity);
}
/**
* Arrays类里的拷贝方法
* 将旧数组中的数据拷贝到一个新的,长度扩容后的数组里
*/
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
2.StringBuilder
StringBuilder与StringBuffer的区别在于:
- StringBuffer由synchronized关键字修饰,是线程同步的,是线程安全的,但是效率慢。
- 而StringBuilder未被synchronized修饰,不是线程安全的,但是效率高。
其他与StringBuffer一样。