The elements of the subclass of an arrayist will not use the replaced method in Java

advertisements

I have an ArrayList with a certain class (Piece) from which I want to pick items with a subclass (Miner and Bomb). However, when I pick some items it will use the methods of the super class (Piece), and not it's subclasses.

Since ArrayList<Pieces> will contain several subclass objects of pieces, it is not possible to define a more specific class for ArrayList. Is it possible to pick some elements from ArrayList and still use the methods from the subclasses?

(I found it a little hard to find the solution for this simple problem. I searched for overriding, arrayList, methods along some other things, but couldn't find it. If it was too obvious to found, I'd like to know where you found it.)

Class Main:

import java.util.ArrayList;
public class Main {

    public static void main(String[] args) {
        ArrayList<Piece> pieceList = new ArrayList<>();
        pieceList.add(new Miner());
        pieceList.add(new Bomb());

        // create new classes to test canDefeat
        if (new Miner().canDefeat(new Bomb())) {
            System.out.println("Yes! This line will be printed.");
        }

        System.out.println();

        // test canDefeat with pieces from the pieceList
        if (pieceList.get(0).canDefeat(pieceList.get(1))) {
               System.out.println("No, this line will not be printed");
        }

    }
}

Class Piece:

public abstract class Piece {

    public boolean canDefeat(Piece opponent) {
        return false;
    }
}

Class Miner:

public class Miner extends Piece {

    public boolean canDefeat(Bomb opponent) {
        return true;
    }

}

Class Bomb:

public class Bomb extends Piece {

}


The why of this is easy:

  1. Your Miner doesn't override canDefeat(Piece), it adds a new method with the signature canDefeat(Bomb).

  2. In the code pieceList.get(0).canDefeat(pieceList.get(1)), all the compiler has to work with is the type Piece, and so it resolves that to Piece#canDefeat(Piece).

The what to do instead is more tricky.

I would suggest that what kinds of pieces can defeat what other kinds of pieces isn't an attribute of each piece; for one thing, that will get brittle fairly quickly if you change the rules.

