01 · Java basics — note 2 of 12
Variables and primitive types
Store raw values in fixed-size boxes and choose types that never overflow.
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
- A computer needs to know how much memory to reserve before it can store a value.
- Arithmetic on fixed-size numbers runs directly on CPU registers, making operations fast.
- Strict types catch mismatched data, such as putting text into a number slot, at compile time rather than while your program is running.
- 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:
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);
}
}Current score: 25
Maximum attempts: 3In this program:
scoreis a variable of typeint. It is initialised with10, and then reassigned to25.maxAttemptsis declared with thefinalkeyword. Marking a variablefinalcreates a constant: once assigned, its value cannot change. Attempting to assign a new value tomaxAttemptslater 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, andlong. All four are signed two's complement integers. - Floating-point numbers:
floatanddouble. 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:trueorfalse.
| Type | Size in bits | Range | Typical use |
|---|---|---|---|
byte | 8 bits | -128 to 127 | Raw binary streams, compact byte arrays |
short | 16 bits | -32,768 to 32,767 | Memory-constrained 16-bit integers, rare in DSA |
int | 32 bits | -2,147,483,648 to 2,147,483,647 | Default integer type: loop counters, array indices, counts |
long | 64 bits | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | Accumulators, prefix sums, products, values over 2.1 billion |
float | 32 bits | approx ±1.4e-45 to ±3.4e+38 (~6 to 7 decimal digits) | Graphics coordinates, memory-sensitive decimal data |
double | 64 bits | approx ±4.9e-324 to ±1.8e+308 (~15 to 17 decimal digits) | Default floating-point type: fractional math, probabilities |
char | 16 bits | '\u0000' to '\uffff' (0 to 65,535 unsigned) | Single characters, alphabet frequency array indices |
boolean | not fixed by the specification | true or false | Conditional 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 anint. To write alongliteral, append an uppercaseLsuffix (such as100_000_000_000L). Always use uppercaseL, because lowercasellooks almost identical to the digit1. - Digit separators: You can place underscores (
_) between digits to improve readability in large numbers (such as1_000_000). The compiler ignores underscores during compilation. - Number bases: Write
0xor0Xprefix for hexadecimal (base 16, e.g.0xFF), and0bor0Bprefix 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 aStringobject, not achar. Because characters are stored as 16-bit numbers,'A'has an underlying integer value of65.
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);
}
}Formatted million: 1000000
Distance in meters: 149600000000
Hexadecimal 0xFF: 255
Binary 0b1011: 11
Letter: A, ASCII code: 65In this program:
millionuses underscores for grouping digits.planetDistancerequires theLsuffix because its value exceedsInteger.MAX_VALUE.hexMaskassigns decimal255using base-16 notation.binaryMaskassigns decimal11using base-2 notation (8 + 2 + 1).charCodereads the numeric code of character'A', which is65.
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:
- Use
intby default: Array indices, collection sizes, loop bounds, and element counts always fit comfortably in anint. Standard library methods likearray.lengthandString.length()returnint. - Switch to
longfor sums and products: The maximum value ofintis 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 exceedsInteger.MAX_VALUEand will overflow if stored in anint. Uselongfor prefix sums, accumulators, and products. - Use
booleanfor state: Tracking whether a graph node has been visited, or whether a condition has been met, belongs in abooleanvariable orboolean[]array. - Use
charfor 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 to2. This allows a plainint[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:
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);
}
}127
-128
-127The 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:
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);
}
}Safe total: 4000000000Notice 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:
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:
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:
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);
}
}count is an int: 42
message is a String: Algoz
isReady is a boolean: trueThe 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:
varis not dynamic typing. Once inferred, the type of the variable is permanently fixed. Assigning aStringtocountlater 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
varfor local variables inside methods. It cannot be used for method parameters, return types, or class fields. - Readability risk with numbers: Writing
var total = 0;infersint. If the loop later accumulates values past 2.1 billion,totalsilently overflows because the reader assumed it was along. 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
0indicates a positive number or zero; a1indicates 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
0to1and1to0) and then add1to the result.
For example, consider an 8-bit byte representation of the number 1:
1in 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 (1to2,147,483,647). - All bit patterns starting with
1represent negative numbers: exactly $2^31 = 2,147,483,648$ values (-1down 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:
- Binary of
Integer.MIN_VALUE:10000000 00000000 00000000 00000000 - Invert all bits:
01111111 11111111 11111111 11111111(which isInteger.MAX_VALUE,2_147_483_647) - Add 1: wraps back to
10000000 00000000 00000000 00000000(Integer.MIN_VALUE)
The negation wraps straight back to where it started:
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);
}
}Integer.MIN_VALUE: -2147483648
Math.abs(Integer.MIN_VALUE): -2147483648
Negated (-Integer.MIN_VALUE): -2147483648Both 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:0float,double:0.0char:'\u0000'(null character, numeric value 0)boolean:false- Object references:
null
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]);
}
}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:
public class UnassignedDemo {
public static void main(String[] args) {
int total;
System.out.println(total);
}
}Compiling this code fails immediately:
UnassignedDemo.java:4: error: variable total might not have been initialized
System.out.println(total);
^
1 errorWhere 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:
numbersis an input array containing large integer values.calculateTotalSumiterates throughnumbersand accumulates values into a 64-bitlongto prevent silent 32-bit overflow.isLowercaseLetterchecks whether a given character falls within the'a'through'z'ASCII range.buildLetterFrequenciesuses character subtraction (current - 'a') to map letters directly to array indices from0to25.
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]);
}
}Safe sum across large integers: 2500000000
Frequency of 'a': 1
Frequency of 'z': 1Traps
Silent int overflow in sums
Adding two large
intvalues wraps silently into a negative number when the sum exceeds2_147_483_647. Casting one operand tolongbefore addition ensures the operation runs with 64-bit precision.✕ WRONGint total = a + b;✓ RIGHTlong total = (long) a + b;Omitting the L suffix on large long literals
The compiler interprets whole-number literals as
intby default. Writing a number greater than2_147_483_647without theLsuffix causes a compile error:integer number too large.✕ WRONGlong limit = 3_000_000_000;✓ RIGHTlong limit = 3_000_000_000L;Binary search midpoint overflow
When
leftandrightare both large,left + rightexceedsInteger.MAX_VALUEand 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;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 tolongbefore callingMath.absif the input can equalInteger.MIN_VALUE.✕ WRONGint distance = Math.abs(Integer.MIN_VALUE);✓ RIGHTlong distance = Math.abs((long) Integer.MIN_VALUE);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);Char arithmetic producing an integer instead of a character
In Java, binary operations on
charpromote operands toint. Evaluating'a' + 1produces integer98, not character'b'. You must explicitly cast back tocharwhen character output is required.✕ WRONGchar next = 'a' + 1;✓ RIGHTchar next = (char) ('a' + 1);Infinite loops caused by byte overflow
A
bytevariable cannot exceed127. Whenireaches127,i++wraps around to-128, so the conditioni < 150remains permanently true and creates an infinite loop. Always useintfor loop counters.✕ WRONGfor (byte i = 0; i < 150; i++) { ... }✓ RIGHTfor (int i = 0; i < 150; i++) { ... }Accidental type narrowing when using var
Writing
var total = 0;causes the compiler to infer typeint. If the variable accumulates large values in a loop, it will overflow silently. Use literal0Lor declarelong total = 0;explicitly.✕ WRONGvar total = 0;✓ RIGHTvar total = 0L;