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 (int ↔ Integer).
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:
| Type | Width | Range / Values |
|---|---|---|
boolean | 1 (logically) | true, false |
byte | 8 | −128 to 127 |
short | 16 | −32 768 to 32 767 |
int | 32 | −2³¹ to 2³¹ − 1 |
long | 64 | −2⁶³ to 2⁶³ − 1 |
float | 32 | IEEE 754 binary32 |
double | 64 | IEEE 754 binary64 |
char | 16 | Unicode 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 isList<Integer>.
Wrapper types and autoboxing
Each primitive has a corresponding wrapper class in java.lang:
| Primitive | Wrapper |
|---|---|
boolean | Boolean |
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
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 types —
int[],String[][],List<T>[]. Every array type derives fromObject. - Annotation types — declared with
@interface; technically a special interface kind. - The
nulltype — the type of thenullliteral. 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:
| Method | Purpose |
|---|---|
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, andtoStringbased 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
CloneableandSerializablebut notList<T>. - The
lengthis a field (not a method), and indexing uses[](not.get()). - Arrays are covariant:
Number[]may hold anInteger[]reference. The covariance is unsafe (a write may throwArrayStoreException) 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
varwhen the right-hand side makes the type obvious. - Avoid
varwhen the inferred type is non-obvious or when the explicit type carries documentation value. - Be aware that
varinfers the most specific type:var list = new ArrayList<String>()producesArrayList<String>, notList<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.