巨人的肩膀JAVA面试总结

Posted 生命是有光的

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了巨人的肩膀JAVA面试总结相关的知识,希望对你有一定的参考价值。

1、💪

目录

1.0、什么是面向对象

对比面向过程,面向对象是不同的处理问题的角度。面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者,及各自需要做什么

比如:洗衣机洗衣服。面向过程就会将任务拆解成一系列的函数:1.打开洗衣机 2.放衣服 3.放洗衣粉 4.清洗

面向对象会拆出人和洗衣机两个对象,人:打开洗衣机、放衣服、放洗衣粉,洗衣机只需要清洗即可。

面向过程比较直接高效,面向对象更易于维护、扩展和复用。

面向对象有三大特性:封装、继承和多态

  • 封装的意义在于明确标识出允许外部使用的所有成员函数和数据项,内部细节对外部调用透明,外部调用无需关心内部的实现,Java中有两个比较经典的场景

    1. Javabean 的属性私有,对外提供 get、set 方法访问

      private String name;
      public void setName(String name)
          this.name = name;
      
      
    2. orm 框架:操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入 mybatis,调相应的方法即可

  • 继承:继承父类的方法,并作出自己的改变和扩展

    1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
    2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
    3. 子类可以用自己的方式实现父类的方法。
  • 多态:比如都是动物类型的对象,执行 run 方法,cat 猫类和 dog 狗类会表现出不同的行为特征。

    • 多态的使用前提:必须存在继承或者实现关系必须存在父类类型的变量引用(指向)子类类型的对象需要存在方法重写
    • 多态本质上分为两种:编译时多态(又称静态多态)运行时多态(又称动态多态)。**我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。**这也是为什么有时候多态方法又被称为延迟方法的原因。

1.1、JDK、JRE、JVM之间的区别

  • JDK:Java SE Development Kit,Java标准开发包,它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
  • JRE:Java Runtime Environment,它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序
  • JVM:Java Virtual Mechinal,Java 虚拟机,它是整个 Java 实现跨平台的最核心的部分,负责运行字节码文件。

我们写出来的 Java 代码,想要运行,需要先编译成字节码,那就需要编译器,而 JDK 中就包含了编译器 javac,编译之后的字节码,想要运行,就需要一个可以执行字节码的程序,这个程序就是 JVM Java虚拟机,专门用来执行 Java 字节码的。

JDK包含JRE,JRE包含JVM。

1.2、什么是字节码

Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。

Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。

1.3、hashCode()与equals()之间的联系

在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),我们可以利用hashCode来做一些提前的判断,比如:

  • 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
  • 如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象
  • 如果两个对象相等,那么他们的hashCode就一定相同

在Java的集合类的实现中,在比较两个对象是否相等时,会先调用对象的hashCode()方法得到哈希值进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同。如果hashCode相同,那么就会进一步调用equals()方法进行比较,可以用来最终确定两个对象是不是相等的。所以如果我们重写了equals()方法,一般也要重写 hashCode() 方法。

1.4、String、StringBuffer、StringBuilder的区别

  • String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的【补充:String 不是基本数据类型,是引用类型,底层用 char 数组实现的】,String类利用了 final 修饰的 char 类型数组存储字符,它里面的对象是不可变的,也就可以理解为常量,显然线程安全。

    private final char value[];
    
  • StringBuffer是线程安全的,StringBuffer 属于可变类,对方法加了同步锁,线程安全【说明:StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的

  • StringBuilder是线程不安全的

执行效率:StringBuilder > StringBuffer > String

线程安全:当多个线程访问某一个类(对象或方法)时,对象对应的公共数据区始终都能表现正确,那么这个类(对象或方法)就是线程安全的

对于三者使用的总结:

  1. 操作少量的数据: 适用 String
  2. 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
  3. 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

String 为什么要设计为不可变类?不可变对象的好处是什么?

主要的原因主要有以下三点:

  1. 字符串常量池的需要:字符串常量池是 Java 内存中一个特殊的存储区域, 当创建一个 String 对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象
  2. 允许 String 对象缓存 HashCode:Java 中 String 对象的哈希码被频繁地使用, 比如在 HashMap 等容器中。字符串不变性保证了 hash 码的唯一性,因此可以放心地进行缓存。这也是一种性能优化手段,意味着不必每次都去计算新的哈希码;
  3. String 被许多的 Java 类(库)用来当做参数,例如:网络连接地址 URL、文件路径 path、还有反射机制所需要的 String 参数等, 假若 String 不是固定不变的,将会引起各种安全隐患

