我是靠谱客的博主 大气面包,最近开发中收集的这篇文章主要介绍jdk11-String源码分析,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

目录

char类型

一、定义

二、属性

String类型的性能优化

三、构造方法

四、其他方法

length方法

isEmpty方法

charAt方法

codePointAt方法

codePointBefore方法

getChars方法

equals方法


在进行String源码分析之前,我们先来介绍一下char数据类型。

char类型

char类型用于表示单个字符。通常用来表示字符常量。是一个无符号十六进制的值,从u000到uffff。

char数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。,关于UTF-16编码具体可参考 字符编码笔记:ASCII,Unicode , UTF-8,UTF-16和ISO8859-1(Latin-1)。

下面有两个个概念。

代码点:代码点(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,代码点采用十六进制书写,并加上前缀U+,例如U+0041就是字母A的代码点。

Unicode的代码点可以分成17个代码级别(code plane)。第一个代码级别称为基本的多语言级别(简称BMP),代码点从U+0000到U+FFFF,其中包括了经典的Unicode代码;其余的16个附加级别,代码点从U+10000到U+FFFFF,其中包括了一些辅助字符。

代码单元:在BMP中,每个字符用16位表示,称为代码单元(code unit)。辅助字符采用一对连续的代码单元进行编码。

因为一个char类型字符代码的是一个代码单元,而UTF-16编码辅助平面字符需要用两个代码单元来表示一个代码点。这可能会带来一些误解,因此,尽量避免用char类型。

一、定义

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

从该类的声明中我们可以看出String是final类型的,表示该类不能被继承,同时该类实现了三个接口:java.io.Serializable、 Comparable<String>、 CharSequence

二、属性

@Stable
private final byte[] value;
private final byte coder;
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
static final boolean COMPACT_STRINGS;
static {
    COMPACT_STRINGS = true;
}
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16  = 1;
  • value表示用来存储字符串对象的值,这里用byte[],@Stable表示该数组不会为null,final表示字符串一旦被初始化后是不会被改变的。

  • coder表示编码方式,Latin1(1个字节)或者UTF16(2个字节或4个字节)。

  • COMPACT_STRINGS 默认将 COMPACT_STRINGS 设置为 true。而如果要取消紧凑的布局可以通过配置 VM 参数-XX:-CompactStrings实现。

  • hash:hash值,默认为0。

String类型的性能优化

从jdk9之后,value的类型由char[]数组变成了byte[]数组。那么为什么要做这样的优化呢?为了节省空间,提高String的性能。

char占用两个字节(16bit),比如字符A,则为0x00 0x41,前面8个bit就是浪费的。也就是说刚好ISO-8859-1(0~255)编码范围的字符都会浪费8个bit,之外的字符则不会浪费。

jdk9之后,用byte[]数组存储字符串,一个byte占一个字节,即8bit。

在不指定字符编码的情况下,会有两种编码方式,Latin1(ISO-8859-1)和UTF-16,当字符都在Latin1范围内的时候就采用Latin1编码方式(紧凑布局),否则使用UTF-16。使用Latin1编码方式的时候,用了byte[]数组之后相对于使用char[]数组,空间节省了一半。

因为改变了String的实现,使用了Latin1和UTF16编码方式,需要增加一个属性coder来表明使用的是哪种编码方式。LATIN1=0;UTF16=1。

字符串对象是 Java 中大量使用的对象,而且我们会轻易大量使用它而从不考虑它的代价,所以对其的空间优化是有必要的,Java9 开始对String类进行优化, 这能帮助我们减少字符串在堆中占用的空间,而且还能减轻GC压力。同时也能看到该空间优化对中文来说意义不大。

三、构造方法

String有很多构造方法,这里介绍几个常见的构造方法。

无参数的构造函数

public String() {
    this.value = "".value;
    this.coder = "".coder;
}

传入String的构造函数

@HotSpotIntrinsicCandidate
public String(String original) {
    this.value = original.value;
    this.coder = original.coder;
    this.hash = original.hash;
}

传入char[]数组的构造方法

public String(char value[]) {
    this(value, 0, value.length, null);
}
String(char[] value, int off, int len, Void sig) {
    if (len == 0) {
        this.value = "".value;
        this.coder = "".coder;
        return;
    }
    if (COMPACT_STRINGS) {
        byte[] val = StringUTF16.compress(value, off, len);
        if (val != null) {
            this.value = val;
            this.coder = LATIN1;
            return;
        }
    }
    this.coder = UTF16;
    this.value = StringUTF16.toBytes(value, off, len);
}
//如果COMPACT_STRINGS=true,则采用LATIN1编码方式,如果其中有一个字符不在LATIN1编码范围内,则采用UTF-16编码。
//LATIN1:StringUTF16.compress(value, off, len)
public static byte[] compress(char[] val, int off, int len) {
    byte[] ret = new byte[len];
    if (compress(val, off, ret, 0, len) == len) {
        return ret;
    }
    return null;
}
//LATIN1:char[]-->byte[]
@HotSpotIntrinsicCandidate
public static int compress(char[] src, int srcOff, byte[] dst, int dstOff, int len) {
    for (int i = 0; i < len; i++) {
        char c = src[srcOff];
        if (c > 0xFF) {//判断是否在LATIN1编码0~255范围内
            len = 0;
            break;//如果不在则返回,用UTF16编码方式
        }
        dst[dstOff] = (byte)c;
        srcOff++;
        dstOff++;
    }
    return len;
}
//UTF16:StringUTF16.toBytes(value, off, len)
//UTF16:char[]--->byte[]
@HotSpotIntrinsicCandidate
public static byte[] toBytes(char[] value, int off, int len) {
    byte[] val = newBytesFor(len);
    for (int i = 0; i < len; i++) {
        putChar(val, i, value[off]);
        off++;
    }
    return val;
}
//newBytesFor() 因为utf16编码方式,一个char占用16bit,一个byte占用8bit,一个char会占用2个byte的长度,这个函数计算出char[]数组转换为bit[]之后,bit[]数组的长度。 
public static byte[] newBytesFor(int len) {
    if (len < 0) {
        throw new NegativeArraySizeException();
    }
    if (len > MAX_LENGTH) {
        throw new OutOfMemoryError("UTF16 String size is " + len +
                                   ", should be less than " + MAX_LENGTH);
    }
    return new byte[len << 1];//左移一位,相当于乘以2.
}
//将char字符写入到byte数组中。一个char字符,写入到byte[]中,占用两个位置。
@HotSpotIntrinsicCandidate
// intrinsic performs no bounds checks
static void putChar(byte[] val, int index, int c) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    index <<= 1;
    val[index++] = (byte)(c >> HI_BYTE_SHIFT);
    val[index]   = (byte)(c >> LO_BYTE_SHIFT);
}
static final int HI_BYTE_SHIFT;
static final int LO_BYTE_SHIFT;
static {
    if (isBigEndian()) {
        HI_BYTE_SHIFT = 8;
        LO_BYTE_SHIFT = 0;
    } else {
        HI_BYTE_SHIFT = 0;
        LO_BYTE_SHIFT = 8;
    }
}

