死磕java-特好用的字符串拼接类StringJoiner
我们使用分割符进行字符串拼接的时候都是用StringBuffer或者StringBuilder类写字符串拼接的方法,每一次拼接好的对象转化成字符串的时候都需要去掉最后一个分割符;或者判断是否为首次拼接来加分割符。JDK8之后加了一个StringJoiner类,我们在拼接字符串的时候不需要这么麻烦了,使用这个类就可以了。
1. StringJoiner类基本使用
我们可以构造由定界符分隔的字符序列,并可选地以提供的前缀开头并以提供的后缀结尾。然后调用add方法添加字符串。例子代码如下:
代码如下:
StringJoiner stringJoiner = new StringJoiner(",", "<", ">"); // 添加null字符串 stringJoiner.add(null); stringJoiner.add("1111"); System.out.println(stringJoiner.toString()); StringJoiner stringJoiner2 = new StringJoiner("|", "(", ")"); // 添加null字符串 stringJoiner2.add("aaaa"); stringJoiner2.add("bbbb"); System.out.println(stringJoiner.merge(stringJoiner2)); 结果如下: <null,1111> <null,1111,aaaa|bbbb>
JDK8的stream流中也是用了这个类,代码如下:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4); String commaSeparatedNumbers = numbers.stream() .map(i -> i.toString()) .collect(Collectors.joining(", "));
2. StringJoiner源码解析
(1)未添加元素的时候,toString()方法返回值是什么?
默认情况下,当尚未添加任何元素时,也就是value未进行初始化的时候,由toString()返回的由前缀+后缀组成的字符串或value的属性。其实我们尚未添加任何元素时,返回的属性是emptyValue,由前缀+后缀组成的字符串。当我们添加元素的时候,返回的属性是value。两者的类型不同,emptyValue是String类型,value是StringBuilder类型。源码如下:
// StringBuilder值-在任何时候,字符都是由前缀构成的,添加的元素由定界符分隔, // 但没有后缀,因此我们可以更轻松地添加元素,而不必每次都摇晃后缀。 private StringBuilder value; // 默认情况下,当尚未添加任何元素(即,如果为空)时,由toString()返回的 // 由前缀+后缀组成的字符串或value的属性。用户可能会将其覆盖为其他值,包括空String。 private String emptyValue;
注意:属性value都是由前缀构成的和添加的元素由定界符分隔,但没有后缀,因此我们可以更轻松地添加元素,而不必每次都摇晃后缀。
toString()源码如下:
// 添加元素为空,返回前后缀拼接字符串 @Override public String toString() { // 添加元素为空,返回前后缀拼接字符串 if (value == null) { return emptyValue; } else { // 判断后缀是否为空 if (suffix.equals("")) { return value.toString(); } else { int initialLength = value.length(); String result = value.append(suffix).toString(); // reset value to pre-append initialLength // 重置值以预先添加initialLength, 为了后面继续操作 value.setLength(initialLength); return result; } } }
(2)add的元素为null的时候,会怎样添加元素?
如果传过来的参数为null的时候,它不会做空校验,它会把null转化成字符串添加进去。
StringJoiner的add方法源码如下:
/** * 注意: * 1.参数类型是CharSequence接口,String、StringBuilder、StringBuffer等 * 继承了这个接口 * 2.传过来的参数为null的时候,它不会做空校验,它会把null转化成字符串添加进去 */ public StringJoiner add(CharSequence newElement) { prepareBuilder().append(newElement); return this; }
注意:add方法的参数类型是CharSequence接口,String、StringBuilder、StringBuffer等继承了这个接口。
其实底层调用了AbstractStringBuilder类的append方法,源码如下:
// 这里重写了Appendable的类 @Override public AbstractStringBuilder append(CharSequence s) { // s为null的时候,添加null字符串 if (s == null) return appendNull(); // String类型时 if (s instanceof String) return this.append((String)s); // AbstractStringBuilder类型时,其子类就两个: // StringBuffer和StringBuilder if (s instanceof AbstractStringBuilder) return this.append((AbstractStringBuilder)s); // 继承CharSequence接口,除了上面类型的其他类型操作 return this.append(s, 0, s.length()); }
关于StringJoiner类的使用和源码解析讲的也差不多了,这个类不复杂,各位读者一看就明白了,下面我们拓展一些知识点。
知识拓展:
1. StringBuffer和StringBuilder继承了AbstractStringBuilder类,StringJoiner的add方法会调用StringBuffer的扩容方法ensureCapacityInternal()。
(1)Arrays.copyOf方法为浅拷贝,为什么这里需要用浅拷贝?
因为扩容的时候把原来的数据都copy过来了,如果存取是对象的引用的话,那么修改原来的对象的属性值时就会发生变化,如果是深拷贝的话copy的是属性值而不是对象的引用,我们修改数组上某个对象的属性就不会改变了,java底层好多调用了copy方法,这就是为什么java的拷贝是浅拷贝了,根深蒂固的原因。举例说明:
Department[] dept = new Department[2]; dept[0] = d1; Department[] departments = Arrays.copyOf(dept, dept.length); System.out.println("======== 改变前 ========="); System.out.println(dept[0].getId()); System.out.println(departments[0].getId()); System.out.println("======== 改变后 ========="); d1.setId(2); System.out.println(dept[0].getId()); System.out.println(departments[0].getId()); 这里浅拷贝和深拷贝结果就不一样了。
ensureCapacityInternal()源码如下:
// 扩容 private void ensureCapacityInternal(int minimumCapacity) { // 正数的时候 // overflow-conscious code if (minimumCapacity - value.length > 0) { // 扩容, minimumCapacity为正数, // newCapacity()就不需要校验minimumCapacity为非负数了 value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
- ensureCapacityInternal()方法中调用了newCapacity(minimumCapacity)方法,这个方法的运行机制为机制:①先在数组原来大小的基础上扩容为原来的2倍加2为newCapacity,如果newCapacity大于传过来的参数,就以此进行下一步操作,否则就把传来的参数minCapacity赋值给newCapacity。②判断newCapacity是否小于等于0或者比MAX_ARRAY_SIZE还大,则容量为巨容,否则为newCapacity大小。
/** * 返回至少等于给定最小容量的容量。 如果足够,则返回增加了相同数量+ 2的当前容量。 * 不会返回大于{@code MAX_ARRAY_SIZE} 的容量,除非给定的最小容量大于该容量。 */ private int newCapacity(int minCapacity) { // 在原来的基础上扩容两倍加2 // 有溢出意识的代码 // overflow-conscious code int newCapacity = (value.length << 1) + 2; // 扩容容量小于传过来的参数的时候 if (newCapacity - minCapacity < 0) { // 参数赋值 newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; }
(1)为什么MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8?
数组在java里是一种特殊类型,既不是基本数据类型(开玩笑,当然不是)也不是引用数据类型。有别于普通的“类的实例”对象,java里数组不是类,所以也就没有对应的class文件,数组类型是由jvm从元素类型合成出来的;在jvm中获取数组的长度是用arraylength这个专门的字节码指令的;在数组的对象头里有一个_length字段,记录数组长度,只需要去读_length字段就可以了。所以ArrayList中定义的最大长度为Integer最大值减8,这个8就是就是存了数组_length字段。
原文链接:blog.csdn.net/alexdedream…
具体的对象头内容,可以参考 www.2cto.com/kf/201603/4… 这篇博客
源码如下:
/** * 要分配的数组的最大大小(除非必要)。 一些虚拟机在数组中保留一些头信息。 * 尝试分配更大的阵列可能会导致 OutOfMemoryError:请求的阵列大小超出VM限制 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
为了方便喜欢钻研者随时随地都可以读到技术文章,我创建了一个公众号,后续文章会在公众号上面及时更新,下面是我的公众号,读者可以关注: