泛型作用
为了代码可以被不同类型的对象所重用。在泛型出现以前,类似的功能借助Object类来实现,使用的时候使用强制类型转换调用。比如ArrayList类:
public class ArrayList{
Object[] elementData;
...
public Object get(int i){}
public void add(Object o){}
}
public static void main(String[] args) {
String name = (String)list.get(0);
list.add(new String(""));
}
这样带来很多问题,由于add的时候没有进行类型检查,可以将任何类型的对象加入列表。在get的时候,如果类型不支持转换就会报错。所以在java中引入了泛型,带来了更好的可读性和安全性。
泛型写法
泛型类
//写在类名后面,通常用KV、TUS表示,在类中可以当作正常类使用
public class Main<T, V>{
private T name;
private V age;
public Main(T user, V i) {
name = user;
age = i;
}
public T getName() {
return name;
}
public V getAge() {
return age;
}
public static void main(String[] args) {
Main<String, Integer> main = new Main<>("user", 12);
System.out.println(MessageFormat.format("用户名:{0} 年龄:{1}", main.getName(), main.getAge()));
//MessageFormat.format用占位符占位,性能更优
//String.format用正则表达式占位,两者功能一致
}
}
泛型方法
//方法签名中写在返回类型前,调用是写在方法名前
public class Main{
public <K, V> void getUser(K k, V v) {
return;
}
public static void main(String[] args) {
Main main = new Main();
main.<String, Integer>getUser("user", 12);
//类型可以省略,编译器有足够的信息推断出泛型的实际类型
//当只有一个泛型类,有多个参数时,编译器推断所有参数的公共父类为泛型类,如果没有公共则报错。
}
}
泛型变量限定
泛型中,可以用extends关键字对类型变量进行限定。在仔细学习之前,我一直认为这里的extends就是通配符,我还纳闷书里为什么会把这俩内容分开写。但是反复阅读实践后发现,变量限定用在泛型类定义中,通配符用在泛型类使用中,下面详述一下泛型变量限定。
使用最简单的泛型类时,发现在类或者方法中,我们不能对该类型的变量做出任何的操作(除了Object类中方法,至于为什么会在后文解释)。
//在设计的时候我们知道C类接受的都是集合类,我们想在C中访问一些集合类的方法,这个时候简单的泛型类就无法胜任。
public static class C<T>{
T c;
public void compute(){
c.toString();
//c.size();
}
}
//当使用extends限定时,就可以使用父类的所有方法等
public static class C<T extends List>{
T c;
public void compute(){
c.toString();
c.size();
}
}
这里的限定不光可以是类,还可以是接口,还可以同时有多个限定用&隔开。但需要注意的是,只能有一个类且必须要写到第一个位置,这与泛型类在虚拟机中的处理有关,下文会解释。
通配符
即便是引入了泛型的限定,但在泛型类或泛型方法定义的那一刻,泛型已经变成了一个既定的具体类。这种固定的泛型系统在灵活性上还是会有所欠缺,那么有没有一种方法,在定义之后仍然是多态的,这就要用到通配符。
先看一个实际的应用
boolean addAll(Collection<? extends E> c);
//这个是List类中的addAll方法,这里就很灵活,可以合并原有类型的子类型集合
除了子类型限定extends关键字外,还有超类型限定super关键字。一个是表明泛型类上界,一个是下界。因为定义上的差别,也导致了使用时两者的差别。用一句话来说,就是super可以向泛型对象写入,extends可以从泛型对象读取,而归根结底是因为java中多态特性,子类型可以赋值给父类型的特点。
public class Main{
public static class A<T>{
T t;
public T get(){
return t;
}
public void set(T t){
this.t = t;
}
}
public static class B{
public void add(){
}
}
public static class B1 extends B{
}
public static void main(String[] args) {
A<? extends B> a = new A<>();
B b = a.get();
//a.set(new B());
//extends时写入报错,因为在向对象内的泛型对象赋值,而对象内的类型是子类型。
//同样当用super时get报错,set可以执行。
}
}
如果取值传值想同时使用的话,可以使用<?>通配符。这里书中还有一些内容,比如子类型赋值给父类型的冲突,通配符其他用法,看来看去还是好复杂就不详述了。
虚拟机中的泛型代码(底层实现)
在JVM虚拟机中是没有泛型对象的,所有对象都必须是具体的普通类。也就说泛型的处理是发生在编译的过程中,编译后得到的字节码中不含泛型。
类型擦除
是指在编译阶段,将泛型类型替换为原始类型。原始类型指的是Object类,如果有限定就使用第一个限定类(这也是为什么在extends后要把类放在接口前面)。这是在泛型类内部发生的,而在泛型类外会直接隐去泛型,只保留类名。
泛型翻译
对象在处理的过程中,会使用原始类型进行处理。但对外表现的仍是用户指定的类型,这中间就需要用到类型转换。而泛型的作用,就是自动的、隐式的、安全的完成类型转换的工作。
public class Main{
public static class A<T>{
T t;
public T get(){
return t;
}
}
//编译过后会把所有的T用Object替换,这是类型擦除
public static void main(String[] args) {
A<String> a = new A();
String str = a.get();
//会先以原始类型方法进行调用,然后将返回的Object类型转换为String。
//也就说编译器会将这一条函数调用,翻译成以上两条字节码
}
}
还有一个需要注意的点,在泛型函数中,如果有父类是泛型类,子类继承的情况时。会出现函数重载的情况,但我们本意是用子类函数重写父类函数。出现这类情况的原因是,父类类型擦除后实际参数类型是Object,子类参数类型是具体类,没有重写覆盖到,反而成了重载。这样就会出现调用的一些错误,解决方法可以使用桥方法。桥方法我也没搞透,详细的可以去书中研究。
引用检查
既然泛型类中使用Object类,那在平时的编写中,比如如下情况,又为什么会报错呢。
public class Main{
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
//list.add("1");报错
}
}
这是因为编译器在编译前会对类型进行检查,检查无误后,在进行类型擦除。这里还有一个点需要注意,借用某位博主的总结。
public class Main{
public static void main(String[] args) {
ArrayList list = new ArrayList<Integer>();
list.add(1);
list.add("1");//这里并不会报错
}
}
也就说java在设计的时候,是对引用进行类型检查。这里的引用中并没有声明类型,所以检查不出错误。直接加入到原始类型的List中去了。
泛型注意事项
使用约束
- 不能使用基本类型作为泛型参数
- 程序运行中,获取泛型类的类型,之和类型名有关(原因就是底层实现)
- 泛型类不能有数组,而且泛型参数也不能是数组
- 泛型不能实例化
这里只提取了几个常用的,具体原因都与泛型实现方法有关,具体原因不展开,书里有。
继承规则
T是S的子类,但List<T>和List<S>之间没有任何关系。
反射的利用
对于泛型类型,由于会进行类型擦除,获取不到太多的信息。而反射提供了在运行时分析任意类和对象的功能,那么可以借助反射获取泛型类信息。