Skip to content
SparkTime, home
Java From Zero to Job-Ready
Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
12 · Interviews and Readiness
Lesson 2 of 41

Lesson 2 · Java Foundations

Variables, Types, Operators and Arrays

How Java stores and combines data: primitive types versus references, what happens when a number overflows, why integer division surprises people, how casting and conversion work, when to use var, and how arrays hold a fixed number of values.

On this page

Key takeaways

  • Java is statically typed. Every variable has a type that the compiler checks, and a primitive variable holds a value while a reference variable holds a reference to an object.
  • int arithmetic wraps around silently on overflow, integer division discards the fraction, and double cannot represent most decimal fractions exactly. Choose types for the data you have.
  • Widening conversions happen automatically, narrowing conversions need an explicit cast and can lose information. Casting does not round.
  • Use BigDecimal, created from a String, for money. Never use double for currency.
  • An array has a fixed length, zero-based indexes and default values. Array variables are references, so assigning one array variable to another shares the same array.

A shop writes a program to total its daily sales. On a quiet day the numbers are small and the total is right. On a very busy day the total suddenly becomes negative. Nothing crashed, no error appeared, and the program carried on happily with a wrong answer.

That is a flavour of bug that every Java developer meets once, and it comes from not understanding how Java stores numbers. This article gives you the model: what kinds of values exist, how big they can be, how they are converted into each other, and what an array really is.

What you will learn

#
  • What a variable is, and what "statically typed" means.
  • The eight primitive types and how to choose between them.
  • The difference between a primitive and a reference.
  • Arithmetic, including the cases that surprise beginners.
  • Conversion and casting.
  • Why double is wrong for money, and what to use.
  • Local type inference with var.
  • Arrays: creation, indexing, copying and the aliasing trap.

The problem: a program must remember things

#

Programs work with data: a price, a name, a count of items. To use data you must store it somewhere and refer to it later. A variable is a named place to store a value. In Java, every variable also has a type, which says what kind of value it can hold and what operations are allowed on it.

Java
int quantity = 3;String product = "notebook";

Java is statically typed: the type of every variable is known when the program is compiled, and the compiler refuses to compile code that mixes types incorrectly. That is stricter than some languages, and the strictness is the point. A whole class of mistakes is caught before the program runs.

Declaring, initialising and naming

#
Java
int count;          // declaration: reserves a variable named countcount = 10;         // assignment: stores a valueint total = 0;      // declaration and initialisation togetherfinal int MAX_ITEMS = 100;  // final: can never be reassigned

Conventions: variable names use lowerCamelCase (totalPrice), constants use UPPER_SNAKE_CASE (MAX_ITEMS), and names should describe the meaning, not the type. Java insists that a local variable is assigned before it is read:

Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        int total;        System.out.println(total);    }}
Code
Main.java:5: error: variable total might not have been initialized        System.out.println(total);                           ^1 error

The compiler refuses because total has no value on that path. Local variables get no automatic default. (Fields of objects do, as article 4 explains.)

The eight primitive types

#

Primitive types are the built-in basic types. They hold plain values directly, and they are not objects. Java defines exactly eight.

Type

Size

What it holds

Range or detail

byte

8 bits

Small whole numbers

-128 to 127

short

16 bits

Small whole numbers

-32,768 to 32,767

int

32 bits

Whole numbers (the default choice)

-2,147,483,648 to 2,147,483,647

long

64 bits

Large whole numbers

about -9.2 x 10^18 to 9.2 x 10^18

float

32 bits

Approximate decimals

IEEE 754 single precision

double

64 bits

Approximate decimals (the default for decimals)

IEEE 754 double precision

char

16 bits

A single UTF-16 code unit

0 to 65,535, for example 'A'

boolean

not specified

true or false

Used in conditions

These ranges come from the Java Language Specification. A few practical rules:

  • Use int for whole numbers unless you know they can be larger, in which case use long. Add an L to long literals: 3_000_000_000L. (Underscores in number literals are allowed, and make big numbers readable.)
  • Use double for measurements that are naturally approximate, such as a temperature or a distance, and not for money. We return to this below.
  • Use boolean for yes or no. Java does not treat numbers as booleans: if (count) is a compile error, and you must write if (count > 0).
  • You will rarely use byte, short or float in everyday code. They appear in binary data and large numeric arrays.
  • A char is a 16-bit unit, which is not always a whole visible character. That subtlety matters when we study strings in article 8.
Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        int age = 29;        long population = 8_000_000_000L;        double temperature = 36.6;        char grade = 'A';        boolean passed = true;        System.out.println("age=" + age);        System.out.println("population=" + population);        System.out.println("temperature=" + temperature);        System.out.println("grade=" + grade);        System.out.println("passed=" + passed);        System.out.println("int range: " + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE);    }}
Code
age=29population=8000000000temperature=36.6grade=Apassed=trueint range: -2147483648 to 2147483647

