Command Palette

Search for a command to run...

[Java Basics] Variable Scope in Java: Local, Field and Static

The scope of a variable is the region of source code in which its name means that variable. Outside that region the name is not merely empty — it does not exist, and javac rejects the program. Scope is decided entirely by where the declaration sits, and in Java that means: by the braces around it.

This article covers the four kinds of variable a Java program can declare, where each one is visible, when each one is created and destroyed, and the errors that appear when a read falls outside the region the compiler allows. Every output line and every error message below was produced by compiling and running the code on OpenJDK 21.0.6.

Three nested braces, one variable declared at each level

One rule does most of the work: a name lives from its declaration to the closing brace of the block that contains it.

Java has no global variable

Many tutorials, and many course outlines, contrast "local variables" with "global variables". Java has no such thing. There is no place to put a variable that is not inside a class, so there is no name that every part of a program can reach without qualification.

The nearest equivalent is a static field: a variable declared directly in a class body with the static keyword. One copy exists per class, it is reachable from any method of that class by its bare name, and from anywhere else as ClassName.field.

public class NoGlobal {
    // the nearest thing Java has to a "global variable": a static field
    static int requestCount = 0;

    static void handle() {
        requestCount++;                    // no object needed, no parameter passed
    }

    public static void main(String[] args) {
        handle();
        handle();
        handle();
        System.out.println("NoGlobal.requestCount = " + NoGlobal.requestCount);
        System.out.println("Counter.total         = " + Counter.total);
        Counter.total = 42;
        System.out.println("Counter.total         = " + Counter.total);
    }
}

class Counter {
    static int total = 7;                  // reachable from another class by its name
}
NoGlobal.requestCount = 3
Counter.total         = 7
Counter.total         = 42

That is as global as Java gets, and it still has an owner. requestCount is not floating free; it belongs to NoGlobal. When the outline for this series says "local versus global", read it as "local versus static field", and the rest of the article is about the four places a name can actually live.

The four scopes a variable can have

Every variable in a Java program is one of four things, and its scope follows from which one it is.

KindDeclared whereVisible whereOne copy per
Local variableinside a method, constructor or blockfrom its declaration to the closing brace of that blockexecution of the block
Method parameterin the parameter listthe whole method bodycall
Instance fieldin the class body, no staticevery instance method of the classobject
static fieldin the class body, with staticevery method of the classclass

The first two live on the call stack, the last two in the object or in the class. One program shows all four at once.

public class FourScopes {
    static String appName = "scope-demo";      // static field: one copy, whole class
    String user;                               // instance field: one copy per object

    FourScopes(String user) {
        this.user = user;
    }

    void report(int visits) {                  // visits: parameter, whole method body
        String prefix = "[" + appName + "]";   // local: the method body
        if (visits > 0) {
            String note = visits + " visit(s)";// local: only this if block
            System.out.println(prefix + " " + user + " -> " + note);
        }
        // note is gone here
        System.out.println(prefix + " " + user + " reported");
    }

    public static void main(String[] args) {
        FourScopes a = new FourScopes("ann");
        FourScopes b = new FourScopes("bob");
        a.report(2);
        b.report(0);
        System.out.println("appName = " + appName + ", a.user = " + a.user + ", b.user = " + b.user);
    }
}
[scope-demo] ann -> 2 visit(s)
[scope-demo] ann reported
[scope-demo] bob reported
appName = scope-demo, a.user = ann, b.user = bob

appName is one value shared by everything. user is two values, one in each object. visits and prefix exist only while report is running, and note only while the if body is running. this.user appears in the constructor because a parameter named user is hiding the field there; that is the subject of the shadowing section below, and this itself gets a proper treatment in the article on classes and objects.

A field is visible from a static method only through an object

The class body is one scope, but a static method has no object, so an instance field has no storage to read from inside it.

public class StaticContext {
    static int shared = 1;
    int perObject = 2;

    public static void main(String[] args) {
        System.out.println("shared    = " + shared);
        System.out.println("perObject = " + perObject);
    }
}
StaticContext.java:7: error: non-static variable perObject cannot be referenced from a static context
        System.out.println("perObject = " + perObject);
                                            ^
1 error

The name perObject is in scope — the compiler found it, which is why the message is not cannot find symbol. What is missing is an object to read it from. Naming one fixes it:

