5、(**)String类
1、String概述
- 字符串
- 常量,创建之后不能更改,改变的只是地址
//String的属性值
privatefinalcharvalue[];
//数组被使用的开始位置
privatefinalintoffset;
//String中元素的个数
privatefinalintcount;
//String类型的hash值
privateinthash; // Default to 0
- 从源码看出String底层使用一个字符数组来维护的。
- 成员变量可以知道String类的值是final类型的,不能被改变的,所以只要一个值改变就会生成一个新的String类型对象,存储String数据也不一定从数组的第0个元素开始的,而是从offset所指的元素开始。
java
String的构造方法
String()
//初始化一个新创建的 String 对象,使其表示一个空字符序列。
String(byte[] bytes)
//通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
String(byte[] bytes, Charsetcharset)
//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
String(byte[] bytes, intoffset, intlength)
//通过使用平台的默认字符集解码指定的 byte 子数组,构造一个新的 String。
String(byte[] bytes, intoffset, intlength, Charsetcharset)
//通过使用指定的 charset 解码指定的 byte 子数组,构造一个新的 String。
String(byte[] bytes, intoffset, intlength, StringcharsetName)
//通过使用指定的字符集解码指定的 byte 子数组,构造一个新的 String。
String(byte[] bytes, StringcharsetName)
//通过使用指定的 charset 解码指定的 byte 数组,构造一个新的 String。
String(char[] value)
//分配一个新的 String,使其表示字符数组参数中当前包含的字符序列。
String(char[] value, intoffset, intcount)
//分配一个新的 String,它包含取自字符数组参数一个子数组的字符。
String(int[] codePoints, intoffset, intcount)
//分配一个新的 String,它包含 Unicode 代码点数组参数一个子数组的字符。
String(Stringoriginal)
//初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
String(StringBufferbuffer)
//分配一个新的字符串,它包含字符串缓冲区参数中当前包含的字符序列。
String(StringBuilderbuilder)
//分配一个新的字符串,它包含字符串生成器参数中当前包含的字符序列。
2、创建字符串对象方式
- 直接赋值(在方法区的常量池)
Stringstr="hello";
- 通过构造方法(实例化)创建在堆内存
Stringstr=newString("hello");
“==”和equals()两种方式的比较
- 编写代码比较
publicstaticvoidmain(String[] args) {
Stringstr1="Lance";
Stringstr2=newString("Lance");
Stringstr3=str2; //引用传递,str3直接指向st2的堆内存地址
Stringstr4="Lance";
/**
* ==:
* 基本数据类型:比较的是基本数据类型的值是否相同
* 引用数据类型:比较的是引用数据类型的地址值是否相同
* 所以在这里的话:String类对象==比较,比较的是地址,而不是内容
*/
System.out.println(str1==str2);//false
System.out.println(str1==str3);//false
System.out.println(str3==str2);//true
System.out.println(str1==str4);//true
}
- ==
- 比较基本数据类型的值
- 比较引用数据类型的地址是否相同
- equals()
- 比较的是字符串内容
- 2、内存图分析
- str1先存入常量池中,str4因为常量池中存在相同的内容所以直接拿来使用
- str2和str3在堆内存中,引用(地址)相同
字符串常量池
- 采用直接赋值的方式
String str1 = "Lance"
时,会将匿名对象放入对象池。- 每当下一次对不同的对象进行直接赋值的时候会直接利用池中原有的匿名对象
- 手动入池
String str = new String("Lance").intern();
publicstaticvoidmain(Stringargs[]){
//对匿名对象"hello"进行手工入池操作
Stringstr=newString("Lance").intern();
Stringstr1="Lance"; // 此时会直接利用池中原有的匿名变量
System.out.println(str==str1);//true
}
两种创建方式的区别
- 直接赋值:只开辟一块堆内存空间,并且会自动入池,不会产生垃圾
- 构造方法:会开辟两块堆内存空间
(相当于先直接赋值创建匿名对象“hello”一次再构造方法创建一次),其中一块堆内存会变成垃圾被系统回收,而且不能够自动入池,需要通过public String intern();
方法进行手动入池- 在开发的过程中不会采用构造方法进行字符串的实例化
3、String的方法
1、String的判断
booleanequals(Objectobj):比较字符串的内容是否相同
booleanequalsIgnoreCase(Stringstr):比较字符串的内容是否相同,忽略大小写
booleanstartsWith(Stringstr):判断字符串对象是否以指定的str开头
booleanendsWith(Stringstr):判断字符串对象是否以指定的str结尾
publicstaticvoidmain(String[] args) {
// 创建字符串对象
Strings1="hello";
Strings2="hello";
Strings3="Hello";
// boolean equals(Object obj):比较字符串的内容是否相同
System.out.println(s1.equals(s2)); //true
System.out.println(s1.equals(s3)); //false
System.out.println("-----------");
// boolean equalsIgnoreCase(String str):比较字符串的内容是否相同,忽略大小写
System.out.println(s1.equalsIgnoreCase(s2)); //true
System.out.println(s1.equalsIgnoreCase(s3)); //true
System.out.println("-----------");
// boolean startsWith(String str):判断字符串对象是否以指定的str开头
System.out.println(s1.startsWith("he")); //true
System.out.println(s1.startsWith("ll")); //false
}
2、String的截取
intlength():获取字符串的长度,其实也就是字符个数
charcharAt(intindex):获取指定索引处的字符类似于数组取数
intindexOf(Stringstr):获取str在字符串对象中第一次出现的索引,返回位置,下标从0开始
Stringsubstring(intstart):从start开始截取字符串
Stringsubstring(intstart,intend):从start开始,到end结束截取字符串。包括start,不包括end
publicstaticvoidmain(Stringargs[]) {
// 创建字符串对象
Strings="helloworld";
// int length():获取字符串的长度,其实也就是字符个数
System.out.println(s.length()); //10
System.out.println("--------");
// char charAt(int index):获取指定索引处的字符
System.out.println(s.charAt(0)); //h
System.out.println(s.charAt(1)); //e
System.out.println("--------");
// int indexOf(String str):获取str在字符串对象中第一次出现的索引
System.out.println(s.indexOf("l")); //2
System.out.println(s.indexOf("owo")); //4
System.out.println(s.indexOf("ak")); //-1
System.out.println("--------");
// String substring(int start):从start开始截取字符串
System.out.println(s.substring(0)); //helloworld
System.out.println(s.substring(5)); //world
System.out.println("--------");
// String substring(int start,int end):从start开始,到end结束截取字符串
// [start,end)
System.out.println(s.substring(0, s.length())); //helloworld
System.out.println(s.substring(3, 8)); //lowor
}
3、String的转换
char[] toCharArray():把字符串转换为字符数组
StringtoLowerCase():把字符串转换为小写字符串
StringtoUpperCase():把字符串转换为大写字符串
publicstaticvoidmain(Stringargs[]) {
// 创建字符串对象
Strings="abcde";
// char[] toCharArray():把字符串转换为字符数组
char[] chs=s.toCharArray();
for (intx=0; x<chs.length; x++) {
System.out.println(chs[x]);
}
System.out.println("-----------");
// String toLowerCase():把字符串转换为小写字符串
System.out.println("HelloWorld".toLowerCase());
// String toUpperCase():把字符串转换为大写字符串
System.out.println("HelloWorld".toUpperCase());
}
其他方法
去除字符串两端空格:Stringtrim()
按照指定符号分割字符串:String[] split(Stringstr)
publicstaticvoidmain(Stringargs[]) {
// 创建字符串对象
Strings1="helloworld";
Strings2=" helloworld ";
Strings3=" hello world ";
System.out.println("---"+s1+"---");
System.out.println("---"+s1.trim() +"---");
System.out.println("---"+s2+"---");
System.out.println("---"+s2.trim() +"---");
System.out.println("---"+s3+"---");
System.out.println("---"+s3.trim() +"---");
System.out.println("-------------------");
// String[] split(String str)
// 创建字符串对象
Strings4="aa,bb,cc";
String[] strArray=s4.split(",");
for (intx=0; x<strArray.length; x++) {
System.out.println(strArray[x]);
}
}
输出结果:
---helloworld---
---helloworld---
---helloworld---
---helloworld---
---helloworld---
---helloworld---
-------------------
aa
bb
cc
4、String的不可变性
- 不可变的好处:
- 可以实现多个变量引用堆内存中的同一个字符串实例,避免创建的开销
- 出于安全性考虑
- HashMap中的key为String类型
- 当我们在传参的时候,使用不可变类不需要去考虑谁可能会修改其内部的值,如果使用可变类的话,可能需要每次记得重新拷贝出里面的值,性能会有一定损失
示例
这里采用享元模式(共享元素的模式,一个系统中如果有多处用到了相同的元素,只需要储存一次,让所有地方都使用这一个),当使用直接赋值的方式时,每生成一个对象就将其存入共享的常量池(String Pool)内,当第二次生成同样内容的时候就共享此对象而不是创建新的
5、字符串常量池
1、常量池表(Constant_Pool table)
- Class文件中存储所有常量(包括字符串)的table
- 其实就是Class文件中的字节码指令
2、运行时常量池(Runtime Constant Pool)
- JVM内存中方法区的一部分,这是运行时的内容,这部分内容(绝大部分)是随着JVM运行的时候,从常量池转化而来,每个Class对应一个运行时常量池
- 除了Class中常量池内容,还可能包括动态生成并加入这里的内容
3、字符串常量池(String Pool)
- 是JVM实例全局共享的,全局只有一个,也在方法区中
- JVM规范要求进入这里的String实例叫“被驻留的Interned string”,各个JVM可以有不同的实现,HotSpot是设置了一个哈希表StringTable来引用堆中的字符串实例,被引用就是被驻留
6、字符串创建的详细分析
intx=10;
Stringy="hello";
- 1、首先10和"hello"会在经过javac(或其他编译器)编译过后变为class文件中
constant_pool table
的内容- 2、当我们的程序(JVM)运行时,每个Class
constant_pool table
中的内容会被加载到JVM内存中的方法区中各自Class的Runtime Constant Pool
- 3、一个没有被String Pool包含的Runtime Constant Pool中的字符串("hello")会被加入到String Pool中(HotSpot使用hashtable引用方式),步骤如下:
- 在Java Heap(堆)中根据"hello"字面量create一个字符串对象
- 将字面量"hello"与字符串对象的引用(地址)在hashtable中关联起来键-值
形式是:“hello”=对象的引用地址
- 如何判断一个新的字符串出现在Runtime Constant Pool中是否需要在Java Heap中创建新对象呢?
会先根据equals来比较Runtime Constant Pool中的这个字符串是否和StringPool中某一个相等,如果有就不创建直接引用,如果没有就执行第三步- 例如:
使用Strings=newString("hello");会创建几个对象
答:会创建2个对象
首先,出现了字面量"hello",那么去StringPool中查找是否有相同字符串存在.
// 因为"hello"是匿名对象,所以要用到直接赋值的创建方式,去常量池中查找
因为程序就这一行,所以肯定没有,那么就在JavaHeap中用字面量"hello"首先创建1个String对象。
接着,newString("hello"),关键字new又在JavaHeap中创建了1个对象,然后调用接收String
参数的构造器进行了初始化。
最终s的引用是后面这个String对象.
6、StringBuffer和StringBuilder
1、概述
- StringBuilder:
- (单线程)非线程安全的,不能同步访问
- 可变的字符序列
- 执行速度最快
- 继承于AbstractStringBuilder
- 实现了CharSequence接口
- StringBuffer:
- 多线程安全的(可以同步访问)
- 继承于AbstractStringBuilder
publicfinalclassStringBuilder
extendsAbstractStringBuilder
implementsjava.io.Serializable, CharSequence{
}
2、常用方法
1、insert
privatestaticvoidtestInsertAPIs(){
System.out.println("---------- testInsertAPIs -----------");
StringBuildersbuilder=newStringBuilder();
// 在位置0处插入字符数组
sbuilder.insert(0, newchar[]{'a', 'b', 'c', 'd', 'e'}); // abcde
// 在位置0处插入字符数组。0表示字符数组起始位置,3表示长度
sbuilder.insert(0, newchar[]{'A', 'B', 'C', 'D', 'E'}, 0, 3); // ABCabcde
// 在位置0处插入float
sbuilder.insert(0, 1.414f); // 1.414ABCabcde
// 在位置0处插入double
sbuilder.insert(0, 3.14159d);
// 在位置0处插入boolean
sbuilder.insert(0, true);
// 在位置0处插入char
sbuilder.insert(0, '\n');
// 在位置0处插入int
sbuilder.insert(0, 100);
// 在位置0处插入long
sbuilder.insert(0, 12345L);
// 在位置0处插入StringBuilder对象
sbuilder.insert(0, newStringBuilder("StringBuilder"));
// 在位置0处插入StringBuilder对象。6表示被在位置0处插入对象的起始位置(包括),13是结束位置(不包括)
sbuilder.insert(0, newStringBuilder("STRINGBUILDER"), 6, 13);
// 在位置0处插入StringBuffer对象。
sbuilder.insert(0, newStringBuffer("StringBuffer"));
// 在位置0处插入StringBuffer对象。6表示被在位置0处插入对象的起始位置(包括),12是结束位置(不包括)
sbuilder.insert(0, newStringBuffer("STRINGBUFFER"), 6, 12);
// 在位置0处插入String对象。
sbuilder.insert(0, "String");
// 在位置0处插入String对象。1表示被在位置0处插入对象的起始位置(包括),6是结束位置(不包括)
sbuilder.insert(0, "0123456789", 1, 6);
sbuilder.insert(0, '\n');
// 在位置0处插入Object对象。此处以HashMap为例
HashMapmap=newHashMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
sbuilder.insert(0, map);
System.out.printf("%s\n\n", sbuilder);
}
2、append
/**
* StringBuilder 的append()示例
*/
privatestaticvoidtestAppendAPIs() {
System.out.println("------------------- testAppendAPIs -----------------
--");
StringBuildersbuilder=newStringBuilder();
// 追加字符数组
sbuilder.append(newchar[]{'a','b','c','d','e'});
// 追加字符数组。0表示字符数组起始位置,3表示长度
sbuilder.append(newchar[]{'A','B','C','D','E'}, 0, 3);
// 追加float
sbuilder.append(1.414f);
// 追加double
sbuilder.append(3.14159d);
// 追加boolean
sbuilder.append(true);
// 追加char
sbuilder.append('\n');
// 追加int
sbuilder.append(100);
// 追加long
sbuilder.append(12345L);
// 追加StringBuilder对象
sbuilder.append(newStringBuilder("StringBuilder"));
// 追加StringBuilder对象。6表示被追加对象的起始位置(包括),13是结束位置(不包括)
sbuilder.append(newStringBuilder("STRINGBUILDER"), 6, 13);
// 追加StringBuffer对象。
sbuilder.append(newStringBuffer("StringBuffer"));
// 追加StringBuffer对象。6表示被追加对象的起始位置(包括),12是结束位置(不包括)
sbuilder.append(newStringBuffer("STRINGBUFFER"), 6, 12);
// 追加String对象。
sbuilder.append("String");
// 追加String对象。1表示被追加对象的起始位置(包括),6是结束位置(不包括)
sbuilder.append("0123456789", 1, 6);
sbuilder.append('\n');
// 追加Object对象。此处以HashMap为例
HashMapmap=newHashMap();
map.put("1", "one");
map.put("2", "two");
map.put("3", "three");
sbuilder.append(map);
sbuilder.append('\n');
// 追加unicode编码
sbuilder.appendCodePoint(0x5b57); // 0x5b57是“字”的unicode编码
sbuilder.appendCodePoint(0x7b26); // 0x7b26是“符”的unicode编码
sbuilder.appendCodePoint(0x7f16); // 0x7f16是“编”的unicode编码
sbuilder.appendCodePoint(0x7801); // 0x7801是“码”的unicode编码
System.out.printf("%s\n\n", sbuilder);
}
3、replace
reverse():反转
replace(start,end,str):从[start,end)替换str
setCharAt(n, ch):在位置n上设置字符ch
/**
* StringBuilder 的replace()示例
*/
privatestaticvoidtestReplaceAPIs() {
System.out.println("------------------- testReplaceAPIs-------------------");
StringBuildersbuilder;
sbuilder=newStringBuilder("0123456789");
sbuilder.replace(0, 3, "ABCDE"); // [0,3)
System.out.printf("sbuilder=%s\n", sbuilder);// sbuilder=ABCDE3456789
sbuilder=newStringBuilder("0123456789");
sbuilder.reverse();
System.out.printf("sbuilder=%s\n", sbuilder);// 9876543210
sbuilder=newStringBuilder("0123456789");
sbuilder.setCharAt(0, 'M');
System.out.printf("sbuilder=%s\n", sbuilder);// M123456789
System.out.println();
}
4、delete
- deleteCharAt(n):删除位置n上的字符
- delete(start,end):删除字符[start,end)
- substring(n):获取从位置n开始的字符串
- substring(start,end):获取从[start,end)的字符串
- subSequence(start,end):获取从[start,end)的字符串(CharSequence对象)
privatestaticvoidtestDeleteAPIs() {
System.out.println("------------------- testDeleteAPIs -------------------");
StringBuildersbuilder=newStringBuilder("0123456789");
// 删除位置0的字符,剩余字符是“123456789”。
sbuilder.deleteCharAt(0);
// 删除位置3(包括)到位置6(不包括)之间的字符,剩余字符是“123789”。
sbuilder.delete(3,6);
// 获取sb中从位置1开始的字符串
Stringstr1=sbuilder.substring(1);
// 获取sb中从位置3(包括)到位置5(不包括)之间的字符串
Stringstr2=sbuilder.substring(3, 5);
// 获取sb中从位置3(包括)到位置5(不包括)之间的字符串,获取的对象是CharSequence对象,此处转型为String
Stringstr3= (String)sbuilder.subSequence(3, 5);
System.out.printf("sbuilder=%s\nstr1=%s\nstr2=%s\nstr3=%s\n",
sbuilder, str1, str2, str3);
}
5、index
indexof(str):找到字符串str第一次出现的下标
indexof(str,n):找到字符串str第一次出现的下标,从n开始
lastIndexof(str):从后往前,找到字符串str第一次出现的下标
lastIndexof(str,n):从后往前,找到字符串str第一次出现的下标,从n开始
/**
* StringBuilder 中index相关API演示
*/
privatestaticvoidtestIndexAPIs() {
System.out.println("-------------------------------- testIndexAPIs --------------------------------");
StringBuildersbuilder=newStringBuilder("abcAbcABCabCaBcAbCaBCabc");
System.out.printf("sbuilder=%s\n", sbuilder);
// 1. 从前往后,找出"bc"第一次出现的位置 1
System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\")",
sbuilder.indexOf("bc"));
// 2. 从位置5开始,从前往后,找出"bc"第一次出现的位置 22
System.out.printf("%-30s = %d\n", "sbuilder.indexOf(\"bc\", 5)",
sbuilder.indexOf("bc", 5));
// 3. 从后往前,找出"bc"第一次出现的位置 22
System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\")",
sbuilder.lastIndexOf("bc"));
// 4. 从位置4开始,从后往前,找出"bc"第一次出现的位置 4
System.out.printf("%-30s = %d\n", "sbuilder.lastIndexOf(\"bc\", 4)",
sbuilder.lastIndexOf("bc", 4));
System.out.println();
}
6、其他API
/**
* StringBuilder 的其它API示例
*/
privatestaticvoidtestOtherAPIs() {
System.out.println("----------- testOtherAPIs -----------");
StringBuildersbuilder=newStringBuilder("0123456789");
intcap=sbuilder.capacity();
System.out.printf("cap=%d\n", cap);
/*
capacity()返回的是字符串缓冲区的容量
StringBuffer( ); 分配16个字符的缓冲区
StringBuffer( int len ); 分配len个字符的缓冲区
StringBuffer( String s ); 除了按照s的大小分配空间外,再分配16个 字符的缓冲区
你的StringBuffer是用字符构造的,"0123456789"的长度是10另外再分配16个字符,所
以一共是26。
*/
charc=sbuilder.charAt(6);
System.out.printf("c=%c\n", c);
char[] carr=newchar[4];
sbuilder.getChars(3, 7, carr, 0); // 将[3,7)的值,依次从下标0的位置上开始赋值
for (inti=0; i<carr.length; i++){
System.out.printf("carr[%d]=%c ", i, carr[i]);
}
System.out.println();
}
3、小结
- String:字符串常量
- StringBuffer:字符串变量(线程安全)
- StringBuilder:字符串变量(非线程安全)
- 执行速度方面:StringBuilder > StringBuffer > String
区别
- String和StringBuffer类型的主要区别在于String是不可变的对象
- 每次在对String更改之后其实等同于生成了一个新的String对象,然后再将指针指向新生成的String对象
- 所以经常改变内容的字符串最好不要用String对象,因为每次生成对象都会对系统的性能产生影响,特别是内存中无引用对象变多了之后,JVM的GC垃圾回收器就会开始工作
- 而StringBuffer类操作时就只是对其本身进行操作,而不是生成新的对象。经常改动字符串内容的时候推荐使用StringBuffer对象
特殊情况:
StringS1=“Thisisonlya”+“simple”+“test”;
StringBufferSb=newStringBuilder(“Thisisonlya”).append(“
simple”).append(“test”);
/*
这里是因为JVM把S1看作This is only a simple test
如果拼接的字符串是来自其他的String对象,速度就没那么快了
*/
使用场景
- 操作少量的数据用String
- 单线程操作字符串缓冲区下操作大量数据(需要经常修改)用StringBuilder
- 多线程操作字符串缓冲区下操作大量数据(需要经常修改)用StringBuffer
- StringBuilder提供与StringBuffer相同的API,通常被用做StringBuffer的简易替换(一般情况下都是单线程且StringBuilder的运行速度更快)
面试题
StringBuilder与StringBuffer的区别,StringBuilder和String的区别
- StringBuilder的效率更高,线程不安全,StringBuilder线程安全,效率低
- String是不可变的,StringBuilder是可变的
- 如果只是简单使用,不需要对字符串进行频繁修改可以用String,反之就需要用StringBuilder(String会产生多余的字符串,占用内存空间)