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

int is the default integer type and the right choice for most counters and indices. Use long when values can exceed ~2.1 billion (always suffix the literal with L). Use byte or short only to save memory in large arrays — the JVM promotes them to int for arithmetic anyway:

byte b = 127; // -128 to 127 short s = 32_000; // -32768 to 32767 int i = 2_147_483_647; // ~2.1 billion max (Integer.MAX_VALUE) long l = 9_000_000_000L; // must suffix with L for long literals // Hex, octal, binary literals int hex = 0xFF; // 255 int bin = 0b1010_1010; // 170 — underscores allowed for readability // Compile-time constants from wrapper classes System.out.println(Integer.MAX_VALUE); // 2147483647 System.out.println(Integer.MIN_VALUE); // -2147483648 System.out.println(Long.MAX_VALUE); // 9223372036854775807 // Unsigned arithmetic (Java 8+) — use Integer/Long static methods int unsigned = Integer.parseUnsignedInt("4294967295"); // max unsigned 32-bit System.out.println(Integer.toUnsignedString(unsigned)); // 4294967295

double is the default for decimal numbers and should be your first choice. float offers half the memory (4 bytes vs 8) but only 7 significant decimal digits — use it only in memory-constrained contexts like large graphics or sensor arrays. Neither is suitable for financial calculations; use BigDecimal instead:

double pi = 3.141592653589793; // 15 significant digits float price = 9.99f; // must suffix with f // Special values System.out.println(Double.POSITIVE_INFINITY); // Infinity System.out.println(Double.NEGATIVE_INFINITY); // -Infinity System.out.println(Double.NaN); // NaN (e.g. 0.0/0.0) System.out.println(Double.isNaN(0.0 / 0.0)); // true // Floating-point is imprecise — never use == for comparison double a = 0.1 + 0.2; System.out.println(a == 0.3); // false! System.out.println(Math.abs(a - 0.3) < 1e-9); // true — use epsilon comparison // Scientific notation double avogadro = 6.022e23; double electron = 1.6e-19; Never use float or double for monetary values. Use BigDecimal instead — floating-point cannot represent most decimal fractions exactly, so rounding errors accumulate.

char holds a single Unicode code unit (UTF-16). It is technically an unsigned 16-bit integer and can participate in arithmetic. boolean holds exactly true or false and is the type of all conditional expressions:

// 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

Each primitive has a corresponding wrapper class in java.lang. Wrappers are required when generics are involved (e.g. List<Integer>) and provide utility methods. Java automatically converts between primitives and wrappers — autoboxing (primitive → wrapper) and unboxing (wrapper → primitive):

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

Widening conversions (smaller → larger type) happen implicitly and are always safe. Narrowing conversions (larger → smaller type) require an explicit cast and may lose data — the extra bits are silently dropped:

// 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)

BigDecimal provides arbitrary-precision decimal arithmetic. Always construct it from a String or BigDecimal.valueOf(double) — never from a raw double literal, which already carries floating-point imprecision. All operations return a new BigDecimal (it is immutable):

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.

Java integer arithmetic wraps around silently on overflow — no exception is thrown. Use Math.addExact(), Math.multiplyExact(), etc. to get an ArithmeticException on overflow, or use long / BigInteger when the range may be exceeded:

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

Any type that is not a primitive is a reference type — the variable holds a memory address (reference) to an object on the heap, not the object itself. This includes all classes, interfaces, arrays, enums, and records. Reference variables can be null:

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