Contents

Java has exactly 8 primitive types. They are stored by value (not reference), require no object allocation, and cannot be null. The table below summarises size, range, default value, and literal suffix:

TypeSizeRangeDefaultLiteral suffix
byte8-bit-128 to 1270none
short16-bit-32,768 to 32,7670none
int32-bit-2,147,483,648 to 2,147,483,6470none
long64-bit-9.2×10¹⁸ to 9.2×10¹⁸0LL or l
float32-bit IEEE 754~±3.4×10³⁸ (7 decimal digits)0.0ff or F
double64-bit IEEE 754~±1.8×10³⁰⁸ (15 decimal digits)0.0dd or D (optional)
char16-bit Unicode'\u0000' to '\uffff' (0–65,535)'\u0000'single quotes
booleanJVM-definedtrue / falsefalsenone

Integer types store whole numbers — numbers with no decimal point, like 0, 42, -7, or 1_000_000. Java has four integer types: byte, short, int, and long. They all do the same thing — hold whole numbers — but differ in how much memory they use and therefore how large a number they can hold.

Think of it like choosing a container size: a small container uses less space but overflows with too much. The key question when picking an integer type is: what is the largest number this variable will ever need to hold?

One important rule: when you write a long literal in your code you must add the suffix L, otherwise Java treats the number as an int and will give a compile error if it exceeds int's range.

// byte — tiny range, mainly used for raw binary data byte b = 100; // valid: within -128 to 127 // byte bad = 200; // compile error: 200 is outside byte range // short — rarely used in application code short s = 30_000; // int — the everyday default for whole numbers int age = 29; int population = 1_400_000_000; // 1.4 billion — fits in int int maxInt = Integer.MAX_VALUE; // 2,147,483,647 // int overflow = maxInt + 1; // won't compile as-is, wraps silently at runtime // long — when int's range isn't enough long worldPopulation = 8_100_000_000L; // ~8.1 billion — requires L suffix long fileSize = 5_368_709_120L; // 5 GB in bytes long nowMs = System.currentTimeMillis(); // milliseconds since epoch (always long) // L suffix is required for long literals outside int range // long bad = 9_000_000_000; // compile error — looks like int, but too big long ok = 9_000_000_000L; // correct // Java allows _ in numeric literals for readability (Java 7+) int million = 1_000_000; // same as 1000000, just easier to read // Other literal formats for int int hex = 0xFF; // hexadecimal — 255 int binary = 0b1010_1010; // binary — 170 // Min/max constants available from wrapper classes System.out.println(Integer.MAX_VALUE); // 2,147,483,647 System.out.println(Integer.MIN_VALUE); // -2,147,483,648 System.out.println(Long.MAX_VALUE); // 9,223,372,036,854,775,807 When in doubt, use int. Only upgrade to long when you have a concrete reason to believe the value could exceed ~2.1 billion. Avoid byte and short for individual variables — the JVM promotes them to int internally for all arithmetic anyway, so you get no performance benefit, only a narrower range that requires extra casts.

Floating-point types store numbers with a decimal point — like 3.14, -0.5, or 1.99. The name "floating-point" refers to the fact that the decimal point can shift position to represent both very large numbers (like 6.022e23) and very small ones (like 1.6e-19) using the same number of bits.

Java has two floating-point types:

Important limitation: floating-point types cannot represent most decimal fractions exactly. This is not a Java bug — it is a fundamental property of how binary floating-point works. The number 0.1 has no exact binary representation, just like 1/3 has no exact decimal representation. This means 0.1 + 0.2 does not equal exactly 0.3 in floating-point arithmetic. For financial calculations where every cent matters, use BigDecimal instead.

