Decisions and Logic

Comparisons

Assume x, y, and z are numbers (int, double, etc). We can compare them using the following operations:

x < y
x > y
x <= y
x >= y
x == y
x != y

WARNING: Don't confuse x = y and x == y!!!!!

Beware of round-off errors when comparing inexact numbers:

x == Math.sqrt(x * x); // returns false!

Use this instead:

class MyUtils {
   private static final double DELTA = 1e-10;
   public static boolean close(double x, double y) {
      return Math.abs(x – y) <= DELTA;
   }
   // etc.
}

Comparing Objects

There are two ways to compare objects, literal and logical equality. Assume the following declarations have been made:

Rectangle r1 = new Rectangle(2, 3, 5, 6),
          r2 = new Rectangle(2, 3, 5, 6),
          r3 = r1,
          r4;

Here's what memory looks like:

Note that r4 contains null.

Note that r1 and r3 are literally equal (same object) and therefore logically equal. r1 and r4 are logically equal (same attributes, but not literally equal.

Literal equality is determined by ==.

Logical equality is (sometimes) determined by calling the equals method:

r1 == r2 // = false
r1 == r3 // = true
r1.equals(r2) // = true
r4 == null // = true

Comparing Strings

Although Strings are objects, sometimes the == operator measures logical equality. It is recommended that programmers only use equals to compare strings:

Use == for literal equality and equals for logical equality.

String s1 = "Hello", s2 = "Hello", s3 = s1;

s1 == s2; // = ???
s1 == s3; // = ???
s1.equals(s2); // = true
s2.equals(s3); // = true

Some objects, like strings, also have a natural ordering. Objects can be compared using the compareTo method:

String s4 = "bye";

s1.compareTo(s4) < 0; // = false because "Bye" comes before "Hello" in the dictionary

Boolean Logic

boolean a = true, b = false, c = true, d;

a && b && c // = false
a || b || c // = true
!(a && b) // = true
!(a && b) = !a || !b // DeMorgan's Law

Example: The UNC Admissions Calculator

Conditional Execution

Conditionals, also called selections, allow programmers to specify that a command should be executed only if a certain condition is true. Of course the truth of the condition generally won't be known until runtime. Selections are classified as one-way, two-way, and multi-way.

One-Way Selections

The form of a one-way selection is:

if (CONDITION) COMMAND;

Semantically, the condition is evaluated. If the value of this condition is true, then the command is executed, otherwise, control falls through to the next command. We often use fragments of flowcharts to describe the semantics of control commands:

One-way conditionals are used to validate inputs.

Two-Way Selections

The syntax of a two-way selection is

if (EXPRESSION) COMMAND; else COMMAND;

Once again, the expression is called the condition. The first command is called the consequence, and the second command is called the alternative:

if (CONDITION) CONSEQUENCE; else ALTERNATIVE;

Semantically, the condition is evaluated. If its value is true, then the consequence is evaluated and the alternative is ignored. Otherwise, the alternative is evaluated and the consequence is ignored. Here's the flowchart representation:

In the following example, a two-way selection prevents the user from dividing by zero:

if (den) {
   result = 1/den;
} else {
   System.err.println("can't divide by 0");
}

Multi-Way Selections

Multi-way selections can be achieved by nesting two-way selections:

if (income < 0)
   System.err.println("income must be non-negative");
else if (income < 10000)
   tax = 0;
else if (income < 20000)
   tax = .2 * income;
else if (income < 35000)
   tax = .3 * income;
else if (income < 50000)
   tax = .5 * income;
else
   tax = .75 * income;

Question: What would a flow chart for this command look like?

Question: Why don't we need to check that income is also greater than or equal to the lower limit:

if (income < 0)
  System.err.println("income must be non-negative");
else if (0 <= income && income < 10000)
   tax = 0;
else if (10000 <= income && income < 20000)
   tax = .2 * income;
// etc.

Beware of the dangling else.

Question: How much tax does a person making $15,000 pay? (Answer = unknown.) $8000?  (Answer = $800.)

if (income < 10000)
if (5000 < income)
tax = .1 * income;
else tax = .2 * income;

Both Java and C++ inherit the bizarre switch command from C. The syntax and semantics of the switch command are so awkward that they almost render the command unusable. Here is the syntax:

switch(EXPRESSION)
{
   case EXPRESSION: COMMANDS
   case EXPRESSION: COMMANDS
   case EXPRESSION: COMMANDS
   // etc.
   default: COMMANDS
}

The expression in parenthesis, called the key, must produce an integer or integer subtype value when executed. The switch command contains one or more case clauses. Each case clause begins with an expression called a guard, followed by zero or more commands, called consequences. The last case clause traditionally begins with the reserved word "default". The list of commands following it are called alternatives:

switch(KEY)
{
   case GUARD: CONSEQUENCES
   case GUARD: CONSEQUENCES
   case GUARD: CONSEQUENCES
   // etc.
   default: ALTERNATIVES
}

Semantically, the key is evaluated. The value of the key is compared to the value of each guard, starting with the topmost guard. When a match is found, all commands following the guard, including commands that follow subsequent guards, are executed. The defaule guard matches all keys. For example, here is a flowchart describing the semantics of a switch command with three case clauses guarded by expressions called GUARD1, GUARD2, and GUARD3, respectively:

Notice that if KEY == GUARD1 is false, but KEY == GUARD2 is true (i.e., not 0 or false), then CONSEQUENCES1 will be ignored, but CONSEQUENCES2 and CONSEQUENCES3 will be executed. This happens even if KEY == GUARD3 is false!

To prevent the consequences of subsequent case clauses from being executed, programmers often end each sequence of consequences with a break command. Break commands will be discussed later. Essentially, a break command causes control to exit the current control command and pass to the next command.

Here's an example of a switch command:

switch(op)
{
   case '+': result = arg1 + arg2; break;
   case '*': result = arg1 * arg2; break;
   case '-': result = arg1 - arg2; break;
   case '%': result = arg1 % arg2; break;
   case '/':
      if (arg2) result = arg1 / arg2;
      else System.err.println("can't divide by 0");
      break;
   default: System.err.println("unrecognized operator " + op);
}

We can only use this switch command if op happens to be declared as a character, since char is a subtype of int:

double arg1, arg2, result;
char op; // = operator used to combine arg1 & arg2

If we declare op as a string:

string op;

then we must use nested if commands.

Sequence Commands

What happens if several commands must be executed when a condition is true:

if (CONDITION)
   COMMAND1;
   COMMAND2;
   COMMAND3;

Unfortunately, the compiler ignores indentation. Commands 2 and 3 will be executed if CONDITION is true or false.

However, most languages allow programmers to group a sequence of commands into a single command called a sequence. Syntactically, a sequence is just a list of commands, each terminated by a semi-colon, delimited by curly braces:

{ COMMAND; COMMAND; COMMAND; etc. }

Semantically, each command in a sequence is executed in order of appearance:

The following example shows how several commands can be grouped into a single command serving as the consequence of a conditional. Note that this example also shows that commands can be nested inside of commands. It also shows that sequences may contain declarations. A sequence that contains declarations is called a block. These will be discussed in the next chapter:

if (rad > 0)
{
   int min = center + rad;
   int max = center + rad;
   if (min < result && result < max)
   {
      double error = fabs(result - center);
      System.out.println("you only missed by " +
         error + " inches");
   }
}

Example 1 Calculators

import java.util.Scanner;

public class Calculator {


   public static void main(String[] args) {

      Scanner kbd = new Scanner(System.in);
      System.out.print("enter an arithmetic expression: ");
      if (!kbd.hasNextDouble()) {
         System.err.println("First argument must be a double");
         return;
      }
      double arg1 = kbd.nextDouble();
      String op = kbd.next();

      if (!kbd.hasNextDouble()) {
         System.err.println("Second argument must be a double");
         return;
      }
      double arg2 = kbd.nextDouble();

      if (op.equals("+")) {
         System.out.println("result = " + (arg1 + arg2));
      } else if (op.equals("*")) {
         System.out.println("result = " + (arg1 * arg2));
      } } else if (op.equals("-")) {
         System.out.println("result = " + (arg1 - arg2));
      }} else if (op.equals("/")) {
         System.out.println("result = " + (arg1 / arg2));
      }
      else {
         System.err.println("Unrecognized operator: " + op);
      }

   }
}

Example 2: bank Accounts

 

public class BankAccount {
   private double balance;
   private String password;
   public BankAccount(String pswd) {
      password = pswd;
      balance = 0;
   }
   private boolean isAuthorized(String pswd) {
      return password.equals(pswd);
   }
   public double getBalance(String pswd) {
      if (isAuthorized(pswd)) {
         return balance;
      } else {
         System.err.println("password error");
         return 0;
      }
   }
   public void deposit(double amt, String pswd) {
      if (isAuthorized(pswd)) {
         balance = balance + amt;
      } else {
         System.err.println("password error");
      }
   }
   public void withdraw(double amt, String pswd) {
      if (isAuthorized(pswd) && amt <= balance) {
         balance = balance - amt;
      } else {
         if (amt <= balance) {
            System.err.println("password error");
         } else {
            System.err.println("insufficient funds");
         }
      }
   }

}