Background Image
TECHNOLOGIE

Enums Java pour un meilleur code

Paul Nienaber
Senior Consultant

March 21, 2025 | 5 Lecture minute

Les Enums existent depuis un certain temps, et ils sont principalement utilisés comme ils ont été conçus pour la première fois en C : Il s'agit d'un ensemble de valeurs distinctes, nommées, utilisées pour des choses telles que le branchement basé sur un cas de condition préalablement nommé et stocké. Mais les enums Java sont plus qu'un simple ensemble de valeurs distinctes avec des noms, et nous pouvons en tirer parti pour améliorer l'adhésion à une source unique de vérité dans notre code.

Les enums Java sont essentiellement des objets à part entière dotés d'un constructeur optionnel et de variables membres. Cette caractéristique unique, dont sont dépourvues les classes normales, constitue un idiome utile pour un code simple, évident et très facile à maintenir, lorsque nous voulons faire des choses comme manipuler des types de données basés sur un nom de type de chaîne ou même sur un nom de classe.

Scattered Code 

Far too often code ends up containing a set of handlers (code or simply metadata) as objects or plain methods, and a separate list allowing lookup or conditional calling of those entities.  Taking advantage of the `.values()` static method and the enum class's `static` block, we can reduce the lookup mapping and its separate target entities to a single source of truth so our code is both more readable and safer to modify.

enum DataMapper { 

  JSONBLOB("json", MyStruct.class, 

      (OpaqueDataContainer x) -> x.getJson()); 

  BASE64BINARY("base64", byte[].class, 

      (OpaqueDataContainer x) -> Base64.getDecoder().decode(x.getBase64)); 

 

  private static Map<String, DataMapper> externalTypeNameMap; 

  private final String externalTypeName; 

  private final Class<?> conversionResultType; 

  private final Function<OpaqueDataContainer, Object> mapper; 

 

  static { 

    ExternalTypeNameMap = new HashMap<>(); 

    For (DataMapper m : DataMapper.values()) { 

      ExternalTypeNameMap.put(m.externalTypeName, m); 

    } 

  } 

 

  private DataMapper( 

      String externalTypeName, 

      Class<?> conversionResultType, 

      Function<OpaqueDataContainer, Object> mapper) { 

    this.externalTypeName = externalTypeName; 

    this.conversionResultType = conversionResultType; 

    this.mapper = mapper; 

  } 

 

  Class<?> getConversionResultType() { 

    return conversionResultType; 

  } 

 

  Object getMappedValue(OpaqueDataContainer d) { 

    return mapper.apply(d); 

  } 

 

  DataMapper get(String externalTypeName) { 

    Return  

} 

For other mapper interfaces (e.g. providing a key for extracting the data), you can use other functor types from java.util.function, or write your own interface as needed. 

Note in the example that via an extra member `conversionResultType` you can trivially handle return type polymorphism coupled to the various mapping-input types with a cast in the calling code: 

OpaqueDataContainer d = getOpaquePolymorphicDataSomehow(); 

DataMapper m = DataMapper.get(d.getTypeName()); 

someOtherOverloadedHandler(d.getNativeType().cast(m.getMappedValue(d))); 

The .values()-based lookup table can also be used elegantly in simpler situations, for example a config parameter string lookup. 

enum MyConfigParam { 

  PEPSI("cola"), 

  ROOTBEER("rootbeer"); 

 

  private final String configString; 

  private static Map<String, MyConfigParam> configStringMap; 

 

  static { 

    configStringMap = new HashMap<>(); 

    for (MyConfigParam p : MyConfigParam.values()) { 

      configStringMap.put(p.configString, p); 

    } 

  } 

 

  static MyConfigParam getByConfigString(String configString) { 

    return configStringMap.get(configString); 

  } 

 

  @Override 

  public String toString() { 

    return configString; 

  } 

} 

This approach does come with some very minor caveats—which can be easily addressed... 

Implementation Details 

Variable Scope 

Enum definition parameter lists cannot reference static variables of the enum class itself, so constants and similar should most commonly be addressed by putting these values inside a containing class, whether another application class or simply a utility container class. 

Reuse, Reduce 

As the context for the enum declaration parameters isn’t procedural and Java lacks any facility to reuse expression or return values, a common gotcha here is that using the same value more than once in constructing an enum value requires duplicating the expression or call that produces it —when we were just working to avoid unnecessary code duplication.  It's easy to use a mapper-function factory lambda to allow reuse of data such as other enum property values (remember, we're avoiding multiple sources of truth).  If it's more appropriate you can use this for only some of your enum objects by providing and using an additional constructor.  Building on the first example above:

ENCODEDPOJO("binarypojoblob", Object[].class, 

    (Class<?> c) -> 

      (OpaqueDataContainer x) ->x.unpackPojoAs(c)); 

 

private DataMapper( 

    String externalTypeName, 

    Class<?> conversionResultType, 

    Function<Class<?>, Function<OpaqueDataContainer, Object>> mapperFactory) { 

  this.externalTypeName = externalTypeName; 

  this.conversionResultType = conversionResultType; 

  this.mapper = mapperFactory.apply(conversionResultType); 

} 

Compared to the regular constructor, here we instead use a factory lambda, allowing the constructor to provide the conversionResultType context which is then captured in the closure and eliminating the value being written out in multiple places. 

A Lint Trap (Recycle) 

A similar gotcha arises when you want to reuse an object or value in the handler Lambda.  This may be necessary either to avoid instantiating a parser or factory object every single time the `mapper` is called, or to avoid duplication of the same data or call expression. 

It’s tempting to address this need for value reuse by making the value a statically initialized static member in your outer class.  However, it's discouraged (and enforced by some linters) not to access a non-const static member of an outer class, and the member of the outer class also cannot be both const and programmatically derived (in its `static` block). It's simple to reuse your lambda factory interface to do this or create another parameter-less factory interface if you desire. 

FORMATTEDDATETIME("timestamp", Date.class, 

    (Class <?> ignored) -> { 

      DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 

      return (OpaqueDataContainer x) -> df.parse(x.getString()); 

    }); 

Here we’ve taken the same factory interface as before, ignoring the Class <?> parameter as it’s unused for the lambda-generation logic in the factory. 

Class Preservation

If you use class object as keys in a lookup table, and you dynamically load and unload any of those classes, you may have another problem:  Having them referenced as map keys stores a reference to the class and not its hashcode so the class cannot be unloaded until it's removed from the map.  You might consider strongly tying together the class unloading and enum lookup map updating code to enforce the consistency required for the unloads to work. 

Better Code 

With some simple but unorthodox use of enums, you can take maintenance tripwires out of your code, and make it easier to grok the whole picture of your converter handler or other functional mapping.  I don't know about you, but I don't plan on suffering from hardcoded lookup tables again if I can avoid it. 

Ready to enhance your code with innovative techniques? Reach out to Improving for expert guidance and support.

Technologie

Dernières réflexions

Explorez nos articles de blog et laissez-vous inspirer par les leaders d'opinion de nos entreprises.
Asset - Image 1 Why Successful Software Projects Require More Than Technical Skills
TECHNOLOGIE

Comment j'ai activé la livraison "Au moins une fois" de MQTT5 vers un sujet Kafka

Livraison des messages "au moins une fois" de MQTT5 à Kafka, garantissant la fiabilité pour la tolérance aux pannes et l'intégrité des données.