A Pattern for Extending Java Enums

A Pattern for Extending Java Enums

By Lance Finney, OCI Principal Software Engineer

November 2012


Introduction

java.lang.Enum was added to Java™ in Java SE 5 in order to provide a type-safe restricted list of constants. Java's enums are more flexible than the enumeration constructs in many other languages in that each enum instance is a full-blown Object, with the ability to implement properties and logic.

However, there are some areas in which Java's enums are restricted - enum instances implicitly extend java.lang.Enum, are implicitly final, and cannot be defined using generics. This means that a developer cannot directly create an enum that contains all the instances of another, cannot inherit logic from an abstract parent, and cannot be typed for generics.

There is a petition to modify Java in future versions to support these features. In the meantime, we need to use workarounds. This article presents a pattern that can be used to simulate multiple enum types inheriting behavior from a common abstract superclass.

Setting up the Example

Imagine that you are writing an application to model solar systems, in which basic data about each celestial body would be used to calculate other physical and orbital parameters. For example, you might start with the mass, radius, and semi-major axis of the star, some planets, and some moons to calculate the length of planets' and moons' orbital periods and the surface gravity of all of the bodies. Below is some code that might be used to calculate them:

Common Superclass

  1. package com.ociweb.sett.initial;
  2.  
  3. /**
  4.  * A common abstract superclass for celestial bodies.
  5.  */
  6. public class CelestialBody {
  7. /**
  8.   * The Gravitational Constant, used for calculations: 6.67300e-11
  9.   * m³/(kg·s²).
  10.   */
  11. public static final double GRAVITATIONAL_CONSTANT = 6.67300e-11;
  12.  
  13. private final String name;
  14. private final double mass; // mass in kg
  15. private final double radius; // radius in m
  16. private Satellite[] satellites;
  17.  
  18. protected CelestialBody(String name, double mass, double radius) {
  19. this.name = name;
  20. this.mass = mass;
  21. this.radius = radius;
  22. }
  23.  
  24. /**
  25.   * @return the surface gravity, in m/s²
  26.   */
  27. public double getSurfaceGravity() {
  28. return GRAVITATIONAL_CONSTANT * mass / Math.pow(radius, 2.0);
  29. }
  30.  
  31. /**
  32.   * @return the mass of this celestial body, in kg
  33.   */
  34. public final double getMass() {
  35. return mass;
  36. }
  37.  
  38. /**
  39.   * @param satellites an array containing the satellites which orbit this
  40.   * celestial body, or null if this celestial body has no
  41.   * satellites
  42.   */
  43. public void setSatellites(Satellite[] satellites) {
  44. this.satellites = satellites;
  45. }
  46.  
  47. /**
  48.   * @return an array containing the satellites which orbit this celestial
  49.   * body, or null if this celestial body has no satellites
  50.   */
  51. public final Satellite[] getSatellites() {
  52. return satellites;
  53. }
  54.  
  55. /**
  56.   * @return the name of this celestial body
  57.   */
  58. public final String getName() {
  59. return name;
  60. }
  61. }