Primitives versus references

#

This distinction is the single most important idea in this article, and it comes back in almost every later one.

  • A primitive variable holds the value itself. If you copy it, you get an independent copy.
  • A reference variable holds a reference to an object that lives elsewhere. If you copy the variable, you copy the reference, so both variables point to the same object.

The mental model: a primitive variable is a box that contains a number. A reference variable is a box that contains a note saying where the object is. The Java Language Specification puts it this way: the values of a reference type are references to objects, so several references can point to the same object. A special value, null, means "this reference points to nothing".

Every type that is not one of the eight primitives is a reference type: String, arrays, and every class you write. Watch the difference in action:

Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        int a = 10;        int b = a;      // copies the value        b = 99;        System.out.println("a=" + a + " b=" + b);        int[] first = {1, 2, 3};        int[] second = first;   // copies the reference, not the array        second[0] = 99;        System.out.println("first[0]=" + first[0]);        System.out.println("same array? " + (first == second));    }}
Code
a=10 b=99first[0]=99same array? true

Changing b did not change a, because each has its own value. Changing second[0] changed what first[0] shows, because both variables refer to one array. The comparison first == second asks whether two references point to the same object, which is true here. You will see why this matters when we meet == versus equals in article 7.

Operators and their surprises

#

Java has the arithmetic operators you expect: +, -, *, /, and % (remainder). Several behave in ways beginners do not expect.

Integer division discards the fraction

#

When both operands are integers, the result is an integer, and the fractional part is thrown away (truncated toward zero):

Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        System.out.println(7 / 2);        // both int        System.out.println(7 / 2.0);      // one double        System.out.println(-7 / 2);        System.out.println(7 % 3);        System.out.println(-7 % 3);       // the sign follows the left operand        int items = 7;        int people = 2;        double wrong = items / people;    // division happens first, as ints        double right = (double) items / people;        System.out.println(wrong + " versus " + right);    }}
Code
33.5-31-13.0 versus 3.5

Look at the last line. Assigning the result to a double does not rescue you: items / people is computed as integer division first, giving 3, and only then is 3 converted to 3.0. The fix is to make one operand a double before dividing. The remainder % takes the sign of the left operand, so -7 % 3 is -1, not 2. Many programs that assume a non-negative remainder (for example when wrapping an index around) are wrong for negative numbers.

Overflow is silent

#

An int has 32 bits, so it can only hold values in a fixed range. When arithmetic goes past the end, Java does not report an error. The Java Language Specification says so plainly: the integer operators do not indicate overflow or underflow in any way. The value wraps around.

Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        int big = Integer.MAX_VALUE;        System.out.println(big + 1);        int salesPerShift = 1_000_000;        System.out.println(salesPerShift * salesPerShift);       // int overflow        System.out.println(1L * salesPerShift * salesPerShift);  // long arithmetic        try {            Math.addExact(big, 1);        } catch (ArithmeticException e) {            System.out.println("addExact says: " + e.getMessage());        }    }}
Code
-2147483648-7273799681000000000000addExact says: integer overflow

This is the bug from the start of the article. When a total is stored in an int, a large enough sum wraps to a negative number. There are three defences: use long when values can be large, use the exact methods such as Math.addExact and Math.multiplyExact, which throw ArithmeticException instead of wrapping, and know the range of the data you are handling. (The try and catch here are explained in article 10. For now, read them as "attempt this, and handle the error if it happens".)

Comparison and logical operators

#

Comparison operators (==, !=, <, <=, >, >=) produce a boolean. Logical operators combine booleans: && (and), || (or) and ! (not). && and || are short-circuit operators: the right side is evaluated only if it is needed. That makes this safe: if (list != null && list.size() > 0), because the second check never runs when the first fails.

Compound assignment, increment and precedence

#

x += 5 means x = x + 5 (with an implicit cast back to the type of x). i++ adds one. When an expression mixes operators, multiplication and division happen before addition and subtraction, and parentheses override everything. When in doubt, add parentheses. A clear expression is better than a clever one.

String concatenation uses + as well: if either operand is a String, the other is converted to text and joined. "Total: " + 1 + 2 gives "Total: 12", because the expression is evaluated left to right, so "Total: " + 1 becomes a string first.

Conversion and casting

#

Java converts between numeric types in two ways.

Widening goes from a smaller range to a larger one (int to long, int to double). It is safe and happens automatically.

Narrowing goes the other way, and may lose information, so Java makes you say so with a cast:

