The TimeUnit enum takes the approach I used of naming each conversion method explicitly for what the target scale was (such as
toFahrenheit
). In my implementation, I achieved this by defining one method for each scale being converted to with that target scale's name and then switching on the enum instance's own values to determine what the source scale was. The appropriate case
matching the source scale handled the conversion to the atget scale prescribed by the method name.Guava's
CaseFormat
enum approach is orthogonal to this approach. CaseFormat
implements a single, generic method called to
that switches on the thing being converted to (the target) rather than switching on the source thing. The source item is already known because each value in the enum overrides its own "to" (or "convertTo") method (each enum value has its own method, so these overridden methods are often referred to as per-instance methods). The advantage of this approach is that the switching is done on the target rather than on the source. Although switching must still be done, it does feel more appropriate in general to switch on the target than on the source. This second approach allows polymorphism to be used to inherently "know" which source scale is being used. In the next part of this post, I show my
TemperatureUnit.java
enum ported to use this alternative implementation style. It is shown next as TemperatureUnit2
. As with the previous post, I must issue a disclaimer and major caveat here: this code's primary intention is demonstration and it has NOT been thoroughly tested or vetted for correctness. In fact, the porting of the first example is likely to retain any errors in it and might even introduce some new ones! The point of the code is to demonstrate an alternative approach to implementing these conversions.TemperatureUnit2.java
package dustin.examples; package dustin.examples; import java.math.BigDecimal; import java.math.RoundingMode; /** * <p>Enum representing different temperature scales * (<span style="font-weight: bold; color: red;">NOT</span> ready for production * -see <a href="#warning">warning</a>).</p> * * <p><span style="font-weight: bold; color: red;">WARNING:</span> * <a name="warning">This class has</a> * <span style="font-weight: bold; color: red;">NOT</span> been adequately * tested and some conversions are likely to not be properly coded. This * example is intended for demonstrative purposes only.</p> */ public enum TemperatureUnit2 { /** Celsius, used by most of the world's population. */ CELSIUS(new BigDecimal("0"), new BigDecimal("100"), '\u00B0' + "C", "Anders Celsius") { /** * {@inheritDoc} * * <p>The source temperature scale in this case is Celsius.</p> */ @Override public BigDecimal convertTo( final TemperatureUnit2 targetTemperatureScale, final BigDecimal sourceTemperature) { BigDecimal target = null; switch (targetTemperatureScale) { case CELSIUS : target = sourceTemperature; break; case FAHRENHEIT : target = sourceTemperature.multiply(NINE).divide(FIVE).add(THIRTY_TWO); break; case KELVIN : target = sourceTemperature.add(KELVIN.freezingPoint); break; case RANKINE : target = sourceTemperature.add(NINE.divide(FIVE)).add(THIRTY_TWO).add(RANKINE_FAHRENHEIT_DELTA); break; } return target; } }, /** Fahrenheit, commonly used in the United States. */ FAHRENHEIT(new BigDecimal("32"), new BigDecimal("212"), '\u00B0' + "F", "Daniel Gabriel Fahrenheit") { /** * {@inheritDoc} * * <p>The source temperature scale in this case is Fahrenheit.</p> */ @Override public BigDecimal convertTo( final TemperatureUnit2 targetTemperatureScale, final BigDecimal sourceTemperature) { BigDecimal target = null; switch (targetTemperatureScale) { case CELSIUS : target = sourceTemperature.subtract(THIRTY_TWO).divide(NINE.divide(FIVE)); break; case FAHRENHEIT : target = sourceTemperature; break; case KELVIN : target = sourceTemperature.add(RANKINE_FAHRENHEIT_DELTA).divide(NINE.divide(FIVE)); break; case RANKINE : target = sourceTemperature.add(RANKINE_FAHRENHEIT_DELTA); break; } return target; } }, /** Kelvin, commonly used in scientific endeavors. */ KELVIN(new BigDecimal("273.15"), new BigDecimal("373.15"), "K", "William Thomson, 1st Baron Kelvin") { /** * {@inheritDoc} * * <p>The source temperature scale in this case is Kelvin.</p> */ @Override public BigDecimal convertTo( final TemperatureUnit2 targetTemperatureScale, final BigDecimal sourceTemperature) { BigDecimal target = null; switch (targetTemperatureScale) { case CELSIUS : target = sourceTemperature.subtract(KELVIN.freezingPoint); break; case FAHRENHEIT : target = (sourceTemperature.subtract(KELVIN.freezingPoint)).multiply(NINE).divide(FIVE).add(THIRTY_TWO); break; case KELVIN : target = sourceTemperature; break; case RANKINE : target = sourceTemperature.multiply(NINE.divide(FIVE)); break; } return target; } }, /** Rankine temperature scale. */ RANKINE(new BigDecimal("491.67"), new BigDecimal("671.641"), '\u00B0' + "R", "William John Macquorn Rankine") { /** * {@inheritDoc} * * <p>The source temperature scale in this case is Rankine.</p> */ @Override public BigDecimal convertTo( final TemperatureUnit2 targetTemperatureScale, final BigDecimal sourceTemperature) { BigDecimal target = null; switch (targetTemperatureScale) { case CELSIUS : target = (sourceTemperature.subtract(THIRTY_TWO).subtract(RANKINE_FAHRENHEIT_DELTA)).divide(NINE.divide(FIVE), 2, RoundingMode.HALF_UP); break; case FAHRENHEIT : target = sourceTemperature.subtract(RANKINE_FAHRENHEIT_DELTA); break; case KELVIN : target = sourceTemperature.divide(NINE.divide(FIVE), 2, RoundingMode.HALF_UP); break; case RANKINE : target = sourceTemperature; break; } return target; } }; /** Freezing point of water for each temperature scale. */ private BigDecimal freezingPoint; /** Boiling point of water for each temperature scale. */ private BigDecimal boilingPoint; /** Units by which this temperature scale is expressed. */ private String units; /** Name of person that this temperature scale is named for. */ private String namedFor; private static final BigDecimal FIVE = new BigDecimal("5"); private static final BigDecimal NINE = new BigDecimal("9"); private static final BigDecimal THIRTY_TWO = new BigDecimal("32"); private static final BigDecimal KELVIN_CELSIUS_DELTA = new BigDecimal("273"); private static final BigDecimal RANKINE_FAHRENHEIT_DELTA = new BigDecimal("459.67"); /** * Constructor for TemperatureUnit that accepts key characteristics of each * temperature scale. * * @param newFreezingPoint Freezing point for this temperature scale. * @param newBoilingPoint Boiling point for this temperature scale. * @param newUnits Units of measurement for this temperature scale. * @param newNamedFor Name of person after which temperature scale was named. */ TemperatureUnit2( final BigDecimal newFreezingPoint, final BigDecimal newBoilingPoint, final String newUnits, final String newNamedFor) { this.freezingPoint = newFreezingPoint; this.boilingPoint = newBoilingPoint; this.units = newUnits; this.namedFor = newNamedFor; } /** * Conversion method to be implemented by each type of temperature scale. * * @param targetTemperatureScale Temperature scale to convert to. * @param sourceTemperature Temperature value to be converted. * @return Value of temperature in target temperature scale that corresponds * to provided value for source temperature scale; may be null if no * match can be calculated. */ public abstract BigDecimal convertTo( final TemperatureUnit2 targetTemperatureScale, final BigDecimal sourceTemperature); /** * Provide the freezing point of water for this temperature scale. * * @return Freezing point of this temperature scale. */ public BigDecimal getFreezingPoint() { return this.freezingPoint; } /** * Provide the boiling point of water for this temperature scale. * * @return Boiling point of this temperature scale. */ public BigDecimal getBoilingPoint() { return this.boilingPoint; } /** * Unit of measurement for this temperature scale. * * @return Unit of measurement for this temperature scale. */ public String getUnits() { return this.units; } /** * Provide the name of the person for which this temperature scale was named. * * @return Name of person for which this temperature scale was named. */ public String getNamedFor() { return this.namedFor; } }
The more generic approach employed by
CaseFormat
as implemented here is roughly the same code as the first example, but with the methods on the enum handled differently. In my implementation of the TimeUnit
specific conversion method approach, I had the conversion methods written in general for the entire enum, named for each target they were converting to, and switching on whatever the source's value was. The CaseFormat
-like implementation simply twisted this approach sideways so that roughly the same number of switches and cases are required, but now the switching is on the target type rather than on the source type. Each type has its own version of the method. This does seem to more neatly encapsulate knowledge in each enum value's scope and does take advantage of polymorphism.The similarity in size of the source code for these two examples is shown in the next screen snapshot.
As the image shows,
TemperatureUnit2
is less than a kilobyte larger than TemperatureUnit
. The difference in terms of .class
files is more pronounced.Besides needing to move the individual
case
statements around when porting this code, another interesting observation was the need to change instance variable references to other enum value variables. In other words, while I could use this.freezingPoint
when switch on the source value, I needed to use the source's freezingPoint (KELVIN.freezingPoint
for example) instead when switching on the target value.With the
CaseFormat
-inspired generic method name being more pleasing in terms of encapsulating knowledge in each enum value and in terms of taking advantage of polymorphic behavior, one may ask why I did not use this approach in the first place. The answer is that I thought of it instead from a user perspective. As a developer, I slightly prefer the specifically named method. I think it's a tad more readable and it's certainly more concise. With that in mind, I used the TimeUnit
-inspired specifically named conversion method approach. One approach (the one covered in this post) allows the "source" value to know what it is already (via polymorphism) and have to ask and switch on the "target" value while the other approach (the one covered in the previous post) allows the "target" to be known via method name and has to ask and switch on the "source" value.As I stated in my response to the great feedback on my earlier post, I would have a difficult time arguing against either of these approaches. In my opinion, one approach is more elegant from the perspective of the developer writing the enum with conversion functionality while the other approach is more elegant and slightly more readable for the developer using the enum. Neither's disadvantage when compared to the other is particularly large and I can live with either approach.
The good news is that Java does not force me to choose from either style presented so far. I can use a hybrid of the two approaches that combines the specific conversion approach of
TimeUnit
with implementation using overridden methods on each enum value to employ polymorphism. What this means is that I can eliminate switch statements altogether because the specific conversion method implies the target unit and the enum value in which the method is overridden implies the source unit. This is shown in the next code listing (for TemperatureUnit3
).package dustin.examples; import java.math.BigDecimal; import java.math.RoundingMode; /** * <p>Enum representing different temperature scales * (<span style="font-weight: bold; color: red;">NOT</span> ready for production * -see <a href="#warning">warning</a>).</p> * * <p><span style="font-weight: bold; color: red;">WARNING:</span> * <a name="warning">This class has</a> * <span style="font-weight: bold; color: red;">NOT</span> been adequately * tested and some conversions are likely to not be properly coded. This * example is intended for demonstrative purposes only.</p> */ public enum TemperatureUnit3 { /** Celsius, used by most of the world's population. */ CELSIUS(new BigDecimal("0"), new BigDecimal("100"), '\u00B0' + "C", "Anders Celsius") { /** {@inheritDoc} */ @Override public BigDecimal convertToCelsius(final BigDecimal sourceTemperature) { return sourceTemperature; } /** {@inheritDoc} */ @Override public BigDecimal convertToFahrenheit(final BigDecimal sourceTemperature) { return sourceTemperature.multiply(NINE).divide(FIVE).add(THIRTY_TWO); } /** {@inheritDoc} */ @Override public BigDecimal convertToKelvin(final BigDecimal sourceTemperature) { return sourceTemperature.add(KELVIN.freezingPoint); } /** {@inheritDoc} */ @Override public BigDecimal convertToRankine(final BigDecimal sourceTemperature) { return sourceTemperature.add(NINE.divide(FIVE)).add(THIRTY_TWO).add(RANKINE_FAHRENHEIT_DELTA); } }, /** Fahrenheit, commonly used in the United States. */ FAHRENHEIT(new BigDecimal("32"), new BigDecimal("212"), '\u00B0' + "F", "Daniel Gabriel Fahrenheit") { /** {@inheritDoc} */ @Override public BigDecimal convertToCelsius(final BigDecimal sourceTemperature) { return sourceTemperature.subtract(THIRTY_TWO).divide(NINE.divide(FIVE)); } /** {@inheritDoc} */ @Override public BigDecimal convertToFahrenheit(final BigDecimal sourceTemperature) { return sourceTemperature; } /** {@inheritDoc} */ @Override public BigDecimal convertToKelvin(final BigDecimal sourceTemperature) { return sourceTemperature.add(RANKINE_FAHRENHEIT_DELTA).divide(NINE.divide(FIVE)); } /** {@inheritDoc} */ @Override public BigDecimal convertToRankine(final BigDecimal sourceTemperature) { return sourceTemperature.add(RANKINE_FAHRENHEIT_DELTA); } }, /** Kelvin, commonly used in scientific endeavors. */ KELVIN(new BigDecimal("273.15"), new BigDecimal("373.15"), "K", "William Thomson, 1st Baron Kelvin") { /** {@inheritDoc} */ @Override public BigDecimal convertToCelsius(final BigDecimal sourceTemperature) { return sourceTemperature.subtract(KELVIN.freezingPoint); } /** {@inheritDoc} */ @Override public BigDecimal convertToFahrenheit(final BigDecimal sourceTemperature) { return (sourceTemperature.subtract(KELVIN.freezingPoint)).multiply(NINE).divide(FIVE).add(THIRTY_TWO); } /** {@inheritDoc} */ @Override public BigDecimal convertToKelvin(final BigDecimal sourceTemperature) { return sourceTemperature; } /** {@inheritDoc} */ @Override public BigDecimal convertToRankine(final BigDecimal sourceTemperature) { return sourceTemperature.multiply(NINE.divide(FIVE)); } }, /** Rankine temperature scale. */ RANKINE(new BigDecimal("491.67"), new BigDecimal("671.641"), '\u00B0' + "R", "William John Macquorn Rankine") { /** {@inheritDoc} */ @Override public BigDecimal convertToCelsius(final BigDecimal sourceTemperature) { return (sourceTemperature.subtract(THIRTY_TWO).subtract(RANKINE_FAHRENHEIT_DELTA)).divide(NINE.divide(FIVE), 2, RoundingMode.HALF_UP); } /** {@inheritDoc} */ @Override public BigDecimal convertToFahrenheit(final BigDecimal sourceTemperature) { return sourceTemperature.subtract(RANKINE_FAHRENHEIT_DELTA); } /** {@inheritDoc} */ @Override public BigDecimal convertToKelvin(final BigDecimal sourceTemperature) { return sourceTemperature.divide(NINE.divide(FIVE), 2, RoundingMode.HALF_UP); } /** {@inheritDoc} */ @Override public BigDecimal convertToRankine(final BigDecimal sourceTemperature) { return sourceTemperature; } }; /** Freezing point of water for each temperature scale. */ private BigDecimal freezingPoint; /** Boiling point of water for each temperature scale. */ private BigDecimal boilingPoint; /** Units by which this temperature scale is expressed. */ private String units; /** Name of person that this temperature scale is named for. */ private String namedFor; private static final BigDecimal FIVE = new BigDecimal("5"); private static final BigDecimal NINE = new BigDecimal("9"); private static final BigDecimal THIRTY_TWO = new BigDecimal("32"); private static final BigDecimal KELVIN_CELSIUS_DELTA = new BigDecimal("273"); private static final BigDecimal RANKINE_FAHRENHEIT_DELTA = new BigDecimal("459.67"); /** * Constructor for TemperatureUnit that accepts key characteristics of each * temperature scale. * * @param newFreezingPoint Freezing point for this temperature scale. * @param newBoilingPoint Boiling point for this temperature scale. * @param newUnits Units of measurement for this temperature scale. * @param newNamedFor Name of person after which temperature scale was named. */ TemperatureUnit3( final BigDecimal newFreezingPoint, final BigDecimal newBoilingPoint, final String newUnits, final String newNamedFor) { this.freezingPoint = newFreezingPoint; this.boilingPoint = newBoilingPoint; this.units = newUnits; this.namedFor = newNamedFor; } /** * Conversion method to be implemented by each type of temperature scale for * converting from that temperature scale to the Celsius temperature scale. * * @param sourceTemperature Temperature value to be converted. * @return Value of temperature in Celsius temperature scale that corresponds * to provided value for source temperature scale; may be null if no * match can be calculated. */ public abstract BigDecimal convertToCelsius(final BigDecimal sourceTemperature); /** * Conversion method to be implemented by each type of temperature scale for * converting from that temperature scale to the Fahrenheit temperature scale. * * @param sourceTemperature Temperature value to be converted. * @return Value of temperature in Fahrenheit temperature scale that corresponds * to provided value for source temperature scale; may be null if no * match can be calculated. */ public abstract BigDecimal convertToFahrenheit(final BigDecimal sourceTemperature); /** * Conversion method to be implemented by each type of temperature scale for * converting from that temperature scale to the Kelvin temperature scale. * * @param sourceTemperature Temperature value to be converted. * @return Value of temperature in Kelvin temperature scale that corresponds * to provided value for source temperature scale; may be null if no * match can be calculated. */ public abstract BigDecimal convertToKelvin(final BigDecimal sourceTemperature); /** * Conversion method to be implemented by each type of temperature scale for * converting from that temperature scale to the Rankine temperature scale. * * @param sourceTemperature Temperature value to be converted. * @return Value of temperature in Rankine temperature scale that corresponds * to provided value for source temperature scale; may be null if no * match can be calculated. */ public abstract BigDecimal convertToRankine(final BigDecimal sourceTemperature); /** * Provide the freezing point of water for this temperature scale. * * @return Freezing point of this temperature scale. */ public BigDecimal getFreezingPoint() { return this.freezingPoint; } /** * Provide the boiling point of water for this temperature scale. * * @return Boiling point of this temperature scale. */ public BigDecimal getBoilingPoint() { return this.boilingPoint; } /** * Unit of measurement for this temperature scale. * * @return Unit of measurement for this temperature scale. */ public String getUnits() { return this.units; } /** * Provide the name of the person for which this temperature scale was named. * * @return Name of person for which this temperature scale was named. */ public String getNamedFor() { return this.namedFor; } }
Although I did not have a clear favorite between the implementation of
TemperatureUnit1
and TemperatureUnit2
, I do clearly favor the implementation of TemperatureUnit3
. There are no switch
statements with the source scale implied by the particular enum value on which the overridden method exists and the target scale implied by the name of that overridden method. In other words, the defining location of the method implies the source and the name of the method implies the target. That's slick. It also gives me my preferred client experience so that client code does not need to pass a target enum into a general method, but can instead call the appropriately named method.As a side note, this "hybrid" approach is how
TimeUnit
is implemented. The TimeUnit
implementation differs from the TemperatureUnit3
implementation in that TimeUnit
's method defined for the overall enum is NOT abstract, but instead throws an AbstractMethodError if invoked. The TemperatureUnit3
implementation makes the overall enum's methods explicitly abstract rather than providing an implementation at that level and throwing the error. I'm biased, but I prefer the approach in TemperatureUnit3
because it leads to a compile-time error if a particular enum value does not override the abstract method. One thing I would do to improve TemperatureUnit3
for real use is to check for null on all passed-in temperatures before attempting to perform calculations. TimeUnit
does not need to make this check because it accepts primitive long
s rather than reference types such as BigDecimal
.A Quick Diversion: Looking at the Bytecode
Before ending this post, a few observations can be made from looking at the generated bytecode. The source code files are roughly the same size for all three implementations as shown in the next screen snapshot.
Things get more interesting with the Java bytecode. The
TemperatureUnit2
and TemperatureUnit3
enums are very similar from a .class
file perspective. This is a reflection of the fact that both use per-instance methods in the enums. The existence of per-instance methods on those enums leads to the generated class files with $
in their name (such as TemperatureUnit2$5.class
and TemperatureUnit3$3.class
).Running javap on the "main" enum
.class
file for each of the three cases provides interesting output.As the screenshots of the
javap
output above show, the enums with per-instance methods (methods overridden on each value in the enum) feature synthetic methods as identified by the access$x000();
output (where 'x' is an integer).The
javap
tool can be run against the individual generated .class
files (the ones with $
in their name) to see what they are. This is shown in the next three screenshots, first for the TemperatureUnit
(which has one of these generated files), then for the TemperatureUnit2
(which has five of these generated files), and then for TemperatureUnit3
(which has four of these generated files).javap Demonstrates Switch in TemperatureUnit in Enum Leads to One Generated $.class File
javap Demonstrates Switch and Per-Instance Methods in TemperatureUnit2 Lead to Five Generated $.class Files
javap Demonstrates Per-Instance Methods in TemperatureUnit3 Lead to Four Generated $.class Files
The three alternative implementations of enums that perform conversions between temperature scales lead to different results in the bytecode. The original implementation,
TemperatureUnit
, had no per-instance methods and its only conversion method was a single method for the entire enum that used switch. Because it had one switch and no per-instance methods, it only had one extra generated $.class
file.The second of the three implementations,
TemperatureUnit2
, uses per-instance methods but still requires a switch. This results in five extra generated $.class
files, four for the four per-instance methods and one for the switch
still needed. The third of the three implementations, TemperatureUnit3
, requires no switch statement and has four per-instance methods, resulting in four generated $.class
files. Even though the third implementation actually has four overridden methods per instance, it is the number of instances with overridden methods that determines how many generated $.class
files there are.Conclusion
There is more than one approach that can be used to implement handy conversion functionality between values of an enum. Conversion methods are perfect for enums because enums tend to represent finite ranges of data and so it is often possible to convert from one type in that range to another type in that range. With any of the discussed approaches, the logic is encapsulated within the same enum definition that it affects.
1 comment:
Interesting, this approach is very similar to a commercial implementation of Unit-API I developed a little over a year ago for a leading telco.
Post a Comment