Статьи

Java Generics Tutorial — пример класса, интерфейс, методы, шаблоны и многое другое

Обобщение является одной из основных функций Java-программирования и было представлено в Java 5. Если вы работали над коллекциями Java и с версией 5 или выше, я уверен, что вы использовали ее. Использовать дженерики с классами коллекций очень просто, но они предоставляют гораздо больше возможностей, чем просто создание типа коллекции, и мы попытаемся изучить особенности дженериков в этой статье. Понимание дженериков может иногда сбивать с толку, если мы используем жаргонные слова, поэтому я постараюсь сделать его простым и легким для понимания.
В этом уроке мы рассмотрим следующие темы об обобщениях. Java-генерики-учебник

  1. Пример Java Generics
  2. Обобщения с классом и интерфейсами
  3. Соглашение об именовании типов Generics
  4. Обобщения в методах и конструкторах
  5. Общие параметры ограниченного типа
  6. Дженерики и Наследование
  7. Общие классы и подтипы
  8. Универсальные шаблоны
    1. Обобщения Верхний ограниченный шаблон
    2. Дженерики Неограниченный Wildcard
    3. Обобщения Нижний ограниченный шаблон
  9. Подтипирование с использованием универсального подстановочного знака
  10. Тип Erasure

Пример Java Generics

Обобщения были добавлены в Java 5 для обеспечения проверки типов во время компиляции и устранения риска ClassCastException который был распространен при работе с классами коллекций. Вся структура коллекции была переписана для использования обобщений для обеспечения безопасности типов. Давайте посмотрим, как дженерики помогают нам безопасно использовать классы коллекций.

1
2
3
4
5
6
7
List list = new ArrayList();
list.add("abc");
list.add(new Integer(5)); //OK
 
for(Object obj : list){
        String str=(String) obj; //type casting leading to ClassCastException at runtime
}

Выше код компилируется нормально, но выдает ClassCastException во время выполнения, потому что мы пытаемся привести Object в списке к String, тогда как один из элементов имеет тип Integer. После Java 5 мы используем классы коллекций, как показано ниже.

1
2
3
4
5
6
7
List<String> list1 = new ArrayList<String>(); // java 7 ? List<String> list1 = new ArrayList<>();
list1.add("abc");
//list1.add(new Integer(5)); //compiler error
 
for(String str : list1){
     //no type casting needed, avoids ClassCastException
}

Обратите внимание, что во время создания списка мы указали, что тип элементов в списке будет String. Поэтому, если мы попытаемся добавить любой другой тип объекта в список, программа выдаст ошибку времени компиляции. Также обратите внимание, что в цикле for нам не требуется приведение типов элемента в списке, следовательно, удаление ClassCastException во время выполнения.

Обобщения с классом и интерфейсами

Мы можем определить наши собственные классы и интерфейсы с помощью обобщенного типа. Универсальный тип — это класс или интерфейс, параметризованный над типами. Мы используем угловые скобки (<>) для указания параметра типа.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package com.journaldev.generics;
 
public class GenericsTypeOld {
 
    private Object t;
 
    public Object get() {
        return t;
    }
 
    public void set(Object t) {
        this.t = t;
    }
 
        public static void main(String args[]){
        GenericsTypeOld type = new GenericsTypeOld();
        type.set("Pankaj");
        String str = (String) type.get(); //type casting, error prone and can cause ClassCastException
    }
}

Обратите внимание, что при использовании этого класса мы должны использовать приведение типов, и он может создавать ClassCastException во время выполнения. Теперь мы будем использовать дженерики, чтобы переписать тот же класс с типом дженериков, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.journaldev.generics;
 
public class GenericsType<T> {
 
    private T t;
 
    public T get(){
        return this.t;
    }
 
    public void set(T t1){
        this.t=t1;
    }
 
    public static void main(String args[]){
        GenericsType<String> type = new GenericsType<>();
        type.set("Pankaj"); //valid
 
        GenericsType type1 = new GenericsType(); //raw type
        type1.set("Pankaj"); //valid
        type1.set(10); //valid and autoboxing support
    }
}

