欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

java深入理解の剖析String和StringBuilder

程序员文章站 2022-04-15 19:03:07
文章目录1、基本用法2、走进String内部(基于Java 7)3、编码转换4、不可变性5、常量字符串6、hashCodejava中处理字符串的主要类是String和StringBuilder,我们分别介绍String的基本用法和原理,分析String的不可变性,常量字符串、hashCode和正则表达式1、基本用法定义String:'通过常量定义String变量:'String name = "Fan";'通过new创建String变量:'String name = new String("...


java中处理字符串的主要类是 StringStringBuilder,我们分别介绍String的基本用法和原理,分析String的不可变性,常量字符串、hashCode和正则表达式

1、基本用法

定义String:

'通过常量定义String变量:'
String name = "Fan";

'通过new创建String变量:'
String name = new String("Fan");

'直接使用+和+=运算符:'
String name = "Fan";
name +="是我";
String description = ",是我java";
System.out.println(name+description);//Fan是我,是我java

String的基本方法:

1int length():获取长度
2char charAt(int index);根据位置获取位置上某个字符。
3int indexOf(int ch):返回的是ch在字符串中第一次出现的位置。
4int indexOf(int ch,int fromIndex):从fromIndex指定位置开始,获取ch在字符串中出现的位置。
5int indexOf(String str):返回的是str在字符串中第一次出现的位置。
6int indexOf(String str,int fromIndex):从fromIndex指定位置开始,获取str在字符串中出现的位置。
7int lastIndexOf(String str):反向索引。
8boolean contains(str);字符串中是否包含某一个子串
9boolean isEmpty():原理就是判断长度是否为010boolean startsWith(str);字符串是否以指定内容开头。
11boolean endsWith(str);字符串是否以指定内容结尾。
12boolean equals(str);判断字符内容是否相同
13boolean.equalsIgnorecase();判断内容是否相同,并忽略大小写。
14、String trim();将字符串两端的多个空格去除
15int compareTo(string);对两个字符串进行自然顺序的比较
16、String toUpperCsae() 大转小 String toLowerCsae() 小转大
17、 String subString(begin); String subString(begin,end);获取字符串中子串
18、String replace(oldchar,newchar);将字符串指定字符替换。

简单举例:

1int length():获取长度
String s = "We are students";
int len=s.length();
2char charAt(int index);根据位置获取位置上某个字符。
String s = "We are students";
char c = s.charAt(14);
3int indexOf(int ch):返回的是ch在字符串中第一次出现的位置。
String s = "We are students";
int num = s.indexOf("s");
4、String[] spilt();分割字符
String str = "Hello,world";
String[] arr = str.split(",");
arr[0]="Hello",arr[1] = "world";

2、走进String内部(基于Java 7)

String类内部用一个字符数组表示字符串,实例变量表示为:

private final char value[];

String有两个构造方法,可以根据char数组创建String变量:

public String(char value[])
public String(char value[],int offset,int count)  

需要说明的是,String会根据参数新创建一个数组,并赋值内容,而不会直接用参数中的字符数组。String中大部分方法内部也都是操作的这个字符数组。如:
1)length()方法返回的是这个数组的长度。
2)substring()方法是根据参数,调用构造方法String(char value[],int offset,int count)新建一个字符串。
3)indexOf()方法查找字符或子字符串时是在这个数组中进行查找

3、编码转换

String内部是按照UTF-16BE处理字符的,对于BMP字符,使用一个char,两个字节,对于增补字符,使用两个char,四个字节。不同的编码可能用于不同的字符集,使用不同的字节数目,以及不同的二进制表示。那么这些编码与java内部之间如何相互转换呢?

Java使用Charset类表示各种编码,他有两个常用的静态方法:

public static Charset defaultCharset()
public static Charset forName(String charsetName)

第一个方法返回系统的默认编码,比如,在笔者的计算机中执行如下语句:

System.out.println(Charset.defaultCharset().name());
'输出为UTF-8'

第二个方法返回给定编码名称的Charset对象,Charset名称可以是US-ASCII、ISO-8859-1、windows-1252,GB2312、GBK、GB18030、Big5、UTF-8等,比如:

Charset charset = Charset.forName("GBK");

String类提供如下方法,返回字符串按给定编码字节表示,需要注意的是这几个方法返回的都是byte数组