StaticContextOk o = new StaticContextOk();
System.out.println("shared    = " + shared);
System.out.println("perObject = " + o.perObject);
shared    = 1
perObject = 2

This is the variable-shaped twin of non-static method cannot be referenced from a static context, covered in the article on methods.

Block scope: the closing brace is where the name ends

A block is any pair of braces: a method body, an if body, a loop body, or a bare { } written for no reason but grouping. A local declared inside a block ends at that block's closing brace, and afterwards the name does not exist.

Three nested scopes over one source file, with the two reads javac rejects

public class Scopes {
    static int classLevel = 1;

    static void run() {
        int methodLevel = 2;
        if (methodLevel > 0) {
            int blockLevel = 3;
            use(classLevel, methodLevel, blockLevel);
        }
        use(classLevel, methodLevel);
        use(blockLevel);
    }

    static void other() {
        use(classLevel);
        use(methodLevel);
    }

    static void use(int... values) { }

    public static void main(String[] args) {
        run();
        other();
    }
}
Scopes.java:11: error: cannot find symbol
        use(blockLevel);
            ^
  symbol:   variable blockLevel
  location: class Scopes
Scopes.java:16: error: cannot find symbol
        use(methodLevel);
            ^
  symbol:   variable methodLevel
  location: class Scopes
2 errors

Two errors, and both are the same error. Line 11 sits in run, one brace outside the if, so blockLevel is gone. Line 16 sits in other, an entirely different method, so methodLevel is gone. classLevel is read from three places without complaint, because a field's scope is the whole class body.

cannot find symbol is worth reading carefully: the compiler is not saying the variable is empty, uninitialized or out of range. It is saying there is no such name here at all.

The for header counter

The most common encounter with block scope is a loop counter. A variable declared in the initializer of a for header belongs to the loop, header and body together, and dies with it.

public class BlockScope {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println("i = " + i);
        }
        System.out.println("after the loop, i = " + i);
    }
}
BlockScope.java:6: error: cannot find symbol
        System.out.println("after the loop, i = " + i);
                                                    ^
  symbol:   variable i
  location: class BlockScope
1 error

If you need the counter afterwards — to know where a search stopped, say — declare it before the loop and leave the initializer empty:

int i = 0;
for (; i < 3; i++) {
    System.out.println("i = " + i);
}
System.out.println("after the loop, i = " + i);
i = 0
i = 1
i = 2
after the loop, i = 3

Most of the time you do not need it, and the version that scopes i to the loop is the better one precisely because the name cannot leak.

A variable declared inside an if

The same rule, with a different brace. An if body is a block.

public class IfScope {
    public static void main(String[] args) {
        int age = 20;
        if (age >= 18) {
            String status = "adult";
        }
        System.out.println(status);
    }
}
IfScope.java:7: error: cannot find symbol
        System.out.println(status);
                           ^
  symbol:   variable status
  location: class IfScope
1 error

The fix is to split declaration from assignment: declare status in the scope that has to read it, and assign inside the branch.

int age = 20;
String status = "minor";       // declared in the scope that needs to read it
if (age >= 18) {
    status = "adult";          // assignment, not declaration
}
System.out.println(status);
adult

A bare block behaves identically, which is the clearest demonstration that braces alone are what matter:

{
    int temp = 5;
    System.out.println("inside the block, temp = " + temp);
}
System.out.println("outside the block, temp = " + temp);
BareBlock.java:7: error: cannot find symbol
        System.out.println("outside the block, temp = " + temp);
                                                          ^
  symbol:   variable temp
  location: class BareBlock
1 error

Other places a scope opens

Three more constructs declare a variable whose scope is the block they head. All three behave exactly like the for counter. The excerpt below sits in a class that imports java.util.Scanner.

int[] data = {1, 2, 3};

for (int n : data) {                       // n: the enhanced-for body
    System.out.println("n = " + n);
}

try (Scanner sc = new Scanner("42")) {     // sc: the try block
    System.out.println("read " + sc.nextInt());
}

try {
    throw new IllegalStateException("boom");
} catch (RuntimeException e) {             // e: the catch block
    System.out.println("caught " + e.getMessage());
}
n = 1
n = 2
n = 3
read 42
caught boom

Read n after the enhanced for and the message is the one you already know:

OtherScopesBad.java:6: error: cannot find symbol
        System.out.println(n);
                           ^
  symbol:   variable n
  location: class OtherScopesBad
