String类(仅限于JDK8)

576 阅读12分钟

Java String类

String类是最常用引用类,它的重要性不言而喻,所以我们需要从底层对其进行一些探索,使得我们对其更加的了解。

String类的属性有

  1. char value[](用于存储字符)
  2. int hash(默认hash为0,用于缓存)

String类有两种定义方式:

  1. 直接通过双引号进行定义,如:String s="abc";

  2. 通过new String("abc");方式进行定义,而在String类中又有多个重载的构造方法 String构造方法:

String()
/*
该构造方法为创建一个空的字符串,将""的value赋值给当前创造的对象的value
*/    
String(String original)
/*
该字符串通过将参数字符串中的value赋值给当前创造的对象的value
*/    
String(char value[])
/*
该字符串通过Arrays.copyOf(value, value.length)对当前的对象中的value进行赋值。
使用Arrays.copyOf的原因是因为,由于数组之间的赋值通过引用同一块地址进行赋值,所以在改变一个数组的同时会对内存空间中的值进行修改,导致另一个数组的值也进行改变。因为String是一个不可变的对象,如果直接进行数组间的辅助会与设计String的思想发生冲突,所以该构造方法就通过Arrays.copyOf的方式另开辟一块内存用于存储当前对象的值,这样String的不可变性质就得到了保障
*/    
String(char value[], int offset, int count)
/*
offset:char数组偏移量,字符串中第一个字符在字符串的位置
count:字符串的总长度
该构造方法首先会判断offset的值是否小于0,是的话抛出异常,否则执行后面的代码。
判断count的值是否小于等于0,小于0直接抛异常,等于0就是给当前对象中的value赋空值并return
然后再判断offset+count的值是否会大于字符数组的长度,大于抛异常
若以上的条件都不满足就会调用Arrays.copyOfRange(value, offset, offset+count),使用该方法的原因与上面的构造方法理由一样,保证String的不可变的特性
*/    
String(int[] codePoints, int offset, int count)
/*
codePoints:int数组,表示为ASCll值
该构造方法的意思就是通过传进来的ASCll数组进行String类的创建
首先会跟上面的构造方法一样判断offset和count
判断之后对int数组中的值进行判断,判断条件为数值是否在char范围内,在的话就continue,否则再判断是否在Unicode,在的话char数组+1,否则抛出IllegalArgumentException异常
通过上面的所有条件判断之后最后进行int转换为char的循环,在循环的过程中会判断当前数值是否在char范围内,在的话就直接进行转换赋值给char数组,若不在就会将这个数值进行范围缩小使其范围在char范围内,分别调用lowSurrogate和highSurrogate,所以一个不在char范围内的数值会占用两个char数值的位置。
*/    
//以下两个构造方法已经被弃用    
/*String(byte ascii[], int hibyte, int offset, int count)
String(byte ascii[], int hibyte)*/
String(byte bytes[], int offset, int length, String charsetName)  
/*
对charsetName变量进行判断,若charsetName==null则抛出空指针异常
再进行对offset和length字段的大小关系判断,若有问题则抛出异常,判断条件与上面的对offset和length判断的一致
最后调用StringCoding.decode(String charsetName, byte[] ba, int off, int len)对其对象中的value进行赋值 
*/    
String(byte bytes[], int offset, int length, Charset charset)
/*
对charset变量进行判断,若charset==null则抛出空指针异常
再进行对offset和length字段的大小关系判断,若有问题则抛出异常,判断条件与上面的对offset和length判断的一致
最后调用StringCoding.decode(Charset cs, byte[] ba, int off, int len)对其对象中的value进行赋值
*/     
String(byte bytes[], String charsetName)
/*
调用String(byte bytes[], int offset, int length, String charsetName)
其中offset=0,length=bytes.length
*/
String(byte bytes[], Charset charset)
/*
调用String(byte bytes[], int offset, int length, Charset charset)
其中offset=0,length=bytes.length
*/    
String(byte bytes[], int offset, int length)
/*
对offset和length进行判断
再调用StringCoding.decode(byte[] ba, int off, int len)方法
*/	 
String(byte bytes[])
/*
调用String(byte bytes[], int offset, int length)
其中offset=0,length=bytes.length
*/    
String(StringBuffer buffer)
/*
使用Arrays.copyOf(buffer.getValue(), buffer.length())对其对象中的value进行赋值,原因与上面的一致,保证String的不可变的特性,又因为StringBuffer是一个线程安全的类,所以使用synchronized代码块对其进行线程安全保障
*/
String(StringBuilder builder)
/*
使用Arrays.copyOf(builder.getValue(), builder.length())对其对象中的value进行赋值,原因与上面一致,保证String的不可变特性
*/    
String(char[] value, boolean share)    
/*
对创造的对象中的value进行赋值,share变量没有什么作用
*/

