← Java tutorials

LESSON 22 / 40 · FREE TUTORIAL

One Command, Different Attacks

Use parent class contract to command different objects

90–110 minutesThis chapter uses JDK 21 and does not require the preview function.Face Object Progress

WHAT YOU WILL MAKE

Making Mixed Battle Waves BattleWave

The commander only shouts attack once, zombie swipes, skeleton shoots arrows, and spider bites. Follow the reference to the real object, and you'll see how Java selects the correct method and completes the mixing wave. Front-end backpack: Enemy family, extends, super and method rewriting in Lesson 21. Let the entire wave of monsters listen to the same command today.

Before you start: It is recommended to complete Lesson 21 first

The object is found by reference and the object provides overriding behavior
  1. #1 · Skeleton objectattack() returns archery description
VariableRefers to
Enemy enemy#1

01 / 13

Before starting: predict first, then let the program answer

This chapter uses JDK 21 and does not require the preview function. The story is borrowed from the block world, and the example runs on the ordinary Java console; if the values ​​​​and rules are course models, they will be clearly stated.

Each common example is a complete independent file, saved according to the file name marked on the box. Place each example in a different folder to avoid duplicate names of auxiliary classes. When the complete project is divided into sections, they are connected sequentially in the same file.

Terminal command: replace the Chinese placeholder with the actual file name and class name

javac -encoding UTF-8 ExampleFile.java
java ExampleClass
  • Prediction: Write the expected output first, or draw out the changes in references and data.
  • Verification: Check line by line after running, and then only change one condition.
  • Explanation: Explain whether the results come from Java rules, course contracts, or input data.

02 / 13

The parent class reference accommodates the subclass object

Enemy enemy = new Skeleton(...) The left side shows that the program uses it through the Enemy contract, and the right side determines that the Skeleton is actually created.

The object is found by reference and the object provides overriding behavior
  1. #1 · Skeleton objectattack() returns archery description
VariableRefers to
Enemy enemy#1

L22Example01.java · Complete independent example

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
}
class Skeleton extends Enemy {
    public String reload() { return name + " Supplementary arrows"; }
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " Archery"; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
}
public class L22Example01 {
    public static void main(String[] args) {
        Enemy a = new Zombie("Carrion");
        Enemy b = new Skeleton("Xiaobai");
        System.out.println(a.attack());
        System.out.println(b.attack());
    }
}

Output

Carrion Swing
Xiaobai Archery

03 / 13

Dynamic dispatch selection actual method

Java uses dynamic dispatch for instance methods. The compiler first confirms that there is an attack in the Enemy contract, and then executes the rewritten version of the actual subclass at runtime.

The object is found by reference and the object provides overriding behavior
  1. #1 · Skeleton objectattack() returns archery description
VariableRefers to
Enemy enemy#1

L22Example02.java · Complete independent example

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
}
class Skeleton extends Enemy {
    public String reload() { return name + " Supplementary arrows"; }
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " Archery"; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
}
public class L22Example02 {
    public static void main(String[] args) {
        Enemy e = new Skeleton("Xiaobai");
        System.out.println(e.attack());
        e = new Zombie("Carrion");
        System.out.println(e.attack());
    }
}

Output

Xiaobai Archery
Carrion Swing

04 / 13

Mixed waves under parent class number assembly

If the array element type is written as Enemy, all Enemy subclasses can be placed in the same wave.

An Enemy array holds multiple subclasses
  1. Enemy[]Save references of the same family
  2. Array orderDetermine the order of appearance
  3. Dynamic dispatchDecide on attack method

L22Example03.java · Section 1/2, sequentially connected to the same file

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
    public int damage() { return 1; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
    @Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
    Skeleton(String name) { super(name); }
    public String reload() { return name + " Supplementary arrows"; }
    @Override public String attack() { return name + " Archery"; }
    @Override public int damage() { return 6; }
}
class Spider extends Enemy {
    Spider(String name) { super(name); }
    @Override public String attack() { return name + " Bite"; }
    @Override public int damage() { return 5; }
}
public class L22Example03 {
    static void runWave(Enemy[] wave) {
        if (wave == null) return;
        for (Enemy enemy : wave)
            if (enemy != null)
                System.out.println(enemy.attack() + " Damage=" + enemy.damage());

L22Example03.java · Paragraph 2/2, sequentially connected to the same file

    }
    static long totalDamage(Enemy[] wave) {
        if (wave == null) return 0;
        long total = 0;
        for (Enemy enemy : wave)
            if (enemy != null) total += enemy.damage();
        return total;
    }
    public static void main(String[] args) {
        Enemy[] wave = {new Zombie("Carrion"), new Skeleton("Xiaobai"),
                        new Spider("Spider")};
        runWave(wave);
    }
}

Output

Carrion Swing Damage=4
Xiaobai Archery Damage=6
Spider Bite Damage=5

05 / 13

Method parameters use parent class contract

If the method only requires the ability of Enemy, the parameters will be written as Enemy, and there is no need to repeat a method for each subclass.

