java基础之泛型知识点总结

作者:程序员孙大圣 时间:2021-09-03 21:30:29 

一、什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

没有泛型之前:


private static void genericTest() {
   List arrayList = new ArrayList();
   arrayList.add("总有刁民想害朕");
   arrayList.add(7);

for (int i = 0; i < arrayList.size(); i++) {
       Object item = arrayList.get(i);
       if (item instanceof String) {
           String str = (String) item;
           System.out.println("泛型测试 item = " + str);
       }else if (item instanceof Integer)
       {
           Integer inte = (Integer) item;
           System.out.println("泛型测试 item = " + inte);
       }
   }
}

如上代码所示,在没有泛型之前 类型的检查 和 类型的强转 都必须由我们程序员自己负责,一旦我们犯了错,就是一个运行时崩溃等着我们。 

有了泛型之后:


private static void genericTest2() {
    List<String> arrayList = new ArrayList<>();
    arrayList.add("总有刁民想害朕");
    arrayList.add(7); //..(参数不匹配:int 无法转换为String)
    ...
}

如上代码,编译器在编译时期即可完成 类型检查 工作,并提出错误(其实IDE在代码编辑过程中已经报红了)

 二、泛型的特性是什么?

大家都知道,Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

如在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与++模板机制实现方式之间的重要区别。 

什么是类型擦除?

类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。

通过两个例子来理解泛型的类型擦除。

例一:


public class Test {

public static void main(String[] args) {

ArrayList<String> list1 = new ArrayList<String>();
       list1.add("abc");

ArrayList<Integer> list2 = new ArrayList<Integer>();
       list2.add(123);

System.out.println(list1.getClass() == list2.getClass());
   }

}

在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型StringInteger都被擦除掉了,只剩下原始类型。

例二:通过反射添加其它类型元素 


public class Test {

public static void main(String[] args) throws Exception {

ArrayList<Integer> list = new ArrayList<Integer>();

list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

list.getClass().getMethod("add", Object.class).invoke(list, "asd");

for (int i = 0; i < list.size(); i++) {
           System.out.println(list.get(i));
       }
   }

}

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

类型擦除后保留的原始类型,那么什么是原始类型呢?

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

例三:原始类型Object


class Pair<T> {  
   private T value;  
   public T getValue() {  
       return value;  
   }  
   public void setValue(T  value) {  
       this.value = value;  
   }  
}

Pair的原始类型为:


class Pair {  
   private Object value;  
   public Object getValue() {  
       return value;  
   }  
   public void setValue(Object  value) {  
       this.value = value;  
   }  
}

因为在Pair<T>中,T 是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair,如Pair<String>Pair<Integer>,但是擦除类型后他们的就成为原始的Pair类型了,原始类型都是Object

从上面的例2中,我们也可以明白ArrayList<Integer>被擦除类型后,原始类型也变为Object,所以通过反射我们就可以存储字符串了。

如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。

比如: Pair这样声明的话:


public class Pair<T extends Comparable> {}

那么原始类型就是Comparable

要区分原始类型和泛型变量的类型。

在调用泛型方法时,可以指定泛型,也可以不指定泛型。

  • 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object

  • 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类


public class Test {  
   public static void main(String[] args) {  

/**不指定泛型的时候*/  
       int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
       Number f = Test.add(1, 1.2); //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number  
       Object o = Test.add(1, "asd"); //这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object  

/**指定泛型的时候*/  
       int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类  
       int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float  
       Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
   }  

//这是一个简单的泛型方法  
   public static <T> T add(T x,T y){  
       return y;  
   }  
}

其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候的泛型为Object,就比如ArrayList中,如果不指定泛型,那么这个ArrayList可以存储任意的对象。

三、泛型的使用方式 

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

1.泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来 

这样的话,用户明确了什么类型,该类就代表着什么类型…用户在使用的时候就不用担心强转的问题,运行时转换异常的问题了。


/**
* Java泛型
*/
public class Demo {
   public static void main(String[] args) {
       // 定义泛型类 Test 的一个Integer版本
       Test<Integer> intOb = new Test<Integer>(88);
       intOb.showType();
       int i = intOb.getOb();
       System.out.println("value= " + i);
       System.out.println("----------------------------------");
       // 定义泛型类Test的一个String版本
       Test<String> strOb = new Test<String>("Hello Gen!");
       strOb.showType();
       String s = strOb.getOb();
       System.out.println("value= " + s);
   }
}
/*
使用T代表类型,无论何时都没有比这更具体的类型来区分它。如果有多个类型参数,我们可能使用字母表中T的临近的字母,比如S。
*/
class Test<T> {
   private T ob;

/*
   定义泛型成员变量,定义完类型参数后,可以在定义位置之后的方法的任意地方使用类型参数,就像使用普通的类型一样。
   注意,父类定义的类型参数不能被子类继承。
   */

//构造函数
   public Test(T ob) {
       this.ob = ob;
   }

//getter 方法
   public T getOb() {
       return ob;
   }

//setter 方法
   public void setOb(T ob) {
       this.ob = ob;
   }

public void showType() {
       System.out.println("T的实际类型是: " + ob.getClass().getName());
   }
}

