A Java file starts with a class. The compiler enforces this absolutely—no class, no program. When you type public class HelloWorld { }, you're creating a blueprint the JVM uses to spin up objects later. The public keyword makes the class visible everywhere, and the name must match the filename character for character, case included. Get this wrong and compilation fails immediately. It's the first error most beginners hit, and it teaches a lesson about Java's pickiness before you've even run a line of code.
The program starts running inside the main method, and its signature is non-negotiable: public static void main(String[] args). Each word pulls its weight. public lets the JVM reach in from outside. static attaches the method to the class itself, so the JVM doesn't need to create an object first. void tells the caller there's no return value. String[] args grabs whatever the user typed on the command line. Drop any of these pieces and the JVM spits out Main method not found and quits.
Variables and Data Types: Storing Information
Java demands you declare a type for every variable before you use it. The compiler catches type mismatches early, often saving you from runtime disasters. The language gives you eight primitives: byte, short, int, long, float, double, boolean, and char. For everyday integer work, int is the default—it fits a range of roughly ±2.1 billion into 32 bits. Need bigger numbers? A long stretches to 64 bits, but you must tag the literal with an L: long population = 7_800_000_000L;. Those underscores between digits are pure decoration; the compiler ignores them.
Decimal numbers default to double, which gives you about 15 significant digits. If you want a float instead, slap an f on the end: float price = 19.99f;. The boolean type stores exactly true or false—no numeric stand-ins allowed, unlike C or Python. A char is a 16-bit Unicode character, so it handles everything from Latin letters to emoji.
Then there are reference types—String, arrays, objects. These don't hold raw values; they hold addresses on the heap. A String looks like a primitive but isn't. String greeting = "Hello"; creates an object of java.lang.String. Strings are immutable. Concatenate two of them and you get a brand-new object; the originals sit untouched in memory. In a tight loop, that habit creates a trail of discarded objects. A StringBuilder is the fix, but you need to recognize the problem first.
Operators and Expressions
Arithmetic works the way you'd expect—until it doesn't. Integer division drops the remainder silently: 7 / 2 is 3, not 3.5. To keep the fraction, cast one operand: (double) 7 / 2. The modulo operator % hands back the remainder, handy for checking even/odd or wrapping around array indices.
Comparison operators (==, !=, <, >, <=, >=) spit out booleans. The trap everyone falls into: comparing strings with ==. That checks whether two references point to the exact same object, not whether the text matches. Use .equals(): if (str1.equals(str2)). Logical operators && and || short-circuit. The right side never runs if the left side already determines the answer. This is a feature you can lean on: if (obj != null && obj.getValue() > 0) avoids a NullPointerException because the second condition is skipped when obj is null.
Assignment has shorthand forms: +=, -=, *=, and friends. The increment and decrement operators ++ and -- come in two flavors that trip up the unwary. Prefix (++x) increments first, then uses the value. Postfix (x++) uses the current value, then increments. int x = 5; int y = ++x; leaves both at 6. int x = 5; int y = x++; sets y to 5 and x to 6. The difference matters whenever the expression is part of something larger.

Control Flow: Making Decisions and Repeating Actions
if-else branches on a boolean condition. Braces are technically optional for a single statement, but skipping them invites the dangling-else problem and makes later edits error-prone. Just use braces every time. The switch statement handles multiple exact matches and works with int, char, String, and enums. Each case needs a break unless you genuinely want fall-through. A default label catches anything that slipped past.
Java offers three loop styles. The classic for loop packs initialization, condition, and update into one line. The enhanced for-each loop cleans up array and collection iteration: for (int num : numbers) { }. You lose the index, but you also lose off-by-one errors. while checks the condition at the top; do-while checks at the bottom, so the body runs at least once no matter what.
break kills a loop on the spot. continue jumps to the next iteration. Both accept labels, which let you control nested loops from the inside. A labeled break can bail out of an outer loop when a search finds its target, avoiding the clumsy flag variables you'd need otherwise.
Methods: Organizing Code into Reusable Blocks
A method wraps a task in a named block: modifiers returnType methodName(parameters) { body }. Java passes everything by value. For primitives, the method gets a copy of the number. For objects, it gets a copy of the reference—so you can change the object's fields, but you can't make the caller's reference point somewhere else.
Overloading lets you reuse a method name with different parameter lists. The compiler tells them apart by the number, types, and order of parameters. Return type alone isn't enough. So public int add(int a, int b) and public double add(double a, double b) coexist peacefully. It keeps APIs cleaner: one name, multiple type signatures.
Setting up a development environment often involves a virtual machine to sandbox your tools. A clean, isolated workspace lets you experiment without trashing your main OS. The same instinct for compartmentalization shows up in security work—knowing how to wall off projects is a habit that pays dividends when you start thinking about attack surfaces and containment.
Arrays: Storing Multiple Values of the Same Type
An array holds a fixed-size sequence of same-typed values. Declare it with square brackets: int[] scores; or int scores[];. The first style keeps the type together, and most Java codebases prefer it. Indices start at zero. The last element lives at length - 1. Step outside that range and the JVM throws an ArrayIndexOutOfBoundsException.
You can initialize an array inline: int[] primes = {2, 3, 5, 7, 11};. The length property tells you how many elements it holds—it's a final field, not a method call. Multidimensional arrays are just arrays of arrays: int[][] matrix = new int[3][4];. The inner arrays don't have to match in size, so you can create jagged structures.
Assigning an array with = copies the reference, not the data. Both variables now point to the same heap object. For an actual copy, reach for System.arraycopy() or Arrays.copyOf(). The java.util.Arrays class also packs utilities for sorting, searching, filling, and comparing—battle-tested implementations that beat hand-rolled loops on both correctness and speed.

Basic Input and Output
Console output has three variants. System.out.println() appends a newline. System.out.print() doesn't. System.out.printf() formats with specifiers: %d for integers, %f for floats, %s for strings, %n for a platform-aware line break. A typical line: System.out.printf("Name: %s, Age: %d%n", name, age);.
Reading input means bringing in java.util.Scanner. Wire it to System.in: Scanner scanner = new Scanner(System.in);. Methods like nextInt(), nextDouble(), and nextLine() pull tokens from the input stream. The classic headache: mixing nextInt() with nextLine(). The number method leaves a trailing newline in the buffer, and the next nextLine() swallows it as an empty string. The cure is an extra nextLine() right after the number to flush the buffer.
Close the Scanner when you're finished to release resources. For small programs reading from System.in, people often skip this—closing the scanner also shuts the underlying stream, which may not be what you want in a longer-lived application.
Compiling and Running Your First Program
Source code lives in .java files. The javac compiler turns them into bytecode inside .class files. To compile HelloWorld.java, open a terminal in the file's directory and type javac HelloWorld.java. No errors? A HelloWorld.class file appears. Run it with java HelloWorld—drop the .class extension. The java command loads the bytecode, finds main, and starts executing.
Compiler errors mention a line number, but the real mistake often sits on the line above. A missing semicolon on line 5 produces an error flagged at line 6. Train yourself to scan upward from the reported line. IDEs catch these in real time, but the command-line workflow still matters. Remote servers, Docker containers, and minimal Linux setups don't run IntelliJ. Knowing how to read raw javac output keeps you productive when the GUI isn't an option.
Once the syntax becomes second nature, the next layer is handling what goes wrong at runtime. Our article on Mastering Java: Effective Exception Handling picks up where solid syntax leaves off—because even perfectly compiled code hits problems you can't see until the program actually runs.
