From 81557f01875e3158524105f9edca4081f9c4f3e4 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Sat, 23 Aug 2025 10:47:59 +0800 Subject: [PATCH 1/5] Introduce appendPair method for efficient two-digit integer appending\n\nThis change adds a new internal API to efficiently append two-digit integers\n(00-99) to a StringBuilder. It includes:\n- AbstractStringBuilder.appendPair(int i): The core implementation.\n- JavaLangAccess.appendPair(StringBuilder, int): For internal access.\n- System.JavaLangAccessImpl.appendPair(StringBuilder, int): Bridge to AbstractStringBuilder.\n- DecimalDigits.appendPair(StringBuilder, int): Public static utility method.\n\nImproved Javadoc comments for clarity and consistency across all new methods. Co-authored-by: Qwen-Coder --- .../java/lang/AbstractStringBuilder.java | 25 +++++++++++++++++++ .../share/classes/java/lang/System.java | 4 +++ .../jdk/internal/access/JavaLangAccess.java | 16 ++++++++++++ .../jdk/internal/util/DecimalDigits.java | 19 ++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java index d317557cbb19c..edca90f439695 100644 --- a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java +++ b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java @@ -906,6 +906,31 @@ public AbstractStringBuilder append(char c) { return this; } + /** + * Appends the two-digit string representation of the {@code int} + * argument to this sequence. + *

+ * The integer {@code i} is formatted as two decimal digits. + * If the value is between 0 and 9, it is formatted with a leading zero + * (e.g., 5 becomes "05"). If the value is outside the range 0-99, + * the behavior is unspecified and may result in unexpected output. + * + * @param i an {@code int} (should be between 0 and 99 inclusive). + * @throws IndexOutOfBoundsException if {@code i} is outside the range 0-99. + */ + void appendPair(int i) { + byte coder = this.coder; + int count = this.count; + byte[] value = ensureCapacitySameCoder(this.value, coder, count + 2); + if (isLatin1(coder)) { + DecimalDigits.uncheckedPutPairLatin1(value, count, i); + } else { + DecimalDigits.uncheckedPutPairUTF16(value, count, i); + } + this.value = value; + this.count = count + 2; + } + /** * Appends the string representation of the {@code int} * argument to this sequence. diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index a40c27bbf472a..b466ef72df769 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -2149,6 +2149,10 @@ public byte[] getBytesUTF8NoRepl(String s) { return String.getBytesUTF8NoRepl(s); } + public void appendPair(StringBuilder buf, int v) { + buf.appendPair(v); + } + public void inflateBytesToChars(byte[] src, int srcOff, char[] dst, int dstOff, int len) { StringLatin1.inflate(src, srcOff, dst, dstOff, len); } diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index aa5b6e438f549..1161f85e3795f 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -386,6 +386,22 @@ public interface JavaLangAccess { */ void uncheckedPutCharUTF16(byte[] bytes, int index, int ch); + /** + * Appends the two-digit string representation of the {@code int} + * argument to the given {@code StringBuilder}. + *

+ * This method is intended for internal use by {@code DecimalDigits.appendPair}. + * The integer {@code v} is formatted as two decimal digits. + * If the value is between 0 and 9, it is formatted with a leading zero + * (e.g., 5 becomes "05"). If the value is outside the range 0-99, + * the behavior is unspecified. + * + * @param buf the {@code StringBuilder} to append to. + * @param v the {@code int} value (should be between 0 and 99 inclusive). + * @see jdk.internal.util.DecimalDigits#appendPair(StringBuilder, int) + */ + void appendPair(StringBuilder buf, int v); + /** * Encode the given string into a sequence of bytes using utf8. * diff --git a/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java b/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java index 6c0c745651e34..785b38ca9207a 100644 --- a/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java +++ b/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java @@ -25,6 +25,7 @@ package jdk.internal.util; +import jdk.internal.access.SharedSecrets; import jdk.internal.misc.Unsafe; import jdk.internal.vm.annotation.Stable; @@ -443,4 +444,22 @@ private static void uncheckedPutCharUTF16(byte[] buf, int charPos, int c) { assert charPos >= 0 && charPos < (buf.length >> 1); UNSAFE.putCharUnaligned(buf, ARRAY_BYTE_BASE_OFFSET + ((long) charPos << 1), (char) c); } + + /** + * Appends the two-digit string representation of the {@code int} + * argument to the given {@code StringBuilder}. + *

+ * The integer {@code v} is formatted as two decimal digits. + * If the value is between 0 and 9, it is formatted with a leading zero + * (e.g., 5 becomes "05"). If the value is outside the range 0-99, + * the behavior is unspecified. + * + * @param buf the {@code StringBuilder} to append to. + * @param v the {@code int} value (should be between 0 and 99 inclusive). + * @see jdk.internal.access.JavaLangAccess#appendPair(StringBuilder, int) + */ + public static void appendPair(StringBuilder buf, int v) { + SharedSecrets.getJavaLangAccess() + .appendPair(buf, v); + } } From 8d6ad46d90f2fa718af436660eb5e18c32ab6837 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Sat, 23 Aug 2025 10:54:45 +0800 Subject: [PATCH 2/5] Refactor DateTimeHelper to use DecimalDigits.appendPair for date/time formatting\n\nThis change updates DateTimeHelper.formatTo methods for LocalDate and LocalTime\nto use the new DecimalDigits.appendPair method for formatting month, day, hour,\nminute, and second components. This improves code clarity and leverages the\nnewly introduced efficient two-digit integer appending functionality. Co-authored-by: Qwen-Coder --- .../jdk/internal/util/DateTimeHelper.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java b/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java index bbb0b6738d189..7faabd283a61e 100644 --- a/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java +++ b/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java @@ -49,24 +49,24 @@ public static void formatTo(StringBuilder buf, LocalDateTime dateTime) { * Requires extra capacity of 10 to avoid StringBuilder reallocation. */ public static void formatTo(StringBuilder buf, LocalDate date) { - int year = date.getYear(), - month = date.getMonthValue(), - day = date.getDayOfMonth(); - int absYear = Math.abs(year); + int year = date.getYear(), + absYear = Math.abs(year); if (absYear < 1000) { if (year < 0) { buf.append('-'); } - buf.repeat('0', absYear < 10 ? 3 : absYear < 100 ? 2 : 1); - buf.append(absYear); + DecimalDigits.appendPair(buf, absYear / 100); + DecimalDigits.appendPair(buf, absYear % 100); } else { if (year > 9999) { buf.append('+'); } buf.append(year); } - buf.append(month < 10 ? "-0" : "-").append(month) - .append(day < 10 ? "-0" : "-").append(day); + buf.append('-'); + DecimalDigits.appendPair(buf, date.getMonthValue()); + buf.append('-'); + DecimalDigits.appendPair(buf, date.getDayOfMonth()); } /** @@ -74,14 +74,14 @@ public static void formatTo(StringBuilder buf, LocalDate date) { * Requires extra capacity of 18 to avoid StringBuilder reallocation. */ public static void formatTo(StringBuilder buf, LocalTime time) { - int hour = time.getHour(), - minute = time.getMinute(), - second = time.getSecond(), + DecimalDigits.appendPair(buf, time.getHour()); + buf.append(':'); + DecimalDigits.appendPair(buf, time.getMinute()); + int second = time.getSecond(), nano = time.getNano(); - buf.append(hour < 10 ? "0" : "").append(hour) - .append(minute < 10 ? ":0" : ":").append(minute); if ((second | nano) > 0) { - buf.append(second < 10 ? ":0" : ":").append(second); + buf.append(':'); + DecimalDigits.appendPair(buf, second); if (nano > 0) { buf.append('.'); int zeros = 9 - DecimalDigits.stringSize(nano); From c56f16d11ed179eb2a045bbaac8b7e6cbf7da92f Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Sat, 23 Aug 2025 17:25:53 +0800 Subject: [PATCH 3/5] Optimize year formatting in DateTimeHelper by reducing modulo operation Co-authored-by: Qwen-Coder Refactored the year formatting logic to use subtraction instead of modulo for calculating the lower two digits, which can be slightly more efficient. Added a comment to clarify the safety of the input range for DecimalDigits.appendPair. --- .../share/classes/jdk/internal/util/DateTimeHelper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java b/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java index 7faabd283a61e..356aee739f2d7 100644 --- a/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java +++ b/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java @@ -55,8 +55,9 @@ public static void formatTo(StringBuilder buf, LocalDate date) { if (year < 0) { buf.append('-'); } - DecimalDigits.appendPair(buf, absYear / 100); - DecimalDigits.appendPair(buf, absYear % 100); + int y01 = absYear / 100; + DecimalDigits.appendPair(buf, y01); + DecimalDigits.appendPair(buf, absYear - y01 * 100); } else { if (year > 9999) { buf.append('+'); From 916db357dc1ddd5dc613a7f2d8e41475642cada7 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Thu, 28 Aug 2025 09:49:55 +0800 Subject: [PATCH 4/5] Use DecimalDigits.appendPair for formatting in time classes This change modifies the toString() methods in MonthDay, YearMonth, ZoneOffset, and ChronoLocalDateImpl to use DecimalDigits.appendPair for formatting two-digit numbers. This provides a more efficient and consistent way to format these values. Also added a comment in ChronoLocalDateImpl.toString() to explain why get() is used instead of getLong() for performance reasons, as the values are guaranteed to be within the int range for all chronologies. Co-authored-by: Qwen-Coder --- .../share/classes/java/time/MonthDay.java | 12 ++++++++---- .../share/classes/java/time/YearMonth.java | 8 +++++--- .../share/classes/java/time/ZoneOffset.java | 11 +++++++---- .../java/time/chrono/ChronoLocalDateImpl.java | 18 ++++++++++++------ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/java.base/share/classes/java/time/MonthDay.java b/src/java.base/share/classes/java/time/MonthDay.java index 6244c14e6e126..f6db056101f5d 100644 --- a/src/java.base/share/classes/java/time/MonthDay.java +++ b/src/java.base/share/classes/java/time/MonthDay.java @@ -86,6 +86,8 @@ import java.time.temporal.ValueRange; import java.util.Objects; +import jdk.internal.util.DecimalDigits; + /** * A month-day in the ISO-8601 calendar system, such as {@code --12-03}. *

@@ -748,10 +750,12 @@ public int hashCode() { */ @Override public String toString() { - return new StringBuilder(10).append("--") - .append(month < 10 ? "0" : "").append(month) - .append(day < 10 ? "-0" : "-").append(day) - .toString(); + StringBuilder buf = new StringBuilder(10); + buf.append("--"); + DecimalDigits.appendPair(buf, month); + buf.append('-'); + DecimalDigits.appendPair(buf, day); + return buf.toString(); } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/YearMonth.java b/src/java.base/share/classes/java/time/YearMonth.java index b24151de3f022..65f444d35aba6 100644 --- a/src/java.base/share/classes/java/time/YearMonth.java +++ b/src/java.base/share/classes/java/time/YearMonth.java @@ -99,6 +99,8 @@ import java.time.temporal.ValueRange; import java.util.Objects; +import jdk.internal.util.DecimalDigits; + /** * A year-month in the ISO-8601 calendar system, such as {@code 2007-12}. *

@@ -1206,9 +1208,9 @@ public String toString() { } else { buf.append(year); } - return buf.append(month < 10 ? "-0" : "-") - .append(month) - .toString(); + buf.append('-'); + DecimalDigits.appendPair(buf, month); + return buf.toString(); } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/ZoneOffset.java b/src/java.base/share/classes/java/time/ZoneOffset.java index d93c6e2d46d0b..d98e36914d238 100644 --- a/src/java.base/share/classes/java/time/ZoneOffset.java +++ b/src/java.base/share/classes/java/time/ZoneOffset.java @@ -88,6 +88,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReferenceArray; +import jdk.internal.util.DecimalDigits; import jdk.internal.vm.annotation.Stable; /** @@ -465,12 +466,14 @@ private static String buildId(int totalSeconds) { StringBuilder buf = new StringBuilder(); int absHours = absTotalSeconds / SECONDS_PER_HOUR; int absMinutes = (absTotalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; - buf.append(totalSeconds < 0 ? "-" : "+") - .append(absHours < 10 ? "0" : "").append(absHours) - .append(absMinutes < 10 ? ":0" : ":").append(absMinutes); + buf.append(totalSeconds < 0 ? '-' : '+'); + DecimalDigits.appendPair(buf, absHours); + buf.append(':'); + DecimalDigits.appendPair(buf, absMinutes); int absSeconds = absTotalSeconds % SECONDS_PER_MINUTE; if (absSeconds != 0) { - buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds); + buf.append(':'); + DecimalDigits.appendPair(buf, absSeconds); } return buf.toString(); } diff --git a/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java b/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java index ca226b70d24be..67f08c5cb4f73 100644 --- a/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java +++ b/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java @@ -74,6 +74,8 @@ import java.time.temporal.ValueRange; import java.util.Objects; +import jdk.internal.util.DecimalDigits; + /** * A date expressed in terms of a standard year-month-day calendar system. *

@@ -426,18 +428,22 @@ public int hashCode() { @Override public String toString() { - // getLong() reduces chances of exceptions in toString() - long yoe = getLong(YEAR_OF_ERA); - long moy = getLong(MONTH_OF_YEAR); - long dom = getLong(DAY_OF_MONTH); + // Using get() instead of getLong() for performance reasons, + // as the values of YEAR_OF_ERA, MONTH_OF_YEAR, and DAY_OF_MONTH + // are guaranteed to be within the int range for all chronologies. + int yoe = get(YEAR_OF_ERA); + int moy = get(MONTH_OF_YEAR); + int dom = get(DAY_OF_MONTH); StringBuilder buf = new StringBuilder(30); buf.append(getChronology().toString()) .append(" ") .append(getEra()) .append(" ") .append(yoe) - .append(moy < 10 ? "-0" : "-").append(moy) - .append(dom < 10 ? "-0" : "-").append(dom); + .append('-'); + DecimalDigits.appendPair(buf, moy); + buf.append('-'); + DecimalDigits.appendPair(buf, dom); return buf.toString(); } From 84ce1bdc23dffdf8af77ea3a194d51b43e28f5bb Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Thu, 28 Aug 2025 13:23:32 +0800 Subject: [PATCH 5/5] Add DecimalDigitsTest to verify appendPair method with LATIN1 and UTF16 encoding --- .../jdk/internal/util/DecimalDigitsTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 test/jdk/jdk/internal/util/DecimalDigitsTest.java diff --git a/test/jdk/jdk/internal/util/DecimalDigitsTest.java b/test/jdk/jdk/internal/util/DecimalDigitsTest.java new file mode 100644 index 0000000000000..cdf70514e750d --- /dev/null +++ b/test/jdk/jdk/internal/util/DecimalDigitsTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024, Alibaba Group Holding Limited. 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. + */ + +package test.jdk.internal.util; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import jdk.internal.util.DecimalDigits; + +/* + * @test + * @bug 0000000 + * @summary Test DecimalDigits.appendPair method with LATIN1 and UTF16 encoding + * @modules java.base/jdk.internal.util + * @run testng test.jdk.internal.util.DecimalDigitsTest + */ +public class DecimalDigitsTest { + + @Test + public void testAppendPair() { + // Test values from 0 to 99 + for (int i = 0; i <= 99; i++) { + StringBuilder sb = new StringBuilder(); + DecimalDigits.appendPair(sb, i); + String expected = String.format("%02d", i); + assertEquals(sb.toString(), expected, "Failed for value: " + i); + } + } + + @Test + public void testAppendPairWithLatin1Encoding() { + // Test appendPair with LATIN1 encoding + StringBuilder sb = new StringBuilder(); + + // Force the StringBuilder to use LATIN1 encoding by ensuring all characters are LATIN1 + // This is the default behavior when all characters fit in LATIN1 + for (int i = 0; i <= 99; i++) { + sb.setLength(0); // Clear the StringBuilder + DecimalDigits.appendPair(sb, i); + String expected = String.format("%02d", i); + assertEquals(sb.toString(), expected, "Failed for value: " + i + " with LATIN1 encoding"); + } + } + + @Test + public void testAppendPairWithUTF16Encoding() { + // Test appendPair with UTF16 encoding + StringBuilder sb = new StringBuilder(); + + // Force the StringBuilder to use UTF16 encoding by adding a non-LATIN1 character first + sb.append('€'); // Euro sign is not in LATIN1 + + for (int i = 0; i <= 99; i++) { + int currentLength = sb.length(); + DecimalDigits.appendPair(sb, i); + String expected = String.format("%02d", i); + + // Check that the pair was appended correctly + String appended = sb.substring(currentLength); + assertEquals(appended, expected, "Failed for value: " + i + " with UTF16 encoding"); + } + } +} \ No newline at end of file