Java 7 源码学习系列(一)——String

String 类提供了检查字符序列中单个字符的方法,比如有比较字符串,搜索字符串,提取子字符串,床架哪一个字符串的副本,字符串你的大小写转换等。 实例映射是基于Character类中指定的Unicode标准。

Java语言提供了对字符串连接运算符的特别支持(+),该符号也可用于将其他类型转换成字符串。字符串的连接实际上是通过StringBuffer或者StringBuilder的append()方法来实现的,字符串的转换通过toString方法实现。

private final char value[];

String其实是char[]实现的,是一个字符数组,用于存储字符串内容。从final这个官架子中看出,内容一旦被初始化了是不能被更改的。 String s = “a”; s = “b”; 不是对s的修改,而是重新指向了新的字符创

private int hash;
private static final long seriVersionUID = xxxxxxxxxxxxxxxxxxxxxxxxxxxL’
private static final ObjectStreamFiled[] serialPersistentFields = new ObjectStreamFiled(0);

String实现了Serializable接口,所以支持序列化和反序列化。 Java的序列化机制是通过运行时判断累的serialVersionUID来验证版本一致性的。反序列化时,JVM把传进来的字节流中的人serialVersionUID与本地相应实体类的serialVersionUID进行比较,相同认为一致

String的构造

使用字符数组、字符串构造一个String

可以使用一个字符数组来创建一个String,当我们使用字符数组创建String的时候,会用到Arrays.copyOf方法和Arrays.copyOfRange方法,这两个方法是将原有的字符数组中的内容逐一的复制到String中的字符数组中。不仅可也使用整个字符数组,也可以使用字符数组的一部分,只要多传入两个参数int offsetint count就可以了。 也可以使用一个String类型的对象来初始化一个String,直接将原String中的valuehash两个属性直接复制给目标String

使用字节数组构造一个String

String实例中的字符数组char[]以unicode码来存储,String和char为内存形式,byte是网络传输或存储的序列化形式。经常需要将byte[]数组和String进行相互转化。String和byte[]相互转化,需要注意编码问题。 String(byte[] tytes, Charset charset)是通过charset来解码指定的byte数组,将其解码成unicode的char[]数组,构造成新的String。如果没有指明解码使用的字符集,StringCoding的decode方法首先调用系统的默编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作。

使用StringBuffer和StringBuilder构造String

