1、泛型概述
泛型(Generics)是自JDK5.0开始引入的一种Java语言新特性,其实质是将原本确定不变的数据类型参数化,作为对原有Java类型体系的扩充,使用泛型可以提高Java应用程序的类型安全、可维护性和可靠性。
泛型是指参数化类型的能力。可以定义带泛型类型的类或方法,随后编译器会用具体的类型来替换他。泛型就是添加了一个类型参数,你可以在用泛型类或者泛型方法的时候确定这个泛型为一个确定的类型。在以前的java版本中是没有泛型的只能用根类Object来表示泛型,但是这样的话就不能表示一个确定的类型,因为object是所有类的父类,所以它表示所有类型。
泛型允许编译器实施由开发者设定的附加类型约束,将类型检查从运行时挪到编译时进行,这样类型错误可以在编译时暴露出来,而不是在运行时才发作,这非常有助于早期错误检查,提高程序的可靠性,同时还可以减少强制类型转换的编码量。
java中加入了泛型以后,所有的集合框架都重新写了,使它们支持泛型。这样你就可以这样写:
ArrayList<String> al=new ArrayList<String>();
表示一个String型的Arraylist,但是泛型有一个问题就是它不支持基本类型作为类型参数。Java中的集合可以添加任何类型的对象,所有的元素均当作Object类型来处理,当从集合中取出元素时,必须进行强制类型转换。而使用泛型可以在创建集合时指定其允许保存的元素类型,然后由编译器负责添加元素的类型合法性检查,而从集合中取出元素时,也不需要强制类型转换了。
泛型思想早在C++语言的模板(Templates)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,那Object转型为任何对象成都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会被转嫁到程序运行期中。
Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经被替换为原来的原始类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList与ArrayList就是同一个类。所以说泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。使用泛型机制编写的程序代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类来说尤其有用。泛型程序设计(Generic Programming)意味着编写的代码可以被很多不同类型的对象所重用。
在JDK1.5之前,Java泛型程序设计是用继承来实现的。因为Object类是所有类的基类,所以只需要维持一个Object类型的引用即可。就比如ArrayList只维护一个Object类型的数组:
public class ArrayList{ //JDK1.5之前的
public Object get(int i){......}
public void add(Object o){......}
......
private Object[] elementData;
}
这样会有两个问题:
1、没有错误检查,可以向数组列表中添加类的对象
2、在取元素的时候,需要进行强制类型转换
这样,很容易发生错误,比如:
/*jdk1.5之前的写法,容易出问题*/
ArrayList arrayList1=new ArrayList();
arrayList1.add(1);
arrayList1.add(1L);
arrayList1.add("asa");
int i=(Integer) arrayList1.get(1);
这里的第一个元素是一个长整型,而你以为是整形,所以在强转的时候发生了错误。所以在JDK1.5之后,加入了泛型来解决类似的问题。例如在ArrayList中使用泛型:
ArrayList<String> arrayList2=new ArrayList<String>(); //限定数组列表中的字符串类型
// arrayList2.add(1); //因为限定了类型,所以不能添加整形
// arrayList2.add(1L);//因为限定了类型,所以不能添加整长形
arrayList2.add("asa");//只能添加字符串
String str=arrayList2.get(0);//因为知道取出来的值的类型,所以不需要进行强制类型转换
还要明白的是,泛型特性是向前兼容的。尽管JDK5.0的标准类库中的许多类,比如集合框架,都已经泛型化了,但是使用集合类(比如HashMap和ArrayList)的现有代码可以继续不加修改地在JDK1.5中工作。当然,没有利用泛型的现有代码将不会赢得泛型的类型安全的好处。
在学习泛型之前,简单介绍下泛型的一些基本术语,以ArrayList和ArrayList做简要介绍:
- 整个称为ArrayList泛型类型。
- ArrayList中的 E称为类型变量或者类型参数。
- 整个ArrayList称为参数化的类型。
- ArrayList中的integer称为类型参数的实例或者实际类型参数。
- ArrayList中的念为typeof Integer。
- ArrayList称为原始类型。
2、泛型的使用
泛型:是一种把明确类型的工作推迟到创建对象或者调用方法的时候才去明确的特殊的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,而这种参数类型可以用在类、方法和接口中,分别被称为泛型类、泛型方法、泛型接口。
注意:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。
优点:
- 避免了类型强转的麻烦。
- 它提供了编译期的类型安全,确保在泛型类型(通常为泛型集合)上只能使用正确类型的对象,避免了在运行时出现ClassCastException。
泛型虽然通常会被大量的使用在集合当中,但是我们也可以完整的学习泛型知识。泛型有三种使用方式,分别为:泛型类、泛型方法、泛型接口。将数据类型作为参数进行传递。
1.泛型类的定义和使用
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种集合框架容器类,如:List、Set、Map。一个泛型类(generic class)就是具有一个或多个类型变量的类。定义一个泛型类十分简单:
修饰符 class 类名<代表泛型的变量> { }
/**
* @param <T> 这里解释下<T>中的T:
* 此处的T可以随便写为任意标识,常见的有T、E等形式的参数表示泛型
* 泛型在定义的时候不具体【Object类型】,使用的时候才变得具体。
* 在使用的时候确定泛型的具体数据类型。即在创建对象的时候确定泛型。
*/
public class Pair<T> {
//t这个成员变量的类型为T,T的类型由外部指定
private T t;
//泛型构造方法形参t的类型也为T,T的类型由外部指定
public Pair(T t) {
this.t=value;
}
//泛型方法getT的返回值类型为T,T的类型由外部指定
public T getValue() {
return t;
}
public void setValue(T value) {
this.t = value;
}
}
泛型在定义的时候不具体,使用的时候才变得具体。在使用的时候确定泛型的具体数据类型。即:在创建对象的时候确定泛型T的类型。
现在我们就可以使用这个泛型类了:
public static void main(String[] args) throws ClassNotFoundException {
Pair<String> pair=new Pair<String>("Hello");
String str=pair.getValue();
System.out.println(str);
pair.setValue("World");
str=pair.getValue();
System.out.println(str);
}
Pair类引入了一个类型变量T,用尖括号<>括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个域和第二个域使用不同的类型:
public class Pair{......}
注意:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示关键字与值的类型。(需要时还可以用临近的字母U和S)表示“任意类型”。
2.泛型接口的定义和使用
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中。
定义格式:
修饰符 interface接口名<代表泛型的变量> { }
看一下下面的例子,你就知道怎么定义一个泛型接口了:
/**
* 定义一个泛型接口
*/
public interface GenericsInteface<T> {
public abstract void add(T t);
}
使用格式
(1). 定义类时确定泛型的类型
public class GenericsImp implements GenericsInteface<String> {
@Override
public void add(String s) {
System.out.println("设置了泛型为String类型");
}
}
(2).始终不确定泛型的类型,直到创建对象时,确定泛型的类型
public class GenericsImp<T> implements GenericsInteface<T> {
@Override
public void add(T t) {
System.out.println("没有设置类型");
}
}
// 在应用中确定类型
public class GenericsTest {
public static void main(String[] args) {
GenericsImp<Integer> gi = new GenericsImp<>();
gi.add(66);
}
}
3.泛型方法的定义和使用
泛型方法,是在调用方法的时候指明泛型的具体类型 。
泛型类在多个方法签名间实施类型约束。在List中,类型参数V出现在get()、add()、contains()等方法的签名中。当创建一个Map类型的变量时,您就在方法之间宣称一个类型约束。您传递给add()的值将与get()返回的值的类型相同。类似地,之所以声明泛型方法,一般是因为想要在该方法的多个参数之间宣称一个类型约束。
- 定义格式:
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ }
public class Pair<T> {
//t这个成员变量的类型为T,T的类型由外部指定
private T t;
//泛型构造方法形参t的类型也为T,T的类型由外部指定
public Pair(T t) {
this.t=value;
}
//泛型方法getT的返回值类型为T,T的类型由外部指定
public T getValue() {
return t;
}
public void setValue(T value) {
this.t = value;
}
}
或者
public class GenericsClassDemo {
/**
*
* @param t 传入泛型的参数
* @param <T> 泛型的类型
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E等形式的参数常用于表示泛型。
*/
public <T> T genercMethod(T t){
System.out.println(t.getClass());
System.out.println(t);
return t;
}
}
调用方法时,确定泛型的类型
public static void main(String[] args) {
//这里的泛型跟下面调用的泛型方法可以不一样。
GenericsClassDemo genericString = new GenericsClassDemo();
//传入的是String类型,返回的也是String类型
String str = genericString.genercMethod("hello");
//传入的是Integer类型,返回的也是Integer类型
Integer i = genericString.genercMethod(123);
}
执行结果
class java.lang.String
hello
class java.lang.Integer
123
这里可以看出,泛型方法随着我们的传入参数类型不同,他得到的类型也不同。泛型方法能使方法独立于类而产生变化。
4.泛型变量的类型限定
有的时候,类、接口或方法需要对泛型类型变量加以约束。看下面的例子:
public static T get(T t1,T t2) {
if(t1.compareTo(t2)>=0);//编译错误
return t1;
}
因为,在编译之前,也就是我们还在定义这个泛型方法的时候,我们并不知道这个泛型类型T到底是什么类型,所以,只能默认T为原始类型Object。所以它只能调用来自于Object的那几个方法,而不能调用compareTo()方法。可我的本意就是要比较t1和t2,而且必须拥有compareTo方法,该怎么办呢?这个时候,就要使用类型限定,对类型变量T 设置限定(bound)来实现这一点。
我们知道,所有实现Comparable接口的方法,都会有compareTo()方法。所以,可以对做如下限定:
public static <T extends Comparable> get(T t1,T t2){
if(t1.compareTo(t2)>=0);
return t1;
}
在上述例子中,调用此方法时,传入的对象必须实现了 Comparable 接口,否则就会产生一个编译错误。这里大家可能会有一个疑惑,为什么使用关键字 extends ?, Comparable 是一个接口,那不是应该用 implements 吗?
注意,此处的 extends 其实并不表示继承,可以将其理解成“绑定”,“绑定”的可以是类,也可以是接口,表示 T 应该是绑定类型的子类型。选择关键字 extends 的原因是更接近子类的概念, 并且 Java 的设计者也不打算在语言中再添加一个新的关键字。
当然,一个变量类型或通配符可以有多个限定,但其中只能有一个类。这也符合我们原本的知识体系——只能继承一个类,但能实现多个接口。
public class Practical<T extends Person & ActionListener & MouseListener> { ... }
如果用一个类作为限定,则它必须是限定列表中的第一个,不同的限定类型之间用“ &” 分隔。
总之,类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:
- 不管该限定是类还是接口,统一都使用关键字extends
- 可以使用&符号给出多个限定
- 如果限定既有接口也有类,那么类必须只有一个,并且放在首位置
程序中类型限定在的主要好处如下:
- 在对象放入集合前,为其作个限制
- 在获取集合中的对象时,不用进行造型的操作
- 当有不同类型的对象添加到集合中的时候,编译时就能检查出错
【例】HashSet集合中使用泛型。
public class GenericDemo {
public static void main(String args[]) {
HashSet<Integer> set = new HashSet<Integer>();
for (int i = 1; i < 6; i++) {
set.add(new Integer(i));
}
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
程序运行结果:
1
2
3
4
5
【例】使用泛型方法计算任意多个数值类型的数据的和。
import java.util.ArrayList;
public class GenericDemo1{
public static double getSum(ArrayList<? extends Number> list){
double sum = 0;
for(Number n:list){
sum += n.doubleValue();
}
return sum;
}
public static void main(String[] args) {
ArrayList<Integer> a = new ArrayList<Integer>();
a.add(3);
a.add(5);
a.add(7);
System.out.println(getSum(a));
}
}
程序运行结果:15.0
程序分析:与泛型类的情况类似,方法也可以泛型化,且无论其所属的类是否为泛型类。其做法是在某一个方法中使用类型参数。泛型机制允许开发者对类型参数指定附加的约束。可以使用extends关键字来指明类型参数必须符合这种继承上的限制。
5.通配符类型(了解)
通配符是Java泛型中的一个重要的概念,对于泛型类之间的继承关系等具有重大的意义。在了解什么是通配符之前,先来了解一下泛型类型的继承规则。
1.泛型类型的继承规则
首先思考一个问题,现在有一个 Employee 类和一个继承了 Employee 类的 Manager 类,如下所示:
public class Employee { ... }
public class Manager extends Employee { ... }
那么对于一个普通的泛型类 Practical:
public class Practical<T> { ... }
类型 Practical<Manager> 是 Practical<Employee> 的子类吗?答案是否定的,通过下面这段代码可以对此进行验证:
Practical<Manager> manager = new Practical<>();
Practical<Employee> employee = manager; // error
上述代码的第二行,用一个 Practical<Employee> 类型的变量引用了一个 Practical<Manager>类型的对象,然而编译器显示这样是不合法的。我们知道,如果 B 是 A 的子类的话,是可以用 A 类型的变量引用 B 类型的对象(向上造型)的。显然,这个例子证明了 Practical<Manager> 并不是 Practical<Employee> 的子类。也许这看起来会很奇怪,但这样设定是出于安全性的考虑,可以避免一些风险的出现。
public class Practical<T> {
ArrayList<T> list;
public Practical() {
list = new ArrayList<>();
}
public void add(T t) {
list.add(t);
}
public T getFirst() {
// return the top ranked;
}
}
假设上面的功能可以实现, 我们补全了一部分 Practical 类的内容,然后执行以下操作:
Practical<Manager> manager = new Practical<>();
Practical<Employee> employee = manager; // 假设可以
manager.add(new Manager());
employee.add(new Employee());
Employee firstEmployee = manager.getFirst();
观察上面的代码,就会发现很多不合理之处,首先第一行实例化了一个新的对象之后,此对象中的 list 列表的成员按道理应该是 Manager 类型的,在第三行向 list 中加入了一个 Manager 类型的对象之后,第四行又加入了一个 Employee 类型的对象(???此处 Employee 可是 Manager 的父类)。在此之后还选出了 list 中的第一名(暂且理解为绩效最好的吧),将“经理”和“普通员工”放一起比较显然是不合理的。
当然,Java泛型类型的继承规则保证了这种“离谱”的情况将不会发生!
2.通配符类型
正如上面所提到的,Practical< Manager > 并不是 Practical< Employee > 的子类,这样做虽然是安全的,但使用起来并没有那么令人愉快。为此,Java 的设计者发明了一种巧妙的(仍然是安全的)“ 解决方案”:通配符类型。
通配符类型中, 允许类型参数变化。例如, 通配符类型 Practical<? extends Employee> 表示任何符合规定的泛型 Practical 类型,即它的类型参数是 Employee 的子类,如 Practical<Manager>,但不是 Practical<String>。
假设要编写一个打印雇员对的方法:
public void printEmployee(Practical<Employee> p) { ... }
3.通配符子类限定(extends)
正如前面讲到的,不能将Practical<Manager> 传递给这个方法,这一点很受限制。解决的方法很简单,使用通配符类型:
public void printEmployee(Practical<? extends Employee> p) { ... }
类型 Practical<Manager>是Practical<? extends Employee>的子类型,这样做就可以很好的满足此场景的需求。
不过,此时又会有新的疑问了,这样会打破之前所说的Java泛型类型的继承规则带来的安全性吗?答案当然是否定的。对于 Practical<? extends Employee>,其 add 方法和 getFirst 方法似乎是这样的:
// 注意编程时不能这样定义具体的方法,通配符并不是类型变量。
// 因此,不能在编写代码中使用“ ?” 作为一种类型,这里只是为了方便表述。
public void add(? extends Employee t) { ... }
public ? extends Employee getFirst() { ... }
对于 add 方法,编译器只知道它需要某个 Employee 的子类型,但并不知道具体是哪种类型。因此,它拒绝传递任何特定的类型。简单的来说,使用 extends 关键字的通配符类型时,带有类型变量参数的方法将无法被调用。
4.通配符的超类限定(super)
除了 extends 关键字之外,通配符类型还可以指定一个超类型限定,如下所示:
Practical<? super Employee> p = new Practical<>();
注意,只有通配符类型可以使用 super 关键字进行限定,前面第一节中提到的类型变量的限定并不行。与 extends 关键字相反,super 关键字表示 Employee 的所有父类型。同样的,其支持的“行为”也正好与 extends 关键字相反,可以作为方法的参数类型(可以传入限定类型及其子类型,即下面例子中Employee及其子类),但不能作为返回值类型。 对应于上一节中的解释,相信这并不难理解。
Practical<? super Employee> p = new Practical<>();
p.add(new Employee()); // ok
Employee e = p.getFirst(); // error
5.无限定通配符
还可以使用无限定的通配符, 例如,Practical<?>。乍一看这好像与原始的 Practical 类型一样,实际上, 它们有很大的不同:
public void add(? t) { ... }
public ? getFirst() { ... }
getFirst 方法的返回值只能赋给一个 Object 类型的变量,而 add 方法不能被调用, 甚至不能通过传入一个 Object 类型的对象调用它。
6.通配符总结
- 类型变量(如 T)可以用 extends 关键字进行绑定,表示 T 应该是绑定类型的子类型,但无法使用 super 关键字。
- 对于通配符 ?,可以用 extends 和 super 关键字进行限定,也可以使用无限定的通配符。
- 对于使用 extends 关键字限定的和无限定的通配符,可以作为返回值,但不能作为传入的参数类型。
- 对于使用 super 关键字限定的的通配符则正好相反,不能作为返回值,但可以作为传入的参数类型。
【例】使用泛型方法计算任意多个数值类型的数据的平均值。
public class GenericDemo3 {
public static double getAverage(ArrayList<? extends Number> list) {
double total = 0.0;
for (Number number : list)
total += number.doubleValue();
return total / list.size();
}
public static void main(String[] args) {
ArrayList<Integer> integerList = new ArrayList<Integer>();
integerList.add(100);
integerList.add(200);
System.out.println(integerList);
//integerList.add(300);
System.out.println(getAverage(integerList));
ArrayList<Double> doubleList = new ArrayList<Double>();
doubleList.add(10.0);
doubleList.add(20.0);
System.out.println(getAverage(doubleList));
ArrayList<String> StringList = new ArrayList<String>();
//System.out.println(getAverage(StringList));
}
}
程序运行结果:
[100, 200]
150.0
15.0
程序分析:在泛型类和泛型方法中,都可以使用类型参数。若施加的类型约束只想作用于一个方法的多个参数之间,而不涉及到类中的其他方法时,则应将之定义为泛型方法,否则,将之定义为泛型类。
【例】泛型中通配符?的使用。
import java.util.ArrayList;
public class GenericDemo4 {
public static void printList(ArrayList<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
}
程序分析
Java5.0新特性(增强for循环),它可以遍历集合中的元素,这里需要注意的地方是element的类型必须是Object的,原因是这里使用了通配符"?",它可以代表任意类型的数据,所以必须用Object类型的变量来接收。