夯实Java基础(十九)——泛型
Posted tang-hao-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了夯实Java基础(十九)——泛型相关的知识,希望对你有一定的参考价值。
1、什么是泛型
泛型是Java1.5中出现的新特性,也是最重要的一个特性。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。这个类型参数将在程序运行时确定。
我们可以把泛型理解为作用在类或者接口上面的标签。根据这个标签的类型传入规定的数据类型,否则就会出错,其中类型必须是类类型,不能是基本数据类型。例如我们国家中医存药的箱子,每个箱子上面都贴有一个标签,如果上面贴的是冬虫夏草,那么就只能放冬虫夏草,而和其他的药物混合放在一起就非常的乱,很容易出现错误。
泛型就是这样的道理,我们先来看下泛型最简单的使用吧:
ArrayList<String> list=new ArrayList<>(); list.add("Hello"); //只能放字符串,如果放数字编译报错 //list.add(666);
注意:Java1.7之后泛型可以简化,就是变量前面的参数类型必须要写,而后面的参数类型可以写出来,也可以省略不写。
2、为什么要泛型
简单举个例子,这个应该是网上最经典的例子:
//创建集合对象 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接收,然后再转成相对应的数据类型,这样是也可以的,但是万一在转换的时候某个数据类型看错了或者记错了,那么还不是会出现转换异常。就算运气好全部都对了,但是这种强制类型转换一大堆的代码,你的同事或者领导看了可能会拿刀来砍死你,非常不利于代码后期的维护。
可见没有泛型是万万不能的,同时我们还能得出泛型带来的一些好处:
①、可以有效的防止类型转换异常的出现。
②、可以让代码更简洁,从而提高代码的可读性、可维护性和稳定性。
③、可以增强for循环遍历集合,进而提升性能,因为都是相同类型的。
④、可以解决类型安全编译警告。因为没有泛型时所有类型向上转型为Object类型,所以存在类型安全问题。
泛型它有三种使用方式,分别为:泛型类、泛型接口、泛型方法。我们接下来学习它们怎么使用。
3、泛型类
泛型类就是把泛型定义在类上,它的使用比较的简单。我们先来定义一个最普通的泛型类:
//定义泛型类,其中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;
4、泛型接口
泛型接口和泛型类的定义和使用几乎相同,只是在语句上面有些不同罢了,所以这里就不多说什么了。直接来看一下例子:
//定义一个泛型接口 public interface IGeneric<T> public T show(); class Test implements IGeneric<String> @Override public String show() return "hello";
泛型接口实现类中是否明确传入参数和泛型类是一样的,所以就不说了,可以参考泛型类。
5、泛型方法
前面介绍了泛型类和泛型接口,它们两者的使用相对来说比较的简单,然后我们再来看一下泛型方法,泛型方法比它们两者稍微复杂一点点。在前面的泛型类中我们也看到了方法中有用到泛型,它的格式是这样的:
//这里不是泛型方法,它们是有区别的 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类型,而在调用方法的时候,分别传入了数字类型和字符类型。
至此我们得出结论:泛型方法是在调用方法的时候指明泛型的具体类型,所以泛型方法可以是静态的;泛型类和泛型接口是在实例化类的时候指明泛型的具体类型,所以泛型类和泛型接口中的方法不能是静态的。
6、泛型在继承方面的体现(兼容性)
如果某个类继承了另一个类,那么它们之间的转换就会变得简单,例如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
7、通配符
为什么要用通配符呢?那肯定是泛型在某些地方还不是非常完美,所以才要用到通配符呀,我们来分析一下:
在上面的一节中我们讲了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() 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()方法都是根据索引来操作的,它们都是数字类型,所以它是可以被调用的。
7.1通配符上限
7.2通配符下限
以上是关于夯实Java基础(十九)——泛型的主要内容,如果未能解决你的问题,请参考以下文章