泛型概括

什么是泛型?

Java 泛型(generics)是 JDK 5 中引入的一个新特性,提供了编译时类型安全检测机制,允许在定义类、接口或方法时使用一个标识来表示某个属性的类型、方法的返回值、参数类型

为什么使用泛型?

泛型是Java编程语言中的一项重要特性,它提供了以下几个主要的好处和用途:

  • 保证了类型的安全性:使用泛型可以在编译时期捕获类型错误,避免在运行时期出现类型转换异常,减少了因类型不匹配而引起的潜在错误。
  • 避免频繁的类型转换操作:在泛型中,类型转换是隐式的,并不需要显式地进行类型转换,使用泛型可以避免频繁的类型转换操作(装箱、拆箱),从而减少了代码中的冗余部分,使代码更简洁易读
  • 提高了代码的可读性:通过在定义类、接口或方法时使用泛型,能够明确指定类或方法的输入和输出的数据类型,从而使得代码更加具有自描述性

使用案例

(1)泛型可以统一集合中的数据类型,提高安全性,减少强制类型转换,不使用泛型时,会出现一些问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo {
public static void main(String[] args) {
// 需求:不使用泛型,将学生成绩存入集合
ArrayList arrayList = new ArrayList();
arrayList.add(70);
arrayList.add(80);
arrayList.add(90);
// 问题一:类型不安全,此时编译不出异常,但是不是需要存储的数据
arrayList.add("小明");

// 遍历集合
for (Object o : arrayList) {
// 问题二:强转时可能出现ClassCastException(类型转换异常)
// 由于需要强转成int型输出成绩,一旦出现了其他数据类型的数据就会强转出错
int results = (Integer) o;
System.out.println(results);
}
}
}

(2)使用了泛型后,会解决问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo {
public static void main(String[] args) {
// 需求:使用泛型,将学生成绩存入集合
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(70);
arrayList.add(80);
arrayList.add(90);
// 此时编译非int时会报异常,保证了数据安全
// arrayList.add("小明");

// 遍历集合
for (Integer i : arrayList) {
// 此时不用强转就可以遍历
System.out.println(i);
}
}
}

泛型使用方式

泛型类

(1)语法

1
2
// 类使用泛型
修饰符 class 类名<泛型类型>{}

(2)代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 类中指定的泛型<T>可以随便写为任意标识,常见的有T、E、K、V等形式的参数。
* 表示泛型泛型在定义的时候不具体,使用的时候确定泛型的具体数据类型,即在创建对象的时候确定泛型。
*/
public class Demo<T> {
// 成员变量id的类型为T,T的类型由外部指定
private T id;

// 泛型构造方法形参id的类型也为T,T的类型由外部指定
public Demo(T id) {
this.id = id;
}
// 泛型方法getT的返回值类型为T,T的类型由外部指定
public void setId(T id) {
this.id = id;
}
}

泛型方法

(1)语法

1
2
// 方法使用泛型
修饰符 <泛型类型> 方法返回值 方法名(形参列表){方法体}

(2)代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Demo<T> {
/**
* @param t 传入泛型的参数
* @param <T> 泛型的类型
* @return T 返回值为T类型
* 说明:
* (1)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* (2)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* (3)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
*/
public <T> T testMethod(T t) {
System.out.println(t.getClass());
System.out.println(t);
return t;
}
}

泛型接口

(1)语法

1
2
// 接口使用泛型
修饰符 interface 接口名<泛型类型>{}

(2)代码示例

1
2
3
4
5
6
/**
* 定义一个泛型接口
*/
public interface testInteface<T> {
public abstract void add(T t);
}

泛型嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void test3() {
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

//泛型嵌套
Set<Map.Entry<String, Integer>> entry = map.entrySet();
//遍历
Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry1 = iterator.next();
String key = entry1.getKey();
Integer value = entry1.getValue();
System.out.println("key:" + key + " " + "value:" + value);
System.out.println("entry:" + entry1);
}
}

泛型通配符

泛型通配符是问号?符号,可以在使用泛型的时候代表一切类型,不知道使用什么类型来接收的时候使用

