Java学习之String类源码分析

String类的介绍

  1. String类实现了Serializable, Comparable, CharSequence接口。
  2. String类是final类,也即意味着String类不能被继承,并且它的成员方法都默认为final方法。
  3. String是一个不可变对象,它是一个不可变的字符数组。
  4. 因为String类不可变,所以是多线程安全的。
  5. 因为String类不可变,所以保证了hash码的唯一性,因此可以放心地进行缓存。

1、String类构造方法源码分析(从JDK1.0版本开始)

//字符串是常量,它们的值在创建之后不能更改。字符串缓冲区支持可变的字符串。因为 String 对象是不可变的,所以可以共享。
String s="abc";//因为在实际开发中,大部分时间,开发人员都是和字符串打交道,所以默认给出一种简单的创建方法
//String s="abc" 等效于:
char[] charArray = {
    
    'a','b','c'};
String s = new String(charArray);//每一个对象都会增加内存 所以要尽量少创建对象 节省内存
//其他创建String对象的构造方法:
char[] a = {
    
    'a','b','c'};
String s03 = new String(new Char[]{
    
    'a','b','c'},1,2);//等效于String s03 = new String(a,1,2)

(1)源码分析:

  1. 当传的值为字符串时

    String s = new String("abc");
    //对右边的String按ctrl键进入String构造函数源码
     public String(String original) {
          
          
            this.value = original.value;	//把传进来的original的value赋值到自身的value,这里的value是一个char value[]数组
            this.hash = original.hash;		//默认int hash等于0,hash是为了判定这个对象是不是唯一的,就是对象的地址
     }
    
  2. 当没有传值时

    String s1 = new String();
    //对右边的String按ctrl键进入String构造函数源码
    public String() {
          
          
            this.value = "".value;			//把空的字符串的value赋值到自身的value,这里的value是一个char value[]数组
    }
    
  3. 当传的值为char数组时

     String s1 = new String(new char[]{
          
          'a','b','c'};
    //对右边的String按ctrl键进入String构造函数源码
    public String(char value[]) {
          
          
            this.value = Arrays.copyOf(value, value.length);//复制一份一模一样的数组赋值到自身的value
    }
    
  4. 当传的值为char数组,从第几个下标开始,子数组的长度时

     String s1 = new String(new char[]{
          
          'a','b','c'},1,2);
    //对右边的String按ctrl键进入String构造函数源码
     public String(char value[], int offset, int count) {
          
          
            if (offset < 0) {
          
          		//如果开始的下标小于0,则抛出字符串下标越界异常
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
          
          		
                if (count < 0) {
          
          	//如果子数组的长度小于0,则抛出字符串下标越界异常
                    throw new StringIndexOutOfBoundsException(count);
                }
                if (offset <= value.length) {
          
          	//因为count<=0 所以取得的子数组为空 返回空
                    this.value = "".value;
                    return;
                }
            }
            // Note: offset or count might be near -1>>>1.
            if (offset > value.length - count) {
          
          
                //假设数组长度为3,count为2,即子数组开始的下标最大为3-2=1,所以offset>value.length - count就会抛出字符串下标越界异常
                throw new StringIndexOutOfBoundsException(offset + count);
            }
            this.value = Arrays.copyOfRange(value, offset, offset+count);//copyOfRange方法:给我一个value数组,把这个value数组从指定下标offset到指定下标offset+count的这个范围拆分下来,得到一个子数组
    }
    

(2)不可变性

//1、当数组长度一致时
String s = "123";
s = "456";
s = "789";
System.out.println(s);//输出:789
//分析:当s = "123"时,s会形成一个final数组,即不可变形态。
//当s = "456"时,会先检查s原来的数组的长度,检查为长度一致,所以会直接覆盖原先的s,此时s输出为456
//当s = "789"时,会先检查s原来的数组的长度,检查为长度一致,所以会直接覆盖原先的s,此时s输出为789
//2、当数组长度不一致时
String s = "123";
s = "4567";
s = "78901";
System.out.println(s);//输出:78901
//分析:当s = "123"时,s会形成一个final数组,即不可变形态。
//当s = "456"时,会先检查s原来的数组的长度,检查为长度不一致,重新生成了一个新的数组,再装载新的元素
//当s = "789"时,会先检查s原来的数组的长度,检查为长度不一致,重新生成了一个新的数组,再装载新的元素
//测试String对象用"+"拼接字符串性能
long startTime=System.currentTimeMillis();  //生成当前时间戳(单位:ms)
String s = "";
for(int i = 0 ; i < 100000 ; i++){
    
    
	s = s + "1";
}
System.out.println(System.currentTimeMillis()-startTime);
//输出:3092
//当i<200000 输出13564
//假设此时有10万用户使用需要拼接一个字符串,需要大概等待3秒(电脑性能不同,结果不同),20万时大概需要等待13秒,以此类推,越来越久。

总结:因为String对象的不可变性,在拼接字符串的时候会生成新的数组,消耗大量内存,而且手动释放不了,垃圾回收效率低,所以String对象处理拼接字符串的性能极低,所以要避免多个用户频繁修改同一个字符串变量。

2、一些常用的相关方法

(1) charAt(int index)方法:返回指定索引处的 char 值。返回char

//索引 == 下标 字符串也是从0下标开始的
char[] c = {
    
    'a','b','c','d','e'};
String s = new String(c);
char c1 = s.charAt(1);//charAt(1) 这里的1是下标 
System.out.println(c1);//输出: b
//
//源码分析
public char charAt(int index) {
    
    
        if ((index < 0) || (index >= value.length)) {
    
    
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
}
//index即索引,如果索引值小于0或者大于value数组的长度的话 就会抛出字符串越界异常

(2) concat(String str)方法:将指定字符串连接到此字符串的结尾。 返回String

//拼接字符串的一般方法:
String s01 = "123";
String s02 = "456";
String s03 = s01 + s02;
System.out.println(s03);//输出:123456
//concat方法:
String s01 = "123";
String s02 = "456";
String s03 = s01.concat(s02);
System.out.println(s03);//输出:123456
//源码分析 
public String concat(String str) {
    
    
        int otherLen = str.length();
        if (otherLen == 0) {
    
    	//如果拼接的字符串为空,假设s2="",则直接返回原字符串,即s03=s01.
            return this;
        }
        int len = value.length;//value是当前对象,即s01,value.length就是s01的长度
        char buf[] = Arrays.copyOf(value, len + otherLen);//基于value数组和len + otherLen长度进行复制来生成一个新的数组
        str.getChars(buf, len);//从buf数组的len索引开始将str字符串复制到buf目标字符数组中
        return new String(buf, true);//返回一个新的String对象
}

与用”+“连接字符串的性能相比:

long startTime=System.currentTimeMillis();  //生成当前时间戳(单位:ms)
String s="";
for(int i=0;i<100000;i++){
    
    
	s=s.concat("1");
}
System.out.println(System.currentTimeMillis()-startTime);
//输出:1761
//当i<200000时,需要6390ms 大概需要6秒
//上面测试的使用"+"号连接字符串:
//当i<100000时,输出6390	当i<200000时,输出13564
//结论:相对于+号 性能有提升,将近一倍的优化效率,但性能还是很差

(3)startsWith(String suffix)endsWith(String suffix) : 测试此字符串是否以指定的后缀开始,测试此字符串是否以指定的后缀结束。 返回boolean

String s1 = "[email protected]";
String s2 = "[email protected]";
String s3 = "one.png";
String s4 = "two.jpg";
boolean b1 = s1.endsWith("qq.com");
boolean b2 = s2.endsWith("qq.com");
boolean b3 = s3.startsWith("one");
boolean b4 = s4.startsWith("one");
System.out.println(b1);//输出:true
System.out.println(b2);//输出:false
System.out.println(b3);//输出:true
System.out.println(b4);//输出:false
//源码分析(以s3.startsWith("one")为例)
public boolean startsWith(String prefix, int toffset) {
    
    
        char ta[] = value;			//value是使用startsWith()方法的对象,转换成char数组,即s3
        int to = toffset;			//toffset是从指定索引开始,toffset默认值为0
        char pa[] = prefix.value;//传进来的字符串(参数)转换成char数组,即s3.startsWith("one")中的one字符串
        int po = 0;					
        int pc = prefix.value.length;//传进来的字符串(参数)的长度
        // Note: toffset might be near -1>>>1.
        if ((toffset < 0) || (toffset > value.length - pc)) {
    
    
//索引小于0,或者大于比较的两个字符串长度之差,即s3长度为7,s3.startsWith("one")中one的长度为3,所以只能取4个字符,所以索引不能大于4。假设索引为5,即从s3的n开始取3个字符,但此时s3只剩下两个字符,就会报越界错误。
            return false;
        }
        while (--pc >= 0) {
    
    		//每次执行pc都会减一,直到pc小于0时结束
            if (ta[to++] != pa[po++]) {
    
    
//因为没有设置索引,所以,to和po都为0,即s3和one字符串都从下标0开始两两比较,如果比较不相等,则false
                return false;
            }
        }
    //如果整个数组都比较完成没有返回false,则返回true
        return true;
}
//源码分析(以s1 = "[email protected]";s1.endsWith("qq.com")为例)
public boolean endsWith(String suffix) {
    
    
     return startsWith(suffix, value.length - suffix.value.length);
}
//suffix即qq.com,参数中传进来的字符串。
//value.length - suffix.value.length,value.length为s1的长度15,suffix.value.length长度为6
//他们之差等于9,就以索引9开始用startsWith()方法,进行s1和参数中传进来的字符串qq.com开始比较,见上一个源码分析

(4)equals(Object anObject):将此字符串与指定的对象比较。返回boolean

对象类型中“==”比较(面试题):

//"=="的比较
String s01 = new String("123");
String s02 = new String("123");//只要在java中出现new就是新的对象
String s03 = "123";	//jvm把"123"字符串放在内存之中,如果"123"要用,就把"123"拿给你
String s04 = "123";	//同上
String s05 = s01;
String s06 = new String(s01);
boolean b = s01 == s02;
boolean b1 = s01 == s03;
boolean b2 = s03 == s04;
boolean b3 = s05 == s04;
boolean b4 = s05 == s01;
boolean b5 = s06 == s01;
System.out.println(b); //输出:false
System.out.println(b1);//输出:false 因为s01还在new 只要是new 就是新的对象 新的地址
System.out.println(b2);//输出:true
System.out.println(b3);//输出:false 因为s01是新的,赋值给s05,变成和s01一样新的对象
System.out.println(b4);//输出:true	同上
System.out.println(b5);//输出:false 因为s06new了

总结:在基本类型中,双等号比较的是值,但是在对象类型中比较的是内存地址
equals()方法:

//对象类型大部分都是通过一个特别的方法来完成值的比较
String s01 = new String("123");
String s02 = new String(s01);
boolean b = s02.equals(s01);	//比较值
System.out.println(b); //输出:true
//源码分析
public boolean equals(Object anObject) {
    
    
    if (this == anObject) {
    
    	//先判断是否是同一个对象,即比较地址,地址相同就是同一个对象
        return true;
    }
    if (anObject instanceof String) {
    
    	//instanceof:测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型,即判断anObject对象是否是String类的实例
        String anotherString = (String)anObject;	//如果是String类的实例,就强转为String类型
        int n = value.length;	//使用此方法的对象自身的长度
        if (n == anotherString.value.length) {
    
    	 //强转后的String对象
            char v1[] = value;				    //自身对象转换成char数组
            char v2[] = anotherString.value;	 //强转后的String对象转换成char数组
            int i = 0;
            while (n-- != 0) {
    
    					//每次执行n减一,直到n等于0退出循环
                if (v1[i] != v2[i])			//从索引0开始两两比较,如果比较出不相等,则返回false
                    return false;
                i++;					   //若相等,则索引加一,继续比较
            }
            return true;				   //整个数组比较完n=0退出循环后,返回true
        }
    }
    return false;						  //判断anObject对象不是String类的实例,返回false
}

前后端接收机制问题:
当前端发送的username传来值时,这样写是对的

String username = "tpp";	    //前端发送
String username1 = null;	    //后端接收机制
if(username.equals(username1))	//值比较null,这种操作java中允许
{
    
    
 	System.out.println("登录成功");
}
System.out.println("登录失败")

当前端发送的username传来null时

String username = null;	    	//前端发送
String username1 = null;		//后端接收机制
if(username.equals(username1))	//null比较值,这种操作java中是不允许的
{
    
    
 	System.out.println("登录成功");
}
System.out.println("登录失败")
//输出结果:Exception in thread "main" java.lang.NullPointerException 会报空指针错误

在实际开发中,正确的写法:

String username = null;	    //前端发送
String username1 = null;	    //后端接收机制
if(username != null && username.equals(username1))//&&不能写成& 
{
    
    
 	System.out.println("登录成功");
}
System.out.println("登录失败")
//输出结果:登录失败		这样就不会报空指针错误了
//为什么&&不能写成&?因为&对每一个条件都要判断,然而&&只要前面的条件是false就输出false,而不继续判断后面的条件了。这里如果写成&,执行了username != null之后也要执行username.equals(username1),就会报空指针错误。

希望可以和大家互相学习,谢谢大家!!!

猜你喜欢

转载自blog.csdn.net/qq_42039952/article/details/115360091