Showing posts with label Java 16. Show all posts
Showing posts with label Java 16. Show all posts

Friday, March 26, 2021

Implementing equals(Object) with instanceof Pattern Matching

Pattern matching for the instanceof operator was introduced as a preview feature with JDK 14 and was finalized with JDK 16. Because instanceof pattern matching is finalized for JDK 16, it is not surprising to now see changes being made to the JDK to take advantage of pattern matching for the instanceof operator. These changes to the JDK to leverage instanceof pattern matching can provide ideas and examples for where to begin applying this in our own code. In this post, I look at the use of instanceof pattern matching in implementation of the ubiquitous equals(Object) methods.

In a message posted to the core-libs-dev OpenJDK mailing list related to a code review for JDK-8263358 ("Update java.lang to use instanceof pattern variable"), Brian Goetz provided a reminder that a standard approach used in implementation of equals(Object) can now be modified to take advantage of pattern matching for instanceof.

In the message, Goetz uses an example of how we have often implemented equals(Object) (but using instanceof pattern matching in this example consistent with the past):

if (!(o instanceof Key that)) return false;
return name == that.name && Arrays.equals(ptypes, that.ptypes);

Goetz points out that this can now be written with a single statement, in this manner:

return (o instanceof Key that)
   && name == that.name
   && Arrays.equals(ptypes, that.ptypes);

Goetz's message concludes with this:

The use of "if it's not, return false" is a holdover from when we couldn't express this as a single expression (which is almost always preferable), which means we had to fall back to control flow. Now we don't have to.

A new commit was made based on Goetz's feedback and that commit is Commit e9d913152cefda827e01c060a13f15eacc086b33. One can review the several changes associated with this commit to see multiple statements converted into single statements in the various equals(Object) methods.

Being able to use instanceof pattern matching to implement equals(Object) with one fewer statement is a small improvement that is nevertheless welcome.

Monday, December 7, 2020

JDK 16: Stream to List In One Easy Call

As Java functional streams have become increasingly popular, an increasing number of requests is being made for new stream operations to be supported. Amidst these requests for numerous disparate new operations, one operation that seems to be requested more than the others is an operation that directly provides a List from a Stream. JDK 16 Early Access Build 27 introduces Stream.toList(), which is the subject of this post.

Before the JDK 16 Early Access Build 27 introduction of Stream.toList(), the most common approach for acquiring a List from a Stream was to invoke the approprite Collector:

stream.collect(Collectors.toList())

This is not a lot of code and it's fairly straightforward once you see it, but many have wanted an even more concise syntax for this frequently used stream operation. JDK 16 brings us this:

stream.toList()

It may be tempting to go into one's code base and use stream.toList() as a drop-in replacement for stream.collect(Collectors.toList()), but there may be differences in behavior if the code has a direct or indirect dependency on the implementation of stream.collect(Collectors.toList()) returning an ArrayList. Some of the key differences between the List returned by stream.collect(Collectors.toList()) and stream.toList() are spelled out in the remainder of this post.

The Javadoc-based documentation for Collectors.toList() states (emphasis added), "Returns a Collector that accumulates the input elements into a new List. There are no guarantees on the type, mutability, serializability, or thread-safety of the List returned..." Although there are no guarantees regarding the "type, mutability, serializability, or thread-safety" on the List provided by Collectors.toList(), it is expected that some may have realized it's currently an ArrayList and have used it in ways that depend on the characteristics of an ArrayList.

The following code snippet (full code listing on GitHub) shows a method that can be executed against the List implementations returned by Collectors.toList() and Stream.toList() to see what they have in common and how the are different.

/**
 * Analyzes the supplied {@code List} and writes to standard output
 * some key characteristics of the supplied {@code List}.
 *
 * @param listDescription Description of {@code List} to be analyzed.
 * @param listUnderAnalysis {@code List} to be analyzed.
 */
