Guide contents

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.

Java
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());
        }
    }
}
Output
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 overflow

boolean 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.

Java
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());
        }
    }
}
Output
0.30000000000000004
false
true
NaN false true
Infinity -Infinity
true false
-Infinity
-1
int: / by zero

Floating-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.

Java
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)));
    }
}
Output
66
B
B
length = 4
code points = 3
charAt(1) is high surrogate: true
codePointAt(1) = U+1F600
charCount = 2

c + 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.

Java
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());
        }
    }
}
Output
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.

Java
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);
    }
}
Output
30 30
-56
3 -3
2147483647 0
3 3.5 -3 -1
-4 1
-1702967296 2592000000
16777216

byte + 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.

Java
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);
    }
}
Output
ada -> 1
limit = 100
Integer 10 10

var 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.

Java
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);
    }
}
Output
29 30 31
one two | two
parsed sum = 12
rows = 3

The 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.

Java
// 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());
    }
}
Output
London
Cannot invoke "Main$Address.city()" because the return value of "Main$Customer.address()" is null

The 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.addExact and Math.multiplyExact throw instead. Math.abs(Integer.MIN_VALUE) is negative.
  • 0.1 + 0.2 != 0.3; NaN is unequal to itself; -0.0 == 0.0 but Double.equals says they differ. Money uses long units or BigDecimal.
  • char is a UTF-16 code unit; code points above U+FFFF take two chars, so length() is not the character count.
  • == on wrappers compares identity; the Integer cache makes it look correct for -128 to 127. Use equals.
  • Unboxing null throws NullPointerException, often hidden in int x = map.get(key).
  • Arithmetic on byte, short, and char promotes to int; mixed int and long promotes to long only at the operator where a long appears.
  • var is compile-time inference for locals only; it needs an initializer.
  • Switch expressions do not fall through, must be exhaustive, and use yield in blocks.

Summary

  • Eight primitives: byte, short, int, long, float, double, char, boolean.
  • Integer.valueOf caches -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 double to int truncates toward zero and saturates at the int range.
  • strictfp has no effect since Java 17.
  • var arrived in Java 10, switch expressions in Java 14, unnamed variables in Java 22, compact source files and instance main in Java 25.
  • Helpful NPE messages name the expression that was null.