Java核心技术 读书笔记

3.8.6 中断流程控制语句
3.9 大数
3.10 数组
3.10.1 for-each 循环
3.10.2 数组初始化 + 匿名数组
3.10.3 数组拷贝
3.10.4 命令行参数
3.10.5 数组排序
Arrays工具类
3.10.6 多维数组
3.10.7 不规则数组
4 对象与类
4.1 面向对象概述
4.1.1 类
4.1.4 类之间的关系
4.2 使用预定义的类
4.2.1 对象和对象变量
4.2.2 LocalDate类
4.2.3 访问器和修改器
4.3 用户自定义类
4.3.1 自定义类
4.3.4 构造器
4.3.5 隐式参数和显式参数
4.3.6 封装的优点
4.3.7 基于类的访问权限
4.3.9 final实例域
4.4 静态域和静态方法
4.4.1 静态域
4.4.2 静态变量
4.4.3 静态方法
4.4.4 工厂方法
4.4.5 main方法
4.5 方法参数
4.6 对象构造
4.6.1 重载
4.6.2 默认域初始化
4.6.3 无参数的构造器
4.6.4 显式域初始化
4.6.6 调用另外的一个构造器
4.6.7 初始化块
4.7 包
4.7.1 类的导入
4.7.3 将类放在包中(文件结构)
4.8 类路径
4.8.1 如何设置类路径
4.9 文档注释
4.10 类设计技巧
5 继承
5.1 类,超类和子类
5.1.1 子类
5.1.2 覆盖方法
5.1.3 子类构造器
5.1.4 继承层次
5.1.5 多态
5.1.6 理解方法的调用
绑定
5.1.7 阻止继承:final类和方法
内联
5.1.8 强制类型转换
5.1.9 抽象类
5.1.10 受保护的访问
5.2 Object类
5.2.1 equals方法
5.2.2 继承和相等测试
5.2.3 hashCode方法
5.2.4 toString 方法
5.3 泛型数组列表
5.3.1 访问数组列表中的元素
5.4 自动装箱和自动拆箱
5.5 参数数量可变的方法
5.6 枚举类
5.6.1 什么是枚举类
5.6.2 抽象枚举类
5.6.3 枚举的使用场景
5.7 反射
动态代理
动态代理的实现
反射
什么是反射?
获取class对象
从字节码文件中获取构造方法
从字节码文件中获取成员变量
从字节码文件中获取成员方法
6 接口,lambda表达式和内部类
6.1 接口
6.1.1 接口的概念
6.1.2 接口的特性
6.1.3 接口和抽象类
6.1.4 静态方法和默认实现
6.1.5 默认方法冲突
6.2 接口示例
6.2.1 Comparator接口
6.2.2 Cloneable接口
6.3 lambda表达式
6.3.1 lambda表达式的写法
6.3.2 lambda表达式更近一步的简化
6.3.3 方法引用
静态方法引用
实例方法引用
特定类型的方法引用
构造器引用(非常少用)
6.4 内部类
6.4.1 成员内部类
6.4.2 静态内部类
6.4.3 局部内部类
6.4.3 匿名内部类(重要)
8 Java 泛型
8.1 泛型是什么
8.2 泛型类
8.3 泛型接口
8.4 泛型方法
8.5 泛型的注意事项
8.5.1 泛型擦除
8.5.2 不可作用于基本类型
9 常用API
9.1 Object类型
9.1.1 equals方法
9.1.2 clone方法
9.1.3 toStirng方法
9.2 Objects类型
9.2.1 equals方法
9.2.2 isNull和nonNull方法
9.3 wrap-up类
9.3.1 获得包装类的途径
9.3.2 包装类提供的方法
9.4 StringBuilder类
9.4.1 常用方法
9.4.2 StringBuffer
9.4.3 StringJoinner
9.5 BigDecimal类
9.5.1 构造方法
9.5.2 运算方法
9.6 Date类
9.6.1 构造方法
9.6.2 操作时间的方法
9.7 SimpleDateFormat类
9.7.1 构造方法
9.7.2 修改Date对象格式的方法
9.8 Calendar类
9.8.1 提供的日历方法
9.8.2 JDK8之前的时间类
9.9 Arrays类
toString方法
copyOfRange方法
copyOf方法
setAll方法
sort方法
9.10 正则表达式
9.10.1 正则表达式的特殊字符
10 异常
10.1 异常的体系结构
10.2 异常的作用
10.3 异常的处理
10.3.1 JVM自己处理
10.3.2 自定义处理的方法
try-catch 的灵魂四问
10.3.3 抛出异常处理
什么时候去抛出异常,什么时候去捕获异常?
11.4 自定义异常
11.4.1 Lead in
11.4.2 自定义异常的方法
11.4.3 异常类中常用的方法
getMessage方法
toString方法
printStackTrace方法
11 集合
11.1 Collection集合接口
11.1.1 Collection集合的结构
11.1.2 Collection类中的常用方法
add方法
clear方法
contains方法
isEmpty方法
remove方法
size方法
toArray方法
addAll方法
11.1.3 Collection集合的遍历方法
迭代器
增强for
Lambda表达式
11.1.4 集合存储对象的原理
11.2 List集合接口
11.2.1 List集合的结构
11.2.2 List集合增加的方法
add方法(特定下标添加)
remove方法(特定下标删除)
set方法(特定下标修改)
get方法(返回特定下标元素)
11.2.3 List类型的遍历方法
普通for
迭代器
增强for
lambda
11.2.4 ArrayList实现类

  1. ArrayList底层实现
  2. ArrayList的工作流程
  3. ArrayList集合的适用场景
    11.2.5 LinkedList实现类
  4. LinkedList 底层实现
  5. LinkedList的双向链表
  6. LinkedList的特点
  7. LinkedList的应用场景
    可以用来设计队列
    可以用来设计栈
    11.3 Set集合接口
    11.3.1 Set集合的结构
    11.3.2 Set集合增加的方法
    11.3.3 HashSet实现类
  8. HashSet的底层原理
  9. HashSet的特点
  10. HashSet的隐患
    11.3.4 LinkedHashSet实现类
    LinkedHashSet的底层原理
    11.3.4 TreeSet
  11. TreeSet的底层原理
  12. 自定义类型的TreeSet集合
    11.4 不同的集合的适用场景
    11.5 集合的并发修改异常
    11.6 Collections工具类
    11.6.1 可变参数
    11.6.2 Collections工具类
    addAll方法
    Shuffle方法
    sort方法
    11.7 Map集合接口
    11.7.1 Map集合的体系结构
    11.7.2 Map集合中常用的方法
    size
    put
    clear
    keySet
    values
    get
    remove
    containsKey
    containsValue
    11.7.3 Map的遍历方式
    11.7.4 HashMap
    11.7.5 LinkedHashMap
    11.7.6 TreeMap
    11.8 集合的嵌套
    12 Stream流
    12.1 认识Stream流
    12.2 Stream流的一般使用过程
    12.3 Stream流的常用方法
    12.3.1 获取Stream的常用方法
    12.3.2 Stream中常见的中间方法
    filter 过滤
    sorted 排序
    limit 前N个元素
    skip 跳过N个元素
    distinct 去重
    map 加工
    concat 合并
    12.3.3 Stream中常见的终结方法
    forEach 方法
    count 方法
    max 方法
    min 方法
    用来收集的终结方法
    collect 方法
    toArray 方法
    13 方法引用
    13.1 方法引用的条件
    13.2 方法引用的使用
    13.2.1 静态方法的方法引用
    13.2.2 成员方法的方法引用
    13.2.3 父类的成员方法的方法引用
    13.2.4 本类中的成员方法的方法引用
    13.2.5 构造方法的方法引用
    13.2.6 类名引用成员方法(非静态方法)
    13.2.7 数组的构造方法引用
    13.3 方法引用的总结
    14 文件
    14.1 File的概述和构造方法
    14.1.1 File的概述
    14.1.2 File的构造方法
    根据文件路径创建文件对象
    根据File父路径和String子路径创建
    根据String父路径和String子路径进行创建
    14.2 File类中常见的成员方法
    14.2.1 判断和获取
    对文件类型进行判断
    获取文件的基本信息
    文件的长度
    文件的绝对路径
    文件的定义路径
    文件的名字
    文件的最后修改时间
    14.2.2 文件的创建和删除
    createNewFile 创建一个新的空文件
    mkdir 创建单级文件夹
    mkdirs 创建多级文件夹
    delete 删除文件,空文件夹
    14.2.3 文件的获取并遍历
    listRoots 列出文件系统中所有的盘符
    list 获取当前路径下的所有内容
    list(FilenameFliter filter) 按照过滤器获取当前路径下的内容
    listFiles 获取当前路径下的所有内容
    listFiles(FilenameFliter filter) 按照过滤器获取当前路径下的内容
    listFiles(FileFilter filter)按照过滤器获取当前路径下的内容
    14.2.4 四个经典问题
    在当前目录下的一个未知目录下创建一个txt文件
    判断当前目录下有没有以txt结尾的文件(不考虑子文件夹)
    删除当前目录下所有的文件(考虑子文件夹)
    统计一个文件夹的总大小(考虑子文件夹)
    统计当前目录下不同类型的文件的数量(包含所有的嵌套文件夹)
    14.3 IO流
    14.3.1 IO流的概述
    14.3.2 IO流的分类
    14.3.3 IO流的体系结构
    14.3.4 字节输出流
    FOS的基本原理
    创建对象的细节
    路径的传递
    路径是否存在
    追加模式标志
    写入数据的细节
    三种写入数据的方式
    换行
    续写
    关闭FOS的细节
    14.3.5 字节输入流
    创建FIS对象的细节
    读取数据的细节
    FIS提供了两种读取数据的方法
    使用FIS进行循环读取
    文件拷贝(流关闭顺序)
    使用带参的read方法进行读取
    14.3.6 IO流的异常捕获处理
    手写异常处理流程
    JDK7 try-catch的简化
    JDK9 try-catch的简化
    14.3.7 字符集详解
    Ascii英文字符集
    Ascii字符集的存储规则
    中文字符集发展
    GBK字符集的存储规则
    Unicode字符集的存储规则
    为什么会有乱码?
    14.3.8 Java中的字符编码和解码
    编码
    解码
    14.3.9 字符输入流
    FileReader
    FileReader的基本使用流程
    read方法
    底层原理
    空参的read方法
    带参的read方法
    14.3.10 字符输出流
    FileWriter的使用流程
    创建FileWriter
    写文件
    关闭流
    字符输出流的底层原理
    字符输入流 FileReader的原理
    字符输出流 FileWriter的原理
    14.3.11 字节流和字符流的使用场景
    拷贝文件夹中的所有文件(考虑子文件夹)
    14.3.12 缓冲流(BIO)
    缓冲流的体系结构
    字节缓冲流
    字节缓冲区的底层原理
    字节缓冲流的使用
    创建字节缓冲流对象
    字节缓冲流的读写
    字节缓冲流的关闭
    字符缓冲流
    readLine方法
    newLine方法
    缓冲流的总结
    缓冲流的分类
    缓冲流为什么可以提升性能。
    字符缓冲流提供的新方法
    使用缓冲流对文件中的数据进行重新排序

学习路线图

https://www.bilibili.com/read/cv24091867/

3.8.6 中断流程控制语句
break label

虽然,很多人不喜欢goto语句,但是实际上,适当使用goto语句反而有益于程序的设计风格

java将goto语句作为保留字,同时设计了一种break语句来致敬goto语句 break label语句

label:{
while(…) {

break label;
}
}
当执行break语句的时候,其会跳出整个label标签所指代的代码块

也就是,break label可以一次性跳出一整个代码块,而不是像普通break一样只跳出一层

实际上,break label语句,并不一定用在循环结构中,只要是想跳出代码块就都可以使用break label语法,即使是在if语句中

label:{
if(…) {

break label;
}
}
注意,break label语句只能从一个语句块中跳出来,而不能跳入一个语句块中

continue

continue语句也会中断正常的控制流程。其会将控制转移到最内层循环的首部。

continue语句也有一种带标签的写法。其可以跳到与标签匹配的循环的首部

3.9 大数
java.math 包中有两个有用的类:

BigInteger类:

可以实现任意精度的整数运算

BigDecimal类:

可以实现任意精度的浮点数运算

上述的两个类,对数字的长度都没有要求(随便给多长都可以)

这两个类都提供了静态方法 valueOf() 可以将普通的字面值转换为大数类型

BigInteger a = BigInteger.valueOf(100);
但是非常可惜,java中没有提供C++类似的运算符号重载功能(只对String类型设计了+连接运算),不可以使用 +,*等运算符号作用在实例上,只能使用类中定义好的add和multiply等方法。

BigInteger a = BigInteger.valueOf(100);
a = a.add(BigInteger.valueOf(2)).divide(BigInteger.valueOf(2));
// a = (a + 2) / 3
// 连续使用.运算符去多次连续调用方法? 链式编程
提供的方法有:

BigInteger add(BigInteger other) +

BigInteger subtract(BigInteger other) -

BigInteger multiply(BigInteger other) *

BigInteger divide(BigInteger other) /

BigInteger mod(BigInteger other) %

int compareTo(BigInteger other) 提供了一个比较方法,等于返回0,大于返回+,小于返回-

static BigInteger valueOf(long x) 返回值等于x的一个大整数类的实例

BigDecimal类中提供的方法大体相似,但是,mod方法必须给定舍入方式作为第二个参数,RoundingMode.HALF_UP就是常见的四舍五入的方法

3.10 数组
数组用来存放相同类型的数据

声明一个数组变量:

int []a;
int a[];
// 这两种方法都可以
为数组变量分配一个内存中的数组:

int []a = new int[100];
// 为数组a分配一个可以存放100个int数据的空间
数组的下标还是从0开始,如果访问a[100]会出现边界错误

数组一旦被创建,其中的元素都会被初始化:

数字类型:元素会被初始化为0

boolean类型:元素被初始化为false

对象类型:元素被初始化为null,表示现在没有实例被分配

数组一旦被创建,就不可以改变其长度,如果想要动态改变数组的长度,应该使用数组列表(Array List),可以使用 .length 属性来获得一个数组的长度

3.10.1 for-each 循环
for-each循环可以不用使用下标来访问一个容器中的所有元素

语法:

for(type variable : collection) statement
其中,variable是用来暂时存放collection中的元素,collection是一个数组,或者,一个实现了Iterable的接口的类对象(Array List)也可以

3.10.2 数组初始化 + 匿名数组
可以在使用一种简化的语法来在创建一个数组对象的同时赋予初始值

// 先创建一个变量
int []a = new int[100];
// 再对变量进行初始化

// 在创建变量的同时就对其进行初始化
int []a = {1,2,4};
在这种语法中,是不需要使用new的,在创建一个数组的同时,使用花括号中的值对数组进行初始化

Java中的[]的符号和C++不同,其被定义用来判断数组的边界。

需要注意的是,java中是允许数组的长度为0的。但是长度为0的数组不等于null。类类型变量的值为null说明,这个变量没有被分配实例。但是这里是分配了实例的,只是这个实例没有占用内存。

也可以创建一个匿名数组。

(我感觉,就是,之前必须先创建一个对象,再对对象进行初始化,现在可以在不指明所分配对象的前提下,先把内存分配好,同时初始化)

new int[]{1,2,3};
// 可以用来重新初始化一个数组(其实本质上是重新分配一个)
a = new int[]{1,2,3,5}
Java中,声明和定义的数组类似于C++中的堆栈:

int* a = new int[100];
// 这样定义的数组,内存是分配在heap(堆)上面的
int a[100];
// 这样定义的数字,内存是分配在stack(栈)上面的
// 注意啊,这两种方式都不会对元素进行初始化
这个地方可以再提一下C++中的动态数组和静态数组

静态数组在栈上创建,同时具有自动存储期期限。不需要我人为去管理数组的内存。他们在自己的作用域外就会被销毁。同时静态数组在编译的时候会固定其大小,之后就不能再改变了。

int array[100];
动态数组具有人为可控的存储时间,存储在堆上。他们可以在任何时间被以任何大小创建(空间够的话)。但是我必须人为去在内存中定位其存储的空间,并且要手动去删除它。

int* array = new int[100];
3.10.3 数组拷贝
如果直接将数组进行相互赋值,实际上,相当于只是将两个不同的指针指向了相同的一块内存区域(浅复制)

int []a = new int[]{1,2,3,4};
int []b = a;
b[0] = 3;
// 那么,a中的第一个元素也会变成3
如果期望的复制方法是深复制,那么,就可以使用Arrays类中的方法copyOf(按元素复制,深复制)

int []a = Arrays.copyOf(a, size);
// 第一个参数是被复制的数组
// 第二个参数是复制多少个元素,如果size小于b的size,b中多余的元素会被初始化,如果大于,则只复制前面的元素
3.10.4 命令行参数
Java中的main函数的参数列表中有 String args[] 这个参数,其就是用来接受命令行中传入的参数的。

public class Message{
public static void main(String args[]) {
System.out.print(args[0]);
}
}

// java Message Hello 就会在命令行中显示Hello
和C++中不同的是,C++的命令行参数会在第一个下标的地方额外存放一下程序的名字。

Java不会存储额外的程序的名字,第一个下标就是第一个输入的参数。

3.10.5 数组排序
对数值类型的数组可以用Arrays类中的sort方法进行排序(一个静态方法)。

int []a = new int[1000];
Arrays.sort(a);
那就是,相当于,Java中的数组传入的时候都是给的引用,所以sort方法中,对a的操作会直接体现的方面外面的a上。

Math.random方法可以返回一个从0到1之间的随机浮点数

Arrays工具类
static String toString(type[] a)

static String copyOf(type[] a)

static String copyOfRange(type[] a)

static String sort(type[] a)

static String binarySearch(type[] a, type v)

找到了就返回相应的下标值,否则就返回一个负数

static String binarySearch(type[] a, int start, int end, type v)

在一个给定的范围查找,找到了就返回相应的下标值,否则就返回一个负数r,-r-1就是如果这个数字应该在数组中出现的话,其出现的位置的下标

static void fill(type[] a, type v)

将a中的所有的元素干成v

static boolean equals(type[] a, type[] b)

检查两个数组是否相同(对应下表的元素全部都相同)

3.10.6 多维数组
语法:

// 声明一个二维数组
double [][]a;
注意,这样只是声明了一个二维数组,其还没有被分配具体的内存空间,不可以直接使用。

初始化:

a = new double[X][Y];
也可以用一些数值来对数据进行初始化(语法更加简洁)

double [][]a = {{1,2}, {3,4}};
一旦数组被初始化,就可以用两个下标来访问多维数组中的元素

System.out.print(a[1][1]);
对于一个二维数组,其中的元素是一个一维数组,所以,对一个二维数组使用for each循环,只能遍历每个行,但是如果还想要遍历每个元素,就必须再嵌套一个for each循环去遍历行中的每个元素。

for(double []t : a) {
for(double T : t) {
System.out.print(a);
}
}
使用Arrays.deepToString()方法可以快速将一个二维数组打印为列表形式。

3.10.7 不规则数组
实际上,Java中的是没有多维数组的机制的,其只有一维数组,多维数组被解释为数组的数组。

Java中二维数组的行是可以交换的。

double[] temp = a[1];
a[1] = a[2];
a[2] = temp;
Java中二维数组也不要求行的规模是一样的。

// 创建一个有10行的二维数组
double [][]temp = new double[10][];

for(int i = 0; i < 10; i ++) {
// 为每一个一维数组分配空间
temp[i] = new double[i + 1];
}
数组这块其实和C++还蛮不同的。

// Java
double [][]temp = new double[2][4];
// C++
// C++在定义一个数组的时候实际上是不需要人为去申请空间的(定义了一个2行4列的数组)
double temp[2][4];

// 这一句话,我声明的是不知道具体个数的double类型的指针,但是每个指针都要求分配的长度为4(C++要求列必须被指定)
// 然后在具体分配的时候,我为2个这样的指针分配了空间
double (*temp)[4] = new double[2][4];

// 这种写法才类似于Java中的定义
// 我要的是一个double类型的二级指针,我分配的时候为这个二级指针分配了两个double类型的指针
double *temp = new (double)[2];
// 再为这两个double类型的指针去具体分配内存
for(int i = 0; i < 2; i ++) {
temp[i] = new double[4];
}
4 对象与类
4.1 面向对象概述
面向对象编程OOP是现在的主流,Java是完全面向对象的。

OOP由对象构成,每个对象中有对用户公开的特定功能部分,也有隐藏的实现部分。

结构化编程是先考虑算法的设计,再去考虑数据的存储方式。

OOP则是先考虑数据的存储方式,再考虑操作数据的算法。

4.1.1 类
类是构造对象的模板或者蓝图。

由类构造的对象的过程叫做创建类的实例。

对象中的数据叫做实例域,操纵数据的过程是方法。

封装(数据隐藏)将数据和方法组合在一起,对用户隐藏数据的实现方式。封装的关键在于,一个类中的方法,只能访问自己的实例域。程序只能通过类提供的方法实现与对象数据的交互。封装提高了重用性和可靠性。

继承让用户自定义类变得轻松。可以通过扩展一个类来建立另外一个类。

4.1.4 类之间的关系
依赖:(uses-a)

如果一个类中的方法操纵另外一个类的对象, 就说这个类依赖于另一个类。

类之间存在的依赖关系应该被减少,意味着类B的改变不会影响到类A,降低类之间的耦合程度。

聚合:(has-a)

如果类A中的数据中含有类B的实例,那么就说A和B具有聚合关系。

继承:(is-a)

如果类A是类B的更加特殊的形式,那么类A就可以从类B中继承而来。

4.2 使用预定义的类
使用Java中预先定义的Data类来介绍类的使用方法。

4.2.1 对象和对象变量
使用对象之前,必须首先构造对象,并且指定其初始状态。

使用构造器构造新的实例。构造器是一种特殊的方法,用来构造和初始化对象。

构造一个对象的时候,使用new操作符作用在构造方法前。

// 构造了一个Date对象,同时构造方法对对象内的数据进行初始化
new Date();

// new Date()这个整体可以视为就是一个对象的实例,可以将其作为参数传递,也可以调用其中的方法
Sysetm.out.print(new Date()); // 自动调用toString方法
String s = new Date.toString(); // 调用方法

// 也可以将对象传递给一个对象变量
Date a = new Date();
需要注意的是,对象变量只是一个引用,其并不包含一个对象的内容。

对象变量中的值只是存储在另外一个地方的对象的一个引用。(new返回的内容也是一个引用)

也可以将对象变量的值设置为null,来表示这个变量没有被分配任何的实例。

局部变量不会被自动初始化为null。(方法内所定义的变量叫做局部变量)

在使用一个对象变量之前,必须对着对象变量先初始化。

可以用一个新的实例化的对象(new)来初始化这个对象变量。

也可以使用已经存在的对象来初始化这个对象变量。	

实际上,Java中的对象变量类似于C++中的对象指针。

和C++中的引用不同的是,C++的引用必须在定义的时候被赋值,且一旦被定义,就不可以改变引用绑定的内存。

4.2.2 LocalDate类
Date类本身对时间的表达是用与纪元相差的毫秒数来表示。

Date可以用来表示时间,但是其用来表示日历就非常鸡肋。

使用LocalDate类来表示日历。

不使用构造器来创建LocalDate类对象,使用其提供的静态方法来创建。

LocalDate.now();
// 创建一个以当前时间初始化的LocalDate类对象
LocalDate.of(1999,12,31);
// 用年月日的格式来创建一个以特定时间初始化的对象
使用LocalDate类的好处就在于,其提供的方法很容易就可以预测时间。

LocalDate now = LocalDate.of(1999,12,31).plusDays(1000);
// 这样就可以创建一个2002.9.26的对象
// 不需要像Date对象那样还需要去自己推导
LocalDate类中所提供的接口API。

4fd54aa4c4215ffdb4cc623d537eb32

4.2.3 访问器和修改器
访问器方法,只访问对象而不修改对象属性的方法就称为访问器方法。

C++中,后缀为const的方法是访问器方法,否则,默认为修改器方法。Java中是没有这种明显的语法区别的。

4.3 用户自定义类
现在介绍如何设计复杂应用程序所需要的各种主力类。

这些类没有main方法,但是有自己的数据域和实例方法(实例方法不是静态方法)。这些类自己不能运行,通过设计的目的是被调用。

一个完整的程序一般是,将许多的类组合在一起,其中只有一个类具有main方法。

4.3.1 自定义类
一般来说,主力类是用来实现复杂的计算功能,这些类一般没有main方法,但是都有自己的实例域和实例方法。

一个完整的程序一般都会含有很多个类,但是其中只有一个类含有main方法。(主类)

java中一般对类的定义方法如下:

class ClassName {
field1;
field2;

constructor1;
constructor2;

method1;
method2;

}

一般来说,用public访问修饰符修饰的类就是整个文件中的主类,而main函数就在主类中,同时整个文件的名字应该和主类的类名相同。而且提供给javac命令进行编译的类名应该也是主类的类名,且不会带上.java的扩展名(即使带上也无所谓)。

public访问修饰符表示任何类的任何方法都可以访问被修饰的东西(方法,变量)。

private访问修饰符则表示只有该类本身的方法可以访问这些被修饰的东西。

javac命令后面的文件名字是支持采用通配符进行匹配。比如,现在有两个文件是AC.java,AB.java,可以使用如下的命令来同时编译这两个文件为类文件。

javac A*.java
如果在AB.java中包含了AC.java中的类,那么还可以使用 javac AB.java 来同时编译这两个文件。当javac编译AB的时候,如果遇到了AC中的类,会去查找AC的已经被编译为.class的AB文件,如果没有找到的话,就会去搜索AB的.java文件,然后编译这个文件。

类的实例域中含有其他类的类类型实例是非常常见的情况。

现在定义一个本章中经常使用的类,Employee类作为程序的例程。

// EmployeeTest.java
public class EmployeeTest {
public static void main(String[] args) {
// operations as below
}
}

class Employee {
private String name;
private double salary;
private LocaData hireDay;

public Employee(String n, double s, int year, int month, int day) {
    name = n;
    salary = s;
    hireDay = locaDate.of(year, month, day);
}

public String getName() {
    return name;
}

public double getSalary() {
    return salary;
}

public LocaDate getHireDay() {
    return hireDay;
}

public void raiseSalary(double byPercent) {
    double raise = salary * byPercent / 100;
    salary += raise;
}

}
4.3.4 构造器
现在为一个Employee类定义一个构造器。

public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
hireDay = locaDate.of(year, month, day);
}
可以看出:

构造器的名字必须和类名相同。

每个类可以含有不止一个的构造器。

构造器的参数可以没有,也可以有很多个。

构造器没有返回值。

在使用new操作符生成一个实例的时候,构造器会被自动同时调用来初始化实例域中的变量。

需要注意的是

Java中去生成一个类的实例的时候,必须使用new操作符,但是C++不用,C++可以直接用类名作为变量类型来声明这样一个实例。

构造器不能对一个已经存在的对象再次调用。

在构造器的函数中,不可以定义跟类中实例域中相同的变量。这些被定义的变量,只能在构造器的内部访问,是无法给到类中的实例域的。

4.3.5 隐式参数和显式参数
隐式参数是在方法名前的本类对象(this,用来指代本类,方法可以访问类中的实例和其他方法的本质在于,方法中默认传入了this这个隐式参数)。

显式参数是通过在方法名后的括号中的内容进行传入。

public void raiseSalary(double byPercent) {
    double raise = salary * byPercent / 100;
    salary += raise;
}

这段代码其实等同于:

public void raiseSalary(double byPercent) {
    double raise = this.salary * byPercent / 100;
    this.salary += raise;
}

使用this参数就可以非常清楚地将实例域的变量和局部变量非常明显地进行区分。

也使得在方法中定义跟实力域中的变量同名的变量成为可能(更常用的其实是在方法的参数中去定义同名的变量)

在C++中,方法可以在类中进行声明,但是在类外进行定义,如果在类中定义的方法,C++会自动将其成为inline函数。

但是在Java中,方法只能在类中进行声明和定义,而且Java中的方法是否会变成inline方法是Java虚拟机所决定的。

4.3.6 封装的优点
一般来说,类中对于数据部分需要提供以下的几个部分:

一个私有的数据域。

一个公有的数据域访问器。

一个公有的数据域修改器。

使用访问器和修改器的好处比起提供一个公有的数据域非常明显:

可以改变方法的内部实现,不会影响到其他的代码。

修改器可以对赋值进行检查,而直接对公有数据域赋值则没有这种检查。

注意,不要返回一个可变对象的引用!

public LocaDate getHireDay() {
return hireDay;
}
这个方法中,返回一个LocalDate对象。

虽然,LocalDate没有提供修改器,但是如果,使用的是Date作为返回的类型的话,Date是可以修改的,这样就会导致,在外面接受这个方法所返回的Date实例的时候,外面的接受变量会指向类中Date变量相同的内存区域,那么,就可以通过外面的接受变量来修改类中的private实例域。

image-20230803172405706

public Date getHireDay() {
return hireDay;
}

Date D = Harry.getHireDay();

// 通过对调用D中的方法,就可以修改类中的private变量的值
// 这个D和类中的hireDay都指向了同一块内存区域
D.setTime(1500);
因此,如果要返回一个可变对象,不应该直接返回其的引用,应该返回其的克隆(将这个对象的实例在另外一个内存区域保存一个副本)。

一般来说,类应该都会提供一个克隆的接口。

public Date getHireDay() {
return (Date)hireDay.clone();
}
4.3.7 基于类的访问权限
简而言之,一个类中所定义的方法,可以访问这个类中的所有数据。

如果,传入这个方法的参数也是同属于这个类的一个实例,那么,方法同样可以访问传入的实例中的所有数据(包括private数据)。

public boolean equals(Employee other) {
return this.name.equals(other.name)
}
在上述的代码中,方法equals访问了other中的私有数据,因为方法equals同样属于Employee类,所以可以。

C++中同样拥有这种属性。

4.3.9 final实例域
可以将实例域中的变量用final进行修饰。

被final修饰的实例域变量无法被修改,因此在构造器中,必须对final变量进行初始化。

一般来说,final修饰的都是基本类型域和不可变类(不可变对象指的是方法不会对对象本身产生影响的对象,String对象就是一个典型的不可变类)。

需要注意的是,如果final修饰了一个可变类的对象:

private final StringBuilder test;
final修饰了test,test是一个StringBuilder类的对象实例,是一个引用类型,那么test不能再被修改,也就是说,test不能再指向一个另外的StringBuilder对象,但是test所指向的内存区域还是可以被修改的。

public void doTest() {
test.append(“Modified!”);
}
4.4 静态域和静态方法
4.4.1 静态域
如果将类中的域(成员变量)定义为static,那么,在每个类中都只有一个这样的变量,这个了类的所有对象都是这个变量的引用。

相当于,static变量单独在内存中的某一块进行存放,每个对象中的这个static实例化后的变量都指向了同一片内存区域。换句话说,就是所有的对象都共享了同一个这个static变量的内存区域。

4.4.2 静态变量
一般来说,静态的变量使用较少,能想到就只有类的实例计数器,静态的常量使用较多(类似于数学中各种常量的定义,PI)。

每个类对象都可以将类的公有域进行修改,所以最好不要把域设计为public。

4.4.3 静态方法
静态方法不能被作用在对象上。

可以认为,静态方法中,不会被默认传入this指针作为隐藏的参数。(类中的方法可以访问类中所有的域就是因为传入了this),因此,静态方法对于类中的域的可访问性就受到了很大的限制。

静态方法无法访问类中定义的实例域,只能访问同为static定义的静态域。但是,实例方法却可以自由访问静态域。

注意,最好是采用类去调用静态方法,而不要用对象去调用。因为,静态方法本质上和对象无关,只和类有关,用对象调用会非常迷惑。

public class static_test {
// 静态域
static int a;
// 实例域
int b;

// 静态方法只能访问静态域中的变量
public static int getA() {
    return a;
}

// 实例方法可以访问类中的静态域和实例域中的所有变量
public int getA_B(int flag) {
 	if(flag == 1)
    	return a;
    else
        return b;
}

}
什么时候适合去定义一个静态方法:

这个方法和类中的状态(实例域)无关,方法所需要的一切参数都由方法的参数显式提供。

这个方法即使需要访问类中的域,也只需要访问类中的静态域。

4.4.4 工厂方法
静态方法还有一个作用是用来和构造器进行互补。实际上就是,构造器只有一个,所提供的初始化对象的方式是非常有限的,所以我可以采用静态方法来作为另外一种构造器,返回不同风格的格式化对象。

比如NumberFormat类中使用工厂方法来生成不同风格的格式化对象。

NumberFormat current = NUmberFormat.getCurrencyInsatnce(); // 获得以百分比格式化的对象
NumberFormat percent = NUmberFormat.getPercentInsatnce(); // 获得以百分率格式化的对象
工厂方法有趣的地方在于,它可以做到一些构造器做不到的事情。

构造器无法改变返回的类的类型,比如一个A类的构造器只能得到A类的对象,但是我用工厂方法可以改变返回的类型。

NumberFormat current = NUmberFormat.getCurrencyInsatnce(); // 获得以百分比格式化的对象
调用这个方法可以返回一个DecimalFormat的类型。DecimalFormat是NumberFomat的子类。

构造器的名字只能跟类的名字相同。但是会希望,不同风格的同一个类的对象有不同的名字。

4.4.5 main方法
main方法非常明显是一个静态方法。

因此,main方法不需要从对象中进行调用。实际上在运行main方法的时候,还没有任何的对象被实例化出来。

静态的main方法将执行并且创建所需要的对象。

有一个调试的小技巧就是,可以在每一个类中塞一个main函数,来查看对象中的状态。

public class test{
public static void main(Strings[] args) {
// …
}
}
如果想要独立地测试test类的话,可以运行

java test
但是如果test类属于另外一个更大的类Test,则运行

java Test
的时候,test的main方法不会被运行。java 只会运行对应类中的main函数。

同一个文件内,如果有好几个类(主类和次类),运行主类的main函数可以使用java命令,但是我想要运行次类的main函数则怎么办?

注意,在运行java命令之前,必须先运行javac命令将.java文件进行编译。

实际上只要编译后,无论编译后的字节码文件的名字叫什么,只要里面的类中含有main函数,就可以用java命令进行调用。

image-20230808151445886

image-20230808151436235

4.5 方法参数
一般来说,为方法传入参数的方式有两种:

按值传递,表示传入方法的是值,是变量的一份拷贝。

按引用传递,表示传入方法的是引用,是变量的地址。

方法内,对按值传递的变量进行修改不会对方法外的变量有任何影响。

对按引用传递的变量,方法内一旦修改后,方法外的变量也会被修改。

在java中,方法采用的参数传入的方式都是按值传递。也就是说,方法中所获得总是传入参数的一份拷贝,也就是不能去修改传递给他的任何参数变量的内容。

在运行这段代码之后,percent不会有任何的改变。

double percent = 10;

public static void tri(double x) {
x = 3 * x;
}

tri(percent)
percent在传入tri方法后,会新建一个方法内的变量x,x的值会被初始化为percent的值,然后进行一系列的计算,在方法结束后,参数变量x就不会再使用了,percent的值还是10没有变化。

对于基本类型而言,java方法的传入方式就是老老实实的按值传递,但是对对象类型呢?

实际上,java中的对象变量全是引用形式存在的,因此,java对于对象类型的参数传入就是引用传入吗?

不完全是,java参数的传入方式永远都是按值传入,当一个对象引用被传入方法,方法会新建一个对象引用,指向传入的对象引用所指向的同一片内存区域。

也就是说,我可以在方法内部去用参数修改到方法外的对象中的域,但是我不可以去改变这个方法外这个引用的指向,方法内的对于指向的修改是无法影响到方法外的。

public static void tri(Employee x) {
x.raiseSalary(300);
}

harry = new Employee();
Employee.raiseSalary(harry); // 这行代码执行后,harry中的Salary就会发生改变
public static void swap(Employee A, Employee B) {
Employee temp = A;
A = B;
B = temp;
}

a = new Employee();
b = new Employee();
Employee.swap(a, b);
// 这段代码执行后,方法内A和B的指向是互换了,但是不会影响到方法外的变量的指向,因为A和B都只是a和b的拷贝而已。
4.6 对象构造
4.6.1 重载
多个方法可以用相同的名字,不同的参数,这样就实现了方法的重载。

因此,想要去准确地调用一个方法, 就需要提供方法的名字,和参数列表,即,方法的签名。注意,返回值并不是方法的签名。

如果给出了一个无法匹配的方法签名,或者给定的签名可以和很多个方法进行相同程度的匹配,就会出现运行时错误。

4.6.2 默认域初始化
只要是在构造器中,没有被显示赋值的域(类变量),都会被赋值为默认值。其中:

数值类型为0

布尔类型为false

类类型(对象引用)为null

这个地方可以提一嘴后面的无参构造器,如果,在定义一个类的时候,没有提供任何的构造器,那么,类就会自己生成一个无参的构造器,同时里面不会对任何的域进行显式的初始化,这就会导致所有的域都会被初始化为默认值。

因此,就体现了出域变量和普通的方法局域变量的区别,局域变量必须要被人为的初始化,但是域可以不用初始化。

4.6.3 无参数的构造器
如果,在定义一个类的时候,没有提供任何的构造器,那么,类就会自己生成一个无参的构造器。

在无参数的构造器中,所有的域都会被初始化为默认值。

如果类中给定了任何的一个构造器,类就不会自己生成无参构造器了,那如果我还是想要用无参的形式去调用一个构造器呢?

那就要必须自己人为去定义这样的一个无参构造器。

public ClassName() {
//
}
4.6.4 显式域初始化
除了在构造器中去初始化域的值,还可以在声明域的时候就直接给域赋值一个初始值。

