Java Arena
Step 1 of 4

instanceof with a pattern

This module is an extra: the MOOC doesn't cover it. It's about two features that work together. Patterns test what kind of object you have and give you its parts in the same step, and sealed types fix the list of classes that may implement an interface. With both, a method can handle each kind of object in its own way, and javac checks that no kind was forgotten. They came to Java in stages, from Java 16 to Java 21, and newer Java code uses them often, so they're worth being able to read and write. The module uses records (module 56) and switch expressions (module 57).

instanceof, then a cast

In module 36, a program asked an object for its type with instanceof, and then cast it to get a variable of that type. A chat app has text messages and photo messages, which are both ChatMessages. Printing the photos the old way:

for (ChatMessage message : chat) {
    if (message instanceof PhotoMessage) {
        PhotoMessage photo = (PhotoMessage) message;
        System.out.println(photo.sender() + " sent " + photo.file());
    }
}

The type PhotoMessage is written three times in two lines, and the cast can't even fail, since the line before it has just checked the type.

A type pattern

Since Java 16, instanceof can be followed by a type and a new variable's name. message instanceof PhotoMessage photo checks the type, and when it matches, it declares the variable photo, which refers to the same object, as a PhotoMessage. PhotoMessage photo is a type pattern, and photo is a pattern variable.

Main.java
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList<ChatMessage> chat = new ArrayList<>();
        chat.add(new TextMessage("Aino", "Lunch at noon?"));
        chat.add(new PhotoMessage("Ben", "menu.jpg", 840));
        chat.add(new TextMessage("Ben", "Sure, see you there"));

        for (ChatMessage message : chat) {
            if (message instanceof TextMessage typed) {
                System.out.println(typed.sender() + ": " + typed.text());
            } else if (message instanceof PhotoMessage photo) {
                System.out.println(photo.sender() + " sent " + photo.file() + ", " + photo.kilobytes() + " kB");
            }
        }
    }
}
ChatMessage.java
public interface ChatMessage {
    String sender();
}
TextMessage.java
public record TextMessage(String sender, String text) implements ChatMessage {
}
PhotoMessage.java
public record PhotoMessage(String sender, String file, int kilobytes) implements ChatMessage {
}
Output
Aino: Lunch at noon?
Ben sent menu.jpg, 840 kB
Ben: Sure, see you there
  • The messages are records. Each record's accessor sender() is the method that the interface ChatMessage asks for.
  • When the object isn't a TextMessage, the test is false and nothing is declared: there's no cast that could throw a ClassCastException.
  • instanceof is false for null, so a pattern variable is never null.

Where the pattern variable exists

A pattern variable exists only where Java knows that the test was true. In the if's own block it can be used; in the else block it can't:

public class Main {
    public static void main(String[] args) {
        Object value = "Lunch at noon?";
        if (value instanceof String text) {
            System.out.println(text.length());
        } else {
            System.out.println(text);
        }
    }
}
What javac prints
Main.java:7: error: cannot find symbol
            System.out.println(text);
                               ^
  symbol:   variable text
  location: class Main
1 error

After &&, the variable can be used in the same condition, since the right side of && is only evaluated when the left side is true:

Object[] values = {"Lunch at noon?", 42, "ok"};
for (Object value : values) {
    if (value instanceof String text && text.length() > 5) {
        System.out.println("long text: " + text);
    }
}
Output
long text: Lunch at noon?

With || it doesn't work: the right side of || runs exactly when the test was false, and then there's no variable.

The test can also be turned around with !. When the method returns as soon as the object isn't of the type, the pattern variable can be used in the rest of the method:

public class Main {
    public static void main(String[] args) {
        System.out.println(shout("see you there"));
        System.out.println(shout(42));
    }

    public static String shout(Object value) {
        if (!(value instanceof String text)) {
            return "not text";
        }
        return text.toUpperCase() + "!";
    }
}
Output
SEE YOU THERE!
not text

Your task

Challenge 1 of 3

The chat app now has stickers too. The method preview in Main.java still uses instanceof and a cast for each kind of message. Rewrite it with type patterns, so that it has no casts left. It should still print the same:

Aino: Lunch at noon?
Ben sent a photo, 840 kB
Aino sent the sticker thumbs up

Challenge 2

The chat app keeps its contacts in a list. A ChatUser has a nickname and a phone number, and two ChatUsers are the same person when their phone numbers are equal, whatever their nicknames. In ChatUser.java, write the method equals(Object compared) the way module 32 did, but with a pattern in place of the cast: true for the very same object, false when compared isn't a ChatUser (null included), and otherwise a comparison of the phone numbers. The class already has a hashCode that agrees with it. The check runs:

ArrayList<ChatUser> contacts = new ArrayList<>();
contacts.add(new ChatUser("Aino", "040 123 4567"));
contacts.add(new ChatUser("Ben", "050 765 4321"));
ChatUser newcomer = new ChatUser("Aino K.", "040 123 4567");
System.out.println(contacts.contains(newcomer));
System.out.println(contacts.indexOf(new ChatUser("Benny", "050 765 4321")));
System.out.println(newcomer.equals("040 123 4567"));
System.out.println(newcomer.equals(null));

and expects:

true
1
false
false

Challenge 3

In a chat, @Aino in a text message mentions Aino. Write the method mentions(ArrayList<ChatMessage> chat, String name) in Main.java. It prints each text message whose text contains @ and the name, as sender: text, in the chat's order, and returns how many it printed. Other kinds of message are skipped. Write the test as one condition: a type pattern, &&, and a check that uses the pattern variable. The check runs:

ArrayList<ChatMessage> chat = new ArrayList<>();
chat.add(new TextMessage("Ben", "@Aino lunch at noon?"));
chat.add(new PhotoMessage("Aino", "menu.jpg", 840));
chat.add(new TextMessage("Chen", "Can I join? @Ben"));
chat.add(new TextMessage("Aino", "Sure @Chen @Ben"));
System.out.println(mentions(chat, "Ben") + " mentions");

and expects:

Chen: Can I join? @Ben
Aino: Sure @Chen @Ben
2 mentions