algoz
Chapters/01 Java basics/Variables and primitive types

Variables and primitive types

Store raw values in fixed-size boxes and choose types that never overflow.

You'll gain
Right type, no silent overflowand choose sizes with confidence
Practice
Sheet coming sooncoding problems for this note
Summary

Java stores raw values directly in eight fixed-size primitive types, where picking the right size stops silent overflow before it corrupts your calculations.

The idea in real life

Think of a workshop where tools live in fixed compartments molded to fit each tool. A small compartment holds a single drill bit, while a wide drawer holds a heavy wrench. If you try to force a sledgehammer into a drill bit slot, it will not fit, and if you swap a label, you will grab the wrong tool. Variables are those labeled compartments in computer memory, and primitive types are the exact compartment sizes that keep every number and character in its place.

Why it exists

  1. A computer needs to know how much memory to reserve before it can store a value.
  2. Arithmetic on fixed-size numbers runs directly on CPU registers, making operations fast.
  3. Strict types catch mismatched data, such as putting text into a number slot, at compile time rather than while your program is running.
  4. Picking the wrong type causes silent numeric overflow, where positive sums wrap into negative answers and ruin algorithm results.

How it works

Variables

A named container in memory holding one value of a declared type.

In Java, every variable is a named storage location in memory. Because Java is statically typed, you must declare the type of a variable before you can use it, and that variable can only hold values compatible with its declared type. As you saw in how a Java program runs, statements run sequentially from top to bottom.

Working with a variable involves three actions:

  • Declaration: You state the type and the variable name. This tells the compiler how much space to reserve and what operations are valid.
  • Assignment: You store a value into the variable using the assignment operator (=).
  • Initialisation: The very first assignment given to a variable.

You can declare a variable and initialise it in a single statement:

Main.java
public class Main {
    public static void main(String[] args) {
        int score = 10;
        score = 25;

        final int maxAttempts = 3;

        System.out.println("Current score: " + score);
        System.out.println("Maximum attempts: " + maxAttempts);
    }
}
Output
Current score: 25
Maximum attempts: 3

In this program:

  • score is a variable of type int. It is initialised with 10, and then reassigned to 25.
  • maxAttempts is declared with the final keyword. Marking a variable final creates a constant: once assigned, its value cannot change. Attempting to assign a new value to maxAttempts later in the code causes a compile-time error.

The eight primitive types

Eight built-in types with fixed sizes and exact numeric ranges.

Java provides eight built-in primitive types (defined in the Java Language Specification, JLS §4.2). They represent raw binary values rather than objects.

These eight types fall into four groups:

  • Integer numbers: byte, short, int, and long. All four are signed two's complement integers.
  • Floating-point numbers: float and double. They store numbers with fractional parts using the IEEE 754 standard (covered in note 3, floating point).
  • Characters: char. Represents a single 16-bit Unicode character.
  • Truth values: boolean. Represents logical truth with only two possible values: true or false.
TypeSize in bitsRangeTypical use
byte8 bits-128 to 127Raw binary streams, compact byte arrays
short16 bits-32,768 to 32,767Memory-constrained 16-bit integers, rare in DSA
int32 bits-2,147,483,648 to 2,147,483,647Default integer type: loop counters, array indices, counts
long64 bits-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807Accumulators, prefix sums, products, values over 2.1 billion
float32 bitsapprox ±1.4e-45 to ±3.4e+38 (~6 to 7 decimal digits)Graphics coordinates, memory-sensitive decimal data
double64 bitsapprox ±4.9e-324 to ±1.8e+308 (~15 to 17 decimal digits)Default floating-point type: fractional math, probabilities
char16 bits'\u0000' to '\uffff' (0 to 65,535 unsigned)Single characters, alphabet frequency array indices
booleannot fixed by the specificationtrue or falseConditional flags, visited markers in graphs and grids

All numeric sizes in Java are strictly fixed across every operating system and CPU architecture. An int is guaranteed to be 32 bits on Windows, macOS, and Linux alike.

Literals

How you write constant values directly in your Java source code.

