Lecture 7

In this lecture we will discuss the implicit visibility of instance fields and methods, and also use relational and logical expressions to test whether or not variables fall in some range of values.

Instance variables, this, and implicit visibility

Let's return to a basic version of the Person class:

public class Person {
  private String name;
  private int age;
  private String hobby;
  
  public Person(String name, int age, String hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
  }
  
  public String getName() { return this.name; }
  public int getAge() { return this.age; }
  public String getHobby() { return this.hobby; }
  
}

Every place where we are referring to an instance variable (name, age, or hobby), we are doing so through the hiddle parmeter this, which stores a reference to the object instance on which the method was invoked.

Here is a slightly modified class that is exactly equivalent to the first version:

public class Person {
  private String name;
  private int age;
  private String hobby;
  
  public Person(String name, int age, String hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
  }
  
  public String getName() { return name; }
  public int getAge() { return age; }
  public String getHobby() { return hobby; }
  
}

Note that in the getter methods getName, getAge, and getHobby, we are referring to the instance variables name, age, and hobby without the prefix "this.".  Here is what is happening.  Inside the definition of getName (for example), the Java compiler looks for the definition of the variable name.  No matching parameter or method variable is found.  So, the compiler then searches the Person class (which contains the method).  A matching name variable is found, which is an instance variable.  The Java compiler then resolves the variable name to mean "the instance variable name in the object referred to by the hidden this parameter".

The same principle applies when invoking an instance method without specifying a method: the this reference is used implicitly.  For example, we could add the following instance method to the Person class:

public void introduce() {
  System.out.println("Hello, my name is " + getName());
}

The compiler knows that we means to invoke getName on this.

Testing ranges of values

A common task in programming is to test whether or not the value of a variable falls within some range (or ranges) of values.

Say we want to determine whether or not a character (char) value is an upper case alphabetic character.  One way to do it would be to explicitly test for each possible value:

public static boolean isUpperAlpha(char c) {
  if (c == 'A' || c == 'B' || c == 'C' ||
     ...many similar cases omitted...) {
     return true;
  } else {
    return false;
  }
}

This is quite tedious.  A better approach is to test whether or not the character value falls within the range of characters from 'A' to 'Z'.  We can do this because each character value in Java has an integer code.  You can find the integer code of a character value by converting it to an int using a cast.  Here are some examples from the DrJava interactions window:

Welcome to DrJava.
> (int) 'A'
65
> (int) 'Z'
90
> (int) 'a'
97
> (int) '0'
48

Here we can see that the chacter 'A' is represented by the integer value 65, 'Z' is 90, 'a' is 97, and the digit '0' is 48.  As it turns out, the values 'A' (65) through 'Z' (90) form a contiguous sequence.  Therefore, we can use relational expressions to test whether a particular character value falls within this range.

public static boolean isUpperAlpha(char c) {
  if (c > 'A' && c < 'Z') {
    return true;
  } else {
    return false;
  }
}

This method returns true if the value of the char parameter c is both greater than or equal to the value of 'A' and less than or equal to the value of 'Z'.  Let's try using this method in the DrJava interactions window.  (Assume that we've defined the isUpperAlpha static method inside a class called Chars.)

Welcome to DrJava.
> Chars.isUpperAlpha('A')
true
> Chars.isUpperAlpha('B')
true
> Chars.isUpperAlpha('Z')
true
> Chars.isUpperAlpha('a')
false
> Chars.isUpperAlpha('0')
false


The lower case alphabetic characters also fall in a contiguous range, so we can define an isLowerAlpha method in much the same way:

public static boolean isLowerAlpha(char c) {
  if (c >= 'a' && c <= 'z') {
    return true;
  } else {
    return false;
  }
}

Now, say that we want to write a method, isAlpha, to test whether or not a char value is alphabetic (either upper or lower case).  Now there are two ranges of values we have to try.  Although each sub-range, 'A'..'Z' and 'a'..'z', is contiguous, the overall range 'A'..'z' contains some characters that are not alphabetic, and thus we must test each range separately.

One way to implement this method is by replicating the expressions used in isUpperAlpha and isLowerAlpha:

public static boolean isAlpha(char c) {
  if ((c >= 'A' && c <= 'Z') ||
      (c >= 'a' && c <= 'z')) {
    return true;
  } else {
    return false;
  }
}

In the DrJava interactions window we can see that this approach works (again, assume this method is in a class called Chars):

> Chars.isAlpha('a')
true
> Chars.isAlpha('A')
true
> Chars.isAlpha('Z')
true
> Chars.isAlpha('$')
false

However, an expression consisting of 4 relational sub-expressions connected by 3 logical operators is fairly complicated.  A much better idea is to make use of methods that we've already written!  Here's an improved version of isAlpha called isAlpha2 (again, this is in a class called Chars):

public static boolean isAlpha2(char c) {
  if (isUpperAlpha(c) || isLowerAlpha(c)) {
    return true;
  } else {
    return false;
  }
}

Not only is this version of the method simpler, it is also easier to read and understand.  This is worth mentioning as a general rule:

When possible, invoke an existing function rather than replicated code.  This will make your program simpler and easier to understand.

One final simplification: the if/else statement in each of these methods is unnecessary, because the condition of the if/else statement computes the precise value that we want to return from the method.  For example, here is a simplified isAlpha3 using this approach:

public static boolean isAlpha3(char c) {
  return isUpperAlpha(c) || isLowerAlpha(c);
}