Instead, I would look at alternatives like:

  • Perhaps each piece can have a strength level, where higher strength means the piece can defeat a piece with lower strength. (Example below.)

  • You could have a BattleLogic class that can compare types of pieces to determine the outcome, which would either work with strength per the above, or by type using instanceof, etc. Normally, when you reach for instanceof you need to stop and think about what you're doing and whether it's really the right tool for the job (it usually isn't), but it may well be the right tool for this job. The advantage to a BattleLogic class is that none of the pieces knows anything about the other pieces; instead, you have one thing that understands the game as a whole and how the pieces rank when they fight. (Example below.)

If you want each Piece to know what other types of pieces it can defeat, you can do that, but I think it will tend to lead to an unmaintainable mess. Two ways come to mind:

  • Having a list of the types it can defeat. (Example below.)

  • Using reflection to achieve what you were trying to do originally. (Example below.)

Here's the strength suggestion in code:

import java.util.ArrayList;

class Piece {

    private int strength;

    public Piece(int strength) {
        this.strength = strength;
    }

    public int getStrength() {
        return this.strength;
    }

    public boolean canDefeat(Piece opponent) {
        return this.getStrength() > opponent.getStrength();
    }
}

class Miner extends Piece {

    public Miner() {
        super(10);
    }
}

class Bomb extends Piece {
    public Bomb() {
        super(5);
    }
}

public class Main {

    public static void main(String[] args) {
        ArrayList<Piece> pieceList = new ArrayList<>();
        pieceList.add(new Miner());
        pieceList.add(new Bomb());

        // create new classes to test canDefeat
        if (new Miner().canDefeat(new Bomb())) {
            System.out.println("Yes! This line will be printed.");
        }

        System.out.println();

        // test canDefeat with pieces from the pieceList
        if (pieceList.get(0).canDefeat(pieceList.get(1))) {
            System.out.println("Yes! This line will also be printed");
        }

    }
}

And the BattleLogic suggestion in code:

import java.util.ArrayList;

abstract class Piece {
}

class Miner extends Piece {
}

class Bomb extends Piece {
}

class BattleLogic {

    /**
     * Does a battle between the two pieces.
     *
     * @param   a   The first piece
     * @param   b   The second piece
     * @return  0 for a tie, a negative number if a beats b, or a positive number if b beats a
     */
    public static int battle(Piece a, Piece b) {
        if (a instanceof Miner && b instanceof Bomb) {
            return -1;
        }
        // ...further tests here...
        return 0;
    }

}

public class Main {

    public static void main(String[] args) {
        ArrayList<Piece> pieceList = new ArrayList<>();
        pieceList.add(new Miner());
        pieceList.add(new Bomb());

        // create new classes to test canDefeat
        if (BattleLogic.battle(new Miner(), new Bomb()) < 0) {
            System.out.println("Yes! This line will be printed.");
        }

        System.out.println();

        // test canDefeat with pieces from the pieceList
        if (BattleLogic.battle(pieceList.get(0), pieceList.get(1)) < 0) {
            System.out.println("Yes! This line will also be printed");
        }

    }
}

Here's that third suggestion, having each type of piece know what pieces it can defeat, in case. Again, though, I don't suggest this; I think having the pieces know this much about each other is going to be a maintenance problem:

// Not really recommended
import java.util.List;
import java.util.ArrayList;

abstract class Piece {
    private List<Class> iDefeat;

    public Piece(Class... defeats) {
        this.iDefeat = new ArrayList<Class>();
        for (Class c : defeats) {
            this.iDefeat.add(c);
        }
    }

    public boolean canDefeat(Piece piece) {
        return this.iDefeat.contains(piece.getClass());
    }
}

class Bomb extends Piece {
}

class Miner extends Piece {

    public Miner() {
        super(Bomb.class);
    }
}

public class Main {

    public static void main(String[] args) {
        ArrayList<Piece> pieceList = new ArrayList<>();
        pieceList.add(new Miner());
        pieceList.add(new Bomb());

        // create new classes to test canDefeat
        if (new Miner().canDefeat(new Bomb())) {
            System.out.println("Yes! This line will be printed.");
        }

        System.out.println();

        // test canDefeat with pieces from the pieceList
        if (pieceList.get(0).canDefeat(pieceList.get(1))) {
            System.out.println("Yes! This line will also be printed");
        }

    }
}

Or you could make the list static if you're going to have millions of pieces and memory is an issue; this is just a sketch.

And finally, the way using reflection:

// Not reall recommended
import java.lang.reflect.Method;
import java.util.ArrayList;

class Piece {

    public Piece() {
    }

    private static Method getMethodOrNull(Class cls, String name, Class... paramTypes) {
        try {
            return cls.getMethod(name, paramTypes);
        }
        catch (NoSuchMethodException nsme) {
            return null;
        }
    }

    public boolean canDefeat(Piece opponent) {
        try {
            Class thisClass = this.getClass();
            Class oppClass = opponent.getClass();
            Method m = null;

            while (oppClass != Piece.class && (m = getMethodOrNull(thisClass, "canDefeat", oppClass)) == null) {
                oppClass = oppClass.getSuperclass();
            }
            if (m != null) {
                return (boolean)m.invoke(this, opponent);
            }
            return false;
        }
        catch (Exception iae) {
            throw new RuntimeException("Can't access method", iae);
        }
    }
}

class Miner extends Piece {

    public boolean canDefeat(Bomb b) {
        return true;
    }

}

class Bomb extends Piece {
}

public class Main {

    public static void main(String[] args) {
        ArrayList<Piece> pieceList = new ArrayList<>();
        pieceList.add(new Miner());
        pieceList.add(new Bomb());

        // create new classes to test canDefeat
        if (new Miner().canDefeat(new Bomb())) {
            System.out.println("Yes! This line will be printed.");
        }

        System.out.println();

        // test canDefeat with pieces from the pieceList
        if (pieceList.get(0).canDefeat(pieceList.get(1))) {
            System.out.println("Yes! This line will also be printed");
        }

    }
}