Master Java's complete type system including all eight primitive types, their wrapper class counterparts, widening and narrowing conversions, autoboxing pitfalls, and variable scoping rules.
Type systems are the first line of defense against bugs. Java is statically typed, meaning the type of every variable is known at compile time, and the compiler rejects type mismatches before the code ever runs. This prevents entire categories of runtime errors that are common in dynamically-typed languages like Python or JavaScript.
Java provides two categories of types: primitives (int, long, double, float, boolean, char, byte, short) that store values directly in memory, and reference types (objects, arrays, strings) that store a memory address pointing to heap data. Understanding this distinction explains why int x = 5 copies the value 5, while List<String> a = b makes both variables point to the same list.
In this lesson you will learn every primitive type's size and range, when to choose one over another, how Java automatically converts between compatible types, and when you must explicitly cast. You will also learn about the wrapper class ecosystem that enables primitives to work with generics and the Collections framework.
Java has exactly eight primitive types. Memorizing their sizes prevents overflow bugs in financial, scientific, and embedded applications.
1// Integer types
2byte b = 127; // 8-bit signed: -128 to 127
3short s = 32767; // 16-bit signed: -32,768 to 32,767
4int i = 2_147_483_647; // 32-bit signed: ~±2.1 billion (most common)
5long l = 9_223_372_036_854_775_807L; // 64-bit; note L suffix
6
7// Floating-point types
8float f = 3.14f; // 32-bit IEEE 754; note f suffix
9double d = 3.141592653589793; // 64-bit IEEE 754 (default for decimals)
10
11// Character and boolean
12char c = 'A'; // 16-bit Unicode code unit: 0 to 65,535
13boolean flag = true; // true or false only; NOT 0/1
14
15// Underscores in numeric literals (Java 7+) improve readability
16int million = 1_000_000;
17long billion = 1_000_000_000L;
18double pi = 3.141_592_653_589_793;Choosing the right type:
int for counting and indexing (default integer type)long when values can exceed ~2 billion (timestamps, file sizes, IDs)double for floating-point math (default decimal type)float unless memory is severely constrained — it has only ~7 significant digitsboolean for flags and conditions — never use int 0/1 as Java does not treat them as booleanchar only when representing a single Unicode character; prefer String for textInteger overflow is silent:
1int max = Integer.MAX_VALUE; // 2,147,483,647
2int overflow = max + 1; // wraps to -2,147,483,648 — no exception!
3System.out.println(overflow); // -2147483648
4
5// Fix: use long
6long safe = (long) max + 1; // 2,147,483,648Every primitive has a corresponding wrapper class in java.lang. Wrappers are needed when a primitive must be treated as an object (e.g., stored in a List<Integer>).
1// Wrapper classes
2Integer intObj = Integer.valueOf(42); // explicit boxing
3Double dblObj = Double.valueOf(3.14);
4Boolean boolObj = Boolean.valueOf(true);
5
6// Autoboxing: compiler inserts Integer.valueOf() automatically
7Integer auto = 42; // equivalent to Integer.valueOf(42)
8
9// Unboxing: compiler inserts .intValue() automatically
10int unboxed = auto; // equivalent to auto.intValue()
11
12// Useful wrapper class methods
13int parsed = Integer.parseInt("123"); // String → int
14String str = Integer.toString(123); // int → String
15int binary = Integer.parseInt("1010", 2); // binary string → int (10)
16String hex = Integer.toHexString(255); // "ff"
17int max = Integer.MAX_VALUE; // 2147483647
18int clamped = Math.min(value, Integer.MAX_VALUE);Autoboxing pitfalls — the NullPointerException trap:
1Integer cached = null;
2int primitive = cached; // NullPointerException at runtime!
3
4// Always null-check before unboxing
5if (cached != null) {
6 int safe = cached;
7}
8
9// Optional.ofNullable is cleaner in modern Java
10int value = Optional.ofNullable(cached).orElse(0);Integer cache (-128 to 127):
1Integer a = 127;
2Integer b = 127;
3System.out.println(a == b); // true — same cached instance
4
5Integer c = 128;
6Integer d = 128;
7System.out.println(c == d); // false — different instances
8System.out.println(c.equals(d)); // true — always use equals() for wrappersThe JVM caches Integer instances for -128 to 127 for performance. This is why == on Integer objects gives surprising results outside that range. **Always use .equals() to compare wrapper objects.**
Java's type conversion rules are strict: the compiler only allows automatic conversion when there is no risk of data loss (widening). Conversions that could lose data require an explicit cast (narrowing).
Widening conversions (automatic):
1byte b = 100;
2short s = b; // byte → short (automatic)
3int i = s; // short → int (automatic)
4long l = i; // int → long (automatic)
5float f = l; // long → float (automatic, but may lose precision!)
6double d = f; // float → double (automatic)
7
8// Widening hierarchy:
9// byte → short → int → long → float → double
10// char ↗Narrowing conversions (explicit cast required):
1double pi = 3.14159;
2int truncated = (int) pi; // cast: 3 (decimal part discarded)
3byte b = (byte) 300; // cast: 44 (overflow wraps around: 300 - 256)
4char c = (char) 65; // cast: 'A' (int code point to char)
5
6// Numeric string parsing
7int fromString = Integer.parseInt("42");
8long fromLong = Long.parseLong("9876543210");
9double fromDbl = Double.parseDouble("3.14");
10
11// Safe narrowing with range check
12public int toIntSafe(long value) {
13 if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
14 throw new ArithmeticException("Value out of int range: " + value);
15 }
16 return (int) value;
17}Implicit int promotion in expressions:
1byte a = 10;
2byte b = 20;
3// byte c = a + b; // COMPILE ERROR: a+b promotes to int
4byte c = (byte)(a + b); // explicit cast required
5int d = a + b; // fine: int result stored in intIn any arithmetic expression, operands smaller than int (byte, short, char) are automatically promoted to int before the operation. This prevents overflow in intermediate calculations but requires a cast if you assign back to a smaller type.
Java has three variable scopes, and understanding them prevents "variable not found" compiler errors and unintended state sharing.
1public class ScopeDemo {
2 // Static variable: belongs to the class, shared across all instances
3 static int instanceCount = 0;
4
5 // Instance variable: belongs to each object, lives on the heap
6 private String name;
7 private int age;
8
9 public ScopeDemo(String name, int age) {
10 // 'this' disambiguates instance variable from parameter
11 this.name = name;
12 this.age = age;
13 instanceCount++; // shared counter
14 }
15
16 public void demonstrateLocalScope() {
17 // Local variable: exists only within this method
18 int localValue = 42;
19 System.out.println(localValue); // OK
20
21 if (localValue > 0) {
22 int blockScoped = 100; // exists only in this if-block
23 System.out.println(blockScoped); // OK
24 }
25 // System.out.println(blockScoped); // COMPILE ERROR: out of scope
26
27 for (int i = 0; i < 3; i++) {
28 // 'i' exists only in the for-loop header and body
29 System.out.println(i);
30 }
31 // System.out.println(i); // COMPILE ERROR
32 }
33}Local Variable Type Inference with var (Java 10+):
1// var infers the type from the right-hand side at compile time
2var name = "Alice"; // String
3var count = 42; // int
4var list = new ArrayList<String>(); // ArrayList<String>
5var entries = Map.of("a", 1, "b", 2); // Map<String, Integer>
6
7// var in for-each loops
8for (var entry : entries.entrySet()) {
9 System.out.printf("%s = %d%n", entry.getKey(), entry.getValue());
10}
11
12// Limitations of var:
13// var x; // COMPILE ERROR: needs initializer
14// var x = null; // COMPILE ERROR: cannot infer type from null
15// private var name; // COMPILE ERROR: only local variables
16// var[] arr = ...; // COMPILE ERROR: cannot use var for arraysUse var when the type is obvious from the right-hand side (e.g., new HashMap<>()). Avoid var when the type adds readability (e.g., var result = process() is ambiguous — what does process() return?).
Java uses final to declare constants and prevent reassignment. Understanding the difference between value immutability and object immutability is crucial.
1// Constants: final + static + ALL_CAPS by convention
2public class MathConstants {
3 public static final double PI = 3.141592653589793;
4 public static final int MAX_INT = Integer.MAX_VALUE;
5 public static final String APP_NAME = "MyApp";
6}
7
8// Final local variable: assigned once, cannot be reassigned
9final int maxRetries = 3;
10// maxRetries = 5; // COMPILE ERROR
11
12// Final reference: the reference cannot change, but the object can!
13final List<String> items = new ArrayList<>();
14items.add("first"); // OK: modifying the object
15items.add("second"); // OK
16// items = new ArrayList<>(); // COMPILE ERROR: cannot reassign final reference
17
18// String immutability
19String s1 = "hello";
20String s2 = s1.toUpperCase(); // returns NEW string; s1 unchanged
21System.out.println(s1); // "hello"
22System.out.println(s2); // "HELLO"
23
24// String pool: literals are interned
25String a = "hello";
26String b = "hello";
27System.out.println(a == b); // true — same pool reference
28System.out.println(a == new String("hello")); // false — new object
29System.out.println(a.equals(new String("hello"))); // true — content equal
30
31// Always use .equals() for String comparison
32if (userInput.equals("admin")) { ... } // correct
33if ("admin".equals(userInput)) { ... } // null-safe: won't NPE
34if (Objects.equals(userInput, "admin")) { ... } // best: fully null-safeA complete program that demonstrates all primitive types, wrapper classes, conversions, autoboxing edge cases, and variable scope in one runnable example.
1import java.util.ArrayList;
2import java.util.List;
3import java.util.Optional;
4
5/**
6 * TypeSystemDemo.java
7 * Demonstrates Java's complete type system with safe and unsafe patterns.
8 */
9public class TypeSystemDemo {
10
11 // Class constant
12 public static final long MAX_SAFE_FILE_SIZE = 5L * 1024 * 1024 * 1024; // 5 GB
13
14 // Instance variable
15 private final String label;
16
17 public TypeSystemDemo(String label) {
18 this.label = label;
19 }
20
21 public static void main(String[] args) {
22 primitiveRanges();
23 wrapperClassDemo();
24 typeConversionDemo();
25 autoboxingPitfalls();
26 varKeywordDemo();
27 }
28
29 static void primitiveRanges() {
30 System.out.println("=== Primitive Type Ranges ===");
31 System.out.printf("byte: %d to %d%n", Byte.MIN_VALUE, Byte.MAX_VALUE);
32 System.out.printf("short: %d to %d%n", Short.MIN_VALUE, Short.MAX_VALUE);
33 System.out.printf("int: %d to %d%n", Integer.MIN_VALUE, Integer.MAX_VALUE);
34 System.out.printf("long: %d to %d%n", Long.MIN_VALUE, Long.MAX_VALUE);
35 System.out.printf("float: %.2e to %.2e%n", Float.MIN_VALUE, Float.MAX_VALUE);
36 System.out.printf("double: %.2e to %.2e%n", Double.MIN_VALUE, Double.MAX_VALUE);
37
38 // Integer overflow demonstration
39 int max = Integer.MAX_VALUE;
40 System.out.printf("%nInteger.MAX_VALUE + 1 = %d (overflow!)%n", max + 1);
41 System.out.printf("(long) Integer.MAX_VALUE + 1 = %d (correct)%n", (long) max + 1);
42 }
43
44 static void wrapperClassDemo() {
45 System.out.println("\n=== Wrapper Classes ===");
46
47 // Parsing strings
48 int parsed = Integer.parseInt("42");
49 long big = Long.parseLong("9876543210");
50 double pi = Double.parseDouble("3.14159");
51 boolean flag = Boolean.parseBoolean("true");
52
53 System.out.printf("parseInt('42') = %d%n", parsed);
54 System.out.printf("parseLong('9876...') = %d%n", big);
55 System.out.printf("parseDouble('3.14...')= %.5f%n", pi);
56 System.out.printf("parseBoolean('true') = %b%n", flag);
57
58 // Radix conversions
59 System.out.printf("%nDecimal 255 in hex : %s%n", Integer.toHexString(255));
60 System.out.printf("Decimal 255 in binary : %s%n", Integer.toBinaryString(255));
61 System.out.printf("Decimal 255 in octal : %s%n", Integer.toOctalString(255));
62 System.out.printf("Binary '11111111' : %d%n", Integer.parseInt("11111111", 2));
63 }
64
65 static void typeConversionDemo() {
66 System.out.println("\n=== Type Conversions ===");
67
68 // Widening (automatic)
69 int i = 1_000_000;
70 long l = i; // widening: no cast needed
71 double d = l; // widening: no cast needed
72 System.out.printf("int %d → long %d → double %.1f%n", i, l, d);
73
74 // Narrowing (requires explicit cast)
75 double pi = 3.14159;
76 int truncated = (int) pi;
77 System.out.printf("%ndouble %.5f → int %d (truncated, not rounded)%n",
78 pi, truncated);
79
80 // Rounding properly
81 int rounded = (int) Math.round(pi);
82 System.out.printf("Properly rounded: %d%n", rounded);
83
84 // Byte overflow
85 int big = 300;
86 byte overflow = (byte) big;
87 System.out.printf("%nint 300 cast to byte: %d (300 - 256 = 44)%n", overflow);
88
89 // char ↔ int
90 char letter = 'A';
91 int code = letter; // widening: char → int
92 char back = (char)(code + 1); // narrowing: int → char
93 System.out.printf("%nchar 'A' = int %d; int %d = char '%c'%n",
94 code, code + 1, back);
95 }
96
97 static void autoboxingPitfalls() {
98 System.out.println("\n=== Autoboxing Pitfalls ===");
99
100 // Integer cache: -128 to 127
101 Integer a = 127, b = 127;
102 Integer c = 128, d = 128;
103 System.out.println("127 == 127 (cache): " + (a == b)); // true
104 System.out.println("128 == 128 (no cache): " + (c == d)); // false
105 System.out.println("128 equals 128: " + c.equals(d)); // true
106
107 // NullPointerException from unboxing null
108 Integer nullInt = null;
109 try {
110 int value = nullInt; // NullPointerException here!
111 } catch (NullPointerException e) {
112 System.out.println("NullPointerException caught: unboxing null Integer");
113 }
114
115 // Safe unboxing with Optional
116 int safe = Optional.ofNullable(nullInt).orElse(0);
117 System.out.println("Safe default for null Integer: " + safe);
118
119 // Performance: avoid autoboxing in tight loops
120 long start = System.nanoTime();
121 Integer sum1 = 0;
122 for (int i = 0; i < 100_000; i++) sum1 += i; // autoboxing each iteration
123 long autoboxTime = System.nanoTime() - start;
124
125 start = System.nanoTime();
126 int sum2 = 0;
127 for (int i = 0; i < 100_000; i++) sum2 += i; // pure primitive
128 long primitiveTime = System.nanoTime() - start;
129
130 System.out.printf("%nAutoboxing loop: %d µs%n", autoboxTime / 1000);
131 System.out.printf("Primitive loop: %d µs (faster!)%n", primitiveTime / 1000);
132 }
133
134 static void varKeywordDemo() {
135 System.out.println("\n=== var (Local Type Inference) ===");
136
137 var name = "Alice"; // inferred: String
138 var count = 42; // inferred: int
139 var ratio = 3.14; // inferred: double
140 var list = new ArrayList<String>(); // inferred: ArrayList<String>
141
142 list.add("Java");
143 list.add("is");
144 list.add("powerful");
145
146 for (var item : list) { // inferred: String
147 System.out.println(item);
148 }
149
150 System.out.printf("%nvar name → %s (type: %s)%n",
151 name, name.getClass().getSimpleName());
152 System.out.printf("var count → %d%n", count);
153 System.out.printf("var ratio → %.2f%n", ratio);
154 }
155}This program covers the entire Java type system in a runnable format: primitive ranges via MIN_VALUE/MAX_VALUE constants, wrapper class parsing and radix conversions, widening and narrowing casts with overflow demonstration, the notorious Integer cache gotcha that catches many developers off guard, the NullPointerException from unboxing null, a performance comparison of autoboxing versus primitive arithmetic, and the var keyword for type inference. Run it and observe that the autoboxing loop is measurably slower — this matters in data-processing code.
Now that you understand Java's type system, the next lesson covers operators and control flow: arithmetic, relational, logical, and bitwise operators, then if-else, switch expressions (Java 14+), for loops, while loops, and the enhanced for-each loop. These are the building blocks for all program logic.
Your progress is saved in this browser, on this device only.