07/03/2024

Глава 15. Обобщенные типы

Глава 15. Обобщенные типы

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

Этот принцип действует и для классов. Ограничение можно выставить по типу базового класса, интерфейса или указать некий "условный тип".

Обобщения(generics) реализуют концепцию параметризованных типов, позволяющих создавать компоненты(контейнеры), которые удобно использовать с разными типами.

Под темином "обобщение" следует понимать "применимость к большой группе классов".

Простые обобщения

Одна из причин использования обобщений - это классы контейнеры. Контейнер - это хранилище для объектов.

Пример класса-контейнера с использованием "условного" типа для хранения. На это указывает параметр-тип в угловых скобках после имени класса.

public class Holder3<T> {
    private T a;

    public Holder3(T a) {
        this.a = a;
    }

    public void set(T a) {
        this.a = a;
    }

    public T get() {
        return a;
    }

    public static void main(String[] args) {
        Holder3<Automobile> h3 =
                new Holder3<Automobile>(new Automobile());
        Automobile a = h3.get();
    }
}

class Automobile {
}

Библиотека кортежей

При вызове метода иногда возникает необходимость вернуть несколько объектов. Команда return позволяет вернуть только один объект, но задачу можно решить созданием объекта, содержащего несколько возвращаемых объектов. Можно писать специальный класс при необходимости, но обобщения позволяют решить задачу сразу на все случаи.

Группа объектов, завернутых в один объект, называется кортежем. Кортеж может иметь произвольную длину и все объекты могут относиться к разным типам. Получатель может читать элементы, но не может добавлять новые (концепция "объект передачи данных").

public class TwoTuple<A, B> {
    public final A first;
    public final B second;

    public TwoTuple(A first, B second) {
        this.first = first;
        this.second = second;
    }
}
public class ThreeTuple<A, B, C> extends TwoTuple<A, B> {

    public final C third;

    public ThreeTuple(A first, B second, C third) {
        super(first, second);
        this.third = third;
    }
}

Поля first, second, third объявляются public и final, что позволяет не добавлять get и set методы, тк получить значение можно просто обратившись к переменной, тк она public, а изменить нельзя, тк она final. Данная форма оказывается короче чем добавление геттеров и сеттеров. Для того чтобы изменить элементы кортежа лучше будет просто создать новый объект. Для оздания кортежа с большим количеством элементов, можно применить наследование.

Класс стека

При каждом вызове push() новый узел Node<T> создается и связывается с предыдущим узлом Node<T>. При вызове pop() всегда возвращается top.item, после чего текущий узел Node<T> удаляется и происходит переход к следующему узлу; но при достижении сторожа(значение null, признак пустого стека) перемещение не выполняется.

public class LinkedStack<T> {

  private static class Node<U> {
    U item;
    Node<U> next;
    Node() { item = null; next = null; }
    Node(U item, Node<U> next) {
      this.item = item;
      this.next = next;
    }
    boolean end() { return item == null && next == null; }
  }

  private Node<T> top = new Node<T>(); // End sentinel

  public void push(T item) {
    top = new Node<T>(item, top);
  }	

  public T pop() {
    T result = top.item;
    if(!top.end())
      top = top.next;
    return result;
  }

  public static void main(String[] args) {
    LinkedStack<String> lss = new LinkedStack<String>();
    for(String s : "Phasers on stun!".split(" "))
      lss.push(s);
    String s;
    while((s = lss.pop()) != null)
      System.out.println(s);
  }

}

RandomList

public class RandomList<T> {
  private ArrayList<T> storage = new ArrayList<T>();
  private Random rand = new Random(47);
  public void add(T item) { storage.add(item); }
  public T select() {
    return storage.get(rand.nextInt(storage.size()));
  }
  public static void main(String[] args) {
    RandomList<String> rs = new RandomList<String>();
    for(String s: ("The quick brown fox jumped over " +
        "the lazy brown dog").split(" "))
      rs.add(s);
    for(int i = 0; i < 11; i++)
      System.out.print(rs.select() + " ");
  }
}

Обобщенные интерфейсы

С интерфейсами обобщения работают почти также, как с классами. Рассмотрим интерфейс для создания генератора. От него требуется возвращать следущий элемент передаваемого типа. Для этого добавляется метод next().

public interface Generator<T> { T next(); }

Продемонстрируем реализацию интерфейса Generator.

public class Coffee {

    private static long counter = 0;
    private final long id = counter++;

    public String toString() {
        return this.getClass().getSimpleName() + " " + id;
    }
}

class Latte extends Coffee {
}

class Mocha extends Coffee{
}

