diff --git a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java index 3a303121aa85a..87ac55b274ad0 100644 --- a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java +++ b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java @@ -29,6 +29,7 @@ import jdk.internal.math.FloatToDecimal; import java.io.IOException; +import java.nio.CharBuffer; import java.util.Arrays; import java.util.Spliterator; import java.util.stream.IntStream; @@ -1821,4 +1822,115 @@ private final void appendChars(CharSequence s, int off, int end) { } 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. + *

+ * The length of this sequence increases by {@code count} times the + * string representation length. + *

+ * 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. + *

+ * The length of this sequence increases by {@code count} times the + * {@code CharSequence} length. + *

+ * 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; + } } diff --git a/src/java.base/share/classes/java/lang/String.java b/src/java.base/share/classes/java/lang/String.java index ecebb77dd59e5..77075f64a3d8c 100644 --- a/src/java.base/share/classes/java/lang/String.java +++ b/src/java.base/share/classes/java/lang/String.java @@ -4522,12 +4522,34 @@ public String repeat(int count) { final int limit = len * count; final byte[] multiple = new byte[limit]; System.arraycopy(value, 0, multiple, 0, len); - int copied = len; + repeatCopyRest(multiple, 0, limit, len); + 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) { - System.arraycopy(multiple, 0, multiple, copied, copied); + // Power of two duplicate. + System.arraycopy(buffer, offset, buffer, offset + copied, copied); } - System.arraycopy(multiple, 0, multiple, copied, limit - copied); - return new String(multiple, coder); + // Duplicate remainder. + System.arraycopy(buffer, offset, buffer, offset + copied, limit - copied); } //////////////////////////////////////////////////////////////// diff --git a/src/java.base/share/classes/java/lang/StringBuffer.java b/src/java.base/share/classes/java/lang/StringBuffer.java index b0ee2a5c2e3dc..959dd01fe5c84 100644 --- a/src/java.base/share/classes/java/lang/StringBuffer.java +++ b/src/java.base/share/classes/java/lang/StringBuffer.java @@ -708,6 +708,28 @@ public synchronized StringBuffer reverse() { 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 @IntrinsicCandidate public synchronized String toString() { diff --git a/src/java.base/share/classes/java/lang/StringBuilder.java b/src/java.base/share/classes/java/lang/StringBuilder.java index 17fd105ad3fd0..add50081c7709 100644 --- a/src/java.base/share/classes/java/lang/StringBuilder.java +++ b/src/java.base/share/classes/java/lang/StringBuilder.java @@ -446,6 +446,28 @@ public StringBuilder reverse() { 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 @IntrinsicCandidate public String toString() { diff --git a/test/jdk/java/lang/StringBuilder/StringBufferRepeat.java b/test/jdk/java/lang/StringBuilder/StringBufferRepeat.java new file mode 100644 index 0000000000000..2d1f3c64f60cc --- /dev/null +++ b/test/jdk/java/lang/StringBuilder/StringBufferRepeat.java @@ -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 + } + + } +} diff --git a/test/jdk/java/lang/StringBuilder/StringBuilderRepeat.java b/test/jdk/java/lang/StringBuilder/StringBuilderRepeat.java new file mode 100644 index 0000000000000..c191e85d7f172 --- /dev/null +++ b/test/jdk/java/lang/StringBuilder/StringBuilderRepeat.java @@ -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 + } + + } +}