Data Types & Variables Interview Questions & Answers

33 questions Updated 2026-06-18

Java data types and variables interview questions — primitives vs wrappers, autoboxing, Integer caching, String immutability and the pool, pass-by-value, casting and the var keyword.

Read the in-depth guideJava Data Types & Variables — Primitives, Wrappers & the String Pool

Java has exactly eight primitives, each with a fixed size and no methods:

Type Size Range / notes
byte 8-bit −128…127
short 16-bit −32,768…32,767
int 32-bit ~±2.1 billion (default for integer literals)
long 64-bit very large; literal suffix L
float 32-bit IEEE-754, suffix f
double 64-bit IEEE-754 (default for decimal literals)
char 16-bit a single UTF-16 code unit, 065535
boolean JVM-defined true / false

Primitives hold their value directly (on the stack or inline in an object), unlike objects which are accessed through references. Everything else in Java — String, arrays, your classes — is a reference type.

Every primitive has an object wrapper in java.lang: int->Integer, double->Double, char->Character, boolean->Boolean, etc. The primitive stores a raw value; the wrapper is a full object on the heap that contains the value plus object overhead (header, identity, can be null).

int a = 5;              // raw value, can never be null
Integer b = 5;          // object reference, can be null
Integer c = null;       // legal; int x = c; would throw NullPointerException

You need wrappers wherever Java requires an object: generics (List<Integer>, never List<int>), collections, and nullable fields. Primitives are faster and lighter, so prefer them for arithmetic and hot loops.

Autoboxing is the compiler automatically converting a primitive to its wrapper; unboxing is the reverse. It lets primitives and wrappers mix seamlessly — Integer.valueOf(...) / intValue() calls are inserted for you.

List<Integer> nums = new ArrayList<>();
nums.add(5);            // autobox: int 5 -> Integer.valueOf(5)
int first = nums.get(0); // unbox: Integer -> intValue()

The hidden cost: unboxing a null wrapper throws NullPointerException, and boxing in a tight loop creates throwaway objects (GC pressure). A classic trap is Integer sum = 0; for (...) sum += x; which boxes/unboxes every iteration — use a primitive int accumulator instead.

Because == on wrappers compares references, and Java caches boxed Integer objects for the range −128 to 127 (the IntegerCache). Values in that range return the same cached object; values outside it create new objects each time.

Integer a = 127, b = 127;
System.out.println(a == b);   // true  — same cached object

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

The lesson interviewers want: always compare wrapper values with .equals() (or unbox to primitives), never ==. The cache exists because small integers are extremely common, so reusing them saves allocations.

== compares references for objects (do both variables point to the same object?) and raw values for primitives. .equals() is a method that compares logical equality — what "equal" means is defined by the class.

String a = new String("hi");
String b = new String("hi");
a == b        // false — two different objects
a.equals(b)   // true  — same characters

int x = 5, y = 5;
x == y        // true  — primitive value comparison

Object.equals defaults to == (reference identity), so a class must override equals to get meaningful value comparison — String, Integer, and the collections all do.

A String's internal character data is final and never changes after construction; any "modification" returns a new String. Immutability buys several things: safe sharing in the string pool, thread safety without locks, usability as hash keys (the hash can be cached and never goes stale), and security (a path/URL can't be mutated after a check).

String s = "hello";
s.concat(" world");      // creates a new String, discarded here
System.out.println(s);   // "hello" — s itself is unchanged
s = s.concat(" world");  // reassign the reference to the new String

The trade-off is that heavy string building creates garbage — which is exactly why StringBuilder exists.

The string pool is a special area where the JVM stores one shared copy of each distinct string literal. Two identical literals refer to the same pooled object, saving memory. Strings made with new are not pooled — they create a fresh heap object — unless you call .intern().

String a = "hi";          // pooled
String b = "hi";          // same pooled object
String c = new String("hi"); // new heap object, NOT pooled
a == b           // true
a == c           // false
a == c.intern()  // true — intern() returns the pooled copy

This is why beginners hit bugs comparing strings with ==: literals seem to work (same pooled object) but new/computed strings don't. Always use .equals().

  • String — immutable; every concatenation allocates a new object. Fine for a few fixed pieces, wasteful in loops.
  • StringBuilder — a mutable, resizable character buffer. Not synchronized, so it's fast; the right default for building strings.
  • StringBuffer — same API as StringBuilder but synchronized (thread-safe), hence slower. Rarely needed today.