class Employee {
private static int nextId;
private int id = 100;
}
同时,初始化域的值可以不是一个常量,其可以是一个方法。

class Employee {
private static int nextId;
private id = assignId();
//…

private static assignId() {
	int r = nextId;
	nextId++;
	return r;
}

}
需要注意的是,如果是为一个静态的域变量这样去初始化的话,方法也必须是static的。静态的上下文中必须使用静态的方法。

4.6.6 调用另外的一个构造器
这个技术非常类似于C++中的委托构造。

即在自己的构造器方法中,去初始化一部分域变量,然后让另外的一个构造器来承担剩下的初始化工作。

在Java中,具体的实现方法是使用this关键字。

class Employee {
private int id1;
private int id2;

public Employee(int id1) {
    this.id1 = id1;
}

// 我本身只初始化id2,id1交给另外一个构造器去初始化 
public Employee(int id1, int id2) {
    this(id1);
    this.id2 = id2;
}

}
注意,在C++中是无法在一个构造器中去直接调用一个构造器的。

4.6.7 初始化块
在一个类的声明中,可以去定义这样的一些初始化块。初始化块也会在构造类对象的时候被自动使用。

class Employee {
private int id1;
private int id2;
private static int id3;

{
    id1 = 2;
    id2 = 3;
}

static
{
	id3 = 4;    
}

public Employee() {
    id1 = 1;
    id2 = 2;
    id3 = 3;
}

}
实际上,这种方法不太常用,而且不建议和无参构造器一起使用,会存在非常严重的隐患问题。

实际上,初始化块会先于所有的构造器被调用来初始化域中的变量。初始化块实际上就可以认为是单独写出来的初始化语句。

初始化块的执行顺序一般如下:

如果是第一次去创建一个类的对象,首先会执行顶层父类的静态初始化块,然后一层一层向下执行,最后是自己的静态初始化块。

然后,或者,如果不是第一次创建类的对象,执行所有父类的初始化块,然后是构造器,最后是自己的初始化块和构造器。

而调用一个构造器的顺序一般如下:

所有的数据域都被初始化为默认值。

按照类中变量的声明出现的顺序,依次去执行,初始化语句和初始化块。

如果构造器的第一行调用了另外的一个构造器,去执行这个构造器。

最后是执行自己的构造器的主体。

只有是第一次创建某个类的对象的时候才会去执行静态域的初始化语句。同时,如果静态域没有被显式的初始化,其就会被初始化为默认的初始值。静态域的初始化语句和初始化块会按照声明的顺序被执行。

4.7 包
包是 Java 中所提供的一种用来组织类的方式。

所有标准的 java 包都处在 java 和 javax 包的层次中。

包的主要目的在于,确保类名的唯一性。

同时,嵌套定义的包之间是没有任何关系的。java.util 和 java.util.jar 中有着完全独立的类集合。

4.7.1 类的导入
包同时也定义了一种访问控制。

包中的一个类可以访问这个包中的所有类,但是只能访问其他包中的共有类(public)。

有两种方法来访问一个包中的公有类:

在类名之前添加完整的包名:

java.time.LocalData toDay = java.time.LocalDate.now();
使用import语句:

使用import语句可以导入一个特定的类,或者整个包(使用*)。

导入类后,使用类中的方法,就不需要再使用类名进行调用。

import 语句,应该位于整个代码的顶层,但是应该位于 package 语句的后面。

import java.util.*;
LocalData toDay = LocalDate.now();
很多人可能会误解,package语句和C++语言中的#include语句。

实际上,这两种机制完全不一样。

在Java中,我可以在书写的过程,如果遇到需要某个包中的类,但是我不想用import去引入整个包,那么我可以给出完整的包名,java编译器就会到对应的文件中去寻找字节码。(java是可以去查看文件的内容的),但是C++不可以查看文件内容,这就导致了,如果我必须使用include语句将整个代码中所有需要用到的外部声明在文件的开头进行声明。C++中和package语句比较相似的内容是namespace机制。

package chapter3;

import chapter3.mainPackage.packageTest2;
public class packageTest {
public static void main(String[] args) {
packageTest2.Hello();
packageTest2 Test = new packageTest2();
Test.Hello();
}
}
import 语句,只能导入类,他导入的不是类中的方法。

如果想要去调用类中的静态方法的话,就还是需要用类名去进行调用。

4.7.3 将类放在包中(文件结构)
如果要将一个类放在一个包中,那么,就需要在文件的开始,使用package语句。

如果不使用package语句的话,则文件就会被放在默认包中(default package)。

如果测试类和主类不在一个文件夹下(主类在测试类的子目录下)的话。(测试类中import了主类)

如果编译测试类,那么java编译器中的虚拟机就会去查找这个主类,并进行编译。

导入的类文件和其真实存在的目录必须一致。

虽然java在编译源文件的时候并不会去检查目录的结构。但是只有文件和真实存在的目录一致的时候才能正常进行编译。

4.8 类路径
类存储在文件系统的子目录中。(类的名字必须和包的名字匹配)

类文件也可以存放在JAR(java归档文件中),一个JAR文件中,包含了很多个压缩形式的类文件和子目录。

java编译器总是会在当前的目录下先去寻找类文件。

首先javac会查找 jre/lib 和 jre/lib/ext 目录下的归档文件,如果找不到的话,就会查找类路径中的文件。

如果找不到的话,就会去类的路径下查找文件。

举个例子:

有一份代码中已经有如下的import内容。

import java.util.;
import com.horstmann.corejava.
;
如果在类中用到了一个 Employee 类。 那么编译器查找这个类的顺序为:

查找 java.lang.Emplpyee ,因为,java.lang 这个包是默认导入的。

查找 java.util.Employee。

查找 com.horstmann.corejava.Employee。

查找当前包中的 Employee。

实际上,编译器的工作并不只是去定位了某个类的位置。

编译器同时还要检查源文件(.java文件)和类文件(.class)的日期,如果源文件更新的话,那么就会重新编译这个源文件。

一个源文件中,只有一个公有类,这个公有类的名字必须和文件名字相同。

4.8.1 如何设置类路径
使用 java 指令的 -classpath 或者 -cp 选项来进行设定。

同时,设定的时候,还有一些规则:

使用分号进行隔开不同的地址。

使用 . 表示当前的地址。

java -classpath c:\classdir;.;c:\archives\archive.jar Myprog
4.9 文档注释
4.10 类设计技巧
一定要保证数据的私有性。

数据的私有性是保证类的封装性的很重要的一点。

一定要人为去初始化数据。

虽然Java不会初始化局部变量,但是Java会初始化类中的数据域为默认值。不应该依赖系统的初始化。

不要在类中使用过多的基本类型。

如果要使用很多的基本类型,那么就可以,用一个新的类来包括这些基本类型,使得类中的数据域可以更加简洁,易懂。

不是所有的域都需要独立的访问器和修改器。

有些数据一旦被初始化就是不希望被改变的,对于这些数据,可以不给定他们的访问呢其和修改器。

将职责过多的类进行分解。

一个类只做一个类应该做的事情,不要给一个类赋予太多的责任。

如果一个类明显承担了两种功能,那么就应该将这个类拆分为两个不同的类。

类的名字应该表现其职责。

优先使用不可变的类。

其原因在于,如果有多个线程同时对一个类进行修改,更新。

这就会导致并发更改,并发更改的揭盖是不可预料的。

但是如果这个类是一个不可变的类,那么就可以放心在多个线程之间共享这个对象。

5 继承
继承就是,基于一个已经存在的类去创建一个新的类,复用已经存在的类的方法和数据域。

5.1 类,超类和子类
继承的明显的关系就是:继承的类往往是被继承的类的一个特例,也就是 is-a 的关系。

5.1.1 子类
定义一个子类的语法如下:

public class Manager extends Employee {
// method and data
}
使用关键字extends来代替了C++中的:实现子类的定义。

Java中的继承都是公有继承,没有C++中的私有继承和保护继承。

extends后面的类表示一个已经存在的类,也称为超类,父类,基类。

extends前面的类则表示创建出来的一个心累,也称为,子类,派生类,孩子类。

子类拥有的功能比父类更多,因此,我觉得,将父类叫做超类实际上非常令人困惑,不过可能是翻译的问题,super class也许应该被翻译为在上面的类?

在子类中定义的方法,只能被子类的对象调用,父类的对象是不可以进行调用的。

下面是一个简单的继承的例子:

package chapter5;

public class inheritanceTest {
static Manager ManagerTest = new Manager(1, 200);

public static void main(String[] args) {
    System.out.print(ManagerTest.getBouns());
}

}

class Employee {
private int id;

public Employee(int id) {
    this.id = id;
}

public int getId() {
    return id;
}

}

class Manager extends Employee {
private double bouns;

public Manager(int id, double bouns) {
    super(id);
    this.bouns = bouns;
}

public double getBouns() {
    return bouns;
}

}
5.1.2 覆盖方法
有些父类中的方法并不适用于子类,因此子类需要去覆盖重写父类中所提供的方法。

方法的覆盖其实类似于重载,但是重载是相同的方法名,不同的参数列表。而覆盖是,相同的方法名,相同的参数列表。

以下是一个覆盖的例子:

class Employee {
private int id;

public Employee(int id) {
    this.id = id;
}

public int getId() {
    return id;
}

}
package chapter5;

public class overWriteTest {
public static void main(String[] args) {
EmployeeInheritate test = new EmployeeInheritate(1);
test.getId();
}
}

class EmployeeInheritate extends Employee {

public EmployeeInheritate(int id) {
    super(id);
}

@Override
public int getId() {
    System.out.print("Hello World");
    return -1;
}

}
从这里可以学到的一点是:

静态方法,只能调用在本类中定义的静态方法和静态变量。

但是,静态方法自己内部,是可以去定义非静态变量和实例化非静态对象的。

同时还要注意的一就是:super和this看来非常像是一个引用,因为可以通过它来调用其他类中的方法等,但是实际上,他们都不是引用,具体的表现就在于,不能将他的值赋给其他的引用对象。他们都只是一个关键字。

以下还有一个例子,可以体现继承所带来的另外一个优点:多态性。

class Employee {
private int id;
private double salary;
public Employee() {
this.id = -1;
salary = 100;
}
public Employee(int id) {
this.id = id;
salary = 100;
}

public int getId() {
    return id;
}

public double getSalary() {
    return salary;
}

}
class Manager extends Employee {
private double bouns;

public Manager(int id, double bouns) {
    super(id);
    this.bouns = bouns;
}

public double getBouns() {
    return bouns;
}

@Override
public double getSalary() {
    return super.getSalary() + getBouns();
}

}
package chapter5;

public class polymorphismTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
Employee boss = new Manager(1, 200);
Employee employee1 = new Employee(2);
Employee employee2 = new Employee(2);

    staff[0] = boss;
    staff[1] = employee1;
    staff[2] = employee2;

    for(int i = 0; i < 3; i ++) {
        System.out.println(staff[i].getSalary());
    }
}

}
多态性就是指的是,一个对象变量,可以指示多种不同的实际变量类型。

动态绑定指的是,在运行的时候,可以自动选择调用哪个方法的现象。

对于Java和C++而言,都是一个意思,我可以用父类的引用变量去指向一个子类的对象,虽然我调用的方法和变量就局限在父类所含有的那些(发生了对象切片),但是在实际调用的时候,编译器会根据引用实际指向的对象去选择要调用哪个类中所定义的方法。且,C++中,要将希望发生动态绑定的方法定义为虚拟方法,但是在Java中,不需要这么做,java是要求对不希望发生动态绑定的方法用final进行标记。

5.1.3 子类构造器
通过继承得到的子类也需要一个自己的构造器。

因为,子类一般来说,会定义自己的数据域,而这些数据域,也需要构造器来进行初始化。

但是,对于类而言,其数据域为了保护类的封装性,都会被定义为private类型,因此,子类即使是继承父类的数据域,也无法进行访问。子类如果想要对父类中所定义的数据进行初始化,就只能调用父类的构造器。

如果没有显式去调用父类的构造器,那么,就会自动去调用父类的默认构造器(无参构造器),如果父类中没有这个构造器,就会报错。

同时,对于调用其他类的构造器的语法上有所要求,就是,在构造器中,如果要调用其他类的构造器,那么这句话必须位于构造器的开始。

调用父类的构造器采用:super(parameters); 的语法

class Manager extends Employee {
private double bouns;

public Manager(int id, double bouns) {
    super(id);
    this.bouns = bouns;
}

}
5.1.4 继承层次
继承并不只是一个层次。

Manager这个子类也可以作为父类去派生出其他的子类。从某个特定的类到其祖先类的路径被称为继承链。

但是要注意的是,Java并不支持多继承(一个类同时继承自多个不同的父类)。

在同一层次的各个兄弟派生类之间,实际上是没有任何关系的。

5.1.5 多态
多态性就是指的是,一个对象变量,可以指示多种不同的实际变量类型。

多态也表明了,每个子类的对象实际上也是其父类的对象,是一种 is-a 关系。

is-a 关系也表明了另外一种法则:置换法则。

置换法则即为,任何在程序中出现了父类的地方,都可以换成子类。

一个Employee类的变量,可以指向一个Employee对象,也可以指向任何派生自Employee类的子类的对象(Manager类的对象)。

package chapter5;

public class polymorphismTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
Employee boss = new Manager(1, 200);
Employee employee1 = new Employee(2);
Employee employee2 = new Employee(2);

    staff[0] = boss;
    staff[1] = employee1;
    staff[2] = employee2;

    for(int i = 0; i < 3; i ++) {
        System.out.println(staff[i].getSalary());
    }
}

}
但是,要注意的是,子类对象赋值给父类的变量是OK的,但是父类的对象赋值给子类是不OK的,因为子类可以调用的方法父类对象中没有定义。

Manager boss = new Employee(1); // 这是不可以的
5.1.6 理解方法的调用
现在假设,我要去调用 X.f(args); 其中,X对象的是C类对象的一个引用。

编译器会检查 f 的方法名,先在本类中,列出所有方法名字为 f 的方法,然后再列举出其超类中,名字为 f,且访问权限修饰为public的方法。(子类即使是继承了父类,但是对于父类中访问修饰为private的内容都无法访问)。列举同名方法

编译器会去检查,调用方法f的时候,所提供的参数列表。进行参数匹配,选择最为合适的方法进行调用。在这个过程中,运行参数的类型发生类型转换。如果没有找到匹配的方法,或者匹配的方法有多个都会报错。参数匹配

当程序以动态绑定的方法进行运行的时候,虚拟机会找到类X所引用对象的实际类型最合适的类中的那个方法进行调用,如果X变量实际指向了一个D类的对象,D类是C类的子类,那么,虚拟机会先在D类中进行查找,如果找不到匹配的方法,才会在C类中进行查找。

可以看出,如果虚拟机在每一次进行方法调用的时候都进行上述的搜索过程,其时间代价是非常大的。

因此,虚拟机会为每个类都预先创建一个方法表。

方法表中,会指明各个不同方法签名所对应的方法应该去哪个类中进行调用,比如,对于一个Manager类而言:

Manager:
getName() -> Employee.getName()
getId() -> Empployee.getId()
getSalary() -> Manager.getSalary() // 被覆盖的方法
getBouns() -> Manager.getBouns() // 自己定义的新方法
因为方法签名实际上不包含方法的返回类型,所以我可以在覆盖一个方法的同时,将返回值的类型进行修改。

绑定
指的是一个方法的调用与方法所在的类(方法主体)关联起来。

绑定分为:静态绑定,动态绑定。

class Animal{
public String name=“Animal”;

public String getName() {
    System.out.println ("Animal的方法getName被调用");
    return name;
}
public void animalSeeName(){
    System.out.println ("animalSeeName方法调用");
    System.out.println (name);
}

}

class Dog extends Animal{
public String name=“Dog”;

public String getName() {
    System.out.println ("Dog的方法getName被调用");
    return name;
}
public void dogSeeName(){
    System.out.println ("dogSeeName方法调用");
    System.out.println (name);
}

}
对于属性而言,Java采用的是静态绑定:如果子类和父类拥有同名成员变量,那么在编译时定义的是什么类,绑定的就是该类的属性。

在这里插入图片描述

如果去打印Dog类中的name属性, 可以发现属性的值就是Dog。

如果Dog类中没有去定义name属性的话,打印出来的结果就是Animal。

对于方法而言,Java采用的是,动态绑定:编译时静态绑定,也就是定义时是什么类,就绑定什么类的方法,到了运行时,它就动态绑定它实际指向的对象的方法。

public static void main(String[] args) {
Animal animal=new Dog();
Dog dog =new Dog();

animal.getName();
// animal编译时绑定的是Animal类的getName方法,但到了运行时,
// 指向是一个Dog对象,所以执行的是Dog类的getName方法。
dog.getName();

}
动态绑定有一个非常重要的好处:无需对现有的代码进行修改,就可以对程序进行扩展。

那么,如果,子类和父类都定义了同名的变量。然后在子类的方法中调用了这个同名变量,那么调用的是子类中的呢,还是父类中的呢?

很明显,这个结果是根据方法所在的类而决定的。

如果方法是在父类中定义和执行的,那么就是父类的变量,反之就是子类的变量。

public static void main(String[] args) {
Animal animal=new Dog();
Dog dog =new Dog();

animal.animalSeeName();
//"Animal" 因为animalSeeName()在Animal类中定义和使用,
dog.dogSeeName();
//"Dog" 因为dogSeeName()在Dog类中定义和使用

System.out.println();
System.out.println(animal.getName());
//"Dog" 因为执行的是Dog的方法getName(),在Dog类中定义和使用,
System.out.println(dog.getName());
//"Dog" 因为执行的是Dog的方法getName()在Dog类中定义和使用,

}
// 因为animalSeeName()和dogSeeName()都是在他们自己的类中都有定义的所以就是调用他们自己的方法,那他们找变量都会现在自己的类中找到进行调用。
// 因为getName()方法在Dog类方法中定义了,同时覆盖了Animal中的该方法,因此调用的就只能都是Dog类中的getName方法了。
5.1.7 阻止继承:final类和方法
对于哪些不允许被扩展的类称为final类。被final修饰符所定义的类是不允许被继承的。8

public final class Excutive extends Manager {

}
不仅仅是类,类中的某些方法也可以被声明为final。类中被声明为final的方法不需要在其子类被覆盖。

除了方法,域也可以被声明为final,对于final修饰的域来说,对象一旦被构造了,就不再允许去改变他们的值了。

被final所修饰的类,其中的所有方法都是默认用final进行修饰了,但是类中的域不是final所修饰的。

内联
对于那些经常被调用,且篇幅较短的方法而言,Java的编译器会将其内联化进行优化处理。

比如一些访问器,Java编译器就有可能将其优化为直接对域进行访问。

如果子类中,对父类中的inline方法进行了覆盖,那么优化器就会取消对覆盖方法的inline化。

5.1.8 强制类型转换
对于基本类型,Java提供了类型之间的强制类型转换机制。

double a = 2.5;
int b = (int)a; // b = 2;
对于类的引用变量类型之间,Java也提供了强制类型转换的方法。

Employee[] staff = new Employee[3];
Employee boss = new Manager(1, 200);
Employee employee1 = new Employee(2);
Employee employee2 = new Employee(2);
可以采用相同的语法进行引用类型之间的类型转换。

Manager Boss = (Manager)boos;
对于Java虚拟机而言,在将一个值存入一个对象的时候,会检查是否允许这个操作,比如:

将子类的引用存入父类的引用是完全OK的。

将父类的引用存入子类的引用是要求先进行类类型转换的。

如果,父类的引用指向了一个引用的对象,那么向子类引用的转换就是非法的。(子类中可以调用的方法和域在父类中没有定义)

如果,父类的引用指向了一个子类的对象,那么向子类引用的转换就是合法的。(实际上就是子类的引用指向了子类的对象)

Manager Boss = (Manager)employee1; // 这种类型转换就是非法的
最基本的是,强制类型转换,只能发生在继承层次之间。进行转换的两个类引用变量要求相互有继承关系。

但是,要注意的是,在类的引用变量之间进行强制类型转换不是一个好的做法。

对于父类引用和子类引用都指向一个子类引用的时候,两个类共有的方法和域都是可以被直接调用的,只要需要调用子类中特有的方法和域的时候,才需要进行强制类型转换。但是父类需要子类的方法本身就非常的匪夷所思。出现这种情况,一般需要考虑的是,类的设计有问题。

C++中,可以使用 dynamic_cast 进行强制类型转换,当转换不成功的时候就会返回NULL。但是对于Java而言,如果转换不成功,会抛出一个异常,如果不对这个异常进行捕捉,就会导致程序的运行终止。因此,Java在进行引用的强制类型转换之前,需要先人为判断一下,这个转换是是不是合法的,可以通过 instanof 关键字进行判断。

if(boss instanof Manager) {
Manager Boss = (Manager)boss;
}
5.1.9 抽象类
对于一个继承层次而言,处于更高层次的类,其通用性就更强,也就更加抽象。

对于这些抽象性很强的类,就可以将他们仅仅作为用于派生的基类,而不作为用来实例化的实例类。

举例而言,对于Employee和Student这两个类,姓名,性别,都是这两个类共有的域。

因此,可以将这两个域移动到更加高的继承层次中。形成一个Person的抽象类。

在Person类中,定义了一个虚方法getDescription。虚方法就是使用abstract修饰符进行定义的方法,这种方法表示的仅仅是一种占位,这个方法会在其派生的子类中被实现,而在派生类中,不需要进行实现。

abstract class Person {
private String name;

public abstract String getDescription();
public Person(String name) {
    this.name = name;
}

public String getName() {
    return this.name;
}

}
一个类不管是否含有abstract方法,都可以被定义为抽象类,为了使得程序的清晰度提高,如果一个类含有一个或者多个抽象方法,这个类要被定义为抽象类。

在抽象类中,不仅仅可以含有抽象方法,其也可以含有通用的具体方法和一些具体的域。

对于抽象类而言,其派生生的子类有两种情况:

派生的类中,只实现了部分父类中的定义的抽象方法,那么这个子类也要被标记为抽象类。

派生的类中,实现了所有的父类中所定义的抽象方法,那么这个类就是一个具体类。

抽象类是不可以被实例化的,因为其中可能还有没有被实现的方法,但是可以有抽象类类型的引用,作为父类的引用指向一个子类。

在C++中,可以使用在函数的尾部标记=0的方法来讲方法标记为虚方法,只要C++中的类中的一个方法是虚方法,那么,这个类就是抽象类,C++没有对类有专门的抽象标记。

抽象方法是一个非常重要的概念,其存在的目的在于,在抽象类中,有一些子类所共有的方法,但是,这些方法的执行依赖于子类中所定义的数据域,因此就必须在抽象类中用抽象方法进行占位,然后在子类和派生类中进行实现。

package chapter5;

public class abstractClass {
public static void main(String[] args) {
Student test = new Student(“Jack”);
System.out.println(test.getDescription());

    Person test2 = new Student("Jack");
    System.out.println(test2.getName());
    System.out.println(test2.NameForPerson());
    System.out.println(((Student)test2).NameForStudent());
}

}

abstract class Person {
private String name = “Machael”;

public abstract String getDescription();

public Person(String name) {
    this.name = name;
}

public String getName() {
    return this.name;
}

public String NameForPerson() {
    return this.name;
}

}

class Student extends Person {
private String name = “Jane”;

public Student(String name) {
    super(name);
}

public String NameForStudent() {
    return this.name;
}

@Override
public String getDescription() {
    return new String("The name of the Student is " + getName());
}

}
这份代码非常具有启发性,其输出的结果为:

The name of the Student is Jack
Jack
Jack
Jane
首先,我们可以将一个子类理解为两个不同的部分,第一个部分就是父类的部分,其父类域由父类自己的构造器进行初始化,第二个部分就是子类的部分,子类域用自己的初始化方法进行初始化,父类和子类中的同名变量。

当我去调用一个子类的方法的时候,比如这份代码中的getName方法,首先,会在Student类中进行寻找,没有找到,于是去父类中找,在父类的部分中找到了,所以会去调用父类中的name变量,但是这个name变量在Person test2 = new Student(“Jack”);中被父类的构造器在父类的域中初始化为了Jack,所以输出的结果也是Jack。

但是,当我去调用子类中的NameForStudent这个方法的时候,在子类中找到了,因此会去调用子类中的name变量,这个name变量是没有被父类的构造器所初始化的,所以,输出的结果依然是Jane。那要是再进一步,如果,子类中没有定义这个name呢,那么在子类中找不到这个变量,就回去父类中找,找到的就是被构造器修改过的name变量了,也就是Jack。

5.1.10 受保护的访问
对于一个父类而言,其中的数据域和部分的方法采用private进行修饰,那么派生自这个类的子类无法访问这些内容。

如果想要子类可以访问父类中的这些数据域和方法的话,可以在子类中将这些部分定义为protected。

被protected所修饰的内容,可以在其所有的子类中进行访问,但是不能被其他的类访问。

但是实际上,protected修饰的内容,不仅仅对于所有的子类可见。对于同一个包中的所有其他的类都可见。

以下是Java中四种不同的访问修饰符的权限。

image-20230907104241927

以下是一个简单的例子,说明protected的作用范围:

package chapter5;

public class protectedTest {
public static void main(String[] args) {
// protected内容对子类可见
extended test = new extended(100);
System.out.println(test.getNum());
// protected内容对同一个包内的其他类都可见
notExtended test2 = new notExtended();
System.out.println(test2.getNum());
}
}

class base {
protected int num;

public base(int num) {
    this.num = num;
}

}

class notExtended {
public base test;

public notExtended() {
    test = new base(100);
}

public int getNum() {
    return test.num;
}

}

class extended extends base {
public extended(int num) {
super(num);
}

public int getNum() {
    return num;
}

}
5.2 Object类
Object类是所有类的祖先类。

但是直接写一个类时候是不需要去致命其继承自Object(默认继承的)。

因此,Object类的引用变量可以指向任何的类对象。

实际上,Java中,只有一些基本类型不是类对象(其他的都是类对象)。

5.2.1 equals方法
Object类中,提供的默认的equals方法会检查隐式参数this和输入的参数的引用,如果相同则返回true,否则返回false。

但是,如果两个引用的指向一样,那这两个对象当然是相同的,这样的比较没有什么意义。

经常需要比较的是,两个对象的状态。如果两个同类对象中的域都相同的话,那么就可以人为这个两个对象是一样的。

以下是一个简单的例子:

class Employee {
private int id;
private double salary;

public Employee() {
    this.id = -1;
    salary = 100;
}

public Employee(int id) {
    this.id = id;
    salary = 100;
}

public int getId() {
    return id;
}

public double getSalary() {
    return salary;
}

public boolean equals(Object otherObject) {
    if(this == otherObject) return true; // 如果是两个相同的对象比较,当然是一样的。
    if(otherObject == null) return false; // 和null对比进行比较,结果当然是false。
    if(getClass() != otherObject.getClass()) return false; // 如果进行比较的两个对象的类都不一样,结果当然是false。
    
    // 现在可以确定的是,进行比较的两个对象是同一个类的,因此,可以进行强制类型转换。
    Employee other = (Employee)otherObject;
    return salary == other.salary && id == other.id;
}

}
5.2.2 继承和相等测试
如果要进行比较的类是父类和子类的对象上述代码就会返回false。

实际上想要的是,比较的如果是父类和父类,那么就要求所有的域都相等,如果比较的是父类和子类,那么就要求父类的部分相同即可。

以下是一个比较完善的equals方法的写法:

public boolean equals(Object otherObject) {
if(this == otherObject) return true; // 1. 如果是两个相同的对象比较,当然是一样的。
if(otherObject == null) return false; // 2. 和null对比进行比较,结果当然是false。

// 3. 如果equals的语义在子类中没有变化,则使用instanceof进行检测
//    如果equals的语义在子类中有变化,则使用getClass()进行检测
if(getClass() != otherObject.getClass()) return false; // 4. 如果进行比较的两个对象的类都不一样,结果当然是false。
if(!(otherObject instanceof ClassName)) return false;

// 5. 现在可以确定的是,进行比较的两个对象是同一个类的,因此,可以进行强制类型转换。
Employee other = (Employee)otherObject;
// 6. 在进行完强制类型转换后,就可以对这两个对象中的域惊醒一一比较了,只有域完全相同的两个对象才是相同的。
return salary == other.salary && id == other.id;

}
需要注意的是,在子类中定义equals的方法的时候,可以先调用一下超类中的equals,如果两个对象的超类部分都不相同的话,那这两个子类也一定不相同。

5.2.3 hashCode方法
hashCode是从对象中所导出的一个整数类型的数值。

hashCode在默认的Object类中的定义为,该对象的存储地址,因此两个不同的对象,其导出的hashCode一般都不一样。

hashCode在String类中,被重新定义为了从对象的内容导出,因此,两个值相同的String类对象的hashCode就完全一样。

一般来说,如果我重新定义了类中的equals方法,都要去重新定义hashcode方法。

首先,在Java中,有一种数据结构叫做Hashmap。

Hashmap中,其键值对通常都是类类型。

当我想要再Hashmap中插入一个键值对的时候,我先计算键的哈希码,其决定了键值对的桶(链表)。

实际上,因为hashcode这个方法能被重写,所以不同的对象可能有相同的hashcode,这就会导致哈希冲突。所以,在一个桶中存放所有键的hashcode相同的键值对。

查找键值对的时候,也是,先通过键的hashcode定义桶,再利用equals方法在桶中依次去匹配值。

因此,对于一个相同的对象而言,我们希望他的hashcode相同,其equals方法也相同。

如果我们只定义了equals方法,其用具体的域进行判断,但是我没有定义hashcode方法,这就会导致,两个相同的对象拥有不同的内存地址,有不同的hashcode,这就会导致,哈希冲突(两个相同的对象被放到了不同的桶中)。

所以,重写hashcode方法的导向就是将地址导出修改为内容导出。

class Employee {
private int id;
private double salary;
public Employee() {
this.id = -1;
salary = 100;
}
public Employee(int id) {
this.id = id;
salary = 100;
}

public int getId() {
    return id;
}

public double getSalary() {
    return salary;
}

public int hashCode() {
    return Integer.valueOf(id).hashCode() + Double.valueOf(salary).hashCode();
}

}
但是,hashCode方法还可以做得更好。

使用Object类中自带的hash方法就可以组合多个不同的参数的hashcode。

public int hashCode() {
return Object.hash(id, salary);
}
5.2.4 toString 方法
toString的本意是返回一个表示对象值的字符串。

一般来说,toString所被期望的返回内容为:类名 + 域值。

对于Employee类而言,所应该重写的toString方法为:

public String toString() {
return “Employee[id=” + Integer.valueOf(id).toString() + “,salary=” + Double.valueOf(salary).toString() + “]”;
}
但是,类名是可以用getClass方法获取到的,所以还可以写得更好一点就是:

public String toString() {
return getClass().getName() + “[id=” + Integer.valueOf(id).toString() + “,salary=” + Double.valueOf(salary).toString() + “]”;
}
如果父类中采用的是上述的定义方法的话,那么子类中定义toString方法就可以调用父类中的方法:

public String toString(){
return super.toString() + “,[bouns=” + Double.valueOf(bouns).toString() + “]”;
}
需要注意的是,因为所有的类中都定义了toString方法,所以,在类和类之间使用+进行连接的时候,就会自动转化为toString方法的调用,同理,也可以直接对一个类对象使用print方法进行输出。

5.3 泛型数组列表
泛型数组列表,就是可变长模板数组,其拥有两个特点:

长度可变。

可以任意指定其中元素的类型。

可以采用ArrayList类来创建一个泛型数组列表的对象:

ArrayList array = new ArrayList();
两边都要写模板的类型,非常麻烦,后期的Java允许只在左边声明:

ArrayList array = new ArrayList<>();
使用add方法可以在数组中进行数据的添加:

array.add(new Employee(1));
array.add(new Employee(2));
array.add(new Employee(3));
这里就体现了泛型数组列表和数组的区别,确实,数组列表引用对象管理了一个内部数组,这个数组的空间当然可能被用尽,但是,用尽了,ArrayList还可以再申请,但是普通的数组不可以。

假如我可以大概估计到我需要使用多少个元素,我就可以用ensureCapacity方法来对动态内存分配进行优化,ArrayList的动态内存分配可能会分配过多的空间导致内存浪费,但是使用ensureCapacity就可以一次性分配足够的空间。(100表示这个动态数组最小的内存空间)

ArrayList array2 = new ArrayList();
array2.ensureCapacity(100);
同样,我也可以在在定义这个可变长数组的时候,就指定这个数组的初始大小:

ArrayList array3 = new ArrayList(100);
如果我可以保证,数组的大小不再变化,我就可以使用trimToSize方法,来让垃圾回收器回收掉多余的内存。

利用size方法可以返回当前数组的实际大小。

可以发现的是,ArrayList其实非常相似于vector,但是和vector的区别在于,C++是可以重载运算符号的,因此为了便于对vector中的元素进行访问,C++重载了[]运算符号,但是Java没有运算符重载的功能,所以,只能显式进行访问。

5.3.1 访问数组列表中的元素
因为Java中没有对运算符号的重载,所以Java只能通过调用方法来对ArrayList中的元素进行访问和修改。

使用get方法访问元素,使用set方法修改元素。

import java.util.ArrayList;

public class ArrayLsitTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(Integer.valueOf(100));
test.add(Integer.valueOf(200));
test.add(Integer.valueOf(300));

    System.out.println(test.get(0).toString());

    test.set(0, 1000);

    System.out.println(test.get(0).toString());
}

}
还需要注意的一点是,是不能用set方法去尝试插入元素的。

set方法只能用来对已经有元素的位置进行重设,即使是对于已经分配了初始容量的ArrayList。

C++中的vector容器,如果对其分配一个初始的容量,那么就可以访问并且修改。

下图所示的示例也是不被允许的:

import java.util.ArrayList;

public class ArrayLsitTest {
public static void main(String[] args) {
ArrayList test2 = new ArrayList(100);
test2.set(2, 2000);
System.out.println(test2.get(2).toString());
}
}
最开始,最开始,ArrayList中的get方法,返回的是Object对象引用,因此,需要对其进行强制类型转换,但是现在的get方法,貌似,返回的已经是模板类型的引用,这一点无需在意。

下文中的示例代码所返回的对象类型就是Integer。

import java.time.LocalDate;

public class LocalDateTest {
public static void main(String[] args) {
LocalDate now = LocalDate.now();
now = now.plusDays(1000);
System.out.println(now.getYear());
}
}
add方法还可以指定插入元素的下标。

注意,插入元素的下标最多最多也就是整个数组的末尾。

import java.util.ArrayList;

public class ArrayLsitTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(Integer.valueOf(100));
test.add(Integer.valueOf(200));
test.add(Integer.valueOf(300));

    int n = test.size();
    test.add(n,100);
    for(Integer T : test) {
        System.out.println(T.toString());
    }
}

}
remove方法可以删除数组列表中的元素,同样,remove方法不能作用在没有元素存在的下标上。

import java.util.ArrayList;

public class ArrayLsitTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(Integer.valueOf(100));
test.add(Integer.valueOf(200));
test.add(Integer.valueOf(300));

    test.remove(2);

    for(Integer T : test) {
        System.out.println(T.toString());
    }
}

}
和C++一样,Java对于容器提供了for-each的循环方式。

for(Integer T : test) {
System.out.println(T.toString());
}
同样,在for-each循环中,不允许对容器的长度进行修改。

因为,for-each循环依赖于容器的迭代器,对长度进行修改后,迭代器不能得到及时更新。

import java.util.ArrayList;

public class ArrayLsitTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(Integer.valueOf(100));
test.add(Integer.valueOf(200));
test.add(Integer.valueOf(300));

    int n = test.size();
    test.add(n,100);
    int i = 0;
    for(Integer T : test) {
        if(i == 0) {
            test.add(n,500);
            i = i + 1;
        }
        System.out.println(T.toString());
    }
}

}
要注意的是,get方法其实并不安全。

get方法并不会去检查所插入的对象的类是不是数组列表的模板类。

只有在具体访问的时候,利用强制类型转化的时候,编译器才会进行类型检查。

5.4 自动装箱和自动拆箱
指的是,Java中,基本类型和其对应的类类型可以被编译器自动进行转换。

下面就是一个简单的例子:

ArrayList list = new ArrayList();
list.add(1);
编译器会在需要进行自动装箱的地方自动插入强制类型转换的代码:

ArrayList list = new ArrayList();
list.add(Integer.valueOf(1));
自动插箱也是同理的过程。

在Java中,可以使用 == 符号对类对象进行比较,但是比较的依据是对象所存储的地址。

但是对于自动装箱而言,其转换的对象是有可能指向同一块地方的,对于boolean,byte,char,只在-128到127之间的short和int被装箱在同一个对象中。

对于Integer这种基本类型对应类中,还提供了一些实用的方法:

// 字符串转Int(可以指定进制)
parseInt(String s);
parseInt(String s, int radix);
// Int转字符串(可以指定进制)
toString(int i);
toString(int i, int radix);
5.5 参数数量可变的方法
使用 … 可以传入任意数量的相同类型的参数。

package chapter5;

public class variableParameter {
public static void main(String[] args) {
String[] strings = new String[3];
strings[0] = “hello”;
strings[1] = “world”;
strings[2] = “fuck”;

    testParameter(strings);
}

public static void testParameter(String... strings) {
    for(String T : strings) {
        System.out.println(T);
    }
}

}
需要注意的是,Object… 其实等同于Object[]。

5.6 枚举类
5.6.1 什么是枚举类
枚举类也是类,不过其比较特殊。

写一个枚举类的基本格式如下:

public enum nameEnum {
name1, num2, … ; // 枚举类,第一行必须写进行枚举的名称。
// 剩下的部分可以定义,其他类中的所有可以定义的成员
}
和C++中不太一样的是,Java枚举中定义的枚举不会个分配一个常量值。Java中的枚举变量定义后就是那个样子。

以下是一个定义枚举类的实例:

package chapter6;

public enum enumTest {
// 枚举类的第一行必须是进行枚举的变量名。
X, Y, Z;

// 其余的地方可以是任何一个正常类可以包含的成员。
private String name;

}
这个地方就可以深入去了解以下,枚举类中定义的枚举变量到底是什么东西?

首先,通过反编译就可以看见,X,Y,Z的定义的类型为:

public static final enumTest X = new enumTest();
public static final enumTest Y = new enumTest();
public static final enumTest Z = new enumTest();
也就是说,X,Y,Z是一个静态的enumTest类的常量对象。

同时还可以发现一个点是,enumTest类的构造函数是private类型的,因此可见,枚举类型是无法new一个实例出来的。(因此,即使可以在枚举类中去定义一些方法,但是因为不能被实例化,非静态方法都没什么意义)

这也可以理解,因为,枚举类的本意就不是用来进行实例化的,枚举类最大的用处是进行数据分类。

image-20231016232207859

枚举类中还提供了一些方法可以调用:

values方法,可以返回所有的枚举对象。

enumTest[] test = enumTest.values();
for(enumTest it : test) {
System.out.println(it);
}
valueof方法,按照名字返回某个枚举对象。

System.out.println(enumTest.valueOf(“X”));
ordinal方法,返回指定的枚举对象的下标。

System.out.println(enumTest.X.ordinal());
5.6.2 抽象枚举类
抽象枚举类就是,在枚举类中有定义一些抽象方法。

但是要注意的是,定义枚举抽象类的时候,不可以去用 abstract 修饰符。

首先,梳理一下。

抽象枚举类中含有,抽象方法,因此,其不能被实例化。(抽象类本身不能对外界实例化是因为构造器私有)

但是在枚举类中,枚举的变量是抽象类类型的对象。(抽象类本身对内部是可以实例化的,private构造方法都类中还是可见的)

但是因为抽象枚举类含有了抽象方法,其对内部也不能实例化了,那怎么办?

在罗列枚举变量的时候,实现一下抽象方法就可以了。

以下是一个实现抽象枚举类的实例:

enum enumTest2 {
// 枚举类的第一行必须是进行枚举的变量名。
// 因为,枚举类本身包含有抽象方法,因此不能被实例化,但是枚举变量又是抽象类对象,所有不可以直接罗列出枚举变量
X() {
@Override
void go() {
System.out.println(“X”);
}
},
Y(“Jack”) {
@Override
void go() {
System.out.println(getName());
}
},
Z() {
@Override
void go() {
System.out.println(“Z”);
}
};

enumTest2() {
   
}

enumTest2(String name) {
    this.name = name;
}

public String getName() {
    return name;
}

String name;
// 枚举类中可以包含抽象方法,这样的一个枚举类称为抽象枚举类。
abstract void go();

}
抽象枚举类中的方法也被调用。

package chapter6;

public class enumClasa {
public static void main(String[] args) {
enumTest2 test2 = enumTest2.X;
test2.go();
}
}
5.6.3 枚举的使用场景
首先,有一个比较奇怪,但是非常有说服力的场景。

可以使用枚举来实现单例设计模式。

单例设计模式指的就是,这一个类型的对象就只有一个,也就是,不能用new去创建这个类的对象。

对于枚举来说,其构造器天生就是私有的,枚举类天生就不可以被new出对象,因此,可以使用枚举来实现单例设计模式。

更有意思的是,枚举甚至还是进程安全的,用来做单例更放心,嘿嘿。

第二点就是,枚举一般会用来做数据的分类。

实际使用中,经常会用常量来作为数据分类的依据,比如:

package chapter6;

public class check {
final int Boy = 0;
final int Girl = 1;

public static void main(String[] args) {
    check(0);
    check(1);
}

public static void check(int flag) {
    switch (flag){
        case 0:
            System.out.println("Bpy");
            break;
        case 1:
            System.out.println("Girl");
            break;
    }
}

}
但是,很明显,使用常量的话,其对于输入的值没有任何约束,还需要额外去应对非法输入的情况,属于是硬编码,可读性不好。

使用枚举类型的话,就会好很多:

package chapter6;

public class check {
public static void main(String[] args) {
check(Gender.Boy);
check(Gender.Girl);
}

public static void check(Gender flag) {
    switch (flag){
        // 这里是针对于枚举类型的一个优化,其不用再使用Gender去访问,直接写枚举变量就可以
        case Boy:
            System.out.println("Boy");
            break;
        case Girl:
            System.out.println("Girl");
            break;
    }
}

}

enum Gender {
Boy, Girl;
}
使用枚举的话,其只能接受枚举类型的变量,而枚举类型中,又只提供了两个性别的枚举变量,所以是安全的,可读性也更好。

但是不代表枚举就完全代替了常量作为数据分类的依据,常量更加灵活方便,其实使用地更多。

5.7 反射
这块有点抽象,看黑马的视频了。

【黑马Java进阶教程,轻松掌握Java反射原理】https://www.bilibili.com/video/BV1ke4y1w7yn?vd_source=ec0be56eb19728cf092c619dfd937067

动态代理
image-20230919215113092

一般来说,如果我想要在一个类中去添加一些新的方法,采用的方法就是在类中直接写这些方法。

这种方法就是,侵入式修改,但是非常容易导致bug,不推荐这种方式。

动态代理的本质就是无侵入式的给代码增加新的功能。

代理就是在执行的过程中,遇到了吃饭这个功能,采取student中进行调用。

Q:程序为什么需要代理?

类中一个方法只应该关心最主要的部分,而除了主要部分之外的准备阶段就应该让代理来承担。

如果对象身上的职责太多,就可以通过代理来转移部分的职责。

Q:代理是什么样子的?

代理里面应该有什么方法。

代理里面的方法应该就是需要让代理来承担部分责任的方法。但是,代理本身是不知道方法的细节的,代理会自己执行职责后,去调用类中的方法来做后面的核心部分。

对象有什么方法需要代理,就要有相应的方法。

Q:代理是怎么知道有哪些方法需要代理呢?

在具体的代码中,通过接口去实现。

首先在类中定义一个接口,这个接口中的方法就是先要被代理的方法。

准备工作

一般来说,代理是通过接口实现的。

把想要代理的方法写在接口中,然后让类来实现这个接口。

调用者首先调用代理对象,代理对象先运行自己的方法,再去调用对象的方法。

主类的实现:

package code.javaLearn.src.chapter5;

// 在类中去实现这个接口
public class reflectTest implements reflect {
private String name;

public reflectTest(String name) {
    this.name = name;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Override
public String Sing(String name) {
    System.out.println(this.name + " sings " + name);
    return "Thank You";
}

@Override
public void dance() {
    System.out.println(this.name + " dances ");
}

}
接口的实现:

package code.javaLearn.src.chapter5;

// 接口中的方法都是抽象方法
public interface reflect {
// 将想要代理的方法写在接口中
public abstract String Sing(String name);
public abstract void dance();
}
动态代理的实现
image-20230919223601303

java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法newProxyInstance。

package code.javaLearn.src.chapter5;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtil {

 /*
 功能:
    为一个reflectTest类创建一个代理
 形参:
    要传入需要代理的对象才能创建代理
 返回值:
    给类所创建的代理,类型是接口类型。

 在外面去调用这个代理的方法:
    1. 获取代理的对象
       代理对象 = ProUtil.createProxy(reflectTest A);
    2. 在去调用代理中的Sing方法
       代理对象.Sing(); 实际上会去调用invoke方法。
 */

public static reflect createProxy(reflectTest A){
    /*
    形参1:类加载器,加载一个类的字节码文件,就是本类的类加载器。将现在所生成的代理加载到内存当中
    形参2:指定接口,这些接口中有哪些方法。将接口的字节码放在数组当中。
    形参3:用来指定生成的对象要干什么事情。
     */
    reflect proxy = (reflect) Proxy.newProxyInstance(ProxyUtil.class.getClassLoader(),
            new Class[]{reflect.class,},
            // 形参3:用来指定生成的对象要干什么事情。
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    // 参数1:代理的对象
                    // 参数2:要运行的方法 sing
                    // 参数3:调用sing方法的时候,要去传递的实参。

                    if("Sing".equals(method.getName())) {
                        System.out.println("准备话筒,收钱");
                    }
                    else if("dance".equals(method.getName())) {
                        System.out.println("准备场地,收钱 ");
                    }
                    // 去reflectTest类中去具体调用剩下的方法
                    return method.invoke(A, args);
                }
            }
    );
    return proxy;
}

}
在主类中实现一个main方法去测试反射。

package code.javaLearn.src.chapter5;

// 在类中去实现这个接口
public class reflectTest implements reflect {
private String name;

public reflectTest(String name) {
    this.name = name;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Override
public String Sing(String name) {
    System.out.println(this.name + " sings " + name);
    return "Thank You";
}

@Override
public void dance() {
    System.out.println(this.name + " dances ");
}

public static void main(String[] args) {
    //在外面去调用这个代理的方法:
    //1. 获取代理的对象
    //   代理对象 = ProUtil.createProxy(reflectTest A);
    //2. 在去调用代理中的Sing方法
    //   代理对象.Sing(); 实际上会去调用invoke方法。

    reflectTest bigStar = new reflectTest("JiGe");
    reflect proxy = ProxyUtil.createProxy(bigStar);

    String ans = proxy.Sing("及你太美");
    System.out.println(ans);

    proxy.dance();
}

}
再捋一下程序执行的逻辑:

首先在测试类中,创建一个代理,传入类的对象到createProxy中去生成一个代理。

在createProxy方法中,去生成一个代理,在外面通过代理去调用方法的时候,对于代理而言其调用的是invoke方法。

对于invoke方法,最重要的是后面两个参数,第一个是调用的方法,第二个是要传入方法的参数。

在外面的调用中,调用了sing方法, 那么在invoke方法中就会去匹配,匹配到了就执行sing方法的前置。

执行完前置后,执行sing方法自己的内容,通过调用方法本身的invoke方法实现。

反射
什么是反射?
反射允许对封装类的字段,方法和构造函数的信息进行编程访问。

类中一半来说,经常使用的就是:字段(成员变量),构造方法,成员方法。

反射就是可以单独获取类中的这三个不同的内容。

IO流是从上往下一行一行去读。很难去区分各个方法谁是谁,局部变量和成员变量。

反射,可以获取到变量,还可以获取到变量的所有内容(修饰符,名字,类型,获得其值,甚至重新为其进行赋值),对于构造方法而言可以获取修饰符,名字,形参,甚至可以用构造方法去创建一个对象,对于普通方法,可以获取修饰符,名字,形参,返回值,异常,甚至是注解这些都可以被获取到。反射获得的方法也可以运行。

先获取,再解剖。

image-20230920172804171

反射获取内容,不是从java文件中获取,而是从字节码文件中获取。

获取class对象
Class.forName(“全类名”);

Class类就是用来描述类的字节码。

全类名就是:包名 + 类名的格式。

类名.class;

对象.getClass();

getClass方法是定义在Object类中,所有对象都可以调用。

Java中创建一个类的对象的三个不同的阶段:

源代码阶段(这个阶段没有把代码加载到内存,只是再硬盘中进行编译)

编写Java文件。(.java)

编译为字节码文件。(.class)

对于这个阶段,采用的方法是Class.forName(“全类名”);

加载阶段

使用A.class方法将类加载到内存中。

运行阶段

在运行阶段,可以创建这个类的对象,这样就可以用对象.getClass();这个方法进行获取字节码文件对象。

package code.javaLearn.src.chapter5;

/*
本代码的目的在于,学习如何去获得一个类的字节码文件。
*/

//1. Class.forName(“全类名”);
// Class类就是用来描述类的字节码。
//2. 类名.class;
//3. 对象.getClass();
// getClass方法是定义在Object类中,所有对象都可以调用。

public class MyReflect {
public static void main(String[] args) {
// 目标是在main函数中去获取到StudentForMyReflect的字节码文件

    //1. Class.forName("全类名");
    //最为常用的方法
    Class clazz;
    try {
        // clazz 就是 StudentForMyReflect类 的字节码文件对象
        // 要注意的是,包名必须写全
        clazz = Class.forName("code.javaLearn.src.chapter5.StudentForMyReflect");
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
    System.out.println(clazz);

    //2. 类名.class;
    //更多的是用做参数进行传递
    Class clazz2 = StudentForMyReflect.class;
    System.out.println(clazz2);

    //3. 对象.getClass();
    //存在一定的局限性,就是当,我们已经有一个对象的时候才会使用这种方法
    StudentForMyReflect test = new StudentForMyReflect();
    Class clazz3 = test.getClass();
    System.out.println(clazz3);
}

}

class StudentForMyReflect {
private String name;
private int age;

public StudentForMyReflect() {
}
public StudentForMyReflect(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}

public int getAge() {
    return age;
}
public void setAge(int age) {
    this.age = age;
}

@Override
public String toString() {
    return "StudentForMyReflect{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
}

}
从字节码文件中获取构造方法
在Java中,什么东西都可以看作是一个对象。

字节码文件可以看作是一个Class对象。

构造方法可以看作是一个Constructor对象。

成员变量可以看作是一个Field对象。

成员方法可以看作是一个Method对象。

获取构造方法的方法在Class类中,大概有以下几种方式:

Construcor<?>[] getConstructors():返回所有的public的构造方法,放到一个数组中进行返回。

Construcor<?>[] getDeclaredConstructors():返回所有的构造方法(不论权限修饰符),放到一个数组中进行返回。

Construcor<?> getConstructor():只获取单个的公共构造方法。

Construcor<?> getDeclaredConstructor():只获取单个的构造方法,不论其是公共的还是私有的。

所使用的Student类

class Student {
private String name;
private int age;

public Student() {
}
public Student(String name) {
    this.name = name;
}
protected Student(int age) {
    this.age = age;
}
private Student(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

}
从Student类中去获取构造方法

public class getConstructorTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//1. 首先去获取Student类的字节码文件
Class clazz = Class.forName(“Student”);

    //2. 从字节码文件中去获取构造方法

    // 获取所有的public的构造方法
    Constructor[] cons = clazz.getConstructors();
    for(Constructor T : cons) {
        System.out.println(T);
    }

    // 获取所有定义的构造方法,包括公有和私有
    Constructor[] cons2 = clazz.getDeclaredConstructors();
    for(Constructor T : cons2) {
        System.out.println(T);
    }

    // 获取单个的public的构造方法
    // 获取空参的构造方法
    Constructor cons3 = clazz.getConstructor();
    System.out.println(cons3);
    // 获取带有参数的构造方法,其中传入的参数必须和某个构造方法的参数相同(参数要用字节码文件进行传入)
    Constructor cons4 = clazz.getConstructor(String.class);
    System.out.println(cons4);

    // 获取单个的任意构造方法
    Constructor cons5 = clazz.getDeclaredConstructor(int.class);
    System.out.println(cons5);
    Constructor cons6 = clazz.getDeclaredConstructor(String.class, int.class);
    System.out.println(cons6);

    // 一旦获取到了构造方法,我可以做到很多事情

    // 获取权限的修饰符号
    // public 1 private 2 等,注意,这个的定义不是严格按照顺序进行定义的
    // Idea中,辅助补全就是利用的权限修饰符号来告诉你哪些可以调用
    int modifier = cons6.getModifiers();
    System.out.println(modifier);

    // 用构造方法创建对象
    Student stu = (Student) cons4.newInstance("CL");
    System.out.println(stu);
}

}
从字节码文件中获取成员变量
在Class类中,用来获取成员变量的方法有:

Field[] getFields():获取所有的public的成员变量。

Field[] getDeclaredFields():获取所有的声明过的成员变量(包括private和public)

Field getFileld(String name):返回单个公共成员变量对象。

Field getDeclaredField(String name):返回任意的成员变量对象

获取到成员变量后,可以获得其所有的内容。

修改后的Student类

class Student {
private String name;
private int age;
public String gender;

public Student() {
}

public Student(String name) {
    this.name = name;
}

protected Student(int age) {
    this.age = age;
}

private Student(String name, int age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public int getAge() {
    return age;
}

public void setAge(int age) {
    this.age = age;
}

}
从Student类中去获取成员变量

import java.lang.reflect.Field;

public class getFieldTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
// 首先还是用Class类中的forName方法来从Student类中得到字节码对象
Class clazz = Class.forName(“Student”);

    // 获取所有的public成员变量
    Field[] fields = clazz.getFields();
    for(Field T : fields) {
        System.out.println(T);
    }
    System.out.println();
	
    // 获取所有的public和private成员变量
    Field[] fields2 = clazz.getDeclaredFields();
    for(Field T : fields2) {
        System.out.println(T);
    }
    System.out.println();
	
    // 获取单个的public成员变量
    Field fields3 = clazz.getField("gender");
    System.out.println(fields3);
    System.out.println();

    // 获取单个的public和private成员变量
    Field fields4 = clazz.getDeclaredField("name");
    System.out.println(fields4);
    System.out.println();

    Field fields5 = clazz.getDeclaredField("age");
    System.out.println(fields5);
    System.out.println();

    // 得到成员变量的字节码文件后,可以做到很多事情。
    // 获得成员变量的权限修饰符
    int modifiers = fields5.getModifiers();
    System.out.println(modifiers);
    System.out.println();

    // 获得成员变量的名字
    for(Field T : fields2) {
        System.out.println(T.getName());
    }

    // 获得成员变量的类型
    for(Field T : fields2) {
        Class<?> type = T.getType();
        System.out.println(type);
    }

    // 获得成员变量的值
    // 要注意的是,成员变量的值应该和具体的对象有关,所以需要传入一个对象的参数
    Student test = new Student("zhangsan");
    // 如果要获取到的成员变量是private的,那么可以用setAccessible函数来暂时修改其访问性
    Field name = clazz.getDeclaredField("name");
    name.setAccessible(true);
    String tempName = (String) name.get(test);
    System.out.println(tempName);

    // 修改成员变量的值
    name.set(test, "lisi");
    System.out.println(test.getName());
}

}
从字节码文件中获取成员方法
Class类中用来获取成员方法的方法:

Method[] getMethods():用来获取所有的public方法,获取的方法包含继承的方法

Method[] getDeclaredMethods():用来获取所有的方法,包括private,但是不包含继承的。

Method getMethod(String name, Class<?>… parameterTypes):用来获取单个的public修饰的方法。

Method getDeclaredMethod(String name, Class<?>… parameterTypes):用来获取单个的任意方法,包括private方法。

Method类中用来创建对象的方法:

Object invoke(Object obj, Object... args); 用来运行方法

参数1:用obj对象来调用方法。

参数2:调用方法所需要进行传递的参数。

获取方法和调用方法的代码示例:

写代码的过程中,发现需要注意的几点就是:

方法的执行是需要依赖一个具体的对象的,对象需要作为参数传入invoke函数。

    // 获得所有的public成员方法,包括继承得到的方法
    Method[] methods = clazz.getMethods();
    for(Method T : methods) {
        System.out.println(T);
    }
    System.out.println();

    // 获得所有的定义的方法,包括private方法,不包括继承得到的方法
    Method[] methods2 = clazz.getDeclaredMethods();
    for(Method T : methods2) {
        System.out.println(T);
    }
    System.out.println();

    // 获得单个的方法
    Method methods4 = clazz.getMethod("getName");
    // 方法的调用是依赖于具体的对象的
    Student test2 = new Student("jack");
    String ans = (String) methods4.invoke(test2);
    System.out.println(ans);

    // 获得单个的任意方法
    Method methods5 = clazz.getDeclaredMethod("setAge", int.class);
    methods5.invoke(test2,100);
    System.out.println(test2);

6 接口,lambda表达式和内部类
6.1 接口
6.1.1 接口的概念
首先,最重要的一点是,接口不是类,但是其具有类的一些特点。

接口描述的是一个类所需要去履行的服务的内容。

例如,Arrays类中,提供了一个sort方法, 这个sort方法会去要求类中实现了Comparable接口。

public interface Comparable() {
int compareTo(Object other);
}
使用关键字interface去定义一个接口。

在接口中,可以去声明方法,一个或者多个都可以,但是接口是不能包含任何的实例域的。

在SE 8的版本之前,接口中所声明的方法本来是一定不可以给定实现的,但是现在版本的SE允许给出一些简单的方法实现。

在接口中的所有方法,包括后面提到的静态常量都是默认为public访问修饰。

对于一个Employee类来说,可以这样去实现它的Comparable接口。

如果一个类需要实现一个接口,可以用implements关键字,同时如果有多个接口需要实现,都可以顺序写在后面。

class Employee implements Comparable{
private int id;
private double salary;

public Employee() {
    this.id = -1;
    salary = 100;
}

public Employee(int id) {
    this.id = id;
    salary = 100;
}

public int getId() {
    return id;
}

public double getSalary() {
    return salary;
}

@Override
public int hashCode() {
    return Integer.valueOf(id).hashCode() + Double.valueOf(salary).hashCode();
}

@Override
public String toString() {
    return getClass().getName() + "[id=" + Integer.valueOf(id).toString() + ",salary=" + Double.valueOf(salary).toString() + "]";
}

@Override
public int compareTo(Object o) {
    if(((Employee) o).id == this.id) {
        return 0;
    }
    else if(((Employee) o).id > this.id) {
        return 1;
    }
    else return -1;
}

}
用sort方法去测试Comparable接口用的方法。

package chapter5;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;

public class InterfaceTest {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 用反射的方法去获得Employ类的构造方法
Class clazz = Class.forName(“chapter5.Employee”);
Constructor constructor = clazz.getConstructor(int.class);

    // 用反射获得的构造方法去创建一个泛型类的列表
    ArrayList<Employee> employees = new ArrayList<>();
    for(int i = 0; i < 3; i ++) {
        employees.add((Employee) constructor.newInstance(3 - i));
    }
	
    // sort方法只能作用在一般的数组上,所以需要先转换为一般的数组
    Object[] employeesArray =  employees.toArray();
    Arrays.sort(employeesArray);

    for(Object T : employeesArray) { 
        System.out.println(T);
    }
}

}
需要注意的是,要记得,如果要去覆盖父类中的一个方法,不可以去缩窄这个方法的访问权限。

因此,对于一个接口中的方法,定义的时候,被接口默认为了public,那么,在实现的时候,就必须去显示声明为public。

6.1.2 接口的特性
首先,接口不是一个类,其中含有还没有被实现的方法。因此,接口是一定不能被实例化的。

不过不要忘记的是,接口被类进行implements实现的时候,本质上仍然是一个继承的关系,因此可以用接口去定义一个接口的变量。

package chapter6;

public class interfaceTest implements Comparable {

@Override
public int compareTo() {
    return 0;
}

public static void main(String[] args) {
    // interfaceTest这个类实现了Comparable接口,因此其本质上是Comparable接口的一个子类
    Comparable p = new interfaceTest();

    if(p instanceof Comparable) {
        System.out.println("Yes");
        p = (Comparable) p;
        System.out.println(p.compareTo());
    }
}

}

interface Comparable {
public abstract int compareTo();
}
有一个非常有意思的事情,虽然我无法去直接实例化一个interface。

但是我可以将一个实现了interface的类的引用变量强制转换为interface类型。

在接口中, 一般会去定义的是一些类所共通的方法,以及一些常量。

接口中所声明的方法都会被默认为public,同时接口中被定义的变量都会被默认为是public static final的静态常量。

不推荐在写Java接口的时候去显示地去指明这些前缀。

允许在接口中只存在方法或者常量的声明。

6.1.3 接口和抽象类
至于为什么已经虚基类了,还需要去定义这么的一些接口。

是因为,虚基类在Java中,只能被继承一个,但是接口没有继承的数量的限制。一个类可以同时实现很多个接口。(最本质的原因)

感觉,接口就是用来补偿Java在设计之初所导致的无法实现多继承的一个机制。

同时,多继承这件事情本身会为语言带来非常复杂的实现机制。同时也会导致语言的效率降低。

总结而言就是,接口可以提供多继承的好处,但是同时也可以避免多继承的复杂性和低效性。

使用接口的好处有很多。

可以从另外一个角度实现多继承。Java是不允许有多继承的。可以使用接口来扩展类的功能。

对于一个实现了接口的类,可以非常明显地知道这个类可以实现的功能。更加清楚一个类的功能。

便于快速去实现业务的切换。可以便于进行接口编程。使用相同的接口去指向不同的实现类即可。(一个接口可以被多个不同的类实现,同时因为接口一旦没implements其中的所有的虚方法都会被实现,可以通过灵活切换接口引用变量所指向的实现类,来实现不同的方法实现调用)。

使用接口实现快速业务切换的例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先,面向对象的编程思想在于将实体抽象为类进行表示。

在这个例子中,就可以将学生抽象为一个Student类,将班级抽象为一个Class类,将两套不同的成绩查看方法抽象为同时实现一个接口的两个不同的实现类。

通过在Class类中采用不同的实现类就可以实现快速的业务切换。

package chapter6;

import java.util.ArrayList;

public class InterfaceTest {
public static void main(String[] args) {
Class test = new Class();
test.getStudentInfo();
test.getScoreInfo();
}
}

// 学生类,只包含,姓名,性别,成绩
class student {
public String name;
public String gender;
public double score;

student(String name, String gender, double score) {
    this.name = name;
    this.gender = gender;
    this.score = score;
}

student() {

}

public String getName() {
    return name;
}

public String getGender() {
    return gender;
}

public double getScore() {
    return score;
}

public void setName(String name) {
    this.name = name;
}

public void setGender(String gender) {
    this.gender = gender;
}

public void setScore(String gender) {
    this.gender = gender;
}

}

// 班级类,其实含有一个interface的实现类
class Class {
public ArrayList students;

// 在这个地方可以快速实现业务的切换
// 将ClassOperator1换成2就可以使用不同的业务实现逻辑
ClassOperator operator = new ClassOperator2();

Class() {
    students = new ArrayList<student>();
    students.add(new student("Jack", "male", 100));
    students.add(new student("Chlorine", "male", 99));
    students.add(new student("CheLessm", "female", 98));
}

void getScoreInfo() {
    operator.getScoreInfo(students);
}

void getStudentInfo() {
    operator.getStudentInfo(students);
}

}

// 班级数据操作接口,提供实现类必须实现的方法
interface ClassOperator {
double getScoreInfo(ArrayList students);
void getStudentInfo(ArrayList students);
}

// 接口的一个实现类,其实表示的是第一套业务实现流程
class ClassOperator1 implements ClassOperator {
public void getStudentInfo(ArrayList students) {
for(student it : students) {
// cout << it.getName() << " " << it.getGender() << " " << it.getScore() << endl;
System.out.println(it.getName() + " " + it.getGender() + " " + it.getScore());
}
}

public double getScoreInfo(ArrayList<student> students) {
    int sum = 0;
    int cnt = 0;
    for(student it : students) {
        cnt = cnt + 1;
        sum += it.getScore();
    }
    System.out.println((double)sum / cnt);
    return (double)sum / cnt;
}

}

// 接口的二个实现类,其实表示的是第二套业务实现流程
class ClassOperator2 implements ClassOperator {
public void getStudentInfo(ArrayList students) {
int cnt_male = 0;
int cnt_female = 0;
for(student it : students) {
if(it.getGender() == “male”) cnt_male ++;
else cnt_female ++;
System.out.println(it.getName() + " " + it.getGender() + " " + it.getScore());
}
System.out.println("Man: " + cnt_male + " Woman: " + cnt_female);
}

public double getScoreInfo(ArrayList<student> students) {
    int cnt = 0;
    double max = -1;
    double min = 101;
    double sum = 0;

    for(student it : students) {
        cnt = cnt + 1;
        if(it.getScore() > max) max = it.getScore();
        else min = Math.min(min, it.getScore());
        sum += it.getScore();
    }

    sum -= max;
    sum -= min;
    cnt -= 2;

    System.out.println((double)sum / cnt);
    return (double)sum / cnt;
}

}
6.1.4 静态方法和默认实现
在JDK8以后,Java允许接口中定义特殊的的方法

允许给方法提供一个默认的实现(允许方法不再是一个抽象方法)。

但是要注意的是,这个方法只是被public修饰,不能被接口使用. 进行调用,可以通过实现类进行调用。

package chapter6;

public class defaultInterface {
public static void main(String[] args) {
A test = new B();
test.Hello();
}
}

interface A {
default void Hello() {
System.out.println(“Hello”);
}
}

class B implements A {

}
允许提供静态方法。(就可以使用接口直接进行调用)

package chapter6;

public class defaultInterface {
public static void main(String[] args) {
B.Hello();
}
}

interface B {
static void Hello() {
System.out.println(“Hello”);
}
}
允许定义private方法。(private方法只能被本接口中的方法调用)

package chapter6;

public class defaultInterface {
public static void main(String[] args) {
D test = new D();
test.getHello();
}
}

interface C {
private void Hello() {
System.out.println(“Hello”);
}

default void getHello() {
    Hello();
}

}

class D implements C {

}
JDK8增加这些方法的目的在于去增强接口的表达能力,便于项目的扩展和维护。

6.1.5 默认方法冲突
因为现在允许对接口中的方法提供一个默认实现,就会导致很多情况出现默认方法冲突。

Java中,提供了很简单的应对规则。

子类和接口中的方法冲突。超类优先。(子类优先)

package chapter6;

public class defaultInterface {
public static void main(String[] args) {
D test = new D();
test.Hello();
}
}

interface A {
default void Hello() {
System.out.println(“Hello”);
}
}

class D implements A {
// 在重写方法的时候,不能提供更窄的访问权限。
// interface中的方法,会被默认标记为public。
public void Hello() {
System.out.println(“Override Hello”);
}
}
两个接口间中的默认方法的冲突。

就必须在实现类中去覆盖这个方法,然后人为去选择一个接口中的方法进行调用。

需要额外特别注意的是,这里所使用的super关键字不是为了去调用父类中的内容,而是为了显式指定调用哪一个接口中的方法。

package chapter6;

public class defaultInterface {
public static void main(String[] args) {
C test = new C();
test.Hello();
}
}

interface A {
default void Hello() {
System.out.println(“Hello From A”);
}
}

interface B {
default void Hello() {
System.out.println(“Hello From B”);
}
}

class C implements A, B {
public void Hello() {
A.super.Hello();
}
}
继承的父类中有和实现的接口所冲突的方法。

在这种情况下,记住,始终是类中的方法优先。

接口中的同名方法都会被类中的方法所覆盖。

package chapter6;

public class defaultInterface {
public static void main(String[] args) {
C test = new C();
test.Hello();
}
}

class A {
public void Hello() {
System.out.println(“Hello From class A”);
}
}

interface B {
public default void Hello() {
System.out.println(“Hello From interface B”);
}
}

class C extends A implements B {

}
6.2 接口示例
6.2.1 Comparator接口
如果想要一个对一个对象数组进行排序,最常见的方法就是使用Arrays.sort方法。

使用Arrays.sort方法的前提就是,这个对象,是实现了Comparale接口了的。

还有一个使用sort方法的途径,就是,传入一个comparator的接口实例。

要注意的是,上述的两种方法是完全不同的。

使用Comparable接口实现的例子:

package chapter6;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class ComparatorTest {
public static void main(String[] args) {
number[] numberList = new number[3];

    numberList[0] = new number(1);
    numberList[1] = new number(3);
    numberList[2] = new number(2);

    Arrays.sort(numberList);

    for(Object T : numberList) {
        System.out.println(((number)T).num);
    }
}

}

class number implements Comparable{
int num;

number() {
    this.num = -1;
}

number(int num) {
    this.num = num;
}

@Override
public int compareTo(Object o) {
    return this.num - ((number)o).num;
}

}
使用Comparator接口实现的例子:

package chapter6;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class ComparatorTest {
public static void main(String[] args) {
number[] numberList = new number[3];

    numberList[0] = new number(1);
    numberList[1] = new number(3);
    numberList[2] = new number(2);

    Arrays.sort(numberList, new numberComparator());

    for(Object T : numberList) {
        System.out.println(((number)T).num);
    }
}

}

class number{
int num;

number() {
    this.num = -1;
}

number(int num) {
    this.num = num;
}

}

class numberComparator implements Comparator {

@Override
public int compare(number o1, number o2) {
    return o1.num - o2.num;
}

}
6.2.2 Cloneable接口
首先,如果想要去克隆一个对象,最简单想到的就是直接用=去进行复制。

但是对于类对象的引用变量而言,使用 = 只会导致浅复制,两个类变量会同时指向同一块内存区域。

这样很明显是不OK的,复制的本意就是去创造两个完全独立的对象。

image-20231013201205992

一般来说,在默认的Object类中,提供了一个clone方法,但是这个clone方法只能提供最简单的浅复制。(实际上,使用super.clone()方法就是去调用的Object类中的clone方法)

浅复制一定就是坏的吗?其实不然,如果数据域中存在不可变类型的数据,比如String等,实际上指向同一块内存区域也没啥,反而可以节省内存大小。因此,最为理想的clone方法的实现是,不可变对象浅复制,可变对象深度复制。

以下是实现一个Cloneable接口的实例:

package chapter6;

import javax.xml.crypto.Data;
import java.util.Date;

public class cloneableTest {
public static void main(String[] args) throws CloneNotSupportedException {
test A = new test(“A”);
test B = (test) A.clone();

    A.setTime(new Date(14686531L));

    System.out.println(A.getTime());
    System.out.println(B.getTime());
}

}

class test implements Cloneable {
public String name;
public Date time;

test(String name) {
    this.name = name;
    time = new Date();
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Date getTime() {
    return time;
}

public void setTime(Date time) {
    this.time = time;
}

public Object clone() throws CloneNotSupportedException {
    // 调用Object类中的clone方法去
    test cloned = (test)super.clone();

    // 因为Date类是可变的,所以需要为time变量进行一个深复制
    cloned.time = (Date) time.clone();

    return cloned;
}

}
这里需要解释一下的是,为什么time调用一次clone方法就变成了深度拷贝。

假如,不对time调用一个clone方法,那么test调用的clone方法会进行浅复制,A中的time直接 = B中的time,指向同一个区域,是浅复制。但是如果,在clone方法中,对于time单独调用一次clone方法,虽然,也是浅复制,但是Date中的数据域会被复制到一个新的类中(应该不是类引用变量),所以对于Date类而言,浅复制就是深复制,B中的time会是一个单独的额外的time。

6.3 lambda表达式
lambda表达式的目的就在于去简化匿名内部类的写法。

首先,不是所有的匿名内部类都可以用lambda表达式进行重写。

lambda只能简化,函数式接口的匿名实现类,的对象的定义形式。

函数式接口?函数式接口就是接口内只有一个抽象方法的接口。其会被用@FunctionalInterface进行标记。

6.3.1 lambda表达式的写法
lambda表达式的写法就是:(形参列表) -> {方法体}

这个表达式会用来重写接口中的抽象函数。(因为接口中只有一个抽象函数)

public class LambdaTest {
public static void main(String[] args) {
Animal dog = ()->{System.out.println(“Wang Wang Wang”);};
dog.go();
}
}

@FunctionalInterface
interface Animal {
abstract public void go();
}
实际上,有很多常用的接口都是函数式接口,都可以通过lambda表达式来简化书写的格式。

sort方法中要用到的Comparato接口,只有一个compare方法,可以简化。

Arrays.sort(dogForSorts, new Comparator() {
@Override
public int compare(DogForSort o1, DogForSort o2) {
if(o2.age > o1.age) return 1;
else if(o2.age == o1.age) return 0;
else return -1;
}
});
for(Object it : dogForSorts) System.out.println(((dog)it).Id);

Arrays.sort(dogForSorts, (DogForSort o1, DogForSort o2)->{return o2.age - o1.age;});
for(Object it : dogForSorts) System.out.println(((dog)it).Id);
Arrays.setAll方法中的修改器接口,只有一个apply方法,可以简化。

Arrays.setAll(dogs_all, new IntFunction() {

@Override
public dog apply(int value) {

    return new dog(((dog) finalDogs_all1[value]).Id + "OK");
};

});
for(Object it : dogs_all) System.out.println(((dog)it).Id);

Arrays.setAll(dogs_all,(value)->{return new dog(((dog) finalDogs_all1[value]).Id + “OK”);});
for(Object it : dogs_all) System.out.println(((dog)it).Id);
6.3.2 lambda表达式更近一步的简化
lambda表达式追求最为简洁的书写格式。

因此,还提供了额外的3种简化规则:

参数类型可以不写。(自己反推的时候,由抽象方法告知具体的类型)

Arrays.sort(dogForSorts, (DogForSort o1, DogForSort o2)->{return o2.age-o1.age;});
Arrays.sort(dogForSorts, (o1, o2)->{return o2.age-o1.age;});
当只有一个参数的时候,不用写形参列表外面的括号。

Arrays.setAll(dogs_all,(int value)->{return new dog(((dog) finalDogs_all1[value]).Id + “OK”);});
Arrays.setAll(dogs_all, value->{return new dog(((dog) finalDogs_all1[value]).Id + “OK”);});
如果,方法体中,只有一句话,那么不用写方法体的{},也不用写最后的分号,如果是return语句,甚至可以不写return。

Arrays.setAll(dogs_all, value->new dog(((dog) finalDogs_all1[value]).Id + “OK”));
6.3.3 方法引用
方法引用的目的在于,更进一步去简化lambda表达式。

(方法引用的标志和C++中的域操作符号如出一辙:“::”)

实际上,对于语法的省略并不是硬性要求的。只是说,看到这种吊人写的吊代码,看得懂就行。

静态方法引用
静态方法引用的使用:

ClassName::staticMethod;
如果,在lambda表达式中,只使用了一个静态方法,而且,前后的参数列表一致,那么就可以使用静态方法引用。

// 最常见的版本
Arrays.sort(students_object, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(((student) o1).score > ((student) o2).score) return 1;
else if(((student) o1).score < ((student) o2).score) return -1;
else return 0;
}
});

