最全String 相关知识总结

String 类的特点–以final修饰的类

1> 以 JDK1.8 版本来看,String 内部实际存储结构为 char 数组,部分源码:

public final class String implements java.io.Serializable,Comparable<String>,CharSequence {
	// 用于存储字符串
	private final char value[];
	// 缓存字符串的 hashcode
	private int hash;
	...
}

以上 String 是被final修饰的,是不可被继承的类,为什么要这样设计呢?

Java 语言之父 James Gosling 的回答是:

  • 会更倾向于使用 final,因为它能够缓存结果,传参时不需要考虑谁会修改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。
  • 迫使 String 类设计成不可变的另一个原因是安全,在调用其他方法时,比如调用一些系统级操作指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起严重的系统崩溃问题,这是迫使 String 类设计成不可变类的一个重要原因。

总结来说,使用 final 修饰的第一个好处是安全;第二个好处是高效,以 JVM 中的字符串常量池来举例,如下两个变量:

String s1 = "java";
String s2 = "java";

只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率,如下图所示:
在这里插入图片描述
如果 String 是可变的,那当 s1 的值修改之后,s2 的值也跟着改变了,这样就和我们预期的结果不相符了,因此也就没有办法实现字符串常量池的功能了。

2> 多构造方法

// String 为参数的构造
public String(String original) {
	this.value = original.value;
	this.hash = original.hash;
}

// char[] 为参数构造方法
public String(char value[]) {
	this.value = Arrays.copyOf(value.value.length);
} 

// StringBuffer 为参数的构造方法
public String(StringBUffer buffer) {
	synchronized(buffer) {
		this.value = Arrays.copyOf(buffer.getValue(),buffer.length());
	}
}

// StringBuilder 为参数的构造方法
public String(StringBuilfer builder) {
	this.value = Arrays.copyOf(builder.getValue(),builder.length());
}

其中,比较容易被我们忽略的是以 StringBuffer 和 StringBuilder 为参数的构造函数,因为这三种数据类型,我们通常都是单独使用的,所以需要关注。

equals() 和 == 比较字符串

1> equals() 方法
在 Object类中 equals() 默认比较的是对象的地址值是否相同,意义不大,一般子类都会重写此方法

public boolean equals(Object obj){
    return this == obj;
}

String类重写了 equals() 方法的源码分析:

public boolean equals(Object anObject) {
	// 对象引用相同直接返回 true
	if (this == anObject) {
		return true;
	}
	// 判断需要对比的值是否为 String 类型,如果不是则返回 false
	if (anObject instanceof String) {
		String anotherString = (String)anObject;
		int n = value.length;
		if (n == anotherString.value.length) {
			// 把两个字符串都转换为 char 数组对比
			char v1[] = value;
			char v2[] = anotherString.value;
			int i = 0;
			// 循环对比两个字符串的每一个字符
			while (n-- != 0) {
				// 如果其中有一个字符不相等就 false,否则继续对比
				if (v1[i] != v2[i])
					return false;
				i++;
			}
			return true;
		}
	}
	return false;
}

重写后的 equals() 方法需要接收一个 Object 类型的参数值,在比较时会先通过 instanceof 判断是否是 String 类型,如果不是则会返回 false ,instanceof 的使用如下:

Object oString = "123";
Object oInt = 123;
System.out.println(oString instanceof String);	// 返回 true
System.out.println(oInt instanceof String);		// 返回 false

当判断参数为 String 类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回 true,否则则返回 false。

还有一个和 equals() 比较类似的方法 equalsIgnoreCase(), 忽略字符串的大小写进行字符串比较

2> == 对于基本数据类型来说,就是比较 “值” 是否相等的;而对于引用类型来说,是用于比较引用地址值是否相同,前面我们知道 Object 中的 equals() 方法

public boolean equals(Object obj) {
	return (this == obj);
}

可以看出,Object 中的 equals() 方法其实就是 ==,而 String 重写了 equals() 方法把它修改成比较两个字符串的字面值是否相等。

CompareTo() 比较两个字符串

compareTo() 方法用于比较两个字符串,返回的结果为 int 类型的值,源码如下:

public int compareTo(String anotherString) {
	int len1 = value.legnth;
	int len2 = anotherString.value.legnth;
	// 获取到两个字符串长度最短的那个 int 值
	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;
	
}
  • 取得value数组的长度,取得value数组里面的元素, 如果两个串长一样,则返回值 0;如果不一样:基于字符串中每个字符的Unicode值比较若此字符串按字典顺序小于字符串参数,则返回一个小于 0 的值;若此字符串按字典顺序大于字符串参数,则返回一个大于 0 的值。
  • 可以看出当 equals() 方法返回 true 时,或者是 compareTo() 方法返回 0 时,则表示两个字符串完全相同。

String 和 StringBuilder、StringBuffer 的区别

由于String 类型是不可变的,所以在字符串拼接的时候如果使用String性能比较低,就需要另一个数据类型 StringBuffer,它提供了append 和 insert 方法可以用于字符串的拼接,并且使用 synchronized保证线程安全。源码如下:

@Override
public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
}

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

由于使用 synchronized 来保证了线程安全,所以性能不好,于是在 JDK1.5 的时候有了StringBuilder,它同样提供了append和insert的拼接方法,但没有使用 synchronized,所以性能好,在并发操作的环境下可以使用。

String 和 JVM

String 常见的创建方式有两种,new String() 的方式和直接赋值,直接赋值的方式会先去字符串常量池中查找是否已经有此值,有则把引用地址直接指向此值,否则会先在常量池中创建,然后再把引用指向此值;而new String() 的方式一定会先再堆上创建一个字符串对象,然后再去常量池查询此字符串的值是否已经存在,如果不存在会先再常量池中创建此字符串,然后把引用的值指向此字符串。

String s1 = new String("Java");
String s2 = s1.intern();
String s3 = "Java";
System.out.println(s1 == s2); // false
System.out.println(s2 == s3); // true

在这里插入图片描述

编译器对字符串拼接的优化

String s1 = "Ja" + "va";
String s2 = "Java";
System.out.println(s1 == s2);  //  true

虽然 s1 拼接了多个字符串,但对比的结果却是 true,通过发编译工具查看结果如下:

Compiled from "StringDemo.java"
public class StringDeno {
  public StringDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 3: 0

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String Java
       2: astore_1
       3: ldc           #2                  // String Java
       5: astore_2
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: aload_1
      10: aload_2
      11: if_acmpne     18
      14: iconst_1
      15: goto          19
      18: iconst_0
      19: invokevirtual #4                  // Method java/io/PrintStream.println:(Z)V
      22: return
    LineNumberTable:
      line 5: 0
      line 6: 3
      line 7: 6
      line 8: 22
}

从反编译代码 #2 可以看出,代码 “Ja” + “va” 被直接编译成立 “Java”,因此 s1 == s2 的结果才是true。但是值得注意的是,常量在编译期就被确定了,所以编译器可以这样优化,但如果拼接的是一个变量,结果就不一样了。

int a = 0;
String s1 = "hello" + a;
String s2 = "hello0";
System.out.println(s1 == s2); // false

变量是在运行期才确定的,所以在拼接时候在堆上会产生一个临时对象保存在变量 s1中。

事实上,在做 + 拼接操作,编译器都有一定的优化,首先我们先理解一面一句代码的含义:
在这里插入图片描述
eg:

String s1 = new String("hel") + new String("lo");
s1 = intern();
String s2 = new StringBuilder("hel").append("lo").toString();
System.out.println(s1 == s2.intern());	// false

1> String s1 = new String(“hel”) + new String(“lo”);
常量池生成的对象:hel 、lo。注意常量池并没有生成hello对象
堆生成的对象:new String(“hel”)、new String(“lo”)、拼接操作生成的new String(“hello”),并且此时的heelo在堆里面
2> s1 = intern();
字符串常量池中,创建一个引用,指向s1指向的对象new String(“hello”) 即:获取或创建一个字符串对象或引用
3> String s2 = new StringBuilder(“hel”).append(“lo”).toString();
s2 的引用地址指向堆里面再次生成的 new String(“hello”)
4> s2.intern()
s2 的引用地址指向第 2> 步中 s1.intern() 返回的变量(池中"hello"串不存在入池并返回引用,存在则直接返回引用)

  • 关于intern() 方法任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。
    public String intern()
    返回值:一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。
  • 对于拼接操作,编译器会默认优化成 new String().append().toString()
    在这里插入图片描述
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值