泛型(generics)从表面的意思上来看,就是“广泛的类型”,实际上就是类型参数化。 注意两点:1、类型 2、参数。就是把类型作为参数传递给某处需要它的地方。
类型参数化这五个字可以完美的解释泛型的一切,理解了这个关键字泛型==,泛型就很容易理解了。
我们都知道,参数的特点是未知,可变的。那么同样,当我们不能确定某个类型具体是什么的情况下,我们可以使用泛型,在使用的时候“告诉”编译器,这个类型是什么。核心是提高代码的复用性。
例子:对String、Integer对象进行打印(这里仅仅是模拟,用打印代替更复杂的操作 )
class SoutString{ private String val; SoutString(String val){ this.val = val; } public void sout(){ System.out.println(val); } } class SoutInteger{ private Integer val; SoutInteger(Integer val){ this.val = val; } public void sout(){ System.out.println(val); } } public class SoutTest { public static void main(String[] args) { SoutString s1 = new SoutString("str"); SoutInteger s2 = new SoutInteger(111); s1.sout(); s2.sout(); } }使用SoutString、SoutInteger就可以很方便的完成。但是Java中有几千个类,每个需要打印呢,再采用这种方法就很麻烦了。即使借助一些辅助工具快速完成,代码依然会臃肿无比。我们可以很明显的看出,这两个类,只有变量的类型不一致,其余的都一样。那么只需要把类型当作参数传递给Sout对象即可,我们可以使用泛型,即参数化类型。
public class Sout<T> { private T val; Sout(T val){ this.val = val; } public void sout(){ System.out.println(val); } public static void main(String[] args) { Sout<String> s1 = new Sout<>("str"); Sout<Integer> s2 = new Sout<>(111); s1.sout(); s2.sout(); } }在上述代码中我们使用到了,T表示这是个类型参数,当然我们也可以使用A、B、C等等任意一个字母,但是java中有一些默认的命名的规则,为了保证编码风格,大家还是尽量使用默认规则。
上文提到,java中关于泛型有一些默认的规则。 K - Key(键) V - Value(值) T - Type(java类型) E - Element(在集合中使用,因为集合中存在的是元素) N - Number(数值类型)
在定义ServiceImpl1时,我们指定了Service的T是String,但是如果还需要一个ServiceImpl2来保证Service的T是Integer呢?甚至是Double呢?如果在定义ServiceImpl2、ServiceImpl3、ServiceImpl4就违背设计泛型的初衷了。所以泛型同样支持实现类本身也是泛型类。
// 声明泛型类ServiceImpl2用T,使用泛型ServiceImpl2 就要指定T具体的类型了 /* 但是注意,此处还使用到了泛型接口Service,使用泛型接口Service要指定具体的类型。 但是这个类型是由ServiceImpl2确定的,所以ServiceImpl2中的形参T作为Service接口的实参,传递给Service. */ /* 把service、ServiceImpl2当作方法就容易理解了。 比如 声明了一个Service方法,我们在使用Service方法是需要传递具体的实参。 然后ServiceImpl2方法内部调用了Service方法。那么ServiceImpl2方法的形参对于service来说,就是实参 */ public class ServiceImpl2<T> implements Service<T > { private T val; @Override public T getT() { System.out.println(val.getClass()); return this.val; } @Override public void setT(T t) { this.val = t; } public static void main(String[] args) { //String作为具体的类型传递给ServiceImpl2,ServiceImpl2内部会把String再传给Service ServiceImpl2<String> s1 = new ServiceImpl2<>(); ServiceImpl2<Integer> s2 = new ServiceImpl2<>(); s1.setT("str"); s2.setT(111); System.out.println(s1.getT());//class java.lang.String System.out.println(s2.getT());//class java.lang.Integer } }现在有一个新的需求,以Sout为例
public class Sout<T> { private T val; Sout(T val){ this.val = val; } public void sout(){ System.out.println(val.getClass()); System.out.println(val); } public static void main(String[] args) { Sout<String> s1 = new Sout<>("str"); Sout<Integer> s2 = new Sout<>(111); s1.sout(); s2.sout(); } }我们在使用Sout类时,可以直接指定类型,可以指定String、Integer等等。如果现在有一个新的需求,只允许Number的子类才能使用Sout,该怎么办呢?还记得上文中提到的<T>有两个含义,1、表明这是个泛型类或者泛型接口。2、表明类型的范围是T。 那么如果需要限制类型的范围,直接在 “<>”中间限制类型范围即可。java提供了extends关键字,用于限制类型的范围。比如<T extends Number>表明使用时 一定要指定具体的类型,并且这个类型必须是Number的子类。 上面的代码就可以改为:
public class Sout<T extends Number> { private T val; Sout(T val){ this.val = val; } public void sout(){ System.out.println(val.getClass()); System.out.println(val); } public static void main(String[] args) { Sout<String> s1 = new Sout<>("str");//此时此行代码,会报错,报错信息:Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number' Sout<Integer> s2 = new Sout<>(111); s1.sout(); s2.sout(); } }同样,如果限制Sout的类型参数必须是Integer的父类,那么使用<T super Integer>即可。
现在同样有一个新的需求,我们需要一个专门处理泛型类Sout的方法。 要求泛型类Sout的参数类型必须是Number的子类,但是这个方法只针对Integer的子类进行处理。 我们可以使用泛型方法完成这个需求:
public class TestMethod2 { // 定义泛型类Sout时,指定了参数类型必须是Number的子类 static class Sout<T extends Number> { private T val; Sout(T val){ this.val = val; } public T getVal() { return val; } } //show3方法只允许接受泛型类Sout,并且参数类型必须是Integer的子类 // 只需要在声明泛型的 “ <>标志 " 中限制类型的范围即可 public static <T extends Integer> void show(Sout<T > sout){ System.out.println(sout.getVal().getClass()); System.out.println(sout.getVal()); } public static void main(String[] args) { show(new Sout<Integer>(111)); } }那如果不允许使用泛型方法呢?java 提供了通配符(?)完成这个功能。代码如下:
public static void show2(Sout<? extends Integer> sout){ System.out.println(sout.getVal().getClass()); System.out.println(sout.getVal()); } public static void main(String[] args) { //show(new Sout<Integer>(111)); show2(new Sout<Integer>(222)); }通配符(?)表示任意类型(不确定的java类型)。当我们在使用泛型时,无法确定泛型的具体的参数类型,可以使用通配符代替。划重点,1、使用泛型时。2、无法确定。这两点已经表达出了很重要的信息。 第一点,使用通配符的前提时,必须是一个泛型类。通配符的作用范围是变量 第二点,上文就说过,我们想要使用泛型,必须指定具体的类型,但是如果不确定的话,就可以使用通配符。 比如:
public class TestMethod3 { // 此处我们无法确定Sout的具体参数类型,这个是动态传递过来的,此时可以用通配符代替 // 注意:? 虽然代表任意类型,但是这个“任意”的范围也要遵守 在定义泛型类Sout时,指定的范围,即extends Number private Sout<?> sout; public void setSout(Sout<?> sout) { this.sout = sout; } public Sout<?> getSout() { return sout; } public static void main(String[] args) { TestMethod3 testMethod3 = new TestMethod3(); testMethod3.setSout(testMethod3.new Sout<Integer>(222) ); testMethod3.getSout().sout(); } class Sout<T extends Number> { private T val; Sout(T val){ this.val = val; } public T getVal() { return val; } public void sout(){ System.out.println(getVal().getClass()); System.out.println(getVal()); } } }PESC原则有个大前提,就是这个规则是基于集合的。 首先来看下List的get以及add方法
public interface List<E> extends Collection<E> { boolean add(E e) E get(int index) } /* 当我们使用 List<? extends Number> list时,意味着可以向list添加任意类(只要是Number的子类) 那么,list.add(new Integer(1));此时向List中传入的Integer作为具体的类型。 但是接下来list.add(new Double(1.1)); 这个时候又传入了Double作为具体的类型。与Integer冲突。 所以,编译器不会允许向List<? extends XXX> 形式的集合添加任何元素。- 、 但是从List<? extends XXX> list形式的集合获取元素时允许的, 因为list中无论是Double还是Integer,都是Number类型。 */ List<? extends Number> list = new ArrayList<>(); list.add(new Integer(1));//编译报错 list.add(new Double(1.1));//编译报错 Number number = list.get(0);//编译通过结论: <? extends T>只能从中获取数据,不能添加数据。像一个生成者一样,只从生产者 手中获取东西,这就是 Producer Extends (PE)。
/* 当我们使用 List<? super Integer> list时,可以向list添加Integer类的父类。 此时我们传入一个Number类型作为具体的参数,这时,因为java中继承的传递性,凡是Integer的子类,就也一定是Number的子类,所以此时add(Integer的子类)是完全可以的。 但是因为,Integer的父类,父接口也有很多,所以编译器无法确定传入的到底是哪个父类, 所以get对象时,只能使用Object作为返回值。对于读操作而言,非常不合适。 */ List<? super Integer> list = new ArrayList<>(); list.add(new Integer(1)); Object object = list.get(0);结论:<? super Integer>只能添加数据,不能获取数据(准确的说,只能使用Object接受对象,然而这样丧失了泛型的意义)。像消费者一样,需要你生产对象放进去。这就是 Consumer Super(CS)。
public static void main(String[] args) { List<Integer> list1 = new ArrayList<>(); list1.add(1); list1.add(2); list1.add(3); List<Integer> list2 = new ArrayList<>(); copy(list1,list2); System.out.println("======================================"); System.out.println(Arrays.toString(list2.toArray())); } public static void copy(List<? extends Integer> list1,List<? super Integer> list2){ //list1.add(new Integer(1)); 编译报错 for (Integer integer : list1) { list2.add(integer); //Object object = list2.get(0); } }T表示具体类型,作用在接口、类、方法上 ?表示任意类型,作用在变量上 有了泛型T,才能使用 ?。?的初衷就是无法确定T到底是什么类型,所以才有了?。也就是说T是?的前提。只有使用<T>声明了泛型类,后续在使用这个泛型类时,才有机会使用到?
1、编译时,编译器不会对List进行类型安全检查,会对List<Object>这种带有参数的进行类型安全检查 2、可以把任何带参数的类型传递给原始类型List,但却不能把List<Integer>传递给接受 List<Object>的方法,因为会产生编译错误
public static void show1(List list) { } public static void show2(List<Object> list) { } public static void main(String[] args) { List<Integer> list = new ArrayList<>(); show1(list);//编译通过 show2(list);//编译不通过 }List<?>表示可以存放任意类型。 List<Object>表示传进去的具体类型是Object,不能把List<Integer>传递给接受 List<Object>的方法
public static void show1(List<?> list) { } public static void show2(List<Object> list) { } public static void main(String[] args) { List<Integer> list = new ArrayList<>(); show1(list);//编译通过 show2(list);//编译不通过 }泛型实际上是一个语法糖,在java文件编译为class文件的时候,就进行了类型擦除。 类型擦除:就是从泛型类中清除类型参数的相关信息,并且在必要的时候进行类型检查和类型转换。 先看个例子:
// 这是个java文件 public class Sout<T> { private T val; Sout(T val) { this.val = val; } public void sout() { System.out.println(this.val.getClass()); System.out.println(this.val); } public T getVal() { return this.val; } public static void test(Sout<? extends Integer> asd) { asd.sout(); } } // 这是编译后的class文件 public class Sout { private Object val; Sout(Object val) { this.val = val; } public void sout() { System.out.println(val.getClass()); System.out.println(val); } public Object getVal() { return val; } public static void test(Sout asd) { asd.sout(); } }我们可以清楚的看到class文件中根本就没有T的影子。这是因为javac进行编译的时候,基于一个最左原则(可确定的 、最顶级的父类型)将类型参数T替换掉了。 所谓的最左原则换句话说:就是 如果T有上界(比如 T extends XXX),那么就使用XXX代替类型参数T,否则使用Object来代替T,因为Object是所有类的父类嘛。 因此,会引发一个结果:泛型类实例的class都是同一个class,就是说 编译器只为泛型类生成一份字节码。
既然class文件底层都没有泛型参数的信息了,那么相应的,我们使用泛型的java代码在编译的时候也会通过类型转换与检查来去除泛型,看例子:
// 这是java文件 public static void main(String[] args) { Sout<String> s1 = new Sout<>("haha"); String val1 = s1.getVal(); s1.sout(); Sout<Integer> s2 = new Sout<>(2222); Integer val2 = s2.getVal(); s2.sout(); } //这是反编译后的class文件 public static void main(String args[]) { Sout s1 = new Sout("haha"); String val1 = (String)s1.getVal(); s1.sout(); Sout s2 = new Sout(Integer.valueOf(2222)); Integer val2 = (Integer)s2.getVal(); s2.sout(); }