1
2
3
// List<?>是List<String>、List<Object>等各种泛型List的父类。
List<?>
Map<?,?>

泛型上下限

(1)语法及简介

上下限简介
类型名称 <? extends 类 > 对象名称不能往里存,只能往外取,只能接收该类型及其子类,即<=
类型名称 <? super 类 > 对象名称往外取只能赋值给Object变量,不影响往里存,只能接收该类型及其父类型,即>=

(2)代码示例

1
2
3
4
5
6
// 只允许泛型为Number及Number子类的引用调用,(无穷小,Number]
<? entends Number>
// 只允许泛型为Number及Number父类的引用调用,[Number,无穷大)
<? super Number>
// 只允许泛型为实现Comparable接口的实现类的引用调用
<? entends Comparable>

(3)已知Object类、Animal类、Dog类、Cat类、其中Animal是Dog和Cat的父类

1
2
3
class Animal{}//父类
class Dog extends Animal{}//子类
class Cat extends Animal{}//子类

泛型的上限只能是该类型的类型及其子类<? extends 类 >

1
2
3
4
// ArrayList<? extends Animal> list1 = new ArrayList<Object>();//报错
ArrayList<? extends Animal> list2 = new ArrayList<Animal>();
ArrayList<? extends Animal> list3 = new ArrayList<Dog>();
ArrayList<? extends Animal> list4 = new ArrayList<Cat>();

泛型的下限只能是该类型的类型及其父类<? super 类 >

1
2
3
4
ArrayList<? super Animal> list5 = new ArrayList<Object>();
ArrayList<? super Animal> list6 = new ArrayList<Animal>();
// ArrayList<? super Animal> list7 = new ArrayList<Dog>();//报错
// ArrayList<? super Animal> list8 = new ArrayList<Cat>();//报错

泛型擦除

什么是泛型擦除?

类型擦除是指在 Java 编译器编译泛型代码时,将泛型类型信息擦除并替换为原始类型或最顶级父类的过程。简单来说,就是泛型相关信息只存在于代码的编译阶段,在编译之后的字节码文件(class 文件)中不包含任何泛型信息,泛型参数会被替换为其上界或者 Object 类型。具体来说,泛型擦除会导致以下几个变化:

  • 替换泛型类型参数:在字节码中,泛型类型参数会被替换为其最顶级的边界类型或者 Object 类型。例如,一个 List<T> 类型,在类型擦除后会被替换为 List 或者 List<Object>
  • 移除类型参数的具体类型:泛型类型中的具体类型信息,如泛型参数的实际类型或者参数化类型的参数,会被移除。例如,一个 List<String> 在类型擦除后会变成简单的 List
  • 强制进行类型转换:由于类型擦除导致泛型参数丢失,编译器会在需要的地方插入强制类型转换以保持类型安全。这些类型转换操作可能会在运行时引发异常,因此开发人员需要注意正确处理类型转换的问题。
1
2
3
4
5
6
7
8
// 泛型擦除前
public static <T> List<T> Example(T t1, T t2){
return new ArrayList<T>();
}
// 泛型擦除后
public static <Object> List Example(Object t1, Object t2){
return new ArrayList();
}

为什么要进行泛型擦除?

Java的泛型是在JDK 5中引入的,它提供了编译时类型检查和更强的类型安全性,为了保持与之前版本的兼容性,所以才使用泛型擦除。泛型擦除是Java编译器在生成字节码时的一种优化方式,它的目的是为了兼容Java的泛型和之前版本的非泛型代码。

  • 编译器的兼容性:Java泛型是在JDK 5引入的,为了保持对旧版本Java的兼容性,编译器在生成字节码时将泛型类型擦除为它们的上界或者Object类型。
  • 减少重复字节码:如果不进行泛型擦除,每个具体的泛型类型都会生成一个独立的类文件,导致生成大量重复的字节码。泛型擦除可以通过类型擦除的方式减少字节码的冗余,提高了编译后的代码的效率和性能。
  • 泛型类型的互操作性:泛型擦除使得使用泛型的代码可以与不使用泛型的代码进行互操作,让泛型代码能够与之前的非泛型代码无缝地集成。