// double — the default. 15-16 significant digits of precision. double pi = 3.141592653589793; double gravity = 9.81; // m/s² double temp = -40.0; // works for negatives and decimals // float — must add f suffix; 7 significant digits float price = 9.99f; // float bad = 9.99; // compile error — 9.99 is a double, not a float // The precision difference matters at scale: double d = 1.0 / 3.0; // 0.3333333333333333 (16 digits) float f = 1.0f / 3.0f; // 0.33333334 (7 digits, then rounding error) // Imprecision: 0.1 + 0.2 is not exactly 0.3 double sum = 0.1 + 0.2; System.out.println(sum); // 0.30000000000000004 (not 0.3!) System.out.println(sum == 0.3); // false — never use == to compare doubles // Correct way: check if values are close enough (within a small tolerance) double epsilon = 1e-9; System.out.println(Math.abs(sum - 0.3) < epsilon); // true // Special floating-point values System.out.println(1.0 / 0.0); // Infinity (not an exception!) System.out.println(-1.0 / 0.0); // -Infinity System.out.println(0.0 / 0.0); // NaN (Not a Number) System.out.println(Double.isNaN(0.0 / 0.0)); // true — use isNaN() to check System.out.println(Double.isInfinite(1.0 / 0.0)); // true // Scientific notation literals double avogadro = 6.022e23; // 6.022 × 10²³ double electron = 1.6e-19; // 1.6 × 10⁻¹⁹ Never use float or double for money. Because they cannot represent decimal fractions exactly, rounding errors will accumulate — adding up 100 payments of $1.10 may give you $109.99999 instead of $110.00. Use BigDecimal for any calculation where precision of every decimal place matters.

char holds a single character — a letter, digit, punctuation mark, or symbol. Under the hood, Java stores characters as numbers using the Unicode standard (specifically UTF-16), which assigns a unique number to every character in every human writing system. That is why you can also assign a number directly to a char, or add and subtract characters like numbers — the arithmetic operates on those underlying code point values.

boolean holds only one of two values: true or false. It is the result type of every comparison (age > 18), logical expression (a && b), and condition check in Java. Flags, switches, status indicators — if a variable can only ever be yes or no, use boolean.

// char — single quotes, Unicode escape, or numeric value char letter = 'A'; char newline = '\n'; char copyright = '\u00A9'; // © char fromNum = 65; // 'A' — char is an unsigned 16-bit int // char arithmetic char next = (char) ('A' + 1); // 'B' System.out.println((int) 'A'); // 65 — cast to int to see code point // String vs char: String is a class (reference type), char is a primitive char c = 'X'; String s = String.valueOf(c); // char → String // boolean — only true or false boolean active = true; boolean finished = false; // Result of comparisons and logical operations is always boolean boolean inRange = (age >= 18) && (age <= 65); boolean isEmpty = list.size() == 0; // or list.isEmpty() // Default value in arrays / fields boolean[] flags = new boolean[5]; // all false by default char[] chars = new char[3]; // all '\u0000' by default

Java's generic collections — List, Map, Set and so on — can only store objects, not raw primitives. That is a problem because int, double, boolean, and the others are not objects — they are plain values. Java solves this with wrapper classes: one class for each primitive that boxes the value inside an object. int becomes Integer, double becomes Double, boolean becomes Boolean, and so on.

You rarely need to convert manually — Java does it for you automatically. This automatic conversion is called autoboxing (primitive → wrapper object) and unboxing (wrapper object → primitive). Wrapper classes also provide useful utility methods for parsing strings, converting between number bases, and finding min/max values:

// Wrapper types: Byte, Short, Integer, Long, Float, Double, Character, Boolean // Autoboxing — compiler inserts Integer.valueOf(42) Integer x = 42; // Unboxing — compiler inserts x.intValue() int y = x; // Useful static methods on wrapper classes int parsed = Integer.parseInt("123"); long parsedL = Long.parseLong("9000000000"); double parsedD = Double.parseDouble("3.14"); String hex = Integer.toHexString(255); // "ff" String binary = Integer.toBinaryString(42); // "101010" String fromInt = Integer.toString(255, 16); // "ff" (any radix) int max = Integer.max(10, 20); // 20 int clamp = Math.min(Math.max(value, 0), 100); // clamp 0..100 // Integer cache: -128 to 127 are cached — == works in this range only Integer a = 127, b = 127; System.out.println(a == b); // true (cached) Integer c = 200, d = 200; System.out.println(c == d); // false (different objects outside cache) System.out.println(c.equals(d)); // true — always use equals() for wrappers // Null danger: unboxing null throws NullPointerException Integer n = null; int v = n; // NullPointerException at runtime! Always use equals() (not ==) to compare Integer, Long, and other wrapper objects. The JVM caches Integer instances only in the range −128 to 127; outside that range, == compares references and will return false even for equal values.

Sometimes you have a value of one type but need to use it as another. Converting between numeric types is called casting. There are two directions:

// Widening — implicit, no data loss byte b = 42; short s = b; // byte → short int i = s; // short → int long l = i; // int → long float f = l; // long → float (may lose precision for very large longs) double d = f; // float → double // Widening order: byte → short → int → long → float → double // Narrowing — requires explicit cast, may truncate double pi = 3.14159; int iPi = (int) pi; // 3 — truncates (not rounds) fractional part long big = 123_456_789_000L; int small = (int) big; // -539222987 — bits truncated, surprising result! // char ↔ int char c = 'A'; int code = c; // widening: 65 char back = (char) 90; // narrowing: 'Z' // Safe narrowing — check range first long val = 1_000L; if (val >= Integer.MIN_VALUE && val <= Integer.MAX_VALUE) { int safe = (int) val; // safe to cast } // String conversions — NOT casting, use parse methods int fromStr = Integer.parseInt("42"); String toStr = String.valueOf(42); // or Integer.toString(42)

As explained above, double cannot represent most decimal fractions exactly. For financial or other precision-critical calculations, Java provides java.math.BigDecimal. Unlike double, BigDecimal stores numbers in decimal internally, so 0.1 really is exactly 0.1. It also lets you control exactly how rounding should happen (HALF_UP, HALF_EVEN, etc.) — something you cannot do with double.

The trade-off is that BigDecimal is slower and more verbose than double. You cannot use operators like +, *, - — instead you call methods like add(), multiply(), subtract(). One critical rule: always create a BigDecimal from a String literal, never from a double — passing a double to the constructor transfers the imprecision that was already in the double into your BigDecimal, defeating the purpose:

import java.math.*; // BAD — double constructor preserves the float imprecision BigDecimal bad = new BigDecimal(0.1); // 0.1000000000000000055511151231257827021181583404541015625 // GOOD — use String or valueOf BigDecimal price = new BigDecimal("19.99"); BigDecimal tax = new BigDecimal("0.08"); BigDecimal qty = BigDecimal.valueOf(3); // Arithmetic — all operations return a new BigDecimal BigDecimal subtotal = price.multiply(qty); // 59.97 BigDecimal taxAmt = subtotal.multiply(tax) .setScale(2, RoundingMode.HALF_UP); // 4.80 BigDecimal total = subtotal.add(taxAmt); // 64.77 // Common operations BigDecimal a = new BigDecimal("10.5"); BigDecimal b = new BigDecimal("3.2"); System.out.println(a.add(b)); // 13.7 System.out.println(a.subtract(b)); // 7.3 System.out.println(a.multiply(b)); // 33.60 System.out.println(a.divide(b, 4, RoundingMode.HALF_UP)); // 3.2813 System.out.println(a.abs()); // 10.5 System.out.println(a.negate()); // -10.5 // Comparison — use compareTo(), not equals() (scale is included in equals) BigDecimal x = new BigDecimal("2.0"); BigDecimal y = new BigDecimal("2.00"); System.out.println(x.equals(y)); // false — different scale! System.out.println(x.compareTo(y)); // 0 — same numeric value // Rounding modes BigDecimal half = new BigDecimal("2.5"); System.out.println(half.setScale(0, RoundingMode.HALF_UP)); // 3 System.out.println(half.setScale(0, RoundingMode.HALF_DOWN)); // 2 System.out.println(half.setScale(0, RoundingMode.HALF_EVEN)); // 2 (banker's rounding) Use compareTo(), not equals(), to check whether two BigDecimal values are numerically equal. equals() also compares scale, so 2.0.equals(2.00) is false. To sort BigDecimal values in a TreeSet or similar, this distinction matters.

