Java Data Types
Every value in Java has a type, and the type determines three things: how much memory the value occupies, what range of values it can hold, and which operations are valid on it. Java's types fall into two groups: eight primitive types built into the language, and reference types (classes, interfaces, arrays, records, enums), which are unlimited in number.
Primitive Types
Primitives hold a value directly, are never null, and are not objects.
| Type | Size | Range | Default | Typical use |
|---|---|---|---|---|
byte | 8-bit | −128 to 127 | 0 | Raw binary data, I/O buffers |
short | 16-bit | −32,768 to 32,767 | 0 | Rarely used; memory-constrained arrays |
int | 32-bit | −2,147,483,648 to 2,147,483,647 | 0 | Default integer type |
long | 64-bit | ±9.2 × 10¹⁸ | 0L | Large counts, timestamps, IDs |
float | 32-bit | ~±3.4 × 10³⁸, ~7 significant digits | 0.0f | Graphics, large numeric arrays |
double | 64-bit | ~±1.8 × 10³⁰⁸, ~15 significant digits | 0.0 | Default floating-point type |
char | 16-bit | '\u0000' to '\uffff' (0 to 65,535) | '\u0000' | Single UTF-16 code unit |
boolean | JVM-dependent | true or false | false | Conditions and flags |
Sizes are fixed by the language specification and identical on every platform. An int is 32 bits on a Raspberry Pi and on a 64-bit server. This is part of what makes compiled Java code portable.
Integer types
byte b = 100;
short s = 30_000;
int i = 2_000_000_000;
long l = 9_000_000_000L;
An integer literal is an int by default. A value outside int range needs the L suffix to be read as a long; without it, 9_000_000_000 is a compile error (integer number too large). Lowercase l is also accepted but looks like the digit 1, so uppercase is universal.
Integer arithmetic wraps silently on overflow:
int max = Integer.MAX_VALUE; // 2147483647
int wrapped = max + 1; // -2147483648
No exception is thrown. The result rolls over to the minimum value. When overflow must be detected, Math.addExact, Math.multiplyExact, and related methods throw ArithmeticException instead of wrapping. For values that can grow without bound, use java.math.BigInteger.
Use int unless there is a reason not to. long is for values that can exceed about two billion: milliseconds since 1970, file sizes, database IDs. byte and short do not make arithmetic faster; the JVM computes in int anyway, so they are useful mainly for compact arrays and binary protocols.
Floating-point types
float f = 3.14f;
double d = 3.14;
double sci = 6.022e23; // scientific notation
A decimal literal is a double by default; float f = 3.14; is a compile error because assigning a double to a float may lose precision. The f suffix makes the literal a float.
Floating-point numbers are binary approximations, and most decimal fractions cannot be represented exactly:
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 + 0.2 == 0.3); // false
This is not a Java bug; it is inherent to IEEE 754 binary floating point and happens in every language that uses it. The consequences:
- Never compare floating-point values with
==. Compare the absolute difference against a small tolerance. - Never use
floatordoublefor money. Usejava.math.BigDecimal, which represents decimal fractions exactly, or store amounts aslongcents.
Floating-point types also have special values: Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, and Double.NaN (not a number). Dividing a double by zero produces infinity rather than throwing; 0.0 / 0.0 produces NaN. NaN is not equal to anything, including itself, so Double.isNaN(x) is the only correct test.
double is the default for a reason: it has roughly twice the precision of float and on modern hardware is not slower. Use float only when memory matters, such as large arrays of coordinates, or when an API (OpenGL, some ML libraries) requires it.
char
A char holds a single 16-bit UTF-16 code unit, written in single quotes.
char letter = 'A';
char digit = '7';
char newline = '\n';
char unicode = '\u00E9'; // é
char fromCode = 65; // also 'A': chars are numeric
Because char is a numeric type, arithmetic works on it:
char c = 'A';
c++; // 'B'
int code = 'A'; // 65
char next = (char) ('A' + 2); // 'C'; the cast is required because 'A' + 2 is an int
Characters outside the Basic Multilingual Plane (most emoji, some CJK characters) need two char values, a surrogate pair. A single char cannot hold them; use an int code point and the Character class methods when handling such text.
boolean
A boolean is either true or false. It cannot be converted to or from a number:
boolean ready = true;
int flag = ready ? 1 : 0; // explicit conversion via the ternary operator
boolean b = 1; // compile error
if (ready == true) { } // works but redundant; write if (ready)
Conditions in if, while, and for must be boolean. The JVM specification does not fix the storage size of a boolean; it is typically one byte in arrays and an int-sized slot on the stack.
Reference Types
Everything that is not a primitive is a reference type. A reference variable holds the address of an object (or null); the object itself lives on the heap.
String name = "Ada"; // String
int[] scores = {90, 85, 77}; // array
List<String> items = new ArrayList<>(); // interface type, implemented by ArrayList
LocalDate today = LocalDate.now(); // class from java.time
Reference types start with an uppercase letter by convention, which is the quickest visual way to tell them from primitives. Their default value in a field is null.
String
String is a class, not a primitive, but it has language-level support: literals in double quotes, + for concatenation, and automatic conversion of other values when concatenated. Strings are immutable: no method changes a String; every modifying operation returns a new one.
String s = "hello";
s.toUpperCase(); // returns "HELLO" but s is unchanged
s = s.toUpperCase(); // now s is "HELLO"
Strings are covered in depth in their own section.
Arrays
An array holds a fixed number of values of one type. The type of an array of int is int[].
int[] counts = new int[5]; // five ints, all 0
String[] names = {"Ada", "Linus"};
counts[0] = 42;
System.out.println(names.length); // 2
An array of primitives stores the values directly in a contiguous block. An array of objects stores references, initially all null.
Wrapper Classes
Each primitive has a corresponding class in java.lang:
| Primitive | Wrapper |
|---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
Wrappers exist because generics and collections work only with reference types. List<int> is illegal; List<Integer> is the form.
List<Integer> numbers = new ArrayList<>();
numbers.add(5); // autoboxing: int 5 becomes Integer 5
int first = numbers.get(0); // unboxing: Integer becomes int
The compiler inserts the conversions automatically (autoboxing and unboxing). Two consequences deserve attention:
Integer a = 127, b = 127;
Integer c = 1000, d = 1000;
System.out.println(a == b); // true (small values are cached)
System.out.println(c == d); // false (different objects)
System.out.println(c.equals(d)); // true
== on wrapper objects compares references, not values. Only values from −128 to 127 are guaranteed to come from a cache. Always compare wrappers with equals or unbox them first.
Integer count = null;
int n = count; // NullPointerException: unboxing null
A wrapper can be null; a primitive cannot. Unboxing null throws. This is a frequent source of bugs when reading from maps whose keys may be absent.
Wrapper classes also host utility methods: Integer.parseInt("42"), Double.parseDouble("3.5"), Integer.MAX_VALUE, Character.isDigit('7'), Integer.toBinaryString(10).
Primitive Versus Reference: Side by Side
| Aspect | Primitive | Reference |
|---|---|---|
| Stores | The value itself | A reference to an object |
Can be null | No | Yes |
| Default in a field | 0, 0.0, false, '\u0000' | null |
Compared with == | By value | By identity (same object) |
Compared with equals | Not applicable | By content, if the class defines it |
| Usable in generics | No (use wrapper) | Yes |
| Memory | Fixed, small, on the stack or inline in objects | Object on the heap plus a reference |
| Performance | Fastest | Extra indirection and allocation |
The performance difference matters in tight loops and large arrays. An int[] of a million elements is four megabytes of contiguous memory; an Integer[] is a million separate objects plus a million references. For bulk numeric work, primitives and primitive arrays are the right choice.
Choosing a Type
- Whole numbers:
int. Switch tolongif values can exceed ±2 billion. - Decimal numbers for science, graphics, statistics:
double. - Money or anything requiring exact decimals:
BigDecimal. - Single character:
char. Text:String. - Yes/no:
boolean. - A value that might be absent in a collection or generic context: the wrapper class, with explicit
nullhandling, orOptional. - A fixed-size sequence of one type: array. A growable sequence:
List.
public class Product {
private final long id; // IDs can be large; long is standard
private final String name; // text
private final BigDecimal price; // money: exact decimal arithmetic
private int stock; // small whole number that changes
private boolean discontinued; // flag
private final char sizeCode; // single letter: 'S', 'M', 'L'
public Product(long id, String name, BigDecimal price, int stock, char sizeCode) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
this.sizeCode = sizeCode;
}
}
Each field uses the narrowest type that correctly represents the data. price is a BigDecimal rather than a double so that 19.99 * 3 is exactly 59.97, not 59.970000000000006.