1 error

Declaration order: locals must come first, fields need not

For a local variable, the scope starts at the declaration, not at the opening brace. A read on an earlier line is outside it.

public class LocalOrder {
    public static void main(String[] args) {
        System.out.println("total = " + total);
        int total = 10;
    }
}
LocalOrder.java:3: error: cannot find symbol
        System.out.println("total = " + total);
                                        ^
  symbol:   variable total
  location: class LocalOrder
1 error

A field is different. Its scope is the whole class body, so a method may use a field declared far below it, and the file compiles and runs:

public class FieldOrder {
    // show() reads a field that is declared 4 lines BELOW it
    static void show() {
        System.out.println("greeting = " + greeting);
    }

    static String greeting = "hello";

    public static void main(String[] args) {
        show();
    }
}
greeting = hello

This is why the order of members in a class is a style decision rather than a compiler requirement, while the order of statements in a method is not.

There is one exception, and it is narrow: a field initializer may not read a field declared after it, because initializers run top to bottom.

public class FieldForward {
    static int a = b + 1;    // reads b before b is declared
    static int b = 2;

    public static void main(String[] args) {
        System.out.println("a = " + a + ", b = " + b);
    }
}
FieldForward.java:2: error: illegal forward reference
    static int a = b + 1;    // reads b before b is declared
                   ^
1 error

The name is in scope — that is why the error is not cannot find symbol — but the value does not exist yet. Inside a method body the same reference is legal, because by the time any method runs, every initializer has already finished.

Definite assignment for locals, default values for fields

Scope decides whether a name exists. A separate rule decides whether it holds anything. The article on variables and data types covered it in full; the short version is that fields are default-initialized and locals are not.

public class FieldDefaultsOk {
    static int staticCount;          // no initializer
    int instanceCount;               // no initializer
    String label;                    // no initializer

    public static void main(String[] args) {
        FieldDefaultsOk f = new FieldDefaultsOk();
        System.out.println("staticCount   = " + staticCount);
        System.out.println("instanceCount = " + f.instanceCount);
        System.out.println("label         = " + f.label);
    }
}
staticCount   = 0
instanceCount = 0
label         = null

Add one uninitialized local to the same main and the program stops compiling:

int local;                   // no initializer, and no default either
System.out.println("local         = " + local);
FieldDefaults.java:13: error: variable local might not have been initialized
        System.out.println("local         = " + local);
                                                ^
1 error

The word to notice is might. The compiler is not tracking values; it is proving that every path reaching the read has already written the variable. One branch that forgets is enough to fail the proof:

public class BranchAssign {
    public static void main(String[] args) {
        int score = 55;
        String grade;                 // declared, deliberately not initialized
        if (score >= 60) {
            grade = "pass";
        }
        System.out.println(grade);
    }
}
BranchAssign.java:8: error: variable grade might not have been initialized
        System.out.println(grade);
                           ^
1 error

Adding an else that assigns grade makes the proof succeed and the program prints fail. This is also why splitting a declaration from its assignment, as in the if fix above, still needs every branch covered — or a starting value at the declaration.

Shadowing: when a local hides a field

Two variables may share a name if one is a field and the other is a local or a parameter. Inside the method, the nearer declaration wins, and the field becomes unreachable by its bare name. That is shadowing, and it is legal, deliberate and the source of one of the most common silent bugs in Java.

The broken setter writing the parameter slot back into itself, against this.name writing across to the field

The broken setter

public class BrokenSetter {
    String name = "unset";

    void setName(String name) {
        name = name;                 // assigns the parameter to itself
    }

    public static void main(String[] args) {
        BrokenSetter b = new BrokenSetter();
        System.out.println("before setName: b.name = " + b.name);
        b.setName("ada");
        System.out.println("after  setName: b.name = " + b.name);
    }
}
before setName: b.name = unset
after  setName: b.name = unset

Both name tokens on that line resolve to the parameter, so the statement copies the parameter into itself and the field never moves. It is not a compile error, and it is not a warning either — javac -Xlint:all BrokenSetter.java prints nothing at all. The only evidence is the output.

this.name names the field explicitly, which is the fix:

void setName(String name) {
    this.name = name;            // field on the left, parameter on the right
}
before setName: f.name = unset
after  setName: f.name = ada

