1

Ok, in this answer here on stack, the poster of the answer shows an example of how you can use abstract methods in an enumeration. I'll repeat that answer here for posterity, albeit slightly-modified to better illustrate the basis of my question.

Consider this enum which uses an abstract method:

public enum Vehicle {
    CAR {
        public String action() { return "DRIVE!"; }
    },
    TRUCK {
        public String action() { return "DRIVE!"; }
    },
    BUS {
        public String action() { return "DRIVE!"; }
    },
    TRACTOR {
        public String action() { return "DRIVE!"; }
    },
    MOTORCYCLE {
        public String action() { return "RIDE!"; }
    },
    BOAT {
        public String action() { return "SAIL!"; }
    },
    AIRPLANE {
        public String action() { return "PILOT!"; }
    };

    public abstract String action();
}

As you can see, since 'action' is an abstract function, every single element has to define the override via an anonymous subclass of the enum.

Contrast this with this abstract-free, functionally-equal version which even uses the exact same API:

enum Vehicle {
    CAR,
    TRUCK,
    BUS,
    TRACTOR,
    MOTORCYCLE,
    BOAT,
    AIRPLANE;

    public String action(){
        switch(this)
        {
            case MOTORCYCLE : return "RIDE!";
            case BOAT       : return "SAIL!";
            case AIRPLANE   : return "FLY!";
            default         : return "DRIVE!";
        }
    }
}

In this example, you only have to specify the specific cases which differ from a default. Plus, it keeps the values in a nice, clean readable list and reduces a ton of extraneous code as well.

Perhaps I'm missing something, but is there a technical benefit of the abstract method approach? What exactly does it give you that the non-abstract version doesn't? Does it have any extra capabilities?

Note: I suspect the actual answer is because it's not really a function of an enum per se, but rather it's because the enum is compiled to a class and a class supports abstract functions.

However, I'm not exactly sure that's correct either because as others have shown, an enum compiles down to a static final class which means it can't be subclassed. Perhaps the compiler doesn't add the 'final' when using abstract functions. Not sure as I haven't been able to view generated output so I can't say for sure, but that would make sense.

But specifically for this question, is there anything an enum with an abstract function can do that a non-abstract version can't?

Community
  • 1
  • 1
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

3 Answers3

3

The question shouldn't be what one can do over the other because they can both behave in the exactly the same way if programmed correctly.

That is the key part. The version using the switch statement is easy to program incorrectly when adding new items to the enum in the future. Forgetting to specify a case for the new item will cause the default to be used which may not be what you want.

I always use the abstract method version because forgetting to override action() is a compile time error while forgetting to add a new case is a run time error.

The more errors you can catch at compile time, the better.

Brent C
  • 833
  • 1
  • 9
  • 15
  • Saying that isn't the right question is a bit arrogant. I was asking for technical differences. Plus, I would make the *subjective* opinion that your way is just as error-prone. You can easily make a cut-and-paste error, and having to parse through all of that code makes it much less scannable. But again, that's my opinion, which wasn't what I asked in this question. – Mark A. Donohoe Jul 26 '17 at 17:16
  • 1
    I apologize if my answer came across as rude. I didn't mean to. I was simply trying to point out that the technical difference is more subtle than A can allow you to accomplish something B can't. Instead the technical difference is related to correctness, which I will argue isn't subjective at all. See: https://stackoverflow.com/questions/27183917/force-exhaustive-switch – Brent C Jul 26 '17 at 17:21
  • But I'd argue it *is* subjective, because IMPO, having to create new anonymous subclasses--which is what the abstract approach does--isn't nearly as clean as the only-one-type version which I proposed. Yes, the abstract version would force you to override, but that also means you can't have an implicit default, which is exactly what the 'default' clause gives you. I would agree however to the point that perhaps Java should follow Swift in forcing their switch-case statements to be exhaustive which would address your concern, at least when there isn't a default specified. – Mark A. Donohoe Jul 26 '17 at 17:26
  • The thing about having an implicit default is that it precludes you from the compile time safety of the requirement to implement an abstract method, so I see that as negative not positive (with my personal anecdotal evidence from all the times I have used the abstract enum method approach, I don't believe I ever had a use for a default value; what was returned/invoked was always unique). – Brent C Jul 26 '17 at 18:58
  • Yes, but in that case, at least as I understand your explanation, I wouldn't use abstract methods, but rather distinct values passed into the constructor. Same compile-time safety, but much less baggage if all you're talking about are values. Of course that won't work if you need a function to resolve them, but it just shows there's a lot of flexibility in what you can do. Thanks! :) – Mark A. Donohoe Jul 26 '17 at 19:01
