Java 基础 第 3 阶段:高级应用——尚硅谷学习笔记(含面试题) 2023 年

  • Java 基础 第 3 阶段:高级应用——尚硅谷学习笔记(含面试题) 2023 年
  • 第 9 章 异常处理
    • 9.1 异常的概述

      • 9.1.1 什么是异常?
      • 9.1.2 异常的抛出机制
      • 9.1.3 如何对待异常
    • 9.2 异常的体系结构及常见的异常
      • 9.2.1 Throwable
      • 9.2.2 Error 和 Exception
    • 9.3 编译时异常和运行时异常
    • 9.4 异常处理的方式
      • 9.4.1 异常处理概述
      • 9.4.2 方式 1:捕获异常(try-catch-finally)
      • 9.4.3 方式 2:声明抛出异常类型(throws)
      • 9.4.4 如何选择异常处理的两种方式?
    • 9.5 手动 throw 异常对象
    • 9.6 自定义异常类
      • 9.6.1 为什么需要自定义异常类
      • 9.6.2 如何自定义异常类
    • 9.7 企业真题
  • 第 10 章 多线程
    • 10.1 程序、进程与线程
    • 10.2 创建多线程
      • 10.2.1 方式 1:继承 Thread 类
      • 10.2.2 方式 2:实现 Runnable 接口
      • 10.2.3 方式 3:实现 Callable 接口 (jdk5.0 新增)
      • 10.2.4 方式 4:使用线程池(jdk5.0 新增)
    • 10.3 Thread 类的常用方法
      • 10.3.1 构造器
      • 10.3.2 常用方法
      • 10.3.3. 线程的优先级
    • 10.4 多线程的生命周期
      • 10.4.1 jdk5.0 之前的线程的生命周期
      • 10.4.2 jdk5.0 之后的线程的生命周期
    • 10.5 线程安全问题
      • 10.5.1 同步机制解决线程安全问题
      • 10.5.2 同步机制解决线程安全问题的原理
      • 10.5.3 同步代码块
      • 10.5.4 同步方法
      • 10.5.5 同步锁机制
      • 10.5.6 Lock 锁(jdk5.0 新增)
    • 10.6 同步机制相关的问题
      • 10.6.1 单例设计模式的线程安全问题
      • 10.6.2 死锁
    • 10.7 线程间的通信
    • 10.8 生产者与消费者问题
    • 10.9 企业真题
  • 第 11 章 常用类与基础 API
    • 11.1 String 类

      • 11.1.1 String 的特性
      • 11.1.2 String 的内存解析
      • 11.1.3 String 的常用 API
    • 11.2 StringBuffer、StringBuilder 类
    • 11.3 jdk8 之前的日期、时间 API
      • 11.3.1 java.lang.System 类的日期、时间 API
      • 11.3.2 java.util.Date 类的日期、时间 API
      • 11.3.3 java.text.SimpleDateFormat
      • 11.3.4 java.util.Calendar(日历)
    • 11.4 jdk8 中新的日期、时间 API
      • 11.4.1 本地日期时间:LocalDate、LocalTime、LocalDateTime
      • 11.4.2 瞬时:Instant
      • 11.4.3 日期时间格式化:DateTimeFormatter
    • 11.5 比较器
      • 11.5.1 自然排序:java.lang.Comparable
      • 11.5.2 定制排序:java.util.Comparator
    • 11.6 系统相关类
      • 11.6.1 java.lang.System 类
      • 11.6.2 java.lang.Runtime 类
    • 11.7 数学相关的类
      • 11.7.1 java.lang.Math
      • 11.7.2 BigInteger
      • 11.7.3 BigDecimal
      • 11.7.4 java.util.Random
    • 11.8 企业真题
  • 第 12 章 集合框架
    • 12.1 数组存储数据方面的特点和弊端
    • 12.2 集合框架概述
    • 12.3 Collection 接口的常用方法
      • 12.3.1 添加
      • 12.3.2 判断
      • 12.3.3 删除
      • 12.3.4 其它
    • 12.4 Iterator(迭代器)接口
      • 12.4.1 Iterator 接口
      • 12.4.2 迭代器的执行原理
      • 12.4.3 foreach 循环
    • 12.5 Collection 的子接口:List
      • 12.5.1 List 接口概述
      • 12.5.2 List 接口方法
      • 12.5.3 List 接口主要实现类:ArrayList
      • 12.5.4 List 的实现类:LinkedList
      • 12.5.5 List 的实现类:Vector
    • 12.6 Collection 的子接口:Set
      • 12.6.1 Set 接口概述
      • 12.6.2 Set 主要实现类:HashSet
      • 12.6.3 Set 实现类:LinkedHashSet
      • 12.6.4 Set 实现类:TreeSet
    • 12.7 Map 接口
      • 12.7.1 Map 接口概述
      • 12.7.2 Map 中 key-value 特点
      • 12.7.3 Map 接口的常用方法
      • 12.7.4 Map 的主要实现类:HashMap
      • 12.7.5 Map 实现类:LinkedHashMap
      • 12.7.6 Map 实现类:TreeMap
      • 12.7.7 Map 实现类:Hashtable
      • 12.7.8 Map 实现类:Properties
    • 12.8 Collections 工具类
    • 12.9 企业真题
  • 第 13 章 泛型
    • 13.1 泛型的理解
    • 13.2 泛型在集合、比较器中的使用
      • 13.2.1 集合中使用泛型
      • 13.2.2 比较器中使用泛型
    • 13.3. 自定义泛型类、泛型接口、泛型方法
      • 13.3.1 自定义泛型类
      • 13.3.2 自定义泛型接口
      • 13.3.3 自定义泛型方法
    • 13.4 泛型在继承上的体现
    • 13.5 通配符的使用
    • 13.6 企业真题
  • 第 14 章 数据结构与集合源码
    • 14.1 数据结构

      • 14.1.1 研究对象一:数据间逻辑关系
      • 14.1.2 研究对象二:数据的存储结构
      • 14.1.3 研究对象三:运算结构
    • 14.2 数组
    • 14.3 链表
    • 14.4 栈
    • 14.5 队列
    • 14.6 树与二叉树
      • 14.6.1 树
      • 14.6.2 二叉树的基本概念
      • 14.6.3 二叉树的遍历
      • 14.6.4 经典二叉树
    • 14.7 List 接口下的实现类的源码剖析
      • 14.7.1 List 接口特点
      • 14.7.2 动态数组 ArrayList 与 Vector
      • 14.7.3 ArrayList 部分源码分析
      • 14.7.4 ArrayList 相关方法图示
      • 14.7.5 Vector 部分源码分析
      • 14.7.6 链表 LinkedList
      • 14.7.7 链表与动态数组的区别
      • 14.7.8 LinkedList 源码分析
      • 14.7.9 LinkedList 相关方法图示
    • 14.8 Map 接口下的实现类的源码剖析
      • 14.8.1 哈希表的物理结构
      • 14.8.2 HashMap 中数据添加过程
      • 14.8.3 HashMap 源码剖析
      • 14.8.4 LinkedHashMap 源码剖析
    • 14.9 Set 接口下的实现类的源码剖析
      • 14.9.1 Set 集合与 Map 集合的关系
      • 14.9.2 HashSet 源码剖析
      • 14.9.3 LinkedHashSet 源码剖析
      • 14.9.4 TreeSet 源码剖析
    • 14.10 HashMap 的相关问题
      • 14.10.1 哈希算法的理解?
      • 14.10.2 Entry 中的 hash 属性为什么不直接使用 key 的 hashCode() 返回值?
      • 14.10.3 HashMap 是如何决定某个 key-value 存在哪个桶的?
      • 14.10.4 为什么要保持 table 数组一直是 2 的 n 次幂?
      • 14.10.5 解决[index]冲突问题?
      • 14.10.6 为什么 JDK1.8 会出现红黑树和链表共存?
      • 14.10.7 加载因子的值大小有什么关系?
      • 14.10.8 什么时候树化?什么时候反树化?
      • 14.10.9 key-value 中的 key 是否可以修改?
      • 14.10.10 JDK1.7 中 HashMap 的循环链表是怎么回事?如何解决?
    • 14.11 企业真题
  • 第 15 章 File 类与 IO 流
    • 15.1 File 类

      • 15.1.1 File 类的概述
      • 15.1.2 File 类的构造器
      • 15.1.3 File 类的常用方法
    • 15.2 IO 流
      • 15.2.1 IO 流的概述
      • 15.2.2 IO 流的分类
      • 15.2.3 IO 流的 API
    • 15.3 节点流之一:FileReader、FileWriter
      • 15.3.1 字符输入流:Reader
      • 15.3.2 字符输出流:Writer
      • 15.3.3 文件字符输入流:FileReader
      • 15.3.4 文件字符输出流:FileWriter
      • 15.3.5 关于 flush(刷新)
    • 15.4 节点流之二:FileInputStream、FileOutputStream
      • 15.4.1 字节输入流:InputStream
      • 15.4.2 字节输出流:OutputStream
      • 15.4.3 文件字节输入流:FileInputStream
      • 15.4.4 文件字节输出流: FileOutputStream
    • 15.5 读写数据的过程
    • 15.6 处理流之一:缓冲流
    • 15.7 处理流之二:转换流
      • 15.7.1 InputStreamReader 与 OutputStreamWriter
      • 15.7.2 字符编码和字符集
    • 15.8 处理流之三:对象流
      • 15.8.1 对象流:ObjectOutputStream、ObjectInputStream
      • 15.8.2 对象流 API
      • 15.8.3 认识对象序列化机制
      • 15.8.4 反序列化失败问题
    • 15.9 其它流
      • 15.9.1 数据流:DataInputStream 、DataOutputStream
      • 15.9.2 标准的输入流、标准的输出流:System.in 、System.out
      • 15.9.3 打印流:PrintStream、PrintWriter
      • 15.9.4 Scanner 类
    • 15.10 企业真题
  • 第 16 章 网络编程
    • 16.1 网络编程概述
    • 16.2. IP 地址和域名
      • 16.2.1 IP 地址
      • 16.2.2 域名
    • 16.3 端口号
    • 16.4 网络通信协议
      • 16.4.1 TCP、UDP
      • 16.4.2 TCP 的三次握手
      • 16.4.2 TCP 的四次挥手
    • 16.5 网络编程 API
      • 16.5.1 InetAddress 类
      • 16.5.2 Socket 类
      • 16.5.3 DatagramSocket 类
      • 16.5.4 DatagramPacket 类
    • 16.6 TCP 网络编程
      • 16.6.1 通信模型
      • 16.6.2 开发步骤
    • 16.7 UDP 网络编程
      • 16.7.1 通信模型
      • 16.7.2 开发步骤
    • 16.8 URL 编程
    • 16.9 企业真题
  • 第 17 章 反射机制
    • 17.1 反射的概述
    • 17.2 Class:反射的源头
    • 17.3 类的加载过程、类的加载器
    • 17.4 反射的应用 1:创建运行时类的对象
    • 17.5 反射的应用 2:获取运行时类所有的结构
    • 17.6 反射的应用 3:调用指定的结构
    • 17.7 反射的应用 4:注解的使用
    • 17.8 体会:反射的动态性
    • 17.9 企业真题
  • 第 18 章 JDK8 到 JDK17 新特性
    • 18.1 JDK 新特性的概述
    • 18.2 JDK8:lambda 表达式
    • 18.3 函数式接口
    • 18.4 方法引用、构造器引用、数组引用
    • 18.5 JDK8:Stream API
    • 18.6 JDK8 之后的新特性:语法层面
    • 18.7. JDK8 之后的新特性:Optional API
    • 18.8 企业真题

第 9 章 异常处理

9.1 异常的概述

9.1.1 什么是异常?

指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致 JVM 的非正常停止。

9.1.2 异常的抛出机制

Java 中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出(throw)。

然后程序员可以捕获(catch)到这个异常对象,并处理。如果没有捕获(catch)这个异常对象,那么这个异常对象将会导致程序终止。

9.1.3 如何对待异常

对于程序出现的异常,一般有两种解决方法:

  • 一是遇到错误就终止程序的运行。
  • 另一种方法是程序员在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检测、以及异常的处理,保证代码的健壮性。

9.2 异常的体系结构及常见的异常

9.2.1 Throwable

  • java.lang.Throwable:异常体系的根父类。

  • Throwable 中的常用方法:

    • public void printStackTrace():
      打印异常的详细信息。
      包含了异常的类型、异常的原因、异常出现的位置、在开发和调试阶段都得使用 printStackTrace()。
    • public String getMessage():
      获取发生异常的原因。

9.2.2 Error 和 Exception

  • Throwable 可分为两类:
    Error 和 Exception。
    分别对应着 java.lang.Error 与 java.lang.Exception 两个类。

  • Error:
    Java 虚拟机无法解决的严重问题。
    如:
    JVM 系统内部错误、资源耗尽等严重情况。
    一般不编写针对性的代码进行处理。

    • 例如:
      StackOverflowError(栈内存溢出)和 OutOfMemoryError(堆内存溢出,简称 OOM)。
  • Exception:
    其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。

    • 例如:
      空指针访问。
      试图读取不存在的文件。
      网络连接中断。
      数组角标越界。

9.3 编译时异常和运行时异常

Java 程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发生。

  • 比如:
    除数为 0,数组下标越界等。

因此,根据异常可能出现的阶段,可以将异常分为:

  • 编译时期异常(即 checked 异常、受检异常):
    在代码编译阶段,编译器就能明确警
    示当前代码可能发生(不是一定发生)xx 异常,并明确督促程序员提前编写处理它的代码。如果程序员没有编写对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,
    或者不是靠加简单判断就可以避免的。

    • 例如:
      FileNotFoundException(文件找不到异常)。
  • 运行时期异常(即 runtime 异常、unchecked 异常、非受检异常):
    在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了 xx 异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。

    • 例如:
      java.lang.RuntimeException:类及它的子类都是运行时异常。

      • 比如:
        ArrayIndexOutOfBoundsException:
        数组下标越界异常。
        ClassCastException:
        类型转换异常。

9.4 异常处理的方式

9.4.1 异常处理概述

  • 过程 1:“抛”

    • “自动抛” : 程序在执行的过程当中,一旦出现异常,就会在出现异常的代码处,自动生成对应异常类的对象,并将此对象抛出。
    • “手动抛” :程序在执行的过程当中,不满足指定条件的情况下,我们主动的使用"throw + 异常类的对象"方式抛出异常对象。
  • 过程 2:“抓”

    • 狭义上讲:try-catch 的方式捕获异常,并处理。
    • 广义上讲:把“抓”理解为“处理”。则此时对应着异常处理的两种方式:
      ① try-catch-finally
      ② throws

9.4.2 方式 1:捕获异常(try-catch-finally)

try-catch-finally 语法格式:

try
{...... //可能产生异常的代码
}
catch(异常类型1 e)
{...... //当产生异常类型1型异常时的处置措施
}
catch(异常类型2 e)
{......     //当产生异常类型2型异常时的处置措施
}
finally
{...... //无论是否发生异常,都无条件执行的语句
}

try-catch-finally 使用细节:

  • 将可能出现异常的代码声明在 try 语句中。一旦代码出现异常,就会自动生成一个对应异常类的对象。并将此对象抛出。
  • 针对于 try 中抛出的异常类的对象,使用之后的 catch 语句进行匹配。一旦匹配上,就进入 catch 语句块进行处理。
  • 一旦处理结束,代码就可继续向下执行。

catch 中异常处理的方式:

  • 自己编写输出的语句。
  • printStackTrace():打印异常的详细信息。 (推荐)
  • getMessage():获取发生异常的原因。

finally 的使用说明:

  • finally 的理解:
    我们将一定要被执行的代码声明在 finally 结构中。
  • 什么样的代码我们一定要声明在 finally 中呢?
    • 我们在开发中,有一些资源(比如:输入流、输出流,数据库连接、Socket 连接等资源),在使用完以后,必须显式的进行关闭操作,否则,GC 不会自动的回收这些资源。进而导致内存的泄漏。
    • 为了保证这些资源在使用完以后,不管是否出现了未被处理的异常的情况下,这些资源能被关闭。我们必须将这些操作声明
      在 finally 中。

9.4.3 方式 2:声明抛出异常类型(throws)

throws 语法格式:

修饰符 返回值类型 方法名(参数) throws 异常类名 1,异常类名 2...
{// 可能存在编译时异常
}

是否真正处理了异常?

  • 从编译是否能通过的角度看,给出了异常万一出现时候的解决方案。此方案就是,继续向上抛出(throws)。
  • 但是,此 throws 的方式,仅是将可能出现的异常抛给了此方法的调用者。此调用者仍然需要考虑如何处理相关异常。从这个角度来看,throws 的方式不算是真正意义上处理了异常。

方法的重写 throws 的要求:

  • 子类重写的方法抛出的异常类型可以与父类被重写的方法抛出的异常类型相同,或是父类被重写的方法抛出的异常类型的子类。

9.4.4 如何选择异常处理的两种方式?

  • 如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用 try-catch-finally 来处理,保证不出现内存泄漏。
  • 如果父类被重写的方法没有 throws 异常类型,则子类重写的方法中如果出现异常,只能考虑使用 try-catch-finally 进行处理,不能 throws。
  • 开发中,方法 a 中依次调用了方法 b,c,d 等方法,方法 b,c,d 之间是递进关系。此时,如果方法 b,c,d 中有异常,我们通常选择使用 throws,而方法 a 中通常选择使用 try-catch-finally。

9.5 手动 throw 异常对象

Java 中异常对象的生成有两种方式:

  • 由虚拟机自动生成:
    程序运行过程中,虚拟机检测到程序发生了问题,那么针对当前代码,就会在后台自动创建一个对应异常类的实例对象并抛出。
  • 由开发人员手动创建:
    new 异常类型(实参列表);
    如果创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,但是一旦 throw 抛出,就会对程序运行产生影响了。

9.6 自定义异常类

9.6.1 为什么需要自定义异常类

  • Java 中不同的异常类,分别表示着某一种具体的异常情况。那么在开发中总是有些异常情况是核心类库中没有定义好的,此时我们需要根据自己业务的异常情况来定义异常类。
  • 例如年龄负数问题,考试成绩负数问题,某员工已在团队中等。

9.6.2 如何自定义异常类

要继承一个异常类型:

  • 自定义一个编译时异常类型:
    自定义类继承 java.lang.Exception。
  • 自定义一个运行时异常类型:
    自定义类继承 java.lang.RuntimeException。

建议大家提供至少两个构造器,一个是无参构造,一个是(String message)构造器。

自定义异常需要提供 serialVersionUID。

9.7 企业真题

  1. Java 的异常体系简单介绍下。
    答:
    Java 的异常体系是指 Java 语言中用于处理程序运行时错误的一组类和接口,它们构成了一个层次化的结构。在 Java 中,异常被定义为一种表示程序运行时错误的对象,它们可以由系统或程序员自己抛出,被程序的调用栈捕获并处理。Java 的异常体系主要包括以下几个部分:
    ① Throwable 类:Throwable 是所有异常类的父类,它定义了常见的异常处理方法,如 getMessage()、printStackTrace()等。
    ② Error 类:Error 是一种表示严重的系统级错误的异常,如 OutOfMemoryError、StackOverflowError 等,程序一般无法恢复。
    ③ Exception 类:Exception 是一种表示程序运行时错误的异常,它可以被程序处理并恢复。Exception 又分为两种:
    RuntimeException 类:RuntimeException 是一种表示程序逻辑错误的异常,如 NullPointerException、ArrayIndexOutOfBoundsException 等。
    非 RuntimeException 类:非 RuntimeException 是一种表示程序外部错误的异常,如 IOException、SQLException 等。
    ④ Checked 异常:Checked 异常是指在编译时必须处理的异常,如 IOException、SQLException 等。
    ⑤ Unchecked 异常:Unchecked 异常是指在编译时不必处理的异常,如 NullPointerException、ArrayIndexOutOfBoundsException 等。

    Java 的异常体系为程序员提供了一种有效的处理程序运行时错误的方式,使得程序的稳定性和可靠性得到提高。

  2. Java 异常处理机制?
    答:
    两种处理方案:try-catch-finally、throws。

    Java 异常处理机制是一种用于处理程序运行时错误的机制。当程序出现错误时,会抛出一个异常对象,这个异常对象可以被程序捕获并进行处理。Java 中的异常分为两种:受检异常和非受检异常。

    受检异常(Checked Exception)必须在方法签名中声明,并且在调用该方法时必须进行处理,否则编译器会报错。常见的受检异常有 IOException、SQLException 等。

    非受检异常(Unchecked Exception)不需要在方法签名中声明,也不需要进行处理。常见的非受检异常有 NullPointerException、ArrayIndexOutOfBoundsException 等。

    Java 中的异常处理机制主要包括 try-catch-finally 语句块和 throw 语句。try-catch-finally 语句块用于捕获和处理异常,throw 语句用于抛出异常。在 try 块中执行代码,如果发生异常则进入 catch 块,如果没有发生异常则跳过 catch 块。无论是否发生异常,finally 块中的代码都会执行。

  3. 异常的两种类型,Error 和 Exception 的区别?
    答:
    Error 和 Exception 都是 Java 中的 Throwable 类的子类,但它们之间有一些区别。

    Error 是指 JVM 无法处理的严重问题,通常是由于系统资源耗尽或其他不可恢复的错误导致的。例如,OutOfMemoryError,StackOverflowError 等。Error 通常是不可处理的,程序应该尽可能地避免出现这种情况。

    Exception 是指可以被程序处理的异常。它们通常由程序错误或意外情况引起,例如输入无效,文件不存在等。Exception 分为两种类型:checked exception 和 unchecked exception。checked exception 是在编译时就可以被检测到的异常,必须在代码中处理或声明抛出。unchecked exception 是在运行时才能被检测到的异常,不需要在代码中处理或声明抛出。

    总之,Error 是无法处理的异常,而 Exception 是可以被处理的异常。

  4. 运行时异常与一般异常有何异同?
    答:
    运行时异常和一般异常的主要区别在于编译期检查和处理的时机不同。

    一般异常(Checked Exception)必须在编译期间显式地捕获和处理,否则代码就无法编译通过。这种异常通常是由于外部因素导致的,例如文件不存在、网络连接失败等,程序员需要在代码中显式地处理这些异常情况。

    而运行时异常(Unchecked Exception)则不需要在编译期间显式地捕获和处理,可以在运行期间由虚拟机自动抛出。这种异常通常是由于程序逻辑错误导致的,例如空指针异常、数组越界异常等。程序员可以通过编写正确的代码来避免这些异常的发生,但是如果发生了,通常也无法通过捕获和处理来修复程序的错误。

    因此,一般异常通常是由程序员自己引起,并需要在代码中显式地处理;而运行时异常通常是由程序逻辑错误引起的,程序员需要通过编写正确的代码来避免这些异常的发生。

  5. 说几个你常见到的异常?
    答:
    ① NullPointerException:当调用一个空对象的方法或访问其属性时抛出。
    ② ArrayIndexOutOfBoundsException:当访问数组元素超出其范围时抛出。
    ③ ClassCastException:当试图将一个对象强制转换为不兼容的类型时抛出。
    ④IllegalArgumentException:当传递的参数不符合方法要求时抛出。
    ⑤ IOException:当发生输入/输出异常时抛出。
    ⑥ FileNotFoundException:当尝试打开不存在的文件时抛出。
    ⑦ ArithmeticException:当发生算术错误时抛出,如除以零。
    ⑧ InterruptedException:当线程在等待或睡眠时被中断时抛出。
    ⑨ UnsupportedOperationException:当不支持请求的操作时抛出。
    ⑩ SQLException:当访问数据库时发生错误时抛出。

  6. 说说 final、finally、finalize 的区别?
    答:
    final 是 Java 中的关键字,可以用来修饰类、方法和变量。当一个类被声明为 final 时,它不能被继承;当一个方法被声明为 final 时,它不能被重写;当一个变量被声明为 final 时,它的值不能被修改。

    finally 是 Java 中的关键字,用于定义一个代码块,无论是否发生异常,该代码块中的代码都会被执行。通常用于释放资源、关闭连接等操作。

    finalize 是 Java 中的 Object 类中的一个方法,用于在对象被垃圾回收器回收之前执行一些清理操作。一般不建议在代码中显式调用该方法,而是让垃圾回收器自动调用。

  7. 如果不使用 try-catch,程序出现异常会如何?
    答:
    如果不使用 try-catch,程序出现异常会导致程序崩溃,并在控制台输出异常信息。这可能会导致数据丢失,影响程序的正常运行。使用 try-catch 可以捕获异常并进行处理,避免程序崩溃,保证程序的稳定性和可靠性。

  8. try-catch 捕捉的是什么异常?
    答:
    try-catch 可以捕捉任何类型的异常,包括系统异常、运行时异常和自定义异常。

  9. 如果执行 finally 代码块之前方法返回了结果或者 jvm 退出了,这时 finally 块中的代码还会执行吗?
    答:
    如果方法返回了结果或者 JVM 退出了,finally 块中的代码也会执行。finally 块是用来确保在任何情况下都会执行特定的代码,无论是正常情况下还是出现异常的情况下。因此,即使方法返回了结果或者 JVM 退出了,finally 块中的代码也会被执行。

    特别的,如果在方法中调用了 System.exit(0),JVM 会立即退出,finally 块中的代码将不会执行。因为 System.exit(0)会直接终止 JVM,包括所有线程的执行,因此 finally 块中的代码也无法执行。

  10. 在 try 语句中有 return 语句,最后写 finally 语句,finally 语句中的的代码会不会执行?何时执行?如果执行是在 return 前还是后?
    答:
    在 try 语句中有 return 语句时,finally 语句中的代码一定会执行。finally 语句中的代码会在 return 语句执行之前执行。即使在 try 语句块中使用了 return 语句,finally 语句中的代码也会被执行,确保代码块中的资源被正确释放。

  11. 捕获异常在 catch 块里一定会进入 finally 吗?catch 里能 return 吗?catch 里 return 还会进 finally 吗?在 try 里 return 是什么情况?
    答:
    捕获异常在 catch 块里不一定会进入 finally,只有在 try 或 catch 块中有 return、throw 或异常抛出时,才会进入 finally 块。

    catch 里可以 return,但是要注意在 return 之前可能需要释放资源或做一些清理工作。catch 里 return 不会进入 finally。

    在 try 里 return 时,如果 try 块中的代码执行完毕并成功返回,则会直接跳出 try 块并返回值,不会进入 finally。但是如果 try 块中的代码抛出异常,则会先进入 catch 块,处理完异常后再进入 finally 块。

  12. throw 和 throws 的区别?
    答:
    throw 和 throws 是 Java 中的关键字,用于异常处理。

    throw 用于手动抛出一个异常,它通常用于方法体内部,用于抛出一个异常对象。例如:

    public void method()
    {if (somethingIsWrong){throw new MyException("出了问题。");}
    }
    

    在上面的例子中,如果 somethingIsWrong 为 true,就会抛出一个 MyException 异常。

    throws 则用于方法声明上,用于声明该方法可能会抛出哪些异常。例如:

    public void method() throws MyException
    {// 做一些可能引发MyException的事情
    }
    

    在上面的例子中,方法 method 可能会抛出一个 MyException 异常,因此在方法声明上使用了 throws 关键字。

    总结:
    throw 用于抛出一个异常对象,通常用于方法体内部。
    throws 用于方法声明上,用于声明该方法可能会抛出哪些异常。

  13. 子类重写父类抛出异常的方法,能否抛出比父类更高级别的异常类?
    答:
    不可以。子类重写父类抛出异常的方法时,抛出的异常类必须是父类方法抛出异常类的子类或者相同的异常类。抛出更高级别的异常类会破坏异常处理机制的完整性,可能导致程序无法正常处理异常。

  14. 如何自定义一个异常?
    答:
    在 Java 中,我们可以通过继承 Exception 或 RuntimeException 来自定义一个异常类。其中,Exception 表示编译时异常,必须在方法声明中抛出或捕获处理;而 RuntimeException 表示运行时异常,可以不在方法声明中抛出或捕获处理。我们一般建议自定义异常类时继承 Exception。

    自定义异常类需要满足以下要求:
    ① 继承 Exception 或 RuntimeException。
    ② 提供至少两个构造方法,一个是无参构造方法,一个是带有 String 参数的构造方法,用于传递异常信息。
    ③ 建议为异常类提供 serialVersionUID,用于序列化和反序列化操作。
    下面是一个自定义异常类的示例代码:

    public class MyException extends Exception
    {private static final long serialVersionUID = 1L;public MyException(){super();}public MyException(String message){super(message);}}
    

    使用自定义异常类时,可以在方法声明中抛出异常,并在方法体中使用 throw 语句抛出自定义异常。例如:

    public void doSomething() throws MyException
    {// 做点什么throw new MyException("出了问题");
    }
    

    在调用该方法时,需要使用 try-catch 语句捕获该异常或继续向上抛出该异常。例如:

    public static void main(String[] args)
    {try{doSomething();}catch (MyException e){e.printStackTrace();}
    }
    

第 10 章 多线程

10.1 程序、进程与线程

程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码。

进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。程序是静态的,进程是动态的。

进程作为操作系统调度和分配资源的最小单位。

线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。

线程作为 CPU 调度和执行的最小单位。

线程调度策略:

  • 分时调度:所有线程轮流使用 CPU 的使用权,并且平均分配每个线程占用 CPU 的时间。
  • 抢占式调度:让优先级高的线程以较大的概率优先使用 CPU。如果线程的优先级相同,那么会随机选择一个(线程随机性),Java 使用的为抢占式调度。

单核 CPU 与多核 CPU。

并行与并发:

  • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个 CPU 上同时执行。比如:多个人同时做不同的事。

  • 并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同
    时执行的效果。

10.2 创建多线程

10.2.1 方式 1:继承 Thread 类

Java 通过继承 Thread 类来创建并启动多线程。

步骤如下:
① 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程需要完成的任务。
② 创建 Thread 子类的实例,即创建了线程对象。
③ 调用线程对象的 start()方法来启动该线程。

代码示例:

