小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
文章目录
介绍
什么是泛型
Java泛型(generic type)是JDK 5中引入的一个新特性,泛型提供了编译时类型安全监测机制,该机制允许程序员在编译时监测非法的类型。使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用,例如,ArrayList就是一个无处不在的集合类。
为什么使用泛型
泛型的本质是参数化类型,也就是所操作的数据类型被指定为一个参数。为了方便理解,我们看下面代码:
void f(int a){
}
//调用f()方法
f(10)
我们调用f()方法时,传入的 10 是数据参数。而泛型是这样的,传入一个类型,可以是 String,Integer 等,例如:
List<String> stringArrayList = new ArrayList<String>();
也就是 List 中加入的数据只能是 String 类型的。这也是使用泛型的优点了,在实际使用之前类型就已经确定了,不需要强制类型转换。
泛型分类
泛型的分类
泛型有三种常用的使用方式:泛型类,泛型接口和泛型方法。下面分别介绍。
泛型类
一个泛型类(generic class)就是具有一个或多个类型变量的类。我们通过具体例子来说明,写一个最普通的泛型类:
Holder 类
public class Holder<T>{
private T t;
//虽然在方法中使用了泛型,但是这并不是一个泛型方法。后边泛型方法会讲。
//这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
//所以在这个方法中才可以继续使用 T 这个泛型。
public T getT(){
return t;
}
public void setT(T t){
this.t = t;
}
/*这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别
public E setKey(E t) {
this.t = t;
}*/
}
public class Holder<T> 此处 T 可以随便写为任意标识,常见的如T、E、K、V 等形式的参数常用于表示泛型,习惯使用有意义单词首个字母大写。在实例化泛型类时,必须指定 T 的具体类型。
常见字母(见名知意)
E - Element (在集合中使用,因为集合中存放的是元素)
T - Type(Java 类)
K - Key(键)
V - Value(值)
N - Number(数值类型)
? - 表示不确定的java类型
Test类
public class Test {
public static void main(String[] args) {
Holder<String> h1 = new Holder<>();
Holder<Double> h2 = new Holder<>();
h1.setT("abc");
h2.setT(3.14);
String s = h1.getT();
Double d = h2.getT();
System.out.println(s);
System.out.println(d);
}
}
输出结果
abc
3.14
定义的泛型类,就一定要传入泛型类型实参么?并不是这样,如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。我们看下面例子,
Holder 类不变,Test类修改如下
public class Test {
public static void main(String[] args) {
//raw type
//原始类型,即Object
Holder h = new Holder();
//有警告,应该有泛型,不传的话也能执行
h.setT("abc");
h.setT(3.14);
//但是取值的时候必须强转
//最后一次放入的是Double类型,所以强转为Double
Double d = (Double)h.getT();
System.out.println(d);
}
}
输出结果
3.14
泛型接口
泛型接口与泛型类的定义及使用基本相同。接口中泛型字母只能使用在方法中,不能使用在全局常量中。
现在定义一个泛型接口:
public interface Generator<T> {
public T generate();
}
通过类去实现这个泛型接口的时候指定泛型T的具体类型:
public class NumberGenerator implements Generator<Integer>{
int[] ages = {9,18,20};
@Override
public Integer generate() {
return ages[new Random().nextInt(3)];
}
}
public class Main {
public static void main(String[] args) {
NumberGenerator numberGenerator = new NumberGenerator();
System.out.println(numberGenerator.generate());
}
}
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。
泛型方法举例:public static <T> void display(T t)
泛型的声明,必须在方法的修饰符(public、static、final、abstract等)之后,返回值声明之前
public <T>这个 T 是个修饰符的功能,表示是个泛型方法,就像有 static 修饰的方法是个静态方法一样,所以<T> 不是返回值,表示传入参数有泛型。只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型 T。与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
其中第一个<T>是与传入的参数 (T t) 相对应的,相当于返回值的一个泛型。
由于这个方法我们什么都不返回,所以后边跟的 void。如果想返回 T 类型,那就应该这样写public static <T> T display(T t),代表方法必须返回 T 类型的(由传入的(T t)决定)
单独的 T 代表一个类型 ,所以(T t) 这里的 T 表示我们需要给这个方法传入 T 类型的参数。我们之前学的泛型类 Class<T>代表这个类型所对应的类,要区别的是 Class<?>表示类型不确定的类。
举例:
//无返回值有参的方法, 参数为泛型
public <T> void show(T t) {
System.out.println("显示:" + t);
}
//有返回值的有参方法, 只有一个参数化类型,返回类型是泛型
public <T> T get(T t) {
return t;
}
//有返回值的有参方法, 有多个参数化类型,
public <T, K> K get(T t, K k) {
return k;
}
//泛型方法与可变参数
public static <T> void printMsg(T... args) {
for (T t : args) {
System.out.println("泛型测试t is " + t);
}
}
如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
public class Holder<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
//泛型方法与可变参数
//如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
//即使静态方法要使用泛型类中已经声明过的泛型也不可以。
//编辑器会提示:cannot be referenced from a static context
public static <T> void printMsg(T... args) {
for (T t : args) {
System.out.println("泛型测试t is " + t);
}
}
}
泛型通配符和上下边界
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。为泛型添加上边界,即传入的类型实参必须是指定类型的子类型。
方法中使用泛型时
先看一个例子,Holder 类不变,Test 类修改如下
public class Main {
public static void main(String[] args) {
Holder<Integer> a = new Holder<>();
Holder<Double> b = new Holder<>();
Holder<String> c = new Holder<>();
a.setT(10);
b.setT(3.14);
c.setT("abc");
f1(a);
f1(b);
f1(c);
f2(a);
f2(b);
//f2(c);只允许传数字,不能传其他类型
}
/*
*<?>表示某种特定类型的,不确定,未指定
*/
private static void f1(Holder<?> h) {
Object v = h.getT();
System.out.println(v);
//在这个方法里不能这样写:h.set(3.14);
//只能放入null
//h.setT(null);
}
/*
*<? extends Number>
*表示某种特定类型的
*并且是Number的子类型
*/
private static void f2(Holder<? extends Number> h) {
Number v = h.getT();
System.out.println(v);
//限制放入数据
}
}
输出结果
10
3.14
abc
10
3.14
泛型类
如果我们把泛型类 Holder 的定义改成:
public class Holder<T extends Number> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
}
那么在创建 Holder 实例时:
第二行代码会报错,是因为 String 不是 Number 的子类。
泛型方法
在泛型方法中添加上下边界限制的时候,必须在权限声明与返回值之间的<T>上添加上下边界:
public <T extends Number> T get(T t) {
return t;
}
特性
泛型只在编译阶段有效,我们看以下代码:
Holder<String> h1 = new Holder<>();
Holder<Double> h2 = new Holder<>();
System.out.println(h1.getClass());
System.out.println(h2.getClass());
运行结果:
在编译之后程序会采取去泛型化的措施。也就是说 Java 中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。