2

I'm here mostly to agree with Andreas's answer, but also to add that Java enums let you implement the behavior described in your example in a lot of different ways.

One complaint you had was that since action() was abstract, you had to implement it on every enum constant, and you liked that the switch strategy allowed you to define the default case. Here's another way to do it with a default case:

enum Vehicle {

    CAR,
    TRUCK,
    BUS,
    TRACTOR,
    MOTORCYCLE {
        public String action() { return "RIDE!"; }
    },
    BOAT {
        public String action() { return "SAIL!"; }
    },
    AIRPLANE {
        public String action() { return "PILOT!"; }
    };

    public String action(){
        return "DRIVE!";
    }
}

Another complaint you had was that all the boilerplate code made it difficult to scan the enum. If you wanted to reduce boilerplate while still getting compile-time enforcement that each enum constant has its own "action", you could do it this way:

enum Vehicle {

    CAR        ("DRIVE!"),
    TRUCK      ("DRIVE!"),
    BUS        ("DRIVE!"),
    TRACTOR    ("DRIVE!"),
    MOTORCYCLE ("RIDE!"),
    BOAT       ("SAIL!"),
    AIRPLANE   ("PILOT!");

    private final String action;

    Vehicle(String action) {
        this.action = action;
    }

    public String action() {
        return action;
    }
}

Personally, I love how much Java enums can do, and all the different strategies/techniques they support.

Ben P.
  • 52,661
  • 6
  • 95
  • 123
  • Switching to yours as the answer for showing a way I hadn't seen before. Interesting how your implicit subclass allows you to override 'action' even though it wasn't defined as abstract, nor did you specify '@override' in the subclasses. Your second approach I've seen a lot and works for returning simple values. I don't think that can be used for different functions/logic, but still, it's a good include. – Mark A. Donohoe Jul 26 '17 at 18:10
  • 3
    I left out the `@Override` annotations for the sake of brevity; in a real application I'd include them. – Ben P. Jul 26 '17 at 18:11
  • I will add however that in your first example, by *not* declaring 'action' as abstract here, you're removing one of the main pro's of this approach, which is having the compiler catch not handling new cases. Not saying that's a bad thing. Just calling out that it seems to then lose any leverage it had over the switch statement. – Mark A. Donohoe Jul 26 '17 at 18:34
  • 1
    I think it's nearly identical (in terms of safety against programmer error) to the `switch` implementation, but I think it has a _very slight_ edge in that the danger comes when someone adds a new enum constant and so they are going to be adding code very close to the other enum constants, so they're more likely to notice that there's a method they might want to override. – Ben P. Jul 26 '17 at 18:55
  • I guess what it comes down to is how you choose to segregate your logic. If it makes sense for all of the logic for one enum to be in the same place, use the abstract version. If however it makes more sense to keep the logic of a specific function together, regardless of type, then the switch case is the way to go. Pros and cons of each. Thanks! :) – Mark A. Donohoe Jul 26 '17 at 18:59
1

What can a Java enum with abstract functions do that a non-abstracting version without using them can't?

Nothing!

It isn't a question of code ability. It's a question of convenience, code style, and code protection.

