在面向对象编程语言中,多态算是一种泛化机制。例如,你可以将方法的参数类型设置为基类,那么该方法就可以接受从这个基类中导出的任何类作为参数,这样的方法将会更具有通用性。此外,如果将方法参数声明为接口,将会更加灵活。
一、什么是泛型
在学习泛型之前,我们先看一个实例来真实的体验一下泛型带来的好处?
package study.javaenhance; import java.util.ArrayList;
import java.util.Collection; public class GenericTest
{
public static void main(String[] args)
{
//JDK1.5 前
ArrayList collection = new ArrayList();
collection.add("abc");
collection.add(1);
collection.add(1L);
for (int i=0;i<collection.size();i++)
{
String abc = (String) collection.get(i);
//1.必须强制转换为对应的类型;2.强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException
} } }
这样的实现面临两个问题:
1、当我们获取一个值的时候,必须进行强制类型转换。
2、存在潜在风险问题,没有对添加的数据提前编译的时候告知.假定我们预想的是利用collection来存放String集合,因为ArrayList只是维护一个Object引用的数组,我们无法阻止将Integer类型(Object子类)的数据加入collection。然而,当我们使用数据的时候,需要将获取的Object对象转换为我们期望的类型(String),如果向集合中添加了非预期的类型(如Integer),编译时我们不会收到任何的错误提示。但当我们运行程序时却会报异常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at study.javaenhance.GenericTest.main(GenericTest.java:17)
这显然不是我们所期望的,如果程序有潜在的错误,我们更期望在编译时被告知错误,而不是在运行时报异常。
那么,下面我们采用泛型来试试看呢?
package study.javaenhance; import java.util.ArrayList;
import java.util.Collection; public class GenericTest
{
public static void main(String[] args)
{
//JDK1.5 前
/*ArrayList collection = new ArrayList();
collection.add("abc");
collection.add(1);
collection.add(1L);
for (int i=0;i<collection.size();i++)
{
String abc = (String) collection.get(i);
//1.必须强制转换为对应的类型;2.强制转型编译时不会出错,而运行时报异常java.lang.ClassCastException
}*/ //jdk1.5 后泛型的使用
ArrayList<String> collection = new ArrayList<String>();
collection.add("abc");
//collection.add(1); //提示报错
//collection.add(1L); //提示报错
for (int i=0;i<collection.size();i++)
{
String abc = collection.get(i);
//不需要强制转换为对应的类型
} } }
可以看到当你定义为泛型以后,当你想集合中添加的元素的时候,只能添加进入String类型的元素,而不能添加进去int long 等其它类型进去了,可见在添加元素编译前就进行了校验了,解决了前面问题的第二点,另外取出元素的时候也不需要在强制转换了,解决了前面问题的第一点,可见完美的解决了前面的问题。
那么到底什么是泛型呢?概念如下:
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
结合上面的例子:ArrayList<String> collection = new ArrayList<String>();
这个时候ArrayList<String> 这个时候String 就是类型的实参,然后在ArrayList 类中会有形参进行接收,如下:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
....
.....
......
public E get(int index) {
RangeCheck(index); return (E) elementData[index];
}
可以看到,List接口中采用泛型化定义之后,<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。且get()方法的返回结果也直接是此类型形参(也就是对应的传入的类型实参)。
当然,我们也可以从用途来理解泛型:泛型是提供给编译器使用的,在编译阶段判断输入的数据是否合法,编译生成字节码后会去掉泛型的类型信息。
二、泛型的内部原理和应用
上面提到,泛型是提供给编译器使用的,在编译的阶段会判断输入的数据是否合法,编译生成字节码后会去掉泛型的信息,那么为了验证这个结论,我们可以看一个简单的例子:
//泛型仅仅在编译阶段用
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list1.getClass() == list2.getClass());
可以看到结果为true,
在这个例子中,我们定义了两个ArrayList数组,不过一个是ArrayList<String>泛型类型,只能存储字符串。一个是ArrayList<Integer>泛型类型,只能存储整型。最后,我们通过list2对象和list1对象的getClass方法获取它们的类信息并比较,发现结果为true。
这是为什么呢,明明我们定义了两种不同的类型?因为,在编译期间,所有的泛型信息都会被擦除,List<Integer>和List<String>类型,在编译后都会变成List类型(原始类型)。Java中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。
上述结论可通过下面反射的例子来印证:
//绕过编译阶段
ArrayList<String> a = new ArrayList<String>();
a.add("pony1223");
Class clazz = a.getClass();
Method method = clazz.getMethod("add",Object.class);
method.invoke(a,123);
System.out.println(a);
因为绕过了编译阶段也就绕过了泛型,输出结果为:
[pony1223, 123]
通过上面的学习,我们对泛型已经有了基本的认识,因此我们作一个总结即泛型中的一些术语:
ArrayList<E>类定义和ArrayList<Integer>类引用中涉及的术语:
1、 整个ArrayList<E>称为泛型类型
2、ArrayList<E>中E称为类型变量或类型形参
3、整个ArrayList<Integer>称为参数化的类型
4、ArrayList<Integer>中的Integer叫类型参数的实例或类型实参
5、ArrayList<Integer>中的<>念typeof
6、ArrayList称为原始类型
Collection<String> = new Vector(); Collection = new Vector<String >(); //参数化类型与原始类型的兼容性-编译警告
Vector<String> v = new Vector<Object>(); //错,参数化类型不考虑类型参数的继承关系
Vector<Object> v = new Vector<String>();//错
Vector<Integer> v[] = new Vector<Integer>[10]; //错,在创建数组实例时,数组的元素不能使用参数化的类型
Vector v1 = new Vector<Integer>(); Vector<Object> v = v1; //编译可以通过!编译器只会按行解释
三、泛型的通配符
为了引出通配符,我们先看一个例子:
//通配符
List<Integer> ext1 = new ArrayList<Integer>();
List<Number> ext2 = ext1; //编译器报错,提示不能转换类型,注意再次强调编译器是按行解释的,不要认为进行运行操作
//这种是正确的
List ext3 = new ArrayList<Integer>();
List<Number> ext4 = ext3; //编译器不会报错兼容性,所以不要认为这里等效于List<Number> = new ArrayList<Integer>
List<Number> ext5 = new ArrayList<Integer>(); //报错
List<Integer> ext1 = new ArrayList<Integer>();
//List<Number> ext2 = ext1;
上面会报错原因在于:ext1 和 ext2 是两个不同的类型,要想相等,必须有共同的父类引用类型.
因为Integer虽然是Number的子类,但List<Integer>不是List<Number>的子类型。假定代码没有问题,那么我们可以使用语句ext2.add(newDouble())在一个List中装入了各种不同类型的子类,这显然是不可以的,因为我们在取出List中的对象时,就分不清楚到底该转型为Integer还是Double了。
因此,我们需要一个在逻辑上可以用来同时表示为List<Integer>和List<Number>的父类的一个引用类型,类型通配符应运而生。在本例中表示为List<?>即可。如下例子:
//通配符
List<Integer> ext1 = new ArrayList<Integer>();
//List<Number> ext2 = ext1; //编译器报错,提示不能转换类型,注意再次强调编译器是按行解释的,不要认为进行运行操作
//这种是正确的
List ext3 = new ArrayList<Integer>();
List<Number> ext4 = ext3; //编译器不会报错兼容性,所以不要认为这里等效于List<Number> = new ArrayList<Integer>
//List<Number> ext5 = new ArrayList<Integer>(); //报错
//通配符应用
List<String> ext5 = new ArrayList<String>();
printCollection(ext1);
printCollection(ext4);
printCollection(ext5); } private static void printCollection(List<?> list) {
for(int i=0;i<list.size();i++)
{
System.out.println(list.get(i));
}
}
注意点:
private static void printCollection(List<?> list) {
for(int i=0;i<list.size();i++)
{
System.out.println(list.get(i));
}
list.add("string");//错 ,因为它不知道将来传递进来的一定是String
list.size();//对,此方法与类型参数没有关系
list = new ArrayList<Date>();//对 ? 通配符任意类型
}
通配符知识扩展:
<?>可以引用各种参数化的类型,可以调用与参数无关的方法,不能调用与参数有关的方法
限定通配符的上边界
正确:Vector<? extends Number> v=new Vector<Integer>();
错误:Vector<? extends Number> v=new Vector<String>();
限定通配符的下边界
正确:Vector<? Super Integer> v=new Vector<Number>();
错误:Vector<? extends Integer > v=new Vector<Byte>();
泛型的综合应用:
private static void pringMap() {
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("abc",1);
map.put("def",2);
map.put("ghj",3); Set<Map.Entry<String,Integer>> set = map.entrySet();
for (Entry<String, Integer> entry : set) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
} }
四、自定义泛型类、泛型方法和应用
首先我们了解什么是泛型类:定义泛型类,就是在类名的后面加上泛型标记。为什么会这样用呢?看下面一个例子:
自定义泛型的方法呢?在返回值的类型之前加上一个说明<T>
package study.javaenhance; /**
* 基本的增删改查
* @author Pony
*
*/
public class GenericDao
{
public <T> void add(T t){} public <T> T findById(int id){return null;} public <T> void delete(T t){} public <T> T update(T t){return null;}
}
我们可以看到在没有使用泛型类的时候需要在每个泛型方法的前面都加上<T>;那么有没有办法统一起来呢,不要去重复写呢?那么就是泛型类出来了,改写如下:
package study.javaenhance; /**
* 基本的增删改查
* @author Pony
*
*/
public class GenericDao<T>
{
public void add(T t){} public T findById(int id){return null;} public void delete(T t){} public T update(T t){return null;}
}
可以看到省掉了每个方法前面的<T>.即如果类的实例对象中的多处要用到同一个泛型参数,即这些地方引用的泛型类型要保持同一个实际类型时,这个时候就要采用泛型类型的方式进行定义,也就是类级别的泛型,如上就是。
类级别的泛型是根据引用该类名时指定的类型信息来参数化类型变量的,就是前面提到的类型实参来初始化类型的形参。
注意的是:
1.在对泛型类型进行参数化的时候,类型实参的实例必须是引用类型,不能是基本类型。
2.当一个变量被声明为泛型的时候,只能被实例变量和方法调用,而不能被静态变量和方法调用,因为静态的成员是被所有的参数化的类共享的,所以静态成员是不应该有类级别的类型参数的 ,可以定义为方法级别的。
自定义泛型的方法,前面说到过,就是在返回值的类型前面加上一个说明<T>
下面通过一个例子来看自定义泛型方法:
//不采用泛型方法
add(1,2);
add(1.0,2.0);
add(1.0,2.0);
} public static int add(int x,int y){return x+y;}
public static float add(float x,float y){return x+y;}
public static double add(double x,double y){return x+y;}
在没有实现泛型之前,我们需要通过重载的方式进行,但明明功能是一样的,因为类型不同导致的,那么有没有办法来解决通用的类型接受,然后返回对应的类型呢?可以采用自定义泛型方法解决。
//不采用泛型方法
add(1,2);
add(1.0,2.0);
add(1.0,2.0);
} public static <T> T add(T x ,T y){return null;}
那么返回的T 到底是什么类型呢? 我们推测尝试看下:
Integer result1 = add(1,2);
float result2 = add(1.0,2.0); //报错
double result3 = add(1.0,2.0);
String result4 = add(1.0,"abc");//报错
这个例子貌似不太实用,而我们只是想通过这个例子说明怎么自定义泛型.以及如何进行泛型的推断.我们可以推断,Integer+String -->Object,推断出这两个T应该是Object. Integer + float --> Number,推断出这两个T应该是Number类型.
下面我们做一道题目为:交换任意数组中的任意两个元素的位置.
思考,这里是要任意数组中的两个元素的位置,因此可以看出,类型不能够写死,那么我们需要定义为泛型,如下:
//调用方法
swap(new String[]{"a","b","c","d"},1,2);//OK 没有问题
swap(new int[]{1,2,3,4},1,2);//报错,错误提示为:The method swap(T[], int, int) in the type GenericTest is not applicable for the arguments (int[], int, int) } public static <T> void swap(T[] arr,int a,int b)
{
T temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
错误提示为,没法转换到类型int[],那么为什么第一个是正确的呢?这里需要注意的是:因为int是基本类型,只有引用类型才能作为泛型的实际参数,(泛型的基本类型只能是引用类型,不能是值类型);虽然int[] 是Object类型,但int是基本类型。
总结:基本类型是不能使用泛型T;即如果实参传递给形参的时候,形参是泛型的时候,实参是不能是基本类型的。
可能会有人会有疑问,为什么上面的add(1,2) 没有报错,里面传递的是int类型,这里是因为int 在传递给泛型接受的时候,发现它不是引用类型,于是就自动进行装箱和拆箱的操作,装成了引用类型,而int[] 类型,不进行装箱和拆箱操作是因为int[] 本身是引用类型,泛型就不会将一个引用类型再进行装箱和拆箱操作了,但是int[] 虽然是引用类型,但里面的每一个元素却会是int类型,那么在里面就会出错了,因为泛型只能用于引用类型。
在定义泛型的时候,和通配符号? 一样,也可以使用extend限定符号。也就是说在<> typeof 里面当成类型和返回值的时候也可以用限定符,不是仅仅做参数列表的时候可以用。
总结:
1.方法泛型类型的参数的尖括号应当出现在方法的其他所有修饰符之后和方法的返回类型之前,也就是说紧邻返回值之前。按照惯例,类型的形参通常用单个大写字母来表示。
2.除了在应用泛型的时候可以加上使用extends 限定符,在定义泛型的时候也可以使用extends限定符号,可以参见上面的例子。
3.普通方法,构造方法和静态方法都可以使用泛型方法,编译器也不允许创建类型变量的数组。
4.亦可以用类型变量来表示一次,称为参数化的异常,可以用于方法的throws列表中,但是不能用于catch子句中。
5.在泛型中可以同时又多个类型参数,在定义他们的尖括号中用逗号分隔开,比如Map<K,V>
五、自定义泛型方法的练习与类型推断总结
1.编写一个泛型方法,自动将Object类型的对象转换成其他类型
Object obj = "abc";
String x3 = autoConvert(obj);
} private static <T> T autoConvert(Object obj){
return (T)obj;
}
2.定义一个方法,可以将任意类型的数组中的所有元素填充为相应类型的某个对象.
private static <T> void fillArray(T[] a,T obj){
for(int i=0;i<a.length;i++){
a[i] = obj;
}
}
3.采用自定义泛型方法的方式,打印出集合中的所有的内容
public static void printCollection(Collection<?> collection){
//collection.add(1);
System.out.println(collection.size());
for(Object obj : collection){
System.out.println(obj);
}
} public static <T> void printCollection2(Collection<T> collection){
//collection.add(1);
System.out.println(collection.size());
for(Object obj : collection){
System.out.println(obj);
} }
总结:
1.编译器判断泛型方法的实际类型参数的过程称为类型推断,类型推断是相对于直觉推断,其实实现方法是一种非常复杂的过程。
2.根据调用泛型方法时实际传递参数类型或返回值得类型来进行推断,具体规则如下:
(1)当某个类型变量只在整个参数列表中的所有参数和返回值中的一处被调用,那么根据调用方法时该处的实际应用类型来确定,这很容易凭感觉推断出来,即直接根据调用方法时候传递的参数类型或返回值来确定泛型参数的类型,如:swap(new String[3],3,4) ----> static <E> void swap(E[] a,int i,int b)
(2)当某个类型变量在整个参数类别中的所有参数和返回值中的多处调用了,如果调用方法时这多处的实际应用类型都是对应同一种类型,那么这个也很容易推断出来:如:
add(3,5) ---> static <T> T add(T a,T b)
(3)当某个类型变量在整个参数列表中的所有类型和返回值多处应用,如果调用方法时候这多处的实际应用类型对应到了不同的类型上面,并且没有使用返回值,这个时候取多个实际参数中的最大交际类型,例如:下面实际语句实际对应的类型就是Number类型了,编译没有问题,但是运行的时候会出现错误:
fill(new Integer[3],3.5f) ---->static <T> void fill(T[] a,T v);
(4)当某个类型变量在参数列表中的所有参数和返回值多处应用,如果调用方法时候这个多处的实际应用对应不同的类型,并且使用了返回值,这个时候优先会考虑返回值额类型
(5)参数类型的推断具有传递性。
六、通过反射获得泛型的实际类型参数
Vector<Date> v = new Vector<Date>();
通过v.getClass()是无法获得泛型的参数化类型的。
将其传递给一个方法,可实现此功能.
public static void applyVector(Vector<Date> v)
{
}
如下:
Method applyMethod = GenericTest.class.getMethod("applyVector", Vector.class);
Type[] types = applyMethod.getGenericParameterTypes();
ParameterizedType pType = (ParameterizedType)types[0];
System.out.println(pType.getRawType());//Vector
System.out.println(pType.getActualTypeArguments()[0]);//Date
七、泛型相关面试题目
1. Java中的泛型是什么 ? 使用泛型的好处是什么?
泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。
泛型是一种编译时类型确认机制。它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。
2、Java的泛型是如何工作的 ? 什么是类型擦除 ?
泛型的正常工作是依赖编译器在编译源码的时候,先进行类型检查,然后进行类型擦除并且在类型参数出现的地方插入强制转换的相关指令实现的。
编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List类型来表示。为什么要进行擦除呢?这是为了避免类型膨胀。
3. 什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。
4. List<? extends T>和List <? super T>之间有什么区别 ?
这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。在本段出现的连接中可以找到更多信息。
5. 如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:
public V put(K key, V value) {
return cache.put(key, value);
}
6. Java中如何使用泛型编写带有参数的类?
这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。
7. 编写一段泛型程序来实现LRU缓存?
对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。
8. 你可以把List<String>传递给一个接受List<Object>参数的方法吗?
对任何一个不太熟悉泛型的人来说,这个Java泛型题目看起来令人疑惑,因为乍看起来String是一种Object,所以List<String>应当可以用在需要List<Object>的地方,但是事实并非如此。真这样做的话会导致编译错误。如果你再深一步考虑,你会发现Java这样做是有意义的,因为List<Object>可以存储任何类型的对象包括String, Integer等等,而List<String>却只能用来存储Strings。
List<Object> objectList;
List<String> stringList; objectList = stringList; //compilation error incompatible types
9. Array中可以用泛型吗?
这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
10. 如何阻止Java中的类型未检查的警告?
如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告
,例如List<String> rawList = new ArrayList()
注意: Hello.java使用了未检查或称为不安全的操作;
这种警告可以使用@SuppressWarnings("unchecked")注解来屏蔽。
11、Java中List<Object>和原始类型List之间的区别?
原始类型和带参数类型<Object>之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用Object作为类型,可以告知编译器该方法可以接受任何类型的对象,比如String或Integer。这道题的考察点在于对泛型中原始类型的正确理解。它们之间的第二点区别是,你可以把任何带参数的泛型类型传递给接受原始类型List的方法,但却不能把List<String>传递给接受List<Object>的方法,因为会产生编译错误。
12、Java中List<?>和List<Object>之间的区别是什么?
这道题跟上一道题看起来很像,实质上却完全不同。List<?> 是一个未知类型的List,而List<Object>其实是任意类型的List。你可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给List<Object>。
List<?> listOfAnyType;
List<Object> listOfObject = new ArrayList<Object>();
List<String> listOfString = new ArrayList<String>();
List<Integer> listOfInteger = new ArrayList<Integer>(); listOfAnyType = listOfString; //legal
listOfAnyType = listOfInteger; //legal
listOfObjectType = (List<Object>) listOfString; //compiler error - in-convertible types
13、List<String>和原始类型List之间的区别.
该题类似于“原始类型和带参数类型之间有什么区别”。带参数类型是类型安全的,而且其类型安全是由编译器保证的,但原始类型List却不是类型安全的。你不能把String之外的任何其它类型的Object存入String类型的List中,而你可以把任何类型的对象存入原始List中。使用泛型的带参数类型你不需要进行类型转换,但是对于原始类型,你则需要进行显式的类型转换。
List listOfRawTypes = new ArrayList();
listOfRawTypes.add("abc");
listOfRawTypes.add(123); //编译器允许这样 - 运行时却会出现异常
String item = (String) listOfRawTypes.get(0); //需要显式的类型转换
item = (String) listOfRawTypes.get(1); //抛ClassCastException,因为Integer不能被转换为String List<String> listOfString = new ArrayList();
listOfString.add("abcd");
listOfString.add(1234); //编译错误,比在运行时抛异常要好
item = listOfString.get(0); //不需要显式的类型转换 - 编译器自动转换
参考资料:
张孝祥老师 JAVA增强视频
http://www.cnblogs.com/lwbqqyumidi/p/3837629.html
http://www.cnblogs.com/lzq198754/p/5780426.html
http://blog.csdn.net/sunxianghuang/article/details/51982979