封面来源:碧蓝航线 穹顶下的圣咏曲 活动CG

参考视频:尚硅谷 宋红康 Java 零基础教程 P450 - P494

1. String

1.1 String 的特性

String 类:代表字符串。Java 程序中的所有字符串字面值(如 “abc” )都作为此类的实例实现。

String 是一个 final 类,代表不可变的字符序列,不可被继承。

字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。

String 对象的字符内容是存储在一个字符数组 value[] 中的。

1
2
3
4
5
6
7
8
9
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; // 用于存储字符串数据

/** Cache the hash code for the string */
private int hash; // Default to 0
// ......
}

理解不可变性

1
2
3
4
5
6
7
8
9
10
@Test
public void test1() {
String s1 = "abc"; // 字面量的定义方式
String s2 = "abc";

System.out.println(s1 == s2); // true
s1 = "hello";
System.out.println(s1); // hello
System.out.println(s2); // abc
}

通过字面量的方式(区别与 new)给一个字符串赋值,此时的字符串值声明在字符串常量池中。

字符串常量池中是不会存储相同内容的字符串的。

不可变性的体现:

1、当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。

2、当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。

3、当调用 Stringreplace() 方法修改指定字符或字符串时, 也需要重新指定内存区域赋值,不能使用原有的 value 进行赋值。

1.2 String 对象的创建

有以下几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
String str = "hello";

// 本质上 this.value = new char[0];
String s1 = new String();

// this.value = original.value;
String s2 = new String(String original);

// this.value = Arrays.copyOf(value, value.length);
String s3 = new String(char[] a);

String s4 = new String(char[] a, int startIndex, int count);

String s1 = “abc”; 与 String s2 = new String(“abc”); 有区别吗?

字符串常量存储在字符串常量池中,目的是为了共享。

字符串 常量对象存储在堆中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test2() {
// 通过字面量定义的方式:此时 s1 和 s2 的数据声明在方法区中的字符串常量池中
String s1 = "JavaEE";
String s2 = "JavaEE";
// 使用 new 的方式,此时 s3 和 s4 保存的是地址值,是数据在堆空间中开辟空间以后对应的地址值
String s3 = new String("JavaEE");
String s4 = new String("JavaEE");

System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1 == s4); // false
System.out.println(s3 == s4); // false
System.out.println("********************");

Person p1 = new Person("Mofan", 18);
Person p2 = new Person("Mofan", 18);
System.out.println(p1.name.equals(p1.name)); // true
System.out.println(p1.name == p1.name); // true

p1.name = "默烦";
System.out.println(p2.name); // Mofan
}

拓展:通过 String s = new String("abc"); 的方式创建对象,在内存中创建了 两个 对象,一个是堆空间中 new 结构,另一个是 char[] 对应的常量池中的数据:abc。

不同拼接对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test3() {
String s1 = "Spring";
String s2 = "MVC";

String s3 = "SpringMVC";
String s4 = "Spring" + "MVC";
String s5 = s1 + "MVC";
String s6 = "Spring" + s2;
String s7 = s1 + s2;

System.out.println(s3 == s4); // true
System.out.println(s3 == s5); // false
System.out.println(s3 == s6); // false
System.out.println(s5 == s6); // false
System.out.println(s3 == s7); // false
System.out.println(s5 == s7); // false
System.out.println(s6 == s7); // false

String s8 = s5.intern();
System.out.println(s3 == s8); // true
}

常量与常量 的拼接结果在常量池,且常量池中不会存在相同内容的常量。

只要其中 有一个是变量,结果就在堆中。

如果拼接的结果调用 intern() 方法,返回值就在常量池中

在 JVM 中,字符串常量池归于方法区中,不同的 JDK 版本存在着差异。在 JDK 1.6 中,字符串常量池归于方法区(具体实现:永久代);在 JDK 1.7 中,字符串常量池归于堆中;在 JDK 1.8 中,字符串常量池归于方法区(具体实现:元空间)。

intern() 方法的作用

调用 intern() 方法时,JVM 会检查字符串池中是否已经包含了一个等于当前字符串内容的字符串对象:

  • 如果包含,intern() 方法会返回该对象的引用;
  • 如果不包含,intern() 方法会将当前字符串对象添加到字符串池中,并返回其引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testIntern() {
String str1 = new String("java");
String str2 = new String("java");
assertThat(str1 == str2).isFalse();

// 调用 intern
String internStr1 = str1.intern();
String internStr2 = str2.intern();
assertThat(internStr1 == internStr2).isTrue();

// 与原先的字符串比较
assertThat(internStr1 == str1).isFalse();
assertThat(internStr2 == str2).isFalse();
}

str1 虽然与 str2 的内容相同,但它们是不同的实例,因此相等性判断结果是 false

在调用 intern() 方法后,返回的都是字符池中的引用,因此相等性判断结果是 true,但它们与先前的字符串是不同的实例。

在需要大量重复字符串的场景下,使用 intern() 方法可以减少内存消耗;在需要大量字符串比较的场景下,使用 intern() 可以提高性能,因为能够使用 == 进行引用比较,而不是使用 equals() 方法进行内容比较。

调用 intern() 方法会有一定的性能消耗,并且使用不当还可能会导致内存泄漏(字符串池中的字符串不会被 GC),因此 intern() 方法应该根据实际情况权衡使用。

1.3 常用方法

常用方法一

int length():返回字符串的长度:return value.length

char charAt(int index):返回某索引处的字符:return value[index]

boolean isEmpty():判断是否是空字符串:return value.length== 0

String toLowerCase():使用默认语言环境,将 String 中的 所有 字符转换为小写

String toUpperCase():使用默认语言环境,将 String 中的 所有 字符转换为大写

String trim():返回字符串的副本,忽略前导空白和尾部空白

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void test1() {
String s1 = "Hello World";
System.out.println(s1.length());
System.out.println(s1.charAt(0));
System.out.println(s1.charAt(10));

System.out.println(s1.isEmpty());

String s2 = s1.toLowerCase();
System.out.println(s2); // s1 所有字符小写
System.out.println(s1); // s1 仍然为原来的字符串

String s3 = " Hel lo World ";
String s4 = s3.trim();
System.out.println("------" + s3 + "------");
System.out.println("------" + s4 + "------");
}

输出结果:

String常用方法_1

boolean equals(Object obj):比较字符串的内容是否相同

boolean equalsIgnoreCase(String anotherString):与 equals() 方法类似, 忽略大小写

String concat(String str):将指定字符串连接到此字符串的结尾,等价于用“+”

int compareTo(String anotherString):比较两个字符串的大小,根据 ASCII 进行比较,与字符串长度无关

String substring(int beginIndex):返回一个新的字符串,它是此字符串的从 beginlndex 开始截取到最后的一个子字符串。

String substring(int beginIndex, int endIndex):返回一个新字符串,它是此字符串从 beginIndex 开始截取到 endIndex(不包含)的一个子字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test2() {
String s1 = "Hello World";
String s2 = "hello world";
System.out.println(s1.equals(s2));
System.out.println(s1.equalsIgnoreCase(s2));

String s3 = "abc";
String s4 = s3.concat("def");
System.out.println(s4);

String s5 = "abh";
String s6 = new String("abe");
// 联想 ASCII 码
System.out.println(s3.compareTo(s6));
System.out.println(s5.compareTo(s6));

String s7 = "默烦真帅!";
// 对于字符串的截取是左闭右开 [0, 2)
System.out.println(s7.substring(2));
System.out.println(s7);
System.out.println(s7.substring(0, 2));
}