You might as well ask: What can an enhanced for-loop do that a regular for-loop can't? Also nothing, but it is a lot more convenient for iterating an Iterable or an array than using the Iterator yourself.

As you showed yourself, you can do the exact same thing using a switch statement, but switch statements are notoriously fragile, i.e. they easily break when changes are made, because it's too easy to forget adding extra case statements when a new value is added.

Andreas
  • 154,647
  • 11
  • 152
  • 247
  • Not quite the right analogy because the two loops use different APIs whereas these approaches don't. To the consumer, they are completely identical. Still, as others have pointed out, there are pros and cons to both and you called them out. However, you also correctly stated the technical answer is 'nothing' so you get the vote. – Mark A. Donohoe Jul 26 '17 at 17:39
  • 1
    @MarqueIV How are the two loops using different APIs for iterating a `Collection`? Or an `array`? `for (String str : stringList) { ... }` vs `for (Iterator iter = stringList.iterator(); iter.hasNext(); ) { String str = iter.next(); ... }` is exactly the same API, though first is much easier to write and read. `for (String str : stringArray) { ... }` vs `for (int i = 0; i < stringArray.length; i++) { String str = stringArray[i]; ... }` is again same "API". The enhanced for-loop does *not* use a different API!! There is nothing an enhanced for-loop can do that a regular for-loop can't. – Andreas Jul 26 '17 at 17:43
  • You just answered your own question. They are *not* the same API. They achieve the same function, but the consumer has two different ways to write them when enumerating over them. Compare that with the enum examples I gave above. There is only one way for a consumer to call Vehicle.CAR.action(). Yes, the *implementation* is different, but I'm talking about the API. Make sense? – Mark A. Donohoe Jul 26 '17 at 17:47
  • 1
    @MarqueIV Does not make sense at all. `for` is a *statement*, not an API. `Collection` and `Iterator` and their *methods* are APIs. And we're talking about how to *implement* the `action()` method. Whether you implement the method using a `switch` statement or using `abstract` method is similar to how you implement a loop using `while`, `for`, or enhanced-`for`. They can all do it, but some are better ways to do it. I stand by my analogy. – Andreas Jul 26 '17 at 18:05
  • Ok, I concede your correct usage of the term 'API' here, but you've just proven my point. Your examples are two different ways to *consume* the same API. My example showed two different ways to *implement* that API but there is still just one API. Small but subtle difference. That's what I meant by not the right analogy. Had you instead shown two different-but-identical-in-function ways to *implement* the collection and explained the technical differences, that would be more akin to my question. In other words, we're arguing apples-to-bicycles here. – Mark A. Donohoe Jul 26 '17 at 18:27
  • In other words, and using your own definitions, the enhanced for-loop vs a regular for loop are just statements. They're not part of the API in question, hence my saying they aren't a good analogy. – Mark A. Donohoe Jul 26 '17 at 18:30
  • 1
    And `switch` is just a statement. You question is about two different Java syntaxes/constructs for accomplishing the same thing (`switch` vs subclass overrides), similar to how the two different `for` loops are two different syntaxes for accomplishing the same thing. Perfect analogy, about Java syntax/features, and has nothing to do with API. – Andreas Jul 26 '17 at 18:37
  • Aaaah... but creating new subclasses is *not* just a syntax difference! You're creating new, distinct types, even if they are anonymous! Again, your point does show there are multiple ways to achieve the same result (i.e. iterating a loop) but I was talking about object construction, or the implementation and what that implementation exposes (i.e. it's API!). – Mark A. Donohoe Jul 26 '17 at 18:49
  • And actually, now that I think about it, the two enums are technically *not* truly identical from a definition standpoint. Yes, their APIs may be, but one exposes all of its values as the same concrete class type while the other exposes them as multiple, anonymous subclasses. If you compared the type of Car to Truck, they would be the same in one case but different in the other. Anyway, that's just adding some more details to the difference in the two implementations and won't affect actual usage. – Mark A. Donohoe Jul 26 '17 at 18:55