一、java.lang.String类
1、String类和字符串对象的特点
(1)它在java.lang包,使用它不需要导包
(2)它是final修饰的类,不能被继承
面试题?问,能继承String类吗?
(3)它是支持比较大小的,因为它实现Comparable接口(自然排序)
两个字符串比较大小, 字符串1.compareTo(字符串2) 结果是>0,<0,=0,分别表示字符串1大于,小于,等于字符串2
补充:字符串不仅支持Comparable接口(自然排序),还有一个类实现Comparator接口(定制排序)
这个类是 java.text.Collator,它实现Comparator接口,有 compare方法,用于比较两个字符串大小。
Collator 类执行区分语言环境的 String 比较。
它是个抽象类,可以调用静态方法static Collator getInstance(Locale desiredLocale) 获取Collator 的子类对象
(4)字符串是代表一个字符序列,即一串字符
这串字符的长度可以是0~n。
例如:"" 空字符串,字符串的长度为0
"a" 或 " " 一个字符的字符串,长度为1,但是他们不同于 'a' 和 ' ',后面这个是char类型,是基本数据类型。
"hello" 一个多字符的字符串,长度为5
char类型的单引号中间 有且只有一个字符,
之前有同学写'12',这样报错,因为12是两个字符,从数字角度是一个数字值,但是从字符的角度是2个字符。
(5)字符串底层(内部)是使用char[]进行保存的。
即"hello" --> 底层 --> {'h','e','l','l','o'}
(6)String类型的对象是不可变
什么是对象不可变?
一个字符串对象一旦创建好了,它的字符串对象的内容和长度就不能修改了。
如果对某个字符串对象进行修改,例如:拼接,替换等,都会产生新对象,即不是在原来的字符串对象中直接修改的。
为什么不可变?
A:在String类中用private final char value[];来存储字符串内容
private:私有的,外面是无法直接访问/操作value数组的,即外面无法直接得到value数组的首地址,然后修改元素
final:不能对value变量重新赋值,即不能让value指向新的数组,这就导致我们无法通过扩容,复制等方式来修改字符串的长度
B:String类中所有的方法(包括concat,replace等),对字符串的修改都不是直接对当前字符串的value数组进行修改的,而是重新new了一个对象。
为什么要设计成不可变?
字符串对象是Java程序中最最常见的一种对象,即程序中会大量出现字符串对象。
那么如果字符串对象不可变的话,就可以“共享”字符串对象。
例如:"hello"无论在程序中出现几次,我们使用的是同一个字符串对象,可以节省大量的内存。
这些共享对象存在哪里?
字符串常量池中。
字符串常量池在哪里?
因为JVM的品牌不同,JDK版本不同,那么字符串常量池都会有所不同。
Oracle的hotspot的JVM,在JDK1.6时,字符串常量池在方法区,
在JDK1.7时,字符串常量池在堆,
在JDK1.8时,字符串常量池在元空间,
(7)在JDK1.9时,String内部的value数组,从char[]类型,变为byte[]。
为什么?
因为Java中一个char是占2个字节。而Java程序中大多数的字符串都是由ASCII码表中的字符构成的,
这些字符本身的存储只需要1个字节就够了,那么分配2个字节就有点浪费。
所以它修改为byte[],如果是1个字节就能存的,就用一个字节,一个字节不够的,用2个字节。
可以节省内存。
(8)字符串对象的hash值,有一个hash属性存储。
private int hash;
因为字符串对象不可变,所以直接存在hash值,下次获取时,就不用现计算。
如果没有存,就意味着,每次用的时候,需要调用hashCode函数,根据算法现计算。
如果存了hash,每次用的时候,调用hashCode函数,直接返回hash中存的值。
public class TestString {
@Test
public void test06() {
String str = "hello"; //str中存储的是"hello"对象的首地址
str = str + "world";//把新对象的首地址重新赋值给str变量,现在str中存储的是"helloworld"对象的首地址
//这里修改的不是"hello"对象,而是str变量中存储的首地址
System.out.println(str);
}
@Test
public void test05() {
String str1 = "hello";
String str2 = "hello";
System.out.println(str1 == str2);//true
}
@Test
public void test04(){
String str = "hello";
System.out.println(str + "world");//helloworld 这里打印的是新字符串对象
System.out.println(str);//hello
}
@Test
public void test03(){
String str = "hello";
str.concat("world");//拼接 拼接后产生新的字符串对象,如果没有接收,就丢了
System.out.println(str); //hello
}
@Test
public void test02(){
String[] arr = {"张三","李四","王五","赵六","柴林燕"};
Arrays.sort(arr); //按照元素类型的自然排序规则进行排序的,即按照String类型实现Comparator接口的compareTo方法排序
System.out.println(Arrays.toString(arr));//[张三, 李四, 柴林燕, 王五, 赵六]
Arrays.sort(arr, Collator.getInstance(Locale.CHINA));
System.out.println(Arrays.toString(arr));//[柴林燕, 李四, 王五, 张三, 赵六]
}
@Test
public void test01(){
// java.text.Collator,它实现Comparator接口,有 compare方法,用于比较两个字符串大小。
String s1 = "张三";
String s2 = "李四";
//自然排序比较大小,Comparable接口的compareTo方法
System.out.println(s1.compareTo(s2));//-2094 按照字符串的Unicode编码值比较大小
if(s1.compareTo(s2) < 0){
System.out.println(s1 + " < " + s2);//张三 < 李四
}else if(s1.compareTo(s2) > 0){
System.out.println(s1 + " > " + s2);
}else{
System.out.println(s1 + " = " + s2);
}
//定制排序比较大小,Comparator接口的compare方法
Collator collator = Collator.getInstance(Locale.CHINA);
System.out.println(collator.compare(s1,s2));//1 区分语言环境的 String 比较 在中文中“张”在“李”后面
if(collator.compare(s1,s2) < 0){
System.out.println(s1 + " < " + s2);
}else if(collator.compare(s1,s2) > 0){
System.out.println(s1 + " > " + s2);//张三 > 李四
}else{
System.out.println(s1 + " = " + s2);
}
}
}
二、字符串的兄弟:可变字符序列
1、String的很大一个特点:字符串对象不可变。
优点:字符串对象不可变,字符串常量对象就可以共享,节省一些内存,并且节省很多时间。
例如:程序中出现“hello"字符串常量,都是同一个,对象个数减少了,节省一些内存,不需要重修new对象,就可以节省时间和内存。
在字符串常量池中查找字符串,很方便,hash结构。
缺点:当遇到字符串的拼接等修改操作时,每次都会产生新对象,这就又浪费内存
特别是大量字符串的拼接操作,频繁的拼接操作,就很耗内存。
即使是现在Java编译已经对 大量的字符串拼接操作做了优化,优化为StringBuilder的方式,也仍然有很多“中间”的垃圾对象产生。
2、Java针对上面的缺点,设计了两个新的字符串兄弟类型,它们都是可变字符序列。
java.lang.StringBuffer(JDK1.0就有,伴随着String诞生的)
java.lang.StringBuilder(JDK1.5才有的)
3、StringBuffer和StringBuilder的区别:
StringBuffer:线程安全的可变字符序列。
StringBuilder:此类提供一个与 StringBuffer 兼容的 API,但不保证同步。(同步是解决线程安全问题的一个方案)
换句话说StringBuilder是线程不安全的。
该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。
什么是线程安全?
比喻: 杨浩波 和 蔚春晓住一个屋,就一个“卫生间”。
早上的时候起床时间差不多,都很着急。
杨浩波先进入卫生间,动作有点慢,在5分钟之内,没搞定他的事情。
此时,蔚春晓就进入了,看到一些“尴尬”的场景,这个就是线程安全问题。
两个线程在使用“共享数据”时,就会出现,一个线程还没有完全使用完数据,另一个线程插入进来,
对数据的访问或修改扥结果可能就不正确。
StringBuffer和StringBuilder的API完全“一样” 。
4、API,以StringBuffer为例,关于StringBuilder的话就是把StringBuffer换成StringBuilder即可。
(1)构造器:StringBuffer和StringBuilder的对象创建必须通过构造器,不能像字符串一样,直接""赋值
(2)StringBuffer append(各种数据类型 b):追加
(3)StringBuffer delete(int start, int end):删除[start, end)位置的字符
StringBuffer deleteCharAt(int index):删除[index]位置的字符
(4)StringBuffer insert(int offset, 各种数据类型 c):在[offset]位置插入xx
(5) StringBuffer reverse() :字符串的反转
(6)StringBuffer replace(int start, int end, String str):替换[start,end)位置的字符为str
void setCharAt(int index, char ch) :替换[index]位置的字符为ch指定字符
以下方法和String差不多
(7)int length()
(8)char charAt(int index)
(9)int indexOf(String str)
(10)int lastIndexOf(String str)
(11) String substring(int start)
String substring(int start, int end)
(12)String toString()
public class TestStringBuffer {
@Test
public void test07() {
StringBuffer buffer = new StringBuffer("hello");
buffer.append("java").append("atguigu");
//转为字符串
String str = buffer.toString();
System.out.println(str);
}
@Test
public void test06() {
StringBuffer buffer = new StringBuffer("hello");
buffer.reverse();
System.out.println(buffer);//olleh
}
@Test
public void test05() {
StringBuffer buffer = new StringBuffer("hello");
buffer.insert(2,"java").insert(0,"atguigu");
System.out.println(buffer);//atguiguhejavallo
}
@Test
public void test04(){
StringBuffer buffer = new StringBuffer("hellojavaworld");
buffer.deleteCharAt(0).delete(3,7);
System.out.println(buffer);//ellaworld
}
@Test
public void test03(){
StringBuffer buffer = new StringBuffer("hello");
buffer.append(1)
.append('a')
.append(true); //支持连写,因为append的返回值还是StringBuffer类型,而且还是buffer对象自己
System.out.println(buffer);//hello1atrue
}
@Test
public void test02(){
StringBuffer buffer = new StringBuffer("hello");
buffer.append(1);
buffer.append('a');
buffer.append(true);
System.out.println(buffer);//hello1atrue
}
@Test
public void test01(){
// StringBuffer buffer = "hello";//错误, 左边是StringBuffer类型,右边是String类型,它们是兄弟关系,是不能互相赋值的。
}
}
问?如何实现可变字符序列?
(1)StringBuffer和StringBuilder底层也是用char[] value存储字符串内容的。
这个char[] value没有用final修饰,意味着这个char[]可以扩容,缩容等。
(2)如何扩容?
哪些方法和扩容有关?
append,insert
A:append:分析
首先,StringBuffer s = new StringBuffer(); 发现new char[16];
默认char[] value数组,是长度为16.
其次,在append方法中,
如果value数组够装,就直接在原value数组中拼接,
如果value数组不够存拼接后的字符串,就先对value进行扩容,扩容为原来的2倍+2,然后在新的value数组中拼接字符串。
B:insert:分析
首先,看是否需要扩容,如果要扩容,就扩容为原来的2倍+2
然后,把插入位置[index]之后的字符往后移动,移动的位数,是和你新插入的字符串的长度。
最后,把插入的字符串放进去。
(3)删除字符
deleteCharAt,只是把被删除位置[index]后面的元素往前移动了,覆盖了删除位置[index]的字符,数组的长度没变。
(4)缩容
void trimToSize():复制了一个count长度的数组
重点调用的API:
Arrays.copyOf():用于复制数组,新数组可能是更大/更小的数组
System.arraycopy():用于移动元素
public class TestStringBuffer2 {
@Test
public void test04(){
StringBuffer s = new StringBuffer("hello");
s.deleteCharAt(2);
s.trimToSize();
}
@Test
public void test03(){
StringBuffer s = new StringBuffer("hello");
s.deleteCharAt(2);
}
@Test
public void test02(){
StringBuffer s = new StringBuffer("hello");
s.insert(2,"java");
}
@Test
public void test01(){
StringBuffer s = new StringBuffer();
s.append("hello").append("world").append("atguigu").append("java");
}
}
三者对比
public class TestTime {
@Test
public void testString(){
long start = System.currentTimeMillis();
String s = new String("0");
for(int i=1;i<=10000;i++){
s += i;
}
long end = System.currentTimeMillis();
System.out.println("String拼接+用时:"+(end-start));//367
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("String拼接+memory占用内存: " + memory);//473081920字节
}
@Test
public void testStringBuilder(){
long start = System.currentTimeMillis();
StringBuilder s = new StringBuilder("0");
for(int i=1;i<=10000;i++){
s.append(i);
}
long end = System.currentTimeMillis();
System.out.println("StringBuilder拼接+用时:"+(end-start));//5
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("StringBuilder拼接+memory占用内存: " + memory);//13435032
}
@Test
public void testStringBuffer(){
long start = System.currentTimeMillis();
StringBuffer s = new StringBuffer("0");
for(int i=1;i<=10000;i++){
s.append(i);
}
long end = System.currentTimeMillis();
System.out.println("StringBuffer拼接+用时:"+(end-start));//5
long memory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
System.out.println("StringBuffer拼接+memory占用内存: " + memory);//13435032
}
}