PrepZone Logo
PrepZone

Types, Variables and Wrappers

Primitives and their defaults, autoboxing traps, enums, and the operators worth a second look.

Why this matters

  • The == versus .equals() question is the most common source of silent bugs for new Java developers, and autoboxing is why.
  • Integer overflow and floating-point imprecision cause money and timing bugs that no test catches unless you know to look.
  • Enums replace a whole category of bug-prone integer constants, and most people under-use them.
Stack — per thread
main() frameuser → reference
greet() framecount = 3 (primitive)

Popped the moment a method returns.

Heap — shared
User objectname = "Asha"
String object"Asha"

Cleared by the garbage collector, not by returning.

The variable sits in the thread's stack frame. The object it names lives in the shared heap, which is why two variables can point at one object.

The eight primitives

Size, range and default

  • byte — 8 bits, −128 to 127, default 0. Used for raw binary data.
  • short — 16 bits, about ±32 thousand, default 0. Rarely used directly.
  • int — 32 bits, about ±2.1 billion, default 0. The default choice for whole numbers.
  • long — 64 bits, about ±9.2 quintillion, default 0L. Needed for timestamps in milliseconds.
  • float — 32-bit decimal, default 0.0f. Roughly 7 significant digits.
  • double — 64-bit decimal, default 0.0d. The default choice for decimals, about 15 digits.
  • char — 16 bits, a single UTF-16 unit, default '\u0000'. Unsigned, unlike the rest.
  • boolean — true or false, default false. Its size is not specified.

Overflow is silent

Arithmetic wraps around rather than failing, which is the single most dangerous default in the language.

Java
int max = Integer.MAX_VALUE;        // 2_147_483_647
System.out.println(max + 1);        // -2147483648 — wrapped, no warning

long millisPerDay = 24 * 60 * 60 * 1000;          // fine
long millisPerYear = 24 * 60 * 60 * 1000 * 365;   // wrong: int overflow before assignment
long correct = 24L * 60 * 60 * 1000 * 365;        // one L fixes the whole expression

The third line is the classic trap. Every operand is an int, so the multiplication happens in 32 bits and overflows before the result is widened to long. Marking the first operand L forces the whole expression into 64-bit arithmetic.

Floating point is approximate

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

// For money, use BigDecimal — and the String constructor, not the double one
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b));               // 0.3

double stores binary fractions, and 0.1 has no exact binary representation, just as 1/3 has no exact decimal one. Never use double for currency. Either use BigDecimal constructed from strings, or store whole cents in a long.

Wrappers and autoboxing

Each primitive has an object counterpart — int/Integer, double/Double and so on — because collections and generics can only hold objects. The compiler converts automatically.

Java
List<Integer> scores = new ArrayList<>();
scores.add(42);              // autoboxing: int → Integer
int first = scores.get(0);   // unboxing: Integer → int

That convenience has three sharp edges.

Java
Integer a = 127, b = 127;
System.out.println(a == b);        // true  — both from the cache

Integer c = 128, d = 128;
System.out.println(c == d);        // false — two distinct objects
System.out.println(c.equals(d));   // true  — compares values

Integer absent = null;
int value = absent;                // NullPointerException on unboxing

The three edges

  • Cached range. Integer caches instances from −128 to 127, so == accidentally works there and silently stops working above it. Always compare wrapper values with .equals().
  • Unboxing null. A null wrapper unboxes by calling .intValue() on null, producing a NullPointerException at a line with no visible method call.
  • Boxing in loops. A long accumulator typed as Long allocates a new object on every iteration. In a hot loop this is a real cost.

Enums instead of constants

An enum is a class with a fixed set of instances. It gives you type safety, readable output, and a natural place to attach behaviour.

Java
public enum Status {
    PENDING("Awaiting review"),
    APPROVED("Ready to ship"),
    REJECTED("Returned to sender");

    private final String description;

    Status(String description) {
        this.description = description;
    }

    public String description() {
        return description;
    }

    public boolean isFinal() {
        return this == APPROVED || this == REJECTED;
    }
}
Aspectint constantsenum
Type safetyAny int is accepted, valid or notOnly declared values compile
PrintingShows a meaningless numberShows the constant name
Can hold data or methodsNoYes — fields, constructors, methods
Works in switchYesYes, and the compiler can check exhaustiveness
Comparison==== is safe; instances are singletons
  • Type safety

    int constantsAny int is accepted, valid or not
    enumOnly declared values compile
  • Printing

    int constantsShows a meaningless number
    enumShows the constant name
  • Can hold data or methods

    int constantsNo
    enumYes — fields, constructors, methods
  • Works in switch

    int constantsYes
    enumYes, and the compiler can check exhaustiveness
  • Comparison

    int constants==
    enum== is safe; instances are singletons

Enum is one of the highest-value, lowest-effort upgrades in everyday Java.

Operators worth a second look

  • && and || short-circuit; & and | do not. if (list != null && !list.isEmpty()) is safe only because of short-circuiting.
  • Integer division truncates. 7 / 2 is 3. Write 7 / 2.0 for 3.5.
  • % keeps the sign of the left operand. -7 % 3 is -1, not 2.
  • i++ returns the old value; ++i returns the new one. Avoid using either inside a larger expression.
  • >>> shifts in zeros regardless of sign, while >> preserves the sign bit.
  • var infers the type of a local variable from its initialiser. It is still statically typed; use it when the right side already names the type clearly.

Common misreadings

  • "var makes Java dynamically typed." The type is fixed at compile time; only the writing is shorter.
  • "char can be negative." It is the one unsigned primitive, ranging from 0 to 65535.
  • "== works for Integer." It works accidentally inside the cache range, which makes the bug worse, not better.
  • "float saves meaningful memory." On modern hardware double is the sensible default; float mainly buys imprecision.
  • "Local variables default to zero." Fields do. Locals must be assigned before use, enforced at compile time.

Quick recall

Everything you need if you only revisit this box.

  • Eight primitives hold values directly; everything else is a reference. Fields get defaults, locals do not.
  • Integer arithmetic wraps silently. Use L early in long expressions, or Math.addExact when wrapping would be a bug.
  • double cannot represent 0.1 exactly. Use BigDecimal built from strings, or integer cents, for money.
  • Integer caches −128 to 127, so == works by accident there. Always use .equals() for wrapper values.
  • Unboxing a null wrapper throws NullPointerException at a line with no visible call.
  • Enums give type safety, readable names and attached behaviour; == is the correct comparison for them.
  • && and || short-circuit; & and | evaluate both sides. Integer division truncates and % follows the left operand's sign.

Test yourself

Answer these before moving on — recall is what makes it stick.