文章目录
泛型
与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
它也是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
注意泛型添加的位置,如果是类上的泛型,添加在类名之后;如果是方法上的泛型,添加在修饰符之后,返回值之前。
List和List<?>和List的区别
public class Test01 {
public static void main(String[] args) {
List list01=new ArrayList(10);
List<Object> list02=new ArrayList<>(10);
List<Integer> integerList=new ArrayList<>(10);
//List<Object>与其他两者的区别
list01=integerList;
List<?> list03=integerList;
//下面一行编译报错。注意数组可以这样赋值,因为它是协变的,而泛型不是协变的(非协变:Integer和Object是继承关系,但是List<Integer>和List<Object>非继承关系)。
//list02=integerList;
//List<?>与其他两者的区别
list01.add(new Object());
list02.add(new Object());
//下面一行编译报错。List<?>称为通配符集合,可以接受任何类型集合的引用赋值,但是不能添加任何元素,虽然可以remove和clear
//list03.add(new Object());
//没有报错,非泛型集合却可以赋值给任何有限制的集合。
List<? extends String> list04=list01;
}
}
<?>、<? extends T>、<? super T>
public void testFun(List<? extends Cat> list){
//下面一行报错。所有的extends限制的集合都不能添加除了null之外的所有元素
// list.add(new Cat());
//返回带类型的元素
Cat cat = list.get(0);
//所以extends是put受限
}
public void testFun02(List<? super Cat> list){
//可以添加Cat及其子类对象
list.add(new Cat());
list.add(new CatSon());
//下面一行报错
//list.add(new Animal());
//所有的super限制的集合执行get操作时,返回的元素泛型丢失
Object o=list.get(0);
//所以super是get受限
}
//而List<?>称为通配符集合,可以接受任何类型集合的引用赋值,但是不能添加任何元素,虽然可以remove和clear
//如果只是不断地向集合获取元素的话,属于Get First,使用<? extends T>
//相反,如果只是不断地向集合添加元素,属于Put First,使用<? super T>
PECS(producer-extends,consumer-super)
如果参数化类型表示一个生产者T,就使用<? extends T>
注意:生产者或消费者指的是参数化类型的角色。
public void pushAll(Iterable<? extends E> src){
for(E e:src){
push(e);
}
}
如果只是不断地获取元素的话,属于生产者(GET FIRST),使用<? extends T>
参数化类型(这里指src)不断的提供元素以供外部push,所以是生产者,
如果参数化类型表示一个消费者T,就使用<? super T>
public void popAll(Collection<? super E> dst){
while(!isEmpty()){
dst.add(pop())
}
}
如果只是不断地向消费元素的话,属于消费者(PUT FIRST),使用<? super T>
参数化类型(dst)不断的消费pop所返回的元素,所以是消费者。
其他
- 返回类型仍然是
Set<E>
,不要使用通配符类型作为返回类型(会强制用户在客户端代码中使用通配符类型)。 Effective Java P122 - 类型限制
<E extends Comparable<E>>
可以读作“可以与自身进行比较的每个类型E” 。 Effective Java P118
public static <T extends Comparable<T>> T max(List<T> list){
if(c.isEmpty())
....
T result = null;
for(T t : list){
if(result == 0 || t.compareTo(result) > 0){
result = Object.requireNonNull(E);
}
}
return result;
}
- 根据PECS,上一条应该修改成
public static <T extends Comparable<? super T>> T max(List<? extends T> list);
首先list提供t,属于生产者,因此将类型从List<T>
改成List<? extends T>
。
接着T的compare消费T实例。因此,参数化类型Comparable<T>
被有限制通配符类型Comparable<? super T>
取代。Effective Java P123
- 所有的Comparable和Comparator都是消费者。所以使用时始终应该是
Comparable<? super T>
优先于Comparable<T>
,Comparator<? super T>
优先于Compator<T>
。
可变参数方法和泛型不能良好的合作
Effective Java P125
1.堆污染:当一个参数化类型指向一个不是该类型的对象时,就会产生堆污染
2.将值保存在泛型可变参数数参数中是不安全的
3.@SafeVarargs注解可以禁止带泛型可变参数的方法的警告(@SafeVarargs只能用在无法被覆盖的方法上)
泛型可变参数方法在以下条件下是安全的:
1.没有在可变参数数组中保存任何值
2.没有对不被信任的代码开放该数组
不安全的案例:
static <T> T[] toArray(T... args){
return args;
}
//不安全,违反了第二个条件,返回了可变参数数组
static <T> T[] pickTwo(T a,T b){
return toArray(a,b);
}
//这个方法本身并没有危险,也不会产生警告,但是它调用了带有泛型可变参数的toArray方法,当
public static void main(String[] args){
String[] arr=pickTwo("aaa","bbb");
}
//会抛出一个ClassCastException
//所以允许另一个方法访问一个泛型可变参数数组是不安全的
只要保证可变参数数组只是用来将数量可变的参数从调用程序传到方法(毕竟这才是可变参数的目的),那么该方法就是安全的。(即不违反保证的安全的两个条件)
也可以使用一个List参数代替可变参数
例1:
static List<T> flatten(List<List<? extends T>> lists){
List<T> result = new ArrayList();
for(List<? extends T> list : lists){
result.addAll(list);
}
return result;
}
//客户端
list = flatten(List.of(list1,list2));
例2:
static <T> List<T> pickTwo(T a,T b){
return List.of(a,b);
}
//客户端
list = pickTwo("aaa","bbb");
这种做法的优势在于编译器可以证明该方法是类型安全的,不必再通过@SafeVarargs证明安全性,缺点在于类似例1的客户端代码比较繁琐,运行起来速度会慢一点。
优先使用类型安全的异构容器
类似Set和Map<K,V>的容器,泛型充当“参数化了的容器”,这样就限制了每个容器只能有固定数目的类型参数,一般情况下,这也正是我们想要的。
但是有些时候,我们需要更多的灵活性,例如数据库拥有任意数量的列,需要以类型安全的方式访问所有列。这种情况下就需要将键(key)进行参数化而不是将容器参数化,然后将参数化的键提交给容器来插入或者获取值,用泛型系统确保值的类型与键相符。
原理
使用Class对象充当参数化键的部分,泛型化Class,类的类型从字面上看不再只是简单的Class,而是Class,当一个类的字面(类型)被用在方法中,用来传达编译时和运行时的类型信息时,就被称为类型令牌。
实现
public class ContainTestImpl{
private final Map<Class<?>, Object> hashMap = new HashMap<>();
public <T> void put(Class<T> tClass, T value) {
hashMap.put(Objects.requireNonNull(tClass),value);
}
public <T> T get(Class<T> tClass) {
//通过cast方法检测值是否为Class对象所表示的类型的实例
return tClass.cast(hashMap.get(tClass));
}
}
需要注意的是,这里的无限制通配符属于的是Class,而不是hashMap,所以不存在不能将任何东西放进Map的情况。
通配符类型是嵌套的,它不是属于Map类型,而是属于它的键的类型。
每个键都可以有一个不同的参数化类型,可以是Class、Class等等。异构就是从这里来的。
局限1
上面的实现存在着局限性,恶意的客户端可以很轻松的破坏类型安全,只要以原生态形式使用Class对象,但会造成客户端代码编译时产生未受检的警告,这与一般的集合实现,如HashMap、HashSet没什么区别。
你可以很轻松的使用原生态类型HashSet将String放入HashSet中。
如果愿意付出一点代价,就可以拥有运行时类型安全。即在put方法中检验value是否真的是tClass表示的类型的实例。
public <T> void put(Class<T> tClass,T value){
hashMap.put(tClass,tClass.cast(value));
}
java.util.Collections中就有一些集合包装类采用了类似的技巧,它们称作“checked-Set、checkedList、checkedMap…”
局限2
第二个局限就在于它不能用在不可具体化类型中。即你可以保存String或者String[]的key,但是不能保存List的key,甚至不能通过编译,原因在于你无法为List获得一个Class对象,List.class是个语法的错误。
有限制的类型令牌
可能需要限制那些可以传给方法的类型,这可以通过有限制的类型令牌实现,利用有限制类型参数或者有限制通配符来限制可以表示的类型。
例如 : getAnnotation(AnnotatedElement接口下)
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
asSubClass方法
假设需要将一个类型为Class<?>的对象传给一个有限制的类型令牌的方法,例如getAnnotation(AnnotatedElement接口下),你可以直接转换为Class,
但是这种转换是非受检的,所以类Class提供了一个安全(且动态)地执行这种转换的方法,即asSubClass
//Class类下
public <U> Class<? extends U> asSubclass(Class<U> clazz) {
if (clazz.isAssignableFrom(this))
return (Class<? extends U>) this;
else
throw new ClassCastException(this.toString());
}
参考
https://www.cnblogs.com/wuqinglong/p/9456193.html