Java Data Types

Java Data Types

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
}



Two Categories of Data Types

Java data types fall into two broad categories:

  1. Primitive Data Types – These are the basic building blocks provided by the language itself. There are exactly 8 primitive types: byte, short, int, long, float, double, boolean, and char. They hold their values directly in memory and are not objects.
  2. Non-Primitive (Reference) Data Types – These are types created from classes. They hold a reference (memory address) to an object rather than the value itself. Examples include 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.



Primitive Data Types

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.



Integer Types: byte, short, int, long

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.

byte

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
}

short

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
}

int

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
}

long

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
}



Floating-Point Types: float and double

These types store decimal (fractional) numbers. The key difference between them is precision — how many decimal digits they can accurately represent.

float

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
}

double

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)
}



boolean

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
}



char

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 (Reference) Data Types

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

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

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
}

Classes and Objects

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
}



Key Differences: Primitive vs Non-Primitive

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

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 (Automatic)

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:

byteshortintlongfloatdouble

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 (Manual)

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:

doublefloatlongintshortbyte

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
}



Wrapper Classes

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)
}



Autoboxing and Unboxing

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.

  • Autoboxing: Automatic conversion from a primitive to its wrapper class (intInteger)
  • Unboxing: Automatic conversion from a wrapper class to its primitive (Integerint)
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
}



When to Use Which Data Type

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.



Common Mistakes to Avoid

Even experienced developers make these mistakes. Understanding them early will save you hours of debugging.

1. Integer Overflow

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
}

2. Floating-Point Precision Errors

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
}

3. Comparing Wrapper Objects with ==

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
}

4. NullPointerException with Unboxing

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;
    }
}

5. Forgetting Suffixes for long and float

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
}



Summary

  • Java has 8 primitive types: byte, short, int, long, float, double, boolean, and char.
  • Non-primitive (reference) types include String, arrays, and any class you define. They hold references to objects, not the values themselves.
  • Use int as your default for whole numbers and double as your default for decimals.
  • Widening casting (small to large) happens automatically; narrowing casting (large to small) must be explicit and risks data loss.
  • Every primitive has a wrapper class (Integer, Double, etc.) for use in collections and APIs that require objects.
  • Autoboxing/unboxing converts between primitives and wrappers automatically, but beware of null values causing NullPointerException.
  • Watch out for integer overflow, floating-point precision issues, and reference comparison pitfalls with wrapper classes.

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




Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *