Create program flow control constructs including if/else, switch statements and expressions, loops, and break and continue statements.
Implement inheritance, including abstract and sealed types as well as record classes. Override methods, including that of the Object class. Implement polymorphism and differentiate between object type and reference type. Perform reference type casting, identify object types using the instanceof operator, and pattern matching with the instanceof operator and the switch construct.
if
StatementOne of the most fundamental control flow statements in Java and many other programming languages is the if
statement. It allows your program to make decisions and execute different code paths based on whether certain conditions are met.
In essence, the purpose of an if
statement is to conditionally execute a block of code. If the specified condition evaluates to true
, the code block will run. If not, that block is skipped over and the program continues with the next statement after the if block.
Here’s the flowchart diagram for the if
statement:
┌─────────┐
│ Start │
└────┬────┘
│
┌─────┴─────┐
│ Condition │
└─────┬─────┘
│
┌──────┴──────┐
┌────┤ Is true? ├────┐
│ └─────────────┘ │
│ │
┌──┴──┐ ┌──┴──┐
│ Yes │ │ No │
│ │ │ │
│─────┴──────────┐ ┌──┴─────┴─────┐
│ Execute │ │ Execute │
│ if block │ │ else block │
└─────────┬──────┘ └──────┬───────┘
│ │
└────────┬────────┘
│
┌─────┴─────┐
│ End │
└───────────┘
The basic syntax of an if
statement looks like this:
if (condition) {
// Code to execute if condition is true
}
The condition goes inside parentheses and must evaluate to a boolean value, either true
or false
. The code to conditionally execute goes between curly braces. If the code block contains only a single statement, you can omit the curly braces:
if (x > 10)
System.out.println("x is greater than 10");
However, using curly braces is considered good practice even for single statements, as it makes your code clearer and less prone to errors if you later add more statements to the block.
You can chain multiple conditions together using the else if
construct:
if (condition1) {
// Code to execute if condition1 is true
} else if (condition2) {
// Code to execute if condition1 is false and condition2 is true
} else {
// Code to execute if both condition1 and condition2 are false
}
Here, each else if
condition will only be checked if all the prior if
/else if
conditions evaluated to false
. As soon as one condition is found to be true
, its corresponding block executes and the rest of the if
/else if
/else
chain is skipped. The final else
block runs if none of the conditions were true
.
There’s no hard limit to how many else if
statements you can have, but if you find yourself with very long if
/else if
chains, you may want to consider refactoring to a cleaner approach, such as a switch
statement or polymorphism.
A common point of confusion is attempting to access variables declared inside an if
block from the corresponding else
block:
if (condition) {
int x = 10;
} else {
System.out.println(x); // Compile error - x is not in scope!
}
This fails because variables declared inside an if
or else
block are only in scope within that block. To use a variable in both the if
and else
sections, you must declare it outside (before) the if
statement.
if
StatementsJava has been expanding its pattern matching capabilities, making it easier to work with complex data structures. Let’s explore how pattern matching works with if
statements.
Type patterns allow you to test if an object is an instance of a particular type and, if so, create a variable of that type in one step:
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
Here, obj
is tested to see if it’s an instance of String
. If so, it’s cast to String
and assigned to the pattern variable s
, which can then be used in the if
block.
There are a few rules when using type patterns in if
statements:
instanceof
.For example:
if (getObject() instanceof String s) {
System.out.println(s); // s in scope here
} else {
System.out.println(s); //Compile error! s is definitely not a String
}
// ...
Object getObject() {
return "hi";
}
Java 21 introduced record patterns, which allow you to destructure record instances directly in the if statement. This provides a more declarative and composable way to work with data. Here’s an example:
record Book(String title, String author) {}
static void printDetails(Object obj) {
if (obj Book(String title, String author)) {
System.out.println("Title: " + title);
System.out.println("Author: " + author);
}
}
In this code, Book(String title, String author)
is a record pattern. It not only checks if obj
is an instance of Book
, but also extracts the title
and author
components directly into pattern variables. This eliminates the need for separate accessor method calls.
Record patterns can also be nested, allowing you to destructure complex object graphs in a single step:
record Book(String title, String author) {}
record Library(String name, Book bestSeller) {}
Library myLibrary = new Library("City Library", new Book("Java Programming", "John Doe"));
if (myLibrary instanceof Library(var name, Book(var title, var author))) {
System.out.println("Best seller at " + name + " is '" + title + "' by " + author);
}
In this example, we’re using a nested record pattern to match against the Library
record and its Book
component simultaneously. If the pattern matches, we get direct access to the name
, title
, and author
components without needing to use accessor methods.
Record patterns also work with generic records. The compiler will infer the type arguments when possible:
record Box<T>(T t) {}
public class GenericRecord {
static void unbox(Box<Box<Integer>> box) {
if (box instanceof Box(Box(var u))) {
System.out.println("Unboxed Integer: " + u);
}
}
public static void main(String[] args) {
unbox(new Box<>(new Box<>(8))); // Prints Unboxed Integer: 8
}
}
Here, the compiler infers that u
is an Integer
, based on the type of box
.
It’s important to note that record patterns don’t match against null
. If you need to handle potential null
values, you should do so before the pattern matching:
if (obj != null && obj instanceof Book(String title, String author)) {
System.out.println("Title: " + title);
System.out.println("Author: " + author);
}
In summary, here are some key points about record patterns:
var
keyword can be used to infer the type of a component.null
values do not match any record pattern.An important concept to understand when using pattern matching in if
statements is flow scoping. This refers to how the compiler reasons about the scope and availability of pattern variables based on the flow of control through your code.
Consider this example:
if (obj instanceof String s) {
System.out.println(s); // s is definitely a String here
} else {
System.out.println(s); // Compiler error: s might not be initialized
}
System.out.println(s); // Compiler error: s is not in scope here
Inside the if
block, s is definitely a String
, the compiler knows this because the instanceof
check must have succeeded for that block to execute. Therefore, it’s safe to use s
as a String
within this scope.
However, in the corresponding else
block, s
is not considered initialized. The compiler doesn’t assume the opposite of the if
condition, it reasons that if the else
block is executing, the instanceof
check must have failed, and so s
was never assigned a value. Attempting to use s
here results in a compile error.
Outside the if-else
statement entirely, s
is not in scope at all. Pattern variables are only accessible within the if
block where they’re declared, and in subsequent else if
or else
blocks if the compiler can prove they were definitely assigned.
Flow scoping becomes more complex when you have multiple pattern variables in play:
if (obj instanceof String s || obj instanceof Integer i) {
// s or i is in scope, but not both
} else {
// neither s nor i are in scope
}
In this case, inside the if
block, only one of s
or i
will be in scope, depending on which instanceof check succeeded. The compiler doesn’t let you use a pattern variable unless it can definitively say it was assigned.
If you need to use a pattern variable in multiple scopes, you must assign it separately:
String s = null;
if (obj instanceof String temp) {
s = temp;
}
// s is now in scope, but may be null if the if block didn't execute
This may feel like a limitation, but it is actually a powerful safety feature. By tightly controlling the scope of pattern variables, Java helps prevent common bugs and makes your code more robust.
It’s worth noting that flow scoping only applies to the declared pattern variables themselves, not the original variables. In the example above, obj
remains in scope throughout, because it was declared before the if
statement.
switch
StatementSometimes, you need to check the value of a variable or expression and execute different code depending on what that value is. If there are only a couple of options, an if-else
statement works fine:
String animal = "cat";
if(animal.equals("dog")) {
System.out.println("Woof!");
} else {
System.out.println("Meow!");
}
But what if there are many possible values to check? You could chain a bunch of if-else
statements together:
String animal = "horse";
if(animal.equals("dog")) {
System.out.println("Woof!");
} else if(animal.equals("cat")) {
System.out.println("Meow!");
} else if(animal.equals("pig")) {
System.out.println("Oink!");
} else if(animal.equals("horse")) {
System.out.println("Neigh!");
} else {
System.out.println("Unknown animal!");
}
However, this can get cumbersome and messy fast. That’s where the switch
statement comes in. It allows you to define separate code blocks for different values of a variable or expression.
Here’s the diagram for the switch
statement:
┌─────────────────────────────────────┐
│ switch (variable) │
│ ┌───────────────────────────────┐ │
│ │ case value1: │ │
│ │ // code block │ │
│ │ break; │ │
│ ├───────────────────────────────┤ │
│ │ case value2: │ │
│ │ // code block │ │
│ │ break; │ │
│ ├───────────────────────────────┤ │
│ │ case value3: │ │
│ │ // code block │ │
│ │ break; │ │
│ ├───────────────────────────────┤ │
│ │ default: │ │
│ │ // code block │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
And this is its basic syntax:
switch(variable) {
case value1:
// code to run if variable == value1
break;
case value2:
// code to run if variable == value2
break;
default:
// code to run if no case matches
}
So the animal example could be rewritten more cleanly as:
String animal = "horse";
switch(animal) {
case "dog":
System.out.println("Woof!");
break;
case "cat":
System.out.println("Meow!");
break;
case "pig":
System.out.println("Oink!");
break;
case "horse":
System.out.println("Neigh!");
break;
default:
System.out.println("Unknown animal!");
}
Each case
defines a value to compare the switch variable against. If there’s a match, the code for that case executes. The break
causes execution to jump to the end of the switch
block. If no case matches, the default
block runs.
It’s important to include a break
(or return
) statement for each case, otherwise execution falls through to the next case, which is rarely what you want. The default case doesn’t need an explicit break
since it’s the last one.
case
StatementsNot just any type can be used in a switch
. Historically, switches could only work with these integral types and their wrapper classes:
int
/Integer
byte
/Byte
short
/Short
char
/Character
Then, in later versions of Java, String
, records, and the constants of an enum
were added as an option.
Also, you can use var
in a switch
statement as long as the type resolves to one of the other permitted types:
var animal = "horse";
switch(animal) {
case "dog":
System.out.println("Woof!");
break;
case "cat":
System.out.println("Meow!");
break;
case "pig":
System.out.println("Oink!");
break;
case "horse":
System.out.println("Neigh!");
break;
default:
System.out.println("Unknown animal!");
}
In this case, animal
is inferred to be a String
based on the value assigned to it. Since String
is a valid type for a switch, using var
here is perfectly fine.
However, if you try to do something like this:
var data = 3.14;
switch(data) {
// ...
}
You will get a compilation error because data
is inferred to be a double
, which is not a permitted type for switch
statements.
About enums, let’s consider an interface Season
and an enum Weather
that implements this interface:
sealed interface Season permits Weather {}
enum Weather implements Season { SPRING, SUMMER, FALL, WINTER }
In older versions of Java, the switch statement required you to use only the simple names of the enum constants:
void oldEnumSwitch(Weather w) {
switch (w) {
case SPRING -> {
System.out.println("It's spring!");
}
case SUMMER -> {
System.out.println("It's summer!");
}
case FALL -> {
System.out.println("It's fall!");
}
case WINTER -> {
System.out.println("It's winter!");
}
}
}
This restriction worked fine for basic use cases but became cumbersome when dealing with more complex scenarios, such as combining enum types or using sealed interfaces.
In Java 21, you can now use fully qualified names of enum constants and mix them with other case labels, providing greater flexibility and allowing for more complex switch expressions:
void newEnumSwitch1(Season s) {
switch (s) {
case Weather.SPRING -> { // Qualified name of enum constant
System.out.println("It's spring!");
}
case Weather.SUMMER -> {
System.out.println("It's summer!");
}
case Weather.FALL -> {
System.out.println("It's fall!");
}
case Weather.WINTER -> {
System.out.println("It's winter!");
}
}
}
Additionally, the requirement that the selector expression be of an enum type is relaxed, allowing you to use qualified names of enum constants even if the selector expression is not of the enum type, as long as it is assignment compatible, like in the above example.
However, an invalid use case would be when the enum constant is not fully qualified:
void invalidEnumSwitch(Season s) {
switch (s) {
case SPRING -> { // Error: SPRING must be qualified as Weather.SPRING
System.out.println("It's spring!");
}
case Weather.SUMMER -> {
System.out.println("It's summer!");
}
case Weather.FALL -> {
System.out.println("It's fall!");
}
case Weather.WINTER -> {
System.out.println("It's winter!");
}
default -> {
System.out.println("Unknown season");
}
}
}
case
StatementsWhen defining the values for each case
, there are some important rules to keep in mind. The value must be a compile-time constant, meaning it has to be known at the time the code is compiled, not determined at runtime.
So you can use literal values like "dog"
or 3
, final
variables (as long as they’re initialized with a constant value), and enum
constants. But you can’t use a regular variable or a method call, even if the method always returns the same value. For example:
final int NUMBER = 2;
int getSome() {
return 1;
}
int x = 3;
switch(value) {
case NUMBER: // OK, NUMBER is final and initialized with a constant
case getSome(): // Error! Method calls aren't allowed
case x: // Error! x is not final
...
}
Sometimes, you might want to run the same code for multiple case
values. Rather than duplicating the code, you can simply list the values together for a single case:
int dayNumber;
switch(dayName) {
case "Monday":
dayNumber = 1;
break;
case "Tuesday":
dayNumber = 2;
break;
case "Saturday", "Sunday": // Runs the same code for "Saturday" and "Sunday"
dayNumber = 0;
break;
default:
throw new IllegalArgumentException("Invalid day: " + dayName);
}
I mentioned this earlier, but it’s worth reiterating, don’t forget to break
out of each case block (or use return
), unless you specifically want execution to fall through to the next case. Forgetting a break
is a common source of bugs in switch statements.
switch
ExpressionJava 14 officially introduced a new form of switch
, known as the switch
expression. It has a few key differences from the traditional switch
statement. First, here’s the syntax:
variable = switch(anotherVariable) {
case value1 -> expression1;
case value2 -> { statements; yield expression2; }
default -> expression3;
};
Instead of case:
and break
, the switch expression uses ->
to map each case to a value or block of code. If you need multiple statements for a case, use curly braces and the yield
keyword to specify the value to return.
Note the semicolons. Each case needs one at the end, as does the entire switch
expression.
The switch
expression must always return a value, and each case must cover all possibilities (either explicitly or with a default
). The data types of all the case
results must also be consistent with each other.
Here’s a more concrete example:
String animal = "horse";
String sound = switch(animal) {
case "dog" -> "Woof!";
case "cat" -> "Meow!";
case "pig" -> "Oink!";
case "horse" -> "Neigh!";
case "human" -> {
String greeting = "Hello!";
yield greeting; // Use yield when there are multiple statements
}
default -> throw new IllegalArgumentException("Unknown animal: " + animal);
};
In this case, each animal is mapped directly to the sound it makes, except for human
which has a block of code. The default
case throws an exception since the switch
expression must cover all possible input values.
switch
StatementsJava 21 introduced a powerful new feature: pattern matching in switch
statements and expressions. This allows you to test the structure of an object directly in the switch
, making your code more expressive and less error-prone.
Let’s start with a simple example:
Object obj = "Hello, World!";
String result = switch (obj) {
case Integer i -> "It's an integer: " + i;
case String s -> "It's a string: " + s;
case Double d -> "It's a double: " + d;
default -> "It's something else";
};
System.out.println(result); // Outputs: It's a string: Hello, World!
In this example, we’re switching on an Object
, and each case checks if the object is of a specific type. If it matches, we can use the variable declared in the pattern (like s
for String) directly in the case body.
We can add guards to our case labels for even more precise matching:
Object obj = 42;
String category = switch (obj) {
case Integer i when i < 0 -> "Negative integer";
case Integer i when i > 0 -> "Positive integer";
case Integer i -> "Zero";
case String s when s.length() > 5 -> "Long string";
case String s -> "Short string";
default -> "Something else";
};
System.out.println(category); // Outputs: Positive integer
The when
clause allows us to add additional conditions to our pattern matching.
However, when using pattern matching, the order of cases matters. More specific patterns should come before more general ones:
Object obj = "Hello";
String result = switch (obj) {
case String s when s.length() > 5 -> "Long string";
case String s -> "Short string";
case CharSequence cs -> "Some other CharSequence";
default -> "Not a CharSequence";
};
System.out.println(result); // Outputs: Long string
If we were to put the case String s
before case String s when s.length() > 5
, the guard would never be reached, and the compiler would warn us about an unreachable case.
Pattern matching in switch also introduces a more elegant way to handle null
values:
String str = null;
String description = switch (str) {
case null -> "It's null!";
case String s -> "It's a string of length " + s.length();
};
System.out.println(description); // Outputs: It's null!
In traditional switches, a null
value would throw a NullPointerException
. With pattern matching, we can explicitly handle the null
case.
However, you have to be careful about having only one match-all case label in a switch
block. If, for example, you add a default
case to the above example:
String str = null;
String description = switch (str) {
case null -> "It's null!";
case String s -> "It's a string of length " + s.length();
default -> "default";
};
System.out.println(description); // Outputs: It's null!
You’ll get a compilation error: switch has both an unconditional pattern and a default label
.
Having more than one match-all case labels in a switch
statement or expression generates a compile-time error. The match-all case labels are:
default
case labelHowever, the following compiles:
Object obj = null; // Notice the Object type
String description = switch (obj) {
case String s -> "It's a string of length " + s.length();
case null, default -> "It's null or not a string!";
};
System.out.println(description); // Outputs: It's null or not a string!
If a selector expression evaluates to null
and the switch block does not have null
case label, like in the following case:
Object obj = null;
String description = switch (obj) { // Throws NullPointerException
case String s -> "It's a string of length " + s.length();
default -> "It's null or not a string";
};
System.out.println(description);
Then a NullPointerException
is thrown.
Another key benefits of pattern matching in switch
is exhaustiveness checking. The compiler ensures that all possible cases are covered:
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
public class SwitchExhaustiveness {
public static void main(String[] args) {
Shape shape = new Circle(5);
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
};
System.out.println("Area: " + area);
}
}
In this example, because Shape
is a sealed interface and we’ve covered all its permitted subclasses, the compiler knows we’ve exhaustively covered all possibilities. If the expression is a sealed
type, only the classes declared in the permits
clause of the sealed
type need to be handled by the switch
.
However, if you don’t cover all the possibilities:
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
};
A compile-time error is generated: switch expression does not cover all possible input values
.
The issue can be fixed simply by adding a default
case:
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
default -> 0;
};
Finally, in a switch
statement, the compiler can also infer the type arguments for a generic record pattern. For example, taking into account the following record declaration:
record Point<T, U>(T x, U y) { }
The compiler can infer Point(var x, var y)
as Point<Long, Long>(Long x, Long x)
:
Point<Long, Long> p = new Point(1L, 2L);
switch (p) {
case Point(var x, var y) ->
System.out.println(x + ", " + y);
}
while
LoopA while
loop allows you to repeatedly execute a block of code as long as a specified boolean
condition remains true
.
Here’s the flowchart diagram for the while
statement:
┌─────────┐
│ Start │
└────┬────┘
│
┌─────┴─────┐
┌────┤ Condition │
│ └─────┬─────┘
│ │
│ ┌─────┴─────┐
│ │ Is true? ├───────┐
│ └─────┬─────┘ │
│ │ │
│ ┌─────┴─────┐ ┌─────┴─────┐
│ │ Yes │ │ No │
│ └─────┬─────┘ └─────┬─────┘
│ │ │
│ ┌─────┴─────┐ │
│ │ Execute │ │
│ │ Loop │ │
│ │ Body │ │
│ └─────┬─────┘ │
│ │ │
└──────────┘ │
│
┌────┴────┐
│ End │
└─────────┘
There are actually two variants of the while
loop in Java:
while
loopdo-while
loopThe standard while
loop has the following structure:
while(condition) {
// code block to be executed
}
The condition is a boolean
expression that is evaluated before each iteration of the loop. If the condition is true
, the code block is executed. This process repeats until the condition becomes false
.
It’s important to note that if the condition is false when the loop is first reached, the code block will not be executed at all. The loop will be skipped entirely.
Here’s an example that prints the numbers 0 through 9:
int count = 0;
while(count < 10) {
System.out.println(count);
count++;
}
The loop will continue executing until count is no longer less than 10.
The do-while
loop is similar to the standard while
loop but with one key difference: the condition is evaluated after the code block has executed. This means the code block will always execute at least once, even if the condition is initially false.
Here’s the syntax of a do-while
loop:
do {
// code block to be executed
} while(condition);
As you can see, the code block comes before the while
keyword and condition. The condition is checked after each iteration, determining whether the loop should continue or terminate.
The following example is functionally equivalent to the previous while
loop example:
int count = 0;
do {
System.out.println(count);
count++;
} while(count < 10);
Even though the structure is different, this do-while
loop achieves the same result as the standard while
loop, printing the numbers 0 through 9.
So why would you choose a do-while
loop over a standard while
loop? It really depends on the specific problem you’re trying to solve. If you know you always want the code block to execute at least once regardless of the initial condition state, a do-while
can be a good choice and can make your intention clearer. However, in many cases, a standard while
loop is sufficient and more commonly used.
It’s possible to place one loop inside the body of another loop. This is known as loop nesting. Nested loops allow you to iterate over multiple dimensions, such as the rows and columns of a 2D array.
Here’s an example that uses nested while
loops to print out a multiplication table:
int i = 1;
while(i <= 10) {
int j = 1;
while(j <= 10) {
System.out.print(i * j + "\t");
j++;
}
System.out.println();
i++;
}
The outer loop iterates from 1 to 10, representing the rows of the multiplication table. For each iteration of the outer loop, the inner loop also iterates from 1 to 10, representing the columns. The product of the current row and column values is printed, followed by a tab (\t
) character for formatting. After each row is complete, a newline is printed to move to the next row.
While this example uses while
loops, you can also nest do-while
loops in a similar manner. The choice of loop type depends on the specific requirements of your use case.
break
and continue
StatementsThe break
statement is used to immediately terminate a loop or switch statement. When encountered inside a loop, break
causes program control to transfer to the next statement after the loop.
Here’s an example of using break
in a while
loop:
int count = 0;
while(true) {
System.out.println(count);
count++;
if(count >= 5) {
break;
}
}
This loop will continue infinitely because the condition is always true. However, the break
statement inside the loop will cause it to terminate once count reaches 5.
On the other hand, the continue
statement is used to skip the rest of the current loop iteration and immediately move on to the next iteration.
Here’s an example that uses continue
:
int i = 0;
while(i < 10) {
if(i % 2 == 0) {
i++;
continue;
}
System.out.println(i);
i++;
}
This loop iterates from 0 to 9. However, when i
is even (divisible by 2), the continue
statement is executed, causing the rest of the iteration to be skipped. As a result, only the odd numbers are printed.
However, it’s important to note that using break
or continue
can sometimes lead to unreachable code, which will cause a compilation error.
Consider this example:
while(condition) {
// code block
break;
// more code
}
The code after the break
statement will never be executed because break
always causes the loop to terminate. The Java compiler will detect this and raise an “unreachable code” compilation error.
The same applies to continue
. Any code placed after a continue
statement in the same loop iteration will be unreachable.
To avoid these errors, make sure that any code placed after a break
or continue
has a chance to execute under some condition.
Finally, you can associate a label with a loop. Labels provide a way to break out of or continue a specific outer loop from within a nested loop. Here’s the syntax for adding a label to a loop:
label:
while(condition) {
// code block
}
The label is an identifier followed by a colon. It’s placed just before the loop declaration.
Here’s an example that demonstrates the use of labels:
int i = 0;
outerLoop:
while(i < 10) {
int j = 0;
while(j < 10) {
if(j == 5) {
break outerLoop;
}
System.out.println("i: " + i + ", j: " + j);
j++;
}
i++;
}
In this case, the outer loop is labeled outerLoop
. Inside the nested loop, there’s a condition that checks if j
is equal to 5. When this condition is met, the break
statement is used with the outerLoop
label, causing execution to jump out of both the inner and outer loops. Without the label, the break would only exit the inner loop.
Like break
, continue
can also be used with a label to skip to the next iteration of an outer loop.
Here’s an example that demonstrates this:
int i = 0;
outerLoop:
while(i < 3) {
int j = 0;
while(j < 3) {
if(i == 1 && j == 1) {
i++;
continue outerLoop;
}
System.out.println("i: " + i + ", j: " + j);
j++;
}
i++;
}
In this example, the outer loop is labeled outerLoop
. The outer loop iterates over the values of i
from 0 to 2, and the inner loop iterates over the values of j
from 0 to 2.
Inside the nested loops, there’s a condition that checks if both i
and j
are equal to 1. When this condition is met, the continue
statement is used with the outerLoop
label. This causes the program control to immediately jump to the next iteration of the outer loop, skipping the rest of the inner loop.
As a result, the output of this code will be:
i: 0, j: 0
i: 0, j: 1
i: 0, j: 2
i: 1, j: 0
i: 2, j: 0
i: 2, j: 1
i: 2, j: 2
Notice that the output i: 1, j: 1
is missing because when i
and j
are both 1, the continue outerLoop
statement is executed, causing the program to skip to the next iteration of the outer loop, bypassing the print statement.
Using continue
with a label is less common than using break
with a label, but it can be useful in situations where you want to skip multiple levels of nested loops based on a certain condition.
for
LoopLike while
loops, for
loops are used to repeatedly execute a block of code. However, for
loops provide a more concise syntax for iterating over a range of values or elements in a collection.
In Java, there are two types of for
loops:
for
loopfor-each
loop (also known as the enhanced for
loop)Here’s a diagram with the key points of the for
loops:
┌─────────────────────────────────────────────────────────────┐
│ Java for Loops │
│ │
│ Traditional for Loop │ for-each Loop │
│ │ │
│ for (int i = 0; i < 5; i++) │ for (int num : numbers) { │
│ { │ // code block │
│ // code block │ } │
│ } │ │
│ │ │
│ Components: │ Components: │
│ 1. Initialization │ 1. Element variable │
│ 2. Condition │ 2. Collection to iterate │
│ 3. Update statement │ │
│ │ │
│ Use when: │ Use when: │
│ - Need index │ - Don't need index │
│ - Custom increments │ - Iterating full collection │
│ - Multiple counters │ - Simpler syntax preferred │
└─────────────────────────────────────────────────────────────┘
Let’s start by examining in more detail the traditional for
loop.
for
LoopThe traditional for
loop has the following structure:
for(initialization; booleanExpression; updateStatement) {
// code block to be executed
}
The loop consists of three parts separated by semicolons:
Initialization: This is where you initialize the loop variable(s). It’s executed only once at the beginning of the loop.
Boolean Expression: This is the condition that’s checked before each iteration. If it evaluates to true
, the loop continues. If it’s false
, the loop terminates.
Update Statement: This is where you specify how the loop variable(s) should be updated after each iteration. It’s executed at the end of each iteration.
Here’s a simple example that prints the numbers 0 to 4:
for(int i = 0; i < 5; i++) {
System.out.println(i);
}
The loop initializes i
to 0, checks if i
is less than 5, and if so, executes the code block (printing the value of i
). After each iteration, i
is incremented by 1. The loop continues until i
is no longer less than 5.
You can also use the var
keyword in the initialization part:
for(var i = 0; i < 5; i++) {
System.out.println(i);
}
If you omit the boolean expression, it defaults to true
, creating an infinite loop:
for(int i = 0; ; i++) {
System.out.println(i);
}
This loop will continue indefinitely because there’s no condition to make it false
. To stop an infinite loop, you’d need to use a break
statement or some other means of interrupting the loop.
You can initialize multiple variables and include multiple update statements in a for
loop by separating them with commas:
for(int i = 0, j = 10; i < j; i++, j--) {
System.out.println("i: " + i + ", j: " + j);
}
This loop initializes i
to 0 and j
to 10, checks if i
is less than j
, and if so, executes the code block. After each iteration, i
is incremented, and j
is decremented.
It’s important to note that you cannot redeclare a variable in the initialization block of a for
loop:
int i = 0;
for(int i = 0; i < 5; i++) { // Doesn't compile
System.out.println(i);
}
This code will not compile because i
is declared twice. If you need to use a variable that’s already declared, simply omit the data type in the initialization block:
int i = 0;
for(i = 0; i < 5; i++) { // OK
System.out.println(i);
}
Also, all variables declared in the initialization block must be of the same data type or compatible types:
for(int i = 0, long j = 10; i < j; i++, j--) { // Doesn't compile
System.out.println("i: " + i + ", j: " + j);
}
This code will not compile because i
is of type int
, and j
is of type long
. Here is the corrected example:
for(int i = 0, j = 10; i < j; i++, j--) {
System.out.println("i: " + i + ", j: " + j);
}
Alternatively, if you need to use different data types, you should declare them before the loop:
int i = 0;
long j = 10;
for(; i < j; i++, j--) {
System.out.println("i: " + i + ", j: " + j);
}
Regarding the scope of a variable declared in the initialization block, it is limited to the for
loop. You cannot use it outside the loop:
for(int i = 0; i < 5; i++) {
System.out.println(i);
}
System.out.println(i); // Doesn't compile
Once again, if you need to use the final value of the loop variable after the loop, you must declare it before the loop:
int i;
for(i = 0; i < 5; i++) {
System.out.println(i);
}
System.out.println(i); // OK, prints 5
In many cases, you may need to compare the current loop variable with other elements in the loop. The traditional for
loop makes this possible by allowing you to read elements forward or backward:
int[] arr = {1,2,3,4,5};
for(int i = 0; i < arr.length; i++) {
// Read forward
if(i < arr.length - 1) {
System.out.println("Current: " + arr[i] + ", Next: " + arr[i+1]);
}
// Read backward
if(i > 0) {
System.out.println("Current: " + arr[i] + ", Previous: " + arr[i-1]);
}
}
The forward reading if condition checks if the current element is not the last one and if so, it prints the current element and the next one.
The backward reading if condition checks if the current element is not the first one and if so, it prints the current element and the previous one.
for-each
LoopThe for-each
loop, also known as the enhanced for
loop, provides a simpler way to iterate over arrays and collections. It eliminates the need to explicitly declare and update loop variables.
The structure of a for-each
loop is as follows:
for(dataType item : collection) {
// code block to be executed
}
The loop consists of two parts with three elements:
dataType: The data type of the elements in the collection.
item: A variable that will hold the current element during each iteration.
collection: The array or collection to be iterated over.
Here’s an example that prints the elements of an array using a for-each
loop:
int[] numbers = {1, 2, 3, 4, 5};
for(int num : numbers) {
System.out.println(num);
}
In each iteration, the loop assigns the next element of the numbers array to the num
variable and executes the code block.
The for-each
loop can be used with with arrays and with any object that implements the Iterable
interface, which includes most collection classes, such as ArrayList
and HashSet
.
If you’re wondering if everything that applies to for
loops applies to for-each
loops, the answer is not quite. While for
loops and for-each
loops share some similarities, there are a few key differences in how they behave and what they can do:
Iteration: A for-each
loop automatically iterates over all elements in an array or collection, from the first to the last. You don’t have control over the index or the order of iteration. A traditional for
loop, on the other hand, gives you full control over the initialization, condition, and update statements, allowing you to iterate in any order or skip elements.
Modification: A for-each
loop does not prevent you from modifying the elements of the array or collection within the loop, but it does not provide direct access to the index. You can modify the elements if the underlying collection supports modification. In contrast, a traditional for
loop allows you to modify elements by accessing them via their index.
Iterating over arrays and collections: A for-each
loop can be used to iterate over arrays and any object that implements the Iterable
interface, which includes most collection classes. A traditional for
loop can be used to iterate over arrays and collections, but you need to use an explicit index or iterator.
Accessing index: In a for-each
loop, you don’t have direct access to the index of the current element. If you need the index, you’ll have to use a traditional for
loop, which gives you access to the index through the loop variable.
Performance: For arrays, the performance difference between a for-each
loop and a traditional for loop is generally negligible. For collections, the performance is similar as a for-each loop
is syntactic sugar for using an iterator.
Here’s an example that demonstrates a situation where a for-each
loop cannot be used:
int[] numbers = {1, 2, 3, 4, 5};
for(int i = 0; i < numbers.length; i++) {
if(numbers[i] % 2 == 0) {
numbers[i] *= 2; // Double even numbers
}
}
In this case, we need to modify the elements of the array based on a condition. We also need access to the index to perform the modification. This cannot be done with a for-each
loop.
However, if we just needed to print the doubled even numbers, a for-each
loop would be suitable:
int[] numbers = {1, 2, 3, 4, 5};
for(int num : numbers) {
if(num % 2 == 0) {
System.out.println(num * 2); // Print doubled even numbers
}
}
In summary, for-each
loops provide a concise syntax for iterating over all elements, while for loops offer more control and flexibility, allowing you to access indexes and iterate in custom ways.
Just like while
loops, for
loops can also be nested. This allows you to iterate over multidimensional arrays or perform complex iterations.
int[][] matrix = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
for(int[] row : matrix) {
for(int cell : row) {
System.out.print(cell + " ");
}
System.out.println();
}
This code uses two nested for-each
loops to iterate through a 2D array. The outer loop iterates over each row, and the inner loop iterates over each cell in the current row.
break
and continue
StatementsThe break
statement can be used in for
loops to prematurely terminate the loop.
int[] numbers = {1, 2, 3, 4, 5};
for(int num : numbers) {
if(num == 3) {
break;
}
System.out.println(num);
}
In this example, the loop will terminate when num
is equal to 3. The output will be:
1
2
On the other hand, the continue
statement can be used in for
loops to skip the rest of the current iteration and move on to the next one.
int[] numbers = {1, 2, 3, 4, 5};
for(int num : numbers) {
if(num % 2 == 0) {
continue;
}
System.out.println(num);
}
This loop will print only the odd numbers in the array. When num
is even, the continue
statement is executed, and the rest of the iteration is skipped.
Also, using break
or continue
in for
loops can sometimes lead to unreachable code, resulting in compilation errors.
for(int i = 0; i < 10; i++) {
System.out.println(i);
break;
System.out.println("Unreachable"); // Unreachable code
}
In this example, the code after the break
statement is unreachable because break
always causes the loop to terminate. The Java compiler will detect this and throw a compilation error.
The same principle applies to continue
. Any code after a continue
statement in the same iteration will be unreachable.
To avoid these errors, ensure that any code placed after a break
or continue
statement has a chance to execute under some condition.
Labels can be added to for
loops in the same way as while
loops. They are useful for breaking out of or continuing outer loops from within nested loops:
int[][] matrix = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
outerLoop:
for(int[] row : matrix) {
for(int cell : row) {
if(cell == 5) {
break outerLoop;
}
System.out.print(cell + " ");
}
System.out.println();
}
In this example, the outer loop is labeled as outerLoop
. When the value of cell
is 5, the break
statement is used with the outerLoop
label, causing the program to terminate both the inner and outer loops. This is what happens:
(1, 2, 3)
of matrix
.
(4, 5, 6)
.
break outerLoop;
is executed.The program ends at this point. The output is:
1 2 3
4
The third row (7, 8, 9)
is never processed because the loops were terminated early.
The if
statement allows your program to conditionally execute a block of code based on a boolean condition.
The basic syntax of an if
statement is: if (condition) { code }
. The code block executes if the condition is true.
You can chain multiple conditions using else if
. The conditions are checked in order until one is true or the else
block is reached.
Variables declared inside an if
or else
block are only in scope within that block.
if
statements can use pattern matching with the instanceof
operator, assigning the matched object to a pattern variable for use in the if
block.
Java 21 introduced record patterns, allowing destructuring of record instances directly in if
statements.
Record patterns can be nested, enabling the destructuring of complex object graphs in a single step.
Pattern matching doesn’t match against null
values.
The scope of pattern variables is tightly controlled by the compiler based on flow scoping rules to prevent bugs.
The switch
statement allows executing different code blocks based on the value of a variable or expression.
Java 21 allows the use of fully qualified names of enum constants in switch
statements.
Enum constants can now be mixed with other case labels in the same switch
.
The requirement for the selector expression to be of an enum type is relaxed, allowing use of qualified names of enum constants even if the selector is not of the enum type (but is assignment compatible).
Each case
in a switch
defines a value to compare against. If there’s a match, that case’s code block executes.
Include a break
statement at the end of each case block to prevent fall-through, unless fall-through is desired.
switch
statements can work with String
, enum
constants, and integral types like int
, char
, etc.
Case values must be compile-time constants.
Java 14 officially introduced switch
expressions, which use ->
to map cases to result values and must cover all input possibilities.
Java 21 introduced pattern matching in switch
statements and expressions. his allows testing the structure of an object directly in the switch
.
You can use type patterns, record patterns, and add guards with when
clauses for more precise matching.
The order of cases matters; more specific patterns should come before more general ones.
Pattern matching in switch
introduces a way to handle null
values explicitly.
The compiler ensures exhaustiveness in switch
statements and expressions: The switch
block must have clauses that deal with all possible values of the selector expression.
The while
loop repeatedly executes a code block as long as its boolean condition remains true.
If the condition is initially false
, the code block will not execute at all.
The do-while
loop is similar but the condition is checked after each iteration, so the code block always executes at least once.
You can nest one loop inside another to iterate over multiple dimensions.
The break
statement immediately terminates a loop, while continue
skips to the next iteration.
You can give a loop a label and then use break
or continue
with that label to break out of or continue an outer labeled loop.
The for
loop provides a concise syntax for iterating over a range of values.
The traditional for
loop has an initialization, condition, and update statement. The code block executes repeatedly until the condition is false
.
Variables declared in the initialization block are limited in scope to the for
loop.
The for-each
loop (enhanced for
loop) simplifies iterating over arrays/collections, eliminating the need for explicit indexing.
for-each
can’t be used if you need the index or want to iterate in a custom order. Use a traditional for
loop in those cases.
You can use break
/continue
and labels with for
loops just like with while
loops.
Avoid unreachable code after break
or continue
statements.
1. What will be the output of the following program?
public class IfStatementTest {
public static void main(String[] args) {
int x = 10;
if (x > 5) {
if (x < 20) {
System.out.println("x is between 5 and 20");
}
} else {
System.out.println("x is 5 or less");
}
}
}
A) x is between 5 and 20
B) x is 5 or less
C) x is greater than 20
D) The program does not compile
E) The program compiles but does not produce any output
2. Given the following code:
record Person(String name, int age) {}
record Employee(int id, Person person) {}
public class RecordPattern {
public static void main(String[] args) {
Employee emp = new Employee(1001, new Person("Alice", 30));
// Insert code here
}
}
Which of the following options correctly uses record pattern matching in an if
statement to extract and print the name and age of a Person
record in Java 21?
A)
if (emp instanceof Employee) {
var (id, Person(name, age)) = emp;
System.out.println(name + " is " + age + " years old.");
}
B)
if (emp instanceof Employee(_, Person(var name, var age))) {
System.out.println(name + " is " + age + " years old.");
}
C)
if (emp instanceof Employee e) {
System.out.println(e.person().name() + " is " + e.person().age() + " years old.");
}
D)
if (emp instanceof Employee(var id, Person(var name, var age))) {
System.out.println(name + " is " + age + " years old.");
}
E)
if (emp instanceof Employee(var id, var person)) {
System.out.println(person.name() + " is " + person.age() + " years old.");
}
3. Which of the following code snippets compile without error?
public class FlowScopingTest {
public static void main(String[] args) {
int x = 10;
if (x > 5) {
int y = x * 2;
}
// Code snippet 1
System.out.println(y);
if (x < 20) {
int z = x + 5;
}
// Code snippet 2
z += 5;
int a = 5;
if (a > 0) {
a = 15;
}
// Code snippet 3
System.out.println(a);
if (x > 0) {
int b = x + 3;
if (b > 15) {
b -= 2;
}
}
// Code snippet 4
System.out.println(b);
}
}
A) Code snippet 1
B) Code snippet 2
C) Code snippet 3
D) None of the above
4. What will be the output of the following program?
public class SwitchTest {
public static void main(String[] args) {
int dayOfWeek = 3;
String dayType;
switch (dayOfWeek) {
case 1:
case 7:
dayType = "Weekend";
break;
case 2:
case 3:
case 4:
case 5:
case 6:
dayType = "Weekday";
break;
default:
dayType = "Invalid day";
}
System.out.println(dayType);
}
}
A) Weekend
B) Invalid day
C) Weekday
D) The program does not compile
E) The program compiles but does not produce any output
5. What will be the output of the following program?
public class SwitchExpressionTest {
public static void main(String[] args) {
int score = 85;
String grade = switch (score) {
case 90, 100 -> "A";
case 80, 89 -> "B";
case 70, 79 -> "C";
case 60, 69 -> "D";
default -> "F";
};
System.out.println(grade);
}
}
A) A
B) F
C) The program does not compile
D) B
E) The program compiles but does not produce any output
6. Given the following code:
public class SwitchEnums {
sealed interface Vehicle permits CarType {}
enum CarType implements Vehicle { SEDAN, SUV, HATCHBACK, CONVERTIBLE }
void processVehicle(Vehicle v) {
switch(v) {
// Insert case statements here
}
}
}
Which of the following case
statements are valid in Java 21 when inserted in the switch
expression?
A)
case CarType.SEDAN, CarType.HATCHBACK -> System.out.println("Compact vehicle");
case CarType.SUV -> System.out.println("Large vehicle");
case CarType.CONVERTIBLE -> System.out.println("Open-top vehicle");
B)
case SEDAN, HATCHBACK -> System.out.println("Compact vehicle");
case SUV -> System.out.println("Large vehicle");
case CONVERTIBLE -> System.out.println("Open-top vehicle");
C)
case CarType.SEDAN || CarType.HATCHBACK -> System.out.println("Compact vehicle");
case CarType.SUV -> System.out.println("Large vehicle");
case CarType.CONVERTIBLE -> System.out.println("Open-top vehicle");
D)
case Vehicle.SEDAN, Vehicle.HATCHBACK -> System.out.println("Compact vehicle");
case Vehicle.SUV -> System.out.println("Large vehicle");
case Vehicle.CONVERTIBLE -> System.out.println("Open-top vehicle");
7. Given the following code:
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
Shape shape = new Circle(5);
double area = switch (shape) {
// Insert case statements here
};
Which of the following case
statements correctly implements pattern matching for the Shap
e hierarchy when inserted in the switch
expression?
A)
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case null -> 0;
B)
default -> 0;
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Triangle t -> 0.5 * t.base() * t.height();
C)
case Shape s when s instanceof Circle ->
Math.PI * ((Circle)s).radius() * ((Circle)s).radius();
case Shape s when s instanceof Square ->
((Square)s).side() * ((Square)s).side();
case Shape s when s instanceof Triangle ->
0.5 * ((Triangle)s).base() * ((Triangle)s).height();
D)
case Circle c -> Math.PI * c.radius() * c.radius();
case Square s -> s.side() * s.side();
case Triangle t -> 0.5 * t.base() * t.height();
8. What will be the output of the following program?
public class LabeledBreakTest {
public static void main(String[] args) {
int count = 0;
outerLoop:
while (count < 5) {
while (true) {
count++;
if (count == 3) {
break outerLoop;
}
}
}
System.out.println(count);
}
}
A) 2
B) 3
C) 4
D) 5
E) The program does not compile
9. What will be the output of the following program?
public class ForLoopTest {
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
}
System.out.println(sum);
}
}
A) 5
B) 10
C) 15
D) 20
E) The program does not compile
10. What will be the output of the following program?
public class EnhancedForLoopTest {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
int sum = 0;
for (int num : numbers) {
if (num % 2 == 0) {
continue;
}
sum += num;
}
System.out.println(sum);
}
}
A) 9
B) 10
C) 12
D) 15
E) The program does not compile
Do you like what you read? Would you consider?
Do you have a problem or something to say?