10、常用类
1. Object 类
理论上 Object 类是所有类的父类,即直接或间接地继承
java.lang.Object
类。由于所有的类都继承 Object 类,因此省略了
extends Object
关键字。
Object 类中主要有以下方法:
- toString()
- getClass()
- equals()
- clone()
- finalize()
- …
1.1 clone()
方法
在 Java 语言中, clone() 方法被对象调用,用来复制对象。
所谓的复制对象,即分配一个和源对象同样大小的空间,在这个空间中创建一个新的对象。
在 Java 语言中,有以下两种方式创建对象:
-
使用 new 操作符创建一个对象
new 操作符的本意是分配内存。
- 程序执行到 new 操作符时, 首先去看 new 操作符后面的类型,需要分配多大的内存空间;
- 分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化;
- 构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
-
使用 clone() 方法复制一个对象
clone() 方法第一步与 new 类似,都是分配内存。
-
调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同;
-
然后再使用源对象中对应的各个域,填充新对象的域;
-
填充完成之后,clone() 方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
-
clone 与 copy 的区别:
- copy 只是简单复制了引用,两个对象都是指向内存同一个
- clone 会生成一个新的对象,并且与源对象具有相同的属性值和方法
深拷贝与浅拷贝:
Object 在对某个对象实施 Clone 时对其是一无所知的,它仅仅是简单地执行域对域的 copy,这就是浅拷贝
- 克隆类和原始类可能会共享一部分信息(原始类存在引用类型的变量)
重写 clone() 方法,实现深拷贝,代码示例如下:
1 | public class Person implements Cloneable { |
注意事项:
- 使用
clone()
方法必须实现接口 Cloneable,默认实现的就是浅拷贝(引用拷贝) - 如果想要深拷贝一个对象,这个对象必须要实现 Cloneable 接口,重写
clone()
方法,并且在clone()
方法内部,把该对象引用的其他对象也要 clone 一份 , 这就要求这个被引用的对象必须也要实现 Cloneable 接口并且实现clone()
方法。 - 如果在拷贝一个对象时,要想让这个拷贝的对象和源对象完全彼此独立,那么在引用链上的每一级对象都要被显式的拷贝。
- String 存在于堆内存、常量池;这种比较特殊, 本身没有实现 Cloneable,传递是引用地址;由本身的 final 性, 每次赋值都是一个新的引用地址,原对象的引用和副本的引用互不影响。因此String就和基本数据类型一样,表现出了"深拷贝"特性。
1.2 toString()
方法
POJO 类必须写 toString
方法。
1.3 getClass()
方法
返回对象的类。
1.4 finalize()
方法
用于实例被垃圾回收器回收时的触发的操作。
当 GC (垃圾回收器) 确定不存在对该对象的有更多引用时,对象的垃圾回收器就会调用这个方法。
工作原理:
一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其 finalize()
方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。
1.5 equals()
方法
用于比较两个对象是否相等。
equals()
方法比较两个对象,是判断两个对象引用指向的是同一个对象,即它只是检查两个对象是否指向内存中的同一个地址。
Object
类的 equals()
源码:
1 | public boolean equals(Object obj) { |
如果希望不同内存但相同内容的两个对象使用 equals()
时返回 true,则需要重写父类的 equal()
。String 类已经重写了 Object
类中的 equals()
,比较内容是否相等。
String
类的 equals()
源码:
1 | public boolean equals(Object anObject) { |
==
和 equals()
比较:
==
是判断两个对象的内存地址是否相等,即判断是否是同一个对象。equals()
有两种情况:- 第一种情况就是没有被重写的
equals()
方法,它就相当于通过==
去比较; - 第二种情况是重写了的
equals()
方法,一般重写后都是去比较两个对象的内容(或者说属性)是否相等,具体看重写的方法。
- 第一种情况就是没有被重写的
1.6 hashCode()
方法
用于获取对象的 hash 值(是一个整数,表示在哈希表中的位置)
如果两个对象相等,那么它们的 hashCode 值一定要相等
想象一下,假如两个 Java 对象 A 和 B,A 和 B 相等(
eqauls()
结果为 true),但 A 和 B 的 hashCode不同,则 A 和 B 存入 HashMap 时的哈希码计算得到的 HashMap 内部数组位置索引可能不同,那么 A 和 B 很有可能允许同时存入 HashMap,显然相等 / 相同的元素是不允许同时存入 HashMap,HashMap 不允许存放重复元素。
不同对象的 hashCode 可能相同
假如两个 Java 对象 A 和 B,A 和 B 不相等(
eqauls()
结果为 false),但 A 和 B 的 hashCode 相等,将 A 和 B 都存入 HashMap 时会发生哈希冲突,也就是 A 和 B 存放在 HashMap 内部数组的位置索引相同,这时 HashMap 会在该位置建立一个链接表,将 A 和 B 串起来放在该位置,显然,该情况不违反 HashMap 的使用原则,是允许的。当然,哈希冲突越少越好,尽量采用好的哈希算法以避免哈希冲突。
hashCode 不同的对象一定不相等
如果子类重写了 equals() 方法,就需要重写 hashCode()
方法。比如 String 类就重写了 equals()
方法,同时也重写了 hashCode()
方法。
因为当你需要将对象存入到底层为散列表结构的集合中时,是先判断 hashcode 值,碰到相同值时再通过
equals()
进一步判断。hashCode()
和equals()
两个方法用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度。如果在重写equals()
时,不重写hashCode()
,就会导致在某些场景下,例如将两个相等的自定义对象存储在 Set 集合时,如果只重写了 equals 方法,那么默认情况下,Set 进行去重操作时,会先判断两个对象的 hashCode 是否相同,此时因为没有重写 hashCode 方法,所以会直接执行 Object 中的 hashCode 方法,而 Object 中的 hashCode 方法对比的是两个不同引用地址的对象,所以结果是 false,那么 equals 方法就不用执行了,直接返回的结果就是 false:两个对象不是相等的,于是就在 Set 集合中插入了两个相同的对象;但是,如果在重写 equals 方法时,也重写了 hashCode 方法,那么在执行判断时会去执行重写的 hashCode 方法,此时对比的是两个对象的所有属性的 hashCode 是否相同,于是调用 hashCode 返回的结果就是 true,再去调用 equals 方法,发现两个对象确实是相等的,于是就返回 true 了,因此 Set 集合就不会存储两个一模一样的数据了,于是整个程序的执行就正常了。但这其实是针对当该类会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中用到的时候这种情况。
1.7 wait()
方法及其重载
-
wait()
方法让当前线程进入等待状态(直到其它线程调用此对象的
notify()
方法或notifyAll()
方法) -
wait(long timeout)
方法让当前线程处于等待(阻塞)状态,直到其他线程调用此对象的
notify()
方法或notifyAll()
方法,或者超过参数 timeout 设置的超时时间。- 如果 timeout 参数为 0,则不会超时,会一直进行等待
- timeout - 等待时间,以毫秒为单位
-
wait(long timeout, int nanos)
方法与
wait(long timeout)
方法类似,多了一个 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)
注意事项:
- 当前线程必须是此对象的监视器所有者,否则会发生 IllegalMonitorStateException 异常。
- 如果当前线程在等待之前或在等待时被任何线程中断,则会抛出 InterruptedException 异常。
- 如果传递的参数不合法,则会抛出 IllegalArgumentException 异常。
1.8 notify()
方法
用于唤醒在该对象上等待的某个线程
- 如果所有的线程都在此对象上等待,那么只会选择一个线程,选择是任意性的,并在对实现做出决定时发生。
notify()
方法只能被作为此对象监视器的所有者的线程来调用。
一个线程要想成为对象监视器的所有者,可以使用以下 3 种方法:
- 执行对象的同步实例方法
- 使用 synchronized 内置锁
- 对于 Class 类型的对象,执行同步静态方法
一次只能有一个线程拥有对象的监视器。
一个线程在对象监视器上等待可以调用 wait() 方法。
1.9 notifyAll()
方法
用于唤醒在该对象上等待的所有线程
等待/通知机制,是指线程 A 调用了对象 O 的 wait()
方法进入等待状态,而线程 B 调用了对象 O 的 notify()
/ notifyAll()
方法,线程 A 收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的 wait()
方法和 notify()
/ notifyAll()
方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
2. 包装类
Java 为每种基本数据类型分别设计了对应的类,称之为包装类 (Wrapper Classes)。
基本数据类型 | 对应的包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
- 所有的包装类 (Byte, Short, Integer, Long, Float, Double) 都是抽象类 Number 的子类。
- 每个包装类的对象可以封装一个相应的基本类型的数据,并提供了其它一些有用的方法。
- 包装类对象一经创建,其内容(所封装的基本类型数据值)不可改变。
基本类型和对应的包装类可以相互装换:
-
由基本类型向对应的包装类转换称为装箱
- 使用
valueOf()
方法将基本类型转换为相应的包装类对象,如:Integer obj = Integer.valueOf(6);
- Java 编译器可以直接将基本类型转换为相应的对象(自动装箱),如:
Integer obj = 6;
- 使用包装类 (Wrapper) 构造函数将基本类型转换为包装对象。(但是在 Java 9 之后,不再使用构造函数)
- 使用
-
包装类向对应的基本类型转换称为拆箱
-
使用每个包装类中对应的值方法(
intValue()
、doubleValue()
等)1
2Integer obj = 6;
int m = obj.intValue(); -
Java 编译器可以自动将对象转换为相应的原始类型(自动拆箱)
1
2Integer obj = 6;
int m = obj;
-
特别地,
-
将字符串转换为整数
Integer 类有一个静态的
parseInt()
方法,可以将纯数字字符串转换为整数1
int n = Integer.parseInt(String s, int radix); // s 表示字符串,radix 表示进制,默认为 10
-
将整数转化为字符串
toString()
方法,如:String str = Integer.toString(666);
- 直接在整数后面加空字符串
基本类型比相应的对象更有效,当需要效率时,总是建议使用基本类型。
3. Math 类
3.1 常量值
-
Math.E
1
public static final double E = 2.7182818284590452354;
-
Math.PI
1
public static final double PI = 3.14159265358979323846;
3.2 Math.exp() 方法
用于返回自然数底数 e 的参数次方
3.3 Math.pow() 方法
用于返回第一个参数的第二个参数次方
3.4 Math.sqrt() 方法
用于返回参数的算术平方根
3.5 Math.cbrt() 方法
用于返回参数的立方根
3.6 Math.ceil() 方法
对一个数进行上舍入,返回值大于或等于给定的参数,类型为 double 型
- 个人理解:假设一个坐标轴,给定的参数为上面一点,返回的是给定参数的右边部分的最左端的整数值
3.7 Math.floor() 方法
对一个数进行下舍入,返回值小于或等于给定的参数,类型为 double 型
- 个人理解:假设一个坐标轴,给定的参数为上面一点,返回的是给定参数的左边部分的最右端的整数值
3.8 Math.rint() 方法
返回最接近参数的整数值(如果有 2 个数同样接近,则会返回偶数的那个)
3.9 Math.round() 方法
“四舍五入”:
-
参数为正数:小数部分 ≥0.5 时,整数取值向右一个整数,即+1。
-
参数为负数:小数部分 ≤0.5 时,取值右侧的整数。
注意事项:
- 参数为 float 型时返回 int 型值
- 参数为 double 型时返回 long 型值
3.10 Math.random() 方法
用于返回一个随机数,随机数范围为 0.0 =< Math.random < 1.0
4. Random 类
Random 类共有两种构造方法:
-
Random()
此构造方法是以系统自身的时间为种子数来构造 Random 对象。
-
Random(long)
此构造方法可以自己来选定具体的种子来构造 Random 对象。
虽然 Random 类产生的数字是随机的,但在相同种子数(seed)下的相同次数产生的随机数是相同的(伪随机)。
5. Date 类
封装当前的日期和时间。
Date 类提供两个构造函数来实例化 Date 对象:
-
Date()
使用当前日期和时间来初始化对象。
-
Date(long)
接收一个参数,该参数是自 1970 年 1 月 1 日 00:00:00 GMT 以来的毫秒。
getTime()
方法
返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
格式化日期
-
使用 SimpleDateFormat 类
代码片段示例:
1
2
3Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
System.out.println("当前时间为: " + ft.format(date));SimpleDateFormat 类的
parse()
方法能够按照给定的 SimpleDateFormat 对象的格式化存储来解析字符串更多有关 SimpleDateFormat 类内容详见:Java 日期时间 | 菜鸟教程 (runoob.com)
-
使用
printf()
方法使用两个字母格式,以 %t 开头
更多有关
printf()
方法内容详见:Java 日期时间 | 菜鸟教程 (runoob.com)
sleep()
使当前线程进入停滞状态(阻塞当前线程)
6. Calendar 类
Calendar 类是一个抽象类,在实际使用时实现特定的子类的对象,创建对象的过程对程序员来说是透明的,只需要使用
getInstance()
方法创建即可。
-
创建一个代表系统当前日期的 Calendar 对象
1
Calendar c = Calendar.getInstance();
-
创建一个指定日期的 Calendar 对象
1
2
3
4
5Calendar c = Calendar.getInstance();
c.set(int field, int value); // 第一种
c.set(int year, int month, int date); // 第二种
c.set(int year, int month, int date, int hourOfDay, int minute); // 第三种
c.set(int year, int month, int date, int hourOfDay, int minute, int second); // 第四种
注意事项:
-
Calendar 类的月份是从 0 开始的
1
2
3
4Calendar c = Calendar.getInstance();
c.set(2024, 2 - 1, 27, 10, 57); // 2 - 1 表示 2 月份
System.out.println(c.getTime());
System.out.println(c.get(Calendar.MONTH) + 1); // + 1 还原本来的月份值 -
Calendar 类的 DAY_OF_WEEK 是从星期日开始的
1
2
3
4Calendar c = Calendar.getInstance();
c.set(2024, 2 - 1, 27, 10, 57);
System.out.println(c.getTime());
System.out.println(c.get(Calendar.DAY_OF_WEEK) - 1); // - 1 还原本来的星期值 -
将日期设为 0 以后,月份将变成了上个月,但月份可以为0
1
2Calendar c = Calendar.getInstance();
c.set(2024, 2 - 1, 0); // 结果为 1 月 31 日
常用 Calendar 类对象字段类型
常量 | 描述 |
---|---|
Calendar.YEAR | 年份 |
Calendar.MONTH | 月份 |
Calendar.DATE | 日期 |
Calendar.DAY_OF_MONTH | 日期,和 Calendar.DATE 意义完全相同 |
Calendar.HOUR | 12 小时制的小时 |
Calendar.HOUR_OF_DAY | 24 小时制的小时 |
Calendar.MINUTE | 分钟 |
Calendar.SECOND | 秒 |
Calendar.DAY_OF_WEEK | 星期几 |
Calendar 类的 add(int field, int amount)
方法在可以给定的日历字段中增加或减去指定的时间量
Calendar 的 getInstance()
方法返回一个默认用当前的语言环境和时区初始化的 GregorianCalendar 对象。
- 特别地,GregorianCalendar 类中含有一个
isLeapYear(int year)
方法,用于确定给定的年份是否为闰年。
7. String 类
String 为字符串常量。即:String 对象创建后是不可以更改的(改变的只是地址,原来的字符串还是存在的,并且产生垃圾)。
举例说明:
1 | String str = "Hello "; |
String 底层使用一个字符数组来维护,String 类的值是 final 类型的,不能被改变,只要一个值改变就会生成一个新的 String 类型对象。
String 类的构造方法详见源码
常量池
-
Class 常量池
- Class 常量池在 .class 文件中,用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)
- 字面量包括:文本字符串、基本数据类型的值、被声明为 final 的常量等;
- 符号引用包括:类和接口的全限定名、字段名称和描述符、方法名称和描述符。
- 每个 .class 文件都有一个 Class 常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- Class 常量池在 .class 文件中,用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)
-
运行时常量池
运行时常量池是 Class 常量池被加载到内存之后的版本,每个 Class 对应一个运行时常量池
- 字面量可以动态的添加(通过 String 类的
intern()
方法) - 符号引用可以被解析为直接引用
- 运行时常量池包含字符串常量池和其它运行时常量
- 字面量可以动态的添加(通过 String 类的
-
字符串常量池
- 字符串池在JDK 1.7 之后被分离到堆区
- 字符串常量池是 JVM 实例全局共享的,全局只有一个,存放的是字符串常量的引用值
- 字符串常量池中的字符串只存在一份!(字符串常量池的好处就是减少相同内容字符串的创建,节省内存空间)
- JDK1.6:运行时常量池、字符串常量池是在方法区内
- JDK1.8:元空间(metaspace)成为方法区的新实现,运行时常量池、字符串常量池被移动到堆中。
创建字符串对象方式:
-
直接赋值方式创建,对象是在字符串常量池
1
String str = "hello";
通过直接赋值方式创建字符串对象时,JVM 首先会对这个字符串进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回;如果不存在,则在字符串常量池中创建该字符串对象并且返回引用。
-
通过构造方法创建,字符串对象是在堆内存
1
2
3
4
5
6
7
8
9
10
11/**
* 在 new String 对象的时候,
* 1.如果所需字符串在创建对象之前,就在常量池中已经存在了,那么:
* 1.1 在堆里面创建一个对象,把字符串名指向这个对象;
* 1.2 把这个对象的成员变量 value[] 的值指向常量池中的对象。
* 2.如果所需字符串创建对象之前,在常量池中不存在:
* 2.1 在堆里面创建一个对象,把字符串名指向这个对象;
* 2.2 而常量池中没有值为所需字符串的对象,于是进行创建;
* 2.3 然后字符串名指向的对象的成员变量 value[] 指向常量池值为"hello"这个对象就行了。
*/
String str = new String("hello");通过构造方法来构造字符串对象时,不管字符串常量池中有没有相同内容的对象的引用,新的字符串对象都会创建。
- 使用构造方法进行 String 类对象实例化,产生的对象不会保存在对象池中,此对象无法重用。如果想将这个对象的引用加入到字符串常量池,只能通过手工入池方法完成:
public native String intern();
。intern()
方法用于在运行时将字符串添加到内部的字符串池中。调用intern()
后,首先检查字符串常量池中是否有该对象的引用,如果存在,则将这个引用返回给变量,否则将引用加入并返回给变量。
- 使用构造方法进行 String 类对象实例化,产生的对象不会保存在对象池中,此对象无法重用。如果想将这个对象的引用加入到字符串常量池,只能通过手工入池方法完成:
注意事项:
- 实际开发中都是采用直接赋值方式,并且不要频繁修改,因为会产生垃圾空间;
- 开发中,在比较字符串内容是否相同时,一般将常量写在前面,这样避免
equals()
前面的字符串内容为 null,equals()
方法本身具有 null 判断的功能。
String 类常用方法:
特别地,trim()
方法用于删除字符串的头部和尾部空白,不包括字符串中间的空白。
StringBuilder 和 StringBuffer 类
当对字符串进行修改的时候,需要使用 StringBuilder 和 StringBuffer 类。
和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuilder 和 StringBuffer 类的 API详见源码
- StringBuilder 和 StringBuffer 类的容量(capacity) 默认为 16,可以通过构造函数指定容量;如果通过构造函数传入的是字符串或是字符序列,那么 capacity 大小在字符串长度的基础上再加 16。
String、StringBuffer 和 StringBuilder 之间的区别:
-
String 为字符串常量;StringBuffer、StringBuilder 均为字符串变量。
-
执行速度比较:StringBuilder > StringBuffer > String(大多数情况下)
StringBuffer 和 StringBuilder 对象是变量,对变量操作就是对对象进行直接操作,不会进行创建,删除操作。因此在某些情况下 String 的执行速度是比较慢的,因为操作 String 对象时可能会有删除、新建操作。
-
在线程安全方面,StringBuffer 中很多方法都被 synchronized 关键字修饰,所以可以保证线程是安全的。
- 如果要进行的操作是多线程的,那么就要使用 StringBuffer;
- 但是在单线程的情况下,建议使用速度比较快的 StringBuilder。
总结:
- String 适合在少量字符串操作的情况下使用;
- StringBuilder 适合单线程下在字符缓冲区进行大量操作的情况;
- StringBuffer 适合多线程下在字符缓冲区进行大量操作的情况。
8. File 类
Java 文件类以抽象的方式代表文件名和目录路径名。该类主要用于文件和目录的创建、文件的查找和文件的删除等。
- File 对象代表磁盘中实际存在的文件和目录。
构造方法:
-
File(String pathname, int prefixLength)
-
File(String child, File parent)
-
File(String pathname)
通过将给定路径名字符串转换成抽象路径名来创建一个新 File 实例
-
File(String parent, String child)
根据 parent 路径名字符串和 child 路径名字符串创建一个新 File 实例
-
File(File parent, String child)
通过给定的父抽象路径名和子路径名字符串创建一个新的 File 实例
-
File(URI uri)
通过将给定的 file: URI 转换成一个抽象路径名来创建一个新的 File 实例
常用方法:
-
获取方法
-
public String getAbsolutePath()
— 返回 File 的绝对路径名字符串public String getPath()
— 将此 File 转换为路径名字符串,获取构造路径public String getName()
— 返回此 File 表示的文件或目录的名称public long length()
— 返回此 File 表示的文件的字节大小
-
-
判断方法
public boolean exists()
— 判断此 File 标识的文件或目录是否实际存在public boolean isDirectory()
— 判断此 File 标识的是否为文件夹public boolean isFile()
— 判断此 File 表示的是否为文件
-
创建和删除方法
public boolean creatNewFile()
— 当且仅当不存在该名称的文件时,创建一个新的空文件public boolean mkdir()
— 创建由 File 表示的目录public boolean mkdirs()
— 创建由 File 表示的目录,包括任何必须但不存在的父目录public boolean delete()
— 删除由此 File 表示的文件或目录(只能删除文件或者空文件夹,不能删除非空文件夹)
-
遍历目录方法
public String[] list()
— 返回一个 String 数组,表示该 File 目录中的所有子文件或目录的名称public File[] listFiles()
— 返回一个 File 数组,表示 File 目录中的所有子文件或目录的路径
注意事项:
- 一个 File 对象代表的是硬盘中的一个路径或者一个文件
- 无论该路径下是否存在文件或目录,都不影响 File 对象的创建