// 一般的lambda表达式简化后的结果
Arrays.sort(students_object, (o1, o2)->{ return Double.compare(((student)o1).score, ((student)o2).score); });

// 更进一步的省略
Arrays.sort(students_object, (o1, o2)-> Double.compare(((student)o1).score, ((student)o2).score) );

// 静态方法不省略
Arrays.sort(students_object, (o1, o2) -> CompareByScore.compare(o1, o2));

// 静态方法省略
Arrays.sort(students_object, CompareByScore::compare);

class CompareByScore {
public static int compare(Object o1, Object o2) {
return Double.compare(((student)o1).score, ((student)o2).score);
}
}
这里可以细致的看一下。

Arrays.sort(students_object, (o1, o2) -> CompareByScore.compare(o1, o2));
前面的参数列表是(o1, o2),后面传入compare函数的参数列表也是(o1, o2)。

因此,可以使用方法引用来简化方法调用的方式。

实例方法引用
实例方法的使用:

ClassName::InstanceMethod;
如果Lambda表达式中只调用了一个实例方法,且前后参数列表一致的话,就可以使用实例方法来进行简化。

和静态方法引用不同的是,要调用实例方法,就必须要先定义一个实例。

// 实例方法不省略
Arrays.sort(students_object, (o1, o2) -> test.compare(o1, o2));

// 实例方法省略
Arrays.sort(students_object, test::compare);
特定类型的方法引用
特定类型的方法引用的写法是:

TypeName::TypeMethod;
当lambda表达式中,只有一个实例方法,且第一个参数是方法的主调,且后面的参数都是传入该方法的参数,则可以调用。

// 特定类型的方法引用
String[] strs = {“Hello”, “World”};

Arrays.sort(strs, new Comparator() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
对这个例子就可以看出,第一个参数o1最为compare方法的主调,后面的参数o2传入compare方法。

因此,可以使用特定类型的方法引用,这里的特定类型就是String类型,因为调用的compare方法就是来自于String类的。

可以先使用不省略的方法引用:

Arrays.sort(strs, (o1, o2)->o1.compareTo(o2));
省略后的代码如下:

Arrays.sort(strs, String::compareTo);
构造器引用(非常少用)
使用构造器引用的写法:

ClassName::new;
如果在一个lambda表达式中,只去创建对象,且参数列表前后一致的话,就可以使用构造器引用。

// 构造器引用
createStudent CS = new createStudent() {
@Override
public student create(String name, String gender, double score) {
return new student(name, gender, score);
}
};

// 不省略的构造器引用
CS = (String name, String gender, double score) -> {return new student(name, gender, score);};

// 省略的构造器引用
CS = student::new;
6.4 内部类
内部类,本质上还是一个类。

内部类是定义在一个类中的类,包含了一个类的所有成分,但是这个类又没有必要单独设计的时候就可以设计为内部类。

public class Car {
public class Engine {
// …
}
}
6.4.1 成员内部类
成员内部类,依赖于外部类而存在。

因此,不可以直接去new一个成员内部类。

一个类中存在两个类,那么按道理,构造方法应该都要调用。

但是因为内部类自己是无法单独创建对象的,所以内部类的构造函数不可以单独调用。

因此,一个合理的设计方法就是,先创建外部类,再创建内部类:

Outer.Inner in = new Outer().new Inner();
成员内部类,可以简单当作一个在类中定义的方法,因此,外部类中的所有内容对于内部类而言都是可见的。

如果内部类中定义了和外部类相同的变量,那么应该怎么各自访问?

// 使用this引用,可以访问内部自己的变量
System.out.println(this.age);

// 使用Outer.this,可以访问外部类自己的变量
System.out.println(Outer.this.age);
以下是一个内部类的实例;

package chapter6;

public class Outer {
private int age = 3;
public static String a;

public class Inner {
    private int age = 2;
    private String name;
    public static String schloolName; // JDK16后

    public void test() {

    }

    public void test2() {
        System.out.println(name);
        System.out.println(schloolName);

        System.out.println(age);
        System.out.println(a);
    }

    public void test3() {
        int age = 1;

        System.out.println(age);
        // this 访问当前类中的数据域
        System.out.println(this.age); // 2
        // 使用Outer.this 访问外部类的数据域
        System.out.println(Outer.this.age); // 3
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

}
6.4.2 静态内部类
静态内部类就是使用static所修饰定义的内部类。

静态内部类的成员也没有限制,都可以包含,并且访问权限也可以按照需要进行设计。

静态内部类和成员内部类不同的一点是,其可以单独被创建出来,而不需要先创建一个外部类,因为其实际上不依赖于对象存在。

package chapter6;

public class InnerClass {
public static void main(String[] args) {
Outer.StaticInner test = new Outer.StaticInner(“Jack”);
test.test();
}
}
静态内部类可以被当作是一个在类中定义的一个静态方法。

因此,在静态内部类中,只能去访问外部类的静态成员,无论在内部中是怎么去设计访问权限的,对于外部类而言,静态内部类中的所有东西都是静态的。

如果是定义了的同名变量,可以通过static成员自有的属性,利用类名字去显式调用。

package chapter6;

public class Outer {
private static int age = 1;
public static String a;

public static class StaticInner {
    private String name;
    private static int age = 2;

    public void test() {
        int age = 3;
        
        System.out.println(age); // 3
        System.out.println(StaticInner.age); // 2
        System.out.println(Outer.age); // 1
    }

    public StaticInner(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static int getAge() {
        return age;
    }

    public static void setAge(int age) {
        StaticInner.age = age;
    }
}

}
6.4.3 局部内部类
局部内部类就是定义在方法中,代码块,构造器中的类。

鸡肋语法,不要管。

6.4.3 匿名内部类(重要)
匿名内部类,是一种特殊的内部类,其特殊在于,不需要去为这个类声明名字。

匿名内部类主要的场景就是,某个地方需要一个类,但是又没有必要单独写一个类。

其一般定义的语法为:

// 这个地方,黑马应该是写错了,定义匿名内部类的时候,是不会传入参数的
new class/interface (parameters) {
// 一般去重写方法
}
以下是一个匿名内部类的实例:

package chapter6;

import java.awt.*;

public class innerClass_no_name {
public static void main(String[] args) {
Animal a = new cat();
a.cry();

    // 匿名内部类会被当作是一个Animal的子类被创建出来,所以可以用Animal类型进行引用
    Animal b = new Animal() {

        @Override
        public void cry() {
            System.out.println("Mew Mew Mew");
        }
    };
    b.cry();
}

}

class cat extends Animal {

@Override
public void cry() {
    System.out.println("Mew Mew Mew");
}

}

abstract class Animal {
public abstract void cry();
}
匿名内部类通常作为参数传入到方法中。

public class innerClass_no_name {
public static void main(String[] args) {
// 创建的是swim的实现类
swim dog = new swim() {
@Override
public void swim() {
System.out.println(“Dog swims”);
}
};

    // 创建的是swim的实现类
    swim cat = new swim() {
        @Override
        public void swim() {
            System.out.println("cat swims");
        }
    };

    // 将内部类传入方法进行执行
    go(dog);
    go(cat);
    
    // 实际上,可以再简化一点,直接传入匿名内部类。
    go(new swim() {
        @Override
        public void swim() {
            System.out.println("Dog swims");
        }
    });
}

public static void go(swim a) {
    a.swim();
}

}

interface swim {
void swim();
}
8 Java 泛型
8.1 泛型是什么
泛型程序设计,就是在定义类,接口,方法的时候,可以同时声明的多个类型变量。

实际上,泛型程序设计,就是将类型本身也作为参数去进行传入。

之前所涉及到的ArrayList类就是一个泛型涉及的例子。(以下就是ArrayList的定义)

public class ArrayList extends AbstractList
可见,类型E作为参数,被传入到了ArrayList的定义中,因此,在这个类中就可以使用这个类型E。

比如以下的get方法,就是将获取到的index下标对应的对象作为类型E进行返回。

public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
为什么一定要把类型作为参数进行传入呢?

实际上,在ArrayList类中,是可以不显式指定类型的。

package chapter6;

import java.util.ArrayList;

public class ArrayListTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(“Hello”);
Object temp1 = test.get(0);
Object temp2 = (String) test.get(0);
System.out.println(temp2);
}
}
但是,这样做得坏处在于,ArrayList中,所有的类型E就会退化为Object。

因此,ArrayList的add函数,可以接受任何类型的对象,也就是,ArrayList现在可以存入任何类型的对象。

同时,ArrayList的get方法,也只能将对象按照Object类型进行返回。

package chapter6;

import java.util.ArrayList;

public class ArrayListTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(“Hello”);

    test.add(new Animal() {

        @Override
        public void cry() {
            System.out.println("FUCK");
        }
    });

    for(Object it : test) {
        if(it instanceof String) {
            System.out.println(it);
        }
        else if(it instanceof Animal) {
            ((Animal) it).cry();
        }
    }
}

}
看吧,这就体现出问题了,如果不将类型作为参数传入ArrayList去约定ArrayList类中的变量类型,就会导致,我没办法确定我现在取出来的变量类型,因此,我就无法正确地去操作取出来地这个类对象。

因此,对于ArrayList而言,最正确的使用方法就是,在定义的时候,指定成员的类型。

这样的话,就可以清楚的知道,所取出来的对象是什么类型,该怎么去操作。

package chapter6;

import java.util.ArrayList;

public class ArrayListTest {
public static void main(String[] args) {
ArrayList test = new ArrayList();
test.add(“Hello”);
test.add(“World”);

    for(String it : test) {
        System.out.println(it);
    }
}

}
总结而言,使用泛型来提供类型变量的主要的好处就有:

指明可以操作的数据类型。

可以自动进行类型检查,避免额外的强制类型转换。

8.2 泛型类
为什么我们需要去定义一个泛型类?

因为,在有些地方,我们会想去指定,在这个类中可以操作的数据类型是什么。

泛型类的定义方法:

class name <type1, type2, type3 …> {
// statement
}
以下是一个定义泛型类的具体的例子:(模拟一下ArrayList)

class MyArrayList {
Object[] array;
private int index;

public MyArrayList() {
    index = 0;
    array = new Object[100];
}

public boolean add(E e) {
    array[index ++] = e;
    return true;
}

// 这里需要注意的一点是,array是Object类型的,如果返回值写E的话,相当于用一个E的引用变量指向一个Object类型,
// 子类是不可以在不经过前置类型转换的前提下去指向一个父类对象的
public E get(int index) {
    return (E) array[this.index];
}

}
实际上,在定义的时候,所写明的类型变量Type,在方法体中,是不会知道这个Type是什么意思的,即使写的是String这种有含义的名称。

也就是说,给定了类型Type,目的不是在方法中去调用Type中的方法,而只是限定我这个类中,存储的对象,只有Type。

package chapter6;

public class geneticClass {
public static void main(String[] args) {
MyArrayList test = new MyArrayList<>();

    test.add(new Animal() {
        @Override
        public void cry() {
            System.out.println("Fuck");
        }
    });

    test.add(new Animal() {
        @Override
        public void cry() {
            System.out.println("World");
        }
    });

    System.out.println(test.index);

    Animal a = test.get(0);
    a.cry();
}

}

class MyArrayList {
Object[] array;
public int index;

public MyArrayList() {
    index = 0;
    array = new Object[100];
}

public boolean add(String e) {
    array[index ++] = e;
    return true;
}

public String get(int index) {
    return (String) array[index];
}

}
在这个例子中就可以看出,虽然我写的是String,但是我传入的类型是Animal,那么在类中,所有的String都会变成Animal。

在定义一个泛型类的时候,传入的泛型类型,还可以继承自某个父类。

class MyArrayList {
Object[] array;
public int index;

public MyArrayList() {
    index = 0;
    array = new Object[100];
}

public boolean add(T e) {
    array[index ++] = e;
    return true;
}

public T get(int index) {
    return (T) array[index];
}

}
有意思的一点是,即使我这么改写了代码,指定传入的类型T是继承自Animal。

上文中的测试类依旧可以运行,从这一点,也可以反面认定,匿名内部类所创建的就是该类的一个子类。

8.3 泛型接口
泛型接口的定义和泛型类的定义如出一辙:

有一个预定俗称的规定要记住,就是Type在实现的时候,一般都会给一个大写的字母表示。

public interface geneticInterface <Type1, Type2, Type3, …> {
// statment
}
现在有一个需求,有一个Teacher类,有一个Student类。

他们都需要实现两个方法,一个是add,一个是按照名字查找。

一般来说,这种通用的方法,都会写到一个operator接口中,然后让这两个类各自去实现。

但是问题是,现在操作的数据不一样了,一个类中要操作的数据是Student,一个类中操作的数据是Teacher。

那么, 就可以把这个接口定义为一个泛型接口,具体的类型在他们各自被实现的时候去指定就好啦。

interface
Student
Teacher
泛型接口的定义:

package chapter6;

import java.util.ArrayList;

// 泛型接口
public interface geneticInterface {
void add(T t);
ArrayList getByNames(String name);
}
Student类的定义:

package chapter6;

import java.util.ArrayList;

public class Student implements geneticInterface{

@Override
public void add(Student student) {

}

@Override
public ArrayList<Student> getByNames(String name) {
    return null;
}

}
Teacher类的定义:

package chapter6;

import java.util.ArrayList;

public class Teacher implements geneticInterface{

@Override
public void add(Teacher teacher) {

}

@Override
public ArrayList<Teacher> getByNames(String name) {
    return null;
}

}
需要注意的是,泛型接口在被implements的时候,也需要去指定泛型的类型参数是什么。

对于接口而言,泛型提供了同一个接口去操作不同类型的数据的能力。

8.4 泛型方法
定义泛型方法的语法和泛型类泛型接口的语法基本一致。

需要注意的是,类型变量一旦被定义了,就可以在方法的任意地方使用,包括在返回值的类型中。

访问修饰符号 static <类型变量> 返回值类型 方法名(parameters) {
// …
}
以下是一个泛型方法的例子:

public static T test(T t) {
return t;
}
泛型方法有一个很浅显的使用场景。

同一个方法,需要去处理不同类型的参数。

对于如下的这个代码,很自然得,我想把go函数的参数定义为ArrayList 类型,来处理ArrayList和ArrayList的数据。

对于ArrayList而言,不存在所谓的继承关系,Car类型的列表是无法用来指向BMW和BENZ类型的列表的,他们之间一点关系没有。

因此,这个地方就可以采用泛型方法,

package chapter6;

import chapter5.ArraylistTest;

import java.util.ArrayList;

public class geneticFunction {
public static void main(String[] args) {
ArrayList bmws = new ArrayList<>();
ArrayList benzs = new ArrayList<>();

    // go(bmws);
    go(bmws);
    go(benzs);
    
    // 不希望传入的对象
    go(new Dog());
}

public static <T> void go(ArrayList<T> t) {

}

}

class Car {}

class BMW extends Car {}

class BENZ extends Car {}
这么写go函数会存在一个问题,就是,T可以表示任何类型,也就是说,其他类型的的数据也可也传入go函数当中。

因此,我们可以去限定类型变量T的范围,可以通过extends关键字,来定义其必须为某个类的子类。

public static void go(ArrayList t) {

}
在声明一个方法的时候,所使用的类型变量T,就可以表示任意类型的类型。

在使用泛型的地方,使用?就可以表示一切类型。

同时 ? extends Car 表示 上边界,表示该类型必须是Car的子类,? super Car表示下边界,该类型必须是Car的父类。

可以使用类型通配符:?

public static void go(ArrayList<? extends Car> t) {

}
实际上,就是,使用?可以不用写。类型通配符号就是使用泛型的一个简单的写法。

8.5 泛型的注意事项
8.5.1 泛型擦除
泛型只存在于编译之前,在文件编译完成之后,是没有泛型的。

泛型只是在编译之前用来约束可以操作的数据类型。

实际上,在编译之后,所有的泛型都会变回Object类型和强制类型转换。

这个是编译之前的代码。

package chapter6;

import java.util.ArrayList;

public class geneticErase {
public static void main(String[] args) {
ArrayList List = new ArrayList<>();
List.add(“Hello”);
List.add(“World”);

    for(int i = 0; i < List.size(); i ++) {
        String temp = List.get(i);
        System.out.println(temp);
    }

}

}
这个是编译之后的class文件,经过反编译后的代码。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package chapter6;

import java.util.ArrayList;

public class geneticErase {
public geneticErase() {
}

public static void main(String[] args) {
    ArrayList<String> List = new ArrayList();
    List.add("Hello");
    List.add("World");

    for(int i = 0; i < List.size(); ++i) {
        // 这个地方就变成了强制类型转换
        String temp = (String)List.get(i);
        System.out.println(temp);
    }

}

}
8.5.2 不可作用于基本类型
ArrayList中如果要去定义一个泛型的话,在声明的时候,类型变量不可以传入基本类型。

// 这种写法是完全错误的
ArrayList test = new ArrayList<>();
如果要去使用基本数据类型的话,可以使用他们的包装类。

// 这种写法是可以的
ArrayList test = new ArrayList<>();
在定义一个ArrayList的时候,写的int类型是不会发生自动装箱的。

自动装箱体现在,我一旦声明了一个Interger类型的ArrayList,我可以直接add一个int类型的数据。

test.add(12);
9 常用API
API指的就是编程接口,是Java自己已经写好的程序,程序员可以通过调用接口来实现一些功能,提升效率。

主要比较常用的接口都在Base包下面。

9.1 Object类型
Object类型,是所有类型的祖宗类,一切的类都会直接或者间接去继承Object类。

在Object类中提供了很多的方法来实现一些基本的功能。

9.1.1 equals方法
Object类中的equals方法,只是简单的去判断本对象和待比较的对象的地址是否相同。

也就是说,两个内容完全相同的对象,直接调用equals方法,会返回不相等的结果。

但是我们希望的是,equals方法是通过两个对象的内容来判断是否相等。

所以,equals方法,存在的意义就是被子类重写,改写为按照对象内容进行比较。

一般来说,一旦重写equals方法,就要重写hashcode方法。(因为hashmap中要使用hashcode和equals来判断桶)

以下是重写equals方法例子:

class student_equals {
String name;
int age;
double[] score;

student_equals() {

}

public student_equals(String name, int age, double[] score) {
    this.name = name;
    this.age = age;
    this.score = score;
}

@Override
public boolean equals(Object obj) {
    // 先检查是不是同一个对象在比较
    if(this == obj) return true;
    // 再检查进行比较的两个对象是不是同一个类的
    if(obj == null || obj.getClass() != this.getClass()) return false;

    // 进行强制类型转换
    student_equals t = (student_equals) obj;

    // 依次比较各个成员变量是否相等
    return age == t.age && Objects.equals(name, t.name) && Arrays.equals(score, t.score);
}

}
9.1.2 clone方法
clone方法的本意是返回一个和本对象完全相同的对象。

但是Object类中的clone方法的实现方法是简单的浅复制。

对于基本数据类型而言,浅复制是OK的,但是对于对象变量而言,浅复制就会导致多个变量指向同一个对象的内存区域。

因此,clone方法存在的意义,也是要在类中进行重写,变成,对类对象变量进行按需求深复制,基本数据类型浅复制。

以下是一个clone方法重写的例子:

class student_equals implements Cloneable {
String name;
int age;
double[] score;

student_equals() {

}

public student_equals(String name, int age, double[] score) {
    this.name = name;
    this.age = age;
    this.score = score;
}

@Override
protected Object clone() throws CloneNotSupportedException {
	// 先用Object类中的clone方法,浅复制一份
    student_equals temp = (student_equals) super.clone();
    // 对于需要使用深复制的成员,按照需求进行额外的深度复制即可
    // 这个地方是因为,数组是重写了clone方法的,可以直接进行调用
    temp.score = temp.score.clone();

    return temp;
}

}
另外,需要注意的是,不是所有的类都可以被clone。

一般来说,对于允许进行clone的类,都会去实现Cloneable接口。

Cloneable接口中,没有任何方法需要实现,其只作为一个标记的功能,这样的接口也被称为,标记接口。

9.1.3 toStirng方法
见 5.2.4 toString方法的描述。

9.2 Objects类型
Objects类型是一个纯粹的工具类。其中提供了用来操作对象的静态方法。

同时,Objects类型是一个final类,其不可以被继承。

Objects被设计的目的就是用来调用其中的静态函数来操作对象,而不是去实例化一个对象。

其中提供了一些方法来进行调用。

9.2.1 equals方法
和Object类中的equals方法不同,前者的equals方法是存在安全隐患的。

一般,调用equals方法都是用对象进行调用,这个时,我们并没有去检查test1的指向是否为空。

如果test1为空,那么调用equals方法就会报错。

test1.equals(test2);
Objects类中的equals方法,对Object类中的equals方法进行了改良,增加了一个非空检查。

以下是Objects类中的equals方法:

public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
9.2.2 isNull和nonNull方法
isNull用来判断,一个对象引用是否是空。

nonNull用来判单,一个对象引用是否是非空的。

package chapter6;

import java.util.Arrays;
import java.util.Objects;

public class equalsTest {
public static void main(String[] args) {
String str1 = null;
String str2 = new String(“Hello”);

    System.out.println(Objects.isNull(str1)); // true
    System.out.println(Objects.isNull(str2)); // false

    System.out.println(Objects.nonNull(str1)); // false
    System.out.println(Objects.nonNull(str2)); // true
}

}
9.3 wrap-up类
Java的核心思想在于,万物皆对象。

但是Java中的基本数据类型,却不是对象类型。

所以,就出现了包装类,其可以将基本数据类型变成类类型,也可也将包装类类型,变成基本数据类型。

以下以Interger类型为例子,来介绍基本的使用方法。

9.3.1 获得包装类的途径
获得一个Integer类的常见方法有两种,其中valueOf现在更为推荐,但是用String更为常用。

package chapter6;

public class warpUpClass {
public static void main(String[] args) {
// 用字符串构造
Integer test1 = new Integer(“123”);

    // 用基本数据类型构造
    Integer test2 = new Integer(123);

    // 用静态方法valueOf进行返回一个类对象
    Integer test3 = Integer.valueOf(123);
    Integer test4 = Integer.valueOf("123");

    System.out.println(test2);
}

}
包装类中,最重要的一点就是,自动装箱。(详细见 5.4 自动装箱)

9.3.2 包装类提供的方法
包装类中,还提供了其他的方法。

值转字符串:toString

toString方法,有静态版本和非静态版本。

静态版本,可以从Integer对象中,进行调用,返回当前对象的一个String类型。

Integer test = Integer.valueOf(123456);
String test_str = test.toString();
非静态版本,可以从Integer类中直接调用,返回传入int类型数值的一个String类型。

String test_str_2 = Integer.toString(123);
字符串转换为值:parseInt,valueOf

parseInt只有一个静态版本,返回传入String类型的一个int类型的对应值。

int test_num = Integer.parseInt(“1234”);
valueOf方法比较特殊,其重载了很多个不同的版本。

总结而言就是,其只有静态版本。

valueOf,哪个对象调用了valueOf,最后就会变成哪个类型,无论传入的是int类型,还是String类型。

String temp_str = String.valueOf(123);
最常用的还是valueOf方法。

package chapter6;

public class warpUpClass {
public static void main(String[] args) {
// 非静态toString 方法,将数值转换为字符串
Integer test = Integer.valueOf(123456);
String test_str = test.toString();

    // 静态的toString 方法,将数值转换为字符串
    String test_str_2 = Integer.toString(123);

    // 也可也用String类中的静态valueOf方法,要明确的一点是,哪个类调用vlaueOf方法,参数最终就会变成什么类型
    String test_str_3 = String.valueOf(123);

    // 使用parseInt将字符串转换为数值类型,parseInt只有静态类型
    int test_num = Integer.parseInt("1234");

    // 也可也使用,valueOf的静态方法
    test_num = Integer.valueOf("1234");

    System.out.println(test_str);
}

}
另外,包装类还有一个特殊的性质就是,在和字符串进行运算的时候,基本数据类型会被优先转换为String类型。

String ans = “123” + 12; // “12312”
9.4 StringBuilder类
StringBuilder是为了弥补String类型不可变的缺点,而诞生的可变字符串对象。

StringBuilder是一种容器,容器的元素是字符,用来修改字符串,效率更高吗,,代码更加简洁。

其提供了一些方便的操作字符串的方法:

9.4.1 常用方法
append方法

append方法可以在容器后面,添加任意的基本数据类型。

package chapter6;

public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder test = new StringBuilder();

    test.append("123");
    System.out.println(test);

    test.append(123);
    System.out.println(test);

    test.append("123.2");
    System.out.println(test);

}

}
需要注意的是,append方法会返回this,也就是,append方法添加内容后,会返回这个对象的引用。

从而,append方法可以支持链式编程。

package chapter6;

public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder test = new StringBuilder();
test.append(“123”).append(123).append(“123.2”);
System.out.println(test);
}
}
上述两份代码的执行结果是完全相同的。

使用StringBuilder的好处在于:

使用StringBuilder的效率比直接使用String类型进行操作的效率高很多,尤其是频繁的拼接和修改。

用String去直接修改的例子:

package chapter6;

public class StringBuilderTest {
public static void main(String[] args) {
String test = new String(“”);
for(int i = 0; i < 1000000; i ++) {
// 这个地方是创建了新的对象返回,String对象还是不可变类
test = test + “123”;
}
System.out.println(test);
}
}
用StringBuilder去进行修改的例子:

package chapter6;

public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder test = new StringBuilder();
for(int i = 0; i < 1000000; i ++) {
// 这个地方是创建了新的对象返回,String对象还是不可变类
// test = test + “123”;
test.append(“123”);
}
System.out.println(test);
}
}
但是如果不会对字符串进行频繁的操作的话,其实更建议使用String方法。

reverse方法

reverse方法,可以反转一个字符串。

注意,reverse方法,不会返回一个逆转的字符串对象,其会就地对字符串进行反转。

package chapter6;

public class StringBuilderTest {
public static void main(String[] args) {
StringBuilder test = new StringBuilder();
test.append(“Hello World”);
test.reverse();
System.out.println(test);
}
}
substring方法

substring有两个版本,其中可以从index开始截取到尾部的所有字符,也可也指定截取的范围 [start, end)

System.out.println(test.substring(1)); // ello World
System.out.println(test.substring(1,3)); // el
toString方法

toString方法会将一个stringBuilder对象中的字符串进行返回。

同时,可以直接去打印一个toString方法,会自动调用toString方法。

System.out.println(test.toString());
length方法

length方法会直接返回对象中字符串的长度。

System.out.println(test.length());
9.4.2 StringBuffer
StringBuffer的用法和StringBuilder完全一致。

只是StringBuilder是线程不安全的,StringBuffer是线程安全的。

所谓的线程安全,就是多个用户同时使用StringBuffer进行操作的时候,不会出错。

9.4.3 StringJoinner
通常,在进行字符串拼接的过程中,对于间隔符号,开始和结束的符号都有一定特殊的要求。

如果使用StringBuilder的话,就需要严格判断其下标进行特殊处理,非常麻烦。

使用StringJoinner的话,就可以指定间隔符号,开始和结束的符号,非常方便。

总结而言,对于字符串拼接而言,更加快速,高效,代码简洁。(便于字符串之间的拼接操作)

需要注意的是,StringJoinner类拥有StringBuilder类的部分相同功能的方法。(add,length,tostring)

在构造一个StringJoinner对象的时候,指定其分割符号,或者同时指定分割符号和开始结束符号。

package chapter6;

import java.util.StringJoiner;

public class StringBuilderTest {
public static void main(String[] args) {
StringJoiner test = new StringJoiner(“#”);
test.add(“Hello”).add(“World”);
System.out.println(test); // Hello#World // 指定分割符号

    StringJoiner test2 = new StringJoiner("#","[","]"); // 指定分割符号,开始,结束符号
    test2.add("Hello").add("World");
    System.out.println(test2); // [Hello#World]
}

}
9.5 BigDecimal类
如果直接用计算机进行浮点数运算的话,会存在数据浮动的现象。

double a = 0.1;
double b = 0.2;
double c = a + b; // c = 0.30000004
这是因为,在计算机中,无法精确地去表示一个浮点数所导致的。

同时,如果要使用计算机进行超级大数字的运算的时候,也会因为各种数值溢出而导致运算的结果出错。

总结而言,BigDecimal类就是用来解决数据浮动和溢出的问题。

BigDecimal类将数字看作是字符串进行处理。

9.5.1 构造方法
提供了两种不同的构造方法。

可以使用一个double类型的浮点数进行构造,也可以通过一个String类型的字符串进行构造。

BigDecimal test1 = new BigDecimal(123);
BigDecimal test2 = new BigDecimal(“123”);
但是,使用double类型进行初始化不会解决浮点数的浮动问题,因为其后面的浮动小数位也会被存在BigDecimal类中,因此,更推荐的是去使用String类型进行初始化。

实际上,BigDecimal类中,也提供了一个valueOf方法,其目的和包装类中的同名方法一致,都是讲传入的参数最后转换成一个BigDecima类对象后返回。

BigDecimal test1 = BigDecimal.valueOf(123.123);
9.5.2 运算方法
使用BigDecimal类的对象进行运算的时候,不可以直接用运算符号去进行计算。

需要使用BigDecimal中所提供的一些运算方法去进行计算。

add方法可以实现两个BigDecimal类中的值进行相加,最后的结果会作为一个新的BigDecimal对象进行返回。

package chapter6;

import java.math.BigDecimal;

public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal test1 = new BigDecimal(123);
BigDecimal test2 = new BigDecimal(456);

    BigDecimal ans = test1.add(test2);
    System.out.println(ans);
}

}
subtract方法可以实现两个BigDecimal类中的值进行相加,最后的结果会作为一个新的BigDecimal对象进行返回。

package chapter6;

import java.math.BigDecimal;

public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal test1 = new BigDecimal(123);
BigDecimal test2 = new BigDecimal(456);

    BigDecimal ans = test1.subtract(test2);
    System.out.println(ans);
}

}
multiply方法可以实现两个BigDecimal类中的值进行相加,最后的结果会作为一个新的BigDecimal对象进行返回。

package chapter6;

import java.math.BigDecimal;

public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal test1 = new BigDecimal(123);
BigDecimal test2 = new BigDecimal(456);

    BigDecimal ans = test1.multiply(test2);
    System.out.println(ans);
}

}
divide方法可以实现两个BigDecimal类中的值进行相加,结果会作为一个新的BigDecimal对象进行返回。

divide方法需要特别注意下,BigDecimal类的目的就是去获得精确的计算结果,如果 5 / 3 这种本来就没有精确结果的数据进行运算,divide方法就会报错,因此,需要在无法获得精确计算结果的时候,去指定保留的位数。

同时,BigDecimal类中还定义了一个枚举类,RoundingMode,可以在其中选择舍入的模式,同时作为参数传入divide方法中。

package chapter6;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal test1 = new BigDecimal(123);
BigDecimal test2 = new BigDecimal(456);

    BigDecimal ans = test1.divide(test2, 2, RoundingMode.CEILING);
    System.out.println(ans);
}

}
doubleValue方法,可以将BigDecimal类对象中的数值,转换为double类型后返回。

package chapter6;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal test1 = new BigDecimal(123);
System.out.println(test1.doubleValue());
}
}
9.6 Date类
Date表示系统中的时间和日期。

Date类实际上已经过时了,其中提供了很多的方法,但是能用的不多。

9.6.1 构造方法
这个部分只介绍没有过时的Date类的构造方法。

Date类提供了一个无参构造器,其以当前的时间创建一个Date类的对象。

Date test = new Date();
Date类还提供了一个有参的构造器,其传入一个long类型的参数,初始化Date类中的毫秒值。

Date test = new Date(12345);
9.6.2 操作时间的方法
getTime方法

getTime方法用来返回从1970年到现在的所有的ms值。(所返回的时间类型是long类型)

long time = test.getTime();
setTime方法

setTime方法传入一个long类型的参数,用来修改现在Date类中的毫秒值。

test.setTime(1234567);
toString方法

toString方法将Date类中毫秒值转换为对应的星期,月份,时间,时区,年份,按照顺序变成一个字符串返回。

System.out.println(test.toString());
import java.util.Date;

public class DateTest {
public static void main(String[] args) {
Date test = new Date();
System.out.println(test.getTime());

    System.out.println(test.toString());

    test.setTime(12345667);
    System.out.println(test.getTime());
}

}
9.7 SimpleDateFormat类
很明显可以看出,使用Date类去表示时间,非常不方便。

使用SimpleDateFormat类可以将一个Date类的对象表示为自己想要的时间格式。

9.7.1 构造方法
SimpleDateFormat类,在构造一个对象的时候,就需要去指定时间表示的格式。

其提供了两种不同的构造方法。

无参构造器,会将Date类型的对象,表示为默认的时间格式。

SimpleDateFormat test = new SimpleDateFormat();
带有参数的构造器。会传入一个字符串表示时间的表示格式。

这个字符串里面会含有一些特殊的字符,比如:

image-20231026165756504

除了这些字符以外,其他的字符可以随意表示。

一下是一个常见的SimpleDateFormat的构造方法:

SimpleDateFormat test = new SimpleDateFormat(
“yyyy-MM-dd” + “\n” +
“HH:mm:ss” + “\n” +
“EEE” + “\n” +
“a”
);
9.7.2 修改Date对象格式的方法
使用SimpleDateFormat类中提供的format方法,可以实现对时间格式的修改。

可以直接对一个Date类的时间进行修改,也可以对一个long类型的ms值进行修改。

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateTest {
public static void main(String[] args) {
Date time = new Date();
SimpleDateFormat test = new SimpleDateFormat(
“yyyy-MM-dd” + “\n” +
“HH:mm:ss” + “\n” +
“EEE” + “\n” +
“a”
);
System.out.println(test.format(time));
System.out.println(test.format(time.getTime()));
}
}
9.8 Calendar类
Calendar类是表示系统时间的日历的类。

其可以单独获取时间中的,时,分,秒,年份,月份,日期。

需要额外注意的是,Calendar类实际上是一个抽象类。(因此,这里不去介绍Calendar的构造方法)

抽象类本身是不可以对外构造一个对象的,但是其依旧提供了获取对象的方法。

getInstance方法可以获取到Calendar的一个子类GeogeriaCalender的对象作为一个通用的对象。

9.8.1 提供的日历方法
getInstance方法

这个方法会用当前的世家你去初始化一个通用的子类对象返回。

返回的子类对象中toString方法是重写过的方法,直接打印的话会显示所有的时间信息。(带着Field一起打印)

Calendar test = Calendar.getInstance();
get方法

用来获取Calendar类中某个的属性的信息。

test.get(Calendar.YEAR)
需要注意到的是,属性是作为常量被定义在Calendar类中的。

常见的Calendar类中所定义的属性有如下几种:

image-20231026171618345

getTime方法

可以将现在Calendar类中的时间去初始化一个Date对象然后返回。

Date time = test.getTime();
System.out.println(time);
getTimeInMills方法

可以将现在Calendar类中的时间转换为从1970年的时间点后到现在的所有毫秒值进行返回。

System.out.println(test.getTimeInMillis());
set方法

可以指定Calendar类中的某个属性进行修改。

test.set(Calendar.YEAR, 2022);
System.out.println(test.get(Calendar.YEAR));
add方法

可以指定Calendar类中的某个属性进行增加或者减小某个值。

test.add(Calendar.YEAR, -1);
System.out.println(test.get(Calendar.YEAR));
9.8.2 JDK8之前的时间类
在JDK8之前所提供的所有时间类基本上都过时了。

可以发现:

JDK8之前所提供的所有时间类基本上都有很多的过时方法,在编译器中就已经不推荐使用了。

所提供的时间类基本上都是可变类,其对象中的内容被修改后,就会丢失原有的时间信息。

是线程不安全的。

不能表示太精确的时间,最多只能到ms。

所以在JDK8之后,提供了很多其他的时间类。

其方法更加合理。

都是不可变对象。

线程安全。

可以更加精确地表示时间,最多可以到ns。

9.9 Arrays类
Arrays类是一个工具类,其中提供了很多用来操作数组的静态方法。

toString方法
toString方法,会返回数组的具体内容。

是不是去逐个调用数组内每个元素的toString方法?

是的。而且,toString方法只能处理数组,对于ArrayList这种范型数组也需要转换为普通的对象数组后才能操作。

import java.util.ArrayList;
import java.util.Arrays;

public class ArraysTest {
public static void main(String[] args) {
ArrayList dogList = new ArrayList<>();
for(int i = 0; i < 3; i ++) {
dogList.add(new dog(String.valueOf(i)));
}
System.out.println(Arrays.toString(dogList.toArray()));
}
}

class dog {
String Id;

public dog(String Id) {
    this.Id = Id;
}

public String toString() {
    return "Dog's Id is : " + Id;
}

}
copyOfRange方法
copyOfRange方法,会拷贝数组中的指定的范围。(返回的内容是Object类型的对象数组)

是深复制?是不是逐个去调用数组中的clone方法?

首先,是浅复制,在不重写hashcode方法的前提下,打印拷贝出来的部分数组元素中的hashcode,可以发现和原数组的一模一样。(这个时候的hashcode还是从元素的地址中导出)

而且,copyOfRange方法不会去调用clone方法,也就是,一直都只会是浅复制。

// copyOfRange
Object[] dogs_part = Arrays.copyOfRange(dogList.toArray(), 1, 3);
for(Object it : dogs_part) System.out.println(it.hashCode() + " ");
for(Object it : dogList.toArray()) System.out.println(it.hashCode() + " ");
copyOf方法
copyOf方法,将所有的数组内容,拷贝到一个新的数组中去。(返回的内容是Object类型的对象数组)

