Core Syntax #
Java is a highly structured language — every line of code must live in a clear context: inside a class, inside a method, with an explicit type. For developers coming from Python or JavaScript, Java’s strict syntax rules feel excessive at first, but that strictness is exactly what makes Java code easy to read and predictable at scale. This article covers the Java syntax foundations you need before writing any program: how code is compiled and run, the file structure you must follow, the package and import system, access modifiers, and the naming conventions used across the entire Java ecosystem.
How Java Compiles and Runs Code #
Before looking at syntax, it’s important to understand the path from source code to a running program. Java uses a two-stage approach that sets it apart from both compiled languages like C++ and interpreted languages like Python:
flowchart LR
A["HelloWorld.java\n(Source Code)"] -->|"javac"| B["HelloWorld.class\n(Bytecode)"]
B -->|"java"| C["JVM\n(Java Virtual Machine)"]
C -->|"JIT Compilation"| D["Machine Code\n(Native)"]
D --> E("[Program Output]")
style A color:#000,stroke:#f59e0b,stroke-width:2px
style B color:#fff,stroke:#3b82f6,stroke-width:2px
style C color:#fff,stroke:#7c3aed,stroke-width:2px
style E color:#fff,stroke:#16a34a,stroke-width:2pxjavac compiles .java source code into .class bytecode — an intermediate format that isn’t native machine code. The JVM then runs this bytecode on any platform (Windows, macOS, Linux) without recompiling. That’s the meaning behind Java’s slogan: “Write once, run anywhere”.
# Compile: source code → bytecode
javac HelloWorld.java # produces HelloWorld.class
# Run the bytecode on the JVM
java HelloWorld # note: no .class extension
Java Program Structure #
Every Java program starts from the same structure. Here’s the simplest complete program with an explanation of each part:
// 1. Package declaration — optional, but recommended
package com.example.app;
// 2. Import classes from other packages
import java.util.List;
import java.util.ArrayList;
// 3. Class declaration — the file name must match the public class name
public class HelloWorld {
// 4. Entry point — the JVM looks for this method to start the program
public static void main(String[] args) {
// 5. Statements end with a semicolon
System.out.println("Hello, World!");
// 6. Variables must be declared with a type
String message = "Java " + Runtime.version().feature();
System.out.println(message);
}
}
Rules that are mandatory and cause compile errors when violated:
✓ The file name must exactly match the public class name
→ file: HelloWorld.java, class: public class HelloWorld
→ file: UserService.java, class: public class UserService
✓ Every statement ends with a semicolon (;)
✓ All code must be inside a class
✓ One file can have only one public class
(it may have several non-public classes)
Class Declarations #
The class is the smallest unit of code organization in Java. All code — variables, methods, logic — must be inside a class.
// Anatomy of a class declaration
[modifier] class ClassName [extends ParentClass] [implements Interface1, Interface2] {
// fields (variables)
// constructors
// methods
}
Class Declaration Variations #
// Public class — accessible from other packages
public class User {
String name;
int age;
}
// Class that inherits from another class
public class Admin extends User {
String accessLevel;
}
// Class that implements an interface
public class EmailService implements NotificationService {
@Override
public void send(String message) {
// implementation
}
}
// Final class — cannot be inherited
public final class Constants {
public static final double PI = 3.14159;
}
// Abstract class — cannot be instantiated directly
public abstract class Shape {
public abstract double calculateArea(); // abstract method
}
classDiagram
class Shape {
<<abstract>>
+calculateArea() double
}
class Circle {
-double radius
+calculateArea() double
}
class Square {
-double side
+calculateArea() double
}
class Drawable {
<<interface>>
+draw() void
}
Shape <|-- Circle
Shape <|-- Square
Drawable <|.. CircleThe main Method #
The main method is the entry point of every Java program. The JVM looks for a method with this exact signature to start execution:
public static void main(String[] args) {
// program code
}
Every keyword in this signature has a reason:
public → the JVM (from outside the class) must be able to call it
static → the JVM calls it without creating an object first
void → returns no value to the JVM
String[] args → receives arguments from the command line
Reading Command Line Arguments #
public class Greet {
public static void main(String[] args) {
// java Greet Budi 25
if (args.length < 2) {
System.out.println("Usage: java Greet <name> <age>");
return;
}
String name = args[0]; // "Budi"
int age = Integer.parseInt(args[1]); // 25
System.out.println("Hello, " + name + "! You are " + age + " years old.");
}
}
Since Java 21, there’s the Unnamed Main Method and Instance Main Methods (preview) features that let you write programs without the public static void main boilerplate. For now, though, the standard signature above is still used in almost every production codebase.Packages and Imports #
Packages are Java’s mechanism for grouping related classes while avoiding name conflicts. Think of a package like a folder in the filesystem.
Package Declaration #
// First line of the file (before any imports)
package com.example.ecommerce.service;
// Naming convention: reversed domain name + module name
// com.companyName.applicationName.moduleName
Imports #
// Import a specific class — recommended
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
// ANTI-PATTERN: wildcard import — imports every class in the package
import java.util.*; // ✗ unclear which classes are actually used
// Static import — use static members without the class name
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
public class Circle {
double calculateCircumference(double r) {
return 2 * PI * r; // use PI directly, not Math.PI
}
double calculateDiagonal(double a, double b) {
return sqrt(a*a + b*b); // use sqrt directly
}
}
Directory Structure Matching Packages #
The package name must mirror the file’s directory structure:
src/
└── com/
└── example/
└── ecommerce/
├── service/
│ ├── UserService.java → package com.example.ecommerce.service
│ └── OrderService.java → package com.example.ecommerce.service
├── model/
│ ├── User.java → package com.example.ecommerce.model
│ └── Order.java → package com.example.ecommerce.model
└── repository/
└── UserRepository.java → package com.example.ecommerce.repository
Access Modifiers #
Access modifiers control where a class, method, or field can be accessed from. Java has four access levels:
public class ModifierExample {
public String publicName; // accessible from anywhere
protected String protectedName; // package + subclasses outside the package
String defaultName; // same package only (no keyword)
private String privateName; // this class only
// Methods with various modifiers
public void publicMethod() { }
protected void protectedMethod() { }
void defaultMethod() { } // package-private
private void privateMethod() { }
}
flowchart TD
subgraph "Access Scope"
A["private\nThis class only"]
B["(default)\nClasses in the same package"]
C["protected\nDefault + subclasses outside the package"]
D["public\nAll classes in all packages"]
end
A --> B --> C --> D
style A color:#fff,stroke:#e05252,stroke-width:2px
style B color:#000,stroke:#f59e0b,stroke-width:2px
style C color:#fff,stroke:#3b82f6,stroke-width:2px
style D color:#fff,stroke:#16a34a,stroke-width:2pxThe Encapsulation Principle #
Use the narrowest access modifier possible — this is the least privilege principle that makes code safer and easier to refactor:
// ANTI-PATTERN: all fields public — the class has no control over its data
public class Account {
public double balance; // ✗ anyone can change it directly
public String accountNumber; // ✗ no validation
}
// CORRECT: private fields, access via methods that control validation
public class Account {
private double balance;
private String accountNumber;
public Account(String accountNumber, double initialBalance) {
if (initialBalance < 0) throw new IllegalArgumentException("Balance cannot be negative");
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public double getBalance() { return balance; }
public String getAccountNumber() { return accountNumber; }
public void withdraw(double amount) {
if (amount > balance) throw new IllegalStateException("Insufficient balance");
balance -= amount;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit amount must be positive");
balance += amount;
}
}
Primitive vs Reference Types #
Java distinguishes two categories of types with very different behavior:
// PRIMITIVE types — stored directly on the stack, not objects
byte b = 127; // 8-bit, -128 to 127
short s = 32767; // 16-bit
int i = 2_147_483_647; // 32-bit (most common)
long l = 9_223_372_036L; // 64-bit, add L at the end
float f = 3.14f; // 32-bit, add f at the end
double d = 3.14159265358979; // 64-bit (most common for decimals)
char c = 'A'; // 16-bit Unicode
boolean flag = true;
// REFERENCE types — store a memory address pointing to an object on the heap
String name = "Budi"; // String object
int[] numbers = {1, 2, 3}; // array
List<String> list = new ArrayList<>(); // Collection
Critical Differences: Assignment and Comparison #
// PRIMITIVE: assignment copies the value
int a = 10;
int b = a;
b = 20;
System.out.println(a); // 10 — a is unchanged
// REFERENCE: assignment copies the address (not the value)
int[] arrA = {1, 2, 3};
int[] arrB = arrA; // arrB points to the SAME object
arrB[0] = 99;
System.out.println(arrA[0]); // 99 — arrA changed too!
// Reference comparison
String s1 = new String("hello");
String s2 = new String("hello");
// ANTI-PATTERN: comparing references with ==
if (s1 == s2) { } // ✗ false — different addresses even though contents match
// CORRECT: compare values with .equals()
if (s1.equals(s2)) { } // ✓ true
ComparingStringwith==instead of.equals()is one of the most common bugs in Java. The==operator on reference types compares memory addresses, not values. Always use.equals()to compare object contents.
Naming Conventions #
Java has naming conventions followed by nearly the entire ecosystem. Breaking them doesn’t cause errors, but it makes your code look foreign to other Java developers:
// CLASSES & INTERFACES: PascalCase
public class UserService { }
public class HttpClient { }
public interface Serializable { }
public @interface Override { } // annotation
// METHODS & VARIABLES: camelCase
public void calculateTotalPrice() { }
int productCount = 0;
String fullName = "";
boolean isLoggedIn = false;
// CONSTANTS: UPPER_SNAKE_CASE
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_CHARSET = "UTF-8";
public static final double DISCOUNT_LIMIT = 0.5;
// PACKAGES: all lowercase, no underscores
package com.example.userservice;
package org.apache.commons.lang3;
// GENERIC TYPE PARAMETERS: single capital letter
class Box<T> { } // T = Type
class Pair<K, V> { } // K = Key, V = Value
class Repository<E> { } // E = Entity
Statements, Blocks, and Expressions #
Understanding these three concepts helps you read Java code structure better:
public class BasicConcepts {
public static void main(String[] args) {
// STATEMENT: a single instruction, ending with a semicolon
int x = 10;
System.out.println(x);
x++;
// BLOCK: a group of statements inside curly braces
{
int y = 20; // y only exists inside this block
System.out.println(x + y);
}
// System.out.println(y); // ERROR: y is out of scope
// EXPRESSION: a combination of values and operators that produces a value
int result = x * 2 + 5; // arithmetic expression
boolean ok = result > 10; // boolean expression
String s = "value: " + result; // string concatenation expression
// An expression can stand as a statement if it has side effects
x = x + 1; // assignment
x++; // increment
method(); // method call
}
static void method() { }
}
Console Output #
Java provides several ways to print output, each with a different use:
// System.out.println — prints with a newline at the end
System.out.println("Hello"); // "Hello\n"
System.out.println(42); // automatic conversion to String
System.out.println(3.14);
System.out.println(true);
// System.out.print — prints without a newline
System.out.print("Name: ");
System.out.print("Budi");
System.out.println(); // manual newline
// System.out.printf — formatted like C printf
System.out.printf("Name: %s, Age: %d%n", "Budi", 25);
System.out.printf("Price: $%.2f%n", 99999.5);
// String.format — format into a String without printing directly
String message = String.format("ID: %05d, Status: %s", 42, "active");
System.out.println(message); // "ID: 00042, Status: active"
// Common format specifiers:
// %s → String
// %d → integer
// %f → float/double
// %.2f → 2 decimals
// %n → newline (platform-independent, better than "\n")
// %05d → integer with zero padding, width 5
Summary #
- Two-stage execution —
javaccompiles.javainto.classbytecode, then the JVM runs the bytecode; this is what makes Java write once, run anywhere.- File name = class name — the
.javafile name must be identical to thepublicclass inside it; violating this causes an immediate compile error.- Packages mirror directories — declaring
package com.example.servicerequires the file to live incom/example/service/; the reversed domain naming convention prevents conflicts between libraries.- Keep access modifiers as narrow as possible — use
privatefor fields and open access only through validating methods; this is the foundation of OOP encapsulation.==vs.equals()—==compares memory addresses for reference types; always use.equals()to compare the contents of objects likeString.- Primitive vs reference types — primitives (
int,double,boolean, etc.) are copied on assignment; references store an address so two variables can point to the same object.- Naming conventions — PascalCase for classes/interfaces, camelCase for methods/variables, UPPER_SNAKE_CASE for constants; the whole Java ecosystem follows these.