Command Palette

Search for a command to run...

[Java Basics] Java Clean Code Best Practices for Beginners

javac will accept a class named shoppingCart, a method sixty lines long, a number like 0.08 written out in four different places, and a for loop nested five deep. All of it compiles, all of it runs, and none of it produces a single warning. The compiler's job is to reject programs that are not legal Java, and it does that job well. Everything past that point is aimed at people.

That is what "clean code" means, and it is why the advice in this article can feel unfalsifiable compared to the rest of the course. So every guideline below is anchored to something you can check: a real JDK class that follows the convention, a real warning javac prints, or a refactor applied in stages where the program is run after each stage and the output does not change.

Two panels of code shape: the left indented into a deep arrow, the right flat, with the same amount of code

Every warning, error and line of output below was produced by compiling and running the code on OpenJDK 21.0.6. Nothing here is a performance recommendation, and nothing here was timed.

What clean code means when the compiler does not care

Here is a class that breaks every Java naming convention there is.

class shoppingCart {
    int TOTAL_ITEMS = 0;

    void Add_Item() {
        TOTAL_ITEMS++;
    }

    public static void main(String[] args) {
        shoppingCart c = new shoppingCart();
        c.Add_Item();
        c.Add_Item();
        System.out.println("items = " + c.TOTAL_ITEMS);
    }
}

Compiled with every warning the compiler has, it says nothing at all:

javac -Xlint:all shoppingCart.java
java shoppingCart
items = 2

No errors, no warnings, correct output. The conventions are not enforced by anything mechanical, which means following them is a decision you make on purpose, for a reader. Usually that reader is you, three months from now, holding a bug report and no memory of writing this.

Three things are worth wanting from code, and they are all about that reader:

PropertyThe question it answers
Predictable namesCan I tell what this holds without looking it up?
One home per ruleIf the tax rate changes, how many places do I edit?
Short unitsHow much do I have to hold in my head to follow one method?

Nothing in this article is a rule the language enforces. Everything in it is a habit that makes those three answers better.

Java naming conventions, and why the JDK is the reference

The Java naming conventions are not somebody's preference. They are what the standard library has followed from its first release, so every Java programmer already reads code that way — and your code is read against that expectation whether you meant it to be or not.

KindConventionReal JDK example
Class, interface, enumUpperCamelCase, a nounArrayList, StringBuilder, Comparable
MethodlowerCamelCase, a verb phraseparseInt, isEmpty, toUpperCase
Variable, fieldlowerCamelCase, a nounsize, elementData, modCount
ConstantUPPER_SNAKE_CASEInteger.MAX_VALUE, Math.PI
Packageall lowercase, dotsjava.util, java.lang, java.time

Those are not invented for the table. Integer.MAX_VALUE really is 2147483647, Math.PI really is 3.141592653589793, and StringBuilder really lives in java.lang — you have been reading names in this shape all course.

Two identical classes side by side, one ignoring the naming conventions and one following the JDK, with javac reporting the same clean result for both

What the convention buys, given that javac does not care

The shape of a name carries its category before you read a single character of it. Order is a type. order is a value. ORDER_LIMIT is a constant that will not move. When a codebase keeps that promise, four things become free:

  • shoppingCart at the top of a file makes you stop and check whether it is a class or a variable. ShoppingCart does not.
  • TOTAL_ITEMS that changes on every add is actively lying — the shouting case says "this is fixed" and the code says otherwise.
  • Your IDE's completion, search and refactoring all lean on the same expectation.
  • Add_Item compiles, but no Java programmer will guess that name when looking for it. addItem is the name they will type.

The naming rule that matters more than the casing is length. A name should be as long as it needs to be to answer the question. int d tells you nothing; int daysSinceLastLogin tells you everything, and you will never type it more than a handful of times. The exception is a loop index in a three-line loop, where i is genuinely clearer than currentIndex because everyone already knows what i means.

Let javac find the problems: -Xlint:all

