15-110 Lecture 3 (Wednesday, June 30, 2010)

Vocabulary Words

Primitive (Simple) Types

A Quick Note on "floats" and "doubles"

As we mentioned earlier, Java uses "floating point" numbers to represent numbers that include a fractional component. As compared to "fixed point" numbers, the number of digits to the right of the decimal "floats" as the absolute magnitude of the number changes.

If the magnitude is very great, the precision of the fractional component is reduced. If the magnitude is very small, the precision of the fractional compentent is higher. This makes sense from a technical standpoint, becuase the same amount of memory is used, regardless of the magnitude. So, if more memory is used for the magnitude, less is available for the fraction. And, it makes sense from a user's perspective, because we care more about the fine details of the fraction on small numbers than we do on large ones. The fractions are more important when looking through a microscope than a telescope, I'd guess.

An old-school "floating point" number used a fixed number of digits to represent the number. So, there was a one-for-one trade-off between sigits used for the integer component and the fractional component. This isn't exactly the way it works with most modern computers. Instead, the decimal place doesn't necessarily move -- but the precision changes. In other words, the greater the magnitude, the more of an error might be present in the fractional component. This is sometimes called "rounding error", even though there isn't really any rounding involved. You'll learn all about the details once you get to 15-213.

For now, I'd like to note just one thing. This error means that, when doing computation, we need to think about the order in which we do it. In principle, when we do math, as long as we follow the precendence rules, the order doesn't matter. But, because of the roudning error, when it comes to floating point arithmetic, it sometimes does.

If we, for example, take a fraction, then multiply by a very large number, and then divide by another very large number and then add a fraction, we've made a mess. When we multiply the first fraction by the large number, we've encountered a "rounding error" by going from a "small number" to a larger one. When we divide by the larger number, we find ourselves back in the "small number range" and can support a lot of precision -- but we're just guessing. The real precision was "rounded away" when we multiplied by the large number. So, the result of the calculation is less precise than it need be. We are much better off doing the division fo the two large numbers first, to get the smaller result, then doing the multiplaction and addition.

Calculator Model

Today, we are going to construct a simple calculator. It works just like the "4 button" calculators many of you have seen (anyone seen one lately?) It can add, subtract, multiply, and divide.

In order to perform computation, the user enters one operand, then the operation, then the next operand. The calculator remembers the first operand and the operation until the second operand is entered, at which time it performs the computation. The result of the computation replaces the first operand in the calculator's memory.

In this way, the result of one computation can be carried forward to the next -- notice it is in the same place as was originally used for the first operand. For this reason, the place in memory where it is stored is often times called the "accumulator" -- it accumulates the results over time. In other words, it holds the present result.

The calculator displays the number most recently entered by the user, until the user asks to add, subtract, multiply, or divide. When this happens, the accumulator value is displayed. Please pay careful attention to this. It is a bit ticklish. If the user presses, "5 + 3", "3" is displayed. If the user continues "5 + 3 +", 8 is displayed.

So, we see that the calculator is, in effect, remembering three things: the accumulator value and the value that is most recently displayed. It also temporarily holds onto an input value -- but it doesn't need to remember it long, as it is promptly rolled into the accumulator by via the stored operation and also displayed.

Calculator Interface/Class Skeleton

Recall that, in English, an "interface" means, "The place where the outer surface of two things come together." In software, an "interface" describes how a piece of a program can be used by other pieces. In the context of object-oriented software, an "interface" is the set of "public" methods of an object that can be used by other objects.

Java has a special construct known as the interface that can be used to define, in a formal way, an interface that can be adopted when writing a class specification. But, we'll get to that later.

For now, we are going to begin our effort to write a calculator class specification by writing what is commonly known as a "class skeleton". It will be a class specification containing only empty method specifications. In so doing, it will define the interface of a calculator -- those methods that can be invoked by other objects.

  class Calculator {

    public void add() { }
    public void subtract() { }
    public void multiply() { }
    public void divide() { }
    public void equals() { }
    public void enterNumber(double input) { }
    public void display() { }
  }
  