class Americano extends Coffee {
}
public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {

    // Реализация генератора

    private Class[] types = {Americano.class, Mocha.class, Latte.class};

    private Random rand = new Random(47);

    @Override
    public Coffee next() {
        try {
            return (Coffee) types[rand.nextInt(types.length)].getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // Реализация итератора
    private int size = 0;

    public CoffeeGenerator(int size) {
        this.size = size;
    }

    public CoffeeGenerator() {
    }

    class CoffeeIterator implements Iterator<Coffee> {

        int count = size;

        @Override
        public boolean hasNext() {
            return count > 0;
        }

        @Override
        public Coffee next() {
            count--;
            return CoffeeGenerator.this.next();
        }
    }

    @Override
    public Iterator<Coffee> iterator() {
        return new CoffeeIterator();
    }


    public static void main(String[] args) {
        CoffeeGenerator generator = new CoffeeGenerator();
        for (int i = 0; i < 5; i++) {
            System.out.println(generator.next());
        }
        System.out.println();
        for (Coffee c : new CoffeeGenerator(5)) {
            System.out.println(c);
        }
    }
    
}

Пример генератора для генерации чисел Фибоначчи.

Вариант с рекурсией:

public class FibonacciGenerator implements Generator<Integer> {

    private static int counter = 0;

    @Override
    public Integer next() {
        return fib(counter++);
    }

    public Integer fib(int n) {
        if (n < 2) return 1;
        return fib(n - 2) + fib(n - 1);
    }

    public static void main(String[] args) {
        FibonacciGenerator generator = new FibonacciGenerator();
        for (int i = 0; i < 10; i++) {
           System.out.println(generator.next());
        }
    }
}

Вариант без рекурсии

public class FibonacciGenerator implements Generator<Integer> {

    private int first = 0;
    private int second = 1;

    private static int counter = 0;

    @Override
    public Integer next() {
        if (counter < 1) {
            counter++;
            return 1;
        } else {
            int result = first + second;
            first = second;
            second = result;
            counter++;
            return result;
        }
    }

    public static void main(String[] args) {
        FibonacciGenerator generator = new FibonacciGenerator();
        for (int i = 0; i < 10; i++) {
            System.out.println(generator.next());
        }
    }
}

Создание итератора для вывода числе Фибоначчи с помощью паттерна Адаптер:

public class IterableFibonacci extends FibonacciGenerator implements Iterable<Integer> {

    private int count;

    public IterableFibonacci(int count) {
        this.count = count;
    }

    class FibonacciIterator implements Iterator<Integer> {


        @Override
        public boolean hasNext() {
            return count > 0;
        }

        @Override
        public Integer next() {
            count--;
            return IterableFibonacci.this.next();
        }
    }

    @Override
    public Iterator<Integer> iterator() {
        return new FibonacciIterator();
    }

    public static void main(String[] args) {
        for (Integer n : new IterableFibonacci(5)) {
            System.out.println(n);
        }
    }
}

Обобщенные методы

Параметризация возможна не только для классов но и для методов. Как правило, применять обощенные методы следует там, где только возможно.

Чтобы определить обобщенный метод, следует поместить список параметров-типов перед возвращаемым значением:

public <T> void f(T x) {
    System.out.println(x.getClass().getName());
}

Использование автоматического определения аргументов-типов

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

public class New {
  public static <K,V> Map<K,V> map() {
    return new HashMap<K,V>();
  }
  public static <T> List<T> list() {
    return new ArrayList<T>();
  }
  public static <T> LinkedList<T> lList() {
    return new LinkedList<T>();
  }
  public static <T> Set<T> set() {
    return new HashSet<T>();
  }	
  public static <T> Queue<T> queue() {
    return new LinkedList<T>();
  }
  // Examples:
  public static void main(String[] args) {
    Map<String, List<String>> sls = New.map();
    List<String> ls = New.list();
    LinkedList<String> lls = New.lList();
    Set<String> ss = New.set();
    Queue<String> qs = New.queue();
  }
}

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

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

В Java 13 работают оба варианта.

public class LimitsOfInference {
  static void f(Map<Person, List<? extends Pet>> petPeople) {}
  public static void main(String[] args) {
    // f(New.map()); // Does not compile
    f(New.<Person, List<Pet>>map());
  }
}

Списки аргументов переменной длины и обобщенные методы

Обобщенные методы можно использовать со списками переменной длины.

public class GenericVarargs {
  public static <T> List<T> makeList(T... args) {
    List<T> result = new ArrayList<T>();
    for(T item : args)
      result.add(item);
    return result;
  }
  public static void main(String[] args) {
    List<String> ls = makeList("A");
    System.out.println(ls);
    ls = makeList("A", "B", "C");
    System.out.println(ls);
    ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));
    System.out.println(ls);
  }
}