1 The Basic Steps: Migrating Dillos
1.1 Data Definitions
1.2 Examples of Data
1.3 Functions
1.4 Test Cases
1.5 Templates
2 Migrating Mixed Data: Animals
2.1 Data Definitions for Mixed Data
2.2 Methods over Mixed Data
2.3 Conditionals
2.4 Class Diagrams
3 Migrating Family Trees
3.1 Template Naming Conventions
4 Summary
4.1 Java Naming Conventions

Migrating to Java

In your How to Design Programs course, you learned a design methodology consisting of five steps: data definitions, examples of data, templates, test cases, functions. This same methodology works in Java. In fact, you can easily convert any program you wrote in Racket to a similar function in Java once you know how to convert each individual step.

1 The Basic Steps: Migrating Dillos

We’ll show the basic conversion steps using the following Racket data definition and program for armadillos:

  ; A dillo is (make-dillo number boolean)

  (define-struct dillo (length dead?))

  

  ;;; Examples of Data

  (define dead-dillo (make-dillo 2 true))

  (define big-dillo (make-dillo 6 false))

  

  ;;; Template

  ; dillo-func : dillo -> ???

  (define (dillo-func a-dillo)

    (... (dillo-length a-dillo) ...

         (dillo-dead? a-dillo) ... ))

  

  ; hit-with-truck : dillo -> dillo

  ; produce a dead dillo that is 1 unit longer than the given dillo

  (define (hit-with-truck adillo)

    (make-dillo (+ 1 (dillo-length adillo)) true))

  

  ;;; Test cases

  (check-expect (hit-with-truck dead-dillo) (make-dillo 3 true))

  (check-expect (hit-with-truck big-dillo) (make-dillo 7 true))

1.1 Data Definitions

Recall that structs provide a way to make compound data in Racket. In Java, the construct for compound data is called a class. The following Java code defines the Dillo class:

  class Dillo {

    int length ;

    boolean isDead ;

  

    Dillo (int length, boolean isDead) {

      this.length = length ;

      this.isDead = isDead ;

    }

  }

The first three lines capture the class name (Dillo), field names (length and isDead), and field types (int and boolean).

The next three lines of code define the Java analog to make-dillo. Unlike Racket, Java does not define constructors automatically (for useful reasons that we will get to later in the course). The name of the class comes first, followed by a list of parameters, one for each field name (on the first line). The second and third lines assign the parameter values to the actual fields.