A literal is the source code representation of a fixed value. Java provides syntax options to make literals clear and readable:

  • Integer literals: A whole number without a suffix (such as 42) is treated as an int. To write a long literal, append an uppercase L suffix (such as 100_000_000_000L). Always use uppercase L, because lowercase l looks almost identical to the digit 1.
  • Digit separators: You can place underscores (_) between digits to improve readability in large numbers (such as 1_000_000). The compiler ignores underscores during compilation.
  • Number bases: Write 0x or 0X prefix for hexadecimal (base 16, e.g. 0xFF), and 0b or 0B prefix for binary (base 2, e.g. 0b1011).
  • Character literals: Enclose a single character in single quotes (such as 'A' or '\n'). Double quotes ("A") denote a String object, not a char. Because characters are stored as 16-bit numbers, 'A' has an underlying integer value of 65.
Main.java
public class Main {
    public static void main(String[] args) {
        int million = 1_000_000;
        long planetDistance = 149_600_000_000L;
        int hexMask = 0xFF;
        int binaryMask = 0b1011;
        char letter = 'A';
        int charCode = letter;

        System.out.println("Formatted million: " + million);
        System.out.println("Distance in meters: " + planetDistance);
        System.out.println("Hexadecimal 0xFF: " + hexMask);
        System.out.println("Binary 0b1011: " + binaryMask);
        System.out.println("Letter: " + letter + ", ASCII code: " + charCode);
    }
}
Output
Formatted million: 1000000
Distance in meters: 149600000000
Hexadecimal 0xFF: 255
Binary 0b1011: 11
Letter: A, ASCII code: 65

In this program:

  • million uses underscores for grouping digits.
  • planetDistance requires the L suffix because its value exceeds Integer.MAX_VALUE.
  • hexMask assigns decimal 255 using base-16 notation.
  • binaryMask assigns decimal 11 using base-2 notation (8 + 2 + 1).
  • charCode reads the numeric code of character 'A', which is 65.

Going deeper

Choosing a type for DSA

Use int by default, and choose long when totals can exceed two billion.

In coding interview problems and competitive programming, your choice of type comes down to four practical rules:

  1. Use int by default: Array indices, collection sizes, loop bounds, and element counts always fit comfortably in an int. Standard library methods like array.length and String.length() return int.
  2. Switch to long for sums and products: The maximum value of int is approximately 2.14 billion ($2.14 \times 10^9$). If a problem gives an array of $10^5$ elements where each element can be up to $10^5$, their sum can reach $10^10$. That sum exceeds Integer.MAX_VALUE and will overflow if stored in an int. Use long for prefix sums, accumulators, and products.
  3. Use boolean for state: Tracking whether a graph node has been visited, or whether a condition has been met, belongs in a boolean variable or boolean[] array.
  4. Use char for letter calculations: Because characters are numbers under the hood, subtracting 'a' from any lowercase character normalises it into a zero-based index: 'c' - 'a' evaluates to 2. This allows a plain int[26] array to count letter frequencies without needing a map.

Avoid using byte or short for local calculations. The JVM promotes byte and short operands to int before performing arithmetic anyway (covered in note 5, casting and type promotion), so using smaller types in local code adds cast overhead without saving memory.

Integer overflow

Exceeding a type limit wraps around silently to the opposite extreme.

In Java, integer types have fixed bit widths. When a calculation produces a number larger than the maximum value a type can hold, the extra high-order bits are discarded. The value wraps around into negative numbers.

Java integer arithmetic does not throw an exception when overflow occurs. The calculation proceeds silently with the corrupted value:

Main.java
public class Main {
    public static void main(String[] args) {
        byte x = 126;
        x += 1;
        System.out.println(x);
        x += 1;
        System.out.println(x);
        x += 1;
        System.out.println(x);
    }
}
Output
127
-128
-127

The program uses x += 1 rather than x = x + 1: x + 1 is an int, so x = x + 1 does not compile, while += quietly converts the result back to byte (casting and promotion explains why).

Step through it below. Watch adding 1 flip the bits, and the sign bit turn the largest byte, 127, into the smallest, -128. Switch type to int to see the same thing happen at 2,147,483,647.

It also works the other way: subtracting 1 from the smallest value wraps to the largest, so Integer.MIN_VALUE - 1 is Integer.MAX_VALUE.

This silent wrapping is a frequent source of bugs in algorithms. For example, in binary search, computing the middle index as (left + right) / 2 overflows when left + right exceeds 2_147_483_647. The sum wraps to a negative number, and the division yields a negative index, triggering an ArrayIndexOutOfBoundsException. Writing left + (right - left) / 2 avoids the overflow entirely.