Java
// File: Main.javapublic class Main {    public static void main(String[] args) {        int whole = 42;        double widened = whole;          // automatic        double price = 3.99;        int truncated = (int) price;     // cast: drops the fraction, does not round        int rounded = (int) Math.round(price);        int tooBig = 200;        byte squeezed = (byte) tooBig;   // wraps into the byte range        char letter = 'a';        int code = letter + 1;           // char is promoted to int        char next = (char) (letter + 1);        System.out.println(widened);        System.out.println(truncated + " " + rounded);        System.out.println(squeezed);        System.out.println(code + " " + next);        System.out.println(Integer.parseInt("123") + 1);    }}
Code
42.03 4-5698 b124

Notice that (int) 3.99 is 3, because a cast truncates. If you want rounding, you must ask for it with Math.round. Casting 200 to a byte produced -56, because 200 does not fit in 8 bits and wraps. Casting is a statement that you know what you are doing, and the compiler trusts you. char arithmetic works on the character's numeric code, so 'a' + 1 is the integer 98, and you cast back to char to get b. Finally, Integer.parseInt converts text to a number, and throws a NumberFormatException if the text is not a valid number, which matters whenever you handle user input.

Money: why double is the wrong tool

#

Computers store double values in binary. Most decimal fractions, such as 0.1, have no exact binary representation, so small errors creep in:

Java
// File: Main.javaimport java.math.BigDecimal;public class Main {    public static void main(String[] args) {        System.out.println(0.1 + 0.2);        System.out.println(0.1 + 0.2 == 0.3);        BigDecimal a = new BigDecimal("0.10");        BigDecimal b = new BigDecimal("0.20");        BigDecimal sum = a.add(b);        System.out.println(sum);        System.out.println(sum.compareTo(new BigDecimal("0.30")) == 0);        System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1.00")));        System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1.00")));    }}
Code
0.30000000000000004false0.30truefalse0

The double result is 0.30000000000000004, and the comparison with 0.3 is false. That is not a Java bug. It is how binary floating-point works, and it is the same in most languages. For prices, balances and taxes, use java.math.BigDecimal, which stores decimal numbers exactly as an unscaled integer and a scale. Three rules from its documentation:

  • Create it from a String, as in new BigDecimal("0.10"). The double constructor faithfully captures the binary approximation of 0.1, which is not what you meant.
  • Compare with compareTo, not equals. The documentation notes that equals requires both the numerical value and the representation (the scale) to be the same, so 1.0 and 1.00 are not equals even though they are numerically equal. The last two lines of output show exactly that.
  • Division can need a rounding rule. Dividing 1 by 3 has no exact decimal answer, so BigDecimal throws unless you give it a RoundingMode.

An alternative that many systems use is to store money as a whole number of the smallest unit, for example paise as a long. Both approaches are sound. Using double is not.

var: let the compiler infer the type

#

Since Java 10, you can write var for a local variable with an initialiser, and the compiler infers the type:

Java
var names = new java.util.ArrayList<String>();   // inferred as ArrayList<String>var count = 10;                                    // int

JEP 286, which introduced it, limits var to local variables with initialisers (and loop variables). It cannot be used for fields, method parameters or return types, and the variable still has a fixed static type: this is not dynamic typing. The JEP itself notes that, like any feature, it can be used to write clear or unclear code. A sensible rule: use var when the type is obvious from the right-hand side (var list = new ArrayList<String>()), and write the type when it helps the reader (int total = computeTotal() is clearer than var total = computeTotal() if the return type is not obvious).

Arrays

#

An array is a fixed-size, ordered sequence of values of the same type, accessed by a zero-based index.

Java
// File: Main.javaimport java.util.Arrays;public class Main {    public static void main(String[] args) {        int[] scores = new int[4];          // four ints, all 0 by default        scores[0] = 70;        scores[1] = 85;        String[] names = new String[2];     // references default to null        boolean[] flags = new boolean[2];   // booleans default to false        int[] primes = {2, 3, 5, 7, 11};    // literal form        System.out.println(Arrays.toString(scores));        System.out.println(Arrays.toString(names));        System.out.println(Arrays.toString(flags));        System.out.println(primes.length);        int[] copy = Arrays.copyOf(primes, 3);        System.out.println(Arrays.toString(copy));        int[] alias = primes;        alias[0] = 100;        System.out.println(primes[0]);        int[][] grid = new int[2][3];        grid[1][2] = 7;        System.out.println(Arrays.deepToString(grid));        int[] unsorted = {5, 2, 9, 1};        Arrays.sort(unsorted);        System.out.println(Arrays.toString(unsorted));        System.out.println(Arrays.equals(new int[]{1, 2}, new int[]{1, 2}));        System.out.println(new int[]{1, 2}.equals(new int[]{1, 2}));    }}
Code
[70, 85, 0, 0][null, null][false, false]5[2, 3, 5]100[[0, 0, 0], [0, 0, 7]][1, 2, 5, 9]truefalse