private static void analyzeList(
   final String listDescription, final List<String> listUnderAnalysis)
{
   out.println(listDescription + ": ");
   out.println("\tClass Type: " + listUnderAnalysis.getClass().getCanonicalName());
   out.println("\tAble to add to List? " + isListAddCapable(listUnderAnalysis));
   out.println("\tAble to sort List?   " + isListSortable(listUnderAnalysis));
}

When the simple analysis code above is executed against implementations of List returned by Stream.collect(Collectors.toList()) and Stream.toList(), the output appears as shown next.

Stream.collect(Collectors.toList()): 
	Class Type: java.util.ArrayList
	Able to add to List? true
	Able to sort List?   true
Stream.toList(): 
	Class Type: java.util.ImmutableCollections.ListN
	Able to add to List? false
	Able to sort List?   false
[NOT Stream] List.of(): 
	Class Type: java.util.ImmutableCollections.ListN
	Able to add to List? false
	Able to sort List?   false

The output shown above demonstrates that Stream.toList() provides a List implementation that is immutable (type ImmutableCollections.ListN that cannot be added to or sorted) similar to that provided by List.of() and in contrast to the mutable (can be changed and sorted) ArrayList provided by Stream.collect(Collectors.toList()). Any existing code depending on the ability to mutate the ArrayList returned by Stream.collect(Collectors.toList()) will not work with Stream.toList() and an UnsupportedOperationException will be thrown.

Although the implementation nature of the Lists returned by Stream.collect(Collectors.toList()) and Stream.toList() are very different, they still both implement the List interface and so they are considered equal when compared using List.equals(Object). This is demonstrated in the full code listing on GitHub.

The addition of method toList() to the Stream interface is a small thing, but it does make an often-used technique more convenient.

Saturday, November 28, 2020

JDK 16: Checking Indexes and Ranges of Longs

In my last post, I described the day period support added with JDK 16 Early Access Build 25. That same build also added methods for checking indexes and ranges of long values, which is the subject of this post.

JDK-8255150 ("Add utility methods to check long indexes and ranges") is the Enhancement used to add utility methods for checking long indexes and ranges similar to what JDK-8135248 ("Add utility methods to check indexes and ranges") added for integers with JDK 9. JDK-8255150 states, "The goal is to add a similar set of methods [as JDK-8135248] but rather than operate on int arguments, the new methods operate on long arguments."

JDK-8255150 lists the method signatures for the three new methods being added to the Objects class (descriptions provided by JDK-8135248):

  • Checking whether an index is within bounds:
    public static long checkIndex(long index, long length)
  • Checking whether an absolute range is within bounds:
    public static long checkFromToIndex(long fromIndex, long toIndex, long length)
  • Checking whether a relative range is within bounds:
    public static long checkFromIndexSize(long fromIndex, long size, long length)

Because these new methods "mirror the int utility methods," it is useful to look at JDK-8135248 to see more historical context for the justification for the introduction of these methods. That Enhancement states, "There are numerous methods in the JDK that check if an index or an absolute/relative range is valid before accessing the contents of an array (or in general a memory region for the case of a direct java.nio.ByteBuffer). ... Such checks, while not difficult, are often easy to get wrong and optimize correctly, thus there is a risk to the integrity and security of the runtime."

JDK-8135248 also talks about possibilities for optimization, "A further desire for such methods is some or all can be made intrinsic (see JDK-8042997), thus hinting to the HotSpot runtime compiler to use unsigned comparisons and better optimize array access (via aaload/store or Unsafe) in loops (especially those that are unrolled)."

A class that demonstrates these newly added methods, LongIndexRangeChecksDemo, is available on GitHub. All examples in this class demonstrate the various checks throwing IndexOutOfBoundsExceptions to indicate that the proposed index and/or size values do not fall within the allowed range. The main(String[]) function executes all the example methods and its output is divided into described sections below.

checkIndex Example Output

The message associated with this example clearly describes the index that is out of bounds and how that index is out of bounds.

