【Java编程的逻辑】 String & StringBuilder & Arrays

String

String类内部用一个字符数组表示字符串,实例变量定义为:

private final char value[];

String中的大部分方法内部都是操作的这个字符数组,比如:

  • length():返回这个数组的长度
  • substring():根据参数,调用构造方法public String(char value[], int offset, int count)新建一个字符串
  • indexOf():查找字符或者子字符串时是在这个数组中进行查找

与包装类类似,String类也是不可变类,即对象一旦创建,就没法修改了。String类声明为final,不能被继承,内部char数组也是final的,初始化之后就不能再改变了。

String类中提供了很多看似修改的方法,其实是通过创建新的String对象来实现的,原来的String对象不会被修改啊

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

与包装类类似,定义为不可变类,程序可以更为简单、安全、容易理解。但如果频繁修改字符串,而每次修改都新建一个字符串,那么性能太低,这时可以考虑StringBuilder和StringBuffer

常量字符串

Java中的字符串常量是非常特殊的,除了可以直接赋值给String变量外,它自己就像一个String类型的对象,可以直接调用String的各种方法。比如

System.out.println("哈哈".length());
System.out.println("你好".contains("?不好"));

实际上,这些常量就是String类型的对象,在内存中,它们被放在一个共享的地方,这个地方称为字符串常量池,它保存所有的常量字符串,每个常量只会保存一份,被所有使用者共享。
当通过常量的形式使用一个字符串的时候,使用的就是常量池中的那个对应的String类型的对象。

String name1 = "Hello";
String name2 = "Hello";
System.out.println(name1 == name2);

输出true。
可以认为,”Hello”在常量池中有一个对应的String类型对象,变量name1和name2都是指向这个String类型对象的

如果不是通过常量直接赋值,而是通过new创建,==就不会返回true了

String name1 = new String("Hello");
String name2 = new String("Hello");
System.out.println(name1 == name2);
System.out.println(name1.equals(name2));

分别输出 false 和 true
因为name1和name2分别指向了不同的对象了,只是这两个对象内部的值是一样的

StringBuilder

StringBuilder也封装了一个字符数组。与String不同,StringBuilder不是final的,可以修改。但是字符数组中不一定所有位置都已经被使用,它有一个实例变量,表示数组中已经使用的字符个数。

既然StringBuilder是可以变的,那么我们通过两个方法来看看其中的原理

public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}

append会直接复制字符到内部的字符数组中,如果字符数组长度不够,会进行扩展,实际使用的长度count提现。
具体来说,ensureCapacityInternal(count + len)会确保数组的长度足以容纳新添加的字符,str.getChars会赋值新添加的字符到字符数组中,count += len会保存实际使用的长度。

我们再来看看是如何确保数组长度足以容纳新添加的字符

/**
 * @param minimumCapacity 需要的长度
 */
private void ensureCapacityInternal(int minimumCapacity) {
    // 1.当需要的长度大于当前字符数组的长度时,就进行扩容
    if (minimumCapacity - value.length > 0) {
        // 2.扩展的逻辑是:分配一个足够长度的数组,然后将原内容赋值到这个新数组中
        value = Arrays.copyOf(value,
                newCapacity(minimumCapacity));
    }
}

private int newCapacity(int minCapacity) {
    // 3.扩展策略:当前长度 * 2 + 2
    int newCapacity = (value.length << 1) + 2;
    // 如果通过3的方式长度还不够,那么就改为加需要的长度
    if (newCapacity - minCapacity < 0) {
        newCapacity = minCapacity;
    }
    // 简单的容错处理
    return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
        ? hugeCapacity(minCapacity)
        : newCapacity;
}

具体的步骤就如注释中备注的那样。我们重点关注一下扩容的策略,为什么不直接加上需要的长度呢?而是要先用当前数组的长度 * 2 再加上2呢? 这是一种折中策略,一方面要减少内存分配的次数,另一方面要避免空间的浪费。在不知道最终需要多长的情况下,指数扩展是一种常见的策略,广泛应用于各种内存分配相关的计算机程序中。

StringBuilder还有很多其他方法,这里我们再来看一个插入的方法:在指定索引offset处插入字符串str

public AbstractStringBuilder insert(int offset, String str) {
    if ((offset < 0) || (offset > length()))
        throw new StringIndexOutOfBoundsException(offset);
    if (str == null)
        str = "null";
    int len = str.length();
    ensureCapacityInternal(count + len);
    System.arraycopy(value, offset, value, offset + len, count - offset);
    str.getChars(value, offset);
    count += len;
    return this;
}

实现思路:在确保有足够长度后,首先将原数组中offset开始的内容向后挪动n个位置,n为待插入字符串的长度,然后将待插入字符串赋值进offset位置。
挪动位置调用了System.arraycopy()

public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

将数组src中srcPos开始的length个元素复制到数组dest中destPos中。

int[] src = new int[]{1, 2, 3, 4};
int[] dest = new int[]{5, 6, 7, 8, 9, 10};
System.arraycopy(src, 1, dest, 2, 2);
for (int i = 0; i < dest.length; i++) {
    System.out.print(dest[i] + " ");    
}   

输出

5, 6, 2, 3, 9, 10

Arrays

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值