Java Primitive Types: Sizes, Defaults, and Behavior
A practical guide to Java primitive types: sizes, default values, memory behavior, autoboxing traps, and when wrappers are required.
Java has eight primitive types: boolean, byte, char, short, int, long, float, and double. Unlike reference types, they store values directly instead of pointing to objects on the heap. That distinction drives how Java primitive types behave during assignment, equality checks, method calls, and memory allocation. Getting these behaviors right matters because primitives appear in nearly every Java codebase, and small mistakes around them produce subtle runtime bugs.
The Eight Primitive Types and Their Ranges
Each primitive type has a fixed size defined by the JVM specification, except boolean, whose size depends on the JVM implementation. The numeric types use two's complement for signed integers and IEEE 754 for floating-point values.
| Type | Size | Range / Values |
|---|---|---|
| boolean | JVM-dependent | true or false |
| byte | 8 bits | -128 to 127 |
| short | 16 bits | -32,768 to 32,767 |
| int | 32 bits | -2^31 to 2^31-1 |
| long | 64 bits | -2^63 to 2^63-1 |
| char | 16 bits | 0 to 65,535 (UTF-16 code unit) |
| float | 32 bits | IEEE 754 single precision |
| double | 64 bits | IEEE 754 double precision |
char is a 16-bit unsigned type that holds a single UTF-16 code unit. That means a char can represent most characters directly, but supplementary characters outside the Basic Multilingual Plane require two char values, which is why String methods like codePointAt exist.
Default Values and Initialization
Fields of primitive type receive a default value when an object is constructed. Numeric fields default to 0, boolean to false, and char to '\u0000'. Local variables get no default, and the compiler rejects any read of a local variable that has not been definitely assigned.
public class Counter { private int count; // defaults to 0 private boolean active; // defaults to false }
public void increment() { int value; // no default // System.out.println(value); // compile error: variable not initialized value = 1; System.out.println(value); }
The distinction between field defaults and local variable rules is a common source of confusion. A field can be read safely after construction, but a local variable used before assignment fails at compile time. This is deliberate: object fields have a well-defined lifecycle, while locals are expected to be initialized at the point of use.
Assignment and Equality Semantics
Assigning one primitive to another copies the value. There is no shared storage between two primitive variables, so changing one never changes the other.
int a = 5; int b = a; // copies 5 b = 10; System.out.println(a); // 5
For float and double, the == operator follows IEEE 754 rather than mathematical equality: +0.0 and -0.0 compare equal, NaN compares unequal to every value including itself, and two computations that should produce the same mathematical result can differ in the last bit. Direct equality is therefore often the wrong check for floating-point values. Comparing with a tolerance, or using Double.compare when you need a consistent total order that handles NaN and signed zero, is usually safer.
Autoboxing and Unboxing
Java automatically converts between primitives and their wrapper classes in assignments, method arguments, and expressions. The compiler inserts boxing and unboxing calls where the types do not match.
Integer boxed = 42; // boxing int unboxed = boxed; // unboxing
The Java language specification requires Integer values from -128 to 127 to be cached. Standard JVMs also maintain small-value caches for Byte, Short, Long, and Character, but the exact cache bounds are implementation details. Autoboxing outside the guaranteed range generally creates a new object, so == on two boxed values can be false even when the values are equal; it always compares references, not values.
Integer x = 200; Integer y = 200; System.out.println(x == y); // usually false; never compare wrappers with ==
This is one of the most common bugs around Java primitive types. The code looks correct because the values are equal, but == on wrappers compares references. Use .equals() or unbox both sides before comparing.
Memory and Performance Considerations
Primitive arrays store values contiguously with no per-element object overhead. An int[] with one million elements occupies about 4 MB. An Integer[] of the same length holds one million references plus one million Integer objects, each with an object header, so the real footprint is several times larger. For large collections, caches, and data-processing pipelines, this difference is significant.
JIT escape analysis can sometimes eliminate boxing allocations when the boxed object never leaves the method. That optimization is real but not guaranteed, so hot paths that repeatedly box values can still allocate. Writing the loop with primitives removes the question entirely.
long sum = 0; for (int i = 0; i < values.length; i++) { sum += values[i]; }
Using Integer for the accumulator would unbox and rebox on every addition unless escape analysis eliminated the allocations; the primitive version has no allocation at all.
Choosing the Right Primitive Type
int is the natural default for whole numbers. Use long when the range of int is insufficient. byte and short are useful mainly in large arrays where memory matters, but arithmetic on them is promoted to int, so you often need explicit casts.
byte a = 10; byte b = 20; byte c = (byte) (a + b); // arithmetic promotes to int
double is the default for floating-point arithmetic. float halves the storage in arrays but has roughly 7 decimal digits of precision, which is too little for many calculations. boolean is the right type for flags. char is rarely the best choice for text processing; String and code-point-based iteration handle Unicode more safely.
Common Pitfalls with Primitives and Wrappers
Comparing boxed values with == is the most frequent bug. Outside the guaranteed cache range, autoboxing can produce distinct Integer objects, so == may return false even though the values are equal. Use .equals() or unbox before comparing.
Unboxing a null wrapper throws NullPointerException at runtime, with no compile-time warning. This happens in expressions like Integer value = null; int result = value + 1;.
Integer overflow is silent in Java. int a = Integer.MAX_VALUE; a + 1 wraps to Integer.MIN_VALUE. Use Math.addExact() when overflow should be treated as an error.
float and double should not be used for currency. BigDecimal is the correct choice for decimal money values.
When Wrappers Are Required
Generic containers force wrapper types. List<Integer>, Optional<Integer>, and Map<String, Integer> cannot store primitives directly, and the same rule applies to generic methods and generic records. If nullability is required, for example a database column that can be null, a wrapper is the only option.
For local variables, array elements, and object fields, primitives are the default choice. The decision comes down to whether you need null values or must place the value inside a generic structure.