定义
泛型可以用一句话来概括:类型参数化,即将类型看作一个参数。它的出现可以让我们不用预先去考虑我们需要什么类型的数据,而是在实际使用时将该类型作为一种参数传进去。核心概念就是告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
其基本写法就是使用一对尖括号,中间包含标识符,其允许在定义类、接口时通过一个标识符表示一种特定的类型。
//这个holder类只能存放integer类型的数据
//当我们需要存放string类型的数据时,我们需要再写一个holder类
class holder{
private Intger a;
public holder(Integer a){ this.a = a; }
Interger get(){ return a; }
}
//在泛型出现之前,为了让holder类存放不同类型的数据只能使用向上向下转型技术
class objectHolder{
private Object a;
public holder(Object a){ this.a = a; }
Object get(){ return a; }
}
//使用了泛型之后,我们可以用一个holder类存放任何类型的数据。
//在创建了一个holder的实例之后,编译器会自动帮你负责转型操作,并且负责转型操作的正确性。
class holderWithGenericity<T> {
private T a;
public holderWithGenericity(T a){ this.a = a; }
T get(){ return a; }
}
//holder无法存放string数据
//objectHolder需要向上向下转型
objectHolder holder2 = new objectHolder("Hello, genericity");//自动向上转型
String string1 = (String)h.get();//需要显式向下转型
//泛型的转型操作由编译器完成并且保证类型的正确性
//我们可以将实际使用的类型String作为一个参数写入到<>中,从而能够复用holderWithGenericity类
holderWithGenericity<String> h = new holderWithGenericity<String>("Hello, genericity");
String string2 = h.get();//不需要转型
出现背景
在一些特定场景中,我们无法提前预知我们所需要的数据类型,在没有泛型的时候,通过使用Object
作为参数能实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而且这种转换可能会出现类型转换错误,这种错误在编译期间不会报错,只有在运行时才会抛出异常,非常不方面。
而泛型能够通过<>
将类型作为一种参数传进去,由编译器来帮我们进行隐式地类型转换,同时在编译的时候就能检查类型安全。
专业术语
- 泛型参数:<>里面的东西,泛型参数可以是确定的类型参数也可以是不确定的通配符。
- 类型参数:T, K, V等等占位符,这些占位符本身是无意义的,就是一个符号而已,可以在方法体中直接用(就把他们当作形参一样来用)。
- 上届通配符:<? extends Base>,申明通配符是某个特定类的任何导出类来限定,即可以持有任何
Base类的子类
类型的对象。 - 超类型通配符:<? super Child >,声明通配符是由某个特定类的任何基类来限定,即可以持有任何
Child类的父类
类型的对象。
使用场景
泛型类
泛型可以适用于类的定义中,这个类被称为泛型类,最典型的就是各种容器类,如:List, Set, Map等等。通过这样的方法,我们可以完成一个泛型类能够处理多种类型即一组类的操作对外开放相同的接口(不用为了每个类型都编写一个特定的对外开放的接口)。
写法:在类的定义中类名之后加上<>
,中间写入通配符
//在类的定义时类名后面加上<>,中间放入通配符
public class GenericClass<T>{
private T variable;
public GenericClass(T var){
this.variable = var;
}
public T get(){return key; }
}
//在实例化泛型类时,必须指定T的类型。
//JDK1.7之后,第二个<>中不需要再写具体的类型了,可以少写一个类型参数。
GenericClass<String> genericClass = new GenericClass<>("hello");
//arrayList实例化
ArrayList<String> stringArrays = new ArrayList<>();
泛型接口
泛型也可以用于接口中,其具体用法和泛型类没有区别,只是将泛型这种概念应用到接口上去。
//写法与泛型类一样,在接口名后加<>,中间写入通配符
public interface Generator<T>{
public T next();
}
//使用时(implement时)需要在<>内给出具体类型实参
public class NumGenerator implements Generator<Integer>{
int[] args = {18, 19, 20};
@Override
public Integer next(){
Random rand = new Random();
return args[rand.nextInt(3)];
}
public static void main(String[] args){
NumGenerator numGen = new NumGenerator();
int rand = numGen.next();
}
}
泛型方法
除此之外,我们还可以将泛型应用在类中的某个方法上,类本身可以是泛型的,也可以不是。
泛型方法独立于类而改变方法。作为准则,尽量使用泛型方法而不是泛型类,泛型类中某一个方法要比泛型化整个类要简单易懂的多。
一个static
的方法无法访问其属于的类中的泛型参数,具体原因会在底层原理中解释。所以如果一个类无论是本身自带泛型参数,还是说想用类的泛型参数,其必须是一个泛型方法。
写法:在返回值之前加上<>
,并在其中填入通配符,在方法的形参中给出类型形参 形参
。通过在返回值前面加一个来申明这是一个泛型方法,持有一个泛型T,然后才可以在形参列表中使用泛型T。
public class GenericMethods {
//类型通配符为T,方法的形参类型为T,形参名为x。
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f(""); //Output: java.lang.String
gm.f(1); //Output: java.lang.Integer
gm.f(1.0); //Output: java.lang.Double
gm.f(1.0F); //Output: java.lang.Float
gm.f('c'); //Output: java.lang.Character
gm.f(gm); //Output: GenericMethods
}
}
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。这称为类型参数推断。
待解决问题:static和泛型的关系
类型参数标识符
在阅读源码的过程中,我们经常会遇见各种不同的通配符写法,比如 T,E,K,V,? 等等。
上界通配符
上界通配符<? extends T>
:适用于T类型或任何T类型的子类型。
(其中T可以是一个泛型类型参数,也可以是一个具体的类型)
- 用于泛型方法中
下界通配符
无界通配符
泛型边界
底层原理
Java泛型是使用擦除实现的。当使用泛型时,任何具体的类型信息都被擦除了,List<String>
和List<Integer>
最终都被擦除为List<Object>
。其在编译期间,把某一个具体类型强制转换为类型参数,并进行类型安全检查,但实质上来说,运行时这些具体的类型都会被擦除掉。具体来说就是,泛型的所有动作都发生在边界处--------对入参的编译检查和对返回值的转型。
- 比如一个类 其类型参数为T,那么在实例化时假定给的类型实参为String。那么泛型所做的事就是在每次调用该泛型类(入参)时检查是否为String,在返回时自动转型为String,在整个运行过程中无法获得类型信息(String)。在运行过程中,String都被擦除为Object。
擦除式的泛型实现机制使得我们无法获取类型参数的类型信息,但我们可以使用边界对泛型使用的参数类型进行约束。边界最重要的作用是:我们可以在泛型过程中调用那些边界的方法。
-
extends
关键字限定下界。
<T extends Base>
:类型参数的实参可以是Base及其子类。
用途:- 可以在方法体中写T调用Base的方法(编译器允许这种做法,因为类型擦除只擦除到了T,并没有擦去Base,所以可以直接写T调用Base的方法)
- 增加易扩展性
-
super
关键字限定上界。
<T super Derived>
:类型参数的实参可以是Derived及其父类。
局限性
-
基本类型无法作为类型参数