Contents
- Primitive Types — Quick Reference
- Integer Types — byte, short, int, long
- Floating-point Types — float, double
- char and boolean
- Wrapper Classes and Autoboxing
- Type Casting — Widening and Narrowing
- BigDecimal — Precise Decimal Arithmetic
- Integer Overflow
- Reference Types
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:
| Type | Size | Range | Default | Literal suffix |
| byte | 8-bit | -128 to 127 | 0 | none |
| short | 16-bit | -32,768 to 32,767 | 0 | none |
| int | 32-bit | -2,147,483,648 to 2,147,483,647 | 0 | none |
| long | 64-bit | -9.2×10¹⁸ to 9.2×10¹⁸ | 0L | L or l |
| float | 32-bit IEEE 754 | ~±3.4×10³⁸ (7 decimal digits) | 0.0f | f or F |
| double | 64-bit IEEE 754 | ~±1.8×10³⁰⁸ (15 decimal digits) | 0.0d | d or D (optional) |
| char | 16-bit Unicode | '\u0000' to '\uffff' (0–65,535) | '\u0000' | single quotes |
| boolean | JVM-defined | true / false | false | none |
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.