调用new String("ab")会在JVM中创建几个对象?

public class StringTest {
    public static void main(String[] args) {
        String s=new String("ab");
    }
}

对应的JVM指令

通过JVM指令我们发现,他分别会在堆内存和字符串常量池中创建对象

那么以下代码会创建几个对象呢?

public class StringTest07 {
    public static void main(String[] args) {
        String s=new String("a")+new String("b");
    }
}

对应的JVM指令

1、new出一个StringBuilder对象

2、new String("a");

3、在字符串常量池中创建"a"对象

4、new String("b");

5、在字符串常量池中创建"b"对象

一共五个对象吗?答案是否定

我们发现最后会调用StringBuilder中的toString()方法对s进行引用赋值

StringBuilder中toString()源码

发现底层调用了String类的构造方法,注意该构造方法只会在堆空间创建对象,不会在字符串常量池中创建

6、toString()方法创建了一个String()对象

所以总共有六个对象

String类的intern方法

因为String是由一个char数组构成的,所以大多数的String方法都是对char数组进行操作,由于方法众多所以不在此进行讲解,有兴趣的可以自己进行查看

但是String类中有一个很重要的方法,intern()。我们对它来进行讲解

首先我们查看源码关于intern()方法的注释

  /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */

通过源码的注释可知:

调用intern()方法会首先去常量池中查找有没有对应的值,采用equals方法进行比较。如果有对应的值就直接返回常量池引用,如果没有则会把对象的引用地址复制一份,放入字符常量池,并返回字符常量池串池中的引用地址

代码测试

public class StringTest {
    public static void main(String[] args) {
        String s1=new String("ab");//1
        s1.intern();//2
        String s2="ab";//3
        System.out.println(s1==s2);
    }
}

代码测试结果为fasle

原因:首先执行1的时候创建两个对象,分别位于堆内存和字符串常量池中,那么执行2的时候由于常量池中有了"ab",就不会再字符串常量池中创建了,所以在执行3的时候就会直接引用字符串常量池中对应的地址,所以结果为false

那么以下代码又会是如何呢?

public class StringTest {
    public static void main(String[] args) {
        String s1=new String("a")+new String("b");//1
        s1.intern();//2
        String s2="ab";//3
        System.out.println(s1==s2);
    }
}

代码测试结果为true

原因:执行1的时候不会在字符串常量池中生成对象,那么在执行2的时候就会将1执行之后的地址复制一份放到字符串常量池中,所以执行3的时候就会去字符串常量池中找,发现了那一份复制的对象与自身的值相等,就将该地址返回,所以返回true

String类的不可变特性

不可变的意思为一个对象一旦被创建,则它的内部状态将永远不会发生改变。所以,没有一个线程可以修改其内部状态和数据,同时其内部状态也绝不会自行发生改变。所以也正是这一特性使得String类在多线程的环境中变得安全可靠

String类的不可变特性是否保证的呢?

打开源码发现

1、String类被final修饰符所修饰,被final修饰符修饰的类无法被继承,这也使得无法通过子类来对父类进行访问

