In the previous exercise, we looked at extreme testing, and how encapsulation makes that possible. In this one, we’ll see some of the tradeoffs that can be made with fields and methods.
1 public class SphereCalc {
2 double radius;
3
4 public void setRadius( double r ) {
5 radius = r;
6 }
7
8 public double getRadius() {
9 return radius;
10 }
11
12 public double getSurfaceArea() {
13 return 4*Math.PI*radius*radius;
14 }
15
16 public double getVolume() {
17 return 4*Math.PI*Math.pow(radius,3) / 3.0;
18 }
19 }
This object is very similar to the ones in the last couple of exercises. A
single instance variable this time, one mutator method (setRadius())
and three accessor methods. (The surface area of a sphere is
, and the volume of a sphere is
.)
1 public class SphereCalcTester {
2 public static void main( String[] args ) {
3
4 SphereCalc c = new SphereCalc();
5
6 c.setRadius(5);
7 if ( isNear(c.getSurfaceArea(), 314.159265359) )
8 System.out.println("PASS: surfaceArea for " + c.getRadius());
9 else
10 System.out.println("FAIL: surfaceArea not what was expected!");
11 if ( isNear(c.getVolume(), 523.598775598) )
12 System.out.println("PASS: volume for " + c.getRadius());
13 else
14 System.out.println("FAIL: volume not what was expected!");
15
16 c.setRadius(0.1);
17 if ( isNear(c.getSurfaceArea(), 0.125663706) )
18 System.out.println("PASS: surfaceArea for " + c.getRadius());
19 else
20 System.out.println("FAIL: surfaceArea not what was expected!");
21 if ( isNear(c.getVolume(), 4.18879E-3) )
22 System.out.println("PASS: volume for " + c.getRadius());
23 else
24 System.out.println("FAIL: volume not what was expected!");
25
26
27 }
28
29 public static boolean isNear( double a, double b ) {
30 return Math.abs(a-b) < 1E-9;
31 }
32 }
This is clearly a tester and not just a simple driver program; I have tests that are passing or failing. Writing this was pretty annoying, but I wanted to show you the idea without making it too crazy, so I had to use my calculator with a couple of test cases to see what they ought to be. In a future exercise we’ll see a much better way to do a lot of tests like this without repeating so much code, but it’s too complicated for now.
Line 4 instantiates a SphereCalc object, then line 6 sets its radius to 5.
Starting down on line 29, there’s a little helper function I wrote. It receives
two doubles and returns true if the absolute value of their difference is
very small (smaller than
). It’s best to avoid using
just == on two floating-point values since sometimes repeating decimals or
slight differences in rounding will make two values that ought to be the same
slightly different.
(Instead of isNear() I probably could have called the function
isVeryCloseToEqual() but I didn’t feel like typing that more than once.)
So lines 7 through 14 just call the methods from SphereCalc and make sure
they return numbers close enough to the expected values. If so, we print out
“PASS” and if not we print out “FAIL” and a little bit of detail. Normally
you’d want to print out more information with the failure (like which radius
failed and what the expected value was and what you got instead), but I didn’t
want to clutter up the code.
Oh, and in case you’ve never seen it before, an E inside a floating-point
number means “times ten to the”. On line 21, 4.18879E-3 means
) A.K.A. 0.00418879.
Okay, so now let’s look at an slightly different way of splitting up the
work in the SphereCalc object. (You’ll need to type this one in, too,
if you’re going to do the Study Drill.)
1 public class SphereCalc2 {
2 double radius, area, volume;
3
4 public void setRadius( double r ) {
5 radius = r;
6 area = 4*Math.PI*r*r;
7 volume = 4*Math.PI*Math.pow(r,3) / 3.0;
8 }
9
10 public double getRadius() {
11 return radius;
12 }
13
14 public double getSurfaceArea() {
15 return area;
16 }
17
18 public double getVolume() {
19 return volume;
20 }
21 }
SphereCalc2 has three fields instead of just one. And inside the
setRadius() mutator method, it doesn’t just set the radius, it also goes
ahead and computes the surface area and volume, too.
There’s a trade-off here. Each instance of a SphereCalc2 object would take up
slightly more memory than each SphereCalc object, because of the extra
fields, and creating a instance of a SphereCalc2 object would take slightly
longer than instantiating a SphereCalc object because it does more
calculations up front.
However, if you had a SphereCalc object and you called getVolume()
over and over again in a loop or something, it would have to do that
calculation over and over. Whereas a SphereCalc2 object has already done
the calculation and just gets to return that single value over and over.
Which approach is better? You’d have to run tests and see how your object is being used to find out.
SphereCalc2 has one serious problem, however. Well, it’s more like a
vulnerability than a problem. When someone is using a SphereCalc2 object
and they want to change the radius, we expect them to use the provided
setRadius() method. We hope that’s what they will do.
But as you might recall from TVActorDriver.java way back in Exercise 4,
a driver class can access instance variables directly. At least, the way
we’ve been writing them up to this point.
What’s to prevent someone from writing code like this?
SphereCalc2 sph = new SphereCalc2();
sph.setRadius(5);
sph.radius = 7; // OH NOES!
System.out.println( sph.getVolume() );
Now, it probably wouldn’t look so evil. It might be like on line 16 on the tester. Instead of writing:
c.setRadius(0.1); // <-- why write this...
c.radius = 0.1; // <-- when it's SO much easier to write this?
It’s more efficient, right? Who wants to call a method when you can just put a value in a variable?!? Not this guy!
Anyway, hopefully that illustrates the “problem”. For the solution, you’ll have to come back in the next exercise.
“Learn Object-Oriented Java the Hard Way” is ©2015–2016 Graham Mitchell