1. Terminology

Note
При изучении Generics, используя русскоязычные источники, возникает путаница в терминах, которые используют различные авторы/ресурсы. Для того, что бы этого избежать путаницы следует использовать англоязычные термины.

Приведем терминологию, которая связана с использованием Generics. Она позволит однозначно интерпретировать встречаемые термины.

Термин Расшифровка

Generic types (дженерик-типы/обобщенные типы)

Дженерик-класс или дженерик-интерфейс с одним или несколькими параметрами в заголовке

Generic methods (дженерик-метод/обобщенный метод)

Метод с одним или несколькими type parameters в определении метода

Parameterized types (параметризованный тип)

Вызов дженерик-типа. Для дженерик-типа List<E> параметризованным типом будет, например, List<Box>

Raw type (сырой тип)

Имя дженерик-типа без аргументов типа. Для List<E> сырой тип — это List

Type parameter (параметр типа)

Используются при объявлении дженерик-типов. Для Box<T> T — это параметр типа

Type argument (аргумент типа)

Тип объекта, который может использоваться вместо параметра типа. Например, для Box<Paper> Paper — это аргумент типа

Wildcard (неизвестный тип)

Обозначается символом ? — неизвестный тип

Bounded wildcard (wildcard c ограничением)

Wildcard, который ограничен сверху — <? extends Garbage> или снизу — <? super Garbage>

2. Generics

Generics (aka parameterized types) позволяют нам уйти от жесткого определения используемых типов.

Написание кода с generics имеет ряд преимуществ по сравнению с кодом без generics:

  • Более строгие проверки типов во время компиляции
    Компилятор Java применяет строгую проверку типов к коду с Generics и выдает ошибки, если код нарушает безопасность типов. Исправлять ошибки compile time лучше, чем исправлять ошибки runtime, которые иногда даже сложно локализовать (т.е. найти)

  • Предоставление программистам возможности реализовать общие алгоритмы
    Используя generics, программисты могут реализовать универсальные алгоритмы, которые работают с классами разных типов, а так же безопасны по типу и легче читаются.

  • Избежать постоянного приведения типов

Without Generics
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // need to cast
Use Generics
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast

2.1. Введение

Рассмотрим проблему, в которой Generics могут понадобиться. Допустим, определен класс для представления счета в банке. К примеру, он мог бы выглядеть следующим образом:

class Account {
    private int id;
    private int sum;

    Account(int id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public int getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}

Класс Account имеет два поля: id - уникальный идентификатор счета и sum - сумма на счете.

В данном случае идентификатор задан как целочисленное значение, например: 1, 2, 3, 4 и так далее. Однако также нередко для идентификатора используются и строковые значения (например UUID: "11079c0a-1395-4074-9d4b-41c2733e85d2"). И числовые, и строковые значения имеют свои плюсы и минусы. И на момент написания класса можно точно не знать, что лучше выбрать для хранения идентификатора - строки или числа. Так же, возможно, этот класс будет использоваться другими разработчиками, которые могут иметь свое мнение по данной проблеме. Например, в качестве типа id они захотят использовать какой-то свой класс.

И на первый взгляд можно решить данную проблему следующим образом: задать id как поле типа Object, который является универсальным и базовым суперклассом для всех остальных типов:

public class Account {
    private final Object id;
    private int sum;

