Reputation: 23
So I have an abstract class Map
with an ArrayList
of BaseElement
's.
BaseElement
is also an abstract class, which is extended by three classes: Node
, Way
and Relation
. It only has an attribute called id
.
Inside the class Map
i want to have a addElement
method and a getElement
method. The addElement
method should add the element to the list and return the element back, without losing it's type. The getElement
should find the element inside the list, by it's id and return it (also without losing the type).
Now I tried to use type generics to do this, but I'm still pretty new to type generics.
Currently this is my Map
class:
public abstract class Map {
private final String name;
private final ArrayList<BaseElement> elements;
protected Map(String name) {
this.name = name;
this.elements = new ArrayList<>();
this.defineElements();
}
protected abstract void defineElements();
protected <T extends BaseElement> T addElement(T element) {
elements.add(element);
return element;
}
public BaseElement getElement(int elementID) {
return elements.stream()
.filter(element -> element.getID() == elementID)
.findFirst()
.orElse(null);
}
public String getName() {
return name;
}
}
For example I want to add an element with addElement
like so...:
public class EarthMap extends Map {
public EarthMap() {
super("Earth");
}
@Override
protected void defineElements() {
Node exampleNode = this.addElement(BaseElementFactory.createNode(new Location(1.2345, 2.3456)));
}
}
and get the element again with getElement
like so:
public class Main {
public static void main(String[] args) {
new EarthMap().getElement(0).getLocation() // getLocation() is defined in class Node, not in BaseElement
}
}
I'm really not sure how to do this, without breaking the logic here. I have to store multiple objects that extend from BaseElement
in a list, without losing their type.
Can I do this with type generics? Or do I have to use another technique?
Upvotes: 2
Views: 1654
Reputation: 338386
An object does not lose its type when retrieved from a Generics-savvy collection (having a parameterized type). You can test for the subtype by using instanceof
and then cast.
You asked how to call a subclass-specific method on an object retrieved from Generics-enabled collection:
new EarthMap().getElement(0).getLocation() // getLocation() is defined in class Node, not in BaseElement
Using the latest features in Java, this work can be as simple as:
BaseElement element = new EarthMap().getElement(0) ;
switch ( element )
{
case Node node -> node.getLocation() ; // Method defined in class `Node`, not `BaseElement`.
case Way way -> way.someWaySpecificMethod();
case Relation relation -> relation.someRelationSpecificMethod();
}
… with the compiler verifying you have covered all possible cases.
Such an approach can be used instead of the cumbersome Visitor Pattern mentioned in the Comments on this Question. The pattern-matching and sealed-class features will generally make for more transparent and straightforward code.
When you retrieve an object from a container using Java Generics, the object does not “lose its type”. The object’s type is masked, being returned as the more general type parameterized on the collection. But the object still knows its own type.
We can verify this fact by way of polymorphism. If the subclasses have their own distinct behavior, we should see that behavior rather than the superclass behavior if their type is preserved.
Let's use a simple example, a superclass of Animal
with two subclasses, Dog
& Cat
.
public abstract class Animal {
public void eat () {
System.out.println( "Eating." );
}
}
public class Dog extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat and veg. Woof." );
}
}
public class Cat extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat. Meow." );
}
}
An app to call eat
method on each of the animals in a sample collection.
List < Animal > animals =
List.of(
new Cat() ,
new Cat() ,
new Dog()
);
animals.stream().forEach( Animal :: eat );
When run.
Eating meat. Meow.
Eating meat. Meow.
Eating meat and veg. Woof.
So, we can see the cats still know they are cats, and the dog still knows it is a dog.
Your Question asks about methods not defined on the superclass, methods that are specific to a particular subclass.
For that, let’s add Dog#snooze
method and a Cat#petMe
method, each specific to that class only.
public class Dog extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat and veg. Woof." );
}
public void snooze () {
System.out.println( "I’m gonna take a nap at your feet, if you don’t mind." );
}
}
public class Cat extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat. Meow." );
}
public void petMe () {
System.out.println( "Pet me, now, servant!" );
}
}
To call those specific methods, we need to cast each object from the generic Animal
to its more specific concrete class.
for ( Animal animal : animals )
{
if ( animal instanceof Dog )
{
Dog dog = ( Dog ) animal;
dog.snooze();
}
if ( animal instanceof Cat )
{
Cat cat = ( Cat ) animal;
cat.petMe();
}
}
When run.
Pet me, now, servant!
Pet me, now, servant!
I’m gonna take a nap at your feet, if you don’t mind.
instanceof
In Java 16 and later, we can now shorten that code testing instanceof
. On a single line we can test, cast, and assign to a variable. See JEP 394: Pattern Matching for instanceof.
// Java 16+ ➤ JEP 394: Pattern Matching for instanceof
for ( Animal animal : animals )
{
if ( animal instanceof Dog dog )
{
{
dog.snooze();
}
}
else if ( animal instanceof Cat cat )
{
cat.petMe();
}
else
{
System.out.println( "Oops, encountered unexpected type of animal." );
}
}
switch
This work gets even simpler using a new feature being previewed in Java 17.
JEP 406: Pattern Matching for switch (Preview) brings that instanceof
pattern-matching to the switch
statement.
Beware: You need to set your project & runtime to enable this not-yet-official feature. By default, disabled.
// Preview feature in Java 17 ➤ JEP 406: Pattern Matching for switch (Preview)
for ( Animal animal : animals )
{
switch ( animal )
{
case Dog dog -> dog.snooze();
case Cat cat -> cat.petMe();
case null -> System.out.println( "Oops, no animal." );
default -> System.out.println( "Oops, encountered unexpected type of animal." );
}
}
Java 17, the current Long-Term Support (LTS) version, brings another new feature: Sealed classes. In a sealed class, you declare for a particular superclass all possible subclasses. The superclass is then considered closed for extension by any others. Therefore, the compiler can identify, at compile-time, a list of all subclasses.
Notice the default
at bottom of that switch
in our code above. It serves as a catch-all in case we have not accounted for all possible sub-types of Animal
. The compiler can make use of the sealed-class list of known subclasses to tell if our switch
statement has covered all possible cases.
If we seal our animal, dog, and cat classes:
// Mark the class as `sealed`.
public abstract sealed class Animal permits Cat, Dog
{
public void eat () {
System.out.println( "Eating." );
}
}
// Mark as `final` as one way to seal the superclass.
public final class Dog extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat and veg. Woof." );
}
public void snooze () {
System.out.println( "I’m gonna take a nap at your feet, if you don’t mind." );
}
}
// Mark as `final` as one way to seal the superclass.
public final class Cat extends Animal {
@Override
public void eat () {
System.out.println( "Eating meat. Meow." );
}
public void petMe () {
System.out.println( "Pet me, now, servant!" );
}
}
… then we can further simplify that code above by omitting the default
case. Furthermore, if we later add another subtype, the compiler will alert us by emitting a compiler error. If we edit our code to omit any of the subclasses (Dog
case or Cat
case here), the compiler likewise emits a compiler error.
// Preview feature in Java 17 ➤ JEP 406: Pattern Matching for switch (Preview)
// Combined with Java 17+ feature ➤ JEP 409: Sealed Classes
for ( Animal animal : animals )
{
switch ( animal )
{
case Dog dog -> dog.snooze();
case Cat cat -> cat.petMe();
case null -> System.out.println( "Oops, no animal." );
// NO LONGER NEEDED… default -> System.out.println( "Oops, encountered unexpected type of animal." );
}
}
If you want to collect BaseElement
objects, then retrieve them and treat them as their more specific narrower type (Node
, Way
and Relation
), just cast. Or let the latest Java features do the casting for you.
Upvotes: 2
Reputation:
Suggested changes to your code:
public abstract class Main<BaseElement>
private final ArrayList<BaseElement> elements;
protected BaseElement addElement(BaseElement element)
There is no need to use extend
as it is already an abstract class
after that, any subclass of BaseELement will work fine :)
Upvotes: 1