This approach is flawed.
It does not prevent a declaration such as
class D extends A<B> {
}
which will compile, but throw an exception at runtime, more specific:
new D().self().getClass() // => ClassCastException
To let classes provide functionality beyond their known superclass, you might try the adapter pattern.
It's base is usually an interface, like
interface Adaptable {
T getAdapter(Class<? extends T> key);
// for those who don't like to type long method names
default T as(Class<? extends T> key) {
return getAdapter(key);
}
}
An implementation might look like
class A implements Adaptable {
@Override
public T getAdapter(Class<? extends T> key) {
/*
* To be less strict, one might also check for 'key.isInstance(this)',
* but it's an implementation decision.
*/
if(getClass() == key) {
return key.cast(this);
}
return null;
}
}
However, the adapter pattern allows to provide other objects, usually specialized views on the target, see the FileSource
example below.
The major downside of this approach is that a client will always have to check on whether an adapter is available. However, if a client knew that an object is of the subclass it is looking for, it could simply cast it, so we don't really lose anything. The interface can also be extended using java.util.Optional
, but the basic idea remains the same.
interface Adaptable {
Optional<T> getAdapter(Class<? extends T> key);
}
For an example use case, let's say there is a Source
class that models an available source for whatever process. As we know that source handling is often tricky and therefore hard to normalize into a single class or interface, we let the Source
class implement Adaptable
.
class Source implements Adaptable {
@Override
public Optional<T> getAdapter(Class<? extends T> key) {
if(getClass() == key) {
return Optional.of(key.cast(this));
}
return Optional.empty();
}
}
Now there is a basic implementation, a FileSource
, that is generally available as java.io.File
.
class FileSource extends Source {
private File pointer;
public File asFile() {
return pointer;
}
}
A client can now check whether a source is available as file and perform some operation using the underlying java.io.File
.
Source source;
...
source.getAdapter(FileSource.class).ifPresent(fileSource -> {
File file = fileSource.asFile();
// do your magic with 'file'
});
Even better, FileSource
could simply provide an adapter for File
. At this point, a client does not even need to care about the implementation subclass, but only on what s/he actually wants.
class FileSource extends Source {
private File pointer;
@Override
public Optional<T> getAdapter(Class<? extends T> key) {
if(File.class == key) {
return Optional.of(key.cast(asFile()));
}
return super.getAdapter(key);
}
public File asFile() {
return pointer;
}
}
and
Source source;
...
source.getAdapter(File.class).ifPresent(file -> {
// do your magic with file
});