    public Account(Object id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public Object getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Program {
    public static void main(String[] args) {
        Account acc1 = new Account(1984, 5000); // id - число
        int acc1Id = (int) acc1.getId();
        System.out.println(acc1Id);

        Account acc2 = new Account("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000); // id - строка
        System.out.println(acc2.getId());
    }
}
Output
1984
11079c0a-1395-4074-9d4b-41c2733e85d2

В данном случае все замечательно работает. Однако в данном случае мы сталкиваемся с проблемой безопасности типов. Например, в следующем случае мы получим ошибку:

public class Program {
    public static void main(String[] args) {
        Account acc1 = new Account("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000);
        int acc1Id = (int) acc1.getId(); // error
        System.out.println(acc1Id);
    }
}
Output
Exception in thread "main" java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

Проблема может показаться искусственной, так как в данном случае видно, что в конструктор передается строка, поэтому вряд ли будет пытаться преобразовать к типу int. Однако в процессе разработки можно не знать, какой именно тип представляет значение в id, и при попытке получить число, в данном случае, приходится столкнуться с исключением java.lang.ClassCastException.

Писать для каждого отдельного типа свою версию класса Account тоже не является хорошим решением, так как в этом случае приходится столкнуться с проблемой дублирования кода.

2.2. Generic classes

Проблема безопасности типов и проблема дублирования кода, в случаях описанных выше, устраняются с помощью Generics (в русскоязычном сообществе иногда называют обобщения). Generics позволяют не указывать конкретный тип, который будет использоваться. Поэтому класс Account можно определить как обобщенный:

public class Account<T> {
    private final T id;
    private int sum;

    public Account(T id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public T getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}

С помощью буквы T в определении класса class Account<T> мы указываем, что данный тип T будет использоваться этим классом. Параметр T в угловых скобках называется type parameter (параметр типа), так как вместо него можно подставить любой тип. При этом пока не известно, какой именно это будет тип: String, Integer или какой-то другой ссылочный тип. Причем буква T выбрана условна, это может и любая другая буква или набор символов.

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

Метод getId() возвращает значение переменной id, но так как данная переменная представляет тип T, то данный метод также возвращает объект типа T: public T getId().

Используем данный класс:

public class Program {
    public static void main(String[] args) {
        Account<String> acc1 = new Account<String>("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000);
        String acc1Id = acc1.getId();
        System.out.println(acc1Id);

        Account<Integer> acc2 = new Account<Integer>(1984, 5000);
        Integer acc2Id = acc2.getId();
        System.out.println(acc2Id);
    }
}
Output
11079c0a-1395-4074-9d4b-41c2733e85d2
1984

При определении переменной данного класса и создании объекта после имени класса в угловых скобках нужно указать, какой именно тип будет использоваться вместо type parameter. При этом надо учитывать, что они работают только с объектами, но не работают с примитивными типами. То есть мы можем написать Account<Integer>, но не можем использовать тип int или double, например, Account<int>. Вместо примитивных типов надо использовать классы-обертки: Integer вместо int, Double вместо double и т.д.

2.3. Generic interfaces

Интерфейсы, как и классы, также могут быть обобщенными. Создадим generic интерфейс Accountable и используем его в программе:

interface Accountable<T> {
    T getId();

    int getSum();

    void setSum(int sum);
}
public class Account implements Accountable<String> {
    private final String id;
    private int sum;

    public Account(String id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public String getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Program {
    public static void main(String[] args) {
        Accountable<String> acc1 = new Account("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000);
        Account acc2 = new Account("c46ef036-3618-4144-b769-a6ba8e32947d", 4300);
        System.out.println(acc1.getId());
        System.out.println(acc2.getId());
    }
}
Output
11079c0a-1395-4074-9d4b-41c2733e85d2
c46ef036-3618-4144-b769-a6ba8e32947d

При реализации подобного интерфейса есть две стратегии:

  • когда при реализации для type parameter интерфейса задается конкретный тип (пример выше), тогда класс, реализующий интерфейс, жестко привязан к этому типу

  • когда при реализации интерфейса, обобщенный класс также использует тот же type parameter (пример ниже)

interface Accountable<T> {
    T getId();

    int getSum();

    void setSum(int sum);
}
public class Account<T> implements Accountable<T> {
    private final T id;
    private int sum;

    public Account(T id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public T getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Program {
    public static void main(String[] args) {
        Account<String> acc1 = new Account<String>("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000);
        Account<Integer> acc2 = new Account<Integer>(1984, 4300);
        System.out.println(acc1.getId());
        System.out.println(acc2.getId());
    }
}
Output
11079c0a-1395-4074-9d4b-41c2733e85d2
1984

2.4. Generic methods

Кроме Generic types можно также создавать generic methods, которые точно также будут использовать type parameters. Например:

class Printer {
    public <T> void print(T[] items) {
        for (T item : items) {
            System.out.println(item);
        }
    }
}
public class Program {
    public static void main(String[] args) {
        Printer printer = new Printer();
        String[] people = {"Tom", "Alice", "Sam", "Kate", "Bob", "Helen"};
        Integer[] numbers = {23, 4, 5, 2, 13, 456, 4};
        printer.<String>print(people);
        printer.<Integer>print(numbers);
    }
}
Output
Tom
Alice
Sam
Kate
Bob
Helen
23
4
5
2
13
456
4

Особенностью generic methods является использование type parameters в объявлении метода после всех модификаторов и перед типом возвращаемого значения.

public class Printer {
    public <T> void print(T[] items) {
    }
}

Затем внутри метода все значения типа T будут представлять данный type parameter.

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

public class Program {
    public static void main(String[] args) {
        Printer printer = new Printer();
        printer.<String>print(people);
        printer.<Integer>print(numbers);
    }
}

2.5. Generic constructor

Конструкторы, как и методы, также могут быть обобщенными. В этом случае перед конструктором также указываются в угловых скобках type parameters:

public class Account {
    private final String id;
    private int sum;

    public <T> Account(T id, int sum) {
        this.id = id.toString();
        this.sum = sum;
    }

    public String getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Program {
    public static void main(String[] args) {
        Account acc1 = new Account("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000);
        Account acc2 = new Account(1984, 4000);
        System.out.println(acc1.getId());
        System.out.println(acc2.getId());
    }
}
Output
11079c0a-1395-4074-9d4b-41c2733e85d2
1984

В данном случае конструктор принимает параметр id, который представляет тип T. В конструкторе его значение превращается в строку и сохраняется в локальную переменную.

2.6. Использование нескольких type parameters

Можно также задать сразу несколько type parameters (параметров типа):

public class Account<T, S> {
    private final T id;
    private S sum;

    public Account(T id, S sum) {
        this.id = id;
        this.sum = sum;
    }

    public T getId() {
        return id;
    }

    public S getSum() {
        return sum;
    }

    public void setSum(S sum) {
        this.sum = sum;
    }
}
public class Program {
    public static void main(String[] args) {
        Account<String, Double> acc1 = new Account<String, Double>("11079c0a-1395-4074-9d4b-41c2733e85d2", 5000.87);
        String id = acc1.getId();
        Double sum = acc1.getSum();
        System.out.printf("Id: %s.  Sum: %f \n", id, sum);
    }
}
Output
Id: 11079c0a-1395-4074-9d4b-41c2733e85d2. Sum: 5000.87

В данном случае тип String будет передаваться на место type parameter T, а тип Double - на место type parameter S.

2.7. Raw types

Raw type (сырой тип) - это использование generic types (т.е. generic классов или интерфейсов) без каких-либо type arguments. Например, возьмем generic class Box:

public class Box<T> {
    public void set(T t) {
        // ...
    }

    // ...
}

Чтобы создать parameterized type Box <T>, подставляется конкретный type argument для параметра типа T:

public class Program {
    public static void main(String[] args) {
        Box<Integer> intBox = new Box<>();
    }
}

Если конкретный type argument опущен, создается raw type Box <T>:

public class Program {
    public static void main(String[] args) {
        Box rawBox = new Box();
    }
}

Следовательно, Box является raw type для parameterized type Box <T>. Однако не generic class или generic interface не является raw type.

Raw types встречаются в legacy code (устаревшем коде), потому что большинство API классов, такие как классы Java Collection Framework, не использовали generics до JDK 5.0. При использовании raw types, по сути, получается поведение, которое было распространено до generics - Box выдает Object. Для обратной совместимости позволяется raw types присваивать parameterized types:

public class Program {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        Box rawBox = stringBox;
    }
}

Но если parameterized type присвоить raw type то компилятор выдаст предупреждение:

public class Program {
    public static void main(String[] args) {
        Box rawBox = new Box();
        Box<Integer> intBox = rawBox;
    }
}
Output
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Также компилятор выдаст предупреждение если использовать raw type для вызова parameterized method определенного в соответствующем parameterized class:

public class Program {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        Box rawBox = stringBox;
        rawBox.set(8); // warning: unchecked invocation to set(T)
    }
}
Output
uses unchecked or unsafe operations.
Recompile with -Xlint:unchecked for details.

Предупреждение показывает, что raw type обходят проверки parameterized type, откладывая обнаружение небезопасного кода до runtime. Следовательно, следует избегать использования raw type.

3. Generics и наследование

Generics classes могут участвовать в иерархии наследования: могут наследоваться от других, либо выполнять роль базовых классов. Рассмотрим различные ситуации.

3.1. Базовый generic class

При наследовании от generic class класс-наследник должен передавать данные о типе в конструкции базового класса:

public class Account<T> {
    private final T id;

    public T getId() {
        return this.id;
    }

    public Account(T id) {
        this.id = id;
    }
}
public class DepositAccount<T> extends Account<T> {
    public DepositAccount(T id) {
        super(id);
    }
}

В конструкторе DepositAccount() идет обращение к конструктору базового класса, в который передаются данные о типе.

Варианты использования классов:

public class Program {
    public static void main(String[] args) {
        DepositAccount dAccount1 = new DepositAccount(1984);
        System.out.println(dAccount1.getId());

        DepositAccount dAccount2 = new DepositAccount("11079c0a-1395-4074-9d4b-41c2733e85d2");
        System.out.println(dAccount2.getId());
    }
}
Output
1984
11079c0a-1395-4074-9d4b-41c2733e85d2

При этом класс-наследник может добавлять и использовать какие-то свои параметры типов:

public class Account<T> {
    private final T id;

    public T getId() {
        return this.id;
    }

    public Account(T id) {
        this.id = id;
    }
}
public class DepositAccount<T, S> extends Account<T> {
    private final S name;

    public S getName() {
        return this.name;
    }

    public DepositAccount(T id, S name) {
        super(id);
        this.name = name;
    }
}

Варианты использования:

public class Program {
    public static void main(String[] args) {
        DepositAccount<Integer, String> dAccount1 = new DepositAccount(1984, "Tom");
        System.out.println(dAccount1.getId() + " : " + dAccount1.getName());

        DepositAccount<String, Integer> dAccount2 = new DepositAccount("11079c0a-1395-4074-9d4b-41c2733e85d2", 23456);
        System.out.println(dAccount2.getId() + " : " + dAccount2.getName());
    }
}
Output
1984 : Tom
11079c0a-1395-4074-9d4b-41c2733e85d2 : 23456

И еще одна ситуация - класс-наследник вообще может не быть обобщенным:

public class Account<T> {
    private final T id;

    public T getId() {
        return this.id;
    }

    public Account(T id) {
        this.id = id;
    }
}
public class DepositAccount extends Account<Integer> {
    public DepositAccount(Integer id) {
        super(id);
    }
}

Здесь при наследовании явным образом указывается тип, который будет использоваться конструкциями базового класса, то есть тип Integer. Затем в конструктор базового класса передается значение именно этого типа - в данном случае это id переданный в конструктор класса-наследника.

Вариант использования:

public class Program {
    public static void main(String[] args) {
        DepositAccount dAccount1 = new DepositAccount(1984);
        System.out.println(dAccount1.getId());
    }
}
Output
1984

3.2. Generic класс-наследник

Также может быть ситуация, когда базовый класс является обычным необобщенным классом. Например:

public class Account {
    private final String name;

    public String getName() {
        return this.name;
    }

    public Account(String name) {
        this.name = name;
    }
}
public class DepositAccount<T> extends Account {
    private final T id;

    public T getId() {
        return this.id;
    }

    public DepositAccount(String name, T id) {
        super(name);
        this.id = id;
    }
}

В этом случае использование конструкций базового класса в наследнике происходит как обычно.

3.3. Преобразование обобщенных типов

Объект одного generic types можно привести к другому типу, если они используют один и тот же type parameter. Рассмотрим преобразование типов на примере следующих двух обобщенных классов:

public class Account<T> {
    private final T id;

    public Account(T id) {
        this.id = id;
    }

    public T getId() {
        return this.id;
    }
}
public class DepositAccount<T> extends Account<T> {
    public DepositAccount(T id) {
        super(id);
    }
}

Можно привести объект DepositAccount<Integer> к Account<Integer> или DepositAccount<String> к Account<String>:

public class Program {
    public static void main(String[] args) {
        DepositAccount<Integer> depAccount = new DepositAccount(1984);
        Account<Integer> account = (Account<Integer>) depAccount;
        System.out.println(account.getId());
    }
}
Output
1984

Но сделать то же самое с разнотипными объектами нельзя. Например, следующий код не будет работать:

public class Program {
    public static void main(String[] args) {
        DepositAccount<Integer> depAccount = new DepositAccount(1984);
        Account<String> account = (Account<String>) depAccount; // compile error
    }
}
Output
error: incompatible types: DepositAccount<Integer> cannot be converted to Account<String>

4. Bounded Type Parameters

Когда указывается type parameter у generics, то по умолчанию он может представлять любой тип. Однако иногда необходимо, чтобы он соответствовал только некоторому ограниченному набору типов. В этом случае применяются ограничения. Они позволяют указать базовый класс, которому должен соответствовать type parameter.

Для установки ограничения после type parameter ставится слово extends, после которого указывается базовый класс ограничения:

public class Account {
}
public class Transaction<T extends Account> {
}

К примеру, в данном случае для параметра T в Transaction ограничением является класс Account. То есть на место параметра T мы можем передать либо класс Account, либо один из его классов-наследников.

Например, рассмотрим следующий код:

public class Account {
    private final String id;
    private int sum;

    public Account(String id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public String getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Transaction<T extends Account> {
    private T from; // с какого счета перевод
    private T to; // на какой счет перевод
    private int sum; // сумма перевода

    public Transaction(T from, T to, int sum) {
        this.from = from;
        this.to = to;
        this.sum = sum;
    }

    public void execute() {
        System.out.printf("Before transaction.\nAccount %s: %d \nAccount %s: %d \n\n",
                from.getId(), from.getSum(), to.getId(), to.getSum());
        if (from.getSum() > sum) {
            from.setSum(from.getSum() - sum);
            to.setSum(to.getSum() + sum);
            System.out.printf("Execute transaction: from %s to %s sum: %d \n\n",
                    from.getId(), to.getId(), sum);
        } else {
            System.out.println("Operation is invalid\n");
        }
        System.out.printf("After transaction.\nAccount %s: %d \nAccount %s: %d \n\n",
                from.getId(), from.getSum(), to.getId(), to.getSum());
    }
}
public class Program {
    public static void main(String[] args) {
        Account acc1 = new Account("9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4", 4500);
        Account acc2 = new Account("6077f64b-ab62-4584-8c34-fba3926a50ee", 1500);

        Transaction<Account> tran1 = new Transaction<Account>(acc1, acc2, 4000);
        tran1.execute();
        tran1 = new Transaction<Account>(acc1, acc2, 4000);
        tran1.execute();
    }
}
Output
Before transaction.
Account 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4: 4500
Account 6077f64b-ab62-4584-8c34-fba3926a50ee: 1500

Execute transaction: from 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4 to 6077f64b-ab62-4584-8c34-fba3926a50ee sum: 4000

After transaction.
Account 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4: 500
Account 6077f64b-ab62-4584-8c34-fba3926a50ee: 5500

Before transaction.
Account 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4: 500
Account 6077f64b-ab62-4584-8c34-fba3926a50ee: 5500

Operation is invalid

After transaction.
Account 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4: 500
Account 6077f64b-ab62-4584-8c34-fba3926a50ee: 5500

В данном случае класс Transaction, который представляет операцию перевода средств между двумя счетами, типизирован параметром T. У T в качестве ограничения установлен класс Account. При создании объекта Transaction в его конструктор передаются два объекта Account (т.е. два счёта), между которыми надо осуществить перевод, и сумма перевода.

При этом важно понимать, что поскольку установленно подобное ограничение, то компилятор будет распознавать объекты типа T как объекты типа Account. И в этом случае можно вызывать у объектов типа T методы класса Account. Это было бы невозможно, если бы не было задано подобное ограничение:

private class Transaction<T> {
    // остальное содержимое
}

В этом случае была бы ошибка.

4.1. Generic types в качестве ограничений

В качестве ограничений могут выступать и другие generic types, которые сами могут иметь ограничения.

public class Account<T> {
    private final T id;
    private int sum;

    public Account(T id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public T getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Transaction<T extends Account> {
    private T from; // с какого счета перевод
    private T to; // на какой счет перевод
    private int sum; // сумма перевода

    public Transaction(T from, T to, int sum) {
        this.from = from;
        this.to = to;
        this.sum = sum;
    }

    public void execute() {
        System.out.printf("Before transaction.\nAccount %s: %d \nAccount %s: %d \n\n",
                from.getId(), from.getSum(), to.getId(), to.getSum());
        if (from.getSum() > sum) {
            from.setSum(from.getSum() - sum);
            to.setSum(to.getSum() + sum);
            System.out.printf("Execute transaction: from %s to %s sum: %d \n\n",
                    from.getId(), to.getId(), sum);
        } else {
            System.out.println("Operation is invalid\n");
        }
        System.out.printf("After transaction.\nAccount %s: %d \nAccount %s: %d \n\n",
                from.getId(), from.getSum(), to.getId(), to.getSum());
    }
}
public class Program {
    public static void main(String[] args) {
        Account<Integer> acc1 = new Account<Integer>(1984, 4500);
        Account<Integer> acc2 = new Account<Integer>(2020, 1500);

        Transaction<Account<Integer>> tran1 = new Transaction<Account<Integer>>(acc1, acc2, 4000);
        tran1.execute();
        tran1 = new Transaction<Account<Integer>>(acc1, acc2, 4000);
        tran1.execute();
    }
}
Output
Before transaction.
Account 1984: 4500
Account 2020: 1500

Execute transaction: from 1984 to 2020 sum: 4000

After transaction.
Account 1984: 500
Account 2020: 5500

Before transaction.
Account 1984: 500
Account 2020: 5500

Operation is invalid

After transaction.
Account 1984: 500
Account 2020: 5500

В данном случае ограничением для Transaction является тип Account, который типизирован типом Integer.

4.1.1. Interfaces в качестве ограничений

В качестве ограничений могут выступать также интерфейсы. В этом случае передаваемый на место type parameter тип должен реализовать данный интерфейс:

interface Accountable {
    String getId();

    int getSum();

    void setSum(int sum);
}
public class Account implements Accountable {
    private final String id;
    private int sum;

    public Account(String id, int sum) {
        this.id = id;
        this.sum = sum;
    }

    public String getId() {
        return id;
    }

    public int getSum() {
        return sum;
    }

    public void setSum(int sum) {
        this.sum = sum;
    }
}
public class Transaction<T extends Accountable> {
    private final T from; // с какого счета перевод
    private final T to; // на какой счет перевод
    private final int sum; // сумма перевода

    public Transaction(T from, T to, int sum) {
        this.from = from;
        this.to = to;
        this.sum = sum;
    }

    public void execute() {
        System.out.printf("Before transaction.\nAccount %s: %d \nAccount %s: %d \n\n",
                from.getId(), from.getSum(), to.getId(), to.getSum());
        if (from.getSum() > sum) {
            from.setSum(from.getSum() - sum);
            to.setSum(to.getSum() + sum);
            System.out.printf("Execute transaction: from %s to %s sum: %d \n\n",
                    from.getId(), to.getId(), sum);
        } else {
            System.out.println("Operation is invalid\n");
        }
        System.out.printf("After transaction.\nAccount %s: %d \nAccount %s: %d \n\n",
                from.getId(), from.getSum(), to.getId(), to.getSum());
    }
}
public class Program {
    public static void main(String[] args) {
        Account acc1 = new Account("9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4", 4500);
        Account acc2 = new Account("6077f64b-ab62-4584-8c34-fba3926a50ee", 1500);
        Transaction<Account> tran1 = new Transaction<Account>(acc1, acc2, 4000);
        tran1.execute();
    }
}
Output
Before transaction.
Account 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4: 4500
Account 6077f64b-ab62-4584-8c34-fba3926a50ee: 1500

Execute transaction: from 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4 to 6077f64b-ab62-4584-8c34-fba3926a50ee sum: 4000

After transaction.
Account 9f5bd57d-e0d6-4e4c-8673-c79b5b7e03e4: 500
Account 6077f64b-ab62-4584-8c34-fba3926a50ee: 5500

4.2. Generic methods с ограничением

Generic methods с ограничением являются ключом к реализации универсальных алгоритмов. Рассмотрим следующий метод, который подсчитывает количество элементов в массиве T[] больше указанного элемента elem.

public class ArrayUtil {
    public static <T> int countGreaterThan(T[] anArray, T elem) {
        int count = 0;
        for (T e : anArray) {
            if (e > elem) {
                count++;
            }
        }
        return count;
    }
}
Output
error: bad operand types for binary operator '>'
            if (e > elem) {
                  ^
  first type:  T
  second type: T
  where T is a type-variable:
    T extends Object declared in method <T>countGreaterThan(T[],T)

Реализация метода проста, но он не компилируется, потому что оператор больше > применяется только к примитивным типам, таким как short, int, double, long, float, byte и char. Нельзя использовать оператор > для сравнения объектов. Чтобы устранить проблему, можно использовать type parameter, ограниченный интерфейсом Comparable<T> :

public interface Comparable<T> {
    int compareTo(T o);
}

В результате получится такой код:

public class ArrayUtil {
    public static <T extends Comparable<T>>int countGreaterThan(T[] anArray, T elem) {
        int count = 0;
        for (T e : anArray) {
            if (e.compareTo(elem) > 0) {
                count++;
            }
        }
        return count;
    }
}

4.3. Множественные ограничения

Также можно установить сразу несколько ограничений. Например, пусть класс Transaction может работать только с объектами, одновременно реализуют интерфейс Accountable и являются наследниками класса Person:

class Person {
}
interface Accountable {
}
class Transaction<T extends Person & Accountable> {
}

Type parameter с несколькими границами является подтипом всех типов, перечисленных в привязке. Если одна из границ является классом, она должна быть указана в первую очередь. Например:

class A {
}
interface B {
}
interface C {
}
class D <T extends A & B & C> {
}

Если тип A не будет указан первым, то возникнет compile-time error:

class D <T extends B & A & C> {
}
Output
error: interface expected here
public class D <T extends B & A & C> {
                              ^

5. Generics, Inheritance, and Subtypes

Возможно присвоить объект одного типа объекту другого типа при условии, что типы совместимы. Например, можно типу Object присвоить Integer, поскольку Object является одним из суперклассов Integer:

public class Program {
    public static void main(String[] args) {
        Object someObject = new Object();
        Integer someInteger = new Integer(10);
        someObject = someInteger; // OK
    }
}

В объектно-ориентированной терминологии это называется отношением is. Поскольку Integer является разновидностью Object, присвоение разрешено. Но Integer также является разновидностью Number, поэтому следующий код также валиден:

public class Program {
    public static void someMethod(Number n) {
        /* ... */
    }

    public static void main(String[] args) {
        someMethod(new Integer(10)); // OK
        someMethod(new Double(10.1)); // OK
    }
}

То же самое и с generics. Вы можете выполнить вызов обобщенного типа, передав Number в качестве аргумента типа, и любой последующий вызов add() будет разрешен, если аргумент совместим с Number:

public class Program {
    public static void main(String[] args) {
        Box<Number> box = new Box<Number>();
        box.add(new Integer(10));   // OK
        box.add(new Double(10.1));  // OK
    }
}

Теперь рассмотрим следующий метод:

public class Program {
    public void boxTest(Box<Number> n) {
        /* ... */
    }
}

Какие аргументы он принимает? Посмотрев на его, видно, что он принимает единственный аргумент, тип которого - Box <Number>. Но что это значит? Можно ли передать в качестве аргумента Box <Integer> или Box <Double>, как и следовало ожидать? Ответ - «нет», потому что Box <Integer> и Box <Double> не являются подтипами Box <Number>.

связь обобщенных подтипов

Это распространенное заблуждение, когда дело доходит до программирования с использованием generics, но это важная концепция, которую нужно изучить.

5.1. Обобщенные классы и подтипы

Можно создать подтипы общего класса или интерфейса, расширив или реализуя его. Отношения между параметрами типа одного класса или интерфейса и параметрами типа другого определяются ключевыми словами extends и implements.

Используя классы Collections в качестве примера, ArrayList <E> реализует List <E>, а List <E> расширяет Collection <E>. Итак, ArrayList <String> является подтипом List <String>, который является подтипом Collection <String>. Пока вы не изменяете аргумент типа, отношения подтипов между типами сохраняются.

пример иерархии обобщенных подтипов

Теперь представьте, что мы хотим определить наш собственный интерфейс списка, PayloadList, который связывает необязательное значение универсального типа P с каждым элементом. Его объявление может выглядеть так:

interface PayloadList<E,P> extends List<E> {
    void setPayload(int index, P val);
}

Следующие параметризации PayloadList являются подтипами List <String>:

  • PayloadList<String,String>

  • PayloadList<String,Integer>

  • PayloadList<String,Exception>

иерархия playLoadList

6. Type Inference

Type Inference - это способность компилятора Java просматривать каждый вызов метода и соответствующее объявление, чтобы определить тип аргумента (или аргументов), которые делают вызов возможным. Inference алгоритм определяет типы аргументов, если они доступны, и тип которому результат присваивается или возвращается. Inference алгоритм пытается найти наиболее точный тип, который применим со всеми аргументами.

В следующем примере, Inference алгоритм определяет, что второй аргумент, передаваемый методу pick(), имеет тип Serializable:

public class Program {
    static <T> T pick(T a1, T a2) {
        return a2;
    }

    public static void main(String[] args) {
        Serializable s = pick("d", new ArrayList<String>());
    }
}

6.1. Type Inference and Generic Methods

Type Inference, позволяет вызывать Generic Methods, как обычный метод, без указания типа в угловых скобках.

Полный синтаксис для вызова этого метода:

public class Program {
    public static void main(String[] args) {
        Pair<Integer, String> p1 = new Pair<>(1, "apple");
        Pair<Integer, String> p2 = new Pair<>(2, "pear");
        boolean same = Util.<Integer, String>compare(p1, p2);
    }
}

Компилятор может определить тип ориентируясь на аргументы переданные в метод:

public class Program {
    public static void main(String[] args) {
        Pair<Integer, String> p1 = new Pair<>(1, "apple");
        Pair<Integer, String> p2 = new Pair<>(2, "pear");
        boolean same = Util.compare(p1, p2);
    }
}

Эта функция называется Type Inference, она позволяет вызывать Generic Method как обычный метод без указания типа в угловых скобках.

6.2. Type Inference and Instantiation of Generic Classes

Можно заменить аргументы типа, необходимые для вызова конструктора обобщенного класса, пустым набором параметров типа <>, если компилятор может вывести аргументы типа из контекста. Эта пара угловых скобок неофициально называется diamond.

Например, рассмотрим следующее объявление переменной:

public class Program {
    public static void main(String[] args) {
        Map<String, List<String>> myMap = new HashMap<String, List<String>>();
    }
}

Можно заменить параметризованный тип конструктора на пустой набор параметров типа <>:

public class Program {
    public static void main(String[] args) {
        Map<String, List<String>> myMap = new HashMap<>();
    }
}

Обратите внимание, что для использования Type Inference во время создания экземпляра обобщенного класса необходимо использовать diamond. В следующем примере компилятор генерирует предупреждение о непроверенном преобразовании, поскольку конструктор HashMap() относится к Raw type HashMap, а не к типу Map <String, List <String>>:

public class Program {
    public static void main(String[] args) {
        Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
    }
}

6.2.1. Type Inference and Generic Constructors of Generic and Non-Generic Classes

Конструкторы могут быть обобщенными (другими словами, объявлять свои собственные параметры обобщенного типа) как в обобщенных, так и в необобщенных классах. Рассмотрим следующий пример:

class MyClass<X> {
    <T> MyClass(T t) {
        // ...
    }
}

Рассмотрим следующий экземпляр класса MyClass:

public class Program {
    public static void main(String[] args) {
        new MyClass<Integer>("");
    }
}

Этот оператор создает экземпляр параметризованного типа MyClass <Integer>; оператор явно указывает тип Integer для параметра обобщенного типа X обобщенного класса MyClass <X>. Конструктор для этого обобщенного класса содержит параметр обобщенного типа T. Компилятор определяет тип String для параметра обобщенного типа T конструктора этого обобщенного класса (поскольку фактическим параметром этого конструктора является объект String).

Компиляторы из версий предшествующих Java SE 7, могут определять фактические параметры типов обобщенных конструкторов, аналогично обобщенным методам. Однако компиляторы в Java SE 7 и более поздних версиях могут вывести фактические параметры типа создаваемого экземпляра универсального класса, если вы используете <>:

public class Program {
    public static void main(String[] args) {
        MyClass<Integer> myObject = new MyClass<>("");
    }
}

В этом примере компилятор определяет тип Integer для параметра обобщенного типа X обобщенного класса MyClass <X>. Он определяет тип String для параметра формального типа T конструктора этого универсального класса.

6.2.2. Target Types

Компилятор Java использует target typing для параметров типа вызова обобщенного метода. Target type выражения - это тип данных, ожидаемый компилятором Java в зависимости от того, где находится выражение. Рассмотрим метод Collections.emptyList(), который объявлен следующим образом:

public class Program {
    static <T> List<T> emptyList();
}

Рассмотрим следующий оператор присваивания:

public class Program {
    public static void main(String[] args) {
        List<String> listOne = Collections.emptyList();
    }
}

Этот оператор ожидает экземпляр List <String>; этот тип данных является Target Types. Поскольку метод emptyList() возвращает значение типа List <T>, компилятор делает вывод, что аргумент типа T должен быть значением String. Это работает как в Java SE 7, так и выше. В качестве альтернативы можно использовать подтверждающий тип и указать значение T следующим образом:

public class Program {
    public static void main(String[] args) {
        List<String> listOne = Collections.<String>emptyList();
    }
}

Однако в данном контексте это необязательно. Однако это было необходимо в других контекстах. Рассмотрим следующий метод:

public class Program {
    void processStringList(List<String> stringList) {
        // process stringList
    }
}

Если есть необходимость вызвать метод processStringList() с пустым списком. В Java SE 7 следующий код не компилируется:

public class Program {
    void processStringList(List<String> stringList) {
        // process stringList
    }

    public static void main(String[] args) {
        processStringList(Collections.emptyList());
    }
}

Компилятор Java SE 7 выдает сообщение об ошибке, подобное следующему:

Output
List<Object> cannot be converted to List<String>

Компилятору требуется значение для аргумента типа T, поэтому он начинается со значением Object. Следовательно, вызов Collections.emptyList() возвращает значение типа List <Object>, которое несовместимо с методом processStringList(). В Java SE 7 необходимо указать значение значения аргумента типа следующим образом:

public class Program {
    void processStringList(List<String> stringList) {
        // process stringList
    }

    public static void main(String[] args) {
        processStringList(Collections.<String>emptyList());
    }
}

В Java SE 8 в этом больше нет необходимости. Понятие target type было расширено за счет включения аргументов метода, таких как аргумент метода processStringList(). В этом случае для processStringList() требуется аргумент типа List <String>. Метод Collections.emptyList() возвращает значение List <T>, поэтому, используя целевой тип List <String>, компилятор делает вывод, что аргумент типа T имеет значение String. Таким образом, в Java SE 8 следующий код компилируется:

public class Program {
    void processStringList(List<String> stringList) {
        // process stringList
    }

    public static void main(String[] args) {
        processStringList(Collections.emptyList());
    }
}

7. Wildcards

В Generics мета-символ вопросительный знак (?), называемый wildcard и фактически представляющий неизвестный тип. Wildcard можно использовать в различных ситуациях:

  • как тип параметра

  • как тип поля

  • как тип локальной переменной

  • иногда как возвращаемый тип

Несмотря на это, лучшей практикой программирования является использование конкретных типов. Wildcards никогда не используется в качестве type argument (аргумента типа) для вызова generic method, создания экземпляра generic class или супертипа.

Wildcards делятся на три типа:

  • Upper Bounded Wildcards - <? extends Number>

  • Unbounded Wildcards - <?>

  • Lower Bounded Wildcards - <? super Integer>

7.1. Upper bounded wildcard (Подстановочные знаки с ограничением сверху)

Можно использовать upper bounded wildcard, чтобы ослабить ограничения на переменную. Например, необходимо написать метод, который работает с List <Integer>, List <Double> и List <Number>; этого можно добиться, используя upper bounded wildcard.

Чтобы объявить upper bounded wildcard, используется wildcard мета-символ ?. За ним следует ключевое слово extends, за которым следует его верхняя граница. Обратите внимание, что в этом контексте extends используется в общем смысле и обозначает либо extend (как в классах), либо implements (как в интерфейсах).

Чтобы написать метод, который работает со списками Number и подтипами Number, такими как Integer, Double и Float, необходимо указать List <? extends Number>. List <Number> является более строгим, чем List <? extends Number>, потому что первый соответствует только списку типа Number, тогда как последний соответствует списку типа Number или любому из его подклассов.

Рассмотрим следующий метод process():

public class Program {
    public static void process(List<? extends Foo> list) {
        // ...
    }
}

Верхний ограниченный подстановочный знак, <? extends Foo>, где Foo - это любой тип, соответствует Foo и любому подтипу Foo. Метод process() может обращаться к элементам списка как к типу Foo:

public class Program {
    public static void process(List<? extends Foo> list) {
        for (Foo elem : list) {
            // ...
        }
    }
}

В цикле foreach переменная elem выполняет итерацию по каждому элементу в списке. Любой метод, определенный в классе Foo, теперь можно использовать через elem.

Метод sumOfList() возвращает сумму чисел в списке:

public class Program {
    public static double sumOfList(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list) {
            s += n.doubleValue();
        }
        return s;
    }
}

Следующий код, используя список объектов Integer:

public class Program {
    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        System.out.println("sum = " + sumOfList(li));
    }
}
Output
sum = 6.0

Список значений Double может использовать тот же метод sumOfList():

public class Program {
    public static void main(String[] args) {
        List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
        System.out.println("sum = " + sumOfList(ld));
    }
}
Output
sum = 7.0

7.2. Unbounded wildcard (Неограниченные неопределенные типы)

Тип unbounded wildcard указывается с помощью wildcard мета-символа ?, например List <?> - список неизвестного типа. Есть два сценария, в которых unbounded wildcard является полезным подходом:

  • Если разрабатываемый метод можно реализовать с использованием методов, предоставляемых в классе Object.

  • Когда код использует методы в обобщенном классе, которые не зависят от параметра типа. Например, List.size() или List.clear(). Фактически, Class <?> так часто используется, потому что большинство методов в Class <T> не зависят от T.

Рассмотрим следующий метод printList():

public class Program {
    public static void printList(List<Object> list) {
        for (Object elem : list) {
            System.out.println(elem + " ");
        }
        System.out.println();
    }
}

Цель метода printList() - распечатать список любого типа, но он не может этого сделать, так как он печатает только список экземпляров Object; он не может печатать List <Integer>, List <String>, List <Double> и так далее, потому что они не являются подтипами List <Object>. Чтобы написать общий метод printList(), используйте List <?>:

public class Program {
    public static void printList(List<?> list) {
        for (Object elem: list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }
}

Поскольку для любого конкретного типа A List <A> является подтипом List <?>, можно использовать printList() для печати списка любого типа:

public class Program {
    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        List<String> ls = Arrays.asList("one", "two", "three");
        printList(li);
        printList(ls);
    }
}

7.3. Lower bounded wildcard (неопределенные типы с ограничением снизу)

Аналогично upper bounded wildcard, lower bounded wildcard ограничивает неизвестный тип определенным типом или супертипом этого типа.

Lower bounded wildcard выражается с помощью wildcard мета-символа ?, за которым следует ключевое слово super и затем следует его нижняя граница: <? super А>.

Допустим, необходимо написать метод, который добавляет объекты класса Integer в список. Для максимальной гибкости необходимо, чтобы метод работал с List <Integer>, List <Number> и List <Object>, т.е. со всеми списками, которые может содержать целочисленные значения.

Чтобы написать метод, который работает со списками Integer и супертипами Integer, такими как Integer, Number и Object, необходимо указать List <? super Integer>. List <Integer> является более строгим, чем List <? super Integer>, потому что первый соответствует списку только типа Integer, тогда как последний соответствует списку любого типа, который является супертипом Integer.

Следующий код добавляет числа от 1 до 10 в конец списка:

public class Program {
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 10; i++) {
            list.add(i);
        }
    }
}

7.4. Принцип PECS

С Wildcard действует так называемый Get Put principle:

  • Используйте extends wildcard когда необходимо ТОЛЬКО получать значения из структуры (объекта).

  • Используйте super wildcard когда необходимо ТОЛЬКО добавлять значения в структуру (объект).

  • Не используйте wildcard когда необходимо И получать, И добавлять значения в структуру (объект).

Данный принцип ещё называют PECS (Producer Extends Consumer Super) принципом.

Ограничения Wildcards

Предположим, есть метод, который принимает в качестве параметра Collection объектов Thing, но необходимо сделать его более гибким:

Случай 1: Необходимо просмотреть коллекцию и произвести какие-то действия с каждым компонентом как с объектом Thing.

Тогда список является Producer, поэтому необходимо использовать Collection<? extends Thing>.

Основная идея в том, что Collection<? extends Thing> может содержать любой подтип Thing, и, таким образом, каждый элемент будет вести себя как Thing при выполнении необходимой операции. На самом деле невозможно ничего добавить в Collection<? extends Thing>, потому что не возможно знать во время выполнения, какой определенный подтип Thing содержится в коллекции.

Случай 2: Необходимо добавлять объекты в коллекцию.

Тогда список является Consumer, поэтому необходимо использовать Collection<? super Thing>.

Причиной, почему так необходимо делать, является то, что в отличие от Collection<? extends Thing>, Collection<? super Thing> всегда может содержать Thing независимо от того, что является фактическим параметризованным типом.

7.5. Подстановочные знаки и подтипы

Подстановочные знаки можно использовать для создания связи между универсальными классами или интерфейсами.

Имея два обычных (не обобщенных) класса:

class A {
}
class B extends A {
}

Можно написать такой код:

public class Program {
    public static void main(String[] args) {
        B b = new B();
        A a = b;
    }
}

Этот пример показывает, что наследование обычных классов следует правилу создания подтипов: класс B является подтипом класса A, если B расширяет A. Это не применяется к универсальным типам:

public class Program {
    public static void main(String[] args) {
        List<B> lb = new ArrayList<>();
        List<A> la = lb; // compile-time error
    }
}

Учитывая, что Integer является подтипом Number, какова связь между List <Integer> и List <Number>?

связь list из примера

Хотя Integer является подтипом Number, List <Integer> не является подтипом List <Number> и, по сути, эти два типа не связаны. Общим родителем List <Number> и List <Integer> является List <?>.

Чтобы создать связь между этими классами, чтобы код мог обращаться к методам Number через элементы List <Integer>, используйте подстановочный знак с ограничением сверху:

public class Program {
    public static void main(String[] args) {
        List<? extends Integer> intList = new ArrayList<>();
        List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>
    }
}

Поскольку Integer является подтипом Number, а numList - списком объектов Number, теперь существует связь между intList (списком объектов Integer) и numList. На следующей диаграмме показаны отношения между несколькими классами List, объявленными с ограниченными сверху и снизу подстановочными знаками.

подтипы с ограничением сверху и снизу

7.6. Захват подстановочных знаков и вспомогательные методы.

В некоторых случаях компилятор определяет тип подстановочного знака. Например, список может быть определен как List <?>, но при оценке выражения компилятор выводит конкретный тип из кода. Этот сценарий известен как захват подстановочного знака.

По большей части не стоит беспокоиться о захвате подстановочных знаков, за исключением случаев, когда генерируется сообщение об ошибке, содержащее фразу «capture of».

Пример WildcardError при компиляции вызывает ошибку захвата:

public class WildcardError {
    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

В этом примере компилятор обрабатывает входной параметр i как имеющий тип Object. Когда метод foo() вызывает List.set (int, E), компилятор не может подтвердить тип объекта, который вставляется в список, и возникает ошибка. Когда возникает этот тип ошибки, это обычно означает, что компилятор считает, что идет присваивание неправильного типа переменной. По этой причине в язык Java были добавлены generics - для обеспечения безопасности типов во время компиляции.

Пример WildcardError генерирует следующую ошибку при компиляции javac-реализацией Oracle JDK 7:

WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error

Обойти ошибку компилятора можно написав частный вспомогательный метод, который захватывает подстановочный знак. Пример, создадим вспомогательный метод fooHelper(), в WildcardFixed:

public class WildcardFixed {
    void foo(List<?> i) {
        fooHelper(i);
    }

    // Helper method created so that the wildcard can be captured
    // through type inference.
    private <T> void fooHelper(List<T> l) {
        l.set(0, l.get(0));
    }
}

Благодаря вспомогательному методу компилятор использует выведение типов, чтобы определить, что T является CAP1, переменной захвата, в вызове. Теперь пример успешно компилируется.

По соглашению вспомогательные методы обычно называются originalMethodNameHelper().