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.
Comments
Loading comments...
Leave a Comment