Technology  /  Java

โ˜• Java 15 guides ยท updated 2026

From the JVM to streams, generics, and concurrency โ€” the language behind most enterprise backends, taught with modern Java in mind.

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 below

Once declared, a variableโ€™s type is fixed. You can change the value, never the type:

int count = 5;
count = 12; // fine
count = "twelve"; // compile error: incompatible types

If 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 variable

final 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.

TypeSizeRange / NotesDefault
int32 bitsยฑ2.1 billion โ€” your default whole number0
long64 bitsยฑ9.2 quintillion โ€” timestamps, IDs, money-in-cents0L
double64 bitsFloating point โ€” your default decimal0.0
boolean1 bit*true / false only โ€” no truthy/falsy in Javafalse
byte8 bitsโˆ’128 to 127 โ€” binary data, network protocols0
short16 bitsยฑ32,767 โ€” rare in practice0
float32 bitsLess precise decimal; needs f suffix: 3.14f0.0f
char16 bitsSingle 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; // 2147483647
System.out.println(max + 1); // -2147483648 โ€” no exception, no warning

This 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.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false

double 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 literal
int hexColor = 0xFF_EC_DE; // hex, binary (0b1010) also supported

Primitives 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 itself
String 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 independent
String t = s; // t gets a copy of the reference; both point to the same object

And critically โ€” primitives can never be null, objects can:

int x = null; // compile error
String y = null; // fine โ€” and the source of a million NullPointerExceptions

Wrapper 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 object
int first = scores.get(0); // unboxing: Integer โ†’ int

Convenient โ€” 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 runtime

get 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 here
for (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"; // String
var scores = new ArrayList<Integer>(); // ArrayList<Integer>
var entry = Map.entry("k", 1); // Map.Entry<String,Integer> โ€” nice win

Important: 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:


Conversions and Casting

Widening (small โ†’ large) happens automatically because no information can be lost:

int i = 100;
long l = i; // fine
double d = l; // fine

Narrowing (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 round
long big = 4_000_000_000L;
int oops = (int) big; // -294967296 โ€” silent overflow, your problem now

For rounding rather than truncation, use Math.round(price).

Strings convert via parse methods, and failure throws:

int n = Integer.parseInt("42"); // 42
int bad = Integer.parseInt("42.5"); // NumberFormatException
String s = String.valueOf(42); // "42" โ€” the other direction

Wrap 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:

  1. Whole numbers โ†’ int. Move to long for timestamps, database IDs, byte counts, or anything that could exceed 2.1 billion.
  2. Decimals โ†’ double, except money โ†’ BigDecimal or cents-as-long. No exceptions to the money rule. Ever.
  3. Flags โ†’ boolean. Java has no truthiness; conditions must be actual booleans, which kills the if (count) bugs other languages allow.
  4. Text โ†’ String, even single characters in most application code; char earns its keep only in low-level parsing.
  5. In collections and generics โ†’ wrappers, everywhere else โ†’ primitives.
  6. 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.