DEV Community

Cover image for Lessons from a Second Month Learning Java
Alex Ortiz
Alex Ortiz

Posted on

Lessons from a Second Month Learning Java

With two months under my belt learning Java, I've refactored the toy programs I wrote in last month's post. My goal here is to optimize my programs based on what I've learned in the last four weeks.

Program Table Of Contents
Refactored Program 1: Two Languages
Refactored Program 2: Even Odd While Loop
Bonus Refactored Program 2: Custom Even Odd While Loop
Refactored Program 3: The Nth Letter of the Alphabet
Refactored Program 4: Combine Two Arrays
Refactored Program 5: Prime Detector
Wrap-up

Refactored Program 1: Two Languages

My previous TwoLanguages program was this:

/*
A program that prints a different sentence depending on which of two predefined programming languages the user inputs.
*/

import java.util.Scanner; // import the Scanner class

class TwoLanguages {
  public static void main(String[] args) { // program execution begins

    String python = new String(); // declare the python variable
    python = "Python"; // define the python variable

    String java = new String(); // declare the java variable
    java = "Java"; // define the java variable

    Scanner keyboardInput1 = new Scanner(System.in); // declare a Scanner object to accept user input

    System.out.println("Which language are you asking about?"); //  prompt the user for input

    String i = keyboardInput1.nextLine(); // declare a variable for storing the user's input

    // An if-else-if ladder to determine the program's response.
    if (i.equalsIgnoreCase("Python")) {
      System.out.println("Python is a great language for newcomers to programming! It's easy to pick up and has a huge developer community surrounding it.");
    }

    else if (i.equalsIgnoreCase("Java")) {
      System.out.println("Java is a language after my own heart; my brain `gets` it! I think it will serve me well in the 2020s for the scientific work I want to do.");
    }

    else {
        System.out.println("The program doesn't understand. Please try again.");
      }
    }
}

Here's the new one, RefactoredTwoLanguages:

import java.util.Scanner;

/*
A program that prints a different sentence depending on which of two predefined programming languages the user inputs.
*/

class RefactoredTwoLanguages {
  public static void main(String[] args) {
    Scanner userInput = new Scanner(System.in);
    System.out.println("Would you like to know about Java or Python? (Type 'Java' or 'Python')");

    var i = userInput.nextLine().toLowerCase();

    switch(i) {
      case "java":
      System.out.println("Java is a language after my own heart; my brain `gets` it! I think it will serve me well in the 2020s for the scientific work I want to do.");
      break;

      case "python":
      System.out.println("Python is a great language for newcomers to programming! It's easy to pick up and has a huge developer community surrounding it.");
      break;

      default:
      System.out.println("Please re-run the program and enter a different language. The only valid values are `Java` or `Python` (not case-sensitive)");
      break;
    }
  }
}

Refactored Code Summary

Here's what the compiler does in this program:

  • Import the package java.util.Scanner
  • Begin program execution
  • Create a Scanner class object to accept user input via the keyboard
  • Print a prompt displaying a question for the user and wait for the user to type an input
  • Create a variable, i, to store whatever the user types, automatically converting it into all lowercase; moreover, the compiler will infer the data type based on the user's entry
  • Evaluate the entry stored in i:
    • If it's "java", print the corresponding statement
    • If it's "python", print the corresponding statement
    • If it's anything else, print the corresponding default statement
  • Exit the program

Old vs New: What's Changed

First, here's a summary of the changes:

CHANGE LOG

  • Moved the import statement to the top of the file
  • Removed two unnecessary String variable declarations and instantiations
  • Made a minor tweak to the Scanner variable name
  • Reworded the print statement which prompts the user for a language
  • Made variable i a variable of type var
  • Added a chained method, .toLowerCase(), to improve functionality
  • Implemented a switch statement in place of an if-else-if ladder

Details

Here's more about each change.

Moved the import statement to the top of the file

I read the Google Java Style Guide, which is a lot to absorb but is a great reference for two reasons:

  1. it functions as a digest important Java conventions for structuring, naming, formatting, and other aspects of writing and organizing Java code, and
  2. it conveys what Google considers to be the best practices for Java styling to maximize readability

If it's good enough for Google, it's good enough for me, a newcomer.

After reading the document, I moved import statements to the top of my source files, removed unnecessary or unhelpful whitespace, and made other cosmetic or readability improvements to my code. This is true in this refactored program as well as the rest.

Removed two unnecessary String variable declarations and instantiations

Re-reading my old code, I realized that the python and java variables didn't serve any purpose. The user is going to input "Python" or "Java" via the keyboard, so storing those strings in variables beforehand is redundant. My original reasoning for including them might have been to compare the input strings with the ones stored in the python and java variables. But I never did do that, since the i.equalsIgnoreCase() method addressed that need in the old program.

In any case, I deleted the variables.

Made a minor tweak to the Scanner variable name

Previously, I named the Scanner object keyboardInput1. This had been lazy naming: there was no keyboardInput2 in my program, so no need to specify a number in the name. This time, I chose the name userInput.

Reworded the print statement which prompts the user for a language

The previous print statement was "Which language are you asking about?". The new one is more specific: "Would you like to know about Java or Python? (Type 'Java' or 'Python')".

Made variable i a variable of type var

I recently learned about local variables, which are variables declared and used within the scope of a method or code block. I also learned about local variable type inference, a feature that allows the Java compiler to infer the data type based on the value stored in a local variable. To avail yourself of this functionality, you use the keyword var instead of specifying a particular primitive type or reference type in your declaration. So I did that here with i. The only wrinkle is—and I might be misunderstanding this point—is that i appears to be an instance variable of my class, not a local variable of a specific method, constructor, etc. Nonetheless, I replaced int with var to practice my new learning, and it worked.

