Generics - DmitryGontarenko/usefultricks GitHub Wiki

About generics

Generics (обобщения) - обобщения позволяют типам (классам и интерфейсам) быть параметрами при определении класов, интерфейсов и методов. Передача разных параметров дает нам возможность повторно использовать один и тот же код.
Еще одно преимущеста обобщений в том, что они позволяют предотвращать ошибки времени выполнения (runtime) при приобразовании типов, переводя такие ошибки в ошибки компиляции (compile exception). Это намного упрощает поиск ошибок и делает код безопаснее.

Рассмотрми небольшой пример:

public class GenericsApplication {
    public static void main(String[] args) {
        GenericTest<String> t1 = new GenericTest<>("Paul");
        GenericTest<Integer> t2 = new GenericTest<>(17);

        t1.display(); // Paul
        t2.display(); // 17
    }
}

class GenericTest<T> {
    private T obj;

    /**
     * Пример обобщенного типа в качестве возвращаемого значения
     */
    public T getObj() {
        return obj;
    }

    public GenericTest(T obj) {
        this.obj = obj;
    }

    void display() {
        System.out.println(obj);
    }
}

При создании экземпляра мы параметризировали класс типами String и Integer.
Таким образом, если мы захотим присвоить одну переменную другой - это приведет к ошибки компиляции:

        t1 = t2; // Incompatible types. Required: String, Found: Integer

А если не использовать обобщения при создании экземпляров или заменить тип T на тип Object, то такой код не вызовет ошибок компиляции, но при запуске программы вызовет ошибку времени выполнения:

        GenericTest t1 = new GenericTest("Paul");
        GenericTest t2 = new GenericTest(17);

        t1 = t2;
        String text = (String) t1.obj; // ClassCastException: java.lang.Integer cannot be cast to java.lang.String

Multiple Type Parameters

Generic-класс может иметь несколько параметров типа.
Например, создадим класс, который будет хранить пару ключ-значение:

public class GenericsApplication {
    public static void main(String[] args) {
        Pair<String, Integer> pair1 = new Pair<>("Sarah", 18);
        Pair<String, String> pair2 = new Pair<>("Russia", "st. Petersburg");

        System.out.println("Name: " + pair1.getKey() + ", age: " + pair1.getValue()); 
        // Name: Sarah, age: 18
        System.out.println("Country: " + pair2.getKey() + ", city: " + pair2.getValue()); 
        // Country: Russia, city: st. Petersburg
    }
}

class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

Generic methods

Обобщенные методы - это методы, которые могут использовать свои собственные параметры типа. Область действия таких параметров будет ограничена методом, в котором они объявлены.
Параметры указываются внутри угловых скобок, перед типом возвращаемого значения, например:
public static <V> int method(V value);

Рассмотрим на примере:

public class GenericsApp {
    public static void main(String[] args) {
        Pair<Integer, String> p1 = new Pair<>(1, "Sarah");
        Pair<Integer, String> p2 = new Pair<>(1, "Sarah");

        boolean result = Service.compare(p1, p2);
        System.out.println(result); // true
    }
}

@AllArgsConstructor
class Pait<K, V> {
    private K key;
    private V value;
}

class Service {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        return p1.getKey().equals(p2.getKey()) &&
               p2.getValue().equals(p2.getValue());
    }
}

Полный синтаксис для вызова метода будет с использованием ковычек перед именем самого метода, но этого указывать не обязательно:

boolean result2 = Service.<Integer, String>compare(p1, p2);

Generics extends

Все Generics в коде существуют только в момент компиляции, а в момент выполнения (запуска программы) заменяются на тип Object. Тем самым Generic работают и помогают в поиске ошибок именно в моменте компиляции.
Исходя из этого, мы не можем вызвать у Обобщенного типа какой-либо кастомный метод (кроме методов самого класса Object).

Рассмотрим на примере - мы написали обобщенный класс и хотим вызвать в цикле метод из этого класса:

class Service<T> {
    T[] array;

    void display() {
        for (T temp : array) {
            temp.someMethod(); // Error: Cannot resolve method 'someMethod()'
        }
    }
}

В таком случае мы получим ошибку компиляции, т.к. у класса Object отсутствует такой метод.

На помощью нам придет возможность наследования в Generics - <T extends Class>.
Рассмотрим готовый пример с использованием наследования:

/**
 * Клиентский код
 */
public class GenericsApplication {
    public static void main(String[] args) {
        Backend[] backends = {
                new Backend("Java"),
                new Backend("Python")
        };
        Service<Developer> service = new Service<>(backends);
        service.display(); // Developer writes Java code
                           // Developer writes Python code
    }
}

interface Developer {
    void write();
}

@AllArgsConstructor
class Backend implements Developer  {
    private String language;

    @Override
    public void write() {
        System.out.println("Developer writes " + language + " code");
    }
}

@AllArgsConstructor
class Service<T extends Developer> {
    private T[] array;

    void display() {
        for (T temp : array) {
            temp.write();
        }
    }
}

В данном случае мы указали, что обобщенный тип T наследуется от типа Developer и может вызывать его методы.

Multiple extends

В Generics есть возможность наследоваться сразу от нескольких классов или интерфейсов.
Рассмотрим синтаксис:

class A { /* ... */  }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }
class F <T extends A & B, U extends C> { /* ... */ }

Наследование нескольких классов или интерйесов для одного параметра осуществляется с помощью символа &.
Если же нужно указать несколько параметров, используется запятая.
ВАЖНО! При множественном наследовании первым должен стоять класс (если он пристутствует), а затем уже интерфесы. Иначе будет получена ошибка компиляции.

  • можно использовать только один класс