输出结果:

String常用方法_2

常用方法二

boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束

boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始

boolean startsWith(String prefix, int toffset):测试字符串中从指定索引开始的子字符串是否以指定前缀开始

boolean contains(CharSequence s):当且仅当此字符串包含指定的char值序列时,返回true

int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引

int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始

int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引

int lastIndexOf(String str, int fromlndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索

注意: indexOflastIndexOf 方法如果未找到都是返回 -1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test3() {
String s1 = "hello world";
System.out.println(s1.endsWith("ld")); // true

System.out.println(s1.startsWith("He")); // false

System.out.println(s1.startsWith("ll", 2)); // true

String s2 = "wo";
System.out.println(s1.contains(s2)); // true

System.out.println(s1.indexOf("lo")); // 3
System.out.println(s1.indexOf("lol")); // -1

System.out.println(s1.indexOf("lo", 5)); // -1

String s3 = "hellorworld";
System.out.println(s3.lastIndexOf("or")); // 7
System.out.println(s3.lastIndexOf("or", 6)); // 4
}

问:什么情况下,indexOf(str)lastIndexOf(str) 的返回值相同?

答:str 只存在一个或者不存在。

常用方法三

String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。

String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个 子字符串。

boolean matches(String regex):告知此字符串是否匹配给定的正则表达式

String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。

String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过 limit 个,如果超过了,剩下的全部都放到最后一个元素中。

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
@Test
public void test4() {
String s1 = "默烦真帅,默烦帅帅帅";
String s2 = s1.replace('帅', '强');

System.out.println(s1);
System.out.println(s2);

String s3 = s1.replace("默烦", "mofan");
System.out.println(s3);
System.out.println("********************");

String s4 = "12hello34world567hello8901java789654";
String s5 = s4.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
System.out.println(s5);

String s6 = "123456";
// 判断 s6 是否全数字组成
boolean matches = s6.matches("\\d+");
System.out.println(matches);
String tel = "0825-8888666";
// 判断这是否是一个遂宁的固定电话
boolean result = tel.matches("0825-\\d{7,8}");
System.out.println(result);
System.out.println("********************");

String s7 = "hello|world|java";
String[] split1 = s7.split("\\|");
for (int i = 0; i < split1.length; i++) {
System.out.print(split1[i]);
}
System.out.println();
String s8 = "hello.world.java";
String[] split2 = s8.split("\\.");
for (int i = 0; i < split2.length; i++) {
System.out.print(split2[i]);
}
}

输出结果:

String常用方法_3

1.4 String 转换

字符串转换为基本数据类型、包装类:

Integer 包装类的 public static int parselnt(Strings):可以将由“数字”字符组成的字符串转换为整型

类似地,使用 java.lang 包中的Byte、Short、 Long、 Float、 Double类调相应的类方法可以由“数字”字符组成的字符串,转化为相应的基本数据类型。

基本数据类型、包装类转换为字符串:

调用 String 类的 public String valueOf(int n) 可将 int 型转换为字符串

相应的 valueOf(byte b)、valueOf(long I)、valueOf(float f)、valueOf(doubled)、valueOf(boolean b) 可由参数的相应类型到字符串的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// String 与基本数据类型、包装类之间的转换
@Test
public void test1() {
String str1 = "123";
// int num = (int) str1; // 错误的
int num = Integer.parseInt(str1);
System.out.println(num + 1); // 124

String str2 = String.valueOf(num);
System.out.println(str2 instanceof String); // true
String str3 = num + "";

System.out.println(str1 == str3); // false
}

String 与字符数组之间的转换

字符数组转换为字符串:

String 类的构造器:String(char[])String(char[], int offset, int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。

字符串转换为字符数组:

public char[] toCharArray():将字符串中的全部字符存放在-一个字符数组中的方法。

public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2() {
String str1 = "abc123";
char[] charArray = str1.toCharArray();
for (int i = 0; i < charArray.length; i++) {
System.out.print(charArray[i]);
System.out.print(" ");
}
System.out.println();
char[] chars = {'h', 'e', 'l', 'l', 'o'};
String str2 = new String(chars);
System.out.println(str2);
}

输出结果:

1
2
a b c 1 2 3 
hello

String 与字节数组转换

字节数组转换为字符串:

String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。

String(byte[],int offset, int length):用指定的字节数组的一部分,即从数组起始位置 offset 开始 length 个字节构造一个字符串对象。

字符串转换为字节数组:

public byteQ getBytes():使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。

public byte[] getBytes(String charsetName):使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test3() throws UnsupportedEncodingException {
String str1 = "abc123默烦";
byte[] bytes = str1.getBytes(); // 使用默认的字符集转换
System.out.println(Arrays.toString(bytes));

// 使用 GB2312 进行转换
byte[] str2 = str1.getBytes("GB2312");
System.out.println(Arrays.toString(str2));

// 使用默认的字符集进行解码
String str3 = new String(bytes);
System.out.println(str3);
// 编码解码字符集不一致
System.out.println(new String(str2));
System.out.println(new String(str2, "GB2312"));
}

输出结果:

String与字节数组之间的转换

1.5 StringBuffer 与 StringBuilder

StringBuffer

java.lang. StringBuffer 代表 可变 的字符序列,JDK1.0 中声明,可以对字符串内容进行增删,此时不会产生新的对象。

很多方法与String相同。

作为参数传递时,方法内部可以改变值。

1
2
3
4
5
6
@Test
public void test1() {
StringBuffer sb1 = new StringBuffer("abc");
sb1.setCharAt(0, 'm');
System.out.println(sb1); // mbc
}

底层分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2() {
// char[] value = new char[16]; 底层创建一个长度 16 的数组
StringBuffer stringBuffer = new StringBuffer();
// 输出 0,因为 return count
System.out.println(stringBuffer.length());
stringBuffer.append('a'); // value[0] = a;
stringBuffer.append('b'); // value[1] = a;

// char[] value = new char["abc".length() + 16]
StringBuffer sb2 = new StringBuffer("abc");
System.out.println(sb2.length()); // 3
}

扩容问题:如果要添加的数据底层数组盛不下了,就需要扩容底层数组。默认情况下,扩容为原来容量的 2 倍 + 2,同时将原有数组中的元素复制到新的数组中。

AbstractStringBuilder 源码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 容量确保
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
// 扩容方法
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}

StringBuilder

StringBuilderStringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样

面试题:对比String、StringBuffer、 StringBuilder 之间的区别:

  • String(JDK1.0):不可变字符序列,底层使用 char[] 进行存储

  • StringBuffer(JDK1.0):可变字符序列、效率低、线程安全,底层使用 char[] 进行存储

  • StringBuilder(JDK 5.0):可变字符序列、效率高、线程不安全,底层使用 char[] 进行存储

注意:作为参数传递的话,方法内部 String 不会改变其值,StringBufferStringBuilder会改变其值。

StringBuffer 常用方法

StringBuffer append(xxx):提供了很多的 append() 方法,用于进行字符串拼接

StringBuffer delete(int start, int end):删除指定位置的内容

StringBuffer replace(int start, int end, String str):把 [start,end) 位置替换为 str

StringBuffer insert(int offset, xxx):在指定位置插入xxx

StringBuffer reverse():把当前字符序列逆转

当 append 和 insert 时,如果原来的 value 数组长度不够,可扩容

上面这些方法都支持方法链操作。

方法链原理:

1
2
3
4
5
6
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this; // 原理
}

