Reputation: 100000
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
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
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
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