2

Say we have a class like this:

class Bar {
  boolean b;
}

class Foo {
  String zoo;
  Bar bar;
}

and then we have a class that extends Foo:

class Stew extends Foo {
   public Stew(Bar b, String z){
      this.bar = b;
      this.zoo = z;
   }
}

my question is - is there any way to prevent Stew from having any non-method fields that aren't in Foo? In other words, I don't want Stew to have any fields, I just want Stew to implement a constructor and maybe a method or two.

Perhaps there is an annotation I can use, that can do this?

Something like:

@OnlyAddsMethods
class Stew extends Foo {
   public Stew(Bar b, String z){
      this.bar = b;
      this.zoo = z;
   }
}

purpose - I am going to serialize Stew to JSON, but I don't want Stew to have any new fields. I want to let any developer working on this file to know that any additional fields will be ignored (or won't be recognized) etc.

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • 2
    At compile time? At runtime? Why do you consider fields a problem that needs preventing? – meriton Nov 21 '18 at 01:03
  • The question would be why do you need `Stew to extend Foo` and prior to that why do you need `bar` and `zoo` in Foo? You can always choose to have a constructor which doesn't need to initialize the properties of parent class. Is that what you're looking for? – Naman Nov 21 '18 at 01:03
  • @meriton the class will get serialized to JSON and I want to prevent them from adding fields, just curious if it's possible – Alexander Mills Nov 21 '18 at 01:03
  • @meriton at compile time or runtime, but preferably compile time – Alexander Mills Nov 21 '18 at 01:04
  • Sounds like `@JsonIgnoreProperties` is what you might be looking for? Related - https://stackoverflow.com/questions/29630371/jackson-ignore-all-properties-of-superclass-from-external-library – Naman Nov 21 '18 at 01:05
  • 1
    If you go the annotation route, you can create an annotation processor that emits errors if you declare any fields inside a class with the corresponding annotation. This would happen during compilation. – Slaw Nov 21 '18 at 01:06
  • @nullpointer sure that might work, but it would be interesting if there were an annotation that can do this for the class – Alexander Mills Nov 21 '18 at 01:07
  • @Slaw sure please show an example so we can learn – Alexander Mills Nov 21 '18 at 01:07
  • You can use default methods to add functionality without fields. – shmosel Nov 21 '18 at 01:07
  • @nullpointer Who said anything about JSON serialization? – shmosel Nov 21 '18 at 01:08
  • @nullpointer that link you provided doesn't make sense for this question - I am not trying ignore properties of the superclass, I am trying to ignore any properties of the subclass. The subclass here is only for implementing a constructor and methods. – Alexander Mills Nov 21 '18 at 01:11
  • 1
    @AlexanderMills I got the problem statement, just wanted to mark question-related. Also, why in such case is `Stew` even extending `Foo` from a design perspective? – Naman Nov 21 '18 at 01:17
  • @nullpointer the Foo class is autogenerated code ... generating the code to create a constructor is tricky and too verbose in my case. I need the user to implement the contructor. Ultimately the constructor needs to get created so that the field `Bar bar;` gets populated dynamically. Do you get it? – Alexander Mills Nov 21 '18 at 01:18
  • 1
    @AlexanderMills No, I don't. You could have generated the provided constructor in `Foo` itself. If that's not possible, I would have rather tried to solve that first. – Naman Nov 21 '18 at 01:20
  • @nullpointer well the other purpose of the subclass is as a type alias, it's serving a dual-purpose – Alexander Mills Nov 21 '18 at 01:21
  • @AlexanderMills Not really sure what the purpose of the aliasing here is. But if that's a must and unavoidable, you might try out the suggested answer in that case. – Naman Nov 21 '18 at 01:28

3 Answers3

4

The Java Language offers no built-in way to prevent a subclass from adding fields.

You might be able to write an annotation processor (which are essentially plugins for the java compiler) to enforce such an annotation, or use the reflection api to inspect subclass field declarations in the superclass constructor or a unit test. The former offers compile time support and possibly even IDE support, but is much harder to implement than the latter.

The latter could look something like this:

public class Super {
    protected Super() {
        for (Class<?> c = getClass(); c != Super.class; c = c.getSuperClass()) {
            if (c.getDeclaredFields().length > 0) {
                throw new IllegalApiUseException();
            }
        }
    }
}

You might want to permit static fields, and add nicer error messages.

meriton
  • 68,356
  • 14
  • 108
  • 175
2

That would be an odd feature.

You could use, say, a javac processor to check at compile time or reflection at runtime, but that would be an odd choice.

A better approach is to change the design.

Delegation is usually a better choice than inheritance.

So, what can we pass in to the constructor that wont have state. An enum is the perfect match. It could have global state, but you really can't check for that unfortunately.

interface FooStrategy {
    MyRet fn(Foo foo, MyArg myArg);
}
public final class Foo<S extends Enum<S> & FooStrategy> {
    private final S strategy;
    private String zoo;
    private Bar bar;
    public Foo(S strategy, Bar bar, String zoo) {
        this.strategy = strategy;
        this.bar = bar;
        this.zoo = zoo;
    }
    // For any additional methods the enum class may provide.
    public S strategy() {
        return strategy;
    }
    public MyRet fn(Foo foo, MyArg myArg) {
        return strategy.fn(this, myArg);
    }
    ...
}

You can use a different interface (and object) for the strategy to work on Foo, they probably shouldn't be the same.

Also strategy should probably return a different type.

Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
2

You can't force the client code to have classes without fields, but you can make the serialization mechanism ignore them. For example, when using Gson, this strategy

class OnlyFooBar implements ExclusionStrategy {
    private static final Class<Bar> BAR_CLASS = Bar.class;
    private static final Set<String> BAR_FIELDS = fieldsOf(BAR_CLASS);
    private static final Class<Foo> FOO_CLASS = Foo.class;
    private static final Set<String> FOO_FIELDS = fieldsOf(FOO_CLASS);

    private static Set<String> fieldsOf(Class clazz) {
        return Arrays.stream(clazz.getDeclaredFields())
                     .map(Field::getName)
                     .collect(Collectors.toSet());
    }

    @Override
    public boolean shouldSkipField(FieldAttributes f) {
        String field = f.getName();
        Class<?> clazz = f.getDeclaringClass();
        return !(BAR_CLASS.equals(clazz) && BAR_FIELDS.contains(field)
                || FOO_CLASS.equals(clazz) && FOO_FIELDS.contains(field));
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}

when used in a Gson, will ignore all other fields except required ones:

Gson gson = new GsonBuilder().setPrettyPrinting()
                             .addSerializationExclusionStrategy(new OnlyFooBar())
                             .create();
jihor
  • 2,478
  • 14
  • 28