JDK源码解析之String与包装类

Integer

内部存储数据格式

/**
 * The value of the {@code Integer}.
 *
 * @serial
 */
private final int value;

所以Integer类中只有构造器和getter方法,并没有setter方法,原因就是value被final修饰不可再更改。因此我们想要修改Integer类的值只能新建Integer对象

构造器

 * @deprecated
 * It is rarely appropriate to use this constructor. The static factory
 * {@link #valueOf(int)} is generally a better choice, as it is
 * likely to yield significantly better space and time performance.
 */
@Deprecated(since="9")
public Integer(int value) {
    this.value = value;
}

这种构造器现已不推荐使用,建议换成静态方法public static Integer valueOf(int i)关于该方法将在常量池中进一步说明

 * @deprecated
 * It is rarely appropriate to use this constructor.
 * Use {@link #parseInt(String)} to convert a string to a
 * {@code int} primitive, or use {@link #valueOf(String)}
 * to convert a string to an {@code Integer} object.
 */
@Deprecated(since="9")
public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
}

将传入的字符串按10进制进行解释。如"123",返回value为123的Integer对象

常量池

实际上利用static在类加载时,预先为[-128,127]区间整数建立Integer对象,并在日后需要时将引用指向这些预加载的对象。
下面我们看一下public static Integer valueOf(int i)的源码

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

当传入参数IntegerCache.low<=i<=IntegerCache.high时,返回IntegerCache.cache[]中的对象,否则在堆中新建实例。
接着我们再看Integer中的静态内部类IntegerCache的部分源码

// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
    Integer[] c = new Integer[size];
    int j = low;
    for(int i = 0; i < c.length; i++) {
        c[i] = new Integer(j++);
   }
    archivedCache = c;
}
cache = archivedCache;

可以看到在类加载时会预先为某个区间缓存。(这个区间就是[-128,127],为啥看不懂)
我们可以用如下代码进行测试

Integer a1 = -127;
Integer a2 = -127;
Integer b1 = 128;
Integer b2 = 128;
System.out.println(a1==a2);
System.out.println(b1==b2);

常用方法

  1. hashCode

        /**
         * Returns a hash code for an {@code int} value; compatible with
         * {@code Integer.hashCode()}.
         *
         * @param value the value to hash
         * @since 1.8
         *
         * @return a hash code value for an {@code int} value.
         */
        public static int hashCode(int value) {
            return value;
        }
    

    将Integer中保存的int值作为hash code

  2. equals

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
    

    调用equals()判断相等时,首先先进行类型匹配判断,如果传入了非Integer的实例,则直接判定为false。

    Integer i = 10;
    Byte b = 10;
    System.out.println("i == b " + i.equals(b));\\i==b false
    

    之后比较内部保存的int变量。

    Integer a1 = 10;
    Integer a2 = new Integer(10);
    System.out.println(a1==a2);//false
    System.out.println(a1.equals(a2));//true
    

    note:关于其中==与equals方法的不同这里不做说明,详情百度

  3. sum
    在java里没有重载操作符的,所以

    Integer a1 = 10;
    Integer a2 = new Integer(10);
    System.out.println(20==(a1+a2)); //true
    

    上面这段代码实际上进行了自动拆箱(见下),因为Integer是不能相加的。最后是两个int类型的数据判断,而非地址判断。

自动拆装箱

自动拆装箱是一个语法糖。所谓语法糖就是让你在写代码的时候少写点,后期编译器帮你加上。我们来看这一段程序
```java
public static void main(String[] args) {
	Integer a1 = 10;
	Integer a2 = new Integer(10);
	System.out.println(Integer.valueOf(20)==(a1+a2));
}
```
通过javap指令查看字节码指令我们可以看到,实际运行(部分)时是这样的
  ```
   2: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   5: astore_1
  ```
  执行`Integer a1 = 10;`实际调用了Integer类的静态方法`Integer.valueOf`进行装箱
  在判断`Integer.valueOf(20)==(a1+a2)`时
  ```
28: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
  31: aload_1
  32: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
  35: aload_2
  36: invokevirtual #6                  // Method java/lang/Integer.intValue:()I
  39: iadd
```
调用`Integer.intValue()`方法进行拆箱,最后是俩个基本类型做比较

String

官方文档 Java SE8 String

成员变量

/**
 * The value is used for character storage.
 *
 * @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 * field after construction will cause problems.
 *
 * Additionally, it is marked with {@link Stable} to trust the contents
 * of the array. No other facility in JDK provides this functionality (yet).
 * {@link Stable} is safe here, because value is never null.
 */
@Stable
private final byte[] value;

存储字符串内容。
note:同Integer一样,也是final修饰。表示String一经创建则不可变

/**
 * The identifier of the encoding used to encode the bytes in
 * {@code value}. The supported values in this implementation are
 *
 * LATIN1
 * UTF16
 *
 * @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 * field after construction will cause problems.
 */
private final byte coder;

编码类型。支持LATIN1和UTF16,其中UTF16编码一个字符占2位

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

hash值缓存。关于hash将在下文详细说明

构建String对象

  1. 直接赋值
    String s1 = "abc";
    引用方法区的运行时常量池对象(关于常量池在下文中具体说明),这一过程中没有新建String对象
  2. String(String)
    String s2 = new String("abc");
    这种方法比较浪费内存。该构造方法会在heap中申请一块内存,内存中存放字符串内容的引用。所以在使用时,我们访问了引用指向内存中的String引用。因此初始化String为常量时,推荐上面的写法。

常量池

和字符串相关的常量池有三个,分别是:.class文件中的常量池,运行时常量池字符串常量池

1.常量池
存放编译期生成的字面量及符号引用,详情请调用javap指令看一下,这里只放个截图
在这里插入图片描述
2. 运行时常量池
位于方法区,在类加载之后由常量池转换而来,还附加了部分符号引用的直接引用

3. 全局字符串常量池
存放String实例的引用

接下来我们看一看这几个常量池的关系:
运行时常量池由常量池转化而来,在程序初始 全局字符串常量池 沿用了 运行时常量池
为了验证这一结论,笔者设计这么一段程序 如有错误,还请务必指出

public class C1{
    public static String s0 = "abc";
    public String s1 = "abc";
}
public class language {
    public static String s0 = "abc";

    public static void main(String[] args) {
        C1 tmp = new C1();
        String s1 = "abc";
        String s2 = new String("abc");
        System.out.println(C1.s0==s0);	//ture
        System.out.println(s1==s0); 	//true
        System.out.println(s1==tmp.s1); //true
    }
}

首先,C1.s0==s0说明运行时常量池不是私有的,而是共享的。
其次,下面两句表明:对于已出现的字面量,将直接引用运行时常量池中的实例对象,而不是在首次运行时创建

而对于没有出现在运行时常量池中的字符串,如下

    public static String s0 = "abc";
    public static String s3 = "abcabc";

    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = new String("abc");
        System.out.println((s1+s1)==s3); \\false
    }