Обратите внимание на использование класса GenericsType в методе main. Нам не нужно выполнять приведение типов, и мы можем удалить ClassCastException во время выполнения. Если мы не предоставляем тип во время создания, компилятор выдаст предупреждение, что «GenericsType является необработанным типом. Ссылки на универсальный тип GenericsType <T> должны быть параметризованы ». Когда мы не предоставляем тип, тип становится Object и, следовательно, он разрешает объекты как String, так и Integer, но мы всегда должны стараться избегать этого, потому что нам придется использовать приведение типов при работе с необработанным типом, который может вызвать ошибки во время выполнения.

Совет : Мы можем использовать @SuppressWarnings("rawtypes") для подавления предупреждения компилятора, ознакомьтесь с руководством по Java-аннотациям .

Также обратите внимание, что он поддерживает Java Autoboxing .

Сопоставимый интерфейс является отличным примером универсальных интерфейсов и написан так:

1
2
3
4
5
6
package java.lang;
import java.util.*;
 
public interface Comparable<T> {
    public int compareTo(T o);
}

Аналогичным образом мы можем использовать дженерики в наших интерфейсах и классах. Мы также можем иметь несколько параметров типа, как в интерфейсе карты. Опять же, мы можем предоставить параметризованное значение параметризованному типу, например, new HashMap<String, List<String>>(); действует.

Соглашение об именовании типов Generics

Соглашение об именах помогает нам легко понять код, а соглашение об именах — одна из лучших практик языка программирования Java. Таким образом, дженерики также имеют свои собственные соглашения об именах. Обычно имена параметров типа представляют собой одинарные прописные буквы, чтобы их было легко отличить от переменных Java. Наиболее часто используемые имена параметров типа:

  • Электронный элемент (широко используется в Java Collections Framework, например, ArrayList, Set и т. Д.)
  • K — Ключ (используется на карте)
  • N — номер
  • T — Тип
  • V — значение (используется на карте)
  • S, U, V и т. Д. — 2-го, 3-го, 4-го типов

Обобщения в методах и конструкторах

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

Вот класс, показывающий пример типа обобщений в методе.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.journaldev.generics;
 
public class GenericsMethods {
 
    //Generics in method
    public static <T> boolean isEqual(GenericsType<T> g1, GenericsType<T> g2){
        return g1.get().equals(g2.get());
    }
 
    public static void main(String args[]){
        GenericsType<String> g1 = new GenericsType<>();
        g1.set("Pankaj");
 
        GenericsType<String> g2 = new GenericsType<>();
        g2.set("Pankaj");
 
        boolean isEqual = GenericsMethods.<String>isEqual(g1, g2);
        //above statement can be written simply as
        isEqual = GenericsMethods.isEqual(g1, g2);
        //This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets.
        //Compiler will infer the type that is needed
    }
}

Обратите внимание на сигнатуру метода isEqual, показывающую синтаксис для использования универсального типа в методах. Также обратите внимание, как использовать эти методы в нашей Java-программе. Мы можем указать тип при вызове этих методов или вызвать их как обычный метод. Компилятор Java достаточно умен, чтобы определить тип используемой переменной, это средство называется выводом типа .

Общие параметры ограниченного типа

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

1
2
3
public static <T extends Comparable<T>> int compare(T t1, T t2){
        return t1.compareTo(t2);
    }

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

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

Generics также поддерживает множественные границы, то есть <T расширяет A & B & C>. В этом случае A может быть интерфейсом или классом. Если A является классом, то B и C должны быть интерфейсами. Мы не можем иметь более одного класса в нескольких границах.

Дженерики и Наследование

Мы знаем, что наследование Java позволяет нам присваивать переменную A другой переменной B, если A является подклассом B. Поэтому мы можем подумать, что любому универсальному типу A можно назначить универсальный тип B, но это не так. Давайте посмотрим на это с помощью простой программы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.journaldev.generics;
 
public class GenericsInheritance {
 
    public static void main(String[] args) {
        String str = "abc";
        Object obj = new Object();
        obj=str; // works because String is-a Object, inheritance in java
 
        MyClass<String> myClass1 = new MyClass<String>();
        MyClass<Object> myClass2 = new MyClass<Object>();
        //myClass2=myClass1; // compilation error since MyClass<String> is not a MyClass<Object>
        obj = myClass1; // MyClass<T> parent is Object
    }
 
    public static class MyClass<T>{}
 
}

Нам не разрешено присваивать переменную MyClass <String> переменной MyClass <Object>, поскольку они не связаны, фактически родительский элемент MyClass <T> равен Object.

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

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