javac carries a set of checks that go well beyond what the language requires, and you already have them installed. It is quiet by default, but -Xlint:all turns on every warning category it knows. Here is a small class with a real bug in it:

import java.util.ArrayList;
import java.util.List;

public class Warnings {
    static int discountPercent(String tier) {
        int percent;
        switch (tier) {
            case "gold":
                percent = 20;
            case "silver":
                percent = 10;
                break;
            default:
                percent = 0;
        }
        return percent;
    }

    public static void main(String[] args) {
        List cart = new ArrayList();
        cart.add("Book");
        System.out.println("gold   -> " + discountPercent("gold"));
        System.out.println("silver -> " + discountPercent("silver"));
        System.out.println("cart   -> " + cart);
    }
}

Plain javac Warnings.java gives you almost nothing:

Note: Warnings.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

With the flag on, the same file reports four problems:

javac -Xlint:all Warnings.java
Warnings.java:20: warning: [rawtypes] found raw type: List
        List cart = new ArrayList();
        ^
  missing type arguments for generic class List<E>
  where E is a type-variable:
    E extends Object declared in interface List
Warnings.java:20: warning: [rawtypes] found raw type: ArrayList
        List cart = new ArrayList();
                        ^
  missing type arguments for generic class ArrayList<E>
  where E is a type-variable:
    E extends Object declared in class ArrayList
Warnings.java:21: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
        cart.add("Book");
                ^
  where E is a type-variable:
    E extends Object declared in interface List
Warnings.java:10: warning: [fallthrough] possible fall-through into case
            case "silver":
            ^
4 warnings

That last one is not a style note. It is the bug:

gold   -> 10
silver -> 10
cart   -> [Book]

case "gold" sets percent = 20, then falls straight through into case "silver" and overwrites it with 10. A gold customer gets the silver discount, forever, and the only thing in the toolchain that noticed is a warning that is off by default.

Four warnings that catch real beginner mistakes

The categories below are the ones worth knowing early, because each corresponds to a mistake that is easy to make and hard to see. This file has all four:

public class Traps {
    static int counter = 0;

    public static void main(String[] args) {
        int score = 95;
        if (score > 90);
        {
            System.out.println("this always prints");
        }

        int zero = 0;
        System.out.println(10 / 0);

        Traps t = new Traps();
        t.counter = 5;

        int total = 10;
        total += 3.7;
        System.out.println(total + " " + zero);
    }
}
Traps.java:6: warning: [empty] empty statement after if
        if (score > 90);
                       ^
Traps.java:12: warning: [divzero] division by zero
        System.out.println(10 / 0);
                                ^
Traps.java:15: warning: [static] static variable should be qualified by type name, Traps, instead of by an expression
        t.counter = 5;
         ^
Traps.java:18: warning: [lossy-conversions] implicit cast from double to int in compound assignment is possibly lossy
        total += 3.7;
                 ^
4 warnings

Read them in order. The stray semicolon after if (score > 90) makes the if control nothing, so the block below runs unconditionally — this is the single most common typo in beginner Java and the compiler will tell you about it for free. 10 / 0 is a guaranteed ArithmeticException at runtime that the compiler can already see. t.counter reads like an instance field and is not one. And total += 3.7 silently truncates to 13, because compound assignment hides a cast that plain total = total + 3.7 would refuse to compile.

Two flags are worth putting in your build early:

FlagWhat it does
-Xlint:allTurn on every warning category
-WerrorTreat any warning as a compile error

Together they turn javac into something that refuses to build a file with a known problem in it:

error: warnings found and -Werror specified
1 error
3 warnings

javac --help-lint prints the full list of categories, so you can turn one off deliberately rather than turning them all off.

What a clean compile does not prove

A build with zero warnings is not a correctness certificate. Here is a program that compiles clean under -Xlint:all and is wrong:

public class Silent {
    public static void main(String[] args) {
        String expected = "admin";
        String typed = new java.util.Scanner("admin").next();

        if (typed == expected) {
            System.out.println("Access granted");
        } else {
            System.out.println("Access denied");
        }
    }
}
Access denied