Overflow happens when the result of a calculation is too large (or too small) to fit in the type that holds it. For example, int can hold at most 2,147,483,647. If you add 1 to that, there is nowhere for the extra bit to go — the value wraps around and becomes −2,147,483,648. Java does this silently, without throwing any exception or giving any warning, which can cause subtle and hard-to-find bugs.

Java provides "exact" versions of the common arithmetic operations — Math.addExact(), Math.multiplyExact(), etc. — that throw an ArithmeticException immediately if the result would overflow. Use these whenever overflow is a real concern:

// Silent overflow — result wraps around int max = Integer.MAX_VALUE; // 2147483647 System.out.println(max + 1); // -2147483648 — no exception! long lmax = Long.MAX_VALUE; System.out.println(lmax + 1); // -9223372036854775808 // Exact arithmetic — throws ArithmeticException on overflow try { int result = Math.addExact(Integer.MAX_VALUE, 1); // throws } catch (ArithmeticException e) { System.out.println("Overflow detected: " + e.getMessage()); } int safe1 = Math.addExact(100, 200); // 300 long safe2 = Math.multiplyExact(100_000L, 100_000L); // 10_000_000_000 long safe3 = Math.subtractExact(0L, Long.MIN_VALUE); // throws // When values might overflow int, use long long bigProduct = (long) width * height; // cast one operand first! // Without cast: width * height overflows int before widening to long // For truly unbounded integers, use BigInteger import java.math.BigInteger; BigInteger factorial100 = BigInteger.ONE; for (int i = 2; i <= 100; i++) { factorial100 = factorial100.multiply(BigInteger.valueOf(i)); } System.out.println(factorial100); // 93326215443944152681699238856266700490715968264381621468...

Primitive types store their value directly in the variable — the variable is the value. Reference types work differently: the variable stores an address that points to where the actual object lives in memory (the heap). You are not working with the object directly; you are working through a reference to it.

Everything that is not a primitive is a reference type: all classes (String, ArrayList, your own classes), arrays, interfaces, enums, and records. This distinction has two practical consequences: reference variables can be null (meaning "points to nothing"), and assigning one reference variable to another copies the address, not the object — both variables then point to the same object.

// Reference types — the variable holds a reference (address), not the object String name = "Alice"; // String is a reference type int[] nums = {1, 2, 3}; // arrays are reference types List list = new ArrayList<>(); // null — absence of a reference (primitives cannot be null) String s = null; // s.length() → NullPointerException — always check before dereferencing if (s != null) { System.out.println(s.length()); } // Reference assignment — both variables point to the same object int[] original = {1, 2, 3}; int[] copy = original; // NOT a copy — same array! copy[0] = 99; System.out.println(original[0]); // 99 — mutation visible through original // To copy an array: use Arrays.copyOf or clone int[] realCopy = Arrays.copyOf(original, original.length); // Primitive arrays — stored as objects on the heap byte[] buffer = new byte[1024]; // 1 KB buffer int[] matrix = new int[100]; // Wrapper classes bridge primitives and reference types List scores = new ArrayList<>(); scores.add(95); // autoboxed: int → Integer int first = scores.get(0); // unboxed: Integer → int // Difference summary: // Primitive: stored by value, cannot be null, no methods, stack-allocated // Reference: stored by reference, can be null, has methods, heap-allocated Arrays of primitives (e.g. int[]) are themselves reference types — the variable holds a reference to the array object on the heap. Assigning one array variable to another copies the reference, not the data. Use Arrays.copyOf() or System.arraycopy() to get a true independent copy.