Java Data Types: Primitives, Wrappers, and Reference Types
Java is statically typed: every variable has a type declared at compile time. There are two categories: primitive types (8 built-in types stored directly on the stack) and reference types (objects stored on the heap, accessed via a reference). Choosing the right type affects memory, performance, and correctness, especially for numeric precision and overflow.
Contents
Primitive Types: Quick Reference
Java has exactly 8 primitive types. They are stored by value (not reference), require no object allocation, and cannot be null
| 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 | Ll |
float | 32-bit IEEE 754 | ~ยฑ3.4ร10ยณโธ (7 decimal digits) | 0.0f | fF |
double | 64-bit IEEE 754 | ~ยฑ1.8ร10ยณโฐโธ (15 decimal digits) | 0.0d | dD |
char | 16-bit Unicode | '\u0000' to '\uffff' (0โ65,535) | '\u0000' | single quotes |
boolean | JVM-defined | true / false | false | none |
Integer Types: byte, short, int, long
Integer types store whole numbers: numbers with no decimal point, like 042-71_000_000byteshortintlong
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 longLint
// 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
intlongbyteshortintFloating-point Types: float, double
Floating-point types store numbers with a decimal point, like 3.14-0.51.996.022e231.6e-19
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
in code, Java treats it as a3.14 automatically. Usedouble for almost all decimal arithmetic: scientific calculations, measurements, geometry, statistics.double - float: 4 bytes, about 7 significant decimal digits. Half the memory of
, but also half the precision. Because modern hardware handlesdouble just as fast asdouble , you would only choosefloat when memory genuinely matters, for example, an array of millions of 3D coordinates in a graphics engine. Float literals must be suffixed withfloat .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.11/30.1 + 0.20.3BigDecimal
// 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โปยนโน
floatdoubleBigDecimalchar and boolean
charchar
booleantruefalseage > 18a && bboolean
// 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
Wrapper Classes and Autoboxing
Java's generic collections (ListMapSetintdoublebooleanintIntegerdoubleDoublebooleanBoolean
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!
equals()==IntegerLongInteger==falseType Casting: Widening and Narrowing
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 . This is always safe because the larger type can hold everything the smaller type can, so Java does it automatically with no cast needed.long - Narrowing: going from a larger type to a smaller one, e.g.
โlong orint โdouble . 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 asint . The compiler is making you acknowledge you know data may be lost.(targetType)
// 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: Precise Decimal Arithmetic
As explained above, doublejava.math.BigDecimaldoubleBigDecimal0.10.1double
The trade-off is that BigDecimaldouble+*-add()multiply()subtract()BigDecimalStringdoubledoubledoubleBigDecimal
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)
compareTo()equals()BigDecimalequals()2.0.equals(2.00)falseBigDecimalTreeSetInteger Overflow
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
Java provides "exact" versions of the common arithmetic operations (Math.addExact()Math.multiplyExact()ArithmeticException
// 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...
Reference Types
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 (StringArrayListnull
// 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
int[]Arrays.copyOf()System.arraycopy()