Catching overflow

Detect overflow with Math methods or prevent it by computing in long.

When an algorithm must avoid or detect overflow, Java provides two strategies:

First, compute in a wider type. If you are adding two int variables that might overflow, cast one variable to long before the addition:

Main.java
public class Main {
    public static void main(String[] args) {
        int first = 2_000_000_000;
        int second = 2_000_000_000;

        // Cast to long before adding; otherwise int addition overflows first
        long safeTotal = (long) first + second;
        System.out.println("Safe total: " + safeTotal);
    }
}
Output
Safe total: 4000000000

Notice that writing long total = first + second; without the cast would still overflow: Java evaluates first + second as 32-bit int addition first, produces a wrapped negative result, and only then widens that negative result to long.

Second, use exact math methods. The java.lang.Math class includes overflow-checking methods: Math.addExact, Math.subtractExact, and Math.multiplyExact. These methods perform the arithmetic and throw an ArithmeticException if the result overflows:

OverflowDemo.java
public class OverflowDemo {
    public static void main(String[] args) {
        int limit = Integer.MAX_VALUE;
        int result = Math.addExact(limit, 1);
        System.out.println(result);
    }
}

When you run this program, the JVM throws an exception and halts execution:

text
Exception in thread "main" java.lang.ArithmeticException: integer overflow
	at java.base/java.lang.Math.addExact(Math.java:883)
	at OverflowDemo.main(OverflowDemo.java:4)

In interview problems, computing in long is usually the better choice: you get the right answer instead of an error to handle.

Local variable type inference with var

Let the compiler infer the type when the assignment is already obvious.

Starting in Java 10 (JEP 286), you can declare local variables using the var reserved type name instead of typing the explicit type name:

Main.java
public class Main {
    public static void main(String[] args) {
        var count = 42;
        var message = "Algoz";
        var isReady = true;

        System.out.println("count is an int: " + count);
        System.out.println("message is a String: " + message);
        System.out.println("isReady is a boolean: " + isReady);
    }
}
Output
count is an int: 42
message is a String: Algoz
isReady is a boolean: true

The compiler examines the initialiser expression on the right-hand side and infers the exact type at compile time. Variable count is typed as int, message as String, and isReady as boolean.

Important rules apply to var:

  • Static typing remains: var is not dynamic typing. Once inferred, the type of the variable is permanently fixed. Assigning a String to count later produces a compile-time error.
  • Initialiser is required: Because the compiler must inspect the right-hand side, you cannot declare var x; without assigning a value immediately.
  • Local scope only: You can only use var for local variables inside methods. It cannot be used for method parameters, return types, or class fields.
  • Readability risk with numbers: Writing var total = 0; infers int. If the loop later accumulates values past 2.1 billion, total silently overflows because the reader assumed it was a long. For numeric accumulators in DSA, state the type explicitly: long total = 0L;.

Under the hood

Two's complement representation

Negative numbers invert all bits and add one, creating an asymmetric range.

All signed integer types in Java (byte, short, int, and long) use binary two's complement representation (JLS §4.2.1).

In two's complement:

  • The leftmost bit (the most significant bit) is the sign bit. A 0 indicates a positive number or zero; a 1 indicates a negative number.
  • For a non-negative number, the remaining bits represent the value in standard binary.
  • To negate any number, you invert every bit (change 0 to 1 and 1 to 0) and then add 1 to the result.

For example, consider an 8-bit byte representation of the number 1:

  • 1 in binary: 00000001
  • Invert all bits: 11111110
  • Add 1: 11111111 (this represents -1)

This system provides two major hardware advantages: addition and subtraction use the exact same circuitry, and there is only one representation for zero (00000000).

However, two's complement causes an asymmetric range. With 32 bits, there are $2^32 = 4,294,967,296$ unique bit patterns:

  • One pattern is used for zero: 00000000 00000000 00000000 00000000.
  • Bit patterns starting with 0 (excluding zero) represent positive numbers: $2^31 - 1 = 2,147,483,647$ values (1 to 2,147,483,647).
  • All bit patterns starting with 1 represent negative numbers: exactly $2^31 = 2,147,483,648$ values (-1 down to -2,147,483,648).

