Java泛型是J2 SE1.5中引入的一个新特性,其本质是参数化类型,也就是说所操作的数据类型被指定为一个参数(type parameter)这种参数类型可以用在接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

原文链接:全网最完整Java学习笔记

基本介绍

泛型,即参数化类型。泛型的出现是为了统一集合当中的数据类型。可在编译阶段约束操作的数据类型,并进行检查
参数化类型:在方法定义时,将方法签名中的形参数据类型设置为参数(可称之为类型参数:尖括号 <> 中的泛型标识,用于指代任何数据类型),调用该方法时再从外部传入一个具体的数据类型和变量。

泛型的本质

将类、接口和方法中具体的类型参数化,并且提供了编译时类型安全检测机制。通过使用泛型,可以避免使用Object类导致的类型转换错误和减少了代码的冗余。泛型使用过程中,数据类型被设置为一个参数,使用时从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型若不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
细节:不能写基本数据类型;指定泛型具体类型后,传递数据时可传该类型和其子类类型;若不写泛型,默认是Object。

使用场景:定义类、方法、接口的时候,若类型不确定,可定义泛型;若类型不确定,但知道继承体系,可用泛型通配符 ?

注意:泛型不具备继承性,但数据具备继承性

泛型的标志

尖括号<>是泛型的标志,例如ArrayList<E>就是一个泛型,<E>将实际的集合元素类型参数化了,这样我们使用时可以指定new ArrayList<String>,将它指定:

1
2
3
4
5
6
7
8
9
// ArrayList<E>是标准的类泛型,在使用时指定这个“E”具体是什么
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
}

// 我们在使用ArrayList时,尖括号指定<String>,这样它就只能存String类型的元素了
// 一旦存其他类型,就会在代码下面出现红色波浪线,编译期间就报错
ArrayList<String> list = new ArrayList<String>();

若不用泛型,而用public class ArrayList<Object>{}方式声明ArrayList,就可往集合里存所有类型的参数,编译不报错,但可读性很差,不知道它具体应存哪些类型,存的类型非业务所需类型时,编译期间不报错,直到生产环境运行时报错,就会出现不好的影响。

详细介绍:

泛型:将具体的类型参数化,是一种编程范式,提供了编译时类型安全检测机制。

通过使用泛型,可以将数据类型作为参数传递给类、接口或方法,可以在编译时期进行类型检查,避免在运行时期出现类型转换错误。

泛型的范围:泛型接口,泛型类(创建对象时再指定具体类型),泛型方法。

实现方式:以泛型类举例。只需要在类名后面使用尖括号<>将一个符号或多个符号包裹起来,这样在类里面就可以使用该符号代替具体类型了。使用泛型类时,调用者实际传进来什么类型,编译时就会将泛型符号擦除,替换成这个实际类型。

泛型标识:泛型符号可以是任意符号,但我们约定使用T、E、K、V等符号。Java 常见泛型标识及其代表含义如下:

1
2
3
4
5
T :代表一般的任何类。
E :代表 Element 元素的意思,或者 Exception 异常的意思。
K :代表 Key 的意思。
V :代表 Value 的意思,通常与 K 一起配合使用。
S :代表 Subtype 的意思。

格式

泛型参数类型

我们可以看见,前面 ArrayList,尖括号内是“E”,然后我们可能看见其他泛型尖括号内是“T”,具体是哪个大写字母,其实并没有特定的要求,只是遵循了某些约定俗成的惯例。

泛型参数类型的惯例

  • **<E>**:表示元素(Element),通常在集合类中使用。例如,List,Set
  • **<T>**:表示类型(Type),通常在一般类型中使用。例如,Box,Comparable
  • **<K><V>**:分别表示键(Key)和值(Value),通常在映射(Map)类中使用。例如,Map<K, V>,Entry<K, V>。
  • **<N>**:表示数字(Number),在需要表示数字的泛型中使用。

