泛型支持在定义类、接口和方法时将类型作为参数。泛型参数与方法声明中使用的形式参数非常相似,泛型参数使我们可以在不同的输入中重用相同的代码。但与形式参数不同之处在于,形式参数的输入是值,而类型参数的输入是类型。
我们可以将泛型 < T > 理解为占位符,在定义类时不清楚它会是什么类型,但是类中的代码逻辑是通用的,无论类在使用时< T >会被什么参数类型替代。
比如我们经常使用的List集合,在创建时通常都要指定其类型:
//指定泛型类型为String
List<String> stringList = new ArrayList<>();
stringList.add("hello");
stringList.add("world");
//试图添加非String类型元素,编译会报错
stringList.add(100);
当然我们也可以不指定集合泛型E,但是它会带来隐患:
//未指定元素泛型类型,可以存储任何object类型
List frulist = new ArrayList();
frulist .add("apple");
frulist .add("banana");
//隐患1:可以加入其它类型元素
frulist.add(100);
//隐患2:取出元素时,必须进行类型转换,容易出错
String str = (String)frulist.get(0);
上面已经提到,泛型支持在定义类、接口和方法时将类型作为参数。下面我们通过例子来看下具体泛型具体使用方式。
泛型类的定义方式如下,类型参数 T由< >包裹,紧跟在类名之后,类型参数可以有多个,以英文逗号分割。
class name<T1, T2, ..., Tn> {
/* ... */
}
知道了泛型类的格式,我们来具体实践下,先定义一个非泛型的类Box,它只有一个Object类型成员变量,同时提供简单的set\get方法
class Box{
private Object content;
public void set(Object object) {
this.content = object;
}
public Object get() {
return content;
}
}
Box类的成员属性content是Object类型,所以我们可以自由的存放任何类型数据,在使用时可能会像下面这样:
public static void main(String[] args) {
//创建一个box类,来存放String类型数据
Box box = new Box();
box.set("hello world");
//取值时,都要进行强制类型转换
String content = (String) box.get();
//.....很多行代码后,不小心存了boolean类型
box.set(false);
//...很多行代码后,又一个疏忽,带来了ClassCastException
Integer count = (Integer) box.get();
}
可以看到,在使用非泛型的Box类时,虽然存放的元素类型非常自由,但也存在很多严重问题,比如我们创建Box类对象,本来是想存放String类型数据,却可能不小心使用box的set()方法存了boolean类型,另外每次使用box.get()取值时,都要进行强制类型转换,很容易遇见java.lang.ClassCastException
这时候我们就可以使用泛型类了,对Box类进行改造,在类的声明时加入泛型参数 < T >,然后在Box类内部就可以使用泛型参数 T 代替原来的Object,set\get方法所使用的参数类型,也都使用 T 来代替,此时我们的Box类就是泛型类了。
class Box <T> {
private T content;
public void set(T object) {
this.content = object;
}
public T get() {
return content;
}
}
这时我们在使用泛型类Box时,指定类型参数< T >的实际类型即可
public static void main(String[] args) {
//创建Box类时,指定泛型参数为String
Box<String> box = new Box();
box.set("hello world");
//由于指定了泛型,不需要在进行强制类型转换
String content = box.get();
//不小心存了boolean类型,IDE在编译时会报错
box.set(false);
}
到了这里你是否联想到,我们经常使用的集合List< T >,Map < K,V>,例如HashMap的源码中类声明部分:
//HashMap类源码
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//........../
}
泛型方法是引入自己的类型参数的方法。类似于声明泛型类型,但是类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。
泛型方法的语法包括尖括号< >内的类型参数列表,该列表出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。
泛型方法长什么样子呢,下面看个例子:
/**
*泛型类Pair,用于创建key-value类型键值对
*类似与JDK中Map内部Entry
*/
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
public class Util {
/**
* 泛型方法compare
* 泛型参数列表出现在必须位于方法的返回值之前。
* 泛型参数在声明后,才能在方法内部使用
* 泛型类中的返回值为T的方法不是泛型方法
**/
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
/**
*泛型方法,
*计算数组T[]中大于指定元素elem的元素数量
* > 是泛型的继承,extends 限定了方法
* 中使用的必须是Comparable的子类,这样才能在方法里使用compareTo方法
*/
public static <T extends Comparable<T> > int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
//使用泛型方法,并指定参数类型
boolean same = Util.<Integer, String>compare(p1, p2);
}
}
按照惯例,类型参数名称是单个大写字母。便于区分类型变量和普通类或接口名称。最常用的类型参数名有:
以上是官方推荐的几种,除此之外,还可以使用S、U等等
在泛型里代码中,使用 > 作为通配符,表示一个未知的类型。
那为什么要使用通配符呢?主要是因为在java中,数组是可以协变的,比如Cat extends Animal,那么Animal[] 与Cat[]是兼容的。而集合是不能协变的,也就是说List < Animal >不是List< Cat >的父类,二者不能相互赋值,这就导致了一个逻辑上问——能够存放父类Animal元素的集合,却不能存放它的子类Cat。
abstract class Animal {
public abstract void eat();
}
class Cat extends Animal{
@Override
public void eat() {}
}
public class TestC{
public static void main(String[] args) {
//Animal是Cat父类,数组可以赋值
Animal[] animal = new Cat[5];
//Animal是Cat父类,但是不意味着List集合是List父类,
List<Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
//下面这行代码会编译失败,编译器无法推断出List是List的子类
animals = cats; // incompatible types
}
}
为了解决上面描述的问题,泛型的通配符就派上用途了。泛型通配符分为三种类型:
无边界配符(Unbounded Wildcards)
< ? >就是无边界通配符,比如List> list
表示持有某种特定类型对象的List,但是不知道是哪种类型,所以不能add任何类型的对象。它与List list
并不相同,List list
是表示持有Object类型对象的List,可以add任何类型的对象。
上边界限定的通配符(Upper Bounded wildcards)
extends E>, E指是就是该泛型的上边界,表示泛型的类型只能是E类或者E类的子类 ,这里虽然用的是extends关键字, 却不仅限于继承了E的子类, 也可以代指接口E的实现类。 public static void main(String[] args) {
// extends Animal>限定了泛型类只能是Animal或其子类对象的List
List<? extends Animal> animals = new ArrayList<>();
List<Cat> cats = new ArrayList<>();
//下面代码不会报错,二者引用可以赋值
animals = cats;
//但是集合中无法add元素,因为无法确定持有的实际类型,
animals.add(new Cat());//error
//从集合中获取对象是是可以的,因为在这个List中,不管实际类型是什么,肯定都能转型为Animal
Animal animal = animals.get(0);
}
下边界限定的通配符(Lower Bounded wildcards)
super E>,表示泛型的类型只能是E类或者E类的父类,List super Integer> list表示某种特定类型(Integer或者Integer的父类)对象的List。可以确定这个List持有的对象类型肯定是Integer或者其父类。 //某种特定类型(Integer或者Integer的父类)对象的List
List<? super Integer> list = new ArrayList<>();
//往list里面add一个Integer或者其子类的对象是安全的,
//因为Integer或者其子类的对象,都可以向上转型为Integer的父类对象
list.add(new Integer(1));
//下面代码编译错误,因为无法确定实际类型,所以往list里面add一个Integer的父类对象是不被允许的
list.add(new Object());
所以从上面上边界限定的通配符和下边界限定的通配符的特性,可以知道:
Java 中的的泛型是伪泛型,这是因为泛型信息只存在于代码编译阶段,编译后与泛型相关的信息会被擦除掉,称为类型擦除(type erasure)。
编译器在编译期,会将泛型转化为原生类型。并在相应的地方插入强制转型的代码。什么是原生类型呢?原生类型就是删去类型参数后泛型类的类型名,比如:
List<String> stringList = new ArrayList<String>();
List<Integer> integerList = new ArrayList<Integer>();
//下面的结果为true,因为类型擦除后,二者原生类型都是List
System.out.println(stringList.getClass() == integerList.getClass());
如果泛型参数中,有限定符则会使用 第一个限定符的类型来替换,比如
class Box <T extends Number> {
private T content;
}
类型擦除后的原生类型变为其限定符类型:
class Box{
private Number content;
}
泛型所涵盖内容不止这么多,希望通过本文对你了解JAVA中泛型有所帮助。