Java常用类库
一、StringBuffer类
String类很强大,但是其内容不允许修改,而StringBuffer类的出现可以实现字符串内容的修改处理。
StringBuffer包含以下主要的方法:
- 构造方法:StringBuffer()
- 构造方法:StringBuffer(String s)
- 追加:append(类型 变量)
- 插入数据:insert(int offset, 类型 变量)
- 删除指定范围数据:delete(int start, int end)
- 字符串内容反转:reverse()
public class StringBufferDemo {
public static void main(String[] args) {
StringBuffer buffer = new StringBuffer("hsafrg");
System.out.println("origin -> " + buffer);
buffer.append(" 6789");
System.out.println("append -> " + buffer);
buffer.insert(0, "insert");
System.out.println("insert -> " + buffer);
buffer.delete(0,1);
System.out.println("delete -> " + buffer);
buffer.reverse();
System.out.println("reverse -> " + buffer);
}
}
origin -> hsafrg
append -> hsafrg 6789
insert -> inserthsafrg 6789
delete -> nserthsafrg 6789
reverse -> 9876 grfashtresn
实际上,
- String里的“+”字符串连接符就等价于append()方法,所有的“+”在编译后都变成了append()方法;
- String类对象转换成StringBuffer类对象可以通过StringBuffer的构造方法或者append()方法实现
- 所有类的对象都可通过toString()方法转换成String类型。
二、StringBuilder类
StringBuilder和StringBuffer的功能类似,不同的是StringBuffer线程安全,所有的方法都使用了synchronized关键字进行标注,StringBuilder非线程安全。
因此可以概括一下String、StringBuilder及StringBuffer的区别:
- String类是字符串的首选类型,其内容不允许修改;
- StringBuilder和StringBuffer内容允许修改;
- StringBuffer线程安全,StringBuilder非线程安全。
三、CharSequence接口
字符序列CharSequence是一个描述字符串结构的接口,String、StringBuilder及StringBuffer三个类都实现了该接口,只要有字符串就可以为CharSequence接口实例化,只要看到CharSequence,她描述的就是一个字符串,CharSequence接口主要包含一下3个方法:
- 获取指定索引字符:charAt(int index)
- 获取字符串长度:int length()
- 截取部分字符串:subSequence(int start, int end)
public class CharSequenceDemo {
public static void main(String[] args) {
CharSequence str = "6464646";
System.out.println("length() -> "+ str.length());
System.out.println("charAt() -> "+ str.charAt(2));
System.out.println("subSequence() -> "+ str.subSequence(1,3));
}
}
length() -> 7
charAt() -> 6
subSequence() -> 46
四、Runtime类
Runtime类描述的是运行时状态,且是单例设计,要想获取实例化对象要依靠getRuntime()方法完成。它的实例化对象包含下面几个重要方法:
- availableProcessors():获取本机CPU内核数
- maxMemory():最大可用内存空间
- freeMemory():空闲内存空间
- totalMemory():可用内存空间
- gc():手动释放内存
public class RunTimeDemo {
public static void main(String[] args) {
Runtime run = Runtime.getRuntime();
System.out.println("availableProcessors -> " + run.availableProcessors());
System.out.println("maxMemory -> " + run.maxMemory());
System.out.println("freeMemory -> " + run.freeMemory());
System.out.println("totalMemory -> " + run.totalMemory());
}
}
availableProcessors -> 4
maxMemory -> 3808428032
freeMemory -> 252053712
totalMemory -> 257425408
以上方法代码不常见,但在jvm调优时很重要。
五、对象克隆 Cloneable
对象克隆指的是对象复制,即:使用已有的对象创建一个新的对象,属于全新的复制。需要用到Object类的clone()方法,且被克隆类必须实现Cloneable接口:
/**
* Cloneable接口使得对象具有拷贝功能
* 拷贝后改变原对象的值,看拷贝生成的新对象是否改变,若不改变就是深拷贝
* 这里是深拷贝
* Feng, Ge,2021年4月15日14:57:12
*/
public class CloneDemo {
public static void main(String[] args) throws CloneNotSupportedException {
A a = new A("A", 1);
A b = (A) a.clone();
System.out.println(a);
System.out.println(a.getName());
System.out.println(a.getNum());
System.out.println(b);
System.out.println(b.getName());
System.out.println(b.getNum());
System.out.println("===================================");
// 修改A的值,看B的值是否改变
a.setName("OK");
System.out.println(a);
System.out.println(a.getName());
System.out.println(a.getNum());
System.out.println(b);
System.out.println(b.getName());
System.out.println(b.getNum());
}
}
class A implements Cloneable{
private String name;
private int num;
public A(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return name;
}
public int getNum() {
return num;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
jdk.A@28d25987
A
1
jdk.A@4501b7af
A
1
===================================
jdk.A@28d25987
OK
1
jdk.A@4501b7af
A
1
可以看出,克隆后是一个新的对象,但内容一样。
但是这里两个属性都是值类型的,若有属性是引用类型的呢?如:
public class CloneDemo {
public static void main(String[] args) throws CloneNotSupportedException {
C c = new C("hhhh");
A a = new A("A", 1, c);
A b = (A) a.clone();
System.out.println(a);
System.out.println(a.getName());
System.out.println(a.getNum());
System.out.println(a.getC().getName());
System.out.println("===================================");
System.out.println(b);
System.out.println(b.getName());
System.out.println(b.getNum());
System.out.println(b.getC().getName());
System.out.println("=============改变值后=================");
// 修改A的值,看B的值是否改变
a.setName("OK");
c.setName("00000");
System.out.println(a);
System.out.println(a.getName());
System.out.println(a.getNum());
System.out.println(a.getC().getName());
System.out.println("===================================");
System.out.println(b);
System.out.println(b.getName());
System.out.println(b.getNum());
System.out.println(b.getC().getName());
}
}
class A implements Cloneable{
private String name;
private int num;
private C c;
public A(String name, int num, C c) {
this.name = name;
this.num = num;
this.c = c;
}
public String getName() {
return name;
}
public int getNum() {
return num;
}
public void setName(String name) {
this.name = name;
}
public C getC() {
return c;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class C {
private String name;
public C (String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
jdk.A@4501b7af
A
1
hhhh
===================================
jdk.A@523884b2
A
1
hhhh
=============改变值后=================
jdk.A@4501b7af
OK
1
00000
===================================
jdk.A@523884b2
A
1
00000
可以看到,值类型的属性未变化,引用类型的则变化了。
也就是说,对于这个 super.clone()
方法,若字段是值类型的,则是深拷贝,若字段是引用类型,则是浅拷贝。本文后面会单独讨论。
六、Math类
Math类主要进行数学计算操作,提供基础的计算公式,所有的方法都是static修饰的,均可通过类名直接调用。
public class MathDemo {
public static void main(String[] args) {
System.out.println(Math.abs(-11.66));
System.out.println(Math.max(-11.66, 5.6));
System.out.println(Math.log(5.6));
System.out.println(Math.round(5.5));
System.out.println(Math.round(-5.5));
System.out.println(Math.round(-5.51));
}
}
11.66
5.6
1.7227665977411035
6
-5
-6
七、BigInteger和BigDecimal大数字操作类
面对大数字时,如下:
一种处理方法是把这样的大数当成String类型来处理,另外就是用BigInteger和BigDecimal两个专用的类来处理。
BigInteger numA = new BigInteger("666666666666666666666");
BigInteger numB = new BigInteger("666666666666666666666");
System.out.println(numA.add(numB));
System.out.println(numA.subtract(numB));
System.out.println(numA.multiply(numB));
System.out.println(numA.divide(numB));
1333333333333333333332
0
444444444444444444443555555555555555555556
1
在实际开发中,如计算没有超过基本数据类型的范围,尽量使用基本数据类型进行处理,BigInteger或BigDecimal会使得性能变差。
另外,不能以为用了BigDecimal后,计算结果就一定精确。此话怎讲?
首先,阿里有一个关与BigDecimal的规范:
1、为什么说double不精确?
因为计算机采用二进制处理数据,但是很多小数,如0.1的二进制是一个无线循环小数,而这种数字在计算机中是无法精确表示的。
所以,人们采用了一种通过近似值的方式在计算机中表示,于是就有了单精度浮点数和双精度浮点数等。作为单精度浮点数的float和双精度浮点数的double,在表示小数的时候只是近似值,并不是真实值。
所以,当使用BigDecimal(Double)创建一个的时候,得到的BigDecimal是损失了精度的。而使用一个损失了精度的数字进行计算,得到的结果也是不精确的。
想要避免这个问题,可以通过BigDecimal(String)的方式创建BigDecimal,这样的情况下,0.1就会被精确的表示出来。其表现形式是一个无标度数值1,和一个标度1的组合。
2、BigDecimal是如何保证精确的?
实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。
八、TimerTask和Timer定时任务类
public class TimerTest {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TaskTest(), 100, 1000);
}
}
class TaskTest extends TimerTask{
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}
1575448673410
1575448674411
1575448675412
1575448676412
1575448677413
1575448678414
1575448679414
1575448680415
1575448681416
1575448682417
1575448683418
1575448684419
九、深拷贝、浅拷贝
假设B复制了A,修改A的时候,看B是否发生变化:
- 如果B跟着也变了,说明是浅拷贝(修改堆内存中的同一个值)
- 如果B没有改变,说明是深拷贝(修改堆内存中的不同的值)
浅拷贝:
- 1、当类的成员变量是基本数据类型时,浅拷贝会复制该属性的值赋值给新对-象。
- 2、当成员变量是引用数据类型时,浅拷贝复制的是引用数据类型的地址值。这种情况下,当拷贝出的某一个类修改了引用数据类型的成员变量后,会导致所有拷贝出的类都发生改变。
浅拷贝的方式有两种:通过拷贝构造方法实现浅拷贝、通过重写clone()方法进行浅拷。
方式一通过拷贝构造方法实现浅拷贝
/* 拷贝构造方法实现浅拷贝 */
public class CopyConstructor {
class Person{
//两个属性值:分别代表值传递和引用传递
private Age age;
private String name;
public Person(Age age,String name) {
this.age=age;
this.name=name;
}
//拷贝构造方法
public Person(Person p) {
this.name=p.name;
this.age=p.age;
}
}
class Age{
private int age;
public Age(int age) {
this.age=age;
}
}
public static void main(String[] args) {
Age a=new Age(20);
Person p1=new Person(a,"ok");
Person p2=new Person(p1);
System.out.println("p1是"+p1);
System.out.println("p2是"+p2);
//修改p1的各属性值,观察p2的各属性值是否跟随变化
p1.setName("小傻瓜");
a.setAge(99);
System.out.println("修改后的p1是"+p1);
System.out.println("修改后的p2是"+p2);
}
p1是ok 20
p2是ok 20
修改后的p1是小傻瓜 99
修改后的p2是ok 99
p1值传递部分的属性值发生变化时,p2不会随之改变;
p1引用传递部分属性值发生变化时,p2也随之改变。
这里的Age a对象是p1和p2共有的,所以无论谁操作a使之变化,都会影响另一个,这符合浅拷贝的含义。
要注意:
如果在拷贝构造方法中,对引用数据类型变量逐一开辟新的内存空间,创建新的对象,也可以实现深拷贝。
而对于一般的拷贝构造,则一定是浅拷贝。
方式二通过重写clone()方法进行浅拷
Object类有一个方法为 clone() 方法可以实现浅拷贝。
但是需要注意:
1、clone() 方法是受保护的(被protected修饰),无法直接使用。
2、使用clone() 方法的类必须实现Cloneable接口,否则会抛出异常CloneNotSupportedException。
对于这两点,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。
直接调用super.clone()实现的clone方法全部都是浅拷贝。
/*
* 创建学生类,实现Cloneable接口
*/
class Student implements Cloneable{
//学生类的成员变量(属性),其中一个属性为类的对象
private String name;
private Age aage;
private int length;
//构造方法,其中一个参数为另一个类的对象
public Student(String name,Age a,int length) {
this.name=name;
this.aage=a;
this.length=length;
}
//重写Object类的clone方法
public Object clone() {
Object obj=null;
//调用Object类的clone方法,返回一个Object实例
try {
obj= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
public static void main(String[] args) {
Age a=new Age(20);
Student stu1=new Student("ok",a,175);
//通过调用重写后的clone方法进行浅拷贝
Student stu2=(Student)stu1.clone();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
}
姓名是: ok, 年龄为: 20, 长度是: 175
姓名是: ok, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: ok, 年龄为: 99, 长度是: 175
深拷贝:
深拷贝不仅会复制成员变量为基本数据类型的值,给新对象。还会给是引用数据类型的成员变量申请储存空间,并复制引用数据类型成员变量的对象。这样拷贝出的新对象就不怕修改了是引用数据类型的成员变量后,对其它拷贝出的对象造成影响了。
深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
因为创建内存空间和拷贝整个对象图,所以深拷贝相比于浅拷贝速度较慢并且花销较大。
一、通过重写clone方法来实现深拷贝
与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝,来实现深拷贝。
class Age implements Cloneable{
private int age;
public Age(int age) {
this.age=age;
}
//重写Object的clone方法
public Object clone() {
Object obj=null;
try {
obj=super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return obj;
}
}
class Student implements Cloneable{
private String name;
private Age aage;
private int length;
//构造方法,其中一个参数为另一个类的对象
public Student(String name,Age a,int length) {
this.name=name;
this.aage=a;
this.length=length;
}
//重写Object类的clone方法
public Object clone() {
Object obj=null;
//调用Object类的clone方法——浅拷贝
try {
obj= super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
//调用Age类的clone方法进行深拷贝
//先将obj转化为学生类实例
Student stu=(Student)obj;
//学生类实例的Age对象属性,调用其clone方法进行拷贝
stu.aage=(Age)stu.getaAge().clone();
return obj;
}
}
public static void main(String[] args) {
Age a=new Age(20);
Student stu1=new Student("ok",a,175);
//通过调用重写后的clone方法进行浅拷贝
Student stu2=(Student)stu1.clone();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
System.out.println();
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a.setAge(99);
//stu1.setaAge(new Age(99)); 使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
stu1.setLength(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
}
姓名是: ok, 年龄为: 20, 长度是: 175
姓名是: ok, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: ok, 年龄为: 20, 长度是: 175
二、通过对象序列化实现深拷贝
虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。
将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。
class Age implements Serializable{
//年龄类的成员变量(属性)
private int age;
//构造方法
public Age(int age) {
this.age=age;
}
}
class Student implements Serializable{
private String name;
private Age aage;
private int length;
//构造方法,其中一个参数为另一个类的对象
public Student(String name,Age a,int length) {
this.name=name;
this.aage=a;
this.length=length;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
Age a=new Age(20);
Student stu1=new Student("ok",a,175);
//通过序列化方法实现深拷贝
ByteArrayOutputStream bos=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bos);
oos.writeObject(stu1);
oos.flush();
ObjectInputStream ois=new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
Student stu2=(Student)ois.readObject();
System.out.println(stu1.toString());
System.out.println(stu2.toString());
System.out.println();
//尝试修改stu1中的各属性,观察stu2的属性有没有变化
stu1.setName("大傻子");
//改变age这个引用类型的成员变量的值
a.setAge(99);
stu1.setLength(216);
System.out.println(stu1.toString());
System.out.println(stu2.toString());
}
姓名是: ok, 年龄为: 20, 长度是: 175
姓名是: ok, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: ok, 年龄为: 20, 长度是: 175
可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。
十、LocalDate和Date的区别
LocalDate 代表日期;
LocalTime表示时刻,类似11:23这样的时刻;
LocalDateTime就是 日期+时间
- java.util.Date 和 SimpleDateFormatter 都不是线程安全的,而 LocalDate 、LocalTime 和最基本的 String 一样,是不变类型,不但线程安全,而且不能修改。
- java.util.Date 月份从0开始,一月是0,十二月是11。java.time.LocalDate 月份和星期都改成了 enum ,就不可能再用错了。
- java.util.Date是一个“万能接口”,它包含日期、时间,还有毫秒数。如果你只需要日期或时间那么有一些数据就没啥用。在新的Java 8中,日期和时间被明确划分为LocalDate和LocalTime,LocalDate无法包含时间,LocalTime无法包含日期。当然,LocalDateTime才能同时包含日期和时间。
- java.util.Date 推算时间(比如往前推几天/ 往后推几天/ 计算某年是否闰年/ 推算某年某月的第一天、最后一天、第一个星期一等等)要结合Calendar要写好多代码,相当麻烦。