7.4.2 Calendar类

因为Date类在设计上存在一些缺陷,所以Java提供了Calendar类来更好地处理日期和时间Calendar是一个抽象类,它用于表示日历。

推荐使用公历

历史上有着许多种纪年方法,它们的差异实在太大了,为了统一计时,全世界通常选择最普及、最通用的日历:Gregorian Calendar,也就是日常介绍年份时常用的”公元几几年”。

公历GregorianCalendar类

Calendar类本身是一个抽象类,它是所有日历类的模板,并提供了一些所有日历通用的方法;但它本身不能直接实例化,程序只能创建Calendar子类的实例,Java本身提供了一个GregorianCalendar类,个代表格里高利日历的子类,它代表了通常所说的公历.

可以知己扩展Calendar类

当然,也可以创建自己的Calendar子类,然后将它作为Calendar对象使用(这就是多态)。在Internet上,也有对中国农历的实现。本章不会详细介绍如何扩展Calendar子类,读者可以查看上述Calendar的源码来学习。

通过getInstance方法创建Calendar实例

Calendar类是一个抽象类,所以不能使用构造器来创建Calendar对象。但它提供了几个静态getInstance()方法来获取Calendar对象,这些方法根据TimeZone, Locale类来获取特定的Calendar,如果不指定TimeZoneLocale,则使用默认的TimeZoneLocale来创建Calendar

Calendar和Date的相互转换

CalendarDate都是表示日期的工具类,它们直接可以自由转换,如下代码所示:

通过Calendar的getTime方法直接取出Date

通过Calendar实例的getTime方法即可取得Date对象:

1
2
3
4
//创建一个默认的 Calendar对象
Calendar calendar=Calendar.getInstance();
/从 Calendar对象中取出Date对象
Date date=calendar.getTime();

先创建Calendar实例,再通过setTime方法将Date设置到Calendar实例中

因为Calendar/GregorianCalendar没有构造函数可以接收Date对象,所以必须先获得一个calendar实例,然后调用其setTime(Date date)方法将Date对象中的时间传给Calendar实例.

1
2
3
//通过Date对象获得对应的Calendar对象
Calendar calendar=Calendar.getInstance();
calendar. setTime(new Date());

Calendar类常用方法

Calendar类提供了大量访问、修改日期时间的方法,常用方法如下:

方法 描述
void add(int field, int amount) 根据日历的规则,为给定的日历字段添加或减去指定的时间量。
int get(int field) 返回指定日历字段的值。
int getActualMaximum(int field) 返回指定日历字段可能拥有的最大值。例如月,最大值为11
int getActualMinimum(int field) 返回指定日历字段可能拥有的最小值。例如月,最小值为0。
void roll( int field, int amount) add()方法类似,区别在于加上amount后超过了该字段所能表示的最大范围时,**也不会向上一个字段进位**。
void set(int field, int value) 将给定的日历字段设置为给定值。
roid set(int year, int month, int date) 设置Calendar对象的年、月、日三个字段的值。
void set(int year, int month, int date, int hourOfDay, int minute, int second) 设置Calendar对象的年、月、日、时、分、秒6个字段的值。

上面的很多方法都需要一个int类型的field参数,fieldCalendar类的类变量,如:Calendar.YEARCalendar.MONTH等分别代表了年、月、日、小时、分钟、秒等时间字段。
需要指出的是, Calendar.MONTH字段代表月份,月份的起始值不是1,而是0,所以要设置8月时,用7而不是8

如下程序示范了Calendar类的常规用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取今天的日历
Calendar c = Calendar.getInstance();
// 取出年
System.out.println(c.get(YEAR));
// 取出月份
System.out.println(c.get(MONTH));
// 取出日
System.out.println(c.get(DATE));
System.out.println("-------------------------");
// 分别设置年、月、日、小时、分钟、秒
c.set(2003, 10, 23, 12, 32, 23); // 2003-11-23 12:32:23
System.out.println(c.getTime());
// 将Calendar的年前推1年
c.add(YEAR, -1); // 2002-11-23 12:32:23
System.out.println(c.getTime());
System.out.println("-------------------------");
// 将Calendar的月前推8个月
c.roll(MONTH, -8); // 2002-03-23 12:32:23
System.out.println(c.getTime());

Calendar类还有如下几个注意点。

1. add与roll的区别

add方法

add(int field, int amount)的功能非常强大,add主要用于改变Calendar的特定字段的值。

  • 如果需要增加某字段的值,则让amount为正数;
  • 如果需要减少某字段的值,则让amount为负数即可

add方法的规则

add(int field, int amount)有如下两条规则。

  1. 当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大.
    1
    2
    3
    4
    5
    6
    System.out.println("-------------------------");
    Calendar cal1 = Calendar.getInstance();
    cal1.set(2003, 7, 23, 0, 0, 0); // 2003-8-23
    System.out.println(cal1.getTime());
    cal1.add(MONTH, 6); // 2003-8-23 => 2004-2-23
    System.out.println(cal1.getTime());
  2. 如果下一级字段也需要改变,那么该字段会修正到变化最小的值.
    1
    2
    3
    4
    5
    6
    System.out.println("-------------------------");
    Calendar cal2 = Calendar.getInstance();
    cal2.set(2003, 7, 31, 0, 0, 0); // 2003-8-31
    // 因为进位到后月份改为2月,2月没有31日,自动变成29日
    cal2.add(MONTH, 6); // 2003-8-31 => 2004-2-29
    System.out.println(cal2.getTime());

对于上面的例子,8-31就会变成2-29。因为MONTH的下一级字段是DATE,从31到29改变最小。所以上面2003-8-31的MONTH字段增加6后,不是变成2004-3-2,而是变成2004-2-29。

roll方法的规则

roll()的规则与add()的处理规则不同:当被修改的字段超出它允许的范围时,上一级字段不会增大

1
2
3
4
5
6
System.out.println("-------------------------");
Calendar cal3 = Calendar.getInstance();
cal3.set(2003, 7, 23, 0, 0, 0); // 2003-8-23
// MONTH字段“进位”,但YEAR字段并不增加
cal3.roll(MONTH, 6); // 2003-8-23 => 2003-2-23
System.out.println(cal3.getTime());

下一级字段的处理规则与add()方法相似:

1
2
3
4
5
6
7
System.out.println("-------------------------");
Calendar cal4 = Calendar.getInstance();
cal4.set(2003, 7, 31, 0, 0, 0); // 2003-8-31
// MONTH字段“进位”后变成2,2月没有31日,
// YEAR字段不会改变,2003年2月只有28天
cal4.roll(MONTH, 6); // 2003-8-31 => 2003-2-28
System.out.println(cal4.getTime());

2. 设置Calendar的容错性

调用Calendar对象的set()方法来改变指定时间字段的值时,有可能传入一个不合法的参数

setLenient方法

Calendar提供了一个setLeniente(false)用于设置它的容错性, Calendar默认支持较好的容错性,通过setLenient(false)可以关闭Calendar的容错性,让它进行严格的参数检查。

两种解释日历的模式

Calendar有两种解释日历字段的模式:lenient模式和non-lenient模式。
Calendar处于lenient模式时,每个时间字段可接受超出它允许范围的值;
Calendar处于non-lenient模式时,如果为某个时间字段设置的值超出了它允许的取值范围,程序将会抛出异常。

例如为MONTH字段设置13,这将会导致怎样的后果呢?看如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;
import static java.util.Calendar.*;

public class LenientTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
// 结果是YEAR字段加1,MONTH字段为1(二月)
cal.set(MONTH, 13); // ①
System.out.println(cal.getTime());

// 关闭容错性
cal.setLenient(false);
// 导致运行时异常
cal.set(MONTH, 13); // ②
System.out.println(cal.getTime());
}
}

上面程序①②两处的代码完全相似,但它们运行的结果不一样:
①处代码可以正常运行,因为设置MONTH字段的值为13,将会导致YEAR字段加1;
②处代码将会导致运行时异常,因为设置的MONTH字段值超出了MONTH字段允许的范围。

3. set()方法延迟修改

set(field, value)方法将日历字段field更改为value,此外它还设置了一个内部成员变量,以指示日历字段field已经被更改。尽管日历字段field是立即更改的,但Calendar所代表的时间却不会立即修改,直到下次调用get()getTime()getTimeInMillis()add()roll()时才会重新计算日历的时间。这被称为set方法的延迟修改,采用延迟修改的优势是多次调用set()不会触发多次不必要的计算。

下面程序演示了set()方法延迟修改的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.*;
import static java.util.Calendar.*;

public class LazyTest {
public static void main(String[] args) {
Calendar cal = Calendar.getInstance();
cal.set(2003, 7, 31); // 2003-8-31
// 将月份设为9,但9月31日不存在。
// 如果立即修改,系统将会把cal自动调整到10月1日。
cal.set(MONTH, 8);

// 下面代码输出10月1日
System.out.println(cal.getTime()); //①

// 设置DATE字段为5
cal.set(DATE, 5); // ②
System.out.println(cal.getTime()); // ③
}
}

上面程序中创建了代表2003-8-31的Calendar对象,当把这个对象的MONTH字段加1后应该得到2003-10-1(因为9月没有31日),如果程序在①号代码处输出当前Calendar里的日期,也会看到输出2003-10-1,③号代码处将输出2003-10-5

如果程序将①处代码注释起来,因为Calendarset()方法具有延迟修改的特性,即调用set()方法后Calendar实际上并未计算真实的日期,它只是使用内部成员变量表记录MONTH字段被修改为8,接着程序设置DATE字段值为5,程序内部再次记录DATE字段为5—就是9月5日,因此看到③处输出2003-9-5。

7.4 日期 时间类

Java原本提供了DateCalendar用于处理日期、时间的类,包括创建日期、时间对象,获取系统当前日期、时间等操作。但Date不仅无法实现国际化,而且它对不同属性也使用了前后矛盾的偏移量,比如月份与小时都是从0开始的,月份中的天数则是从1开始的,年又是从1900开始的。
java util.Calendar则显得过于复杂,从下面介绍中会看到传统Java对日期、时间处理的不足。Java8吸取了Joda-Time库(一个被广泛使用的日期、时间库)的经验,提供了一套全新的日期时间库.

7.4.1 Date类

Java提供了Date类来处理日期、时间(此处的Date是指java.util包下的Date类,而不是java.sql包下的Date类),Date对象既包含日期,也包含时间。Date类从JDK1.0起就开始存在了,但正因为它历史悠久,所以它的大部分构造器、方法都已经过时,不再推荐使用了。

Date类还可以使用的构造器

Date类提供了6个构造器,其中4个已经Deprecated,也就是Java不再推荐使用了,剩下的两个构造器如下:

构造器 描述
Date() 生成一个代表当前日期时间的Date对象。该构造器在底层调用System.currentTimeMillis()获得long整数作为日期参数.
Date(long date) 根据指定的long型整数来生成一个Date对象。该构造器的参数表示创建的Date对象和GMT1970年1月1日00:00:00之间的时间差,以毫秒作为计时单位。

Date类中还可以使用的方法

Date构造器相同的是,Date对象的大部分方法也Deprecated了,剩下为数不多的几个方法:

方法 描述
boolean after(Date when) 测试该日期是否在指定日期when之后.
boolean before(Date when) 测试该日期是否在指定日期when之前。
long getTime() 返回该时间对应的long型整数,即从GMT1970-01-01 00:00:00到该Date对象之间的时间差,以毫秒作为计时单位。
void setTime(long time) 设置该Date对象的时间

Date实例

1
2
3
4
5
6
7
8
9
10
11
import java.util.Date;

public class DateTest {
public static void main(String[] args) {
Date date1 = new Date();
Date date2 = new Date(System.currentTimeMillis() + 100);
System.out.println(date2);
System.out.println(date1.compareTo(date2));
System.out.println(date2.after(date1));
}
}

运行结果

1
2
3
Fri Oct 04 20:36:19 CST 2019
-1
true

尽量少用Date类

总体来说,Date是一个设计相当糟糕的类,因此Java官方推荐尽量少用Date的构造器和方法。
如果需要对日期、时间进行加减运算,或获取指定时间的年、月、日、时、分、秒信息,可使用Calendar工具类

7.3.6 BigDecimal类

floatdouble这两个基本类型的浮点数容易引起精度丢失.

为了能精确表示、计算浮点数,Java提供了BigDecimal类,该类提供了大量的构造器用于创建BigDecimal对象,包括把所有的基本数值型变量转换成一个BigDecimal对象,也包括利用数字字符串数字字符数组来创建BigDecimal对象。

因为double存在精度问题,所以调用BigDecimal类是构造器时中应该传入字符串值,不要传入double值。

如果必须使用double浮点数作为BigDecimal构造器的参数时,不要直接将该double浮点数作为构造器参数来创建BigDecimal对象,而是应该通过BigDecimal.valueOf(double value)静态方法来创建BigDecimal对象

BigDecimal类提供了add()subtract()multiply()divide()pow()等方法对精确浮点数进行常规算术运算。

实例

下面程序示范了BigDecimal的基本运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.math.*;

public class BigDecimalTest
{
public static void main(String[] args)
{
BigDecimal f1 = new BigDecimal("0.05");
BigDecimal f2 = BigDecimal.valueOf(0.01);

BigDecimal f3 = new BigDecimal(0.05);
System.out.println("使用String作为BigDecimal构造器参数:");
System.out.println("0.05 + 0.01 = " + f1.add(f2));
System.out.println("0.05 - 0.01 = " + f1.subtract(f2));
System.out.println("0.05 * 0.01 = " + f1.multiply(f2));
System.out.println("0.05 / 0.01 = " + f1.divide(f2));

System.out.println("使用double作为BigDecimal构造器参数:");
System.out.println("0.05 + 0.01 = " + f3.add(f2));
System.out.println("0.05 - 0.01 = " + f3.subtract(f2));
System.out.println("0.05 * 0.01 = " + f3.multiply(f2));
System.out.println("0.05 / 0.01 = " + f3.divide(f2));
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
使用String作为BigDecimal构造器参数:
0.05 + 0.01 = 0.06
0.05 - 0.01 = 0.04
0.05 * 0.01 = 0.0005
0.05 / 0.01 = 5
使用double作为BigDecimal构造器参数:
0.05 + 0.01 = 0.06000000000000000277555756156289135105907917022705078125
0.05 - 0.01 = 0.04000000000000000277555756156289135105907917022705078125
0.05 * 0.01 = 0.0005000000000000000277555756156289135105907917022705078125
0.05 / 0.01 = 5.000000000000000277555756156289135105907917022705078125

创建BigDecimal对象时,一定要使用String对象作为构造器参数,而不是直接使用double数字。

如何对double做精确的运算

如果程序中要求对double浮点数进行加、减、乘、除基本运算,则需要:

  • 先将double类型数值包装成BigDecimal对象,
  • 然后调用BigDecimal对象的方法执行运算
  • 最后将结果转换成double型变量。

这是比较烦琐的过程,可以考虑以BigDecimal为基础定义一个Arith工具类来进行运行.

该工具类代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

import java.math.*;

public class Arith {
// 默认除法运算精度
private static final int DEF_DIV_SCALE = 10;

// 构造器私有,让这个类不能实例化
private Arith() {
}

// 提供精确的加法运算。
public static double add(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}

// 提供精确的减法运算。
public static double sub(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}

// 提供精确的乘法运算。
public static double mul(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}

// 提供(相对)精确的除法运算,当发生除不尽的情况时.
// 精确到小数点以后10位的数字四舍五入。
public static double div(double v1, double v2) {
BigDecimal b1 = BigDecimal.valueOf(v1);
BigDecimal b2 = BigDecimal.valueOf(v2);
return b1.divide(b2, DEF_DIV_SCALE, RoundingMode.HALF_UP).doubleValue();
}

public static void main(String[] args) {
System.out.println("0.05 + 0.01 = " + Arith.add(0.05, 0.01));
System.out.println("1.0 - 0.42 = " + Arith.sub(1.0, 0.42));
System.out.println("4.015 * 100 = " + Arith.mul(4.015, 100));
System.out.println("123.3 / 100 = " + Arith.div(123.3, 100));
}
}

运行上面程序将看到如下运行结果:

1
2
3
4
0.05 + 0.01 = 0.06
1.0 - 0.42 = 0.58
4.015 * 100 = 401.5
123.3 / 100 = 1.233

7.3.5 Java7的ThreadLocalRandom与Randon

Random类专门用于生成一个伪随机数,它有两个构造器:

  1. 一个构造器使用默认的种子(以当前时间作为种子),
  2. 另一个构造器需要程序员显式传入一个long型整数的种子

ThreadLocalRandom类是Java7新增的一个类,它是Random的增强版。在并发访问的环境下,使用ThreadLocalRandom来代替Random可以减少多线程资源竞争,最终保证系统具有更好的线程安全性。
ThreadLocalRandom类的用法与Random类的用法基本相似,它提供了一个静态current()方法来获取ThreadLocalRandom对象,获取该对象之后即可调用各种nextXxx()方法来获取伪随机数了。

ThreadLocalRandomRandom都比Mathrandom方法提供了更多的方式来生成各种伪随机数:

  • 可以生成浮点类型的伪随机数,
  • 也可以生成整数类型的伪随机数,
  • 还可以指定生成随机数的范围。

Random类实例

关于Random类的用法如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.*;

public class RandomTest {
public static void main(String[] args) {
// 使用当前时间作为种子
Random rand = new Random();
// 生成随机的boolean值
System.out.println("rand.nextBoolean():" + rand.nextBoolean());

byte[] buffer = new byte[16];
rand.nextBytes(buffer);
System.out.println("rand.nextBytes(buffer):"+Arrays.toString(buffer));

// 生成0.0~1.0之间的伪随机double数
System.out.println("rand.nextDouble():" + rand.nextDouble());

// 生成0.0~1.0之间的伪随机float数
System.out.println("rand.nextFloat():" + rand.nextFloat());

// 生成平均值是 0.0,标准差是 1.0的 伪高斯数
System.out.println("rand.nextGaussian():" + rand.nextGaussian());

// 生成一个处于int整数取值范围的 伪随机整数
System.out.println("rand.nextInt():" + rand.nextInt());

// 生成0~26之间的伪随机整数
System.out.println("rand.nextInt(26):" + rand.nextInt(26));

// 生成一个处于long整数取值范围的伪随机整数
System.out.println("rand.nextLong():" + rand.nextLong());
}
}

运行效果1:

1
2
3
4
5
6
7
8
rand.nextBoolean():true
rand.nextBytes(buffer):[-6, 11, 108, 120, 126, -13, 84, -56, 45, 114, -62, -98, 113, 85, 89, 113]
rand.nextDouble():0.8120795682241748
rand.nextFloat():0.5804569
rand.nextGaussian():-1.0469886892688502
rand.nextInt():589045688
rand.nextInt(26):2
rand.nextLong():-474418649996311808

运行效果2:

1
2
3
4
5
6
7
8
rand.nextBoolean():true
rand.nextBytes(buffer):[31, -120, -74, -13, 32, -55, 3, 0, 39, -47, -122, -61, 113, -113, -45, 102]
rand.nextDouble():0.6114733221402615
rand.nextFloat():0.4678058
rand.nextGaussian():-1.1066057114192767
rand.nextInt():1796032384
rand.nextInt(26):13
rand.nextLong():5259734743396927359

Random类如何获取大于等于N小于等于M之间的随机数

RandomnextInt方法只能传入一个参数,只能获取大于等于0小于N之间的随机整数.如果要获取大于等于N,小于M的随机整数需要通过数学计算来完成.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Random;

public class RandomIntN2MTest {

public static void main(String[] args) {
Random random = new Random();
int times = 20;
for (int i = 0; i < times; i++) {
// 生成[0,2)之间的随机整数,也就是[0,1]之间的随机整数
System.out.print(random.nextInt(2) + ",");
}
System.out.println();
int min = 2, max = 4;
for (int i = 0; i < times; i++) {
// 生成[min,max)之间的随机正整数
System.out.print((random.nextInt(max - min) + min) + ",");
}
System.out.println();
for (int i = 0; i < times; i++) {
// 生成[min,max]之间的随机正整数
System.out.print((random.nextInt(max - min + 1) + min) + ",");
}
}
}
1
2
3
0,1,0,0,0,0,1,0,0,0,1,1,1,1,0,1,0,1,1,0,
3,3,3,3,3,3,2,3,3,2,3,2,3,3,3,2,3,3,3,3,
3,4,4,2,3,2,2,2,3,3,4,2,2,2,3,4,2,4,4,2,

Random对象的种子相同则生成的随机数也相同

Random使用一个48位的种子,如果这个类的两个实例是用同一个种子创建的,对它们以同样的顺序调用方法,则它们会产生相同的数字序列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

import java.util.*;

public class SeedTest {
public static void main(String[] args) {
Random r1 = new Random(50);
System.out.println("第一个种子为50的Random对象");
System.out.println("r1.nextBoolean():\t" + r1.nextBoolean());
System.out.println("r1.nextInt():\t\t" + r1.nextInt());
System.out.println("r1.nextDouble():\t" + r1.nextDouble());
System.out.println("r1.nextGaussian():\t" + r1.nextGaussian());
System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%%");

Random r2 = new Random(50);
System.out.println("第二个种子为50的Random对象");
System.out.println("r2.nextBoolean():\t" + r2.nextBoolean());
System.out.println("r2.nextInt():\t\t" + r2.nextInt());
System.out.println("r2.nextDouble():\t" + r2.nextDouble());
System.out.println("r2.nextGaussian():\t" + r2.nextGaussian());
System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%%");

Random r3 = new Random(100);
System.out.println("种子为100的Random对象");
System.out.println("r3.nextBoolean():\t" + r3.nextBoolean());
System.out.println("r3.nextInt():\t\t" + r3.nextInt());
System.out.println("r3.nextDouble():\t" + r3.nextDouble());
System.out.println("r3.nextGaussian():\t" + r3.nextGaussian());
System.out.println("%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
}
}

运行上面程序,看到如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
第一个种子为50的Random对象
r1.nextBoolean(): true
r1.nextInt(): -1727040520
r1.nextDouble(): 0.6141579720626675
r1.nextGaussian(): 2.377650302287946
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
第二个种子为50的Random对象
r2.nextBoolean(): true
r2.nextInt(): -1727040520
r2.nextDouble(): 0.6141579720626675
r2.nextGaussian(): 2.377650302287946
%%%%%%%%%%%%%%%%%%%%%%%%%%%%
种子为100的Random对象
r3.nextBoolean(): true
r3.nextInt(): -1139614796
r3.nextDouble(): 0.19497605734770518
r3.nextGaussian(): 0.6762208162903859
%%%%%%%%%%%%%%%%%%%%%%%%%%%%

种子相同随机数相同

只要两个Randon对象的种子相同,而且方法的调用顺序也相同,它们就会产生相同的数字序列
也就是说, Random产生的数字并不是真正随机的,而是一种伪随机。
为了避免两个Random对象产生相同的数字序列,通常推荐使用当前时间作为Random对象的种子

如下代码所示:
Random rand=new Random(System.currentTimeMillis());
或者直接Random rand=new Random(),因为系统会默认使用当前时间作为种子.

ThreadLocalRandom

在多线程环境下使用ThreadLocalRandom的方式与使用Random基本类似

如下程序片段示范了ThreadLocalRandom的用法。

1
2
3
ThreadLocalRandom rand=ThreadLocalRandom.current();
// 获取[4,20)之间的随机整数
int vall=rand.nextInt(4,20);

ThreadLocalRandom的nextInt​

ThreadLocalRandomnextInt方法比Random提供的nextInt要强,
ThreadLocalRandom来提供了nextInt(int min, int max)方法来获取大于等于min,小于max的方法.通过ThreadLocalRandom的这个方法我们可以很方便的获取大于等于N小于等于M之间的随机正整数.

7.3.4 Math类

Java提供了基本的(+)、减(-)、乘(*)、除(/)、求余(%)等基本算术运算的运算符,但对于更复杂的数学运算,例如,三角函数、对数运算、指数运算等则无能为力。Java提供了Math工具类来完成这些复杂的运算,Math类是一个工具类,它的构造器被定义成private的,因此无法创建Math类的对象;Math类中的所有方法都是类方法,可以直接通过类名来调用它们。Math类除提供了大量静态方法之外,还提供了两个类变量:PIE,它们的值分别等于πe

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class MathTest {

public static void main(String[] args) {
/*---------下面是三角运算---------*/
// 将弧度转换角度
System.out.println("Math.toDegrees(1.57):" + Math.toDegrees(1.57));
// 将角度转换为弧度
System.out.println("Math.toRadians(90):" + Math.toRadians(90));

// 计算反余弦,返回的角度范围在 0.0 到 pi 之间。
System.out.println("Math.acos(1.2):" + Math.acos(1.2));
// 计算反正弦;返回的角度范围在 -pi/2 到 pi/2 之间。
System.out.println("Math.asin(0.8):" + Math.asin(0.8));
// 计算反正切;返回的角度范围在 -pi/2 到 pi/2 之间。

System.out.println("Math.atan(2.3):" + Math.atan(2.3));
// 计算三角余弦。
System.out.println("Math.cos(1.57):" + Math.cos(1.57));
// 计算值的双曲余弦。
System.out.println("Math.cosh(1.2 ):" + Math.cosh(1.2));
// 计算正弦
System.out.println("Math.sin(1.57 ):" + Math.sin(1.57));
// 计算双曲正弦
System.out.println("Math.sinh(1.2 ):" + Math.sinh(1.2));
// 计算三角正切
System.out.println("Math.tan(0.8 ):" + Math.tan(0.8));
// 计算双曲正切
System.out.println("Math.tanh(2.1 ):" + Math.tanh(2.1));

// 将矩形坐标 (x, y) 转换成极坐标 (r, thet));
System.out.println("Math.atan2(0.1, 0.2):" + Math.atan2(0.1, 0.2));

/*---------下面是取整运算---------*/
// 取整,返回小于目标数的最大整数。
System.out.println("Math.floor(-1.2 ):" + Math.floor(-1.2));
// 取整,返回大于目标数的最小整数。
System.out.println("Math.ceil(1.2):" + Math.ceil(1.2));
// 四舍五入取整
System.out.println("Math.round(2.3 ):" + Math.round(2.3));

/*---------下面是乘方、开方、指数运算---------*/
// 计算平方根。
System.out.println("Math.sqrt(2.3 ):" + Math.sqrt(2.3));
// 计算立方根。
System.out.println("Math.cbrt(9):" + Math.cbrt(9));
// 返回欧拉数 e 的n次幂。
System.out.println("Math.exp(2):" + Math.exp(2));
// 返回 sqrt(x2 +y2)
System.out.println("Math.hypot(4 , 4):" + Math.hypot(4, 4));
// 按照 IEEE 754 标准的规定,对两个参数进行余数运算。
System.out.println("Math.IEEEremainder(5 , 2):" + Math.IEEEremainder(5, 2));
// 计算乘方
System.out.println("Math.pow(3, 2):" + Math.pow(3, 2));
// 计算自然对数
System.out.println("Math.log(12):" + Math.log(12));
// 计算底数为 10 的对数。
System.out.println("Math.log10(9):" + Math.log10(9));
// 返回参数与 1 之 和 的自然对数。
System.out.println("Math.log1p(9):" + Math.log1p(9));

/*---------下面是符号相关的运算---------*/
// 计算绝对值。
System.out.println("Math.abs(-4.5):" + Math.abs(-4.5));
// 符号赋值,返回带有第二个浮点数符号的第一个浮点参数。
System.out.println("Math.copySign(1.2, -1.0):" + Math.copySign(1.2, -1.0));
// 符号函数;如果参数为 0,则返回 0;如果参数大于 0,
// 则返回 1.0;如果参数小于 0,则返回 -1.0。
System.out.println("Math.signum(2.3):" + Math.signum(2.3));

/*---------下面是大小相关的运算---------*/
// 找出最大值
System.out.println("Math.max(2.3 , 4.5):" + Math.max(2.3, 4.5));
// 计算最小值
System.out.println("Math.min(1.2 , 3.4):" + Math.min(1.2, 3.4));
// 返回第一个参数和第二个参数之间与第一个参数相邻的浮点数。
System.out.println("Math.nextAfter(1.2, 1.0):" + Math.nextAfter(1.2, 1.0));
// 返回比目标数略大的浮点数
System.out.println("Math.nextUp(1.2 ):" + Math.nextUp(1.2));
// 返回一个伪随机数,该值大于等于 0.0 且小于 1.0。
System.out.println("Math.random():" + Math.random());
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Math.toDegrees(1.57):89.95437383553926
Math.toRadians(90):1.5707963267948966
Math.acos(1.2):NaN
Math.sqrt(2.3 ):1.51657508881031
Math.cbrt(9):2.080083823051904
Math.exp(2):7.38905609893065
Math.hypot(4 , 4):5.656854249492381
Math.IEEEremainder(5 , 2):1.0
Math.pow(3, 2):9.0
Math.log(12):2.4849066497880004
Math.log10(9):0.9542425094393249
Math.log1p(9):2.302585092994046
Math.abs(-4.5):4.5
Math.copySign(1.2, -1.0):-1.2
Math.signum(2.3):1.0
Math.max(2.3 , 4.5):4.5
Math.min(1.2 , 3.4):1.2
Math.nextAfter(1.2, 1.0):1.1999999999999997
Math.nextUp(1.2 ):1.2000000000000002
Math.random():0.06276607672087764

7.3.3 Java9改进的String StringBuffer和StringBuilder类

字符串就是一连串的字符序列,Java提供了StringStringBufferStringBuilder三个类来封装字符串,并提供了一系列方法来操作字符串对象。

String介绍

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改的,直至这个对象被销毁。

StringBuffer介绍

StringBuffer对象则代表一个字符序列可变的字符串,当一个String Buffer被创建以后,通过String Buffer提供的append()insert()reverse()setCharAt()setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象.
StringBuilder类是JDK1.5新增的类,它也代表可变字符串对象。实际上, StringBuilderStringBuffer基本相似,两个类的构造器和方法也基本相同。

StringBuilder和StringBuffer的不同

StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。
因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用StringBuilder,需要考虑线程安全的使用StringBuffer类。

CharSequence接口介绍

StringStringBuilderStringBuffer都实现了CharSequence接口,因此CharSequence可认为是一个字符串的协议接口。

java9对字符串类的改进

Java9改进了字符串的实现(包括StringStringBufferStringBuilder)。

  • Java9以前字符串采用char数组来保存字符,因此字符串的每个字符占2字节;
  • Java9的字符串采用byte数组再加一个encoding-flag字段来保存字符,因此字符串的每个字符只占1字节

所以Java9的字符串更加节省空间,但字符串的功能方法没有受到任何影响。

String API详解

String类提供了大量构造器来创建String对象,其中如下几个有特殊用途。

String类构造器

构造器 描述
String() 创建一个包含0个字符串序列的String对象.
String(byte[] bytes, Charset charset) 使用指定的字符集将指定的byte数组解码成一个新的String对象。
String(byte[] bytes, int offset,int length) 使用平台的默认字符集将指定的byte数组从offset位置开始、取出长度为length的子数组,然后解码成一个新的String对象。
String(byte[] bytes, int offset, int length, String charsetName) 使用指定的字符集将指定的byte数组从offset开始、取出长度为length的子数组,然后解码成一个新的String对象。
String(char[] value, int offset, int count) 将指定的字符数组从offset开始、取出长度为count的字符元素,然后连缀成字符串。
String(String original) 根据字符串直接量来创建一个String对象。也就是说,新创建的String对象是该参数字符串的副本
String(StringBuffer buffer) 根据StringBuffer对象来创建对应的String对象。
String(StringBuilder builder) 根据StringBuilder对象来创建对应的String对象。

String类方法

String类也提供了大量方法来操作字符串对象,下面详细介绍这些常用方法

根据下标获取一个字符

方法 描述
char charAt(int index) 获取字符串中指定位置的字符。其中,参数index指的是字符串的序数,字符串的序数从0开始到length()-1

复制字符序列到字符数组的方法

方法 描述
void getChars(int srcBegin, int srcEnd,char[] dst, int dstBegin) 该方法将字符串中从srcBegin开始,到srcEnd结束的字符复制到dst字符数组中,其中dstBegin为目标字符数组的起始复制位置。

获取字符长度的方法

方法 描述
int length() 返回当前字符串长度。

比较是否相同的方法

方法 描述
boolean contentEquals(StringBuffer sb) 将该String对象与StringBuffer对象进行比较,当它们包含的字符序列相同时返回true
boolean equals(Object anObject) 将该字符串与指定对象比较,如果二者包含的字符序列相等则返回true;否则返回false
boolean equalsIgnoreCase(String str) 与前一个方法基本相似,只是忽略字符的大小写。

生成新字符串的方法

方法 描述
String concat(String anotherString) 将该String对象与anotherString连接在一起。与Java提供的字符串连接运算符"+"的功能相同。
String replace(char oldChar, char newChar) 将字符串中的第一个oldChar替换成newChar.
String substring(int beginIndex) 获取从beginIndex位置开始到结束的子字符串。
String substring(int beginIndex, int endIndex) 获取从beginIndex位置开始到endIndex位置的子字符串。

静态方法

方法 描述
static String valueOf(X x) 一系列用于将基本类型值转换为String对象的方法。
static String copyValueOf(char[] data) 将字符数组连缀成字符串,与构造器String(char[] content)的功能相同
static String copyValueof(char[] data, int offset, int count) char数组的子数组中的元素连缀成字符串,与构造器String(char[] value, int offset, int count)的功能相同。

比较大小方法

方法 描述
int compareTo(String anotherString) 比较两个字符串的大小。
  • 如果两个字符串的字符序列相等,则返回0;
  • 如果两个字符串的字符序列不相等,则从两个字符串第0个字符开始比较,返回第一个不相等的字符差
  • 如果较长字符串的前面部分恰巧是较短的字符串,则返回它们的长度差

查找字符出现位置的方法

方法 描述
int indexOf(int ch) 找出ch字符在该字符串中第一次出现的位置.
int indexOf(int ch, int fromIndex) 找出ch字符在该字符串中从fromIndex开始后第一次出现的位置。
int indexOf(String string) 找出string子字符串在该字符串中第一次出现的位置。
int indexOf(String string, int fromIndex) 找出string子字符串在该字符串中从fromIndex开始后第一次出现的位置。
int lastIndexOf( int ch) 找出ch字符在该字符串中最后一次出现的位置.
int lastIndexOf(int ch, int fromIndex) 找出ch字符在该字符串中从fromIndex开始后最后一次出现的位置
int lastIndexOf(String str) 找出str子字符串在该字符串中最后一次出现的位置。
int lastIndexOf(String str,int fromIndex) 找出str子字符串在该字符串中从fromIndex开始后最后次出现的位置。

大小写转换方法

方法 描述
String toLowerCase() 将字符串转换成小写。
String toUpperCase() 将字符串转换成大写。

转成字节数组或字符数组的方法

方法 描述
byte[] getBytes() 将该String对象转换成byte数组。
char[] toCharArray() 将该String对象转换成char数组。

判断是否带前缀或后缀

方法 描述
boolean startsWith(String prefix) String对象是否以prefix开始。
boolean startsWith(String prefix, int offset) String对象从offset位置算起,是否以prefix开始。
boolean endswith(String suffix) 返回该Sing对象是否以suffix结尾。

StringBuffer和StringBuilder

String类是不可变的, String的实例一旦生成就不会再改变了,使用字符串会产生字符串直接量,通过连接符+也会产生字符串直接量。
因为String是不可变的,所以会额外产生很多临时变量,使用StringBufferStringBuilder就可以避免这个问题。
StringBuilder提供了一系列插入追加改变该字符串里包含的字符序列的方法。而StringBuffer与其用法完全相同,只是**StringBuffer是线程安全的**。

StringBuilder和StringBuffer的属性

StringBuilderStringBuffer有两个属性:lengthcapacity,其中

  1. length属性表示其包含的字符序列的长度。与String对象的length不同的是, StringBuilderStringBufferlength是可以改变的,可以通过lengthsetLength( int len)方法来访问和修改其字符序列的长度。
  2. capacity属性表示StringBuilder容量, capacity通常比length大,程序通常无须关心capacity属性。如下程序示范了StringBuilder类的用法。
  • StringBuilderlength方法返回其字符序列的长度,
  • StringBuildercapacity()方法的返回值是容量,容量比length返回值大。

StringBuilder类有追加插入替换删除,反转等操作,这些操作可以改变了StringBuilder里的字符序列。

StringBuilder方法详解

追加方法

方法 描述
StringBuilder append(boolean b) 追加布尔参数到StringBuilder中
StringBuilder append(char c) 追加char参数到StringBuilder中
StringBuilder append(char[] str) Appends the string representation of the char array argument to this sequence.
StringBuilder append(char[] str, int offset, int len) Appends the string representation of a subarray of the char array argument to this sequence.
StringBuilder append(CharSequence s) Appends the specified character sequence to this Appendable.
StringBuilder append(CharSequence s, int start, int end) Appends a subsequence of the specified CharSequence to this sequence.
StringBuilder append(double d) Appends the string representation of the double argument to this sequence.
StringBuilder append(float f) Appends the string representation of the float argument to this sequence.
StringBuilder append(int i) Appends the string representation of the int argument to this sequence.
StringBuilder append(long lng) Appends the string representation of the long argument to this sequence.
StringBuilder append(Object obj) Appends the string representation of the Object argument.
StringBuilder append(String str) Appends the specified string to this character sequence.
StringBuilder append(StringBuffer sb) Appends the specified StringBuffer to this sequence.
StringBuilder appendCodePoint(int codePoint) Appends the string representation of the codePoint argument to this sequence.

删除方法

方法 描述
StringBuilder delete(int start, int end) Removes the characters in a substring of this sequence.
StringBuilder deleteCharAt(int index) Removes the char at the specified position in this sequence.

插入方法

方法 描述
StringBuilder insert(int offset, boolean b) Inserts the string representation of the boolean argument into this sequence.
StringBuilder insert(int offset, char c) Inserts the string representation of the char argument into this sequence.
StringBuilder insert(int offset, char[] str) Inserts the string representation of the char array argument into this sequence.
StringBuilder insert(int index, char[] str, int offset, int len) Inserts the string representation of a subarray of the str array argument into this sequence.
StringBuilder insert(int dstOffset, CharSequence s) Inserts the specified CharSequence into this sequence.
StringBuilder insert(int dstOffset, CharSequence s, int start, int end) Inserts a subsequence of the specified CharSequence into this sequence.
StringBuilder insert(int offset, double d) Inserts the string representation of the double argument into this sequence.
StringBuilder insert(int offset, float f) Inserts the string representation of the float argument into this sequence.
StringBuilder insert(int offset, int i) Inserts the string representation of the second int argument into this sequence.
StringBuilder insert(int offset, long l) Inserts the string representation of the long argument into this sequence.
StringBuilder insert(int offset, Object obj) Inserts the string representation of the Object argument into this character sequence.
StringBuilder insert(int offset, String str) Inserts the string into this character sequence.

查找下标方法

方法 描述
int indexOf(String str) Returns the index within this string of the first occurrence of the specified substring.
int indexOf(String str, int fromIndex) Returns the index within this string of the first occurrence of the specified substring, starting at the specified index.
int lastIndexOf(String str) Returns the index within this string of the rightmost occurrence of the specified substring.
int lastIndexOf(String str, int fromIndex) Returns the index within this string of the last occurrence of the specified substring.

获取或设置字符方法

方法 描述
char charAt(int index) Returns the char value in this sequence at the specified index.
void setCharAt(int index, char ch) The character at the specified index is set to ch.

容量和长度相关方法

方法 描述
int capacity() Returns the current capacity.
int length() Returns the length (character count).
void setLength(int newLength) Sets the length of the character sequence.
void ensureCapacity(int minimumCapacity) Ensures that the capacity is at least equal to the specified minimum.

截取子串方法

方法 描述
String substring(int start) Returns a new String that contains a subsequence of characters currently contained in this character sequence.
String substring(int start, int end) Returns a new String that contains a subsequence of characters currently contained in this sequence.
CharSequence subSequence(int start, int end) Returns a new character sequence that is a subsequence of this sequence.

反转方法

方法 描述
StringBuilder reverse() Causes this character sequence to be replaced by the reverse of the sequence.

替换方法

方法 描述
StringBuilder replace(int start, int end, String str) Replaces the characters in a substring of this sequence with characters in the specified String.

重写的Object方法

方法 描述
String toString() Returns a string representing the data in this sequence.

代码点相关方法

方法 描述
int offsetByCodePoints(int index, int codePointOffset) Returns the index within this sequence that is offset from the given index by codePointOffset code points.
int codePointAt(int index) Returns the character (Unicode code point) at the specified index.
int codePointBefore(int index) Returns the character (Unicode code point) before the specified index.
int codePointCount(int beginIndex, int endIndex) Returns the number of Unicode code points in the specified text range of this sequence.

其他方法

方法 描述
void trimToSize() Attempts to reduce storage used for the character sequence.
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) Characters are copied from this sequence into the destination character array dst.

StringBuilderString之间区别

  • StringBuilder的字符序列是可变的。
  • String的是不可变的

7.3.2 Java7新增的Objects类

Java7新增了一个Objects工具类,它提供了一些工具方法来操作对象,这些工具方法大多是”空指针”安全的。
比如你不能确定一个引用变量是否为null,如果贸然地调用该变量的toString方法,则可能引发NullPointerException异常;但如果使用Objects类提供的toString(Object object)方法,就不会引发空指针异常,当objectnull时,程序将返回一个"null"字符串。

java工具类命名习惯

Java为工具类的命名习惯是添加一个字母s,比如

  • 操作数组的工具类是Arrays,
  • 操作集合的工具类是Collections

测试变量是否非null的方法

Objects提供的requireNonNull()方法,该方法主要用来对方法形参进行输入校验

  • 当传入的参数不为null时,该方法返回参数本身;
  • 否则将会引发NullPointerException异常。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Objects;

public class ObjectsTest {
// 定义一个obj变量,它的默认值是null
static ObjectsTest obj;

public static void main(String[] args) {
// 输出一个null对象的hashCode值,输出0
System.out.println(Objects.hashCode(obj));
// 输出一个null对象的toString,输出null
System.out.println(Objects.toString(obj));
// 要求obj不能为null,如果obj为null则引发异常
System.out.println(Objects.requireNonNull(obj, "obj参数不能是null!"));
}
}

运行结果:

1
2
3
4
5
0
null
Exception in thread "main" java.lang.NullPointerException: obj参数不能是null!
at java.util.Objects.requireNonNull(Objects.java:228)
at ObjectsTest.main(ObjectsTest.java:13)

7.3 常用类

本节将介绍Java提供的一些常用类,如StringMathBigDecimal等的用法

7.3.1 Object类

Object类是所有数组枚举类的父类,也就是说,Java允许把任何类型的对象赋给Object类型的变量。

当定义一个类时没有使用extends关键字为它显式指定父类,则该类默认继承Object父类。

因为所有的Java类都是Object类的子类,所以任何Java对象都可以调用Object类的方法。

Object类常用方法

Object类提供了如下几个常用方法

常用方法

方法 描述
boolean equals(Object obj) 判断指定对象与该对象是否相等。如果两个对象是同一个对象,则Object类认为这两个对象相等。因此该equals方法通常没有太大的实用价值。
int hashCode() 返回该对象的hash Code值。在默认情况下, Object类的hashCode()方法根据该对象的地址来计算(即与System.identityHashCode(Object x)方法的计算结果相同)。但很多类都重写了Object类的hashCode()方法,不再根据地址来计算其hashCode()方法值。
String toString() 返回该对象的字符串表示,当程序使用System.out.printIn()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。 Object类的toString方法返回"运行时类名@十六进制hashCode值"格式的字符串,但很多类都重写了Object类的toString方法,用于返回可以表述该对象信息的字符串.

垃圾回收相关方法

方法 描述
protected void finalize() 当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源

反射相关方法

方法 描述
Class<?> getClass() 返回该对象的运行时类。

Object中进程相关的方法

除此之外, Object类还提供了wait()notify()notifyAll()几个方法,通过这几个方法可以控制线程的暂停和运行。

克隆方法

Java还提供了一个protected修饰的clone()方法,该方法用于帮助其他对象来实现”自我克隆”,所谓”自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。由于Object类提供的clone方法使用了protected修饰,因此该方法只能被子类重写或调用。

如何支持克隆

自定义类实现”克隆”的步骤如下。
自定义类实现Cloneable接口。Cloneable接口是一个标记性的接口,接口里没有定义任何方法,实现该接口的对象可以实现”自我克隆”。
自定义类实现自己的clone()方法。
实现clone()方法时通过super.clone();调用Object实现的clone方法来得到该对象的副本,并返回该副本。

Object类的clone方法只克隆值或地址

Object类提供的Clone机制只对对象的实例变量进行”简单复制”。

  • 如果对象实例变量的类型是基本类型,则直接复制值即可.
  • 如果对象实例变量的类型是引用类型,则只是复制这个引用变量中保存的地址.

这样被克隆对象的引用类型的实例变量,与克隆得到的对象的引用类型的实例变量指向的是内存中的同一个实例

只是浅克隆

所以,Object类的clone方法只是一种”浅克隆”。它只克隆该对象的所有成员变量值,不会对引用类型的成员变量值所引用的对象进行克隆

如果开发者需要对对象进行”深克隆”’则需要开发者自己进行”递归”克隆,保证所有引用类型的成员变量值所引用的对象都被复制了。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Address {
String detail;

public Address(String detail) {
this.detail = detail;
}
}

// 实现Cloneable接口
class User implements Cloneable {
int age;
Address address;

public User(int age) {
this.age = age;
address = new Address("花果山水帘洞");
}

// 通过调用super.clone()来实现clone()方法
public User clone() throws CloneNotSupportedException {
return (User) super.clone();
}
}

public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
User u1 = new User(29);
// clone得到u1对象的副本。
User u2 = u1.clone();
// 不是相同的对象
System.out.println(u1 == u2);
// 只复制成员变量的值,不产生新对象,所以两个实例变量是同一个对象的引用
System.out.println(u1.address == u2.address);
}
}

运行结果:

1
2
false
true

7.2.2 Runtime类与Java9的ProcessHandle

Runtime类代表Java程序的运行时环境,每个Java程序都有一个与之对应的Runtime实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime实例,但可以通过getRuntime()方法获取与之关联的Runtime对象。

垃圾回收方法

System类似的是, Runtime类也

  • 提供gc()方法来通知系统进行垃圾回收方法
  • 提供runFinalization()方法用来通知系统调用finalize()方法来进行资源清理

加载文件和加载动态链接库方法

Runtime类提供load(String fileName)loadLibrary(String libName)方法来加载文件加载动态链接库

访问JVM相关信息

Runtime类代表Java程序的运行时环境,可以访问JVM的相关信息,如处理器数量内存信息

1
2
3
4
5
6
7
8
9
10
public class RuntimeTest {
public static void main(String[] args) {
// 获取Java程序关联的运行时对象
Runtime rt = Runtime.getRuntime();
System.out.println("处理器数量:" + rt.availableProcessors());
System.out.println("空闲内存数:" + rt.freeMemory());
System.out.println("总内存数:" + rt.totalMemory());
System.out.println("可用最大内存数:" + rt.maxMemory());
}
}

运行结果:

1
2
3
4
处理器数量:4
空闲内存数:126927264
总内存数:128974848
可用最大内存数:1884815360

启动进程运行命令

Runtime类还可以直接单独启动一个进程来运行操作系统的命令Runtime提供了一系列exec()方法来运行操作系统命令。

实例

启动Windows系统里的”记事本”程序。

1
2
3
4
5
6
7
public class ExecTest {
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
// 运行记事本程序
rt.exec("notepad.exe");
}
}

运行效果:记事本被打开

获取exec启动的进程信息

获取Process对象

通过exec启动平台上的命令之后,它就变成了一个进程,javaProcess来代表进程,可以使用Process来接收exec方法的返回值。
Java9还新增了一个ProcessHandle接口,通过该接口可获取进程的ID父进程后代进程;通过该接口的onExit()方法可在进程结束时完成某些行为.
ProcessHandle还提供了一个ProcessHandle.Info内部类,用于获取进程的命令、参数、启动时间、累计运行时间、用户等信息。

如何获取ProcessHandle对象

通过Process对象的方法toHandle()可以取得ProcessHandle对象,然后就可以通过ProcessHandle对象来获取进程相关信息。

下面程序示范了通过ProcessHandle获取进程的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

import java.util.concurrent.*;

public class ProcessHandleTest {
public static void main(String[] args) throws Exception {
Runtime rt = Runtime.getRuntime();
// 运行记事本程序
Process p = rt.exec("notepad.exe");
ProcessHandle ph = p.toHandle();
System.out.println("进程是否运行: " + ph.isAlive());
System.out.println("进程ID: " + ph.pid());
System.out.println("父进程: " + ph.parent());
// 获取ProcessHandle.Info信息
ProcessHandle.Info info = ph.info();
// 通过ProcessHandle.Info信息获取进程相关信息
System.out.println("进程命令: " + info.command());
System.out.println("进程参数: " + info.arguments());
System.out.println("进程启动时间: " + info.startInstant());
System.out.println("进程累计运行时间: " + info.totalCpuDuration());
// 通过CompletableFuture在进程结束时运行某个任务
CompletableFuture<ProcessHandle> cf = ph.onExit();
cf.thenRunAsync(() -> {
System.out.println("程序退出");
});
Thread.sleep(5000);
}
}

实例 执行进程并返回进程的输出

被调用的进程

下面创建一个PrintArgs程序,用来给其他进程调用.

1
2
3
4
5
6
7
8
9
10
11
12
13
package system.test;

public class PrintArgs {
public static void main(String args[]) {
// 获取类名
String simpleName = PrintArgs.class.getSimpleName();
System.out.println("----- " + simpleName + " start -----------------------------------");
for (int i = 0; i < args.length; i++) {
System.out.println("[args-" + i + "]:" + args[i]);
}
System.out.println("----- " + simpleName + " end -----------------------------------");
}
}

编译 打包 测试

先在当前目录下编译这个java文件:

1
E:\dev2\idea_workspace\Test\src\system\test>javac -d . -encoding utf-8 PrintArgs.java

然后打包成可执行jar包:

1
2
3
4
5
E:\dev2\idea_workspace\Test\src\system\test>jar cvfe PrintArgs.jar system.test.PrintArgs system
已添加清单
正在添加: system/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: system/test/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: system/test/PrintArgs.class(输入 = 1094) (输出 = 620)(压缩了 43%)

接着,查看jar包是否打包正确

1
2
3
4
5
6
E:\dev2\idea_workspace\Test\src\system\test>jar tf PrintArgs.jar
META-INF/
META-INF/MANIFEST.MF
system/
system/test/
system/test/PrintArgs.class

最后,运行这个可执行jar包

1
2
3
4
5
6
7
8
E:\dev2\idea_workspace\Test\src\system\test>java -jar PrintArgs.jar 1 2 3
----- PrintArgs start -----------------------------------
工作目录:E:\dev2\idea_workspace\Test\src\system\test
[args-0]:1
[args-1]:2
[args-2]:3
----- PrintArgs end -----------------------------------

调用进程

可以看到通过命令java -jar PrintArgs.jar 1 2 3可以正确运行这个jar包,接下来就是,通过Process来执行这个命令。
创建Process有如下两种方式:

  1. 通过Runtime.exec("cmd命令")来运行一个进程.
  2. 通过ProcessBuilder对象的start方法来运行一个进程.

现在

工具类 运行进程并返回进程的标准输出

注意获取输出的时候要注意控制台的编码,我的控制台默认编码是gbk.如果是其他编码则需要指定编码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package system.test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
* 进程执行器.
*/
public class ProcessRunner {
/**
* 运行cmd命令,默认cmd的编码为gbk.
*
* @param commandStr cmd命令.cmd命令以空格作为分隔符,第一个参数表示要执行的程序,后面的的参数是该程序的命令行参数.
* @return 程序的标准输出.
*/
public String runProcess(String commandStr) {
return runProcess(commandStr, "gbk");
}

/**
* @param commandStr cmd命令.cmd命令以空格作为分隔符,第一个参数表示要执行的程序,后面的的参数是该程序的命令行参数.
* @param cmdEncoding cmd的编码.
* @return 程序的标准输出.
*/
public String runProcess(String commandStr, String cmdEncoding) {
BufferedReader br = null;
StringBuilder sb = new StringBuilder();
try {
Process p = Runtime.getRuntime().exec(commandStr);
br = new BufferedReader(new InputStreamReader(p.getInputStream(), cmdEncoding));
String line;
while ((line = br.readLine()) != null) {
sb.append(line.concat("\n"));
}
// 等待进程运行结束.
int exitCode = p.waitFor();
// 如果进程的返回值不是0,则表明进程执行失败.
if (exitCode != 0)
// 返回null表示程序执行失败.
return null;

} catch (Exception e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return sb.toString();
}

/**
* 执行进程生成器中的程序和参数,并返回程序的输出.默认cmd的编码为gbk.
*
* @param processBuilder
* @return 程序执行的结果字符串.
*/
public String runProcess(ProcessBuilder processBuilder) {
return runProcess(processBuilder, "gbk");
}

/**
* 执行进程生成器中的程序和参数,并返回程序的输出.
*
* @param processBuilder 进程生成器.进程生成器中存放了要执行的程序,该程序的参数,该程序的工作空间等.
* @param cmdEncoding cmd的编码.程序将按照这个编码来读取程序的标准输出。
* @return 程序执行的结果字符串.
*/
public String runProcess(ProcessBuilder processBuilder, String cmdEncoding) {
Process process = null;
StringBuffer sb = new StringBuffer();
try {
process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), cmdEncoding));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line + "\n");
}
// 等待进程运行结束
int exitCode = process.waitFor();
// 进程结束了,关闭接收流.
reader.close();
// 如果线程返回值不是0则表示线程执行失败.
if (exitCode != 0)
return null;

} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return sb.toString();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package system.test;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class ProcessTest {
public static void main(String[] args) {
// 测试通过Runable来执行一个进程.
testRuntime();

// 测试通过ProcessBuilder来执行一个进程.
testProcessBuilder();
}

private static void testRuntime() {
// Runtime启动的进程的工作目录和父进程一样
String jarPath = "E:\\dev2\\idea_workspace\\Test\\src\\system\\test\\PrintArgs.jar";
System.out.println(new ProcessRunner().runProcess("java -jar " + jarPath + " 1 2 3"));
}

private static void testProcessBuilder() {
List<String> command = new ArrayList<>();
command.add("java");
command.add("-jar");
command.add("PrintArgs.jar");
command.add("1");
command.add("2");
command.add("3");
// java -jar PrintArgs.jar 1 2 3
ProcessBuilder processBuilder = new ProcessBuilder(command);
// ProcessBuilder方法启动的进程可以指定工作目录,如果不指定则和父进程的工作目录相同
processBuilder.directory(new File("E:\\dev2\\idea_workspace\\Test\\src\\system\\test"));
String processOutput = new ProcessRunner().runProcess(processBuilder);
System.out.println(processOutput);
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
----- PrintArgs start -----------------------------------
工作目录:E:\dev2\idea_workspace\Test
[args-0]:1
[args-1]:2
[args-2]:3
----- PrintArgs end -----------------------------------
----- PrintArgs start -----------------------------------
工作目录:E:\dev2\idea_workspace\Test\src\system\test
[args-0]:1
[args-1]:2
[args-2]:3
----- PrintArgs end -----------------------------------

Runtime方式和ProcessBuilder方式的对比

ProcessBuilder设置命令比较方便

  • Runtime方式需要把要指定的cmd命令,全部写在一个字符串里,如果命令比较长则容易出现错误.
  • ProcessBuilder方式设置命令比较简单,命令写在List中或者字符串变参中,写起来比较方便,不容易出现错误.

ProcessBuilder可以指定工作目录

  • Runtime方式的工作目录默认与父进程相同,运行程序时,一般需要输入程序的绝对路径,比较繁琐.
  • ProcessBuilder方式可以指定工作目录,这样可执行程序的路径可以使用相对路径

JDK1.5之后推荐使用ProcessBuilder方式来运行进程

7.2 系统相关

Java程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java提供了System类和Runtime类来与程序的运行平台进行交互。

7.2.1 System类

System类代表当前Java程序的运行平台,程序不能创建System类的对象, System类提供了一些类变量类方法,允许直接通过System类来调用这些类变量和类方法。
System类提供了代表标准输入标准输出错误输出类变量,并提供了一些静态方法用于访问环境变量系统属性的方法,还提供了加载文件动态链接库的方法。

加载文件和动态链接库

加载文件和动态链接库主要对native方法有用,对于一些特殊的功能(如访问操作系统底层硬件设备等)Java程序无法实现,必须借助C语言来完成,此时需要使用C语言Java方法提供实现。

其实现步骤如下:

  1. Java程序中声明native修饰的方法,类似于abstract方法,只有方法签名,没有实现。编译该Java程序,生成一个class文件。
  2. javah编译第1步生成的class文件,将产生一个.h文件
  3. 写一个cpp文件实现native方法,这一步需要包含第2步产生的.h文件(这个.h文件中又包含了JDK带的jni.h文件)
  4. Java中用System类的loadLibrary()方法或Runtime类的loadLibrary()方法加载第4步产生的动态链接库文件,Java程序中就可以调用这个native方法了。

访问环境变量和系统属性

下面程序通过System类来访问操作的环境变量系统属性

  1. 通过调用System类的getenv()方法可以访问程序所在平台的环境变量
  2. 通过getProperties()getProperty()方法可以访问程序所在平台的系统属性.

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

import java.io.*;
import java.util.*;

public class SystemTest {
public static void main(String[] args) throws Exception {
// 获取系统所有的环境变量
Map<String, String> env = System.getenv();
// 遍历输出所有的环境遍历
for (String name : env.keySet()) {
System.out.println(name + " ---> " + env.get(name));
}
// 获取指定的环境变量
System.out.println("-----------------------------------------------");
String[] envKeys = { "JAVA_HOME", "CLASSPATH" };
for (int i = 0; i < envKeys.length; i++) {
System.out.println(envKeys[i] + " ---> " + System.getenv(envKeys[i]));
}
System.out.println("-----------------------------------------------");

// 获取所有的系统属性
Properties props = System.getProperties();
// 将所有系统属性保存到props.txt文件中
props.store(new FileOutputStream("props.txt"), "System Properties");
// 输出特定的系统属性
System.out.println(System.getProperty("os.name"));
}
}

控制台部分输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
......
M2_HOME ---> D:\dev\apache-maven-3.5.4
ProgramFiles ---> C:\Program Files
......
OS ---> Windows_NT
......
TEMP ---> C:\Users\lan\AppData\Local\Temp
......
-----------------------------------------------
JAVA_HOME ---> E:\Java8\Java\jdk1.8.0_131
CLASSPATH ---> .;E:\Java8\Java\jdk1.8.0_131\lib\dt.jar;E:\Java8\Java\jdk1.8.0_131\lib\toos.jar;E:\dev2\AspectJ1.9.4\lib\aspectjrt.jar;E:\dev2\junit\junit-4.12.jar;
-----------------------------------------------
Windows 10

生成的props.txt文件部分内容:

1
2
3
4
5
6
7
8
9
10
11
#System Properties
#Mon Sep 30 13:34:28 CST 2019
java.runtime.name=Java(TM) SE Runtime Environment
sun.boot.library.path=E\:\\Java8\\Java\\jdk1.8.0_131\\jre\\bin
java.vm.version=25.131-b11
java.vm.vendor=Oracle Corporation
java.vendor.url=http\://java.oracle.com/
.......
sun.desktop=windows
sun.cpu.isalist=amd64

垃圾回收相关方法

System

  • 提供gc()方法用来通知系统进行垃圾回收,
  • 提供runFinalization()方法用来通知系统调用finalize()方法来进行资源清理

获取当前时间的方法

currentTimeMillis()nanoTime(),它们都返回一个long整数。实际上它们都返回当前时间与UTC1970年1月1日 0点整的时间差,前者以毫秒作为单位,后者以纳秒作为单位。

必须指出的是,这两个方法返回的时间粒度取决于底层操作系统,可能所在的操作系统根本不支持以毫秒、纳秒作为计时单位。
例如,许多操作系统以几十毫秒为单位测量时间,这种情况下currentTimeMillis()方法不可能返回精确的毫秒数:
nanoTime()方法很少用,因为大部分操作系统都不支持使用纳秒作为计时单位

标准输入输出相关的属性和方法

除此之外, System类的inouterr分别代表系统的标准输入标准输出错误输出流,其中,标准输入通常是键盘,标准输出通常是显示器

提供了setIn()setOut()setErr()方法来改变系统的标准输入、标准输出和标准错误输出流。

identityHashCode方法

System类还提供了一个identityHashCode(Object x)方法,该方法返回指定对象的精确hashCode值,也就是根据该对象的地址计算得到的hashCode值。

不同对象的identityHashCode值一定不同

当某个类的hashCode()方法被重写后,该类实例的hashCode()方法就不能唯一地标识该对象;但通过identityHashCode()方法返回的hashCode值,依然是根据该对象的地址计算得到的hashCode值。不同的对象在内存中的地址不可能相同,所以,如果两个对象的identityHashCode值相同,则两个对象绝对是同一个对象

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class IdentityHashCodeTest {
public static void main(String[] args) {
// 下面程序中s1和s2是两个不同对象
String s1 = new String("Hello");
String s2 = new String("Hello");
// String重写了hashCode()方法——改为根据字符序列计算hashCode值,
// 因为s1和s2的字符序列相同,所以它们的hashCode方法返回值相同
System.out.println(s1.hashCode() + "----" + s2.hashCode());

// s1和s2是不同的字符串对象,所以它们的identityHashCode值不同
System.out.println(System.identityHashCode(s1) + "----" + System.identityHashCode(s2));
// 相同的字符串直接量放在常量池中,是同一个对象
String s3 = "Java";
String s4 = "Java";

// s3和s4是相同的字符串对象,所以它们的identityHashCode值相同
System.out.println(System.identityHashCode(s3) + "----" + System.identityHashCode(s4));
}
}

运行结果:

1
2
3
69609650----69609650
366712642----1829164700
2018699554----2018699554