Definition of Satellites (Planets and Moons)

  1. package com.ociweb.sett.initial;
  2.  
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.concurrent.TimeUnit;
  6.  
  7. /**
  8.  * A class representing a generic satellite (planet, moon, etc.).
  9.  */
  10. public final class Satellite extends CelestialBody {
  11. private final CelestialBody parentBody;
  12. private final double semiMajorAxis; // in m
  13.  
  14. public Satellite(String name, CelestialBody parentBody, double mass,
  15. double radius, double semiMajorAxis) {
  16. super(name, mass, radius);
  17. this.parentBody = parentBody;
  18. this.semiMajorAxis = semiMajorAxis;
  19. }
  20.  
  21. /**
  22.   * @return the celestial body around which this celestial body orbits
  23.   */
  24. public CelestialBody getParentBody() {
  25. return parentBody;
  26. }
  27.  
  28. /**
  29.   * @return the orbital period, in seconds
  30.   */
  31. public double getOrbitalPeriod() {
  32. return 2 * Math.PI * Math.sqrt(Math.pow(semiMajorAxis, 3.0) /
  33. (GRAVITATIONAL_CONSTANT * parentBody.getMass()));
  34. }
  35.  
  36. /**
  37.   * Prints a summary of the calculated orbital and physical characteristics
  38.   * of this class and any of its own satellites.
  39.   */
  40. public void printSummary() {
  41. System.out.println("Summary for " + getParentage());
  42.  
  43. long orbitalPeriod = (long) getOrbitalPeriod();
  44. System.out.printf("Orbital Period: %1$d hours/%2$d days\n",
  45. TimeUnit.SECONDS.toHours(orbitalPeriod),
  46. TimeUnit.SECONDS.toDays(orbitalPeriod));
  47.  
  48. System.out.printf("Surface Gravity: %1$e m/s²\n\n",
  49. getSurfaceGravity());
  50.  
  51. Satellite[] satellites = getSatellites();
  52. if (satellites != null) {
  53. for (Satellite child : satellites) {
  54. child.printSummary();
  55. }
  56. }
  57. }
  58.  
  59. /**
  60.   * Generates and returns a String listing all the orbital parents of this
  61.   * body. So, for the Earth's moon, it would return ", satellite of EARTH,
  62.   * satellite of SOL".
  63.   *
  64.   * @return a String showing orbital parentage
  65.   */
  66. private String getParentage() {
  67. List<CelestialBody> parents = new ArrayList<CelestialBody>();
  68. CelestialBody parentBody = this.parentBody;
  69. parents.add(parentBody);
  70. while (parentBody instanceof Satellite) {
  71. Satellite satelliteParent = (Satellite) parentBody;
  72. parentBody = satelliteParent.getParentBody();
  73. parents.add(parentBody);
  74. }
  75.  
  76. StringBuilder sb = new StringBuilder(getName());
  77. for (CelestialBody parent : parents) {
  78. sb.append(", satellite of ");
  79. sb.append(parent.getName());
  80. }
  81. sb.append(":");
  82.  
  83. return sb.toString();
  84. }
  85.  
  86. }

Main execution class that sets up relationships and prints the summary

  1. package com.ociweb.sett.initial;
  2.  
  3. /**
  4.  * The main class for this example, it prints a summary of the Solar System
  5.  * (using only the inner planets for simplicity).
  6.  */
  7. public class SolarSystem {
  8.  
  9. public static void main(String[] args) {
  10. CelestialBody sol = new CelestialBody("SOL", 1.98892e30, 6.955e8);
  11. Satellite mercury = new Satellite("MERCURY", sol, 0.3302e24, 2.4397e6,
  12. 57.91e9);
  13. Satellite venus = new Satellite("VENUS", sol, 4.869e24, 6.0518e6,
  14. 108.21e9);
  15. Satellite earth = new Satellite("EARTH", sol, 5.9742e24, 6.3781e6,
  16. 149.6e9);
  17. Satellite luna = new Satellite("LUNA", earth, 0.07349e24, 1.7381e6,
  18. 0.3844e9);
  19. earth.setSatellites(new Satellite[]{luna});
  20. Satellite mars = new Satellite("MARS", sol, 0.64185e24, 3.3962e6,
  21. 227.92e9);
  22. Satellite phobos = new Satellite("PHOBOS", mars, 1.072e16, 1.11e4,
  23. 9.38e6);
  24. Satellite deimos = new Satellite("DEIMOS", mars, 1.5e15, 6.2e4,
  25. 2.346e7);
  26. mars.setSatellites(new Satellite[]{phobos, deimos});
  27. sol.setSatellites(new Satellite[]{mercury, venus, earth, mars});
  28.  
  29. printSummary(sol);
  30. }
  31.  
  32. /**
  33.   * Prints a summary for the star's solar system.
  34.   *
  35.   * @param star the star to be examined
  36.   */
  37. public static void printSummary(CelestialBody star) {
  38. System.out.println("Summary for " + star.getName() + ":");
  39. System.out.printf("Surface Gravity: %1$e m/s²\n\n",
  40. star.getSurfaceGravity());
  41.  
  42. for (Satellite satellite : star.getSatellites()) {
  43. satellite.printSummary();
  44. }
  45. }
  46. }

Execution Result

  1. > Summary for SOL:
  2. > Surface Gravity: 2.743748e+02 m/s²
  3. >
  4. > Summary for MERCURY, satellite of SOL:
  5. > Orbital Period: 2111 hours/87 days
  6. > Surface Gravity: 3.701906e+00 m/s²
  7. >
  8. > Summary for VENUS, satellite of SOL:
  9. > Orbital Period: 5392 hours/224 days
  10. > Surface Gravity: 8.871392e+00 m/s²
  11. >
  12. > Summary for EARTH, satellite of SOL:
  13. > Orbital Period: 8766 hours/365 days
  14. > Surface Gravity: 9.799823e+00 m/s²
  15. >
  16. > Summary for LUNA, satellite of EARTH, satellite of SOL:
  17. > Orbital Period: 658 hours/27 days
  18. > Surface Gravity: 1.623304e+00 m/s²
  19. >
  20. > Summary for MARS, satellite of SOL:
  21. > Orbital Period: 16484 hours/686 days
  22. > Surface Gravity: 3.713369e+00 m/s²
  23. >
  24. > Summary for PHOBOS, satellite of MARS, satellite of SOL:
  25. > Orbital Period: 7 hours/0 days
  26. > Surface Gravity: 5.805905e-03 m/s²
  27. >
  28. > Summary for DEIMOS, satellite of MARS, satellite of SOL:
  29. > Orbital Period: 30 hours/1 days
  30. > Surface Gravity: 2.603928e-05 m/s²

The calculation of the surface gravity of each celestial body is performed in CelestialBody. Satellite has the calculation for the orbital period, along with much of the logic necessary to print the summary.

Unfortunately, SolarSystem is a very awkward class. Most of it involves setting up the definition of the SolarSystem and the relationship between the Sun, the planets, and the moons. Since we have a set number of planets and moons (ignoring for the purposes of this article the continued discovery of more moons), this seems to be a perfect place to introduce some enums:

package com.ociweb.sett.initial;
 
/**
 * A enumeration to define stars at the center of solar systems.
 */
public enum Star {
    SOL
}
package com.ociweb.sett.initial;
 
/**
 * An enumeration of the planets in our solar system. We only show the inner
 * planets, for simplicity.
 */
public enum Planet {
    MERCURY, VENUS, EARTH, MARS
}
package com.ociweb.sett.initial;
 
/**
 * An enumeration of the moons that orbit the Earth.
 */
public enum EarthMoon {
    LUNA
}
package com.ociweb.sett.initial;
 
/**
 * An enumeration of the moons that orbit Mars.
 */
public enum MarsMoon {
    PHOBOS, DEIMOS
}

Unfortunately, these two sets of code are independent: we have a type-safe grouping of stars, planets, and each set of moons in one spot, and we have logic that applies to all of these groupings in another spot. How do we combine them?

In an ideal world

In an ideal world, we would be able to combine the properties and logic we have for celestial bodies and satellites with their enumerations by having Star and Satellite extend CelestialBody with Planet, EarthMoon, and MarsMoon extending Satellite. That way, we would have our type-safe restricted set of planets or moons, and they would each get to share the property and behavior definitions that are common between them. Unfortunately, though, an enum type implicitly extends java.lang.Enum, so we can't have our enums extend Satellite.

Another ideal implementation might be to have an abstract Satellite enum that defines the properties and behavior that Planet, EarthMoon, and MarsMoon. Unfortunately, we are again stymied, because all enums are implicitly final.

Note: while this limitation is true for enums based on java.lang.Enum, it was not true for some third-party Enum implementations that pre-date Java 5. If you want to explore one of those, check out net.java.dev.reusablecomponents.lang.Enum.

Extending the Enum through Interfaces

Although we can't combine the enums and the CelestialBody logic in a form as easy as the ideal world implementations imagined above, we can still combine them using an interface and some inner classes.

Essentially, we separate the base property declarations from the calculation logic; we move the property declarations into interfaces and move concrete method implementations into helper classes.

Thus, instead of sharing a common abstract superclass that provides properties and logic, the enums will share a common Properties interface. There will then be a common inner implementation of the Properties interface that can be overriden. Additionally, we will move some of the logic into static utility delegate classes.

Updating the Example

CelestialBody

To see this in action, let's start with the new CelestialBody. In the initial version, this CelestialBody was a class with four property fields (namemassradius, and the satellites) and a method that calculated a physical property (getSurfaceGravity()). In the new version, it's an interface that includes and inner interface to expose those same four properties and a static utility inner class to calculate the physical property:

There's enough code here to need to look at the individual parts one-by-one. First, we'll look at the outer interface, CelestialBody. This is the interface that is implemented by enums like Star and extended by interfaces like Satellite.

  1. package com.ociweb.sett;
  2.  
  3. /**
  4.  * An interface for defining celestial bodies (start, planets, moons, etc.).
  5.  */
  6. public interface CelestialBody {
  7. /**
  8.   * The Gravitational Constant in units of m³/(kg·s²).
  9.   */
  10. public static final double GRAVITATIONAL_CONSTANT = 6.67300e-11;
  11.  
  12. Properties getProperties();
  13.  
  14. < ...snip inner interfaces and classes...>
  15. }

There's actually very little code here. We declare the gravitational constant here because it will be used by all of the children, but it could really be defined anywhere.

In contrast, getProperties() is key to this entire pattern. It provides access to the inner interface Properties, which we will look at next. This is not strictly necessary - we could combine the interfaces. However, Properties has lots of methods, and the inability to provide an abstract superclass for the child enums means that each of the children would have to implement all the methods. Since most of the implementations are identical, this would involve a significant amount of duplicated boilerplate code. By extracting those methods into a separate interface, we can provide a shared delegate implementation.

Through this method, clients of the SolarSystem API will be able to access all the necessary information about the relationships between bodies and the logic used to calculate the physical properties. We're partially decoupling the body definition from the physical attributes, so that we can use enums for the type-safe declarations without being hobbled too much by enum's restrictions.

Next, let's look at the Properties interface that is accessed through the getter mentioned above:

  1. interface Properties {
  2. /**
  3.   * @return the mass of this celestial body, in kg
  4.   */
  5. double getMass();
  6.  
  7. /**
  8.   * @return the radius of this celestial body, in m.
  9.   */
  10. double getRadius();
  11.  
  12. /**
  13.   * @return an array containing the satellites which orbit this
  14.   * celestial body, or null if this celestial body has no satellites
  15.   */
  16. Satellite[] getSatellites();
  17.  
  18. /**
  19.   * Returns the name of this celestial body. Implementation of this
  20.   * method can be provided by {@link Enum}.
  21.   *
  22.   * @return the name of this celestial body
  23.   */
  24. String name();
  25.  
  26. /**
  27.   * Prints a summary for the celestial body and any satellites it has.
  28.   */
  29. void printSummary();
  30. }

The methods declared here are mostly the methods exposed in the old version of CelestialBody, with the addition of moving printSummary() into here. In the old version, there wasn't a good way to share printing behavior between the different classes because Star and Planet weren't related. Now we can.

