diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java index 16d7193c5561f..1adb49ff37cef 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -544,6 +544,12 @@ public final class DateTimeFormatter { */ private final ZoneId zone; + /** + * Flag indicating whether this formatter only uses ChronoField instances. + * This is used to optimize the storage of parsed field values in the Parsed class. + */ + final boolean onlyChronoField; + //----------------------------------------------------------------------- /** * Creates a formatter using the specified pattern. @@ -1474,11 +1480,12 @@ public static final TemporalQuery parsedLeapSecond() { * @param resolverFields the fields to use during resolving, null for all fields * @param chrono the chronology to use, null for no override * @param zone the zone to use, null for no override + * @param onlyChronoField flag indicating whether this formatter only uses ChronoField instances */ DateTimeFormatter(CompositePrinterParser printerParser, Locale locale, DecimalStyle decimalStyle, ResolverStyle resolverStyle, Set resolverFields, - Chronology chrono, ZoneId zone) { + Chronology chrono, ZoneId zone, boolean onlyChronoField) { this.printerParser = Objects.requireNonNull(printerParser, "printerParser"); this.resolverFields = resolverFields; this.locale = Objects.requireNonNull(locale, "locale"); @@ -1486,6 +1493,7 @@ public static final TemporalQuery parsedLeapSecond() { this.resolverStyle = Objects.requireNonNull(resolverStyle, "resolverStyle"); this.chrono = chrono; this.zone = zone; + this.onlyChronoField = onlyChronoField; } //----------------------------------------------------------------------- @@ -1523,7 +1531,7 @@ public DateTimeFormatter withLocale(Locale locale) { if (this.locale.equals(locale)) { return this; } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone, onlyChronoField); } /** @@ -1569,7 +1577,7 @@ public DateTimeFormatter localizedBy(Locale locale) { Objects.equals(z, zone)) { return this; } else { - return new DateTimeFormatter(printerParser, locale, ds, resolverStyle, resolverFields, c, z); + return new DateTimeFormatter(printerParser, locale, ds, resolverStyle, resolverFields, c, z, onlyChronoField); } } @@ -1595,7 +1603,7 @@ public DateTimeFormatter withDecimalStyle(DecimalStyle decimalStyle) { if (this.decimalStyle.equals(decimalStyle)) { return this; } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone, onlyChronoField); } //----------------------------------------------------------------------- @@ -1649,7 +1657,7 @@ public DateTimeFormatter withChronology(Chronology chrono) { if (Objects.equals(this.chrono, chrono)) { return this; } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone, onlyChronoField); } //----------------------------------------------------------------------- @@ -1706,7 +1714,7 @@ public DateTimeFormatter withZone(ZoneId zone) { if (Objects.equals(this.zone, zone)) { return this; } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone, onlyChronoField); } //----------------------------------------------------------------------- @@ -1748,7 +1756,7 @@ public DateTimeFormatter withResolverStyle(ResolverStyle resolverStyle) { if (Objects.equals(this.resolverStyle, resolverStyle)) { return this; } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone, onlyChronoField); } //----------------------------------------------------------------------- @@ -1814,7 +1822,7 @@ public DateTimeFormatter withResolverFields(TemporalField... resolverFields) { if (Objects.equals(this.resolverFields, fields)) { return this; } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, fields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, fields, chrono, zone, onlyChronoField); } /** @@ -1863,7 +1871,7 @@ public DateTimeFormatter withResolverFields(Set resolverFields) { if (resolverFields != null) { resolverFields = Collections.unmodifiableSet(new HashSet<>(resolverFields)); } - return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone); + return new DateTimeFormatter(printerParser, locale, decimalStyle, resolverStyle, resolverFields, chrono, zone, onlyChronoField); } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index 4708094effbde..8f345d62abb27 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -200,6 +200,12 @@ public final class DateTimeFormatterBuilder { */ private int valueParserIndex = -1; + /** + * Flag indicating whether this builder only uses ChronoField instances. + * This is used to optimize the storage of parsed field values in the Parsed class. + */ + private boolean onlyChronoField = true; + /** * Gets the formatting pattern for date and time styles for a locale and chronology. * The locale and chronology are used to lookup the locale specific format @@ -2371,11 +2377,35 @@ private int appendInternal(DateTimePrinterParser pp) { active.padNextWidth = 0; active.padNextChar = 0; } + + // Update the onlyChronoField flag if the printer/parser uses non-ChronoField instances + checkField(pp); active.printerParsers.add(pp); active.valueParserIndex = -1; return active.printerParsers.size() - 1; } + /** + * Update the onlyChronoField flag if the printer/parser uses non-ChronoField instances + * @param pp the printer-parser + */ + private void checkField(DateTimePrinterParser pp) { + TemporalField field; + if (pp instanceof NumberPrinterParser npp) { + field = npp.field; + } else if (pp instanceof TextPrinterParser tpp) { + field = tpp.field; + } else if (pp instanceof DefaultValueParser dvp) { + field = dvp.field; + } else { + return; + } + + if (!(field instanceof ChronoField)) { + active.onlyChronoField = false; + } + } + //----------------------------------------------------------------------- /** * Completes this builder by creating the {@code DateTimeFormatter} @@ -2443,7 +2473,7 @@ private DateTimeFormatter toFormatter(Locale locale, ResolverStyle resolverStyle } CompositePrinterParser pp = new CompositePrinterParser(printerParsers, false); return new DateTimeFormatter(pp, locale, DecimalStyle.STANDARD, - resolverStyle, null, chrono, null); + resolverStyle, null, chrono, null, onlyChronoField); } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/format/DateTimeParseContext.java b/src/java.base/share/classes/java/time/format/DateTimeParseContext.java index 0a4c7e825a3b6..fff139dc9f38f 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeParseContext.java +++ b/src/java.base/share/classes/java/time/format/DateTimeParseContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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 @@ -120,7 +120,7 @@ final class DateTimeParseContext { DateTimeParseContext(DateTimeFormatter formatter) { super(); this.formatter = formatter; - parsed.add(new Parsed()); + parsed.add(new Parsed(formatter.onlyChronoField)); } /** diff --git a/src/java.base/share/classes/java/time/format/Parsed.java b/src/java.base/share/classes/java/time/format/Parsed.java index 1ec956dfa2cd3..12623367ca08d 100644 --- a/src/java.base/share/classes/java/time/format/Parsed.java +++ b/src/java.base/share/classes/java/time/format/Parsed.java @@ -1,5 +1,6 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 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 @@ -98,6 +99,7 @@ import java.time.temporal.TemporalQueries; import java.time.temporal.TemporalQuery; import java.time.temporal.UnsupportedTemporalTypeException; +import java.util.EnumMap; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -128,7 +130,7 @@ final class Parsed implements TemporalAccessor { /** * The parsed fields. */ - final Map fieldValues = new HashMap<>(); + final Map fieldValues; /** * The parsed zone. */ @@ -169,7 +171,12 @@ final class Parsed implements TemporalAccessor { /** * Creates an instance. */ - Parsed() { + @SuppressWarnings("unchecked") + Parsed(boolean onlyChronoField) { + // Create the EnumMap with raw types and cast it appropriately + // This is safe because ChronoField implements TemporalField + fieldValues = onlyChronoField ? (Map) (Map) new EnumMap<>(ChronoField.class) + : new HashMap<>(); } /** @@ -177,7 +184,7 @@ final class Parsed implements TemporalAccessor { */ Parsed copy() { // only copy fields used in parsing stage - Parsed cloned = new Parsed(); + Parsed cloned = new Parsed(fieldValues instanceof EnumMap); cloned.fieldValues.putAll(this.fieldValues); cloned.zone = this.zone; cloned.zoneNameType = this.zoneNameType; diff --git a/test/micro/org/openjdk/bench/java/time/format/DateTimeFormatterParse.java b/test/micro/org/openjdk/bench/java/time/format/DateTimeFormatterParse.java new file mode 100644 index 0000000000000..cdf6f47083576 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/time/format/DateTimeFormatterParse.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025, 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 org.openjdk.bench.java.time.format; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +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; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +@State(Scope.Thread) +public class DateTimeFormatterParse { + static final DateTimeFormatter formatterLocalTime = DateTimeFormatter.ofPattern("HH:mm:ss"); + static final DateTimeFormatter formatterLocalTimeWithNano = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + static final DateTimeFormatter formatterLocalDate = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + static final DateTimeFormatter formatterLocalDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"); + static final DateTimeFormatter formatterLocalDateTimeWithNano = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS"); + static final DateTimeFormatter formatterOffsetDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZZZ"); + static final DateTimeFormatter formatterZonedDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSZZZZZ'['VV']'"); + + static final String STR_LOCALTIME = "21:11:48"; + static final String STR_LOCALTIME_WITH_NANO = "21:11:48.456"; + static final String STR_LOCALDATE = "2024-08-01"; + static final String STR_LOCALDATETIME = "2024-08-01T21:11:48"; + static final String STR_LOCALDATETIME_WITH_NANO = "2024-08-01T21:11:48.456"; + static final String STR_INSTANT = "2024-08-12T03:25:54.980339Z"; + static final String STR_OFFSETDATETIME = "2024-08-12T11:50:46.731509+08:00"; + static final String STR_ZONEDDATETIME = "2024-08-12T11:50:46.731509+08:00[Asia/Shanghai]"; + + @Benchmark + public LocalTime parseLocalTime() { + return LocalTime.parse(STR_LOCALTIME, formatterLocalTime); + } + + @Benchmark + public LocalTime parseLocalTimeWithNano() { + return LocalTime.parse(STR_LOCALTIME_WITH_NANO, formatterLocalTimeWithNano); + } + + @Benchmark + public LocalDate parseLocalDate() { + return LocalDate.parse(STR_LOCALDATE, formatterLocalDate); + } + + @Benchmark + public LocalDateTime parseLocalDateTime() { + return LocalDateTime.parse(STR_LOCALDATETIME, formatterLocalDateTime); + } + + @Benchmark + public LocalDateTime parseLocalDateTimeWithNano() { + return LocalDateTime.parse(STR_LOCALDATETIME_WITH_NANO, formatterLocalDateTimeWithNano); + } + + @Benchmark + public OffsetDateTime parseOffsetDateTime() { + return OffsetDateTime.parse(STR_OFFSETDATETIME, formatterOffsetDateTime); + } + + @Benchmark + public ZonedDateTime parseZonedDateTime() { + return ZonedDateTime.parse(STR_ZONEDDATETIME, formatterZonedDateTime); + } + + @Benchmark + public Instant parseInstant() { + return Instant.parse(STR_INSTANT); + } +} \ No newline at end of file