Beginner

Variables and Types in Java

Learn about variables, data types, and type conversions in Java with practical Docker-ready examples

Java is a statically typed language, which means every variable must be declared with a specific type before it can be used. The compiler enforces these types at compile time, catching type errors before your program ever runs. This is one of Java’s defining characteristics and a key reason it’s trusted for large-scale enterprise systems.

As an object-oriented, class-based language, Java draws a clear distinction between primitive types (like int and double) and reference types (like String and arrays). Primitives live directly on the stack and hold their values, while reference types hold pointers to objects on the heap. Understanding this distinction is fundamental to writing effective Java code.

In this tutorial, you’ll learn how to declare variables, work with Java’s eight primitive types and common reference types, perform type conversions, and use constants. All examples are runnable with Docker using the same Eclipse Temurin JDK image from the Hello World tutorial.

Primitive Types and Variable Declaration

Java has eight primitive types that form the foundation of its type system. Each has a fixed size in memory, making Java’s behavior predictable across platforms.

Create a file named Variables.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class Variables {
    public static void main(String[] args) {
        // Integer types
        byte smallNumber = 127;              // 8-bit, range: -128 to 127
        short mediumNumber = 32767;          // 16-bit, range: -32768 to 32767
        int wholeNumber = 42;                // 32-bit, most common integer type
        long bigNumber = 9_000_000_000L;     // 64-bit, note the L suffix

        // Floating-point types
        float decimal = 3.14f;               // 32-bit, note the f suffix
        double preciseDecimal = 3.14159265;  // 64-bit, default for decimal literals

        // Character and boolean
        char letter = 'A';                   // 16-bit Unicode character
        boolean isActive = true;             // true or false

        // String (reference type, but used like a primitive)
        String greeting = "Hello, Java!";

        // Print all variables
        System.out.println("=== Primitive Types ===");
        System.out.println("byte: " + smallNumber);
        System.out.println("short: " + mediumNumber);
        System.out.println("int: " + wholeNumber);
        System.out.println("long: " + bigNumber);
        System.out.println("float: " + decimal);
        System.out.println("double: " + preciseDecimal);
        System.out.println("char: " + letter);
        System.out.println("boolean: " + isActive);
        System.out.println("String: " + greeting);

        // Numeric underscores for readability (Java 7+)
        int million = 1_000_000;
        long creditCardNumber = 1234_5678_9012_3456L;
        System.out.println("\n=== Readable Numeric Literals ===");
        System.out.println("million: " + million);
        System.out.println("credit card: " + creditCardNumber);
    }
}

Type Conversions and Casting

Java’s strong type system controls how values move between types. Widening conversions (smaller to larger type) happen automatically, but narrowing conversions (larger to smaller) require an explicit cast — the compiler forces you to acknowledge the potential data loss.

Create a file named TypeConversions.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TypeConversions {
    public static void main(String[] args) {
        // Widening conversion (implicit) - no data loss
        int myInt = 100;
        long myLong = myInt;         // int -> long (automatic)
        double myDouble = myLong;    // long -> double (automatic)

        System.out.println("=== Widening (Implicit) ===");
        System.out.println("int: " + myInt);
        System.out.println("int -> long: " + myLong);
        System.out.println("long -> double: " + myDouble);

        // Narrowing conversion (explicit cast required)
        double pi = 3.14159;
        int truncated = (int) pi;    // Truncates decimal part
        byte small = (byte) 300;     // Overflows: 300 % 256 = 44

        System.out.println("\n=== Narrowing (Explicit Cast) ===");
        System.out.println("double pi: " + pi);
        System.out.println("(int) pi: " + truncated);
        System.out.println("(byte) 300: " + small);

        // String conversions
        String numberStr = "42";
        int parsed = Integer.parseInt(numberStr);
        double parsedDouble = Double.parseDouble("3.14");
        String backToString = String.valueOf(parsed);

        System.out.println("\n=== String Conversions ===");
        System.out.println("String -> int: " + parsed);
        System.out.println("String -> double: " + parsedDouble);
        System.out.println("int -> String: " + backToString);

        // Autoboxing: primitive <-> wrapper object
        int primitiveInt = 5;
        Integer wrappedInt = primitiveInt;    // Autoboxing
        int unwrapped = wrappedInt;           // Unboxing

        System.out.println("\n=== Autoboxing ===");
        System.out.println("primitive: " + primitiveInt);
        System.out.println("wrapped: " + wrappedInt);
        System.out.println("unwrapped: " + unwrapped);
    }
}