The Properties inner interface is represented by ImmutableProperties, a concrete immutable representation. The immutable representation is necessary because the Properties instance is a property of the enum instance, and enum instances must be immutable constants, by definition. The enum instances will be holding onto a Properties object. If a caller were able to modify the state of a Properties instance in such a way that it modified the version held by the enum, then two distinct copies of the same enum instance would have different state. That is unacceptable.

  1. /**
  2.  * An immutable representation of the properties of the {@link
  3.  * CelestialBody}. Immutability is necessary to honor the contract for
  4.  * enums.
  5.  */
  6. static final class ImmutableProperties implements Properties {
  7. private final double mass;
  8. private final double radius;
  9. private final String name;
  10. private final Satellite[] satellites;
  11.  
  12. public ImmutableProperties(String name, double mass, double radius,
  13. Satellite... satellites) {
  14. this.name = name;
  15. this.mass = mass;
  16. this.radius = radius;
  17. this.satellites = satellites == null ? null : satellites.clone();
  18. }
  19.  
  20. @Override
  21. public double getMass() {
  22. return mass;
  23. }
  24.  
  25. @Override
  26. public double getRadius() {
  27. return radius;
  28. }
  29.  
  30. @Override
  31. public Satellite[] getSatellites() {
  32. return satellites == null ? null : satellites.clone();
  33. }
  34.  
  35. @Override
  36. public String name() {
  37. return name;
  38. }
  39.  
  40. public void printSummary() {
  41. System.out.println("Summary for " + name() + ":");
  42. System.out.printf("Surface Gravity: %1$e m/s²\n\n",
  43. Util.calcSurfaceGravity(this));
  44.  
  45. if (satellites != null) {
  46. for (Satellite satellite : getSatellites()) {
  47. satellite.getProperties().printSummary();
  48. }
  49. }
  50. }
  51. }

There's not much to note about the code here, except for the cloning code used to defensively protect immutability, and the tree traversal behavior of printSummary().

The final section of CelestialBody is a static utility delegate class that we can use to calculate the surface gravity. In the old version, this was a simple instance method on the CelestialBody, but the separation of inheritance through enums from behavior through interfaces requires us to move it somewhere else.

  1. /**
  2.  * A static class that is used to simulate the logic that would be
  3.  * implemented in an abstract superclass for Satellite enums, if Java
  4.  * allowed such a thing.
  5.  */
  6. static class Util {
  7.  
  8. // private constructor to ensure static-only access
  9. private Util() {
  10. }
  11.  
  12. /**
  13.   * Calculates and returns the surface gravity of the given {@link
  14.   * CelestialBody}. The result is given in m/s².
  15.   *
  16.   * @param celestialBody the body whose surface gravity is being
  17.   * calculated
  18.   * @return the surface gravity, in m/s²
  19.   */
  20. public static double calcSurfaceGravity(Properties celestialBody) {
  21. return GRAVITATIONAL_CONSTANT * celestialBody.getMass() / Math.pow(
  22. celestialBody.getRadius(), 2.0);
  23. }
  24. }

Overall this version of CelestialBody is definitely unwieldly compared to the old version. However, by rewriting CelestialBody as an interface instead of a class, it can be used as part of the interface for our enum types. The way we will use this is to define other types by implementing this interface, with the properties decoupled to the Properties inner interface. Children enums of this pattern will implement the outer interface, CelestialBody, and delegate all requests for information about logic and relationships to the inner interface, Properties. The advantage of this is that we get all of the power of enums in the main class but flexibility of shared interface and behavior in the inner classes.

Star

Next, let's look at Star. In the initial version, it was just an enum that didn't have any of the properties or logic. Now, we can have that enum implement CelestialBody and use its ImmutableProperties implementation to get the properties, including an array of all of its Planets.

  1. package com.ociweb.sett;
  2.  
  3. /**
  4.  * An enumeration of the stars at the center of solar systems.
  5.  */
  6. public enum Star implements CelestialBody {
  7. SOL(1.98892e30, 6.955e8, Planet.values()); // singleton
  8.  
  9. private final Properties properties;
  10.  
  11. private Star(double mass, double radius, Satellite[] satellites) {
  12. this.properties = new ImmutableProperties(name(), mass, radius,
  13. satellites);
  14. }
  15.  
  16. public Properties getProperties() {
  17. return properties;
  18. }
  19.  
  20. }

This implementation allows us to combine three elements that were separated into multiple incompatible places before:

  1. A type-safe enumeration of all the stars (only one in this case). In the previous version, this was the only purpose of com.ociweb.sett.initial.Star.
  2. The physical properties (mass and radius) of each star. In the previous version, these were set in the constructor to com.ociweb.sett.initial.CelestialBody within com.ociweb.sett.initial.SolarSystem
  3. The relationship between a star and its planets. In the previous version, this was set by a mutator on com.ociweb.sett.initial.CelestialBody within com.ociweb.sett.initial.SolarSystem

At this point, we can already see some payoff for the added complexity we saw in com.ociweb.sett.CelestialBody.

Satellite

Next, let's look at the new Satellite, which is the most complicated of the new files, and which shows all parts of this pattern.

Satellite itself is now an interface that extends CelestialBody to add a reference to the parent body (as in the star around which a planet orbits or the planet around with a moon orbits). It contains an immutable Properties inner class of its own that implements the CelestialBody.Properties interface and adds satellite-specific logic.

There's enough code here to need to look at the individual parts one-by-one. First, we'll look at the outer interface, Satellite. This is the interface that is implemented by enums like Planet, EarthMoon, and MarsMoon.

  1. package com.ociweb.sett;
  2.  
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.concurrent.TimeUnit;
  6.  
  7. /**
  8.  * An interface representing a generic satellite (planet, moon, etc.). This
  9.  * interface is often implemented by {@link Enum} types. In order to simulate
  10.  * the behavior of abstract-class inheritance for those enums, this interface
  11.  * has inner classes and inner interfaces.
  12.  */
  13. public interface Satellite {
  14. /**
  15.   * @return the representation of all the values of the Satellite
  16.   */
  17. Properties getProperties();
  18.  
  19. /**
  20.   * Returns the parent body for this Satellite (as in the star around which
  21.   * this planet orbits).
  22.   *
  23.   * @return the parent body for this Satellite
  24.   */
  25. CelestialBody getParentBody();
  26.  
  27. < ...snip inner interfaces and classes...>
  28. }

The Satellite interface inherits getProperties() from its parent interface (included here as a reminder but not in the actual code) and adds one more: getParentBody().

getParentBody() allows bidirectional traversal from moon to star. Ideally, we could just add this logic to the Properties, but cannot due to the order of static initialization. We create each Satellite instance in the constructor of the parent (that is, the planets are initialized within the constructor for Sol, Phobos and Deimos are initialized within the constructor for Mars, etc.). This means that Sol is not fully created at the point that Mars is being initialized, and Mars is not fully created at the point that Phobos is being initialized. As a result, we cannot initialize a reference to the parent body when we create our Properties convenience object - the static initialization is simply not ready for that order of operation. Instead, we must postpone discovery of the parent body until after the structure of the solar system has been created. So that forces us to have the method getParentBody() be separate from the Properties in all the Satellite enum instances.

