Return to lecture notes index
May 19, 2005 (Lecture 3)

A Quick Exercise

Before leaving, we lead the class in our first programming exercises: "Hello World". We entered, compiled, and ran the following code:

  import java.io.*;

  class Program {
    public static void main (String[] args) {
      System.out.println ("Hello world");
    }
  }
  

Compilation, Execution, and the JVM

The first step in the process is to "compile" the program. This means to translate the Java code into a highly specialized set of instructions that actually direct the computer hardware.

In many languages, this compialtion process produces an executible program that runs on the actual hardware. The problem with this approach is that each program must be recompiled for each platform. For example, the smae C++ program would need to be compiled separately for each of PC, Sun, SGI, and Mac computers. This makes the software more complex to deploy and maintain.

Java uses a virtual machine, known as the Java Virtual Machine (JVM). Basically, Sun defined a hardware system and then wrote a program to simulate it. This single program is compiled for each supported architecture. Then, Java programs are compiled for the JVM. As a result, any compiled Java program can run on any JVM -- regardless of the actual, physical host hardware. This is said to be a "compile once run anywhere" system.

So, when you run "javac SomeClass.java", it compiles the Java code to machine code for the JVM. Java calls this machine code the byte code. When you run, "java SomeClass", you are starting ther JVM program and asking it to start the static main() method of the class SomeClass, defined within the file SomeClass.class.

static methods, sometimes known as class methods aren't actually behaviors of instantiated objects. Instead, they are just "things to do" that live within the name space of the class.

Primitives versus Objects

Let's begin by considering one very important question. In truth, it is one that is sometimes on the minds of intro students -- even at the end of the semester:

I have an idea about what is and what isn't an object, and what is and what isn't a primitive, but I really don't know exactly what the difference is. What is it?

Primitive variables are simply named storage areas. And, in Java, typed, named storage. We can identify a particular storage area using a variable name and, using this name, we can read and write a value. Since the storage is typed, the storage area can only hold one type of value. If we try to read or write a different type of value, the compiler will stop us.

Objects are a more sophisticated language construct. Object are complex types that are composed of one or more values, known as member fields (instance and class variables) and behavior definitions (methods).

You may have, in a prior course, discussed Abstract Data Types (ADTs). Abstract data types are models that include both the data, itself, and the ways that the data can be used or manipulated. For example, we might use the array data structure to store collections of all sorts of different things: business cards, an hourly planner, or the pages of an event log . It might make sense to linearly search the array for all three applications. But, not everything that applies to one applies to another.

I really wish I could insert new hours between old ones, just as I can stick new business cards into the middle of a Roladex. But, unfortunately, the world doesn't work this way. There's only one hour between 2:00PM and 3:00PM on any given day. It makes sense to insert buiness cards, but not hours. Similarly, it might make sense to add pages to the end of an event log -- but I can't add hours to the end of a day. No matter how hard I try, there are only 24 of them.

If we build a library of array operations, such as search insert-in-middle, insert-at-and, &c, and then use this library for each of these applications, it will work -- but we run the risk of making mistakes. There is nothing that prevents the creation of the 36 hour day, except correct programming.

But, if we have a way of binding the right operations to the right data in software, we can avoid these mistakes. The insert method associated with business cards won't be available for the hourly planner.

Classes give us a way of doing this. They allow us, the language to enforce the properties of our abstract data types by binding the operations and the data together to give us a complete blueprint for some type of object -- while isolating these things from the properties and behaviors of all other classes of objects.

So, to answer the original question, primitives are just typed places to store and retrieve information. Objects are complete models of components of a system that completely contain the attributes and behaviors of the objects. Objects both include their own attributes and behaviors, and exclude others.

The Age Old Question: What Is "Person p;"?

Assume that we've got some class, Person:
  class Person 
  {
    // Full definition
  }
  

Consider the following declaration:

  Person p;
  

What is "p"? About half of the class answered, "A Person". Well, not exactly.

If we were programming in C++, that answer would be correct. "p" would be the name of the variable that contains the members methods and fields of the Person class. In other words, "p would be a Person."

But, in Java, "p" is not a Person. Instead, it is a primitive. Specifically, it is a reference variable. A reference is nothing more than a name for an object -- a way of "referring to an object".

In Java, references are the handle we use to manipulate objects. If we want to interact with an object, we do so by using its name. Remember that, at this point in lecture, I started calling people by name and asking them to do things. This is exactly the way Java works.

"p.walk()" sends the walk message to the object whose name (reference) is stored in the variable "p" in exactly the same way that "Dom, please stand up" send the message to Dom to stand up.