The two strings are equal. == compares references, so the answer is false, and javac -Xlint:all prints nothing whatsoever. There is a whole section on this below, because it is the most common real bug in beginner Java.

-Xlint:all is also silent about unused local variables, unused private fields and unused private methods — none of those categories exist in javac. Your IDE greys them out; the compiler does not care. So use the warnings as a free extra pair of eyes, not as proof.

Magic numbers: give every literal a name

A magic number is a literal sitting in an expression with nothing to say what it means. Here is a checkout total with six of them:

public class Checkout {
    static double total(double[] prices, int[] quantities, String code, boolean member) {
        double t = 0;
        for (int i = 0; i < prices.length; i++) {
            t += prices[i] * quantities[i];
        }
        if (code != null) {
            if (code.equals("SAVE10")) {
                if (t > 100) {
                    t = t - t * 0.10;
                } else {
                    t = t - 5;
                }
            } else if (code.equals("SAVE20")) {
                if (t > 200) {
                    t = t - t * 0.20;
                }
            }
        }
        if (member) {
            t = t - t * 0.05;
        }
        t = t + t * 0.08;
        if (t < 0) {
            t = 0;
        }
        return Math.round(t * 100) / 100.0;
    }
}

Read t = t + t * 0.08; and try to say what 0.08 is without scrolling. Tax? A service charge? Is it the same 0.08 as the one in the invoice printer three files over? The literal answers none of that, and if the tax rate changes you have to find every copy of it by searching for a number.

The fix is private static final fields with names:

private static final double SAVE10_RATE = 0.10;
private static final double SAVE20_RATE = 0.20;
private static final double SAVE10_FLAT_OFF = 5.0;
private static final double SAVE10_MIN_ORDER = 100.0;
private static final double SAVE20_MIN_ORDER = 200.0;
private static final double MEMBER_RATE = 0.05;
private static final double TAX_RATE = 0.08;

and then t = t + t * TAX_RATE;. Nothing else in the method body changes. Running the same six cases against both versions gives the same six answers:

no code, guest   -> 98.82
SAVE10, guest    -> 93.42
SAVE10, member   -> 88.75
SAVE20, member   -> 93.88
small cart, SAVE10 -> 3.24
empty cart       -> 0.0

That is what makes it a refactor and not a rewrite: the behaviour is byte-for-byte identical, and the only thing that changed is what a reader can see. Note what the naming exposed on the way — SAVE10_FLAT_OFF = 5.0 and SAVE10_RATE = 0.10 are two completely different kinds of discount that the original code hid behind two similar-looking literals.

Two literals do not need names: 0 and 1, when they mean "nothing" and "one step". for (int i = 0; i < n; i++) is fine. if (retries > 3) is not.

Splitting a long method into named pieces

total above is twenty-five lines doing five separate jobs: adding up the cart, applying a coupon, applying a member discount, adding tax, and rounding. None of those jobs has a name, so none of them can be found, tested, or reused, and reading the method means holding all five in your head at once.

The 25-line method body banded into five coloured regions, each connected to the named method it becomes

Extraction gives each band a name. The body of total becomes the list of things it does:

static double total(double[] prices, int[] quantities, String code, boolean member) {
    double amount = subtotal(prices, quantities);
    amount -= couponDiscount(amount, code);
    amount -= memberDiscount(amount, member);
    amount += tax(amount);
    return roundToCents(Math.max(amount, 0));
}

private static double subtotal(double[] prices, int[] quantities) {
    double sum = 0;
    for (int i = 0; i < prices.length; i++) {
        sum += prices[i] * quantities[i];
    }
    return sum;
}

private static double couponDiscount(double amount, String code) {
    if (code == null) {
        return 0;
    }
    if (code.equals("SAVE10")) {
        return amount > SAVE10_MIN_ORDER ? amount * SAVE10_RATE : SAVE10_FLAT_OFF;
    }
    if (code.equals("SAVE20") && amount > SAVE20_MIN_ORDER) {
        return amount * SAVE20_RATE;
    }
    return 0;
}