==========================
== checkIndex Exception ==
==========================
java.lang.IndexOutOfBoundsException: Index 7 out of bounds for length 5
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:88)
	at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:412)
	at java.base/java.util.Objects.checkIndex(Objects.java:435)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.lambda$demoCheckIndexException$0(LongIndexRangeChecksDemo.java:34)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.executeDemonstration(LongIndexRangeChecksDemo.java:96)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.demoCheckIndexException(LongIndexRangeChecksDemo.java:33)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.main(LongIndexRangeChecksDemo.java:115)

checkFromToIndex Example Output

The message clearly indicates that the range specified by the "from" and "to" indexes is too large for the expected length capacity. Note that the "[" opening the range description indicates "inclusive" and the ")" ending the range description indicates "exclusive".

================================
== checkFromToIndex Exception ==
================================
java.lang.IndexOutOfBoundsException: Range [2, 6) out of bounds for length 3
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckFromToIndex(Preconditions.java:94)
	at java.base/jdk.internal.util.Preconditions.checkFromToIndex(Preconditions.java:459)
	at java.base/java.util.Objects.checkFromToIndex(Objects.java:461)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.lambda$demoCheckFromToIndexException$1(LongIndexRangeChecksDemo.java:48)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.executeDemonstration(LongIndexRangeChecksDemo.java:96)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.demoCheckFromToIndexException(LongIndexRangeChecksDemo.java:47)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.main(LongIndexRangeChecksDemo.java:116)

checkFromIndexSize Example Output

This example indicates that the range formed by a "from" index and range size is out of bounds for the specified length capacity.

==================================
== checkFromIndexSize Exception ==
==================================
java.lang.IndexOutOfBoundsException: Range [2, 2 + 6) out of bounds for length 3
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckFromIndexSize(Preconditions.java:100)
	at java.base/jdk.internal.util.Preconditions.checkFromIndexSize(Preconditions.java:507)
	at java.base/java.util.Objects.checkFromIndexSize(Objects.java:487)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.lambda$demoCheckFromIndexSizeException$2(LongIndexRangeChecksDemo.java:62)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.executeDemonstration(LongIndexRangeChecksDemo.java:96)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.demoCheckFromIndexSizeException(LongIndexRangeChecksDemo.java:61)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.main(LongIndexRangeChecksDemo.java:117)

checkFromIndexSize Overflow Example Output

This example indicates that the range formed by a "from" index and range size are out of bounds because a numeric overflow occurred when adding the size to the initial index. This is a nice catch because an overly simplistic homegrown approach that checked that the supplied initial index and supplied size are both positive and then checked the sum of the index and size against the allowed length would be faulty logic due to the overflow possibility.

=============================================
== checkFromIndexSize (Overflow) Exception ==
=============================================
java.lang.IndexOutOfBoundsException: Range [2, 2 + 9223372036854775807) out of bounds for length 3
	at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
	at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckFromIndexSize(Preconditions.java:100)
	at java.base/jdk.internal.util.Preconditions.checkFromIndexSize(Preconditions.java:507)
	at java.base/java.util.Objects.checkFromIndexSize(Objects.java:487)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.lambda$demoCheckFromIndexSizeExceptionOnOverflow$3(LongIndexRangeChecksDemo.java:77)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.executeDemonstration(LongIndexRangeChecksDemo.java:96)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.demoCheckFromIndexSizeExceptionOnOverflow(LongIndexRangeChecksDemo.java:76)
	at dustin.examples.jdk16.check.LongIndexRangeChecksDemo.main(LongIndexRangeChecksDemo.java:118)

Common Uses

The greatest beneficiary of these newly added long-supporting methods may be the authors, maintainers, and users of the foreign memory access API as described in this mailing list message: "We have to jump through quite a few hoops in the implementation of the foreign memory access API in order to leverage the intrinsification of int-based index checks, and even then we are not covering the cases where the numbers are larger than ints. Looking forward to being able to remove those hacks!"