Constants, Var, and Null Handling

Java uses the final keyword to declare constants — values that cannot be reassigned after initialization. Since Java 10, var enables local variable type inference, letting the compiler determine the type from the assigned value. And since every reference type can be null, understanding null handling is essential.

Create a file named ConstantsAndVar.java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ConstantsAndVar {
    // Class-level constants (convention: UPPER_SNAKE_CASE)
    static final double PI = 3.14159265358979;
    static final int MAX_RETRIES = 3;
    static final String APP_NAME = "CodeArchaeology";

    public static void main(String[] args) {
        // Constants
        System.out.println("=== Constants (final) ===");
        System.out.println("PI: " + PI);
        System.out.println("MAX_RETRIES: " + MAX_RETRIES);
        System.out.println("APP_NAME: " + APP_NAME);

        // Local variable type inference with var (Java 10+)
        var count = 10;              // Inferred as int
        var message = "Hello";       // Inferred as String
        var ratio = 3.14;            // Inferred as double
        var active = true;           // Inferred as boolean

        System.out.println("\n=== Type Inference (var) ===");
        System.out.println("count: " + count + " (type: int)");
        System.out.println("message: " + message + " (type: String)");
        System.out.println("ratio: " + ratio + " (type: double)");
        System.out.println("active: " + active + " (type: boolean)");

        // Null handling
        String name = null;
        System.out.println("\n=== Null Handling ===");
        System.out.println("name is null: " + (name == null));

        // Safe null check before using a reference
        if (name != null) {
            System.out.println("name length: " + name.length());
        } else {
            System.out.println("name is not set, skipping length check");
        }

        // Assigning a value
        name = "Java";
        System.out.println("name after assignment: " + name);
        System.out.println("name length: " + name.length());
    }
}

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official Eclipse Temurin JDK image
docker pull eclipse-temurin:21-jdk

# Run the primitive types example
docker run --rm -v $(pwd):/app -w /app eclipse-temurin:21-jdk \
    sh -c "javac Variables.java && java Variables"

# Run the type conversions example
docker run --rm -v $(pwd):/app -w /app eclipse-temurin:21-jdk \
    sh -c "javac TypeConversions.java && java TypeConversions"

# Run the constants and var example
docker run --rm -v $(pwd):/app -w /app eclipse-temurin:21-jdk \
    sh -c "javac ConstantsAndVar.java && java ConstantsAndVar"

Expected Output

=== Primitive Types ===
byte: 127
short: 32767
int: 42
long: 9000000000
float: 3.14
double: 3.14159265
char: A
boolean: true
String: Hello, Java!

=== Readable Numeric Literals ===
million: 1000000
credit card: 1234567890123456

=== Widening (Implicit) ===
int: 100
int -> long: 100
long -> double: 100.0

=== Narrowing (Explicit Cast) ===
double pi: 3.14159
(int) pi: 3
(byte) 300: 44

=== String Conversions ===
String -> int: 42
String -> double: 3.14
int -> String: 42

=== Autoboxing ===
primitive: 5
wrapped: 5
unwrapped: 5

=== Constants (final) ===
PI: 3.14159265358979
MAX_RETRIES: 3
APP_NAME: CodeArchaeology

=== Type Inference (var) ===
count: 10 (type: int)
message: Hello (type: String)
ratio: 3.14 (type: double)
active: true (type: boolean)

=== Null Handling ===
name is null: true
name is not set, skipping length check
name after assignment: Java
name length: 4

Key Concepts

  • Eight primitive types: Java provides byte, short, int, long, float, double, char, and boolean — each with a fixed size and guaranteed behavior across platforms.
  • Static typing: Every variable must be declared with a type. The compiler catches type mismatches before the program runs.
  • Widening vs narrowing: Conversions from smaller to larger types are automatic; larger to smaller requires an explicit cast and may lose data.
  • Autoboxing: Java automatically converts between primitives (int) and their wrapper objects (Integer) when needed, bridging the primitive/object divide.
  • final for constants: The final keyword prevents reassignment. Class-level constants are conventionally named in UPPER_SNAKE_CASE.
  • var for type inference: Since Java 10, local variables can use var to let the compiler infer the type from the right-hand side.
  • Null safety: Reference types can be null. Always check for null before calling methods on a reference to avoid NullPointerException.
  • Numeric literals: Underscores in numeric literals (e.g., 1_000_000) improve readability without affecting the value.

Running Today

All examples can be run using Docker:

docker pull eclipse-temurin:21-jdk
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining