Data Types #

Java is a statically typed language — every variable, parameter, and return value must have an explicitly declared type, and the compiler verifies consistency before the program runs. This differs from Python or JavaScript, which determine types at runtime. Java divides data types into two broad categories: primitives (values stored directly on the stack) and references (variables store an address pointing to an object on the heap). Understanding this difference — especially its implications for assignment, comparison, and performance — is the foundation for writing correct, efficient Java code.

Primitive Data Types #

Java has exactly eight primitive types defined in the language specification that can’t be changed. Every other type in Java is a reference type (object).

TypeSizeValue RangeDefault ValueLiteral
byte8-bit-128 to 1270byte b = 42;
short16-bit-32,768 to 32,7670short s = 1000;
int32-bit±2,147,483,6470int i = 100;
long64-bit±9,223,372,036,854,775,8070Llong l = 100L;
float32-bit±3.4×10³⁸, ~7 digit precision0.0ffloat f = 3.14f;
double64-bit±1.7×10³⁰⁸, ~15 digit precision0.0ddouble d = 3.14;
char16-bit‘\u0000’ to ‘\uffff’ (Unicode)'\u0000'char c = 'A';
boolean1-bit*true or falsefalseboolean ok = true;

*The actual size of boolean in the JVM depends on the implementation, usually 1 byte.

flowchart TD
    A[Java Data Types] --> B["Primitive\nvalues stored on the stack"]
    A --> C["Reference\nobject address on the heap"]
    B --> D["Integers\nbyte short int long"]
    B --> E["Decimals\nfloat double"]
    B --> F["Characters\nchar"]
    B --> G["Logic\nboolean"]
    C --> H[String]
    C --> I[Array]
    C --> J["Objects / Classes"]
    C --> K[Interface]

    style B color:#fff,stroke:#3b82f6,stroke-width:2px
    style C color:#fff,stroke:#7c3aed,stroke-width:2px

Integers #

int is the default choice for integers — used for almost every general need. Use long only when values exceed int’s limit, and byte/short only for large arrays where memory savings are significant.

// int — most common, enough for most needs
int userCount = 1_000_000; // underscores as thousand separators (Java 7+)
int year          = 2025;

// long — for values above ~2 billion, add L at the end of the literal
long worldPopulation  = 8_100_000_000L; // L is required, otherwise a compile error
long timestampMs    = System.currentTimeMillis();

// byte and short — rarely used, except for large binary data arrays
byte[] imageData = new byte[1920 * 1080 * 3]; // saves memory for large buffers

Integer overflow happens silently — no exception, the result wraps around. This is a source of hard-to-find bugs:

int max = Integer.MAX_VALUE; // 2,147,483,647
int result = max + 1;
System.out.println(result);    // -2,147,483,648 — wraps to negative!

// For operations that might overflow, use long or Math.addExact()
long safe = (long) max + 1;  // 2,147,483,648 — correct
Math.addExact(max, 1);       // throws ArithmeticException on overflow

Decimals #

double is the default choice for decimal numbers. float is only used for large arrays that need memory savings, or when interacting with APIs that specifically require float (like OpenGL).

double temperature     = 36.6;       // default — no suffix needed
double pi       = Math.PI;    // 3.141592653589793
float  coordinate = 120.5f;    // f is required, otherwise it's treated as double

// Ordinary decimal operations
double area = Math.PI * 5.0 * 5.0;
System.out.println(area); // 78.53981633974483

Don’t use double for financial calculations (money). double uses a binary representation that can’t represent decimals like 0.1 exactly:

System.out.println(0.1 + 0.2);         // 0.30000000000000004 — not 0.3!
System.out.println(1.03 - 0.42);        // 0.6099999999999999 — not 0.61!

double price = 19.99;
double total = price * 3;
System.out.println(total);              // 59.97 — happens to be fine
System.out.println(price * 3 == 59.97); // false — not safe for comparison

Use BigDecimal for all calculations involving money. This is covered in more detail in its own section.

char and boolean #

char stores a single 16-bit Unicode character. Because Java uses Unicode natively, char can store characters from almost any language in the world.

char letter    = 'A';
char digit    = '5';       // the character '5', not the int 5
char unicode  = '\u00e9';  // 'é' — an accented character
char newline  = '\n';      // escape character

// char can be used in arithmetic (its Unicode value)
char next = (char) ('A' + 1); // 'B'
System.out.println((int) 'A');   // 65 — the Unicode value

boolean active    = true;
boolean alreadyPaid = false;
boolean result    = (5 > 3) && (10 != 20); // true

Main Reference Types #

Reference types are all types other than the eight primitives. A reference-typed variable stores the address of an object on the heap, not the object’s value directly.

String #

String is the most frequently used reference type in Java. Although it looks like a primitive because it can be created without new, String is an object of the java.lang.String class.

// Two ways to create a String
String s1 = "hello";              // string literal — from the String pool
String s2 = new String("hello");  // a new object on the heap — avoid unless needed