此外,还定义了一下方法:

1
2
3
4
5
public int indexOf(String str);
public String substring(int start, int end); // 截取 [start, end)
public int length();
public char charAt(int n);
public void setCharAt(int n ,char ch); // 修改

方法测试:

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
@Test
public void test3() {
StringBuffer s1 = new StringBuffer("abc");
s1.append(1);
s1.append("1");
System.out.println(s1);

s1.delete(2, 4);
System.out.println(s1);

StringBuffer s2 = new StringBuffer("abc11");
s2.replace(2, 4, "hello");
System.out.println(s2);

StringBuffer s3 = new StringBuffer("abc11");
s3.insert(2, false);
System.out.println(s3);
System.out.println(s3.length());

StringBuffer s4 = new StringBuffer("abc11");
System.out.println(s4.reverse());

StringBuffer s5= new StringBuffer("abc11");
String substring = s5.substring(1, 3);
System.out.println(substring);
System.out.println(s5);
}

输出结果:

StringBuffer常用方法测试结果

三者效率从高到低排列:String 小于 StringBuffer 小于 StringBuilder

1.6 String 相关笔试题

实现字符串的部分反转

题目:将字符串进行反转,将字符集中指定的部分进行翻转,比如将 “abcdefg” 反转为 “abfedcg”。

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
/**
* @author 默烦 2020/11/3
*/
public class StringDemo1 {
@Test
public void test1() {
String str = "abcdefg";
String reverse = reverse_3(str, 2, 5);
System.out.println(reverse);
}

// 将字符串进行反转,将字符集中指定的部分进行翻转,比如将 "abcdefg" 反转为 "abfedcg"
// 方式一: 转换成 char[]
public String reverse_1(String str, int startIndex, int endIndex) {
if (str != null) {
char[] chars = str.toCharArray();

for (int x = startIndex, y = endIndex; x < y; x++, y--) {
char temp = chars[x];
chars[x] = chars[y];
chars[y] = temp;
}
return new String(chars);
}
return null;
}

// 方式二:使用 String 的拼接
public String reverse_2(String str, int startIndex, int endIndex) {
if (str != null) {
String reverseStr = str.substring(0, startIndex);
for (int i = endIndex; i >= startIndex; i--) {
reverseStr += str.charAt(i);
}
reverseStr += str.substring(endIndex + 1);
return reverseStr;
}
return null;
}

// 方式三:在方式二的基础上进行优化
public String reverse_3(String str, int startIndex, int endIndex) {
StringBuilder builder = new StringBuilder(str.length());
builder.append(str.substring(0, startIndex));
for (int i = endIndex; i >= startIndex; i--) {
builder.append(str.charAt(i));
}
builder.append(str.substring(endIndex + 1));
return builder.toString();
}
}

获取一个字符串在另一个字符串出现的次数

题目:获取一个字符串在另一个字符串出现的次数,比如:“ab” 在 “abkkcadkabkebfkabkskab” 中出现的次数。

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
/**
* @author 默烦 2020/11/3
*/
public class StringDemo2 {
@Test
public void test() {
String mainStr = "abkkcadkabkebfkabkskab";
String subStr = "ab";
System.out.println(getCount(mainStr, subStr)); // 4
}

/*
* 获取一个字符串在另一个字符串中出现的次数
* 比如:"ab" 在 "abkkcadkabkebfkabkskab" 中出现的次数
* */
// 获取 subStr 在 mainStr 中出现的次数
public int getCount(String mainStr, String subStr) {
int mainLength = mainStr.length();
int subLength = subStr.length();
int count = 0;
int index = 0;
if (mainLength >= subLength) {
// 方式一
/* while ((index = mainStr.indexOf(subStr)) != -1) {
count++;
mainStr = mainStr.substring(index + subLength);
}*/
// 方式二:方式一的改进
while ((index = mainStr.indexOf(subStr, index)) != -1){
count++;
index += subLength;
}
return count;
} else {
return 0;
}
}
}

获取两个字符串中最大相同子串

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
/**
* @author 默烦 2020/11/3
*/
public class StringDemo3 {
/*
* 获取两个字符串中最大相同子串。比如:
* str1 = "abcwerthelloyuiodef";str2 = "cvhellobnm'
* 提示:将短的那个串进行长度依次递减的子串与较长的串比较。
*/
// 前提:两个字符串中只有一个最大相同子串
public String getMaxSameString(String str1, String str2) {
if (str1 != null && str2 != null) {
String maxStr = (str1.length() >= str2.length()) ? str1 : str2;
String minStr = (str1.length() < str2.length()) ? str1 : str2;
int length = minStr.length();
for (int i = 0; i < length; i++) {
for (int x = 0, y = length - i; y <= length; x++, y++) {
String substring = minStr.substring(x, y);
if (maxStr.contains(substring)) {
return substring;
}
}
}
}
return null;
}

@Test
public void test() {
String string1 = "abcwerthelloyuiodef";
String string2 = "cvhellobnm";
String maxSameString = getMaxSameString(string1, string2);
System.out.println(maxSameString);
}
}

那如果两个字符串中有多个相同的子串呢?

根据我们编写的代码,很显然只能输出最先出现的子串,纵使它并不是最大的子串。

我们可以对代码进行修改,将相同的子串存在动态数组 ArrayList 中,然后进行比较,输出最大的子串。

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
/**
* @author 默烦 2020/11/3
*/
public class StringDemo3 {
public String getMaxSameString(String str1, String str2) {
if (str1 != null && str2 != null) {
ArrayList<String> list = new ArrayList<>();
String maxStr = (str1.length() >= str2.length()) ? str1 : str2;
String minStr = (str1.length() < str2.length()) ? str1 : str2;
int length = minStr.length();
for (int i = 0; i < length; i++) {
for (int x = 0, y = length - i; y <= length; x++, y++) {
String substring = minStr.substring(x, y);
if (maxStr.contains(substring)) {
list.add(substring);
}
}
}
// 有相同的子串时
if (list.size() > 0) {
String max = list.get(0);
for (int i = 0; i < list.size() - 1; i++) {
// 得到长度最大的子串
if (max.length() < list.get(i + 1).length()) {
max = list.get(i + 1);
}
// 如果存在相同长度的子串,输出 ASCII 码更大的子串
if (max.length() == list.get(i + 1).length()) {
if (max.compareTo(list.get(i + 1)) <= 0) {
max = list.get(i + 1);
}
}
}
return max;
} else {
// 没有相同的子串,返回 null
return null;
}
}
return null;
}

@Test
public void test() {
String string1 = "abcwerthelloyuiodef";
String string2 = "cvhellobnm";
String maxSameString = getMaxSameString(string1, string2);
System.out.println(maxSameString);
}
}