A common use of these methods is likely to be as method guards for checking method parameters against expected preconditions similar to how other Objects' methods such as checkIndex(int, int), checkFromToIndex(int, int, int), checkFromIndexSize(int, int, int), requireNonNull(T), and requireNonNull(T, String) are used.

Monday, November 23, 2020

Day Period Support in JDK 16

JDK 16 Early Access Build 25 (2020/11/18) includes changes for JDK-8247781 ("Day periods support"). As stated in the JDK 16 Early Access Build 25 Release Notes ("Day period support added to java.time formats"), the new functionality "translates day periods defined in Unicode Consortium's CLDR."

In most English-language situations using a "12-hour clock", the "day periods" might be used instead of the AM (ante meridiem) and PM (post meridiem) designators. The Unicode documentation "Day Period Rule Sets" describes day period rules like this: "Each locale can have a set of day period rules, which determine the periods during a day for use in time formats like '10:00 at night', or to select statements like 'Your email arrived last night.' If locales do not have dayPeriodRules, the computation of dayPeriods falls back to AM/PM."

As with most things, it's perhaps easiest to see this new functionality via code examples and their associated output. The first example shown here is adapted from the JDK 16 Early Access Build 25 Release Notes. Note that the "B" is used in the pattern to specify that a day period is to be used.

/**
 * Writes the current day period out to standard output.
 *
 * This is based on the example included with the Release Notes
 * (https://jdk.java.net/16/release-notes).
 */
public void printCurrentDayPeriod()
{
   final String currentDayPeriodStr
      = DateTimeFormatter.ofPattern("B").format(LocalTime.now());
   out.println("Pattern 'B' (time now): \"" + currentDayPeriodStr + "\"");
}

The output for the above code sample when run in evening hours is now shown:

Pattern 'B' (time now): "in the evening"

The next code sample contains two methods, showing variations of date/time formats using the "B" format pattern letter.

/**
 * Prints representation of supplied {@link ZonedDateTime} with hour,
 * day period, and time zone details to standard output.
 *
 * @param zonedDateTime Date/time that will be written to standard output
 *    and will include hour, day period, and zone details.
 */
public void printHourDayPeriodAndZone(final ZonedDateTime zonedDateTime)
{
   final String dateTimeStr
      = DateTimeFormatter.ofPattern("hh B, zzzz").format(zonedDateTime);
   out.println("Hour/Day Period/Zone: \"" + dateTimeStr + "\"");
}

/**
 * Prints representation of supplied {@link ZonedDateTime} with hour,
 * minutes, day period, and time zone details to standard output.
 *
 * @param zonedDateTime Date/time that will be written to standard output
 *    and will include hour, minutes, day period, and zone details.
 */
public void printHourDayMinutePeriodAndZone(final ZonedDateTime zonedDateTime)
{
   final String dateTimeStr
      = DateTimeFormatter.ofPattern("K:mm B z").format(zonedDateTime);
   out.println("Hour/Minute/Day Period/Zone: \"" + dateTimeStr + "\"");
}

The following two lines of output are shown when the two methods above are executed during evening hours:

Hour/Day Period/Zone: "08 in the evening, Mountain Standard Time"
Hour/Minute/Day Period/Zone: "8:07 in the evening MST"

Because the output for the examples to this point were all executed in evening hours, the day period ("in the evening") has been the same for all examples executed above.

The next code listing iterates over each hour of the day to indicate different day period expressions for the different hours when Locale.US is specified. Note that the dates/times constructed in this example have non-zero fractional hours (non-zero minutes, seconds, and nanoseconds).

/**
 * Prints Day Period phraseology for each of 24 hours of day with
 * arbitrary minutes, seconds, and nanoseconds to standard output.
 */
