Java 泛型

一、泛型概述

1. 什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参列表,普通方法的形参列表中,每个形参的数据类型是确定的,而变量是一个参数。在调用普通方法时需要传入对应形参数据类型的变量(实参),若传入的实参与形参定义的数据类型不匹配,则会报错。

泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2. 泛型使用场景

在 ArrayList 集合中,可以放入所有类型的对象,假设现在需要一个只存储了 String 类型对象的 ArrayList 集合。

代码如下:

@ Test
public void test() {
    ArrayList list = new ArrayList();
    list.add("aaa");
    list.add("bbb");
    list.add("ccc");
    for (int i = 0; i < list.size(); i++) {
        System.out.println((String)list.get(i));
    }
}
  • 上面代码没有任何问题,在遍历 ArrayList 集合时,只需将 Object 对象进行向下转型成 String 类型即可得到 String 类型对象。

但如果在添加 String 对象时,不小心添加了一个 Integer 对象,会发生什么?看下面代码:

@ Test
public void test() {
    ArrayList list = new ArrayList();
    list.add("aaa");
    list.add("bbb");
    list.add("ccc");
    list.add(111);
    for (int i = 0; i < list.size(); i++) {
        System.out.println((String)list.get(i));
    }
}
  • 上述代码在编译时没有报错,但在运行时却抛出了一个 ClassCastException 异常,其原因是 Integer 对象不能强转为 String 类型。

那如何可以避免上述异常的出现?即我们希望当我们向集合中添加了不符合类型要求的对象时,编译器能直接给我们报错,而不是在程序运行后才产生异常。这个时候便可以使用泛型了。

使用泛型代码如下:

@ Test
public void test() {
    ArrayList list = new ArrayList<>();
    list.add("aaa");
    list.add("bbb");
    list.add("ccc");
    list.add(111);// 在编译阶段,编译器会报错
    for (int i = 0; i < list.size(); i++) {
        System.out.println((String)list.get(i));
    }
}

< String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,当添加一个非 String 对象时,编译器会直接报错。这样,我们便解决了上面产生的 ClassCastException 异常的问题(这样体现了泛型的类型安全检测机制)

3. 泛型概述小结

  1. 当具体的数据类型确定后,泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。所以说,泛型一定程度上提高了软件的安全性,防止出现低级的失误。
  2. 泛型提高了程序代码的可读性。在定义泛型阶段(类、接口、方法)或者对象实例化阶段,由于 < 类型参数 > 需要在代码中显式地编写,所以程序员能够快速猜测出代码所要操作的数据类型,提高了代码可读性。

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

二、泛型类

1. 泛型类的定义

(1)类型参数用于类的定义中,则该类被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map等。

泛型类的基本语法如下:

class 类名称 <泛型标识> {
  private 泛型标识 /*(成员变量类型)*/ 变量名; 
  .....

  }
}
  • 尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。

泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:

  T :代表一般的任何类。
  E :代表 Element 元素的意思,或者 Exception 异常的意思。
  K :代表 Key 的意思。
  V :代表 Value 的意思,通常与 K 一起配合使用。
  S :代表 Subtype 的意思,文章后面部分会讲解示意。

举例如下:

public class Generic { 
    // key 这个成员变量的数据类型为 T, T 的类型由外部传入  
    private T key;

	// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
    public Generic(T key) { 
        this.key = key;
    }
    
	// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
    public T getKey(){ 
        return key;
    }
}
  • 在泛型类中,类型参数定义的位置有三处,分别为: 
1.非静态的成员属性类型

2.非静态方法的形参类型(包括非静态成员方法和构造器)

3.非静态的成员方法的返回值类型

(2)泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数

代码如下:

public class Test {    
    public static T one;   // 编译错误    
    public static T show(T one){ // 编译错误    
        return null;    
    }    
}  
  •  泛型类中的类型参数的确定是在创建泛型类对象的时候(例如 ArrayList< Integer >)。
  •  而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的。

(3)静态泛型方法中可以使用自身的方法签名中新定义的类型参数(即泛型方法,后面会说到),而不能使用泛型类中定义的类型参数。

代码如下:

public class Test2 {   
	// 泛型类定义的类型参数 T 不能在静态方法中使用  
    public static  E show(E one){ // 这是正确的,因为 E 是在静态方法签名中新定义的类型参数    
        return null;    
    }    
}  

(4)泛型类不只接受一个类型参数,它还可以接受多个类型参数。

代码如下:

public class MultiType  {
	E value1;
	T value2;
	
	public E getValue1(){
		return value1;
	}
	
	public T getValue2(){
		return value2;
	}
}

2. 泛型类的使用 

在创建泛型类的对象时,必须指定类型参数 T 的具体数据类型,即尖括号 <> 中传入的什么数据类型,T 便会被替换成对应的类型。如果 <> 中什么都不传入,则默认是 < Object >。

假设有个泛型类如下:

public class Generic { 
 
    private T key;

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

    public T getKey(){ 
        return key;
    }
}

当创建一个 Generic< T > 类对象时,会向尖括号 <> 中传入具体的数据类型。 

代码如下:

@ Test
public void test() {
	Generic generic = new Generic<>();// 传入 String 类型
	
	// <> 中什么都不传入,等价于 Generic generic = new Generic<>();
	Generic generic = new Generic();
}
 
  

传入 String 类型时,原泛型类可以想象它会自动扩展,其类型参数会被替换。 

扩展如下: 

public class Generic { 
 
    private String key;

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

    public String getKey() { 
        return key;
    }
}
  • 可以发现,泛型类中的类型参数 T 被 <> 中的 String 类型全部替换了。
  • 使用泛型的上述特性便可以在集合中限制添加对象的数据类型,若集合中添加的对象与指定的泛型数据类型不一致,则编译器会直接报错,这也是泛型的类型安全检测机制的实现原理。

 三、泛型方法

1. 泛型方法的定义

当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数

基本语法如下:

public <类型参数> 返回类型 方法名(类型参数 变量名) {
    ...
}

(1)只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。

举例如下:

public class Test {
	// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
	public void testMethod(U u){
		System.out.println(u);
	}
	
	//  真正声明了下面的方法是一个泛型方法
	public  T testMethod1(T t){
		return t;
	}
}

(2)泛型方法中可以同时声明多个类型参数。

举例如下:

public class TestMethod {
	public  T testMethod(T t, S s) {
		return null;
	}
}

(3)泛型方法中也可以使用泛型类中定义的泛型参数。

举例如下:

public class TestMethod {
	public  U testMethod(T t, U u) {
		return u;
	}
}

(4)特别注意的是:泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,它们一点关系都没有。

举例如下:

public class Test {
	public void testMethod(T t) {
		System.out.println(t);
	}
	
	public  T testMethod1(T t) {
		return t;
	}
}

上面代码中,Test< T > 是泛型类,testMethod() 是泛型类中的普通方法,其使用的类型参数是与泛型类中定义的类型参数。
而 testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数。
虽然泛型类中定义的类型参数标识和泛型方法中定义的类型参数标识都为< T >,但它们彼此之间是相互独立的。也就是说,泛型方法始终以自己声明的类型参数为准。

注意事项:

  1. < T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。
  2. 为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
  3. 与泛型类的类型参数定义一样,此处泛型方法中的 T 可以写为`任意标识`,常见的如 T、E、K、V 等形式的参数常用于表示泛型。

前面在泛型类的定义中提到,在静态成员中不能使用泛型类定义的类型参数,但我们可以将静态成员方法定义为一个泛型方法。

代码如下:

public class Test2 {   
	// 泛型类定义的类型参数 T 不能在静态方法中使用
	// 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
    public static  E show(E one) {     
        return null;    
    }    
}  

 2. 泛型方法的使用

泛型类,在创建类的对象的时候确定类型参数的具体类型;
泛型方法,在调用方法的时候再确定类型参数的具体类型。

泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
当调用泛型方法时,根据外部传入的实际对象的数据类型,编译器就可以判断出类型参数 T所代表的具体数据类型。

举例如下:

public class Demo {  
  public static void main(String args[]) {  
    GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象  
    
    String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串  
    int i = d.fun(30);  // 给GenericMethod中的泛型方法传递数字,自动装箱  
    System.out.println(str); // 输出 汤姆
    System.out.println(i);  // 输出 30

	GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin
  }  
}

class GenericMethod {
	// 普通的泛型方法
	public  T fun(T t) { // 可以接收任意类型的数据  
    	return t;
  	} 

	// 静态的泛型方法
	public static  void show(E one){     
         System.out.println("静态泛型方法 " + one);
    }
}  
  • 不难发现,当调用泛型方法时,根据传入的实际对象,编译器会判断出类型形参 T 所代表的具体数据类型。

3. 泛型方法中的类型推断

在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。

当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。
在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。

举例如下:

public class Test {

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

    public static void main(String[] args) {  
        // 一、不显式地指定类型参数
        //(1)传入的两个实参都是 Integer,所以泛型方法中的 ==  
        int i = Test.add(1, 2);
        
        //(2)传入的两个实参一个是 Integer,另一个是 Float,
        // 所以取共同父类的最小级, == 
		Number f = Test.add(1, 1.2);

		// 传入的两个实参一个是 Integer,另一个是 String,
		// 所以取共同父类的最小级, == 
        Object o = Test.add(1, "asd");
  
        // 二、显式地指定类型参数
        //(1)指定了 = ,所以传入的实参只能为 Integer 对象    
        int a = Test.add(1, 2);
		
		//(2)指定了 = ,所以不能传入 Float 对象
        int b = Test.add(1, 2.2);// 编译错误
        
        //(3)指定 = ,所以可以传入 Number 对象
        // Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
        Number c = Test.add(1, 2.2); 
    }  
}
 
  

 

 

 

 

你可能感兴趣的:(java,开发语言)