java的String详解(持续更新)

参考文章: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方式。
  1. 采用字面值时,jvm首先会去字符串池中查找有没有相同的对象,如果存在就返回这个对象,没有则创建后返回
  2. 采用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

  1. 是不是有点晕了,哈哈哈,我们先来看一下jdk7的情况:
 - 首先时String s3 = new String("1") + new String("1")这条语句,string创建一个对象有两种方式,
   直接赋值和new一个对象,都会字符串池中创建相应的对象,但是这条语句只在字符串池中创
   建了两个”1“的对象,并没有在字符串池中创建“11”这个对象。
   
 - 接着就是s3.intern(),因为字符串池中没有”11“,在jdk7中,字符串池中不需要再存储一份对
   象了,可以直接存储堆中的**引用**。这份引用直接指向 s3 引用的对象,也就是说s3.intern() ==s3
   会返回true。
 
 - 主要在于你有没有一开始new的时候在字符串池和堆中创建两个不同的对象,如果有new的时候在
    字符串池中创建了一个对象,则**字符串池不会使用引用**。

  1. 对于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.字符串的日常使用

  1. int -> String

int i=12345;
String s="";
第一种方法:s=i+"";
第二种方法:s=String.valueOf(i);
这两种方法有什么区别呢?作用是不是一样的呢?是不是在任何下都能互换呢?

  1. 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

  1. 使用+拼接字符串的实现原理:Java中的+对字符串的拼接,其实现原理是使用StringBuilder.append。

  2. 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,这也就呼应到前面我们说的字符串的不变性问题上了。

  1. 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常用方法

  1. char charAt(int index)
    返回 char指定索引处的值。
  2. concat(String str)
    将指定的字符串连接到该字符串的末尾。
  3. contains(CharSequence s)
    当且仅当此字符串包含指定的char值序列时才返回true。
  4. String substring(int beginIndex)
    返回一个字符串,该字符串是此字符串的子字符串。
    String substring(int beginIndex, int endIndex)
    返回一个字符串,该字符串是此字符串的子字符串。
发布了36 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/s_xchenzejian/article/details/96297258