Added a chained method, .toLowerCase(), to improve functionality

This line of code was one of my favorite lines of code from the last month:

var i = userInput.nextLine().toLowerCase();

Why? This line of code solved a problem I'd been banging my head against the wall with for a few days. The problem was related to my use of a switch statement in place of the previous if-else-if ladder (also referred to simply as an if-then-else statement). I'll explain more in this next section below.

Implemented a switch statement in place of an if-else-if ladder

In my original program, the user is asked a question with the expectation of entering either "Python" or "Java" in response. But there was a chance that the user would enter either word in lowercase rather than in capitalized form. So I wrote the program to ignore casing:

if (i.equalsIgnoreCase("Python")) {...} // accepts any combination of lowercase and uppercase

This worked well. But in my refactored program, I wanted to replace the if-then-else statement with a switch statement. For one, a switch statement is more readable, and for two, the goal of my exercise was to refactor my code while retaining an identical user experience and program output.

That's when I ran into a problem: I tried switch (i.equalsIgnoreCase()) to achieve a similar result, but I discovered that this is illegal in Java. This is because the input to a switch statement can't be a boolean value; unfortunately for me, this is exactly what the result of i.equalsIgnoreCase("blah") returns: a boolean value. So I was stuck. I would need to manually program different case values for the different combinations of lowercase and uppercase letters.

That is, until I realized that there was another solution: store the value of i in the same output format regardless of how the user chooses to input the letters in the words "Python" or "Java". To do that, I used a chained method, .toLowerCase(), as in

userInput.nextLine().toLowerCase()

So whether the user enters a word in ALL CAPS, all lowercase, or mixedCase, it won't matter: "java", jAva", "jaVa", "javA", "Java", "JAva", "JAVa", "JAVA", "jaVA", etc. will be stored in i as the same representation: "java".

With this solution in place, I only needed two case labels and one default label in my switch statement to account for all possible scenarios.

Output(s)

Example 1:

$ java RefactoredTwoLanguages
Would you like to know about Java or Python? (Type 'Java' or 'Python')
JaVA
Java is a language after my own heart; my brain `gets` it! I think it will serve me well in the 2020s for the scientific work I want to do.
$ java RefactoredTwoLanguages
Would you like to know about Java or Python? (Type 'Java' or 'Python')
JAva
Java is a language after my own heart; my brain `gets` it! I think it will serve me well in the 2020s for the scientific work I want to do.

Example 2:

$ java RefactoredTwoLanguages
Would you like to know about Java or Python? (Type 'Java' or 'Python')
PYTHOn
Python is a great language for newcomers to programming! It's easy to pick up and has a huge developer community surrounding it.
$ java RefactoredTwoLanguages
Would you like to know about Java or Python? (Type 'Java' or 'Python')
pYtHoN
Python is a great language for newcomers to programming! It's easy to pick up and has a huge developer community surrounding it.

Example 3:

$ java RefactoredTwoLanguages
Would you like to know about Java or Python? (Type 'Java' or 'Python')
C++
Please re-run the program and enter a different language. The only valid values are `Java` or `Python` (not case-sensitive)

Key Takeaway(s)

Cosmetic changes can aid readability. A well-selected chained method can save you precious time and obviate the need for long control structures. Your choices in one part of your code (such as in a method) can affect your options in another part of your code (such as in a switch statement), so you must plan ahead. Switch statements offer a cleaner, more readable alternative to if-then-else statements.

Refactored Program 2: Even Odd While Loop

Here's my old EvenOddWhileLoop program:

/*
A program that prints even and odd numbers from zero to ten using a while loop with a nested if statement.
*/

class EvenOddWhileLoop {
  public static void main(String[] args) {

    int x = 0; // a variable to store the starting point, the integer 0

    while (x < 11) { // set a max of x = 10

      if (x % 2 == 0) { // if dividing by two leaves no remainder
        System.out.println(x + " is an even number.");
        x++; // increment the current value of x by 1
      }

      else {
        System.out.println(x + " is an odd number.");
        x++; // increment the current value of x by 1
      }
    }
  }
}

And here's my new RefactoredEvenOddWhileLoop program:

/*
A program that prints even and odd numbers from zero through ten.
*/

class RefactoredEvenOddWhileLoop {
  public static void main(String[] args) {
    var i = 0;
    while (i < 11) {
      System.out.println((i % 2 == 0) ? i + " is an even number." : i + " is an odd number.");
      i++;
    }
  }
}

Refactored Code Summary

Heres what the compiler will do:

  • Begin program execution
  • Create a variable named i; this will serve as an increment variable with an initial value of 0. The compiler will infer its type, and that will be int
  • Consider the current value of i and take the following actions while i is less than 11:
    • If the value is divisible by 2 with no remainder, print the first statement
    • Otherwise, print the second statement
    • Increment the current value of i by 1
  • Exit the program

That's all there is to it!

Old vs New: What's Changed

Here's a summary of the changes:

CHANGE LOG

  • Replaced the int data type with the var keyword
  • Cosmetic change: renamed the variable x to i
  • Removed unnecessary vertical whitespace
  • Implemented a ternary operator to replace the true/false branches of the former if-then-else statement

Details

Here's each change in greater detail.

Replaced the int data type with the var keyword

