Stage 1: Java fundamentals, lesson 2 of 10

Variables and data types

Beginner3 min read@since 10Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Java is statically typed: every variable has a type that the compiler checks before the program runs.

There are 8 primitive types stored directly as values: byte, short, int, long, float, double, char and boolean. Everything else is a reference type that points to an object on the heap, such as String, arrays and your own classes.

Each primitive has a wrapper class (Integer, Double, Boolean…) so it can be stored in collections. Java converts between them automatically; this is called autoboxing (Java 5).

Since Java 10 you can write var for local variables and let the compiler infer the type. The type is still fixed at compile time; var is not dynamic typing.

The eight primitive types

  • byte (8-bit, -128 to 127), short (16-bit), int (32-bit, about ±2.1 billion), long (64-bit)
  • float (32-bit) and double (64-bit) for decimals
  • char (a 16-bit UTF-16 unit, such as 'A')
  • boolean (true or false)

Use int for whole numbers, long for large counts, ids and timestamps, double for measurements, and BigDecimal for money.

Literals: writing values in code

Underscores make big numbers readable. Prefixes choose the base, and suffixes choose the type.

Java
int million = 1_000_000;
int mask = 0b1010_1010;     // binary
int colour = 0xFF8800;      // hexadecimal
long big = 9_000_000_000L;  // L for long
float ratio = 0.75f;        // f for float
char letter = 'A', rupee = '\u20B9';
int oops = 017;             // octal! this is 15, not 17

var: local type inference

Since Java 10, var lets the compiler infer a local variable's type from its initializer. It's still statically typed. It works only for local variables that are initialised, not for fields, parameters or return types. Use it when the type is obvious from the right-hand side.

Java
var names = new ArrayList<String>();   // ArrayList<String>
var total = 0L;                         // long
for (var entry : map.entrySet()) { }    // Map.Entry<K, V>
// var x;           // compile error: needs an initializer
// var n = null;    // compile error: type can't be inferred

Type conversion and casting

Widening (smaller to larger, such as int to long or double) happens automatically. Narrowing needs an explicit cast and can lose information: decimals are truncated and large values wrap around.

Java
int i = 42;
long l = i;              // widening: automatic
double d = i;            // 42.0

int truncated = (int) 3.99;    // 3
byte wrapped = (byte) 200;     // -56 (200 doesn't fit in a byte)
char c = (char) 66;            // 'B'
int code = 'A';                // 65

Wrapper classes and autoboxing

Each primitive has a wrapper class: Integer, Long, Double, Character, Boolean and so on. Collections need objects, so Java converts automatically (autoboxing and unboxing). Two traps: unboxing null throws NullPointerException, and == on wrappers compares references.

Java
List<Integer> scores = new ArrayList<>();
scores.add(90);                  // autoboxing int -> Integer
int first = scores.get(0);       // unboxing

Integer a = 127, b = 127, x = 128, y = 128;
System.out.println(a == b);      // true  (cached values -128..127)
System.out.println(x == y);      // false (different objects)
System.out.println(x.equals(y)); // true: always use equals

Integer missing = null;
int boom = missing;              // NullPointerException

Default values, scope and constants

Fields get default values (0, false, null); local variables don't and must be assigned before use. A variable lives only inside the block ({ }) where it's declared. Mark values that never change as final; constants are static final and written in UPPER_SNAKE_CASE.

Integer overflow

Integer arithmetic silently wraps around when it goes past the maximum. Use long, the Math.*Exact methods that throw on overflow, or BigInteger for arbitrarily large numbers.

Java
int max = Integer.MAX_VALUE;         // 2147483647
System.out.println(max + 1);          // -2147483648
long ok = (long) max + 1;             // 2147483648
Math.addExact(max, 1);                // throws ArithmeticException
BigInteger huge = BigInteger.TWO.pow(100);

Example

Java
int age = 30;                       // 32-bit whole number
long population = 8_100_000_000L;   // underscores allowed since Java 7
double price = 499.99;
char grade = 'A';
boolean active = true;
String name = "Asha";

Integer boxed = age;                // autoboxing
int back = boxed;                   // unboxing

var cities = new ArrayList<String>();   // Java 10+: inferred as ArrayList<String>
cities.add("Pune");

Common mistake

Comparing wrapper objects with ==. Integer a = 1000, b = 1000; a == b is false. Use a.equals(b).

Under the hood

Local primitives live in the method's stack frame and cost no allocation, while wrappers are heap objects. Integer caches the values -128 to 127, which is why == seems to "work" for small numbers and then fails for larger ones. Integer overflow wraps silently: Integer.MAX_VALUE + 1 is negative. Use Math.addExact or a long when that matters, and BigDecimal for money.

Check yourself

What does Integer a = 128, b = 128; a == b give?

How this connects

Part of Java from zero, Upgrade from Java 8 to Java 25.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.