Because zero takes one of the non-negative slots, there is one more negative number than positive numbers. The value -2,147,483,648 (Integer.MIN_VALUE) has no positive 32-bit counterpart.

When you call Math.abs(Integer.MIN_VALUE) or compute -Integer.MIN_VALUE, Java attempts to negate -2,147,483,648:

  1. Binary of Integer.MIN_VALUE: 10000000 00000000 00000000 00000000
  2. Invert all bits: 01111111 11111111 11111111 11111111 (which is Integer.MAX_VALUE, 2_147_483_647)
  3. Add 1: wraps back to 10000000 00000000 00000000 00000000 (Integer.MIN_VALUE)

The negation wraps straight back to where it started:

Main.java
public class Main {
    public static void main(String[] args) {
        int lowest = Integer.MIN_VALUE;
        int absoluteResult = Math.abs(lowest);
        int negatedResult = -lowest;

        System.out.println("Integer.MIN_VALUE: " + lowest);
        System.out.println("Math.abs(Integer.MIN_VALUE): " + absoluteResult);
        System.out.println("Negated (-Integer.MIN_VALUE): " + negatedResult);
    }
}
Output
Integer.MIN_VALUE: -2147483648
Math.abs(Integer.MIN_VALUE): -2147483648
Negated (-Integer.MIN_VALUE): -2147483648

Both Math.abs(Integer.MIN_VALUE) and -Integer.MIN_VALUE return -2147483648. If an interviewer asks whether Math.abs(x) is guaranteed to return a non-negative number, the answer is no: when x is Integer.MIN_VALUE or Long.MIN_VALUE, the result is negative.

Default values and definite assignment

Fields receive zeroed defaults, while local variables require an explicit value.

In Java, where a variable is declared determines whether it is automatically initialised (JLS §4.12.5):

  • Fields and array elements: Member variables of a class (both static and instance fields) and newly allocated array elements receive default values automatically from the runtime:
    • byte, short, int, long: 0
    • float, double: 0.0
    • char: '\u0000' (null character, numeric value 0)
    • boolean: false
    • Object references: null
Main.java
public class Main {
    static int defaultNumber;
    static boolean defaultFlag;
    static char defaultLetter;
    static double defaultDecimal;

    public static void main(String[] args) {
        int[] numbers = new int[1];
        boolean[] flags = new boolean[1];

        System.out.println("Default int field: " + defaultNumber);
        System.out.println("Default boolean field: " + defaultFlag);
        System.out.println("Default char code: " + (int) defaultLetter);
        System.out.println("Default double field: " + defaultDecimal);
        System.out.println("Default array element: " + numbers[0]);
        System.out.println("Default boolean array element: " + flags[0]);
    }
}
Output
Default int field: 0
Default boolean field: false
Default char code: 0
Default double field: 0.0
Default array element: 0
Default boolean array element: false
  • Local variables: Local variables declared inside methods do not receive default values. Java enforces a rule called definite assignment (JLS §16). The compiler must be able to prove that a local variable is assigned a value along every execution path before its value is read.

Attempting to read an unassigned local variable causes a compile error:

UnassignedDemo.java
public class UnassignedDemo {
    public static void main(String[] args) {
        int total;
        System.out.println(total);
    }
}

Compiling this code fails immediately:

text
UnassignedDemo.java:4: error: variable total might not have been initialized
        System.out.println(total);
                           ^
1 error

Where primitives live in memory

Primitives store their raw bits directly inside their assigned memory location.

A primitive variable holds its binary value directly in its storage slot, without any object header, method table, or pointer indirection.

Where that memory slot resides depends on where the variable is declared:

  • Local primitive variables declared inside methods live directly on the call stack inside that method's stack frame. When the method finishes execution, its stack frame is discarded and that memory is reused.
  • Primitive fields declared inside an object, along with primitive array elements, live inside that object's memory block on the heap. Chapter 02 covers the stack and the heap in depth.

In Java

Here is a complete program that brings these concepts together: calculating a running total safely using long to guard against overflow, indexing an alphabet frequency table with char, and tracking conditions with boolean flags.

In this program:

  • numbers is an input array containing large integer values.
  • calculateTotalSum iterates through numbers and accumulates values into a 64-bit long to prevent silent 32-bit overflow.
  • isLowercaseLetter checks whether a given character falls within the 'a' through 'z' ASCII range.
  • buildLetterFrequencies uses character subtraction (current - 'a') to map letters directly to array indices from 0 to 25.