public byte[] getBytes()//使用系统默认编码
public byte[] getBytes(String charsetName)//方法参数为编码名称
public byte[] getBytes(Charset charset)//方法参数为Charset对象

String类有如下构造方法,可以根据字节和编码创建字符串,也就是说,根据给定编码的字节显示,创建Java的内部表示:

public String(byte bytes[], int offset, int length,String charsetName)
public String(byte byte[],Charset charset)

字符串之间的转换,link

4、不可变性

与包装类类似,String类也是不可变类,即对象一旦创建,就没有办法修改了。类也声明为了final,不能被继承,内部char数组value也是final的,初始化后就不能再变了。

String看似提供了很多修改的方法,其实是通过创建新的String对象来实现的,原来的String对象不会被修改。比如:
concat()方法:

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

通过Arrays.copyOf方法创建了一块新的字符串数组,复制原内容,然后通过new创建了一个新的String,然后最后一行调用的是String的另一个构造方法,定义为:

'这是个非公开的构造方法,直接使用传递过来的数组作为内部数组:'
  String(char[] value, boolean share){
  		//assert share : "unshared not supported"
  		this.value = value;	
  }

与包装类类似,定义为不可变类,程序可以简单、安全、容易理解。但是如果是频繁使用修改字符串,应该考虑使用StringBuilder和StringBuffer这两个类。(1.8以后,java内部修改字符串时,在使用String时,都被内部替换为了StringBuffer,但是如果要大量的修改的字符串,就需要java内部不断替换,可能会出现替换不完全的情况,从而浪费了空间,因而尽量使用StringBuffer类

5、常量字符串

常量字符串是非常特殊的:除了可以直接赋值给String变量外,它自己就行一个String类型的对象,可以直接调用String的各种方法。如下:

System.out.println("Fanlee".length());
System.out.println("Fanlee".indexOf("e"));
System.out.println("Fan".contains("a"));

实际上,这些常量就是String类型的对象,在内存中,他们被放在一个共享的地方,称为字符串常量池(1.7以后这个常量池位于堆中的一块区域)他保存所有的常量字符串,每个常量只会保存一份,被使用者共享。当通过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应的String类型的对象
如下代码:

String name1 = "Fan";
String name2 = "Fan";
System.out.println(name1==name2);//true

输出为true。可以认为,"Fan"在常量池中有一个对应的String类型的对象,我们假定名称为xiaoli,上面代码实际上类似于:

String xiaoli = new String(new char[]{'F','a','n'});
String name1 = xiaoli;
String name2 = xiaoli;
System.out.println(name1==name2);//true

实际上只有一个String对象,三个变量都指向字符串常量池中的这个对象。因此相等。

但是如果不是 通过常量直接创建的,而是通过new创建的,==就是false了

String name1 = new String("Fan");
String name2 = new String("Fan");
System.out.println(name1==name2);//false

上面代码类似于:

String xiaoli = new String(new char[]{'F','a','n'});
String name1 = new String(xiaoli);
String name2 = new String(xiaoli);
System.out.println(name1==name2);//false

String类中以String为参数的构造方法如下:

public String(String original){
	this.value = original.value;
	this.hash = original.hash;
}

hash时String类中另一个实例变量,表示缓存的hashCode值可与看出name1和name2指向不同的String对象。只是这两个对象内部的value值指向相同的char数组。其内存布局如图所示:
java深入理解の剖析String和StringBuilder图中name1!=name2(指向的两个对象不同)。
name1.equals(name2),是true(指向的两个对象的value相同),因为String重写了equals方法。

6、hashCode

在上面提到的hash这个实例变量,他的定义如下:

private int hash; // Default to 0

hash变量缓存了hashCode方法中的值,也就是说,第一次调用hashCode方法的时候,会把结果保存在hash这个变量中,以后再调用就直接返回保存的值

String类的hashCode方法,代码如下:

public int hashCode(){
	int h = hash;
	if(h==0&& value.length>0){
		char val[] = value;
		for(int i = 0;i<value.length;i++){
			h = 31*h+val[i];
		}
		hash = h;
	}
	return h;
}

如果缓冲的hash不为0,就直接返回了,否则根据字符数组中的内容计算hash,计算方法是:

s[0]*31^(n-1)+s[1]*31^(n-2)+...+s[n-1]

s表示字符串,s[0]表示第一个字符,n表示字符长度,s[0]*31^(n-1)表示31的(n-1)次方,再乘以第一个字符的值。

为什么要采用这种方式呢?
使用这个式子hash值与每一个字符值都有关,也与每一个字符的位置有关,位置i(int>=1)的因素通过31的(n-i)次方表示,使用31大致是因为两个原因:一方面可以产生更分散的散列,即不同字符串hash值也不一样。另一方面计算效率比较高,31h与32h-h即(h<<5)-h等价。可以用更高效率的移位和减法操作代替乘法操作。

7、正则表达式

正则表达式可以理解为一个字符串,但是表达的是一个规则,一般用于文本的匹配、查找、替换等。在这做一些简略的介绍:
Java中有专门的类(Pattern和Matcher)用于正则表达式,但对于简单的情况,String类提供了更为简洁的操作,String中接受正则表达式的方法有:

public String[] split(String regex) //分隔字符串
public boolean matches(String regex) //检查是否匹配
public String replaceFirst(String regex, String replacement) //字符串替换
public String replaceAll(String regex, String replacement) //字符串替换

在Java9对String进行了优化,它的内部不再是char数组,而是byte数组,如果字符都是ASCII字符,他就可以使用一个字节表示一个字符了,而不用UTF-16BE,节省内存

8、剖析StringBuilder

字符串的的修改操作比较频繁,应该采用StringBuilderStringBuffer类,这两个 类的方法基本是完全一样的,它们的代码实现完全一样,唯一不同的是。StringBuilder是线程不安全的,StringBuffer是线程安全的。
线程安全是需要成本的,影响性能,而字符串对象及操作大部分情况下不存在线程安全问题,适合使用StringBuilder类。

基本用法

创建StringBuilder对象:

StringBuilder sb = new StringBuilder();

通过append方法添加字符串:

sb.append("Fan");
sb.append(",lee");

通过toString方法获取构建之后的字符串:

System.out.println(sb.toString());
'输出:' Fan,lee

基本实现原理

介绍一下StringBuilder的内部组成及一些主要方法的实现,代码基于Java 7。
与String类似,StringBulider类也封装了一个字符数组,定义如下:

char value[];

与String不同,他不是final修饰的,可以改变。另外,与String不同,字符串数组中不一定所有位置都已经被使用,他有一个实例变量,表示数组中已经使用的字符个数

int count;

StringBuilder继承自AbstractStringBuilder,他的默认的构造方法是:

public StringBuilder(){
	super(16);
}

调用的父类构造方法,父类对应的构造方法是:

AbstractStringBuilder(int capacity){
	value = new char[capacity]
}

也就是说,new StringBuilder()代码内部会创建一个长度为16的字符数组,count 的默认值是0。而append方法的代码是:

public AbstractStringBuilder append(String str){
	if(str==null) str="null";
	int len = str.length();
	ensureCapacityInternal(count + len);//count + len共需要的长度
	str.getChar(0,len,value,count);
	count +=len;
	return this;
}

append会直接复制字符串到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度用count体现。具体来说,ensureCapacityInternal(count + len)会确保数组中的长度足以容纳添加的字符,str.getChars方法会复制新添加的字符到字符数组中,count +=len会增加实际使用的长度。

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)

