Sintaks Utama #
Java adalah bahasa yang sangat terstruktur — setiap baris kode harus berada dalam konteks yang jelas: di dalam kelas, di dalam metode, dengan tipe data yang eksplisit. Bagi developer yang datang dari Python atau JavaScript, ketatnya aturan sintaks Java terasa berlebihan di awal, tetapi justru itulah yang membuat kode Java mudah dibaca dan diprediksi di skala besar. Artikel ini membahas fondasi sintaks Java yang perlu kamu pahami sebelum menulis program apapun: bagaimana kode dikompilasi dan dijalankan, struktur file yang wajib dipenuhi, sistem package dan import, modifier akses, serta konvensi penamaan yang digunakan seluruh ekosistem Java.
Bagaimana Java Mengompilasi dan Menjalankan Kode #
Sebelum melihat sintaks, penting memahami alur dari source code hingga program berjalan. Java menggunakan pendekatan dua tahap yang membedakannya dari bahasa compiled seperti C++ maupun interpreted seperti 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["Kode Mesin\n(Native)"]
D --> E([Output Program])
style A fill:#f59e0b,color:#000
style B fill:#3b82f6,color:#fff
style C fill:#7c3aed,color:#fff
style E fill:#16a34a,color:#fffjavac mengompilasi source code .java menjadi bytecode .class — format perantara yang bukan kode mesin native. JVM kemudian menjalankan bytecode ini di platform apapun (Windows, macOS, Linux) tanpa perlu kompilasi ulang. Inilah arti dari slogan Java: “Write once, run anywhere”.
# Kompilasi: source code → bytecode
javac HelloWorld.java # menghasilkan HelloWorld.class
# Jalankan bytecode di JVM
java HelloWorld # perhatikan: tanpa ekstensi .class
Struktur Program Java #
Setiap program Java dimulai dari struktur yang sama. Berikut adalah program lengkap paling sederhana beserta penjelasan tiap bagiannya:
// 1. Deklarasi package — opsional, tapi direkomendasikan
package com.example.app;
// 2. Import kelas dari package lain
import java.util.List;
import java.util.ArrayList;
// 3. Deklarasi kelas — nama file harus sama dengan nama kelas public
public class HelloWorld {
// 4. Entry point — JVM mencari metode ini untuk memulai program
public static void main(String[] args) {
// 5. Statement diakhiri titik koma
System.out.println("Halo, Dunia!");
// 6. Variabel harus dideklarasikan dengan tipe
String pesan = "Java " + Runtime.version().feature();
System.out.println(pesan);
}
}
Aturan yang wajib dipenuhi dan menyebabkan compile error jika dilanggar:
✓ Nama file harus sama persis dengan nama kelas public
→ file: HelloWorld.java, kelas: public class HelloWorld
→ file: UserService.java, kelas: public class UserService
✓ Setiap statement diakhiri titik koma (;)
✓ Semua kode harus berada di dalam kelas
✓ Satu file hanya boleh punya satu kelas public
(boleh punya beberapa kelas non-public)
Deklarasi Kelas #
Kelas adalah unit terkecil organisasi kode di Java. Semua kode — variabel, metode, logika — harus berada di dalam kelas.
// Anatomi deklarasi kelas
[modifier] class NamaKelas [extends KelasInduk] [implements Interface1, Interface2] {
// field (variabel)
// konstruktor
// metode
}
Variasi Deklarasi Kelas #
// Kelas publik — bisa diakses dari package lain
public class Pengguna {
String nama;
int umur;
}
// Kelas yang mewarisi kelas lain
public class Admin extends Pengguna {
String levelAkses;
}
// Kelas yang mengimplementasikan interface
public class EmailService implements NotificationService {
@Override
public void kirim(String pesan) {
// implementasi
}
}
// Kelas final — tidak bisa diwarisi
public final class Konstanta {
public static final double PI = 3.14159;
}
// Kelas abstrak — tidak bisa diinstansiasi langsung
public abstract class Bentuk {
public abstract double hitungLuas(); // metode abstrak
}
classDiagram
class Bentuk {
<<abstract>>
+hitungLuas() double
}
class Lingkaran {
-double jariJari
+hitungLuas() double
}
class Persegi {
-double sisi
+hitungLuas() double
}
class Drawable {
<<interface>>
+gambar() void
}
Bentuk <|-- Lingkaran
Bentuk <|-- Persegi
Drawable <|.. LingkaranMetode main #
Metode main adalah pintu masuk setiap program Java. JVM mencari metode dengan signature tepat ini untuk memulai eksekusi:
public static void main(String[] args) {
// kode program
}
Setiap kata kunci dalam signature ini punya alasan:
public → JVM (dari luar kelas) harus bisa memanggilnya
static → JVM memanggil tanpa membuat objek terlebih dahulu
void → tidak mengembalikan nilai ke JVM
String[] args → menerima argumen dari command line
Membaca Argumen Command Line #
public class Sapa {
public static void main(String[] args) {
// java Sapa Budi 25
if (args.length < 2) {
System.out.println("Penggunaan: java Sapa <nama> <umur>");
return;
}
String nama = args[0]; // "Budi"
int umur = Integer.parseInt(args[1]); // 25
System.out.println("Halo, " + nama + "! Umur kamu " + umur + " tahun.");
}
}
Sejak Java 21, ada fitur Unnamed Main Method dan Instance Main Methods (preview) yang memungkinkan menulis program tanpa boilerplate public static void main. Tapi untuk saat ini, signature standar di atas masih digunakan di hampir semua codebase production.Package dan Import #
Package adalah mekanisme Java untuk mengelompokkan kelas yang saling berkaitan sekaligus menghindari konflik nama. Bayangkan package seperti folder di filesystem.
Deklarasi Package #
// Baris pertama file (sebelum import apapun)
package com.example.ecommerce.service;
// Konvensi penamaan: nama domain dibalik + nama modul
// com.namaPerusahaan.namaAplikasi.namaModul
Import #
// Import kelas spesifik — direkomendasikan
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
// ANTI-PATTERN: wildcard import — mengimpor semua kelas dalam package
import java.util.*; // ✗ tidak jelas kelas mana yang digunakan
// Import static — untuk menggunakan anggota static tanpa nama kelas
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
public class Lingkaran {
double hitungKeliling(double r) {
return 2 * PI * r; // langsung pakai PI, bukan Math.PI
}
double hitungDiagonal(double a, double b) {
return sqrt(a*a + b*b); // langsung pakai sqrt
}
}
Struktur Direktori Sesuai Package #
Nama package harus mencerminkan struktur direktori file:
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
Modifier Akses #
Modifier akses mengontrol dari mana sebuah kelas, metode, atau field bisa diakses. Java punya empat level akses:
public class ContohModifier {
public String namaPublik; // bisa diakses dari mana saja
protected String namaProtected; // package + subclass di luar package
String namaDefault; // hanya dalam package yang sama (tanpa keyword)
private String namaPrivate; // hanya dalam kelas ini
// Metode dengan berbagai modifier
public void metodePubilk() { }
protected void metodeProtected() { }
void metodeDefault() { } // package-private
private void metodePrivate() { }
}
graph TD
subgraph "Jangkauan Akses"
A["private\nHanya kelas ini"]
B["(default)\nKelas dalam package yang sama"]
C["protected\nDefault + subclass di luar package"]
D["public\nSemua kelas di semua package"]
end
A --> B --> C --> D
style A fill:#e05252,color:#fff
style B fill:#f59e0b,color:#000
style C fill:#3b82f6,color:#fff
style D fill:#16a34a,color:#fffPrinsip Enkapsulasi #
Gunakan modifier akses sesempit mungkin — ini adalah prinsip least privilege yang membuat kode lebih aman dan mudah di-refactor:
// ANTI-PATTERN: semua field public — kelas tidak punya kontrol atas datanya
public class Rekening {
public double saldo; // ✗ siapapun bisa ubah langsung
public String nomorRek; // ✗ tidak ada validasi
}
// BENAR: field private, akses via metode yang mengontrol validasi
public class Rekening {
private double saldo;
private String nomorRek;
public Rekening(String nomorRek, double saldoAwal) {
if (saldoAwal < 0) throw new IllegalArgumentException("Saldo tidak boleh negatif");
this.nomorRek = nomorRek;
this.saldo = saldoAwal;
}
public double getSaldo() { return saldo; }
public String getNomorRek() { return nomorRek; }
public void tarik(double jumlah) {
if (jumlah > saldo) throw new IllegalStateException("Saldo tidak mencukupi");
saldo -= jumlah;
}
public void setor(double jumlah) {
if (jumlah <= 0) throw new IllegalArgumentException("Jumlah setor harus positif");
saldo += jumlah;
}
}
Tipe Data Primitif vs Referensi #
Java membedakan dua kategori tipe data yang punya perilaku sangat berbeda:
// Tipe PRIMITIF — disimpan langsung di stack, bukan objek
byte b = 127; // 8-bit, -128 s/d 127
short s = 32767; // 16-bit
int i = 2_147_483_647; // 32-bit (paling umum)
long l = 9_223_372_036L; // 64-bit, tambahkan L di akhir
float f = 3.14f; // 32-bit, tambahkan f di akhir
double d = 3.14159265358979; // 64-bit (paling umum untuk desimal)
char c = 'A'; // 16-bit Unicode
boolean flag = true;
// Tipe REFERENSI — menyimpan alamat memori ke objek di heap
String nama = "Budi"; // objek String
int[] angka = {1, 2, 3}; // array
List<String> list = new ArrayList<>(); // Collection
Perbedaan Kritis: Assignment dan Perbandingan #
// PRIMITIF: assignment menyalin nilai
int a = 10;
int b = a;
b = 20;
System.out.println(a); // 10 — a tidak berubah
// REFERENSI: assignment menyalin alamat (bukan nilai)
int[] arrA = {1, 2, 3};
int[] arrB = arrA; // arrB menunjuk objek yang SAMA
arrB[0] = 99;
System.out.println(arrA[0]); // 99 — arrA ikut berubah!
// Perbandingan referensi
String s1 = new String("halo");
String s2 = new String("halo");
// ANTI-PATTERN: membandingkan referensi dengan ==
if (s1 == s2) { } // ✗ false — alamat berbeda meski isinya sama
// BENAR: bandingkan nilai dengan .equals()
if (s1.equals(s2)) { } // ✓ true
Kesalahan membandingkanStringdengan==alih-alih.equals()adalah salah satu bug paling umum di Java. Operator==pada tipe referensi membandingkan alamat memori, bukan nilai. Selalu gunakan.equals()untuk membandingkan isi objek.
Konvensi Penamaan #
Java memiliki konvensi penamaan yang diikuti hampir universal oleh seluruh ekosistem. Melanggar konvensi ini tidak menyebabkan error, tetapi akan membuat kode kamu terlihat asing bagi developer Java lain:
// KELAS & INTERFACE: PascalCase
public class UserService { }
public class HttpClient { }
public interface Serializable { }
public @interface Override { } // anotasi
// METODE & VARIABEL: camelCase
public void hitungTotalHarga() { }
int jumlahProduk = 0;
String namaLengkap = "";
boolean sudahLogin = false;
// KONSTANTA: UPPER_SNAKE_CASE
public static final int MAX_RETRY_COUNT = 3;
public static final String DEFAULT_CHARSET = "UTF-8";
public static final double BATAS_DISKON = 0.5;
// PACKAGE: semua huruf kecil, tanpa underscore
package com.example.userservice;
package org.apache.commons.lang3;
// GENERIC TYPE PARAMETER: satu huruf kapital
class Box<T> { } // T = Type
class Pair<K, V> { } // K = Key, V = Value
class Repository<E> { } // E = Entity
Statement, Blok, dan Ekspresi #
Pemahaman tentang tiga konsep ini membantu membaca struktur kode Java dengan lebih baik:
public class KonsepDasar {
public static void main(String[] args) {
// STATEMENT: instruksi tunggal, diakhiri titik koma
int x = 10;
System.out.println(x);
x++;
// BLOK: kelompok statement dalam kurung kurawal
{
int y = 20; // y hanya ada di dalam blok ini
System.out.println(x + y);
}
// System.out.println(y); // ERROR: y sudah di luar scope
// EKSPRESI: kombinasi nilai dan operator yang menghasilkan nilai
int hasil = x * 2 + 5; // ekspresi aritmatika
boolean ok = hasil > 10; // ekspresi boolean
String s = "nilai: " + hasil; // ekspresi string concatenation
// Ekspresi bisa berdiri sebagai statement jika punya efek samping
x = x + 1; // assignment
x++; // increment
metode(); // pemanggilan metode
}
static void metode() { }
}
Output ke Konsol #
Java menyediakan beberapa cara untuk mencetak output, masing-masing dengan kegunaan berbeda:
// System.out.println — cetak dengan newline di akhir
System.out.println("Halo"); // "Halo\n"
System.out.println(42); // konversi otomatis ke String
System.out.println(3.14);
System.out.println(true);
// System.out.print — cetak tanpa newline
System.out.print("Nama: ");
System.out.print("Budi");
System.out.println(); // newline manual
// System.out.printf — format seperti C printf
System.out.printf("Nama: %s, Umur: %d%n", "Budi", 25);
System.out.printf("Harga: Rp%.2f%n", 99999.5);
// String.format — format ke String tanpa langsung cetak
String pesan = String.format("ID: %05d, Status: %s", 42, "aktif");
System.out.println(pesan); // "ID: 00042, Status: aktif"
// Format specifier yang umum:
// %s → String
// %d → integer
// %f → float/double
// %.2f → 2 desimal
// %n → newline (platform-independent, lebih baik dari "\n")
// %05d → integer dengan padding nol, lebar 5
Ringkasan #
- Dua tahap eksekusi —
javacmengompilasi.javamenjadi bytecode.class, kemudian JVM menjalankan bytecode; ini yang membuat Java write once, run anywhere.- Nama file = nama kelas — nama file
.javaharus identik dengan nama kelaspublicdi dalamnya; pelanggaran ini langsung menyebabkan compile error.- Package mencerminkan direktori — deklarasi
package com.example.servicemengharuskan file berada di direktoricom/example/service/; konvensi nama domain dibalik mencegah konflik antar library.- Modifier akses sesempit mungkin — gunakan
privateuntuk field dan buka akses hanya via metode yang memvalidasi; ini adalah fondasi enkapsulasi OOP.==vs.equals()—==membandingkan alamat memori untuk tipe referensi; selalu gunakan.equals()untuk membandingkan isi objek sepertiString.- Tipe primitif vs referensi — primitif (
int,double,boolean, dll) disalin saat assignment; referensi menyimpan alamat sehingga dua variabel bisa menunjuk objek yang sama.- Konvensi penamaan — PascalCase untuk kelas/interface, camelCase untuk metode/variabel, UPPER_SNAKE_CASE untuk konstanta; konvensi ini diikuti seluruh ekosistem Java.