In Java, we manipulate objects using their names, which are called references. These variables, which hold references, are known as reference variables.

When we use the "new" operator to create a new object, it returns a name for the new object. This name, a reference, is stored in a reference variable (or the "new" is pretty useless).

Person p = new Person();

Identity Is Not Necessarily Equality

In Java, as with many other languages, we can check to see if two primitives are the same by using the "==" operator. But, in Java, I am going to refer to this as the "identity" operator. It doesn't ask the question, "Are these two things equal?" Instead, it asks the question, "Are these the same thing?"

If two variables name the same thing, then the thing names by both is obviously eual to itself. So, when we are playing with primitives, identity and equality are exactly the same thing.

But, when we shift our focus to objects, they can become two different things. We use the "==" operator to test for equal identity, and the ".equals()" method to test for equality.

"==" evaluates to true if, and only if, the references on both sides name the same object. For example, two different $1 bills are not the same dollar bill -- even though their value is equal.

All objects have a .equals() method. If it is properly defined, it is used to compare the value of objects:

  paperMoney bill1;
  paperMoney bill2;

  if (bill1.equals(bill2))
    System.out.prinln ("Both bills have the same value");

  if (bill1 == bill2)
    System.out.prinln ("bill1 and bill2 are exactly the same piece of cloth.");

  

Although the .equals() method is automatically defined on all objects, it almost always has to be redefined for each class. The default comparison is pretty brain-dead. So, the implementor of the proverbial Person class should implement a .equals() method that is able to meaningfully determine if two people are equal -- a pretty cool job for a mortal, if you ask me.

We'll talk more about the .equals() method shortly. In particular, we'll discuss the mechanism by which Java ensures that it is present in all objects.

The String Example

Java's String class makes for a great example of the distinction. In lecture, I actually used this example to motivate the equality versus identity discussion above -- but it makes more sense to reverse it in the lecture notes.

Remember that in Java, unlike other languages such as C, Strings are first class objects. By comparison, what C programmers call a string is nothing more than an array of characters. C++ has String object. But, unlike C++, Java's strings are immutable. Once created, they cannot change. Instead, we jsut create new ones and allow the garbage collector to recycle the old ones.

Regardless, take a look at this code fragment and predict its output:

  String s1 = new String ("Surprise!");
  String s2 = new String ("Surprise!");

  if (s1 == s2)
    System.out.println ("The two strings are the same.");
  else
    System.out.println ("The two strings are different.");
  

When I polled the class, about 2/3 of the class got this right. No doubt the number was so high simply because the example would have been boring otherwise. It prints, "The two strings are different."

But, after our discussion about "==" vs ".equal", this should make sense. The version that does the more intuitive comparison follows:

  String s1 = new String ("Surprise!");
  String s2 = new String ("Surprise!");

  if (s1.equals(s2))
    System.out.println ("The two strings are the equal.");
  else
    System.out.println ("The two strings are not equal.");
  

This prints, as everyone understood, "The two strings are equal".

String Methods

Just to refresh your memory, below are some frequently used String methods:

Overloading Methods

Java allows the overloading of methods. Pay attention! Overloading is different than overriding, which we'll discuss shortly. Methods are said to be overloaded when multiple methods have the same name, but a different signature. If two or more methods have the same name, but different parameters, Java will invoke the one with the matching parameter list. It is important to note that, for this purpose, Java does not consider the return type -- so, if two methods have the same name, it is not sufficient for them to differ only by the return type.

The substring() methods described above are an excellent example of overloading a method name.

Composition vs. Inheritence

Let's continue a thread I introduced briefly yesterday. Recall that there are two common, basic relationships among object: composition and inheritence.

Somehow many people tend to confuse these in their design. This is because they can sometimes accomplish the same thing. But, unfortunately, if misused, software becomes hard to understand, maintain, and enhance. Whereas, if properly understood and applied, these ideas can drastically improve design.

Fortunately, with a little thought, the two don't appear very similar at all. And, there is a litmus test for which one to use: Is the relationship between the classes has a or is a?

Let's discuss what I mean by has a as compared to is a .

Composition

Composition is very easy for most people to understand. If you examine any complext object, you will often find that it is composed of many different parts. For exmaple, if we examine my car, we will find that it contains doors, windows, a motor (actually two), the transmission, the radiatior, &c. And, we can further decompose these parts into parts. Ever opened a transmission? Gears. Pulleys. Bands. Pins. Broken things.

