Polyglot
Languages Java types
Java § types

Types

The Java type system is statically checked, partitioned into primitive types and reference types. The eight primitive types — boolean, byte, short, int, long, float, double, char — represent unboxed values stored inline in variables, parameters, and fields. Every other type is a reference type: classes, interfaces, arrays, enum types, annotation types, and the special null type. The two categories are connected by autoboxing — automatic conversion between primitive types and their wrapper class types (intInteger).

Compared with C# (which has user-defined value types via struct) and C++ (which has full value semantics for any class), Java’s type system is more restrictive: only the eight primitives have value semantics, and everything else is a reference. Project Valhalla is a long-running effort to add user-defined value types to Java; until it ships, the partition is fixed.

This page covers the principal type-system surface; the dedicated pages cover References and primitives, Generics, and Classes, records, and sealed types.

Primitive types

The eight primitive types and their characteristics:

TypeWidthRange / Values
boolean1 (logically)true, false
byte8−128 to 127
short16−32 768 to 32 767
int32−2³¹ to 2³¹ − 1
long64−2⁶³ to 2⁶³ − 1
float32IEEE 754 binary32
double64IEEE 754 binary64
char16Unicode BMP code unit (UTF-16)

The widths are part of the language specification and are not implementation-defined. A Java program written against int (32 bits) behaves identically on every conforming JVM.

int    n = 42;
long   big = 1_000_000_000_000L;          // L suffix; underscores admitted (since Java 7)
double pi = 3.14159265358979;
char   ch = 'A';
boolean ok = true;

Numeric literals admit underscores as readability separators (1_000_000); they are ignored by the lexer. The 0x, 0b (since Java 7), and 0 prefixes admit hexadecimal, binary, and octal forms.

The principal practical consequences of the primitive vs reference partition:

  • Primitive values are stored inline; assigning a primitive copies the value.
  • Primitive values cannot be null.
  • Primitive values do not have methods; 42.toString() is a compile-time error in Java.
  • Generic type parameters cannot be primitives; List<int> is illegal — the conventional substitute is List<Integer>.

Wrapper types and autoboxing

Each primitive has a corresponding wrapper class in java.lang:

PrimitiveWrapper
booleanBoolean
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter

The wrapper is a reference type: it is a class with one instance field (the underlying primitive value), heap-allocated, immutable, and admitting all the conventional reference-type operations (assignment to Object, storage in a generic collection, null value).

Autoboxing (since Java 5) admits implicit conversion between a primitive and its wrapper:

Integer boxed   = 42;            // autoboxing: int -> Integer
int     unboxed = boxed;          // unboxing:   Integer -> int

List<Integer> ints = new ArrayList<>();
ints.add(1);                      // autoboxes the int
int first = ints.get(0);          // unboxes the Integer

Autoboxing has non-trivial performance cost (heap allocation per box, GC pressure) and admits surprising behaviour around equality:

Integer a = 127;
Integer b = 127;
boolean eq1 = a == b;             // true: small Integers are cached

Integer c = 128;
Integer d = 128;
boolean eq2 = c == d;             // false: outside the cache range, distinct objects

boolean eq3 = c.equals(d);        // true: value comparison

The standard library caches Integer instances for values −128 to 127 (and similar caches for other wrappers), so reference comparison happens to work for small values and not for large ones. The conventional discipline is to use .equals() for wrapper comparison and == only for reference comparison.

Reference types

Every type that is not a primitive is a reference type. The categories:

  • Class types — declared with class, record, or (the implicit) enum.
  • Interface types — declared with interface.
  • Array typesint[], String[][], List<T>[]. Every array type derives from Object.
  • Annotation types — declared with @interface; technically a special interface kind.
  • The null type — the type of the null literal. Has no name; admits assignment to any reference type.

A reference variable holds either null or a reference to a heap-allocated object. Assignment copies the reference, not the object:

StringBuilder a = new StringBuilder("hello");
StringBuilder b = a;              // b refers to the same StringBuilder as a
b.append(", world");
System.out.println(a);            // "hello, world"

The full implications of reference vs primitive are in References and primitives.

The Object root

Every class transitively derives from java.lang.Object. The root provides the universal methods:

MethodPurpose
equals(Object)Value equality; default is reference equality
hashCode()Hash for collections; must be consistent with equals
toString()Textual representation; default includes the class name and identity hash
getClass()The runtime Class<?>
clone()Shallow copy; protected, requires Cloneable
wait(), notify(), notifyAll()Legacy intrinsic-lock condition primitives
finalize()Pre-collection cleanup; deprecated since Java 9

The conventional discipline is that user-defined classes override equals, hashCode, and toString whenever value-equality semantics are appropriate. Records (since Java 14) generate all three automatically.

Custom value-like types: records

Java 14 introduced records: concise classes for value-oriented data with structural equality:

public record Point(double x, double y) { }

Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);

boolean eq = p1.equals(p2);       // true: structural equality
double  x  = p1.x();              // accessor method (no x() field syntax)
String  s  = p1.toString();       // "Point[x=3.0, y=4.0]"

The compiler synthesises:

  • Private final fields for each component.
  • A canonical constructor.
  • Accessor methods for each component (named after the component, not getX).
  • equals, hashCode, and toString based on the components.

Records are reference types — they live on the heap — but exhibit value-like behaviour: they are immutable and support structural equality. They are the closest Java comes to value types until Project Valhalla ships.

The full treatment is in Classes, records, and sealed types.

Custom reference types: classes

A class declares a reference type:

public class Counter {
    private int count = 0;

    public Counter() { }
    public Counter(int initial) { this.count = initial; }

    public void increment() { count++; }
    public int  value()     { return count; }
}

Counter c = new Counter(10);
c.increment();
int v = c.value();                // 11

