Java String - Go语言中文社区

Java String


Java String

String 源码(JDK 1.8.0_171)

String 不是基本类型,是一个类。分析一个类,应该从类定义(继承,实现接口等),变量,方法,内部类等等进行分析。

1. 类定义

public final class String implements java.io.Serializable, Comparable<String>, CharSequence

String类被final修饰,意味着这个类不能被继承。问题:那么为什么String不能被继承?

​ 实现Serializable序列化接口。

​ 实现Comparable接口,它其中只用一个方法public int compareTo(T o);用来比较两个String对象大小。

​ 实现CharSequence接口,它其中有int length();char charAt(int index);public String toString();等方法。

2. 变量定义

String类中有两个重要的成员变量value[] hash

/** The value is used for character storage. */
private final char value[];

value 被关键字private final 修饰,这意味着value对外不可见,且对修改关闭。这里需要注意的是value不可修改只是引用地址不可修改,value地址指向的是堆中的数据。如下所示例子,编译器将会报错。

final int[] value={1,2,3,4};
int[] arr = {5,6,7,8};
value = arr;

​ 但如果是修改value中的值,如下图所示,输出结果为 10

final int[] value={1,2,3,4};
value[0] = 10;
System.out.println(value[0]);

​ 如果觉得有点绕,可以看下图所示的堆内的简化版指向图。新建String对象a b

新建字符串

​ 当a=b时,只是将a中的地址引用修改,原来"aa"还是没有改变。

修改字符串的地址引用

​ 阅读String源码会发现,除了在构造方法上对value赋值外,没有对value中的值进行任何的修改。而由于valueprivate修饰,对外不可见,所以相当于String不可变。这里指的不可变不是指String实例对象不可变,是指字符串常量池中的String对象不可变。问题:那么为什么要把String设计成不可变?

/** Cache the hash code for the string */
private int hash; // Default to 0

​ 由于String的不可变性,所以缓存了hashcode,减少每次需要hashcode时而进行一次运算带来的开销。

3. 方法