Wildcards

В Generics коде символ знак вопроса ? называется wildcard и представляет неизвестный тип. Это значит Wildcard можно использовать в качестве типа параметра, поля или локальной переменной.

Например, когда в одном методе, в качестве параметров передаются экземпляры Generics-классов с разными generics-типами - необходимо использовать Wildcard:

public class WildcardApp {
    public static void main(String[] args) {
        Employee[] arrays1 = { new Fullday(16), new Fullday(43), new Fullday(25) };
        Employee[] arrays2 = { new Partday(24), new Partday(21), new Partday(19) };

        Util<Employee> u1 = new Util<>(arrays1);
        Util<Employee> u2 = new Util<>(arrays2);

        Util.compare(u1, u2); // 20
    }
}

interface Employee {
    int getAge();
}

@AllArgsConstructor
class Fullday implements Employee {
    private int age;

    @Override
    public int getAge() {
        return age;
    }
}

@AllArgsConstructor
class Partday implements Employee {
    private int age;

    @Override
    public int getAge() {
        return age;
    }
}

@AllArgsConstructor
class Util<T extends Employee> {
    private T[] ages;

    static void compare(Util<?> a1, Util<?> a2) {
        System.out.println(a1.countAge() - a2.countAge());
    }

    private int countAge() {
        int sum = 0;
        for (T temp : ages) {
            sum += temp.getAge();
        }
        return sum;
    }
}

Есть несколько типов Wildcard:

  • Unbounded - тип класса для подстановки ничем не ограничивается, например List<?>;
  • Upper-bounded - с помощью ключевого слова extends, для типа класса при подстановке указывается верхняя граница, например List<? extends Class>;
  • Lower-bounded - с помощью ключевого слова super, для типа класса при подстановке указывается нижняя границы, например List<? super Class>.

Подробнее можно увидить на примере:

public class WildcardApp {
    public static void main(String[] args) {
        List<A> aList = new ArrayList<>();
        List<B> bList = new ArrayList<>();
        List<C> cList = new ArrayList<>();

        // List<? extends A> a
        Service.upper(aList);
        Service.upper(bList);
        Service.upper(cList);

        // List<? super B> c
        Service.lower(aList);
        Service.lower(bList);
        Service.lower(cList); // Compile Error - метод не может принять тип класса С

        // List<?> list
        Service.unbounded(aList);
        Service.unbounded(bList);
        Service.unbounded(cList);
        Service.unbounded(new ArrayList<String>()); // Ошибки нет, метод может принять даже тип класса String
    }
}


interface A { /* ... */  }

class B implements A { /* ... */ }

class C extends B { /* ... */ }

class Service {
    public static void upper(List<? extends A> a) {
        /*
        Данный метод в качестве параметра может принимать
        интерфейс А и все его реализации
         */
    }

    public static void lower(List<? super B> c) {
        /*
        Данный метод в качестве параметра может принимать
        только класс B и его суперкласс
         */
    }

    public static void unbounded(List<?> list) {
        /*
        Данный метод в качестве параметра может принимать
        любой тип
         */
    }
}

Polymorphism and Generics

Полиморфизм в Обобщениях по разному может работать с массивами и коллекциями.
Например, у нас есть следующая структура классов:

interface A { /* ... */  }
class B implements A { /* ... */ }
class C implements A { /* ... */ }

И мы создаем метод, который принимает массив типа А:

    public static void changeArray(A[] array) {
        array[0] = new B();
        array[1] = new C();
    }

При попытке передать в этот метод массив типа B, ошибки на уровне комптляции не возникнет, но после запуска программы появится ошибка времени выполнения. Т.к. мы пытаемся добавить в массив типа B, элемент типа С:

    B[] arr = { new B(), new B() };
    Service.changeArray(arr); // Runtime Error: в методе arrays попытка записать в массив типа B элемент типа С

В том случае, если мы создадим метод, который будет принимать в качесве параметра коллекцию типа A, мы не сможем передать в этот метод коллекцию какого-либо другого типа, даже если этот тип будет наследником типа A:

    public static void concrete(List<A> a) {
        /*
        В этом методе мы указали в качестве параметра тип А.
        Хотя этот интерфейс и имеет реализацию, передать в этот метод
        в качестве параметра можно будет только коллекцию типа A.
         */
    }

    List<B> bList = new ArrayList<>();
    Service.concrete(bList); // Compile Error: метод принимает коллекцию типа А, а мы пытаемся передать тип В

Решить проблему нам может помочь использование Wildcard.
Создадим метод, который будет принимать Uppder-Wildcard:

    public static void upper(List<? extends A> a) {
        /*
        Этот метод в качестве параметра может принять коллекцию типа А
        и всех его реализаций.
        Но такую коллекцию нельзя будет изменить, а только просматривать.
         */
        a.add(new B()); // Compile Error - изменить коллекцию нельзя
        System.out.println(a.size());
    }

    List<B> bList = new ArrayList<>();
    Service.upper(bList);

Что бы создать метод, который сможет принимать коллекцию и работать с ней, необходимо использовать Lower-Wildcard:

    public static void lower(List<? super B> a) {
        /*
        Этот метод принимает класс B и его суперкласс.
        Такую коллекцию можно будет редактировать
         */
        a.add(new B());
        System.out.println(a.size());
    }

    List<B> bList = new ArrayList<>();
    Service.lower(bList); // 1

Sources

  1. SmartMe University. Обобщения
  2. Java Documentation. Generics
⚠️ **GitHub.com Fallback** ⚠️