Now let's look at the concrete implementation of CelestialBody.Properties that Satellite has as an inner class. Again, this is an immutable implementation, and it defines all of the information about a Satellite we need to calculate the orbital period, the surface gravity, and print a full report:

  1. /**
  2.  * An immutable representation of the properties of the {@link Satellite}.
  3.  * Immutability is necessary to honor the contract for enums.
  4.  */
  5. static final class ImmutableProperties implements CelestialBody
  6. .Properties {
  7. private final Satellite satellite;
  8. private final double mass;
  9. private final double radius;
  10. private final double semiMajorAxis;
  11. private final String name;
  12. private final Satellite[] satellites;
  13.  
  14. public ImmutableProperties(Satellite satellite, String name,
  15. double mass, double radius, double semiMajorAxis,
  16. Satellite... satellites) {
  17. this.satellite = satellite;
  18. this.name = name;
  19. this.mass = mass;
  20. this.radius = radius;
  21. this.semiMajorAxis = semiMajorAxis;
  22. this.satellites = satellites == null ? null : satellites.clone();
  23. }
  24.  
  25. @Override
  26. public double getMass() {
  27. return mass;
  28. }
  29.  
  30. @Override
  31. public double getRadius() {
  32. return radius;
  33. }
  34.  
  35. @Override
  36. public Satellite[] getSatellites() {
  37. return satellites == null ? null : satellites.clone();
  38. }
  39.  
  40. @Override
  41. public String name() {
  42. return name;
  43. }
  44.  
  45. private CelestialBody getParentBody() {
  46. return satellite.getParentBody();
  47. }
  48.  
  49. /**
  50.   * Calculates and returns the orbital period of this {@link Satellite}
  51.   * which orbits the given parent body. The result is given in seconds.
  52.   *
  53.   * @param parentBody the body around which the satellite orbits
  54.   * @return the orbital period, in seconds
  55.   */
  56. public double getOrbitalPeriod(CelestialBody parentBody) {
  57. return 2 * Math.PI * Math.sqrt(Math.pow(semiMajorAxis, 3.0) /
  58. (GRAVITATIONAL_CONSTANT *
  59. parentBody.getProperties().getMass()));
  60. }
  61.  
  62. /**
  63.   * Prints a summary of the calculated orbital and physical
  64.   * characteristics of this {@link Satellite} and any of its own
  65.   * satellites.
  66.   */
  67. @Override
  68. public void printSummary() {
  69. System.out.println("Summary for " + getParentage());
  70.  
  71. CelestialBody parentBody = getParentBody();
  72. long orbitalPeriod = (long) getOrbitalPeriod(parentBody);
  73. System.out.printf("Orbital Period: %1$d hours/%2$d days\n",
  74. TimeUnit.SECONDS.toHours(orbitalPeriod),
  75. TimeUnit.SECONDS.toDays(orbitalPeriod));
  76.  
  77. System.out.printf("Surface Gravity: %1$e m/s²\n\n",
  78. CelestialBody.Util.calcSurfaceGravity(this));
  79.  
  80. if (satellites != null) {
  81. for (Satellite child : satellites) {
  82. child.getProperties().printSummary();
  83. }
  84. }
  85. }
  86.  
  87. /**
  88.   * Generates and returns a String listing all the orbital parents of
  89.   * this satellite.
  90.   * <p/>
  91.   * For example, for the Earth's moon, it would return
  92.   * <pre>", satellite of EARTH, satellite of SOL"</pre>.
  93.   *
  94.   * @return a String showing orbital parentage
  95.   */
  96. private String getParentage() {
  97. List<CelestialBody> parents = new ArrayList<CelestialBody>();
  98. CelestialBody parentBody = getParentBody();
  99. parents.add(parentBody);
  100. while (parentBody instanceof Satellite) {
  101. Satellite satelliteParent = (Satellite) parentBody;
  102. parentBody = satelliteParent.getParentBody();
  103. parents.add(parentBody);
  104. }
  105.  
  106. StringBuilder sb = new StringBuilder(name());
  107. for (CelestialBody parent : parents) {
  108. sb.append(", satellite of ");
  109. sb.append(parent.getProperties().name());
  110. }
  111. sb.append(":");
  112.  
  113. return sb.toString();
  114. }
  115. }

Much of this implementation is very similar to the ImmutableProperties implementation in CelestialBody. However, it also adds the ability to get the parent body (by delegating to the related Satellite object) and to calculate the orbital period from physical properties of the satellite and its parent. It also has a more complicated implementation of printSummary() to support the listing of parent relationship.

Planet

Now that we've completed the heavy lifting of understanding the Satellite interfaces and the inner classes that support it, let's look at Planet to see how it is used:

  1. package com.ociweb.sett;
  2.  
  3. /**
  4.  * An enumeration of the planets in our solar system. We only show the inner
  5.  * planets, for simplicity.
  6.  */
  7. public enum Planet implements Satellite {
  8. MERCURY(0.3302e24, 2.4397e6, 57.91e9),
  9. VENUS(4.869e24, 6.0518e6, 108.21e9),
  10. EARTH(5.9742e24, 6.3781e6, 149.6e9, EarthMoon.values()),
  11. MARS(0.64185e24, 3.3962e6, 227.92e9, MarsMoon.values());
  12.  
  13. final ImmutableProperties properties;
  14.  
  15. private Planet(double mass, double radius, double semiMajorAxis) {
  16. this(mass, radius, semiMajorAxis, null);
  17. }
  18.  
  19. private Planet(double mass, double radius, double semiMajorAxis,
  20. Satellite[] satellites) {
  21. this.properties = new ImmutableProperties(this, name(), mass, radius,
  22. semiMajorAxis, satellites);
  23. }
  24.  
  25. public ImmutableProperties getProperties() {
  26. return properties;
  27. }
  28.  
  29. @Override
  30. public CelestialBody getParentBody() {
  31. return Star.SOL;
  32. }
  33.  
  34. }