假设这里用大头方式,则 HI_BYTE_SHIFT = 8;LO_BYTE_SHIFT = 0;

对于char字符存储进byte[]数组

一个char字符占用两个byte[]数组的位置,先存高8位的bit,再存低8位的bit。

val[index++] = (byte)(c >> HI_BYTE_SHIFT); 存高8位,将字符c右移8位,保存高8位。

val[index]   = (byte)(c >> LO_BYTE_SHIFT);   存低8位,将字符c右移0位,c是16位,byte类型是8位,相当于将c强转为byte类型,直接截取低8位。

例如:c=289;(所有的字符,都可以用数字来表示)

存储的二进制为:0000  0001  0010   0001

高8位:0000 0001 0010 0001>>8 = 0000 0000 0000 0001--->转为byte得到 0000 0001

低8位:0000 0001 0010 0001>>0 = 0000 0001 0010 0001--->转为byte得到 0010 0001

 

因此,byte[]数组中存储的值分别为:0000 0001 和 0010 0001

在有些函数中,我们可能需要将byte[]数组转化为char[]数组,在之后的源码中也会看到如下源码

@HotSpotIntrinsicCandidate
// intrinsic performs no bounds checks
static char getChar(byte[] val, int index) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    index <<= 1;
    return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
                  ((val[index]   & 0xff) << LO_BYTE_SHIFT));
}

上面的函数则是将byte[]数组转化为char字符,我们来看下具体是如何实现的?

还是刚刚的c=289 存在byte[]数组中是 0000 0001 和 0010 0001

c=(char)(((0000 0001 &0xff )<<8)|((0010 0001 & 0xff)<<0))  【为什么要&0xff?是为了保持低8位不变,高位补0,如果不&0xff,那么高位会自动补足符号位。而这里我们只想保持原来的二进制不变。】

高8位: 

0000 0001 & 0xff =

  0000 0000 0000 0000 0000 0000 0000 0001

&

  0000 0000 0000 0000 0000 0000 1111 1111

=0000 0000 0000 0000 0000 0000 0000 0001

再 0000 0000 0000 0000 0000 0000 0000 0001 <<8 = 0000 0000 0000 0000 0000 0001 0000 0000

低8位

  0000 0000 0000 0000 0000 0000 0010 0001

&

  0000 0000 0000 0000 0000 0000 1111 1111