public String(StringBUffer buffer){
    synchronized(buffer){
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}

public String(StringBuilder builder){
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

这样的方式使用较少,一般直接使用它们的toString()方法来获取String。 StringBuilder的toString方法会更快,应为StringBuffer的toString方法是synchronized的,牺牲了效率的情况下保证了线程的安全。

一个特殊的保护类型的构造方法

Java7提供了一个保护类型的构造方法。

String(char[] value, boolean share){
    // assert share : "unshared not supported";
    this.value = value;
}

这个方法(方法一)根据文档说明,share暂时只支持true值,加入这个字段也是为了与String(char[] value)(方法二)进行区分。方法二在创建String是会用到Arrays的copyOf方法将value中的内容逐一复制到String中,而方法一直接将value的引用赋值给String的value。如果通过方法一构造出来的String和参数中的char[]共享一个数组。

方法一的好处:
性能好 - 直接给数组复制,而不是逐一拷贝
节约内存 - 共享了内部数组 
安全 - 虽然更改了传参的数组会影响字符串的值,但在使用中,对于调用他的方法来说,无论是源字符串还是新字符串,其value数组本身都是String对象的私有属性,从外部无法访问,所以都很安全

方法一设置为了protected,因为一旦该方法设置为公有,在外面可以直接访问,就破坏了字符串你的不可变性。 Java7中有很多String里面的方法使用了这种“性能好,节约内存,安全”,比如:substringreplaceconcatvalueOf

在Java7中,substring已经不再是优秀的方法了。在之前的版本中,虽然这种方法有很多优点,但有一个指明的缺点:有可能造成内存泄露。
PS: https://droidyue.com/blog/2014/12/14/substring-memory-issue-in-java/
Java6的subString中的构造方法的调用是直接共享内存,导致对于长字符串的内部数组不能被回收,造成内存泄露。在Java7中做了改进,使用了Arrays.copyOfRange方法,确保长字符串对象和内部数组能一起被回收。

其他方法

length() //返回字符串长度
isEmpty() //返回字符串是否为空
charAt(int index) //返回字符串第(index + 1)个字符
char[] toCharArray() //转化成字符数组
trim() //去掉两端空格
toUpperCase() //转化为大写
toLowerCase() //转化为小写
String concat(String str) // 拼接字符串, 使用String(char[] value, boolean share);
String replace(char oldChar, char newChar) //将字符串中的oldChar字符换成newChar字符, 使用String(char[] value, boolean share);
boolean matches(String regex) //判断字符串是否匹配给定的regex正则表达式
boolean contains(CharSequence s)    //判断字符串是否包含字符序列s
String[] split(String regex, int limit) //按照regex将字符串分成limit份
String[] split(String regex)

getBytes

将一个字符串转换成字节数组,使用时一定要注意编码问题

String s = "你好,世界";
byte[] bytes = s.getBytes();

在不同平台上的运行结果不一样。没有指定编码方式,所以会使用系统默认的编码方式,比如在中文操作系统中可能会使用GBK或者GB2312进行编码,但是在英文操作系统中,坑你使用ISO-8859-1进行编码。 为了避免不必要的麻烦,要执行编码方式:

String s = "你好,世界";
byte[] bytes = s.getBytes("utf-8");

比较方法

boolean equals(Object anOnject);
boolean contentEquals(StringBuffer sb);
boolean contentEquals(CharSequence cs);
boolean equalsIgnoreCase(String anotherString);
int compareTo(String anotherString);
int compareToIgnoreCase(String str);
boolean regionMatches(int toffset, String other, int ooffset, int len); //局部匹配
boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len); //局部匹配

前三个方法,String和要和比较的目标对象的字符数组内容进行比较,一样返回true,否则false,关键代码如下:

int n = value.length;
while (n-- != 0) {
    if (v1[i] != v2[i])
        return false;
    i++;
}

第四个和前三个的区别在于会将两个字符数组的内容都使用toUpperCase防范转换成大写再进行比较。

equals
  1. 判断了this == anObject, 判断是否是同一个对象
  2. anObject instanceof String ,判断anObject是不是String类型,不是直接返回false
  3. 比较两个数组的长度。不一样直接返回 false
  4. 逐一比较值 虽然代码较多,但是很大程度上提高了比较的效率。
contentEquals

有两个重载,StringBuffer需要考虑线程安全,都在contentEquals(CharSequence cs)实现。 在contentEquals(CharSequence cs)分成三种情况,一种是cs instanceof AbstractStringBuilder, 这是给StringBufferStringBuilder区分是否加锁。另一种是String类型,直接调用equals(cs)方法。另一种是通用的CharSequence,与String的equals相类似。

equalsIgnoreCase
public boolean equalsIgnoreCase(String anotherString){
    return (this == anotherString) ? true : (anotherString != null) && (ahotherString.value.length == value.length) && regionMatches(true, 0, anotherString, 0, value.length);
}

hashCode

使用了数学公式

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

在存储数据计算hash地址的时候,我们希望尽量减少有同样的hash地址,所谓“冲突”。如果使用相同hash地址的数据过多,那么这些数据所组成的hash链就更长,从而降低了查询效率!所以在选择系数的时候要选择尽量长的系数并且让乘法尽量不要溢出的系数,因为如果计算出来的hash地址越大,所谓的“冲突”就越少,查找起来效率也会提高。 31可以 由i*31== (i < < 5)-1来表示,现在很多虚拟机里面都有做相关优化,使用31的原因可能是为了更好的分配hash地址,并且31只占用5bits! 在java乘法中如果数字相乘过大会导致溢出的问题,从而导致数据的丢失. 而31则是素数(质数)而且不是很长的数字,最终它被选择为相乘的系数。

hashCode可以保证相同的字符串的hash值肯定相同,但是hash值相同并不一定是value值就相同。

substring

Java7中穿创建一个新的String并返回,将yu7anlai的char[]中的值逐一复制到新的String中,两个数组并不是共享的,虽然损失一些性能,但是有效地避免了内存泄露。

replaceFirst、replaceAll、replace

String replaceFirst(String regex, String replacement)
String replaceAll(String regex, String replacement)
String replace(CharSequence target, CharSequence replacement)

replace中的参数是CharSequence,即可以支持鞥字符的替换,也可以支持字符串你的替换

CopyValueOf和valueOf

早期为了避免直接将参数的char[]数组作为String的value值,在外部变化导致字符串值的变化,提供了copyValueOf方法。每次拷贝生成新的自出数组来构造新的String对象。 现在构造器中通过拷贝新数组实现了,所以两者已经没区别了。

public static String valueOf(boolean b) {
      return b ? "true" : "false";
  }

  public static String valueOf(char c) {
       char data[] = {c};
       return new String(data, true);
  }
  public static String valueOf(int i) {
      return Integer.toString(i);
  }

  public static String valueOf(long l) {
     return Long.toString(l);
  }

 public static String valueOf(float f) {
     return Float.toString(f);
 }

 public static String valueOf(double d) {
    return Double.toString(d);
}

intern()

返回一个字符串对象的内部化引用。 当intern被调用是,如果对象池中已经包含了相等的字符串你对象则返回对象池中的实例,否则添加字符串到对象池并返回改字符串的引用。

对“+”的重载

Java不支持重载运算符,String中的“+“是java中唯一的一个重载运算符了。 反编译后可以发现,String对“+”的支持是使用了StringBuilder以及他的append、toString两个方法

public static void main(String[] args) {
    String string = "abc";
    String string2 = string + "efg";
}

public static void main(String args[]) {
    String string = "abc";
    String string2 = (new StringBuilder(String.valueOf(string))).append("efg").toString();
}
String.valueOf与Integer.toString的区别

有三种方式将int类型的变量变成String类型

int i = 5;
String i1 = "" + i; // method 1
String i2 = String.valueOf(i); // method 2
String i3 = Integer.toString(i); // method 3

方法2和方法3没有任何区别,String.valueOf是调用Integer.toString来实现的 方法1其实是 String i1 = (new StringBuilder()).append(i).toString();

-- 2019-01-14

该如何创建字符串,使用” “还是构造函数?

在java中,有两种方式可以创建字符串

String x = "abc";
String y = new String("abc");

实例一

String a = "abcd";
String b = "abcd";
System.out.println("a == b : " + (a == b)); // true
System.out.println("a.equals(b) : " + (a.equals(b))); // true

说明a和b指向了方法区中的同一个字符串常量,引用都是相同的。

当相同的字符串常量被多次创建时,置灰保存字符串常量的一个副本,称为“字符串驻留”。 在Java中,所有编译时字符串常量都是驻留的。

实例二

String c = new String("abcd");
String d = new String("abcd");
System.out.println("c == d : " + (c == d)); // false
System.out.println("c.equals(d) : " + (c.equals(d))); // true

指向中不同的对象,不同的对象有不同的引用。

运行时发生字符串驻留

String c = new String("abcd").intern();
String d = new String("abcd").intern();
System.out.println("c == d : " + (c == d)); // true
System.out.println("c.equals(d) : " + (c.equals(d))); // true

字面值“abcd”已经是字符串类型,使用构造方式会创建一个额外没有的对象。

SO: 只需要创建一个字符串,可以使用双引号的方式 需要在堆中创建一个新的对象,选择构造函数的方式

--2019-01-14

我终于搞清楚了和String有关的那点事儿。

Q1: String s = new String("hello"); 定义了几个对象? A1: 若常量池中已经存在了“hello”,则直接饮用,也就是此时置灰创建一个对象, 如果常量池中不存在“hello”,则先创建后饮用,也就是两个

Q2: 如何理解Stringintern方法 A2: 当一个String实例str调用intern()方法时,Java查找常量池中是否存在相同Unicode的字符串敞亮,如果有,则返回其饮用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的饮用。

String s1 = "hello";
String s2 = new String("hello");
String s3 = new String("hello").intern();

System.out.println(s1 == s2);
System.out.println(s1 == s3);

//返回结果如下
false
true

未能很好理解字面量等在编译,类加载,运行时等常量池的动作,需后续继续理解

-- 2019-01-15

三张图彻底了解Java中字符串的不变性

一旦一个String对象在内存(堆)中被创建出来,他就无法被修改。特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回一个新的对象。

如果需要一个可修改的字符串,应该是用StringBuffer或者StringBuilder。否则会有大量时间浪费在垃圾回收商,因为每次试图修改都有新的string对象呗创建出来

-- 2019-01-15

为什么Java要把字符串设计成不可变的

字符串池

字符串池是方法区中的一部分特殊的存储。当一个字符串内部被创建的时候,手下你会去这个字符串池中查找,如果找到,直接返回对该字符串的饮用。

String s1 = "abcd";
String s2 = "abcd";

如果字符串可变,当两个饮用指向同一个字符串时,对其中一个做修改会影响另一个。

缓存Hashcode

经常会用到字符串的哈希码。 比如在HashMap中,字符串不可变能保证其hashcode永远保持一致,这样就可以避免一些不必要的麻烦。意味着每次在使用一个字符串时,hashcode不需要重现计算 因为String的不可变,所以对象被创建,该hash值也不可变,所以每次需要使用hashcode时,直接返回即可

使其他类的使用更加便利

未理解该示例和所要表达的一致性。。。。。。

安全性

如果String可变,网络连接,打开文件等操作会导致安全问题。 在某个方法调用连接操作时,会认为已经连接到某机器上,但是当该饮用被改变了,导致实际上并没有。 可变的字符串也可能导致反射的安全问题。

不可变对象天生就是线程安全的

因为不可变对象不能被改变,所以他们可以自由的再多个线程之间共享。不需要任何同步处理。

-- 2019-01-15

三张图彻底了解JDK 6和JDK 7中substring的原理及区别

subStsring()的作用

substring(int beginIndex, int endIndex) 方法截取字符串并返回其[beginIndex, endIndex - 1]范围内的内容。

调用substring()时发生了什么

String是不可变的,所有当使用x.substring(1, 3)对字符串变量赋值时,它会指向一个全新的字符串。

但JDK6和JDK7中调用substring时发生的事情并不一样

JDK6中的substring

JDK6中String类包含三个成员变量:char value[]int offsetint count。他们分别用来存储真正的字符数组,数组的第一个位置索引以及字符串包含的字符个数

当调用substring方法的时候,会床架哪一个新的String对象。但这个Strwing的值仍然指向堆中的同一个字符数组,两个对象只有count和offset的值不同。

关键代码

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

public String substring(int beginIndex, int endIndex){
    return new String(offset + beginIndex, endIndex - beginIndex, value);
}

导致的问题

如果有一个很长很长的字符串,但使用substring进行切割时只需要很小的一段。这可能导致性能问题。非常长的字符数组被引用而不乏被回收,可能导致内存泄露。

解决方式

x = x.substring(x, y) + ""

生成一个新的字符串并引用它。

JDK7 中的substring

在堆内存中创建一个新的数组。

关键代码

public String(char value[], int offset, int count) {
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}

public String substring(int beginIndex, int endIndex) {
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

使用new String创建一个新字符串,避免老字符串的引用,从而解决了内存泄露问题。

-- 2019-01-16

Java中的Switch对整型、字符型、字符串型的具体实现细节

Java7中,switch的参数可以是String类型。 到目前为止,switch支持一下集中数据类型:byte, short, int, char, String。

switch 对整型的支持

通过反编译,和之前的代码比较发现,除了在case行多了两行注释外没有任何区别,所以switch对int的判断是直接比较整数的值

switch对字符型的支持

通过反编译,和之前的代码比较发现,对char类型进行比较的时候,实际上比较的是ascii码,编译器会把char型变量转换成对应的int型变量

switch对字符串的支持

public class switchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
        case "hello":
            System.out.println("hello");
            break;
        case "world":
            System.out.println("world");
            break;
        default:
            break;
        }
    }
}

//反编译后

public class switchDemoString
{
    public switchDemoString()
    {
    }
    public static void main(String args[])
    {
        String str = "world";
        String s;
        switch((s = str).hashCode())
        {
        default:
            break;
        case 99162322:
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

通过查看反编译后的代码发现,字符串的switch是通过equals()hashCode()来实现的。switch中只能使用整型。 进行switch的是哈希值,然后通过使用equals方法比较进行安全检查,因为哈希可能会发生碰撞。 因此它的性能不如使用枚举进行switch或者使用纯整数常量。 因为字符串一段你创建,会把哈希值缓存起来。所以这个hashCode()方法的调用开销其实不会很大。

总结

swich只支持一种数据类型,就是整型,其他数据类型都是转换成整型之后再使用switch的。

-- 2019-01-16