泛型类

泛型类定义了一个泛型参数,创建对象时给它传入这个参数的实际类型。 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// ArrayList简化版
public class MyList<E> {
// 初始容量
private static final int INITIAL_CAPACITY = 10;
// 存储元素的数组
private Object[] elementData;
// 当前元素数量
private int size;
// 构造方法,初始化数组和大小
public MyList() {
elementData = new Object[INITIAL_CAPACITY];
size = 0;
}
/**
* 添加元素到列表中
* @param e
* @return boolean
*/
public boolean add(E e) {
// 如果当前数组容量不足,则扩容
if (size == elementData.length) {
grow();
}
// 将元素添加到数组中,并增加元素数量
elementData[size++] = e;
return true;
}
/**
* 获取指定位置的元素
* @param index
* @return {@link E }
*/
@SuppressWarnings("unchecked")
public E get(int index) {
// 检查索引是否有效
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
// 返回元素,强制转换为泛型类型
return (E) elementData[index];
}
// 返回当前元素数量
public int size() {
return size;
}
// 扩容方法,将数组容量扩大 1.5 倍
private void grow() {
int newCapacity = elementData.length + (elementData.length >> 1);
elementData = Arrays.copyOf(elementData, newCapacity);
}

public static void main(String[] args) {
MyList<String> myList = new MyList<>();
myList.add("Hello");
myList.add("World");
myList.add("!");
System.out.println("元素数量: " + myList.size());
// 输出: 第一个元素: Hello
System.out.println("第一个元素: " + myList.get(0));
// 输出: 第二个元素: World
System.out.println("第二个元素: " + myList.get(1));
// 输出: 第三个元素: !
System.out.println("第三个元素: " + myList.get(2));
// 尝试获取索引越界的元素,抛出 IndexOutOfBoundsException
// System.out.println(myList.get(3));
}
}

泛型接口

泛型接口和泛型类类似,也是定义了一个泛型参数。不同的点是,泛型接口在被实现或者被继承时需要指定具体类型。

如果泛型接口的实现类不是泛型

  • 实现泛型接口时,如果没有省略尖括号“<>”,则必须在接口“<>”中指定类型
  • 实现泛型接口时,如果省略了尖括号“<>”,则默认“<>”内是Object类

如果泛型接口的实现类是泛型

  • 实现泛型接口时,实现类也必须是泛型类,并且类型与泛型接口保持一致
1
2
3
4
// 接口泛型
interface InterfaceName<T> {
// 接口中的方法可以使用类型参数T
}

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 泛型接口
public interface IGen<E> {
public void fun(E e);
}
// 如果泛型接口的实现类不是泛型,实现泛型接口时,如果没有省略尖括号“<>”,则必须在接口“<>”中指定类型
// 泛型接口实现类必须指定具体类型,否则会报错
public class Gen implements IGen<Integer>{
@Override
public void fun(Integer integer) {
System.out.println(integer);
}
}
// 如果泛型接口的实现类不是泛型,实现泛型接口时,如果省略了尖括号“<>”,则默认“<>”内是Object类
// 泛型接口实现类:如果接口省略<>,则默认是Object
public class Gen implements IGen{
/**
* 如果参数不是Object,就报错。因为接口了省略<>,默认是IGen<Object>
* @param integer
*/
@Override
public void fun(Object integer) {
System.out.println(integer);
}
}
// 如果泛型接口的实现类是泛型,实现泛型接口时,实现类也必须是泛型类,并且类型与泛型接口保持一致
// 泛型接口实现类也是泛型类时,泛型参数类型必须与泛型接口保持一致,用<E>,而不是<T>等
public class Gen<E> implements IGen<E>{
@Override
public void fun(E e) {
System.out.println(e);
}
}

泛型方法

当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。

然后返回类型、参数类型都可以用这个,当然也可以不用。格式:

1
2
3
4
// 方法泛型
public <T> 返回类型 方法名(参数类型 parameter) {
// 方法体
}

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Test {
/**
* 泛型方法1:返回值和参数可以T
* @param t
* @return {@link T }
*/
public <T> T fun1(T t){
return t;
}

/**
* 泛型方法1:参数可以是多个,但类型必须都是T
* @param a
* @param b
* @return {@link T }
*/
public <T> T fun2(T a,T b){
return a;
}

/**
* 泛型方法2:返回值和参数并不一定是T,也可以是具体类型
* @param integer
* @return {@link Integer }
*/
public <T> void fun3(Integer integer){
System.out.println(integer);
}

public static void main(String[] args) {
Test test = new Test();
// 实参传入String类型
System.out.println(test.fun1("Hello"));
// 实参传入int类型
System.out.println(test.fun1(1111));
// 实参传入多个String类型
System.out.println(test.fun2("Hello", "World"));
test.fun3(2222);
}
}

类型通配符

类型通配符跟泛型参数<T>、<E>等类似,用于表示不确定的类型,不同的点在于:

  • 类型参数:用于声明泛型类、泛型接口或泛型方法。声明时是未知类型,使用时擦除成具体的类型(在编译时泛型擦除)。
  • 类型通配符:用于使用泛型时,表示一种未知的类型。

类型通配符有三种

  • <?> 无限定的通配符。可用来表示任何类型。无限定通配符只能读Object类型的值,只能写null类型的值,其他类型都不能读写。
  • <? extends T>有上界的通配符。表示继承自T的任何类型,这里上界指的就是T。它通常用于生产者,即返回T。上界通配符只允许读值,不允许写null以外值。
  • <? super T> 有下界的通配符。表示子类是T的任何类型,这里下界指的就是T。它通常用于消费者,即写入T。下界类型通配符只允许写值,不允许读Object以外的值。

无限定类型通配符:<?>

例如List<?>:表示元素类型未知的List,它的元素可以匹配任何类型。无限定通配符只能读Object类型的值,只能写null类型的值,其他类型都不能读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 public class Test {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
strings.add("he");
integers.add(23);
// 1.无界通配符用作形参,实参可以是任意元素
// 打印集合:List<?>可以接受所有List元素类型的实参
printList(strings);
printList(integers);
List<?> anyList = new ArrayList<>();
// 正确,因为无限定通配符可以写null类型的值。
anyList.add(null);
// 下面代码会报错,因为无限定通配符只能读Object类型的值,只能写null类型的值,其他类型都不能读写。
// anyList.add(23);
}
/**
* * 打印集合:List<?>可以接受所有List元素类型的实参。Collection<?>可以接受任何类型任何元素类型的集合
* @param c 集合元素
*/
public static void printList(List<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
}

上界类型通配符:List<? extends 指定类型>

表示继承自T的任何类型,这里上界指的就是T。它主要用于写入数据的场景。

上界类型通配符只允许读值,不允许写null以外的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
strings.add("he");
integers.add(23);
// 上界通配符可以接受继承于T的类,即T的子类
printNumbers(integers);
// 下面会报错,因为上界通配符,元素类型只能是Number的子类
// printNumbers(strings);
}
/**
* 上界通配符可以接受继承于T的类,即T的子类
* @param list
*/
public static void printNumbers(List<? extends Number> list) {
list.get(0);
// 下面会报错,因为上界类型通配符只允许读值,不允许写null以外值。
for (Number number : list) {
System.out.println(number);
}
}

下界类型通配符:List<? super 指定类型>

表示子类是T的任何类型,这里下界指的就是T。它主要用于读取数据的场景。

下界类型通配符只允许写值,不允许读Object以外的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
strings.add("he");
integers.add(23);
addNumbers(integers);
printList(integers);
// 下面会报错,因为上界通配符,元素类型只能是Number的子类
// printNumbers(strings);
}

/**
* * 下界通配符可以接受实现了Integer接口的类,即Integer的父类
* @param list
*/
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
// 正确。因为下界类型通配符允许写值
Object object = list.get(0);
// 下面会报错,因为下界类型通配符只允许写值,不允许读Object以外类型的值。
// Number number=list.get(0);
}

可变参数

(int… a)是将所有int参数封装到a数组里。

注意可变参数要放在后面。例如(int a,int… b)正确,(int… a,int b)会报错

Arrays工具类中有一个静态方法:

  • public static <T> List<T> asList(T.. a):返回由指定数组支持的固定大小的列表
  • 返回的集合不能做增删操作,可以做修改操作

List接口中有一个静态方法:

  • public static <E> List<E> of(E.. elements):返回包含任意数量元素的不可变列表
  • 返回的集合不能做增删改操作

Set接口中有一个静态方法:

  • public static <E>Set<E>of([...elements):返回一个包含任意数量元素的不可变集合
  • 在给元素的时候,不能给重复的元素
  • 返回的集合不能做增删操作,没有修改的方法

知识加油站

泛型的向上转型

泛型类或接口可以向上转型为父类,泛型符号不能向上转型。

泛型向上转型指的是将一个泛型对象转换为其父类类型或者接口类型的过程。这个过程实际上是将泛型对象的类型参数擦除,重新赋值为其父类或接口类型。

1
2
3
4
5
6
7
// 泛型类或接口可以向上转型:ArrayList<T>可以向上转型为List<T>
ArrayList<Integer> arrayList = new ArrayList<>();
List<Integer> list = arrayList; // 向上转型为 List<Integer>

// 泛型符号不能向上转型:ArrayList<Integer>泛型不可以向上转化为ArrayList<Number>。因为ArrayList<Number>接收ArrayList<float>,但ArrayList< Integer>不可以接收ArrayList< Float>,不能转回来
ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number> numberList = integerList; // 编译错误:不能将 ArrayList<Integer> 向上转型为 ArrayList<Number>

泛型的向下转型

向下转型和向上转型类似,指的是将一个父类类型的对象转换为子类类型的过程。这种转换需要进行类型检查,确保转换是安全的。

1
2
List<Integer> list = new ArrayList<>();
ArrayList<Integer> arrayList = (ArrayList<Integer>) list; // 向下转型为 ArrayList<Integer>

泛型擦除

泛型擦除:java的泛型是伪泛型,因为java在编译期间,所有的泛型类型都会被擦掉,并转换为普通类型

泛型擦除的主要目的是为了向低版本兼容,因为Java泛型是在JDK 1.5之后才引入的特性,为了保证旧有的代码能正常运行,Java编译器采用了泛型擦除来兼容之前的代码。

为什么要有泛型,而不是使用Object类

因为泛型是在编译时泛型擦除和替换实际类型的,而使用Object类会很麻烦,需要经常强制转换。

如List集合里,若直接声明存Object类,存的时候可通过多态机制直接向上转型,而取的时候就麻烦了,要强转Object类为String等对象,然后才能访问该对象的成员;而且不知道实际元素到底是String类型还是Integer等其他类型,还要通过i instanceof String判断类型,就更麻烦了。

泛型的优点

  • 防止运行时报错:可以在编译时检查类型安全,防止在程序运行期间出现BUG。
  • 隐式转换:所有的强制转换都是自动和隐式的,可以提高代码的重用率。

编译时安全检查:

Java在1.5版本中引入了泛型,在没有泛型之前,每次从集合中读取对象都必须进行类型转换,而这么做带来的结果就是:如果有人不小心插入了类型错误的对象,那么在运行时转换处理阶段就会出错。
在提出泛型之后,我们可以告诉编译器集合中接受哪些对象类型。编译器会自动的为你的插入进行转化,并在编译时告知是否插入了类型错误的对象。这使程序变得更加安全更加清楚。