Book Image

Learn Java 17 Programming - Second Edition

By : Nick Samoylov
4 (1)
Book Image

Learn Java 17 Programming - Second Edition

4 (1)
By: Nick Samoylov

Overview of this book

Java is one of the most preferred languages among developers. It is used in everything right from smartphones and game consoles to even supercomputers, and its new features simply add to the richness of the language. This book on Java programming begins by helping you learn how to install the Java Development Kit. You’ll then focus on understanding object-oriented programming (OOP), with exclusive insights into concepts such as abstraction, encapsulation, inheritance, and polymorphism, which will help you when programming for real-world apps. Next, you’ll cover fundamental programming structures of Java such as data structures and algorithms that will serve as the building blocks for your apps with the help of sample programs and practice examples. You’ll also delve into core programming topics that will assist you with error handling, debugging, and testing your apps. As you progress, you’ll move on to advanced topics such as Java libraries, database management, and network programming and also build a sample project to help you understand the applications of these concepts. By the end of this Java book, you’ll not only have become well-versed with Java 17 but also gained a perspective into the future of this language and have the skills to code efficiently with best practices.
Table of Contents (23 chapters)
1
Part 1: Overview of Java Programming
5
Part 2: Building Blocks of Java
15
Part 3: Advanced Java

Java statements

A Java statement is a minimal construct that can be executed. It describes an action and ends with a semicolon (;). We have seen many statements already. For example, here are three statements:

float f = 23.42f;
String sf = String.valueOf(f);
System.out.println(sf);

The first line is a declaration statement combined with an assignment statement. The second line is also a declaration statement combined with an assignment statement and method invocation statement. The third line is just a method invocation statement.

Here is a list of Java statement types:

  • An empty statement that consists of only one symbol, ; (semicolon)
  • A class or interface declaration statement (we will talk about this in Chapter 2, Java Object-Oriented Programming (OOP))
  • A local variable declaration statement: int x;
  • A synchronized statement: this is beyond the scope of this book
  • An expression statement
  • A control flow statement

An expression statement can be one of the following:

  • A method invocation statement: someMethod();
  • An assignment statement: n = 23.42f;
  • An object creation statement: new String("abc");
  • A unary increment or decrement statement: ++x ; or --x; or x++; or x--;

We will talk more about expression statements in the Expression statements section.

A control flow statement can be one of the following:

  • A selection statement: if-else or switch-case
  • An iteration statement: for, or while, or do-while
  • An exception-handling statement: throw, try-catch, or try-catch-finally
  • A branching statement: break, continue, or return

We will talk more about control statements in the Control flow statements section.

Expression statements

An expression statement consists of one or more expressions. An expression typically includes one or more operators. It can be evaluated, which means it can produce a result of one of the following types:

  • A variable: x = 1, for example
  • A value: 2*2, for example

It returns nothing when the expression is an invocation of a method that returns void. Such a method is said to produce only a side effect: void someMethod(), for example.

Consider the following expression:

x = y++; 

The preceding expression assigns a value to an x variable and has a side effect of adding 1 to the value of the y variable.

Another example would be a method that prints a line, like this:

System.out.println(x); 

The println() method returns nothing and has a side effect of printing something.

By its form, an expression can be one of the following:

  • A primary expression: a literal, a new object creation, a field or method access (invocation).
  • A unary operator expression: x++, for example.
  • A binary operator expression: x*y, for example.
  • A ternary operator expression: x > y ? true : false, for example.
  • A lambda expression: x -> x + 1 (see Chapter 14, Java Standard Streams).
  • If an expression consists of other expressions, parentheses are often used to identify each of the expressions clearly. This way, it is easier to understand and to set the expressions’ precedence.

Control flow statements

When a Java program is executed, it is executed statement by statement. Some statements have to be executed conditionally, based on the result of an expression evaluation. Such statements are called control flow statements because, in computer science, a control flow (or flow of control) is the order in which individual statements are executed or evaluated.

A control flow statement can be one of the following:

  • A selection statement: if-else or switch-case
  • An iteration statement: for, while, or do-while
  • An exception-handling statement: throw, try-catch, or try-catch-finally
  • A branching statement: break, continue, or return

Selection statements

Selection statements are based on an expression evaluation and have four variations, as outlined here:

  • if (expression) {do something}
  • if (expression) {do something} else {do something else}
  • if (expression) {do something} else if {do something else} else {do something else}
  • switch...case statement

Here are some examples of if statements:

