Data Types in Java
Master Java's 8 primitive types, their wrapper classes, type casting, String handling, and how Java stores values in memory.
Java has two categories of types: primitives (8 built-in value types) and reference types (objects, including arrays, Strings, and every class you write). Understanding when to use each — and how Java stores them — is fundamental to writing correct Java code. Choosing the wrong type is a common source of subtle bugs, especially around overflow and floating-point precision.
The 8 Primitive Types
Primitives are the building blocks of all Java data. They are stored directly in memory (not as objects), which makes them fast and memory-efficient. Knowing each type’s size and range helps you pick the right one and avoid overflow surprises.
| Type | Size | Range | Default | Use for |
|---|---|---|---|---|
byte | 8-bit | -128 to 127 | 0 | Raw binary data, file I/O |
short | 16-bit | -32,768 to 32,767 | 0 | Rarely used directly |
int | 32-bit | -2.1B to 2.1B | 0 | Whole numbers (default integer type) |
long | 64-bit | ±9.2 × 10¹⁸ | 0L | Large whole numbers, timestamps |
float | 32-bit | ~7 decimal digits | 0.0f | Rarely used; prefer double |
double | 64-bit | ~15 decimal digits | 0.0 | Decimal numbers (default float type) |
char | 16-bit | ’ ’ to ’' | ' ‘ | Single Unicode character |
boolean | 1-bit | true / false | false | Flags and conditions |
// Integer types
byte b = 127;
short s = 30_000; // underscores allowed for readability
int i = 2_147_483_647; // Integer.MAX_VALUE
long l = 9_223_372_036_854_775_807L; // L suffix required for long literals
// Floating-point
float f = 3.14f; // f suffix required
double d = 3.141592653589793; // default for decimal literals
// Character
char c = 'A';
char euro = '€'; // Unicode escape = €
System.out.println((int) c); // 65 — char is really an unsigned 16-bit int
// Boolean
boolean isJavaFun = true;
boolean isEmpty = false;
Integer Overflow
Integer overflow is one of the most dangerous silent bugs in Java — the runtime does not throw an exception, it just wraps around to a wrong value. Knowing this upfront will save you from hard-to-trace calculation errors.
int max = Integer.MAX_VALUE; // 2147483647
System.out.println(max + 1); // -2147483648 — overflows silently!
// Safe check using Math:
int result = Math.addExact(max, 1); // throws ArithmeticException on overflow
Floating-Point Precision
Floating-point types (double and float) use binary representation internally, which cannot exactly represent most decimal fractions. This is not a Java bug — it is how IEEE 754 works in every major language. The practical rule: never use double for money or any calculation where exact decimal values matter. Use BigDecimal instead.
System.out.println(0.1 + 0.2); // 0.30000000000000004 — NOT 0.3
System.out.println(0.1 + 0.2 == 0.3); // false
// For exact decimal arithmetic, use BigDecimal:
import java.math.BigDecimal;
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.add(b)); // 0.3 — exact
// Always construct BigDecimal from String, not double:
new BigDecimal(0.1) // WRONG — 0.1000000000000000055511...
new BigDecimal("0.1") // CORRECT — exactly 0.1
Wrapper Classes
Every primitive has a corresponding wrapper class — an object representation that adds utility methods and enables use in generics. You cannot put an int into a List<Integer>, but you can put an Integer. Java’s autoboxing feature handles the conversion automatically in most cases.
| Primitive | Wrapper | Useful methods |
|---|---|---|
int | Integer | parseInt, valueOf, MAX_VALUE, toBinaryString |
long | Long | parseLong, toBinaryString, bitCount |
double | Double | parseDouble, isNaN, isInfinite |
boolean | Boolean | parseBoolean, toString |
char | Character | isDigit, isLetter, toUpperCase |
// Parsing strings to numbers — essential for user input
int age = Integer.parseInt("25");
double price = Double.parseDouble("19.99");
boolean flag = Boolean.parseBoolean("true");
// Useful constants
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.MIN_VALUE); // -2147483648
System.out.println(Double.MAX_VALUE); // 1.7976931348623157E308
System.out.println(Integer.toBinaryString(42)); // 101010
// Wrapper utility methods
System.out.println(Character.isDigit('7')); // true
System.out.println(Character.isLetter('A')); // true
System.out.println(Character.toUpperCase('a')); // A
Autoboxing and Unboxing
Java automatically converts between primitives and their wrapper classes. This convenience feature is transparent in most code, but the null-unboxing pitfall it creates is a common source of NullPointerException bugs.
// Autoboxing — primitive to wrapper
Integer boxed = 42; // same as: Integer.valueOf(42)
List<Integer> list = new ArrayList<>();
list.add(99); // int 99 autoboxed to Integer
// Unboxing — wrapper to primitive
int unboxed = boxed; // same as: boxed.intValue()
int total = list.get(0) + 1; // unboxing before arithmetic
// Pitfall — null unboxing throws NullPointerException
Integer nullValue = null;
int x = nullValue; // NullPointerException at runtime!
Type Casting
Widening (automatic, no data loss)
Widening conversions are safe and automatic — Java allows them because the destination type is always large enough to hold the value. No data is lost.
byte → short → int → long → float → double
int i = 100;
long l = i; // widening — automatic
double d = i; // widening — automatic
System.out.println(d); // 100.0
Narrowing (explicit, may lose data)
Narrowing conversions require an explicit cast because you are telling the compiler “I know this might lose data — do it anyway.” Always check the range before narrowing to avoid silently corrupted values.
double d = 9.99;
int i = (int) d; // explicit cast — truncates decimal
System.out.println(i); // 9 — NOT rounded, just truncated
long big = 1_000_000_000_000L;
int small = (int) big; // data loss — only keeps lower 32 bits
System.out.println(small); // -727379968 — garbage
// Safe check before narrowing:
if (big >= Integer.MIN_VALUE && big <= Integer.MAX_VALUE) {
int safe = (int) big;
}
char ↔ int
char is an unsigned 16-bit integer under the hood, so it can be widened to int automatically and narrowed back with a cast. This lets you do arithmetic on characters — useful for encryption algorithms and character manipulation.
char c = 'A';
int code = c; // widening: 65
char back = (char)(code + 1); // narrowing: 'B'
System.out.println(back); // B
Strings in Depth
String is immutable — once created, its value never changes. This is not just an implementation detail: it is what makes strings safe to share across threads, use as HashMap keys, and pass to methods without fear of the caller mutating them. Every “modification” creates a new object, so be aware of that cost when building strings in loops.
String s = "Hello";
s = s + " World"; // "Hello" is discarded; new String created in memory
// String pool — literals are interned (shared)
String a = "Java";
String b = "Java";
System.out.println(a == b); // true — same object in pool
System.out.println(a.equals(b)); // true
// new String bypasses the pool
String c = new String("Java");
System.out.println(a == c); // false — different objects
System.out.println(a.equals(c)); // true — same content
// ALWAYS use .equals() to compare String content, never ==
Common String Methods
String s = " Hello, Java World! ";
s.trim() // "Hello, Java World!" — removes leading/trailing whitespace
s.strip() // same but Unicode-aware (Java 11+)
s.toLowerCase() // " hello, java world! "
s.toUpperCase() // " HELLO, JAVA WORLD! "
s.contains("Java") // true
s.startsWith(" Hello") // true
s.endsWith("!") // false (has trailing spaces)
s.replace("Java", "Python") // " Hello, Python World! "
s.split(", ") // [" Hello", "Java World! "]
s.indexOf("Java") // 8
s.substring(8, 12) // "Java"
s.charAt(8) // 'H' ... (index 8 of this string)
s.isEmpty() // false
s.isBlank() // false (Java 11+)
"".isEmpty() // true
" ".isBlank() // true
StringBuilder — Efficient String Building
When building strings in a loop, use StringBuilder to avoid creating many intermediate String objects. Each + concatenation in a loop allocates a new String, which becomes a garbage-collection burden. StringBuilder mutates a single internal buffer and produces one String at the end.
// BAD — creates 1000 String objects
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // new String every iteration
}
// GOOD — StringBuilder mutates in place
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString(); // one final String
// StringBuilder chaining
String csv = new StringBuilder()
.append("Alice")
.append(",")
.append(25)
.append(",")
.append("Engineer")
.toString(); // "Alice,25,Engineer"
var — Local Type Inference (Java 10+)
var reduces boilerplate in local variable declarations by letting the compiler infer the type from the initialiser. The type is still fixed at compile time — var is not dynamic typing, just a way to avoid writing the type name twice when it is already obvious from the right-hand side.
var count = 0; // int
var name = "Alice"; // String
var items = new ArrayList<String>(); // ArrayList<String>
var scores = new int[]{1, 2, 3}; // int[]
// var only works for local variables with an initialiser
// var x; // ERROR — no initialiser
// var obj = null; // ERROR — type cannot be inferred from null
Summary
| Category | Types | Notes |
|---|---|---|
| Integer | byte short int long | Default to int; use long for large values |
| Floating-point | float double | Default to double; use BigDecimal for exact math |
| Character | char | 16-bit Unicode; rarely used directly |
| Boolean | boolean | true or false only |
| Text | String | Immutable object; use StringBuilder for building |
| Wrappers | Integer Double etc. | Object form of primitives; needed for generics |