上面的代码会输出长度最长的相同子串,如果存在多个长度相同的相同子串,那么就会根据 ASCII 码进行比较判断。

写出下面程序的运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test1() {
String str = null;
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str);

System.out.println(stringBuffer.length()); // 4

System.out.println(stringBuffer); // "null"

// 抛异常 NullPointerException
StringBuffer stringBuffer1 = new StringBuffer(str);
System.out.println(stringBuffer1);
}

1.7 StringJoiner

在对字符串进行拼接时,常被要求以某种符号进行间隔,又或者希望给最终的字符串加上前缀或后缀,相比于使用 StringBuilder,在 Java8 中引入的 StringJoiner 更能胜任这种需求。

以某种符号进行间隔

1
2
3
4
5
6
7
@Test
public void testSimpleToUse() {
StringJoiner joiner = new StringJoiner(",");
joiner.add("hello")
.add("world");
Assertions.assertEquals("hello,world", joiner.toString());
}

未添加任何需要拼接的字符串

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testEmptyValue() {
StringJoiner joiner = new StringJoiner("-");
Assertions.assertEquals("", joiner.toString());
joiner = new StringJoiner("-", "==>", "<==");
// 没添加任何内容时,返回前缀和后缀的拼接
Assertions.assertEquals("==><==", joiner.toString());
// 指定空值
joiner.setEmptyValue("empty");
Assertions.assertEquals("empty", joiner.toString());
}

当未添加任何需要进行拼接的字符串时,返回前缀和后缀拼接起的字符串;如果指定了 emptyValue,则返回指定的 emptyValue

前缀与后缀的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testPrefixAndSuffix() {
StringJoiner joiner = new StringJoiner("-", "==>", "<==");
joiner.add("1").add("2");
Assertions.assertEquals("==>1-2<==", joiner.toString());

joiner = new StringJoiner("-", "", "<==");
joiner.add("1").add("2");
Assertions.assertEquals("1-2<==", joiner.toString());

joiner = new StringJoiner("-", "==>", "");
joiner.add("1").add("2");
Assertions.assertEquals("==>1-2", joiner.toString());
}

获取拼接后的字符串长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testLength() {
StringJoiner joiner = new StringJoiner("-");
Assertions.assertEquals(0, joiner.length());

joiner = new StringJoiner("-", "==>", "<==");
Assertions.assertEquals(6, joiner.length());

String emptyValue = "empty";
joiner.setEmptyValue(emptyValue);
Assertions.assertEquals(emptyValue.length(), joiner.length());

joiner.add("1").add("2");
Assertions.assertEquals(9, joiner.length());
}

当未添加任何需要进行拼接的字符串且未指定前缀和后缀时,拼接后的字符串长度为 0;如果指定了前缀和后缀,但未指定 emptyValue,拼接后的字符串长度为前缀和后缀的长度之和;如果还指定了 emptyValue,那拼接后的字符串长度即为 emptyValue 的长度。

当添加了需要进行拼接的字符串时,拼接后的字符串长度为前缀、后缀及进行拼接的字符串长度之和。

StringJoiner 的合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testMerge() {
StringJoiner joiner = new StringJoiner("-", "==>", "<==");
try {
joiner.merge(null);
Assertions.fail();
} catch (Exception e) {
Assertions.assertTrue(e instanceof NullPointerException);
}

StringJoiner other = new StringJoiner(",", "==@", "@==");
other.add("one").add("two");
joiner.merge(other);
Assertions.assertEquals("==>one,two<==", joiner.toString());

// merge 过一次,重新 new 一个
joiner = new StringJoiner("-", "==>", "<==");
joiner.add("1").add("2");
joiner.merge(other);
Assertions.assertEquals("==>1-2-one,two<==", joiner.toString());
}

如果需要合并的 StringJoinernull,将抛出 NullPointerException

如果需要合并的 StringJoiner 不为 null,最终得到的字符串将使用原始 StringJoiner 的前缀和后缀,中间内容则会以原始 StringJoiner 的分隔符分隔两个 StringJoiner 的中间内容。

新增的 String.join() 方法

Java8 中的 String 类新增了两个 join() 静态方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static String join(CharSequence delimiter, CharSequence... elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
// Number of elements not likely worth Arrays.stream overhead.
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}

public static String join(CharSequence delimiter,
Iterable<? extends CharSequence> elements) {
Objects.requireNonNull(delimiter);
Objects.requireNonNull(elements);
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}

底层是使用 StringJoiner 进行实现的,将给定的字符串数组或字符串集合按照给定的分隔符进行拼接。

总结

Java8 中新增的 StringJoiner 类和 String.join() 方法使得按照某种分隔符进行字符串拼接的需求变得简单起来。相比于 StringJoinerString.join() 方法无法指定拼接后字符串的前缀和后缀。

StringJoiner 的源码很简单,在此不再叙述,但很建议进行阅读,个人认为设计得很巧妙,如果要求自行设计一个 StringJoiner,虽然能够实现,但代码的简洁性应该不会有源码的那么好。

2. JDK8 之前日期时间 API

2.1 java.lang.System 类

java.lang.System 类:

System 类提供的 public static long currentTimeMillis() 用来返回当前时间与 1970 年 1 月 1 日 0 时 0 分 0 秒之间以毫秒为单位的时间差。

此方法适于计算时间差。

计算世界时间的主要标准有:

  • UTC(Coordinated Universal Time)

  • GMT(Greenwich Mean Time)

  • CST(Central Standard Time)

代码示例

1
2
3
4
5
6
@Test
public void test1() {
long time = System.currentTimeMillis();
// 返回时间戳
System.out.println(time); // 1604324600453
}

2.2 java.util.Date 类

java.util.Date 类表示特定的瞬间,精确到亳秒。

构造器:

  • Date():使用无参构造器创建的对象可以获取本地当前时间。

  • Date(long date):创建指定毫秒数的 Data 对象

