参考文章:https://blog.csdn.net/seu_calvin/article/details/52291082/
https://www.cnblogs.com/wxgblogs/p/5635099.html
1.首先了解一些必备的知识点
String:字符串,值在创造后不能改变,当你修改了String类就会生成一个新的字符串,旦你的程序对字符串有大量修改,那么在jvm的堆内存中就会生成大量的旧的临时垃圾字符串对象。
优点:线程安全,可以在多个线程中共享不需要加锁,第二是由于不变性所以它的hashcode可以被缓存后提升效率,这也是为什么我们见到的大多数的HashMap的key都是使用String类型的。
StringBuffer:buffer缓冲区的意思,官方解释:线程安全,可变的字符序列。 字符串缓冲区就像一个String ,但可以修改。 在任何时间点,它包含一些特定的字符序列,但可以通过某些方法调用来更改序列的长度和内容。
每个字符串缓冲区都有一个容量。 只要字符串缓冲区中包含的字符序列的长度不超过容量,就不必分配新的内部缓冲区数组。 如果内部缓冲区溢出,则会自动变大。
优点: 可以操作字符串而不会产生大量对象,而且对于string拼接的”+“号,底层其实也是使用stringbuffer或者stringbuilder来完成的。
StringBuilder:相较于StringBuffer,StringBuilder中的方法没有使用 synchronized 关键字进行修饰,可以被认为是线程不安全的,但是性能会更加好。
参考文章(两篇都很):https://www.cnblogs.com/fangfuhai/p/5500065.html
2. String Pool(字符串池)
首先看一下下面的代码,猜一下运行结果。
String str="111";
String str1="111";
String str2=new String("111");
String str3=new String("111");
System.out.println(str==str1);
System.out.println(str==str2);
System.out.println(str2==str3);
结果是true,false, false;
- java创建字符对象有两种方式
1).采用字面值的方式
2).采用new方式。
- 采用字面值时,jvm首先会去字符串池中查找有没有相同的对象,如果存在就返回这个对象,没有则创建后返回。
- 采用new方式,jvm首先去字符串池中查找,没有则创建,有则跳过,然后在堆中也创建一个对象,返回堆中对象。
字符串池的优缺点:
- 优点:避免了相同字符串的创建,节省内存。
- 缺点:牺牲了jvm在常量池中遍历对象所需要的时间,不过利大于弊。
String的String Pool是一个固定大小的Hashtable,在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定。
3. StringBuffer初始化及扩容机制
1.StringBuffer()的初始容量可以容纳16个字符,当该对象的实体存放的字符的长度大于16时,实体容量就自动增加。StringBuffer对象可以通过length()方法获取实体中存放的字符序列长度,通过capacity()方法来获取当前实体的实际容量。
2.StringBuffer(int size)可以指定分配给该对象的实体的初始容量参数为参数size指定的字符个数。当该对象的实体存放的字符序列的长度大于size个字符时,实体的容量就自动的增加。以便存放所增加的字符。
3.StringBuffer(String s)可以指定给对象的实体的初始容量为参数字符串s的长度额外再加16个字符。当该对象的实体存放的字符序列长度大于size个字符时,实体的容量自动的增加,以便存放所增加的字符。
**4.intern方法
public String intern()返回字符串对象的规范表示。
返回值是一个String对象,作用是调用时,先在字符串池中查看是否存在,存在则直接返回池中的对象,不存在则创建后返回在池中的对象。
首先几段代码,猜一下结果。
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
System.out.println(s == s2);
System.out.println(s.intern() == s);
System.out.println(s.intern() == s2);
}
结果为:false,false,true
public static void main(String[] args) {
String s2 = "1";
String s = new String("1");
System.out.println(s == s2);
System.out.println(s.intern() == s);
System.out.println(s.intern() == s2);
}
结果为:false,false, true
可以看出,无论数据库池里面是否有这个对象,new对象时返回的对象就是堆栈中的,而不是字符串池的,然后调用intern可以返回字符串池中的相应对象,接下来的代码就会有点混淆了,而且jdk6前后的结果时不一样的。
重点(s3.intern()位置不同造成的影响)
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
System.out.println(s3.intern() == s4);
System.out.println(s3.intern() == s3);
结果为:
jDK6:false,false,false
JDK7:true,true ,true
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
System.out.println(s3.intern() == s4);
System.out.println(s3.intern() == s3);
jDK6:false,false,false
JDK7:false,true, flase
- 是不是有点晕了,哈哈哈,我们先来看一下jdk7的情况:
- 首先时String s3 = new String("1") + new String("1")这条语句,string创建一个对象有两种方式,
直接赋值和new一个对象,都会字符串池中创建相应的对象,但是这条语句只在字符串池中创
建了两个”1“的对象,并没有在字符串池中创建“11”这个对象。
- 接着就是s3.intern(),因为字符串池中没有”11“,在jdk7中,字符串池中不需要再存储一份对
象了,可以直接存储堆中的**引用**。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3
会返回true。
- 主要在于你有没有一开始new的时候在字符串池和堆中创建两个不同的对象,如果有new的时候在
字符串池中创建了一个对象,则**字符串池不会使用引用**。
- 对于jdk6及之前的版本
Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm区的,Perm区是一个类静态的区域,
主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m,一旦常量池中大量
使用 intern 是会直接产生java.lang.OutOfMemoryError:PermGen space错误的。在 jdk7 的版本
中,字符串常量池已经从Perm区移到正常的Java Heap区域了。不懂可以看下面这张图,
所以在两个不同的区域存储,就不会调用引用,没有关联,s3.intern() ==s3就返回false。
5. intern的实际使用例子;
static final int MAX = 100000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
//为长度为10的Integer数组随机赋值
Integer[] sample = new Integer[10];
Random random = new Random(1000);
for (int i = 0; i < sample.length; i++) {
sample[i] = random.nextInt();
}
//记录程序开始时间
long t = System.currentTimeMillis();
//使用/不使用intern方法为10万个String赋值,值来自于Integer数组的10个数
for (int i = 0; i < MAX; i++) {
arr[i] = new String(String.valueOf(sample[i % sample.length]));
//arr[i] = new String(String.valueOf(sample[i % sample.length])).intern();
}
System.out.println((System.currentTimeMillis() - t) + "ms");
System.gc();
}
从运行结果来看,不使用intern()的情况下,程序生成了101762个String对象,而使用了intern()方法时,程序仅生成了1772个String对象。自然也证明了intern()节省内存的结论。
细心的同学会发现使用了intern()方法后程序运行时间有所增加。这是因为程序中每次都是用了new String后又进行intern()操作的耗时时间,但是不使用intern()占用内存空间导致GC的时间是要远远大于这点时间的.
对于反射不是很清楚的可以看一下这篇文章:https://blog.csdn.net/sinat_38259539/article/details/71799078(详细,强烈推荐)
6.使用反射在方法中改变参数类型为String的值
我们都知道在方法中对象做参数的话,传的是引用,这样很多小白就经常有疑问,String不是也是对象吗,为什么在调用方法改了String对象的值,原函数的值不会改变呢。我们先来看一下下面的这段代码。
-注意点:
Object的hashCode()默认是返回内存地址的,但是hashCode()可以重写,所以hashCode()不能代表内存地址的不同System.identityHashCode(Object)方法可以返回对象的内存地址,不管该对象的类是否重写了hashCode()方法。
public class test2 {
public static void main(String[] args) throws Exception {
String a="bbb";
String b=new String("aaa");
System.out.println("a+b调用函数前"+a+b);
System.out.println("调用函数前a的地址"+System.identityHashCode(a));
System.out.println("调用函数前b的地址"+System.identityHashCode(b));
chang(a);
chang(b);
System.out.println("a+b调用函数后"+a+b);
System.out.println("调用函数后a的地址"+System.identityHashCode(a));
System.out.println("调用函数后b的地址"+System.identityHashCode(b));
}
public static void chang(String stringBuilder) throws Exception{
System.out.println("参数的地址"+System.identityHashCode(stringBuilder));
stringBuilder="123";
System.out.println("改变值后参数的地址"+System.identityHashCode(stringBuilder));
System.out.println("改变参数后的值"+stringBuilder);
}
}
结果为
a+b调用函数前bbbaaa
调用函数前a的地址460141958
调用函数前b的地址1163157884
参数的地址460141958
改变值后参数的地址1956725890
改变参数后的值123
参数的地址1163157884
改变值后参数的地址1956725890
改变参数后的值123
a+b调用函数后bbbaaa
调用函数后a的地址460141958
调用函数后b的地址1163157884
大家可以看到,调用函数前a的地址460141958,参数的地址460141958,参数刚开始确实跟主函数的地址是相同的,但是使用使用stringBuilder="123"后地址就改变了,小白经常会以为String重新赋值就是改变他的值了,这其实只是新建了一个String对象而已。(相信眼尖的可以看到,改变值后参数的地址1956725890两次都是相同的,那是因为他们都是取自字符串池的,不懂的从头看一下这篇文章吧)
那如何改变String的值呢,我们在源码中看到String的value是final,应该是不能改变的,但是可以通过反射改变,具体看下面的代码/
public class test2 {
public static void main(String[] args) throws Exception {
String a="bbb";
String b=new String("aaa");
System.out.println(a);
chang(a);
System.out.println(a);
}
public static void chang(String stringBuilder) throws Exception{
Class clazz=stringBuilder.getClass();
Field field=clazz.getDeclaredField("value");
field.setAccessible(true);
field.set(stringBuilder,new char[] {'1', '2'});
System.out.println(stringBuilder);
}
}
7.字符串的日常使用
- int -> String
int i=12345;
String s="";
第一种方法:s=i+"";
第二种方法:s=String.valueOf(i);
这两种方法有什么区别呢?作用是不是一样的呢?是不是在任何下都能互换呢?
- String -> int
s=“12345”;
int i;
第一种方法:i=Integer.parseInt(s);
第二种方法:i=Integer.valueOf(s).intValue();
这两种方法有什么区别呢?作用是不是一样的呢?是不是在任何下都能互换呢?
第一种方法:s=i+""; //会产生两个String对象
第二种方法:s=String.valueOf(i); //直接使用String类的静态方法,只产生一个对象
第一种方法:i=Integer.parseInt(s);//直接使用静态方法,不会产生多余的对象,但会抛出异常
第二种方法:i=Integer.valueOf(s).intValue();//Integer.valueOf(s) 相当于 new Integer(Integer.parseInt(s)),也会抛异常,但会多产生一个对象
String的四种拼接方法的区别
https://www.cnblogs.com/lujiahua/p/11408689.html
-
使用+拼接字符串的实现原理:Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。
-
concat是如何实现的
我们再来看一下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);
}
这段代码首先创建了一个字符数组,长度是已有字符串和待拼接字符串的长度之和,再把两个字符串的值复制到新的字符数组中,并使用这个字符数组创建一个新的String对象并返回。
通过源码我们也可以看到,经过concat方法,其实是new了一个新的String,这也就呼应到前面我们说的字符串的不变性问题上了。
- StringBuffer和StringBuilder
接下来我们看看StringBuffer和StringBuilder的实现原理。
和String类类似,StringBuilder类也封装了一个字符数组,定义如下:
char[] value;
与String不同的是,它并不是final的,所以他是可以修改的。另外,与String不同,字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数,定义如下:
int count;
其append源码如下:
public StringBuilder append(String str) {
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;
}
append会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展。
StringBuffer和StringBuilder类似,最大的区别就是StringBuffer是线程安全的,看一下StringBuffer的append方法。
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
该方法使用synchronized进行声明,说明是一个线程安全的方法。而StringBuilder则不是线程安全的。
StringUtils.join是如何实现的
通过查看StringUtils.join的源代码,我们可以发现,其实他也是通过StringBuilder来实现的。
public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) {
if (array == null) {
return null;
}
if (separator == null) {
separator = EMPTY;
}
// endIndex - startIndex > 0: Len = NofStrings *(len(firstString) + len(separator))
// (Assuming that all Strings are roughly equally long)
final int noOfItems = endIndex - startIndex;
if (noOfItems <= 0) {
return EMPTY;
}
final StringBuilder buf = new StringBuilder(noOfItems * 16);
for (int i = startIndex; i < endIndex; i++) {
if (i > startIndex) {
buf.append(separator);
}
if (array[i] != null) {
buf.append(array[i]);
}
}
return buf.toString();
}
效率比较
既然有这么多种字符串拼接的方法,那么到底哪一种效率最高呢?我们来简单对比一下。
long t1 = System.currentTimeMillis();
//这里是初始字符串定义
for (int i = 0; i < 50000; i++) {
//这里是字符串拼接代码
}
long t2 = System.currentTimeMillis();
System.out.println(“cost:” + (t2 - t1));
我们使用形如以上形式的代码,分别测试下五种字符串拼接代码的运行时间。得到结果如下:
- cost:5119
StringBuilder cost:3
StringBuffer cost:4
concat cost:3623
StringUtils.join cost:25726
从结果可以看出,用时从短到长的对比是:
StringBuilder<StringBuffer<concat<+<StringUtils.join
StringBuffer在StringBuilder的基础上,做了同步处理,所以在耗时上会相对多一些。
StringUtils.join也是使用了StringBuilder,并且其中还是有很多其他操作,所以耗时较长,这个也容易理解。其实StringUtils.join更擅长处理字符串数组或者列表的拼接。
那么问题来了,前面我们分析过,其实使用+拼接字符串的实现原理也是使用的StringBuilder,那为什么结果相差这么多,高达1000多倍呢?
我们再把以下代码反编译下:
long t1 = System.currentTimeMillis();
String str = “hollis”;
for (int i = 0; i < 50000; i++) {
String s = String.valueOf(i);
str += s;
}
long t2 = System.currentTimeMillis();
System.out.println("+ cost:" + (t2 - t1));
反编译后代码如下:
long t1 = System.currentTimeMillis();
String str = "hollis";
for(int i = 0; i < 50000; i++)
{
String s = String.valueOf(i);
str = (new StringBuilder()).append(str).append(s).toString();
}
long t2 = System.currentTimeMillis();
System.out.println((new StringBuilder()).append("+ cost:").append(t2 - t1).toString());
我们可以看到,反编译后的代码,在for循环中,每次都是new了一个StringBuilder,然后再把String转成StringBuilder,再进行append。
而频繁的新建对象当然要耗费很多时间了,不仅仅会耗费时间,频繁的创建对象,还会造成内存资源的浪费。
所以,阿里巴巴Java开发手册建议:循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。而不要使用+。
总结
本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建字符串的方式来进行字符串的拼接。
常用的字符串拼接方式有五种,分别是使用+、使用concat、使用StringBuilder、使用StringBuffer以及使用StringUtils.join。
由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串拼接,就要考虑内存问题和效率问题。
因此,经过对比,我们发现,直接使用StringBuilder的方式是效率最高的。因为StringBuilder天生就是设计来定义可变字符串和字符串的变化操作的。
但是,还要强调的是:
1、如果不是在循环体中进行字符串拼接的话,直接使用+就好了。
2、如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder。
String常用方法
- char charAt(int index)
返回 char指定索引处的值。 - concat(String str)
将指定的字符串连接到该字符串的末尾。 - contains(CharSequence s)
当且仅当此字符串包含指定的char值序列时才返回true。 - String substring(int beginIndex)
返回一个字符串,该字符串是此字符串的子字符串。
String substring(int beginIndex, int endIndex)
返回一个字符串,该字符串是此字符串的子字符串。