// O(n²) garbage — a new String each iteration
String r = "";
for (String p : parts) r += p;

// one buffer, amortized O(n)
StringBuilder sb = new StringBuilder();
for (String p : parts) sb.append(p);
String r2 = sb.toString();

Note the compiler optimizes simple a + b + c into StringBuilder calls — the problem is concatenation inside loops, where it can't.

Java is always pass-by-value — no exceptions. The subtlety is that for objects, the value being copied is the reference (the pointer), not the object. So a method can mutate the object the reference points to, but reassigning the parameter doesn't affect the caller's variable.

void mutate(int[] arr) { arr[0] = 99; }   // changes the caller's array
void reassign(int[] arr) { arr = new int[]{0}; } // no effect on caller

int[] a = {1};
mutate(a);    // a is now {99}
reassign(a);  // a is still {99} — the copied reference was replaced locally

Say it crisply in an interview: "Java passes references by value." The reference is copied; both copies point at the same object until one is reassigned.

Instance and static fields are automatically initialized to a zero value: 0 for numeric primitives, false for boolean, \u0000 (the null character) for char, and null for any reference type.

class Box {
  int n;        // 0
  boolean ok;   // false
  String name;  // null
}

The crucial exception: local variables get no default. The compiler requires you to assign one before use, or it's a compile error ("variable might not have been initialized") — a deliberate guard against reading garbage.

Integer arithmetic wraps around silently using two's-complement — no exception is thrown. Exceeding Integer.MAX_VALUE rolls over to Integer.MIN_VALUE.

int max = Integer.MAX_VALUE;   // 2_147_483_647
System.out.println(max + 1);   // -2147483648  (wraps, no error)

// common bug: this overflows before being assigned to long
long bad = 1_000_000 * 1_000_000;        // -727379968
long good = 1_000_000L * 1_000_000;      // 1000000000000 (promote first)

Mitigations: promote to long, use Math.addExact/multiplyExact (which throw ArithmeticException on overflow), or BigInteger for unbounded math.

float and double are binary IEEE-754 floating point. Many decimal fractions (like 0.1) have no exact binary representation, so they're stored as the nearest approximation and tiny errors accumulate.

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

For money or anything needing exact decimals, use BigDecimal (and construct it from a String, not a double):

new BigDecimal("0.1").add(new BigDecimal("0.2")); // 0.3 exactly

Never compare floats with ==; compare within a small epsilon instead.

char is a 16-bit unsigned integer holding a single UTF-16 code unit ('A', '7', '€'). Because it's numeric under the hood, it participates in arithmetic and auto-promotes to int.

char c = 'A';
int code = c;            // 65 (implicit widening)
char next = (char) (c + 1); // 'B' (must cast back, since c+1 is int)
System.out.println('a' + 'b'); // 195, not "ab" — numeric addition!

A frequent gotcha: 'a' + 'b' adds the code points, while "" + 'a' + 'b' concatenates. Characters beyond the Basic Multilingual Plane (emoji) need two chars (a surrogate pair) or int code points.

Widening (smaller -> larger type, e.g. int->long->double) is safe and implicit — no data loss, no cast needed. Narrowing (larger -> smaller, e.g. double->int) can lose data, so it requires an explicit cast and you accept the truncation.

int i = 100;
long l = i;          // widening — automatic
double d = i;        // widening — automatic

double pi = 3.99;
int n = (int) pi;    // narrowing — explicit; truncates to 3 (not rounded)
byte b = (byte) 300; // narrowing — overflows to 44

