文章目录
1 泛型
1.1 泛型是什么
Java泛型是JDK 5中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型包括泛型类、泛型接口、泛型方法。
1.2 类型擦除
Java的泛型是伪泛型,这是因为Java在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说的类型擦除。
// 分别创建List<String>对象和List<Integer>对象
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
// 调用getClass()方法来比较l1和l2的类是否相等
System.out.println(l1.getClass() == l2.getClass()); // true
不管为泛型的类型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。
换句话说,Java并不存在泛型类!Java提供的泛型只是在编译期提高检查,以提高代码的安全性!
2 泛型使用方式
2.1 泛型接口
在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。
// 定义接口时指定了一个类型形参,该形参名为E
public interface List<E> {
// 在该接口里,E可作为类型使用
// 下面方法可以使用E作为参数类型
void add(E x);
Iterator<E> iterator();
...
}
// 定义接口时指定了一个类型形参,该形参名为E
public interface Iterator<E> {
// 在该接口里E完全可以作为类型使用
E next();
boolean hasNext();
}
// 定义该接口时指定了两个类型形参,其形参名为K、V
public interface Map<K , V> {
// 在该接口里K、V完全可以作为类型使用
Set<K> keySet();
V put(K key, V value) ;
...
}
2.2 泛型类
// 定义Apple类时使用了泛型声明
public class Apple<T> {
// 使用T类型形参定义实例变量
private T info;
public Apple() {
}
// 下面方法中使用T类型形参来定义构造器
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
public static void main(String[] args) {
// 因为传给T形参的是String实际类型,所以构造器的参数只能是String
Apple<String> a1 = new Apple<>("苹果");
System.out.println(a1.getInfo());
// 因为传给T形参的是Double实际类型,所以构造器的参数只能是Double或者double
Apple<Double> a2 = new Apple<>(5.67);
System.out.println(a2.getInfo());
}
}
2.3 泛型子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,但需要指出的是,当使用这些泛型接口、泛型类时不能再包含类型形参。
// 定义类A继承Apple类,Apple类不能跟类型形参
public class A extends Apple<T>{ } // 定义错误!!!
定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据。与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型。
// 使用Apple类时为T形参传入String类型
public class A extends Apple<String>
如果使用Apple类时没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告。如果希望看到该警告提示的更详细信息,则可以通过为javac命令增加-Xlint:unchecked选项来实现。此时,系统会把Apple类里的T形参当成Object类型处理。
2.4 泛型方法
泛型方法的定义方式:
修饰符 <T , S> 返回值类型 方法名(形参列表) {
// 方法体...
}
比如,假设需要实现这样一个方法——负责将一个数组的所有元素添加到一个Collection集合中:
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}
上述泛型方法中定义了一个T类型形参,这个T类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数。当程序调用fromArrayToCollection()方法时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。
3 类型通配符
3.1 类型通配符
为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是未知类型元素的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。
程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object。因此,把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。
public class Test {
public static void main(String[] args) {
ArrayList<Integer> c = new ArrayList<>();
c.add(1);
c.add(2);
c.add(3);
test(c);
}
public static void test(List<?> c) {
for (Object o : c) {
System.out.println(o);
System.out.println(o instanceof Integer);
}
}
}
3.2 类型通配符的上限
当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,我们不想使这个List<?>是任何泛型List的父类,只想表示它是某一类泛型List的父类。
public class Test {
public static void main(String[] args) {
ArrayList<Son> sons = new ArrayList<>();
ArrayList<Father> fathers = new ArrayList<>();
ArrayList<GrandFather> grandFathers = new ArrayList<>();
test(sons);
test(fathers);
test(grandFathers); // 编译不通过
}
public static void test(List<? extends Father> c) {
for (Object o : c) {
System.out.println(o);
}
}
}
3.3 类型通配符的下限
与类型通配符上限类似,使用? super来表示通配符的下限:
public class Test {
public static void main(String[] args) {
ArrayList<Son> sons = new ArrayList<>();
ArrayList<Father> fathers = new ArrayList<>();
ArrayList<GrandFather> grandFathers = new ArrayList<>();
test(sons); // 编译不通过
test(fathers);
test(grandFathers);
}
public static void test(List<? super Father> c) {
for (Object o : c) {
System.out.println(o);
System.out.println(o instanceof Integer);
}
}
}
4 使用例子
- 构建集合时,比如ArrayList、HashMap时需要指定泛型
- 发送数据到kafka和消费kafka的数据时,需要指定泛型,代表消息的key和value的类型
- 自定义通用返回结果CommonResult,通过参数T指定返回结果data的数据类型