Migrating to Java
Our first task in this course is to help you migrate what you learned in your first programming course to Java. A few new concepts will come up this week, but mostly we are shifting you to a new language and new way of organzing code.
If you came through How to Design Programs (HtDP), you learned a design methodology consisting of five steps: data definitions, examples of data, templates, test cases, functions. This same methodology works in Java. We will follow these same five steps, just using Java as the programming language. If you did not previous take a How to Design Programs course, you’ll have to learn the terminology for the steps as we go along.
We’ll show the migration to Java by writing code that creates and operates on (arma)dillos, a kind of data that we want to create (looking ahead to writing programs to manage a small zoo). Let’s assume two pieces of information matter about each dillo: how long it is, and whether or not it is dead. We will also write a function called canShelter that consumes a dillo and determines whether the dillo is both dead and large enough that someone could take shelter in its shell (this isn’t hypothetical: a relative of the armadillo, the Glyptodon, could grow as large as a Volkswagen and was believed to be used for human shelter).
These notes use colored backgrounds for content specific to those coming from 1101/1102/Racket (blue), 1004/Python (pink), and prior Java experience (orange). Those with prior Java experience likely learned different ways to do some of what we cover here, and your comments will explain why we are doing things differently.
Here is the Racket code, showing the design recipe steps, that we will convert to Java in these notes:
;;; Data Definition |
; A dillo is (make-dillo number boolean) |
(define-struct dillo (length dead?)) |
|
;;; Examples of Data |
(define baby-dillo (make-dillo 8 false)) |
(define adult-dillo (make-dillo 24 false)) |
(define huge-dead-dillo (make-dillo 65 true)) |
|
;;; A Function |
; can-shelter : dillo -> boolean |
; determine whether dillo is dead and larger than 60 inches |
(define (can-shelter adillo) |
(and (dillo-dead? adillo) |
(> (dillo-length adillo) 60))) |
|
;;; Test cases |
(check-expect (can-shelter baby-dillo) false) |
(check-expect (can-shelter huge-dead-dillo) true) |
Our task is to migrate the ideas in this code one step at a time, starting with the data definition, the part of the code that indicates that we want to be able to create dillos.
We do not provide the Python version of the code because CS1004 does not cover the same design recipe or design concepts that we are building on in CS2102.
1 Migrating Data Definitions
In Java, if you want a kind of data that isn’t something simple like a number, string, or boolean, you create a class. Classes let you specify compound data, which is data that has different components (such as a phone contact having both a name and a phone number). If you are coming from Racket, Java classes capture what you used to put in define-structs.
The following Java code defines the Dillo class with components for length and death status:
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 types for each field (int and boolean). Types are a new concept for many of you – we will return to them in more detail over the next couple of days.
The next three lines of code define the what is called the constructor: the function you call to create armadillos (analogous to make-dillo from Racket). 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 save the parameter values in the actual fields.
Unlike Racket, Java does not define constructors (like make-dillo) automatically (we will discuss why later in the course). You have to define the constructor function manually.
Java includes the types of each field 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.
Those of you coming from cs1004 would have seen the class definition for Dillos written as follows:
class Dillo: |
def __init__(self, length, dead): |
self.length = length |
self.dead = dead |
return |
|
def Length(self): |
return self.length |
|
def Dead(self): |
return self.dead |
|
Things to note:
In Java, the fields must be declared by listing their names (and types) separately at the top of the class.
In Java, use the name of the class to name the constructor, rather than __init__.
The constructor in Python takes an extra parameter, self, which is not included in Java. As we will see shortly, Java lets you use the term this to do what you used to do with self.
Java constructors do not need the return statement (in fact, Java requires return to be followed by the value to return).
In Java, you do not need to define the Length and Dead functions. Java gives you a way to get to those values automatically (we’ll discuss this shortly).
2 Migrating Examples of Data
After creating classes, you should always create some examples of data from your classes. These examples both show how to use your classes and give you ready-made data inputs to use in testing your functions. In general, your examples should cover the interesting options within your data: for dillos, this means having examples of each of live and dead dillos, including of different lengths.
Here, we will make three dillos, a live one of length 8, a live one of length 24, and a dead one of length 65. Here’s what the live dillo of length 8 looks like in each of Racket and Python. In each case, we saved the dillo we made through the name "baby dillo":
-- in Racket |
(define baby-dillo (make-dillo 8 false)) |
|
-- in Python |
babyDillo = Dillo(8, False) |
In both Racket and Python, you could simply put these definitions in your 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 babyDillo = new Dillo (8, false); |
Dillo adultDillo = new Dillo (24, false); |
Dillo hugeDeadDillo = new Dillo (65, true); |
} |
The first line inside the class is the constructor for the class. Since this class has no fields, the constructor is trivial. It still has to be included, however.
The next three lines create Dillos and store them under names that we can use to retrieve them later. To create a Dillo in Java, we use the new construct, followed by the name of the class you want to create and the parameters required by the constructor for that class. To save a value (like a Dillo) under a name, we write the type of the value, the name, an = sign, and the value to assign to the name.
When you use the new operator, Java performs the operations in the constructor: so new Dillo(8, false) creates a Dillo, sets the new Dillo’s length to 8 and its isDead to false.
For those coming from Racket, things to note about creating examples of data:
Names in Java cannot contain hyphens or other punctuation. The convention in Java is to use something called camel case: string the words together by using a capital letter for each word after the first. Thus, huge-dead-dillo in Racket becomes hugeDeadDillo in Java.
For those coming from CS1004, things to note about creating examples of data:autofau
In Java, you need to use new in front of the name of the class that you are creating from; Python didn’t need this.
Note the capitalization on the boolean: False in Python and false in Java. In Java, False and false are actually different – we’ll discuss the difference in a day or two. For now, note that we are using lowercase letters on booleans.
3 Running Programs
We now have a program containing two classes: Dillo and Examples. Let’s see how to run the program.
In either Racket or Python, you learned to load or run a program with a single step (or push of a button). Java requires two steps: compiling and running.
When you compile, Java reads the definitions of your classes and checks for various errors in your code (we’ll talk about these checks in more detail later). It doesn’t execute the content of any of the classes.
When you run your code (which you can only do after you compile), Java looks for a special class called Main, which has a method (function) named main. Java runs the main method automatically. We’ll talk about writing the Main class later in the term. For now, simply include this Main.java file in each collection of code that you write.
The default Main class that we gave you currently doesn’t do anything other than print a message telling you to run your tests (which we will discuss shortly). To execute the code in your Examples class, you need to run your tests. You can also interact with Java at the console/interactions prompt, as you used to in DrRacket, as a way to execute expressions.
4 Key Terminology: Objects
So far, our Examples class mainly has uses of new whose results are stored under names. Whenever you use new on a class, you get something called an object. An object represents a particular value or entity within that class. For example, new Dillo(8, false) is a concrete live Dillo of length 8. A class, in contrast, describes a kind of data you want to create, but lacks specific values for the component data. Some people explain objects with a physical analogy: an item that exists in the physical world (such as a specific Dillo along the roadside) corresponds to an object, while a description of what information makes up a Dillo corresponds to a class. We say that an object is an instance of a class.
The difference between classes (descriptions of data) and objects (actual data) is an essential concept in programming. You’ve encountered this distinction before, but perhaps under different terminology. Those coming from CS1004 have already used these terms. For those coming from Racket, a class is similar to define-struct and an object is the value you got from calling make-dillo or some other constructor provided by define-struct. In Racket, we used the term value for concrete data, but that also included simple data like numbers and strings. Here, objects are values that are made by calling new for a class.
Other nuances of classes and objects will come up as we begin writing functions. Before we get to that, let’s stop and make sure you understand what happens under the hood when you use the class and new constructs.
5 Under the Hood: Classes, Objects, Naming, and new
To understand how Java "works", we will map out what happens under the hood as you compile and run programs. Java organizes your programs and computations into three main "areas" under the hood: known classes, existing objects, and named values. Before you compile a program, all of these are empty (well, mostly: Java has a bunch of built-in classes, but we haven’t talked about those yet). So here’s the map before you compile a program:
Different areas of the map get populated by different constructs and operators in Java. Roughly speaking, the class construct modifies the known classes area (in the compile step), new modifies the existing objects area (in the run or testing steps), and = modifies the named values area (in the run step).
This slide sequence shows how the map gets modified as you create a class and run several expressions (either in an Examples class or on the command line).
What should you understand from this?
The known classes area gets modified when you compile the program – that’s when Java finds out what classes your program knows about.
The existing objects and named values areas get modified when you run or test the program. While we’ve only looked at defining examples so far, you can imagine from your prior programming experience that you could create objects and name values frequently while running a program. These areas are much more dynamic.
Objects are only accessible through names. Under the hood, there are often objects that exist but aren’t accessible. This isn’t a problem!. What matters is that you have names for the objects that you need to get to. This is actually a fairly complex topic that we will revisit throughout the course.
We will return to this picture throughout the course. If you aren’t comfortable with it, be sure to do the extra exercises (linked to the syllabus page) for working with the program-execution map.
6 Migrating Functions
Now we will write the canShelter function over Dillos in Java. In object-oriented programming (OOP), functions are placed in the class for the primary 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, the method we are trying to write determines whether a Dillo is dead and large enough for a person to take shelter in. For the second criterion, we will check whether the Dillo has length longer than 60 (inches).
The Java method for this appears at the bottom of the Dillo class shown below. The first line of the definition states the type of data returned (boolean), the method name (canShelter), and parameters (none in this case). The second line contains the body of the method, prefixed with the keyword return (required). The body of the method shows the && notation for writing and in Java. We’ll explain the this in a moment.
class Dillo { |
int length ; |
boolean isDead ; |
|
Dillo (int length, boolean isDead) { |
this.length = length ; |
this.isDead = isDead ; |
} |
|
// determines whether Dillo is dead and longer than 60 |
boolean canShelter() { |
return (this.isDead && this.length > 60); |
} |
} |
But wait – didn’t we initially say that the canShelter method should take a dillo? Why isn’t there a parameter then? This is one of the essential traits of object-oriented programming. Every method goes inside a class. That means the only way you can "get to" a method in order to call it is to go through an object (in this case, a dillo). Since you need an object to even call a method, you don’t need that object as a parameter.
As an example, here’s how you would call the canShelter method on babyDillo (which we defined in the Examples class):
babyDillo.canShelter(); |
When you run this expression, Java will evaluate the body of the method, which contains
this.isDead && this.length > 60 |
Here, this refers to the object that you used to get to the method. So it is as if you typed
babyDillo.isDead && babyDillo.length > 60 |
Java doesn’t actually replace "this" with "babyDillo", but this is the essence of what happens under the hood. If you had called
hugeDeadDillo.canShelter() |
Java would instead use the values of isDead and length that are stored inside hugeDeadDillo.
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.
For those coming from Racket, the 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.
Double forward-slashes in Java comment out the rest of line (as did semi-colons in Racket).
For multi-word method names, we use a convention in which we use lower case for the first word and upper case for the rest (unlike Racket, Java doesn’t allow hyphens in method names).
When you run a method, the contents of the under-the-hood map change as the method executes. This slide sequence shows what happens to the map if you run the canShelter method. Here is another slide sequence showing what happens on a method that takes another object as an input (a longerThan method on Dillos).
7 Migrating Test Cases
Whenever you write a method, you should write some examples of how you expect the method to behave. These examples provide some (but not all) test cases that you need to confirm that a method works properly. Those coming from CS1101/1102 are used to writing test cases (and having them graded); if testing hasn’t been part of your programming practice up to now, you’ll need to learn how to do this. We’ll talk about testing as we go through the course.
Normally, we write examples of method use before we write the method itself. In this introductory segment, we showed you how to write methods first as that provides useful context for writing test expressions for them. Java has an associated testing framework called JUnit (the course webpages describe how to configure your programming environment with JUnit). We will write tests using this framework.
First, we show how to write tests that check boolean conditions (such as calls to canShelter).
We will capture the following two Racket can-shelter tests in Java:
(check-expect (can-shelter baby-dillo) false) |
(check-expect (can-shelter huge-dead-dillo) true) |
Here is the JUnit code for two canShelter tests (this code lives in a file called Examples.java).
import static org.junit.Assert.*; |
import org.junit.Test; |
public class Examples { |
public Examples () {} ; |
|
Dillo babyDillo = new Dillo (8, false); |
Dillo adultDillo = new Dillo (24, false); |
Dillo hugeDeadDillo = new Dillo (65, true); |
|
// check that small live dillos can't shelter |
@Test |
public void testBabyShelter() { |
assertFalse(babyDillo.canShelter()); |
} |
|
// check that large dead dillos can shelter |
@Test |
public void testHugeDeadShelter() { |
assertTrue(hugeDeadDillo.canShelter()); |
} |
} |
Each test case is written as a method in the Examples class. Each test must be annotated with @Test just before the method header. Each method returns a type called void (which means that it doesn’t return any value). For now, just remember to include the public annotation before void; we’ll talk about public in a couple of weeks.
Note on file setup: There are two lines of code just above the Examples class that start with import. These tell Java that you want to use JUnit. You must include them in any file that contains JUnit tests.
Note "public" annotation on the Examples class: Once a class contains tests, Java requires that the class and its constructor be annotated with public. Again, we will talk more about public later in the course; for now, just follow the pattern.
What if we want to test an expression that does not evaluate to boolean? Then we use a JUnit construct called assertEquals. An assertEquals call takes two arguments: the expected result of the test and the expression that you want to test. (This is similar to check-expect from 1101/2, but there you usually wrote the expected answer second). Since we don’t yet have any methods that return something other than boolean, let’s write a test to check that adultDillo is three times longer than babyDillo. That test looks as follows:
// check that adultDillo is 3 times longer than babyDillo |
@Test |
public void threeTimesBaby() { |
assertEquals(babyDillo.length * 3, adultDillo.length); |
} |
You have now seen your first complete Java program, along with the components you are expected to include (classes, examples of data, and test cases). You will have at least an Examples class and classes for the kind of data you are developing in every program you write for this course.
Must tests go into a class named Examples? We have used the convention of an Examples class to help everyone get started gently. In professional Java practice, there can be multiple classes with tests, and those classes can have other names. You can name the examples-and-tests class(es) whatever you want, as long as either Example or Test is part of the class name. Unless you have Java experience, we recommend you stick to Examples for now (but you may use other names if prior experience has prepared you to do so).
8 Migrating Mixed Data: Animals
So far, we’ve defined a class for Dillos. What if we were managing an entire zoo with other kinds of animals as well? We would need to define classes for those other kinds of animals. We would probably have methods or fields in other classes that could hold any kind of animal (for example, a class storing information about shows at the zoo might need fields for the featured animal and the duration of the show: the featured animal could be from one of several classes).
In this section, we will add a second kind of animal, create a type for animals, and write a method isNormalSize that determines whether an animal’s length is in the usual range for its kind.
8.1 Defining Data with Variants
Data has variants if it has encompasses other kinds of data with different components. Animals have variants (not all animals have the same attributes), as do shapes (different attributes define circles and rectangles, for example). If you are coming from CS1101, you may have heard the term mixed data for data with variants (if you don’t recall that term, don’t worry about it).
Let’s define a class for boa constrictors, so we have more than one kind of animal. A boa will have three attributes: its name, its length, and what it likes to eat. Here’s the Java class:
class Boa { |
String name ; |
int length ; |
String eats ; |
|
Boa (String name, int length, String eats) { |
this.name = name ; |
this.length = length ; |
this.eats = eats ; |
} |
} |
If you are working through these notes on your own, add some examples of Boa to your Examples class (the notes will do that later).
We mentioned wanting to write methods on animals. As we have already seen, Java requires us to label all fields, parameters and methods outputs with specific types of data. So if want to have a field that allows "either dillos or boas", we need a type for "any animal". For example, we might want to create a class for Cages, where each cage holds some animal:
class Cage { |
int size; |
___________ resident; |
} |
Our goal is to fill in the blank with a type that can accept either boas or dillos.
If you are coming from CS1101, you would have seen this written through the following comment
; An animal is |
; - dillo, or |
; - boa |
To introduce a new type that is simply one of several classes, we use a construct called an interface. We first create an interface, then we connect it to the classes that belong to it. First, here’s the code to create the 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. 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 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.
What about examples of data? How do we create IAnimals? We can only create objects from classes, not from interfaces. Every Dillo and every Boa is an example of IAnimal, so there’s no need for you to create additional examples of data just because you added an interface.
8.2 Methods over Data with Variants
Let’s write a method on IAnimal that determines whether the animal is normal size for its type. We’ll say that a boa is normal size if its length is between 30 and 60 and a dillo is normal size if its length is between 12 and 24.
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 (<= 30 (boa-length an-ani)) |
(<= (boa-length an-ani 60)))] |
[(dillo? an-ani) (and (<= 12 (dillo-length an-ani)) |
(<= (dillo-length an-ani) 24))])) |
|
; tests |
(define mean-boa (make-boa "Slinky" 36 "pets")) |
(check-expect (normal-size? babyDillo) false) |
(check-expect (normal-size? mean-boa) true) |
First, we extend our Examples class with examples of boas and the test cases for our new method:
import static org.junit.Assert.*; |
import org.junit.Test; |
|
public class Examples { |
public Examples () {} ; |
|
Dillo babyDillo = new Dillo (8, false); |
Dillo adultDillo = new Dillo (24, false); |
Dillo hugeDeadDillo = new Dillo (65, true); |
|
Boa meanBoa = new Boa("Slinky", 36, "pets") ; |
Boa thinBoa = new Boa("Slim", 24, "lettuce") ; |
|
// check that small live dillos can't shelter |
@Test |
public void testBabyShelter() { |
assertFalse(babyDillo.canShelter()); |
} |
|
// check that large dead dillos can shelter |
@Test |
public void testHugeDeadShelter() { |
assertTrue(hugeDeadDillo.canShelter()); |
} |
|
// check that adultDillo is 3 times longer than babyDillo |
@Test |
public void threeTimesBaby() { |
assertEquals(babyDillo.length * 3, adultDillo.length); |
} |
|
// check that an undersize boa is not normal |
@Test |
public void testSlimAbnormal() { |
assertFalse(thinBoa.isNormalSize()); |
} |
|
// check that an oversize dillo is not normal |
@Test |
public void testHugeDeadAbnormal() { |
assertFalse(hugeDeadDillo.isNormalSize()); |
} |
|
// in practice (or on homework), you should also test |
// oversize boas, normal size boas, undersize dillos, |
// and normal dillos |
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 Dillo’s canShelter method):
class Dillo implements IAnimal { |
int length ; |
boolean isDead ; |
|
Dillo (int length, boolean isDead) { |
this.length = length ; |
this.isDead = isDead ; |
} |
|
public boolean isNormalSize () { |
return 12 <= this.length && this.length <= 24 ; |
} |
} |
|
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 30 <= this.length && this.length <= 60 ; |
} |
} |
Wait – we now appear to have two methods, each called isNormalSize. How does Java know which one to use? Remember that we call methods through objects, and each object carries a copy of its methods. So if you call
babyDillo.isNormalSize() |
Java will use the version of the method from the Dillo class. This feature of choosing which version of a method to use based on the class for an object is called dispatch. This is another fundamental element of OOP. For now, all you need to understand is that you get to methods through objects, so you can have different "versions" of the same method in different classes, and Java will find the right one automatically (by going through the object).
You may have noticed that the keyword public precedes each isNormalSize method. We’ll get back to why in a moment.
For those coming from CS1101, 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.
The method body in each class implements the same computation as the answer expression in the corresponding cond-clause in Racket.
The notion of a template on animals, like you had in Racket, doesn’t have a place to sit in Java (since methods are dispersed across objects). Therefore, we won’t write templates on simple mixed data in Java.
8.3 Requiring a Method in all Classes in an Interface
When we described our desired isNormalSize method, we said that it should work on all animals. We have an interface for animals, but we haven’t yet written anything that requires all animals to have this method. We could add another class that implements IAnimal, but nothing would force it to provide isNormalSize. We address this by expanding the IAnimal interface to require isNormalSize:
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.
Now we can get back to the word public that precedes each isNormalSize. In Java, every concrete method required by an interface must begin with this keyword. We will talk more about public and what it does in the coming weeks. For now, just include it when you write methods that are required in interfaces.
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 ; |
} |
|
// determines whether Dillo is dead and longer than 60 |
boolean canShelter() { |
return (this.isDead && this.length > 60); |
} |
|
// determines whether Dillo has length within 12 to 24 inches |
public boolean isNormalSize () { |
return 12 <= this.length && this.length <= 24 ; |
} |
} |
|
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 ; |
} |
|
// determines whether Boa has length within 30 to 60 inches |
public boolean isNormalSize () { |
return 30 <= this.length && this.length <= 60 ; |
} |
} |
|
-------- Examples.java --------------- |
|
import static org.junit.Assert.*; |
import org.junit.Test; |
|
public class Examples { |
public Examples () {} ; |
|
Dillo babyDillo = new Dillo (8, false); |
Dillo adultDillo = new Dillo (24, false); |
Dillo hugeDeadDillo = new Dillo (65, true); |
|
Boa meanBoa = new Boa("Slinky", 36, "pets") ; |
Boa thinBoa = new Boa("Slim", 24, "lettuce") ; |
|
// check that small live dillos can't shelter |
@Test |
public void testBabyShelter() { |
assertFalse(babyDillo.canShelter()); |
} |
|
// check that large dead dillos can shelter |
@Test |
public void testHugeDeadShelter() { |
assertTrue(hugeDeadDillo.canShelter()); |
} |
|
// check that adultDillo is 3 times longer than babyDillo |
@Test |
public void threeTimesBaby() { |
assertEquals(babyDillo.length * 3, adultDillo.length); |
} |
|
// check that an undersize boa is not normal |
@Test |
public void testSlimAbnormal() { |
assertFalse(thinBoa.isNormalSize()); |
} |
|
// check that an oversize dillo is not normal |
@Test |
public void testHugeDeadAbnormal() { |
assertFalse(hugeDeadDillo.isNormalSize()); |
} |
|
// in practice (or on homework), you should also test |
// oversize boas, normal size boas, undersize dillos, |
// and normal dillos |
} |
|
9 An Aside: Conditionals in Java
This is a good time to show you how to write conditionals (such as if/else statements or cond expressions) in Java. Let’s add a method to the Dillo class that returns how many calories a dillo needs in a meal, based on its attributes. In particular, dead dillos don’t eat, dillos shorter than 12 inches get 500 calories and larger dillos get 800 calories:
Here’s the Racket version of the function we are trying to write:
;; 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) 12) 500] |
[else 800])) |
Here’s the Java version of this method:
// produce number of calories needed per meal for this dillo |
int mealSize () { |
if (this.isDead) { |
return 0; |
} else if (this.length < 12) { |
return 500; |
} else { |
return 800; |
} |
} |
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.
In CS1101, you used cond for two different situations: you used it to determine which variant of data you had (for example, asking dillo? or boa? in normal-size?), and you used it to make decisions based on attributes of data (such as determining calories based on the length of a dillo). Even though you used the same cond construct for both situations in Racket, you handle them differently in Java. In particular:
Conditionals about the type of an object go away in Java. Dispatch performs the work of those cond statements automatically.
Conditionals that check non-type attributes of data get converted to if/else constructs in Java.
10 Migrating Family Trees
Finally, we consider a data definition that contains references to itself. Let’s write a class to capture a simple family-tree in which each person has a name, a mother, and a father.
Those coming from CS1101 saw the following Racket data definition for this problem. We will migrate this definition to Java.
; 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))))) |
If you are coming from CS1004 and have not yet worked with trees, this is one of the topics that you just have to catch up with on your own. Experience working with trees is part of the assumed background of CS2102.
As with other tree programs, there are two "new" kinds of data required to capture a family tree: one for a (known) person, and one for an unknown person (where "unknown" here means that you ran out of information about the family history at that point). We need to define both classes, and the interface to tie them together.
You can translate the Racket definition over to Java in a step-by-step fashion, using the following steps:
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 Java classes that we are about to present arise from following these translation steps.
The initial Java classes and interfaces are as follows:
interface IFamTree {} |
|
class Unknown implements IFamTree { |
Unknown () {} |
} |
|
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 ; |
} |
} |
Note here that we used the interface IFamTree as the type of the mother and father fields in the Person class). Why did we use this type rather than Person? Try writing an example family tree: parents have to be able to be Unknown as well as Person. The IFamTree type allows this, while type Person would not.
When we create examples of data, however, we will use the Person type for a person, as shown below:
Person ft1 = |
new Person("Bart", |
new Person("Marge", new Unknown(), new Unknown()), |
new Person("Homer", ...)); |
We could have used IFamTree as the type of ft1, but that could cause other problems. We’ll talk more about choosing between types next week. As an initial rule, use the most specific type you can that covers all of the data that you want to allow.
Those with prior Java experience might have handled unknown people differently, using the null construct instead of a class like Unknown. While that’s a common technique, it actually violates OO design principles. Well-designed OO code should not check the type of data (dispatch does that automatically). If you use null for unknown, your methods in the Person class would need a conditional to check whether the mother/father is null. That’s checking the type of data.
You should only use null in situations where you have "no information". In the case of unknown people, you actually do have information, namely that the person who belongs in this space has not been identified. That information doesn’t have attributes, but it stands for something concrete. You can still write methods to handle not-yet-identified people. You therefore should have a class for the Unknown person rather than use null.
It may feel like unnecessary overhead at first, but it actually leads to much cleaner code, especially once you have multiple methods that process the data and its variants.
10.1 Migrating Templates
CS1101/1102 also introduced templates, skeletons of code that help you write methods that traverse data. Templates apply in Java as well, serving the same purpose of helping you correctly traverse data without missing steps. We will just write the template here: for CS1101/1102 students these should look familiar minus the syntax. For those without those courses, just try to follow along to pick up the pattern.
We present the templates within /* ... */ markers, which is how you write multi-line comments in Java.
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() ... |
} |
*/ |
} |
The interesting part of the template is in the Person class: it captures the expectation that a method on a Person likely needs to call itself (recursively) on each of the mother and father fields. The template in the Unknown class doesn’t seem to contribute much, since it has no contents. We show it here because it reinforces that a method on a type needs to have versions that process every variant of that type. If you try to write a method on family trees, you will need a version of that method in the Person class and a method in the Unknown class.
In 2102, we will not require you to write templates as separate comments in your code (as in CS1101/1102). We will, however, expect your programs to follow the recursive structure of template code: if you are processing a family tree, we should see recursive calls to the method on each parent in the Person class, and not logic that digs into the details of a parent (without a good reason). If you are new to templates and this pattern is a bit confusing, ask someone about it in office hours.
10.2 Side Note: 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.
11 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:
If you need a new kind of data, create a class with a field for each attribute of that data.
If you need data that can have variants, create an interface. Make sure the class for each variant implements the interface.
Put all of your examples of data and test cases into a separate Examples class.
Java programs should not have conditionals that check the type of an object. Dispatching will do that automatically. When writing a method over variants, the class for each variant gets its own version of the method that is specific to that variant. This split into multiple methods applies to templates as well.
11.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.
Class names are in camel case
Field and identifier names are in mixed case
Method names start with verbs and are in mixed case
Predicate names are prefixed with is or has and are in mixed case
Interface names are in camel case preceded by a capital letter I
Constants are in all-caps with underscores to separate words