Например, ArrayList <E> реализует List <E>, который расширяет Collection <E>, поэтому ArrayList <String> является подтипом List <String>, а List <String> является подтипом Collection <String>.

Отношение подтипа сохраняется до тех пор, пока мы не изменим аргумент типа, ниже приведен пример нескольких параметров типа.

1
2
interface MyList<E,T> extends List<E>{
}

Подтипами List <String> могут быть MyList <String, Object>, MyList <String, Integer> и так далее.

Универсальные шаблоны

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

Обобщения Верхний ограниченный шаблон

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

1
2
3
4
5
6
7
public static double sum(List<Number> list){
        double sum = 0;
        for(Number n : list){
            sum += n.doubleValue();
        }
        return sum;
    }

Теперь проблема с вышеупомянутой реализацией состоит в том, что она не будет работать со списком целых или двойных чисел, потому что мы знаем, что List <Integer> и List <Double> не связаны, это когда подстановочный знак с ограничением сверху полезен. Мы используем универсальный шаблон с ключевым словом extends и классом или интерфейсом верхней границы, что позволит нам передавать аргумент типа верхней границы или его подклассов.

Вышеуказанная реализация может быть изменена, как показано ниже программы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.journaldev.generics;
 
import java.util.ArrayList;
import java.util.List;
 
public class GenericsWildcards {
 
    public static void main(String[] args) {
        List<Integer> ints = new ArrayList<>();
        ints.add(3); ints.add(5); ints.add(10);
        double sum = sum(ints);
        System.out.println("Sum of ints="+sum);
    }
 
    public static double sum(List<? extends Number> list){
        double sum = 0;
        for(Number n : list){
            sum += n.doubleValue();
        }
        return sum;
    }
}

Это похоже на написание нашего кода в терминах интерфейса, в приведенном выше методе мы можем использовать все методы класса Number с верхней границей. Обратите внимание, что с ограниченным сверху списком нам не разрешается добавлять в список любые объекты, кроме null. Если мы попытаемся добавить элемент в список внутри метода sum, программа не скомпилируется.

Дженерики Неограниченный Wildcard

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

1
2
3
4
5
public static void printData(List<?> list){
        for(Object obj : list){
            System.out.print(obj + "::");
        }
    }

Мы можем предоставить List <String> или List <Integer> или любой другой тип аргумента списка объектов для метода printData . Как и в случае с верхним списком, мы не можем ничего добавлять в список.

Обобщения Нижний ограниченный шаблон

Предположим, что мы хотим добавить целые числа в список целых чисел в методе, мы можем сохранить тип аргумента как List <Integer>, но он будет связан с целыми числами, тогда как List <Number> и List <Object> также могут содержать целые числа, поэтому мы можем использовать подстановочный знак нижней границы для достижения этой цели. Для этого мы используем универсальный шаблон (?) С ключевым словом super и классом нижней границы.

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

1
2
3
public static void addIntegers(List<? super Integer> list){
        list.add(new Integer(50));
    }

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

1
2
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number>  numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

Тип Erasure

Generics был добавлен для обеспечения проверки типов во время компиляции, и он не используется во время выполнения, поэтому java-компилятор использует функцию стирания типов для удаления всего кода проверки обобщенных типов в байтовом коде и вставки приведения типов при необходимости. Стирание типа гарантирует, что новые классы для параметризованных типов не создаются; следовательно, генерики не несут затрат времени выполнения.

Например, если у нас есть универсальный класс, как показано ниже;

01
02
03
04
05
06
07
08
09
10
11
12
public class Test<T extends Comparable<T>> {
 
    private T data;
    private Test<T> next;
 
    public Test(T d, Test<T> n) {
        this.data = d;
        this.next = n;
    }
 
    public T getData() { return this.data; }
}

Компилятор Java заменяет параметр ограниченного типа T на первый связанный интерфейс, Comparable, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
public class Test {
 
    private Comparable data;
    private Test next;
 
    public Node(Comparable d, Test n) {
        this.data = d;
        this.next = n;
    }
 
    public Comparable getData() { return data; }
}

Вот и все для дженериков в Java, дженерики — это действительно обширная тема, и для ее понимания и эффективного использования требуется много времени. Этот пост является попыткой предоставить базовые сведения об обобщениях и о том, как мы можем использовать их для расширения нашей программы с безопасностью типов.