Assignment 06

Task

Revisit A02, only this time make the cone its own class and create instances of it.

Concepts: Constructors, instance variables, encapsulation
Textbook: Chapter 3

Steps

You are going to create a Cone class, test that it is correct, and then use that class to solve a problem involving ice-cream cones. Do this in the following order.

Step 1: Cone.java

Write a public class named Cone. Put this in its own file (for now). The class should have at least two private instance variable for the radius and height. The class should also contain the following public constructor and methods:

Your methods must match these signatures exactly. (You can change the parameter names r and h if you like, but not their order or what they represent.)

Your methods should return the correct values. The necessary formulas were given in A02. These methods should not print anything to the screen.

Error-checking of passed values is optional. For example, you could prevent negative values for the radius by setting it to 0 or taking its absolute value instead; but this is not required--you may allow another programmer to make a cone with a negative radius if they really want to.

Step 2: Testing

The primary reason for writing methods is so that you can easily reuse chunks of code. It reduces errors and makes code easier to maintain if you can avoid duplicating code.

But the separation of code into small, independent modules has additional benefits. It can make the code more readable overall. And you can then unit test each of these small pieces to know they are correct. This helps greatly when debugging, especially for larger programs.

Here is a sample unit test for your cone: ConeTester.java.

Download this file and put it in the same directory as your Cone.java file. You should then be able to compile and run ConeTester to ensure that all your methods have the correct signatures and return the correct values. Although ConeTester uses some concepts we haven't covered in lab yet--such as static methods and if statements--you should still be able to figure out how it works. (See Section 5.2.2 in your textbook to learn why this tester includes an whole equals method just to compare two double values.)

Step 3: Main program (UsernameA06.java)

Now that you know your Cone is correct, you could reuse it in a number of different programs. Here is one such program.

An ice cream company sells a pre-packaged ice cream cone similar to a Cornetto or Drumstick: a waffle cone coated with chocolate on the inside and filled with ice cream to form a flat top even with the top of the cone. The company has a long-standing Original size, but they would now like to add a smaller and larger-sized cone to their line:

ConeRadius (cm)Height (cm)
Original311
Small2.7510
Large3.513

However, someone pointed out that this change in cone size might affect the ratio of waffle cone to ice cream, and thus change the flavor of the treat. The company wants to know what the exact numbers are on this. So that's your job.

Specifically, for each of the 3 sizes, print:

Additionally, on the same line as each measure, print what % this is of the Original cone's corresponding measure. Print this % as an int. Round it with Math.rint; don't just cast it. And remember to include the % sign.

You may also print out the radius and height for each cone, if you wish. This is optional, and so you do not need to include the % for these two measures (although you may).

Use your Cone class for all calculations, so that the only literals you will have in main will be the radii given above. Note that you may not need to call every Cone method to solve this particular problem.

You can avoid duplicating code three times in your main method by writing a static method in your UsernameA06 class. This is recommended but optional, so the details are in the FAQs.

Sample Output

Dimensions of 3 ice cream cones (as % of Original): 

ORIGINAL
Ice cream volume: 103.67255756846318 cm^3 (100%)
Waffle cone area: 107.45900217885213 cm^2 (100%)
Ice cream to cone ratio: 0.9647638212377322 (100%)

SMALL
Ice cream volume: 79.1943148092427 cm^3 (76%)
Waffle cone area: 89.60103170808534 cm^2 (83%)
Ice cream to cone ratio: 0.8838549434034745 (92%)

LARGE
...

This is only the first half of the output, just to show you a sample format that meets the requirements. You'll need the dimensions of the LARGE cone as well.

What to Submit

Submit only your UsernameA06 and Cone classes. Paste the Cone class into your UsernameA06.java file. Remember to remove the public modifier from the Cone class and do not accidentally paste one class within the other.

Upload your complete UsernameA06.java file to Tamarin.

Grading [7 points]

