泛型方法
一般定义如下,即方法的前面加了个<T>
public class FTest {
public <T> List<T> f(T t){...};
}
三种泛型参数的推断方式:
(1)直接在f()前面加确定泛型
fTest.<Integer>f(xxx);
(2)通过输入参数确定, 下面这个推断为Integer
int number = 0;
fTest.f(number);
(3)可通过 返回值 确定
List<Integer> list = fTest.f(xxx);
Q: 下面这段代码哪里有问题? 是toString()那里吗?
public class A<T> {
public static void test(T t){
System.out.println(t.toString());
}
}
A:
test是static方法, 因此无法感知A<T>实例里的T
需要改成
public static <T> void test(T t)
toString()那里没问题,toString就是Object的方法。
注意void前面的 <T>
不是返回值,此处的返回值是void ,此处的<T> 表示传入参数有泛型,<T>存在的作用,是为了保证参数中能够出现T这种数据类型。
泛型参数和类型消除
Q: 泛型参数T在运行时,会变成什么?
A: 统一变成Object且不包含任何类型信息。
Q: 泛型参数T可以可以使用instanceof做比较吗?
class A<T> {
void f(Object arg)
if(arg instanceof T) {
...
}
}
A: 不能,编译器会报错。
Q: 泛型参数T可以进行new T()或者new T[]操作吗?
A: 不能,编译器会报错。
Q: 能调用泛型参数对象里的方法吗?
T.f();
A: 只能调用Object的方法。
Q: 可以用T做强制转化吗?
T t = (T)object;
A: 能运行, 但不会真正发生转型, 编译时会触发waring警告。
新建泛型对象时的问题
先假定有2个类, 基类Parent 和子类Child
class Parent{}
class Child extends Parent{}
回答以下问题:
Q: 下面这句话(声明list对象,左边父类泛型,右边子类泛型)有问题吗?
List<Parent> list = new ArrayList<Child>()
A:
有问题,编译就错误了。 List<Parent>和ArrayList<Child>并不存在父子类的关系
Q:这个list对象声明中, ? extends Parent = Child 有什么限制?
List<? extends Parent> list = new ArrayList<Child>();
A:
这个list可以调用A a = list.get(), 但是不能list.add(new Parent()),这是因为
- list.get()所做的操作是在返回时, 把内部的<? extend Parent> 强转成Parent, 是合理的,任何Parent的子类都可以转成Parent
- list.add(new Parent())所做的操作是在输入时, 把外部的A转成内部的<? extend Parent>, 这是不合理的,因为我们不知道这个Parent对象可以转成哪个Parent的子类。
Q:这个list对象声明中,? super Child = Parent 有什么特点?
List<? super Child> list = new ArrayList<Parent>();
下面谁会报错
list.add(new Child());
list.add(new Parent());
Parent a= list.get();
Child b = list.get();
- Child c = list.get() 或者Parent p = list.get()所做的操作是在返回时, 把内部的<? super Child> 强转成外部的Parent或者Child, 是不合理的, 因为编译器觉得child的父类 不一定 能转成parent或者child,所以禁止了这种行为( 比如parent的父类是object, 但object不一定就能转成parent或者child)
- list.add(new Child())所做的操作是在输入时, 把外部的child或者parent转成内部的<? super Child>, 这是合理的,因为child和parent一定能转成child的父类。
根据以上两个<? extends Parent>和<? super Child>的例子我们可以发现规律,子类往父类的强制类型转换是允许的,因为所有的子类都具有父类的共性,而反方向父类往子类转换是不允许的,因为每一个子类不一定相同,都有父类没有特征:
- 香蕉、苹果两者都可以说它俩都是水果
- 水果不能直接说它一定是是香蕉或者苹果,因为水果下面的种类很多,不知道是具体的哪一个
Q:List<?> list = new ArrayList
A:
get和add都不行,只能做remove等无返回值无输入A的操作。
PS: 注意,不是说不能调用get或add方法, 而是调用get或add时,不能使用A这个对象去操作。
即无法做add(A) 或者 A a = get(0)
但是可以做add(object) 或者Object o = get(0)
因为可以转为Object, 但是无法转为A。
PECS原则
注意PECS原则和上面的区别!
上面之前提到的? extend或者? supert, 都是在声明对象的时候用的。
而PECS原则是用于泛型对象的方法输入参数!
假设有一个类定义如下:
public static class MyList<T> {
List<T> list = new ArrayList<>();
// 把输入参数塞给自己,类似于生产操作
public void pushList(List<T> t) {
list.addAll(t);
}
// 把自己的内容塞给输入参数,类似于让输入参数做消费。
public void pollList(List<T> t) {
t.addAll(list);
}
}
Q:下面代码能正常运行吗?
MyList<Number> myList = new MyList<>();
List<Integer> intList = new ArrayList<>();
myList.pushList(intList);
List<Object> objectList = new ArrayList<>();
myList.pollList(objectList);
A:
不能正常运行, pushList和pollList都会报错
因为编译器检查后,认为 List<Integer>和List<Number>不是一个东西!
则T就是泛型参数。
Q: 如果上文要支持pushList,应该怎么修改pushList方法的定义?
A:
改成这样:
// 把输入参数塞给自己,类似于生产操作
public void pushList(List<? extends T> t) {
list.addAll(t);
}
即编译器认为,List<Integer> 和List<? extend Number>是一个东西,允许!
Q: 如果要支持pollList,怎么修改定义?
A:
// 把自己的内容塞给输入参数,类似于让输入参数做消费。
public void pollList(List<? super T> t) {
t.addAll(list);
}
因为是把自己的东西塞给输入参数, 而想要能塞进去,必须保证自己这个T,是输入参数的子类(Number是Object的子类),反过来说,输入参数必须是T的父类,所以用super
于是编译器认为,List<Object> 和List<? super Number>是一个东西,允许!
PECS原则出自Effective Java, 注意只是一个编程建议而已!
- 如果有一个类A,泛型参数为T
- 如果他一般只用于接收输入容器List后,塞入自己内部的T容器, 则类A就叫生产者, 因此输入参数最好定义为<? extend T>最好, 以便能接收任何T子类的容器。
- 如果他一般只用于接收输入容器后List, 把自己内部的T元素塞给它, 那么这个类A就叫消费者, 输入参数最好定义为<? super T> 最好,以便自己的T元素能塞给任何T元素的父类容器。
本博客文章除特别声明外,均可自由转载与引用,转载请标注原文出处:http://www.yelbee.top/index.php/archives/231/