Java 泛型
1、泛型介绍
1.1、泛型概述
在前面学习集合时,我们都知道集合中是可以存放任意对象的,只要把对象丢尽集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对象,并且进行相应的操作,这时必须采用类型转换。
public class GenericDemo {
public static void main(String[] args) {
List list = new ArrayList();
list.add("abc");
list.add("itcast");
list.add(5);//由于集合没有做任何限定,任何类型都可以给其中存放
Iterator it = list.iterator();
while(it.hasNext()){
//需要打印每个字符串的长度,就要把迭代出来的对象转成String类型
String str = (String) it.next();
System.out.println(str.length());
}
}
}
程序在运行时发生了 java.lang.ClassCastException
为什么会发生类型转换异常呢?我们来分析下:
由于集合中什么类型的元素都可以存储。导致取出时,如果出现强转就会引发运行时 ClassCastException。怎么来解决这个问题呢?
我们知道在前面学习数组时,只要数组定义好,那么这时给数组中存放的数据类型就确定,这时如果给数组中存放的数据类型和数组类型不一致,编译时就会报错。
现在继续分析上述程序发生异常的原因是由于给集合中存放了不同类型的对象,才导致在取出类型转换发生异常。如果也能像数组一样,在定义集合时就明确集合中存放对象的类型,如果存放的类型不一致,直接在编译时就报错。这样的话,我们就能把运行时类型转换的异常避免掉。
JDK1.5以后,出现了解决方案,使用容器时,必须明确容器中元素的类型。这种机制称之为 :泛型
。
泛型格式:
<数据类型>
,这种格式不是很难理解,<>
尖括号也是括号,往括号里面写东西其实就是在传递参数。
泛型这种机制有啥好处呢:
1,安全机制。
2,将运行时期的ClassCastException
,转移到了编译时期变成了编译失败。
3,泛型技术,是给编译器使用的技术。
4,避免了强转的麻烦。
public class GenericDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("abc");
list.add("itcast");
//list.add(5);//当集合明确类型后,存放类型不一致就会编译报错
//集合已经明确具体存放的元素类型,那么在使用迭代器的时候,迭代器也同样会知道具体遍历元素类型
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str.length());
}
}
}
1.2、泛型简单使用
既然泛型了解泛型特点后,那么我们现在简单的使用下泛型。
定义容器,要求容器中元素唯一,并且按指定的方法排序。
思路:
1、元素唯一,那么只能使用Set集合
2、还要对元素排序,那么就使用TreeSet集合
3、还要按照指定方式排序,那么就要让集合自定具备比较功能,即就是要让要给集合传递一个比较器。
public class GenericDemo {
public static void main(String[] args) {
// 定义容器,要求容器中元素唯一,并且按指定的方法排序。并且使用匿名内部类的形式来完成比较器传递
TreeSet<String> set = new TreeSet<String>(new Comparator<String>(){
@Override
public int compare(String o1, String o2) {
int temp = o1.length() - o2.length();
return temp == 0 ? o1.compareTo(o2):temp;
}
});
set.add("abc");
set.add("bbb");
set.add("sh");
set.add("itcast");
for (String str : set) {
System.out.println(str);
}
}
}
2、泛型使用
2.1、泛型类的使用
泛型基本有了个了解之后,在这里要给大家说下,泛型这种技术是编译时期的技术,也就是说在编译的时候编译会根据所写泛型进行类型的检查,当编译通过后,生成的class文件中,是没有泛型这个机制的,这种机制也称为泛型的擦除。这个小知识,作为了解。
假如我们现在需要描述一个工具类用来获取和设置数据,怎么描述呢?
//这类只能设置和获取字符串
class Tool{
private String str;
public String getStr() {
return str;
}
public void setStr(String str) {
this.str = str;
}
}
//这个类只能设置获取Integer类型
class Tool2{
private Integer value;
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
}
上述代码明显有问题,由于要设置和获取的数据类型很多很多,那如何描述呢?于是我们想到了可以使用Object代表任何类型,代码进行改进
class Tool{
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
可是问题来了,这时当我们获取到数据之后都是Object类型,那么当要使用对象的特有方法,就要向下转型,这样就带来类型转换的异常发生风险。能不能不要转型我们存进去什么类型取出来就是什么类型呢?
即就是在使用这个类的时候,我们就应该知道我们要设置的是什么类型的数据,当然明确了设置,那么获取到的也是相应的类型。这时就可以适用泛型类
来解决这个问题。
class Tool<T>{
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
测试:
public class GenericDemo2 {
public static void main(String[] args) {
Tool<String> t = new Tool<String>();
t.setObj("itcast");
String s = t.getObj();
System.out.println(s);
}
}
2.2、泛型方法的使用
在前面学习中我们把泛型都定义在类上了,也就是说在创建类的对象时候,需要明确具体的类型,但是这个局限性很大,如果我们需要在使用类中某个方法时才能明确具体的类型那怎么办呢?
这时我们可以把泛型定义方法上,这样的方法就称为泛型方法。
class Utils<W>{
//打印功能的方法,可以打印任意类型,把泛型定义方法上
public <T> void print(T t){
System.out.println(t);
}
//如果方法是静态,那么方法上是无法使用类上的泛型,因为类上的泛型是随着对象的创建才明确出来的
public static <Q> void show(Q q){
System.out.println("show.."+q);
}
//也可以使用类上的泛型
public void method(W w){
System.out.println(w);
}
}
2.3、泛型接口的使用
当然泛型也可以定在接口中,定义在接口中的泛型,再使用的时候,如果子类已经明确具体的类型,那么子类在实现的时候就把类型明确出来,如果子类不明确具体类型,需要子类创建对象时才能明确,这时在子类描述时可以在子类上继续加泛型。
public class GenericDemo2 {
public static void main(String[] args) {
new InterImpl().show();
new InterImpl2<String>().show();
}
}
interface Inter<E>{
void show();
}
//子类明确具体类型
class InterImpl implements Inter<String>{
public void show(){
System.out.println("show run");
}
}
//如果子类不明确具体数据类型,这时可以在子类上继续使用泛型
class InterImpl2<T> implements Inter<T>{
public void show(){
System.out.println("show run");
}
}
3、泛型通配符
3.1、泛型通配符
如下代码演示了如何在开发使用泛型通配符
public class GenericDemo2 {
public static void main(String[] args) {
Set<Student> set = new HashSet<Student>();
set.add(new Student("zhangsan",31));
set.add(new Student("lisi",23));
set.add(new Student("wangwu",21));
List<Worker> list = new ArrayList<Worker>();
list.add(new Worker("xiaoqiang", 45));
list.add(new Worker("huaan", 41));
list.add(new Worker("daming", 47));
List<String> list2 = new ArrayList<String>();
list2.add("xiaoqiang");
list2.add("huaan");
list2.add("daming");
printCollection(list);
}
public static void printCollection(Collection<?> list) {
for (Iterator<?> it = list.iterator(); it.hasNext();) {
System.out.println(it.next());
}
}
}
上述代码中,由于定义打印集合的功能,应该是可以打印任意集合中元素的,这时根本无法确定具体集合中的元素类型是什么,就可以使用泛型的通配符
机制来完成。
总结:
当使用泛型类或者接口时,传递的具体的类型不确定,可以通过通配符(?
)表示。但是一旦使用泛型的通配符机制后,只能使用Object
类中的共性方法,集合中元素自身方法无法使用。
3.2、泛型限定
上述打印集合的功能,看似很强大,可以打印任意集合,可是问题也来了。如果想要对被打印的集合中的元素类型进行限制,只在指定的一些类型,进行打印。怎么做呢?
要解决这个问题,我们就要学习泛型的限定。
只需要打印学生和工人的集合。找到学生和工人的共性类型Person。
泛型的限定可以这样书写:
? extends Person : 接收Person类型或者Person的子类型。
public static void printCollection(Collection<? extends Person> list) {
for (Iterator<? extends Person> it = list.iterator(); it.hasNext();) {
System.out.println(it.next());
}
}
上述这种限定称为:限定泛型的上限:? extends E:接收E类型或者E的子类型。
当然有上限,肯定也有泛型的下限:? super E:接收E类型或者E的父类型。
3.3、通配符在API中的体现
查阅Collection接口,就可以直接看到其中使用通配符?,boolean containsAll(Collection<?> c),这里为什么直接使用的问号呢?这里需要大家查看continasAll的源码,而我们在程序中创建都是Collection接口的子类对象,假设我们创建的对象是ArrayList对象,查阅源码发现并没有在ArrayList中关于containsAll方法的复写,继续往上找,发现原来在AbstractList抽象类中对containsAll方法进行的实现,并且其中调用了contains方法,在继续查阅contains方法,最后发现使用的equals方法在比较,而使用equals方法比较的是任意的两个对象,那么在程序判断的一个集合中的元素在不在另外一个集合中包含,这个集合中的元素无法确定,只能使用通配符类标识。
3.4 、泛型限定在API中的体现
通过前面介绍泛型的限定有上限限定和下限限定,我们先来看关于上限限定的例子。
在API中TreeSet集合的构造函数中允许我们传递一个另外一个集合作为TreeSet集合的构造时的初始化数据,并且对传递的这个集合做了相应的限定。TreeSet(Collection<? extends E> c)
我们知道TreeSet集合在定义时要求必须明确其中存放对象的类型,即TreeSet集合的定义格式如下:
class TreeSet<E>{
TreeSet(Collection<? extends E> c){}
}
那么为什么TreeSet集合的构造函数要求传递数据类型必须是E的子类类型或者E本身呢?大家回想一下,TreeSet集合要保证排序,那么就必须要求在我们创建TreeSet对象是明确的类型本身必须具备比较功能(这个对象要实现Comparable接口),如果我们在创建TreeSet对象时明确数据类型,并且在创建TreeSet时给TreeSet中传递另外一个集合进行初始化,那么这个集合中的对象是不是必须保证和TreeSet对象要求的数据类型保持一致,或者是其子类(只要是其子类,当然就具备比较功能)呢。所以在设计TreeSet构造函数函数如果传递集合进行初始化动作,就使用到了泛型的上限。
再看TreeSet集合的另外一个构造函数TreeSet(Comparator<? super E> comparator) ,这又是什么意思呢?
class TreeSet<E>{
TreeSet(Comparator<? super E> c){}
}
其实我们知道TreeSet集合在定义是要求其中存放的对象类型必须是E类型,当我们给集合传递比较器后,每当给集合中存放元素,都会取出集合中已经存放的元素,并用其和正要存放元素进行比较。大家想想取出的元素要传递给比较器进行比较,那么比较器用什么来接受传递进来的这些对象呢,比较器用E本身接受是没有问题的,当然比较器用E的父类接受也是可以的,即就形成了多态,这样的话就形成了限定下限了。
4、泛型细节
4.1、泛型细节
泛型是在限定数据类型,当在集合或者其他地方使用到泛型后,那么这时一旦明确泛型标识的类型,那么在使用的时候只能给其传递和标注类型匹配的类型,否则就会报错。
ArrayList**** al = new ArrayList****();//这样语句属于语法错误,因为泛型限定不一致。
要么都限定为Dog,要么都限定为Animal,不能在限定的时候类型不匹配。
4.2、泛型限定的应用
案例:获取集合中元素的最大值。
思路:
1,定义变量记录每次比较后较大的值,初始化元素中任意一个。
2,遍历容器
3,在遍历中和变量中记录的元素进行比较。并将较大的值记录到变量中。
4,遍历结束,变量中记录的就是最大值。
public class GenericTest2 {
public static void main(String[] args) {
Collection<Student> c1 = new ArrayList<Student>();
c1.add(new Student("xiaoming1", 30));
c1.add(new Student("xiaoming2", 36));
c1.add(new Student("xiaoming3", 22));
Student stu = getMax(c1);
System.out.println(stu);
Collection<String> c2 = new ArrayList<String>();
c2.add("abcd");
c2.add("java");
c2.add("z");
c2.add("nba");
String s = getMax(c2);
System.out.println("s="+s);
}
// 要操作的元素的类型确定不?不确定。使用泛型限定。getMax方法接收的集合中的元素无论时什么类型,必须具备自然排序,必须是Comparable的子类。
public static <T extends Comparable<? super T>> T getMax(Collection<? extends T> c){
Iterator<? extends T> it = c.iterator();
T max = it.next();
while(it.hasNext()){
T temp = it.next();
if(temp.compareTo(max)>0){
max = temp;
}
}
return max;
}
}