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)
    返回一个字符串,该字符串是此字符串的子字符串。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值