Variables, Primitive Types, and Operators in Java
Executive Summary
Variables in Java are statically typed slots: you declare the type once, and the compiler enforces it from that line onward. Java has exactly eight primitive types (byte, short, int, long, float, double, char, boolean) with sizes fixed by the language specification, so an int is 32 bits on every JVM everywhere. Operators group into six families: arithmetic, increment and decrement, comparison, logical, bitwise, and assignment plus ternary. Conversions widen automatically but narrow only with an explicit cast, integer division truncates, and overflow wraps silently instead of failing. Choose deliberately: long for IDs and anything that grows, boolean for flags, and never floating point for money.
What a Variable Really Holds
A variable is a named slot with one job: hold a value of a specific type. Also, you declare the type up front, and the compiler enforces it from that line onward. Static typing is a deal with the compiler: it argues with you at build time so production does not argue with you at runtime.
int attempts = 3; // one int slot named attempts
double ratio = 0.75; // one double slot
boolean ready = false; // one boolean slot
final int MAX_ATTEMPTS = 5; // a constant: one assignment allowed
Primitive variables hold their values directly: the slot contains the number, not a pointer to it. Reference variables store a reference to an object instead, and Part 2 covers them. Consequently, copying a primitive copies the value, and two int variables never share state.
Two naming rules keep Java code readable. Variables use lowerCamelCase (attempts, maxRetries), and constants use UPPER_SNAKE_CASE (MAX_ATTEMPTS). IntelliJ enforces both, and so should you.
The Eight Primitive Types in Java
Java defines exactly eight primitive types, and their sizes are fixed by the language specification. In fact, an int is 32 bits on every JVM, on every operating system, forever. Therefore, identical arithmetic produces identical results everywhere, a quiet superpower for backend teams.
| Type | Size | Range | Default | Typical use |
|---|---|---|---|---|
| byte | 8 bits | -128 to 127 | 0 | Raw data, network buffers |
| short | 16 bits | -32,768 to 32,767 | 0 | Rare; legacy file formats |
| int | 32 bits | -2,147,483,648 to 2,147,483,647 | 0 | Counts, loops, indexes |
| long | 64 bits | About -9.2e18 to 9.2e18 | 0L | IDs, timestamps, big counters |
| float | 32 bits | About 6 to 7 significant digits | 0.0f | Measurements (rarely) |
| double | 64 bits | About 15 significant digits | 0.0d | Science and geometry, not money |
| char | 16 bits | 0 to 65,535 (UTF-16 code unit) | ‘\u0000’ | Single characters |
| boolean | Not specified | true or false | false | Flags and conditions |
Two rows deserve comment. char is unsigned and stores a UTF-16 code unit, so it holds a character and still participates in arithmetic with int. boolean has no defined size; treat it as a logical value, never as a number, and never encode flags as 0 and 1.
Where var Fits
Since Java 10, var lets the compiler infer a local variable’s type from its initializer. The line var attempts = 3; declares an int, checked at compile time like any other. In other words, it is syntax sugar, not dynamic typing. However, var works only for local variables with an initializer, never for fields or parameters. Deep coverage belongs to the modern Java features article; for now, write explicit types while the type system is still new to you.
Variables in Practice: Scope and Initialization
Java checks two things about every variable: is it initialized before use, and is it visible where used? Fields and array elements get default values (0, 0.0, false). Locals get none, and the compiler forces you to assign before the first read. That “variable might not have been initialized” error is the compiler protecting you.
public class ScopeDemo {
static int total; // field: defaults to 0
public static void main(String[] args) {
int local; // legal to declare
// System.out.println(local); // does not compile: not initialized
if (args.length > 0) {
int inside = 1; // scope: this block only
local = inside;
}
local++; // fine: assigned on some path
System.out.println(local);
}
}
Scope runs from the declaration to the closing brace of its block. Shadowing, an inner variable reusing an outer name, compiles, however it creates bugs that read fine and behave wrong. IntelliJ flags shadowed names; keep them unique.
Operators in Java, One Family at a Time
Every operator reduces to one of six families, documented in the expressions chapter of the JLS. So, learn them once and you can read any expression with confidence.
- Arithmetic: + – * / % plus unary + and -.
- Increment and decrement: ++ and –, prefix and postfix.
- Comparison: == != < > <= >=.
- Logical: && || ! (short-circuit) and & | (always evaluate both sides).
- Bitwise and shift: & | ^ ~ << >> >>>.
- Assignment and ternary: =, compound forms such as +=, and ? :
int a = 10, b = 3;
int quotient = a / b; // 3: integer division truncates
int remainder = a % b; // 1
int post = a++; // post is 10, then a becomes 11
int pre = ++a; // a becomes 12, then pre is 12
boolean adult = a > 17;
boolean both = adult && b > 0; // if adult is false, b > 0 never runs
String parity = (a % 2 == 0) ? "even" : "odd";
Two families deserve respect. Division and modulus truncate toward zero, and the remainder keeps the sign of the dividend: -7 % 2 is -1, not 1. Moreover, && and || stop early, which matters when the second check would crash: list != null && list.size() > 0 is safe, and the single-& version is not.
Precedence follows a fixed order: * and / bind before + and -, comparisons bind before &&, and && binds before ||. In practice parentheses are free, so add them wherever a reader could hesitate.
Casting and Conversions
Java converts between primitives in two directions. Widening is automatic because no information is lost: byte to short to int to long to float to double, and char to int. Narrowing needs an explicit cast because data can be lost, and the compiler refuses to guess your intent.
int big = 100;
long wide = big; // widening: automatic
int back = (int) wide; // narrowing: explicit cast required
char letter = 'A';
int code = letter + 1; // 66: char promotes to int
char next = (char) (letter + 1); // 'B': cast back explicitly
Compound assignment hides a cast. Therefore byte b = 10; b += 1; compiles, but b = b + 1; does not, because the right side promotes to int. Knowing this prevents a confusing compile error in your first week.
Overflow and Floating Point: Two Silent Failures
Wrong code first. This is the bug shape that costs real money, and I have watched it survive code review twice:
// WRONG: truncating division and floating-point money
double average = total / count; // int / int truncates first: 7 / 2 becomes 3.0
double price = 0.1 + 0.2; // 0.30000000000000004
if (price == 0.3) { /* never runs */ }
// RIGHT: cast before dividing, keep money exact
double average = (double) total / count; // 3.5
long cents = 10 + 20; // exact: 30
var total = new java.math.BigDecimal("0.10")
.add(new java.math.BigDecimal("0.20")); // 0.30 exactly
The delta: integer division ran before the widening, and binary floating point cannot represent 0.1 exactly, exactly as IEEE 754 defines. double is built for measurement, not accounting.
Integers fail differently. They wrap instead of drifting:
int views = Integer.MAX_VALUE;
views = views + 1; // -2147483648: wraps silently, no exception
Java integers are two’s complement, so overflow lands at the negative end without a sound. In practice, the fix is a wider type before the boundary, not after the incident.
How Real Systems Do This
Production systems pick primitives for growth, not for today’s data. In 2014, the view counter for one famous music video reached 2,147,483,647, and the platform had to upgrade the counter to 64 bits. Payment systems store money as long minor units (cents) and reach for BigDecimal only where rounding rules demand decimal arithmetic. ID generators return long from day one, because retrofitting int identifiers across tables is a migration nightmare.
In my experience auditing a checkout service, totals were double while refunds were long cents. The two systems disagreed about “the same amount” by fractions of a cent, and nightly reconciliation flagged mismatches for weeks before anyone connected the two. Consequently, my rule is blunt: the moment a number represents money, it stops being a primitive decision and becomes a domain decision.
Decision Framework
- Can the value pass 2.1 billion, or is it an ID, timestamp, or offset? Use long from day one.
- Is it money? Use long minor units, or BigDecimal when rounding rules demand decimals. Never float or double.
- Is it a flag? boolean. Encoding flags as 0 and 1 reads worse and breaks under arithmetic.
- Is it a measurement where “close” is fine? double covers almost all scientific and physical values.
- Is it a single character or code unit? char. Anything longer is String, covered in the arrays and strings article.
- Are you choosing float to save memory? Measure first. In practice, on modern 64-bit JVMs, double is almost always the right call.
When NOT to Use This
- Do not model domain values as bare primitives when meaning matters. An int age, an int customerId, and an int balance are three concepts sharing one type; records in Part 2 make those states distinct and safe.
- Do not use floating point where exactness is required: invoices, taxes, interest. The per-operation error is tiny, and the compounded error across a ledger is not.
- Do not use var outside local variables. Fields and parameters need explicit types for readers and for the API contract; var on a field does not even compile.
Common Mistakes
- Integer division where a fraction was intended. Averages silently floor, and nobody notices until a report is wrong by half.
- Choosing int for counters and IDs. Growth turns a silent wrap into negative totals in production.
- Comparing doubles with ==. The branch never fires for computed values such as 0.1 + 0.2.
- Assuming % always returns a positive number. In Java, -7 % 2 is -1, so parity checks break on negatives.
- Concatenating before adding: “Total: ” + 1 + 2 prints “Total: 12”. Add first, or use parentheses.
- Forgetting that compound assignment hides a cast: b += 1 compiles for byte, b = b + 1 does not.
Key Takeaways
- Java has exactly eight primitive types with fixed sizes; an int is 32 bits everywhere, forever, by specification.
- Primitives hold values directly, so copies never share state. Reference variables hold references, and Part 2 separates the two worlds cleanly.
- Integer division truncates. Cast to double before dividing whenever a fraction matters.
- Overflow wraps silently. Use long for IDs, timestamps, and anything that grows.
- Never use float or double for money: long minor units or BigDecimal.
- && and || short-circuit; & and | evaluate both sides. Use the double forms in conditions.
- var infers local types at compile time; it changes nothing about static typing.
FAQ
What are the eight primitive types in Java?
byte, short, int, long, float, double, char, and boolean. Their sizes are 8, 16, 32, 64, 32, and 64 bits, plus 16-bit char, while boolean has no specified size. The ranges are fixed by the language specification and never vary by platform.
Is String a primitive type in Java?
No. String is a reference type: a variable holds a reference to an immutable object, not the characters themselves. The arrays and strings article covers String behavior and the StringBuilder alternative.
What is the default value of an int in Java?
Zero for fields and array elements. Local variables have no default; the compiler rejects any read before an assignment, which turns a whole class of bugs into build errors.
Why does 0.1 + 0.2 not equal 0.3 in Java?
double uses IEEE 754 binary fractions, and 0.1 has no exact binary representation. The nearest double for 0.1 plus the nearest for 0.2 lands just above 0.3. Compare with a tolerance, or use BigDecimal for exact decimal arithmetic.
When should I use long instead of int?
Whenever growth is possible: counters, IDs, timestamps, file offsets, event totals. Sixty-four-bit arithmetic costs nothing measurable on modern CPUs, and retrofitting a widened column later is expensive in every direction.
Conclusion
Primitive types in Java are small, fixed, and honest: the compiler checks them, their sizes never move, and every silent failure in this article has a one-line prevention. Operators are the grammar that combines them, and casting is where the care goes.
Pick types for the data you will have in five years, not the data you have today. The compiler forgives both choices; production does not.
Last updated on 6 September 2026.
