Alexander Mills
Alexander Mills

Reputation: 100000

Extend class without adding any new fields

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.

Upvotes: 2

Views: 2062

Answers (3)

jihor
jihor

Reputation: 2599

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();

Upvotes: 2

Tom Hawtin - tackline
Tom Hawtin - tackline

Reputation: 147154

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.

Upvotes: 2

meriton
meriton

Reputation: 70564

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.

Upvotes: 4

Related Questions