• 第01篇_Java基础

    第01章_基础语法

    第一节 快速入门

    1. Java语言简介

    Java语言是美国Sun公司在1995年推出的高级编程语言,主要应用于企业应用开发,如商城系统、物流系统、网银系统等。

     

    2. JDK/JRE与JVM

    JDK (Java Development Kit):指Java程序开发工具包,包含JRE和开发人员使用的一些工具(如:javac、javap、jconsole等)。

    JRE(Java Runtime Environment):指Java程序的运行时环境,包含JVM和运行时所需要的核心类库。

    JVM(Java Virtual Machine):指Java虚拟机,用于运行 Java 字节码,常见的有HotSpot JVM、GraalVM、Android Runtime (ART)等。

    image-20221018132256210

    扩展:

    1. HotSpot JVM 分为 Oracle HotSpot JVM 和 OpenJDK HotSpot JVM ,后者又衍生了一些发行版,如:BishengJDK。

    2. OpenJDK 是完全开源免费的,而Oracle JDK 是商业收费的,JDK8u221 是其最后一个永久免费版本,后续版本只免费使用3年。

     

    3. 安装JDK

     

    4. HelloWorld案例

    1) 编写源文件

    新建HelloWorld.java文件,输入以下代码:

    注意

    1. 这里的文件名必须和类名一致,注意大小写。

     

    2) 编译源文件

    使用javac命令对源文件进行编译,如果编译成功,将会生成一个HelloWorld.class字节码文件。

     

    3) 运行字节码

    使用java命令进行运行,注意文件名不要加.class后缀。

     

    5. 面试补充

    1) 编译流程

    Java源文件经过javac命令编译后,生成.class字节码文件,字节码文件可被虚拟机解释执行。

    Java程序转变为机器代码的过程

    扩展:

    1. 使用字节码的好处:在一定程度上解决了解释型语言执行效率低的问题,又保留了解释型语言可移植的特点。

     

    2) JIT和AOT

    JIT(Just in Time Compilation)运行时编译,在第一次编译后,将字节码对应的机器码保存下来,下次可以直接使用。

    Java程序转变为机器代码的过程

    AOT(Ahead of Time Compilation)提前编译,在程序被执行前就将其编译成机器码,属于静态编译

     

    3) Java vs C++

     

     

    第二节 数据类型

    1. 数据类型分类

    在Java中,提供了四类八种基本数据类型,分别是:

    引用数据类型包含基本数据类型之外的所有类型,如字符串、类、数组、接口、lamda表达式等。

    提示

    1. 字面量除了上述各种基本类型的值外,还有字符串null值两种。

    2. 成员变量具有默认值,但局部变量必须初始化后才能使用,否则会出现编译错误。

     

     

    2. 数据类型转换

    参与的计算的数据,必须要保证数据类型的一致性,否则将发生类型的转换,转换可分为:

    特殊的,对于byteshortchar类型,在赋值时,如果右边为不超过取值范围的常量,则编译器会自动补上强制类型转换,在参与运算时,至少会被提升为int类型。

     

     

    3. 关于字符编码问题

    字符编码大致可分为Unicode编码非Unicode编码,不同编码对应了不同的存储格式:

     

    1) Unicode编码

    Unicode编码为全球约110万字符分配的的唯一数字编号,编号范围为ox0000~0x10FFFF

    大部分常见的字符都在ox0000~0xFFFF之间,大部分常见的中文字符在ox4E00~ox9FFF之间,如"马"的Unicode编码为U+9A6C

    Unicode编码只是给字符编号,但未说明其二进制形式的编号如何存储存储方式主要有如下三种:

    提示:

    1. 如果第一个字节存储二进制编号的最高位则称为"大端"字节序,如果存储最低位则称为"小端"字节序

    2. UTF-8-BOM:默认情况下,文件不会声明自身的编码格式,但UTF-8编码的文本文件,可以设置前三个字节为0xEF0xBB0xBF来表示当前文件为UTF-8文件,这三个字节被称为BOM(ByteOrder Mark)头,支持UTF-8-BOM格式的文件编辑器可自动识别。

     

    2) 非Unicode编码

     

    3) 乱码原因

    字符乱码主要有两种原因:

    4) Java内部字符处理

    在Java内部(内存中)进行字符处理时,采用的都是Unicode编码,具体编码格式是UTF-16BE

    进行字符处理时的基础类型是char,其它的Character、String、StringBuilder都是基于char类型的。

    char本质上是一个固定占用两个字节的无符号正整数,这个正整数对应于Unicode编号,用于表示那个Unicode编号对应的字符。

    由于char固定占用两个字节,只能表示Unicode编号在65536以内的字符,而不能表示超出范围的字符,超出范围的字符得使用两个char。

    注意

    1. 如果使用字面量'马'进行赋值,使用不同的编码格式打开文件可能看到不同的显示效果。

    2. 推荐JAVA文件采用UTF-8格式,并采用UTF-8格式进行编译。

     

    4. 关于浮点数精度丢失问题

    浮点数在转换为二进制时,可能出现无限循环,如 0.2 -> 001100110011...,因此存储时可能会被截断,出现精度损失:

    当浮点数进行运算时,就可能出现下面情况:

    此时,可以考虑使用 BigDecimalLong 类型(放大N倍)来存储浮点数。

     

     

    第三节 运算符

    1. 支持的运算符

    image-20221018165355070

     

    2. 移位运算符

    移位运算符将被操作的数据被视为二进制数,移位就是将其向左或向右移动若干位的运算。

    它的优点是高效(移位操作对应硬件指令,一般只需要一个时钟周期)和节省内存(使用一个整数来存储多个布尔值或标志位),应用场景如:优化乘2或除2操作、使用 int 或 long 存储多个标志位、哈希算法等。

    注意:

    1. 如果移位位数大于32位(int)/64位(long),则会对位数取余,例如:int x = 1; x<<42;等同于x<<10

     

    3.注意事项

    1. 整数的除法运算,将会丢弃余数,例如1/2=0

    2. 负数的取余运算,结果的正负始终与前一个操作数相同,例如-3%2==-13%-2==1

    3. 复合赋值运算符中隐含了一个强制类型转换,例如short s = 1; s = s + 1;会报错,而s += 1;却不会。

    4. 【重要】对于&和|运算符,如果两边为布尔型,则执行不短路的逻辑运算,如果为int型,则执行按位与和按位或。

     

     

    第四节 流程控制

    1. 分支语句

    注意

    1. switch语句支持的数据类型有整型字符串枚举

    2. 如果case语句的后面不写break语句,将出现穿透现象

     

    2. 循环语句

    提示

    1. do-while循环至少会被执行一次,可以用do{}while(flase)来实现goto语句

     

    3. break和continue

    break可用于跳出本层循环或结束switch语句,continue可用于结束本次循环。

     

    第五节 数组

    1. 数组的基本使用

     

    2. 多维数组

     

    3. Arrays工具类

    java.util.Arrays 类包含用来操作数组的各种方法,比如排序和搜索等。其所有方法均为静态方法,调用起来非常简单。

     

     

    第六节 方法

    1. 方法的基本使用

    注意:

    1. 通过 static 关键字可以定义静态方法,静态方法属于,可通过类名对象调用。

    2. 静态方法只能访问静态方法和静态变量,而实例方法则没有这个限制。

     

    2. 方法重载

    方法重载是指在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。参数列表不同可以是参数个数不同、参数类型不同和参数的顺序不同三种形式。

    注意:方法重载与参数的名称以及返回值类型无关。

     

    3. 可变参数

    方法的最后一个参数可以定义为可变参数同时支持数组和可变参数形式的传参

    注意

    1. 可变参数形式与数组形式的方法定义相互冲突,只能保留一个。

    2. 形参格式使用数组形式的方法则只能通过数组传参。

     

     

    第七节 面试补充

    1. 值传递 vs 引用传递

     

    2. 引用拷贝 vs 浅拷贝 vs 深拷贝

    image-20250301144156369

     

     

    第02章_面向对象

    第一节 类与对象

    1.基本使用

    是一组相关属性和行为的集合,对象是一类事物的具体体现。

    注意:可以通过this关键字来显示指明需要访问成员变量,来区分同名的局部变量。

     

    2. 权限修饰符

    权限修饰符可以用来修改方法属性等的访问权限,体现了面向对象的封装性。

    1. public:公有的,对所有人开放,无访问限制。

    2. protected:被保护的,对同包和其子类开放。

    3. [default]:默认值(缺省值),对同包开放。

    4. private:私有的,仅对所属类开放。

    注意:

    1. 一般将类定义为public,此时要求文件名和类名保持一致;如果定义为[default],则仅有同包中才能访问该类。

    2. 一般将成员变量定义为private,并提供相应的Getter/Setter方法和构造器。

    3. 不能对父类的private方法进行重写。

     

    3. static修饰符

    static可以用来修饰变量方法代码块内部类等,被修饰的内容保存在静态区(方法区的一部分),随着类的加载而加载,且只加载一次,加载后被所有对象共享,一般用类名进行调用。

    注意

    1. 静态属性和静态方法一般使用类名来调用,虽然也可以通过对象来调用,但不推荐。

    2. 静态方法和静态代码块只能访问类中的静态变量,不能直接访问普通成员变量或成员方法。

    3. 除静态代码块外,Java中还有构造代码块,它们的执行顺序为:静态代码块->Main函数->构造代码块->构造函数->普通代码块

    4. 在Java1.7后,静态代码块不能存在于主类中,防止干扰main函数的执行。

    5. 关于静态内部类的使用请参考内部类章节。

     

    4. final修饰符

    final表示不可改变,可以用于修饰方法变量等,分别具有不同的含义。

    注意:

    1. 被final修饰的静态变量一般称为常量,通常使用大写字母+下划线的形式命名。

    2. 必须保证类当中所有重载的构造方法,最终都会对final变量进行赋值。

     

    5. 面试补充

    面向对象的三大基本特征:封装继承多态

    面向对象与面向过程的主要区别如下:

     

     

    第二节 继承与多态

    1. 继承的基本使用

    继承指子类继承父类的特征和行为,使子类对象具有父类的实例域和方法,不仅复用了代码,而且使不同子类的对象能够被统一处理。

    注意:

    1. Java只支持单继承,一个类只能有一个父类,并且最终都继承自java.lang.Object类。

    2. 构造子类对象前必须先构造父类对象,如果父类没有无参构造,则必须在子类构造器的首行通过super显式调用有参构造。

    3. 不建议在构造方法中调用非private方法,特别是可被子类重写的方法,这将带来不必要的麻烦。

    4. 在子类中,可以使用super关键字来显式调用父类属性或方法。

    5. 子类初始化顺序为:基类静态代码块->子类静态代码块->基类实例代码块->基类构造方法->子类实例代码块->子类构造方法

     

     

    2. 方法重写与动态绑定

    方法重写指在子类中覆盖父类的实例方法,以此来实现子类的特异性功能,其要求方法名、参数列表和返回值完全一致,一般使用@Override注解标识。由于实例方法可能被重写,因此采用动态绑定方式,实际调用的方法取决于对象的动态类型

    注意:

    1. 使用final修饰的类不能被继承,使用final修饰的方法不能被重写,可用于加强封装性。

    2. 使用向下转型时可以先用instanceof关键字进行判断是否是某个类的子类,避免ClassCastException。

     

    3. 重名与静态绑定

    子类还可以定义与父类重名的变量静态方法,其采用静态绑定(编译期绑定)方式,实际调用取决于对象的静态类型

    注意:

    1. 此处所说的重名指方法名(变量名)与参数列表皆相同,而非方法重载的情形(重载始终优先于重写)。

     

     

    第三节 接口

    1. 定义接口

    接口是Java提供的一种的引用类型,其中可以定义常量抽象方法默认方法和静态方法(JDK 8)以及私有方法(JDK 9)等。

    接口也可以继承其它接口,并且能够同时继承多个,如果父接口中有重名的方法,那么子接口中只需要重写一次。

    注意:

    1. 接口中只可以定义public static final修饰的常量,且不可以定义任何变量

    2. 接口中没有静态代码块、构造方法,不能实例化对象。

     

    2. 实现接口

    一个类可以实现多个接口,一个接口也可以被多个类实现。

    类在继承基类的同时,也可以实现一个或多个接口,但要求extends关键字在implements之前

    注意:

    1. 如果实现类不是抽象类,则必须实现接口的所有抽象方法。

    2. 子接口在重写默认方法时,default关键字可以保留,而子类重写默认方法时,不可以保留,因为类中不存在默认方法。

    3. 如果继承来的方法和接口的默认方法重名,则子类会优先使用继承来的方法,即继承优于实现

     

    3. 使用接口

     

    4. 接口+组合替代继承

    继承至少有两个好处:一个是复用代码;另一个是利用多态和动态绑定统一处理多种不同子类的对象。使用组合替代继承,可以复用代码,但不能统一处理。使用接口替代继承,针对接口编程,可以实现统一处理不同类型的对象,但接口没有代码实现,无法复用代码。将组合和接口结合起来替代继承,就既可以统一处理,又可以复用代码了。

    如下例,Child复用了Base的代码,又都实现了IAdd接口,这样,既复用代码,又可以统一处理,还不用担心破坏封装。

     

     

    第四节 抽象类

    1. 抽象类的定义和使用

    抽象类指使用abstract修饰的类,一般存在抽象方法。抽象方法指只有方法声明,但没有方法体的方法。

    注意:

    1. 抽象类的子类,必须重写抽象父类中所有的抽象方法,除非该子类也是抽象类。

    2. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

    3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

     

    2. 抽象类和接口

    抽象类和接口经常配合使用,其主要区别如下:

     

     

    第五节 内部类

    1. 成员内部类

    成员内部类指定义在其它类内部的类,如List返回的Iterator对象等,适用于与外部类关系密切且需要访问外部类实例的变量或方法的类。

    注意:

    1. 如需创建成员内部类对象,则必须先创建外部类对象,成员内部类会保存外部类对象的引用。

    2. 内部类在编译后会生成独立的.class文件,类名用$进行分隔,如Body$Heart.class

    3. 如果在内部类中访问了外部类的私有变量或方法,则在编译时会被替换为[default]权限的access$0系列方法。

    4. 外部类只可以用public或[default]修饰,但内部类还额外支持private和protected修饰符

    5. 接口也可以定义内部接口和内部抽象类等,了解即可。

     

    2. 静态内部类

    静态内部类指定义在其它类内部的静态类,如LinkedList类内部的Node类等,适用于与外部类关系密切但不依赖于外部类实例的类。

    注意

    1. 创建静态内部类对象时不依赖外部类对象,因此它只能访问外部类的静态变量和方法,不可以访问实例变量和方法。

    2. 静态内部类除了可以定义成员方法、成员变量、构造方法、静态final变量等,还额外支持静态变量和静态方法

     

    3. 方法内部类

    方法内部类指定义在方法内部的类,方法可以是成员方法或静态方法,适用于只在当前方法中使用该类的场景。

    注意:

    1. 如需在方法内部类中访问方法参数或局部变量,则该变量必须是final的,或等效final的(因为该变量是通过构造参数直接传入)。

    2. 方法内部类不能写任何权限修饰符。

     

    4. 匿名内部类

    匿名内部类指定义在方法内部的匿名类,它没有类名和构造方法,且只能在定义类时创建对象,适用于接口回调的场景。

    注意:

    1. 匿名内部类可以定义实例变量、实例方法、初始化代码块、参数列表等,其中参数列表将会传递给父类的构造方法。

    2. 与方法内部类一样,匿名内部类也可以访问外部类的变量和方法,可以访问等效final的方法参数和局部变量。

     

     

    第六节 枚举类

    枚举(enum)是一种特殊的类,它只有类中声明的有限个对象,对象名称一般大写。

     

    1. 基本枚举类

     

    2. 带成员的枚举类

    枚举类可以定义自己的成员变量成员方法以及继承接口等。

    注意:

    1. 枚举对象必须定义在类的前面,并且以分号结尾,再写其它代码。

    2. 不需要提供Setter方法,枚举对象不允许修改。

     

    3. 枚举的本质

    枚举本质上还是一个类,上述案例转换为普通类如下:

     

     

    第七节 常用类

    1. 包装类

    1) 包装类介绍

    Java中提供了四类八种基础类型,为了适应不同场合,又分别提供了对应的包装类。

    image-20221027133900647

     

    2) 装箱与拆箱

    基本类型与对应的包装类对象之间,来回转换的过程称为”装箱“与”拆箱“。从JDK1.5开始,该动作可以自动完成。

     

    3) 包装类与字符串转换

     

    4) 面试补充
    包装类型 vs 基本类型

     

    包装类型的缓存机制

    包装类内部建有缓存机制,以降低包装类对象的创建消耗,如:

    在使用自动装箱/拆箱或调用包装类的 valueOf()xxxValue() 方法时,如果在缓存范围内,就会用到缓存对象。

    则会自动取池中对象,可用==进行地址比较,否则需要用equals比较。

    注意:

    1. 特别注意:建议包装类对象之间值的比较,全部使用 equals 方法比较,即使在缓存区间内。

     

     

    2. 字符串(String)

    1) String类基本使用

    Java中使用java.lang.String类代表字符串,创建字符串的方式如下:

    String类在创建后不可修改,并且对字符串常量进行入池共享。

    注意:

    1. 通过字符串字面量构造也会创建String类对象,并且会自动入池,推荐使用。

    2. String类与字节数组之间的转换依赖字符编码,如未指明则使用系统默认字符编码:Charset.defaultCharset().name()

    3. 在 JDK8 及之前,String内部采用 char[] 实现,但在JDK9及之后,使用 byte[] + coder 实现;

     

    2) String的常用方法

    image-20221029143742709

     

    3) 字符串常量池

    字符串常量池是 JVM 为 String 类开辟的一块内存区域,主要用于 String 类对象的共享,以提升性能和减小内存占用。

    对于编译期可以确定值的字符串,也就是常量字符串 ,JVM 会自动将其存入字符串常量池。

    也可通过String#intern 方法将字符串手动入池,返回值是调用字符串池中相同字符串(通过equals比较)的引用;

    注意:

    1. String#intern 方法返回的引用不一定是调用 intern 方法对象的引用,入池不一定能成功!

     

    4) StringBuilder

    如果对字符串操作很频繁或者在循环中使用字符串,则推荐使用StringBuilder类或其线程安全版本的StringBuffer类来处理。

    注意:

    1. StringBuilder内部采用非final修饰的字符数组,可以根据字符串的修改来进行动态扩容,而不会创建很多中间字符串对象。

    2. 如果不是在循环中使用,则编译器一般可以自动优化为StringBuilder操作,而在循环中,可能会创建多个StringBuilder对象。

     

    5) StringJoiner

    在JDK1.8+版本,添加了更适合进行字符串拼接操作的类StringJoiner,同时在String类中加了一个静态方法String.join()

     

    6) 面试补充
    String类如何保证不可变性?

     

     

    3. 日期和时间

    1) 相关基本概念

    关于日期和时间,有一些基本概念,包括纪元时时刻时区年历等。

    纪元时(Epoch Time):一个特殊的时刻(零点),指格林尼治标准时间1970年1月1日0时0分0秒

    时刻相对于纪元时的毫秒数(允许负数)。这个毫秒数在世界各地都相同,但是不同地区对其解读不一样(与时区和年历有关)。

    时区:全球一共有24个时区,英国格林尼治是0时区(GMT+0),北京是东八区(GMT+8:00),也就是说格林尼治凌晨1点,北京是早上9点。

    年历:描述了一年有多少月,每月有多少天,甚至一天有多少小时,在中文系统中,一般使用公历。

    注意:时刻是一个绝对时间,但对时刻的解读,则是相对的,与时区和年历相关。

     

    2) Date(时刻)

    java.util.Date是早期引入的日期API,主要用于表示时刻的概念,同时承担了年历的作用,但由于不支持国际化,许多方法被标记过时了,我们只需学习未过时的方法即可。

     

    3) TimeZone(时区)

    java.util.TimeZone(抽象类)表示时区,默认时区由系统属性`user.timezone属性决定。

    注意:

    1. 系统属性可通过System.getProperty("")获取,并在启动时通过java -Duser.timezone=Asia/Shanghai xxxx参数进行调整。

     

    4) Locale(地区和语言)

    java.util.Locale表示国家(或地区)和语言,中国内地的代码是CN,中国台湾地区的代码是TW,美国的代码是US,中文语言的代码是zh,英文语言的代码是en等。

     

    5) Calendar(日历)
    实例化

    java.util.Calendar(抽象类)表示与年月日相关的日历信息,可以对日期和时间进行设置和修改。

     

    获取日历信息

    其内部有一个与Date中类似的长整型变量protected long time,用于记录时刻(默认为当前时间),并且还有一个数组protected int fields[],表示日历中各个字段的值。这个数组的长度为17,能够表示的字段主要有:

    字段含义
    Calendar.MONTH月(0~11)
    Calendar.DAY_OF_MONTH当前月的第N日(1~xx)
    Calendar.HOUR_OF_DAY当前日的第N时(0~23)
    Calendar.MINUTE分钟(0~59)
    Calendar.SECOND秒(0~59)
    Calendar.MILLISECOND毫秒(0~999)
    Calendar.DAY_OF_WEEK当前周的第N天(1~7,1为周日)

    注意:

    1. Calendar中定义了表示各个星期、各个月的静态变量,如Calendar.SUNDAY表示周日、Calendar.JULY表示7月。

     

    直接设置时间

    Calendar支持根据Date毫秒数设置时间,也支持根据年月日等日历字段设置时间。

     

    增加或减少时间

    除了直接设置,也支持根据字段增加和减少时间,正数表示增加,负数表示减少,增减支持自动调整

     

    转换为Date或毫秒数

     

    与其它Calendar进行比较

     

    6) DateFormat(格式化)

    java.text.DateFormat(抽象类)用于Date与字符串之间的相互转换。我们一般用它的实现类SimpleDateFormat,它接受一个pattern作为构造参数,pattern中的英文字符(a~z|A~Z)表示特殊含义,其他字符原样输出。部分特殊字符含义如下:

    格式字符串含义格式字符串含义
    yyyy年份mm分钟
    MM月份ss
    HH小时(24小时制)E星期几
    hh小时(12小时制)a上午或下午(仅12小时制有效)

    特殊的,如果想原样输出英文字符,可以将其用单引号括起来。

     

     

    4. 日期和时间(JDK8+)

    JDK8对日期和时间进行了增强,位于java.time包下。

     

    1) Instant(时刻)

     

    2) ZoneId/ZoneOffset(时区/时区偏移)

     

    3) LocalDateTime(时区无关日期时间)
    基本使用

    注意:

    1. DayOfWeek是一个枚举,有7个取值,从DayOfWeek.MONDAY到DayOfWeek.SUN-DAY。

     

    LocalDate和LocalTime

    LocalDateTime由两部分组成,一部分是日期(LocalDate),另一部分是时间(LocalTime),它们三者之间可以相互转换。

     

    4) ZonedDateTime(时区相关日期时间)

    注意:

    1. LocalDateTime只记录了年月日等相关信息,不记录时区信息,其它方法一般都要传递时区。

    2. ZonedDateTime不仅记录了时刻,还记录了时区,ZonedDateTime ≈ LocalDateTime + 时区。

     

    5) DateTimeFormatter

     

    6) 修改日期和时间
    直接设置或增减方式

    修改日期和时间有两种方式,一种是直接设置绝对值,另一种是在现有值的基础上进行相对增减操作。

    注意:

    1. ChronoField是一个枚举,里面定义了很多表示日历的字段,MILLI_OF_DAY表示在一天中的毫秒数,值从0到(246060*1000) -1。

     

    TemporalAdjuster接口方式

    针对复杂的时间调整,JDK8专门定义了一个函数式接口TemporalAdjuster,Instant、LocalDateTime和LocalDate等都实现了它。与此相关的还有TemporalAdjusters类,里面提供了很多TemporalAdjuster的实现。

     

    7) 计算时间段

    JDK8中用Period表示日期差,差值为N年N月N日;用Duration表示时间差,单位可以为天(1天24小时)、时、分、秒、毫秒等。

     

    8) 与Date/Calendar对象的转换

     

     

    5. 其它常用类

    1) 根类(Object/Objects)

    java.lang.Object类是Java语言中的根类,即所有类的父类,java.util.Objects是与之配套的工具类。

    方法名说明
    protected Object clone()创建并返回对象的浅拷贝
    boolean equals(Object obj)比较两个对象是否相等,默认按地址进行比较,可进行重写
    protected void finalize()在对象被回收前由垃圾回收器调用
    Class<?> getClass()获取对象的运行时对象的类
    int hashCode()获取对象的 hash 值(Int类型
    void notify()唤醒在该对象上等待的某个线程
    void notifyAll()唤醒在该对象上等待的所有线程
    String toString()返回对象的字符串表示形式
    void wait()让当前线程进入无限等待状态,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法
    void wait(long timeout)让当前线程进入超时等待状态
    void wait(long timeout, int nanos)让当前线程进入超时等待状态,支持纳秒级别控制(0-999999)

    注意:

    1. 重写 equals 的同时一般也重写hashcode(),用于初步判断对象是否相等,可在 HashSet、HashMap 等容器中优化性能。

    2. 对象比较推荐使用Objects.equals(Object obj1, Object obj2)方法,支持NULL值比较。

     

    2) 系统与运行时(System/Runtime)

    java.lang.System类中提供了大量的静态方法,可以获取与系统相关的信息或进行系统级操作。java.lang.Runtime描述的是运行时状态,在每一个JVM进程中都会提供唯一的一个Runtime类实例化对象,开发者可以通过Runtime类对象获取与JVM有关的运行时状态。

    注意:

    1. System和Runtime都可以调用gc方法,但不建议在代码中手动干预,并且代码中的gc操作是可以通过JVM的运行参数来屏蔽的。

     

    3) 随机数(Random)

    Random类的实例用于生成伪随机数流。下面是一个生成范围1-n随机数的示例:

    注意:

    1. 如果随机数种子相同,那么它们将生成并返回相同的数字序列。

    2. Random类是线程安全的,但如果并发太高,会产生竞争,这时候可以考虑ThreadLocalRandom类或Math.random()方法。

    3. Java类库中还有一个随机类SecureRandom,可以产生安全性更高、随机性更强的随机数,用于安全加密等领域。

     

    4) 数学运算(Math)

    java.lang.Math类包含了一些数学常量和基本数学运算,如初等指数、对数、平方根和三角函数等。

     

    5) 大数运算(BigDecimal)

    java.math.BigDecimal用于对浮点数进行精确的运算,一般用于金额、利率计算等场景。

    BigDecimal是对象类型,不能使用传统的+-*/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法,方法中的参数也必须是BigDecimal的对象。

    注意:

    1. 不推荐基于浮点数直接构建BigDecimal对象,应先转换为字符串,再基于数值字符串构建BigDecimal对象。

    2. 如果进行除法运算的时候,结果不能整除(有余数),这个时候会报java.lang.ArithmeticException,需要设置精度和舍入模式。

    3. BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法,防止 1.0 != 1.00

    4. 如果需要处理的数为大整数,可以使用java.math.BigInteger来替代。

     

    6) 文本扫描器(Scanner)

    java.util.Scanner类是一个可以使用正则表达式来解析基本类型和字符串的简单文本扫描器。

     

    7) 比较器(Comparator)

    java.util.Comparator接口用于比较两个对象的大小,小于时返回负数,等于时返回0,大于时返回正数。

     

    8) 关闭钩子(AutoCloseAble)

    JDK1.7引入了AutoCloseAble接口,配合try(获取资源){}语法进行使用,这样获取的资源可以自动关闭,无需手动释放。

     

    9) 空值处理(Optional)

    java.util.Optional是Java 8引入的一个泛型容器类,内部只有一个类型为T的单一变量value,可能为null,也可能不为null。

    Optional有什么用呢?它用于准确地传递程序的语义,它清楚地表明,其代表的值可能为null,程序员应该进行适当的处理。

     

    10) Unsafe类

     

     

    第03章_泛型

    第一节 泛型简介

    1. 什么是泛型?

    泛型是计算机编程中一种重要的思维方式,它将程序算法数据类型相分离,使得同一套程序算法能够应用于各种数据类型,并且可以保证类型安全,提高可读性。

    通俗来说,泛型就是类型参数化,即通过参数的形式传入类型,将代码与具体的数据类型解绑,同一套代码可用于多种数据类型。

     

    2. 泛型的本质

    泛型的本质是类型擦除(这对后面理解泛型非常重要)。在编译过程中,所有的泛型都将会被替换为Object类(或其上界类),并在合适的位置插入必要的强制类型转换,虚拟机只能执行这种转换后的非泛型代码

     

    3. 泛型的好处

    前面得知,泛型类最后依旧会被转换为非泛型类,那么我们使用泛型类有什么好处呢? 主要有两点:

    1. 避免强制类型转换,做到在编译期进行类型安全检查,防止类型转换异常(ClassCastException)。

    2. 精简代码,增强代码的健壮性和可维护性。

     

     

    第二节 泛型的基本使用

    泛型根据定义的位置不同,分为泛型类泛型接口泛型方法三类。

     

    1. 泛型类

     

    2. 泛型接口

     

    3. 泛型方法

    注意:

    1. 如果方法为静态方法,那么将不能够使用类上声明的泛型(静态变量同理),因为他们是类级别共享的。

    2. 一个方法是不是泛型的,与它所在的类是不是泛型没有任何关系,可以使用类上的泛型,也可以新定义方法的泛型。

     

    4. 泛型的类型信息

    虽然泛型在编译时被擦除为Object或上界类,但是在运行时,泛型引用的对象是实际的不同具体类型,并且可以获取和使用该类型信息。

     

     

    第三节 限定泛型

    1. 无限定泛型的使用限制

    在上述案例中,声明泛型时未做任何额外的限制,因此在泛型具体化时,可以使用任意类型,这种未被限制的泛型称为无限定泛型。也正是因为在具体化时没有限制类型的取值范围,因此无限定泛型在使用时将会受到一些限制

    例如,在通过泛型引用E e操作指向的对象时,由于E可能是任意类型,因此只能调用任意类型的根类Object的属性或方法。

    为了减弱上述限制,我们可以在声明泛型时进一步限定泛型可具体化的类型范围,要求其必须继承某类或实现某个接口,这样就可以在保证类型安全的前提下使用该类(接口)的一系列方法了,这种被限制可具体化类型范围的泛型称为限定泛型

     

    2. 限定为某类(接口)或其子类

    上文提到的Pair<U,V>类,对其进行扩展,限定泛型可具体化的类型必须是Number或其子类,格式为:<泛型名 extends 上界类名>

    注意:

    1. 对于限定泛型,在进行类型擦除时,将转换为它的上界类。

    2. 上界接口可以存在多个,如:T extends Base & Comparable & Serializable,其中Base为上界类,其它为上界接口。

     

    2. 上界类为泛型类

    上界类也可以是一个带泛型的泛型类,那么在声明限定泛型时,必须对上界类的泛型进行具体化:

    注意:

    1. 在实例化泛型类时,如果泛型的具体类型省略,将默认为Object类型,但是在具体化上界类时,并非如此。

     

    3. 上界类为其它泛型

    上界类还可以是已声明的其它泛型,当该泛型被具体化时,才会确定上界类的具体类型。如上述的DynamicArray的addAll()方法:

    如果不使用T extends E将会怎样?即addAll()方法定义如下所示,可以看到,将会出现编译错误。

    为什么会出现编译错误呢?我们分析下,如果DynamicArray<Integer>能给DynamicArray<Number>赋值将会怎么样?

    注意:

    1. 在add()方法中,形参为E,类型擦除后转换为Number,可以传入Number及Integer等子类;

    2. 但是在addAll()方法中,形参为DynamicArray<E>,类型擦除后为DynamicArray<Number>,而DynamicArray<Integer>是不允许传给DynamicArray<Number>的,否则将会出现上述隐患;

     

     

    第四节 泛型通配符

    泛型具体化时(而非声明时),支持一些通配符的使用,它可以通配多种具体类型,但同时也带来了一些限制,下面将会详细介绍。

     

    1. 通用泛型通配符

    通用泛型通配符用于在具体化泛型时通配所有的具体类型,它简化了泛型的声明和使用,格式为:?

    相应的,由于具体类型未知,因此在使用被通配的泛型对象时,也有一些限制。

    为减弱上述限制,根据不同的使用场景,提供了两种特定通配范围的泛型通配符:子类型泛型通配符和超类型泛型通配符。

     

    2. 子类型泛型通配符

    子类型泛型通配符对通配的具体类型范围做出了一些限制,用于通配ParentClass其子类,格式为:? extends ParentClass

    在得知子类型泛型通配符只通配某个类及其子类后,那么就可以确定它的上界类了,上界类确定后就可以使用上界类的属性和方法,并且可以赋值给上界类。(注意:该例中上界类为E,同样是一个未知类型,因此没有其它额外的方法可以调用,同样也只能够赋值给E)

     

    3. 超类型泛型通配符

    超类型泛型通配符和子类型泛型通配符相反,它用于通配ChildClass及其父类,格式为:? super ChildClass

    在得知超类型泛型通配符只通配某个类及其父类后,那么就可以确定它的下界类了,下界类确定后就可以使用下界类作为引用

    注意:关于限定泛型、子类型通配符、超类型通配符的赋值兼容

     

    再来看另外一个关于超类型通配符的使用场景:

     

    4. 限定泛型和泛型通配符对比

     

     

    第五节 泛型的局限性

    前面提到,Java中的泛型是通过类型擦除来实现的,所有的泛型在编译时都会被替换为Object或上界类,运行时Java虚拟机不知道泛型这回事,这带来了很多局限性,其中有的部分是比较容易理解的,有的则是非常违反直觉的 。

     

    1. 泛型的具体化类型不能是基本类型

    泛型的具体化类型不能是基本类型,应该使用它的包装类。

     

    2. 不能通过泛型直接创建对象

    不能通过泛型直接创建对象,需要传入泛型对应的类型信息,通过反射创建。

    提示:

    1. 实际上,可以参考第二节中,通过泛型引用的对象获取Class信息,进而创建对象。

     

    3. 泛型类的不同具体化本质上还是同一个类

    泛型在编译时将会被擦除为Object(或上界类),不同的具体化类型只是在编译时自动插入了不同的强制类型转换,本质上还是同一个类。

    由于是同一个类,因此它们的类型信息完全一致,并且类上所具有的静态资源也是共享的

    注意:

    1. 内部的first/second编译时都被擦除为Object类型,但是运行时分别指向不同的具体化类型对象。

     

    4. 类上的泛型不能用于静态变量或静态方法

    泛型类的泛型不能用于静态变量或静态方法,应为静态方法单独声明泛型,而静态变量不允许为泛型

     

    5. 类型擦除可能会引发一些冲突

     

    6. 不能直接创建泛型数组

    如下创建泛型数组的代码是禁止的:

    因为数组是Java直接支持的概念,它知道数组元素的实际类型,在类型不对时可快速触发运行时异常,因此编译时允许赋值给父类数组。

    但是如果允许创建泛型数组,如下:

    由于Pair<Double, String>和Pair<Object, Integer>的类型都是Pair,因此第二行赋值时即不会编译报错,也不会立即触发运行时异常,埋下了隐患,因此Java禁止创建泛型数组。

    如果我们非要创建泛型类型的数组,可以使用原始类型来创建,这样可以跳过编译检查,但是问题还是存在的。

    最好的解决办法是,使用泛型容器来代替泛型数组

     

    7. 泛型容器不能直接转换为数组

    有时候我们希望将泛型容器直接转化为一个泛型数组,如下:

    实现toArray()方法时,一般是先创建一个泛型数组,然后拷贝数据再返回该数组。

    由于前面已经提到,直接创建泛型数组是行不通的:E[] arr = new E[size]; // err,因此,可能会想到如下两种方式:

    虽然这两者方式没有编译错误,但是在运行时都会抛出如下异常:

    要想实现上述需求,必须知道数组元素的类型信息,才能创建泛型数组,可以修改实现如下:

    提示:

    1. 实际上,可以通过一些运行时类型信息来获取元素的类型信息,从而传入Array.newInstance创建数组,可以对比第二节相关案例。

     

     

    第04章_容器

    第一节 容器类概述

    1. 容器体系简介

    容器类主要分为集合类容器(Collection)映射类容器(Map)。集合类容器包括列表(List)队列(Queue)集合(Set)三大类,其中队列又衍生出双端队列(Deque),它们都是容器类的超级接口,并且一般都定义了对应的抽象类。

    在日常开发中,我们一般使用上述接口或抽象类的具体子类,常用的容器如下:

    容器容器类说明
    数组列表ArrayList基于数组实现的列表
    链式列表LinkedList基于链表实现的列表,也可作为链式双端队列
    数组双端队列ArrayDeque基于循环数组实现的双端队列
    链式双端队列LinkedList基于链表实现的双端队列,也可作为链式列表
    优先级队列PriorityQueue基于实现的单端队列,元素可以按优先级出列
    哈希集合HashSet基于哈希表+链表(或红黑树)实现的无序集合
    带链的哈希集合LinkedHashSet继承自HashSet,在其基础上通过额外的来维护插入有序
    树状集合TreeSet基于红黑树实现的规则有序集合
    枚举集合EnumSet基于数组实现的高效集合,只适用于枚举类型元素
    哈希映射HashMap基于哈希表+链表(或红黑树)实现的无序映射
    带链的哈希映射LinkedHashMap继承自HashMap,在其基础上通过额外的来维护存取有序
    树状映射TreeMap基于红黑树实现的规则有序映射
    枚举映射EnumMap基于位向量实现的高效映射,只适用于枚举元素

    注意:

    1. 容器一般会继承对应的抽象类及直接实现对应的超级接口,如ArrayList继承了AbstractList,并且还直接实现了List接口。

    2. 但是也有些例外,如ArrayDeque没有对应的AbstractDeque,EnumSet和EnumMap没有直接实现对应的Set和Map接口等。

     

    2. 常见接口和抽象类简介

    1) Iterable<T>和Iterator<E>

    Iterable<T>接口表示“可迭代的”,它提供了获取迭代器(Iterator<E>)的方法,通过迭代器可以进行遍历操作,并且支持ForEach语法。

    ListIterator<E>扩展了Iterator接口,增加了一些向前遍历、添加元素、修改元素、返回索引位置等方法。

    提示

    1. 只要对象实现了Iterable接口,就可以使用foreach语法,编译器会转换为调用Iterable和Iterator接口的方法。

     

    2) Collection<E>与AbstractCollection<T>

    Collection<E>表示单列集合,只定义了基本的增删改查和遍历等方法,没有定义元素间的顺序或位置,也没有规定是否有重复元素。

    注意:

    1. Collection的add方法默认为抛出UnsupportedOperationException异常。

     

    3) 列表相关接口和抽象类

    List<E> 是 Collection<E> 的子接口,表示有顺序和位置的集合,增加了根据索引位置进行操作的方法。

     

    4) 队列相关接口和抽象类

    Queue<E>是Collection<E>的子接口,表示先进先出的队列,在尾部添加,从头部查看或删除

    Deque<E>是Queue<E>的子接口,表示更为通用的双端队列,有明确的在头或尾进行查看、添加和删除的方法。

     

    5) 集合相关接口和抽象类

    Set<E>是Collection<E>的子接口,它没有增加新的方法,但保证不含重复元素。SortedSet<E>和NavigableSet<E>在Set的基础上进行了扩充,方便实现TreeSet子类。

     

    6) 映射相关接口和抽象类

    Map<K,V>表示键值对集合(映射),它的元素为Entry<K,V>类型,经常根据键进行操作。SortedMapMap<K,V>和NavigableMapMap<K,V>在Map的基础上进行了扩充,方便实现TreeMap子类。

     

    3. 容器使用注意事项

    1) 根据容器特性选择合适的容器

    不同类型的容器有不同的适用场景,如数组类容器适合随机访问,链式容器适合头尾存取,堆类型容器适合TopN问题,树状容器适合元素按规则排序的场景等,应该根据使用场景选用合适的容器。

     

    2) 本章节介绍的容器都是线程不安全的

    除了HashtableVectorStack外,我们本章介绍的各种容器类都是线程不安全的。如需多线程操作同一个容器,可以使用Collections工具类提供的synchronizedXXX方法对容器对象进行同步,或者使用专门的线程安全容器类。

     

    3) 容器在通过迭代器遍历时会检测结构性变化

    容器类提供的迭代器都有一个特点,会在迭代时检测容器的结构性变化(通过modCount来实现),如通过容器引用去添加或删除元素等,将会抛出ConcurrentModificationException。如确实需要增删元素,可以通过迭代器的add和remove方法操作。

     

     

    第二节 列表(List)

    1. 数组列表(ArrayList)

    ArrayList<E>是List<E>的子类,基于数组实现,它的随机访问效率很高,但从中间插入和删除元素需要移动元素,效率比较低

    注意:

    1. ArrayList 在头部插入/删除时,由于需要移动后面所有元素,因此时间复杂度是O(n),但在尾部插入/删除时,复杂度是O(1)

    2. 特殊的。如果在插入时触发了扩容,则不管是在头部还是尾部,时间复杂度都是O(n)。

     

    1) ArrayList常用方法

    注意:

    1. 基于索引操作的方法,在操作节点前都会检查索引是否越界,如果越界将会抛出IndexOutOfBoundsException。

    2. 基于索引操作的插入类方法,当索引为0时,插入到头部,索引为size()时,插入到尾部

    3. 基于索引操作的删除和查看方法,索引范围必须为0~size()-1;

    4. 集合转数组,使用 list.toArray(new String[0]),其中 new String[0] 用来指定返回数组的类型。

    5. 数组转集合,使用 Arrays.asList(myArray) 获取的是不可变子集合,可再套一层移除这个限制:new ArrayList<>(arr)。

     

    2) ArrayList实现原理

    ArrayList内部使用数组elementData来存储元素,默认长度为10,长度会随着元素个数的变化动态分配(1.5倍),一般会有一些预留的空间,由另外一个整数size来记录实际的元素个数。

     

    3) 注意事项:并发修改异常

    由于迭代器内部会维护一些索引位置相关的数据,因此要求在迭代过程中,容器不能发生结构性变化,否则这些索引位置就失效了,就会抛出ConcurrentModificationException。所谓结构性变化,就是添加和删除元素等,只是修改元素内容不算结构性变化。

    如何避免异常呢?可以使用迭代器的remove方法,或直接通过list.removeIf来实现相同功能。

     

    4) 扩展:迭代器的实现原理

    为什么上面可以使用迭代器的remove方法来删除呢?这涉及到迭代器的实现原理,它内部维护了三个成员变量:

    当外部类调用add、remove等影响结构性的方法时,modCount都会自增,而每次迭代器操作的时候都会检查expectedModCount是否与外部类的modCount相同,这样就能检测出结构性变化。

    如果使用迭代器的remove方法,它在调用ArrayList的remove方法时,可以同步更新内部的cursor、lastRet和expectedModCount的值,因此可以正确删除。不过,需要注意的是,调用迭代器的remove方法前必须先调用next,否则会抛出IllegalStateException。

    注意:

    1. 迭代器是一种关注点分离的思想,将数据的实际组织方式与数据的迭代遍历相分离,是一种常见的设计模式。

    2. 迭代器语法更加简洁,并且对于部分容器,性能更加高效,推荐优先使用

     

     

    2. 链式列表(LinkedList)

    LinkedList<E>是List<E>的间接子类,基于链表实现,随机访问效率比较低,但增删元素只需要调整邻近节点的链接。此外,它还继承了Deque\<E\>接口,可以用作双端队列先进先出队列等。

    注意:

    1. 在头部或尾部插入/删除。复杂度都为O(1),但在中间插入/删除/查找,需要指针寻址,复杂度是O(n)

    2. LinkedList底层是链表,随机访问的复杂度为O(n),基于性能考虑,所以没有实现随机访问接口(RandomAccess)。

     

    1) LinkedList常用方法

    注意:

    1. 栈/队列是双端队列的特殊情况,它们的方法都可以使用双端队列的方法替代,不过使用不同的名称和方法,概念上更为清晰。

    2. offer/poll/peek开头的方法在已满或为空时返回false或null(虽然LinkedList没有”已满“的概念,但其它队列/栈可能会有)。

    3. add/remove/get和push/pop/element开头的方法在已满或为空时会抛出IllegalStateException或NoSuchElementException。

     

    2) LinkedList实现原理

    LinkedList是一个双端链表,每个元素(节点)在内存中单独存放,元素之间通过前驱指针后继指针进行链接。

    而LinkedList内部只需保存一个头指针和一个尾指针即可,分别指向第一个节点和最后一个节点,通过指针寻址操作,关联所有元素,构成逻辑上的双端链表。

     

     

    第三节 队列(Queue/Deque)

    1. 数组队列(ArrayDeque)

    ArrayDeque<E>是Deque<E>的子类,基于循环数组实现,它可以用作双端队列先进先出队列等。和链式双端队列相比,从两端操作的效率会更高一些,但是不支持索引操作,并且在中间插入和删除很慢

     

    1) ArrayDeque常用方法

    构造方法如下,其它常用方法和LinkedList中介绍的类似,不再赘述。

     

    2) ArrayDeque实现原理

    下面重点看下ArrayDeque的循环数组是如何实现的,ArrayDeque内部主要有如下实例变量:

    通过引入头指针尾指针使物理上的简单数组(从头到尾)变为了一个逻辑上循环的数组,避免了在头尾操作时的移动。头尾有四种分布:

    队列的长度始终可以通过(tail - head) & (elements.length - 1)算出。而在添加新元素时,如在尾部添加,则tail = (tail + 1) & (elements.length - 1),如在头部添加,则head = ( head-1 ) & ( elements.length-1 ),如果出现head==tail,则表示容器已满,需要将容量扩为之前的2倍

    注意:

    1. ArrayDeque中,有效元素不允许为null,contains等方法在内部遍历时也将null视为结尾。

    2. 通过位与运算,可以有效提高计算下标的效率,并且可以确保索引不会越界,这在循环数组中的应用非常常见。

     

     

    2. 链式队列(LinkedList)

    LinkedList<E>还继承了Deque<E>接口,可以用作双端队列、先进先出队列、栈等,在链式列表章节已有介绍。

     

     

    3. 优先级队列(PriorityQueue)

    PriorityQueue<E>Queue\<E\>的子类,表示优先级队列,基于二叉堆实现的。常见的应用场景有“求前K个最大的元素”、“求实时中值”等。

     

    1) PriorityQueue常用方法

     

    2) PriorityQueue实现原理

    优先级队列基于实现,而堆是一颗完全二叉树,在从左到右并分层进行编号后,可以直接计算出任意节点的父节点和左右子节点的编号,如编号为i的节点,其父节点编号为i/2,左右子节点的编号分别为2\*i2\*i+1,可以将这个编号作为数组的索引,将每个节点按编号存储在一个连续的数组中,不仅节省空间,而且访问效率非常高。

    image-20230226181241753

    但在插入和删除(即将尾部元素覆盖头部元素)元素时,需要进行向上调整(siftup)或向下调整(siftdown)来维持堆的性质,效率都为Olog2N。

    注意:

    1. 堆分为小顶堆和大顶堆,大顶堆指每个元素不大于其父元素根节点就是最大节点,元素之间可以重复,小顶堆与之类似。

     

     

    第四节 映射(Map)

    1. 哈希映射(HashMap)

    HashMap<K,V>是Map<K,V>的子接口,基于哈希表实现(哈希表+链表/红黑树),要求元素的键(key)重写hashCode和equals方法,操作效率很高,但元素间没有顺序。

     

    1) HashMap常用方法

     

    2) HashMap实现原理

    HashMap内部有一个Node类型的数组table,称为哈希表(哈希桶),每个元素(table[i])指向一个单向链表(或红黑树)。

    当put新元素时,先计算key对应的hash值,再通过取余( h%(length-1),可优化为h&(length-1) )得到数组中的索引位置buketIndex,然后将value存放在该位置或该位置指向的链表(或红黑树)中。

    image-20230226214123166

    注意:

    1. 长度大于等于8时,并非直接转换为红黑树,而是先判断如果数组长度小于64,则先进行数组扩容,以优化查询速度。

     

     

    2. 带链的哈希映射(LinkedHashMap)

    LinkedHashMap<K,V>继承自HashMap<K,V>,在其哈希表+链表(或红黑树)的基础上额外添加了一条用于维护元素顺序的双向链表,这个链表可以按插入顺序排序,也可以按访问顺序排序。

     

    1) LinkedHashMap常用方法

    构造方法如下,其它方法和HashMap类似,但是get/put等方法内部会额外维护一个插入或访问顺序,同时遍历时按照该顺序进行。

    提示:

    1. 如果键本来就是有序的,使用LinkedHashMap比TreeMap效率更高。

     

    2) LinkedHashMap实现原理

    LinkedHashMap是HashMap的子类,内部增加了如下实例变量:

    其中Entry继承了HashMap.Node,增加了两个变量before和after, 分别指向前驱节点和后继节点。

    当处于“插入有序”模式时,哈希表新增元素的同时,也会添加到链表的末尾。当处于“访问有序”模式时,无论是插入、修改或访问,都会将该节点移到链表的末尾。

     

    3) 应用:LRU缓存

     

     

    3. 树状映射(TreeMap)

    TreeMap<K,V>是Map<K,V>的间接子接口,基于排序二叉树(红黑树)实现,要求键(key)实现Comparable<E>接口,或者创建TreeSet时提供一个Comparator<E>对象,其操作效率稍低,但键(key)可以按比较有序。

     

    1) TreeMap常用方法

    构造方法如下,其它方法和HashMap类似。此外,还有一些继承自SortedMap和NavigableMap的方法,由于使用较少,请查阅API文档。

    注意:

    1. TreeMap使用键的比较结果(而非equals)对键进行排重,即使键实际上不同,但只要比较结果相同,就会被认为相同。

     

    2) TreeMap实现原理

    TreeMap是基于红黑树实现的,主要成员变量如下:

     

    4. 枚举映射(EnumMap)

    EnumMap<K,V>是Map<K,V>的子接口,使用比哈希表效率更高的静态数组实现,但是要求元素必须为枚举类型。

     

    1) EnumMap常用方法

    构造方法如下,需要通过枚举类的Class信息进行构造,同时key必须为枚举类型

    下面是一个简单的使用示例:

    注意:

    1. EnumMap是有顺序的,为枚举元素定义的顺序

    2. 当put的值为null时,将会被替换为EnumMap.NULL存储,而值为真正的null表示该key不存在。

    3. 上述两种场景在get时都会返回null,但是在遍历时,不存在的key将会被跳过,如:{SMALL=null, MEDIUM=中}。

    4. 虽然使用普通的HashMap可以实现相同的功能,但是使用EnumMap更加简洁安全和高效。

     

    2) EnumMap实现原理

    EnumMap内部有两个长度相等的静态数组,一个表示所有可能的键, 一个表示对应的值,值为 null 表示没有该键值对,键都有一个对应的索引,根据索引可直接访问和操作其键和值,效率很高。

     

     

    第五节 集合(Set)

    1. 哈希集合(HashSet)

    HashSet<E>是Set<E>的子接口,基于HashMap<E,Object>实现,因此同样要求元素的键(key)重写hashCode和equals方法, 特性也基本类似,如访问效率高,元素间没有顺序等;

     

    1) HashSet常用方法

    提示:

    1. 推荐使用 new HashSet<>(data)去重,因为 HashSet 的 contains() 方法比 ArrayList 的更高效。

     

    2) HashSet实现原理

    HashSet的内部有一个HashMap,操作基本都是委托其完成的。

     

     

    2. 带链的哈希集合(LinkedHashSet)

    LinkedHashSet<E>继承自HashSet<E>,基于LinkedHashMap<K,V>实现,默认支持插入有序,不支持访问有序。

     

    1) LinkedHashSet常用方法

    构造方法如下,其它常用方法和HashSet的使用类似,但add等方法内部会额外维护一个插入顺序,同时遍历时按照该顺序进行。

     

    2) LinkedHashSet实现原理

    LinkedHashSet继承自HashSet,构造时内部的map被初始化为LinkedHashMap,因此支持按插入有序:

     

     

    3. 树状集合(TreeHashSet)

    TreeSet<E>是Set<E>的间接子接口,基于TreeMap<E,Object>实现,, 特性也基本类似,同样也要求元素的键实现Comparable<E>接口,或者创建TreeMap时提供一个Comparator<E>对象。

     

    1) TreeHashSet常用方法

    构造方法如下,其它常用方法和HashSet中介绍的类似,不再赘述。此外,有一些继承自SortedSet和NavigableSet的方法,由于使用较少,请查阅API文档。

     

    2) TreeSet实现原理

    TreeSet的内部有一个NavigableMap,操作基本都是委托其完成的。

     

     

    4. 枚举集合(EnumSet)

    EnumSet<E>是Set<E>的子接口,基于位向量实现,效率非常高,但是元素要求必须为枚举类型。

     

    1) EnumSet常用方法

    构造函数如下,其它方法和HashSet使用类似。

    一个简单的使用示例如下:

     

    2) EnumSet实现原理

    EnumSet与之前介绍的Set实现类不同,它内部没有用对应的Map类EnumMap,而是使用了一种极为高效的位向量方式。

    位向量就是用一个位表示一个元素的状态(是否存在),用一组位表示一个集合的状态。如前面的枚举类型Day,它有7个枚举值,可以用一个字节的低7位表示,最高位补0,当对应元素存在时,则置为1,否则为0。

    image-20230227195421219

    当枚举类型的枚举值个数<=64时,将创建RegularEnumSet实现类,内部采用64位的long类型存储元素是否存在的信息。否则将创建JumboEnumSet实现类,采用long类型的数组存储,并用size记录元素的个数。

    在进行一些增删改查时,基本都是使用位操作来进行的,因此效率非常高,部分操作如下:

    扩展:取补集时为什么要移除高位多余的1?

    因为elements是64位的,当前枚举类可能没有用那么多位,取反后高位部分都变为了1,因此需将超出universe.length的部分设为0。

    在移动位数为负数的情况下,上述代码相当于:elements &= -1L >>> (64-universe.length)。如universe.length 为 7,则 -1L>>> ( 64-7 ) 就是二进制的 1111111,与 elements 相与,就会将超出universe.length部分的高 57 位都变为0。

     

     

    第六节 相关工具类

    1. Collections

    Collections工具类以静态方法的方式提供了很多通用算法和功能。

     

    1) 对容器进行操作

    针对容器接口的通用操作,这是面向接口编程的一种体现,是接口的典型用法。

     

    2) 返回一个容器

    目的是为了使更多类型的数据更为方便和安全地参与到容器类协作体系中。

    注意:为什么使用了泛型后还会有类型安全问题呢? 因为Java是通过擦除来实现泛型的,类型参数是可选的,并且JDK5前的老代码都没有泛型。

     

    3) 其它

     

     

    第七节 面试补充

    1. List相关

    1) ArrayList与Array(数组)的区别?

     

    2) ArrayList与LinkedList的区别?

     

    3) ArrayList能存储null值吗?

     

    4) 关于Vector和Stack

     

     

    2. Set相关

    1) HashSet vs LinkedHashSet vs TreeSet

     

     

    3. Queue相关

    1) ArrayDeque 与 LinkedList 的区别

     

     

    4. Map相关

    1) HashMap的长度为什么是 2 的幂次方?

     

    2) HashMap多线程操作会导致死循环?

     

    3) ConcurrentHashMap 能保证复合操作的原子性吗?

     

     

    5. 其它

    1) 正确判断集合是否为空

     

     

    第05章_异常

    第一节 异常类

    1. 异常类简介

    异常指程序运行过程中出现的错误,以java.lang.Throwable为根,Java定义了非常多的异常: image-20221111181541923

    Throwable:是所有异常的基类。它有两个主要子类:java.lang.Errorjava.lang.Exception

     

     

    2. 自定义异常类

    应用程序可以通过继承Exception或其子类创建自定义异常。特别的,如果继承的是RuntimeException,那么创建的将会是未受检异常。

     

     

    第二节 异常处理

    1. 抛出异常(throw)

    throw用来抛出一个异常对象,并将这个异常对象传递到调用者处,并结束当前方法的执行。

     

    2. 捕获异常(try…catch)

    异常抛出后,会沿着方法栈往调用者传递,我们可以对其进行捕捉和处理。

    异常捕捉后,可以获取异常相关的信息,如下:

    如需捕捉多个异常,则可以按照如下格式书写,注意越明确的类型应越先捕捉

    注意:

    1. 如果异常一直未被捕捉,最后会被Java虚拟机处理,默认行为是打印堆栈信息,然后退出线程

     

     

    3. 声明异常(throws)

    对于受检异常,如果未在当前方法进行捕捉,则必须通过throws关键字在方法上进行声明,提醒调用者处理异常。

    注意:

    1. 子类方法不能声明或抛出父类方法中未声明的异常。

    2. 你可以声明抛出异常,但实际并不抛出,这一般用在在父类方法,方便子类进行扩展。

     

     

    4. finally代码块

    try后面还可以跟finally语句,finally内的代码不管有无异常发生,都会执行,一般用于释放资源,如数据库连接、文件流等。

    注意

    1. 如果程序被突然终止(宕机、断电等)或在try/catch中调用了退出JVM相关的方法,则finally代码块不会被执行。

    2. 如果某些资源即使在程序退出后也不能自动释放,则不能依赖finally代码块,如持久化存储的业务标记。

    3. 如果finally代码块中有return语句或抛出异常,则会覆盖try代码块中的返回结果,应避免该情况。

     

     

    5. try-with-resources(JDK7+)

    try-with-resources语句配合java.lang.AutoCloseable接口,可以实现资源的自动关闭(基于finally代码块实现)。

     

    6. try-with-resources(JDK9+)

    在Java 9之前,资源必须声明和初始化在try语句块内,Java 9去除了这个限制,资源可以在try语句外被声明和初始化,但必须是final的或者是事实上final的(即虽然没有声明为final但也没有被重新赋值)。

     

     

    第三节 异常相关扩展

    1. 异常链

    在catch代码块中可重新抛出异常,异常可以是原来的,也可以是新建的,并且可以关联原来的异常形成异常链。

    上述案例中,捕捉到NumberFormatException异常后,转化为统一的BizException重新抛出,并将exception作为cause传递给了新建的BizException,这样就形成了一个异常链,捕获到BizException的代码可以通过getCause()得到底层的NumberFormatException。

    某些Java的异常类并没有定义带cause的构造方法,但可以通过Throwable的Throwable initCause(Throwable cause)方法来设置cause,但是必须注意,该方法只能被调用一次。

     

     

    2. 异常与枚举结合

     

     

    3. 特殊情况下的finally

    如果在try或者catch语句内有return语句,则return语句执行后的结果先会缓存,待finally语句执行结束后才返回(但是该值不能被改变)。

    如果在finally中也有return语句呢? 那么try和catch内的return会丢失,实际会返回finally中的返回值。finally中有return不仅会覆盖try和catch内的返回值,还会掩盖try和catch内的异常,就像异常没有发生一样。

    同理,如果finally代码块中抛出了异常,则原返回值或异常也将会被掩盖。

    因此,应该尽量避免在finally中使用return语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。

     

     

    第06章_文件

    第一节 文件概述

    1. 基础概念

    文件:文件是操作系统对磁盘数据的抽象,方便用户进行数据管理。

    文件存储:文件在磁盘上以二进制形式进行存储,根据解读方式的不同,可分为UTF-8文本文件、JPG图片文件、MP4视频文件、ZIP压缩文件等多种类型,一般以后缀名进行标识。

    文本文件:如果文件能以某种编码(UTF-8、GBK等)映射为可读的字符形式,那么该类文件称为文本文件。文本文件具有换行的概念,在Windows系统中使用\r\n这2个字节表示换行符(Linux为\n,MAC系统为\r)。

    注意:

    1. 在Windows系统中,文件名是大小写不敏感的,即同目录下的a.txt和A.txt是同一个文件。

    2. 文件IO比较慢,且需经过内核态和用户态的两次复制,因此文件操作时一般按块进行,并设置一定大小的缓冲区。

     

    2. 文件与目录(File)

    1) File类

    java.io.File 类封装了操作系统和文件系统的差异,提供了统一的文件和目录API。它可以表示文件,也可以表示目录,构造方法如下:

    File 类中有 4 个静态变量, 表示路径或目录的分隔符

     

    2) 文件基本信息

    注意, File 对象没有返回创建时间的方法 , 因为创建时间不是一个公共概念 , Linux/Unix 就没有创建时间的概念。

     

    3) 文件安全与权限

    File 类中与安全和权限相关的主要方法有:

     

    4) 常用文件操作

    当 File 对象代表文件时,主要操作有创建 、 删除 、 重命名等。

     

    5) 常用目录操作

    当 File 对象代表目录时,可以执行目录相关的操作,如创建、遍历等。

     

    6) 文件和目录操作案例

     

     

    第二节 字节流

    在Java中,将文件及其它输入输出设备抽象为,并构建了基于流的相关协作体系,默认情况下,流为字节形式,称为字节流

     

    1. InputStream/OutputStream

    InputStream/OutputStream(抽象类)表示最顶层的字节输入流和字节输出流,其中定义了它们的一些共性方法:

     

    2. FileInputStream/FileOutputStream

    FileInputStream/FileOutputStream继承自InputStream/OutputStream, 表示文件输入流文件输出流,即输入输出目的地为文件。

    下面是一些按字节读写文件的示例:

     

    3. ByteArrayInputStream/ByteArrayOutputStream

    ByteArrayInputStream/ByteArrayOutputStream也继承自InputStream/OutputStream, 表示字节数组输入流字节数组输出流,即输入输出目的地为字节数组。

    下面示例将从文件输入流读取数据到字节数组输出流,然后转化为字符串输出。

     

    4. DataInputStream/DataOutputStream

    DataInputStream/DataOutputStream是装饰类基类FilterInputStream/FilterOutputStream的子类,并且实现了DataInput/DataOutput接口,可以以各种基本类型和字符串读取或写入数据

    下面是一个使用DataInputStream/DataOutputStream装饰FileInputStream/FileOutputStream后,用来序列化对象的使用示例:

     

    5. ObjectInputStream/ObjectOutputStream

    ObjectInputStream/ObjectOutputStream继承自InputStream/OutputStream,并实现了ObjectInput/ObjectOutput接口,可以读取和写入实现了java.io.Serializable接口的对象

    下面是一个使用ObjectInputStream/ObjectOutputStream来写入和读取对象的示例:

    实际上,List以及之前介绍的String、Date、Double、Map等, 都实现了Serializable接口,上述示例可以再次简化:

    扩展:

    1. ObjectInput/ObjectOutput是DataInput/DataOutput的子接口,增加了Object readObject()void writeObject(Object obj)方法。

     

    6. BufferedInputStream/BufferedOutputStream

    BufferedInputStream/BufferedOutputStream也是装饰类基类FilterInputStream/FilterOutputStream的子类,它提供了对流进行缓冲的作用,提升操作流的性能。

    在使用FileInputStream/FileOutputStream时,应该几乎总是在它的外面包上对应的缓冲类,如下所示:

    提示:

    1. BufferedInputStream/BufferedOutputStream是对InputStream/OutputStream的装饰,使用了装饰器模式

     

    7. RandomAccessFile

    如果需要对文件进行随机读写重复读,可以使用RandomAccessFile,它一个更接近于操作系统API的封装类。

    注意:

    1. 虽然RandomAccessFile有类似于读写字节流的方法,但大多是实现DataInput/DataOutput接口而来,并不是InputStream/OutputStream的子类。

     

    8. MappedByteBuffer

    如果需要处理大型文件或在不同应用程序之间共享数据,可以使用MappedByteBuffer,它是文件映射到内存的字节数组,操作该字节数组即可操作文件,大多数操作系统都支持该机制,称为内存映射文件

    内存映射文件基于FileInputStream/FileOutputStream或RandomAccessFile,它们都有一个获取FileChannel方法,而FileChannel可以将文件映射到内存,映射完成后,文件就可以关闭了,后续对文件的读写可以通过MappedByteBuffer完成。

    注意:

    1. 映射模式受限于文件打开的方式,若是输入流或写模式打开文件,则不能设置为READ_WRITE映射模式。

    2. 内存映射文件仅在发生实际读写时,才会将要读写的部分按页映射到内存。数据读写完毕后,由操作系统进行同步,只要操作系统不崩溃,一定可以同步到磁盘上,即使应用程序已经退出。

    3. 在该种方式下,程序直接访问内核内存空间,仅需一次数据拷贝过程,比普通文件读写的性能更高

    4. 内存映射文件也有局限性,比如,它不太适合处理小文件,它是按页分配内存的,对于小文件,会浪费空间,另外,映射文件要消耗一定的操作系统资源,初始化比较慢。

    MappedByteBuffer代表内存中的字节数组,是 ByteBuffer(Buffer) 的子类,它可以简单理解为一个字节数组包装类,这个字节数组的长度是不可变的,在内存映射文件中,这个长度由map方法中的参数size决定。

    内存映射文件的另一个重要特点是,它可以被多个不同的应用程序共享,多个程序可以映射同一个文件,映射到同一块内存区域,一个程序对内存的修改,可以让其他程序也看到,这使得它特别适合用于不同应用程序之间的通信

     

    9. 字节流操作实用方法

     

     

    第三节 字符流

    字符流以字符为单位读取和解读流中的字节,一个字符可能包含多个字节,这取决解读时使用的字符编码。

    注意:对于增补字符集,一个完整的字符内容可能需要两个字符(char)来表示。

     

    1. Reader/Writer

    Reader/Writer(抽象类)表示最顶层的字符输入流字符输出流,其中定义了它们的一些共性方法:

     

    2. InputStreamReader/OutputStreamWriter

    InputStreamReader/OutputStreamWriter是适配器类,继承自Reader/Writer,能将字节流(InputStream/OutputStream)转换为字符流(Reader/Writer)

    下面是一个将字节流适配为字符流并进行字符读写的示例:

    提示:

    1. InputStreamReader和OutputStreamWriter分别是字节流和字符流之间的适配器,使用了适配器模式

     

    3. FileReader/FileWriter

    FileReader/FileWriter继承自Reader/Writer, 表示文件字符输入流文件字符输出流,即输入输出目的地为文件。

    注意:

    1. FileReader和FileWriter以及下面介绍的几种字符流操作类,都不能直接指定编码类型,只能使用默认编码。

    2. 如需指定字符流的编码类型,可以使用适配器InputStreamReader/OutputStreamWriter将字节流转换为指定编码的字节流。

     

    4. CharArrayReader/CharArrayWriter

    CharArrayReader/CharArrayWriter也继承自Reader/Writer, 表示字符数组输入流字符数组输出流,即输入输出目的地为字符数组。

    下面是一个从文件字符流中读数据到字节数组输出流的示例:

     

    5. StringReader/StringWriter

    StringReader/StringWriter也继承自Reader/Writer, 表示字符串输入流字符串输出流,即输入输出目的地为字符串。它与CharArrayReader/CharArrayWriter类似,只是输入源为String,输出目标为StringBuffer。实际上,String和StringBuffer内部是由char数组组成的,所以它们本质上是一样的。

     

    6. BufferedReader/BufferedWriter

    BufferedReader/BufferedWriter是装饰类,直接继承自Reader/Writer,提供缓冲以及按行读写的功能。

    注意:

    1. 通过System.lineSeparator()也可以获取平台特定的换行符。

    2. FileReader/FileWriter是没有缓冲的,也不能按行读写, 因此一般应该在它们的外面包上对应的缓冲类。

    3. BufferedReader/BufferedWriter是Reader/Writer的装饰,使用了装饰器模式

    下面是一个带缓冲的文件字符流的读写示例:

     

    7. PrintWriter/PrintStream

    PrintWriter继承自Writer,是一个非常方便的类,可以直接指定文件名/File/OutputStream/Writer等作为构造参数,还可以指定编码类型,支持自动缓冲,可以自动将多种类型转换为字符串,在输出到文件时 ,可以优先选择该类。

    注意:

    1. 如果以Writer为参数的构造方法,则PrintWriter就不会包装BufferedWriter了,其它类型参数则会。

    下面是使用PrintWriter改造上面写学生信息的示例:

    PrintStream继承自FilterOutputStream,属于字节流,但其功能与PrintWriter非常的相似。一些差异点如下:

     

    8. Scanner

    Scanner是一个单独的类,它是一个简单的文本扫描器,能够从流中提取基本类型和字符串。

    使用Scanner改造上面解析每行学生信息的示例如下:

     

    9. 标准流

    操作系统在启动时通常会打开三个标准流:

    System.in:标准输入流(InputStream),一般指键盘,可以和Scanner配合使用,从键盘输入数据。

    System.out:标准输出流(PrintStream),一般指控制台,输出提示信息。

    System.err:标准错误流(PrintStream),一般也是控制台,输出错误信息,如使用e.printStackTrace()打印异常信息。

    标准流可以重定向,如将标准输入流重定向到文件,从文件中接受输入,或将标准输出流(错误流)重定向到文件,将输出写到文件。

    标准输入输出流也是操作系统的重要协作机制,命令从标准输入接受参数,处理结果写到标准输出,这个标准输出可以连接到下一个命令作为标准输入,构成管道式的处理链条。

     

    10. 字符流操作实用方法

     

     

    第四节 常见文件类型处理

    1. Properties

    Properties文件一般用于配置程序的属性参数,每一行表示一个属性,属性是以等号(=)或冒号(:)分隔的键值对,如下例所示:

    Java中有一个专门的类Properties来处理该类属性文件,它会自动忽略文件中的空行和注释行(#或!开头)以及分隔符前后的空格。

    一个使用Properties加载属性文件并获取属性的示例如下:

    值得注意的是,Properties不能直接处理中文,在配置文件中,所有非ASCII字符需要使用Unicode编码,如name=老马需替换为name=\u8001\u9A6c。如果你使用IDE进行编辑,或许它会帮你自动转换,不过,你也可以使用JDK命令:native2ascii -encoding UTF-8 native.properties ascii.properties进行转换。

     

    2. CSVFormat/CSVPrinter

    CSV(Comma-Separated Values)文件一般用于表示表格类型的数据,每一行表示一条记录,记录包含多个字段,字段之间用逗号、制表符、冒号、分号等分隔。

    如果字段内容包含分隔符或换行符等特殊字符,主要有两种方式处理:

    1. 方式一:使用特殊符号如双引号(")将字段内容括起来,如果字段内容有",则用两个"表示。

    2. 方式二:使用转义字符如反斜杠()对特殊字符进行转义,如果字段内容有\,则用两个\表示,如hello\, world \\ abc'n"老马"

    CSV文件需要处理转义字符、空格、null值以及注释等复杂情形,可以采用Apache Commons CSV库来解析CSV文件,导入依赖如下:

    解析CSV文件主要依赖CSVFormat类,有一些预定义的格式,如CSVFormat.DEFAULTCSVFormat.RFC4180等,也可以通过如下一些方法自定义CSVFormat对象。

    写CSV文件,可以使用 CSVPrinter 类,它有许多打印相关的方法:

    下面是一个读取和写入CSV文件的示例:

     

    3. Workbook(Excel)

    Excel是广泛使用的表格文档格式,通常使用POI类库来进行处理,主要的类如下:

    类名说明
    WorkbookExcel文件(接口),HSSFWork-book和XSSFWorkbook实现类分别表示.xls文件和.xlsx文件
    Sheet工作表
    Row数据行
    Cell单元格

    使用POI类库前先导入对应的依赖如下:

    下面是一个简单的Excel文件读取和写入示例:

     

    4. Jsoup(HTML)

    Jsoup是一种常用的HTML分析器,Maven依赖如下:

    下面是使用Jsoup解析URL的示例:

     

    5. GZIPOutputStream/GZIPInputStream

    Java内置了gzipzip两种压缩格式的支持,其中gzip只能压缩一个文件,而zip文件中可以包含多个文件。

    压缩和解压gzip文件使用GZIPOutputStreamGZIPInputStream装饰类,它们分别继承自DeflaterOutputStream(FilterOutputStream)和InflaterInputStream(FilterInputStream)。

     

    6. ZipOutputStream/ZipInputStream

    压缩和解压zip文件使用ZIPOutputStreamZIPInputStream装饰类,也继承自DeflaterOutputStream(FilterOutputStream)和InflaterInputStream(FilterInputStream),但是使用起来稍微复杂些。

     

    第五节 扩展:关于JDK序列化

    1. JDK序列化简介

    序列化就是将对象转化为字符流/字节流反序列化就是将字符流/字节流转化为对象,主要有两个用途:一个是对象持久化;另一个是跨网络的数据交换和远程过程调用。

    在标准JDK中,通过ObjectInputStream`ObjectOutputStream流提供了基于java.io.Serializable接口的序列化机制。 它有很多优点,使用简单,可自动处理对象引用和循环引用,也可以方便地进行定制,处理版本问题等,但它也有一些重要的局限性:

    由于这些局限性,在跨语言的数据交换格式中,经常采用XML或JSON格式,它们清晰易读,各种语言基本都支持,缺点是性能和序列化大小。在性能和序列化大小敏感的领域,往往会采用更为精简高效的二进制方式,如ProtoBuf、Thrift、MessagePack等 。

    注意:

    1. 如果尝试序列化未实现Serializable接口的对象,那么将会抛出java.io.NotSerializableException

    2. 如果 a、b 两个对象都引用同一个对象 c ,序列化后c 只会保存一份 , 并且反序列化后依然指向相同对象。

    3. 如果 a 、 b 两个对象有循环引用,即 a 引用了 b , 而 b 也引用了 a,反序列化后依然 可以保持引用关系。

     

    2. 配置JDK序列化

    默认的序列化机制将对象中的所有字段保存和恢复,但某些字段信息,如对象的创建时间,默认hashcode()返回值等并不需要保存,我们可以将字段声明为 transient,则默认的序列化机制将会忽略它。如 LinkedList 中的这些字段:

    之后,我们可以在类中定义 writeObject/readObject 方法来自己保存该字段。

    如 LinkedList 的序列化和反序列化代码如下:

     

    3. 关于类的版本

    默认情况下,Java根据类中一系列的信息自动生成一个版本号, 如果类的定义发生了变化 , 版本号就会变化,如果反序列化时的版本号不一致,则会抛出java.io.InvalidClassException

    我们可以手动在类中添加如下静态变量来标识类的版本,而非由Java自动生成,以便更好地控制序列化的版本和节省性能。

    如果版本号一致,但实际的字段不匹配,Java 会分情况自动进行处理 , 以尽量保持兼容性。

     

     

    第07章_注解

    第一节 注解简介

    1. 什么是注解?

    注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等,注解可以被编译器、程序运行时、和其他工具使用,用于增强或修改程序行为等

     

    2. 注解的本质

    注解本质上就是一个接口,该接口默认继承Annotation接口,我们使用javap将生成的注解class文件反编译后,可以看到如下内容:

     

     

    3. 元注解

    元注解是一种用于修饰注解的注解,常用的元注解如下:

     

    1) @Target

    @Target表示注解的目标,取值为一个或多个ElementType枚举值。如果没有声明@Target,默认为适用于所有类型。

     

    2) @Retention

    @Retention表示注解信息保留到什么时候,取值为一个RetentionPolicy枚举值。如果没有声明@Retention,默认为CLASS。

     

    3) @Inherited

    @Inherited表示注解将会被子类继承。如下示例中,Child类并没有直接声明Test注解,但依然检测其存在。

     

    4) @Documented

    @Documented表示将注解信息包含到Javadoc中。

     

    5) Repeatable

    @Repeatable表示可以在同一个地方多次应用该注解。

     

     

    4. 内置注解

    1) @FunctionInterface

    可修饰接口,用于检查被标注的接口是否为函数式接口(只有一个抽象方法的接口)。

     

    2) @Override

    可修饰方法,表示该方法是“重写”方法,可以减少编程错误(如父类方法名修改后,若子类方法名忘记修改,存在注解时将报错)。

     

    3) @Deprecated

    可修饰方法字段参数等,表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的。

     

    4) @SuppressWarnings

    可修饰方法等,用于压制Java的编译警告,通过必填参数设置压制的类型。

     

     

    第二节 自定义注解

    1. 注解格式

    注解的定义和接口类似,格式如下:

     

    2. 注解参数

    注解本质上就是一个接口,定义注解参数即在接口中定义抽象方法,其中方法名表示参数名,返回值类型表示参数的类型

    注意:

    1. 参数的类型必须为如下类型:基本类型(不包括包装类型)、StringClass枚举注解,以及这些类型的数组

    2. 参数可以通过default关键字指定默认值,默认值必须为一个常量,不能为null

    3. 如提供了参数,但未指定默认值,则必须在使用注解时提供具体的值(不能为null)。

     

     

    第三节 使用和解析注解

    1. 使用注解

    查看元注解@Target的参数值,明确注解可使用的位置,然后在目标位置添加注解并填充参数。

    只有一个参数,且名称为value时,提供参数值时可以省略"value="。

    数组赋值时,值使用{}包裹,如果数组中只有一个值,则{}可以省略。

     

    2. 解析注解

    注解只是对程序的标识,创建注解后,我们应同时提供处理这些标识的其它代码,以使添加的注解生效。我们主要考虑@Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。

    一个简单的示例如下:

     

     

    第四节 注解应用案例

    1. 定制序列化

     

    2.DI容器

     

    3. DI容器-支持单例

     

    4. 简单测试框架

     

     

    第08章_反射

    第一节 反射的概念

    1. 什么是反射?

    一般来说,在操作某个数据的时候,我们都是知道并且依赖于数据的类型的,并且编译器也是根据其类型,进行代码的检查和编译。如:

    1. 根据类型使用new创建对象。

    2. 根据类型定义变量,类型可能是基本类型、类、接口或数组。

    3. 将特定类型的对象传递给方法。

    4. 根据类型访问对象的属性,调用对象的方法等。

    但是反射不一样,它是在运行时(而非编译时)动态获取类型的信息,如接口信息、成员信息、方法信息、构造方法信息等。这些信息使用Class<T>类进行封装,获取Class类后就可以创建对象、访问和修改成员、调用方法等。

     

    2. 获取Class类

    在Java中,每个已加载的类在内存都有一份类信息,使用Class类进行封装,每个对象都有指向它所属类信息的引用。获取方法如下:

    特殊的,基本类型没有getClass()方法,但也都有对应的Class对象,Class的类型参数为对应的包装类型:

    void作为特殊的返回类型,也有对应的Class:

    对于数组每种类型及每个维度都有对应数组类型的Class对象:

    有了Class对象后,我们就可以了解到关于类型的很多信息,并基于这些信息采取一些行动,下面会分组进行介绍。

    注意:

    1. 同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

     

    3.Class的种类

    Class对象代表的类型既可以是普通的类,也可以是内部类,还可以是基本类型、数组等,可以通过以下方法进行区分 :

     

    4. 慎用反射

    反射虽然是灵活的,但一般情况下,并不是我们优先建议的,主要原因是:

    简单的说,如果能用接口实现同样的灵活性,就不要使用反射。

    另外,反射也不是万能的,有些信息无法通过反射获取,如类字段的顺序,方法的参数名称(需要手动在编译时开启 -parameters 参数)等,有些信息即使反射获得后也不能使用,如Unsafe.getUnsafe()方法,在业务代码中是不能调用的。

     

     

    第二节 反射信息

    1. 类信息

    Class有如下方法,可以获取与类名称有关的信息:

    类名称之间的不同可参考如下表格:

    image-20230302100903584

    关于数组类型getName()返回值的说明

    1. 格式为:数组维度+数据类型,其中数组维度用[表示,有几个[表示是几维数组。

    2. 数据类型可以是基本类型,有:boolean(Z), byte(B), char(C), short(S), int(I), long(J), float(F), double(D)。

    3. 数据类型也可以是引用类型(类或接口等),用L+全类名+;表示。

     

    2. 类字段信息

    类中定义的静态变量实例变量都被称为字段,字段信息用Field类封装,可通过Class类的如下方法获取:

    获取Field类对象后,即可通过其方法获取字段信息及修改字段内容。

    注意:

    1. 对于静态变量,get/set方法的obj参数直接传null即可。

    2. private字段不允许直接调用get/set方法,需要先setAccessible(true)关闭Java的检查机制,否则会抛IllegalAccessException。

    3. 如果字段值为基本类型,get/set会自动在基本类型与对应的包装类型间进行转换。

     

    3. 类方法信息

    类中定义的静态方法实例方法都被称为方法,用Method类封装,可通过Class类的如下方法获取:

    获取Method对象后,即可通过其方法获取方法信息及调用方法等。

    关于invoke方法的使用有如下几点注意事项:

    1. 对于静态方法,invoke时obj参数直接传null即可。

    2. invoke方法的参数args可以为null,也可以为一个空数组,返回值被包装为Object类型。

    3. 如果目标方法调用抛出异常,将会被包装为InvocationTargetException重新抛出,可以通过getCause方法得到原异常。

    下面是一个使用invoke调用静态方法的示例:

     

     

    4. 关于修饰符

    获取修饰符时,得到的是一个int类型,可通过Modifier类的如下方法进行解析:

     

     

    第三节 反射操作

    1. 创建对象

    获取类信息(Class)后,可以使用其获取构造器和创建对象

     

    2. 类型检查和转换

    前面介绍过,instanceof关键字可以用来判断引用指向的实际对象类型,但是instanceof后面的类型是在代码中确定的,如果要检查的类型是动态的,可以使用Class类的isInstance方法,效果是一样的:

    isInstance判断的是对象和类之间的关系,Class还有一个方法isAssignableFrom可以判断类与类之间的关系:

    在程序中也往往需要进行强制类型转换,而强制转换到的类型要在写代码时就知道的,如果是动态的,可以封装为如下toType方法:

    3. 类的加载

    Class有三个重载静态方法,可以根据类名加载类:

    其中className与Class.getName()的返回值是一致,如加载String类型的一维数组使用[java.lang.String;

    需要注意的是,基本类型不支持forName方法:

     

     

    第四节 反射扩展

    1. 反射与数组

    对于数组类型的Class,有一个专门的方法,可以获取它的元素类型:

    另外,java.lang.reflect包中专门提供了一个针对数组反射操作的类Array,以便于统一处理多种类型的数组,主要方法有:

     

    2. 反射与枚举

    对于枚举类型的Class,有一个专门方法 , 可以获取所有的枚举常量:

     

    3. 反射与内部类

    对于内部类类型的Class,也有一些特殊的方法:

     

    4. 反射与泛型

    虽然泛型在运行时会被擦除,但在类信息Class中仍然有关于泛型的一些信息,可以通过反射获取。

    其中Type是一个接口,Class实现了Type,Type的其他子接口还有:

    一个简单的使用示例如下:

     

    5. 反射与注解

     

     

    第五节 应用示例

    1. SimpleMapper

    如下示例使用反射实现一个简单的通用序列化/反序列化类SimpleMapper:

     

     

    第六节 Unsafe类

    在Java的底层源码中,存在一个sun.misc.Unsafe类,也可以用于创建对象。

    由于它的构造方法是私有的,也没有暴露外部对象,因此只能通过反射来获取,示例如下:

    必须注意的是,他直接调用的底层C++代码,跳过了Java的对象管理和内存管理以及垃圾回收等机制,不会调用Java类的构造方法(可能突破单例模式限制),并且可能造成内存泄漏,因此请谨慎使用。

    注意:

    1. 不能在业务代码中调用Unsafe.getUnsafe(),将会抛出SecurityException,因为该方法被@CallerSensitive注解。

     

     

    第09章_函数式编程

    第一节 Lambda表达式

    1. Lambda表达式引入

    接口常作为方法的形参来传递代码,如Collections.sort方法的Comparator类型参数:

    它真实需要的不是一个Comparator对象,而是在对象的int compare(T o1, T o2)方法中包含的大小比较逻辑。

    但由于无法直接传递代码,因此只能传递一个具有该功能的对象。在Java 8之前,最简洁的方式是使用匿名内部类构建一个对象:

    在Java8,引入了Lambda表达式,它是一种紧凑的代码传递方式,传递代码不再有实现接口的模板代码,而是直接给出了方法的实现代码,变得更为直观。

    注意:

    1. 虽然Lambda表达式非常简洁,但它只支持函数式接口,其它的接口类型还需使用匿名内部类。

     

    2. Lambda表达式语法

    Lambda表达式由->分隔为两部分,前面()内是方法的参数列表,后面{}内是方法的实现代码。其中参数列表由函数式接口的抽象方法决定,必须保证参数的类型和顺序完全一致,但参数名称不做要求。

    编译器会尽可能的对Lambda表达式进行推断,以简化其书写:

    注意:

    1. 如需在Lambda表达式中访问局部变量,则该变量必须是final的,或等效final的(因为该变量是通过构造参数直接传入)。

    2. Lambda表达式不是匿名内部类的语法糖,它是基于invokedynamic指令实现的,并不会生成很多类。

     

    3. Lambda使用场景

     

     

    第二节 函数式接口

    1. 什么是函数式接口?

    函数式接口只有一个抽象方法的接口,一般使用@FunctionalInterface进行注解(非强制,与@Override注解类似)。

    注意:

    1. 函数式接口允许有多个非抽象方法。

     

    2. 预定义函数式接口

    Java 8 预定义了大量的函数式接口,用于常见类型的代码传递,这些函数定义在java.util.function包下,主要的有:

    img

    对于基本类型boolean/int/long/double,为避免装箱和拆箱,Java 8 提供了一些专门的函数。比如,int相关的主要函数有:

    img

     

    3. 函数式接口使用示例

    为便于举例,我们定义一个简单的学生类Student,以及一个Student列表:

     

    1) Predicate

     

    Predicate接口提供了and/or/negate三个方法用于组合其它Predicate。

     

    2) Function

     

    Function提供了andThen/compose分别用于后置/前置组合其它Function。

    注意:

    1. 组合后,前面函数式接口的输出必须兼容后面函数式接口的输入,如one.compose(two)是错误的,Integer不能赋值给String。

     

    3) Consumer

     

    Consumer提供了andThen用于后置组合其它Consumer。

     

     

    第三节 方法引用

    1. 什么是方法引用

    方法引用是Lambda表达式的进一步简化。前面说到,Lambda表达式用于传递一段代码,如果这段代码在其它地方已经存在,则可以通过类名/变量名::方法名的格式直接引用。

     

    2. 引用静态方法

    可以通过类名引用静态方法,要求被引用的方法和抽象方法的形参列表及返回值完全一致。

     

    3. 引用构造方法

    也可以通过类名引用构造方法,和引用静态方法要求相同,即被引用的方法和抽象方法的形参列表及返回值完全一致。

     

    4. 引用实例方法

    1) 通过类名

    可以通过类名引用实例方法,但由于实例方法必须通过实例变量调用,因此只能引用抽象方法第一个形参类型中的实例方法,并且剩余形参列表和返回值也要求和引用方法完全一致。

    在运行时,抽象方法第一个参数不作为引用方法的参数传入,而用于调用该引用方法。

     

    2) 通过变量名引用

    通过变量名引用它的任意实例方法,它将通过该变量进行调用。

    注意:

    1. 这个变量名也可以是super或者this

     

     

    第四节 流式编程

    流式编程通常是对集合数据进行处理,让集合中的对象像水流一样流动,分别进行去重过滤映射等操作,就像生产线一样。

    对此,Java 8 引入了一套新的类库,位于包java.util.stream下,称之为Stream API。它有如下一些特征:

     

    1. 获取流

    1)从集合获取流

    Stream API的主要操作定义在Stream接口中,他类似于一个功能更加丰富的迭代器,可以通过Collection接口(JKD8+)的默认方法获取:

    注意:

    1. 顺序流采用单线程处理,并行流并行处理,线程个数一般与系统的CPU核数一样,以充分利用CPU的计算能力。

    2. 并行流的实现基于Java 7引入的fork/join框架,处理由fork和join两个阶段组成,fork就是将要处理的数据拆分为小块,多线程按小块进行并行计算,join就是将小块的计算结果进行合并。

     

    2) 从数组获取流

    Arrays有一些stream方法,可以将数组或子数组转换为流,比如:

     

    3) 构建流

    Stream有一些静态方法,可以构建流:

    一些简单示例如下:

     

    4) 合并流

    如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :

     

     

    2. 中间操作

    中间操作(intermediate operation) 不触发实际的执行,用于构建流水线,返回的是Stream对象。

     

    1) filter

    过滤不符合条件的元素。

     

    2) map

    将元素转换为其它类型。

    map函数接受的参数是一个Function<T, R>,为避免装箱/拆箱,提高性能,Stream还有如下返回基本类型特定流的方法:

    DoubleStream/IntStream/LongStream是基本类型特定的流,有一些专门的更为高效的方法。比如,求学生列表的分数总和,代码可以为:

     

    3) distinct

    过滤重复的元素,只留下其中一个,是否重复是根据equals方法来比较的。

    虽然都是中间操作,但distinct与filter/map是不同的,filter/map都是无状态的,对于流中的每一个元素,它的处理都是独立的,处理后即交给流水线中的下一个操作。

    但distinct不同,它是有状态的,在处理过程中,它需要在内部记录之前出现过的元素,如果已经出现过,即重复元素,它就会过滤掉,不传递给流水线中的下一个操作。

    对于顺序流,内部实现时,distinct操作会使用HashSet记录出现过的元素,如果流是有顺序的,需要保留顺序,会使用LinkedHashSet。

     

    4) sorted

    对流中的元素进行排序,要求元素实现Comparable接口或传入一个Comparator。

    与distinct一样,sorted也是一个有状态的中间操作,在处理过程中,需要在内部记录出现过的元素,与distinct不同的是,每碰到流中的一个元素,distinct都能立即做出处理,要么过滤,要么马上传递给下一个操作,但sorted不能,它需要先排序,为了排序,它需要先在内部数组中保存碰到的每一个元素,到流结尾时,再对数组排序,然后再将排序后的元素逐个传递给流水线中的下一个操作。

     

    5) skip/limit

    skip跳过流中的n个元素,如果流中元素不足n个,返回一个空流。limit限制流的长度为maxSize,用它们组合可以截取第n+1 ~ n+maxSize的元素。

    skip和limit都是有状态的中间操作。对前n个元素,skip的操作就是过滤,对后面的元素,skip就是传递给流水线中的下一个操作。

    limit的一个特点是,它不需要处理流中的所有元素,只要处理的元素个数达到maxSize,后面的元素就不需要处理了,这种可以提前结束的操作被称为短路操作

     

    6) peek

    peek主要目的是支持调试,可以使用该方法观察在流水线中流转的元素。

     

    7) flatMap

    接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换为一个流Stream,然后把新生成流的每一个元素传递给下一个操作,完成了一个1到n的映射。

    相应的,针对基本类型,flatMap还有如下类似方法:

     

    3. 终端操作

    终端操作(terminal operation) 触发实际执行,返回具体结果。

     

    1) max/min

    返回流中的最大值/最小值,值的注意的是,它的返回值类型是Optional<T>,而不是T,表示可能返回null(在流中不含任何元素的情况下)。

     

    2) count

    返回流中元素的个数。

     

    3) allMatch/anyMatch/noneMatch

    接受一个谓词Predicate,返回一个boolean值,用于判定流中的元素是否满足一定的条件,它们的区别是:

    如果流为空,这几个函数的返回值都是true

    这几个操作都是短路操作,都不一定需要处理所有元素就能得出结果,比如,对于allMatch,只要有一个元素不满足条件,就能返回false。

     

    4) findFirst/findAny

    返回类型都是Optional,如果流为空,返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素,它们都是短路操作。

     

    4) forEach/forEachOrdered

    接受一个Consumer,对流中的每一个元素,传递元素给Consumer,区别在于,在并行流中,forEach不保证处理的顺序,而forEachOrdered会保证按照流中元素的出现顺序进行处理。

     

    5) toArray

    将流转换为数组。

     

    6) reduce

    代表归约或折叠,即将流中的元素归约为一个值,完成n到1的映射。它有三个重载形式,使用它们可以实现max/min/count等函数:

    第一个基本等同于调用:

    比如,使用reduce求分数最高的学生(max),代码可以为:

    第二个reduce函数多了一个identity参数,表示初始值,它基本等同于调用:

    第一个和第二个reduce的返回类型只能是流中元素的类型,而第三个更为通用,它的归约类型可以自定义

    另外,它多了一个combiner参数,combiner用在并行流中,用于合并子线程的结果,对于顺序流,它基本等同于调用:

    注意与第二个reduce函数相区分,它的结果类型不是T,而是U。

    以上,可以看出,reduce虽然更为通用,但比较费解,难以使用,一般情况,应该优先使用其他函数。

     

     

    4. 收集器

    1) 基本原理

    在之前的代码中,如过滤得到90分以上的学生列表:

    最后的collect方法是如何将Stream转换为List<Student>的呢?先看下collect方法相关的定义:

    对于顺序流,collect内部与这些接口方法的交互大概是这样的:

    Collectors.toList()具体是什么呢?看下代码:

    也就是说,collect(Collectors.toList())背后的伪代码如下所示:

     

    2) 容器收集器

     

     

    3) 字符串收集器

    除了将元素流收集到容器中,另一个常见的操作是收集为一个字符串。

     

    5. 分组收集器

    分组类似于SQL语句中的group by子句,它将元素流中的每个元素进行分组,然后针对分组进行处理和收集。

     

    1) 简单分组

    最基本的分组收集器及其示例如下:

     

    2) 基本原理

    跟踪groupingBy的源代码如下:

    对最后一个重载的groupingBy方法返回的收集器,其收集元素的基本过程和伪代码为:

    在groupingBy函数中,默认的Map工厂方法为HashMap::new下游收集器为toList,它们都可以修改,实现更强大的功能。

     

    3) 分组收集

    通过修改Map工厂方法和下游收集器,在分组后可以进行一系列的自定义操作。

    下面java.util.stream.Collectors包中提供的一些常用下游收集器:

    下面是一些示例:

    注意:

    1. 存在更为通用的名为reducing的归约收集器,由于比较复杂且少用,暂不介绍。

     

    5) 收集前处理

    在分组后,直接交给下游收集器处理的一般为元素本身,可通过mapping方法为下游收集器组合一个前置Function,在下游收集前,对传入的元素进行映射转换等一系列处理。

     

    6) 收集后处理

    相应的,也可以通过collectingAndThen方法为下游收集器组合一个后置Function,在下游收集完成后,在分组内进行排序(sort)、过滤(filter)、限制返回元素(skip/limit)等一系列操作。

     

     

     

     

    7) 分区

    分组的一个特殊情况是分区,就是将流按true/false分为两个组,Collectors有专门的分区函数:

    下面是一些简单示例:

     

    8) 多级分组

    groupingBy和partitioningBy都可以接受一个下游收集器,而下游收集器又可以是分组或分区。