Read more at jamessimone.net, or return to the homepage





Enum Apex Class Gotchas

The abstract enum class in Apex can be very helpful as a class-like object, but there are a few things you should keep in mind to avoid getting bitten by the use of enums. I was bitten hard by the use of enums recently, so I’d like to go a bit into depth on these shady, ill-documented characters; their usages, where they shine, and where you too might get into gray areas using them.

What are enums? That’s a tricky question to answer. Here's the official documentation description:

An enum is an abstract data type with values that each take on exactly one of a finite set of identifiers that you specify. Enums are typically used to define a set of possible values that don’t otherwise have a numerical order, such as the suit of a card, or a particular season of the year.

The easy way to think of them are compile-time "constants" that can be statically typed. They’re sort of like a list of static strings, in that sense, and they obey some interesting rules in addition to the basics:

Enums can be defined within a class or in a separate .cls file with the following notation:

1link//or global

2linkpublic enum ExampleEnum { FirstValue, SecondValue }

All enums have access to two methods: name() and ordinal() - name simply returns the string version of your enum instance, ordinal returns its index in the list.

Enums are sealed; you can’t add further methods or attributes to an enum. In other words ... they’re sort of a poor man’s class. The latest version of Java actually offers up a different take on this specifically for POJOs (plain old Java objects, sometimes called "beans" as well ... ): records. I mention this specifically because Enums, like these new Record types, are a derivative of the plain Object class; they just can't be tested for using something like instanceof (try it yourself in Apex: System.debug(LoggingLevel.ERROR instanceof Enum); will sadly fail to compile), yet they do have hidden implementations of the basic Object methods, hashCode() and equals().

The rather excellent documentation on using custom types as Map keys or within Sets is for sure worth a read if you're looking to understand why these methods matter; it's also particularly important if you're looking to implement custom sorting using the Comparable interface in Apex

So why do people like to use enums? They offer better type-safety than the use of static strings. For another, they self-encapsulate their data; while the use of a static string potentially says something about what you’re interacting with, in reality the VALUE behind that string can change - either with reassignment, if the variable isn’t listed as final, or simply by changing the code.

Plus, in an if/else or switch statement, testing against an enum’s value gives the developer the pleasure of understanding immediately the statement being evaluated at hand. You don’t have to go look up the value of that string - it’s right there in front of you! It’s a great mechanism for capturing certain known conditions; while the docs use the example of Seasons, and cardinal directions, perhaps a better example might be types of errors returned from an API (if they come in string form ...).

So ... that all seems pretty good, actually. What are the downsides to enums, then?

linkUsing Enums In Data Classes

For one — you might be tempted (as I was) to use enums in other languages when communicating to Salesforce via API. Spoiler alert: that’s not gonna work. You CAN include an enum (or even a list of enum values!) as a property on an object being used in an Apex endpoint:

1linkpublic enum Season { WINTER, SPRING, SUMMER, FALL }

2link

3linkpublic class DataObject {

4link public Integer MyProperty { get; set }

5link public Season MySeason { get; set; }

6link}

7link

8link//... in your web service

9link@HttpPost

10linkglobal static String post(DataObject data) {

11link //.. do something now that your object's been correctly deserialized

12link}

13link

What you can't, under any circumstances do, is try to send an enum to Salesforce to represent that the corresponding MySeason on the data object. You have to send a string instead. Otherwise your service is going to throw an error trying to deserialize your object — ouch. The reason for that will become abundantly clear as I walk you through this next part - the danger of using enums as key values in Map instances with Salesforce.

linkEnums in Batchable & Queueable Apex

When can we reference prior values in a new execution context? Within Batchable and Queueable Apex! Let’s dive in to a simple example (one of the reasons I loathe Batchable Apex — this "simple" example takes up quite a bit of real estate):

I probably account for 300+ of the (at this moment) 541 views of this question on the Salesforce Stack Exchange, which first alerted me to this issue several years ago. I hadn't personally been bitten by this particular issue; at the time I was writing a custom equality library for Apex and I was trying to figure out if there was a graceful way to handle Enums.

