0

This question is based on Clean Code, chapter "Functions", section "The Error.java dependency magnet".

public enum Error {
      OK,
      INVALID,
      NO_SUCH
}

The author claims that all classes that import and use this enum must be recompiled in case of a change. This is opposed to adding new Exception derivatives. In the latter case there is supposedly no recompilation necessary.

But isn't it the case that if you use the package that has the derived exceptions as soon as the package is changed by adding a new derived Exception, if your code depends on the package it will need to be recompiled? Or will you code only need to be recompiled if it actually uses one of the new derived Exceptions?

Roland
  • 7,525
  • 13
  • 61
  • 124

2 Answers2

3

There's a little bit more to this paragraph. I'll quote:

Classes like this are a dependency magnet; many other classes must import and use them. Thus, when the Error enum changes, all those other classes need to be recompiled and redeployed.

This reads confusingly at first, but makes sense in hindsight.

An enum has guaranteed enum constants declared in it; adding another value to the enum requires another compilation for that change to be picked up. This is true for all classes that make use of the enum, even if they don't make use of the value.

On the other hand, if you declare several independent Exception-derived classes, if your particular class doesn't need that specific Exception, it won't need to recompile it to make use of it.

This ties into your next question:

...[I]sn't it the case that if you use the package that has the derived exceptions as soon as the package is changed by adding a new derived Exception, if your code depends on the package it will need to be recompiled?

This ties into a more common trend that I've seen with Java programs (and conflicts with a code smell identified in J1): unless you need everything in the package, don't import the whole package. Your imports may suddenly use the same class name to mean completely different things.

I doubt that there's any substantial difference when it comes to the package-level dependency; I can't find any evidence that suggests that Java conditionally compiles select classes, but I also don't believe that it will recompile your class just to include the exceptions that you're not using, either.

Community
  • 1
  • 1
Makoto
  • 104,088
  • 27
  • 192
  • 230
1

Java is incredibly smart when it comes to binary compatibility, due to the way it uses method signatures, etc, so for the most part, compatibility is easily maintained. You can add sub-classes and pass them into methods that accept their super-class, even if those methods weren't compiled with the sub-classes present, you can add methods to classes and add interfaces for them to implement, and binary compatibility will still be the same for the most part. This isn't so true for enums. Enums often use the .ordinal() method (when compiled) to do certain tasks (see below for more bytecode stuff), so that's why you probably have to recompile when they're classes change.


Makoto's answer covers the conceptual stuff quite well, but the direct cause has to do with the bytecode generated by javac.

The below class:

public class TestEnums {

    public static void main(String[] args) throws Throwable {
        Matter matter = Matter.SOLID;
        switch (matter) {
            case SOLID:
                System.out.println("a");
                break;
            case LIQUID:
                System.out.println("b");
                break;
            case GAS:
                System.out.println("c");
                break;
        }
    }

    private enum Matter {
        SOLID,
        LIQUID,
        GAS
    }

}

generates the following bytecode (in the main method):

 public static main([Ljava/lang/String;)V throws java/lang/Throwable 
   L0
    LINENUMBER 27 L0
    GETSTATIC TestEnums$Matter.SOLID : LTestEnums$Matter;
    ASTORE 1
   L1
    LINENUMBER 28 L1
    GETSTATIC TestEnums$1.$SwitchMap$TestEnums$Matter : [I
    ALOAD 1
    INVOKEVIRTUAL TestEnums$Matter.ordinal ()I
    IALOAD
    TABLESWITCH
      1: L2
      2: L3
      3: L4
      default: L5
   L2
    LINENUMBER 30 L2
   FRAME APPEND [TestEnums$Matter]
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "a"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L6
    LINENUMBER 31 L6
    GOTO L5
   L3
    LINENUMBER 33 L3
   FRAME SAME
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "b"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L7
    LINENUMBER 34 L7
    GOTO L5
   L4
    LINENUMBER 36 L4
   FRAME SAME
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "c"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L5
    LINENUMBER 39 L5
   FRAME SAME
    RETURN
   L8
    LOCALVARIABLE args [Ljava/lang/String; L0 L8 0
    LOCALVARIABLE matter LTestEnums$Matter; L1 L8 1
    MAXSTACK = 2
    MAXLOCALS = 2

getting past the verbosity of the bytecode, the main logic of the switch statement is present in the form of a TableSwitch:

INVOKEVIRTUAL TestEnums$Matter.ordinal ()I
IALOAD
TABLESWITCH
  1: L2
  2: L3
  3: L4
  default: L5

If you look, you see that the switch invokes the .ordinal() method to determine which value to go to, so if you insert a value at the start of the enum:

private enum Matter {
    PLASMA, //Lying elementary school science teachers don't tell you about this one
    SOLID,
    LIQUID,
    GAS
}

the ordinals are changed. Since bytecode uses the ordinal method a lot when it comes to manipulating enums, code may have to be recompiled to maintain compatibility with the enum.

Socratic Phoenix
  • 556
  • 1
  • 7
  • 19