常用方法:

  • getTime():返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。

  • toString():把此 Date 对象转换为以下形式的String:dow mon dd hh:mm:ss zzz yyyy。其中:dow 是一周中的某一天(Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz 是时间标准。

  • 其它很多方法都过时了。

拓展:java.sql.Date 对应着数据库中的日期类型的变量。

测试代码

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
@Test
public void test2() {
// 构造器一:Date()
Date date = new Date();
// Mon Nov 02 21:54:29 CST 2020
System.out.println(date.toString());
// 1604325269401
System.out.println(date.getTime());

// 构造器二
Date date1 = new Date(1604325269401L);
// Mon Nov 02 21:54:29 CST 2020
System.out.println(date1);

// 创建 java.sql.Date 对象
java.sql.Date date2 = new java.sql.Date(1213164415467L);
// 2008-06-11
System.out.println(date2);

// java.util.Date 转换成 java.sql.Date
java.sql.Date date3 = new java.sql.Date(1213164415467L);
System.out.println((java.sql.Date) date3);

Date date4 = new Date();
java.sql.Date date5 = new java.sql.Date(date4.getTime());
System.out.println(date5);
}

2.3 java.text.SimpleDateFormat 类

Date 类的 API 不易于国际化,大部分被废弃了,java.text.SimpleDateFormat 类是一个不与语言环境有关的方式来格式化和解析日期的具体类。

它允许进行格式化:日期 → 文本、解析:文本→日期。

格式化:

SimpleDateFormat():默认的模式和语言环境创建对象

public SimpleDateFormat(String pattern):该构造方法可以用参数 pattern 指定的格式创建一个对象,该对象调用:
public String format(Date date):方法格式化时间对象 date

解析:

public Date parse(String source):从给定字符串的开始解析文本,以生成个日期。

基本使用

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
package com.yang.dataandtime;

import org.junit.Test;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @author 默烦 2020/11/3
*/
public class DateTimeTest2 {

// SimpleDateFormat 的实例化:使用默认的构造器
@Test
public void test1() throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat();

Date date = new Date();
System.out.println(date);

// 格式化:日期 ---> 字符串
String format = sdf.format(date);
System.out.println(format);

// 解析:字符串 ---> 日期
String string = "20-11-3 下午8:20";
Date parse = sdf.parse(string);
System.out.println(parse);
}


// 按照指定的方式进行格式化和解析,使用带参的构造器
@Test
public void test2() throws ParseException {
Date date = new Date();
// SimpleDateFormat sdf = new SimpleDateFormat("yyyyy.MMMMM.dd GGG hh:mm aaa");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 格式化
String format = sdf.format(date);
System.out.println(format);
// 解析:要求字符串必须符合 SimpleDateFormat 识别的格式(通过构造器参数体现)
// 否则就会抛异常
Date parse = sdf.parse("2020-11-03 08:28:51");
System.out.println(parse);
}
}

运行结果:

1
2
3
4
5
Tue Nov 03 20:41:06 CST 2020
20-11-3 下午8:41
Tue Nov 03 20:20:00 CST 2020
2020-11-03 08:41:23
Tue Nov 03 08:28:51 CST 2020

在实际使用过程中,我们一般会使用 SimpleDateFormat 带参的构造器,而不会使用默认的。

注意: 在将字符串解析成日期时,要求字符串必须符合 SimpleDateFormat 识别的格式(通过构造器参数体现),否则就会抛异常。

练习一:将字符串 “2020-11-11” 转换为java.sql.Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* 练习一:将字符串 "2020-11-11" 转换为java.sql.Date
* */
@Test
public void test3() throws ParseException {
String birth = "2020-11-11";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date date = dateFormat.parse(birth);
System.out.println(date.getClass().getName());
System.out.println(date);

java.sql.Date birthDate = new java.sql.Date(date.getTime());
System.out.println(birthDate.getClass().getName());
System.out.println(birthDate);
}

运行结果:

1
2
3
4
java.util.Date
Wed Nov 11 00:00:00 CST 2020
java.sql.Date
2020-11-11

2.4 java.util.Calendar 类

Calendar 是一个抽象基类,主用用于完成日期字段之间相互操作的功能。

获取 Calendar 实例的方法:

  • 使用 Calendar.getInstance() 方法

  • 调用它的子类 GregorianCalendar 的构造器。

一个 Calendar 的实例是系统时间的抽象表示,通过 get(int field) 方法来取得想要的时间信息。比如 YEAR、 MONTH、DAY_OF_WEEK、 HOUR_OF_ DAY、MINUTE、SECOND。

  • public void set(int field, int value)

  • public void add(int field, int amount)

  • public final Date getTime()

  • public final void setTime(Date date)

注意:

获取月份时:一月是 0, 二月是 1,以此类推,12 月是 11

获取星期时:周日是 1,周一是 2,…, 周六是7

方法的使用

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
package com.yang.dataandtime;

import org.junit.Test;

import java.util.Calendar;
import java.util.Date;

/**
* @author 默烦 2020/11/3
* <p>
* 日历类的使用
*/
public class CalendarTest {
@Test
public void test1() {
// 实例化
Calendar calendar = Calendar.getInstance();
System.out.println(calendar.getClass().getName());

// 常用方法
// get()
// 获取今天是本月的第几天,相当于几号
int days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
System.out.println(calendar.get(Calendar.DAY_OF_YEAR));

// set()
calendar.set(Calendar.DAY_OF_MONTH, 22);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);

//add()
calendar.add(Calendar.DAY_OF_MONTH, -3);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);

// getTime():日历类 ---> Date
Date time = calendar.getTime();
System.out.println(time);

// setTime():Date ---> 日历类
Date date = new Date();
calendar.setTime(date);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
}
}
Calendar测试结果

3. JDK8 中日期时间 API

3.1 新 API 出现的背景

有一说一,虽然 JDK 1.0 中就出现了 java.util.Date 类,但是它大多数方法在 JDK 1.1 引入 Calendar 类后就被弃用了。然而 Calendar 类也是个垃圾:

  • 可变性:像日期和时间这样的类应该是不可变的。

  • 偏移性:Date中的年份是从 1900 开始的,月份都从0开始。

  • 格式化:格式化只对 Date 有用,Calendar 则不行。

  • 此外,它们也不是线程安全的,不能处理闰秒等。

就以偏移量来说:

1
2
3
4
5
6
7
8
9
10
@Test
public void test1() {
Date date = new Date(2020, 11, 11);
// Sat Dec 11 00:00:00 CST 3920
System.out.println(date);
// 偏移量
Date date1 = new Date(2020 - 1900, 11, 11);
// Fri Dec 11 00:00:00 CST 2020
System.out.println(date1);
}

看看第一次输出个什么玩意?再看看要想输出正确的时间有多麻烦。

因此,在 JDK 8 中就引入了新的 API。

新的时间 API

第三次引入的 API 是成功的,并且 Java 8 中引入的 java.time API 已经纠正了过去的缺陷,将来很长一段时间内它 都会为我们服务。

Java 8 吸收了 Joda-Time 的精华,以一个新的开始为 Java 创建优秀的API。新的 java.time 中包含了所有关于本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。历史悠久的 Date 类新增了 tolnstant() 方法,用于把 Date 转换成新的表示形式。这些新增的本地化时间日期 API 大大简化了日期时间和本地化的管理。

java.time:包含值对象的基础包

java.time.chrono:提供对不同的日历系统的访问

java.time.format:格式化和解析时间和日期

java.time.temporal:包括底层框架和扩展特性

java.time. zone:包含时区支持的类

一般来说,只会用到基础包和 format 包,也可能会用到 temporal 包。一般来说,只会用到三分之一。

3.2 Local 系列

LocalDate、LocalTime、 LocalDateTime 类是其中较重要的几个类,它们的实例是 不可变的对象,分别表示使用 ISO-8601 日历系统的日期、时间、日期和时间。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。

LocalDate 代表 IOS 格式(yyyy-MM-dd)的日期,可以存储生日、纪念日等日期。

LocalTime 表示一个时间,而不是日期。

LocalDateTime 是用来表示日期和时间的,这是一个最常用的类(最常用)。

注:IS0-8601 日历系统是国际标准化组织制定的现代公民的日期和时间的表示法,也就是公历。

基本使用

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
@Test
public void test2() {
// now(): 获取当前的日期、时间、日期 + 时间
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();

System.out.println(localDate);
System.out.println(localTime);
System.out.println(localDateTime);

// of(): 设置指定的年月日时分秒,没有偏移量
LocalDateTime localDateTime1 = LocalDateTime.of(2020, 11, 11, 22, 22, 22);
System.out.println(localDateTime1);

// getxxx()
System.out.println(localDateTime.getDayOfMonth());
System.out.println(localDateTime.getDayOfWeek());
System.out.println(localDateTime.getMonth());
System.out.println(localDateTime.getMonthValue());
System.out.println(localDateTime.getMinute());

// 设置,体现不可变性
LocalDate localDate1 = localDate.withDayOfMonth(22);
System.out.println(localDate);
System.out.println(localDate1);
LocalDateTime localDateTime2 = localDateTime.withHour(4);
System.out.println(localDateTime);
System.out.println(localDateTime2);

// 相加
LocalDateTime localDateTime3 = localDateTime.plusMonths(3);
System.out.println(localDateTime);
System.out.println(localDateTime3);

// 相减
LocalDateTime localDateTime4 = localDateTime.minusDays(6);
System.out.println(localDateTime);
System.out.println(localDateTime4);
}

运行结果:

Local系列测试结果

3.3 瞬时:Instant

Instant:时间线上的一个瞬时点。这可能被用来记录应用程序中的事件时间截。

在处理时间和日期的时候,我们通常会想到年、月、日、时、分,秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向机器的,或者说是连续的。在此模型中,时间线中的一个点表示为一个很大的数,这有利于计算机处理。在 UNIX 中,这个数从 1970 年开始,以秒为的单位。同样的,在 Java 中,也是从1970年开始,但以毫秒为单位。

java.time 包通过值类型 Instant 提供机器视图,不提供处理人类意义上的时间单位。Instant 表示时间线上的一点,而不需要任何上下文信息,例如:时区。概念上讲,它只是简单的表示自 1970 年 1 月 1 日 0 时 0 分 0 秒(UTC)开始的秒数。因为 java.time 包是基于纳秒计算的,所以 Instant 的精度可以达到纳秒级。

(1 ns= 10-9 s)1 秒 = 1000毫秒 = 106 微秒 = 109 纳秒

时间戳 是指格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒(北京时间 1970 年 01 月 01 日 08 时 00 分 00 秒)起至现在的总秒数。

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void test3() {
// 获取零时区对应的时间
Instant instant = Instant.now();
System.out.println(instant);

// 我们现在所处的时间(东八区)
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);

// 获取自 1970 年 1 月 1 日 0 时 0 分 0 秒(UTC)开始的毫秒数
long milli = instant.toEpochMilli();
System.out.println(milli);

Instant ofEpochMilli = Instant.ofEpochMilli(1604414599712L);
System.out.println(offsetDateTime);
}

运行结果:

Instant测试结果

3.4 格式化与解析

java.time.format. DateTimeFormatter类,该类提供了三种格式化方法:

预定义的标准格式。如:ISO_ LOCAL_ DATE_ TIME;ISO_ LOCAL_ DATE;ISO_ LOCAL_TIME

本地化相关的格式。 如:ofLocalizedDateTime(FormatStyle.LONG)

自定义的格式。如:ofPattern(“yyy-MM-dd hh:mm:ss E”)

方法 描述
ofPattern(String patterm) 静态方法,返回一个指定字符串格式的 DateTimeFormatter
format(TemporalAccessor t) 格式化一个日期、时间,返回字符串
parse(CharSequence text) 将指定格式的字符序列解析为-个日期、时间

基本使用

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
@Test
public void test4() {
// 方式一:预定义标准
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
// 格式化
LocalDateTime localDateTime = LocalDateTime.now();
String str1 = formatter.format(localDateTime);
System.out.println(localDateTime);
System.out.println(str1);
// 解析
TemporalAccessor parse = formatter.parse("2020-11-03T22:54:29.124");
System.out.println(parse);

// 方式二:本地化
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
String str2 = formatter1.format(localDateTime);
System.out.println(str2);

DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
String str3 = formatter2.format(LocalDate.now());
System.out.println(str3);

// 方式三:自定义
// 小写 hh 为 12 小时制,大写 HH 为 24 小时制
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str4 = formatter3.format(LocalDateTime.now());
System.out.println(str4);

TemporalAccessor parse1 = formatter3.parse("2020-11-03 23:02:35");
System.out.println(parse1);
}

运行结果:

DateTimeFormatter运行结果

3.5 获取间隔

在 JDK8 中还提供了新的 API 用于计算两个时间或者日期之间的间隔。

