Lecture 7: Nested Data and Approximate Numbers
Overview
Writing methods for classes that refer to other classes
Wish lists for building complex methods
A new kind of number
7.1 Nested Data
In the last section, we saw more examples of classes, and how we can use the tester library to get more convenient checking of our methods’ results.
In this section we’ll get more practice with classes whose fields have the type of other classes.
7.1.1 Representing Regions
A common idea in geometric or mapping applications is describing a region in 2D space. Let’s take rectangular regions as an example. Here’s a rectangular region whose lower-left corner is at 30, 40, and whose upper-right corner is at 100, 200:
How might we represent rectangles like this with a class? There are a few options: we could use the lower-left and upper-right corner, we could use all four corners, or we could use the lower-left corner with a width and a height. Many more options are possible.
Let’s work with the first of these for now, where we want to represent the region using the lower-left and upper-right corner. One attempt at this might look like:
class RectRegion { int lowX; int lowY; int highX; int highY; // Constructor to be written }
Notice that we have pairs of fields, lowX and lowY, that have meaning together. In particular, we’ve seen before that it’s useful to represent such pairs of x/y values in a Point class. So, another option is to use two Point fields, instead of four numeric fields:
class RectRegion { Point lowerLeft; Point upperRight; RectRegion(Point lowerLeft, Point upperRight) { this.lowerLeft = lowerLeft; this.upperRight = upperRight; } }
This choice is useful because it lets us re-use any methods that are already written on Point, and it also captures the relationship between the numbers. Remember that we had this definition of Point before:
class Point { int x; int y; Point(int x, int y) { this.x = x; this.y = y; } }
Let’s think about a few examples of RectRegions:
class ExamplesRegion { RectRegion r1 = new RectRegion(???, ???); RectRegion r2 = new RectRegion(???, ???); }
Do Now!
What must be the type of the expressions we use to fill in the ??? above?
Since the two fields of RectRegion are both Points, we must pass references to Point objects when creating the RectRegion. If we want r1 to match the example in the picture above, for example, we could write it as:
class ExamplesRegion { RectRegion r1 = new RectRegion(new Point(30, 40), new Point(100, 200)); RectRegion r2 = new RectRegion(???, ???); }
We could pick another example for r2 and write it in a similar way:
RectRegion r2 = new RectRegion(new Point(10, 10), new Point(50, 50));
We could also create the Points, and then use them in the creation of r2 by using field access:
Point p1 = new Point(10, 10); Point p2 = new Point(50, 50); RectRegion r2 = new RectRegion(p1, p2);
Let’s put all these examples together, and rename the last one to r3, so we can see them all in one place:
class ExamplesRegion { RectRegion r1 = new RectRegion(new Point(30, 40), new Point(100, 200)); RectRegion r2 = new RectRegion(new Point(10, 10), new Point(50, 50)); Point p1 = new Point(10, 10); Point p2 = new Point(50, 50); RectRegion r3 = new RectRegion(p1, p2); }
Do Now!
How many total Point objects are created in the program above?
It’s useful to draw this out as a diagram as we have done before, because it will help us understand the relationships between the objects.
There are a few interesting things about this diagram worth calling out:
The Points with labels 8 and 9 are referenced both by the ExamplesRegion object in the p1 and p2 fields, and by the bottom-most RectRegion object’s lowerLeft and upperRight fields. These two Point objects were created once, and then referenced several times.
There are two different Point objects that both have the x and y fields equal to 10 (the ones labelled 6 and 7). The same is true for the objects whose x and y fields are 50. Because we used new to create different objects, these are separate, even though they have the same fields.
The numbered labels match the printed output numbering, so if you run the program above, you should see numbers corresponding to these outputs.
This gives us a nice review of how objects and references are laid out after creation. Now we can go on to defining some methods on the new class we’ve defined.
A natural method to want on RectRegion is one that checks if a given Point is contained within the region. For example, the point 60, 60 is in the example rectangle region we gave in the beginning, but it wouldn’t be contained in the second and third example rectangles. The point 20, 20 is contained in the latter two but not the first. This gives us the header and examples; we’ll write the examples as tests, using the testing support:
class RectRegion { // fields and constructor ... /* @param p The coordinates of a point to check for containment within this region @return true if the coordinate is contained in the region, false otherwise */ boolean contains(Point p) { } } class ExamplesRegion { // Definitions of r1 through r3.... Point toTest1 = new Point(60, 60); Point toTest2 = new Point(20, 20); boolean testContains(Tester t) { return t.checkExpect(this.r1.contains(this.toTest1), true) && t.checkExpect(this.r2.contains(this.toTest1), false) && t.checkExpect(this.r3.contains(this.toTest1), false) && t.checkExpect(this.r1.contains(this.toTest2), false) && t.checkExpect(this.r2.contains(this.toTest2), true) && t.checkExpect(this.r3.contains(this.toTest2), true); } }
Next, we need to implement the method body. While there are a few ways we could try to write it, this is a good opportunity to introduce a new strategy for working through a method implementation. It would be quite helpful here if Point had a method that could compare it to another Point, and check if both the x and y fields are greater than those on the given point, and another to tell if the x and y fields are less. These would let us check if the Point given to contains is between the two Points referred to by lowerLeft and upperRight. To be concrete, if these methods were defined, we could implement contains with something like:
class RectRegion { // fields and constructor ... /* @param p The coordinates of a point to check for containment within this region @return true if the coordinate is contained in the region, false otherwise */ boolean contains(Point p) { return this.lowerLeft.belowLeftOf(p) && this.upperRight.aboveRightOf(p); } }
Of course, these methods don’t exist, so the method above will simply report an error. But that’s OK, as long as we go back and implement belowLeftOf and aboveRightOf. The implementation above gave us a wish list of methods that, if only we had them, would make our job in our current task much easier. This is a common pattern when writing a more complicated program, and is one strategy for decomposing a problem into smaller pieces.
Now we just need to go back and implement belowLeftOf and aboveRightOf.
Do Now!
Implement these two methods, and check that the implementation works as expected.
Exercise
Can you think of a way to implement contains that uses only one of belowLeftOf or aboveRightOf
7.1.2 More Regions
Above, we defined rectangular regions. A natural next step is to define circular ones. A simple definition might consider the circle’s center point and its radius:
class CircRegion { Point center; int radius; CircRegion(Point center, int radius) { this.center = center; this.radius = radius; } }
Let’s try implementing contains for CircRegion as well:
class CircRegion { // ... fields and constructor ... /* @param p the point to check for containment within the circle @return true if the point is within radius units of the center of the circle */ boolean contains(Point p) { } } class ExamplesRegion { // ... rectangle examples and test ... CircRegion c1 = new CircRegion(new Point(200, 50), 10); CircRegion c2 = new CircRegion(new Point(20, 300), 25); Point circleTest1 = new Point(210, 50); Point circleTest2 = new Point(20, 315); boolean testContainsCirc(Tester t) { return t.checkExpect(this.c1.contains(this.circleTest1), true) && t.checkExpect(this.c1.contains(this.circleTest2), false) && t.checkExpect(this.c2.contains(this.circleTest1), false) && t.checkExpect(this.c2.contains(this.circleTest2), true); } }
Again, we can fill in the implementation while wishing for a function to exist. In this case, if we can calculate the distance from the center point to the given point, we could then compare that distance to the radius. If that distance is less than the radius, the point is inside, otherwise it isn’t.
/* @param p the point to check for containment within the circle @return true if the point is within radius units of the center of the circle */ boolean contains(Point p) { return this.center.distance(p) < this.radius; }
Now all we need to do is implement distance. To do that, we need to do a calculation like the following, where x1, x2, y1, and y2 are the corresponding fields on this point and the other point:
√(x1 - x2)2 + (y1 - y2)2
We know how to do the squaring operator through multiplication, but we don’t know how to ask Java to perform a square root yet. This is provided as a method on a built-in called Math. The method is called sqrt, which takes a number and returns its square root. However, if we try to make an example out of this, and store its result in a int field, we get:
int sqrt1 = Math.sqrt(5);
ExamplesRegion.java:32: error: incompatible types: possible lossy conversion from double to int |
int sqrt1 = Math.sqrt(5); |
^ |
This refers to a type we haven’t seen before called double. Java (and many languages) has two different kinds of numbers that it works with. We’ve seen ints, which (along with a few other types) represent exact integers. The other kind represents approximations of numbers, and double is the most common type we’ll see for approximate numbers. This (finally) lets us write numbers that aren’t just integers. Some examples:
double d1 = 5.5; double d2 = -3.33333;
The type double is what sqrt returns, so we can use that instead of int in our earlier example:
double sqrt1 = Math.sqrt(5);
Now, we get an actual answer, which is an approximation of √5.
This new type isn’t without its pitfalls – doubles are truly approximations. Try out this example, for instance:
double d3 = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
This may seem like a small error. However, if you’re writing code for a bank that makes errors on a fraction of a penny billions of times a day, you’ll start to care an awful lot. In addition, in a long-running program, errors can stack up, with literally deadly consequences. The summary of that article is that the accumulated error from repeated addition of 1/10 became large enough to cause a targeting error. Our stakes are a little lower, but the importance of understanding that doubles are only approximate cannot be overstated.. Physics and math applications of computing often study numerical methods for quantifying and accounting for such inaccuracies.
For our purposes in region calculation, we can happily rely on the result of the sqrt operation to check for a point being contained in a CircRegion. It requires changing the return type of distance to be double rather than int, to match the return type of Math.sqrt:
class Point { /* @param other A point to calculate the distance to @return The (approximate) units between the points, calculated by the root of sum of squares of differences in coordinates */ double distance(Point other) { return Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)); } }
With this, our whole solution runs through without issues. Note that we used another method on Math, the pow method, which takes two numeric arguments, and returns the value of the first raised to the power of the second. So in the example above, the 2 indicates squaring the argument.
7.2 Summary
In this section, we saw:
Examples of classes that contain fields of the types of other classes we wrote – nested data.
Using wish lists to keep track of methods we should implement next, in order to break down a larger problem.
Examples of doubles, which can represent fractional numbers, but only approximately.