What Are Variables and Data Types?
A variable is a named container that holds a value your program can read and modify. Every variable has a data type that tells the JVM two things: what kind of value it can hold, and how much memory to allocate for it. Without variables there is no program ā they are the fundamental mechanism for storing state.
In Java, you must declare the type of a variable before you use it. This is what static typing means: the type is known at compile time, not discovered at runtime. The compiler uses that information to catch errors before your code ever runs ā trying to store a text string in an integer variable is rejected immediately, not after deployment.
// Every variable declaration has three parts:
// type name value
int age = 25;
String name = "Alice";
boolean active = true;
double price = 19.99;
// The compiler rejects mismatches immediately:
int count = "hello"; // compile error: incompatible types
int count;
System.out.println(count); // compile error: variable might not be initialised
The data type also determines what operations are valid. You can
do arithmetic on an int, call .toUpperCase() on a
String, and compare a boolean with if ā
but not the other way around. This is the contract the type system enforces.
In a large codebase or team, static types are documentation that the compiler
verifies. When a method declares it returns int, every caller
knows exactly what they get ā no guessing, no runtime surprises. This is one
of the main reasons Java has dominated enterprise development for 30 years:
the type system catches a significant class of bugs before the code ships.
Java's Type System
Java is statically typed: every variable has a declared type fixed at compile time. The compiler rejects type mismatches before the code ever runs. All types fall into two categories:
- Primitive types ā 8 built-in types (
int,boolean,doubleā¦). Store actual values. Cannot benull. Live on the stack. - Reference types ā everything else (classes, arrays, interfaces). Store a memory
address pointing to an object on the heap. Can be
null.
- Declare before use:
int count = 0;ā type, name, value. - Local variables have no default ā use them uninitialised and the compiler errors.
- Instance fields do get defaults (0, false, null) ā but don't rely on that; initialise explicitly.
- Naming:
camelCasefor variables,UPPER_SNAKE_CASEfor constants.
How Variables Live in Memory
This is the most important mental model in Java. Get it wrong and NullPointerExceptions and unexpected mutations will haunt you.
/*
* JVM MEMORY ā Variable Storage
* ================================
*
* āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
* ā STACK (per thread) ā
* ā ā
* ā Method frame: main() ā
* ā āāāāāāāāāāāāā¬āāāāāāāāā ā
* ā ā int age ā 25 ā ā Primitive: VALUE stored directly ā
* ā āāāāāāāāāāāāā¼āāāāāāāā⤠ā
* ā ā name ā 0x742 ā ā Reference: ADDRESS to heap object ā
* ā āāāāāāāāāāāāā“āāāā¬āāāāā ā
* āāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
* ā
* āāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
* ā HEAP (shared by all threads) ā
* ā ā
* ā 0x742: āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
* ā ā String "Alice" ā ā
* ā ā value: ['A','l','i','c'] ā ā
* ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
* āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
*
* PRIMITIVES ā value on stack (fast, no pointer lookup)
* REFERENCES ā address on stack, object on heap (GC managed)
*/
The assignment trap: primitives vs references
// PRIMITIVES: assignment copies the VALUE
int a = 10;
int b = a; // b gets its own copy of 10
b = 20;
System.out.println(a); // Still 10. a is unaffected.
// REFERENCES: assignment copies the ADDRESS (same object!)
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1; // arr2 points to the SAME array
arr2[0] = 999;
System.out.println(arr1[0]); // 999 ā arr1 sees the change!
Java is always pass-by-value. For primitives, the method gets a copy ā changes don't affect the caller. For objects, the method gets a copy of the reference ā the caller's variable still points to the same object, so mutations to the object ARE visible, but reassigning the parameter is not.
Primitive Data Types
Java has exactly 8 primitives. Sizes are platform-independent ā an int is always 32 bits
on every JVM, unlike C/C++.
| Type | Size | Range | Default | When to use |
|---|---|---|---|---|
byte |
8 bits | -128 to 127 | 0 | Binary data, byte streams |
short |
16 bits | -32,768 to 32,767 | 0 | Rarely ā legacy/interop only |
int |
32 bits | -2,147,483,648 to 2,147,483,647 | 0 | Default choice for integers |
long |
64 bits | ±9.2 à 1018 | 0L | Timestamps, IDs, large counters |
float |
32 bits | ~6-7 sig. digits | 0.0f | Graphics, memory-critical only |
double |
64 bits | ~15-16 sig. digits | 0.0d | Default choice for decimals |
char |
16 bits | 0 to 65,535 (Unicode) | '\u0000' | Single Unicode character |
boolean |
JVM-dependent | true / false | false | Flags, conditions |
// Integer literals ā Java 7+ allows underscores for readability
int million = 1_000_000;
long timestamp = 1_716_000_000_000L; // L suffix required for long literals > int range
int hex = 0xFF; // 255
int binary = 0b1010_0011; // 163
// Floating point
double pi = 3.141592653589793; // double is the default
float price = 19.99f; // f suffix required for float literals
// char is numeric ā arithmetic works
char c = 'A';
System.out.println((int) c); // 65
System.out.println((char)(c + 1)); // B
// boolean: integers are NOT valid in Java (unlike C/C++)
// if (1) { } // ERROR ā only true/false allowed
IEEE 754 cannot represent many decimal fractions exactly. 0.1 + 0.2 is
0.30000000000000004. For financial calculations, always use
BigDecimal with String constructors (not double constructors):
BigDecimal price = new BigDecimal("19.99"); // ā
exact
BigDecimal wrong = new BigDecimal(19.99); // ā inherits float imprecision
Reference Types: String, Arrays, Wrappers
String
String is immutable ā every "modification" creates a new object. String literals
are interned in the String Pool; new String(...) always creates a heap object.
// Literals go to the String Pool
String name = "John Doe";
// Useful methods
name.length(); // 8
name.toUpperCase(); // "JOHN DOE"
name.contains("Doe"); // true
name.replace("John", "Jane"); // "Jane Doe"
name.split(" "); // ["John", "Doe"]
" hello ".strip(); // "hello" (Java 11+, Unicode-aware)
// Text blocks ā Java 15+
String json = """
{
"name": "John",
"age": 30
}
""";
// Formatting ā Java 15+
String msg = "Hello %s, you are %d".formatted(name, 30);
// ā WRONG: == compares references, not content
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
System.out.println(Objects.equals(s1, s2)); // true + null-safe
// StringBuilder for building strings in loops
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) sb.append("a");
String result = sb.toString();
// String concatenation in a loop creates 10,000 intermediate objects ā use StringBuilder
Arrays
// Declaration and initialisation
int[] numbers = {1, 2, 3, 4, 5};
int[] scores = new int[10]; // All zeros by default
String[] names = {"Alice", "Bob"};
// Arrays are fixed-size ā use List for dynamic collections
numbers[0]; // 1 (0-indexed)
numbers[numbers.length - 1]; // last element
numbers.length; // field, not method!
// 2D array
int[][] matrix = {{1,2},{3,4}};
int val = matrix[1][0]; // 3
Wrapper Classes
Each primitive has an object wrapper needed for generics and collections. Java handles conversion automatically via autoboxing/unboxing.
List<Integer> list = new ArrayList<>();
list.add(42); // Autoboxing: int ā Integer
int x = list.get(0); // Unboxing: Integer ā int
// Useful utility methods
int parsed = Integer.parseInt("123");
String bin = Integer.toBinaryString(42); // "101010"
int max = Integer.MAX_VALUE; // 2,147,483,647
boolean dig = Character.isDigit('5'); // true
// ā DANGER: unboxing null throws NullPointerException
Integer maybeNull = null;
int value = maybeNull; // NullPointerException at runtime!
// ā
SAFE
int value = (maybeNull != null) ? maybeNull : 0;
int value = Optional.ofNullable(maybeNull).orElse(0);
var, final and Scope
Local variable type inference: var (Java 10+)
// var: compiler infers type from the right-hand side
var name = "Alice"; // String
var age = 25; // int
var prices = new ArrayList<Double>(); // ArrayList<Double>
var map = Map.of("a", 1); // Map<String, Integer>
// var restrictions ā all compile errors:
// var x; no initialiser
// var nothing = null; cannot infer from null
// var nums = {1, 2, 3}; cannot infer array type
// Only for local variables ā not fields, params, return types
Use var when the type is obvious from the initialiser:
var response = httpClient.send(request, BodyHandlers.ofString());
Avoid var when the type isn't self-evident:
var result = calculate(); ā what type is this? Makes code review harder
in teams. IntelliJ shows the inferred type on hover, but reviewers on GitHub don't have that.
final variables and constants
// final local variable: cannot be reassigned after first assignment
final int MAX_RETRIES = 3;
// MAX_RETRIES = 5; // ERROR
// Class-level constant: public static final + UPPER_SNAKE_CASE
public static final int DEFAULT_TIMEOUT_MS = 30_000;
public static final String DB_URL = "jdbc:mysql://localhost:3306/mydb";
// IMPORTANT: final on a reference freezes the reference, NOT the object
final List<String> names = new ArrayList<>();
names.add("Alice"); // ā
modifying the object is allowed
// names = new ArrayList<>(); // ā reassigning the reference is not
Variable scope
public class ScopeExample {
private static int classCounter = 0; // class scope ā shared by all instances
private String instanceField = "hello"; // instance scope ā each object gets its own
public void doWork(String param) { // param: method scope
int local = 10; // method scope
for (int i = 0; i < 5; i++) { // i: loop scope only
int loopVar = i * 2; // loopVar: loop scope only
}
// i and loopVar not accessible here
}
}
Type Conversion
Widening (implicit) ā no data loss
// byte ā short ā int ā long ā float ā double (automatic)
byte b = 10;
int i = b; // automatic
double d = i; // automatic
// ā ļø Arithmetic promotes operands to at least int
byte b1 = 10, b2 = 20;
// byte result = b1 + b2; // ERROR: b1+b2 is int
int result = b1 + b2; // OK
Narrowing (explicit cast) ā may lose data
double d = 100.99;
int i = (int) d; // 100 ā decimal part silently lost
int big = 130;
byte small = (byte) big; // -126 ā overflow! 130 doesn't fit in byte
// Safe pattern: validate range before casting
long value = getSomeValue();
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
int safe = (int) value;
} else {
throw new ArithmeticException("Value out of int range");
}
String conversions
// Primitive ā String
String s = String.valueOf(42); // preferred
String s = Integer.toString(42); // also fine
// String ā Primitive
int n = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
boolean b = Boolean.parseBoolean("true");
long l = Long.parseLong("999999999999");
// Always guard parsing ā throws NumberFormatException on bad input
try {
int val = Integer.parseInt(userInput);
} catch (NumberFormatException e) {
// handle gracefully
}
Naming Conventions
| Element | Convention | Examples |
|---|---|---|
| Variables & methods | camelCase | userName, totalAmount, calculateTotal() |
| Classes & interfaces | PascalCase | UserAccount, HttpClient |
| Constants | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT_MS |
| Packages | lowercase, reversed domain | com.example.myapp.service |
| Boolean variables | is/has/can/should prefix | isActive, hasPermission, canEdit |
// ā BAD: vague, single letters, type prefixes
int x = 42;
String strName = "John";
boolean flag = true;
// ā
GOOD: intention-revealing, no type in name
int userAge = 42;
String customerName = "John";
boolean isEmailVerified = true;
Common Pitfalls
double result = 5 / 2; // 2.0, NOT 2.5 ā both operands are int
double result = 5 / 2.0; // ā
2.5 ā one operand is double
double result = (double) 5 / 2; // ā
2.5
int overflow = Integer.MAX_VALUE + 1; // -2,147,483,648 (wraps!)
// ā
Use Math.addExact() ā throws ArithmeticException on overflow
int safe = Math.addExact(Integer.MAX_VALUE, 1);
// ā
Or use long
long result = (long) Integer.MAX_VALUE + 1;
String a = "hello";
String b = new String("hello");
a == b; // false ā different objects
a.equals(b); // true ā same content
Objects.equals(a, b); // ā
true + null-safe
double sum = 0.1 + 0.2;
sum == 0.3; // false! (sum is 0.30000000000000004)
// ā
Compare with epsilon
Math.abs(sum - 0.3) < 1e-10; // true
// ā
Or use BigDecimal for exact arithmetic
new BigDecimal("0.1").add(new BigDecimal("0.2"))
.equals(new BigDecimal("0.3")); // true
Senior Topics: Performance and Hidden Traps
Autoboxing performance cost
// ā SLOW: Integer wrapper creates objects on every iteration
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i; // unbox, add, rebox ā allocates ~1M Integer objects
}
// ā
FAST: primitive, no allocation
int sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i;
}
// ~10-15x faster. Use primitives in hot paths.
The Integer cache trap (classic senior interview question)
// JVM caches Integer instances from -128 to 127
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true ā same cached object
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false ā different objects, outside cache
// Lesson: NEVER use == to compare Integer objects.
// Always use .equals() or unbox to int first.
int = 4 bytes on the stack. Integer = 16 bytes on the heap (object
header + value) plus the 4-byte reference on the stack. An int[1000] costs ~4 KB.
An Integer[1000] costs ~20 KB. For large arrays in memory-sensitive code (caches,
processing pipelines), use primitive arrays or libraries like Eclipse Collections that support
primitive collections natively.
Modern alternative: records (Java 16+)
When you need a typed data carrier, prefer record over a class with fields.
Records are immutable by default, generate equals(), hashCode()
and toString() automatically, and make intent explicit.
// ā Old way: boilerplate class with fields
public class Point {
private final int x;
private final int y;
// constructor, getters, equals, hashCode, toString... 40 lines
}
// ā
Modern: record ā one line, same behaviour
record Point(int x, int y) {}
Point p = new Point(10, 20);
p.x(); // accessor (no "get" prefix in records)
p.equals(new Point(10, 20)); // true ā structural equality generated
Interview Questions
Q: What's the difference between primitive and reference types?
Primitives store values directly on the stack; cannot be null; no methods. Reference types store
a heap address on the stack; can be null; have methods. Assignment of a primitive copies the
value; assignment of a reference copies the pointer ā both variables then point to the same
object.
Q: Why does 0.1 + 0.2 != 0.3?
IEEE 754 binary floating-point cannot represent most decimal fractions exactly. The closest
binary approximation of 0.1 plus the closest approximation of 0.2 does not equal the closest
approximation of 0.3. Use BigDecimal with String constructors for exact arithmetic.
Q: What is autoboxing?
Automatic conversion between primitive and wrapper type. list.add(42) autoboxes
int to Integer. Unboxing a null wrapper throws
NullPointerException ā a common production bug.
Q: What is the Integer cache and why does it matter?
The JVM caches Integer instances for values -128 to 127. Integer a = 127;
Integer b = 127; a == b is true (same cached instance). For 128, it's
false. This is why you must never use == to compare wrapper objects ā
the results are unpredictable across the cache boundary.
Q: When would you choose long over int for IDs?
Always use long for database primary keys and external IDs. At scale,
auto-increment
int IDs overflow (~2.1 billion). Twitter's Snowflake IDs, UUID representations, and
most ORMs use Long. The JPA @Id with Long is the
standard for a reason.
Q: What's wrong with using String concatenation in a loop?
String is immutable. Each += in a loop allocates a new
String object ā O(n²) allocations total. Use StringBuilder for
explicit loops. Note: the compiler optimises concatenation in a single expression ("a"
+ "b" + "c") to a StringBuilder chain automatically, so this only applies
to loops.
Q: What does final guarantee on a reference type?
Only that the reference cannot be reassigned ā the object itself is fully mutable.
final List<String> list = new ArrayList<>(); list.add("x"); is valid.
For true immutability, the object itself must be designed as immutable (like
String, record, or Collections.unmodifiableList()).