Things to note when mapping from structs to classes:
  • Java includes the types of data in the code (Racket had the types only in comments).

  • Java uses int where Racket used number.

  • Java doesn’t allow punctuation in field names (such as the ? in dead?, so we will follow the common naming convention of isX.

1.2 Examples of Data

Our Racket program contains two examples of dillos:

  (define dead-dillo (make-dillo 2 true))

  (define big-dillo (make-dillo 6 false))

In Racket, we could simply put these definitions in our file (at the so-called "top level"). In Java, all definitions must lie inside classes. We therefore need a class in which to put our examples. We will create a class called Examples to hold all our examples of data:

  class Examples {

    Examples () {} ;

  

    Dillo deadDillo = new Dillo (2, true) ;

    Dillo bigDillo = new Dillo (6, false) ;

  

  }

The first line inside the class is the constructor for the class. Since this class has no fields, the constructor is trivial.

The operation new Dillo serves the role of make-dillo in Racket: it creates a Dillo with the given arguments as the field values.

Why didn’t we just put these inside the Dillo class? Remember, the analog to a class (for now) is a Racket struct. We didn’t put examples inside the structures in Racket. We put them outside the structure definition. We’re simply following the same pattern here.

In Java terminology, we refer to each of deadDillo and bigDillo as an object. Racket used the term value for all data, including instances of structs. In object-oriented programming, an object is an instance of a class. Each object has its own copy of the fields declared in the class (the this keyword used in the Dillo constructor emphasizes that the code references the copies of the specific object being created). The uniqueness of field variables will get more nuanced as we get further into Java, but the "own copy" metaphor suffices for now.

1.3 Functions

Now we will write the hit-with-truck function over Dillos in Java. In object-oriented programming (OOP), functions are stored in the same class as the data on which they operate; this is one of OOP’s hallmark features. OOP uses the term method instead of function; think of methods as the ways in which one can interact with an object.

As a reminder, here is the Racket definition:

  ; hit-with-truck : dillo -> dillo

  ; produce a dead dillo that is one unit longer than the given dillo

  (define (hit-with-truck adillo)

    (make-dillo (+ 1 (dillo-length adillo)) true))

The corresponding Java method appears at the bottom of the Dillo class shown below. The first line of the definition states the type of data returned (Dillo), the method name (hitWithTruck), and parameters (none in this case). The second line contains the body of the method, prefixed with the keyword return (required). As in our examples of data, we converted the make-dillo call from Racket into new Dillo in Java.

  class Dillo {

    int length ;

    boolean isDead ;

  

    Dillo (int length, boolean isDead) {

      this.length = length ;

      this.isDead = isDead ;

    }

  

    // produces a dead Dillo that is one unit longer than this one

    Dillo hitWithTruck () {

      return new Dillo(this.length + 1 , true) ;

    }

  }

Several things to note about this method definition:
  • The adillo parameter from the Racket program has disappeared. Since the method lives inside a class, every method takes the current object as an implicit argument named this. Thus, we write this.length in place of (dillo-length adillo) from the Racket version.

  • The Racket contract moves into the method header. The return type is declared before the method name. The input types get declared in the parameter list, with the exception of the implicit this parameter. We will see examples of methods with additional parameters shortly.

  • The Racket purpose moves into a Java comment. Double back-slashes in Java comment out the rest of line as did semi-colons in Racket.

  • Java doesn’t allow hyphens in method names, so we use a different convention for multi-word names. In particular, we lower-case the first word, then upper case the first letter of all remaining words (this is a common Java convention).

1.4 Test Cases

Normally, we still write test cases before writing methods. We presented method definitions first to provide useful context for writing test cases in Java. In Racket, we wrote test cases through check-expect:

  (check-expect (hit-with-truck dead-dillo) (make-dillo 3 true))

  (check-expect (hit-with-truck big-dillo) (make-dillo 7 true))

For now, we will continue to use check-expect-style test cases in Java, with the help of a testing library. (We will cover subtleties of testing later in the course.) The course webpage describes how to download and configure your Java environment to use the library. Once the library is in place, we can add the check-expect tests to the Examples class as follows:

  import tester.* ;

  class Examples {

    Examples () {} ;

  

    Dillo deadDillo = new Dillo (2, true) ;

    Dillo bigDillo = new Dillo (6, false) ;

  

    boolean test1(Tester t) {

      return t.checkExpect(deadDillo.hitWithTruck(),

                           new Dillo(3, true)) ;

    }

  

    boolean test2(Tester t) {

      return t.checkExpect(bigDillo.hitWithTruck(),

                           new Dillo(7, true)) ;

    }

  }

Each test case is written as a method in the Examples class. Each test-case method must return a boolean, have a name starting with the characters test, and take a Tester object as its sole input.

The body of the test-case methods demonstrate how to call methods on objects in Java. Whereas in Racket we would write (hit-with-truck big-dillo), in Java we write bigDillo.hitWithTruck(). Literally, this code says to access the hitWithTruck method inside of the bigDillo object (bigDillo.hitWithTruck), then to call the method with (no) arguments (the () after hitWithTruck).

Note the similarity and differences between accessing fields and methods in Java objects. Both take the form object.<item>, but methods require a (possibly empty) list of arguments after the method name.

As in Racket, checkExpect takes two arguments: the computed result and the expected answer. In Java, checkExpect is a method inside Tester objects (in these examples, the Tester is named t). You don’t need to understand how the tester library works beyond this example: simply copy the pattern for each test case, inserting your test expression and expected answer as you did in Racket.

1.5 Templates

Now that you know what methods look like in Java, we can step back and migrate templates (in practice, we will still write templates before methods). The Racket template for Dillos is

  ; dillo-func : dillo -> ???

  (define (dillo-func a-dillo)

    (... (dillo-length a-dillo) ...

         (dillo-dead? a-dillo) ... ))

Just as in Racket, the Java template is a skeleton of a method definition. The Java template leaves the return type unspecified, omits the argument (since this object is implicit) and accesses the fields of the object. The /* and */ characters enclose a multi-line comment.

  class Dillo {

    int length ;

    boolean isDead ;

  

    Dillo (int length, boolean isDead) {

      this.length = length ;

      this.isDead = isDead ;

    }

  

    /* TEMPLATE

    ?? Template () {

      this.length ...

      this.isDead ...

    }

    */

  

    // produces a dead Dillo one unit longer than this one

    Dillo hitWithTruck () {

      return new Dillo(this.length + 1 , true) ;

    }

  }

You have now seen your first complete Java program, developed using the same steps and principles that you learned with Racket.

2 Migrating Mixed Data: Animals

Now that we know how to migrate single classes, let’s migrate a mixed data definition. Recall that mixed data introduces a new type name for any of several existing types of data:

2.1 Data Definitions for Mixed Data

As an example, let’s add a definition for a boa constrictor and a mixed-data type for animals, which can be boas or dillos:

  ;; A boa is (make-boa string number string)

  (define-struct boa (name length eats))

  

  ;; An animal is

  ;; - a boa, or

  ;; - a dillo

The Java class for the Boa is similar to that for Dillos:

  class Boa {

    String name ;

    int length ;

    String eats ;

  

    Boa (String name, int length, String eats) {

      this.name = name ;

      this.length = length ;

      this.eats = eats ;

    }

  

      /* TEMPLATE

    ?? Template () {

      this.name ...

      this.length ...

      this.eats ...

    }

    */

  }

Now we have to migrate the animal data definition. One difference between Racket and Java is that all type names in Java are explicit in the code, rather than implicit in comments. Our animal definition therefore needs to correspond to some construct. The appropriate Java construct is an interface:

  interface IAnimal {}

Right now, all this interface does is declare a new type name called IAnimal (by convention, interfaces in Java start with a capital letter I). We will do more with it shortly.

The interface declaration introduces IAnimal as a type name, but we have not yet made boas and dillos valid variants of animals (as the Racket definition stated). To do that, we add IAnimal to the first line of each of the Boa and Dillo class definitions through an implements clause, as follows:

  interface IAnimal {}

  

  class Dillo implements IAnimal {

    int length ;

    ...

  }

  

  class Boa implements IAnimal {

    String name ;

    ...

  }

In Java, implements achieves two things: it declares that a given class is a valid value of the type with the name of the interface, and it requires the class to satisfy all constraints of the interface. IAnimal is now a valid type name. It doesn’t yet impose constraints on its implementing classes, but we’ll get to that shortly.

If you are coming from previous Java experience and would not have used an interface here, hold that thought. We will address your question in a couple of days when everyone has seen enough Java to understand the answer.

Otherwise, our running code remains the same. As in Racket, we don’t need examples of data on IAnimal because the Boa and Dillo examples suffice.

2.2 Methods over Mixed Data

Let’s write a method on IAnimal that determines whether the animal is normal size for its type. The corresponding Racket function is:

  ;; normal-size? : animal -> boolean

  ;; determine whether animal is within an expected size range

  (define (normal-size? an-ani)

    (cond [(boa? an-ani) (and (<= 5 (boa-length an-ani))

                              (<= (boa-length an-ani 10)))]

          [(dillo? an-ani) (and (<= 2 (dillo-length an-ani))

                                (<= (dillo-length an-ani) 3))]))

  

  (check-expect (normal-size? bigDillo), false)

  (check-expect (normal-size? boa1), true)

First, we extend our Examples class with examples of boas and the test cases for our new method:

  class Examples {

    Examples () {} ;

  

    Dillo deadDillo = new Dillo (2, true) ;

    Dillo bigDillo = new Dillo (6, false) ;

    Boa boa1 = new Boa("Slinky", 6, "pets") ;

    Boa boa2 = new Boa("Slim", 4, "lettuce") ;

  

    boolean test1(Tester t) {

      return t.checkExpect(deadDillo.hitWithTruck(),

                           new Dillo(3, true)) ;

    }

  

    boolean test2(Tester t) {

      return t.checkExpect(bigDillo.hitWithTruck(),

                           new Dillo(7, true)) ;

    }

  

    boolean test3(Tester t) {

      return t.checkExpect(bigDillo.isNormalSize(), false) ;

    }

      boolean test4(Tester t) {

      return t.checkExpect(boa1.isNormalSize(), true) ;

    }

  }

We remarked earlier that in OOP, all methods live with their corresponding data. Since the data on animals lie in the Boa and Dillo classes, the isNormalSize method should live there too. We therefore put an isNormalSize method in each of the Boa and Dillo classes (for brevity, we omit the template and hitWithTruck method):

  class Dillo implements IAnimal {

    int length ;

    boolean isDead ;

  

    Dillo (int length, boolean isDead) {

      this.length = length ;

      this.isDead = isDead ;

    }

  

    public boolean isNormalSize () {

      return 2 <= this.length && this.length <= 3 ;

    }

  }

  

  class Boa implements IAnimal {

    String name ;

    int length ;

    String eats ;

  

    Boa (String name, int length, String eats) {

      this.name = name ;

      this.length = length ;

      this.eats = eats ;

    }

  

    public boolean isNormalSize () {

      return 5 <= this.length && this.length <= 10 ;

    }

  }

Compare the Java and Racket versions. What can we observe?
  • The cond from the Racket version isn’t explicit in the Java code. This is an artifact of how OOP organizes programs. There is no need for the cond because the appropriate implementation of isNormalSize for each class is inside the objects themselves.

    This feature, called dispatch, is another fundamental element of OOP. Once every object carries its own methods, methods don’t need to ask about the type of an object. This important principle is worth repeating. OOP methods should never ask for the type of an object. The code structure, and hence the language, take care of that automatically.

    As a consequence of automatic dispatch, there is no separate template for animals beyond the Boa and Dillo templates.

  • The method body in each class implements the same computation as the answer expression in the corresponding cond-clause in Racket.

The contract on the original normal-size? function in Racket stated that it consumed an animal. Our isNormalSize methods in Java are defined for Boa and Dillo, but not IAnimal. Once types are explicit in code, we expect languages to help us track whether we are using those types correctly. In this case, we would like Java to warn us if we ever create a class that implements IAnimal but doesn’t provide an isNormalSize method. We get this behavior by including isNormalSize in the IAnimal interface:

  interface IAnimal {

    boolean isNormalSize () ;

  }

Now, if a class implements IAnimal but does not include an isNormalSize method, Java will flag an error. This is your first example of a constraint that an interface imposes on its implementing classes.

You may have noticed that the keyword public precedes each isNormalSize method. In Java, every concrete method required by an interface must begin with this keyword. We will return to public and what it means in the coming weeks.

For reference, here is the complete Java code for our animals example:

  interface IAnimal {

    boolean isNormalSize () ;

  }

  

  class Dillo implements IAnimal {

    int length ;

    boolean isDead ;

  

    Dillo (int length, boolean isDead) {

      this.length = length ;

      this.isDead = isDead ;

    }

  

    /* TEMPLATE

    ?? Template () {

      this.length ...

      this.isDead ...

    }

    */

  

    // produces a dead Dillo one unit longer than this one

    Dillo hitWithTruck () {

      return new Dillo(this.length + 1 , true) ;

    }

  

    public boolean isNormalSize () {

      return 2 <= this.length && this.length <= 3 ;

    }

  }

  

  class Boa implements IAnimal {

    String name ;

    int length ;

    String eats ;

  

    Boa (String name, int length, String eats) {

      this.name = name ;

      this.length = length ;

      this.eats = eats ;

    }

  

    /* TEMPLATE

    ?? Template () {

      this.name ...

      this.length ...

      this.eats ...

    }

    */

    public boolean isNormalSize () {

      return 5 <= this.length && this.length <= 10 ;

    }

  }

  

  class Examples {

    Examples () {} ;

  

    Dillo deadDillo = new Dillo (2, true) ;

    Dillo bigDillo = new Dillo (6, false) ;

    Boa boa1 = new Boa("Slinky", 6, "pets") ;

    Boa boa2 = new Boa("Slim", 4, "lettuce") ;

  

    boolean test1(Tester t) {

      return t.checkExpect(deadDillo.hitWithTruck(),

                           new Dillo(3, true)) ;

    }

  

    boolean test2(Tester t) {

      return t.checkExpect(bigDillo.hitWithTruck(),

                           new Dillo(7, true)) ;

    }

  

    boolean test3(Tester t) {

      return t.checkExpect(bigDillo.isNormalSize(), false) ;

    }

  

    boolean test4(Tester t) {

      return t.checkExpect(boa1.isNormalSize(), true) ;

    }

  }

2.3 Conditionals

In the previous example, the cond from the Racket program disappeared because Java automatically dispatches on types of objects. Not all conditionals are about types of objects though. Consider the following program:

  ;; meal-size : dillo -> number

  ;; produce number of calories needed per meal for given dillo

  (define (meal-size adillo)

    (cond [(dillo-dead? adillo) 0]

          [(< (dillo-length adillo) 4) 500]

          [else 800]))

Conditionals about the program logic (rather than object types) use the if/else constructs in Java. Here’s the corresponding code (as a method in the Dillo class):

  // produce number of calories needed per meal for this dillo

  int mealSize () {

    if (this.isDead) {

      return 0;

    } else if (this.length < 4) {

      return 500;

    } else {

      return 800;

    }

  }

Things to note:
  • Parentheses are required around the question expression after if.

  • When the answer part of a conditional has only one statement (as each of these cases does), Java lets you omit the curly braces around the answer. We’ve shown the general case here so you see the commonly-accepted style of including the braces. We don’t care which approach you take on assignments.

2.4 Class Diagrams

In OOP, it is common to draw a class diagram summarizing the contents of and relationships between classes and interfaces. The class diagram for our animals example follows:

Class
diagram with Boa, Dillo, and IAnimal

In this diagram, each box represents a class or an interface. Each box has three sections: one for the name, one for the fields, and one for the methods. We distinguish interfaces from classes in two ways: first, by the naming convention (starting with capital I), second by the connections between boxes. The dashed arrows should be read "Dillo implements IAnimal" and "Boa implements IAnimal". If your data is complicated, a class diagram can be a good way to summarize your data and its relationships.

3 Migrating Family Trees

Finally, we consider a data definition that contains references to user-defined data (the "arrows" from Racket data definitions). Consider a simple family-tree definition in which each person has a name, mother, and a father:

  ; A famTree is

  ; (make-unknown)

  ; (make-person string famTree famTree)

  (define-struct unknown ())

  (define-struct person (name mother father))

  

  (define ft1

     (make-person "Bart"

                  (make-person "Marge" (make-unknown) (make-unknown))

                  (make-person "Homer" (make-unknown)

                                       (make-person

                                        "Abe"

                                        (make-unknown)

                                        (make-unknown)))))

Summarizing the steps we have seen so far, we need to
  • Convert each struct to a class

  • Introduce an interface for the mixed-datum name famTree

  • Have each class implement the interface

  • Use the interface name for the type name wherever the data definition refers to famTree

The initial classes (with templates) follow:

  interface IFamTree {}

  

  class Unknown implements IFamTree {

    Unknown () {}

  

    /*

    public ?? Template () {

       ...

    }

    */

  }

  

  class Person implements IFamTree {

    String name ;

    IFamTree mother ;

    IFamTree father ;

  

    Person (String name, IFamTree mother, IFamTree father) {

      this.name = name ;

      this.mother = mother ;

      this.father = father ;

    }

  

    /*

    public ?? Template () {

       this.name ...

       this.mother.Template() ...

       this.father.Template() ...

    }

    */

  }

Note that this conversion is no different from the one on animals, with the exception of the recursive template calls on mother and father. Arrows become method calls in templates, just as they did in Racket.

This example also illustrates that interfaces may be used as type names in Java (see IFamTree as the type for mother and father in the Person class). It is worth asking ourselves here whether IFamTree is a better type than Person for mother and father. In this case, yes, because parents have to be able to be Unknown: the IFamTree type allows this, while type Person would not.

In contrast, consider the types when we add our data example ft1 to the Examples class. There, we would write:

  Person ft1 =

    new Person("Bart",

                new Person("Marge", new Unknown(), new Unknown()),

                new Person("Homer", ...));

Arguably, we could ascribe either type Person or IFamTree to ft1. Here, we want the more-specific Person type. If we wrote a test case that tried to access a field of ft1, the compiler would give us an error because an IFamTree might be Unknown which has no fields.

Upshot: when specifying the type of a field or parameter, be as general as possible. When declaring the type of an object or variable, be as specific as possible.

3.1 Template Naming Conventions

You may have noticed that we changed our naming convention on templates as compared to in Racket. In Racket, we gave templates type-specific yet generic names like BoaFunc or PersonFunc. Here, we use the totally generic Template. The family tree example motivates using a common name like this. Do you see why? (think about it before reading on.)

Imagine that we had named the two templates in this example UnknownFunc and PersonFunc. Which one should we call on this.mother in the Person template? We don’t know, and that’s the point: the mother could be either another (known) person or an unknown person. We need some common name to allow mother to be either of these concrete types.

We had the same situation in Racket though. Why didn’t we encounter a problem there? Remember that in Racket we would have made a template for the mixed-data definition for family trees. In particular, we would have had a FamTreeFunc with a cond clause that determined which kind of family tree we have. When we discussed animals earlier, we noted that since Java does the cond automatically, we no longer write templates on mixed-data types like animal and family tree. This means we no longer get a shared name for the template function for free either.

Why not at least use a shared name like FamTreeFunc in the template anyway, since both classes implement the IFamTree interface? We could certainly do that, but it won’t scale for long. Next week, we will start working with classes that implement multiple interfaces. When that happens, this approach gets messy quickly. Also, such names were more important in Racket when template definitions were at the top level and could correspond to any data definition. In Java, the templates are inside the classes, so the names aren’t needed to link templates to their data.

The lesson here is simple: use a generic name for all of your template functions. It doesn’t weaken the utility of the methodology.

4 Summary

The entire design methodology from How to Design Programs ports to Java. Data definitions, examples of data, templates, and test cases map over with a simple syntactic translation. For functions, you need to learn some different syntactic conventions, but the shape of the code is largely the same. To summarize, here are the high-level points to remember:

4.1 Java Naming Conventions

Throughout these notes and the course, we will adhere to standard naming conventions from Java. In a multi-word name, the words are concatenated with conventional capitalization patterns. Camel case means that the first letter of each word is capitalized, as in CamelCaseText. Mixed case means that the first letter of every word after the first word is capitalized, as in usesMixedCase.