String类中提供多种构造方法,本质就是将的参数传递给成员变量value[]进行初始化。接下来我们讲讲主要的几个方法。

  • equals 方法

    public boolean equals(Object anObject) {
     if (this == anObject) {
         return true;
     }
     if (anObject instanceof String) {
         String anotherString = (String)anObject;
         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;
    }
    

    equals方法第一是先比较两个对象的地址是否相同。再判断参数对象是否是String实例。如果是String实例,进行比较两个实例的数组长度。长度相同则开始对两个String对象的value[]中的值进行一一比较。如果value[]中的值都相同则返回true

  • 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);
    }
    

    concat方法是将两个字符串连接起来。这个操作并不是在this这个字符串对象中进行,而是产生一个新的String对象。Arrays中包含了操纵数组的各种方法。

  • replace方法

    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 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);
         }
     }
     return this;
    }
    

    replace方法是替换操作,主要是将原来字符串中的oldChar全部替换成newChar。先找到第一个所要替换的字符串的位置i ,将i之前的字符直接复制到一个新char数组。然后从i开始再对每一个字符进行判断是不是所要替换的字符。

  • split方法

    public String[] split(String regex, int limit) {
     /* fastpath if the regex is a
      (1)one-char String and this character is not one of the
         RegEx's meta characters ".$|()[{^?*+\", or
      (2)two-char String and the first char is the backslash and
         the second is not the ascii digit or ascii letter.
      */
     char ch = 0;
     if (((regex.value.length == 1 &&
          ".$|()[{^?*+\".indexOf(ch = regex.charAt(0)) == -1) ||
          (regex.length() == 2 &&
           regex.charAt(0) == '\' &&
           (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
           ((ch-'a')|('z'-ch)) < 0 &&
           ((ch-'A')|('Z'-ch)) < 0)) &&
         (ch < Character.MIN_HIGH_SURROGATE ||
          ch > Character.MAX_LOW_SURROGATE))
     {
         int off = 0;
         int next = 0;
         boolean limited = limit > 0;
         ArrayList<String> list = new ArrayList<>();
         while ((next = indexOf(ch, off)) != -1) {
             if (!limited || list.size() < limit - 1) {
                 list.add(substring(off, next));
                 off = next + 1;
             } else {    // last one
                 //assert (list.size() == limit - 1);
                 list.add(substring(off, value.length));
                 off = value.length;
                 break;
             }
         }
         // If no match was found, return this
         if (off == 0)
             return new String[]{this};
    
         // Add remaining segment
         if (!limited || list.size() < limit)
             list.add(substring(off, value.length));
    
         // Construct result
         int resultSize = list.size();
         if (limit == 0) {
             while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                 resultSize--;
             }
         }
         String[] result = new String[resultSize];
         return list.subList(0, resultSize).toArray(result);
     }
     return Pattern.compile(regex).split(this, limit);
    }
    

    ​ 这个方法看起来比较复杂,但其实我们一般都不会用到那一大串的内容,一般我们用到最后那一句return Pattern.compile(regex).split(this, limit);是使用Pattern的正则方式去解析并拆分成字符串数组。

4. 回答问题

​ 结合上面对String类的了解,我们现在现在来对上面的两个问题 那么为什么String不能被继承? 那么为什么要把String设计成不可变? 进行解答。

那么为什么String不能被继承?

​ 假设String是可继承的。新建一个ChildString继承String类,重写它的lenth()方法。

public class ChildString extends String {

 @Override
 public int length() {
     return 999999;
 }
}

​ 再写一个方法根据字符串长度创建数组长度。

private static int[] getArray(String string) {
 return new int[string.length()];
}

public static void main(String[] args) {
 getArray(new String());
 getArray(new ChildString());
}

​ 如代码所示,getArray(new ChildString()); 创建了长度为999999的数组,造成安全漏洞。在JavaString的使用率非常高,并且也可以看出,虽然它是非基本类型,但是开发过程已经将它当做基本类型来使用了。如果String变为可继承,那引发的安全问题可想而知。所以干脆直接将String变成不可继承,杜绝此类安全问题的产生。

​ 再有一个就是效率问题,我们都知道实例对象的方法调用是先在当前类中查找该方法,如果没有再去父类中查找,以此类推。String不可继承,不再需要往父类查找方法消耗时间。

那么为什么要把String设计成不可变?

​ 上文讲到因为String的不可继承,成员变量value[]final private修饰,所以String实例拥有不可变性。上面问题也提到JavaString的使用率非常高,可以说被当做基本类型来使用。String可变则会引发线程安全问题。如下代码:

StringBuilder s1 = new StringBuilder("a");
StringBuilder s2 = s1;
s2.append("c");
System.out.println(s1); // ac

​ 此处用StringBuilder来模拟String可变。如代码所示,最后s1输出结果是ac。假设如在某个地方设置String类型的数据库密码,而后有人获取这个String s1 实例给另一个String s2实例,在不知情的情况下对s2进行的修改,那在系统中的数据库密码就被修改了,从而导致系统崩溃。

Map<StringBuilder,Object> data = new HashMap<>();
StringBuilder s1 = new StringBuilder("a");
data.put(s1,"1");
StringBuilder s2 = s1;
s2.append("c");
data.put(s2,"2");
System.out.println(data); // ac:2

​ 再比如在Mapput(String,Object)的值,此处用StringBuilder来模拟String可变。由上面可知,由于s2的修改导致s1也变成了ac,最后并没有得到我们想要的结果。

String不可变,我们将 s1s2s2进行修改则会新建一个String实例,而不会改变s1指向的值,保证了线程安全。

String实例在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

​ 我们都知道JVMHeap中有字符串常量池。其实说白了,要String不可变就是为了更好的实现字符串常量池。字符串池的实现可以在运行时节约很多Heap空间,因为不同的字符串变量都指向池中的同一个字符串。不需要考虑被修改的问题。如果可以被改来改去的,字符串常量池就没有存在的意义了。

字符串常量池

​ 由上文可知StringJava的使用率非常高,而且很经常使用相同的字符串。为了减少字符串对象的重复创建,JVMHeap区(1.7 开始在Heap)维护了一个特殊的内存,这段内存被成为字符串常量池。这是一个缓存区域,将已经创建的对象放入缓存中,下次创建相同内容的对象直接将引用赋值给新对象,这样就减少了内存空间的使用,加快运行速度。

​ 字符串进入到常量池中的方法有两种。

  1. 通过调用String intern()方法 。执行intern()方法时,若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用。
  2. “”引起来的内容(字面量)。引号引起来的字符串,首先从常量池中查找是否存在此字符串,如果不存在则在常量池中添加此字符串对象,然后引用此字符串对象。如果存在,则直接引用此字符串。这种在编译时期就会增加到常量池中。

​ 为了更好的理解,看看下面的代码。

//符合条件 2 在常量池中创建 "ab"。
String s0 = "ab";
//s0时已经创建了"ab",直接返回常量池中的地址引用。
String s1 = "ab";
//编译器会优化成 String s2 = "ab"; 与s1情况相同。
String s2 = "a" + "b";
System.out.println(s0 == s1);   //true
System.out.println(s1 == s2);   //true

//符合条件 2 在常量池中创建 "cd"。
String s3 = "cd";
//通过new的方式创建时,在常量池中查询是否存在,没有则会创建"cd",然后在堆中创建"cd"。
//s4引用 -> 堆中"cd"
String s4 = new String("cd");
//此时 s3引用 -> 常量池中的"cd"   s4引用 -> 堆中的"cd"  所以是不相等的。
System.out.println(s3 == s4);   //false

//符合条件 2 在常量池中创建 "ef"。
String s5 = "ef";
//两个 String 实例的拼接实际上是使用StringBuilder。使用 append() 方法拼接,返会一个新的String对象。但是并没有在常量池中查看创建"ef"
//s6引用 -> 堆中"ef"
String s6 = new String("e") + new String("f");
//此时 s5引用 -> 常量池中的"ef"   s6引用 -> 堆中"ef"  所以是不相等的。
System.out.println(s5 == s6);   //false

String s7 = new String("mn");
String s8 = new String("mn");
//这个比较好理解,就是比较两个对象地址。
System.out.println(s7 == s8);   //false

//符合条件 2 在常量池中创建 "op"。
String s9 = "op";
//s10引用 -> 堆中"op"
String s10 = new String("op");
//使用 intern()方法,将"op"加入到常量池中。
//如果已存在则直接返回地址引用。
//如果不存在则存储一份堆中"op"的引用,相当于常量池中的"op" == 堆中的 "op"。
//s10引用 -> 常量池中的"op"
s10.intern();
//此时 s9引用 -> 堆中的 "op"。  s10引用 -> 常量池中的"op"  不相等
System.out.println(s9 == s10);   //false

//s11引用 -> 堆中"xy"
String s11 = new String("x") + new String("y");
//使用 intern()方法,将"xy"加入到常量池中。
//不存在则存储一份堆中"xy"的引用,常量池中的"xy" = 堆中的 "xy"。
s11.intern();
String s12 = "xy";
//此时 s11引用 -> 堆中的 "xy"。  s12引用 -> 常量池 -> 堆中的 "xy"  相等
System.out.println(s11 == s12);   //true

String 、StringBuilder、StringBuffer

​ 由于String的不可变性,当我们需要可变的操作时String就无法做到。查看源码StringBuilder StringBuffer都继承AbstractStringBuilder抽象类,成员变量value[]是包内可见可修改的,保证了可变性。

/**
 * The value is used for character storage.
*/
char[] value;

​ 查看StringBuilder的源码,value[]的初始容量在不指定的情况下为16

public StringBuilder() {
 super(16);
}
AbstractStringBuilder(int capacity) {
 value = new char[capacity];
}

​ 当容量不足的情况下将需要进行扩容,扩容的大小为当前长度的 2 倍 + 2,如果新扩容的容量还是比实际字符数量小,则直接将扩容至实际字符数量。当然并不是可以无限的扩容下去,当实际字符大小大于Integer.MAX_VALUE则会内存溢出错误,否则最大为MAX_ARRAY_SIZE。 扩容中原先的数组复制过来,再丢弃旧的数组。

private int newCapacity(int minCapacity) {
 // overflow-conscious code
 int newCapacity = (value.length << 1) + 2;
 if (newCapacity - minCapacity < 0) {
     newCapacity = minCapacity;
 }
 //MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8
 return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
     ? hugeCapacity(minCapacity)
     : newCapacity;
}
private int hugeCapacity(int minCapacity) {
 if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
     throw new OutOfMemoryError();
 }
 return (minCapacity > MAX_ARRAY_SIZE)
     ? minCapacity : MAX_ARRAY_SIZE;
}

​ 在需要大量字符串拼接的时候,不要使用String来拼接,我们知道String的不可变性,导致拼接的时候创建的是一个新的String实例,从而导致资源的浪费。因为StringBuilder的可变性,使用StringBuilder来进行字符串拼接,操作的始终都是同一个实例,并不会浪费多余的空间。如果可以确定字符长度,在初始化是指定大小,减少扩容带来的性能消耗。

StringBuffer的大多数方法都是与StringBuilder相同,不同的是StringBuffer中大部分方法被synchronized修饰,意味着StringBuffer是线程安全的。三者在不同的场景各有各的优势,在实际开发中,应当选择适合的。

有问题请可以在评论区指正。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/xiyatu123/article/details/89137837
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