Java【泛型】
Java泛型的概述
- 不同类的数据如果封装方法相同,不必为每一种类单独定义一个类,只需定义一个泛型类,减少类的声明,提高编程效率。通过准确定义泛型类,可避免对象类型转换时产生的错误。泛型又提供了一种类型安全检测机制,只有数据类型相匹配的变量才能正常的赋值,否则编译器就不通过。
- Java中的泛型与C++类模板的作用相同,但是编译方式不同,Java泛型类只会生成一部分目标代码,牺牲运行速度,而C++的类模板针对不同模板参数静态实例化,目标代码体积大一点,但运行快。Java 7后为了向后兼容,每次定义时都要声明泛型的参数类型。
类型擦除
- 泛型的本质是为了将类型参数化,它通过擦除的方式来实现。
- 编译器会先检查代码中传入 < T > 的数据类型并记录下来,然后再对代码进行编译。
- 在编译期间,擦除代码中的所有泛型语法,并做出一些类型转换动作。
- 泛型信息只存在于代码编译阶段,在代码编译结束后,与泛型相关的信息会被擦除掉。
- 在代码成功编译后,其内的所有泛型信息都会被擦除,并且类型参数 T 会被统一替换为其原始类型(默认是 Object 类)。若使用到了 extends 语法的类型参数 T 被擦除后会替换为 Number。
Java中的泛型标记符
泛型标记符用于表示类型参数,只是一种约定。下面是一些常用的泛型标记符及其含义:
E:代表元素(Element)类型,或者 Exception 异常的意思,通常在集合中使用,如 List<E>。
K:代表键(Key)的类型,通常在 Map 中使用,如 Map<K, V>。
V:代表值(Value)的类型,通常在 Map 中使用,如 Map<K, V>。
T:代表任意类型(Type),通常在方法中使用,如 public <T> T method(T obj)。
N:代表数字类型,如 Number 类型。
R:代表返回类型,如 public <T> R method(T obj)。
Java中的泛型方式
1、泛型类
class 类名称 <泛型标识> {
private 泛型标识 /*(成员变量类型)*/ 变量名;
.....}
}
定义泛型类
public class Generic<T> {
// key 这个成员变量的数据类型为 T, T 的类型由外部传入
private T key;
// 泛型构造方法形参 key 的类型也为 T,T 的类型由外部传入
public Generic(T key) {
this.key = key;
}
// 泛型方法 getKey 的返回值类型为 T,T 的类型由外部指定
public T getKey(){
return key;
}
}
//泛型类可以接受多个类型变量类型
public class MultiType <E,T> {
E value1;
T value2;
public E getValue1(){
return value1;
}
public T getValue2(){
return value2;
}
}
类型参数定义的位置
- 非静态的成员属性类型
- 非静态方法的形参类型(包括非静态成员方法和构造器)
- 非静态的成员方法的返回值类型
注:类型参数的确定是在创建泛型类对象的时候,而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数。
public class Test2<T> {
// 泛型类定义的类型参数 T 不能在静态方法中使用
public static <E> E show(E one){ // 这是正确的,因为 E 是在静态方法签名中新定义的类型参数
return null;
}
}
使用泛型类
public void test() {
Generic<String> generic = new Generic<>();// 传入 String 类型
/* < String > 是一个泛型,其限制了 ArrayList 集合中存放对象的数据类型只能是 String,
当添加一个非 String 对象时,在编译阶段,编译器会报错 */
// <> 中什么都不传入,等价于 Generic<Object> generic = new Generic<>();
Generic generic = new Generic();
}
2、泛型接口
public interface 接口名<类型参数> {
...
}
定义泛型接口
注意:在泛型接口中,静态成员也不能使用泛型接口定义的类型参数。
interface Hello<U, R> {
int n = 10;
//U name; //报错!接口中的属性默认是静态的,因此不能使用类型参数声明
R get(U u);//普通方法中,可以使用类型参数
void hi(R r);//抽象方法中,可以使用类型参数
// 在jdk8 中,可以在接口中使用默认方法, 默认方法可以使用泛型接口的类型参数
default R method(U u) {
return null;
}
}
继承泛型接口
//定义一个接口A来继承泛型接口Hello,定义时必须确定泛型接口Hello中的类型参数。
interface A extends Hello<String, Double> {
...
}
// 当去实现A接口时,因为A在继承Hello接口时,指定了类型参数U为String,R为Double
// 所以在实现Hello接口的方法时,使用String替换U,用Double替换R
class IAA implements A {
public Double get(String s) {
return null;
}
public void hi(Double d) {
...
}
}
实现泛型接口
- 指定泛型接口类型参数
// 定义一个类 IBB 实现泛型接口Hello,在 类 IBB 定义时需要确定泛型接口 Hello 中的类型参数。
// 实现接口时,需要指定泛型接口的类型参数
// 给 U 指定 Integer, 给 R 指定了 Float
// 所以,当我们实现Hello方法时,会使用 Integer 替换 U, 使用 Float 替换 R
class IBB implements Hello<Integer, Float> {
public Float get(Integer integer) {
return null;
}
public void hi(Float afloat) {
...
}
}
- 没有确定泛型接口类型参数
// 定义一个类 ICC 实现泛型接口 Hello
// 若是没有确定泛型接口Hello中的类型参数,则默认为 Object。
// 建议直接写成 Hello<Object, Object>
class ICC implements Hello { //等价 class ICC implements Hello<Object, Object>
public Object get(Object o) {
return null;
}
public void hi(Object o) {
...
}
}
// 定义一个类 IDD 实现泛型接口 Hello
// 若是没有确定泛型接口 Hello 中的类型参数,也可以将 IDD 类也定义为泛型类,其声明的类型参数必须要和接口 Hello 中的类型参数相同。
class IDD<U, R> implements IUsb<U, R> {
...
}
3、泛型方法
定义泛型方法
当在一个方法签名中的返回值前面声明了一个 < T > 时,该方法就被声明为一个泛型方法。< T >表明该方法声明了一个类型参数 T,并且这个类型参数 T 只能在该方法中使用。当然,泛型方法中也可以使用泛型类中定义的泛型参数。
public <类型参数> 返回类型 方法名(类型参数 变量名) {
...
}
(1)只有在方法签名中声明了< T >的方法才是泛型方法,仅使用了泛型类定义的类型参数的方法并不是泛型方法。
public class Test<U> {
// 该方法只是使用了泛型类定义的类型参数,不是泛型方法
public void testMethod(U u){
System.out.println(u);
}
// <T> 真正声明了下面的方法是一个泛型方法
public <T> T testMethod1(T t){
return t;
}
}
(2)泛型方法中可以同时声明多个类型参数。
public class TestMethod<U> {
public <T, S> T testMethod(T t, S s) {
return null;
}
}
(3)泛型方法中也可以使用泛型类中定义的泛型参数。
public class TestMethod<U> {
public <T> U testMethod(T t, U u) {
return u;
}
}
(4)泛型类中定义的类型参数和泛型方法中定义的类型参数是相互独立的,泛型方法始终以自己声明的类型参数为准。
public class Test<T> {
public void testMethod(T t) { //testMethod1() 是一个泛型方法,他使用的类型参数是与方法签名中声明的类型参数
System.out.println(t);
}
public <T> T testMethod1(T t) {
return t;
}
}
(5)将静态方法声明为泛型方法
在静态成员中不能使用泛型类定义的类型参数,但可以将静态成员方法定义为一个泛型方法。
public class Test2<T> {
// 泛型类定义的类型参数 T 不能在静态方法中使用
// 但可以将静态方法声明为泛型方法,方法中便可以使用其声明的类型参数了
public static <E> E show(E one) {
return null;
}
}
泛型方法的使用
- 泛型方法,在调用方法的时候再确定类型参数的具体类型。
- 泛型方法签名中声明的类型参数只能在该方法里使用,而泛型接口、泛型类中声明的类型参数则可以在整个接口、类中使用。
public class Demo {
public static void main(String args[]) {
GenericMethod d = new GenericMethod(); // 创建 GenericMethod 对象
String str = d.fun("汤姆"); // 给GenericMethod中的泛型方法传递字符串
int i = d.fun(30); // 给GenericMethod中的泛型方法传递数字,自动装箱
System.out.println(str); // 输出 汤姆
System.out.println(i); // 输出 30
GenericMethod.show("Lin");// 输出: 静态泛型方法 Lin
}
}
class GenericMethod {
// 普通的泛型方法
public <T> T fun(T t) { // 可以接收任意类型的数据
return t;
}
// 静态的泛型方法
public static <E> void show(E one){
System.out.println("静态泛型方法 " + one);
}
}
泛型方法中的类型推断
在调用泛型方法的时候,可以显式地指定类型参数,也可以不指定。当泛型方法的形参列表中有多个类型参数时,在不指定类型参数的情况下,方法中声明的的类型参数为泛型方法中的几种类型参数的共同父类的最小级,直到 Object。在指定了类型参数的时候,传入泛型方法中的实参的数据类型必须为指定数据类型或者其子类。
public class Test {
// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}
public static void main(String[] args) {
// 一、不显式地指定类型参数
//(1)传入的两个实参都是 Integer,所以泛型方法中的<T> == <Integer>
int i = Test.add(1, 2);
//(2)传入的两个实参一个是 Integer,另一个是 Float,
// 所以<T>取共同父类的最小级,<T> == <Number>
Number f = Test.add(1, 1.2);
// 传入的两个实参一个是 Integer,另一个是 String,
// 所以<T>取共同父类的最小级,<T> == <Object>
Object o = Test.add(1, "asd");
// 二、显式地指定类型参数
//(1)指定了<T> = <Integer>,所以传入的实参只能为 Integer 对象
int a = Test.<Integer>add(1, 2);
//(2)指定了<T> = <Integer>,所以不能传入 Float 对象
int b = Test.<Integer>add(1, 2.2);// 编译错误
//(3)指定<T> = <Number>,所以可以传入 Number 对象
// Integer 和 Float 都是 Number 的子类,因此可以传入两者的对象
Number c = Test.<Number>add(1, 2.2);
}
}