不可变对象指对象一旦被创建,状态就不能再改变,任何修改都会创建一个新的对象,如 String、Integer及其它包装类.不可变对象最大的好处是线程安全.

String 字符串修改实现的原理?

当用 String 类型来对字符串进行修改时,其实现方法是首先创建一个 StringBuilder,其次调用 StringBuilder 的 append() 方法,最后调用 StringBuilder 的 toString() 方法把结果返回。

String str = “i” 和 String str = new String(“i”) 一样吗?

不一样,因为内存的分配方式不一样。String str = “i” 的方式,Java 虚拟机会将其分配到常量池中。

String str = new String(“i”) 则会被分到堆内存中。

public class StringTest     
    public static void main(String[] args) 
    String str1 = "abc";
    String str2 = "abc";
    String str3 = new String("abc");
    String str4 = new String("abc");
    System.out.println(str1 == str2);      // true
    System.out.println(str1 == str3);      // false
    System.out.println(str3 == str4);      // false
    System.out.println(str3.equals(str4)); // true
   


在执行 String str1 = “abc” 的时候,JVM 会首先检查字符串常量池中是否已经存在该字符串对象,如果已经存在,那么就不会再创建了,直接返回该字符串在字符串常量池中的内存地址;如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中创建该字符串对象,然后再返回。所以在执行 String str2 = “abc” 的时候,因为字符串常量池中已经存在“abc”字符串对象了,就不会在字符串常量池中再次创建了,所以栈内存中 str1 和 str2 的内存地址都是指向 “abc” 在字符串常量池中的位置,所以 str1 = str2 的运行结果为 true

而在执行 String str3 = new String(“abc”) 的时候,JVM 会首先检查字符串常量池中是否已经存在“abc”字符串,如果已经存在,则不会在字符串常量池中再创建了;如果不存在,则就会在字符串常量池中创建 “abc” 字符串对象,然后再到堆内存中再创建一份字符串对象

String类的常用方法都有哪些?

  • indexOf():返回指定字符的索引
  • charAt():返回指定索引处的字符
  • replace():字符串替换
  • trim():去除字符串两端空白
  • split():分割字符串,返回一个分割后的字符串数组
  • length():返回字符串长度
  • substring():截取字符串
  • equals():字符串比较

String 有哪些特性?

  • 不变性
  • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用
  • final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性

在使用HashMap的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

1.5、==和equals方法的区别

  • ==:如果是基本数据类型,比较是值,如果是引用类型,比较的是引用地址
  • equals:具体看各个类重写equals方法的比较逻辑,比如String类,虽然是引用类型,但是String类中重写了equals方法,方法内部比较的是字符串中的各个字符是否全部相等。

1.6、重载和重写的区别

  • 重载:方法重载发生在同一个类中,方法名称必须相同,参数类型不同、个数不同、顺序不同也是方法重载,方法返回值和访问修饰符可以不同,如果只是方法返回值不同就不是重载,在编译时就会报错。
  • 重写:方法重写发生在父子类中,子类重写父类的方法,方法名称、参数列表必须相同
    • 返回值范围 ≤ 父类返回值范围
    • 抛出异常范围 ≤ 父类抛出异常范围
    • 访问修饰符范围 ≥ 父类访问修饰符范围
    • 如果父类方法访问修饰符为 private ,则子类就不能重写该方法(子类不能重写父类的私有方法)
public int add(int a,String b)
public String add(int a,String b)
// 上方不是重载,重载与返回值和访问修饰符无关,只看方法名称和参数

构造器是否可被重写?

构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。

1.7、List和Set的区别

  • List:有序,可重复按对象插入的顺序保存对象允许多个Null元素对象。可以使用迭代器 Iterator 取出所有元素,再逐一遍历各个元素。或者使用 get(int index) 方法获取指定下标的元素。
  • Set:无序,不可重复。最多只允许一个 NULL 元素对象,取元素只能用迭代器 Iterator 取得所有元素,再逐一遍历各个元素。

1.8、ArrayList和LinkedList的区别

  • 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
  • ArrayList更适合随机查找,LinkedList更适合删除和添加(不要下意识地认为 LinkedList 作为链表就最适合元素增删的场景,LinkedList 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1),其他情况增删元素的时间复杂度都是 O(n) )
  • ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用
  • 两个都不保证线程安全。
  • 我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!就连 LinkedList 的作者都说他自己几乎从来不会使用 LinkedList

