Как работать с Generics

Generics штука на самом деле простая. Они позволяют нам параметризировать классы или даже функции.

Лучше всего они проявляют себе для различных структур данных типа списка, или множества или словарика

Вот есть например у нас динамический список, который с дженериком выглядит так:

// вот тут параметр T, который означает что вместо него можно подставить какой угодно класс
public class DynamicArray <T> {
    Object[] data = new Object[3];
    int size = 0;
    
    // тут опять параметр T, который говорит что мы будем добавлять элемент типа T
    public void add(T element) {
        if (size >= data.length) {
            Object[] newData = new Object[data.length * 2];
            for (int i = 0; i < data.length; ++i) {
                newData[i] = data[i];
            }
            this.data = newData;
        }
        data[size] = element; // тут мы заносим элемент типа T в массив объектов Object[]
        size += 1;
    }
    
    // это нам должно вернуть элемент типа T
    public T at(int index) {
        if (index >= size) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return (T) data[index]; // преобразуем Object в T, так как data[index] это экземпляр класса Object
    }

    @Override
    public String toString() {
        T[] subArray = (T[]) Arrays.copyOfRange(data, 0, size);
        return Arrays.toString(subArray);
    }

    public int getSize() { return size; }
}

прелесть дженерика в том, что когда мы пишем

new DynamicArray<Integer>();

то java создает для нас класс следующего вида

public class DynamicArray {
    Object[] data = new Object[3];
    int size = 0;

    public void add(Integer element) { /* ... */ }

    public Integer at(int index) {
        if (index >= size) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return (Integer) data[index];
    }

    @Override
    public String toString() {
        Integer[] subArray = (Integer[]) Arrays.copyOfRange(data, 0, size);
        return Arrays.toString(subArray);
    }

    public int getSize() { return size; }
}

то есть везде где у нас было написано T подставляется Integer. А если б мы написали

new DynamicArray<String>();

то java при компиляции создала бы для нас такой класс

public class DynamicArray {
    Object[] data = new Object[3];
    int size = 0;

    public void add(String element) { /* ... */ }

    public String at(int index) {
        if (index >= size) {
            throw new ArrayIndexOutOfBoundsException();
        }
        return (String) data[index];
    }

    @Override
    public String toString() {
        String[] subArray = (String[]) Arrays.copyOfRange(data, 0, size);
        return Arrays.toString(subArray);
    }

    public int getSize() { return size; }
}

а когда мы пишем и так и сяк

new DynamicArray<Integer>();
new DynamicArray<String>();

то соответственно где-то там внутри создается два разных класса.

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

Например, в нашем DynamicArray классе нам абсолютно все равно какого типа объекты лежат в массиве.

Вся сложная логика списка у нас зашита в метод add, в котором просто идет перераспределение памяти:

// в данном коде никаких упоминаний ни T ни Integer нет
if (size >= data.length) {
    Object[] newData = new Object[data.length * 2];
    for (int i = 0; i < data.length; ++i) {
        newData[i] = data[i];
    }
    this.data = newData;
}
data[size] = element; // тут мы заносим элемент типа T в массив объектов Object[]
size += 1;

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

Специализация дженерика

Допустим хочу я, чтобы мой список умел находить максимальный элемент в списке и я напишу:

public class DynamicArray <T> {
    public Object[] data = new Object[3];
    int size = 0;

    public int getSize() { /*...*/ }

    public void add(T element) { /* ... */ }

    public T at(int index) { /* ... */ }

    @Override
    public String toString() { /* ... */ }
    
    // ну а шо, вполне себе функция по поиску максимум
    public T Max() {
        T max = (T) data[0]; 
        for(T el : (T[]) data) {
            if (el > max) {
                max = el;
            }
        }
        return max;
    }    
}

но к сожалению такой код даже не скомпилируется, потому джава не знает как сравнивать объекта типа T

ведь T это может быть какой угодно объекта, например класс Main вашей программы, ну и скажите мне какой Main тут больше

Main main1 = new Main();
Main main2 = new Main();

никто не знает 🤔

Поэтому чтобы такая логика начала работать необходимо уточнить какие именно объекты типа T мы хотим видеть.

Для этого мы используем при объявлении дженерика ключевое слово extends, например так

public class DynamicArray <T extends Comparable> {
    /* ... */
}

это значит что класс T должен быть наследником класса Comparable,

как мы помним, Comparable это интерфейс, который отвечает за возможность сравнения двух объектов. Его реализуют такие объекты как Integer, String и прочие. И имено поэтому мы можем их сравнивать между собой.

Но добавить extends Comparable не достаточно, надо все равно еще подкрутить метод Max.

Там есть операция сравнения, а она вообще вшита в джаву и использовать ее можно только для примитивных типов

public T Max() {
    T max = (T) data[0]; 
    for(T el : (T[]) data) {
        if (el > max) {  // <<< ВОТ ОНА
            max = el;
        }
    }
    return max;
}  

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

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

функция compareTo должна работать по следующему принципу

  • если this больше чем obj, то она возвращает положительное число (как правило 1)
  • если this равно obj, то она возвращает 0
  • если this меньше чем obj, то она возвращает отрицательное число (как правило -1)

таким образом в нашей функции Max мы вместо проверки на el > max будем проверять, что el.compareTo(max) равно 1

public T Max() {
    T max = (T) data[0]; 
    for(T el : (T[]) data) {
        if (el.compareTo(max) == 1) {  // <<< ВОТ ТАК
            max = el;
        }
    }
    return max;
}  

такие дела

Задание

Рассмотрим словарик сделанный на базе класса ArrayList

package com.company;

import java.util.ArrayList;

public class DynamicDict <K, V> {
    ArrayList<K> keys = new ArrayList<>();
    ArrayList<V> values = new ArrayList<>();

    // метод который присваивает значение ключу
    public void put(K key, V value) {
        int iKey = keys.indexOf(key);
        if (iKey == -1) {
            keys.add(key);
            values.add(value);
        } else {
            values.set(iKey, value);
        }
    }

    // метод который возвращает значение по ключу
    public V get(K key) {
        int iKey = keys.indexOf(key);
        if (iKey != -1) {
            return values.get(iKey);
        }
        return null;
    }
}

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

То есть как-то так работает:

DynamicDict<String, Integer> dict = new DynamicDict();
dict.put("Осень", 5);
dict.put("Зима", 9);
dict.put("Весна", 7);
dict.put("Лето", 4);

System.out.println(dict.getMaxKey()); // напечатает "Зима"

Тут должно быть сразу понятно, что класс V должен стать Comparable, хотя у нас и указано два дженерика, мы можем специализировать только один:

public class DynamicDict <K, V extends Comparable> {
    /* ... */
}

а дальше уже сами подумайте.

Если будут сложности, то пишите мне в вк mkatash