// String is IMMUTABLE — every "modification" produces a new object
String name = "Budi";
name.toUpperCase();               // doesn't change 'name'!
String upperName = name.toUpperCase(); // ✓ store the result

// Common String operations
String s = "Hello, Java!";
s.length();          // 12
s.toUpperCase();     // "HELLO, JAVA!"
s.substring(7, 11);  // "Java"
s.contains("Java");  // true
s.replace("Java", "World"); // "Hello, World!"
s.split(", ");       // ["Hello", "Java!"]
s.trim();            // removes whitespace at the start/end
s.isEmpty();         // false
s.startsWith("He");  // true

The String Pool and Comparison #

Java has a String pool — a special memory area where identical string literals share a single object to save memory.

flowchart LR
    subgraph Stack
        A["s1"]
        B["s2"]
        C["s3"]
    end
    subgraph "Heap — String Pool"
        D["\"hello\""]
    end
    subgraph "Heap — Outside Pool"
        E["\"hello\" new object"]
    end

    A -->|"reference"| D
    B -->|"reference"| D
    C -->|"reference"| E

    style D color:#fff,stroke:#16a34a,stroke-width:2px
    style E color:#fff,stroke:#e05252,stroke-width:2px
String s1 = "hello";
String s2 = "hello";              // s1 and s2 point to the SAME object from the pool
String s3 = new String("hello");  // a NEW object outside the pool

// ANTI-PATTERN: comparing Strings with ==
if (s1 == s2) { }     // true — happens to work because of the pool, but not reliable
if (s1 == s3) { }     // false — different objects even though the contents match

// CORRECT: always use .equals() to compare String contents
if (s1.equals(s2)) { }     // ✓ true
if (s1.equals(s3)) { }     // ✓ true
if ("hello".equals(s1)) { } // ✓ "Yoda condition" pattern — safe if s1 is null

StringBuilder for Repeated Concatenation #

// ANTI-PATTERN: String concatenation in a loop — creates many temporary objects
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i + ", "; // ✗ every += creates a new String object
}

// CORRECT: use StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i).append(", "); // ✓ in-place modification
}
String result = sb.toString();

Wrapper Classes #

Every primitive type has a wrapper class that wraps it into an object. Wrappers are needed when an API requires a reference type — like List<Integer> (you can’t have List<int>).

PrimitiveWrapper Class
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean
// Wrappers provide useful utility methods
int max = Integer.MAX_VALUE;      // 2,147,483,647
int from  = Integer.parseInt("42"); // String → int conversion
String to = Integer.toString(42);   // int → String conversion
int min   = Integer.min(10, 20);    // 10

// Type checks
Character.isLetter('A');  // true
Character.isDigit('5');   // true
Character.isWhitespace(' '); // true
Character.toUpperCase('a');  // 'A'

Autoboxing and Unboxing #

Java automatically converts between primitives and wrappers — called autoboxing (primitive → wrapper) and unboxing (wrapper → primitive).

// Autoboxing — happens automatically
List<Integer> list = new ArrayList<>();
list.add(42);        // int 42 is automatically wrapped into Integer(42)

Integer x = 100;       // autoboxing from int to Integer

// Unboxing — happens automatically
int value = list.get(0); // Integer is automatically unwrapped to int
int y = x + 1;             // x is unboxed to int before the + operation

Unboxing from null causes NullPointerException — a trap that often goes unnoticed:

Integer wrapper = null;
int primitive = wrapper; // ✗ NullPointerException when unboxing null!

// Always check for null before unboxing
if (wrapper != null) {
    int primitive = wrapper; // ✓ safe
}

// Or use Objects.requireNonNullElse
int primitive = Objects.requireNonNullElse(wrapper, 0);

Also, == comparison on wrappers can be misleading because Integer only caches values from -128 to 127:

Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — from the cache

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false — different objects, outside the cache!
System.out.println(c.equals(d)); // true — use equals()

Type Casting #

Type casting is an explicit conversion from one type to another. Java distinguishes two kinds of casting based on whether information can be lost.

Widening (Implicit) — Automatic, Safe #

Conversion from a smaller type to a larger one happens automatically without data loss:

byte  b = 42;
short s = b;      // byte → short, automatic
int   i = s;      // short → int, automatic
long  l = i;      // int → long, automatic
float f = l;      // long → float, automatic (precision may decrease)
double d = f;     // float → double, automatic
flowchart LR
    A["byte\n8-bit"] --> B["short\n16-bit"] --> C["int\n32-bit"] --> D["long\n64-bit"] --> E["float\n32-bit"] --> F["double\n64-bit"]
    style A color:#fff,stroke:#e05252,stroke-width:2px
    style F color:#fff,stroke:#16a34a,stroke-width:2px

Narrowing (Explicit) — Manual, Can Lose Data #

Conversion from a larger type to a smaller one must be written explicitly — the compiler forces you to be aware that data may be lost:

