Back to Blog
Java

Java Primitive Types: Sizes, Defaults, and Behavior

java primitive types: Explains the eight Java primitive types, their sizes, default values, memory behavior, and the hidden costs of autoboxing.

javaprimitive typesjvmtype systemautoboxingmemory management
Editorial illustration of eight fixed-size value blocks representing Java primitive types arranged on a grid.

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.

TypeSizeRange / Values
booleanJVM-dependenttrue or false
byte8 bits-128 to 127
short16 bits-32,768 to 32,767
int32 bits-2^31 to 2^31-1
long64 bits-2^63 to 2^63-1
char16 bits0 to 65,535 (UTF-16 code unit)
float32 bitsIEEE 754 single precision
double64 bitsIEEE 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 compares the exact bit representation. Two computations that should produce the same mathematical result can differ in the last bit, so direct equality is often the wrong check for floating-point values. Comparing with a tolerance or using Double.compare 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 JVM caches boxed values for Integer, Short, Byte, Character, and Long within a specific range, -128 to 127 for Integer by default. Boxed values outside that range create new objects, so == on two boxed values compares object identity, not value.

Integer x = 200; Integer y = 200; System.out.println(x == y); // false: different objects

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.

Escape analysis in modern JVMs 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 in this loop would create many temporary objects unless escape analysis removes them. 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. Two Integer objects holding 200 are distinct objects, so == returns false even though the values are equal. Use .equals() or unbox before comparing.

Unboxing a null wrapper throws NullPointerException. This happens silently 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 to Avoid Primitives

Generic containers force wrapper types. List<Integer>, Optional<Integer>, and Map<String, Integer> cannot store primitives directly. If nullability is required, for example a database column that can be null, a wrapper is the only option.

Records and generic methods also require wrappers when the type parameter is involved. But 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.

java primitive types: Practical Usage and Code Examples | RYUSLOG DEV