this is the receiver of the current instance method, and it gets its own treatment in the article on classes and objects. For now, treat this.x as the way to say "the field, not the local".

Shadowing is not confined to parameters. Any local with a field's name does the same thing:

public class Shadowing {
    int count = 100;                 // the field

    void demo() {
        System.out.println("field           = " + count);
        int count = 5;               // a local that shadows the field
        System.out.println("local           = " + count);
        System.out.println("field via this  = " + this.count);
        count++;                     // touches the local only
        System.out.println("local after ++  = " + count);
        System.out.println("field after ++  = " + this.count);
    }

    public static void main(String[] args) {
        new Shadowing().demo();
    }
}
field           = 100
local           = 5
field via this  = 100
local after ++  = 6
field after ++  = 100

Note the first line: before the local's declaration, count still means the field. The name changes meaning halfway down the method. That is legal, and it is also a good argument for never writing it.

Shadowing a field on purpose has exactly one common use, the constructor or setter parameter named after the field, and there this. makes the intent explicit. Everywhere else, rename the local.

You cannot shadow a local with another local

The permission stops at fields. Two locals with the same name in the same method are rejected, whether the second one is in the same block or a nested one.

int x = 1;
int x = 2;
RedeclareSame.java:4: error: variable x is already defined in method main(String[])
        int x = 2;
            ^
1 error
int x = 1;
if (x > 0) {
    int x = 2;           // a nested block cannot shadow an enclosing local
    System.out.println(x);
}
RedeclareNested.java:5: error: variable x is already defined in method main(String[])
            int x = 2;           // a nested block cannot shadow an enclosing local
                ^
1 error

A parameter counts as a local for this rule:

static void f(int n) {
    int n = 3;               // a local cannot shadow a parameter either
    System.out.println(n);
}
RedeclareParam.java:3: error: variable n is already defined in method f(int)
        int n = 3;               // a local cannot shadow a parameter either
            ^
1 error

Two sibling blocks may reuse a name, because the first one has already ended before the second begins:

public class SiblingBlocks {
    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            System.out.println("first loop, i = " + i);
        }
        for (int i = 10; i < 12; i++) {      // legal: the first i no longer exists
            System.out.println("second loop, i = " + i);
        }
        if (args.length == 0) {
            int n = 1;
            System.out.println("then branch, n = " + n);
        } else {
            int n = 2;                        // legal: a sibling block, not a nested one
            System.out.println("else branch, n = " + n);
        }
    }
}
first loop, i = 0
first loop, i = 1
second loop, i = 10
second loop, i = 11
then branch, n = 1

Run with no arguments, the else branch never executes, but it compiles: the two n declarations are in different blocks, and neither encloses the other.

Lifetime is not scope

Scope is a property of the source text: which lines may name the variable. Lifetime is a property of the running program: how long the storage exists. They are usually confused because for a local they coincide, and for a field they do not.

Three counters on one time axis, one local, one instance field, one static field

KindCreatedDestroyedCopies alive at once
Local variablewhen execution reaches its declarationwhen the block ends, with the stack frameone per active block
Method parameterwhen the method is calledwhen the method returnsone per active call
Instance fieldwhen the object is createdwhen the object becomes unreachableone per object
static fieldwhen the class is loadedwhen the class is unloaded, in practice at JVM exitone

Three counters in one class make the difference visible.

public class Lifetime {
    static int staticCalls = 0;      // lives from class load until the JVM exits
    int instanceCalls = 0;           // lives as long as this object is reachable

    void tick() {
        int localCalls = 0;          // created and destroyed on every call
        localCalls++;
        instanceCalls++;
        staticCalls++;
        System.out.println("local " + localCalls
                + " | instance " + instanceCalls
                + " | static " + staticCalls);
    }

    public static void main(String[] args) {
        Lifetime a = new Lifetime();
        a.tick();
        a.tick();
        a.tick();

        System.out.println("-- a second object --");
        Lifetime b = new Lifetime();
        b.tick();
        b.tick();
    }
}
local 1 | instance 1 | static 1
local 1 | instance 2 | static 2
local 1 | instance 3 | static 3
-- a second object --
local 1 | instance 1 | static 4
local 1 | instance 2 | static 5

Three shapes in one printout. localCalls never gets past 1, because each call gets a brand new slot initialized to 0. instanceCalls counts to 3 for a and then restarts at 1 for b, because each object carries its own copy. staticCalls climbs to 5, because there is one copy for the class and both objects increment it.

Recursion makes the local case sharper still: a method that calls itself has several frames alive at once, and each frame has its own copy of every local. The name is one name; the storage is one slot per frame.

Loop-body locals are re-created every iteration

A loop body is a block, so a variable declared inside it is created and destroyed on every pass. It cannot accumulate anything.

public class LoopLocal {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            int sum = 0;             // re-created every iteration
            sum += i;
            System.out.println("inside  i=" + i + " sum=" + sum);
        }

        int total = 0;               // declared outside, so it survives the loop
        for (int i = 1; i <= 3; i++) {
            total += i;
            System.out.println("outside i=" + i + " total=" + total);
        }
    }
}
inside  i=1 sum=1
inside  i=2 sum=2
inside  i=3 sum=3
outside i=1 total=1
outside i=2 total=3
outside i=3 total=6

sum shows the value of one iteration, never the running total, because int sum = 0; runs again every pass. total accumulates because its declaration is outside the loop and executes once. Move a declaration across a brace and the meaning of the program changes.

Reading sum after the loop is the block-scope error again, so the mistake usually announces itself:

LoopLocalRead.java:7: error: cannot find symbol
        System.out.println("sum = " + sum);
                                      ^
  symbol:   variable sum
  location: class LoopLocalRead
1 error

The dangerous version is the one where a variable of that name already exists outside the loop, so the read compiles and quietly reports the last iteration instead of the total.

Nested blocks, and what lambdas add

An inner block can see everything the enclosing block declared before it, and can write to it. Nesting adds names; it never hides the outer ones, because a redeclaration is rejected outright.

public class NestedBlock {
    public static void main(String[] args) {
        int outer = 1;
        {
            int inner = 2;
            outer += inner;          // an inner block reads and writes the outer local
            System.out.println("inner block: outer=" + outer + " inner=" + inner);
        }
        System.out.println("after block: outer=" + outer);
    }
}
inner block: outer=3 inner=2
after block: outer=3

Ordinary blocks are that simple. Anonymous classes and lambdas are not: they can outlive the method that created them, so a local they capture must be final or effectively final — assigned once and never reassigned. Reassigning n after a lambda has captured it is rejected:

LambdaCapture.java:4: error: local variables referenced from a lambda expression must be final or effectively final
        Runnable r = () -> System.out.println("n = " + n);
                                                       ^
1 error

Capture rules belong with lambdas themselves and are covered in the advanced course. The reason is worth one sentence here: a lambda copies the captured local, so allowing reassignment would give two values that silently disagree.

Declare at the narrowest scope that works

The practical rule follows from everything above: declare each variable as close to its first use as possible, in the smallest block that contains every use. Not because narrow scope is tidy, but because it is what makes code movable.

// wide: every local lives to the end of the method, so nothing is separable
static void wide(int[] data) {
    int sum = 0;
    int max = data[0];
    int i = 0;
    for (i = 0; i < data.length; i++) {
        sum += data[i];
        if (data[i] > max) {
            max = data[i];
        }
    }
    System.out.println("wide:   sum=" + sum + " max=" + max + " i=" + i);
}

Every name in wide is alive for the whole method, so a reader has to hold five of them at once, and extracting the maximum calculation into its own method means checking each one for a use further down. Narrow the scopes and the pieces come apart on their own:

static int sum(int[] data) {
    int total = 0;
    for (int n : data) {
        total += n;
    }
    return total;
}

static int max(int[] data) {
    int best = data[0];
    for (int n : data) {
        if (n > best) {
            best = n;
        }
    }
    return best;
}
wide:   sum=21 max=9 i=4
narrow: sum=21 max=9

Same answers. The difference is that total and best cannot be read anywhere except the four lines that own them, so moving those lines is a mechanical operation rather than an investigation. A variable declared at the top of a long method is an unpaid debt: someone eventually has to work out how far its influence reaches.

Three habits follow from that. Declare inside the loop when the value is per-iteration and outside when it accumulates. Prefer the for header for a counter you do not need afterwards. And when you find yourself declaring a variable several lines before its first use, move the declaration down.

Common mistakes and the errors they produce

