While trying to program to interfaces, I regularly find myself in the following situation:
- I have several very similar classes representing containers or algorithms for different types.
- I would like to define a common interface for these classes.
Consider, e.g., a string container. Such a container will likely have string processing methods. Since those methods are easily represented using generic interfaces, I am ignoring them. Here, I want to focus on methods that can be used to process or provide references to other string containers:
public class StringContainer {
StringContainer produce() {
return new StringContainer();
}
void consume(StringContainer stringContainer) {
}
}
This class can be used just fine in code like:
public class Main {
public static void main(String[] args) {
StringContainer stringContainer = new StringContainer();
stringContainer.produce();
stringContainer.consume(stringContainer);
}
}
The problem is: I'm using a concrete class and not an interface to refer to the string container. What if I want to introduce a double container or a list container later and want to leave the rest of the code as is?
Maybe generics could form a solution to this problem? Here is my try. I first define a generic container class:
interface Container<T> {
Container<T> produce();
void consume(Container<T> container);
}
I then create type-specific implementations of the form:
public class StringContainer implements Container<String> {
@Override
public Container<String> produce() {
return new StringContainer();
}
@Override
public void consume(Container<String> container) {
}
public void consume(StringContainer container) {
}
}
The above classes can be used as follows:
public class Main {
public static void main(String[] args) {
Container<String> stringContainer = new StringContainer();
stringContainer.produce();
stringContainer.consume(stringContainer);
}
}
However, the above approach has several drawbacks:
- The
consume(Container<String> container)
method accepts other types thanStringContainer
. - In
consume(Container<String> container)
, the parametrized typeContainer<String>
has to be used when processingcontainer
. I can't assign it toStringContainer
variables (without type checks or casts). - The alternative
consume(StringContainer container)
method is defined forStringContainer
objects, but can't be called from aContainer<String>
reference. - Finally, to me, the line
Container<String> stringContainer = new StringContainer();
has an awkward-looking notation that suggests a diamond operator is missing innew StringContainer()
.
What is the idiomatic way to define a general interface for several type-specific classes, which doesn't have (all) the above drawbacks?
Should I ignore point 4 and address points 1 and 2 by adding type checks/casts, throwing an UnsupportedOperationException
or IllegalArgumentException
in case passed objects aren't StringContainer
s?
Or is there another way to use generics? Can type bounds help me, for example?
Or should I look for a solution outside of generics?
CodePudding user response:
Is this what you're looking for? It's called a recursive type bound.
interface Container<T extends Container<T>> {
T produce();
void consume(T container);
}
class StringContainer implements Container<StringContainer> {
@Override
public StringContainer produce() {
return new StringContainer();
}
@Override
public void consume(StringContainer container) {
}
}
CodePudding user response:
It seems that you have two APIs, and you should treat them separately with separate interfaces. Yes, you can merge them into the same interface with distinct method names.
I think you should have two interfaces for your "containers" and for your "containers of containers". Here's what I'd make it:
interface Container<T> {
T produce();
void consume(T container);
}
interface MetaContainer<T, R extends Container<T>> {
R produceContainer();
void consumeContainer(R container);
}
class StringContainer implements Container<String>, MetaContainer<String, StringContainer> {
@Override
public String produce() {
return "";
}
@Override
public void consume(String container) {
}
@Override
public StringContainer produceContainer() {
return this;
}
@Override
public void consumeContainer(StringContainer container) {
}
}
I implemented both interfaces using the same class to emulate your StringContainer
class.