Друг удвоил свою зарплату после изучения дженериков Java!

Java

задний план

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

Эта статья относится к подробному объяснению дженериков Java, универсальному методу в Java и подробному объяснению дженериков Java.

Я организовал некоторые связанные книги и документы, которыми можно поделиться с вами бесплатно, просто нажмите на ссылку, чтобы присоединиться к группе!

1 Обзор

Обобщения занимают очень важное место в java и широко используются в объектно-ориентированном программировании и различных шаблонах проектирования.

Что такое дженерики? Зачем использовать дженерики?

Обобщения или «параметризованные типы». Когда дело доходит до параметров, наиболее привычным является определение метода с формальными параметрами, а затем передача фактических параметров при вызове метода. Итак, как вы понимаете параметризованные типы?

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

Затем передайте конкретный тип (аргумент типа) при использовании/вызове.

Суть дженериков заключается в параметризации типов (без создания нового типа, путем указания разных типов, указанных дженериками, для управления типом конкретных ограничений формального параметра). То есть при использовании дженериков

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

2. Каштан

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

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

Несомненно, результат работы программы закончится крахом:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

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

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

List<String> arrayList = new ArrayList<String>();
...
//arrayList.add(100); 在编译阶段,编译器就会报错

3. Особенности

Обобщения действительны только во время компиляции. См. код ниже:

List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();

Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();

if(classStringArrayList.equals(classIntegerArrayList)){
    Log.d("泛型测试","类型相同");
}

Выходной результат:D/泛型测试: 类型相同.

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

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

4. Использование дженериков

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

4.3 Общие классы

Универсальные типы используются в определениях классов и называются универсальными классами. С помощью дженериков можно выполнять операции над группой классов, открывая один и тот же интерфейс для внешнего мира. Наиболее типичными являются различные классы контейнеров, такие как: List, Set, Map.

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

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

Наиболее распространенный общий класс:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
//传入的实参类型需与泛型的类型参数类型相同,即为Integer.
Generic<Integer> genericInteger = new Generic<Integer>(123456);

//传入的实参类型需与泛型的类型参数类型相同,即为String.
Generic<String> genericString = new Generic<String>("key_vlaue");
Log.d("泛型测试","key is " + genericInteger.getKey());
Log.d("泛型测试","key is " + genericString.getKey());
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456
12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue

Должен ли определенный универсальный класс передавать аргумент универсального типа? Это не так, при использовании дженериков, если переданы дженерик-параметры, будут сделаны соответствующие ограничения по переданным дженерикам параметрам, и тогда дженерики будут играть роль ограничений, которые должны играть. Если аргумент универсального типа не передается, тип, определенный универсальным методом или переменной-членом в универсальном классе, может быть любого типа.

См. пример:

Generic generic = new Generic("111111");
Generic generic1 = new Generic(4444);
Generic generic2 = new Generic(55.55);
Generic generic3 = new Generic(false);

Log.d("泛型测试","key is " + generic.getKey());
Log.d("泛型测试","key is " + generic1.getKey());
Log.d("泛型测试","key is " + generic2.getKey());
Log.d("泛型测试","key is " + generic3.getKey());
D/泛型测试: key is 111111
D/泛型测试: key is 4444
D/泛型测试: key is 55.55
D/泛型测试: key is false

Уведомление:

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

if(ex_num instanceof Generic){ }

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

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

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

Когда класс, реализующий универсальный интерфейс, не передает общие аргументы:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class FruitGenerator<T> implements Generator<T>{
 * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
 */
class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

Когда класс, реализующий универсальный интерфейс, передает общие аргументы:

/**
 * 传入泛型实参时:
 * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
 * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
 */
public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

4.5 Общие подстановочные знаки

мы знаемIngeterдаNumberПодкласс , который мы также проверили в главе о функцияхGeneric<Ingeter>иGeneric<Number>На самом деле это тот же базовый тип. Затем возникает проблема при использованииGeneric<Number>Можно ли использовать метод в качестве формального параметра?Generic<Ingeter>экземпляр входящего? логически похоже наGeneric<Number>иGeneric<Ingeter>Можно ли его рассматривать как общий тип с отношениями родитель-потомок?

Чтобы выяснить это, мы используемGeneric<T>Этот универсальный класс продолжается следующим примером:

public void showKeyValue1(Generic<Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gNumber);

// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> 
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);

Через оперативную информацию мы можем видетьGeneric<Integer>Не может считаться подклассом Generic. Отсюда видно, что один и тот же универсальный тип может соответствовать нескольким версиям (поскольку тип параметра не определен), а экземпляры универсального класса разных версий несовместимы.

