Java----谈谈String

一.简介

String 可以说是 Java 中使用得最频繁的一个类了,不管是作为开发者的业务使用,还是一些系统级别的字符使用, String 都发挥着重要的作用。又因 String 是不可变的、f且 Java 在运行时也保存了一个字符串池(String pool) ,就使得 String 变得很特殊。

二.==/equals

String 对象的创建有两种方式:

1
2
String strComplier = "A";
String strNew = new String("A");

虽然同样是创建字符串 “A” ,但是这两种方式在内存分配上是由区别的。

1.String strComplier = “A”;

Java 程序在运行的时候会维护一个常量池,编译期生成的各种字面量和符号引用会在类加载后进入方法区的运行时常量池。对于 上述这种实现字符串的方式就可以在编译的时候确定字符串的内容,因此这一行生成的内存结构就如下图。
image.png

不严谨的讲:虚拟机栈中的 strComplier 存储的就是 A 在常量池中的地址。

2. String strNew = new String(“A”);

因为使用的 new 的方式,所以这句代码只有的运行的时候才能确定字符串的内容。而对于 new 关键字,java 是将对象的实例数据存放堆上的,但是又因 String 常量池的存在,因此实际上在堆上的 String 对象的数据又指向了字符串常量池。

image.png

不严谨的讲:虚拟机栈中的 strNew 存储的就是 strNew 这个对象在堆内存的地址,而 strNew 中的字符串数据又指向了常量池中的 A .

3.==/equals

有了前面的知识基础之后,对于字符串这两个比较操作就十分简单了。

(1).==

== 比较的两个对象的引用是否相等,也就是说比较两个地址是否相等,显然对于下面的比较。

1
2
3
String a = "A";
String a1 = "A";
System.out.println(a1 == a); //指向的都是 A 的地址,地址相同,返回的是 true

1
2
3
String a = new String("A");
String a1 = new String("A");
System.out.println(a==a1);//分别指向的是在堆内存上的对象的地址,地址不同,返回的是 false
1
2
3
String a = "A";
String a1 = new String("A");
System.out.println(a == a1);//一个指向常量池,一个指向堆,返回 false
1
2
3
String a = "A";
String a1 = a; //编译的时候不能确定
System.out.println(a == a1);//一个指向常量池,一个指向堆,返回 false

通过这个例子可以看出实际上对于 == 的比较,只要在编译的时候能够确定的,都是相同 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String a = "A1";
String a1 = "A" +1;
System.out.println(a == a1);//true

String a = "A1";
String a1 = a +1;
System.out.println(a == a1);//false

String a = "A1";
final String a1 = a + 1;
System.out.println(a == a1);// final 修饰的在 a1 在编译的时候就能确定下来

private static String get1() {
return "1";
}
String a = "A";
final String a1 = get1();
String a2 = "a" + a1;
System.out.println(a == a2); //使用了 方法,只能在运行的时候确定,所以返回false

(2)equals
1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}

对于 equals 的比较:

  • 首先如果是两个对象相同的对象就直接返回 true .
  • 接着先判断时候属于 String 类的对象,不是就直接返回 false ,这就说明 对于 StringBuilder(“A”) 和 String(“A”) 显然 equals 返回的是 false ,但是对于前面说过的两种方式,实际上都是 String 对象。
  • 然后,就要判断编码的方式如果编码的方式不同就直接返回 false ,如果编码方式相同就比较两个字符串是否完全相等。
  • 如果两个字符串是完全的一样, 就是 true .
1
2
3
4
5
6
7
String stringCompiler = "A";
String stringNew = new String("A");
StringBuilder stringBuilder = new StringBuilder("A");
StringBuffer stringBuffer = new StringBuffer("A")
System.out.println(stringCompiler.equals(stringNew)); // true
System.out.println(stringCompiler.equals(stringBuilder)); // false
System.out.println(stringNew.equals(stringBuffer)); // false
(3)引申
1
String strNew = new String("A");

对于这行代码,前面说过会在堆保存对象的实例数据,然后在常量池保存了 “A” 这个常量。所以这一句实际涉及了两个 String 对象。

三.final

1
2
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {}

