Home > Software engineering >  Accessing subclasses from Arraylist
Accessing subclasses from Arraylist

Time:10-23

I'm trying to create a simple rpg game of sorts in Java. Right now, I'm trying to display a random selection of 3 opponents for the player to choose from using a JavaFX listview (battleOptions in the code). In my main class I have this onclick of a button to initiate listview to populate opponents.

public void battle() {
    if (!prep) {
        ArrayList<Enemy> classes = new ArrayList<>();
        Swordsman p1 = new Swordsman();
        classes.add(p1);

        Archer p2 = new Archer();
        classes.add(p2);

        for (int i = 0; i < 3; i  ) {
            battleOptions.getItems().add((classes.get(randomnumber(0, 1))));
        }
    }
}

And this is the Enemy class referenced in the above ArrayList:

public class Enemy {
    public static String name = "Enemy";
    
    int damage = 1;
    int hp = 1;
    int defense = 1;
    int mana = 1;
    int speed = 1;
    int luck = 1;
}
class Swordsman extends Enemy {
    public static String name = "Swordsman";
    public static double damage = 0.3;
    public static double hp = 0.15;
    public static double defense = 0.2;
    public static double mana = 0.1;
    public static double speed = 0.1;
    public static double luck = 0.15;
}

(Archer is the same as Swordsman excluding the name and percentages differing)

In its current state this code does add the subclasses to the list view as: com.example.test.Swordsman/Archer@x.

x differs and I'm assuming this is some location in memory or something. This is obviously not what I want to show the player, rather I want to use Swordsman/Archer.name and show that, but it only shows the enemy class' values for name and everything else, despite the fact that it shows the subclass before.

I have tried changing the ArrayList to Object but then it wouldn't let me use any values (I mean the .name and stuff I don't know what they're called). As well as some other stuff which I don't remember. I would like to avoid having to hardcode to if (randomnumber==1) {...} especially for once I fully flesh out and add more classes.

Sorry for the long post, would appreciate any help/advice!

CodePudding user response:

This is not a solution. Instead, suggestions to improve your code that may or may not fix your problems.

Let start with "Enemy". It is OK to have a base "enemy" class that will set default values for the different types of "enemy" characters to use. This is common in games. Characters typically have default parameters and then the specific races or subtypes will add or subtract points based on the attributes of that type. So, let's do that for "Archer" and "Swordsman"

public class Enemy {
    
    protected final String id;

    protected int damage = 10;
    protected int hp = 10;
    protected int defense = 10;
    protected int mana = 10;
    protected int speed = 10;
    protected int luck = 10;
    
    protected Enemy () {
        Random rnd = new Random();
        int number = rnd.nextInt(99999999);
        id = String.format("d", number);
    }

    // omitted getters and setters
}

I added the final (constant) string field id to capture a unique identifier for each instance. I will use this in the subclasses toString() method to display the name with the unique identifiers. This way, when displaying the characters, each one will display differently to let you know they are not mere copies of each other.

You could redefine all the variables in the subclass like you were attempting to do. For example, you can have damage define in both classes, then in the getter method, you can add them together and return the value like this:

public int getDamage () {
    return this.damage   super.damage;
}

But, why do this? By making the fields in the superclass protected, your subclasses will have direct access to them. If you need to add bonus points to those fields, just add the bonus/penalty points in the constructor.

public final class Swordsman extends Enemy {

    public static final String NAME = "Swordsman";

    public Swordsman() {
        damage  = 20; // added 20 points to the default "damage" field value of 10
        // other fields here
    }
    // getters and setters defined in Enemy class

    @Override
    public String toString() {
        return "Swordsman ["  id   "]";
    }
    
    @Override
    public String getStats() {
        return NAME   super.getStats();
    }
}

This is a much cleaner approach because subclasses of Enemy need not to redefine what Enemy already has. The only thing they need to define is attributes that are unique to their type. For example, maybe you need to define a melee bonus to swordsmen when in close combat, whereas archers may not have melee bonuses. Likewise, archers may have a bonus for ranged attacks whereas swordsmen do not. Those attributes (fields) and methods will need to be added to those classes respectfully.

When running this code using the snippet below

public class Game {
    public static void main(String[] args) {
        Swordsman s1 = new Swordsman();
        Swordsman s2 = new Swordsman();
        Swordsman s3 = new Swordsman();
        Swordsman s4 = new Swordsman();
        
        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        System.out.println(s4);
    }
}

It will display something like this:

Swordsman [04923128]
Swordsman [59484796]
Swordsman [11003419]
Swordsman [12543814]

Other things to note on my implementation:

  1. No static fields: If you need some attribute to be shared across all instances of a class, then you will need to use static for that. For example, name attribute is static in the subclasses. All instances of Swordsman class will be labeled "swordsman". I also made this final so that the value of the attribute cannot be changed. Basically, that field is a shared constant.
  2. Fields in the Enemy class are protected so that they are accessible by its subclasses.
  3. Swordsman class is final so that it cannot be extended. If you need to make a subclass of Swordsman, you will need to remove this qualifier.
  4. Subclass do not need getters and setters. Those should be in the Enemy class

Other things you need to consider, every class should override equals(), hashCode(), and toString(). Your IDE should be able to generate these for you. If a class don't add new fields, the subclass will not need to re-override these methods, with the exception of toString() because most likely you will want to display the object's name field value (along with some other stuff like its hash code).

One last thing I want to point out in case you didn't know, which is my guess based on the code you posted. When you use static to qualify a field or a method, you are not overriding the field or method. You are essentially creating a new method or field in the subclass with the same name and signature. Remember, static members belong to the class and not the instance. This is another reason you shouldn't use static in your implementation. For what you are trying to do, it simply does not make any sense to do so.

One last, last, thing LOL

Why not use Java records for this? Simply because records are immutable and these characters need to be updated constantly (damage during combat, other attributes when leveling up, etc.) For this reason, it is better to use a conventional POJO rather than a Java record. I thought it was important to point this out for anyone who is wondering.

  • Related