Java面试题之基础篇

03-12 1277阅读

本篇开始主要就是总结一下,在秋招和春招期间Java常见的面试题,也欢迎大家在评论区进行面试题的补充。

1. 面向对象OOP的理解,并说明其3大特性

面向对象是一种编程思想, 它是将现实世界中的所有事物都抽象成类,类有属性和方法,可以将类实例化后使用这个对象的属性和方法。

面向对象有3个特点:封装、继承、多态

  • 封装:隐藏内容的实现细节,而提供一些公共的接口、方法,来直接使用功能,也就是把该隐藏的隐藏起来,该展示的展示出来,它是通过访问修饰符来控制。
  • 继承:比如动物这个概念是父类,猫、狗是子类,那么子类继承父类,子类就可以拿到父类中的一些特性,并且子类也有自己的特点。所以继承就是子类继承父类,子类就可以使用父类的属性和方法(共性抽取,实现代码的复用)。
  • 多态:同一个对象,在不同时刻体现出来的不同状态,比如说,买东西你使用二维码付款,可以使用微信,也可以使用支付宝,也可以使用银联来支付,这种就是同一个行为可以有不同的表现形式,那么多态也就是同一个接口,使用不同的实例就可以执行不同的操作,但这个必须是在继承的体系下,并且子类要重写父类的方法,然后通过父类的引用调用重写的方法(向上转型 Fu f = new Zi() ),好处,可替换性,可扩充性,灵活性,简化性。

    2. 访问修饰符都有什么

    访问修饰符作用:通过控制类和访问权限来实现封装,类可以将数据和封装数据的方法结合在一起,而访问权限用来控制方法或者字段能否直接在类外使用。

    Java面试题之基础篇

    3. == 和 equals 的区别

    对于 Object 来说,equals 是用 == 来实现的,所以 == 和 equals 是相同的,都是去比较对象的引用是否相同。但 Java 中其它的类比如 Interger、String 都是使用重写后的 equals,都是去比较的值。

    4. 什么是方法的重载,返回值不同还是不是重载

    方法重载就是指在同一个类中定义多个方法,它们方法名相同但参数列表不同。

    那么如果方法名相同参数列表不同,但返回值不同还算不算是重载?

    public String myMethod(int arg1) {
        // 方法体
    }
    public int myMethod(int arg1) {
        // 方法体
    }

    JVM 调用方法是通过方法签名来判断到底要选择调用哪个方法,而方法签名 = 方法名称 + 参数类型 + 参数个数 这样组成的一个唯一值,这个唯一值就是方法签名。所以可以看出返回类型不是方法签名的组成部分。

    5. 重载和重写的区别

    相同点:方法名一样;

    不同点:

    重载:保证两点:

    1. 方法名相同;

    2. 参数列表不同;

    其他都是无关项(比如访问修饰符、返回值等)

    重写:保证三点:

    1. 在继承的体系下;

    2. 核心重写,外壳不变,即就是方法内部代码的实现要改变,而方法的名称、参数列表、返回类型这些都不变。

    3. 访问权限修饰符要大于等于父类;抛出的异常类型要小于等于父类,也就是要抛出父类的子类异常;返回值类型要小于等于父类。

    6. 重载的底层逻辑理解

    首先明确,重载是 Javac 编译器在编译阶段根据静态类型,去选择对应的重载版本。

    其次对于重载版本优先级的选取过程:先会匹配参数的个数,然后再去匹配参数类型的所属类;如果找不到直接的所属类,那会向上转型(包装类->接口->父类)就是在继承的关系中从低到高搜索看有没有实现;如果向上转型找不到,那再查找可变的参数列表,如果都找不到就方法报错。

    7. 常见的集合类都有哪些

    Java 集合类主要由Collection和map这两个接口派生出来的,Collection有3个子接口分别是:List、Set、Queue。

    • List 代表有序可重复的集合,可以直接根据元素的索引来访问;
    • Set 代表无序不可重复的集合,只能根据元素本身来访问;
    • Queue 是队列集合。
    • Map 代表的是存储 key-vaule 的键值对集合,可以根据元素的key来访问value。

      集合中场景的实现类有 ArrayList、LinkedList、HashSet、TreeSet、PriorityQueue(优先级队列)、HashMap、TreeMap、ConcurrentHashMap等。

      8. ArrayList 和 LinkedList 的区别

      1. 底层实现不同:ArrayList 是基于动态数组的数据结构,而 LinkedList 是基于双向链表的数据结构。
      2. 随机访问性能不同:ArrayList 优于 LinkedList,因为 ArrayList 可以根据下标以 O(1) 时间复杂度对元素进行随机访问。而 LinkedList 的访问时间复杂度为 O(n),因为它需要遍历整个链表才能找到指定元素。
      3. 插入和删除的性能不同:LinkedList 优于 ArrayList,因为 LinkedList 的插入和删除操作时间复杂度为 O(1),而 ArrayList 的时间复杂度为 O(n)。

      所以基于这种特点,如果随机访问比较多的业务场景可以选择使用 ArrayList,如果添加和删除比较多的业务场景可以选择使用LinkedList。

      补充:动态扩容

      ArrayList 当没插入元素时,不分配内存空间,当第一次插入元素时,先分配 10 个对象空间,然后后面扩容是按 1.5 倍来扩容。这个扩容本质上是新建一个更大的数组,然后把旧数组中的内容复制过去

      所以可以分析一下,ArrayList 插入数据,时间主要是花费在了复制数组上,而 LinkedList 插入元素,时间主要是花费在了构建结点上了。

      当元素少的时候,插入元素 LinkedList 比 ArrayList 快,因为构建的结点花费时间少,而数组复制时间长

      当元素量很大的时候,插入元素 ArrayList 比 LinkedList 快,因为此时的这个 ArrayList 数组已经扩容到足够大了,可以直接添加元素,而 LinkedList 此时还是要不断构建结点进行插入,所以比较慢。

      9. String、StringBuffer、StringBuilder区别

      String 底层是字符数组,并且数组被 final 修饰,所以一旦指向某个对象后,就不能再指向其它对象了,所以 String 是不可变的。

      StringBuffer 和 StringBuilder 它底层还是一个字符数组,不过它没有被 final 修饰,所以是可变的;StringBuffer 线程安全,但效率低,StringBuilder 线程不安全,但效率高;两个类都提供了非常方便操作字符串的方法,比如:append(拼接)、insert(插入)、reverse(反转)....;

      StringBuffer和StringBuilder的扩容机制是一样的,都是从当前容量开始扩容,它默认的初始容量为16

      • 一次追加长度超过当前容量,则会按照 当前容量*2+2 扩容一次
      • 一次追加长度不仅超过初始容量,而且按照 当前容量*2+2 扩容一次也不够,其容量会直接扩容到与所添加的字符串长度相等的长度。之后再追加的话,还会按照 当前容量*2+2进行扩容

        10. String a = “abc” 和 new String("abc") 区别

        • String a = “abc‘:JVM 会使用常量池来管理这个字符串,如果常量池中已经有这个字符串了,那就将引用直接赋值给变量 a,如果常量池中没有这个字符串,那就将这个字符串存入常量池中

        • new String("abc"):JVM 会先在常量池中存入这个 ”abc“ 字符串,然后再创建一个新的 String 对象,这个对象会被保存到堆内存中,堆中对象的数据会指向常量中的字符串

          对比下来,new 会消耗更多内存,所以建议使用第一种

          11. hashcode 和 equals 关系

          重写 equals 方法,也需要对 HashCode 进行重写

          总结:

          1. 首先明确:一般使用的是重写后的equals,只会比较内容,而Hashcode是根据哈希函数去计算哈希地址,也就是从地址的角度去考虑,equals代表内容,而Hashcode代表地址;

          2. 然后就是比如给Hashset 中插入一个元素,hashset 特点是无序和不可重复的,所以给Hashset中插入元素,会先根据哈希函数去计算哈希值获取存储对象的地址,然后在散列表中去找这个地址,然后通过equals 去对比这个地址上对象的内容是否一致,如果一致就不插入,如果不一致就插入到链表后;

          3. 所以基于2中的这个过程来看,重写equals也要重写hashcode来保证内容和地址上的一致性;

          4. 最后就是两个对象相同,那么哈希地址一定会相同

          5. 如果哈希地址相同,那么两个对象并不一定相同

          12. ArrayList 和 Vector 区别

          Java面试题之基础篇

          相同点:如图可以看出,ArrayList 和 Vector 都实现了 List 这个接口,它们都是动态数组的实现,也拥有相同的方法,可以对元素进行添加、删除、查找等操作。

          异同点:

          1. 线程安全:Vector 是线程安全的,而 ArrayList 不是。所以在多线程的环境下,应该使用 Vector。
          2. 扩容方式:当数组容量不足时,ArrayList 默认是按照 1.5 倍来扩容的,而 Vector 是按照 2 倍来扩容的。这就说明在添加元素时,ArrayList 相比与 Vector  需要更频繁的进行扩容操作。
          3. 性能选择:Vector 是线程安全的,所以它的性能通常会比 ArrayList 差。

          基于上述内容,如果不需要考虑线程安全问题,并且需要快速的存取操作,使用 ArrayList 更优。如果需要考虑线程安全问题以及更好的数据存储能力,则使用 Vector 更优。

          13. HashMap 和 HashSet 区别

          1. 存储方式不同: HashMap 存储的是键值对,将键映射到值,可通过键来访问值;而HashSet 存储的是唯一值的集合,它底层就是只使用 HashMap 键值对里面的 key。
          2. 实现方式不同:HashMap 内部采用的是哈希表数据结构来存储键值对,而 HashSet 采用的是哈希表或者二叉树数据结构来存储唯一值的集合。
          3. 存储特点不同:HashSet 存储的是无序不可重复的元素集合;而 HashMap 存储的是键值对,键是唯一的,而值可以重复。
          4. 数据访问方式不同:HashMap 可以通过键来访问对应的值,通过 get() 方法;而 HashSet 只能通过迭代器(Iterator)或 forEach() 来遍历元素,没有直接获取单个元素的方法。
          5. 扩容方式不同:HashMap 扩容的时候会重新调整内部存储结构,将所有键值对重新散列到新的存储区域中;而 HashSet 扩容则仅仅只是增加了哈希桶的数量,然后将原有的元素重新分配到新的桶中。

          14. HashTable、HashMap、ConcurrentHashMap 区别

          总体来看,HashTable、HashMap、ConcurrentHashMap 都是 Map 接口的实现类,都是以 key-value 的形式来存储数据。

          下面对这三个分别进行阐述对比

          HashMap

          1.  HashMap 的键值可以为null (当key为空时,哈希会被赋值为0)
          2.  HashMap 的默认初始容量是16, 最大容量是2^30;
          3.  HashMap 使用的数据结构是 数组 + 链表 + 红黑树。当链表长度 > 8,并且桶的数量必须大于64时,链表才会转化为红黑树(桶的数量小于64时只会扩容);如果链表长度
          4. HashMap 效率非常高,但线程不安全;

          HashTable

          1. HashTable 的键值不能为 null;
          2. HashTable 虽然线程安全,但只是简单得用 Synchronized 给所有方法加锁,相当于是对 this 加锁,也就是对整个 HashTable 对象进行加锁(非常无脑的加锁方式)。这就会导致一个 HashTable 对象只有一把锁,如果两个线程同时访问一个对象时,就会发生锁冲突;
          3. HashTable 效率非常低,因为是无脑加锁,比如一些读操作不存在线程不安全问题,还是加锁了,导致效率非常低;
          4. HashTable 底层的数据结构是 数组 + 链表; 

          ConcurrentHashMap

          1. ConcurrentHashMap 键值不可以为 null;
          2. ConcurrentHashMap 使用的数据结构为 数组 + 链表 + 红黑树
          3. ConcurrentHashMap 最大的特点就是线程安全,ConcurrentHashMap 相比较与 HashTable 做了很多的优化。最核心的思路就是:降低锁冲突的概率; 

          主要的做法就是:

          (1)锁粒度的控制

          ConcurrentHashMap 不是锁整个对象,而是使用多把锁,对每个哈希桶(链表)都进行加锁,只有当两个线程同时访问同一个哈希桶时,才会产生锁冲突,这样也就降低了锁冲突的概率,性能也就提高了

          (2)ConcurrentHashMap 只给写操作加锁,读操作没加锁

          如果两个线程同时修改,才会有锁冲突

          如果两个线程同时读,就不会有锁冲突

          如果一个线程读,一个线程写,也是不会有锁冲突的

          (这个操作也是可能会锁冲突的,因为有可能,读的结果是一个修改了一半的数据

          不过ConcurrentHashMap在设计时,就考虑到这一点,就能够保证读出来的一定时一个“完整的数据”,要么是旧版本数据,要么是新版本数据,不会是读到改了一半的数据;而且读操作中也使用到了volatile保证读到的数据是最新的)

          (3)充分利用到了CAS的特性

           比如更新元素个数,都是通过CAS来实现的,而不是加锁

          (4)ConcurrentHashMap 对于扩容操作,进行了特殊优化

          ConcurrentHashMap 在扩容时,不再是直接一次性完成搬运数据,而是搬运一点,具体是这样的

          扩容过程中,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运一部分到新的空间上,直到最终搬运完成,就释放旧的空间

          在这个过程中如果要查询元素,旧的和新的一起查询;如果要插入元素,直接在新的上插入

          ;如果是要删除元素,那就直接删就可以了;

          15. 哈希冲突的解决方案有哪些

          哈希冲突是指在哈希表中,两个或多个元素被映射到了同一个位置的情况。

          常见的解决哈希冲突的常用方法有以下三种:链地址法、开放地址法和再哈希法。

          1. 开放地址法: 当发生哈希冲突时,通过一定的探测方法(比如线性探测、二次探测等)在哈希表中找到下一个可用的位置。这种方法优点是不需要额外的存储空间,适用于元素较少的情况下;缺点是容易产生聚集现象,也就是某些桶中的元素过多,而其他的桶中元素很少。
          2. 链地址法:将哈希表中的每个桶都设置为一个链表,当发生哈希冲突时,将新的元素插入到链表的末尾。这种方法优点是适用于元素较多的情况;缺点是当链表过长时,查询效率也会变低。
          3. 再哈希法:当发生哈希冲突时,使用另一个哈希函数计算出一个新的哈希值,然后将元素插入到对应的桶中。优点是适用于元素数量较少的情况;缺点是需要额外的哈希函数,且当哈希函数不够随机时,容易产生聚集现象。

          线性探测&二次探测:

          1. 线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
          2. 二次探测:线性探测找位置是一个一个往后找,缺点是将冲突的元素都放在一起,并且删除也不方便。那么二次探测为了解决这个问题,找“下一个”位置有了不同的方法,找下一个空位置的方法为Hi = (H0 + i^2) % m。二次探测的优点是相对于线性探测来说,它更加均匀地分布元素,但缺点是容易产生二次探测聚集现象,即某些桶中的元素过多,而其他桶中的元素很少。

          HashMap 如何解决哈希冲突?

          在 Java 中,HashMap 使用的是链地址法解决哈希冲突的,对于存在冲突的 key,HashMap 会把这些 key 组成一个单向链表,之后使用尾插法把这个 key 保存到链表尾部。

          16. 什么是负载因子,为什么是0.75

          HashMap 的负载因子是 HashMap 在扩容时的第一个阈值,当 HashMap 中的元素个数超过了容量乘以负载因子时,就会扩容。默认的负载因子是 0.75,也就是说当 HashMap 中的元素个数超过了容量的 75% 时,就会进行扩容。当然,我们也可以通过构造函数来指定负载因子。

          HashMap 扩容的目的是为了减少哈希冲突,提高 HashMap 性能的。

          至于为什么说负载因子是0.75,官方给出来的理由是默认负载因子为 0.75,是因为它提供了空间和时间复杂度之间的良好平衡。 负载因子太低会导致大量的空桶浪费空间,负载因子太高会导致大量的碰撞,降低性能。0.75 的负载因子在这两个因素之间取得了良好的平衡

          17. 抽象类是什么,它和接口有什么区别

          抽象类就是一个类中没有足够的信息去描绘出一个具体的对象,所以它是不能实例化的,所以抽象类的作用就是为了被继承的,但是其他都是存在的,成员方法,成员变量,构造方法都是有的。抽象类中普通方法可以有具体的实现,抽象方法是不能有具体的实现的,并且当抽象类被继承的时候,这个抽象方法要被重写。并且他是不能被private static final修饰的。

          接口是用来实现的,是用来解决多继承的手段,在 jdk1.7中方法是被 public abstract 修饰的,普通的成员方法是不能有具体的实现的,在 jdk 1.8中如果要有具体的实现必须给方法前加 default,并且方法也可以被 static 修饰

          比较:

          1. 目的不同:抽象类是给其他类提供了一个通用的模板和基类,实现代码的复用和统一,并且保证子类实现了父类中的抽象方法;接口是提供了某个具体的行为或动作,来让其他的类来使用,可以理解为接口提供了某个和实现类不相关的行为,作为实现类的补充,并且接口也是解决多继承的手段;

          2. 抽象类的成员方法和普通的类相同;接口的成员方法是被 public statac final 修饰的;

          3. 抽象类是用来被继承的;接口是用来被实现的;

          4. 关键字不同:抽象类是abstract,然后被继承是extends;接口是 interface,实现接口是 implements

          5. 抽象类中有普通方法,抽象方法,构造方法,成员变量等;接口是没有构造方法的,而普通方法在接口中也是不能有具体的实现的,在 jdk1.8后可以加default然后实现了;

          既然已经有子类来继承父类这个概念,为什么还要有子类继承抽象类的概念

          目的不同:子类继承父类,是为了实现共性抽取,代码复用,子类继承抽象类,是为了使用抽象类这个通用的模板,以实现代码复用和统一,并且也为了保证必须实现抽象类中的抽象方法;

          18. 为什么 HashMap 会死循环

          总结:HashMap 死循环发生在 JDK 1.7 版本中,形成死循环的原因是 HashMap 在 JDK 1.7 使用的是头插法,头插法 + 多线程并发操作 + HashMap 扩容,这几个点加在一起就形成了 HashMap 的死循环,解决死循环可以采用线程安全容器 ConcurrentHashMap 替代。

          HashMap 导致死循环的原因是由以下条件共同导致的:

          1. HashMap 使用头插法进行数据插入(JDK 1.8 之前);
          2. 多线程同时添加;
          3. 触发了 HashMap 扩容。

          常见的解决方法

          1. 升级到高版本 JDK(JDK 1.8 以上),高版本 JDK 使用的是尾插法插入新元素的,所以不会产生死循环的问题;
          2. 使用线程安全容器 ConcurrentHashMap 替代(推荐使用此方案);
          3. 使用 synchronized 或 Lock 加锁 HashMap 之后,再进行操作,相当于多线程排队执行(比较麻烦,也不建议使用)

          19. 深克隆和浅克隆的区别

          深克隆和浅克隆最大的区别就在于克隆出来的新对象是否与原始对象共享引用类型的属性。

          • 浅克隆:克隆出来的新对象与原始对象共享引用类型的属性。也就是说,新对象引用指向的就是原始对象,所以才可以共享引用类型属性。如果修改了新对象中的引用类型属性,原始对象中的相应属性也会被改变。在 Java 中,可以通过实现 Cloneable 接口和重写 clone() 方法来实现浅克隆。(可以理解为只是名义上克隆了,本质还是指向的是同一个对象)
          • 深克隆:克隆出来的新对象与原始对象不共享引用类型属性。也就是说,新对象引用指向的就是新对象,而不是指向原始对象。如果修改了新对象中的引用类型属性,原始对象中的相应属性不会被改变(可以理解为真的克隆了,所以指向的不是同一个)

            深克隆的实现方法:

            1. 所有引用属性都实现克隆,整个对象就变成了深克隆。
            2. 使用 JSON 工具,如 GSON、FastJSON、Jackson 序列化和反序列化对象实现深克隆。

            在 Java 中,序列化是指将对象转换为字节流的过程,以便可以将其存储在文件中、通过网络发送或在进程之间传递。反序列化是指将字节流转换回对象的过程。 

            20. 说一下 Java 内存模型(Java Memory Model,JMM)

            当问到 Java 内存模型的时候,需要注意,Java 内存模型(Java Memory Model,JMM)它和 JVM 内存布局(JVM 运行时数据区域)是不一样的,它们是两个完全不同的概念。

            首先要知道为什么要有 Java 内存模型

            Java 内存模型存在的原因在于解决多线程环境下并发执行时的内存可见性和一致性问题。

            Java 内存模型(Java Memory Model,简称 JMM)是一种规范,它定义了 Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式,即规范了 Java 虚拟机与计算机内存之间是如何协同工作的。具体来说,它规定了一个线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步地访问共享变量。

            Java 内存模型主要包括以下内容:

            1. 主内存(Main Memory):所有线程共享的内存区域,包含了对象的字段、方法和运行时常量池等数据。
            2. 工作内存(Working Memory):每个线程拥有自己的工作内存,用于存储主内存中的数据的副本,线程只能直接操作工作内存中的数据。
            3. 内存间交互操作:线程通过读取和写入操作与主内存进行交互。读操作将数据从主内存复制到工作内存,写操作将修改后的数据刷新到主内存。
            4. 原子性(Atomicity):JMM 保证基本数据类型(如 int、long)的读写操作具有原子性,即不会被其他线程干扰,保证操作的完整性。
            5. 可见性(Visibility):JMM 确保一个线程对共享变量的修改对其他线程可见。这意味着一个线程在工作内存中修改了数据后,必须将最新的数据刷新到主内存,以便其他线程可以读取到更新后的数据。
            6. 有序性(Ordering):JMM 保证程序的执行顺序按照一定的规则进行,不会出现随机的重排序现象。这包括了编译器重排序、处理器重排序和内存重排序等。

            Java 内存模型通过以上规则和语义,提供了一种统一的内存访问方式,使得多线程程序的行为可预测、可理解,并帮助开发者编写正确和高效的多线程代码。开发者可以利用 JMM 提供的同步机制(如关键字 volatile、synchronized、Lock 等)来实现线程之间的同步和通信,以确保线程安全和数据一致性。

            21. 异常如何处理,finally 中的代码一定会执行吗

            1. 捕获异常:将业务代码放在 try 内部,当业务代码发生异常时,系统都会自动创建一个异常对象,然后 JVM 就会在 catch 内,找这个异常对应的处理模块

            2. 处理异常:在 catch 中处理异常,记录日志,然后根据对应的异常类型,和当前业务场景进行处理

            3. 回收资源:将业务代码中打开的某个资源,执行完毕后进行关闭,不论是否发生异常都要使用 finally 进行处理

            谈一谈抛出异常

            当程序出现错误时,系统会自动抛出异常,也可以由程序主动抛出异常,通过 throw 关键字

            谈一谈 Java 异常接口

            Throwable 是异常的最顶层父类,它有两个直接子类,分别为 Error、Exception

            Error是 错误,一般是和虚拟机相关的(比如栈溢出也是),一般这种错误都会使程序中断,也不应使用 catch 来捕获 error 对象

            Exception 是异常,其子类分为 编译时异常 和 运行时异常两大类(RuntimeException)

            finally 中的代码不一定会执行

            正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,finally 中的代码是不会执行的。而 exit() 方法会执行 JVM 关闭钩子方法或终结器,但 halt() 方法并不会执行钩子方法或终结器。

            22. this 和super的区别

            相同点:

            1. 在构造方法中调用,必须是在构造方法的第一行,并且super和this不能同时存在;

            2. super()和this()都是需要对象的,所以不能在static环境中使用;

            3. 都是关键字;

            不同点:

            1. this是对当前对象属性和方法的引用,super是子类对象调用父类方法和属性的引用,两个不能同时存在;

            2. 在构造方法中,一定会存在super 的调用,不论写不写都有,而this 是不写就不调用;

            23. 说一说对泛型的理解

            普通的类和方法,只能使用具体的类型,如果要应用多种类型的代码,就非常的不方便,而从 JDK1.5 之后,就引入了泛型这个概念,可以类比泛型和函数, 函数传参传入的是值,而泛型传的是类型,这样泛型就可以用于多种类型,它是将类型当做参数了

            泛型存在的意义,就是指定当前的容器,想要什么类型的对象,就让编译器去检查,然后把想要的类型,当做参数去传递

            泛型擦除机制的理解

            擦除机制就是,在编译的过程将,将泛型 T 替换成 Object,并且擦除机制是编译时期的一种机制,运行期间是没有这个概念的

            通配符的理解

            通配符的作用是用来解决泛型无法协变问题的,这个协变的意思就是,比如 孩子类 child 是 父类 parent 的子类,那么 List 也应该是 List 的子类,但是泛型是不支持这样的父子类关系的

            通配符 ? ,表示可以接收任何类型,List 可以表示各种泛型 List 的父类,意思是元素类型未知的 List

            • 通配符上界 List

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]