One entrance serves the entire enemy family
  1. Parameter EnemyRely only on common capabilities
  2. null elementThis lesson is explicitly skipped
  3. Empty arrayTotal damage is 0

L22Example04.java · Section 1/2, sequentially connected to the same file

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
    public int damage() { return 1; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
    @Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
    Skeleton(String name) { super(name); }
    public String reload() { return name + " Supplementary arrows"; }
    @Override public String attack() { return name + " Archery"; }
    @Override public int damage() { return 6; }
}
class Spider extends Enemy {
    Spider(String name) { super(name); }
    @Override public String attack() { return name + " Bite"; }
    @Override public int damage() { return 5; }
}
public class L22Example04 {
    static void runWave(Enemy[] wave) {
        if (wave == null) return;
        for (Enemy enemy : wave)
            if (enemy != null)
                System.out.println(enemy.attack() + " Damage=" + enemy.damage());

L22Example04.java · Paragraph 2/2, sequentially connected to the same file

    }
    static long totalDamage(Enemy[] wave) {
        if (wave == null) return 0;
        long total = 0;
        for (Enemy enemy : wave)
            if (enemy != null) total += enemy.damage();
        return total;
    }
    public static void main(String[] args) {
        runWave(new Enemy[]{null, new Skeleton("Xiaobai")});
        runWave(null);
        System.out.println(totalDamage(new Enemy[0]));
    }
}

Output

Xiaobai Archery Damage=6
0

06 / 13

The parent class reference only looks at the contract entry

Even if the actual object is a Skeleton, an Enemy reference can only directly call members declared in the Enemy.

Objects may have more references exposing only Enemy members
  1. attack / damageAlready in the parent class contract
  2. reloadOnly unique to skeletons
  3. Compilation checkParent class reference cannot directly add arrows

BadReload.java · Intentional error example

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
}
class Skeleton extends Enemy {
    public String reload() { return name + " Supplementary arrows"; }
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " Archery"; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
}
public class BadReload {
    public static void main(String[] args) {
        Enemy e = new Skeleton("Xiaobai");
        System.out.println(e.reload());
    }
}

07 / 13

Downward transformation should be limited and safe

Downcast treats Enemy as a more specific Skeleton. Use it only if you really need the subclass-specific capabilities, and you must check them first.

First prove the actual type and then access the subclass-specific members
  1. instanceof checkIs it indeed Skeleton?
  2. Mode variable sArrows can be replenished after successful matching
  3. Forced transformationIf there is no match, an exception will be thrown.

L22Example06.java · Complete independent example

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
}
class Skeleton extends Enemy {
    public String reload() { return name + " Supplementary arrows"; }
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " Archery"; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
}
public class L22Example06 {
    public static void main(String[] args) {
        Enemy e = new Skeleton("Xiaobai");
        if (e instanceof Skeleton s) System.out.println(s.reload());
        e = new Zombie("Carrion");
        System.out.println(e instanceof Skeleton);
    }
}

Output

Xiaobai Supplementary arrows
false

BadCast.java · Intentional error example

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
}
class Skeleton extends Enemy {
    public String reload() { return name + " Supplementary arrows"; }
    Skeleton(String name) { super(name); }
    @Override public String attack() { return name + " Archery"; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
}
public class BadCast {
    public static void main(String[] args) {
        Enemy e = new Zombie("Carrion");
        Skeleton s = (Skeleton) e;
        System.out.println(s.reload());
    }
}

08 / 13

Let objects replace type branches

If the main program determines the attack method according to the type, each branch will be modified when adding new enemies. Overriding makes the object responsible for its own behavior.

The behavior is written into the subclass and the loop only sends commands
  1. Join WitchNew class implementation attack
  2. Old cycleNo type branch added
  3. Extended verificationProve compatibility with output

L22Example07.java · Section 1/2, sequentially connected to the same file

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
    public int damage() { return 1; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
    @Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
    Skeleton(String name) { super(name); }
    public String reload() { return name + " Supplementary arrows"; }
    @Override public String attack() { return name + " Archery"; }
    @Override public int damage() { return 6; }
}
class Spider extends Enemy {
    Spider(String name) { super(name); }
    @Override public String attack() { return name + " Bite"; }
    @Override public int damage() { return 5; }
}
class Witch extends Enemy {
    Witch(String name) { super(name); }
    @Override public String attack() { return name + " Throw potion"; }
    @Override public int damage() { return 7; }
}
public class L22Example07 {

L22Example07.java · Paragraph 2/2, sequentially connected to the same file

