JRE Emulation & Shared Code

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-08. It concerns client code (the Java that the compiler translates to JavaScript and the library GWT translates it against), shared code (compiled twice, once by javac for the server and once by the GWT compiler for the browser) and build settings (-sourceLevel, jre.checks.*, the module XML). Every behaviour in the tables was observed by compiling a probe module with the 2.13.1 compiler, loading it in headless Chrome and comparing it with the same statements run on a JDK 21 JVM; the exact outputs and compiler messages are quoted as they were produced.

What "emulation" means

Browsers do not have a JVM, so the GWT compiler cannot translate calls into rt.jar/the platform modules. Instead it needs the source of every class that client code touches. For your own classes that source is on the module’s source path; for java.lang.String, java.util.ArrayList and the rest of the JRE it is the JRE emulation library: hand-written, translatable Java source files, shipped inside gwt-user-2.13.1.jar under com/google/gwt/emul/ and registered by the module com.google.gwt.emul.Emulation. A comment in that file tells you not to inherit it directly but to inherit com.google.gwt.core.Core, which every normal GWT module does through com.google.gwt.user.User. The module consists of a <super-source/> element: the files below com/google/gwt/emul are re-rooted so that com/google/gwt/emul/java/util/ArrayList.java is seen by the compiler as java/util/ArrayList.java.

