Q:Java 中的异常处理(重要)

Java 异常类层次结构图

image-20230328230358717

image-20230328230710500

在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的⼦类 Exception (异常)和 Error (错误)。 Exception 能被程序本身处理( trycatch ), Error 是⽆法处理的(只能尽量避免)。

Exception :程序本身可以处理的异常,可以通过 catch 来进⾏捕获。 Exception ⼜可以分为 受检查异常(系统编译前 必须处理 try catch/ throws) 和 不受检查异常(可以不处理)。

Error : Error 属于程序⽆法处理的错误 ,我们没办法通过 catch 来进⾏捕获 。例如,Java 虚拟机运⾏错误( Virtual MachineError )、虚拟机内存不够错误( OutOfMemoryError )、类定义错误( NoClassDefFoundError)等 。这些异常发⽣时,Java虚拟机(JVM)⼀般会选择线程终⽌

Throwable 类常⽤⽅法
  • public string getMessage() :返回异常发⽣时的简要描述
  • public string toString() :返回异常发⽣时的详细信息
  • public string getLocalizedMessage() :返回异常对象的本地化信息。使⽤ Throwable 的⼦类覆盖这个⽅法,可以⽣成本地化信息。如果⼦类没有覆盖该⽅法,则该⽅法返回的信息与getMessage 返回的结果相同-
  • public void printStackTrace() :在控制台上打印 Throwable 对象封装的异常信息
异常处理总结
  • try 块: ⽤于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟⼀个 finally 块。
  • catch 块: ⽤于处理 try 捕获到的异常。
  • finally 块: ⽆论是否捕获或处理异常, finally 块⾥的语句都会被执⾏。当在 try 块或
  • catch 块中遇到 return 语句时, finally 语句块将在⽅法返回之前被执⾏。
finally 不会被执行的情况
  1. 在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常
    语句之后, finally 还是会被执⾏
  2. 程序所在的线程死亡。
  3. 关闭 CPU。

当 try 语句和 finally 语句中都有 return 语句时,在⽅法返回之前,finally 语句的内容将被执⾏,并且 finally 语句的返回值将会覆盖原始的返回值。

Java 序列化中如果有些字段不想进⾏序列化,怎么办?

(transient关键字)

获取⽤键盘输⼊常⽤的两种⽅法

Scanner input = new Scanner(System.in);

BufferedReader input = new BufferedReader(new InputStreamReader(System.in));

Q: Java IO

Java 中 IO 流分为⼏种?

  • 按照流的流向分,可以分为输⼊流和输出流;
  • 按照操作单元划分,可以划分为字节流和字符流;
  • 按照流的⻆⾊划分为节点流和处理流。

Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派⽣出来的

  • InputStream/Reader: 所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
既然有了字节流,为什么还要有字符流?

问题本质想问:不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么**I/O 流操作要分为字节流操作和字符流操作呢?**

回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是⾮常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流比较好,如果涉及到字符的话使⽤字符流好。

BIO,NIO,AIO 有什么区别?

Java共支持3种网络编程的I/O模型:BIO、NIO、AIO

BIO

同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

BIO(Blocking I/O)就是传统的Java IO编程,其相关的类和接口在java.io包下。

img

NIO:

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理

伪异步I/O采用线程池和任务队列实现,当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。JDK的线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机

img

AIO

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,一般适用于连接数较多且连接时间较长的应用

BIO NIO区别:

  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多
  • BIO是阻塞的,NIO则是非阻塞的
  • BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道
  • 读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

用途

  • BIO方式适用于连接数目比较少且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中 JDK1.4以前的唯一选择,程序只管简单易理解。

  • NIO方式适用于连接数目多且比较短的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

  • AIO方式适用于连接数目多且连接比较长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK1.7开始支持。

Q:深拷贝 浅拷贝

  • 对象克隆最终都离不开「直接赋值」「浅拷贝」「深拷贝」 这三种方式,
  • 直接赋值

    • img
    • 对象的直接赋值的方式没有生产新的对象,只是生新增了一个对象引用
  • 「浅拷贝」「深拷贝」实现方式正是通过重写的类必须实现Cloneable接口,重写函数调用 Object 类的 clone() 方法来完成

    • 浅拷贝,只是简单重写了clone() 方法,内部调用的还是 父类的clone方法

      • 总结:对基本类型数据进行值传递,对引用类型数据进行引用传递的拷贝

      • 对于基本数据类型的成员变量,浅拷贝直接进行值传递,也就是将属性值复制了一份给新的成员变量

      • 对于引用数据类型的成员变量,比如成员变量是数组、某个类的对象等,浅拷贝就是引用的传递,也就是将成员变量的引用(内存地址)复制了一份给新的成员变量,他们指向的是同一个事例。在一个对象修改成员变量的值,会影响到另一个对象中成员变量的值。

        • 对于引用类型成员变量有个特例

          「String、Integer 等包装类都是不可变的对象,当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值,然后将原来的引用指向新的地址] 在修改上的效果等价于深拷贝!

    • 深拷贝:重新编写clone() 方法,

      • 总结: 对基本类型数据进行值传递,对引用类型数据创建一个新的对象,并复制其内容

      • 深拷贝有两种实现方式:序列化对象方式和属性二次调用clone方法

        • 序列化拷贝:

          因为序列化拷贝是把对象转换为字节序列,再把字节序列恢复成对象,不是属性的拷贝,所以使用序列化拷贝可以不实现Cloneable接口,但要实现Serializable接口

          因为transient变量无法序列化, 使用这种方法将无法拷贝transient变量

        • 二次调用clone

          需要这个成员变量类型实现Cloneable接口