What to notice? Well, a few things. First, notice that every class specification has a name. In this case, the name is "Calculator". By convention, we always capitalize the first letter of a class's name. If the class's name uses multipel words, such as "StudentAccount", we capitalize exactly the first letter of each word.

Notice the "reserved word" "class". A "reserved word" is a word that has a special meaning to Java. The reserved word, "class" indicates that we are wrtiting a class specification. It is always followed by the name of the class that we are specifying, in this case, "Calculator".

Also notice the {}-braces. When you see them, think "begin" and "end". Notice the set associated with the class specification. They enclose all of the methods specifications in the example above. They denote the beginning and the ending of the class specification. Everything within the {}-braces is said to be the "body".

Notice also the seven method specifications: add, subtract, multiple, divide, enterNumber, equals, and display. Each has its own set of {}-braces. In the next step, we will add the code within the braces to complete the methods. What goes in between those braces is said to be the body of the method specification.

Consider the method specification for enterNumber:

public void enterNumber(double input) { }

We can see that the method name is "enterNumber". In other words, the "identifier" for this method is "enterNumber". It is "public", which means that it can be used from other classes of objects (we'll talk about private methods soon enough).

Within the ()-parenthesis, we see what is known as the "formal argument list". The formal argument list tells us what needs to be given to the object allong with the request to "enterNumber" -- in this case, the number to enter. Notice that it is a "double", as opposed to, for example, an "int". We'll soon see examples of methods specifications that use more than one argument.

Notice also the Java reserved word "void". It is basically a placeholder. It indicates that when this method is done, it doesn't give anything back to the caller. Remember, for this model of the calculator, the calculator will eventually display the result. As a constract, consider the example we did in class, where was asked, "add 5 and 4" and expected a reply so that the requestor could play with the value from there. Next class, we'll see methods that have more interesting return types. In these cases, the placeholder "void" will be one of replaced by "int", "float", "char", &c.

Calculator Example

Below is the calculator example we fleshed out in class:

  class Calculator {
    private double accumulator;
    private double display;
    private char operation;

    public Calculator() {
      accumulator = 0.0;
      display = 0.0;
      operation = '=';
    }


    public void add() {
      display = accumulator;
      operation = '+';
    }


    public void subtract() {
      display = accumulator;
      operation = '-';
    }


    public void multiply() {
      display = accumulator;
      operation = '*';
    }


    public void divide() {
      display = accumulator;
      operation = '/';
    }


    public void equals() {
      display = accumulator;
      operation = '=';
      display();
    }


    public void enterNumber(double input) {
      if ('='  ==  operation)
        accumulator = input;
  
      if ('+'  ==  operation)
        accumulator = accumulator + input;
  
      if ('-'  ==  operation)
        accumulator = accumulator - input;
  
      if ('*'  ==  operation)
        accumulator = accumulator * input;
  
      if ('/'  ==  operation)
        accumulator = accumulator / input;
  
      operation = '=';
      display = input;
    }


    public void display() {
      System.out.println (display);
    }
  
  }
  

The are a bunch of interesting things to notice here. The first are the declaration of the "instance variables", those variables that exist independently in each and every instance of the Calculator:

    private double accumulator;
    private double display;
    private char operation;
  

Notice that they are qualified as being "private". This means that they can only be accessed by methods described within the Calculator class -- not from methods of other classes. This type of access protection ensures that we control their access through the methods that we write. Most instance variables will be "private". You also see the general form of a declaration: " "

The next thing you might notice is the "constructor". It looks a lot like a method specification -- but you'll notice a few differences. First, it has exactly the same name as the class -- they are both called, "Calculator". The constructor always has the same exact name, including capitalization, as the class, itself.

Second, you might notice that there is no return type specification, not even a "void". Remember, the constructor is not "called", so it cannot "return". It is simply a description of how Java should go about initializing an instance -- it is not a generally callable method. This is also the reson that we left it out of the "class skeleton" that we created above.

The constructor's basic job is to give each of the instance variables an initial value and also to do any other necessary initialization. The constructor is shown below:

    public Calculator() {
      accumulator = 0.0;
      display = 0.0;
      operation = '=';
    }
  

The add() method, shown below, is a good example of a simple method. You can see the simple assignments performed within the method body. The "assignment operator", a single =-sign, works just as it did in your math classes -- it assigns the value from the right-hand side to the variable on theleft-hand side.

    public void add() {
      display = accumulator;
      operation = '+';
    }
  

The enterNumber() method, shown below, is a bit more sophisticated. Notice that the value "input" is passed in by the caller. Since input is declared within the formal argument list, we know that it's "scope" is local -- it can only be used within this method. We also know that it is initialized with a value passed in by the caller.

We also see, for the first time, the "if statement". The if statement allows for conditional execution. If the predicate is true, the body is executed. If not, the body is skipped. The predicate of the if statement is a "boolean expression". In other words, it is an expression that evaluates to true or false. If it evaluates to true, the body is executed. If it evaluates to false, the body is not executed.

  Consider the specific example below:
      if ('+'  ==  operation)
        accumulator = accumulator + input;
  

If the "operation" variable holds the "+" sign, then the assignment "accumulator = accumulator + input" is made. Otherwise it is skipped. The {}-braces are optional if only one statement is controlled by the if -- but required if there are more than one. We'll see examples of this later. But, below is an example:

Consider the specific example below: if ('+' == operation) { accumulator = accumulator + input; System.out.println ("Added the numbers."); }

The only other thing to notice is the use of '-single-quotes. Single quotes are used to enclose "char" types, single characters. We'll learn soon that "-double-quotes are used to enclose strings of characters, such as "Hello World". Since it is possible to have a "String" that only happens to contain a single character, we need to be careful to use the type fo quote that matches the declared type -- we can't base it on the lenght of the particular case.

The full method is shown below:

    public void enterNumber(double input) {
      if ('='  ==  operation)
        accumulator = input;
  
      if ('+'  ==  operation)
        accumulator = accumulator + input;
  
      if ('-'  ==  operation)
        accumulator = accumulator - input;
  
      if ('*'  ==  operation)
        accumulator = accumulator * input;
  
      if ('/'  ==  operation)
        accumulator = accumulator / input;
  
      operation = '=';
      display = input;
    }
  

Structure and Style

In Java, {}-braces denote the beginning and ending of blocks. a }-brace is never followed immediately by a semicolon. But, each "statement" within a block is ended with a semicolon. It is technically legal to place several statements on the same line -- but this is generally considered bad form as it makes the code hard to read.

