Java泛型
在集合中,集合是不知道向里面添加的是什么的,为了通用性,全都作为Object,导致取出来后要进行强制类型转换.而泛型允许程序在创建集合时指定集合元素的类型.
泛型入门
使用泛型
1 | import java.util.ArrayList; |
上面程序成功创建了一个特殊的List集合:strList,这个List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象.
在上面的代码中我指定了集合所能装入的类型:String.这样就可以避免一不小心加入类型错误的元素,而且程序 更加简洁,集合自动记住所有集合元素的数据类型,从而无须对集合元素进行强制类型转换.泛型从JDK5开始支持.
JDK7中的变化
在jdk7中,不需要这么写了ArrayList<String> strList = new ArrayList<String>();
,可以把后面一个尖括号里的内容省略:ArrayList<String> strList = new ArrayList<>();
.
深入泛型
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参).
定义泛型接口,类
下面是JDK5改写之后的List接口,Iterator接口,Map接口:
1 | public interface List<E> |
观察发现:泛型允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。
我们可以将ArrayList<String>
理解为ArrayList的一个子类,但仅仅是这么理解,实际上并没有产生这种子类.我倾向于把它解释为一种”标记”.
泛型不仅仅只是用于集合,任何类,接口都可以增加泛型声明.
1 | public class Apple<T> |
注意:构造器名仍是Apple,不是Apple<T>.
从泛型类派生普通子类
从泛型派生子类,泛型类型需具体化
也就是说这种写法是错误的:public class A extends Apple<T>{ }
如果需要这样做有两种方法:
- 指定泛型类型(传入实际的类型参数)
1 | public class A extends Apple<String> |
这种写法会使得Apple类中的T全部替换为String,A继承了这个具体的Apple类.
- 不指定泛型类型(不给T传入实际的类型参数)
1 | public class A extends Apple |
这样子做会使得T形参被当做Object处理.
并不存在泛型类
前面提到可以把ArrayList<String>类当成ArrayList的子类,事实上,ArrayList<String>类也确实像一种特殊的ArrayList类:该 ArrayList<String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList<String>生成新的class文件,而且也不会把ArrayList<Strin>当成新类来处理。
如果用==
来比较ArrayList<String>和ArrayList<Integer>,结果是true.并且对于泛型类的实例,不能用instanceof来检测它是否是ArrayList<T>的实例.
从本质上说,泛型只是为编译器提供了一种编译前的静态检查,在编译时不管你指定的泛型是什么,都会被视作Object,这是为了兼容JDK5之前的代码,这种叫做类型擦除,后文还会再讲.
类型通配符
如果Foo是Bar的一个子类型(子类或者子接口),而 G是具有泛型声明的类或接口,G<Foo>并不是G<Bar>的子类型!这一点非常值得注意,因为它与大部分人的习惯认为是不同的。
数组和泛型有所不同 ,假设Foo是Bar的一个子类型(子类或者子接口),那么Foo[]依然是Bar[]的子类型;但 G<Foo>不是G<Bar>的子类型。
1 | Integer[] ia = new Integer[5]; |
使用类型通配符
既然不能向上面那样表示父类,那就换个东西表示,这就是?
.类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?> (意思是元素类型未知的List)。这个问号(?) 被称为通配符, 它的元素类型可以匹配任何类型。所以?
其实不是代表父类,而是代表任意,只是效果相同.
1 | List<Integer> iList = new ArrayList(); |
但是?
仅仅是用来表示是父类,但是不能向里面添加任何东西(除了null).我们只能以Object的形式将集合里的元素取出来,却不能加进去.因为它有可能List<Object>,有可能是List<String>等等,而泛型的核心思想是:**把一个集合中的内容限制为一个特定的数据类型.**只要违背了这个原则就会引起编译错误.
泛型方法
在java中,泛型类和接口的定义非常简单,但是泛型方法就比较复杂了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
1 | /** |
请注意第二条.
泛型方法的基本用法
1 | public <T> T genericMethod(Class<T> tClass)throws InstantiationException , |
注意<T>,只有在方法中声明了泛型,这个方法才叫做泛型方法.除此之外的使用泛型类的标识的方法其实不是泛型方法.
也可以在一个泛型类中使用泛型方法:
1 | class Hero<T> implements Serializable { |
静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
因为静态方法是随着类加载就初始化的,而泛型是在实例化时才传入具体的泛型类型:
public static void show(T t){..}
这样是错误的.
泛型上下边界
使用?
带来的问题是,它太宽泛了.
? extends
ArrayList heroList<? extends Hero> 表示这是一个Hero泛型或者其子类泛型
heroList的泛型可能是Hero
heroList 的泛型可能是APHero
heroList 的泛型可能是ADHero
所以,可以确凿的是,从heroList取出来的对象,一定是可以转型成Hero的.但是,不能往里面放东西,因为放APHero就不满足<ADHero>,放ADHero又不满足<APHero>.即使只存在Hero和APHero也不行,因为ArrayList<Hero>和ArrayList<APHero>仍然不是一样东西.
1 | package generic; |
当然也可以不用这种方式:
1 | List<Hero> Heros = new ArrayList<>(); |
这样子也能添加,但取出来的是Hero.
? super
ArrayList heroList<? super Hero>表示这是一个Hero泛型或者其父类泛型
heroList的泛型可能是Hero
heroList的泛型可能是Object.
可以往里面插入Hero以及Hero的子类,但是取出来有风险,因为不确定取出来是Hero还是Object.这和上面的代码效果一样.
总结
- 如果希望只取出,不插入,就使用? extends Hero
- 如果希望只插入,不取出,就使用? super Hero
- 如果希望,又能插入,又能取出,就不要用通配符?
阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
1 | public class Collections { |
有限制泛型
1 | public class Node<T extends Comparable<T>> { |
类型参数T是有限制的,要求T必须继承Comparable接口,并且这个接口也是被泛型化的.