Comp 210 Lab 13: Lists in C and Java

(While waiting for lab to start up, you can go ahead and create a directory for this lab, and copy ~comp210/Labs/lab13/List* to it.)

Table of Contents

  • Lists in C
  • Lists in Java
  • Running a Java Program
  • Casting
  • Lists in C

    In lecture, you were introduced to lists in C.
    // Give a shorter name to the structure:
    typedef  struct Cons_s  Cons;
    
    // Define the structure
    struct Cons_s {
      int car;
      Cons* cdr;
      };
    
    The empty list is NULL (which can be had via #include <stdlib.h>), and you can make a two-element list named two by writing Cons* two = makeCons( 3, makeCons( 5, NULL) ). You can then look at the int named two->car or the Cons* named two->cdr. Unlike Scheme, the type Cons as defined is only good for lists of ints.

    In lecture, makeCons wasn't shown; you can probably figure it out, but here it is:

    /* makeCons
     * create a list with desired car and cdr
     */
    Cons* makeCons( int datum, Cons* rest ) {
      Cons* result;          // This is the value we'll return.
      result = new Cons;     // Create the new structure (with garbage contents).
    
      result->car = datum;   // Set up the innards of the structure.
      result->cdr = rest;
    
      return result;
      }
    

    How would you write a function to print out a list? Think it over in your head, and then check here. Exercise: Make a copy of the file ~comp210/Labs/lab13/List.c, and add a function length, which takes in an argument of type Cons* and returns an int. Hint: write the corresponding Scheme function, and it should translate easily into C.
    Exercise: Write a tail-recursive version of length. What C construct does tail-recursion translate into?
    If you want more practice, you can also right the function sum, in both a non-tail-recursive and tail-recursive fashion. Check with your therapy leader (or send mail to ian or any TA) to make sure your loop is correct.


    Lists in Java

    The rest of the lab will introduce you to Java.

    Whereas Scheme is a functional language (predisposed towards using functions without side-effects), Java is (like C) an imperative language: its basic unit of computation is the assignment statement. In fact, Java borrows a lot of superficial syntax from C: =, if, ==, for, while, etc.

    However, Java is also object-oriented. An object is a structure which has been beefed up to not only have fields (like ints or chars), but also have functions associated with the structure. Such functions happen to be called methods. (Another object-oriented language is SmallTalk; C++ claims to be one, too.)

    Let's look at an example. We will mirror the data description for lists:

    A List (of integers) is either
  • Null, or
  • Cons( int car, List cdr );
  • In Scheme such data descriptions were in our comments, but not truly part of the program; it was up to us to write code which was consistent with the data description. Even in C, with its weak notion of types, we didn't have the above data description as part of the program. The entire point of structures was to help the program's data format to come closer to the data definition. However, using objects (classes), Java does have a way to internalize the data description so that the program knows about it.

    We will say that there is a class List. However, every list actually has a more specific type; it is one of the two sub-classes Null or Cons. Here is how we write this in Java. This is part of the file ~comp210/Labs/lab13/List.java. For the moment, ignore lines mentioning length(); we'll come back to that.

    abstract class List {
    
      // Any List, of any subclass, can tell us its length.
      // However, the code must wait, since the answer depends on whether
      // the List is ultimately a native instance of Cons or Null.
      //
      abstract public int length();
      }
    
    
    class Null extends List {
      public int length() { 
        // --- you'll complete this function momentarily! ---
        return -99;
        }
      }
    
    class Cons extends List {
      public int car;
      public List cdr;
    
      public Cons( int datum, List rest ) {    // The constructor for a Cons object
        car = datum;                           //   (this is what "new" calls).
        cdr = rest;
        }
    
      public int length() { 
        // --- you'll complete this function momentarily! ---
        return -99;
        }
      }
    
    The abstract class List is defining a new class of objects (similar to declaring a struct in C). The word abstract means that there aren't any objects that are native Lists; instead all lists are particular subclasses of list. Yep, you guessed it -- all lists are either Null or Cons. Each of those classes are everything that a List is, and perhaps more, which is why each of these classes extends List. Note that any Cons is a List, but not necessarily vice-versa.

    There's not a whole lot involved with class Null. However, if an object is of class Cons, then it has a car and cdr. Just as we did for C structures, we declare those fields.

    However, recall that in C we had wrote a function makeCons which (1) called new to create the structure, and then (2) filled in the structure. Since this pattern happens so often with objects, object-oriented languages have special methods called constructors. The constructor for a class is a function which has the same name as the class itself, no declared return type, and does the above two steps. So in this case, the method Cons( int datum, List rest ) is the constructor. If lst is an instance of a List, then new Cons( 4, lst ) creates a new Cons object, and passes 4, lst to the constructor.

    Finally, just include the keyword public as a matter of faith, putting it in front of each field and method of an object. (Actually, you can get by with only using it for main and toString, but that's another story.)

    For example, here is a short test-program for Lists:

      /* List.main
       * A test suite.
       * This method should be inside the class List.
       */
      public static void main( String[] args ) {
        List l1, l2;
        l1 = new Null();
        l2 = new Cons( 3, new Cons( 5, new Cons( 4, new Null() )));
    
        System.out.println( "l1 is " + l1 + ", and has length " + l1.length() + "." );
        System.out.println( "l2 is " + l2 + ", and has length " + l2.length()  + ".");
        }
      }
    
    Okay, there's some verbiage to get around, but the gist of the program is in the lines
        l1 = new Null();
        l2 = new Cons( 3, new Cons( 5, new Cons( 4, new Null() )));
    
    which creates lists of length zero and three, respectively. (If you were wondering, yes the class Null has a default constructor which takes no arguments and does nothing.) As for explaining the rest of List.main:
  • public static void main( String[] args )
    is how you need to declare the function main. For now, just memorize the mantra; we won't worry about the meaning of public static. Observe that each different class (here, List, Cons, Null) may have its own main.
  • List l1, l2;
    This is analagous to C's Cons* l1, * l2; but you don't need to worry about that nasty star.
  • System.out.println()
    is analagous to printf, except that it takes a String argument. (Don't read this: Actually, System is a class which contains a field out which is an object for which the method println is defined. You shouldn't have read that.)
  • As in C, things between double-quotes are strings. Furthermore, + applied to strings means "concatenate them". But what about "l2 is " + l2? This is a string added to a List. It turns out, Java will take the List l2 and convert it to a String. Java does this by using a List method with the particular name toString(), if you've defined that. If not, Java uses a generic (and probably unhelpful) toString() method. It's good to know that Java Strings, unlike C, are not crazy character arrays, and are straightforward to use.
  • Finally, what is this l2.length()?
    It's trying to look like both accessing a field of a structure, and a function call. Can't it make up it's mind? Well, it is both: remember that in the object-oriented world, structures now have functions (called methods) associated with them; length() is a function that all List objects know about. Think of l2.length() as saying "go have the object l2 determine its length, and report back to us". Thus l1.length() and l2.length() each return the length of the respective object. (What's actually happening is that the function length() is being passed with an implicit parameter, the object itself, in this case l1 or l2.)

    This philosophy, that an object has certain things it knows how to do, and you just ask it the right message (length in this case), is the heart of object-oriented programming. That is, notice that l1.length() calls Null's length(), while l2.length() calls Cons's length(). However main() didn't need to worry about figuring which one of those two functions was the correct one to call -- it just asked the objects l1,l2, which invoked the appropriate functions themselves.

  • Running a Java Program

    Okay okay enough talk -- let's run a program! If you haven't typed in the above code, you can copy it from the file ~comp210/Labs/lab13/ListSimple.java to your own directory, fire up emacs, and open the file. (By opening a file with the .java suffix, emacs knows to enter its Java mode.)

    As with C programs, there are two steps to running the program:

  • First, compile the file. You could type javac ListSimple.java from the UNIX prompt, but heck let's have emacs do this for us by just typing M-c.

    This creates the files List.class, Null.class, and Cons.class. (This is analagous to calling g++ to compile C programs, which creates a single executable file.)

  • Now we run the program, which in our case is the function main() associated with class List.
      owl% java List
    
    This should run your program (by invoking the Java interpreter).

  • Well, the result of running SimpleList were probably not too exciting: not only did the length function always return -99, but Null.toString and Cons.toString didn't provide anything interesting to System.out.println. Just as List.c defined print and printWoParens to achieve nice output, we'll have to do the same to our Java program. This has been done for you in List.java.

    Casting

    The file List.java nearly has the following code:
        List lst3;
        // ...lst3 is assigned the value (3 5 4)
        lst3 = new Cons( 17, lst3.cdr );   // NOT QUITE -- must cast lst3 on rhs
        // That was the equivalent of "(set! lst3 (cons 17 (cdr lst3)))", so
        // now, lst3 is the list (17 5 4)
    
    Of course, lst3.cdr refers to the "rest" of lst3. However, the compiler quibbles about this: remember that cdr was only defined for Cons objects; but how does the computer know that lst3 is a Cons and not a Null, since all we told it is that lst3 is a List?

    Well, it turns out, the compiler doesn't know (it's not smart enough to infer this). If you were to try compiling the above, you would get the error "No method cdr for class List". But we know, by cleverly looking at how lst3 gets a value, that at the moment, lst3 really is a Cons, not just any old List. You do this by casting lst3 to be a Cons: A cast consists of preceding an expression by its type in parentheses. Thus instead of l3.cdr, write write ((Cons) lst3).cdr.

    instanceof

    Note that you don't want to cast frivolously; only when you know for certain that a variable currently happens to be a more specific type than it is declared as. If you try to cast something to (say) Cons when it really isn't one, your program will screech to a halt with an error.

    A safer way might be to do a sanity-check, to make sure the cast is legal. Java provides an operator instanceof which asks if an object can be interpreted as an instance of some class. For instance:

    if (lst3 instanceof Cons)
      lst3 = new Cons( 17, ((Cons) lst3).cdr );  // this cast guaranteed safe
    else
      // hmm, hopefully shouldn't reach this
      System.out.println( "List.main: Uh-oh, lst3 is unexpected." + lst3 );
    

    Exercise: Copy ~comp210/Labs/lab13/List.java to your own directory, and modify it so that Null.length() and Cons.length() work correctly.
    Exercise: Declare the method sum(), and write it.
    Exercise: Re-write length and sum so they are equivalent to their tail-recursive version. Be sure to use a loop.

    Of course, in all these examples, there's nothing special about using lists of int; any type would do. The final homework will have you work with lists of String.

    I'll start compiling a list of some common Java errors.


    Back to Comp 210 Home