flowchart LR src["Your client and shared sources
com.example.bookshelf.client / .shared"] --> jdt["GWT compiler front end
(JDT, -sourceLevel 17)"] emul["gwt-user: com/google/gwt/emul
String, ArrayList, Optional, streams ..."] --> jdt jdt --> check{"Does every referenced
type have source?"} check -- "no" --> err["No source code is available for type ...
did you forget to inherit a required module?"] check -- "yes" --> js["Optimised JavaScript
permutation .cache.js"] jvm["Server JVM: real java.*"] -. "never used for the browser build" .-> js

Consequences worth keeping in mind:

  • The emulated classes are subsets with their own behaviour. A method that exists on the JVM may be absent, and a method that exists may behave differently in details (examples below).

  • Only the types and members that your code actually reaches survive into the output (dead-code elimination), so referencing a heavy class costs code size, not just correctness. Compile Reports & Optimization and Code Splitting cover how to measure it.

  • A class that has no emulation is not "slow" or "unsupported at runtime": it simply does not compile. The error appears at compile time, which is why the Developer Guide advises running early and often.

  • GWT also ships a few similar classes under other names where emulating the real class would be too expensive; the Developer Guide lists com.google.gwt.i18n.client.DateTimeFormat, com.google.gwt.i18n.client.NumberFormat, com.google.gwt.user.client.rpc.IsSerializable and com.google.gwt.user.client.Timer.

What the library provides

The reference page RefJreEmulation.html lists the supported packages, and for each type the supported constructors, methods and fields. It is generated from the emulation sources, so it follows the release (2.13.0 reworked it to also document what is not supported, below). The packages it covers, and what the 2.13.1 sources contain, are:

Table 1. JRE packages emulated by GWT 2.13.1
Package What is there (not exhaustive)

java.lang

Object, String, StringBuilder/StringBuffer, the boxes (Boolean, Byte, Short, Integer, Long, Float, Double, Character, Number, Void), Math/StrictMath, Enum, Record, Class (names only), Throwable and the usual exceptions, Iterable, Comparable, Runnable, AutoCloseable, System (a few members), ThreadLocal, StackTraceElement

java.lang.annotation, java.lang.reflect

the annotation meta types; from reflect only Array and Type

java.util

the collection hierarchy (ArrayList, LinkedList, HashMap, LinkedHashMap, TreeMap, HashSet, TreeSet, ArrayDeque, PriorityQueue, EnumMap, EnumSet, BitSet, Collections, Arrays), Optional*, Objects, StringJoiner, Random, Date, a partial Locale, Comparator, Iterator, the *SummaryStatistics classes

java.util.function, java.util.stream

the functional interfaces; Stream, IntStream, LongStream, DoubleStream, Collectors, StreamSupport (see the Java Reference pages on streams and lambdas for the language side)

java.util.concurrent, .atomic

ConcurrentHashMap, ConcurrentMap, Callable, Future, Executor, ExecutorService, ScheduledExecutorService, Executors, TimeUnit, Flow, AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference, AtomicReferenceArray: enough to compile code written against the interfaces, in a single-threaded runtime

java.util.logging

Logger, Level, Handler, LogRecord, Formatter, LogManager, SimpleConsoleLogHandler; whether anything is output is decided by the logging module (gwt.logging.enabled), covered with Logging

java.math

BigInteger, BigDecimal, MathContext, RoundingMode

java.io

byte and char stream base classes (InputStream, OutputStream, ByteArrayInputStream, ByteArrayOutputStream, Reader, Writer, StringReader, BufferedWriter, PrintStream, OutputStreamWriter), Serializable, Closeable, Flushable, IOException, UncheckedIOException. There is no File, no file or socket stream

java.nio.charset, java.text, java.security, java.sql, java.beans

Charset/StandardCharsets; Normalizer and ParseException; MessageDigest (the sources contain a SHA-256 implementation) and its exception types; java.sql.Date/Time/Timestamp; Beans.isDesignTime()

The headline point is that the library is a deliberately small, browser-oriented subset: several packages are present only with a handful of types. Absent entirely, and therefore compile errors, are among others java.time, java.util.regex, java.util.UUID, java.io.File, java.lang.Thread, java.util.Scanner and java.net. The sample code below compiles a class that uses several of them and shows the messages the compiler produces.

package com.example.bad.client;

import com.google.gwt.core.client.EntryPoint;
import java.time.LocalDate;
import java.util.UUID;
import java.util.regex.Pattern;

public class BadEntry implements EntryPoint {
  public void onModuleLoad() {
    LocalDate d = LocalDate.now();
    UUID id = UUID.randomUUID();
    Pattern p = Pattern.compile("a+");
    try { Thread.sleep(10); } catch (InterruptedException e) { }
    new java.io.File("books.txt").exists();
  }
}

Compiling it with java -cp ... com.google.gwt.dev.Compiler -sourceLevel 17 -strict com.example.bad.Bad fails with one error per unsupported type (the compiler repeats the list in a second "Finding entry point classes" pass):

[ERROR] Errors in 'file:/.../com/example/bad/client/BadEntry.java'
   [ERROR] Line 10: No source code is available for type java.time.LocalDate; did you forget to inherit a required module?
   [ERROR] Line 12: No source code is available for type java.util.regex.Pattern; did you forget to inherit a required module?
   [ERROR] Line 11: No source code is available for type java.util.UUID; did you forget to inherit a required module?
   [ERROR] Line 14: No source code is available for type java.io.File; did you forget to inherit a required module?
   [ERROR] Line 13: No source code is available for type java.lang.Thread; did you forget to inherit a required module?
[ERROR] Hint: Check the inheritance chain from your module; it may not be inheriting a required module or a module may not be adding its source path entries properly

The hint is misleading for JRE types: no module can be inherited to obtain java.time.LocalDate; either avoid it, use a GWT replacement, or supply the class yourself with super-source (below). A class that is emulated but lacks a member produces a different message, because the type resolves and only the method does not:

[ERROR] Line 13: The method format(String, int) is undefined for the type String
[ERROR] Line 16: The method forName(String) is undefined for the type Class

Because some errors surface only in the second phase, read the whole list before fixing one by one, and compile with -strict (alias -failOnError, which the archetype turns on through <failOnError>true</failOnError>) so that a unit with errors is never silently skipped.

What is missing, and why

The page RefJreMissingEmulation.html (introduced with 2.13.0, whose notes describe it as improved tracking that documents unsupported APIs with links to issues) lists, per package, the methods, fields and inner classes that exist in the JDK but not in GWT, each linked to the GitHub issue that tracks the work or explains why it will not happen. It does not list top-level classes that are not emulated at all. The groups that matter most in practice are:

Table 2. Missing emulation by cause (issue numbers from the 2.13 page)
Tracking issue Theme Typical missing members

gwtproject/gwt#9630

Reflection

Class.forName, getMethod, getDeclaredFields, isInstance, cast, newInstance, Arrays.copyOf(Object[], int, Class), Collections.checkedList and friends

gwtproject/gwt#3946

Localization

String.format, String.formatted, PrintStream.printf, most of Locale (getDisplayName, getAvailableLocales, …​)

gwtproject/gwt#10211, #10212, #10213

System resources and properties

System.exit, System.getenv, System.setProperty, Integer.getInteger, Boolean.getBoolean

gwtproject/gwt#10221

Miscellaneous Java 17 omissions

InputStream.readAllBytes, Collections.emptySortedSet, Objects.checkIndex(long, long), System.lineSeparator(), Double.toHexString

gwtproject/gwt#9643, #9909

Unsigned arithmetic and newer Math

Integer.divideUnsigned, Integer.toUnsignedLong, Long.compareUnsigned, Math.multiplyHigh, Math.fma

gwtproject/gwt#1989

Unicode

code-point overloads of Character methods, Character.UnicodeBlock, Character.getType

gwtproject/gwt#10216

Raw bits

Double.doubleToRawLongBits, Float.floatToRawIntBits

gwtproject/gwt#10217, #10218, #611

Concurrent APIs, java.nio, java.time

ConcurrentHashMap.mappingCount, bulk operations; BitSet.valueOf(ByteBuffer); Date.toInstant()

Two practical rules follow. First, check RefJreMissingEmulation.html before adding a JDK 9-17 convenience method to shared code: a method that is new since Java 8 may exist (2.13.0 added Java 9+ methods to Integer, Long, Math, String, Objects, Collectors, Stream, IntStream and others) or may be tracked as missing. Second, anything in the "Reflection" group will not come back as such: GWT compiles to one script, can load code only through code splitting, and has no runtime class metadata beyond names. Use deferred binding, generators or annotation processors to produce what reflection would have found, as covered on Deferred Binding & Generators.

For dates and times the practical answer is java.util.Date plus com.google.gwt.i18n.client.DateTimeFormat (and the org.gwtproject.* or third-party java.time ports described on Ecosystem & Libraries) rather than java.time; for text formatting use NumberFormat, DateTimeFormat and Messages interfaces from Internationalization rather than String.format. The Java side of the same topics is on the Dates and Times, Strings and Text and Annotations and Reflection pages.

Supported Java language levels and features

The language level (syntax) and the library level (emulation) move independently: the compiler’s front end (JDT) accepts the syntax, while the library has to contain the classes the syntax relies on. The release notes give the history:

Table 3. Java support by GWT release (from the release notes and the 2.13.1 sources)
GWT Language and library support

Before 2.8.0

Java 7 or lower syntax; no lambdas

2.8.0 (2016)

Source level Java 8: lambdas, method references, default and static interface methods; "partial support for Java 8 standard library APIs" (Optional, streams, java.util.function). Interface default and static methods are not visible to generators

2.9.0

Adds language levels 9, 10 and 11; tooling (compiler, server code) is tested on Java 8, 11 and 14

2.10.0 / 2.11.0

Library level raised to Java 11 for collections, streams and more (List.of, Set.of, Map.of, Optional.or, Collectors.toUnmodifiableList, Stream.takeWhile/dropWhile, Predicate.not); 2.11.0 is tested on Java 21

2.12.0

Syntax of Java 12 to 17: text blocks, instanceof patterns, records, switch expressions; String methods of Java 11, Normalizer, Java 10 collection methods; the minimum Java version for the dev tools is 11

2.13.0 / 2.13.1

SourceLevel has JAVA8, JAVA9, JAVA10, JAVA11 and JAVA17; more Java 9+ methods in Integer, Long, Math, String, Objects, Stream, Collectors; 2.13.1 fixes record equals for null components. The release is "likely to be the last where the compiler and dev tools run on Java 11"

Planned

The roadmap states the goal of raising both the supported language features and the JRE emulation to Java 21; Java 18-21 syntax (for example pattern matching in switch) is not available in 2.13.1

The compiler flag is -sourceLevel (values auto, 1.8, 9, 10, 11, 17 in the 2.13.1 help text). The gwt-maven-plugin exposes it as <sourceLevel>, whose default is the user property maven.compiler.source; the archetype-generated parent POM sets it explicitly next to failOnError:

<plugin>
  <groupId>net.ltgt.gwt.maven</groupId>
  <artifactId>gwt-maven-plugin</artifactId>
  <version>1.3.0</version>
  <extensions>true</extensions>
  <configuration>
    <sourceLevel>17</sourceLevel>
    <failOnError>true</failOnError>
  </configuration>
</plugin>

The command-line compiler’s default is 1.8 (the default argument registered in ToolBase; the -help text and the Developer Guide are right), so Java 12 to 17 syntax needs an explicit -sourceLevel 17. The value auto resolves to the running JVM’s level. Set it explicitly in the build, and keep the release setting of maven-compiler-plugin and the GWT sourceLevel equal so javac (server and tests) and the GWT front end accept the same code.

The following shared class uses Java 17 syntax and compiled and ran in the browser build and on the JVM:

package com.example.probe.shared;

public final class Pricing {
  private Pricing() {}

  public static String label(Object o) {
    if (o instanceof Integer i && i > 10) {      // instanceof pattern (2.12.0)
      return "many:" + i;
    }
    String kind = o == null ? "null" : o.getClass().getSimpleName();
    return switch (kind) {                        // switch expression (2.12.0)
      case "null" -> "no value";
      case "String" -> "text";
      default -> "other:" + kind;
    };
  }
}

Records (record Copy(String title, int n) {}), text blocks, var, and sealed hierarchies with permits also compiled at -sourceLevel 17; in the browser a record printed as Copy[title=Emma, n=2] and its generated equals returned true, as on the JVM. What does not compile is Java 21 pattern matching in switch. With level 17 the compiler reports:

[ERROR] Line 18: The preview feature Pattern Matching in Switch is only available with source level 19 and above
[ERROR] Line 17: Cannot switch on a value of type Object. Only convertible int values, strings or enum variables are permitted

Use instanceof patterns and if/else chains until the supported level moves. The syntax itself is the ordinary Java covered in the Records and Sealed Classes, Pattern Matching, Lambdas and Method References, Generics and Streams and Collectors pages; the point here is only whether the GWT version and its library accept it. Two details are specific to GWT:

  • Generators cannot see Java 8 default and static interface methods; this is why the notes recommend annotation processors over generators for new code.

  • Anything generated at run time by libraries that depend on reflection (annotation-driven dependency injection, serialisation by reflection, Jackson without a GWT adapter) has no equivalent; the replacements are covered on Ecosystem & Libraries.

Language semantics that differ in the browser

The Developer Guide’s "Language support" section lists the places where the JavaScript target leaks through. Checked against 2.13.1:

  • Exceptions the JVM raises implicitly (NullPointerException, StackOverflowError, OutOfMemoryError) do not occur as such. Dereferencing null threw com.google.gwt.core.client.JavaScriptException, so never catch (NullPointerException e) to detect a null. A bad cast threw java.lang.ClassCastException, as expected.

  • Array indexes and integer division by zero are not checked. Writing arr[2] of a one-element int[] raised nothing, and 1 / 0 on int produced 0 rather than ArithmeticException.

  • Assertions are removed unless the compiler runs with -checkAssertions (the Developer Guide still names -ea).

  • Threads: synchronized is accepted and ignored; Object.wait/notify/notifyAll make the compile fail; Thread itself does not exist. Asynchrony is callbacks, Scheduler and Timer.

  • Reflection: Object.getClass().getName() and getSimpleName() work (com.example.probe.client.ProbeEntry$Copy and Copy were printed), but not much more. -XclassMetadata/-XnoclassMetadata controls whether those names are kept.

  • Finalizers, strictfp and Throwable.getStackTrace() have no meaningful implementation in production mode.

Numbers: int, long, float and double

JavaScript has one numeric type, the 64-bit floating point number. GWT builds the Java primitives on top of it, and the differences are the main source of bugs in code that was written for the JVM and is later shared. The probe below runs the same statements in the browser (2.13.1, production-style output) and on JDK 21. The operands come from Integer.parseInt/Long.parseLong on purpose: the compiler folds constant expressions (Integer.MAX_VALUE + 1 printed -2147483648 in the browser only because it was folded at compile time).

int r1 = Integer.parseInt("2147483647");
int r2 = Integer.parseInt("1");
p("runtime MAX + 1", r1 + r2);
p("runtime MAX + 1 (|0)", (r1 + r2) | 0);
p("runtime 46341*46341", Integer.parseInt("46341") * Integer.parseInt("46341"));
p("runtime 1 << 33", r2 << Integer.parseInt("33"));
p("runtime byte 127+1", (byte) (Byte.parseByte("127") + r2));
p("runtime long MAX+1", Long.parseLong("9223372036854775807") + Long.parseLong("1"));
p("runtime long mul", Long.parseLong("4294967296") * Long.parseLong("4294967296"));
p("runtime (int) long", (int) Long.parseLong("4294967297"));
Table 4. Observed results (browser vs JDK 21)
Expression Browser JVM Reading

int MAX_VALUE + 1

2147483648

-2147483648

int addition does not wrap

(r1 + r2) | 0

-2147483648

-2147483648

the | 0 idiom restores 32-bit wrapping

46341 * 46341

2147488281

-2147479015

int multiplication does not wrap either (65536 * 65536 + 1 with runtime operands gave 4294967297 in a second probe)

1 << 33

2

2

shifts are masked to 5 bits as in Java

(byte) (127 + 1)

-128

-128

narrowing casts do wrap

long MAX_VALUE + 1

-9223372036854775808

-9223372036854775808

long overflow is emulated

4294967296L * 4294967296L

0

0

ditto

(int) 4294967297L

1

1

long to int truncates correctly

31 * h + c hash loop over a 27-character string

6.851451984453826e+40

-817261598

unwrapped int multiplication accumulates beyond 2^53

The rules that explain the table:

  • byte, short, char and int are doubles. Overflow of int arithmetic is not wrapped (the Developer Guide says the same about overflowing integral types). The cure for code that depends on 32-bit wrapping (hashes, checksums, random number generators, Math.abs(Integer.MIN_VALUE), which printed 2147483648) is an explicit | 0 or the library methods, which already contain it; String.hashCode() on the browser agreed with the JVM ("title".hashCode() is 110371416 on both). Division is correct: 5 / 2 is 2 and -5 / 2 is -2, -7 % 3 is -1.

  • long is emulated and is correct over the full 64-bit range. In 2.13.1 the representation is hybrid (com.google.gwt.lang.LongLib): values whose magnitude is below 2^44 are held as a plain JavaScript number and operated on with double arithmetic, and larger values are held as three chunks of 22, 22 and 20 bits (the l, m, h fields visible in the generated code) with slower arithmetic. Counting loops over ids and timestamps mostly stay in the cheap representation; heavy 64-bit bit manipulation does not. The generated function for Pricing.cents is the call mul_0(units, fromInt_0(unitCents)), not a native *.

  • long and JavaScript do not mix freely. The Developer Guide notes that long cannot be used in JSNI because it is not a native JavaScript number; keep exported or native signatures to int, double or String and see JsInterop for the rules.

  • float is a double. 0.1f + 0.2f printed 0.30000000447034836 in the browser while the JVM prints 0.3: the extra precision is visible whenever a float is converted to text or compared exactly. Use double (or BigDecimal) deliberately in shared code.

  • double formatting and hashing differ in places. Double.toString(1e21) printed 1.0E21 and Double.toString(100.0) printed 100.0, as in Java, but Double.valueOf(1.5).hashCode() is 1 in the browser (the emulation returns (int) d) against 1073217536 on the JVM. Do not persist or compare hash codes of doubles across client and server. Long.hashCode matched the JVM (705032705 for 5000000000L).

  • Casting (int) 3.9e10 saturates to 2147483647, as in Java; Math.round(-2.5) gave -2, as in Java.

These semantics are why the Bookshelf Book of the scenario keeps a long id and an int copies and does no arithmetic on them beyond counting and comparing; a checksum (an ISBN check digit, a cache key) belongs in a method with explicit | 0 or in a long. The Java-side background is on the Primitive Types and Variables, Operators and Expressions and Numbers and Math pages.

Runtime checks you can tune: jre.checks

Separately from language semantics, the library contains defensive checks (index bounds in ArrayList, iterator state, argument and type checks) controlled by configuration properties defined in Preconditions.gwt.xml: jre.checks.checkLevel (NORMAL, OPTIMIZED or MINIMAL, default NORMAL) and the finer jre.checks.bounds, jre.checks.api, jre.checks.numeric and jre.checks.type (each AUTO, ENABLED or DISABLED), plus jre.checkedMode. The compiler flag -XcheckCasts is marked deprecated in favour of jre.checks.checkLevel. Compiling the probe with -setProperty jre.checks.checkLevel=MINIMAL changed this line:

NORMAL  : ArrayList.get(3) throws = java.lang.IndexOutOfBoundsException: Index: 3, Size: 0
MINIMAL : new ArrayList().get(3) = undefined

while Integer.parseInt("x") still threw NumberFormatException: For input string: "x" and List.of(1).add(2) still threw UnsupportedOperationException. The whole probe’s single permutation file went from 37,223 bytes (NORMAL) to 32,343 bytes (MINIMAL) with -style OBF. Lower levels trade diagnostics for size; use them only for a release build that is covered by tests at NORMAL, and never to hide a bug: with checks off the code above does not fail, it silently returns undefined.

Strings and regular expressions

String is the JavaScript string, and the emulation re-implements the Java methods on top of it. Most methods behave as in Java (substring, indexOf, replace, trim, toUpperCase gave STRASSE for "stra\u00dfe" as on the JVM); the differences are concentrated in four places.

Regular expressions are JavaScript regular expressions. String.matches, replaceAll, replaceFirst and split hand the pattern to the browser’s RegExp. The Javadoc comment of the emulated split says the same and advises using only the subset of regular expression syntax common to Java and JavaScript. Observed with Chrome:

Table 5. Java regex constructs in the browser
Construct Browser result JVM result

"a1b22c".split("[0-9]+"), "a.b".replaceAll("\\.", "-")

[a, b, c], a-b

the same

"John Smith".replaceAll("(\\w+) (\\w+)", "$2, $1")

Smith, John

the same

"AbC".matches("(?i)abc")

JavaScriptException: (SyntaxError) : Invalid regular expression: /^(?:(?i)abc)$/: Invalid group

true

"aaa".matches("a++") (possessive quantifier)

JavaScriptException: (SyntaxError) : Invalid regular expression: /^(?:a++)$/: Nothing to repeat

true

"ab".matches("\\p{Alpha}+")

false (no error: \p is a literal p without the Unicode flag)

true

"x".matches(""), "a".matches("\\Qa\\E")

false

false for the POSIX class (it is a character set), true for \Q...\E

The dangerous cases are the silent ones (\p{Alpha}, \Q..\E, character-class intersections [a-z&&[^bc]], lookbehind support that depends on the browser), which return a wrong answer rather than failing. Keep regular expressions in shared code to the common subset, put them in one class, and test that class both on the JVM and with a GWTTestCase (see Testing). The Java-side syntax is explained on the Regular Expressions page.

The portable alternative is com.google.gwt.regexp.shared.RegExp. It is in the module com.google.gwt.regexp.RegExp, has a JavaScript-style API (compile(pattern), compile(pattern, flags), exec, test, replace, split, quote, getGlobal, …​) and exists in two implementations: on the browser it calls the native RegExp; on the JVM it is backed by java.util.regex.Pattern, and it translates the JavaScript replacement syntax ($&, $$, backslashes) for Java. Its Javadoc warns that Java-only constructs such as [a-z&&[^bc]], (?<=foo), \A and \Q work only in the JVM implementation and are rejected by neither. The shared validation class used in the Bookshelf (for the ISBN field of the book editor) is therefore:

package com.example.probe.shared;

import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;

/** Shared validation: runs unchanged in the browser and on the server. */
public final class Isbn {
  private static final RegExp ISBN13 =
      RegExp.compile("^(97[89])-(\\d{1,5})-(\\d{1,7})-(\\d{1,7})-(\\d)$");

  private Isbn() {}

  public static String describe(String text) {
    MatchResult m = ISBN13.exec(text);
    return m == null ? "invalid" : "prefix " + m.getGroup(1) + ", check digit " + m.getGroup(5);
  }

  public static String hyphenated(String compact) {
    return RegExp.compile("(\\d{3})(\\d)(\\d{4})(\\d{4})(\\d)")
        .replace(compact, "$1-$2-$3-$4-$5");
  }
}

Both sides printed prefix 978, check digit 7, invalid and 978-0-5965-2068-7 for the inputs 978-0-596-52068-7, 12-34 and 9780596520687. For the module, add <inherits name="com.google.gwt.regexp.RegExp"/>; on the server the classes come from the GWT library jar (they are in gwt-servlet-jakarta-2.13.1.jar as well as in gwt-user).

String.split follows Java 8 semantics since 2.13.0. The release notes list it as a breaking change. The implementation now discards a zero-length match at the start of the input, and trailing empty strings are removed unless the limit is negative. Observed on 2.13.1 and identical on the JVM:

"a,b,,".split(",")        = [a, b]
",a,b".split(",")         = [, a, b]
"abc".split("")           = [a, b, c]
"abc".split("", -1)       = [a, b, c, ]
"".split(",")             = []        (length 1: the single empty string)

Code written against earlier GWT versions that relied on a leading empty element from a zero-width split must be adjusted when upgrading. The same release makes Collectors.toMap throw when a value is null (the JVM behaviour, although not what its Javadoc says) and removes the AccessControlException emulation.

Characters and locales are ASCII-oriented. The Character emulation says several methods "do not properly handle characters outside of ASCII": isLetter, isDigit, isLetterOrDigit, isLowerCase, isUpperCase, and Character.isLetter('\u00e9') returned false in the browser (it is true on the JVM). Use RegExp with the right flags, or compare against explicit ranges, for user-facing text such as the author names of the Bookshelf, and use the i18n classes for case and number formatting.

Formatting is not String.format. String.format, formatted and printf are missing (the localization issue above); use NumberFormat, DateTimeFormat, Messages interfaces and SafeHtml templates.

Writing shared code

Shared code is Java that must compile twice: with javac against the full JDK for the server, and with the GWT compiler against the emulation for the browser. The Bookshelf keeps it in its own Maven module, bookshelf-shared, whose jar is also published with the sources classifier because the GWT compiler needs source files:

<!-- bookshelf-shared/pom.xml (excerpt) -->
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-source-plugin</artifactId>
    </plugin>
  </plugins>
</build>

<!-- bookshelf-client/src/main/module.gwt.xml (excerpt) -->
<module>
  <inherits name="com.google.gwt.user.User" />
  <source path="client" />
  <source path="shared" />
  <entry-point class="com.example.bookshelf.client.App" />
</module>

The module file lives in the package com.example.bookshelf, so <source path="shared"/> means com.example.bookshelf.shared, found in the sources jar of bookshelf-shared. If that jar was not built, the client compile fails, for example when running mvn test -pl bookshelf-client -am without package:

No source code is available for type com.example.bookshelf.shared.Book; did you forget to inherit a required module?

Use mvn verify (or install) so the sources jar exists before the client is compiled.

The discipline for shared classes is short:

  • Only use what both sides have. Restrict shared classes to the emulated subset: no reflection, java.time, java.util.regex, String.format, threads, files or javax/jakarta classes. A server import in a shared file is a client compile error, and the other way round the server may use the full JDK only in server-only classes.

  • No dependence on the numeric and string deviations above. Hash codes of double, unwrapped int arithmetic, regex flavours and float text must not be part of a contract between client and server.

  • Serialisation types are shared types. Classes sent through GWT-RPC (such as Book and BookPage) implement java.io.Serializable or IsSerializable, have a default constructor and use only serialisable field types; that rule belongs to GWT-RPC.

  • Keep a JVM-only escape hatch inside the class rather than in a copy. Two mechanisms exist: @GwtIncompatible, and GWT.isClient()/GWT.isScript() checks.

@GwtIncompatible (com.google.gwt.core.shared.GwtIncompatible; a copy under any other package works because only the simple name matters) removes the annotated type, method, constructor or field from what the GWT compiler sees, while javac still compiles it for the server. Its Javadoc warns that removing members this way can have surprising effects together with method overloading or inheritance. The probe’s Pricing.today() uses java.time and was ignored by the browser build, which compiled without error:

import com.google.gwt.core.shared.GwtIncompatible;

public final class Pricing {
  @GwtIncompatible("uses java.time, which is not emulated")
  public static String today() {
    return java.time.LocalDate.now().toString();
  }
}

When the behaviour differs but both versions must exist in the same compilation unit, ask the environment. com.google.gwt.core.shared.GWT is the class made for shared code; it also exists on the JVM, where isClient(), isScript() and isProdMode() return false and getVersion() returns null:

import com.google.gwt.core.shared.GWT;

public final class Environment {
  public static String describe() {
    return GWT.isScript() ? "browser, compiled JavaScript"
        : GWT.isClient() ? "browser, Dev Mode or test"
        : "plain JVM (server or JUnit)";
  }
}

Running the shared GWT calls in a plain main on JDK 21 printed false false false null for isClient, isScript, isProdMode and getVersion, and GWT.create(Runnable.class) threw:

java.lang.UnsupportedOperationException: ERROR: GWT.create() is only usable in client code!  It cannot be called, for example, from server code.  If you are running a unit test, check that your test case extends GWTTestCase and that GWT.create() is not called from within an initializer or constructor.

Prefer @GwtIncompatible or super-source over runtime branching for anything that pulls in a non-emulated class: a GWT.isClient() branch still has to compile on both sides, whereas the @GwtIncompatible member is not seen by GWT at all, and the optimiser removes unreachable branches of isScript() checks only after the types in them have been resolved.

super-source: supplying your own emulation

The <super-source> module element "re-roots" a source path, so that files below it replace or add classes as if the directory prefix were not there (the module XML elements are covered in Project Structure & Modules). The Developer Guide’s own example is adding java.util.UUID: put the file in com/example/myproject/jre/java/util/UUID.java, add <super-source path="jre"/> to MyProject.gwt.xml, and the compiler sees it as java/util/UUID.java. GWT uses the same technique for its own emulation. The same mechanism lets shared code have a different implementation in the browser than on the server without changing the class’s name or callers:

<!-- com/example/probe/Probe.gwt.xml -->
<module rename-to="probe">
  <inherits name="com.google.gwt.user.User"/>
  <source path="client"/>
  <source path="shared"/>
  <super-source path="super"/>
  <entry-point class="com.example.probe.client.ProbeEntry"/>
</module>
// com/example/probe/shared/Platform.java  (used by javac for the server)
package com.example.probe.shared;

public class Platform {
  public static String describe() {
    return "JVM: " + System.getProperty("java.version");
  }
}

// com/example/probe/super/com/example/probe/shared/Platform.java  (seen by the GWT compiler instead)
package com.example.probe.shared;

import com.google.gwt.core.client.GWT;

public class Platform {
  public static String describe() {
    return "Browser: " + GWT.getModuleName();
  }
}

In the browser Platform.describe() returned Browser: probe; on the JVM the same class name returned the JVM: ... text. The two files have the same fully qualified name com.example.probe.shared.Platform, so callers (here ProbeEntry) and the shared module need no change. This is exactly how com.google.gwt.regexp.shared.RegExp is built: a pure Java version in the shared path for the server, a super-sourced version that calls native JavaScript for the browser (its module declares both <source path="shared"/> and <super-source path="super"/>).

Rules and caveats:

  • The path is relative to the module file and the whole subtree is re-rooted, so the package directories must be repeated below it (super/com/example/probe/shared/).

  • If both a normal source and a super-source define the same class, the super-source wins for the GWT compiler. javac must not compile the super-source files (they would clash with the real class), so keep them outside the Java source directories: the default layout of gwt-maven-plugin reserves src/main/super/ (and src/test/super/) for them, used by the goals gwt:add-super-sources and gwt:add-test-super-sources, and the files have to end up in the sources jar so the client build can see them.

  • Use it to provide a class that GWT lacks (a minimal UUID, a Pattern facade), or to give shared classes a browser-specific body. Do not use it to patch GWT’s own emulation of a class that exists unless you accept maintaining it across releases.

  • In classic Dev Mode (history, removed from the workflow of the Super Dev Mode era) the native JRE classes always superseded source for java.*, so such overrides were never exercised there. Super Dev Mode compiles to JavaScript and does use them, so a test that runs in a browser or with GWTTestCase is the check.

  • The mechanism is GWT-specific; J2CL uses a different convention for native overrides (see J2CL & the Future), so code that depends on super-source needs a second implementation when moving to J2CL.

The GWT class API

The "GWT Class API" reference page, RefGWTClassAPI.html, is a short pointer: it only introduces the class library (user interface, RPC, internationalisation, unit testing) and links to the Javadoc; the Javadoc is the actual reference. The class that every application meets first is com.google.gwt.core.client.GWT; com.google.gwt.core.shared.GWT is its JVM-safe base for shared code. The members below were checked in the 2.13.1 sources and, where marked, run:

Table 6. Members of GWT (2.13.1)
Member Behaviour

static <T> T create(Class<?> classLiteral)

Deferred binding: the argument must be a class literal, replaced at compile time by new of the rebound type (GWT.create(CatalogService.class) yields the generated RPC proxy). On the JVM it throws the UnsupportedOperationException quoted above

isClient()

true in Dev Mode and Production Mode, false in a plain JVM or during the bootstrap of a GWTTestCase. Replaced by true by the compiler

isScript()

true when running as compiled JavaScript (it printed true), false in bytecode (Dev Mode, JUnit)

isProdMode()

true in compiled JavaScript, false in Dev Mode and on the JVM

getModuleName()

the running module’s (renamed) name; the probe printed probe

getModuleBaseURL()

the URL prefix for module-relative resources such as RPC entry points; guaranteed to end with / when non-empty (the probe printed http://127.0.0.1:8473/probe/)

getModuleBaseForStaticFiles()

prefix for static files produced by the compiler; normally equal to the module base, different when static resources are served from another server

getHostPageBaseURL()

the URL prefix of the host page (http://127.0.0.1:8473/)

getPermutationStrongName()

the permutation’s hash (32 hexadecimal characters in the probe); HostedMode in Dev Mode (HOSTED_MODE_PERMUTATION_STRONG_NAME)

getVersion()

the GWT version (2.13.1 in the probe); on the JVM through the shared class it is null

log(String) and log(String, Throwable)

to the Dev Mode shell, or to the browser console in Super Dev Mode; calls are optimised out of production output

setUncaughtExceptionHandler, getUncaughtExceptionHandler, reportUncaughtException(Throwable)

the handler for exceptions about to escape to the browser from event callbacks, timers and RPC. null by default in Production Mode (exceptions go to the console); in Dev Mode the default logs through GWT.log

runAsync(RunAsyncCallback) and runAsync(Class<?>, RunAsyncCallback)

code-splitting split point; in plain bytecode it simply schedules the callback. Covered with Code Splitting

debugger()

emits a JavaScript debugger statement; no effect in Dev Mode or server code

getUniqueThreadId()

"" in Production Mode; a per-window id in Dev Mode

getTypeName(Object)

deprecated; use getClass().getName()

UncaughtExceptionHandler

nested interface, onUncaughtException(Throwable)

The Bookshelf entry point uses several of them in a few lines:

public class App implements EntryPoint {
  @Override
  public void onModuleLoad() {
    GWT.setUncaughtExceptionHandler(e -> {
      GWT.log("Uncaught", e);                  // console in Super Dev Mode, removed in production
      Window.alert("Something went wrong; please reload the page.");
    });
    // Defer the rest so that exceptions in it reach the handler.
    Scheduler.get().scheduleDeferred(() -> {
      CatalogServiceAsync catalog = GWT.create(CatalogService.class);
      ((ServiceDefTarget) catalog).setServiceEntryPoint(GWT.getModuleBaseURL() + "catalog");
      if (!GWT.isProdMode()) {
        GWT.log("Running " + GWT.getModuleName() + " on GWT " + GWT.getVersion());
      }
    });
  }
}

@RemoteServiceRelativePath removes the need for setServiceEntryPoint in almost every case; the explicit form is shown because it is where getModuleBaseURL() is used. A user-facing message in the handler must not include the exception text, which may contain internal details.

Outdated material in older books and posts

Older GWT material, including the first-edition chapters on the generated JavaScript and the JRE emulation library (GWT 1.3 and 1.4, 2007), describes a world with these differences:

Table 7. Outdated statements and what holds in 2.13.1
Older statement Today

Client code must be written to Java 1.4: no generics, enums, annotations or varargs; set the IDE to 1.4 compatibility

Java 8 syntax since 2.8, Java 17 syntax since 2.12 (-sourceLevel 17); server code was never restricted

long is a 64-bit floating-point value with reduced range

long has been emulated correctly since GWT 1.5 and is hybrid-represented in 2.13.1

The emulation library covers java.lang and java.util basics

it now includes streams, Optional, java.util.function, java.util.concurrent interfaces, java.math, java.util.logging and Java 9-17 additions; the exact list is generated and published per release

The reference to check supported classes is a single list

two pages: RefJreEmulation.html (supported) and RefJreMissingEmulation.html (unsupported, with issue links)

JUnit 3 GWTTestCase and hosted mode to test shared code

JUnit 4 GWTTestCase with HtmlUnit, plain JUnit for the JVM side

Pitfalls

Table 8. Common problems with the emulation and shared code
Symptom Cause Fix

No source code is available for type java.time.LocalDate; did you forget to inherit a required module?

The class is not in the emulation

Use Date plus DateTimeFormat, a port, or a super-source class; do not inherit a module

The method format(String, int) is undefined for the type String

Member missing from an emulated class (localization, issue #3946)

NumberFormat, DateTimeFormat, Messages

Invalid regular expression: /^(?:(?i)abc)$/: Invalid group raised only in the browser

Java regex syntax passed to the JavaScript engine

Use the common subset, RegExp with flags, or simplify the expression

Property 'java.version' is not defined.

System.getProperty accepts only properties defined by the module (for example user.agent) and the compiler resolves the name at compile time

Do not read JVM properties on the client; define a configuration property in the module

The preview feature Pattern Matching in Switch is only available with source level 19 and above

Java 21 syntax at -sourceLevel 17

instanceof patterns or if chains

No source code is available for type com.example.bookshelf.shared.Book

The sources jar of the shared module was not built or the module does not declare <source path="shared"/>

mvn verify/install; check the <source> element and the maven-source-plugin

Hash, checksum or random generator gives different values on client and server

Unwrapped int multiplication; double.hashCode

| 0, or use long and the library hash methods

catch (NullPointerException) never runs in production

A null dereference becomes JavaScriptException

Check for null explicitly

A [a-z&&[^bc]] or \p{Alpha} pattern "works" on the server and silently matches nothing in the browser

Java-only regex constructs

Keep shared patterns in the common subset and test in both runtimes

Code works with checks and returns undefined or garbage after a size-optimised build

jre.checks.checkLevel=MINIMAL removes bounds and API checks

Test at NORMAL; use lower levels only deliberately

What changed recently

  • 2.8.0: Java 8 syntax and the first Java 8 library APIs; classic Dev Mode deprecated; JSNI methods may no longer reference methods of Double, String and Boolean.

  • 2.9.0: language levels 9 to 11; Elemental deprecated in favour of Elemental2.

  • 2.11.0: library at Java 11 level for collections, streams and more; Jakarta artifacts for GWT-RPC and RequestFactory.

  • 2.12.0: Java 12-17 syntax, -XfragmentMerge without effect (its handler was removed in 2.13.0), Normalizer, Java 11 String methods.

  • 2.13.0: String.split Java 8 semantics, Collectors.toMap throws on null values, AccessControlException emulation removed, Java 9+ methods in Integer, Long, Math, StrictMath, String, BigInteger, BitSet, Objects, streams and Collectors, and the new missing-emulation reference page.

  • 2.13.1: record equals compares fields with Objects.equals so null components work.

What’s Changed & Migration lists the changes across the whole section.