8302323: Add repeat methods to StringBuilder/StringBuffer

Reviewed-by: tvaleev, redestad
This commit is contained in:
Jim Laskey 2023-04-03 15:29:21 +00:00
parent dd7ca757a7
commit 9b9b5a7a5c
6 changed files with 571 additions and 5 deletions

View file

@ -29,6 +29,7 @@ import jdk.internal.math.DoubleToDecimal;
import jdk.internal.math.FloatToDecimal; import jdk.internal.math.FloatToDecimal;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Spliterator; import java.util.Spliterator;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@ -1821,4 +1822,115 @@ abstract sealed class AbstractStringBuilder implements Appendable, CharSequence
} }
count += end - off; count += end - off;
} }
private AbstractStringBuilder repeat(char c, int count) {
int limit = this.count + count;
ensureCapacityInternal(limit);
boolean isLatin1 = isLatin1();
if (isLatin1 && StringLatin1.canEncode(c)) {
Arrays.fill(value, this.count, limit, (byte)c);
} else {
if (isLatin1) {
inflate();
}
for (int index = this.count; index < limit; index++) {
StringUTF16.putCharSB(value, index, c);
}
}
this.count = limit;
return this;
}
/**
* Repeats {@code count} copies of the string representation of the
* {@code codePoint} argument to this sequence.
* <p>
* The length of this sequence increases by {@code count} times the
* string representation length.
* <p>
* It is usual to use {@code char} expressions for code points. For example:
* {@snippet lang="java":
* // insert 10 asterisks into the buffer
* sb.repeat('*', 10);
* }
*
* @param codePoint code point to append
* @param count number of times to copy
*
* @return a reference to this object.
*
* @throws IllegalArgumentException if the specified {@code codePoint}
* is not a valid Unicode code point or if {@code count} is negative.
*
* @since 21
*/
public AbstractStringBuilder repeat(int codePoint, int count) {
if (count < 0) {
throw new IllegalArgumentException("count is negative: " + count);
} else if (count == 0) {
return this;
}
if (Character.isBmpCodePoint(codePoint)) {
repeat((char)codePoint, count);
} else {
repeat(CharBuffer.wrap(Character.toChars(codePoint)), count);
}
return this;
}
/**
* Appends {@code count} copies of the specified {@code CharSequence} {@code cs}
* to this sequence.
* <p>
* The length of this sequence increases by {@code count} times the
* {@code CharSequence} length.
* <p>
* If {@code cs} is {@code null}, then the four characters
* {@code "null"} are repeated into this sequence.
*
* @param cs a {@code CharSequence}
* @param count number of times to copy
*
* @return a reference to this object.
*
* @throws IllegalArgumentException if {@code count} is negative
*
* @since 21
*/
public AbstractStringBuilder repeat(CharSequence cs, int count) {
if (count < 0) {
throw new IllegalArgumentException("count is negative: " + count);
} else if (count == 0) {
return this;
} else if (count == 1) {
return append(cs);
}
if (cs == null) {
cs = "null";
}
int length = cs.length();
if (length == 0) {
return this;
} else if (length == 1) {
return repeat(cs.charAt(0), count);
}
int offset = this.count;
int valueLength = length << coder;
if ((Integer.MAX_VALUE - offset) / count < valueLength) {
throw new OutOfMemoryError("Required length exceeds implementation limit");
}
int total = count * length;
int limit = offset + total;
ensureCapacityInternal(limit);
if (cs instanceof String str) {
putStringAt(offset, str);
} else if (cs instanceof AbstractStringBuilder asb) {
append(asb);
} else {
appendChars(cs, 0, length);
}
String.repeatCopyRest(value, offset << coder, total << coder, length << coder);
this.count = limit;
return this;
}
} }

View file