Java集合

说说List,Set,Map三者的区别? 是否有序,可重复

Q:Arraylist 与 LinkedList 区别?
  • LinkedList

    1. 基于双向链表,无需连续内存(JDK1.6 之前为循环链表,JDK1.7 取消了循环。)
    2. 随机访问慢(要沿着链表遍历)
    3. 头尾插入删除性能高
    4. 占用内存多

    ArrayList

    1. 基于数组,需要连续内存
    2. 随机访问快(指根据下标访问)
    3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
    4. 可以利用 cpu 缓存,局部性原理

补充内容:RandomAccess 接⼝

源码我们发现实际上 RandomAccess 接⼝中什么都没有定义。所以,在我看来RandomAccess 接⼝不过是⼀个标识罢了。

标识实现这个接⼝的类具有随机访问功能。并不是说 ArrayList实现 RandomAccess 接⼝才具有快速随机访问功能的!

Q: ArrayList 与 Vector 区别呢?为什么要⽤Arraylist取代Vector呢?
  • ArrayList 是 List 的主要实现类,底层使⽤ Object[ ] 存储,适⽤于频繁的查找⼯作,线程不安全 ;

  • Vector 是 List 的古⽼实现类,底层使⽤ Object[ ] 存储,线程安全的。

Vector类的所有方法都是同步的(synchronized)。你可以使用两个线程安全的访问Vector对象。但是,如果你只用单个线程来访问Vector对象——这是更加常见的情况——那么你的代码将会在同步操作上浪费相当多的时间。相反,ArrayList类的方法不是同步的。因此现在的建议一般是在不需要同步时使用ArrayList而不是Vector。

Q: ArrayList 扩容机制

ArrayList源码+扩容机制分析-CSDN

三种初始化方式:

  • ArrayList() 会使用长度为零的数组
  • ArrayList(int initialCapacity) 会使用指定容量的数组
  • public ArrayList(Collection<? extends E> c) 会使用 c 的大小作为数组容量
  • add(Object o) 首次扩容为 10,再次扩容为上次容量的 1.5 倍
  • addAll(Collection c) 没有元素时(首次),扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
  • 扩容函数 grow()

    • System.arraycopy() 和 Arrays.copyOf()
    • copyOf()内部实际调用了 System.arraycopy() 方法
    • arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组

    • ensureCapacity方法 : 最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数(直接增大到大概的容量,省去中间多次扩容)

Q:HashMap 和 Hashtable 的区别
  • 都是数组加链表的形式
  1. 线程是否安全: HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(但因为效率问题:如果你要保证线程安全的话就使⽤ConcurrentHashMap 吧!);

  2. 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable基本被淘汰,不要在代码中使⽤它;

  3. Null key Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。

  4. 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :

    • ① 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。
    • ② 创建时如果给定了容量初始值,那么Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩,也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。

Q : HashMap 和 HashSet区别

如果你看过 HashSet 源码的话就应该知道: HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。

Q: HashMap

基本数据结构
  • 1.7 数组 + 链表
  • 1.8 数组 + (链表 | 红黑树)
树化与退化

树化意义

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 $O(1)$,而红黑树的查找,更新的时间复杂度是 $O(log_2⁡n )$,TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表
索引计算

索引计算方法

  • 首先,计算对象的 hashCode()
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希
    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
  • 最后 & (capacity – 1) 得到索引

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模 如:97%64=33 等价于 97&(64-1) =33
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable
HashMap 并发问题
JDK1.7 的死锁问题

JDK7版本中的HashMap扩容时使用头插法,假设此时有元素一指向元素二的链表,当有两个线程使用HashMap扩容的时,若线程一在迁移元素时阻塞,但是已经将指针指向了对应的元素,线程二正常扩容,因为使用的是头插法,迁移元素后将元素二指向元素一。此时若线程一被唤醒,在现有基础上再次使用头插法,将元素一指向元素二,形成循环链表。若查询到此循环链表时,便形成了死锁。而JDK8版本中的HashMap在扩容时保证元素的顺序不发生改变,就不再形成死锁,但是注意此时HashMap还是线程不安全的。

详见HashMap这篇博文

数据错乱(1.7,1.8 都会存在)

Q: Hashtable vs ConcurrentHashMap

要求

  • 掌握 Hashtable 与 ConcurrentHashMap 的区别
  • 掌握 ConcurrentHashMap 在不同版本的实现区别

Hashtable 对比 ConcurrentHashMap

  • Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
  • Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
  • ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突

ConcurrentHashMap 1.7

  • 数据结构:Segment + HashEntry + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突

    Segment 实现了 ReentrantLock ,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。

    • ⼀个 ConcurrentHashMap ⾥包含⼀个 Segment 数组。 Segment 的结构和 HashMap 类似,是⼀种数组和链表结构,⼀个 Segment 包含⼀个 HashEntry 数组,每个 HashEntry 是⼀个链表结构的元素,每个 Segment 守护着⼀个 HashEntry 数组⾥的元素,当对 HashEntry 数组的数据进⾏修改时,必须⾸先获得对应的 Segment 的锁。

ConcurrentHashMap 1.8

  • JDK1.8 的 ConcurrentHashMap 实现是 Node 数组 + 链表 / 红⿊树。不过,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当冲突链表达到⼀定⻓度时,链表会转换成红⿊树。
Q: ⽐较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
  • HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
  • LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
  • TreeSet 底层使⽤红⿊树,能够按照一定的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排序