Basic Java Bootcamp

Would you like to try an interactive, classroom-based approach to learning Java?

About This Bootcamp

Do you know a programming language other than Java fairly well, but would like to learn Java? Join this bootcamp to gain proficiency with the basics!

The notes are meant to be used in an interactive, classroom setting. The notes by themselves do not present sufficient explanation for each activity. There is little to no explanation of any of the technical aspects of the language. Explanations come during the structured in-person lessons.

How It Works

This bootcamp is designed to be run under the I DO, WE DO, YOU DO model.

For each activity, the instructor (I DO) will first demo the finished product, explain the learning objectives, and highlight the code structure. Next, the students and teacher (WE DO) will do a code along; students can copy-paste or type the code for each activity and make sure they can run the code. The instructor will ask students to predict what certain small changes (or extensions) to the code will do, then instructor and students will try these changes together. A number of language-specific questions from students can show up in this stage as well, that’s great! Finally, several exercises to extend the activity code further are presented: the instructor can initiate one or more of these activities with the students (keeping the WE DO phase going a little longer), but should release the students into the YOU DO phase to complete the suggested extensions for independent practice when it makes sense to do so. If the independent practice takes place in a classroom or lab, TAs and instructors should be available, and students, when confident, should perform peer teaching and learning as well.

Learning Objectives

By the end of this bootcamp, you will have a good understanding of:
  • The structure of a Java app
  • The main method
  • Declaring variables inside methods (var)
  • Printing strings (System.out.println)
  • Formatting strings (String.format, System.out.printf)
  • Operations on numbers
  • Operations on strings, such as repeat, split, and strip
  • Assignments, If-statements, While-loops, and For-loops
  • try, catch, and throw
  • Command line arguments and arrays
  • Defining new fields and methods
  • static and final
  • Using built-in packages like java.time
  • Using JShell
  • Generating random numbers
  • What arrays, lists, sets, and maps are, and how to use them
  • Mutable vs. Immutable objects
  • Defining classes
  • Constructors, accessors, toString
  • The basics of JUnit
  • Records
  • Utility classes
  • Abstract classes
  • Inheritance and Polymorphism
This is a programming bootcamp, not a “Java Developer” bootcamp

The activities here are designed to expose you to elements of the Java programming language. They do not cover “enterprise” programming techniques such as organizing apps with Maven, using frameworks such as Spring, or building networked or web applications. All activities are very small command line apps, with the only external library being JUnit for unit testing.

Prerequisites

This boot camp assumes that you already:

  1. Have programmed before, even just a little, in some language, and used variables, if and while statements, and functions
  2. Have a Java Development Kit (JDK) installed on your machine, and know how to compile and run Java programs from the command line
  3. Know how to use JShell

Please go through my Getting Started with Java page if you need to complete items 2 and 3.

Activities

We’ll work through these activities in order. You might find it rewarding, as you type in and run each program (or perform the shell exercises), to try to predict what each Java construct does, and how it is similar to and different from, those in the languages you know already.

Making Waves

main Variables Math for-loops String repeat System.out.println

ocean waves

Let’s begin by making console art 🧑🏽‍🎨. There’s a little bit of arithmetic here, but how we came up with the little formula is not important right now. If you can figure it out, fine, but the emphasis in this first activity is on getting a feel for Java command line applications, not showing off your knowledge of trigonometry:

WavePrintingApp.java
class WavePrintingApp {
    public static void main(String[] args) {
        for (var row = 0; row < 64; row++) {
            var waveHeight = Math.abs(Math.sin(row * Math.PI / 16.0) + 1);
            var numberOfStars = (int) Math.round(12 * waveHeight);
            System.out.println("*".repeat(numberOfStars));
        }
    }
}

To run it:

$ javac WavePrintingApp.java&& java WavePrintingApp
************
**************
*****************
*******************
********************
**********************
***********************
************************
************************
************************
***********************
**********************
********************
*******************
*****************
**************
************
**********
*******
*****
****
**
*



*
**
****
*****
*******
**********
************
**************
*****************
*******************
********************
**********************
***********************
************************
************************
************************
***********************
**********************
********************
*******************
*****************
**************
************
**********
*******
*****
****
**
*



*
**
****
*****
*******
**********
Exercise: The trig function Math.sin might be a little scary, so modify the program to make a “sawtooth” wave.
Exercise: The values 64 (number of rows), 12 (half the maximum number of stars per row), and 16.0 (half the number of rows per cycle) are just sitting in the middle of the code, making it look kind of mystical. Such unexplained values in the middle of one’s code are called magic numbers and we should avoid them. Fix the code to use well-named variables for these values, so a reader can surmise more about what is going on.
Exercise: Experiment with different expressions for generating lines of stars. Let your creativity show through.

Coin Flipping

coin-flip.png

final String formatting Comments

What are your odds of tossing a certain number of heads in a row? Inquiring minds want to know. We can figure out that you have a 1 out of 2 chance of tossing heads, then 1 out of 4 to get two heads in a row, 1 out of 8 for three. But how about in general? Or at least for anything up to 10 tosses?

Let’s print the odds for the first few, both in the “one out of N” form and as percentages, too:

CoinFlippingOddsApp.java
/**
 * A command line application that reports the odds of tossing a fair coin 1-10
 * times and getting heads each time.
 */
class CoinFlippingOddsApp {
    public static void main(String[] args) {

        // We'll show odds both in "1 in n" format and in percentage format
        final var message = "Odds of throwing %d heads in a row is 1 in %d (%.8g%%)";

        var possibilities = 2;
        for (var tosses = 1; tosses <= 10; tosses++) {
            final var percentage = 100.0 / possibilities;
            final var line = String.format(message, tosses, possibilities, percentage);
            System.out.println(line);

            // Ready for the next iteration
            possibilities *= 2;
        }
    }
}

Try it yourself! You should see output like this:

$ javac CoinFlippingOddsApp.java && java CoinFlippingOddsApp
Odds of throwing 1 heads in a row is 1 in 2 (50.000000%)
Odds of throwing 2 heads in a row is 1 in 4 (25.000000%)
Odds of throwing 3 heads in a row is 1 in 8 (12.500000%)
Odds of throwing 4 heads in a row is 1 in 16 (6.2500000%)
Odds of throwing 5 heads in a row is 1 in 32 (3.1250000%)
Odds of throwing 6 heads in a row is 1 in 64 (1.5625000%)
Odds of throwing 7 heads in a row is 1 in 128 (0.78125000%)
Odds of throwing 8 heads in a row is 1 in 256 (0.39062500%)
Odds of throwing 9 heads in a row is 1 in 512 (0.19531250%)
Odds of throwing 10 heads in a row is 1 in 1024 (0.097656250%)
Exercise: How did we know which variables to mark final and which ones not to?
Exercise: Modify the program for up to 100 tosses.
Exercise: Modify the program to work for a biased coin that comes up heads two-thirds of the time.

Egg Counting

egg-crates.png

Console readline parseInt if-statements Catching exceptions printf

Time to get interactive. Let’s see how to write Java console programs that prompt a user for input.

