From e97b3583c4629fd4dd539b5f4ac9486a593a18f6 Mon Sep 17 00:00:00 2001 From: jher235 Date: Thu, 18 Dec 2025 02:14:10 +0900 Subject: [PATCH 1/4] [LANG-1805] Fix inaccurate capacity calculation in join(boolean[]) The previous implementation of join(boolean[], ...) calculated the initial StringBuilder capacity based on 'array.length' instead of the actual number of elements to be joined ('endIndex - startIndex'). This caused excessive memory allocation when joining a small range of a large array (e.g., joining 5 elements from an array of 10,000). This commit fixes the calculation to use 'noOfItems', ensuring precise memory allocation. Signed-off-by: jher235 --- src/main/java/org/apache/commons/lang3/StringUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java index e7fd432cdc9..bae791a8eba 100644 --- a/src/main/java/org/apache/commons/lang3/StringUtils.java +++ b/src/main/java/org/apache/commons/lang3/StringUtils.java @@ -3857,10 +3857,11 @@ public static String join(final boolean[] array, final char delimiter, final int if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(array.length * 5 + array.length - 1); + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 5 + noOfItems - 1); for (int i = startIndex; i < endIndex; i++) { stringBuilder .append(array[i]) From b4857550a5c1a6f0b480bc41dd06193ecb8050e4 Mon Sep 17 00:00:00 2001 From: jher235 Date: Thu, 18 Dec 2025 02:35:32 +0900 Subject: [PATCH 2/4] [LANG-1805] Fix inaccurate capacity calculation in join(char[]) The previous implementation of join(char[], ...) calculated the initial StringBuilder capacity based on 'array.length' instead of the actual number of elements to be joined ('endIndex - startIndex'). This caused excessive memory allocation when joining a small range of a large array (e.g., joining 5 elements from an array of 10,000). This commit fixes the calculation to use 'noOfItems', ensuring precise memory allocation. Signed-off-by: jher235 --- src/main/java/org/apache/commons/lang3/StringUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java index bae791a8eba..4f2d22ef76d 100644 --- a/src/main/java/org/apache/commons/lang3/StringUtils.java +++ b/src/main/java/org/apache/commons/lang3/StringUtils.java @@ -4008,10 +4008,11 @@ public static String join(final char[] array, final char delimiter, final int st if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(array.length * 2 - 1); + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 2 - 1); for (int i = startIndex; i < endIndex; i++) { stringBuilder .append(array[i]) From 6f62ba97223837005e4e5ea5bcf2df4640e71127 Mon Sep 17 00:00:00 2001 From: jher235 Date: Thu, 18 Dec 2025 03:00:10 +0900 Subject: [PATCH 3/4] [LANG-1805] Optimize StringBuilder usage in join() for primitives - Refactor `join` methods for primitive types (char, byte, short, int, long, float, double) to improve performance. - Pre-allocate `StringBuilder` capacity based on the actual number of elements (`noOfItems`) instead of the default 16 chars or `array.length`. - Fix inaccurate capacity calculation in existing methods (char, byte, short) that incorrectly used `array.length`, causing memory waste for sub-arrays. - Eliminate the final `substring()` call by appending delimiters conditionally within the loop, reducing unnecessary String object allocation. Signed-off-by: jher235 --- .../org/apache/commons/lang3/StringUtils.java | 102 ++++++++++-------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/StringUtils.java b/src/main/java/org/apache/commons/lang3/StringUtils.java index 4f2d22ef76d..b3f2a63132f 100644 --- a/src/main/java/org/apache/commons/lang3/StringUtils.java +++ b/src/main/java/org/apache/commons/lang3/StringUtils.java @@ -3862,12 +3862,13 @@ public static String join(final boolean[] array, final char delimiter, final int return EMPTY; } final StringBuilder stringBuilder = new StringBuilder(noOfItems * 5 + noOfItems - 1); - for (int i = startIndex; i < endIndex; i++) { + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -3933,16 +3934,18 @@ public static String join(final byte[] array, final char delimiter, final int st if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(); - for (int i = startIndex; i < endIndex; i++) { + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 4); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -4013,12 +4016,13 @@ public static String join(final char[] array, final char delimiter, final int st return EMPTY; } final StringBuilder stringBuilder = new StringBuilder(noOfItems * 2 - 1); - for (int i = startIndex; i < endIndex; i++) { + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -4084,16 +4088,18 @@ public static String join(final double[] array, final char delimiter, final int if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(); - for (int i = startIndex; i < endIndex; i++) { + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 10); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -4159,16 +4165,18 @@ public static String join(final float[] array, final char delimiter, final int s if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(); - for (int i = startIndex; i < endIndex; i++) { + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 8); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -4234,16 +4242,18 @@ public static String join(final int[] array, final char delimiter, final int sta if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(); - for (int i = startIndex; i < endIndex; i++) { + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 4); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -4472,16 +4482,18 @@ public static String join(final long[] array, final char delimiter, final int st if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(); - for (int i = startIndex; i < endIndex; i++) { + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 10); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** @@ -4666,16 +4678,18 @@ public static String join(final short[] array, final char delimiter, final int s if (array == null) { return null; } - if (endIndex - startIndex <= 0) { + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { return EMPTY; } - final StringBuilder stringBuilder = new StringBuilder(); - for (int i = startIndex; i < endIndex; i++) { + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 6); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { stringBuilder - .append(array[i]) - .append(delimiter); + .append(delimiter) + .append(array[i]); } - return stringBuilder.substring(0, stringBuilder.length() - 1); + return stringBuilder.toString(); } /** From c798cb461cc1ea52a4d7e3e4a1568521a617f07d Mon Sep 17 00:00:00 2001 From: jher235 Date: Fri, 19 Dec 2025 01:46:20 +0900 Subject: [PATCH 4/4] [LANG-1805] Add JMH benchmark for StringUtils.join on primitive arrays Signed-off-by: jher235 --- .../lang3/StringUtilsJoinBenchmark.java | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 src/test/java/org/apache/commons/lang3/StringUtilsJoinBenchmark.java diff --git a/src/test/java/org/apache/commons/lang3/StringUtilsJoinBenchmark.java b/src/test/java/org/apache/commons/lang3/StringUtilsJoinBenchmark.java new file mode 100644 index 00000000000..f8be18ec59c --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/StringUtilsJoinBenchmark.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.lang3; + +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +public class StringUtilsJoinBenchmark { + + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @State(Scope.Thread) + @Fork(1) + @Warmup(iterations = 2, time = 1) + @Measurement(iterations = 3, time = 1) + public static class IntArrayBenchmark { + + @Param({ "10", "100", "1000", "10000" }) + private int size; + + private int[] intArray; + + @Setup + public void setup() { + intArray = new int[size]; + Random random = new Random(235); + for (int i = 0; i < size; i++) { + intArray[i] = random.nextInt(10000); + } + } + + @Benchmark + public String testJoinIntWithoutInitCapacity() { + return StringUtilsWithoutInitCapacity.join(intArray, ',', 0, intArray.length); + } + + @Benchmark + public String testJoinIntWithInitCapacity() { + return StringUtilsWithInitCapacity.join(intArray, ',', 0, intArray.length); + } + } + + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.NANOSECONDS) + @State(Scope.Thread) + @Fork(1) + @Warmup(iterations = 2, time = 1) + @Measurement(iterations = 3, time = 1) + public static class BooleanArrayBenchmark { + + private boolean[] boolArray; + + @Setup + public void setup() { + boolArray = new boolean[10000]; + for (int i = 0; i < boolArray.length; i++) { + boolArray[i] = (i % 2 == 0); + } + } + + @Benchmark + public String testJoinBooleanInitArrayLength() { + return StringUtilsWithoutInitCapacity.join(boolArray, ',', 0, 10); + } + + @Benchmark + public String testJoinBooleanInitNoOfItems() { + return StringUtilsWithInitCapacity.join(boolArray, ',', 0, 10); + } + } + + public static class StringUtilsWithInitCapacity { + + public static final String EMPTY = ""; + + public static String join(final boolean[] array, final char delimiter, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 5 + noOfItems - 1); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { + stringBuilder + .append(delimiter) + .append(array[i]); + } + return stringBuilder.toString(); + } + + public static String join(final int[] array, final char delimiter, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + final StringBuilder stringBuilder = new StringBuilder(noOfItems * 4); + stringBuilder.append(array[startIndex]); + for (int i = startIndex + 1; i < endIndex; i++) { + stringBuilder + .append(delimiter) + .append(array[i]); + } + return stringBuilder.toString(); + } + } + + public static class StringUtilsWithoutInitCapacity { + + public static final String EMPTY = ""; + + public static String join(final boolean[] array, final char delimiter, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + if (endIndex - startIndex <= 0) { + return EMPTY; + } + final StringBuilder stringBuilder = new StringBuilder(array.length * 5 + array.length - 1); + for (int i = startIndex; i < endIndex; i++) { + stringBuilder + .append(array[i]) + .append(delimiter); + } + return stringBuilder.substring(0, stringBuilder.length() - 1); + } + + public static String join(final int[] array, final char delimiter, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + if (endIndex - startIndex <= 0) { + return EMPTY; + } + final StringBuilder stringBuilder = new StringBuilder(); + for (int i = startIndex; i < endIndex; i++) { + stringBuilder + .append(array[i]) + .append(delimiter); + } + return stringBuilder.substring(0, stringBuilder.length() - 1); + } + } +} \ No newline at end of file