1.9、ArrayList的优缺点?

ArrayList的优点如下:

  • ArrayList 底层以数组实现,ArrayList 实现了 RandomAccess 接口,根据索引进行随机访问的时候速度非常快。
  • ArrayList 在尾部添加一个元素的时候非常方便。

ArrayList 的缺点如下:

  • 在非尾部的增加和删除操作,影响数组内的其他数据的下标,需要进行数据搬移,比较消耗性能

ArrayList 比较适合顺序添加、随机访问的场景。

1.10、Array和ArrayList有何区别?什么时候更适合用Array?

  1. Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型
  2. Array 大小是固定的,ArrayList 的大小是动态变化的
  3. ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等

1.11、遍历一个List有哪些不同的方式?

遍历方式有以下几种:

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止
  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的差异,提供统一遍历集合的接口。Java 在 Collections 集合都支持了 Iterator 遍历
  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中不能对集合进行增删操作

Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

  • 如果一个数据集合实现了该接口,最好使用for 循环遍历
  • 如果没有实现该接口,则推荐使用 Iterator 或 foreach 遍历。

1.12、ArrayList的扩容机制

ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。默认情况下,新的容量会是原容量的1.5倍

1.13、ConcurrentHashMap的实现原理是什么?

先来看下JDK1.7

JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。

如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。Segment 默认为 16,也就是并发度为 16。

再来看下JDK1.8

在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的Node数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加细粒度的锁。

将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点(红黑树的根节点),就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

1.14、JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?

  • 在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换
  • 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

1.15、ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相对于一个小型的HashMap
  3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
  4. 先生成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值

1.8版本

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
  3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
  4. ConcurrentHashMap是支持多个线程同时扩容的
  5. 扩容之前也先生成一个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

1.16、HashMap 与 ConcurrentHashMap 的区别是什么?

  • HashMap 不是线程安全的,而 ConcurrentHashMap 是线程安全的。
  • ConcurrentHashMap 采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段 segment,而且每个小的片段 segment 上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段 segment,然后再在这个片段上面进行插入,而且这里还需要获取 segment 锁,这样做明显减小了锁的粒度。

1.17、JDK1.7和JDK1.8的ConcurrentHashMap实现有什么不同

  • 线程安全实现方式 :JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)
  • 并发度 :JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

1.18、ConcurrentHashMap的put方法执行逻辑是什么

先来看JDK1.7

首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁

获取到锁后:

  1. 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  2. 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  3. 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  4. 释放 Segment 的锁。

再来看JDK1.8

  1. 根据 key 计算出 hash值。
  2. 判断是否需要进行初始化。
  3. 定位到 Node,拿到首节点 f,判断首节点 f:
    • 如果为 null ,则通过cas的方式尝试添加。
    • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。
    • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。
  4. 当在链表长度达到8的时候,数组扩容或者将链表转换为红黑树。

1.19、ConcurrentHashMap的get方法执行逻辑是什么

先来看JDK1.7

首先,根据 key 计算出 hash 值定位到具体的 Segment ,再根据 hash 值获取定位 HashEntry 对象,并对 HashEntry 对象进行链表遍历,找到对应元素。

由于 HashEntry 涉及到的共享变量都使用 volatile 修饰,volatile 可以保证内存可见性,所以每次获取时都是最新值。

再来看JDK1.8

  1. 根据 key 计算出 hash 值,判断数组是否为空;
  2. 如果是首节点,就直接返回;
  3. 如果是红黑树结构,就从红黑树里面查询;
  4. 如果是链表结构,循环遍历判断。

1.20、ConcurrentHashMap 的 get 方法是否要加锁,为什么?

get 方法不需要加锁。因为 Node 的元素 value 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改节点的 value 或者新增节点的时候是对线程B可见的。这也是它比其他并发集合比如 Hashtable、HashMap效率高的原因。

1.21、get 方法不需要加锁与 volatile 修饰的哈希桶数组有关吗?

没有关系。哈希桶数组table用 volatile 修饰主要是保证在数组扩容的时候保证可见性

1.22、ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

我们先来说value 为什么不能为 null。因为 ConcurrentHashMap 是用于多线程的 ,如果ConcurrentHashMap.get(key)得到了 null ,这就无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,就有了二义性。

而用于单线程状态的 HashMap 却可以用containsKey(key) 去判断到底是否包含了这个 null 。

1.23、ConcurrentHashMap 和 Hashtable 的效率哪个更高?为什么?