    static void runWave(Enemy[] wave) {
        if (wave == null) return;
        for (Enemy enemy : wave)
            if (enemy != null)
                System.out.println(enemy.attack() + " Damage=" + enemy.damage());
    }
    static long totalDamage(Enemy[] wave) {
        if (wave == null) return 0;
        long total = 0;
        for (Enemy enemy : wave)
            if (enemy != null) total += enemy.damage();
        return total;
    }
    public static void main(String[] args) {
        runWave(new Enemy[]{new Witch("Purple robe"), new Spider("Spider")});
    }
}

Output

Purple robe Throw potion Damage=7
Spider Bite Damage=5

09 / 13

wave simulator test bench

The wave program checks the order, each attack, total damage, and incorrect input.

Mixed objects, empty waves and empty elements must be verified
  1. Sequential testingAppearance item by item in array
  2. Border testingEmpty arrays and null
  3. Total damageAdd the damage of each object
Acceptance comparison table
SceneEnterExpected
NormalThree kinds of enemiesOutput three attacks in order
BorderEmpty arrayNo output, total damage 0
ErrorArray contains nullSkip without crashing
ExtensionJoin WitchThe old cycle does not need to be modified
ReturnTotal damageEqual to the sum of damage of each object

10 / 13

Challenge Blueprint Unification Wave

The challenge program is called BattleWave. The three subclasses return attack description and damage respectively, and the two general methods are responsible for playing waves and counting total damage.

Parent class array unified appearance attack and damage statistics
  1. runWavePlay each attack
  2. totalDamageOnly summarize numerical values
  3. Enemy ContractTwo methods share the same entrance

Acceptance conditions

  • The same Enemy array holds three subclasses.
  • instanceof does not appear in runWave.
  • Each object outputs its own attack.
  • totalDamage returns 15.
  • Empty waves return 0, and null elements are safely skipped.

11 / 13

Challenges Realizing Combat Waves

Create a new BattleWave.java and run it in the Java 21 console.

A loop triggers three actual behaviors
  1. Three kinds of objects4, 6, 5 points of damage
  2. null slotExplicitly skip
  3. long total numberGet 15

BattleWave.java · Section 1/2, sequentially connected to the same file

class Enemy {
    protected final String name;
    Enemy(String name) { this.name = name; }
    public String attack() { return name + " Normal attack"; }
    public int damage() { return 1; }
}
class Zombie extends Enemy {
    Zombie(String name) { super(name); }
    @Override public String attack() { return name + " Swing"; }
    @Override public int damage() { return 4; }
}
class Skeleton extends Enemy {
    Skeleton(String name) { super(name); }
    public String reload() { return name + " Supplementary arrows"; }
    @Override public String attack() { return name + " Archery"; }
    @Override public int damage() { return 6; }
}
class Spider extends Enemy {
    Spider(String name) { super(name); }
    @Override public String attack() { return name + " Bite"; }
    @Override public int damage() { return 5; }
}
public class BattleWave {
static void runWave(Enemy[] wave) {
    if (wave == null) return;
    for (Enemy enemy : wave)
        if (enemy != null)
            System.out.println(enemy.attack() + " Damage=" + enemy.damage());

BattleWave.java · Paragraph 2/2, sequentially connected to the same file

}
static long totalDamage(Enemy[] wave) {
    if (wave == null) return 0;
    long total = 0;
    for (Enemy enemy : wave)
        if (enemy != null) total += enemy.damage();
    return total;
}
    public static void main(String[] args) {
        Enemy[] wave = {new Zombie("Carrion"), new Skeleton("Xiaobai"),
                        new Spider("Spider"), null};
        runWave(wave);
        System.out.println("Total damage=" + totalDamage(wave));
        System.out.println("Empty wave times=" + totalDamage(new Enemy[0]));
    }
}

Output

Carrion Swing Damage=4
Xiaobai Archery Damage=6
Spider Bite Damage=5
Total damage=15
Empty wave times=0

12 / 13

Expansion tasks and clearance review

Assignments must indicate the declared type and actual object type of each variable.

Three levels of mission training unified command
  1. Standard declaration typeFor example Enemy
  2. mark actual typeFor example Spider
  3. Standard call resultBite comes from rewrite method

Third gear operation

  • Wooden pickaxe: Use the Enemy array to uniformly output three attacks.
  • Iron Pickaxe: Add loot method to Enemy, and use polymorphic unified settlement to drop.
  • Diamond Pickaxe: Added Healer to allow the same takeTurn contract to attack as well as heal, and indicate whether an interface should be used instead.
Acceptance comparison table
Self-evaluationCanStill need to practice
I understand the parent class reference□□
I can explain dynamic dispatch□□
I would avoid type branches□□

13 / 13

Own acceptance record

Beautiful output is the starting point, and being able to explain boundaries is considered mastery. Select one case each from normal, borderline and error, write down your expectations before running, and leave evidence after running.

Leave a basis for judgment
CategoryThe operation you selectedCheck key points
NormalA common input or a successful actionIs the result consistent line by line?
BorderRange endpoints and adjacent valuesIs there one more step or one less thing?
ErrorIllegal quantity, missing object or bad dataWhether it is explicitly rejected and whether the status is maintained
  • Bug records: triggering conditions, actual phenomena, causes, and minimum repairs.
  • Illustrated record: describe the starting point, end point and meaning of each arrow.
  • Adaptation record: only change one rule, and modify expectations and tests at the same time.

TAKE IT WITH YOU

Take the lesson offline

The complete lesson, code, diagrams and exercises, in your preferred language.