This wasn't a strictly necessary change, but I wanted to continue testing type inference. So instead of explicitly declaring the variable as being of primitive type int, I used the var keyword to let the compiler infer int from the stored value of 0.

Cosmetic change: renamed the variable x to i

One of the books I'm reading is Jeanne Boyarski and Scott Selikoff's OCP Java SE 11 Programmer I Study Guide. In it, Boyarski & Selikoff explain that i is short for "increment" variable and is conventionally used as a counter in loops in many programming languages. (This apparently was done pre-Java, so presumably in C and C++.) Conventions, even very small ones like this one, can reduce mental overhead over time, so I made the change and will keep using i in future programs when a variable's purpose is incrementation.

Removed unnecessary vertical whitespace

Ironically, more whitespace doesn't automatically make code cleaner or more readable. Selective use of vertical whitespace does. In this program, I determined that less is more.

Implemented a ternary operator to replace the true/false branches of the former if-then-else statement

With respect to decision-making programs, Java has a bit of "paper-rock-scissors" that I'm still learning about. Joshua Bloch would say you should prefer for loops to while loops if you can help it and you should "prefer for-each loops to traditional for loops" also (Item 58, Effective Java, 3rd Edition).

Translated:

 while loop < for loop < for-each loop // it depends!

In my original program, I used a while loop with an embedded if-then-else statement. In my refactored version, the next best approach would seem to be a for loop. But as the name of this program suggests, the point is to implement a while loop, so a for loop is off the table. Besides, I found my for loop attempts lacking:

// Worked but was ugly
for (var i = 0; i < 11; i++) {
  System.out.println((i % 2) == 0 ? i + " is an even number." : i + " is an odd number.");
}
...

In this snippet, I save a line of code by having the compiler create a local variable, i, inside the for loop. Then the compiler runs a ternary operation for as long as the i < 11 condition is met. A ternary operation is a shortcut that evaluates a boolean expression and returns one of two statements—one if it's true, the other if it's false.

Said differently, the ternary has three operands:

  1. a boolean condition to test,
  2. a statement to process if the condition evaluates to true, and
  3. a statement to process if the condition evaluates to false

For example, the compiler here will perform a modulus division; if the evaluation results in true, the compiler will print "...is an even number". If the evaluation results in false, the compiler will print "...is an odd number". This process repeats for every value until the compiler can exit the for loop.

In any case, this worked and wasn't terrible, but that's a lot to cram into three lines of code.

My next attempt at a for loop was slightly better but definitely longer:

// Also worked and was less ugly yet longer
for (var i = 0; i < 11; i++) {
  if ((i % 2) == 0) {
    System.out.println(i + " is an even number.");
  } else {
    System.out.println(i + " is an odd number.");
  }
}
...

What went well here is that the code is easier to read. But now I've added an if-then-else statement with separate if and else branches, making my code longer. Note that the if block also has a ternary operator inside. So this attempt compiles fine and prints the desired output, but it nests a ternary within an if-then-else statement within a for loop. (The original program, EvenOddWhileLoop, had an if-then-else statement nested within a while loop. So technically, this version is slightly better.)

In the end, I opted to keep the while loop, as seen in my RefactoredEvenOddWhileLoop program. There is a downside, though. As Bloch explains in Effective Java, a while loop doesn't let me declare i as a local variable within the loop itself. For example, I can't write while (var i = 0 && i < 11). I don't know if it's because you can't include two boolean expressions there, or if it's something to do with the var keyword. But I have to declare i as an instance variable of the class, before the compiler reaches the while loop. On the other hand, in a for loop, I can declare i as a local initialization variable, within the scope of the for loop itself. This is a better practice (Bloch, "Item 57: Minimize the scope of local variables", Effective Java, 3rd Edition). So I may end up creating an "EvenOddForLoop" program to replace this one in a future refactoring.

In any case, I wanted to practice succinctness and practice ternary operations, so I added the ternary operator to my refactored while loop. To recap what the ternary does, here's the full ternary operator:

(i % 2 == 0) ? i + " is an even number." : i + " is an odd number."

It first evaluates the expression involving integer i. If the expression evaluates to true, the compiler returns the print statement left of the colon (:). If the outcome is false, the compiler returns the print statement to the right of the colon. So the ternary operator functions like a shortcut for an if-then-else statement, which is great.


As a related aside, here's something I was stuck on for a while. I might still have something wrong here, but here's my present understanding:

In RefactoredEvenOddWhileLoop, the first operand is of type boolean and is evaluating an int, whereas the other two operands are of type String. This would be problematic for the compiler but for the fact that the System.out.println() method automatically converts the output of all three into Objects of type String. Per Boyarski & Selikoff, the compiler calls the toString() method under-the-hood; therefore the ternary works fine even though there's one numerical operand and two String operands in play. So upon compilation, there's no error. I think that's right, but don't quote me on it.


In summary, I tried a few different refactored programs, all of which worked. I selected the one that offered the best trade-off between simplicity and readability, even though using a while loop is not a best practice.

Output(s)

$ java RefactoredEvenOddWhileLoop
0 is an even number.
1 is an odd number.
2 is an even number.
3 is an odd number.
4 is an even number.
5 is an odd number.
6 is an even number.
7 is an odd number.
8 is an even number.
9 is an odd number.
10 is an even number.

Key Takeaway(s)

Don't cram too much code into a single line just to be succinct. Simplicity and readability involve trade-offs. You can write the program in various ways, but select the one that achieves the mission while navigating multiple concerns. Listen to Joshua Bloch; he gives well-reasoned advice.


