Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JDK-8302323 Add repeat methods to StringBuilder/StringBuffer #12728

Closed
wants to merge 14 commits into from
111 changes: 111 additions & 0 deletions src/java.base/share/classes/java/lang/AbstractStringBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1821,4 +1821,115 @@ private final void appendChars(CharSequence s, int off, int end) {
}
count += end - off;
}

/**
* Appends {@code count} copies of the character {@code c} to this sequence.
* <p>
* The length of this sequence increases by {@code count}.
*
* @param c character to append
* @param count number of times to copy
*
* @return a reference to this object.
*
* @since 21
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
* @throws IllegalArgumentException if {@code count} is less than zero
* @throws StringIndexOutOfBoundsException if the result overflows the buffer
*/
public AbstractStringBuilder repeat(char c, int count) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
if (count < 0) {
throw new IllegalArgumentException("count is less than zero: " + count);
}
if (count == 0) {
return this;
}
ensureCapacityInternal(this.count + count);
if (isLatin1() && StringLatin1.canEncode(c)) {
int index = this.count;
while (count-- != 0) {
value[index++] = (byte) c;
}
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
this.count = index;
} else {
if (isLatin1()) {
inflate();
}
int index = this.count;
while (count-- != 0) {
StringUTF16.putCharSB(value, index++, c);
}
this.count = index;
}
return this;
}

private AbstractStringBuilder repeatNull(int count) {
if (count < 0) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException("count is less than zero: " + count);
} else if (count == 0) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
return this;
}
int offset = this.count;
appendNull();
int length = this.count - offset;
int valueLength = length << coder;
if ((Integer.MAX_VALUE - offset) / count < valueLength) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
throw new OutOfMemoryError("Required length exceeds implementation limit");
}
int limit = count * length;
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
ensureCapacityInternal(offset + limit);
String.repeatCopyRest(value, offset << coder, limit << coder, length << coder);
this.count = offset + limit;
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.
*
* @param cs a {@code CharSequence}
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
* @param count number of times to copy
*
* @return a reference to this object.
*
* @since 21
* @throws IllegalArgumentException if {@code count} is less than zero
* @throws StringIndexOutOfBoundsException if the result overflows the buffer
*/
public AbstractStringBuilder repeat(CharSequence cs, int count) {
if (cs == null) {
return repeatNull(count);
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
} else if (count < 0) {
throw new IllegalArgumentException("count is less than zero: " + count);
} else if (count == 0) {
return this;
} else if (count == 1) {
return append(cs);
}

int length = cs.length();
if (length == 1) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
return repeat(cs.charAt(0), count);
}
int valueLength = length << UTF16;
if ((Integer.MAX_VALUE - this.count) / count < valueLength) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
throw new OutOfMemoryError("Required length exceeds implementation limit");
}
int limit = count * length;
int offset = this.count;
ensureCapacityInternal(offset + limit);
if (cs instanceof String str) {
putStringAt(offset, str);
} else if (cs instanceof AbstractStringBuilder asb) {
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
append(asb);
} else {
appendChars(cs, 0, length);
}
String.repeatCopyRest(value, offset << coder, limit << coder, length << coder);
this.count = offset + limit;
return this;
}
}
30 changes: 26 additions & 4 deletions src/java.base/share/classes/java/lang/String.java
Original file line number Diff line number Diff line change
Expand Up @@ -4453,12 +4453,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
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
* 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);
}

////////////////////////////////////////////////////////////////
Expand Down
22 changes: 22 additions & 0 deletions src/java.base/share/classes/java/lang/StringBuffer.java
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,28 @@ public synchronized StringBuffer reverse() {
return this;
}

/**
* @since 21
* @throws IllegalArgumentException {@inheritDoc}
JimLaskey marked this conversation as resolved.
Show resolved Hide resolved
* @throws StringIndexOutOfBoundsException {@inheritDoc}
*/
@Override
public synchronized StringBuffer repeat(char c, int count) {
super.repeat(c, count);
return this;
}

/**
* @since 21
* @throws IllegalArgumentException {@inheritDoc}
* @throws StringIndexOutOfBoundsException {@inheritDoc}
*/
@Override
public synchronized StringBuffer repeat(CharSequence cs, int count) {
super.repeat(cs, count);
return this;
}

@Override
@IntrinsicCandidate
public synchronized String toString() {
Expand Down
22 changes: 22 additions & 0 deletions src/java.base/share/classes/java/lang/StringBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,28 @@ public StringBuilder reverse() {
return this;
}

/**
* @since 21
* @throws IllegalArgumentException {@inheritDoc}
* @throws StringIndexOutOfBoundsException {@inheritDoc}
*/
@Override
public StringBuilder repeat(char c, int count) {
super.repeat(c, count);
return this;
}

/**
* @since 21
* @throws IllegalArgumentException {@inheritDoc}
* @throws StringIndexOutOfBoundsException {@inheritDoc}
*/
@Override
public StringBuilder repeat(CharSequence cs, int count) {
super.repeat(cs, count);
return this;
}

@Override
@IntrinsicCandidate
public String toString() {
Expand Down
159 changes: 159 additions & 0 deletions test/jdk/java/lang/StringBuilder/Repeat.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* 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 Repeat
* @run testng/othervm -XX:+CompactStrings Repeat
*/
@Test
public class Repeat {
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());
}

public void exceptions() {
StringBuilder sb = new StringBuilder();

try {
sb.repeat(' ', Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | StringIndexOutOfBoundsException ex) {
// Okay
}

try {
sb.repeat(" ", Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | StringIndexOutOfBoundsException ex) {
// Okay
}

try {
sb.repeat(MYCHARS, Integer.MAX_VALUE);
throw new RuntimeException("No OutOfMemoryError thrown");
} catch (OutOfMemoryError | StringIndexOutOfBoundsException ex) {
// Okay
}

try {
sb.repeat(' ', -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException | StringIndexOutOfBoundsException ex) {
// Okay
}

try {
sb.repeat("abc", -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException | StringIndexOutOfBoundsException ex) {
// Okay
}

try {
sb.repeat(MYCHARS, -1);
throw new RuntimeException("No IllegalArgumentException thrown");
} catch (IllegalArgumentException | StringIndexOutOfBoundsException ex) {
// Okay
}

}
}