概念
所谓泛型,通俗地讲就是通过占位符的方式声明抽象的类型,然后在编译期告诉编译器具体传入的类型。(我们只需要在使用的时候定义好具体的类型)。
demo
定义实体类
public class Customer {
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
(1)jdk1.5之前(没有泛型) :在jdk1.5之前,如果想要通过通用的模板设置不同类型的参数,常用的方式就是将参数声明成Object类型,案例代码如下:
public class Holder1 {
public Object object;
public Holder1(Object object) {
this.object = object;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
public static void main(String[] args) {
//set Customer
Holder1 holder1 = new Holder1(new Customer());
Customer customer = (Customer) holder1.getObject();
//set other type
holder1.setObject("object");
String str = (String) holder1.getObject();
}
}
通过上面的代码可以看见,Holder1类型通过声明不同类型set可以传入不同的传输,但是在取值的时候需要进行类型的转换。
(2)jdk1.5后(存在泛型) :当有了泛型之后不需要每次都声明具体的参数类型,通过泛型的语法,我们只需要在使用的时候声明好类型就解决了上面的问题,案例代码如下:
public class Holder2<T> {
public T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String[] args) {
//set Customer
Holder2<Customer> holder2 = new Holder2();
holder2.setT(new Customer());
//set String
Holder2<String> holder21 = new Holder2<>();
holder21.setT("object");
}
}
通过泛型的声明,只需要在使用Holder2类的指定好具体的类型编译器就可以自己识别,并且在get是时候需要在指定返回的类型。
泛型接口
泛型支持接口,具体做法如下:首先定义需要用到的类
public class Fruit {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Apple extends Fruit {
public Apple() {
}
}
public class Pear extends Fruit {
public Pear() {
}
}
接下来定义泛型接口
public interface Generator<T> {
T generate();
}
实现类
public class AppleGenerator implements Generator<Apple> {
@Override
public Apple generate() {
return new Apple();
}
}
可以看见Generotor接口定义了泛型,在创建具体实现对象的时候指定好生产的类就可以避免数据的转换。
语法:当我们使用泛型接口的时候,我们需要将泛型参数放在接口名的后面,并且用尖括号包住。
泛型方法
泛型方法可以独立于类存在,也就是说不管所在类是不是泛型类,都可以定义泛型方法。使用方式如下:
public class GenericMethod {
public <T> void generate(T t) {
System.out.println(t.getClass().getName());
}
public static void main(String[] args) {
GenericMethod method = new GenericMethod();
method.generate("hello");
method.generate(1L);
method.generate(1);
method.generate(true);
}
}
语法:当我们使用泛型方法的时候,我们需要将泛型参数放在返回值的前面,并且用尖括号包住。
泛型方法对可变参数的支持
泛型方法中也支持对可变参数的泛型化,使用方式如下:
public class GenericVarargs {
public static <T> List<T> produceList(T... args) {
List<T> list = Lists.newArrayList();
for (T t : args) {
list.add(t);
}
return list;
}
public static void main(String[] args) {
List<Integer> integerList = produceList(1, 2, 3, 4, 5, 6, 7, 8, 9);
System.out.println(integerList);
}
}
可以看见,上面的例子定义了带有可变参数的泛型方法。在使用的时候,我们可以指定可变参数的类型然后生成指定类型的集合。通过这种方式我们可以很容易地创建我们想要的集合。
匿名内部类对泛型的支持
匿名内部类中也可以使用泛型,使用方式和泛型类类似,案例代码如下:
public class GenericAnonymous {
public static Generator<Apple> generator() {
return new Generator<Apple>() {
@Override
public Apple generate() {
return new Apple();
}
};
}
public static void main(String[] args) {
Apple apple = GenericAnonymous.generator().generate();
}
}
泛型擦除
1. 首先让我们看一个例子,代码如下:
public class GenericErasureBootstrap {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
System.out.println(stringList.getClass() == integerList.getClass());
}
}
可能以我们第一感觉会输出false,但事实上是输出的true。因为对于Java来说,List和List是同一个类型,这是Java内部的泛型擦除机制让这种情况得以发生,接下来我会详细地讲解什么是泛型擦除。
2. 泛型参数信息:在Java中要想获取到泛型参数信息可以通过如下的方式:
public static void getGenericType() {
List<String> list = new ArrayList<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
}
输出的结果是==[E]==,可以看见只会拿到声明的占位符的信息,并没有具体的String类型的信息。这样就不难理解为什么上面声明的两个不同泛型类型的集合会被认为是同一个类型,因为对Java来说,他们都是List类型。
3. 泛型存在的问题:同样我们先看一个例子:
public class Computer {
public void inspect() {
}
}
public class Calculate<T> {
private T t;
public Calculate(T t) {
this.t = t;
}
public void cal() {
//error Cannot resolve method inspect()
t.inspect();
}
public static void main(String[] args) {
Calculate<Computer> calculate = new Calculate<>(new Computer());
calculate.cal();
}
}
上面的例子编译就不会通过,在编译器Java并不知道T到底是什么类型。(这也可以认识是Java泛型的一种缺陷),解决方式如下:
public class Calculate<T extends Computer> {
...
}
通过将泛型参数声明为T extends Computer,就可以保证参数类型是Computer的子类,这样编译就能够通过。
4. 为什么需要泛型参数
Java这么做主要是为了兼容之前的代码。假设如果有A,B两个类,这两个类是之前就已经存在的并且B还引用A。如果没有泛型擦除,将A改成了泛型类,那么B引用A必将出现错误。因此泛型擦除主要就是解决之前代码的兼容性问题。
泛型擦除引发问题
(1) 由于存在泛型擦除,因此任何在运行时需要知道具体类型的操作都无法进行。比如instanceof,但是可以通过如下方式解决这个问题
public void ofInstance(Object object) {
if (c.isInstance(object)) {
System.out.println("c instanceof "+object.getClass().getTypeName());
}
}
通过isInstance方法可以达到相同的目的。
(2) 如果相同对泛型类型进行实例化,使用new也是会发生错误,因此可以通过工厂方法来进行对象的创建
public interface Factory<T> {
T create();
}
public class IntegerFactory implements Factory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
也可以通过模板的方式进行创建
public abstract class GeneralFactory<T> {
final T element;
public GeneralFactory() {
element = create();
}
public abstract T create();
}
public class Creator extends GeneralFactory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
边界
泛型的边界(通过extends)用来限制泛型的范围,默认会使Object,如下所示:
public class ScopeBootstrap<T extends Number> {
private T t;
public ScopeBootstrap() {
}
public ScopeBootstrap(T t) {
this.t = t;
}
public T get() {
return t;
}
}
public class SubScopeBootstrap<T> extends ScopeBootstrap {
private T t;
public SubScopeBootstrap(T t) {
this.t = t;
}
public T getSub() {
return t;
}
}
对于ScopeBootstrap来说,它的参数化类型需要是Number,而子类SubScopeBootstrap继承了它因此泛型参数也必须是Number类型。
通配符
首先让我们声明几个模型
public class Fruit {
}
public class Apple extends Fruit {
}
public class pear extends Fruit{
}
? extends T*:当我们通过这种方式声明泛型的时候,代表声明的泛型类型是T的子类列表(包括T)。具体例子如下
public static void main(String[] args) {
List<? extends Fruit> list = new ArrayList<Apple>();
//error
list.add(new Apple());
List<Apple> apples = Lists.newArrayList();
Apple apple = new Apple();
apples.add(apple);
list = apples;
boolean isContain = apples.contains(apple);
int index = apples.indexOf(apple);
System.out.println((Apple) list.get(0));
System.out.println("iscontain = " + isContain);
System.out.println("index = " + index);
}
运行上面这段代码,会出现一些情况:
首先声明了一个Apple容器但是用的是List<? extends Fruit>进行接收,这种情况下编译器是不能判断容器中具体存放的类型的,因此不能向list中加入任何的元素。
,但是可以通过赋值的形式将一个apples容器指向list因为list获取的元素肯定是Fruit这是编译器可以检测到的。并且对于list来说contains和indexof的操作都是合法的。因为源码中这两个操作接收的就是Object类型。
? super T 超类型通配符,通过这种方式,传入泛型的类型下限就是特定类型T,必须是T的超类或者本身才可以,这在一定程度上放宽了泛型类型。案例如下
public <T> void addFruit(List<? super T> list, T item) {
list.add(item);
}
List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = new ArrayList<>();
addFruit(appleList, new Apple());
addFruit(fruitList, new Fruit());
通过super限定了传入的容器类型必须是T或者是T的超类,这样扩大了容器的可传入类型。
? 无界通配符:某种程度上,它几乎等同于任何类型。它只是用来声明在用泛型编写代码。常常用来处理比如Map中某一个可能是特定类型,某一个是任何类型。可以使用例如Map<String,?>来声明。
总结: 对于extends,常常在读取例如容器中的值时候使用,比如容器中的类型是不确定的,需要通过传入的参数来确定,那么可以通过它来进行限定;对于super,如果设置的值类型也是不确定的,比如设置容器的值,但是它的类型需要动态的传进来,那么可以通过此方式将泛型类型限定在指定类型的超类范围之内,这样就保证了类型的安全。通俗地说extends保证了返回的类型是统一的,super可以保证设置的值是在指定范围内的。