The following program will prompt you to enter a number of eggs, then will show you how the pros count them. Let’s hope the hens that laid them are cage free. You shouldn’t buy eggs from factory farms with caged chickens. That would be terrible.

EggCountApp.java
/**
 * An application that prompts the user for a number of eggs, and reports this
 * number in grosses and dozens.
 */
class EggCountApp {
    public static void main(String[] args) {
        System.out.println("""
                Welcome to the Egg Counter! 🥚🥚🥚

                If you have less than a billion eggs, we'll help you count
                them, like the pros do.
                """);
        try {
            String response = System.console().readLine("How many eggs? ");
            var eggs = Integer.parseInt(response);
            if (eggs < 0) {
                throw new IllegalArgumentException("So sorry you have negative eggs!");
            } else if (eggs >= 1000000000) {
                throw new IllegalArgumentException("No way can you have that many!");
            }
            var gross = eggs / 144;
            var excessOverGross = eggs % 144;
            var dozens = excessOverGross / 12;
            var leftOver = excessOverGross % 12;
            System.out.printf("You have %d gross, %d dozen, and %d%n",
                    gross, dozens, leftOver);
        } catch (NumberFormatException e) {
            System.err.println("App can only handle integers up to a billion");
        } catch (IllegalArgumentException e) {
            System.err.println(e.getLocalizedMessage());
        }
    }
}

Here's a run:

$ javac EggCountApp.java && java EggCountApp
How many eggs? 8398
You have 58 gross, 3 dozen, and 10
$ javac EggCountApp.java && java EggCountApp
How many eggs? 500
You have 3 gross, 5 dozen, and 8
$ javac EggCountApp.java && java EggCountApp
How many eggs? 2000000888
No way can you have that many!
$ javac EggCountApp.java && java EggCountApp
How many eggs? -5
So sorry you have negative eggs!
$ javac EggCountApp.java && java EggCountApp
How many eggs? CHICKEN DINNER HAHA
App can only handle integers up to a million
Exercise: Clean up the magic numbers. Consider defining new variables for eggs per dozen, dozens per gross, and so on.
Exercise: A “great gross” is 12 grosses (1728 items). Make the program handle this unit.
Exercise: Make the program only show units for which the count is nonzero.

Elementary School Vibes

Command line arguments Arrays Custom fields Custom methods static Throwing exceptions

Get in your time machine and go back to elementary school, people!

elementary school

You’re gonna need your multiplication tables. Since they don’t print those out any more (paper shortage) you better generate your own. In this activity we’ll print a multiplication table to standard output whose size will be a command line argument. We’ll see that in Java, the command line arguments are passed in an array to the main method of the app.

As in the previous activity, this program has user input, so checking is required. That’s one of the major principles of software security: validating input. In this app, we don’t want non-integers and we don’t want values too big or too small.

MultiplicationTableApp.java
class MultiplicationTableApp {

    private static final int MIN_SIZE = 1;
    private static final int MAX_SIZE = 20;

    private static final String ARGUMENT_COUNT_ERROR = "Exactly one command line argument required";
    private static final String ARGUMENT_FORMAT_ERROR = "Argument must be an integer";
    private static final String SIZE_ERROR = String.format(
            "Size must be between %d and %d", MIN_SIZE, MAX_SIZE);