public void printDayPeriodsByHour()
{
   out.println("===== Hours With Non-Zero Minutes/Seconds/Nanoseconds =====");
   final DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern("hh B");
   for (int hour = 0; hour < 24; hour++)
   {
      final OffsetDateTime dateTime
         = Instant.now().atOffset(ZoneOffset.UTC).withHour(hour);
      out.println("Hour " + hour + ": \"" + dateTimeFormat.format(dateTime) + "\"");
   }
}

The output from running the code above shows how different hours correspond to different "day periods" for US.Locale. Note that this output is based on a date/time with non-zero fractional hours (times are not exact hours).

===== Hours With Non-Zero Minutes/Seconds/Nanoseconds =====
Hour 0: "12 at night"
Hour 1: "01 at night"
Hour 2: "02 at night"
Hour 3: "03 at night"
Hour 4: "04 at night"
Hour 5: "05 at night"
Hour 6: "06 in the morning"
Hour 7: "07 in the morning"
Hour 8: "08 in the morning"
Hour 9: "09 in the morning"
Hour 10: "10 in the morning"
Hour 11: "11 in the morning"
Hour 12: "12 in the afternoon"
Hour 13: "01 in the afternoon"
Hour 14: "02 in the afternoon"
Hour 15: "03 in the afternoon"
Hour 16: "04 in the afternoon"
Hour 17: "05 in the afternoon"
Hour 18: "06 in the evening"
Hour 19: "07 in the evening"
Hour 20: "08 in the evening"
Hour 21: "09 at night"
Hour 22: "10 at night"
Hour 23: "11 at night"

Because the date/time used in the example above has non-zero fractional hours, the en_US day period expressions are "at night", "in the morning", "in the afternoon", and "in the evening".

The next code snippet shows a method that ensures the formatted date/time has "exact hours" (its minutes, seconds, and nanoseconds are all zero).

/**
 * Prints Day Period phraseology for each of 24 hours of day with
 * zero minutes, zero seconds, and zero nanoseconds to standard output.
 */
public void printDayPeriodsByWholeHours()
{
   out.println("===== Exact Hours =====");
   final DateTimeFormatter dateTimeFormat = DateTimeFormatter.ofPattern("hh B");
   for (int hour = 0; hour < 24; hour++)
   {
      final OffsetDateTime dateTime = OffsetDateTime.of(
         2020, 11, 23, hour, 0, 0, 0, ZoneOffset.UTC);
      out.println("Hour " + hour + ": \"" + dateTimeFormat.format(dateTime) + "\"");
   }
}

When the above code is executed, the following output is seen:

===== Exact Hours =====
Hour 0: "12 midnight"
Hour 1: "01 at night"
Hour 2: "02 at night"
Hour 3: "03 at night"
Hour 4: "04 at night"
Hour 5: "05 at night"
Hour 6: "06 in the morning"
Hour 7: "07 in the morning"
Hour 8: "08 in the morning"
Hour 9: "09 in the morning"
Hour 10: "10 in the morning"
Hour 11: "11 in the morning"
Hour 12: "12 noon"
Hour 13: "01 in the afternoon"
Hour 14: "02 in the afternoon"
Hour 15: "03 in the afternoon"
Hour 16: "04 in the afternoon"
Hour 17: "05 in the afternoon"
Hour 18: "06 in the evening"
Hour 19: "07 in the evening"
Hour 20: "08 in the evening"
Hour 21: "09 at night"
Hour 22: "10 at night"
Hour 23: "11 at night"

The output above is mostly the same as for the output associated with dates/times that had fractional hours, but the whole hours example is different for hour 0 (day period of "midnight") and hour 12 (day period of "noon").

For my last example, I'm going to use Dominican Republic/Spanish ("es DO") for the Locale with the same code just demonstrated. Here is that output:

Hour 0: "12 de la madrugada"
Hour 1: "01 de la madrugada"
Hour 2: "02 de la madrugada"
Hour 3: "03 de la madrugada"
Hour 4: "04 de la madrugada"
Hour 5: "05 de la madrugada"
Hour 6: "06 de la mañana"
Hour 7: "07 de la mañana"
Hour 8: "08 de la mañana"
Hour 9: "09 de la mañana"
Hour 10: "10 de la mañana"
Hour 11: "11 de la mañana"
Hour 12: "12 del mediodía"
Hour 13: "01 de la tarde"
Hour 14: "02 de la tarde"
Hour 15: "03 de la tarde"
Hour 16: "04 de la tarde"
Hour 17: "05 de la tarde"
Hour 18: "06 de la tarde"
Hour 19: "07 de la tarde"
Hour 20: "08 de la noche"
Hour 21: "09 de la noche"
Hour 22: "10 de la noche"
Hour 23: "11 de la noche"

Support for "day period" presentation in formatted dates/times provides Java developers with more flexibility in expressing day period details than simply using AM and PM. All code listings shown in this post are available on GitHub.

Friday, August 21, 2020

JDK16 javac xlint Warning about Default Constructors

I mentioned in my blog post "Explicit No-Arguments Constructor Versus Default Constructor" that "it is possible that one day javac will have an available lint warning to point out classes with default constructors." In that post, I referenced JDK-8071961 ("Add javac lint warning when a default constructor is created"), which has now been implemented as of JDK 16 Early Access Build #12. This post introduces that newly available javac -xlint warning.

To see this new javac -Xlint warning in action, one must download at least JDK 16 Early Access Build #12 (19 August 2020) or later.

To demonstrate the new javac -Xlint warning, we need a class with no explicit constructor so that javac will generate a "default constructor."

<rant>(By the way, a minor pet peeve of mine is when someone comments an explicit constructor with no arguments with Javadoc text that states "Default constructor." It's not really a default constructor once it's explicitly specified!)</rant>

An example of a class with no explicit constructor is available on GitHub and is shown here:

DefaultConstructor.java
package dustin.examples.jdk16;

import static java.lang.System.out;

/**
 * This class intentionally does NOT specify an explicit constructor
 * so that a "default constructor" will be generated and trigger the
 * new JDK 16 warning.
 */
public class DefaultConstructor
{
   private String name;

   public String getName()
   {
      return name;
   }

   public void setName(final String newName)
   {
      name = newName;
   }

   public static void main(final String[] arguments)
   {
      final DefaultConstructor instance = new DefaultConstructor();
      instance.setName(arguments.length > 0 ? arguments[0] : "");
      out.println("Hello " + instance.getName() + "!");
   }
}

If we compile the new class with no explicitly specified constructor with javac provided by the OpenJDK JDK 16 Early Access Build #12 or later, we won't see the new warning demonstrated unless we export the package that class is in and enable -Xlint warnings. An example of exporting the package is available on GitHub and is shown here:

module-info.java
module dustin.examples
{
   exports dustin.examples.jdk16;
}

Compiling with -Xlint

When I run javac -X with the JDK 16 Early Access Build #12 compiler, I see these Xlint-related options that are now available (emphasis added):

  -Xlint                       Enable recommended warnings
  -Xlint:(,)*
        Warnings to enable or disable, separated by comma.
        Precede a key by - to disable the specified warning.
        Supported keys are:
          all                  Enable all warnings
          auxiliaryclass       Warn about an auxiliary class that is hidden in a source file, and is used from other files.
          cast                 Warn about use of unnecessary casts.
          classfile            Warn about issues related to classfile contents.
          deprecation          Warn about use of deprecated items.
          dep-ann              Warn about items marked as deprecated in JavaDoc but not using the @Deprecated annotation.
          divzero              Warn about division by constant integer 0.
          empty                Warn about empty statement after if.
          exports              Warn about issues regarding module exports.
          fallthrough          Warn about falling through from one case of a switch statement to the next.
          finally              Warn about finally clauses that do not terminate normally.
          missing-explicit-ctor Warn about missing explicit constructors in public classes in exported packages.
          module               Warn about module system related issues.
          opens                Warn about issues regarding module opens.
          options              Warn about issues relating to use of command line options.
          overloads            Warn about issues regarding method overloads.
          overrides            Warn about issues regarding method overrides.
          path                 Warn about invalid path elements on the command line.
          processing           Warn about issues regarding annotation processing.
          rawtypes             Warn about use of raw types.
          removal              Warn about use of API that has been marked for removal.
          requires-automatic   Warn about use of automatic modules in the requires clauses.
          requires-transitive-automatic Warn about automatic modules in requires transitive.
          serial               Warn about Serializable classes that do not provide a serial version ID. 
                             Also warn about access to non-public members from a serializable element.
          static               Warn about accessing a static member using an instance.
          text-blocks          Warn about inconsistent white space characters in text block indentation.
          try                  Warn about issues relating to use of try blocks (i.e. try-with-resources).
          unchecked            Warn about unchecked operations.
          varargs              Warn about potentially unsafe vararg methods
          preview              Warn about use of preview language features
          none                 Disable all warnings