ConcurrentHashMap 的效率要高于 Hashtable,因为 Hashtable 给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在 JDK1.7 中采用分段锁实现线程安全,在 JDK1.8 中采用CAS+synchronized实现线程安全。

1.24、具体说一下Hashtable的锁机制

Hashtable 是使用 synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差!

1.25、jdk1.7到jdk1.8HashMap发生了什么变化

  • jdk1.7中底层是数组+链表,jdk1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
  • jdk1.7链表插入使用的是头插法,jdk1.8中链表插入使用的是尾插法,因为jdk1.8中插入 key 和 value 时需要判断链表元素个数,当链表个数达到8个,需要将链表转化为红黑树,所以每次插入都需要遍历统计链表中元素个数,所以正好直接使用尾插法
  • jdk1.7中哈希算法比较复杂,存在各种右移与异或运算,jdk1.8中进行了简化,jdk1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

1.26、解决hash冲突的办法有哪些?HashMap用的哪种?

解决Hash冲突方法有:开放定址法、再哈希法、链地址法(拉链法)。HashMap中采用的是 链地址法 :将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。

1.27、为什么在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转为红黑树?

因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。

不用红黑树,用二叉查找树可以吗?

可以。但是二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

1.28、HashMap默认加载因子是多少?为什么是0.75

0.75是对空间和时间效率的一个平衡选择,一般不要修改,除非在时间和空间比较特殊的情况下 :

  • 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值
  • 相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1

1.29、HashMap的Put方法

HashMap的Put方法的流程:

  1. 根据 put 进来的 key 通过哈希算法得出数组下标
  2. 如果数组下标位置元素为空,则直接将 key 和 value 封装为 Entry 对象(jdk1.7中是 Entry 对象,jdk1.8中是 Node 对象),并将对象放入该位置
  3. 如果数组下标位置元素不为空,则要分情况讨论:
    1. 如果是jdk1.7,则先判断是否需要扩容 HashMap,如果要扩容就扩容,如果不用扩容就生成 Entry 对象,并使用头插法将对象插入到当前位置的链表中
    2. 如果是jdk1.8,则会先判断当前位置上的 Node 类型,看是红黑树的 Node,还是链表的 Node
      1. 如果是红黑树的 Node,则将 key 和 value 封装为一个红黑树结点并添加到红黑树中去
      2. 如果是链表的 Node,则将 key 和 value 封装为一个链表 Node 并通过 尾插法插入到链表的最后位置,因为是尾插法,所以需要遍历链表,当发现链表结点个数大于等于8,那么就会将该链表转成红黑树
      3. 将 key 和 value 封装为 Node 插入到链表或者红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束 put 方法。

jdk1.7是先判断需要需要扩容,再进行插入。jdk1.8是先插入,然后再判断是否需要扩容

1.30、HashMap为什么线程不安全

  • 多线程下扩容死循环。JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  • 多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。

1.31、深拷贝和浅拷贝

java当中的克隆跟生物上所说的克隆类似,就是复制出一个一模一样的个体,当然迁移到java当中,那就是复制出一个一模一样对象。

克隆的作用:对象克隆主要是为了解决引用类型在进行等号赋值时使得两个引用同时指向同一个对象实例,从而导致通过两个引用去操作对象时,会直接更改实例中的属性破坏对象的相互独立性

//例如一下代码段
public class Test 

    public static void main(String[] args) 
        // TODO 
        Student s1 = new Student("Tom", 12);
        Student s2 = s1;//s2的引用指向s1
        System.out.println(s1);  // name = Tom,age = 12
        System.out.println(s2);	 // name = Tom,age = 12
        s2.setName("Jerry");//修改s2的值时s1的属性
        System.out.println(s1);	 // name = Jerry,age = 12
        System.out.println(s2);  // name = Jerry,age = 12
    

由上述运行结果可知,在引用类型当中,由于都是指向同一个对象实例,当我们用引用类型去修改对象实例的值时,原来对象的属性也会跟着改变,从而导致了数据的不一致性。对象的克隆就能解决上述问题,防止发生此类情况。

  • 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址,也就是在进行修改的时候同样会修改原来的属性
  • 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。深度克隆就是把对象的所有属性都统统复制一份新的到目标对象里面去,使他成为一个独立的对象,当修改新的对象实例的属性时,原来对象中的属性任然不变。
  • 基本数据类型就是 int、double等,实例对象就是 User 类、 Student类等

如何实现对象的克隆?

