Oracle is proud to announce the general availabilty of JDK 17. This release is the eighth Feature Release delivered on time through the six-month release cadence. This level of predictability allows developers to easily manage their adoption of innovation thanks to s steady stream of expected changes.
Wednesday, August 24, 2022
The arrival of Java 17!
Wednesday, June 8, 2022
Bruce Eckel on Java pattern matching guards and dominance
Pattern matching guards let you refine the matching condition beyond simply matching on the type.
The previous article in this series, “Bruce Eckel on pattern matching in Java,” introduced pattern matching and this article delves into the details. Keep in mind that, at the time of this writing, pattern matching for switch is a second preview feature in JDK 18 as JEP 420, and the Java team intends to add additional features.
Guards
A guard allows you to refine the matching condition beyond simply matching on the type. It is a test that appears after the type and &&. The guard can be any Boolean expression. If the selector expression is the same as the type for the case and the guard evaluates to true, the pattern matches, as follows:
// enumerations/Shapes.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
// Run with java flag: --enable-preview
import java.util.*;
sealed interface Shape {
double area();
}
record Circle(double radius) implements Shape {
@Override public double area() {
return Math.PI * radius * radius;
}
}
record Rectangle(double side1, double side2)
implements Shape {
@Override public double area() {
return side1 * side2;
}
}
public class Shapes {
static void classify(Shape s) {
System.out.println(switch(s) {
case Circle c && c.area() < 100.0
-> "Small Circle: " + c;
case Circle c -> "Large Circle: " + c;
case Rectangle r && r.side1() == r.side2()
-> "Square: " + r;
case Rectangle r -> "Rectangle: " + r;
});
}
public static void main(String[] args) {
List.of(
new Circle(5.0),
new Circle(25.0),
new Rectangle(12.0, 12.0),
new Rectangle(12.0, 15.0)
).forEach(t -> classify(t));
}
}
/* Output:
Small Circle: Circle[radius=5.0]
Large Circle: Circle[radius=25.0]
Square: Rectangle[side1=12.0, side2=12.0]
Rectangle: Rectangle[side1=12.0, side2=15.0]
*/
(The {NewFeature} comment tag excludes this example from the Gradle build that uses JDK 8.)
The first guard for Circle determines whether that Circle is small. The first guard for Rectangle determines whether that Rectangle is a square.
Here’s a more complex example: A Tank can hold different types of liquids, and the Level of the tank must be between zero and 100%.
// enumerations/Tanks.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
// Run with java flag: --enable-preview
import java.util.*;
enum Type { TOXIC, FLAMMABLE, NEUTRAL }
record Level(int percent) {
Level {
if(percent < 0 || percent > 100)
throw new IndexOutOfBoundsException(
percent + " percent");
}
}
record Tank(Type type, Level level) {}
public class Tanks {
static String check(Tank tank) {
return switch(tank) {
case Tank t && t.type() == Type.TOXIC
-> "Toxic: " + t;
case Tank t && ( // [1]
t.type() == Type.TOXIC &&
t.level().percent() < 50
) -> "Toxic, low: " + t;
case Tank t && t.type() == Type.FLAMMABLE
-> "Flammable: " + t;
// Equivalent to "default":
case Tank t -> "Other Tank: " + t;
};
}
public static void main(String[] args) {
List.of(
new Tank(Type.TOXIC, new Level(49)),
new Tank(Type.FLAMMABLE, new Level(52)),
new Tank(Type.NEUTRAL, new Level(75))
).forEach(
t -> System.out.println(check(t))
);
}
}
The record Level includes a compact constructor that ensures that percent is valid. Records were introduced in a previous article, “Bruce Eckel on Java records,” in this series.
Here’s a note for line [1]: If a guard contains multiple expressions, simply enclose it in parentheses.
Since the code switches on Tank rather than Object, the final case Tank acts the same as a default because it catches all Tank cases that don’t match any of the other patterns.
Dominance
The order of the case statements in a switch can be important because if the base type appears first, it dominates anything appearing afterwards.
// enumerations/Dominance.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
import java.util.*;
sealed interface Base {}
record Derived() implements Base {}
public class Dominance {
static String test(Base base) {
return switch(base) {
case Derived d -> "Derived";
case Base b -> "B"; // [1]
};
}
}
The base type Base is in last place, at line [1]—and that’s where it should be. But if you move that line up, the base type will appear before case Derived, which would mean that the switch would never be able to test for Derived because any derived class would then be captured by case Base. If you try this experiment, the compiler reports an error: this case label is dominated by a preceding case label.
Order sensitivity often appears when you use guards. Moving the final case in Tanks.java to a higher position in the switch produces that same domination error message. When you have multiple guards on the same pattern, more-specific patterns must appear before more-general patterns. Otherwise, a more-general pattern will match before more-specific patterns, and the latter will never be checked. Fortunately, the compiler reports dominance problems.
The compiler can detect dominance problems only when the type in a pattern dominates the type in another pattern. The compiler cannot know whether the logic in guards produces problems.
// enumerations/People.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
// Run with java flag: --enable-preview
import java.util.*;
record Person(String name, int age) {}
public class People {
static String categorize(Person person) {
return switch(person) {
case Person p && p.age() > 40 // [1]
-> p + " is middle aged";
case Person p &&
(p.name().contains("D") || p.age() == 14)
-> p + " D or 14";
case Person p && !(p.age() >= 100) // [2]
-> p + " is not a centenarian";
case Person p -> p + " Everyone else";
};
}
public static void main(String[] args) {
List.of(
new Person("Dorothy", 15),
new Person("John Bigboote", 42),
new Person("Morty", 14),
new Person("Morty Jr.", 1),
new Person("Jose", 39),
new Person("Kane", 118)
).forEach(
p -> System.out.println(categorize(p))
);
}
}
/* Output:
Person[name=Dorothy, age=15] D or 14
Person[name=John Bigboote, age=42] is middle aged
Person[name=Morty, age=14] D or 14
Person[name=Morty Jr., age=1] is not a centenarian
Person[name=Jose, age=39] is not a centenarian
Person[name=Kane, age=118] is middle aged
*/
The guard in pattern line [2] seems like it would match Kane at age 118, but instead Kane matches with the pattern at line [1]. You cannot rely on the compiler to help with the logic of your guard expressions.
Without the last case Person p, the compiler complains that the switch expression does not cover all possible input values. With that case, a default is still not required, so the most general case becomes the default. Because the argument to the switch is a Person, all cases are covered (except for null).
Coverage
Pattern matching naturally guides you toward using the sealed keyword. This helps ensure that you’ve covered all possible types passed into the selector expression. See how that works in practice with the following example:
// enumerations/SealedPatternMatch.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
// Run with java flag: --enable-preview
import java.util.*;
sealed interface Transport {};
record Bicycle(String id) implements Transport {};
record Glider(int size) implements Transport {};
record Surfboard(double weight) implements Transport {};
// If you uncomment this:
// record Skis(int length) implements Transport {};
// You get an error: "the switch expression
// does not cover all possible input values"
public class SealedPatternMatch {
static String exhaustive(Transport t) {
return switch(t) {
case Bicycle b -> "Bicycle " + b.id();
case Glider g -> "Glider " + g.size();
case Surfboard s -> "Surfboard " + s.weight();
};
}
public static void main(String[] args) {
List.of(
new Bicycle("Bob"),
new Glider(65),
new Surfboard(6.4)
).forEach(
t -> System.out.println(exhaustive(t))
);
try {
exhaustive(null); // Always possible! // [1]
} catch(NullPointerException e) {
System.out.println("Not exhaustive: " + e);
}
}
}
/* Output:
Bicycle Bob
Glider 65
Surfboard 6.4
Not exhaustive: java.lang.NullPointerException
*/
The sealed interface Transport is implemented using record objects, which are automatically final. The switch covers all possible types of Transport, and if you add a new type, the compiler detects it and tells you that you haven’t exhaustively covered all possible patterns. But line [1] shows that there’s still one case that the compiler doesn’t insist you cover: null.
If you remember to explicitly add a case null, you’ll prevent the exception. But the compiler doesn’t help you here, possibly because that would affect too much existing switch code.
Source: oracle.com
Tuesday, May 24, 2022
Bruce Eckel on pattern matching in Java
You can currently use pattern variables for instanceof and for switch. See how they work.
JDK 16 finalized JDK Enhancement Proposal (JEP) 394, Pattern matching for instanceof. Below you can see the old and new ways of doing the same thing.
// enumerations/SmartCasting.java
// {NewFeature} Since JDK 16
public class SmartCasting {
static void dumb(Object x) {
if(x instanceof String) {
String s = (String)x;
if(s.length() > 0) {
System.out.format(
"%d %s%n", s.length(), s.toUpperCase());
}
}
}
static void smart(Object x) {
if(x instanceof String s && s.length() > 0) {
System.out.format(
"%d %s%n", s.length(), s.toUpperCase());
}
}
static void wrong(Object x) {
// "Or" never works:
// if(x instanceof String s || s.length() > 0) {}
// error: cannot find symbol ^
}
public static void main(String[] args) {
dumb("dumb");
smart("smart");
}
}
/* Output:
4 DUMB
5 SMART
*/
(The {NewFeature} comment tag excludes this example from the Gradle build that uses JDK 8.)
In dumb(), once instanceof establishes that x is a String, you must explicitly cast it to String s. Otherwise, you would be inserting casts throughout the rest of the function. But in smart(), notice the x instanceof String s.
This automatically creates a new variable s of type String. Note that s is available throughout the scope, even within the remainder of the if conditional, as you see in && s.length() > 0. This produces more-compact code.
In constrast, wrong() shows that only && can be used in a pattern-matching if expression. Using || would mean that x is an instanceof String or that s.length() > 0. That would mean x might not be a String, in which case Java could not smart-cast x to create s; therefore, s would not be available on the right side of the ||.
JEP 394 calls s a pattern variable.
Although this feature does clean up some messy if statements, that wasn’t the motivation for adding it. It was included as a building block for pattern matching, as you will see shortly.
By the way, this feature can produce some strange scoping behavior such as the following:
// enumerations/OddScoping.java
// {NewFeature} Since JDK 16
public class OddScoping {
static void f(Object o) {
if(!(o instanceof String s)) {
System.out.println("Not a String");
throw new RuntimeException();
}
// s is in scope here!
System.out.println(s.toUpperCase()); // line [1]
}
public static void main(String[] args) {
f("Curiouser and Curiouser");
f(null);
}
}
/* Output:
CURIOUSER AND CURIOUSER
Not a String
Exception in thread "main" java.lang.RuntimeException
at OddScoping.f(OddScoping.java:8)
at OddScoping.main(OddScoping.java:15)
*/
In line [1], s is in scope only if you don’t include the statement that throws the exception. If you comment out throw new RuntimeException(), the compiler tells you it can’t find s in line [1], which is the behavior you normally would expect.
Initially this might look like a bug, but it is designed that way; this behavior is explicitly described in JEP 394. Although this is arguably a corner case, you can imagine how difficult it might be to track down a bug caused by this behavior.
Pattern matching
Now that all the foundational pieces are in place, you can look at the bigger picture of pattern matching in Java.
At the time of this writing, pattern matching for instanceof was delivered in JDK 16 as JEP 394. Pattern matching for switch was a preview feature in JDK 17 as JEP 406, and there was a second preview in JDK 18 as JEP 420. This means pattern matching probably won’t change or change significantly in future versions, but it hasn’t been finalized. By the time you read this, the features shown here could be out of preview and so you might not need to use the compiler and runtime flags specified in the example comments.
Violating the Liskov Substitution Principle
Inheritance-based polymorphism performs type-based behavior but requires those types to be in the same inheritance hierarchy. Pattern matching allows you to perform type-based behavior on types that don’t all have the same interface or are not in the same hierarchy.
This is a different way to use reflection. You still determine types at runtime, but this is a more formalized and structured way to do it than reflection.
If the types of interest all have a common base type and use only methods defined in that common base type, you are conforming to the Liskov Substitution Principle (LSP). In this case, pattern matching is unnecessary, because you can just use normal inheritance polymorphism, as follows:
// enumerations/NormalLiskov.java
import java.util.stream.*;
interface LifeForm {
String move();
String react();
}
class Worm implements LifeForm {
@Override public String move() {
return "Worm::move()";
}
@Override public String react() {
return "Worm::react()";
}
}
class Giraffe implements LifeForm {
@Override public String move() {
return "Giraffe::move()";
}
@Override public String react() {
return "Giraffe::react()";
}
}
public class NormalLiskov {
public static void main(String[] args) {
Stream.of(new Worm(), new Giraffe())
.forEach(lf -> System.out.println(
lf.move() + " " + lf.react()));
}
}
/* Output:
Worm::move() Worm::react()
Giraffe::move() Giraffe::react()
*/
All methods are neatly defined in the LifeForm interface, and no new methods are added in any of the implemented classes.
But what if you need to add methods that can’t easily be placed in the base type? Some worms can reproduce when they are divided, for example, and a giraffe certainly can’t do that. A giraffe can kick, but it’s hard to imagine how you’d represent that in the base class in a way that it didn’t make the Worm implementation problematic.
The Java Collections library ran into this problem and attempted to solve it by adding optional methods in the base type that were implemented in some subclasses but not in others. This approach conformed to LSP but produced a confusing design.
Java was fundamentally inspired by Smalltalk, a dynamic language that reuses code by taking existing classes and adding methods. A Smalltalk design for a Pet hierarchy might end up looking like the following:
// enumerations/Pet.java
public class Pet {
void feed() {}
}
class Dog extends Pet {
void walk() {}
}
class Fish extends Pet {
void changeWater() {}
}
You can take the basic Pet functionality and extend that class by adding methods as you need them. This is different than what is normally advocated for Java (and shown in NormalLiskov.java), where you carefully design the base type to include all possible methods needed throughout the hierarchy, thus conforming to LSP. Although this is a nice aspirational goal, it can be impractical.
Attempting to force the dynamically typed Smalltalk model into the statically typed Java system is bound to create compromises. In some cases, those compromises might be unworkable. Pattern matching allows you to use Smalltalk’s approach of adding new methods to derived classes while still maintaining most of the formality of LSP. Basically, pattern matching allows you to violate LSP without creating unmanageable code.
With pattern matching, you can deal with the non-LSP nature of the Pet hierarchy by checking for, and writing different code for, each possible type.
// enumerations/PetPatternMatch.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
import java.util.*;
public class PetPatternMatch {
static void careFor(Pet p) {
switch(p) {
case Dog d -> d.walk();
case Fish f -> f.changeWater();
case Pet sp -> sp.feed();
};
}
static void petCare() {
List.of(new Dog(), new Fish())
.forEach(p -> careFor(p));
}
}
The p in switch(p) is called the selector expression. Prior to pattern matching, a selector expression could be only an integral primitive type (char, byte, short, or int), the corresponding boxed form (Character, Byte, Short, or Integer), String, or an enum type. With pattern matching, the selector expression is expanded to include any reference type. Here, the selector expression can be a Dog, Fish, or Pet.
Notice that this is similar to dynamic binding within an inheritance hierarchy, but instead of putting the code for different types within the overridden methods, you’ll put it in the different case expressions.
The compiler forced the addition of case Pet because that class can legitimately exist without being a Dog or a Fish. Without case Pet, then, the switch didn’t cover all possible input values. Using an interface for the base type eliminates this constraint but adds a different one. The following example is placed in its own package to prevent name clashes:
// enumerations/PetPatternMatch2.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
package sealedpet;
import java.util.*;
sealed interface Pet {
void feed();
}
final class Dog implements Pet {
@Override public void feed() {}
void walk() {}
}
final class Fish implements Pet {
@Override public void feed() {}
void changeWater() {}
}
public class PetPatternMatch2 {
static void careFor(Pet p) {
switch(p) {
case Dog d -> d.walk();
case Fish f -> f.changeWater();
};
}
static void petCare() {
List.of(new Dog(), new Fish())
.forEach(p -> careFor(p));
}
}
If Pet is not sealed, the compiler complains that the switch statement does not cover all possible input values. In this case, it’s because the interface Pet could be implemented by anyone else in any other file, breaking the exhaustive coverage by the switch statement. By making Pet sealed, the compiler can ensure that the switch covers all possible Pet types.
Pattern matching doesn’t constrain you to a single hierarchy the way inheritance polymorphism does; that is, you can match on any type. For example, to do this, you can pass Object into the switch as follows:
// enumerations/ObjectMatch.java
// {NewFeature} Preview in JDK 17
// Compile with javac flags:
// --enable-preview --source 17
// Run with java flag: --enable-preview
import java.util.*;
record XX() {}
public class ObjectMatch {
static String match(Object o) {
return switch(o) {
case Dog d -> "Walk the dog";
case Fish f -> "Change the fish water";
case Pet sp -> "Not dog or fish";
case String s -> "String " + s;
case Integer i -> "Integer " + i;
case String[] sa -> String.join(", ", sa);
case null, XX xx -> "null or XX: " + xx;
default -> "Something else";
};
}
public static void main(String[] args) {
List.of(new Dog(), new Fish(), new Pet(),
"Oscar", Integer.valueOf(12),
Double.valueOf("47.74"),
new String[]{ "to", "the", "point" },
new XX()
).forEach(
p -> System.out.println(match(p))
);
}
}
/* Output:
Walk the dog
Change the fish water
Not dog or fish
String Oscar
Integer 12
Something else
to, the, point
null or Object: XX[]
*/
When passing an Object parameter to switch, a default is required by the compiler—again, to cover all possible input values (except for null, for which a case is not required by the compiler, even though it can happen).
It’s possible to combine the null case with a pattern, as seen in case null, XX xx. This works because an object reference can be null.
Source: oracle.com
Friday, February 18, 2022
Java’s new HTTP Client API, UNIX-domain sockets, and Simple Web Server
Download a PDF of this article
Networking capabilities were built into Java’s core libraries right from the start. For developers, the fact that networking was built into a major development platform was a welcome change, because organizations didn’t have to rely on third-party libraries or invest resources to create their own low-level networking code to support common networking protocols.
Of course, in the 1990s, not every application needed networking—but now networking support is an absolute requirement for nearly all platforms. Indeed, most of today’s most popular business and consumer applications wouldn’t exist without the ability to connect to services using the HTTP protocol.
That said, Java’s very widely used HTTP Client API hadn’t seen a significant improvement until the new API started to incubate in JDK 9 as JEP 110 and was made standard in JDK 11 as JEP 321.
David Delabassée, a developer advocate in the Java Platform Group at Oracle, recently spoke with Oracle Java architects Daniel Fuchs and Michael McMahon to discuss some of the recent network updates to the Java platform. Here are a few of the highlights from that conversation.
Delabassée: Why was the new Java HTTP Client API introduced?
McMahon: The original, 25-year-old Java HTTP Client API is old and has limitations. It was hard to use and hard to maintain. Part of the API supported protocols that aren’t used anymore, such as Gopher, which adds to complexity.
Plus, the old client is blocking and ties up a thread for the entire duration of a request, regardless of how long that is going to take. In addition, the old client was for the HTTP/1.1 protocol and needed to evolve to support the newer HTTP/2 protocol.
Fuchs: The new HTTP Client API started to incubate in JDK 9 and was later made standard and permanent in JDK 11. It supports both HTTP/1.1 and HTTP/2, supports WebSockets, and improves security. More importantly, the new client is modern and easy to use. For example, it uses the popular Builder pattern.
Delabassee: How does the new client work?
Fuchs: To send a request, you first need to create an HTTP client; there is a builder for that. Using a builder, you create a client instance and configure its state. For example, you can specify which protocol to use (such as HTTP/1.1 or HTTP/2) and whether to follow redirects. You also can set the proxy selector, set the necessary contexts for Transport Layer Security (TLS), and so on.
Once built, your HTTP client can be used to send multiple requests and is immutable. Requests can be sent multiple times, and you can choose whether they are sent synchronously or asynchronously.
Delabassée: Moving beyond the HTTP Client API, UNIX-domain socket channels were added to Java 16. What can you tell us about that?
McMahon: UNIX-domain sockets are like TCP/IP sockets, except that these sockets are used only for local interprocess communication (IPC), and they are addressed through file system pathnames, rather than through an IP address and port number.
UNIX-domain socket channels, which appeared in JEP 380, offer several benefits for applications that need to do only local IPC or IPC between containers. Performance is one benefit: By cutting out the TCP/IP stack, you can improve throughput and lower CPU utilization. These sockets can also improve security because if you need only local IPC, you don’t need to open ports to the network.
With UNIX-domain sockets, you can also make use of file system access controls and user credentials for additional security. When testing them with Docker, we found that UNIX-domain sockets make it easier to set up communication between containers.
To make all this work, JEP 380 added the following API elements:
◉ A new socket address class, java.net.UnixDomainSocketAddress
◉ A UNIX constant value in the existing java.net.StandardProtocolFamily enum
◉ New open factory methods on SocketChannel and ServerSocketChannel that specify the protocol family
◉ Updates to the SocketChannel and ServerSocketChannel specifications to allow you to define how the channels to the UNIX-domain sockets behave
It should also be mentioned that despite the name, UNIX-domain sockets are also available on Windows!
Delabassée: Typically, with a network connection, security is enforced at the network level, such as by using firewalls and reverse proxies. How can security be enforced with UNIX-domain sockets?
McMahon: One level of security is the old Java platform security model. For that, there’s a single permission, which enables or disables the feature if a security manager is running.
Beyond that, the main way you get security is through the operating system, its user and groups, and its file permissions, where you can restrict access to file system nodes through user IDs and group IDs.
An additional useful feature, which works on the UNIX platform but not on Windows, is via testing user credentials. If a client socket channel is connected to a server socket channel, either side can request the identity of the user at the opposite side.
Delabassée: There is a new simple web server and API targeted for Java 18. Can you give us a quick overview?
McMahon: JEP 408 will deliver a simple web server to serve static content. JEP 408 includes an API for programmatic creation and customization of the server and its components.
As its name implies, the Simple Web Server’s design is explicitly minimal. It is intended for development purposes, as well as for testing and educational use, but it is certainly not a full-featured server that you should use in production! For example, it doesn’t offer security features.
The Simple Web Server is based on the web server implementation in the com.sun.net.httpserver package that has been included in the JDK since 2006. That package is officially supported, and it’s extended with APIs to simplify server creation and enhance request handling. The Simple Web Server can be used via the dedicated command-line tool jwebserver or programmatically via its API.
To reiterate, the Simple Web Server is intended for educational, development testing, and debugging purposes only.
Delabassee: Any final thoughts about the new networking capabilities?
Fuchs: We discussed the HTTP Client API, UNIX-domain socket channels, and the Simple Web Server. Those features are quite visible because developers can use them directly. In parallel to those features, the Java teams continue to work on the platform itself, such as on some features that are not so visible but nevertheless are strategic going forward.
For example, slowly but surely Project Loom is coming—and so we are looking at making sure that our networking implementation will play nice with virtual threads. That was the motivation for JEP 353, which reimplements the legacy Socket API, and JEP 373, which reimplements the legacy DatagramSocket API.
Source: oracle.com
Friday, February 11, 2022
Design implications of Java’s switch statements and switch expressions
The two constructs might look similar, but they behave quite differently, and each is best suited for a different type of programmatic logic.
Download a PDF of this article
The recent evolution of the Java language has introduced interesting new features and constructs intended to make developers more productive. And it is one of these features, known as switch expressions, that I’d like to talk about.
I will start with the following assertion: switch statements and switch expressions have different design purposes. One is not just a different version of the other. Rather, switch expressions are a new construct intended for a different purpose than switch statements. To be clear, the older switch statement and the newer switch expression share some syntactical similarities, but they pursue significantly different design goals.
My intention in this article is not to just talk about this new construct from the perspective of its syntax but rather investigate its design implications for developers’ work. Surely, the intended developer productivity improvements are not meant to be measured in how fast you can type but rather in how well Java’s language constructs support coding requirements and help developers convey the meaning of the algorithm they are working on. This means that every Java developer needs to understand the intended purposes of such language constructs.
switch statements are not an alternative if/else construct
Let’s start by addressing one of the most common misconceptions about the switch statement, namely that it is usually explained by comparing it to the if/else statement as an alternative mechanism for implementing conditional logic.
In my opinion, this is not really the best way of understanding the switch statement. The if/else construct is designed to choose an alternate execution path, essentially presenting a binary fork mechanism that selects the way in which program logic would flow, as shown in Figure 1.