So, in modeling complex objects, we often decompose them into smaller, more easily manageable parts. And, this is often helpful to us in many ways. For example, if GM or Ford were to build a model of a car as a single class, it would be very complicated and hard to get correct. And, once they did, it could not be readily adapted to other situations. But, for example, if they modeled each of the major components separately, and then created a class that contained each part and enabled them to interact, they would still have the same model of the whole care, except that it would probably have been easier, faster, adn cheapear to build -- and except for the fact that it is more adaptable. For example, if the emissions standards changed, they could perhaps swap just the catalytic convert class contained within the car and have a brand new model -- now they coudl easily see how any candidate would work. Similarly, they could test more powerful engines and see which suspension components might also need to be replaced.

When we build classes by composition, sometimes the instances of the classes are private. Sometimes they are public. And, sometimes, they shouldn't be used anywhere except within the complex class. We can accomplish this by nesting one class definition within another and making it private.

At this point, it should be pretty straight-forward to see the has a relationship -- a car has a transmission. A car has a tire. This is the relationship that we model by compostion.

Inheritence

Okay everyone. Please picture a mammal. No! Don't think of a dog. No! Not a cat! Forget that cow. And, I don't even want to hear about the duck-billed platapus. Just picture a mammal that isn't some particular type of mammal. Can't do it, can you? So there!

All mammals share certain characteristics that make them mammals. In particular, they are warm-blooded, and young offspring are milk-feeding. All, except the duck-billed platapus are live bearing (as opposed to egg-hatching) -- but let's forget this freak of nature.

The bottom line is that by telling you that an animal is a mammal, I am immediately telling you that it has certain characterisitcs and behaviors. Specific mammals might have additional, or even slightly different characteristics or behaviors. But they are still mammals.

In software, if we were asked to model a whole bunch of different mammals, we might begin by building a model of the mammal class that captures the characteristics of all mammals. Then, you might build models of other types of mammals that extend what you already defined in the mammal class, for example, monotrmes, marsupials, and placental mammals.

Then, you might construct models of each individual type of mammal further extending and specifying these categories. Or, you might have yet more intermediate categories, such as a primate type extending the placental mammal type.

This illustrates the heart of the "is a relationship". A "marsupial is a mammal.". A "primate is a. placental." And a "placental is a mammal." So, a "primate is a mammal" (Transative property).

Java allows inheritence from one other class. Unlike C++, and other languages, it does not allow multiple inheritence. So, for example, I couldn't build one of those cute new cell-phone/PDA combo things by inheriting from both the PDA and cellphone classes.

I think this is a big loss for Java. But, the truth is that many people don't understand inheritence -- and multiple inheritence proved to be too much for the back-yard-grade C++ programmer. It produced many disasters -- and few success stories. It is also a bit of a pain from the perspective of the compiler writer and for performance -- so Java took it out. In truth, very few people miss this feature of C++.

The class that is behing inherited from is known as the base or parent class. The class that is a specific type of the parent class is known as the derived class or the child class.

Java has three access specifiers: public, private and protected. Public members of the base class remain public in the derived classes. Private members of the base class are inaccessible in the derived classes. And protected members of the base class are private in the derived class, but inaccessible from other classes.

Sometimes, when we design a class, it should never be built, because it is incomplete. A Mammal class is one such example. We call this type of class an abstract class. We can inherit from it, but we cannot instantiate it. We can even define abstract methods by providing a signature of a method ending in a semicolon, prefixed with the abstract specifier. This means that we can't instantiate a dervied class, unless it implementes this method.

abstract class Mammal {
  // Some implementation of the mammal class here

  abstract public Mammal liveBirth(); // an abstract method

}

class HomoSapien extends Mammal {
  // This class will inherit from the Mammal class

  public Mammal liveBirth(); // implementation of the abstract method
}

The Object Class

Every class in the Java language, and every class that you write, automatically extends the Object class. This has two significant benefits:

Overriding Methods

What if we are inheriting certain behaviors from another class, but we want to customize one of those behaviors to be more suitable for the more specific type. For instance, suppose we define a Person to drink water, but we want to define a Student to drink beer.


class Person
{
        public void drink() { // drinks water }
}

class Student extends Person
{
        public void drink() { // drinks beer }
}

This is called overriding the drink() method of the parent class. Overriding tells the computer to replace the inherited behavior of the method with the new method. Other people will drink water when they are given the drink() command, but students will instead drink beer.

We will override the .equals() and .toString() methods for almost all of the classes that we implement. This ensures that programs can compare them, and print them out using System.out.println().