5000字详解面试常问的泛型知识点

写在文章开头

泛型作为jdk1.5进入的技术,它的出现使得Java程序员在操作集合时获取元素进行强转和误插入问题。也因为泛型的出现进行代码架构设计的类的层级关系和设计更加清晰明确,笔者也会通过这篇文章演示泛型的经典用法,并通过这些示例对市面上常见的面试题进行深入剖析。

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

泛型使用示例

泛型接口

接口定义,可以看到我们只需在接口上增加泛型声明<T>即可,后续我们在继承时可以具体指明类型约束实现类,同样也可以不指明。

java 复制代码
/**
 * 泛型接口
 *
 * @param <T>
 */
public interface GeneratorInterface<T> {
    T getVal();
}

下面就是不指明类型的实现类,通过这种抽象的方式在后续的使用时,我们就可以灵活设置类型了。

java 复制代码
/**
 * 实现泛型接口不指定类型
 * @param <T>
 */
public class GeneratorImpl<T> implements GeneratorInterface<T> {
    @Override
    public T getVal() {
        return null;
    }
}

就像下面这样,在创建示例时指明:

java 复制代码
public class Main {
    public static void main(String[] args) {
        GeneratorInterface<String> generatorInterface=new GeneratorImpl<>();
        generatorInterface.getVal();
    }
}

当然我们也可以在创建这个类时指明:

java 复制代码
/**
 * 泛型接口指定类型
 */
public class GeneratorImpl2 implements GeneratorInterface<String> {
    @Override
    public String getVal() {
        return null;
    }

}

泛型方法

声明泛型方法的方式很简单,只需在返回类型前面增加一个<E> 即可:

java 复制代码
public class GeneratorMethod {
    /**
     * 泛型方法
     * @param array
     * @param <E>
     */
    public static <E> void printArray(List<E> array){
        for (E e : array) {
            System.out.println(e);
        }
    }
    
}

如此一来,我们就可以在使用时灵活指定元素类型:

java 复制代码
 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);
    }

泛型类

与泛型接口用法差不多,在类名后面增加<T>即可。

java 复制代码
/**
 * 泛型类的用法
 * @param <T>
 */
public class GenericObj<T> {
    private T key;

    public T getKey() {
        return key;
    }

    public void setKey(T key) {
        this.key = key;
    }

   
}

通过抽象泛型,在创建时灵活指令类型,从而约束泛型类的参数类型:

java 复制代码
public class Main {
    public static void main(String[] args) {
        GenericObj<Integer> obj=new GenericObj();
        obj.setKey(1);

    }
}

泛型的使用场景

泛型大部分是应用于项目开发中通用对象例如我们常用的Map

java 复制代码
public interface Map<K,V> {
   //......略
   //通过泛型接口的泛型约束键值对类型
   V put(K key, V value);
}

为什么说Java是一门伪泛型语言

Java本质就一门伪泛型语言,泛型的作用仅仅在编译期间进行类型检查的,一旦生成字节码之后,关于泛型的一切都会消失。

如下所示,Integer类型数组我们完全可以通过反射将字符串存到列表中。

ini 复制代码
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泛型在编译器后擦除的原因还有如下原因:

  1. 避免引入泛型创建没必要的新类型
  2. 节约虚拟机开销

这一点我们用如下的例子就能看出,相同参数不通泛型的方法根本不能重载

泛型存在的意义

说到这里很多读者可能会问:既然编译器要把泛型擦除,为什么还要用泛型呢?用Object不行嘛?

  1. 使用泛型后便于集合的取操作,且提高的代码的可读性。
  2. 如下代码所示,虽然一下代码在编译后会擦除为Object类型,但是通过泛型限定后,JVM就会自动将其强转为Comparable类型,减少我们编写一些没必要的代码。
java 复制代码
public class Test2 {
    public static void main(String[] args) {
        List<? extends Comparable> list=new ArrayList<>();
        for (Comparable comparable : list) {
            comparable.compareTo("1");
        }
    }
}

是桥方法

桥方法其实并不是什么高大上的概念,无非是继承泛型类并指定泛型类型IDE会自动为我们创建构造方法调用父类的有参构造函数确保泛型多态类。

泛型类,指定了一个有参构造函数

java 复制代码
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;
    }
}

实现类,自动补充构造方法并调用父类构造方法确保实现泛型的多态性。

java 复制代码
public class MyNode extends Node<Integer>{

    //继承泛型类后自动添加的,用于保证泛型的多态性
    public MyNode(Integer data) {
        super(data);
    }
}

泛型的限制

  1. 泛型不可以被实例化:泛型会在编译器擦除,所以泛型在编译器还未知,所以不可被实例化。
  1. 泛型参数不可以是基本类型:我们都知道泛型仅在编译器存在,当编译结束泛型就会被擦除,对象就会编程Object类型,所以基本类型作为泛型参数ide就会直接报错。
  1. 泛型无法被实例化,无论是泛型变量还是泛型数组,从上文我们就知道泛型会在编译期完成后被擦除,这正是因为JVM不想为泛型创建新的类型造成没必要的开销。

  2. 不能抛出或者捕获T类型的泛型异常,之所以catch使用泛型会编译失败,是因为若引入泛型后,编译器无法直到这个错误是否是后续catch类的父类。

  1. 不能声明泛型错误:如下所示,泛型会在编译器被擦除,那么下面这段代码的catch就等于catch两个一样的错误,出现执行矛盾。
java 复制代码
try{
}catch(Problem<String> p){
}catch(Problem<Object> p){
}
  1. 不能声明两个参数一样泛型不同的方法:编译器擦除后,参数一样,所以编译失败
  2. 泛型不能被声明为static,泛型只有在类创建时才知晓,而静态变量在类加载无法知晓,故无法通过编译。

泛型的通配符

什么是通配符

道通配符是解决泛型之间无法协变的问题,当我们使用一种类型作为泛型参数时,却无法使用他的父类或者子类进行赋值,而通配符就是解决这种问题的对策。

上界通配符

有时我们不知道子类的具体类型,上界通配符就是用于解决那些父类引用指向子类泛型引用的场景,所以上界通配符的设计增强了代码的通用性。

对此我们给出一段示例,首先定义父类。

arduino 复制代码
/**
 * 水果父类
 */
public class Fruit {
}

对应的子类代码如下。

scala 复制代码
/**
 * 水果的子类 苹果
 */
public class Apple extends Fruit {
}

然后封装水果类的容器代码。

kotlin 复制代码
/**
 * 容器类
 * @param <T>
 */
public class Container<T> {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

测试代码如下,可以看到上界通配符使得苹果类可以作为水果类的指向引用,即上界通配符是用于子类的上界,方便父类管理子类。

java 复制代码
/**
 * 泛型测试
 */
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

下界通配符

还是以上文的例子进行演示,只不过通配符改为下界通配符:

java 复制代码
/**
 * 泛型测试
 */
public class TestParttern {

    public static void main(String[] args) {
     
        Container<? super Apple> container1=new Container<Fruit>();
    }
}

下界通配符决定了泛型的最大粒度的上限,通俗来说只要是苹果类的父亲都可以作为被指向的引用,通过super声明,它可以很直观的告诉我们泛型参数必须传super后的父类如下所示

java 复制代码
 Container<? super Apple> container1=new Container<Fruit>();

为什么下界通配符只能set不能get(或者说get的是object)?原因如下:

  1. 下界通配符决定泛型的类型上限,所有水果类的父亲都可以作为指向的引用
  2. get时无法知晓其具体为哪个父亲,所以取出来的类型只能是object
java 复制代码
 Container<? super Apple> container1=new Container<Fruit>();
Object data = container1.getData();

如何获取泛型类型

typescript 复制代码
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
    }
}

泛型常见笔试题(校招常见)

  1. 例1
java 复制代码
    public final class Algorithm {
        public static <T> T max(T x, T y) {
            return x > y ? x : y;
        }
    }
bash 复制代码
答:错误,T类型未知,无法比较,编译失败
  1. 例2
java 复制代码
    public class Singleton<T> {

        public static T getInstance() {
            if (instance == null)
                instance = new Singleton<T>();

            return instance;
        }

        private static T instance = null;
    }

答案

java 复制代码
不能,泛型不能被static修饰

小结

我是sharkchiliCSDN Java 领域博客专家开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程JVMMySQL数据库个人专栏导航。

参考

Java泛型中extends和super的区别?:blog.csdn.net/qq_36898043...

Java 基础 - 泛型机制详解:www.pdai.tech/md/java/bas...

第二十章 泛型:www.jishuchi.com/read/onjava...

复制代码
相关推荐
尚学教辅学习资料2 分钟前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng1 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马1 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
paopaokaka_luck6 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风7 小时前
详解K8S--声明式API
后端
Peter_chq7 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml48 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~8 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616888 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端