Here we see more of the power of this pattern. This enum defines the same four inner planets that the original version of Planet defined. However, now all of the configuration information for each of the planets is here (instead of built manually in the SolarSystem class), and we get the orbital calculation logic that is shared with other Satellites.

For example, Mercury and Venus are defined with their physical properties in their constructors (mass, radius, and semi-major axis). Earth and Mars are similar, except they also load their moons into the constructors (those moons are passed into the ImmutableProperties).

The only methods that need to be implemented here for the Satellite interface are getProperties() (this one method definition replaces four that would be needed otherwise) and getParentBody() (which has a simple implementation since each planet orbits the same parent).

EarthMoon and MarsMoon

Finally, let's look at the enums which share Satellite with Planet as an interface, but themselves represent satellites of planets:

 
  1. package com.ociweb.sett;
  2.  
  3. /**
  4.  * An enumeration of the moons that orbit the Earth.
  5.  */
  6. public enum EarthMoon implements Satellite {
  7. LUNA(0.07349e24, 1.7381e6, 0.3844e9);
  8.  
  9. final Properties properties;
  10.  
  11. private EarthMoon(double mass, double radius, double semiMajorAxis) {
  12. this.properties = new ImmutableProperties(this, name(), mass, radius,
  13. semiMajorAxis);
  14. }
  15.  
  16. public Properties getProperties() {
  17. return properties;
  18. }
  19.  
  20. @Override
  21. public CelestialBody getParentBody() {
  22. return Planet.EARTH.getProperties();
  23. }
  24.  
  25. }
  1. package com.ociweb.sett;
  2.  
  3. /**
  4.  * An enumeration of the moons that orbit Mars.
  5.  */
  6. public enum MarsMoon implements Satellite {
  7. PHOBOS(1.072e16, 1.11e4, 9.38e6),
  8. DEIMOS(1.5e15, 6.2e4, 2.346e7);
  9.  
  10. final Properties properties;
  11.  
  12. private MarsMoon(double mass, double radius, double semiMajorAxis) {
  13. this.properties = new ImmutableProperties(this, name(), mass, radius,
  14. semiMajorAxis);
  15. }
  16.  
  17. public Properties getProperties() {
  18. return properties;
  19. }
  20.  
  21. @Override
  22. public CelestialBody getParentBody() {
  23. return Planet.MARS.getProperties();
  24. }
  25.  
  26. }

These enums are very similar to Planet, but none of the moons defined here has its own satellite. However, it would be easy to add any such third layer of satellite by extending the pattern.

Finally, to pull this all together, we have the main class:

  1. package com.ociweb.sett;
  2.  
  3. /**
  4.  * The main class for this example, it prints a summary of the Solar System.
  5.  */
  6. public class SolarSystem {
  7.  
  8.     public static void main(String[] args) {
  9.         Star.SOL.getProperties().printSummary();
  10.     }
  11. }

This class produces the same printed output as the earlier version, but it is much, much simpler. Instead of defining all the properties and relationships and some of the printing behavior like the early version did, we can now just start with the Sun and let the definitions and logic in all the enums and interfaces do all the work for us.

Summary

With this thorough example, we see how we can use a combination of interfaces and utility classes to simulate having abstract enum implementations. With this pattern, we can share behavior across related enums while maintaining the type-safe restrictions that we get with enums.

It would be nice to have full support for extendible enums in Java, and perhaps that will come in future versions. In the meantime, this pattern might solve problems in your application.

References

secret