• Java基础(八)--String(源码)、StringBuffer、StringBuilder


    String源码:基于jdk1.8

    public final class String implements Serializable, Comparable<String>, CharSequence {
    	/** The value is used for character storage. */
    	private final char[] value;
    	/** Cache the hash code for the string */
        private int hash;
    	
    	public String() {
            this.value = "".value;
        }
    
        public String(String var1) {
            this.value = var1.value;
            this.hash = var1.hash;
        }
    
        public String(char[] var1) {
            this.value = Arrays.copyOf(var1, var1.length);
        }
        public String(byte bytes[], int offset, int length) {
            checkBounds(bytes, offset, length);
            this.value = StringCoding.decode(bytes, offset, length);
        }
    
    }
    

    主要参数:

      1、char[] value,用来存储数据的char类型数组

      2、int hash,字符串的hash

    结论:

      1、String是final修饰的,证明是不可变的

      2、实现了Comparable接口,可以通过CompareTo()进行比较,实现里Serializable接口,可以在网络中传输

      3、通过数组保存数据

    常用方法:

    //获取String中第index个字符,首位为0
    public char charAt(int index) {
    	if ((index < 0) || (index >= value.length)) {
    		throw new StringIndexOutOfBoundsException(index);
    	}
    	return value[index];
    }
    //当前字符串和另一个字符串比较,比较同每个位置字符的大小,'A':65,'Z':90,'a':97,'z':122
    public int compareTo(String anotherString) {
    	int len1 = value.length;
    	int len2 = anotherString.value.length;
    	int lim = Math.min(len1, len2);
    	char v1[] = value;
    	char v2[] = anotherString.value;
    
    	int k = 0;
    	while (k < lim) {
    		char c1 = v1[k];
    		char c2 = v2[k];
    		if (c1 != c2) {
    			return c1 - c2;
    		}
    		k++;
    	}
    	return len1 - len2;
    }
    //拼接字符串
    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);
    }
    //是否包含某个字符串
    public boolean contains(CharSequence s) {
    	return indexOf(s.toString()) > -1;
    }
    //根据数组生成一个字符串
    public static String copyValueOf(char data[]) {
    	return new String(data);
    }
    //是否以某个字符串结尾
    public boolean endsWith(String suffix) {
    	return startsWith(suffix, value.length - suffix.value.length);
    }
    //比较两个字符串是否相等
    public boolean equals(Object anObject) {
    	if (this == anObject) {                    //首先通过==比较,==比较的是对象,如果相等,返回true
    		return true;
    	}
    	if (anObject instanceof String) {			//判断是否是String类型的实例,如果不是,返回false
    		String anotherString = (String)anObject;      //强转成String
    		int n = value.length;
    		if (n == anotherString.value.length) {
    			char v1[] = value;
    			char v2[] = anotherString.value;
    			int i = 0;
    			while (n-- != 0) {
    				if (v1[i] != v2[i])		 //逐个字符进行比较
    					return false;
    				i++;
    			}
    			return true;
    		}
    	}
    	return false;
    }
    //返回该字符串的hash值
    //空字符串返回0,否则计算公式如下:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    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;
    }
    //判断是否包含字符串,和contains相同,包含返回0,否则-1
    public int indexOf(int ch) {
    	return indexOf(ch, 0);
    }
    //本地方法
    public native String intern();
    //判断是否为空," "并不是空字符串
    public boolean isEmpty() {
    	return value.length == 0;
    }
    //匹配正则表达式
    public boolean matches(String regex) {
    	return Pattern.matches(regex, this);
    }
    //将字符串中某个字符替换为另一个字符
    public String replace(char oldChar, char newChar) {
    	if (oldChar != newChar) {              //判断两个字符是否相同
    		int len = value.length;
    		int i = -1;
    		char[] val = value; /* avoid getfield opcode */
    
    		while (++i < len) {             //判断是否包含这个字符
    			if (val[i] == oldChar) {
    				break;
    			}
    		}
    		if (i < len) {                //包含该字符,生成一个新的char[],并把原字符之前的值复制进去
    			char buf[] = new char[len];
    			for (int j = 0; j < i; j++) {
    				buf[j] = val[j];
    			}
    			while (i < len) {          //把新的字符及后面的字符放到数组中
    				char c = val[i];
    				buf[i] = (c == oldChar) ? newChar : c;
    				i++;
    			}
    			return new String(buf, true);    //生成一个新的String
    		}
    	}
    	return this;
    }
    //截取字符串,首位为0
    public String substring(int beginIndex) {
    	if (beginIndex < 0) {
    		throw new StringIndexOutOfBoundsException(beginIndex);
    	}
    	int subLen = value.length - beginIndex;
    	if (subLen < 0) {
    		throw new StringIndexOutOfBoundsException(subLen);
    	}
    	return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    //字符串中字符转小写
    public String toLowerCase() {
    	return toLowerCase(Locale.getDefault());
    }
    //字符串中字符转大写
    public String toUpperCase() {
    	return toUpperCase(Locale.getDefault());
    }
    //剔除字符串两端的空格字符
    public String trim() {
    	int len = value.length;
    	int st = 0;
    	char[] val = value;    /* avoid getfield opcode */
    
    	while ((st < len) && (val[st] <= ' ')) {                  //从左到右找到第一个空格字符后面一个下标
    		st++;
    	}
    	while ((st < len) && (val[len - 1] <= ' ')) {                //从右到左找到第一个空格字符前面一个下标
    		len--;
    	}
    	return ((st > 0) || (len < value.length)) ? substring(st, len) : this;//截取这两个下标的字符串
    }
    //生成一个字符串
    public static String valueOf(char c) {
    	char data[] = {c};
    	return new String(data, true);
    }
    

    结论:所有改变字符串的操作都是重新生成一个新的字符串,包括+,+=

    深入理解:

    public static void main(String[] args) {
    	String str1 = "abc";
    	String str2 = new String("abc");
    	String str3 = "abc";
    	String str4 = new String("abc");
    
    	System.out.println(str1==str2);
    	System.out.println(str1==str3);
    	System.out.println(str2==str4);
    }
    

    结果:

    false
    true
    false
    

    我们首先讲一下class常量池和字符串常量池的概念,之前在jvm已经讲过了,这里再简单讲一下

    class常量池(Class Constant Pool):

      我们写的每一个Java类被编译后,就会形成一份class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信

    就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References);

    字面量包括:

      1.String 2.八种基本类型的值 3.被声明为final的常量等;

    符号引用包括:

      1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。

    字符串常量池(String Constant Pool):

      在HotSpot VM里实现的string pool功能的是一个StringTable类,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上。

    Class常量池里面的字符串在类加载过程放到字符串常量池

    在JDK1.7版本,字符串常量池从方法区移到了堆中了。

    解释:

      String str1 = "abc";和String str3 = "abc; 都在编译期间生成了字面量,保存在字符串常量池中。jvm每次先到常量池中查找是否存在这个

    字符串,如果存在str3直接指向这个字符串

      而通过new生成的是对象,保存在堆中。每次new都会生成一个对象,所以是不相等的

    String、StringBuffer、StringBuilder

    public static void main(String[] args) {
    	String s = "aaa" ;
    	s += "bbb";
    	System.out.println(s);
    }

    反编译查看字节码

    public class com.it.Test {
      public com.it.Test();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: ldc           #2                  // String aaa
           2: astore_1
           3: new           #3                  // class java/lang/StringBuilder
           6: dup
           7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
          10: aload_1
          11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          14: ldc           #6                  // String bbb
          16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
          22: astore_1
          23: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
          26: aload_1
          27: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          30: return
    }

    第三行能看到遇到+的时候,会new一个StringBuilder,然后append这个字符串,然后调用toString(),如果多次用+=,就会生成很多

    StringBuilder对象,就会浪费内存

    如果是这样使用+,就没问题

    public static void main(String[] args) {
    	String s = "aaa" + "bbb" + "ccc";
    }  

    在编译期间直接生成一个字符串"aaabbbccc",是因为"aaa","bbb","ccc",都是编译器可知的常量,当然一般也不会这样用的

    public static void main(String[] args) {
    	String s1 = "bbb" ;
    	String s = "aaa" + s1 + "ccc";
    }

    如果这样使用,也是不行的

    String本身contact()效率也不高,每次生成一个新的char[],然后通过数组new一个String,所以需要拼接字符串的时候,可以使用StringBuilder

    public static void main(String[] args) {
    	StringBuilder builder = new StringBuilder("aaa");
    	builder.append("bbb");
    }
    public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class java/lang/StringBuilder
           3: dup
           4: ldc           #3                  // String aaa
           6: invokespecial #4                  // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
           9: astore_1
          10: aload_1
          11: ldc           #5                  // String bbb
          13: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
          16: pop
          17: return
    }

    而StringBuffer和StringBuilder都继承了AbstractStringBuilder,也是通过char[]进行保存数据,所以效率会好很多

    StringBuilder和StringBuffer区别就是是否保证线程安全,StringBuffer通过synchronized保证线程安全

    性能比较:

    "aaa"+"ccc"这样直接字符串叠加,性能肯定是最好的

    而其余情况下:StringBuilder>StringBuffer>String

    所以字符串改动较少使用String,否则使用StringBuilder,多线程环境下使用StringBuffer

    PS;这里说下,我们使用Hibernate/JPA的时候,需要在Dao层拼接SQL,没必要用StringBuffer好吗,不要老是想着并发的问题。一般的Dao都是无状态的bean,不存在

    线程安全问题。而且方法的执行在栈中进行的,栈是线程私有,没有特殊情况,还是要使用StringBuilder。

    面试题:来自下面的链接

    public static void main(String[] args) {
    	String s1 = "aaa2";
    	final String s2 = "aaa";
    	String s3 = "aaa";
    	String s4 = "aaa" + 2;
    	String s5 = s3 + 2;
    	String s6 = s2 + 2;
    	System.out.println(s1 == s4);
    	System.out.println(s1 == s5);
    	System.out.println(s1 == s6);
    
    	final String s7 = getHello();
    	String s8 = s6 + 2;
    	System.out.println(s1 == s8);
    }
    
    public static String getHello() {
    	return "hello";
    }

    结果:

    true
    false
    true
    false
    

    s1==s4为true:

      "hello"+2在编译期间就已经被优化成"hello2",因此在运行期间,变量a和变量b指向的是同一个对象

    s1==s5为false:

      由于有符号引用的存在,所以  String c = b + 2;不会在编译期间被优化,不会把b+2当做字面常量来处理的,因此这种方式生

    成的对象事实上是保存在堆上的。因此a和c指向的并不是同一个对象。javap -c得到的内容

    s1==s6为true:

      对于被final修饰的变量,会在class文件常量池中保存一个副本,也就是说不会通过连接而进行访问,对final变量的访问在编译

    期间都会直接被替代为真实的值。那么String c = b + 2;在编译期间就会被优化成:String c = "hello" + 2; 

    s1==s8为false:

      这里面虽然将b用final修饰了,但是由于其赋值是通过方法调用返回的,那么它的值只能在运行期间确定,因此a和c指向的不是同一个对象。

    再来一个栗子:

    public static void main(String[] args) {
    	String a = "hello";
    	String b =  new String("hello");
    	String d = b.intern();
    
    	System.out.println(b==d);
    	System.out.println(a==d);
    }

    结果:

    false
    true
    

    解释:

      这里面涉及到的是String.intern方法的使用。在String类中,intern方法是一个本地方法,在JAVA7之前,intern方法会在运行时常量池

    中查找是否存在内容相同的字符串,如果存在则返回指向该字符串的引用,如果不存在,则会将该字符串入池,并返回一个指向该字符串的引用。

    因此,a和d指向的是同一个对象。

    String str = new String("abc")创建了多少个对象?

    反编译:

    public static void main(java.lang.String[]);
        Code:
           0: new           #2                  // class java/lang/String
           3: dup
           4: ldc           #3                  // String abc
           6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
           9: astore_1
          10: return
    }
    

    结果是只创建一个对象

      而这道题目让人混淆的地方就是这里,这段代码在运行期间确实只创建了一个对象,即在堆上创建了"abc"对象。

      而为什么大家都在说是2个对象呢,这里面要澄清一个概念,该段代码执行过程和类的加载过程是有区别的。在类加载的过程中,确实在运行时

    常量池中创建了一个"abc"对象,而在代码执行过程中确实只创建了一个String对象。

      因此,这个问题如果换成 String str = new String("abc")涉及到几个String对象?合理的解释是2个。

      个人觉得在面试的时候如果遇到这个问题,可以向面试官询问清楚”是这段代码执行过程中创建了多少个对象还是涉及到多少个对象“再根

    据具体的来进行回答。

    内容参考:https://www.cnblogs.com/dolphin0520/p/3778589.html和Java编程思想

  • 相关阅读:
    OptaPlanner实用技术 批量规划和实时规划(2)
    【Golang】创建有配置参数的结构体时,可选参数应该怎么传?
    k8s:Pod生命周期
    k8s:容器生命周期回调
    在工厂<> 中 <> 已经是个追加物料
    The headers or library files could not be found for jpeg
    从进入内核态看内存管理
    迅雷加速原来分析
    idea编译build一直卡在那不动解决方法
    python入门爬虫豆瓣电影Top250
  • 原文地址:https://www.cnblogs.com/huigelaile/p/11016967.html
Copyright © 2020-2023  润新知