时间间隔(Duration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 测试获取时间间隔
*/
@Test
public void testDuration() {
Instant instant1 = Instant.now();
Instant instant2 = instant1.plusSeconds(233);
System.out.println(instant1);
System.out.println(instant2);

Duration duration1 = Duration.between(instant1, instant2);
Duration duration2 = Duration.between(instant2, instant1);
Duration duration3 = duration1.plusDays(2);
System.out.println(duration1);
System.out.println(duration2);
System.out.println(duration3);

System.out.println(duration1.getSeconds());
System.out.println(duration2.getSeconds());
}

运行结果:

2021-01-11T10:10:30.407Z
2021-01-11T10:14:23.407Z
PT3M53S
PT-3M-53S
PT48H3M53S
233
-233

需要注意的是: Duration 仅支持时间操作(InstantLocalTimeLocalDateTime),不支持日期(LocalDate)。

那如何计算两个日期之间的间隔呢?

日期间隔(Period)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 测试获取日期间隔
*/
@Test
public void testPeriod() {
LocalDate localDate1 = LocalDate.now();
LocalDate localDate2 = localDate1.plusDays(1);

Period period1 = Period.between(localDate1, localDate2);
System.out.println(period1);
System.out.println("两个日期间隔: " + period1.getDays());

// 2021-01-01 00:00:00
// 时间戳转 LocalDate
LocalDate day1 = Instant.ofEpochMilli(1609430400000L).atZone(ZoneOffset.ofHours(8)).toLocalDate();
System.out.println("日期 1 为: " + day1);
// 2020-12-31 23:59:59 精确到毫秒
LocalDate day2 = Instant.ofEpochMilli(1609430399999L).atZone(ZoneOffset.ofHours(8)).toLocalDate();
System.out.println("日期 2 为: " + day2);
Period period2 = Period.between(day2, day1);
System.out.println(period2);
System.out.println("两个日期间隔: " + period2.getDays());
System.out.println("改变参数位置后, 两个日期间隔: " + Period.between(day1, day2).getDays());
}

运行结果:

P1D
两个日期间隔: 1
日期 1 为: 2021-01-01
日期 2 为: 2020-12-31
P1D
两个日期间隔: 1
改变参数位置后, 两个日期间隔: -1

Period 并不能直接获取两个跨月份日期之间的时间间隔,这时需要对日期进行 toEpochDay(),然后相减:

1
2
3
4
5
6
7
8
9
10
@Test
public void testDateInterval() {
LocalDate firstDay = LocalDate.of(2023, 1, 1);
LocalDate now = LocalDate.of(2023, 3, 18);

Period between = Period.between(firstDay, now);
Assertions.assertEquals(17, between.getDays());
Assertions.assertEquals(2, between.getMonths());
Assertions.assertEquals(76, now.toEpochDay() - firstDay.toEpochDay());
}

3.6 校正器

为了让我们能够更好地按照某种规则获取下次的时间,JDK8 中提供了校正器 TemporalAdjuster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 测试校正器的使用
*/
@Test
public void testTemporalAdjusters() {
LocalDateTime localDateTime1 = LocalDateTime.now();
System.out.println(localDateTime1);
// 本月 20 号这个时间点
LocalDateTime localDateTime2 = localDateTime1.withDayOfMonth(20);
System.out.println(localDateTime2);

// 获取下一个星期天
LocalDateTime localDateTime3 = localDateTime1.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
System.out.println(localDateTime3);
}

运行结果:

2021-01-11T18:44:56.354
2021-01-20T18:44:56.354
2021-01-17T18:44:56.354

除此之外,还可以 自定义校正器。比如,编写一个方法获取下一个工作日:

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
/**
* 测试获取下一工作日
*/
@Test
public void testGetNextWorkDay() {
// 当前星期一
LocalDateTime localDateTime = LocalDateTime.now();
// 星期五
LocalDateTime friday = localDateTime.plusDays(4);
// 星期六
LocalDateTime saturday = localDateTime.plusDays(5);

System.out.println(localDateTime);
System.out.println(getNextWorkDay(localDateTime));
System.out.println(getNextWorkDay(friday));
System.out.println(getNextWorkDay(saturday));
}

/**
* 获取下一工作日
* @param localDateTime 当前时间
* @return 下一工作日
*/
private LocalDateTime getNextWorkDay(LocalDateTime localDateTime){
return localDateTime.with(temporal -> {
LocalDateTime dateTime = (LocalDateTime) temporal;
DayOfWeek dayOfWeek = dateTime.getDayOfWeek();
if (DayOfWeek.FRIDAY.equals(dayOfWeek)) {
return dateTime.plusDays(3);
} else if (DayOfWeek.SATURDAY.equals(dayOfWeek)) {
return dateTime.plusDays(2);
} else {
return dateTime.plusDays(1);
}
});
}

运行结果:

2021-01-11T18:54:54.578
2021-01-12T18:54:54.578
2021-01-18T18:54:54.578
2021-01-18T18:54:54.578

3.7 其他 API

Zoneld:该类中包含了所有的时区信息,一个时区的 ID,如 Europe/Paris

ZonedDateTime:一个在 ISO-8601 日历系统时区的日期时间,如 2007-12-03T10:15:30+01:00 Europe/Paris。

  • 其中每个时区都对应着 ID,地区 ID 都为“{区域}/{城市}”的格式,例如:Asia/Shanghai 等

Clock:使用时区提供对当前即时、日期和时间的访问的时钟。

  • 持续时间:Duration, 用于计算两个 “时间” 间隔

  • 日期间隔:Period,用于计算两个 “日期” 间隔

TemporalAdjuster:时间校正器。有时我们可能需要获取例如:将日期调整到“下一个工作日”等操作。

TemporalAdjusters:该类通过静态方法(firstDayOfXxx() / lastDayOfXxx() / nexXX())提供了大量的常用 TemporalAdjuster 的实现。

与传统日期处理的转换

与传统日期处理的转换

3.8 API 之间的转换

Date 转 LocalDateTime

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Date date = new Date();
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();

LocalDateTime localDateTime = instant.atZone(zoneId).toLocalDateTime();
System.out.println("Date => " + date);
System.out.println("LocalDateTime => " + localDateTime);
}

还可以:

1
2
3
4
5
6
7
public static void main(String[] args) {
Date date = new Date();
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(),
ZoneId.systemDefault());
System.out.println("Date => " + date);
System.out.println("LocalDateTime => " + localDateTime);
}

还可以:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Date date = new Date();
LocalDateTime localDateTime = date.toInstant()
.atOffset(ZoneOffset.of("+8"))
.toLocalDateTime();

System.out.println("Date => " + date);
System.out.println("LocalDateTime => " + localDateTime);
}

LocalDateTime 转 Date

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
LocalDateTime localDateTime = LocalDateTime.now();

ZonedDateTime zdt = localDateTime.atZone(ZoneId.systemDefault());
Date date = Date.from(zdt.toInstant());

System.out.println("LocalDateTime => " + localDateTime);
System.out.println("Date => " + date);
}

还可以:

1
2
3
4
5
6
7
public static void main(String[] args) {
LocalDateTime localDateTime = LocalDateTime.now();
Date date = Date.from(localDateTime.toInstant(ZoneOffset.of("+8")));

System.out.println("LocalDateTime => " + localDateTime);
System.out.println("Date => " + date);
}

获取 LocalDateTime 的秒数和毫秒数

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
// 获取秒数
long second = LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
// 获取毫秒数
long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();

System.out.println("秒数 ==> " + second);
System.out.println("毫秒数 ==> " + milliSecond);
}

LocalDateTime 与 String 的互相转换

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
// 时间转字符串格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String string = LocalDateTime.now(ZoneOffset.of("+8")).format(formatter);

// 字符串转时间
String dateTimeStr = "2020-11-29 23:32:45";
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr, df);

System.out.println("String ==> " + string);
System.out.println("LocalDateTime ==> " + localDateTime);
}

计算两个日期之间的天数

1
2
3
4
5
6
7
8
9
private Long getBetweenDay(Date end, Date start) {
if (end == null) {
end = new Date();
}
// 将日期转换成 Epoch 天,Epoch 就是从1970-01-01(ISO)开始的天数,类似于时间戳
LocalDate ldEnd = end.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate ldStart = start.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
return ldEnd.toEpochDay() - ldStart.toEpochDay();
}

4. Java 比较器

4.1 前言

在 Java 中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。

Java实现对象排序的方式有两种:

  • 自然排序:java.lang.Comparable

  • 定制排序:java.util.Comparator

4.2 Comparable

JDK 中实现了 Comparable 接口的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 1. 像 String、包装类等实现了 Comparable 接口,重写了 compareTo(obj) 方法,给出了比较两个对象大小的方式
* 2. 像 String、包装类等重写 compareTo(obj) 方法以后,实现了从小到大的排序
* 3. 重写 compareTo(obj) 方法的规则:
* 如果当前对象 this 大于形参对象 obj,返回正整数
* 如果当前对象 this 小于形参对象 obj,返回负整数
* 如果当前对象 this 等于形参对象 obj,返回零
* */
@Test
public void test1() {
String[] arr = new String[]{"AA","CC","KK", "MM", "GG", "JJ" ,"DD"};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // 按照 ASCII 进行排序
}

自定义类实现 Comparable

对于自定义类来说,如果需要排序,我们可以让其实现 Comparable 接口,重写 compareTo(obj) 方法,在这个方法中可以指明如何排序。

假设我们自定义一个实体类 Goods,需要对其实例按照 price 进行升序排序,那么这个实体类需要实现 Comparable 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Goods implements Comparable<Goods> {
private String name;
private double price;

public Goods(String name, double price) {
this.name = name;
this.price = price;
}

// 省略无参构造函数、SET / GET 方法和 toString() 方法

@Override
public int compareTo(Goods o) {
if (this.price > o.price) {
return 1;
} else if (this.price < o.price) {
return -1;
} else {
return 0;
}
}
}

测试方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2() {
Goods[] arr = new Goods[4];
arr[0] = new Goods("Mouse1", 34);
arr[1] = new Goods("Mouse2", 43);
arr[2] = new Goods("Mouse3", 12);
arr[3] = new Goods("Mouse4", 65);

Arrays.sort(arr);
for (Goods goods : arr) {
System.out.println(goods);
}
}

