Skip to content

Latest commit

 

History

History
497 lines (391 loc) · 16.9 KB

File metadata and controls

497 lines (391 loc) · 16.9 KB

Appendix A: Kris' Special Class

In this book, we use a simple template for all your early Java programs. Why? Well, in many books, tutorials, etcetera for Java, they use a very simple (well, for Java, simple) Hello World program.

blah, blah, blah

But in this book, I use a special template. One that you should use when working thru the examples and exercises in this book.

The SPECIAL ZipCode java template!

public class ZipCode {

  void compute() {

    //Your code goes in here...

  }


  // don't change this...
  public static void main(String[] args) { new ZipCode().compute(); }
}
// and to run it in jshell
new ZipCode().compute();

Appendix B: Advanced Ideas

We’re going to look at a few "modern" ways of handling a collection of data. Frequently, you have a list, or an array, of data that needs to be gone through to print it out, transform it in some way, or to summarize it (such as a total or an average). As you have seen in the code patterns section, there are common loops used for such things, a simple pattern that you can memorize.

There are other methods of doing these things, and we’re going to discuss a few of them here. These ideas are based primarily on methods made popular by Hadoop and other "big data" applications and tools. And what’s good for "big" data is often good for "small" data as well.

Each of these sections is an example of a more "elegant" way of expressing coding logic. By studying each one and comparing it to the ways we’ve discussed before using loops and conditional statements, we’re expanding your understanding, making you see how these techniques can be used to create more extensible and elegant code.

Let’s use this array for the following examples.

// For jshell - create a simple Grocery class
class Grocery {
    String name;
    double price;

    Grocery(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return name + ": $" + price;
    }
}

// Create the grocery list
List<Grocery> groceries = Arrays.asList(
    new Grocery("Breakfast Cereal", 5.50),
    new Grocery("Rice", 14.99),
    new Grocery("Oranges", 6.49),
    new Grocery("Crackers", 4.79),
    new Grocery("Potatoes", 3.99)
);

A common grocery list, we have this as a list (or array) of objects (what’s known as a key/value data structure).

Simplifying Loops

Now, if you wanted to print out each item’s name in the grocery list to the console, you could do something like this:

int idx = 0;
while (idx < groceries.size()) {
  System.out.println(groceries.get(idx).name);
  idx = idx + 1;
}

This is a very common code pattern in Java. It’s also fraught with possible errors. It relies on the idx variable. If we forget or mess up the increment step at the end of the loop, we could be in trouble. Rather, how about this:

groceries.forEach(item -> {
  System.out.println(item.name);
});

forEach is a higher-order function that takes in another function as an argument and executes the provided function once for each element in the array. It is meant to simplify your code. By using forEach, we remove the extraneous code for tracking and accessing the array using an index, and focus on our logic: printing out the name of each grocery item.

A higher-order function is a function that does at least one of the following:

  • takes one or more functions as arguments (i.e. procedural parameters),

  • returns a function as its result.

Let’s look at a few other uses of higher-order functions. Say we wanted to get a list of just the prices of our grocery list. If we use a loop, we have a very recognizable pattern.

int index = 0;
List<Double> prices = new ArrayList<>();
while (index < groceries.size()) {
  prices.add(groceries.get(index).price);
  index = index + 1;
}

But if we use a different higher-order function, map:

// Need to import: import java.util.stream.Collectors;
List<Double> prices = groceries.stream()
    .map(item -> item.price)
    .collect(Collectors.toList());

The value of prices would be: [5.5,14.99,6.49,4.79,3.99]. And if we were to want something a little more useful than just producing a list, we would do this when using a loop:

int index = 0;
double total = 0;
while (index < groceries.size()) {
  total = total + groceries.get(index).price;
  index = index + 1;
}

Now we are tracking two pieces of information, the index and the total. With the sum of all the prices ending up in total.

But look how much simpler our code can be if we use reduce, another higher-order function.

double total = groceries.stream()
    .mapToDouble(item -> item.price)
    .sum();

The result, if we print total is the awkward number 35.760000000000005, and why that is, well, later we’ll discuss it. But, 35.76 should suffice.

If we wanted to pull out all the even numbers from a list of numbers, we’d need a function like this to decide if a number if even. (Remember the trick using modulus?)

static boolean even(int value) {
    return value % 2 == 0;
}

System.out.println(even(3) + " " + even(4) + " " + even(126));

// giving us 'false true true'

If we remove the name even from the definition, and use a higher-order function named filter, we have something like this.

List<Integer> numList = Arrays.asList(1,2,3,4,5,6,7,8);
List<Integer> evenNumbers = numList.stream()
    .filter(value -> value % 2 == 0)
    .collect(Collectors.toList());

filter is a function which filters out the elements in an array that don’t pass a test. You can visualize it this way:

static List<Integer> filter(List<Integer> array, Predicate<Integer> test) {
  List<Integer> passed = new ArrayList<>();
  for (Integer element : array) {
    if (test.test(element)) {
      passed.add(element);
    }
  }
  return passed;
}

static boolean even(int value) {
    return value % 2 == 0;
}

List<Integer> dataList = Arrays.asList(1,2,3,4);

System.out.println(
    filter(dataList, x -> even(x))
); // produces [2,4]

See what I’ve done? First, I’ve shown you how to express filter with both a loop and an if statement, expressing the function in more verbose code to give you the idea of what’s going on. Second, I’ve then used it to show it in action.

I’ve defined two functions, filter and even. Then created a short array/list called dataList. Finally, I’ve printed the result of calling filter(dataList,even). Wait, what? I passed the function’s name, even as an argument to another function. Well, sure, why not? Functions in Java are called first class objects, just like a variable or an object or a value.

And it turns out Java already has a function called filter, a higher order function. And I can reduce it even more to something like this:

List<Integer> result = Arrays.asList(1,2,3,4).stream()
    .filter(value -> value % 2 == 0)
    .collect(Collectors.toList());

// or, If I have 'dataList' defined as List.of(1,2,3,4)
List<Integer> result = dataList.stream()
    .filter(value -> value % 2 == 0)
    .collect(Collectors.toList());

And as you will see further down, we can even reduce that to a simpler form, called a lambda.

In these four cases, we see how we can use a different form of computing, a functional form, to simplify our code by removing loops and their trappings and replacing them with higher-order functions, letting us hand some of our logic to the language itself. And making our code more elegant in the process.

Simplifying Conditionals

We can use the same ideas with conditionals. Conditionals can get thick and complicated without too much effort. Say we need to keep track of and perform different discounts for various purposes. Sounds like an if statement! With else statements too!

But else statements, for instance, have a habit of complicating code.

Every time you add an else statement, you increase the complexity of your code two-fold. Conditional constructs like if-else and switch statements are foundational blocks in the world of programming. But they can also get in the way when you want to write clean, extensible code.

Let’s create a function that computes a discount for a price amount based on sone discount code. We might, happily, build something like this:

static double discount(double amount, String code) {
  if (code.equals("TWENTYOFF")) {
      return amount * 0.80;
  } else if (code.equals("QUARTEROFF")) {
      return amount * 0.75;
  } else if (code.equals("HALFOFF")) {
      return amount * 0.50;
  } else { // no discount
      return amount;
  }
} // whew! that a lot of braces.

double netprice = discount(200.00, "HALFOFF"); // would be 100.

But think about adding another discount, we’d have to add another if, more braces, and make sure we nest it in there carefully, otherwise we break the whole, rickety, mess.

I know! Let’s use a switch statement, and simplify! Well…​

Switch statements too, have a way of expanding on you, getting long, and sometimes complex, requiring care to maintain and/or extend. Say you wanted to add some more discounts to the following switch statement?

static double discount(double amount, String code) {
  switch (code) {
    case "TWENTYOFF":
      return amount * 0.80;
    case "QUARTEROFF":
      return amount * 0.75;
    case "HALFOFF":
      return amount * 0.50;
    default:
      return amount;
  }
}

double netprice = discount(200.00, "HALFOFF"); // would be 100.

We have to add two lines of code for each case. And if you make a mistake, you break the whole contraption.

But consider this idea: use a combination of a simple data structure and a small piece of code (called an arrow function (or "lambda")).

// Create a map to store discount codes and their multipliers
Map<String, Double> DISCOUNT_MULTIPLIER = new HashMap<>();
DISCOUNT_MULTIPLIER.put("TWENTYOFF", 0.80);
DISCOUNT_MULTIPLIER.put("QUARTEROFF", 0.75);
DISCOUNT_MULTIPLIER.put("HALFOFF", 0.50);

static double discount(double amount, String code) {
  // Get multiplier for this code, use 1.0 (no discount) if code not found
  Double multiplier = DISCOUNT_MULTIPLIER.get(code);
  if (multiplier == null) {
    multiplier = 1.0;  // No discount
  }
  return amount * multiplier;
}

Whoa! How easy is it to add another 1, 3 or 7 discount cases? Just one line each. This re-factor effectively decouples the data we use from the core calculation logic, which makes it much easier to modify either independently. No ifs, elses or switches, just an object holding data, a simple lambda (arrow) function which does simple math.

Lambdas (or Arrow Functions)

One of the ways we do a lot of this kind of simplification within code is by replacing more complex logic with simpler forms.

In Java, we have function expressions which give us an anonymous function (a function without a name). Here we are creating an anonymous function and assigning it to a variable.

// In Java, we can create function objects using interfaces
BiFunction<Integer, Integer, Integer> anon = (a, b) -> a + b;

// this is similar to a static method
static int anon(int a, int b) {
    return a + b;
}

It’s really just a different form of the same thing.

But we also have lambdas or arrow functions with a more flexible syntax that has some bonus features and gotchas. We could write the above example as:

BiFunction<Integer, Integer, Integer> anon = (a, b) -> a + b; // from above

BiFunction<Integer, Integer, Integer> anon = (a, b) -> a + b; // Sweet!

// or we could
BiFunction<Integer, Integer, Integer> anon = (a, b) -> { return a + b; };
// if we only have one parameter we can use Function instead
Function<Integer, Integer> anon = a -> a + a;
// and without parameters we use Supplier
Supplier<String> anon = () -> "nothing"; // this returns "nothing"

// this looks pretty nice when you change something like:
Arrays.asList(1,2,3,4).stream()
    .filter(new Predicate<Integer>() {
        public boolean test(Integer value) {
            return value % 2 == 0;
        }
    })
    .collect(Collectors.toList());
// to:
Arrays.asList(1,2,3,4).stream()
    .filter(value -> value % 2 == 0)
    .collect(Collectors.toList());

See how much easier it is to read the last line in the example over the previous filter using the anonymous function? Lambdas are a powerful way to express small functions, and use them in a variety of ways. They are often paired with higher-order functions, as they simplify the code quite a bit.

Polymorphism and K.I.S.S.

Remember "keep it simple, stupid"? Yeah, we suffer from over-complicating things in coding as well. Another way to replace conditionals is by using a key feature of object-oriented programming languages: polymorphism. Let’s show some code which helps bill a customer.

// For jshell - create a Customer class
class Customer {
    String name;
    double amount;
    String paymentMethod;

    Customer(String name, double amount, String paymentMethod) {
        this.name = name;
        this.amount = amount;
        this.paymentMethod = paymentMethod;
    }
}

// list of customers we want to 'checkout'
List<Customer> customers = Arrays.asList(
  new Customer("sam", 75.00, "credit-card"),
  new Customer("frodo", 50.00, "debit-card"),
  new Customer("galadriel", 25.00, "cash")
);

I’m going to gloss over the code needed to do each of the three kinds of payment. But show you how I might have to account for all three inside a checkout function.

static void checkout(double amount, String paymentMethod) {
  switch (paymentMethod) {
    case "credit-card":
      // Complex code to charge amount to the credit card.
      break;
    case "debit-card":
      // Complex code to charge amount to the debit card.
      break;
    case "cash":
      // Complex code to put amount into the cash drawer.
      break;
  }
}

Now, I’d like to take the list of customers, and checkout each one. (Notice how I’m using the higher-order function here, not a for loop.)

customers.forEach(customer -> {
  checkout(customer.amount, customer.paymentMethod);
});

But if I use polymorphism, I can make each customer’s checkout method wired directly to the data list. And look how I have broken the large function up, into three simpler things.

// First, create an interface for payment methods
interface PaymentProcessor {
    void charge(double amount);
}

class CreditCardCheckout implements PaymentProcessor {
  public void charge(double amount) {
    // Complex code to charge amount to the credit card.
  }
}
class DebitCardCheckout implements PaymentProcessor {
  public void charge(double amount) {
    // Complex code to charge amount to the debit card.
  }
}
class CashCheckout implements PaymentProcessor {
  public void charge(double amount) {
    // Complex code to put amount into the cash drawer.
  }
}

// Update Customer class to use PaymentProcessor
class Customer {
    String name;
    double amount;
    PaymentProcessor paymentMethod;

    Customer(String name, double amount, PaymentProcessor paymentMethod) {
        this.name = name;
        this.amount = amount;
        this.paymentMethod = paymentMethod;
    }
}

List<Customer> customers = Arrays.asList(
  new Customer("sam", 75.00, new CreditCardCheckout()),
  new Customer("frodo", 50.00, new DebitCardCheckout()),
  new Customer("galadriel", 25.00, new CashCheckout())
);
customers.forEach(customer -> {
  customer.paymentMethod.charge(customer.amount);
});

I am using a class in this example, well, three of them actually. One for each payment method. I can put the complex code within each class, and if I set them all up to have a charge method (a method being the term we use to talk about a function wired to a class), I know I just need to call charge on each customer, and the classes will all figure out which piece of code to use. This is an example of polymorphism, "many forms, same name".

Another example, commonly used in explaining polymorphism, is a series of geometric shapes, like Square, Triangle and Circle. Each of those shapes has a different way of computing the area of itself. A Square’s area() is (side * side), right? But a Circle’s area() is (3.14159 * (radius * radius)). Two different ways of calculating the area of a shape, depending on the kind of shape we’re working with. Each of these shapes would have it’s own class, each with a different definition of how to find the area of the shape. That’s polymorphism in a nutshell.

Each of these techniques are currently considered "advanced" Java, even though in many cases they are simpler and less error-prone than more "traditional" loops and conditionals.

Be sure to consider how each of them are largely the same in functionality but simpler in expressing the logic of your program. Remember to make your code more elegant by adding more simplicity.