@ -4563,14 +4563,36 @@ public final class String
final int limit = len * count; final int limit = len * count;
final byte[] multiple = new byte[limit]; final byte[] multiple = new byte[limit];
System.arraycopy(value, 0, multiple, 0, len); System.arraycopy(value, 0, multiple, 0, len);
int copied = len; repeatCopyRest(multiple, 0, limit, len);
for (; copied < limit - copied; copied <<= 1) {
System.arraycopy(multiple, 0, multiple, copied, copied);
}
System.arraycopy(multiple, 0, multiple, copied, limit - copied);
return new String(multiple, coder); return new String(multiple, coder);
} }
/**
* Used to perform copying after the initial insertion. Copying is optimized
* by using power of two duplication. First pass duplicates original copy,
* second pass then duplicates the original and the copy yielding four copies,
* third pass duplicates four copies yielding eight copies, and so on.
* Finally, the remainder is filled in with prior copies.
*
* @implNote The technique used here is significantly faster than hand-rolled
* loops or special casing small numbers due to the intensive optimization
* done by intrinsic {@code System.arraycopy}.
*
* @param buffer destination buffer
* @param offset offset in the destination buffer
* @param limit total replicated including what is already in the buffer
* @param copied number of bytes that have already in the buffer
*/
static void repeatCopyRest(byte[] buffer, int offset, int limit, int copied) {
// Initial copy is in the buffer.
for (; copied < limit - copied; copied <<= 1) {
// Power of two duplicate.
System.arraycopy(buffer, offset, buffer, offset + copied, copied);
}
// Duplicate remainder.
System.arraycopy(buffer, offset, buffer, offset + copied, limit - copied);
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
/** /**

View file

@ -708,6 +708,28 @@ import jdk.internal.vm.annotation.IntrinsicCandidate;
return this; return this;
} }
/**
* @throws IllegalArgumentException {@inheritDoc}
*
* @since 21
*/
@Override
public synchronized StringBuffer repeat(int codePoint, int count) {
super.repeat(codePoint, count);
return this;
}
/**
* @throws IllegalArgumentException {@inheritDoc}
*
* @since 21
*/
@Override
public synchronized StringBuffer repeat(CharSequence cs, int count) {
super.repeat(cs, count);
return this;
}
@Override @Override
@IntrinsicCandidate @IntrinsicCandidate
public synchronized String toString() { public synchronized String toString() {

View file

@ -446,6 +446,28 @@ public final class StringBuilder
return this; return this;
} }
/**
* @throws IllegalArgumentException {@inheritDoc}
*
* @since 21
*/
@Override
public StringBuilder repeat(int codePoint, int count) {
super.repeat(codePoint, count);
return this;
}
/**
* @throws IllegalArgumentException {@inheritDoc}
*
* @since 21
*/
@Override
public StringBuilder repeat(CharSequence cs, int count) {
super.repeat(cs, count);
return this;
}
@Override @Override
@IntrinsicCandidate @IntrinsicCandidate
public String toString() { public String toString() {

View file

@ -0,0 +1,194 @@
/*
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import java.util.Arrays;
/**
* @test
* @bug 8302323
* @summary Test StringBuffer.repeat sanity tests
* @run testng/othervm -XX:-CompactStrings StringBufferRepeat
* @run testng/othervm -XX:+CompactStrings StringBufferRepeat
*/
@Test
public class StringBufferRepeat {
private static class MyChars implements CharSequence {
private static final char[] DATA = new char[] { 'a', 'b', 'c' };
@Override
public int length() {
return DATA.length;
}
@Override
public char charAt(int index) {
return DATA[index];
}
@Override
public CharSequence subSequence(int start, int end) {
return new String(Arrays.copyOfRange(DATA, start, end));
}
}
private static final MyChars MYCHARS = new MyChars();
public void sanity() {
StringBuffer sb = new StringBuffer();
// prime the StringBuffer
sb.append("repeat");
// single character Latin1
sb.repeat('1', 0);
sb.repeat('2', 1);
sb.repeat('3', 5);
// single string Latin1 (optimized)
sb.repeat("1", 0);
sb.repeat("2", 1);
sb.repeat("3", 5);
// multi string Latin1
sb.repeat("-1", 0);
sb.repeat("-2", 1);
sb.repeat("-3", 5);
// single character UTF16
sb.repeat('\u2460', 0);
sb.repeat('\u2461', 1);
sb.repeat('\u2462', 5);
// single string UTF16 (optimized)
sb.repeat("\u2460", 0);
sb.repeat("\u2461", 1);
sb.repeat("\u2462", 5);
// multi string UTF16
sb.repeat("-\u2460", 0);
sb.repeat("-\u2461", 1);
sb.repeat("-\u2462", 5);
// CharSequence
sb.repeat(MYCHARS, 3);
// null
sb.repeat((String)null, 0);
sb.repeat((String)null, 1);
sb.repeat((String)null, 5);
sb.repeat((CharSequence)null, 0);
sb.repeat((CharSequence)null, 1);
sb.repeat((CharSequence)null, 5);
String expected = "repeat233333233333-2-3-3-3-3-3\u2461\u2462\u2462\u2462\u2462\u2462\u2461\u2462\u2462\u2462\u2462\u2462-\u2461-\u2462-\u2462-\u2462-\u2462-\u2462abcabcabc" +
"nullnullnullnullnullnullnullnullnullnullnullnull";
assertEquals(expected, sb.toString());
// Codepoints
sb.setLength(0);
sb.repeat(0, 0);
sb.repeat(0, 1);
sb.repeat(0, 5);
sb.repeat((int)' ', 0);
sb.repeat((int)' ', 1);
sb.repeat((int)' ', 5);
sb.repeat(0x2460, 0);
sb.repeat(0x2461, 1);
sb.repeat(0x2462, 5);
sb.repeat(0x10FFFF, 0);
sb.repeat(0x10FFFF, 1);
sb.repeat(0x10FFFF, 5);
expected = "\u0000\u0000\u0000\u0000\u0000\u0000\u0020\u0020\u0020\u0020\u0020\u0020\u2461\u2462\u2462\u2462\u2462\u2462\udbff\udfff\udbff\udfff\udbff\udfff\udbff\udfff\udbff\udfff\udbff\udfff";
assertEquals(expected, sb.toString());
}
public void exceptions() {
StringBuffer sb = new StringBuffer();
try {
sb.repeat(' ', Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | IndexOutOfBoundsException ex) {
// Okay
}
try {
sb.repeat(" ", Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | IndexOutOfBoundsException ex) {
// Okay
}
try {
sb.repeat(MYCHARS, Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | IndexOutOfBoundsException ex) {
// Okay
}
try {
sb.repeat(' ', -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat("abc", -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat(MYCHARS, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat(0x10FFFF + 1, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat(-1, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
}
}

View file

@ -0,0 +1,194 @@
/*
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import java.util.Arrays;
/**
* @test
* @bug 8302323
* @summary Test StringBuilder.repeat sanity tests
* @run testng/othervm -XX:-CompactStrings StringBuilderRepeat
* @run testng/othervm -XX:+CompactStrings StringBuilderRepeat
*/
@Test
public class StringBuilderRepeat {
private static class MyChars implements CharSequence {
private static final char[] DATA = new char[] { 'a', 'b', 'c' };
@Override
public int length() {
return DATA.length;
}
@Override
public char charAt(int index) {
return DATA[index];
}
@Override
public CharSequence subSequence(int start, int end) {
return new String(Arrays.copyOfRange(DATA, start, end));
}
}
private static final MyChars MYCHARS = new MyChars();
public void sanity() {
StringBuilder sb = new StringBuilder();
// prime the StringBuilder
sb.append("repeat");
// single character Latin1
sb.repeat('1', 0);
sb.repeat('2', 1);
sb.repeat('3', 5);
// single string Latin1 (optimized)
sb.repeat("1", 0);
sb.repeat("2", 1);
sb.repeat("3", 5);
// multi string Latin1
sb.repeat("-1", 0);
sb.repeat("-2", 1);
sb.repeat("-3", 5);
// single character UTF16
sb.repeat('\u2460', 0);
sb.repeat('\u2461', 1);
sb.repeat('\u2462', 5);
// single string UTF16 (optimized)
sb.repeat("\u2460", 0);
sb.repeat("\u2461", 1);
sb.repeat("\u2462", 5);
// multi string UTF16
sb.repeat("-\u2460", 0);
sb.repeat("-\u2461", 1);
sb.repeat("-\u2462", 5);
// CharSequence
sb.repeat(MYCHARS, 3);
// null
sb.repeat((String)null, 0);
sb.repeat((String)null, 1);
sb.repeat((String)null, 5);
sb.repeat((CharSequence)null, 0);
sb.repeat((CharSequence)null, 1);
sb.repeat((CharSequence)null, 5);
String expected = "repeat233333233333-2-3-3-3-3-3\u2461\u2462\u2462\u2462\u2462\u2462\u2461\u2462\u2462\u2462\u2462\u2462-\u2461-\u2462-\u2462-\u2462-\u2462-\u2462abcabcabc" +
"nullnullnullnullnullnullnullnullnullnullnullnull";
assertEquals(expected, sb.toString());
// Codepoints
sb.setLength(0);
sb.repeat(0, 0);
sb.repeat(0, 1);
sb.repeat(0, 5);
sb.repeat((int)' ', 0);
sb.repeat((int)' ', 1);
sb.repeat((int)' ', 5);
sb.repeat(0x2460, 0);
sb.repeat(0x2461, 1);
sb.repeat(0x2462, 5);
sb.repeat(0x10FFFF, 0);
sb.repeat(0x10FFFF, 1);
sb.repeat(0x10FFFF, 5);
expected = "\u0000\u0000\u0000\u0000\u0000\u0000\u0020\u0020\u0020\u0020\u0020\u0020\u2461\u2462\u2462\u2462\u2462\u2462\udbff\udfff\udbff\udfff\udbff\udfff\udbff\udfff\udbff\udfff\udbff\udfff";
assertEquals(expected, sb.toString());
}
public void exceptions() {
StringBuilder sb = new StringBuilder();
try {
sb.repeat(' ', Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | IndexOutOfBoundsException ex) {
// Okay
}
try {
sb.repeat(" ", Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | IndexOutOfBoundsException ex) {
// Okay
}
try {
sb.repeat(MYCHARS, Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | IndexOutOfBoundsException ex) {
// Okay
}
try {
sb.repeat(' ', -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat("abc", -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat(MYCHARS, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat(0x10FFFF + 1, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
try {
sb.repeat(-1, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException ex) {
// Okay
}
}
}