In Java, every variable has a declared type enforced at compile time — 8 primitive types (int, double, boolean, char, byte, short, long, float) stored by value, and reference types (all objects) stored as heap pointers, with autoboxing bridging primitives and their wrapper class equivalents.
Java is statically typed — every variable has a declared type that the compiler enforces. There is no type coercion. No surprises at runtime about what a variable holds.
Primitive types
Java has 8 primitive types — not objects. They are stored directly (on the stack for local variables) and compared by value. Everything else is a reference type (an object on the heap).
| Type | Size | Default | Range / Notes |
|---|---|---|---|
byte | 8 bit | 0 | −128 to 127 |
short | 16 bit | 0 | −32,768 to 32,767 |
int | 32 bit | 0 | −2,147,483,648 to 2,147,483,647 |
long | 64 bit | 0L | ±9.2 × 10¹⁸. Suffix L required: 9876543210L |
float | 32 bit | 0.0f | IEEE 754 single. Suffix f: 3.14f |
double | 64 bit | 0.0d | IEEE 754 double. Default for decimals |
char | 16 bit | '\u0000' | Single Unicode code unit. Single quotes |
boolean | 1 bit | false | Only true or false. No int conversion |
// Declare type explicitly — compiler enforces it
int age = 28;
double salary = 85_000.50; // underscores for readability (Java 7+)
boolean active = true;
char grade = 'A';
long population = 1_400_000_000L;
// var — local type inference (Java 10+)
var name = "Priya"; // inferred as String
var items = new ArrayList<String>(); // inferred as ArrayList<String>
// var is only allowed for local variables — not fields, parameters, return types
// Literals
int hex = 0xFF; // 255
int bin = 0b1010; // 10
int oct = 0777; // deprecated style, avoid
// Widening — automatic, safe
int i = 42;
long l = i; // int widens to long automatically
double d = i; // int widens to double
// Narrowing — explicit cast required (potential data loss)
double pi = 3.14159;
int floor = (int) pi; // 3 — truncates, does NOT roundString and wrapper types
// String — immutable, reference type, but gets special treatment
String s1 = "Hello"; // string pool (preferred)
String s2 = new String("Hello"); // new heap object (avoid)
// CRITICAL: == compares references, not values
System.out.println(s1 == s2); // false — different objects
System.out.println(s1.equals(s2)); // true — same content
// String is immutable — "changing" it creates a new object
String s = "Hello";
s = s + " World"; // new String object created; old "Hello" eligible for GC
// StringBuilder for building strings in a loop (mutable, fast)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i).append(", ");
}
String result = sb.toString(); // "0, 1, 2, 3, 4, "
// Text blocks (Java 13+)
String json = """
{
"name": "Priya",
"age": 28
}
""";
// Wrapper types — int → Integer, double → Double, etc.
Integer boxed = 42; // autoboxing: int → Integer
int unboxed = boxed; // unboxing: Integer → int
// Autoboxing pitfall
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true — cached range (−128 to 127)
Integer x = 1000;
Integer y = 1000;
System.out.println(x == y); // false — outside cache range!
System.out.println(x.equals(y)); // true — always use equals()Type casting and instanceof
// Pattern matching instanceof (Java 16+)
Object obj = "Hello, Java!";
// Old style
if (obj instanceof String) {
String s = (String) obj; // explicit cast needed
System.out.println(s.length());
}
// Pattern matching — cleaner
if (obj instanceof String s) {
System.out.println(s.length()); // s is already cast
}
// Switch expressions (Java 14+) with pattern matching (Java 21)
String result = switch (obj) {
case Integer i -> "Integer: " + i;
case String s -> "String: " + s.toUpperCase();
case null -> "null";
default -> "Other: " + obj.getClass().getSimpleName();
};
// Records (Java 16+) — compact immutable data carriers
record Point(double x, double y) {
// Compact canonical constructor
Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("Negative coordinates");
}
double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
}
Point p = new Point(3, 4);
System.out.println(p.x()); // 3.0
System.out.println(p.distanceFromOrigin()); // 5.0
System.out.println(p); // Point[x=3.0, y=4.0] — auto toStringfinal, static, and constants
// final variable — cannot be reassigned after initialisation
final int MAX_SIZE = 100;
// MAX_SIZE = 200; // compile error
// static — belongs to the class, not instances
class MathUtils {
public static final double PI = 3.14159265358979;
public static int add(int a, int b) { return a + b; }
}
MathUtils.add(2, 3); // call without creating an object
double pi = MathUtils.PI;
// Sealed classes (Java 17+) — restrict which classes can extend
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Exhaustive switch on sealed type — compiler verifies all cases covered
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
}; // no default needed — sealed== compares references for objects, not values. For String and any object, == checks if both variables point to the same object in memory. "hello" == "hello" may be true due to string pooling, but new String("hello") == new String("hello") is false. Always use .equals() for objects.Integer a = 127; Integer b = 127; a == b is true because JVM caches these. Integer a = 1000; Integer b = 1000; a == b is false — different objects. This is a real source of bugs. Always use .equals() for Integer comparison.The Java Memory Model and value semantics
The Java Language Specification §17 defines the Java Memory Model (JMM). For local variables, primitives are stored on the thread stack — each thread has its own copy. Objects are allocated on the heap and shared across threads via references. The JMM defines a happens-before relation — if action A happens-before action B, then all effects of A are visible to B. Without explicit synchronisation, there is no happens-before between threads, and the JVM and CPU are free to reorder operations. This is why unsynchronised access to shared mutable state produces data races.
String interning and the string pool
String literals are automatically interned — stored in the JVM's string pool (part of the heap since Java 7; previously PermGen). All string literals with the same content share the same object. String.intern() explicitly interns a string. This is why "hello" == "hello" is usually true (same interned object) but new String("hello") == new String("hello") is false (two new heap objects). The JLS §3.10.5 specifies that string literals of the same compile-time constant value share the same String object.