泛型是Java1.5中出现的新特性,也是最重要的一个特性。泛型的本质是参数化类型,也就是说所操做的数据类型被指定为一个参数。这种参数类型能够用在类、接口和方法的建立中,分别称为泛型类、泛型接口、泛型方法。这个类型参数将在程序运行时肯定。java
咱们能够把泛型理解为做用在类或者接口上面的标签。根据这个标签的类型传入规定的数据类型,不然就会出错,其中类型必须是类类型,不能是基本数据类型。例如咱们国家中医存药的箱子,每一个箱子上面都贴有一个标签,若是上面贴的是冬虫夏草,那么就只能放冬虫夏草,而和其余的药物混合放在一块儿就很是的乱,很容易出现错误。安全
泛型就是这样的道理,咱们先来看下泛型最简单的使用吧:ide
ArrayList<String> list=new ArrayList<>(); list.add("Hello"); //只能放字符串,若是放数字编译报错 //list.add(666);
注意:Java1.7以后泛型能够简化,就是变量前面的参数类型必需要写,然后面的参数类型能够写出来,也能够省略不写。性能
简单举个例子,这个应该是网上最经典的例子:学习
//建立集合对象 ArrayList list=new ArrayList(); list.add("Hello"); list.add("World"); list.add(111); list.add('a'); //遍历集合内容 for (int i = 0; i < list.size(); i++) { String str= (String) list.get(i); System.out.println(str); }
上面程序运行结果毫无疑问会出现异常java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String,这就是没有泛型的弊端。由于上面的ArrayList中就能够存听任意类型(即Object类型),咱们知道,全部的类型均可以用Object类型来表示,而Object在类型转换方面很容易出现错误。测试
也许有人会想能够先用Object类型代替String接收,而后再转成相对应的数据类型,这样是也能够的,可是万一在转换的时候某个数据类型看错了或者记错了,那么还不是会出现转换异常。就算运气好所有都对了,可是这种强制类型转换一大堆的代码,你的同事或者领导看了可能会拿刀来砍死你,很是不利于代码后期的维护。this
可见没有泛型是万万不能的,同时咱们还能得出泛型带来的一些好处:spa
①、能够有效的防止类型转换异常的出现。.net
②、可让代码更简洁,从而提升代码的可读性、可维护性和稳定性。3d
③、能够加强for循环遍历集合,进而提高性能,由于都是相同类型的。
④、能够解决类型安全编译警告。由于没有泛型时全部类型向上转型为Object类型,因此存在类型安全问题。
泛型它有三种使用方式,分别为:泛型类、泛型接口、泛型方法。咱们接下来学习它们怎么使用。
泛型类就是把泛型定义在类上,它的使用比较的简单。咱们先来定义一个最普通的泛型类:
//定义泛型类,其中T表示一个泛型标识 public class Generic<T> { private T key; public Generic() { } public Generic(T key) { this.key = key; } //这里不是泛型方法,它们是有区别的 public T getKey() { return key; } public void setKey(T key) { this.key = key; } }
泛型类的测试代码以下,咱们只需在类实例化的时候传入想要的类型,而后泛型类中的T就会自动转换成该相应的类型。
public static void main(String[] args) { //有参构造器初始化,传入String类型 Generic<String> generic = new Generic<>("Generic1..."); System.out.println(generic.getKey());//Generic1... //无参构造器初始化,传入Integer类型 Generic<Integer> generic1 = new Generic<>(); generic1.setKey(123456); int key = generic1.getKey(); System.out.println(key);//123456 }
而后咱们再来看下泛型中泛型标识符,在上面这个泛型类中 T 表示的是任意类型,泛型中还有不少这样的标识,例如K表示键,V表示值,E表示集合,N表示数子类型等等。
在泛型类中还有两种特殊的使用方式,是关于继承的时候是否传入具体的参数。
①、当继承泛型类的子类明确传入泛型实参时:
//子类中明确传入泛型参数类型 class SubGeneric extends Generic<String>{ } //测试类 class Test{ public static void main(String[] args) { SubGeneric subGeneric = new SubGeneric(); //调用继承自父类的属性 subGeneric.setKey("SubGeneric..."); String key = subGeneric.getKey(); System.out.println(key); } }
能够得出子类在继承了带泛型的父类时,明确的指明了传入的参数类型,那么子类在实例化时,不须要再指明泛型,用的是父类的类型。
②、当继承泛型类的子类没有明确传入泛型实参时:
//子类没有明确传入泛型参数类型 class SubGeneric<T> extends Generic<T>{ private T value; public T getValue() { return value; } public void setValue(T value) { this.value = value; } } class Test{ public static void main(String[] args) { SubGeneric<Integer> subGeneric = new SubGeneric<>(); //调用继承自父类的属性 subGeneric.setKey(123456); int key = subGeneric.getKey(); System.out.println(key); //调用子类本身的属性 SubGeneric<String> subGeneric1=new SubGeneric<>(); subGeneric1.setValue("SubGeneric..."); String value = subGeneric1.getValue(); System.out.println(value); } }
当子类没有明确指明传入的参数类型时,那么在子类实例化时,都是根据子类中传入的类型来肯定的父类的类型。若是父类和子类中有一个明确,另外一个没有明确或者二者明确的类型不同,那么它们的类型会自动提高为Object类型。以下:
//一个明确,一个不明确 class SubGeneric<T> extends Generic<String>{ private T value; }
泛型接口和泛型类的定义和使用几乎相同,只是在语句上面有些不一样罢了,因此这里就很少说什么了。直接来看一下例子:
//定义一个泛型接口 public interface IGeneric<T> { public T show(); } class Test implements IGeneric<String>{ @Override public String show() { return "hello"; } }
泛型接口实现类中是否明确传入参数和泛型类是同样的,因此就不说了,能够参考泛型类。
前面介绍了泛型类和泛型接口,它们二者的使用相对来讲比较的简单,而后咱们再来看一下泛型方法,泛型方法比它们二者稍微复杂一点点。在前面的泛型类中咱们也看到了方法中有用到泛型,它的格式是这样的:
//这里不是泛型方法,它们是有区别的 public T getKey() { return key; }
可是它并非泛型方法。泛型方法的中的返回值必须是用 <泛型标识> 来修饰(包括void),只有声明了<T>的方法才是泛型方法,这里<T>代表该方法将使用泛型标识T,此时才能够在方法中使用泛型标识T。而单独只用一个泛型标识 T 来表示会报错,系统没法解析它是什么。固然咱们也可使用其余的泛型标识符如:K、V、E、N等。
泛型方法的声明格式以下:
//这里的泛型标识 T 与泛型类中的 T 没有任何关系 public <T> T show(T t){ return t; }
注意:泛型方法的泛型与所属的类的泛型没有任何关系。咱们能够举例说明:
public class GenericMethod<T> { //这里的泛型标识 T 与泛型类中的 T 没有任何关系 public <T> T show(T t){ return t; } } class Test{ public static void main(String[] args) { //实例化泛型类,传入String类型 GenericMethod<String> genericMethod = new GenericMethod<>(); //调用方法,传入数字 Integer show = genericMethod.show(123456); System.out.println(show);//123456 //调用方法,传入字符 Character a = genericMethod.show('a'); System.out.println(a);//a } }
在测试类中,建立类的实例时,泛型传入的是String类型,而在调用方法的时候,分别传入了数字类型和字符类型。
至此咱们得出结论:泛型方法是在调用方法的时候指明泛型的具体类型,因此泛型方法能够是静态的;泛型类和泛型接口是在实例化类的时候指明泛型的具体类型,因此泛型类和泛型接口中的方法不能是静态的。
若是某个类继承了另外一个类,那么它们之间的转换就会变得简单,例如Integer继承Number:
Number number=123; Integer integer=456; number=integer;//能够赋值 Number[] numbers=null; Integer[] integers=null; numbers=integers;//能够赋值
上面这么作彻底能够,可是把它们用在泛型中却不是这么一回事了。
List<Number> list1=null; List<Integer> list2=null; //编译报错,不兼容的类型 //list1=list2;
这是由于Integer继承自Number类,可是List<Integer>并非继承自List<Number>的,它们两是都继承自Object这个根父类。看到下面这张图片可能会更好的理解(图片引用自https://blog.csdn.net/whdalive/article/details/81751200)
这里用这样一段话来归纳:虽然类A是类B的父类,可是G<A>和G<B>它们之间不具有任何子父类关系,两者都是并列关系,惟一的关系就是都继承自Object这个根父类。
再来看一下另外一种状况:带泛型的类(接口)与另外一个类(接口)有继承(实现)的关系。
举例:在集合中,ArrayList <E>实现List <E> , List <E>扩展Collection <E> 。 所以ArrayList <String>是List <String>的子类型,List <String>是Collection <String>的子类型。 因此只要不改变类型参数,就会在类型之间保留子类型关系。
Collection<String> collection=null; List<String> list=null; ArrayList<String> arrayList=null; collection=list; list=arrayList;
这里其实就是普通的继承(实现),相似于Integer继承在Number类同样。图片以下:
参考文章:https://blog.csdn.net/whdalive/article/details/81751200
为何要用通配符呢?那确定是泛型在某些地方还不是很是完美,因此才要用到通配符呀,咱们来分析一下:
在上面的一节中咱们讲了Integer是Number的一个子类,而List<Integer>和List<Number>两者都是并列关系,它们之间毫无关系,惟一的关系就是都继承自Object这个根父类。那么问题来了,咱们在使用List<Number>做为方法的形参时,可否传入List<Integer>类型的参数做为实参呢?其实这个时候答案已经很明显了,是不能的。因此这时候就能够用到通配符了。
在没有使用通配符的状况下咱们的代码要这样写:
public class Generic { public static void main(String[] args) { List<Number> list1=new ArrayList<>(); list1.add(1); list1.add(2); List<Integer> list2=new ArrayList<>(); list2.add(3); list2.add(4); show(list1); //报错说不能应用Integer类型 //show(list2); } public static void show(List<Number> list){ System.out.println(list.toString());//[1, 2] } }
若是还想要支持Integer类型则须要添加新的方法:
public static void show1(List<Integer> list){ System.out.println(list.toString());//[1, 2] }
这样就会致使代码大量的冗余,很是不利于阅读。因此此时就须要泛型的通配符了,格式以下:
public static void show(List<?> list){ System.out.println(list.toString());//[1, 2][3, 4] }
当咱们使用了通配符以后,就能轻松解决以上问题了。在Java泛型中用 ? 号用来表示通配符,?号通配符表示当前能够匹配任意类型,任意的Java类均可以匹配。可是在使用通配符以后必定要注意:该对象中的有些方法任然能够调用,而有些方法则不能调用了。例如:
public static void show(List<?> list){ //list.add(66);//不能使用add() list.add(null);//惟独能添加的只有null Object remove = list.remove(0);//可使用remove() System.out.println(remove); Object get = list.get(0);//可使用get() System.out.println(get); System.out.println(list.toString()); }
这是由于只有调用的时候才知道List<?>通配符中的具体类型是什么。因此对于上面的list而言,若是调用add()方法,那么咱们并不知道要添加什么样的类型,因此会报错。而remove()和get()方法都是根据索引来操做的,它们都是数字类型,因此它是能够被调用的。
通配符还有两种扩展的用法:
List<? extends ClassName>,它表示的是传入的参数必须是ClassName的子类(包括该父类),相似于数学中(∞,ClassName],而后简单举例说明:
public static void main(String[] args) { List<Number> list1=new ArrayList<>(); list1.add(1); List<Integer> list2=new ArrayList<>(); list2.add(2); List<Object> list3=new ArrayList<>(); list3.add(3); show(list1);//传入Number类型 show(list2);//传入Integer类型 //show(list3);//传入Object类型,报错说不能应用Object类型,由于这里最大只能用到Number类型 } public static void show(List<? extends Number> list){ System.out.println(list.toString()); }
List<? super ClassName>,它表示的是传入的参数必须是ClassName的父类(包括该子类),相似于数学中[ClassName,∞)
public static void main(String[] args) { List<Number> list1=new ArrayList<>(); list1.add(1); List<Integer> list2=new ArrayList<>(); list2.add(2); List<Object> list3=new ArrayList<>(); list3.add(3); show(list1);//传入Number类型 //show(list2);//传入Integer类型,报错说不能应用Integer类型,由于此时最小只能用到Number类型 show(list3);//传入Object类型, } public static void show(List<? super Number> list){ System.out.println(list.toString()); }