Override method in extended Class with same erasure

118 Views Asked by At

So I have some classes like BooleanLogger, IntLogger etc., which implement my own Logger interface.

This is the Logger interface:

public interface Logger {
    String serialise();
    Object deserialise(String value);
    void setValue(Object value);
}

Here's a snippet of BooleanLogger:

public class BooleanLogger extends MutableBoolean implements Logger {
    private MutableBoolean value;

    // ...

    @Override
    public String serialise() {
        return serialise(this.value.booleanValue());
    }

    public static String serialise(Boolean value) {
        return Boolean.toString(value);
    }

    @Override
    public Object deserialise(String value) {
        return Boolean.parseBoolean(value);
    }

    @Override
    public void setValue(Boolean value) {
        this.value = new MutableBoolean(value);
        logChange();
    }

    @Override
    public void setValue(Object value) {
        this.value = new MutableBoolean((Boolean) value);
        logChange();
    }

    private void logChange() {
        System.out.println(this.value); // temporary
    }
}

So the basic idea is essentially to override any setter methods within MutableBoolean such that we log the new value whenever it's modified. I've implemented that part trivially so far, but that's not what I'm concerned about.

Elsewhere in my code, I have a map Map<String, Logger> loggerMap. I want to be able to get a Logger from this map and set its value, given a serialised String value. For example:

Logger logger = loggerMap.get("myLogger1");
logger.setValue(logger.deserialise(value));

It's done this way because I don't know the type of Logger I'm getting, so everything needs to work regardless.

Note the reason for this serialising/deserialising is these values are coming from Redis and hence are stored as Strings.

The problem I'm having is that in the individual loggers like BooleanLogger, it says:

'setValue(T)' in 'org.apache.commons.lang3.mutable.Mutable' clashes with 'setValue(Object)' in 'myLogger.BooleanLogger'; both methods have same erasure, yet neither overrides the other

The only solution I can think of is to no longer extend from MutableBoolean, MutableInt etc., and instead just copy all the methods into my own BooleanLogger, IntLogger classes. However, I have these classes for a fair few types and it would quickly become cumbersome and frustrating to do it this way. Right now I only have to deal with any methods that change the underlying value.

As far as the logic goes, I've already successfully implemented everything with my public class ArrayLogger<T> extends ArrayList<T> implements Logger {} as this is the only one that does not extend from a Mutable type that clashes on the setValue method.

EDIT: I've realised that even if I make my own class, I'll have the same issue just

'setValue(Object)' in 'myLogger.BooleanLogger' clashes with 'setValue(T)' in 'org.apache.commons.lang3.mutable.Mutable'; both methods have same erasure, yet neither overrides the other

Assuming public class BooleanLogger implements Mutable<Boolean>, Serializable, Comparable<MutableBoolean>, Logger {}

So unless I make my own Mutable<T> type (and possibly others...), I may need a different solution.

1

There are 1 best solutions below

12
On

The root problem is that your code doesn't actually make sense.

This part, specifically:

interface  Logger {
  void setValue(Object value);
}

You've broken the type system here. That says to me I can call setValue(whateverObjectIPlease) on any logger of any stripe and it should just work. Turns out that is incorrect - if I invoke .setValue(x) on any Logger whose actual type is BooleanLogger, and x is anything but a Boolean instance, it fails.

Hence, this is not great API. Now also consider that it fundamentally clashes with apache's Mutable hierarchy and it gets worse.

There's a second fundamental problem in your design.

Your BooleanLogger class is confused about itself and is its own factory.

Specifically, the deserialize method has nothing to do with a BooleanLogger instance. It doesn't interact with any field whatsoever, and could have been static, other than the fact that you want it to participate in class hierarchies and static methods don't.

That is what factories are for. They let you abstract non-instance parts of class structures (so, constructors, and static methods).

One way to go is to make a LoggerFactory or LoggerType interface, have exactly 1 instance for each type. You'd have something like:

interface LoggerFactory<T extends Logger> {
  T deserialize(String in);
}

class BooleanLoggerFactory<BooleanLogger> {
  BooleanLogger deserialize(String in) {
    return new BooleanLogger(Boolean.parseBoolean(value));
  }
}

Voila - you can abstract that way to your hearts content.

But, if you find that too complicated, just 'collapse' your setValue and deserialize methods.

Right now your deserialize method is lying in the sense that it says it is an instance method but it really isn't.. and your setValue method is lying in the sense that it indicates any object of any stripe will do when that isn't the case either.

But... combine the two... and both lies disappear in a puff of smoke. Behold:

interface Logger {
  /** Sets the value of this logger by deserializing {@code in}. */
  abstract void deserialize(String in);

  // completely delete setValue. It has no business being here.
}

class BooleanLogger implements Logger {
  @Override public void deserialize(String in) {
    setValue(Boolean.parseBoolean(in));
  }

  public void setValue(boolean b) {
    this.v = v;
  }
}

Problem solved. If you really insist on having that setValue method be part of Logger itself, introduce generics. It was your choice to sign up to apache's Mutable type hierarchy, and it uses generics, so - you signed up for that too:

public interface Logger<T> {
    String serialise();
    Object deserialise(String value);
    void setValue(T value);
}

public class BooleanLogger implements Logger<Boolean> {
  ...

  @Override public void setValue(Boolean b) {
    this.v = b.booleanValue();
  }
}