The full treatment is in Classes, records, and sealed types.

Interfaces

An interface declares a contract:

public interface Comparable<T> {
    int compareTo(T other);
}

public interface Drawable {
    void draw();
    default boolean isVisible() { return true; }     // default method (Java 8)
}

Java interfaces are first-class language constructs: a class may implement any number of them, default methods admit interface evolution, and functional interfaces (interfaces with exactly one abstract method) are the substrate on which lambda expressions rest. The full treatment is in Interfaces.

Enum types

An enumeration declares a set of named instances:

public enum Direction {
    NORTH, EAST, SOUTH, WEST;

    public Direction opposite() {
        return switch (this) {
            case NORTH -> SOUTH;
            case SOUTH -> NORTH;
            case EAST  -> WEST;
            case WEST  -> EAST;
        };
    }
}

Direction d = Direction.NORTH;
Direction o = d.opposite();

Java enums are full classes: they may have fields, methods, constructors, and may implement interfaces. Each enum constant is a singleton instance. The mechanism is more capable than C-family enumerations and is the conventional way to express a closed set of named values.

public enum Status {
    OK(200, "OK"),
    NOT_FOUND(404, "Not Found"),
    SERVER_ERROR(500, "Server Error");

    private final int    code;
    private final String message;

    Status(int code, String message) {
        this.code    = code;
        this.message = message;
    }

    public int code()       { return code; }
    public String message() { return message; }
}

Arrays

Arrays are reference types that hold a fixed-size sequence of elements:

int[]    arr     = new int[5];                      // {0, 0, 0, 0, 0}
int[]    init    = { 1, 2, 3, 4, 5 };
int[][]  matrix  = new int[3][4];                    // multi-dimensional
int[][]  jagged  = new int[3][];                     // jagged: array of arrays
jagged[0] = new int[] { 1, 2, 3 };

int n = arr.length;                                  // length is a field, not a method
int x = arr[0];
arr[0] = 42;

Arrays differ from List<T> in several ways:

  • Fixed length, set at construction.
  • Stored inline in the array allocation; primitive arrays do not box.
  • Implement Cloneable and Serializable but not List<T>.
  • The length is a field (not a method), and indexing uses [] (not .get()).
  • Arrays are covariant: Number[] may hold an Integer[] reference. The covariance is unsafe (a write may throw ArrayStoreException) and is one of the language’s design regrets.

Generics with arrays are problematic: new T[n] is illegal because of type erasure; new ArrayList<String>[10] is illegal because of array covariance. The conventional defence is to use List<T> rather than arrays when generics are involved.

Type aliases

Java does not have type aliases. Each type has its canonical name; aliases must be expressed as subclasses, wrapper classes, or import statements:

import java.util.List;
import java.util.Map;

// no alias for List<Map<String, Integer>>; the type must be spelt out each time

The lack of type aliases is occasionally inconvenient but rarely consequential.

var and type inference

Java 10 introduced var for local-variable type inference:

var n      = 42;                                  // int
var s      = "hello";                              // String
var list   = new ArrayList<String>();              // ArrayList<String>
var map    = Map.of("key", 1);                     // Map<String, Integer>

var is restricted to local variables with initialisers; it is not admitted for fields, parameters, or return types. The conventions:

  • Use var when the right-hand side makes the type obvious.
  • Avoid var when the inferred type is non-obvious or when the explicit type carries documentation value.
  • Be aware that var infers the most specific type: var list = new ArrayList<String>() produces ArrayList<String>, not List<String>.

Generics

A generic type or method takes type parameters:

public class Box<T> {
    private final T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String>  s = new Box<>("hello");        // diamond operator (since Java 7)
Box<Integer> i = new Box<>(42);

public static <T> T first(List<T> list) {
    return list.get(0);
}

Java generics are erased at runtime: Box<String> and Box<Integer> are the same runtime class, and the type arguments are not available to reflection. The mechanism is less powerful than C#‘s reified generics but adequate for type-safe collections and the streams API.

The full treatment is in Generics.

Conversions

Java admits several kinds of conversion:

Implicit (widening)

int    n = 42;
long   l = n;            // implicit; long is wider
double d = n;            // implicit

Widening primitive conversions are implicit; they preserve the value (within the limits of floating-point precision).

Explicit (narrowing)

double d = 3.14;
int    n = (int) d;       // explicit; truncates to 3
long   l = 1_000_000_000_000L;
int    m = (int) l;       // explicit; truncates the high bits

Object o = "hello";
String s = (String) o;    // explicit reference cast; checked at runtime

Narrowing primitive conversions require an explicit cast and may lose information. Reference casts are checked at runtime; failure throws ClassCastException.

Boxing and unboxing

Integer boxed   = 42;             // autoboxing
int     unboxed = boxed;          // auto-unboxing

Discussed above. The unboxing path may throw NullPointerException if the wrapper is null.

Pattern-matching cast

Java 16+ admits a pattern form that combines test and cast:

if (o instanceof String s) {
    System.out.println(s.length());
}

The bound variable s is in scope where the test holds. The construction is the conventional contemporary form for type-driven branching.

Object representation, alignment, padding

Java does not expose object representation directly. The JVM controls layout: every object has a header (typically two words: the mark word for sync/hash/lock state, and the class pointer), followed by the field data. The layout of the field data is JVM-specific; HotSpot’s layout favours alignment over compactness, which can produce substantial padding for objects with awkwardly-arranged fields.

Java does not admit user-controlled layout; programs that need C-compatible layouts use ByteBuffer and the java.nio machinery, or MemorySegment (introduced in Java 14, stabilised in Java 22 as part of the Foreign Function and Memory API).

The treatment of memory in detail is in Memory and the JVM.