// 自定义线程类
public class MyThread extends Thread
{// 定义指定线程名称的构造方法public MyThread(String name){// 调用父类的 String 参数的构造方法,指定线程的名称super(name);}// 重写 run 方法,完成该线程执行的逻辑@Overridepublic void run(){for (int i = 0; i < 10; i++){System.out.println(getName()+":正在执行!"+i);}}
}public class TestMyThread
{public static void main(String[] args){// 创建自定义线程对象 1MyThread mt1 = new MyThread("子线程 1");// 开启子线程 1mt1.start();// 创建自定义线程对象 2MyThread mt2 = new MyThread("子线程 2");// 开启子线程 2mt2.start();// 在主方法中执行 for 循环for (int i = 0; i < 10; i++){System.out.println("main 线程!"+i);}}
}

10.2.2 方式 2:实现 Runnable 接口

Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法。

步骤如下:
① 定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样是该线程的线程执行体。
② 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正 的线程对象。
③ 调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的 run 方法。

代码示例:

public class MyRunnable implements Runnable
{@Overridepublic void run(){for (int i = 0; i < 20; i++){System.out.println(Thread.currentThread().getName() + " " + i);}}
}public class TestMyRunnable
{public static void main(String[] args){// 创建自定义类对象,线程任务对象MyRunnable mr = new MyRunnable();// 创建线程对象Thread t = new Thread(mr, "长江");t.start();for (int i = 0; i < 20; i++){System.out.println("黄河 " + i);}}
}

10.2.3 方式 3:实现 Callable 接口 (jdk5.0 新增)

与使用 Runnable 相比, Callable 功能更强大些。

  • 相比 run()方法,可以有返回值。
  • 方法可以抛出异常。
  • 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)。

Future 接口:

  • 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutureTask 是 Futrue 接口的唯一的实现类。
  • FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值

缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。

代码示例:

// 1.创建一个实现 Callable 的实现类
class NumThread implements Callable
{// 2.实现 call 方法,将此线程需要执行的操作声明在 call()中@Overridepublic Object call() throws Exception{int sum = 0;for (int i = 1; i <= 100; i++){if (i % 2 == 0){System.out.println(i);sum += i;}}return sum;}
}public class CallableTest
{public static void main(String[] args){// 3.创建 Callable 接口实现类的对象NumThread numThread = new NumThread();// 4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,创建 FutureTask 的对象FutureTask futureTask = new FutureTask(numThread);// 5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Thread 对象,并调用 start()new Thread(futureTask).start();// 接收返回值try{// 6.获取 Callable 中 call 方法的返回值// get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 call()的返回值。Object sum = futureTask.get();System.out.println("总和为:" + sum);}catch (InterruptedException e){e.printStackTrace();}catch (ExecutionException e){e.printStackTrace();}}
}

10.2.4 方式 4:使用线程池(jdk5.0 新增)

现有问题:

  • 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
  • 那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

思路:

  • 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

代码示例:

class NumberThread1 implements Runnable
{@Overridepublic void run(){for(int i = 0;i <= 100;i++){if(i % 2 == 0){System.out.println(Thread.currentThread().getName() + ": " + i);}}}
}class NumberThread2 implements Runnable
{@Overridepublic void run(){for(int i = 0;i <= 100;i++){if(i % 2 != 0){System.out.println(Thread.currentThread().getName() + ": " + i);}}}
}class NumberThread3 implements Callable
{@Overridepublic Object call() throws Exception{int evenSum = 0;    // 记录偶数的和for(int i = 0;i <= 100;i++){if(i % 2 == 0){evenSum += i;}}return evenSum;}
}public class ThreadPoolTest
{public static void main(String[] args){// 1. 提供指定线程数量的线程池ExecutorService service1 = Executors.newFixedThreadPool(10);ThreadPoolExecutor service2 = (ThreadPoolExecutor) service1;// 设置线程池的属性// 设置线程池中线程数的上限service2.setMaximumPoolSize(50);// 2.执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接口实现类的对象service1.execute(new NumberThread1());    // 适合适用于 Runnableservice1.execute(new NumberThread2());   // 适合适用于 Runnabletry{Future future = service1.submit(new NumberThread2());  // 适合使用于 CallableSystem.out.println("总和为:" + future.get());}catch (Exception e){e.printStackTrace();}// 3.关闭连接池service1.shutdown();}
}

10.3 Thread 类的常用方法

10.3.1 构造器

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口中的 run 方法。
  • public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

10.3.2 常用方法

  • public void run() :此线程要执行的任务在此处定义代码。
  • public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
  • public String getName() :获取当前线程名称。
  • public void setName(String name):设置该线程名称。
  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类。
  • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
  • public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调度器又将其调度出来重新执行。
  • public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
  • void join() :等待该线程终止。
  • void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果 millis 时间到,将不再等待。
  • void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
  • public final void stop():已过时,不建议使用。强行结束一个线程的执行,直接进入死亡状态。run()即刻停止,可能会导致一些清理性的工作得不到完成,如文件,数据库等的关闭。同时,会立即释放该线程所持有的所有的锁,导致数据得不到同步的处理,出现数据不一致的问题。
  • void suspend() / void resume() : 这两个操作就好比播放器的暂停和恢复。二者必须成对出现,否则非常容易发生死锁。suspend()调用会导致线程暂停,但不会释放任何锁资源,导致其它线程都无法访问被它占用的锁,直到调用 resume()。已过时,不建议使用。
  • setDaemon(true):调用 setDaemon(true)方法可将指定线程设置为守护线程。

10.3.3. 线程的优先级

  • Thread 类内部声明的三个常量:

    • MAX_PRIORITY(10):最高优先级。
    • MIN _PRIORITY (1):最低优先级。
    • NORM_PRIORITY (5):普通优先级,默认情况下 main 线程具有普通优先级。
  • getPriority():获取线程的优先级。

  • setPriority():设置线程的优先级,范围[1,10]。

10.4 多线程的生命周期

10.4.1 jdk5.0 之前的线程的生命周期

线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU 需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。

10.4.2 jdk5.0 之后的线程的生命周期

jdk5.0 及之后,Thread 类中定义了一个内部类 State。

public enum State
{NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;
}

10.5 线程安全问题

什么是线程的安全问题?多个线程操作共享数据,就有可能出现安全问题。

10.5.1 同步机制解决线程安全问题

要解决多线程并发访问一个资源的安全性问题,也就是解决重复票与不存在票问题,Java 中提供了同步机制 (synchronized)来解决。

10.5.2 同步机制解决线程安全问题的原理

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代码,都要先获得“锁”,我们称它为同步锁。

因为 Java 对象在堆中的数据分为分为对象头、实例变量、空白的填充。

对象头中包含:

  • Mark Word:记录了和当前对象有关的 GC、锁标记等信息。
  • 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。
  • 数组长度(只有数组对象才有)。

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的 ID,这样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新获得/占用”同步锁“对象。

10.5.3 同步代码块

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块的资源实行互斥访问。

语法格式:

synchronized(同步锁)
{需要同步操作的代码
}

10.5.4 同步方法

同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能进入这个方法,其他线程在外面等着。

语法格式:

public synchronized void method()
{可能会产生线程安全问题的代码
}

10.5.5 同步锁机制

  • 重点关注两个事:

    • 共享数据及操作共享数据的代码。
    • 同步监视器,即同步锁要保证唯一性。
  • 在实现 Runnable 接口的方式中,同步监视器可以考虑使用:this。

  • 在继承 Thread 类的方式中,同步监视器要慎用 this,可以考虑使用:当前类.class。

  • 非静态的同步方法,默认同步监视器是 this

  • 静态的同步方法,默认同步监视器是当前类本身。

10.5.6 Lock 锁(jdk5.0 新增)

与采用 synchronized 相比,Lock 可提供多种锁方案,更灵活、更强大。Lock 通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

  • ReentrantLock 类实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一
    些特性。此外,它还提供了在激烈争用情况下更佳的性能。

Lock 锁也称同步锁,加锁与释放锁方法,如下:

  • public void lock() :加同步锁。
  • public void unlock() :释放同步锁。

代码示例:

class LockTest
{// 1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例private final ReentrantLock lock = new ReenTrantLock();public static void main(String[] args){// 2. 调动 lock(),实现需共享的代码的锁定lock.lock();try{//保证线程安全的代码;}finally{// 3. 调用 unlock(),释放共享代码的锁定lock.unlock();}}
}

10.6 同步机制相关的问题

10.6.1 单例设计模式的线程安全问题

懒汉式单例模式的线程安全写法可以使用双重检查锁定(Double-Checked Locking)来实现。具体实现如下:

public class Singleton
{private static volatile Singleton instance; // volatile关键字确保instance的可见性和顺序性private Singleton(){}public static Singleton getInstance(){if (instance == null){// 第一次检查,如果instance已经存在,则直接返回synchronized (Singleton.class){// 加锁,确保只有一个线程进入临界区if (instance == null){// 第二次检查,防止多个线程同时进入第一次检查后的if语句块instance = new Singleton();}}}return instance;}
}

在以上代码中,使用了双重检查锁定来确保线程安全。第一次检查用于判断 instance 是否已经存在,如果已经存在,则直接返回;否则进入临界区加锁。在加锁后,再次检查 instance 是否已经存在,防止多个线程同时进入第一次检查后的 if 语句块。如果 instance 仍然为 null,则创建一个新的实例。最后,返回 instance。同时,使用 volatile 关键字确保 instance 的可见性和顺序性,避免由于指令重排而导致的线程安全问题。

10.6.2 死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

诱发死锁的原因:

  • 互斥条件。
  • 占用且等待。
  • 不可抢夺(或不可抢占)。
  • 循环等待。
  • 以上 4 个条件,同时出现就会触发死锁。

解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。

  • 针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
  • 针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
  • 针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
  • 针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。

10.7 线程间的通信

  • 在同步机制下,考虑线程间的通信。
  • wait():
    线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它还要等着别的线程执行一个特别的动作,也即“通知(notify)”或者等待时间到,在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列(ready queue)中。
  • notify():则选取所通知对象的 wait set 中的一个线程释放。
  • notifyAll():则释放所通知对象的 wait set 上的全部线程。

10.8 生产者与消费者问题

等待唤醒机制可以解决经典的“生产者与消费者”的问题。生产者与消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

生产者与消费者问题中其实隐含了两个问题:

  • 线程安全问题:因为生产者与消费者共享数据缓冲区,产生安全问题。不过这个问题可以使用同步解决。
  • 线程的协调工作问题: - 要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产
    者往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题。

代码示例:

public class ConsumerProducerTest
{public static void main(String[] args){Clerk clerk = new Clerk();Producer p1 = new Producer(clerk);Consumer c1 = new Consumer(clerk);Consumer c2 = new Consumer(clerk);p1.setName("生产者 1");c1.setName("消费者 1");c2.setName("消费者 2");p1.start();c1.start();c2.start();}
}// 生产者
class Producer extends Thread
{private Clerk clerk;public Producer(Clerk clerk){this.clerk = clerk;}@Overridepublic void run(){System.out.println("=========生产者开始生产产品========");while (true){try{Thread.sleep(40);}catch (InterruptedException e){e.printStackTrace();}// 要求 clerk 去增加产品clerk.addProduct();}}
}// 消费者
class Consumer extends Thread
{private Clerk clerk;public Consumer(Clerk clerk){this.clerk = clerk;}@Overridepublic void run(){System.out.println("=========消费者开始消费产品========");while (true){try{Thread.sleep(90);}catch (InterruptedException e){e.printStackTrace();}// 要求 clerk 去减少产品clerk.minusProduct();}}
}//资源类
class Clerk
{private int productNum = 0;    // 产品数量private static final int MAX_PRODUCT = 20;private static final int MIN_PRODUCT = 1;// 增加产品public synchronized void addProduct(){if (productNum < MAX_PRODUCT){productNum++;System.out.println(Thread.currentThread().getName() + "生产了第" + productNum + "个产品");// 唤醒消费者this.notifyAll();}else{try{this.wait();}catch (InterruptedException e){e.printStackTrace();}}}// 减少产品public synchronized void minusProduct(){if (productNum >= MIN_PRODUCT){System.out.println(Thread.currentThread().getName() + "消费了第" + productNum + "个产品");productNum--;// 唤醒生产者this.notifyAll();}else{try{this.wait();}catch (InterruptedException e){e.printStackTrace();}}}
}

10.9 企业真题

  1. 什么是线程?
    答:
    线程是操作系统能够进行运算调度的最小单位,它被包含在进程中,是进程中的实际运作单位。一个进程可以包含多个线程,每个线程之间可以共享进程的资源,如内存、文件句柄等,同时也可以拥有自己的独立资源,如程序计数器、寄存器等。线程可以在同一进程内并发执行,提高了程序的运行效率。

  2. 线程和进程有什么区别?
    答:
    ① 定义:进程是计算机中正在运行的一个程序,它拥有自己的内存空间和系统资源,而线程是进程中的一个执行单元,一个进程可以包含多个线程。
    ② 资源占用:进程拥有独立的内存空间和系统资源,而线程共享所属进程的资源,包括内存、文件等。
    ③ 并发性:进程之间相互独立,互不干扰,而线程之间共享进程的资源,需要通过同步机制来保证并发性。
    ④ 切换开销:进程间切换需要保存和恢复进程的上下文,开销较大,而线程间切换只需要保存和恢复线程的上下文,开销较小。
    ⑤ 安全性:由于线程共享进程的资源,如果一个线程出现问题,可能会影响到整个进程的稳定性和安全性,而进程之间相互独立,一个进程出现问题不会影响其他进程的运行。
    ⑥ 独立性:进程之间可以独立运行,可以实现分布式计算和多任务处理,而线程依赖于进程,不能独立运行。
    综上所述,进程和线程都是计算机中实现多任务处理的重要概念,它们各自有自己的特点和优势,应根据实际需求选择合适的方式来实现多任务处理。

  3. 多线程使用场景?
    答:
    ① 处理大量数据:使用多线程可以同时处理多个数据,提高数据处理的速度。
    ② 并发访问:多线程可以同时访问同一个资源,提高资源的利用效率。
    ③ 服务器端编程:服务器需要同时处理多个客户端请求,使用多线程可以提高服务器的并发处理能力。
    ④ 图形界面程序:图形界面程序需要同时处理多个用户输入事件,使用多线程可以提高程序的响应速度。
    ⑤ 大规模计算:使用多线程可以同时进行多个计算任务,提高计算的效率。
    ⑥ 多媒体处理:多媒体处理需要同时进行多个任务,使用多线程可以提高处理速度。
    ⑦ 游戏开发:游戏需要同时处理多个角色的动作,使用多线程可以提高游戏的流畅度和响应速度。
    ⑧ 数据库操作:数据库需要同时处理多个查询和更新请求,使用多线程可以提高数据库的并发处理能力。

  4. 如何在 Java 中出实现多线程?
    答:
    ① 继承 Thread 类并重写 run()方法。

    public class MyThread extends Thread
    {@Overridepublic void run(){// 业务逻辑}
    }// 启动线程
    MyThread thread = new MyThread();
    thread.start();
    

    ② 实现 Runnable 接口并重写 run()方法。

    public class MyRunnable implements Runnable
    {@Overridepublic void run(){// 业务逻辑}
    }// 启动线程
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    

    ③ 实现 Callable 接口并重写 call()方法。

    public class MyCallable implements Callable<String>
    {@Overridepublic String call() throws Exception{// 业务逻辑return "Hello World!";}
    }// 启动线程
    MyCallable callable = new MyCallable();
    Future<String> future = executor.submit(callable);
    String result = future.get();
    

    ④ 使用线程池

    public class ThreadPoolExample
    {public static void main(String[] args){// 创建一个固定大小的线程池,其中包含5个线程ExecutorService executor = Executors.newFixedThreadPool(5);// 提交10个任务给线程池执行for (int i = 1; i <= 10; i++){executor.submit(new Task(i));}// 关闭线程池executor.shutdown();}// 定义一个任务类,实现Runnable接口static class Task implements Runnable{private int taskId;public Task(int taskId){this.taskId = taskId;}@Overridepublic void run(){System.out.println("任务 " + taskId + " 正在线程中运行。 " + Thread.currentThread().getName());}}
    }
    
  5. Thread 类中的 start()和 run()有什么区别?
    答:
    start()方法用于启动一个新的线程,而 run()方法只是普通的方法调用。当调用 start()方法时,系统会创建一个新的线程,并在该线程中执行 run()方法。如果直接调用 run()方法,那么该方法就会在当前线程中执行,不会创建新的线程。因此,如果想要实现多线程,必须使用 start()方法来启动新线程,而不是直接调用 run()方法。

  6. 启动一个线程是用 run()还是 start()?
    答:
    启动一个线程应该使用 start()方法,而不是直接调用 run()方法。因为直接调用 run()方法不会创建一个新的线程,而只是在当前线程中执行 run()方法的代码。而使用 start()方法会创建一个新的线程并在新线程中执行 run()方法的代码。这样可以避免阻塞当前线程的执行,提高程序的并发性能。

  7. Java 中 Runnable 和 Callable 有什么不同?
    答:
    在 Java 中,Runnable 和 Callable 都是用来实现多线程的接口,但是它们有以下不同点:
    ① 返回值类型不同:Runnable 的 run()方法没有返回值,而 Callable 的 call()方法有返回值。
    ② 异常处理不同:Runnable 的 run()方法不能抛出 checked exception,但是 Callable 的 call()方法可以抛出 checked exception,需要在调用时进行处理。
    ③ 使用方式不同:Runnable 接口通常用于执行一些没有返回值的简单任务,而 Callable 接口通常用于执行一些有返回值的复杂任务,例如计算、查询等。
    ④ 使用线程池时不同:如果使用线程池来执行任务,Runnable 可以直接提交给线程池执行,但是 Callable 需要使用 Future 来获取返回值。

    总之,Runnable 和 Callable 都是用来实现多线程的接口,但是它们的使用方式和作用略有不同。

  8. 什么是线程池,为什么要使用它?
    答:
    线程池是一种管理和复用线程的机制,它可以在应用程序中预先创建一定数量的线程,并将这些线程放入一个池中,当需要执行任务时,从线程池中取出一个线程来执行任务,任务执行完毕后,该线程不被销毁,而是返回线程池等待下一次任务的到来。

    使用线程池的好处在于:
    ① 减少线程的创建和销毁次数,避免频繁创建和销毁线程对系统资源的消耗。
    ② 提高线程的复用率,使线程可以被重复利用,减少线程的阻塞等待时间。
    ③ 控制线程的数量,避免线程数量过多导致系统资源的浪费和线程调度的开销。
    ④ 提高系统的稳定性和可靠性,避免线程的崩溃和异常对系统的影响。
    ⑤ 提高程序的响应速度和吞吐量,提高程序的并发能力。

  9. sleep() 和 yield()区别?
    答:
    sleep()和 yield()都是线程控制的方法,但它们的作用不同。
    ① sleep()会让当前线程进入阻塞状态,让出 CPU,不参与调度,直到休眠时间结束或被中断才会继续执行。sleep()方法是静态方法,可以通过 Thread 类直接调用。
    ② yield()会让当前线程让出 CPU,但是不会进入阻塞状态,而是重新回到就绪状态,等待线程调度器再次调度。yield()方法也是静态方法,可以通过 Thread 类直接调用。

    因此,sleep()和 yield()的区别在于 sleep()会让线程进入阻塞状态,而 yield()则不会。另外,sleep()方法可以指定休眠的时间,而 yield()方法则不可以。

  10. 线程创建的中的方法、属性?
    答:
    线程创建时可以指定以下方法和属性:

    方法:
    ① start():启动线程。
    ② run():线程的执行体,需要自行实现。
    ③ join():等待线程执行完毕。
    ④ interrupt():中断线程。

    属性:
    ① name:线程名字。
    ② daemon:是否为守护线程。
    ③ priority:线程优先级。
    ④ isAlive():线程是否处于活动状态。
    ⑤ isDaemon():线程是否为守护线程。
    ⑥ isInterrupted():线程是否被中断。

  11. 线程的生命周期?
    答:
    Java 中线程的生命周期包括以下五个状态:
    ① 新建状态(New):当线程对象被创建时,它处于新建状态。此时,线程还没有开始执行,也没有分配到系统资源。
    ② 就绪状态(Runnable):当线程调用 start()方法后,它进入就绪状态。此时,线程已经分配到了系统资源,但还没有开始执行。处于就绪状态的线程会等待 CPU 时间片的分配。
    ③ 运行状态(Running):当线程获得 CPU 时间片后,它进入运行状态。此时,线程开始执行 run()方法中的代码。
    ④ 阻塞状态(Blocked):线程在等待某个条件的时候,可能会进入阻塞状态。比如,线程在等待获取锁时,如果锁已经被其他线程占用,则该线程会进入阻塞状态。
    ⑤ 终止状态(Terminated):当线程执行完 run()方法中的代码后,它就进入了终止状态。此时,线程已经完成了它的任务,不再占用系统资源。

  12. 线程的基本状态以及状态之间的关系?
    答:
    线程有五种基本状态:
    ① 新建状态(New):当线程对象被创建时,它处于新建状态。
    ② 就绪状态(Runnable):当线程被启动后,它进入就绪状态,等待系统调度它运行。
    ③ 运行状态(Running):当线程被系统调度后,它进入运行状态,开始执行任务。
    ④ 阻塞状态(Blocked):当线程因为某些原因无法运行时,它进入阻塞状态。
    ⑤ 终止状态(Terminated):当线程执行完任务后,或者因为异常或其他原因被强制终止时,它进入终止状态。

    线程状态之间的关系如下:
    ① 新建状态可以转化为就绪状态。
    ② 就绪状态可以转化为运行状态。
    ③ 运行状态可以转化为就绪状态或阻塞状态。
    ④ 阻塞状态可以转化为就绪状态。
    ⑤ 运行状态或阻塞状态可以转化为终止状态。

  13. stop()和 suspend()方法为何不推荐使用?
    答:
    stop()方法会立即停止线程,可能会导致线程资源无法释放,造成死锁或者数据不一致等问题,因此不推荐使用。

    suspend()方法会暂停线程,但是不会释放线程占用的资源,如果在暂停期间其他线程需要使用该资源,就会发生死锁等问题,因此也不推荐使用。同时,suspend()方法还可能导致线程状态不可预测,无法保证线程的正确性。

  14. Java 线程优先级是怎么定义的?
    答:
    Java 线程优先级是通过整数来定义的,范围从 1 到 10。默认情况下,所有线程都具有相同的优先级,即 NORM_PRIORITY(5)。较高的优先级使线程更有可能在竞争资源时获得 CPU 时间,但不能保证高优先级的线程总是优先于低优先级的线程。线程优先级的设置可以使用 Thread 类的 setPriority()方法进行设置。

  15. 你如何理解线程安全的?线程安全问题是如何造成的?
    答:
    线程安全是指当多个线程同时访问同一个共享资源时,不会出现任何意外的结果,即程序执行的结果与单线程执行的结果是一致的。线程安全问题通常是由于多个线程同时访问同一个共享资源时,对该资源的读写操作没有得到正确的同步处理所导致的,例如在并发环境下访问同一个全局变量、共享缓存、共享文件等。这样可能会导致数据不一致、死锁、竞态条件等问题,从而影响程序的正确性和稳定性。为了解决线程安全问题,常见的做法是使用锁、信号量、原子操作等同步机制来保证多个线程对共享资源的访问是有序的,从而避免出现竞争条件和数据不一致的问题。

  16. 多线程共用一个数据变量需要注意什么?
    答:
    ① 线程安全:多线程共用一个数据变量时,需要确保数据的安全性,即线程之间不能互相干扰或破坏数据。
    ② 同步机制:为了保证数据的正确性,需要使用同步机制,例如锁、信号量等,确保同一时间只有一个线程能够访问数据变量。
    ③ 内存可见性:多线程共用一个数据变量时,需要确保数据的内存可见性,即当一个线程修改了数据变量时,其他线程能够及时看到这个变化。
    ④ 死锁问题:当多个线程同时需要访问多个共用的数据变量时,可能会出现死锁问题。因此需要避免多个线程同时访问多个共用的数据变量。
    ⑤ 性能问题:多线程共用一个数据变量时,需要考虑线程之间的调度和竞争问题,以及锁的开销等因素,确保程序的性能。

  17. 多线程保证线程安全一般有几种方式?
    答:
    Java 多线程保证线程安全一般有以下几种方式:
    ① 同步代码块:使用 synchronized 关键字来同步代码块,使得多个线程在执行该代码块时只能有一个线程执行。
    ② 同步方法:将需要同步的方法声明为 synchronized 方法,同一时间只能有一个线程执行该方法。
    ③ Lock 锁:使用 Lock 接口提供的 lock()和 unlock()方法来实现同步,相比于 synchronized 关键字,Lock 锁提供了更灵活的同步方式。
    ④ volatile 关键字:使用 volatile 关键字来保证变量的可见性和禁止指令重排,从而保证多线程环境下变量的正确性。
    ⑤ 原子类:使用 Java 提供的原子类来保证操作的原子性,如 AtomicInteger、AtomicLong 等。
    ⑥ ThreadLocal 类:使用 ThreadLocal 类来实现线程本地变量,保证每个线程都有自己的变量副本,从而避免并发访问问题。
    ⑦ 并发容器:Java 提供了许多线程安全的容器类,如 ConcurrentHashMap、ConcurrentLinkedQueue 等,可以直接使用这些容器类来避免并发访问问题。

  18. 用什么关键字修饰同步方法?
    答:
    Java 中,可以使用 synchronized 关键字修饰同步方法。

  19. synchronized 加载静态方法和普通方法区别?
    答:
    在 Java 中,synchronized 关键字可以用于修饰静态方法和普通方法,但它们之间有一些区别。

    对于静态方法,同步锁是当前类的 Class 对象。而对于普通方法,同步锁是当前实例对象。因此,如果一个线程访问一个静态 synchronized 方法,那么其他线程将不能访问该类的任何其他静态 synchronized 方法,但是可以访问该类的非静态 synchronized 方法。而如果一个线程访问一个非静态 synchronized 方法,那么其他线程将不能访问该实例的其他非静态 synchronized 方法,但是可以访问该类的所有静态 synchronized 方法。

    因此,不同的方法使用不同的同步监视器,这是它们之间的区别。静态方法使用类的 Class 对象作为同步监视器,而普通方法使用实例对象作为同步监视器。

  20. Java 中 synchronized 和 ReentrantLock 有什么不同?
    答:
    synchronized 和 ReentrantLock 都是用于实现线程同步的机制,但它们有以下不同:
    ① 性能:ReentrantLock 性能比 synchronized 更好,因为它是基于 CAS (Compare and Swap) 实现的,而 synchronized 是基于 JVM 实现的。在高并发情况下,ReentrantLock 的性能优势更加明显。
    ② 可中断性:ReentrantLock 具有可中断性,即当一个线程正在等待获取锁时,可以通过调用 lockInterruptibly() 方法来让该线程中断等待,而 synchronized 没有这个特性。
    ③ 公平锁:ReentrantLock 可以是公平锁,即按照等待时间的先后顺序来获取锁,而 synchronized 只能是非公平锁。
    ④ 可重入性:synchronized 是可重入锁,即线程可以重复获取已经持有的锁,而 ReentrantLock 也是可重入锁。
    ⑤ 等待可中断:ReentrantLock 可以设置等待获取锁的超时时间,而 synchronized 没有这个特性。
    ⑥ 条件锁:ReentrantLock 可以通过 Condition 接口来实现条件锁,即只有满足某个条件时才能获取锁,而 synchronized 没有这个特性。

    综上所述,ReentrantLock 相对于 synchronized 更加灵活和强大,但使用起来也更加复杂,需要手动释放锁和处理异常等问题。在需要高并发性能和更多功能的场景下,可以选择使用 ReentrantLock。

  21. 当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法?
    答:
    当一个线程进入一个对象的一个 synchronized 方法后,其它线程无法进入此对象的其他 synchronized 方法,因为它们都需要获取同步监视器(也称为锁)才能执行。同步监视器是对象级别的,因此当一个线程持有同步监视器时,其他线程无法获得该监视器。但是,非 synchronized 方法不需要获取同步监视器,因此可以在同一时间被多个线程并发调用。

  22. 线程同步与阻塞的关系?同步一定阻塞吗?阻塞一定同步吗?
    答:
    线程同步和阻塞是两个概念,它们之间有关联,但并不一定相互依存。

    线程同步是指多个线程在执行过程中,需要协调和同步彼此的操作,以保证数据的正确性和程序的正确性。线程同步可以通过使用锁、信号量等机制来实现。

    阻塞是指线程在执行过程中被暂停,等待某些条件的满足后再继续执行。线程阻塞可以是由于等待某个资源的可用性,或者等待某个操作的完成等原因导致的。

    同步和阻塞之间有关系,因为在进行线程同步的过程中,可能会需要对某些资源进行加锁,这样其他线程就不能访问该资源,从而导致其他线程阻塞。但是同步和阻塞并不一定相互依存,同步操作不一定会导致阻塞,阻塞操作也不一定需要同步。

    例如,线程间的消息传递机制可以实现同步操作,但是不需要阻塞。而单线程中的阻塞操作,也不需要同步。因此,同步和阻塞是两个概念,需要根据具体的场景来进行选择和使用。

  23. 什么是死锁,产生死锁的原因及必要条件?
    答:
    死锁是多个进程或线程在互相等待对方释放资源而陷入无限等待的一种状态。这种状态下,每个进程或线程都在等待其他进程或线程释放资源,而不能继续执行下去。

    死锁产生的原因通常有以下几种:
    ① 资源竞争:多个进程或线程争夺有限的资源,如磁盘、内存等,当某个进程或线程持有一部分资源而又需要其他资源时,就会造成死锁。
    ② 进程或线程的顺序不当:当多个进程或线程按照不同的顺序请求资源时,也容易产生死锁。
    ③ 资源分配不当:当系统分配资源不当时,也会导致死锁的产生。

    死锁产生的必要条件有以下四个:
    ① 互斥条件:至少有一个资源必须处于非共享模式,即一次只能被一个进程或线程使用。
    ② 占有和等待条件:一个进程或线程必须占有至少一个资源,并等待其他进程或线程释放所需的资源。
    ③ 非抢占条件:资源不能被抢占,只能由占有资源的进程或线程显式地释放。
    ④ 循环等待条件:多个进程或线程形成一个循环等待资源的环路。

  24. 如何避免死锁?
    答:
    死锁是指两个或多个进程在执行过程中因争夺资源而产生的一种僵局,彼此都在等待对方先释放资源而无法继续执行下去。以下是避免死锁的几种方法:
    ① 避免资源竞争:尽量避免多个进程同时竞争同一资源,可以通过资源分配策略、资源复制等方式来实现。
    ② 避免持有和等待:一个进程不能持有某个资源并等待另一个进程占用的资源,可以通过一次性获取所有需要的资源来避免此情况。
    ③ 避免循环等待:多个进程之间不能形成循环等待资源的关系,可以通过给资源编号,按照一定顺序申请资源来避免。
    ④ 破坏资源占用和等待条件:通过限制进程的最大资源需求量、强制进程释放已占用的资源等方式来破坏资源占用和等待条件。
    ⑤ 破坏进程等待条件:通过限制进程等待时间、强制进程放弃等待的资源等方式来破坏进程等待条件。
    总之,避免死锁需要综合考虑多种因素,采用合适的方法来解决问题。

  25. Java 中 notify()和 notifyAll()有什么区别?
    答:
    notify()和 notifyAll()都是用于线程间通信的方法,它们的主要区别在于:
    ① notify()只会唤醒等待队列中的一个线程,而 notifyAll()会唤醒等待队列中的所有线程。
    ② notify()是随机唤醒等待队列中的一个线程,而 notifyAll()会唤醒所有等待队列中的线程,让它们去竞争锁。

    因此,如果你想唤醒所有等待队列中的线程,可以使用 notifyAll();如果你只想唤醒一个线程,可以使用 notify()。但是需要注意的是,notify()可能会导致某些线程一直等待下去,因为它只唤醒一个线程,如果唤醒的是不需要的线程,那么这个线程就会一直等待下去,而 notifyAll()则不会有这个问题。

  26. 为什么 wait()和 notify()方法要在同步块中调用?
    答:
    wait()和 notify()方法需要在同步块(synchronized block)中调用,主要是因为它们都需要获取对象的监视器(monitor)才能执行。监视器是 Java 中的一种内部锁机制,用于控制并发访问。在同步块中,线程可以获取对象的监视器,从而保证对共享资源的访问是同步的。

    具体来说,wait()方法会释放对象的监视器,让其他线程可以获取并修改共享资源。notify()方法会通知等待在对象上的一个线程,让其重新竞争该对象的监视器。如果这些方法不在同步块中调用,就可能会导致多个线程同时竞争对象的监视器,从而破坏同步性和线程安全性。

    因此,为了保证线程安全和同步性,wait()和 notify()方法必须在同步块中调用,以确保线程能够正确获取和释放对象的监视器。

  27. 多线程中生产者消费者代码(同步、wait、notifly 编程)?
    答:
    以下是一个使用同步、wait 和 notify 编程的生产者消费者示例:

    public class ProducerConsumer
    {private List<Integer> buffer = new ArrayList<>(); // 创建一个空的列表用于存储数据private int maxSize = 5; // 定义列表的最大长度为 5public void produce() throws InterruptedException{synchronized (this){// 同步代码块,确保线程安全while (buffer.size() == maxSize){// 如果列表已经达到最大长度wait();    // 线程进入等待状态}int num = (int) (Math.random() * 100);    // 生成一个随机数buffer.add(num);    // 将随机数添加到列表中System.out.println("Produced: " + num);    // 输出生产的数据notify(); // 唤醒一个等待的线程}}public void consume() throws InterruptedException{synchronized (this){// 同步代码块,确保线程安全while (buffer.size() == 0){// 如果列表为空wait();    // 线程进入等待状态}int num = buffer.remove(0);    // 从列表中删除第一个元素System.out.println("Consumed: " + num);    // 输出消费的数据notify();    // 唤醒一个等待的线程}}public static void main(String[] args){ProducerConsumer pc = new ProducerConsumer();    // 创建一个生产者消费者对象Thread t1 = new Thread(new Runnable(){// 创建一个用于生产数据的线程@Overridepublic void run(){try {while (true){// 一直循环pc.produce();    // 生产数据Thread.sleep(1000);    // 线程休眠1秒}}catch (InterruptedException e){e.printStackTrace();}}});Thread t2 = new Thread(new Runnable(){// 创建一个用于消费数据的线程@Overridepublic void run(){try{while (true){// 一直循环pc.consume();    // 消费数据Thread.sleep(1000);    // 线程休眠1秒}}catch (InterruptedException e){e.printStackTrace();}}});t1.start();    // 启动生产数据的线程t2.start();    // 启动消费数据的线程}}
    

    在这个示例中,生产者和消费者都在同一个类中实现。buffer 是一个用于存储数据的列表,maxSize 是列表的最大长度。produce()和 consume()方法都使用 synchronized 关键字来保证线程安全。在 produce()方法中,如果 buffer 已经达到最大长度,则线程会进入等待状态,直到有其他线程调用 notify()方法唤醒它。然后,它会生成一个随机数并将其添加到 buffer 中。在 consume()方法中,如果 buffer 为空,则线程会进入等待状态,直到有其他线程调用 notify()方法唤醒它。然后,它会从 buffer 中删除第一个元素。

    在 main()方法中,我们创建了两个线程,一个用于生产数据,另一个用于消费数据。这两个线程会一直运行,直到程序被终止。

  28. wait()和 sleep()有什么区别?调用这两个函数后,线程状态分别作何改变?
    答:
    wait()和 sleep()在 Java 中都是用于线程的暂停,但是它们的用途和行为是不同的。

    wait()是 Object 类的方法,用于线程间的协调。调用 wait()方法的线程会释放对象锁并进入等待状态,直到其他线程调用对象的 notify()或 notifyAll()方法来唤醒它。调用 wait()方法后,线程状态变为 WAITING。

    sleep()是 Thread 类的方法,用于让线程暂停一段时间。调用 sleep()方法后,线程会进入阻塞状态,并且不会释放对象锁。sleep()方法会让线程休眠指定的时间,然后自动唤醒线程。调用 sleep()方法后,线程状态变为 TIMED_WAITING。

    综上所述,wait()和 sleep()的区别在于它们的用途和行为,调用后线程的状态也不同。wait()用于线程间的协调,调用后线程状态变为 WAITING;sleep()用于让线程暂停一段时间,调用后线程状态变为 TIMED_WAITING。

  29. 手写一个线程安全的单例模式(Singleton)。
    答:
    饿汉式:

    public class Singleton
    {private static Singleton instance = new Singleton();private Singleton(){}public static Singleton getInstance(){return instance;}
    }
    

    安全的懒汉式:

    public class Singleton
    {private static Singleton instance;private Singleton(){}public static synchronized Singleton getInstance(){if (instance == null){instance = new Singleton();}return instance;}
    }
    

    内部类:

    public class Singleton
    {private Singleton(){}private static class SingletonHolder{private static final Singleton INSTANCE = new Singleton();}public static Singleton getInstance(){return SingletonHolder.INSTANCE;}
    }
    

    以上三种方式都是线程安全的单例模式,其中饿汉式在类加载时就创建了实例,安全的懒汉式使用了 synchronized 关键字来保证线程安全,而内部类则利用了类加载的线程安全性来保证单例。

  30. 手写一个懒汉式的单例模式,解决其线程安全问题,并且说明为什么这样子去解决?
    答:
    懒汉式单例模式指的是在需要使用实例时才去创建实例,而不是在类加载时就创建实例。线程安全问题是因为多个线程同时访问时可能会创建多个实例,导致不符合单例模式的定义。

    为了解决线程安全问题,可以使用 synchronized 关键字来实现同步锁,保证在多线程环境下只会创建一个实例。

    以下是手写的懒汉式单例模式代码:

    public class Singleton
    {private static Singleton instance;private Singleton(){}public static synchronized Singleton getInstance(){if (instance == null){instance = new Singleton();}return instance;}
    }
    

    在 getInstance 方法上添加 synchronized 关键字,保证在多线程环境下只有一个线程可以进入创建实例的代码块,从而避免创建多个实例的问题。

    这样子去解决的原因是因为 synchronized 关键字可以保证在同一时刻只有一个线程可以进入同步代码块,其它线程需要等待该线程执行完毕后才能进入。这样可以保证在多线程环境下只会创建一个实例,从而解决了线程安全问题。

第 11 章 常用类与基础 API

11.1 String 类

11.1.1 String 的特性

  • java.lang.String 类代表字符串。Java 程序中所有的字符串文字(例如"hello" )都可以看作是实现此类的实例。
  • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。
  • 字符串 String 类型本身是 final 声明的,意味着我们不能继承 String。
  • String 对象的字符内容是存储在一个字符数组 value[]中的。“hello” 等效于 char[] data={‘h’,‘e’,‘l’,‘l’,‘o’}。
  • String 的两种定义方式:
    ① 字面量的定义方式 String s = “hello”
    ② new 的方式:String s = new String(“hello”);
  • Java 语言提供对字符串串联符号(“+”)以及将其他对象转换为字符串的特殊支持(toString()方法)。

11.1.2 String 的内存解析

字符串常量池、堆内存。

11.1.3 String 的常用 API

  • 构造器

    • public String() :初始化新创建的 String 对象,以使其表示空字符序列。
    • String(String original): 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
    • public String(char[] value) :通过当前参数中的字符数组来构造新的 String。
    • public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的 String。
    • public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的 String。
    • public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数中的字节数组来构造新的 String。
  • String 与其他结构之间的转换

    • 字符串 ——> 基本数据类型、包装类:

      • Integer 包装类的 public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
      • 类似地,使用 java.lang 包中的 Byte、Short、Long、Float、Double 类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。
    • 基本数据类型、包装类 ——> 字符串: - 调用 String 类的 public String valueOf(int n)可将 int 型转换为字符串。 - 相应的 valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、
      valueOf(boolean b)可由参数的相应类型到字符串的转换。
    • 字符数组 ——> 字符串: - String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字
      符数组中的全部字符和部分字符创建字符串对象。
    • 字符串 ——> 字符数组:
      • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。
      • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。
    • 字符串 ——> 字节数组:(编码)
      • public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。
      • public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。
    • 字节数组 ——> 字符串:(解码)
      • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。
      • String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置 offset 开始取 length 个字节构造一个字符串对象。
      • String(byte[], String charsetName ) 或 new String(byte[], int, int,String charsetName ):解码,按照指定的编码方式进行解码。
  • 常用方法

    • boolean isEmpty():字符串是否为空。
    • int length():返回字符串的长
      度。
    • String concat(xx):拼接。
    • boolean equals(Object obj):比较字符串是否相等,区分大小写。
    • boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写。
    • int compareTo(String other):比较字符串
      大小,区分大小写,按照 Unicode 编码值比较大小。
    • int compareToIgnoreCase(String other):比较字符串大小,不区分大小写 。
    • String toLowerCase():将字符串中大写字母转为小写。
    • StringtoUpperCase():将字符串中小写字母转为大写。
    • String trim():去掉字符串前后空白符。
    • public String intern():结果在常量池中共享。
  • 查找

    • boolean contains(xx):是否包含 xx。
    • int indexOf(xx):从前往后找当前字符串中 xx,即如果有返回第一次出现的下标,要是没有返回-1。
    • int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。
    • int lastIndexOf(xx):从后往前找当前字符串中 xx,即如果有返回最后一次出现的下标,要是没有返回-1。
    • int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。
  • 字符串截取

    • String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从 beginIndex 开始截取到最后的一个子字符串。
    • String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从 beginIndex
      开始截取到 endIndex(不包含)的一个子字符串。
  • 和字符、字符数组相关

    • char charAt(index):返回[index]位置的字符。
    • char[] toCharArray(): 将此字符串转换为一个新的字符数组返回。
    • static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String。
    • static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String。
    • static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String。
    • static String copyValueOf(char[] data, int
      offset, int count):返回指定数组中表示该字符序列的 String。
  • 开头与结尾

    • boolean startsWith(xx):测试此字符串是否以指定的前缀开始。
    • boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始。
    • boolean endsWith(xx):测试此字符串是否以指定的后缀结束。
  • 替换

    • String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。
    • String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
    • String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
    • String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

11.2 StringBuffer、StringBuilder 类

因为 String 对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK 又在 java.lang 包提供了可变字符序列 StringBuffer 和 StringBuilder 类型。

区分 String、StringBuffer、StringBuilder

  • String:不可变的字符序列; 底层使用 char[]数组存储。(JDK8.0)
  • StringBuffer:可变的字符序列;线程安全(方法有 synchronized 修饰),效率低;底层使用 char[]数组存储 。(JDK8.0)
  • StringBuilder:可变的字符序列; jdk1.5 引入,线程不安全的,效率高;底层使用 char[]数组存储。(JDK8.0)

11.3 jdk8 之前的日期、时间 API

11.3.1 java.lang.System 类的日期、时间 API

System 类提供的 public static long currentTimeMillis():用来返回当前时间与 1970 年 1 月 1 日 0 时 0 分 0 秒之间以毫秒为单位的时间差。

  • 此方法适于计算时间差。

11.3.2 java.util.Date 类的日期、时间 API

表示特定的瞬间,精确到毫秒。

  • 构造器

    • Date():使用无参构造器创建的对象可以获取本地当前时间。
    • Date(long 毫秒数):把该毫秒值换算成日期时间对象。
  • 常用方法

    • getTime(): 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
    • toString(): 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz 是时间标准。

11.3.3 java.text.SimpleDateFormat

  • java.text.SimpleDateFormat 类是一个不与语言环境有关的方式来格式化和解析日期的具体类。

  • 可以进行格式化:日期 ——> 文本。

  • 可以进行解析:文本 ——> 日期。

  • 构造器

    • SimpleDateFormat() :默认的模式和语言环境创建对象。
    • public SimpleDateFormat(String pattern):该构造方法可以用参数 pattern 指定的格式创建一个对象。
  • 格式化

    • public String format(Date date):方法格式化时间对象 date。
  • 解析

    • public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。

11.3.4 java.util.Calendar(日历)

  • Calendar 类是一个抽象类,主用用于完成日期字段之间相互操作的功能。

  • 获取 Calendar 实例的方法

    • 使用 Calendar.getInstance()方法。
    • 调用它的子类 GregorianCalendar(公历)的构造器。
  • 一个 Calendar 的实例是系统时间的抽象表示,可以修改或获取 YEAR、MONTH、DAYOFWEEK、HOUROFDAY 、MINUTE、SECOND 等日历字段对应的时间值。

    • public int get(int field):返回给定日历字段的值。
    • public void set(int field,int value) :将给定的日历字段设置为指定的值。
    • public void add(int field,int amount):根据日历的规则,为给定的日历字段添加或者减去指定的时间量。
    • public final Date getTime():将 Calendar 转成 Date 对象。
    • public final void setTime(Date date):使用指定的 Date 对象重置 Calendar 的时间。

11.4 jdk8 中新的日期、时间 API

11.4.1 本地日期时间:LocalDate、LocalTime、LocalDateTime

  • LocalDate、LocalTime、LocalDateTime ——>类似于 Calendar。

11.4.2 瞬时:Instant

• Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。

  • Instant ——>类似于 Date。

11.4.3 日期时间格式化:DateTimeFormatter

  • DateTimeFormatter ——>类似于 SimpleDateFormat。

11.5 比较器

在 Java 中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。

Java 实现对象排序的方式有两种:

  • 自然排序:java.lang.Comparable
  • 定制排序:java.util.Comparator

11.5.1 自然排序:java.lang.Comparable

Comparable 接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。

实现 Comparable 的类必须实现 compareTo(Object obj)方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象 this 大于形参对象 obj,则返回正整数,如果当前对象 this 小于形参对象 obj,则返回负整数,如果当前对象 this 等于形参对象 obj,则返回零。

11.5.2 定制排序:java.util.Comparator

思考:

  • 当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码。(例如:一些第三方的类,你只有.class 文件,没有源文件)
  • 如果一个类,实现了 Comparable 接口,也指定了两个对象的比较大小的
    规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?

JDK 在设计类库之初,也考虑到这种情况,所以又增加了一个 java.util.Comparator 接口。强行对多个对象进行整体排序的比较。

  • 重写 compare(Object o1,Object o2)方法,比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。
  • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。

11.6 系统相关类

11.6.1 java.lang.System 类

System 类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于 java.lang 包。

由于该类的构造器是 private 的,所以无法创建该类的对象。其内部的成员变量和成员方法都是 static 的,所以也可以很方便的进行调用。

成员变量:

  • Scanner scan = new Scanner(System.in);
  • System 类内部包含 in、out 和 err 三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。

成员方法:

  • native long currentTimeMillis(): 该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和 GMT 时间(格林威治时间)1970 年 1 月 1 号 0 时 0 分 0 秒所差的毫秒数。
  • void exit(int status): 该方法的作用是退出程序。其中 status 的值为 0 代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程
    中实现程序的退出功能等。
  • void gc(): 该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。
  • String getProperty(String key): 该方法的作用是获得系统中属性名为 key 的属性对应的值。

11.6.2 java.lang.Runtime 类

  • 每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。
  • public static Runtime getRuntime(): 返回与当前 Java 应用程序相关的运行时对象。应用程序不能创建自己的 Runtime 类实例。
  • public long totalMemory():返回 Java 虚拟机中初始化时的内存总量。此方法返回的值可能随时间的推移而变化,这取决于主机环境。默认为物理电脑内存的 1/64。
  • public long maxMemory():返回 Java 虚拟机中最大程度能使用的内存总量。默认为物理电脑内存的 1/4。
  • public long freeMemory():回 Java 虚拟机中的空闲内存量。调用 gc 方法可能导致 freeMemory 返回值的增加。

11.7 数学相关的类

11.7.1 java.lang.Math

java.lang.Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。

11.7.2 BigInteger

Integer 类作为 int 的包装类,能存储的最大整型值为 2^31 - 1。

Long 类也是有限的,最大为 2^63 - 1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。

java.math 包的 BigInteger 可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。

构造器:

  • BigInteger(String val):根据字符串构建 BigInteger 对象

方法:

  • public BigInteger abs():返回此 BigInteger 的绝对值的 BigInteger。
  • BigInteger add(BigInteger val) :返回其值为 (this + val) 的 BigInteger。
  • BigInteger subtract(BigInteger val) :返回其值为 (this - val) 的 BigInteger。
  • BigInteger multiply(BigInteger val) :返回其值为 (this * val) 的 BigInteger。
  • BigInteger divide(BigInteger val) :返回其值为 (this / val) 的 BigInteger。整数相除只保留整数部分。
  • BigInteger remainder(BigInteger val) :返回其值为 (this % val) 的 BigInteger。
  • BigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟 (this % val) 的两个 BigInteger 的数组。
  • BigInteger pow(int exponent) :返回其值为 (this^exponent) 的 BigInteger。

11.7.3 BigDecimal

一般的 Float 类和 Double 类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到 java.math.BigDecimal 类。

BigDecimal 类支持不可变的、任意精度的有符号十进制定点数。

构造器:

  • public BigDecimal(double val)
  • public BigDecimal(String val)

常用方法:

  • public BigDecimal add(BigDecimal augend)
  • public BigDecimal subtract(BigDecimal subtrahend)
  • public BigDecimal multiply(BigDecimal multiplicand)
  • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode):divisor 是除数,scale 指明保留几位小数,roundingMode
    指明舍入模式。(ROUNDUP :向上加 1、ROUNDDOWN :直接舍去、ROUNDHALFUP:四舍五入)

11.7.4 java.util.Random

  • 用于产生随机数。
  • boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 boolean 值。
  • void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。
  • double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.0 和 1.0 之间均匀分布的 double 值。
  • float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.0 和 1.0 之间均匀分布的 float 值。
  • double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的 double 值,其平均值是 0.0,标准差是 1.0。
  • int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。
  • int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在 0(包括)和指定值(不包括)之间均匀分布的 int 值。
  • long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。

11.8 企业真题

  1. 以下两种方式创建的 String 对象有什么不同?

    String str1 = "Hello";
    String str2 = new String("Hello");
    

    答:
    ① 直接赋值创建 String 对象:

    String str1 = "Hello";
    

    这种方式创建的 String 对象是在字符串常量池中创建的,如果已经存在相同的字符串常量,就会直接返回该常量的引用,而不会重新创建。

    ② 使用 new 关键字创建 String 对象:

    String str2 = new String("Hello");
    

    这种方式创建的 String 对象是在堆内存中创建的,不会共享存储空间,即使是相同的字符串内容也会创建多个对象。

    因此,使用直接赋值方式创建 String 对象更加高效,能够节省存储空间和创建对象的时间。

  2. String s = new String(“xyz”);创建了几个 String Object?
    答:
    如果我们只考虑这一行代码,确实只创建了一个 String Object。但是,由于 Java 有一个字符串池(string pool)的概念,如果字符串池中没有"xyz"这个字符串,那么这个字符串会被添加到字符串池中。因此,实际上可能会创建两个 String Object:一个在堆中,一个在字符串池中。但是,这取决于 JVM 的实现方式和优化策略,因此不是绝对的。

  3. String a=“abc” String b=“a”+“bc” ,那么 a==b?
    答:
    true,因为"a"+“bc"会被编译器优化为"abc”,所以 a 和 b 都是指向值为"abc"的 String 对象。

  4. String 中 “+” 怎样实现?
    答:
    在 Java 中,字符串连接可以使用 “+” 运算符来实现。当两个字符串相加时,Java 会将它们连接起来,形成一个新的字符串。例如:

    String str1 = "Hello";
    String str2 = "World";
    String str3 = str1 + " " + str2;
    System.out.println(str3);
    

    输出结果为: “Hello World”。

    在底层实现中,Java 会使用 StringBuilder 或 StringBuffer 类来实现字符串连接。这两个类都提供了 append() 方法来实现字符串的拼接,而 “+” 运算符就是使用了这些方法。在实际开发中,建议使用 StringBuilder 或 StringBuffer 类来进行字符串拼接,因为它们的效率更高。

  5. Java 中 String 是不是 final 的?
    答:
    是的,Java 中的 String 是 final 的,这意味着它们是不可变的。一旦一个 String 对象被创建,它的值就不能被改变。如果需要修改一个字符串,必须创建一个新的 String 对象来代替原来的对象。这种不可变性使得 String 对象在多线程环境下更加安全和可靠。

  6. String 为啥不可变,在内存中的具体形态?
    答:
    String 不可变的原因是因为它的值被创建后就不能再被修改。这是因为 String 类被设计为 final 类,因此它的方法都不能被重写,也不能被子类化。这就意味着一旦一个 String 对象被创建,它的值就不能被更改,只能创建一个新的 String 对象来代替。

    在内存中,每个 String 对象都有一个字符串常量池。字符串常量池是一个特殊的内存区域,用于存储所有的字符串常量。当创建一个 String 对象时,如果字符串常量池中已经存在相同值的字符串常量,则返回该常量的引用,否则就会在常量池中新建一个字符串常量,并返回该常量的引用。

    具体来说,当我们使用字符串字面值创建一个 String 对象时,Java 会首先在字符串常量池中查找是否已经存在相同值的字符串常量。如果存在,则返回该常量的引用;如果不存在,则在字符串常量池中新建一个字符串常量,并返回该常量的引用。如果我们使用 new 关键字显式创建一个 String 对象,则该对象会被存储在堆内存中,而不是字符串常量池中。

    总之,String 的不可变性是由 final 关键字和其设计决定的,而字符串常量池则是一种优化机制,用于避免重复创建相同值的字符串常量。

  7. String 可以在 switch 中使用吗?
    答:
    是的,从 Java 7 开始,String 类型可以在 switch 语句中使用。在之前的版本中,只能使用基本数据类型和枚举类型。在使用字符串时,每个 case 分支都必须是一个字符串常量。例如:

    String str = "hello";
    switch (str)
    {case "hello":System.out.println("Hello!");break;
    case "world":System.out.println("World!");break;
    default:System.out.println("Other!");
    }
    

    注意,在 switch 语句中使用字符串时,需要使用双引号将字符串括起来。

  8. String 中有哪些方法?列举几个。
    答:
    ① length():返回字符串的长度。
    ② charAt(int index):返回指定位置的字符。
    ③ indexOf(String str):返回指定字符串在该字符串中第一次出现的位置。
    ④ substring(int beginIndex, int endIndex):返回从指定位置开始到结束位置的子字符串。
    ⑤ equals(Object obj):比较字符串是否相等。
    ⑥ toUpperCase():将字符串转换为大写。
    ⑦ toLowerCase():将字符串转换为小写。
    ⑧ replace(char oldChar, char newChar):用新字符替换旧字符。
    ⑨ trim():去除字符串两端的空格。
    ⑩ split(String regex):将字符串按照指定正则表达式分割成字符串数组。

  9. subString()到底做了什么?
    答:
    Java 中的 subString()方法用于从一个字符串中获取一个子字符串。它接受两个参数,即要提取的子字符串的起始索引和结束索引。它返回从起始索引到结束索引(不包括结束索引)的子字符串。

    subString()方法实际上在原始字符串上创建了一个新的字符串对象,该字符串对象包含从原始字符串中提取的子字符串。这个新字符串对象是原始字符串的一个子字符串,它与原始字符串共享相同的字符数组。因此,如果在原始字符串中进行更改,则可能会影响子字符串。

    需要注意的是,如果传递给 subString()方法的起始索引或结束索引超出了字符串的范围,则会抛出 StringIndexOutOfBoundsException 异常。

  10. Java 中操作字符串有哪些类?他们之间有什么区别?
    答:
    Java 中操作字符串的类有 String、StringBuffer 和 StringBuilder。它们之间的区别如下:
    ① String 类是不可变的,意味着一旦创建了一个字符串对象,就不能改变它的值,任何对字符串的修改都会创建一个新的字符串对象。String 类适用于字符串不需要频繁修改的场景。
    ② StringBuffer 类是可变的,可以对字符串进行修改,适用于需要频繁修改字符串的场景。在多线程环境下,StringBuffer 是线程安全的。
    ③ StringBuilder 类也是可变的,与 StringBuffer 类相似,但不保证线程安全。StringBuilder 类适用于在单线程环境下,需要频繁修改字符串的场景。

    总之,如果需要频繁修改字符串并且在多线程环境下,建议使用 StringBuffer 类。如果在单线程环境下需要频繁修改字符串,建议使用 StringBuilder 类。如果字符串不需要频繁修改,使用 String 类即可。

  11. String 的线程安全问题?
    答:
    String 是不可变对象,因此它是线程安全的。多个线程可以同时访问同一个 String 对象,而不会出现竞争条件或线程安全问题。但是,如果在多个线程之间共享可变的 StringBuilder 或 StringBuffer 对象时,就会出现线程安全问题,因为这些对象是可变的。在这种情况下,需要使用同步机制来保证线程安全。

  12. StringBuilder 和 StringBuffer 的线程安全问题?
    答:
    StringBuilder 和 StringBuffer 都是可变字符串的实现类,区别在于 StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。

    线程安全的意思是多个线程同时操作一个对象时,不会出现数据不一致或者异常的情况。在多线程环境下,如果使用 StringBuilder 进行字符串的操作,可能会出现数据不一致的问题。因为 StringBuilder 不是线程安全的,多个线程同时操作一个 StringBuilder 对象时,会出现竞争条件,导致数据不一致。

    而 StringBuffer 是线程安全的,它的所有公共方法都是同步的,保证了多个线程同时操作一个 StringBuffer 对象时,数据的一致性。

    因此,如果在多线程环境下需要进行字符串的操作,应该使用 StringBuffer,而不是 StringBuilder。如果在单线程环境下进行字符串的操作,建议使用 StringBuilder,因为它比 StringBuffer 更高效。

  13. 简单说说 Comparable 和 Comparator 的区别和场景?
    答:
    Comparable 是一个接口,它是 Java 中的一个内部比较器,可以通过实现该接口来定义类的内部比较规则。实现 Comparable 接口的类可以进行自然排序,即按照类的内部定义的比较规则进行排序。

    Comparator 也是一个接口,它是 Java 中的一个外部比较器,可以通过实现该接口来定义类的外部比较规则。实现 Comparator 接口的类可以进行定制排序,即按照自己定义的比较规则进行排序。

    总的来说,Comparable 是用于定义类的内部比较规则,Comparator 是用于定义类的外部比较规则。如果我们需要对已有的类进行排序,而该类没有实现 Comparable 接口,或者我们需要按照不同的比较规则进行排序,那么就需要使用 Comparator 接口来进行定制排序。

  14. Comparable 接口和 Comparator 接口实现比较?
    答:
    Comparable 接口是 Java 中的一个接口,它只有一个方法 compareTo(Object obj),用于定义对象之间的自然排序。如果一个类实现了 Comparable 接口,就可以使用 Collections.sort 或 Arrays.sort 方法对该类的对象进行排序。

    Comparator 接口也是 Java 中的一个接口,它有两个方法 compare(Object obj1, Object obj2) 和 equals(Object obj),用于定义对象之间的比较。如果一个类实现了 Comparator 接口,就可以使用 Collections.sort 或 Arrays.sort 方法,并传入该类的对象作为比较器,对其他类的对象进行排序。

    两者的区别在于 Comparable 接口是在对象内部实现的,而 Comparator 接口是在对象外部实现的。如果一个类只需要一种排序方式,可以实现 Comparable 接口;如果需要多种排序方式,或者需要对其他类的对象进行排序,可以实现 Comparator 接口。

第 12 章 集合框架

12.1 数组存储数据方面的特点和弊端

数组存储多个数据方面的特点:

  • 数组一旦初始化,其长度就是确定的。
  • 数组中的多个元素是依次紧密排列的,有序的,可重复的。
  • 数组一旦初始化完成,其元素的类型就是确定的。不是此类型的元素,就不能添加到此数组中。
  • 元素的类型既可以是基本数据类型,也可以是引用数据类型。

数组存储多个数据方面的弊端:

  • 数组一旦初始化,其长度就不可变了。
  • 数组中存储数据特点的单一性。对于无序的、不可重复的场景的多个数据就无能为力了。
  • 数组中可用的方法、属性都极少。具体的需求,都需要自己来组织相关的代码逻辑。
  • 针对于数组中元素的删除、插入操作,性能较差。

12.2 集合框架概述

  • Java 集合可分为 Collection 和 Map 两大体系。
  • Collection 接口:用于存储一个一个的数据,也称单列数据集合。
    • List 子接口:用来存储有序的、可以重复的数据。(主要用来替换数组,"动态"数组)

      • 实现类:ArrayList(主要实现类)、LinkedList、Vector。
    • Set 子接口:用来存储无序的、不可重复的数据。
      • 实现类:HashSet(主要实现类)、LinkedHashSet、TreeSet。
  • Map 接口:用于存储具有映射关系“key-value 对”的集合,即一对一对的数据,也称双列数据集合。
    • HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、Properties。
  • JDK 提供的集合 API 位于 java.util 包内。

12.3 Collection 接口的常用方法

12.3.1 添加

  • add(E obj):添加元素对象到当前集合中。
  • addAll(Collection other):添加 other 集合中的所有元素对象到当前集合中。

12.3.2 判断

  • int size():获取当前集合中实际存储的元素个数。
  • boolean isEmpty():判断当前集合是否为空集合。
  • boolean contains(Object obj):判断当前集合中是否存在一个与 obj 对象 equals 返回 true 的元素。
  • boolean containsAll(Collection coll):判断 coll 集合中的元素是否在当前集合中都存在。即 coll 集合是否是当前集合的“子集” 。
  • boolean equals(Object obj):判断当前集合与 obj 是否相等。

12.3.3 删除

  • void clear():清空集合元素。
  • boolean remove(Object obj) :从当前集合中删除第一个找到的与 obj 对象 equals 返回 true 的元素。
  • boolean removeAll(Collection coll):从当前集合中删除所有与 coll 集合中相同的元素。
  • boolean retainAll(Collection coll):从当前集合
    中删除两个集合中不同的元素,使得当前集合仅保留与 coll 集合中的元素相同的元素,即当前集合中仅保留两个集合的交集。

12.3.4 其它

  • Object[] toArray():返回包含当前集合中所有元素的数组。
  • hashCode():获取集合对象的哈希值。
  • iterator():返回迭代器对象,用于集合遍历。

12.4 Iterator(迭代器)接口

12.4.1 Iterator 接口

在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK 专门提供了一个接口 java.util.Iterator。Iterator 接口也是 Java 集合中的一员,但它与 Collection、Map 接口有所不同。

Collection 接口与 Map 接口主要用于存储元素。

Iterator,被称为迭代器接口,本身并不提供存储对象的能力,主要用于遍历 Collection 中的元素。

Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator()方法,那么所有实现了 Collection 接口的集合类都有一个 iterator()方法,用以返回一个实现了 Iterator 接口的对象。

  • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。
  • 集合对象每次调用 iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。

Iterator 接口的常用方法如下:

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。

注意:在调用 it.next()方法之前必须要调用 it.hasNext()进行检测。若不调用,且下一条记录无效,直接调用 it.next()会抛出 NoSuchElementException 异常。

代码示例:

// 1. 如何获取迭代器(Iterator)对象?
Iterator iterator = coll.iterator();// 2. 如何实现遍历
while(iterator.hasNext())
{// next():①指针下移 ② 将下移以后集合位置上的元素返回System.out.println(iterator.next());
}

12.4.2 迭代器的执行原理

Iterator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素,接下来通过一个图例来演示 Iterator 对象迭代元素的过程:

使用 Iterator 迭代器删除元素:java.util.Iterator 迭代器中有一个方法:void remove()。

注意:

  • Iterator 可以删除集合的元素,但是遍历过程中通过迭代器对象的 remove 方法,不是集合对象的 remove 方法。
  • 如果还未调用 next()或在上一次调用 next() 方法之后已经调用了 remove() 方法,再调用 remove()都会报 IllegalStateException。
  • Collection 已经有 remove(xx)方法了,为什么 Iterator 迭代器还要提供删除方法呢?因为迭代器的 remove()可以按指定的条件进行删除。

迭代器的执行原理是设计模式的一种。
迭代器不负责数据的存储,负责对集合类的遍历。

12.4.3 foreach 循环

foreach 循环(也称增强 for 循环)是 JDK5.0 中定义的一个高级 for 循环,专门用来遍历数组和集合的。

语法格式:

for(元素的数据类型 局部变量 : Collection 集合或数组)
{//操作局部变量的输出操作
}

对于集合的遍历,增强 for 的内部原理其实是个 Iterator 迭代器。

12.5 Collection 的子接口:List

12.5.1 List 接口概述

鉴于 Java 中数组用来存储数据的局限性,我们通常使用 java.util.List 替代数组。

List 集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

JDK API 中 List 接口的实现类常用的有:ArrayList、LinkedList 和 Vector。

12.5.2 List 接口方法

List 除了从 Collection 集合继承的方法外,List 集合里添加了一些根据索引来操作集合元素的方法。

  • 插入元素

    • void add(int index, Object ele):在 index 位置插入 ele 元素。
    • boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来。
  • 获取元素

    • Object get(int index):获取指定 index 位置的元素。
    • List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合。
  • 获取元素索引

    • int indexOf(Object obj):返回 obj 在集合中首次出现的位置。
    • int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置。
  • 删除和替换元素

    • Object remove(int index):移除指定 index 位置的元素,并返回此元素。
    • Object set(int index, Object ele):设置指定 index 位置的元素为 ele。

12.5.3 List 接口主要实现类:ArrayList

ArrayList 是 List 接口的主要实现类

本质上,ArrayList 是对象引用的一个”变长”数组。

Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实例。Arrays.asList(…) 返回值是一个固定长度的 List 集合。

12.5.4 List 的实现类:LinkedList

对于频繁的插入或删除元素的操作,建议使用 LinkedList 类,效率较高。这是由底层采用链表(双向链表)结构存储数据决定的。

方法:

  • void addFirst(Object obj)
  • void addLast(Object obj)
  • Object getFirst()
  • Object getLast()
  • Object removeFirst()
  • Object removeLast()

12.5.5 List 的实现类:Vector

Vector 是一个古老的集合,JDK1.0 就有了。大多数操作与 ArrayList 相同,区别之处在于 Vector 是线程安全的。

在各种 List 中,最好把 ArrayList 作为默认选择。当插入、删除频繁时,使用 LinkedList;Vector 总是比 ArrayList 慢,所以尽量避免使用。

方法:

  • void addElement(Object obj)
  • void insertElementAt(Object obj,int index)
  • void setElementAt(Object obj,int index)
  • void removeElement(Object obj)
  • void removeAllElements()

12.6 Collection 的子接口:Set

12.6.1 Set 接口概述

Set 接口是 Collection 的子接口,Set 接口相较于 Collection 接口没有提供额外的方法。

Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中,则添加操作失败。

Set 集合支持的遍历方式和 Collection 集合一样:foreach 和 Iterator。

Set 的常用实现类有:HashSet、TreeSet、LinkedHashSet。

12.6.2 Set 主要实现类:HashSet

HashSet 是 Set 接口的主要实现类,大多数时候使用 Set 集合时都使用这个实现类。

HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存储、查找、删除性能。

HashSet 具有以下特点:

  • 不能保证元素的排列顺序。
  • HashSet 不是线程安全的。
  • 集合元素可以是 null。

HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法得到的哈希值相等,并且两个对象的 equals()方法返回值为 true。

对于存放在 Set 容器中的对象,对应的类一定要重写 hashCode()和 equals(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

HashSet 集合中元素的无序性,不等同于随机性。这里的无序性与元素的添加位置有关。具体来说:我们在添加每一个元素到数组中时,具体的存储位置是由元素的 hashCode()调用后返回的 hash 值决定的。导致在数组中每个元素不是依次紧密存放的,表现出一定的无序性。

12.6.3 Set 实现类:LinkedHashSet

LinkedHashSet 是 HashSet 的子类,不允许集合元素重复。

LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以添加顺序保存的。

LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。

12.6.4 Set 实现类:TreeSet

TreeSet 是 SortedSet 接口的实现类,TreeSet 可以按照添加的元素的指定的属性的大小顺序进行遍历。

TreeSet 底层使用红黑树结构存储数据。

方法:

  • Comparator comparator()
  • Object first()
  • Object last()
  • Object lower(Object e)
  • Object higher(Object e)
  • SortedSet subSet(fromElement, toElement)
  • SortedSet headSet(toElement)
  • SortedSet tailSet(fromElement)

TreeSet 特点:不允许重复、实现排序。(自然排序或定制排序)

TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排序。

  • 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。

    • 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。
    • 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。
  • 定制排序:如果元素所属的类没有实现 Comparable 接口,或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑使用定制排序。定制排序,通过 Comparator 接口来实现。需要重写 compare(T o1,T o2)方法。
    • 利用 int compare(T o1,T o2)方法,比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数,表示 o1 小于 o2。
    • 要实现定制排序,需要将实现 Comparator 接口的实例作为形参传递给 TreeSet 的构造器。

12.7 Map 接口

Java 提供了专门的集合框架用来存储这种映射关系的对象,即 java.util.Map 接口。

12.7.1 Map 接口概述

Map 与 Collection 并列存在。用于保存具有映射关系的数据:key-value。

Collection 集合称为单列集合,元素是孤立存在的。

Map 集合称为双列集合,元素是成对存在的。

Map 中的 key 和 value 都可以是任何引用类型的数据。但常用 String 类作为 Map 的“键”。

Map 接口的常用实现类:HashMap、LinkedHashMap、TreeMap 和 Properties。其中,HashMap 是 Map 接口使用频率最高的实现类。

12.7.2 Map 中 key-value 特点

这里主要以 HashMap 为例说明。HashMap 中存储的 key、value 的特点如下:

  • Map 中的 key 用 Set 来存放,不允许重复,即同一个 Map 对象所对应的类,须重写 hashCode()和 equals()方法。
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value,不同 key 对应的 value 可以重复。value 所在的类要重写 equals()方法。
  • key 和 value 构成一个 entry。所有的 entry 彼此之间是无序的、不可重复的。

12.7.3 Map 接口的常用方法

添加、修改操作:

  • Object put(Object key,Object value):将指定 key-value 添加到(或修改)当前 map 对象中。
  • void putAll(Map m):将 m 中的所有 key-value 对存放到当前 map 中。

删除操作:

  • Object remove(Object key):移除指定 key 的 key-value 对,并返回 value。
  • void clear():清空当前 map 中的所有数据。

元素查询的操作:

  • Object get(Object key):获取指定 key 对应的 value。
  • boolean containsKey(Object key):是否包含指定的 key。
  • boolean containsValue(Object value):是否包含指定的 value。
  • int size():返回 map 中 key-value 对的个数。
  • boolean isEmpty():判断当前 map 是否为空。
  • boolean equals(Object obj):判断当前 map 和参数对象 obj 是否相等。

元视图操作的方法:

  • Set keySet():返回所有 key 构成的 Set 集合。
  • Collection values():返回所有 value 构成的 Collection 集合。
  • Set entrySet():返回所有 key-value 对构成的 Set 集合。

12.7.4 Map 的主要实现类:HashMap

HashMap 是 Map 接口使用频率最高的实现类。

HashMap 是线程不安全的。允许添加 null 键和 null 值。

存储数据采用的哈希表结构,底层使用一维数组 + 单向链表 + 红黑树进行 key-value 数据的存储。与 HashSet 一样,元素的存取顺序不能保证一致。

HashMap 判断两个 key 相等的标准是:两个 key 的 hashCode 值相等,通过 equals() 方法返回 true。

HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

12.7.5 Map 实现类:LinkedHashMap

LinkedHashMap 是 HashMap 的子类。

存储数据采用的哈希表结构+链表结构,在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的先后顺序,可以保证遍历元素时,与添加的顺序一致。

通过哈希表结构可以保证键的唯一、不重复,需要键所在类重写 hashCode()方法、equals()方法。

12.7.6 Map 实现类:TreeMap

TreeMap 存储 key-value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 key-value 对处于有序状态。

TreeSet 底层使用红黑树结构存储数据

TreeMap 的 Key 的排序:

  • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。
  • 定制排序:创建 TreeMap 时,构造器传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key
    实现 Comparable 接口。

TreeMap 判断两个 key 相等的标准:两个 key 通过 compareTo()方法或者 compare()
方法返回 0。

12.7.7 Map 实现类:Hashtable

Hashtable 是 Map 接口的古老实现类,JDK1.0 就提供了。不同于 HashMap,Hashtable 是线程安全的。

Hashtable 实现原理和 HashMap 相同,功能相同。底层都使用哈希表结构(数组 + 单向链表),查询速度快。

与 HashMap 一样,Hashtable 也不能保证其中 Key-Value 对的顺序。

Hashtable 判断两个 key 相等、两个 value 相等的标准,与 HashMap 一致。

与 HashMap 不同,Hashtable 不允许使用 null 作为 key 或 value。

12.7.8 Map 实现类:Properties

Properties 类是 Hashtable 的子类,该对象用于处理属性文件。

由于属性文件里的 key、value 都是字符串类型,所以 Properties 中要求 key 和 value 都是字符串类型。

存取数据时,建议使用 setProperty(String key,String value)方法和 getProperty(String key)方法。

12.8 Collections 工具类

参考操作数组的工具类:Arrays,Collections 是一个操作 Set、List 和 Map 等集合的工具类。

Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法(均为 static 方法)。

排序操作:

  • reverse(List):反转 List 中元素的顺序。
  • shuffle(List):对 List 集合元素进行随机排序。
  • sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序。
  • sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行
    排序。
    swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换。

查找:

  • Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素。
  • Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合
    中的最大元素。
  • Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素。
  • Object min(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合
    中的最小元素。
  • int binarySearch(List list,T key)在 List 集合中查找某个元素的下标,但是 List 的元素必须是 T 或 T 的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。
  • int binarySearch(List list,T key,Comparator c)在 List 集合中查找某个元素的下标,但是 List 的元素必须是 T 或 T 的子类对象,而且集合也事先必须是按照 c 比较器规则进行排序过的,否则结果不确定。
  • int frequency(Collection c,Object o):返回指定集合中指定元素的出现次数。

复制、替换:

  • void copy(List dest,List src):将 src 中的内容复制到 dest 中。
  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值。
  • 提供了多个 unmodifiableXxx()方法,该方法返回指定 Xxx 的不可修改的视图。

添加:

  • boolean addAll(Collection c,T… elements)将所有指定元素添加到指定 collection 中。

同步:

  • Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。

12.9 企业真题

  1. List,Set,Map 是否继承自 collection 接口?
    答:
    在 Java 中,List,Set 和 Map 是三种常用的集合类型,它们分别属于 java.util 包中的不同接口。List 和 Set 继承自 Collection 接口,而 Map 接口与 Collection 接口并无直接关系。这里是它们的关系层次:

    • java.util.Collection

      • java.util.List
      • java.util.Set
    • java.util.Map

    Collection 接口是 Java 集合框架的根接口,定义了一组操作集合的基本方法,如 add,remove,size 等。List 和 Set 是 Collection 接口的两个子接口,分别表示有序可重复和无序不可重复的集合。

    Map 接口表示键值对的映射关系,它不能直接视为一种集合类型,因为它不继承自 Collection 接口。但是,它提供了一些方法可以将其键值对转换成 Set 或 Collection 类型的集合,例如 keySet()方法可以返回一个包含所有键的 Set 集合,values()方法可以返回一个包含所有值的 Collection 集合。

  2. 说说 List,Set,Map 三者的区别?
    答:
    List、Set 和 Map 都是 Java 中的集合类型,它们之间的主要区别如下:
    ① List 是一个有序的集合,它允许重复的元素。List 中的元素可以通过索引访问,也可以使用迭代器进行遍历。
    ② Set 是一个不允许重复元素的集合,它不保证元素的顺序。Set 中的元素可以使用迭代器进行遍历,但不能通过索引访问。
    ③ Map 是一个键值对的集合,每个元素都包含一个键和一个值。Map 中的键是唯一的,但值可以重复。Map 中的元素可以使用键来访问,也可以使用迭代器进行遍历。

总的来说,List 适用于需要有序并且可以重复的元素的场景,Set 适用于需要去重并且不需要保证元素顺序的场景,Map 适用于需要按键访问值的场景。

  1. 写出 list、map、set 接口的实现类,并说出其特点?
    答:
    List、Map、Set 都是 Java 中常用的接口,它们的实现类如下:
    ① List 的实现类:

    • ArrayList:基于数组实现的 List,支持快速随机访问,但插入和删除元素的效率相对较低。
    • LinkedList:基于链表实现的 List,插入和删除元素的效率比 ArrayList 高,但随机访问的效率较低。

    ② Map 的实现类:

    • HashMap:基于哈希表实现的 Map,支持快速的插入、删除和查找操作。
    • TreeMap:基于红黑树实现的 Map,支持有序遍历和范围查找。

    ③ Set 的实现类:

    • HashSet:基于哈希表实现的 Set,支持快速的插入、删除和查找操作。
    • TreeSet:基于红黑树实现的 Set,支持有序遍历和范围查找。

    这些实现类的特点可以总结如下:

    • ArrayList 和 LinkedList 都是有序的 List 实现,但它们的实现方式不同,因此它们的性能特点也不同。
    • HashMap 和 HashSet 都是基于哈希表实现的,具有快速的插入、删除和查找操作的特点。
    • TreeMap 和 TreeSet 都是基于红黑树实现的,支持有序遍历和范围查找,但相对于哈希表实现的容器,它们的插入、删除和查找操作的速度较慢。
  2. 常见集合类的区别和适用场景?
    答:
    Java 中常见的集合类包括 List、Set、Map 等,它们的区别和适用场景如下:
    ① List:List 是一个有序的集合,可以存储重复的元素。常见的实现类有 ArrayList 和 LinkedList。ArrayList 适用于随机访问,而 LinkedList 适用于插入和删除操作频繁的情况。
    ② Set:Set 是一个不允许重复元素的集合,通常用于去重。常见的实现类有 HashSet、LinkedHashSet 和 TreeSet。HashSet 是最常用的实现类,它使用哈希算法来实现元素的存储和查找,具有很好的性能。LinkedHashSet 继承自 HashSet,保留了插入顺序。TreeSet 则是基于红黑树实现的,可以对元素进行排序。
    ③ Map:Map 是一种键值对映射的集合,每个键只能对应一个值。常见的实现类有 HashMap、LinkedHashMap 和 TreeMap。HashMap 是最常用的实现类,它使用哈希算法来实现键值对的存储和查找,具有很好的性能。LinkedHashMap 继承自 HashMap,保留了插入顺序。TreeMap 则是基于红黑树实现的,可以对键进行排序。

    适用场景:
    ① List 适用于需要按照插入顺序存储数据、允许存储重复元素的情况。
    ② Set 适用于需要去重的情况。
    ③ Map 适用于需要按照键值对方式存储数据的情况。

  3. 集合的父类是谁?哪些安全的?
    答:
    Java 中集合的父类是 java.util.Collection。
    线程安全的集合类有:
    ① java.util.concurrent.ConcurrentHashMap
    ② java.util.concurrent.CopyOnWriteArrayList
    ③ java.util.concurrent.CopyOnWriteArraySet
    ④ java.util.concurrent.ConcurrentSkipListMap
    ⑤ java.util.concurrent.ConcurrentSkipListSet
    ⑥ java.util.Collections.synchronizedList
    ⑦ java.util.Collections.synchronizedSet
    ⑧ java.util.Collections.synchronizedMap

    这些集合类在多线程环境下使用是安全的,因为它们内部实现了同步机制来保证线程安全。

    线程不安全的集合类有:
    ① java.util.ArrayList
    ② java.util.LinkedList
    ③ java.util.HashSet
    ④ java.util.TreeSet
    ⑤ java.util.HashMap
    ⑥ java.util.TreeMap

    这些集合类在多线程环境下使用时可能会出现线程安全问题,需要在使用时进行同步处理来保证线程安全。

  4. 集合说一下哪些是线程不安全的?
    答:
    在 Java 集合框架中,有一些集合类是线程不安全的,也就是说在多线程环境下使用它们可能会导致数据不一致或者其他并发问题。以下是一些线程不安全的 Java 集合类:
    ① ArrayList

    • ArrayList 是一个可变数组,当多个线程同时修改同一个 ArrayList 实例时,可能会导致数据不一致问题,因为 ArrayList 内部并没有实现同步机制。

    ② HashMap

    • HashMap 是一个哈希表,也是线程不安全的。当多个线程同时修改同一个 HashMap 实例时,可能会导致数据不一致问题,因为 HashMap 内部并没有实现同步机制。

    ③ HashSet

    • HashSet 是一个基于 HashMap 实现的集合类,因此它也是线程不安全的。

    ④ LinkedList

    • LinkedList 是一个链表,也是线程不安全的。当多个线程同时修改同一个 LinkedList 实例时,可能会导致数据不一致问题,因为 LinkedList 内部并没有实现同步机制。

    ⑤ TreeMap

    • TreeMap 是一个基于红黑树实现的映射表,也是线程不安全的。当多个线程同时修改同一个 TreeMap 实例时,可能会导致数据不一致问题,因为 TreeMap 内部并没有实现同步机制。

    要在多线程环境中使用这些集合类,可以考虑使用它们的线程安全版本,例如:Vector、Hashtable、ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet 等。这些线程安全的集合类可以保证在多线程环境中使用时不会出现数据不一致或者其他并发问题。

  5. 遍历集合的方式有哪些?
    答:
    在 Java 中,遍历集合的方式主要有以下几种:
    ① 使用迭代器(Iterator):通过调用集合的 iterator() 方法获取迭代器,然后使用 while 循环和 hasNext()、next() 方法遍历集合中的元素。

    List<String> list = new ArrayList<>();Iterator<String> iterator = list.iterator();while (iterator.hasNext())
    {String element = iterator.next();// 处理元素
    }
    

    ② 使用增强型 for 循环(foreach):Java 5 引入了 foreach 循环,可以更方便地遍历集合中的元素。

    List<String> list = new ArrayList<>();for (String element : list)
    {// 处理元素
    }
    

    ③ 使用 Lambda 表达式:Java 8 引入了 Lambda 表达式,可以更简洁地遍历集合中的元素。

    List<String> list = new ArrayList<>();list.forEach(element -> {// 处理元素
    });
    

    ④ 使用 Stream API:Java 8 还引入了 Stream API,可以更灵活地对集合中的元素进行过滤、转换等操作。

    List<String> list = new ArrayList<>();list.stream().filter(element -> element.startsWith("a")).forEach(element -> {// 处理元素
    });
    

    以上是 Java 中比较常用的几种遍历集合的方式,选择具体的方式可以根据实际情况进行考虑。

  6. List 下面有哪些实现?
    答:
    在 Java 中,List 是一个接口,它定义了一组操作有序集合元素的方法。List 接口有许多实现,其中一些最常用的包括:
    ① ArrayList:基于数组实现的可调整大小的列表,支持快速的随机访问和在列表末尾的常数时间添加和删除元素。
    ② LinkedList:基于链表实现的列表,支持 O(1)时间复杂度的添加和删除元素,但随机访问元素的时间复杂度为 O(n)。
    ③ Vector:与 ArrayList 类似的动态数组,但是所有公共操作都是同步的,因此效率较低,通常不推荐使用。
    ④ Stack:基于 Vector 实现的栈,支持 LIFO(后进先出)操作。

    除了这些常见的实现,还有其他一些实现可以根据具体需要选择使用,例如 CopyOnWriteArrayList、ImmutableList 等。

  7. ArrayList 与 LinkedList 区别?
    答:
    ArrayList 和 LinkedList 是 Java 中常用的两种集合框架。它们的最主要的区别在于它们实现的数据结构不同。

    ArrayList 是基于数组实现的动态数组,它允许随机访问元素,因此可以通过索引快速访问集合中的任何元素。当需要频繁地访问、遍历元素时,ArrayList 的性能很好。但是,当需要在集合的中间或起始位置插入或删除元素时,需要对数组进行移动,因此这样的操作会比较耗时。

    LinkedList 是基于链表实现的双向链表,它允许快速地在任何位置插入或删除元素,因为只需要改变相应节点的指针即可。但是,由于链表中每个节点都需要存储指向前后节点的指针,因此需要更多的内存空间来存储相同数量的元素。此外,由于无法随机访问元素,所以当需要访问或遍历元素时,LinkedList 的性能会比 ArrayList 差。

    因此,当需要频繁地插入或删除元素时,可以选择 LinkedList,而当需要频繁地访问元素时,可以选择 ArrayList。

  8. ArrayList 与 Vector 区别呢?为什么要用 ArrayList 取代 Vector 呢?
    答:
    ArrayList 和 Vector 都是 Java 中的集合类,它们的主要区别在于线程安全和性能方面。

    Vector 是线程安全的,它的所有方法都是同步的,因此在多线程环境下使用是安全的。但是,同步会带来一定的性能开销,所以在单线程环境下使用 Vector 可能会比较慢。

    而 ArrayList 不是线程安全的,它的方法都不是同步的,因此在多线程环境下使用需要额外考虑线程安全的问题。但是,由于不需要同步,所以在单线程环境下使用 ArrayList 比 Vector 快。

    因此,如果在多线程环境下需要使用集合类,则应使用 Vector;如果在单线程环境下使用集合类,则应使用 ArrayList。

    另外,由于现在计算机的硬件性能越来越高,而且 Java 的并发编程库也越来越完善,所以在很多情况下,使用线程安全的 Vector 反而会成为性能瓶颈。因此,一般建议在单线程环境下使用 ArrayList,同时在多线程环境下使用 Java 的并发编程库中的线程安全集合类,如 ConcurrentHashMap、ConcurrentLinkedQueue 等。

  9. Java.util.ArrayList 常用的方法有哪些?
    答:
    Java.util.ArrayList 是一个常用的动态数组类,提供了许多有用的方法来操作和管理数组元素。以下是一些常见的方法:
    ① add(Object obj):向列表中添加一个元素。
    ② add(int index, Object obj):在指定索引处添加一个元素。
    ③ remove(Object obj):从列表中删除指定元素。
    ④ remove(int index):从列表中删除指定索引处的元素。
    ⑤ get(int index):返回列表中指定索引处的元素。
    ⑥ set(int index, Object obj):将列表中指定索引处的元素替换为指定元素。
    ⑦ size():返回列表中元素的个数。
    ⑧ clear():从列表中删除所有元素。
    ⑨ isEmpty():如果列表为空,则返回 true,否则返回 false。
    ⑩ contains(Object obj):如果列表包含指定元素,则返回 true,否则返回 false。

    这些方法可以让您方便地操作和管理 Java.util.ArrayList 中的元素。

  10. Arraylist 是有序还是无序?为什么?
    答:
    ArrayList 是有序的。它是一个基于数组的动态数据结构,可以根据索引访问其中的元素。插入和删除元素时,它会自动调整内部数组的大小和元素的位置,以保持元素的顺序。

    在 ArrayList 中,元素的顺序是按照它们添加到列表中的顺序排列的。因此,第一个添加到 ArrayList 的元素将始终是第一个元素,第二个添加的元素将始终是第二个元素,以此类推。

    相比之下,HashSet 和 HashMap 是无序的,因为它们使用哈希表来存储元素,而哈希表中的元素没有特定的顺序。

  11. Set 集合有哪些实现类,分别有什么特点?
    答:
    Java 中,Set 集合有以下实现类:
    ① HashSet:基于哈希表实现,不保证元素的顺序,允许使用 null 元素,具有快速的添加、删除和查找操作。
    ② LinkedHashSet:基于哈希表和双向链表实现,可以维护元素的插入顺序,允许使用 null 元素,具有快速的添加、删除和查找操作。
    ③ TreeSet:基于红黑树实现,可以按照元素的自然顺序或指定的 Comparator 进行排序,不允许使用 null 元素,具有快速的添加、删除和查找操作,但是比 HashSet 和 LinkedHashSet 慢。

    这些实现类都实现了 Set 接口,因此具有相同的基本功能,包括添加、删除、查找元素等。不同的实现类适用于不同的场景,可以根据需求选择合适的实现类。例如,如果需要快速的添加、删除和查找操作,可以选择 HashSet 或 LinkedHashSet;如果需要按照元素的顺序进行排序,则可以选择 TreeSet。

  12. List 集合和 Set 集合的区别?
    答:
    在 Java 中,List 和 Set 都是集合接口,它们都可以用来存储一组对象。但是,它们有以下区别:
    ① 重复元素:List 允许重复元素,而 Set 不允许重复元素。如果你尝试向一个 Set 中添加已经存在的元素,它将不会被添加到 Set 中。
    ② 排序:List 是有序的,而 Set 是无序的。在 List 中,元素按照它们被添加的顺序排序。而 Set 中没有这种顺序,元素的顺序是不确定的。
    ③ 访问元素:List 可以通过索引访问元素,而 Set 不能。在 List 中,你可以通过索引来访问特定位置的元素。而在 Set 中,你需要遍历整个集合来查找特定的元素。
    ④ 实现类:List 有许多实现类,如 ArrayList、LinkedList 等,而 Set 只有几个主要的实现类,如 HashSet、TreeSet 等。这些实现类具有不同的性能和行为特性,你需要根据你的需求来选择合适的实现类。

    总之,如果你需要一个有序的集合并且允许重复元素,那么使用 List 是更好的选择;如果你需要一个不允许重复元素的无序集合,那么使用 Set 是更好的选择。

  13. Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用 == 还是 equals()? 它们有何区别?
    答:
    在 Java 中,Set 的元素是不能重复的,是通过比较对象的 equals()方法来判断元素是否重复的,而不是通过 == 运算符。

    == 运算符比较的是两个对象的引用是否相同,即它们是否指向同一个对象。而 equals()方法比较的是两个对象的内容是否相同,即它们的属性值是否相等。

    例如,假设有一个 Person 类:

    public class Person
    {private String name;private int age;// 构造方法和getter/setter方法省略...@Overridepublic boolean equals(Object o){if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return age == person.age && Objects.equals(name, person.name);}@Overridepublic int hashCode(){return Objects.hash(name, age);}
    }
    

    在上面的代码中,equals()方法比较的是 Person 对象的属性值是否相等,而 hashCode()方法则返回一个基于属性值计算出来的哈希码,用于在 Set 中快速查找元素。

    当你向一个 Set 中添加 Person 对象时,Set 会首先调用该对象的 hashCode()方法获取哈希码,然后根据哈希码找到相应的桶(bucket),再在桶中查找是否有与该对象相等的元素。如果没有找到相等的元素,该对象就会被添加到 Set 中。

    如果两个 Person 对象的属性值完全相同,那么它们的 hashCode()方法返回的哈希码也应该相同,这样才能使它们被放到同一个桶中。而 equals()方法则用于在桶中查找元素时比较两个对象是否相等。

  14. TreeSet 两种排序方式在使用的时候怎么起作用?
    答:
    在 Java 中,TreeSet 是基于红黑树实现的集合类,它可以按照元素自然顺序(natural ordering)或者通过传入的 Comparator 排序来存储元素。

    默认情况下,TreeSet 使用元素自然顺序进行排序。这意味着,如果要存储的元素实现了 Comparable 接口,即实现了 compareTo() 方法,那么 TreeSet 将使用该方法对元素进行排序。如果要存储的元素没有实现 Comparable 接口,那么在添加元素时会抛出 ClassCastException 异常。

    如果要指定 TreeSet 使用传入的 Comparator 进行排序,需要在创建 TreeSet 对象时传入一个实现了 Comparator 接口的比较器对象。Comparator 接口中有一个 compare() 方法,可以用来比较两个元素的大小。在使用 Comparator 排序时,TreeSet 会使用该比较器对象的 compare() 方法来进行排序。

    例如,以下代码演示了如何创建一个按照元素长度排序的 TreeSet:

    import java.util.*;public class Example
    {public static void main(String[] args){TreeSet<String> set1 = new TreeSet<>(); // 默认排序方式set1.add("apple");set1.add("banana");set1.add("orange");System.out.println(set1); // 输出 [apple, banana, orange]Comparator<String> comparator = new Comparator<String>(){@Overridepublic int compare(String s1, String s2){return s1.length() - s2.length();}};TreeSet<String> set2 = new TreeSet<>(comparator); // 指定排序方式set2.add("apple");set2.add("banana");set2.add("orange");System.out.println(set2); // 输出 [apple, orange, banana]}
    }
    
  15. TreeSet 的数据结构?
    答:
    Java 中的 TreeSet 是一种基于红黑树的数据结构,它实现了 SortedSet 接口,并且能够保证集合中的元素处于有序状态。TreeSet 中的元素按照它们的自然顺序进行排序,或者可以通过提供一个 Comparator 来指定排序规则。

    TreeSet 的基本操作包括添加、删除和查询元素,这些操作的时间复杂度都是 O(log n),其中 n 是集合中的元素个数。由于 TreeSet 维护了一个有序状态,因此它还提供了一些与有序集合相关的操作,例如获取第一个元素、获取最后一个元素、获取小于或等于给定元素的最大元素、获取大于或等于给定元素的最小元素等等。

    需要注意的是,TreeSet 中不允许存储重复的元素,因为它是一个集合而不是一个列表。如果需要存储重复的元素,可以考虑使用 Java 中的其他集合类,例如 ArrayList 或 LinkedList。

  16. 说一下 Java 的集合 Map 有哪些 Map?
    答:
    Java 中的集合 Map 是一种键值对存储结构,其中每个元素由一对键和值组成。Java 提供了多种不同类型的 Map,包括:
    ① HashMap:基于哈希表实现,提供快速的插入、查找和删除操作。它不保证元素的顺序,也不保证在多线程环境下的并发安全性。
    ② TreeMap:基于红黑树实现,提供有序的插入、查找和删除操作。它按照键的自然顺序或者指定的比较器进行排序。
    ③ LinkedHashMap:基于哈希表和双向链表实现,保证元素的插入顺序,也可以按照访问顺序进行排序。
    ④ WeakHashMap:基于哈希表实现,键值对的键是“弱引用”,当键不再被引用时会自动从 Map 中删除。
    ⑤ IdentityHashMap:基于哈希表实现,比较键时使用对象的“身份标识”而不是“相等性”。
    ⑥ EnumMap:基于数组实现,键必须是枚举类型,提供了枚举类型的高效存储和访问。
    ⑦ ConcurrentHashMap:基于分段锁机制实现,支持高并发的插入、查找和删除操作。

    这些 Map 都实现了 Map 接口,提供了类似于 put、get、remove 等方法,但它们的实现方式和性能特点有所不同,开发者可以根据具体的需求选择合适的 Map 实现。

  17. final 怎么用,修饰 Map 可以继续添加数据吗?
    答:
    在 Java 中,使用 final 关键字修饰的 Map 变量只是保证了该变量的引用不可更改,即不能再将该变量指向另一个 Map 对象。但是,Map 本身是可变的数据结构,即使使用 final 修饰,仍然可以向其中添加、删除、修改键值对。

    例如:

    final Map<String, Integer> map = new HashMap<>();
    map.put("key1", 1);
    map.put("key2", 2);
    System.out.println(map); // 输出 {key1=1, key2=2}map.put("key3", 3);
    System.out.println(map); // 输出 {key1=1, key2=2, key3=3}
    

    在上面的例子中,map 变量被声明为 final,但是我们仍然可以向其中添加新的键值对,而不会引发编译错误或运行时异常。

    需要注意的是,如果在初始化 Map 对象时使用了 Collections.unmodifiableMap()方法,那么得到的 Map 对象就是不可变的,任何对其的修改操作都会导致 UnsupportedOperationException 异常。

    例如:

    final Map<String, Integer> map = Collections.unmodifiableMap(new HashMap<>());
    map.put("key1", 1); // 抛出 UnsupportedOperationException 异常
    
  18. Set 和 Map 的比较?
    答:
    Set 和 Map 是 Java 集合框架中两个不同的接口,它们之间有以下几个区别:
    ① 目的不同:Set 用于存储不重复的元素,而 Map 用于存储键值对。
    ② 实现方式不同:Set 使用了 HashSet、TreeSet 等实现方式,而 Map 使用了 HashMap、TreeMap 等实现方式。
    ③ 存储方式不同:Set 中只存储元素,而 Map 中存储键值对。
    ④ 访问方式不同:Set 中只能访问元素,而 Map 中可以通过键获取对应的值。
    ⑤ 迭代方式不同:Set 中使用迭代器遍历元素,而 Map 中使用迭代器遍历键值对或者通过遍历键或值的集合来遍历。

    总之,Set 和 Map 是 Java 集合框架中两个不同的接口,它们之间的区别在于目的、实现方式、存储方式、访问方式和迭代方式。在使用时需要根据实际需求选择使用哪一个接口。

  19. HashMap 说一下,线程安全吗?
    答:
    HashMap 是 Java 中的一种数据结构,它提供了一种快速的键值对映射关系。HashMap 不是线程安全的,因为它的实现是非同步的,它不能保证在多线程环境下的正确性。

    在多线程环境下使用 HashMap 会存在并发访问的问题,可能会导致数据的不一致性或者抛出异常。如果需要在多线程环境下使用 HashMap,可以考虑使用 ConcurrentHashMap 或者使用同步控制来保证线程安全。ConcurrentHashMap 是线程安全的 HashMap 实现,使用了更加高效的并发控制算法,能够在多线程环境下保证线程安全和高效性能。而同步控制则可以使用 synchronized 或者 ReentrantLock 等机制来实现。

  20. HashMap 和 Hashbable 的区别?
    答:
    HashMap 和 Hashable 是两个不同的概念。

    在 Java 中,HashMap 是一种基于哈希表实现的数据结构,用于存储键值对。它允许快速访问、添加和删除元素,其时间复杂度为 O(1)。HashMap 中的键和值都可以为 null。在使用 HashMap 时,需要重写 hashCode()和 equals()方法。

  21. Hashtable 是怎么实现的,为什么线程安全?
    答:
    Hashtable 是一种将键映射到值的数据结构,其实现方式基于哈希表。在哈希表中,每个键都被映射到一个整数索引,该索引指向存储值的桶。在 Hashtable 中,每个键值对都被存储在一个 Entry 对象中,该对象包括键、值和指向下一个 Entry 对象的指针。哈希表中的每个桶都是一个链表,存储具有相同哈希值的 Entry 对象。

    Hashtable 是线程安全的,因为它的访问方法都是同步的。Hashtable 的每个公共方法都使用 synchronized 关键字进行同步,以确保在多线程环境下数据的正确性和一致性。当一个线程访问 Hashtable 中的一个方法时,它会获取对象的锁定,这将阻止其他线程同时访问 Hashtable。因此,Hashtable 保证了在多线程环境中的线程安全性。然而,由于同步是需要开销的,所以在高并发环境下,使用 ConcurrentHashMap 等其他并发集合可能会比 Hashtable 更高效。

  22. HashMap 和 LinkedHashMap 的区别?
    答:
    HashMap 和 LinkedHashMap 都是 Java 中的 Map 接口的实现类,它们的主要区别在于它们维护键值对的方式不同。

    HashMap 使用哈希表来存储键值对,它通过将键对象的哈希码映射到内部数组中的位置来快速查找值。由于哈希表的内部结构是基于哈希函数的,所以 HashMap 的查找效率非常高。但是,由于哈希表不维护插入元素的顺序,因此 HashMap 并不保证遍历顺序和插入顺序相同。

    相反,LinkedHashMap 维护了一个双向链表来保持插入顺序。它在 HashMap 的基础上添加了一个指向前一个节点和后一个节点的指针来维护插入顺序。因此,当你遍历一个 LinkedHashMap 时,它的遍历顺序就是插入顺序。

    在使用 HashMap 和 LinkedHashMap 时,你需要根据自己的需求来选择不同的实现类。如果你需要高效的查找操作,并且不需要保证遍历顺序和插入顺序相同,那么 HashMap 是更好的选择。如果你需要保证遍历顺序和插入顺序相同,那么你应该使用 LinkedHashMap。

  23. HashMap 和 TreeMap 的区别?
    答:
    HashMap 和 TreeMap 都是 Java 中常用的集合类,它们的主要区别在于实现方式和性能表现。

    HashMap 是基于哈希表实现的,它可以快速地进行插入、查找和删除操作,时间复杂度为 O(1)。但是 HashMap 中的元素是无序的,不支持按照键的顺序进行遍历。

    TreeMap 是基于红黑树实现的,它可以自动按照键的顺序进行排序,因此支持按照键的顺序进行遍历。但是 TreeMap 的插入、查找和删除操作的时间复杂度为 O(log n),稍微慢一些。

    另外,HashMap 允许 key 和 value 为 null,而 TreeMap 不允许 key 为 null。在使用 HashMap 时需要注意 key 的 hashcode 和 equals 方法的正确实现,否则可能会出现意外的结果;而在使用 TreeMap 时需要注意 key 的比较器的正确实现,否则也可能会出现意外的结果。

    因此,如果需要快速地进行插入、查找和删除操作,并且不需要按照键的顺序进行遍历,可以使用 HashMap;如果需要按照键的顺序进行遍历,可以使用 TreeMap。

  24. HashMap 里面实际装的是什么?
    答:
    在 JDK 7 中,HashMap 内部使用 Entry 类来表示键值对,每个 Entry 包含一个 key 和一个 value,并且可以用链表的形式将多个 Entry 连接起来,以处理哈希冲突。但是,在 JDK 8 中,Entry 被替换为了 Node,它与 Entry 类似,但是还包含了一个 hash 值,用于快速定位桶中的元素。

    通过使用 Node,JDK 8 可以更高效地处理哈希冲突,同时还可以减少在桶中存储的对象数量,从而减少了内存开销。此外,JDK 8 还引入了红黑树来优化 HashMap,当链表长度过长时,会将链表转换为红黑树,以提高性能。

  25. HashMap 的 key 存储在哪里?和 value 存储在一起吗?那么 value 存储在哪里?说具体点?
    答:
    在 Java 中,HashMap 的 key 是存储在一个数组中的,而 value 则是存储在相应的节点(Node)中。具体来说,HashMap 是通过一个数组来实现键值对存储的,该数组中的每个元素都是一个单向链表的头节点,每个链表节点则包含了一个键值对。如果链表中的元素数量过多,链表会被转换为红黑树,以提高查找效率。

    下面是 HashMap 中的主要组成部分:
    ① 数组:HashMap 内部存储键值对的数据结构是一个数组,数组中的每个元素都是一个单向链表的头节点。
    ② 链表:当 HashMap 中的元素数量较少时,每个数组元素对应的链表中会存储一个或多个键值对。
    ③ Node:当 HashMap 中的元素数量较多时,每个数组元素对应的链表会被转换为红黑树,每个节点都是一个 Node 对象,包含了一个键值对,以及指向下一个节点的指针。
    ④ 红黑树:当链表中的元素数量过多时,链表会被转换为红黑树,以提高查找效率。

    因此,HashMap 的 key 存储在数组中,而 value 则存储在节点(Node)中,这些节点可以是链表节点或红黑树节点。每个节点都包含了一个键值对,以及指向下一个节点的指针。

  26. 自定义类型可以作为 Key 么?
    答:
    是的,Java 中自定义类型可以作为 Key。

    在 Java 中,作为 Key 的类型必须实现 equals()和 hashCode()方法,这是因为 Java 中的 Map 和 Set 都是基于哈希表实现的。如果自定义类型没有实现这两个方法,那么在使用 Map 或 Set 时可能会导致意外的行为。

    equals()方法用于比较两个对象是否相等,通常需要比较对象的成员变量。hashCode()方法用于计算哈希码,通常需要基于对象的成员变量计算哈希码。

    例如,假设我们有一个自定义类型 Person,它有两个成员变量 name 和 age:

    public class Person
    {private String name;private int age;public Person(String name, int age){this.name = name;this.age = age;}// getters and setters@Overridepublic boolean equals(Object o){if (this == o) return true;if (!(o instanceof Person)) return false;Person person = (Person) o;return age == person.age && Objects.equals(name, person.name);}@Overridepublic int hashCode(){return Objects.hash(name, age);}
    }
    

    这里我们实现了 equals()和 hashCode()方法,使得 Person 类型可以作为 Map 或 Set 的 Key。在比较两个 Person 对象是否相等时,我们比较它们的 name 和 age 成员变量。在计算哈希码时,我们基于 name 和 age 计算哈希码。这样,我们就可以在 Map 或 Set 中使用 Person 类型作为 Key 了。

  27. 集合类的工具类是谁?用过工具类哪些方法?
    答:
    Java 中,集合类的工具类是 java.util.Collections。

    java.util.Collections 类提供了很多静态方法来操作集合,例如:

    • sort(List list):对给定的列表进行升序排序。
    • reverse(List list):反转给定列表中元素的顺序。
    • shuffle(List<?> list):随机打乱给定列表中元素的顺序。
    • binarySearch(List<? extends Comparable<? super T>> list, T key):在给定的列表中使用二分查找算法查找指定元素。
    • addAll(Collection<? super T> c, T… elements):向给定的集合中添加一个或多个元素。
    • frequency(Collection<?> c, Object o):返回给定集合中指定元素的出现次数。
    • max(Collection<? extends T> coll):返回给定集合中的最大元素。
    • min(Collection<? extends T> coll):返回给定集合中的最小元素。

    这些方法是 java.util.Collections 类中的一部分,还有其他方法可供使用。使用这些方法可以方便地对集合进行操作。

  28. Collection 和 Collections 的区别?
    答:
    Collection 和 Collections 是两个不同的概念。

    Collection 是 Java 中的一个接口,它是 Java 集合框架中的一个基础接口,定义了一些常用的集合操作方法,包括添加、删除、遍历、查询等。它是 List、Set、Queue、Deque、Map 等集合接口的父接口。

    Collections 是 Java 中的一个工具类,它提供了一系列静态方法,用于操作集合对象。它包括了对集合的排序、查找、替换、填充、复制等操作。它是对 Java 集合框架的一个补充,可以方便地操作集合对象。

    因此,可以看出,Collection 和 Collections 是两个不同的概念。Collection 是一个接口,定义了集合的基本操作;而 Collections 是一个工具类,提供了对集合进行操作的静态方法。

  29. ArrayList 如何实现排序?
    答:
    在 Java 中,可以使用 Collections 类的 sort() 方法对 ArrayList 进行排序。

    假设要对一个 ArrayList 对象进行排序,可以按照以下步骤进行:
    ① 导入 java.util.Collections 类。

    import java.util.Collections;
    

    ② 调用 Collections 类的 sort() 方法,将要排序的 ArrayList 对象作为参数传递给该方法。例如,假设要对名为 list 的 ArrayList 对象进行排序,可以使用以下代码:

    Collections.sort(list);
    

    ③ 如果要对自定义对象进行排序,则需要在该对象中实现 Comparable 接口,并重写 compareTo() 方法。compareTo() 方法用于定义对象之间的比较规则,以便在排序时进行比较。例如,假设要对一个名为 Person 的自定义类进行排序,可以使用以下代码:

    import java.util.Collections;
    import java.util.ArrayList;public class Person implements Comparable<Person>
    {private String name;private int age;// 构造方法和其他方法// 实现 compareTo() 方法@Overridepublic int compareTo(Person other){// 根据年龄进行比较return this.age - other.age;}
    }// 在另一个类中使用 sort() 方法进行排序
    ArrayList<Person> people = new ArrayList<Person>();
    people.add(new Person("Alice", 25));
    people.add(new Person("Bob", 30));
    people.add(new Person("Charlie", 20));
    Collections.sort(people);
    

    在这个例子中,Person 类实现了 Comparable 接口,并重写了 compareTo() 方法,根据年龄进行比较。在排序时,使用 Collections.sort() 对 people 列表进行排序。

  30. HashMap 是否线程安全,怎样解决 HashMap 的线程不安全?
    答:
    HashMap 是非线程安全的数据结构,因为它不是同步的。这意味着,如果多个线程同时访 HashMap,并且其中一个线程在对其进行修改,则可能导致 HashMap 的状态不一致,从而导致数据的破坏。

    为了解决 HashMap 的线程不安全问题,可以采用以下几种方法:

    ① 使用 ConcurrentHashMap 类:ConcurrentHashMap 是 Java 中一个线程安全的 Map 实现,它使用锁分段技术来保证线程安全,使得多个线程可以同时访问不同的分段,从而提高并发性能。

    ② 使用 Collections.synchronizedMap()方法:该方法将 HashMap 包装成一个线程安全的 Map,使用了同步块来保证线程安全,但是性能相对较差,因为所有的操作都需要获得同步锁。

    ③ 使用 Lock 接口:可以使用 Java 中的 Lock 接口来实现线程安全的 HashMap,例如使用 ReentrantLock。这种方法需要手动加锁和解锁,需要注意加锁和解锁的顺序,否则可能会导致死锁。

    总之,如果需要在多线程环境下使用 HashMap,应该使用线程安全的 Map 实现,例如 ConcurrentHashMap,以避免数据的破坏和不一致。

第 13 章 泛型

13.1 泛型的理解

< E > ,泛型参数,使用引用数据类型来赋值。

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值或参数的类型。这个类型参数将在使用时(例如,继承或实现这个接口、创建对象或调用方法时)确定(即传入实际的类型参数,也称为类型实参)。

13.2 泛型在集合、比较器中的使用

Java 泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生 ClassCastException 异常。即,把不安全的因素在编译期间就排除了,而不是运行期;既然通过了编译,那么类型一定是符合要求的,就避免了类型转换。同时,代码更加简洁、健壮。

把一个集合中的内容限制为一个特定的数据类型,这就是泛型背后的核心思想。

13.2.1 集合中使用泛型

Java 泛型可以在集合中用于定义集合中元素的类型。使用泛型可以确保集合中只包含指定类型的元素,避免了类型转换错误和运行时错误。下面是 Java 泛型在 List 和 Map 中的使用示例:

List 中的泛型代码示例:

// 定义一个 List,元素类型为 String
List<String> list = new ArrayList<>();// 添加元素
list.add("apple");
list.add("banana");
list.add("orange");// 遍历元素
for (String fruit : list)
{System.out.println(fruit);
}

Map 中的泛型代码示例:

// 定义一个 Map,键类型为 String,值类型为 Integer
Map<String, Integer> map = new HashMap<>();// 添加键值对
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);// 遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet())
{System.out.println(entry.getKey() + " -> " + entry.getValue());
}

在这些示例中,泛型 < String > 和 < String, Integer > 定义了集合中元素的类型。在 List 中,我们可以将元素添加到集合中并遍历集合中的元素。在 Map 中,我们可以将键值对添加到集合中并遍历键值对。

13.2.2 比较器中使用泛型

Java 泛型可以在比较器中使用,以便于对不同类型的对象进行比较。下面是一个代码示例,演示如何使用 Java 泛型创建一个比较器:

import java.util.Comparator;public class MyComparator<T extends Comparable<T>> implements Comparator<T>
{public int compare(T a, T b){return a.compareTo(b);}
}

在上面的代码中,我们使用 T extends Comparable< T > 来限定泛型类型 T 必须实现 Comparable 接口,这样我们才能使用 a.compareTo(b) 方法比较两个对象的大小。

我们还可以使用 T 类型的 Comparator 对象来创建一个比较器,代码示例如下:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;public class Main
{public static void main(String[] args){List<Integer> numbers = new ArrayList<>();numbers.add(3);numbers.add(1);numbers.add(4);numbers.add(2);Comparator<Integer> comparator = new MyComparator<>();Collections.sort(numbers, comparator);System.out.println(numbers); // 输出 [1, 2, 3, 4]}
}

在上面的代码中,我们创建了一个 MyComparator 对象,并将其传递给 Collections.sort 方法进行排序。由于 MyComparator 实现了 Comparator< Integer > 接口,因此可以将其用于比较 Integer 类型的对象。最后,我们输出了排序后的结果。

注意,在上面的示例中,我们使用了 Integer 类型作为泛型类型,但实际上,任何实现了 Comparable 接口的类型都可以作为泛型类型。

13.3. 自定义泛型类、泛型接口、泛型方法

泛型(Generics)是 Java 中的一项重要特性,它可以让我们编写更加通用和类型安全的代码。Java 中的泛型可以应用于类、接口、方法等不同的场景中。

13.3.1 自定义泛型类

在 Java 中,我们可以通过在类名后面使用尖括号定义泛型参数,来创建一个泛型类。泛型类可以使用任意类型作为其参数类型,包括基本类型和引用类型。

以下是一个简单的泛型类代码示例:

public class MyGenericClass<T>
{private T data;public MyGenericClass(T data){this.data = data;}public T getData(){return data;}
}

在上面的示例中,我们定义了一个名为 MyGenericClass 的泛型类,它有一个类型参数 T。在构造方法中,我们使用泛型参数 T 来定义一个数据成员 data。在 getData() 方法中,我们使用泛型参数 T 作为返回值类型。

使用泛型类时,我们需要在实例化时指定具体的类型参数。代码示例:

MyGenericClass<Integer> myIntObj = new MyGenericClass<>(123);
MyGenericClass<String> myStringObj = new MyGenericClass<>("hello");

在上面的示例中,我们分别创建了一个 MyGenericClass 类型的对象 myIntObj 和 myStringObj,它们的类型参数分别是 Integer 和 String。

13.3.2 自定义泛型接口

在 Java 中,我们也可以定义泛型接口。定义方式与泛型类类似,只需在接口名后面添加尖括号并指定泛型参数即可。

以下是一个简单的泛型接口代码示例:

public interface MyGenericInterface<T>
{public T getValue();
}

在上面的示例中,我们定义了一个名为 MyGenericInterface 的泛型接口,它有一个类型参数 T。在接口中,我们定义了一个 getValue() 方法,它的返回值类型为泛型参数 T。

实现泛型接口时,需要指定具体的类型参数。代码示例:

public class MyIntegerClass implements MyGenericInterface<Integer>
{public Integer getValue(){return 123;}
}public class MyStringClass implements MyGenericInterface<String>
{public String getValue(){return "hello";}
}

在上面的示例中,我们分别实现了 MyGenericInterface 接口,并指定了具体的类型参数。MyIntegerClass 类的 getValue() 方法返回一个 Integer 类型的值,而 MyStringClass 类的 getValue() 方法返回一个 String 类型的值。

13.3.3 自定义泛型方法

除了泛型类和泛型接口外,Java 中还支持定义泛型方法。泛型方法是在方法返回类型前添加尖括号并指定泛型参数的方法。

以下是一个简单的泛型方法代码示例:

public class MyGenericMethod
{public static <T> T getValue(T[] arr, int index){return arr[index];}
}

在上面的示例中,我们定义了一个名为 getValue 的泛型方法,它有两个参数:一个泛型数组 arr 和一个整型变量 index。在方法返回类型前,我们使用泛型参数 T 来指定返回值类型。在方法体中,我们通过数组下标获取指定位置的元素,并返回它。

使用泛型方法时,我们可以在调用方法时指定具体的类型参数,也可以让编译器自动推断类型参数。代码示例:

Integer[] intArr = {1, 2, 3};
String[] strArr = {"hello", "world"};Integer intVal = MyGenericMethod.<Integer>getValue(intArr, 1);
String strVal = MyGenericMethod.<String>getValue(strArr, 0);System.out.println(intVal); // 输出 2
System.out.println(strVal); // 输出 "hello"

在上面的示例中,我们分别调用了 MyGenericMethod.getValue() 方法,并指定了具体的类型参数。我们也可以省略类型参数,让编译器根据方法参数类型推断出类型参数。代码示例:

Integer intVal = MyGenericMethod.getValue(intArr, 1);
String strVal = MyGenericMethod.getValue(strArr, 0);System.out.println(intVal); // 输出 2
System.out.println(strVal); // 输出 "hello"

在上面的示例中,我们省略了类型参数,编译器根据方法参数类型自动推断出类型参数。

13.4 泛型在继承上的体现

  • 类 SuperA 是类 A 的父类,则 G< SuperA > 与 G< A > 的关系:G< SuperA > 和 G< A >是并列的两个类,没有任何子父类的关系。

  • 比如:ArrayList< Object > 、ArrayList< String >没有关系。

  • 类 SuperA 是类 A 的父类或接口,SuperA< G > 与 A< G >的关系:SuperA< G > 与 A< G > 有继承或实现的关系。即 A< G >的实例可以赋值给 SuperA< G >类型的引用(或变量)。

  • 比如:List< String > 与 ArrayList< String >。

13.5 通配符的使用

  • ? 的使用

    • 以集合为例:可以读取数据、不能写入数据。(例外:null)
  • ? extends A
    • 以集合为例:可以读取数据、不能写入数据。(例外:null)
  • ? super A
    • 以集合为例:可以读取数据、可以写入 A 类型或 A 类型子类的数据。(例外:null)

13.6 企业真题

  1. Java 的泛型是什么?有什么好处和优点?JDK 不同版本的泛型有什么区别?
    答:
    Java 的泛型是一种编程语言特性,它允许在编写代码时指定一种数据类型,这种类型可以在运行时确定。具体来说,泛型可以用来创建类、接口和方法,允许在定义这些结构时使用类型参数。

    Java 泛型的好处和优点有很多,包括:
    ① 类型安全性:使用泛型可以在编译时检查类型匹配,避免在运行时出现类型错误导致的异常。
    ② 代码重用性:使用泛型可以编写通用代码,可以用于处理各种类型的数据,从而提高代码重用性。
    ③ 代码可读性:使用泛型可以使代码更加清晰和易读,因为它可以显式地指定参数类型,避免了类型转换和强制类型声明的冗余代码。
    ④ 更好的性能:使用泛型可以消除对象类型转换的开销,从而提高程序的性能。

    JDK 不同版本的泛型有一些区别,主要是在编译器的类型擦除和泛型语法的支持方面。以下是一些主要的区别:

    ① Java 5 之前的版本没有泛型,需要使用 Object 类型来实现类似的功能。
    ② Java 5 中引入了泛型,但是它的实现方式是通过类型擦除来实现的,这意味着在编译期间会将泛型类型擦除为原始类型,从而在运行时无法获得泛型类型信息。
    ③ Java 7 中引入了菱形语法,允许在实例化泛型类型时省略泛型类型的实际参数。
    ④ Java 8 中引入了类型推断,允许编译器根据上下文推断泛型类型,从而提高代码的可读性和简洁性。

    总的来说,泛型是 Java 语言中非常有用的特性,可以提高程序的安全性、可读性和性能,同时也有很多实际应用场景。

  2. 说说你对泛型的了解?
    答:
    泛型是一种编程语言特性,它可以让代码具有更高的灵活性和可重用性。泛型可以在类型安全的前提下,让代码适用于多种不同的数据类型,从而避免了代码的重复编写和维护,提高了代码的可维护性和可读性。

    在 Java 中,泛型可以用来定义类、接口和方法。通过在定义时使用类型参数(例如,T、E 等),可以将类型的具体实现推迟到使用泛型的时候再确定。这样,一个泛型类或方法可以适用于多种不同的数据类型,而不需要为每种数据类型都编写一个独立的类或方法。Java 中的泛型可以实现类型擦除,即在编译时擦除类型信息,使得泛型代码在运行时和非泛型代码一样高效。

    泛型是一种非常有用的编程特性,可以提高代码的可重用性和可读性,是面向对象编程中不可或缺的一部分。

第 14 章 数据结构与集合源码

14.1 数据结构

数据结构,就是一种程序设计优化的方法论,研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,目的是加快程序的执行速度、减少内存占用的空间。

14.1.1 研究对象一:数据间逻辑关系

数据的逻辑结构指反映数据元素之间的逻辑关系,而与数据的存储无关,是独立于计算机的。

  • 集合结构:数据结构中的元素之间除了“同属一个集合” 的相互关系外,别无其他关系。集合元素之间没有逻辑关系。
  • 线性结构:数据结构中的元素存在一对一的相互关系。结构中必须存在唯一的首元素和唯一的尾元素。体现为:一维数组、链表、栈、队列。
  • 树形结构:数据结构中的元素存在一对多的相互关系。
  • 图形结构:数据结构中的元素存在多对多的相互关系。

14.1.2 研究对象二:数据的存储结构

数据的物理结构/存储结构:包括数据元素的表示和关系的表示。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。

  • 结构 1:顺序结构

    • 顺序结构就是使用一组连续的存储单元依次存储逻辑上相邻的各个元素。
    • 优点: 只需要申请存放数据本身的内存空间即可,支持下标访问,也可以实现随机访问。
    • 缺点: 必须静态分配连续空间,内存空间的利用率比较低。插入或删除可能需要移动大量元素,效率比较低
  • 结构 2:链式结构

    • 不使用连续的存储空间存放结构的元素,而是为每一个元素构造一个节点。节点中除了存放数据本身以外,还需要存放指向下一个节点的指针。
    • 优点:不采用连续的存储空间导致内存空间利用率比较高,克服顺序存储结构中预知元素个数的缺点。插入或删除元素时,不需要移动大量的元素。
    • 缺点:需要额外的空间来表达数据之间的逻辑关系,不支持下标访问和随机访问。
  • 结构 3:索引结构

    • 除建立存储节点信息外,还建立附加的索引表来记录每个元素节点的地址。索引表由若干索引项组成。索引项的一般形式是:(关键字,地址)。
    • 优点:用节点的索引号来确定结点存储地址,检索速度快。
    • 缺点: 增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表,因而会花费较多的时间。
  • 结构 4:散列结构

    • 根据元素的关键字直接计算出该元素的存储地址,又称为 Hash 存储。
    • 优点:检索、增加和删除结点的操作都很快。
    • 缺点:不支持排序,一般比用线性表存储需要更多的空间,并且记录的关键字不能重复。

14.1.3 研究对象三:运算结构

  • 施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。
  • 分配资源,建立结构,释放资源。
  • 插入和删除。
  • 获取和遍历。
  • 修改和排序。

14.2 数组

在 Java 中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型。

物理结构特点:

  • 申请内存:一次申请一大段连续的空间,一旦申请到了,内存就固定了。
  • 不能动态扩展(初始化给大了,浪费;给小了,不够用),插入快,删除和查找慢。
  • 存储特点:所有数据存储在这个连续的空间中,数组中的每一个元素都是一个具体的数据(或对象),所有数据都紧密排布,不能有间隔。

14.3 链表

  • 逻辑结构:线性结构。
  • 物理结构:不要求连续的存储空间。
  • 存储特点:链表由一系列结点 node(链表中每一个元素称为结点)组成,结点可以在代码执行过程中动态创建。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
  • 常见的链表结构有:单链表、双链表、循环单链表。

14.4 栈

  • 栈(Stack)又称为堆栈或堆叠,是限制仅在表的一端进行插入和删除运算的线性表。

  • 栈按照先进后出(FILO,first in last out)的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶。每次删除(退栈)的总是删除当前栈中最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能删除。

  • 核心类库中的栈结构有 Stack 和 LinkedList。

    • Stack 就是顺序栈,它是 Vector 的子类。
    • LinkedList 是链式栈。
  • 体现栈结构的操作方法:

    • peek()方法:查看栈顶元素,不弹出。
    • pop()方法:弹出栈。
    • push(E e)方法:压入栈。
  • 时间复杂度:

    • 索引: O(n)
    • 搜索: O(n)
    • 插入: O(1)
    • 移除: O(1)

14.5 队列

  • 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表。
  • 队列是逻辑结构,其物理结构可以是数组,也可以是链表。
  • 队列的修改原则:队列的修改是依先进先出(FIFO)的原则进行的。新来的成员总是加入队尾(即不允许"加塞"),每次离开的成员总是队列头上的(不允许中途离队),即当前"最老的"成员离队。

14.6 树与二叉树

14.6.1 树

  • 结点:树中的数据元素都称之为结点。
  • 根节点:最上面的结点称之为根,一颗树只有一个根且由根发展而来,从另外一个角度来说,每个结点都可以认为是其子树的根。
  • 父节点:结点的上层结点。
  • 子节点:节点的下层结点。
  • 兄弟节点:具有相同父节点的结点称为兄弟节点。
  • 结点的度数:每个结点所拥有的子树的个数称之为结点的度。
  • 树叶:度数为 0 的结点,也叫作终端结点。
  • 非终端节点(或分支节点):树叶以外的节点,或度数不为 0 的节点。
  • 树的深度(或高度):树中结点的最大层次数。
  • 结点的层数:从根节点到树中某结点所经路径上的分支树称为该结点的层数,根节点的层数规定为 1,其余结点的层数等于其父亲结点的层数 + 1。
  • 同代:在同一棵树中具有相同层数的节点。

14.6.2 二叉树的基本概念

二叉树(Binary tree)是树形结构的一个重要类型。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。许多实际问题抽象出来的数据结构往往是二叉树形式,二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。

14.6.3 二叉树的遍历

  • 前序遍历:中左右(根左右)

    • 即先访问根结点,再前序遍历左子树,最后再前序遍历右子 树。前序遍历运算访问二叉树各结点是以根、左、右的顺序进行访问的。
  • 中序遍历:左中右(左根右)

    • 即先中前序遍历左子树,然后再访问根结点,最后再中序遍 历右子树。中序遍历运算访问二叉树各结点是以左、根、右的顺序进行访问的。
  • 后序遍历:左右中(左右根)

    • 即先后序遍历左子树,然后再后序遍历右子树,最后访问根 结点。后序遍历运算访问二叉树各结点是以左、右、根的顺序进行访问的。

14.6.4 经典二叉树

  • 满二叉树: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第 n 层的结点数是 2 的 n - 1 次方,总的结点个数是 2 的 n 次方 - 1。

  • 完全二叉树: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。

  • 二叉排序/查找/搜索树:即为 BST (binary search/sort tree)。满足如下性质:

    • 若它的左子树不为空,则左子树上所有结点的值均小于它的根节点的值。
    • 若它的右子树上所有结点的值均大于它的根节点的值。
    • 它的左、右子树也分别为二叉排序/查找/搜索树。
  • 平衡二叉树:(Self-balancing binary search tree,AVL)首先是二叉排序树,此外具有以下性质:

    • 它是一棵空树或它的左右两个子树的高度差的
      绝对值不超过 1。
    • 并且左右两个子树也都是一棵平衡二叉树。
    • 不要求非叶节点都有两个子结点。
  • 平衡二叉树的目的是为了减少二叉查找树的层次,提高查找速度。平衡二叉树的常用实现有红黑树、AVL、替罪羊树、Treap、伸展树等。

  • 红黑树:即 Red-Black Tree。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

  • 红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,它是在 1972 年由 Rudolf Bayer 发明的。红黑树是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在 O(log n)时间内做查找,插入和删除, 这里的 n 是树中元素的数目。

  • 红黑树的特性:

    • 每个节点是红色或者黑色。
    • 根节点是黑色。
    • 每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL 或 NULL)的叶子节点)
    • 每个红色节点的两个子节点都是黑色的。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
    • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。(确保没有一条路径会比其他路径长出 2 倍)
  • 当我们插入或删除节点时,可能会破坏已有的红黑树,使得它不满足以上 5 个要求,那么此时就需要进行处理,使得它继续满足以上的 5 个要求:

    • recolor :将某个节点变红或变黑。
    • rotation :将红黑树某些结点分支进行旋转。(左旋或右旋)
  • 红黑树可以通过红色节点和黑色节点尽可能的保证二叉树的平衡。主要是用它来存储有序的数据,它的时间复杂度是 O(logN),效率非常之高。

14.7 List 接口下的实现类的源码剖析

14.7.1 List 接口特点

  • List 集合所有的元素是以一种线性方式进行存储的,例如,存元素的顺序是 11、22、33。那么集合中,元素的存储就是按照 11、22、33 的顺序完成的)。

  • 它是一个元素存取有序的集合。即元素的存入顺序和取出顺序有保证。

  • 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。

  • 集合中可以有重复的元素,通过元素的 equals 方法,来比较是否为重复的元素。

  • 注意:
    List 集合关心元素是否有序,而不关心是否重复。

  • List 接口的主要实现类

    • ArrayList:动态数组。
    • Vector:动态数组。
    • LinkedList:双向链表。
    • Stack:栈。

14.7.2 动态数组 ArrayList 与 Vector

  • Java 的 List 接口的实现类中有两个动态数组的实现:ArrayList 和 Vector。

  • ArrayList 与 Vector 的区别

    • 它们的底层物理结构都是数组,我们称为动态数组。
    • ArrayList 是新版的动态数组,线程不安全,效率高,Vector 是旧版的动态数组,线程安全,效率低。
    • 动态数组的扩容机制不同,ArrayList 默认扩容为原来的 1.5 倍,Vector 默认扩容增加为原来的 2 倍。
    • 数组的初始化容量,如果在构建 ArrayList 与 Vector 的集合对象时,没有显式指定初始化容量,那么 Vector 的内部数组的初始容量默认为 10,而 ArrayList 在 JDK 6.0 及之前的版本也是 10,JDK8.0 之后的版本 ArrayList 初始化为长度为 0 的空数组,之后在添加第一个元素时,再创建长度为 10 的数组。
    • 原因:
      • 用的时候,再创建数组,避免浪费。因为很多方法的返回值是 ArrayList 类型,需要返回一个 ArrayList 的对象,例如:后期从数据库查询对象的方法,返回值很多就是 ArrayList。有可能你要查询的数据不存在,要么返回 null,要么返回一个没有元素的 ArrayList 对象。

14.7.3 ArrayList 部分源码分析

  • JDK1.7 中的源代码:

    // 属性
    private transient Object[]elementData;    // 存储底层数组元素
    private int size;    // 记录数组中存储的元素的个数// 构造器
    public ArrayList()
    {this(10);    // 指定初始容量为 10
    }
    public ArrayList(int initialCapacity)
    {super();// 检查初始容量的合法性if(initialCapacity < 0)throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);// 数组初始化为长度为 initialCapacity 的数组this.elementData = new Object[initialCapacity];
    }// 方法:add()相关方法
    public boolean add(E e)
    {ensureCapacityInternal(size + 1);    // 查看当前数组是否够多存一个元素elementData[size++] = e;    // 将元素 e 添加到 elementData 数组中return true;
    }
    private void ensureCapacityInternal(int minCapacity)
    {modCount++;// 如果 if 条件满足,则进行数组的扩容if(minCapacity - elementData.length > 0)grow(minCapacity);
    }
    private void grow(int minCapacity)
    {// overflow-conscious codeint oldCapacity = elementData.length;    // 当前数组容量int newCapacity = oldCapacity + (oldCapacity >> 1);    // 新数组容量是旧数组容量的1.5倍if(newCapacity - minCapacity < 0)    // 判断旧数组的 1.5 倍是否够newCapacity = minCapacity;// 判断旧数组的 1.5 倍是否超过最大数组限制if(newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 复制一个新数组elementData = Arrays.copyOf(elementData, newCapacity);
    }// 方法:remove()相关方法
    public E remove(int index)
    {rangeCheck(index);    //判断 index 是否在有效的范围内modCount++;    //修改次数加 1// 取出[index]位置的元素,[index]位置的元素就是要被删除的元素,用于最后返回被删除的元素E oldValue = elementData(index);int numMoved = size - index - 1;    // 确定要移动的次数// 如果需要移动元素,就用 System.arraycopy 移动元素if(numMoved > 0)System.arraycopy(elementData, index + 1, elementData, index, numMoved);//将 elementData[size-1]位置置空,让 GC 回收空间,元素个数减少elementData[--size] = null;return oldValue;
    }
    private void rangeCheck(int index)
    {if(index >= size)    //index 不合法的情况throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    E elementData(int index)
    {// 返回指定位置的元素return(E)elementData[index];
    }// 方法:set()方法相关
    public E set(int index, E element)
    {rangeCheck(index);    // 检验 index 是否合法// 取出[index]位置的元素,[index]位置的元素就是要被替换的元素,用于最后返回被替换的元素E oldValue = elementData(index);// 用 element 替换[index]位置的元素elementData[index] = element;return oldValue;
    }// 方法:get()相关方法
    public E get(int index)
    {rangeCheck(index);    // 检验 index 是否合法return elementData(index);    // 返回[index]位置的元素
    }// 方法:indexOf()
    public int indexOf(Object o)
    {// 分为 o 是否为空两种情况if(o == null){// 从前往后找for(int i = 0; i < size; i++)if(elementData[i] == null)return i;}else{for(int i = 0; i < size; i++)if(o.equals(elementData[i]))return i;}return -1;
    }// 方法:lastIndexOf()
    public int lastIndexOf(Object o)
    {// 分为 o 是否为空两种情况if(o == null){// 从后往前找for(int i = size - 1; i >= 0; i--)if(elementData[i] == null)return i;}else{for(int i = size - 1; i >= 0; i--)if(o.equals(elementData[i]))return i;}return -1;
    }
    
  • JDK1.8 中的源代码:

    // 属性
    transient Object[] elementData;
    private int size;
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 构造器
    public ArrayList()
    {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;    // 初始化为空数组
    }// 方法:add()相关方法
    public boolean add(E e)
    {// 查看当前数组是否够多存一个元素ensureCapacityInternal(size + 1);    // Increments modCount!// 存入新元素到[size]位置,然后 size 自增 1elementData[size++] = e;return true;
    }
    private void ensureCapacityInternal(int minCapacity)
    {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    private static int calculateCapacity(Object[] elementData, int minCapacity)
    {// 如果当前数组还是空数组if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){// 那么 minCapacity 取 DEFAULT_CAPACITY 与 minCapacity 的最大值return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;
    }// 查看是否需要扩容
    private void ensureExplicitCapacity(int minCapacity)
    {modCount++;    // 修改次数加 1// 如果需要的最小容量比当前数组的长度大,即当前数组不够存,就扩容if (minCapacity - elementData.length > 0)grow(minCapacity);
    }
    private void grow(int minCapacity)
    {// overflow-conscious codeint oldCapacity = elementData.length;    // 当前数组容量int newCapacity = oldCapacity + (oldCapacity >> 1);    // 新数组容量是旧数组容量的 1.5 倍// 看旧数组的 1.5 倍是否够if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 看旧数组的 1.5 倍是否超过最大数组限制if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 复制一个新数组elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

14.7.4 ArrayList 相关方法图示

  • ArrayList 采用数组作为底层实现

  • ArrayList 自动扩容过程

  • ArrayList 的 add(E e)方法

  • ArrayList 的 add(int index,E e)方法

14.7.5 Vector 部分源码分析

  • JDK1.8 中的源代码:

    // 属性
    protected Object[] elementData;
    protected int elementCount;// 构造器
    public Vector()
    {this(10);    // 指定初始容量 initialCapacity 为 10
    }
    public Vector(int initialCapacity)
    {this(initialCapacity, 0);    // 指定 capacityIncrement 增量为 0
    }
    public Vector(int initialCapacity, int capacityIncrement)
    {super();// 判断了形参初始容量 initialCapacity 的合法性if (initialCapacity < 0)throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);// 创建了一个 Object[]类型的数组this.elementData = new Object[initialCapacity];// 增量,默认是 0,如果是 0,后面就按照 2 倍增加,如果不是 0,后面就按照你指定的增量进行增量this.capacityIncrement = capacityIncrement;
    }// 方法:add()相关方法
    // synchronized 意味着线程安全的
    public synchronized boolean add(E e)
    {modCount++;// 看是否需要扩容ensureCapacityHelper(elementCount + 1);// 把新的元素存入[elementCount],存入后,elementCount 元素的个数增 1elementData[elementCount++] = e;return true;
    }
    private void ensureCapacityHelper(int minCapacity)
    {// 看是否超过了当前数组的容量if (minCapacity - elementData.length > 0)grow(minCapacity);    // 扩容
    }
    private void grow(int minCapacity) 、
    {// overflow-conscious codeint oldCapacity = elementData.length;    // 获取目前数组的长度// 如果 capacityIncrement 增量是 0,新容量 = oldCapacity 的 2 倍// 如果 capacityIncrement 增量是不是 0,新容量 = oldCapacity + capacityIncrement 增量;int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);// 如果按照上面计算的新容量还不够,就按照你指定的需要的最小容量来扩容 minCapacityif (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 如果新容量超过了最大数组限制,那么单独处理if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 把旧数组中的数据复制到新数组中,新数组的长度为 newCapacityelementData = Arrays.copyOf(elementData, newCapacity);
    }//方法:remove()相关方法
    public boolean remove(Object o)
    {return removeElement(o);
    }
    public synchronized boolean removeElement(Object obj)
    {modCount++;// 查找 obj 在当前 Vector 中的下标int i = indexOf(obj);// 如果 i>=0,说明存在,删除[i]位置的元素if (i >= 0){removeElementAt(i);return true;}return false;
    }// 方法:indexOf()
    public int indexOf(Object o)
    {return indexOf(o, 0);
    }
    public synchronized int indexOf(Object o, int index)
    {if (o == null){// 要查找的元素是 null 值for (int i = index ; i < elementCount ; i++)if (elementData[i]==null)    // 如果是 null 值,用==null 判断return i;}else{//要查找的元素是非 null 值for (int i = index ; i < elementCount ; i++)if (o.equals(elementData[i]))    // 如果是非 null 值,用 equals 判断return i;}return -1;
    }//方法:removeElementAt()
    public synchronized void removeElementAt(int index)
    {modCount++;// 判断下标的合法性if (index >= elementCount){throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);}else if (index < 0){throw new ArrayIndexOutOfBoundsException(index);}// j 是要移动的元素的个数int j = elementCount - index - 1;// 如果需要移动元素,就调用 System.arraycopy 进行移动if (j > 0){// 把 index+1 位置以及后面的元素往前移动// index+1 的位置的元素移动到 index 位置,依次类推一共移动 j 个System.arraycopy(elementData, index + 1, elementData, index, j);}// 元素的总个数减少elementCount--;// 将 elementData[elementCount]这个位置置空,用来添加新元素,位置的元素等着被 GC 回收elementData[elementCount] = null;/* to let gc do its work */
    }
    

14.7.6 链表 LinkedList

Java 中有双链表的实现:LinkedList,它是 List 接口的实现类。

14.7.7 链表与动态数组的区别

  • 动态数组底层的物理结构是数组,因此根据索引访问的效率非常高。但是非末尾位置的插入和删除效率不高,因为涉及到移动元素。另外添加操作时涉及到扩容问题,就会增加时空消耗。
  • 链表底层的物理结构是链表,因此根据索引访问的效率不高,即查找元素慢。但是插入和删除不需要移动元素,只需要修改前后元素的指向关系即可,所以插入、删除元素快。而且链表的添加不会涉及到扩容问题。

14.7.8 LinkedList 源码分析

  • JDK1.8 中的源代码:

    // 属性
    transient Node<E> first;   // 记录第一个结点的位置
    transient Node<E> last;    // 记录当前链表的尾元素
    transient int size = 0;    // 记录最后一个结点的位置// 构造器
    public LinkedList()
    {}// 方法:add()相关方法
    public boolean add(E e)
    {linkLast(e);    // 默认把新元素链接到链表尾部return true;
    }
    void linkLast(E e)
    {final Node<E> l = last;    // 用 l 记录原来的最后一个结点// 创建新结点final Node<E> newNode = new Node<>(l, e, null);// 现在的新结点是最后一个结点了last = newNode;// 如果 l==null,说明原来的链表是空的if (l == null)// 那么新结点同时也是第一个结点first = newNode;else// 否则把新结点链接到原来的最后一个结点的 next 中l.next = newNode;// 元素个数增加size++;// 修改次数增加modCount++;
    }// 其中,Node 类定义如下
    private static class Node<E>
    {E item;    // 元素数据Node<E> next;    // 下一个结点Node<E> prev;    // 前一个结点Node(Node<E> prev, E element, Node<E> next){this.item = element;this.next = next;this.prev = prev;}
    }// 方法:获取 get()相关方法
    public E get(int index)
    {checkElementIndex(index);return node(index).item;
    }// 方法:插入 add()相关方法
    public void add(int index, E element)
    {checkPositionIndex(index);   // 检查 index 范围if (index == size)   // 如果 index==size,连接到当前链表的尾部linkLast(element);elselinkBefore(element, node(index));
    }
    Node<E> node(int index)
    {assert isElementIndex(index);/*index < (size >> 1)采用二分思想,先将 index 与长度 size 的一半比较,如果 index<size/2,就只从位置 0往后遍历到位置 index 处,而如果 index>size/2,就只从位置 size 往前遍历到位置 index 处。这样可以减少一部分不必要的遍历。*/// 如果 index<size/2,就从前往后找目标结点if (index < (size >> 1)){Node<E> x = first;for (int i = 0; i < index; i++)x = x.next;return x;}else{// 否则从后往前找目标结点Node<E> x = last;for (int i = size - 1; i > index; i--)x = x.prev;return x;}
    }// 把新结点插入到[index]位置的结点 succ 前面
    void linkBefore(E e, Node<E> succ)
    {// succ 是[index]位置对应的结点assert succ != null;final Node<E> pred = succ.prev;    // [index]位置的前一个结点// 新结点的 prev 是原来[index]位置的前一个结点// 新结点的 next 是原来[index]位置的结点final Node<E> newNode = new Node<>(pred, e, succ);// [index]位置对应的结点的 prev 指向新结点succ.prev = newNode;// 如果原来[index]位置对应的结点是第一个结点,那么现在新结点是第一个结点if (pred == null)first = newNode;elsepred.next = newNode;    // 原来[index]位置的前一个结点的 next 指向新结点size++;modCount++;
    }// 方法:remove()相关方法
    public boolean remove(Object o)
    {// 分 o 是否为空两种情况if (o == null){// 找到 o 对应的结点 xfor (Node<E> x = first; x != null; x = x.next){if (x.item == null){unlink(x);   // 删除 x 结点return true;}}}else{// 找到 o 对应的结点 xfor (Node<E> x = first; x != null; x = x.next){if (o.equals(x.item)){unlink(x);   // 删除 x 结点return true;}}}return false;
    }
    E unlink(Node<E> x)
    {// x 是要被删除的结点assert x != null;final E element = x.item;    // 被删除结点的数据final Node<E> next = x.next;    // 被删除结点的下一个结点final Node<E> prev = x.prev;    // 被删除结点的上一个结点// 如果被删除结点的前面没有结点,说明被删除结点是第一个结点if (prev == null){// 那么被删除结点的下一个结点变为第一个结点first = next;}else{// 被删除结点不是第一个结点// 被删除结点的上一个结点的 next 指向被删除结点的下一个结点prev.next = next;// 断开被删除结点与上一个结点的链接x.prev = null;    // 使得 GC 回收}// 如果被删除结点的后面没有结点,说明被删除结点是最后一个结点if (next == null){// 那么被删除结点的上一个结点变为最后一个结点last = prev;}else{// 被删除结点不是最后一个结点// 被删除结点的下一个结点的 prev 执行被删除结点的上一个结点next.prev = prev;// 断开被删除结点与下一个结点的连接x.next = null;     // 使得 GC 回收}// 把被删除结点的数据也置空,使得 GC 回收x.item = null;// 元素个数减少size--;// 修改次数增加modCount++;// 返回被删除结点的数据return element;
    }
    public E remove(int index)
    {// index 是要删除元素的索引位置checkElementIndex(index);return unlink(node(index));
    }
    

14.7.9 LinkedList 相关方法图示

  • 只有 1 个元素的 LinkedList

  • 包含 4 个元素的 LinkedList

  • add(E e)方法

  • add(int index,E e)方法

  • remove(Object obj)方法

  • remove(int index)方法

14.8 Map 接口下的实现类的源码剖析

14.8.1 哈希表的物理结构

  • HashMap 和 Hashtable 底层都是哈希表(也称散列表),其中维护了一个长度为 2 的幂次方的 Entry 类型的数组 table,数组的每一个索引位置被称为一个桶(bucket),你添加的映射关系(key,value)最终都被封装为一个 Map.Entry 类型的对象,放到某个 table[index]桶中。

  • 使用数组的目的是查询和添加的效率高,可以根据索引直接定位到某个 table[index]。

14.8.2 HashMap 中数据添加过程

  • JDK7 中过程分析:

    • 在底层创建了长度为 16 的 Entry[] table 的数组。

      HashMap map = new HashMap();
      map.put(key1,value1);
      
      • 分析过程如下:
        ① 将 (key1,value1) 添加到当前 hashmap 的对象中。首先会调用 key1 所在类的 hashCode() 方法,计算 key1 的哈希值 1,此哈希值 1 再经过某种运算(hash()),得到哈希值 2。此哈希值 2 再经过某种运算(indexFor()),确定在底层 table 数组中的索引位置 i。

        • 如果数组索引为 i 上的数据为空,则 (key1,value1) 直接添加成功,位置 1。

        • 如果数组索引为 i 上的数据不为空,有 (key2,value2),则需要进一步判断:

          • 判断 key1 的哈希值 2 与 key2 的哈希值是否相同:

            • 如果哈希值不同,则 (key1,value1) 直接添加成功,位置 2。
            • 如果哈希值相同,则需要继续调用 key1 所在类的 equals() 方法,将 key2 放入 equals() 形参进行判断:
              • equals 方法返回 false:则 (key1,value1) 直接添加成功,位置 3。
              • equals 方法返回 true:默认情况下,value1 会覆盖 value2。
          • 位置 1:直接将 (key1,value1) 以 Entry 对象的方式存放到 table 数组索引 i 的位置。
          • 位置 2、位置 3:(key1,value1) 与现有的元素以链表的方式存储在 table 数组索引 i 的位置,新添加的元素指向旧添加的元素。

          ② 在不断的添加的情况下,满足如下条件的情况下,会进行扩容:

          if ((size >= threshold) && (null != table[bucketIndex])) :
          

          默认情况下,当要添加的元素个数超过 12(即:数组的长度 * loadFactor 得到的结果)时,就要考虑扩容。

        • 补充:JDK7 源码中定义的:

          static class Entry<K,V> implements Map.Entry<K,V>
          
    • 获取 Map 集合中 key1 对应的 value 值的操作。

      map.get(key1);
      

      ① 计算 key1 的 hash 值,用这个方法 hash(key1)。
      ② 找 index = table.length-1 & hash。
      ③ 如果 table[index] 不为空,那么就挨个比较哪个 Entry 的 key 与它相同,就返回它的 value。

    • 从 Map 集合中删除 key1 及其对应的 value 值。

      map.remove(key1);
      

      ① 计算 key1 的 hash 值,用这个方法 hash(key1)。
      ② 找 index = table.length-1 & hash。
      ③ 如果 table[index] 不为空,那么就挨个比较哪个 Entry 的 key 与它相同,就删除它,把它前面的 Entry 的 next 的值修改为被删除 Entry 的 next。

  • JDK8 中过程分析:

    • 下面说明是 JDK8 相较于 JDK7 的不同之处:
      ① 使用 HashMap()的构造器创建对象时,并没有在底层初始化长度为 16 的 table 数组。
      ② jdk8 中添加的 key,value 封装到了 HashMap.Node 类的对象中。而非 jdk7 中的 HashMap.Entry。
      ③ jdk8 中新增的元素所在的索引位置如果有其他元素。在经过一系列判断后,如果能添加,则是旧的元素指向新的元素。而非 jdk7 中的新的元素指向旧的元素。“七上八下”
      ④ jdk7 时底层的数据结构是:数组+单向链表。 而 jdk8 时,底层的数据结构是:数组+单向链表+红黑树。

    • 红黑树出现的时机:当某个索引位置 i 上的链表的长度达到 8,且数组的长度超过 64 时,此索引位置上的元素要从单向链表改为红黑树。如果索引 i 位置是红黑树的结构,当不断删除元素的情况下,当前索引 i 位置上的元素的个数低于 6 时,要从红黑树改为单向链表。

14.8.3 HashMap 源码剖析

  • JDK1.7 中的源代码:

    • Entry:
      key-value 被封装为 HashMap.Entry 类型,而这个类型实现了 Map.Entry 接口。

      public class HashMap<K,V>
      {transient Entry<K,V>[] table;static class Entry<K,V> implements Map.Entry<K,V>{final K key;V value;Entry<K,V> next;int hash;/*** Creates new entry.*/Entry(int h, K k, V v, Entry<K,V> n){value = v;next = n;key = k;hash = h;}// ......}
      }
      
    • 属性:

      // table 数组的默认初始化长度
      static final int DEFAULT_INITIAL_CAPACITY = 16;// 哈希表
      transient Entry<K,V>[] table;// 哈希表中 key-value 的个数
      transient int size;// 临界值、阈值(扩容的临界值)
      int threshold;// 加载因子
      final float loadFactor;// 默认加载因子
      static final float DEFAULT_LOAD_FACTOR = 0.75f;
      
    • 构造器:

      public HashMap()
      {// DEFAULT_INITIAL_CAPACITY:默认初始容量 16// DEFAULT_LOAD_FACTOR:默认加载因子 0.75this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
      }public HashMap(int initialCapacity, float loadFactor)
      {// 校验 initialCapacity 合法性if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity:" + initialCapacity);// 校验 initialCapacity 合法性if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// 校验 loadFactor 合法性if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " + loadFactor);// 计算得到 table 数组的长度(保证 capacity 是 2 的整次幂)int capacity = 1;while (capacity < initialCapacity)capacity <<= 1;// 加载因子,初始化为 0.75this.loadFactor = loadFactor;// threshold 初始为默认容量threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);// 初始化 table 数组table = new Entry[capacity];useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);init();
      }
      
    • put()方法:

      public V put(K key, V value)
      {// 如果 key 是 null,单独处理,存储到 table[0]中,如果有另一个 key 为 null,value 覆盖if (key == null)return putForNullKey(value);// 对 key 的 hashCode 进行干扰,算出一个 hash 值/** hashCode 值 xxxxxxxxxx table.length-1 000001111** hashCode 值 xxxxxxxxxx 无符号右移几位和原来的 hashCode 值做^运算, 使得 hashCode* 高位二进制值参与计算, 也发挥作用,降低 index 冲突的概率。*/int hash = hash(key);// 计算新的映射关系应该存到 table[i]位置, i = hash & table.length-1,可以保证 i// 在[0,table.length-1]范围内int i = indexFor(hash, table.length);// 检查 table[i]下面有没有 key 与我新的映射关系的 key 重复,如果重复替换 valuefor (Entry<K, V> e = table[i]; e != null; e = e.next){Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))){V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;// 添加新的映射关系addEntry(hash, key, value, i);return null;
      }// 如果 key 是 null,直接存入[0]的位置
      private V putForNullKey(V value)
      {// 判断是否有重复的 key,如果有重复的,就替换 valuefor (Entry<K, V> e = table[0]; e != null; e = e.next){if (e.key == null){V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;// 把新的映射关系存入[0]的位置,而且 key 的 hash 值用 0 表示addEntry(0, null, value, 0);return null;
      }final int hash(Object k)
      {int h = 0;if (useAltHashing){if (k instanceof String){return sun.misc.Hashing.stringHash32((String) k);}h = hashSeed;}h ^= k.hashCode();// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
      }static int indexFor(int h, int length)
      {return h & (length - 1);
      }void addEntry(int hash, K key, V value, int bucketIndex)
      {// 判断是否需要库容// 扩容:(1)size 达到阈值(2)table[i]正好非空if ((size >= threshold) && (null != table[bucketIndex])){// table 扩容为原来的 2 倍,并且扩容后,会重新调整所有 key-value 的存储位置resize(2 * table.length);// 新的 key-value 的 hash 和 index 也会重新计算hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}// 存入 table 中createEntry(hash, key, value, bucketIndex);
      }void createEntry(int hash, K key, V value, int bucketIndex)
      {Entry<K, V> e = table[bucketIndex];// 原来 table[i]下面的映射关系作为新的映射关系 nexttable[bucketIndex] = new Entry<>(hash, key, value, e);// 个数增加size++;
      }
      
  • JDK1.8 中的源代码:

    • Node:
      key-value 被封装为 HashMap.Node 类型或 HashMap.TreeNode 类型,它俩都直接或间接的实现了 Map.Entry 接口。
      存储到 table 数组的可能是 Node 结点对象,也可能是 TreeNode 结点对象,它们也是 Map.Entry 接口的实现类。即 table[index]下的映射关系可能串起来一个链表或一棵红黑树。

      public class HashMap<K,V>
      {transient Node<K,V>[] table;// Node 类static class Node<K,V> implements Map.Entry<K,V>{final int hash;final K key;V value;Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next){this.hash = hash;this.key = key;this.value = value;this.next = next;}// 其它结构:......}// TreeNode 类static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>{TreeNode<K,V> parent;TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;boolean red;    // 是红结点还是黑结点TreeNode(int hash, K key, V val, Node<K,V> next){super(hash, key, val, next);}}//......
      }
      
    • 属性:

      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;    // 默认的初始容量16
      static final int MAXIMUM_CAPACITY = 1 << 30;    // 最大容量 1 << 30
      static final float DEFAULT_LOAD_FACTOR = 0.75f;    // 默认加载因子
      static final int TREEIFY_THRESHOLD = 8;    // 默认树化阈值 8,当链表的长度达到这个值后,要考虑树化
      static final int UNTREEIFY_THRESHOLD = 6;    // 默认反树化阈值 6,当树中结点的个数达到此阈值后要考虑变为链表
      // 当单个的链表的结点个数达到 8,并且 table 的长度达到 64,才会树化。
      // 当单个的链表的结点个数达到 8,但是 table 的长度未达到 64,会先扩容
      static final int MIN_TREEIFY_CAPACITY = 64;    // 最小树化容量 64
      transient Node<K,V>[] table;    // 数组
      transient int size;   // 记录有效映射关系的对数,也是 Entry 对象的个数
      int threshold;    // 阈值,当 size 达到阈值时,考虑扩容
      final float loadFactor;    // 加载因子,影响扩容的频率
      
    • 构造器:

      public HashMap()
      {this.loadFactor = DEFAULT_LOAD_FACTOR;    // all other fields defaulted (其他字段都是默认值)
      }
      
    • put()方法:

      public V put(K key, V value)
      {return putVal(hash(key), key, value, false, true);
      }static final int hash(Object key)
      {int h;// 如果 key 是 null,hash 是 0// 如果 key 非 null,用 key 的 hashCode 值 与 key 的 hashCode 值高 16 进行异或// 即就是用 key 的 hashCode 值高 16 位与低 16 位进行了异或的干扰运算/*index = hash & table.length-1如果用 key 的原始的 hashCode 值 与 table.length-1 进行按位与,那么基本上高 16 没机会用上。这样就会增加冲突的概率,为了降低冲突的概率,把高 16 位加入到 hash 信息中。*/return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
      }final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
      {Node<K,V>[] tab;    // 数组Node<K,V> p;    // 一个结点int n, i;    // n 是数组的长度 i 是下标// tab 和 table 等价// 如果 table 是空的if ((tab = table) == null || (n = tab.length) == 0){n = (tab = resize()).length;/*tab = resize();n = tab.length;*//*如果 table 是空的,resize()完成了①创建了一个长度为 16 的数组②threshold = 12n = 16*/}// i = (n - 1) & hash ,下标 = 数组长度-1 & hash// p = tab[i] 第 1 个结点// if(p == null) 条件满足的话说明 table[i]还没有元素if ((p = tab[i = (n - 1) & hash]) == null){// 把新的映射关系直接放入 table[i]tab[i] = newNode(hash, key, value, null);// newNode()方法就创建了一个 Node 类型的新结点,新结点的 next 是 null}else{Node<K,V> e; K k;// p 是 table[i]中第一个结点// if(table[i]的第一个结点与新的映射关系的 key 重复)if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;  // 用 e 记录这个 table[i]的第一个结点else if (p instanceof TreeNode){//如果 table[i]第一个结点是一个树结点//单独处理树结点//如果树结点中,有 key 重复的,就返回那个重复的结点用 e 接收,即 e!=null//如果树结点中,没有 key 重复的,就把新结点放到树中,并且返回 null,即 e=nulle = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);}else{// table[i]的第一个结点不是树结点,也与新的映射关系的 key 不重复// binCount 记录了 table[i]下面的结点的个数for (int binCount = 0; ; ++binCount){// 如果 p 的下一个结点是空的,说明当前的 p 是最后一个结点if ((e = p.next) == null){// 把新的结点连接到 table[i]的最后p.next = newNode(hash, key, value, null);// 如果 binCount>=8-1,达到 7 个时if (binCount >= TREEIFY_THRESHOLD - 1)    // -1 for 1s// 要么扩容,要么树化treeifyBin(tab, hash);break;}// 如果 key 重复了,就跳出 for 循环,此时 e 结点记录的就是那个 key 重复的结点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;    // 下一次循环,e=p.next,就类似于 e=e.next,往链表下移动}}// 如果这个 e 不是 null,说明有 key 重复,就考虑替换原来的 valueif (e != null){// existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);    // 什么也没干return oldValue;}}++modCount;// 元素个数增加// size 达到阈值if (++size > threshold)resize();    // 一旦扩容,重新调整所有映射关系的位置afterNodeInsertion(evict);    // 什么也没干return null;
      }final Node<K,V>[] resize()
      {// oldTab 原来的 tableNode<K,V>[] oldTab = table;// oldCap:原来数组的长度int oldCap = (oldTab == null) ? 0 : oldTab.length;// oldThr:原来的阈值int oldThr = threshold;//最开始 threshold 是 0// newCap,新容量// newThr:新阈值int newCap, newThr = 0;if (oldCap > 0)    // 说明原来不是空数组{if (oldCap >= MAXIMUM_CAPACITY)    // 是否达到数组最大限制{threshold = Integer.MAX_VALUE;return oldTab;}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// newCap = 旧的容量*2 ,新容量<最大数组容量限制// 新容量:32,64,...// oldCap >= 初始容量 16// 新阈值重新算 = 24,48 ....newThr = oldThr << 1;    // double threshold}else if (oldThr > 0)    // initial capacity was placed in thresholdnewCap = oldThr;else    // zero initial threshold signifies using defaults{newCap = DEFAULT_INITIAL_CAPACITY;    // 新容量是默认初始化容量 16// 新阈值= 默认的加载因子 * 默认的初始化容量 = 0.75*16 = 12newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0){float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;    // 阈值赋值为新阈值 12,24.。。。// 创建了一个新数组,长度为 newCap,16,32,64.。。@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null)    // 原来不是空数组{// 把原来的 table 中映射关系,倒腾到新的 table 中for (int j = 0; j < oldCap; ++j){Node<K,V> e;if ((e = oldTab[j]) != null)    // e 是 table 下面的结点{oldTab[j] = null;    // 把旧的 table[j]位置清空if (e.next == null)    // 如果是最后一个结点newTab[e.hash & (newCap - 1)] = e;    // 重新计算 e 的在新 table 中的存储位置,然后放入else if (e instanceof TreeNode)    // 如果 e 是树结点// 把原来的树拆解,放到新的 table((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else    // preserve order{Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;// 把原来 table[i]下面的整个链表,重新挪到了新的 table 中do{next = e.next;if ((e.hash & oldCap) == 0){if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else{if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}}while ((e = next) != null);if (loTail != null){loTail.next = null;newTab[j] = loHead;}if (hiTail != null){hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
      }Node<K,V> newNode(int hash, K key, V value, Node<K,V> next)
      {// 创建一个新结点return new Node<>(hash, key, value, next);
      }final void treeifyBin(Node<K,V>[] tab, int hash)
      {int n, index;Node<K,V> e;// MIN_TREEIFY_CAPACITY:最小树化容量 64// 如果 table 是空的,或者 table 的长度没有达到 64if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();    // 先扩容else if ((e = tab[index = (n - 1) & hash]) != null){// 用 e 记录 table[index]的结点的地址TreeNode<K,V> hd = null, tl = null;/*do...while,把 table[index]链表的 Node 结点变为 TreeNode 类型的结点*/do{TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;    // hd 记录根结点else{p.prev = tl;tl.next = p;}tl = p;}while ((e = e.next) != null);// 如果 table[index]下面不是空if ((tab[index] = hd) != null)hd.treeify(tab);   // 将 table[index]下面的链表进行树化}
      }
      
    • HashMap 的 put()执行过程:

14.8.4 LinkedHashMap 源码剖析

  • JDK1.7 中的源代码:

    • 内部定义的 Entry 如下:

      static class Entry<K,V> extends HashMap.Node<K,V>
      {Entry<K,V> before, after;Entry(int hash, K key, V value, Node<K,V> next){super(hash, key, value, next);}
      }
      
    • LinkedHashMap 重写了 HashMap 中的 newNode()方法:

      Node<K,V> newNode(int hash, K key, V value, Node<K,V> e)
      {LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);linkNodeLast(p);return p;
      }TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next)
      {TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);linkNodeLast(p);return p;
      }
      

14.9 Set 接口下的实现类的源码剖析

14.9.1 Set 集合与 Map 集合的关系

Set 的内部实现其实是一个 Map,Set 中的元素,存储在 HashMap 的 key 中。即 HashSet 的内部实现是一个 HashMap,TreeSet 的内部实现是一个 TreeMap,LinkedHashSet 的内部实现是一个 LinkedHashMap。

14.9.2 HashSet 源码剖析

  • HashSet 源代码:

    // 构造器
    public HashSet()
    {map = new HashMap<>();
    }
    public HashSet(int initialCapacity, float loadFactor)
    {map = new HashMap<>(initialCapacity, loadFactor);
    }
    public HashSet(int initialCapacity)
    {map = new HashMap<>(initialCapacity);
    }// 这个构造器是给子类 LinkedHashSet 调用的
    HashSet(int initialCapacity, float loadFactor, boolean dummy)
    {map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }// add()方法:
    public boolean add(E e)
    {return map.put(e, PRESENT)==null;
    }private transient HashMap<E,Object> map;
    private static final Object PRESENT = new Object();// iterator()方法:
    public Iterator<E> iterator()
    {return map.keySet().iterator();
    }
    

14.9.3 LinkedHashSet 源码剖析

  • LinkedHashSet 源代码:

    //构造器
    public LinkedHashSet()
    {super(16, .75f, true);
    }
    public LinkedHashSet(int initialCapacity)
    {super(initialCapacity, .75f, true);    // 调用 HashSet 的某个构造器
    }
    public LinkedHashSet(int initialCapacity, float loadFactor)
    {super(initialCapacity, loadFactor, true);    // 调用 HashSet 的某个构造器
    }
    

14.9.4 TreeSet 源码剖析

  • TreeSet 源代码:

    public TreeSet()
    {this(new TreeMap<E,Object>());
    }
    TreeSet(NavigableMap<E,Object> m)
    {this.m = m;
    }private transient NavigableMap<E,Object> m;// add()方法:
    public boolean add(E e)
    {return m.put(e, PRESENT)==null;
    }private static final Object PRESENT = new Object();
    

14.10 HashMap 的相关问题

14.10.1 哈希算法的理解?

hash 算法是一种可以从任何数据中提取出其“指纹”的数据摘要算法,它将任意大小的数据映射到一个固定大小的序列上,这个序列被称为 hash code、数据摘要或者指纹。比较出名的 hash 算法有 MD5、SHA。hash 是具有唯一性且不可逆的,唯一性是指相同的“对象”产生的 hash code 永远是一样的。

14.10.2 Entry 中的 hash 属性为什么不直接使用 key 的 hashCode() 返回值?

  • 不管是 JDK1.7 还是 JDK1.8 中,都不是直接用 key 的 hashCode 值直接与 table.length-1 计算求下标的,而是先对 key 的 hashCode 值进行了一个运算,JDK1.7 和 JDK1.8 关于 hash()的实现代码不一样,但是不管怎么样都是为了提高 hash code 值与 (table.length-1)的按位与完的结果,尽量的均匀分布。

  • JDK1.7 中的源代码:

    final int hash(Object k)
    {int h = hashSeed;if (0 != h && k instanceof String){return sun.misc.Hashing.stringHash32((String) k);}h ^= k.hashCode();h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
    }
    
  • JDK1.8 中的源代码:

    static final int hash(Object key)
    {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
  • 虽然算法不同,但是思路都是将 hashCode 值的高位二进制与低位二进制值进行了异或,然高位二进制参与到 index 的计算中。

    • 为什么要 hashCode 值的二进制的高位参与到 index 计算呢?

      • 因为一个 HashMap 的 table 数组一般不会特别大,至少在不断扩容之前,那么 table.length-1 的大部分高位都是 0,直接用 hashCode 和 table.length-1 进行& 运算的话,就会导致总是只有最低的几位是有效的,那么就算你的 hashCode() 实现的再好也难以避免发生碰撞,这时让高位参与进来的意义就体现出来了。它对 hashcode 的低位添加了随机性并且混合了高位的部分特征,显著减少了碰撞冲突的发生。

14.10.3 HashMap 是如何决定某个 key-value 存在哪个桶的?

  • 因为 hash 值是一个整数,而数组的长度也是一个整数,有两种思路:
    ① hash 值 % table.length 会得到一个 [0,table.length - 1] 范围的值,正好是下标范围,但是用%运算效率没有位运算符&高。
    ②hash 值 & (table.length - 1),任何数 & (table.length - 1)的结果也一定在[0, table.length - 1]范围。

  • JDK1.7 中的源代码:

    static int indexFor(int h, int length)
    {// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";return h & (length-1);    // 此处 h 就是 hash
    }
    
  • JDK1.8 中的源代码:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
    {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)    // i = (n - 1) & hashtab[i] = newNode(hash, key, value, null);//......
    }
    

14.10.4 为什么要保持 table 数组一直是 2 的 n 次幂?

因为如果数组的长度为 2 的 n 次幂,那么 table.length-1 的二进制就是一个高位全是 0,低位全是 1 的数字,这样才能保证每一个下标位置都有机会被用到。

14.10.5 解决[index]冲突问题?

  • 虽然从设计 hashCode()到上面 HashMap 的 hash()函数,都尽量减少冲突,但是仍然存在两个不同的对象返回的 hashCode 值相同,或者 hashCode 值就算不同,通过 hash()函数计算后,得到的 index 也会存在大量的相同,因此 key 分布完全均匀的情况是不存在的。那么发生碰撞冲突时怎么办?

  • JDK1.8 之间使用:数组+链表的结构。

  • JDK1.8 之后使用:数组+链表/红黑树的结构。

  • 即 hash 相同或 hash&(table.lengt-1)的值相同,那么就存入同一个“桶”table[index]中,使用链表或红黑树连接起来。

14.10.6 为什么 JDK1.8 会出现红黑树和链表共存?

因为当冲突比较严重时,table[index]下面的链表就会很长,那么会导致查找效率大大降低,而如果此时选用二叉树可以大大提高查询效率。但是二叉树的结构又过于复杂,占用内存也较多,如果结点个数比较少的时候,那么选择链表反而更简单。所以会出现红黑树和链表共存。

14.10.7 加载因子的值大小有什么关系?

  • 如果太大,threshold 就会很大,那么如果冲突比较严重的话,就会导致 table[index]下面的结点个数很多,影响效率。
  • 如果太小,threshold 就会很小,那么数组扩容的频率就会提高,数组的使用率也会降低,那么会造成空间的浪费。

14.10.8 什么时候树化?什么时候反树化?

static final int TREEIFY_THRESHOLD = 8;    // 树化阈值
static final int UNTREEIFY_THRESHOLD = 6;    // 反树化阈值
static final int MIN_TREEIFY_CAPACITY = 64;    // 最小树化容量
  • 当某 table[index]下的链表的结点个数达到 8,并且 table.length>=64,那么如果新 Entry 对象还添加到该 table[index]中,那么就会将 table[index]的链表进行树化。
  • 当某 table[index]下的红黑树结点个数少于 6 个,此时,
    • 当继续删除 table[index]下的树结点,最后这个根结点的左右结点有 null,或根结点的左结点的左结点为 null,会反树化。
    • 当重新添加新的映射关系到 map 中,导致了 map 重新扩容了,这个时候如果 table[index]下面还是小于等于 6 的个数,那么会反树化。

14.10.9 key-value 中的 key 是否可以修改?

  • key-value 存储到 HashMap 中会存储 key 的 hash 值,这样就不用在每次查找时重新计算每一个 Entry 或 Node(TreeNode)的 hash 值了,因此如果已经 put 到 Map 中的 key-value,再修改 key 的属性,而这个属性又参与 hashcode 值的计算,那么会导致匹配不上。
  • 这个规则也同样适用于 LinkedHashMap、HashSet、LinkedHashSet、Hashtable 等所有散列存储结构的集合。

14.10.10 JDK1.7 中 HashMap 的循环链表是怎么回事?如何解决?

  • 避免 HashMap 发生死循环的常用解决方案:

    • 多线程环境下,使用线程安全的 ConcurrentHashMap 替代 HashMap,推荐。
    • 多线程环境下,使用 synchronized 或 Lock 加锁,但会影响性能,不推荐。
    • 多线程环境下,使用线程安全的 Hashtable 替代,性能低,不推荐。
  • HashMap 死循环只会发生在 JDK1.7 版本中,主要原因:头插法+链表+多线程并发+扩容。

  • 在 JDK1.8 中,HashMap 改用尾插法,解决了链表死循环的问题。

14.11 企业真题

  1. 链表和数组有什么区别?
    答:
    链表和数组是两种不同的数据结构,它们的区别如下:
    ① 存储方式:数组是一块连续的内存空间,而链表是通过指针将一系列节点连接起来的。
    ② 插入和删除操作:在数组中,如果要插入或删除一个元素,需要将其它元素向前或向后移动以保持连续性;而在链表中,只需要修改指针即可。
    ③ 访问元素的效率:在数组中,可以通过下标直接访问元素,因此访问的效率比较高;而在链表中,需要从头结点开始遍历才能访问到某个节点,因此访问的效率比较低。
    ④ 内存的分配和释放:数组在创建时需要一次性分配连续的内存空间,而链表则可以动态地分配内存空间,只需要在需要时申请新的节点即可。

    总的来说,链表适合处理需要频繁插入、删除操作的情况,而数组适合处理需要快速访问元素的情况。

  2. 栈是如何运行的?
    答:
    栈是一种后进先出(LIFO)的数据结构,它的运行方式是通过将数据压入栈顶或从栈顶弹出数据来实现。当数据被压入栈顶时,它会被放置在栈顶位置,并成为当前栈顶元素;当数据从栈顶弹出时,它会被移除,并将栈顶指针指向下一个元素。栈的基本操作包括压入(push)、弹出(pop)、获取栈顶元素(top)、判断栈是否为空(isEmpty)等。在程序中,栈通常用来存储临时数据、函数调用参数和返回地址等信息。由于栈的大小是有限制的,当栈满时,会抛出栈溢出(StackOverflow)异常。

  3. ArrayList 的默认大小是多少,以及扩容机制?
    答:
    Java 中,ArrayList 的默认大小是 10,即在创建 ArrayList 对象时,它的内部数组默认的初始大小为 10。如果在添加元素时,发现内部数组已满,则会触发扩容机制,将内部数组的大小翻倍,即扩容为原来的两倍。如果此时需要添加的元素个数超过了新的数组大小,则会再次触发扩容,以此类推。因此,ArrayList 的扩容机制是动态的,可以根据实际需要进行自适应扩容。

  4. ArrayList 的底层是怎么实现的?
    答:
    Java 中的 ArrayList 底层是通过数组实现的。它是一个动态数组,可以自动扩容。当数组的容量不足时,会自动创建一个新的数组,将原数组中的元素复制到新数组中,然后再将新元素添加到新数组中。

    ArrayList 的实现主要包含以下几个关键点:
    ① 数组:ArrayList 底层是一个 Object 类型的数组,用于存储元素。
    ② size:ArrayList 中元素的个数。
    ③ capacity:ArrayList 数组的容量,即数组的长度。
    ④ add()方法:添加元素时,首先判断数组是否已满,如果已满,则创建一个新数组,并将原数组中的元素复制到新数组中,然后再将新元素添加到新数组中。
    ⑤ remove()方法:删除元素时,将要删除的元素后面的所有元素向前移动一位,然后将最后一个元素设为 null。
    ⑥ indexOf()方法:查找元素时,遍历数组,如果找到了目标元素,则返回其下标,否则返回-1。
    ⑦ get()和 set()方法:获取和设置元素时,通过下标直接访问数组中的元素。

    总之,ArrayList 底层是一个数组,通过动态扩容和数组操作实现了添加、删除、查找和获取元素等功能。

  5. 在 ArrayList 中 remove 后面几个元素该怎么做?
    答:
    在 ArrayList 中删除元素时,删除后面的元素可以直接使用 ArrayList 类中提供的 remove 方法,该方法会自动将被删除元素后面的元素向前移动一个位置,填补删除元素的空缺。例如:

    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    list.add(5);
    list.remove(2);    // 删除索引为2的元素,即3
    System.out.println(list);    // 输出[1, 2, 4, 5]
    

    在这个例子中,删除索引为 2 的元素后,元素 4 和 5 会向前移动一个位置,填补删除元素的空缺。

  6. ArrayList 在 JDK1.7 和 JDK1.8 的区别?
    答:
    在 JDK1.7 中,ArrayList 是基于数组实现的,它在初始化时就会创建一个默认大小的数组,并在添加元素时动态扩容。这种实现方式被称为“饿汉式”,因为在创建对象时就已经预先分配了内存空间。

    而在 JDK1.8 中,ArrayList 的实现方式发生了改变。它不再在初始化时创建数组,而是在第一次添加元素时才会创建一个默认大小的数组。这种实现方式被称为“懒汉式”,因为它在需要的时候才会分配内存空间。

    这种改变带来了一些好处,例如减少了内存占用和初始化时间,但也带来了一些潜在的问题,例如在高并发环境下可能会出现线程安全问题。因此,在多线程环境下,最好使用线程安全的 ArrayList 实现类,例如 Vector 或 CopyOnWriteArrayList。

  7. 数组和 ArrayList 的区别?
    答:
    数组和 ArrayList 都可以用来存储一组数据,但是它们有以下区别:
    ① 数组长度固定,而 ArrayList 可以动态增长或缩小。
    ② 数组可以存储基本数据类型和对象,而 ArrayList 只能存储对象。
    ③ 数组可以直接访问任意元素,而 ArrayList 需要通过索引访问元素。
    ④ 数组的效率比 ArrayList 高,因为它不需要额外的空间来存储元素的索引。
    ⑤ ArrayList 提供了一些方便的方法来操作元素,如 add、remove、get、set 等,而数组需要手动实现这些方法。

    总的来说,ArrayList 是对数组的常见操作的封装,提供了更加方便和灵活的操作方式。但是在性能方面,数组仍然具有优势。

  8. 什么是线程安全的 List?
    答:
    在 Java 中,线程安全的 List 是指多个线程可以安全地同时访问该 List,而不会导致数据不一致或其他错误。其中,Vector 是线程安全的 List,因为它的方法都是同步的,多个线程同时访问不会导致数据不一致。而 ArrayList 是线程不安全的 List,如果多个线程同时访问,可能会导致数据不一致或其他错误。为了解决 ArrayList 的线程安全问题,可以使用同步机制进行处理,比如使用 Collections.synchronizedList 方法将其转换为线程安全的 List。另外,Java 还提供了一些更高效的线程安全的集合类,比如 JUC 包中的 ConcurrentHashMap。

  9. 说说 HahMap 底层实现?
    答:
    HashMap 底层实现主要是基于哈希表(Hash Table)和链表(LinkedList)的组合实现。

    具体来说,HashMap 中的元素存储在一个数组中,每个数组元素称为桶(Bucket),每个桶存储的是一个链表的头节点。当需要添加一个元素时,首先根据该元素的 hashCode()方法计算出该元素在数组中的位置,如果该位置已经有元素存在,则需要遍历该位置上的链表,找到最后一个节点,将新元素添加到该链表的尾部;如果该位置上没有元素存在,则直接将新元素添加到该位置上。

    当需要查找一个元素时,首先根据该元素的 hashCode()方法计算出该元素在数组中的位置,然后遍历该位置上的链表,查找是否存在该元素。如果存在,则返回该元素;如果不存在,则返回 null。

    当需要删除一个元素时,首先根据该元素的 hashCode()方法计算出该元素在数组中的位置,然后遍历该位置上的链表,查找是否存在该元素。如果存在,则将该元素从链表中删除;如果不存在,则不做任何操作。

    需要注意的是,当链表中的元素数量过多时,会影响 HashMap 的性能,因为需要遍历更长的链表才能找到目标元素。因此,当链表中的元素数量超过一定阈值时,会将该链表转化为红黑树(Tree),以提高查找效率。

  10. HashMap 初始值 16,临界值 12 是怎么算的?
    答:
    HashMap 的初始容量为 16,这是因为 HashMap 的底层实现是基于数组的,而 16 是一个比较合适的数值,可以保证在大多数情况下,存储的元素可以均匀的分布在数组中,从而达到较好的性能。

    临界值 12 是根据 HashMap 的负载因子(loadFactor)计算得出的。负载因子是 HashMap 中的一个参数,表示当数组中元素的数量达到总容量的百分之多少时,就需要将数组扩容。HashMap 的默认负载因子为 0.75,也就是说当元素数量达到总容量的 75%时,就需要将数组扩容。

    因此,当 HashMap 的容量为 16 时,它的临界值为 16 * 0.75 = 12。当元素数量达到 12 时,就需要将数组扩容,以保证 HashMap 的性能。

  11. HashMap 长度为什么是 2 的幂次方?
    答:
    HashMap 的长度为 2 的幂次方是为了方便计算元素的索引值,即将元素的哈希值与 HashMap 的长度进行取模运算,得到的结果就是元素在 HashMap 中的索引位置。如果 HashMap 的长度不是 2 的幂次方,那么在进行取模运算时就需要进行复杂的除法运算,降低了效率。而如果 HashMap 的长度是 2 的幂次方,那么取模运算可以转化为位运算,即对 HashMap 的长度进行按位与操作,这样可以大大提高计算效率。

  12. HashMap 怎么计算哈希值和索引?扩容机制?怎么解决 hash 冲突?
    答:
    ① HashMap 计算哈希值和索引:
    HashMap 的哈希值是通过 key 的 hashCode()方法计算得出的,然后通过哈希函数计算出在数组中的索引位置。哈希函数的计算方法是将哈希值与数组长度进行取模运算,得到的余数就是 key 在数组中的索引位置。

    ② HashMap 扩容机制:
    当 HashMap 中元素的数量超过了负载因子乘以数组长度时,就会发生扩容操作。扩容操作会重新计算每个元素的哈希值和索引位置,并将元素重新放到新的数组中。

    扩容时,数组的长度会翻倍,同时原有的元素也会被重新分配到新的索引位置上。这个过程需要重新计算哈希值和索引位置,因此会比较耗时。

    ③ HashMap 解决 hash 冲突的方法:
    当不同的 key 计算出的哈希值相同时,就会发生哈希冲突。HashMap 使用链表法解决哈希冲突,即在发生哈希冲突时,将新元素插入到链表的末尾。

    如果链表的长度超过了阈值(默认为 8),就会将链表转换为红黑树,以提高查询效率。当红黑树的节点数小于等于 6 时,会将红黑树转换回链表。

  13. HashMap 底层是数组 + 链表,有数组很快了,为什么加链表?
    答:
    HashMap 加链表是为了解决哈希冲突问题。当不同的键值对经过哈希函数计算后,得到的哈希值相同,即发生了哈希冲突。如果直接将键值对存储在同一个数组位置上,会导致数据覆盖,因此需要在同一个数组位置上使用链表来存储多个键值对。当发生哈希冲突时,HashMap 会将新的键值对添加到链表的末尾,而不是覆盖原有的键值对。这样,当需要访问某个键值对时,HashMap 只需要遍历链表即可,不需要遍历整个数组,提高了访问效率。

  14. HashMap 为什么长度达到一定的长度要转化为红黑树?
    答:
    在 Java 中,HashMap 是一种基于哈希表实现的数据结构。当 HashMap 中的元素数量达到一定的数量时,哈希表的性能会受到影响,因为哈希表的冲突会变得更加频繁,查找、插入、删除等操作的时间复杂度也会变高。

    为了解决这个问题,Java 中的 HashMap 引入了红黑树,当哈希表中的某个桶中的元素数量达到了一个阈值(默认为 8),且该桶中元素的数量大于等于 64 时,就会将该桶中的元素转化为红黑树。

    红黑树的插入、查找、删除等操作的时间复杂度都是 O(logn),比单向链表的 O(n)效率高得多,因此使用红黑树可以提高 HashMap 的性能。

    值得注意的是,当哈希表中的元素数量减少时,红黑树会被转化为普通的链表,这是因为链表在元素数量较少时的性能更优。

  15. HashMap 什么时候扩充为红黑树,什么时候又返回到链表?
    答:
    在 Java 8 中,当 HashMap 的某个桶中元素数量超过 TREEIFY_THRESHOLD(默认为 8)时,该桶中的元素将被转换为红黑树,以提高查找效率。当桶中元素数量小于 UNTREEIFY_THRESHOLD(默认为 6)时,红黑树将被转换回链表,以减少空间占用。

  16. 在 JDK1.8 中,HashMap 的数据结构与 1.7 相比有什么变化,这些变化的好处在哪里?
    答:
    在 JDK1.8 中,HashMap 的数据结构发生了一些变化,具体如下:
    ① 初始化方式:在 JDK1.8 中,当我们创建 HashMap 实例时,底层并没有初始化 table 数组。当首次添加(key,value)时,进行判断,如果发现 table 尚未初始化,则对数组进行初始化。
    ② 内部类:在 JDK1.8 中,HashMap 底层定义了 Node 内部类,替换了 JDK1.7 中的 Entry 内部类。这意味着我们创建的数组是 Node[]。
    ③ 插入方式:在 JDK1.8 中,如果当前的(key,value)经过一系列判断之后,可以添加到当前的数组角标 i 中。如果此时角标 i 位置上有元素,旧的元素会指向新的(key,value)元素(尾插法),而在 JDK1.7 中是将新的(key,value)指向已有的旧的元素(头插法)。
    ④ 数据结构:在 JDK1.7 中,HashMap 使用的是数组+单向链表的数据结构。而在 JDK1.8 中,除了数组和单向链表,还使用了红黑树。当数组索引 i 位置上的元素的个数达到 8,并且数组的长度达到 64 时,会将此索引 i 位置上的多个元素改为使用红黑树的结构进行存储。使用红黑树的时间复杂度为 O(logn),比单向链表的时间复杂度 O(n)更优,因此可以提高 HashMap 的性能。当使用红黑树的索引 i 位置上的元素的个数低于 6 时,就会将红黑树结构退化为单向链表。

    这些变化让 HashMap 在 JDK1.8 中更加高效、可靠,提高了其性能和扩展性。

  17. HashMap 的 get()方法的原理?
    答:
    Java 中,HashMap 的 get()方法的原理如下:
    ① 首先,HashMap 会根据传入的 key 计算出其对应的 hash 值。
    ② 然后,HashMap 会根据 hash 值找到对应的桶(bucket)。
    ③ 在桶中,HashMap 会遍历所有的键值对,找到与传入的 key 相等的键值对。
    ④ 如果找到了对应的键值对,HashMap 就会返回该键值对的 value 值。
    ⑤ 如果找不到对应的键值对,HashMap 就会返回 null。

    需要注意的是,当 HashMap 中的键值对数量很大时,遍历桶中的键值对可能会比较耗时,因此在实际使用中,应尽量保证 HashMap 中的键值对数量不要过大,以提高 get()方法的性能。

  18. hashcode()和 equals()的区别?
    答:
    hashcode()和 equals()都是 Object 类中定义的方法,用于判断对象是否相等。但是它们的作用不同:
    ① hashcode()方法用于获取对象的哈希码,返回一个 int 类型的值。哈希码是根据对象的内部信息计算出来的一个数值,可以用于快速查找和比较对象。如果两个对象的 hashcode()值相等,不一定表示它们相等,但如果两个对象不相等,它们的 hashcode()值一定不相等。
    ② equals()方法用于比较两个对象是否相等,返回一个 boolean 类型的值。默认情况下,equals()方法比较的是对象的引用地址,即两个对象的引用地址相同才认为它们相等。但是我们可以重写 equals()方法,根据对象的内部信息来判断两个对象是否相等。如果两个对象相等,它们的 hashcode()值一定相等;如果两个对象的 hashcode()值相等,它们不一定相等,需要再调用 equals()方法进行比较。

    因此,hashcode()和 equals()方法是互相关联的,如果重写 equals()方法,也需要重写 hashcode()方法,保证两个方法的一致性。

  19. hashCode() 与 equals() 生成算法、方法怎么重写?
    答:
    在 Java 中,hashCode()和 equals()方法都是用来比较对象的,其中 hashCode()方法用来获取对象的哈希码,equals()方法用来比较两个对象是否相等。

    重写 hashCode()方法需要注意以下几点:
    ① hashCode()方法的返回值应该是一个整数,通常使用对象的属性进行计算,保证相同的对象返回相同的 hashCode()值。
    ② hashCode()方法的计算不能依赖于对象的内存地址,因为在不同的 JVM 中,同一对象的内存地址可能不同。
    ③ hashCode()方法的计算应该尽量简单,避免过于复杂的计算,影响性能。

    重写 equals()方法需要注意以下几点:
    ① equals()方法应该比较对象的属性,而不是比较对象的内存地址。
    ② equals()方法应该满足自反性、对称性、传递性和一致性。
    ③ equals()方法应该检查参数是否为 null,并且检查参数类型是否相同。

    以下是一个示例:

    public class Person
    {private String name;private int age;// 构造函数、getter、setter@Overridepublic int hashCode(){final int prime = 31;int result = 1;result = prime * result + age;result = prime * result + ((name == null) ? 0 : name.hashCode());return result;}@Overridepublic boolean equals(Object obj){if (this == obj)return true;if (obj == null)return false;if (getClass() != obj.getClass())return false;Person other = (Person) obj;if (age != other.age)return false;if (name == null){if (other.name != null)return false;}else if (!name.equals(other.name))return false;return true;}
    }
    

    在这个示例中,hashCode()方法使用了对象的 name 和 age 属性进行计算,保证相同的对象返回相同的 hashCode()值。equals()方法比较了对象的 name 和 age 属性,满足了自反性、对称性、传递性和一致性。

  20. 说一下 equals 和 == 的区别,然后问 equals 相等 hash 值一定相等吗?hash 值相等 equals 一定相等吗?
    答:
    equals 方法是用来比较两个对象的内容是否相等,而 == 操作符是用来比较两个对象的引用是否相等。

    在 Java 中,equals 相等的两个对象的 hash 值不一定相等,因为 hash 值是根据对象的内容计算得出的,而不是根据对象的引用。而 hash 值相等的两个对象的 equals 不一定相等,因为不同的对象可能具有相同的 hash 值,这种情况称为哈希冲突。

    因此,在使用哈希表等数据结构时,需要同时实现 equals 和 hashCode 方法,并保证它们的一致性,即如果两个对象的 equals 相等,则它们的 hashCode 也相等。这样可以确保在哈希表中查找对象时能够正确地匹配对象。

  21. HashSet 存放数据的方式?
    答:
    Java 中的 HashSet 使用哈希表(Hash Table)来存储数据。具体地说,HashSet 内部是一个 HashMap 对象,其中的键(key)就是存放的数据,值(value)则是一个固定的 Object 对象。当向 HashSet 中添加元素时,实际上是向 HashMap 中添加键值对,键为存放的数据,值为一个固定的 Object 对象。在查询元素时,HashSet 会根据哈希值(Hash Code)来快速定位元素,从而实现高效的查找。

  22. Set 是如何实现元素的唯一性?
    答:
    Java 中的 Set 是通过 hashCode()和 equals()方法来实现元素的唯一性的。

    当向 Set 中添加一个元素时,首先会调用该元素的 hashCode()方法,获取该元素的哈希值。Set 会将该元素的哈希值与其他元素的哈希值进行比较,如果存在相同的哈希值,则继续调用 equals()方法进行比较。如果 equals()方法返回 true,则说明该元素已经存在于 Set 中,不会被添加;否则将该元素添加到 Set 中。

    因此,为了保证元素的唯一性,需要重写该元素的 hashCode()和 equals()方法,使它们能够正确地计算元素的哈希值和比较元素的相等性。

  23. 用哪两种方式来实现集合的排序?
    答:
    Java 中可以使用以下两种方式来实现集合的排序:
    ① 实现 Comparable 接口

    在集合元素类中实现 Comparable 接口,并重写 compareTo 方法,实现元素的比较规则。然后使用 Collections.sort 方法进行排序。

    示例代码:

    public class Person implements Comparable<Person>
    {private String name;private int age;public Person(String name, int age){this.name = name;this.age = age;}public String getName(){return name;}public int getAge(){return age;}@Overridepublic int compareTo(Person o){return Integer.compare(this.age, o.age);}
    }public static void main(String[] args)
    {List<Person> list = new ArrayList<>();list.add(new Person("Tom", 20));list.add(new Person("Jerry", 18));list.add(new Person("Alice", 22));Collections.sort(list);System.out.println(list);
    }
    

    输出结果:

    [Person{name='Jerry', age=18}, Person{name='Tom', age=20}, Person{name='Alice', age=22}]
    

    ② 实现 Comparator 接口

    创建一个实现 Comparator 接口的比较器类,并重写 compare 方法,实现元素的比较规则。然后使用 Collections.sort 方法并传入比较器对象进行排序。

    示例代码:

    public class PersonComparator implements Comparator<Person>
    {@Overridepublic int compare(Person o1, Person o2){return Integer.compare(o1.getAge(), o2.getAge());}
    }public static void main(String[] args)
    {List<Person> list = new ArrayList<>();list.add(new Person("Tom", 20));list.add(new Person("Jerry", 18));list.add(new Person("Alice", 22));Collections.sort(list, new PersonComparator());System.out.println(list);
    }
    

    输出结果:

    [Person{name='Jerry', age=18}, Person{name='Tom', age=20}, Person{name='Alice', age=22}]
    

    以上两种方式都可以实现集合的排序,但实现 Comparable 接口的方式更加方便,因为不需要创建额外的比较器类。但是,如果需要实现多种比较规则,或者对已有的类进行排序,就需要使用实现 Comparator 接口的方式。

第 15 章 File 类与 IO 流

15.1 File 类

15.1.1 File 类的概述

File 类及本章下的各种流,都定义在 java.io 包下。

一个 File 对象代表硬盘或网络中可能存在的一个文件或者文件目录,与平台无关。

File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入、输出流。

  • File 对象可以作为参数传递给流的构造器。

想要在 Java 程序中表示一个真实存在的文件或目录,那么必须有一个 File 对象,但是 Java 程序中的一个 File 对象,可能没有一个真实存在的文件或目录。

15.1.2 File 类的构造器

  • public File(String pathname) :以 pathname 为路径创建 File 对象,可以是绝对路径或者相对路径,如果 pathname 是相对路径,则默认的当前路径在系统属性 user.dir 中存储。

  • public File(String parent, String child) :以 parent 为父路径,child 为子路径创建 File 对象。

  • public File(File parent, String child) :根据一个父 File 对象和子文件路径创建 File 对象。

  • 关于路径:

    • 绝对路径:从盘符开始的路径,这是一个完整的路径。
    • 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。
      • IDEA 中,main 中的文件的相对路径,是相对于"当前工程"。
      • IDEA 中,单元测试方法中的文件的相对路径,是相对于"当前 module"。

15.1.3 File 类的常用方法

获取文件和目录基本信息:

  • public String getName() :获取名称。
  • public String getPath() :获取路径。
  • public String getAbsolutePath():获取绝对路径。
  • public File getAbsoluteFile():获取绝对路径表示的文件。
  • public String getParent():获取上层文件目录路径。若无,返回 null。
  • public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
  • public long lastModified() :获取最后一次的修改时间,毫秒值。

列出目录的下一级:

  • public String[] list() :返回一个 String 数组,表示该 File 目录中的所有子文件或目录。
  • public File[] listFiles() :返回一个 File 数组,表示该 File 目录中的所有的子文件或目
    录。

File 类的重命名功能:

  • public boolean renameTo(File dest):把文件重命名为指定的文件路径。

判断功能的方法:

  • public boolean exists() :此 File 表示的文件或目录是否实际存在。
  • public boolean isDirectory() :此 File 表示的是否为目录。
  • public boolean isFile() :此 File 表示的是否为文件。
  • public boolean canRead() :判断是否可读。
  • public boolean canWrite() :判断是否可写。
  • public boolean isHidden() :判断是否隐藏。

创建、删除功能:

  • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回 false。
  • public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
  • public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创建。
  • public boolean delete() :删除文件或者文件夹 删除注意事项:
    ① Java 中的删除不走回收站。
    ② 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。

15.2 IO 流

15.2.1 IO 流的概述

Java 程序中,对于数据的输入、输出操作以“流(stream)” 的方式进行,可以看做是一种数据的流动。

I/O 流中的 I/O 是 Input/Output 的缩写, I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

  • 输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
  • 输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。

15.2.2 IO 流的分类

  • 按数据的流向不同分为:输入流和输出流。

    • 输入流 :把数据从其他设备上读取到内存中的流。以 InputStream、Reader 结尾。
    • 输出流 :把数据从内存 中写出到其他设备上的流。以 OutputStream、Writer 结尾。
  • 按操作数据单位的不同分为:字节流(8bit)和字符流(16bit)。
    • 字节流 :以字节为单位,读写数据的流。以 InputStream、OutputStream 结尾。
    • 字符流 :以字符为单位,读写数据的流。以 Reader、Writer 结尾。
  • 根据 IO 流的角色不同分为:节点流和处理流。
    • 节点流:直接从数据源或目的地读写数据。
    • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

15.2.3 IO 流的 API

IO 的 4 个抽象基类:InputStream 、 OutputStream 、 Reader 、 Writer。

常用的节点流:

  • 文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter
  • 字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、
    CharArrayReader、CharArrayWriter - 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。

常用处理流:

  • 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、
    BufferedWriter - 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。
  • 转换流:InputStreamReader、OutputStreamReader
    • 作用:实现字节流和字符流之间的转换。
  • 对象流:ObjectInputStream、ObjectOutputStream
    • 作用:提供直接读写 Java 对象功能。

15.3 节点流之一:FileReader、FileWriter

15.3.1 字符输入流:Reader

  • java.io.Reader 抽象类是表示用于读取字符流的所有类的父类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
  • public int read(): 从输入流读取一个字符。 虽然读取了一个字符,但是会自动提升为 int 类型。返回该字符的 Unicode 编码值。如果已经到达流末尾了,则返回 -1。
  • public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中 。每次最多读取 cbuf.length 个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(char[] cbuf,int off,int len):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中,从 cbuf[off]开始的位置存储。每次最多读取 len 个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public void close() :关闭此流并释放与此流相关联的任何系统资源。
  • 注意:当完成流的操作时,必须调用 close()方法,释放系统资源,否则会造成内存泄漏。

15.3.2 字符输出流:Writer

  • java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
  • public void write(int c) :写出单个字符。
  • public void write(char[] cbuf):写出字符数组。
  • public void write(char[] cbuf, int off, int len):写出字符数组的某一部分。off:数组的开始索引;len:写出的字符个数。
  • public void write(String str):写出字符串。
  • public void write(String str, int off, int len) :写出字符串的某一部分。off:字符串的开始索引;len:写出的字符个数。
  • public void flush():刷新该流的缓冲。
  • public void close() :关闭此流。
  • 注意:当完成流的操作时,必须调用 close()方法,释放系统资源,否则会造成内存泄漏。

15.3.3 文件字符输入流:FileReader

  • java.io.FileReader 类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。
  • FileReader(File file): 创建一个新的 FileReader ,给定要读取的 File 对象。
  • FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文件的名称。

15.3.4 文件字符输出流:FileWriter

  • java.io.FileWriter 类用于写出字符到文件,构造时使用系统默认的字符编码和默认字节缓冲区。
  • FileWriter(File file): 创建一个新的 FileWriter,给定要读取的 File 对象。
  • FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件的名称。
  • FileWriter(File file,boolean append): 创建一个新的 FileWriter,指明是否在现有文件末尾追加内容。

15.3.5 关于 flush(刷新)

  • 因为内置缓冲区的原因,如果 FileWriter 不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要 flush() 方法了。
  • flush() :刷新缓冲区,流对象可以继续使用。
  • close():先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。
  • 注意:即便是 flush()方法写出了数据,操作的最后还是要调用 close 方法,释放系统资源。

15.4 节点流之二:FileInputStream、FileOutputStream

15.4.1 字节输入流:InputStream

  • java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
  • public int read(): 从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为 int 类型。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b 中 。每次最多读取 b.length 个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(byte[] b,int off,int len):从输入流中读取一些字节数,并将它们存储到字节数组 b 中,从 b[off]开始存储,每次最多读取 len 个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。
  • 注意:close()方法,当完成流的操作时,必须调用此方法,释放系统资源。

15.4.2 字节输出流:OutputStream

  • java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
  • public void write(int b) :将指定的字节输出流。虽然参数为 int 类型四个字节,但是只会保留一个字节的信息写出。
  • public void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len) :从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。
  • public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • 注意:close()方法,当完成流的操作时,必须调用此方法,释放系统资源。

15.4.3 文件字节输入流:FileInputStream

  • java.io.FileInputStream 类是文件输入流,从文件中读取字节。
  • FileInputStream(File file): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File 对象 file 命名。
  • FileInputStream(String name): 通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名。

15.4.4 文件字节输出流: FileOutputStream

  • java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。
  • public FileOutputStream(File file):创建文件输出流,写出由指定的 File 对象表示的文件。
  • public FileOutputStream(String name): 创建文件输出流,指定的名称为写出文件。
  • public FileOutputStream(File file, boolean append): 创建文件输出流,指明是否在现有文件末尾追加内容。

15.5 读写数据的过程

  • 步骤 1:创建 File 类的对象,作为读取或写出数据的端点。

  • 步骤 2:创建相关的流的对象。

  • 步骤 3:读取、写出数据的过程。

  • 步骤 4:关闭流资源。

  • 读取数据的代码示例:

    import java.io.*;public class ReadFileExample
    {public static void main(String[] args){try {// 步骤1:创建File类的对象File file = new File("example.txt");// 步骤2:创建相关的流的对象FileReader reader = new FileReader(file);BufferedReader bufferedReader = new BufferedReader(reader);// 步骤3:读取数据的过程String line;while ((line = bufferedReader.readLine()) != null){System.out.println(line);}// 步骤4:关闭流资源bufferedReader.close();reader.close();}catch (IOException e){e.printStackTrace();}}
    }
    
  • 写出数据的代码示例:

    import java.io.*;public class WriteFileExample
    {public static void main(String[] args){try {// 步骤1:创建File类的对象File file = new File("example.txt");// 步骤2:创建相关的流的对象FileWriter writer = new FileWriter(file);BufferedWriter bufferedWriter = new BufferedWriter(writer);// 步骤3:写出数据的过程bufferedWriter.write("Hello, world!");// 步骤4:关闭流资源bufferedWriter.close();writer.close();}catch (IOException e){e.printStackTrace();}}
    }
    

15.6 处理流之一:缓冲流

为了提高数据读写的速度,Java API 提供了带缓冲功能的流类:缓冲流。

缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

  • 字节缓冲流:BufferedInputStream,BufferedOutputStream
  • 字符缓冲流:BufferedReader,BufferedWriter

缓冲流的基本原理:在创建流对象时,内部会创建一个缓冲区数组(缺省使用 8192
个字节(8Kb)的缓冲区),通过缓冲区读写,减少系统 IO 次数,从而提高读写的效
率。

构造器:

  • public BufferedInputStream(InputStream in) :创建一个 新的字节型的缓冲输入流。
  • public BufferedOutputStream(OutputStream out): 创建一个新的字节型的缓冲输出流。

字符缓冲流特有方法:

  • BufferedReader:public String readLine(): 读一行文字。
  • BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符号。

15.7 处理流之二:转换流

15.7.1 InputStreamReader 与 OutputStreamWriter

  • InputStreamReader

    • 转换流 java.io.InputStreamReader,是 Reader 的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

    • 构造器:

      • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
      • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。
  • OutputStreamWriter

    • 转换流 java.io.OutputStreamWriter ,是 Writer 的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

    • 构造器:

      • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
      • OutputStreamWriter(OutputStream in,String charsetName): 创建一个指定字符集的字符流。

15.7.2 字符编码和字符集

  • 编码与解码 :
    计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。

  • 字符编码(Character Encoding) : 就是一套自然语言的字符与二进制数之间的对应规则。

  • 编码表:生活中文字和计算机中二进制的对应规则。

  • 乱码的情况:按照 A 规则存储,同样按照 A 规则解析,那么就能显示正确的文本符号。反之,按照 A 规则存储,再按照 B 规则解析,就会导致乱码现象。

  • 编码:字符——>字节

  • 解码:字节——>字符

  • 字符集 Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

  • 计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有 ASCII 字符集、GBK 字符集、Unicode 字符集等。

15.8 处理流之三:对象流

15.8.1 对象流:ObjectOutputStream、ObjectInputStream

  • ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。通过在流中使用文件可以实现 Java 各种基本数据类型的数据以及对象的持久存储。
  • ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写出的基本数据类型的数据和对象进行读入操作,保存在内存中。
  • 说明:对象流的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。

15.8.2 对象流 API

  • ObjectOutputStream 中的构造器:

    • public ObjectOutputStream(OutputStream out): 创建一个指定的 ObjectOutputStream。
  • ObjectOutputStream 中的方法:

    • public void writeBoolean(boolean val):写出一个 boolean 值。
    • public void writeByte(int val):写出一个 8 位字节。
    • public void writeShort(int val):写出一个 16 位的 short 值。
    • public void writeChar(int val):写出一个 16 位的 char 值。
    • public void writeInt(int val):写出一个 32 位的 int 值。
    • public void writeLong(long val):写出一个 64 位的 long 值。
    • public void writeFloat(float val):写出一个 32 位的 float 值。
    • public void writeDouble(double val):写出一个 64 位的 double 值
    • public void writeUTF(String str):将表示长度信息的两个字节写入输出流,后跟字符串 s 中每个字符的 UTF-8 修改版表示形式。根据字符的值,将字符串 s 中每个字符转换成一个字节、两个字节或三个字节的字节组。注意,将 String 作为基本数据写入流中与将它作为 Object 写入流中明显不同。 如果 s 为 null,则抛出 NullPointerException。
    • public void writeObject(Object obj):写出一个 obj 对象。
    • public void close() :关闭此输出流并释放与此流相关联的任何系统资源。
  • ObjectInputStream 中的构造器:

    • public ObjectInputStream(InputStream in): 创建一个指定的 ObjectInputStream。
  • ObjectInputStream 中的方法:

    • public boolean readBoolean():读取一个 boolean 值。
    • public byte readByte():读取一个 8 位的字节。
    • public short readShort():读取一个 16 位的 short 值。
    • public char readChar():读取一个 16 位的 char 值。
    • public int readInt():读取一个 32 位的 int 值。
    • public long readLong():读取一个 64 位的 long 值。
    • public float readFloat():读取一个 32 位的 float 值。
    • public double readDouble():读取一个 64 位的 double 值。
    • public String readUTF():读取 UTF-8 修改版格式的 String。
    • public void readObject(Object obj):读入一个 obj 对象。
    • public void close() :关闭此输入流并释放与此流相关联的任何系统资源。

15.8.3 认识对象序列化机制

  • 何为对象序列化机制?

    • 对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
  • 序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

  • 反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。

  • 序列化机制的重要性

    • 序列化是 RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
    • 序列化的好处,在于可将任何实现了 Serializable 接口的对象转化为字节数据,使其在保存和传输时可被还原。
  • 实现原理

    • 序列化:用 ObjectOutputStream 类保存基本类型数据或对象的机制。方法为:

      • public final void writeObject (Object obj) : 将指定的对象写出。
    • 反序列化:用 ObjectInputStream 类读取基本类型数据或对象的机制。方法为:
      • public final Object readObject () : 读取一个对象。
  • 如何实现序列化机制

    • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现 java.io.Serializable 接口。Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序
      列化或反序列化,会抛出 NotSerializableException。

    • 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现 Serializable 接口。

    • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。

    • 静态(static)变量的值不会序列化。因为静态变量的值不属于某个对象。

    • 代码示例:

      import java.io.*;public class Person implements Serializable
      {private static final long serialVersionUID = 1L;private String name;private int age;public Person(String name, int age){this.name = name;this.age = age;}public String getName(){return name;}public int getAge(){return age;}public static void main(String[] args){Person person = new Person("Tom", 18);try {// 将对象序列化为字节流FileOutputStream fos = new FileOutputStream("person.ser");ObjectOutputStream oos = new ObjectOutputStream(fos);oos.writeObject(person);oos.close();fos.close();// 将字节流反序列化为对象FileInputStream fis = new FileInputStream("person.ser");ObjectInputStream ois = new ObjectInputStream(fis);Person p = (Person) ois.readObject();ois.close();fis.close();System.out.println(p.getName() + " " + p.getAge());}catch (IOException | ClassNotFoundException e){e.printStackTrace();}}
      }
      

15.8.4 反序列化失败问题

  • 问题 1:

    • 对于 JVM 可以反序列化对象,它必须是能够找到 class 文件的类。如果找不到该类的 class 文件,则抛出一个 ClassNotFoundException 异常。
  • 问题 2:

    • 当 JVM 反序列化对象时,能找到 class 文件,但是 class 文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:

      • 该类的序列版本号与从流中读取的类描述符的版本号不匹配。
      • 该类包含未知数据类型。
    • 解决办法:

      • Serializable 接口给需要序列化的类,提供了一个序列版本号:serialVersionUID 。凡是实现 Serializable 接口的类都应该有一个表示序列化
        版本标识符的静态变量:

        static final long serialVersionUID = 234242343243L;    // 值随意指定即可
        
      • serialVersionUID 用来表明类的不同版本间的兼容性。简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。

      • 如果类没有显示定义这个静态常量,它的值是 Java 运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。因此,建议显式声明。

      • 如果声明了 serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已。

15.9 其它流

15.9.1 数据流:DataInputStream 、DataOutputStream

  • 如果需要将内存中定义的变量(包括基本数据类型或引用数据类型)保存在文件中,那怎么办呢?

  • Java 提供了数据流和对象流来处理这些类型的数据:

    • 数据流:DataOutputStream、DataInputStream

      • DataOutputStream:允许应用程序将基本数据类型、String 类型的变量写入输出流中。
      • DataInputStream:允许应用程序以与机器无关的方式从底层输入流中读取基本数据类型、String 类型的变量。
  • 数据流的弊端:只支持 Java 基本数据类型和字符串的读写,而不支持其它 Java 对象的类型。而 ObjectOutputStream 和 ObjectInputStream 既支持 Java 基本数据类型的数据读写,又支持 Java 对象的读写。

15.9.2 标准的输入流、标准的输出流:System.in 、System.out

  • System.in 和 System.out 分别代表了系统标准的输入和输出设备。
  • 默认输入设备是:键盘,输出设备是:显示器。
  • System.in 的类型是 InputStream。
  • System.out 的类型是 PrintStream,其是 OutputStream 的子类 FilterOutputStream 的子类。
  • 重定向:通过 System 类的 setIn,setOut 方法对默认设备进行改变。
    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

15.9.3 打印流:PrintStream、PrintWriter

  • 实现将基本数据类型的数据格式转化为字符串输出。
  • 打印流:PrintStream 和 PrintWriter。
    • 提供了一系列重载的 print()和 println()方法,用于多种数据类型的输出。
    • PrintStream 和 PrintWriter 的输出不会抛出 IOException 异常。
    • PrintStream 和 PrintWriter 有自动 flush 功能。
    • PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
    • System.out 返回的是 PrintStream 的实例
  • 构造器:
    • PrintStream(File file) :创建具有指定文件且不带自动行刷新的新打印流。
    • PrintStream(File file, String csn):创建具有指定文件名称和字符集且不带自动行刷新的新打印流。
    • PrintStream(OutputStream out) :创建新的打印流。
    • PrintStream(OutputStream out, boolean autoFlush):创建新的打印流。autoFlush 如果为 true,则每当写入 byte 数组、调用其中一个 println 方法或写入换行符或字节 (‘\n’) 时都会刷新输出缓冲区。
    • PrintStream(OutputStream out, boolean autoFlush, String encoding) :创建新的打印流。
    • PrintStream(String fileName):创建具有指定文件名称且不带自动行刷新的新打印流。
    • PrintStream(String fileName, String csn) :创建具有指定文件名称和字符集且不带自动行刷新的新打印流。

15.9.4 Scanner 类

  • 构造方法:

    • Scanner(File source) :构造一个新的 Scanner,它生成的值是从指定文件扫描的。
    • Scanner(File source, String charsetName) :构造一个新的 Scanner,它生成的值是从指定文件扫描的。
    • Scanner(InputStream source) :构造一个新的 Scanner,它生成的值是从指定的输入流扫描的。
    • Scanner(InputStream source, String charsetName) :构造一个新的 Scanner,它生成的值是从指定的输入流扫描的。
  • 常用方法:

    • boolean hasNextXxx(): 如果通过使用 nextXxx()方法,此扫描器输入信息中的下一个标记可以解释为默认基数中的一个 Xxx 值,则返回 true。
    • Xxx nextXxx(): 将输入信息的下一个标记扫描为一个 Xxx

15.10 企业真题

  1. 谈谈 Java IO 里面的常用类,字节流,字符流?
    答:
    Java IO 里面的常用类包括:

    • InputStream/OutputStream:字节流的基本类,用于读写字节数据。
    • Reader/Writer:字符流的基本类,用于读写字符数据。
    • FileInputStream/FileOutputStream:用于读写文件的字节流类。
    • FileReader/FileWriter:用于读写文件的字符流类。
    • ByteArrayInputStream/ByteArrayOutputStream:用于读写字节数组的流类。
    • CharArrayReader/CharArrayWriter:用于读写字符数组的流类。
    • BufferedInputStream/BufferedOutputStream:用于带缓冲的读写字节数据的流类。
    • BufferedReader/BufferedWriter:用于带缓冲的读写字符数据的流类。
    • DataInputStream/DataOutputStream:用于读写基本数据类型的字节流类。
    • InputStreamReader/OutputStreamWriter:用于将字节流转换成字符流的类。
    • ObjectInputStream/ObjectOutputStream:用于读写 Java 对象的字节流类。
    • PipedInputStream/PipedOutputStream:用于在线程之间传递数据的流类。
    • PrintStream/PrintWriter:用于将数据打印到输出流的类。

    总的来说,Java IO 提供了丰富的流类来满足各种读写需求,其中字节流和字符流是最基本的两种类型,字节流适用于读写二进制数据,而字符流适用于读写文本数据。在实际应用中需要根据具体的需求选择合适的流类。

  2. Java 中有几种类型的流?JDK 为每种类型的流提供一些抽象类以供继承,请说出他们分别是哪些类?
    答:
    Java 中有四种类型的流:字节流、字符流、字节缓冲流和字符缓冲流。

    JDK 为每种类型的流提供一些抽象类以供继承,如下所示:
    ① 字节流:
    InputStream:所有字节输入流的父类。
    OutputStream:所有字节输出流的父类。
    ② 字符流:
    Reader:所有字符输入流的父类。
    Writer:所有字符输出流的父类。
    ③ 字节缓冲流:
    BufferedInputStream:字节缓冲输入流。
    BufferedOutputStream:字节缓冲输出流。
    ④ 字符缓冲流:
    BufferedReader:字符缓冲输入流。
    BufferedWriter:字符缓冲输出流。

  3. 流一般需不需要关闭?如果关闭的话用什么方法?处理流是怎么关闭的?
    答:
    在 Java 中,流一般需要关闭以释放资源和避免内存泄漏。关闭流可以使用 close()方法进行处理。例如,对于文件输入流,可以使用以下代码关闭流:

    FileInputStream fis = new FileInputStream("example.txt");
    //处理流
    fis.close();
    

    对于网络连接,可以使用以下代码关闭流:

    Socket socket = new Socket("127.0.0.1", 8080);
    OutputStream os = socket.getOutputStream();
    //处理流
    os.close();
    socket.close();
    

    在 Java 7 及以上版本中,可以使用 try-with-resources 语句来自动关闭流,示例如下:

    try (FileInputStream fis = new FileInputStream("example.txt"))
    {//处理流
    }
    
  4. OutputStream 里面的 write()是什么意思?
    答:
    OutputStream 中的 write()是将数据写入输出流的方法。它的作用是将指定的字节数组写入输出流中。在 Java 中,OutputStream 是一个抽象类,它提供了几个不同的 write()方法的实现,可以将字节数组、单个字节、字符串、整数等类型的数据写入输出流中。当数据被写入输出流后,它们就可以被传输到其他地方,例如网络连接、文件、内存等。

  5. BufferedReader 属于哪种流?他主要是用来做什么的?
    答:
    BufferedReader 属于字符流,主要用于读取文本数据。它可以将字符从输入流中读取到缓冲区中,从而提高读取效率,同时也提供了一些方便的读取方法。常见的应用场景包括读取文本文件、读取网络数据等。

  6. 什么是缓冲区?有什么作用?
    答:
    缓冲区是一种临时存储数据的区域,用于提高数据的读写效率。在 Java 中,缓冲区主要用于输入输出流的操作中,如 BufferedReader 和 BufferedWriter 等。

    缓冲区的作用是将数据暂时保存在内存中,等到缓冲区满了或者需要进行数据读取时,再一次性将数据写入到文件或者从文件中读取到缓冲区中。这样可以减少与硬盘的交互次数,提高数据读写效率,同时也减轻了硬盘的负担,延长了硬盘的使用寿命。

    另外,缓冲区还可以提供一些额外的功能,如数据的压缩、加密、解密等操作,使得数据的处理更加灵活和高效。

  7. 字节流和字符流是什么?怎么转换?
    答:
    字节流和字符流是 Java 中用于处理输入输出的两种不同的流类型。字节流以字节为单位进行读写,适用于处理二进制文件和网络数据流等;字符流以字符为单位进行读写,适用于处理文本文件和文本数据流等。

    在 Java 中,字节流主要由 InputStream 和 OutputStream 两个抽象类及其实现类构成,而字符流主要由 Reader 和 Writer 两个抽象类及其实现类构成。常用的字节流实现类有 FileInputStream、FileOutputStream、BufferedInputStream 等,常用的字符流实现类有 FileReader、FileWriter、BufferedReader 等。

    转换字节流和字符流可以使用 InputStreamReader 和 OutputStreamWriter 类。InputStreamReader 将字节流转换为字符流,OutputStreamWriter 将字符流转换为字节流。例如,可以使用以下代码将字节流转换为字符流:

    InputStream inputStream = new FileInputStream("file.txt");
    Reader reader = new InputStreamReader(inputStream, "UTF-8");
    

    其中,"file.txt"是要读取的文件名,"UTF-8"是字符集编码。可以使用以下代码将字符流转换为字节流:

    OutputStream outputStream = new FileOutputStream("file.txt");
    Writer writer = new OutputStreamWriter(outputStream, "UTF-8");
    
  8. 什么是 Java 序列化,如何实现?
    答:
    Java 序列化是将 Java 对象转换为字节序列的过程,以便在网络上传输或将其保存到磁盘上。序列化的过程中,Java 会将对象的状态保存为一组字节,并将其写入到输出流中,以便在需要时可以重新构造出原始对象。

    要实现 Java 序列化,需要以下步骤:
    ① 让 Java 类实现 Serializable 接口。这个接口是一个标记接口,没有任何方法,只是用来表示该类可以被序列化。
    ② 创建一个 ObjectOutputStream 实例,并将其连接到一个输出流中。这个输出流可以是文件输出流、网络输出流等。
    ③ 调用 ObjectOutputStream 的 writeObject()方法,将需要序列化的 Java 对象写入输出流中。
    ④ 关闭输出流。

    反序列化则是将字节序列转换为 Java 对象的过程。要实现反序列化,需要以下步骤:
    ① 创建一个 ObjectInputStream 实例,并将其连接到一个输入流中。这个输入流可以是文件输入流、网络输入流等。
    ② 调用 ObjectInputStream 的 readObject()方法,从输入流中读取字节序列,并将其转换为 Java 对象。
    ③ 关闭输入流。

    需要注意的是,在序列化和反序列化过程中,Java 对象的类名、字段名等信息都会被写入到字节序列中,因此在反序列化时,需要确保读取的字节序列所对应的 Java 类仍然存在,否则会抛出 ClassNotFoundException 异常。

  9. Java 有些类中为什么需要实现 Serializable 接口?
    答:
    Java 中的 Serializable 接口是一个标记接口,用于指示一个类的对象可以被序列化。序列化是指将对象转换为字节流的过程,以便在网络上传输或保存到文件中。实现 Serializable 接口的类可以被 Java 中的序列化机制所处理,从而可以实现对象的持久化和远程调用等功能。因此,如果一个类需要被序列化,就需要实现 Serializable 接口。

第 16 章 网络编程

16.1 网络编程概述

计算机网络:把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源。

网络编程的目的:直接或间接地通过网络协议与其它计算机实现数据交换,进行通讯。

需要解决的三个问题:

  • 问题 1:如何准确地定位网络上一台或多台主机?
  • 问题 2:如何定位主机上的特定的应用?
  • 问题 3:找到主机后,如何可靠、高效地进行数据传输?

16.2. IP 地址和域名

16.2.1 IP 地址

  • IP 地址:指互联网协议地址(Internet Protocol Address),俗称 IP。IP 地址
    用来给网络中的一台计算机设备做唯一的编号。

  • IP 分类:

    • 角度一:IPv4、IPv6。

      • IPv4:是一个 32 位的二进制数,通常被分为 4 个字节,表示成 a.b.c.d 的形式,以点分十进制表示,例如 192.168.65.100 。其中 a、b、c、d 都是 0~255 之间的十进制整数。
      • IPv6:由于互联网的蓬勃发展,IP 地址的需求量愈来愈大,但是网络地址资源有限,使得 IP 的分配越发紧张。为了扩大地址空间,拟通过 IPv6 重新定义地址空间,采用 128 位地址长度,共 16 个字节,写成 8 个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开。比如:ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,按保守方法估算 IPv6 实际可分配的地址,整个地球的每平方米面积上仍可分配 1000 多个地
        址,这样就解决了网络地址资源数量不够的问题。
    • 角度二:公网地址、私网地址(或局域网)。

      • 192.168.开头的就是私有地址,范围即为 192.168.0.0–192.168.255.255,专门为组织机构内部使用。
  • 使用 InetAddress 类表示 IP 地址。

    • 实例化:getByName(String host) 、getLocalHost()。
    • 常用方法:getHostName() 、getHostAddress()。

16.2.2 域名

  • Internet 上的主机有两种方式表示地址:

    • 域名(hostName):https://www.baidu.com
    • IP 地址(hostAddress):202.108.22.5
  • 域名解析:因为 IP 地址数字不便于记忆,因此出现了域名。域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS,Domain Name System,域名系统)负责将域名转化成 IP 地址,这样才能和主机建立连接。

16.3 端口号

如果说 IP 地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程。

不同的进程分配不同的端口号。

端口号:用两个字节表示的整数,它的取值范围是 0~65535。

  • 公认端口:0~1023。被预先定义的服务通信占用,如:HTTP(80),FTP(21),Telnet(23)。
  • 注册端口:1024~49151。分配给用户进程或应用程序。如:Tomcat(8080),MySQL(3306),Oracle(1521)。
  • 动态/ 私有端口:49152~65535。

16.4 网络通信协议

网络通信协议有两套参考模型。

  • OSI 参考模型:模型过于理想化,未能在因特网上进行广泛推广。
  • TCP/IP 参考模型(或 TCP/IP 协议):事实上的国际标准。

16.4.1 TCP、UDP

在传输层中涉及到两个协议:TCP、UDP。二者的对比。

  • TCP:可靠的连接(发送数据前,需要三次握手、四次挥手),进行大数据量的传输,效率低。
  • UDP:不可靠的连接(发送前,不需要确认对方是否在)、使用数据报传输(限制在 64kb 以内)、效率高。

16.4.2 TCP 的三次握手

TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

  • 第一次握手,客户端向服务器端发起 TCP 连接的请求。
  • 第二次握手,服务器端发送针对客户端 TCP 连接请求的确认。
  • 第三次握手,客户端发送确认的确认。

16.4.2 TCP 的四次挥手

TCP 协议中,在发送数据结束后,释放连接时需要经过四次挥手。

  • 第一次挥手:客户端向服务器端提出结束连接,让服务器做最后的准备工作。此时,客户端处于半关闭状态,即表示不再向服务器发送数据了,但是还可以接受数据。
  • 第二次挥手:服务器接收到客户端释放连接的请求后,会将最后的数据发给客户端。并告知上层的应用进程不再接收数据。
  • 第三次挥手:服务器发送完数据后,会给客户端发送一个释放连接的报文。那么客户端接收后就知道可以正式释放连接了。
  • 第四次挥手:客户端接收到服务器最后的释放连接报文后,要回复一个彻底断开的报文。这样服务器收到后才会彻底释放连接。这里客户端,发送完最后的报文后,会等待 2MSL,因为有可能服务器没有收到最后的报文,那么服务器迟迟没收到,就会再次给客户端发送释放连接的报文,此时客户端在等待时间范围内接收到,会重新发送最后的报文,并重新计时。如果等待 2MSL 后,没有收到,那么彻底断开。

16.5 网络编程 API

16.5.1 InetAddress 类

  • InetAddress 类主要表示 IP 地址,两个子类:Inet4Address、Inet6Address。

  • InetAddress 类没有提供公共的构造器,而是提供 了 如下几个 静态方法来获取 InetAddress 实例。

    • public static InetAddress getLocalHost()
    • public static InetAddress getByName(String host)
    • public static InetAddress getByAddress(byte[] addr)
  • InetAddress 提供了如下几个常用的方法。

    • public String getHostAddress() :返回 IP 地址字符串(以文本表现形式)。
    • public String getHostName() :获取此 IP 地址的主机名。
    • public boolean isReachable(int timeout):测试是否可以达到该地址。

16.5.2 Socket 类

网络上具有唯一标识的 IP 地址和端口号组合在一起构成唯一能识别的标识符套接字 (Socket)。

利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。网络通信其实就是 Socket 间的通信。

通信的两端都要有 Socket,是两台机器间通信的端点。

Socket 允许程序把网络连接当成一个流,数据在两个 Socket 间通过 IO 传输。

一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。

Socket 分类:

  • 流套接字(stream socket):使用 TCP 提供可依赖的字节流服务。

    • ServerSocket:此类实现 TCP 服务器套接字。服务器套接字等待请求通过网络传入。
    • Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
  • 数据报套接字(datagram socket):使用 UDP 提供“尽力而为”的数据报服务。
    • DatagramSocket:此类表示用来发送和接收 UDP 数据报包的套接字。

16.5.3 DatagramSocket 类

  • DatagramSocket 类的常用方法:

    • public DatagramSocket(int port)创建数据报套接字并将其绑定到本地主机上的指定端口。套接字将被绑定到通配符地址,IP 地址由内核来选择。
    • public DatagramSocket(int port,InetAddress laddr)创建数据报套接字,将其绑定到指定的本地地址。本地端口必须在 0 到 65535 之间(包括两者)。如果 IP 地址为 0.0.0.0,套接字将被绑定到通配符地址,IP 地址由内核选择。
    • public void close()关闭此数据报套接字。
    • public void send(DatagramPacket p)从此套接字发送数据报包。DatagramPacket 包含的信息指示:将要发送的数据、其长度、远程主机的 IP 地址和远程主机的端口号。
    • public void receive(DatagramPacket p)从此套接字接收数据报包。当此方法返回时, DatagramPacket 的缓冲区填充了接收的数据。数据报包也包含发送方的 IP 地址和发送方机器上的端口号。 此方法在接收到数据报前一直阻塞。数据报包对象的 length 字段包含所接收信息的长度。如果信息比包的长度长,该信息将被截短。
    • public InetAddress getLocalAddress()获取套接字绑定的本地地址。
    • public int getLocalPort()返回此套接字绑定的本地主机上的端口号。
    • public InetAddress getInetAddress()返回此套接字连接的地址。如果套接字未连接,则返回 null。
    • public int getPort()返回此套接字的端口。如果套接字未连接,则返回 -1。

16.5.4 DatagramPacket 类

  • DatagramPacket 类的常用方法:

    • public DatagramPacket(byte[] buf,int length)构造 DatagramPacket,用来接收长度为 length 的数据包。 length 参数必须小于等于 buf.length。
    • public DatagramPacket(byte[] buf,int length,InetAddress address,int port)构造数据报
      包,用来将长度为 length 的包发送到指定主机上的指定端口号。length 参数必须小于等于 buf.length。
    • public InetAddress getAddress()返回某台机器的 IP 地址,此数据报将要发往该机器或者是从该机器接收到的。
    • public int getPort()返回某台远程主机的端口号,此数据报将要发往该主机或者是从该主机接收到的。
    • public byte[] getData()返回数据缓冲区。接收到的或将要发送的数据从缓冲区中的偏移量 offset 处开始,持续 length 长度。
    • public int getLength()返回将要发送或接收到的数据的长度。

16.6 TCP 网络编程

16.6.1 通信模型

Java 语言的基于套接字 TCP 编程分为服务端编程和客户端编程,其通信模型如图所示:

16.6.2 开发步骤

  • 客户端程序包含以下四个基本的步骤 :

    • 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
    • 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用
      getOutputStream()方法获得输出流,进行数据传输。
    • 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线路。
    • 关闭 Socket :断开客户端到服务器的连接,释放线路。
  • 服务器端程序包含以下四个基本的 步骤:

    • 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
    • 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
    • 调用该 Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和输入流,开始网络数据的发送和接收。
    • 关闭 Socket 对象:客户端访问结束,关闭通信套接字。

16.7 UDP 网络编程

16.7.1 通信模型

UDP 协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送,至于对方是否可以接收到这些数据内容,UDP 协议无法控制,因此说,UDP 协议是一种不可靠的协议。无连接的好处就是快,省内存空间和流量,因为维护连接需要创建大量的数据结构。

UDP 会尽最大努力交付数据,但不保证可靠交付,没有 TCP 的确认机制、重传机制,如果因为网络原因没有传送到对端,UDP 也不会给应用层返回错误信息。

UDP 协议是面向数据报文的信息传送服务。UDP 在发送端没有缓冲区,对于应用层交付下来的报文在添加了首部之后就直接交付于 ip 层,不会进行合并,也不会进行拆分,而是一次交付一个完整的报文。比如我们要发送 100 个字节的报文,我们调用一次 send()方法就会发送 100 字节,接收方也需要用 receive() 方法一次性接收 100 字节,不能使用循环每次获取 10 个字节,获取十次这样的做法。

UDP 协议没有拥塞控制,所以当网络出现的拥塞不会导致主机发送数据的速率降低。虽然 UDP 的接收端有缓冲区,但是这个缓冲区只负责接收,并不会保证 UDP 报文的到达顺序是否和发送的顺序一致。因为网络传输的时候,由于网络拥塞的存在是很大的可能导致先发的报文比后发的报文晚到达。如果此时缓冲区满了,后面到达的报文将直接被丢弃。这个对实时应用来说很重要,比如:视频通话、直播等应用。因此 UDP 适用于一次只传送少量数据、对可靠性要求不高的应用环境,数据报大小限制在 64K 以下。

类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。DatagramPacket 对象封装了 UDP 数据报,在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号。

16.7.2 开发步骤

  • 发送端程序包含以下四个基本的步骤:

    • 创建 DatagramSocket :默认使用系统随机分配端口号。
    • 创建 DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长
      度,接收方的 IP 地址和端口号。
    • 调用该 DatagramSocket 类对象的 send 方法 :发送数据报 DatagramPacket 对象。
    • 关闭 DatagramSocket 对象:发送端程序结束,关闭通信套接字。
  • 接收端程序包含以下四个基本的步骤 :

    • 创建 DatagramSocket :指定监听的端口号。 -创建 DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果,并指定最大可以接收的数据长度。
    • 调用 该 DatagramSocket 类对象的 receive 方法 :接收数据报 DatagramPacket 对
      象。
    • 关闭 DatagramSocket :接收端程序结束,关闭通信套接字。

16.8 URL 编程

Java 后台:将写好的 Java 程序部署在 Tomcat 服务器。启动 Tomcat 服务器。

前台:使用浏览器进行访问。需要使用 url。(HTML+CSS+JavaScript)

URL 的作用:定位互联网上某一资源的地址。

URL 的格式:

http://192.168.21.107:8080/examples/abcd.jpg?name=Tom
应用层协议    ip地址    端口号    资源地址     参数列表

使用 URL 实现数据的下载。

16.9 企业真题

  1. TCP 协议和 UDP 协议的区别?
    答:
    TCP 协议和 UDP 协议是网络传输协议,二者的区别主要有以下几点:
    ① 连接方式:TCP 协议是面向连接的协议,UDP 协议是无连接的协议。
    ② 可靠性:TCP 协议提供可靠的传输,保证数据的完整性和正确性,而 UDP 协议不保证数据的可靠性,因此传输速度更快。
    ③ 传输速度:UDP 协议的传输速度比 TCP 协议快,因为它没有建立连接的过程和数据校验的过程。
    ④ 数据包大小:TCP 协议传输的数据包大小是有限制的,而 UDP 协议可以传输更大的数据包。
    ⑤ 流量控制:TCP 协议具有流量控制和拥塞控制的功能,可以调整数据的传输速度,而 UDP 协议则没有这个功能。
    ⑥ 应用场景:TCP 协议适用于需要可靠传输的场景,如文件传输、电子邮件等;UDP 协议适用于实时传输和数据量较小的场景,如音频、视频等。

    综上所述,TCP 协议和 UDP 协议各有优缺点,根据不同的应用场景选择合适的协议可以提高网络传输效率和数据传输质量。

  2. 简单说说 TCP 协议的三次握手与四次挥手机制?
    答:
    TCP 协议的三次握手是指,在建立 TCP 连接时,客户端和服务器双方需要进行三次通信来确认彼此的身份和确认双方的通信能力。具体流程如下:
    ① 客户端发送一个 SYN 报文给服务器端,表示客户端请求连接。
    ② 服务器接收到客户端的 SYN 报文后,向客户端发送一个 SYN+ACK 报文,表示服务器已经收到客户端的请求,并请求确认连接。
    ③ 客户端接收到服务器端的 SYN+ACK 报文后,向服务器端发送一个 ACK 报文,表示客户端已经确认连接,连接建立成功。

    TCP 协议的四次挥手是指,在关闭 TCP 连接时,客户端和服务器双方需要进行四次通信来关闭连接。具体流程如下:
    ① 客户端向服务器端发送一个 FIN 报文,表示客户端要关闭连接。
    ② 服务器接收到客户端的 FIN 报文后,向客户端发送一个 ACK 报文,表示服务器已经接收到客户端的请求,但是服务器还需要一些时间来处理数据。
    ③ 服务器处理完数据后,向客户端发送一个 FIN 报文,表示服务器也要关闭连接。
    ④ 客户端接收到服务器端的 FIN 报文后,向服务器端发送一个 ACK 报文,表示客户端已经接收到服务器端的请求,连接关闭成功。

第 17 章 反射机制

17.1 反射的概述

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。

加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类只有一个 Class 对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为:反射。

Java 给我们提供了一套 API,使用这套 API 我们可以在运行时动态的获取指定对象所属的类,创建运行时类的对象,调用指定的结构(属性、方法)等。

反射相关的主要 API:

  • java.lang.Class:代表一个类。
  • java.lang.reflect.Method:代表类的方法。
  • java.lang.reflect.Field:代表类的成员变量。
  • java.lang.reflect.Constructor:代表类的构造器。

反射的优点和缺点:

  • 优点:
  • 提高了 Java 程序的灵活性和扩展性,降低了耦合性,提高自适应能力。
  • 允许程序创建和控制任何类的对象,无需提前硬编码目标类。
  • 缺点:
  • 反射的性能较低。
    • 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上。
  • 反射会模糊程序内部逻辑,可读性较差。

反射,平时开发中,我们使用并不多。主要是在框架的底层使用。

17.2 Class:反射的源头

Class 的理解:

  • 针对于编写好的.java 源文件进行编译(使用 javac.exe),会生成一个或多个.class 字节码文件。接着,我们使用 java.exe 命令对指定的.class 文件进行解释运行。这个解释运行的过程中,我们需要将.class 字节码文件加载(使用类的加载器)到内存中(存放在方法区)。加载到内存中的.class 文件对应的结构即为 Class 的一个实例。

Class 本身也是一个类。

Class 对象只能由系统建立对象。

一个加载的类在 JVM 中只会有一个 Class 实例。

一个 Class 对象对应的是一个加载到 JVM 中的一个.class 文件。

每个类的实例都会记得自己是由哪个 Class 实例所生成。

通过 Class 可以完整地得到一个类中的所有被加载的结构。

Class 类是 Reflection 的根源,针对任何你想动态加载、运行的类,唯有先获得相应的 Class 对象。

获取 Class 的实例的几种方式。

  • 类.class
  • 对象.getClass()
  • Class 调用静态方法 forName(String className)
  • 使用 ClassLoader 的方法 loadClass(String className)

Class 可以指向哪些结构?

  • 简言之,所有 Java 类型。
    ① class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部类
    ② interface:接口
    ③ []:数组
    ④ enum:枚举
    ⑤ annotation:注解@interface
    ⑥ primitive type:基本数据类型
    ⑦ void

17.3 类的加载过程、类的加载器

  • 类的加载过程

    • 过程 1:类的装载(loading)

      • 将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象。此过程由类加载器完成。
    • 过程 2:链接(linking)
      ① 验证(Verify):确保加载的类信息符合 JVM 规范,例如:以 cafebabe 开头,没有安全方面的问题。
      ② 准备(Prepare):正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
      ③ 解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。

    • 过程 3:初始化(initialization)

      • 执行类构造器()方法的过程。类构造器()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。
      • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
      • 虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
  • 类的加载器

    • 作用:负责类的加载,并对应于一个 Class 的实例。

    • 分类:

      • 启动类加载器(引导类加载器,Bootstrap ClassLoader)

        • 使用 C/C++语言编写的,不能通过 Java 代码获取其实例。
        • 负责加载 Java 的核心库。(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路径下的内容)
      • 扩展类加载器(Extension ClassLoader)
        • 负责加载从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录下加载类库。
      • 应用程序类加载器(系统类加载器,AppClassLoader)
        • 我们自定义的类,默认使用的类的加载器。
      • 用户自定义类的加载器。
        • 实现应用的隔离(同一个类在一个应用程序中可以加载多份);数据的加密。

17.4 反射的应用 1:创建运行时类的对象

在 Java 中,我们可以通过反射来创建运行时类的对象。具体步骤如下:
① 获取运行时类的 Class 对象,可以使用 Class.forName()方法或者运行时类的.class 属性来获取。
② 使用 Class 对象的 newInstance()方法来创建运行时类的对象。该方法会调用运行时类的无参构造器来创建对象。
③ 如果运行时类没有无参构造器,可以使用 Class 对象的 getConstructor()方法来获取指定参数类型的构造器,然后使用 Constructor 对象的 newInstance()方法来创建运行时类的对象。

代码示例:

public class ReflectDemo
{public static void main(String[] args) throws Exception{// 获取运行时类的Class对象Class clazz = Class.forName("com.example.Person");// 使用Class对象的newInstance()方法来创建对象Person person1 = (Person) clazz.newInstance();// 使用Constructor对象的newInstance()方法来创建对象Constructor constructor = clazz.getConstructor(String.class, int.class);Person person2 = (Person) constructor.newInstance("Tom", 20);}
}

需要注意的是,使用反射创建对象的效率较低,因为需要通过反射调用构造器来创建对象,而不是直接调用 new 关键字。因此,在实际开发中,应该尽量避免频繁使用反射来创建对象。

17.5 反射的应用 2:获取运行时类所有的结构

在 Java 中,可以使用反射获取运行时类所有的结构,包括类名、父类、接口、构造方法、字段、方法等。以下是一些常用的反射方法:

  • 获取类名:
Class<?> clazz = obj.getClass();
String className = clazz.getName();
  • 获取父类:
Class<?> superClass = clazz.getSuperclass();
  • 获取实现的接口:
Class<?>[] interfaces = clazz.getInterfaces();
  • 获取构造方法:
Constructor<?>[] constructors = clazz.getConstructors();
  • 获取字段:
Field[] fields = clazz.getFields();
  • 获取方法:
Method[] methods = clazz.getMethods();

通过反射,可以动态地获取运行时类的结构信息,并且可以根据需要进行访问和修改。但是,使用反射也需要谨慎,因为它可能会影响程序的性能和安全性。

17.6 反射的应用 3:调用指定的结构

在 Java 中,可以使用反射调用指定类的方法。首先需要获取该类的 Class 对象,然后通过 Class 对象获取指定方法的 Method 对象,最后通过 Method 对象调用该方法。

代码示例:

public class Person
{private String name;private int age;public Person(String name, int age){this.name = name;this.age = age;}public void sayHello(){System.out.println("Hello, I'm " + name + ", " + age + " years old.");}
}public class Main
{public static void main(String[] args) throws Exception{Class clazz = Person.class;Constructor constructor = clazz.getConstructor(String.class, int.class);Object person = constructor.newInstance("Tom", 20);Method method = clazz.getMethod("sayHello");method.invoke(person);}
}

以上代码中,首先通过 Class 对象获取构造方法,然后创建一个 Person 对象。然后通过 Class 对象获取 sayHello 方法的 Method 对象,最后通过 Method 对象调用该方法。输出结果为:

Hello, I'm Tom, 20 years old.

17.7 反射的应用 4:注解的使用

在 Java 语言中,反射是指在运行时动态地获取类的信息并进行操作的能力。注解是一种在程序中添加元数据的方式,它可以在类、方法、字段等元素上添加注释,提供额外的信息。注解的使用可以通过反射来实现。

例如,我们定义了一个注解@MyAnnotation:

public @interface MyAnnotation
{String value() default "";
}

我们可以在程序中使用@MyAnnotation 注解:

@MyAnnotation(value="Hello World")
public class MyClass
{// ...
}

在程序运行时,我们可以通过反射获取这个类的注解信息:

Class<MyClass> clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class);
String value = annotation.value();    // value的值为"Hello World"

通过反射获取注解信息,我们可以根据注解的值来进行不同的操作,例如根据注解值来动态生成代码、修改程序行为等等。

17.8 体会:反射的动态性

  • 反射的动态性指的是在程序运行时,可以通过反射机制获取并操作对象的信息,而不需要在编译时就确定对象的类型。这种动态性使得程序具有更大的灵活性和扩展性。

  • 例如,我们可以通过反射机制获取一个类的所有方法、属性、构造器等信息,然后动态地创建对象、调用方法、修改属性等。这样就可以在运行时根据需要动态地创建和操作对象,而不需要在编译时就确定对象的类型和行为。

  • 反射的动态性也带来了一些挑战和风险,因为在运行时动态创建和操作对象可能会导致一些不可预测的行为和错误。因此,在使用反射机制时需要谨慎考虑其使用场景和限制条件,以确保程序的正确性和稳定性。

  • 以下是一个简单的反射示例代码,演示如何通过反射机制获取类的信息并创建对象:

    public class MyClass
    {private int value;public MyClass(int value){this.value = value;}public void setValue(int value){this.value = value;}public int getValue(){return value;}
    }public class ReflectionExample
    {public static void main(String[] args) throws Exception{// 获取类对象Class<?> clazz = MyClass.class;// 获取构造器并创建对象Constructor<?> constructor = clazz.getConstructor(int.class);MyClass obj = (MyClass) constructor.newInstance(10);// 获取方法并调用Method method = clazz.getMethod("setValue", int.class);method.invoke(obj, 20);// 获取属性并修改Field field = clazz.getDeclaredField("value");field.setAccessible(true);field.set(obj, 30);// 输出结果System.out.println(obj.getValue()); // 输出30}
    }
    

    在上面的示例中,我们首先通过 MyClass.class 获取了该类的类对象 clazz,然后通过 clazz 获取了构造器、方法和属性等信息。接着,我们使用反射机制创建了一个 MyClass 对象 obj,并通过反射调用了 setValue 方法和修改了 value 属性的值。最后,我们输出了 obj 的 value 属性值,结果为 30。

17.9 企业真题

  1. 对反射了解吗?反射有什么好处?为什么需要反射?
    答:
    反射是一种程序能够在运行时自我检查和修改自身行为和状态的能力。在 Java 语言中,反射允许程序在运行时获取类的信息,比如类名、方法、属性等,并且可以动态地调用这些方法和属性。

    反射的好处是它可以使程序更加灵活和可扩展。通过反射,程序可以在运行时动态地加载和使用类,而不需要在编译时就确定类的类型。这使得程序可以根据需要动态地创建对象、调用方法、访问属性等。

    反射的另一个好处是它可以帮助程序员编写更加通用和灵活的代码。通过反射,程序员可以编写通用的代码,而不需要针对每个具体的类编写不同的代码。这使得代码更加简洁、易于维护和扩展。

    需要反射的主要原因是程序需要在运行时动态地加载和使用类。这通常发生在以下情况下:
    ① 当程序需要动态地创建对象时,比如根据用户输入的类名创建对象。
    ② 当程序需要动态地调用方法时,比如根据用户输入的方法名和参数类型调用方法。
    ③ 当程序需要动态地访问属性时,比如根据用户输入的属性名获取属性值。

    总之,反射是一种非常有用的技术,可以使程序更加灵活、通用和可扩展。但是,由于反射需要在运行时动态地加载和使用类,所以它可能会影响程序的性能。因此,在使用反射时需要权衡性能和灵活性之间的平衡。

  2. 反射的使用场合和作用、及其优缺点?
    答:
    反射是指在运行时动态地获取类的信息并进行操作的能力。它可以在程序运行时动态地获取类或对象的属性、方法、构造函数等信息,并进行操作。反射可以让程序员在不知道类名的情况下,通过字符串来获取类的信息、创建对象、调用方法等。反射在以下场合有很好的应用:
    ① 框架开发:反射可以在框架开发中使用,例如 Spring 框架中的 IOC 容器就是基于反射实现的。
    ② 动态代理:反射可以用于动态代理,它可以在运行时动态地创建代理对象,并增加一些额外的功能。
    ③ 序列化和反序列化:反射可以在序列化和反序列化过程中使用,例如 Java 中的 ObjectInputStream 和 ObjectOutputStream 类就是基于反射实现的。
    ④ 单元测试:反射可以用于单元测试中,例如 JUnit 框架就是基于反射实现的。

    反射的优点:
    ① 反射提供了动态性,可以在运行时获取类的信息,方便进行操作。
    ② 反射可以实现类似于泛型的功能,可以在编译时不确定类型,在运行时才确定。
    ③ 反射可以实现一些复杂的功能,例如动态代理、注解处理等。

    反射的缺点:
    ① 反射效率较低,反射调用方法和访问属性的效率比直接调用要低。
    ② 反射容易导致代码不稳定,容易出现一些运行时异常,例如 ClassCastException 等。
    ③ 反射使用不当会导致安全问题,例如通过反射获取私有属性或方法,破坏了封装性和安全性。

  3. 实现 Java 反射的类有什么?
    答:
    实现 Java 反射的类有以下几个:
    ① Class 类:表示一个类或接口,在运行时可以通过它获取类的信息,如类名、父类、接口、构造方法、成员变量、方法等。
    ② Constructor 类:表示一个构造方法,在运行时可以通过它创建对象。
    ③ Method 类:表示一个方法,在运行时可以通过它调用方法。
    ④ Field 类:表示一个成员变量,在运行时可以通过它获取或设置成员变量的值。
    ⑤ Modifier 类:表示一个修饰符,在运行时可以通过它获取修饰符的信息,如 public、private、static 等。
    ⑥ Array 类:表示一个数组,在运行时可以通过它创建数组对象,并获取数组元素的值。

  4. 反射是怎么实现的?
    答:
    在 Java 中,反射是通过 Class 类来实现的。每个类在内存中都有一个 Class 对象,该对象包含了类的所有信息,包括类的名称、属性、方法等。

    通过 Class 类的 newInstance()方法可以动态地创建一个类的实例。通过 Class 类的 getMethod()、getField()、getDeclaredMethod()、getDeclaredField()等方法可以获取类的方法和属性,从而动态地调用方法或修改属性的值。

    反射的实现基于 Java 虚拟机的动态加载机制,可以在程序运行时动态地加载和使用类。这种机制使得 Java 程序可以在运行时根据需要加载和使用不同的类,从而实现更加灵活和动态的编程。

  5. Class 类的作用?生成 Class 对象的方法有哪些?
    答:
    Class 类的作用是描述类的结构信息,包括类的属性、方法、构造器等。它是 Java 反射的核心类,通过 Class 类可以获取一个类的信息并操作它的属性和方法。

    生成 Class 对象的方法主要有三种:
    ① 调用对象的 getClass()方法:可以通过一个已经存在的对象来获取它所属的类的 Class 对象。
    ② 使用类的.class 语法:在编译期间就可以确定一个类的 Class 对象,可以直接使用类名.class 来获取。
    ③ 使用 Class.forName()方法:可以通过类的全限定名来获取 Class 对象,这种方式可以在运行时动态加载类。

  6. Class.forName(“全路径”) 会调用哪些方法 ? 会调用构造方法吗?加载的类会放在哪?
    答:
    Class.forName(“全路径”) 会调用类的静态初始化块(static{})和静态变量的初始化,但不会调用构造方法。加载的类会放在 JVM 的方法区(Method Area)中。

  7. 类加载流程?
    答:
    类加载是 Java 虚拟机将类的.class 文件中的二进制数据读入内存中,使其成为方法区内的 Class 对象的过程。类加载的流程包括以下几个步骤:
    ① 加载:查找并加载类的二进制数据。在此阶段,Java 虚拟机需要完成以下任务:
    a. 通过一个类的全限定名获取定义此类的二进制字节流。
    b. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构。
    c. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
    ② 链接:将类的二进制数据合并到 JVM 的运行状态之中。在此阶段,Java 虚拟机需要完成以下任务:
    a. 验证:确保加载的类的正确性。
    b. 准备:为类的静态变量分配内存,并设置默认初始值。
    c. 解析:将类中的符号引用转换为直接引用。
    ③ 初始化:为类的静态变量赋予正确的初始值,并执行初始化代码块。在此阶段,Java 虚拟机需要完成以下任务:
    a. 执行类构造器()方法的过程。
    b. 如果该类还有父类,JVM 会先初始化其父类。

    以上是类加载的主要流程。需要注意的是,类的加载过程是按需的,即只有在使用时才会进行类的加载。此外,JVM 还提供了一些机制来控制类的加载行为,例如类加载器、双亲委派机制等。

  8. 说一下创建对象的几种方法?
    答:
    ① 使用 new 关键字创建对象:

    MyClass object = new MyClass();
    

    ② 使用 Class 类的 newInstance() 方法创建对象:

    MyClass object = (MyClass) Class.forName("MyClass").newInstance();
    

    ③ 使用 clone() 方法创建对象:

    MyClass object = (MyClass) oldObject.clone();
    

    ④ 使用反序列化创建对象:

    ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
    MyClass object = (MyClass) in.readObject();
    

    ⑤ 使用工厂方法创建对象:

    MyClass object = MyClassFactory.createMyClass();
    

    ⑥ 使用构造方法引用创建对象:

    MyClass object = MyClass::new;
    
  9. 如何找到对象实际类的?
    答:
    可以使用 getClass()方法来获取对象实际的类,例如:

    Object obj = new String("Hello");
    Class<?> cls = obj.getClass();
    System.out.println(cls.getName());
    

    输出结果为:java.lang.String

  10. Java 反射创建对象效率高还是通过 new 创建对象的效率高?
    答:
    在创建对象方面,通过 new 创建对象的效率要高于 Java 反射创建对象。

    这是因为 Java 反射创建对象需要进行额外的操作,在运行时需要进行类的加载、方法的调用等操作,这些都会消耗一定的时间,因此相对于直接通过 new 创建对象,Java 反射创建对象的效率会稍微慢一些。

    但是,Java 反射创建对象也具有其独特的优点,比如可以在运行时动态地创建对象、调用方法等,这对于某些特定的场景是非常有用的。因此,在具体实现中需要根据实际情况进行选择。

  11. 如何利用反射机制来访问一个类的方法?
    答:
    可以通过以下步骤来利用反射机制访问一个类的方法:
    ① 获取该类的 Class 对象,可以通过 Class.forName()方法或者类名.class 来获取。
    ② 获取指定方法的 Method 对象,可以通过 Class 对象的 getMethod()方法或者 getDeclaredMethod()方法来获取。
    ③ 调用 Method 对象的 invoke()方法来执行该方法,需要传入该方法所属的对象和方法参数。

    示例代码:

    // 获取类的Class对象
    Class<?> clazz = MyClass.class;try
    {// 获取指定方法的Method对象Method method = clazz.getMethod("methodName", int.class, String.class);// 创建该方法所属的对象MyClass obj = new MyClass();// 调用方法Object result = method.invoke(obj, 123, "abc");// 输出方法返回值System.out.println(result);
    }
    catch (Exception e)
    {e.printStackTrace();
    }
    
  12. 说一下 Java 反射获取私有属性,如何改变值?
    答:
    Java 反射可以通过 getDeclaredField()方法获取私有属性,并通过 setAccessible(true)方法设置访问权限,最终通过 set()方法修改值。示例代码如下:

    public class MyClass
    {private int myPrivateField = 10;
    }public class Main{public static void main(String[] args) throws Exception{MyClass obj = new MyClass();Field privateField = MyClass.class.getDeclaredField("myPrivateField");privateField.setAccessible(true);int newValue = 20;privateField.set(obj, newValue);System.out.println(obj.myPrivateField);}}
    

    上述代码中,我们使用反射获取了 MyClass 类中的私有属性 myPrivateField,并设置了访问权限。然后通过 set()方法将其值修改为 20,并打印出结果。

    需要注意的是,在 JDK17 中,内部的私有结构已经不再支持反射调用。这是因为 Java 官方认为这种调用方式可能会导致安全问题。因此,我们需要在编写代码时避免使用这种方法。

第 18 章 JDK8 到 JDK17 新特性

18.1 JDK 新特性的概述

  • 几个重要的版本:

    • jdk 5.0、jdk 8.0:里程碑式的版本。
    • jdk 9.0 开始每 6 个月发布一个新的版本。
    • LTS(长期支持版本): jdk8、jdk 11、jdk 17。
  • 如何学习新特性?

    • 角度 1:新的语法规则

      • 自动装箱、自动拆箱、enum、泛型、Lambda 表达式、接口中的默认方法、静态方法、局部变量的类型推断、switch 表达式、文本块等。
    • 角度 2:增加、过时、删除 API - Stream、Optional、新的日期时间的 API、HashMap 的底层结构、String 的底层结构
      等。
    • 角度 3:底层的优化、内存结构(永久代——>元空间)、新的 JS 执行引擎、新的垃圾回收器、GC 参数、JVM 的优化。

18.2 JDK8:lambda 表达式

在给函数式接口提供实例时,都可以考虑使用 lambda 表达式。

基本语法的使用:

  • Lambda 表达式:在 Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或箭头操作符。它将 Lambda 分为两个部分:

    • 左侧:指定了 Lambda 表达式需要的参数列表。
    • 右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即 Lambda 表达式要执行的功能。

下面是一个使用 lambda 表达式的简单代码示例:

import java.util.ArrayList;
import java.util.List;public class LambdaExample
{public static void main(String[] args){List<String> names = new ArrayList<>();names.add("Alice");names.add("Bob");names.add("Charlie");// 使用lambda表达式遍历List并打印每个元素names.forEach(name -> System.out.println(name));// 使用lambda表达式对List进行排序names.sort((name1, name2) -> name1.compareTo(name2));// 使用lambda表达式过滤List中长度大于4的元素List<String> longNames = names.stream().filter(name -> name.length() > 4).collect(Collectors.toList());}
}

在上面的示例中,我们使用了 lambda 表达式来遍历 List、对 List 进行排序以及过滤 List 中的元素。在遍历 List 时,我们使用了 forEach()方法,该方法接受一个 Consumer 接口作为参数,该接口中定义了一个 accept()方法,用于处理每个元素。在排序 List 时,我们使用了 sort()方法,该方法接受一个 Comparator 接口作为参数,该接口中定义了一个 compare()方法,用于比较两个元素的大小关系。在过滤 List 时,我们使用了 stream()方法和 filter()方法,这两个方法都接受一个 Predicate 接口作为参数,该接口中定义了一个 test()方法,用于判断元素是否符合条件。

18.3 函数式接口

只包含一个抽象方法(Single Abstract Method,简称 SAM)的接口,称为函数式接口。当然该接口可以包含其他非抽象方法。

可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。

我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。

在 java.util.function 包下定义了 Java 8 的丰富的函数式接口。

  • 常见的函数式接口。Comparator、Runnable、java.util.function 下定义的丰富的函数式接口。

    • 消费型接口:Consumer void accept(T t)
    • 供给型接口:Supplier T get()
    • 函数型接口:Function<T,R> R apply(T t)
    • 判断型接口:Predicate boolean test(T t)
  • 消费型接口示例:将字符串转为大写输出。

    Consumer<String> consumer = str -> System.out.println(str.toUpperCase());
    consumer.accept("hello world");
    
  • 供给型接口示例:生成一个随机数。

    Supplier<Integer> supplier = () -> (int) (Math.random() * 100);
    int num = supplier.get();
    
  • 函数型接口示例:将字符串转为整型并乘以 2。

    Function<String, Integer> function = str -> Integer.parseInt(str) * 2;
    int result = function.apply("10");
    
  • 判断型接口示例:判断字符串是否为空。

    Predicate<String> predicate = str -> str == null || str.isEmpty();
    boolean isEmpty = predicate.test("");
    

18.4 方法引用、构造器引用、数组引用

方法引用、构造器引用、数组引用:看做是 lambda 表达式的进一步凝练。

方法引用格式:

  • 格式:使用方法引用操作符 “::” 将类(或对象) 与 方法名分隔开来。
  • 两个:中间不能有空格,而且必须英文状态下半角输入。

如下三种主要使用情况:

  • 情况 1:对象 :: 实例方法名
  • 情况 2:类 :: 静态方法名
  • 情况 3:类 :: 实例方法名

方法引用使用前提:

  • 要求 1:Lambda 体只有一句语句,并且是通过调用一个对象的类现有的方法来完成的。

    • 例如:System.out 对象,调用 println()方法来完成 Lambda 体
      Math 类,调用 random()静态方法来完成 Lambda 体
  • 要求 2:

    • 针对情况 1:函数式接口中的抽象方法 a 在被重写时使用了某一个对象的方法 b。如果方法 a 的形参列表、返回值类型与方法 b 的形参列表、返回值类型都相同,则我们可以使用方法 b 实现对方法 a 的重写、替换。
    • 针对情况 2:函数式接口中的抽象方法 a 在被重写时使用了某一个类的静态方法 b。如果方法 a 的形参列表、返回值类型与方法 b 的形参列表、返回值类型都相同,则我们可以使用方法 b 实现对方法 a 的重写、替换。
    • 针对情况 3:函数式接口中的抽象方法 a 在被重写时使用了某一个对象的方法 b。如果方法 a 的返回值类型与方法 b 的返回值类型相同,同时方法 a 的形参列表中有 n 个参数,方法 b 的形参列表有 n-1 个参数,且方法 a 的第 1 个参数作为方法 b 的调用者,且方法 a 的后 n-1 参数与方法 b 的 n-1 参数匹配(类型相同或满足多态场景也可以)
      例如:
      t->System.out.println(t)
      () -> Math.random() 都是无参。
  • 方法引用代码示例:
    ① 静态方法引用

    public class Main
    {public static void main(String[] args){List<String> list = Arrays.asList("apple", "banana", "orange");list.forEach(Main::print); // 静态方法引用}public static void print(String str){System.out.println(str);}}
    

    ② 实例方法引用

    public class Main
    {public static void main(String[] args){List<String> list = Arrays.asList("apple", "banana", "orange");Main main = new Main();list.forEach(main::print); // 实例方法引用}public void print(String str){System.out.println(str);}}
    

    ③ 对象方法引用

    public class Main
    {public static void main(String[] args){List<String> list = Arrays.asList("apple", "banana", "orange");list.forEach(String::toUpperCase); // 对象方法引用}
    }
    
  • 构造器引用示例:
    ① 无参构造器引用

    public class Main
    {public static void main(String[] args){Supplier<Person> supplier = Person::new; // 无参构造器引用Person person = supplier.get();}
    }class Person
    {public Person(){System.out.println("Create a person.");}
    }
    

    ② 有参构造器引用

    public class Main
    {public static void main(String[] args){BiFunction<String, Integer, Person> function = Person::new; // 有参构造器引用Person person = function.apply("Tom", 20);}
    }class Person
    {private String name;private int age;public Person(String name, int age){this.name = name;this.age = age;System.out.println("Create a person: " + name + ", " + age);}
    }
    
  • 数组引用示例:

    public class Main
    {public static void main(String[] args){Function<Integer, int[]> function = int[]::new; // 数组引用int[] array = function.apply(10);}
    }
    

18.5 JDK8:Stream API

Java8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API。

Stream API ( java.util.stream) 把真正的函数式编程风格引入到 Java 中。这是目前为止对 Java 类库最好的补充,因为 Stream API 可以极大提供 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,讲的是数据,而 Stream 是有关计算的,讲的是计算。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

注意:
① Stream 自己不会存储元素。
② Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream。
③ Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。
④ Stream 一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。

Stream 的操作三个步骤:

  • 创建 Stream 一个数据源(如:集合、数组),获取一个流。
  • 中间操作 每次处理都会返回一个持有结果的新 Stream,即中间操作的方法返回值仍然是 Stream 类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行 n 次处理,但是在终结操作前,并不会真正执行。
  • 终止操作(终端操作) 终止操作的方法返回值类型就不再是 Stream 了,因此一旦执行终止操作,就结束整个 Stream 操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束 Stream。

以下是一个简单的代码示例,展示了如何使用 Java 8 的 Stream API 从列表中过滤和映射数据:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;public class StreamExample
{public static void main(String[] args){List<String> names = Arrays.asList("John", "Sarah", "Mark", "Bob", "Mary");// 使用Stream API过滤出名字长度大于等于4的人,并将结果收集到一个新列表中List<String> filteredNames = names.stream().filter(name -> name.length() >= 4).collect(Collectors.toList());System.out.println("Filtered names: " + filteredNames);// 使用Stream API将名字转换为小写,并将结果收集到一个新列表中List<String> lowerCaseNames = names.stream().map(String::toLowerCase).collect(Collectors.toList());System.out.println("Lower case names: " + lowerCaseNames);}
}

输出:

Filtered names: [John, Sarah, Mark, Mary]
Lower case names: [john, sarah, mark, bob, mary]

代码解释:

① 首先,我们创建了一个字符串列表 names,其中包含了一些名字。
② 我们使用 stream()方法将列表转换为一个流。
③ 然后,我们使用 filter()方法过滤出名字长度大于等于 4 的人,并将结果收集到一个新列表中。
④ 接下来,我们使用 map()方法将所有名字转换为小写,并将结果收集到一个新列表中。
⑤ 最后,我们打印出过滤后的名字列表和所有名字的小写列表。

这个示例只是 Stream API 功能的冰山一角,Stream API 还有很多其他有用的方法,例如 distinct()、sorted()、limit()和 reduce()等等。

18.6 JDK8 之后的新特性:语法层面

  • jShell 工具:

    • Java 终于拥有了像 Python 和 Scala 之类语言的 REPL 工具(交互式编程环境,read - evaluate - print - loop):jShell。以交互式的方式对语句和表达式进行求值。即写即得、快速运行。
    • 利用 jShell 在没有创建类的情况下,在命令行里直接声明变量,计算表达式,执行语句。无需跟人解释”public static void main(String[] args)”这句"废话"。
  • try-catch 结构的变化:

    • 在 try 的后面可以增加一个(),在括号中可以声明流对象并初始化。try 中的代码执行完毕,会自动把流对象释放,就不用写 finally 了。

    • 语法格式:

      try(资源对象的声明和初始化)
      {业务逻辑代码,可能会产生异常
      }
      catch(异常类型 1 e)
      {处理异常代码
      }
      catch(异常类型 2 e)
      {处理异常代码
      }
      
  • 局部变量的类型推断:var

  • instanceof 的模式匹配:

    • instanceof 模式匹配通过提供更为简便的语法,来提高生产力。有了该功能,可以减少 Java 程序中显式强制转换的数量,实现更精确、简洁的类型安全的代码。

    • Java 14 之前旧写法:

      if(obj instanceof String)
      {String str = (String)obj;    //需要强转}
      else
      {}
      
    • Java 14 新特性写法:

      if(obj instanceof String str)
      {}
      else
      {}
      
  • switch 的模式匹配:

    • switch 语句是一种选择结构,通常用于根据一个表达式的值来决定程序的执行流程。在 Java 中,switch 语句可以使用模式匹配来匹配多个值。

    • 下面是一个简单的代码示例,展示了如何使用 switch 语句的模式匹配功能:

      public class SwitchExample
      {public static void main(String[] args){int num = 1;switch (num){// num为switch语句的表达式case 1 -> System.out.println("One"); // 如果num等于1,则执行此分支case 2 -> System.out.println("Two"); // 如果num等于2,则执行此分支case 3 -> System.out.println("Three"); // 如果num等于3,则执行此分支default -> System.out.println("Other"); // 如果num不匹配任何一个case分支,则执行此分支}}
      }
      

      上面的代码中,我们使用了 Java 14 中的新特性-箭头表达式,可以更简洁的书写代码,效果与 Java 12 中的 switch 表达式相同。

      在这个示例中,我们定义了一个整型变量 num,并使用 switch 语句对它进行匹配。在每个 case 分支中,我们使用箭头表达式输出不同的值。如果 num 的值不匹配任何一个 case 分支,程序将执行 default 分支。

  • 文本块:

    • 在 Java 13 及以上版本中,可以使用文本块来更方便地处理多行文本。文本块的格式为三个双引号(“”")开始,三个双引号结束,中间可以插入多行文本。

    • 下面是一个详细的代码示例:

      public class TextBlockExample
      {public static void main(String[] args){// 定义一个文本块String textBlock = """Hello,This is a text block.It can span multiple lines.""";// 输出文本块System.out.println(textBlock);// 可以在文本块中使用双引号和换行符String textBlockWithQuoteAndNewLine = """"Hello," she said.This is a text block.It can span multiple lines.""";// 输出带有双引号和换行符的文本块System.out.println(textBlockWithQuoteAndNewLine);// 可以在文本块中插入表达式String name = "Tom";String textBlockWithExpression = """Hello, %s.This is a text block.It can span multiple lines.""".formatted(name);// 输出带有表达式的文本块System.out.println(textBlockWithExpression);}
      }
      

      输出结果:

      Hello,
      This is a text block.
      It can span multiple lines."Hello," she said.
      This is a text block.
      It can span multiple lines.Hello, Tom.
      This is a text block.
      It can span multiple lines.
      

      在文本块中,可以使用反斜杠(\)来转义双引号、换行符等特殊字符。如果要在文本块中插入美元符号($),需要使用两个美元符号来转义。例如:

      String textBlockWithEscape = """"Hello,\" she said."This is a text block.It can span multiple lines.This is a variable: $$""";
      System.out.println(textBlockWithEscape);
      

      输出结果:

      "Hello," she said."
      This is a text block.
      It can span multiple lines.
      This is a variable: $
      
  • 新的引用数据类型:record (记录)

    • 在 Java 16 中,引入了一种新的引用数据类型:record。record 是一种不可变的类,用于表示数据记录。它提供了一个简单的方法来定义一个类,该类包含一组属性和一些实用方法。

    • 以下是一个简单的 record 代码示例:

      public record Person(String name, int age)
      {// 构造器、获取方法和其他方法
      }
      

      这个 record 定义了一个名为 Person 的类,它有两个属性:name 和 age。它还提供了一个构造函数和两个访问器方法:getName()和 getAge()。这些方法都是自动生成的,因为 record 是不可变的,所以不需要 setter 方法。

    • 以下是一个更详细的示例,它展示了如何使用 record 来定义一个学生类:

      public record Student(String name, int age, String major, double gpa)
      {// 构造器public Student{if (age < 0 || gpa < 0 || gpa > 4.0){throw new IllegalArgumentException("Invalid age or GPA");}}// 获取方法public String getName(){return name;}public int getAge(){return age;}public String getMajor(){return major;}public double getGpa(){return gpa;}// 其他方法public boolean isHonors(){return gpa >= 3.5;}public String toString(){return name + " (" + age + ") - " + major + ", GPA: " + gpa;}
      }
      

      这个 record 定义了一个名为 Student 的类,它有四个属性:name、age、major 和 gpa。它还提供了一个构造函数,该函数检查年龄和 GPA 是否有效。它还提供了访问器方法和其他方法,例如 isHonors()和 toString()。

      使用 record 可以使代码更简洁,更易于阅读和维护。它还可以提高代码的性能,因为它是不可变的,所以可以避免同步问题。

  • 密封类:sealed class

    • 密封类(sealed class)是一种特殊的类,它可以被继承,但是只能在定义的文件中被继承,不能在其他文件中被继承。密封类常用于表示有限数量的类型,例如枚举类型。

    • 下面是一个 Java 中密封类的详细代码示例和注释:

      // 定义一个密封类Animal
      public sealed class Animal permits Dog, Cat, Bird
      {// 定义一个抽象方法makeSound,所有子类都必须实现该方法public abstract void makeSound();
      }// 定义一个继承自Animal的子类Dog
      public final class Dog extends Animal
      {// 实现makeSound方法public void makeSound(){System.out.println("Woof!");}
      }// 定义一个继承自Animal的子类Cat
      public final class Cat extends Animal
      {// 实现makeSound方法public void makeSound(){System.out.println("Meow!");}
      }// 定义一个继承自Animal的子类Bird
      public final class Bird extends Animal
      {// 实现makeSound方法public void makeSound(){System.out.println("Chirp!");}
      }// 定义一个测试类
      public class Test
      {public static void main(String[] args){// 创建一个Dog对象并调用makeSound方法Animal animal1 = new Dog();animal1.makeSound();    // 输出Woof!// 创建一个Cat对象并调用makeSound方法Animal animal2 = new Cat();animal2.makeSound();    // 输出Meow!// 创建一个Bird对象并调用makeSound方法Animal animal3 = new Bird();animal3.makeSound();    // 输出Chirp!}
      }
      

      在上面的代码中,我们定义了一个密封类 Animal,它有三个子类:Dog、Cat 和 Bird。每个子类都必须实现 Animal 类中定义的抽象方法 makeSound。在测试类中,我们创建了三个 Animal 对象,并分别调用它们的 makeSound 方法,输出了它们各自的声音。

18.7. JDK8 之后的新特性:Optional API

Optional 类是 Java 8 中引入的一个新特性,用于解决空指针异常的问题。Optional 类可以包含一个非空的对象,也可以表示一个空对象。它可以用于返回值、方法参数、变量等等,以避免空指针异常的发生。

下面是一个使用 Optional 类的代码示例:

import java.util.Optional;public class OptionalExample
{public static void main(String[] args){// 创建一个非空的Optional对象Optional<String> nonEmptyOptional = Optional.of("Hello World");// 创建一个空的Optional对象Optional<String> emptyOptional = Optional.empty();// 判断Optional对象是否为空System.out.println(nonEmptyOptional.isPresent());    // trueSystem.out.println(emptyOptional.isPresent());    // false// 获取Optional对象中的值String value = nonEmptyOptional.get();System.out.println(value);    // Hello World// 如果Optional对象为空,则返回指定的默认值String defaultValue = emptyOptional.orElse("Default Value");System.out.println(defaultValue); // Default Value// 如果Optional对象为空,则执行指定的操作emptyOptional.ifPresent(s -> System.out.println("Optional is empty"));// 使用map操作将Optional对象中的值转换成另一个值Optional<Integer> integerOptional = nonEmptyOptional.map(String::length);System.out.println(integerOptional.get()); // 11}
}

在上面的示例代码中,我们创建了一个非空的 Optional 对象和一个空的 Optional 对象。我们使用 isPresent 方法来判断 Optional 对象是否为空,使用 get 方法来获取 Optional 对象中的值。我们使用 orElse 方法来获取 Optional 对象中的值,如果 Optional 对象为空,则返回指定的默认值。我们使用 ifPresent 方法来执行操作,如果 Optional 对象为空,则不执行任何操作。我们使用 map 方法来将 Optional 对象中的值转换成另一个值。

18.8 企业真题

  1. 谈谈 java8 新特性?
    答:
    Java 8 引入了很多新特性,其中最重要的包括:
    ① Lambda 表达式:Lambda 表达式是一种简洁的语法,用于表示可传递的函数式接口。它可以使代码更加简洁、易读、易维护。
    ② 方法引用:方法引用是一种简洁的语法,用于表示已经存在的方法的引用。它可以使代码更加简洁、易读、易维护。
    ③ Stream API:Stream API 是一种新的 API,用于处理集合数据。它可以使代码更加简洁、易读、易维护。
    ④ 接口默认方法:接口默认方法是在接口中定义的具有默认实现的方法。它可以使接口更加灵活、易扩展。
    ⑤ 函数式接口:函数式接口是只有一个抽象方法的接口。它可以用于 Lambda 表达式和方法引用。
    ⑥ Optional 类:Optional 类是代表一个可能存在的值或不存在的值的容器类。它可以用于避免空指针异常。
    ⑦ Date/Time API:Date/Time API 是一个新的 API,用于处理日期和时间。它可以使日期和时间的处理更加简单、易读、易维护。
    ⑧ Nashorn JavaScript 引擎:Nashorn JavaScript 引擎是一种新的 JavaScript 引擎,用于在 Java 应用程序中执行 JavaScript 代码。它可以使 Java 应用程序更加灵活、易扩展。

  2. JDK1.8 在数据结构上发生了哪些变化 ?
    答:
    JDK1.8 在数据结构上发生了以下变化:
    ① 元空间替代永久代:在 JDK1.8 中,永久代被元空间替代。元空间是一块本地内存区域,与堆内存分离,用于存储类的元数据信息。这样可以避免永久代的内存溢出问题,并且可以动态地调整元空间大小。
    ② HashMap 底层结构:在 JDK1.8 中,HashMap 的底层结构发生了变化。在之前的版本中,HashMap 使用数组+链表的方式实现,但是在 JDK1.8 中,当链表长度达到一定阈值(默认为 8)时,会将链表转化为红黑树,提高了查找效率。

    除此之外,JDK1.8 还引入了一些新的数据结构,如 ConcurrentHashMap 的分段锁机制、Stream API 等,以提高程序的性能和可读性。

  3. 你说的了解 Java 的新特性 ,你说说 JDK8 改进的地方?
    答:
    JDK8 改进的地方包括:
    ① Lambda 表达式:Lambda 表达式是一种简洁的语法,可以实现函数式编程,提高了代码的可读性和简洁性。
    ② Stream API:Stream API 是一种处理集合数据的新方式,可以实现更加简洁高效的数据处理。
    ③ 接口的默认方法:接口的默认方法可以在不破坏原有实现的情况下,为接口添加新的方法。
    ④ 时间日期 API:新的时间日期 API 提供了更加丰富和易用的时间日期处理功能。
    ⑤ 方法引用:方法引用可以让代码更加简洁,提高可读性。
    ⑥ 并行流:并行流可以利用多核处理器的优势,提高集合数据的处理效率。
    ⑦ 重复注解:重复注解可以让同一个注解在同一个元素上出现多次,增加了灵活性。
    ⑧ Type Annotations:Type Annotations 可以在更加精细的层次上注解类型,提高了代码的可读性和可维护性。

  4. JDK1.8 用的是哪个垃圾回收器?
    答:
    JDK1.8 默认使用的是 Parallel GC(并行垃圾回收器)。

    随着计算机硬件的不断进步和应用场景的多样化,垃圾回收器也在不断地发展和优化。从 Parallel GC 到 G1GC 再到 ZGC,Java 垃圾回收器的发展方向大致可以归纳为以下几个方面:
    ① 并行化:Parallel GC 是一种以并行方式进行垃圾回收的垃圾回收器。它通过将内存分割成多个区域,然后在多个线程中同时进行垃圾回收,从而提高了回收效率。在多核 CPU 的环境下,Parallel GC 表现出色,可以有效地利用多核处理器的优势。
    ② 低延迟:G1GC(G1 垃圾回收器)是一种低延迟垃圾回收器。它采用了基于区域的内存管理方式,可以在不影响应用程序性能的情况下,实现更加精细的内存管理和垃圾回收。G1GC 还具备自适应的垃圾回收策略,可以根据应用程序的实际情况,自动调整垃圾回收的时间和频率。
    ③ 大内存支持:ZGC(Z 垃圾回收器)是一种专门针对大内存场景的垃圾回收器。它可以在数十 GB、甚至数百 GB 级别的堆内存上进行垃圾回收,而且不会出现明显的停顿。ZGC 采用了柔性化的垃圾回收策略,可以在不同的内存使用情况下,自动调整垃圾回收的时间和方式。

    总的来说,Java 垃圾回收器的发展方向是趋向于并行化、低延迟和大内存支持。随着计算机硬件的不断升级,垃圾回收器的性能和功能也会不断提高,为 Java 应用程序提供更加优秀的性能和稳定性。

  5. Lambda 表达式有了解吗,说说如何使用的?
    答:
    我了解 Lambda 表达式的概念和用法。Lambda 表达式是 Java 8 引入的一种新特性,用于简化函数式编程的语法。它可以将函数作为参数传递,也可以将函数作为返回值。

    Lambda 表达式的语法如下:

    (parameters) -> expression
    

    或者

    (parameters) -> { statements; }
    

    其中,parameters 是函数的参数列表,expression 是函数的返回值,statements 是函数体。Lambda 表达式可以用在函数式接口中,函数式接口是只有一个抽象方法的接口。Lambda 表达式可以使用 Java 中的函数式接口,例如 Java.util.function 包中的接口,如 Consumer、Function、Predicate 等。

    举个例子,下面是一个使用 Lambda 表达式的示例代码:

    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    names.forEach(name -> System.out.println(name));
    

    上述代码使用了 forEach 方法和 Lambda 表达式,遍历了一个字符串列表并打印了每个字符串的值。

  6. 什么是函数式接口?有几种函数式接口?
    答:
    函数式接口是只包含一个抽象方法的接口。在 Java 8 中引入了函数式接口的概念,以支持 Lambda 表达式和方法引用。函数式接口的一个重要特点是可以通过 Lambda 表达式或方法引用来创建其实例。

    Java 8 中提供了一些常用的函数式接口,包括:
    ① Consumer:消费型接口,接受一个参数,无返回值。
    ② Supplier:供给型接口,不接受任何参数,返回一个值。
    ③ Function<T, R>:函数型接口,接受一个参数,返回一个值。
    ④ Predicate:判断型接口,接受一个参数,返回一个布尔值。

    除了这些常用的函数式接口之外,Java 8 还提供了一些其他的函数式接口,如 BiFunction、UnaryOperator、BinaryOperator 等。这些函数式接口可以帮助开发人员更方便地使用 Lambda 表达式和方法引用。

  7. 创建 Stream 的方式?
    答:
    Java 中创建 Stream 的方式有以下几种:
    ① 通过集合创建 Stream:可以使用 Collection 接口提供的 stream()方法或 parallelStream()方法来创建 Stream。
    ② 通过数组创建 Stream:可以使用 Arrays 类提供的 stream()方法来创建 Stream。
    ③ 通过 Stream.of()方法创建 Stream:可以使用 Stream.of()方法创建包含任意数量元素的 Stream。
    ④ 通过 Stream.iterate()方法创建 Stream:可以使用 Stream.iterate()方法创建一个包含无限元素的 Stream。
    ⑤ 通过 Stream.generate()方法创建 Stream:可以使用 Stream.generate()方法创建一个包含无限元素的 Stream。
    ⑥ 通过 BufferedReader.lines()方法创建 Stream:可以使用 BufferedReader.lines()方法创建一个包含文件中所有行的 Stream。

    以上是创建 Stream 的几种方式,可以根据具体的需求选择最合适的方式。

  8. 你讲讲 stream 表达式是咋用的,干啥的?
    答:
    Stream 表达式是 Java 8 中引入的一种新特性,它提供了一种流式处理数据的方式,可以用来操作集合、数组等数据结构。通过 Stream,我们可以使用类似于 SQL 的语法来对数据进行过滤、排序、映射等操作,大大简化了代码的编写和维护。

    具体来说,Stream 表达式可以通过以下步骤来使用:
    ① 获取一个数据源,比如一个集合。
    ② 对数据源进行过滤、排序、映射等操作,生成一个新的 Stream。
    ③ 对新生成的 Stream 进行聚合操作,如求和、取最大值等。
    ④ 最终生成一个结果。

    Stream 表达式的好处在于它可以大大简化代码的编写和维护,同时也可以提高代码的可读性和可维护性。例如,使用 Stream 表达式可以将一些复杂的操作链式化,从而减少了代码的嵌套层次,使得代码更加简洁易懂。

    总之,Stream 表达式是一种强大的工具,可以帮助 Java 后端开发工程师更加高效地处理数据。

  9. 集合用 Stream 流怎么实现过滤?
    答:
    可以使用 Stream 的 filter() 方法来实现集合的过滤。例如,假设有一个 List 类型的集合,我们可以通过以下代码实现对集合中所有长度大于等于 5 的元素进行过滤:

    List<String> list = Arrays.asList("apple", "banana", "orange", "pear", "grape");List<String> filteredList = list.stream().filter(s -> s.length() >= 5).collect(Collectors.toList());
    

    在上面的代码中,我们首先将 List 转换为 Stream,然后使用 filter() 方法对元素进行过滤,最后使用 collect() 方法将过滤后的元素收集到一个新的 List 中。

  10. 用 Stream 怎么选出 List 里想要的数据?
    答:
    使用 Stream 可以通过 filter 方法来筛选出 List 中想要的数据,示例代码如下:

    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> filteredNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
    System.out.println(filteredNumbers); // 输出 [2, 4]
    

    在上述代码中,我们先定义了一个包含数字的 List,然后使用 stream()方法将其转换为一个 Stream 对象。接着使用 filter()方法对 Stream 中的元素进行筛选,筛选条件为 n % 2 == 0,即只选择偶数。最后使用 collect()方法将筛选出来的元素收集到一个新的 List 中。

  11. 说说 JDK15、JDK16、JDK17 中的新特性都有什么?
    答:
    以下是 JDK 15、JDK 16 和 JDK 17 中的一些主要新特性。请注意,这不是一个详尽无遗的列表,而仅仅是重要特性的概述。

    • JDK 15(2020 年 9 月发布)
      ① JEP 339: Edwards-Curve 数字签名算法 (EdDSA):提供了一个新的签名方案,支持高性能且更安全的加密。
      ② JEP 360: 封装类(Sealed Classes,预览):允许开发者限制某个类或接口的扩展,从而可以更清晰地表示其设计意图。
      ③ JEP 371: 隐藏类(Hidden Classes):允许在运行时生成类,并确保它们不会与其他类相互干扰,有助于框架的设计与实现。
      ④ JEP 372: 移除 Applet API:由于 Java Web Start 和相关浏览器插件已被废弃,因此移除了 Applet API。
      ⑤ JEP 378: 文本块(第二次预览):简化多行字符串字面量的编写,使代码更易读。

    • JDK 16(2021 年 3 月发布)
      ① JEP 338: 向量 API (Incubator):引入了一个新的向量 API,以提高硬件加速计算的性能。
      ② JEP 389: 外部内存访问 API (Incubator):提供了一种新的 API,用于访问不受 JVM 直接管理的内存,以提高性能和简化原生内存访问。
      ③ JEP 390: 警告使用不稳定的值类型:警告开发者使用值类型时可能遇到的潜在问题。
      ④ JEP 392: 将 JEP 389 的外部内存访问 API 引入 JDK 16,并将其作为一个孵化器模块发布。
      ⑤ JEP 393: 覆盖记录类的默认方法:允许在记录类中覆盖 equals()、hashCode() 和 toString() 等默认方法。
      ⑥ JEP 394: 模式匹配实例(第二次预览):引入了模式匹配和 instanceof 运算符的结合,简化了类型检查和类型转换的代码。

    • JDK 17(2021 年 9 月发布)
      ① JEP 356: 增强伪随机数生成器(PRNG)API:改进了伪随机数生成器,包括性能提升和更简洁的 API。
      ② JEP 382: 新 macOS 渲染管道:为 macOS 提供了一个新的 Metal 渲染管道,以替代已废弃的 Quartz 渲染管道。
      ③ JEP 389: 外部内存访问 API(第二次孵化):对在 JDK 16 中引入的外部内存访问 API 进行了改进。
      ④ JEP 411: 废弃实验性的 AOT 和 JIT 编译器:废弃了实验性的 ahead-of-time 编译器和 just-in-time 编译器。
      ⑤ JEP 391: macOS/AArch64 端口:为 macOS 上的 ARM64 架构提供了一个原生端口。
      ⑥ JEP 390: 警告使用不稳定的值类型:警告开发者使用值类型时可能遇到的潜在问题。
      ⑦ JEP 395: 封装类和接口(正式版):正式引入了 JDK 15 预览版中的封装类和接口特性。
      ⑧ JEP 398: 移除 Applet API:正式从 JDK 中移除已废弃的 Applet API。

    这些新特性在不同程度上改进了 Java 的性能、安全性、易用性和功能。请务必查阅 JDK 发行说明以获取更详细的信息和其他较小的改进。

Java 基础 第3阶段:高级应用——尚硅谷学习笔记(含面试题) 2023年相关推荐

  1. B站MySQL(尚硅谷)学习笔记

    B站MySQL基础(尚硅谷)学习笔记 最近在学习数据库技术,并且把视频中的知识点进行了汇总,字数较多,仅供参考. 会持续更新 欢迎读者提出问题与错误,一起交流~ 视频前几集所讲述的基本知识: DB:数 ...

  2. Java基础(四)(根据尚硅谷宋红康老师Java基础第四部分)(重点)

    Java面向对象编程(中) 主要讲解宋老师里面课后题(无答案的),以便后期复习使用.(可以私信我要文档资料 简答题 Object类的使用说明: 为什么要重写toString()方法? 在Object类 ...

  3. 8-zookeeper算法基础(尚硅谷学习笔记)

    目录 拜占庭将军问题 paxos算法 paxos算法流程 情况一 情况二 情况1 情况2 ZAB协议 ZAB协议内容 消息广播 过程 可能出现的问题 崩溃恢复 异常假设 leader选举 数据恢复 C ...

  4. JUC 2020 周阳 尚硅谷 学习笔记

    这里写目录标题 一 JUC 介绍 1 进程线程介绍介绍 2 并发并行的介绍 3 wait 和 sleep的区别 4 线程的状态 二 卖票算法的企业级模板实现 企业级简单实现(synchronized) ...

  5. 尚硅谷学习笔记——Java8新特性

    2014年3月发布 公司往往不是追求技术的新,而且追求技术的稳定.所以大多用的之前的版本. Java 8 新特性简介 速度更快 修改底层数据结构:如HashMap(数组-链表-红黑树),HashSet ...

  6. [React] 尚硅谷 -- 学习笔记(二)

    第二章 React面向组件编程 基本理解和使用 自定义组件 定义组件 工厂函数组件(简单组件) function MyComponent () {return <h2>工厂函数组件(简单组 ...

  7. SpringBoot(尚硅谷学习笔记)

    1.SpringBoot优点(官网 spring.io) Create stand-alone Spring applications 创建独立Spring应用 Embed Tomcat, Jetty ...

  8. Nginx尚硅谷学习笔记

    Nginx 1.Nginx是什么 1.1 反向代理 1.2 负载均衡 1.3 动静分离 2. niginx安装.常用命令和配置文件 2.1 linux系统中安装nginx 2.2 nginx常用命令 ...

  9. 尚硅谷学习笔记-节点的常用属性和方法

    节点的常用属性和方法[图片在末尾] 方法: 通过具体的元素节点调用 getElementsByTagName() 方法,获取当前节点的指定标签名孩子节点 appendChild( oChildNode ...

最新文章

  1. ThinkPHP 3.2 支付宝即时到账接口开发
  2. linux统计某个目录大小,Linux下统计某个目录的文件个数(转)
  3. Linux 用户管理命令笔记
  4. python实现RSA加密解密 及 签名验签功能
  5. Effective C# 原则33:限制类型的访问(译)
  6. 著名程序员 Eric S. Raymond :用 SaaS 是一种危险的愚蠢行为
  7. 与技术无关,犯错了,希望大家帮下
  8. UVA 2519 Radar Installtion
  9. 【python】-- 类的继承(新式类/经典类)、多态
  10. 两条平行导线同向电流_如何根据功率计算电流?老师傅说这么做很简单
  11. QGIS编译---QGIS3.10.6 + Qt5.11.2 + VS2015 ---32位版本
  12. html样式类collapse,[ CSS-CSS3 ] 史上最全CSS样式一览表
  13. 快速解决“多分类不平衡”问题
  14. 远程电脑打游戏鼠标漂移无法操作解决办法
  15. Musixtex写五线谱
  16. python中的函数及面向对象的知识点
  17. MyBatis基础篇
  18. 115浏览器 for mac(多功能网页浏览器)
  19. html5立体,Html5--立体文字
  20. 20171693测绘工程三班钱慧亚 结构体笔记

热门文章

  1. 创建hive的AES加密解密函数
  2. 教你如何在Sco Unix5.05安装大硬盘(启动输入硬盘参数方法)
  3. greys在线问题诊断工具
  4. 【H5/JS】游戏常用算法-碰撞检测-包围盒检测算法(2)-矩形
  5. Facebook大数据技术架构的演进路线
  6. 产品经理 - 路漫漫其修远兮
  7. ftp匿名登录 漏洞复现
  8. kbhit()函数用法
  9. 超强的苹果官网滚动文字特效实现
  10. unity3D数据的本地存储(xml的方式)