Notice that the code is indented within each set of {}-braces. Each level of nexting gets anotehr level of indenting. To keep the code readable, indents should be fairly small -- just 2-3 characters. Otherwise, as the code becomes more complex and we find ourselves with nested if statements within loops within method specifications within classes, we'll waste too much real estatre on indents.

Vertical space, meaning blank lines, should be used to group related code together to make it more readable. Identifiers should always be meaningful and descriptive -- they should not be cryptic or so abbreviated as to be meaningless. As a matter of good style, when an identifier consists of multiple words, we combine them by running them together and capitalizing exactly the first letter of each word. We do not use _-underscores. The only exception is that we do not capitalize the first letter of an identifer -- except for the names of classes (and interfaces), where we do capitalize the first letter.

From Classes to Objects

Last class, we created the Calculator class specification. But, a specification is, like a schematic or a blue print, just a descriptive document. We haven't yet created an actual calculator. So, let's talk about how to do that.

We're not going to build our calculator out of wood, plastic or metal. Instead, it is going to be a model constructed within the program. So, we need to ask Java to create the new calculator. We do this using Java's new operator, as follows:

  new Calculator();
  

The "new" operator allocates the storage for the Calculator and then uses the "constructor" specified within the Calculator class to initialize its instance variables and perform any other initialization. Once the new operator is done, a new Calculator, all ready to go, exists within the program.

In this case, the constructor doesn't happen to take any arguments, but if it did, they would be passed in within the ()-parentheses.

References and Reference Variables

Okay, so, we've created a new Calculator object, but how do we use it? In order to use an object, we need a reference to it. A reference is something we use to "refer to an object". In other words, it is something that we use to identify which object to use. In general, references are stored within reference variables and are accessed through those variables.

The new-operator gives us a reference to the newly created object. We can capture that reference into a variable by assigning it. Consider the code below:

  Calculator c;
  c = new Calculator();
  

The variable "c" is known as a reference variable because it stores a reference to the calculator. And, this is very important: It stores a reference to a Calculator, not the Calculator object, itself.

Please note that other intro sections confuse the details here. They make claims such as "c is an object" or "C is a Calculator". These claims are false. "c" denotes a variable capable of holding a reference to a calculator -- not a calculator. A reference is a simple, primitive value. The reference, itself, is not complex and cannot exhibit any behaviors -- it is simply read and assigned, just like an "int", "float", or "char".

Next class, we'll take a look at an example that makes it particularly clear that these variables are references to object, not objects themselves. It'll also make it clear that they are primitive values, not objects.

We manipulate an object via a reference to it via the "scope oeprator", which is a period. For example, "c.add();" asks the calculator to add. "c.enterNumber(6);" asks the calculator to enter the number 6 as input.

The "main" method
Okay. So, we know how to create class specs. And, we know how to turn those specs into objects. But, how does a program actually get started? This happens via a special method known as "main". When we start up Java, we give it the program and it runs the main() method. This method is static. In other words, it isn't the behaivor of an object, it is just floating out there by itself. Java actually runs it, rather than an object exhibiting it.

The main() method is the starting point. It will create the initial objects and get the program going. The main method always has exactly the same form. And, it is very important that it always takes the same form shown below. Java looks for a method exactly like this as the starting point. If the arguments don't match or the name doesn't match, it won't be recognized as the starting point. So, memorize this!

  public static void main(String[] args) {
    /*
     * Your code here
     */
  }
  

Notice the qualifer "static" -- you now know what this means. This means that this method isn't part of an object. Instead, it is just "floating". For the moment, I'd like to punt on "String[] args". In short, "String[]" represents a "String array", which is basically a list of Strings. We'll hit the details soon.

In class, we created the following main() method -- it does nothing more than create a new calculator and ask it to go through a few of its motions.

  public static void main(String[] args) {
    Calculator c = new Calculator();

    c.enterNumber(4);
    c.display();
    c.add();
    c.display();
    c.enterNumber(5);
    c.display();
    c.add();
    c.display();
    c.enterNumber(6);
    c.display();
    c.equals();
    c.display();
  }
  

Person vs. Machine: Closing the Gap

Next, I'd like to talk about how to actually get our program to run. But, before I do that, I want to provide some perspective about how our code interacts with the computer and the environment. This will help us to better understand the toos we use to make our program run and how they work.

When people write programs, they are doing it with a mind toward solving some real problem. They want to be concerned with the problem and its solution, not the details of the machine that is a tool for solving it.

For this reason, programmers usually write programs in English-like programming languages: C, C++, and our favorite, Java, for example. Although these languages are structured in a way that is useful to a computer, they are designed to be understandable and convenient for people.

Ultimately, a program written by a computer in one of these languages, a so-called High-Level Language is translated into another form that is better tailored for the machine. This new form is often known as an assembly language program or, in Java, byte code.

To help understand the difference, I like to think about a car with a driver and passenger. The passenger might give the driver directions:

Back out of the driveway and go right. Continue for 4 blocks. At the stop sign, make a right. Travel about 1/2 mile. You'll see a grocery store. The parking lot is on the right-hand side of the road, just past that grocery store. Park there.