MistakeWhat happensMessage
Reading a for counter after the loopcompile errorcannot find symbol
Declaring inside an if and reading outsidecompile errorcannot find symbol
Reading a local above its own declarationcompile errorcannot find symbol
Reading a catch parameter or enhanced-for variable after its blockcompile errorcannot find symbol
Reading an instance field from maincompile errornon-static variable x cannot be referenced from a static context
Reading a local that some path leaves unassignedcompile errorvariable x might not have been initialized
A field initializer reading a field declared below itcompile errorillegal forward reference
Redeclaring a local in the same or a nested blockcompile errorvariable x is already defined in method ...
Writing name = name; in a settercompiles, does nothing, no warningnone
Declaring an accumulator inside the loop bodycompiles, resets every iterationnone
Shadowing a field by accident, then reading the localcompiles, wrong valuenone

The last three produce no diagnostic at all, and they share a single cause: the name resolved to something, just not to the variable you meant. When a value is wrong rather than missing, the first question to ask is which declaration the name actually refers to.

FAQ

What is the difference between a local variable and a global variable in Java?

Java has no global variables. A local variable is declared inside a method or block and exists only there; the closest equivalent to a global is a static field, which is declared in a class body, has one copy per class, and is reachable as ClassName.field from anywhere that can see the class. It still belongs to that class, so it is namespaced rather than global.

Why does Java say cannot find symbol for my loop counter?

Because a variable declared in a for header belongs to the loop and ceases to exist at its closing brace, so after the loop the name is not defined. Either declare the variable before the loop and leave the header's initializer empty, or restructure so the counter is not needed afterwards.

Can a local variable have the same name as a field in Java?

Yes. The local shadows the field for the rest of its own scope, and the field is then reachable only as this.field for an instance field or ClassName.field for a static one. It compiles without a warning, so shadow deliberately or not at all.

Why is name = name not an error in Java?

Because both occurrences resolve to the nearest declaration, which is the parameter, so the statement is a legal self-assignment of the parameter to itself. The field is never touched, javac reports nothing, and -Xlint:all reports nothing either. Write this.name = name;.

When is a local variable destroyed in Java?

At the closing brace of the block that declares it, when its stack frame is discarded. A variable declared inside a loop body is destroyed and re-created on every iteration, which is why it cannot accumulate a total across iterations.

Can I declare two variables with the same name in nested blocks in Java?

No. javac rejects it with variable x is already defined in method ..., and the rule covers parameters too. Two sibling blocks that do not contain each other may reuse a name, because the first declaration has already gone out of scope before the second appears.

Conclusion

Scope in Java is decided by braces and nothing else. A local runs from its declaration to the closing brace of its block; a parameter covers the whole method body; a field covers the whole class body, which is why a method can use a field declared below it. cannot find symbol means the name does not exist at that point, non-static variable ... cannot be referenced from a static context means it exists but has no object to live in, and variable x might not have been initialized means it exists and holds nothing the compiler can vouch for.

Lifetime is the separate question of how long the storage lasts: one slot per call for a local, one per object for an instance field, one per class for a static field. Shadowing is where the two ideas collide — the name still resolves, just to the wrong slot, and name = name; is the case that costs people an afternoon. Declaring at the narrowest scope that works is what prevents most of it.

Next in this series: recursion — a method that calls itself, the base case that stops it, how each call gets its own copy of every local, and why a missing base case ends in StackOverflowError.

Related Posts

[Java Basics] Loops in Java: for, while and do-while

Loops in Java explained by running them: the exact execution order of a for header, while vs do-while, the enhanced for and why it cannot write back, off-by-one errors against length, and the three ways to write an infinite loop.

[Java Basics] Inheritance in Java: extends and super

How extends works in Java, what a subclass inherits and what it does not, why constructors are never inherited, how super(...) chains constructors up to java.lang.Object and back down, field hiding versus overriding, protected across packages, final classes, the fragile base class problem, and when composition is the better answer.

[Java Basics] Conditional Statements in Java: if, else if, else and switch

Conditional statements in Java: the if / else if / else chain, why braces matter, the dangling else, boolean-only conditions, switch with break and fall-through, arrow labels, switch expressions with yield, and pattern matching in Java 21.

[Java Basics] Basic Array Algorithms in Java: Max, Min, Search and Sort

Max, min, sum, average, filtering, reversing, linear and binary search, bubble, selection and insertion sort, and Arrays.sort in Java - every algorithm instrumented with comparison and swap counters and every output produced on JDK 21.