何为泛型

泛型概念

泛型故名思意,就是广泛的类型,其本质就是参数化类型。在各种资料里面,泛型的概念都是通过 ArrayList 这个类来引入的,那么这里也用它来引入泛型的概念。

ArrayList 我没有使用过,但是它是一个变长数组,可以通过方法 .add() 来不断增加元素,而不用担心空间大小不够的问题。这个类可以实现任意类型的可变数组,而实现的方法就是通过泛型来实现:

1
2
public class ArrayList<E> extends AbstractList<E> // ArrayList 类的声明
public void add(int index, E element) // add方法的一个重载

首先可能就注意到了,这个类名是 ArrayList<E> 并且在 add 方法的参数里面,element 的类型是 E ,这个 E 就是一个类型的代称,取什么名字都行,这个 E 表示什么类型,完全取决于在实例化 ArrayList 的时候,给它指定的类型。

比如我们想要一个 String 类型的可变数组,那么声明方法就是:

1
ArrayList<String> strs=new ArrayList<String>;

这样,上面的 <E> 被声明的时候填充成了 <String> 那么这个 E 就变成了 String ,下面的 element 的类型就变成了 String,这个 ArrayList 类,就变成了一个专门实现字符串数组的类。

泛型的优势

当然,由于向上转型的存在,我们可以用 Object 类来代替实现泛型,由于数据的载体是 Object[] ,所以这样实现还能让一个数组里面存储不同的类,第一个元素可以是 String 类型,第二个可以是 Integer 类型。但是这样就会带来一个问题,针对数据操作的时候,不但要将 Object 向下转型为对应类型,还要防止出现误转型,也就是把 String 转成了 Integer ,会抛出 ClassCastException 。

所以使用泛型来来编写一个数据模板是非常合适的,它可以通过指定类型来实现所有类的可变数组,而不需要为不同的类编写不同的可变数组,大大减少了代码量。

泛型的向上转型

从 ArrayList 的定义来看,它是实现了一个接口 List<T> 也就是说,他们是一个类继承关系。

1
public class ArrayList<T> implements List<T> 

那么我们就可以对它进行向上转型,需要注意的是,这里的向上转型是对携带了 泛型<T> 标志的类的继承关系的向上转型。转型过程中,泛型<T> 表示的类型需要一样:

1
List<String> a=ArrayList<String>;

这是一种转型,还有一种转型是对泛型的向上转型,也就是对 <T> 这里面的类 T 的向上转型。

对于这种向上转型,Java 是不支持的,原因和上面泛型的优势是一样的,可能会出现 ClassCastException ,也就是误类转型。

1
2
3
4
ArrayList<Number> a=ArrayList<Integer>;
a.add(1); // 1 Integer 属于 Number
a.add(1.0); //1.0 Float 属于 Number
Integer b=a.get(1); //获取索引一的元素也就是 1.0,会抛出ClassCastException

所以编译器为了避免出现这样的情况,就根本不允许你对泛型进行向上转型,也就是不让你玩泛型的多态。

泛型的本质

实现泛型

众所周知,JVM 虚拟机是一个很笨的东西,像泛型这样灵活的东西,它不太好实现。

怎么办呢,交给编译器实现吧,而 Java 的编译器实现泛型的方法,就是编译器代替人完成安全的强制转型。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
class Tempest<T> {
private T son;
public void setSon(T son) {
this.son=son;
}
public T getSon() {
return son;
}
}
Tempest<String> Xorex=new Tempest<>();
Xorex.setSon("Xorex");
String Name=Xorex.getSon();

上面的代码会被编译器自动转化为下面的代码(泛型 <T> 被全部替换为 Object,同时自动帮我们根据输入泛型的类型,将 Object 强制转型为我们输入的类型):

1
2
3
4
5
6
7
8
9
10
11
12
class Tempest {
private Object son;
public void setSon(Object son) {
this.son=son;
}
public Object getSon() {
return son;
}
}
Tempest Xorex=new Tempest();
Xorex.setSon("Xorex");
String Name=(String)Xorex.getSon();

所以,所谓的泛型本质上就是编译器帮我们完成了安全的强制转型,无论是 Tempest<String> 还是 Tempest<Integer> ,都为同一个类(操作的时候编译器会自动帮我们加上强制转型为不同的类型,看起来像是不同的类),Tempest<Object> 最后编译也只会出现一个文件:Tempest.class

<T> 本质上就是告诉编译器,操作完之后记得把 Object 强制转型为 T 类型,如果不加 <T> ,那么默认就不执行强制转型,直接返回 Object 类型。比如如果使用 Class 的实例的 newInstance() 获取某个类的实例的话,返回的是 Object 类型,而使用 Class<String> 的实例的 newInstance() 获取某个类的实力的话,返回的则是 String 类型。

泛型的局限性

因而,由于泛型的实现并不是我们所想象的那样:编译器帮我们写一个对应类型的类出来,而是用 Object 的强制转型来骗我们,因而就有了一些和前者不同的局限性:

1. <T> 不能是基本类型

因为实际上 <T> 可以是各种类始由于 Object 来存储接收并实现的,所以 <T> 就只能是引用类型——类,而对于基本类型 int、float 这些则无法使用泛型。

2. 无法获得泛型的 Class 实例

其实前面已经提过了,我们无论是对 Tempest<String> 还是 Tempest<Integer> 对于它们的实例,使用 .getClass() 获得的是本质上都是 Tempest<Object> 的 Class,也就是 Tempest.class 文件中的信息。

同样编译器也只有 Tempest.class 这种操作。

3. 无法判断带泛型类实例的类型

也就是说 Tempest<String>Tempest<Integer> 两者的实例,是没有办法判断是属于二者的,他们只能判断出来都属于 Tempest<Object>

4. 无法实例化 T 类型

1
2
3
4
5
class Tempest<T> {
public void toInstance() {
T One=new T(); //不可以
}
}

因为上面的代码会被转化为 Object One=new Object() 然后就没有任何意义。想要实现实例化 T,那么就必须借助传入的 Class 类型的实例。

1
2
3
public void toInstance(Class<T> Tclass) {
T One=Tclass.newInstance();
}

需要注意的是一定要用 Class<T> 这样在调用 Class 的 newInstance() 的时候,会直接返回 T 类型的实例,否则编译器不会给你自动强制转型为 T 类型,而是返回实际的类型 Object 的实例。

实质带来的可能性问题

还需要小心一个关于泛型本质的潜在问题,由于泛型的 <T> 骗着我们,实际上这个东西是 Object,就会导致潜在的覆写问题。

对于所有类都会继承的 Object 类,他有一个方法:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

当我们这样自定义一个方法的时候:

1
2
3
public boolean equals(T another) {
return false;
}

由于它的 T 实际上会被编译器搞成 Object ,所以导致了这个方法的签名和 Object 类的 equals 的方法签名相同,导致了意外的覆写父类的方法,由于看起来 T 和 Object 不一样,所以可能不太在意,但是实际上经过编译器处理过后,两者是一样的!

使用泛型

普通使用

对于最普通的泛型使用,就是代替 Object 来接受各种不同的类。其实最开始引入泛型概念的时候就已经讲述过了。

泛型接口

可以在接口中使用泛型,在实现接口的时候,可以选择固定接口的 <T> 为特定类型,或者直接沿用接口的 <T> 。

继承泛型

前面说了,因为本质上泛型实现是 Object 实现的,只不过是编译器记住了所属于的类型,给它加了一个强制转型,我们无法通过一个实例来得到泛型的具体类型。但是凡事都有例外,我们可以用特殊的方法来实现这样的需求。

答案就是用一个子类去继承父类,然后我们就可以通过子类的实例来获取父类的泛型的具体类型。这是因为子类的 .class 文件是包含了父类的所有信息的,自然也包括父类的泛型。但是获取的方法有点麻烦,就不研究了。

1
2
class Xorex extends Tempest<String> {
}

上面就可以通过 Xorex 的实例来获得父类 Tempest 的泛型类型为 String。

静态方法使用泛型

首先说明:在静态方法中使用泛型的方法格式:

1
public static <T> Xorex(T Info) 

与普通的方法不同的是在 static 后面多了一个 <T> 就像在 class 名字后面多了一个 <T> 是一样的,表示声明一个要使用的泛型。因为静态方法是脱离类的实例使用的,所以无论它的类 class 名字后面声明了几个泛型,都和静态方法无关,那些泛型都是在实例化的时候才会被编译器确定类型,静态方法的使用和实例化无关。

于是,静态方法需要自己定义自己需要的泛型,上面的 static 后面的 <T> 就是 Xorex 方法自己定义的泛型,通过传入的参数 Info 的类型来确定 <T> 的具体类型,如果无法通过传入参数来确定的话,则默认为 Object 类型。如果静态方法所在类也拥有泛型的话,建议静态方法的泛型命名避开类的命名,毕竟两者是不同的东西。

多泛型类型

只需要在 <> 里面用逗号分隔开多个泛型即可,比如 <T,K> 就成功声明了两个泛型,使用的时候:<String,Integer> 即可。

泛型通配符

通配符 ?

对于一个含有泛型的类来说,如果不确定具体的泛型是谁,尤其是当它要作为参数传入一个方法的时候,我们现在的认知来说,有两种方法实现:

第一种是在本方法所在的类名后定义一个泛型,然后再方法参数中加入这个泛型,在实例化的时候就把类型确定下来。但是在一个本来就是有泛型的类中的话,会影响其他方法中的泛型,而且如果使用者不会使用这个方法,还要白白在实例化的时候加上类型限制。

第二种是将这个方法改成静态方法,和类的实例脱节,自创一套泛型系统来使用。但是这可能会破坏封装性和完整性,自创泛型系统也有点小麻烦。

这里可以引入第三种解决方法,使用 ? 通配符,表示不确定的泛型。

1
2
public void Xorex(Tempest<?> a) {
}

这样这个方法就可以接收所有 Tempest 的任意具体泛型。

泛型的上限限定

上限限定使用 extends 表示,可以用在类名后声明泛型: class Tempest<T extends Number> 和方法接收参数:public void Xorex(<? extends Number>)

两者都表示接收的类型必须是 Number 及其子类,相当于给泛型一个上限限定,类在抽象不能超过 Number 的范围,这样里面的代码可以专注于只处理数字,不用担心有其他奇奇怪怪的类型。

泛型的下限限定

下限限定使用 super 表示,使用方法和上面的 extends 相同,其实这两种上限和下限以及泛型与反射有更加巧妙地用途,可惜现在的我暂时能力有限,看的不是太懂,先建立起整个 Java 知识体系。具体的这些,以后一定会再次详细的,透彻的研究一遍的。