Question #6.01 After reading through this lab
(which you did to answer the prelab questions), how long do you
estimate it will take you? Be honest with your answer, and do not
worry if it's nowhere close.
To be answered in Exercise
Questions/lab06.txt
.
Question #6.02 Record how long it takes you to
actually complete this lab. Use this question (in the answer file)
to keep track of your start and stop times. Be sure to subtract out
your minesweeper breaks.
To be answered in Exercise
Questions/lab06.txt
.
Most of the problems in the previous lab exercises had
relatively simple solutions because the data objects in the problem
could be represented using the predefined Java types. We can
represent a volume with a double
, a name with a
String
, a vowel with a char
, and so
on.
However, nearly all real-world problems involve data objects that cannot be directly represented using the predefined Java types.
For example, suppose that you know a certain gourmet chef named Pierre whose recipes are for 12 servings. Of course, it's rather rare that exactly 12 people come in and order exactly the same thing, so Pierre frequently must scale his recipes for the number of people who do come in (e.g., 1 customer gets 1/12th of a recipe, 2 customers get 1/6th of a recipe, 15 customers get 5/4ths of a recipe, etc.). The recipes use fractions for some ingredients (e.g., 1/2 tsp. salt, 3/4 cup flour, etc.) so that he must multiply fractions to scale a recipe (e.g., 5/4ths of 1/2 tsp.).
Pierre comes and asks you to write program to help in the scaling.
My Problem: Two-Dimensional Coordinates
Throughout this lab, I will also work on another example in complete detail. This problem is to represent two-dimensional coordinates, (x, y).
There are a lot of similarities in the implementation of the solutions for fractions and for coordinates, but there are significant differences as well. So in writing your own code, look at the generic code patterns and my specific solution for coordinates. Then build an answer for your fraction solution.
Do this...
edu.institution
.username
.hotj.lab06
lab06.txt
in your Exercise
Questions
folder.Fraction
. This is where the fraction objects
will be described.FractionTest
.FractionInput
.PierreCLIDriver
. Much of the code in this file is
commented out; you'll remove the comments as you go through the lab
exercise.In Java, a new data type can be created with a class:
class-declaration pattern |
public class
|
Helpful hint: You must not use the
static
keyword at all in this lab. If you can "fix"
your code with static
, don't! Make sure there are no
static
s in your code, and find another solution.
You've used classes in the past to store away
static
methods, but this is the lab when you put
static
methods behind you. You're going to use classes
the way they were intended: as blueprints for new objects.
All of the reference data types that you've already used---like
Keyboard
, Screen
, and
String
---come from a class. In fact, as far as Java is
concerned, the classes you define are just as good as
these other classes no matter where they come from.
An object consists of two things:
For example, the Screen
must have a connection to
the screen itself. This connection is data. Somehow a
Screen
object must keep track of this data. A
Screen
object also has actions we perform on it, like
print()
and printFormatted()
. Similarly,
a String
is a string of characters, a bunch
of things; and a String
has a whole variety of
operations we can invoke on it.
An attribute of an object is a data item necessary to represent the object. These attributes are coded up as variables in our class, but in a slightly different way than the variables you've seen before.
Let's consider my two-dimensional-coordinates example. Such coordinates comes in this form: (x, y); e.g., (2, 3), (3,14, 2.8), etc.
Hence, a two-dimensional coordinate has two attributes: an x-coordinate and a y-coordinate, both real numbers. I must keep track of both the x- and y-coordinates, otherwise I'm not storing a coordinate.
An attribute is implemented as an instance variable in a class:
public class Coordinate { /** * The x-coordinate. */ private double myX; /** * The y-coordinate. */ private double myY; }You should put a Javadoc comment in front of each instance variable; it does not have to be elaborate.
Instance variables are a little different than other variables. By convention, all of the attributes of an object are grouped together at the beginning of the Java class (not inside a method).
The pattern is mostly the same as any other variable declaration:
instance-variable pattern |
private
|
The basic pattern of "data type followed by identifier" is still there, but we typically do not initialize instance variables. You can initialize an instance variable (with the same syntax as for a local variable), but generally instance variables are initialized in a constructor which we'll get to in the next section of this lab.
One significant difference is the location of the declaration of the instance variables. The declaration is in the class, not inside a method. So you now know about three kinds of variables:
class
(but not in a
method) is an instance variable.Another difference between an instance-variable declaration and
other variable declarations is the keyword private
.
The keyword private
describes the
visibility of the instance variable; in other
words, "who else can see this variable?" Private information is
kept just to yourself, and that's exactly what private
visibility means for an instance variable.
In contrast, you declare your classes and methods as
public
for public visibility: these
classes and methods are open to the public; anyone can access
them.
As noted in the pattern, our convention for naming instance
variables will be the same as for all variable identifiers except
we'll add a "my
" prefix. This will be our
clue that these variables are instance variables. (This prefix
means nothing to the compiler, although your IDE may make use of
them.) Read the "my" as personal ownership, because we'll be using
internal perspective to write our OCDs, and when
we do this later in this lab exercise, you'll see how the "my"
prefix is beneficial.
Fraction
ClassNow let's turn to your fraction problem. First you need to identify the attributes of a fraction. Every fraction has this form:
numerator
/denominator
For example, 1/2, 5/4, 37/1002, etc. Both the numerator and denominator are integers.
Only the numerator and denominator are the attributes of a
fraction. The slash /
isn't an attribute
because every fraction has one. The attributes of a class
are only the objects needed to uniquely represent each
unique instance.
Do this...
Define two integer instance variables named
myNumerator
and
myDenominator
in the Fraction
class. Write Javadoc comments
for both variables.
Helpful hint: Use the Coordinate
example to write your Fraction
code. However, never
ever forget that these two problems are very different (as well as
being quite similar). Note how their forms are different;
note how their attributes are different.
Consider the declarations of oldMeasure
and
scaleFactor
in
PierreCLIDriver#main(String[])
. When
Fraction
objects are created for them, they can be
pictured like this:
Each instance of Fraction
has its own copy
of each of the attributes we defined.
A class also has methods which provide operations for the instances of a class. These methods are different from the static methods we have seen before because they will work on the attributes of an instance.
The instance variables of a class are kept private so that a program using the class is not permitted to access them directly. This is a good idea because a program that directly accesses the instance variables of a class becomes dependent on those particular instance variables. If those instance variables are changed (which is fairly common), then these other programs must also be changed, increasing the cost of software maintenance. Also, we may want to assume certain things about our instance variables, and if everyone has access to them, they can do bad things to our data.
On the other hand, we want users of the class to be able to
perform operations on the instances. As a result, methods should
usually be declared as public
.
You've already seen how to declare class methods; the
only change in writing instance methods is that we drop
the word static
. Here's the pattern:
instance-method-declaration pattern |
public
|
It's hard to see something that's been omitted, but note that
the word static
never appears here. If you flip back
to Lab #4, you'll see that's the only change to the method
declaration compared to a class method declaration.
But that one change makes all the difference. Instead of being
invoked on the class, an instance method is invoked on instances.
For example, suppose two String
objects named
greeting
and closing
are defined like
so:
String greeting = "hello", closing = "goodbye";
The String
class has an instance method
named length()
that has no parameters. Since it's an
instance method, this code is invalid:
String.length()
The compiler will complain that you're trying to invoke an instance method in a "static context" (or something along those lines). Think about it: you want the length of what string? You have to specify the exact string whose length you want.
Instance method are a way to ask a specific instance like so:
greeting.length()
and
closing.length()
The first expression evaluates to 5 and the second to 7.
The general pattern for invoking an instance method is this:
instance-method-invocation pattern |
|
Object-oriented programmers like to think of calling a method as
sending a message to an object. When we send the
length()
message to greeting
,
greeting
responds with the number of characters it
contains; when we send the same length()
message to
closing
, closing
responds with the number
of characters it contains.
Notice how the focus is on the objects. We ask the
greeting
and closing
objects for their
lengths. We do not ask the String
class. This
is because greeting
knows what characters it has
stored away and it knows its length. The String
class
doesn't know or even care.
To facilitate debugging a class, it is often a good idea to begin with a method that can be used to display class objects.
Consider the
Coordinate
class again. I could write aprint()
method that prints the coordinate to aScreen
, but this would mean that anyone using myCoordinate
class will also need theScreen
class. It'd be better if I could just send aCoordinate
object as a parameter to aprint()
method like so:theScreen.println("The coordinates are: " + coord);There's a similar line in your driver.
Java allows us to do this fairly simply. The key here is the
+
operator. When applied to String
s, it's
the concatenation operator. The problem is that coord
and newMeasure
are Coordinate
and
Fraction
objects, respectively, not
String
s.
Since this is such a common problem, there's a built-in solution
in Java: if an operand to +
is not a
String
, Java turns it into a String
automatically as long as we define a toString()
method for our class. Java will take care of the rest. (Actually,
toString()
has default definitions in
Coordinate
and Fraction
, but they're not
particularly useful.)
Java specifies the toString()
specification like
so:
Specification:
receive: nothing.
return: aString
representation of the object.
Coordinate#toString()
should return aString
representation of theCoordinate
instance. The specification doesn't tell the whole story, though. Let's consider the behavior ofCoordinate#toString()
.Before we get to this behavior, remember the internal perspective mentioned above. When specifying the behavior of an instance method, use the first person to describe the behavior. Imagine yourself as an instance of the object; even state your identity at the beginning of the paragraph:
Behavior of Coordinate#toString()
I am a
Coordinate
object. When I receive a message to return myString
representation, I will concatenate theseString
s:"("
, my x-coordinate, a","
, my y-coordinate, and a")"
.Because I use an internal perspective, my attributes pop out of the behavior paragraph. It's very natural for me to talk about "my x-coordinate". Where might I find this value? In
myX
, of course!
Objects of Coordinate#toString
Description Type Kind Movement Name literal strings String
literals local "("
,","
,")"
my x coordinate double
instance variable instance myX
my y coordinate double
instance variable instance myY
concatenated string String
varying out result
I don't put an object entry in the objects table for "I" or "me". The members of the "me" object that are needed will be explicitly mentioned in the behavior paragraph (like "my x-coordinate" and "my y-coordinate"), and those should be included in the table.
Compare the object table to the Java-mandated specification above.
Operations of Coordinate#toString()
Description Predefined? Name Library concatenate strings yes +
built-in return the concatenation yes return
built-in The algorithm is rather simple:
Algorithm of Coordinate#toString()
- Let
result
be the concatenation of"("
,myX
,","
,myY
,")"
.- Return
result
.As code, it looks a lot like code we've written before:
public String toString() { String result = "(" + myX + "," + myY + ")"; return result; }The only difference from previous methods is that the instance variable are used without a local declaration. The instance variables are declared once in the class, never again.
This method goes in the
Coordinate
class, after the instance variables.
One of the most important things to note about an instance
method is that I am not declared (using the language of an
internal perspective). In more proper object-oriented terms, the
instance is not declared. We get the instance and its instance
variables for free. A Coordinate
instance has a
myX
and a myY
that it gets for free
in this method. These instance variables are accessed
directly, without any other declaration.
This point cannot be emphasized too much because you
will forget this and try to redeclare the instance
variables. (Everyone does.) Look at the
Coordinate#toString()
method again and note that
myX
and myY
are never explicitly
declared in that method. We get them for free in the method since
they've been declared as instance variables in the same class.
We do have to declare the variables, but only once as instance variables at the beginning of the class.
Question #6.03 Write a good behavior paragraph
for Fraction#toString()
. Keep in mind that the form
(i.e., the punctuation) of a coordinate looks different than that
of a fraction.
To be answered in Exercise
Questions/lab06.txt
.
Do this...
Declare a toString()
method for your
Fraction
class.
Before we can run or test anything, though, we have to first be able to construct instances.
Creating and initializing an object is known as constructing an object (or instance). Java allows us to define a special method called a constructor that specifies exactly what to do when an instance is created. We can use the constructor to initialize the instance variables of the instance.
A constructor does not return anything to its caller;
it cannot return anything. It initializes the
attributes of an object when that object is created. It's a subtle
distinction, but think of new
as creating a new
instance and returning the new instance, but returning it only
after the constructor initializes it. This is very
important in the syntax for a constructor.
The simplest constructor is the default constructor. A default constructor does not have any arguments or parameters.
To call the default constructor for my
Coordinate
class, I would write this:Coordinate coord = new Coordinate();Defining a constructor is just a little different from a method. The design is similar (i.e., OCD), but the responsibility is much more focused that with a normal method.
Behavior of Coordinate#Coordinate()
I am a
Coordinate
object. My default initialization will set my x-coordinate to 0.0 and my y-coordinate to 0.0.
Objects of Coordinate#Coordinate()
Description Type Kind Movement Name my x-coordinate double
instance variable instance myX
my y-coordinate double
instance variable instance myY
zero double
literal local 0.0
Operations of Coordinate#Coordinate()
Description Predefined? Name Library set a variable yes assignment expression built-in Since all of the action of a constructor is internal---nothing is returned, nothing is printed---the specification of a constructor describes the constructor's postcondition. That is, describe what should be true when the constructor is finished. It does not give the actual computation code, but the computation code is usually strongly implied.
Specification of
Coordinate#Coordinate()
:postcondition:
myX
== 0.0 andmyY
== 0.0.
So we implement a default constructor with public access in the class
Coordinate
like so:public Coordinate() { myX = 0.0; myY = 0.0; }Just like an instance method, a constructor has free access to the class's instance variables.
Constructors are commonly placed after the instance variables and before the instance methods.
Here's the general pattern for a constructor declaration:
constructor-declaration pattern |
public
|
Do this...
Using this information, define a default constructor for your
Fraction
class, that satisfies the following
specification:
Specification:
postcondition: myNumerator == 0 and myDenominator == 1.
Since division by zero is undefined, do not set
myDenominator
to be zero. Any
non-zero number would be okay, but 1 is pretty standard.
Do this...
Write a Javadoc for the
constructor (just like a method); the postcondition actually gives
you a perfect description of the constructor for the comment; just
write it in English, not mathematically. Then compile your code.
toString()
Once your code compiles fine, we can start testing both the
default constructor and the toString()
method.
Consider the
Coordinate
class. We could test this in a JUnit test case namedCoordinateTest
.Now that we're working with our own instances of our own classes, we can actually use instance variables in our test-case classes. So the first thing I do in my
CoordinateTest
class is to declare threeCoordinate
instance variables:private Coordinate myCoordinate1; private Coordinate myCoordinate2; private Coordinate myCoordinate3;The convention for JUnit is that we use a method named
setUp()
to initialize the instance variables (rather than using a constructor). This is so thatsetUp()
can be called before every test-case method.So I define this method like so in my
CoordinateTest
class:@Override public void setUp() { myCoordinate1 = new Coordinate(); myCoordinate2 = null; myCoordinate3 = null; }Capitalization matters. If you define
setup()
rather thansetUp()
, your tests won't work right. The@Override
should help; it should make sure that you're writing the right version ofsetUp()
.I'll return later and initialize
myCoordinate2
andmyCoordinate3
with a useful and interesting values later.Now I can test:
public void testToString() { assertEquals("string version of origin", "(0.0,0.0)", myCoordinate1.toString()); }This test method itself is mostly the same as the other test methods we've written. The main difference now is working from an instance (
myCoordinate1
), not a class (Coordinate
).This test answers this question: when I construct a
Coordinate
using the default constructor, what should itstoString()
return? Looking at the definitions, it's clear that the default constructor will construct aCoordinate
object whosetoString()
will return"(0,0)"
.
The setUp()
method should look something like
this:
setup-method pattern |
@Override
public void setUp() {
|
The @Override
will generate an error if you
misspell or miscapitalize "setUp
". If will also
generate an error if you forget to create a true test-case
class.
Do this...
Create three instance variables, a suitable setUp()
method, and a testToString()
method in the
FractionTest
class. Add a Javadoc comment to each of these
things. Then compile and run the test-case class for a green
bar.
Make sure your testToString()
really tests the way
a Fraction
is supposed to look: 0/1
.
Don't get distracted by the extra punctuation used for a
Coordinate
.
At this point I have the ability to create objects for the coordinate (0,0), and you can create objects for the fraction 0/1. These objects aren't going to get us far in life. We need to be able to specify other values for the x- and y-coordinates and for the numerator and denominator.
As mentioned above, objects are created by new
and
initialized with a constructor. Presumably, we need another
constructor.
Behavior of Coordinate#Coordinate(double,double)
I am a
Coordinate
object. To explicitly construct me, I will receive x- and y-coordinates. I will use these values to initialize my own x- and y-coordinates.Values are passed around as arguments and parameters. When I receive an x-coordinate, it is not mine yet! I receive it generally (e.g.,
x
, and if it's okay, then I'll use it to initialize my x-coordinate.
Objects Description Type Kind Movement Name an x-coordinate double
varying in x
a y-coordinate double
varying in y
my x-coordinate double
instance variable instance myX
my y-coordinate double
instance variable instance myY
The operations are "receiving" and "setting"---parameters and assignment expressions, respectively.
Algorithm of Coordinate#Coordinate(double,double)
- Receive
x
andy
.- Set
myX
equal tox
.- Set
myY
equal toy
.My specification indicates, again, what I expect to be true after the constructor executes:
Specification:
receive:
x
andy
, twodouble
values.
postcondition:myX
==x
andmyY
==y
.
However, a constructor's name is the same name as the class. We
already have one constructor named Coordinate
for the
Coordinate
class, how can we have another?
Fortunately, Java allows us to reuse a method name like this as
many times as we want so long as the number or
types of the parameters differ. Defining the same method
(or constructor) multiple times is called
overloading the method (or constructor).
The two constructors for the
Coordinate
class are different because the default constructor has no parameters while this new one has two parameters:public Coordinate(double x, double y) { myX = x; myY = y; }
Any constructor with one or more parameters is known as an explicit-value constructor because it takes explicit values to initialize the instance.
Now I can create any
Coordinate
I want:myCoordinate2 = new Coordinate(1.2, 3.4); myCoordinate2 = new Coordinate(9.8, 7.6);
Do this...
Define a second Fraction
constructor that satisfies
this specification:
Specification:
receive:
numerator
anddenominator
, two integers.
precondition:denominator
!= 0.
postcondition:myNumerator
==numerator
andmyDenominiator
==denominator
.
The precondition should be checked like so at the very beginning of your constructor:
if (denominator == 0) throw new IllegalArgumentException("Denominator must be non-zero.");
Be sure to test the denominator
parameter
and not the instance variable. The instance variable will
always be 0, and so the test would always fail! The purpose of
this test is to make sure that the canditiate denominator is
acceptable. If it's zero, it's unacceptable, and we'll never
initialize myDenomiator
to that
candidate.
This precondition for the constructor is also an
invariant for the class. An invariant is a boolean
condition that should be true for all instances (i.e., before and
after each method is executed). For all Fraction
objects, the denominator must be non-zero. We generally enforce an
invariant as a precondition for a constructor which then re-assures
us that it's true for all instances.
toString()
We've already tested toString()
with the default
constructor, but maybe we just got lucky there. We should also test
it with our new explicit-value constructor.
This is pretty straightforward: whatever values we use to
construct a Coordinate
, we should expect those same
values to be put into the String
returned by
toString()
.
First I set my other
Coordinate
instances to more interesting values inCoordinateTest#setUp()
:myCoordinate2 = new Coordinate(1.2, 3.4); myCoordinate3 = new Coordinate(9.8, 7.6);In
testToString()
, I write a two more assertions:assertEquals("(1.2,3.4)", myCoordinate2.toString()); assertEquals("(9.8,7.6)", myCoordinate3.toString());
Do this...
Make similar changes and additions to your
FractionTest
class. You should have five
instance variables in all: the first one uses the default
constructor (as you already have it); use the following fractions
for the other four instance variables: 1/2, 3/4, -2/3, 10/7. Compile and run for a green bar.
The values picked here are not completely arbitrary. To keep yourself on track with this lab exercise, you should use exactly these values.
Unlike my Coordinate
instances, you do
have a restriction on your Fraction
instances: their
denominators cannot be zero. You need to test to see if this is
being enforced by the new constructor.
Here's the whole test method you need for the
FractionTest
class:
public void testFailedInstances() { try { Fraction fraction = new Fraction(1, 0); fail("constructed a fraction with zero denominator"); } catch (IllegalArgumentException e) { } }
We saw this technique before in testing preconditions. Remember
that we expect the call to the constructor to fail which
generates an exception that skips over the call to
fail()
. It's only when the constructor fails to fail
that fail()
will be executed. (Say that ten times
really fast!)
You already have the precondition check in the constructor, so this should pass right away without any other changes.
Accessors are methods that retrieve an attribute of the object. These are often simple one line methods.
For example, it might be useful to be able to extract the x- and y-coordinates of a
Coordinate
object. These tasks have simple specifications and definitions that can be done in parallel here:
x-coordinate accessor y-coordinate accessor Specification:return: my X-value.
Specification:return: my Y-value.
public double getX() { return myX; } public double getY() { return myY; }
Accessors must go in the class (as all methods do); conventionally they are put in after the constructors.
Conventionally, accessor methods begin with a get
prefix to indicate that we're getting a value from the instance,
often just the value in an instance variable. This is in contrast
to setting the value of an instance variable or to computing a
value from the instance variables. (Incidentally, this
get
prefix is pretty much universally held; the
my
prefix on instance variables is less common.)
I test my new accessors:
public void testGetX() { assertEquals(0, myCoordinate1.getX(), 1e-8); assertEquals(1.2, myCoordinate2.getX(), 1e-8); assertEquals(9.8, myCoordinate3.getX(), 1e-8); } public void testGetY() { assertEquals(0, myCoordinate1.getY(), 1e-8); assertEquals(3.4, myCoordinate2.getY(), 1e-8); assertEquals(7.6, myCoordinate3.getY(), 1e-8); }
Test each accessor on every instance. Write one test method for each accessor to make it easier to debug (and to inflate the number of tests!).
Do this...
Add accessor methods getNumerator()
and
getDenominator()
to Fraction
. And write
Javadoc comments for them. Then
add new test methods testGetNumerator()
and
testGetDenominator()
to your FractionTest
class. Test your accessors like the Coordinate
accessors were tested above.
Remember the difference in data types between the instance
variables in your Fraction
class and in my
Coordinate
class. This has implications for your
accessors themselves as well as what version of
assertEquals()
you use.
Input is, quite frankly, annoying. Reading in a coordinate or fraction can be done, and it's actually rather easy if we assume we always get good input. That's what we'll assume.
Suppose that I wanted to read in a
Coordinate
like(3,4)
. Since input is so tricky and so particular, I will create a separate class to handle the input of aCoordinate
.I describe the problem like so:
Behavior of CoordinateInput#read(Keyboard)
I am a
CoordinateInput
object. To read a coordinate object, I will receive a keyboard, and then I will read in five things, in order:
- A left parenthesis.
- An x-coordinate.
- A comma.
- A y-coordinate.
- A right parenthesis.
I return a new coordinate based on the x- and y-coordinates that I read in.
The problem with this method is all of the extra things
I need to read in addition to the two coordinate values. The
Keyboard
class does not work well reading in
the separate punctuation characters separate from the numbers.
Instead, if I insist that a coordinate has no spaces in it, but has
spaces before and after it, I can use a Keyboard
object to read in a whole word (i.e., all characters up to
the next whitespace character). Java provides a
StringTokenizer
class which will break up the word
into its components so that I'll have access to the two coordinates
that I want. I can also check to make sure that the punctuation
marks are also there.
Quite frankly, there are so many other interesting things in
Java to look at, that it is not worth the time for us to explore
the StringTokenizer
in depth.
So without further ado, here's the method I write:
public Coordinate read(Keyboard in) { String input = in.readWord(); StringTokenizer parser = new StringTokenizer(input, "(,)", true); if (!parser.nextToken().equals("(")) throw new RuntimeException("Bad format for coordinate: " + input); double x = Double.parseDouble(parser.nextToken()); if (!parser.nextToken().equals(",")) throw new RuntimeException("Bad format for coordinate: " + input); double y = Double.parseDouble(parser.nextToken()); if (!parser.nextToken().equals(")") throw new RuntimeException("Bad format for coordinate: " + input); return new Coordinate(x, y); }
Input is always nasty, as this method demonstrates quite well.
If you're curious what's going on with this code, read the Java
API to find out more about the StringTokenizer
class
(found in the java.util
package). You should also look
up the Double#parseDouble(String)
method.
Once
CoordinateInput#read()
is defined, I can read in aCoordinate
like so:CoordinateInput input = new CoordinateInput(); Coordinate coordinate = input.read(theKeyboard);
Question #6.04 Write a behavior paragraph for
FractionInput#read(Keyboard)
.
To be answered in Exercise
Questions/lab06.txt
.
This code should satisfy your needs:
public Fraction read(Keyboard in) { String fraction = in.readWord(); StringTokenizer parser = new StringTokenizer(fraction, "/", true); int numerator = Integer.parseInt(parser.nextToken()); if (!parser.nextToken().equals("/")) throw new RuntimeException("Missing the slash"); int denominator = Integer.parseInt(parser.nextToken()); return new Fraction(numerator, denominator); }
You'll have to import ann.easyio.Keyboard
and
java.util.StringTokenizer
.
Do this...
Implement FractionInput#read(Keyboard)
.
It's difficult to test keyboard input with a JUnit test case. So
let's turn our attention to the driver in the
PierreCLIDriver
class.
Do this...
Uncomment the lines in the main()
method of
PierreCLIDriver
that declare and read in values for
oldMeasure
and scaleFactor
. Temporarily,
add this line at the end:
theScreen.println("The fractions are " + oldMeasure + " and " + scaleFactor);
Be sure to test this driver several times on different fractions to make sure the input method works correctly. Also test it on a zero denominator (which should cause the driver to crash (i.e., quit with an exception)).
What about some real computations?
I might want to add two
Coordinates
:coordinate1.add(coordinate2)This is thought of as sending the
add
message tocoordinate1
, withcoordinate2
as a message argument. The result that's returned should be a newCoordinate
set to the sum ofcoordinate1
andcoordinate2
.Computationally, this would be
(x1,y1) + (x2,y2) = (x1+x2, y1+y2)
Behavior of Coordinate#add(Coordinate)
I am a
Coordinate
object. I receive another coordinate object. To produce the sum, I construct a new coordinate: (my x-coordinate plus the received-coordinate's x-coordinate, my y-coordinate plus the received-coordinate's y-coordinate). I return this sum.
Objects of Coordinate#add(Coordinate)
Description Type Kind Movement Name the received coordinate Coordinate
varying in coordinate2
the sum Coordinate
varying out sum
my x-coordinate double
instance variable instance myX
my y-coordinate double
instance variable instance myY
coordinate2
's x-coordinatedouble
accessor call local coordinate2.getX()
coordinate2
's y-coordinatedouble
accessor call local coordinate2.getY()
the sum's x-coordinate double
varying local x
the sum's y-coordinate double
varying local y
I could store the x- and y-coordiantes from
coordinate2
in local variables, but the accessor methods are so short that it's just easier to use them directly. Since an accessor method gives you direct access to data, it's fair to consider it an object rather than an operation (although technically they are methods are so are operations).I do store the sum's coordinates in local variables because I'm constructing that value in this method.
Operations of Coordinate#add(Coordinate)
Description Predefined? Name Library receive a coordinate yes parameter mechanism built-in add double
syes +
built-in construct a new coordinate yes Coordinate
constructorCoordinate
Algorithm of Coordinate#add(Coordinate)
- Receive
coordinate2
.- Let
x
bemyX
pluscoordinate2.getX()
.- Let
y
bemyY
pluscoordinate2.getY()
.- Let
sum
be a newCoordinate
constructed usingx
andy
.- Return
sum
.So my resulting method is this:
public Coordinate add(Coordinate coordinate2) { double x = myX + coordinate2.getX(); double y = myY + coordinate2.getY(); Coordinate sum = new Coordinate(x, y); return sum; }And then I need to test my method. I could write this:
public void testAdd() { Coordinate sum = myCoordinate1.add(myCoordinate2); assertEquals(1.2, sum.getX(), 1e-8); assertEquals(3.4, sum.getY(), 1e-8); sum = myCoordinate2.add(myCoordinate3); assertEquals(11.0, sum.getX(), 1e-8); assertEquals(11.0, sum.getY(), 1e-8); sum = myCoordinate3.add(myCoordinate2); assertEquals(11.0, sum.getX(), 1e-8); assertEquals(11.0, sum.getY(), 1e-8); }However, this is clumsy. It's much better if I write a helper method first:
public void assertCoordinate(String message, Coordinate expected, Coordinate computed) { assertEquals(message, expected.getX(), computed.getX(), 1e-8); assertEquals(message, expected.getY(), computed.getY(), 1e-8); }Then my test method is much simpler:
public void testAdd() { assertCoordinate("first plus second", new Coordinate(1.2, 3.4), myCoordinate1.add(myCoordinate2)); assertCoordinate("second plus three", new Coordinate(11.0, 11.0), myCoordinate2.add(myCoordinate3)); assertCoordinate("third plus second", new Coordinate(11.0, 11.0), myCoordinate3.add(myCoordinate2)); }Now my tests read as if I'm testing
Coordinate
s, not x- and y-values.
While it is useful to define all of the arithmetic methods for a
Fraction
, the particular operation that we need in
order to solve our problem is multiplication (the others are left
as exercises).
You have this expression in the driver:
oldMeasure.multiply(scaleFactor)
This should multiply the two Fraction
objects
oldMeasure
and scaleFactor
.
The multiplication of two fractions is a little more involved that coordinate addition:
n1 n2 n1 * n2 ---- * ---- = --------- d1 d2 d1 * d2
That's essentially the behavior of the multiply()
method, except you want to keep in mind the internal
perspective---n1
and d1
are actually
my values (because I'm a Fraction
object).
The n2
and d2
come from another
Fraction
object that I receive.
The specification for such an operation can be written as follows:
Specification:
receive:
fraction2
, aFraction
operand.
return:product
, aFraction
, containing the product of the numerators and denominators of the two operands.
Do this...
Write a definition of Fraction#multiply(Fraction)
given the definition, specification, and example above. Write out
the OCD if you get stuck.
Do this...
Write a helper method
FractionTest#assertFraction(String,Fraction,Fraction)
which works similar to
CoordinateTest#assertCoordinate(String,Coordinate,Coordinate)
.
Do this...
Write another test method,
FractionTest#testMultiply()
. Write assertions in this
method (using the new helper method) to test these
multiplications:
1/2 * 3/4 = 3/8 -2/3 * 10/7 = -20/21
Both factors in both equations are fractions you should already
have in instance variables in your in
FractionTest
.
Do this...
Compile your code, and run for a green bar. Also, uncomment
the last two commented lines of code in the
PierreCLIDriver
class, and run it, too.
Mathematically speaking, all of these fractions are the same
value: 1/2, 2/4, 3/6, 4/8, 5/10, etc. However, with respect to
Java, a Fraction
object storing 1/2 is different from
a Fraction
object storing 2/4; the internal data is
different.
We also have to worry about negative values: mathematically, -1/2 == 1/-2 and 1/2 == -1/-2.
What we want is a canonical form for fractions:
The question is when and where do you want to do this? You want to simplify every fraction, but you don't want to write the code for this too many times. Consider that you could define addition, subtraction, division, negation, and six different kinds of comparison operations. Do you want to have to remember to simplify in each of these methods? No, you don't.
Truth be told, canonical forms are just a special kind of invariant. Invariants have to be tested when new objects are created (i.e., in constructors) or when instance variables are changed (e.g., in mutators, see below).
First, you need a simplify()
method. Think of this
as your canonical-form enforcer.
Behavior of Fraction#simplify()
I am a fraction. When I need to be simplified, I divide my numerator and my denominator by the largest number that divides them both. Also, if my denominator is negative, I negate both my numerator and my denominator.
From gradeschool, you might remember this "largest number that divides them both" being called the greatest common divisor (GCD).
Notice that no information goes in or out of this method. That means no parameters and nothing returned! I manipulate my own numerator and denominator.
Objects of Fraction#simplify()
Description Type Kind Movement Name my numerator int
instance variable instance myNumerator
my denominator int
instance variable instance myDenominator
the GCD int
varying local gcd
Operations of Fraction#simplify()
Description Predefined? Name Library compute the GCD no computeGCD
Fraction
divide integers yes /
built-in negate a variable yes *= -1
built-in
Algorithm of Fraction#simplify()
- Let
gcd
be the greatest common divisor ofmyNumerator
andmyDenominator
.- If
gcd
!= 0, thenEnd if.
- Set
myNumerator
equal tomyNumerator
divided bygcd
.- Set
myDenominator
equal tomyDenominator
divided bygcd
.- If
myDenominator
is negative, thenEnd if.
- Negate
myNumerator
.- Negate
myDenominator
.
The last step uses a bit of a trick for keeping the denominator always positive. Consider what happens:
Original | Simplified |
---|---|
1/2 | 1/2 |
-1/2 | -1/2 |
1/-2 | -1/2 |
-1/-2 | 1/2 |
This is exactly what we want.
Notice that after this method has finished, the state of the
object may have changed (i.e., myNumerator
and myDenominator
may be different). A method
that changes the state of the object is called a
mutator. One common tell-tale sign of a mutator is
that the method doesn't return anything---a void
return type. That's exactly what happens here: I'm a
Fraction
object, and I've modified the contents of my
instance variables, so what would I have to share with the rest of
the world? I've changed, so I don't have to return any
data.
You'll need this method in your Fraction
class:
/** * This method finds the greatest common divisor of two integers, * using Euclid's (recursive) algorithm * @param alpha one integer. * @param beta the other integer. * @return the greatest common divisor of alpha and beta. */ private int computeGCD(int alpha, int beta) { alpha = Math.abs(alpha); beta = Math.abs(beta); if (beta == 0) return alpha; else return computeGCD(beta, alpha % beta); }
It even has the Javadoc
comments for you. (It also has private
visibility
which is allowed; we use it here because no one else
really needs to know about this method outside the class.)
Do this...
Add the computeGCD()
method to your
Fraction
class.
Negating an integer is just a matter of multiplying by negative one:
myNumerator *= -1;
Do this...
Define the method Fraction#simplify()
using the
algorithm above, give it some Javadoc comments, and compile your code. Make sure you
conform to the method specification described above (no
parameters, no returned value)
The method does you no good, though, until you call it. But where, when, and how?
Where? The discussion above suggested that a fraction should be simplified whenever it is created.
When? Simplifying a fraction is the last step in constructing a fraction.
But how? It's a method, and you've seen the pattern for invoking
methods, however, simplify()
is an instance method,
and you need to invoke it on yourself.
Think about this in internal perspective: "I am a
Fraction
object, and... blah, blah, blah... In the
end, I simplify myself." What name do "I" have? Parameters have
names, but the current instance doesn't have an obvious name. Truth
be told, this "me" instance does have a name, but we don't
need it. Invoking an instance method on yourself can be done by
omitting the instance and the period.
It looks a little strange at first, perhaps, but keep in mind that with an internal perspective, you're basically talking to yourself. In fact, you're ordering yourself around: "Hey, me, go simplify myself!" Maybe the syntax is appropriately strange!
If I had a
Coordinate#simplify()
, here's what it'd look like in my default contructor:public Coordinate() { myX = 0.0; myY = 0.0; simplify(); }As a coordinate object, I set my x and y coordinates to default values, and then I simplify myself.
Do this...
Invoke the Fraction#simplify()
method at the end of
the explicit-value constructor.
Question #6.05 Why don't you have to
call simplify()
at the end of the default constructor?
You can, but it's not necessary.
To be answered in Exercise
Questions/lab06.txt
.
There are a variety of ways we could test the
simplify()
method. We could test it in
testToString()
, testMultiply()
, and any
other test method that tests a computation and uses
simplify()
. In fact, you must go back to
those methods and make sure that you account for simplified
results.
However, we should be a little more deliberate about testing the
simplify()
method itself. It doesn't hurt if we
incidentally test it through the other computation methods, but we
should dedicate one test method just to the simplification
problem.
Do this...
Add a new test method code testSimplify() to
FractionTest
.
You won't invoke simplify()
directly since you can
count on the explicit-value constructor calling it.
Do this...
Start out by testing the negation possibilities. Look at the table
above to simplify negative fractions. Here's what one of those
assertions might look like:
assertFraction("numerator and denominator negative", new Fraction(1, 2), new Fraction(-1, -2));
This might seem a little odd since the computation is so simple, but it's actually testing quite a lot!
Do this...
Now test the common-factor simplification. These are up to you.
Come up with at least three fractions that aren't simplified, and
test their simplifications in a similar way. At least one test
should have a numerator greater than a denominator. At least one
test should trigger both the GCD simplification and have a
negative denominator.
Do this...
Add a test to testMultiply()
that generates a product
that should be simplified. This is to make sure that simplification
is done by the multiply()
method. You can reuse some
of the earlier fractions to demonstrate this.
Now that you have seen how to build a class, we need to expand our design methodology to incorporate classes:
Using this methodology and the Java class mechanism, we can now create a software model of any object! (The rest of your computer programming education is about learning the most efficient and effective ways of doing this.) The class mechanism thus provides the foundation for object-oriented programming (OOP), and mastering the use of classes is essential for anyone wishing to program in the object-oriented world.
Submit copies of all your code plus a sample execution of your test-case class and five executions of your driver.
Question #6.06 Did you remember to record the
total amount of time it took you to complete this lab in Question
#6.02?
To be answered in Exercise
Questions/lab06.txt
.
accessor, canonical form, class, construct an object, constructor, data type, default constructor, denominator, explicit-value constructor, greatest common divisor (GCD), instance variable, internal perspective, invariant, invoke a method, local variable, message, mutator, numerator, object attribute, object operation, object-oriented programming, OOP, overloading, parameter, postcondition, private visibility, public visibility, visibility