private static double memberDiscount(double amount, boolean member) {
    return member ? amount * MEMBER_RATE : 0;
}

private static double tax(double amount) {
    return amount * TAX_RATE;
}

private static double roundToCents(double amount) {
    return Math.round(amount * 100) / 100.0;
}

Run the same six cases again:

no code, guest   -> 98.82
SAVE10, guest    -> 93.42
SAVE10, member   -> 88.75
SAVE20, member   -> 93.88
small cart, SAVE10 -> 3.24
empty cart       -> 0.0

Identical, for the third time. That is the whole discipline of refactoring: change the shape in a step small enough that you can run the program afterwards and prove nothing moved. If you cannot run it after a step, the step was too big.

What the extraction bought is not shortness. It is that questions now have addresses. "Where is the tax rate?" is tax. "Do members stack with coupons?" is answered by reading five lines of total. And couponDiscount is now a method small enough to call directly with a made-up amount and check by eye.

Refactoring usually makes the file longer

Be honest about the cost. The three versions of Checkout.java are 29, 37 and 50 lines. Naming the constants added eight lines; extracting the methods added thirteen more. Signatures, return statements and blank lines are not free.

Line count is not the metric. The metric is how much you must read to answer one question, and that went from twenty-five lines to about five. A file that is longer but navigable beats one that is shorter and has to be read end to end.

Guard clauses instead of nested ifs

Validation written as nested if statements drifts to the right until the real work sits at the far edge of the screen, and every else is separated from its if by a screenful of code:

static String registerNested(String name, int age, String email) {
    if (name != null) {
        if (!name.isBlank()) {
            if (age >= 18) {
                if (email != null && email.contains("@")) {
                    return "registered: " + name;
                } else {
                    return "bad email";
                }
            } else {
                return "too young";
            }
        } else {
            return "blank name";
        }
    } else {
        return "no name";
    }
}

A guard clause inverts each test and returns immediately. The failures are handled and forgotten one at a time, and what remains at the bottom is the case you actually came for:

static String registerGuarded(String name, int age, String email) {
    if (name == null) {
        return "no name";
    }
    if (name.isBlank()) {
        return "blank name";
    }
    if (age < 18) {
        return "too young";
    }
    if (email == null || !email.contains("@")) {
        return "bad email";
    }
    return "registered: " + name;
}

Six inputs through both versions, printing each result and whether they agree:

no name  |  no name  |  same=true
blank name  |  blank name  |  same=true
too young  |  too young  |  same=true
bad email  |  bad email  |  same=true
bad email  |  bad email  |  same=true
registered: Lan  |  registered: Lan  |  same=true

Same behaviour, and three practical wins. Each rejection reason sits on the same line as the condition that caused it, so you never scroll to match an else to its if. The success path is at one indent level instead of four. And adding a fifth rule is one more block at the top rather than another layer of nesting around everything.

The rule generalises past validation: handle the exceptional case and leave, then write the normal case flat. If a method's deepest indentation is three levels or more, that is usually a guard clause or an extracted method waiting to happen.

== and equals: the bug a clean compile will not catch

You met the Integer cache earlier in this course, in the data types article. It comes back here because it is the one bug that survives every check in this article — clean names, short methods, zero warnings — and still ships.

== on any reference type asks "are these the same object?" equals asks "do these represent the same value?" For boxed numbers the two answers differ at a specific place:

Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println("Integer 127: a == b       -> " + (a == b));
System.out.println("Integer 128: c == d       -> " + (c == d));
System.out.println("Integer 128: c.equals(d)  -> " + c.equals(d));
Integer 127: a == b       -> true
Integer 128: c == d       -> false
Integer 128: c.equals(d)  -> true

Autoboxing goes through Integer.valueOf, which returns a cached object for small values and a fresh one above the cache. Walking across the boundary shows exactly where it flips:

125 -> x == y is true
126 -> x == y is true
127 -> x == y is true
128 -> x == y is false
129 -> x == y is false
130 -> x == y is false
-128 -> true
-129 -> false

