一、泛型介绍
1 泛型入门
集合元素过去默认为Object类型,无法指定元素类型,编译时不检查类型,而且每次取出对象都要进行强制类型转换,泛型出现避免了这种臃肿的代码。下列代码会看到编译时不检查元素类型导致的异常。
public class ListErr { public static void main(String[] args) { List strList = new ArrayList(); strList.add("人有悲欢离合"); strList.add("月有阴晴圆缺"); strList.add(23333); //ClassCastException strList.forEach(str -> System.out.println(((String) str).length())); } }
泛型用于指定集合存储数据的类型。
List<String> books=new ArrayList<String>();
上述代码定义集合时使用泛型,创建对象时构造器也给出泛型类型,这样显然是多余的。java7做了改进。
List<String> books=new ArrayList<>();
2 深入泛型
2.1定义泛型接口、类
我们可以在定义一个类时允许它使用泛型,通过阅读java提供的集合接口源码可以知道如何定义泛型接口。
//定义接口时指定了一个类型形参E public interface List<E> extends Collection<E> { //在接口中将形参E作为类型使用 boolean add(E e); Iterator<E> itorator; // ... }
在实际使用时,只需要在声明对象时传入E的实参即可 。
2.2 从泛型类派生子类
从泛型类派生子类时,我们可以为泛型指定实参,也可以不使用,注意不要再使用形参T。
public class Apple<T> { private T info; public T getInfo(){ return this.info; } }
public class SmallApple extends Apple<String> { private String info; public String getInfo() { return this.info; } }
2.3 并不存在泛型类
实际上,泛型只是设计来用于方便编程,并不会由于指定类型不同而生成不同的class文件。
public class GenericTest { public static void main(String[] args) { List<String> strList = new ArrayList<>(); List<Integer> intList = new ArrayList<>(); // true System.out.println(strList.getClass() == intList.getClass()); } }
也就是说,不管传入实参是不是一个类型,它们仍然被当成一个类型的数据,不允许在静态成员中使用泛型形参。
二、通配符
如果在使用时泛型类时不传入泛型实参会出现警告,但是如果我们并不能确定其类型如何处理?第一种想法是传入Object类型的实参,但是实际上这种办法是行不通的。
public class Test { public void listTest(List<Object> c){ for(int i=0;i<c.size();i++){ System.out.println(c.get(i)); } } public static void main(String[] args) { List<String> strList=new ArrayList<>(); //编译不能通过,The method testList(List<String>) is undefined for the type Test //testList(strList); } }
这是因为Java中List并不是List的子类,指定类型为Object传入参数类型只能是Object。
为了解决这个需求,可以使用类型通配符。
public void listTest(List<?> c){ for(int i=0;i<c.size();i++){ System.out.println(c.get(i)); } }
List表明他是任何泛型List的父类,现在任何的List类型都可以调用listTest()方法。上面的代码解决了不指定类型抛出警告的问题,在有的时候却会使代码臃肿:使用了泛型还要进行强制类型转换。我们也可以设定通配符上限,只代表某一类泛型的父类。
public abstract class shape { public abstract void draw(Canvas c); }
public class Circle extends Shape { @Override public void draw(Canvas c) { System.out.println("在画布" + c + "上画个圈圈"); } }
public class Retangle extends Shape { @Override public void draw(Canvas c) { System.out.println("在画布" + c + "上画个块块"); } }
public class Canvas { public void drawAll(List<? extends Shape> shapes) { for (Shape s : shapes) { s.draw(this); } } }
在并不知道受限制的通配符的具体类型时,不可以将Shape及其子类对象加入这个泛型集合中。
public void addRetangle(List<? extends Shape> shapes){ // shapes.add(0, new Retangle()); }
三、泛型类与泛型方法
1.泛型类
不仅使用通配符时可以设置形参上限,定义类型形参时也可以设置类型上限。
public class Apple<T extends Number> { private T info; public static void main(String[] args) { Apple<Integer> intApple=new Apple<>(); //编译不通过,Bound mismatch //Apple<String> strApple=new Apple<>(); } }
而且Java可以在定义形参时设置一个父类上限,多个接口上限。下列代码要求T类型必须是Number类的子类,而且必须实现了java.io.Seriazable接口。
public class Apple<T extends Number & java.io.Serializable> {...}
2.泛型方法
可以单独为方法指定泛型形参。在该方法内部可以把指定的泛型形参当成正常类型使用。
public class GenericMethodTest { static <T> void fromArrayToCollections(T[] a,Collection<T> c){ for(T o: a){ c.add(o); } }
public static void main(String[] args) { Object [] oa=new Object[100]; Collection<Object> co=new ArrayList<>(); //下列T代表Object类型 fromArrayToCollections(oa, co); String [] sa=new String[100]; Collection<String> cs=new ArrayList<>(); //下列T代表String类型 fromArrayToCollections(sa, cs); //下列T代表Object类型 fromArrayToCollections(sa, co); Number [] na=new Number[100]; //The method fromArrayToCollections(T[], Collection<T>) //in the type GenericMethodTest is not applicable for //the arguments (Number[], Collection<String>) //fromArrayToCollections(na, cs); } }
四、泛型方法的自动类型推断
编译器会根据泛型方法传入的实参自动推断形参的值,通常会推断出最直接的类型参数。
为了让编译器可以推断出泛型类型,不要让编译器迷惑,否则就会出错。
public class ErrorTest { static <T> void test(Collection<T> from,Collection<T> to) { for (T ele : from){ to.add(ele); } } public static void main(String[] args) { List<String> as = new ArrayList<>(); List<Object> ao = new ArrayList<>(); //The method test(Collection<T>, Collection<T>) //in the type ErrorTest is not applicable for //the arguments (List<Object>, List<String>) //test(as, ao); } }
可以进行如下修改,避免编译失败。
public class ErrorTest { static <T> void test(Collection<? extends T> from,Collection<T> to) { for (T ele : from){ to.add(ele); } } public static void main(String[] args) { List<String> ao = new ArrayList<>(); List<Object> as = new ArrayList<>(); test(ao, as); } }
五、泛型通配符与泛型方法区别
什么时候使用泛型方法,什么时候使用类型通配符呢?一般能够使用通配符,都可以改写为泛型方法。
public interface Collection<E> extends Iterable<E> { boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); } public interface Collection<E> extends Iterable<E> { <T> boolean containsAll(Collection<T> c); <T extends E> boolean addAll(Collection<T> c); }
这两个方法的类型形参T其实都只使用了一次,唯一效果就是在调用时传入实际类型参数,因此Collection接口设计时采用的时上示第一种:类型通配符,类型通配符就是被设计来支持灵活子类化的。
泛型方法用来表示方法一个或者多个参数之间的依赖关系,或者参数与返回值的关系,如果没有这种依赖关系,就不要使用泛型方法。
有时候会同时使用泛型通配符和泛型方法。比如Collctions.copy()方法。dest和src的参数之间存在依赖关系:src的集合元素必须是dest类型或其子类,故用泛型方法表示dest的类型,而该方法无需向src中添加修改元素,也没有其它参数src的类型,因此使用通配符更合适。
public static <T> void copy(List<? super T> dest, List<? extends T> src) {...}
六、泛型构造器、设置通配符下限
1.泛型构造器
java中也支持泛型构造器。在泛型类中允许使用菱形语法,但不允许在显示声明构造器泛型类型的情况下使用菱形语法。
class Foo <E>{ public <T> Foo(T t) { System.out.println(t); } } public class GeneratorConstructor { public static void main(String[] args) { new Foo("人不轻狂枉少年"); new <Integer> Foo(20); //使用菱形语法,E传入实参为String类型,E泛型构造器中T参数传入实参为Integer Foo <String> foo=new Foo<>(5); //E传入实参为String类型,显示声明构造器中的方法T传入实参为Integer Foo <String> foo2=new <Integer> Foo <String>(5); //编译错误 //Explicit type arguments cannot be used with '<>' in an allocation expression //Foo <String> foo3=new <Integer> Foo <>(5); } }
2.设置通配符下限
考虑场景:在copy方法中吧集合src中的元素复制到dest集合中,同时要求返回最后一个添加的元素。如果我们采用设置通配符上限的方法,那么返回最后一个添加的元素时,将返回一个丢失实际类型的参数。
public class MyUtils_1 { public static <T> T copy(Collection<T> dest, Collection<? extends T> src){ T last = null; for(T ele : src){ last = ele; dest.add(ele); } //返回的类型为T,但last元素实际上可能是T的子类 return last; } }
Java中设计了类型通配符下限解决这一需求。
public class MyUtils { //下面dest集合元素的类型必须与src元素的类型相同,或是其父类 public static <T> T copy(Collection<? super T> dest, Collection<T> src){ T last = null; for(T ele : src){ last = ele; dest.add(ele); } return last; } }
七、java8改进的泛型参数推断机制
java8增强了泛型方法的类型推断能力:允许通过调用方法的上下文推断类型参数的目标类型,允许在方法调用链中将推断到的泛型参数传递至最后一个方法。
class MyUtil<E>{ public static <Z> MyUtil<Z> nil(){ return null; } public static <Z> MyUtil<Z> cons(Z head,MyUtil<Z> tail){ return null; } E head(){ return null; } } public class InferenceTest { public static void main(String[] args) { //通过方法赋值的目标参数来推断类型参数为String MyUtil<String> ls=MyUtil.nil(); //通过cons()方法所以需要的参数类型来推断类型参数的类型为Integer MyUtil.cons(42, MyUtil.nil()); } }
八 泛型擦除与转换
当把一个带有泛型信息的变量赋值给一个不带泛型信息的变量时,泛型信息将被擦除,对元素的类型参数检查将变成类型的上限。
class Apple<T extends Number>{ T size; public Apple(){ } public T getSize() { return size; } public void setSize(T size) { this.size = size; } } public class ErasureTest { public static void main(String[] args) { Apple<Integer> ap=new Apple<>(); Integer apSize=ap.getSize(); Apple ap2=ap; //Type mismatch: cannot convert from Number to Integer //Integer apSize2=ap2.getSize(); } }
理论上,List是List的子类,把List对象直接赋给List应该出现编译错误,但实际上不会。
public class ErasureTest2 { public static void main(String[] args) { List<Integer> li = new ArrayList<>(); li.add(1); // 泛型擦除 List list = li; System.out.println(li.get(0)); // 警告:The expression of type List needs unchecked conversion to conform // to List<String> ls = list; // Exception :java.lang.Integer cannot be cast to java.lang.String System.out.println(ls.get(0)); } }
这篇文章就介绍到这里了。