=0000 0000 0000 0000 0000 0000 0010 0001<<0= 0000 0000 0000 0000 0000 0000 0010 0001

然后作或|运算

   0000 0000 0000 0000 0000 0001 0000 0000

|

  0000 0000 0000 0000 0000 0000 0010 0001

=0000 0000 0000 0000 0000 0001 0010 0001

最后 强转为char类型,截取低16位= 0000 0001 0010 0001=1+32+256=289

感觉char->byte[] 和 byte[]-->char 这个过程很有意思。

四、其他方法

length方法

public int length() {
    return value.length >> coder();
}

返回字符串的长度,指的是返回该字符串的字符(代码单元)的个数【辅助平面的Unicode代码点,有两个代码单元,即两个字符】,而不是value[]数组的长度,所以,这里做了右移运算。LATIN1编码时:coder()=0,字符串的长度=value数组长度的本身;UTF16编码时,code()=1,字符串的长度=value数组长度的一半。

isEmpty方法

public boolean isEmpty() {
    return value.length == 0;
}

 

charAt方法

返回给定位置的代码单元。辅助平面的Unicode代码点,有两个代码单元,即两个字符。

public char charAt(int index) {
    if (isLatin1()) {
        return StringLatin1.charAt(value, index);
    } else {
        return StringUTF16.charAt(value, index);
    }
}
//latin:byte[]-->char  StringLatin1.charAt(value, index)
public static char charAt(byte[] value, int index) {
    if (index < 0 || index >= value.length) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return (char)(value[index] & 0xff);
}
//utf16:byte[]-->char StringUTF16.charAt(value, index)
public static char charAt(byte[] value, int index) {
    checkIndex(index, value);
    return getChar(value, index);
}
//byte[]-->char 上面有讲过
static char getChar(byte[] val, int index) {
    assert index >= 0 && index < length(val) : "Trusted caller missed bounds check";
    index <<= 1;
    return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
                  ((val[index]   & 0xff) << LO_BYTE_SHIFT));
}

codePointAt方法

返回从给定位置开始或结束的代码点,如果是辅助平面的代码点,则这个函数相当于返回两个字符(两个代码单元)。

LATIN1编码:直接将 byte 数组对应索引的元素与0xff做&操作并转成 int 类型。

UTF16编码:  会返回该位置开始或结束的代码点,会去判断该位置的字符是否处于辅助平面,如果是则返回两个字节(代码单元)-->返回的是对应的int的数字。

public int codePointAt(int index) {
    if (isLatin1()) {
        checkIndex(index, value.length);
        return value[index] & 0xff;
    }
    int length = value.length >> 1;
    checkIndex(index, length);
    return StringUTF16.codePointAt(value, index, length);
}
//checked=false
private static int codePointAt(byte[] value, int index, int end, boolean checked) {
    assert index < end;
    if (checked) {
        checkIndex(index, value);
    }
    char c1 = getChar(value, index);
    if (Character.isHighSurrogate(c1) && ++index < end) {
        if (checked) {
            checkIndex(index, value);
        }
        char c2 = getChar(value, index);
        if (Character.isLowSurrogate(c2)) {
           return Character.toCodePoint(c1, c2);
        }
    }
    return c1;
}

codePointBefore方法

用于返回指定索引值前一个字符的代码点,实现与codePointAt方法类似,只是索引值要减1。

getChars方法

用于获取字符串对象指定范围内的字符到目标 char 数组中,主要是根据两种编码做不同处理,如果是 LATIN1 编码则直接将 byte 数组对应索引的元素与0xff做&操作并转成 char 类型。而如果是 UTF16 编码则需要两个字节一起转为 char 类型。

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
    checkBoundsBeginEnd(srcBegin, srcEnd, length());
    checkBoundsOffCount(dstBegin, srcEnd - srcBegin, dst.length);
    if (isLatin1()) {
        StringLatin1.getChars(value, srcBegin, srcEnd, dst, dstBegin);
    } else {
        StringUTF16.getChars(value, srcBegin, srcEnd, dst, dstBegin);
    }
}

equals方法

比较两个字符串对象是否相等

1.比较两个对象的引用是否一样;

2.判断是否为字符串对象;

3.根据编码方式比较;

3.1 latin1:直接比较value的每一个值是否相等;

3.2 utf16:将byte[]转化为char类型之后,一一比较每个char是否相等;

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

....还有好多方法懒得写,反正几乎所有的方法都会分两种编码方式来写。

因为已经有其他人写过这个源码分析啦,可以直接参考:

从JDK源码看String(上)

从JDK源码看String(下)

字符串连接你用+还是用StringBuilder

 

最后

以上就是大气面包为你收集整理的jdk11-String源码分析的全部内容,希望文章能够帮你解决jdk11-String源码分析所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(77)

评论列表共有 0 条评论

立即
投稿
返回
顶部