Java 泛型

本文最后更新于:2024年3月18日 凌晨

Java 泛型

泛型简介

  • 泛型是Java语言的新特性,泛型的本质是参数化类型,也就是说,程序中的数据类型被指定为一个参数,泛型可以用在类,接口和方法的创建中,分别称为泛型类,泛型接口,泛型方法,下面给出了一个简单的使用泛型的例子,其中,<>之间定义形式类型参数。

[例14-1]:泛型的简单使用示例。

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
class Example<T> {   //T为类型参数。
private T obj;// 定义泛型成员变量。

public Example(T obj) {
this.obj = obj;
}

public T getObj() {
return obj;
}

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

public static void main(String[] args) {
Example<String> str = new Example<String>("Hello!");
str.showType();
String s = str.getObj();
System.out.println("Value=" + s);
}
}

T的实际类型java.lang.String
Value=Hello!
  • Java SE1.5之前的Java版本不支持泛型,系统为实现方法参数的通用性,一般将参数定义为Object类型,我们知道,任何对象均可传递给Object类型引用变量,从而实现参数的"任意化”,但是要将对象转换为原有类型就必须使用强制类型转换。
  • 泛型在定义时不指定参数的类型,用的时候来确定,这增加了程序的通用性,起到了程序"模版”化的效果,泛型的好处是在编译时检查类型安全,并且所有的强制类型转换都是自动和隐式的,泛型在使用中还有一些如下规则和限制:
    • 泛型的类型参数只能是类(包括自定义类),不能是简单类型。
    • 泛型的类型参数可以有多个,例如,Map<K,V>
    • 泛型的参数类型可以使用extends语句,例如<T extends Numbe>,extends并不代表继承,它是类型范围限制,表示T<=Number
    • 泛型的参数类型还可以是通配符类型,例如,ArratList<? Extends Number>,表示Number范围的某个类型,其中"?”代表未定类型。

泛型方法

  • 你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数,根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。
  • 下面是定义泛型方法的规则:
    • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的<E>)
    • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开,一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型称的标识符。
    • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
    • 泛型方法体的声明和其他方法一样,注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)

实例

  • 下面的例子演示了如何使用泛型方法打印不同字符串的元素:
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
public class GenericMethodTest
{
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray )
{
// 输出数组元素。
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}

public static void main( String args[] )
{
// 创建不同类型数组: Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };

System.out.println( "整型数组元素为:" );
printArray( intArray ); // 传递一个整型数组。

System.out.println( "\n双精度型数组元素为:" );
printArray( doubleArray ); // 传递一个双精度型数组。

System.out.println( "\n字符型数组元素为:" );
printArray( charArray ); // 传递一个字符型数组。
}
}
  • 编译以上代码,运行结果如下所示:
1
2
3
4
5
6
7
8
整型数组元素为:
1 2 3 4 5

双精度型数组元素为:
1.1 2.2 3.3 4.4

字符型数组元素为:
H E L L O

有界的类型参数

  • 可能有时候,你会想限制那些被允许传递到一个类型参数的类型种类范围,例如,一个操作数字的方法可能只希望接受Number或者Number子类的实例,这就是有界类型参数的目的。
  • 要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。

实例

  • 下面的例子演示了"extends"如何使用在一般意义上的意思"extends"(类)或者"implements"(接口),该例子中的泛型方法返回三个可比较对象的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MaximumTest
{
// 比较三个值并返回最大值。
public static <T extends Comparable<T>> T maximum(T x, T y, T z)
{
T max = x; // 假设x是初始最大值。
if ( y.compareTo( max ) > 0 ){
max = y; //y 更大。
}
if ( z.compareTo( max ) > 0 ){
max = z; // 现在 z 更大。
}
return max; // 返回最大对象。
}
public static void main( String args[] )
{
System.out.printf( "%d, %d 和 %d 中最大的数为 %d\n\n",3, 4, 5, maximum( 3, 4, 5 ) );

System.out.printf( "%.1f, %.1f 和 %.1f 中最大的数为 %.1f\n\n",6.6, 8.8, 7.7, maximum( 6.6, 8.8, 7.7 ) );

System.out.printf( "%s, %s 和 %s 中最大的数为 %s\n","pear","apple", "orange", maximum( "pear", "apple", "orange" ) );
}
}
  • 编译以上代码,运行结果如下所示:
1
2
3
4
5
3, 45 中最大的数为 5

6.6, 8.87.7 中最大的数为 8.8

pear, apple 和 orange 中最大的数为 pear

泛型类

  • 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
  • 和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开,一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符,因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

实例

  • 如下实例演示了我们如何定义一个泛型类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Box<T> {

private T t;

public void add(T t) {
this.t = t;
}

public T get() {
return t;
}

public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();

integerBox.add(new Integer(10));
stringBox.add(new String("测试"));

System.out.printf("整型值为:%d\n\n", integerBox.get());
System.out.printf("字符串为:%s\n", stringBox.get());
}
}
  • 编译以上代码,运行结果如下所示:
1
2
3
整型值为:10

字符串为:测试。

通配符与类型参数

  • 本质下面几种符号都是通配符,只不过是编码时的一种约定俗成的东西,通常情况下,T,E,K,V,? 是这样约定的:
    • T (type)表示具体的一个 Java 类型。
    • K V (key value)分别代表 Java 键值对中的Key Value
    • E (element)代表Element
    • 表示不确定的 Java 类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.*;

public class GenericTest {

public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();

name.add("icon");
age.add(18);
number.add(314);

getData(name);
getData(age);
getData(number);

}

public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}
}
  • 输出结果为:
1
2
3
data :icon
data :18
data :314

解析:因为getData()方法的参数是List类型的,所以name, age, number都可以作为这个方法的实参,这就是通配符的作用。

类型通配符上限

  • < ? extends E>:在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类。
    • 如果传入的类型不是 E 或者 E 的子类,编译不成功。
    • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用。

实例

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
import java.util.*;

public class GenericTest {

public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();

name.add("icon");
age.add(18);
number.add(314);

//getUperNumber(name);//1
getUperNumber(age);//2
getUperNumber(number);//3

}

public static void getData(List<?> data) {
System.out.println("data :" + data.get(0));
}

public static void getUperNumber(List<? extends Number> data) {
System.out.println("data :" + data.get(0));
}
}
  • 输出结果:
1
2
data :18
data :314

解析:在1处会出现错误,因为getUperNumber()方法中的参数已经限定了参数泛型上限为Number,所以泛型为String是不在这个范围之内,所以会报错。

类型通配符下限

  • < ? super E>:用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private <T> void test(List<? super T> dst, List<T> src){
for (T t : src) {
dst.add(t);
}
}

public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = new ArrayList<>();
new Test3().test(animals,dogs);
}
// Dog 是 Animal 的子类。
class Dog extends Animal {

}

通配符 ? 与 T 的区别

  • T是一个确定的类型,通常用于泛型类和泛型方法的定义。
  • ?是一不确定的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

通过 T 来确保泛型参数的一致性

1
2
3
4
5
// 通过 T 来确保泛型参数的一致性。
public <T extends Number> void test(List<T> dest, List<T> src);

// 通配符是不确定的,所以这个方法不能保证两个 List 具有相同的元素类型。
public void test(List<? extends Number> dest, List<? extends Number> src);

T 可以多重限定而 ? 不行

1
2
3
4
public class MultiLimit implements MultiLimitInterfaceA, MultiLimitInterfaceB {
public static<T extends MultiLimitInterfaceA &MultiLimitInterfaceB>void test(T t){
}
}
  • 使用&符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 MultiLimitInterfaceAMultiLimitInterfaceB 的共有子类型,此时变量 t 就具有了所有限定的方法和属性,对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。

? 可以使用超类限定而 T 不行

  • T 只具有一种类型限定方式:
1
T extends A
  • 但是通配符 ? 可以进行两种限定:
1
2
? extends A
? super A

Class<T>Class<?> 区别

  • Class<T> 在实例化的时候,T要替换成具体类。
  • Class<?> 它是个通配泛型,?可以代表任何类型,所以主要用于声明时的限制情况。
1
2
3
4
// 可以。
public Class<?> clazz;
// 不可以,因为 T 需要指定类型。
public Class<T> clazzT;
  • 所以当不知道定声明什么类型的 Class 的时候可以定义一个Class<?>
  • 那如果也想 public Class<T> clazzT; 这样的话,就必须让当前的类也指定T
1
2
3
4
5
public class Test3<T> {
public Class<?> clazz;
// 不会报错。
public Class<T> clazzT;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!