Main.java
public class Main {
    // Calculates the sum of all elements in an array using long to avoid overflow.
    static long calculateTotalSum(int[] numbers) {
        long runningTotal = 0;
        for (int i = 0; i < numbers.length; i++) {
            runningTotal += numbers[i];
        }
        return runningTotal;
    }

    // Returns true if the given character is a lowercase English letter.
    static boolean isLowercaseLetter(char letter) {
        return letter >= 'a' && letter <= 'z';
    }

    // Counts occurrences of each lowercase letter in the given character array.
    static int[] buildLetterFrequencies(char[] characters) {
        int[] frequencies = new int[26];
        for (int i = 0; i < characters.length; i++) {
            char current = characters[i];
            if (isLowercaseLetter(current)) {
                int index = current - 'a';
                frequencies[index]++;
            }
        }
        return frequencies;
    }

    public static void main(String[] args) {
        int[] largeValues = { 1_500_000_000, 1_000_000_000 };
        long totalSum = calculateTotalSum(largeValues);

        char[] sampleText = { 'a', 'l', 'g', 'o', 'z' };
        int[] frequencies = buildLetterFrequencies(sampleText);

        System.out.println("Safe sum across large integers: " + totalSum);
        System.out.println("Frequency of 'a': " + frequencies[0]);
        System.out.println("Frequency of 'z': " + frequencies[25]);
    }
}
Output
Safe sum across large integers: 2500000000
Frequency of 'a': 1
Frequency of 'z': 1

Traps

  1. Silent int overflow in sums

    Adding two large int values wraps silently into a negative number when the sum exceeds 2_147_483_647. Casting one operand to long before addition ensures the operation runs with 64-bit precision.

    ✕ WRONGint total = a + b;
    ✓ RIGHTlong total = (long) a + b;
  2. Omitting the L suffix on large long literals

    The compiler interprets whole-number literals as int by default. Writing a number greater than 2_147_483_647 without the L suffix causes a compile error: integer number too large.

    ✕ WRONGlong limit = 3_000_000_000;
    ✓ RIGHTlong limit = 3_000_000_000L;
  3. Binary search midpoint overflow

    When left and right are both large, left + right exceeds Integer.MAX_VALUE and wraps to a negative number, causing an index error. Subtracting first avoids large intermediate values.

    ✕ WRONGint mid = (left + right) / 2;
    ✓ RIGHTint mid = left + (right - left) / 2;
  4. Assuming Math.abs is always non-negative

    Because two's complement has no positive counterpart for -2_147_483_648, Math.abs(Integer.MIN_VALUE) returns -2147483648. Cast to long before calling Math.abs if the input can equal Integer.MIN_VALUE.

    ✕ WRONGint distance = Math.abs(Integer.MIN_VALUE);
    ✓ RIGHTlong distance = Math.abs((long) Integer.MIN_VALUE);
  5. Reading an uninitialised local variable

    Unlike class fields and array elements, local variables do not receive default values. Accessing an unassigned local variable causes a compile error: variable might not have been initialized.

    ✕ WRONGint count; System.out.println(count);
    ✓ RIGHTint count = 0; System.out.println(count);
  6. Char arithmetic producing an integer instead of a character

    In Java, binary operations on char promote operands to int. Evaluating 'a' + 1 produces integer 98, not character 'b'. You must explicitly cast back to char when character output is required.

    ✕ WRONGchar next = 'a' + 1;
    ✓ RIGHTchar next = (char) ('a' + 1);
  7. Infinite loops caused by byte overflow

    A byte variable cannot exceed 127. When i reaches 127, i++ wraps around to -128, so the condition i < 150 remains permanently true and creates an infinite loop. Always use int for loop counters.

    ✕ WRONGfor (byte i = 0; i < 150; i++) { ... }
    ✓ RIGHTfor (int i = 0; i < 150; i++) { ... }
  8. Accidental type narrowing when using var

    Writing var total = 0; causes the compiler to infer type int. If the variable accumulates large values in a loop, it will overflow silently. Use literal 0L or declare long total = 0; explicitly.

    ✕ WRONGvar total = 0;
    ✓ RIGHTvar total = 0L;
AlgozSet in Geist & Geist MonoJava track