ArticleS
.
UncleBob
.
JavaEnums
Edit Page:
!title Java Enums are Cool When I first read about the new enums in Java 5, I was skeptical. These enums allow you to declare variables and methods and even abstract methods! What would you use that for? Well, I have found out the answer to at least part of that question. While reviewing and improving a bit of code from an external source, I had the opportunity to take a set of private static final constants that represented months, and turn them into a Month enum. At first the Month enum was just a simple enum. But as I continued to improve the external code, I found more and more methods that operated on the month constants, or that were deeply associated with them. Every time I found one, I moved it to the Month enum. The result is as follows: {{{import java.text.DateFormatSymbols; public enum Month { JANUARY(1), FEBRUARY(2), MARCH(3), APRIL(4), MAY(5), JUNE(6), JULY(7), AUGUST(8), SEPTEMBER(9), OCTOBER(10),NOVEMBER(11),DECEMBER(12); private static DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(); private static final int[] LAST_DAY_OF_MONTH = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; public int index; Month(int index) { this.index = index; } public static Month make(int monthIndex) { for (Month m : Month.values()) { if (m.index == monthIndex) return m; } throw new IllegalArgumentException("Invalid month index " + monthIndex); } public int lastDay() { return LAST_DAY_OF_MONTH[index]; } public int quarter() { return 1 + (index - 1) / 3; } public String toString() { return dateFormatSymbols.getMonths()[index - 1]; } public String toShortString() { return dateFormatSymbols.getShortMonths()[index - 1]; } public static Month parse(String s) { s = s.trim(); for (Month m : Month.values()) if (m.matches(s)) return m; try { return make(Integer.parseInt(s)); } catch (NumberFormatException e) {} throw new IllegalArgumentException("Invalid month " + s); } private boolean matches(String s) { return s.equalsIgnoreCase(toString()) || s.equalsIgnoreCase(toShortString()); } } }}} I find this to be quite remarkable, and very useful. I can now say things like:{{{Month m = Month.parse(inputString);}}} or {{{Month m...; int lastDay = m.lastDay();}}} In java 5 enums do not define constants. They define singleton objects within a class hierarchy. There is a class named Month. There is another class representing JANUARY that derives from Month. The JANUARY class has but a single instance named JANUARY. The JANUARY enumerator '''is''' that single instance of the JANUARY class. In effect, enums are way to create singleton classes. There will never be more than one instance of each enumerator, and the instances are created before the enumeration is used (probably at load time). And yet the enumerators are classes with the full repertoire of features that a class is allowed to have. Consider this the next time you want a singleton:{{{enum Database { INSTANCE; public void open(){...} ... } }}} You could use it as follows: {{{Database.INSTANCE.open(...);}}} !commentForm -r #----- Blog Comment Marker (Please don't delete me) -----# !* Tue, 25 Apr 2006 09:37:09, Ravi Venkataraman, DRY and repeated calculations Ilja said, "I still don't see what this has to do with DRY, though." The solution presented in the original article would do the computations every single time one asked for the information. For example, if we said, Month.parse("Jan") [or Month.make(1)] a hundred times, it would go through the same steps 100 times. That is where the repetition comes in. Also, that doesn't seem like the way humans think. We know that the first month is January, etc. We access it more like a Map, rather than filtering an array. We don't say, "OK, I've been given the string Aug, let me start with the first month. That is Jan or January. Does it look like Aug? No? OK, on to the next month...." That is exactly how the code is working. If we did not know that August was the eighth we could count from January to get the desired value. The next time, we were asked to do it, we would immediately reply "Eighth month" without repeating the tedious counting from January. We just see "Aug" and say, "August, eighth month of the year" almost instantaneously. We get the value directly from memory. In the parse and make methods, especially the parse method, one has to stop and think about the code to try and understand what is happening. Not so when we use the Map. (A Map approach can also be easily implemented for the parsing month names by mapping both the short name and the full name of the month to a Month object.) All this would be done as a static block at initialization so that it gets done only once, then the results would be used repeatedly. The Maps serve somewhat analogous to memory. Initializing the values in a static initializer is akin to us learning something for the first time, storing it in a Map is like us memorizing it! And that is my main problem with the example given: we seem to have been successfully coerced into thinking about the computer/compiler's needs rather than thinking in a natural human way. In itself, in this example in particular, it will not cause much of a problem. But when this coding style is extended to more complicated situations, it becomes difficult to maintain precisely because it is not aligned with more natural human thought processes, but is an effort to think like the computer. Unfortunately, I see this happening a lot. A prime example is the zero-based array offset in C-based languages. It is most irritating. Lisp, too, used the same hardware and faced the same problem, but they handled it neatly by using the cdr and car abstraction layers for lists so that one never sees the ugliness of the zero-based counting unlike C, C++, Java, C#, etc., where the underlying plumbing is exposed for all to see. As a result of these language decisions, I think that, in general, Java and other mainstream language programmers are more comfortable coding at a lower level of abstraction and do not see the benefits of the expressiveness gained from higher abstractions. *! !* Sun, 23 Apr 2006 14:46:18, Ilja Preuß, Violation of DRY? "Well, the way it has been written, you have to loop through the collection one-by-one, do an equals comparison to find the match. It is an O(n) method." That is true. I don't see that as a problem, especially as we know that n is fixed and small. After all, the big O notation only says something about big n - an O(n) algorithm can be well faster than an O(1) algorithm for small n. (That might not be true in this case. Anyway, I doubt that the difference would be significant, anyway.) [quote] This is simply too much work and is not the right level of abstraction I want to be working with. [/quote] Well, I agree that it isn't as expressive as using the Map, which would make me prefer the Map solution. I still don't see what this has to do with DRY, though. *! !* Sat, 22 Apr 2006 03:04:33, ${unclebob}, Could the map be slower than the linear search? Ravi, I agree that the suggested replacement is simpler. But that hides the creation of the map. I doubt there is an overall savings in complexity. As for the loop being too much work, I wonder which approach would win the race? The 'get' function of a map is not very expensive, but it might execute more instructions than 6 iterations around the search loop. *! !* Wed, 19 Apr 2006 20:28:40, Ravi Venkataraman, Ilja PreuB said, "Ravi, whatever algorithm you use to look up the month by index - a Map, an array or the loop - it simply needs to be executed every time you, wel, want to look up a month by index. How could that violate against the DRY principle?" Well, the way it has been written, you have to loop through the collection one-by-one, do an equals comparison to find the match. It is an O(n) method. This is simply too much work and is not the right level of abstraction I want to be working with. Maps are intended to provide O(1) access. Furthermore, when things do not change, as in the number of months in a year, it makes sense to optimize it by using a Map. Compare the original code {{{ public static Month make(int monthIndex) { for (Month m : Month.values()) { if (m.index == monthIndex) return m; } } }}} with the suggested replacement: {{{ public static Month make(int monthIndex) { return monthNameMap.get(monthIndex); } }}} I like the simplicity of the second example. *! !* Wed, 19 Apr 2006 19:55:36, Joe Gee, Degeneration of Java Enums I'm a little slow on this boat, the Craftsman article this month brought me in. I love the month example use of enums. What I have seen them do in our production code already, however, is get awful busy as responsiblities continued to get moved onto the enums. Often, then, the enum ends up getting refactored into a strategy hierarchy, usually with a seperate factory to eliminate dependancy cycles. I have found that refactoring is a bit of a pain, because I haven't found a good way to break it into small enough steps to suit me yet. *! !* Sat, 1 Apr 2006 03:23:00, BDory, Enum's singleton... That use of enum to implement singleton is dangerous, after all that's not what enum is bornt for and who knows when Sun changes the implementation of enum... *! !* Tue, 28 Mar 2006 14:57:04, Vishal Shah, parse method UB, I think your parse method could probably be splitted into 2 methods with names like parseDateString() and parseMonthIndex(). I suggest this,because it works with both date strings (formatted) and with int indexes (such as 2 for Feb.) . It somehow makes me think that it might need to change for more than one reason. And forgot even that, it confuses me at the first sight. Thanks as usual for cracking another Java 1.5 lang feature. --Vishal Shah *! !* Fri, 24 Mar 2006 15:36:13, Ilja Preuß, Disconcerting? Sean, why is it disconcerting that Java enums work differently than in other languages? In my opinion, there are very nice to use. It's quite powerful to be able to give them polymorphic behaviour. And yes, they can easily be used to implement a fixed set of Strategies or States. So what's wrong about that? *! !* Fri, 24 Mar 2006 15:30:40, Ilja Preuß, the make method Ravi, whatever algorithm you use to look up the month by index - a Map, an array or the loop - it simply needs to be executed every time you, wel, want to look up a month by index. How could that violate against the DRY principle? What *I* dislike most about the method is the name - as it doesn't really *make* a Month. But that might just be me... *! !* Fri, 24 Mar 2006 15:21:48, Ilja Preuß, LAST_DAY_OF_MONTH array I don't like that array because in my eyes it actually makes it *harder* to read the code in contrast to putting the data in the constructor. It becomes harder to see which month corresponds with which value. And frankly, I wouldn't call two arguments "a lot". Regarding ordinal()+1 being the index just "accidentally" - I wouldn't call it an accident to list the months in order. ;) But that's probably just a matter of taste... *! !* Thu, 23 Mar 2006 22:15:57, Ravi Venkataraman, Is this violation of the DRY principle? The fact that for a given index, a month is always the same suggests that we not calculate it every single time. The calculation would make sense only if we expect different answers every time. Is this repeated calculation a case of violation of DRY? *! !* Thu, 23 Mar 2006 15:54:12, ${unclebob}, Why use a loop to map an index to a Month object? I wasn't particularly happy with the loop in the 'make' function. On the other hand, I didn't have a good alternative either. I suppose I could have created an array of months indexed by the month number, but why bother? C++ programmers probably wrankle at this because in C++ an enum is an int and you can just cast your way between the two. But in Java an enum is not an int; it's an object. And so there is no direct mapping unless you create it. Now since there are only 12 months, and since loops like this are very very fast, and since the number of times I expect someone to call 'make' is relatively low, I don't see the harm. *! !* Thu, 23 Mar 2006 15:47:59, ${unclebob}, lastDay Yes, the lastDay method is improperly named. Since Month doesn't know anything about years (and shouldn't!) the leap year calculation has to be somewhere else. Therefore this method should be named something like lastNonLeapYearDay - but less wordy. Others asked why this wasn't part of the constructor. Why should it be? I don't want to load up the constructors with lots of arguments because it makes the enumerators harder to read IMHO. Remember I have absolute control over how many of these objects there are, so I don't need to worry about creating a generic class. Someone noted that 'index' is just 'ordinal()+1'. This is only true if I list the months in order. I don't like those kind of accidental dependencies. *! !* Thu, 23 Mar 2006 15:44:10, ${unclebob}, Not Exactly. Keith: Thanks, I wasn't aware of that nuance. I knew that the {} could be used to override methods, but not that a new class would not be formed without them. Bizarre. I wonder if that's actually part of the language or just part of the implementation. *! !* Thu, 23 Mar 2006 12:29:21, Sean Kent, I must admit that I don't like Java enums a whole lot. The complete deviation from how other similar languages have represented them (those languages being C++ and the .Net variations) is rather disconcerting (I should note that it also appears to just be an implicit implementation of strategy, or a similar pattern). Also, your lastDay() method isn't very nice, as it doesn't handle February properly without knowing if it is a leap year. This may not be an issue in your case, but taking the code as it is shown would cause it to behave differently than possibly expected (especially since the name of the method suggests that it will return the last day of the month). *! !* Thu, 23 Mar 2006 11:43:17, , I'm sorry. Looking at the code above makes me think that enums are crap, not cool. Why isn't the last day of the month in an instance variable? Why use a loop to map an index to a Month object? A lot of the code there looks like hacks around limitations of Java's enum, rather than being a good idea in itself. *! !* Thu, 23 Mar 2006 10:58:55, Keith Gregory, Not Exactly > In java 5 enums do not define constants. They define singleton objects within a class hierarchy. > There is a class named Month. There is another class representing JANUARY that derives from Month. By default, enums define singleton instances of a class, but only one class. This code, for example, prints "true". public class EnumTest { enum MyEnum { ARGLE, BARGLE } public static void main(String[] args) { System.out.println(MyEnum.ARGLE.getClass() == MyEnum.BARGLE.getClass()); } } You can, however, force it to create separate classes, like so ... hackishness of this technique left as an excercise for the reader ... public class EnumTest2 { enum MyEnum { ARGLE() {}, BARGLE() {} } public static void main(String[] args) { System.out.println(MyEnum.ARGLE.getClass() == MyEnum.BARGLE.getClass()); } } *! !* Thu, 23 Mar 2006 10:46:22, , Why do you use the LAST_DAY_OF_MONTH array, instead of passing the value in the constructor? Also notice that your index is the same as ordinal()+1. *! !* Thu, 23 Mar 2006 10:19:57, ${ottinger}, See, this is how it ought to be I was very down on Java as a "better C++" when it was really just heavily restricted. Okay, and it had GC, which is very appealing when done well. If someone wants to build a better C++, then it's going to be with extensions like good generics and this enum design, not by removing useful features that "might be confusing to novices". Sometimes less isn't more. This enum feature is the kind of thing that motivates me to learn to use Java more, not the lack of overloads and templates. *!
Hints:
Use alt+s (Windows) or control+s (Mac OS X) to save your changes. Or, tab from the text area to the "Save" button!
Grab the lower-right corner of the text area to increase its size (works with some browsers).