Oracle 为Java 提供了丰富的基础类库, Java 8 提供了4000 多个基础类(包括下一章将要介绍的集合框架) ,通过这些基础类库可以提高开发效率, 降低开发难度。对于合格的Java 程序员而言, 至少要熟悉Java SE 中70% 以上的类(当然本书并不是让读者去背诵Java API 文档),但在反复查阅API 文档的过程中, 会自动记住大部分类的功能、方法, 因此程序员一定要多练, 多敲代码。
Java 提供了String 、StringBuffer 和StringBuilder 来处理字符串, 它们之间存在少许差别, 本章会详细介绍它们之间的差别,以及如何选择合适的字符串类。Java 还提供了Date 和Calendar 来处理日期、时间,其中Date 是一个己经过时的API, 通常推荐使用Calendar 来处理日期、时间。
正则表达式是一个强大的文本处理工具, 通过正则表达式可以对文本内容进行查找、替换、分割等操作。从JDK 1 .4以后, Java 也增加了对正则表达式的支持,包括新增的Pattem 和Matcher 两个类,并改写了String 类, 让String 类增加了正则表达式支持, 增加了正则表达式功能后的String 类更加强大。
Java 还提供了非常简单的国际化支持, Java 使用Locale 对象封装一个国家、语言环境, 再使用ResourceBundle 根据Locale 加载语言资源包,当ResourceBundle 加载了指定Locale 对应的语言资源文件后, ResourceBundle 对象就可调用getString()方法来取出指定key 所对应的消息字符串。
7.1 与用户豆动
7.1.1 使用Scanner 获取键盘输入
使用Scanner 类可以很方便地获取用户的键盘输入, Scanner 是一个基于正则表达式的文本扫描器,它可以从文件、输入流、宇符串中解析出基本类型值和字符串值。Scanner 类提供了多个构造器,不同的构造器可以接收文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。
Scanner 主要提供了两个方法来扫描输入。
- hasNextXxx(): 是否还有下一个输入项, 其中Xxx 可以是Int 、Long 等代表基本数据类型的字符串。如果只是判断是否包含下一个字符串,则直接使用hasNext() 。
- nextXxx(): 获取下一个输入工页。Xxx 的含义与前一个方法中的Xxx 相同。
在默认情况下, Scanner 使用空白(包括空格、Tab 空白、回车)作为多个输入项之间的分隔符。下面程序使用Scanner 来获得用户的键盘输入。
public class ScannerKeyBoardTest { public static void main(String[] args) { // System.in代表标准输入,就是键盘输入 Scanner sc = new Scanner(System.in); // 增加下面一行将只把回车作为分隔符 // sc.useDelimiter(" "); // 判断是否还有下一个输入项 while(sc.hasNext()) { // 输出输入项 System.out.println("键盘输入的内容是:" + sc.next()); } } }
运行上面程序,程序通过Scanner 不断从键盘读取键盘输入,每次读到键盘输入后,直接将输入内容打印在控制台。
如果希望改变Scanner 的分隔符(不使用空白作为分隔符) ,例如, 程序需要每次读取一行,不管这一行中是否包含空格, Scanner 都把它当成一个输入项。在这种需求下,可以把Scanner 的分隔符设置为回车符,不再使用默认的空白作为分隔符。
Scanner 的读取操作可能被阻塞(当前执行顺序流暂停)来等待信息的输入。如果输入源没有结束,Scanner 又读不到更多输入项时(尤其在键盘输入时比较常见) , Scanner 的hasNext()和next()方法都有可能阻塞, hasNext()方法是否阻塞与和其相关的next()方法是否阻塞无关。
为Scanner 设置分隔符使用useDelimiter(String pattem)方法即可, 该方法的参数应该是一个正则表达式。只要把上面程序中粗体字代码行的注释去掉, 该程序就会把键盘的每行输入当成一个输入项, 不会以空格、Tab 空白等作为分隔符。
事实上, Scanner 提供了两个简单的方法来逐行读取。
- boolean hasNextLine(): 返回输入源中是否还有下一行。
- String nextLine(): 返回输入源中下一行的字符串。
Scanner 不仅可以获取字符串输入项,也可以获取任何基本类型的输入项,如下程序所示。
public class ScannerLongTest { public static void main(String[] args) { // System.in代表标准输入,就是键盘输入 Scanner sc = new Scanner(System.in); // 判断是否还有下一个long型整数 while(sc.hasNextLong()) { // 输出输入项 System.out.println("键盘输入的内容是:" + sc.nextLong()); } } }
Scanner 不仅能读取用户的键盘输入,还可以读取文件输入。只要在创建Scanner 对象时传入一个File 对象作为参数,就可以让Scanner 读取该文件的内容。例如如下程序。
public class ScannerFileTest { public static void main(String[] args) throws Exception { // 将一个File对象作为Scanner的构造器参数,Scanner读取文件内容 Scanner sc = new Scanner(new File("ScannerFileTest.java")); System.out.println("ScannerFileTest.java文件内容如下:"); // 判断是否还有下一行 while(sc.hasNextLine()) { // 输出文件中的下一行 System.out.println(sc.nextLine()); } } }
7.2 系统相关
Java 程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java 提供了System 类和Runtime 类来与程序的运行平台进行交互。
7.2.1 System 类
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(System.getenv("JAVA_HOME")); // 获取所有的系统属性 Properties props = System.getProperties(); // 将所有系统属性保存到props.txt文件中 props.store(new FileOutputStream("props.txt") , "System Properties"); // 输出特定的系统属性 System.out.println(System.getProperty("os.name")); } }
上面程序通过调用System 类的getenv() 、getProperties()、getProperty()等方法来访问程序所在平台的环境变量和系统属性,程序运行的结果会输出操作系统所有的环境变量值,并输出JAVA_HOME 环境变量,以及os.name 系统属性的值。
System 类还提供了一个identityHashCode(Object x)方法, 该方法返回指定对象的精确hashCode 值,也就是根据该对象的地址计算得到的hashCode 值。当某个类的hashCode()方法被重写后,该类实例的hashCodeO方法就不能唯一地标识该对象: 但通过identityHashCode()方法返回的hashCode 值,依然是根据该对象的地址计算得到的hashCode 值。所以,如果两个对象的identityHashCode 值相同,则两个对象绝对是同一个对象。如下程序所示。
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)); } }
通过identityHashCode(Object x) 方法可以获得对象的identityHashCode 值,这个特殊的identityHashCode 值可以唯一地标识该对象。因为identityHashCode 值是根据对象的地址计算得到的,所以任何两个对象的identityHashCode 值总是不相等。
7.2.2 Runtime 类与Java 9 的ProcessHandle
Runtime 类代表Java 程序的运行时环境,每个Java 程序都有一个与之对应的Runtime 实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime 实例,但可以通过getRuntime()方法获取与之关联的Runtime 对象。
与System 类似的是, Runtime 类也提供了gc()方法和runFinalization()方法来通知系统进行垃圾回收、清理系统资源,并提供了load(String filename)和loadLibrary(String libname)方法来加载文件和动态链接库。
Runtime 类代表Java 程序的运行时环境,可以访问JVM的相关信息,如处理器数量、内存信息等。如下程序所示。
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()); } }
除此之外, Runtime 类还有一个功能它可以直接单独启动一个进程来运行操作系统的命令,如下程序所示。
public class ExecTest { public static void main(String[] args) throws Exception { Runtime rt = Runtime.getRuntime(); // 运行记事本程序 rt.exec("notepad.exe"); } }
Runtime 提供了一系列exec()方法来运行操作系统命令,关于它们之间的细微差别,请读者自行查阅API 文档。
通过exec 启动平台上的命令之后,它就变成了一个进程, Java 使用Process 来代表进程。Java 9 还新增了一个ProcessHandle 接口,通过该接口可获取进程的m 、父进程和后代进程;通过该接口的onExit()方法可在进程结束时完成某些行为。
ProcessHandle 还提供了一个ProcessHandle. Info 类,用于获取进程的命令、参数、启动时间、累计运行时间、用户等信息。下面程序示范了通过ProcessHandle 获取进程的相关信息。
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); } }
7.3 常用类
本节将介绍Java 提供的一些常用类,如String 、Math 、BigDecimal 等的用法。
7.3.1 Object 类
Object 类是所有类、数组、枚举类的父类,也就是说, Java 允许把任何类型的对象赋给Object 类型的变量。当定义一个类时没有使用extends 关键字为它显式指定父类,则该类默认继承Object 父类。因为所有的Java 类都是Object 类的子类,所以任何Java 对象都可以调用Object 类的方法。Object类提供了如下几个常用方法。
- boolean equals(Object obj): 判断指定对象与该对象是否相等。此处相等的标准是,两个对象是同一个对象,因此该equals()方法通常没有太大的实用价值。
- protected void finalize(): 当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源。
- Class<?> getClass(): 返回该对象的运行时类。
- int hashCode(): 返回该对象的hashCode 值。在默认情况下, Object 类的hashCode()方法根据该对象的地址来计算(即与System.identityHashCode(Object x)方法的计算结果相同) 。但很多类都重写了Object 类的hashCode()方法,不再根据地址来计算其hashCode()方法值。
- String toString(): 返回该对象的字符串表示,当程序使用System.out.println()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toStringO方法返回该对象的字符串表示。Object 类的toString()方法返回"运行时类名@十六进制hashCode 值"格式的字符串,但很多类都重写了Object 类的toString()方法,用于返回可以表述该对象信息的字符串。
除此之外, Object 类还提供了wait()、notify()、notifyAll()几个方法,通过这几个方法可以控制线程的暂停和运行。
Java 还提供了一个protected 修饰的clone()方法,该方法用于帮助其他对象来实现"自我克隆",所谓"自我克隆"就是得到一个当前对象的副本,而且二者之间完全隔离。由于Object 类提供的clone()方法使用了protected 修饰,因此该方法只能被子类重写或调用。
自定义类实现"克隆"的步骤如下。
1、自定义类实现Cloneable 接口。这是一个标记性的接口,实现该接口的对象可以实现"自我克隆",接口里没有定义任何方法。
2、自定义类实现自己的clone()方法。
3、实现clone()方法时通过super.clone() ;调用Object 实现的clone()方法来得到该对象的副本,并返回该副本。如下程序示范了如何实现"自我克隆"。
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(); // 判断u1、u2是否相同 System.out.println(u1 == u2); //① // 判断u1、u2的address是否相同 System.out.println(u1.address == u2.address); //② } }
7.3.2 Java 7 新增的Objects 类
Java 7 新增了一个Objects 工具类, 它提供了一些工具方法来操作对象,这些工具方法大多是"空指针"安全的。比如你不能确定一个引用变量是否为null,如果贸然地调用该变量的toString()方法,则可能引发NullPointerExcetpion 异常; 但如果使用Objects 类提供的toString(Object o)方法, 就不会引发空指针异常,当o为null 时,程序将返回一个"null"字符串。
如下程序示范了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!")); } }
7.3.3 Java 9 改进的String 、StringBuffer 和StringBuilder 类
字符串就是一连串的字符序列, Java 提供了String 、StringBuffer 和StringBuilder 三个类来封装宇符串,并提供了一系列方法来操作字符串对象。
String 类是不可变类,即一旦一个String 对象被创建以后,包含在这个对象中的字符序列是不可改变的, 直至这个对象被销毁。
StringBuffer 对象则代表一个字符序列可变的字符串,当一个StringBuffer 被创建以后,通过StringBuffer 提供的append() 、insert()、reverse() 、setCharAt()、setLength()等方法可以改变这个字符串对象的宇符序列。一旦通过StringBuffer 生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String 对象。
StringBuilder 类是JDK 1.5 新增的类, 它也代表可变字符串对象。实际上, StringBuilder 和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是, StringBuffer 是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象, 则应该优先考虑使用StringBuilder 类。
- String(): 创建一个包含0 个宇符串序列的String 对象(并不是返回null ) 。
- 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(byte[] bytes, String charsetName): 使用指定的字符集将指定的byte[]数组解码成一个新的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 类也提供了大量方法来操作字符串对象, 下面详细介绍这些常用方法。
- char charAt(int index): 获取字符串中指定位置的字符。其中, 参数index 指的是字符串的序数,字符串的序数从0 开始到length()-1。
- int compareTo(String anotherString): 比较两个宇符串的大小。如果两个字符串的字符序列相等,则返回0 ; 不相等时,从两个字符串第0 个字符开始比较, 返回第一个不相等的字符差。另一种情况,较长字符串的前面部分恰巧是较短的字符串,则返回它们的长度差。
- String concat(String s的:将该String 对象与由连接在一起。与Java 提供的字符串连接运算符" +"的功能相同。
- boolean contentEquals(StringBuffer sb): 将该String 对象与StringBuffer 对象sb 进行比较,当它们包含的字符序列相同时返回true 。
- 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)构造器的功能相同。
- boolean endsWith(String suffix): 返回该String 对象是否以suffix 结尾。
- boolean equals(Object anObject): 将该字符串与指定对象比较,如果二者包含的字符序列相等,则返回true; 否则返回false 。
- boolean equalsIgnoreCase(String str):与前一个方法基本相似,只是忽略字符的大小写。
- byte[] getBytes(): 将该String 对象转换成byte 数组。
- void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin): 该方法将字符串中从srcBegin 开始,到srcEnd 结束的字符复制到dst 字符数组中,其中dstBegin 为目标字符数组的起始复制位置。
- int indexOf(int ch): 找出ch字符在该字符串中第一次出现的位置。
- int indexOf(int ch, int fromIndex): 找出ch 字符在该字符串中从fromIndex 开始后第一次出现的位置。
- int indexOf(String str):找出str子字符串在该宇符串中第一次出现的位置。
- int indexOf(String str, int fromIndex): 找出str子字符串在该字符串中从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 子字符串在该字符串中从fromlndex 开始后最后一次出现的位置。
- int length(): 返回当前字符串长度。
- String replace(char oldChar, char newChar): 将字符串中的第一个oldChar 替换成newChar 。
- boolean startsWith(String prefix): 该String 对象是否以prefix 开始。
- boolean startsWith(String prefix, int toffset): 该String 对象从toffset 位置算起,是否以prefix 开始。
- String substring(int beginlndex): 获取从beginlndex 位置开始到结束的子字符串。
- String substring(int beginlndex, int endIndex): 获取从beginIndex 位置开始到endlndex 位置的子字符串。
- char[] toCharArray(): 将该String 对象转换成char 数组。
- String toLowerCase(): 将宇符串转换成小写。
- String toUpperCase(): 将字符串转换成大写。
- static String valueOf(X x): 一系列用于将基本类型值转换为String 对象的方法。
public class StringBuilderTest { public static void main(String[] args) { StringBuilder sb = new StringBuilder(); // 追加字符串 sb.append("java");//sb = "java" // 插入 sb.insert(0 , "hello "); // sb="hello java" // 替换 sb.replace(5, 6, ","); // sb="hello,java" // 删除 sb.delete(5, 6); // sb="hellojava" System.out.println(sb); // 反转 sb.reverse(); // sb="avajolleh" System.out.println(sb); System.out.println(sb.length()); // 输出9 System.out.println(sb.capacity()); // 输出16 // 改变StringBuilder的长度,将只保留前面部分 sb.setLength(5); // sb="avajo" System.out.println(sb); } }
7.3 .4 Math 类
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()); } }
7.3.5 Java 7 的ThreadLocalRandom 与Random
public class RandomTest { public static void main(String[] args) { Random rand = new Random(); System.out.println("rand.nextBoolean():" + rand.nextBoolean()); byte[] buffer = new byte[16]; rand.nextBytes(buffer); System.out.println(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()); } }
下面就对上面的介绍做一个实验,可以看到当两个Random 对象种子相同时,它们会产生相同的数字序列。值得指出的, 当使用默认的种子构造Random 对象时,它们属于同一个种子。
public class SeedTest { public static void main(String[] args) { Random r1 = new Random(50); System.out.println("第一个种子为50的Random对象"); System.out.println("r1.nextBoolean(): " + r1.nextBoolean()); System.out.println("r1.nextInt(): " + r1.nextInt()); System.out.println("r1.nextDouble(): " + r1.nextDouble()); System.out.println("r1.nextGaussian(): " + r1.nextGaussian()); System.out.println("---------------------------"); Random r2 = new Random(50); System.out.println("第二个种子为50的Random对象"); System.out.println("r2.nextBoolean(): " + r2.nextBoolean()); System.out.println("r2.nextInt(): " + r2.nextInt()); System.out.println("r2.nextDouble(): " + r2.nextDouble()); System.out.println("r2.nextGaussian(): " + r2.nextGaussian()); System.out.println("---------------------------"); Random r3 = new Random(100); System.out.println("种子为100的Random对象"); System.out.println("r3.nextBoolean(): " + r3.nextBoolean()); System.out.println("r3.nextInt(): " + r3.nextInt()); System.out.println("r3.nextDouble(): " + r3.nextDouble()); System.out.println("r3.nextGaussian(): " + r3.nextGaussian()); } }
运行上面程序,看到如下结果:
第一个种子为50的Random对象 r1.nextBoolean(): true r1.nextInt(): - r1.nextDouble(): 0.26675 r1.nextGaussian(): 2.7946 --------------------------- 第二个种子为50的Random对象 r2.nextBoolean(): true r2.nextInt(): - r2.nextDouble(): 0.26675 r2.nextGaussian(): 2.7946 --------------------------- 种子为100的Random对象 r3.nextBoolean(): true r3.nextInt(): - r3.nextDouble(): 0. r3.nextGaussian(): 0.03859
Random rn=new Random(System.currentTimeMillis());
在多线程环境下使用ThreadLocalRandom 的方式与使用Random 基本类似,如下程序片段示范了ThreadLocalRandom 的用法。
ThreadLocalRandom rand=ThreadLocalRandom.current(); //生成0,20的随机数 int a=rand.nextInt(0, 20); //生成10,100的随机数 double b=rand.nextDouble(10.0, 100.0);
7.3.6 BigDecimal 类
前面在介绍float 、doub le 两种基本浮点类型时已经指出,这两个基本类型的浮点数容易引起精度丢失。先看如下程序。
public class DoubleTest { public static void main(String args[]) { System.out.println("0.05 + 0.01 = " + (0.05 + 0.01)); System.out.println("1.0 - 0.42 = " + (1.0 - 0.42)); System.out.println("4.015 * 100 = " + (4.015 * 100)); System.out.println("123.3 / 100 = " + (123.3 / 100)); } }
程序输出结果是:
0.05 + 0.01 = 0.0000005 1.0 - 0.42 = 0.00001 4.015 * 100 = 401.994 123.3 / 100 = 1.99999
上面程序运行结果表明, Java 的double 类型会发生精度丢失,尤其在进行算术运算时更容易发生这种情况。不仅是Java,很多编程语言也存在这样的问题。
为了能精确表示、计算浮点数, Java 提供了BigDecimal 类, 该类提供了大量的构造器用于创建BigDecimal 对象,包括把所有的基本数值型变量转换成一个BigDecimal 对象,也包括利用数字字符串、数字字符数组来创建BigDecimal 对象。
查看BigDecimal 类的BigDecimal(double val)构造器的详细说明时,可以看到不推荐使用该构造器的说明,主要是因为使用该构造器时有一定的不可预知性。当程序使用new BigDecimal(0.1)来创建一个BigDecimal 对象时,它的值并不是0.1, 它实际上等于一个近似0.1的数。这是因为0.1无法准确地表示为double 浮点数, 所以传入BigDecimal 构造器的值不会正好等于0 .1 (虽然表面上等于该值)。
如果使用BigDecimal(String val)构造器的结果是可预知的一一写入new BigDecimal("0.1 ")将创建一个BigDecimal,它正好等于预期的0.1 。因此通常建议优先使用基于String 的构造器。
如果必须使用double 浮点数作为BigDecimal 构造器的参数时,不要直接将该double 浮点数作为构造器参数创建BigDecimal 对象,而是应该通过BigDecimal. valueOf(double value) 静态方法来创建BigDecimal 对象。
BigDecimal 类提供了add() 、subtract() 、multiply() 、divide() 、pow()等方法对精确浮点数进行常规算术运算。下面程序示范了BigDecimal 的基本运算。
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)); } }
上面程序中f1 和f3都是基于0 .05 创建的BigDecimal 对象,其中f1是基于"0 . 05"字符串,但f3是基于0 . 05 的double 浮点数。运行上面程序,看到如下运行结果:
使用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.000000078125 0.05 - 0.01 = 0.000000078125 0.05 * 0.01 = 0.00000000078125 0.05 / 0.01 = 5.000000000000000078125
从上面运行结果可以看出BigDecimal 进行算术运算的效果, 而且可以看出创建BigDecimal 对象时,一定要使用String 对象作为构造器参数,而不是直接使用double 数字。
如果程序中要求对double 浮点数进行加、减、乘、除基本运算,则需要先将double 类型数值包装成BigDecimal 对象,调用BigDecimal 对象的方法执行运算后再将结果转换成double 型变量。这是比较烦琐的过程,可以考虑以BigDecimal 为基础定义一个Arith 工具类,该工具类代码如下。
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)); } }
Arith 工具类还提供了mam 方法用于测试加、减、乘、除等运算。运行上面程序将看到如下运行结果:
0.05 + 0.01 = 0.06 1.0 - 0.42 = 0.58 4.015 * 100 = 401.5 123.3 / 100 = 1.233
7.4 日期、时间类
Java 原本提供了Date 和Calendar 用于处理日期、时间的类,包括创建日期、时间对象, 获取系统当前日期、时间等操作。但Date 不仅无法实现国际化,而且它对不同属性也使用了前后矛盾的偏移量,比如月份与小时都是从0 开始的,月份中的天数则是从1开始的,年又是从1900 开始的,而java. util. Calendar 则显得过于复杂,从下面介绍中会看到传统Java 对日期、时间处理的不足。Java 8 吸取了Joda-Time 库( 一个被广泛使用的日期、时间库)的经验, 提供了一套全新的日期时间库。
7.4.1 Date 类
- Date(): 生成一个代表当前日期时间的Date 对象。该构造器在底层调用System. currentTimeMillis()获得long 整数作为日期参数。
- Date(long date): 根据指定的long 型整数来生成一个Date 对象。该构造器的参数表示创建的Date对象和GMT 1970 年1 月1 日00:00:00 之间的时间差,以毫秒作为计时单位。与Date 构造器相同的是, Date 对象的大部分方法也Deprecated 了,剩下为数不多的几个方法。
- boolean after(Date when): 测试该日期是否在指定日期when 之后。
- boolean before(Date when): 测试该日期是否在指定日期when 之前。
- long getTime(): 返回该时间对应的long 型整数,即从GMT 1970-01-0100 : 00:00 到该Date 对象之间的时间差,以毫秒作为计时单位。
- void setTime(long time): 设置该Date 对象的时间。
下面程序示范了Date 类的用法。
public class DateTest { public static void main(String[] args) { Date d1 = new Date(); // 获取当前时间之后100ms的时间 Date d2 = new Date(System.currentTimeMillis() + 100); System.out.println(d2); System.out.println(d1.compareTo(d2)); System.out.println(d1.before(d2)); } }
总体来说, Date 是一个设计相当糟糕的类,因此Java 官方推荐尽量少用Date 的构造器和方法。如果需要对日期、时间进行加减运算, 或获取指定时间的年、月、日、时、分、秒信息, 可使用Calendar工具类。
7.4.2 Calendar 类
因为Date 类在设计上存在一些缺陷, 所以Java 提供了Calendar 类来更好地处理日期和时间。Calendar 是一个抽象类, 它用于表示日历。
历史上有着许多种纪年方法,它们的差异实在太大了,比如说一个人的生日是"七月七日",那么一种可能是阳(公)历的七月七日, 但也可以是阴(农)历的日期。为了统一计时,全世界通常选择最普及、最通用的日历: Gregorian Calendar ,也就是日常介绍年份时常用的"公元几几年" 。
Calendar 类本身是一个抽象类, 它是所有日历类的模板,并提供了一些所有日历通用的方法; 但它本身不能直接实例化,程序只能创建Calendar 子类的实例, Java 本身提供了一个GregorianCalendar 类,一个代表格里高利日历的子类,它代表了通常所说的公历。
Calendar 类是一个抽象类,所以不能使用构造器来创建Calendar 对象。但它提供了几个静态getInstanceO方法来获取Calendar 对象,这些方法根据TimeZone , Locale 类来获取特定的Calendar ,如果不指定TimeZone 、Locale , 则使用默认的TimeZone 、Locale 来创建Calendar 。
Calendar 与Date 都是表示日期的工具类, 它们直接可以自由转换,如下代码所示。
// 创建一个默认的Calendar 对象 Calendar calendar=Calendar.getInstance(); // 从Calendar 对象中取出Date 对象 Date date=calendar.getTime(); // 通过Date 对象获得对应的Calendar 对象 // 因为Calendar / GregorianCalendar 没有构造函数可以接收Date 对象 // 所以必须先获得一个Calendar 实例,然后调用其setTime ()方法 Calendar calendar1=Calendar.getInstance(); calendar1.setTime(date);
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): 将给定的日历字段设置为给定值。
- void 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 参数, field 是Calendar 类的类变量, 如Calendar. YEAR 、Calendar. MONTH 等分别代表了年、月、日、小时、分钟、秒等时间宇段。需要指出的是, Calendar.MONTH宇段代表月份, 月份的起始值不是1,而是0 ,所以要设置8 月时,用7 而不是8 。如下程序示范了Calendar类的常规用法。
public class CalendarTest { public static void main(String[] args) { Calendar c = Calendar.getInstance(); // 取出年 System.out.println(c.get(YEAR)); // 取出月份 System.out.println(c.get(MONTH)); // 取出日 System.out.println(c.get(DATE)); // 分别设置年、月、日、小时、分钟、秒 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()); // 将Calendar的月前推8个月 c.roll(MONTH , -8); //2002-03-23 12:32:23 System.out.println(c.getTime()); Calendar cal1 = Calendar.getInstance(); cal1.set(2003, 7, 23, 0, 0 , 0); // 2003-8-23 cal1.add(MONTH, 6); //2003-8-23 => 2004-2-23 System.out.println(cal1.getTime()); 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 java基础类库 System.out.println(cal2.getTime()); 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()); 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()); } }
Calendar 类还有如下几个注意点。
add(int field, int amount)有如下两条规则。
- 当被修改的宇段超出它允许的范围时,会发生进位,即上一级字段也会增大。
- 如果下一级宇段也需要改变,那么该字段会修正到变化最小的值。
roll()的规则与add()的处理规则不同: 当被修改的字段超出它允许的范围时,上一级字段不会增大。下一级字段的处理规则与add()相似。
2. 设置Calendar 的容错性
调用Calendar 对象的set()方法来改变指定时间宇段的值时,有可能传入一个不合法的参数,例如为MONTH 宇段设置13 ,这将会导致怎样的后果呢?看如下程序。
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()); } }
3. set()方法延迟修改
set(f, value)方法将日历宇段f 更改为value , 此外它还设置了一个内部成员变量,以指示日历宇段f己经被更改。尽管日历宇段f 是立即更改的, 但该C alendar 所代表的时间却不会立即修改,直到下次调用get() 、getTime()、getTimeInMillis() 、add() 或roll()时才会重新计算日历的时间。这被称为set()方法的延迟修改, 采用延迟修改的优势是多次调用set()不会触发多次不必要的计算(需要计算出一个代表实际时间的long 型整数) 。
下面程序演示了set()方法延迟修改的效果。
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()); //③ } }
7.4.3 Java 8 新增的日期、时间包
Java 8 开始专门新增了一个java.time 包, 该包下包含了如下常用的类。
- Clock: 该类用于获取指定时区的当前日期、时间。该类可取代System 类的currentTimeMillis()方法,而且提供了更多方法来获取当前日期、时间。该类提供了大量静态方法来获取Clock 对象。
- Duration: 该类代表持续时间。该类可以非常方便地获取一段时间。
- Instant: 代表一个具体的时刻,可以精确到纳秒。该类提供了静态的now()方法来获取当前时刻,也提供了静态的now(Clock clock)方法来获取clock 对应的时刻。除此之外,它还提供了一系列minusXxx()方法在当前时刻基础上减去一段时间, 也提供了plusXxx()方法在当前时刻基础上加上一段时间。
- LocalDate : 该类代表不带时区的日期,例如2007-12-03 。该类提供了静态的now()方法来获取当前日期,也提供了静态的now(Clock clock)方法来获取clock 对应的日期。除此之外, 它还提供了minusXxx()方法在当前年份基础上减去几年、几月、几周或几日等,也提供了plusXxx()方法在当前年份基础上加上几年、几月、几周或几日等。
- LocalTime: 该类代表不带时区的时间,例如10:15 : 30 。该类提供了静态的now()方法来获取当前时间,也提供了静态的now(Clock clock)方法来获取clock 对应的时间。除此之外,它还提供了minusXxx()方法在当前年份基础上减去几小时、几分、几秒等,也提供了plusXxx()方法在当前年份基础上加上几小时、几分、几秒等。
- LocalDateTime: 该类代表不带时区的日期、时间, 例如2007-12-03Tl 0: 15 :3 0 。该类提供了静态的now()方法来获取当前日期、时间,也提供了静态的now(Clock clock)方法来获取clock 对应的日期、时间。除此之外,它还提供了minusXxx()方法在当前年份基础上减去几年、几月、几日、几小时、几分、几秒等, 也提供了plusXxx()方法在当前年份基础上加上几年、几月、几日、几小时、几分、几秒等。
- MonthDay: 该类仅代表月日,例如04-12 。该类提供了静态的now()方法来获取当前月日,也提供了静态的now(Clock clock)方法来获取clock 对应的月日。
- Year: 该类仅代表年,例如2014 。该类提供了静态的now()方法来获取当前年份,也提供了静态的now(Clock clock)方法来获取clock 对应的年份。除此之外,它还提供了minusYears()方法在当前年份基础上减去几年,也提供了plusYears()方法在当前年份基础上加上几年。
- YearMonth: 该类仅代表年月, 例如2014-04 。该类提供了静态的now()方法来获取当前年月,也提供了静态的now(Clock clock)方法来获取clock 对应的年月。除此之外,它还提供了minusXxx()方法在当前年月基础上减去几年、几月,也提供了plusXxx()方法在当前年月基础上加上几年、几月。
- ZonedDateTime : 该类代表一个时区化的日期、时间。
- Zoneld: 该类代表一个时区。
- DayOtweek: 这是一个枚举类, 定义了周日到周六的枚举值。
- Month: 这也是一个枚举类, 定义了一月到十二月的枚举值。
下面通过一个简单的程序来示范这些类的用法。
public class NewDatePackageTest { public static void main(String[] args) { // -----下面是关于Clock的用法----- // 获取当前Clock Clock clock = Clock.systemUTC(); // 通过Clock获取当前时刻 System.out.println("当前时刻为:" + clock.instant()); // 获取clock对应的毫秒数,与System.currentTimeMillis()输出相同 System.out.println(clock.millis()); System.out.println(System.currentTimeMillis()); // -----下面是关于Duration的用法----- Duration d = Duration.ofSeconds(6000); System.out.println("6000秒相当于" + d.toMinutes() + "分"); System.out.println("6000秒相当于" + d.toHours() + "小时"); System.out.println("6000秒相当于" + d.toDays() + "天"); // 在clock基础上增加6000秒,返回新的Clock Clock clock2 = Clock.offset(clock, d); // 可看到clock2与clock1相差1小时40分 System.out.println("当前时刻加6000秒为:" +clock2.instant()); // -----下面是关于Instant的用法----- // 获取当前时间 Instant instant = Instant.now(); System.out.println(instant); // instant添加6000秒(即100分钟),返回新的Instant Instant instant2 = instant.plusSeconds(6000); System.out.println(instant2); // 根据字符串中解析Instant对象 Instant instant3 = Instant.parse("2014-02-23T10:12:35.342Z"); System.out.println(instant3); // 在instant3的基础上添加5小时4分钟 Instant instant4 = instant3.plus(Duration .ofHours(5).plusMinutes(4)); System.out.println(instant4); // 获取instant4的5天以前的时刻 Instant instant5 = instant4.minus(Duration.ofDays(5)); System.out.println(instant5); // -----下面是关于LocalDate的用法----- LocalDate localDate = LocalDate.now(); System.out.println(localDate); // 获得2014年的第146天 localDate = LocalDate.ofYearDay(2014, 146); System.out.println(localDate); // 2014-05-26 // 设置为2014年5月21日 localDate = LocalDate.of(2014, Month.MAY, 21); System.out.println(localDate); // 2014-05-21 // -----下面是关于LocalTime的用法----- // 获取当前时间 LocalTime localTime = LocalTime.now(); // 设置为22点33分 localTime = LocalTime.of(22, 33); System.out.println(localTime); // 22:33 // 返回一天中的第5503秒 localTime = LocalTime.ofSecondOfDay(5503); System.out.println(localTime); // 01:31:43 // -----下面是关于localDateTime的用法----- // 获取当前日期、时间 LocalDateTime localDateTime = LocalDateTime.now(); // 当前日期、时间加上25小时3分钟 LocalDateTime future = localDateTime.plusHours(25).plusMinutes(3); System.out.println("当前日期、时间的25小时3分之后:" + future); // 下面是关于Year、YearMonth、MonthDay的用法示例----- Year year = Year.now(); // 获取当前的年份 System.out.println("当前年份:" + year); // 输出当前年份 year = year.plusYears(5); // 当前年份再加5年 System.out.println("当前年份再过5年:" + year); // 根据指定月份获取YearMonth YearMonth ym = year.atMonth(10); System.out.println("year年10月:" + ym); // 输出XXXX-10,XXXX代表当前年份 // 当前年月再加5年,减3个月 ym = ym.plusYears(5).minusMonths(3); System.out.println("year年10月再加5年、减3个月:" + ym); MonthDay md = MonthDay.now(); System.out.println("当前月日:" + md); // 输出--XX-XX,代表几月几日 // 设置为5月23日 MonthDay md2 = md.with(Month.MAY).withDayOfMonth(23); System.out.println("5月23日为:" + md2); // 输出--05-23 } }
7.5 正则表达式
正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作。String类里也提供了如下几个特殊的方法。
- boolean matches(String regex): 判断该宇符串是否匹配指定的正则表达式。
- String replaceAll(String regex, String replacement): 将该宇符串中所有匹配regex 的子串替换成replacement。
- String replaceFirst(String regex, String replacement): 将该字符串中第一个匹配regex 的子串替换成replacement 。
- String[] split(String regex): 以regex 作为分隔符,把该字符串分割成多个子串。
上面这些特殊的方法都依赖于Java 提供的正则表达式支持,除此之外, Java 还提供了Pattem 和Matcher 两个类专门用于提供正则表达式支持。
很多读者都会觉得正则表达式是一个非常神奇、高级的知识,其实正则表达式是一种非常简单而且非常实用的工具。正则表达式是一个用于匹配字符串的模板。实际上,任意字符串都可以当成正则表达式使用,例如"abc" , 它也是一个正则表达式,只是它只能匹配"abc"字符串。
如果正则表达式仅能匹配"abc"这样的字符串,那么正则表达式也就不值得学习了。下面开始学习如何创建正则表达式。
7.5.1 创建正则表达式
前面己经介绍了, 正则表达式就是一个用于匹配字符串的模板,可以匹配一批字符串,所以创建正则表达式就是创建一个特殊的字符串。正则表达式所支持的合法字符如表7.1 所示。
除此之外,正则表达式中有一些特殊字符,这些特殊字符在正则表达式中有其特殊的用途,比如前面介绍的反斜线() 。如果需要匹配这些特殊字符,就必须首先将这些字符转义,也就是在前面添加一个反斜线() 。正则表达式中的特殊字符如表7. 2 所示。
上面的正则表达式依然只能匹配单个字符,这是因为还未在正则表达式中使用"通配符","通配符"是可以匹配多个字符的特殊字符。正则表达式中的"通配符"远远超出了普通通配符的功能,它被称为预定义字符,正则表达式支持如表7.3 所示的预定义字符。
上面的7 个预定义字符其实很容易记忆——d 是digit 的意思,代表数字, s 是space的意思, 代表空白, W 是word 的意思, 代表单词。d 、s 、w 的大写形式恰好匹配与之相反的字符。
在一些特殊情况下,例如,若只想、匹配a~ f 的字母,或者匹配除ab 之外的所有小写字母,或者匹配中文字符,上面这些预定义字符就无能为力了,此时就需要使用方括号表达式,方括号表达式有如表7 .4 所示的几种形式。
前面例子中需要建立一个匹配000-000-0000 形式的电话号码时, 使用了\d\d\d-\d\d\d-\d\d\d\d正则表达式,这看起来比较烦琐。实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式。
- Greedy (贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去, 直到无法匹配为止。如果你发现表达式匹配的结果与预期的不符, 很有可能是因为一一你以为表达式只会匹配前面几个宇符,而实际上它是贪婪模式,所以会一直匹配下去。
- Reluctant (勉强模式):用问号后缀(?)表示,它只会匹配最少的字符。也称为最小匹配模式。
- Possessive (占有模式) : 用加号后缀(+)表示,目前只有Java 支持占有模式, 通常比较少用。
三种模式的数量表示符如表7.6 所示。
关于贪婪模式和勉强模式的对比, 看如下代码:
String str = "hello , java !" ; // 贪婪模式的正则表达式 System.out . println(str.replaceFirst( "\w*" , "□" )) ; //输出□ , java! //勉强模式的正则表达式 System. out . println(str.replaceFirst( "\w*? " , "□" ) ) ; //输出□hello , java!
当从"hello , java ! "字符串中查找匹配"\w*''子串时,因为"w*"使用了贪婪模式, 数量表示符( * )会一直匹配下去, 所以该字符串前面的所有单词字符都被它匹配到, 直到遇到空格,所以替换后的效果是"□, java! " ; 如果使用勉强模式, 数量表示符(*)会尽量匹配最少字符, 即匹配0 个字符, 所以替换后的结果是" □hello , java ! " 。
7.5.2 使用正则表达式
一旦在程序中定义了正则表达式,就可以使用Pattern 和Matcher 来使用正则表达式。
Pattem 对象是正则表达式编译后在内存中的表示形式,因此, 正则表达式宇符串必须先被编译为Pattern 对象, 然后再利用该Pattern 对象创建对应的Matcher 对象。执行匹配所涉及的状态保留在Matcher对象中, 多个Matcher 对象可共享同一个Pattern 对象。
因此, 典型的调用顺序如下:
// 将一个字符串编译成Pattern 对象 Pattern p = Pattern. compile( "a*b" ) ; // 使用Pattern 对象创建Matcher 对象 Matcher m = p.matcher( "aaaaab" ) ; boolean b = m.matches( ); // 返回true
上面定义的Pattern 对象可以多次重复使用。如果某个正则表达式仅需一次使用, 则可直接使用Pattern 类的静态matches()方法,此方法自动把指定字符串编译成匿名的Pattern 对象,并执行匹配, 如下所示。
boolean b = Pattern .matches( "a*b ", "aaaaab"); // 返回true
上面语句等效于前面的三条语句。但采用这种语句每次都需要重新编译新的Pattern 对象,不能重复利用己编译的Pattern 对象, 所以效率不高。
Pattem 是不可变类, 可供多个并发线程安全使用。
Matcher 类提供了如下几个常用方法。
- find(): 返回目标字符串中是否包含与Pattern 匹配的子串。
- group () : 返回上一次与Pattern 匹配的子串。
- start(): 返回上一次与Pattern 匹配的子串在目标字符串中的开始位置。
- end(): 返回上一次与Pattern 匹配的子串在目标字符串中的结束位置加1 。
- lookingAt() : 返回目标字符串前面部分与Pattern 是否匹配。
- matches() : 返回整个目标字符串与Pattern是否匹配。
- reset(): 将现有的Matcher 对象应用于一个新的字符序列。
通过Matcher 类的find()和group()方法可以从目标字符串中依次取出特定子串(匹配正则表达式的子串) ,例如互联网的网络爬虫, 它们可以自动从网页中识别出所有的电话号码。下面程序示范了如何从大段的宇符串中找出电话号码。
public class FindGroup { public static void main(String[] args) { // 使用字符串模拟从网络上得到的网页源码 String str = "我想求购一本《疯狂Java讲义》,尽快联系我" + "交朋友,电话号码是" + "出售二手电脑,联系方式"; // 创建一个Pattern对象,并用它建立一个Matcher对象 // 该正则表达式只抓取13X和15X段的手机号, // 实际要抓取哪些电话号码,只要修改正则表达式即可。 Matcher m = Pattern.compile("((13\d)|(15\d))\d{8}") .matcher(str); // 将所有符合正则表达式的子串(电话号码)全部输出 while(m.find()) { System.out.println(m.group()); } } }
从上面运行结果可以看出, find()方法依次查找字符串中与Pattern 匹配的子串, 一旦找到对应的子串,下次调用find()方法时将接着向下查找。
find()方法还可以传入一个int 类型的参数,带int 参数的find()方法将从该int 索引处向下搜索。start()和end()方法主要用于确定子串在目标字符串中的位置,如下程序所示。
public class StartEnd { public static void main(String[] args) { // 创建一个Pattern对象,并用它建立一个Matcher对象 String regStr = "Java is very easy!"; System.out.println("目标字符串是:" + regStr); Matcher m = Pattern.compile("\w+") .matcher(regStr); while(m.find()) { System.out.println(m.group() + "子串的起始位置:" + m.start() + ",其结束位置:" + m.end()); } } }
上面程序使用find() 、group()方法逐项取出目标字符串中与指定正则表达式匹配的子串, 并使用start()、end()方法返回子串在目标字符串中的位置。运行上面程序,看到如下运行结果:
目标字符串:Java is very easy! Java子串的起始位置:0,其结束位置:4 is子串的起始位置:5,其结束位置:7 very子串的起始位置:8,其结束位置:12 easy子串的起始位置:13,其结束位置:17
matches()和lookingAt()方法有点相似,只是matches()方法要求整个字符串和Pattern 完全匹配时才返回true ,而lookingAt()只要字符串以Pattern 开头就会返回true。 reset()方法可将现有的Matcher 对象应用于新的字符序列。看如下例子程序。
public class MatchesTest { public static void main(String[] args) { String[] mails = { "" , "", "", "" }; String mailRegEx = "\w{3,20}@\w+\.(com|org|cn|net|gov)"; Pattern mailPattern = Pattern.compile(mailRegEx); Matcher matcher = null; for (String mail : mails) { if (matcher == null) { matcher = mailPattern.matcher(mail); } else { matcher.reset(mail); } String result = mail + (matcher.matches() ? "是" : "不是") + "一个有效的邮件地址!"; System.out.println(result); } } }
除此之外,还可以利用正则表达式对目标字符串进行分割、查找、替换等操作,看如下例子程序。
public class ReplaceTest { public static void main(String[] args) { String[] msgs = { "Java has regular expressions in 1.4", "regular expressions now expressing in Java", "Java represses oracular expressions" }; Pattern p = Pattern.compile("re\w*"); Matcher matcher = null; for (int i = 0 ; i < msgs.length ; i++) { if (matcher == null) { matcher = p.matcher(msgs[i]); } else { matcher.reset(msgs[i]); } System.out.println(matcher.replaceAll("哈哈:)")); } } }
public class StringReg { public static void main(String[] args) { String[] msgs = { "Java has regular expressions in 1.4", "regular expressions now expressing in Java", "Java represses oracular expressions" }; for (String msg : msgs) { System.out.println(msg.replaceFirst("re\w*" , "哈哈:)")); System.out.println(Arrays.toString(msg.split(" "))); } } }
7.6 变量处理和方法处理
Java 9 引入了一个新的VarHandle 类,井增强了原有的MethodHandle 类。通过这两个类,允许Java像动态语言一样引用变量、引用方法,并调用它们。
7.6.1 Java 9 增强的MethodHandle
- MethodHandles: MethodHandle 的工厂类, 它提供了一系列静态方法用于获取MethodHandle 。
- MethodHandles.Lookup: Lookup 静态内部类也是MethodHandle 、VarHandle 的工厂类,专门用于获取MethodHandle 和VarHandle 。
- MethodType: 代表一个方法类型。MethodType 根据方法的形参、返回值类型来确定方法类型。
下面程序示范了MethodHandle 的用法。
public class MethodHandleTest { // 定义一个private的类方法 private static void hello() { System.out.println("Hello world!"); } // 定义一个private的实例方法 private String hello(String name) { System.out.println("执行带参数的hello" + name); return name + ",您好"; } public static void main(String[] args) throws Throwable { // 定义一个返回值为void、不带形参的方法类型 MethodType type = MethodType.methodType(void.class); // 使用MethodHandles.Lookup的findStatic获取类方法 MethodHandle mtd = MethodHandles.lookup() .findStatic(MethodHandleTest.class, "hello", type); // 通过MethodHandle执行方法 mtd.invoke(); // 使用MethodHandles.Lookup的findVirtual获取实例方法 MethodHandle mtd2 = MethodHandles.lookup() .findVirtual(MethodHandleTest.class, "hello", // 指定获取返回值为String, 形参为String的方法类型 MethodType.methodType(String.class, String.class)); // 通过MethodHandle执行方法,传入主调对象和参数 System.out.println(mtd2.invoke(new MethodHandleTest(), "孙悟空")); } }
程序使用MethodHandles.Lookup 对象根据类、方法名、方法类型来获取MethodHandle 对象。由于此处的方法名只是一个字符串,而该字符串可以来自于变量、配置文件等,这意味着通过MethodHandle 可以让Java 动态调用某个方法。
7.6.2 Java 9 增加的VarHandle
class User { String name; static int MAX_AGE; } public class VarHandleTest { public static void main(String[] args)throws Throwable { String[] sa = new String[]{"Java", "Kotlin", "Go"}; // 获取一个String[]数组的VarHandle对象 VarHandle avh = MethodHandles.arrayElementVarHandle(String[].class); // 比较并设置:如果第三个元素是Go,则该元素被设为Lua boolean r = avh.compareAndSet(sa, 2, "Go", "Lua"); // 输出比较结果 System.out.println(r); // 输出true // 看到第三个元素被替换成Lua System.out.println(Arrays.toString(sa)); // 获取sa数组的第二个元素 System.out.println(avh.get(sa, 1)); // 输出Kotlin // 获取并设置:返回第三个元素,并将第三个元素设为Swift System.out.println(avh.getAndSet(sa, 2, "Swift")); // 看到第三个元素被替换成Swift System.out.println(Arrays.toString(sa)); // 用findVarHandle方法获取User类中名为name, // 类型为String的实例变量 VarHandle vh1 = MethodHandles.lookup().findVarHandle(User.class, "name", String.class); User user = new User(); // 通过VarHandle获取实例变量的值,需要传入对象作为调用者 System.out.println(vh1.get(user)); // 输出null // 通过VarHandle设置指定实例变量的值 vh1.set(user, "孙悟空"); // 输出user的name实例变量的值 System.out.println(user.name); // 输出孙悟空 // 用findVarHandle方法获取User类中名为MAX_AGE, // 类型为Integer的类变量 VarHandle vh2 = MethodHandles.lookup().findStaticVarHandle(User.class, "MAX_AGE", int.class); // 通过VarHandle获取指定类变量的值 System.out.println(vh2.get()); // 输出0 // 通过VarHandle设置指定类变量的值 vh2.set(100); // 输出User的MAX_AGE类变量 System.out.println(User.MAX_AGE); // 输出100 } }
从上面前两行粗体字代码可以看出,程序调用MethodHandles 类的静态方法可获取操作数组的VarHandle 对象,接下来程序可通过VarHandle 对象来操作数组的方法,包括比较并设置数组元素、获取并设置数组元素等, VarHandle 具体支持哪些方法则可参考API 文档。
上面程序中后面三行粗体字代码则示范了使用VarHand le 操作实例变量的情形,由于实例变量需要使用对象来访问, 因此使用VarHandle 操作实例变量时需要传入一个User 对象。VarHandle 既可设置实例变量的值,也可获取实例变量的值。当然VarHandle 也提供了更多的方法来操作实例变量,具体可参考API 文挡。
使用VarHandle 操作类变量与操作实例变量差别不大,区别只是类变量不需要对象,因此使用VarHandle 操作类变量时无须传入对象作为参数。
VarHandle 与MethodH andle 一样,它也是一种动态调用机制,当程序通过MethodHandles.Lookup来获取成员变量时,可根据字符串名称来获取成员变量,这个字符串名称同样可以是动态改变的,因此非常灵活。
7.7 Java 9 改进的国际化与格式化
全球化的Intemet 需要全球化的软件。全球化软件,意味着同一种版本的产品能够容易地适用于不同地区的市场, 软件的全球化意味着国际化和本地化。当一个应用需要在全球范围使用时,就必须考虑在不同的地域和语言环境下的使用情况,最简单的要求就是用户界面上的信息可以用本地化语言来显示。
国际化是指应用程序运行时,可根据客户端请求来自的国家/地区、语言的不同而显示不同的界面。例如,如果请求来自于中文操作系统的客户端,则应用程序中的各种提示信息错误和帮助等都使用中文文字: 如果客户端使用英文操作系统, 则应用程序能自动识别,并做出英文的响应。
引入国际化的目的是为了提供自适应、更友好的用户界面,并不需要改变程序的逻辑功能。国际化的英文单词是Intemationalization , 因为这个单词太长了,有时也简称I18N ,其中I 是这个单词的第一个字母, 18 表示中间省略的字母个数,而N 代表这个单词的最后一个字母。
一个国际化支持很好的应用, 在不同的区域使用时, 会呈现出本地语言的提示。这个过程也被称为Localization , 即本地化。类似于国际化可以称为I18N ,本地化也可以称为L10N 。
Java 9 国际化支持升级到了Unicode 8.0 字符集,因此提供了对不同国家、不同语言的支持,它已经具有了国际化和本地化的特征及API, 因此Java 程序的国际化相对比较简单。尽管Java 开发工具为国际化和本地化的工作提供了一些基本的类, 但还是有一些对于Java 应用程序的本地化和国际化来说较困难的工作,例如: 消息获取,编码转换,显示布局和数字、日期、货币的格式等。
当然,一个优秀的全球化软件产品,对国际化和本地化的要求远远不止于此,甚至还包括用户提交数据的国际化和本地化。
7.7.1 Java 国际化的思路
Java 程序的国际化思路是将程序中的标签、提示等信息放在资源文件中, 程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value 对,每个资源文件中的key 是不变的,但value 则随不同的国家、语言而改变。
Java 程序的国际化主要通过如下三个类完成。
- java.util. ResourceBundle: 用于加载国家、语言资源包。
- java.util. Locale: 用于封装特定的国家/区域、语言环境。
- java.text. MessageFormat: 用于格式化带占位符的字符串。
为了实现程序的国际化,必须先提供程序所需要的资源文件。资源文件的内容是很多key-value 对,其中key 是程序使用的部分,而value 则是程序界面的显示字符串。资源文件的命名可以有如下三种形式。
- baseName_language_country. properties
- baseName_language. properties
- baseName.properties
其中baseName 是资源文件的基本名, 用户可随意指定:而language 和country都不可随意变化,必须是Java 所支持的语言和国家。
7.7.2 Java 支持的国家和语言
public class LocaleList { public static void main(String[] args) { // 返回Java所支持的全部国家和语言的数组 Locale[] localeList = Locale.getAvailableLocales(); // 遍历数组的每个元素,依次获取所支持的国家和语言 for (int i = 0; i < localeList.length ; i++ ) { // 输出出所支持的国家和语言 System.out.println(localeList[i].getDisplayCountry() + "=" + localeList[i].getCountry()+ " " + localeList[i].getDisplayLanguage() + "=" + localeList[i].getLanguage()); } } }
7.7.3 完成程序国际化
对于如下最简单的程序:
public class RawHello { public static void main(String[] args) { System.out.println("Hello World"); } }
这个程序的执行结果也很简单一一肯定是打印出简单的"Hello World" 字符串,不管在哪里执行都不会有任何改变!为了让该程序支持国际化, 肯定不能让程序直接输出"Hello World " 字符串,这种写法直接输出一个字符串常量, 永远不会有任何改变。为了让程序可以输出不同的字符串,此处绝不可使用该字符串常量。为了让上面输出的宇符串常量可以改变,可以将需要输出的各种字符串( 不同的国家/语言环境对应不同的宇符串)定义在资源包中。
为上面程序提供如下两个文件。
第一个文件: mess _ zh_CN .properties , 该文件的内容为:
#资源文件的内容是key - value 对 hello =你好!
第二个文件: mess_en_US .properties , 该文件的内容为:
# 资源文件的内容是key - value 对 hello=Welcome !
Java 9 支持使用UTF- 8 字符集来保存属性文件,这样在属性文件中就可以直接包含非西欧字符,因此属性文件也不再需要使用native2ascii 工具进行处理。唯一要注意的是,属性文件必须显式保存为UTF- 8 字符集。
看到这两份文件文件名的baseName 是相同的: mess 。前面己经介绍了资源文件的三种命名方式,其中baseName 后面的国家、语言必须是Java 所支持的国家、语言组合。将上面的Java 程序修改成如下形式。
public class Hello { public static void main(String[] args) { // 取得系统默认的国家/语言环境 Locale myLocale = Locale.getDefault(Locale.Category.FORMAT); // 根据指定国家/语言环境加载资源文件 ResourceBundle bundle = ResourceBundle .getBundle("mess" , myLocale); // 打印从资源文件中取得的消息 System.out.println(bundle.getString("hello")); } }
上面程序中的打印语句不再是直接打印"Hello World" 字符串,而是打印从资源包中读取的信息。如果在中文环境下运行该程序,将打印"你好!"; 如果在"控制面板"中将机器的语言环境设置成美国,然后再次运行该程序,将打印"Welcome! "字符串。
从上面程序可以看出,如果希望程序完成国际化,只需要将不同的国家/语言(Locale) 的提示信息分别以不同的文件存放即可。例如,简体中文的语言资源文件就是Xxx_zh_CN. properties 文件,而美国英语的语言资源文件就是Xxx_en_US.properties 文件。
Java 程序国际化的关键类是ResourceBundle ,它有一个静态方法: getBundle(String baseName, Locale locale) ,该方法将根据Locale 加载资源文件,而Locale 封装了一个国家、语言,例如,简体中文环境可以用简体中文的Locale 代表,美国英语环境可以用美国英语的Locale 代表。
从上面资源文件的命名中可以看出,不同国家、语言环境的资源文件的baseName 是相同的,即baseName 为mess 的资源文件有很多个,不同的国家、语言环境对应不同的资源文件。
Java 程序国际化的关键类是ResourceBundle 和Locale , ResourceBundle 根据不同的Locale 加载语言资源文件,再根据指定的key 取得己加载语言资源文件中的字符串。
7.7.4 使用MessageFormat 处理包含占位符的字符串
此时需要使用MessageFormat 类,该类包含一个有用的静态方法。
- format(String pattern , Object... values) : 返回后面的多个参数值填充前面的pattern 字符串,其中pattem 字符串不是正则表达式,而是一个带占位符的字符串。
借助于上面的MessageFormat 类的帮助, 将国际化程序修改成如下形式。
public class HelloArg { public static void main(String[] args) { // 定义一个Locale变量 Locale currentLocale = null; // 如果运行程序的指定了两个参数 if (args.length == 2) { // 使用运行程序的两个参数构造Locale实例 currentLocale = new Locale(args[0] , args[1]); } else { // 否则直接使用系统默认的Locale currentLocale = Locale.getDefault(Locale.Category.FORMAT); } // 根据Locale加载语言资源 ResourceBundle bundle = ResourceBundle .getBundle("myMess" , currentLocale); // 取得已加载的语言资源文件中msg对应消息 String msg = bundle.getString("msg"); // 使用MessageFormat为带占位符的字符串传入参数 System.out.println(MessageFormat.format(msg , "yeeku" , new Date())); } }
从上面的程序中可以看出,对于带占位符的消息字符串,只需要使用MessageFormat 类的format()方法为消息中的占位符指定参数即可。
7.7.5 使用类文件代替资源文件
- 该类的类名必须是baseName_language_country,这与属性文件的命名相似。
- 该类必须继承ListResourceBundle, 并重写getContents()方法,该方法返回Object 数组,该数组的每一项都是key-value 对。
下面的类文件可以代替上面的属性文件。
public class myMess_zh_CN extends ListResourceBundle { // 定义资源 private final Object myData[][]= { {"msg","{0},你好!今天的日期是{1}"} }; // 重写方法getContents() public Object[][] getContents() { // 该方法返回资源的key-value对 return myData; } }
(1) baseName_zh_CN.class
( 2) baseName_zh_CN. properties
(3) baseName_zh.class
(4) baseName_zh.properties
( 5) baseName.class
( 6) baseName.properties
系统按上面的顺序搜索资源文件,如果前面的文件不存在,才会使用下一个文件。如果一直找不到对应的文件,系统将抛出异常。
7.7.6 Java 9 新增的日志API
Java 9 强化了原有的日志API.这套日志API 只是定义了记录消息的最小API.开发者可将这些日志消息路由到各种主流的日志框架(如SLF4J 、Log4J 等) ,否则默认使用Java 传统的java. util.logging日志API 。
这套日志API 的用法非常简单,只要两步即可。
1、调用System 类的getLogger(String name)方法获取System.Logger 对象。
2、调用System.Logger 对象的log()方法输出日志。该方法的第一个参数用于指定日志级别。
为了与传统java.util.logging 日志级别、主流日志框架的级别兼容, Java 9 定义了如表7 . 7 所示的日志级别。
该日志级别是一个非常有用的东西: 在开发阶段调试程序时,可能需要大量输出调试信息: 在发布软件时,又希望关掉这些调试信息。此时就可通过日志来实现,只要将系统日志级别调高,所有低于该级别的日志信息就都会被自动关闭,如果将日志级别设为OFF ,那么所有日志信息都会被关闭。
例如,如下程序示范了Java 9 新增的日志API 。
public class LoggerTest { public static void main(String[] args)throws Exception { // 获取System.Logger对象 System.Logger logger = System.getLogger("fkjava"); // 设置系统日志级别(FINE对应DEBUG) Logger.getLogger("fkjava").setLevel(Level.INFO); // 设置使用a.xml保存日志记录 Logger.getLogger("fkjava").addHandler(new FileHandler("a.xml")); logger.log(System.Logger.Level.DEBUG, "debug信息"); logger.log(System.Logger.Level.INFO, "info信息"); logger.log(System.Logger.Level.ERROR, "error信息"); } }
除简单使用之外, Java 9 的日志API 也支持国际化——System 类除使用简单的getLogger(String name)方法获取System.Logger 对象之外,还可使用getLogger(String name, ResourceBundle bundle)方法来获取该对象,该方法需要传入一个国际化语言资源包,这样该Logger 对象即可根据key 来输出国际化的日志信息。
public class LoggerI18N { public static void main(String[] args)throws Exception { // 加载国际化资源包 ResourceBundle rb = ResourceBundle.getBundle("logMess", Locale.getDefault(Locale.Category.FORMAT)); // 获取System.Logger对象 System.Logger logger = System.getLogger("fkjava", rb); // 设置系统日志级别(FINE对应DEBUG) Logger.getLogger("fkjava").setLevel(Level.INFO); // 设置使用a.xml保存日志记录 Logger.getLogger("fkjava").addHandler(new FileHandler("a.xml")); // 下面3个方法的第二个参数是国际化消息的key logger.log(System.Logger.Level.DEBUG, "debug"); logger.log(System.Logger.Level.INFO, "info"); logger.log(System.Logger.Level.ERROR, "error"); } }
7.7.7 使用NumberFormat 格式化数字
MessageFormat 是抽象类Format 的子类, Format 抽象类还有两个子类: NumberFormat 和DateFormat ,它们分别用以实现数值、日期的格式化。NumberFormat 、DateFormat 可将数值、日期转换成宇符串,也可以将字符串转换成数值、日期。
NumberFormat 和DateFormat 都包含了format() 和parse()方法,其中format()用于将数值、日期格式化成字符串, parse()用于将字符串解析成数值、日期。
NumberFormat 也是一个抽象基类,所以无法通过它的构造器来创建NumberFormat 对象,它提供了如下几个类方法来得到NumberFormat 对象。
- getCurrencylnstance(): 返回默认Locale 的货币格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale 的货币格式器。
- getlntegerInstance(): 返回默认Locale 的整数格式器。也可以在调用该方法时传入指定的Locale ,则获取指定Locale 的整数格式器。
- getNumberInstance(): 返回默认Locale 的通用数值格式器。也可以在调用该方法时传入指定的Locale ,则获取指定Locale 的通用数值格式器。
- getPercentlnstance(): 返回默认Locale 的百分数格式器。也可以在调用该方法时传入指定的Locale ,则获取指定Locale 的百分数格式器。
一旦取得了NumberFormat 对象后,就可以调用它的format()方法来格式化数值,包括整数和浮点数。如下例子程序示范了NumberFormat 的三种数字格式化器的用法。
public class NumberFormatTest { public static void main(String[] args) { // 需要被格式化的数字 double db = .567; // 创建四个Locale,分别代表中国、日本、德国、美国 Locale[] locales = {Locale.CHINA, Locale.JAPAN , Locale.GERMAN, Locale.US}; NumberFormat[] nf = new NumberFormat[12]; // 为上面四个Locale创建12个NumberFormat对象 // 每个Locale分别有通用数值格式器、百分比格式器、货币格式器 for (int i = 0 ; i < locales.length ; i++) { nf[i * 3] = NumberFormat.getNumberInstance(locales[i]); nf[i * 3 + 1] = NumberFormat.getPercentInstance(locales[i]); nf[i * 3 + 2] = NumberFormat.getCurrencyInstance(locales[i]); } for (int i = 0 ; i < locales.length ; i++) { String tip = i == 0 ? "----中国的格式----" : i == 1 ? "----日本的格式----" : i == 2 ? "----德国的格式----" :"----美国的格式----"; System.out.println(tip); System.out.println("通用数值格式:" + nf[i * 3].format(db)); System.out.println("百分比数值格式:" + nf[i * 3 + 1].format(db)); System.out.println("货币数值格式:" + nf[i * 3 + 2].format(db)); } } }
7.7.8 使用DateFormat 格式化日期、时间
与NumberFormat 相似的是, DateFormat 也是一个抽象类,DateFormat 对象。
- getDateInstance(): 返回一个日期格式器, 它格式化后的字符串只有日期, 没有时间。该方法可以传入多个参数, 用于指定日期样式和Locale 等参数:如果不指定这些参数,则使用默认参数。
- getTimeInstance(): 返回一个时间格式器, 它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定时间样式和Locale 等参数: 如果不指定这些参数,则使用默认参数。
- getDateTimeInstance(): 返回一个日期、时间格式器, 它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数, 用于指定日期样式、时间样式和Locale 等参数;如果不指定这些参数,则使用默认参数。
上面三个方法可以指定日期样式、时间样式参数,它们是DateFormat 的4 个静态常量: FULL 、LONG 、MEDIUM 和SHORT , 通过这4 个样式参数可以控制生成的格式化宇符串。看如下例子程序。
public class DateFormatTest { public static void main(String[] args) throws ParseException { // 需要被格式化的时间 Date dt = new Date(); // 创建两个Locale,分别代表中国、美国 Locale[] locales = {Locale.CHINA, Locale.US}; DateFormat[] df = new DateFormat[16]; // 为上面两个Locale创建16个DateFormat对象 for (int i = 0 ; i < locales.length ; i++) { df[i * 8] = DateFormat.getDateInstance(SHORT, locales[i]); df[i * 8 + 1] = DateFormat.getDateInstance(MEDIUM, locales[i]); df[i * 8 + 2] = DateFormat.getDateInstance(LONG, locales[i]); df[i * 8 + 3] = DateFormat.getDateInstance(FULL, locales[i]); df[i * 8 + 4] = DateFormat.getTimeInstance(SHORT, locales[i]); df[i * 8 + 5] = DateFormat.getTimeInstance(MEDIUM , locales[i]); df[i * 8 + 6] = DateFormat.getTimeInstance(LONG , locales[i]); df[i * 8 + 7] = DateFormat.getTimeInstance(FULL , locales[i]); } for (int i = 0 ; i < locales.length ; i++) { String tip = i == 0 ? "----中国日期格式----":"----美国日期格式----"; System.out.println(tip); System.out.println("SHORT格式的日期格式:" + df[i * 8].format(dt)); System.out.println("MEDIUM格式的日期格式:" + df[i * 8 + 1].format(dt)); System.out.println("LONG格式的日期格式:" + df[i * 8 + 2].format(dt)); System.out.println("FULL格式的日期格式:" + df[i * 8 + 3].format(dt)); System.out.println("SHORT格式的时间格式:" + df[i * 8 + 4].format(dt)); System.out.println("MEDIUM格式的时间格式:" + df[i * 8 + 5].format(dt)); System.out.println("LONG格式的时间格式:" + df[i * 8 + 6].format(dt)); System.out.println("FULL格式的时间格式:" + df[i * 8 + 7].format(dt)); } String str1 = "2017/10/07"; String str2 = "2017年10月07日"; // 下面输出 Sat Oct 07 00:00:00 CST 2017 System.out.println(DateFormat.getDateInstance().parse(str2)); // 下面输出 Sat Oct 07 00:00:00 CST 2017 System.out.println(DateFormat.getDateInstance(SHORT).parse(str1)); // 下面抛出 ParseException异常 System.out.println(DateFormat.getDateInstance().parse(str1)); } }
7.7.9 使用SimpleDateFormat 格式化日期
SimpleDateFormat 可以非常灵活地格式化Date ,也可以用于解析各种格式的日期字符串。创建SimpleDateFormat 对象时需要传入一个pattern 字符串,这个pattern 不是正则表达式,而是一个日期模板字符串。
public class SimpleDateFormatTest { public static void main(String[] args) throws ParseException { Date d = new Date(); // 创建一个SimpleDateFormat对象 SimpleDateFormat sdf1 = new SimpleDateFormat("Gyyyy年中第D天"); // 将d格式化成日期,输出:公元2017年中第282天 String dateStr = sdf1.format(d); System.out.println(dateStr); // 一个非常特殊的日期字符串 String str = "143月21"; SimpleDateFormat sdf2 = new SimpleDateFormat("yMMMd"); // 将日期字符串解析成日期,输出:Fri Mar 21 00:00:00 CST 2014 System.out.println(sdf2.parse(str)); } }
7.8 Java 8 新增的日期、时间格式器
Java 8 新增的日期、时间API 里不仅包括了Instant、LocalDate 、LocalDateTime 、LocalTime 等代表日期、时间的类,而且在java.time.format 包下提供了一个DateTimeFormatter 格式器类,该类相当于前面介绍的DateFormat 和SimpleDateFormat 的合体,功能非常强大。
- 直接使用静态常量创建DateTimeFormatter 格式器。DateTimeFormatter 类中包含了大量形如ISO_LOCAL_DATE 、ISO_LOCAL_TIME 、ISO_LOCAL_DATE_TIME 等静态常量,这些静态常量本身就是DateTimeFormatter 实例。
- 使用代表不同风格的枚举值来创建DateTimeFormatter 格式器。在FormatStyle 枚举类中定义了FULL 、LONG 、MEDIUM 、SHORT 四个枚举值,它们代表日期、时间的不同风格。
- 根据模式字符串来创建DateTimeFormatter 格式器。类似于SimpleDateFormat , 可以采用模式字符串来创建DateTimeFormatter,如果需要了解DateTimeFormatter 支持哪些模式宇符串,则需要参辛苦该类的API 文档。
7.8.1 使用DateTimeFormatter 完成格式化
使用DateTimeFormatter 将日期、时间(LocalDate 、LocalDateTime 、LocalTime 等实例)格式化为字符串, 可通过如下两种方式。
- 调用DateTimeFormatter 的format(TemporalAccessor temporal)方法执行格式化, 其中LocalDate 、LocalDateTime 、LocalTime 等类都是TemporalAccessor 接口的实现类。
- 调用Local,Date 、LocalDateTime 、LocalTime 等日期、时间对象的format(DateTimeFormatter formatter)方法执行格式化。
上面两种方式的功能相同,用法也基本相似,如下程序示范了使用DateTimeFormatter 来格式化日期、时间。
public class NewFormatterTest { public static void main(String[] args) { DateTimeFormatter[] formatters = new DateTimeFormatter[]{ // 直接使用常量创建DateTimeFormatter格式器 DateTimeFormatter.ISO_LOCAL_DATE, DateTimeFormatter.ISO_LOCAL_TIME, DateTimeFormatter.ISO_LOCAL_DATE_TIME, // 使用本地化的不同风格来创建DateTimeFormatter格式器 DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM), DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG), // 根据模式字符串来创建DateTimeFormatter格式器 DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss") }; LocalDateTime date = LocalDateTime.now(); // 依次使用不同的格式器对LocalDateTime进行格式化 for(int i = 0 ; i < formatters.length ; i++) { // 下面两行代码的作用相同 System.out.println(date.format(formatters[i])); System.out.println(formatters[i].format(date)); } } }
7.8.2 使用DateTimeFormatter 解析字符串
public class NewFormatterParse { public static void main(String[] args) { // 定义一个任意格式的日期时间字符串 String str1 = "2014==04==12 01时06分09秒"; // 根据需要解析的日期、时间字符串定义解析所用的格式器 DateTimeFormatter fomatter1 = DateTimeFormatter .ofPattern("yyyy==MM==dd HH时mm分ss秒"); // 执行解析 LocalDateTime dt1 = LocalDateTime.parse(str1, fomatter1); System.out.println(dt1); // 输出 2014-04-12T01:06:09 // ---下面代码再次解析另一个字符串--- String str2 = "2014$$$4月$$$13 20小时"; DateTimeFormatter fomatter2 = DateTimeFormatter .ofPattern("yyy$$$MMM$$$dd HH小时"); LocalDateTime dt2 = LocalDateTime.parse(str2, fomatter2); System.out.println(dt2); // 输出 2014-04-13T20:00 } }
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/h6javajc/1020.html