As shown in these usage details, one can use -Xlint, -Xlint:all, or -Xlint:missing-explicit-ctor to see this new warning about default constructors being exposed by classes in publicly exported packages.

Compiling the new class with -Xlint, -Xlint:all, or -Xlint:missing-explicit-ctor demonstrates the new warning about default constructors being used in a formal API:

-Xlint:all

-Xlint and -Xlint:missing-explicit-ctor

As shown in the screenshots, the warning message states (emphasis added by me): "warning: [missing-explicit-ctor] class DefaultConstructor in exported package dustin.examples.jdk16 declares no explicit constructors, thereby exposing a default constructor to clients of module dustin.examples"

The warning message that javac provides when -Xlint is appropritely specified describes the issue and specifically calls out the exported package with the offending class and the name of module that exports that package.

Summary of Steps to See Default Constructor Warning

  1. Download and "install" OpenJDK 16 Early Access Build #12 (or later) from https://jdk.java.net/16/
  2. Write Java class with no explicitly specified constructor so that javac will generate a "default constructor" (example).
  3. Export package with class with no explicit constructor via a module-info.java file (example).
  4. Compile class with no explicit constructor with -Xlint:all provided to the javac compiler.

Not All Classes with No Explicit Constructors Will Be Flagged

Not all Java classes that lack an explicit constructor will lead to this new warning being emitted even when a relevant -Xlint option is specified. As stated earlier, even the DefaultConstructor class used in this post's example does not lead to the warning message being generated until it's package is exported in the module-info.java file. Joe Darcy explains on the OpenJDK compiler-dev mailing list:

In terms of detailed criteria to issue the new warnings, there was the usual tension in warnings between reducing false positives and false negatives. For example, warning for *any* default constructor, even in a throw-away class, would be more annoying than helpful. With some guidance from the JDK code base, criteria in the current patch are a default constructor merits a warning if:

  • The class is in a named package and the packaged has an unqualified export from its module AND
  • The class is public and, if it is a nested class, all of its lexically enclosing types are public too.

An unqualified export, where the package is available to use to any module and not just named ones, was taken to indicate classes in the package can comprise a "formal API". It would be simple to change this to an unqualified export, but I wanted to avoid unwanted instances of a new warning. If a public nested class is a non-public enclosing class, the nested class is not directly part of the exported API. These combinations of kinds of exports and nesting are tested in the tests in the DefaultCtor directory.

Why Warn on Use of Default Constructors in a "Formal API" Class?

The previously mentioned Joe Darcy post explains why this warning has been added:

Some background on the design of the warning and broader usage context, while default constructors can be convenient for informal code, they can be a bit troublesome for more formal APIs, such as the public classes of packages in the JDK. One issue is that default constructors do not have javadoc. Another is that a class that semantically should not be instantiated, say it is a solely a holder for static constants and methods, can get instantiated and subclassed. (Subclasssing such a class is an anti-pattern to allow use short names for the static members, which is no longer necessary since static imports as of Java SE 5.0.)