The cache is a JVM implementation detail, not a language rule, and you can prove it by moving the boundary at startup:

java -XX:AutoBoxCacheMax=200 Boundary
125 -> x == y is true
126 -> x == y is true
127 -> x == y is true
128 -> x == y is true
129 -> x == y is true
130 -> x == y is true

Same class file, same code, different answer. Any code whose correctness depends on == for boxed values is depending on a JVM flag.

Strings have the same shape of problem, with the compiler making it worse by being helpful:

String s1 = "hello";
String s2 = "hello";
String s3 = "hel" + "lo";
String part = "hel";
String s4 = part + "lo";
literal   : s1 == s2      -> true
const fold: s1 == s3      -> true
runtime   : s1 == s4      -> false
runtime   : s1.equals(s4) -> true

s3 is folded to the constant "hello" at compile time and interned with the other literals, so == is true. s4 is built at runtime and is a different object, so == is false. This is why the bug is so dangerous: == on strings works perfectly for every literal you type into a test, and fails the first time the string comes from a Scanner, a file or a network response.

⚠️ javac -Xlint:all reports nothing for any of the comparisons in this section. There is no warning to turn on.

The habit is simple and has no exceptions worth learning as a beginner: use equals for objects, == for primitives. == on references is correct in three narrow cases — comparing against null, comparing enum constants, and a deliberate "is this literally the same object" check — and in every one of those you will know that is what you meant.

A getter that hands out your internal state

Encapsulation is not finished when the field is private. A getter that returns the field itself hands the caller a live handle to your internals, and every rule the class enforces goes with it.

class LeakyTeam {
    private final List<String> members = new ArrayList<>();

    void add(String name) {
        if (members.size() >= 3) {
            throw new IllegalStateException("a team holds at most 3 people");
        }
        members.add(name);
    }

    List<String> getMembers() {
        return members;
    }
}

add protects the invariant. getMembers gives it away:

LeakyTeam leaky = new LeakyTeam();
leaky.add("Lan");
leaky.getMembers().add("Minh");
leaky.getMembers().add("Huy");
leaky.getMembers().add("Nam");
leaky.getMembers().add("Trang");
leaky size after add()      -> 1
leaky size after outside add-> 5
leaky members               -> [Lan, Minh, Huy, Nam, Trang]

Five members in a team that is documented and coded to hold three. The private final field did nothing, because final protects the reference and not the object it points at, and the caller was handed that object.

The fix is to return a copy:

List<String> getMembers() {
    return new ArrayList<>(members);
}
caller's list -> [Lan, Minh, Huy]
team's list   -> [Lan]
team size     -> 1

The caller can do whatever it likes to its copy and the team is untouched. If you would rather the caller find out immediately that it is not allowed to write, Collections.unmodifiableList(members) returns a read-only view instead:

safe getMembers().add()     -> java.lang.UnsupportedOperationException
safe size                   -> 1

The same trap applies to arrays, since getScores() returning int[] scores hands over the array itself, and to any mutable object you store in a field and return whole. The test to apply: after a caller uses this getter, can the object still promise everything its methods claim?

Comments that lie, and the comment that earns its place

A comment is the only part of a file the compiler never checks, so it is the only part that can be wrong forever without anything breaking.

// Returns the price with 10% VAT added.
static double addVat(double price) {
    return price * 1.08;
}
comment says 10%, code does -> 108.0

The rate changed and the comment did not. Now every reader has to decide which of the two to believe, and the honest answer is neither — you have to go and find out. A comment that restates what the code does is a second copy of the truth that nobody updates.

Most of these comments are a name asking to be written. Instead of explaining 1.08, name it:

private static final double VAT_RATE = 0.08;

static double priceWithVat(double price) {
    return price * (1 + VAT_RATE);
}

That version cannot go stale, because the explanation and the value are the same token.

The comment that does earn its place says why, and specifically records something a reader cannot recover from the code:

int[] unsorted = { 9, 3, 7, 1 };
System.out.println(Arrays.binarySearch(unsorted, 9));
-5

9 is at index 0, and binarySearch reports "not found", because it is only defined on a sorted array. A sort call sitting above a binarySearch looks removable to anyone who does not know that. So:

// binarySearch is undefined on an unsorted array: it can report "not found"
// for an id that is present. Sorting here is what makes the call valid.
Arrays.sort(ids);
3

That comment carries information the code cannot: a constraint from somewhere else, and a warning about a change that would look safe. Other comments worth writing are the reason a workaround exists, a link to the requirement behind a strange rule, and a deliberate "yes, this really is what I meant" on something that looks like a typo. Everything else is better spent on the name.

Formatting: pick one style and stop arguing

Formatting is the part of clean code with the least payoff per hour of discussion, which is why nearly every team stops discussing it and delegates the whole question to a tool.

The Java house style, the one every example in this course follows and the one you will meet in almost every Java codebase, is short enough to state:

  • four spaces of indentation, no tabs
  • opening brace on the same line as the declaration, closing brace on its own line
  • one statement per line, and braces even around single-statement if bodies
  • a blank line between methods, and between logical groups inside a method
  • spaces around binary operators and after commas

Braces around a one-line if are the only item there that beginners routinely skip, and it is the one with a real failure mode: adding a second line to a braceless if later puts that line outside the condition, silently. The compiler will not warn you. The stray-semicolon [empty] warning from earlier is the same class of accident.

Every mainstream Java IDE has a format-on-save action, and build tools have formatter plugins that can reformat a whole project or fail a build on badly formatted files. Turn one on, commit the result once, and never spend another code review on brace placement. The value of consistent formatting is not that any particular style is better; it is that when everything is formatted the same way, an unusual-looking line is a signal rather than noise.

Code smells worth recognising early

A code smell is not a bug. It is a shape that reliably turns out to be hiding one. The useful thing about the four below is that you can spot every one of them without reading the code.

Four panels showing code smells as silhouettes: a seamless slab, a signature running off the edge, one block stamped three times, and literals scattered through a body

SmellThe tellThe fix
Long methodOne block of code with no blank lines and no seamsExtract each job into a named method
Long parameter listFive or more parameters, several the same typeGroup them into an object, or split the method
Copy-pasted blockThe same shape appearing two or three timesExtract it once and call it three times
Magic numbersBare literals in expressions, often repeatedprivate static final with a name
Deep nestingThe code drifts right into an arrowGuard clauses and extraction
A comment explaining a block"Now we calculate the..." above ten linesThe block is a method; the comment is its name

The two most expensive are duplication and long parameter lists. Duplication is expensive because a fix has to be found and applied in every copy, and the copies drift apart until they are no longer obviously the same code. A long parameter list is expensive because Java matches arguments by position, not by name: in save(name, age, email, phone, active, role), swapping email and phone compiles cleanly and produces wrong data at runtime.

The last row is the most useful habit in the whole list. Any time you are about to write // now calculate the discount above a block of code, you have already named the method — write discountFor(...) instead and let the name live where a reader will find it.

FAQ

Does clean code make my Java program slower?

Do not decide your style on performance grounds, and do not take a number from an article either. Nothing in this article was timed, deliberately: readability is a decision about people, and the shape of your code is almost never what determines whether a program is fast enough. If a specific piece of code genuinely matters, measure that code with a proper benchmark rather than reasoning from its shape.

How long should a Java method be?

There is no number worth defending. A more useful test: can you name the method accurately without using "and"? calculateTotalAndPrintReceiptAndSaveToFile is three methods that have not been separated yet. If a method fits on a screen and does one nameable thing, its length is fine.

Do I have to follow the naming conventions if my code works?

Nothing enforces them — class shoppingCart compiles and runs, as shown at the top of this article. But every Java programmer and every tool reads names against the JDK's conventions, so ignoring them costs your reader a pause on every identifier. It is free to get right and it is the first thing an interviewer or a reviewer notices.

