Variables, Types and Operators
Java's static types: primitive types versus objects, var, literals and conversions, integer division and overflow, floating-point surprises and BigDecimal for money.
Declaring variables
A variable is declared with its type, a name and usually a starting
value. After that the type is fixed: the compiler checks that every
value you store fits it (see
static typing). Names start with a letter and use camelCase by
convention: meterId, pricePerKwh.
void main() {
String meterId = "E-001";
int kwh = 350;
double pricePerKwh = 0.30;
boolean smartMeter = true;
char tariffCode = 'D';
kwh = kwh + 12; // a variable can change...
final int vatPercent = 21; // ...a final one cannot
IO.println(meterId + " used " + kwh + " kWh");
IO.println("Cost before VAT: " + kwh * pricePerKwh);
IO.println("Smart meter: " + smartMeter + ", tariff " + tariffCode);
IO.println("VAT: " + vatPercent + "%");
}
E-001 used 362 kWh Cost before VAT: 108.6 Smart meter: true, tariff D VAT: 21%
The examples on this page are compact source files (see
Compact source files) that print with IO.println; inside a classic class,
System.out.println prints exactly the same.
Constants with final
final makes a variable a
constant: it gets a value once and can never be reassigned. Constants
shared by a whole class are written as
static final fields in capitals, such as
MAX_READINGS. Assigning to a final variable is a
compiler error:
void main() {
final int vatPercent = 21;
vatPercent = 9;
IO.println(vatPercent);
}
Main.java:3: error: cannot assign a value to final variable vatPercent
Java also refuses to read a local variable before it has a value:
int kwh; IO.println(kwh); gives
error: variable kwh might not have been initialized.
There are no "undefined" or random values, as in JavaScript or C.
The var keyword
Since Java 10 you may write var instead of the type of
a local variable. This is
local variable type inference: the compiler works out the type from the starting value and fixes
it, exactly as if you had written it (see
type inference). var is not a dynamic type.
void main() {
var meterId = "E-001"; // String
var kwh = 350; // int
var price = 0.30; // double
var readings = new ArrayList<Integer>();
readings.add(kwh);
IO.println(meterId + " " + kwh + " " + price + " " + readings);
kwh = 351; // fine: still an int
IO.println(kwh);
}
E-001 350 0.3 [350] 351
After var kwh = 350, the line kwh = "351"
fails with
incompatible types: String cannot be converted to int.
var only works for local variables with a starting
value: not for fields or method parameters, and not as
var x; or var x = null; (the compiler
answers cannot infer type for local variable x). Use it
where the type is obvious from the right-hand side, as with
new ArrayList<Integer>(); write the type where it
helps the reader.
Primitive types
Java has exactly eight primitive types: built-in types whose variables hold the value itself, not a reference to an object. Their sizes are the same on every platform, which is part of Java's portability.
| Type | Size | Holds | Example |
|---|---|---|---|
byte |
8 bits | Whole numbers −128 to 127 | Raw bytes from a file or network |
short |
16 bits | Whole numbers −32,768 to 32,767 | Rarely used |
int |
32 bits | Whole numbers up to about ±2.1 billion | Counts, kWh, ticket numbers |
long |
64 bits | Whole numbers up to about ±9.2 × 1018 | Watt-hours over years, timestamps in milliseconds |
float |
32 bits | Floating-point, about 7 significant digits | Rarely used; prefer double |
double |
64 bits | Floating-point, about 15–16 significant digits | Measurements, temperatures |
char |
16 bits | One UTF-16 code unit (a character) | 'D', '€' |
boolean |
— | true or false |
Flags, conditions |
The whole-number types store negative numbers in
two's complement; float and double are IEEE 754
floating-point numbers. Each type's limits are available as constants:
void main() {
IO.println(Byte.MIN_VALUE + " to " + Byte.MAX_VALUE);
IO.println(Short.MIN_VALUE + " to " + Short.MAX_VALUE);
IO.println(Integer.MIN_VALUE + " to " + Integer.MAX_VALUE);
IO.println(Long.MIN_VALUE + " to " + Long.MAX_VALUE);
IO.println(Float.MAX_VALUE + " and " + Double.MAX_VALUE);
IO.println((int) Character.MAX_VALUE);
}
-128 to 127 -32768 to 32767 -2147483648 to 2147483647 -9223372036854775808 to 9223372036854775807 3.4028235E38 and 1.7976931348623157E308 65535
Large and small double values print in scientific
notation: 1.7976931348623157E308 means 1.797… ×
10308. Everything that is not one of the eight primitives
is a reference type: String, arrays,
List, your own classes and records. A variable of a
reference type holds a
reference
to an object on the heap, or null; a primitive variable
can never be null.
Literals
A
literal
is a value written directly in the code. A whole number is an
int unless it ends in L
(long); a number with a decimal point is a
double unless it ends in f
(float). Underscores make long numbers readable.
void main() {
long customersEver = 3_000_000_000L; // L: a long literal
int hex = 0x1F;
int binary = 0b1010;
int octal = 017; // leading zero: octal!
float tariff = 0.27f; // f: a float literal
double usage = 1.25e3; // 1.25 × 10³
char euro = '€';
IO.println(customersEver);
IO.println(hex + " " + binary + " " + octal);
IO.println(tariff + " " + usage + " " + euro);
}
3000000000 31 10 15 0.27 1250.0 €
Without the L, long big = 3000000000; does
not compile (error: integer number too large): the
literal itself must fit in an int before it is stored.
The leading-zero rule is a classic trap: 017 is octal,
so a ticket number or postal code with a leading zero must be a
string, not a number.
Wrapper classes and autoboxing
Collections such as List can only hold objects, not
primitive values. For that reason every primitive type has a
wrapper class: a class whose
objects hold one primitive value. They are Byte,
Short, Integer, Long,
Float, Double, Character and
Boolean. The wrapper classes also hold useful static
methods, such as Integer.parseInt.
Since Java 5 the compiler converts between the two forms for you.
Autoboxing wraps an
int into an Integer where an object is
needed, and unboxing takes the value out again where a
primitive is needed:
void main() {
int count = 3;
Integer boxed = count; // autoboxing: int -> Integer
int unboxed = boxed + 1; // unboxing: Integer -> int
List<Integer> readings = new ArrayList<>();
readings.add(342); // boxed automatically
IO.println(boxed + " " + unboxed + " " + readings);
IO.println(Integer.parseInt("42") + Double.parseDouble("1.5"));
IO.println(Integer.toBinaryString(10) + " " + Integer.MAX_VALUE);
}
3 4 [342] 43.5 1010 2147483647
That is why you write List<Integer> and never
List<int>, which does not compile. Boxing is
convenient, but a wrapper is an object, and objects behave
differently from values in two ways that cause real bugs.
Comparing wrapper objects
For objects, == asks "is this the same object?", not
"is this the same value?". Java keeps a cache of
Integer objects for the values −128 to 127, so small
numbers happen to be the same object and larger ones are not:
void main() {
Integer meterA = 127, meterB = 127;
Integer meterC = 128, meterD = 128;
IO.println(meterA == meterB);
IO.println(meterC == meterD);
IO.println(meterC.equals(meterD));
}
true false true
Code that compares readings with == therefore passes
every test with small numbers and fails in production with large
ones. Compare wrapper objects with equals, or unbox
them to int first. The same rule applies to strings,
see
comparing strings.
null and unboxing
A wrapper variable can be null; an
int cannot. Unboxing a null throws a
NullPointerException, often in a line that does not
look dangerous at all:
void main() {
Map<String, Integer> usage = new HashMap<>();
usage.put("E-001", 350);
int kwh = usage.get("E-002"); // no such meter: get returns null
IO.println(kwh);
}
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
The message explains what happened: to unbox, Java called
intValue() on the result of Map.get, which
was null. Use
usage.getOrDefault("E-002", 0) or check for
null first; see
Null, Optional and NullPointerException.
Conversions and casts
A conversion to a type that can hold every value of the old one is a
widening conversion and happens automatically:
int to long, int to
double. The opposite direction is a
narrowing conversion: it
can lose information, so Java demands an explicit cast, the
target type in parentheses.
void main() {
int kwh = 350;
long total = kwh; // widening: always safe
double precise = kwh; // int -> double: also automatic
IO.println(total + " " + precise);
double average = 11.78;
int rounded = (int) average; // narrowing: needs a cast, drops the fraction
IO.println(rounded);
IO.println((int) -3.99);
IO.println(Math.round(average));
IO.println((byte) 200);
IO.println((int) 3e10);
}
350 350.0 11 -3 12 -56 2147483647
A cast from double to int truncates toward
zero; it does not round. Use Math.round to round.
Casting a whole number to a smaller type keeps only the low bits
((byte) 200 becomes -56), while a
double that is too large for an int is
clamped to Integer.MAX_VALUE. Without the cast, the
compiler refuses:
void main() {
double average = 11.78;
int rounded = average;
IO.println(rounded);
}
Main.java:3: error: incompatible types: possible lossy conversion from double to int
Text is not converted by casting at all:
(int) "42" does not compile. Use
Integer.parseInt, Double.parseDouble and
friends, which throw a NumberFormatException for text
that is not a number (see
Converting input to numbers). The other way round, String.valueOf(350) or simply
"" + 350 gives text.
Arithmetic operators
Java has the usual operators: +, -,
*, /, and % for the
remainder. The type of the result depends on the types of the
operands: two int values give an int, and
as soon as one side is a double the result is a
double.
Integer division
Dividing two whole numbers is
integer division: the result is truncated toward zero, exactly as in C#. Python's
// rounds down instead, which differs for negative
numbers; Java offers that behavior as Math.floorDiv and
Math.floorMod.
void main() {
IO.println(7 / 2);
IO.println(7 % 2);
IO.println(7 / 2.0);
IO.println(-7 / 2);
IO.println(-7 % 2);
IO.println(Math.floorDiv(-7, 2) + " " + Math.floorMod(-7, 2));
int day1 = 350, day2 = 351;
IO.println((day1 + day2) / 2);
IO.println((day1 + day2) / 2.0);
}
3 1 3.5 -3 -1 -4 1 350 350.5
The average of 350 and 351 comes out as 350: a silent loss of
precision, not an error. Write 2.0, or cast one operand
to double, when you want a fraction. Dividing a whole
number by zero throws
java.lang.ArithmeticException: / by zero; dividing a
double by zero does not, as the floating-point section
shows.
Increment and compound assignment
kwh += 12 is short for kwh = kwh + 12, and
likewise -=, *=, /= and
%=. ++ and -- add or subtract
one. Placed after the variable (k++) the expression
gives the old value; placed before (++k) it gives the
new value.
void main() {
int kwh = 350;
kwh += 12;
kwh -= 2;
kwh *= 2;
IO.println(kwh);
int k = 5;
IO.println(k++ + " " + k);
IO.println(++k + " " + k);
int i = 5;
i += 2.7; // compiles: the result is cast back to int
IO.println(i);
}
720 5 6 7 7 7
Use ++ as a statement on its own line
(count++;); inside larger expressions it is hard to
read. The last lines show a trap: a compound assignment includes a
hidden cast to the variable's type, so i += 2.7 quietly
drops the fraction, while the long form is refused:
void main() {
int i = 5;
i = i + 2.7;
IO.println(i);
}
Main.java:3: error: incompatible types: possible lossy conversion from double to int
Overflow
Each whole-number type has a fixed range, and Java does not warn you when a result leaves it: the value wraps around to the other end of the range (an overflow). There is no runtime error, just a wrong number.
void main() {
int max = Integer.MAX_VALUE;
IO.println(max + 1);
int whPerYear = 365 * 24 * 1000 * 1000; // looks fine...
long whPerYearLong = 365L * 24 * 1000 * 1000;
IO.println(whPerYear);
IO.println(whPerYearLong);
IO.println(Math.addExact(max, 1));
}
-2147483648 170065408 8760000000 Exception in thread "main" java.lang.ArithmeticException: integer overflow
The second line is the dangerous case: the real value, 8.76 billion
watt-hours, needs a long, but the calculation is done
in int because all four literals are int.
Making the first operand a long (365L)
makes the whole calculation long. Where an overflow
must never pass unnoticed, Math.addExact,
Math.multiplyExact and friends throw an exception
instead.
Floating-point surprises
double stores numbers in binary, and most decimal
fractions, such as 0.1, have no exact binary form (see
why 0.1 + 0.2 is not 0.3). Small errors appear, and comparisons with == fail.
Division by zero gives special values instead of an exception:
void main() {
IO.println(0.1 + 0.2);
IO.println(0.1 + 0.2 == 0.3);
IO.println(Math.abs((0.1 + 0.2) - 0.3) < 1e-9);
IO.println(1.0 / 0);
IO.println(0.0 / 0);
IO.println(Double.NaN == Double.NaN);
IO.println(Double.isNaN(0.0 / 0));
IO.println(Math.round(2.5) + " " + Math.round(-2.5));
}
0.30000000000000004 false true Infinity NaN false true 3 -2
Compare measured values with a tolerance, as in the third line.
NaN ("not a number") is not even equal to itself; test
for it with Double.isNaN. And note
Math.round: it rounds halves up toward positive
infinity, so 2.5 becomes 3 but
-2.5 becomes -2. (C#'s
Math.Round and Python's round round halves
to the nearest even number instead.) double is fine for
temperatures and measurements; for money it is the wrong type.
BigDecimal for money
BigDecimal is a class in
java.math for exact decimal numbers of any size: a
whole number of digits plus a scale, the number of digits
after the decimal point. It is Java's type for money. Because it is
a class, you calculate with methods instead of operators:
void main() {
BigDecimal tariff = new BigDecimal("0.30"); // euros per kWh
BigDecimal kwh = new BigDecimal("350");
BigDecimal net = tariff.multiply(kwh);
BigDecimal vat = net.multiply(new BigDecimal("0.21")).setScale(2, RoundingMode.HALF_UP);
BigDecimal total = net.add(vat);
IO.println("Net: " + net);
IO.println("VAT: " + vat);
IO.println("Total: " + total);
IO.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
IO.println(new BigDecimal(0.1));
IO.println(BigDecimal.valueOf(0.1));
}
Net: 105.00 VAT: 22.05 Total: 127.05 0.3 0.1000000000000000055511151231257827021181583404541015625 0.1
Three rules make BigDecimal work for you:
-
Create it from a string, as in
new BigDecimal("0.30"), or withBigDecimal.valueOf(0.1). The constructor that takes adoublefaithfully copies the double's binary error, as the second-to-last line shows. -
Round explicitly with
setScale(2, RoundingMode.HALF_UP)(orHALF_EVEN, "banker's rounding", if your rules demand it). The scale of a product is the sum of the scales: 0.30 × 350 gives105.00. - Give division a scale and rounding mode. A division without them must be exact, and fails otherwise.
void main() {
BigDecimal yearly = new BigDecimal("1000.00");
BigDecimal monthly = yearly.divide(new BigDecimal("12"));
IO.println(monthly);
}
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
void main() {
BigDecimal yearly = new BigDecimal("1000.00");
BigDecimal monthly = yearly.divide(new BigDecimal("12"), 2, RoundingMode.HALF_UP);
IO.println(monthly);
BigDecimal a = new BigDecimal("2.50");
BigDecimal b = new BigDecimal("2.5");
IO.println(a.equals(b));
IO.println(a.compareTo(b) == 0);
}
83.33 false true
The last two lines show the final trap: equals also
compares the scale, so 2.50 and 2.5 are
not "equal". Compare amounts with compareTo, which
returns 0 for the same value. Formatting amounts as
€ 127,05 is covered in
Strings and Text.
Comparison and logical operators
Comparisons (==, !=, <,
<=, >, >=) produce a
boolean. The logical operators combine them:
&& (and), || (or) and
! (not). && and
|| use
short-circuit evaluation: the right side is skipped when the left side already decides the
result.
void main() {
int kwh = 1250;
boolean highUse = kwh > 1000;
boolean smartMeter = false;
IO.println(highUse && !smartMeter);
IO.println(highUse || smartMeter);
IO.println(kwh == 1250 && kwh != 0);
IO.println(10 == 10.0);
String note = null;
IO.println(note != null && note.length() > 0); // && stops early: no crash
IO.println(1 + 2 + " kWh");
IO.println("kWh: " + 1 + 2);
IO.println('A' + 1);
}
true true true true false 3 kWh kWh: 12 66
The last three lines show how + depends on its
operands, evaluated from left to right: number plus number is
addition, but once a string is involved everything after it is
joined as text. A char is a number underneath, so
'A' + 1 is 66 (its Unicode code point plus one). Java
has no "truthiness": a condition must be a real
boolean.
void main() {
int kwh = 350;
if (kwh) {
IO.println("Usage recorded");
}
}
Main.java:3: error: incompatible types: int cannot be converted to boolean
In Python or JavaScript if kwh would test for "not
zero"; in Java you write what you mean:
if (kwh > 0). The single & and
| also work on booleans but always evaluate both sides;
on whole numbers they are bitwise operators, together with
^, ~, <<,
>> and >>>.
Operator precedence
As in mathematics, *, / and
% bind more strongly than + and
-; comparisons come after arithmetic, then
&&, then ||, and assignment comes
last. Operators of equal rank are evaluated from left to right.
void main() {
char grade = 'B';
IO.println(grade + 1);
IO.println((char) (grade + 1));
IO.println('7' - '0');
IO.println(2 + 3 * 4);
IO.println((2 + 3) * 4);
IO.println(10 - 4 - 3);
IO.println(100 / 10 / 5);
}
67 C 7 14 20 3 2
'7' - '0' turns a digit character into its number
value, a common trick when parsing by hand. When in doubt, add
parentheses: they cost nothing and make the intent clear. The full
order is the same as in C# and very close to Python's (see
operator precedence).