Skip to content

type usages

Kevin Bourrillion edited this page Dec 6, 2024 · 59 revisions

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

A type usage happens wherever some type is being indicated: in a variable declaration, return type, cast expression, etc.

In var s = ""; and in class Foo {} we have implicit type usages of String and Object. Whether we care about including implicit type usages as "real" type usages depends on the context.

The compiler computes a deterministic static type for each expression and expression context. Although these also "use" the type in some sense, they aren't considered type usages. (Another example: in class StringList implements List<String> { @Override public NotAString get(int i) { ... } }, the NotAString is checked against such a computed type.)

Kinds of type usages

A type usage always appears in a type context, either directly or as a type component of some other type expression.

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. Generally, anything that sits in a type context is a type usage. We'll now list all the kinds of type contexts (as of Java 22).

In API elements

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 ... the same as [])
  • record component (since it serves as all three of the above at once)
  • supertype in a class or type parameter declaration (by default, Object)
  • thrown exception (after throws in a signature)

In statements and expressions

The type...

  • of a variable declaration (local variable, exception parameter, or lambda expression parameter; if a type is given)
  • in a cast, or instanceof used as the type comparison operator
  • 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)
  • in a type pattern that declares a pattern variable

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>

A type contains another type if it has that type as one of its type components.

Every type contains 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 inner type Foo<Bar>.Qux contains 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.

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 contained by another type. 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 token Map in Map.Entry<Foo, Bar> is not a type component; it's covered in the next section on class usages.

NOT type 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.

  1. in an import declaration (either as the class to import, or qualifying it)
  2. as name-qualification of a static member (including a static nested class)
  3. in a class-typed class literal, before .class
  4. in a constructor declaration, before (
  5. for disambiguation before .this or .super
  6. in an annotation, after @
  7. as the record class name in a record pattern, before (
  8. in a sealed class header, after permits
  9. in a module declaration, after uses/provides/with

Class usages are not type usages.

Recognizing type usages vs. class usages

You are probably looking at a type usage, if:

  • a compound or primitive type could make logical sense here
  • a non-instantiable class like System would make no logical sense here
  • List used here would feel like a "raw type"
  • what's being expressed implicitly applies to subclasses/subtypes too

Note on grammar

In the language grammar we write a "type expression" to indicate a type; the JLS calls this an UnannType. When talking about a type usage, we also take into account some surrounding context: any type-use annotations which may have preceded that (including those to the left of modifiers and declaration annotations!), as well as any C-style array brackets which might appear later.

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 it also automatically lets it be used in a few other places:

  • On a class or type parameter declaration. 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 Java permits type-use annotations in these locations, JSpecify defines its annotation types to have no meaning when used in those locations.

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> [].