则新建对象加入全局字符串常量池

在1.8以前,对字符串的相加会被编译器转换为StringBuilder的append,然后toString返回字符串;
在1.8以后,则是调用了方法`makeConcatWithConstants`进行字符串的拼接,执行过程较为复杂,暂不说明<del>如果看到记得喊我回来天坑</del>

常用方法

  1. intern

    Returns a canonical representation for the string object.

    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.

    根据JDK源码的注释,intern方法用于向全局字符串常量池(以下简称常量池,并非代表class文件中的常量池)中添加String实例。
    调用重写的equals()方法判断是否相同。
    如果在常量池中已有相同的文本,则返回常量池中的引用;如果没有,则向常量池中添加该对象的引用,并返回该对象的引用
    下面用一段程序验证前半句

    public class C1{
        public static String s1 = "abcabc";
    }
    
    public static void main(String[] args) {
        String s0 = "abc";
        String s1 = s0 + s0;
        System.out.println(s1==C1.s1);	//false
        String s2 =s1.intern();
        System.out.println(s2==C1.s1);	//true
        System.out.println(s1==s2);		//false
    }
    

    在类加载时,会将字面量添加到常量池中,而根据s1==C1.s1可知s1此并不在常量池中,调用intern方法向常量池中添加,由于常量池中已存在该文本实例,因此返回了C1.s1的引用

  2. equals

    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;
    }
    
        @HotSpotIntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            int len = value.length >> 1;
            for (int i = 0; i < len; i++) {
                if (getChar(value, i) != getChar(other, i)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
    

    首先比较地址,之后比较类型,最后遍历是否相同

  3. hashcode

    public static int hashCode(byte[] value) {
        int h = 0;
        int length = value.length >> 1;
        for (int i = 0; i < length; i++) {
            h = 31 * h + getChar(value, i);
        }
        return h;
    }
    

    就一散列码,没啥好说的,如果有对系数31感兴趣的可以看看底下知乎的链接

  4. subString

    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = length() - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        if (beginIndex == 0) {
            return this;
        }
        return isLatin1() ? StringLatin1.newString(value, beginIndex, subLen)
                          : StringUTF16.newString(value, beginIndex, subLen);
    }
    
    public static String newString(byte[] val, int index, int len) {
        if (String.COMPACT_STRINGS) {
            byte[] buf = compress(val, index, len);
            if (buf != null) {
                return new String(buf, LATIN1);
            }
        }
        int last = index + len;
        return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
    }
    

    注意:String类是不可变的,因此创建字串是构建了一个新的String实例,而并非对原有String改动。中间借助了byte数组作为缓存实现对String的改动。

参考文章

  1. 知乎:Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的?
  2. 周志明-深入理解JAVA虚拟机
  3. java String hashCode() 设计的道理?

猜你喜欢

转载自blog.csdn.net/white_156/article/details/107209614