If you have already written code in another language, Java will feel both familiar and unexpectedly strict. Here every variable has its type declared in advance, the program won't run until it compiles, and some of the freedoms you're used to are simply forbidden. Let's go over the basics: how to declare a variable, how int differs from Integer, why strings can't be changed, and what control flow is made of.
A primitive stores the value right inside the variable. A wrapper stores only a reference: autoboxing creates an object on the heap, unboxing pulls the value back — and if there is no reference, unboxing `null` kills the program.
Why So Strict
Java is a statically typed language. This means the type of every variable is known before the program runs, at compile time. The compiler checks that you aren't adding a number to a list or calling a non-existent method on a string. Errors of this kind are caught immediately, not while the program is running in front of a user.
The short formula: in Java, compilation comes first, then execution. Source code (.java) is turned into bytecode (.class), which the JVM virtual machine executes. That's why a typo in a method name isn't a crash in front of a user but a build error right in your editor.
The Entry Point: The main Method
Every Java program starts with the main method. The JVM looks specifically for it and runs it first:
live example
public class App {
public static void main(String[] args) {
System.out.println("Hello, Java"); // print a string to the console
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
The signature word by word:
public— the method is accessible from outside; the JVM has to be able to see it;static— the method belongs to the class, not to an object, so it can be called without creating an instance;void— the method returns nothing;String[] args— an array of command-line arguments.
For now, remember this line as the "magic startup incantation": what class, public, and static actually mean is covered in the article on OOP.
Variables and var
A variable is declared using the pattern "type name = value":
int age = 30;
String name = "Anna";
boolean active = true;
The type on the left is mandatory — the compiler has to know what you intend to store. But Java 10 introduced the var keyword: it asks the compiler to infer the type automatically from the right-hand side.
var age = 30; // the compiler sees a number → int
var name = "Anna"; // sees a string → String
var items = new ArrayList<String>(); // the type is clear from the constructor
Important: var is not an "untyped" variable like in dynamic languages. The type is still locked in; you just don't write it by hand. You can't later assign a string to age. And var only works where the type is obvious from the value: local variables inside methods, the variable of a for loop, a resource in try (var conn = ...), and lambda parameters. In class fields, method parameters, and return types var is not allowed — there the type is what readers of the signature rely on.
Primitives Versus Objects
Java has two worlds of values.
Primitive types are "bare" values that sit directly in memory. There are eight of them, but these are the ones that matter at the start:
int count = 100; // integer, 32 bits
long big = 9_000_000L; // large integer, 64 bits (underscores for readability)
double price = 19.99; // fractional
boolean ok = false; // true / false
char letter = 'A'; // a single character in single quotes
Object (reference) types are everything else: strings, lists, your own classes. The variable holds not the value itself but a reference to an object in memory. Every primitive has an object "twin" — a wrapper: int → Integer, long → Long, boolean → Boolean, double → Double.
Why do wrappers exist? Many parts of the standard library (collections, for example) can only work with objects, not primitives. You can't put an int into a list, but you can put an Integer.
Autoboxing
So you don't have to switch between worlds by hand, Java converts for you — autoboxing (primitive → wrapper) and unboxing (wrapper → primitive). Run this and look at the output:
live example
public class Boxing {
public static void main(String[] args) {
Integer boxed = 5; // autoboxing: int 5 → Integer
int back = boxed; // unboxing: Integer → int
System.out.println("boxed=" + boxed + ", back=" + back);
Integer a = 100, b = 100;
Integer x = 1000, y = 1000;
System.out.println("100 == 100 → " + (a == b));
System.out.println("1000 == 1000 → " + (x == y));
System.out.println("equals → " + x.equals(y));
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Those last two lines are the main trap. For objects == compares references, not values, and with small numbers this is especially sneaky: Java caches and reuses values from −128 to 127, so two hundreds are the very same object and == gives true, while two thousands are different objects and give false. Wrappers must be compared with .equals(); there is more on this in the material on collections.
The second trap: a wrapper can be null, and trying to unbox null into a primitive will crash the program with a NullPointerException. The rule is simple: if a value is definitely present and there is just one — use the primitive int; if the value may be absent or has to go into a collection — use Integer.
Strings and Their Immutability
String is an object, and in Java strings are immutable. Any operation that "changes" a string actually creates a new one, while the old one stays the same. The same example also shows how .equals() differs from ==:
live example
public class Strings {
public static void main(String[] args) {
String greeting = "Hello";
String full = greeting + ", world"; // a NEW string was created
System.out.println(greeting); // "Hello" — unchanged
System.out.println(full);
var sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append(i).append(' '); // we modify the same buffer
}
System.out.println(sb.toString());
String copy = new String("Hello");
System.out.println(copy.equals(greeting) + " — the contents match");
System.out.println((copy == greeting) + " — but the objects differ");
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
Immutability is there for safety and predictability: since a string can't be corrupted, you can freely pass it anywhere and use it as a key. The downside is that if you concatenate thousands of strings with + in a loop, a new object is created every time; that is what StringBuilder is for — it modifies a single buffer.
And since Java 15 there are "text blocks" in triple quotes for multi-line text:
String json = """
{
"name": "Anna"
}
""";
Operators
Java's operators are familiar to almost any programmer:
int sum = 2 + 3; // arithmetic: + - * / %
boolean adult = age >= 18; // comparison: == != < > <= >=
boolean both = active && adult; // logic: && (and), || (or), ! (not)
The logical && and || are "lazy": if the left side of && is already false, Java doesn't even evaluate the right side. This is handy for checks like if (user != null && user.isActive()) — the second condition won't run if user is null.
There's also the ternary operator — a short form for choosing one of two values:
String label = adult ? "adult" : "minor";
Control Flow
Branching — if / else if / else:
if (price > 100) {
System.out.println("expensive");
} else if (price > 50) {
System.out.println("moderate");
} else {
System.out.println("cheap");
}
Loops — for, while, and for-each for iterating over collections:
for (int i = 0; i < 3; i++) { // classic counter
System.out.println(i);
}
int n = 3;
while (n > 0) { // while the condition is true
n--;
}
var names = List.of("Anna", "Boris");
for (String person : names) { // for-each: over each element
System.out.println(person);
}
Selection by value — switch. Modern Java has a short arrow form that returns a result directly and needs no break:
live example
public class Roles {
public static void main(String[] args) {
for (String role : new String[]{"admin", "editor", "guest"}) {
int level = switch (role) {
case "admin" -> 3;
case "editor" -> 2;
default -> 1; // the mandatory "everything else" branch
};
System.out.println(role + " → " + level);
}
}
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
The old form with case ... : and break still works, but the arrow switch is shorter and doesn't let you accidentally "fall through" into the next branch.
In Short
- Java is a statically typed language: the type is known at compile time, and errors are caught before the program runs.
- Every program starts with the
public static void main(String[] args)method. vardoesn't cancel typing — it merely infers the type automatically for local variables.- Primitives (
int,double,boolean) store the value directly; object wrappers (Integer,Double) store a reference, can benull, and are needed for collections. Converting between them is autoboxing. - Wrappers and strings are compared with
.equals():==compares references, and above 127 you see the difference immediately. - Strings are immutable: a "change" creates a new object, and heavy concatenation is what
StringBuilderis for. - Control flow is standard:
if/else,for,while,for-each, and the modern arrowswitch.
What to Read Next
- OOP in Java: classes, objects, inheritance — what actually lies behind
class,public, andstatic. - Java Collections — where
Integerwrappers and string immutability show up in practice. - Records, Optional, and modern Java — where
var,switch, and text blocks grew into. - The Java Developer's Tools — how to build and run the code we wrote here.