The passenger provided a set of high-level instruction to the driver. These instructions were provided in a way that the driver understood and used commands that were descriptive in the context of the problem: Navigating the city en route to the grocery store.

But the car, the machine, can't understand these instructions. It requires instructions in a different language. The instructions might begin like this:

Press break pedal: Not less than 25 lbs of pressure. Push key into keyswitch, twist forward to on position; listen for click. Twist forward again with not less than 10 lbs of pressure. Listen for motor. Release keyswitch pressure. Slide gear selector down two notches into reverse. Reduce brake pressure to 5 lbs. Roll to bottom of driveway. Rapdily increase break pressure to not less than 25 lbs.

Etc. Etc. Etc.

The driver has to translate the high-level instructions provided by the passenger into a low-level (physical) language understood by the car. This language is coposed of much smaller steps. And, it might vary slightly from car to car. For example, the appropriate actions taken by the driver will be different for a car with a "standard" transmission than one with an "automatic" transmission. So, although the passenger's language is problem-oriented and car-independent, the driver's translation is car-oriented and car-specific.

The programming process happens in much the same way. Human programmers produce high-level directions in languages like Java. In this respect, we are acting like the passenger. These documents are then given to a program called a compiler that acts like the driver and translates them into a language appropriate for the machine.

The resulting program, which is machine specific, can run on only one type of computer, such as iMacs running OS X. This is why, when you go to the store, you have to buy different versions of the same program for different types of computers. The same high-level programs were compiled for different types of computer.

Java and the Virtual Machine

The designers of Java wanted to take a different approach. They wanted a programming system which was "compile once-run anywhere". In other words, they wanted one set of machine-oriented instructions to work on all computers.

This solution obviously can't work -- at least without something else entering the picture. Why not? Different computers understand different low-level instructions.

So, here's what they did. They invented a new type of computer, the Java Machine. And, they wrote a compiler that would convert Java high-level programs into low-level programs that would only run on this Java Machine.

But, they didn't build this machine in hardware. Instead they wrote a program to model it in software. Running this program creates the so-called Java Virtual Machine (JVM). The JVM was then compiled for many different types of physical computers. This software was then made available for many systems: It is part of the jdk that you might ahve downloaded to run java from home.

So, here's the Java model. The JVM is written and compiled for many different systems and is distributed widely. Java programmers compile software for the JVM. When one wants to run a Java program, one starts the JVM program and lets it run the compiled Java program.

Command-line Tools

We can compile this program using the Java compiler. It shouldn't be surprising that this program is named, javac. Here's how it works:
javac Calculator.java
If we look in the file system, we now see the byte-code -- the low-level program for the JVM. The byte code has the extension ".class".
ls Calculator.class

We can start java programs by providing the class name, without the extension, to the JVM. This is done, again no great surprise, with a program called "java" as follows:

java Calculator

The above example prints, as intended, the output of our Calculator's quick-and-dirty test program onto a console.

Making it Happen

The process used to create the "Calculator program" involved three steps: creating the Java program, compiling it, and starting it. If it had been a more complex program we might also have had to manage multiple pieces or debug mistakes.

Literal Values and Strings

I'd like to observe that all literal values are known before a program ever runs -- they are embedded within the code, itself. As a result, the compiler simply creates them when compiling -- they are born with the program, they are not created at runtime.

So, consider the following:

  String s1  = "Hello World";
  String s2  = "Hello World";
  

The code above initializes each of s1 and s2 to reference the literal String "Hello World". The way this works is that, before the program runs, Java creates a "Hello World" String object. At runtime, the literal "Hello World" embedded within the program basically acts as a reference to this pre-existing object. So, both s1 and s2 reference exactly the same object.

The == Operator

Some sections tell their students that, "You can't use == to compare Objects." And, this is true -- but as they present it, it makes no sense. You can use the ==-operator to compare string references. The ==-operator works exactly as it does for every other primitive type: It looks into the box and it sees if the two values, in this case references, match.

