Java 一篇弄懂泛型
一篇弄懂泛型
一、认识泛型
泛型最常用的场景是在集合中使用,想必我们对这样的代码也不陌生
ArrayList list = new ArrayList(); //这样做能够规定该集合只能装载string元素,避免当代码增多时忘记集合装载的内容
可以理解泛型为一个模板可以充当你需要的类型
二、使用泛型
泛型类
public class Order { String orderName; int orderId; //类的内部结构就可以使用类的泛型 T orderT; public Order(){ //编译不通过,不能new泛型对象 // T[] arr = new T[10]; //编译通过 T[] arr = (T[]) new Object[10]; } public Order(String orderName,int orderId,T orderT){ this.orderName = orderName; this.orderId = orderId; this.orderT = orderT; } //如下的个方法都不是泛型方法 public T getOrderT(){ return orderT; } public void setOrderT(T orderT){ this.orderT = orderT; } @Override public String toString() { return "Order{" + "orderName='" + orderName + '\'' + ", orderId=" + orderId + ", orderT=" + orderT + '}'; } //静态方法中不能使用类的泛型。 // public static void show(T orderT){ // System.out.println(orderT); // } public void show(){ //编译不通过 // try{ // // // }catch(T t){ // // } } //泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没任何关系。 //换句话说,泛型方法所属的类是不是泛型类都没关系。 //泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。 public static List copyFromArrayToList(E[] arr){ ArrayList list = new ArrayList(); for(E e : arr){ list.add(e); } return list; } }
【SubOrder.java】
//SubOrder:不是泛型类 public class SubOrder extends Order { public static List copyFromArrayToList(E[] arr){ ArrayList list = new ArrayList(); for(E e : arr){ list.add(e); } return list; } }
- 泛型接口
public interface Comparable { /** * 返回负数: 当前实例比参数o小 * 返回0: 当前实例与参数o相等 * 返回正数: 当前实例比参数o大 */ int compareTo(T o); }
- 泛型方法
public class GeneTest{ public E show(E obj){ System.out.println(obj); } }
小结: - 泛型类中的泛型对象不能使用new方法
- 静态方法中不要使用泛型参数,因为静态方法在类加载时就已经初始化了这时还编译器无法确定泛型。
- 若父类是泛型,则在继承时子类分为:全继承(将父类的泛型全部继承),部分继承(将父类部分泛型指定具体类型,再将每指定具体类型的泛型继承),全不继承(将父类的泛型全看作object对象)。
- 泛型类及接口的泛型是在new对象的时候确定的,而泛型方法的泛型是在调用方法时才决定而非在new对象时确定
- 异常类没有泛型。
三、擦拭法
其实虚拟机是不认识泛型的,泛型是依赖编辑器完成的,在早期的jdk中,java采用将属性、参数声明为object方式来增加自由度,到jdk1.5后引入了泛型,为了兼容早期的版本,于是在jvm将泛型看作是object。
ArrayList<String> list = new ArrayList<>(); list.add("sad"); list.add("shi"); list.get(0);
上述代码等同于
ArrayList<Object> list = new ArrayList<>(); list.add((String) "sad"); list.add((String) "shi"); (String) list.get(0);
四、通配符
说到通配符,首先我们来聊聊泛型的继承,看下面一段代码
public class BlogCode { public static void main(String[] args) { Data<Integer> dInt = new Data<>(2); Data<Number> dNum = dInt; } } class Data<T>{ T data; public Data(T data) { this.data = data; } public T getData() { return data; } public void setData(T data) { this.data = data; } } class NumFather{ Number num; public NumFather(Number num) { this.num = num; } public Number getNum() { return num; } public void setNum(Number num) { this.num = num; } } class Num extends NumFather{ int number = 1; public Num(Number num, int number) { super(num); this.number = number; } } class NumSon extends Num{ double dou = 2.0; public NumSon(Number num, int number, double dou) { super(num, number); this.dou = dou; } }
有同学会认为,上面的代码是没有问题的,因为Number是Integer的父类,这不就是多态嘛,其实不是这样的,同一个类指定不同类型作为泛型的对象之间是并列关系,并不是继承关系,编译器不会通过这样的语法。
如何让上面代码成立?
Data<Integer> dInt = new Data<>(2); Data<? extends Number> dNum = dInt; System.out.println(dNum.getData());
只需要加一个通配符即可,下面我们来讲以下通配符
通配符的关键字为? 我们常用<? extends 类型> 和 <? super 类型>
- 以上面的Data类为例,当我们采用了Data<? extends Number>修饰的时候,该变量可以指向Data<泛型为Number或其子类>的对象,但不能修改其泛型字段指向其他对象。
Data<? super NumSon> dataSon = new Data<>(new NumSon(1,2,3)); Data<Num> data = new Data<>(new Num(4,5)); Data<NumFather> dataFa = new Data<>(new NumFather(6)); //通过编译,super通配符修饰的对象 dataSon = data; Data<? extends NumFather> exFather = data; //无法通过编译,因为尝试修改其泛型字段指向其他对象 exFather.setData(new Num(1,2));
原因:当我们初始化变量时指向了一个Data<integer>的对象,而在后面的代码中又将该变量指向了一个Data<double>的对象,这是错误的,所以编译器为了防止这种错误的发生,不允许让<? extends 类型>的对象在初始化完成之后又指向其他的地址。
2. 与<? extends 类型>相反,<? super T>允许该泛型修饰的对象可以指向T类型及其父类的对象,但你不方便去读取该泛型对象的泛型字段。以如下代码为例</double></integer>
Data<Number> dNum = new Data<>(2); Data<? super Integer> dSuper = dNum; //成功,虽然读取了但没让具体对象去指向它 System.out.println(dNum.getData()); //无法通过编译,因为无法确定返回的类型是Integer还是它的父类 Integer data = dSuper.getData();
小结:PESC原则:extends通配符则只用来读取泛型字段而不修改,super修饰符则只用来修改泛型字段而不读取。
如果需要返回T,它是生产者(Producer),要使用extends通配符;如果需要写入T,它是消费者(Consumer),要使用super通配符。