同时可以指定,拷贝过去到新数组的长度,多余的内容用null填充,少于的内容则不会被复制过去。(可以指定复制的元素个数)

// copyOf
Object[] dogs_all = Arrays.copyOf(dogList.toArray(), 2);
for(Object it : dogs_all) System.out.println(((dog)it).Id);
dogs_all = Arrays.copyOf(dogList.toArray(), 10);
for(Object it : dogs_all) System.out.println(((dog)it).Id);
setAll方法
setAll方法,给数组中的所有元素进行相同的操作。

这里有个比较坑的点是,需要自己去实现一个匿名内部类,在其中去实现apply方法。

apply方法中,参数value表示是对外面局部变量dogs_all的访问下标。

apply方法内部,有一个return语句,表示在这个value的下标,将原本的元素替换为现在所返回的这个元素。

一个坑爹的地方在于,如果在内部类内部去访问外面的局部变量,为了安全,必须这个局部变量定义为final。

// setAll
Object[] finalDogs_all = dogs_all;
Object[] finalDogs_all1 = dogs_all;
Arrays.setAll(dogs_all, new IntFunction() {

@Override
public dog apply(int value) {

    return new dog(((dog) finalDogs_all1[value]).Id + "OK");
};

});
for(Object it : dogs_all) System.out.println(((dog)it).Id);

sort方法
sort方法,给数组中所有的元素进行排序。

sort方法可以直接作用在基本类型构成的数组上。

但是,如果想要sort方法作用在自己定义好的类型对象上的时候,就必须告诉sort方法,比较的规则。

定义比较方法规则的方法有两种:

在定义类的时候,implements Comparable 接口。

在调用sort方法的时候,传入一个Comparator接口的实现类。

他妈的,Arrays.sort只能给对象数组使用,如果要使用ArrayList对象进行sort的话,需要提前使用ArrayList中的提供的toArray方法,将ArrayList转换为Object类型的数组。再传入sort方法进行排序,这个时候,如果实现一个Comparator对象,就必须使用Object泛型。

实现Comparable接口

class DogForSort extends dog implements Comparable{
int age;

public DogForSort(int age, String Id) {
    super(Id);
    this.age = age;
}

@Override
public int compareTo(DogForSort o) {
    if(this.age > o.age) return 1;
    else if(this.age == o.age) return 0;
    else return -1;
    // reurn this.age - o.age;
}

}
实现Comparator接口

Arrays.sort(dogForSorts, new Comparator() {
@Override
public int compare(DogForSort o1, DogForSort o2) {
if(o2.age > o1.age) return 1;
else if(o2.age == o1.age) return 0;
else return -1;
// return o2.age - o1.aeg;
}
});
for(Object it : dogForSorts) System.out.println(((dog)it).Id);
要注意的一点是,他们重写的方法不是同一个。

Comparable接口重写的是compareTo方法。

Comparator接口重写的是compare方法。

在自定义升序和降序的时候,有一个小技巧,就是,把想要左边那个放到不等号右边就是升序,反之就是降序。

另外,JDK对于所返回的值也有要求,如果是严格大于,应该返回正整数,如果是等于,返回0,如果是小于,返回负整数。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.function.IntFunction;

public class ArraysTest {
public static void main(String[] args) {
ArrayList dogList = new ArrayList<>();
for(int i = 0; i < 3; i ++) {
dogList.add(new dog(String.valueOf(i)));
}

    // toString()
    System.out.println(Arrays.toString(dogList.toArray()));

    // copyOfRange
    Object[] dogs_part = Arrays.copyOfRange(dogList.toArray(), 1, 3);
    for(Object it : dogs_part) System.out.println(it.hashCode() + " ");
    for(Object it : dogList.toArray()) System.out.println(it.hashCode() + " ");

    // copyOf
    Object[] dogs_all = Arrays.copyOf(dogList.toArray(), 2);
    for(Object it : dogs_all) System.out.println(((dog)it).Id);
	// dogs_all = Arrays.copyOf(dogList.toArray(), 10);
	// or(Object it : dogs_all) System.out.println(((dog)it).Id);

    // setAll
    Object[] finalDogs_all = dogs_all;
    Object[] finalDogs_all1 = dogs_all;
    Arrays.setAll(dogs_all, new IntFunction<dog>() {

        @Override
        public dog apply(int value) {

            return new dog(((dog) finalDogs_all1[value]).Id + "OK");
        };
    });
    for(Object it : dogs_all) System.out.println(((dog)it).Id);

    Arrays.setAll(dogs_all,(value)->{return new dog(((dog) finalDogs_all1[value]).Id + "OK");});
    for(Object it : dogs_all) System.out.println(((dog)it).Id);

    Arrays.setAll(dogs_all,(value)-> new dog(((dog) finalDogs_all1[value]).Id + "OK"));
    for(Object it : dogs_all) System.out.println(((dog)it).Id);

    // sort
    DogForSort[] dogForSorts = {new DogForSort(1,"1"),
                                new DogForSort(2,"2"),
                                new DogForSort(3, "3")};
    Arrays.sort(dogForSorts);
    for(Object it : dogForSorts) System.out.println(((dog)it).Id);

    Arrays.sort(dogForSorts, new Comparator<DogForSort>() {
        @Override
        public int compare(DogForSort o1, DogForSort o2) {
            if(o2.age > o1.age) return 1;
            else if(o2.age == o1.age)  return 0;
            else return -1;
        }
    });
    for(Object it : dogForSorts) System.out.println(((dog)it).Id);

    Arrays.sort(dogForSorts, (DogForSort o1, DogForSort o2)->{return o2.age - o1.age;});
    for(Object it : dogForSorts) System.out.println(((dog)it).Id);
}

}

class dog {
String Id;

public dog() {}

public dog(String Id) {
    this.Id = Id;
}

public String toString() {
    return "Dog's Id is : " + Id;
}

public Object clone() {
    dog temp = new dog("");
    temp.Id = this.Id;
    return temp;
}

}

class DogForSort extends dog implements Comparable{
int age;

public DogForSort(int age, String Id) {
    super(Id);
    this.age = age;
}

@Override
public int compareTo(DogForSort o) {
    if(this.age > o.age) return 1;
    else if(this.age == o.age) return 0;
    else return -1;
}

}
9.10 正则表达式
所谓的正则表达式就是一组由特定字符组成的,用来表示字符串特定的格式的匹配规则。

正则表达式一般有两个作用:

用来校验数据的格式。

用来检索想要搜索的内容。

实际上上述的功能使用自定义的字符串方法也可以实现,但是使用正则表达式可以极大程度简化代码的书写。

Java中使用正则表达式匹配的语法:(String类中提供了静态方法matches用来匹配正则表达式)

String.matches(“Regex”);
matches方法会返回true或者false表示字符串匹配成功与否。

其中,要注意的是,只有正则表达式可以吃掉所有的字符串中的字符才表示匹配成功。

9.10.1 正则表达式的特殊字符
集合:[]

集合中可以包含多个字符,但是集合始终只匹配一个字符。

String test = “a”;
System.out.println(test.matches(“[abc]”)); // true
test = “abc”;
System.out.println(test.matches(“[abc]”)); // false
集合中也可以使用-来表示字母(包括大写小写)和数字之间的一个范围。

test = “a”;
System.out.println(test.matches(“[a-c]”)); // true
test = “5”;
System.out.println(test.matches(“[1-6]”)); // true
特殊的转义字符:

需要特殊注意的是,正则里面的转义字符也是从\开始,字符串里面的特殊转义字符也是,想要在字符串里面表示转义字符是个正则表达式用的,就要使用两个\来取消掉在字符串里面的转义效果。

\d:表示匹配0-9的数字

test = “5”;
System.out.println(test.matches(“\d”)); // true
\D:表示不能匹配0-9的数字

test = “a”;
System.out.println(test.matches(“\D”)); // true
\s:表示匹配一个空格

test = " “;
System.out.println(test.matches(”\s")); // true
\S:表示不匹配一个空格

test = “a”;
System.out.println(test.matches(“\S”)); // true
\w:表示匹配一个单词的组成成分,数字,字母,下划线

test = “a”;
System.out.println(test.matches(“\w”)); // true
\W:表示不匹配一个单词的组成部分

test = “%”;
System.out.println(test.matches(“\W”)); // true
数量词

数量词用来表示紧挨在其后的字符出现的次数。

?:表示 1个 或者 0个

test = “a”;
System.out.println(test.matches(“a?”)); // true
test = “”;
System.out.println(test.matches(“a?”)); // true
*:表示 0个 或者 多个

test = “aaaa”;
System.out.println(test.matches(“a*”)); // true
test = “”;
System.out.println(test.matches(“a*”)); // true
+:表示 1个或者 多个

test = “aaaa”;
System.out.println(test.matches(“a+”)); // true
test = “”;
System.out.println(test.matches(“a+”)); // false
{n}:表示必须出现n次

test = “aaaa”;
System.out.println(test.matches(“a{4}”)); // true
{n, }:表示至少出现n次

test = “aaaaaaaa”;
System.out.println(test.matches(“a{4,}”)); // true
{n, m}:表示出现n次到m次

test = “aaa”;
System.out.println(test.matches(“a{2,5}”)); // true
(?i):表示其后面的所有内容都要忽略大小写

test = “A”;
System.out.println(test.matches(“(?i)a”)); // true
|:表示或

test = “A”;
System.out.println(test.matches(“A|a”)); // true
():表示分组

test = “Abc”;
System.out.println(test.matches(“A((?i)BC)”)); // true
^:表示开头

需要注意的是,如果只满足开头,但是长度不满足要求,依旧会返回false。

test = “AbcD”;
System.out.println(test.matches(“^Ab(\w\w)”)); // true
现在有一个例子,要求匹配的字符串是,全部都是数字,长度为6到20,不能以0开头。

String number = “1234”;
System.out.println(number.matches(“[1-9]\d{5,19}”)); // false
10 异常
异常就是在程序的运行过程中,所出现的问题。

Java会将出现的问题封装为一个异常对象,向主调函数抛出,最终会给到JVM进行处理(不自己处理的话)。

JVM会停下现在正在运行的代码,打印这个异常对象。

Integer.valueOf(“abc”); // 将非数值字符串转换为整型字符串会抛出异常
有一些非常常见的异常:

数组访问越界异常。

整数除0异常。

读写不存在的文件异常。

读写网络数据的过程中,断网也会引发异常。

10.1 异常的体系结构
image-20240126113837378

Error:

表示系统级别的严重错误。Error类是给Java开发的公司所使用的。和我们无关。并不是异常。 

Exception:

表示异常,是程序可能出现的问题。我们一般会利用Exception和它的子类来封装程序所出现的问题。

Exception类下有两个大类:

RuntimeException(及其下的子类):表示运行时出现异常,在编译阶段不会报错。 

编译时异常:在编译阶段就会出现的异常。

异常分为:编译时异常和运行时异常。

编译时异常发生在,将Java文件采用javac命令编译为字节码文件的过程中。

运行时异常则发生在,用Java命令去运行一个java程序的过程中。

为什么要这样去设计两个不同的异常类别呢?

为什么不都设计为编译时异常?

因为在编译的过程中,实际上并没有去运行代码,只是对编写的代码进行语法检查和性能优化。比如索引越界异常,其在代码的语法上是没有问题的,只有实际运行的时候,去内存中取值的时候才有出现问题。(编译的时候检查不出来问题)

为什么不都设计为运行时异常?

因为编译异常更主要的目的在于去给程序员提醒去检查本地的信息是否正确。

比如,写错了一些语法,就要程序员自己去检查程序的编写是否正确。

10.2 异常的作用
异常一般的作用可以分为以下的两个类别:

用来表示有关Bug的相关信息。

用作方法的一个特殊的返回值,来告知调用处底层的执行情况。

举个例子。

现在在Student类中有这个一个函数。

public void setAge(int age) {
if(age > 0 && age < 100)
this.age = age;
else {
System.out.println(“年龄不符合要求”);
}
}
当age不符合要求的时候,会在控制台输出错误的信息。

也就是Student对象的错误信息直接给到了控制台,而没有给到调用处。

在调用setAge方法的地方,是不知道setAge方法是否正确执行了。

因此,可以使用异常来告知调用处方法的执行情况。

可以使用 throws 关键字 来表示抛出一个异常。

抛出异常的一般语法如下:

// throw 用在具体的抛出异常的地方
throw new Exception();
// throws 用在类定义的时候,表示这个类可能抛出的异常
public void setAge(int age) throws Exception
public void setAge(int age) throws Exception {
if(age > 0 && age < 100)
this.age = age;
else {
System.out.println(“年龄不符合要求”);
throw new Exception();
}
}
10.3 异常的处理
异常的处理一般有以下的三种方法:

JVM自己处理。

自定义处理的方法。

抛出异常,让调用处进行处理。

10.3.1 JVM自己处理
JVM处理异常的方式就是不处理。

程序一旦出现需要JVM处理的异常,程序就会停止运行,将异常交给JVM处理后,程序就会结束。

将所有的错误信息输出在控制台即可。

public class ExceptionTest {
public static void main(String[] args) {
System.out.println(1 / 0);
}
}
“C:\Program Files\Java\jdk-21\bin\java.exe” “-javaagent:C:\Java\IntelliJ IDEA 2023.3.2\lib\idea_rt.jar=49255:C:\Java\IntelliJ IDEA 2023.3.2\bin” -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\liudaoyu\IdeaProjects\JavaLearn\out\production\JavaLearn ExceptionTest
Exception in thread “main” java.lang.ArithmeticException: / by zero
at ExceptionTest.main(ExceptionTest.java:3)

Process finished with exit code 1
10.3.2 自定义处理的方法
如果想要自己去处理抛出的异常,就需要先去捕获抛出的异常。

使用这种方法处理异常最大的好处就在于,程序不用停止运行,在try-catch捕获处理异常过后,剩下的代码依然可以继续运行。

捕获-处理异常的一般语法如下:

try {
// 可能抛出遗产的代码
} catch(ExceptionClassName ExceptionName) {
// 处理异常的代码
}
在try块中的代码,相当于去new了一个异常对象,然后利用throw关键字进行抛出。

一旦有异常被抛出,catch块就会检查抛出的异常是否可以被自己的参数列表所接受,如果可以接受,则进入catch块中去执行异常的处理代码,在catch块中的处理代码执行完后,会去继续执行catch块后的代码。

try-catch 的灵魂四问
try块中如果没有抛出异常会怎么样?

其不会去运行catch部分的代码,会继续去运行catch后的代码。

public class ExceptionTest {
public static void main(String[] args) {
try{
System.out.println(1);
System.out.println(“A”);
} catch (ArithmeticException e) {
System.out.println(“异常”);
}
System.out.println(“B”);
}
}
try中有多个异常需要处理怎么办?

首先需要明确的是,一个catch块只能去处理一个类型的异常。

但是可以同时有多个catch块去处理不同类型的异常。

最正确的做法就是,try块中有多少个种类的异常,就要去catch多少个种类的异常。

public class ExceptionTest {
public static void main(String[] args) {
try{
System.out.println(1 / 0); // 除0异常
int[] arr = new int[12];
System.out.println(arr[13]); // 数组索引越界异常
System.out.println(“A”);
} catch (ArithmeticException e) {
System.out.println(“除0异常”);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(“数组索引越界异常”);
}
System.out.println(“B”);
}
}
try中有没有捕获的异常怎么办?

如果try中年抛出了一个无法被catch的异常。

就相当于try-catch块白写了,这个无法被捕获的异常就只能通过JVM虚拟机进行处理。

也就是,程序会停止运行的同时,在控制台对异常信息通过红色的文字进行输出。

public class ExceptionTest {
public static void main(String[] args) {
try{
int[] arr = new int[]{1,2,3,4,5};
System.out.println(arr[13]); // 数组索引越界异常
System.out.println(“A”);
// 没有catch到数组索引越界异常
} catch (ArithmeticException e) {
System.out.println(“除0异常”);
}
System.out.println(“B”);
}
}
“C:\Program Files\Java\jdk-21\bin\java.exe” “-javaagent:C:\Java\IntelliJ IDEA 2023.3.2\lib\idea_rt.jar=61256:C:\Java\IntelliJ IDEA 2023.3.2\bin” -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\liudaoyu\IdeaProjects\JavaLearn\out\production\JavaLearn ExceptionTest
Exception in thread “main” java.lang.ArrayIndexOutOfBoundsException: Index 13 out of bounds for length 5
at ExceptionTest.main(ExceptionTest.java:5)

Process finished with exit code 1
try中抛出了异常,try中剩下的代码怎么办?

如果在try块中抛出了异常,那么try块内剩余的代码就没有必要继续运行了。

也就是,try块后的代码不再运行,直接跳转到对应的catch块中进行异常的处理,在异常的处理结束之后继续执行try-catch块后面的代码。

public class ExceptionTest {
public static void main(String[] args) {
try{
int[] arr = new int[]{1,2,3,4,5};
System.out.println(arr[13]); // 数组索引越界异常
// 前面有代码抛出了异常,不会运行下面的代码
System.out.println(“Hello World”);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(“除0异常”);
}
System.out.println(“B”);
}
}
拓展:为什么JVM处理异常在控制台打印的信息会是红色的?

实际上,有默认的JVM进行处理,就相当于,new 了一个异常的对象,然后去调用这个异常对象的printStackTrace方法。

public void printStackTrace() {
printStackTrace(System.err);
}
可以看到,调用了System.err方法,err类和out类都是用来在控制台进行输出的方法。

err类是用来在控制台打印错误信息的,默认就是红色的字体。

另外需要注意的是,如果和err一起还有其他的正常打印的信息,其打印的顺序可能不一致。

这个地方涉及到一些有关多线程的问题。

public class ExceptionTest {
public static void main(String[] args) {
System.err.println(“错误信息”);
}
}
image-20240129212918163

10.3.3 抛出异常处理
抛出异常处理涉及到两个关键字:

throw:用在方法的内部,用来结束现在执行的方法,向调用处返回一个异常。让调用处来处理这个异常,同时方法中剩下的代码不再进行处理。

throws:用在方法的定义处,表示这个方法可能抛出的异常。

如果在方法内部,throw了异常,就需要在方法的定义的地方去throws异常,表示自己可能抛出的异常。

运行时异常(RuntimeException)可以省略不写,但是编译时异常(Exception)必须写。

抛出异常的一般语法如下:

class ClassName throws Exception1, Exception2 … {
thorws Exception1;
thorws Exception1;
}
举个例子:(求数组中的最大值)

public class ExceptionTest {
public static void main(String[] args) {
// 求数组中的最大值
int[] arr = new int[]{1,2,3,4,5};

}

public static int getMax(int[] arr) {
    int len = arr.length;
    int ans = arr[0];
    for (int i = 0; i < arr.length; i++) {
        if(arr[i] > ans) {
            ans = arr[i];
        }
    }
    return ans;
}

}
这份代码其实隐藏了2个隐患。

第一个:传入的arr数组可能是一个没有被new过的空指针,会引发空指针异常。

第二个:传入的arr数组可能是一个刚刚被new过,没有分配任何数据的长度为0的数组。

那么就需要利用try-catch块去处理这些异常。

public static int getMax(int[] arr) {
// 这两个异常都是运行时异常,因此可以不在方法声明的地方写出来
// 空指针异常
if(arr == null) {
throw new NullPointerException();
}
int len = arr.length;
// 数组指针越界异常
if(len == 0) {
throw new IndexOutOfBoundsException();
}
int ans = arr[0];
for (int i = 0; i < arr.length; i++) {
if(arr[i] > ans) {
ans = arr[i];
}
}
return ans;
}
什么时候去抛出异常,什么时候去捕获异常?
抛出异常的目的在于:告知调用处,底层代码的执行出了问题,需要调用处进行处理。因此,抛出异常都是在方法内部进行书写。

捕获异常的目的在于:不让程序停止,在方法的调用处进行处理。

11.4 自定义异常
自定义异常出现的目的在于,用户想要抛出一个适合自己的异常,但是这个异常没有被Java系统预先实现。

所有就需要用户去自定义一个异常,再来进行抛出。

11.4.1 Lead in
现在有如下的一个例子:

image-20240129215117842

首先需要去定义一个GirlFriend类。

setAge方法中,需要去对输入的age的格式和范围进行判断。

public void setAge(String age) throws NumberFormatException {
// setAge有要求
// 要求年龄 在 18 - 40之间
// 同时要求年龄是数字组成的

// 这个地方可能会抛出异常,就是NumberFormat格式不对异常
int Age = Integer.parseInt(age);
// 如果Age的格式是正确的,那么就去检查Age的范围是否正确
if(Age < 18 || Age > 40) {
    // 实际上这个地方就可以发现,并没有合适的异常类去表示年龄不在范围内,暂时先采用RuntimeException进行处理
    throw new RuntimeException();
}
this.age = Age;

}
setName方法中,需要去对输入的name的长度进行判断。

public void setName(String name) throws RuntimeException{
// 如果年龄的长度过短或者过长,就会抛出异常.
if(name.length() < 3 || name.length() > 10) {
throw new RuntimeException();
}
this.name = name;
}
上面的代码中,将名字和年龄的输入格式异常都定义为了RuntimeException类型的异常。

在这里就可以看出的是,对于一些用户自己去抛出的异常,有的时候会难以去找到一个合适的Java系统自己提供的异常类型,这个时候就需要自己去定义一些异常的类型。

11.4.2 自定义异常的方法
自定义异常出现的原因在于没有合适的异常对象来抛出。

同时如果抛出RuntimeException父类的异常对象,其范围太广,用一个相同的catch块来处理适用性太弱了。

自定义一个异常的步骤如下:

自定义一个异常类,这个类的名字必须见名知意。(自定义异常最重要的就是类的名字)

写明继承关系。(如果是运行时异常,就继承自RuntimeException类,如果是编译异常,就继承自Exception类)(方法内部有运行时异常可以不写throws,但是编译时异常就必须写throws)。

写明无参构造方法。

写明有参数构造方法。

自定义一个异常的最终目的就在于可以让异常更加清楚明白,因此,一个自定义异常类最重要的就是类的名字。

以下是一个自定义异常类的例子。

需要注意的是,自定义异常类的写法都是死的,基本没什么变化。

最重要的就是异常类的名字,要求在控制台查看的时候,可以一眼看出是哪里出的问题。

class NameFormatException extends RuntimeException {
public NameFormatException() {
}
// 调用父类RuntimeException的有参构造方法
// 其中传入的message就是额外田间的错误信息
public NameFormatException(String message) {
super(message);
}
}
因此,setName方法中,就可以抛出这个自定义异常类。

public void setName(String name) throws RuntimeException{
// 如果年龄的长度过短或者过长,就会抛出异常.
if(name.length() < 3 || name.length() > 10) {
throw new NameFormatException();
}
this.name = name;
}
11.4.3 异常类中常用的方法
在前文中,我们一般在catch方法中进行的异常处理就是去输出一个描述错误相关信息的语句。

实际上,在JVM中,默认的是去调用异常类中的printStackTrace方法。

getMessage方法
这个方法的目的在于去返回有关错误的详细信息。

getMessage方法会告诉你具体是哪里错了。

public class ExceptionMethod {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
try {
System.out.println(arr[6]);
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage());
}
}
}
getMessage方法返回的信息为:

“C:\Program Files\Java\jdk-21\bin\java.exe” “-javaagent:C:\Java\IntelliJ IDEA 2023.3.2\lib\idea_rt.jar=51584:C:\Java\IntelliJ IDEA 2023.3.2\bin” -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\liudaoyu\IdeaProjects\JavaLearn\out\production\JavaLearn ExceptionMethod
Index 6 out of bounds for length 5

Process finished with exit code 0
toString方法
toString方法则是返回一个比较简短的错误信息。

System.out.println(e.toString());
toString返回的内容为抛出异常的类的名字 + 具体的错误信息,但是不会和getMessage内容那么详细。

“C:\Program Files\Java\jdk-21\bin\java.exe” “-javaagent:C:\Java\IntelliJ IDEA 2023.3.2\lib\idea_rt.jar=49239:C:\Java\IntelliJ IDEA 2023.3.2\bin” -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\liudaoyu\IdeaProjects\JavaLearn\out\production\JavaLearn ExceptionMethod
java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 5

Process finished with exit code 0
printStackTrace方法
printStackTrace方法则是返回所有和异常有关的详细信息。

注意,这个方法是直接在控制台通过System.err进行打印,因此输出的字体会是红色的。

“C:\Program Files\Java\jdk-21\bin\java.exe” “-javaagent:C:\Java\IntelliJ IDEA 2023.3.2\lib\idea_rt.jar=50094:C:\Java\IntelliJ IDEA 2023.3.2\bin” -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath C:\Users\liudaoyu\IdeaProjects\JavaLearn\out\production\JavaLearn ExceptionMethod
java.lang.ArrayIndexOutOfBoundsException: Index 6 out of bounds for length 5
at ExceptionMethod.main(ExceptionMethod.java:5)

Process finished with exit code 0
11 集合
Java中,将容器统称为集合。

集合分为了两大主要的类别,Collection类(单列集合),和Map类(双列集合)。

Collection单列集合的含义是,集合中的元素只有一列,也就是每一项元素都只有一个值。

Map双列集合的含义是,集合中的元素有两列,是以键值对的形式保存的。

Java实际上提供了很多的不同集合,来满足不同的数据保存,以及操作性能的要求。(增,删,改,查的性能)

11.1 Collection集合接口
11.1.1 Collection集合的结构
前面学过的ArrayList就是Collection集合中的一种。

Java中,首先定义了Collection的一个泛型接口,并在其中定义了很多Collection集合的公用功能。

Collection接口下,又定义了常用的主要的两个子接口,分别是List和Set。他们中又各自定义自己的特殊性质。

List集合,其中的元素是添加有序(添加进去的元素的顺序就是保存的顺序)的,是可以重复的,是带有索引(可以通过下标访问)的。

其中,ArrayList类,LinkedList类,就是代表性的实现类。

Set集合,其中的元素是无序(添加进去后按照一定的顺序重新排列)的,不可以重复的,没有索引的。

其中,HashSet类,LinkedHashSet类,TreeSet类,就是代表性的实现类。

Collection
List
Map
ArrayList
LinkedList
HashSet
LinkedHashSet
TreeSet
package chapter6;

import java.util.ArrayList;
import java.util.HashSet;

public class CollectionTest {
public static void main(String[] args) {
// 这里可以用Collection变量来接受ArrayList类型的对象,是一种很经典的多态写法
// 可以灵活地去切换对同一块内存区域的操作方法
ArrayList arrayListTest = new ArrayList<>();
arrayListTest.add(1);
arrayListTest.add(2);
arrayListTest.add(3);
for (Integer integer : arrayListTest) {
System.out.println(integer);
}

    HashSet<Integer> hashSetTest = new HashSet<>();
    hashSetTest.add(1);
    hashSetTest.add(2);
    hashSetTest.add(2);
    hashSetTest.add(3);
    for (Integer integer : hashSetTest) {
        System.out.println(integer);
    }
}

}
11.1.2 Collection类中的常用方法
在Collection类中,提供了很多常用的方法。

这些方法,在Collection类中的所有的实现类中,都可以调用。

add方法
add方法被用来向Collection的对象中添加元素。

ArrayList arrayListTest = new ArrayList<>();
arrayListTest.add(1);
arrayListTest.add(2);
arrayListTest.add(3);
clear方法
clear方法用来清空整个集合中的所有元素。

arrayListTest.clear();
System.out.println(arrayListTest.size()); // 0
contains方法
contains方法用来判断某个对象是否处于类中。(地址比较?内容比较?)

是按照类对象的地址进行比较的。(实际上,是调用对象自己的equals方法进行比较)

ArrayList arrayListTest2 = new ArrayList<>();
student2 jack1 = new student2(“jack”, “male”, 123);
student2 jack2 = new student2(“jack”, “male”, 123);

arrayListTest2.add(jack1);
System.out.println(arrayListTest2.contains(jack1)); // true
System.out.println(arrayListTest2.contains(jack2)); // false
isEmpty方法
isEmpty方法用来判断集合中是否为空。

System.out.println(arrayListTest2.isEmpty());
arrayListTest2.add(jack1);
System.out.println(arrayListTest2.isEmpty());
remove方法
remove方法用来删除集合中的某个对象。

和C++中的erase方法不同的是,remove方法只会删除所匹配到的第一个元素,erase会全部删掉。

这个地方应该也是用equals方法去判断是否是这个对象应该被删除。

arrayListTest2.add(jack1);
System.out.println(arrayListTest2.contains(jack1));
arrayListTest2.remove(jack1);
System.out.println(arrayListTest2.contains(jack1));
size方法
size方法用来获取集合中的元素的数目。

arrayListTest2.add(jack1);
arrayListTest2.add(jack1);
System.out.println(arrayListTest2.size()); // 2
toArray方法
toArray方法可以将一个集合,作为Object[]数组进行返回。

为什么是Object数组是因为,集合中的泛型实际上只存在于编译之前,编译之后会擦除。

是有其他方法可以在泛型集合中插入其他类型的元素的,因此,用Object数组进行返回更为安全。

arrayListTest2.add(jack1);
arrayListTest2.add(jack1);
Object[] array = arrayListTest2.toArray();
for (Object o : array) {
System.out.println(((student2)o).name);
}
但是,如果可以百分百保证,集合中只存在一个类型的数据的话,也可以指定返回的类型。

可以申请好一个指定类型的数组空间,作为参数传入toArray方法中,就会自动进行强制类型转换。

arrayListTest2.add(jack1);
arrayListTest2.add(jack1);
Object[] array = arrayListTest2.toArray();
student2[] array2 = arrayListTest2.toArray(new student2[arrayListTest2.size()]);
for (student2 student2_it : array2) {
System.out.println(student2_it.name);
}
addAll方法
addAll方法可以用来进行数据的导入。

可以将一个集合中的数据,附加到另外一个集合的后面。(和append类型)

但是,要求是,两个集合的数据类型必须一致。

ArrayList arrayListTest2 = new ArrayList<>();
student2 jack1 = new student2(“jack”, “male”, 123);
student2 jack2 = new student2(“jack”, “male”, 123);
arrayListTest2.add(jack1);
arrayListTest2.add(jack2);

ArrayList arrayListTest3 = new ArrayList<>();
student2 Mick1 = new student2(“Mick1”, “male”, 123);
student2 Mick2 = new student2(“Mick2”, “male”, 123);
arrayListTest3.add(Mick1);
arrayListTest3.add(Mick2);

arrayListTest2.addAll(arrayListTest3);
for (student2 student2_it : arrayListTest2) {
System.out.println(student2_it.name);
}
11.1.3 Collection集合的遍历方法
迭代器
对于集合而言,使用迭代器进行遍历是最推荐的形式。

Collection集合中,提供了Iterator()泛型方法,其会返回一个指向集合第一个元素的iterator对象。

ArrayList arrayListTest3 = new ArrayList<>();

iterator对象中,提供了两个方法。

hasNext:用来询问当前位置有没有元素,同时,抛出boolean值。

next:用来获得当前位置的元素,同时,迭代器向后移动。(返回当前位置的元素)

使用迭代器进行遍历的一般结构:

Iterator iterator = CollectionTest.Iterator();
while(CollectionTest.hasNext()) {
// …
CollectionTest.next();
}
ArrayList arrayListTest3 = new ArrayList<>();
student2 Mick1 = new student2(“Mick1”, “male”, 123);
student2 Mick2 = new student2(“Mick2”, “male”, 123);
arrayListTest3.add(Mick1);
arrayListTest3.add(Mick2);

Iterator iterator = arrayListTest2.iterator();
while(iterator.hasNext()) {
// 需要注意的是, 并不推荐直接用next方法来访问当前位置的元素
// 最好的方法是,使用一个另外的变量来单独存放next方法抛出的元素,再进行访问
System.out.println(iterator.next().name);
}
增强for
对于Java中的数组和集合而言,可以使用for-each循环来进行遍历其中的元素。

使用for-each循环的一般结构为:

for(元素类型 变量 : 集合/数组) {
// …
}
ArrayList arrayListTest2 = new ArrayList<>();
student2 jack1 = new student2(“jack”, “male”, 123);
student2 jack2 = new student2(“jack”, “male”, 123);
arrayListTest2.add(jack1);
arrayListTest2.add(jack2);
for (student2 student2_it : arrayListTest2) {
System.out.println(student2_it.name);
}
Lambda表达式
使用Lambda表达式,会比其他的的遍历方法更加直接,简单。

对于,Collection集合,Lambda表达式并不会带来很大的简化,但是对于后面的Map集合,就会简单很多。

对于Collection集合,有forEach方法可以对集合中的元素遍历。

在iterator的接口中,定义了forEach方法。

default void forEach(Consumer Action);
对于forEach的参数,action是一个Consumer类,可以采用匿名内部类的方法来传参。

Consumer是一个接口,是一个函数式接口,接口中,只有一个函数,可以采用lambda表达式进行简化。

package chapter6;

import java.util.ArrayList;
import java.util.Collection;
import java.util.function.Consumer;

public class ForEachTest {
public static void main(String[] args) {
// 经典的多态写法
Collection collection = new ArrayList<>();
collection.add(1);
collection.add(2);
collection.add(3);

    // 使用forEach循环对collection进行遍历
    // 其参数consumer对象采用匿名内部类的方法进行传入
    collection.forEach(new Consumer<Integer>() {
        @Override
        // accept方法中,其参数就是collection中每一个元素的遍历
        // 只需要,在accept方法中,写出需要对元素进行的操作即可
        public void accept(Integer integer) {
            System.out.println(integer);
        }
    });
}

}
可以看看在方法accept的内部没事怎么实现对collection中的元素的遍历。

default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
// 可以看到的是,forEach方法中,其用for-each循环去遍历this
// 讲this传给了accept方法中。因此,遍历不是accept做得,遍历是forEach做的,访问是accept做的。
for (T t : this) {
action.accept(t);
}
}
现在采用lambda表达式,去简化表达。

collection.forEach(integer -> {System.out.println(integer);});
因为是在方法体中,只调用println这一个静态方法,所以还可以使用方法引用来简化。

collection.forEach(System.out::println);
11.1.4 集合存储对象的原理
用一个使用Collection集合的例子进行说明。

最需要注意的一点是,集合中,存储的是对象的地址,而不是对象本身。

package chapter6;

import java.util.ArrayList;
import java.util.Collection;

public class CollectionStore {
// 首先,程序从main方法作为程序的入口
// main方法进入本地的栈中
public static void main(String[] args) {
// 在堆上,先创建Movies容器(new),将内存中的地址返回给Movies
Collection Movies = new ArrayList<>();

    // 调用add方法,先在对上创建一个匿名的movie对象,将该对象的地址返回给add方法
    // 集合将这个地址放到第一个元素的位置(集合中存储的实际上是地址)
    Movies.add(new Movie("1", 100));
    Movies.add(new Movie("2", 100));

    // for-each循环获取集合中的元素
    // 依次将Movies中的元素(地址),给到movie 
    for (Movie movie : Movies) {
        System.out.println(movie.name);
    }

}

}

class Movie {
String name;
double score;

public Movie(String name, double score) {
    this.name = name;
    this.score = score;
}

}
11.2 List集合接口
List接口中,继承了所有来自于Collection接口中的方法。(全部都可以进行调用)

List接口下的实现类的共有特点是:

有序(数据的存取顺序相同)

可以重复

有索引(可以用下标对元素进行访问)(get方法)

11.2.1 List集合的结构
List集合是一个泛型的祖宗接口,在其中定义了整个List集合中的特点。

List接口下,有两个独立的实现类,分别是,ArrayList,和LinkedList。

这两个实现类的特点没有区别, 都是支持有序存取,可以重复,支持索引。

那为什么会有两个不同的实现类?

因为,ArrayList和LinkedList的底层实现不同,其适用于不同的应用场景。

ArrayList集合的底层实现是数组,其查询效率不错,但是增删改的效率低下。

LinkedList的底层实现是双向链表,其查询效率和修改效率不行,但是增加和删除的效率较高。

List
ArrayList
LinkedList
11.2.2 List集合增加的方法
因为List集合可以支持按照索引进行查询元素,所以List集合根据这个特点新增了几个和索引有关的方法。

需要注意的一点是,List只是一个接口,但是可以采用多态的写法去指向一个实现类对象。如果想要去调用List接口中定义的方法,那么,就只能用List去接收,不能再用Collection了。

import java.util.ArrayList;
import java.util.List;

public class ListTest {
public static void main(String[] args) {
List test = new ArrayList<>();
test.add(1);
test.add(2);
test.add(3); // 1 2 3
}
}
add方法(特定下标添加)
// 1. add
test.add(3, 4);
System.out.println(test); // 1 2 3 4
remove方法(特定下标删除)
// 2. remove
test.remove(3);
System.out.println(test); // 1 2 3
set方法(特定下标修改)
// 3. set
test.set(1,4);
System.out.println(test); // 1 4 3
get方法(返回特定下标元素)
Java中没有重载运算符号的能力,所以,即使ArrayList支持底层的数组,但是依旧不可以用[]进行访问。

// 4. get
Integer ans = test.get(2);
System.out.println(ans);
11.2.3 List类型的遍历方法
因为,List接口是支持索引的,所以List实现类会比Collection类的遍历方法要多一种。(结合Size和索引)

package chapter6;

import java.util.ArrayList;
import java.util.List;

public class ListTest {
public static void main(String[] args) {
List list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
}
}
普通for
使用get方法,可以根据下标,获取到List中的元素。