BONUS Refactored Program 2: Custom Even Odd While Loop

Last time, I also created a CustomEvenOddWhileLoop program:

/*
A program that accepts user input for the minimum and maximum values in an arbitrary integer interval, then prints the numbers and specifies if they are even or odd.
*/

import java.util.Scanner;

class CustomEvenOddWhileLoop {
  public static void main(String[] args) {

    Scanner lowerBound = new Scanner(System.in);
    System.out.println("Enter a minimum:");
    int a = lowerBound.nextInt(); // a variable to store the lower bound of the interval

    Scanner upperBound = new Scanner(System.in);
    System.out.println("Enter a maximum:");
    int b = upperBound.nextInt(); // a variable to store the upper bound of the interval

    while (a <= b) { // i.e., set a max of a = b

      if (a % 2 == 0) { // if dividing by 2 leaves no remainder
        System.out.println(a + " is an even number.");
        a++;
      }

      else {
        System.out.println(a + " is an odd number.");
        a++;
      }
    }
  }
}

It was a satisfying, if wordy, program. Instead of printing the pre-selected values 0 through 10, the program let the user provide a custom range.

Here's the first of two valid implementations for RefactoredCustomEvenOddWhileLoop:

import java.util.Scanner;

/*
A program that prints whether numbers are even or odd, based on a range supplied by the user. The valid input value range is from -2,147,483,648 to +2,147,483,647.
*/

class RefactoredCustomEvenOddWhileLoop {
  public static void main(String[] args) {
    Scanner userInputMin = new Scanner(System.in);
    System.out.println("Enter a minimum value:");
    int min = userInputMin.nextInt();

    Scanner userInputMax = new Scanner(System.in);
    System.out.println("Enter a maximum value:");
    int max = userInputMax.nextInt();

    // Implemtation #1: An if-then-else statement within a while loop
    while (min <= max) {
      if (min % 2 == 0) { // if dividing by 2 leaves no remainder
        System.out.println(min + " is an even number.");
        min++;
      } else {
        System.out.println(min + " is an odd number.");
        min++;
      }
    }
  }
}

Refactored Code Summary for Implementation 1

In this first version, the compiler will do these sequentially:

  • Import the java.util.Scanner package
  • Begin program execution
  • Create an object named userInputMin of the Scanner class; the new object will be capable of accepting user input via the keyboard
  • Print a statement to the console asking the user to input a minimum value
  • Create a variable named min to store the user-supplied minimum value
  • Repeat the last three steps, but with a userInputMax object, a print statement about a maximum value, and a new variable named max
  • Execute a while loop that runs until min and max have the same value:
    • Run a modulus operation to check if min is an even number, and
    • if yes, print the corresponding statement in the if block, then increment min by 1
    • if no, print the alternative statement in the else block, then increment min by 1
  • Exit the program

And here's the second valid implementation:

import java.util.Scanner;

/*
A program that accepts user input for the minimum and maximum values in an arbitrary integer interval, then prints the numbers and specifies if they are even or odd.
*/

class RefactoredCustomEvenOddWhileLoop {
  public static void main(String[] args) {
    Scanner userInputMin = new Scanner(System.in);
    System.out.println("Enter a minimum value:");
    int min = userInputMin.nextInt();

    Scanner userInputMax = new Scanner(System.in);
    System.out.println("Enter a maximum value:");
    int max = userInputMax.nextInt();

    // Implementation #2: A ternary operator within a while loop
    while (min <= max) {
      System.out.println((min % 2 == 0) ? min + " is an even number." : min + " is an odd number.");
      min++;
    }
  }
}

Note how much shorter this one is, since it uses the ternary operator in place of an if-then-else statement.

Refactored Code Summary for Implementation 2

In this second version, the compiler will perform these steps:

  • Do everything as in Implementation 1 prior to the while loop
  • As before, execute a while loop that runs until min and max have the same value, but this time, follow control flow based on a ternary operation
  • Exit the program

So the logic is practically identical to Implementation 1's logic while requiring fewer lines of code. You'll notice that this while loop is identical to the while loop in RefactoredEvenOddWhileLoop. My opinion seems to be that ternary operators are visually palatable when used within a while loop, but not when used in a for loop 🤷🏾‍♂️.

Old vs New: What's Changed

Finally, here's a summary of the changes:

CHANGE LOG

  • Moved the import statement to the top of the file
  • Removed arbitrary vertical whitespace
  • Renamed the Scanner object variables from lowerBound and upperBound to userInputMin and userInputMax, respectively
  • Renamed the storage variables from a and b to min and max, respectively
  • Cosmetic change: removed the line break before the else in the if-then-else statement (Implementation 1 only)

Details

For the first three of these changes, I have the same reasons as the ones given for RefactoredTwoLanguages and RefactoredEvenOddWhileLoop: simplicity, readability, and convention.

However, the last change is worth detailing:

Cosmetic change: removed the line break before the else in the if-then-else statement (Implementation 1 only)

Here's what the Google Java Style Guide has to say about formatting with braces:

"Braces follow the Kernighan and Ritchie style ("Egyptian brackets") for nonempty blocks and block-like constructs: [...] Line break after the closing brace, only if that brace terminates a statement or terminates the body of a method, constructor, or named class. For example, there is no line break after the brace if it is followed by else or a comma." [See Braces > Nonempthy blocks: K & R style]

That translates into the following:

// Do this
if (condition) {
  ...
} else {
  ...
} else if {
  ...
}

// Not this
if (condition) {
  ...
}
else {
  ...
}
else if {
  ...
}