Conclusion

This seemingly small change to add this new warning about "default constructors" in "formal API" classes has required more effort than might initially be assumed. A large number of issues were written to not only introduce the xlint warning, but to clean up numerous classes throughout the JDK that triggered this warning when compiled. Furthermore, naming and logging can often be tricky and the particular warning mssage went through review and iterative changes as well.

Saturday, August 1, 2020

Finalizing instanceof Pattern Matching and Records in JDK 16

Gavin Bierman has recently posted two approachable posts regarding instanceof pattern matching and records on the amber-spec-experts mailing list and both posts are thorough but succinct. I recommend that anyone interested in the latest state of these two Project Amber initiatives read these posts: "Finalizing in JDK 16 - Pattern matching for instanceof" and "Finalizing in JDK 16 - Records". Even for those not interested in the details, it is probably interesting to see the likelihood of instanceof pattern matching and records being finalized in JDK 16.

Pattern Matching instanceof Operator

In "Finalizing in JDK 16 - Pattern matching for instanceof," Bierman writes: "In JDK 16 we are planning to finalize two JEPs: Pattern matching for instanceof and Records." About instanceof pattern matching, Bierman states, "Adding conditional pattern matching to an expression form is the main technical novelty of our design of this feature."

The "Finalizing in JDK 16 - Pattern matching for instanceof" post talks about advantages of the instanceof pattern matching approach. Advantages discussed in the post include refactoring of a "common programming pattern" (checking for instanceof and then explicitly casting the thing checked with instanceof to the type it was checked against) to smaller code that is more readable. Another discussed advantage is the ability "to compactly express things with expressions that are unnecessarily complicated using statements."

Bierman also discusses scope issues, local pattern variable "poisoning", and how these will be dealt with using "flow scoping." About this, Bierman writes:

Java already uses flow analysis - both in checking the access of local variables and blank final fields, and detecting unreachable statements. We lean on this concept to introduce the new notion of flow scoping. A pattern variable is only in scope where the compiler can deduce that the pattern has matched and the variable will be bound. This analysis is flow sensitive and works in a similar way to the existing analyses. ... The motto is "a pattern variable is in scope where it has definitely matched". This is intuitive, allows for the safe reuse of pattern variables, and Java developers are already used to flow sensitive analyses.

The post on instanceof pattern matching ends with a discussion of the "Swiss cheese property" ("unfortunate interaction of flow scoping and shadowing [local pattern variables shadowing a field declaration]") and expresses the hope that IDEs will help developers to deal with these unfortunate interactions.

Records

Bierman's post "Finalizing in JDK 16 - Records" provides some nice summary descriptions related to Records. The first is, "Record classes are a special kind of class that are used primarily to define a simple aggregate of values." This is immediately followed by, "Records can be thought of as nominal tuples; their declaration commits to a description of their state and given that their representation, as well as all of the interesting protocols an object might expose -- construction, property access, equality, etc -- are derived from that state description."

This post covers the "implicit declaration of a number of members" of a Record from the Record's "state description." The post also provides a brief overview of why a developer might "explicitly [provide] a canonical constructor" and how to do so. About this, Bierman writes, "The intention of a compact constructor declaration is that only validation and/or normalization code need be given in the constructor body; the remaining initialization code is automatically supplied by the compiler."

"Finalizing in JDK 16 - Records" also discusses canonical constructor accessibility, @Override, and general annotations related to Records. The post concludes by mentioning that Records can be declared locally and explains how this is an advantage for an expected common use case of Records: "Records will often be used as containers for intermediate data within method bodies. Being able to declare these record classes locally is essential to stop proliferation of classes."

Conclusion

The two Project Amber initiatives of instanceof pattern matching and records will be welcome additions to the Java programming language. Although these features have been available in versions of the JDK before JDK 16 as preview features, it will be their finalization (hopefully in JDK 16) that will enable many more developers to apply them in production code.