1 - Compiles
Your program compiles successfully (no errors).
0.8 - Cone Constructor
Has a public constructor that takes the radius and height (in that order) as doubles.
1.2 - Cone accessor (get) and mutator (set) methods for radius and height
0.3 each for get and set method, with the method signatures specified above.
1.6 - Cone accessors for slant height, volume, lateral surface area, and total surface area
0.4 each. All methods are instance methods (not static) and exactly match the method signatures given above. Returned values are correct regardless of the order in which methods are called or if the dimensions of the cone are changed using the set methods.
0.2 - No printing from Cone
Cone methods and constructor should not print to the screen.
0.2 - Encapsulation
All your instance variables are private, and you do not use any class (static) variables.
2.0 - UsernameA06
Program clearly prints the correct volume, lateral surface area, and ratio of these two for the 3 requested cones (0.9). For each dimension printed, also gives the percentage (as a rounded integer) that this is of the Original cone's corresponding measure (0.45). Creates and uses at least one Cone object to do this (0.35). All calculations are performed by the program, not by you (0.3).

FAQs

Demo code:
Here is an example class--HighScore--from the first lab this week. It has been expanded and commented to show encapsulation principles at work. All the //comments explain what is going on in each method. Then, to show how such a class might be used, see ArcadeGame.java. (This class also uses some features you've only learned about in lecture at this point: a Scanner and if statements.)
You need to do the exact same sequence of operations to print out the details and %s for each ice cream cone. And you need to repeat this three times with only the specific cone changing each time. This means it would be a good idea to write this code as a static helper method in your UsernameA06 class. Then you only have to write this code once and can just call the method three times, passing the method a different cone to work with each time. Since all the code then occurs only once, it is also much easier to correct and maintain, rather than trying to fix 3 different copy-and-pasted versions.

So, for example, you could write a method like this in UsernameA06:

  public static void printIceCreamDetails(Cone selected, Cone base)

In this method, print out the lateral surface area, volume, and ratio of the selected cone. Also calculate the % of each dimension in terms of the base cone.

Then, in main, after you create the 3 ice cream Cones, you can call this helper method like this:

  System.out.println("ORIGINAL");
  printIceCreamDetails(original, original);
  System.out.println();
  System.out.println("SMALL");
  printIceCreamDetails(small, original);
  //...

This will save you a fair amount to editing/code-tweaking time. Note too how easy it would then be to change your program later to include more cones, different cones, or to determine the %s based on a cone other than the Original. This flexibility is another advantage of breaking your code into methods instead of manually duplicating code. (Note how the ConeTester class above did the same sort of thing with the test and equals methods.) This example also demonstrates how you can pass whole objects around to other methods.

So the printIceCreamDetails method described above can print to the screen? Shouldn't methods just return their results?
It depends on the intent of the method. If the class as a whole is meant to model an object or if the method is just doing some computation, it is generally cleaner to have those methods just return their results. That way the class and/or methods can be reused more easily, even in cases where you no longer want the results printed on the screen.

On the other hand, sometimes you just want to move code out of main into a separate method. This serves two purposes. First, it can make main easier to read. Instead of a big block of code, there is just a single method call:

  System.out.println("SMALL");
  printIceCreamDetails(small, original);

This makes is easier to quickly understand what main is doing. Then, to see the particular details of how each ice cream cone's details are printed, you can go look at that method.

The second purpose of moving code from main into a method is that you can then easily repeat that single block of code by calling the method a number of times. This is certainly true for printIceCreamDetails.

So, since printIceCreamDetails is a helper method to main and its purpose is to help you generate output, it is fine to let it print to the screen for you.

I tried using Math.rint, but it returns a double. What's up with that?
Sorry I didn't get a chance to explain this in a bit more detail. If you check the API, it says this:
static double rint(double a)
          Returns the double value that is closest in value to the argument 
          and is equal to a mathematical integer.

