1 汇总
1.1 [1]46个
01.关键字(共46个)
数据类型 【byte、short、int、long、float、double、char、boolean】
数据类型 【new、void、instanceof】
---------------------------------------------------------------------------------------------------------
语句 【if、else、return】、【switch、case、default】、【continue、break】、【for、do、while】
语句 【try、catch、throws、finally】、【throw、this、super】
---------------------------------------------------------------------------------------------------------
修饰 【public、private、protected、abstract】【final、static、native、transient】
修饰 【synchronized、volatile】
---------------------------------------------------------------------------------------------------------
包 【package、import】
类/接口 【class、interface】、【extends、implements】
---------------------------------------------------------------------------------------------------------
null 【既不是对象也不是一种类型,一种特殊的值,可以将其赋予任何引用类型,也可以将null转化成任何类型】
const 【JAVA目前没有const,但const是保留字,类似final,为后续JDK版本的扩展,而进行保留】
goto 【JAVA目前没有goto,但goto是保留字,在C++中,break跳不出多重循环,只能使用goto指定要跳出的地点】
---------------------------------------------------------------------------------------------------------
JAVA无意义 【future、generic、operator、outer、rest、var】
1.2 [1]null
01.介绍
a.历史
Java的设计原理是为了简化事情,没有浪费时间在指针、操作符重载、多继承实现的原因,但null却与此正好相反;
b.为什么需要学习null?
因为如果你对null不注意,Java将使你遭受空指针异常的痛苦,并且得到一个沉痛的教训。
c.JAVA中NULL是什么?
null是JAVA中一个很重要的概念。
null设计初衷是为了表示一些缺失的东西,例如缺失的用户、资源或其他东西。
但是,一年后,令人头疼的空指针异常给Java程序员带来不少的骚扰。
d.总结
null是任何一个引用类型变量的默认值
null不能调用任何“非静态方法”,会报错
null可以通过instanceof判断类型
02.深入了解
a.NULL大小写
null是Java中的关键字,像public、static、final一样,对大小写敏感;
不能将null写成Null或NULL,会导致编译器无法识别而报错
b.NULL特殊值
null既不是对象也不是一种类型,它仅是一种特殊的值,你可以将其赋予任何引用类型,也可以将null转化成任何类型
String str = null;
Integer itr = null;
Double dbl = null;
String myStr = (String) null;
Integer myItr = (Integer) null;
Double myDbl = (Double) null;
System.out.println(myStr); // null
System.out.println(myItr); // null
System.out.println(myDbl); // null
不难发现,在编译和运行时期,将null强制转换成任何引用类型都是可行的,而且在运行时期都不会抛出空指针异常
c.null可以赋值给引用变量,但不能赋值给基本变量
int i = null; // 提示报错
short s = null; // 提示报错
byte b = null; // 提示报错
double d = null; // 提示报错
Integer itr = null; // 提示不报错
int j = itr; // 提示不报错,但是运行时空指针报错
不难发现,将null赋值给包装类object,然后将object赋给各自的基本类型,编译器不会报错,但是运行时期报错;
报错内容为:Exception in thread "main" java.lang.NullPointerException,这是因为Java中的自动拆箱导致的
d.null包装类
任何含有null值的包装类在Java拆箱生成基本数据类型时候都会抛出一个空指针异常。一些程序员犯这样的错误,
他们认为自动装箱会将null转换成各自基本类型的默认值,
----------------------------------------------------------
例如对于int转换成0,布尔类型转换成false,但是那是不正确的,如下面所示:
Integer iAmNull = null;
int i = iAmNull; // 空指针报错
不能发现,在使用HashMap和Integer键值的时候会发生很多这样的错误
----------------------------------------------------------
Map numberAndCount = new HashMap<>();
int[] numbers = {3, 5, 7,9, 11, 13, 17, 19, 2, 3, 5, 33, 12, 5};
for(int i : numbers){
int count = numberAndCount.get(i);
numberAndCount.put(i, count++); // 空指针报错
}
----------------------------------------------------------
场景:找到一个数字在数组中出现了多少次
首先得到以前的数值,然后再加一,最后把值放回Map里。
程序员可能会以为,调用put方法时,自动装箱会自己处理好将int装箱成Interger,
但是他忘记了当一个数字没有计数值的时候,HashMap的get()方法将会返回null,而不是0
Integer的默认值是null,当把null值传递给一个int型变量的时候自动装箱将会返回空指针异常。
e.null使用instanceof判断类型
Integer iAmNull = null;
if(iAmNull instanceof Integer){
System.out.println("iAmNull is instance of Integer");
}else{
System.out.println("iAmNull is NOT an instance of Integer");
}
这是instanceof操作一个很重要的特性,使得对类型强制转换检查很有用
f.非静态方法调用null的引用类型变量
public class DEMO {
public static void main(String[] args) {
DEMO myObject = null;
myObject.iAmStaticMethod(); // 不抛异常
myObject.iAmNonStaticMethod(); // 空指针异常
}
private static void iAmStaticMethod(){
System.out.println("静态方法使用静态绑定,不会抛出空指针异常");
}
private void iAmNonStaticMethod(){
System.out.println("非静态方法,抛出指针异常");
}
}
用静态方法来使用一个值为null的引用类型变量,因为静态方法使用静态绑定,不会抛出空指针异常
g.null传值
public void print(Object obj) 可以这样调用 print(null),
null传递给方法使用,方法可以接收任何引用类型,不会抛出空指针异常,只是优雅的退出
h.null比较
String abc = null;
String cde = null;
if (abc == cde) {
System.out.println("null == null is true in Java");
}
if (null != null) {
System.out.println("null != null is false in Java");
}
==或者!=操作来比较null值,但不能使用小于或者大于;跟SQL不同,在Java中null==null将返回true
1.3 [1]goto
01.汇总
没有goto关键字,避免与其相关的复杂性和潜在错误
1.4 [1]&和&&
01.汇总
&:逻辑与,&两边的表达式都会进行运算;整数的位运算符
&&:短路与,&&左边的表达式结果为false时,&&右边的表达式不参与计算(短路)
1.5 [1]i++和++i
01.汇总
i++:先引用后增加
++i:先增加后引用
02.使用
for循环中,单独i++与++i没有区别,通常用i++
1.6 [1]instanceof
01.汇总
比较的是【对象】,【不能比较基本类型】
用来在运行时判断对象是否是指定类及其父类的一个实例
1.7 [2]object
01.常见
用于对象克隆:clone() protected native Object clone()
获取哈希码:hashCode() public native int hashCode()
获取类结构信息:getClass() public final native Class<?> getClass()
把对象转变成字符串:toString() public String toString()
默认比较对象的地址值是否相等,子类可以重写比较规则:equals() public boolean equals(Object)
02.线程
多线程中唤醒功能:notify() public final native void notify()
多线程中唤醒所有等待线程的功能:notifyAll() public final native void notifyAll()
让持有对象锁的线程进入等待:wait() public final void wait()
让持有对象锁的线程进入等待,设置超时毫秒数时间:wait(long timeout)
让持有对象锁的线程进入等待,设置超时纳秒数时间:wait(long timeout, int nanos)
垃圾回收前执行的方法:finalize()
1.8 [2]scanner
01.Scanner类
a.Java中Scanner类中的方法:next()、nextLint()都会【读入控制台的字符】,区别如下:
a.next()/nextInt()
不会读入【字符前后的空格/TAB】,只读入【字符】
【开始读入字符,遇到"空格/TAB/字符"截止(字符前后的空格/TAB/字符,不算数)】
b.nextLine()
会读入【字符前后的空格/TAB】,也会读入【字符】
【开始读入空格/TAB/字符,遇到"回车"截止】
b.举例一
a.代码
Scanner input = new Scanner(System.in); |
System.out.println("请输入成绩:"); | 请输入成绩:
int javaScore = input.nextInt(); | 100
System.out.println("请输入姓名:"); | 请输入姓名:
String name = input.nextLint(); | 输入姓名是:◡
System.out.println("输入姓名是:" + name); |
b.分析
输入"10"后,【回车】,但"nextInt()不接收回车";
但,"回车被nextLint()接收,直接执行下一条语句,即System.out.println("输入姓名是:" + name); "
c.举例二
a.代码
Scanner input = new Scanner(System.in); |
System.out.println("请输入姓名:"); | 请输入姓名:
String name = input.nextLint(); | 我是 张三
System.out.println("输入姓名是:" + name); | 请输入姓名:我是
System.out.println("请输入成绩:"); | 请输入成绩:
int javaScore = input.nextInt(); | Exception in thread "main" java.util.Input
b.分析
输入"我是 张三"后,【回车】,"nextLint()遇到回车截止,但会读入空格/TAB/字符"
同时,"next()无法识别空格,错把"空格"当作结束,而在输出流当中,仍存在"张三"被nextInt()接收,导致错误"
d.举例三
a.代码1
System.out.println("-----------"); |
System.out.println(); | 空语句,先执行()内,再进行回车
System.out.println("-----------"); |
b.代码2
System.out.print("*************"); | 等同于 System.out.println("*************");
System.out.print("\n");
b.代码3
e.举例四
a.代码
System.out.println(3 * 0.3); | 结果:0.8999999999999999 int自动转换为double
System.out.println(3 * 0.3d); | 结果:0.8999999999999999 int自动转换为double
System.out.println(3 * 0.3f); | 结果:0.90000004 int自动转换为float
b.分析
a.理解1
int = 32bits = 4字节 容纳2^32的数字
float = 32bits = 4字节 无穷无尽的小数
double = 64bits = 8字节 无穷无尽的小数 int自动转换为float,【2^32】无法容纳【无穷无尽的小数】
b.理解2
二进制 整数 5 = 2^0 + 2^2 17 = 2^0 + 2^4
二进制 小数 0.6 = 2^-1
1.9 [2]java.util
01.常见类
ArrayList 动态数组实现的列表,可以自动扩展
LinkedList 双向链表实现的列表
HashMap 基于哈希表的 Map 实现,不保证元素的顺序
TreeMap 基于红黑树的 Map 实现,按键的自然顺序或指定的比较器排序
HashSet 基于哈希表的 Set 实现,不允许重复元素
TreeSet 基于 TreeMap 实现的 Set,按自然顺序或指定的比较器排序
LinkedHashSet 结合了哈希表和链表的特性,保持插入顺序
PriorityQueue 优先级队列,实现了基于优先级的队列功能
Collections 提供了静态方法来操作集合(如排序、查找)
Calendar 用于操作和处理日期和时间的抽象类(Calendar 是一个抽象类,具体实现如 GregorianCalendar)
Date 处理日期和时间的类(现在推荐使用 LocalDate 和 LocalDateTime 等 Java 8 的新日期时间 API)
Random 用于生成伪随机数的类
02.常见接口
List 线性集合,允许重复元素并按插入顺序排序
Set 集合,不允许重复元素
Map 键值对集合,键唯一
Queue 队列接口,支持插入和删除操作
Deque 双端队列接口,支持在两端插入和删除元素
1.10 [2]java.lang
01.接口
CharSequence:字符值的可读序列
Comparable:自然排序
Iterable:迭代器
Runnable:线程
02.超类
Object:所有类的父类
03.包装类
Byte:字节 8 bit
Character:2 字节 16 bit
Short:2 字节 16 bit
Integer:4 字节 32 bit
Long:8 字节 64 bit
Float:4 字节 32 bit
Double:8 字节 64 bit
Boolean
04.字符串
String:不可变字符串
StringBuffer:线程同步,方法使用 synchronized
StringBuilder:线程不安全
05.线程
Thread:实现接口 Runnable
ThreadGroup
06.异常
Throwable:Java 中异常的父类,子类有 Error 和 Exception
Error:错误
ArithmeticException:异常算术
ArrayIndexOutOfBoundsException:数组越界异常
ClassCastException:类型转换异常d
ClassNotFoundException:类找不到异常
IllegalArgumentException:方法参数异常
NoSuchMethodException:没有此方法
NullPointerException:空指针异常
NumberFormatException:字符串转数字异常
StringIndexOutOfBoundsException:字符串越界异常
Exception:异常
07.运算
Math:基本数学运算
StrictMath
Number:表示数字值,是基本数据类型 Byte, Short, Integer, Long, Float, Double 的父类
08.其他
Number
System
Package
1.11 [3]spi、api
01.汇总
a.目的
SPI:用于定义服务的标准接口,供第三方实现和扩展
API:用于提供功能的访问接口,供开发者调用和使用
b.使用场景
SPI:通常用于框架或库中,允许扩展和定制功能
API:用于应用程序之间的通信和功能调用
c.实现方式
SPI:需要服务提供者实现接口,并通过框架加载和使用
API:由提供者实现,开发者直接调用
d.加载机制
SPI:通常通过 `ServiceLoader` 或类似机制动态加载实现
API:通常通过静态引用或动态调用使用
e.总结
SPI 是一种扩展机制,允许第三方实现接口来扩展框架的功能
API 是一种访问机制,提供功能的调用接口
01.SPI(Service Provider Interface)
a.定义
SPI 是一种服务提供者接口,用于定义服务的标准接口和规范
它允许第三方开发者实现这些接口,并将实现提供给框架或应用程序使用
b.用途
SPI 主要用于框架或库中,允许开发者通过实现接口来扩展框架的功能
例如,Java 中的 JDBC 是一个典型的 SPI,数据库厂商可以通过实现 JDBC 接口来提供数据库驱动
c.工作原理
SPI 定义了一组接口,服务提供者需要实现这些接口
框架或应用程序通过 SPI 加载和使用这些实现
Java 提供了 `java.util.ServiceLoader` 类来动态加载 SPI 实现
d.示例
JDBC 驱动程序:数据库厂商实现 JDBC 接口,提供数据库访问功能
Java Cryptography Architecture (JCA):允许第三方提供加密算法实现
02.API(Application Programming Interface)
a.定义
API 是应用程序编程接口,用于定义应用程序或服务之间的交互方式
它提供了一组功能和方法,供开发者调用和使用
b.用途
API 主要用于应用程序之间的通信和集成。它可以是库、框架、操作系统或服务提供的接口
c.工作原理
API 定义了一组方法和数据结构,开发者可以通过调用这些方法来实现特定功能
API 可以是本地的(如 Java API)或远程的(如 RESTful API)
d.示例
Java Collections API:提供集合类的标准接口和实现
RESTful API:通过 HTTP 协议提供服务的访问接口
2 基础
2.1 [1]对象
01.面向对象
面向对象是一种思想,世间万物都可以看做一个对象,面向对象软件开发具有以下优点:
代码开发模块化,更易维护和修改。
代码复用性强。
增强代码的可靠性和灵活性。
增加代码的可读性。
02.四大特征
a.总结
封装、继承、多态、抽象
b.封装
对实体的属性和功能实现进行访问控制,无需知道功能如何实现
c.继承
子类继承父类后直接使用父类的属性和方法,实现方式有两种:实现继承、接口继承
实现继承:直接使用基类公开的属性和方法,无需额外编码。
接口继承:仅使用接口公开的属性和方法名称,需要子类实现。
d.多态
一个类的同名方法,在不同情况下的实现细节不同。
多态机制实现不同的内部实现结构共用同一个外部接口
-----------------------------------------------------------------------------------------------------
多态歧义,一个词语必须根据上下文才有实际的含义(打:打篮球、打水、打架)
方法重载add、方法重写、使用父类作为方法的形参、使用父类作为方法的返回值
-----------------------------------------------------------------------------------------------------
使用多态的一个细节:
(1)当子类重写了父类的方法时,父类的引用会调用子类的重名方法:
(2)当子类和父类的属性重名时,父类的引用会调用父类的重名属性。
-----------------------------------------------------------------------------------------------------
多态的实现离不开继承,
对于父类型,可以有三种形式,即普通的类、抽象类、接口。
对于子类型,则要根据它自身的特征,重写父类的某些方法,或实现抽象类/接口的某些抽象方法。
e.抽象
Java支持创建只暴漏接口而不包含方法实现的抽象的类
03.区别
a.面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源。
缺点:没有面向对象易维护、易复用、易扩展
b.面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点:性能比面向过程低
2.2 [1]多态
01.多态
a.定义
同一个接口,使用不同的实例而执行不同操作。同一个行为具有多个不同表现形式或形态的能力。
b.优点
消除类型之间的耦合关系
可替换性(substitutability)
可扩充性(extensibility)
接口性(interface-ability)
灵活性(flexibility)
简化性(simplicity)
02.多态实现:3个条件
继承
子类重写父类的方法
父类引用变量指向子类对象
03.多态原理:动态绑定
动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
Java 中使用父类的引用变量调用子类重写的方法,即可实现多态。
2.3 [1]类与对象
00.总结
类是对象的抽象;对象是类的具体实例
类是抽象的,不占用内存;对象是具体的,占用存储空间
类是一个定义包括在一类对象中的方法和变量的模板
01.相同点
面向对象编程的基本概念:类和对象都是面向对象编程的基本概念。类是对象的蓝图,而对象是类的实例。
封装性:类和对象都支持封装性。类通过定义属性和方法来封装数据和行为,而对象通过实例化类来实现封装。
支持继承和多态:类可以通过继承来扩展,子类可以继承父类的属性和方法。对象可以通过多态性来表现不同的行为。
02.不同点
a.定义与实例
类:类是一个模板或蓝图,用于定义对象的属性和行为。它是一个抽象的概念
对象:对象是类的实例,是一个具体的实体。通过类创建对象,使用对象来访问类的属性和方法
b.内存分配
类:类本身不占用内存,只有在类加载时,类的信息(如方法表)才会被加载到内存中
对象:对象在创建时会在堆内存中分配空间,用于存储对象的属性值
c.使用方式
类:类用于定义属性和方法,可以通过类名直接访问静态成员
对象:对象用于调用类的实例方法和访问实例变量
d.生命周期
类:类的生命周期从类加载到内存开始,到程序结束或类被卸载为止
对象:对象的生命周期从创建开始,到没有引用指向它并被垃圾回收器回收为止
2.4 [1]继承、重写、重载
01.继承
a.接口
接口是常量值和方法定义的集合。接口是一种特殊的抽象类。
b.单继承、多继承
java类是单继承的,classB Extends classA
java接口可以多继承,Interface3 Extends Interface0, Interface1, interface ...
c.类,为什么是单继承,为什么不能多继承?
Java是单继承的,指的是Java中一个类只能有一个直接的父类
Java不能多继承,则是说Java中一个类不能直接继承多个父类
如果A同时继承B和C,而B和C同时有一个D方法,A如何决定该继承那一个呢
d.接口,为什么允许多重继承
接口全都是抽象方法继承谁都无所谓,所以接口可以继承多个接口
e.注意事项
1.一个类如果实现了一个接口,则要实现该接口的所有方法
2.方法的名字、返回类型、参数必须与接口中完全一致。如果方法的返回类型不是void,则方法体必须至少有一条return语句
3.因为接口的方法默认是public类型的,所以在实现的时候一定要用public来修饰(否则默认为protected类型,缩小了方法的使用范围)
02.重写、重载
a.图示
位置 方法名 参数表 返回值 访问修饰符
方法重写 子类 相同 相同 相同或是 不能比父类更严格
方法重载 同类 相同 不相同 无关 无关
b.重写
方法重写(父子继承关系):父类有一个方法,子类重新写了一遍
要求:1.方法名相同;2.参数列表相同
c.重载
重载:一般同类/父子维承也可以
-----------------------------------------------------------------------------------------------------
要点:1.方法名相同;2.参数列表不同(类型不同、个数不同、顺序不同)
注意:1.与返回值无关;2.与参数名无关(仅与参数类型有关)
d.区别
a.重载
发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),
与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。
b.重写
发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,
访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。
e.构造方法能不能重写
不能,构造方法是不能被继承,而重写的前提是 继承
2.5 [2]内部类
01.内部类
a.优点
封装性:内部类可以访问外部类的私有成员,增强了封装性。
代码组织:将相关的逻辑组织在一起,提高代码可读性和维护性。
隐藏实现:内部类可以隐藏不需要暴露给外界的实现细节。
多重继承:内部类可以实现多个接口或继承不同的类,解决 Java 单继承的限制。
事件处理:内部类常用于事件处理,使事件处理代码更清晰、更集中。
b.分类
成员内部类:为位于另一个类的内部,成员内部类可以无条件访问外部类的所有成员属性和成员方法。
局部内部类:在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
匿名内部类:只没有名字的内部类,使用匿名内部类的前提条件:必须继承一个父类或实现一个接口。
静态内部类:在另一个类里面的类,只不过在类的前面多了一个关键字static
02.一个Java文件里可以有多个类吗(不含内部类)?
一个java文件里【可以有多个类】,但【最多只能有一个被public修饰的类】;
如果这个java文件中包含public修饰的类,则这个类的名称必须和java文件名一致。
03.内部类与静态内部类区别?
a.内部类
定义在一个类内部的类叫内部类,包含内部类的类称为外部类。
内部类可以声明public、protected、private等访问限制,可以声明为abstract的供其他内部类或外部类继承与扩展,
或者声明为static、final的,也可以实现特定的接口。外部类按常规的类访问方式使用内部 类,
唯一的差别是外部类可以访问内部类的所有方法与属性,包括私有方法与属性。
b.静态类(只有内部类才能被声明为静态类,即静态内部类)
1.只能在内部类中定义静态类
2.静态内部类与外层类绑定,即使没有创建外层类的对象,它一样存在。
3.静态类的方法可以是静态的方法也可以是非静态的方法,静态的方法可以在外层通过静态类调用,
而非静态的方法必须要创建类的对象之后才能调用。
4.只能引用外部类的static成员变量(也就是类变量)。
5.如果一个内部类不是被定义成静态内部类,那么在定义成员变量或者成员方法的时候,是不能够被定义成静态的。
2.6 [2]对象创建方式
00.汇总
使用new关键字
使用Class.newInstance()
使用Constructor.newInstance()
使用clone()方法
使用反序列化
使用工厂方法
使用依赖注入(DI)框架
01.使用new关键字
a.语法
ClassName obj = new ClassName();
b.示例
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
public static void main(String[] args) {
Person person = new Person("Alice", 25); // 使用 new 关键字创建对象
person.display(); // 输出: Name: Alice, Age: 25
}
}
c.特点
简单直接,适用于大多数场景。
02.使用Class.newInstance()
a.语法
ClassName obj = ClassName.class.newInstance();
b.示例
public class Person {
private String name;
public Person() {
this.name = "Unknown";
}
public void display() {
System.out.println("Name: " + name);
}
public static void main(String[] args) throws Exception {
Person person = Person.class.newInstance(); // 使用反射创建对象
person.display(); // 输出: Name: Unknown
}
}
c.特点
需要类有一个无参构造函数,已被废弃,不推荐使用。
03.使用Constructor.newInstance()
a.语法
Constructor<ClassName> constructor = ClassName.class.getConstructor(parameterTypes);
ClassName obj = constructor.newInstance(args);
b.示例
import java.lang.reflect.Constructor;
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void display() {
System.out.println("Name: " + name);
}
public static void main(String[] args) throws Exception {
Constructor<Person> constructor = Person.class.getConstructor(String.class);
Person person = constructor.newInstance("Alice"); // 使用反射创建对象
person.display(); // 输出: Name: Alice
}
}
c.特点
更灵活,可以调用带参数的构造函数,适用于动态创建对象的场景。
04.使用clone()方法
a.语法
ClassName obj = (ClassName) originalObj.clone();
b.示例
public class Person implements Cloneable {
private String name;
public Person(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void display() {
System.out.println("Name: " + name);
}
public static void main(String[] args) throws Exception {
Person p1 = new Person("Alice");
Person p2 = (Person) p1.clone(); // 使用 clone() 方法创建对象
p2.display(); // 输出: Name: Alice
}
}
c.特点
默认是浅拷贝,需要手动实现深拷贝,适用于需要复制对象的场景。
05.使用反序列化
a.语法
ObjectInputStream in = new ObjectInputStream(inputStream);
ClassName obj = (ClassName) in.readObject();
b.示例
import java.io.*;
public class Person implements Serializable {
private String name;
public Person(String name) {
this.name = name;
}
public void display() {
System.out.println("Name: " + name);
}
public static void main(String[] args) throws Exception {
// 序列化
Person p1 = new Person("Alice");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(p1);
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bis);
Person p2 = (Person) in.readObject(); // 使用反序列化创建对象
p2.display(); // 输出: Name: Alice
}
}
c.特点
适用于对象的持久化和网络传输,需要实现 Serializable 接口。
06.使用工厂方法
a.语法
ClassName obj = FactoryClass.createInstance();
b.示例
public class Person {
private String name;
private Person(String name) {
this.name = name;
}
public static Person create(String name) {
return new Person(name);
}
public void display() {
System.out.println("Name: " + name);
}
public static void main(String[] args) {
Person person = Person.create("Alice"); // 使用工厂方法创建对象
person.display(); // 输出: Name: Alice
}
}
c.特点
将对象的创建逻辑与使用逻辑分离,适用于复杂对象的创建。
07.使用依赖注入(DI)框架
a.示例(Spring)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public Person person() {
return new Person("Alice");
}
}
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Person person = context.getBean(Person.class); // 使用 Spring 创建对象
person.display(); // 输出: Name: Alice
}
}
b.特点
适用于大型项目,简化对象管理,需要引入依赖注入框架。
2.7 [2]对象实例化顺序
01.回答1
父类静态变量
父类静态代码块
子类静态变量
子类静态代码块
父类非静态变量(父类实例成员变量)
父类构造函数
子类非静态变量(子类实例成员变量)
子类构造函数
02.回答2:加载 -> 连接 -> 初始化
代码书写顺序加载父类静态变量和父类静态代码块
代码书写顺序加载子类静态变量和子类静态代码块
父类非静态变量(父类实例成员变量)
父类非静态代码块
父类构造函数
子类非静态变量(子类实例成员变量)
子类非静态代码块
子类构造函数
2.8 [3]接口
01.接口
a.修饰符只能是 public 或 default
public:可以被所有其他类所访问 √
private:只能被自己访问和修改
protected:自身,子类及同一个包中类可以访问
default(默认):同一包中的类可以访问,声明时没有加修饰符,认为是default √
b.默认abstract
接口中所有的方法默认都是 abstract,通常 abstract 省略不写
c.方法能使用final修饰吗?不能
接口中的方法必须要实现类实现
d.接口中可以有构造函数吗?不能
不能包含构造器和初始化块定义
可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义
e.JDK8,【变量、方法】等价写法
public interface MyInterface {
public static final int field1 = 0; // 变量:默认修饰符(public、static、final)
int field2 = 0; // 变量:等价上述写法
public abstract void method1(int a) throws Exception; // 方法:默认修饰符(public、abstract)
void method2(int a) throws Exception; // 方法:等价上述写法
}
02.接口:默认方法、静态方法
a.JDK8后,【默认方法】,【可以对该默认方法重写】
定义:default
调用:Vehicle.super.print();
作用:新增的默认方法在实现类中直接可用
-----------------------------------------------------------------------------------------------------
在Java8之前,在基于抽象的设计中,接口只能有抽象方法,一个接口有一个或多个实现类;
【若接口要增加某个方法,则所有实现类都要新增这个方法的实现,否则就就不满足接口的约束】
【默认接口方法,就是为解决这一问题,实现类可以不用修改继续使用,并且新增的默认方法在实现类中直接可用】
【妥协:维护现有代码的向后兼容性时,静态方法和默认方法是一种很好的折衷,逐步为接口提供附加功能,而不破坏实现类】
-----------------------------------------------------------------------------------------------------
接口默认方法,扩展接口而不必担心破坏实现类
接口默认方法,缩小了接口和抽象类之间的差异。
接口默认方法,无需创建基类,由实现类自己选择覆盖哪个默认方法实现。
接口默认方法,增强了Java 8中的Collections API以支持lambda表达式。
接口默认方法,默认方法不能为java.lang.Object中的方法,因为Object是所有类的基类,这种写法将毫无意义
-----------------------------------------------------------------------------------------------------
如果子类没有重写父接口默认方法的话,会直接继承父接口默认方法的实现;
如果子类重写父接口默认方法为普通方法,则与普通方法的重写类似;
如果子类(接口或抽象类)重写父接口默认方法为抽象方法,那么所有子类的子类需要实现该方法;
b.JDK8后,【静态方法】,【无法对该静态方法重写】【仅对接口方法可见,实例对象无法访问】
定义:static关键字
调用:Vehicle.blowHorn();
作用:将与相关方法内聚到接口,提高内聚性,无需创建额外对象
-----------------------------------------------------------------------------------------------------
【接口提供一种简单的机制,允许通过将相关的方法内聚在接口中,而不必创建新的对象】
【虽然抽象类,也可以“类似接口,提供静态方法,来提高内举性”,但主要区别在于抽象类可以有构造函数、成员变量和方法】
【推荐:把只和接口相关的静态utility方法放在接口中(提高内聚性),而无需额外创建一些utility类专门处理逻辑】
-----------------------------------------------------------------------------------------------------
接口静态方法,接口的一部分,实例对象无法直接访问
接口静态方法,非常适合提供有效的方法,例如null检查,集合排序等
接口静态方法,不允许被实现类覆盖,来提供安全性
2.9 [3]抽象类
01.抽象类
a.优点
模板化编程
在公司里,为了统一出高标准。往往由“高手”先通过抽象类写一个模板,
然后新人们可以仿照这个模板编写自己的模块代码
b.缺点
抽象类必须通过子类继承才有意义,而继承会增加父类和之间的耦合度,
与“高内聚低耦合”的设计原则相违背(这也是很多人反对使用“继承”的原因)。
因此,很多人推荐使用接口。
c.关键字abstract
可以修饰类和方法
不能修饰属性和构造方法
abstract 修饰的类是抽象类,需要被继承
abstract 修饰的方法是抽象方法,需要子类被重写
02.抽象类
a.抽象类不能实例化
抽象类中可能存在抽象方法,而抽象方法没有方法体
b.抽象类必须要有抽象方法吗?不一定
public abstract class TestAbstractClass {
public static void notAbstractMethod() {
System.out.println("I am not a abstract method.");
}
}
c.抽象类能使用final修饰吗?不能
抽象类是被用于继承的,final修饰代表不可修改、不可继承的
2.10 [3]抽象类、接口
01.普通类、抽象类
1.抽象类不能被实例化
2.抽象类可以有抽象方法,抽象方法只需申明,无需实现
3.含有抽象方法的类必须申明为抽象类
4.抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类
5.抽象方法不能被声明为静态
6.抽象方法不能用 private 修饰
7.抽象方法不能用 final 修饰
02.抽象类、接口
a.相同点
接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
b.不同点
接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,
通过实现多个接口可以弥补Java单继承的不足。
c.总结
抽象类:用来捕捉子类的通用特性的,abstract class
接口:抽象方法的集合,interface
2.11 [4]修饰符
01.常见问题
a.图示
类内部 本包 子类 外部包
public √ √ √ √
protected √ √ √ ×
default √ √ × ×
private √ × × ×
b.说明
public:可以被所有其他类所访问
private:只能被自己访问和修改
protected:自身,子类及同一个包中类可以访问
default(默认):同一包中的类可以访问,声明时没有加修饰符,认为是default
2.12 [4]可变参数
01.作用
在不确定参数的个数时,可以使用可变参数。
02.语法
参数类型...
03.特点
每个方法最多只有一个可变参数
可变参数必须是方法的最后一个参数
可变参数可以设置为任意类型:引用类型,基本类型
参数的个数可以是 0 个、1 个或多个
可变参数也可以传入数组
无法仅通过改变 可变参数的类型,来重载方法
通过对 class 文件反编译可以发现,可变参数被编译器处理成了数组
2.13 [4]构造方法
01.构造方法
a.特性
1.构造器必须与类同名(如果一个源文件中有多个类,那么构造器必须与公共类同名)
2.每个类可以有一个以上的构造器
3.构造器可以有0个、1个或1个以上的参数
4.构造器没有返回值
5.构造器总是伴随着new操作一起调用
b.无参构造
1.如果类中没有任何构造方法,则系统自动提供一个无参构造
2.如果类中已经存在了任何构造方法,则系统不再提供无参构造
3.如果给类中编写构造方法,则手动编写一个无参构造,防止报错
c.有参构造
1.一次性给多个属性赋值
2.构造方法与setter、getter方法互补:构造方法先使用,setter、getter后补充值
3.构造方法不能通过方法名直接调用,需要Dog dot=new Dog()实例化
4.非构造方法(普通方法)可以通过方法名调用
5.构造方法之间可以相互调用,通过ths,通过参数区分
6.多个构造方法之间不能循环调用
02.子类构造方法的执行过程
如果子类的构造方法中没有通过 super 显式调用父类的有参构造方法,也没有通过 this 显式调用自身的其他构造方法,则系统会默认先调用父类的无参构造方法。这种情况下,写不写 super(); 语句,效果是一样的
如果子类的构造方法中通过 super 显式调用父类的有参构造方法,将执行父类相应的构造方法,不执行父类无参构造方法
如果子类的构造方法中通过 this 显式调用自身的其他构造方法,将执行类中相应的构造方法
如果存在多级继承关系,在创建一个子类对象时,以上规则会多次向更高一级父类应用,一直到执行顶级父类 Object 类的无参构造方法为止
03.为什么要在类中声明一个无参构造方法?
Java程序编写中,子类的构造方法必定会调用父类的构造方法,
如果在子类的构造方法中没有指定调用父类的某个构造方法,
在实例化子类对象时。子类会默认调用父类的无参构造方法。
如果在父类中没有定义无参构造方法的话,编译会报错。
因此在类中声明一个无参构造函数可以【避免其子类在实例化对象时出错】
04.在调用子类构造方法之前
会先调用【父类没有参数的构造方法】,其目的是【帮助子类做初始化工作】
05.构造方法能不能重写?不能
不能,构造方法是不能被继承,而重写的前提是“继承”
构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。
如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。x
2.14 [4]this、super
01.this
a.用法
1.指代当前对象
2.指代当前类
3.指代构造方法this():表示当前类的构造方法,只能放在首行
b.注意
在新建对象的时候实际上调用了类的无参(没有参数)的构造方法一般默认(在类中可以隐藏)
02.super
a.用法
1.只能指代父类对像象
2.指代父类的构造方法,只能放在首行
b.注意
1.子类必须通过super关键字调用父类有参数的构造函数
2.使用super调用父类构造器的语句必须是子类构造器的第一条语句
如果子类构造器没有显式地调用父类的构造器,则将自动调用父类的默认(没有参数)的构造器
如果父类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用父类的构造器,则java编译器报告错误
03.this与super的区别
a.相同点
super()和this()都必须在构造函数的第一行进行调用,否则就是错误的
this()和super()都指的是对象,所以,均不可以在static环境中使用。
b.不同点
super()主要是对父类构造函数的调用,this()是对重载构造函数的调用
super()主要是在继承了父类的子类的构造函数中使用,是在不同类中的使用;this()主要是在同一类的不同构造函数中的使用
2.15 [5]final
01.final
a.用法
可以修饰:类、方法、变量
b.第1种:final类
final关键字修饰的类不可以被继承
c.第2种:final方法
final关键字修饰的方法不可以被重写
d.第3种:final变量
final关键字修饰的变量,一旦获得了初始值,就不可以被修改
02.final修饰变量,是引用不能变?还是引用的对象不能变?
final修饰【基本类型变量】:【值不能改变】
final修饰【引用类型变量】:栈内存中的【引用】【不能改变】,
所指向的【堆内存中的对象的属性值】可能【可以改变】
2.16 [5]static
01.static
a.用法
可以修饰:成员变量、成员方法、初始化块、内部类
被static修饰的成员是类的成员,它属于类、不属于单个对象
b.第1种:类变量
定义:被static修饰的成员变量叫类变量(静态变量)
类变量属于类,它随类的信息存储在方法区,并不随对象存储在堆中,
类变量可以通过类名来访问,也可以通过对象名来访问,但建议通过类名访问它
c.第2种:类方法
定义:被static修饰的成员方法叫类方法(静态方法)
类方法属于类,可以通过类名访问,也可以通过对象名访问,建议通过类名访问它
d.第3种:静态块
定义:被static修饰的初始化块叫静态初始化块。
静态块属于类,它在类加载的时候被隐式调用一次,之后便不会被调用了。
e.第4种:静态内部类
定义:被static修饰的内部类叫静态内部类。
静态内部类可以包含静态成员,也可以包含非静态成员。
静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。
外部类的所有方法、初始化块都能访问其内部定义的静态内部类。
02.static方法,能直接调用非静态方法吗?
a.回答
不能
b.原因
静态方法只能访问静态成员
调用静态方法时可能对象并没有被初始化,此时非静态变量还未初始化
非静态方法的调用和非静态成员变量的访问要先创建对象
03.static修饰的类,能不能被继承?
a.回答
情况1:顶级类(即非嵌套类)不能被声明为static
情况2:static可以用于修饰嵌套类(即内部类),即静态嵌套类
b.原因
顶级类:不能使用static修饰,因此与继承无关。
静态嵌套类:如果使用static来修饰一个内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。
因此使用static修饰的内部类被称为类内部类,有的地方也称为静态内部类。
2.17 [5]static调用
00.总结
a.同一个类中:
都有static,或都没static:直接调用
有static,调用没static:对象名.方法()
没有static,调用有static:直接调用
b.不在同一个类中:
被调用的方法没static:对象名.方法()
被调用的方法有static:类名.方法()
c.万能方法
new 对象,对象.方法();
Person2 p = new Person2();
p.eatFruit();
d.特别说明
a.区别
static方法是类级别的,属于类
非static方法是对象级别的,属于对象
b.原因
1.类有的,对象自然有
2.对象有的,类不一定有
01.同一个类中
a.都没static,直接调用
public class Person2 {
public void eatFruit() {
System.out.println("吃水果...");
}
public void eatFood() {
System.out.println("吃主食...");
eatFruit(); // 都没static,直接调用
}
public static void main(String[] args) {
Person2 p = new Person2();
p.eatFood();
}
}
b.没static,调用static,对象名.方法()
public class Person2 {
public void eatFruit() {
System.out.println("吃水果...");
}
public static void eatFood() {
System.out.println("吃主食...");
new Person2().eatFruit(); // 没static,调用static,对象名.方法()
}
public static void main(String[] args) {
Person2 p = new Person2();
p.eatFood();
}
}
c.有static,调用static,直接调用
public class Person2 {
public static void eatFruit() {
System.out.println("吃水果...");
}
public static void eatFood() {
System.out.println("吃主食...");
eatFruit(); // 有static,调用static,直接调用
}
public static void main(String[] args) {
Person2 p = new Person2();
p.eatFood();
}
}
02.不在同一个类中
a.万能方法:对象名.方法()
public class Person3 {
public void aa() {
Person2 p = new Person2();
p.eatFruit(); // 万能方法:对象名.方法()
}
public static void main(String[] args) {
Person3 p = new Person3();
p.aa();
}
}
b.被调用的方法有static:类名.方法()
public class Person3 {
public void aa() {
Person2.eatFruit(); // 被调用的方法有static:类名.方法()
}
public static void main(String[] args) {
Person3 p = new Person3();
p.aa();
}
}
2.18 [5]final、static
01.final、static
a.相同点
都不能修饰:构造方法
都可以修饰:类、方法、成员变量
b.区别
修饰类的代码块:static可以、final不可以
修饰方法内的局部变量:static不可以、final可以
static可以修饰类的代码块
2.19 [5]final、volatile
00.总结
a.语义不同
final:主要用于确保变量的不可变性和安全发布
volatile:主要用于确保变量的可见性和防止指令重排序
b.使用场景不同
final:适用于需要创建不可变对象、禁止类继承或方法重写的场景
volatile:适用于需要确保变量在多线程环境下的可见性和防止指令重排序的场景
c.线程安全性
final:可以确保对象在构造完成后,其 final 字段的值对所有线程都是可见的,但它不能确保变量的可见性
volatile:可以确保变量的可见性,但它不能确保变量的不可变性
d.性能影响
final:对性能几乎没有影响,因为它主要在编译时生效
volatile:可能会对性能有一定影响,因为它会导致变量的读写操作不能被缓存,从而增加内存访问的开销
01.final关键字
a.主要作用
a.不可变性
final 关键字可以用来修饰变量,使得变量一旦被初始化之后就不能再被修改
这对于创建不可变对象(immutable objects)非常有用
b.安全发布
当一个对象被正确地构造并且其引用被发布到其他线程时,final 字段的值对所有读取该对象的线程都是可见的
这是因为在对象构造完成后,final 字段的值会被正确地发布
b.使用场景
修饰类:当一个类被声明为 final 时,这个类不能被继承
修饰方法:当一个方法被声明为 final 时,这个方法不能被子类重写
修饰变量:当一个变量被声明为 final 时,这个变量在初始化之后不能被修改
02.volatile关键字
a.主要作用
a.可见性
volatile 关键字确保对变量的写操作对所有线程立即可见
它可以防止线程从缓存中读取变量的值,从而确保每次读取的都是最新的值
b.禁止指令重排序
volatile 关键字还可以防止指令重排序,确保变量的读写操作按照程序的顺序执行
b.使用场景
a.状态标志
在多线程环境下,使用 volatile 关键字来修饰状态标志变量,确保状态的变化对所有线程立即可见
b.双重检查锁定
在实现单例模式的双重检查锁定时,使用 volatile 关键字来修饰实例变量,防止指令重排序
2.20 [5]final、finally、finalize
01.final、finally、finalize
a.final可以修饰类、变量、方法
1.修饰【类】表示该类不能被继承
2.修饰【方法】表示该方法不能被重写
3.修饰【变量】表示该变量是一个常量不能被重新赋值。
b.finally一般作用在try-catch代码块中
在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常
该代码块都会执行,一般用来存放一些关闭资源的代码。
c.finalize是一个方法,属于Object类的一个方法
Object类是所有类的父类,使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作
2.21 [5]静态方法、实例方法
01.区别
特性 静态方法 实例方法
关键字 static 无
归属 类 对象
调用方式 通过类名或对象调用 通过对象调用
访问权限 只能访问静态变量和静态方法 可以访问实例变量、实例方法、静态变量和静态方法
典型用途 工具类方法、工厂方法 操作对象实例变量、与对象状态相关的操作
生命周期 类加载时存在,类卸载时消失 对象创建时存在,对象销毁时消失
02.区别
a.区别1
静态方法中不能使用this关键字,因为ths代表当前对象实例,而静态方法属于类,不属于任何实例
b.区别2
静态方法可以被重载(同类中方法名相同,但参数不同),但不能被子类重写(因为方法绑定在编译时已确定)
实例方法可以被重载,也可以被子类重写
c.区别3
实例方法中可以直接调用静态方法和访问静态变量
d.区别4
静态方法不具有多态性,即不支持方法的运行时动态绑定
2.22 [6]native方法
01.介绍
ava Native Interface (JNI) 是一种框架,允许 Java 代码与其他编程语言(如 C 或 C++)编写的代码进行交互。
通过 JNI,可以在 Java 中调用本地方法,这些方法在 Java 中声明,但在其他语言中实现。
这种机制使得 Java 程序能够利用其他语言的功能和性能优势。
02.场景
功能扩展:当 Java 本身不提供某些功能时,可以使用本地方法来调用其他语言实现的功能。例如,访问特定的硬件或操作系统功能
性能优化:在某些情况下,使用本地代码可以提高性能,特别是在需要进行大量计算或内存管理的场景中
代码重用:可以重用现有的 C/C++ 库,而无需重新实现相同的功能
03.使用方法
a.声明本地方法
在 Java 类中使用 native 关键字声明本地方法。这些方法没有方法体,因为它们的实现是在其他语言中完成的
public class NativeExample {
public native void nativeMethod();
}
b.加载本地库
使用 System.loadLibrary() 方法加载包含本地方法实现的库
static {
System.loadLibrary("NativeLib");
}
c.实现本地方法
在 C/C++ 中实现本地方法。需要使用 javah 工具生成头文件,然后在 C/C++ 中实现这些方法
#include <jni.h>
#include "NativeExample.h"
JNIEXPORT void JNICALL Java_NativeExample_nativeMethod(JNIEnv *env, jobject obj) {
// 本地方法实现
}
d.编译和链接
编译 C/C++ 代码并生成共享库(如 .dll 或 .so 文件),然后在 Java 程序中加载该库
2.23 [6]值传递、址传递
01.Java是值传递,还是引用传递?
a.回答
Java【只有按值传递】,不论是【基本类型还是引用类型】
b.说明
传递参数时,无论是基本数据类型还是对象(或数组),使用的都是值传递的方式
只是对于对象(或数组)而言,传递的值是对象引用副本,而非对象引用本身
c.说明
当一个对象被作为参数传递到方法中时,参数的值就是该对象的引用,引用的值是对象在【堆中的地址】
对象是存储在堆中的,所以传递对象的时候,可以理解为把变量存储的对象地址给传递过去
02.值传递、址传递
a.值传递
提供的值
在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了
b.址传递
提供的变量地址
在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。
传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
2.24 [6]浅拷贝、深拷贝
01.区别
a.浅拷贝
仅拷贝基本类型和引用,堆内的引用对象和被拷贝的对象共享
b.深拷贝
完全拷贝一个对象,包括基本类型和引用类型,堆内的引用对象也会复制一份
02.实现
a.浅拷贝
实现 Cloneable 接口,重写 clone() 方法
b.深拷贝
方式1:使用序列化,将对象序列化到字节流中,再反序列化成新的对象
方式2:手动拷贝对象的每个属性,这样可以完全控制拷贝过程
03.进一步理解
a.方法
protected native Object clone() throws CloneNotSupportedException;
b.解释
clone函数返回的是一个引用(“浅拷贝”),指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。
c.浅拷贝
如果【成员变量是引用数据类型】,复制后的对象与原始的对象【共用同一个成员变量】:
对【复制后的对象中成员变量的更改】也会出现在【原始的对象】中,因为它们共同引用的是堆中的同一个实例
浅拷贝的实现方式为:实现 Cloneable 接口并重写 clone() 方法。
d.深拷贝
如果【成员变量是引用数据类型】,复制后的对象与原始的对象【不共用同一个成员变量】:
【复制这个对象的所有成员变量的实例,而不是复制引用】,默认clone()无法实现,需要【手动增加对成员变量的复制】
2.25 [6]Comparable、Comparator
00.总结
a.Comparable
Comparable是【排序接口】,若一个类实现了Comparable接口,就意味着“该类支持排序”
b.Comparator
Comparator是【比较器】,我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序
c.总结
Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”
00.区别
a.接口位置:
Comparable 在 java.lang 包中
Comparator 在 java.util 包中
b.方法数量:
Comparable 只有一个方法:compareTo(T o)。
Comparator 有两个方法:compare(T o1, T o2) 和 equals(Object obj),但通常只需要实现 compare 方法
c.用途:
Comparable 用于定义对象的自然排序,一个类只能有一个自然排序实现
Comparator 用于定义多个排序规则,可以为一个类创建多个 Comparator 实现
d.实现方式:
Comparable 是类自身实现的,修改类本身来实现排序规则
Comparator 是通过单独的类实现的,不需要修改被比较的类,可以在外部定义排序规则
01.Comparable接口
a.源码
package java.lang;
public interface Comparable<T> {
public int compareTo(T o);
}
b.解读
【实现Comparable接口的类,按照compareTo(T o)比较,这种排列顺序称为“自然顺序”(实体类实现)】
【内部比较器:Comparable是排序接口,只要实现接口的对象直接就成为一个可以比较的对象,但是需要修改源代码】
---------------------------------------------------------------------------------------------------
只实现compareTo(T o)方法
compareTo 方法的返回值有三种情况:
e1.compareTo(e2) > 0,返回正整数,即 e1 > e2,倒序
e1.compareTo(e2) = 0,返回0 ,即 e1 = e2
e1.compareTo(e2) < 0,返回负整数,即 e1 < e2,升序
c.使用方式
Byte、Short、Integer、Long类:默认已经实现compareTo接口,直接按照“内置规则”比较大小
Float、Double类、Character、Boolean类:默认已经实现compareTo接口,直接按照“内置规则”比较大小
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
-----------------------------------------------------------------------------------------------------
ArrayList、LinkedList、Vector父类->Stack:重写List接口(非Set接口)中的sort方法,直接按照“内置规则”比较大小
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
for (Object e : a) {
i.next();
i.set((E) e);
}
}
-----------------------------------------------------------------------------------------------------
实现Comparable接口的【类的对象】的【列表】,可以通过【java.util.Collections.sort】自动排序
public static <T extends Comparable<? super T>> void sort(List<T> list)
public static <T> void sort(List<T> list, Comparator<? super T> c)
-----------------------------------------------------------------------------------------------------
实现Comparable接口的【类的对象】的【数组】,可以通过【java.util.Arrays.sort】自动排序
public static void sort(byte[] a)
public static void sort(short[] a)
public static void sort(int[] a)
public static void sort(long[] a)
public static void sort(float[] a)
public static void sort(double[] a)
public static void sort(char[] a)
public static void sort(Object[] a)
public static void sort(Object[] a, int fromIndex, int toIndex)
public static <T> void sort(T[] a, Comparator<? super T> c)
public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
d.示例
a.代码
public class Person implements Comparable<Person>{
String name;
int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
@Override
public int compareTo(Person p) { // 实现compareTo()方法,并【自定义排序规则】
return this.age-p.getAge(); // e1.compareTo(e2) > 0 或 = 0 或 < 0
}
public static void main(String[] args) {
Person[] people = new Person[]{new Person("xujian", 20), new Person("xiewei", 10)};
System.out.print("排序前:");
for (Person person : people) {
System.out.print(person.getName() + ":" + person.getAge() + ",");
}
Arrays.sort(people); // 调用 Arrays.sort 自动排序
System.out.print("\n排序后:");
for (Person person : people) {
System.out.print(person.getName() + ":" + person.getAge() + ",");
}
}
}
b.结果
排序前:xujian:20,xiewei:10,
排序后:xiewei:10,xujian:20,
02.Comparator接口:一种策略模式(不改变对象自身),而用一个策略对象来改变它的行为
a.源码
package java.util;
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2); // @FunctionalInterface保证仅有1个抽象方法(public abstract)
public boolean equals(Object object); // 并非抽象类方法(public boolean)
default Comparator<T> reversed() { // JDK8,默认方法
return Collections.reverseOrder(this);
}
public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) { // JDK8,静态方法
return new Comparators.NullComparator<>(true, comparator);
}
}
b.解读
【实现Comparator接口的类,按照compare(T o1,T o2)比较,这种排列顺序称为“定制顺序”(无法修改实现类,调用)】
【外部比较器:Comparator是比较器,实现该接口后该类本身不支持排序,但可以“自定义比较器”,无需修改源代码】
---------------------------------------------------------------------------------------------------
一定要实现compare(T o1, T o2) 函数,但可以不实现 equals(Object obj) 函数
e1.compare(e2) > 0,返回正整数,即 e1 > e2,倒序
e1.compare(e2) = 0,返回0 ,即 e1 = e2
e1.compare(e2) < 0,返回负整数,即 e1 < e2,升序
c.使用方式
实现Comparator接口的【类的对象】的【列表】,可以通过【java.util.Collections.sort】自动排序
public static <T> void sort(List<T> list, Comparator<? super T> c)
-----------------------------------------------------------------------------------------------------
实现Comparator接口的【类的对象】的【数组】,可以通过【java.util.Arrays.sort】自动排序
public static <T> void sort(T[] a, Comparator<? super T> c)
public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
d.示例
a.代码
public class PersonCompartor implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge() - o2.getAge();
}
}
public class Person {
String name;
int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
Person[] people = new Person[]{new Person("xujian", 20), new Person("xiewei", 10)};
System.out.print("排序前:");
for (Person person : people) {
System.out.print(person.getName() + ":" + person.getAge() + ",");
}
// 调用 Arrays.sort 排序,并指定 自定义的比较器(实现Comparator接口中的compare()方法)
Arrays.sort(people, new PersonCompartor());
System.out.print("\n排序后:");
for (Person person : people) {
System.out.print(person.getName() + ":" + person.getAge() + ",");
}
}
}
b.结果
排序前:xujian:20,xiewei:10,
排序后:xiewei:10,xujian:20,
3 异常
3.1 [1]图示
01.图示
Throwable
├── Exception
│ ├── 一般讨论的异常 (程序员需要处理)
│ ├── 运行时异常
│ │ ├── RuntimeException
│ │ ├── ClassCastException:类转换异常
│ │ ├── ArrayStoreException:数据存储异常
│ │ ├── NullPointerException:空指针异常
│ │ ├── IllegalArgumentException:非法参数异常
│ │ ├── IndexOutOfBoundsException:数组越界异常
│ │ ├── ...
│ ├── 统称(CheckedException)非运行时异常(检查异常)
│ ├── ClassNotFoundException
│ ├── IOException
│ ├── ...
├── Error
├── 错误 (程序员不能处理)
├── LinkageError
├── VirtualMachineError
├── ...
02.注意
值得注意的一点是,确实存在CheckedException,但却不是继承Exception
3.2 [1]Throwable、Error、Exception
01.Throwable、Error、Exception
a.Throwable
Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。
b.Error错误
Error是错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,
这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,
因此应用程序不应该试图使用catch块来捕获Error对象。
在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。
b.Exception异常
Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。
所有的RuntimeException类及其子类的实例被称为Runtime异常;
不是RuntimeException类及其子类的异常实例则被称为Checked异常。
Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。
如果程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。
Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,
也可以使用try...catch块来实现。
3.3 [1]final、finally、finalize
01.final、finally、finalize
a.final可以修饰类、变量、方法
1.修饰【类】表示该类不能被继承
2.修饰【方法】表示该方法不能被重写
3.修饰【变量】表示该变量是一个常量不能被重新赋值。
b.finally一般作用在try-catch代码块中
在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常
该代码块都会执行,一般用来存放一些关闭资源的代码。
c.finalize是一个方法,属于Object类的一个方法
Object类是所有类的父类,使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作
3.4 [1]try、catch、finally、throws、throw
01.关键字
a.try
将可能发生异常的代码,用包裹起来
如果try中的代码的确发生了异常,则程序不再执行try中异常之后的代码,而是直接跳到catch中执行
b.catch
捕获特定类型的异常
两个catch(已知类型的异常可以捕获,但是未知类型的异常“无法捕获其准确的类型,需要交由Exception e捕获”)
捕获类型的范围大小先小范围,后大范围
c.finally
无论正常,还是异常,始终都会执行的代码
不论执行完try,还是执行完catch,最终都会执行finally的代码
无论如何都会执行? 即使遇到return,也仍然先执行finally,再执行return
什么时候不会执行finally? 除非虚拟机关闭,才不会执行finally
d.throws
自己(当前方法)不能处理,使用throws,上交给上级(方法调用处)处理
场景:辅导员不能够处理的“武汉疫情”throws上报领导
e.throw一般和自定义异常一起使用
JDK中自带了很多类型的异常,但如果这些内置的异常仍然不能满足项目的需求,那么就需要创建自定义异常
如何编写自定义异常?1.继承Exception,构造方法调用super("异常信息")
2.使用throw声明一个自定义异常,并且进行try catch和throws处理异常
02.对比
a.try catch、throws
a.try catch
自己(当前方法)能够处理,使用try catch
场景:辅导员能够处理的“感冒、发烧”try catch
b.throws
自己(当前方法)不能处理,使用throws,上交给上级(方法调用处)处理
场景:辅导员不能够处理的“武汉疫情”throws上报领导
b.throw、throws
a.throw
表示方法内抛出某种异常对象(只能是一个)
用于程序员自行产生并抛出异常
位于方法体内部,可以作为单独语句使用
如果异常对象是非 RuntimeException 则需要在方法申明时加上该异常的抛出,即需要加上 throws 语句 或者 在方法体内 try catch 处理该异常,否则编译报错
执行到 throw 语句则后面的语句块不再执行
b.throws
方法的定义上使用 throws 表示这个方法可能抛出某些异常(可以有多个)
用于声明在该方法内抛出了异常
必须跟在方法参数列表的后面,不能单独使用
需要由方法的调用者进行异常处理
c.总结
throws:关键字用在【方法内部】,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出
throw:关键字用在【方法声明上】,可以抛出多个异常,用来标识该方法可能抛出的异常列表。
3.5 [2]常见问题:20例
00.汇总
a.为什么尽量不要捕获 RuntimeException?
RuntimeException 应该通过预检查来规避
例如使用 if (obj != null) 来避免 NullPointerException
对于无法预检查的异常,如 NumberFormatException,可以使用 catch 捕获处理
b.为什么要使用 try-with-resource 来关闭资源?
try-with-resource 可以自动关闭资源,避免在 try 块中直接关闭资源导致的资源泄漏问题
若资源未实现 AutoCloseable 接口,则在 finally 块中关闭
c.为什么不要捕获 Throwable?
捕获 Throwable 会掩盖程序无法处理的严重错误,
如 OutOfMemoryError 和 StackOverflowError,这些错误不应被程序捕获
d.为什么不要省略异常信息的记录?
省略异常信息会导致无法追踪和解决问题,应记录异常信息以便后续分析和调试
e.为什么不要记录了异常又抛出异常?
记录后再抛出异常会导致重复记录,增加混乱。应选择记录或抛出其中之一
f.为什么不要在 finally 块中使用 return?
finally 块中的 return 会覆盖 try 块中的 return,导致意外的返回值
g.为什么要抛出具体定义的检查性异常而不是 Exception?
抛出具体异常有助于其他开发者理解代码意图,并根据异常类型采取适当的处理措施
h.为什么要捕获具体的子类而不是 Exception 类?
捕获具体子类可以更准确地处理异常,避免捕获不必要的异常,提升程序的健壮性和稳定性
i.自定义异常时为什么不要丢失堆栈跟踪?
丢失堆栈跟踪会导致无法追踪异常来源。应保留原始异常的堆栈信息
j.为什么 finally 块中不要抛出任何异常?
finally 中抛出的异常会覆盖 try 块中的异常,导致原始异常信息丢失
k.为什么不要在生产环境中使用 printStackTrace()?
printStackTrace() 可能暴露敏感信息并影响性能,应使用日志系统记录异常信息
l.对于不打算处理的异常,为什么直接使用 try-finally?
try-finally 可以确保资源清理,而不需要处理异常
m.什么是“早 throw 晚 catch”原则?
尽早抛出异常以便及时处理,尽晚捕获异常以获取更多上下文信息
n.为什么只抛出和方法相关的异常?
抛出相关异常可以使代码更清晰,便于维护和理解
o.为什么切勿在代码中使用异常来进行流程控制?
使用异常进行流程控制会降低代码可读性和性能,应使用合适的控制结构
p.为什么要尽早验证用户输入?
尽早验证可以避免数据库不一致状态,提高程序的健壮性
q.为什么一个异常只能包含在一个日志中?
在多线程环境中,分散的日志信息会导致难以追踪问题,应将相关信息记录在一起
r.为什么要将所有相关信息尽可能地传递给异常?
完整的异常信息和堆栈跟踪有助于定位和解决问题
s.为什么要终止掉被中断线程?
InterruptedException 提示应停止当前操作,继续执行可能导致不稳定
t.对于重复的 try-catch,为什么使用模板方法?
模板方法可以减少代码重复,提高代码的可维护性
3.6 [2]Runtime异常 、非Runtime异常
00.总结
a.汇总
a.检查时间
运行时:异常在运行时检查
非运行时:异常在编译时检查
b.处理要求
运行时:异常不需要显式处理
非运行时:异常必须显式处理(捕获或声明)
c.继承关系
运行时:异常继承自java.lang.RuntimeException
非运行时:异常继承自java.lang.Exception但不继承自java.lang.RuntimeException
b.说明
a.Checked异常和Unchecked异常
Checked异常必须在编译时处理(捕获或声明),而Unchecked异常不需要
Checked异常继承自java.lang.Exception但不继承自java.lang.RuntimeException,而Unchecked异常继承自java.lang.RuntimeException
b.Runtime异常和非Runtime异常
Runtime异常是java.lang.RuntimeException及其子类的实例,非Runtime异常是java.lang.Exception及其子类的实例,但不包括java.lang.RuntimeException及其子类
Runtime异常在运行时检查,非Runtime异常在编译时检查
c.交集和区别
所有Runtime异常都是Unchecked异常,但并非所有Unchecked异常都是Runtime异常
所有Checked异常都是非Runtime异常,但并非所有非Runtime异常都是Checked异常
01.运行时异常(Runtime Exception)
a.定义
运行时异常是在程序运行期间可能发生的异常。这些异常是java.lang.RuntimeException类及其子类的实例。
b.特点
运行时检查:这些异常在运行时才会被检查,编译器不会强制要求处理。
不需要显式处理:程序员可以选择处理这些异常,但不是必须的。
常见类型:常见的运行时异常包括NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException等.
c.示例
public class RuntimeExceptionExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // 这将抛出ArrayIndexOutOfBoundsException
}
}
在这个例子中,访问数组的非法索引会抛出ArrayIndexOutOfBoundsException,这是一个运行时异常,编译器不会强制要求处理。
02.非运行时异常(非Runtime Exception)
a.定义
非运行时异常是在编译时被检查的异常。这些异常是java.lang.Exception类及其子类的实例,但不包括java.lang.RuntimeException及其子类。
b.特点
编译时检查:编译器在编译时会检查这些异常是否被捕获或声明。
必须处理:程序员必须显式地捕获这些异常(使用try-catch块)或在方法签名中声明(使用throws关键字)。
常见类型:常见的非运行时异常包括IOException、SQLException、ClassNotFoundException等.
c.示例
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class NonRuntimeExceptionExample {
public static void main(String[] args) {
try {
File file = new File("test.txt");
FileReader fr = new FileReader(file);
} catch (IOException e) {
System.out.println("File not found");
}
}
}
在这个例子中,FileReader的构造函数可能会抛出IOException,这是一个非运行时异常,因此必须被捕获或声明。
3.7 [2]Checked异常、Unchecked异常
00.总结
a.汇总
a.检查时间
Checked异常:在编译时检查
Unchecked异常:在运行时检查
b.处理要求
Checked异常:必须显式处理(捕获或声明)
Unchecked异常:不需要显式处理
c.继承关系
Checked异常:继承自java.lang.Exception但不继承自java.lang.RuntimeException
Unchecked异常:继承自java.lang.RuntimeException
b.说明
a.Checked异常和Unchecked异常
Checked异常必须在编译时处理(捕获或声明),而Unchecked异常不需要
Checked异常继承自java.lang.Exception但不继承自java.lang.RuntimeException,而Unchecked异常继承自java.lang.RuntimeException
b.Runtime异常和非Runtime异常
Runtime异常是java.lang.RuntimeException及其子类的实例,非Runtime异常是java.lang.Exception及其子类的实例,但不包括java.lang.RuntimeException及其子类
Runtime异常在运行时检查,非Runtime异常在编译时检查
c.交集和区别
所有Runtime异常都是Unchecked异常,但并非所有Unchecked异常都是Runtime异常
所有Checked异常都是非Runtime异常,但并非所有非Runtime异常都是Checked异常
01.Checked异常
a.定义
Checked异常是在编译时被检查的异常。这意味着在编译代码时,编译器会检查这些异常是否被正确处理
如果没有处理这些异常,编译器会报错
b.特点
编译时检查:编译器在编译时会检查这些异常是否被捕获或声明
必须处理:程序员必须显式地捕获这些异常(使用try-catch块)或在方法签名中声明(使用throws关键字)
继承关系:Checked异常是直接或间接继承自java.lang.Exception类,但不继承自java.lang.RuntimeException类
c.示例
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class CheckedExceptionExample {
public static void main(String[] args) {
try {
File file = new File("test.txt");
FileReader fr = new FileReader(file);
} catch (IOException e) {
System.out.println("File not found");
}
}
}
在这个例子中,FileReader的构造函数可能会抛出IOException,这是一个Checked异常,因此必须被捕获或声明。
02.Unchecked异常
a.定义
Unchecked异常是在运行时被检查的异常。这意味着在编译代码时,编译器不会检查这些异常是否被处理
b.特点
运行时检查:这些异常在运行时才会被检查,编译器不会强制要求处理
不需要显式处理:程序员可以选择处理这些异常,但不是必须的
继承关系:Unchecked异常是直接或间接继承自java.lang.RuntimeException类
c.示例
public class UncheckedExceptionExample {
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // 这将抛出ArrayIndexOutOfBoundsException
}
}
在这个例子中,访问数组的非法索引会抛出ArrayIndexOutOfBoundsException,这是一个Unchecked异常,编译器不会强制要求处理
3.8 [2]try-with-resources、try-catch-finally
00.汇总
a.资源管理
try-with-resources:自动管理资源,确保在语句结束时关闭资源
try-catch-finally:需要显式管理资源,确保在finally块中关闭资源
b.代码简洁性
try-with-resources:代码更简洁、更易读,减少了显式关闭资源的代码
try-catch-finally:代码相对冗长,需要显式关闭资源
c.适用范围
try-with-resources:适用于实现了AutoCloseable接口的资源
try-catch-finally:适用于所有需要显式管理的资源
01.try-with-resources
a.定义
try-with-resources是Java 7引入的一种异常处理机制,用于自动管理资源
资源是指在程序完成后必须关闭的对象(如文件、数据库连接等)
try-with-resources语句确保每个资源在语句结束时被关闭
b.特点
自动关闭资源:在try块结束时,自动关闭实现了AutoCloseable接口的资源
简化代码:减少了显式关闭资源的代码,降低了出错的可能性
提高可读性:代码更简洁、更易读
c.使用方式
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResourcesExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
}
}
}
-----------------------------------------------------------------------------------------------------
在这个例子中,BufferedReader和FileReader都是实现了AutoCloseable接口的资源。
try-with-resources语句确保在try块结束时自动关闭这些资源。
02.try-catch-finally
a.定义
try-catch-finally是Java中最常见的异常处理机制。它允许程序员捕获和处理异常,
并在finally块中执行一些清理操作,无论是否发生异常
b.特点
显式处理异常:程序员可以显式捕获和处理异常
清理操作:finally块中的代码无论是否发生异常都会执行,通常用于清理资源
灵活性:可以捕获多个异常类型,并根据需要进行不同的处理
c.使用方式
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryCatchFinallyExample {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("test.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("An error occurred: " + e.getMessage());
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException ex) {
System.out.println("Failed to close the reader: " + ex.getMessage());
}
}
}
}
-----------------------------------------------------------------------------------------------------
在这个例子中,BufferedReader和FileReader在try块中被显式创建,
并在finally块中显式关闭。无论是否发生异常,finally块中的代码都会执行
3.9 [3]try-catch竟然会影响性能?链路变深
01.try-catch竟然会影响性能
a.情况1:没有异常发生
try-catch其实不会影响程序性能,因为在没有异常发生时,代码执行链路不会加深
b.情况2:出现异常
但是如果出现异常,那么程序性能就会受到影响,表现在如下两个方面
1.异常对象创建有性能开销。具体表现在异常对象创建时会去爬栈得到方法调用链路信息
2.try-catch捕获到异常后会让代码执行链路变深
c.建议
因此在日常开发中,可以适当增加防御性编程来防止JVM抛出异常
也建议【尽量将主动的异常抛出替换为提前返回响应】,总之就是【尽量减少非必要的异常】出现
3.10 [3]try-catch-finally中哪个部分可以省略?catch/finally
01.try-catch-finally中哪个部分可以省略?
a.回答
catch、finally省略其中一个
b.原因
如果catch、finally都省略,编译会报错
3.11 [3]return与finally执行顺序,对返回值的影响
01.return与finally执行顺序,对返回值的影响
a.顺序1:try 块中的 return
如果 try 块中有 return 语句,finally 块仍然会执行。
在执行 try 块中的 return 语句之前,Java 会先记录下要返回的值,然后执行 finally 块。
b.顺序2:finally 块的执行:
无论 try 块中是否有异常抛出,finally 块都会执行。
这是 finally 块的一个重要特性,用于确保资源的释放或其他清理操作。
c.顺序3:finally 块中的 return:
如果 finally 块中也有 return 语句,那么这个 return 语句会覆盖 try 块中的 return 语句的返回值。
最终的返回值将是 finally 块中 return 的值。
b.总结
1.finally 块总是会执行,无论 try 块中是否有异常或 return
2.如果 finally 块中有 return 语句,它将覆盖 try 块中的 return 语句的返回值
3.如果 finally 块中没有 return 语句,try 块中记录的返回值将被返回,即使在 finally 中对返回值进行了修改
3.12 [4]finally语句块一定执行吗?不一定
01.finally语句块一定执行吗?
a.回答
不一定
b.3种例外情况
直接返回未执行到 try-finally 语句块
抛出异常未执行到 try-finally 语句块
系统退出未执行到 finally 语句块
01.在执行try块之前直接return
a.代码示例
public class TryCatchTest {
private static int total() {
int i = 11;
if (i == 11) {
return i;
}
try {
System.out.println("执行try");
} finally {
System.out.println("执行finally");
}
return 0;
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行main:11
c.结论
在执行try块之前直接return,finally块不会执行。
02.在执行try块之前制造一个错误
a.代码示例
public class TryCatchTest {
private static int total() {
return 1 / 0;
try {
System.out.println("执行try");
} finally {
System.out.println("执行finally");
}
return 0;
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.结论
如果程序连try块都执行不到,finally块自然不会执行。
03.执行try块后退出JVM
a.代码示例
public class TryCatchTest {
private static int total() {
try {
System.out.println("执行try");
System.exit(0);
} catch (Exception e) {
System.out.println("执行catch");
} finally {
System.out.println("执行finally");
}
return 0;
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try
c.结论
在执行try块中退出JVM,finally块不会执行。
04.finally执行时机
a.常规情况
a.代码示例
public class TryCatchTest {
private static int total() {
try {
System.out.println("执行try");
return 11;
} finally {
System.out.println("执行finally");
}
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try
执行finally
执行main:11
c.结论
finally块执行在try块的return之前。
b.try块中抛出异常
a.代码示例
public class TryCatchTest {
private static int total() {
try {
System.out.println("执行try");
return 1 / 0;
} catch (Exception e) {
System.out.println("执行catch");
return 11;
} finally {
System.out.println("执行finally");
}
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try
执行catch
执行finally
执行main:11
c.结论
finally执行在catch块return的执行前。
05.finally块中的返回值
a.不含返回值但改变变量值
a.代码示例
public class TryCatchTest {
private static int total() {
int i = 0;
try {
System.out.println("执行try:" + i);
return i;
} finally {
++i;
System.out.println("执行finally:" + i);
}
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try:0
执行finally:1
执行main:0
c.结论
Java程序会保留try或catch块中的返回值,finally块执行后返回保留值
b.含有返回值
a.示例1
a.代码示例
public class TryCatchTest {
private static int total() {
try {
System.out.println("执行try");
return 1;
} finally {
System.out.println("执行finally");
return 2;
}
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try
执行finally
执行main:2
b.示例2
a.代码示例
public class TryCatchTest {
private static int total() {
int i = 1;
try {
System.out.println("执行try:" + i);
return i;
} finally {
++i;
System.out.println("执行finally:" + i);
return i;
}
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try:1
执行finally:2
执行main:2
c.示例3
a.代码示例
public class TryCatchTest {
private static int total() {
int i = 1;
try {
System.out.println("执行try:" + i);
} finally {
++i;
System.out.println("执行finally:" + i);
}
return i;
}
public static void main(String[] args) {
System.out.println("执行main:" + total());
}
}
b.输出结果
执行try:1
执行finally:2
执行main:2
d.结论
在finally块中进行return操作,方法整体的返回值为finally块中的return值。
3.13 [4]finalize()方法到底什么时候被调用?
00.回答
finalize()方法在对象生命周期的最后阶段被调用,用来释放资源并执行清理工作
它是Java垃圾回收机制的一部分,但由于不确定性和性能开销,已经不再是资源管理的首选方法
为了更好地管理资源,我们可以使用try-with-resources语句来替代finalize(),让代码更简洁、性能更高效
01.finalize()方法介绍
a.定义
finalize()方法是Java中的一个特殊方法,它属于java.lang.Object类
意味着所有Java类都继承了这个方法。它是Java中用于处理对象销毁前的一项清理工作的一种机制
b.作用
每当对象在垃圾回收器(GC)准备回收的时候,如果对象重写了finalize()方法
Java虚拟机(JVM)会在对象被销毁前自动调用它。
c.特点
finalize()方法是protected的,而不是public的,这意味着它不能被外部类直接调用
它接受Throwable异常,因此如果在finalize()方法中抛出了异常,它不会影响到程序的执行
02.finalize()方法什么时候被调用?
a.调用时机
finalize()方法的调用时机是由JVM的垃圾回收器(GC)来决定的
垃圾回收器会定期检查堆内存,找到不再被引用的对象,并标记它们为垃圾对象
当垃圾回收器准备清理对象时,Java虚拟机会先调用这些对象的finalize()方法
b.非即时性
finalize()方法的调用并不是即时的,它会等到垃圾回收器准备销毁对象时才会调用
因此,并不能保证finalize()会立即在对象销毁之前被调用
03.为什么需要finalize()?
a.资源的管理与清理
在开发过程中,有时候我们可能会创建一些特殊的资源,比如文件句柄、数据库连接、网络连接等
这些资源在使用后是需要及时释放的,否则可能导致资源泄漏。finalize()方法可以用来释放这些非内存资源
b.作为最后一道安全防线
finalize()方法可以看作是最后一道安全防线,它确保在对象生命周期结束时,不会留下遗憾
即便我们忘记了释放某些资源,finalize()方法也可以补救,进行最后一次的清理工作
04.finalize()方法的缺点
a.不确定性
finalize()方法的调用时机是由垃圾回收器决定的
我们无法控制它何时执行,因此无法确保在某个特定时刻释放资源
b.性能开销
垃圾回收器在回收对象时,会先调用对象的finalize()方法
这一过程会增加额外的性能开销,尤其是当对象数量非常庞大时,性能开销会变得非常明显
c.不能抛出异常
在finalize()方法中,我们不能抛出异常。即使抛出异常,它也不会影响到垃圾回收器的执行
05.finalize()的替代方案:try-with-resources语句
a.说明
Java引入了try-with-resources语句,它通过自动关闭资源来避免资源泄漏
b.代码
try (FileReader reader = new FileReader("example.txt")) {
// 读取文件内容
} catch (IOException e) {
e.printStackTrace();
}
c.说明
try-with-resources语句确保了即使在异常发生时
FileReader对象也会在作用域结束时自动关闭,无需显式调用finalize()
4 对象
4.1 [1]变量
01.变量命名
a.图示
【首字母】 【其余部分】
字母 字母
变量名 = 下划线_ 下划线_
美元$ 美元$
数字
变量第一字符只能:字母、_、$ 变量只能使用:字母、_、$、数字
变量第一字符不能:数字
中文也可命名变量,但不建议
b.原则
1.首字母:各国语言,下划线,钱
2.其他:首字母+数字
3.不能是关键字(idea中蓝色字体都是关键字)
4.符号只能是下划线、钱
建议:驼骆峰myFirstNum =10;
02.规范命名
a.图示
包名 全部小写 域名倒置
类名 首字母大写+驼峰原则 Man,GoodMan
方法名 首字母小写+驼峰原则 run(), runRun()
全局变量 首字母小写+驼峰原则 monthSalary
局部变量 首字母小写+驼峰原则 monthSalary
常量 全部字母大写+下划线 MAX_VALUE
b.原则
所有变量、方法、类名:见名知意
类成员变量:首字母小写和驼峰原则: monthSalary
局部变量:首字母小写和驼峰原则
常量:大写字母和下划线:MAX_VALUE
类名:首字母大写和驼峰原则: Man, GoodMan
方法名:首字母小写和驼峰原则: run(), runRun()
03.变量含义标识符:名词性词组
变量名 目标词 + 动词(的过去分词) + [状语] + [目的地]
DataGotFromSD Data Got Form SD 从SD中获取数据
DataDeleteFromSD Data Deleted Form SD 从SD中删除数据
04.函数含义标识符:动词性词组
变量名 动词(一般现在时)+ 目标词 + [状语] + [目的地]
GetDataFromSD Get Data Form SD 从SD中获取数据
DeleteDataFromSD Delete Data Form SD 从SD中删除数据
4.2 [1]常量
01.常量
a.常量值(也称字面量)
a.数字
整数 小数
十进制数 由数字和小数点组成,且必须有小数点 单精度 float类型后面一定要加f(F)
八进制整型常量 以0开头 双精度 默认double
十六进制整型常量 以0x或0X开头
长整型 以L作结尾
b.非数字
字符型 字符常量(两个单引号)、字符串常量(两个双引号)
布尔型 只有两个值,false(假)和 true(真)
b.定义常量,主要是用来避免魔法数字和提高代码可读性
a.四种实现方式
接口常量 接口变量默认public static final
类常量 类常量显示声明public static final
枚举
文件,如Properties文件
02.定义常量,主要是用来避免魔法数字和提高代码可读性
a.四种实现方式
接口常量 接口变量默认public static final
类常量 类常量显示声明public static final
枚举
文件,如Properties文件
b.实例
public interface ConstantInterface {
String SUNDAY = "SUNDAY"; 接口变量默认public static final
}
-----------------------------------------------------------------------------------------------------
public class ConstantClass {
public static final int SUNDAY = 0; 类常量显示声明public static final
}
public final class ConstantClass {
public static final int SUNDAY = 0; 类使用 final class 定义
}
public class ConstantClass {
private static final int SUNDAY = 0;
public static String getSunday() { 类使用 get方法 获取
return SUNDAY;
}
}
-----------------------------------------------------------------------------------------------------
enum Color {
RED, GREEN, BLUE;
}
enum Size {
SMALL("S"),
MEDIUM("M"),
private String suoxie;
private Size(String suoxie){
this.suoxie = suoxie;
}
public String getSuoxie(){
return suoxie;
}
}
enum Dict {
PROSTA("PROSTA","产品状态"),
COUNTRY("COUNTRY","国家");
private final String value;
private final String name;
Dict(String value, String name){
this.value=value;
this.name=name;
}
public String getValue() {
return value;
}
public String getName() {
return name;
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Color.BLUE);
System.out.println(Size.MEDIUM.getSuoxie());
System.out.println(Dict.PROSTA.getName());
System.out.println(Dict.PROSTA.getValue());
}
}
4.3 [1]正则
01.正则表达式 = 转义字符 + 元字符,ASCII,转义字符串,【JAVA默认Unicode编码,兼容ASCII编码】
a.转义字符
转义字符 意义 ASCII码值(十进制)
\a 响铃(BEL) 007
\b 退格(BS) ,将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF) ,将当前位置移到下一行开头 010
\r 回车(CR) ,将当前位置移到本行开头 013
\t 水平制表(HT) (跳到下一个TAB位置) 009
\v 垂直制表(VT) 011
\\ 代表一个反斜线字符''\' 092
\' 代表一个单引号(撇号)字符 039
\" 代表一个双引号字符 034
\? 代表一个问号 063
\0 空字符(NUL) 000
\ddd 1到3位八进制数所代表的任意字符 三位八进制
\xhh 十六进制所代表的任意字符 十六进制
b.元字符
^ --断言目标的开始位置(或多行模式下的行首位置)
$ --断言目标的介绍位置(或多行模式下的结尾位置)
. --匹配除换行符外的其他任何字符
[ --匹配字符类定义
] --介绍内字符定义
| --开始一个可选分支
( --子组的开始标记
) --子组的结束标记
? --量词,表示0次或多次
* --量词,0次或多次匹配
+ --量词,1次或多次匹配
{ --自定义量词的开始标记
} --自定义量词的结束标记
-----------------------------------------------------------------------------------------------------
\d --数字
\D --非数字
\s --空白字符
\S --非空白字符
\w --单词字符
\W --非单词字符
c.ASCII
a-z 十进制码是97-122,八进制码是141-172,"\141" === "a" && "\141" === "\a" ->true
A-Z 十进制码是65-90,八进制码是101-132, "\101" === "A" && "\101" === "\A" -> true
0-9 十进制是48-57,八进制码是060-071,"\060" === "0" && "\060" === "\0" ->true
-----------------------------------------------------------------------------------------------------
ASCII转义字符与Regex转义字符,二者的转义字符是同一样东西,都代表特殊意义的字符;
ASCII可以描述一些无法显示的特殊字符,因此,可以应用于任何地方;
ASCII中的转义字符可以用于【正则匹配】,甚至可以将【正则匹配中的转义字符、元字符,变为对应的ASCII用于正则】
d.转义字符串(Escape String),即字符实体(Character Entity)
第一部分:一个&符号,英文叫ampersand;第二部分:实体名字,或者#加上实体编号;第三部分:一个;分号
-----------------------------------------------------------------------------------------------------
html转义符、java转义符、xml转义符、 oracle转义符、sql转义符
4.4 [1]运算符
01.优先级
括号 () [] {}
自增 a++ a-- ++a --a
按位取反 ~
逻辑非 !
---------------------------------------------------------------------------------------------------------
算数运算符 * / % + -
---------------------------------------------------------------------------------------------------------
位运算符 >> >>> <<
关系运算符 > < >= <= == !=
---------------------------------------------------------------------------------------------------------
位运算符 & ^ |
---------------------------------------------------------------------------------------------------------
逻辑运算符 && ||
---------------------------------------------------------------------------------------------------------
条件运算符 ? : ;
赋值运算符 = += -= *= /= %= <<= >>= &= |= ^=
---------------------------------------------------------------------------------------------------------
instanceof 保留关键字,二元操作符,类似==、>、<),例如,if ("sss" instanceof String) { ... }
instanceof 判断【其左边对象是否为其右边类的实例】,也可以用来判断【继承中的子类的实例是否为父类的实现】
instanceof bc.equals(c) 需比较 c instanceof BigCar
02.分类
a.算数运算符(6个)(JAVA中仅有的两个重载过的运算符:String类中的"+"、"+=")
++ 自增 ++a,--a 前缀 先自增/自减,再使用
-- 自减 a++, a-- 后缀 先使用,再自增/自减
-----------------------------------------------------------------------------------------------------
* 数字"*"
/ 数字"/"
% 数字"%":求余取模(有符号问题,结果永远和"被除数一致":10%-3=1,-10%-3=-1,10%3=1,-10%3=1)
-----------------------------------------------------------------------------------------------------
+ 数字"+"、字符串拼接"+" 坑:num += 10; 不同于 num =+ 10;(等价于num=10,+10代表"正数+10")
- 数字"-"
b.关系运算符
>
<
>=
<=
-----------------------------------------------------------------------------------------------------
==
!=
c.位运算符:仅对byte,short,char —> int —> long有效,不涉及float —> double
~ 按位取反 翻转操作数的每一位,即0变成1,1变成0
-----------------------------------------------------------------------------------------------------
<< 按位左移 左操作数按位左移右操作数指定的位数
>>> 按位右移补零 左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充
>> 按位右移 左操作数按位右移右操作数指定的位数
-----------------------------------------------------------------------------------------------------
& 按位与 如果相对应位都是1,则结果为1,否则为0
^ 按位异或 如果相对应位值相同,则结果为0,否则为1
| 按位或 如果相对应位都是0,则结果为 0,否则为 1
-----------------------------------------------------------------------------------------------------
一个符号:不根据真值表进行判断(两边都会判断),位运算符
Console.log(2<1 & 1/0 == 0); | 报错,Exception in :by zero
Console.log(2<1 | 1/1 == 0); | false
d.逻辑运算符
! 逻辑非 用来反转操作数的逻辑状态,如果条件为true,则逻辑非运算符将得到false
-----------------------------------------------------------------------------------------------------
&& 逻辑与 当且仅当两个操作数都为真,条件才为真
|| 逻辑或 如果任何两个操作数任何一个为真,条件为真
-----------------------------------------------------------------------------------------------------
两个符号:根据真值表进行判断(短路特性),逻辑运算符
Console.log(2<1 && 1/0 == 0); | false && ... = false
Console.log(2>1 || 1/0 == 0); | true || ... = true
e.条件运算符(三目运算符)
X x = 布尔值判断? 为真: 为假;
boolean myBoolean = (myInt == 0) ? false : true;
f.赋值运算符
=
-----------------------------------------------------------------------------------------------------
+=
-=
*=
/=
%=
-----------------------------------------------------------------------------------------------------
<<=
>>=
&=
|=
^=
4.5 [1]switch语句
00.汇总
a.jdk5以前
switch()中,只能是 byte、short、char、int
b.jdk5开始
引入了枚举类型,switch中也可以是enum类型
c.jdk7开始
switch中还可以是字符串(String)
01.JDK8
int rank = 2;
switch (rank) {
case 1:
System.out.println("笔记本电脑");
case 2:
System.out.println("鼠标垫");
case 3:
System.out.println("夏令营");
case 4:
System.out.println("测试");
break;
default:
System.out.println("不奖励");
break;
}
02.JDK21
a.介绍
支持返回值,且可以使用更简洁的 Lambda 风格语法。
b.示例
String message = switch (day) {
case "MONDAY" -> "Start of the week";
case "FRIDAY" -> "End of the work week";
default -> "Midweek";
};
b.示例
// 类型匹配 + null处理
String describe = switch(obj) {
case Integer i -> "整数: " + i;
case String s && s.length()>5 -> "长字符串";
case null -> "空对象";
default -> "未知类型";
};
4.6 [1]boolean转换
01.boolean转换(0 false,1 true)【由于boolean只能存放true或false,不兼容整数或字符,因此不参与“自动类型转换”】
a.对应关系
boolean转化为int 0 false 1 true JAVA本身不支持直接强转,boolean与Int无法转换
int转化为boolean 0 false 非0 true
b.boolean转化为int:唯一方法(三目运算符)
boolean myBoolean = true;
int myInt = myBoolean ? 1 : 0;
System.out.println("boolean true 对应 int " + myInt); | boolean true 对应 int 1
-------------------------------------------------------------------------------------------------
boolean myBoolean = false;
int myInt = myBoolean ? 1 : 0;
System.out.println("boolean false 对应 int " + myInt); | boolean false 对应 int 0
c.int转化为boolean
int myInt = 2;
boolean myBoolean = myInt!= 0;
System.out.println("int " + myInt + " 对应 boolean " + myBoolean);| int 2 对应 boolean true
-------------------------------------------------------------------------------------------------
int myInt = 0;
boolean myBoolean = myInt != 0;
System.out.println("int " + myInt + " 对应 boolean " + myBoolean);| int 0 对应 boolean false
-------------------------------------------------------------------------------------------------
int myInt = 2;
boolean myBoolean = (myInt == 0) ? false : true;
System.out.println("int " + myInt + " 对应 boolean " + myBoolean);| int 0 对应 boolean true
-------------------------------------------------------------------------------------------------
int myInt = 0;
boolean myBoolean = (myInt == 0) ? false : true;
System.out.println("int " + myInt + " 对应 boolean " + myBoolean);| int 0 对应 boolean false
d.为什么Java布尔值只接受true或false,而不接受1或0?
与C和C++之类的语言不同,Java将boolean视为完全独立的数据类型,它具有2个不同的值:true和false
与C和C++之类的语言不同,Java将值1和0的类型为int的数据类型,而int不能隐式转换为boolean
在JVM层面,boolean、byte、short、char、int当作int处理,而boolean为0和1,因此“不可隐式转换”仅在Java层面
4.7 [1]选择、循环、中断
00.总结
a.if-else、if-else、switch
a.if-else-if-else
适合分支较少
判断条件类型不单一
支持取 boolean 类型的所有运算
满足条件即停止对后续分支语句的执行
b.switch
适合分支较多
判断条件类型单一,JDK1.0-1.4 数据类型接受 byte short int char; JDK1.5 数据类型接受 byte short int char enum; JDK1.7 数据类型接受 byte short int char enum String
没有 break 语句每个分支都会执行
b.while、do-while
while 先判断后执行,第一次判断为 false,循环体一次都不执行
do-while 先执行后判断,最少执行1次
c.break
结束当前循环并退出当前循环体
结束 switch 语句
d.continue
结束本次循环,循环体后续的语句不执行
继续进行循环条件的判断,进行下一次循环体语句的执行
01.选择语句(数轴):if与else对立;switch(类似if...else);分支多(switch优于if...else)
a.if
简单if:赋值=,而非 关系==
多重if:大于,越大越优秀;小于,越小越优先;【if...else if...推荐改写为三目运算符 boolean ? ture: false;】
嵌套if:数轴
区别:if判断一件事/多件事;多重if判断一件事;嵌套if判断多件事
b.switch:类似if..else
a.用法
switch 使用“rank=2 与 全部case(1,2,3,4,5...)”进行匹配;
switch 从匹配“case=2”开始,再接着匹配“case=3, 4, 5”,“最后运行defalt语句”,不遇到break不会停止;
switch 如果没有匹配项,则执行defalt语句;
b.总结
switch 支持的表达式:byte,short,char —> int,String,enum
case 必须是常量,int类型,case值不能重复
default 可省略,default要放在最后
break 跳出整个switch语句,并且继续执行该循环下面的语句,直至循环结束
结束标志 break、最后的}
c.多重if结构和switch有什么区别?
a.switch
switch只能判断离散的单点值,例如1、2、3..(注意:1和2之间,有无穷多个小数)
b.多重if
多重if既能判断离散的单点值,也能判断区间值(注意:>90就是一个区间值)
c.场景
能用switch实现“如果<0,标记寒冷”吗?不能,但是多重if可以
02.循环语句:循环来自生活中的重复
a.类型
while 先判断后执行 【if不更新变量,while/do...while/for更新变量】
do...while 先执行后判断 【需要即使不满足条件,也至少执行一次】
for 已知循环次数,已知始末条件 【for有两种类型,可以等价替换为while、do...while】
for 增加型for循环,变量值相当于num[i] 【增强型for循环,优于传统for循环】
for 不要使用for循环遍历容器元素,删除元素 【正确用法:遍历容器的迭代器(Iterator),删除元素】
b.for是while的变体
a.示例1
int[] nums = new int[]{1, 2, 3, 4}
for (int i=0; i<num.length; i++) { for (初始值; 循环条件; 更新变量) {
System.out.println(num[i]); 循环操作
} }
b.示例2
int[] nums = new int[]{1, 2, 3, 4} // JAVA1.5引入增加型for循环,变量值相当于num[i]
for (int i: nums) { for (元素类型 变量值: 数组){
System.out.println(i) 循环操作
} }
03.中断语句
a.break
【switch/当前循环】,【跳出最里层循环,并且继续执行该循环下面的语句】
b.continue
【当前循环】,【让程序立刻跳转到下一次循环的迭代,for跳到更新语句,while/do while跳到布尔表达式判断】
c.return
【函数语句】,【跳出整个函数体,函数体后面的部分不再执行】
d.总结
a.break
当前循环,内部for【终止】,外部for【无影响】;【if不更新变量,while/do...while/for更新变量】
b.continue
当前循环,内部for【跳过内层3】,外部for【无影响】;【if不更新变量,while/do...while/for更新变量】
c.return
【break、continue仅对当前层次的循环有效】;【return直接跳出函数体(包括全部循环、函数体后面的语句)】
4.8 [2]基础类型
01.介绍
类型 默认值 位数 范围
byte 0 8 bits -128 to 127
short 0 16 bits -32768 to 32767
int 0 32 bits -2147483648 to 2147483647
float 0.0f 32 bits ±1.4E-45 to ±3.4028235E+38
long OL 64 bits -9223372036854775808 to 9223372036854775807
double 0.0d 64 bits ±4.9E-324 to ±1.7976931348623157E+308
boolear FALSE 1 bit NA
char 'u0000' 16 bits \u000o to \uFFFF
02.原生数据类型(基础数据类型)(内置数据类型)
a.数字
整数范围,默认int 精度范围永远小于 小数范围,默认double
字节 byte = 8bits = 1字节 -2^7~2^7-1 单精度 float = 32bits = 4字节 -2^149~2^128-1
短整型 short = 16bits = 2字节 -2^15~2^15-1 双精度 double = 64bits = 8字节 -2^1074~2^1024-1
整形 int = 32bits = 4字节 -2^31~2^31-1
长整型 long = 64bits = 8字节 -2^63~2^63-1
b.非数字
字符型 char = 16bit = 2字节 \u0000~\uFFFF
布尔型 boolean = 1bit/未定 true、false
---------------------------------------------------------------------------------------------------------
a.数字
整数范围,默认int 精度范围永远小于 小数范围,默认double
字节 byte = 8bits = 1字节 默认0 单精度 float = 32bits = 4字节 默认0.0f
短整型 short = 16bits = 2字节 默认0 双精度 double = 64bits = 8字节 默认0.0d
整形 int = 32bits = 4字节 默认0
长整型 long = 64bits = 8字节 默认0L
b.非数字
字符型 char = 16bit = 2字节 默认'\u0000' 单引号,单字符(1个汉字,2字节) -2^15 ~ 2^15-1
布尔型 boolean = 1bit/未定 默认False(0 false,1 true)
---------------------------------------------------------------------------------------------------------
a.数字
整数范围,默认int 精度范围永远小于 小数范围,默认double
字节 byte = 8bits = 1字节 Byte -128~127 单精度 float = 32bits = 4字节 Float
短整型 short = 16bits = 2字节 Short -128~127 双精度 double = 64bits = 8字节 Double
整形 int = 32bits = 4字节 Integer -128~127
长整型 long = 64bits = 8字节 Long -128~127
b.非数字
字符型 char = 16bit = 2字节 Character
布尔型 boolean = 1bit/未定 Boolean (0 false,1 true)
4.9 [2]对象类型
01.引用数据类型(对象类型)
类/对象,String = 英文字符占1个、中文字符占2个(GBK)/3个(UTF-8)字节 默认null 双引号,任意字符
String不属于基本数据类型,只是代表一个类,属于引用类型,【对象默认null,所以String默认值也是null】
String也可不用new的形式来创建对象呢?【Java字符串常量池,不需要用new关键字,也会在常量池中创建对象】
接口(interface)
枚举(enum)
注解(annotation)
数组(Array)
4.10 [2]基础类型、对象类型
01.比较方向
a.方向1:概念
a.回答1
原生数据类型:【内置】
引用数据类型:【通过new关键字,将创建对象保存在堆中,然后在栈中创建引用,并指向该对象在堆中的地址】
b.回答2
原生数据类型:【变量名指向“具体数值”】
引用数据类型:【变量名指向存储数据对象的“内存地址”,即“堆中地址”】
b.方向2:内存
原生数据类型:【声明后,JVM会立即分配内存空间给它】
引用数据类型:【声明后,JVM不会分配内存空间给它,只是以“引用方式”指向"具体数值"】
c.方向3:默认值
原生数据类型:【默认:对应数据类型的默认值】
引用数据类型:【只声明,不NEW实例化:对象默认“NULL”,且为全局变量】
【对象默认“非NULL”,且“对象内部的属性值,都是对应数据类型的默认值”】
02.判断问题
a.说明1
==:关系运算符,对象内存地址,【原生数据类型(地址中储存数值),引用数据类型(地址中存储指向堆中的地址)】
equals:默认Object的equals()方法,若父类及子类重写了Object的equals,则判断根据重写规则
b.说明2
原生数据类型:【直接存于栈中的,"没有实例,但有常量池"】
引用数据类型:【"引用"存放于栈中】【"实例"存放于堆中,同时有常量池】
c.说明3
原生数据类型:【基本数据类型 == 比较数值】【基本数据类型 equals 比较数值,调用Object的equals】
引用数据类型:【引用数据类型 == 比较地址】【引用数据类型 equals 先地址,再数值,调用String的equals】
d.比较规则:
一个未继承父类、未实现接口的类,重写equals()保证【先比较地址,再比较类型,最后比较内容】,再重写hashCode
-----------------------------------------------------------------------------------------------------
public class Student {
public String name;
public int age;
@Override
public boolean equals(Object o) { // IDEA 自动生成的equals方法
if (this == o) return true;
if (!(o instanceof Student)) return false; // bc.equals(c) 需比较 c instanceof BigCar
Student student = (Student) o;
return Objects.equals(this.name, student.getName) && this.age == student.age;
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge() // IDEA 自动生成的hashCode方法
}
}
4.11 [3]Array
01.定义
数据类型 长度 内存空间
变量 某个数据类型 一个空间
数组 基本类型,或引用类型(类型兼容) 固定长度 连续空间
容器 包装类,或引用类型(类型兼容) 可变长度 ArrayList“动态数组”实现,连续空间
02.JAVA数组本质是对象
a.图示
例:String[] array = new String[]{"abc", "def", "ghi"}
栈:数组对象(看成一个指针,指向堆中地址)
堆:数组元素
栈 堆 常量池
【0】 -> "abc"
String[] array = new String[]{"abc", "def", "ghi"} 【1】 -> "def"
【2】 -> "ghi"
b.定义
数组,就是相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,
然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标。
-----------------------------------------------------------------------------------------------------
长度:【数组长度一旦定义,则无法再改变】
要素:数组名、下标、类型、数组元素
特点:数组类型与数组元素的类型一致(或兼容)的集合,并使用【变量进行命名】,【下标进行区分】,效率高于容器
一旦数组完成初始化,数组在内存中所占的空间将被固定下来,所以数组的长度将不可改变
不要静态初始化和动态初始化同时使用,千万不要数组初始化时,既指定数组长度,也为每个数组元素分配初始值
03.一维数组、二维数组;List、Map顺序可变,数组顺序不变
a.创建
int[] arr // 可选:int、float、boolean、char、String
int[] arr = new int[3] // 声明并分配一个长度为5的int类型数组
int[] arr = new int[]{0,1,2}
String[] arr
String[] arr = new String[3]
String[] arr = new String[]{"zs","ls","ww"}
int[][] s
int[][] s = new int[2][]
int[][] s = {{011, 012, 013}, {121, 122, 123}, {331, 332, 333}};
String[][] s
String[][] s = new String[2][]
String[][] s = {{"011", "012", "013"}, {"121", "122", "123"}, {"331", "332", "333"}};
s[0] = new String[2];
s[1] = new String[3];
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");
System.out.println(s.length); // 二维数组的【整体长度】:2
System.out.println(s[0].length); // 二维数组的【第1个,一维数组长度】:2
System.out.println(s[1].length); // 二维数组的【第2个,一维数组长度】:3
List<int[]> list = new ArrayList<int[]>();
list.add(new int[]{1, 2});
list.add(new int[]{3, 4});
int[][] s = new int[3][3];
for (int i = 0; i < s.length; i++) {
for (int j = 0; j < s[i].length; j++) {
s[i][j] = i * j;
}
}
b.向数组中添加元素
// 原始数组
int[] items = {1, 2, 3, 4};
// 添加元素(在数组末尾)
int newItem = 5;
int[] newArray = new int[items.length + 1];
for (int i = 0; i < items.length; i++) {
newArray[i] = items[i];
}
newArray[items.length] = newItem;
// 输出新数组
System.out.println("添加元素后的数组:");
for (int i : newArray) {
System.out.print(i + " ");
}
c.向数组中移除元素
// 原始数组
int[] items = {1, 2, 3, 4};
// 要移除的元素的索引
int indexToRemove = 2;
// 创建一个新数组,排除要移除的元素
int[] newArray = new int[items.length - 1];
for (int i = 0, j = 0; i < items.length; i++) {
if (i != indexToRemove) {
newArray[j++] = items[i];
}
}
// 输出新数组
System.out.println("移除元素后的数组:");
for (int i : newArray) {
System.out.print(i + " ");
}
d.遍历二维数组
String[][] name = {
{"11", "12", "13"},
{"21", "22", "23"},
{"31", "32", "33"},
};
// 遍历每个一维数组
for (String[] arr : name) {
// 打印一维数组的内容
for (String element : arr) {
System.out.print(element + " ");
}
// 换行
System.out.println();
}
System.out.println(name[0][0]);
System.out.println(name[0][1]);
System.out.println(name[0][2]);
4.12 [3]包装类
01.常用信息1
a.介绍
Everything is an object,万物皆对象,都有自己的属性和方法;
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程;
基本类型(无属性、无方法,存储相对简单、运算效率较高)-> 引用类型(包装类,有属性、有方法,满足一切皆对象)
b.包装类
a.数字
整数范围,默认int 精度范围永远小于 小数范围,默认double
字节 byte = 8bits = 1字节 Byte -128~127 单精度 float = 32bits = 4字节 Float
短整型 short = 16bits = 2字节 Short -128~127 双精度 double = 64bits = 8字节 Double
整形 int = 32bits = 4字节 Integer -128~127
长整型 long = 64bits = 8字节 Long -128~127
b.非数字
字符型 char = 16bit = 2字节 Character
布尔型 boolean = 1bit/未定 Boolean (0 false,1 true)
c.操作
a.装箱:基础类型 -> 包装类
int a = 10; String a = "123";
Integer b = Integer.valueOf(a); Integer b = Integer.valueOf(a);
b.拆箱:包装类 -> 基础类型
Integer a = 10; Integer a = 10;
int b = a.intValue(); long b = a.longValue();
c.自动装箱、自动拆箱
int a = 10;
Integer b = new Integer(20);
-------------------------------------------------------------------------------------------------
// Integer -> int,反编译可知,【自动拆箱】底层调用【Integer类 实现Number抽象类的 intValue() 方法】
a = b;
-------------------------------------------------------------------------------------------------
// int -> Integer,反编译可知,【自动装箱】底层调用【Integer类 的 静态 valueOf(int i) 方法】
b = a;
02.常用信息2
a.基本类型、包装类型
a.默认值不同
基本类型:默认值0、false
包装类型:默认null
b.初始化的方式不同
基本类型:直接使用
包装类型:采用new的方式创建
c.存储方式有所差异
基本类型:栈
包装类型:堆(成员变量的话,在不考虑T优化的栈上分配时,都是随着对象一起保存在堆上的)
b.自动装箱、自动拆箱
a.介绍
自动装箱:Java编译器自动将【基本数据类型】转换为对应的【包装类】
自动拆箱:Java编译器自动将【包装类】转换为对应的【基本数据类型】
b.底层
【自动拆箱】底层调用【Integer类 实现Number抽象类的 intValue() 方法】
【自动装箱】底层调用【Integer类 的 静态 valueOf(int i) 方法】
4.13 [4]转换:低到高
00.总结
a.等级低到高
byte、short、int、long、float、double
char、int、long、float、double
自动转换:运算过程中,低级可以自动向高级转换
强制转换:高级需要强制转换为低级,可能会丢失精度
b.规则
= 右边先自动转换成表达式中最高级的数据类型,再进行运算。整型经过运算会自动转化最低 int 级别,如两个 char 类型的相加,得到的是一个 int 类型的数值。
= 左边数据类型级别 大于 右边数据类型级别,右边会自动升级
= 左边数据类型级别 小于 右边数据类型级别,需要强制转换右边数据类型
char 与 short,char 与 byte 之间需要强转,因为 char 是无符号类型
4.14 [4]转换:基础类型
01.基础类型的转换
a.自动类型转换
a.范围小 -> 范围大
byte,short,char —> int —> long —> float —> double
【byte,short,char同一等级,彼此不会进行转换】
【由于boolean只能存放true或false,不兼容整数或字符,因此不参与“自动类型转换”】
b.转换时的混合运算
运算中,【不同类型的数据先转化为同一类型】,然后进行运算,如,int + long + float,运算时,类型依次转换
整数类型与小数类型做运算,【类型会变为小数类型】;
-------------------------------------------------------------------------------------------------
范围小的类型【自动转换/自动赋值】给范围大的类型;
范围小的类型【遇到】范围大的类型,【自动转换】为范围大的类型;
范围最大的是【字符串String】:任何类型遇到String,都会自动变为字符串
c.举例
int a = 'A'+ 1234; 正确,char遇到int,会自动转换为int
int b = 10 + 3.14; 报错,Required type: int Provided: double
d.误区
int c = (x + y + z)/3 输出:98 int + int + int/3 = int 实际98.333 -> 98
double c= (x + y + z)/3 输出:98.0 int + int + int/3 = int -> double 实际98.333 -> 98 -> 98.0
b.强制类型转换:必须强转,精度损失
a.范围大 -> 范围小
范围小 = (小类型)范围大;
b.举例
float a = 1234.5f; 正确
int a = 1234.5d; 报错,Required type: int Provided: double
float a = 1234.5d; 报错,Required type: float Provided: double
c.(隐含)强制类型转换
a.float/double,没有内置转换器
float f = 123.4; 小数,默认类型double,报错,Required type: float Provided: double
推荐:【float f = 1234.f;】【float f = (float)123.4;】
b.int,内置转换器:=、+=
short a = 1000; 整数,默认类型int,【但内置转换器】,short范围小 = int范围大,正确
byte b = 10; 整数,默认类型int,【但内置转换器】,byte范围小 = int范围大,正确
long c = 123; 整数,默认类型int,【自动类型转换】,long范围大 = int范围小,正确
c.byte,short特殊情况,整数中只有=,+=内置转换器
byte b1 = 100;
byte b2 = 100;
b2 = b1 + b2; 报错,Required type: byte Provided: int
-------------------------------------------------------------------------------------------------
byte b1 = 100;
byte b2 = 100;
b2 += b1; 正确
-------------------------------------------------------------------------------------------------
结论:byte + byte = int,short + short = int,(byte范围:-128 ~ 127)
d.左右两侧的类型必须一致,但是【数据值只需要兼容,类型本身或范围比其小的类型】
long[] longs = new long[]{(int) 33, 2, 1, 2, 455}; 正确
long[] longs = new long[]{(float) 33, 2, 1, 2, 455}; 报错,Required type: long Provided: float
4.15 [4]转换:String类
01.String类的转换
a.其他类型 -> 字符串
【自动转换】:xxx + ""
【调用Object】:xxx.toString()
【调用String】:String.valueOf(XXX xxx)
b.字符串 -> 其他类型
【调用继承/重写的Object.toString()方法】:xxx.toString()
【将"字符串 -> 包装类",再转换为其他类型】:new Float("1.23").doubleValue()
【调用Byte、Short、Integer、Long类,Float、Double类的parseXxx(String s)】:Integer.parseInt("123");
5 字符串
5.1 [1]语法
01.流程:默认【整数:0,小数:0.0,字符:'\u0000',布尔:flase】【引用:null / 对象内部默认属性值 / ""】
a.JVM内存空间
Code 代码区:类的定义,静态资源 Student.class
Heap 堆空间:分配对象(分配实例) new Student()
Stack 栈空间:临时变量 Student stu
b.步骤
JVM 加载 Student.class 到 Code 代码区
new Student() 会在堆空间分配空间,并创建一个 Student 实例
将 Student 堆中地址 赋值给 栈空间的stu变量
02.对于非基本类型,即"对象类型/引用类型",默认值有如下
a.只声明,不NEW实例化:对象默认“NULL”,且为全局变量
Person per; // per默认null
String str; // str默认null
b.有声明,并NEW实例化:对象默认“非NULL”,且“对象内部的属性值,都是【对应数据类型的默认值】”
Person per = new Person(); // per默认:name为null,age为0
String str = new String(); // str默认"",创建空字符串
String str = new String(""); // str会创建新的空字符串,是空串"",不是null
5.2 [1]常见API
01.String常用方法
equals:字符串是否相同
equalsIgnoreCase:忽略大小写后字符串是否相同
compareTo:根据字符串中每个字符的Unicode编码进行比较
compareToIgnoreCase:根据字符串中每个字符的Unicode编码进行忽略大小写比较
indexOf:目标字符或字符串在源字符串中位置下标
lastIndexOf:目标字符或字符串在源字符串中最后一次出现的位置下标
valueOf:其他类型转字符串
charAt:获取指定下标位置的字符
codePointAt:指定下标的字符的Unicode编码
concat:追加字符串到当前字符串
isEmpty:字符串长度是否为0
contains:是否包含目标字符串
startsWith:是否以目标字符串开头
endsWith:是否以目标字符串结束
format:格式化字符串
getBytes:获取字符串的字节数组
getChars:获取字符串的指定长度字符数组
toCharArray:获取字符串的字符数组
join:以某字符串,连接某字符串数组
length:字符串字符数
matches:字符串是否匹配正则表达式
replace:字符串替换
replaceAll:带正则字符串替换
replaceFirst:替换第一个出现的目标字符串
split:以某正则表达式分割字符串
substring:截取字符串
toLowerCase:字符串转小写
toUpperCase:字符串转大写
trim:去字符串首尾空格
02.StringBuffer、StringBuilder通用方法
构造方法:StringBuffer()、StringBuffer(String str)、StringBuilder()、StringBuilder(String str)
追加内容:append(String str)
插入内容:insert(int offset, String str)
删除内容:delete(int start, int end)、deleteCharAt(int index)
替换内容:replace(int start, int end, String str)
反转内容:reverse()
设置长度:setLength(int newLength)
获取子字符串:substring(int start)、substring(int start, int end)
获取长度:length()
获取容量:capacity()
设置字符:setCharAt(int index, char ch)
5.3 [1]创建对象
00.总结
使用String字符串时,
如果直接使用"hello",即String str1 = "hello",则从常量池中寻找;
如果使用NEW关键字,即String str3 = new String("hello"),则从堆中寻找;
(而堆中又会从常量池中寻找,如果常量池中不存在,则在常量池创建,并引用常量池中的字符串)
(引用:使用常量池中的内存地址)
建议:将频繁使用的常量放入常量池,而非【重复使用NEW关键字开辟内存空间】,以提高空间使用率
01.方式一:通过字符串常量的创建
a.提问
String str = “abc”;,在内存中创建了几个对象?
b.回答
JVM会检查“该对象是否在字符串常量池”,
若存在,则直接返回该对象引用;
若不存在,则新的字符串将在常量池中被创建,再返回该对象引用
02.方式二:通过NEW形式的创建
a.提问
String str = new String("abc");,在内存中创建了几个对象?
b.回答
【在内存中创建两个对象,一次关于堆内存,一次关于常量池,堆内存对象是常量池对象的一个拷贝副本】
String(String original):源码解读,新创建的字符串是参数字符串的副本
c.过程
首先,在【堆内存中,创建一个匿名对象"abc",并且"abc"进行入池操作】,
然后,使用 NEW 关键字,在【堆内存中开辟一块新空间,将"abc"存进去,然后把 地址 返给栈内存中的 st1】,
最后,由于匿名对象"abc"没有被栈中变量使用,变为垃圾对象而被GC回收
-----------------------------------------------------------------------------------------------------
首先,在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,"abc" 将会在常量池中创建;
然后,使用 NEW 关键字,JVM调用String的构造函数在堆内存创建对象,同时引用常量池中的"abc"字符串,
最后,str将最终引用String对象
5.4 [1]不可变
01.String不可变
a.定义
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
...
}
public final class StringBuffer
extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
char[] value; -> AbstractStringBuilder
...
}
public final class StringBuilder
extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
char[] value; -> AbstractStringBuilder
...
}
b.用法
final修饰String类,表示【不可继承类】,是指【字符串对象本身不能改变,不等同于“对象的引用不能改变”】
final修改char[]数组,表示【数组存储于char[]数组】,表示【String对象不可被更改】
02.为什么Java要这样设计?
①安全性:String不可变性保证“参数”不可变,如果网络连接中String参数可变,那么连接的地址可能被篡改
②hash值:String用做HashMap的key,只需对hash进行一次计算
②字符串常量池:如果一个String对象已经被创建,直接从字符串常量池中引用。只有String是不可变的,才可能实现
④线程安全:不可变性,天生具备线程安全
5.5 [1]不可以继承
01.String类是否可以继承?不可以
不可以
String 类在 JDK 中被广泛使用,为了保证正确性、安全性,String 类是用 final 修饰,不能被继承,方法不可以被重写。
5.6 [2]池化
01.String池化
a.定义
字符串池化是指在Java中,字符串常量会被存储在一个共享的池中,以便重用相同的字符串对象。
这种机制可以减少内存开销,因为相同的字符串不会被多次创建。
b.示例
String s1 = new String(""); // 第一种不会入池
String s2 = ""; // 第二种看情况而定(等号右边如果是常量则入池,非常量则不入池)
String s3 = "a" + "b"; // "a"是常量,"b"是常量,常量+常量=常量,所以会入池
String s4 = s1 + "b"; // s1是变量,"b"是常量,变量+常量!=常量,所以不会入池
c.分析
a.String s1 = new String("");:
使用new关键字创建字符串对象,不会将字符串放入池中
每次调用new String("")都会创建一个新的字符串对象
b.String s2 = "";:
如果等号右边是字符串常量,则字符串会被放入池中
如果等号右边是非常量,则不会入池
c.String s3 = "a" + "b";
"a"和"b"都是常量,常量相加的结果也是常量,所以会入池
d.String s4 = s1 + "b";
s1是变量,"b"是常量,变量和常量相加的结果不是常量,所以不会入池
5.7 [2]常量池
01.String常量池
a.定义
字符串常量池位于【堆】内存中,专门用来【存储字符串常量】
它可以提高内存的使用率,避免开辟多块空间存储相同的字符串
b.流程
在创建字符串时,JVM会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用
如果不存在,则实例化一个字符串放到池中,并返回其引用
02.字符型常量、字符串常量
a.形式上
字符常量是单引号引起的一个字符,例如:'a'
字符串常量是双引号引起的若干个字符,例如:"abc"
b.含义上
字符常量相当于一个整形值(ASCII值),可以参加表达式运算
字符串常量代表一个地址值(该字符串在内存中存放位置)
c.占内存大小
字符常量占两个字节
字符串常量占若干个字节(至少一个字符结束标志)
03.常用结论
a.示例1
a.代码
int num = 10;
num = 20;
b.图示
栈(基本类型+引用类型) 堆(对象的示例,理解为“new Person()”) 方法区(包含常量池)
[ ] [ ] [ ]
[ ] [ ] [ 10(×,销毁) ]
[ num ] [ ] [ 20(√,改变指向方向) ]
[ ] [ ] [ ]
[ ] [ ] [ ]
b.示例2
a.代码
String str = 'a';
str = 'b';
b.图示
栈(基本类型+引用类型) 堆(对象的示例,理解为“new Person()”) 方法区(包含常量池)
[ ] [ ] [ ]
[ ] [ ] [ a(×,不销毁) ]
[ str ] [ ] [ b(√,改变指向方向) ]
[ ] [ ] [ ]
[ ] [ ] [ ]
c.示例3
a.代码
String str = 'a';
str = 'b';
str += 'c';
b.图示
栈(基本类型+引用类型) 堆(对象的示例,理解为“new Person()”) 方法区(包含常量池)
[ ] [ ] [ ]
[ ] [ ] [ a(×,不销毁) ]
[ str ] [ ] [ bc(×,不销毁) ]
[ ] [ ] [ bc(√,改变指向方向) ]
[ ] [ ] [ ]
d.结论
字符串是常量,它们的值一旦被创建就无法改变;【常量池中,num会销毁,而str却不会被销毁】
如果需要对String字符串进行频繁修改,不建议使用String类,推荐使用【StringBuffer类】
04.常用结论
a.示例1
a.代码
String str1 = "hello";
String str2 = "hello";
System.out.println("str1==str2结果为:" + (str1==str2)); // 结果为true
b.图示:先去“常量池中查找”,若存在,则直接使用;若不存在,则创建后放入常量池,再使用;
栈(基本类型+引用类型) 堆(对象的示例,理解为“new Person()”) 方法区(包含常量池)
[ ] [ ] [ ]
[ ] [ ] [ ]
[ str1 ] [ ] [hello(先去“常量池中查找” ]
[ str2 ] [ ] [ 若存在,则直接使用) ]
[ ] [ ] [ ]
b.示例2
a.代码
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
String str4 = new String("hello");
String str5 = new String("hello");
System.out.println("str1==str2结果为:" + (str1==str2)); // 结果为true
System.out.println("str3==str4结果为:" + (str3==str4)); // 结果为false
System.out.println("str1==str4结果为:" + (str1==str4)); // 结果为false
b.图示:new String("hello"):凡是有NEW关键字,就会在"堆中开辟新的内存空间"
栈(基本类型+引用类型) 堆(对象的示例,理解为“new Person()”) 方法区(包含常量池)
[ ] [ ] [ ]
[ str1 ] [ ] [ ]
[ str2 ] [ ] [ ]
[ str3 ] [ new String("hello")] [ ]
[ str4 ] [ new String("hello")] [hello(若存在,则直接使用)]
[ str5 ] [ new String("hello")] [ ]
[ ] [ ] [ ]
c.结论
使用String字符串时,
如果直接使用"hello",即String str1 = "hello",则从常量池中寻找;
如果使用NEW关键字,即String str3 = new String("hello"),则从堆中寻找;
(而堆中又会从常量池中寻找,如果常量池中不存在,则在常量池创建,并引用常量池中的字符串)
(引用:使用常量池中的内存地址)
建议:将频繁使用的常量放入常量池,而非【重复使用NEW关键字开辟内存空间】,以提高空间使用率
05.常用结论
a.示例1
a.代码
String str1 = "hello";
String str2 = "hello";
String str3 = "he" + "llo";
System.out.println("str1==str2结果为:" + (str1==str2)); // 结果为true
System.out.println("str1==str3结果为:" + (str1==str3)); // 结果为true
b.解释
首先,我们要知道Java会确保一个字符串常量只有一个拷贝。
因为str1和str2中的”hello”都是字符串常量,它们在编译期就被确定了,所以str1==str2为true;
而“he”、“llo”也都是字符串常量,【当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量】
因此在编译期“he”、“llo”就被解析为一个字符串常量,所以,【str3也是常量池中"hello"中的一个引用】
b.示例2
a.代码
String str1 = "hello";
String str2 = new String("hello");
String str3 = "he" + new String("llo");
System.out.println("str1==str2结果为:" + (str1==str2)); // 结果为false
System.out.println("str1==str3结果为:" + (str1==str3)); // 结果为false
System.out.println("str2==str3结果为:" + (str2==str3)); // 结果为false
b.解释
str1:str1仍使用“常量池中hello的引用”
str2:【new String()创建字符串不是常量,无法在编译期确定;创建字符串不放入常量池中,有独立地址空间】
str3:后半部分new String()创建字符串不是常量,无法在编译期确定;因此,也会创建新的"hello"引用
5.8 [2]intern()方法
00.汇总
a.用途
扩充常量池一个方法
b.工作原理
a.检查字符串常量池
当调用intern()方法时,JVM会检查字符串常量池中是否已经包含了一个与当前字符串内容相同的字符串
b.返回引用
如果字符串常量池中已经存在一个与当前字符串内容相同的字符串,则返回该字符串的引用
c.添加到池中
如果字符串常量池中不存在与当前字符串内容相同的字符串,
则将当前字符串添加到池中,并返回该字符串的引用
c.场景
a.减少内存开销
通过将重复的字符串内容存储在字符串常量池中,可以减少内存开销
b.提高性能
在需要频繁比较字符串内容的场景中,使用intern()方法可以提高性能
因为可以使用==运算符进行引用比较,而不是使用equals()方法进行内容比较
d.使用
a.代码
public class StringInternExample {
public static void main(String[] args) {
// 示例1:字符串常量
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出:true
// 示例2:使用new关键字创建字符串
String s3 = new String("hello");
System.out.println(s1 == s3); // 输出:false
// 示例3:使用intern()方法
String s4 = s3.intern();
System.out.println(s1 == s4); // 输出:true
// 示例4:不同内容的字符串
String s5 = new String("world");
String s6 = s5.intern();
String s7 = "world";
System.out.println(s6 == s7); // 输出:true
}
}
b.说明
示例1:s1和s2都是字符串常量,指向同一个字符串常量池中的对象,因此s1 == s2返回true
示例2:s3是使用new关键字创建的字符串对象,不会放入字符串常量池,因此s1 == s3返回false
示例3:调用s3.intern()方法后,s4指向字符串常量池中的对象,与s1指向同一个对象,因此s1 == s4返回true
示例4:s5是使用new关键字创建的字符串对象,调用s5.intern()方法后,s6指向字符串常量池中的对象,与s7指向同一个对象,因此s6 == s7返回true
01.介绍
a.解读1
String的intern()就是扩充常量池一个方法
存在.class文件中常量池,在运行期被JVM装载,并且可以扩充
当一个String实例str调用intern()方法时,Java查找常量池中是否有相同Unicode的字符串常量,
如果有,则返回其的引用,如果没有,则在常量池中增加一个Unicode等于str的字符串并返回它的引用
b.解读2
public native String intern(); // String类被native修饰
在JDK7后,Oracle接管了JAVA源码后不对外开发,因此,在OpenJDK7源码中,查看String类的intern()方法
如果常量池中存在当前字符串, 直接返回当前字符串;若没有此字符串, 会将此字符串放入常量池后, 再返回;
对于任意两个字符串s和t,当且仅当 s.equals(t) 为true时,s.intern()==t.intern() 才为true
注意一点,【JDK7版本以后,常量池引入到了Heap中,所以可以直接存储引用】
c.解读3
字符串如果通过NEW创建,则必然会直接指向堆中的对象;
如果NEW创建对象后,想把【引用从“堆 -> 常量池”】,则需要调用【String类中的inern()方法】
d.解读4
JDK7前,常量池是存放在方法区中
JDK7,字符串常量池移到了堆中,运行时常量池还在方法区中
JDK8,删除了永久代,方法区这个概念还是保留的,但是方法区的实现变成了元空间,常量池沿用JDK7放在了堆中
JDK8,常量池和静态变量存储到了堆中,类的元数据及运行时常量池存储到元空间中
e.解读5
Java基本类型的包装类的大部分都实现了常量池技术,即Byte、Short、Integer、Long、Character;
这5种包装类默认创建了数值[-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象;
两种浮点数类型的包装类Float、Double,以及Boolean类,并没有实现常量池技术
f.区分
字面量:如 “Java”、“Hello World”、100、3.14 等
常量:相对变量而言,在程序运行其间,不会被程序修改的量
常量池:运行时常量池、字符串常量池、常量池表的统称,需要根据上下文理解“常量池”代表的具体含义
常量池表:Class文件中用于存放编译期生成的各种字面量和符号引用,其中String 常量会被放入字符串常量池
字符串常量池:专门用来存放字符串常量的一个池子,属于“运行时常量池”的一部分
运行时常量池:方法区的一部分
02.示例1
a.代码
String str1 = "hello";
String str2 = new String("hello");
String str3 = new String("hello");
System.out.println(str1 == str2); // 结果为false
str2.intern();
str3 = str3.intern();
System.out.println(str1 == str2); // 结果为false
System.out.println(str1 == str2.intern()); // 结果为true
System.out.println(str1 == str3); // 结果为true
b.解释
str1==str2结果为:false // str2执行intern(),却没有赋给str2
str1==str2.intern()结果为:true // str2.intern()确实返回的是常量池中"hello"引用
03.示例2
a.代码
String str1 = new String("hello");
String str2 = str1.intern();
System.out.println(str1 == str1.intern()); // 结果为false
System.out.println(str2 == str1.intern()); // 结果为true
b.解释
常量池中一开始没有”hello”,当调用str1.intern()后,在常量池中新添加一个“hello”常量;
原来的不在常量池中的”kvill”仍然存在,也就不是“如果表中没有相同值的字符串,则将自己地址注册到表中”
04.示例3
a.常量池中存在字符串
// 在堆中创建对象,在字符串常量池中添加hello,【引用情况:a(hello) -> 堆地址 -> 常量池地址】
String a = new String("hello");
// 调用intern,此时hello已存入常量池中,【引用情况:b(hello) -> 常量池地址】
String b = a.intern();
// 【引用情况:a(hello) -> 堆地址 -> 常量池地址】,【引用情况:b(hello) -> 常量池地址】
System.out.println(a == b); // 结果为false
// 因为常量池中已经包含hello,【引用情况:c(hello) -> 常量池地址】
String c = "hello";
// 【引用情况:b -> 常量池地址】,【引用情况:c(hello) -> 常量池地址】
System.out.println(b == c); // 结果为true
b.常量池中不存在字符串
// 在堆中创建对象,在字符串常量池中添加hello、world,但无法添加helloworld,
// 【引用情况:a(helloworld) -> 堆地址】
String a = new String("hello") + "world";
// 调用intern,此时常量池无"helloworld",则在常量池创建,【引用情况:b(helloworld) -> 常量池地址】
String b = a.intern();
// 【引用情况:a(helloworld) -> 堆地址】,【引用情况:b(helloworld) -> 常量池地址】
// 【JDK7版本以后,常量池引入到Heap中,直接存储引用,故b(helloworld) -> Heap的常量池地址】
System.out.println(a == b); // 结果为true
// 字符串常量池中已存在"helloworld",【引用情况:c(helloworld) -> 常量池地址】,同a、b引用地址
String c = "helloworld";
System.out.println(a == c); // 结果为true
System.out.println(b == c); // 结果为true
// 在堆中创建对象,在字符串常量池中添加he、llo,但无法添加hello,【引用情况:d(hello) -> 堆地址】
String d = new String("he") + "llo";
// 由于变量a已经在常量池中添加"hello",【引用情况:e(hello) -> 常量池地址】
String e = d.intern();
System.out.println(d == e); // 结果为false
5.9 [3]转换
01.String类的转换
a.其他类型 -> 字符串
【自动转换】:xxx + ""
【调用Object】:xxx.toString()
【调用String】:String.valueOf(XXX xxx)
b.字符串 -> 其他类型
【调用继承/重写的Object.toString()方法】:xxx.toString()
【将"字符串 -> 包装类",再转换为其他类型】:new Float("1.23").doubleValue()
【调用Byte、Short、Integer、Long类,Float、Double类的parseXxx(String s)】:Integer.parseInt("123");
5.10 [3]反转
01.字符串反转
使用 StringBuilder 或 StringBuffer 的 reverse 方法,本质都调用了它们的父类 AbstractStringBuilder 的 reverse 方法实现(JDK1.8)
不考虑字符串中的字符是否是 Unicode 编码,自己实现
递归
5.11 [3]拼接
01.字符串拼接
+ 运算符:如果拼接的都是字符串直接量,则适合使用 + 运算符实现拼接;
StringBuilder:如果拼接的字符串中包含变量,并不要求线程安全,则适合使用StringBuilder;
StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,则适合使用StringBuffer;
String类的concat方法:如果只是对两个字符串进行拼接,并且包含变量,则适合使用concat方法;
StringJoiner类(Java 8+):不仅可以指定拼接时的分隔符,还可以指定拼接时的前缀和后缀,这里使用它的add()方法来拼接字符串
5.12 [3]拼接”+“
01.String在使用时,字符串拼接"+"是怎么回事?
a.回答
“+”的拼接:StringBuilder类(反编译)的非静态 append() 方法
“+”的转换:String类 重写 Object类的 toString 方法
b.回答
Java中仅有的两个重载过的运算符:String类中的"+"、"+="
Java语言为字符串连接运算符(+)提供特殊支持,并将其他对象转换为字符串
5.13 [3]三者对比
01.StringBuffer、StringBuilder:均继承AbstractStringBuilder抽象类,【线程是否安全】是最大区别,其他几乎一模一样
a.字符串拼接
+ 循环内,减少“+”, 推荐StringBuild、StringBuffer的append
线程安全 StringBuffer public synchronized StringBuffer insert()
线程不安全 StringBuilder public StringBuilder insert()
b.字符数组:char value[]与char[] value等价
String 不可变的字符数组 private final char value[]; 不可变,拼接等会产生新对象
StringBuffer 可变的字符数组 char[] value; -> AbstractStringBuilder JDK9以后底层用byte[]
StringBuilder 可变的字符数组 char[] value; -> AbstractStringBuilder JDK9以后底层用byte[]
c.线程
String 线程安全 不可变字符串(private final char value[];)
StringBuffer 线程安全 内部使用 synchronized 进行同步
StringBuilder 线程不安全 去掉了线程安全,有效减少了内存开销,字符串拼接的首选
d.使用
String 线程安全 字符串的修改不频繁
StringBuffer 线程安全 字符串的修改频繁,且“字符串是全局变量,或需要多线程支持”
StringBuilder 线程不安全 字符串的修改频繁,且“字符串是局部变量,或需要单线程支持”
5.14 [4]replace、replaceAll
01.replace方法:支持字符和字符串的替换
public String replace(char oldChar, char newChar)
public String replace(CharSequence target, CharSequence replacement)
02.replaceAll方法:基于正则表达式的字符串替换
public String replaceAll(String regex, String replacement)
5.15 [4]StringBuilder的append()、insert()
00.汇总
append 更高效:因为它只涉及简单的追加操作,时间复杂度为 O(1)
insert 较低效:因为它需要移动插入点之后的所有字符,时间复杂度为 O(n)
选择合适的方法:如果可以接受最终结果的顺序颠倒,建议优先使用 append,然后通过 reverse 方法反转结果
否则,在必须使用 insert 的情况下,尽量减少插入次数或避免在开头插入
01.StringBuilder方法效率分析
a.sb.append(content) 的工作原理
a.功能描述
append 方法将内容追加到 StringBuilder 的末尾。它通过将新内容附加到现有字符数组后面并更新长度计数器来实现。
b.示例代码
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // 在末尾追加 " World"
System.out.println(sb.toString()); // 输出:Hello World
c.效率分析
a.时间复杂度
O(1)(常数时间),因为只是在末尾追加内容。
b.扩容机制
如果字符数组容量不足,StringBuilder 会自动扩容。扩容的时间复杂度为 O(n),但这是一个较少发生的操作,对整体性能影响较小。
b.sb.insert(0, content) 的工作原理
a.功能描述
insert 方法将内容插入到指定的索引位置。需要将插入点之后的所有字符向后移动一个位置,以腾出空间给新内容。
b.示例代码
StringBuilder sb = new StringBuilder("World");
sb.insert(0, "Hello "); // 在开头插入 "Hello "
System.out.println(sb.toString()); // 输出:Hello World
c.效率分析
a.时间复杂度
O(n),其中 n 是 StringBuilder 当前的长度。每次插入都需要移动插入点之后的所有字符。
b.频繁插入的影响
如果频繁调用 insert,尤其是插入到开头(如 insert(0, ...)),会导致性能显著下降,因为每次插入都会触发大量数据的移动。
02.为什么 sb.append(content) 比 sb.insert(0, content) 更高效?
a.方法描述
a.sb.append(content)
将内容追加到 StringBuilder 的末尾,不涉及任何元素移动。
时间复杂度:O(1)
场景适用性:适用于顺序追加内容的场景。
b.sb.insert(0, content)
将内容插入到 StringBuilder 的开头,需要移动所有后续元素。
时间复杂度:O(n)
场景适用性:适用于需要在任意位置插入的场景。
b.具体原因
a.append 不涉及元素移动
append 只是将新内容附加到现有内容的末尾,不会影响已有的字符排列,因此操作非常快速。
b.insert 需要移动大量元素
insert 在指定位置插入内容时,需要将插入点之后的所有字符向后移动一个位置。如果插入点靠近开头(如 insert(0, ...)),几乎所有的字符都需要移动,导致性能显著下降。
c.扩容机制的影响
如果 StringBuilder 的容量不足,无论使用 append 还是 insert,都会触发扩容操作。扩容的时间复杂度为 O(n),但这是偶发事件,对 append 的整体性能影响较小。对于 insert,扩容与元素移动的操作叠加,进一步降低了性能。
03.实际性能测试
a.测试代码
public class StringBuilderPerformanceTest {
public static void main(String[] args) {
int iterations = 1_000_000;
// 测试 append 的性能
long startTimeAppend = System.nanoTime();
StringBuilder sbAppend = new StringBuilder();
for (int i = 0; i < iterations; i++) {
sbAppend.append("A");
}
long endTimeAppend = System.nanoTime();
// 测试 insert 的性能
long startTimeInsert = System.nanoTime();
StringBuilder sbInsert = new StringBuilder();
for (int i = 0; i < iterations; i++) {
sbInsert.insert(0, "A");
}
long endTimeInsert = System.nanoTime();
System.out.println("Append time: " + (endTimeAppend - startTimeAppend) / 1_000_000.0 + " ms");
System.out.println("Insert time: " + (endTimeInsert - startTimeInsert) / 1_000_000.0 + " ms");
}
}
b.示例输出(可能因环境不同而有所变化)
Append time: 5.2 ms
Insert time: 1234.5 ms
c.结论
从结果可以看出,append 的执行时间远小于 insert。
5.16 [5]new String(“abc”)会创建几个对象?2个、1个
01.如果"abc"这个字符串常量不存在,则创建2个对象
一是"abc"这个字符串常量
二是new String这个实例对象
02.如果"abc"这个字符串常量存在,则创建1个对象,即new String这个实例对象
5.17 [5]String s=“a”+“b”+“c”+“d”;创建了几个对象?1个
01.回答
1个
02.原因
Java 编译器对字符串常量直接相加的表达式进行优化,不等到运行期去进行加法运算,
在编译时就去掉了加号,直接将其编译成一个这些常量相连的结果。
所以 "a"+"b"+"c"+"d" 相当于直接定义一个 "abcd" 的字符串。
5.18 [6]为什么idea建议使用“+”拼接字符串
00.汇总
1.单纯的字符串拼接使用“+”,更快更简洁
2.循环拼接时使用“+”拼接字符串效率较低,推荐使用StringBuilder
01.单纯的字符串拼接使用“+”,更快更简洁
a.说明
普通的几个字符串拼接成一个字符串,直接使用“+”
因为教材等原因,当前依旧有许多人拼接字符串时认为使用“+”耗性能1,首选StringBuilder
b.代码
/**
* 使用+拼接字符串
*/
public String concatenationStringByPlus(String prefix, int i) {
return prefix + "-" + i;
}
/**
* 使用StringBuilder拼接字符串
*/
public String concatenationStringByStringBuilder(String prefix, int i) {
return new StringBuilder().append(prefix).append("-").append(i).toString();
}
/**
* 测试使用+拼接字符串耗时
*/
@Test
public void testStringConcatenation01ByPlus() {
long startTime = System.currentTimeMillis();
int count = 100000;
for (int i = 0; i < count; i++) {
String str = concatenationStringByPlus("testStringConcatenation01ByStringBuilder:", i);
}
long endTime = System.currentTimeMillis();
System.out.println("testStringConcatenation01ByPlus,拼接字符串" + count + "次,花费" + (endTime - startTime) + "秒");
}
/**
* 测试使用StringBuilder拼接字符串耗时
*/
@Test
public void testStringConcatenation02ByStringBuilder() {
long startTime = System.currentTimeMillis();
int count = 100000;
for (int i = 0; i < count; i++) {
String str = concatenationStringByStringBuilder("testStringConcatenation02ByStringBuilder:", i);
}
long endTime = System.currentTimeMillis();
System.out.println("testStringConcatenation02ByStringBuilder,拼接字符串" + count + "次,花费" + (endTime - startTime) + "秒");
}
-----------------------------------------------------------------------------------------------------
testStringConcatenation01ByPlus,拼接字符串100000次,花费33秒
testStringConcatenation02ByStringBuilder,拼接字符串100000次,花费36秒
02.循环拼接时使用“+”拼接字符串效率较低,推荐使用StringBuilder
a.说明
循环拼接,虽然“+”拼接字符串编译后也会变成StringBuilder
但是每次循环处理都会new一个StringBuilder对象,耗时会大大增加
而直接使用StringBuilder,new一次就可以了,效率相对高
b.代码
/**
* 循环使用+拼接字符串
*/
@Test
public void testLoopStringConcatenation03ByPlus() {
long startTime = System.currentTimeMillis();
int count = 10000;
String str = "testLoopStringConcatenation03ByPlus:";
for (int i = 0; i < count; i++) {
str = str + "-" + i;
}
System.out.println(str);
long endTime = System.currentTimeMillis();
System.out.println("testLoopStringConcatenation03ByPlus,拼接字符串" + count + "次,花费" + (endTime - startTime) + "秒");
}
/**
* 测试循环使用StringBuilder拼接字符串耗时
*/
@Test
public void testLoopStringConcatenation04ByStringBuilder() {
long startTime = System.currentTimeMillis();
int count = 100000;
StringBuilder stringBuilder = new StringBuilder("testLoopStringConcatenation04ByStringBuilder:");
for (int i = 0; i < count; i++) {
stringBuilder.append("-");
stringBuilder.append(i);
}
String str = stringBuilder.toString();
System.out.println(str);
long endTime = System.currentTimeMillis();
System.out.println("testLoopStringConcatenation04ByStringBuilder,拼接字符串" + count + "次,花费" + (endTime - startTime) + "秒");
}
5.19 [6]使用+=赋值后,原始的String对象中的内容会改变吗?
01.回答
字符串【内容不会改变】,但【对象的引用可以改变】
02.原因
public class StringDemo {
public static void main(String[] args) {
String s = "Hello";
s += "World";
System.out.println("s:" + s); // 结果为s:HelloWorld
}
}
final 修饰的类【不能被继承】,是指【字符串对象本身不能改变】,不等同于【对象的引用不能改变】
上述过程中,字符串本身的内容是没有任何变化的,而是分别创建了三块内存空间:(Hello) (World) (HelloWorld)
Hello + World 拼接为 HelloWorld 时,s 无法指向原来的 Hello 对象,而改变指向为 HelloWorld
5.20 [7]String能存储多少个字符?65535
00.面试官问我String能存储多少个字符?
a.概述
首先String的length方法返回是int。所以理论上长度一定不会超过int的最大值。
b.编译器源码限制
编译器源码如下,限制了字符串长度大于等于65535就会编译不通过
-----------------------------------------------------------------------------------------------------
private void checkStringConstant(DiagnosticPosition var1, Object var2) {
if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
this.log.error(var1, "limit.string", new Object[0]);
++this.nerrs;
}
}
c.UTF8编码
a.说明
Java中的字符常量都是使用UTF8编码的,UTF8编码使用1~4个字节来表示具体的Unicode字符
所以有的字符占用一个字节,而我们平时所用的大部分中文都需要3个字节来存储
b.示例
65534个字母,编译通过
String s1 = "dd..d";
21845个中文”自“,编译通过
String s2 = "自自...自";
一个英文字母d加上21845个中文”自“,编译失败
String s3 = "d自自...自";
c.分析
对于s1,一个字母d的UTF8编码占用一个字节,65534字母占用65534个字节,长度是65534,长度和存储都没超过限制,所以可以编译通过。
对于s2,一个中文占用3个字节,21845个正好占用65535个字节,而且字符串长度是21845,长度和存储也都没超过限制,所以可以编译通过。
对于s3,一个英文字母d加上21845个中文”自“占用65536个字节,超过了存储最大限制,编译失败。
d.JVM规范对常量池的限制
a.说明
JVM规范对常量池有所限制。量池中的每一种数据项都有自己的类型
Java中的UTF-8编码的Unicode字符串在常量池中以CONSTANTUtf8类型表示
b.CONSTANTUtf8的数据结构
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
c.分析
我们重点关注下长度为 length 的那个bytes数组,这个数组就是真正存储常量数据的地方,而 length 就是数组可以存储的最大字节数。length 的类型是u2,u2是无符号的16位整数,因此理论上允许的的最大长度是2^16-1=65535。所以上面byte数组的最大长度可以是65535
e.运行时限制
a.说明
String 运行时的限制主要体现在 String 的构造函数上。下面是 String 的一个构造函数:
b.代码
public String(char value[], int offset, int count) {
...
}
c.分析
上面的count值就是字符串的最大长度。在Java中,int的最大长度是2^31-1。所以在运行时,String 的最大长度是2^31-1。
但是这个也是理论上的长度,实际的长度还要看你JVM的内存。我们来看下,最大的字符串会占用多大的内存。
(2^31-1)*16/8/1024/1024/1024 = 2GB
所以在最坏的情况下,一个最大的字符串要占用 2GB的内存。如果你的虚拟机不能分配这么多内存的话,会直接报错的。
f.JDK9优化
补充 JDK9以后对String的存储进行了优化。底层不再使用char数组存储字符串,而是使用byte数组
对于LATIN1字符的字符串可以节省一倍的内存空间
5.21 [7]自己写String类,包名也是java.lang,这个类能编译运行成功吗?
01.回答
编译:能编译成功
运行:运行时会报错,因为根据双亲委派机制,JVM会优先加载JDK中的String类
原理:双亲委派机制确保JVM优先加载标准库中的类,防止自定义类替换标准类
02.代码示例
a.代码
public class String {
public int print(int a) {
int b = a;
return b;
}
public static void main(String[] args) {
new String().print(1);
}
}
b.说明
运行错误:在类java.lang.String中找不到main方法
03.涉及的知识点
a.Java代码的编译过程
将.java源文件转换为.class字节码文件
使用IDEA或命令行工具javac进行编译
b.Java代码的运行过程
包括类的加载和执行
JVM在第一次使用类时加载它
c.类加载器
采用双亲委派模型,优先加载父加载器中的类
04.类加载过程
a.加载
获取类的二进制字节流
转换为方法区的运行时数据结构
生成代表该类的Class对象
b.连接
验证:确保字节流符合Java虚拟机规范
准备:为类的静态变量分配内存并初始化
解析:将符号引用转换为直接引用
c.初始化
执行类的初始化方法<clinit>()
只有在主动使用类时才会初始化
05.代码示例
a.代码
public class MainApp {
public static void main(String[] args) {
Animal animal = new Animal("Puppy");
animal.printName();
}
}
public class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void printName() {
System.out.println("Animal [" + name + "]");
}
}
b.说明
MainApp类加载:JVM加载MainApp类并执行main方法
Animal类加载:JVM加载Animal类,创建实例并调用构造函数
方法调用:通过对象引用调用printName()方法
5.22 [7]破坏双亲委派之后,能重写String类吗?
00.回答
不能
JVM 对核心类的优先加载机制
java.lang.String 的安全性限制
JVM 内部对 String 类的特殊处理
如果尝试修改或替换 String,可能导致程序运行异常甚至 JVM 崩溃。建议在 Java 开发中不要尝试替换或覆盖核心类
01.JVM 对 String 类的特殊限制和处理机制
a.JVM 内置限制
a.定义
java.lang.String 是 JVM 的核心类,其加载和使用受到严格限制。
b.机制
JVM 会优先加载和使用由引导类加载器(Bootstrap ClassLoader)加载的 String 类。
c.结果
即使破坏了双亲委派模型,定义了自己的 java.lang.String 类,JVM 在加载和运行中仍会优先使用引导类加载器加载的原生 String 类。
b.类加载机制
a.定义
引导类加载器加载的类是 JVM 的核心组件(如 java.lang 包下的类)。
b.机制
这些核心类无法被用户自定义的类加载器替换或重新加载。
c.结果
即使破坏双亲委派,java.lang.String 依然会被优先加载,且不允许被覆盖。
c.安全性限制
a.定义
Java 标准库中 java.lang.String 的不可替换性是 JVM 保证平台安全性和一致性的基础。
b.结果
如果允许替换 String,可能会导致各种意料之外的安全问题。
d.defineClass() 方法的限制
a.机制
JVM 提供的 ClassLoader 的 defineClass() 方法会对类的全限定名进行检查。
b.结果
如果类的全限定名以 java. 开头,则直接抛出 SecurityException,防止覆盖 java 包下的核心类。
c.实现
这项检查在 JVM 的底层实现中被硬编码,无法通过常规方式绕过。
02.尝试覆盖 java.lang.String 的结果
a.示例代码
package java.lang;
public class String {
public String() {
System.out.println("My String");
}
}
b.编译结果
a.错误提示
error: cannot access java.lang.String
b.原因
Java 禁止用户在 java.lang 包下定义核心类。
c.绕过编译检查
a.方法
使用某些工具或方法绕过编译检查(如直接修改 .class 文件)。
b.结果
在运行时仍然会因为类加载机制而失败。
d.破坏双亲委派的场景
a.方法
自定义类加载器,优先加载用户定义的类。
b.结果
由于 JVM 会始终优先使用引导类加载器加载核心类,因此定义的 java.lang.String 仍无法被 JVM 认可。
6 数据
6.1 [1]Integer缓存池
01.Integer缓存池
a.定义
如果要“装箱”的数字在[-128,127]以内,则直接从缓冲区获取,否则,需要使用 NEW 关键词创建一个新对象
b.关键点
1.范围:默认缓存值为 -128 到 127 之间的 Integer 对象
2.自动装箱:在自动装箱时(即将 int 类型自动转换为 Integer 对象),如果值在缓存范围内,会返回缓存中的对象
3.提高性能:避免频繁创建和销毁小范围内的 Integer 对象,节省内存,提高性能
02.缓冲区
a.int是基本类型,【基本数据类型 == 比较数值】
int a = 100;
int b = 100;
System.out.println(a == b); // 结果为true
int c = 1000;
int d = 1000;
System.out.println(c == d); // 结果为true
b.Integer是引用类型,【引用数据类型 == 比较地址】,IntegerCache定义范围[-128,127]
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // 结果为true
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // 结果为false
-----------------------------------------------------------------------------------------------------
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
c.Double是引用类型,【引用数据类型 == 比较地址】,没有Double对应的常量池
Double a = 100.0;
Double b = 100.0;
System.out.println(a == b); // 结果为false
Double c = 200.0;
Double d = 200.0;
System.out.println(c == d); // 结果为false
-----------------------------------------------------------------------------------------------------
public static Double valueOf(double d) {
return new Double(d);
}
d.Boolean是引用类型,【引用数据类型 == 比较地址】,没有Boolean对应的常量池
Boolean a = false;
Boolean b = false;
System.out.println(a == b); // 结果为true
Boolean c = true;
Boolean d = true;
System.out.println(c == d); // 结果为true
-----------------------------------------------------------------------------------------------------
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
03.缓冲区
a.代码
Integer i1 = 10; // 自动装箱
Integer i2 = new Integer(10); // 非自动装箱
Integer i3 = Integer.valueOf(10); // 非自动装箱
int i4 = new Integer(10); // 自动拆箱
int i5 = i2.intValue(); // 非自动拆箱
-----------------------------------------------------------------------------------------------------
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
Long h = 2L;
System.out.println(c==d);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
System.out.println(g.equals(a+h));
b.结果
true IntegerCache定义范围[-128,127]
false 没有Double对应的常量池
true a+b包含算数运算,触发拆箱,调用intValue()方法,因此进行数值上的比较
true 先自动拆箱(a+b各自调用intValue后加法运算),再自动装箱(调用Integer.valueOf),最后equals
true a+b包含算数运算,触发拆箱,调用intValue()方法,因此进行数值上的比较
false g.equals(a+b),g(Long包装类),a+b(Integer包装类:自动装箱调用Integer.valueOf)
true g.equals(a+b),g(Long包装类),a+h(Long包装类:自动装箱调用Long.valueOf)
c.分析
a."=="运算符
两边的操作符:均为包装类的引用,【比较地址,是否为同一个对象】
两边的操作符:有一个是表达式(算术运算),【比较数值,触发自动拆箱过程】
b.equals
包装器类型,equals方法【不会进行类型转换,即不会触发自动拆箱过程】
d.场景
a.场景1
调用一个【参数为Object类型】的“方法”,因此传入int类型时,【触发Integer.valueOf自动装箱】
b.场景2
调用一个【参数为Object类型】的“非泛型的容器”,因此传入int类型时,【触发Integer.valueOf自动装箱】
c.建议
优点:默认情况下,使用【原始类型/对象类型】更加简单
缺点:如果在一个循环体中,会创建无用的中间对象,这样会增加GC压力,拉低程序的性能
建议:装箱操作会创建对象,频繁的装箱操作会造成不必要的内存消耗,【写循环时,应当尽量避免装箱操作】
-------------------------------------------------------------------------------------------------
Long l = 0L;
for(int i = 0; i < 50000; i++) {
l += 1L;
}
6.2 [1]Integer与int区别
01.Integer与int区别
a.默认值
Integer的初始值是null,int的初始值是0
b.存储位置
Integer存储在【堆】内存,int类型是直接存储在【栈】空间
c.类型
int是基础数据类型
Integer是引用数据类型(对象类型),它封装了很多的方法和属性,我们在使用的时候更加灵活
6.3 [1]Integer i = new Integer(123) 和 Integer i = 123区别
01.谈谈Integer i = new Integer(123) 和 Integer i = 123 区别
a.自动装箱、自动拆箱
Integer i = new Integer(123):不会触发自动装箱
Integer i = 123:会触发自动装箱
b.执行效率和资源占用上的区别
第二种方式的执行效率和资源占用,在一般性情况下要优于第一种情况(注意,这并不是绝对的)
6.4 [1]Integer i = new Integer(123) 和 Integer.valueOf(123)区别
01.谈谈Integer i = new Integer(123) 和 Integer.valueOf(123) 区别
a.区别
new Integer(123):每次都会新建一个对象
Integer.valueOf(123):会使用缓存池中的对象,多次调用会取得同一个对象的引用
b.缓存池
在 Java 8 中,Integer 缓存池的大小默认为 -128~127
编译器会在自动装箱过程调用valueOf()方法,因为会使用自动装箱来创建,那么就会引用相同的对象
01.new Integer("127")
总是在堆内存中创建一个新的 Integer 对象
值为 127
不使用整数缓存池
02.Integer.valueOf("128")
创建一个新的 Integer 对象,值为 128
不使用整数缓存池,因为 128 超出了默认缓存范围
6.5 [1]Double类的valueOf方法,与Integer类的valueOf方法不同的实现?
01.为什么Double类的valueOf方法,与Integer类的valueOf方法不同的实现?
a.理由
很简单:在某个范围内的整型数值的个数是有限的,而浮点数却不是。
b.valueOf()类似
【Byte、Short、Integer、Long类】【Character类】:6个方法,它们实现valueOf()方法类似
【Float、Double类】:2个方法,它们实现valueOf()方法类似
6.6 [2]0.1+0.2=?
01.0.1+0.2等于多少?
0.1+0.2==0.3000000000000004
02.为什么不是0.3
a.回答
因为采用了IEEE754码制,十进制浮点数无法完全精确转换为二进制浮点数
b.说明
众所周知计算机使用的是二进制
十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101
但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数
c.结果
计算机是没办法用二进制精确的表示0.1的
人们想出了一种采用一定的精度,使用近似值表示一个小数的办法。这就是IEEE 754
03.那你能否实现0.1+0.2==0.3
a.回答
(0.1* 10 + 0.2 *10)/10==0.3
b.说明
其实想要使0.1+0.2等于0.3也是可以实现的 最简单的方法就是
(0.1* 10 + 0.2 *10)/10==0.3
直接乘10再除以10 就好了,因为乘以10以后就是整数运算了,就是精确值了
04.那你能否实现0.1+0.2==0.3
a.回答
使用BigDecimal类存储0.1和0.2,然后再用add方法相加!
b.代码
import java.math.BigDecimal;
public class Main{
public static void main(String[] args) {
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.2);
BigDecimal c = a.add(b);
System.out.println(c);
}
}
6.7 [2]float n=1.8有错
01.回答
错误
02.规则
在Java中,float 类型的变量赋值时需要在数值后面加上 f 后缀,
以表明这是一个 float 类型的常量,而不是默认的 double 类型常量。
因此,java float n=1.8 会有错误。
03.正确的写法应该是
float n = 1.8f;
在这种情况下,1.8f 表示这是一个 float 类型的常量,赋值给 float 类型的变量 n。
如果没有 f 后缀,编译器会报错,因为默认情况下 1.8 被视为 double 类型。
6.8 [2]不用float表示金额
01.原因
由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值
所以,千万不要在代码中使用浮点数来表示金额等重要的指标
02.建议
建议使用BigDecimal或者Long来表示金额
6.9 [2]float丢失精度而BigDecimal不会
00.总结
a.浮点数为什么会丢失精度?
因为某些十进制的小数在二进制中是一个无限循环的小数,所有用浮点数存在进度丢失问题
b.BigDecimal为什么不会丢失精度?
BigDecimal采用标度的形式解决了精度丢失问题。同时在使用BigDecimal的时候,使用String的构造器
最后在使用BigDecimal比较大小的时候不要使用equals,请用compareTo
01.浮点数的表示
a.IEEE 754 标准
a.定义
浮点数在计算机中通常采用 IEEE 754 标准进行表示
这个标准将数值分为三个部分:符号位、指数部分和尾数部分
b.精度丢失
由于尾数的位数有限,某些小数(尤其是十进制小数)无法精确地用二进制表示
b.精度丢失案例
a.示例代码
public class FloatPrecision {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println(c); // 输出 0.30000000000000004
}
}
b.现象
尽管期望 c 的值为 0.3,但实际上被计算为 0.30000000000000004
c.原因
浮点数运算中误差可能会逐渐累积,尤其是在涉及多个浮点数的加减乘除时
c.十进制小数转二进制过程
a.方法
十进制小数转换成二进制小数采用"乘2取整,顺序排列法"
b.示例
0.00011001100110011...(循环)
02.BigDecimal的解决方案
a.通过String构造
a.解决方案
通过BigDecimal("0.1") 的String 构造不会造成精度丢失,避免构造函数用浮点数表示
b.原理
BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的
c.关键参数
a.无标度值(Unscaled Value)
这是一个整数,表示BigDecimal的实际数值
b.标度(Scale)
这是一个整数,表示小数点后的位数
c.实际数值计算公式
unscaledValue × 10^(-scale)
b.假设有一个BigDecimal表示的数值是123.45
无标度值(Unscaled Value)是12345
标度(Scale)是2
计算123.45 = 12345 × 10^(-2)
c.不能用BigDecimal的equals方法做等值
a.原因
equals方法会比较标度
b.解决方案
比较大小的话用 compareTo()比较
c.示例代码
import java.math.BigDecimal;
public class BigDecimalComparison {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("1.0");
BigDecimal num2 = new BigDecimal("1.00");
System.out.println("equals: " + num1.equals(num2)); // 输出false
System.out.println("compareTo: " + num1.compareTo(num2)); // 输出: 0
}
}
6.10 [3]BigDecimal踩坑指南
00.汇总
直接用浮点数初始化
加减乘除时不设精度
用 equals 判断相等
使用 scale 时忽视实际含义
忽略不可变性
忽视性能问题
01.直接用浮点数初始化
a.问题描述
不少小伙伴习惯这样写:
---
BigDecimal num = new BigDecimal(0.1);
System.out.println(num);
---
打印结果:0.1000000000000000055511151231257827021181583404541015625
并非打印的:0.1
问题出在哪?
这不是 BigDecimal 的问题,而是浮点数本身的“锅”。
在Java中,double的精度有限的,0.1 转换成二进制是个无限循环小数,直接传进去会带上误差。
b.正确做法
正确姿势是传字符串:
---
BigDecimal num = new BigDecimal("0.1");
System.out.println(num);
---
打印结果:0.1,是正确的。
注意:永远不要用 BigDecimal(double) 构造函数,用字符串或整数更靠谱。也可以使用BigDecimal.valueOf()函数。
02.加减乘除时不设精度
a.问题描述
有些小伙伴做加减乘除的时候,直接写:
---
BigDecimal a = new BigDecimal("1.03");
BigDecimal b = new BigDecimal("0.42");
//减法
BigDecimal result = a.subtract(b);
System.out.println(result);
---
打印结果:0.61,没问题。
但问题在 除法 时:
---
BigDecimal c = new BigDecimal("10");
BigDecimal d = new BigDecimal("3");
BigDecimal result = c.divide(d);
---
运行直接炸了:java.lang.ArithmeticException: Non-terminating decimal expansion
报错的根本原因:10/3 是无限小数,BigDecimal 默认不保留小数点后面,精度溢出。
b.优化方法
答:加一个 MathContext 或指定精度。
例如:
---
BigDecimal result = c.divide(d, 2, RoundingMode.HALF_UP);
System.out.println(result);
---
打印结果:3.33,可以正常运行。
因此,我们需要注意,在BigDecimal 做除法时 ,必须指定精度。
03.用 equals 判断相等
a.问题描述
BigDecimal 的 equals 会比较 值和精度,这坑了不少人:
---
BigDecimal x = new BigDecimal("1.0");
BigDecimal y = new BigDecimal("1.00");
System.out.println(x.equals(y));
---
打印结果:false。
尽管 1.0 和 1.00 的数值相等,但精度不一样,equals 判定为不同。
b.优化方法
用 compareTo 比较数值:
例如:
---
System.out.println(x.compareTo(y) == 0);
---
打印结果:true
需要特别注意的地方是:我们在判断两个BigDecimal对象是否相等时,应该用 compareTo方法,别用 equals方法。
04.使用 scale 时忽视实际含义
a.问题描述
有些小伙伴搞不清 scale(小数位数)和 precision(总位数)的区别,直接写:
---
BigDecimal num = new BigDecimal("123.4500");
System.out.println(num.scale());
---
打印结果:4
但如果你写成下面这样的:
---
BigDecimal stripped = num.stripTrailingZeros();
System.out.println(stripped.scale());
---
打印结果却是:2
scale 会发生变化,搞不好会影响后续计算。
b.优化方法
答:明确 scale 的含义。
如果要固定小数位,使用 setScale:
---
BigDecimal fixed = num.setScale(2, RoundingMode.HALF_UP);
System.out.println(fixed);
---
打印结果:123.45。
我们不要混淆 scale 和 precision,必要时显式设置小数位数。
05.忽略不可变性
a.问题描述
BigDecimal 是不可变的,但有些小伙伴会这样写:
---
BigDecimal sum = new BigDecimal("0");
for (int i = 0; i < 5; i++) {
sum.add(new BigDecimal("1"));
}
---
打印结果:0
问题原因是 add 方法不会改变原对象,而是返回一个新的 BigDecimal 实例。
b.优化方法
答:用变量接住返回值。
---
BigDecimal sum = new BigDecimal("0");
for (int i = 0; i < 5; i++) {
sum = sum.add(new BigDecimal("1"));
}
System.out.println(sum);
---
打印结果是:5
BigDecimal 操作后需要接住新实例。
06.忽视性能问题
a.问题描述
BigDecimal 是很精确,但也很慢。
如果大量计算时用 BigDecimal,会拖累性能,比如计算利息:
---
BigDecimal principal = new BigDecimal("10000");
BigDecimal rate = new BigDecimal("0.05");
BigDecimal interest = principal.multiply(rate);
---
一个循环里搞上百万次,性能直接拉垮。
b.优化方法
答:能用整数就用整数(比如分代替元)。
批量计算时,用 double 计算,结果最后转换成 BigDecimal。
---
double principal = 10000;
double rate = 0.05;
BigDecimal interest = BigDecimal.valueOf(principal * rate);
System.out.println(interest);
---
打印结果:500.00
参与大批量计算时,两个BigDecimal对象直接计算会比较慢,尽量少用,能优化的地方别放过。
6.11 [3]BigDecimal常见方法
01.关键点
高精度:BigDecimal 可以精确表示任意大小和精度的数值,不会像 double 和 float 那样因为精度问题产生误差。
不可变性:BigDecimal 对象是不可变的,即每次操作都会创建一个新的 BigDecimal 对象。
四舍五入:提供多种舍入模式,如 RoundingMode.HALF_UP、RoundingMode.DOWN 等。
02.常用构造方法
BigDecimal(String val): 通过字符串创建 BigDecimal 对象,推荐使用此方法以避免精度问题。
BigDecimal(double val): 通过 double 创建 BigDecimal 对象,但可能会引入精度问题。
03.常用方法
加法:add(BigDecimal augend)
减法:subtract(BigDecimal subtrahend)
乘法:multiply(BigDecimal multiplicand)
除法:divide(BigDecimal divisor, RoundingMode roundingMode),需要指定舍入模式。
比较:compareTo(BigDecimal val),返回 -1、0、1 分别表示小于、等于、大于。
6.12 [3]BigDecimal金额计算
00.汇总
对Long和BigDecimal的优缺点分析,我们可以得出以下结论:
在金额计算层面,即代码实现中,推荐使用BigDecimal进行所有与金额相关的计算
BigDecimal提供了高精度的数值运算,能够确保金额计算的精确性,避免了因浮点数精度问题导致的财务误差
使用BigDecimal可以简化代码逻辑,减少因处理精度问题而引入的复杂性
01.BigDecimal相加必须用新的对象接收,两个加数本身是不变的
// 错误 // 正确
public static void main() { public static void main() {
// 有三笔钱,计算三笔钱相加的总金额 // 有三笔钱,计算三笔钱相加的总金额
BigDecimal b1 = new BigDecimal("1"); BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = new BigDecimal("2"); BigDecimal b2 = new BigDecimal("2");
BigDecimal b3 = new BigDecimal("3"); BigDecimal b3 = new BigDecimal("3");
// //
List<BigDecimal> bigDecimalList = new ArrayList<>(); List<BigDecimal> bigDecimalList = new ArrayList<>();
bigDecimalList.add(b1); bigDecimalList.add(b1);
bigDecimalList.add(b2); bigDecimalList.add(b2);
bigDecimalList.add(b3); bigDecimalList.add(b3);
// //
BigDecimal sumAmount = BigDecimal.ZERO; BigDecimal sumAmount = BigDecimal.ZERO;
for(BigDecimal bigDecimal : bigDecimalList){ for(BigDecimal bigDecimal : bigDecimalList){
sumAmount.add(bigDecimal); sumAmount = sumAmount.add(bigDecimal);
} }
Log.info("sumAmount={}",sumAmount.toPlainString()); Log.info("sumAmount={}",sumAmount.toPlainString());
} }
02.BigDecimal比较大小一定要用compareTo()
public static void main() {
BigDecimal b1 = new BigDecimal("1");
BigDecimal b2 = new BigDecimal("1.0");
log.info("是否相等:{}", b1.equals(b2)); // false
BigDecimal b3 = new BigDecimal("1");
BigDecimal b4 = new BigDecimal("1.0");
log.info("是否相等:{}", b1.compareTo(b2)); // 0
}
03.用字符串构造函数
public static void main() {
BigDecimal b1 = new BigDecimal(0.1); //这是double转BigDecimal
BigDecimal b2 = new BigDecimal("0.1"); //这是string转BigDecimal推荐String
log.info("b1={}", b1.toPlainString()); //b1=0.1000000000000000055511151231257827021181583404541015625,精度丢失
log.info("b2={}", b2.toPlainString()); //b2=0.1
}
6.13 [3]禁用double构造BigDecimal
00.汇总
避免直接使用 double 构造 BigDecimal,以免引入二进制浮点数的精度误差
优先使用字符串构造器,或使用 BigDecimal.valueOf(double) 以确保精度
01.场景
a.说明
在使用 BigDecimal 时,不建议直接使用 double 作为构造参数
这是因为 double 类型在 Java 中的表示是基于二进制浮点数的,会引入精度误差,从而导致不准确的结果
b.代码
double d = 0.1;
BigDecimal bd = new BigDecimal(d);
System.out.println(bd); // 输出 0.1000000000000000055511151231257827021181583404541015625
02.原因
a.二进制浮点数的精度问题
double 使用 IEEE 754 标准表示小数
在二进制系统中,像 0.1 这样的小数无法精确表示,导致它在存储时会变成一个近似值
这个近似值会直接传递给 BigDecimal 的构造方法,从而生成带有误差的 BigDecimal 值
b.结果不准确,影响业务计算
在一些金融计算或其他对精度要求高的场景中
直接使用 double 构造 BigDecimal 会带来潜在的误差积累,从而影响最终的结果
例如,在多次计算或累加时,误差可能不断放大
03.推荐的替代方案
a.使用字符串或精确值构造 BigDecimal
a.说明
通过传入字符串形式的数字,可以避免精度误差,因为字符串构造器不会引入任何二进制的近似计算
b.代码
BigDecimal bd = new BigDecimal("0.1");
System.out.println(bd); // 输出 0.1
b.使用 BigDecimal.valueOf(double) 方法
a.说明
该方法会将 double 转换为 String 表示,然后构造 BigDecimal,从而避免精度损失
b.代码
BigDecimal bd = BigDecimal.valueOf(0.1);
System.out.println(bd); // 输出 0.1
6.14 [3]禁用BigDecimal的equals比较
00.汇总
不要使用equals方法:它会考虑精度和符号,容易导致误判
推荐使用compareTo方法:只比较数值,忽略精度和正负零的差异,可以实现更符合业务需求的等值比较
01.equals 方法比较严格,包含了精度和符号的比较
a.说明
BigDecimal.equals 不仅比较数值本身,还会比较精度和符号
例如,BigDecimal 的 equals 方法会认为 1.0 和 1.00 是不同的值
因为它们的 scale 不同(即小数位数不同)
b.代码
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.equals(b)); // 输出 false
c.说明
尽管 1.0 和 1.00 数值上是相等的,但 equals 方法会因为精度不同返回 false。
02.equals 方法会区分正负零
a.说明
在 BigDecimal 中,正零 (0.0) 和负零 (-0.0) 是不相等的,而使用 equals 会导致 0.0 和 -0.0 被视为不相等
b.代码
BigDecimal zero1 = new BigDecimal("0.0");
BigDecimal zero2 = new BigDecimal("-0.0");
System.out.println(zero1.equals(zero2)); // 输出 false
c.说明
这可能会导致误判,因为在大多数业务逻辑中,我们认为 0.0 和 -0.0 是等值的。
03.推荐的替代方案:使用 compareTo 方法
a.说明
为了避免这些问题,建议使用 BigDecimal.compareTo 方法
compareTo 方法仅比较数值的大小,不关注精度和符号
因此,在需要判断两个 BigDecimal 是否等值时,使用 compareTo 更为合理:
b.代码
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
System.out.println(a.compareTo(b) == 0); // 输出 true
c.说明
在这种情况下,1.0 和 1.00 被视为相等,即使它们的精度不同,compareTo 也会返回 0
6.15 [3]慎用BigDecimal的toString()
01.问题
之前的处理逻辑如下面代码,当遇到 10.0 这样的数字时,展示的是 1E1 这样的科学计数法,这与期望的 10 不一样。
BigDecimal.valueOf(number).stripTrailingZeros()
02.原因
a.说明
在 Java 中,BigDecimal 的输出格式是受其内部表示方式和 toString 方法的影响
b.浮点数的表示
当你使用 10.0(一个 double 值)来构造 BigDecimal 时,BigDecimal.valueOf(double) 将其转化为字符串
而在这个过程中,浮点数可能会被以科学计数法表示。如果这个值被解析为 10.0,那么它将被表示为 1E+1
c.内部数据结构
BigDecimal 用整型和幂的组合来表示数字,因此对于 10.0,它实际上被认为是 1 乘以 10 的幂(即 10^1)
所以它使用科学计数法的结构
03.解法
a.用 BigDecimal 的 toPlainString() 方法替代
public String toPlainString() {
if(scale==0) {
if(intCompact!=INFLATED) {
return Long.toString(intCompact);
} else {
return intVal.toString();
}
}
if(this.scale<0) {
if(signum()==0) {
return "0";
}
int tailingZeros = checkScaleNonZero((-(long)scale));
StringBuilder buf;
if(intCompact!=INFLATED) {
buf = new StringBuilder(20+tailingZeros);
buf.append(intCompact);
} else {
String str = intVal.toString();
buf = new StringBuilder(str.length()+tailingZeros);
buf.append(str);
}
for (int i = 0; i < tailingZeros; i++)
buf.append('0');
return buf.toString();
}
String str ;
if(intCompact!=INFLATED) {
str = Long.toString(Math.abs(intCompact));
} else {
str = intVal.abs().toString();
}
return getValueString(signum(), str, scale);
}
b.判断是否有小数部分
if(scale==0) {
if(intCompact!=INFLATED) {
return Long.toString(intCompact);
} else {
return intVal.toString();
}
}
-----------------------------------------------------------------------------------------------------
如果 scale 为 0,表示没有小数部分
如果 intCompact 不是 INFLATED(可能表示它是一个较小的整数),则直接将其转换为字符串返回
否则使用 intVal 的字符串表示返回
c.处理没有小数点的情况
if(this.scale<0) {
if(signum()==0) {
return "0";
}
int tailingZeros = checkScaleNonZero((-(long)scale));
StringBuilder buf;
if(intCompact!=INFLATED) {
buf = new StringBuilder(20+tailingZeros);
buf.append(intCompact);
} else {
String str = intVal.toString();
buf = new StringBuilder(str.length()+tailingZeros);
buf.append(str);
}
for (int i = 0; i < tailingZeros; i++)
buf.append('0');
return buf.toString();
}
-----------------------------------------------------------------------------------------------------
如果 scale 为负数,表示没有小数点且有多个尾随零
如果数值的符号是 0,直接返回 "0"
调用 checkScaleNonZero 方法确定需要补充的零的数量
创建一个字符缓冲区 buf,根据 intCompact 的值选择初始化字符串长度
将实际数值(可能是长整型或者大整数)添加到 buf 中
在 buf 后面追加必要的零
最后返回构建的字符串
d.处理有小数点的情况
String str ;
if(intCompact!=INFLATED) {
str = Long.toString(Math.abs(intCompact));
} else {
str = intVal.abs().toString();
}
return getValueString(signum(), str, scale);
-----------------------------------------------------------------------------------------------------
如果 scale 大于 0,表示数值有小数点
判断 intCompact 是否是 INFLATED,选择获取数值的绝对值字符串
通过调用 getValueString 方法传入符号、绝对值字符串和小数位数来生成最终的字符串表示
6.16 [4]Math.round(1.5) 等于多少?
01.运行结果
2(默认四舍五入)
02.java.lang.Math类
ceil() :向上取整,返回小数所在两整数间的较大值,返回类型是 double,如 -1.5 返回 -1.0
floor() :向下取整,返回小数所在两整数间的较小值,返回类型是 double,如 -1.5 返回 -2.0
round() :朝正无穷大方向返回参数最接近的整数,可以换算为 参数 + 0.5 向下取整,返回值是 int 或 long,如 -1.5 返回 -1
03.Math.round 并不总是向上取整
使用的是四舍五入规则
当小数部分小于 0.5 时,向下取整(即取较小的整数)。
当小数部分大于等于 0.5 时,向上取整(即取较大的整数)。
04.计算结果
System.out.println(Math.round(1.4)); // 输出: 1
System.out.println(Math.round(1.5)); // 输出: 2
System.out.println(Math.round(1.6)); // 输出: 2
System.out.println(Math.round(-1.4)); // 输出: -1
System.out.println(Math.round(-1.5)); // 输出: -1
System.out.println(Math.round(-1.6)); // 输出: -2
6.17 [4]Math.round(-1.5) 等于多少?
01.运行结果
-1(默认四舍五入)
02.java.lang.Math 类
ceil() :向上取整,返回小数所在两整数间的较大值,返回类型是 double,如 -1.5 返回 -1.0
floor() :向下取整,返回小数所在两整数间的较小值,返回类型是 double,如 -1.5 返回 -2.0
round() :朝正无穷大方向返回参数最接近的整数,可以换算为 参数 + 0.5 向下取整,返回值是 int 或 long,如 -1.5 返回 -1
03.Math.round 并不总是向上取整
使用的是四舍五入规则
当小数部分小于 0.5 时,向下取整(即取较小的整数)。
当小数部分大于等于 0.5 时,向上取整(即取较大的整数)。
04.计算结果
System.out.println(Math.round(1.4)); // 输出: 1
System.out.println(Math.round(1.5)); // 输出: 2
System.out.println(Math.round(1.6)); // 输出: 2
System.out.println(Math.round(-1.4)); // 输出: -1
System.out.println(Math.round(-1.5)); // 输出: -1
System.out.println(Math.round(-1.6)); // 输出: -2
6.18 [5]Object obj=new Object()占用字节
01.以64位操作系统为例,new Object()占用大小分为两种情况:
a.未开启指针压缩
占用大小为:8(Mark Word)+8(Class Pointer)=16字节
b.开启了指针压缩(默认是开启的)
开启指针压缩后,Class Pointer会被压缩为4字节,最终大小为:8(Mark Word)+4(Class Pointer)+4(对齐填充)=16字节
6.19 [5]存储IPv4地址:32位的无符号整数
00.回答
当存储IPv4地址时,应该使用32位的无符号整数(UNSIGNED INT)来存储IP地址,而不是使用字符串
01.相对字符串存储,使用无符号整数来存储有如下的好处
节省空间,不管是数据存储空间,还是索引存储空间
便于使用范围查询(BETWEEN...AND),且效率更高
7 判断
7.1 [1]判断两个数字
01.判断问题
a.方式1:基本类型
==
b.方式2:包装类型
equals()
c.方式3:浮点数
Math.abs(a - b) < EPSILON
d.方式4:高精度数值(BigDecimal)
compareTo()
7.2 [1]==不能比较String、int
01.代码
String name = "zs";
String pwd = "123";
if(name.equals("zs") && pwd.equals("123") {
System.out.println("成功!")
}
02.说明
运算符“==”不能应用于“java.lang.String”、“int”,应当使用"equals"
7.3 [2]equals()和==
01.判断问题
a.说明1
==:关系运算符,对象内存地址,【原生数据类型(地址中储存数值),引用数据类型(地址中存储指向堆中的地址)】
equals:默认Object的equals()方法,若父类及子类重写了Object的equals,则判断根据重写规则
b.说明2
原生数据类型:【直接存于栈中的,"没有实例,但有常量池"】
引用数据类型:【"引用"存放于栈中】【"实例"存放于堆中,同时有常量池】
c.说明3
原生数据类型:【基本数据类型 == 比较数值】【基本数据类型 equals 比较数值,调用Object的equals】
引用数据类型:【引用数据类型 == 比较地址】【引用数据类型 equals 先地址,再数值,调用String的equals】
02.比较规则
a.说明
一个未继承父类、未实现接口的类,重写equals()保证【先比较地址,再比较类型,最后比较内容】,再重写hashCode
b.代码
public class Student {
public String name;
public int age;
@Override
public boolean equals(Object o) { // IDEA 自动生成的equals方法
if (this == o) return true;
if (!(o instanceof Student)) return false; // bc.equals(c) 需比较 c instanceof BigCar
Student student = (Student) o;
return Objects.equals(this.name, student.getName) && this.age == student.age;
}
@Override
public int hashCode() {
return Objects.hash(getName(), getAge() // IDEA 自动生成的hashCode方法
}
}
7.4 [2]equals()和hashCode()
01.概念
a.eauqls()
用于比较两个对象是否相等
默认Object的equals()方法,若父类及子类重写了Object的equals,则判断根据重写规则
b.hashCode()
用于获取哈希码(散列码)
作用是确定该对象在哈希表中的索引位置,根据“键”快速的检索出对应的“值”,从而判断是不是同一个对象;
02.常见考点
a.遵守规范
如果【两个对象】相等,则它们必须有【相同的哈希码】
如果两个对象有【相同的哈希码】,则它们【未必相等】
b.hashCode()相同,equals()也一定为true吗?
不一定
c.equals()为true,hashCode()也一定相同吗?
不一定
03.常见考点
a.equals()已经实现功能了,还需要 hashCode()做什么?
hashCode(),哈希算法,效率更快
-----------------------------------------------------------------------------------------------------
重写equals(),虽然过程详细,但效率较低,
例如集合中目前2000个元素,加入第2001元素时,调用equals方法2000次
b.为什么不全部使用高效率的 hashCode(),还要用 equals()?
hash相同的对象 -> 对象可能相同、也可能不同
-----------------------------------------------------------------------------------------------------
hashCode()方法不是一个100%可靠的方法,个别情况下,不同的对象生成的hashcode也可能会相同
c.为什么重写equals时,必须重写hashCode方法?
如果重写 equals 后,而未重写 hashCode,
可能会出现equals相同(根据对象的特征进行重写),而hashCode不同
-----------------------------------------------------------------------------------------------------
public class Student {
private String name;
public int age;
// get set ...
// 重写 equals() 不重写 hashcode()
public static void main(String[] args) {
Student stu1 = new Student("BWH_Steven", 22);
Student stu2 = new Student("BWH_Steven", 22);
System.out.println(stu1.equals(stu2)); // 结果为:true
System.out.println(stu1.hashCode()); // 结果为:1128032093
System.out.println(stu2.hashCode()); // 结果为:1066516207
}
}
d.hashCode() 和 equals() 是如何一起判断保证高效又可靠的?
如果把对象保存到 HashTable、HashMap、HashSet 等中(不允许重复),
首先,使用hashCode()去比较,如果【二者hash不同的对象 -> 对象一定不同 】
然后,为保证其绝对可靠,再使用equals()进行再次内容比较,从而准确判断二者是否相同
-----------------------------------------------------------------------------------------------------
如果hashCode()方法结果不同,则两个对象肯定不同,则无需再使用equals()方法验证
7.5 [3]equals():String类
01.说明
String类,重写了Object类中的equals、hashCode方法,并且在Object类上,进行了各自的扩展
例如,equals方法不仅仅是对【先对象的引用地址进行判断,再对"字符"进行逐个比对判断】
02.源码如下:
a.Object类
public boolean equals(Object obj) {
return (this == obj);
}
b.String类
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
7.6 [3]equals:Java核心技术
01.出自【Java核心技术 第一卷:基础知识】
a.建议1
显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量(参数名命名,强制转换请参考建议5)
b.建议2
检测this与otherObject是否引用同一个对象:if(this == otherObject) return true;
c.建议3
检测otherObject是否为null,如果为null,返回false:if(otherObject == null) return false;
d.建议4
比较this与otherObject是否属于同一个类:
如果equals在每个子类中有所改变,用getClass:if(getClass()!=otherObject.getClass()) return false;
如果所有子类都拥有统一的语义,用instanceof:if(!(otherObject instanceof ClassName)) return false;
e.建议5
将otherObject转换为相应的类类型变量:ClassName other = (ClassName) otherObject;
f.建议6
使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true,否则就返回flase:
如果在子类中重新定义equals,就要在其中包含调用super.equals(other)
当此方法被重写时,通常有必要重写hashCode方法,维护hashCode方法的规定(相等对象必须具有相等的哈希码)
8 日期
8.1 [1]JDK8:3类
01.为什么JDK8之前的时间、日期API不好用
a.java.util.Date是从JDK1开始提供,易用性差
默认是中欧时区(Central Europe Time)
起始年份是1900年
起始月份从0开始
对象创建之后可修改
b.JDK1.1废弃了Date中很多方法,新增了并建议使用java.util.Calendar类
相比 Date去掉了年份从1900年开始
月份依然从0开始
选用Date或Calendar,让人更困扰
c.DateFormat格式化时间,线程不安全
略
02.jdk8日期API
a.新增java.time包
区分适合人阅读的和适合机器计算的时间与日期类
日期、时间及对比相关的对象创建完均不可修改
可并发解析与格式化日期与时间
支持设置不同的时区与历法
b.三类API
1.LocalDate 表示日期(年月日)
2.LocalTime 表示时间(时分秒)
3.LocalDateTime 表示日期+时间 (年月日时分秒)
8.2 [1]JDK8:API示例
00.汇总
1.获取当前时间对象
2.获取时间中的具体值(年月日星期几)
3.日期格式之间的转换
4.日期与字符串之间的转换
5.时间增减,plus和minus系列方法
6.直接修改年月日时分秒,with系列方法
7.计算两时刻直接的差值
01.获取当前时间对象
public class dateTest01 {
public static void main(String[] args) {
// 获取当前年月日时间
LocalDate nowdate = LocalDate.now();
// 获取当前时间时分秒
LocalTime nowTime = LocalTime.now();
//获取当前时间年月日时分秒
LocalDateTime now = LocalDateTime.now();
//获取具体时间
LocalDateTime of = LocalDateTime.of(2022, 6, 26, 21, 45, 50);
System.out.println(nowdate);
System.out.println(nowTime);
System.out.println(now);
System.out.println(of);
}
}
02.获取时间中的具体值(年月日星期几)
public class DateTime02 {
public static void main(String[] args) {
//获取具体时间
LocalDateTime now = LocalDateTime.now();
//获取年份
int year = now.getYear();
//获取月份
int monthValue = now.getMonthValue();
//获取几号
int dayOfMonth = now.getDayOfMonth();
//获取一年中第几天
int dayOfYear = now.getDayOfYear();
//获取星期几
DayOfWeek dayOfWeek = now.getDayOfWeek();
//时
int hour = now.getHour();
//分
int minute = now.getMinute();
//秒
int second = now.getSecond();
System.out.println("具体时间:" + now);
System.out.println("获取年份:" + year);
System.out.println("获取月份:" + monthValue);
System.out.println("获取几号:" + dayOfMonth);
System.out.println("获取一年中第几天:" + dayOfYear);
System.out.println("获取星期几:" + dayOfWeek);
System.out.println("时:" + hour);
System.out.println("分:" + minute);
System.out.println("秒:" + second);
}
}
03.日期格式之间的转换
public class Datetime03 {
public static void main(String[] args) {
//获取具体时间
LocalDateTime of = LocalDateTime.now();
//转换为日月年
LocalDate localDate = of.toLocalDate();
//转换为时分秒
LocalTime localTime = of.toLocalTime();
System.out.println("当前具体时间:" + of);
System.out.println("当前时间年月日:" + localDate);
System.out.println("当前时间时分秒:" + localTime);
}
}
04.日期与字符串之间的转换
public class DateTime04 {
public static void main(String[] args) {
//获取具体时间
LocalDateTime of = LocalDateTime.now();
//定义字符串格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss");
//格式化时间为字符串
String format = of.format(dateTimeFormatter);
//定义时间戳格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss");
//格式化字符串为时间戳
LocalDateTime parse = LocalDateTime.parse(format, formatter);
System.out.println("当前时间格式化为字符串:" + format);
System.out.println("当前时间的字符串格式化为Localdatetime格式:" + parse);
}
}
05.时间增减,plus和minus系列方法
public class DateTime05 {
public static void main(String[] args) {
//获取具体时间
LocalDateTime of = LocalDateTime.now();
//时间年份+1 或者-1
LocalDateTime localDateTime = of.plusYears(1);
LocalDateTime localDateTime1 = of.plusYears(-1);
LocalDateTime localDateTime2 = of.plusDays(1);
//时间年份-1 或者+1
LocalDateTime localDateTime3 = of.minusYears(1);
LocalDateTime localDateTime4 = of.minusYears(-1);
LocalDateTime localDateTime5 = of.minusDays(1);
System.out.println(localDateTime);
System.out.println(localDateTime1);
System.out.println(localDateTime2);
System.out.println(localDateTime3);
System.out.println(localDateTime4);
System.out.println(localDateTime5);
}
}
06.直接修改年月日时分秒,with系列方法
public class DateTime06 {
public static void main(String[] args) {
//获取具体时间
LocalDateTime of = LocalDateTime.now();
//直接分别修改年月日
LocalDateTime localDateTime = of.withYear(2022);
LocalDateTime localDateTime1 = of.withMonth(5);
LocalDateTime localDateTime2 = of.withDayOfMonth(20);
System.out.println(localDateTime);
System.out.println(localDateTime1);
System.out.println(localDateTime2);
}
}
07.计算两时刻直接的差值
public class DateTime07 {
public static void main(String[] args) {
//获取具体时间
LocalDate of1 = LocalDate.of(2022, 4, 4);
LocalDate of2 = LocalDate.of(2025, 12, 12);
//计算两个时间的间隔(*LocalDate类型参数)
Period between = Period.between(of1, of2);
//获取时间段的年数
int years = between.getYears();
//获取时间段的月数
int months = between.getMonths();
//获取时间段的天数
int days = between.getDays();
//获取时间段内总共的月数
long l = between.toTotalMonths();
System.out.println(years);
System.out.println(months);
System.out.println(days);
System.out.println(l);//44
}
}
8.3 [1]工具类:3类
01.FactoryUtils获取时间
Date date = FactoryUtils.getYesterday(); 昨天
Date date = FactoryUtils.getToday(); 今天
Date date = FactoryUtils.dateAdd(3, 1); 明天
Date date = FactoryUtils.getDate2("2024-01-01 00:00:00"); Date对象
---------------------------------------------------------------------------------------------------------
String str = FactoryUtils.getDate(new Date(), "HH"); HH
String str = FactoryUtils.getDate(new Date(), "mm"); mm
String str = FactoryUtils.getDate(new Date(), "ss"); ss
String str = FactoryUtils.getDate(new Date(), "yyyyMMdd"); yyyyMMdd
String str = FactoryUtils.getDate(new Date(), "yyyy-MM-dd"); yyyy-MM-dd
String str = FactoryUtils.getDate(new Date(), "yyyy年MM月dd日"); yyyy年MM月dd日
String str = FactoryUtils.getDate(new Date(), "yyyy-MM-dd HH:mm:ss"); yyyy-MM-dd HH:mm:ss
String str = FactoryUtils.getDate("2024-01-01 00:00:00", "yyyy-MM-dd HH:mm:ss"); yyyy-MM-dd HH:mm:ss
02.SimpleDateFormat获取时间
Date date = new SimpleDateFormat("yyyy-MM-dd").parse("2024-05-06"); 当前日期
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2024-05-23 14:35:20"); 当前日期
---------------------------------------------------------------------------------------------------------
String dateString = "2024-05-23 14:35:20";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(dateString);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, -1);
Date previousDay = calendar.getTime(); 当前日期-1天
---------------------------------------------------------------------------------------------------------
calendar.add(Calendar.DAY_OF_MONTH, -1); 昨天
new Date() 今天
calendar.add(Calendar.DAY_OF_MONTH, 1); 明天
---------------------------------------------------------------------------------------------------------
String date = new SimpleDateFormat("HH").format(new Date()); HH
String date = new SimpleDateFormat("mm").format(new Date()); mm
String date = new SimpleDateFormat("ss").format(new Date()); ss
String date = new SimpleDateFormat("yyyyMMdd").format(new Date()); yyyyMMdd
String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); yyyy-MM-dd
String date = new SimpleDateFormat("yyyy年MM月dd日").format(new Date()); yyyy年MM月dd日
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); yyyy-MM-dd HH:mm:ss
03.DateUtil获取时间
Date date = DateUtil.parse("2024-05-06"); 当前日期
Date date = DateUtil.parse("2024-04-11_00:15", "yyyy-MM-dd_HH:mm"); 当前日期
Date date = DateUtil.offsetDay(DateUtil.parse("2024-03-11 00:00:00"), -1); 当前日期-1天
---------------------------------------------------------------------------------------------------------
String date = DateUtil.format(DateUtil.date(), "yyyy-MM-dd"); 今天
String date = DateUtil.format(DateUtil.date(), "yyyy年MM月dd日"); 今天
String ycsj = DateUtil.beginOfMonth(DateUtil.date()).toString(); 当前月第一天
String ymsj = DateUtil.endOfMonth(DateUtil.date()).toString(); 当前月最后一天
String kssj = DateUtil.beginOfDay(DateUtil.yesterday()).toString(); 2024-04-11 00:00:00
String jssj = DateUtil.offsetSecond(DateUtil.endOfDay(DateUtil.yesterday()), -59).toString(); 2024-04-11 23:59:00
String kssj = DateUtil.beginOfDay(DateUtil.date()).toString(); 2024-04-12 00:00:00
String jssj = DateUtil.offsetSecond(DateUtil.endOfDay(DateUtil.date()), -59).toString(); 2024-04-12 23:59:00
String kssj = DateUtil.beginOfDay(DateUtil.tomorrow()).toString(); 2024-04-13 00:00:00
String jssj = DateUtil.offsetSecond(DateUtil.endOfDay(DateUtil.tomorrow()), -59).toString(); 2024-04-13 23:59:00
04.DateUtil比较时间
import cn.hutool.core.date.DateUtil;
import java.util.Date;
public class Main {
public static void main(String[] args) {
// 创建两个日期对象
Date date1 = DateUtil.parse("2024-01-01 10:00:00");
Date date2 = DateUtil.parse("2024-01-01 10:00:00");
// 比较两个日期是否相等
boolean isEqual = DateUtil.equals(date1, date2);
// 输出结果
System.out.println("两个日期是否相等: " + isEqual);
}
}
8.4 [2]java.sql.Date和java.util.Date
01.区别
java.sql.Date 是 java.util.Date 的子类
java.util.Date 是 JDK 中的日期类,精确到时、分、秒、毫秒
02.java.sql.Date
java.sql.Date 与数据库 Date 相对应的一个类型,只有日期部分,时分秒都会设置为 0,如:2019-10-23 00:00:00
要从数据库时间字段取 时、分、秒、毫秒数据,可以使用 java.sql.Timestamp
00.回答
SimpleDateFormat不是线程安全的
01.问题剖析
a.示例
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime date = LocalDateTime.now();
String formattedDate = formatter.format(date);
b.说明
如果你必须使用SimpleDateFormat,并且需要在多线程环境中使用它你可以考虑使用synchronized关键字来确保线程安全
c.示例
public synchronized String formatDate(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return formatter.format(date);
}
d.说明
这样,每次只有一个线程可以访问这个方法,从而避免了并发问题
但是请注意,这可能会降低性能,因为它阻止了多个线程同时执行
另外,如果你在处理用户输入或外部数据源,并且需要确保线程安全,你可能需要使用更复杂的同步机制,如锁或信号量
在这种情况下,你需要仔细考虑如何正确地使用这些工具,以避免死锁或其他并发问题
02.用ThreadLocal来解决
a.说明
ThreadLocal确实可以用来解决线程安全问题,尤其是在处理线程局部变量时
ThreadLocal为每个线程提供了一组独立的变量副本,每个线程都可以独立地访问和使用这些变量,从而避免了线程之间的数据竞争和冲突
如果你需要在SimpleDateFormat中解决线程安全问题,你可以考虑使用ThreadLocal来存储和操作日期格式化对象
这样,每个线程都可以拥有自己的SimpleDateFormat实例,从而避免了多个线程同时访问和修改同一个实例所带来的问题
b.使用ThreadLocal来解决线程安全问题
public class ThreadSafeDateFormat {
private static final ThreadLocal<SimpleDateFormat> formatter =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public synchronized String formatDate(Date date) {
SimpleDateFormat formatter = ThreadLocal.get();
return formatter.format(date);
}
}
c.说明
ThreadSafeDateFormat类使用ThreadLocal来存储SimpleDateFormat实例
在formatDate方法中,我们通过ThreadLocal.get()获取当前线程的SimpleDateFormat实例,并使用它来格式化日期
由于每个线程都有自己的SimpleDateFormat实例,因此它们可以独立地访问和使用这些实例,从而避免了线程安全问题
8.6 [3]日期必须y表示年,而不能用Y
00.汇总
使用 y 来表示常规年份,避免日期格式化错误
避免使用 Y 来表示年份,除非确实需要按照 ISO 周年的格式来解析和显示年份
01.为什么要求日期格式化时必须使用 y 表示年,而不能用 Y?
a.y表示日历年(Calendar Year)
a.说明
y 是标准的表示年份的字符,表示的是通常意义上的公历年,比如 2024 表示的就是这一年的年份
使用 y 时,日期格式化工具会准确地格式化出对应的年份数值:
b.代码
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(sdf.format(new Date())); // 输出: 2024-11-10
b.Y表示星期年(Week Year)
a.说明
Y 表示的是“星期年”或称“ISO周年”(ISO week-numbering year),它是一种基于 ISO 周数的年份表示方式
这种表示法根据每年的第一个星期一所在的周来计算年份,如果某天属于新一年的第一个完整星期,则会归为新年的星期年
例如,如果某年的最后几天在下一年开始的第一个星期中,它们可能会被归入下一年的 week year
同理,如果新年的前几天在上一年的最后一个完整星期内,这些天的星期年可能会归属上一年
这在日期和时间处理中可能导致意外的年份差异
b.代码
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println(sdf.format(new Date())); // 可能输出与实际年份不同的值
02.使用Y的潜在问题
使用 Y 表示年份会引发一些日期计算的错误,因为它依赖于周数的计算方式,不是每次都与实际的公历年份一致
例如:2024年12月31日会被视作 2025 的 week year,导致使用 YYYY 格式化时得到 2025-12-31
在跨年计算或特定日期逻辑中使用 Y 表示年份可能会出现错误,因为 week year 与通常理解的日历年并不总是相符
03.什么时候使用 Y
Y 一般仅用于需要符合 ISO 8601 标准的日期格式
特别是包含 ISO 周数(如“2024-W01-1”表示2024年的第一个星期一)的情况
而在一般情况下,我们都应使用 y 来表示日历年份
9 提问
9.1 [1]一秒告别!=null判空
00.总结
明确数据类型:确定需要判断的数据类型,如String、Object、List、Array或Map
选择合适的工具类:根据数据类型选择相应的工具类,如StringUtils、ObjectUtils或CollectionUtils
使用工具类进行判断:调用工具类中的方法进行空值判断,避免手动编写重复的!= null代码
01.第一步:明确数据类型
在进行空指针判断之前,首先要明确需要判断的数据类型。常见的数据类型包括:
String字符串
Object/自定义对象
List集合
Array数组
Map集合
不同的数据类型对应不同的工具类,合理选择工具类可以大大简化判断逻辑。
02.选择合适的工具类
a.String类型:使用StringUtils工具类
对于字符串类型的判断,推荐使用StringUtils工具类。它不仅能够判断字符串是否为null,还能判断字符串是否为空字符串。
String str = "";
boolean isEmpty = StringUtils.isEmpty(str); // true
-----------------------------------------------------------------------------------------------------
StringUtils.isEmpty()方法的源码如下:
public static boolean isEmpty(@Nullable Object str) {
return str == null || "".equals(str);
}
b.Object类型:使用ObjectUtils工具类
对于普通的对象类型,可以使用ObjectUtils工具类进行判断。
Object obj = null;
boolean isEmpty = ObjectUtils.isEmpty(obj); // true
-----------------------------------------------------------------------------------------------------
ObjectUtils.isEmpty()方法不仅可以判断对象是否为null,还可以判断集合、数组等复杂类型是否为空。
c.Map类型:使用ObjectUtils或CollectionUtils工具类
对于Map类型的判断,可以使用ObjectUtils或CollectionUtils工具类。
Map<String, Object> map = Collections.emptyMap();
boolean isEmpty = ObjectUtils.isEmpty(map); // true
-----------------------------------------------------------------------------------------------------
CollectionUtils.isEmpty()方法的源码如下:
public static boolean isEmpty(@Nullable Map<?, ?> map) {
return map == null || map.isEmpty();
}
d.List类型:使用ObjectUtils或CollectionUtils工具类
对于List类型的判断,同样可以使用ObjectUtils或CollectionUtils工具类。
List<Integer> list = Collections.EMPTY_LIST;
boolean isEmpty = ObjectUtils.isEmpty(list); // true
-----------------------------------------------------------------------------------------------------
CollectionUtils.isEmpty()方法的源码如下:
public static boolean isEmpty(@Nullable Collection<?> collection) {
return collection == null || collection.isEmpty();
}
e.数组类型:使用ObjectUtils工具类
对于数组类型的判断,ObjectUtils工具类同样适用。
Object[] objArr = null;
boolean isEmpty = ObjectUtils.isEmpty(objArr); // true
03.第三步:深入理解工具类的实现原理
a.以ObjectUtils.isEmpty()方法为例,它的源码如下:
public static boolean isEmpty(@Nullable Object obj) {
if (obj == null) {
return true;
}
if (obj instanceof Optional) {
return !((Optional) obj).isPresent();
}
if (obj instanceof CharSequence) {
return ((CharSequence) obj).length() == 0;
}
if (obj.getClass().isArray()) {
return Array.getLength(obj) == 0;
}
if (obj instanceof Collection) {
return ((Collection) obj).isEmpty();
}
if (obj instanceof Map) {
return ((Map) obj).isEmpty();
}
return false;
}
b.总结
从源码中可以看出,ObjectUtils.isEmpty()方法通过判断对象的类型,分别对Optional、CharSequence、数组、集合和Map等类型进行了空值判断。
这种设计使得该方法能够适用于多种数据类型。
04.第四步:处理复杂场景
a.List集合中元素为空的判断
如果需要对List集合中的每个元素进行空值判断,可以使用Arrays工具类进行遍历。
List<Integer> list = Collections.singletonList(null);
boolean allEmpty = Arrays.stream(list.toArray()).allMatch(ObjectUtils::isEmpty);
b.Map集合中元素为空的判断
对于Map集合,可以使用CollectionUtils工具类进行判断。
Map<String, Object> map = Collections.emptyMap();
boolean isEmpty = CollectionUtils.isEmpty(map);
9.2 [1]三目运算符建议类型对齐
00.小结
保持类型一致性,确保 true 和 false 分支的类型相同,避免意外的类型提升
小心自动装箱和拆箱,避免 null 参与三目运算符计算
在返回不同类型时选择合适的公共类型,如使用 Object 或显式转换
01.三目运算符会自动进行类型提升
a.说明
三目运算符的返回值类型是根据 true 和 false 分支的类型推断出来的
为了得到一致的结果,Java 会自动将不同的类型提升为更高精度的类型
例如,若一个分支返回 int 而另一个分支返回 double,Java 会将 int 提升为 double
b.示例
int x = 5;
double y = 10.5;
double result = (x > 0) ? x : y; // 返回 double 类型
System.out.println(result); // 输出 5.0
c.说明
这里返回值 5 被提升为 5.0
虽然代码在这个例子中不会出错,但在某些情况下,这种自动提升会导致意外的精度损失或类型不匹配的问题
02.自动拆箱和装箱可能引发 NullPointerException
a.说明
在 Java 中,基本类型和包装类型的对齐需要特别小心
三目运算符会尝试将包装类型和基本类型对齐成相同类型,这会导致自动装箱和拆箱
如果某个分支为 null 且需要拆箱,可能会引发 NullPointerException
b.示例
Integer a = null;
int b = 10;
int result = (a != null) ? a : b; // 如果 a 为 null,结果会发生自动拆箱,引发 NullPointerException
c.说明
由于 a 为 null,Java 会尝试将其拆箱为 int,从而抛出 NullPointerException
为避免这种情况,可以确保类型对齐,或避免对可能为 null 的对象进行拆箱
03.返回值类型不一致可能导致编译错误
a.说明
如果三目运算符的两种返回类型无法被编译器自动转换为一个兼容类型,代码会直接报错
b.示例
int x = 5;
String y = "10";
Object result = (x > 0) ? x : y; // 编译错误:int 和 String 不兼容
c.说明
在这种情况下,int 和 String 无法被提升到相同类型,因此会引发编译错误
若确实希望返回不同类型的值,可以手动指定共同的超类型,例如将结果定义为 Object 类型
Object result = (x > 0) ? Integer.valueOf(x) : y; // 这里 result 为 Object
04.类型对齐可以提升代码的可读性
a.说明
保持三目运算符返回的类型一致,能让代码更加清晰,便于理解和维护
类型对齐可以避免类型转换和自动提升带来的混乱,使代码更容易预测和理解
b.代码
double result = (condition) ? 1.0 : 0.0; // 返回 double
9.3 [1]禁止使用isSuccess作为变量名
00.小结
isSuccess 这样的命名不清晰,容易与布尔类型的变量产生混淆,进而影响代码的可读性
命名应尽量明确,避免使用容易引起歧义的名称,特别是在布尔值类型的命名时
建议使用更具描述性的名称,如 isSuccessful 或 wasSuccessful,更清晰地表达变量的意义
01.为什么禁止开发人员使用 isSuccess 作为变量名?
a.不符合布尔值命名约定
在 Java 中,通常使用 is 或 has 开头的变量名来表示布尔值(boolean 类型)。这类命名通常遵循特定的语义约定,表示某个条件是否成立。例如:
isEnabled 表示某个功能是否启用;
hasPermission 表示是否有权限。
问题:
isSuccess 看起来像一个布尔值(boolean 类型),但它实际上可能并不直接表示一个布尔值,而是一个状态或结果。这种命名可能会导致混淆,开发者可能误以为它是布尔类型的变量,而实际上它可能是一个描述状态的对象、字符串或者其他类型的数据。
b.语义不明确
isSuccess 这个名字表面上表示“是否成功”,但是它缺少具体的上下文,导致语义不够明确。真正表示是否成功的布尔值应该直接使用 boolean 类型的变量,并且使用清晰明确的命名。
例如:
isCompleted:表示某个任务是否完成。
isSuccessful:表示某个操作是否成功。
这些命名能更明确地表达布尔变量的含义,避免理解上的歧义。
c.与标准的 is 前缀混淆
is 前缀通常用来表示“是否”某个条件成立,适用于返回布尔值的方法或者变量。isSuccess 这样的命名会让开发人员误以为它是一个布尔值,或者一个 boolean 类型的值,但实际上它可能是一个复杂类型或者其他非布尔类型,造成不必要的混淆。
例如:
boolean isSuccess = someMethod(); // 看起来是布尔值,但实际类型可能不同
这种情况可能导致开发人员产生误解,认为 isSuccess 代表的是布尔值,但它可能是某个表示成功的对象、枚举或者其他数据类型。
d.更好的命名建议
为了避免歧义和混淆,开发人员应使用更加明确且符合命名规范的名称。以下是一些命名的改进建议:
如果是布尔值,命名为 isSuccessful 或 wasSuccessful。
如果是表示结果的对象,使用更具体的名称,例如 operationResult 或 statusCode,以表明它是一个描述操作结果的变量。
e.提升代码的可读性和可维护性
清晰且具有意义的命名能够帮助团队成员或未来的开发者更快地理解代码的意图。如果变量名过于模糊(如 isSuccess),就可能让人对其实际含义产生疑问,尤其是在阅读较大或复杂的代码时。良好的命名能够提升代码的可读性和可维护性。
9.4 [1]禁止修改serialVersionUID字段的值
00.汇总
禁止开发人员修改 serialVersionUID 字段的值,主要是为了:
确保序列化与反序列化的兼容性,避免版本不匹配导致反序列化失败
避免不必要的版本冲突和数据丢失,特别是在类结构修改时
保持 Java 自动管理 serialVersionUID 的优势,保证类的版本一致性和可维护性
如果确实需要修改 serialVersionUID,应确保修改后的版本与已经序列化的数据兼容,并遵循合理的版本管理策略
01.为什么禁止开发人员修改 serialVersionUID 字段的值?
a.序列化与反序列化兼容性
serialVersionUID 的主要作用是保证在序列化和反序列化过程中,类的版本兼容性。它是用来标识类的版本的,如果序列化和反序列化过程中使用的类的 serialVersionUID 不匹配,就会抛出 InvalidClassException。
- 不匹配的 serialVersionUID 会导致序列化的数据与当前类不兼容,导致反序列化失败。
- 修改 serialVersionUID 的值会改变类的版本标识,导致已序列化的数据在反序列化时不能成功读取,特别是在类结构发生改变(例如添加或删除字段)时。
-----------------------------------------------------------------------------------------------------
// 类的第一次版本
public class MyClass implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
// 其他字段和方法
}
// 类的第二次修改版本
public class MyClass implements Serializable {
private static final long serialVersionUID = 2L; // 修改了 serialVersionUID
private String name;
private int age; // 新增字段
// 其他字段和方法
}
-----------------------------------------------------------------------------------------------------
如果修改了 serialVersionUID,而之前序列化的数据是使用版本 1 的类进行序列化的,反序列化时会因为 serialVersionUID 不匹配而导致失败。
b.避免不必要的版本冲突
Java 会根据类的字段、方法等信息自动生成 serialVersionUID,这个值是基于类的结构计算出来的。如果开发人员修改了 serialVersionUID,可能会破坏 Java 自动生成的版本控制机制,从而导致版本控制不一致,增加了维护复杂性。
如果手动修改 serialVersionUID,容易出现以下几种问题:
由于类结构没有变化,修改 serialVersionUID 可能会导致已序列化的数据无法恢复。
如果不同的开发人员修改了 serialVersionUID,可能会在不同的机器或系统间引起序列化不一致。
c.影响序列化兼容性
Java 提供了两种主要的兼容性规则:
兼容性向前:如果类的字段或方法发生改变,但没有改变 serialVersionUID,则反序列化是可以工作的。
兼容性向后:如果你修改了类的结构(如字段变动、方法签名改变等),并且保持相同的 serialVersionUID,反序列化仍然可以工作。
-----------------------------------------------------------------------------------------------------
如果不小心修改了 serialVersionUID,可能导致以下情况:
向前兼容性:新版本的类不能兼容老版本的对象,导致反序列化失败。
向后兼容性:老版本的类无法反序列化新版本的对象。
d.自动生成 vs 手动指定
自动生成的 serialVersionUID:Java 会根据类的结构自动生成 serialVersionUID,这样如果类的结构发生变化,serialVersionUID 会自动变化,确保不兼容的版本之间不会出现意外的反序列化行为。
手动指定 serialVersionUID:手动修改 serialVersionUID 可能导致版本控制不一致,特别是在多人开发、分布式部署的环境中,容易出现反序列化失败的问题。
e.避免非预期的反序列化问题
手动修改 serialVersionUID 可能会导致数据丢失或反序列化时抛出异常。例如,如果开发人员错误地修改了 serialVersionUID,系统在尝试反序列化时可能会因为 serialVersionUID 不匹配而无法成功加载对象,导致异常的发生。
9.5 [1]阿里规约用静态工厂方法代替构造器
00.总结
使用静态工厂方法代替构造器是一种常见的设计模式,提供了比传统构造器更多的灵活性
静态工厂方法允许命名,以提高代码的可读性和表达力,同时可以返回不同子类型的实例,而不仅限于返回类本身的实例。这种方法还支持缓存和对象复用,避免每次都创建新对象,从而提高性能
随着Java 8和Java 9对接口功能的扩展,静态工厂方法还可以被集成到接口中,通过静态方法直接提供对象创建逻辑,结合私有静态方法,进一步优化代码的封装性和维护性
01.构造函数的问题
a.方法名都是一个,容易混淆
a.问题描述
当类有多个构造器时,方法名相同容易混淆,尤其是参数数量和类型相同但顺序不同的情况下。
b.示例代码
public class Person {
private String name;
private int age;
private boolean isEmployed;
public Person(String name, int age) {
this.name = name;
this.age = age;
this.isEmployed = false; // 默认值
}
public Person(String name, boolean isEmployed) {
this.name = name;
this.age = 0; // 默认值
this.isEmployed = isEmployed;
}
public Person(int age, String name) {
this.name = name;
this.age = age;
this.isEmployed = false; // 默认值
}
}
c.问题分析
参数相同,顺序不同,容易混淆。
参数顺序不同的构造器可能导致调用时传入错误顺序的参数,编译器不会报错,但会导致运行时逻辑错误。
b.扩展性问题
a.问题描述
通过改变参数顺序和数量来实现重载,可能导致构造器组合方式爆炸,难以维护。
b.问题分析
扩展类并添加更多构造器时,可能导致代码复杂化,难以维护。
c.构造函数每次被调用都要创建一个新对象
a.问题描述
每次使用new关键字调用构造函数时,都会创建一个新对象实例。
b.问题分析
创建新对象涉及分配内存和初始化对象,性能上有一定开销。
构造函数的每次调用都创建新对象,使对象的缓存和复用变得困难。
02.静态工厂方法如何解决构造器的问题
a.方法名都是一个,容易混淆
a.解决方案
静态工厂方法可以通过有意义的命名来避免混淆,见名知意。
b.改进后的代码
public class Person {
private String name;
private int age;
private boolean isEmployed;
private Person(String name, int age, boolean isEmployed) {
this.name = name;
this.age = age;
this.isEmployed = isEmployed;
}
public static Person createChild(String name) {
return new Person(name, 0, false); // 默认年龄为0,未就业
}
public static Person createEmployedAdult(String name, int age) {
return new Person(name, age, true); // 成人,已就业
}
public static Person createUnemployedAdult(String name, int age) {
return new Person(name, age, false); // 成人,未就业
}
}
b.扩展性问题
a.解决方案
静态工厂方法可以通过添加新的方法来扩展对象创建的方式,提高代码的扩展性。
b.示例代码
public static Person createLayoffPerson(String name) {
return new Person(name, 35, true); // 默认35岁,在职
}
c.构造函数每次被调用都要创建一个新对象
a.解决方案
静态工厂方法可以通过缓存对象、实现单例模式或其他优化策略,避免每次都创建新对象。
b.示例代码
public class Person {
private String name;
private int age;
private boolean isEmployed;
private static final Person DEFAULT_CHILD_INSTANCE = new Person("Default Child", 0, false);
private Person(String name, int age, boolean isEmployed) {
this.name = name;
this.age = age;
this.isEmployed = isEmployed;
}
public static Person getDefaultChildInstance() {
return DEFAULT_CHILD_INSTANCE;
}
}
03.实例受控
a.定义
通过静态工厂方法和实例缓存机制,在重复调用时返回同一个对象,实现实例受控。
b.示例代码
import java.util.HashMap;
import java.util.Map;
public class Person {
private String name;
private int age;
private boolean isEmployed;
private static final Map<String, Person> instances = new HashMap<>();
private Person(String name, int age, boolean isEmployed) {
this.name = name;
this.age = age;
this.isEmployed = isEmployed;
}
public static Person createChild(String name) {
String key = name + ":child";
if (!instances.containsKey(key)) {
instances.put(key, new Person(name, 0, false));
}
return instances.get(key);
}
public static Person createEmployedAdult(String name, int age) {
String key = name + ":employed:" + age;
if (!instances.containsKey(key)) {
instances.put(key, new Person(name, age, true));
}
return instances.get(key);
}
public static Person createUnemployedAdult(String name, int age) {
String key = name + ":unemployed:" + age;
if (!instances.containsKey(key)) {
instances.put(key, new Person(name, age, false));
}
return instances.get(key);
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", isEmployed=" + isEmployed + "}";
}
}
04.静态工厂方法可以返回任何子类型的对象
a.定义
静态工厂方法可以返回Person类的任意子类型对象,而不仅限于返回Person本身。
b.优势
子类的具体实现对外部是透明的,增强了封装性。
提供了更好的代码管理和扩展性。
05.Java 8中允许接口包含静态方法
a.接口与静态方法
a.变革
提供了接口设计更大的灵活性和功能性。
b.示例代码
public interface Person {
String getName();
int getAge();
static Person create(String name, int age) {
return new PersonImpl(name, age); // 返回接口的实现类实例
}
}
class PersonImpl implements Person {
private final String name;
private final int age;
private PersonImpl(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String getName() {
return name;
}
@Override
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
Person person = Person.create("哪吒", 30);
System.out.println(person.getName() + " is " + person.getAge() + " years old.");
}
}
b.与默认方法结合使用
a.定义
Java 8引入了默认方法,允许接口在提供方法签名的同时也提供默认实现。
b.优势
接口可以拥有类似于抽象类的功能,而无需引入多继承的复杂性。
c.支持函数式接口
a.定义
Java 8引入了函数式接口的概念,特别是在Lambda表达式和方法引用中被广泛使用。
b.示例
在Comparator接口中,静态方法comparing是一个静态工厂方法,用于创建比较器。
06.Java9中支持私有的静态方法,但静态字段必须是公有的
a.私有静态方法
a.目的
增强代码的封装性和重用性。
b.示例代码
public interface Person {
String getName();
int getAge();
static Person createChild(String name) {
validateName(name);
return new Child(name);
}
static Person createEmployedAdult(String name, int age) {
validateName(name);
validateAge(age);
return new EmployedAdult(name, age);
}
private static void validateName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
}
private static void validateAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("Age must be at least 18");
}
}
}
b.静态字段必须是公有的
a.原因
确保接口的实现类和使用者可以一致地访问这些字段,从而保持接口的简洁性和实用性。
9.6 [2]Date类都不建议使用了
01.有什么问题吗java.util.Date?
java.util.Date(Date从现在开始)是一个糟糕的类型,这解释了为什么它的大部分内容在 Java 1.1 中被弃用(但不幸的是仍在使用)。
它的名称具有误导性:它并不代表 a Date,而是代表时间的一个瞬间。所以它应该被称为Instant——正如它的java.time等价物一样。
它是非最终的:这鼓励了对继承的不良使用,例如java.sql.Date(这意味着代表一个日期,并且由于具有相同的短名称而也令人困惑)
它是可变的:日期/时间类型是自然值,可以通过不可变类型有效地建模。可变的事实Date(例如通过setTime方法)意味着勤奋的开发人员最终会在各处创建防御性副本。
它在许多地方(包括)隐式使用系统本地时区,toString()这让许多开发人员感到困惑。有关此内容的更多信息,请参阅“什么是即时”部分
它的月份编号是从 0 开始的,是从 C 语言复制的。这导致了很多很多相差一的错误。
它的年份编号是基于 1900 年的,也是从 C 语言复制的。当然,当 Java 出现时,我们已经意识到这不利于可读性?
它的方法命名不明确:getDate()返回月份中的某一天,并getDay()返回星期几。给这些更具描述性的名字有多难?
对于是否支持闰秒含糊其辞:“秒由 0 到 61 之间的整数表示;值 60 和 61 仅在闰秒时出现,即使如此,也仅在实际正确跟踪闰秒的 Java 实现中出现。” 我强烈怀疑大多数开发人员(包括我自己)都做了很多假设,认为 for 的范围getSeconds()实际上在 0-59 范围内(含)。
它的宽容没有明显的理由:“在所有情况下,为这些目的而对方法给出的论据不必落在指定的范围内; 例如,日期可以指定为 1 月 32 日,并被解释为 2 月 1 日。” 多久有用一次?
02.为啥要改?
我们要改的原因很简单,我们的代码缺陷扫描规则认为这是一个必须修改的缺陷,否则不给发布,不改不行,服了。
避免使用java.util.Date与java.sql.Date类和其提供的API,考虑使用java.time.Instant类或java.time.LocalDateTime类及其提供的API替代。
03.怎么改?
a.耐心比对数据库日期字段和DO的映射
a.确定字段类型
如果字段代表日期和时间,则可能需要使用 LocalDateTime。
如果字段仅代表日期,则可能需要使用 LocalDate。
如果字段仅代表时间,则可能需要使用 LocalTime。
如果字段需要保存时间戳(带时区的),则可能需要使用 Instant 或 ZonedDateTime。
b.更新数据对象类
更新数据对象类中的字段,把 Date 类型改为适当的 java.time 类型。
b.将DateUtil中的方法改造
a.替换原来的new Date()和Calendar.getInstance().getTime()
原来的方式:
Date nowDate = new Date();
Date nowCalendarDate = Calendar.getInstance().getTime();
-------------------------------------------------------------------------------------------------
使用 java.time 改造后:
// 使用Instant代表一个时间点,这与Date类似
Instant nowInstant = Instant.now();
// 如果需要用到具体的日期和时间(例如年、月、日、时、分、秒)
LocalDateTime nowLocalDateTime = LocalDateTime.now();
// 如果你需要和特定的时区交互,可以使用ZonedDateTime
ZonedDateTime nowZonedDateTime = ZonedDateTime.now();
// 如果你需要转换回java.util.Date,你可以这样做(假设你的代码其他部分还需要使用Date)
Date nowFromDateInstant = Date.from(nowInstant);
// 如果需要与java.sql.Timestamp交互
java.sql.Timestamp nowFromInstant = java.sql.Timestamp.from(nowInstant);
-------------------------------------------------------------------------------------------------
一些注意点:
Instant 表示的是一个时间点,它是时区无关的,相当于旧的 Date 类。它通常用于表示时间戳。
LocalDateTime 表示没有时区信息的日期和时间,它不能直接转换为时间戳,除非你将其与时区结合使用(例如通过 ZonedDateTime)。
ZonedDateTime 包含时区信息的日期和时间,它更类似于 Calendar,因为 Calendar 也包含时区信息。
当你需要将 java.time 对象转换回 java.util.Date 对象时,可以使用 Date.from(Instant) 方法。这在你的代码需要与旧的API或库交互时非常有用。
b.一些基础的方法改造
a.dateFormat
原来的方式
public static String dateFormat(Date date, String dateFormat) {
SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
return formatter.format(date);
}
-------------------------------------------------------------------------------------------------
使用java.time改造后
public static String dateFormat(LocalDateTime date, String dateFormat) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
return date.format(formatter);
}
b.addSecond、addMinute、addHour、addDay、addMonth、addYear
原来的方式
public static Date addSecond(Date date, int second) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(13, second);
return calendar.getTime();
}
public static Date addMinute(Date date, int minute) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(12, minute);
return calendar.getTime();
}
public static Date addHour(Date date, int hour) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(10, hour);
return calendar.getTime();
}
public static Date addDay(Date date, int day) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(5, day);
return calendar.getTime();
}
public static Date addMonth(Date date, int month) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(2, month);
return calendar.getTime();
}
public static Date addYear(Date date, int year) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(1, year);
return calendar.getTime();
}
-------------------------------------------------------------------------------------------------
使用java.time改造后
public static LocalDateTime addSecond(LocalDateTime date, int second) {
return date.plusSeconds(second);
}
public static LocalDateTime addMinute(LocalDateTime date, int minute) {
return date.plusMinutes(minute);
}
public static LocalDateTime addHour(LocalDateTime date, int hour) {
return date.plusHours(hour);
}
public static LocalDateTime addDay(LocalDateTime date, int day) {
return date.plusDays(day);
}
public static LocalDateTime addMonth(LocalDateTime date, int month) {
return date.plusMonths(month);
}
public static LocalDateTime addYear(LocalDateTime date, int year) {
return date.plusYears(year);
}
c.dateToWeek
原来的方式
public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
public static String dateToWeek(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return WEEK_DAY_OF_CHINESE[cal.get(7) - 1];
}
-------------------------------------------------------------------------------------------------
使用java.time改造后
public static final String[] WEEK_DAY_OF_CHINESE = new String[]{"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
public static String dateToWeek(LocalDate date) {
DayOfWeek dayOfWeek = date.getDayOfWeek();
return WEEK_DAY_OF_CHINESE[dayOfWeek.getValue() % 7];
}
d.getStartOfDay和getEndOfDay
原来的方式
public static Date getStartTimeOfDay(Date date) {
if (date == null) {
return null;
} else {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime startOfDay = localDateTime.with(LocalTime.MIN);
return Date.from(startOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
}
public static Date getEndTimeOfDay(Date date) {
if (date == null) {
return null;
} else {
LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), ZoneId.systemDefault());
LocalDateTime endOfDay = localDateTime.with(LocalTime.MAX);
return Date.from(endOfDay.atZone(ZoneId.systemDefault()).toInstant());
}
}
-------------------------------------------------------------------------------------------------
使用java.time改造后
public static LocalDateTime getStartTimeOfDay(LocalDateTime date) {
if (date == null) {
return null;
} else {
// 获取一天的开始时间,即00:00
return date.toLocalDate().atStartOfDay();
}
}
public static LocalDateTime getEndTimeOfDay(LocalDateTime date) {
if (date == null) {
return null;
} else {
// 获取一天的结束时间,即23:59:59.999999999
return date.toLocalDate().atTime(LocalTime.MAX);
}
}
e.betweenStartAndEnd
原来的方式
public static Boolean betweenStartAndEnd(Date nowTime, Date beginTime, Date endTime) {
Calendar date = Calendar.getInstance();
date.setTime(nowTime);
Calendar begin = Calendar.getInstance();
begin.setTime(beginTime);
Calendar end = Calendar.getInstance();
end.setTime(endTime);
return date.after(begin) && date.before(end);
}
-------------------------------------------------------------------------------------------------
使用java.time改造后
public static Boolean betweenStartAndEnd(Instant nowTime, Instant beginTime, Instant endTime) {
return nowTime.isAfter(beginTime) && nowTime.isBefore(endTime);
}
9.7 [2]为什么建议谨慎使用继承
00.小结
尽管继承是面向对象编程中的一个重要特性,但滥用继承可能带来许多问题,特别是在以下几个方面:
增加类之间的耦合,降低灵活性
破坏封装性,暴露不应访问的内部实现
可能导致类层次结构复杂,增加理解和维护的难度
限制代码的重用和扩展性
00.使用接口和组合更优
相比继承,接口(Interface) 和 组合(Composition) 更符合面向对象设计的原则
接口允许类只暴露所需的功能,而不暴露实现细节,组合则允许你将多个不同的行为组合在一起
使得系统更加灵活和可扩展。通过接口和组合,可以避免继承的许多问题
推荐设计模式:
策略模式(Strategy Pattern):通过接口和组合来替代继承
装饰器模式(Decorator Pattern):使用组合和代理来扩展行为,而非通过继承
01.为什么建议开发者谨慎使用继承?
a.增加了类之间的耦合性
继承会导致子类和父类之间形成紧密的耦合关系。子类依赖于父类的实现
这意味着如果父类发生变化,可能会影响到所有继承自该父类的子类,导致修改和维护变得更加困难
这种紧密耦合关系也限制了子类的灵活性,因为它必须遵循父类的接口和实现
-----------------------------------------------------------------------------------------------------
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog is eating");
}
}
-----------------------------------------------------------------------------------------------------
如果父类 Animal 做了改动(如修改 eat() 方法的实现),Dog 类也会受到影响。这样的耦合会增加后期维护的复杂度
b.破坏了封装性(Encapsulation)
继承可能破坏封装性,因为子类可以直接访问父类的成员(字段和方法)
尤其是当父类成员被设置为 protected 或 public 时
这种情况可能导致子类暴露不应被外界访问的细节,破坏了数据的封装性
-----------------------------------------------------------------------------------------------------
class Vehicle {
protected int speed;
}
class Car extends Vehicle {
void accelerate() {
speed += 10; // 直接访问父类的 protected 字段
}
}
-----------------------------------------------------------------------------------------------------
在这种情况下,Car 类直接访问了父类 Vehicle 的 speed 字段,而不是通过公共接口来修改它,导致封装性降低
c.继承可能会导致类的层次结构不合理
继承往往会导致不合理的类层次结构,特别是在试图通过继承来表达“是一个”(is-a)关系时
实际情况可能并不符合这种逻辑。滥用继承可能会使类之间的关系变得复杂和不直观,导致代码结构混乱
-----------------------------------------------------------------------------------------------------
假设我们有一个 Car 类和一个 Truck 类,都继承自 Vehicle 类
如果 Car 和 Truck 共享很多方法和属性,这样的设计可能是合适的
但是,如果 Car 和 Truck 之间差异很大,仅通过继承来构建它们的关系,可能会导致继承层次过于复杂,代码阅读和理解变得困难
d.继承可能导致不易发现的错误
由于子类继承了父类的行为,任何对父类的修改都有可能影响到子类的行为
更糟糕的是,错误或不一致的修改可能在父类中发生,而这些错误可能不会立即暴露出来
直到程序运行到某个特定的地方,才会显现出错误
-----------------------------------------------------------------------------------------------------
假设你修改了父类的某个方法,但忘记更新或调整子类中相应的重写方法,这可能会导致难以发现的错误
e.继承限制了灵活性(不可重用性问题)
继承创建了一个父类与子类之间的固定关系,这意味着如果你想在一个完全不同的上下文中重用一个类
你可能不能通过继承来实现。在某些情况下,组合比继承更为灵活,允许你将多个行为组合到一个类中
而不是通过继承来强行构建类的层次结构
-----------------------------------------------------------------------------------------------------
// 组合而非继承
class Engine {
void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine = new Engine(); // 通过组合来使用 Engine
void start() {
engine.start();
}
}
-----------------------------------------------------------------------------------------------------
通过组合,可以灵活地使用不同的组件,而不需要继承整个类。这样做的优点是更具扩展性和灵活性
f.继承限制了方法的重用(可维护性差)
如果你过度依赖继承,你的代码会容易受到父类实现的限制,难以灵活地添加新功能或进行扩展
例如,在继承链中添加新的功能可能会导致一大堆方法的修改和重写
而不通过继承,可以更轻松地将功能作为独立模块来重用
9.8 [2]禁止使用Executors创建线程池?
00.总结
使用 Executors 创建线程池会带来不易察觉的风险,可能导致系统资源耗尽或任务堆积
手动配置 ThreadPoolExecutor 可以更好地控制线程池的行为,使其符合实际业务需求和资源限制
因此,为了系统的健壮性和可控性,建议避免使用 Executors 快捷方法来创建线程池
01.为什么禁止使用 Executors 创建线程池?
a.不透明的任务队列长度导致 OOM 风险
newFixedThreadPool() 和 newSingleThreadExecutor() 使用的是无界队列 LinkedBlockingQueue
无界队列可以存放无限数量的任务,一旦任务量非常大,队列会迅速占用大量内存,导致 OutOfMemoryError(OOM)
newCachedThreadPool() 使用的是 SynchronousQueue,该队列没有存储任务的能力
每个任务到来时必须立即有一个空闲线程来处理任务,否则将创建一个新线程
当任务到达速度超过线程销毁速度时,线程数量会快速增加,导致 OOM
b.线程数无法控制,导致资源耗尽
在 newCachedThreadPool() 创建的线程池中,线程数没有上限
短时间内大量请求会导致线程数暴增,耗尽系统资源
newFixedThreadPool() 和 newSingleThreadExecutor() 虽然限制了核心线程数
但未限制任务队列长度,依然可能耗尽内存
在业务需求不确定或任务激增的场景下,建议明确限制线程池的最大线程数和队列长度
以更好地控制系统资源的使用,避免因线程数无法控制导致的性能问题
c.缺乏合理的拒绝策略控制
Executors 创建的线程池默认使用 AbortPolicy 拒绝策略
即当线程池达到饱和时会抛出 RejectedExecutionException 异常
不同的业务场景可能需要不同的拒绝策略,例如可以使用 CallerRunsPolicy(让提交任务的线程执行任务)
或 DiscardOldestPolicy(丢弃最旧的任务)来平衡任务处理
手动创建 ThreadPoolExecutor 时,可以指定适合业务需求的拒绝策略
从而更灵活地处理线程池满载的情况,避免异常或系统性能下降
d.灵活配置核心参数
使用 ThreadPoolExecutor 的构造方法可以手动设置以下参数,以便根据业务需求灵活配置线程池:
corePoolSize:核心线程数,避免空闲线程被频繁销毁和重建
maximumPoolSize:最大线程数,控制线程池能使用的最大资源
keepAliveTime:非核心线程的存活时间,适合控制线程销毁频率
workQueue:任务队列类型和长度,便于管理任务积压的情况
这些参数的合理配置可以有效平衡线程池的性能、资源占用和任务处理能力,避免使用默认配置时不符合需求的情况
e.推荐的线程池创建方式
建议直接使用 ThreadPoolExecutor 构造方法配置线程池,例如
int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 60L;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
workQueue,
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
9.9 [2]禁止使用日志系统Log4j的API
00.小结
禁止工程师直接使用日志系统(如 Log4j、Logback)中的 API,主要是为了:
解耦日志实现与业务逻辑:通过使用日志抽象层(如 SLF4J),可以更轻松地切换日志框架,避免硬编码
提高灵活性与可维护性:避免在应用中重复使用框架 API,提高日志配置的灵活性和一致性
规范日志记录行为:通过封装日志记录,确保日志级别、格式和内容的统一,增强可读性和可追踪性
优化性能:通过配置日志框架的高级功能(如异步日志),提高日志系统的性能,减少对应用的影响
统一日志管理:避免团队成员在不同模块中使用不一致的日志记录方式,确保日志输出的标准化
最好的做法是通过日志抽象层(如 SLF4J)进行日志记录,同时通过日志管理工具类进行统一的配置和调用,确保日志的高效、规范和灵活性
01.为什么禁止工程师直接使用日志系统 (Log4j、Logback) 中的 API?
a.日志配置与实现的分离
直接使用日志系统的 API 可能会导致日志记录逻辑与应用的业务逻辑紧密耦合,使得日志配置和实现的分离变得困难
现代的日志框架(如 Log4j、Logback)允许通过外部配置文件(如 log4j.xml 或 logback.xml)灵活配置日志级别、输出格式、输出位置等
而不是硬编码到应用代码中。直接使用日志 API 会导致日志的配置与业务代码绑定在一起,不易修改和维护
建议的做法:通过使用日志框架的日志抽象接口(如 org.slf4j.Logger)来记录日志,而不是直接依赖具体的日志实现
这种方式提供了更大的灵活性,日志实现可以在运行时通过配置文件更换而无需修改代码
b.灵活性与可扩展性问题
如果工程师直接使用日志库的 API,项目在需要切换日志框架(比如从 Log4j 转换到 Logback 或其他框架)时
需要修改大量的代码,增加了系统的耦合度和维护难度
另一方面,使用日志抽象层(如 SLF4J)可以避免这一问题,因为 SLF4J 是一个日志抽象层
底层可以切换具体的日志实现而无需改变业务代码
-----------------------------------------------------------------------------------------------------
// 不推荐:直接使用 Log4j 的 API
import org.apache.log4j.Logger;
Logger logger = Logger.getLogger(MyClass.class);
logger.info("This is a log message");
-----------------------------------------------------------------------------------------------------
// 推荐:通过 SLF4J 接口来记录日志
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("This is a log message");
-----------------------------------------------------------------------------------------------------
使用 SLF4J 可以在不同的环境中灵活切换日志实现,而无需修改代码
c.日志记录与调试不一致
如果工程师直接使用日志框架的 API,可能会在日志记录时不遵循一致的日志策略
例如,日志的级别、格式、日志输出的内容等可能不统一,导致日志信息混乱、不易追踪
通过统一的日志抽象接口(如 SLF4J)和规范的日志记录策略(通过 AOP 或日志框架自带的特性)可以保持日志的一致性和规范性
-----------------------------------------------------------------------------------------------------
最佳实践:
通过统一的日志管理类或工具类来封装日志记录方法,确保所有日志记录都遵循统一的格式和规范
在日志中统一使用适当的日志级别(如 DEBUG、INFO、WARN、ERROR)和标准格式
d.日志的性能影响
日志记录可能对应用的性能产生一定的影响,尤其是在日志记录过于频繁或日志输出内容过多的情况下
通过直接使用日志框架的 API,可能无法灵活控制日志输出的频率、内容或过滤策略,从而造成性能问题
很多日志框架(如 Log4j 和 Logback)提供了高级的配置选项,如异步日志、日志缓存等特性,可以显著提高性能
-----------------------------------------------------------------------------------------------------
推荐做法:
使用日志框架提供的异步日志功能来提高性能
配置适当的日志级别,避免在生产环境中输出过多的调试信息
e.日志管理的统一性与规范
在团队开发中,直接使用日志框架的 API 会导致不同开发人员在不同模块中记录日志时不遵循统一规范
导致日志格式不统一、信息不一致,甚至产生重复的日志记录
通过日志管理工具类或封装类,可以确保所有开发人员遵循统一的日志记录策略
-----------------------------------------------------------------------------------------------------
示例:
创建一个统一的 LoggerFactory 工厂类来生成日志记录对象
统一定义日志级别和输出格式,确保日志输出一致
9.10 [3]使用for而不是forEach遍历List
00.汇总
for性能更好
for占用内存更小
for更易控制流程
for访问变量更灵活
for处理异常更方便
for能对集合添加删除
forDebug更友好
for代码可读性更好
for更好的管理状态
for可以使用索引直接访问元素
01.for性能更好
a.说明
在我的固有认知中我是觉得for的循环性能比Stream.forEach()要好的,因为在技术界有一条真理:
越简单越原始的代码往往性能也越好
而且搜索一些文章或者大模型都是这么觉得的,可时我并没有找到专业的基准测试证明此结论。那么实际测试情况是不是这样的呢?虽然这个循环的性能差距对我们的系统性能基本上没有影响,不过为了证明for的循环性能真的比Stream.forEach()好我使用基准测试用专业的实际数据来说话。我的测试代码非常的简单,就对一个List<Integer> ids分别使用for和Stream.forEach()遍历出所有的元素,以下是测试代码:
b.代码
@State(Scope.Thread)
public class ForBenchmark {
private List<Integer> ids ;
@Setup
public void setup() {
ids = new ArrayList<>();
//分别对10、100、1000、1万、10万个元素测试
IntStream.range(0, 10).forEach(i -> ids.add(i));
}
@TearDown
public void tearDown() {
ids = new ArrayList<>();
}
@Benchmark
public void testFor() {
for (int i = 0; i <ids.size() ; i++) {
Integer id = ids.get(i);
}
}
@Benchmark
public void testStreamforEach() {
ids.stream().forEach(x->{
Integer id=x;
});
}
@Test
public void testMyBenchmark() throws Exception {
Options options = new OptionsBuilder()
.include(ForBenchmark.class.getSimpleName())
.forks(1)
.threads(1)
.warmupIterations(1)
.measurementIterations(1)
.mode(Mode.Throughput)
.build();
new Runner(options).run();
}
}
c.说明
我使用ArrayList分对10、100、1000、1万,10万个元素进行测试,以下是使用JMH基准测试的结果,结果中的数字为吞吐量,单位为ops/s,即每秒钟执行方法的次数:
方法 十 百 千 万 10万
forEach 45194532 17187781 2501802 200292 20309
for 127056654 19310361 2530502 202632 19228
for对比 ↑181% ↑12% ↑1% ↓1% ↓5%
从使用Benchmark基准测试结果来看使用for遍历List比Stream.forEach性能在元素越小的情况下优势越明显,在10万元素遍历时性能反而没有Stream.forEach好了,不过在实际项目开发中我们很少有超过10万元素的遍历。
所以可以得出结论:
在小List(万元素以内)遍历中for性能要优于Stream.forEach
02.for占用内存更小
a.说明
Stream.forEach()会占用更多的内存,因为它涉及到创建流、临时对象或者对中间操作进行缓存
for 循环则更直接,操作底层集合,通常不会有额外的临时对象
可以看如下求和代码,运行时增加JVM参数-XX:+PrintGCDetails -Xms4G -Xmx4G输出GC日志:
b.使用for遍历
List<Integer> ids = IntStream.range(1,10000000).boxed().collect(Collectors.toList());
int sum = 0;
for (int i = 0; i < ids.size(); i++) {
sum +=ids.get(i);
}
System.gc();
//GC日志
[GC (System.gc()) [PSYoungGen: 392540K->174586K(1223168K)] 392540K->212100K(4019712K), 0.2083486 secs] [Times: user=0.58 sys=0.09, real=0.21 secs]
从GC日志中可以看出,使用for遍历List在GC回收前年轻代使用了392540K,总内存使用了392540K,回收耗时0.20s
c.使用stream
List<Integer> ids = IntStream.range(1,10000000).boxed().collect(Collectors.toList());
int sum = ids.stream().reduce(0,Integer::sum);
System.gc();
//GC日志
[GC (System.gc()) [PSYoungGen: 539341K->174586K(1223168K)] 539341K->212118K(4019712K), 0.3747694 secs] [Times: user=0.55 sys=0.83, real=0.38 secs]
从GC日志中可以看出,回收前年轻代使用了539341K,总内存使用了539341K,回收耗时0.37s ,从内存占用情况来看使用for会比Stream.forEach()占用内存少37%,而且Stream.foreach() GC耗时比for多了85%。
03.for更易控制流程
a.说明
我们使用for遍历List可以很方便的使用break、continue、return来控制循环
而使用Stream.forEach在循环中是不能使用break、continue
特别指出的使用return是无法中断Stream.forEach循环的
b.代码
List<Integer> ids = IntStream.range(1,4).boxed().collect(Collectors.toList());
ids.stream().forEach(i->{
System.out.println("forEach-"+i);
if(i>1){
return;
}
});
System.out.println("==");
for (int i = 0; i < ids.size(); i++) {
System.out.println("for-"+ids.get(i));
if(ids.get(i)>1){
return;
}
}
---
输出:
forEach-1
forEach-2
forEach-3
==
for-1
for-2
从输出结果可以看出在Stream.forEach中使用return后循环还会继续执行的,而在for循环中使用return将中断循环。
04.for访问变量更灵活
a.说明
这点我想是很多人在使用Stream.forEach中比较头疼的一点
因为在Stream.forEach中引用的变量必须是final类型,也就是说不能修改forEach循环体之外的变量
但是我们很多业务场景就是修改循环体外的变量
b.代码
Integer sum=0;
for (int i = 0; i < ids.size(); i++) {
sum++;
}
ids.stream().forEach(i -> {
//报错
sum++;
});
-----------------------------------------------------------------------------------------------------
像上面的这样的代码在实际中是很常见的,sum++在forEach中是不被允许的,有时为了使用类似的方法我们只能把变量变成一个引用类型:
-----------------------------------------------------------------------------------------------------
AtomicReference<Integer> sum= new AtomicReference<>(0);
ids.stream().forEach(i -> {
sum.getAndSet(sum.get() + 1);
});
-----------------------------------------------------------------------------------------------------
所以在访问变量方面for会更加灵活。
05.for处理异常更方便
a.说明
这一点也是我使用forEach比较头疼的,在forEach中的Exception必须要捕获处理
b.代码
public void testException() throws Exception {
List<Integer> ids = IntStream.range(1, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
//直接抛出Exception
System.out.println(div(i, i - 1));
}
ids.stream().forEach(x -> {
try {
//必须捕获Exception
System.out.println(div(x, x - 1));
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
private Integer div(Integer a, Integer b) throws Exception {
return a / b;
}
-----------------------------------------------------------------------------------------------------
我们在循环中调用了div()方法,该方法抛出了Exception,如果是使用for循环如果不想处理可以直接抛出
但是使用forEach就必须要自己处理异常了,所以for在处理异常方面会更加灵活方便
06.for能对集合添加、删除
a.说明
在for循环中可以直接修改原始集合(如添加、删除元素),而 Stream 不允许修改基础集合
会抛出 ConcurrentModificationException
b.代码
List<Integer> ids = IntStream.range(0, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
if(i<1){
ids.add(i);
}
}
System.out.println(ids);
List<Integer> ids2 = IntStream.range(0, 4).boxed().collect(Collectors.toList());
ids2.stream().forEach(x -> {
if(x<1){
ids2.add(x);
}
});
System.out.println(ids2);
-----------------------------------------------------------------------------------------------------
输出:
[0, 1, 2, 3, 0]
java.util.ConcurrentModificationException
如果你想在循环中添加或者删除元素foreach是无法完成了,所以for处理集合更方便。
07.for Debug更友好
a.说明
Stream.forEach()使用了Lambda表达示,一行代码可以搞定很多功能,但是这也给Debug带来了困难
b.代码
List<Integer> ids = IntStream.range(0, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
System.out.println(ids.get(i));
}
List<Integer> ids2 = IntStream.range(0, 4).boxed().collect(Collectors.toList());
ids2.stream().forEach(System.out::println);
-----------------------------------------------------------------------------------------------------
我们可以看出使用for循环Debug可以一步一步的跟踪程序执行步骤,但是使用forEach却做不到,所以for可以更方便的调试你的代码,让你更快捷的找到出现问题的代码。
08.for代码可读性更好
a.说明
Lambda表达示属于面向函数式编程,主打的就是一个抽象,相比于面向对象或者面向过程编程代码可读性是非常的差,有时自己不写的代码过段时间后自己都看不懂。就比如我在文章《解密阿里大神写的天书般的Tree工具类,轻松搞定树结构!》一文中使用函数式编程写了一个Tree工具类,我们可以对比一下面向过程和面向函数式编程代码可读性的差距:
b.使用for面向过程编程代码
public static List<MenuVo> makeTree(List<MenuVo> allDate,Long rootParentId) {
List<MenuVo> roots = new ArrayList<>();
for (MenuVo menu : allDate) {
if (Objects.equals(rootParentId, menu.getPId())) {
roots.add(menu);
}
}
for (MenuVo root : roots) {
makeChildren(root, allDate);
}
return roots;
}
public static MenuVo makeChildren(MenuVo root, List<MenuVo> allDate) {
for (MenuVo menu : allDate) {
if (Objects.equals(root.getId(), menu.getPId())) {
makeChildren(menu, allDate);
root.getSubMenus().add(menu);
}
}
return root;
}
c.使用forEach面向函数式编程代码
public static <E> List<E> makeTree(List<E> list, Predicate<E> rootCheck, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> setSubChildren) {
return list.stream().filter(rootCheck).peek(x -> setSubChildren.accept(x, makeChildren(x, list, parentCheck, setSubChildren))).collect(Collectors.toList());
}
private static <E> List<E> makeChildren(E parent, List<E> allData, BiFunction<E, E, Boolean> parentCheck, BiConsumer<E, List<E>> children) {
return allData.stream().filter(x -> parentCheck.apply(parent, x)).peek(x -> children.accept(x, makeChildren(x, allData, parentCheck, children))).collect(Collectors.toList());
}
d.总结
对比以上两段代码,可以看出面向过程的代码思路非常的清晰,基本上可以一眼看懂代码要做什么,反观面向函数式编程的代码,我想大都人一眼都不知道代码在干什么的,所以使用for的代码可读性会更好。
09.for更好的管理状态
a.说明
for循环可以轻松地在每次迭代中维护状态,这在Stream.forEach中可能需要额外的逻辑来实现。这一条可理由三有点像,我们经常需要通过状态能控制循环是否执行,如下代码:
b.代码
boolean flag = true;
for (int i = 0; i < 10; i++) {
if(flag){
System.out.println(i);
flag=false;
}
}
AtomicBoolean flag1 = new AtomicBoolean(true);
IntStream.range(0, 10).forEach(x->{
if (flag1.get()){
flag1.set(false);
System.out.println(x);
}
});
c.总结
这个例子说明了在使用Stream.forEach时,为了维护状态,我们需要引入额外的逻辑,如使用AtomicBoolean,而在for循环中,这种状态管理是直接和简单的。
10.for可以使用索引直接访问元素
a.说明
在某些情况下,特别是当需要根据元素的索引(位置)来操作集合中的元素时,for就可以直接使用索引访问了。在Stream.forEach中就不能直接通过索引访问,比如我们需要将ids中的数字翻倍:
b.代码
List<Integer> ids = IntStream.range(0, 4).boxed().collect(Collectors.toList());
for (int i = 0; i < ids.size(); i++) {
ids.set(i,i*2);
}
List<Integer> ids2 = IntStream.range(0, 4).boxed().collect(Collectors.toList());
ids2=ids2.stream().map(x->x*2).collect(Collectors.toList());
c.总结
我们使用for循环来遍历这个列表,并在每次迭代中根据索引i来修改列表中的元素。这种操作直接且直观。而使用Stream.foreach()不能直接通过索引下标访问元素的,只能将List转换为流,然后使用map操作将每个元素乘以2,最后,我们使用Collectors.toList()将结果收集回一个新的List。
9.11 [3]如何使用Iterator安全地删除元素
01.如何使用 Iterator 安全地删除元素
a.Iterator 基础
为了解决 foreach 循环中修改集合的问题,我们可以使用 Iterator 显式地遍历集合。Iterator 是集合框架中的一个接口,它允许我们在遍历集合时安全地修改集合(如删除元素),而不会引发 ConcurrentModificationException。
Iterator 提供了 remove() 方法,该方法能在遍历过程中安全地删除当前元素,而不会破坏集合的结构。关键点是,Iterator 在每次调用 next() 方法后,记录当前元素的位置,而 remove() 方法会标记并删除该位置的元素。
源码分析: 在 ArrayList 类中,remove() 方法会通过 Iterator 的 remove() 方法进行集合修改,调用时会更新 modCount,并且保证删除的元素不会影响剩余元素的顺序。
使用 Iterator 删除元素:我们使用 Iterator 显式地迭代集合并删除元素 "b"。由于 Iterator 提供了 remove() 方法,这种做法可以安全地删除集合中的元素而不会引发异常。
-----------------------------------------------------------------------------------------------------
import java.util.*;
public class IteratorRemoveExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
// 正确的做法: 使用 Iterator 删除元素
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("b".equals(item)) {
iterator.remove(); // 安全删除元素
}
}
System.out.println(list); // 输出: [a, c, d]
}
}
b.Iterator的工作原理
Iterator 的工作原理很简单:它内部维护了一个指针,每次调用 next() 方法时,指针会向前移动,并返回当前元素
删除元素时,Iterator 会在指针所指向的位置删除该元素,从而避免了修改集合结构时可能引发的并发问题
02.并发操作中的Iterator加锁
a.并发问题的来源
在多线程环境下,同时访问和修改同一个集合可能导致线程安全问题。例如,一个线程正在遍历集合,另一个线程正在修改集合,这种并发访问可能导致数据不一致、死锁或其他不可预料的问题。为了保证线程安全,在并发场景下对集合的迭代器进行加锁是十分必要的。
b.如何加锁保护 Iterator
Iterator 本身并不是线程安全的,因此我们需要手动加锁,以确保在一个线程遍历集合时,其他线程不会修改该集合。加锁可以通过 synchronized 关键字来实现。
源码分析: Java 集合类中的 Collections.synchronizedList() 方法是将一个非线程安全的集合包装成一个线程安全的集合。它通过在所有方法上添加同步块来实现线程安全。
并发操作时对 Iterator 加锁: 我们使用 Collections.synchronizedList() 将 list 包装成一个线程安全的集合,并通过 synchronized (list) 块来加锁对 Iterator 的访问。这样,可以确保在遍历集合时,其他线程不会对集合进行修改,从而避免并发问题。
-------------------------------------------------------------------------------------------------
import java.util.*;
public class SynchronizedIteratorExample {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>(Arrays.asList("a", "b", "c", "d")));
// 在并发操作中加锁
synchronized (list) {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("b".equals(item)) {
iterator.remove(); // 删除元素
}
}
}
System.out.println(list); // 输出: [a, c, d]
}
}
c.Iterator 的线程安全性和最佳实践
对于并发场景中的 Iterator,加锁是保证线程安全的最常见方法
然而,为了提高并发性能,还可以考虑使用 CopyOnWriteArrayList 或 ConcurrentLinkedQueue 等线程安全的集合
它们在设计时已经处理了并发问题,避免了手动加锁的需要
9.12 [3]禁止foreach循环对元素remove/add
00.小结
在 foreach 循环中直接进行 remove 或 add 操作是不安全的,主要有以下原因:
ConcurrentModificationException:直接修改集合会触发迭代器的并发修改检测,导致异常
不可预测的行为:修改集合的结构可能导致元素遗漏、顺序错乱或程序逻辑出错
使用 Iterator 替代:使用 Iterator 的 remove() 方法可以避免这些问题,实现安全的元素删除操作
因此,正确的做法是使用 Iterator 显式地处理元素的删除或修改,而不是直接在 foreach 循环中进行修改
01.为什么禁止在 foreach 循环里进行元素的 remove/add 操作?
a.ConcurrentModificationException 异常
当你在 foreach 循环中直接修改集合(例如 remove 或 add 元素),会导致并发修改问题
foreach 循环底层使用了集合的 Iterator 来遍历元素
大多数集合类(如 ArrayList、HashSet 等)都会维护一个 modCount 计数器,表示集合的结构变更次数
当你在遍历时修改集合的结构(如删除或添加元素),modCount 会发生变化
而 Iterator 会检测到这种结构性修改,从而抛出 ConcurrentModificationException 异常
防止程序在多线程环境中出现意外行为
---------------------------------------------------------------------------------------------------------
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("b")) {
list.remove(s); // 会抛出 ConcurrentModificationException
}
}
-----------------------------------------------------------------------------------------------------
在上面的代码中,foreach 循环遍历 list 时,如果删除了元素 b,它会修改 list 的结构
从而导致 Iterator 检测到并发修改,抛出异常
b.不可预测的行为
即使没有抛出 ConcurrentModificationException,在 foreach 循环中修改集合也会导致不可预测的行为
例如,remove 或 add 操作会改变集合的大小和内容,可能会影响迭代的顺序或导致遗漏某些元素,甚至造成死循环或跳过某些元素
-----------------------------------------------------------------------------------------------------
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("b")) {
list.add("e"); // 修改集合的大小
}
System.out.println(s);
}
-----------------------------------------------------------------------------------------------------
在这个例子中,add 操作会向 list 中添加一个新元素 "e",从而修改了集合的结构
因为 foreach 循环的内部实现使用了迭代器,它可能不会考虑到修改后的新元素,导致输出顺序或遍历结果与预期不同
c.迭代器的 remove() 方法
如果需要在循环中删除元素,推荐使用 Iterator 显式地进行删除操作
Iterator 提供了一个安全的 remove() 方法,可以在遍历时安全地删除元素,而不会引发 ConcurrentModificationException
-----------------------------------------------------------------------------------------------------
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("b")) {
iterator.remove(); // 使用 Iterator 的 remove() 方法
}
}
-----------------------------------------------------------------------------------------------------
使用 Iterator.remove() 可以安全地在遍历时删除元素,而不会抛出并发修改异常
9.13 [4]建议初始化HashMap容量大小
00.小结
初始化 HashMap 的容量大小有以下好处:
提高性能:减少扩容次数,优化存取效率
节省内存:避免多次扩容引起的内存浪费
提升线程安全:在并发环境下减少扩容带来的线程不安全风险
合理初始化 HashMap 容量对于高性能应用尤为重要,尤其在存储大量数据时可以显著提升程序的运行效率
01.为什么建议初始化 HashMap 的容量大小?
a.减少扩容次数,提高性能
HashMap 默认的初始容量为 16,当超过负载因子阈值(默认是 0.75,即达到容量的 75%)时
HashMap 会自动进行扩容操作,将容量扩大为原来的两倍
扩容涉及到重新计算哈希并将数据重新分布到新的桶中,这个过程非常耗时,尤其在元素较多时,扩容会显著影响性能
通过设置合适的初始容量,可以避免或减少扩容操作,提高 HashMap 的存取效率
b.节省内存,避免不必要的内存开销
如果预计要存储大量数据但没有指定容量,HashMap 可能会多次扩容,每次扩容会分配新的内存空间
并将原有数据复制到新空间中,造成内存浪费。如果在创建 HashMap 时能合理估算其容量
则可以一次性分配足够的空间,从而避免重复分配内存带来的资源浪费
c.避免扩容带来的线程安全问题
在并发环境下,频繁扩容可能导致线程不安全,即使是 ConcurrentHashMap 也不能完全避免扩容带来的性能和一致性问题
初始化合适的容量可以减少并发环境下扩容带来的风险
d.如何估算合适的容量
预估数据量:如果预计 HashMap 将存储 n 个元素,可以将初始容量设置为 (n / 0.75)
再向上取整为最接近的 2 的幂次方
-----------------------------------------------------------------------------------------------------
int initialCapacity = (int) Math.ceil(n / 0.75);
Map<String, String> map = new HashMap<>(initialCapacity);
-----------------------------------------------------------------------------------------------------
取 2 的幂次方:HashMap 的容量总是以 2 的幂次方增长
因为在进行哈希运算时,可以高效利用按位与操作来计算哈希桶索引
因此,初始容量设为 2 的幂次方会使哈希分布更均匀
e. 示例代码
int expectedSize = 1000; // 预估需要存储的键值对数量
int initialCapacity = (int) Math.ceil(expectedSize / 0.75);
HashMap<String, Integer> map = new HashMap<>(initialCapacity);
9.14 [4]谨慎使用ArrayList中的subList方法
00.总结
使用 ArrayList 的 subList 方法需要注意以下几点:
视图机制:subList 只是原列表的视图,修改其中一个会影响另一个
结构性修改限制:结构性修改原列表后再访问 subList 会抛出 ConcurrentModificationException
批量操作问题:subList 的批量操作可能引发不可预料的错误
建议创建副本:如需独立操作子列表,最好创建 subList 的副本以避免潜在问题
01.为什么要求谨慎使用 ArrayList 中的 subList 方法?
a.subList 返回的是视图,而不是独立副本
ArrayList 的 subList 方法返回的是原列表的一部分视图(view),而不是一个独立的副本。对 subList 的修改会直接影响原列表,反之亦然:
-----------------------------------------------------------------------------------------------------
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);
subList.set(0, 10); // 修改 subList
System.out.println(list); // 原列表也受到影响:[1, 10, 3, 4, 5]
-----------------------------------------------------------------------------------------------------
这种共享视图的机制在某些场景中可能引发意外的修改,导致数据被意外改变,从而影响到原始数据结构的完整性和正确性。
b.subList 的结构性修改限制
当对 ArrayList 本身(而非 subList 视图)进行结构性修改(add、remove 等改变列表大小的操作)后,再操作 subList 会导致 ConcurrentModificationException 异常。这是因为 subList 和原 ArrayList 之间共享结构性修改的状态,一旦其中一个发生修改,另一方就会失效:
-----------------------------------------------------------------------------------------------------
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List<Integer> subList = list.subList(1, 4);
list.add(6); // 修改原列表的结构
subList.get(0); // 抛出 ConcurrentModificationException
-----------------------------------------------------------------------------------------------------
这种限制意味着 subList 不适合在列表频繁变化的场景中使用,否则很容易引发并发修改异常。
c.subList 和 ArrayList 的 removeAll 等操作可能导致错误
subList 生成的视图列表可能会在批量删除操作中出现问题,例如调用 removeAll 方法时,subList 的行为不一致或发生异常。对于 ArrayList 的 subList,一些批量修改方法(如 removeAll、retainAll)可能会在删除视图元素后,导致 ArrayList 产生不可预料的状态,甚至引发 IndexOutOfBoundsException 等异常。
d.推荐的安全使用方式
如果需要一个独立的子列表,可以通过 new ArrayList<>(originalList.subList(start, end))
来创建一个子列表的副本,从而避免 subList 的共享视图问题:
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
ArrayList<Integer> subListCopy = new ArrayList<>(list.subList(1, 4)); // 创建副本
list.add(6); // 修改原列表
subListCopy.get(0); // 安全,不会受到影响
9.15 [4]不推荐使用keySet()遍历HashMap,而是entrySet()
00.汇总
a.entrySet() 方法
优点:避免多次哈希查找,减少内存消耗,代码简单明了
缺点:没有特定缺点,在大多数情况下是最佳选择
b.forEach 方法
优点:代码简洁,可读性强,充分利用 lambda 表达式
缺点:仅适用于 Java 8 及以上版本
c.iterator 方法
优点:适用于需要在遍历过程中修改集合的情况,如删除元素
缺点:代码稍显繁琐,不如 entrySet() 和 forEach 方法直观
d.Streams API 方法
优点:支持复杂操作,如过滤、映射等,代码简洁
缺点:仅适用于 Java 8 及以上版本,性能在某些情况下可能不如 entrySet() 和 forEach
01.说明
在Java编程中,HashMap 是一种非常常见的数据结构。我们经常需要对其中的键值对进行遍历
通常有多种方法可以遍历 HashMap,其中一种方法是使用 keySet() 方法
然而,很多Java大佬并不推荐这种方法。为什么呢?
02.keySet() 方法的工作原理
a.说明
首先,让我们来看一下 keySet() 方法是如何工作的。keySet() 方法返回 HashMap 中所有键的集合 (Set<K>)
然后我们可以使用这些键来获取相应的值。
b.代码
// 创建一个HashMap并填充数据
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
// 使用keySet()方法遍历HashMap
for (String key : map.keySet()) {
// 通过键获取相应的值
Integer value = map.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}
这个代码看起来没什么问题,但在性能和效率上存在一些隐患
03.keySet() 方法的缺点
a.多次哈希查找
如上面的代码所示,使用 keySet() 方法遍历时,需要通过键去调用 map.get(key) 方法来获取值
这意味着每次获取值时,都需要进行一次哈希查找操作。如果 HashMap 很大,这种方法的效率就会明显降低
b.额外的内存消耗
keySet() 方法会生成一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图
但仍然需要额外的内存开销来维护这个集合的结构。如果 HashMap 很大,这个内存开销也会变得显著
c.代码可读性和维护性
使用 keySet() 方法的代码可能会让人误解,因为它没有直接表现出键值对的关系
在大型项目中,代码的可读性和维护性尤为重要
04.更好的选择:entrySet() 方法
a.说明
相比之下,使用 entrySet() 方法遍历 HashMap 是一种更好的选择
entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry<K, V>>)
通过遍历这个集合,我们可以直接获取每个键值对,从而避免了多次哈希查找和额外的内存消耗
b.代码
// 创建一个HashMap并填充数据
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
// 使用entrySet()方法遍历HashMap
for (Map.Entry<String, Integer> entry : map.entrySet()) {
// 直接获取键和值
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
c.entrySet() 方法的优势
a.避免多次哈希查找
在遍历过程中,我们可以直接从 Map.Entry 对象中获取键和值,而不需要再次进行哈希查找,提高了效率
b.减少内存消耗
entrySet() 方法返回的是 HashMap 内部的一个视图,不需要额外的内存来存储键的集合
c.提高代码可读性
entrySet() 方法更直观地表现了键值对的关系,使代码更加易读和易维护
d.性能比较
a.主要性能问题
a.多次哈希查找
使用 keySet() 方法遍历 HashMap 时,需要通过键调用 map.get(key) 方法获取值
这意味着每次获取值时都需要进行一次哈希查找操作。哈希查找虽然时间复杂度为 O(1)
但在大量数据下,频繁的哈希查找会累积较高的时间开销
b.额外的内存消耗
keySet() 方法返回的是一个包含所有键的集合。虽然这个集合是基于 HashMap 的键的视图
但仍然需要额外的内存来维护这个集合的结构
b.更高效的选择:entrySet() 方法
相比之下,entrySet() 方法返回的是 HashMap 中所有键值对的集合 (Set<Map.Entry<K, V>>)
通过遍历这个集合,我们可以直接获取每个键值对,避免了多次哈希查找和额外的内存消耗
c.性能比较示例
import java.util.HashMap;
import java.util.Map;
public class HashMapTraversalComparison {
public static void main(String[] args) {
// 创建一个大的HashMap
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, i);
}
// 测试keySet()方法的性能
long startTime = System.nanoTime(); // 记录开始时间
for (String key : map.keySet()) {
Integer value = map.get(key); // 通过键获取值
}
long endTime = System.nanoTime(); // 记录结束时间
System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");
// 测试entrySet()方法的性能
startTime = System.nanoTime(); // 记录开始时间
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey(); // 直接获取键
Integer value = entry.getValue(); // 直接获取值
}
endTime = System.nanoTime(); // 记录结束时间
System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");
}
}
d.深度解析性能比较示例
a.创建一个大的 HashMap
Map<String, Integer> map = new HashMap<>();
for (int i = 0; i < 1000000; i++) {
map.put("key" + i, i);
}
-------------------------------------------------------------------------------------------------
创建一个包含100万个键值对的 HashMap。
键 为 "key" + i,值 为 i。
这个 HashMap 足够大,可以明显展示两种遍历方法的性能差异。
b.测试 keySet() 方法的性能
long startTime = System.nanoTime(); // 记录开始时间
for (String key : map.keySet()) {
Integer value = map.get(key); // 通过键获取值
}
long endTime = System.nanoTime(); // 记录结束时间
System.out.println("keySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");
-------------------------------------------------------------------------------------------------
使用 keySet() 方法获取所有键,并遍历这些键。
在每次迭代中,通过 map.get(key) 方法获取值。
记录开始时间和结束时间,计算遍历所需的总时间。
c.测试 entrySet() 方法的性能
startTime = System.nanoTime(); // 记录开始时间
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey(); // 直接获取键
Integer value = entry.getValue(); // 直接获取值
}
endTime = System.nanoTime(); // 记录结束时间
System.out.println("entrySet() 方法遍历时间: " + (endTime - startTime) + " 纳秒");
-------------------------------------------------------------------------------------------------
使用 entrySet() 方法获取所有键值对,并遍历这些键值对。
在每次迭代中,直接从 Map.Entry 对象中获取键和值。
记录开始时间和结束时间,计算遍历所需的总时间。
e.性能结果分析
a.场景
假设上述代码的运行结果如下:
keySet() 方法遍历时间: 1200000000 纳秒
entrySet() 方法遍历时间: 800000000 纳秒
可以看出,使用 entrySet() 方法的遍历时间明显短于 keySet() 方法。这主要是因为
b.避免了多次哈希查找
使用 keySet() 方法时,每次获取值都需要进行一次哈希查找
而使用 entrySet() 方法时,键和值直接从 Map.Entry 对象中获取,无需再次查找
c.减少了内存消耗
使用 keySet() 方法时,额外生成了一个包含所有键的集合
而使用 entrySet() 方法时,返回的是 HashMap 内部的一个视图,无需额外的内存开销
f.小结一下
通过性能比较示例,我们可以清楚地看到 entrySet() 方法在遍历 HashMap 时的效率优势
使用 entrySet() 方法不仅能避免多次哈希查找,提高遍历效率,还能减少内存消耗
综上所述,在遍历 HashMap 时,entrySet() 方法是更优的选择
04.几种高效的替代方案
a.使用 entrySet() 方法
import java.util.HashMap;
import java.util.Map;
public class EntrySetTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}
b.使用 forEach 方法
import java.util.HashMap;
import java.util.Map;
public class ForEachTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
map.forEach((key, value) -> {
System.out.println("Key: " + key + ", Value: " + value);
});
}
}
c.使用 iterator 方法
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class IteratorTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
}
}
d.使用 Streams API
import java.util.HashMap;
import java.util.Map;
public class StreamTraversal {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("cherry", 3);
map.entrySet().stream().forEach(entry -> {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
});
}
}