Narrowing truncates toward zero for floating->integer (it doesn't round), and wraps for integer overflow — both common interview "what prints?" traps.

var (Java 10+) is local variable type inference: the compiler infers the static type from the initializer. It's still statically typedvar is not a dynamic/Object type — just less verbose.

var list = new ArrayList<String>();  // inferred ArrayList<String>
var count = 10;                      // int
for (var entry : map.entrySet()) { } // inferred Map.Entry<...>

Restrictions: only for local variables with an initializer. You can't use var for fields, method parameters, return types, or without an initializer (var x; is illegal), and var x = null; won't compile (no type to infer).

A final variable can be assigned only once. For a primitive that fixes the value; for a reference it fixes which object the variable points to — the object itself can still be mutated.

final int MAX = 10;     // MAX = 11; would not compile
final List<String> xs = new ArrayList<>();
xs.add("ok");           // mutating the object is allowed
xs = new ArrayList<>(); // reassigning the reference is not

final also applies to fields (must be set by the end of construction), method parameters, and is required for variables a lambda/anonymous class captures (they must be final or effectively final).

A static (class) variable belongs to the class — there's exactly one copy shared by all instances. An instance variable belongs to each object — every new gets its own copy.

class Counter {
  static int total;   // one shared across all Counters
  int id;             // one per instance
  Counter() { id = ++total; }
}
new Counter(); // id=1, total=1
new Counter(); // id=2, total=2  (total is shared)

Access statics via the class name (Counter.total). Statics are initialized when the class is loaded and live for the program's lifetime, which is why mutable static state is a common source of bugs and memory leaks.

Literals default to int (integers) and double (decimals); suffixes change that: L/l for long, f/F for float, d/D for double. You can also write binary (0b), octal (0), and hex (0x) literals, and use underscores as digit separators for readability.

long big   = 10_000_000_000L;  // L needed — exceeds int range
float rate = 1.5f;
int  mask  = 0xFF;             // 255 in hex
int  bits  = 0b1010;           // 10 in binary
int  million = 1_000_000;      // underscores ignored by the compiler

Forgetting the L on a large literal is a classic bug: 10_000_000_000 alone won't compile because it overflows int.

The ternary condition ? a : b is an expression that evaluates to a when the condition is true, else b — a compact if/else that returns a value.

String label = (n % 2 == 0) ? "even" : "odd";
int max = (a > b) ? a : b;

The subtle trap is type promotion / unboxing of the two branches: if one branch is a primitive and the other a wrapper, the result is unboxed, so a null wrapper branch can throw NullPointerException:

Integer x = null;
Object o = true ? 0 : x;   // fine
int bad  = false ? 0 : x;  // NPE — both branches unboxed to int

An array is an object on the heap with a fixed length set at creation; the variable holds a reference to it. Elements are stored contiguously and default to their zero value (0/false/null).

int[] a = new int[3];        // {0, 0, 0}
int[] b = {1, 2, 3};         // array initializer
String[] names = new String[2]; // {null, null}
System.out.println(a.length);   // 3 — a field, not a method

Key facts: length is a field (no parentheses), the size is immutable once created (need a bigger array? allocate a new one or use ArrayList), and out-of-bounds access throws ArrayIndexOutOfBoundsException.

Java has no true 2D arrays — a int[][] is an array of arrays. That means rows can have different lengths (a jagged array), and each row is a separate heap object.

int[][] grid = new int[2][3];   // 2 rows, 3 cols each
grid[0][1] = 5;

int[][] jagged = new int[2][];  // rows allocated separately
jagged[0] = new int[]{1, 2};
jagged[1] = new int[]{3, 4, 5}; // different length — legal

Because rows are independent objects, iterating with row.length (not a fixed column count) is the safe pattern.

In arithmetic, operands smaller than int (byte, short, char) are first promoted to int, and if any operand is long/float/double the whole expression is promoted to the widest type. This is why byte arithmetic returns an int.

byte a = 10, b = 20;
byte c = a + b;        // won't compile — a + b is int
byte d = (byte)(a + b); // cast back

int i = 5;
double e = i / 2;      // 2.0 — int division happens FIRST, then widens
double f = i / 2.0;    // 2.5 — one double operand promotes the division

The 5 / 2 == 2 integer-division surprise is the most common version of this in interviews.

null is a special literal meaning a reference points to no object. Only reference types can be null — primitives cannot. Using a null reference (calling a method, accessing a field, unboxing) throws NullPointerException.

String s = null;
s.length();          // NullPointerException
int[] a = null;
int n = a.length;    // NPE
Integer boxed = null;
int x = boxed;       // NPE — unboxing null

null is the same regardless of type, and instanceof on null is always false. Modern Java offers Optional and (Java 14+) helpful NPE messages that name the exact variable that was null.

Every class implicitly extends java.lang.Object, inheriting:

  • equals(Object) — logical equality (default: reference identity).
  • hashCode() — int hash, must be consistent with equals.
  • toString() — string form (default: ClassName@hexHash).
  • getClass() — runtime class object.
  • clone() — shallow copy (protected; needs Cloneable).
  • wait()/notify()/notifyAll() — thread coordination on the object's monitor.
  • finalize() — deprecated cleanup hook.
class Point {
  int x, y;
  @Override public String toString() { return "(" + x + "," + y + ")"; }
}

You'll most often override equals, hashCode, and toString — and the first two must be overridden together to honor their contract.

A variable is visible only within the block ({ }) it's declared in, and ceases to exist when that block ends. Inner blocks can see outer variables, but not vice versa, and you can't redeclare a name that's already in scope.

void m() {
  int x = 1;
  if (x > 0) {
    int y = 2;        // visible only inside this if
    System.out.println(x + y);
  }
  // y is out of scope here
}

Loop variables (for (int i ...)) are scoped to the loop. Unlike fields, local variables have no default and must be assigned before use.

Assigning a subclass reference to a superclass variable (upcasting) is implicit and always safe. Going the other way (downcasting) needs an explicit cast and is checked at runtime — a wrong cast throws ClassCastException.

Object o = "hello";          // upcast — implicit
String s = (String) o;       // downcast — explicit, succeeds
Integer bad = (Integer) o;   // compiles, but throws ClassCastException

if (o instanceof String str) // pattern matching (Java 16+) guards the cast
    System.out.println(str.length());

Guard downcasts with instanceof (ideally the pattern form) to avoid runtime failures.

The idiom is static final with an UPPER_SNAKE_CASE name. static means one shared copy; final means it can't be reassigned. The compiler can inline static final primitive/String constants for efficiency.

public class Config {
  public static final int MAX_RETRIES = 3;
  public static final String APP_NAME = "Interviews";
}

Note final on a reference constant only freezes the reference, not the object — static final List<String> X = new ArrayList<>() can still be mutated, so use List.of(...) or Collections.unmodifiableList for a truly constant collection.

Use the wrapper classes' static methods. parseXxx returns a primitive; valueOf returns a wrapper object. Going the other way, String.valueOf or concatenation produces text.

int n      = Integer.parseInt("42");     // primitive int
Integer w  = Integer.valueOf("42");       // Integer object
double d   = Double.parseDouble("3.14");
String s   = String.valueOf(42);          // "42"
String s2  = "" + 42;                      // "42" (concatenation)

A malformed string throws NumberFormatException, so wrap parsing of untrusted input in a try/catch or validate first.

Java has & (AND), | (OR), ^ (XOR), ~ (NOT), and shifts <<, >> (signed/arithmetic right shift), and >>> (unsigned/logical right shift, which fills with zeros).

5 & 3    // 1   (0101 & 0011)
5 | 3    // 7
5 ^ 3    // 6
1 << 4   // 16  (multiply by 2^4)
-8 >> 1  // -4  (sign-preserving)
-8 >>> 28 // 15 (zero-filled — treats bits as unsigned)

>>> is unique to Java (no unsigned types) and is the trap: it's the only shift that ignores the sign bit. Bitwise &/| also work on boolean as non-short-circuiting logical operators.

The enhanced for (for-each) iterates any array or Iterable without an index, reading each element in turn.

for (String name : names) {
  System.out.println(name);
}

Its limits, often asked about: you can't get the index, you can't iterate two collections in lockstep, and you can't modify the collection during iteration (it uses an iterator under the hood, so adding/removing throws ConcurrentModificationException). Reassigning the loop variable also doesn't change the underlying element — use a classic indexed for or an explicit Iterator for those cases.

An expression evaluates to a value (a + b, x > 0, obj.method()); a statement is a complete instruction that performs an action (if, for, assignments, return). Expressions can be nested inside statements.

int max = (a > b) ? a : b;   // ternary is an EXPRESSION (has a value)
if (a > b) { max = a; }      // if is a STATEMENT (no value)

This distinction explains why you can write int x = a > b ? a : b; but not int x = if (...) ...;if produces no value. (Java's switch gained an expression form in Java 14 that does yield a value.)

Text blocks (Java 15+) are multi-line string literals delimited by triple quotes """. They preserve line breaks and let you write JSON, SQL, or HTML without escaping quotes or concatenating lines.

String json = """
    {
      "name": "Ada",
      "role": "engineer"
    }
    """;

Incidental leading whitespace is stripped based on the closing delimiter's indentation, and the result is just a normal String — so it still goes through the pool and supports all String methods.

An identifier may contain letters, digits, _, and $, but can't start with a digit, can't be a reserved keyword (class, int, for…), and is case-sensitive (countCount). Unicode letters are allowed.

int count, _total, $price, αβγ;  // all legal
int 2fast;   // can't start with a digit
int class;   // reserved keyword

By convention (not enforced): camelCase for variables/methods, PascalCase for types, UPPER_SNAKE_CASE for constants, and $ is left for generated code.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.