type usages - jspecify/jspecify GitHub Wiki

This page describes type usages in the manner adopted by the JSpecify project. It's largely based on Java Language Specification 4.11, but there are a few intentional discrepancies.

The short answer

You never actually see types themselves in code, only their usages. (Well,and the classes that define those types.)

Informally, a type usage is the appearance of a Java type as a variable type, return type, type argument, supertype after extends, the type in an instanceof or cast expression... etc.

As definitions go, this is awfully informal. But type usages are simply easier to understand by example. And we'll get to those soon. But first:

Wherever a program uses a value (like 42, or a reference to the object "foo") in some way, there is always a known static type. This static type serves to constrain which values are acceptable. It might be explicit in the source code, or implicit and determined by the compiler, but either way it is deterministic at compile time.

No value will "get through" that point in the code unless it conforms to that static type (setting aside heap pollution). In most cases the static type performs this gatekeeping role directly; in other cases (like a class declaration specifying a superclass) it supports the same purpose more indirectly.

Static types come in three main kinds:

  1. the implicit type of each non-void expression
  2. the implicit expected-type of each expression context (anything that contains an expression)
  3. type usages (usually explicit)

In this example:

@Override Boolean test(Collection<?> c) { return c.size() == 0; }

All three kinds can be seen (respectively):

  1. the type of the expression c.size() == 0 (boolean)
  2. the type the return statement expects for its argument (Boolean)
  3. the return type in the method declaration (Boolean)

The point of this page is to answer: what is kind 3?

Type usages

Since the static type of an expression or expression-context is always implicit, it's tempting to say that a type usage is any explicit appearance of static type in code. And that's approximately right! However, type usages can still be implicit: in var s = ""; and in class Foo {} we have type usages of String and Object, even though String s and extends Object don't appear explicitly.

So, alas, a proper "dictionary definition" still eludes us. A type usage is by nature explicit, but still can be elided in certain cases. So if either it's explicit, or it could've been explicit, it's probably a type usage. Again, the examples below are what will help most.

Type usages are easily confused with class usages, such as import statements; discussed later.

Kinds of type usages

A type usage appears either directly in a type context (like a particular method's return type), or as a type component of some other type (which itself sits at some depth within a type context).

Just as an "expression context" is a "slot" that an expression fills in, a type context is a place where a type usage can appear. Anything that sits in a type context is a type usage. We'll now list all the kinds of type contexts (as of Java 19).

In signatures / APIs

The type of a...

  • field declaration (for an enum constant, this is implicit)
  • method return type (if it has one)
  • parameter of a method or constructor (treating ... as [])
  • record component (since it serves as all of the above at once)
  • supertype in a class declaration (by default, Object)
  • upper bound in a type parameter declaration (by default, Object)
  • thrown exception (i.e., after throws in a signature)
  • "receiver parameter" (vanishingly rare)

In implementation code (statements and expressions)

Implementation code is the contents of a method body, constructor body, or initializer. Local classes are excluded, except the parts that themselves meet this definition, excluding their local classes... you get the idea. Implementation code is made up of statements and expressions, and all statements and expressions appear within implementation code.

The type...

  • of a variable declaration (local variable, exception parameter, or lambda expression parameter)
  • in a cast or instanceof expression
  • after new or before ::new (class or array type to instantiate, or supertype for an anonymous class)
  • of a type argument supplied to a generic method invocation or class instance creation expression (e.g., in Lists.<String>of()), including via member reference
  • in an unbound method reference to an instance method (e.g. the String in String::toUpperCase)

Footnote: Resource variables and pattern variables are considered local variables.

The other kind of type usage is as a type component within some broader compound type (which in turn sits, directly or indirectly, in a type context).

Kind of compound type Example Has these direct type components Example
array type String[][] its component type String[]
parameterized type Map<K, ? extends Number> each non-wildcard type argument or wildcard bound K, Number
inner type Foo<Bar>.Qux its outer type Foo<Bar>

Every type is also a type component of itself. So, technically every type usage is of a type component.

A type component is always a complete type: the components of String[] are not String and []; they are String and String[]. Likewise, the type components of the inner type Foo<Bar>.Qux are Bar, Foo<Bar>, and Foo<Bar>.Qux; Qux by itself is not a valid type and so is not among them.

Footnote: The kinds of compound types listed are not disjoint: a type can have type arguments and be an inner type at the same time.

Footnote: we're glossing over intersection types, which are complicated, and non-denotable anyway.

Type structure

Every type, thereby, has a built-in tree structure.

We use the term root type to refer to the "entire" type that sits directly in a type context, not serving as a type component of any type other than itself. For example, in public List<String[]> foo();, type usages of String and String[] are present, but only List<String[]> itself is the root type.

A type that is not compound is a simple type. It's usually represented as a single token (though possibly name-qualified, and Foo<?> is an apparent exception). But there's no meaningful difference between a simple type and a compound type; just whether its tree depth is one, or greater than one.

Footnote: Because Map.Entry is a static nested class, not an "inner class", the Map in Map.Entry<Foo, Bar> is not a type component; it's covered in the next section on class usages.

Class usages

Declaring a class automatically defines a type. That type can then have type usages of the above kinds (even if, like System, it would rather not!). But the class can also have class usages, where it is used as just a class. The common examples are:

  • in an import statement
  • qualifying a static member access
  • the class name in a constructor declaration
  • after the @ sign in an annotation

These are not type usages, and can't bear type-use annotations. (Just imagine if the last case could!)

Distinguishing type usages from class usages

Yet again, this is tough to define precisely! We have only guidelines:

If... it's probably... but there are exceptions
a compound type is allowed a type usage String[].class has only a class usage
a compound type is not allowed a class usage a thrown or caught exception type is a type usage
a non-instantiable class would be unexpected a type usage a qualified .this/.super is a class usage
a non-instantiable class makes sense a class usage (unknown)
a primitive type is allowed a type usage int.class is sort of a class usage

Other TYPE_USE annotation targets

An annotation marked with @Target(TYPE_USE) can be attached to any kind of explicit type usage listed above. But, confusingly, it also automatically lets it be used in a few other places:

  • On a class or type parameter declaration. But this declaration doesn't use a type; it defines one.
  • On a wildcard. Not a type usage, however, the bounds of the wildcard (if any) are.
  • On a constructor declaration. This one is, at least, a class usage. But a constructor doesn't have a "return type"; that type gets assembled using type arguments determined at the caller's side (which, accordingly, does constitute a type usage).

While JSpecify type-use annotations can be placed in these locations, they are defined to have no meaning in that case. If an annotation is intended to go on a type parameter, for example, we would list @Target(TYPE_PARAMETER) explicitly, even though it is technically redundant.

Footnote: JLS describes certain constructs as "denoting the use of a type", but neither explains what the phrase means nor ever uses it again. These are: unbounded wildcards, constructor declarations, and the ... in a varargs parameter declaration. This is irrelevant for our purposes: the first two are already excluded above, and we address the third case differently, by considering <type> ... to be an implicit type usage of <type> [].

Footnote: one more location where type-use annotations have been permitted is considered a bug.


TODO re "three main kinds" near the top: type members are post-substitution, and the types in their signatures seem like static types too. In class StringList implements List<String> { @Override public NotAString get(int i) { ... } }, the NotAString is checked against such a computed type (and fails). Does it constitute a type usage?? Other cases too?

⚠️ **GitHub.com Fallback** ⚠️