Both are syntactically correct. They make no difference to the compiler. But the K&R style is conventionally correct per Google Java Style Guide, so I tweaked my refactored code accordingly and removed the line break and vertical whitespace.

Output(s) for Refactored Custom Program

Example 1

$ java RefactoredCustomEvenOddWhileLoop
Enter a minimum value:
13
Enter a maximum value:
22
13 is an odd number.
14 is an even number.
15 is an odd number.
16 is an even number.
17 is an odd number.
18 is an even number.
19 is an odd number.
20 is an even number.
21 is an odd number.
22 is an even number.

Example 2

java RefactoredCustomEvenOddWhileLoop
Enter a minimum value:
-5
Enter a maximum value:
122
-5 is an odd number.
-4 is an even number.
-3 is an odd number.
-2 is an even number.
-1 is an odd number.
0 is an even number.
1 is an odd number.
2 is an even number.
3 is an odd number.
... // purposely truncated
116 is an even number.
117 is an odd number.
118 is an even number.
119 is an odd number.
120 is an even number.
121 is an odd number.
122 is an even number.

Key Takeaway(s)

Name all variables as descriptively as possible. This reduces mental overhead on future reads or for external readers of your code. Choose a Style Guide that has well-researched and well-argued choices, then apply your choices consistently when you write code. This includes where and how you use brackets and vertical whitespace.

Refactored Program 3: The Nth Letter of The Alphabet

Here's my original program, TheNthLetterOfTheAlphabet:

/*
A program that prints the letters of the English alphabet along with their ordinals, using a while loop with a nested if-else-if ladder.
*/

class TheNthLetterOfTheAlphabet {
  public static void main(String[] args) {

    // An array to store the letters of the alphabet.

     String alphabet[] = {"a","b","c","d","e","f","g","h","i","h","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"};

    int n = 1; // to track which letter the program will print

    while (n < 28) { // i.e., until the 27th letter is reached <-- TYPO: "27th" should read "26th"; the loop begins at n = 1 and ends when n = 27
      // <-- TYPO: empty comment
      if (n == 1 || n == 21) {
        System.out.println("The " + n + "st letter of the alphabet is " + alphabet[n-1] + ".");
      }

      else if (n == 2 || n == 22) {
        System.out.println("The " + n + "nd letter of the alphabet is " + alphabet[n-1] + ".");
      }

      else if (n == 3 || n == 23) {
        System.out.println("The " + n + "rd letter of the alphabet is " + alphabet[n-1] + ".");
      }

      else {
        System.out.println("The " + n + "th letter of the alphabet is " + alphabet[n-1] + ".");
      }

      n++;
    }
  }
}

Below is the refactored version, RefactoredTheNthLetterOfTheAlphabet:

/*
A program that prints each letter of the English alphabet along with its position (1st, 2nd, 3rd...26th) in the sequence.
*/

class RefactoredTheNthLetterOfTheAlphabet {
  public static void main(String[] args) {
     // Implementation with a for-each loop and switch statement
     String st = "st letter of the alphabet is ";
     String nd = "nd letter of the alphabet is ";
     String rd = "rd letter of the alphabet is ";
     String th = "th letter of the alphabet is ";

     String[] alphabet = {"a","b","c","d","e","f","g","h","i","h","j","k","l","m","n","o","p","q","r","s","t","u", "v","w","x","y","z"};

     var i = 1;

     for (String e : alphabet) {
       switch (i) {
         case 1: case 21:
         System.out.println("The " + i + st + e + ".");
         i++;
         break;

         case 2: case 22:
         System.out.println("The " + i + nd + e + ".");
         i++;
         break;

         case 3: case 23:
         System.out.println("The " + i + rd + e + ".");
         i++;
         break;

         default:
         System.out.println("The " + i + "th letter of the alphabet is " + e + ".");
         i++;
         break;
       }
     }
  }
}

Refactored Code Summary

The compiler will do this:

  • Begin program execution
  • Create a variable named st to store a phrase for later use
  • Create a variable named nd to store a phrase for later use
  • Create a variable named rd to store a phrase for later use
  • Create a variable named th to store a phrase for later use
  • Create an array named alphabet of type String to store the 26 letters of the English alphabet
  • Instantiate a variable named i with a value of 1
  • Create and execute a for-each loop
    • Evaluate the value stored in i
    • In the case of "1" or "21", print the corresponding combination strings (e.g., "The 1st letter...")
    • In the case of "2" or "22", print the corresponding combination of strings (e.g., "The 2nd letter...")
    • In the case of "3" or "23", print the corresponding combination of strings (e.g., "The 3rd letter...")
    • In all other cases, print the default combination of strings (e.g., "The 4th letter...")
  • Exit the program

Old vs New: What's Changed

Here's a summary of the changes:

CHANGE LOG

  • Cosmetic change: changed the position of the array bracket, from String alphabet[] to String[] alphabet
  • Created placeholder variables st, nd, rd, and th as shortcuts for substrings of the print statements
  • Adjusted int n = 1 to var i = 1
  • Replaced the while loop with a for-each loop
  • Implemented a switch statement within the new for-each loop, as a replacement for the original program's if-then-else statement

Details

Here's more information about each change.

Cosmetic change: changed the position of the array bracket, from String alphabet[] to String[] alphabet

This is purely cosmetic and doesn't change what the compiler does. Via Boyarski & Selikoff, I've learned that any of these would be valid positions for the array bracket []:

  • String[] alphabet
  • String [] alphabet
  • String alphabet[]