double d = 9.99;
int    i = (int) d;     // 9 — the decimal part is truncated (not rounded!)
long   l = 123456789L;
byte   b = (byte) l;    // only the low 8 bits are taken, resulting in -128 to 127

// ANTI-PATTERN: forgetting that narrowing can truncate values
int big = 300;
byte small = (byte) big; // 44 — not 300! the value wraps around

// CORRECT: check the range before narrowing if the value is uncertain
if (big >= Byte.MIN_VALUE && big <= Byte.MAX_VALUE) {
    byte small = (byte) big; // safe
}

Casting Reference Types #

// Upcasting — automatic, always safe
Object obj = "this is a String"; // String is a subclass of Object

// Downcasting — must be explicit, can fail
String s = (String) obj;          // ✓ safe because obj really is a String

Object number = Integer.valueOf(42);
// String x = (String) number;    // ✗ ClassCastException at runtime!

// CORRECT: use instanceof before downcasting
if (obj instanceof String str) {  // pattern matching instanceof (Java 16+)
    System.out.println(str.toUpperCase());
}

BigDecimal for Financial Calculations #

For all calculations involving money, taxes, or decimal values that must be exact, use BigDecimal — not double.

import java.math.BigDecimal;
import java.math.RoundingMode;

// ANTI-PATTERN: double for money
double price1 = 19.99;
double price2 = 29.99;
System.out.println(price1 + price2); // 49.980000000000004 — not exact!

// CORRECT: BigDecimal for money
BigDecimal p1 = new BigDecimal("19.99"); // use String, not double!
BigDecimal p2 = new BigDecimal("29.99");
BigDecimal total = p1.add(p2);
System.out.println(total); // 49.98 — exact

// BigDecimal operations
BigDecimal price = new BigDecimal("100.00");
BigDecimal tax = new BigDecimal("0.11");

BigDecimal taxAmount = price.multiply(tax);       // 11.0000
BigDecimal totalDue = price.add(taxAmount);       // 111.0000

// Rounding — always specify a RoundingMode explicitly
BigDecimal rounded = taxAmount.setScale(2, RoundingMode.HALF_UP); // 11.00

// BigDecimal comparison
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
a.equals(b);      // false — equals considers the scale
a.compareTo(b);   // 0 — compareTo only compares numeric values

Don’t create a BigDecimal from a double literal — you inherit double’s imprecision into the BigDecimal:

// ANTI-PATTERN: BigDecimal from a double literal
new BigDecimal(0.1);  // ✗ "0.1000000000000000055511151231257827021181583404541015625"

// CORRECT: BigDecimal from a String
new BigDecimal("0.1"); // ✓ exactly 0.1

Choosing the Right Type #

flowchart TD
    A{"What type of data\ndo you need?"} --> B[Integer]
    A --> C[Decimal]
    A --> D[Text]
    A --> E["True/false logic"]
    A --> F[Single character]

    B --> B1{Value > 2 billion?}
    B1 -- Yes --> B2["long"]
    B1 -- No --> B3["int (default)"]

    C --> C1{"For money\nor exact calculations?"}
    C1 -- Yes --> C2["BigDecimal"]
    C1 -- No --> C3["double (default)"]

    D --> D1{"Lots of\nconcatenation in a loop?"}
    D1 -- Yes --> D2["StringBuilder"]
    D1 -- No --> D3["String"]

    E --> E1["boolean"]
    F --> F1["char"]

    style B2 color:#fff,stroke:#3b82f6,stroke-width:2px
    style B3 color:#fff,stroke:#16a34a,stroke-width:2px
    style C2 color:#fff,stroke:#e05252,stroke-width:2px
    style C3 color:#fff,stroke:#16a34a,stroke-width:2px
    style D2 color:#fff,stroke:#3b82f6,stroke-width:2px
    style D3 color:#fff,stroke:#16a34a,stroke-width:2px

Summary #

  • Eight primitive typesbyte, short, int, long, float, double, char, boolean; use int for integers and double for decimals as defaults.
  • long needs the L suffix, float needs f100L is a long, 3.14f is a float; without a suffix, integer literals are int and decimal literals are double.
  • Integer overflow happens silentlyInteger.MAX_VALUE + 1 becomes negative without an exception; use Math.addExact() or long if there’s an overflow risk.
  • double isn’t suitable for money0.1 + 0.2 ≠ 0.3 because of the binary representation; use BigDecimal with the String constructor for all financial calculations.
  • String is immutable, StringBuilder is mutable — every operation on String creates a new object; use StringBuilder for concatenation in loops.
  • == on Strings/wrappers isn’t reliable — always use .equals() to compare contents; == only compares memory addresses.
  • Autoboxing nullNullPointerException — unboxing a null wrapper into a primitive immediately throws NPE; check for null or use a default value before unboxing.
  • Narrowing casts truncate, not round(int) 9.99 gives 9, not 10; and values can wrap around if they exceed the target type’s range.

← Previous: Constants   Next: Operators →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact