泛型高级进阶二:泛型的高级使用
泛型
泛型高级进阶一:泛型的基本使用
泛型高级进阶二:泛型的高级使用
文章目录
一.泛型通配符(重点)
1、定义
泛型中的问号符“?”名为“通配符”
2、通配符的适用范围:
参数类型
字段类型
局部变量类型
返回值类型(但返回一个具体类型的值更好)
3、语法格式
<?>
<? extends XXX>
<? super XXX>
1.Java泛型PESC原则
1、定义:PECS即 Producer extends Consumer super
- 如果你既要获取又要放置元素,则不使用任何通配符。例如List ,参考: “2.非受限通配符”
- 如果你只需要将类型T放到集合中, 使用<? super T>通配符,参考下面:“3.固定下限通配符
- 如果你只需要从集合中获得类型T , 使用<? extends T>通配符,参考下面:“4.固定上限通配符”
2、优点
提升了API的灵活性
2.非受限通配符
1、定义:
不受具体对象的影响
<?>
2、使用场合
写一个方法,而这方法的实现可以利用Object类中提供的功能时泛型类中的方法不依赖类型参数时
如List.size()方法,它并不关心List中元素的具体类型
代码示例
public static void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
public static void main(String[] args) {
List<String> st= new ArrayList<>();
st.add("aa");
st.add("cc");
printList(st);
List<Integer> number = new ArrayList<>();
number .add(11);
number .add(22);
printList(number);
}
- 这种使用List<?>的方式就是父类引用指向子类对象. 注意, 这里的printList方法不能写成public static void printList(List list)的形式, 原因我在上一篇博文中已经讲过, 虽然Object类是所有类的父类, 但是List跟其他泛型的List如List, List不存在继承关系, 因此会报错.
- 我们不能对List<?>使用add方法, 仅有一个例外, 就是add(null). 为什么呢? 因为我们不确定该List的类型, 不知道add什么类型的数据才对, 只有null是所有引用数据类型都具有的元素
3.固定下限通配符
1、限定了类型的下限,也就它必须为某类型的父类
定义:
<? super A>
List<XXX>比List<? super XXX>要更加严格。因为前者仅仅兼容XXX类型的列表,而后者却兼容任何XXX超类的列表
2、代码示例
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
addNumbers(list1);
System.out.println(list1);
List<Number> list2 = new ArrayList<>();
addNumbers(list2);
System.out.println(list2);
List<Double> list3 = new ArrayList<>();
// addNumbers(list3); // 编译报错
}
我们看到, List<? super E>是能够调用add方法的, 因为我们在addNumbers所add的元素就是Integer类型的, 而传入的list不管是什么, 都一定是Integer或其父类泛型的List, 这时add一个Integer元素是没有任何疑问的. 但是, 我们不能使用get方法, 请看如下代
public static void getTest2(List<? super Integer> list) {
// Integer i = list.get(0); //编译报错
Object o = list.get(1);
}
4.固定上限通配符
1、限定了类型的上限,也就它必须为某类型的子类
<? super XXX>
List<XXX>要比List<? extends XXX>更加严格,因为前者仅能匹配XXX列表,然而后者却可同时匹配XXX及其子类的列表
代码示例
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
// 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
s += n.doubleValue();
}
return s;
}
public static void main(String[] args) {
List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
System.out.println(sumOfList(list1));
List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
System.out.println(sumOfList(list2));
}
有一点我们需要记住的是, List<? extends E>不能使用add方法, 请看如下代码:
public static void addTest2(List<? extends Number> l) {
// l.add(1); // 编译报错
// l.add(1.1); //编译报错
l.add(null);
}
原因很简单, 泛型<? extends E>指的是E及其子类, 这里传入的可能是Integer, 也可能是Double, 我们在写这个方法时不能确定传入的什么类型的数据, 如果我们调用:
5.第三方使用案例
使用? super E还有个常见的场景就是Comparator. TreeSet有这么一个构造方法:
TreeSet(Comparator<? super E> comparator)
例如
public class Person {
private String name;
private int age;
/*
* 构造函数与getter, setter省略
*/
}
public class Student extends Person {
public Student() {}
public Student(String name, int age) {
super(name, age);
}
}
class comparatorTest implements Comparator<Person>{
@Override
public int compare(Student s1, Student s2) {
int num = s1.getAge() - s2.getAge();
return num == 0 ? s1.getName().compareTo(s2.getName()) : num;
}
}
public class GenericTest {
public static void main(String[] args) {
TreeSet<Person> ts1 = new TreeSet<>(new comparatorTest());
ts1.add(new Person("Tom", 20));
ts1.add(new Person("Jack", 25));
ts1.add(new Person("John", 22));
System.out.println(ts1);
TreeSet<Student> ts2 = new TreeSet<>(new comparatorTest());
ts2.add(new Student("Susan", 23));
ts2.add(new Student("Rose", 27));
ts2.add(new Student("Jane", 19));
System.out.println(ts2);
}
}
5.总结
我们要记住这么几个使用原则, 有人将其称为PECS(即"Producer Extends, Consumer Super", 网上翻译为"生产者使用extends, 消费者使用super", 我觉得还是不翻译的好). 也有的地方写作"in out"原则, 总的来说就是:
- in或者producer就是你要读取出数据以供随后使用(想象一下List的get), 这时使用extends关键字, 固定上边界的通配符. 你可以将该对象当做一个只读对象;
- out或者consumer就是你要将已有的数据写入对象(想象一下List的add), 这时使用super关键字, 固定下边界的通配符. 你可以将该对象当做一个只能写入的对象;
- 当你希望in或producer的数据能够使用Object类中的方法访问时, 使用无边界通配符;
- 当你需要一个既能读又能写的对象时, 就不要使用通配符了.
二.泛型擦除
1.定义
Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除
证明示例
- 例1.原始类型相等
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
}
}
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>
泛型类型的,只能存储字符串;一个是ArrayList<Integer>
泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
- 例2.通过反射添加其它类型元素
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
2.擦除后的原始类型:
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
举例:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair的原始类型为:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
因为在Pair中,T 是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair,如Pair或Pair,但是擦除类型后他们的就成为原始的Pair类型了,原始类型都是Object。
例如.通过查看编译后的代码
Class Plate<T>{}
Plate<Interger> interger =new Plate<>();
通过命令编译后:
3.泛型擦除带来的问题及解决
- 先检查,再编译以及编译的对象和引用传递问题
Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 - 自动类型转换
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。
既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
源码做了强制转换:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
-
类型擦除与多态的冲突和解决方法
Bridge Methods 桥方法,桥方法以在扩展泛型时保持多态性
当编译一个扩展参数化类的类,或一个实现了参数化接口的接口时,编译器有可能因此要创建一个合成方法,名为桥方法。它是类型擦除过程中的一部分 -
泛型类型变量不能使用基本数据类型
比如没有ArrayList,只有ArrayList.当类型擦除后, ArrayList的原始类中的类型变量(T)替 换成Object,但Object类型不能 存放int值 -
不能使用instanceof 运算符
因为擦除后,ArrayList只剩下原始类型,泛型信息String不存在了,所有没法使用instanceof -
泛型在静态方法和静态类中的问题
因为泛型类中的泛型参数的实例化是在定义泛型类型对象(比如ArrayList)的时候指定的,而静态成员是不需要使用对象来调用的,所有对象都没创建,如何确定这个泛型参数是什么 -
泛型类型中的方法冲突
因为擦除后两个equals方法变成一样的了 -
没法创建泛型实例
因为类型不确定 -
没有泛型数组
因为数组是协变,擦除后就没法满足数组协变的原则
相关参考
泛型通配符部分参考: https://www.cnblogs.com/wxw7blog/p/7517343.html
泛型擦除部分参考: https://blog.csdn.net/wisgood/article/details/11762427
上一篇: Android自定义控件中获取文字宽高的方法已经drawText中的xy
下一篇: 泛型--通配符