The first style is the most common, so I went with that. But they all instruct the compiler to do the same thing: create an array to store Strings, and name that array "alphabet".

Created placeholder variables st, nd, rd, and th as shortcuts for substrings of the print statements

This was a workaround to improve readability. I wanted my switch statement to be tidier than the if-then-else statement it was meant to replace had been. So instead of specifying the full text as a phrase in the case statements, I pre-made these snippets. But the enhancement wasn't free: the penalty was having to add four new instance variables to my class.

Adjusted int n = 1 to var i = 1

There are two cosmetic changes here:

  1. i is still an integer value, so I don't really need to delegate type inference to the compiler. I could have simply retained int i = 1
  2. I also renamed the variable from n to i, for consistency's sake: i functions as an increment variable in the subsequent switch statement

An Aside about Consistency in Naming

While writing this program, I determined to settle on the following variable naming conventions in future code:

  • as explained before, use i for local variables or instance variables that are used primarily to increment a value (as in i++)
  • use n or x for index variables whose primary function is to be indices in a collection (as in alphabet[n] or alphabet[x])

Consistency in nomenclature helps to free up mental bandwidth for the programmer (one fewer decision to have to make). It also makes it easier for the reader to identify the purpose of code, once they understand your conventions. By analogy, it's common in mathematics for the variables a, b, and c to be used in equations with variables of known or fixed values, and to use the variables x, y, and z when the variables have unknown or varying values.


Replaced the while loop with a for-each loop

I didn't set out to follow Joshua Bloch's advice regarding the benefits of a for-each loop over a for loop (and by extension a while loop) for this program. But that's what happened here. Now I see why it's useful to prefer a for-each loop to a for loop or a while loop. First, the variable e here is local; it's used only within the scope of the for-each loop. Because of that, e will be eligible for garbage collection as soon as this portion of my refactored program completes, as far as I understand. The variable i, on the other hand, does not enjoy this same benefit because it's declared as a regular (instance) variable before the compiler reaches the for-each loop. So even though i is only used within the switch statement, its scope is actually part of the main() method. So even when the switch statement is no longer in play and the for-each loop is no longer in play, i remains in play until the entire program terminates. Apparently, this is an anti-pattern to avoid, as it can lead to memory leaks in some cases.

Implemented a switch statement within the new for-each loop, as a replacement for the original program's if-then-else statement

There are four case branches here compared to four if/else if/else branches. But the switch statement is tidier and easier to read.

I also learned something new and important here. The original if-then-else statement is followed by an incrementation statement (n++;). I tried to do the same here by adding i++; after the switch statement, as a way around specifying i++; in each case branch. That didn't work. The i++; statement will be unreachable by the compiler if you do that, creating infinite loop. You have to add an i++; instruction to each case branch for the program to work properly.

Output(s)

$ java RefactoredTheNthLetterOfTheAlphabet
The 1st letter of the alphabet is a.
The 2nd letter of the alphabet is b.
The 3rd letter of the alphabet is c.
The 4th letter of the alphabet is d.
The 5th letter of the alphabet is e.
The 6th letter of the alphabet is f.
The 7th letter of the alphabet is g.
The 8th letter of the alphabet is h.
The 9th letter of the alphabet is i.
The 10th letter of the alphabet is h.
The 11th letter of the alphabet is j.
The 12th letter of the alphabet is k.
The 13th letter of the alphabet is l.
The 14th letter of the alphabet is m.
The 15th letter of the alphabet is n.
The 16th letter of the alphabet is o.
The 17th letter of the alphabet is p.
The 18th letter of the alphabet is q.
The 19th letter of the alphabet is r.
The 20th letter of the alphabet is s.
The 21st letter of the alphabet is t.
The 22nd letter of the alphabet is u.
The 23rd letter of the alphabet is v.
The 24th letter of the alphabet is w.
The 25th letter of the alphabet is x.
The 26th letter of the alphabet is y.
The 27th letter of the alphabet is z.

Key Takeaway(s)

Storing predefined sentence strings in variables can make your code more readable. For-each loops combine nicely with switch statements. Watch out for infinite loops.

BONUS PROGRAMS :)

While creating the refactored version of this program, I found multiple ways to print the individual elements or all the elements of an array. Below are some of the toy additions that didn't make it into RefactoredTheNthLetterOfTheAlphabet. Note that each of the snippets assumes the class, main method, and array have been declared prior to that snippet.

println(Arrays.toString(alphabet))

import java.util.Arrays;

// Print all array elements using the name of the array
System.out.println(Arrays.toString(alphabet));

This program will only work if the import statement import java.util.Arrays; is included in the source file. That's because this implementation uses the .toString() method, a method of the Arrays class. This method allows me to pass the alphabet array in as an argument in order to print its contents.

Here's the output with the above snippet included:

[a, b, c, d, e, f, g, h, i, h, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]

Initially, I tried to print the contents using just the name of the array within the println() method, but Java doesn't let you do that:

// Both will compile but will not print the array elements

System.out.println(alphabet);
System.out.println(alphabet.toString());

Both of those commands compiled correctly but returned this:

[Ljava.lang.String;@d716361

Boyarski & Selikoff explain that [L means you have a list; java.lang.String is the reference type (String in java is not a primitive type; instead, it is a reference type); and @... is a hash code. This is why I need the .toString() method. Once I import the Arrays package, I can run Arrays.toString(alphabet) to print its elements, as shown further above.


println(alphabet[i])

What if I want to print the elements of this array, but on a different line and without the brackets? I can do so with this:

// Print each member of the array, on a new line
for (var i = 0; i < alphabet.length; i++) {
  System.out.println(alphabet[i]);
}

For this implementation, I don't need the import statement or Arrays.toString(). I'm doing something different here: I'm not instructing the compiler to print "everything in this array", I'm telling it to "print each individual element" and specifying which element to print and how. So it's now an index-based program.

The only downside is longer code: this requires a for loop, a local variable, the length value, and the use of i as both an increment variable (i++) and an index variable ([i]). That's a lot. (By the way, length is a final field, or property, of an Array object. It corresponds in this case to the number of letters in the alphabet, which is 26. In other words, the length of this array is 26 because it has 26 elements inside. And since the index i starts at 0, i < alphabet.length works perfectly as the for loop's condition statement).

But it works and the program outputs this:

a
b
c
d
e
f
g
h
i
h
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z

println(e)

// The most concise, and best-practice, way to print each member of the array, on a new line
for (String e : alphabet) {
  System.out.println(e);
}

This last one implements a for-each loop. It's by far the simplest approach to take, and it's the best-practice one to boot. It doesn't require an import statement. I don't need to declare and use an increment variable. It's easy to read ("For every String element e in the alphabet array, print the element."). And it's crisp, with just e as the argument passed into the println() method.

The result is the same as above:

a
b
c
d
e
f
g
h
i
h
j
k
l
m
n
o
p
q
r
s
t
u
v
w
x
y
z

Refactored Program 4: Combine Two Arrays

Here's my old program, CombineTwoArrays:

/*
A program that combines two arrays into a third array and prints its elements.
*/

class CombineTwoArrays {
  public static void main(String[] args) {

    String array1[] = {"a","b","c"}; // declare and define the first array
    String array2[] = {"d","e","f"}; // declare and define the second array

    // A third array created by the program using the first two arrays as input.  
    String array3[] = {array1[0], array1[1], array1[2], array2[0], array2[1], array2[2]};

    int x = array1.length + array2.length; // for use in the while loop condition below
    int y = 0; // a starting point for the while loop condition below

    while (y < x) {
      System.out.println(array3[y]); // print the members of array3
      y++;
    }
  }
}

And my new one, RefactoredCombineTwoArrays:

/*
A program that combines two arrays into a third array and prints its elements.
*/

class RefactoredCombineTwoArrays {
  public static void main(String[] args) {

    String[] abc = {"a","b","c"};
    String[] def = {"d","e","f"};
    String[] abcdef = { abc[0], abc[1], abc[2], def[0], def[1], def[2] };

    for (String e : abcdef) {
      System.out.println(e);
    }
  }
}

Refactored Code Summary

The compiler does the following:

  • Begin program execution
  • Create an array named abc, with three elements
  • Create an array named def, with three elements
  • Create an array named abcdef, with six elements, using the 0th, 1st, and 2nd index of arrays abc and def, respectively
  • Print the elements of the abcdef array
  • Exit the program

Old vs New: What's Changed

Here's a summary of the changes:

CHANGE LOG

  • Renamed the arrays to be more descriptive
  • Cosmetic change: moved the array symbol [] immediately after the String type
  • Replaced the while loop with a for-each loop
  • Removed the variables x and y

(I tried to implement abcdef in a way that didn't require me to manually specify the indices of abc and def{ abc[0], abc[1], ..., def[2] }—but I couldn't figure out how.)

Details

Here's more about each change.

Renamed the arrays to be more descriptive

Names like array1 and array2 are missed opportunities to create clarity for the code reader. The names abc and def are more descriptive, convey more information, and are shorter to boot.

Cosmetic change: moved the array symbol [] immediately after the String type

The original program places the array brackets [] to the right of the array names. In the new program, I've left-shifted the brackets to just after the String type. This makes no difference to the compiler but is the more conventional placement for the array brackets.

Replaced the while loop with a for-each loop

For reasons covered previously in this article, a for-each loop has advantages over a while loop. This for-each loop also shortened my refactored code dramatically (to nearly half the program length).

Removed the variables x and y

These variables were helper variables to the while loop in the old program. In retrospect, the x was unnecessary even then: array3.length would have gotten the same job done:

while (y < array3.length) // replaces "while (y < x)"

So declaring and instantiating x was wasteful.

As for y, it played a role as both increment variable and index variable. But in my refactored program, I don't need that: the logic baked into the for-each loop automatically iterates through the specified array in a similar way without the need for a dedicated incrementor or index variable.

Output(s)

$ java RefactoredCombineTwoArrays
a
b
c
d
e
f

Key Takeaway(s)

Use descriptive names for arrays. Don't declare unnecessary instance variables to store a property that can be accessed directly, such as length. A well-designed for-each loop can shrink your code dramatically and doesn't require extraneous variable declarations.

Refactored Program 5: Prime Detector

Here's my original PrimeDetector program (formerly referred to as "Prime Checker"):

/*
A program to check if an integer number supplied by the user is or is not a prime number. The maximum input value is 2_147_483_647.
*/

import java.util.Scanner;

class PrimeDetector {
  public static void main(String[] args) {

    Scanner userInput = new Scanner(System.in);
    System.out.println("Enter an integer to check if it is a prime number:"); // ask the user for an integer to check
    int i = userInput.nextInt(); // store the user's integer in the i variable

    if (i <= 0) { // prime numbers can't be negative or zero
      System.out.println("The number "+ i + " is not a prime number.");
    }

    else if (i == 2 || i == 3 || i == 5 || i == 7){ // single-digit prime numbers
      System.out.println("The number "+ i + " is a prime number.");
    }

    // The number "1" or any number divisible by "2", "3", "5", "7", or "9" cannot be prime.
    else if (i != 1 // the number "1" is by definition not a prime
                    && i % 2 != 0
                    && i % 3 != 0
                    && i % 5 != 0
                    && i % 7 != 0
                    && i % 9 != 0) { // a redundant condition that does not harm the program

      System.out.println("The number "+ i + " is a prime number.");
    }

    else {
      System.out.println("The number "+ i + " is not a prime number.");
    }
  }
}

Here's the refactored version, RefactoredPrimeDetector:

import java.util.Scanner;

/*
A program that evaluates an integer number supplied by the user and determines if it is a prime number or not. The input value can be any integer from -2,147,483,648 to +2,147,483,647.
*/

class RefactoredPrimeDetector {
  public static void main(String[] args) {
    Scanner userInput = new Scanner(System.in);
    System.out.println("Enter an integer to check if it is a prime number:");
    var x = userInput.nextInt();

    switch (x) {
      case 0: case 1:
      System.out.println("The number "+ x + " is not a prime number.");
      break;

      case 2: case 3: case 5: case 7:
      System.out.println("The number "+ x + " is a prime number.");
      break;

      default:
        if (x >= 0 && (x % 2 != 0) && (x % 3 != 0) && (x % 5 != 0) && (x % 7 != 0)) {
          System.out.println("The number " + x + " is a prime number.");
        } else {
          System.out.println("The number " + x + " is not a prime number.");
        }
      break;
    }
  }
}

Refactored Code Summary

The compiler will perform these steps, in order:

  • Import the java.util.Scanner package
  • Begin program execution
  • Create a Scanner object named userInput, capable of accepting user input via standard input from the keyboard
  • Create and initialize the variable x to store what the user inputs
  • Evaluate the value of x in the switch statement for the cases listed within
  • Exit the program

Old vs New: What's Changed

Here's a summary of the changes:

CHANGE LOG

  • Changed int to var in the declaration for x
  • Replaced the if-then-else statement with a switch statement

Details

Here's more information about each change.

Changed int to var in the declaration for x

In place of the primitive type int, I went with the var keyword here. I also renamed i to x, since this isn't an increment variable, just a regular placeholder variable for a value.

Replaced the if-then-else statement with a switch statement

For starters, I was able to share one case branch for case 0 and case 1. Doing so replaced some of the logic found in the former if (i <= 0) and else if (i != 1...).

The case branch for case 2, case 3, case 5, and case 7 replaces one of the former else if branches. Admittedly, this part of my refactored code is about the same length as the corresponding else if statement, so the gain in this part of the code is in readability only.

But it's worth noting that the new switch statement is overall both easier to read and slightly shorter than the original if-then-else statement. Admittedly, that's because of how I stacked boolean conditions in the old program, which made it longer than it had to be. Also, the new switch statement includes an if-then-else statement in the default case, so it's not exactly a home run in terms of simplicity.

Still, the default case is easier to digest than what I wrote previously to cover most of the allowable values. It's also where I had the most readability gains in my refactored code. Here's how it works:

  • x is either a prime number or it's not
  • the values 0, 1, 2, 3, 5, and 7 are already accounted for in the other switch statements. So any other legal value for x will be evaluated like this:
    • if it's a negative value, it will fail the x >= 0 part of the if statement, so it will be deemed not prime (negative values cannot be prime, by definition)
    • if it's a positive value (x >= 0) AND it can be divided evenly neither by 2 nor 3 nor 5 nor 7, then it will pass the if statement, so it will be classified as a prime (because, by definition, it will only be divisible by "1" and itself)
    • if it's a positive value (x >= 0) BUT it fails any of the other conditions in the if branch, then it fails the if statement as a whole, so it will be be classified as not a prime

As before, this program will only work for the range of inputs that are legal for int values in Java: i.e., from -2147483648 to +2147483647.

There may be an easier way to implement this program, but I don't know one yet.

Output(s)

$ java RefactoredPrimeDetector
Enter an integer to check if it is a prime number:
-1234567
The number -1234567 is not a prime number.
$ java RefactoredPrimeDetector
Enter an integer to check if it is a prime number:
11
The number 11 is a prime number.
$ java RefactoredPrimeDetector
Enter an integer to check if it is a prime number:
11232134
The number 11232134 is a prime number.

Key Takeaway(s)

When there are a lot of possible values to consider, a switch statement is indeed more useful than an if-then-else statement. Some conditional logic can be programmed more easily and less confusingly inside the default case of a switch statement.


Wrap-up

This concludes my refactoring exercise. The toy programs I made a month ago are now modernized:

  • RefactoredTwoLanguages
  • RefactoredEvenOddWhileLoop (and RefactoredCustomEvenOddWhileLoop)
  • RefactoredTheNthLetterOfTheAlphabet
  • RefactoredCombineTwoArrays
  • RefactoredPrimeDetector

In most cases, I was able to shorten and/or simplify the programs. Even in cases where the new program was the same length or slightly longer than before, the code was generally cleaner and easier to read. I also gained a deeper understanding of some of the rules covered Effective Java while applying some of the more foundational knowledge gained from the other books I've been studying and the Google Java Style Guide I've digested. I'm pleased with the result.

Next Up: Month Three of My Learnings

The more Java basics I learn, the more I continue to appreciate how immense this language is. There's a great deal that works out of the box, and the box itself is enormous. So there's a lot more to learn, and I'm having with the journey.


Till next time.

Top comments (0)