Возвращаясь к приведенному выше примеру, как решить вышеуказанную проблему? не всегда может быть обработан для определения нового методаGeneric<Integer>Тип класса, который явно идет вразрез с идеей множественных наборов в java. Поэтому нам нужно логическое представление, которое одновременноGeneric<Integer>иGeneric<Number>Ссылочный тип родительского класса. Этот тип подстановочного знака появился.

Мы можем изменить вышеуказанный метод на:

public void showKeyValue1(Generic<?> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}

Подстановочные знаки типа обычно используются? Обратите внимание, что вместо конкретных аргументов типа здесь '? ' является аргументом типа, а не параметром типа. Важно сказать три раза! здесь'? ' является аргументом типа, а не параметром типа! здесь'? ' является аргументом типа, а не параметром типа! Грубо говоря, значит, здесь? Подобно Number, String и Integer, это фактический тип.Вы можете поставить ? Думайте об этом как о родительском классе всех типов. является реальным типом.

Можно решить, что когда конкретный тип неясен, этот подстановочный знак равен ?; при работе с типом, когда конкретная функция типа не требуется, используется только функция в классе Object. Затем вы можете использовать подстановочный знак ? для представления неизвестных типов.

4.6 Общие методы

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

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

Универсальный класс указывает конкретный тип универсального типа при создании экземпляра класса; универсальный метод указывает конкретный тип универсального типа при вызове метода.

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}
Object obj = genericMethod(Class.forName("com.test.test"));

4.6.1 Основное использование универсальных методов

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

public class GenericTest {
   //这个类是个泛型类,在上面已经介绍过
   public class Generic<T>{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
        public E setKey(E key){
             this.key = keu
        }
        */
    }

    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public <T,K> K showKeyName(Generic<T> container){
     *        ...
     *        }
     */
    public <T> T showKeyName(Generic<T> container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    public void showKeyValue1(Generic<Number> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic<?> obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

     /**
     * 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
     * 虽然我们声明了<T>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
    public <T> T showKeyName(Generic<E> container){
        ...
    }  
    */

    /**
     * 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class 'T' "
     * 对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。
     * 所以这也不是一个正确的泛型方法声明。
    public void showkey(T genericObj){

    }
    */

    public static void main(String[] args) {


    }
}

4.6.2 Общие методы в классах

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

public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

4.6.3 Общие методы и переменные параметры

Вот еще один пример универсального метода и переменных параметров:

public <T> void printMsg( T... args){
    for(T t : args){
        Log.d("泛型测试","t is " + t);
    }
}
printMsg("111",222,"aaaa","2323.4",55.55);

4.6.4 Статические методы и обобщения

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

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

public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

4.6.5 Краткое изложение общих методов

Общие методы позволяют изменять методы независимо от класса. Вот несколько основных рекомендаций:

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

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

4.6 Общие верхние и нижние границы

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

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

public void showKeyValue1(Generic<? extends Number> obj){
    Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类
//showKeyValue1(generic1);

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

Если мы также изменим определение универсального класса:

public class Generic<T extends Number>{
    private T key;

    public Generic(T key) {
        this.key = key;
    }

    public T getKey(){
        return key;
    }
}
//这一行代码也会报错,因为String不是Number的子类
Generic<String> generic1 = new Generic<String>("11111");

Другой пример универсального метода:

//在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加
//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
}

Из приведенных выше двух примеров видно: добавление верхней и нижней границ универсального типа должно происходить вместе с объявлением универсального типа.

4.7 Несколько слов об универсальных массивах

Я видел, что универсальные массивы упоминаются во многих статьях.Посмотрев документацию по солнцу, в java «невозможно создать массив точного универсального типа».

То есть следующий пример невозможен:

List<String>[] ls = new ArrayList<String>[10]; 

Можно использовать подстановочные знаки для создания общих массивов, как в следующем примере:

List<?>[] ls = new ArrayList<?>[10]; 

Это также возможно:

List<String>[] ls = new ArrayList[10];

использовать нижеSunдокументПример для иллюстрации проблемы:

List<String>[] lsa = new List<String>[10]; // Not really allowed.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Unsound, but passes run time store check    
String s = lsa[1].get(0); // Run-time error: ClassCastException.

В этом случае из-за механизма стирания дженериков JVM JVM не знает общей информации во время выполнения, поэтому ArrayList может быть назначен oa[1] без исключения.

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

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

List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.    
Object o = lsa;    
Object[] oa = (Object[]) o;    
List<Integer> li = new ArrayList<Integer>();    
li.add(new Integer(3));    
oa[1] = li; // Correct.    
Integer i = (Integer) lsa[1].get(0); // OK

5. Наконец

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

Это почти то же самое, что и здесь писать, всем судьям ставьте лайки и подписывайтесь!

Горячие статьи в прошлом:

end