Should I use -Xlint:all on every project?

Turn it on and read what it says. It catches fall-through in switch, stray semicolons after if, guaranteed division by zero, lossy compound assignments and deprecated APIs — all things that are otherwise found at runtime or not at all. Add -Werror once the existing warnings are cleared, so new ones cannot accumulate. If a category is genuinely noise for your project, disable that one category by name rather than all of them.

When is a comment worth writing?

When it records something a reader cannot get from the code: why a workaround exists, what constraint a call depends on, or which requirement forced an odd rule. A comment that restates what the line does is a duplicate that will go stale. If you are about to write a comment describing what a block does, extract the block into a method and use the comment as its name.

Is == ever the right choice in Java?

Yes, in four places: comparing primitives such as int and char, checking a reference against null, comparing enum constants, and a deliberate identity check where you really do mean "the same object". Everywhere else, including all strings and all boxed types, use equals.

What is the difference between refactoring and rewriting?

A refactor changes the structure without changing the behaviour, in steps small enough that you can run the program after each one and see the same output — exactly as done three times with Checkout above. A rewrite changes behaviour, or changes so much at once that you cannot tell whether it did. Refactoring is safe because it is verifiable; rewriting has to be re-tested from scratch.

Conclusion

Clean code has no compiler behind it, so it comes down to a handful of habits you apply on purpose: name things the way the JDK names them, turn -Xlint:all on and read it, give every meaningful literal a name, keep methods short enough to name accurately, leave early instead of nesting, use equals for objects, never hand out your internal state through a getter, and write comments that say why. None of that is difficult. All of it is the difference between code you can change next month and code you rewrite.

That also closes the Java Basics course. Thirty-six articles ago the question was what Java is and why it needs a JVM; from there this course covered the JDK and the compile-and-run cycle, variables, types and casting, operators, strings, input, conditionals and loops, arrays in one and two dimensions and the algorithms that walk them, methods with their parameters, overloads, scope and recursion, then the object-oriented core — classes and objects, fields and constructors, this, static and final, encapsulation, inheritance, polymorphism, abstract classes and interfaces — then exceptions, collections and maps, file input and output, and finally a console project that put all of it into one working program.

Concretely, you can now read a Java class and predict what it does, write one that models a real problem, choose the right collection for a job, handle failure with exceptions instead of hoping, read and write files, and compile, run and debug the result from a terminal without an IDE holding your hand. That is a real foundation, and it is roughly where a junior Java developer starts. It is not the whole language: everything you have written so far has been small, single-threaded and self-contained.

Next comes the Advanced Java course, which starts by going back over the four OOP principles at a depth this course could not afford, then covers nested, inner and anonymous classes, enums with fields and behaviour of their own, generics and why List<String> needed those angle brackets all along, the SOLID principles, and the design patterns you will meet in every real Java codebase. The habits in this article are what make that material readable when you get there.

Related Posts

[Java Basics] Read and Write Text Files in Java

Reading and writing text files in Java: FileReader and FileWriter, why BufferedReader and BufferedWriter matter, try-with-resources, the modern Files and Path API, relative paths, the real exceptions when a file is missing, and the character encoding that decides whether the round trip survives.

[Java Basics] Variables and Data Types in Java: Primitive vs Reference

A complete guide to Java variables and data types - all 8 primitive types with their real ranges, reference types, stack vs heap, literals, integer overflow, var, final and autoboxing, with every example compiled and run on JDK 21.

[Java Basics] Encapsulation in Java: Access Modifiers, Getters and Setters

Encapsulation in Java explained by running it: the four access modifiers demonstrated across two real packages, the protected rule most tutorials state wrongly, getters that leak a mutable field, defensive copies, immutable classes and records, with every javac error reproduced on JDK 21.

[Java Basics] Abstraction in Java: Abstract Classes and Abstract Methods

Abstract classes and abstract methods in Java: why Shape cannot be instantiated, what an abstract class still holds, the template method pattern, anonymous subclasses, constructor order, and every compile error reproduced on JDK 21.