浅克隆:实现 Cloneable 接口并重写 clone() 方法(浅克隆)

//实现Cloneable接口
public class Product implements Cloneable
    private String name;
    private Integer price;
    public String getName() 
        return name;
     
    public void setName(String name) 
        this.name = name;
    
    public Integer getPrice() 
        return price;
    
    public void setPrice(Integer price) 
        this.price = price;
    
    public Product(String name, Integer price) 
        super();
        this.name = name;
        this.price = price;
    
    @Override
    public String toString() 
        return "Product [name=" + name + ", price=" + price + "]";
    
    //重写clone的方法
    @Override
    protected Object clone() throws CloneNotSupportedException 
        return super.clone();
    

测试:

public class Test 
    public static void main(String[] args) 
        // TODO 对象的克隆
        Product product1 = new Product("篮球", 189);
        // 1.在Product实现CoCloneable接口
        // 2.重写clone方法
        Product product2 = product1.clone();
        System.out.println(product2);
        product2.setPrice(200);//篮球涨价了
        System.out.println(product1);//此时修改product2不会影响product1的值
        System.out.println(product2);
    

深克隆:实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深克隆

1.32、HashMap的扩容机制

HashMap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 HashMap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。

jdk1.7版本:

  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表上的每个元素
  3. 取每个元素的key,计算出每个元素在新数组中的下标
  4. 将元素添加到新数组中去
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

jdk1.8版本:

  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表或红黑树
  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
    1. 统计每个下标位置的元素个数
    2. 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组的对应位置
    3. 如果该位置下的元素个数没有超过8,则生成一个链表,并将链表的头节点添加到新数组的对应位置
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.33、Java中的异常体系是怎样的

  • Java中的所有异常都来自顶级父类Throwable。Throwable下有两个子类Exception和Error。

  • Error表示非常严重的错误,比如java.lang.StackOverFlowError 栈溢出和Java.lang.OutOfMemoryError 内存溢出,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现的问题了,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。

  • Exception表示异常,表示程序出现Exception时,是可以靠程序自己来解决的,比如NullPointerException空指针异常IllegalAccessException违法访问异常等,我们可以捕获这些异常来做特殊处理。

  • Exception的子类通常又可以分为RuntimeException运行时异常和非RuntimeException非运行时异常两类

    • RunTimeException表示运行期异常,表示这个异常是在代码运行过程中抛出的,这些异常在写代码时可能发现不了。比如NullPointerException空指针异常、IndexOutOfBoundsException数组下标越界异常等。
    • 非RuntimeException表示非运行期异常,比如 IO异常、SQL异常,是编译器认为这块读文件可能会读不到
    • 二者区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用非运行时异常,否则就选择运行异常。

throw 和 throws 的区别

  1. throw:在方法体内部,表示抛出异常,由方法体内部的语句处理;throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例
  2. throws:在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理;表示出现异常的可能性,并不一定会发生这种异常。

1.34、什么时候应该抛出异常,什么时候捕获异常

异常相当于一种提示,如果我们抛出异常,就相当于告诉上层方法,我抛了一个异常,我处理不了这个异常,交给你来处理,而对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。

所以我们在写一个方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常并进行处理。

本方法能处理异常则捕获异常,不能处理异常则向上抛出。

常见的异常类有哪些?

  • NullPointerException:当应用程序试图访问空对象时,则抛出该异常。
  • SQLException:提供关于数据库访问错误或其他错误信息的异常
  • IndexOutOfBoundsException:指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出
  • FileNotFoundException:当试图打开指定路径名表示的文件失败时,抛出此异常。
  • OException:当发生某种 I/O 异常时,抛出此异常。此类是失败或中断的 I/O 操作生成的异常的通用类。
  • ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常
  • IllegalArgumentException:抛出的异常表明向方法传递了一个不合法或不正确的参数

主线程可以捕获到子线程的异常吗?

线程设计的理念:“线程的问题应该线程自己本身来解决,而不要委托到外部”。

正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线程的异常,我们可以用如下的方式进行处理,使用 Thread 的静态方法。

Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());

1.35、了解ReentrantLock吗

ReetrantLock是一个可重入的独占锁,主要有两个特性,一个是支持公平锁和非公平锁,一个是可重入。

ReetrantLock实现依赖于AQS,ReetrantLock主要依靠AQS维护一个阻塞队列,多个线程对加锁时,失败则会进入阻塞队列。等待唤醒,重新尝试加锁。

1.36、ReentrantLock中tryLock()和lock()方法的区别