Pattern Matching for instanceof

Pattern Matching for instanceof to jedna z nowości, która pojawiła się w Javie 14. Stanowi część większego projektu Amber, w skład którego wchodzą także inne usprawnienia takie jak Switch Expressions, rekordy, czy Text Blocks.

Zadaniem omawianego usprawnienia, jest uproszczenie procesu sprawdzania faktycznego typu obiektu oraz jego późniejszego rzutowania. W Javie 14 funkcjonalność ta dostępna jest jako preview feature, więc należy ją włączyć przy pomocy dodatkowej flagi dodawanej podczas uruchamiania programu.

Kurs Java

Problem do rozwiązania

Dosyć popularną sytuacją, którą możesz spotkać w kodzie, jest konieczność weryfikacji rzeczywistego typu obiektu oraz jego późniejsze rzutowanie. Może to mieć znaczenie w sytuacji, w której korzystamy z polimorfizmu jednak w pewnym momencie chcemy wykonać czynność specyficzną dla konkretnego typu obiektu.

Najpopularniejszym przypadkiem, który może tutaj przychodzić do głowy jest metoda equals(), w której możesz spotkać taki zapis:

Person.java

import java.util.Objects;

class Person {
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) 
            return false;
        Person person = (Person) o;
        return Objects.equals(firstName, person.firstName) &&
                Objects.equals(lastName, person.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName);
    }
}

Typ obiektu w metodzie equals sprawdzany jest z wykorzystaniem operatora instanceof . Jest to konieczne, ponieważ metoda ta ma zdefiniowany parametr typu Object . Oznacza to, że w jego miejsce można przekazać dowolny obiekt. Wykonanie rzutowania, czyli Person person = (Person) o, bez wcześniejszego sprawdzenia typu obiektu, przy pomocy operatora instanceof lub metody getClass() , może zakończyć się wyjątkiem ClassCastException.

Pattern Matching for instanceof

W Javie 14, zapis:

if (!(o instanceof Person))
    return false;
Person person = (Person) o;

można zastąpić przy pomocy:

if (!(o instanceof Person person))
    return false;

a więc cała metoda equals() może wyglądać tak:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person person))
        return false;
    return Objects.equals(firstName, person.firstName) &&
            Objects.equals(lastName, person.lastName);
}

Przykład z dziedziczeniem

Nowy wersja operatora instanceof może być przydatna, gdy wykorzystujemy dziedziczenie i posługujemy się ogólnym typem referencji, a chcemy wykonać pewną operację na szczegółowym typie obiektu, np. pobrać jakąś charakterystyczną dla niego wartość.

pattern matching instanceof dziedziczenie

Person.java

import java.util.Objects;

abstract class Person {
    private String firstName;
    private String lastName;

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person person))
            return false;
        return Objects.equals(firstName, person.firstName) &&
                Objects.equals(lastName, person.lastName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstName, lastName);
    }
}

Student.java

import java.util.Objects;

class Student extends Person {
    private int age;

    public Student(String firstName, String lastName, int age) {
        super(firstName, lastName);
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student student)) return false;
        if (!super.equals(o)) return false;
        return age == student.age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), age);
    }
}

Teacher.java

import java.util.Objects;

class Teacher extends Person {
    private double salary;

    public Teacher(String firstName, String lastName, double salary) {
        super(firstName, lastName);
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }

    public void setSalary(double salary) {
        this.salary = salary;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Teacher teacher)) return false;
        if (!super.equals(o)) return false;
        return Double.compare(teacher.salary, salary) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), salary);
    }
}

DifferentPeople.java

class DifferentPeople {
    public static void main(String[] args) {
        Person[] people = new Person[4];
        people[0] = new Student("Jan", "Kowalski", 24);
        people[1] = new Teacher("Martyna", "Zalewska", 7000);
        people[2] = new Student("Karolina", "Wadecka", 21);
        people[3] = new Teacher("Andrzej", "Bartecki", 7900);

        int studentMaxAge = studentMaxAge(people);
        System.out.println("Najstarszy student ma " + studentMaxAge + " lat");
        double avgTeacherSalary = avgSalary(people);
        System.out.println("Średnia wypłata nauczycieli: " + avgTeacherSalary);
    }
    
    static int studentMaxAge(Person[] people) {
        int maxAge = 0;
        for (Person person : people) {
            if (person instanceof Student student)
                if (student.getAge() > maxAge)
                    maxAge = student.getAge();
        }
        return maxAge;
    }
    
    static double avgSalary(Person[] people) {
        double sumSalary = 0;
        int numberOfTeachers = 0;
        for (Person person : people) {
            if (person instanceof Teacher teacher) {
                sumSalary += teacher.getSalary();
                numberOfTeachers++;
            }
        }
        
        if (numberOfTeachers != 0)
            return sumSalary / numberOfTeachers;
        else 
            return 0;
    }
}

Dzięki wykorzystaniu Pattern Matching for instanceof zapis w metodach studentMaxAge() i avgSalary() może być krótszy, bez utraty czytelności.

java console pattern-matching-instanceof-run

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.