java源码解析之String类(一)

  String是我们接触最多的类,无论是学习中还是工作中,基本每天都会和字符串打交道,从字符串本身的各种拼接、切片、变形,再到和其他基本数据类型的转换,几乎无时无刻都在使用它,今天就让我们揭开String神秘的面纱,这一小节主要讲解String的源代码是怎么构建的,下一节是String的一些疑点难点,通常在面试中会被问到。

  在学习String之前,让我们先简单的看一下JVM的内存模型,附图一张,来自百度百科:

在java的每一个通过编译生成字节码文件后,运行期JVM将字节码文件(也就是class文件)的对象通过类加载器加载到JVM运行区,如上图,通过线程独享和共享分为俩部分,其中线程共享区包括方法区和java堆,线程独享区包括虚拟机栈、本地方法栈和程序计数器

我们常说的基本类型在栈中,引用类型在堆中,其中的堆就是java堆(线程共享),栈就是虚拟机栈(线程独享),而String还涉及到方法区,因为String对象是常量,因为String其实就是char数组,而这个数组被final修饰了,详情在源代码中。

/*
 * 
 * 在java程序中,所有字符串文字都是String的实例,字符串是不变的; 它们的值在创建后无法更改。 
 * 字符串缓冲区(StringBuffer和StringBuild)支持可变字符串。因为String对象是不可变的,所以它们可以共享。 
 * 例如:
 * String str =“abc”;
 * 相当于:
 * char data [] = {'a','b','c'};
 * String str = new String(data);
 * String对象并不是简单的存在于堆内存中,而是在方法区的常量池中,我会在下一节详细讲解                                                          
 * 
 */

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /*
     * 该值用于字符存储。到这里就为什么String字符串会在常量池中我想大家一定豁然开朗了,因为final修饰的就是常量,而且不可修改,也就是说当我们定义一个
     * String a = "gaoxiaolong"
     * 从你的程序开始跑到最后结束运行,这个"gaoxiaolong"都一直存在,当然如果被当成垃圾回收那就另当别论了。而我们可以改变的仅仅是这个String的引用a的指向:
     * a = "gollong",这是a的指向变了,但是"gaoxiaolong"仍然在常量池中,如果再有一个b引用,String b = "gaoxiaolong",这时这个"gaoxiaolong"
     * 就不用再生成了,因为已经存在了。当然如果在这期间"gaoxiaolong"已经被回收就不是这么一回事了。
     */
    private final char value[];

    /*
     * 缓存字符串的哈希码,默认为0
     */
    private int hash; 

    /*
     * 实现Serializable(序列化)接口生成的序列化UID,用于序列化和反序列化识别此对象是String对象, 我会用一章来讲解序列化和反序列化
     */
    private static final long serialVersionUID = -6849794470754667710L;

    /*
     * Java.io.ObjectStreamField类是可序列化字段来自Serializable类的描述。 ObjectStreamFields数组用来声明一个类的序列化字段。
     * 在序列化和反序列化中详细详细讲解
     */
    private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

    /*
     * 初始化新创建的String对象,使其表示空字符序列。 请注意,由于字符串是不可变的,因此不必使用此构造函数。
     */
    public String() {
        this.value = "".value;
    }

    /*
     * 初始化新创建的String对象,使其表示与参数相同的字符序列; 
     * 换句话说,新创建的字符串是参数字符串的副本。 除非需要original的明确副本,否则不必使用此构造函数,因为字符串是不可变的。
     * 可见官方极力推崇String a = "gaoxiaolong"的写法,这种构造器仅仅是创建一个副本。其实这个副本是在堆内存中的一个引用指向方法区中的真正的"gaoxiaolong"
     * 他们还是同一个"gaoxiaolong",只不过栈当中的地址值不同罢了
     */
    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    /*
     * 比较常用的一种构造方法,可以把一个字符数组转化为字符串,其实就是将这个字符数组赋值给当前引用的char[] value属性
     * 分配新的String,使其表示当前包含在字符数组参数中的字符序列。 复制字符数组的内容; 后续修改字符数组不会影响新创建的字符串。
     *    内部调用   Arrays工具类中的copyOf方法
     */
    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
    /*
     * 这个构造器看源码我们可以看到将char数组直接赋值给当前对象的value属性,区别于上一个构造器,上一个是完全复制一份数组,
     * 相互之间不影响,而这个是俩个引用指向同一个数组,一个被改变另一个也会被改变。
     * 但是这个构造器的访问权限是包访问权限,所以我们无法使用。不必纠结
     */
    String(char[] value, boolean share) {
        // assert share : "unshared not supported";
        this.value = value;
    }
    /*
     * 分配一个新的String,其中包含字符数组参数的子数组中的字符。offset参数是子数组的第一个字符的索引,
     * count参数指定子数组的长度。 复制子阵列的内容; 后续修改字符数组不会影响新创建的字符串。
     * 从源码中我们可以看出一种特殊情况:如果count = 0且offset <= value.length那么实际上就是一个空字符串
     * 这里一定要记住,第三个参数是个数,也就是长度,千万不要记成结束位置,切记切记!
     * 这一点比较特殊,在字符串构造器中,都是offset和count参数,而在切片函数substring(int beginIndex, int endIndex)中
     * 所需要的参数却是开始位置和结束位置,一定不要记混了。
     */
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // 如果offset+count的值超过的数组的长度自然报错,如果错误信息返回的是offset+count的值,则是这个错误。
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset + count);
    }

    /*
     * 
     */
    public String(int[] codePoints, int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= codePoints.length) {
                this.value = "".value;
                return;
            }
        }
        if (offset > codePoints.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }

        final int end = offset + count;

        // Pass 1: Compute precise size of char[]
        int n = count;
        for (int i = offset; i < end; i++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                continue;
            else if (Character.isValidCodePoint(c))
                n++;
            else
                throw new IllegalArgumentException(Integer.toString(c));
        }

        // Pass 2: Allocate and fill in char[]
        final char[] v = new char[n];

        for (int i = offset, j = 0; i < end; i++, j++) {
            int c = codePoints[i];
            if (Character.isBmpCodePoint(c))
                v[j] = (char) c;
            else
                Character.toSurrogates(c, v, j++);
        }

        this.value = v;
    }
    /*
     * 检查边界的一个函数,在上面将字符数组转换为String对象时,我们见过了函数内的边界判断,由于关于字节的构造器过多,所以讲反复重用的代码封装成一个函数
     * 到时候直接调用即可,这就是面向对象中封装的思想。
     */
    private static void checkBounds(byte[] bytes, int offset, int length) {
        if (length < 0)
            throw new StringIndexOutOfBoundsException(length);
        if (offset < 0)
            throw new StringIndexOutOfBoundsException(offset);
        if (offset > bytes.length - length)
            throw new StringIndexOutOfBoundsException(offset + length);
    }
    /*
     * 
     */
    @Deprecated
    public String(byte ascii[], int hibyte) {
        this(ascii, hibyte, 0, ascii.length);
    }

    /*
     * @Deprecated注解表示此方法已经过时,但仍然可以使用,只是不推荐
     * ascii为要转换为字符的字节数组,hibyte为每个16位Unicode代码单元的前8位
     * java使用的是Unicode编码表,每个字符都占俩个字节,但是一个字节只是一个字节,所以此方法在转换的时候
     * 想指定另一个字节的数据来达到不同的需求,但是往往是不正确的转换,而使用指定charset完全可以达到此目的。
     */
    @Deprecated
    public String(byte ascii[], int hibyte, int offset, int count) {
        checkBounds(ascii, offset, count);
        char value[] = new char[count];

        if (hibyte == 0) {
            for (int i = count; i-- > 0;) {
                value[i] = (char) (ascii[i + offset] & 0xff);
            }
        } else {
            hibyte <<= 8;
            for (int i = count; i-- > 0;) {
                value[i] = (char) (hibyte | (ascii[i + offset] & 0xff));
            }
        }
        this.value = value;
    }

    /*
     * 下面的都是讲字节数组转换为字符串的构造器,也是方法的重载,为了适应不用的转换需求,不许参数为字节数组,
     * 可变参数为开始位置offset,转换长度length,指定编码方式charset,其中charset有俩种指定方式,一种是出入String类型的charsetName
     * 另一种是传入Charset类型的charset,如果不知道的话,offset默认为0,length默认为字节数组的长度。chatset默认为本地编码
     */
    public String(byte bytes[]) {
        this(bytes, 0, bytes.length);
    }

    /*
     * 
     */
    public String(byte bytes[], int offset, int length, String charsetName) throws UnsupportedEncodingException {
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(charsetName, bytes, offset, length);
    }

    /*
     * 
     */
    public String(byte bytes[], int offset, int length, Charset charset) {
        if (charset == null)
            throw new NullPointerException("charset");
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(charset, bytes, offset, length);
    }

    /*
     */
    public String(byte bytes[], String charsetName) throws UnsupportedEncodingException {
        this(bytes, 0, bytes.length, charsetName);
    }

    /*
     * 
     */
    public String(byte bytes[], Charset charset) {
        this(bytes, 0, bytes.length, charset);
    }

    /*
     * 将字节数组转换为字符串,指定开始位置和长度。
     */
    public String(byte bytes[], int offset, int length) {
        checkBounds(bytes, offset, length);
        this.value = StringCoding.decode(bytes, offset, length);
    }


    /*
     * 将字符串缓冲区对象转换成String对象,实际上就是将一个堆内存中的对象写入常量池中,
     * 使用Arrays.copyOf复制,得到的是俩份完全独立的数组
     * StringBuilder是线程不安全的,效率高
     * 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());
    }
}

 

转载于:https://www.cnblogs.com/gollong/p/9334747.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值