一、泛型的概念
我们经常需要对多个类型的数据做相同的操作,但为各个类型分别编写方法和类是十分低效的。为此,JDK-5引入了泛型。
泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的实质是类型参数化,将数据类型作为参数进行传递。
类型参数必须使用引用型类型。
二、泛型
1.泛型方法
泛型方法可以在调用时接收不同类型的参数、规定不同类型的返回值。
一个泛型方法定义如下:
<类型参数声明>返回类型 方法名(形参表){
//方法定义
}
下面是简单的举例,并使用两种方法调用泛型方法:
public class Demo {
public static void main(String[] args) {
Integer i = 123;
Demo.<Integer>print(i); //给定类型参数调用,该方法必须配合使用句点表示法,无法自动推断时使用
print(i); //Java8新增类型推断,一般采用该方法
}
public static <T> void print(T t) {
System.out.println(t);
}
}
如果想要的类型参数在某一个特定范围内(通常是继承层次中的范围),可以通过使用extends关键字。这个关键词规定了类型实参必须继承了某个类,或者实现了某个接口。
//如没有extends,编译无法通过,因为不知道T是否定义了compareTo方法
public <T extends Comparable<T>> boolean smallerThan(T o1,T o2) {
if(o1.compareTo(o2) < 0)
return true;
else
return false;
}
泛型方法也可以使用可变参数,如:
public <T> void print(T...args) {
for(T t : args)
System.out.println(t);
}
2.泛型类
泛型类可以对不同类型的数据开放相同的接口。最典型的就是各种容器类。
一个泛型类定义如下:
class 类名<类型参数声明>{
//实例/局部变量和实例方法可以使用类声明的类型参数
}
下面是一个简单的举例及两种使用方法:
class Test<T>{
T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
public class Demo {
public static void main(String[] args) {
//方法一:给定类型实参,实例对象只能接受给定类型的数据
Test<Integer> test1 = new Test<Integer>();
test1.set(6);
//方法二:不给定类型实参,实例对象可以接受任何类型的数据
Test test2 = new Test();
test2.set(1);
test2.set(2.0);
test2.set("3");
}
}
泛型方法每次调用时都会重新压栈,重新推断类型,因此是安全的。在可以自动推断的情况下应尽量使用。
泛型类的实例一旦初始化,在其作用域内就始终存在。中途改变其类型参数是不安全的,应使用给定实参的方式。
● 泛型类中的泛型方法
并不是所有泛型类中的方法都是泛型方法,如:
class Test<T>{
T value;
public void set(T value) { //不是一个泛型方法,只是使用了类声明的类型形参
this.value = value;
}
public T get() { //同上,不是一个泛型方法
return value;
}
public <E> void print(E arg){ //这是一个泛型方法,他有单独的类型参数声明,这个方法中T和E均可使用
System.out.print(arg.toString());
}
public <T> void println(T arg) { //这也是一个泛型方法,但它将类给定的类型参数T隐藏(屏蔽)了,这里的T是自身的类型形参
System.out.println(arg.toString());
}
//这两个方法主要是为了说明泛型类中的泛型方法,明确概念,两个函数本身不具应用意义
}
● 静态方法与泛型
静态方法无法访问实例成员,而类型参数也是实例成员的一种,因此静态方法不可使用类的类型参数。
例如在上面的类中添加如下的静态方法是不能通过编译的:
public static void print2(T arg){ //Error:不能对非静态类型T进行静态引用
System.out.println(arg);
}
应当为静态方法给出一个自身的类型参数声明,修改如下:
public static <T> void print2(T arg) {
System.out.println(arg);
}
3.泛型接口
泛型接口的定义与泛型类基本相同,常被用在各种类的生产器,例如比较器。
interface Print <T>{ //输出器
public void print(T o);
}
使用类实现泛型接口时,需要为泛型接口传递类型参数。常见的是泛型类为泛型接口传递形参:
interface Print <T>{
public void print(T o);
}
class Test<T> implements Print<T>{
@Override
public void print(T o) {
System.out.println(o);
}
}
4.类型通配符
当同一个泛型类被不同的类型参数实例化后,就变成了不同的版本。版本不同的实例是不兼容的,哪怕是基类与子类之间。
public class Demo {
public static void main(String[] args) {
test(new Test<Base>()); //无错误
test(new Test<Heir>()); //Error:方法 test(Test<Base>)对于参数(Test<Heir>)不适用
}
public static void test(Test<Base> arg) { //规定仅接受Test<Base>版本,<>里必须给定类型,否则报错
System.out.println(arg);
}
}
class Base{}
class Heir extends Base{}
class Test<T>{}
为了解决这一问题,我们使用 ? 作为类型通配符,它可以指代所有的类型。将上面的test方法作如下修改,就可以通过编译。
//public static void test(Test<Base> arg)
public static void test(Test<?> arg) {
System.out.println(arg);
}
5.泛型的上下边界
使用泛型时,我们可以为接受的类型参数界定上下边界。
规定传入类型必须是某个类型的子类/实现某个接口(上界),或必须是某个类型的父类(下界)。
extends 关键字用于规定上界,规定上界可以允许泛型中使用父类的实例成员,并允许多态。
public class Demo {
public static void main(String[] args) {
test(new Test<Base>(new Base())); //class learning_test.Base
test(new Test<Heir>(new Heir())); //class learning_test.Heir
}
public static void test(Test<? extends Base> arg) {
arg.print();
}
}
class Base{}
class Heir extends Base{}
class Test<T>{
T value;
public Test (T value) {
this.value = value;
}
public void print() {
System.out.println(value.getClass().toString());
}
}
super关键字用于规定下界,即只接收给定类型的父类类型。将上面的test形参该位Test<? super Heir>,main方法中会报错。
test(new Test<Base>(new Base())); //无措
test(new Test<Heir>(new Heir())); //Error:方法 test(Test<? super Base>)对于参数(Test<Heir>)不适用
测试时发现,extends对于任意的泛型都可以使用,但super只能与类型通配符一起使用。
public static <T extends Base> void print1(T o){} //无错误
public static <T super Base> void print2(T o){} //Error:标记“super”上有语法错误,应为,
//第二行的错误报告看上去很奇怪,可能是将super识别为父类关键字super使用了
public static void print3(List<? extends Base> list) {} //无错误
public static void print4(List<? super Base> list) {} //无错误