//1. for
for(int i = 0; i < list.size(); i ++) {
System.out.println(list.get(i));
}
迭代器
//2. iterator
Iterator it = list.iterator();
while(it.hasNext()) {
System.out.println(it.next());
}
增强for
//3. for-each
for (Integer integer : list) {
System.out.println(integer);
}
lambda
//4. lambda
list.forEach(new Consumer() {
@Override
public void accept(Integer integer) {
System.out.println(integer);
}
});
list.forEach(integer -> {System.out.println(integer);});
list.forEach(System.out::println);
11.2.4 ArrayList实现类
对于一个实现类而言,需要去了解它的底层原理,功能,和适用的场景。

  1. ArrayList底层实现
    ArrayList的底层是基于数组进行实现的。

数组,就是数组,是内存中的一块连续的内存区域,每一块区域的大小相同。

ArrayList采用数组进行实现,就决定了其会有数组的特点。

数组按照索引进行查询的效率很高。(不管多大的数据量,索引查询的速度永远都是O(1))

数组的增删效率非常低下。(需要去移动元素,数据量较大的时候,效率更差)

ArrayList查询和修改的效率较高,增加,删除的效率则较低。

  1. ArrayList的工作流程
    首先,当ArrayList对象,刚刚被创建的时候,会先创建一个大小为0的数组,同时有一个size属性,保存现在的大小(同时也是放入元素的下标)。在第一个对这个对象进行add操作的时候,会创建一个另外的大小为10的数组,返回给ArrayList对象,同时将元素放入数组中,Size属性后移。

如果,现在的数组被存满了,那么就会对数组进行扩容操作,是按照1.5倍进行扩容,也就是创建一个大小为15的数组,拷贝之前的元素,再放入新元素。实际上,add操作,可以一次性加入很多元素,就有可能,第一次add就加满了,那么就会按照实际的需求去申请内存再返回。

  1. ArrayList集合的适用场景
    ArrayList具有数组的特点:查询快和修改快,但是增加和删除慢。

因此,ArrayList更适合:

大量使用索引进行查询数据。

数据量较小的情况下,也可以用ArrayList来进行增删。

不适用于,大量进行增加删除的情况。

11.2.5 LinkedList实现类

  1. LinkedList 底层实现
    LinkedList的底层是用双链表进行实现的。

链条的特点在于:

其在内存中的存储不是连续的,是分散存储的,其节点内部会存储其下一个节点的地址。

链表的查询速度很慢,必须查询整个链表才能找到(无论是按照索引还是值进行查询)。

链表的增删改查的性能很好。

链表不会存在,数组容量不够需要扩容的情况。

  1. LinkedList的双向链表
    双向链表的特点在于,其会记住前后节点的地址,

同时会提供一个头尾指针。因此,双向链表可以很容易对头尾节点进行操作。

LinkedList基于双链表进行实现,其继承了链表的特点的同时,还增加了很多和头尾有关的方法。

LinkedList list = new LinkedList<>();
addFirst方法

在集合头部加入一个元素。

// addFirst
list.addFirst(1);
list.addFirst(2);
list.addFirst(3); // 3 2 1
System.out.println(list);
addLast方法

// addLast
list.addLast(1);
list.addLast(2);
list.addLast(3); // 3 2 1 1 2 3
getLast方法

// getLast
System.out.println(list.getLast());
removeFirst方法

// removeFirst
list.removeFirst();
System.out.println(list); // 2 1 1 2 3
removeLast方法

// removeLast
list.removeLast();
System.out.println(list); // 2 1 1 2
3. LinkedList的特点
查询性能会比普通的单向链表好一丢丢。(还是弱于数组)(判断索引选择头开始还是尾开始)

增删的速度还是很快。

对于头尾的元素,增加和删除的的速度非常快,这是LinkedList的一大优势。(用来实现栈和队列)

  1. LinkedList的应用场景
    可以用来设计队列
    队列的特点是,先进先出。(从队尾入队,从队头出队)

队列的入队和出队操作都是对容器的头尾元素进行操作,因此使用LinkedList进行操作非常合适。

// 为了使用LinkedList提供的方法,必须用LinkedList进行指向,不用List
LinkedList queue = new LinkedList<>();
// 入队操作
queue.addLast(1);
// 出队操作
queue.getFirst();
queue.removeFirst();
可以用来设计栈
栈的特点是,先进后出。(栈顶入栈,栈底出栈)

LinkedList stack = new LinkedList<>();
// 压栈操作
stack.addFirst(1);
// 出栈操作
queue.getFirst();
queue.removeFirst();
为了方便对栈的实现,Java还对栈的压栈和出栈操作提供了专用的api

stack.push(2);
stack.pop();
11.3 Set集合接口
11.3.1 Set集合的结构
Set集合是一个泛型接口,其定义了Set集合中大部分的特性。

Set集合中,元素的添加顺序不是其存储的顺序(无序),元素是不能重复的(不重复),且集合中的元素不能通过下标进行访问(无索引)。

Set接口下面,定义了三个实现类,分别是,HashSet,LinkedHashSet,TreeSet。(其各自有自己的特点)

Set
HashSet
LinkedHashSet
TreeSet
为什么需要不同的Set实现类呢?

HashSet是最基本的Set集合,其特点和Set完全相同。

LinkedHashSet则有所不同,其元素的顺序是有序的,元素是不重复的,元素是不用下标进行索引的。

TreeSet是最常用的集合,其可以对其中的元素进行排序,不重复,无索引。

11.3.2 Set集合增加的方法
需要注意的是,Set集合,没有增加Collection接口提供的以外的方法。

也就是说,Set集合,大部分都只有Collection中所提供的方法。

11.3.3 HashSet实现类

  1. HashSet的底层原理
    HashSet的底层,是基于哈希表和链表实现的。(拉链法的哈希表)

在JDK8之前,是单纯的拉链法的哈希表。

但是在JK8之后,是拉链法的哈希表 + 红黑树。

首先需要,先介绍一下哈希值。

在Object类中,提供了一个方法,叫做hashCode方法,这个方法可以返回当前类对象的hash值。

Dog dog = new Dog();
System.out.println(dog.hashCode());
Object类中,是采用对对象的地址进行运算获得哈希值(地址导出)。

同一个对象的hashCode相同,不同对象的hashCode大概率不同。

同时,返回的哈希值是一个int类型的数值,也就是说,最多可以容忍42亿个对象不发生哈希冲突。

对于HashSet集合,其通过一个对象的hashCode来决定一个对象应该存储的位置。

举个例子,以下是JDK8之前的HashSet的工作流程。

当我们去new一个hashSet的时候,会生成一个默认长度为16,装填因子为0.75的哈希表进行返回。

向集合中加入一个元素的时候,先用元素的哈希值对集合的长度取余数,即为放入的下标。

如果这个位置没有元素,则直接放入。

如果这个位置有元素:

先用equals方法进行判断是否是是同一个元素。

如果是同一个元素,则不再放入。

如果是不同的元素,则将这个元素挂在该位置的元素下面。(链表)

数组的长度 * 装填因子,就是最大的装载容量,当数组超过其装载容量,会扩容数组为其原本的容量的二倍,然后将原本数组中的元素进行迁移。

  1. HashSet的特点
    实际上,HashSet就是一个链表数组。

从HashSet的底层原理可以发现其特点:

无序,HashSet中元素的存放顺序和插入的顺序无关,和对象自己的哈希值有关。

元素不重复,如果是哈希值相同的元素,则会被equals方法判断出来,不会插入。

没有索引,元素的存放顺序和插入的顺序无关,索引没有意义。

package chapter6;

import java.util.HashSet;
import java.util.LinkedHashSet;

public class HashSetTest {
public static void main(String[] args) {
HashSet hashSet = new HashSet<>();
hashSet.add(5);
hashSet.add(4);
hashSet.add(1);
hashSet.add(2);
hashSet.add(2);
hashSet.add(3);
System.out.println(hashSet); // [1, 2, 3, 4, 5]
}
}
虽然,HashSet没有索引,但是其可以通过哈希值先去定位链表的表头,再再链表上进行查找,其搜索的效率较高。

同时得益于,HashSet用链表进行实现,其增加,删除的效率也较高。

但是,如果相同哈希值的元素过多,在链表上进行查找的速度也是不可接受的。

因此,在JDK8之后,只要当哈希数组的长度超过64,链表的长度超过16,就会自动变成一个红黑树。

只有,HashSet里面才会出现链表和红黑树的转换(HashSet中的元素,没有定义过比较规则怎么转换为红黑树?)

会报错,实际上,如果对一个没有定义比较规则的类集合进行Collections的sort方法,其也会报错。(注意,不会自动根据hashCode进行比较呢)

红黑树就是平衡二叉搜索树。

普通的二叉搜索树会有跛子的情况,也就是一边的子树过长,退化成一个链表。

因此,需要选择合适的元素作为二叉搜索树的树根。

所谓的平衡二叉树,就是左右子树的深度最大不相差1。

红黑树和普通的二叉搜索树相比,保证了更好的搜索性能,更好的增加,删除的性能。

  1. HashSet的隐患
    对于一个对象而言,我们当然是希望最好是根据对象中的内容去判断两个对象是不是相等的。而不是通过地址。

因此,对于两个内容相等的对象,我们希望其挂在同一个链表的下面。

而对于选择哪个链表去挂,是根据对象自己的哈希值去判断的,因此,我们需要去重写hashCode方法。

可以使用Object类中提供的hash方法,可以组合多个不同的属性来根据内容导出对象的哈希值。

Object.hash(name, age, …);
此外,我们是通过equals方法去判断两个对象是不是相等的。

Object方法中,提供的equals方法,是根据对象的地址去判断的。但是现在对象是否相等,是根据哈希值来判断的,

因此,我们还需要去重写equals方法,将其改写为通过对象内容的判断。

如果不重写hashCode方法,就会导致,多个相同的对象,因为其地址不同,哈希值不同,被放到不同的链表中。

如果不重写equals方法,就会导致,多个相同的对象,因此其地址不同,被判断为不相等,重复插入hashSet中。

我们最终希望的是,HashCode相同的对象,其equals的判断结果也是true。

11.3.4 LinkedHashSet实现类
LinkedHashSet的特点和LinkedList相同,目的都是为了让元素的存放顺序和读取顺序相同。

因此,LinkedHashSet的特点是:有序,不重复,无索引。

LinkedHashSet的底层原理
LinkedHashSet的底层原理是基于哈希表(链表数组)。

但是每个元素都额外增加了两个指针域,分别是pre和next。

LinkedHashSet中,每个元素都会记录其前驱和后继节点的地址。

同时,在整个的LinkedHashSet中,还有两个额外的节点,分别是头节点和尾节点。

但是需要注意的是,LinkedHashSet并没有提供任何和头尾操作有关的额外方法。

LinkedlList更占用内存。

因此,对LinkedHashSet进行print操作,可以发现,其中的元素读取的顺序和加入的顺序一致。

但是并不代表其存储的顺序就是一致的,其只是可以按照存入的顺序进行遍历。

package chapter6;

import java.util.LinkedHashSet;

public class HashSetTest {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add(1);
linkedHashSet.add(2);
linkedHashSet.add(2);
linkedHashSet.add(3);
System.out.println(linkedHashSet); // 1 2 3
}
}
因为是基于哈希表进行实现的,其增删改查的性能都可以。

11.3.4 TreeSet

  1. TreeSet的底层原理
    Tree的底层原理是根据红黑树进行实现的。

红黑树也就是平衡二叉搜索树,其可以对插入其中的元素自动进行排序。(小的在左子树,大的在右子树)

也就是说,TreeSet的特点是,可排序,不重复,没有索引。

TreeSet因为其可以对其中元素进行排序,是最常用的一个类。

如果TreeSet中的对象类型是数值类型,那么就会默认按照大小进行升序排列。如果是字符类型的话,会按照字符的编号进行排序。

  1. 自定义类型的TreeSet集合
    如果想要在TreeSet集合中加入自定义的类型。

就必须告诉TreeSet集合,自定义类型的比较方式是什么。也就是要给出比较的规则。

很明显,只要我们去实现Comparable接口或者给定一个Comparator接口的实现类就可以指定比较的规则。

需要注意的是,这里的两种方式是不同的。

在类的定义中,去实现一个Comparable接口,是定义了这个类的对象的比较规则。

但是,在定义一个TreeSet的集合过程中,传入的Comparator接口的实现类,定义的是这个TreeSet内部的比较规则。

这样就会出现,集合本身定义了比较规则,然后类对象自己也定义了比较规则。

这种情况,TreeSet会就近去选择自己定义的比较规则,忽略类对象自己定义的比较规则。

以下是一个实现Comparable接口的例子。

package chapter6;

import com.sun.source.tree.Tree;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.TreeSet;

public class HashSetTest {
public static void main(String[] args) {
TreeSet testTreeSets = new TreeSet<>();
testTreeSets.add(new testTreeSet(1));
testTreeSets.add(new testTreeSet(3));
testTreeSets.add(new testTreeSet(2));
System.out.println(testTreeSets);
}
}

class testTreeSet implements Comparable {
int value;

testTreeSet(int value) {
    this.value = value;
}

@Override
public String toString() {
    return value + " ";
}

@Override
public int compareTo(Object o) {
    return this.value - ((testTreeSet)o).value;
}

}
以下是实现一个Comparator接口的例子。

需要注意的是,Comparator接口需要以匿名内部类的方式实现后,传入TreeSet的定义中。

package chapter6;

import com.sun.source.tree.Tree;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.TreeSet;

public class HashSetTest {
public static void main(String[] args) {
TreeSet testTreeSets = new TreeSet<>(new Comparator() {
@Override
public int compare(testTreeSet o1, testTreeSet o2) {
return o1.value - o2.value;
}
});
testTreeSets.add(new testTreeSet(1));
testTreeSets.add(new testTreeSet(3));
testTreeSets.add(new testTreeSet(2));
System.out.println(testTreeSets);
}
}

class testTreeSet {
int value;

testTreeSet(int value) {
    this.value = value;
}

@Override
public String toString() {
    return value + " ";
}

}
11.4 不同的集合的适用场景
希望记住元素的添加顺序,允许有重复元素存在,希望查询和修改的速度较快。

使用ArrayList,ArrayList天生可以记住元素的添加顺序,同时对元素是否重复没有要求,可以通过下标进行访问元素,其查询和修改的速度非常快。

元素添加顺序 + 重复元素 + 首尾修改较多(栈和队列的情况)

使用LinkedList,其基于双链表实现,不用担心容量不够的情况,且提供了很多有关首尾操作的API。

不在意元素添加顺序 + 要求元素不重复 + 希望增删改查的速度都较快

可以使用HashSet,其基于哈希表进行实现,查询和修改的效率还算可以,同时底层是基于链表进行实现,所以增加和删除的速度也很快。

在意元素的添加顺序 + 元素不重复 + 效率都还可以

使用LinkHashSet,其底层基于双链表进行实现,其可记录下元素的添加顺序,同时元素的存放位置基于哈希表进行确定,所以增删改查的速度都还可以。

对元素进行排序 + 元素不重复 + 效率还可以

使用TreeSet,其底层基于红黑树进行实现,可以对放入其中的元素,按照自己定义的比较规则进行排序存储。

11.5 集合的并发修改异常
集合的并发修改指的是用多个用户正在对集合进行访问和修改。

举个例子就是,我在访问一个集合的同时,去删除集合中的元素,就会导致并发修改异常。

package chapter11;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;

public class ListMultiModiTest {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, 1, 1, 2, 3, 4);

    Iterator<Integer> iterator = arrayList.iterator();

    // 一边遍历一边修改元素
    while(iterator.hasNext()) {
        Integer temp = iterator.next();
        if(temp == 1) {
            arrayList.remove(temp);
        }
    }
}

}
这样去并发修改数组元素的话就会触发一个异常。

Exception in thread “main” java.util.ConcurrentModificationException
at java.base/java.util.ArrayList I t r . c h e c k F o r C o m o d i f i c a t i o n ( A r r a y L i s t . j a v a : 1013 ) a t j a v a . b a s e / j a v a . u t i l . A r r a y L i s t Itr.checkForComodification(ArrayList.java:1013) at java.base/java.util.ArrayList Itr.checkForComodification(ArrayList.java:1013)atjava.base/java.util.ArrayListItr.next(ArrayList.java:967)
at chapter11.ListMultiModiTest.main(ListMultiModiTest.java:16)
实际上,我们可以用for循环的角度来理解这个问题。

for循环中,如果我们去remove了一个元素,后面的元素就会自动顶上来,导致漏判顶上来的这个元素。

for(int i = 0; i < arrayList.size(); i ++) {
System.out.println(arrayList.get(i));
if(arrayList.get(i) == 1) {
arrayList.remove(i);
}
}
System.out.println(arrayList);
如果这样去删除元素的话,会导致元素不能删除干净。

对于for循环的方法,有两种解决的方法。

删除一个元素后,就对 i – 一次,对顶上来这个元素再判断一次。

或者倒着去判断,这样顶上来的元素是被判断过的元素,不用担心漏判的情况。

对于迭代器遍历的角度而言,迭代器就是知道了这种情况所以就用抛出异常的方式告诉你不许。

所以,迭代器自己提供了一个remove方法,来删除该迭代器位置的元素,同时,在底层做了一个 i – 的操作。

package chapter11;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;

public class ListMultiModiTest {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, 1, 1, 2, 3, 4);
System.out.println(arrayList);

    Iterator<Integer> iterator = arrayList.iterator();

    // 一边遍历一边修改元素
    while(iterator.hasNext()) {
        Integer temp = iterator.next();
        if(temp == 1) {
            // arrayList.remove(temp);
            iterator.remove();
        }
    }

    System.out.println(arrayList);
}

}
11.6 Collections工具类
11.6.1 可变参数
可变参数的定义。

可变参数就是一种特殊的形参,其定义的格式如下:

int…Name;
可变参数的特点在于,其可以接收任意数量的数据作为参数。这里的任意数量指的是,0个,1个,或者多个,甚至,还可以直接传入一个数组。

对于参数的处理更加灵活。

package chapter11;

public class CollectionsTest {
public static void main(String[] args) {
test();
test(1);
test(1,2,3,4);
test(new int[]{1, 3, 4, 5, 5, 6});
}

public static void test(int...nums) {

}

}
可变参数的使用。

在方法的外部,可变参数是一种很怪异的东西。

但是在方法的内部,可变参数实际上就是一个数组。因此,可以使用数组的属性length来对数组进行遍历访问。

package chapter11;

public class CollectionsTest {
public static void main(String[] args) {
test();
test(1);
test(1,2,3,4);
test(new int[]{1, 3, 4, 5, 5, 6});
}

public static void test(int...nums) {
    for (int i = 0; i < nums.length; i++) {
        System.out.println(nums[i]);
    }
}

}
可变参数的注意事项。

在一个形参列表中,只允许有一个可变参数存在。

可变参数必须放在整个形参列表的末尾。

11.6.2 Collections工具类
Collections是一个工具类,其作用是用来操作Collection对象的。

Collections工具类中提供了三个比较常用的方法:addAll,Shuffle,sort。

addAll方法
addAll方法的目的在于去批量添加数据。

addAll的使用方法:

void addAll(Collection a, Type…elems);
实际上,addAll提供了一个泛型的方法,Collection的类型可以用泛型来代替。

void addAll(Collection<? super T> a, T…elems);
也就是说,Animal类型的集合,可以向里面放入Cat类型的数据。

package chapter11;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

public class CollectionsTest {
public static void main(String[] args) {
Collection animals = new ArrayList<>();
Collections.addAll(animals, new Cat(), new Cat(), new Cat());
System.out.println(animals.size());
}
}

class Animal {

}

class Cat extends Animal {

}
这个地方callback一下,?是类型通配符,有了?就不用在前面写。

但是 ,需要注意的是,不可以单独直接去使用?作为参数的类型,因为,方法是必须要知道参数的类型,?可以用在集合中去表示这个集合可以接受任意类型的数据,但是这个参数的类型是一个集合是确定的。

而为什么泛型的T的写法是正确的,是应为a是确定的T类型,方法在使用的时候,根据传入的参数就可以去确定这个T的类型,也即是说参数的类型是确定的。

下面这种写法是错误的:

public static void test2 (? a) {
System.out.println(a.toString());
}
下面是正确的:

public static void test2 (T a) {
System.out.println(a.toString());
}
Shuffle方法
目的是用来打算List集合中元素的顺序。

Set集合中的元素的下标都是唯一确定的,所以打乱是没有效果的。

看起来有用,实际上除了写一个斗地主小游戏,基本没用。

package chapter11;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

public class CollectionsTest {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList<>();
Collections.addAll(arrayList, 1, 2, 3, 4);
Collections.shuffle(arrayList);
System.out.println(arrayList);
}
}
sort方法
目的是在于对List集合中的元素按照自己定义的规则进行排序。

对于基本类型的集合而言,可以直接调用sort方法,默认从小到大进行排序。

对于自定义的对象而言,需要告知sort方法比较的规则。

在自定义对象中,实现Comparable接口。

package chapter11;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

public class CollectionsTest {
public static void main(String[] args) {
ArrayList students = new ArrayList<>();
students.add(new student(1));
students.add(new student(3));
students.add(new student(2));

    Collections.sort(students);
    System.out.println(students);
}

}

class student implements Comparable{
int age;

public student(int age) {
    this.age = age;
}

@Override
public int compareTo(Object o) {
    return this.age - ((student)o).age;
}

@Override
public String toString() {
    return Integer.toString(age);
}

}
在使用sort方法的时候,传入一个Comparator实现类。

package chapter11;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;

public class CollectionsTest {
public static void main(String[] args) {
ArrayList students = new ArrayList<>();
students.add(new student(1));
students.add(new student(3));
students.add(new student(2));

    Collections.sort(students, new Comparator<student>() {
        @Override
        public int compare(student o1, student o2) {
            return o1.age - o2.age;
        }
    });
    System.out.println(students);
}

}
sort方法的比较规则而言。

左边的 - 右边的 就是从小到大排序。

右边的 - 左边的 就是从大到小排序。

11.7 Map集合接口
Collection集合被称为是单列集合,Map集合是双列集合。

Map集合中,所存储的元素的结构是 kay - value 的键值对,一对数据在Map集合是就是一个元素。

在Map中,要求键是唯一存在的,但是值是可以重复的。(相当于主键一样)同时,键和值是一一对应的关系。

如果需要存储一一对应的映射关系的数据的时候,就可以使用Map集合。

11.7.1 Map集合的体系结构
如下是比较常见的Map的实现类

map
HashMap
TreeMap
LinkedHashMap
Map集合体系的整体特点:

Map集合的特点都是针对Map中元素的键,对值是没有任何要求的。

HashMap:无序,不重复,无索引。

LinkedHashMap:有序,不重复,无索引。

TreeMap:按照大小默认升序排序,不重复,无索引。

package chapter11;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;

public class MapTest {
public static void main(String[] args) {
// 多态写法,非常经典
Map<String, Integer> map = new HashMap<>(); // HashMap 无序,不重复,无索引
// 在map中放入数据
map.put(“Jack1”, 12);
map.put(“Jack2”, 12);
map.put(“Jack3”, 12);
map.put(null, 12);
map.put(null, null); // map中允许键和值为null,但是键为null的无法访问
System.out.println(map);

    // LinkedHashMap 有序,不重复,无索引
    Map<String, Integer> map1 = new LinkedHashMap<>();
    map1.put("Jack1", 12);
    map1.put("Jack2", 12);
    map1.put("Jack3", 12);
    map1.put(null, 12);
    map1.put(null, null); // map中允许键和值为null,但是键为null的无法访问
    System.out.println(map1);

    // TreeMap集合 排序,不重复,无索引
    Map<Integer, String> map2 = new TreeMap<>();
    map2.put(1, "java");
    map2.put(3, "java");
    map2.put(2, "java");
    // TreeMap是不能存Null的键的,因为要排序,所以要有值
    System.out.println(map2);
}

}
11.7.2 Map集合中常用的方法
Map集合中所提供的方法可以被其所有的实现类共用。

Map是一个接口,其中提供了很多的操作双列集合的方法。

size
用来判断集合中有多少个键值对。

System.out.println(map2.size());
put
put方法用来向Map集合中加入数据。

需要注意的是,如果对于一个相同的key去put两次,那么只会保留最后一次put的结果。

也就是说,put也可以用来更新Map中的键值对。

map2.put(1, “java”);
clear
用来清空整个map,clear过后得map里面不含有任何元素。

map2.clear();
System.out.println(map2.size()); // 0
keySet
用来获得map中的所有键。

map中的键都有一个特点:不重复。

鉴于这个特点,获得到的key就可以用一个Set集合来存储。

需要注意的是,采用这种方法,可以遍历出key为null的元素。

Set keys = map1.keySet();
for(String key : keys) {
System.out.println(key);
}
values
用来获得map中的所有值。

对于Map而言,只对其中的键有要求,对其中的值是没有要求的,因此值是可以多次重复的,所以不用Set集合进行返回。

values方法将Map中所有的值都装在一个Collection集合中进行返回。

Collection values = map1.values();
for(Integer value : values) {
System.out.println(value);
}
get
get方法可以通过map中的一个特定的键来获取到指定的值。

null是可以作为一个key被索引到的。

System.out.println(map.get(null));
System.out.println(map.get(“Jack1”));
remove
remove方法可以通过map集合中的一个特定的键来移除一个元素。

remove方法在移除元素之后,会将元素的值进行返回。

Integer value = map.remove(“Jack1”);
System.out.println(value);
containsKey
containsKey用在Map集合中用来表示这个Map集合是否包含有对应的键值。

System.out.println(map.containsKey(“Jack”));
containsValue
System.out.println(map.containsValue(1));
11.7.3 Map的遍历方式
通过Map中的键来遍历所有的元素

基本思想就是,通过keySet方法,将所有的key获取到,通过一个Set集合来进行存放,再去遍历这个Set集合,通过Map提供的get方法,来通过键获取值。

Set keys = map.keySet();
for(String key : keys) {
Integer value = map.get(key);
System.out.print(value + " ");
}
通过Map中的键值对来遍历所有的元素

Map集中中,定义了一个额外的类型,称为,entry类型,这个类型将Map中的一行(一个键值对)整体打包为一个元素进行存放,entry类型中提供了两个常用的方法:getkey和getValue。

Map集合中,提供了一个方法entrySet,这个方法可以将map中的元素(键值对)打包为一个整体的元素,再将将所有的entry对象存放在一个Set集合中进行返回。

Set<Map.Entry<String, Integer>> entries = map.entrySet();
for(Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
通过Lambda表达式来便遍历所有的元素

Map集合中,也提供了一个forEach方法,这个forEach方法接受一个Biconsumer接口类型的实现类作为参数,这个接口是一个函数接口,其中只有一个Accept方法,所以可以通过Lambda表达式来创建一个匿名内部类。

在这个Accept方法中,参数是keyName和valueName,方法体的内部就是对键值的访问和操作。

实际上,forEach方法内部,就是利用entrySet的方法进行遍历,在获取到键值对后,利用所传入的Biconsumer实现类中的Accept方法来对键值进行访问操作。

forEach方法的内部操作

default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
使用Lambda表达式进行遍历的写法

// lambda表达式进行遍历
// 完整的写法
map.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String s, Integer integer) {
System.out.println(s + " " + integer);
}
});
// lambda表达式的简化写法
map.forEach((keyName, valueName)->{System.out.println(keyName + " " + valueName);});
11.7.4 HashMap
HashMap的特点在于:无序,不重复,无索引。

HashMap的底层实现原理还是基于哈希表和链表进行实现,实际上,HashSet就是利用HashMap在底层进行实现的,只是HashSet不需要键,只需要值,以下是一个HashSet的构造函数之一。

public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
所以,HashMap就是HashSet,HashSet的特点就是HashMap的特点。

在JDK8之前,HashMap基于:链表 + 哈希表。

在JDK8之后,HashMap基于:链表 + 哈希表 + 红黑树。

HashMap在put元素的时候,以entry对象进行存储。

首先,根据key值和链表的长度求余,计算得到现在的下标位置,如果加入当前这个元素会导致数组超过其装填因子,会扩容为原本的两倍,迁移所有的元素后,返回新的内存地址。

如果当前的位置已经有元素进行存放了:

JDK8之前:直接挂在下面。

JDK8之后:暂时挂在下面,如果链表的长度超过了8,且数组的长度超过了64,就会自动转换为红黑树进行存放

HashMap中,基于hashCode来判断键是否唯一。

因此,如果HashMap中存放的是自定义的元素,就必须去重写hashCode方法和equals方法。

以下是一个重写hashCode方法和equals方法的例子:

class elem {
int age;
String name;
double height;

public int hashCode(){
    return Objects.hash(age, name, height);
}

public boolean equals(Object o) {
    if(o == this) return true;
    else if(o == null || o.getClass() != this.getClass()) return false;
    return ((elem)o).age == this.age && ((elem)o).height == this.height && ((elem)o).name == this.name;
}

public elem(int age, String name, double height) {
    this.age = age;
    this.name = name;
    this.height = height;
}

}
11.7.5 LinkedHashMap
LinkedHashMap的特点在于:有序,不重复,无索引。

LinkedHashMap的有序在于,遍历的顺序和存入的顺序一致,但是在内存中实际存储的顺序则不一定。

LinkedHashMap的底层是基于:双链表 + 哈希表。

LinkedHashMap中的每一个元素而言,都额外提供两个指针来记录前后的顺序关系(双链表),在整体的链表上也提供了头指针和尾指针来记录双链表开始和结束的位置。LinkedHashMap的遍历就是基于头尾指针进行遍历。

实际上,LinkedHashSet就是LinkedHashMap实现的,只是LinkedHashSet不需要键。

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
在put元素的时候,通过entry对象进行存储。

同时采用尾插法,去移动尾指针,和更新前后指针的关系。

因为是采用哈希表进行存储,其增删改查的性能都还可以。

import java.util.*;

public class HashSetTest {
public static void main(String[] args) {
LinkedHashMap<elem, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put(new elem(1,“Jack1”, 1.1), 1);
linkedHashMap.put(new elem(2,“Jack2”, 1.1), 1);
linkedHashMap.put(new elem(3,“Jack3”, 1.1), 1);

    Set<Integer> set = new LinkedHashSet<>();
    System.out.print(linkedHashMap);
}

}
11.7.6 TreeMap
TreeMap集合的特点是:可排序,不重复,无索引。

TreeMap会默认根据键值的大小的进行排序,也就是说如果键值是自定义的类型,就必须去自定义一个排序的规则。可以通过在自定义的类中去实现Comparable接口,或者在创建TreeMap集合的时候去传入一个Comparator实现类。

TreeMap的底层实现原理是基于红黑树进行实现。

因为TreeMap需要按照键值的大小进行排序,所以键不可以为null。

以下是一个TreeMap的例子。

import java.util.*;

public class HashSetTest {
public static void main(String[] args) {
TreeMap<elem, Integer> treeMap = new TreeMap<>();
treeMap.put(new elem(1,“Jack1”, 1.1), 1);
treeMap.put(new elem(2,“Jack2”, 1.1), 1);
treeMap.put(new elem(3,“Jack3”, 1.1), 1);

    Set<Integer> set = new LinkedHashSet<>();
    System.out.print(treeMap);

    TreeMap<elem, Integer> treeMap2 = new TreeMap<>(new Comparator<elem>() {
        @Override
        public int compare(elem o1, elem o2) {
            return ((elem)o1).age - ((elem)o2).age;
        }
    });
    treeMap2.put(new elem(1,"Jack1", 1.1), 1);
    treeMap2.put(new elem(2,"Jack2", 1.1), 1);
    treeMap2.put(new elem(3,"Jack3", 1.1), 1);
}

}

class elem implements Comparable{
int age;
String name;
double height;

public int hashCode(){
    return Objects.hash(age, name, height);
}

public boolean equals(Object o) {
    if(o == this) return true;
    else if(o == null || o.getClass() != this.getClass()) return false;
    return ((elem)o).age == this.age && ((elem)o).height == this.height && ((elem)o).name == this.name;
}

public elem(int age, String name, double height) {
    this.age = age;
    this.name = name;
    this.height = height;
}

@Override
public String toString() {
    return ((Integer)age).toString() + " " + name + ((Double)height).toString();
}

@Override
public int compareTo(Object o) {
    return this.age - ((elem)o).age;
}

}
11.8 集合的嵌套
集合的嵌套就是,集合中的元素可以为一个集合。

举个例子,我想要存储的映射关系是:A = 1, 2, 3的时候,首先,我需要一个Map集合的对象来存储映射关系。

Map集合的第一个元素类型肯定是String,而第二个元素类型肯定就是一个List类型的元素。

以下是一个代码的例子:

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class CollecionInCollection {
public static void main(String[] args) {
// map的第一个元素是String,第二个元素类型是ArrayList
Map<String, ArrayList> map = new HashMap<>();

    String key = "A";

    ArrayList<Integer> values = new ArrayList<>();
    Collections.addAll(values, 1, 2, 3, 4);
	
    map.put(key, values);
    System.out.println(map);
}

}
12 Stream流
12.1 认识Stream流
Stream流和Lambda表达式都是从JDK8开始引入的两个新的特性。

Stream流就是用来操作集合和数组中数据的一种方式。

和集合以及数组本身所提供的操作方法相比,Stream流大量结合了Lambda表达式的编程风格,处理数据更加见解方便,代码的可读性更强。

举个例子,现在有一个String类型的数组,我要找出其中所有的以A开头,长度为3的元素。

采用以前的方法则有:

对于以前的方法而言,只能通过遍历整个数组,找到其中含有的符合条件的元素,需要对数组中所有的元素一个一个按照顺序进行检查。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class SteamFirstTest {
public static void main(String[] args) {
List list = new ArrayList<>();
Collections.addAll(list, “ABC”, “ACD”, “ACC”, “A”,“VB”,“ASDAS”);

    // 采用以前的方法,就只能通过遍历整个数组,一个一个元素去找符合要求的元素
    List<String> ans = new ArrayList<>();
    for(String it : list) {
        if(it.startsWith("A") && it.length() == 3) {
            ans.add(it);
        }
    }
    System.out.println(ans);

}

}
采用Stream流的方法:

可以看出的是,采用Stream方法,代码量被大量减少了。

同时,代码的风格更加简洁易读。

// 采用Stream流的方法
Stream stringStream = list.stream();
// 首先通过filter方法,过滤出所有开头为A长度为3的元素,Lambda表达式,return语句省略只写返回的东西
// 再通过collect方法,将过滤出来的元素,装入到一个集合中,这个集合用toList表示为一个List集合
List ans2 = stringStream.filter(s -> s.startsWith(“A”) && s.length() == 3).collect(Collectors.toList());
System.out.println(ans2);
12.2 Stream流的一般使用过程
第一步:

我们需要确定数据的来源(可以是数组,也可以是Collection集合(单列集合,不含有Map集合))

第二步:

我们需要获得Stream流对象。

第三步:

用Stream类中所提供的方法来对Stream进行处理。可以将Stream流当作一个流水线,在这个流水线上,提供了很多方法来对流水线上的元素进行操作。

第四步:

获得最终的结果,并对最终的结果进行访问或者操作。

12.3 Stream流的常用方法
12.3.1 获取Stream的常用方法
Stream流本身是一个接口,因此我们不可以直接去创建一个Stream流的对象。

对于Collecion集合而言,其提供了Stream方法,可以直接返回一个Stream对象。



对于List集合而言,可以直接通过Stream方法获得Stream对象。

List list = new ArrayList<>();
Collections.addAll(list, “ABC”, “ACD”, “ACC”, “A”,“VB”,“ASDAS”);
Stream stringStream = list.stream();

对于Set集合而言,也可以通过Stream方法获得对应的Stream对象。

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class getStreamTest {
public static void main(String[] args) {
Set set = new HashSet<>();
Collections.addAll(set, 1, 2, 3, 4, 5);

    Stream<Integer> integerStream = set.stream();
    List<Integer> list = integerStream.filter(integer -> {
        if(integer % 2 == 0) {
            return true;
        }
        else return false;
    }).collect(Collectors.toList());

    System.out.println(list);
}

}

对于Map集合,其不是Collection的集合子类,所以其不能直接使用Stream方法获得对应的Stream。

现在有两种思路,第一种就是,使用keySet方法,或者values方法,将Map集合中的键值提取出来,作为一个单独的Set或者List集合,对于Set集合和List集合就可以使用Stream方法获得对于的Stream对象。

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class getStreamTest {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put(“A”, 1);
map.put(“B”, 2);
map.put(“C”, 3);

    List<String> keysConditional = map.keySet().stream().filter(s -> s.contains("A")).collect(Collectors.toList());
    System.out.println(keysConditional);
    List<Integer> valuesContional = map.values().stream().filter(integer -> {
        if (integer % 2 == 0) return true;
        return false;
    }).collect(Collectors.toList());
    System.out.println(valuesContional);
}

}
第二种思路是,将Map中的键值对当作一个整体,也就是用entrySet方法,将Map转换一个Map.entry类型的Set集合,对于这个Set集合,就可以通过Stream方法获得对应的Stream对象。需要注意的是,在Stream类型中的数据类型也是entry类型的,可以通过getKey和getvalue方法去获得entry对象的键和值,并进行访问操作。

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class getStreamTest {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put(“A”, 1);
map.put(“B”, 2);
map.put(“C”, 3);

    List<Map.Entry<String, Integer>> collect = map.entrySet().stream().filter(stringIntegerEntry -> stringIntegerEntry.getKey().contains("A")).collect(Collectors.toList());
    System.out.println(collect);
}

}
对于Array数组而言,可以通过Arrays所提供的Stream方法进行获取。

String[] strings = {“A”, “B”, “C”};
Stream stringStream = Arrays.stream(strings);

List ans = stringStream.filter(s -> s.contains(“A”)).collect(Collectors.toList());
System.out.println(ans);
对于一些离散的临时数据而言,Stream提供了一个方法,of方法,可以将数据转换为Stream类型的对象进行返回。