srcebegin——这是要复制的字符串中第一个字符的索引。
srcEnd——这是要复制的字符串中最后一个字符之后的索引。
dst——这是目标数组。
dstBegin——这是目标数组中的起始偏移量。

ensureCapacityInternal代码如下:

public void ensureCapacityInternal(int minimumCapacity){
	//overflow-conscious code
	if(minimumCapacity-value.length>0)//所需要的长度大于原数组长度
		expandCapacity(minimumCapacity);//调用expandCapacity
}

如果字符数组的长度小于需要的长度,则调用expandCapacity进行扩展,代码是:

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;
	}
	value = Arrays.copyOf(value,newCapacity);
}

扩展的逻辑是:分配一个足够长的新数组,然后将原内容复制到这个新数组中,最后让内部的字符数组指向这个新数组,这个逻辑主要靠下面代码实现:

	value = Arrays.copyOf(value,newCapacity);

newCapacity是怎么算出来的呢?
参数minimumCapacity表示需要的最小长度(也就是count+len),需要多少分配多少不可以吗?显然不行,因为那样就和String一样了,每append一次,都需要进行一次内存分配,效率低下。
这里的扩展策略是:跟当前长度相关,当前长度乘以2,再加上2,如果这个长度还是不够最小需要长度,采用minimumCapacity。