    private static void printTable(int size) {
        if (size < MIN_SIZE || size > MAX_SIZE) {
            throw new IllegalArgumentException(SIZE_ERROR);
        }
        for (var i = 1; i <= size; i++) {
            for (var j = 1; j <= size; j++) {
                System.out.printf("%4d", i * j);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        try {
            if (args.length != 1) {
                throw new IllegalArgumentException(ARGUMENT_COUNT_ERROR);
            }
            printTable(Integer.parseInt(args[0]));
        } catch (NumberFormatException e) {
            System.err.println(ARGUMENT_FORMAT_ERROR);
        } catch (IllegalArgumentException e) {
            System.err.println(e.getLocalizedMessage());
        }
    }
}
$ javac MultiplicationTableApp.java && java MultiplicationTableApp 5   
   1   2   3   4   5
   2   4   6   8  10
   3   6   9  12  15
   4   8  12  16  20
   5  10  15  20  25
$ javac MultiplicationTableApp.java && java MultiplicationTableApp 2
   1   2
   2   4
$ javac MultiplicationTableApp.java && java MultiplicationTableApp
Exactly one command line argument required
$ javac MultiplicationTableApp.java && java MultiplicationTableApp 1 2 3
Exactly one command line argument required
$ javac MultiplicationTableApp.java && java MultiplicationTableApp dog
Argument must be an integer
$ javac MultiplicationTableApp.java && java MultiplicationTableApp 99
Size must be between 1 and 20
Exercise: Improve the error reporting. If the number of command line arguments (length of the args array) is incorrect, report not only that one argument was expected, but also how many arguments were given instead.
Exercise: Do the same if the table size parameter is out of the desired range. Message the user not only what they entered, but also the expected range.
Exercise: Rather than a multiplication table, make a power table. Each cell will need to be more than 4 characters wide, and in fact the columns get wide rather quickly. Therefore, make the cell size dependent on the largest value in the column.

Which Day of the Week Were You Born On?

calendar.png

import The Time Package Locales The conditional operator

Java has a ton of cool libraries, including some calendar features. Enter your date of birth as a command line argument (e.g. 2001-09-25) and this app will tell your what day of the week you were born on.

Ok technically your input does not have to be your birthday—it works for any reasonable date. And for super bonus fun time, this app can display day names in any locale that your computer supports. Programmers should be inclusive!

DayOfWeekApp.java
import java.time.LocalDate;
import java.util.Locale;
import java.time.format.TextStyle;

class DayOfWeekApp {
    public static void main(String[] args) {
        try {
            if (args.length < 1 || args.length > 2) {
                throw new IllegalArgumentException("Program requires 1 or 2 arguments");
            }
            var date = LocalDate.parse(args[0]);
            var locale = args.length == 2
                    ? Locale.forLanguageTag(args[1])
                    : Locale.getDefault();
            System.out.println(
                    date.getDayOfWeek().getDisplayName(TextStyle.FULL, locale));
        } catch (Exception e) {
            System.err.println(e.getLocalizedMessage());
        }
    }
}

Let’s run it:

$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18
Monday
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18 es
lunes
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18 hi
सोमवार
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18 ar
الاثنين
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18 ru
понедельник
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18 fr
lundi
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-50
Text '2022-07-50' could not be parsed: Invalid value for DayOfMonth (valid values 1 - 28/31): 50
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-02-29
Text '2022-02-29' could not be parsed: Invalid date 'February 29' as '2022' is not a leap year
$ javac DayOfWeekApp.java && java DayOfWeekApp 20225-02-01
Text '20225-02-01' could not be parsed at index 0
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-13-01 
Text '2022-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-02  
Text '2022-02' could not be parsed at index 7
$ javac DayOfWeekApp.java && java DayOfWeekApp
Program requires 1 or 2 arguments
$ javac DayOfWeekApp.java && java DayOfWeekApp 2022-07-18 hi extra
Program requires 1 or 2 arguments
Exercise: Add to the output which day of the year it is. You’ll need to check out the documentation for LocalDate or just search the web for how to do this.
Exercise: Modify the program to take in the year, the month, and the day, as three separate command line arguments.
Exercise: Research how to display the inputted date itself, in various text styles. Make your program respond with an entire phrase, such as, in English, “August 1, 1987, was a Saturday.” To get really deep into the world of dates, try to distinguish past dates from future dates in your output. Research will be required, but familiarity with the documentation is a necessary skill for programmers!
Exercise: (Advanced) Find out how to get the list of all locales your system supports, and if the user submits a locate that is not supported, give an error message.

Nonsense Sentences

nonsense.png

Lists Maps Random objects String join

Time to get silly and generate nonsense sentences. We are starting simple, with our program featuring only a single, fixed, story template, and a built-in set of possible words.

This little program features lists and maps. A “list” is what Python calls a list. A “map” is what Python calls a dictionary. If you’ve done Python before, you know that you can modify your lists and maps, but in Java, using List.of and Map.of produce unmodifiable objects! This is nice and secure for fixed data like we have in our program below. (In a future exercise we will see how to make lists and maps that you can modify.)

NonsenseApp.java
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.Random;

class NonsenseApp {
    private static Random random = new Random();

    public static void main(String[] args) {
        var words = Map.of(
                "noun", List.of("dog", "carrot", "chair", "toy", "rice cake"),
                "verb", List.of("ran", "barked", "squeaked", "flew", "fell", "whistled"),
                "adjective", List.of("small", "great", "fuzzy", "funny", "light"),
                "preposition", List.of("through", "over", "under", "beyond", "across"),
                "adverb", List.of("barely", "mostly", "easily", "already", "just"),
                "color", List.of("pink", "blue", "mauve", "red", "transparent"));
        var tokens = List.of(
                "Yesterday", "the", "color", "noun",
                "verb", "preposition", "the", "coach’s",
                "adjective", "color", "noun", "that", "was",
                "adverb", "adjective", "before");

        var firstWord = true;
        for (var token : tokens) {
            if (!firstWord) {
                System.out.print(" ");
            }
            if (words.containsKey(token)) {
                var choices = words.get(token);
                var randomChoice = choices.get(random.nextInt(choices.size()));
                System.out.print(randomChoice);
            } else {
                System.out.print(token);
            }
            firstWord = false;
        }
        System.out.println(".");
    }
}

Have fun generating silly sentences!

Exercise: Add larger word lists and more types of words.
Exercise: Add multiple new sentence templates. A run of the program should generate a random nonsense short story by generating five random sentences, each of which comes from a randomly selected template.

JShell Interlude

JShell BigInteger String length Code points Array operations More operations on collections

jshell.png

So far we’ve been concentrating exclusively on fully-functioning command line apps, that we compile then run on the command line. We’ve only done enough Java to make our applications work, without worrying about all the details of, and all the possible operations on, our numbers, strings, booleans, arrays, lists, and maps.

Let’s shift our focus from apps, just for a short time, and use JShell to practice with some language features that are really good to know about. The session below contains a number of new Java features, some of which we will talk about some during class. If something isn’t mentioned in class, you’re encouraged to read the documentation to learn about it. If still stuck, ask!

Warm Up

Here are a few warm up exercises:

jshell> 5 + 3
$1 ==> 8

jshell> 5 > 3
$2 ==> true

jshell> Math.hypot(3, 4)
$3 ==> 5.0

jshell> var words = List.of("one", "two", "three", "four", "five")
words ==> [one, two, three, four, five]

jshell> words.subList(1, 4)
$5 ==> [two, three, four]

jshell> IntStream.range(1, 10).sum()
$6 ==> 45
Exercise: Explain the result of the .subList expression.
Exercise: Why do you think the result of the last expression above was 45, and not 55? Try rangeClosed in place of range and see what happens.

Numbers

Try these number-oriented expressions on your own. (Notice that Java, like Python, distinguishes between integers and floating point values. Unlike Python, though, Java’s “regular” integers are bounded, so to be like Python you have to use Java’s BigInteger.)

2 + 5 * 8
Math.atan(1) + Math.atan(2) + Math.atan(3)
Math.abs(-200) / Math.pow(2, 3)
88.75 % 2.5
0b01101001
0x2a8f
Integer.toString(100501, 16)
2000000000 + 2000000000
Math.addExact(2000000000, 2000000000)
BigInteger.valueOf(2000000000).add(BigInteger.valueOf(2000000000))
BigInteger.valueOf(2).pow(5000)
88e53 / 3.888E-218
88e199 / 3.888E-218
Exercise: Make sure you understand what happens when regular integer arithmetic “overflows,” what happens when integer arithmetic with addExact, multiplyExact (and similar) overflow, and what happens when floating-point arithmetic overflows.

Strings

Explore Java strings by entering these expressions into JShell:

"".isEmpty()
"    ".isEmpty()
"    ".isBlank()
"    abc   ".stripLeading()
"    abc   ".stripTrailing()
"    abc   ".strip()
"doghouse".startsWith("do")
"doghouse".endsWith("house")
"doghouse".contains("ghou")
"yak".repeat(3)
"Please don't shout!".toUpperCase()
"Please don't shout!".toLowerCase()
"resume".replace('e', 'é')
"dog" + "house"
var poem = """
    Over the wintry
    forest, winds howl in rage
    with no leaves to blow.
    """
var pet = "Sparky"
var fren = "s".toUpperCase() + "park".concat("y")
pet == fren
pet.equals(fren)
var s = "👍👩‍🔬$🇺🇾"
s.length()
s.charAt(7)
s.codePoints().toArray()
s.codePoints().mapToObj(Character::getName).toArray()
"box-3-21-Q255".split("-")
poem.split("\n")
Exercise: Explain the difference between strings that are empty and those that are blank.
Exercise: What did these explorations show you about string comparison with ==?
Exercise: What did these explorations show you about trying to take the length of a string?
== versus .equals

The concept of whether “two expressions are equal to each other or not” is difficult in most programming languages but especially bizarre and complicated in Java. Detailed coverage of equality can take hours to explain. We may be able to cover the basics in this bootcamp, but a fuller treatment is out of scope for the moment.

Arrays

The last four exercises in the section above all produced array objects. In our previous activities we have seen arrays only for command line arguments, but they do appear in other places, too. You can even create your own. Generally, you will prefer lists to arrays, but arrays are very important so you need to know how to use them.

var instructions = "mark ready set go".split(" ")
instructions[0]
instructions[2]
instructions[18]
new boolean[]{true, true, false, true}
new boolean[3]
new int[]{3, 5, 8, 13, 21}
var directions = new String[]{"north", "east", "south", "west"}
directions.length
Arrays.sort(directions)
directions
var a = new int[]{0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
var b = new int[]{0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55}
a == b
Arrays.equals(a, b)
System.out.println(a)
System.out.println(Arrays.toString(a))
Arrays.fill(a, 2)
a
Arrays.setAll(a, i -> 10 * i)
a
a = Arrays.copyOf(b)
a[5] = 100000
Arrays.mismatch(a, b)
Exercise: (Research) Find out how to sort an array in reverse order.

Lists

In the previous activity (Nonsense Sentences), we barely scratched the surface of what lists can do. So there’s now some interesting things to learn. First, let’s review what we know: List.of makes an unmodifiable list:

var frens = List.of("Kim", "Ron", "Wade")
frens.add("Bonnie")

In order to be able to add to a list we have to make a new list. There are various kinds of lists; the most popular are ArrayLists and LinkedLists. Let’s make a new ArrayList and demonstrate that we can indeed add to it:

var frens = new ArrayList<String>(List.of("Kim", "Ron", "Wade"))
frens.add("Monique")
frens

Now a tour of list operations. First, operations that only access the list, without changing it:

var a = List.of(100, 200, 200, 500, 300, 200, 100);
var b = List.of(100, 200, 300, 400, 700);
a.isEmpty()
a.size()
a.get(3)
a.contains(300)
a.containsAll(List.of(200, 100))
a.indexOf(200)
a.lastIndexOf(200)
a.subList(2, 4)
a.equals(List.of(100, 200, 500))
Collections.disjoint(a, b)
Collections.max(a)
Collections.min(a)
Collections.frequency(a, 200)
Collections.frequency(a, 500)
Collections.binarySearch(b, 400)
Collections.binarySearch(b, 100)
Collections.binarySearch(b, 500)

Now here are properties that actually change the list! In JShell, you can perform the operation then inspect the value of the object in one line, using a semicolon:

var a = new ArrayList<Integer>(List.of(34, 5, 2, 8))
a.set(1, 89) ; a
a.replaceAll(x -> x + 10) ; a
a.sort(Comparator.naturalOrder()) ; a
a.sort(Comparator.reverseOrder()) ; a

var a = new ArrayList<Integer>(List.of(100, 90, 80, 70, 60, 50, 40, 30, 20, 10))
Collections.shuffle(a) ; a
Collections.reverse(a) ; a
Collections.sort(a) ; a
Collections.rotate(a, 3) ; a
Collections.swap(a, 2, 7) ; a
Collections.fill(a, 3) ; a

var a = new ArrayList<String>(List.of("Once", "I", "read", "a", "novel"))
a.removeIf(w -> w.length() < 3) ; a

Maps

We saw maps in the previous exercise, just briefly. So try these exercises to help make things clearer (hopefully!). First, for simple unmodifiable maps of up to 10 items, Map.of is fine.

var capitals = Map.of("Hhohho", "Mbabane", "Manzini", "Manzini", 
    "Lubombo", "Siteki", "Shiselweni", "Nhlangano")
capitals.isEmpty()
capitals.size()
capitals.keySet()
capitals.values()
capitals.entrySet()
capitals.containsKey("Manzini")
for (var e : capitals.entrySet()) {
   System.out.println("Capital of " + e.getKey() + " is " + e.getValue()); 
}
capitals.forEach((k,v) -> System.out.println("Capital of " + k + " is " + v))

If your map needs to grow and shrink, create it with “new HashMap” or “new TreeMap”.

var counts = new HashMap<String, Integer>()
counts.put("dog", 3) ; counts
counts.putAll(Map.of("rat", 2, "bat", 5)) ; counts
counts.put("rat", 10) ; counts
counts.putIfAbsent("rat", 5) ; counts
counts.getOrDefault("dog", 0)
counts.getOrDefault("cow", 0)
counts.remove("dog") ; counts
conunts.clear() ; counts
Exercise: Create a large HashMap with, say 50 entries whose keys are strings. Iterate through the map, printing each entry. Then enter the same data into a TreeMap, and print each of its entries with a for-loop. What did you notice about the TreeMap’s iteration?
Exercise: Strike out on your own! Surely there are many, many things you would like to try. Look through Java documentation to find some interesting things, and see if you can craft expressions to discover questions you have that arise.

Small Countries

small-countries.png

Sets String split while loops break replaceAll

Can you guess the 10 smallest countries in the world by population?

Let’s make this question into a quiz game!

We’re going to take this opportunity to introduce a new collection type: the set. We’ll start with the 10 countries in a set. When you guess a country correctly, we’ll remove it. If you manage to empty out the set, you will WIN. 🎉🎉🎉

Fun thing to note: Just as with lists and maps, you make modifiable and unmodifiable sets differently. Here’s a table that can help you remember:

TypeUnmodifiableModifiable
ListList.of(...)new ArrayList<elementType>()
new LinkedList<elementType>()
MapMap.of(...)new HashMap<keyType, valueType>()
new TreeMap<keyType, valueType>()
SetSet.of(...)new HashSet<elementType>()
new TreeSet<elementType>()

Let’s play:

SmallCountryGuessingApp.java
import java.util.Collections;
import java.util.HashSet;

class SmallCountryGuessingApp {
    private static final String names = """
            vaticancity
            nauru
            tuvalu
            palau
            sanmarino
            liechtenstein
            monaco
            stkitts
            marshallislands
            dominica
            """;

    public static void main(String[] args) {
        var countries = new HashSet<String>();
        Collections.addAll(countries, names.split("\n"));

        System.out.println("""
                Welcome to the Country Guessing Game. Guess the 10 smallest
                countries by population. If at any time you want to give up,
                just enter the single letter q at the prompt.
                """);

        var message = "Guess a country";
        while (!countries.isEmpty()) {
            String response = System.console().readLine(message + ": ");
            response = cleanupGuess(response);
            if (response.equals("q")) {
                break;
            } else if (countries.contains(response)) {
                countries.remove(response);
                message = "Great, " + countries.size() + " more to go";
            } else {
                message = "Try again";
            }
        }
        if (countries.isEmpty()) {
            System.out.println("Wow amazing job!");
        } else {
            // Be nice, and show the ones still in the set
            System.out.println("That's okay, you only missed " + countries);
        }
    }

    /**
     * We want to be liberal with what we receive from the user, trying our best to
     * "normalize" it. We lowercase then remove all non-letter characters for ease
     * of comparison. But there are some special cases to consider also.
     */
    private static String cleanupGuess(String guess) {
        guess = guess.toLowerCase().replaceAll("[^\\p{L}]", "");

        // Allow abbreviation for Saint
        guess = guess.replaceAll("^saint", "st");

        // Super special case: Get rid of "nevis" or "andnevis"
        return guess.replaceAll("(and)?nevis", "");
    }
}

Here is a sample run:

$ javac SmallCountryGuessingApp.java&& java SmallCountryGuessingApp
Welcome to the Country Guessing Game. Guess the 10 smallest
countries by population. If at any time you want to give up,
just enter the single letter q at the prompt.

Guess a country: Tuvalu
Great, 9 more to go: palau
Great, 8 more to go: russia
Try again: sao tome
Try again: vatican city
Great, 7 more to go: st kitts  and Nevis
Great, 6 more to go: SAN MARINO
Great, 5 more to go: canada
Try again: marshall
Try again: marshall islands
Great, 4 more to go: antigua
Try again: monaco
Great, 3 more to go: andorra
Try again: q
That's okay, you only missed [dominica, liechtenstein, nauru]

Shoutouts to Kate Galvin and Christopher Beaudoin for the idea, and to Evan Mow for doing the research.

Exercise: Modify the program to accept “Marshall Is”, “The Vatican”, and various misspellings of Liechtenstien.
Exercise: Modify the program to respond to entries like “China” with a knowing response that the user is being silly.

Take a Chance

Classes Programs in multiple files

dice.jpg

Our first few activities were all short command line apps that performed some little task, perhaps with input in the form of command line arguments and responses to prompts, then printed a result of some sort. All of these apps used entities of built-in types (numbers, booleans, strings, arrays), or types from a standard Java library (lists, sets, maps, dates, locales, and so on).

Now we’d like to create a type of our own, which in Java we do with the keyword class (or record, enum, or interface, which we’ll see later). Wait, haven’t we made classes before? Didn’t we create classes called WavePrintingApp, CoinFlippingOddsApp, EggCountApp, MultiplicationTableApp, DayOfWeekApp, NonsenseApp, and SmallCountryGuessingApp? Well yes, but the intent was never to make a type for new entities. It’s just that, weirdly, Java requires us to make a class to house the entry point of the program!

Now, finally, we’re going to build a class whose purpose is to allow the creation of many instances. A die is an n-sided entity (n ≥ 4) whose value is between 1 and n, inclusive. Rolling the die changes its value, but the number of sides cannot be changed.

Die.java
import java.util.Random;

/**
 * A simple class for representing die objects. A die has a given number of
 * sides (at least) four, set when the die is constructed and which can never be
 * changed. The die's value can be changed, but only by calling its roll()
 * method.
 */
public class Die {
    private static Random random = new Random();

    private final int sides;
    private int value;

    /**
     * Constructs a die with the given number of sides and starting value.
     * 
     * @throws IllegalArgumentException if the number of sides is less than 4 or if
     * the starting value is not consistent with the number of sides.
     */
    public Die(int sides, int value) {
        if (sides < 4) {
            throw new IllegalArgumentException("At least four sides required");
        }
        if (value < 1 || value > sides) {
            throw new IllegalArgumentException("Die value not legal for die shape");
        }
        this.sides = sides;
        this.value = value;
    }

    /**
     * Simulates a roll by randomly updating the value of this die. In addition to
     * mutating the die's value, this method also returns the new updated value.
     */
    public int roll() {
        this.value = random.nextInt(sides) + 1;
        return this.value;
    }

    /**
     * Returns the number of sides of this die.
     */
    public int sides() {
        return this.sides;
    }

    /**
     * Returns the value of this die.
     */
    public int value() {
        return this.value;
    }

    /**
     * Returns a description of this die, which is its value enclosed in square
     * brackets, without spaces, for example "[5]".
     */
    @Override
    public String toString() {
        return "[" + this.value + "]";
    }
}

Wow! This file does not have a public static void main method in it. That means it is not an actual app. Instead, it is something we can use in other apps.

As simple example of an app that uses die objects, let’s build a small app to roll a pair of die thousands of times, keeping track of how often we roll a 2, or a 3, or a 4, and so on, up to 12:

RollFrequencyExperimentApp.java
import java.util.HashMap;

class RollFrequencyExperimentApp {

    private static final int NUMBER_OF_TRIALS = 100000;

    public static void main(String[] args) {
        var die1 = new Die(6, 1);
        var die2 = new Die(6, 2);
        var counts = new HashMap<Integer, Integer>();
        for (var i = 0; i < NUMBER_OF_TRIALS; i++) {
            var total = die1.roll() + die2.roll();
            counts.put(total, counts.getOrDefault(total, 0) + 1);
        }
        for (var roll = 2; roll <= 12; roll++) {
            var rolls = counts.getOrDefault(roll, 0);
            var stars = (int) Math.round((350.0 * rolls) / NUMBER_OF_TRIALS);
            System.out.printf("%4d | %6d | %s%n", roll, rolls, "*".repeat(stars));
        }
    }
}

Here is how we compile and run the app:

$ javac RollFrequencyExperimentApp.java && java RollFrequencyExperimentApp
   2 |   2789 | **********
   3 |   5775 | ********************
   4 |   8350 | *****************************
   5 |  11113 | ***************************************
   6 |  13922 | *************************************************
   7 |  16525 | **********************************************************
   8 |  13761 | ************************************************
   9 |  11174 | ***************************************
  10 |   8345 | *****************************
  11 |   5508 | *******************
  12 |   2738 | **********

Note you do not have to explicitly compile Die.java, but you can if you want to. The javac program, when asked to compile RollFrequencyExperimentApp.java, will check to see if any of its dependent Java files have changed since the last compile, and if so, will compile them as well.

Exercise: Add a validation to the Die constructor so that a die must have less than 1024 sides.
Exercise: Increase the number of trials to one million. You might need to adjust the printf fields widths to keep the output nice.

Hello, Does This Thing Even Work?

testing.png

Unit Testing JUnit 5 Assertions

So how do you know if your program even works? Tests can increase our confidence that a program is working, and the act of testing can help us find bugs before we ship our code. Many people would say that programmers have a moral obligation to write tests.

Perhaps you’ve already been told about, or better, have experienced, how testing is essential and can save your life.

Here is a test for the Die class we defined above. As usual, the details of the activity. and fine points of testing in general, and testing in Java specifically, will be covered in the classroom.

DieTest.java
import java.util.HashSet;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class DieTest {

    @Test
    public void testConstructorErrors() {
        // Too few sides
        assertThrows(IllegalArgumentException.class, () -> new Die(3, 2));
        // Die value not legal for die shape - too small
        assertThrows(IllegalArgumentException.class, () -> new Die(9, -2));
        // Die value not legal for die shape - too large
        assertThrows(IllegalArgumentException.class, () -> new Die(9, 10));
    }

    @Test
    public void testAccessors() {
        assertEquals(7, new Die(7, 2).sides());
        assertEquals(3, new Die(7, 3).value());
    }

    @Test
    public void testValueAccessorReturnsValueOfLatestRoll() {
        var die = new Die(20, 1);
        var rolled = die.roll();
        var valueRead = die.value();
        assertEquals(valueRead, rolled);
    }

    @Test
    public void testSmallestRollShouldBeAOne() {
        var smallest = Integer.MAX_VALUE;
        var die = new Die(8, 5);
        for (var i = 0; i < 10000; i++) {
            smallest = Math.min(die.roll(), smallest);
        }
        assertEquals(1, smallest);
    }

    @Test
    public void testLargestRollShouldBeNumberOfSides() {
        var largest = Integer.MIN_VALUE;
        var die = new Die(8, 5);
        for (var i = 0; i < 10000; i++) {
            largest = Math.max(die.roll(), largest);
        }
        assertEquals(die.sides(), largest);
    }

    @Test
    public void testToStringWorks() {
        var die = new Die(10, 3);
        var value = die.value();
        assertEquals("[" + value + "]", die.toString());
    }

    @Test
    public void testAllDieValuesArePossible() {
        var die = new Die(12, 6);
        var seen = new HashSet<Integer>();
        for (var i = 0; i < 10000; i++) {
            seen.add(die.roll());
        }
        for (int dieValue = 1; dieValue <= die.sides(); dieValue++) {
            assertTrue(seen.contains(dieValue));
        }
    }
}

The testing code is in a separate library file you need to have on your machine somewhere. In Java. these compiled libraries are called Jar files. You will want to go download the JUnit Jar file and place it on your machine somewhere. But which file? Answer: Go to this page and download the jar named junit-platform-console-standalone.

For simplicity, I downloaded this file and renamed it to just junit.jar and placed it in my home directory. Here is a test run:

$ javac -cp ~/junit.jar:. DieTest.java && java -jar ~/junit.jar -cp . -c DieTest

╷
├─ JUnit Jupiter ✔
│  └─ DieTest ✔
│     ├─ testAccessors() ✔
│     ├─ testLargestRollShouldBeNumberOfSides() ✔
│     ├─ testConstructorErrors() ✔
│     ├─ testToStringWorks() ✔
│     ├─ testAllDieValuesArePossible() ✔
│     ├─ testValueAccessorReturnsValueOfLatestRoll() ✔
│     └─ testSmallestRollShouldBeAOne() ✔
└─ JUnit Vintage ✔

Test run finished after 139 ms

All our tests passed.

Exercise: Change the test program so that you know some tests will fail. Run the tests and study the output.

You don’t have to rename the Jar file, nor do you have to put it in your home directory. You can put it anywhere you like, really.

You can also run your tests from within VS Code. First, install the Extension Pack for Java. Once that is installed, and you open VS Code in a directory containing your Java app or apps, the extension pack should kick in and you’ll see a JAVA PROJECTS section in a side bar. Navigate under a project to the “Referenced Libraries” section, where you can add your junit.jar file. When editing test files, you’ll be able to launch tests from within the editor.

CLASSWORK
We’ll do the VS Code set up in class.

Playing Cards

poker-hand.png

Records Arrays

In the previous activity we saw how to write classes and create instances of classes. Classes can have constructors, fields, and methods (and a couple other things, but that is for later).

Die objects in the previous activity were mutable: the roll method changed the state of the object it was called on but changing the value of one of its fields. Normally in programming we strive to make immutable objects. The Java record is a special kind of class for making immutable objects with a lot less code. Here is an example record for a playing card:

Card.java
import java.util.Set;
import java.util.Map;

record Card(int rank, String suit) {

    public Card {
        if (!Set.of("♠", "♥", "♦", "♣").contains(suit)) {
            throw new IllegalArgumentException("Illegal suit");
        }
        if (rank < 1 || rank > 13) {
            throw new IllegalArgumentException("Illegal rank");
        }
    }

    @Override
    public String toString() {
        var bases = Map.of("♠", 0x1f0a0, "♥", 0x1f0b0, "♦", 0x1f0c0, "♣", 0x1f0d0);

        // Interestingly there is a extra character between the Jack and
        // Queen of each suit, so we have to make a small adjustment by
        // adding 1 to the code point of the Queens and Kings.
        return Character.toString(bases.get(suit) + (rank < 12 ? rank : rank + 1));
    }
}

Records will only have explicit constructors if we need to do some checking of its fields. Also, all the accessor methods are automatically generated, as are toString, equals, and hashCode methods. You can override them if you like, as we did above.

Before moving on, test:

CardTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class CardTest {

    @Test
    public void testConstructorErrors() {
        assertThrows(IllegalArgumentException.class, () -> new Card(0, "♠"));
        assertThrows(IllegalArgumentException.class, () -> new Card(14, "♠"));
        assertThrows(IllegalArgumentException.class, () -> new Card(3, "♥♥"));
        assertThrows(IllegalArgumentException.class, () -> new Card(3, ""));
        assertThrows(IllegalArgumentException.class, () -> new Card(3, "X"));
    }

    @Test
    public void testConstructorAndAccessors() {
        var card = new Card(5, "♥");
        assertEquals(5, card.rank());
        assertEquals("♥", card.suit());
    }

    @Test
    public void testStringRepresentations() {
        assertEquals("🂡", new Card(1, "♠").toString());
        assertEquals("🂵", new Card(5, "♥").toString());
        assertEquals("🃋", new Card(11, "♦").toString());
        assertEquals("🃞", new Card(13, "♣").toString());
        assertEquals("🃍", new Card(12, "♦").toString());
    }
}
$ javac -cp ~/junit.jar:. CardTest.java && java -jar ~/junit.jar -cp . -c CardTest

╷
├─ JUnit Jupiter ✔
│  └─ CardTest ✔
│     ├─ testConstructorAndAccessors() ✔
│     ├─ testConstructorErrors() ✔
│     └─ testStringRepresentations() ✔
└─ JUnit Vintage ✔

A lot of card games use a deck of all 52 cards. Let’s make a class for decks. We want to be security conscious: every deck must have all 52 cards. You are allowed to shuffle the deck and ask about the positions and values of cards in the deck, but you can not add and remove cards. So what kind of a structure would be best here? Well we need a list rather than a set, because the elements are ordered. And a list is indeed probably the best choice, but you know what? We’re going to go with arrays, because we all need practice with arrays!

Arrays vs. Lists

In practice, Java applications will almost always use lists; arrays are much more rarely used. Arrays are generally only used in “system-level” or “low-level” code where either performance is crucial, or you are mapping to hardware, or you are implementing the internals of some higher-level data type; for example, there are arrays “underneath” ArrayLists, HashMaps, and HashSets.

Arrays are quite underpowered compared to lists, but in our case, this is fine! We will be forced to do this like find the position of an element, and do shuffling, with step-by-code programming. If we used lists, these operations would be trivial.

Deck.java
import java.util.Arrays;
import java.util.Set;
import java.util.List;
import java.util.Random;
import java.util.NoSuchElementException;

/**
 * A typical 52-card deck. Decks can only be modified by putting them into the
 * "new deck" arrangement, shuffling, and splitting. There is no way to enter or
 * remove cards. You can, however, ask the deck for the position of any
 * particular card, and you can ask it which card is at a particular position.
 */
public class Deck {
    private static Random random = new Random();

    /**
     * Because this class is being used in a teaching environment, it uses an actual
     * Java array, rather than an ArrayList. In real life, you would likely always
     * use ArrayLists. By using a regular array, we will have to implement shuffle()
     * and position() ourselves, from first principles, which is actually an
     * important thing for computer scientists to know how to do, as building from
     * first principles is required knowledge for being able to innovate! Also, in
     * high-performance applications, Java programmers *do* resort to using plain
     * arrays, so it is a good idea to get accustomed to them.
     */
    private Card[] cards = new Card[52];

    /**
     * Creates a new deck, with cards ordered the new-deck way.
     */
    public Deck() {
        makeLikeNew();
    }

    /**
     * Orders the cards as follows: AS ... KS AH ... KH AD ... KD AS ... KS.
     */
    public void makeLikeNew() {
        var index = 0;
        for (var suit : Set.of("♠", "♥", "♦", "♣")) {
            for (var rank = 1; rank <= 13; rank++) {
                this.cards[index++] = new Card(rank, suit);
            }
        }
    }

    /**
     * Rearranges the cards within the deck randomly. If we would have used an
     * ArrayList to hold the cards, we would have been able to use
     * Collections.shuffle(this.cards). But there is value, in a teaching
     * environment, so see how one shuffles an array from first principles.
     */
    public void shuffle() {
        for (var i = cards.length - 1; i > 1; i--) {
            var j = random.nextInt(i);
            var c1 = this.cards[i];
            var c2 = this.cards[j];
            this.cards[i] = c2;
            this.cards[j] = c1;
        }
    }

    /**
     * Takes the top k cards from the deck and puts them on the bottom. Values out
     * of the range 1..51 leave the deck unchanged.
     */
    public void split(int k) {
        if (k <= 0 || k > this.cards.length - 1) {
            // Nothing to do, deck stays the same
            return;
        }
        Card[] backup = Arrays.copyOf(this.cards, this.cards.length);
        System.arraycopy(backup, k, this.cards, 0, backup.length - k);
        System.arraycopy(backup, 0, this.cards, this.cards.length - k, k);
    }

    /**
     * Returns the card at position k, where 0 <= k <= 51. If an illegal position is
     * supplied, Java will throw an ArrayIndexOutOfBounds exception. But we are
     * going to trap that here and throw and more general exception, because we
     * don't want callers to be tipped off that there is an array behind the scenes.
     */
    public Card cardAt(int position) {
        try {
            return this.cards[position];
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new IllegalArgumentException("No such position in deck");
        }
    }

    /**
     * Returns the top few cards from the deck, in a list.
     */
    public List<Card> topCards(int size) {
        if (size < 0 || size > this.cards.length) {
            throw new IllegalArgumentException("Illegal size for a hand");
        }
        return Arrays.asList(Arrays.copyOfRange(this.cards, 0, size));
    }

    /**
     * Returns the position of card within the deck, where the top card is at 0 and
     * the bottom card is at 51. Interestingly, there is no direct method on Java
     * array objects to get the index of an element. If we would have used an
     * ArrayList to hold the cards, we would have been able to use .indexOf()
     * directly. But since this class is being presented in a teaching context, this
     * approach allows for practice implementing a linear search.
     */
    public int positionOf(Card card) {
        for (var i = 0; i < this.cards.length; i++) {
            if (this.cards[i].equals(card)) {
                return i;
            }
        }
        throw new NoSuchElementException();
    }
}
Exercise: Write a unit test class for the Deck class.

Now remember that the first type of classes we saw in this bootcamp were classes with just a static main method. Those are called “app” classes, because you run them as apps. But you can also make a class that just has a bunch of static methods. These are called “utility” classes because they collect a bunch of “utility functions” together. Here is one with an eclectic mix of card-related things, just because we are practicing.

CardUtilities.java
import java.util.List;
import java.util.Set;
import java.util.ArrayList;
import java.util.TreeMap;
import java.util.Collections;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.counting;

public class CardUtilities {

    private CardUtilities() {
        // Intentionally empty, because that's how Utility classes roll
    }

    /**
     * Returns the card described by a two-character description of rank then suit.
     * Ranks are A, 2, 3, 4, 5, 6, 7, 8, 9, T, J, Q, K. Example valid descriptions
     * are: <code>"A♠"</code>, <code>"T♦"</code>, and <code>"K♣"</code>.
     */
    public static Card fromDescription(String description) {
        if (!description.matches("[A2-9TJQK][♠♥♦♣]")) {
            throw new IllegalArgumentException(
                    "Illegal card description: [" + description + "]");
        }
        var rank = "-A23456789TJQK".indexOf(description.charAt(0));
        var suit = String.valueOf(description.charAt(1));
        return new Card(rank, suit);
    }

    /**
     * Returns a list of cards from the given input string, which must be in the
     * form of zero or more card descriptions separated by whitespace. An example
     * input is <code>"K♣ K♥ 3♠ T♠"</code>.
     */
    public static List<Card> fromDescriptions(String descriptions) {
        var hand = new ArrayList<Card>();
        for (var description : descriptions.split("\\s+")) {
            hand.add(CardUtilities.fromDescription(description));
        }
        return hand;
    }

    /**
     * A quick-and-dirty analyzer for a five-card hard that produces an English
     * language description of the Poker hand type. The method simply returns the
     * kind without any implied ranking; it will, for example say only "Two Pair"
     * without indicating which cards are in the two pairs.
     */
    public static String pokerHandType(List<Card> hand) {
        if (hand.size() != 5) {
            throw new IllegalArgumentException("Only 5-card hands can be classified");
        }

        // A hand is a flush is all cards have the same suit as the first card in the
        // hand
        var isFlush = hand.stream().allMatch(c -> c.suit().equals(hand.get(0).suit()));

        // Create a tree map of all ranks, mapped to the number of cards in the hand
        // with that rank. For example a hand with three 8s, one king, and one 3, would
        // generate the map {3=1, 8=3, 13=1}. The tree map is internally sorted from
        // lowest ranks to highest ranks.
        var rankCounts = hand.stream()
                .collect(groupingBy(Card::rank, TreeMap::new, counting()));

        // The number of times the rank with the most occurrences appears. For the hand
        // with one three, three eights, and one king, this value of maxRankCount would
        // be 3.
        var maxRankCount = Collections.max(rankCounts.values());

        var isHighStraight = rankCounts.keySet().equals(Set.of(1, 13, 12, 11, 10));
        var isStraight = rankCounts.lastKey() - rankCounts.firstKey() == 4
                || isHighStraight;
        if (isFlush) {
            return isHighStraight ? "Royal Flush"
                    : isStraight ? "Straight Flush" : "Flush";
        } else if (rankCounts.size() == 2) {
            return maxRankCount == 4 ? "Four of a Kind" : "Full House";
        } else if (rankCounts.size() == 3) {
            return maxRankCount == 3 ? "Three of a Kind" : "Two Pair";
        } else if (rankCounts.size() == 4) {
            return "One Pair";
        }
        return isStraight ? "Straight" : "High Card";
    }
}

A tester for it:

CardUtilitiesTest.java
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class CardUtilitiesTest {

    @Test
    public void testASingleCardCanBeCreatedFromDescription() {
        var badDescriptions = List.of("Q♣ ", " 5♥", "W♠", "3S", "12♠");
        for (var badDescription : badDescriptions) {
            assertThrows(IllegalArgumentException.class,
                    () -> CardUtilities.fromDescription(badDescription));
        }
        var card = CardUtilities.fromDescription("A♥");
        assertEquals(1, card.rank());
        assertEquals("♥", card.suit());
    }

    @Test
    public void testMultipleCardsCanBeCreatedFromDescription() {
        var hand = CardUtilities.fromDescriptions("7♥ T♠ Q♦ 8♣ Q♣");
        assertEquals(5, hand.size());
        assertEquals("🂷", hand.get(0).toString());
        assertEquals("🂪", hand.get(1).toString());
        assertEquals("🃍", hand.get(2).toString());
        assertEquals("🃘", hand.get(3).toString());
        assertEquals("🃝", hand.get(4).toString());
    }

    @Test
    public void testPokerHandsCanBeClassified() {
        var fixture = List.of(
                "7♥ T♠ Q♦ T♣ Q♣:Two Pair",
                "K♥ 3♥ Q♥ J♥ 9♥:Flush",
                "K♥ T♥ Q♥ J♥ A♥:Royal Flush",
                "A♥ 2♥ 5♦ 4♥ 3♥:Straight",
                "Q♥ Q♠ Q♦ 5♣ Q♣:Four of a Kind",
                "K♥ T♥ Q♥ J♥ 9♥:Straight Flush",
                "7♥ T♠ Q♦ 5♣ Q♣:One Pair",
                "7♥ Q♠ Q♦ 5♣ Q♣:Three of a Kind",
                "7♥ T♠ J♦ 5♣ Q♣:High Card",
                "K♠ J♥ K♥ K♣ J♣:Full House",
                "K♥ T♥ Q♣ J♥ A♥:Straight");
        for (var scenario : fixture) {
            var items = scenario.split(":");
            var hand = CardUtilities.fromDescriptions(items[0]);
            assertEquals(items[1], CardUtilities.pokerHandType(hand));
        }
    }
}
Exercise: Did your instructor inform you why the constructor was marked private? If not, ask them. Make sure you understand.
Exercise: SURPRIIIIIIISE! The initialization of the variables isFlush and rankCounts used a lot of Java functionality not yet seen before, including streams, lambdas, and collectors. Rewrite the hand classification function to use more “basic” Java constructs (i.e., construct the values of these variables from loops). Note that because you have a test, you can make edits with confidence!

Let’s put the card, deck, and card utilities classes to use in a little app:

RandomPokerHandApp.java
/**
 * This messy app generates a random five card hand, prints each card, then
 * displays the classification of the hand.
 */
public class RandomPokerHandApp {

    public static void main(String[] args) {
        var deck = new Deck();
        deck.shuffle();
        var hand = deck.topCards(5);
        hand.stream().forEach(System.out::println);
        System.out.println(CardUtilities.pokerHandType(hand));
    }
}

Here is how we compile and run the app:

$ javac RandomPokerHandApp.java && java RandomPokerHandApp
🃚
🂶
🃙
🂹
🃉
Three of a Kind
Exercise: Write an app to generate 100,000 or more random poker hands and generate their types, and produce a Map which contains the counts for the number of times a hand of each type appears.
Exercise: Once you understand the implementation of the Deck class using arrays, reimplement the class using an ArrayList for the deck. You should be able to change only the deck class and no other file (not the card class, nor the card utilities class, nor the testers, nor the app) and everything else should still work. Did you do it? Congratulations: you have just experienced the benefits of encapsulation.

Talking Animals

animal-noises.png

Inheritance Polymorphism Abstract Classes Abstract Methods

A little ways outside of town there are a bunch of wild horses, cows, and sheep. Each of the animals have a name, and they all make their own species-specific sounds. But when asked to speak, they all speak exactly the same way: the say their name, then the word “says,” then make their distinctive sound.

Java captures this idea of a collection of related types that share some behaviors but differ in other behaviors in two primary ways: interfaces and abstract classes. Interfaces are generally preferable, but if the different classes have similar structure, then you will need abstract classes. The philosophical ideas behind how all this set up, including explanations of the meanings of the technical terms inheritance and polymorphism and the role they play in this app, will be covered during the code-along.

AnimalSoundsApp.java
abstract class Animal {
    private String name;

    protected Animal(String name) {
        this.name = name;
    }

    public String speak() {
        return name + " says " + sound();
    }

    public abstract String sound();
}

class Cow extends Animal {
    public Cow(String name) {
        super(name);
    }

    @Override
    public String sound() {
        return "moooooo";
    }
}

class Horse extends Animal {
    public Horse(String name) {
        super(name);
    }

    @Override
    public String sound() {
        return "neigh";
    }
}

class Sheep extends Animal {
    public Sheep(String name) {
        super(name);
    }

    @Override
    public String sound() {
        return "baaaa";
    }
}

class AnimalSoundsApp {
    public static void main(String[] args) {
        Animal h = new Horse("CJ");
        System.out.println(h.speak());
        Animal c = new Cow("Bessie");
        System.out.println(c.speak());
        System.out.println(new Sheep("Little Lamb").speak());
    }
}

This program is straightforward to run; no command line arguments or user input required:

$ javac AnimalSoundsApp.java && java AnimalSoundsApp
CJ says neigh
Bessie says moooooo
Little Lamb says baaaa

How about a test? Yes, we should test.

AnimalSoundsTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class AnimalSoundsTest {

    @Test
    public void testCows() {
        var cow = new Cow("Bessie");
        assertEquals("moooooo", cow.sound());
        assertEquals("Bessie says moooooo", cow.speak());
    }

    @Test
    public void testHorses() {
        var horse = new Horse("CJ");
        assertEquals("neigh", horse.sound());
        assertEquals("CJ says neigh", horse.speak());
    }

    @Test
    public void testSheep() {
        var sheep = new Sheep("MacKenzie");
        assertEquals("baaaa", sheep.sound());
        assertEquals("MacKenzie says baaaa", sheep.speak());
    }
}
$ javac -cp ~/junit.jar:. AnimalSoundsTest.java && java -jar ~/junit.jar -cp . -c AnimalSoundsTest
╷
├─ JUnit Jupiter ✔
│  └─ AnimalSoundsTest ✔
│     ├─ testSheep() ✔
│     ├─ testCows() ✔
│     └─ testHorses() ✔
└─ JUnit Vintage ✔
Exercise: Add two more types of animals. You’ll be creating two more classes and writing tests for each.
Exercise: Place each class in its own file, and get the main application, and the tester, to run.
Exercise: Give each animal a method to return its genus and species, and write tests.

Where To Go Next

This bootcamp was not focused on giving you an academic or detailed look into Java. Perhaps you would like to do that next. If so, check out these notes on Java Basics for a much more in-depth coverage of the material breezed through here.

Summary

We’ve covered:

  • Java application structure
  • Variable declarations
  • Formatting and printing strings
  • Basic operations on numbers and strings
  • Throwing and catching excecptions
  • Command line arguments
  • Arrays, lists, maps, and sets
  • A little bit of the java.time package
  • JShell
  • Classes, fields, constructors, methods
  • Records
  • JUnit
  • Abstract classes, inheritance, and polymorphism
  • Utility classes