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
- 21File I/O and Serialization
- 22Debugging: Errors, Stack Traces, the Debugger and Logging
- 23Maven: What It Does and How to Use It
- 24Git for Java Projects: Commits, Branches, Merging and Pull Requests
- 25Testing with JUnit and Mockito: Unit Tests, Mocks and Their Limits
- 26Project 1: Build an Expense Tracker CLI in Plain Java
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
- 36Security Fundamentals and Spring Security: Passwords, JWT, Access Control, CORS and Secrets
- 37Testing Spring Boot Applications: Slice Tests, MockMvc, Real Databases and the Test Pyramid
- 38Production Practices: Configuration, Logging, Docker and Health Checks for Spring Boot
- 39Project 3: The Job Application Tracker API, Assembled and Reviewed
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.
For
Beginners who have run their first Java program and now need to understand how Java represents and manipulates data.
Level
Before you start
Start Here (article 1): you can compile and run a Java file.
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
doubleis 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.
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
#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 reassignedConventions: 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:
// File: Main.javapublic class Main { public static void main(String[] args) { int total; System.out.println(total); }}Main.java:5: error: variable total might not have been initialized System.out.println(total); ^1 errorThe 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 |
|---|---|---|---|
| 8 bits | Small whole numbers | -128 to 127 |
| 16 bits | Small whole numbers | -32,768 to 32,767 |
| 32 bits | Whole numbers (the default choice) | -2,147,483,648 to 2,147,483,647 |
| 64 bits | Large whole numbers | about -9.2 x 10^18 to 9.2 x 10^18 |
| 32 bits | Approximate decimals | IEEE 754 single precision |
| 64 bits | Approximate decimals (the default for decimals) | IEEE 754 double precision |
| 16 bits | A single UTF-16 code unit | 0 to 65,535, for example |
| not specified |
| Used in conditions |
These ranges come from the Java Language Specification. A few practical rules:
- Use
intfor whole numbers unless you know they can be larger, in which case uselong. Add anLto long literals:3_000_000_000L. (Underscores in number literals are allowed, and make big numbers readable.) - Use
doublefor measurements that are naturally approximate, such as a temperature or a distance, and not for money. We return to this below. - Use
booleanfor yes or no. Java does not treat numbers as booleans:if (count)is a compile error, and you must writeif (count > 0). - You will rarely use
byte,shortorfloatin everyday code. They appear in binary data and large numeric arrays. - A
charis a 16-bit unit, which is not always a whole visible character. That subtlety matters when we study strings in article 8.
// 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); }}age=29population=8000000000temperature=36.6grade=Apassed=trueint range: -2147483648 to 2147483647Primitives 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:
// 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)); }}a=10 b=99first[0]=99same array? trueChanging 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):
// 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); }}33.5-31-13.0 versus 3.5Look 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.
// 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()); } }}-2147483648-7273799681000000000000addExact says: integer overflowThis 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:
// 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); }}42.03 4-5698 b124Notice 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:
// 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"))); }}0.30000000000000004false0.30truefalse0The 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 innew BigDecimal("0.10"). Thedoubleconstructor faithfully captures the binary approximation of 0.1, which is not what you meant. - Compare with
compareTo, notequals. The documentation notes thatequalsrequires both the numerical value and the representation (the scale) to be the same, so1.0and1.00are notequalseven 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
BigDecimalthrows unless you give it aRoundingMode.
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:
var names = new java.util.ArrayList<String>(); // inferred as ArrayList<String>var count = 10; // intJEP 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.
// 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})); }}[70, 85, 0, 0][null, null][false, false]5[2, 3, 5]100[[0, 0, 0], [0, 0, 7]][1, 2, 5, 9]truefalseWhat to take from it:
- Length is fixed when the array is created, and is read from the
lengthfield (no parentheses). To "grow" an array you must create a new, larger one and copy the values. Collections such asArrayListdo this for you (article 11). - Default values: numbers are
0, booleans arefalse, and references arenull. - Valid indexes are
0tolength - 1. Anything else throwsArrayIndexOutOfBoundsExceptionat runtime, as you saw in the first article. - Arrays are objects and variables hold references, so
alias = primesshares the array, and a change through one name is visible through the other. To get an independent copy, useArrays.copyOforarray.clone(). - Arrays do not override
equals. The last two lines show thatArrays.equalscompares contents, while the array's ownequalsmethod compares identity, so it returnsfalsefor two separate arrays with the same contents. Thejava.util.Arraysclass 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
doublefirst. - Ignoring overflow when summing or multiplying large numbers. Use
longor the...Exactmethods. - Using
doublefor money. UseBigDecimal(from aString) or whole units. - Casting to round. A cast truncates.
- Comparing
BigDecimalwithequals. UsecompareTo. - 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:
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
#- Next: Decisions, Loops, Methods and Scope teaches how to make a program choose and repeat.
- For the full story of
==andequals:equals,hashCode,toStringand the Object Contract. - For arrays that grow: Lists and Sets.
Sources & further reading
- Technology
- Software Engineering
- #beginners
- #Java
- #Java backend
- #Java basics
- #Java interview
- #JPA
- #learn Java
- #Spring Boot
- #SQL
Connected lessons
Linked from 7 guides
Other pages in the library that point here.
Keep reading
More in Technology →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

