Variables and Data Types
Every variable in Java has a type — the compiler and JVM use it to allocate memory, enforce operations, and catch bugs before production.
Introduction
Every variable in Java has a type — the compiler and JVM use it to allocate memory, enforce operations, and catch bugs before production. Java splits types into two families: primitives (8 built-in value types stored directly) and reference types (objects on the heap, accessed via pointer).
Enterprise systems processing millions of transactions per day care deeply about this distinction. A payment service using double for currency will accumulate rounding errors that fail audit. A high-throughput analytics pipeline boxing int into Integer in a hot loop will trigger GC storms. A trading platform storing prices as Float instead of BigDecimal will lose money — literally.
This lesson teaches Java data types the way production engineers at banks, e-commerce platforms, and streaming services apply them: primitives for performance, wrappers only when nullability or collections require them, BigDecimal for money, and memory-aware choices that survive code review and JVM profiling.
Business problem
Type mistakes become financial and operational incidents:
- Currency rounding errors:
double total = 0.1 + 0.2;→ 0.30000000000000004 — reconciliation failures, PCI audit findings, customer disputes. - Autoboxing GC pressure:
List<Integer>in a loop processing 10M events/sec — millions of short-lived Integer objects; GC pause spikes during peak traffic. - Null wrapper surprises:
Integer count = null;unboxed in arithmetic →NullPointerExceptionin production batch job processing payroll. - Wrong numeric type sizing:
bytefor user ID that exceeds 127 — silent overflow or validation failure at scale.
Why this topic exists
Static typing is Java's first line of defense — types exist to guarantee correctness and enable optimization:
- Compile-time safety: Cannot assign
Stringtoint— entire bug class eliminated before deploy. - Memory efficiency: Primitives store value directly (4 bytes for int); no object header overhead.
- JIT optimization: HotSpot inlines primitive operations, unrolls loops on arrays of
int— impossible on boxed types without escape analysis. - Domain modeling: Type choice communicates intent —
BigDecimal amountvsdouble amounttells reviewers whether you understand financial precision.
Core concepts
Java type system — complete reference:
- 8 primitives: byte (1B), short (2B), int (4B), long (8B), float (4B), double (8B), char (2B), boolean (1B*). Default values: 0, 0.0, false, '\u0000'.
- Wrapper classes: Byte, Short, Integer, Long, Float, Double, Character, Boolean — immutable objects wrapping primitives; required for generics (
List<Integer>). - Reference types: String, arrays, custom classes, records, enums — stored on heap; variable holds reference (pointer).
- Autoboxing: Compiler converts primitive → wrapper automatically (
Integer.valueOf). Unboxing: wrapper → primitive. Hidden allocation cost. - BigDecimal: Immutable arbitrary-precision decimal — mandatory for money, rates, tax. Specify scale + RoundingMode always.
- var (Java 10+): Local type inference — compiler deduces type; not dynamic typing.
var count = 42;→ int.
Internal architecture
Memory layout — where each type lives in the JVM:
┌─────────────────── Stack (per thread) ───────────────────┐│ Local primitives: int count = 42; → 4 bytes on stack ││ Local references: String name = "x"; → 4/8 byte ref ││ (object "x" lives on heap ──────────────────────────┐ │└─────────────────────── ─────────────────────────────────│───┘│┌─────────────────── Heap ───────────────────────────────▼───┐│ String object: [header 12-16B][char[] "x"] ││ Integer object: [header 12-16B][int value 42] ← boxing││ BigDecimal object: [header][int[] magnitude][scale] ││ Array int[1000]: [header][length][4000 bytes data] │└───────────────────────────────────────────────────────────┘Primitive sizes (typical HotSpot 64-bit, compressed oops):byte=1 short=2 int=4 long=8 float=4 double=8 char=2 boolean≈1*Object header ≈ 12–16 bytes + alignment → Integer ≈ 16–24 bytes vs int = 4* boolean array uses 1 byte per element; standalone boolean size JVM-dependent
Four architecture views — primitive map, boxing pipeline, type selection guide, BigDecimal structure:
Code walkthrough
Primitives, wrappers, autoboxing, and BigDecimal — with expected output and line-by-line explanation:
- Line 14 — Integer nullable: Wrapper enables null for "unknown count"; primitive defaults to 0 which may be ambiguous.
- Line 24 — double trap: IEEE 754 cannot represent 0.1 exactly — never use for currency, tax, or ledger amounts.
- Line 27 — BigDecimal String constructor:
new BigDecimal("19.99")is exact;new BigDecimal(19.99)inherits double imprecision. - Line 36 — autobox in loop: Each
scores.add(i)allocates an Integer — 5 allocations for 5 ints. Use IntStream or Tro4j primitive collections at scale. - Line 44 — Integer cache:
==compares references; useequals()always for wrapper comparison.
import java.math.BigDecimal;import java.math.RoundingMode;import java.util.ArrayList;import java.util.List;public class DataTypeDemo {public static void main(String[] args) {// ── 1. Primitive types ──int itemCount = 3; // 4 bytes stack, no objectlong orderId = 9_876_543_210L; // 8 bytes, suffix L requireddouble weightKg = 2.5; // OK for physics, NOT for moneyboolean isPaid = true; // cannot convert to int (unlike C)// ── 2. Wrapper classes — nullable, usable in generics ──Integer nullableCount = null; // valid — primitive int cannot be nullInteger boxed = Integer.valueOf(42); // explicit boxingInteger auto = itemCount; // autoboxing: Integer.valueOf(itemCount)// ── 3. Autoboxing trap — NPE on unboxing ──Integer nullBox = null;// int crash = nullBox; // NPE! unboxing null wrapperint safe = nullBox != null ? nullBox : 0;// ── 4. double vs BigDecimal for money ──double badTotal = 0.1 + 0.2;System.out.println("double 0.1 + 0.2 = " + badTotal);// Output: 0.30000000000000004 ← WRONG for financeBigDecimal price = new BigDecimal("19.99"); // String ctor — exactBigDecimal qty = new BigDecimal("3");BigDecimal lineTotal = price.multiply(qty).setScale(2, RoundingMode.HALF_UP);System.out.println("BigDecimal line total = " + lineTotal);// Output: 59.97 ← exact decimal// ── 5. Autoboxing in collections — allocation cost ──List<Integer> scores = new ArrayList<>();for (int i = 0; i < 5; i++) {scores.add(i); // autobox: int → Integer object on heap}int first = scores.get(0); // unbox: Integer → int// ── 6. Integer cache — identity for small values ──Integer a = 127;Integer b = 127;Integer c = 128;Integer d = 128;System.out.println("127==127: " + (a == b)); // true (cached)System.out.println("128==128: " + (c == d)); // false (new objects)System.out.println("128 equals: " + c.equals(d)); // true (value)}}/** Expected output:* double 0.1 + 0.2 = 0.30000000000000004* BigDecimal line total = 59.97* 127==127: true* 128==128: false* 128 equals: true*/
Production example
Production payment domain model — correct types for a fintech ledger service:
- BigDecimal in record — immutable; setScale in compact constructor normalizes precision on creation.
- String constructor —
Money.usd("100.00")factory prevents double literal imprecision at call sites. - long for IDs — Snowflake/UUID numeric IDs exceed
intrange; use primitivelongnotLongunless nullable. - int for quantity — count of items is never null in valid order; primitive avoids boxing.
package com.acme.payments.domain;import java.math.BigDecimal;import java.math.RoundingMode;import java.util.Objects;// Record — immutable carrier; BigDecimal for all monetary fieldspublic record Money(BigDecimal amount, String currency) {private static final int SCALE = 2;private static final RoundingMode ROUNDING = RoundingMode.HALF_UP;public Money {Objects.requireNonNull(amount, "amount");Objects.requireNonNull(currency, "currency");if (amount.scale() > SCALE) {amount = amount.setScale(SCALE, ROUNDING);}}public static Money usd(String amount) {return new Money(new BigDecimal(amount), "USD");}public Money add(Money other) {requireSameCurrency(other);return new Money(this.amount.add(other.amount), currency);}public Money multiply(int quantity) {return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);}private void requireSameCurrency(Money other) {if (!currency.equals(other.currency)) {throw new IllegalArgumentException("Currency mismatch");}}}// Usage in service layer:// Money subtotal = Money.usd("100.00");// Money tax = subtotal.multiply(8).amount... // 8% via dedicated method// long transactionId = snowflake.nextId(); // primitive long for IDs
Enterprise case study
Stripe / payment processor pattern — never float for money: Production payment systems store amounts in smallest currency unit (cents) as long or use BigDecimal with explicit scale. Stripe's API returns amounts as integers (cents). Many Java fintech services wrap this in a Money value type with BigDecimal internally and scale-2 enforcement. A major European bank migrated legacy double balance fields to BigDecimal — found €2.3M cumulative reconciliation drift over 5 years from floating-point accumulation in nightly batch jobs.
- Before:
double balancein account entity — penny drift after millions ofprocessPaymentcalls at month-end close. - Root cause: Repeated
+= 0.01operations on double over millions of rows. - After:
BigDecimalwithHALF_EVENrounding (banker's rounding) — audit-compliant, zero drift. - Performance: 3× slower than double per operation — acceptable for ledger; use long cents in hot aggregation paths if needed.
Performance considerations
Type choices measurable at production scale:
- Primitive vs boxed:
intarithmetic ~5–10× faster thanIntegerwith autoboxing in tight loops — JMH benchmarks show GC dominates. - Integer cache:
Integer.valueOf(-128..127)returns cached instance — free reuse; outside range = new allocation every time. - BigDecimal cost: ~10–50× slower than
longcents arithmetic — uselong amountCentsin high-throughput aggregation; convert to BigDecimal at API boundary. - String concatenation:
String + intcreatesStringBuilderinternally — fine occasionally; useString.valueOf()or formatted logging in hot paths. - Arrays vs collections:
int[]— contiguous memory, cache-friendly;ArrayList<Integer>— pointer chasing + boxing overhead.
Security considerations
Type misuse creates security and compliance risk:
- Precision attacks: Attacker submits
0.001cent amounts — double rounding may zero out or accumulate unexpectedly; BigDecimal with minimum unit validation prevents this. - Integer overflow:
int quantity * int priceoverflow wraps silently — useMath.multiplyExact()orlongfor intermediate results. - Deserialization: Untrusted JSON to
Objector rawMap— type confusion; use typed DTOs with explicit field types. - Null unboxing: Wrapper null from external API unboxed in calculation — NPE or bypass; validate null before arithmetic.
Scalability considerations
Data type decisions at millions of events per second:
- Primitive collections: Eclipse Collections, FastUtil, Trove —
IntListavoids boxing in analytics pipelines processing billions of events. - Columnar storage: Parquet/Arrow use primitive arrays — Java services writing metrics should match (int/long columns, not boxed).
- Memory per pod: 1M
Integerobjects ≈ 16–24MB heap; 1Mintin array ≈ 4MB — 4–6× difference affects K8s heap sizing. - Serialization: JSON with unquoted numbers — Jackson deserializes to
IntegerorLongbased on magnitude; define schema explicitly.
Production challenges
Real production failures from type misuse:
- NullPointerException on unboxing: External API returns null Integer for optional field — arithmetic without null check crashes batch job.
- Integer == comparison:
Integer a = 200; Integer b = 200; a == b→ false — logic bug in cache key comparison; useequals(). - BigDecimal scale mismatch:
new BigDecimal("10.1").equals(new BigDecimal("10.10"))→ false — equals considers scale; usecompareTo()for value comparison. - float literal to BigDecimal:
new BigDecimal(0.1f)→ 0.099999994 — always construct from String orvalueOf(long).
Common mistakes
- Using
doubleorfloatfor money — useBigDecimalorlongcents. - Comparing wrappers with
==instead ofequals()— reference equality fails outside cache range. - Autoboxing in hot loops without noticing GC impact — profile with JFR "Object Allocation" events.
new BigDecimal(19.99)— double constructor carries imprecision; usenew BigDecimal("19.99").- Assuming
booleanis 1 byte always — arrays yes; standalone local variable size is JVM implementation detail.
Debugging guide
Diagnose type-related bugs in production:
- NPE on line with Integer/Long arithmetic: Unboxing null wrapper — add null check or use
Optional<Integer>. - Penny-off reconciliation: Search codebase for
double+ money keywords — migrate to BigDecimal. - GC allocation spike: JFR "Allocation in new TLAB" → filter
java.lang.Integer— autoboxing hotspot. - Unexpected equality: Log both
==andequals()andhashCode()for wrapper suspects.
# Find double used for money (code review / static analysis)grep -rn "double.*amount\|double.*price\|double.*total" src/# BigDecimal best practice check# ✓ new BigDecimal("10.00")# ✗ new BigDecimal(10.00)# ✓ amount.compareTo(other) == 0# ✗ amount.equals(other) // scale-sensitive
Best practices
- Use int/long primitives for counters, IDs, quantities — default integer type is
int, uselongwhen range exceeds 2³¹. - Use BigDecimal for all monetary values — construct from String; set scale + RoundingMode on every division.
- Use wrapper classes only when null semantics required or generics demand it —
Optional<Integer>often clearer than null Integer. - Compare wrappers with
equals()— never==except knowingly using cached -128..127. - Use long cents pattern for high-throughput ledger aggregation; BigDecimal at domain boundary.
- Validate overflow:
Math.addExact(a, b),Math.multiplyExact(a, b)in financial calculations. - Enable Error Prone / SonarQube rules:
BoxedPrimitiveConstructor,FloatLiteralForDouble.
Anti-patterns
double price = 19.99;in payment entity — floating-point money; PCI and audit failure.Map<String, Object>for typed domain data — use records/DTOs with explicit types.- Autoboxing in stream:
list.stream().mapToInt(x -> x)missed — usingmapinstead ofmapToIntboxes every element. - Storing money as
String— parse validation nightmare; use BigDecimal or long cents internally. BigDecimal.equals()for business comparison — scale 2 vs scale 4 fails equals; usecompareTo() == 0.
Staff engineer notes
- In architecture review, ask "how is money stored?" —
doubleanswer fails review;BigDecimalorlongcents passes. - Autoboxing is invisible in source but visible in JFR — staff engineers profile before optimizing application logic.
- Type choice documents domain understanding —
record Money(BigDecimal amount, Currency currency)is a design decision, not a syntax detail. - When migrating legacy double to BigDecimal, run parallel calculation in shadow mode — quantify drift before cutover.
- Primitive specialization (Project Valhalla, future) will change cost model — but BigDecimal for money remains permanent requirement.
Interview questions
Interview preparation
15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.
Beginner
1What are the 8 primitive types in Java?
BeginnerModel answer
byte (8-bit), short (16-bit), int (32-bit), long (64-bit), float (32-bit float), double (64-bit float), char (16-bit Unicode), boolean (true/false).
Primitives store value directly, have no methods, cannot be null, have default values (0, false, '\u0000').
Follow-up probe
Which is default for integer literals?
2What is the difference between int and Integer?
BeginnerModel answer
int is primitive — 4 bytes, stack or inside object, cannot be null, no methods. Integer is wrapper class — heap object (~16-24 bytes), can be null, has methods like parseInt(), required for generics (List). Autoboxing converts between them. Follow-up probe
When would you use Integer over int?
3What is autoboxing and unboxing?
BeginnerModel answer
valueOf()).
Unboxing: wrapper to primitive (Integer → intValue()).
Syntax-transparent but allocates heap object on every box outside cache range.
Follow-up probe
What happens if you unbox null Integer?
4Why should you not use double for money?
BeginnerModel answer
- double uses IEEE 754 binary floating point
- cannot represent decimal fractions like 0.1 exactly. 0.1 + 0.2 = 0.30000000000000004. Financial systems need exact decimal arithmetic
- use BigDecimal or store cents as long.
Follow-up probe
How does Stripe handle amounts?
5What is BigDecimal and when do you use it?
BeginnerModel answer
- Immutable arbitrary-precision decimal. Use for money, tax rates, interest calculations
- anywhere exact decimal rounding matters. Always construct from String, set scale and RoundingMode on division, compare with compareTo() not equals().
Follow-up probe
What is banker's rounding?
Intermediate
6Explain the Integer cache.
IntermediateModel answer
- Integer.valueOf() caches instances for -128 to 127 (configurable via -XX:AutoBoxCacheMax). Integer a = 127; Integer b = 127; a == b is true (same object). For 128, new objects
- a == b is false. Always use equals() for value comparison.
Follow-up probe
Does Long have a cache too?
7Compare memory usage of int vs Integer for 1 million values.
IntermediateModel answer
int[1_000_000] ≈ 4MB contiguous array. ArrayListwith 1M elements ≈ 4MB for ints + 1M object headers (~12-16B each) ≈ 16-20MB+ plus pointer overhead. Primitive arrays 4-6× more memory efficient. Follow-up probe
What libraries provide primitive collections?
8What is the difference between BigDecimal equals and compareTo?
IntermediateModel answer
00').
compareTo() compares numeric value only: compareTo returns 0 for equal values regardless of scale.
Use compareTo for business logic; equals for HashMap keys if scale normalized.
Follow-up probe
How do you normalize scale?
9When should you use float vs double vs BigDecimal?
IntermediateModel answer
- float: rarely
- limited precision, use double instead. double: scientific computation, graphics, ML features
- never for money. BigDecimal: financial, billing, tax
- exact decimal with explicit rounding.
Follow-up probe
What about long cents pattern?
10How does autoboxing affect Stream performance?
IntermediateModel answer
stream.map(x -> x * 2) on Listboxes/unboxes. Use IntStream, LongStream, DoubleStream with mapToInt, sum() for primitive pipelines — zero allocation, SIMD-friendly, 5-10× faster on large datasets. Follow-up probe
What is mapToInt?
Advanced
11Design a Money type for a global payments platform.
AdvancedModel answer
record Money(BigDecimal amount, Currency currency) with compact constructor normalizing scale to currency default (2 for USD/EUR, 0 for JPY).
Factory from String.
add/subtract require same currency.
multiply(BigDecimal) for rates.
Never expose double.
Persist as DECIMAL(19,4) or bigint cents.
Follow-up probe
How handle FX conversion?
12Your payment service has GC pauses every 30s — suspect autoboxing. How do you prove it?
AdvancedModel answer
Enable JFR allocation profiling. Filter java.lang.Integer, Long allocations. Find hot stack traces — usually stream, collection iteration, or Map. Replace with IntStream, primitive arrays, or Eclipse Collections IntList. Re-profile; GC pause should drop. Follow-up probe
When is boxing acceptable?
13Migrate 500 microservices from double to BigDecimal for money — strategy?
AdvancedModel answer
- Phase 1: static analysis grep + SonarQube rule. Phase 2: introduce Money value type in shared library. Phase 3: shadow mode
- run both double and BigDecimal, alert on drift. Phase 4: migrate per domain team with DB column migration (DOUBLE → DECIMAL). Phase 5: remove double paths. ADR per service.
Follow-up probe
Database migration risks?
14Explain integer overflow in financial calculation.
AdvancedModel answer
- int quantity * int unitPrice can exceed Integer.MAX_VALUE (2.1B)
- wraps to negative silently. Fix: cast to long before multiply, use Math.multiplyExact for fail-fast, or BigDecimal throughout. Audit any int arithmetic on user-controlled inputs.
Follow-up probe
Does Java 8+ help?
15Primitive vs wrapper in REST API DTO — when null matters?
AdvancedModel answer
- int field: JSON missing key → default 0 (ambiguous
- is zero real or missing?). Integer field: missing → null (explicit unknown). Use Optional in domain layer; Integer/Long in DTO for nullable metrics. Document OpenAPI schema required vs optional.
Follow-up probe
Jackson deserialization behavior?
Hands-on exercise
Lab: Types in practice — run the playground and observe output, then extend:
- Run starter code — compare
doublevsBigDecimaloutput for the same calculation. - Fix the "wrong total" by replacing double arithmetic with BigDecimal (
new BigDecimal("0.10")). - Add a method
safeAdd(Integer a, Integer b)that handles null wrappers without NPE. - Print
a == banda.equals(b)for Integer 200 — explain the difference in a comment. - Bonus: rewrite the cart total using
longcents (amount * 100) and compare performance conceptually.
JavaVariables and Data Types
Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.Architecture trade-offs
- int vs Integer: int wins performance and non-null semantics; Integer wins nullability and generics.
- BigDecimal vs long cents: BigDecimal wins precision and readability; long cents wins throughput in aggregation (Stripe pattern).
- double vs float: double wins precision and is default; float rarely used except GPU/large arrays where memory matters.
- Primitive collections vs ArrayList: Primitive wins memory and GC; ArrayList wins API ergonomics and generics.
Summary
Variables and data types are where Java's static typing delivers enterprise value — primitives for performance, wrappers for nullability, BigDecimal for money, and autoboxing as a hidden cost you must profile. You can now choose types like a production engineer, explain the double money trap with output proof, and design Money value types that pass fintech audit. Next: operators and expressions.
Key takeaways
- 8 primitives store values directly — int/long for integers, never double/float for money.
- Wrapper classes enable null and generics — autoboxing hides allocation cost; avoid in hot loops.
- BigDecimal from String with explicit scale + RoundingMode — compare with compareTo(), not equals().
- Integer cache -128..127 — use equals() for all wrapper comparison, never ==.
- Memory: int ≈ 4B vs Integer ≈ 16-24B — type choice affects GC and K8s heap sizing at scale.