Control Flow, Methods and switch

Article · 11:00

The loops and conditionals will look familiar from any C-family language. What has changed recently and is worth knowing properly is switch: since Java 14 it can be an expression, which cannot fall through. Plus varargs and pass-by-value, precisely.

Control Flow, Methods and switch

The loops and conditionals will look familiar from any C-family language. What has changed recently, and is worth knowing properly, is switch: since Java 14 it can be an expression, and that version is both shorter and safer.

public class Flow {
    public static void main(String[] args) {
        int[] scores = {88, 42, 95, 61};

        for (int score : scores) {              // the enhanced for, when you do
            System.out.println(grade(score));   // not need the index
        }

        for (int i = 0; i < scores.length; i++) {
            if (scores[i] == 42) continue;
            if (scores[i] > 90) break;
        }

        int countdown = 3;
        while (countdown > 0) {
            System.out.println("t-minus " + countdown--);
        }

        do {
            System.out.println("runs at least once");
        } while (false);
    }

    static String grade(int score) {
        if (score >= 90) return "excellent";
        else if (score >= 60) return "pass";
        else return "resit";
    }
}

The old switch, and everything wrong with it

public class OldSwitch {
    public static void main(String[] args) {
        System.out.println(describe(404));
        System.out.println(describe(302));      // falls through on purpose
        System.out.println(describe(500));
    }

    static String describe(int code) {
        String result;
        switch (code) {
            case 200:
                result = "ok";
                break;                 // forget this and the next case runs
            case 301:
            case 302:
                result = "redirected";
                break;
            case 404:
                result = "not found";
                break;
            default:
                result = "something else";
        }
        return result;
    }
}

Three problems: a missing break falls through silently, the variable has to be declared before the switch and assigned inside it, and nothing checks that every case is covered.

The switch expression fixes all three

public class NewSwitch {
    public static void main(String[] args) {
        for (int code : new int[]{200, 302, 404, 500}) {
            System.out.println(code + " -> " + describe(code));
        }
        System.out.println(season(12));
    }

    static String describe(int code) {
        return switch (code) {
            case 200 -> "ok";
            case 301, 302 -> "redirected";      // several labels, one arm
            case 404 -> "not found";
            default -> "something else";
        };
    }

    // An arm needing more than an expression uses a block and yield.
    static String season(int month) {
        return switch (month) {
            case 12, 1, 2 -> "winter";
            case 3, 4, 5 -> "spring";
            case 6, 7, 8 -> "summer";
            case 9, 10, 11 -> "autumn";
            default -> {
                var message = "no month " + month;
                yield message;
            }
        };
    }
}

No fall-through, it produces a value, and over an enum or a sealed type it must cover every case or it will not compile. Level 3 builds on that.

Methods

public class Methods {
    public static void main(String[] args) {
        System.out.println(add(2, 3));
        System.out.println(add(2.5, 3.5));        // overloaded: the types pick one
        System.out.println(join("-", "a", "b", "c"));
        System.out.println(join("-"));            // varargs accepts none

        int[] numbers = {3, 1, 2};
        sortInPlace(numbers);
        System.out.println(java.util.Arrays.toString(numbers));
    }

    static int add(int a, int b) {
        return a + b;
    }

    static double add(double a, double b) {       // same name, different signature
        return a + b;
    }

    // varargs: zero or more, arriving as an array.
    static String join(String separator, String... parts) {
        return String.join(separator, parts);
    }

    static void sortInPlace(int[] values) {
        java.util.Arrays.sort(values);            // arrays are objects, so the
    }                                             // caller sees the change
}

Java passes everything by value - including references

This is the single most misunderstood rule in the language, and it is simple once stated precisely: the variable's contents are copied. For a primitive that is the number. For an object that is the reference - so the method can change the object, but cannot repoint the caller's variable.

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

public class PassByValue {
    public static void main(String[] args) {
        int number = 1;
        increment(number);
        System.out.println(number);            // still 1: the int was copied

        List<String> list = new ArrayList<>(List.of("a"));
        mutate(list);
        System.out.println(list);              // [a, b]: same object, changed

        reassign(list);
        System.out.println(list);              // still [a, b]: the copy was repointed
    }

    static void increment(int value) {
        value++;
    }

    static void mutate(List<String> values) {
        values.add("b");                       // reaches the caller's object
    }

    static void reassign(List<String> values) {
        values = new ArrayList<>(List.of("z")); // only this local now points there
    }
}

What to take away

  • The enhanced for when you do not need the index; the classic one when you do.
  • The old switch falls through silently - prefer the arrow form.
  • A switch expression produces a value, and can be checked for exhaustiveness.
  • yield returns a value from a block-bodied arm.
  • Overloading picks by signature; varargs takes zero or more.
  • Everything is passed by value - including the reference, so a method can change an object but never repoint the caller's variable.