Java Variables and Data Types: Primitives, Wrappers, and the Traps Between Them
Javaโs type system is where the language first shows its personality. Every variable has a type, the compiler enforces it, and thereโs a split โ primitives versus objects โ running straight through the middle of the language that trips up beginners and interview candidates alike.
This guide covers the eight primitives, the wrapper classes that shadow them, the var keyword, conversions, and two specific traps (Integer caching and autoboxing in loops) that cause genuine production bugs.
Declaring Variables: The Basics
A Java variable declaration is type name = value:
int age = 34;double price = 19.99;boolean active = true;String city = "Hyderabad"; // String is a class, not a primitive โ more on this belowOnce declared, a variableโs type is fixed. You can change the value, never the type:
int count = 5;count = 12; // finecount = "twelve"; // compile error: incompatible typesIf a variable should never change value at all, mark it final:
final double VAT_RATE = 0.18;VAT_RATE = 0.2; // compile error: cannot assign a value to final variablefinal is Javaโs version of a constant. Idiomatic code uses it liberally โ many teams treat every local variable as effectively final unless thereโs a reason to reassign. It makes code easier to reason about and is required for variables captured by lambdas.
The Eight Primitives
Java has exactly eight primitive types. They are not objects โ theyโre raw values stored directly, with no methods and no object overhead. Memorize the four youโll use constantly; know the rest exist.
| Type | Size | Range / Notes | Default |
|---|---|---|---|
int | 32 bits | ยฑ2.1 billion โ your default whole number | 0 |
long | 64 bits | ยฑ9.2 quintillion โ timestamps, IDs, money-in-cents | 0L |
double | 64 bits | Floating point โ your default decimal | 0.0 |
boolean | 1 bit* | true / false only โ no truthy/falsy in Java | false |
byte | 8 bits | โ128 to 127 โ binary data, network protocols | 0 |
short | 16 bits | ยฑ32,767 โ rare in practice | 0 |
float | 32 bits | Less precise decimal; needs f suffix: 3.14f | 0.0f |
char | 16 bits | Single UTF-16 code unit: 'A' | โ\u0000โ |
*JVM-dependent in memory, but conceptually one bit.
A few things worth knowing beyond the table:
Integer overflow is silent. int doesnโt error when it exceeds its range โ it wraps around:
int max = Integer.MAX_VALUE; // 2147483647System.out.println(max + 1); // -2147483648 โ no exception, no warningThis is a real bug source. If a value can plausibly exceed two billion (event counts, file sizes in bytes, epoch milliseconds), use long. Epoch millis already donโt fit in an int.
Floating point is approximate. This surprises people coming from calculators, not from other languages:
System.out.println(0.1 + 0.2); // 0.30000000000000004System.out.println(0.1 + 0.2 == 0.3); // falsedouble stores binary fractions, and 0.1 has no exact binary representation. The rule every Java developer learns eventually: never use double for money. Use BigDecimal (construct from a String: new BigDecimal("19.99")) or store cents in a long.
Underscores make big literals readable (Java 7+):
long worldPopulation = 8_100_000_000L; // the L suffix marks a long literalint hexColor = 0xFF_EC_DE; // hex, binary (0b1010) also supportedPrimitives vs. Objects: The Line Through the Language
Everything that isnโt one of those eight primitives is an object โ created from a class, accessed through a reference, living on the heap. String, arrays, ArrayList, your own classes: all objects.
The practical differences:
int a = 5; // primitive: the variable holds the value 5 itselfString s = "hello"; // object: the variable holds a *reference* to a String
int b = a; // b gets a copy of the value; a and b are independentString t = s; // t gets a copy of the reference; both point to the same objectAnd critically โ primitives can never be null, objects can:
int x = null; // compile errorString y = null; // fine โ and the source of a million NullPointerExceptionsWrapper Classes and Autoboxing
Each primitive has an object twin: int โ Integer, double โ Double, boolean โ Boolean, and so on. You need them because Javaโs generics only work with objects โ there is no ArrayList<int>, only ArrayList<Integer>.
Java converts between the two automatically. This is autoboxing (primitive โ wrapper) and unboxing (wrapper โ primitive):
List<Integer> scores = new ArrayList<>();scores.add(95); // autoboxing: int 95 โ Integer objectint first = scores.get(0); // unboxing: Integer โ intConvenient โ and the source of two classic traps.
Trap 1: == on wrappers
Integer a = 127, b = 127;System.out.println(a == b); // true
Integer c = 128, d = 128;System.out.println(c == d); // false (!)Why? == on objects compares references, not values. The JVM caches Integer objects from โ128 to 127, so small values share one object; larger values get distinct objects. This exact snippet appears in interviews because it catches people who havenโt internalized the primitive/object divide.
Rule: compare wrapper values with .equals(), or better, use primitives when you can.
Trap 2: unboxing a null
Map<String, Integer> stock = new HashMap<>();int count = stock.get("widget"); // NullPointerException at runtimeget returns null for a missing key; unboxing null into int explodes. Use stock.getOrDefault("widget", 0) or keep the wrapper type and null-check.
Trap 3 (performance): boxing in hot loops
Long sum = 0L; // wrapper โ bad choice herefor (long i = 0; i < 10_000_000; i++) { sum += i; // creates a new Long object every iteration}Changing Long sum to long sum makes this loop several times faster and allocation-free. In tight loops, primitives matter.
var: Local Type Inference (Java 10+)
var lets the compiler infer a local variableโs type from the right-hand side:
var name = "Ada"; // Stringvar scores = new ArrayList<Integer>(); // ArrayList<Integer>var entry = Map.entry("k", 1); // Map.Entry<String,Integer> โ nice winImportant: var is not dynamic typing. The type is fixed at compile time exactly as if youโd written it; you just didnโt have to write it. var name = "Ada"; name = 42; is still a compile error.
Guidelines that hold up in real codebases:
- Use
varwhen the type is obvious from the right side (var user = new User()). - Avoid it when the right side is opaque (
var result = service.process()โ what isresult?). - It only works for local variables with initializers โ not fields, not parameters, not return types.
Conversions and Casting
Widening (small โ large) happens automatically because no information can be lost:
int i = 100;long l = i; // finedouble d = l; // fineNarrowing (large โ small) requires an explicit cast, because youโre accepting possible data loss:
double price = 9.99;int rupees = (int) price; // 9 โ truncates toward zero, doesn't roundlong big = 4_000_000_000L;int oops = (int) big; // -294967296 โ silent overflow, your problem nowFor rounding rather than truncation, use Math.round(price).
Strings convert via parse methods, and failure throws:
int n = Integer.parseInt("42"); // 42int bad = Integer.parseInt("42.5"); // NumberFormatExceptionString s = String.valueOf(42); // "42" โ the other directionWrap parseInt of untrusted input (user forms, CSV files) in a try/catch โ we cover the pattern in Exception Handling.
Default Values and the Initialization Rule
Fields (variables declared in a class) get automatic defaults โ 0, false, or null. Local variables get no default, and the compiler refuses to let you read one before assigning it:
public class Account { int balance; // field: defaults to 0
void demo() { int local; System.out.println(local); // compile error: variable might not be initialized }}This asymmetry is deliberate: an uninitialized local is almost always a bug, and the compiler can prove it locally. Field defaults exist because object initialization is more complex. Practical takeaway: the null in your NullPointerException usually came from a field nobody assigned.
Choosing Types: A Working Decision List
Distilled from code review experience, hereโs what to reach for:
- Whole numbers โ
int. Move tolongfor timestamps, database IDs, byte counts, or anything that could exceed 2.1 billion. - Decimals โ
double, except money โBigDecimalor cents-as-long. No exceptions to the money rule. Ever. - Flags โ
boolean. Java has no truthiness; conditions must be actual booleans, which kills theif (count)bugs other languages allow. - Text โ
String, even single characters in most application code;charearns its keep only in low-level parsing. - In collections and generics โ wrappers, everywhere else โ primitives.
float,short,byteโ donโt use them until a specific API or memory constraint demands it.
Frequently Asked Questions
Is String a primitive? No โ itโs a class, which is why it has methods (length(), toUpperCase()) and can be null, and why == comparisons on it betray you. It feels primitive because literals ("hi") and + get special language support. Full story in the strings guide.
Why does Java have both int and Integer instead of one number type? Performance history and honesty. Primitives are raw CPU values โ an int addition is one instruction, an array of them is a contiguous memory block. Objects carry headers and live behind references. Languages that show you only โnumbersโ still have this machinery underneath; Java exposes the seam so you can choose. The cost is the boxing traps this guide covers; the benefit is that long[] prices = new long[1_000_000] is eight megabytes, not eighty.
When should I actually use char? Almost never in application code โ text is String. It earns its place iterating characters in parsing/algorithm work (s.charAt(i)), and even then, remember the code-point caveat for emoji and non-Latin scripts.
What does static final mean on a constant, exactly? static: one copy, owned by the class. final: unchangeable after assignment. Together with SCREAMING_SNAKE naming, thatโs Javaโs constant idiom: static final int MAX_RETRIES = 3;. Note final on a reference type freezes the reference, not the object โ a final List can still be added to, a subtlety that matters enough to get full treatment in the immutability guide.
How precise is double really? About 15โ16 significant decimal digits โ plenty for physics, ratios, and statistics; fatally imprecise for currency ledgers where cents must sum exactly. The rule stays: measurements โ double, money โ BigDecimal or integer minor units.
Two-Minute Exercise: Type Detective
For each declaration, decide: does it compile, and if so whatโs the runtime value? Answers follow.
var a = 5 / 2 * 1.0;var b = 1.0 * 5 / 2;final var c = Long.valueOf(10);byte d = 127; d++;Integer e = null; int f = (e != null) ? e : 0;Answers: a is 2.0 (division happens in int first โ the multiply promotes too late); b is 2.5 (promotion happens before the divide โ order matters); c compiles, a final Long; d wraps to -128 silently; f is 0 and compiles safely โ the null check prevents the unboxing NPE that plain int f = e; would throw. If a vs b surprised you, reread the promotion section โ that left-to-right evaluation detail causes real rounding bugs in billing code.
Quick Self-Check
Predict the output, then run it:
public class TypeQuiz { public static void main(String[] args) { System.out.println(7 / 2); // ? System.out.println(7 / 2.0); // ? System.out.println((int) 7.9); // ? Integer x = 1000, y = 1000; System.out.println(x == y); // ? System.out.println(x.equals(y)); // ? System.out.println(Integer.MAX_VALUE + 1 > 0); // ? }}Answers: 3 (integer division truncates), 3.5 (one double operand promotes the whole expression), 7 (cast truncates), false (references, outside the cache range), true (value comparison), false (overflow wraps negative).
If you got all six, you understand this chapter better than a lot of working Java developers. Next: Operators and Control Flow, where these types start doing work โ including the modern switch expression that most older tutorials donโt cover.