Java Data Types

June 21, 20265 min readUpdated 8/20/2026

Java splits every value into one of two worlds: primitives, which are raw values, and reference types, which are objects. There are exactly eight primitives and you need all of them. Everything else — String, arrays, your own classes — is a reference type.

Knowing which world a value lives in explains several behaviours that otherwise look arbitrary.

The eight primitives

TypeSizeHoldsDefaultUse it for
byte8 bit-128 to 1270raw binary data
short16 bit±32,7670almost never
int32 bit±2.1 billion0whole numbers — your default
long64 bit±9.2 quintillion0Ltimestamps, IDs, counts that grow
float32 bit~7 digits0.0falmost never
double64 bit~15 digits0.0decimals — your default
char16 bitone character''single characters
boolean1 bit*true/falsefalseyes/no

In practice you will use int, long, double and boolean constantly and the other four rarely. Do not pick short over int to save memory — the JVM pads it anyway, and you have made the code odd for nothing.

int items = 42;
long userId = 9_000_000_000L;      // L suffix required — the literal is an int without it
double price = 19.99;
float rate = 1.5f;                 // f suffix required
char grade = 'A';                  // single quotes. "A" is a String.
boolean shipped = false;

int million = 1_000_000;           // underscores are legal in numeric literals and aid reading

The suffixes are not decoration. long userId = 9000000000; does not compile, because the literal on the right is parsed as an int before it is ever assigned, and it does not fit.

Two traps that cost real money

Integer overflow is silent. When an int exceeds its maximum it wraps around to the negative end. No exception, no warning:

int max = Integer.MAX_VALUE;       // 2147483647
System.out.println(max + 1);       // -2147483648  — wrapped, silently

long safe = (long) max + 1;        // 2147483648  — cast BEFORE the arithmetic
System.out.println(Math.addExact(max, 1) == 0);  // throws ArithmeticException instead

Note the cast placement. (long)(max + 1) would be too late — the overflow already happened in int arithmetic. Use long for anything that counts money in cents, milliseconds, or database IDs.

double cannot represent 0.1. Binary floating point stores fractions as sums of powers of two, and 0.1 is not one. The error is tiny and it compounds:

System.out.println(0.1 + 0.2);           // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3);    // false

// Never compare doubles with ==. Compare within a tolerance:
double a = 0.1 + 0.2;
System.out.println(Math.abs(a - 0.3) < 1e-9);   // true

This is not a Java quirk — it is how binary floating point works everywhere. It matters because the obvious use for a decimal is money, and money is exactly where you must not use it.

Money: use BigDecimal

For any value where a fraction of a cent matters, use BigDecimal, which stores digits in base ten:

BigDecimal price = new BigDecimal("19.99");   // String constructor — always
BigDecimal qty = new BigDecimal("3");
BigDecimal total = price.multiply(qty);
System.out.println(total);                    // 59.97  — exact

// BigDecimal is immutable: every operation returns a NEW value
BigDecimal rounded = total.setScale(2, RoundingMode.HALF_UP);

// equals() compares scale too, so 1.0 and 1.00 are "different". Use compareTo:
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1.00")));      // false
System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1.00")));   // 0

Two rules that cover most BigDecimal bugs: construct from a String, never from a double (new BigDecimal(0.1) faithfully preserves the error you were trying to avoid), and compare with compareTo, not equals.

Both rules, applied in real code — from the console bank app this site uses for examples:

public static BigDecimal of(String value) {
    return new BigDecimal(value).setScale(SCALE, RoundingMode.HALF_UP);
}

public static boolean isPositive(BigDecimal value) {
    // compareTo, never equals: BigDecimal.equals("1.0", "1.00") is false because it compares
    // scale as well as value. This trips up almost everyone once.
    return value.compareTo(BigDecimal.ZERO) > 0;
}

Putting both in one place, as that app does, means the rules are followed once rather than remembered at every call site — which is the practical answer to "how do I not get this wrong".

Wrappers and autoboxing

Every primitive has a matching class — int/Integer, double/Double, boolean/Boolean. You need them because collections and generics only hold objects: there is no List<int>.

Java converts between the two automatically, which is convenient and occasionally dangerous:

List<Integer> numbers = new ArrayList<>();
numbers.add(5);                    // autoboxing: int 5 -> Integer.valueOf(5)
int first = numbers.get(0);        // unboxing: Integer -> int

Integer maybe = null;
// int broken = maybe;             // compiles, then throws NullPointerException at runtime

That last line is the one to remember. A wrapper can be null; a primitive cannot. So every unboxing is a potential NullPointerException, and it happens on a line that looks like a plain assignment.

The other trap is ==. On wrappers it compares object identity, and the JVM caches small values, so it appears to work until it does not:

Integer a = 127, b = 127;
System.out.println(a == b);        // true  — cached (-128..127)

Integer c = 128, d = 128;
System.out.println(c == d);        // false — outside the cache, two objects
System.out.println(c.equals(d));   // true  — always compare values with equals

Primitives versus references, in one picture

A primitive variable holds the value. A reference variable holds an address pointing at an object elsewhere in memory. That single difference explains assignment and equality:

int x = 5;
int y = x;          // y gets a COPY of the value
y = 10;
System.out.println(x);            // 5 — unaffected

int[] p = {1, 2, 3};
int[] q = p;        // q gets a copy of the ADDRESS — both point at one array
q[0] = 99;
System.out.println(p[0]);         // 99 — the same array

It is also why == means "the same object" for references and "the same value" for primitives, and why String comparison needs equals — the subject of String.

Converting between types

int small = 100;
long widened = small;              // automatic — no information can be lost

double d = 9.99;
int narrowed = (int) d;            // explicit cast required; truncates to 9, does NOT round
System.out.println(Math.round(d)); // 10 — round properly

int parsed = Integer.parseInt("42");        // String -> int, throws NumberFormatException if not
String text = String.valueOf(42);           // int -> String

Widening is automatic because nothing can be lost. Narrowing needs a cast because you are telling the compiler you accept the loss — and (int) truncates rather than rounds, which is a classic off-by-one in anything that calculates an average.

Next

You now have values and names for them. Operators is next: what you can do to those values, and the three operator behaviours that catch nearly everyone.