众所周知,String 类是 final ,也就是String 是不可继承的。且对于内部的实现 char 数组也是通过 final 修饰的,加上使用 privvate 进行修饰,且对 String 的所有修改都不会设计 char 数组 的修改吗,即一个 String 对象创建之后所有对它修改后的字符串都是新生成的 String 对象,因此 String 是不可变得。

String 设计成 final 主要有下的原因:

  • 实现字符串常量池,只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以在运行时节约很多 heap 空间,因为不同的字符串变量都指向池中的同一个字符串,即可以实现多个变量引用 JVM 内存中的同一个字符串实例,如果字符串不是不变的,String interning 将不能实现(String interning 是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。
  • 安全问题,在系统中有很多地方都是以字符串的形式存在的,比如数据库的用户名,Socket 的主机和端口,当你在调用其他方法,比如调用一些系统级操作之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,其内部的值被改变了,可能引起严重的系统崩溃问题。
  • 缓存提升性能,String 大量运用在哈希的处理中,由于 String 的不可变性,可以只计算一次哈希值,然后缓存在内部,后续直接取就好了,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。
  • 线程安全,因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。

使用 StringBuilder 和 StringBuffer 拼接字符串

前面说过 String 是final 类的,因此对于字符串的拼接实际上就是创建了新的对象。

1
2
3
4
String str = "111";
str += "222";
str += "333";
System.out.println(str);

编译器每次碰到 ”+=” 的时候,会 new 一个 StringBuilder 出来,接着调用 append 方法,在调用 toString 方法,生成新字符串。因此对于字符串的拼接,应该直接使用 StringBuilder 或者 StringBuffer。虽然 这两类也是 final 但是他们在内部 维护了父类一个可变的 byte 数组,每次 append 的时候就往 byte 数组里面放字符.

StringBuffer 和 StringBuilder用法一模一样,唯一的区别只是 StringBuffer 是线程安全的,它对所有方法都做了同步,StringBuilder 是线程非安全的,所以在不涉及线程安全的场景,比如方法内部,尽量使用 StringBuilder,避免同步带来的消耗.

  • StringBuffer

    1
    2
    3
    4
    5
    6
    @Override
    public synchronized StringBuffer append(Object obj) {
    toStringCache = null;
    super.append(String.valueOf(obj));
    return this;
    }
  • StringBuilder

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Override
    public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
    }

    @Override
    @HotSpotIntrinsicCandidate
    public StringBuilder append(String str) {
    super.append(str);
    return this;
    }

四.null

String 是一个 引用型对象,因此也就存在着 null 值,又因 String 是一个经常使用的对象,如果为 null 的话很可能导致整个系统崩溃,因此 String 对 null 值有很高的容错率

1
2
3
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}

  • String 对象:直接判断是否为 null,如果为 null 给 null 对象赋值为”null”。
  • 非 String 对象:通过调用 String.valueOf 方法,如果是 null 对象,就返回 “null” ,否则调用对象的 toString 方法

对于字符串的拼接也是如此

1
2
3
String s = null;
s = s + "!";
System.out.print(s);// 输出 null!

前面说过对于 “+=” 都是转为 StringBuffer 的 append ,下面就看 AbstractStringBuilder 是如何处理 null 的 (AbstractStringBuilder 是 StringBuffer 和 StringBuilder 的父类 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private AbstractStringBuilder appendNull() {
ensureCapacityInternal(count + 4);
int count = this.count;
byte[] val = this.value;
if (isLatin1()) {
val[count++] = 'n';
val[count++] = 'u';
val[count++] = 'l';
val[count++] = 'l';
} else {
count = StringUTF16.putCharsAt(val, count, 'n', 'u', 'l', 'l');
}
this.count = count;
return this;
}

可以看到也是直接添加 “null” 字符串。

五.保密性

为什么针对安全保密高的信息,char[]比String更好?

因为 String 是不可变的,就是说它一旦创建,就不能更改了,直到垃圾收集器将它回收走。而字符数组中的元素是可以更改的(这就意味着可以在使用完之后将其更改,而不会保留原始的数据)。所以使用字符数组的话,安全保密性高的信息(如密码之类的)将不会存在于系统中被他人看到。

0%