Lesson 1 of 71
Types, Variables, and Expressions
This lesson covers the values a Java program works with and the expressions that combine them. After it, a reader can predict the result of arithmetic on every primitive type, explain when == compares values and when it compares references, and read the newer syntax added between Java 10 and Java 25.
| Topic | Release |
|---|---|
| Primitive types, wrappers, autoboxing | Java 1.0 to 5 |
Math.addExact and friends |
Java 8 |
var for local variables |
Java 10 |
Switch expressions, arrow labels, yield |
Java 14 |
Helpful NullPointerException messages |
Java 14 (on by default since Java 15) |
strictfp has no effect |
Java 17 |
Unnamed variables _ |
Java 22 |
Compact source files and instance main |
Java 25 |
Example 1: primitive types, ranges, and overflow
Java has eight primitive types: values that are stored directly, not as objects. Integer arithmetic wraps around silently when the result does not fit, because the JVM keeps only the low bits.
public class Primitives {
public static void main(String[] args) {
System.out.println("byte " + Byte.MIN_VALUE + " .. " + Byte.MAX_VALUE);
System.out.println("short " + Short.MIN_VALUE + " .. " + Short.MAX_VALUE);
System.out.println("int " + Integer.MIN_VALUE + " .. " + Integer.MAX_VALUE);
System.out.println("long " + Long.MIN_VALUE + " .. " + Long.MAX_VALUE);
System.out.println("char " + (int) Character.MIN_VALUE + " .. " + (int) Character.MAX_VALUE);
System.out.println("float max " + Float.MAX_VALUE);
System.out.println("double max " + Double.MAX_VALUE);
int big = Integer.MAX_VALUE;
System.out.println("MAX + 1 = " + (big + 1));
System.out.println("abs(MIN) = " + Math.abs(Integer.MIN_VALUE));
long safe = (long) big + 1;
System.out.println("as long = " + safe);
try {
Math.addExact(big, 1);
} catch (ArithmeticException e) {
System.out.println("addExact: " + e.getMessage());
}
}
}byte -128 .. 127
short -32768 .. 32767
int -2147483648 .. 2147483647
long -9223372036854775808 .. 9223372036854775807
char 0 .. 65535
float max 3.4028235E38
double max 1.7976931348623157E308
MAX + 1 = -2147483648
abs(MIN) = -2147483648
as long = 2147483648
addExact: integer overflowboolean is the eighth type; its size is not specified by the language. Math.abs(Integer.MIN_VALUE) is negative because +2147483648 does not fit in an int. The cast to long happens before the addition, so the sum is computed in 64 bits. Math.addExact, subtractExact, multiplyExact, and toIntExact throw ArithmeticException instead of wrapping, which matters for money and quantities where a silent wrap is a wrong answer.
Example 2: floating point
float and double are IEEE 754 binary floating-point numbers. Most decimal fractions, such as 0.1, have no exact binary form, so they are stored as the nearest representable value.
public class FloatingPoint {
public static void main(String[] args) {
System.out.println(0.1 + 0.2);
System.out.println(0.1 + 0.2 == 0.3);
System.out.println(Math.abs((0.1 + 0.2) - 0.3) < 1e-9);
double nan = 0.0 / 0.0;
System.out.println(nan + " " + (nan == nan) + " " + Double.isNaN(nan));
System.out.println(1.0 / 0 + " " + -1.0 / 0);
double negZero = -0.0;
System.out.println((negZero == 0.0) + " " + Double.valueOf(negZero).equals(0.0));
System.out.println(1 / negZero);
System.out.println(Double.compare(negZero, 0.0));
try {
System.out.println(1 / 0);
} catch (ArithmeticException e) {
System.out.println("int: " + e.getMessage());
}
}
}0.30000000000000004
false
true
NaN false true
Infinity -Infinity
true false
-Infinity
-1
int: / by zeroFloating-point division by zero gives infinity or NaN ("not a number"); integer division by zero throws. NaN is not equal to anything, including itself, so Double.isNaN is the only reliable test. -0.0 == 0.0 is true, but Double.equals and Double.compare treat them as different, and 1 / -0.0 is negative infinity. Before Java 17, the strictfp modifier forced strict IEEE semantics; since Java 17 all floating-point arithmetic is strict and the modifier is redundant (JEP 306). Prices and other decimal amounts use long units (cents, ticks) or BigDecimal, never double.
Example 3: char and code points
A char is an unsigned 16-bit UTF-16 code unit, not a full character. A Unicode code point (the number assigned to a character) above U+FFFF needs two char values, called a surrogate pair.
public class Chars {
public static void main(String[] args) {
char c = 'A';
System.out.println(c + 1);
System.out.println((char) (c + 1));
c++;
System.out.println(c);
String s = "a😀b"; // 'a', U+1F600 (an emoji), 'b'
System.out.println("length = " + s.length());
System.out.println("code points = " + s.codePointCount(0, s.length()));
System.out.println("charAt(1) is high surrogate: " + Character.isHighSurrogate(s.charAt(1)));
System.out.println("codePointAt(1) = U+" + Integer.toHexString(s.codePointAt(1)).toUpperCase());
System.out.println("charCount = " + Character.charCount(s.codePointAt(1)));
}
}66
B
B
length = 4
code points = 3
charAt(1) is high surrogate: true
codePointAt(1) = U+1F600
charCount = 2c + 1 promotes the char to int, so it prints a number. The compound operator c++ includes a cast back to char. length() counts code units, not characters, so text with emoji or some CJK characters gives a larger number than a reader sees. The next lesson, Strings and Text, covers iterating over code points.
Example 4: references, wrappers, and the Integer cache
A variable of a class type holds a reference: a pointer to an object, or null. Each primitive has a wrapper class (Integer for int, Long for long, and so on). Autoboxing is the automatic conversion from a primitive to its wrapper, and unboxing is the reverse.
public class Boxing {
public static void main(String[] args) {
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println("127 == 127: " + (a == b));
System.out.println("128 == 128: " + (c == d));
System.out.println("128 equals: " + c.equals(d));
System.out.println("128 == int: " + (c == 128));
Long l = 10L;
System.out.println("Long.equals(Integer): " + l.equals(10));
Integer missing = null;
try {
int value = missing;
System.out.println(value);
} catch (NullPointerException e) {
System.out.println("NPE: " + e.getMessage());
}
}
}127 == 127: true
128 == 128: false
128 equals: true
128 == int: true
Long.equals(Integer): false
NPE: Cannot invoke "java.lang.Integer.intValue()" because "<local6>" is null== on two references compares identity: whether both point to the same object. Integer.valueOf, which autoboxing calls, returns cached objects for -128 to 127, so 127 == 127 is true by accident of the cache and 128 == 128 is false. The upper bound can be raised with -XX:AutoBoxCacheMax, so code that relies on the cache is wrong on some JVMs. c == 128 unboxes c and compares values. Long.equals(Integer) is false because the types differ. Unboxing null throws NullPointerException; the message names the method the compiler inserted (intValue). It shows <local6> instead of missing because the class was compiled without local variable tables; javac -g adds them and the message then names the variable.
Example 5: numeric promotion and casts
Binary numeric promotion is the rule that converts both operands of an arithmetic operator to a common type before the operation: double if either is double, else float, else long, else int. A cast converts a value explicitly and can lose information.
public class Promotion {
public static void main(String[] args) {
byte x = 10, y = 20;
int sum = x + y; // byte + byte is int
byte narrowed = (byte) (x + y);
System.out.println(sum + " " + narrowed);
System.out.println((byte) 200);
System.out.println((int) 3.99 + " " + (int) -3.99);
System.out.println((int) 1e20 + " " + (int) Double.NaN);
System.out.println(7 / 2 + " " + 7 / 2.0 + " " + -7 / 2 + " " + -7 % 2);
System.out.println(Math.floorDiv(-7, 2) + " " + Math.floorMod(-7, 2));
long millis = 1_000 * 60 * 60 * 24 * 30; // int overflow, then widened
long correct = 1_000L * 60 * 60 * 24 * 30;
System.out.println(millis + " " + correct);
int i = 16_777_217;
float f = i; // widening, but loses precision
System.out.println((int) f);
}
}30 30
-56
3 -3
2147483647 0
3 3.5 -3 -1
-4 1
-1702967296 2592000000
16777216byte + byte is computed as int, so assigning it back to byte needs a cast. Casting 200 to byte keeps the low 8 bits, which read as -56. Casting double to int truncates toward zero, clamps to the int range, and maps NaN to 0. Integer / and % also round toward zero; Math.floorDiv and Math.floorMod round toward negative infinity, which is what bucket and ring-index code needs. The millis line overflows because every operand is int; one L suffix makes the whole product long. int to float is a widening conversion that can still lose digits, since a float has 24 bits of precision.
Example 6: var and final locals
var (Java 10) declares a local variable whose type the compiler infers from the initializer. The type is fixed at compile time; var is not dynamic typing. A final local can be assigned only once.
import java.util.ArrayList;
import java.util.HashMap;
public class VarDemo {
public static void main(String[] args) {
var count = 10; // int
var total = 10L; // long
var names = new ArrayList<String>();
names.add("ada");
var index = new HashMap<String, Integer>();
index.put("ada", 1);
for (var name : names) {
System.out.println(name + " -> " + index.get(name));
}
final int limit;
if (count > 5) {
limit = 100;
} else {
limit = 10;
}
System.out.println("limit = " + limit);
var x = 'A' + 1; // int, not char
Object type = x;
System.out.println(type.getClass().getSimpleName() + " " + count + " " + total);
}
}ada -> 1
limit = 100
Integer 10 10var requires an initializer, cannot be null alone (var v = null; does not compile), and is not allowed for fields, parameters, or return types. It works in for loops and try-with-resources. var x = 'A' + 1 infers int, because the type is the type of the expression. The final local limit is assigned on each branch exactly once; the compiler checks this with definite assignment analysis. var reads well when the type is visible on the right side (new ArrayList<String>()) and hides information when the right side is a method call whose return type is not obvious.
Example 7: switch expressions, yield, and unnamed variables
A switch expression (Java 14) produces a value. Arrow labels (case X ->) do not fall through, and a case can list several constants. yield returns a value from a block inside a switch expression. The unnamed variable _ (Java 22) marks a variable that must be declared but is never used.
public class Switches {
static int daysIn(String month, boolean leap) {
return switch (month) {
case "FEB" -> leap ? 29 : 28;
case "APR", "JUN", "SEP", "NOV" -> 30;
default -> {
if (month.length() != 3) {
throw new IllegalArgumentException("bad month: " + month);
}
yield 31;
}
};
}
static String oldStyle(int code) {
String result = "";
switch (code) {
case 1:
result += "one ";
case 2: // fall-through from case 1
result += "two";
break;
default:
result = "other";
}
return result;
}
public static void main(String[] args) {
System.out.println(daysIn("FEB", true) + " " + daysIn("JUN", false) + " " + daysIn("JAN", false));
System.out.println(oldStyle(1) + " | " + oldStyle(2));
int parsed = 0;
for (String s : new String[] {"5", "x", "7"}) {
try {
parsed += Integer.parseInt(s);
} catch (NumberFormatException _) {
parsed += 0;
}
}
System.out.println("parsed sum = " + parsed);
int rows = 0;
for (var _ : new int[3]) {
rows++;
}
System.out.println("rows = " + rows);
}
}29 30 31
one two | two
parsed sum = 12
rows = 3The old statement form falls through from case 1 into case 2 because there is no break, a common bug source. A switch expression on a String or number must be exhaustive, which is why default is required here. _ can name a catch parameter, a loop variable, a lambda parameter, or a local; it can appear several times in one scope and cannot be read. Switching on types and records with patterns is covered in Pattern Matching.
Example 8: compact source files and helpful NPE messages
A compact source file (Java 25) is a .java file with no class declaration. The compiler wraps its methods and fields in an implicit class, and main can be an instance method without String[] args. IO.println (Java 25) writes a line to standard output.
// Compact source file: no class, no static void main(String[] args) needed.
record Order(String id, Customer customer) {}
record Customer(String name, Address address) {}
record Address(String city) {}
String cityOf(Order order) {
return order.customer().address().city();
}
void main() {
var ok = new Order("o-1", new Customer("Ada", new Address("London")));
IO.println(cityOf(ok));
var broken = new Order("o-2", new Customer("Bob", null));
try {
cityOf(broken);
} catch (NullPointerException e) {
IO.println(e.getMessage());
}
}London
Cannot invoke "Main$Address.city()" because the return value of "Main$Customer.address()" is nullThe program uses records, short immutable data classes covered in Records and Sealed Types; here they only hold fields. A compact source file runs directly with java Main.java. It suits scripts and small tools; a class that other code imports still needs a named class in a package. The helpful NPE message (JEP 358, Java 14, on by default since Java 15) names the exact call that returned null in a chain, so one stack trace line is enough to find the broken link.
Interview points
- Integer overflow wraps silently;
Math.addExactandMath.multiplyExactthrow instead.Math.abs(Integer.MIN_VALUE)is negative. 0.1 + 0.2 != 0.3; NaN is unequal to itself;-0.0 == 0.0butDouble.equalssays they differ. Money useslongunits orBigDecimal.charis a UTF-16 code unit; code points aboveU+FFFFtake twochars, solength()is not the character count.==on wrappers compares identity; theIntegercache makes it look correct for -128 to 127. Useequals.- Unboxing
nullthrowsNullPointerException, often hidden inint x = map.get(key). - Arithmetic on
byte,short, andcharpromotes toint; mixedintandlongpromotes tolongonly at the operator where alongappears. varis compile-time inference for locals only; it needs an initializer.- Switch expressions do not fall through, must be exhaustive, and use
yieldin blocks.
Summary
- Eight primitives:
byte,short,int,long,float,double,char,boolean. Integer.valueOfcaches -128 to 127 by default; the upper bound is configurable.- Integer division by zero throws; floating-point division by zero gives infinity or NaN.
- Casting
doubletointtruncates toward zero and saturates at theintrange. strictfphas no effect since Java 17.vararrived in Java 10, switch expressions in Java 14, unnamed variables in Java 22, compact source files and instancemainin Java 25.- Helpful NPE messages name the expression that was
null.