/* output
   T的实际类型是: java.lang.Integer
   value= 88
   ----------------------------------
   T的实际类型是: java.lang.String
   value= Hello Gen!
*/

2.泛型接口


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

实现泛型接口,不指定类型:


class GeneratorImpl<T> implements Generator<T>{
   @Override
   public T method() {
       return null;
   }
}

实现泛型接口,指定类型:


class GeneratorImpl<T> implements Generator<String>{
   @Override
   public String method() {
       return "hello";
   }
}

泛型方法:判断一个方法是否是泛型方法关键看方法返回值前面有没有使用 <> 标记的类型,有就是,没有就不是


public class Normal {
  // 成员泛型方法
  public <E> String getString(E e) {
  return e.toString();
  }
  // 静态泛型方法
  public static <V> void printString(V v) {
  System.out.println(v.toString());
  }
 }
 // 泛型类中的泛型方法
 public class Generics<T> {
  // 成员泛型方法
  public <E> String getString(E e) {
  return e.toString();
  }
  // 静态泛型方法
  public static <V> void printString(V v) {
  System.out.println(v.toString());
  }
 }

四、Java中的泛型通配符

常用的 T,E,K,V,?

本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的:

  • ? 表示不确定的 java 类型

  • T (type) 表示具体的一个java类型

  • K V (key value) 分别代表java键值中的Key Value

  • E (element) 代表Element

?  * 通配符:

我有一个父类 Animal 和几个子类,如狗、猫等,现在我需要一个动物的列表,我的第一个想法是像这样的:


List<Animal> listAnimals

但是老板的想法确实这样的:


List<? extends Animal> listAnimals

为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。


static int countLegs (List<? extends Animal > animals ) {
   int retVal = 0;
   for ( Animal animal : animals )
   {
       retVal += animal.countLegs();
   }
   return retVal;
}

static int countLegs1 (List< Animal > animals ){
   int retVal = 0;
   for ( Animal animal : animals )
   {
       retVal += animal.countLegs();
   }
   return retVal;
}

public static void main(String[] args) {
   List<Dog> dogs = new ArrayList<>();
// 不会报错
   countLegs( dogs );
// 报错
   countLegs1(dogs);
}

当调用 countLegs1 时,就会飘红,提示的错误信息如下:

所以,对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 <?> ),表示可以持有任何类型。像 countLegs 方法中,限定了上届,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错。而 countLegs1 就不行。

上界通配符 < ? extends E>:

上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

如果传入的类型不是 E 或者 E 的子类,编译不成功泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

下界通配符 < ? super E>:

下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object

来源:https://blog.csdn.net/sunshunli/article/details/114241283

标签:java,泛型
0
投稿

猜你喜欢

  • Android Studio 3.3.2 正式版的安装教程图解

    2022-01-13 22:22:34
  • C#实现将RTF转为HTML的示例代码

    2023-11-14 02:32:18
  • Java中线程休眠编程实例

    2021-09-06 11:42:55
  • Java7之forkjoin简介_动力节点Java学院整理

    2023-08-31 14:08:28
  • Android 游戏开发中绘制游戏触摸轨迹的曲线图

    2023-02-26 08:50:30
  • maven环境变量配置讲解

    2023-11-29 12:35:51
  • 浅谈java 增强型的for循环 for each

    2023-03-30 18:51:05
  • C#实现XML文件与DataTable、Dataset互转

    2021-06-07 04:00:27
  • Android编程实现分页加载ListView功能示例

    2023-11-13 06:27:27
  • Android Listview多tab上滑悬浮效果

    2021-07-27 22:06:23
  • Android实现锁屏荧光效果

    2023-09-02 03:12:03
  • 基于Android SQLite的升级详解

    2021-06-24 05:26:05
  • C# 启动 SQL Server 服务的实例

    2022-02-10 21:59:38
  • Android微信抢红包功能的实现原理浅析

    2023-11-20 19:14:40
  • C#设置输入法实例分析

    2022-07-07 14:30:05
  • apache ant进行zip解压缩操作示例分享

    2021-11-08 09:16:03
  • C#用NPOI导出导入Excel帮助类

    2022-09-27 00:23:43
  • Spring事务失效的一种原因关于this调用的问题

    2022-07-19 20:59:21
  • 解决SpringBoot web项目启动后立即关闭的问题

    2023-07-26 02:33:37
  • Spring声明式事务和@Aspect的拦截顺序问题的解决

    2023-07-18 12:10:46
  • asp之家 软件编程 m.aspxhome.com