if(x > y){
    //do something
}
if(x > y){
    //do something
} else {
    //do something else
}
if(x > y){
    //do something
} else if (x == y){
    //do something else
} else {
    //do something different
}

A switch...case statement is a variation of an if...else statement, as illustrated here:

switch(x){
    case 5:               //means: if(x = 5)
        //do something 
        break;
    case 7:             
        //do something else
        break;
    case 12:
        //do something different
        break;
    default:             
        //do something completely different
        //if x is not 5, 7, or 12
}

As you can see, the switch...case statement forks the execution flow based on the value of the variable. The break statement allows the switch...case statement to be executed. Otherwise, all the following cases would be executed.

In Java 14, a new switch...case statement has been introduced in a less verbose form, as illustrated here:

void switchStatement(int x){
    switch (x) {
        case 1, 3 -> System.out.print("1 or 3");
        case 4    -> System.out.print("4");
        case 5, 6 -> System.out.print("5 or 6");
        default   -> System.out.print("Not 1,3,4,5,6");
    }
    System.out.println(": " + x);
}

As you can see, it uses an arrow (->) and does not use a break statement.

Execute the main() method of the com.packt.learnjava.ch01_start.ControlFlow class—see the selection() method that calls the switchStatement() method with different parameters, as follows:

switchStatement(1);    //prints: 1 or 3: 1
switchStatement(2);    //prints: Not 1,3,4,5,6: 2
switchStatement(5);    //prints: 5 or 6: 5

You can see the results from the comments.

If several lines of code have to be executed in each case, you can just put braces ({}) around the block of code, as follows:

switch (x) {
    case 1, 3 -> { 
                    //do something
                 }
    case 4    -> {
                    //do something else 
                 }
    case 5, 6 -> System.out.println("5 or 6");
    default   -> System.out.println("Not 1,3,4,5,6");
}

The Java 14 switch...case statement can even return a value, thus becoming in effect a switch expression. For example, here is a case when another variable has to be assigned based on the switch...case statement result:

void switchExpression1(int i){
    boolean b = switch(i) {
        case 0, 1 -> false;
        case 2 -> true;
        default -> false;
    };
    System.out.println(b);
}

If we execute the switchExpression1() method (see the selection() method of the com.packt.learnjava.ch01_start.ControlFlow class), the results are going to look like this:

switchExpression1(0);    //prints: false
switchExpression1(1);    //prints: false
switchExpression1(2);    //prints: true

The following example of a switch expression is based on a constant:

static final String ONE = "one", TWO = "two", THREE = "three", 
                    FOUR = "four", FIVE = "five";
void switchExpression2(String number){
    var res = switch(number) {
        case ONE, TWO -> 1;
        case THREE, FOUR, FIVE -> 2;
        default -> 3;
    };
    System.out.println(res);
}

If we execute the switchExpression2() method (see the selection() method of the com.packt.learnjava.ch01_start.ControlFlow class), the results are going to look like this:

switchExpression2(TWO);            //prints: 1
switchExpression2(FOUR);           //prints: 2
switchExpression2("blah");         //prints: 3

Here’s yet another example of a switch expression, this time based on the enum value:

enum Num { ONE, TWO, THREE, FOUR, FIVE }
void switchExpression3(Num number){
    var res = switch(number) {
        case ONE, TWO -> 1;
        case THREE, FOUR, FIVE -> 2;
    };
    System.out.println(res);
}

If we execute the switchExpression3() method (see the selection() method of the com.packt.learnjava.ch01_start.ControlFlow class), the results are going to look like this:

switchExpression3(Num.TWO);        //prints: 1
switchExpression3(Num.FOUR);       //prints: 2
//switchExpression3("blah"); //does not compile

In case a block of code has to be executed based on a particular input value, it is not possible to use a return statement because it is reserved already for the returning value from a method. That is why, to return a value from a block, we have to use a yield statement, as shown in the following example:

void switchExpression4(Num number){
    var res = switch(number) {
        case ONE, TWO -> 1;
        case THREE, FOUR, FIVE -> {
            String s = number.name();
            yield s.length();
        }
    };
    System.out.println(res);
}

If we execute the switchExpression4() method (see the selection() method of the com.packt.learnjava.ch01_start.ControlFlow class), the results are going to look like this:

switchExpression4(Num.TWO);        //prints: 1
switchExpression4(Num.THREE);      //prints: 5

Iteration statements

An iteration statement can take one of the following three forms:

  • A while statement
  • A do...while statement
  • A for statement, also called a loop statement

