这一章,我们讨论最常用的Java自带的类。
我个人总结,使用频率最高的,Java自带的,至少有如下十一种:
String
StringBuilder
ArrayList
Arrays
Math
System
Object
Integer
Data和Calendar
java.time
异常类
我们一个一个讨论。
String
这个其实我们在第一章《1.基础语法》 的时候就讨论过一点,当时我们讨论字符串的"+运算"。
现在,我们专门讨论一下这个String。
String的概述
String类代表字符串,Java程序中的所有字符串文字都被实现为此类的实例。
也就是说,Java程序中所有的双引号字符串,都是String类的对象。
String类在java.lang包下,使用的时候不需要导包。
String的特点
字符串不可变,它们的值在创建后不能被更改。
虽然String的值是不可变的,但是它们可以被共享。
字符串效果上相当于字符数组char[]
,但是底层原理是字节数组byte[]
。
这几点或许都不好理解。
字符串不可变?
示例代码:
1 2 3 4 String s = "张曼玉" ; System.out.println(s); s = "林青霞" ; System.out.println(s);
运行结果:
这看着可变的啊。
还有可以被共享,这到底在说啥?
我们会在下文解释这几个特点。
String类的构造方法
String类有很多构造方法,我们讨论几种常用的构造方法。
方法名
说明
public String()
创建一个空白字符串对象,不含有任何内容。
public String(char[] chs)
根据字符数组的内容,来创建字符串对象。
public String(byte[] bys)
根据字节数组的内容,来创建字符串对象。
String s = "abc"
直接赋值的方式创建字符串对象。
实际上,String s = "abc"
,不属于构造方法。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 String s1 = new String(); System.out.println("s1:" + s1); char [] chs = {'a' , 'b' , 'c' };String s2 = new String(chs); System.out.println("s2:" + s2); byte [] bys = {97 , 98 , 99 };String s3 = new String(bys); System.out.println("s3:" + s3); String s4 = "abc" ; System.out.println("s4:" + s4);
运行结果:
1 2 3 4 s1: s2:abc s3:abc s4:abc
字符串的内存结构
在上文,我们讨论了字符串初始化的两种方法,两种方法实际上存在区别。
通过构造方法创建
通过new创建的字符串对象,每一次new都会申请一个内存空间,虽然内容相同,但是地址不同。
直接赋值方式创建:
以""方式给出的字符串,只要字符序列相同(顺序和大小写),无论在程序代码中出现几次,JVM都只会建立一个String对象,并在字符串池中维护。
我们举例子说明。
示例代码:
1 2 3 4 5 6 7 8 9 10 char [] chs = {'a' ,'b' ,'c' };String s1 = new String(chs); String s2 = new String(chs); System.out.println(s1 == s2); String s3 = "abc" ; String s4 = "abc" ; System.out.println(s3 == s4); System.out.println(s1 == s3);
运行结果:
这是因为,==
号,对于基本数据类型,比较的是具体的值;对于引用数据类型,比较的是对象地址。
我们来看看具体的内存结构。
解释一下。
首先main方法进栈。 执行char[] chs = {'a','b','c'}
栈内存的main方法中有了char[] chs
,堆内存有了new char[3]
,chs
指向new char[3]
。
String s1 = new String(chs)
栈内存的main方法中有了String s1
,堆内存有了new String(chs)
,s1
指向new String(chs)
。
String s2 = new String(chs)
栈内存的main方法中有了String s2
,堆内存有了new String(chs)
,s2
指向new String(chs)
。特别注意!这里的new String(chs)
和上一步的new String(chs)
不一样,是堆内存的两个不同的对象。
System.out.println(s1 == s2)
,输出false
。
String s3 = "abc"
栈内存的main方法中有了String s3
,堆内存的常量池中有了一个内容"abc"
,s3
指向堆内存中的常量池的空间。
String s4 = "abc"
栈内存的main方法中有了String s4
,堆内存的常量池已经有了一个内容"abc"
,s4
指向堆内存中的常量池的空间。注意,这时候s3
和s4
指向的是同一个内存地址。
System.out.println(s3 == s4)
,输出true
。
System.out.println(s1 == s3)
,输出false
。
最后,main方法出栈。
通过分析内存结构,也解释了我们说的第二个特点,共享。
比如s3
和s4
就共享了堆内存中常量池的同一个东西。
String拼接的内存结构
接下来,我们解释第一个特点,字符串不可变。
来看这个例子。
示例代码:
1 2 3 String s = "hello" ; s += "world" ; System.out.println(s);
运行结果:
这看起来还是可变啊?
我们来看内存结构。
解释一下。
首先main方法入栈。 执行String s = "hello"
栈内存的main方法中有了一个String s,堆内存的常量池中有了一个"hello",s指向堆内存的常量池中的hello。
执行s += "world"
。 这个我们在第一章《1.基础语法》 讨论赋值运算符的时候讨论过。s += "world"
等价于s = s + "world"
。 但是常量池中并没有"world",所以只能再新增一个常量"world"。s + "world"
,s
是"hello"
,s + "world"
的结果是"helloworld"
,常量池中并没有"helloworld"
,所以还需要新增一个"helloworld"
。 然后s
指向常量池中的"helloworld"
。
这就解释了第一个特点,字符串不可变,即对象中的内容是不可变的。
通过上述过程,我们还发现,每次拼接,都会构建一个新的String对象。浪费内存,浪费时间。
解决办法是StringBuilder。我们在本章就会讨论。
字符串的比较
再回到上一个例子。
==
号的作用:对于基本数据类型,比较的是具体的值;对于引用数据类型,比较的是对象地址。
那么,如果我们一定要比较String的具体的值是否相等。怎么办?
equals
1 public boolean equals(String s)
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char [] chs = {'a' , 'b' , 'c' };String s1 = new String(chs); String s2 = new String(chs); String s3 = "abc" ; String s4 = "abc" ; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s3 == s4); System.out.println("--------" ); System.out.println(s1.equals(s2)); System.out.println(s1.equals(s3)); System.out.println(s3.equals(s4));
运行结果:
1 2 3 4 5 6 7 false false true -------- true true true
equals源码分析
关于字符串的equals方法,有一个特点,并没有利用哈希值。
关于什么是哈希值,我们会在《4.集合》 讨论Set的时候,做更详细的讨论。
现在,我们只需要记住,equals没有利用哈希值。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public boolean equals (Object anObject) { if (this == anObject) { return true ; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0 ; while (n-- != 0 ) { if (v1[i] != v2[i]) return false ; i++; } return true ; } } return false ; }
String的常用方法
除了我们讨论过的equals
,还有几个常用方法。我们在这里列举一下。
方法名
说明
public boolean equals(Object object)
比较字符串的内容,严格区分大小写。
public char charAt(int index)
返回指定索引处的char值。
public int length()
返回此字符串的长度。
substring(int beginIndex)
这截取的字符串是从索引beginIndex开始的,到整个字符串的末尾,例如:字符串String s = “abcdef”;调用s.substring(2)表示从字符串的索引2开始截取到整个字符串结束,截取的字符串为cdef。
substring(int beginIndex, int endIndex)
截取的字符串从beginIndex开始,到字符串索引的endIndex - 1结束,即截取的字符串不包括endIndex这个索引对应的字符,所以endIndex的最大值为整个字符串的长度,所以使用这个方法的时候需要特别注意容易发生字符串截取越界的问题。
StringBuilder
在上文,我们讨论字符串的拼接的时候,说String每次拼接,都会构建一个新的String对象。浪费内存,浪费时间。解决办法是StringBuilder。
StringBuilder的概述
StringBuilder是一个可变的字符串类,这里的可变指的是StringBuilder对象中的内容是可变的。这就是StringBuilder和String的主要区别。
StringBuilder的构造方法
方法名
说明
public StringBuilder()
创建一个空白可变字符串对象,不含有任何内容。
public StringBuilder(String str)
根据字符串的内容,来创建可变字符串对象。
示例代码:
1 2 3 4 5 6 7 8 9 StringBuilder sb1 = new StringBuilder(); System.out.println("sb1:" + sb1); System.out.println("sb1.length():" + sb1.length()); StringBuilder sb2 = new StringBuilder("hello" ); System.out.println("sb2:" + sb2); System.out.println("sb2.length():" + sb2.length());
运行结果:
1 2 3 4 sb1: sb1.length():0 sb2:hello sb2.length():5
StringBuilder的添加和反转方法
添加和反转方法:
方法名
说明
public StringBuilder append(任意类型)
添加数据,并返回对象本身
public StringBuilder reverse()
相反的字符序列,并返回对象本身
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 StringBuilder sb1 = new StringBuilder(); System.out.println("sb1:" + sb1); StringBuilder sb2 = sb1.append("hello" ); System.out.println("sb1:" + sb1); System.out.println("sb2:" + sb2); System.out.println(sb1 == sb2); StringBuilder sb3 = new StringBuilder(); sb3.append("hello" ).append("world" ).append("java" ).append(100 ); System.out.println("sb3:" + sb3); StringBuilder sb4 = sb3.reverse(); System.out.println("sb3:" + sb3); System.out.println("sb4:" + sb4); System.out.println(sb3 == sb4);
运行结果:
1 2 3 4 5 6 7 8 sb1: sb1:hello sb2:hello true sb3:helloworldjava100 sb3:001avajdlrowolleh sb4:001avajdlrowolleh true
StringBuilder和String相互转换
通过上面的例子,我们发现StringBuilder的功能比String强大,所以有时候我们想利用StringBuilder的一些功能,那就需要转成StringBuilder类型的;然后在利用完成之后,又想再转回String。
这就涉及到了StringBuilder和String相互转换。
StringBuilder转换为String:
1 public String toString()
通过toString()
就可以实现把StringBuilder
转换为String
。
String转换为StringBuilder:
1 public StringBuilder(String s)
通过构造方法就可以实现把String
转换为StringBuilder
。
StringBuilder的常用方法
小结一下,StringBuilder的常用方法:
方法名
说明
public StringBuilder append (任意类型)
添加数据,并返回对象本身。
public StringBuilder reverse()
相反的字符序列,并返回对象本身。
public int length()
返回长度,实际存储值。
public String toString()
通过toString()
就可以实现把StringBuilder转换为String
ArrayList
这不是我们第一次讨论ArrayList,在《算法入门经典(Java与Python描述):2.数组、链表》 中,我们也有讨论ArrayList,当时我们还专门基于数组实现了一个ArrayList。这次,我们主要讨论在Java中的ArrayList。
ArrayList的概述
ArrayList是集合的一种。
集合是指一种存储空间可变的存储模型,存储的数据容量可以发生改变。
(关于集合,我们在之后的章节中会有更详细的讨论。)
ArrayList的特点:底层是数组实现的,长度可以变化。
ArrayList的构造方法
方法名
说明
public ArrayList()
创建一个空的集合对象
示例代码:
1 ArrayList<String> array = new ArrayList<String>();
注意看,这里有一个<>
,里面写着String
。这个含义是说这个ArrayList的元素的数据类型是String。
除了String,还可以是其他各种各样的引用类型,比如ArryList<Student>
,表示每一个元素的数据类型都是Student。
这就是泛型的使用,用于约束集合中存储元素的数据类型。但是需要注意的是,基本数据类型不可以。
(关于泛型,我们在下一章《4.集合》 做更多的讨论。)
ArrayList的成员方法
方法名
说明
public boolean remove(Object o)
删除指定的元素,返回删除是否成功。
public E remove(int index)
删除指定索引处的元素,返回被删除的元素。
public E set(int index,E element)
修改指定索引处的元素,返回被修改的元素。
public E get(int index)
返回指定索引处的元素。
public int size()
返回集合中的元素的个数。
public boolean add(E e)
将指定的元素追加到此集合的末尾。
public void add(int index,E element)
在此集合中的指定位置插入指定的元素。
示例代码:
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 package com.kakawanyifan;import java.util.ArrayList;public class ArrayListDemo { public static void main (String[] args) { ArrayList<String> array = new ArrayList<String>(); array.add("kakawanyifan" ); array.add("hello" ); array.add("world" ); array.add("java" ); System.out.println(array.remove("world" )); System.out.println(array.remove("javaee" )); System.out.println(array.remove(1 )); System.out.println(array.set(1 ,"javaee" )); System.out.println(array.get(0 )); System.out.println(array.get(1 )); System.out.println(array.size()); System.out.println("array:" + array); } }
运行结果:
1 2 3 4 5 6 7 8 true false hello java kakawanyifan javaee 2 array:[kakawanyifan, javaee]
关于ArrayList,还有一个非常常见的场景,遍历。这个其实很简单,size
和get
,set
配合起来,就可以遍历了。
Arrays
Arrays的常用方法
除了ArrayList,还有一个是Arrays。
Arrays的常用方法
方法名
说明
public static String toString(int[] a)
返回指定数组的内容的字符串表示形式。
public static void sort(int[] a)
按照数字顺序排列指定的数组。
这个我们在《算法入门经典(Java与Python描述):5.排序》 中有大量使用。就不举例了。
工具类的设计思想
Array
这个类,和我们之前的类的最大不同在于。之前的类,一般都需要先实例化,然后再调用。
但Array
这个类,不需要。直接用类名就可以,这是一个典型的工具类。
我们可以看看Array这个类的源代码,其中有两点值得我们学习。
一、构造方法用private,防止外界创建对象。
二、成员方法用public static,为了使用类名访问成员方法。
这两点也是我们在设计工具类时候,非常建议的准则。
构造方法用private,防止外界创建对象。
成员方法用public static,为了使用类名访问成员方法。
这就是我们在上一章《2.面向对象》 所说的,构造方法不一定是public。
Math
Math类概述:Math包含执行基本数字运算的方法
Math中方法的调用方式:方法都是静态的,通过类名.进行调用
Math类的常用方法:
方法名
说明
public static int abs(int a)
返回参数的绝对值。
public static double ceil(double a)
返回大于或等于参数的最小double值,等于一个整数。
public static double floor(double a)
返回小于或等于参数的最大double值,等于一个整数。
public static int round(float a)
按照四舍五入返回最接近参数的int。
public static int max(int a,int b)
返回两个int值中的较大值。
public static int min(int a,int b)
返回两个int值中的较小值。
public static double pow (double a,double b)
返回a的b次幂的值。
public static double random()
返回值为double的正值,[0.0,1.0)。
Math也是一个典型的工具类,同样我们可以来看看Math的设计。
构造方法:
成员方法:
同样符合我们上述关于工具类的设计准则。
构造方法用private
成员方法用public static
System
System类的常用方法:
方法名
说明
public static void exit(int status)
终止当前运行的 Java 虚拟机,非零表示异常终止
public static long currentTimeMillis()
返回当前时间(以毫秒为单位)
注意,虽然返回值的时间单位是毫秒,但是该值的粒度取决于底层操作系统。可能有些操作系统以几十毫秒为测量时间。
是当前和1970年1月1日0时0分0秒0毫秒之间的毫秒数。(如果考虑时差,就不是0时,比如在中国是8时。)
示例代码:
1 2 3 4 5 6 7 8 long start = System.currentTimeMillis();for (int i = 1 ; i <= 10000 ; i++) { System.out.println(i); } long end = System.currentTimeMillis();System.out.println("共耗时:" + (end - start) + "毫秒" );
运行结果:
1 2 3 4 5 6 7 8 9 10 1 2 3 【部分运行结果略】 9998 9999 10000 共耗时:49毫秒
Object
Object,或许在我们写的代码中不算最常见,但这是所有的类的根,每个类都直接或间接继承自Object。所有的对象包括数组都实现了这个类的方法。
包括我们设计的类,也继承自Object。而我们设计的类,通常需要重写的方法有:
hashCode
equals
toString
我们在下一章《4.集合》 就会看到例子。
Integer
基本数据类型的包装类
在第一章《1.基础语法》 ,我们讨论过基本数据类型,七种数值型的,再加上一种布尔型的,一共八种。
byte
short
int
long
float
double
char
boolean
这八种基本数据类型,都有各自对应的包装类。即将基本数据类型封装成了引用数据类型,其好处在于可以在对象中定义更多的功能方法操作该数据。
基本数据类型
包装类
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Character
boolean
Boolean
这里我们主要讨论一下Integer类,其他类都差不多。
Integer的构造方法
Integer,包装基本数据类型int。
Integer类构造方法:
方法名
说明
public Integer(int value)
根据 int 值创建 Integer 对象(过时)
public Integer(String s)
根据 String 值创建 Integer 对象(过时)
public static Integer valueOf(int i)
返回表示指定的 int 值的 Integer 实例
public static Integer valueOf(String s)
返回一个保存指定值的 Integer 对象 String
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Integer i1 = new Integer(100 ); System.out.println(i1); Integer i2 = new Integer("100" ); System.out.println(i2); System.out.println("--------" ); Integer i3 = Integer.valueOf(100 ); System.out.println(i3); Integer i4 = Integer.valueOf("100" ); System.out.println(i4);
运行结果:
1 2 3 4 5 100 100 -------- 100 100
int和String类型的相互转换
上述构造方法在int和String类型的相互转换中常用。
int转换为String
方式一:直接在数字后加一个空字符串。
方式二:通过String类静态方法valueOf()。
示例代码:
1 2 3 4 5 6 7 8 9 int number = 100 ;String s1 = number + "" ; System.out.println(s1); String s2 = String.valueOf(number); System.out.println(s2);
运行结果:
+ ""
,可以将int转换成字符串,这个我们在第一章《1.基础语法》 讨论过。在"+"操作中,如果出现了字符串,+就是字符串连接运算符,否则就是算术运算符。
String转换为int
方式一:先将字符串数字转成Integer,再调用valueOf()方法
方式二:通过Integer静态方法parseInt()进行转换
示例代码:
1 2 3 4 5 6 7 8 9 10 11 String s = "100" ; Integer i = Integer.valueOf(s); int x = i.intValue();System.out.println(x); int y = Integer.parseInt(s);System.out.println(y);
运行结果:
自动拆箱和自动装箱
自动装箱:自动的把基本数据类型转换为对应的包装类类型。
自动拆箱:自动的把包装类类型转换为对应的基本数据类型。
示例代码:
1 2 3 4 1 . Integer a = 100 ;2 . int b = a;
自动装箱,相当于Java编译器替我们执行了Integer.valueOf(XXX);
自动拆箱,相当于Java编译器替我们执行了Integer.intValue(XXX);
Integer的比较
最后,我们来看看一个比较有意思的现象。其实这个现象,我们不是第一次讨论。在《算法入门经典(Java与Python描述):7.哈希表》 这一章,讨论Java中的哈希表的时候,我们也讨论过。
来看例子,示例代码:
1 2 3 4 5 6 7 8 Integer a = 100 ; Integer b = 100 ; Integer c = 200 ; Integer d = 200 ; System.out.println(a==b); System.out.println(c==d);
运行结果:
100 = 100 100=100 1 0 0 = 1 0 0 ,这个没有任何问题。但是难道200 ≠ 200 200\neq 200 2 0 0 = 2 0 0 ?
问题出在Integer.valueOf()
,看看这个到底做了什么。
在[ − 128 , 127 ] [-128,127] [ − 1 2 8 , 1 2 7 ] 之间使用了缓存,这么做的原因是认为该区间会被经常使用到,为了提高效率,所以利用缓存,防止每次自动装箱都创建一次对象实例。
但是呢!需要注意的是,其他的包装类,没有这个"坑"。因为其他的包装类,例如double、float,不存在常用数字,不适合缓存。
Data和Calendar
Data和Calendar都是时间日期类。
Date的构造方法
Date代表了一个特定的时间,精确到毫秒。
Date类构造方法
方法名
说明
public Date()
分配一个 Date对象,并初始化,以便它代表它被分配的时间,精确到毫秒。
public Date(long date)
分配一个 Date对象,并将其初始化为表示从标准基准时间起指定的毫秒数。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.util.Date;public class M { public static void main (String[] args) { Date d1 = new Date(); System.out.println(d1); long date = 1000 *60 *60 ; Date d2 = new Date(date); System.out.println(d2); System.out.println(new Date(0 )); } }
运行结果:
1 2 3 Sun Aug 29 22:16:44 CST 2021 Thu Jan 01 09:00:00 CST 1970 Thu Jan 01 08:00:00 CST 1970
Date的常用方法
方法名
说明
public long getTime()
获取的是日期对象从1970年1月1日 00:00:00到现在的毫秒值。
public void setTime(long time)
设置时间,给的是毫秒值。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.util.Date;public class M { public static void main (String[] args) { Date d = new Date(); System.out.println(d.getTime()); System.out.println(d.getTime() * 1.0 / 1000 / 60 / 60 / 24 / 365 + 1970 + "年" ); long time = System.currentTimeMillis(); d.setTime(time); System.out.println(d); } }
运行结果:
1 2 3 1630246762658 2021.694785726091年 Sun Aug 29 22:19:22 CST 2021
那么,如果我们想输出那种更常见的表示的日期格式怎么办?
比如,下面这种。
SimpleDateFormat
SimpleDateFormat类构造方法
方法名
说明
public SimpleDateFormat()
构造一个SimpleDateFormat,使用默认模式和日期格式。
public SimpleDateFormat(String pattern)
构造一个SimpleDateFormat使用给定的模式和默认的日期格式。
SimpleDateFormat类的常用方法
方法名
说明
public final String format(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 import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;public class M { public static void main (String[] args) throws ParseException { Date d = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss" ); String s = sdf.format(d); System.out.println(s); System.out.println("--------" ); String ss = "2048-08-09 11:11:11" ; SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); Date dd = sdf2.parse(ss); System.out.println(dd); } }
运行结果:
1 2 3 2021年05月10日 15:19:43 -------- Sun Aug 09 11:11:11 CST 2048
关于时间日期格式字符串有很多,但我们只需要记住几个常用的。
格式
释义
举例
yyyy
年
2019
MM
月
02
dd
日
18
HH
小时(24小时制)
13
mm
分钟
53
ss
秒
42
SSS
毫秒
629
Calendar
Calendar的getInstance,返回一个表示当前时间的Calendar对象。
示例代码:
1 Calendar rightNow = Calendar.getInstance();
Calendar类常用方法
方法名
说明
public int get(int field)
返回给定日历字段的值
public abstract void add(int field, int amount)
根据日历的规则,将指定的时间量添加或减去给定的日历字段。
public final void set(int year,int month,int 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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import java.util.Calendar;public class M { public static void main (String[] args) { Calendar c = Calendar.getInstance(); int year = c.get(Calendar.YEAR); int month = c.get(Calendar.MONTH) + 1 ; int date = c.get(Calendar.DATE); System.out.println(year + "年" + month + "月" + date + "日" ); c = Calendar.getInstance(); c.add(Calendar.YEAR,-3 ); year = c.get(Calendar.YEAR); month = c.get(Calendar.MONTH) + 1 ; date = c.get(Calendar.DATE); System.out.println(year + "年" + month + "月" + date + "日" ); c = Calendar.getInstance(); c.add(Calendar.YEAR,10 ); c.add(Calendar.DATE,-10 ); year = c.get(Calendar.YEAR); month = c.get(Calendar.MONTH) + 1 ; date = c.get(Calendar.DATE); System.out.println(year + "年" + month + "月" + date + "日" ); c = Calendar.getInstance(); c.set(2050 ,10 ,10 ); year = c.get(Calendar.YEAR); month = c.get(Calendar.MONTH) + 1 ; date = c.get(Calendar.DATE); System.out.println(year + "年" + month + "月" + date + "日" ); } }
运行结果:
1 2 3 4 2021年5月10日 2018年5月10日 2031年4月30日 2050年11月10日
案例需求:获取任意一年的二月有多少天
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.util.Calendar;import java.util.Scanner;public class M { public static void main (String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入年:" ); int year = sc.nextInt(); Calendar c = Calendar.getInstance(); c.set(year, 2 , 1 ); c.add(Calendar.DATE, -1 ); int date = c.get(Calendar.DATE); System.out.println(year + "年的2月份有" + date + "天" ); } }
运行结果:
1 2 3 请输入年: 1989 1989年的2月份有28天
java.time
我们讨论了Data
和Calendar
,但其实我都不建议大家使用,推荐java.time
。只是可能有一些旧项目,一时半会儿改不了,所以讨论了一下Data
和Calendar
。
Date
和Calendar
,更准确来说是java.util.Date
和java.util.Calendar
,这两个堪称糟糕设计的典范,使用起来极其不方便。其实我本科毕业后,第一份工作是写C#,在见过了C#中畅快无比的DateTime
后,这两个一度让我对整个Java产生了深深的厌恶。
但是Java强大之处在与拥有丰富的生态,比如有一个joda-time
,这是一个第三方开源的时间日期类。
https://github.com/JodaOrg/joda-time
在JDK8中,已经被整合进了Java中。在JDK7之前的版本中,则需要我们手工引入包。
接下来,我们就来讨论java.time
。
常用类的概述和功能介绍
java.time
,注意都是小写,这其实是一个包,这个包中常用的类有七种:
Instant类
Instant类对时间轴上的单一瞬时点建模,可以用于记录应用程序中的事件时间戳,之后学习的类型转换中,均可以使用Instant类作为中间类完成转换
Duration类
Duration类表示秒或纳秒时间间隔,适合处理较短的时间,需要更高的精确性。
Period类
Period类表示一段时间的年、月、日。
LocalDate类
LocalDate是一个不可变的日期时间对象,表示日期,通常被视为年月日。
LocalTime类
LocalTime是一个不可变的日期时间对象,代表一个时间,通常被看作是小时-秒,时间表示为纳秒精度。
LocalDateTime类
LocalDateTime类是一个不可变的日期时间对象,代表日期时间,通常被视为年-月-日=时-分-秒。
ZonedDateTime类
ZonedDateTime是具有时区的日期时间的不可变表示,此类存储所有日期和时间字段,精度为纳秒,时区为区域偏移量,用于处理模糊的本地日期时间。
需要注意的是java.time
中的所有的类均没有公共构造方法,也就是说没办法通过new的方式直接创建,需要采用工厂方法加以实例化。
(关于什么是工厂方法,我们在以后的章节中会详细讨论,现在我们暂时理解需要通过成员方法创建实例。)
now方法
now方法可以根据当前日期或时间创建实例。
示例代码:
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 package com.kakawanyifan;import java.time.*;public class TimeNow { public static void main (String[] args) { Instant instant = Instant.now(); System.out.println(instant); LocalDate localDate = LocalDate.now(); System.out.println(localDate); LocalTime localTime = LocalTime.now(); System.out.println(localTime); LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime); ZonedDateTime zonedDateTime = ZonedDateTime.now(); System.out.println(zonedDateTime); } }
运行结果:
1 2 3 4 5 2021-08-29T19:59:32.002Z 2021-08-30 03:59:32.074 2021-08-30T03:59:32.074 2021-08-30T03:59:32.075+08:00[Asia/Shanghai]
Duration
和Period
没有now方法,这个我们根据这两个类的用途就知道。
除了上述的五个类外,还有几个类也有now方法。
Year类,表示年
YearMonth类,表示年月
MonthDay类,表示月日
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import java.time.*;public class TimeNow { public static void main (String[] args) { Year year = Year.now(); System.out.println(year); YearMonth yearMonth = YearMonth.now(); System.out.println(yearMonth); MonthDay monthDay = MonthDay.now(); System.out.println(monthDay); } }
运行结果:
of方法
of方法可以根据给定的参数,指定任意时间节点,生成对应的日期/时间对象,基本上每个基本类都有of方法用于生成的对应的对象,而且重载形式多种,可以根据不同的参数生成对应的数据。
示例代码:
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 package com.kakawanyifan;import java.time.*;public class TimeOf { public static void main (String[] args) { LocalDate localDate = LocalDate.of(2021 , Month.FEBRUARY,1 ); System.out.println(localDate); localDate = LocalDate.of(2021 ,2 ,1 ); System.out.println(localDate); LocalTime localTime = LocalTime.of(3 ,3 ); System.out.println(localTime); localTime = LocalTime.of(3 ,3 ,3 ); System.out.println(localTime); localTime = LocalTime.of(3 ,3 ,3 ,3 ); System.out.println(localTime); LocalDateTime localDateTime = LocalDateTime.of(2021 ,Month.FEBRUARY,1 ,3 ,3 ,3 ,3 ); System.out.println(localDateTime); localDateTime = LocalDateTime.of(localDate,localTime); System.out.println(localDateTime); } }
运行结果:
1 2 3 4 5 6 7 2021-02-01 2021-02-01 03:03 03:03:03 03:03:03.000000003 2021-02-01T03:03:03.000000003 2021-02-01T03:03:03.000000003
注意,对于月份,2月就写传入2
;但是对于Calendar
,却需要传入1
。因为Calendar
中的月份从0开始,但是对于年份和日期又没这个讲究。所以说Calendar
使用不方便。
推荐采用Month枚举的形式传入,防止记错。
日期比较
前后关系比较
三种常见比较场景
是否是同一天
是在之后
还是在之前
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.kakawanyifan;import java.time.LocalDate;import java.time.Month;public class equalsDemo { public static void main (String[] args) { LocalDate today = LocalDate.now(); LocalDate date = LocalDate.of(2020 , Month.FEBRUARY, 2 ); System.out.println(today.equals(date)); System.out.println(today.isAfter(date)); System.out.println(today.isBefore(date)); } }
运行结果:
检查周期性事件
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.kakawanyifan;import java.time.LocalDate;import java.time.MonthDay;public class birthdayDemo { public static void main (String[] args) { LocalDate today = LocalDate.now(); LocalDate dateOfBirth = LocalDate.of(2010 , 01 , 14 ); MonthDay birthday = MonthDay.of(dateOfBirth.getMonth(), dateOfBirth.getDayOfMonth()); MonthDay currentMonthDay = MonthDay.from(today); if (currentMonthDay.equals(birthday)){ System.out.println("Many Many happy returns of the day !!" ); }else { System.out.println("Sorry, today is not your birthday" ); } } }
运行结果:
1 Sorry, today is not your birthday
时区信息的获取
在上文的ZonedDateTime中,我们发现这个对象不仅有时间日期,还有偏移量+时区。那么时区如何在Java中获取呢?
通过提供的一个类ZonedId的getAvailableZoneIds方法可以获取到一个Set集合,集合中封装了600个时区。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.kakawanyifan;import java.time.ZoneId;import java.util.Set;public class TimeZone { public static void main (String[] args) { Set<String> availableZoneIds = ZoneId.getAvailableZoneIds(); System.out.println(availableZoneIds); ZoneId zoneId = ZoneId.systemDefault(); System.out.println(zoneId); } }
运行结果:
1 2 [Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8, Africa/Nairobi, America/Marigot, 【部分运行结果略】 Asia/Shanghai
关于时区的讨论,推荐一篇文章。
https://www.jianshu.com/p/17d53272f9cb
plus和minus的使用
想要修改某个日期/时间对象的现有实例时,我们可以使用plus和minus来完成操作。
比如:
LocalDate plusDay(long days) 增加天数
LocalDate plusWeeks(long weeks) 增加周数
LocallDate plusMonths(long months) 增加月数
LocalDate plusYears(long years) 增加年数
minus方法与以上类似。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.kakawanyifan;import java.time.LocalDate;public class TimePlus { public static void main (String[] args) { LocalDate localDate = LocalDate.of(2020 ,2 ,2 ); System.out.println(localDate); System.out.println(localDate.plusDays(1 )); System.out.println(localDate.plusWeeks(1 )); System.out.println(localDate.plusMonths(1 )); System.out.println(localDate.plusYears(1 )); } }
运行结果:
1 2 3 4 5 2020-02-02 2020-02-03 2020-02-09 2020-03-02 2021-02-02
除了LocalDate,LocalTime,LocalDateTime也都有相关的plus和minus方法。
java.time
中的类都是不可变的,一旦创建LocalDate,LocalTime,LocalDateTime相关的对象,就无法再修改了。所以上述我们的操作,其内存结构和String拼接或重新赋值的内存结构比较相似。
TemporalAdjuster 调节器
TemporalAdjuster,日期调节器。
例如:
TemporalAdjusters.firstDayOfMonth()
TemporalAdjusters.firstDayOfNextMonth()
TemporalAdjusters.firstDayOfYear()
TemporalAdjusters.lastDayOfYear()
TemporalAdjusters.lastDayOfMonth()
示例代码:
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 package com.kakawanyifan;import java.time.LocalDate;import java.time.temporal.TemporalAdjusters;public class TimeTemporalAdjuster { public static void main (String[] args) { LocalDate now = LocalDate.of(2020 ,2 ,2 ); System.out.println(now); LocalDate firstDayOfMonth = now.with(TemporalAdjusters.firstDayOfMonth()); System.out.println(firstDayOfMonth); LocalDate firstDayOfNextMonth = now.with(TemporalAdjusters.firstDayOfNextMonth()); System.out.println(firstDayOfNextMonth); LocalDate firstDayOfYear = now.with(TemporalAdjusters.firstDayOfYear()); System.out.println(firstDayOfYear); LocalDate lastDayOfYear = now.with(TemporalAdjusters.lastDayOfYear()); System.out.println(lastDayOfYear); LocalDate lastDayOfMonth = now.with(TemporalAdjusters.lastDayOfMonth()); System.out.println(lastDayOfMonth); } }
运行结果:
1 2 3 4 5 6 2020-02-02 2020-02-01 2020-03-01 2020-01-01 2020-12-31 2020-02-29
此外,我们还可以指定上一周的周几,或者下一周的周几。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.kakawanyifan;import java.time.DayOfWeek;import java.time.LocalDate;import java.time.temporal.TemporalAdjusters;public class TimeTemporalAdjuster { public static void main (String[] args) { LocalDate now = LocalDate.of(2020 ,2 ,2 ); System.out.println(now); LocalDate nextDayOfWeekFriday = now.with(TemporalAdjusters.next(DayOfWeek.FRIDAY)); System.out.println(nextDayOfWeekFriday); LocalDate previousDayOfWeekSunday = now.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)); System.out.println(previousDayOfWeekSunday); } }
运行结果:
1 2 3 2020-02-02 2020-02-07 2020-01-26
自定义TemporalAdjuster调节器
如果要自定义日期时间的更改逻辑,可以通过实现TemporalAdjuster类接口的方式来完成。
例如,每个月15号发工资,如果15号是周六周日,就提前发。
示例代码:
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 package com.kakawanyifan;import java.time.DayOfWeek;import java.time.LocalDate;import java.time.temporal.Temporal;import java.time.temporal.TemporalAdjuster;import java.time.temporal.TemporalAdjusters;public class PayDayAdjuster implements TemporalAdjuster { @Override public Temporal adjustInto (Temporal temporal) { LocalDate payDay = LocalDate.from(temporal); int day; if (payDay.getDayOfMonth() != 15 ) { day = 15 ; } else { day = payDay.getDayOfMonth(); } LocalDate realPayDay = payDay.withDayOfMonth(day); if (realPayDay.getDayOfWeek() == DayOfWeek.SUNDAY || realPayDay.getDayOfWeek() == DayOfWeek.SATURDAY) { realPayDay = realPayDay.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY)); } return realPayDay; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.kakawanyifan;import java.time.LocalDate;public class TimeTemporalAdjuster { public static void main (String[] args) { LocalDate payDay = LocalDate.of(2020 ,2 ,2 ); System.out.println(payDay); payDay = payDay.from(new PayDayAdjuster().adjustInto(payDay)); System.out.println(payDay); } }
运行结果:
Date和Calendar转为java.time
利用Date的toInstant(),先转为Instant,之后的转化都基于Instant。对于Calendar,一样的思路。
从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 package com.kakawanyifan;import java.time.*;import java.util.Date;public class TimeConvert { public static void main (String[] args) { Date d = new Date(); Instant instant = d.toInstant(); System.out.println(instant); LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); System.out.println(localDateTime); LocalDate localDate = localDateTime.toLocalDate(); System.out.println(localDate); LocalTime localTime = localDateTime.toLocalTime(); System.out.println(localTime); } }
运行结果:
1 2 3 4 2021-08-29T21:26:21.858Z 2021-08-30T05:26:21.858 2021-08-30 05:26:21.858
从Calendar转的例子。
示例代码:
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 package com.kakawanyifan;import java.time.*;import java.util.Calendar;public class TimeConvert { public static void main (String[] args) { Calendar c = Calendar.getInstance(); Instant instant = c.toInstant(); System.out.println(instant); LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); System.out.println(localDateTime); LocalDate localDate = localDateTime.toLocalDate(); System.out.println(localDate); LocalTime localTime = localDateTime.toLocalTime(); System.out.println(localTime); } }
运行结果:
1 2 3 4 2021-08-29T21:30:18.992Z 2021-08-30T05:30:18.992 2021-08-30 05:30:18.992
对于format,利用DateTimeFormatter就可以了,有一些预设的格式,如果对于预设的格式都不满意,还可以利用ofPattern
自定义格式。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import java.time.*;import java.time.format.DateTimeFormatter;public class TimeFormat { public static void main (String[] args) { LocalDateTime localDateTime = LocalDateTime.now(); String s = localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); System.out.println(s); s = localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE); System.out.println(s); s = localDateTime.format(DateTimeFormatter.ISO_LOCAL_TIME); System.out.println(s); s = localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); System.out.println(s); } }
运行结果:
1 2 3 4 2021-08-30T05:36:20.346 2021-08-30 05:36:20.346 2021-08-30 05:36:20
对于parse,与format非常类似,我们不赘述。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 package com.kakawanyifan;import java.time.*;import java.time.format.DateTimeFormatter;public class TimeParse { public static void main (String[] args) { LocalDateTime localDateTime = LocalDateTime.parse("2020-02-02 20:20:02" ,DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); System.out.println(localDateTime); } }
运行结果:
异常类
异常体系
异常就是程序出现了不正常的情况。
异常的体系结构:
JVM默认处理异常的方式
如果程序出现了问题,我们没有做任何处理,最终JVM会做默认的处理,处理方式有如下两个步骤:
把异常的名称,错误原因及异常出现的位置等信息输出在了控制台
程序停止执行
try-catch方式处理异常
格式:
1 2 3 4 5 try { 可能出现异常的代码; } catch(异常类名 变量名) { 异常的处理代码; }
执行流程:
程序从 try 里面的代码开始执行
出现异常,就会跳转到对应的 catch 里面去执行
执行完毕之后,程序还可以继续往下执行
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;public class ExceptionDemo { public static void main (String[] args) { System.out.println("开始" ); method(); System.out.println("结束" ); } public static void method () { try { int [] arr = {1 , 2 , 3 }; System.out.println(arr[3 ]); System.out.println("这里能够访问到吗" ); } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); } } }
运行结果:
1 2 3 4 5 开始 结束 java.lang.ArrayIndexOutOfBoundsException: 3 at com.kakawanyifan.ExceptionDemo.method(ExceptionDemo.java:13) at com.kakawanyifan.ExceptionDemo.main(ExceptionDemo.java:6)
在《5.IO流》 中,我们还会讨论try-catch-finally这种结构。
Throwable成员方法
方法名
说明
public String getMessage()
返回此 throwable 的详细消息字符串
public String toString()
返回此可抛出的简短描述
public void printStackTrace()
把异常的错误信息输出在控制台
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.kakawanyifan;public class ExceptionDemo { public static void main (String[] args) { System.out.println("开始" ); method(); System.out.println("结束" ); } public static void method () { try { int [] arr = {1 , 2 , 3 }; System.out.println(arr[3 ]); System.out.println("这里能够访问到吗" ); } catch (ArrayIndexOutOfBoundsException e) { System.out.println(e.toString()); System.out.println("------" ); System.out.println(e.getMessage()); System.out.println("------" ); e.printStackTrace(); } } }
运行结果:
1 2 3 4 5 6 7 8 9 开始 java.lang.ArrayIndexOutOfBoundsException: 3 ------ 3 ------ 结束 java.lang.ArrayIndexOutOfBoundsException: 3 at com.kakawanyifan.ExceptionDemo.method(ExceptionDemo.java:13) at com.kakawanyifan.ExceptionDemo.main(ExceptionDemo.java:6)
编译时异常和运行时异常的区别
编译时异常:都是Exception类及其子类,必须显示处理,否则程序就会发生错误,无法通过编译。
运行时异常:都是RuntimeException类及其子类,无需显示处理,也可以和编译时异常一样处理。
throws方式处理异常
格式:
1 2 3 public void 方法() throws 异常类名 { }
示例代码:
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 package com.kakawanyifan;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;public class ExceptionDemo { public static void main (String[] args) { System.out.println("开始" ); method(); try { method2(); } catch (ParseException e) { e.printStackTrace(); } System.out.println("结束" ); } public static void method2 () throws ParseException { String s = "2048-08-09" ; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd" ); Date d = sdf.parse(s); System.out.println(d); } public static void method () throws ArrayIndexOutOfBoundsException { int [] arr = {1 , 2 , 3 }; System.out.println(arr[3 ]); } }
运行结果:
1 2 3 4 5 开始 开始 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at com.kakawanyifan.ExceptionDemo.method(ExceptionDemo.java:30) at com.kakawanyifan.ExceptionDemo.main(ExceptionDemo.java:10)
throws和throw的区别
throws:
用在方法声明后面,跟的是异常类名。
表示抛出异常,由该方法的调用者来处理。
表示出现异常的一种可能性,并不一定会发生这些异常。
throw:
用在方法体内,跟的是异常对象名。
表示抛出异常,由方法体内的语句处理。
执行throw一定抛出了某种异常
自定义异常
自定义异常类
示例代码:
1 2 3 4 5 6 7 8 9 package com.kakawanyifan;public class ScoreException extends Exception { public ScoreException () {} public ScoreException (String message) { super (message); } }
1 2 3 4 5 6 7 8 9 10 11 12 package com.kakawanyifan;public class Teacher { public void checkScore (int score) throws ScoreException { if (score<0 || score>100 ) { throw new ScoreException("你给的分数有误,分数应该在0-100之间" ); } else { System.out.println("成绩正常" ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.kakawanyifan;import java.util.Scanner;public class ExceptionDemo { public static void main (String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入分数:" ); int score = sc.nextInt(); Teacher t = new Teacher(); try { t.checkScore(score); } catch (ScoreException e) { e.printStackTrace(); } } }
运行结果:
1 2 3 4 5 请输入分数: -1 com.kakawanyifan.ScoreException: 你给的分数有误,分数应该在0-100之间 at com.kakawanyifan.Teacher.checkScore(Teacher.java:7) at com.kakawanyifan.ExceptionDemo.main(ExceptionDemo.java:14)