2、String类中最重要的属性就是char数组,char数组保存了当前String类的数据。因为char数组被private、final所修饰,所以无法通过对象来对value属性进行操作

3、在String类中的方法里,涉及到对String类进行修改操作的方法都会new出一个新的String类进行返回,如

这样的操作也保证了String的不可变的特性

但是String的不可变性真的无法进行破坏吗?答案是否定的,接下来我们进行测试

String s1="abcd";
String s2=s1;
Class<? extends String> aClass = s2.getClass();
Field value = aClass.getDeclaredField("value");
value.setAccessible(true);
char[] o = (char[])value.get(s2);
o[0]='e';
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);

通过测试我们发现,利用反射的方法可以对String类的不可变的特性进行破坏。

结论

Java中String类的不可变特性是一种约定,是一种规范,对于这种约定我们理应遵守。String类的不可变特性的实现值得学习,在以后需要使用到这种不可变特性的时候我们可以模仿String类来实现这种特性。

String的内存分配

在Java语言中有8种基本数据类型和一种比较特殊的类型String。这些 类型为了使它们在运行过程中速度更快、更节省内存,都提供了一种常量池的概念。

常量池就类似一.个Java系统级别提供的缓存。8种基本数据类型的常量 池都是系统协调的,String类 型的常量池比较特殊。它的主要使用方法有两种。

  • 直接使用双引号声明出来的String对象会直接存储在常量池中。
    • 比如: String info = "abc" ;
  • 如果不是用双引号声明的String对象,可以使用String提供的intern()方法。

Java 6及以前,字符串常量池存放在永久代。

Java 7中Oracle的工程师对字符串池的逻辑做了很大的改变,即将字符串常量池的位置调整到Java堆内。

  • 所有的字符串都保存在堆(Heap)中,和其他普通对象一样,这样可以让你在进行调优应用时仅需要调整堆大小就可以了。
  • 字符串常量池概念原本使用得比较多,但是这个改动使得我们有足够的理由让我们重新考虑在Java 7中使用String. intern()。

Java8元空间,字符串常量在堆。

String Pool的底层结构

String Pool使用的底层结构为HashTable进行数据存储,HashTable由数组和链表构成,数组用于存储String的hash值,链表用于存储相同hash值的String数据

String的String Pool默认值大小长度是1009。如果放进StringPool的String非常多, 就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String. intern时性能会大幅下降。

使用一XX: StringTableSize可设置StringTable的长度

在jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。StringTableSize设 置没有要求

在jdk7中,StringTable的长度默认值是60013

jdk8开始,1009是StringTable长度可设置的最小值

String拼接操作

一、常量与常量使用+号进行拼接操作,会在编译器进行优化,我们通过查看字节码文件可知

Java代码

字节码文件

二、字面量与变量使用+号进行拼接,我们根据JVM指令进行分析

Java代码

Java代码对应指令

对指令进行分析:

1、创建一个StringBuilder对象

2、调用StringBuilder中的append方法对"ab"进行拼接,再调用append方法对s1中的值进行拼接得到最终的StringBuilder对象

3、调用toString方法对s2变量进行赋值

但是一旦使用变量就会创建一个StringBuilder对象进行拼接嘛?,我们继续使用代码进行测试

Java代码

Class文件代码

通过Class文件发现,被final修饰的变量会在编译期间进行优化。因为被final修饰符修饰的变量会在编译期间载入类的常量池中,所以在编译期间就会进行优化

最后我们进行一个小测试

Java代码

测试结果

对结果进行分析:

1、由于通过字面量赋值的变量会直接放到字符串常量池中,首先会检查字符串常量池中有没有,有的话直接引用,没有的话创建再引用,所以为true

2、由于使用+号对变量进行拼接会在底层创建StringBuilder,然后调用append方法,所以会在堆内存中开辟一块空间,所以为false

3、理由与2相同

4、由于被final修饰的String变量会在编译期间直接存放到常量池中,所以为true

5、理由与2相同

6、理由与2相同