介绍
下面是那种典型用法:
List myIntList = new ArrayList();// 1
myIntList.add(new Integer(0));// 2
Integer x = (Integer) myIntList.iterator().next();// 3
第3 行的类型转换有些烦人,但是,这个类型转换是必须的(essential)。通常情况下,程序员知道一个特定的list里边放的是什么类型的数据。编译器只能保证iterator 返回的是Object 类型。为了保证对Integer 类型变量赋值的类型安全,必须进行类型转换。
当然,因为程序员可能不清楚他们的类型,导致这个类型转换有可能产生一个运行时错误 (run time error)。而如何把一个list(集合) 中的内容限制为一个特定的数据类型呢?这就是generics 背后的核心思想。这是上面程序片断的一个泛型版本:
List<Integer> myIntList = new ArrayList<Integer>(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = myIntList.iterator().next(); // 3
注意变量myIntList 的类型声明。它指定这不是一个任意的List,而是一个Integer 的List。我们说List是一个带一个类型参数的泛型接口(a generic interface that takes a type parameter),本例中,类型参数是Integer。我们在创建这个List 对象的时候也指定了一个类型参数。另一个需要注意的是第3行没了类型转换。
现在,你可能认为我们已经成功地去掉了程序里的混乱。我们用第1行的类型参数取代了第3 行的类型转换。然而,这里还有个很大的不同。编译器现在能够在编译时检查程序的正确性。当我们说myIntList 被声明为ist<Integer>类型,这告诉我们无论何时何地使用myIntList 变量,编译器保证其中的元素的正确的类型。实际结果是,这可以增加可读性和稳定性(robustness),尤其在大型的程序中。
用法:
List list = new ArrayList();// 1
list .add(new Integer(12));// 2
Integer x = (Integer) list .iterator().next();// 3
// 第3 行的类型转换有些烦人,为了保证对Integer 类型变量赋值的类型安全,必须进行类型转换。
//当然,因为程序员可能不清楚他们的类型,导致这个类型转换有可能产生一个运行时错误。
//而如何把一个list(集合) 中的内容限制为一个特定的数据类型呢?
//这就是generics背后的核心思想。这是上面程序片断的一个泛型版本:
List<Integer> list = new ArrayList<Integer>(); // 1
list.add(new Integer(12)); // 2
Integer x = list.iterator().next(); // 3
//注意第1行变量list的类型声明。
//它指定这不是一个任意的List,而是一个Integer 的List。
//我们说List是一个带一个类型参数的泛型接口,我们在创建这个List 对象的时候指定了一个Integer类型参数是。
//另一个需要注意的是第3行没了类型转换。
现在,我们用第1行的类型参数取代了第3 行的类型转换。然而,这里还有个很大的不同。编译器现在能够在编译时检查程序的正确性。当我们说list 被声明为ist<Integer>类型,这告诉我们无论何时何地使用list 变量,编译器保证其中的元素的正确的类型。实际结果是,这可以增加可读性和稳定性,尤其在大型的程序中。
泛型的设计背景
Java中的泛型是什么 ?
所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,在集合中存储对象并在使用前进行类型转换是很不方便。
JDK1.5之后使用泛型来解决。这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection<E>,List<E>,ArrayList<E>这个<E>就是类型参数,即泛型。允许我们在创建集合时再指定集合元素的类型,正如:List<String>,这表明该List只能保存字符串类型的对象。
JDK1.5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
使用泛型的好处是什么?
它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
在集合中没有泛型时任何类型都能够加入集合中,类型不安全,读出来的时候还需要强转。
在集合中有泛型时只有指定类型才能添加到集合中,类型是安全的,读出来的时候不需要强转,很便捷。
定义简单的泛型
下面是从java.util 包中的List 接口和Iterator 接口的定义中摘录的片断:
public interface List<E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
这些都应该是很熟悉的,除了尖括号中的部分,那是接口List 和Iterator 中的形式类型参数的声明(the declarations of the formal type parameters of the interfaces List and Iterator)。类型参数在整个类的声明中可用,几乎是所有可以使用其他普通类型的地方
在介绍那一节我们看到了对泛型类型声明List (the generic type declaration List) 的调用,如List<Integer>。在这个调用中(通常称作一个参数化类型a parameterized type),所有出现的形式类型参数(formal type parameter,这里是E)都被替换成实体类型参数(actual type argument)(这里是Integer)。
你可能想象,List<Integer>代表一个E 被全部替换成Integer 的版本:
public interface IntegerList {
void add(Integer x)
Iterator<Integer> iterator();
}
类型参数就跟在方法或构造函数中普通的参数一样。就像一个方法有形式参数(formal value parameters)来描述它操作的参数的种类一样,一个泛型声明也有形式类型参数(formal type parameters)。当一个方法被调用,实参(actual arguments)替换形参,方法体被执行。当一个泛型声明被调用,实际类型参数(actual type arguments)取代形式类型参数。一个命名的习惯:推荐用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母
在集合中使用泛型
自定义泛型结构
泛型的声明
Interface List<T> 和class GenTest<K,V>
//其中,T,K,V,E不代表值,而是表示类型。这里使用任意字母都可以。
//常用T表示,是Type的缩写。
泛型的实例化:
一定要在类名后面指定类型参数的值(类型)。如:
List<String> strList= new ArrayList<String>();
Iterator<Customer> iterator = customers.iterator();
T只能是类,不能用基本数据类型填充。但可以使用包装类填充,把一个集合中的内容限制为一个特定的数据类型,这就是generics背后的核心思想
Comparable c = new Date();
System.out.println(c.compareTo("red"));
Comparable<Date> c = new Date();
System.out.println(c.compareTo("red"));// CUO
使用泛型的主要优点是能够在编译时而不是在运行时检测错误。
自定义泛型类, 泛型接口
- 1.泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>
- 2.泛型类的构造器如下:public GenericClass(){}。
- 而下面是错误的:public GenericClass<E>(){}
- 3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
- 4. 泛型不同的引用不能相互赋值。
- 尽管在编译时ArrayList<String>和ArrayList<Integer>是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。
- 5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。经验:泛型要使用一路都用。要不用,一路都不要用。
- 6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
- 7. jdk1.7,泛型的简化操作:ArrayList<Fruit> flist = new ArrayList<>();
- 8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换。
- 9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
- 10. 异常类不能是泛型的
- 11. 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];
- 参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。
- 12.父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:
- 子类不保留父类的泛型:按需实现
- 没有类型擦除
- 具体类型
- 子类保留父类的泛型:泛型子类
- 全部保留
- 部分保留
- 结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
自定义泛型方法
方法,也可以被泛型化,不管此时定义在其中的类是不是泛型类。在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的类型。
泛型方法的格式:
[访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称])抛出的异常
泛型方法声明泛型时也可以指定上限
/**
* 泛型方法的格式
* [访问权限] <泛型> 返回类型 方法名([泛型标识 参数名称])抛出的异常
* 泛型方法声明泛型时也可以指定上限
**/
public class DAO {
public<E> E get(intid, E e) {
E result= null;
return result;
}
}
泛型和子类继承
让我们测试一下我们对泛型的理解。下面的代码片断合法么?
List<String> ls = new ArrayList<String>(); //1
List<Object> lo = ls; //2
第1 行当然合法,但是这个问题处在于第2 行。这产生一个问题:一个String 的List 是一个Object 的List 么?大多数人的直觉是回答: “当然!”。因为乍看起来String是一种Object,所以List<String>应当可以用在需要List<Object>的地方,但是事实并非如此。真这样做的话会导致编译错误。
好,在看下面的几行
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 试图把Object 赋值给String
这里,我们使用lo 指向ls。我们通过lo 来访问ls,一个String 的list。我们可以插入任意对象进去。结果是ls 中保存的不再是String。当我们试图从中取出元素的时候,会得到意外的结果。java 编译器当然会阻止这种情况的发生。第2 行会导致一个编译错误。总之,如果Foo 是Bar 的一个子类型(子类或者子接口),而G 是某种泛型声明,那么G<Foo>是G<Bar>的子类型并不成立!!
如果你再深一步考虑,你会发现Java这样做是有意义的,因为List<Object>可以存储任何类型的对象包括String, Integer等等,而List<String>却只能用来存储Strings。
通配符(Wildcards)
考虑写一个例程来打印一个集合(Collection)中的所有元素。下面是在老的语言中你可能写的代码:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (int k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
下面是一个使用泛型的幼稚的尝试(使用了新的循环语法):
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
一个集合,它的元素类型可以匹配任何类型。显然,它被称为通配符。我们可以写:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
现在,我们可以使用任何类型的collection 来调用它。注意,我们仍然可以读取c 中的元素,其类型是Object。这永远是安全的,因为不管collection 的真实类型是什么,它包含的都是Object。但是将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误
因为我们不知道c 的元素类型,不能添加对象。add 方法有类型参数E 作为集合的元素类型。我们传给add 的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。唯一的例外是null,它是所有类型的成员。另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object
有限制的通配符(Bounded Wildcards)
限定通配符和非限定通配符。List<? extends T>和List <? super T>这两个List的声明都是限定通配符的例子:
List<? extends T>可以接受任何继承自T的类型的List.
List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>
泛型方法
考虑写一个方法,它用一个Object 的数组和一个collection 作为参数,完成把数组中所有object 放入collection 中的功能。下面是第一次尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // 编译期错误
}
}
把对象放进一个未知类型的集合中。办法是使用generic methods。就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for (T o : a) {
c.add(o); // correct
}
}
我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T 指Object
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
fromArrayToCollection(sa, cs);// T inferred to be String
fromArrayToCollection(sa, co);// T inferred to be Object
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
fromArrayToCollection(ia, cn);// T inferred to be Number
fromArrayToCollection(fa, cn);// T inferred to be Number
fromArrayToCollection(na, cn);// T inferred to be Number
fromArrayToCollection(na, co);// T inferred to be Object
fromArrayToCollection(na, cs);// compile-time error
}
static <T> void fromArrayToCollection (T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // correct
}
}
注意,我们并没有传送真实类型参数(actual type argument)给一个泛型方法。编译器根据实参为我们推断类型参数的值。它通常推断出能使调用类型正确的最明确的类型参数。
Java的泛型是如何工作的 ? 什么是类型擦除 ?
这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List<String>在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。
什么是泛型中的限定通配符和非限定通配符 ?
这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面<?>表示了非限定通配符,因为<?>可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。
List<? extends T>和List <? super T>之间有什么区别 ?
这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List<Integer>或List<Float>。在本段出现的连接中可以找到更多信息。
如何编写一个泛型方法,让它能接受泛型参数并返回泛型类型?
编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:
public V put(K key, V value) {
return cache.put(key, value);
}
Java中如何使用泛型编写带有参数的类?
这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。
编写一段泛型程序来实现LRU缓存?
对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。
你可以把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
Array中可以用泛型吗?
这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
如何阻止Java中的类型未检查的警告?
如果你把泛型和原始类型混合起来使用,例如下列代码,Java 5的javac编译器会产生类型未检查的警告,例如
List<String> rawList = new ArrayList()