| |
Harvey Mudd College
Computer Science 60
Assignment 4, Due Wednesday, February 17, by 11:59 pm
Late-day ("Euro") reminder: If you use a CS 60 Euro, be sure to submit by 9:59 pm on Thursday, Feb. 18... .
Link to the CS 60 submission site
Back to the top-level CS 60 page
Conversational Java!
This homework highlights the similarities
and differences between the
computational styles of an object-oriented language (Java) and
a functional language (Scheme). First, you'll "get conversant"
with the syntax of Java by implementing a user-interaction loop,
though not one as sophisticated as the Unicalc API. The second
part of this assignment asks you to write two data structures - classes -
one that represents Complex-number data and one
that implements Scheme-like lists, called OpenList.
The Problems
Files
You'll probably want to develop your functions in a hw4 subdirectory
within your cs60 directory. There will be three files to submit this week. They are
- JavaInteraction.java -- a user-interaction loop that exercises
some of Java's syntax.
- Complex.java -- a Java implementation of a complex-number class
- OpenList.java -- a Java implementation of Scheme's fundamental list structure
You can download starter files for all of these from
the top-level CS 60 page.
Style
From this assignment onward, about 15-20% of credit will be awarded
based on your programs' commenting, formatting, and design --
under the heading of style.
The goal is to choose a style that is readable and well-commented. Here are some of the
things to look out for when writing programs for yourself (or others) to read. The
graders will use these guidelines, too:
- Design
- Use small, clear functions -- and use helper functions of your own design as
needed or desired. Large, convoluted functions are difficult to read and understand
-- more importantly, however, they're very difficult for the original programmer
to modify and debug!
- Formatting
- Please use ample whitespace: blank lines between functions,
spaces within expressions to make them readable. Whitespace
should keep code components visually separate.
- Whitespace -- especially indenting -- should make clear the
structure of your program. Even though it's not Python, it's a good
idea to use Python-type indenting.
- You should use meaningful variable names. One-letter variable
names are OK if they are a common idiom, e.g., f and R
for the first and rest of a list or i for an index variable.
But feel free to use longer names there, too.
- You should keep your lines of code to less than 80 characters --
this helps to make it readable from within all kinds of programs, and
avoids wrapping issues.
- Comments
- We ask you to include a top-of-file comment with your name, partner (if any),
time spent, and any other comments for the graders (or instructors)
- Each function, even your own helper functions, should have at least
a short top-of-function comment documenting its purpose, inputs, and outputs.
- Comments within functions are welcome, but not required unless
something tricky is going on. Many people find them helpful to write
because they keep the flow of the program in mind.
(1) JavaInteraction.java Pair or individual problem
[30 points, pair or individual]
For this problem, you should start with the JavaInteraction.java file
available from the top-level CS 60 page. It's a simplified version of
JavaIntro.java, an introduction to several Java constructs. Both
are linked from the top-level page.
First, look over the file JavaIntro.java, run it,
and get familiar with Java's syntax and,
more importantly, its "mindset." Variable types need to be declared before
use - and, optionally, they can be initialized at the same time. Variables' values
may change arbitrarily many times, but the declaration of the variable and its
type can happen only once in each scope.
Be sure you have a clear mental model
of what arrays are and how they are used. In addition, it's important to
remember the difference between String objects and primitive
data types, such as double, char, int, and boolean.
Remember that primitive data types are handled
as raw values; all other data types are objects and are handles
through references.
In JavaInteraction.java all of the code in the file except the
user-interaction loop has been deleted. There is also a start (not
a completed or correct version) to the isPal method at the bottom
of the file.
Your task for this problem is to add the following commentary to the
provided user interactions in JavaInteraction.java:
- note whether the user's input line is a palindrome or not, when spaces are considered
an important part of the input String
- note whether the user's input line is a palindrome or not, when spaces are NOT considered
an important part of the input String
- note whether the user's input line is evaluable. We will consider an input line
evaluable if all of the following conditions hold:
- it has a length of exactly 5 characters
- The first character (index 0) is a digit (from '0' to '9', inclusive)
- The second character (index 1) is a space ' '
- The third character (index 2) is one of '+' or '-' or '*' (an operator)
- The fourth character (index 3) is a space ' '
- The fifth character (index 4) is a digit (from '0' to '9', inclusive)
otherwise, the input String is considered not evaluable
- finally, if the input line is evaluable, compute and then report the
value of the input line, according to the usual arithmetic interpretation of such Strings...
You may write the "conversation" however you'd like -- creative dialog, especially
that flatters the CS 60 graders, is encouraged!
However, you do need to write at least the following four Java functions, or
methods.
Here, we provide the function signatures and a description of what they will need
to do:
- Write the method public static boolean isPal(String s)
which should return true if s is a palindrome, including
any spaces in the consideration of the String.
For example,
isPal( "!wow wow wow!" ) should return true, but
isPal( "madam im adam" ) should return false.
- Write the method public static boolean isPalNoSpace(String s)
which should return true if s is a palindrome, NOT considering
any space characters ' ' in the consideration of the String. Don't worry
about return characters, tab characters, or other whitespace characters at all.
For example,
isPalNoSpace( "!wow wow wow!" ) should return true and
isPalNoSpace( "madam im adam" ) should return true, but
isPalNoSpace( "42 is the answer!" ) should return false, despite its obvious truth!
- Write the method public static boolean canEval(String s)
which should return true if s is an evaluable
String according to the definition above.
For example,
canEval( "6 * 7" ) should return true, but
canEval( "Hello, World!" ) should return false.
- Write the method public static int eval(String s)
which should return the customary value of the arithmetic expression s.
You may assume that eval will only be called on evaluable Strings.
For example,
eval( "6 * 7" ) should return 42, and
eval( "6 - 7" ) should return -1.
The graders will test both your overall main method, along with
each function individually, so be sure to test them before submitting!
(2) Complex.java Pair or individual
[30 points, pair or individual]
Java has eight built-in (primitive) types -- the most common are
such as int, char, boolean,
and double. It has thousands of class types in
its libraries.
Yet one class type that Java does not have is
Complex, representing complex numbers commonly used by
mathematicians, scientists, engineers, and Math 11 students.
This problem asks you to implement a complex number data structure (class).
Complex.java.
Also, the Point class example we developed in class will
be a good reference. After all, 2d points and Complex numbers are
not so different! The complete Point class and a starter
file for your Complex class are available from the CS 60
home page.
Testing your code!
Just as in Scheme, you should test each of
your methods (functions) as you write it. To do this in Java, add lines to
your Complex class's main
method that test each of your methods.
For this assignment, we will check visually
that the answers are what they should be, so feel free
to do the same.
It's much easier to make small syntactic errors in Java
than in Scheme -- there is simply more code where errors can appear!
To help with this, the starter file provides lots of tests in
a comment in main.
As you implement your Java methods, uncomment the appropriate
tests and try them out!
Here is a copy of the starting point for your Complex class. Using this
will help us because it will standarize the names of the data members and
how your objects of type Complex are printed to the screen:
/*
* Complex.java
* a complex number class
*
* Name:
*
* Comments to the graders:
*/
class Complex
{
private double real;
private double imag;
// place your constructors here:
// *** Please use this toString method to keep things consistent ***
// method: toString
// in: no inputs
// out: the String version of this Complex object
public String toString()
{
String sign = "+"; // default sign is plus
if (this.imag < 0.0)
sign = "-"; //
return "" + this.real + " " + sign + " " + Math.abs( this.imag ) + "i";
}
// place your other methods here:
// main: the starting point for our testing code
// in: command-line arguments
// out: nothing (it's void)
public static void main(String[] args)
{
// a welcoming statement...
System.out.println("Welcome to the Complex...");
// feel free to uncomment these lines of code
// to test as you go!
/*
Complex c0 = new Complex( 0.0, 0.0 );
System.out.println("c0 should be 0.0 + 0.0i");
System.out.println("c0 is " + c0);
Complex c1 = new Complex( 42.0, -60.0 );
System.out.println("c1 should be 42.0 - 60.0i");
System.out.println("c1 is " + c1);
*/
// you'll want to write more tests so that you
// know your other functions are working...
// Do not worry about the small floating-point
// differences that necessarily arise when using
// arithmetic on real hardware.
// For instance, 2.4999999999999 is OK for 2.5
// a departing statement...
System.out.println("Java: where the Complex isn't!");
}
}
Notes: Recall that a complex number is of the form a + bi
where a and b are real numbers and i
is an imaginary number. Addition of two complex numbers is defined
by (a + bi) + (c + di) = (a+c) + (b+d)i. Similarly,
to multiply two complex numbers, we simply compute (a + bi) *
(c + di) = ac + adi + bci + bdii = (ac - bd) + (ad + bc) i
since ii (i times i) is -1 by definition.
In your implementation, the numbers a and b in
the complex number a + bi should be represented by doubles:
these will be the two data members of your class. Please use
the names real and imag for these data members --
this will be far more readable/memorable than a and b!
Below are the public methods (functions) which your
Complex class should contain.
Be sure to use exactly these names and the specified kinds of arguments
and return values. You may use any additional private methods
that you'd like to use in order to build these public methods, though
such helper functions are not required.
- The class should have a constructor that takes two double
arguments and sets the real and imaginary components of the complex
number to these values.
Here is the method signature (feel free to
copy and paste:
public Complex( double real_in, double imag_in )
- The class should have a second constructor that takes no arguments
and simply sets the real and imaginary components of the complex
number to both be 0.0.
Here is the method signature:
public Complex( )
- Please use this code...
The class should have a toString() method that
converts complex numbers into strings for the sake of printing.
We have provided this method (below and in the code above) -- please use that,
so that the style for complex-number printing is consistent!
Here is the code again, for reference:
// method: toString
// in: no inputs
// out: the String version of this Complex object
public String toString()
{
String sign = "+"; // default sign is plus
if (this.imag < 0.0)
sign = "-"; //
return "" + this.real + " " + sign + " " + Math.abs( this.imag ) + "i";
}
- The class should have a method named equals that
takes a Complex argument and determines if the two Complex
numbers (this one and the argument) are structurally equal, i.e.,
whether they are the same complex number. The method
should return a boolean which is true if the numbers are
equal and false otherwise - for the purposes of this problem
use == to compare the real and imaginary parts.
Here is the method signature:
public boolean equals( Complex c )
- The class should have a method named add
that takes a Complex argument and returns the sum
of this Complex number and the argument. Thus, the
return value is a Complex number itself. You should
be sure that neither of the summands have their
values changed in any way.
Here is the method signature:
public Complex add( Complex c )
- The class should have a method called negate
that takes no arguments and returns the negative
of this Complex number. Thus, the
return value is a Complex number itself, but the
invoking object has not been changed.
Here is the method signature:
public Complex negate( )
- The class should have a method called conjugate
that takes no arguments and returns the conjugate
of this Complex number. The conjugate of
the number a + bi is a - bi. Thus, the
return value is a Complex number itself, but the
invoking object has not been changed. Notice that this is
slightly different than negation.
Here is the method signature:
public Complex conjugate( )
- The class should have a method called multiply that
takes a Complex argument and returns the product of
this Complex number and the argument.
Thus the return value is a Complex number itself and neither of
the multiplicands have their values changed in any way.
Here is the method signature:
public Complex multiply( Complex c )
- The class should have a method called divide that
takes a Complex argument and returns the product of
this Complex number and the argument. You may
assume that the second input is non-zero. Thus, the return value is a Complex number itself and neither of
the multiplicands have their values changed in any way.
To divide one complex number by another, first multiply both
the numerator and denominator by the conjugate of the denominator. That
ensures that the denominator is a real value! Then, you can simply
divide both the real and imaginary components of the numerator by that value.
A website that explains this in more detail (and offers a calculator
that divides complex numbers is
available here).
Here is the method signature:
public Complex divide( Complex c )
Again, be sure to test out your Complex class before submitting Complex.java.
(3) OpenList.java Pair or individual problem
[40 points, pair or individual]
For the third problem you will create an OpenList data
structure, to be implemented in Java. An "open list" is a
recursively-defined linked list
of cons cells whose data and strucure are not modified so that data-sharing
is encouraged and safe.
Instead of modifying data, each function must return newly created
OpenList -- or whatever other type of data is appropriate.
Thus, this problem introduces the implementation of functional-style data structures
in Java. In addition, it is a chance to either extend your introducton or your review of
what Java looks like and how java code is structured.
We will talk in class about the OpenList class, as well as some
of the details that are provided in the starter file, OpenList.java.
Write your OpenList class in a file called OpenList.java.
You should start with the OpenList.java file available
at the top-level CS 60 page. In it, the constructor, the isEmpty,
the toString, and the assoc methods are written
as examples and for printing consistency.
The provided main method of this OpenList.java
contains a couple of tests. Compile and run
the file before adding code. Read it over
in order to get a sense of what it is doing and how it is
organized. There are many more tests available for copy-and-paste
from the MainFromCompletedOpenList.java file at the CS 60
toplevel page.
Writing the OpenList class
First, in your OpenList class, you will want
to indicate the data that constitute a list. There are two
non-static data members (first and rest)
and one static data member (emptyList) in an OpenList:
// two private data members
private Object first;
private OpenList rest;
// one public data member: the emptyList is static (owned by the class)
public static OpenList emptyList = new OpenList(null, null);
These are the only data members you will need in
your list implementation -- and they're already there for you.
You may use other data members, if you
would like -- in fact, there is considerable leeway
in how you want to implement OpenList. However,
it does have to have all of the methods indicated below, and
they need to behave as specified!
Notice that because the data member named first is
of type Object, this OpenList implementation
will be able to create lists of any Java Objects. Everything
in Java is an Object except these types:
int
double
boolean
char
float
byte
long
short
We will use only objects of type String and of type
OpenList for this assignment. The next problem, on the
other hand, will put your OpenList class to work... .
In your OpenList class, be sure to implement the following methods. Some
are in the file already; for those, read them over as they provide a useful guide to the others:
- OpenList, the CONSTRUCTOR
- This method is provided, and it will be covered in detail in class in order to
motivate the use and syntax of constructors. Constructors
are always named for the type of the objects they construct.
- public OpenList(Object f, OpenList R) is a
constructor that returns an OpenList with f
as its first element and R as its rest.
- toString
- In fact, do not write this method -- instead,
merely use the one provided in the starter file OpenList.java
linked at the assignments page.
Why do we ask you not to write this?
Because this method is how we will test all
of your other methods -- and we need a consistent
way of displaying OpenLists to do that
testing. You will
need the helper method private String printItems(),
as well. It is also provided.
This toString method is a
non-static method
that returns a String representation of the
invoking list. A static version isn't needed.
The toString method is used internally by Java whenever your code adds a String
to an OpenList, e.g., in code like this:
OpenList L = new OpenList( "Hi!", emptyList ); // an example of using the constructor
System.out.println("List L is " + L);
The above code should output
List L is [ Hi! ]
- cons
- public static OpenList cons(Object f, OpenList R) is a
static list-constructing method that takes an Object f
and an OpenList R. Then cons
returns an OpenList in which f is the first
element and R is the rest of the list. It simply
delegates to the constructor!
- public OpenList cons(Object f) is a non-static
list-constructor method that does the same thing, but
uses the invoking OpenList as the "rest" of the
result. If you're already familiar with C++ or Java
data structures,
this method might seem a bit odd: it does not change the
invoking list at all! Instead, it returns a new list that
uses the invoking OpenList as its rest. Notice that
this is a very Scheme-style approach to handling data.
- isEmpty
- public static boolean isEmpty(OpenList L) is a
static predicate returning true if the input list
is the empty list and false otherwise. In Java, 0
and 1 are not boolean values; true and
false are.
- public boolean isEmpty() is a non-static
predicate that returns true if the invoking list
is empty and false otherwise.
- assoc
- public static OpenList assoc(Object o, OpenList DB) and
public OpenList assoc(Object o) are both already written
in the OpenList.java starter file. Read those over as
an example of how OpenLists can be used.
- public OpenList assoc(Object o) is a non-static
version of assoc. It is also already present in the file.
- first
- public static Object first(OpenList L) which is a
static method that returns the first object in
the input list. If the input list is empty, the method
should print an error message and then exit completely.
To exit a Java program
immediately, use the line
System.exit(0);
Compiler complaints about return not being present?
Watch out -- if the compiler sees
if (this.isEmpty()) {
System.exit(0);
}
else {
return this.first
}
it will complain, saying that the method needs to return an Object
in all paths through the code. The compiler doesn't realize that System.exit(0)
is as final as it is. So, you'll need to include a dummy return null in the
if block or get rid of the else keyword entirely.
That said, we won't test this method on non-empty lists!
Note: the basic idea here is simply to return L.first!
- public Object first() which is a non-static
method that returns the first element of
the invoking list. If the invoking list is
empty, the method should print out an error message
and exit.
- rest
- public static OpenList rest(OpenList L) is a
static method that returns the rest of
the input list. If the input list is empty,
your function should print out an error and exit.
- public OpenList rest() is a non-static
method that returns the rest of
the invoking list. If the invoking list is
empty, your function should print out an error and exit.
- length
- public static int length(OpenList L) is a
static length method that returns the length of
the input list.
- public int length() is a non-static
length method that returns the length of
the invoking list.
- list
- public static OpenList list(Object o1) is a
static method that returns an OpenList of one element, o1.
Note that there is no non-static version of the
list method, because there might not be a list around
to invoke it!
- public static OpenList list(Object o1, Object o2) is a
static method that returns an OpenList of two elements (in
the order they're input).
Again, there's no non-static version.
- public static OpenList list(Object o1, Object o2, Object o3)
is a
static method that returns an OpenList of three elements (in
the order they're input).
Again, there's no non-static version.
- append
- public static OpenList append(OpenList L, OpenList M) is a
static append method that takes two OpenLists and
returns an OpenList that is their concatenation.
- public OpenList append(OpenList M) is a non-static
append method that takes an OpenList as input and
returns an OpenList that is the concatenation of the invoking
list and M. Thus, the invoking list "goes first" in the result of append.
Again, the invoking list is not changed --
because this OpenList data structure is an open list, it does not
allow existing data to be changed, only new data to be created.
- reverse
- public static OpenList reverse(OpenList L) is a
static method that returns the reverse of the input list.
(Remember - this does not actually change the input argument
L.) Hint: use append.
- public OpenList reverse() is a non-static
method that returns the reverse of the invoking list. Again,
as with all of the methods in this class, the invoking
list is not changed.
- merge
- public static OpenList merge(OpenList L, OpenList M), which is a
static method that takes in two OpenLists, both of which you should
assume are lists of lower-case-only Strings in sorted
order from early-to-late in the alphabet. Then, merge should
use the merge technique discussed in class to return a list of all of
the elements in both of the input lists, but still fully in order. Thus,
the result is like appending, but the sorted order is preserved.
As a brief review, merging is the process of looking
at the first two elements in both lists and deciding on what to do with one of them.
In the end, each element gets handled once, which
is faster than calling
any general-purpose sorting routine!
You will want to cast the elements to
type String and then use String objects' compareTo
method. Here's an example of how compareTo works:
String m1 = "mudd";
String m2 = "mit";
if ( m2.compareTo(m1) < 0 ) ... // this test will evaluate to true
Basically, if compareTo is less than zero, then the invoking string is
before the input-argument string in terms of dictionary order.
Warning! Java is picky about types!
This is no surprise, but
note that compareTo needs to be called by a
String
with a String as input.
If L is an OpenList, however,
Java thinks that L.first or the result from L.first()
is only of type Object, as stated at the top of the
OpenList class, but not necessarily of type
String!
At times like this, the programmer (you) know that data is of
a certain type (a String in this case), but Java does not.
The mechanism for "coaxing" Java to recognize data as a certain type is
called casting and looks like this:
String firstOfL = (String)L.first(); // we cast the Object returned by first()
String firstOfM = (String)M.first(); // into the String _we_ know each one to be...
Thus, you'll want to use lines like these two in order to
grab the two strings you'll need to implement the merge
function.
- public OpenList merge(OpenList M) which is a non-static
method that takes an OpenList as input and
does the same thing as the static merge, using
this as the other OpenList to be merged.
Again, the two lists, this
and M will contain zero or more strings in sorted order.
Totally gratuitous but cool extra credit: anylist, mergesort, and duperreverse
worth up to +15 points, may be done in pairs or individually
These problems ask you to write additional methods in your OpenList.java
file (that is, your OpenList class). You don't need to submit any other files.
Note, however, that these won't be part of next week's assignment (as has been the case in the past).
- Optional Extra credit of up to +5 points anylist
Write public static OpenList anylist(Object ... elements), which should be
a function of any number of arguments that does essentially the same
work as the list methods, i.e., create an OpenList out of those
input Objects.
The trick here will be to find how Java handles arbitrarily many
inputs. I used this reference
http://today.java.net/pub/a/today/2004/04/19/varargs.html
.
Note that instead of ints, you will be using
Objects.
You don't need to write a non-static version.
- Optional Extra credit of up to +5 points mergesort
Write public static OpenList mergesort(OpenList L), which is a
static method that takes in one OpenList (which will contain ONLY Strings)
and then mergesort should return a sorted OpenList with the same
elements that L contained.
How mergesort works
Mergesort delegates almost all of the work of sorting the input list to the merge
subroutine. It first handles its base cases: when the input list
has 0 or 1 element, it's done, because such a list is already sorted!
On the other hand, if the input list has more than 1 element,
the idea is to first split that input list
into two lists that are within one element of being the same size. After obtaining
two lists from that split, a recursive call to mergesort is made on
each piece.
After this recursive call, both sublists are sorted! Then, to complete the
sorting of the original list, the merge routine is used, since merge
works on already-sorted lists.
I always find it remarkable that this works!
No non-static version is required, but you may certainly
(as always) create small, helper functions of whatever type you like - for example, our
solution used a method named split, which returned an array of two OpenLists.
You can create an array as follows:
// this creates an array of two OpenList references.
// both are initially null!
OpenList[] ourOpenListArray = new OpenList[2];
// at this point, you have
// ourOpenListArray[0] == null and
// ourOpenListArray[1] == null
// but they are both ready to be assigned to other (real!) OpenLists...
It works equally well to write split to returns an object of your own newly-defined class type that holds
the two resulting OpenLists.
- Optional Extra credit of up to +5 points
duperreverse in Java!
Write public OpenList duperreverse(), which is a
non-static method that takes no inputs and should return a list that is the
fully-recursive-reverse of the invoking OpenList, this. This
returned list should have all of its structure reversed relative to the
original list that called duperreverse. that original list should not
change at all.
Here is an example that uses the notation of printed OpenLists, with the
quotes added to emphasize that the elements are Strings:
If the OpenList L printed as
[ "this", ["is", "an", ["open"], "list"] ]
then the list returned by duperreverse( L ) should print as
[ ["list", ["open"], "an", "is"], "this" ]
Notice that even sublists (but not the elements themselves!) are reversed.
You will have to
use the instanceof operator in Java to determine what class-type a particular element is,
in order to know whether or not to continue. For example,
Object s = new String("I'm a string.");
if ( s instanceof String ) ... // this test will evaluate to true
if ( s instanceof OpenList ) ... // this test will evaluate to false
No static version is required. Remember that Java's compiler checks if an instanceof
expression can possibly be true - the code won't compile if it can't! In this case,
s is of type Object, which could be a String or a OpenList.
The compiler doesn't actually read the code, it just checks the types... .
Be sure to submit your three .java files at the submission site under week 4... !
|