A while statement looks like this:

while (boolean expression){
      //do something
}

Here is a specific example (execute the main() method of the com.packt.learnjava.ch01_start.ControlFlow class—see the iteration() method):

int n = 0;
while(n < 5){
 System.out.print(n + " "); //prints: 0 1 2 3 4 
 n++;
}

In some examples, instead of the println() method, we use the print() method, which does not feed another line (does not add a line feed control at the end of its output). The print() method displays the output in one line.

A do...while statement has a very similar form, as we can see here:

do {
    //do something
} while (boolean expression)

It differs from a while statement by always executing the block of statements at least once before evaluating the expression, as illustrated in the following code snippet:

int n = 0;
do {
    System.out.print(n + " ");   //prints: 0 1 2 3 4
    n++;
} while(n < 5);

As you can see, it behaves the same way when the expression is true at the first iteration. But if the expression evaluates to false, the results are different, as we can see here:

int n = 6;
while(n < 5){
    System.out.print(n + " ");   //prints nothing
    n++;
}
n = 6;
do {
    System.out.print(n + " ");   //prints: 6
    n++;
} while(n < 5);

for statement syntax looks like this:

for(init statements; boolean expression; update statements) {
 //do what has to be done here
}

Here is how a for statement works:

  1. init statements initialize a variable.
  2. A Boolean expression is evaluated using the current variable value: if true, the block of statements is executed; otherwise, the for statement exits.
  3. update statements update the variable, and the Boolean expression is evaluated again with this new value: if true, the block of statements is executed; otherwise, the for statement exits.
  4. Unless exited, the final step is repeated.

As you can see here, if you aren’t careful, you can get into an infinite loop:

for (int x = 0; x > -1; x++){
    System.out.print(x + " ");  //prints: 0 1 2 3 4 5 6 ...
}

So, you have to make sure that the Boolean expression guarantees eventual exit from the loop, like this:

for (int x = 0; x < 3; x++){
    System.out.print(x + " ");  //prints: 0 1 2
}

The following example demonstrates multiple initialization and update statements:

for (int x = 0, y = 0; x < 3 && y < 3; ++x, ++y){
    System.out.println(x + " " + y);
}

And here is a variation of the preceding code for statements for demonstration purposes:

for (int x = getInitialValue(), i = x == -2 ? x + 2 : 0, 
             j = 0; i < 3 || j < 3 ; ++i, j = i) {
 System.out.println(i + " " + j);
}

If the getInitialValue() method is implemented like int getInitialValue(){ return -2; }, then the preceding two for statements produce exactly the same results.

To iterate over an array of values, you can use an array index, like so:

int[] arr = {24, 42, 0};
for (int i = 0; i < arr.length; i++){
    System.out.print(arr[i] + " ");  //prints: 24 42 0
}

Alternatively, you can use a more compact form of a for statement that produces the same result, as follows:

int[] arr = {24, 42, 0};
for (int a: arr){
    System.out.print(a + " ");  //prints: 24 42 0
}

This last form is especially useful with a collection, as shown here:

List<String> list = List.of("24", "42", "0");
for (String s: list){
    System.out.print(s + " ");  //prints: 24 42 0
}

We will talk about collections in Chapter 6, Data Structures, Generics, and Popular Utilities.

Exception-handling statements

In Java, there are classes called exceptions that represent events that disrupt the normal execution flow. They typically have names that end with Exception: NullPointerException, ClassCastException, ArrayIndexOutOfBoundsException, to name but a few.

All the exception classes extend the java.lang.Exception class, which, in turn, extends the java.lang.Throwable class (we will explain what this means in Chapter 2, Java Object-Oriented Programming (OOP)). That’s why all exception objects have common behavior. They contain information about the cause of the exceptional condition and the location of its origination (line number of the source code).

Each exception object can be generated (thrown) either automatically by the JVM or by the application code, using the throw keyword. If a block of code throws an exception, you can use a try-catch or try-catch-finally construct to capture the thrown exception object and redirect the execution flow to another branch of code. If the surrounding code does not catch the exception object, it propagates all the way out of the application into the JVM and forces it to exit (and abort the application execution). So, it is good practice to use try-catch or try-catch-finally in all the places where an exception can be raised and you do not want your application to abort execution.

Here is a typical example of exception handling:

