Room Temperature class

advertisement
Room Temperature class.
Step 1:
Start with the basics:
--------/*
* @(#)Temperature.java
*
* Copyright (c) 2000 George T. Heineman
*/
package common;
/**
* The <code>Temperature</code> class contains the basic class representing
* any temperature information.
*
* Temperatures are either Celsius, Fahrenheit, or Kelvin
*
* @author George T. Heineman (heineman@cs.wpi.edu)
* @version 1.0, 2000/09/22
* @since common 1.0
*/
public class Temperature {
/**
* Constants
*/
public static final int CELSIUS = 1;
public static final int FAHRENHEIT = 2;
public static final int KELVIN = 3;
/**
* Unit of this temperature object
*/
protected int unit;
/**
* Value of this temperature object
*/
protected float value;
/**
* Create Temperature object with given value and units
*
* @param value is the temperature
* @param unit is an integer representing the desired units
*/
public Temperature (float value, int unit) {
}
----------
As I think about the constructor, I want to ensure that only valid Temperature objects are created. That is, I
don't want anyone to create an object of (-10000, 2). I also want to ensure that no temperature object is
created with an illegal unit, such as (20, 8).
I decide to localize all temperature value checks in the method setValue (float val) which I know will be
something I will have anyway. I also localize the check for unit in the method setUnit (int unit) which I also
will have.
I write the body of the constructor:
------public Temperature (float value, int unit) {
setValue (value);
setUnit (unit);
}
------Wait a minute. What is wrong with this sequence of operations? Well, -10 is a valid temperature for
CELSIUS but not for KELVIN.
So I must first set the unit, then I can set the value:
----public Temperature (float value, int unit) {
setUnit (unit);
setValue (value);
}
----Let's tackle the setUnit() first. There will be a companion method, getUnit(), so we write them together: I
realize that someone may come along and create new temperature scales, so I create two new constants,
MINTYPE and MAXTYPE and make them protected to the class and any subclass. I place them near the
constant definitions so they will be visible if anyone adds a new temperature scale.
-----protected static final int MINTYPE = 1;
protected static final int MAXTYPE = 3;
/**
* Set unit for the temperature object
*
* @param unit is the desired unit.
*/
public void setUnit (int unit) {
// If invalid unit throw IllegalArgumentException
if ((unit < MINTYPE) || (unit > MAXTYPE)) {
throw new IllegalArgumentException (unit + " is an invalid temperature unit.");
}
this.unit = unit;
}
/**
* Get unit for the temperature object
*
*/
public int getUnit () {
return unit;
}
-----Now I address setValue() and the companion getValue() method. As we discussed in class, there will
always be a minimum temperature value, so we start there. First, we define a static method associated with
the class:
----/**
* Return the minimum value for the given unit
*/
public static float getMinimumValue (int unit) {
if (unit == CELSIUS) {
return (float) -273.16;
} else if (unit == KELVIN) {
return 0;
} else if (unit == FAHRENHEIT) {
return (float) -459.69;
}
// should never get here
throw new IllegalArgumentException (unit + " is an illegal Temperature Unit.");
}
----Now we define a class method for any Temperature objects that uses this method:
----/**
* Return the minimum value for the given temperature object
*/
public float getMinimumValue () {
return getMinimumValue (unit);
}
----We are ready to defint setValue() and getValue():
----/**
* Set unit for the temperature object
*
* @param unit is the desired unit.
*/
public void setValue (float value) {
// If invalid value
if (value < getMinimumValue()) {
throw new IllegalArgumentException (value + " is an invalid temperature.");
}
this.value = value;
}
/**
* Get value for the temperature object
*
*/
public float getValue () {
return value;
}
----As I look at the error exception thrown in setValue() I realize that it is not expressive enough. In particular,
it should state the units of the temperature. So we create a new method:
----/**
* Return units as a string
*/
public static String getUnitString (int u) {
if (u == CELSIUS) {
return "Celsius";
} else if (u == FAHRENHEIT) {
return "Fahrenheit";
} else if (u == KELVIN) {
return "Kelvin";
}
// Should never get here
throw new IllegalArgumentException (u + " is an illegal Temperature unit.");
}
----And we can now rewrite setValue():
----public void setValue (float value) {
// If invalid value
if (value < getMinimumValue()) {
throw new IllegalArgumentException (value + " is an invalid " + getUnitString (unit) + "
temperature.");
}
this.unit = unit;
}
----Ok. Now more functionality. One of the common calculations with temperature is to convert between
different scales. We create a method float convert (int targetUnit) that allows us to convert from one
temperature to another unit (and return the float representing the value).
----/**
* Convert the object to the desired unit
*
* @param targetUnit the target temperature unit
* @returns the temperature converted to the temperature scale.
*/
public float convert (int targetUnit ) {
if (targetUnit == this.unit) return value;
if ((targetUnit < MINTYPE) || (targetUnit > MAXTYPE)) {
throw new IllegalArgumentException (targetUnit + " is an illegal Temperature unit.");
}
if (unit == CELSIUS) {
if (targetUnit == KELVIN) return value + (- getMinimumValue(CELSIUS));
if (targetUnit == FAHRENHEIT) return (value*9/5)+32;
}
if (unit == KELVIN) {
if (targetUnit == CELSIUS) return value + getMinimumValue (CELSIUS);
if (targetUnit == FAHRENHEIT) return ((value + getMinimumValue (CELSIUS))*9/5)+32;
}
if (unit == FAHRENHEIT) {
if (targetUnit == CELSIUS) return (value - 32)*5/9;
if (targetUnit == KELVIN) return ((value - 32)*5/9) + (- getMinimumValue(CELSIUS));
}
// should never get here
throw new IllegalArgumentException ("Temperature Object has illegal unit: " + unit);
}
----Note that this method will not compile unless we have the last throw exception clause. Instead of returning
0.0 or some special number, we throw an exception to signal that the original temperature object was
somehow corrupted.
Another common operation is to compare temperature values. There is a standard etiquette for such
methods. We create a method called "acompareTo(b)" that returns a value < 0 if a<b, a value> 0 if a>b, and
0 if a equals b. This is simliar to the strcmp method common to the C programming language.
----/**
* Compares two temperatures.
*
* @param anotherTemp the <code>Temperature</code> to be compared.
* @return the value <code>0</code> if the argument temperature is equal to
*
this string; a value less than <code>0</code> if this temperature
*
is less than the temperature argument; and a
*
value greater than <code>0</code> if this temperature is
*
greater than the temperature argument.
*/
public int compareTo(Temperature anotherTemp) {
// First convert argument to our units.
float localValue = anotherTemp.convert (unit);
if (value < localValue) return -1;
if (value > localValue) return +1;
return 0; // Match!
}
-----
One last method and we are done. In Java, there is a standard that each object should have a toString()
method to convert the object into a reasonable string representation.
----/**
* Convert temperature into a string representation.
*/
public String toString () {
return value + " " + getUnitString (unit);
}
----I suddenly realize a nifty way to avoid the clumsy use of convert() to create new temperature objects. I
decide to create a constructor that receives as input a Temperature object, and a target unit. I only have to
include a check for null because an unwitting user could pass in a null temp object
/**
* Create Temperature object with given value and units.
*
* @param temp is a valid Temperature object
* @param unit is an integer representing the desired units
*/
public Temperature (Temperature temp, int unit) {
if (temp == null) {
throw new IllegalArgumentException ("Can't pass null as argument to constructor.");
}
// Call method to reuse boundary checks.
setUnit (unit);
setValue (temp.convert (unit));
}
-----That's it for Step 1 of this Lab.
Open questions (do you foresee any problems with setUnit()? i.e., after an object has been created and we
call this method.
Step 2: Create a sub class called RoomTemperature.
The first observation is that RoomTemperature has a different idea of getMinimumValue(). So we must
override this method:
----Public class RoomTemperature extends Temperature {
/**
* Return the minimum value for the Room Temperature object.
*/
public float getMinimumValue () {
if (unit == CELSIUS) {
return (float) 12.777;
} else if (unit == KELVIN) {
return 284.93777;
} else if (unit == FAHRENHEIT) {
return (float) 55.0;
}
// should never get here
throw new IllegalArgumentException (unit + " is an illegal Room Temperature Unit.");
}
}
----As I write this method, I realize that I don't like the hand-converted temperatures. If only I had a method
that could convert temperatures without having an object! I go back to the Temperature class, and add the
following method:
---/**
* Convert the object to the desired unit.
*
* @param unit the target temperature
* @return the temperature converted to the temperature scale.
*/
public static float convert (float srcValue, int srcUnit, int targetUnit) {
if ((targetUnit < MINTYPE) || (targetUnit > MAXTYPE)) {
throw new IllegalArgumentException (targetUnit + " is an illegal Temperature unit.");
}
if (targetUnit == srcUnit) return srcValue;
if (srcUnit == CELSIUS) {
if (targetUnit == KELVIN) return srcValue + (- getMinimumValue(CELSIUS));
if (targetUnit == FAHRENHEIT) return (srcValue*9/5)+32;
}
if (srcUnit == KELVIN) {
if (targetUnit == CELSIUS) return srcValue + getMinimumValue (CELSIUS);
if (targetUnit == FAHRENHEIT) return ((srcValue + getMinimumValue (CELSIUS))*9/5)+32;
}
if (srcUnit == FAHRENHEIT) {
if (targetUnit == CELSIUS) return (srcValue - 32)*5/9;
if (targetUnit == KELVIN) return ((srcValue - 32)*5/9) + (- getMinimumValue(CELSIUS));
}
throw new IllegalArgumentException (srcUnit + " is an illegal Temperature unit.");
}
---Now that I have this method, I can rewrite the existing convert (int targetUnit) to use this static method:
----/**
* Convert the object to the desired unit.
*
* @param targetUnit the target temperature unit
* @return the temperature converted to the temperature scale
*/
public float convert (int targetUnit) {
return convert (value, unit, targetUnit);
}
----Always remember to ensure that there is no duplicate logic in your code. Try to localize all decisions to a
specific place; this will make it easier to change in the future, and ensure that these methods will be
reusable.
We now go back to RoomTemperature and use this new static method.
----/**
* Return the minimum value for the Room Temperature object.
*/
public float getMinimumValue () {
return convert ((float) 55.0, FAHRENHEIT, unit);
}
----But there is a minefield ahead of us. I try to define the following method to use in RoomTemperature. The
intent is for this method to return the minimum value given a target. However, the compiler rejects this
method because in class Temperature I had declared a static method of the same exact signature.
-----/**
* Return the minimum value for the Room Temperature object given a unit.
*
* @param targetUnit the desired unit
*/
public float getMinimumValue (int targetUnit) {
return convert ((float) 55.0, FAHRENHEIT, targetUnit);
}
----So instead, if we ever need to have the same functionality, we will either have to redesign Temperature, or
write the following code:
convert (new RoomTemperature ().getMinimumValue(), targetUnit)
This is not appropriate because if someone did call getMinimumValue(targetUnit), the Temperature method
would be called, thus returning an incorrect result, as far as RoomTemperature is concerned. So, are we at a
stalemate? Let's consider our options.
1) A static method can only access static class attributes, so we can't place the minimum value
in a non-static class variable
2) We could make the static method non-static
3) We could have the static method create an instance
Let’s review our basic decision to make this Static method in the first place. One, we wanted to have a
method that would return the minimum temperature without forcing the user to create an object first. Two,
this has nothing to do with a particular object, but is associated with the class. Now however, as we realize
that there may be subclasses of Temperature, and each one may have their own minimum value, we must
drop both of these assumptions.
For example, if we define a Temperature temp object and accept that subclasses may be created, we
realize that each object must know its minimum temperature. In fact, this value is a relative one based upon
the specific needs of the subclasses. This may not be the strongest argument, but it convinces me to go back
and make the method getMinimumValue no longer a static method.
By dropping this method, we have to revisit the convert method we wrote earlier. Now that there is no
getMininumValue() method, we can write new methods for the specific temperature scales:
getMinimumCelsiusValue()
getMinimumFahrenheitValue()
getMinimumKelvinValue()
These are each static for the class Temperature. Define these as follows and rewrite convert() to use them:
/**
* Return the minimum Celsius value.
*/
public static float getMinimumCelsiusValue() {
return (float) -273.16;
}
/**
* Return the minimum Fahrenheit value.
*/
public static float getMinimumFahrenheitValue() {
return (getMinimumCelsiusValue()*9/5)+32;
}
/**
* Return the minimum Kelvin value.
*/
public static float getMinimumKelvinValue() {
return 0;
}
As you compile RoomTemperature, however, it won’t work because we need to have a default constructor
in the Temperature class (demanded by Java). This is an important realization for you. If you ever design a
class that someone might want to subclass, you need to have a default constructor. For now, we add the
following default constructor to Temperature. To ensure that it is only used by subclasses, we make it
protected. In Temperature, add the following method:
protected Temperature () {}
Now we have a compiled RoomTemperature class. Let’s add the following methods for RoomTemperature
objects to return their minimum value:
/**
* Return the minimum value for the Room Temperature object.
*/
public float getMinimumValue () {
return convert ((float) 55.0, FAHRENHEIT, unit);
}
/**
* Return the minimum value for the Room Temperature object given a unit.
*
* @param targetUnit the desired unit
*/
public float getMinimumValue (int targetUnit) {
return convert ((float) 55.0, FAHRENHEIT, targetUnit);
}
Similarly, we add a MaximumValue:
/**
* Return maximum value for Room Temperature objects
*/
public float getMaximumValue () {
return convert ((float) 82.0, FAHRENHEIT, unit);
}
Because RoomTemperature objects have bounded values, we must override the default setValue() method
that we would have inherited from Temperature. Note that we still want the check for minimum value to
work as it did before, so we use the ability in java to directly invoke a method as implemented by a
superclass even if a subclass had overrided the method. This is a safe way to avoid copying code:
/**
* Room temperatures have maximum value.
*/
public void setValue (float value) {
if (value > getMaximumValue()) {
throw new IllegalArgumentException (value + " is too high for Room Temperature (" +
getUnitString(unit) + ")");
}
// Now that has been approved for maximum value, verify minimum value
super.setValue(value);
}
Using the super.method() invocation, the specific method is invoked in the ancestral class where the
method was inherited from. The constructor is not inherited in the RoomTemperature class, so we need to
define the constructor. Fortunately, we can use the super directive to reuse the constructors.
/**
* Default Constructor.
*/
public RoomTemperature (float value, int unit) {
super(value, unit);
}
Let’s add some code to the main method in Temperature:
RoomTemperature rt = new RoomTemperature (70, Temperature.FAHRENHEIT);
System.out.println (rt);
Note how this reuses the Temperature.toString() method to do its work.
Let’s try to set the temperature to 200 degrees:
RoomTemperature rt = new RoomTemperature (200, Temperature.FAHRENHEIT);
System.out.println (rt);
When you run now, you get the following error/exception message:
java.lang.IllegalArgumentException: 200.0 is too high for Room Temperature (Fahrenheit)
at common.RoomTemperature.setValue(RoomTemperature.java:49)
at common.Temperature.<init>(Temperature.java:63)
at common.RoomTemperature.<init>(RoomTemperature.java:25)
at common.Temperature.main(Temperature.java:276)
at symantec.tools.debug.MainThread.run(Agent.java:56)
Application terminated
We can compare a Temperature object with a RoomTemperature object, because the method is inherited.
But, let’s show some of the dangers you could encounter. Imagine if you decided to override the compareTo
method in RoomTemperature, and you made a mistake:
/**
* Compares two temperatures.
*/
public int compareTo(Temperature anotherTemp) {
// First convert argument to our units.
float localValue = anotherTemp.convert (unit);
if (value <= localValue) return -1;
if (value > localValue) return +1;
// ERROR!!
return 0; // Match!
}
Now, add the following code to main in Temperature:
RoomTemperature rt = new RoomTemperature (72, Temperature.FAHRENHEIT);
System.out.println (rt);
Temperature t2 = new Temperature (72, Temperature.FAHRENHEIT);
System.out.println ("rt compared to t2:" + rt.compareTo(t2));
System.out.println ("t2 compared to rt:" + t2.compareTo(rt));
Now from this code fragment, you get the following output:
rt compared to t2:-1
t2 compared to rt:0
Why did this happen? Well, the first invocation “rt.compareTo(t2)” causes the code in RoomTemperature
to be invoked (the one that has the defect). The second invocation “t2.compareTo(rt)” causes the code in
Temperature to be invoked. Remember how the object-oriented paradigm centers around sending messages
to objects? The compiler lets you be very specific about the methods being called. What if we change the
code fragment above by changing the way in which t2 is created:
Temperature t2 = new RoomTemperature (72, Temperature.FAHRENHEIT);
This is a valid statement. t2 will now contain a RoomTemperature object. What happens to the
System.out.fragment below:
System.out.println ("rt compared to t2:" + rt.compareTo(t2));
System.out.println ("t2 compared to rt:" + t2.compareTo(rt));
The output is:
rt compared to t2:-1
t2 compared to rt:-1
Thus when I define a variable “Temperature t2” I am actually creating a placeholder for an object of class
Temperature, or perhaps a subclass of Temperature. When the compiler sees the statement
“t2.compareTo(rt)” it interprets this to mean, send the “compareTo” message to the object refered to by t2.
As a side note, we have already seen how we had to cast double values as floats for the compiler to work
properly. You can also do this with objects; but be careful.
The following code fragment shows a valid “downcast”. That is, when you cast an object from a class to its
subclass. You should only do this when you know the object in question actually belongs to that class. Here
is a code fragment:
Temperature t2 = new RoomTemperature (72, Temperature.FAHRENHEIT);
RoomTemperature rt2 = (RoomTemperature) t2;
This will work properly. If, however, t2 had not been a member of RoomTemperature, a runtime exception
would have been generated:
Temperature t2 = new Temperature (72, Temperature.FAHRENHEIT);
RoomTemperature rt2 = (RoomTemperature) t2;
Note the difference in execution. This is a dynamic exception.
java.lang.ClassCastException
at common.Temperature.main(Temperature.java:283)
at symantec.tools.debug.MainThread.run(Agent.java:56)
Download