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
forwhen you do not need the index; the classic one when you do. - The old
switchfalls through silently - prefer the arrow form. - A
switchexpression produces a value, and can be checked for exhaustiveness. yieldreturns 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.