What to take from it:

  • Length is fixed when the array is created, and is read from the length field (no parentheses). To "grow" an array you must create a new, larger one and copy the values. Collections such as ArrayList do this for you (article 11).
  • Default values: numbers are 0, booleans are false, and references are null.
  • Valid indexes are 0 to length - 1. Anything else throws ArrayIndexOutOfBoundsException at runtime, as you saw in the first article.
  • Arrays are objects and variables hold references, so alias = primes shares the array, and a change through one name is visible through the other. To get an independent copy, use Arrays.copyOf or array.clone().
  • Arrays do not override equals. The last two lines show that Arrays.equals compares contents, while the array's own equals method compares identity, so it returns false for two separate arrays with the same contents. The java.util.Arrays class has helpers for printing, sorting, copying, searching and comparing.
  • A multi-dimensional array is an array of arrays.

Common mistakes

#
  • Dividing integers and expecting a fraction. Make one operand a double first.
  • Ignoring overflow when summing or multiplying large numbers. Use long or the ...Exact methods.
  • Using double for money. Use BigDecimal (from a String) or whole units.
  • Casting to round. A cast truncates.
  • Comparing BigDecimal with equals. Use compareTo.
  • Off-by-one array errors. The last valid index is length - 1.
  • Copying an array variable and expecting a copy of the array.
  • Reading an uninitialised local variable. The compiler stops you, and the fix is to give it a value.

Exercises

#

Exercise 1

Predict the output

#

Without running it, what does this print? int a = 5; int b = 2; System.out.println(a / b); System.out.println((double) a / b); System.out.println(a % b); System.out.println(-a % b);

Exercise 2: Find the bug

#

A method computes an average like this: double average = (a + b + c) / 3; where a, b and c are int variables. For a = 1, b = 2, c = 2 it returns 1.0. Explain why and fix it.

Exercise 3: Choose the type

#

Which type would you use for each: (a) the number of students in a class, (b) the population of a country, (c) the price of an item in a billing system, (d) whether a user is logged in, (e) the temperature outside?

Exercise 4: Aliasing

#

What is printed? int[] x = {1, 2, 3}; int[] y = x; int[] z = Arrays.copyOf(x, x.length); y[0] = 10; z[1] = 20; System.out.println(x[0] + " " + x[1]);

Exercise 5: Overflow guard

#

Write a method static int safeMultiply(int a, int b) that returns the product, or throws ArithmeticException if it does not fit in an int, without using Math.multiplyExact.

Show solutionHide solution

It prints 2, then 2.5, then 1, then -1. 5 / 2 is integer division, giving 2. Casting a to double first makes the division floating-point, so it gives 2.5. 5 % 2 is 1. -a % b is -5 % 2, and because the remainder takes the sign of the left operand, the result is -1.

Exercise 2

Show solutionHide solution

(a + b + c) / 3 is computed entirely in int arithmetic: the sum is 5, and 5 / 3 is 1 (the fraction is discarded). The result 1 is then widened to 1.0 and stored. The fix is to make the division floating-point before it happens: double average = (a + b + c) / 3.0; or (double) (a + b + c) / 3. Both give 1.6666666666666667.

Exercise 3

Show solutionHide solution

(a) int. (b) long, since a population can exceed about 2.1 billion. (c) BigDecimal (created from a string), or a long holding the smallest currency unit, and not double. (d) boolean. (e) double, because a temperature is a naturally approximate measurement.

The reasoning to practise: look at the largest value, whether fractions matter, and whether exactness matters.

Exercise 4

Show solutionHide solution

It prints 10 2. y is a reference to the same array as x, so y[0] = 10 changes what x[0] shows. z is an independent copy made before the changes, so z[1] = 20 does not affect x, and x[1] is still 2.

Exercise 5

Show solutionHide solution

Compute in long, which is wide enough for any product of two ints, and check the range:

Java
static int safeMultiply(int a, int b) {    long product = (long) a * b;    if (product > Integer.MAX_VALUE || product < Integer.MIN_VALUE) {        throw new ArithmeticException("integer overflow");    }    return (int) product;}

The cast (long) a is essential: if you wrote (long) (a * b), the multiplication would already have overflowed as int before the cast. This is the same idea that Math.multiplyExact implements for you.

Where to go next

#

Sources & further reading

  1. docs.oracle.com
  2. docs.oracle.com
  3. openjdk.org
  • Guide · Technology

    Java Essentials for DSA

  • Guide · Technology

    SQL for Beginners: Learn to Query a Database With Real Examples

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages