在 Java 的广袤编程世界中,有一个特性如同闪耀的星辰,为代码的编写带来了深刻的变革和无尽的优势,它就是泛型。当我们踏入 Java 泛型的领域,就仿佛打开了一扇通往更高层次编程境界的大门。
泛型的出现,不仅仅是一种语法上的创新,更是对编程思维和代码质量的一次重大提升。它让我们在处理各种数据类型时能够更加得心应手,赋予了代码更强的通用性、安全性和可读性。无论是构建复杂的数据结构,还是实现高效的算法逻辑,泛型都在背后默默发挥着至关重要的作用。
一、泛型使用示例
1. 泛型接口
接口定义,可以看到我们只需在接口上增加泛型声明<T>即可,后续我们在继承时可以具体指明类型约束实现类,同样也可以不指明。
/**
* 泛型接口
*
* @param <T>
*/
public interface GeneratorInterface<T> {
T getVal();
}
下面就是不指明类型的实现类,通过这种抽象的方式在后续的使用时,我们就可以灵活设置类型了。
/**
* 实现泛型接口不指定类型
* @param <T>
*/
public class GeneratorImpl<T> implements GeneratorInterface<T> {
@Override
public T getVal() {
return null;
}
}
就像下面这样,在创建示例时指明:
public class Main {
public static void main(String[] args) {
GeneratorInterface<String> generatorInterface=new GeneratorImpl<>();
generatorInterface.getVal();
}
}
当然我们也可以在创建这个类时指明:
/**
* 泛型接口指定类型
*/
public class GeneratorImpl2 implements GeneratorInterface<String> {
@Override
public String getVal() {
return null;
}
}
2. 泛型方法
声明泛型方法的方式很简单,只需在返回类型前面增加一个<E> 即可:
public class GeneratorMethod {
/**
* 泛型方法
* @param array
* @param <E>
*/
public static <E> void printArray(List<E> array){
for (E e : array) {
System.out.println(e);
}
}
}
如此一来,我们就可以在使用时灵活指定元素类型:
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("11");
list.add("11");
list.add("11");
list.add("11");
list.add("11");
list.add("11");
GeneratorMethod.printArray(list);
}
3. 泛型类
与泛型接口用法差不多,在类名后面增加<T>即可。
/**
* 泛型类的用法
* @param <T>
*/
public class GenericObj<T> {
private T key;
public T getKey() {
return key;
}
public void setKey(T key) {
this.key = key;
}
}
通过抽象泛型,在创建时灵活指令类型,从而约束泛型类的参数类型:
public class Main {
public static void main(String[] args) {
GenericObj<Integer> obj=new GenericObj();
obj.setKey(1);
}
}
二、泛型的使用场景
泛型大部分是应用于项目开发中通用对象例如我们常用的Map:
public interface Map<K,V> {
//......略
//通过泛型接口的泛型约束键值对类型
V put(K key, V value);
}
三、详解java泛型常见知识点
1. 为什么说Java是一门伪泛型语言
Java本质就一门伪泛型语言,泛型的作用仅仅在编译期间进行类型检查的,一旦生成字节码之后,关于泛型的一切都会消失。
如下所示,Integer类型数组我们完全可以通过反射将字符串存到列表中。
public static void main(String[] args) throws Exception {
List<Integer> list=new ArrayList<>();
list.add(1);
// list.add("s"); 报错
Class<? extends List> clazz=list.getClass();
// java的泛型时伪泛型,运行时就会被擦除
Method add = clazz.getDeclaredMethod("add", Object.class);
add.invoke(list,"k1");
System.out.println(list);
}
同样的,设计者将Java泛型在编译器后擦除的原因还有如下原因:
- 避免引入泛型创建没必要的新类型
- 节约虚拟机开销
这一点我们用如下的例子就能看出,相同参数不通泛型的方法根本不能重载
2. 泛型存在的意义
说到这里很多读者可能会问:既然编译器要把泛型擦除,为什么还要用泛型呢?用Object不行嘛?
使用泛型后便于集合的取操作,且提高的代码的可读性。
如下代码所示,虽然一下代码在编译后会擦除为Object类型,但是通过泛型限定后,JVM就会自动将其强转为Comparable类型,减少我们编写一些没必要的代码。
public class Test2 {
public static void main(String[] args) {
List<? extends Comparable> list=new ArrayList<>();
for (Comparable comparable : list) {
comparable.compareTo("1");
}
}
}
3. 桥方法是什么
桥方法其实并不是什么高大上的概念,无非是继承泛型类并指定泛型类型,IDE会自动为我们创建构造方法调用父类的有参构造函数确保泛型多态类。
泛型类,指定了一个有参构造函数:
public class Node<T> {
public T data;
public Node(T data) {
this.data = data;
}
public void setData(T data) {
System.out.println("Node.setData");
this.data = data;
}
}
实现类,自动补充构造方法并调用父类构造方法确保实现泛型的多态性。
public class MyNode extends Node<Integer>{
//继承泛型类后自动添加的,用于保证泛型的多态性
public MyNode(Integer data) {
super(data);
}
}
4. 泛型的限制
泛型不可以被实例化:泛型会在编译器擦除,所以泛型在编译器还未知,所以不可被实例化。
泛型参数不可以是基本类型:我们都知道泛型仅在编译器存在,当编译结束泛型就会被擦除,对象就会编程Object类型,所以基本类型作为泛型参数ide就会直接报错。
泛型无法被实例化,无论是泛型变量还是泛型数组,从上文我们就知道泛型会在编译期完成后被擦除,这正是因为JVM不想为泛型创建新的类型造成没必要的开销。
不能抛出或者捕获T类型的泛型异常,之所以catch使用泛型会编译失败,是因为若引入泛型后,编译器无法直到这个错误是否是后续catch类的父类。
不能声明泛型错误:如下所示,泛型会在编译器被擦除,那么下面这段代码的catch就等于catch两个一样的错误,出现执行矛盾。
try{
}catch(Problem<String> p){
}catch(Problem<Object> p){
}
不能声明两个参数一样泛型不同的方法:编译器擦除后,参数一样,所以编译失败
泛型不能被声明为static,泛型只有在类创建时才知晓,而静态变量在类加载无法知晓,故无法通过编译。
5. 泛型的通配符
(1) 什么是通配符
道通配符是解决泛型之间无法协变的问题,当我们使用一种类型作为泛型参数时,却无法使用他的父类或者子类进行赋值,而通配符就是解决这种问题的对策。
(2) 上界通配符
有时我们不知道子类的具体类型,上界通配符就是用于解决那些父类引用指向子类泛型引用的场景,所以上界通配符的设计增强了代码的通用性。
对此我们给出一段示例,首先定义父类。
/**
* 水果父类
*/
public class Fruit {
}
对应的子类代码如下。
/**
* 水果的子类 苹果
*/
public class Apple extends Fruit {
}
然后封装水果类的容器代码。
/**
* 容器类
* @param <T>
*/
public class Container<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
测试代码如下,可以看到上界通配符使得苹果类可以作为水果类的指向引用,即上界通配符是用于子类的上界,方便父类管理子类。
/**
* 泛型测试
*/
public class TestParttern {
public static void main(String[] args) {
//上界通配符限定引用的父类,使得container可以指向继承Fruit的Apple
Container<? extends Fruit> container=new Container<Apple>();
Fruit data = container.getData();
//报错,下文赘述
container.setData(new Apple());
}
}
那么问题来了。为什么上界通配符只能get不能set?如上代码所示,当我们用上界通配符? extends Fruit,我们用其子类作为泛型参数,这只能保证我们get到的都是这个子类的对象。
但我们却忘了一点,当我们用子类apple作为泛型参数时,泛型的工作机制仅仅是对这个对象加个一个编号CAP#1,当我set一个新的对象,编译器无法识别这个对象类型是否和编号匹配。
更通俗的理解,上界通配符决定可以指向的容器,但是真正使用是并不知晓这个容器是哪个子类容器。所以无法set。
(3) 下界通配符
还是以上文的例子进行演示,只不过通配符改为下界通配符:
/**
* 泛型测试
*/
public class TestParttern {
public static void main(String[] args) {
Container<? super Apple> container1=new Container<Fruit>();
}
}
下界通配符决定了泛型的最大粒度的上限,通俗来说只要是苹果类的父亲都可以作为被指向的引用,通过super声明,它可以很直观的告诉我们泛型参数必须传super后的父类如下所示
Container<? super Apple> container1=new Container<Fruit>();
为什么下界通配符只能set不能get(或者说get的是object)?原因如下:
- 下界通配符决定泛型的类型上限,所有水果类的父亲都可以作为指向的引用
- get时无法知晓其具体为哪个父亲,所以取出来的类型只能是object
Container<? super Apple> container1=new Container<Fruit>();
Object data = container1.getData();
6. 如何获取泛型类型
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
//注意这个类要使用子类,笔者为了方便期间使用了 {}
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);//class java.lang.String
}
}
7. 判断泛型是否编译通过
(1) 基于泛型类型比较
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y;
}
}
答:错误,T类型未知,无法比较,编译失败
(2) 基于泛型创建静态单例
public class Singleton<T> {
public static T getInstance() {
if (instance == null)
instance = new Singleton<T>();
return instance;
}
private static T instance = null;
}
答案
不能,泛型不能被static修饰
四、常见面试题
1. 什么是泛型?有什么好处
通过指定泛型的提升代码抽象等级,提升代码复用性。
泛型界定元素的类型避免各种强转操作,且在编译器即可完成泛型检查,提升程序安全性。
2. 什么是类型擦除
java是一门伪泛型语言,我们为元素指定泛型之后,实际执行原理是编译后将object强转为泛型设定的类型。
3. 泛型中上下界限定符extends 和 super有什么区别
extend T 决定类型的上界,它决定这个泛型的类型必须是T或者T的子类,super决定传入类型的超类型必须是指定的类型。
使用时,我们也一般使用PECS原则,即生产时(从元素中获取数据)使用extends 这样读取时的类型可以指定为extends的父类及其子类型:
List<? extends Number> list = new ArrayList<>();
//从列表中读取,可以理解为列表在生产,这里面拿到的元素可以是Number类型的子类
for (Number number : list) {
//do something
}
消费时使用super。这样添加元素时就可以可以指定超类型是T的类及其子类:
List<? super Number> list = new ArrayList<>();
//add 写入可以理解为列表在消费元素,通过super可以传入超类型为Number的元素
list.add(1);
list.add(2.0);