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 |
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?
- byte — 1 byte of memory, holds −128 to 127. Rarely used for individual variables. Useful for raw binary data (file buffers, network packets) or to save memory in very large arrays of small values.
- short — 2 bytes, holds −32,768 to 32,767. Uncommon in everyday code. Sometimes used in audio processing or embedded-style data structures where saving 2 bytes per element matters at scale.
- int — 4 bytes, holds roughly −2.1 billion to +2.1 billion. This is the default integer type in Java and the right choice the vast majority of the time — for loop counters, array indices, ages, quantities, IDs, scores, and most other whole-number values you encounter day to day.
- long — 8 bytes, holds roughly −9.2 quintillion to +9.2 quintillion. Use this when your value could exceed ~2.1 billion: timestamps in milliseconds, file sizes in bytes, population counts, financial totals, database row IDs in large systems.
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:
- double — 8 bytes, about 15–16 significant decimal digits of precision. This is the default floating-point type in Java. When you write 3.14 in code, Java treats it as a double automatically. Use double for almost all decimal arithmetic: scientific calculations, measurements, geometry, statistics.
- float — 4 bytes, about 7 significant decimal digits. Half the memory of double, but also half the precision. Because modern hardware handles double just as fast as float, you would only choose float when memory genuinely matters — for example, an array of millions of 3D coordinates in a graphics engine. Float literals must be suffixed with f.
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 — going from a smaller type to a larger one, e.g. int → long. This is always safe because the larger type can hold everything the smaller type can, so Java does it automatically with no cast needed.
- Narrowing — going from a larger type to a smaller one, e.g. long → int or double → int. This can lose information (the fractional part is dropped, or the value wraps if it is out of range), so Java requires an explicit cast written as (targetType). The compiler is making you acknowledge you know data may be lost.
// 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.