Stream a = Stream.of(“A”, “B”, “C”);
List a1 = a.filter(s -> s.contains(“A”)).collect(Collectors.toList());
System.out.println(a1);

12.3.2 Stream中常见的中间方法
image-20240122171037026

中间方法

所谓中间方法,值的就是,会返回一个新的Stream流的方法,Stream仍然在流水线上。

filter 过滤
filter方法利用Lambda表达式表述过滤的规则,其中Stream中的元素,只有在过滤规则返回true的时候留下。

List scores = new ArrayList<>();
Collections.addAll(scores, 88.5, 100.0, 60.0, 99.0, 99.5, 99.6, 25.0);
// 寻找Stream中成绩大于等于60的元素
List ans = scores.stream().filter(aDouble -> aDouble >= 60).collect(Collectors.toList());
System.out.println(ans);
filter方法中,只传入一个参数,这个参数用来遍历所有的Stream中的元素。

filter方法的方法体中,是对这个参数是否满足条件的判断,要求方法体的末尾返回一个boolean值。

只有满足条件的Stream中的对象才会留在Stream中。

sorted 排序
sorted方法提供两个重载。

最基本的sorted方法,没有参数传入,可以对基本的数据的Stream进行默认的升序排序。

// 寻找成绩大于等于60分的学生,并且按照成绩对其进行升序排序
List ans = scores.stream().filter(aDouble -> aDouble >= 60).sorted().collect(Collectors.toList());
System.out.println(ans);
sorted方法还提供了一个重载方法,是用来对自定义类型的对象Stream进行排序的,所以我们需要传入一个比较规则,也就是Comparator对象。

List students = new ArrayList<>();
Collections.addAll(students, new Student(21, “A”), new Student(25, “B”),
new Student(27, “C”), new Student(26, “D”));
// 使用Lambda表达式生成一个Comparato对象传入sorted方法中
List ans2 = students.stream().filter(student -> student.age >= 25 && student.age <= 30).sorted((o1, o2)->{return o2.age - o1.age;}).toList();
System.out.println(ans2);
limit 前N个元素
limit方法可以选择现在流中的前面N个元素。

// 寻找Stream中年纪最大的三个学生
// 可以将Stream按照年纪进行降序排序后,选中前面三个元素即可
List ans3 = students.stream().sorted(((o1, o2) -> o2.age - o1.age)).limit(3).toList();
System.out.println(ans3);
skip 跳过N个元素
skip方法可以跳过现在流中的前N个元素。

// 找到年纪最小的3个学生
// 有一个思路是,将所有的学生按照年龄降升排序后,选择前面的3个元素
List ans4 = students.stream().sorted((o1, o2) -> o1.age - o2.age).limit(3).toList();
System.out.println(ans4);

// 还有一个思路是,将现在的学生按照年龄降序排序后,跳过前Size - 3个元素,剩下的就是年龄最小的三个元素
List ans5 = students.stream().sorted((o1, o2) -> o2.age - o1.age).skip(students.size() - 3).toList();
System.out.println(ans5);
distinct 去重
distinct可以按照equals方法和hashCode方法来判断现在Stream中的元素是否有重复元素。

distinct方法会去除重复的元素,在Stream流中保留一个相同元素的副本。

需要注意的是,只去重写equals方法是不奏效的。

重写Student类中的equals方法和hashCode方法。

按照Student类中的name作为key,去掉所有name值相同的Stream中的对象。

@Override
public boolean equals(Object obj) {
Student o = (Student)obj;
return o.name == this.name;
}

@Override
public int hashCode() {
return Objects.hash(this.name);
}
使用distinct方法对Stream类中的对象进行去重。

students = new ArrayList<>();
Collections.addAll(students, new Student(21, “A”), new Student(25, “B”),
new Student(27, “C”), new Student(26, “D”),
new Student(10, “A”), new Student(12, “A”));
List ans6 = students.stream().distinct().collect(Collectors.toList());
System.out.println(ans6);
map 加工
map方法可以理解为,用来对Stream类中的对象进行加工。

可以将Stream类中的元素类型从一个类型转变为另外一个类型。

对于自定也类型而言,map方法可以将Stream中的元素变成所有对象的某个成员属性所组成的Stream。

这份代码中,将一个原本为Students对象所组成的流,改变为了由所有Students对象中的name属性所组成的一个String类型的对象的流。

List ans7 = students.stream().map(student -> student.name).collect(Collectors.toList());
System.out.println(ans7);
concat 合并
concat方法可以将两个相同类型的流进行合并,合并后,Stream的类型不变。

需要注意的是,不同类型的两个流也可以合并,但是合并后的流,其类型是Object。

List students2 = new ArrayList<>();
students2.add(new Student(1, “A”));
List students3 = new ArrayList<>();
students3.add(new Student(2, “B”));

Stream stream = Stream.concat(students2.stream(), students3.stream());
List allStudents = stream.collect(Collectors.toList());
System.out.println(allStudents);
需要注意的是,concat合并后的流中的元素的顺序和合并的顺序一致。

如果是concat(A, B);那么Stream中,是A的元素再B的元素之前。

12.3.3 Stream中常见的终结方法
终结方法

所谓终结方法,就是在调用这个方法之后,不会再产生新的Stream流,对Stream流的所有操作到此为止。

forEach 方法
forEach方法就是取遍历流中的所有元素。

集合Collection集合,Map集合的forEach方法功能相同。

采用Lambda表达式进行遍历,输入的参数用来遍历Stream中的每一个元素。

// forEach 方法
studentList.forEach(student -> {System.out.println(student.age + " " + student.name);});
count 方法
count方法用来计数现在Stream方法中有多少个元素。

// count 方法
System.out.println(studentList.stream().count());
max 方法
max方法用来获取现在Stream中的所有元素中的最大的元素。

元素之间的比较规则由传入map的Comparator进行指定。

需要注意的是,这个的Comparator是必须传入的,比较规则不能通过实现Comparable接口来实现。

ans = studentList.stream().max((o1, o2) -> o1.age - o2.age).get();
min 方法
min方法用来获取现在Stream流中的所有元素的最小的元素。

元素之间的比较规则由传入map的Comparator进行指定。

ans = studentList.stream().min((o1, o2) -> o1.age - o2.age).get();
System.out.println(ans);
用来收集的终结方法
Stream流主要是用来操作和处理集合和数组中的数据的。

但是,处理的最终的目的还是数组和集合本身。

所有,一般我们需要将处理完的Stream中的元素重新收集为一个集合或者数组。

collect 方法
collect方法是用来将Stream流收集为一个集合对象。

可以将Stream收集为List集合,Set集合,和Map集合。

其中,需要额外注意的一点是,toMap方法的目的是让Stream流中的元素变成一个Map集合。

Map集合天生有一个特点是,要求Key值不重复,但是toMap方法不会自动去去除相同的key值,所有需要再toMap之前,提前用一次distinct方法去重。

// collect 方法
// 收集为一个List集合
List list = studentList.stream().collect(Collectors.toList());
System.out.println(list);
// 收集为一个Set集合
Set set = studentList.stream().collect(Collectors.toSet());
System.out.println(set);
// 收集为一个Map集合
// 需要注意的是,toMap方法,并不会将Stream中的重复元素去除
Map<Integer, String> map = studentList.stream().distinct().collect(Collectors.toMap(a -> a.getAge(), b -> b.getName()));
System.out.println(map);
toArray 方法
toArray方法的目的在于,将Stream中的元素收集为一个数组。

toArray方法和Collection集合中的toArray方法非常相似,其提供了一个没有参数的版本,其会将Stream流中的元素转换为一个Object数组。

// toArray 方法
// 无参数的版本
Object[] objects = studentList.toArray();
for(int i = 0; i < objects.length; i ++) {
System.out.println(((Student)objects[i]).getAge() + " " + ((Student)objects[i]).getName());
}
toArray方法也提供了一个带有参数的版本,其可以将Stream转换为一个指定类型的数组。

toArray的两个重载形式和Collection集合中所提供的两个toArray方法有异曲同工之秒。

// 有参数的版本,收集到一个指定的数组中
// 带参数的版本中,其接受的参数是一个接口的实现类,用Lambda表示可以表示为,只有一个参数
// 这个参数表示的是Stream流中的元素的数量,在方法体的内部生产长度为元素数量的类对象数组返回即可
Student[] students = studentList.toArray(s -> new Student[s]);
for(Student s : students) {
System.out.println(s.age + " " + s.name);
}
13 方法引用
方法引用就是去复用方法的实现。

将其他地方写过的方法体,复用,当作现在正在书写的方法体。

一般来说,在实现一个函数式接口的时候,可以考虑采用方法引用。

13.1 方法引用的条件
不是所有的地方都可以使用方法引用。

只有函数式接口的实现类可以使用方法引用来简化方法体的书写。

也不是所有的方法都可以作为被引用的方法。

被引用的方法必须满足以下的三个条件:

被引用的方法必须已经存在。

被引用的方法必的参数和返回值必须和函数式接口中的抽象方法一致。

被引用的方法必须满足需求。

举个例子。

在Arrays.sort方法中,我们可以去传入一个Comparator对象,并且去重写其中的compare方法。

我们常用的compare方法体就是 o1.age - o2.age 。

如果这个时候,我们已经写过了一个方法叫做sub方法,其参数和方法体和compare方法体一致的话,就可以用方法引用。

import java.util.Arrays;
import java.util.Comparator;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class MethodRefer {
public static void main(String[] args) {
Integer[] integers = new Integer[10];
for(int i = 0; i < 10; i ++) {
integers[i] = 10 - i;
}

    // 这个地方如果我们已经写过了一个减法函数,那么就可以采用方法引用的形式
    Arrays.sort(integers, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2;
        }
    });

    // 方法引用的格式为 类::类中定义的方法
    Arrays.sort(integers, MethodRefer::sub);

    Arrays.stream(integers).toList().forEach(new Consumer<Integer>() {
        @Override
        public void accept(Integer integer) {
            System.out.println(integer);
        }
    });
}

public static int sub(Integer o1, Integer o2) {
    return o1 - o2;
}

}
13.2 方法引用的使用
方法引用的格式为:

类名::类中定义的方法名;

ClassName::MethodName;
// 方法引用的格式为 类::类中定义的方法
Arrays.sort(integers, MethodRefer::sub);
方法引用出现的目的是去进一步简化Lambda表达式。

13.2.1 静态方法的方法引用
静态不依赖于一个单独的对象,其只依赖于一个具体的类。

所以可以直接使用类名来找到这个方法。

静态方法方法引用的书写格式:

// ClassName::StaticMethodName
Integer::parseInt

现在有如下的需求,将String类型转变为int类型的数据。

apply方法中,返回值是Integer类型,参数是String类型。

Integer类中,提供的parseInt方法,返回值也是Integer类型,参数也是String类信息。

所以可以用Integer.parseInt来作为静态方法引用代替apply的方法体。

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;

public class StaticMethodRefer {
public static void main(String[] args) {
ArrayList strings = new ArrayList<>();
Collections.addAll(strings, “1”, “2”, “3”);

    // 以前的方法,遍历,依次转换
    // 这里需要补充一下的是,map方法的参数接受的是一个Function类的实现类,其是一个泛型方法,第一个参数表示Stream中原本的数据类型
    // 第二个参数表示经过加工后所转换成的数据类型,Function是一个函数式接口,其中只有一个方法apply,表示类型转换的操作。
    strings.stream().map(new Function<String, Integer>() {

        @Override
        public Integer apply(String s) {
            return Integer.parseInt(s);
        }
    }).forEach(System.out::println);

    // 现在的方法,直接使用方法引用
    strings.stream().map(Integer::parseInt).forEach(System.out::println);
}

}
13.2.2 成员方法的方法引用
成员方法是来自于一个具体的对象。

所以需要先new一个存在的对象才可以找到这个成员方法。

成员方法的方法引用的写法为:

// ClassInstanceName::MethodName
new Student()::getAge();
现在的需求是,对数组中的每一个元素都加1。

在原本的apply方法中,有一个Integer参数,返回值也是Integer类型的。

现在有一个类叫做addOne,其中有一个方法叫做add,add方法的参数类型和返回值完全符合,可以用作方法引用。

import java.util.function.Function;
import java.util.stream.Stream;

public class InstanceMethodRefer {
public static void main(String[] args) {
// 现在的需求是对数组中所有的元素都加1
Integer[] integers = new Integer[]{1,2,3,4};
// 原本的方法就是利用map加工数组里面的每一个数据,对其加1后进行返回即可
Stream.of(integers).map(new Function<Integer, Integer>() {

        @Override
        public Integer apply(Integer integer) {
            return integer + 1;
        }
    }).forEach(System.out::println);
    // 现在的方法就是采用方法引用
    Stream.of(integers).map(new addOne()::add).forEach(s -> System.out.println(s));

}

}

class addOne {
public Integer add (Integer integer) {
return integer + 1;
}
}
13.2.3 父类的成员方法的方法引用
一般的写法格式为:

super::MethodName
需要额外注意的是,super和this关键字不能出现在静态方法中。

因此,父类的成员方法引用不可以出现在静态方法的方法体中。

这个地方需要额外说的一点是,main方法也是一个static方法,所以先得new一个自己的类对象出来,才能去调用成员方法。

import java.util.function.Function;
import java.util.stream.Stream;

public class InstanceMethodRefer extends addOne{
public static void main(String[] args) {
InstanceMethodRefer instanceMethodRefer = new InstanceMethodRefer();
instanceMethodRefer.test();
}

public void test() {
    Integer[] integers = new Integer[]{1,2,3,4};
    Stream.of(integers).map(super::add).forEach(System.out::println);
}

}
class addOne {
public Integer add (Integer integer) {
return integer + 1;
}
}
13.2.4 本类中的成员方法的方法引用
一般的写法格式为:

this::MethodName
需要额外注意的是,super和this关键字不能出现在静态方法中。

因此,本类的成员方法引用不可以出现在静态方法的方法体中。

import java.util.function.Function;
import java.util.stream.Stream;

public class InstanceMethodRefer {
public static void main(String[] args) {
InstanceMethodRefer instanceMethodRefer = new InstanceMethodRefer();
instanceMethodRefer.test();
}

public void test() {
    Integer[] integers = new Integer[]{1,2,3,4};
    Stream.of(integers).map(this::add).forEach(System.out::println);
}

public Integer add(Integer integer) {
    return integer + 1;
}

}
13.2.5 构造方法的方法引用
一般的写法为:

ClassName::new
现在有一个需求是,有一个String类型的List集合中,存储了很多的"A,1"类似的元素,现在要将其改为一个Student对象,第一个为名字,第二个为年龄。

根据需求,可以得到的是,从String类型变成Map类型,很自然想到加工方法map。

以前的方法就是,map方法中,按照逗号拆分为2个string分别传入new Student的参数列表中,生成新的Student对象后返回。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

public class InstanceMethodRefer {
public static void main(String[] args) {
List list = new ArrayList<>();
Collections.addAll(list, “A,1”, “B,2”, “C,3”);
list.stream().map(new Function<String, Student>() {
@Override
public Student apply(String s) {
String[] arr = s.split(“,”);
String name = arr[0];
int Age = Integer.parseInt(arr[1]);
return new Student(Age, name);
}
}).forEach(student -> System.out.println(student.age + " " + student.name));
}
}
实际上,我们可以新建一个Student类的构造方法,传入的就是一个String类型的数据,在构造方法中进行拆分。

需要额外注意的一点是,构造方法是不需要有返回值的,所以只需要关注参数列表是否一致即可。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

public class InstanceMethodRefer {
public static void main(String[] args) {
List list = new ArrayList<>();
Collections.addAll(list, “A,1”, “B,2”, “C,3”);
list.stream().map(Student::new).forEach(student -> System.out.println(student.age + " " + student.name));
}
}
public Student(String s) {
String[] arr = s.split(“,”);
String name = arr[0];
int Age = Integer.parseInt(arr[1]);
this.name = name;
this.age = Age;
}
13.2.6 类名引用成员方法(非静态方法)
一般的格式为:

ClassName::MethodName
使用这种方法有很多规则:

不是所有的类中都可以被引用。

被引用的方法的形参,其参数必须和引用方法第二个参数开始完全一致。

第一个参数,就是被引用方法的调用者,决定了可以引用哪些方法,在Stream流中,第一个参数一般都是流中的每一个数据,假设流中的数据都是String类型的话,就只能引用String类中的方法。

上面这些东西还是很抽象。

用一个具体的例子来进行说明。

现在在集合中有很多字符串,需要将这些字符串都转换为大写后进行输出。

以前的方法就是,遍历,用map方法,对每个元素进行一次进行加工。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

public class ClassNameRefer {
public static void main(String[] args) {
List list = new ArrayList<>();
Collections.addAll(list, “aaa”, “bbb”, “ccc”);

    list.stream().map(new Function<String, String>() {
        @Override
        public String apply(String s) {
            return s.toUpperCase();
        }
    }).forEach(s -> System.out.println(s));
}

}
在这个地方,抽象方法apply的第一个参数是String类型的。

那么用现在这种方法的话,就只能去引用String类型中的方法。也就是方法体中的toUpperCase方法。

因此,这里可以采用String的方法引用。

需要注意的是,采用这种方法,是不用去考虑被引用的方法是不是静态方法的。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;

public class ClassNameRefer {
public static void main(String[] args) {
List list = new ArrayList<>();
Collections.addAll(list, “aaa”, “bbb”, “ccc”);

    list.stream().map(String::toUpperCase).forEach(s->System.out.println(s));
}

}
13.2.7 数组的构造方法引用
实际上,数组也是一个对象。

在申请一个数组的时候,实际上数组的构造方法也被调用了。

int[] a = new int[4];
数组构造方法的一般格式为:

int[]::new
数组构造方法的目的就是用来创建一个数组。

可以这么理解,int[] 是一个类,创建的时候 new int[3] 3 是传入构造方法的参数

import java.util.ArrayList;
import java.util.Collections;
import java.util.function.IntFunction;

public class ArrayMethodRefer {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList<>();
// 现在有一些需求是,在集合中有很多的整数,现在想要将这些整数存入到一个数组中
Collections.addAll(arrayList, 1, 2, 3, 4);
// 不带参数的toArray方法会返回一个Object类型的数组
Object[] objects = arrayList.stream().toArray();
// 如果想要返回指定类型的数组的话,就需要使用带参数的toArray方法
Integer[] integers = arrayList.stream().toArray(new IntFunction<Integer[]>() {
@Override
public Integer[] apply(int value) {
return new Integer[value];
}
});
// 可以发现的是,apply方法,他接受的参数是一个int类型的,返回值是一个Integer数组类型的
// 和数组的构造方法的参数列表和返回值一致,因此,可以使用数组的构造方法
integers = arrayList.stream().toArray(Integer[]::new);
for (int i = 0; i < integers.length; i++) {
System.out.println(integers[i]);
}
}
}
13.3 方法引用的总结
如何去选择合适的方法引用?

以下是一部分技巧:

如果现在已经存在一个方法,其可以满足我们的需求,我们就需要去检查其是否满足引用的规则。

如果这个方法是一个静态方法,那么,类名::方法名。

如果这个方法是一个成员方法,那么有以下的三种情况:

如果是其他类中的成员方法,new 类名()::方法名。

如果是本类中的方法,但是参数从第二个参数开始才相同,且调用的方法是第一个参数的类中提供的方法,类名::方法名。

如果是本类中的方法,参数列表完全符合,且调用的地方不是一个静态函数,this::方法名。

如果是父类中的方法,参数列表完全符合,且调用的地方不是一个静态函数,super::方法名。

现在有如下的案例。

现在有一个学生对象组成的集合,现在要将学生的姓名提取出来变成一个数组。

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ReferWork {
public static void main(String[] args) {
List studentList = new ArrayList<>();
Collections.addAll(studentList,
new Student(1, “A”),
new Student(2, “B”),
new Student(3, “C”)
);

    // 这个地方采用方法引用
    // 首先 getName 是Student类中的一个方法,是非静态方法
    // 所以是成员方法的方法引用,有三种情况
    // 1. 不是本类的方法,参数列表和返回值完全可以对上的话,就可以用new ClassName()::MethodName
    // 2. 是本类的方法,但是参数列表和被引处的方法的第二个参数开始才完全匹配,可以使用 ClassName::MethodName
    // 3. 是本类的方法,参数和返回值完全相同,但是引用处处于非静态方法内部,可以使用 this::MethodName
    studentList.stream().map(Student::getName).toArray(String[]::new);
}

}
14 文件
14.1 File的概述和构造方法
14.1.1 File的概述
路径:就是一个文件所保存的位置。

绝对路径:带上盘符的所有路径。

相对路径:不带上盘符,只相对于当前路径的表达方式。

File对象是一个路径的表示,可以是一个文件的路径,可以是一个文件夹的路径,可以存在也可以不存在。

14.1.2 File的构造方法
image-20240131161243222

需要注意的是,String中,/表示的是转义字符。

如果要表示文件分隔符号的画,要使用//。

根据文件路径创建文件对象
所传入的路径可以是绝对路径,也可以是相对路径。

import java.io.File;

