一、 泛型概述

1.1 泛型的定义

JDK1.5之后,Java引入了“类型参数化”的概念,表面意思是表示广泛的、普通的类型。Java的参数化类型被称为泛型。

1.2 如何使用泛型

泛型使用的是一对尖括号定义的,如:List 指定的就是在List集合中只能放String类型的数据。

接下来我们来一个对比评测,看一看使用泛型和不使用泛型的区别。

不使用泛型:

List list1 = new ArrayList();
list1.add("www.educoder.net");
String str1 = (String)list1.get(0);

使用泛型:

List<String> list2 = new ArrayList<String>();
list2.add("www.educoder.net");
String str2 = list.get(0);   

可以发现在这里我们不用强转数据类型了。

1.3 使用泛型的好处

  • 类型安全,通过知道使用泛型定义的变量的类型限制,编译器可以更有效地提高Java程序的类型安全;

  • 消除强制类型转换,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。所有的强制转换都是自动和隐式的;

  • 提高性能。

1.4 格式

<数据类型>

注:此处的数据类型只能是引用类型
例:

List<String> strList = new ArrayList<>();

泛型的字母规范
在定义泛型类时,任意一个大写字母都可以用于指定泛型的变量。

  • E–Element,常用在Java Collection里面,如List< E >, Set< E >
  • T–Type,类型,如String,Integer
  • K–Key,V–Value,代表Map键值对
  • N–Number,数字

二、泛型类

2.1 泛型类的定义

  (修饰符)  Class  类名  <泛型标识>{
  ...
  }

示例:

public class Cat<T> {
    private T info;
    public Cat(T info) {
        this.info = info;
    }
    public void setInfo(T info){
    	this.info = info;
    	}
    public T getInfo(){
    return this.info;
    }
}

2.2 易错点

注意:虚拟机没有泛型类对象——所有对象都属于普通类型 .

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)即不会生成Cat< String>.class,也不会生成Cat< Animal>.class,而是只会生成一个原型Cat.class.

注意: 泛型类的静态成员(静态变量、静态方法)不能使用该类的泛型参数

因为静态成员属于类级别,在类加载时初始化,而泛型类的< T>在创建对象时才确定,,两者生命周期不匹配。

  • 静态变量不能声明为static T info;
  • 静态方法不能直接使用类的< T> 作为返回值或参数,如static T eat(){…}是错误的。但可以自己声明独立的泛型参数,如:static < S> S eat(){…}。这种独立声明的泛型参数与类的泛型参数无关。

注意:编译器不允许创建泛型变量的数组。
即在创建数组实例时,数组的元素不能使用参数化的类型。

例如,下面语句有错误:
Vector< Integer> vectorList[] = new Vector< Integer>[10];

原因:Java 的数组是协变的(子类可以替换父类,比如 String[] 可以赋值给 Object[]),而泛型是不可协变的(List< String> 不能赋值给 List< Object>),两者特性冲突会导致类型安全被破坏。

2.3 泛型类的继承规则

参数化类型不考虑类型参数的继承关系

如:String是Object的子类
Vector< String> v = new Vector< Object>();
// 错误!不写< Object>没错,写了就是明知故犯
Vector< Object> v = new Vector< String>();
// 也错误

再如:Manager是Employee的子类,但是Pair< Manager>不是Pair< Employee>的子类

三、泛型方法

一个方法是不是泛型的,与它所在类是不是泛型没有什么关系。

3.1 定义泛型方法

修饰符 <T,S> 返回值类型 方法名(形参列表){
...//方法体
}

示例:

public <T> void swap(DynamicArray<T>arr,int i,int j){
	T tmp = arr.get(i);
	arr.set(i,arr.get(j));
	arr.set(j,tmp);
}

3.2 泛型参数的类型推断

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

  1. 与类、接口使用泛型类型不同的是,方法中的的泛型参数无须显示传入实际类型参数,编译器会根据实参推断类型参数的值。
    因为编译器根据实参类型推断出泛型所代表的类型,它通常推断出最直接的类型。

示例:

public class testGenericMethod{
	static<T> void fromArrayToCollection(T[] a,Collection<T> c){
		for(T obj : a){
			c.add(obj);
		}
	}
	public static void main(String args[]){
		Object[] o = new Object[20];
		Collection<Object> co = new ArrayList<Object>();
		fromArrayToCollection(o,co);
	}
}

编译器自动推断出T是Object类型,Collection< T>是Collection< Object>,无需手动指定泛型类型fromArrayToCollection< Object>(o, co)。

  1. 在指定了类型参数时,传入的实参数据类型必须是指定数据类型或者其子类,否则编译器无法准确推断出泛型类型。

示例:(反面示例)

public class TestMethod{
	static <T> void copy(Collection<T> from, Collection<T> to){
   	    for(T obj : from){
   	    		to.add(obj);
   	    }
    }
	public static void main(String args[]){
   		Collection<Object> co = new ArrayList<Object>();
   		Collection<String> cs = new ArrayList<String>();
   		copy(co,cs);
   	}
}

copy(co,cs);将产生编译错误
由2.3泛型的继承规则可知,Collection< Object>和Collection< String>无继承关系,不满足 “指定类型参数时,传入的实参数据类型必须是指定数据类型或者其子类” 的条件。

  • 若根据实参推断 T=Object,则 to 参数需要 Collection< Object>或者其子类,但实际是 Collection< String>(不匹配);
  • 若推断 T=String,则 from 参数需要 Collection< String>或者其子类,但实际是 Collection< Object>(不匹配)。

四、泛型接口

4.1 格式

修饰符 interface 接口名 <类型>{
}

示例:

public interface List< E>{
...
}

4.2 实现方式

  1. 明确指定具体类型(常用)
    实现接口时,直接将泛型参数< T>替换为具体类型(如String,Circle…),接口中的T会被自动替换为该类型。

示例:

class Circle implements Comparable<Circle> {                        
    private double radius;
    public Circle(double radius) {
      this.radius = radius;
    }
    @Override
    public int compareTo(Circle c) {                                         
      if (radius < c.radius) 
        return -1;
      else if (radius == c.radius)
        return 0;
      else
        return 1;
    }
}
  1. 保留泛型参数,创建对象时再确定
    实现接口时不指定具体类型,而是继续使用泛型参数(如< T>)。
	public class MyArrayList<E> implements List<E> {
	...
	}

创建对象时再确定类型:

public class GenericsDemo{
	public static void main(String[] args){
		MyArrayList<String>list = new MyArrayList<>();
		list.add("hello");
	}
}

五、类型擦除

5.1 什么是类型擦除

编译器可使用泛型信息,但是这些泛型信息在运行时是不可使用的,这被称为类型擦除。

泛型是使用一种类型擦除的方法来实现的。

5.2 原理

泛型是提供给javac编译器使用的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入。泛型信息仅在编译期用于类型检查,运行期会被擦除为原始类型(如 List< String> 擦除为 List),以保证与旧版本 Java 的兼容性并避免性能损耗。因此,对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样。

注意:由于系统中并不会真正生成泛型类,所以instanceof运算符后面不能使用泛型类。

示例:(反面示例)

Collection c=new ArrayList< String>();
if(c  instanceof List< String>){
	...
}

List< String>不是一个类,所以这是错误的。

六、类型通配符 <?>

当我们为方法中传入一个集合形参,而集合形参的类型不确定,又需要满足“指定类型参数时,传入的实参数据类型必须是指定数据类型或者其子类”的条件时,为了表示各种泛型的父类,我们需要使用类型通配符。
”被称为通配符,它的元素类型可以匹配任何类型。

6.1 无界通配符

定义:
? 表示任意引用类型
特点:

  1. 通用性最强,可以接收任何类型的泛型对象。
  2. 只能从集合中读取元素,不能喜爱那个集合中添加元素(null除外),即只能add(null)。
    原因:编译器无法确定元素类型,避免类型混乱。

示例:

	public void printItem(List<?> c){
		for(int i = 0; i < c.size(); i++){
			System.out.println(c.get(i));
		}
	}

List<?>和List< Object>的区别

  • List<?>: ?代表可以是任意类型。List<?>是List接口各种泛型的父类,任何引用类型的List都可以传入,如List< Object>,List< String>,List< Manager>等。
  • List< Object>: 是指明具体元素类型的泛型集合,只可以在其中添加元素为Object的List,不能接收其他类型的List(参数化类型不考虑类型参数的继承关系)

关系图如下:(箭头表示继承关系,箭头所指是父类,raw是原型)
在这里插入图片描述
注意:通配符不是类型变量,因此不能在编写代码时使用“?”作为一种类型
例如: ? t = p.get(i); 是错误的。

注意: 可以通过调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但肯定是一个Object。

6.2 上界通配符

通过extends关键字可以让参数为给定的上界类型或其子类型。这个上界可以是某个具体的类或者某个具体的接口。指定通配符上限就是为了支持类型协变。

? extends 父类代表“父类及其所有子类类型”。如 List<? extends Number> 表示 “元素类型是 Number 或其子类(Integer、Double 等)的 List 集合”。

特点

  1. 范围受限:只能接收 “父类及子类” 类型的泛型对象(比无界通配符更具体);
  2. 只读不写:通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(null除外)。

示例:

public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number num : list) { 
        total += num.doubleValue();
    }
    return total;
}
public static void main(String[] args) {
    List<Integer> intList = Arrays.asList(1, 2, 3);
    List<Double> doubleList = Arrays.asList(1.5, 2.5);
    System.out.println(sum(intList));  
    System.out.println(sum(doubleList));  4.0
    List<String> strList = Arrays.asList("1", "2");
    // sum(strList); // 编译报错:String 不是 Number 的子类
}

在java继承中,接口是多继承,类是单继承的,则限定列表中可以有多个接口类型,但是至多有一个具体类。
如果用一个具体类作为限定,则这个具体类必须是限定列表中的第一个。

例如:
T extends Comparable&Serializable
T extends Number&ComParable&Serializable

6.3 下界通配符

指定通配符的下限是为了支持类型逆变。逆变用于灵活写入,不能被类型参数代替

? super 子类 代表 “子类及其所有父类类型”,如 List<? super Integer> 表示 “元素类型是 Integer 或其父类(Number、Object 等)的 List 集合”。

示例:

public void copyTo(DynamicArray<? super E> dest){                
    for(int i =0 ;i<size;i++){
        dest.add(get(i));
    }
}
public static void main(String[] args) {
        DynamicArray<Number> numbers = new DynamicArray<>();
        DynamicArray<Integer> ints = new DynamicArray<>();
        ints.add(100);
        ints.add(34);
        ints.copyTo(numbers);
}

6.4 通配符VS类型参数

  • 通配符能覆盖大部分泛型类型匹配场景。在方法中采用类型参数是泛型方法,采用通配符依旧是普通方法,更加简洁,可读性更好。
  • 如果需要处理类型参数之间的依赖关系、返回值与输入类型关联、自由写操作,或定义泛型接口,类、静态泛型方法时,则只能用类型参数。
  • 通配符侧重 “使用泛型的灵活性”,类型参数侧重 “定义泛型的可控性”。

七、总结

7.1 Java 泛型中通配符与具体类型的继承关系示意图

在这里插入图片描述

7.2 补充:协变与逆变概念示意图

类型之间的赋值兼容性方向:
在这里插入图片描述

本文围绕Java泛型展开,核心讲解了JDK1.5引入的泛型(类型参数化)机制,包括其定义、使用好处(类型安全、消除强转、提升性能);详细介绍了泛型类、泛型方法、泛型接口的定义、语法、实现方式及易错点;阐述了类型擦除的原理与限制;重点解析了无界、上界、下界通配符的特性、用法及与类型参数的区别;还说明了泛型关键规则(如参数化类型无继承关系),帮助开发者全面掌握泛型技术以提升代码质量。

感谢阅读!若文中有错误,欢迎评论区指正,共同进步~

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