try {
    //x = someMethodReturningValue();
    if(x > 10){
        throw new RuntimeException("The x value is out
                                    of range: " + x);
    }
    //normal processing flow of x here
} catch (RuntimeException ex) {
    //do what has to be done to address the problem
}

In the preceding code snippet, normal processing flow will be not executed in the case of x > 10. Instead, the do what has to be done block will be executed. But, in the x <= 10 case, the normal processing flow block will be run and the do what has to be done block will be ignored.

Sometimes, it is necessary to execute a block of code anyway, whether an exception was thrown/caught or not. Instead of repeating the same code block in two places, you can put it in a finally block, as follows (execute the main() method of the com.packt.learnjava.ch01_start.ControlFlow class—see the exception() method):

try {
    //x = someMethodReturningValue();
    if(x > 10){
        throw new RuntimeException("The x value is out 
                                    of range: " + x);
    }
    //normal processing flow of x here
} catch (RuntimeException ex) {
   System.out.println(ex.getMessage());   
   //prints: The x value is out of range: ...
   //do what has to be done to address the problem
} finally {
   //the code placed here is always executed
}

We will talk about exception handling in more detail in Chapter 4, Exception Handling.

Branching statements

Branching statements allow breaking of the current execution flow and continuation of execution from the first line after the current block or from a certain (labeled) point of the control flow.

A branching statement can be one of the following:

  • break
  • continue
  • return

We have seen how break was used in switch-case statements. Here is another example (execute the main() method of the com.packt.learnjava.ch01_start.ControlFlow class—see the branching() method):

String found = null;
List<String> list = List.of("24", "42", "31", "2", "1");
for (String s: list){
    System.out.print(s + " ");         //prints: 24 42 31
    if(s.contains("3")){
        found = s;
        break;
    }
}
System.out.println("Found " + found);  //prints: Found 31

If we need to find the first list element that contains "3", we can stop executing as soon as the s.contains("3") condition is evaluated to true. The remaining list elements are ignored.

In a more complicated scenario, with nested for statements, it is possible to set a label (with a : column) that indicates which for statement has to be exited, as follows:

String found = null;
List<List<String>> listOfLists = List.of(
        List.of("24", "16", "1", "2", "1"),
        List.of("43", "42", "31", "3", "3"),
        List.of("24", "22", "31", "2", "1")
);
exit: for(List<String> l: listOfLists){
    for (String s: l){
        System.out.print(s + " "); //prints: 24 16 1 2 1 43
        if(s.contains("3")){
            found = s;
            break exit;
        }
    }
}
System.out.println("Found " + found);  //prints: Found 43

We have chosen a label name of exit, but we could call it any other name too.

A continue statement works similarly, as follows:

String found = null;
List<List<String>> listOfLists = List.of(
                List.of("24", "16", "1", "2", "1"),
                List.of("43", "42", "31", "3", "3"),
                List.of("24", "22", "31", "2", "1")
);
String checked = "";
cont: for(List<String> l: listOfLists){
        for (String s: l){
           System.out.print(s + " "); 
                  //prints: 24 16 1 2 1 43 24 22 31
           if(s.contains("3")){
               continue cont;
           }
           checked += s + " ";
        }
}
System.out.println("Found " + found);  //prints: Found 43
System.out.println("Checked " + checked);  
                            //prints: Checked 24 16 1 2 1 24 22

It differs from break by stating which of the for statements need to continue and not exit.

A return statement is used to return a result from a method, as follows:

String returnDemo(int i){
    if(i < 10){
        return "Not enough";
    } else if (i == 10){
        return "Exactly right";
    } else {
        return "More than enough";
    }
}

As you can see, there can be several return statements in a method, each returning a different value in different circumstances. If the method returns nothing (void), a return statement is not required, although it is frequently used for better readability, as follows:

void returnDemo(int i){
    if(i < 10){
        System.out.println("Not enough");
        return;
    } else if (i == 10){
        System.out.println("Exactly right");
        return;
    } else {
        System.out.println("More than enough");
        return;
    }
}

Execute the returnDemo() method by running the main() method of the com.packt.learnjava.ch01_start.ControlFlow class (see the branching() method). The results are going to look like this:

String r = returnDemo(3);
System.out.println(r);      //prints: Not enough
r = returnDemo(10);
System.out.println(r);      //prints: Exactly right 
r = returnDemo(12);
System.out.println(r);      //prints: More than enough

Statements are the building blocks of Java programming. They are like sentences in English—complete expressions of intent that can be acted upon. They can be compiled and executed. Programming is like expressing an action plan in statements.

With this, the explanation of the basics of Java is concluded. Congratulations on getting through it!