So, yes, it returns a double (though it is equal in value to an int, meaning it is of the form ##.0, where # are digits). This means you'll need to cast its result to actually get an int.

Here's what I mean: I suppose I want to print the result of 107.8 / 11 as the nearest integer. If I just do this:

  107.8 / 111  
it will give me 9.8. (Because 107.8 is a double and 11 is an int, I'll do double math here.) So the closest integer (the one I want to print here) would be 10.

If I just cast to an int, it will simply drop the decimal part. So:

  (int) (107.8 / 11)

will give me 9, not 10. If I only use Math.rint:

  Math.rint(107.8 / 11)

it will give me the double that's equal to the int I want, which is 10.0. So I need to do both: use Math.rint and cast it, but in the right order:

  (int) Math.rint(107.8 / 11)  
What is the purpose of a constructor?
Try reading this.
Why do we have to write the getRadius() and setRadius(double r) methods?
It's to practice encapsulation.

You should make your instance variables private. When you do this, you can no longer affect them directly from another class. But it'd still be nice to be able to change the size of a Cone after you construct it, or to ask a Cone object what its radius is. So you write a couple public methods that lets other classes affect the instance variables only through those methods.

This practice of encapsulation is a good long-term programming practice for large projects. Making your instance variables private means you effectively hide them so you can later change how your class works internally without having to also change all the code in every class that uses that changed class (since they use only the methods). An example of this is that your accessor methods for volume and slant height may hide the fact that you don't even have a variable for those dimensions of the Cone; instead, you simply calculate them each time the value is requested through the accessor method.

Also, you can error-check any changes someone is making to a variable in the mutator method. For example, you could enforce in your setRadius method that no one ever sets the radius to a negative value. This is something you could not control if another class had direct access to the variable.

Finally, you could prohibit changes to some variables all together by simply not writing a mutator method for it. For example, no one can directly change the volume of your Cone; instead, they can only change the radius or height.

So, for all these reasons, encapsulation is good practice--particularly for large projects where you have 10s or 100s of classes. So that's why we're having you do it here--to get into the habit.

Why do I need the getTotalSurfaceArea() method or some of these other methods I never called to solve the ice cream problem?
The intent of this assignment is to write a Cone class you could then reuse to a number of different contexts. The ice cream problem is then just to give you some practice using the Cone class in one specific context. So, yes, there are some methods in the Cone class you won't call for A06.

If the ice cream problem were instead the focus of A06, then you'd probably write a IceCreamCone (rather than Cone) class and then you would not need to implement any methods you weren't going to use for that particular assignment. (However, while designing such an IceCreamCone class, you would still want to think about additional ways an IceCreamCone might be used so that your design would allow for easy expansion if it was ever reused elsewhere.)

Hey, my cone works fine when I use it, but when Tamarin calls my methods, my two area methods return the wrong results!
It's probably because you made the slant height an instance variable, but then failed to manage it properly. This is the sort of bug that comes from using instance variables rather than local variables. :)

Imagine you have three instance variables: radius, height, and slant. But in your constructor, you only do this:

  public Cone(double r, double h) {
   this.radius = r;
   this.height = h;
  }

As you know from testing your code, your getSlantHeight() method works fine. But it probably does something like this:

  public double getSlantHeight() {
    slant = Math.sqrt(Math.pow(radius, 2) + Math.pow(height, 2));
    return slant;
  }

Because you didn't declare slant as a local variable in this method, the method has a side-effect of changing the value in slant each time you call it.

Now, as you saw in the Tamarin output, something weird was happening with both of your area methods. Let's take the lateral SA as an example. Maybe your method looks something like this:

  public double getLateralSurfaceArea() {
    double lsa = Math.PI * radius * slant;
    return lsa;
  }

So you're using slant here. But what value is currently in slant? It depends when this method gets called!

If it's called on a brand new cone, radius and height were just set to something by the constructor... but slant was not set to anything at all. That means it'll be 0. So if you build a new Cone object and call getLateralSurfaceArea() before you call getSlantHeight(), you get a result of 0. But if you swap the order of the two method calls, then things start working right.

Similarly, what happens if you have a "working" cone (because you called getSlantHeight() at some point) but then call one of the two set methods to change either the radius or the height? Your set methods probably don't update slant either, and so it still has the old, now-incorrect value stored in it until someone calls getSlantHeight() again.

So, to fix all this, you either need to manage slant in your constructor and in your two set methods so that it always has the correct value stored in it. Basically, any time you set radius or height, you need to call getSlantHeight() to update slant too.

Or, the approach I would recommend, is get rid of slant as an instance variable so that you can't accidentally do this again. :) Instead, declare it as a local variable in getSlantHeight() (as was done for lsa in the example above). And then, whenever you need the slant height in one of your other methods, just call the method to get the value. Example:

  public double getLateralSurfaceArea() {
    double lsa = Math.PI * radius * this.getSlantHeight();
    return lsa;
  }

You can use the same approach in getTotalSurfaceArea(): simplify your formula by calling/reusing getLateralSurfaceArea().