运行结果:

自定义类实现Comparable测试结果

4.3 Comparator

当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码,或者实现了 java.lang.Comparable 接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序,强行对多个对象进行整体排序的比较。

重写 compare(Object o1,Object o2) 方法,比较 o1 和 o2 的大小:如果方法返回正整数,则表示 o1 大于 o2;如果返回 0, 表示相等;返回负整数,表示 o1 小于 o2。

可以将 Comparator 传递给 sort 方法(如 Collections .sortArrays.sort),从而允许在排序顺序上实现精确控制。

还可以使用 Comparator 来控制某些数据结构(如有序 Set 或有序映射)的顺序,或者为那些没有自然顺序的对象 Collection 提供排序。

对比: Comparable 接口的方式属于一劳永逸,保证 Comparable 接口实现类的对象在任何位置都可以比较大小,而 Comparator 接口属于临时性的比较。

具体使用

我们知道 String 类实现了 Comparable 接口,进行排序时按照 ASCII 码由低到高进行排序,如果我们要将其改成由高到低排序呢?

可以这样做:

1
2
3
4
5
6
@Test
public void test1() {
String[] arr = new String[]{"AA", "CC", "KK", "MM", "GG", "JJ", "DD"};
Arrays.sort(arr, (o1, o2) -> -o1.compareTo(o2));
System.out.println(Arrays.toString(arr));
}

输出结果:

1
[MM, KK, JJ, GG, DD, CC, AA]

如果是一个自定义类呢?

使用前文中实现了 Comparable 接口的 Goods 实体类,要想在自定义排序规则,可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test2() {
Goods[] arr = new Goods[5];
arr[0] = new Goods("Mouse1", 34);
arr[1] = new Goods("Mouse2", 43);
arr[2] = new Goods("Mouse2", 55);
arr[3] = new Goods("Mouse3", 12);
arr[4] = new Goods("Mouse4", 65);
Arrays.sort(arr, (o1, o2) -> {
// 按照产品名称由低到高排序,再按照价格由高到低排序
if (o1.getName().equals(o2.getName())) {
return -Double.compare(o1.getPrice(), o2.getPrice());
} else {
return o1.getName().compareTo(o2.getName());
}
});

for (Goods goods : arr) {
System.out.println(goods);
}
}

输出结果:

自定义类的Comparator测试结果

拓展思考

在上述代码中,我们使用了 Lambda 表达式,具体使用可以在此搜索【Lambda 表达式】查看规则。

Lambda 表达式主要针对函数式接口,我们点开 Comparator 接口查看:

1
2
3
4
5
6
7
8
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);

boolean equals(Object obj);

// 省略其他代码
}

我们发现,Comparator 接口上确实有 @FunctionalInterface 注解,也表明了这个接口确实是函数式接口。但是我们发现这个接口中并不是只有一个抽象方法,还有一个 equals() 方法,那这为什么还是函数式接口呢?

我们打开 JDK 8 API 官方文档查看:

FunctionalInterface注解官方文档说明

上面有一大段英文,简单来说:

如果接口声明了一个覆盖 java.lang.Object 的全局方法之一的抽象方法,那么它不会计入接口的抽象方法数量中,因为接口的任何实现都将具有 java.lang.Object 或其他地方的实现。

所以,这下明白了 Comparator 接口是函数式接口了吧。

5. 其他常用类

5.1 System 类

System 类代表系统,系统级的很多属性和控制方法都放置在该类的内部,该类位于 java.lang 包。

由于该类的构造器是 private 的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是 static 的,所以也可以很方便的进行调用。

成员变量与成员方法

成员变量:

System 类内部包含 in、out 和 err 三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。

成员方法:

native long currentTimeMillis():该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和 GMT 时间(格林威治时间)1970 年 1 月 1 号 0 时 0 分 0 秒所差的毫秒数。

void exit(int status):该方法的作用是退出程序。其中 status 的值为 0 代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

void gc():该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。

String getProperty(String key):该方法的作用是获得系统中属性名为 key 的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:

属性名 属性说明
java.version Java 运行时环境版本
java.home Java 安装目录
os.name 操作系统的名称
os.version 操作系统的版本
user.nane 用户的账户名称
user.hose 用户的主目录
user.dir 用户的当前工作目录

5.2 Math 类

java.lang.Math 提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为 double 型。

常用静态方法:

abs:绝对值

acos,asin,atan,cos,sin,tan:三角函数

sqrt:平方根

pow(double a, doble b):a 的 b 次幂

log:自然对数

exp:e为底指数

max(double a, double b):获取最大的数

min(double a, double b):获取最小的数

random():返回 0.0 到 1.0 之间的随机数

long round(double a):double 型数据 a 转换为 long 型(四舍五入)

toDegrees(double angrad):弧度转为角度

toRadians(double angdeg):角度转为弧度

5.3 BigInteger 与 BigDecimal

BigInteger

Integer 类作为 int 的包装类,能存储的最大整型值为 231 - 1,Long 类也是有限的,最大为263 - 1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。

java.math 包的 BigInteger 可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。

构造器:

  • BigInteger(String val):根据字符串构建 BigInteger 对象

常用方法:

  • public BigInteger abs():返回此 BigInteger 的绝对值的 BigInteger

  • BigInteger add(BigInteger val):返回其值为 (this + val) 的 BigInteger

  • BigInteger subtract(BigInteger val):返回其值为 (this - val) 的 BigInteger

  • BigInteger multiply(BigInteger val):返回其值为 (this * val) 的 BigInteger

  • BigInteger divide(BigInteger val):返回其值为 (this / val) 的 BigInteger,整数相除只保留整数部分

  • BigInteger remainder(BigInteger val):返回其值为 (this % val) 的 BigInteger

  • BigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟 (this % val) 的两个 BigInteger 的数组

  • BigInteger pow(int exponent):返回其值为 (thisexponent) 的 BigInteger

BigDecimal

一般的 Float 类和 Double 类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到 java.math.BigDecimal 类。

BigDecimal 类支持不可变的、任意精度的有符号十进制定点数。

构造器:

  • public BigDecimal(double val)

  • public BigDecimal(String val)

常用方法:

  • public BigDecimal add(BigDecimal augend)

  • public BigDecimal subtract(BigDecimal subtrahend)

  • public BigDecimal multiply(BigDecimal multiplicand)

  • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)

简单的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author 默烦 2020/11/4
*/
public class BigDecimalTest {
@Test
public void test1() {
BigInteger bi = new BigInteger("12433241123");
BigDecimal bd = new BigDecimal("12435.351");
BigDecimal bd2 = new BigDecimal("11");

System.out.println(bi);
/*
* java.lang.ArithmeticException:
* Non-terminating decimal expansion;
* no exact representable decimal result.
* */
// System.out.println(bd.divide(bd2));

System.out.println(bd.divide(bd2, BigDecimal.ROUND_HALF_UP));
// 保留 15 为小数
System.out.println(bd.divide(bd2, 15, BigDecimal.ROUND_HALF_UP));
}
}

输出结果:

1
2
3
12433241123
1130.486
1130.486454545454545