public class FileTest {
public static void main(String[] args) {
File file = new File(“a.txt”);
System.out.println(file);
file = new File(“D://JavaFile//a.txt”);
System.out.println(file);
}
}
根据File父路径和String子路径创建
这么做的目的在于去对父路径和子路径进行一个拼接操作。

Java是一个跨平台的语言,但是win上和Linux上的文件分隔符号不一样,采用这种方法进行拼接,其分隔符号会根据不同的平台自动适应。

import java.io.File;

public class FileTest {
public static void main(String[] args) {
File file = new File(“D://JavaFile”);
file = new File(file, “a.txt”);
System.out.println(file);
}
}
根据String父路径和String子路径进行创建
import java.io.File;

public class FileTest {
public static void main(String[] args) {
String file = “D://JavaFile”;
String file2 = “a.txt”;
File file3 = new File(file, file2);
System.out.println(file);
}
}
14.2 File类中常见的成员方法
14.2.1 判断和获取
对于一个文件而言,其有哪些属性是可以被获取的?

文件的大小。

文件的名字。

文件的路径(相对路径,定义路径)。

最后的修改时间。

是否存在。

因此,Java就提供了可以用来查询上述文件属性的方法。

image-20240131171859227

对文件类型进行判断
对于一个文件的信息的判断主要就集中在:文件?,文件夹?,是否存在?

在这段代码中,有个很奇怪的bug。

我自己手动创建的文件/文件夹,认不出来,返回的都是false。

但是,我如果用mkdirs或者createNewFile方法去创建一下,虽然不会多出任何文件,但是就可以识别了。

import java.io.File;
import java.io.IOException;

public class FileTest {
public static void main(String[] args) throws IOException {
File file1 = new File(“a.txt”);
System.out.println(file1.isFile());
System.out.println(file1.isDirectory());
System.out.println(file1.exists());

    File file2 = new File("bbb");
    System.out.println(file2.isFile());
    System.out.println(file2.isDirectory());
    System.out.println(file2.exists());
}

}
获取文件的基本信息
文件的长度
使用length方法可以获得文件的长度,长度是以字节进行返回(Byte)。

返回的数值是一个long类型的整数。

如果对一个文件夹对象获取length的话,结果是0(无法获取)。

File file = new File(“a.txt”);
System.out.println(file.length());
文件的绝对路径
文件的绝对路径就是文件带上盘符的完整路径。

这个方法的缺点在于无法知道创建File的时候的所传递的参数的具体形式。

System.out.println(file.getAbsolutePath());
文件的定义路径
文件的定义路径就是指,文件在被通过File进行创建的时候所填入的参数是啥。

文件可以被用绝对路径进行创建,这个时候返回的定义路径就是绝对路径。

文件也可以被用i昂对路径进行创建,这个时候返回的路径就是相对路径。

File file2 = new File(“aaa.txt”);
System.out.println(file2.getPath()); // aaa.txt
File file3 = new File(file.getAbsolutePath());
System.out.println(file3.getPath()); // C:\Users\liudaoyu\IdeaProjects\JavaLearn\a.txt
文件的名字
文件的名字包含,文件的文件名以及文件的扩展名。

getName方法会将文件的文件名以及文件的扩展名作为一个整体进行返回。

getName方法还可以作用与一个文件夹上,这个时候返回的就是文件夹的名字。(带有扩展名则为文件,否则,为文件夹)

System.out.println(file.getName());
File file4 = new File(“bbb”);
System.out.println(file4.getName());
文件的最后修改时间
文件的最后修改时间就是将文件的最后修改时间用毫秒值进行返回。

可以用SimpleDateFormat方法对毫秒值进行加工后得到一个想要的时间格式。

long ms = file.lastModified();
// 按照默认的时间格式去创建一个SimpleDateFormat对象
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
System.out.println(simpleDateFormat.format(ms));
14.2.2 文件的创建和删除
createNewFile 创建一个新的空文件
还记得,File对象创建的时候,并不会去管,这个文件是否真的存在。

因此,我想要一个什么样的文件(格式,名字),都需要我事先用一个File对象去指定出来,再通过createNewFile方法去进行创建。

需要注意的是,这个方法会返回一个boolean值,表示创建是否成功,同时会抛出一个IO异常。

如果该路径已经存在?

createNewFile方法不会去覆盖这个文件,拒绝创建同名的已存在文件。该方法会返回false。

File file = new File(“a.txt”); // 已经存在
try {
System.out.println(file.createNewFile());
} catch (IOException ex) {
throw new RuntimeException(ex);
}
如果想要在一个不存在的文件夹中创建一个文件?

会抛出一个IO异常,表示这个不存在的文件夹无法找到。

也就是文件创建只能在一个已有的文件夹中进行创建。

file = new File(“ccc//a.txt”); // 不存在的文件中,创建文件
try {
System.out.println(file.createNewFile());
} catch (IOException e) {
e.printStackTrace();
}
如果想要创建一个不存在的文件夹?

其不会创建一个文件夹,只会创建一个没有后缀名的文件。

createNewFile方法只能创建文件。

file = new File(“ccc”);
try {
System.out.println(file.createNewFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
mkdir 创建单级文件夹
mkdir用来创建路径(创建一个文件夹)。

mkdir方法会返回一个boolean值来表示路径的创建是否成功。

文件夹的路径必须是唯一的。(否则会拒绝创建并返回false)。

File file = new File(“ccc”);
System.out.println(file.mkdir());
和createNewFile相同,其创建过程中的父级路径必须存在。(否则会拒绝创建)。

也就相当于的是,mkdir只能去创建一个单级的文件夹。

File file2 = new File(“ddd//ccc”); // 不存在路径中去创建一个新的路径
System.out.println(file2.mkdir());
mkdirs 创建多级文件夹
创建多级文件夹。(可以同时创建多个不存在的文件夹,嵌套的也可以)。

只要路径中存在不存在的文件夹,mkdirs都会依次去创建出来。

同时,mkdirs也可以创建单级的文件夹。(mkdir其实毫无用处,但是mkdirs的底层是用mkdir进行实现的)。

更推荐直接使用mkdirs方法。

System.out.println(file2.mkdirs());
delete 删除文件,空文件夹
delete方法有一个boolean类型的返回值,其用来表示文件的删除是否成功。

需要注意的是,delete方法会将文件直接删除,其不会通过回收站。 (小心使用delete方法删除文件)。

其次,delete方法的删除能力有限,其只能删除文件和空文件夹,其不能删除有内容的文件夹。

System.out.println(file.delete());
14.2.3 文件的获取并遍历
listRoots 列出文件系统中所有的盘符
listRoots可以获取到当前系统中所有的文件盘符。

一般在打算去遍历整个系统磁盘的时候会用到。

需要注意的是,该方法返回的都是File类型的变量。

File[] files = File.listRoots();
for (int i = 0; i < files.length; i++) {
System.out.println(files[i].getAbsoluteFile());
}
list 获取当前路径下的所有内容
list方法可以获取到当前文件路径 下的所有内容,但是获取到的只有内容的名字。

想要获得可以访问的File的对象还需要自己和工作路径进行一个拼接才可以

File file = new File(“.//”); // 以当前文件路径生成一个File对象
String[] names = file.list();
for (String name : names) {
System.out.println(name);
}
}
list(FilenameFliter filter) 按照过滤器获取当前路径下的内容
这个方法中,list方法的参数列表中,引入了一个过滤器对象:FilenameFilter。

FilenameFilter过滤器对象(实际上是一个函数接口),中有一个抽象方法,accept方法。

这个方法中,提供了两个参数,File dir 和 String name,前者表示父级路径,后者表示的是当前文件的名称。

accept方法的返回值是一个boolean值,返回true表示,这个对象被留在最后的返回值周,false则表示不留下。

String[] ans = file.list(new FilenameFilter() {
@Override
// dir 和 name会依次去表示file所指定的路径下的每个内容(文件和文件夹)
public boolean accept(File dir, String name) {
// 把父路径和子路径拼接在一个,形成一个可以访问的File对象
File newFile = new File(dir, name);
// 如果当前遍历到的这个内容是一个文件,且文件的扩展名是txt,则留下
if(newFile.isFile() && name.endsWith(“.txt”)) {
return true;
}
else {
return false;
}
}
});

for(String Ans : ans) {
System.out.println(Ans);
}
listFiles 获取当前路径下的所有内容
listFiles方法也是去获取当前路径下的所有内容,但是和list不同的是,listFiles方法返回的是File类型的数组。

需要注意的是,listFiles方法是获取不到需要权限进行访问的文件夹的。

如果是,需要权限进行访问的文件夹,就会返回一个空数组。

File file = new File(“.//”);
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
System.out.println(files[i].getAbsoluteFile());
}
listFiles(FilenameFliter filter) 按照过滤器获取当前路径下的内容
这里的FilenameFilter filter和list方法中的一致。

是一个函数式的接口,其内部有一个抽象方法accept,这个方法的参数列表中有两个元素,第一个是File类型所表示的父路径,第二个是String表示内容的名字。

File file = new File(“.//”);
File[] files = file.listFiles(new FilenameFilter() {
@Override
// 这里的dir 和 name会依次去表示当前文件路径下的所有内容
// dir表示当前内容的父级路径,name表示当前内容的名字(带上扩展名)
public boolean accept(File dir, String name) {
File testFile = new File(dir, name);
if(testFile.isFile() && name.endsWith(“.txt”)) {
return true;
}
return false;
}
});
for (int i = 0; i < files.length; i++) {
System.out.println(files[i].getAbsoluteFile());
}
listFiles(FileFilter filter)按照过滤器获取当前路径下的内容
FileFilter同样是一个过滤器对象。

其和FilenameFilter对象不同的地方在于,其内部抽象方法accept的参数列表是一个代表了完整路径的File对象。

File file = new File(“.//”);
File[] files = file.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(“.txt”);
}
});
for(File it : files) {
System.out.println(it.getAbsolutePath());
}
14.2.4 四个经典问题
在当前目录下的一个未知目录下创建一个txt文件
先创建不存在的父级目录,再创建文件即可。

File newFile = new File(“.//aaa”);
newFile.mkdirs();
newFile = new File(“.//aaa//a.txt”);
newFile.createNewFile();
判断当前目录下有没有以txt结尾的文件(不考虑子文件夹)
File newFile = new File(“.//”);
String[] files = newFile.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
File tempFile = new File(dir, name);
if(tempFile.isFile() && name.endsWith(“.txt”)) {
return true;
}
return false;
}
});
if(files.length == 0) {
System.out.println(“NO”);
}
else {
System.out.println("YES: " + files.length);
}
删除当前目录下所有的文件(考虑子文件夹)
凡是要考虑到文件夹内的内容,就可以用到以下的套路。

进入文件夹 (listFiles进入文件夹获取内容)

遍历数组(遍历第一步中获取到的内容)

判断(判断是否为文件)

判断(判断是否为目录)(如果是目录就可以开始递归)

删除一个多级文件夹一般分为两步:

删除文件夹内部的所有内容。

删除文件夹自己。

public static void deleteFiles(File file) {
// 先删除该文件夹下所有的子内容
// 进入文件夹
File[] subFiles = file.listFiles();
// 遍历数组
// subFiles有可能是空的(因为,listFiles不能访问有权限访问的文件夹)
// 因此最好在这里对subFiles做一个非空判断
if(subFiles.length == 0) return;
for(File it : subFiles) {
// 如果是文件的话,直接删除即可
// 判断是否为文件
if(it.isFile()) {
it.delete();
}
// 如果是目录的话,就递归调用函数来删除子文件夹内的内容
// 判断是否为目录
else if(it.isDirectory()) {
deleteFiles(it);
it.delete();
}
}
// 最后连同这个文件夹本身进行删除
file.delete();
}

统计一个文件夹的总大小(考虑子文件夹)
如果是文件的话,就直接累加其大小。

如果是目录的话,就进入文件夹的内部,去获取里面内容的大小再进行累加。

public static long getSize(File file) {
// 当前文件夹内部的内容的总大小
long size = 0;
File[] files = file.listFiles();
if(files.length == 0) return 0;
for(File it : files) {
// 如果it是文件的话
if(it.isFile()) {
size += it.length();
}
// 如果it是文件夹的话
else if(it.isDirectory()) {
size += getSize(it);
}
}
return size;
}
统计当前目录下不同类型的文件的数量(包含所有的嵌套文件夹)
使用Map来表示映射关系,同时便于对于没有记录的类型的计数进行添加。

使用递归的方法来遍历子文件夹中的内容。

public static Map<String, Integer> getCount(File file) {
// String 表示文件的类型 Integer 表示文件出现的次数
// a.txt a.a.txt a
// 没有后缀名的文件不需要统计
HashMap<String, Integer> map = new HashMap<>();
File[] files = file.listFiles();
for(File it : files) {
if(it.isFile()) {
String name = it.getName();
// 正则表达式
String[] arr = name.split(“//.”);
// 没有后缀 不判断
if(arr.length == 1) {

        }
        else if(arr.length >= 2) {
            // 找最后一个元素就是后缀
            String endName = arr[arr.length - 1];
            if(map.containsKey(endName)) {
                Integer count = map.get(endName);
                count ++;
                map.put(endName, count);
            }
            else {
                map.put(endName, 1);
            }
        }
    }
    else {
        // 子文件夹里面获取到的Map
        Map<String, Integer> sonMap = getCount(it);
        // 需要去合并这两个Map
        Set<Map.Entry<String, Integer>> entries = sonMap.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            String key = entry.getKey();
            if(map.containsKey(key)) {
                Integer value = map.get(key);
                value += entry.getValue();
                // 更新原本Map中的value
                map.put(key, value);
            }
            else {
                // 是一个新的entry,可以直接put进去 
                map.put(key, entry.getValue());
            }
        }
    }
}
return map;

}
14.3 IO流
14.3.1 IO流的概述
数据都是保存在内存中,不能永久存储。程序停止,数据丢失。

所以需要去添加一个存档,将数据存储在硬盘当中进行读取恢复。

需要知道两个要点:

文件的位置:File

如何进行读写:IO流

File 就是表示系统中的文件或者文件夹的路径。

通过File可以对文件进行创建,删除,查找等功能,但是这些操作都是针对文件的整体。

IO流则可以将数据写入到文件中(output),也可以将文件中的数据读取出来(input)。

如何去确定读写的方向呢?

Java中,读写的参考物是Java程序,而不是文件。

程序向文件写入数据(output输出数据),读入数据(input读入数据)

14.3.2 IO流的分类
IO流的作用就是去存储和读取数据。

按照IO的方向进行分类的话,可以分为:

IO流
输入流
输出流
其中,输入流表示程序读入文件中的数据。输出流表示程序向文件写出数据。

按照IO流的功能进行分类的话,可以分为:

IO流
字节流
字符流
其中,字节流可以操作所有文件,但是字符流只能操作纯文本文件。

纯文本文件就是,可以通过电脑自带的记事本打开,且里面的内容不会出现乱码的文件。(举一些例子就是,txt文件是纯文本文件,但是docx文件,excel文件利用记事本打开后都是乱码,不是纯文本文件)

对于非纯文本文件,只能使用字节流来进行操作。

14.3.3 IO流的体系结构
字节流和字符流下都有属于自己的输入和输出流。

字节流
inputStream
outputStream
字符流
Reader
Writer
和容器类非常相似的是,上图中所提到的这些流都是抽象类,都有自己的实现子类。

14.3.4 字节输出流
FileOutputStream流是字节流下outputStream输入流的一个子类。

这个子类的命名中,File表示的是操作的对象,即本地文件,outputStream表示的是继承关系,也就是功能是输入流。

使用FileOutputStream的基本过程为:

创建对象(创建一个FileOutputStream对象,使其和一个本地文件构建通道)

写入数据(利用write方法,向这个文件中写入数据)

释放资源(关闭输入流和文件之间的通道,解除文件占用)

以下是一个简单的例子

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileOutputStreamTest {
public static void main(String[] args) throws IOException {
// 1. 创建对象
// 需要注意的是,FileOutputStream创建的时候,会抛出一个文件找不到的编译异常
String path = “.//ddd//aaa.txt”;
FileOutputStream fos = new FileOutputStream(path);
// 2. 写入数据
// 写入的数据可以是一个整数,但是实际写入文件的是一个Ascii码值对于的字符
fos.write(98);
//3. 释放资源
fos.close();
}
}
FOS的基本原理
FOS对象在创建的时候,会通过其传入的路径参数来和本地的一个文件之间构建一个数据的读写通道。

FOS对象通过write方法,来向本地文件中传输数据。(通道一旦构建,数据就可以像流水一样输入到文件中)

在数据传输完成后,可以通过close方法来结束程序对于文件的占用。

创建对象的细节
路径的传递
FOS创建的时候,其表示路径的参数可以通过String类型传递,也可以通过File类型传递。

实际上,如果是采用String类型传递的路径,在底层会重新new一个对应的File对象出出来。

String path = “.//ddd//aaa.txt”;
File path2 = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(path);
FileOutputStream fos2 = new FileOutputStream(path2);
路径是否存在
创建FOS对象的时候,其路径所指向的对象有两种情况。

如果路径指向的文件已经存在,那么FOS会清空这个文件,并重新开始写入。

// 1. 创建对象
// 需要注意的是,FileOutputStream创建的时候,会抛出一个文件找不到的编译异常
String path = “.//ddd//aaa.txt”;
FileOutputStream fos = new FileOutputStream(path);
// 2. 写入数据
// 写入的数据可以是一个整数,但是实际写入文件的是一个Ascii码值对于的字符
// FOS会清空原本的aaa文件,并写入一个字符
fos.write(98);
如果路径指向的文件不存在,那么FOS会创建一个这个文件,但是要求,这个文件的父级目录必须存在,否则会报错。

如果上文中的ddd路径不存在的话,就会报错,报错的类型是一个编译错误,无法找到指定的路径。

追加模式标志
FOS创建的时候,还有一个重载的构造函数。

在这个构造函数中,提供了一个append标志。

public FileOutputStream(String name, boolean append)
throws FileNotFoundException
{
this(name != null ? new File(name) : null, append);
}
如果这个append标志为true,表示打开续写模式,否则关闭续写模式。

如果append模式为true,续写模式打开,则表示打开的这个文件的内容不会被情况,新的内容会被续写到文件的末尾。

写入数据的细节
三种写入数据的方式
需要事先说明的是,虽然看着都是写入的整数,但是实际上写到文件内部的,都是Ascii码对应的字符。

5fed027bbea3f96e1fce2135964cc6a

写入整数

使用wtire方法,一次可以写入一个整数所对应的数据。

写入到文件中的实际字符是这个整数对应ASCII码的字符。

需要注意的是,只要FOS还没有被关系,多次调用write方法就可以实现对文件的连续写入。

public class FileOutputStreamTest {
public static void main(String[] args) throws IOException {
File file = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(file);
fos.write(97);
fos.write(98);
fos.write(99);
fos.close();
}
}
写入字节数组

fos对象也可以使用write方法来写入一个完整的Byte数组。

但是要注意,如果想要写入一个数组,这个数组的格式是一个Byte类型的数组.

public class FileOutputStreamTest {
public static void main(String[] args) throws IOException {
File file = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(file);
byte[] bytes = new byte[]{97, 98, 99};
fos.write(bytes);
fos.close();
}
}
写入字节数组的一部分

也可以只写入这个Byte类型数组的一部分.

这个重构的方法有三个参数,第一个参数是等待被写入的Byte数组,第二个参数off是开始写入的下标,第三个参数len是写入的长度。

File file = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(file);
byte[] bytes = new byte[]{97, 98, 99};
fos.write(bytes, 1, 1);
fos.close();
Tips:

Ascii码的对应关系非常难以背诵,因此当要连续去输入多个字符的时候就会很麻烦。

String类中提供了一个方法getBytes,其可以获得现在对应String的Bytes数组。

File file = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(file);
String str = “Hello World”;
fos.write(str.getBytes());
换行
现在我想写入的是 aaa \n bbb。

如果我连续去用write去进行写入,写入到文件中的结果是 aaabbb 是连续的续写。

因此,我需要在需要换行的地方额外输入一个换行符号。

在不同的操作系统中,换行符号的表示有所不同:

在win操作系统中,换行符号的表示为: \r + \n 其中 \r 表示将光标移动到下一行(回车),\n 表示实际进行换行操作。

在Linux操作系统中,换行符号为 \n

在Mac操作系统中,换行符号为 \r

但是在Java中,其对Win中的换行符进行了简化,无论是 r 还是 n 都可以成功触发换行操作。

File file = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(file);
String str1 = “Hello”;
String str2 = “World”;
fos.write(str1.getBytes());
fos.write(“\n”.getBytes());
fos.write(str2.getBytes());
续写
在创建一个FileOutputStream的时候,有一个重构的构造方法。

其中额外提供了一个Boolean类型的标志append。

public FileOutputStream(File file, boolean append)
如果append标志为true,表示开启续写模式,这个模式下,FOS打开的文件其中的内容不会被清空。

write方法写入的数据会跟在所打开文件的内容下面。

要额外注意的是,新写入的数据不会默认在写入的地方换行。

如果append标志为false,表示关闭续写模式,这个模式下FOS打开的文件其内容就会被清空。

也就是覆写模式。

File file = new File(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(file,true);
String str1 = “Hello”;
String str2 = “World”;
fos.write(str1.getBytes());
fos.write(“\n”.getBytes());
fos.write(str2.getBytes());
关闭FOS的细节
再每次使用完FOS对象后,都需要去关闭FOS对象。

否则,其他的进程如果想要去访问这个文件,就会显示文件被占用。

使用close方法来关闭FOS对象。

fos.close();
同时,只有在FOS对象被close后,外界才能看见被写入文件的数据。

14.3.5 字节输入流
FileInputStream是InputStream下的一个子类。

FileInputStream用来操作本地文件字节的输入流,用来读人本地文件里面的数据。

使用FileInputStream的三个基本的步骤(和FOS的基本过程一样)

创建对象(和本地文件构建通道)

读取数据(利用read方法按照字节或者字节数组进行数据的读取)

释放资源(解除文件占用)

创建FIS对象的细节
和FOS对象的创建不同,如果创建FIS对象的时候,传入的文件地址所指向的文件不存在,那么FIS会报错。

也就是说,FIS不会去创建文件。

FIS的目的在于读取文件中的数据,如果去创建一个空的文件,其中也没有数据来供FIS读取,是没有意义的。

读取数据的细节
FIS读取数据是每次读入一个字节,且实际读入的结果是字符对应的Ascii编码。

当读取到文件的末尾的时候,会返回-1,可以用这个标志来作为文件的结尾标志。

如果文件内的内容本身是一个带符号数,比如-1,FIS在读取的时候会当作两个独立的字符,- 和 1 进行读取。

FIS提供了两种读取数据的方法
// 第一种方法 空参的read方法
// 这个方法每次会读入一个字节的数据,这个数据就是该字符对应Ascii编码的十进制数值
// 这个十进制的数值会作为一个int类型的数据返回
// 当读入到文件末尾的时候,会返回-1
int ch = fis.read();

// 第二种方法 带参数的read方法
// 这个方法每次都会尽量去将传入的byte数组占满,byte数组中的值还是字符所对应的Ascii十进制数值
// 每次read方法会将成功读入到的字节的数量作为int类型的值进行返回,读取到的字节存入传入的数组中
// 但读入到文件末尾的时候,会返回-1(当读入数据的大小小于byte数组的长度的时候,也可以认为读到了文件的末尾)
int len = 2;
byte[] bytes = new byte[len];
int size = fis.read(bytes);
使用FIS进行循环读取
如果使用FIS的空参方法read进行读取数据,其每次只会读入一个字节。

如果先要去读入文件中的多个数据的话,就需要将read方法嵌入到一个循环中,通过循环去读取多个字节。

同时可以利用文件末尾返回-1的特点,来作为循环跳出的条件。

需要注意的是,读取数据的时候,相当于有一个文件的读写指针指向了当前应该读入的内容。

无论是使用有参还是无参的read方法,都会使得这个指针发生移动,如果想要归位这个指针的位置,可以重新打开这个文件的FIS对象。

fis.close();
fis = new FileInputStream(“.//ddd//aaa.txt”);
while((ch = fis.read()) != -1) {
System.out.print((char)ch);
}
文件拷贝(流关闭顺序)
现在有两个文件,aaa和bbb的文本文件。

想要将aaa中的内容拷贝到文件bbb中。

文件拷贝的核心思想就在于,读一个写一个。

可以同时创建一个FIS对象读取aaa的数据,一个FOS对象向bbb写出数据。

public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(“.//ddd//bbb.txt”);

int ch = 0;
while((ch = fis.read()) !=  -1) {
    fos.write(ch);
}

fos.close();;
fis.close();

}
这里需要额外的主义的是,当打开了多个流,流的关闭顺序应该是:

先打开的后关闭,也就是,流的关系顺序和打开的顺序相反。

如果选择使用无参的read方法进行读取,也就是一次只读入一个字节的话,可以发现,程序运行的时间很慢很慢,其主要的原因就在于,每次只读入一个字节效率非常低下,如果想要效率高一点,可以选择使用带参的read方法进行读取。

使用带参的read方法进行读取
使用 read(byte[] bytes) 进行数据的读入。

这个方法需要注意的是,每次的读取都会去尽量占满bytes数组,同时bytes数组也会在每次的读取过程中被重新覆盖。

有很多时候,在最后一个读入的过程中,并没有那么多字节来填满整个bytes数组。

bytes数组中就只会从头开始被覆盖一部分,虽然bytes数组还是每个下标都有值,但是只有部分的数据是有效的。

因此可以结合read方法的返回值(实际读入到了多少个字节)来正确获取到有效的读取到的数据。

String方法提供了一个重载的构造方法,可以根据off和len来只对传入的部分字节创建一个String对象。

FileOutputStream对象的write方法同样提供了一个重载的write方法有类似的效果。

public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(“.//ddd//aaa.txt”);
FileOutputStream fos = new FileOutputStream(“.//ddd//bbb.txt”);

// 每次尽量读入三个字节
byte[] bytes = new byte[3];
int len = 0;
while((len = fis.read(bytes)) != -1) {
    // 只去写入有效长度的字节
    fos.write(bytes, 0, len);
    // 只用有效长度的字节来创建一个新的String对象
    System.out.print(new String(bytes, 0, len));
}

fos.close();
fis.close();

}
14.3.6 IO流的异常捕获处理
手写异常处理流程
首先参考一下以下的代码,是否合适。

将所有可能抛出异常的代码都放到try块中。

public static void main(String[] args){
try {
FileInputStream fis = new FileInputStream(“.//ddd//aaa.txt”);
System.out.print(fis.read());
fis.close();
}
catch(IOException e) {
e.printStackTrace();
}
}
在字节流对象的创建,数据读取写入,文件流对象的关闭这三个过程都有可能产生异常。

如果,在关闭流对象之前的环节出现了报错的情况,程序的执行就会转向catch块中,因此close方法就不会调用,文件就无法解除占用。

很明显,这样是有很大的缺陷的。

因此,我们会用到try-catch的完整形式,finally块。

finally块的目的在于,无论try中有没有出现异常,都会去运行finally块中的内容。

因此,close方法非常适合放到finally块中。

所以我们把代码修改成这个样子。

public static void main(String[] args){
try {
FileInputStream fis = new FileInputStream(“.//ddd//aaa.txt”);
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
finally {
fis.close();
}
}
但是,这个时候,catch块中的fis对象报错了,说找不到这个对象。

这是因此,在Java中,每个 { } 之间都是一个块作用域,在块作用域内声明的变量是无法在作用域外被使用的。

因此try中定义的fis对象出了try就无法使用了,因此我们需要将fis的定义修改到try-catch外面。

public static void main(String[] args){
FileInputStream fis;
try {
fis = new FileInputStream(“.//ddd//aaa.txt”);
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
finally {
fis.close();
}
}
但是,catch块中的fis对象还是报错,说fis对象可能没有被初始化。

因为,fis的初始化是在try块内的,如果初始化这个过程中报错了,fis对象相当于就是创建了但是没有被初始化。

用这个没有初始化的fis对象去调用close方法,很明显是错误的。

因此我们需要在try-catch块外,定义fis对象的时候,进行一个无所谓的初始化,也就是将其初始化为null。

public static void main(String[] args){
FileInputStream fis = null;
try {
fis = new FileInputStream(“.//ddd//aaa.txt”);
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
finally {
fis.close();
}
}
哈哈,这个时候还没有完。

可以看到,close方法本身的异常,我们在cath块中还没有处理。

因此还需要在catch中,嵌套一个try-cath来处理close方法抛出的异常。

public static void main(String[] args){
FileInputStream fis = null;
try {
fis = new FileInputStream(“.//ddd//aaa.txt”);
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
finally{
try {
fis.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
完了吗?哈哈,还是没有。

需要额外注意的是,fis仍然有可能是一个空指针。

如果是一个空指针的话,就没有必要再去调用close方法了,调用了也会报错。

因此,最后添加一个非空判断即可。

public static void main(String[] args){
FileInputStream fis = null;
try {
fis = new FileInputStream(“.//ddd//aaa.txt”);
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
finally {
if(fis != null) {
try {
fis.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}
以上就是一个完整的IO流异常的处理过程。

可见得,实在是,太麻烦了,因此,在JKD7和JDK9都有对异常处理流程的简化。

JDK7 try-catch的简化
在JDK7中,引入了一个接口,叫做,AutoCloseable接口。

实现了这个接口的类对象可以在特定的情况下,被自动释放掉资源。(并非全部的情况下都可以自动释放掉资源)

举个例子:FileInputStream继承自InputStream类,InputStream实现了CloseAble接口,CloseAble接口继承自AutoCloseAble接口。

在JKD7中,将需要自动释放资源的对象的定义,统一移动到try的语句后面,就可以激活对象的自动释放功能。

public static void main(String[] args){
try(FileInputStream fis = new FileInputStream(“.//ddd//aaa.txt”);
FileInputStream fis2 = new FileInputStream(“.//ddd//bbb.txt”)😉 {
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
}
JDK9 try-catch的简化
在JDK9中,沿用了JDK7中的设计。

但是为了增强代码的可读性,JDK9允许在外面去创建流对象,只需要在try后面将需要自动释放资源的对象的名字给出就可以了。

但是,这样就只是去实现了自动释放,对于try-catch外创建流对象的所抛出的异常需要向上级调用抛出处理。

public static void main(String[] args) throws FileNotFoundException {
FileInputStream fis = new FileInputStream(“.//ddd//aaa.txt”);
FileInputStream fis2 = new FileInputStream(“.//ddd//bbb.txt”);
try(fis; fis2) {
System.out.print(fis.read());
}
catch(IOException e) {
e.printStackTrace();
}
}
14.3.7 字符集详解
在计算机中,所有的数据都是按照二进制进行存储的。

计算中中,最小的二进制存储的单位是字节,一个字节 = 8个比特。

Ascii英文字符集
Ascii中,包含了128个常见的字符,和所有的英文字母。

只有128个元素,因此使用一个字节,8个比特(最多可以表示255个字符),就可以实现Ascii的表示。

但是Ascii字符集有一个缺点,就是,只有英文。没有中文和一些特殊符号。

Ascii字符集的存储规则
现在要存储一个英文字母a。

首先通过查表,知道a在Ascii中对应的十进制数值为97。

将97转换为二进制有有,110 0001,考虑到计算机的存储的最小单位是字节,因此需要将110 0001编码为合适的存储形式。

Ascii的编码方法就是在高位去补0,直到补齐8位。

因此,实际存储的数值为 0110 0001

中文字符集发展
为了表示中文。

China在1980年发布了GB2312-80标准中文字符集。这个字符集中只有简体中文,没有繁体。

台湾为了表示自己的繁体中文,发布了BIG5字符集。

为了同时兼顾简体和繁体中午,China发布了GBK字符集,其中包含了简体,繁体,所有的日韩文字中出现的中文。

国际上,为了表示尽能多的国家的语言,推出了Unicode字符集,这个字符集中,基本上收纳了所有国家的所有文字,且,现在还在更新。

GBK和unicode字符集,都兼容Ascii字符集。(因此,英文字母始终都只需要一个字符进行表示,且,英文字母字节的最高位永远都是0)

GBK字符集的存储规则
GBK字符集有两个存储的规定:

一个中文用两个字节进行表示(中文实在太多了,一个字节表示不完)。

中文的最高字节最高比特位一定是1(为了和英文的编码区分开)。因此,中文的高字节转换为10进制后一定是一个负数,第二个字节不一定。

举个例子。

我现在想要存储汉字:汉。

通过查表,知道对应的GBK编码的十进制为47802 。

47802的二进制表示为:10111010 10111010 。(GBK对中文的编码规则就是不编码,对英文的编码规则是高位补0)

实际存储的结果就是:10111010 10111010 。

Unicode字符集的存储规则
Unicode字符集是国际统一码组织推出的一种面向国际的字符集。

Unicode字符集的编码规则经过了很多次的修订:

UTF-16:2到4个字节存储。

UTF-32:固定使用4个字节。(过于占用空间)

UTF-8:变长存储,使用1到4个字节存储。(现在正在使用的编码规则)

UTF-8规则规定了不同语言的存储字节数量。

对于英文,采用单个字节进行存储。

对于中文,采用三个字节进行存储。

UTF-8规则也定义了不同数量的字节数据的编码方法:

1个字节:在最高位补充0,直到满足一个字节。

2个字节:110XXXXX 10XXXXXX

3个字节:1110XXXX 10XXXXXX 10XXXXXX

4个字节:11110XXX 10XXXXXX 10XXXXXX 10XXXXXX

举个例子。

如果想要存储汉字:汉。

首先,经过查表得知汉的十进制为:27721 。

其对应的二进制为:01101100 01001001(只有两个字节)

记住,汉字在Unicode中,存储的字节数量是3个字节,因此,需要通过3字节的编码方式来满足规则。

最终所存储到文件中的数据为:11100110 10110001 10001001

Tips:经常可能弄混的一点是,UTF-8并不是一个字符集,只是Unicode字符集中采用的一种编码方式。

为什么会有乱码?
导致文本文件乱码的原因有两个:

在读取数据的时候,没有将汉字的两个字节完全读入。

在解码的时候,采用的解码方法和编码方法并不一致。

以下是一个读取中文文本的例子。

public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(“.//src//test.txt”);
int ch = 0;
while((ch = fis.read()) != -1) {
System.out.print((char)ch);
}
fis.close();
}
尝试用read方法去读取中文就会发生乱码。

这是要因为,空参的read方法只会去读取一个字节的数据,对于一个中文字符,是没有完全读入其数据的。

即使是有参的read方法,也没有办法很方便地去读取英文和中文字符混杂的情况。

有没有一种方法,可以自己识别现在正在读取的字符是英文还是中文,然后自动去读取对应数量的字节呢?

这里,就需要提到前面的中文和英文字符,在二进制上面的特征了。

英文字符的最高比特位一定是0 。 中文字符的最高比特位一定是1 。

Java中,提供了字符输入输出流来解决这个问题。

为了避免出现乱码,建议:

不要使用字节输入输出流来处理中文。

编码和解码的规则一致。

14.3.8 Java中的字符编码和解码
在Java中,也提供了一些方法来手动进行编码和解码的过程。

编码
String方法中,提供了一个方法,getBytes(),这个方法有两个重载的形式。

对于无参的形式,getBytes方法默认是按照UTF-8的编码形式。(IDEA是UTF-9,Eclipse是GBK)

public static void main(String[] args) throws IOException {
String str = new String(“你好”);
// 1. 普通的getBytes方法,可以按照默认的UTF-8编码规则获得
// 对应字符的二进制表示形式,然后将这个二进制形式转换为十
// 进制,最后按照byte类型进行返回
byte[] bytes = str.getBytes();
System.out.print(Arrays.toString(bytes));
// 2. 带参数的getBytes方法,可以将期望的编码方法用字符串的形式
// 输入到方法中,方法会根据对应的编码规则获得对应的二进制表示
// 形式,最后转换为十进制按照byte类型进行返回
bytes = str.getBytes(“GBK”);
System.out.print(Arrays.toString(bytes));
bytes = str.getBytes(“UTF-8”);
System.out.print(Arrays.toString(bytes));
}
解码
String方法的构造方法实际上就是解码的方法。

String方法提供了很多构造方法的重载,其可以利用一个Byte数组来创建一个String对象,也可以去指定解码的方法。

public static void main(String[] args) throws IOException {
String str = new String(“你好”);
String newStr1 = new String(str.getBytes()); // 解码编码的方法一致,不会乱码。
System.out.println(newStr1);
String newStr2 = new String(str.getBytes(), “GBK”); // 解码编码的方法不一致,会乱码。
System.out.print(newStr2);
}
14.3.9 字符输入流
字符输入流的底层还是字节输入流。(但是引入了字符集的概念)

当输入流遇到了中文,则会按照字符集定义的字节数量去读取字节。

字符输入流FileReader是继承自字符流下Reader的一个子类。(是一个实现类)

字符流
Reader
Writer
FileReader本地文件输入流
FileWriter本地文件输出流
在体系结构上,Reader和Writer都是抽象类,不可以被直接使用。

FileReader
FileReader的作用在于操作本地文件从本地文件中读取数据。

FileReader的基本使用流程
创建对象(和本地的文件建立一个数据通道,用来输入字符数据)

读取数据(FileReader提供了两个重载的Read方法来读取字符数据)

read() 一次读入一个字符

read(char[] buffer) 一次尽可能占满整个buffer数组

关闭FileReader对象(解除文件占用)

read方法
底层原理
read方法的底层还是一次只读取一个字节,但是加入了字符集的概念,其在遇到一个中文的时候,会根据字符集定义的字节数量去读入对应数量的字节。

在读取后,read方法会去解码读取到的字节文件,将其转换为十进制(字符集对应的编号),最后返回的结果就是这个十进制的数字。

举个例子。

英文:0110 0001,直接进行解码得到十进制97,实际返回的值就是97 。

中文:1110010 10110001 10001001

首先,read方法会一次这三个字节,然后解码得到十进制为27721,实际返回的值就是27721 。

如果想要输出这个读取到的中文,通过一个char类型的强制类型转换即可。

空参的read方法
空参的read方法,一次只会读入一个字符。

将这个字符的十进制进行返回,也就是返回值就是实际读取到的字符的十进制编码。

当read方法读取到文件的末尾的时候,就会返回-1,可以用这个作为循环读取的结束标志。

public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader(“.//src//test.txt”);
int ch = 0;
while((ch = fileReader.read()) != -1) {
System.out.print((char)ch);
}
}
带参的read方法
read方法可以传入一个char类型的数组。(字节流传入的参数的是一个byte类型的数组,注意区分)

带参的read方法会在每次读入数据的时候尽可能去将这个数组装满。

方法会将读入到的数据存储在char类型的数组中,将成功读取到的字节数量作为int类型进行返回,当读取到文件末尾,同样会返回-1 。

public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader(“.//src//test.txt”);
int len = 0;
// 指定一个字符缓冲区,一次读取3个字符
char[] chars = new char[3];
while((len = fileReader.read(chars)) != -1) {
// 只选择有效读入的字符来创建字符串
String str = new String(chars, 0, len);
System.out.print(str);
}
}
带参数的read方法的底层稍微有一点区别。

带参数的read方法,实际上将读取数据,解码数据,强制类型转换三个步骤结合在了一起。

装入char类型数组中的内容,实际上就是经过强制类型转换后的结果。

因此char类型数组里面,每个下标处都存储了一个中文字符。

14.3.10 字符输出流
FileWriter的目的就是向本地的文件输出字符数据。(一般用来输出中文字符比较方便)

FileWriter的使用流程
创建FileWriter
和FileReader一样,FileWriter的创建有两个重构的的构造方法。

传入方法的参数可以是String类型的文件路径,也可以是File类型所指向的一个文件对象。

当所传入的文件路径所指向的文件不存在的时候,FileWriter会创建一个新的对象。

如果文件存在的话,FileWriter会清空这个文件,并从头开始覆盖这个文件。(如果不想文件被覆盖重写的话,可以打开文件的续写标志 append = true)

d55c345663ebb17fa2d1ce49f75a82b

写文件
78c4d89c5647344127589543bc95c8d

FileWriter类中提供了write方法写出字符数据,write方法有三种不同类型的重载来对不同类型的数据进行写出。

write方法可以直接写出一个十进制,而实际写出到文件的字符数据是经过查表后对应的中文字符。

public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter(“.//a.txt”);
fw.write(27712);
fw.close();
}
write方法可以直接写出一个String类型的字符串(也可以只写出这个字符串的一部分)。

public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter(“.//a.txt”);
// 写出一个完整的字符串
String str = new String(“黑马程序员”);
fw.write(str);
// 写出一个字符串的一部分
fw.write(str,1,2);
fw.close();
}
write方法可以写出一个字符数组(也可以只写出这个字符数组的一部分)。

public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter(“.//a.txt”);
// 写出一个完整的字符数组
char[] chars = new char[]{‘黑’,‘马’,‘程’,‘序’,‘员’};
fw.write(chars);
// 写出一个字符数组的一部分
fw.write(chars, 1, 2);
fw.close();
}
Tips:

如果使用FileOutputStream对象来对文件写出同样内容的中文字符的时候,文件中会出现乱码。

这是因为FOS对象一次只能写出一个字节,但是IDEA中,默认采用的是UTF-8的编码方式,其中的中文字符占用了3个字节,FOS对象只能写入一个中文字符的一部分,因此,会在实际的文本文件中出现乱码。

FileWriter对象则加入了字符集的概念,其会根据实际写入的内容自适应地去决定每次写过的字节的数量。

关闭流
在使用完FileWriter类型的对象后,同样需要使用close方法解除对文件的占用。

fw.close();
字符输出流的底层原理
字符输入流 FileReader的原理
public static void main(String[] args) throws IOException {
// 在创建一个FileReader对象的同时,其会在内存中,创建一个长度为8192的byte类型的数组
// 这个数组就是字符输入流的缓冲区
FileReader fr = new FileReader(“.//a.txt”);

// 第一次读取,FileReader对象首先回去查看其缓冲区内是否还有待读取的数据,如果缓冲区为空,
// 就先从文件中读取数据,并尽可能是读取到的数据占满整个buffer,此后优先读取buffer中的数
// 据(避免频繁地访问硬盘,优化读取的速度),如果buffer中的数据全部都被读取了,就会去访问
// 文件,并再次尝试读取8192个字符,并从头开始依次去覆盖整个字符数组。如果文件读到末尾了,
// FileReader对象就会返回-1,作为文件结束的标志。


// 在每次读取的过程中,FileReader对象会去判断,当前读取的内容是什么字符,如果是英文字符,
// 那么就只读取一个字节,如果是中文字符,那么就去读取三个字节。FileReader会将读取到的二
// 进制数据强制类型转换为十进制后,将这个int类型的数据进行返回。
int ch = fr.read();
fr.close();

}
总结就是,底层有一个8192长度的byte类型的数组作为缓冲区,先把数据读取到缓冲区内,再从缓冲区内读取数据到程序内部,缓冲区读完了就重新去文件中读取数据。

如果先对一个文件用FileReader打开了,read一次,再用FileWriter打开这个文件,会出现什么情况?

使用FileReader打开文件后,其在底层创建一个8192长度的缓冲区,并且从文件中读取数据来填满这个缓冲区。

接着,FileWriter打开了这个文件,文件的内容在硬盘中被清空,但是FileReader中缓冲区的内容还存在。

因此,依旧可以使用FileReader继续去读取数据,但是仅限于缓冲区里面的内容。

这里还有一个小点,我现在还可以用 -1 去判断文件读取的末尾吗?

还是可以的,当缓冲区内的数据读取完了(缓冲区里面是没有 -1 的现在),FileReader会去文件中读取数据,这个时候文件是个空文件,会返回 -1 给FileReader,结束读取的过程。

字符输出流 FileWriter的原理
public static void main(String[] args) throws IOException {
// 一个FileWriter对象被构造后,其会在内存中创建一个长度为8192的byte类型的数组
// 作为缓冲区,同样的,为了避免频繁的访问硬盘操作,FileWriter对象也提供了相应的
// 的解决方法。
FileWriter fw = new FileWriter(“.//a.txt”);

// FileWriter对象首先会对即将输入的数据进行编码,默认是按照UTF-8的编码规则进行
// 编码操作,如果需要写入的是一个中文字符,那么,就会将其编码为3个字节的二进制数据
// 将编码后的数据存入缓冲区中,只有满足一定的条件,才会触发向文件的写出操作.
// 1. 当缓冲区满了,就会将缓冲区中的数据一次性写入文件中,并且从头开始依次去覆盖
//    数组中的内容。
// 2. 我们可以手动去调用FileWriter中提供的一个方法,flush方法,其会主动刷新缓
//    冲区,将缓冲区中的有效内容一次性写入文件。
// 3. 当FileWriter对象被释放资源的时候,其缓冲区内的数据也会被文件。

fw.write(new String("黑马程序员"));
// 手动调用flush操作进行写出(可以发现文件变大了)
fw.flush();
fw.close();

}
使用flush操作有什么好处?

使用flush操作后,只是缓冲区中的数据被刷新了,FileWriter这个对象本身是还没有被释放的。

因此FileWriter对象和文件之间的数据通路依旧存在,还是可以继续向向文件中写出数据的。

同时,flush操作,只会去写出缓冲区中,还没有被写出过的有效字符。

14.3.11 字节流和字符流的使用场景
字节流,可以实现对任意类型的文件的拷贝操作。

字符流,可以读取纯文本文件的内容,也可以向纯文本文件中写出数据。

拷贝文件夹中的所有文件(考虑子文件夹)
拷贝文件,最好还是按照每个字节去依次拷贝。

也就是,最好还是使用字节流 FileOuputStream 流对象进行数据的读写。

但是可以使用数组的方式进行拷贝。(速度会快一点)

public class FileCopyTest {
public static void main(String[] args) throws IOException {
fileCopy(“.//ddd”, “.//bbb”);
}

public static void fileCopy(String src, String dst) throws IOException {
    File dstFile = new File(dst);
    if(dstFile.exists() == false) dstFile.mkdirs();

    File file = new File(src);
    // 获取文件夹内的所有内容
    File[] files = file.listFiles();

    for (File file1 : files) {
        // 如果是文件就直接按字节进行拷贝
        if(file1.isFile()) {
            FileInputStream FIS = new FileInputStream(file1);
            FileOutputStream FOS = new FileOutputStream(dst + "//" + file1.getName());
            int ch;
            while((ch = FIS.read()) != -1) {
                FOS.write(ch);
            }
            FOS.close();;
            FIS.close();
        }
        // 如果是文件夹,则递归进入文件夹内部进行拷贝操作
        else if(file1.isDirectory()) {
            File dir = new File(dst + "//" +  file1.getName());
            dir.mkdirs();
            fileCopy(file1.getAbsolutePath(), dir.getAbsolutePath());
        }
    }
}

}
14.3.12 缓冲流(BIO)
缓冲流的体系结构
前面学过的字节输入流,字节输出流,字符输入流,字符输出流本质上都是基础流。

现在介绍的缓冲流属于高级流的一种。

对于缓冲流而言,也有类似的体系结构。

缓冲流
字节缓冲流
字符缓冲流
BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
BIS,BOS,BR,BW都是具体的实现类,也是最常用的实现类。

对于字节缓冲流而言,其在底层提供了一个缓冲区来加速读写的速度。

对于字符缓冲流而言,其本身在底层就有一个缓冲区,因此读取速度的提升不明显,但是其提供了两个特有的方法非常重要。

字节缓冲流
字节缓冲区的底层原理
BIO实际上就是一个包装过后的字节流,实际上与文件关联的还是字节流对象。

对于字节缓冲流而言,其在底层提供了一个长度为8192个字节的缓冲区。(字节流本身是没有缓冲区的)

读写的过程中,先利用基本流将数据读入(写出)到缓冲区中。再通过基本流中提供的读写单元(一个字节,或者一个字节数组),将输入缓冲区中的数据转移到输出缓冲区中(输入缓冲区和输出缓冲区是两个独立的缓冲区)。

0ff054bb8b726ad0b1bfa24cf30e755

至于为什么字节缓冲区可以帮助对字节流的读写进行加速?

首先是因为其提供了一个8KB的缓冲区。

将数据转移的场所从之前的硬盘到内存再到硬盘转换为了全部在内存中进行转移。内存的运算速度更快。

字节缓冲流的使用
创建字节缓冲流对象
e23ae49d19c23aa9e6a346d995f3c49

所谓的高级流,实际上就是将基本流进行了一个包装。

经过包装后,字节缓冲流在字节流的基础上,提供了一个缓冲区,以及一些新的方法。

实际上和文件进行操作的还是基本流本身。

创建一个字节缓冲流的时候,需要提前去创建一个字节流的对象和本地的文件进行绑定。

public static void main(String[] args) throws IOException {
// 和本地文件关联
FileInputStream fis = new FileInputStream(“.//a.txt”);
FileOutputStream fos = new FileOutputStream(“.//b.txt”);
// 包装为高级流
BufferedInputStream bis = new BufferedInputStream(fis);
BufferedOutputStream bos = new BufferedOutputStream(fos);
}
因为,实际上和本地文件进行关联的还是基本流对象。所以和文件有关的一些规则,还是继承自基本流的。

可以打开基本流的续写标志使得高级流具有续写功能

public static void main(String[] args) throws IOException {
// 和本地文件关联
FileOutputStream fos = new FileOutputStream(“.//b.txt”, true);
// 包装为高级流
BufferedOutputStream bos = new BufferedOutputStream(fos);
}
如果输出流打开的文件存在,则清空,不想清空就打开appen标志。

如果输出流打开的文件不存在,且父级目录存在的话,就会创建一个新的文件。

字节缓冲流的读写
BufferedInputStream提供的read方法和FileInputStream中所提供的read方法一致。

// 无参的read方法,每次只读取一个字节的数据,将这个字节的数据转换为十进制后返回
// 文件的末尾会返回 -1
int ch = bis.read();
// 有参的read方法,参数传入一个byte类型的数组,每次读取都会试图去将这个byte类型的数组填满
// 将成功读取到的字节数量作为一个int类型的值进行返回,文件的末尾会返回 -1
int len = bis.read(new byte[1024]);
BufferedOutputStream提供的write方法和FilOutputStream中所提供的write方法一致。

// 无参数的write方法,一次只写入一个字节的数据
bos.write(27122);
byte[] bytes = new byte[]{97, 98, 99};
// 有参的write方法,可以一次性写入一整个byte类型的数组
bos.write(bytes);
// 也可以只写入byte数组的一部分,开始下标,长度
bos.write(bytes, 1, 2);
字节缓冲流的关闭
在关闭缓冲流对象的时候,是不需要去关闭基本流对象的。

因为,在缓冲流的close方法的底层,是同时兼顾了基本流的关闭的。(也就是,在关闭缓冲流的时候,会自动去关闭对应的基本流)

字符缓冲流
对于字符缓冲流而言,因为FileReader和FileWriter本身就已经提供了缓冲区。

所以对于字符缓冲流的包装,FileReader和Filewriter的读写速度的提升并不明显。

字符缓冲流更重要的是提供了两个好用的新方法。

readLine方法
readLine方法的作用是去一次性读文件中的一整行。

public static void main(String[] args) throws IOException {
// 创建一个基本流,并用BufferedReader进行包装
BufferedReader bufferedReader = new BufferedReader(new FileReader(“.//a.txt”));
String str;
// readLine方法每次会读取文件中的一整行,并将读取到的内容以String类型进行返回
// 如果readLine读取到文件的末尾了,会返回null
// 需要额外注意的是,readLine读取每行停止的依据是,每行的末尾的回车换行
// 也就是readLine读到换行就会停止,但不会读入
str = bufferedReader.readLine();
bufferedReader.close();
}
利用readLine方法和其提供的结束标志null,可以实现readLine方法的循环读取。

public static void main(String[] args) throws IOException {
// 创建一个基本流,并用BufferedReader进行包装
BufferedReader bufferedReader = new BufferedReader(new FileReader(“.//a.txt”));
String str;
// 不断读取数据,直到文件中没有数据可读,返回null为止
while((str = bufferedReader.readLine()) != null) {
System.out.print(str);
}
}
newLine方法
之前提到的,如果想用FileOutputStream去输出一个回车换行的话,需要根据不同的平台输出不同的内容。

也就是,FOS提供的输出换行的方式并不支持跨平台,不方便。

字符缓冲流中提供了newLine方法来进行跨平台换行。

(newLine方法会自动识别执行文件的操作系统,并输出对应的回车换行符号)

public static void main(String[] args) throws IOException {
// 创建一个字符缓冲输出流对象
BufferedWriter bos = new BufferedWriter(new FileWriter(“.//a.txt”));
// 利用newLine方法输出换行
bos.write(“Hello World”);
bos.newLine();
bos.write(“Fuck World”);
bos.close();
}
缓冲流的总结
缓冲流的分类
缓冲流一共有四种。

字节缓冲输入流

字节缓冲输出流

字符缓冲输出流

字符缓冲输入流

缓冲流为什么可以提升性能。
因为其在底层提供了8KB的缓冲区(对于字节缓冲流而言,对于字符缓冲流而言是8K个char类型(一个char类型含有两个字节))。

将数据转移的过程从硬盘到内存再到硬盘,改变为全在内存中进行操作。(内存的运算速度更快)

字节流的性能提升很明显,但是字符流本身就具有缓冲区所以性能提升不明显。

字符缓冲流提供的新方法
readLine方法

newLine方法

使用缓冲流对文件中的数据进行重新排序
public static void main(String[] args) throws IOException {
// 用BufferedReader去一行一行读取
BufferedReader br = new BufferedReader(new FileReader(“.//src//a.txt”));
BufferedWriter bw = new BufferedWriter(new FileWriter(“.//src//b.txt”));
// 创建容器来保存读取到的String
TreeMap<Integer, String> treeMap = new TreeMap<>(new Comparator() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
// 循环读取,直到读取到文件的末尾
String str;
while((str = br.readLine()) != null) {
treeMap.put(Integer.parseInt(String.valueOf(str.charAt(0))), str);
}
// 遍历treeMap,向文件中输出已经排序号的String
treeMap.forEach(new BiConsumer<Integer, String>() {
@Override
public void accept(Integer integer, String s) {
try {
bw.write(s);
bw.newLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
// 关闭读取和写出流
bw.close();;
br.close();
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值