classes/EnumBatchableExample.cls
1linkpublic class EnumBatchableExample implements Database.Batchable<SObject> {

2link

3link public enum Direction { NORTH, SOUTH, EAST, WEST }

4link

5link public class TestIterable implements Iterable<SObject> {

6link public Iterator<SObject> Iterator() {

7link return new List<SObject>().iterator();

8link }

9link }

10link

11link @testVisible private static Integer firstHashCode;

12link @testVisible private static Integer secondHashCode;

13link @testVisible private static Map<Direction, Integer> directionToNumber = new Map<Direction, Integer>();

14link

15link public Iterable<SObject> start(Database.BatchableContext bc) {

16link System.debug('Starting EnumBatchableExample');

17link

18link Direction north = Direction.NORTH;

19link Direction south = Direction.SOUTH;

20link firstHashCode = north.hashCode();

21link secondHashCode = south.hashCode();

22link directionToNumber.put(north, firstHashCode);

23link directionToNumber.put(south, secondHashCode);

24link

25link System.debug('String version of north: ' + String.valueOf(Direction.NORTH));

26link System.debug('String verison of south: ' + String.valueOf(Direction.SOUTH));

27link System.debug('String verison of this: ' + String.valueOf(this));

28link

29link return new TestIterable();

30link }

31link

32link public void execute(Database.BatchableContext bc, List<SObject> records) {}

33link

34link public void finish(Database.BatchableContext bc) {

35link System.debug('Current map values: ' + directionToNumber);

36link System.debug('EnumBatchableExample finished');

37link }

38link}

And then the tests:

classes/EnumBatchableExampleTests.cls
1link@isTest

2linkprivate class EnumBatchableExampleTests {

3link static Integer northHashcode = EnumBatchableExample.Direction.NORTH.hashCode();

4link static Integer southHashcode = EnumBatchableExample.Direction.SOUTH.hashCode();

5link

6link static EnumBatchableExample.Direction north = EnumBatchableExample.Direction.NORTH;

7link static EnumBatchableExample.Direction south = EnumBatchableExample.Direction.SOUTH;

8link

9link @TestSetup

10link static void setup() {

11link //Ids consistently work as map keys ... is that because

12link //the hashCode is stable between execution contexts?

13link insert new Account(Name = 'EnumBatchableTest');

14link }

15link

16link @isTest

17link static void it_should_retain_hashcode_references() {

18link Account acc = [SELECT Id FROM Account];

19link System.debug('Account Id\'s hashCode for this run: ' + ((Object)acc.Id).hashCode());

20link

21link Database.Batchable<SObject> batchable = new EnumBatchableExample();

22link System.debug('Batchable\'s hashCode for this run: ' + ((Object)batchable).hashCode());

23link System.debug('Custom\'s hashCode for this run: ' + ((Object)new CustomHashCode()).hashCode());

24link

25link Test.startTest();

26link Database.executeBatch(batchable);

27link Test.stopTest();

28link

29link System.assertEquals(northHashcode, EnumBatchableExample.firstHashCode);

30link System.assertEquals(southHashcode, EnumBatchableExample.Direction.SOUTH.hashCode());

31link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(north));

32link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(south));

33link }

34link

35link @isTest

36link static void it_should_retain_hashcode_references_again() {

37link Account acc = [SELECT Id FROM Account];

38link System.debug('Account Id\'s hashCode for this run: ' + ((Object)acc.Id).hashCode());

39link

40link Database.Batchable<SObject> batchable = new EnumBatchableExample();

41link System.debug('Batchable\'s hashCode for this run: ' + ((Object)batchable).hashCode());

42link System.debug('Custom\'s hashCode for this run: ' + ((Object)new CustomHashCode()).hashCode());

43link

44link Test.startTest();

45link Database.executeBatch(batchable);

46link Test.stopTest();

47link

48link System.assertEquals(northHashcode, EnumBatchableExample.firstHashCode);

49link System.assertEquals(southHashcode, EnumBatchableExample.Direction.SOUTH.hashCode());

50link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(north));

51link System.assertEquals(true, EnumBatchableExample.directionToNumber.containsKey(south));

52link }

53link

54link private class CustomHashCode {

55link public Integer hashCode() {

56link return 1;

57link }

58link }

59link}

Both of these tests pass, no problem, and there's no way to "force" the issue because with both Batchable & Queueable Apex, you can't actually test the recursion of these jobs within tests. That said, I can show you the results:

Apex enum debug log

There's a lot to go through:

Intuitively, we have to expect that ".equals(Object o)" is overridden on the SObject class. You can find more tantalizing info about how exactly SObject equality works in one very specific place in the Apex Developer Guide: Understanding Expression Operators. Look for the keywords "Equality Operator", which in tantalizing brevity details what happens when you call "==" on something within Apex. There's also a great ribbing on the SOQL team, for good measure.

linkFeeling A Little Enum

TL;DR, what have we learned?

1linkpublic class MyClass {

2link public MyClass(Enum someEnum) {

3link //some detecting of which enum it is here, I guess

4link }

5link}

That's all for now, folks!

The original version of Apex Enum Class Gotchas can be read on my blog.

Using Enums In Data ClassesEnums in Batchable & Queueable ApexFeeling A Little Enum

Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests



Read more tech articles