In the previous tutorial we discussed Java Variables and learned that every variable must have three things: a type, a name, and a value. The type (also called data type) tells Java what kind of data the variable can hold and how much memory to allocate for it. In this tutorial, we will take a deep dive into Java data types.
Quick recap on variables:
public static void main(String[] args) {
// type name value
String firstName = "Folau";
int age = 30;
double salary = 85000.50;
System.out.println(firstName); // Folau
System.out.println(age); // 30
System.out.println(salary); // 85000.5
}
Java data types fall into two broad categories:
byte, short, int, long, float, double, boolean, and char. They hold their values directly in memory and are not objects.String, Arrays, List, and any class you define.Understanding the difference is fundamental to writing correct, efficient Java code. Let us start with the primitives.
Java has exactly 8 primitive data types. Each has a fixed size in memory and a defined range of values it can hold. Here is a summary:
| Type | Size | Range | Default Value | When to Use |
|---|---|---|---|---|
byte |
1 byte (8 bits) | -128 to 127 | 0 | Raw binary data, file I/O, saving memory in large arrays |
short |
2 bytes (16 bits) | -32,768 to 32,767 | 0 | Rarely used; memory-sensitive applications with small numbers |
int |
4 bytes (32 bits) | -2,147,483,648 to 2,147,483,647 | 0 | Default choice for whole numbers, loop counters, general arithmetic |
long |
8 bytes (64 bits) | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | 0L | Timestamps, database IDs, values exceeding int range |
float |
4 bytes (32 bits) | ±3.4 × 1038 (6-7 decimal digits of precision) | 0.0f | Graphics, game engines, when memory matters more than precision |
double |
8 bytes (64 bits) | ±1.7 × 10308 (15 decimal digits of precision) | 0.0d | Default for decimal numbers, scientific calculations, currency (with caution) |
boolean |
1 bit* | true or false |
false | Flags, conditions, on/off states |
char |
2 bytes (16 bits) | 0 to 65,535 (Unicode characters) | ‘\u0000’ | Single characters, Unicode processing |
* The JVM does not define a precise size for boolean. It typically uses 1 byte internally, but the logical size is a single bit.
These four types store whole numbers (no decimal point). The only difference between them is how much memory they use and the range of values they can hold.
The byte type is the smallest integer type. It is commonly used when reading raw binary data from files or network streams, or when you need to save memory in large arrays.
public static void main(String[] args) {
byte minByte = -128;
byte maxByte = 127;
byte userAge = 25; // Age fits easily in a byte
System.out.println("Min byte: " + minByte); // -128
System.out.println("Max byte: " + maxByte); // 127
System.out.println("Age: " + userAge); // 25
// Useful for binary data
byte[] fileData = {72, 101, 108, 108, 111}; // "Hello" in ASCII
System.out.println(new String(fileData)); // Hello
}
The short type is rarely used in modern Java. It can hold values from -32,768 to 32,767. You might encounter it in legacy code or specialized applications where memory savings on large datasets matter.
public static void main(String[] args) {
short temperature = -15;
short elevation = 8849; // Mount Everest height in meters
short year = 2026;
System.out.println("Temperature: " + temperature + "°C"); // -15°C
System.out.println("Elevation: " + elevation + "m"); // 8849m
System.out.println("Year: " + year); // 2026
}
The int type is the default choice for whole numbers in Java. Unless you have a specific reason to use another type, use int. It can hold values up to approximately 2.1 billion, which is sufficient for most use cases like loop counters, array indices, quantities, and general arithmetic.
public static void main(String[] args) {
int population = 331000000; // US population
int salary = 95000;
int itemCount = 0;
// Common use: loop counter
for (int i = 0; i < 5; i++) {
itemCount++;
}
System.out.println("Population: " + population); // 331000000
System.out.println("Salary: $" + salary); // $95000
System.out.println("Items: " + itemCount); // 5
// You can use underscores for readability (Java 7+)
int billion = 1_000_000_000;
System.out.println("Billion: " + billion); // 1000000000
}
The long type is used when int is not large enough. Common scenarios include timestamps (milliseconds since epoch), database primary key IDs, file sizes in bytes, and large financial calculations. Long literals must end with an L suffix.
public static void main(String[] args) {
long worldPopulation = 8_000_000_000L; // 8 billion - too large for int!
long timestamp = System.currentTimeMillis();
long fileSize = 5_368_709_120L; // 5 GB in bytes
long databaseId = 9_876_543_210L;
System.out.println("World population: " + worldPopulation);
System.out.println("Current timestamp: " + timestamp);
System.out.println("File size: " + fileSize + " bytes");
System.out.println("DB ID: " + databaseId);
// Without the L suffix, Java treats the number as an int and you get an error
// long wrong = 9876543210; // COMPILE ERROR: integer number too large
long correct = 9876543210L; // Works because of the L suffix
}
These types store decimal (fractional) numbers. The key difference between them is precision — how many decimal digits they can accurately represent.
The float type provides about 6-7 digits of precision. Float literals must end with an f suffix. It is used in graphics programming, game engines, and situations where memory is more important than precision.
public static void main(String[] args) {
float pi = 3.14159f; // Note the 'f' suffix
float temperature = 98.6f;
float gpa = 3.85f;
float price = 19.99f;
System.out.println("Pi: " + pi); // 3.14159
System.out.println("Temp: " + temperature); // 98.6
System.out.println("GPA: " + gpa); // 3.85
System.out.println("Price: $" + price); // $19.99
// Without 'f', Java treats decimal numbers as double and you get an error
// float wrong = 3.14; // COMPILE ERROR: incompatible types
float correct = 3.14f; // Works because of the f suffix
}
The double type is the default choice for decimal numbers in Java. It provides about 15 digits of precision, which is more than enough for most applications. When you write a decimal literal like 3.14, Java automatically treats it as a double.
public static void main(String[] args) {
double pi = 3.141592653589793;
double accountBalance = 1_250_000.75;
double interestRate = 0.045; // 4.5%
System.out.println("Pi: " + pi); // 3.141592653589793
System.out.println("Balance: $" + accountBalance); // $1250000.75
// Calculate interest
double interest = accountBalance * interestRate;
System.out.println("Interest: $" + interest); // $56250.03375
// Precision difference between float and double
float floatVal = 1.123456789f;
double doubleVal = 1.123456789;
System.out.println("float: " + floatVal); // 1.1234568 (lost precision after 7 digits)
System.out.println("double: " + doubleVal); // 1.123456789 (accurate to 15 digits)
}
The boolean type can hold only two values: true or false. It is used for flags, conditions, and logical expressions. Booleans are the backbone of control flow in Java — every if statement, while loop, and conditional expression evaluates to a boolean.
public static void main(String[] args) {
boolean isLoggedIn = true;
boolean isAdmin = false;
boolean hasPermission = true;
// Used in conditions
if (isLoggedIn && hasPermission) {
System.out.println("Access granted"); // This prints
}
if (isAdmin) {
System.out.println("Welcome, admin");
} else {
System.out.println("Welcome, user"); // This prints
}
// Boolean from comparisons
int age = 21;
boolean canVote = age >= 18;
boolean isTeenager = age >= 13 && age <= 19;
System.out.println("Can vote: " + canVote); // true
System.out.println("Is teenager: " + isTeenager); // false
// Boolean with methods
String email = "user@example.com";
boolean validEmail = email.contains("@");
System.out.println("Valid email: " + validEmail); // true
}
The char type stores a single character. In Java, characters use Unicode (UTF-16) encoding, which means char can represent not just English letters but characters from virtually any language, as well as symbols and emojis. A char value is enclosed in single quotes ('A'), unlike strings which use double quotes ("A").
public static void main(String[] args) {
char letter = 'A';
char digit = '7';
char symbol = '$';
char space = ' ';
System.out.println("Letter: " + letter); // A
System.out.println("Digit: " + digit); // 7
System.out.println("Symbol: " + symbol); // $
// char is actually a number (Unicode code point)
char letterA = 65; // 65 is the Unicode value for 'A'
System.out.println(letterA); // A
// Unicode escape sequences
char copyright = '\u00A9'; // copyright symbol
char omega = '\u03A9'; // Greek capital letter Omega
System.out.println("Copyright: " + copyright); // ©
System.out.println("Omega: " + omega); // Ω
// You can do arithmetic with char (it is a numeric type)
char ch = 'A';
ch++;
System.out.println(ch); // B
// Convert char to its numeric value
char c = 'Z';
int asciiValue = (int) c;
System.out.println("ASCII value of 'Z': " + asciiValue); // 90
}
Non-primitive data types are also called reference types because a variable of this type does not hold the value directly — it holds a reference (a pointer) to an object in memory. Reference types are created from classes and include String, arrays, collections, and any custom class you define.
String is the most commonly used reference type. Although it is not a primitive, it is so fundamental that Java gives it special treatment (you can create a String without using the new keyword). We cover Strings in detail in the Java String tutorial.
public static void main(String[] args) {
// String is a reference type, but you can create it like a primitive
String name = "Folau";
String greeting = "Hello, " + name + "!";
System.out.println(greeting); // Hello, Folau!
System.out.println(name.length()); // 5
System.out.println(name.toUpperCase()); // FOLAU
// Strings are objects with methods - primitives do not have methods
boolean startsWithF = name.startsWith("F");
System.out.println(startsWithF); // true
}
Arrays are reference types that hold a fixed-size collection of elements of the same type. We cover arrays in detail in the Java Arrays tutorial.
public static void main(String[] args) {
// Array of integers
int[] scores = {95, 87, 73, 100, 88};
// Array of strings
String[] names = {"Alice", "Bob", "Charlie"};
System.out.println("First score: " + scores[0]); // 95
System.out.println("Second name: " + names[1]); // Bob
System.out.println("Array length: " + scores.length); // 5
}
Any class you define creates a new reference type. When you create an object using the new keyword, you get a reference to that object in memory.
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
void greet() {
System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
}
}
public static void main(String[] args) {
// 'user' is a reference variable - it holds the memory address of a User object
User user = new User("Folau", 30);
user.greet(); // Hi, I'm Folau and I'm 30 years old.
// Reference types can be null - primitives cannot
User emptyUser = null;
System.out.println(emptyUser); // null
}
| Feature | Primitive Types | Non-Primitive (Reference) Types |
|---|---|---|
| Defined by | Built into the Java language | Created by the programmer (except String) |
| Naming | Start with lowercase: int, double, boolean |
Start with uppercase: String, User, List |
| Stores | The actual value directly | A reference (memory address) to the object |
| Can be null? | No — always has a value | Yes — can be null |
| Has methods? | No | Yes — e.g., name.length(), user.greet() |
| Memory | Stored on the stack (fast access) | Object stored on the heap, reference on the stack |
| Default value | 0, 0.0, false, or '\u0000' |
null |
Type casting is the process of converting a value from one data type to another. In Java, there are two types of casting:
Widening casting happens automatically when you assign a smaller type to a larger type. There is no risk of data loss because the larger type can hold all possible values of the smaller type.
The widening order is:
byte → short → int → long → float → double
public static void main(String[] args) {
// Widening: Java automatically converts to the larger type
byte myByte = 42;
short myShort = myByte; // byte -> short (automatic)
int myInt = myShort; // short -> int (automatic)
long myLong = myInt; // int -> long (automatic)
float myFloat = myLong; // long -> float (automatic)
double myDouble = myFloat; // float -> double (automatic)
System.out.println("byte: " + myByte); // 42
System.out.println("short: " + myShort); // 42
System.out.println("int: " + myInt); // 42
System.out.println("long: " + myLong); // 42
System.out.println("float: " + myFloat); // 42.0
System.out.println("double: " + myDouble); // 42.0
// This is also widening (int to double)
int score = 85;
double percentage = score; // Automatically converts to 85.0
System.out.println("Percentage: " + percentage); // 85.0
}
Narrowing casting must be done manually by placing the target type in parentheses before the value. This is required when converting a larger type to a smaller type because there is a risk of data loss — the value might not fit in the smaller type.
The narrowing order is:
double → float → long → int → short → byte
public static void main(String[] args) {
// Narrowing: you must explicitly cast with (type)
double myDouble = 9.78;
int myInt = (int) myDouble; // Truncates the decimal, does NOT round
System.out.println("double: " + myDouble); // 9.78
System.out.println("int: " + myInt); // 9 (decimal part lost!)
// More examples
int largeNumber = 130;
byte smallByte = (byte) largeNumber;
System.out.println("int: " + largeNumber); // 130
System.out.println("byte: " + smallByte); // -126 (overflow! 130 exceeds byte range)
double price = 49.99;
float floatPrice = (float) price; // double -> float
long longPrice = (long) price; // double -> long (truncates decimal)
int intPrice = (int) price; // double -> int (truncates decimal)
System.out.println("double: " + price); // 49.99
System.out.println("float: " + floatPrice); // 49.99
System.out.println("long: " + longPrice); // 49
System.out.println("int: " + intPrice); // 49
// Without the cast, you get a compile error
// int x = 3.14; // COMPILE ERROR
int x = (int) 3.14; // OK - x is 3
}
Every primitive type has a corresponding wrapper class that wraps the primitive value in an object. Wrapper classes are part of the java.lang package and are essential when you need to use primitives as objects — for example, in collections like ArrayList, which cannot hold primitive types directly.
| Primitive Type | Wrapper Class |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
boolean |
Boolean |
char |
Character |
Wrapper classes also provide useful utility methods for parsing, converting, and working with their respective types.
public static void main(String[] args) {
// Wrapper classes provide useful utility methods
// Parsing strings to numbers
int num = Integer.parseInt("42");
double price = Double.parseDouble("19.99");
boolean flag = Boolean.parseBoolean("true");
System.out.println(num); // 42
System.out.println(price); // 19.99
System.out.println(flag); // true
// Getting min and max values
System.out.println("int max: " + Integer.MAX_VALUE); // 2147483647
System.out.println("int min: " + Integer.MIN_VALUE); // -2147483648
System.out.println("double max: " + Double.MAX_VALUE); // 1.7976931348623157E308
// Converting numbers to strings
String numStr = Integer.toString(42);
String hexStr = Integer.toHexString(255); // "ff"
String binStr = Integer.toBinaryString(10); // "1010"
System.out.println("Hex: " + hexStr); // ff
System.out.println("Binary: " + binStr); // 1010
// Comparing values
int result = Integer.compare(10, 20);
System.out.println("Compare 10 vs 20: " + result); // -1 (10 < 20)
}
Since Java 5, the compiler can automatically convert between primitives and their wrapper classes. This feature eliminates the need for manual conversion in most cases.
int → Integer)Integer → int)import java.util.ArrayList;
import java.util.List;
public static void main(String[] args) {
// AUTOBOXING: primitive -> wrapper (automatic)
Integer wrappedInt = 42; // int 42 is autoboxed to Integer
Double wrappedDouble = 3.14; // double 3.14 is autoboxed to Double
Boolean wrappedBool = true; // boolean true is autoboxed to Boolean
System.out.println(wrappedInt); // 42
System.out.println(wrappedDouble); // 3.14
System.out.println(wrappedBool); // true
// UNBOXING: wrapper -> primitive (automatic)
int primitiveInt = wrappedInt; // Integer is unboxed to int
double primitiveDouble = wrappedDouble; // Double is unboxed to double
System.out.println(primitiveInt); // 42
System.out.println(primitiveDouble); // 3.14
// Autoboxing is essential for Collections
// ArrayList cannot hold primitive 'int', but autoboxing handles it
List numbers = new ArrayList<>();
numbers.add(10); // autoboxing: int -> Integer
numbers.add(20); // autoboxing: int -> Integer
numbers.add(30); // autoboxing: int -> Integer
// Unboxing when retrieving
int first = numbers.get(0); // unboxing: Integer -> int
System.out.println("First: " + first); // 10
// Autoboxing in arithmetic expressions
Integer a = 10;
Integer b = 20;
int sum = a + b; // Both are unboxed, added, result stays as int
System.out.println("Sum: " + sum); // 30
}
Choosing the right data type is an important decision. Here is practical guidance for real-world scenarios:
| Scenario | Recommended Type | Reason |
|---|---|---|
| Loop counters, array indices | int |
Default choice for whole numbers; fast and sufficient range |
| Age, quantity, count | int |
Simple whole numbers within normal ranges |
| Database IDs | long |
IDs can exceed int range in large databases |
| Timestamps | long |
Milliseconds since epoch requires long range |
| File sizes | long |
Files can exceed 2 GB (int max) |
| Prices, financial calculations | BigDecimal |
Avoid float/double for money — use java.math.BigDecimal for exact precision |
| Scientific calculations | double |
15 digits of precision; default for decimals |
| Graphics, game coordinates | float |
Sufficient precision, half the memory of double |
| Yes/no flags, feature toggles | boolean |
Only two possible values |
| Single characters, Unicode | char |
One character at a time |
| Raw binary data, byte streams | byte |
File I/O and network data operate on bytes |
| Text, names, messages | String |
Reference type for sequences of characters |
| Collections (ArrayList, HashMap) | Wrapper classes | Collections require objects, not primitives |
Rule of thumb: Use int for whole numbers and double for decimals unless you have a specific reason to choose another type. When in doubt, go bigger — the memory savings from using byte or short are rarely worth the complexity.
Even experienced developers make these mistakes. Understanding them early will save you hours of debugging.
When a value exceeds the maximum (or minimum) range of its type, it "wraps around" to the other end. Java does not throw an error — it silently overflows, which can produce very unexpected results.
public static void main(String[] args) {
// Integer overflow - the value wraps around silently!
int maxInt = Integer.MAX_VALUE; // 2,147,483,647
int overflow = maxInt + 1;
System.out.println("Max int: " + maxInt); // 2147483647
System.out.println("Max int + 1: " + overflow); // -2147483648 (wrapped to minimum!)
// This is a common bug when calculating large values
int million = 1_000_000;
int result = million * million; // 1,000,000,000,000 - too big for int!
System.out.println("Million squared (int): " + result); // -727379968 (wrong!)
// Fix: use long
long correctResult = (long) million * million;
System.out.println("Million squared (long): " + correctResult); // 1000000000000
}
Floating-point numbers (float and double) cannot represent all decimal values exactly. This is a fundamental limitation of how computers store decimal numbers in binary. Never use float or double for financial calculations.
import java.math.BigDecimal;
public static void main(String[] args) {
// Floating-point precision problem
double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println("0.1 + 0.2 = " + sum); // 0.30000000000000004 (not 0.3!)
System.out.println("0.1 + 0.2 == 0.3? " + (sum == 0.3)); // false!
// This is why you should NEVER use double for money
double price = 0.10;
double quantity = 3;
double total = price * quantity;
System.out.println("$0.10 x 3 = $" + total); // $0.30000000000000004
// Use BigDecimal for financial calculations
BigDecimal bdPrice = new BigDecimal("0.10");
BigDecimal bdQuantity = new BigDecimal("3");
BigDecimal bdTotal = bdPrice.multiply(bdQuantity);
System.out.println("BigDecimal: $0.10 x 3 = $" + bdTotal); // $0.30
}
Using == on wrapper objects compares references, not values. This can produce surprising results because Java caches Integer values between -128 and 127.
public static void main(String[] args) {
// Cached range (-128 to 127): == works because Java reuses the same objects
Integer x = 100;
Integer y = 100;
System.out.println(x == y); // true (cached, same object)
System.out.println(x.equals(y)); // true
// Outside cached range: == fails because they are different objects
Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false! (different objects in memory)
System.out.println(a.equals(b)); // true (correct way to compare)
// ALWAYS use .equals() to compare wrapper objects
}
If a wrapper object is null and you try to unbox it to a primitive, Java throws a NullPointerException.
public static void main(String[] args) {
Integer wrappedValue = null;
// This throws NullPointerException at runtime!
// int primitiveValue = wrappedValue; // unboxing null -> crash
// Safe approach: check for null first
if (wrappedValue != null) {
int safeValue = wrappedValue;
System.out.println(safeValue);
} else {
System.out.println("Value is null, using default: 0");
int safeValue = 0;
}
}
Long literals need an L suffix, and float literals need an f suffix. Without them, Java treats the value as int or double, which can cause compile errors or unexpected behavior.
public static void main(String[] args) {
// WRONG: no suffix
// long bigNumber = 3000000000; // COMPILE ERROR: integer too large
// float pi = 3.14; // COMPILE ERROR: incompatible types
// CORRECT: with suffix
long bigNumber = 3000000000L; // L suffix for long
float pi = 3.14f; // f suffix for float
System.out.println(bigNumber); // 3000000000
System.out.println(pi); // 3.14
// Tip: Use uppercase L to avoid confusion with the digit 1
long value = 123456789L; // Clear
// long value = 123456789l; // Confusing - lowercase l looks like 1
}
byte, short, int, long, float, double, boolean, and char.String, arrays, and any class you define. They hold references to objects, not the values themselves.int as your default for whole numbers and double as your default for decimals.Integer, Double, etc.) for use in collections and APIs that require objects.null values causing NullPointerException.In the next tutorial, we will cover Java Operators, where you will learn how to perform arithmetic, comparisons, and logical operations on these data types.
Previous: Java Variables | Next: Java Operators