1.为什么需要使用泛型
我们经常用到的list集合,如果不用泛型在编译器期规定其类型那么就可以存放任意类型的值,取出来则是object。如果我们知道存进去的都是什么类型便可以正确强转为正确类型,如果不知道正确类型而取出来进行强制转换则会报错
比如以下代码
List list = new ArrayList();
list.add("213");
list.add(123);
list.add(true);
String s = (String) list.get(0);
String s1 = (String) list.get(1);
报错:Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
List<String> list = new ArrayList<>();
...
如果规定了其类型为string,那么就会在编译器add不是string类型的变量报错。
2.泛型的使用
泛型类
class Fruit <T>{
private T value;
public Fruit(T value){
this.value = value;
}
public T getValue(){ //特别注意这不是泛型方法而是成员方法
return value;
}
}
泛型接口
interface Animal<T>{
public T say();
}
class dog implements Animal<String>{
@Override
public String say() {
return null;
}
}
Animal后必须有类型,否则去掉泛型则方法是返回object否则
class dog<T> implements Animal<T>{
@Override
public T say() {
return null;
}
}
需要这样定义确定类型
泛型方法
public <T> T test(T t){
return t;
}
public static <T> T test(T t){
return t;
}
在返回值前加一个泛型标识则为泛型方法
泛型类的成员方法使用泛型不能定义为静态方法而泛型方法可以定义为静态方法
可变参数
public <E> void arg(E... args){
for (E arg : args) {
}
}
类似于数组的存储规则
通配符
在泛型参数表达式中的问号
public static void showKeyValue(Generic<Number> obj){
System.out.println(obj.getKey);
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number>
改动一下这个方法
public void showKeyValue1(Generic<?> obj){
System.out.println(obj.getKey);
}
类型通配符一般是使用?代替具体的类型实参
可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
泛型的上边界
interface HasColor {
java.awt.Color getColor();
}
class WithColor<T extends HasColor> {
T item;
WithColor(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
- 为泛型添加上边界,即传入的类型实参必须是指定类型或指定类型的父类
3.泛型擦除
引用Onjava8中的
#include <iostream>
using namespace std;
template<class T> class Manipulator {
T obj;
public:
Manipulator(T x) { obj = x; }
void manipulate() { obj.f(); }
};
class HasF {
public:
void f() { cout << "HasF::f()" << endl; }
};
int main() {
HasF hf;
Manipulator<HasF> manipulator(hf);
manipulator.manipulate();
}
/* Output:
HasF::f()
*/
c++中的模版方法 Manipulator类存储了类似于泛型的T类型的对象。 mainpulate则可以直接调用HasF类中的f()方法,c++会在实例化模版时进行检查在Mainpulator实例化的那一刻首先检查HasF中是否含有f()方法否则会报错。
但java中如果这样不会通过编译,找不到该方法。这是因为我们java其实做了泛型擦除。我们使用泛型可以这么理解,编译期可知,我们写入的泛型类型就好像是在编译期被我们规定好了,所以在编写的时候语法会遵循我们规定的泛型类型。而在运行期其实是将我们的泛型擦除了的,ArrayList 和 ArrayList 的class比较会发现是相等的,因为都是ArrayList而类型会被擦除。
如果我们想做到c++模版方法中的功能我们就需要使用到边界,给泛型设定一个边界那么泛型擦除的时候就会是我们设置的边界。
泛型边界extends
public class Manipulator2<T extends HasF> {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
但其实我们可以这么做
class Manipulator3 {
private HasF obj;
Manipulator3(HasF x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
但之所以我们要用到泛型是因为比如在某些时候我们返回一个T方法,那么有了泛型的帮助我们就能返回一个确切的类型。否则如果用object那么我们就能只能进行自己确认强制类型转换。
之所以会出现泛型擦除是因为java并不是在一开始就存在泛型的特性而是后期加入的,擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,这种被称为“迁移兼容性”。因为泛型擦除,泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。如果希望类型参数不被当作object来处理则需要使用边界来管理。
4.泛型数组
class Generic<T> {
}
Generic<Integer>[] gia = new Generic<String>[10];
报错:创建泛型数组
gia = (Generic<Integer>[]) new Object[SIZE];
[Ljava.lang.Object; cannot be cast to [LGeneric;
我们是没办法创建一个泛型数组的。问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 gia
被强制转换为 Generic<Integer>[]
,该信息也仅在编译时存在。成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。
Generic<Integer>[] gia = new Generic[10];
泛型数组被擦除成object数组
public class GenericArray<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArray(int sz) {
array = (T[]) new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
// Method that exposes the underlying representation:
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<>(10);
try {
Integer[] ia = gai.rep();
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
// This is OK:
Object[] oa = gai.rep();
}
}
/* Output:
[Ljava.lang.Object; cannot be cast to
[Ljava.lang.Integer;
*/
我们不能说 T[] array = new T[sz] ,所以我们创建了一个 Object 数组并将其强制转换。
rep() 方法返回一个 T[] ,在主方法中它应该是 gai 的 Integer[],但是如果调用它并尝试将结果转换为 Integer[] 引用,则会得到 ClassCastException ,这再次是因为实际的运行时类型为 Object[]
我们可以通过class类型标记来进行擦除恢复。
public GenericArray(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
GenericArray<Integer> gai = new GenericArray<>(Integer,10);
Integer[] ia = gai.rep();
这样就能恢复泛型的擦除。
java源码ArrayList的toArray方法
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
相当于在编译时规定了类型。所以我们能在返回值拿到相应的类型的数组
4.通配符用于上下边界的情况
class Fruit{
}
class Apple extends Fruit{
}
clss Fruit extends Food{
}
Class Plate<T>{
private T item;
public Plate(T t){
item = t;
}
public void set(T t){
item = t;
}
public T get(){
return item;
}
}
Plate<? extends Fruit> p = new Plate<Apple>(new Apple());
p.set(new Fruit());//error
p.set(new Apple());//error
Fruit newFruit1=p.get();
Plate<? super Fruit> p1=new Plate<Fruit>(new Fruit());
p1.set(new Fruit());
p1.set(new Apple());
Apple newFruit3=p1.get();//error
Fruit newFruit1=p1.get();//error
Object newFruit2=p1.get();
? extends Fruit从这个描述中,编译器无法得知这里需要 Fruit 的哪个具体子类型,因此它不会接受任何类型的 Fruit。所以通配符?和类型参数T的区别就在于,对编译器来说所有的T都代表同一种类型。但通配符?没有这样的约束。但取东西get( )方法还有效
? super Fruit 下界规定了元素的最小粒度的下限,相当于放松了容器的类型控制,只要存粒度比Fruit小就可以,但往外取因为丢失了类型信息所以只能取到object而具体类型信息丢失了。
频繁往外读取内容的,适合用上界Extends。
经常往里插入的,适合用下界Super。
部分引用:onjava及泛型相关博客