So, let's take a look at a classic example and see what happens:

  String s1  = "Hello World";
  String s2  = "Hello World";

  if (s1 == s2) 
    System.out.println ("The two references identify the same object.");
  else
    System.out.println ("The two references identify different objects.");
  

To the surprise of some folks, "The two references identify the same object." is printed. The want to believe that "s1" and "s2" are Strings and that the ==-operator is comparing them. But, we debunked that myth last class. They are not String objects -- they are reference variables. And, in this case, they both point to the single "Hello World" object.

Now consider the example below. It forces the creation of a new String object by calling the "new" operator. You can see that a reference to the "Hello World" object is being passed into the constructor. It does exactly what you'd expect -- it creates another object that also models "Hello World". In effect, what we now have, are two "Red, 2008, Honda Accords". We have two different, but equivalent objects.

  String s1  = "Hello World";
  String s2  = new String("Hello World");

  if (s1 == s2) 
    System.out.println ("The two references identify the same object.");
  else
    System.out.println ("The two references identify different objects.");
  

This time the code prints, "The two references identify different objects.". And, this is, of course, correct -- we created a second object. Remember, we are comparing the refernces, not the objects. Each reference identifies a different object -- it doesn't matter that the two objects happen to be equivalent.

The equals() method

Well, what do we do if we want to know if the objects, themselves, are equivalent? To solve this problem, we can't use the ==-operator. We know that the ==-operator does the same thing for references as it does any other type of primitive variable -- it looks at the value and makes a comparison. It looks at the references, themselves, not the objects that they identify.

As it turns out, in Java, when we write a class specification, we don't start with a blank slate. Instead, we inherit features from a generic class specification called Object. What this means is that, without us having to write them, we get some methods that are part of the Object class's specification.

This includes one method as below:

  public boolean equals (Object o) {
    return (this == o);
  }
  

Notice "this" in the code above. In any method, "this" is a reference to the object that is running the method. So, for example, if we consider "x.equals(y)", within the equals() method, "this" is equal to "x" and "y" is passed in as "o".

So, we see that the equals() method, by default, does exactly the same thing as the ==-operator. And, this isn't surprising. What else can it do in a "one size fits all" way? Comparing objects is difficult stuff. And, it is done differntly for each type of object.

For example, in comapring strings, they are equivalent if each corresponding letter matches. But, in comparing people, they might be considered equal if they have the same number of years of experience and the same degree, or the same height, weight, and game-day stats.

So, by default, the equals() method isn't particularly useful -- it is the same as the more convenient operator. But, the good news is that, in writing our own class specifications, we can override it. If we write a method with exactly the same name and argument list, it will replace the inherited version -- allowing us to compare the objects, themselves, however we'd like. We'll do some of this next class.

But, for now, here's what I'd like you to remember: Use the equals() operator if you want to compare Strings, since they are objects -- only use the ==-operator if you want to compare the references, themselves.

If we rewrite our example form above, it looks as below:

  String s1  = "Hello World";
  String s2  = new String("Hello World");

  if (s1.equals(s2)) 
    System.out.println ("The two Strings are equivalent.");
  else
    System.out.println ("The two Strings are equivalent.);
  

Hidden Instance Variables

Consider the following example:

  class Scorecard {
    private String playerName;

    public Scorecard (String playerName) {
      playerName = playerName;
    }

    ...
  }
  

Do you see the problem? We are trying to initialize the instance variable "playerName" with the argument "playerName" -- and what we've got makes no sense. Both identifiers are in scope. They both are usable. Does the identifier "playerName" represent the instance variable? Or the argument?

Well, the argument hides the instance variable. "playerName" refers to the argument. So, how do we get to the instance variable by the same name?

You've got the tool in your toolbox. Can you think of it? We just discussed it. Remember the "this" reference? Check out how it can solve the problem by giving us a way to get to the instance variable:

  class Scorecard {
    private String playerName;

    public Scorecard (String playerName) {
      this.playerName = playerName;
    }

    ...
  }