比如:默认长度位16 ,
长度不够时,会先扩展到162+2=34,然后就扩展到342+2=70,然后是70*2+2=142,这是一种值数扩展策略。为什么时加2?因为这样,原长度为0时也一样工作。

这样扩展的原因:这是一种折中策略,一方面减少内存分配次数,另一方面要避免空间浪费。在不知道最终需要多长的情况下,指数扩展是一种常用策略,广泛用于各种内存分配相关的计算机程序中

但是如果预先知道需要多长,那么可以调用StringBuilder的另外一个构造方法:

public StingBuilder(int Capacity)

字符串构建完后,我们来看toString方法的代码:

public String toString(){
	//create a copy, don't share the array
	return new String(value,0,count);
}

注意:上面是基于内部数组新建了一个String,这个String构造方法不会直接用value数组,而会新建一个,以保证String的不可变性。

除了appendtoString 方法,StringBuilder还有很多方法,包括更多的构造方法、更多append方法,插入、删除、替换、翻转、长度有关的方法。
主要看下插入方法,在指定索引offset处插入字符串str

public StringBuilder insert(int offset,String str)

原来的字符后移,offset0表示在开头插,为length()表示在结尾插:比如:

StringBuilder sb = new StringBuilder();
sb.append("Fan");
sb.insert(0,"我是");
sb.insert(sb.length(),"。");
sb.insert(2,"小李,代号");
Sysytem.out.println(sb.toString());//我是小李,代号Fan。

然后,insert的实现代码是:

public AbstractStringBuilder insert(int offset,String str){
	if((offset<0) || (offset > length()))
		throw new StringIndexOutOfBoundsException(offset);
	if((str == null))
		str = "null";
	
	int len = str.length();
	ensureCapacityInternal(count + len);
	Sysytem.out.println(value,offset,value,offset+len,count-offset);
	str.getChars(value,offset);
	count +=len;
	return this;
}

这个实现的思路是:在确保有足够长度后,首先将原数组中offset开始的内容向后挪动n个位置,n为待插入字符串长度,然后将待插入字符串复制进offset位置。

挪动位置调用了System.arraycopy()方法,这是个比较常用的方法。其声明如下:

public static native void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)

将数组srcsrcPos开始的length个元素复制到数组dest中的destPos处,这个方法的优点是:即使src和dest是同一个数组,他也可以正确处理。如下:

int[] arr = new int[]{1,2,3,4};
System.arraycopy(arr,1,arr,0,3);
"则此时arr数组内容是{2,3,4,4}"

arraycopy的声明有一个修饰符native,表示他的实现是通过java本地接口实现的。Java本地接口是Java提供的一种技术,用于在Java 中调用非Java实现的代码,实际上arraycopy是用c++语言实现的。(c++实现效率要远远高于java)

String的+和+=运算符

Java中,String可以直接使用+和+=运算符,这是Java编译器提供的支持,背后,Java编译器一般会生成StringBuilder,+和+=操作会转换为append。如:

String hello = "hello";
hello += ",world";
System.out.println(hello);

背后Java会转换为:

StringBuilder hello = new StringBuilder(hello );
hello.append(",world");
System.out.println(hello.toString());

在简单情况下,确实没必要使用StringBuilder替换String.但是在稍微复杂的情况下,java编译器可能没有那么只能,他可能会生成过多的StringBuilder,由其是在循环中:如下:

String h = "Fan";
for(int i=0;i<3;i++){
	h += ",Fan"
}
System.out.println(h);

Java替换后代码是:

String h = "Fan";
for(int i=0;i<3;i++){
    StringBuilder sb = new String(h)
	sb.append(",Fan")
	h = sb.toString();
}
System.out.println(h);

在循环内部,每一次执行+=操作都会生成一个StringBuilder。
所以,简单情况可以直接使用String的+和+=,对于复杂情况,尤其是有循环的时候,应该直接使用StringBuilder,如下:

StringBuilder h = new String("Fan");
for(int i=0;i<3;i++){
	h.append(",Fan")
}
System.out.println(h.toString());

本文地址:https://blog.csdn.net/fan__lee/article/details/109571926