第七章 Java基础类库

前言:如果你真正学习到了这里,那么先容许我夸赞一下你,太棒了!哪怕你对前面六篇文章的掌握度达到50%也是非常了不起的了,关于后面的文章学习我只能说,非常简单,只是我们文章会非常详细的介绍实现原理和一些细节,但是真正的代码程序示范并不难,如果你现在再去看那些Java书籍或Java入门视频,光靠前六章文章的学习,你就可以完全轻松的掌握和明白,Java或者说计算机的学习就是一个慢慢积攒的过程,学到了就是自己的,学不会下次争取学会它!
因为文章还有12章属实多,而且内容并没有前六章重要,所以我会缩短部分内容,精挑内容
还有就是,大家可以尝试着自己每次在练习或跟着看文章的实例代码时去尝试创建包和名字符合的类,不要像我一样懒,很多实例的类名都一样,而且还最喜欢用Demo来命名,这个原因一个是图方便另一个是以前学习的习惯,不要做一个Demo战神,一定要多多试试模拟大型开发命名现场…
关于这章的学习还是比较特殊的,最少也要做到跟着敲一遍代码和坚持看完,一定要坚持看完这章内容!
Oracle为Java提供了丰富的类库,Java8提供了4000多个基础类(包括下一章要将要介绍的集合框架),通过这些基础类库可以提供开发效率,降低开发难度。对于合格的Java程序员而言,至少要熟悉Java SE中70%以上的类(当然并不是让去背诵JavaAPI文档),但在反复查阅API文档的过程中,会自动记住大部分类的功能、方法,因此程序员一定要多练,多敲代码。
Java提供了String、StringBuffer和StringBuilder来处理字符串,它们之间存在差别本章会详细介绍,以及如何选择合适的字符串类、Java还提供了Date和Calendar来处理日期,其中Date是一个已经过时的API。
正则表达式是一个非常强大的的文本处理工具,通过正则表达式可以进行对文本内容的查找、替换、分割等操作。String也支持了正则表达式,增加了正则表达式的String类更为强大。

与用户互动

一个程序如果只是简单的按照既定的流程执行程序,无须用户进行任何处理,这个程序总是比较简单的。实际上绝对部分程序都是需要处理用户动作,包括接收用户键盘输入、鼠标动作等。因为现在还未涉及图形用户接口编程,故先演示如何获取用户键盘输入。

运行Java的参数

回忆一个Java的程序入口——main()方法的方法签名(形参列表):

public static void main(String[]args){...}

下面将详细讲解为什么main()方法采用这个方法签名。

  • public修饰符:Java类是由JVM调用,为了让JVM可以自由调用这个main()方法,所以使用public修饰符把这个方法暴露出来。
  • static修饰符:JVM调用这个主方法时,不会先创建该主类的对象,然后通过对象来调用该主方法。JVM直接通过该类来调用主方法,因此使用static修饰该主方法。
  • void返回值:因为主方法被JVM调用,该方法的返回值将返回给JVM,这没有任何意义,因此main()方法没有返回值。
    上面方法中包括了一个字符串数组形参,根据方法调用的规则:谁先调用方法,谁负责为形参赋值。也就是说,main()方法由JVM调用,即args形参应该由JVM负责赋值。但JVM怎么知道如何为args赋值了?先看下面程序。
public class Demo{
    public static void main(String[]args){
        System.out.println(args.length);//输出长度
        for(var arg:args){//遍历每个数组元素
            System.out.println(arg);
        }
    }
}

上面几乎是最简单的“HelloWorld程序,只是这个程序增加了输出args数组的长度,遍历args数组元素的代码。运行Demo类,看到程序仅仅输出了一个0,这表名args数组是一个长度为0的数组——这是合理的。因为计算机没有思考能力的,它只能忠实地执行用户交给它的人物,既然程序没有给args数组设定参数值,那么JVM就不知道args数组的元素,所以JVM将args数组设置成一个长度为0的数组”
改为如下命令来运行上面程序:

java Demo Java Spring

第七章 Java基础类库_第1张图片
这里输出语句不要写成args了否则就会输出对象内存地址。
第七章 Java基础类库_第2张图片
上图可以看出,如果运行Java程序时在类名后面紧跟一个或多个字符串(多个字符串之间以空格隔开),JVM就会把这些字符串依次赋值给args数组元素。运行Java程序中的参数与args的对应关系如上。
如果某参数本身包含了空格。则应该将该参数用双引号括起来,否则JVM会把这个空格当成参数分隔符,而不是参数本身。例如,采用如下命令运行上面程序。

java Demo "Java Spring"

cmd中 “cls”是清理cmd命令台内容、exit是关闭cmd
Win+R可以快速打开CMD
第七章 Java基础类库_第3张图片
可以看到参数变成了一个,并且输出了完整的字符串。

使用Scanner获取键盘输入

这个类是一个比较简单的操作类,我的某个朋友(啥b)最近被这个类搞糊涂了,但是其实这个类并没有什么难的,只是创建对象参数,然后调用它的方法,并且如果它的运行结果有作用,就用变量来保存它的结果就行了。
运行Java程序时传入参数只能在程序开始之前就设定几个固定的参数。对于更复杂的情形,程序需要在运行过程中取得输入,例如,前面介绍的五子棋游戏需要在程序运行过程中获取用户输入的键盘输入。
使用Scanner类可以很方便地获取用户的键盘输入,Scanner是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本数字和字符串值。Scanner类提供了多个构造器,不同的构造器可以接收文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。
Scanner主要提供了两个方法来扫描输入。

  • hasNextXxx():是否有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果只是判断是否包含下一个字符串,则直接使用hasNext()。
  • nextXxx():获取下一个输入项。Xxx的含义和前一个方法中的Xxx相同。
    在默认情况下,Scanner使用空白(包括空格、Tab空格、回车)作为多个输入项之间的分隔符。
    这里我做了一个比较有健壮性的程序。
import java.util.Scanner;

public class Demo{
    public static void main(String[] args) {
        System.out.println("请输入你的内容");
        var sc = new Scanner(System.in);
        //下面一行将只把回车符作为分隔符
        //sc.useDelimiter("\n");
        while (sc.hasNext()) {//判断是否还有下一个输入项
            //输出输入项
            System.out.println("键盘输入的内容是:"+sc.next());
            System.out.println("是否结束?(输入1结束程序,输入任意则继续运行)");
            if(sc.nextInt()==1){
                System.out.println("(1)检测到内容为1,程序即将结束....");
                //System.exit(0);这个方法也可以结束程序但是我们先用return
                return;//结束方法 退出
            }else{
                System.out.println("请继续输入:");
            }
        }
    }
}

在这里插入图片描述
这里如果你并没有添加什么关于这个程序的流程控制,则这个扫描仪几乎会一只直让你输入,除非你输入了会报错的内容,这明显是不人性化的,通过这个红色的小按钮可以结束程序的运行,但是我们是程序员,一旦要有属于程序封装内部的结束方法。
看上面的代码注意:
第七章 Java基础类库_第4张图片
这里是我们进行流程控制判断的条件
第七章 Java基础类库_第5张图片

这里也必须要有else语句来结束这次输入占用。
第七章 Java基础类库_第6张图片
在这里插入图片描述
可以看到程序的红色按钮变黑表示成功结束程序。
如果希望改变Scanner的分隔符则使用:

sc.useDelimiter("\n");//这里仅表示回车

Scanner的读取操作可以被阻塞(当前执行顺序暂停)来等待信息输入。如果输入源没有结束,Scanner又读不到更多输入项时(尤其在键盘输入时比较常见),Scanner的hasNext()和next()方法都有可能阻塞,hasNext()方法是否阻塞与其相关的next()方法是否阻塞无关。
设置分隔符 使用 useDelimiter(String pattern)方法即可,该方法应该是一个正则表达式。关于正则表达式期待后续文章,只要把上面程序中的粗体代码注释去掉,该程序就会把键盘的每行输入当成一个输入项,不会以空格、Tab空白等作为分隔符。
事实上,Scanner提供了两个简单的方法来逐行读取。

  • boolean hasNextLine():返回输入源中是否还有下一行。
  • String nextLine():返回输入源中下一行的字符串。
    Scanner不仅可以获取字符串输入项,也可以获取任何基本类型的输入项,如下程序所示。
import java.util.Scanner;

public class Demo{
    public static void main(String[] args) {
        var sc = new Scanner(System.in);
        while(sc.hasNextLong()){
            System.out.println("键盘输入的内容是:"+sc.nextLong());
        }
    }
}

第七章 Java基础类库_第7张图片
注意上面程序中while语句部分,正如通过hasNextLong()和nextLong()两个方法,Scanner可以直接从输入流中获得long型整数输入项。与此类似的是,如果需要获取其他基本类型的输入项,则可以使用相应的方法。
上面程序不如上一个程序适应性强,因为这个程序要求键盘必须是整数,否则程序就会退出。
Scanner不仅能读取用户的键盘输入,还可以读取文件输入。只要在创建Scanner对象时传入一个File对象作为参数(IO流的内容)作为参数,就可以让Scanner读取该文件的内容。例如下面程序。

import java.io.File;
import java.util.Scanner;

public class Demo{
    public static void main(String[] args)throws Exception {
        var sc = new Scanner(new File("C:\\Users\\YueDie\\Desktop\\毕业前限定一日.txt"));
        System.out.println("毕业前限定一日文件的内容如下:");
        while(sc.hasNextLine()){
            System.out.println(sc.nextLine());
        }
    }
}

第七章 Java基础类库_第8张图片
第七章 Java基础类库_第9张图片
第七章 Java基础类库_第10张图片
上面创建Scanner对象时将一个File对象作为参数,这表明该程序将会读取桌面文档的内容。上面程序使用了hasNextLine()和nextLine()两个方法来读取文件内容。
因为上面程序涉及到IO流和异常,故主程序什么throws Exception表明main方法不做任何异常处理,关于异常请期待第十章。
如果那个曾经被Scanner类搞糊涂的人看到了这里,那么他一定已经在参加Java代码公司了吧…

系统相关

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

System类

System类代表当前Java程序的运行平台,程序不能创建System类的对象,System类提供了一些类变量和类方法,允许直接通过System类来调用这些类变量和类方法。
System类提供了代表标准输入、标准输出和错误输出的类变量,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库的方法。下面程序通过System类来访问操作的环境变量和系统属性。
注意:加载文件和动态链接库主要对native方法有用,对于一些特殊的功能(如果访问操作系统底层硬件设备等)Java程序无法实现,必须借助C语言来完成,此时需要使用C语言为Java提供实现。其实现步骤为:

  1. Java程序中声明native修饰的方法,类似于abstract方法,只有方法签名,没有实现。使用带-h选项的javac命令编译该Java程序,将生成一个.class文件和一个.h头文件。
  2. 写一个.cpp文件实现native方法,这一步需要包含第1步产生的.h文件(这个.h文件中又包含了JDK带的jni.h文件)。
  3. 将第2步的.cpp文件编译成动态链接库文件。
  4. 在Java中用System类的loadLibrary…()方法或Runtime类的loadLibrary()方法加载第3步产生的动态链接库文件,Java程序中就可以调用这个native方法了。
    注意:在Java9以前,javac命令没有-h选项,因此JDK提供了javah命令来为.class文件生成.h头文件。Java10彻底删除了javah命令,javac的-h选项替代了javah。
import java.io.FileOutputStream;
import java.util.Map;
import java.util.Properties;

public class Demo{
    public static void main(String[]args)throws Exception{
        //获取系统所有的环境变量
        Map<String,String> env = System.getenv();
        for(var name:env.keySet()){
            System.out.println(name+"----->"+env.get(name));
        }
        //获得指定环境的值
        System.out.println(System.getenv("JAVA_HOME"));
        //获取所有的系统属性
        Properties props = System.getProperties();
        //将所有的系统属性保存到文件中 txt
        props.store(new FileOutputStream("C:\\Users\\YueDie\\Desktop\\新建文本文档.txt"),"System Properties");
        //输出特定的系统属性
        System.out.println(System.getProperty("os.name"));
    }
}

第七章 Java基础类库_第11张图片
第七章 Java基础类库_第12张图片
上面程序通过调用System类的getenv()、getProperties()、getProperty()等方法来访问程序所在平台环境变量和系统属性,程序运行的结果会输出操作系统所有的环境变量值,并输出JAVA_HOME环境变量,以及os.name系统属性的值,运行结果如上图所示(部分)。
该程序运行结束后还会在当前路径下生成一个txt文件,该文件中记路了当前平台所有系统属性。
System类提供了通知系统进行垃圾回收的gc()方法,以及通知系统进行资源清理的runFinalization()方法
System类还有两个获取系统当前时间的方法:currentTimeMillis()和nanoTime(),它们都返回一个long型整数,实际上它们都返回当前时间与UTC1970年1月1午夜的时间差,前者以毫秒作为单位,后者以纳秒作为单位。必须指出的是,这两个方法返回的时间粒度取决于底层操作系统,可能所在的操作系统根本不支持以毫秒、纳秒作为计时单位。例如。许多操作系统以几十毫秒为单位测量时间,currentTimeMillis()方法不可能返回精确的毫秒数;而nanoTime()方法很少用,因为大部分操作系统都支持使用纳秒作为计时单位。
除此之外,System类的in、out和err分别代表系统的标准输入(通常是键盘)、标准输出(通常是显示器)和错误输出流,并提供了setIn()、setOut()和setErr()方法来改变系统的标准输入、标准输出和标准标错输出流。
关于如何改变系统的标准输入、输出方法,可以期待第十五章
System类还提供一个identityHashCode(Object x)方法,该方法返回指定对象的精确hashCode值,也就是根据该对象的地址计算到的hashCode值。当某个类的hashCode()方法被重写后,该类实例的hashCode()方法就不能唯一地标识该对象;但通过identityHashCode()方法的hashCode值,依然是根据该对象的地址值计算得到的hashCode值。所以,如果两个对象的identityHashCode值相同,则两个对象绝对是同一个对象。

public class Demo{
    public static void main(String[] args) {
        var s1 = new String("Hello");
        var s2 = new String("Hello");
        //String重写了hashCode()方法——改成根据字符序列计算hashCode值
        //因为s1和s2的字符序列相同,所以它们的hashCode()方法返回值相同
        System.out.println(s1.hashCode()+"----"+s2.hashCode());
        System.out.println(System.identityHashCode(s1)+"----"+System.identityHashCode(s2));
        var s3 = "Java";
        var s4 = "Java";
        System.out.println(System.identityHashCode(s3)+"-----"+System.identityHashCode(s4));
    }
}

第七章 Java基础类库_第13张图片
通过indetityHashCode(Object x)方法可以获得对象的indetityHashCode值,这个特殊的identityHashCode值可以唯一地标识该对象。因为identityHashCode值是根据对象的地址值计算得到,所以任何两个对象的identityHashCode值总是不相等。

Runtime类与Java9的ProcessHandle

Runtime类代表Java程序的运行环境,每个Java程序都有一个与之对应的Runtime实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime实例,但可以通过getRuntime()方法获取与之关联的Runtime对象。
与System类相似的,Runtime类也提供了gc()方法和runFinalizetion()方法来通知系统进行垃圾回收、清理系统资源,并提供了load(String filename)和loadLibrary(String libname)方法来加载文件和动态链接库。
Runtime类代表Java程序的运行环境,可以访问JVM的相关的信息,如处理器数量、内存信息等。

public class Demo{
    public static void main(String[] args) {
        var rt = Runtime.getRuntime();
        System.out.println("处理器数量:"+rt.availableProcessors());
        System.out.println("空闲内存数:"+rt.freeMemory());
        System.out.println("总内存数:"+rt.totalMemory());
        System.out.println("可用最大内存数:"+rt.maxMemory());
    }
}

第七章 Java基础类库_第14张图片
上面的那些方法就是Runtime类提供的访问JVM相关信息的方法。除此之外,Runtime类还有一个功能——它可以直接单独启动一个进程来运行操作系统的命令,如下程序所示。

public class Demo{
    public static void main(String[] args)throws Exception {
        var rt = Runtime.getRuntime();
        //打开记事本
        rt.exec("notepad.exe");
    }
}

第七章 Java基础类库_第15张图片
上面程序中代码将启动Windows系统里的“记事本”程序,Runtime提供了一系列exec()方法来运行操作系统命令,关于它们之间的细微差别,请看API文档。
通过exec启动平台台上的命令之后,它就会变成一个进程,Java使用Process来代表进程。Java9还新增了一个ProcessHandle接口,通过该接口可获取进程的ID、父进程和后代进程;通过该接口的onExit()方法可在进程结束时完成某些行为。
ProcessHandle还提供了一个ProcessHandle.Info类,用于进程的命令、参数、启动时间、累计运行时间、用户等信息。下面程序示范了通过ProcessHandle获取进程的相关信息。
第七章 Java基础类库_第16张图片

import java.util.concurrent.CompletableFuture;

public class Demo{
    public static void main(String[] args)throws Exception {
        var 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 info = ph.info();
        System.out.println("进程命令:"+info.command());
        System.out.println("进程参数:"+info.arguments());
        System.out.println("进程启动时间:"+info.startInstant());
        System.out.println("进程累计运行时间:"+info.totalCpuDuration());
        CompletableFuture<ProcessHandle> cf = ph.onExit();
        cf.thenRunAsync(()->{
           System.out.println("程序退出"); 
        });
        Thread.sleep(5000);
    }
}

上面程序比较简单,就是通过代码获取Process对象的ProcessHandle对象,接下来即可通过ProcessHandle对象来获取进程相关的信息。

常用类

本届将介绍Java提供的一些常用类,如String、Math、BigDecimal等的用法。

Object类

Object类是所有累、数组、枚举类的父类,也就是说,Java允许把任何类型的对象赋值给Object类的变量。当定义一个类没有使用extends关键字为它显式指定父类,则该类默认继承Object父类。
因为所有的Java类都是Object类的子类,所以任何Java对象都可以调用Object类方法。Object类提供了如下几个常用方法。

  1. boolean equals(Object obj):判断指定对象与该对象是否相等。此处相等标准是,两个对象是同一个对象,因此该equals()方法通常没有太大的实用价值。
  2. protected void finalize():当系统中没有引用变量用到该对象时,垃圾回收器调用此方法来清理该对象的资源。
  3. ClassgetClass():返回该对象的运行时类,该方法在第十八章还有更详细的介绍。
  4. int hashCode():返回该对象的hashCode值。在默认情况下,Object类的hashCode()方法根据该对象的地址来计算(即与System.identityHashCode(Object x)方法的计算结果相同)。但很多类都重写了Object类的hashCode()方法,不再根据地址来计算其hashCode()方法值。
  5. String toString():返回该对象的字符串表示,当程序使用System.out.printnl()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。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;
    }
}
class User implements Cloneable{
    int age;
    Address address;
    public User(int age){
        this.age=age;
        address = new Address("广州天河");
    }
    public User clone()throws CloneNotSupportedException{
        return (User) super.clone();
    }
}
public class Demo{
    public static void main(String[] args)throws CloneNotSupportedException {
        var u1 = new User(29);
        var u2 = u1.clone();
        System.out.println(u1==u2);//①
        System.out.println(u1.address==u2.address);//②
    }
}

第七章 Java基础类库_第17张图片
上面程序让User类实现了Cloneable接口,而且实现了clone()方法,因此User对象就可实现“自我克隆”——克隆出来的对象只是原有对象的副本。程序在①号粗体字代码处判断原有的User对象与克隆出来的User对象是否相同,程序返回false。
Object类提供的Clone机制只是对对象里各实例变量进行“简单复制”,Object的Clone变量的类型是引用类型,Object的Clone机制也只是简单的赋值这个引用变量,这样原有的对象的引用类型的实例变量与克隆对象的引用类型的实例变量依然指向内存中的同一个实例,所以上面程序在②号代码处输出true。上面程序“克隆”出来的u1、u2所指向的对象在内存中的存储示意图如下。
第七章 Java基础类库_第18张图片
Object类提供的clone()方法不仅能简单地处理“复制对象”的问题,而且这种“自我克隆”机制十分高效。比如clone一个包含100个元素的int[]数组,用系统默认的clone方法比静态copy方法快近2倍。
需要指出的是,Object类的clone()方法虽然简单、易用,但它只是一种“浅克隆”——它只克隆该对象的所有成员变量值,不会对引用类型的成员值所引用的对象进行克隆。如果开发者需要对对象进行“深克隆”,则需要开发者自己进行“递归”克隆,保证所有引用类型的成员变量值所引用的对象都被复制了。

操作对象的Object工具类

Java7增加了一个Objects工具类,它提供了一些工具方法来操作对象,这些工具方法大多是“空指针”安全的。比如你不能确定一个引用变量是否为null,如果贸然地调用该变量的toString()方法,则可能引用NullPointerException异常;但如果使用Objects类提供的toString(Object o)方法,就不会引用空指针异常,当o为null时,程序将返回一个“null”字符串。
Java为工具类的命名习惯是添加一个字母s,比如操作数组的工具类是Arrays,操作集合的工具类是Collections。
如下程序示范了Objects工具类的用法。

import java.util.Objects;

public class Demo{
    static Demo demo;

    public static void main(String[] args) {
        System.out.println(Objects.hashCode(demo));
        System.out.println(Objects.toString(demo));
        System.out.println(Objects.requireNonNull(demo,"demo参数不为null!"));
    }
}

第七章 Java基础类库_第19张图片
上面程序还示范了Objects提供的requireNonNull()方法,当传入的参数不为null时,该方法返回参数本身;否则将引发NullPointerException异常。该方法主要用来对方法形参进行输入校验,例如如下代码:

public Foo(Bar bar){
	//校验bar参数,如果bar参数为null将引发异常;否则this.bar被赋值为bar参数
	this.bar = Objects.requireNonNull(bar);
}

Java9改进的String、StringBuffer和StringBuilder类

字符串就是一连串的字符序列,Java提供了String、StringBuffer和StringBuilder三个类来封装字符串,并提供了一系列方法来操作字符串对象。
String是一个不可变类,即一旦一个String对象被创建以后,包含这个对象的字符序列是不可改变的,直至这个对象被摧毁。
StringBuffer对象则代表了一个字符蓄力可改变的字符串,当StringBuffer被创建以后没通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将这其转换为一个String对象。
StringBuilder类是JDK1.5新增的类,它也代表可变字符串对象。实际上StringBuffer和StringBuilder基本相似,两个类的构造器和方法也基本相同。不同的是StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用StringBuilder类。
String、StringBuilder、StringBuffer都实现了CharSequence接口,因此CharSequence可认为是一个字符串的协议接口。
Java9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。在Java9以前字符串采用char[]数组来保存字符,因此字符串的每个字符占2字节;而Java9及更新版本的JDK的字符串采用byte[]数组再加上一个encoding-flag字段来保存字符,因此字符串的每个字符只占1字节。所以Java9及更新版本的JDK的字符串更加节省空间,但字符串的功能方法没有受到任何影响。
String类提供了大量构造器来创建String对象,其中如下几个有特殊用途。

  • 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。如下代码所示。

var a = "fkit.org";
System.out.println("a.charAt(5):"+a.charAt(5));

在这里插入图片描述

  • int compareTo(String anotherString):比较两个字符串的大小。如果两个字符串的字符序列相等,则返回0;不相等时,从两个字符串第0个字符串开始比较,返回第一个不相等的字符串差。另一种情况,较长字符串的前面部分恰巧是较短字符串,则返回它们的长度差。
public class Demo{
    public static void main(String[] args) {
        var s1="abcdefghijklmn";
        var s2="abcdefghij";
        var s3="abcdefghijalmn";
        System.out.println("s1.compareTo(s2):"+s1.compareTo(s2));//返回长度差
        System.out.println("s1.compareTo(s3):"+s1.compareTo(s3));//返回‘k’-'a'的差
    }
}
s1.compareTo(s2)4
s1.compareTo(s3)10
  • String concat(String str):将该String对象与str连接在一起。与Java提供的字符串连接运算符“+”的功能相同。
  • boolean contentEquals(StringBuffer sb):将该String对象与StringBuffer对象sb进行比较,当他们包含的字符序列相同时返回true。
  • static String copyValueOf(char[]data):将字符数组连缀成字符串,与String(char[]content)构造器的功能相同。
  • static Strign copyValueOf(char[]data,int offset,int count):将char数组的子数组中的元素连缀成字符串,与String(char[]value,int offset, int Count)构造器的功能相同。
  • boolean endsWith(String suffix):返回该String对象是否以suffix结尾。
public class Demo{
    public static void main(String[] args) {
        var s4 ="fkit.org";var s5=".org";
        System.out.println("s4.endsWith(s5):"+s4.endsWith(s5));
    }
}
s4.endsWith(s5)true
  • boolean equals(Object anObject):将该字符串与指定对象比较,如果二者包含的字符序列相等,则返回true;否则返回false。
  • boolean euqalsIgnoreCase(String str):与前一个方法基本相似,只是忽略字符的大小写。
  • byte[]getBytes():将该String对象转换成byte数组。
  • void getChars(int srcBegin,int srcEnd,char[]dst,int dstBegin):该方法将字符串中从srcBegin开始,到srcEnd结束的字符复制到dst字符数组中,其中dstBegin为目标字符组的起始复制位置。
public class Demo{
    public static void main(String[] args) {
        char []s6 = {'I',' ','l','o','v','e',' ','j','a','v','a' };// s6 = I love java
        var s7 = "ejb";
        s7.getChars(0,3,s6,7);
        System.out.println(s6);
    }
}
I love ejba
  • 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开始后第一次出现的位置。
public class Demo{
    public static void main(String[] args) {
        var sa = "www.fkit.org";var ss = "it";
        System.out.println("sa.indexOf('r'):"+sa.indexOf('r'));
        System.out.println("sa.indexOf('r',2):"+sa.indexOf('r',2));
        System.out.println("ss.indexOf(ss):"+sa.indexOf(ss));
    }
}

第七章 Java基础类库_第20张图片

  • String substring(int beginIndex):获取从beginIndex位置开始到结束的子字符串。
  • String substring(int beginIndex,int endIndex):获取从beginIndex位置开始的endIndex位置的子字符串。
  • char[]toCharArray():将该String对象转换成char数组。
  • String toLowerCase():将字符串转换成小写。
  • String toUpperCase():将字符串转换为大写。
var st = "fkjava.org";
System.out.println("st.toUpperCase():"+st.toUpperCase());
System.out.println("st.toLowerCase():"+st.toLowerCase());
public class Demo{
    public static void main(String[] args) {
        var st = "fkjava.org";
        System.out.println("st.toUpperCase():"+st.toUpperCase());
        System.out.println("st.toLowerCase():"+st.toLowerCase());
    }
}
st.toUpperCase():FKJAVA.ORG
st.toLowerCase():fkjava.org
  • static String valueOf(X x):一系列用于将基本类型数字转换为String对象的方法。
    本总结详细列出了String类的各种方法,可能会觉得繁琐,因为这些方法都可以从API文档中找到,所以后面介绍各常用类时不会再列出每个类里所有方法的详细用法了,读者应该自行查阅API文档来掌握各种方法的用法。
    String类是不可变的,String的实例一点生成就不会改变了,例如下面代码。
public class Demo{
    public static void main(String[] args) {
        var str1 = "java";
        str1  = str1+"struts";
        str1 = str1+"spring";
    }
}

上面程序除了使用3个字符串的直接量外,还会额外生成2个字符串直接量——“java”和"strust"连接生成的"javastrust",接着"javastruts"与"spring"连接生成的"javastrutsspring",程序中的str1依然指向3个不同的字符串对象。
因为String是不可变的,所以会额外产生很多临时变量,使用StringBuffer或StringBuilder就可以避免这个问题。
StringBuilder提供了一系列插入、追加、改变该字符串你包含的字符序列的方法。而StringBuffer与其用法完全相同,只是StringBuffer是线程安全的。
String Builder、StrignBuffer有两个属性:length和capacity,其中length属性表示其包含的字符序列的长度。与String对象的length不同的是,StringBuilder、StringBuffer的length是可以改变的,可以通过length()、setLength(int len)方法来访问和修改器字符序列的长度。capacity属性表示StringBuilder的容量,capacity通常比length大,程序通常无须关心capatity属性。如下程序示范了StringBuilder类的用法。

public class Demo{
    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.length());
        System.out.println(sb.capacity());
        sb.setLength(5);//改变StringBuilder的长度 将只保留前面的部分 sb = "avajo"
        System.out.println(sb);
    }
}

第七章 Java基础类库_第21张图片
上面代码示范了StringBuilder类的追加、插入、替换、删除等操作,这些操作改变了StringBuilder里的字符序列,这就是StringBuilder与String之间最大的区别:StringBuilder的字符序列是可变的。从程序看到StringBuilder的length()方法返回其字符序列的长度,而capacity()返回值则比length()返回值大。

Math类

Java提供了基本的+、-、*、/、%等基本算术运算的运算符,但对于更复杂的数学运算,例如,三角函数、对数运算、指数运算等则无能能为。Java提供了Math工具类来完成这些复杂的运算,Math类是一个工具类,它的构造器被定义成private的,因此无法创建Math类的对象;Math类中的所有方法都是类方法,可以直接通过类名来调用它们。Math类除提供了大量静态方法之外,还提供了两个变量:PI和E,正如它们名字所暗示的,它们的值分别等于pai和e。
Math类的所有方法名都明确标识了该方法的作用,读者可自行查阅API来了解Math类各方法的说明。下面程序示范了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));
        //按照IEEE754标准的规定,对两个参数进行余数运算
        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());
    }
}
Math.toDegrees(1.57)89.95437383553924
Math.toRadians(90)1.5707963267948966
Math.acos(1.2):NaN
Math.asin(0.8)0.9272952180016123
Math.atan(2.3)1.1606689862534056
Math.cos(1.57)7.963267107332633E-4
Math.cosh(1.2)1.8106555673243747
Math.sin(1.57):0.9999996829318346
Math.sinh(1.2)1.5094613554121725
Math.tan(0.8)1.0296385570503641
Math.tanh(2.1)0.9704519366134539
Math.atan2(0.1,0.2)0.4636476090008061
Math.floor(-1.2)-2.0
Math.ceil(1.2)2.0
Math.round(2.3)2
Math.sqrt(2.3)1.51657508881031
Math.cbrt(9)2.080083823051904
Math.exp(2)7.38905609893065
Math.hypot(4,4)5.656854249492381
Math.IEEEremainder(5,2)1.0
Math.pow(3,2)9.0
Math.log(12)2.4849066497880004
Math.log10(9)0.9542425094393249
Math.log1p(9)2.302585092994046
Math.abs(-4.5)4.5
Math.copySign(1.2,-1.0)-1.2
Math.signum(2.3)1.0
Math.max(2.3,4.5)4.5
Math.min(1.2,3.4)1.2
Math.nextAfter(1.2,1.0)1.1999999999999997
Math.nextUp(1.2)1.2000000000000002
Math.random()0.5178180559903655

Process finished with exit code 0

上面程序中关于Math类的用法几乎覆盖了Math类的所有数学计算功能,读者可以参考上面程序来学习Math类的用法。

ThreadLocalRandom与Random

Random类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以前时间作为种子),另一个构造器需要程序员显式地传入一个long型整数的种子。
ThreadLocalRandom类的用法与Random类的用法基本相似,它提供了一个静态的current()方法来获取ThreadLocalRandom对象,获取该对象之后即可调用各种nextXxx()来获取伪随机数了。
ThreadLocalRandom与Random都比Math的random()方法提供了更多的方法来生成各种伪随机数,可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数还可以指定生成随机数的范围。关于Random类的用法如下程序所示。

import java.util.Arrays;
import java.util.Random;

public class RandomTest{
    public static void main(String[] args) {
        var rand = new Random();
        System.out.println("rand.nextBoolean():"+rand.nextBoolean());
        var buffer = new byte[16];
        rand.nextBytes(buffer);
        System.out.println(Arrays.toString(buffer));
        //生成0.0~1.0之间的伪随机dobule数
        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());
    }
}

第七章 Java基础类库_第22张图片
从上面程序中可以看出,Random可以提供很多选项来生成伪随机数。
Random使用一个48位的种子,如果这个类的两个实例是用同一个种子创建的,对它们同样的顺序调用方法,则它们会产生相同的数字序列。
下面就对上面的介绍做了一个实验,可以看到当两个Random对象种子相同时,它们产生相同的数字序列。值得指出的是,当使用默认的种子构造Random对象时,它们是属于同一个种子的。

import java.util.Random;

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

    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=55856:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject SeedTest
第一个种子为50的Random对象
r1.nextBoolean()true
r1.nextInt()-1727040520
r1.nextDouble()0.6141579720626675
r1.nextGaussian()2.377650302287946
-------------------
第二个种子为50的Random对象
r2.nextBoolean()true
r2.nextInt()-1727040520
r2.nextDouble()0.6141579720626675
r2.nextGaussian()2.377650302287946
-------------------
第三个种子为50的Random对象
r3.nextBoolean()true
r3.nextInt()-1139614796
r3.nextDouble()0.19497605734770518
r3.nextGaussian()0.6762208162903859
-------------------

Process finished with exit code 0

从上面运行结果来看,只要两个Random对象的种子相同,而且方法的调用顺序也相同,它们就会产生相同的数字序列。也就是说,Random产生的数字并不是真正随机的,而是一种伪随机。
为了避免两个Random产生相同的数字序列,通常推荐使用当前时间作为Random对象的种子,如下代码所示。

Random rand = new Random(System.currentTimeMillis());

在多线程环境下使用ThreadLocalRandom的方法与使用Random基本类似,如下程序示范了ThreadLocalRandom的用法。

ThreadLocalRandom rand = new ThreadLocalRandom.current();
//生成一个4~20之间的伪随机整数
int vall = rand.nextInt(4,20);
//生成一个2.0~10.0之间的伪随机附点数
int val2 = rand.nextDouble(2.0,10.0);

BigDecimal类

前面在介绍float、double两种基本附点类型时已经指出,这两个基本类型的浮点数容易引起精度丢失。先看如下程序。

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.060000000000000005
1.0 - 0.42 =0.5800000000000001
4.015 + 100 = 401.49999999999994
123.3 / 100 =1.2329999999999999

上面程序运行结果表明,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的基本运算。

import java.math.BigDecimal;

public class DoubleTest{
    public static void main(String[] args) {
        var f1 = new BigDecimal("0.05");
        var f2 = BigDecimal.valueOf(0.01);
        var 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("使用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));
    }
}

第七章 Java基础类库_第23张图片
上面程序中f1和f3都是基于0.05创建的BigDecimal对象,其中f1是基于"0.05"字符串,但f3是基于0.05的double浮点数。运行上面程序看到如上的结果。
从上面运行结果可以看出BigDecimal进行算术运算的效果,而且可以看出创建BigDecimal对象时,一定要使用String对象作为参数构造器,而不是直接使用double数字。
创建BigDecimal对象时,不要直接使用 double 浮点数作为构造器参数来调用BigDecimal构造器,否则同样会发生精度的问题。
如果程序中要求对double浮点数进行加、减、乘、除基本运算,则需要先将double类型数字包装成BigDecimal对象,调用BigDecimal对象的方法执行运算后再讲结果转换为double型变量。这就是比较烦琐的过程,可以考虑以BigDecimal为基础定义一个Arith工具类,该工具类代码如下。

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Arith{
    private static final int DEF_DIV_SCALE  = 10;
    private Arith(){}
    public static double add(double v1,double v2){
        var b1 = BigDecimal.valueOf(v1);
        var b2 = BigDecimal.valueOf(v2);
        return b1.add(b2).doubleValue();
    }
    public static double sub(double v1,double v2){
        var b1 = BigDecimal.valueOf(v1);
        var b2 = BigDecimal.valueOf(v2);
        return b2.subtract(b2).doubleValue();
    }
    public static double mul(double v1,double v2){
        var b1 = BigDecimal.valueOf(v1);
        var b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2).doubleValue();
    }
    public static double div(double v1,double v2){
        var b1 = BigDecimal.valueOf(v1);
        var 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));
    }
}
0.05+0.01=0.06
1.0-0.42=0.0
4.015+100=401.5
123.3/100=1.233

上面运行结果才是期望的结果,这也正是使用BigDecimal类的作用。

Java8的日期、时间类

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

Date类

Java提供了Date类来处理日期、时间(此处的Date是指java.util包下的Date类,而不是java.sql包下的Date类),Date即包含日期,也包含时间。Date类从JDK1.0就开始存在了,但正因为它历史悠久,所以它的大部分构造器、方法都已经过时,不再推荐使用了。
Date类提供了6个构造器,其中4个已经Deprecated(Java不再推荐使用,使用不再推荐的构造器时编译器会提出警告信息,并导致程序性能、安全性等方面的问题),剩下的两个构造器如下。

  • Date():生成一个代表当前日期时间的Date对象。该构造器在底层调用System.currentTimeMillis()获得long整数作为日期参数。
  • Date(long date):根据指定的long型整数来生成一个Date对象。该构造器的参数表示创建的Date对象和GMT1970年1月1日00:00:00之间的时间差,以毫秒作为计时单位。
    与Date构造器相同的是,Date对象的大部分方法也Deprecated了,剩下为数不多的几个方法。
  • boolean after(Date when):测试该日期是否在指定日期when之后。
  • boolean before(Date when):测试该日期是否在指定日期when之前。
  • long getTime():返回该时间对应的long型整数,即从GMT1970-01-01 00:00:00到该Date对象之间的时间差,以毫秒为计时单位。
  • void setTime(long time):设置该Date对象的时间。
    下面程序示范了Date类的用法。
import java.util.Date;

public class DateTest{
    public static void main(String []args){
        var d1 = new Date();
        //获得当前时间之后100ms的时间
        var d2 = new Date(System.currentTimeMillis()+100);
        System.out.println(d2);
        System.out.println(d1.compareTo(d2));
        System.out.println(d1.before(d2));
    }
}

这里不要把Date的 import导成 SQL

Sat Mar 05 21:00:39 CST 2022
-1
true

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

Calendar类

因为Date类在设计上存在一些缺陷,所以Java提供了Calendar类来更好地处理日期和时间,Calendar是一个抽象类,它用于表示日历。
你是上有这许多纪年方法,它们的差异实在是太大了,比如说一个人的生日是“七月七日”,那么一种可能是阳(公)历的七月七日,但也可以是阴(农)历的日期。为了统一计时,全世界通常选择最普及、最通用的日历:Gregorian Calendar,也就是日常介绍年份时常用的“公元几几年”。
当然,也可以创建自己的Calendar子类,然后将它作为Calendar对象(这就是多态)。因为篇幅关系,本章不会详细介绍如何扩展Calendar子类,读者可通过互联网查看Calendar各子类的源码来学习。
Calendar类是一个抽象类,所以不能使用构造器来创建Calendar对象。但它提供了几个静态getInstance()方法来获取Calendar对象,这些方法根据TimeZone,Locale类来获取特定的Calendar,如果不指定TimeZone、Locale,则使用默认的TimeZone、Locale来创建Calendar。
关于TimeZone、Locale的介绍请参考本章后面的知识。
Calendar与Date都是表示日期的工具类,它们直接可以自由转换,如下代码所示。

//创建一个默认的Calendar对象
var calendar = Calendar.getInstance();
//从Calendar对象中提取出Date对象
var date = calendar.getTime();
//通过Date对象获得对应的Calendar对象
//因为Calendar/GregorianCalendar没有构造函数可以接收Date对象
//所以必须先获得一个Calendar实例,然后调用其setTime()方法
//所以必须先获得一个Calendar实例,然后调用其setTime()方法
var calendar2 = Galendar.getInstance();
calendar2.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 second):设置Calendar对象的年、月、日、时、分、秒6个字段的值。
    上面的很多方法都需要一个int类型的field参数,field是Calendar类的类变量,如Calendar,YEAR、Calendar.MONTH等分别代表了年、月、日、小时、分钟、秒等时间字段。需要指出的是Calendar.MONTH字段代表月份,月份的其实质不是1,而是0。所以要设置8月时,用7而不是用8。如下程序示范了Calendar类的常规用法。
import java.util.Calendar;

public class CalendarTest{
    public static void main(String[] args) {
        var c = Calendar.getInstance();
        System.out.println(c.get(Calendar.YEAR));
        System.out.println(c.get(Calendar.MONTH));
        System.out.println(c.get(Calendar.DATE));
        c.set(2003,10,23,12,32,23);
        c.add(Calendar.YEAR,-1);
        c.roll(Calendar.MONTH,-8);
        System.out.println(c.getTime());
    }
}

第七章 Java基础类库_第24张图片
上面程序代码示范了Calendar类的用法,Calendar可以灵活地改变它对应的日期。
上面程序使用了静态导入,它导入了Calendar类里的所有类变量(其实添加improt static即可),它导入了Calendar类里的所有类变量,所以上面程序可以直接使用Calendar类的YEAR,MONTH,DATE等类变量。

import java.util.Calendar;
import static java.util.Calendar.*;

第七章 Java基础类库_第25张图片
Calendar类还有如下几个注意点。

  1. add与roll的区别
    add(int field,int amount)的功能非常强大,add主要用于改变Calendar的特定字段的值。如果需要增加某字段的值,则让amount为正数;如果需要减少某字段的值,则让amount为负数即可。
    add(int field,int amount)有如下两条规则。
  • 当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大。例如:
var call = Calendar.getInstance();
call.set(2003,7,23,0,0,0);//2003-8.23
call.add(MONTH,6);//2003-8-23=>2004-2-23
  • 如果下一集字段也需要改变,那么该字段会修改正到最小的值。例如:
var 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

对于上面的例子,8-31就会变成2-39。因为MONTH的下一级字段是DATE,从31到29改变最小。所以上面2003-8-31的MONTH字段增加6后,不是变成2004-3-2,而是变成2004-2-29。
roll()的规则与add()的处理规则不同:当被修改的字段超过它允许的范围时,上一级字段不会增大。

var cal3 = Calendar,getInstance();
cal3,set(2003,7,23,0,0,0);//2003-8-23
//MONTH字段进位,但YEAR字段并不增加
cal3.rool(MONTH,6);//2003-8-23=>2003-2-23

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

var cal4 = Calendar,getInstance();
cal4.set(2003,7,31,0,0,0);//2003-8-31
//MONTH字段进位后变成2,2月没有31日
cal4.roll(MONTH,6);//2003-8-31 => 2003-2-28
  1. 设置Calendar的容错性
    调用Calendar对象的set()方法来改变指定时间字段的值时,有可能传入一个不合法的参数,例如为MONTH字段设置13,这将会导致怎样的后果了?看如下程序。
import java.util.Calendar;
import static java.util.Calendar.*;
public class CalendarTest{
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();
        cal.set(MONTH,13);//①
        System.out.println(cal.getTime());
        cal.setLenient(false);
        cal.set(MONTH,13);//②
        System.out.println(cal.getTime());
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=58224:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject CalendarTest
Mon Feb 06 20:27:39 CST 2023
Exception in thread "main" java.lang.IllegalArgumentException: MONTH
	at java.base/java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2645)
	at java.base/java.util.Calendar.updateTime(Calendar.java:3428)
	at java.base/java.util.Calendar.getTimeInMillis(Calendar.java:1812)
	at java.base/java.util.Calendar.getTime(Calendar.java:1785)
	at CalendarTest.main(CalendarTest.java:10)

Process finished with exit code 1

上面程序中①和②两处代码完全相似,但它们的运行结果不一样:①号代码可以正常运行,因为设置MONTH字段的值为13,将会导致YEAR字段加1;②号代码将会导致运行时异常,因为设置的MONTH字段值超出了MONTH字段允许的范围。关键字在于程序中粗体代码行,Calendar提供了一个setLenient()用于设置它的容错性,Calendar默认支持较好的容错性,通过setLenient(false)可以关闭Calendar的容错性,让它进行严格的参数检查。
Calendar有两种解释日历字段的模式,lenient模式和non-lenient模式。当Calendar处于lenient模式时,每个时间字段可接受超出它允许范围的值;当Calendar处于non-lenient模式时,如果为某个时间字段设置的值超出了它允许的取值范围,程序将会抛出异常。

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

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

import java.util.Calendar;
import static java.util.Calendar.*;
public class CalendarTest{
    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());//③

    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=58247:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject CalendarTest
Fri Sep 05 20:41:51 CST 2003

Process finished with exit code 0

上面程序中创建了代表2003-8-31的Calendar镀锡,当把这个对象的MONTH字段增加1后应该得到2003-10-1(因为9月没有31日),如果程序在①号代码处输出当前Calendar里的日期,也会看到输出2003-10-1,③号代码处将输出2003-10-5。
如果程序将①处代码注释起来,因为Calendar的set()方法具有延迟修改的特性,即调用set()方法后Calendar实际上并未计算真实的日期,它只是使用内部成员变量记路MONTH字段被修改为8,接着程序设置DATE字段值为5,程序内部再次记路DATE字段为5——就是9月5,因此看到③号处输出2003-9-5。

新的日期、时间包

Java8专门新增了一个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对应的时间。除此之外,它还提供了muinusXxx()方法在当前年份基础上减去几小时、几分、几秒等,也提供了plusXxx()方法在当前年份基础上增加了几小时、几分、几秒等。
  • LocalDateTime:该类代表不带时区的日期、时间,例如2007-12-03T10:15:30。该类提供了静态的now()方法来获取当前日期、时间,也提供了静态的now(Clock clock)方法来获取clock对应的日期、时间。此处之外,它还提供了muinusXxx()方法在当前年份基础上减去几年、几月、几日、几小时、几分、几秒。也提供了plusXxx()方法在当前年份基础上加上几年、几月、几日、几小时、几分、几秒等。
  • MonthDay:该类仅代表月日,例如–04–12。该类提供了静态的now()方法来获取当前月日、也提供了静态的now(Clock clock)方法来获取clock对应的月日。
  • Year:该类仅代表年,例如2014。该类提供了静态的now()方法来获取当前年份,也提供了静态的now(Clock clock)方法来获取clock对应的年份。除此之外,它还提供了minusYears()方法在当前年月基础上减去几年、几月,也提供了plusXxx()方法在当前年月基础上加上几年、几月。
  • ZonedDateTime:该类代表一个时区化的日期、时间。
  • ZoneId:该类代表一个时区
  • DayOfWeek:这个枚举类,定义了周日到周六的枚举值。
  • Month:这也是一个枚举类,定义一月到十二月的枚举值。
    下面通过一个简单的程序来示范这些类的用法。
import java.time.*;

public class NewDatePackageTest{
    public static void main(String[] args) {
        //-----下面是关于Clock的用法-----
        //获取当前Clock
        var clock = Clock.systemUTC();
        //通过Clock获取当前时刻
        System.out.println("当前时刻为:"+clock.instant());
        //获取clock对应的毫秒差,与System.currentTimeMillis()输出相同
        System.out.println(clock.millis());
        System.out.println(System.currentTimeMillis());
        //-----下面是关于Duration的用法-----
        var 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
        var clock2 = Clock.offset(clock,d);
        //可以看到clock2与clock1相差1小时40分
        System.out.println("当前时刻加6000秒为:"+clock2.instant());
        //-----下面是关于Instant的用法-----
        //获取当前时间
        var instant = Instant.now();
        System.out.println(instant);
        //instant增加6000秒(即100分钟),返回新的Instant
        var instant2 = instant.plusSeconds(6000);
        System.out.println(instant2);
        //根据字符串解析Instant对象
        var instant3 = Instant.parse("2014-02-23T10:12:35.342Z");
        System.out.println(instant3);
        //在instan3的基础上添加5小时4分钟
        var instant4 = instant3.plus(Duration.ofHours(5).plusMinutes(4));
        System.out.println(instant4);
        //获取instant4的5天以前的时刻
        var instant5 = instant4.minus(Duration.ofDays(5));
        System.out.println(instant5);
        //----下面是关于LocalDate的用法-----
        var localDate = LocalDate.now();
        System.out.println(localDate);
        //获取2014年的第146天
        localDate = LocalDate.ofYearDay(2014,146);
        System.out.println(localDate);//2014-05-21
        //-----下面是关于LocalTime的用法-----
        //获取当前时间
        var 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);
        //-----下面关于localDateTime的用法-----
        //获取当前日期、时间
        var localDateTime = LocalDateTime.now();
        //当前日期、时间加上25小时3分钟
        var futuer = localDateTime.plusHours(25).plusMinutes(3);
        System.out.println("当前日期、时间的25小时3分钟之后:"+futuer);
        //-----下面是关于Year、YearMonth、MonthDay的用法示例-----
        var year = Year.now();//获取当前的年份
        System.out.println("当前年份:"+year);//输出当前年份
        year = year.plusYears(5);//当前年份再加五年
        System.out.println("当前年份再过5年:"+year);
        //根据指定月份获取YearMonth
        var ym = year.atMonth(10);
        System.out.println("year年10月:"+ym);
        //当前年月再加五年、减3个月
        ym = ym.plusYears(5).minusMonths(3);
        System.out.println("year年10月再加5年、减3个月:"+ym);
        var md = MonthDay.now();
        System.out.println("当前月日:"+md);//输出--xx--xx,代表几月几日
        //设置为5月23日
        var md2 = md.with(Month.MAY).withDayOfMonth(23);
        System.out.println("5月23日为:"+md2);//输出--05--23
        
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=58389:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject NewDatePackageTest
当前时刻为:2022-03-06T13:32:29.847895800Z
1646573549874
1646573549874
6000秒相当于:1006000秒相当于:1小时
6000秒相当于:0天
当前时刻加6000秒为:2022-03-06T15:12:29.889785400Z
2022-03-06T13:32:29.889785400Z
2022-03-06T15:12:29.889785400Z
2014-02-23T10:12:35.342Z
2014-02-23T15:16:35.342Z
2014-02-18T15:16:35.342Z
2022-03-06
2014-05-26
22:33
01:31:43
当前日期、时间的25小时3分钟之后:2022-03-07T22:35:29.921033600
当前年份:2022
当前年份再过5年:2027
year年10月:2027-10
year年10月再加5年、减3个月:2032-07
当前月日:--03-06
523日为:--05-23

Process finished with exit code 0

此程序就是这些常见类的用法示例,这些API和它们的方法都非常简单,而且程序中注释也很清楚,此处不再赘述。

正则表达式

前面已经介绍了,正则表达式就是一个用于匹配字符串的模板,可以匹配一批字符串,所以创建正则表达式就是创建一个特殊的字符串。正则表达式所支持的合法字符如表所示。
第七章 Java基础类库_第26张图片
除此之外,正则表达式中有一些特殊字符,这些特殊字符在正则表达式中有其特殊的用途,比如前面介绍的反斜线(\)。如果需要匹配这些特殊字符,就必须首先将这些字符转移,也就是在前面添加一个反斜线(\)。正则表达式中的特殊字符如表所示。
第七章 Java基础类库_第27张图片
将上面多个字符拼起来,就可以创建一个正则表达式。例如:

public class Demo {
    String s1 = "\u0041\\\\";//匹配A\
    String s2 = "\u0061\t";//匹配a<制表符>
    String s3 = "\\?\\[";//匹配?[
}

可能有读者会觉得第一个正则表达式中怎么有那么多反斜杠啊?这是由于Java字符串中反斜杠本身需要转义,因此两个反斜杠(\)实际上相当于一个(前一个用于转义)。
上面的正则表达式依然只能匹配单个字符,这是因为还未在正则表达式中使用“通配符”,“通配符”是可以匹配多个字符的特殊字符。正则表达式中的“通配符”远远超出了普通通配符的功能,它被称为预定义字符,正则表达式支持如表所示的预订义字符。
第七章 Java基础类库_第28张图片
上面的7个预订字符其实很容易记忆——d是digit的意思,代表数字;s是space的意思,代表空白;w是word的意思,代表单词、d、s、w的大写形式恰好匹配子相反的字符。
有了上面的预订字符后,接下来就可以创建更强大的正则表达式了。例如:

public class Demo {
    String s1 = "c\\wt";//可以匹配cat、cbt、cct、c0t、c9t等一批字符
    String s2 = "\\d\\d\\d-\\d\\d-\\d\\d\\d";//匹配如000-000-0000形式的电话号码
}

在一些特殊情况下,例如,若指向匹配a~f的字母,或者匹配除ab之外的所有小写字母,或者匹配中文字符,上面这些预定义字符就无能为力了,此时就需要使用方法括号表达式,方法括号表达式如表所示的几种形式。
第七章 Java基础类库_第29张图片

方括号表达式比前面的预定义字符灵活多了,几乎可以匹配任何字符。例如,若需要匹配所有的中文字符,就可以利用[\u004-\u0056]形式——因为所有中文字符的Unicode值是连续的,只要找出所有中文字符中最小、最大的Unicode值,就可以利用上面的形式来匹配所有的中文字符。
正则表达式还支持圆括号表达式,用于将多个表达式组成一个子表达式,圆括号中可以使用或运算符(|)。例如,正则表达式"((public)|(protected)|(private))"用与匹配Java的三个访问控制符其中之一。
除此之外。Java正则表达式还支持如表所示的几个边界匹配符。
第七章 Java基础类库_第30张图片

前面例子中需要建立一个000-000-0000形式的电话号码时,使用了\d\d\d-\d\d\d-\d\d\d正则表达式,这看起来比较烦琐。实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式。

  • Greedy(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,知道无法匹配为止。如果你发现表达式匹配的结果与预期不服,很有可能是因为——你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去。
  • Reluctant(勉强模式):用问号后缀(?)表示它只会匹配最少的字符。也称为最小匹配模式。
  • Possessive(占有模式):用加号后缀(+)表示,目前只有Java支持占有模式,通常比较少用。三种模式的数量表示符如表所示。
    第七章 Java基础类库_第31张图片
    关于贪婪模式和勉强模式的对比,看如下代码:
    在这里插入图片描述
    在这里插入图片描述
replaceFirst​(String regex, String replacement);
/*将给定替换的给定 regular expression
匹配的此字符串的第一个子字符串替换。 
*/

来看如下正题代码:

public class Demo{
    public static void main(String[] args) {
        String str = "hello java!";
        //贪婪模式的正则表达式
        System.out.println(str.replaceFirst("\\w*","■"));
        //勉强模式的正则表达式
        System.out.println(str.replaceFirst("\\w*?","■"));
    }
}

当从"hello java!“字符传中查找匹配”\w*“子串时,因为”\w*"使用了贪婪模式,数量表示符(*)会一直匹配下去,所以该字符串前面的所有单词字符都被它匹配到,直到遇到空格:
在这里插入图片描述
在这里插入图片描述
所以替换后的效果是 ■ java!

使用正则表达式

一旦在程序中定义了正则表达式,就可以使用Pattern和Matcher来使用正则表达式。
Pattern对象是正则表达式编译后再内存中表示形式,因此,正则表达式字符串必须先被编译为Pattern对象,然后再利用该Pattern对象创建对应的Mathcer对象。执行匹配所涉及的状态保留在Matcher对象中,多个Matcher对象可共享同一个Pattern对象。
因此,典型的调用顺序如下:

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Demo{
    public static void main(String[] args) {
        //将一个字符串编译成 Pattern对象
        Pattern p =Pattern.compile("a*b");
        //使用Pattern对象创建Matcher对象
        Matcher m = p.matcher("aaaaab");
        boolean b = m.matches();//返回true
    }
}

上面定义的Pattern对象可以多次重复使用。如果某个正则表达式仅需一次使用,则可以直接使用Pattern类的静态matcher()方法,此方法自动把字符串编译成匿名的Pattern对象,并执行匹配,如下所示。

boolean b = Pattern.matches("a*b","aaaaab");//返回true

上面语句等效于前面的三条语句。但采用这种语句每次都需要重新编译新的Pattern对象,不能重复利用已编译的Pattern对象,所以效率不高。
Pattern是不可变类,可供多个并发线程安全使用。
Matcher类提供了如下几个常用方法。

  • finad():返回目标字符串中是否包含与Pattern匹配的字符。
  • group():返回上一次与Pattern匹配的子串。
  • start():返回上一次Pattern匹配的字符串在目标字符中开始位置。
  • end():返回上一次与Pattern匹配的子串在目标字符串中的结束位置加1。
  • lookingAt():返回目标字符串前面部分与Pattern是否匹配。
  • matches():返回整个目标字符串与Pattern是否匹配。
  • reset():将现有的Matcher对象应用于一个新的字符序列。
    在Pattern、Matcher类的介绍中经常看到一个CharSequence接口,该接口代表一个字符序列,其中CharBuffer、String、StringBuffer、StringBuilder都是它的实现类。简单地来说,CharSequence代表一个各种表示形式的字符串。
    通过Matcher类的find()和group()方法可以从目标字符串中依次取出特定子串(匹配正则表达式的子串),例如互联网的网络爬虫,它们可以自动从网页中识别所有的电话号码。下面程序示范了如何从大段的字符串中找出电话号码。
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class FindGroup{
    public static void main(String[] args) {
        //使用字符串模拟从网络上得到
        var str = "我想买本Java书,尽快联系我17332076539"
                +"交朋友,电话号码是14826351536"+
                "出售手办,联系方式是17332076539";
        //创建一个Pattern对象,并用它建立一个Matcher对象
        //该正则表达式只抓取13X和15X段的手机号
        //实际要抓取哪些电话,只要修改正则表达式即可
        Matcher m = Pattern.compile("((17\\d)|(14\\d))\\d{8}").matcher(str);
        /*代码解析
            Pattern.compile();
            "(17\\d)" 匹配17开头的所有数字(0~9)
            | 逻辑运算符 和
            (14\\d) 匹配14开头的 0~9数字
            \\d{8} 8次频率
            1 7 3 3 2 0 7 6 5 3 9
            1 7 0 1 2 3 4 5 6 7 8
            17X
         */
        while (m.find()){//判断是否包含字符序列
            System.out.println(m.group());//输出所包含的字符序列
        }
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51089:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject FindGroup
17332076539
14826351536
17332076539

Process finished with exit code 0

上面代码解析不可能不正确,但是可以暂且这么理解,毕竟只是简单的学习。
从上面运行结果可以看到,find()方法依次查找字符串中与Pattern匹配的子串,一旦找到对应的子串,下次调用find()方法时接着向下查找。
通过程序可以看出,使用正则表达式可以提取网页上的电话号码,也可以提取邮件地址等信息。如果程序再进一步,可以从网上提取超链接信息,再根据超链接打开其他网页,然后在其他网页上重复这个过程就可以实现简单的网络爬虫了。
find()方法还可以传入一个int 类型参数,带int参数的find()方法将从该int索引处向下搜索。
start()和end()方法主要用于确定子串在目标子类传中的位置,如下程序所示。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class StartEnd{
    public static void main(String[] args) {
        //创建一个Pattern对象,并用它创建一个Matcher对象
        var 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());

        }
        System.out.println("----------End----------");
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51160:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject StartEnd
目标字符串是:Java is very easy!
Java子串的起始位置:0,其结束位置:4
is子串的起始位置:5,其结束位置:7
very子串的起始位置:8,其结束位置:12
easy子串的起始位置:13,其结束位置:17
----------End----------

Process finished with exit code 0

上面程序使用find()、group()方法逐项获取出字符串中与指定正则表达式匹配的子串,并使用start()、end()方法返回子串在目标字符串中的位置。运行上面的程序,可以看到如上的结果。
matches()和lookingAt()方法有点相似,只是matches()方法要求整个字符串和Pattern完全匹配时才会返回true,而lookingAt()只要字符串以Pattern开头就会返回true。reset()方法可将现有的Matcher对象应用于新的字符序列。看如下例子程序。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MatchesTest{
    public static void main(String[] args) {
        String[]mails ={
                "[email protected]",
                "[email protected]",
                "[email protected]",
                "[email protected]"
        };
        var mailRegEx = "\\w{3,20}@\\w|\\.(com|org|cn|net|gov)";
        var mailPattern = Pattern.compile(mailRegEx);
        Matcher matcher = null;
        for(var mail:mails){
            if(matcher==null){
                matcher=mailPattern.matcher(mail);
            }else{
                matcher.reset(mail);
            }
            String result = mail+(matcher.matches() ? "是":"不是")+"有效的邮件地址!";
            System.out.println(result);
        }
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51238:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject MatchesTest
kongyeeku@163.com不是有效的邮件地址!
kongyeeku@gmail.com不是有效的邮件地址!
ligang@crazyit.org不是有效的邮件地址!
wawa@abc.xx不是有效的邮件地址!

Process finished with exit code 0

上面程序创建了一个邮件地址的Pattern,接着用这个Pattern与多个邮件地址进行匹配。当程序中的Matcher伪null时,程序调用matcher()方法来创建一个Matcher对象,一旦Matcher对象被创建,程序就调用Matcher的reset()方法将该Matcher应用于新的字符序列。
从某个角度来看,Matcher的matches()、lookingAt()和String类的equals()、startWith()有点相似。区别是String类的equals()和startsWith()都是字符串进行比较,而Matcher的matches()和lookingAt()则是与正则表达式进行匹配。
事实上,String类里也提供了matches()方法,该方法返回该字符串是否匹配指定的正则表达式。例如:

"[email protected]".matches("\\w{3,20}@\\+\\w+\\.(com|org|cn|net|gov)");//返回true

除此之外,还可以利用正则表达式对目标字符串进行分割、查找、替换等操作,看如下例子程序。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

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"
        };
        var p = Pattern.compile("re\\w*");
        Matcher matcher = null;
        for(var i=0;i<msgs.length;i++){
            if(matcher == null){
                matcher=p.matcher(msgs[i]);
            }else{
                matcher.reset(msgs[i]);
            }
            }
        System.out.println(matcher.replaceAll("哈哈)"));
        }
    }
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=64108:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject ReplaceTest
Java 哈哈) oracular exp哈哈)

Process finished with exit code 0

上面程序使用了Matcher类提供的replaceAll()把字符串中所有的正则表达式匹配的子串替换成“哈哈:)”,实际上Matcher类还提供了一个replaceAll(),该方法只替换第一个匹配的子串。运行上面程序,会看到字符串中所有re开头的单词都会被替换成“哈哈:)”。
实际上,String类中也提供了replaceAll()、replaceFirst()、split()等方法。下面的例子程序直接使用String类提供的正则表达式功能来进行替换和分割。

import java.lang.reflect.Array;
import java.util.Arrays;

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(var msg:msgs){
            System.out.println(msg.replaceFirst("re\\w*","哈哈:)"));
            System.out.println(Arrays.toString(msg.split(" ")));
        }
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=64153:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject StringReg
Java has 哈哈:) expressions in 1.4
[Java, has, regular, expressions, in, 1.4]
哈哈:) expressions now expressing in Java
[regular, expressions, now, expressing, in, Java]
Java 哈哈:) oracular expressions
[Java, represses, oracular, expressions]

Process finished with exit code 0

上面程序只使用String类的replaceFirst()和split()方法对目标字符串进行了一次替换和分割。运行上面程序会看到如上所示的结果。
正则表达式是一个功能非常灵活的文本处理工具,增加了正则表达式的Java,可以不再使用StringTokenizer类(也是一个处理字符串的工具,但是远不如正则表达式强大)即可进行复杂的字符串处理。

变量处理和方法处理

Java9引入了一个新的VarHandle,并增强了原有的MethodHandle类。通过这两个类,允许Java像动态语言像动态语言一样引用变量、信用方法,并调用它们。

Java9增强的MethodHandle

MethodHandle为Java增加了方法引用的功能,方法引用的概念有点类似于C的“函数指针”。这种方法引用是一种轻量级的引用方式,它不会检查方法的访问权限,也不管方法所属的类、实际方法或静态方法,MethodHandle就是简单代表特定的方法,并可通过MethodHandle来调用方法。
为了使用MethodHandle,还涉及如下几个类。

  • MethodHandles:MethodHandle的工厂类,它提供了一系列静态方法用于获取MethodHandle。
  • MethodHandles.Lookup静态内部类也是MethodHandle、VarHandle的工厂类,专门用于获取MethodHandle和VarHandle。
  • MethodType:代表一个方法类型。MethodType根据方法的形参,返回值类型来确定方法类型。下面程序示范了MethodHandle的用法。
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleTest{
    //定义一个private类方法
    private static void hello(){
        System.out.println("Hello World!");
    }
    //定义一个prviate实例方法
    private String hello(String name){
        System.out.println("执行带参数的hello"+name);
        return name+",你好";
    }
    public static void main(String[]args) throws Throwable{
        //定义一个返回值为void、不带形参的方法类型
        var type = MethodType.methodType(void.class);
        //使用MethodHandles.Lookup的findStatic获取类方法
        var mtd = MethodHandles.lookup().findStatic(MethodHandleTest.class,"hello",type);
        //通过MethodHandle执行方法
        mtd.invoke();
        //使用MethodHandles.Lookup和findVirtual获取实例方法
        var mtd2 = MethodHandles.lookup().findVirtual(MethodHandleTest.class,"hello",MethodType.methodType(String.class,String.class));
        System.out.println(mtd2.invoke(new MethodHandleTest(),"孙悟空"));
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=58692:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject MethodHandleTest
Hello World!
执行带参数的hello孙悟空
孙悟空,你好

Process finished with exit code 0

从上面三行粗体代码可以看出,程序使用MethodHandles.Lookup对象根据类、方法名、方法类型来获取MethodHandle对象。由于此处的方法名只是一个字符串,而该字符串可以来自于变量、配置文件等,这意味着通过MethodHandle可以让Java动态调用某个方法。
第七章 Java基础类库_第32张图片
第七章 Java基础类库_第33张图片
看第二个方法的执行:
第七章 Java基础类库_第34张图片
第七章 Java基础类库_第35张图片

Java9增加的VarHandle

VarHandle主要用于动态操作数组的元素和对象的成员变量。VarHandle与MethodHandle非常相似,它也需要通过MethodHandles来获取实例,接下来调用VarHandle的方法即可动态操作指定数组的元素或指定对象的成员变量。
下面程序示范了VarHandle的用法。

import java.lang.invoke.MethodHandles;
import java.util.Arrays;

class User{
    String name;
    static int MAX_AGE;
}
public class VarHandleTest{
    public static void main(String[] args) throws Throwable{
        var sa = new String[]{"Java","Kotlin","Go"};
        //获取一个String[]数组的VarHandle对象
        var avh = MethodHandles.arrayElementVarHandle(String[].class);
        //比较并设置:如果第三个元素是Go,则该元素被设置为Lua
        var 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的实例变量。
        var vh1 = MethodHandles.lookup().findVarHandle(User.class,"name",String.class);
        var 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的类变量
        var 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
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=58916:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject VarHandleTest
true
[Java, Kotlin, Lua]
Kotlin
Lua
[Java, Kotlin, Swift]
null
孙悟空
0
100

Process finished with exit code 0

从上面可以看出,程序调用MethodHandles类的静态方法可以获取操作数组的VarHandle对象,接下来程序可通过VarHandle对象来操作数组的方法,包括比较并设置数组元素、获取并设置数组元素等,VarHandle具体支持哪些方法则可参考API文档。
上面程序中后面三行粗体代码示范了使用VarHandle操作实例变量的情形,由于实例变量需要使用对象来访问,因此使用VarHandle操作实例变量时需要传入一个User对象。VarHandle即可设置实例变量的值,也获取实例变量的值。当然VarHandle也提供了更多的方法来操作实例变量,具体可参考API文档。
使用VarHandle操作类与操作实例变量差别不大,区别只是类变量不需要对象,因此使用VarHandle操作类变量时无须传入对象作为参数。
VarHandle与MethodHandle一样,它也是一种动态调用机制,当程序通过MethodHandles.Lookup来获取成员变量时,可根据字符串名称来获取成员变量,这个字符名称同样可以是动态改变的,因此非常灵活。

Java11改进的国际化与格式化

全球化的Internet需要全球化的软件。全球化软件,意味着同一个版本的产品能够容易地适应用于不同地区的市场,软件的全球化意味着国际化和本地化。当一个应用需要在全球范围使用时,就必须考虑在不同的地域和语言环境下的使用情况,最简单的要求就是用户界面上的信息可以本地化语言来显示。
国际化是指应用程序运行时,可根据用户端请求来自的国家/地区、语言的不同而显示不同的界面。
例如,如果请求来自于中文操作系统的客户端,则应用程序中的各种提示信息错误和帮助等都使用中文文章;如果客户使用英文操作系统,则应用程序能自动识别,并做出英文的相应。
引入国际化的目的就是为了提供自适应、更友好的用户界面,并不需要改变程序的逻辑功能。国际化的英文单词是Internationalization,因为这个单词太长了,有事也简称I18N,其中I这个单词的第一个字母,18表示中间省略的字母个数,而N代表这个单词的最后一个字母。
一个国际化支持很好的应用,在不同的区域使用时,会呈现出本地语言的提示。这个过程也被称为Localization,即本地化。类似于国际化可以称为I18N,本地化也可以称为L10N
Java11国际化支持升级到了Unicode10.0字符集,因此提供了对不同国家、不同语言的支持,它已具有了国际化和本地化的特征及API,因此Java程序的国际化相对简单。尽管Java开发工具为国际化和本地化的工作提供了一些基本的类,但还有一些对于Java应用程序的本地化和国际化来说较困难的工作,例如:消息获取、编码转换,显示布局和数字、日期、货币的格式等。
当然,一个优秀的全球化软件产品,对国际化和本地化的要求远远不止于此,甚至还包括用户提交数据的国际化和本地化。

Java国际化思路

第七章 Java基础类库_第36张图片
Java程序的国际化思路是将程序中的标签,提示等信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value对,每个资源文件中的key是不变的,但value则随不同的国家、语言而改变。上图显示了Java程序国际化的思路。
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所支持的语言和国家。

Java支持的国家和语言

事实上,Java不可能支持所有的国家和语言,如果需要获取Java所支持的国家和语言,则可调用Locale类的getAvailableLocales()方法,该方法返回一个Locale数组,该数组里包含了Java所支持的国家和语言。
下面的程序简单地示范了如何获取Java所支持的国家和语言。

由于运行结果实在是太多国家,所以只截取了一部分的运行结果

埃塞俄比亚=ET 提格利尼亚语=ti
中国=CN 藏语=bo
= 上索布语=hsb
厄瓜多尔=EC 克丘亚语=qu
新加坡=SG 泰米尔语=ta
= 拉脱维亚语=lv
纽埃=NU 英语=en
新加坡=SG 中文=zh
蒙特塞拉特=MS 英语=en
根西岛=GG 英语=en
牙买加=JM 英语=en
import java.util.Locale;

public class LocaleList{
    public static void main(String[] args) {
        Locale[]localeList = Locale.getAvailableLocales();
        for(var i = 0; i<localeList.length;i++){
            System.out.println(localeList[i].getDisplayCountry()+"="+localeList[i].getCountry()+" "+localeList[i].getDisplayLanguage()+"="+localeList[i].getLanguage());
        }
    }
}

虽然可以通过查阅相关资料来获取Java语言所支持的国家/语言环境,但如果这些资料不能随手可得,则可以通过上面程序来获得Java语言所支持的国家/语言环境。

完成程序国际化

对于如下最简单的程序:

public class RawHello{
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

第七章 Java基础类库_第37张图片
这个程序的执行结果也很简单——肯定是打印出简单的“Hello World”字符串,不管在哪里执行都不会有任何改变!为了让程序支持国际化,肯定不能让程序直接输出“Hello World”字符串,这种写法直接输出一个字符串常量,永远不会有任何改变。为了让程序可以输出不同的字符串,此处绝不可使用该字符串常量。
为了让上面输出的字符串常量可以改变,可以将需要输出的各种字符串(不同的国家/语言环境对应不同的字符串)定义在资源包中。
为上面程序提供如下两个文件。
第一个文件:mess_zh_Cn.properties,该文件的内容为:

#资源文件的内容是key-value对
hello = "你好"

第二个文件mess_en_US.properties,该文件的内容为:

#资源文件的内容是key-value对
hello=Welcome You!

从Java9开始,Java支持使用UTF-8字符集来保存属性文件,这样在属性文件中就可以直接包含非西欧字符,因此属性文件也不再需要使用native2ascii工具进行处理。唯一要注意的是,属性文件必须显式保存为UTF-8字符集。
Windows是一个非常奇葩的操作系统,它保存文件默认采用GBK字符集。因此,在Windows平台上执行javac命令默认也用GBK字符集读取Java源文件。但实际开发项目时采用GBK字符集会引起很多乱码问题,所以通常推荐源码都使用UTF-8字符集保存。但如果使用UTF-8字符集保存Java源代码,在命令行编译源程序时需要为javac显式指定-encoding utf-8选项,用于告诉javac 命令使用UTF-8字符集读取Java源文件。本文章出于降低学习难度的考虑,开始没有介绍该选项,所以用平台默认的字符集(GBK)来保存Java源文件。
看到这两份文件名的baseName是相同的mess。前面已经介绍了资源文件的三种命名方式,其中baseName后面的国家、语言必须是Java所支持的国家、语言组合。将上面的Java程序改成如下形式。

import java.util.Locale;
import java.util.ResourceBundle;

public class Hello{
    public static void main(String[] args) {
        //取得系统默认的国家/语言环境
        var myLocale = Locale.getDefault(Locale.Category.FORMAT);
        var bundle = ResourceBundle.getBundle("mess",myLocale);
        System.out.println(bundle.getString("hello"));
    }
}

第七章 Java基础类库_第38张图片
第七章 Java基础类库_第39张图片

上面程序中的打印语句不再是直接打印“Hello World”字符串,而是打印从资源包中读取的信息,如果在中文环境下运行该程序,将打印“你好”!;如果在“控制面板”中将机器的语言环境设置成美国,然后再次运行该程序,将打印“Welcome Your!”字符串。
第七章 Java基础类库_第40张图片
从上面程序可以看出,如果希望程序完成国际化,值需要将不同的国家/语言(Locale)的提示信息分别以不同的文件存放即可。例如,简体中文的语言资源文件就是Xxx_zh_CN.properties文件,而美国英文的语言资源文件就是Xxx_en_US.properties文件。
Java国际化的关键类就是ResourceBundle,它由一个静态方法:getBundle(String baseName,Locale locale),该方法将根据Locale加载文件,而Locale封装了一个国家、语言,例如,简体中文环境可以用简体中文的Locale代表,美国英语环境可以用美国英语的Locale代表。
例如,通过如下代码来加载资源文件。

//根据指定国家/语言环境加载资源文件
var bundle = ResourceBundle.getBundle("mess",myLocale);

上面代码将会加载baseName为mess系列资源文件之一,到底加载哪个资源文件,则取决于myLocale;对于简体中文的Locale,则加载mess_zh_CN.properties文件。
一旦加载了该文件后,该资源文件的内容就是多个key-value对,程序就是根据key来获取指定的信息,例如获取key为hello的信息,该消息是“你好!”——这就是Java程序国际化的过程。
如果对于美国英语的Locale,则加载mess_en_US.properties文件,该文件中key为hello的消息是“Welcome You!”。
Java程序国际化的关键类是ResourceBundle和Locale,ResourceBundle根据不同的Locale加载语言资源文件,再根据指定的key取得已加载语言资源文件中的字符串。

使用MessageFormat处理包含占位符的字符串

上面程序中输出的消息是一个简单信息,如果需要输出的消息中必须包含动态的内容,例如,这些内容必须是从程序中取得。比如如下字符串:

你好,yeeku!今天是2014-5-30 下午11:55

在上面的输出字符串中,yeeku是浏览者的名字,必须动态改变,后面的时间也必须动态改变。在这种情况下,可以使用带占位符的消息。例如,提供一个myMess_en_US.properties文件,该文件的内容如下:

msg = Hello,{0}!Today is {1}.

提供一个myMess_zh_CN.properties文件,该文件的内容如下:

msg = 你好,{0}!今天是{1}

上面两个资源文件必须使用UTF-8字符集保存。
当程序直接使用ResourceBundle的getString()方法来取出msg对应的字符串时,在简体中文环境下得到“你好,{0}!今天是{1}”。字符串,这显然是不需要的结果,程序还需要为{0}和{1}两个占位符赋值。此时需要使用MessageFormat类,该类包含了一个有用的静态方法。

  • format(String pattern,Object…values);返回后面的多个参数值填充前面的pattern字符串,其中pattern字符串不是正则表达式,而是一个带占位符的字符串。
    借助于上面的MessageFormat类的帮助,将国际化程序修改如下形式。
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.ResourceBundle;

public class HelloArg{
    public static void main(String[] args) {
        Locale currentLocale = null;
        if(args.length==2){
            currentLocale.new Locale(args[0],args[1]);
        }else{
            currentLocale.Locale.getDefault(Locale.Category.FORMAT);
        }
        var bundle = ResourceBundle.getBundle("myMess",currentLocale);
        var msg = bundle.getString("msg");
        System.out.println(MessageFormat.format(msg,"yeeku",new Date()));
    }
}

从上面程序可以看出,对于带占位符的消息字符串,只需要使用MessageFormat类的format()方法为消息的占位符指定参数即可。

使用类文件替代资源文件

除使用属性文件作为资源文件外,Java也允许使用类文件代替资源文件,即将所有的key-value对存入class文件,而不是属性文件。
使用类文件来替代资源文件必须满足如下条件。

  • 该类的类名必须是baseName_Language_country,这与属性文件的命名相似。
  • 该类必须继承ListResoureeBundle,并重写getContents()方法,该方法返回Object数组,该数组的每一项都是key-value对。
    下面的类文件都可以替代上面的属性文件。
import java.util.ListResourceBundle;

public class myMess_zh_CN extends ListResourceBundle{
    private final Object myData[][]={
            {"msg","{0},你好今天是星期日"}      
    };
    @Override
    protected Object[][] getContents() {
        return myData;
    }
}

上面文件是一个简体中文语言环境的资源文件,该文件可以替代myMess_zh_CN.properties文件,如果需要替代美国英语语言环境的资源文件,则还应该提供一个myMess_en_US类。
如果系统同时存在资源文件、类文件,系统将以类文件为主,而不会调用资源文件。对于简体中文的Locale,ResourceBundlesou搜索资源文件的顺序是:
(1)basaeName_zh_CN.class
(2)baseName_zh_CN.properties
(3)baseName_zh.class
(4)baseName_zh.properties
(5)baseName.class
(6)baseName.properties
系统按照上面的顺序搜索资源文件,如果前面的文件不存在,才会使用下一个文件。如果一直找不到对应的文件,系统将抛出异常。

Java9新增的日志API

Java9强化了原有的日志API,这套日志API只是定义记路消息的最小API,开发者可将这些日志消息路由各种主流的日志(如SLF4J、Log4J等),否则默认使用Java传统的Java.util.logging日志API。
这套日志用法非常简单,只要两步即可。

  1. 调用System类的getLogger(String name)方法获取System.Logger对象。
  2. 调用System.Logger对象的log()方法输出日志。该方法的第一个参数用于指定日志级别。
    为了与传统java.util.logging日志级别、主流日志框架的级别兼容,Java9定义了如下表所示的日志级别。

第七章 Java基础类库_第41张图片
该日志级别是一个非常有用的东西:在开发阶段调试程序时,可能需要大量输出调试信息;在发布软件时,又希望关掉这些调试信息。此时就可通过日志来实现,只要将系统日志级别调高,所有低于该级别的日志信息就都会被自动关闭,如果将日志级别设置为OFF,那么所有日志信息都会被关闭。
例如,下面程序示范了Java9新增的日志API。

import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Demo{
    public static void main(String[] args) throws Exception{
        var logger = System.getLogger("fkjava");
        Logger.getLogger("fkjava").setLevel(Level.FINE);
        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基础类库_第42张图片
上面程序中第一行代码获取Java9提供的日志API,由于此处并未使用第三方日志框架,因此系统默认使用java.util.logging日志作为实现,因此第二行代码使用java.util.logging.Logger来设置日志级别。程序将系统日志级别设置为FINE(同等与DEBUG),这意味着高于或等于DEBUG级别的日志信息都会被输出到a.xml文件。运行上面程序,将可以看到该文件目录下生成了一个a.xml文件,该文件中包含三条日志记路,分别对应于上面三行代码调用log()方法输出的日志记录。
如果将上面第二行粗体代码的日志级别改为SEVERE(等同于ERROR),这意味着高于或等于ERROR级别的日志信息都会被输出到a.xml。再次运行该程序,将会看到该程序生成的a.xml文件仅包含一条日志记路,这意味着DEBUG、INFO级别的日志信息都被自动关闭了。
除简单使用之外,Java9的日志API也支持国际化——System类除使用最简单的getLogger(String name)方法获取System.Logger对象之外,还可使用getLogger(String name,ResourceBundle bundle)方法来获取该对象,该方法需要传入一个国际化语言资源包,这样该Logger对象可根据key来输出国际化的日志信息。
先为美式英语环境提供一个logMess_en_US.properties文件该文件的内容如下:

debug=Debug Message
info=Plain Message
error=Error Message

再为简体中文环境提供一个logMess_zh_CN.properties文件,该文件的内容如下:

debug=调试信息
info=普通信息
error=错误信息

接下来程序可使用ResourceBundle先加载该国际化语言资源包,然后就可通过Java9的日志API来输出国际化的日志信息了。

import java.util.Locale;
import java.util.ResourceBundle;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LoaggerI18N{
    public static void main(String[] args) throws Exception{
        //加载国际化资源包
        var rb = ResourceBundle.getBundle("logMess", Locale.getDefault(Locale.Category.FORMAT));
        //获取系统日志级别
        var logger = System.getLogger("fkjava",rb);
        //设置系统日志级别
        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基础类库_第43张图片
该程序与前一个程序的区别就是第二行代码,这行代码获取System.Logger时加载了ResourceBundle资源包。接下来调用System.Logger的log()方法输出日志信息时,第二个参数应该使用国际化消息key,这样即可输出国际化的日志信息。
在简体中文环境下运行该程序,将会看到a.xml文件中的日志信息是中文信息;在美式英文环境下运行该程序,将会看到a.xml文件中的日志信息是英文信息。

使用NumberFormat格式化数字

MessageFormat是抽象类Format的子类,Format抽象类还有两个子类:NumberFormat和DateFormat、DateFormat用以实现数值、日期的格式化。NumberFormat、DateFormat可以将数字、日期转换成字符串,也可以将字符串转换成数字、日期,下图显示了NumberFormat和DateFormat的主要功能。
第七章 Java基础类库_第44张图片
NumberFormat和DateFormat都包含了format()和parse()方法,其中format()用于将数值、日期格式化字符串,parse()用于字符串解析成数值、日期。
NumberFormat也是一个抽象基类,所以无法通过它的构造器来创建NumberFormat对象,它提供了如下几个类方法来得到NumberFormat对象。
构造器来创建NumberFormat对象,它提供了如下几个类方法来得到NumberFormat对象。

  • getCurrencyInstance():返回默认Locale的货币格式器。也可以调用该方法时传入指定的Locale,则获取指定Locale的货币格式器。
  • getIntegerInstance():返回默认Locale的整数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的整数格式器。
  • getNumberInstance():返回默认Locale的通用数值格式器。也可以在调用该方法时传入指定的Locale,则获取Locale的通用数值格式器。
  • getPercentInstance():返回默认Locale的百分数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的百分数格式器。
    一旦取得了NumberFormat对象后,就可以调用它的format()方法来格式化数值,包括整数和浮点数。如下例子程序示范了NumberFormat的三种数字格式化器的用法。
import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatTest{
    public static void main(String[] args) {
        //需要被格式化的数字
        var ab = 1234000.567;
        Locale[]locales={Locale.CHINA,Locale.JAPAN,Locale.GERMAN,Locale.US};
        var nf = new NumberFormat[12];
        //为上面四个Locale创建12个NumberFormat对象
        //每个Locale分别有通用数值数值格式器、百分数格式器、货币格式器
        for(var 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(var i=0;i<locales.length;i++){
            var tip=i==0?"-----中国的格式-----":
                    i==1?"-----日本的格式-----":
                    i==2?"-----德国的格式-----":"-----美国的格式-----";
            System.out.println(tip);
            System.out.println(("通用数值格式:" + nf[i * 3].format(ab)));
            System.out.println(("百分比数值格式:" + nf[i * 3+1].format(ab)));
            System.out.println(("货币数值格式:" + nf[i * 3+2].format(ab)));
        }
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51336:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject NumberFormatTest
-----中国的格式-----
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:¥1,234,000.57
-----日本的格式-----
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:¥1,234,001
-----德国的格式-----
通用数值格式:1.234.000,567
百分比数值格式:123.400.057 %
货币数值格式:1.234.000,57 ¤
-----美国的格式-----
通用数值格式:1,234,000.567
百分比数值格式:123,400,057%
货币数值格式:$1,234,000.57

Process finished with exit code 0

运行上面程序将看到如上的运行的结果。
从上可以看出,德国的小数点比较特殊,它们采用逗号(,)作为小数点;中国、日本使用¥作为货币符号,而美国则采用$作为货币货号,NumberFormat其实也有国际化的作用!没错,同样的数字在不同国家的写法也是不同的,而NumberFormat的作用就是把数字转换成不同国家的本地写法。
至于使用NumberFormat类将字符串解析成数值的意义不大(因为可以使用Integer、Double等包装类完成这种解析),故此处不再赘述。

使用DateFormat格式化日期、时间

与NumberFormat相似的是,DateFormat也是一个抽象类,它也提供了如下几个类方法用于后去DateFormat对象。

  • getDateInstance():返回一个日期格式器,它格式化后的字符串只有日期,没有时间。该方法可以传入多个参数,用于指定日期样式和Locale等参数;如果不知指定这些参数,则使用默认参数。
  • getTimeInstance():返回一个时间格式器,它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定时间样式、时间样式和Locale等参数;如果不指定这些参数,则使用默认参数。
  • getDateTimeInstance():返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数,用于指定日期样式、时间样式和Locale等参数;如果不指定这些参数,则使用默认参数。
    上面三个方法可以指定日期样式、时间样式参数,它们是DateFormat的4个静态常量:FULL、LONG、MEDIUM和SHORT,通过这4个样式参数可以控制生成的格式化字符串。看如下例子程序。
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
import static java.text.DateFormat.*;

public class DateFormatTest{
    public static void main(String[] args)throws ParseException {
        //需要被格式化的时间
        var dt = new Date();
        //创建两个Locale,分别代表中国、美国
        Locale[]locales={Locale.CHINA,Locale.US};
        var df = new DateFormat[16];
        for(var 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(var i=0;i<locales.length;i++){
            var 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));

        }
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51392:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject DateFormatTest
----中国日期格式----
SHORT格式的日期格式:2022/3/15
MEDIUM格式的日期格式:2022315日
LONG格式的日期格式:2022315日
FULL格式的日期格式:2022315日星期二
SHORT格式的时间格式:下午8:29
MEDIUM格式的时间格式:下午8:29:26
LONG格式的时间格式:CST 下午8:29:26
FULL格式的时间格式:中国标准时间 下午8:29:26
----美国日期格式
SHORT格式的日期格式:3/15/22
MEDIUM格式的日期格式:Mar 15, 2022
LONG格式的日期格式:March 15, 2022
FULL格式的日期格式:Tuesday, March 15, 2022
SHORT格式的时间格式:8:29 PM
MEDIUM格式的时间格式:8:29:26 PM
LONG格式的时间格式:8:29:26 PM CST
FULL格式的时间格式:8:29:26 PM China Standard Time

Process finished with exit code 0

上面程序弓创建了16个DateFormat对象,分别为中国、美国两个Locale各创建8个DateFormat对象,分别SHORT、MEDIUM、LONG、FULL四种样式的日期格式器、时间格式器。运行上面程序,会看到如上运行结果。
从上结果可以看出,正如NumberFormat提供了国际化的能力一样,DateFormat也具有国际化的能力,同一个日期使用不同的Locale格式器转换的效果完全不同,格式化后的字符串正好符合Locale对应的本地习惯。
获得了DateFormat之后,还可以调用它的setLenient(boolean lenient)方法来设置该格式器是否采用严格语法。举例来说,如果采用不严格的日期语法(该方法参数为true),对于字符串“2004-2-31”将会转换成2004年3月2日;如果采用严格的日期语法,解析该字符串时将抛出异常。
DateFormat的parse()方法可以把一个字符串解析成Date对象,但它要求被解析的字符串必须符合日期字符串的要求,否则可能抛出ParseException异常。例如代码片段:

var str1="2017/10/07";
var 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));

上面代码中最后一行代码解析日期字符串时引发ParseException异常,因为“2017/10/07”是一个SHORT样式的日期字符串,必须用SHORT样式的DateFormat实例解析,否则将抛出异常。

使用SimpleDateFormat格式化日期

前面介绍的DateFormat的parse()方法可以把字符串解析成Date对象,但实际上DateFormat的parse()方法不够灵活——它要求被解析的字符串必须满足特定的格式!为了更好地格式化日期、解析日期字符串,Java提供了SimpleDateFormat类。
SimpleDateFormat是DateFormat的子类,正如它的名字所暗示的它是“简单”的日期格式器。很多人对“简单”的日期格式器不屑一顾,实际上SimpleDateFormat比DateFormat更简单,功能更强大。
相对于有些人喜欢深奥的图书、更喜欢“简单”的IT图书,“简单”的东西很清晰、明确,下一步该怎么做,为什么这样做,一切都清清楚楚,无须任何猜测,想象——正好符合计算机哲学——0就是0,1就是1,中间没有任何回旋的余地,如果喜欢深奥的书籍,那就看《老子》吧!够深奥,几乎可以包罗万象,但是有人是通过《老子》开始学习编程的吗…
SimpleDateFormat可以非常灵活地格式化Date,也可以用于解析各种格式的日期字符串。创建SimpleDateFormat对象时需要传入一个pattern字符串,这个pattern不是正则表达式,而是一个日期模板字符串。

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

public class SimpleDateFormatTest{
    public static void main(String[] args)throws ParseException {
        var d = new Date();
        //创建一个SimpleDateFormat对象
        var sdf1 = new SimpleDateFormat("Gyyyy年中第D天");
        //将d格式化成日期,输出:公元2017年中第281天
        var dateStr = sdf1.format(d);
        System.out.println(dateStr);
        //一个非常特殊的日期
        var str = "14###3月##21";
        var sdf2 = new SimpleDateFormat("y###MMM##d");
        System.out.println(sdf2.parse(str));
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51523:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject SimpleDateFormatTest
公元2022年中第74天
Fri Mar 21 00:00:00 CST 2014

Process finished with exit code 0

从上面程序中可以看出,使用SimpleDateFormat可以将日期格式化成形如“公元2014年中第101天”这样的字符串,也可以把形如“14###三月##21”这样的字符串解析成日期,功能非常强大。
SimpleDateFormat把日期格式化怎样的字符串,以及能把怎样的字符串解析成Date,完全取决于创建该对象时制定的pattern参数,pattern是一个使用日期字段站位的日期模板。
如果读者想知道SimpleDateFormat支持哪些日期、时间占位符,可以查阅API文档中SimpleDateFormat类的说明,此处不再赘述。

Java8新增的日期、时间格式器

Java8新增的日期、时间API里不仅包括了Instant、LocalDate、LocalDateTime、LocalTime等代表日期、时间的类,而且在java.time.format包下提供了一个DateTimeFormatter格式器类,该类相当与前面介绍的DateFormat和SimpleDateFormat的合体,功能非常强大。
与DateFormat、SimpleDateFormat类似,DateTimeFormatter不仅可以将日期、时间对象格式化成字符串,也可以将特定的字符串解析成日期、时间对象。
为了使用DateTimeFormatter进行格式化或解析,必须先获取DateTimeFormatter对象,获取DateTimeFormatter对象有如下三种常见的方法。

  • 直接使用静态常量创建DateTimeFormatter格式器。DateTimeFormatter类中包含了大量形如ISO_LOCAL_DATE、ISO_LOCAL_TIME、ISO_DATE_TIME等静态常量,这些静态常量本身就是DateTimeFormatter实例。
  • 使用代表不同分格的枚举值来创建DateTimeFormatter格式器。在FormatStyle枚举类中定义了FULL、LONG、MEDIUM、SHORT四个枚举值,它们代表日期、时间的不同风格。
  • 根据模式字符串来创建DateTimeFormatter格式器。类似于SimpleDateFormat,可以采用模式字符串来创建DateTimeFormatter,如果需要了解DateTimeFormatter支持哪些模式字符串,则需要参考该类的API文档。
    Java8的DateTimeFormatter的官方API文档有错,Java9的DateTimeFormatter官方API已经改正了。

使用DateTimeFormatter完成格式化

使用DateTimeFormatter将日期、时间(LocalDate、LocalDateTime、LocalTime等实例)格式化为字符串,可以通过如下两种方式。

  • 调用DateTimeFormatter的format(TemporalAccessor temporal)方法执行格式化,其中LocalDate、LocalDateTime、LocalTime等类都是TemporalAccessor接口的实现类。
  • 调用LocalDate、LocalDateTime、LocalTime等日期、时间对象的format(DateTimeFormatter formatter)方法执行格式化。
    上面两种方式的功能相同,用法也基本相似,如下程序示范了使用DateTimeFormatter来格式化日期、时间。
import javax.annotation.processing.SupportedSourceVersion;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class NewFormatterTest{
    public static void main(String[] args) {
        var formatters = new 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.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
        };
        var date = LocalDateTime.now();
        for(var i=0;i<formatters.length;i++){
            System.out.println(date.format(formatters[i]));
            System.out.println(formatters[i].format(date));
        }
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51598:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject NewFormatterTest
2022-03-15
2022-03-15
21:25:03.5150182
21:25:03.5150182
2022-03-15T21:25:03.5150182
2022-03-15T21:25:03.5150182
2022315日星期二 下午9:25:03
2022315日星期二 下午9:25:03
20223152022315日
公元2022%%3%%15 21:25:03
公元2022%%3%%15 21:25:03

Process finished with exit code 0

上面程序使用三种方式创建了6个DateTimeFormatter对象,然后程序中两行代码分别使用不同方式来格式化日期。运行上面程序看到如上结果。
可以看出,使用DateTimeFormatter进行格式化时不仅可按系统预置的格式对日期、时间进行格式化,也可以使用模式字符对日期、时间进行自定义格式,由此可见,DateTimeFormatter的功能完全覆盖了传统的DateFormat、SimpleDateFormate的功能。
有些时候,可能还需要使用传统的DateFormat来执行格式化,DateTimeFormatter则提供了一个toFormat()方法,该方法可以获取DateTimeFormatter对应的Format对象。

使用DateTimeFormatter解析字符串

为了使用DateTimeFormatter将指定格式的字符串解析成日期、时间对象(LocalDate、LocalDateTime、LocalTime等实例),可通过日期、时间对象提供的parse(CharSequence text,DateTimeFormatter formatter)方法进行解析。
如下程序示范了使用DateTimeFormatter解析日期、时间字符串。

import java.awt.desktop.SystemSleepEvent;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class NewFormatterParse{
    public static void main(String[] args) {
        //定义一个任意格式的日期、时间字符串
        var str1= "2014==04==12 01时06分09秒";
        //根据需要解析的日期、时间字符串定义解析所用的格式器
        var fomatter1 = DateTimeFormatter.ofPattern("yyyy==MM==dd HH时mm分ss秒");
        //执行解析
        var dt1 = LocalDateTime.parse(str1,fomatter1);
        System.out.println(dt1);
        //---下面代码再次解析另一个字符串---
        var str2="2014$$$4月$$$13 20小时";
        var formatter2 = DateTimeFormatter.ofPattern("yyy$$$MMM$$$dd HH小时");
        var dt2=LocalDateTime.parse(str2,formatter2);
        System.out.println(dt2);//输出201404-13T20:00
    }
}
"C:\Program Files\Java\jdk-11.0.11\bin\java.exe" "-javaagent:D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar=51673:D:\IntelliJ IDEA 2020.1.1\bin" -Dfile.encoding=UTF-8 -classpath D:\DemoProject\out\production\DemoProject NewFormatterParse
2014-04-12T01:06:09
2014-04-13T20:00

Process finished with exit code 0

上面程序中定义了两个不同格式的日期、时间字符串,为了解析它们,程序分别使用对应的格式字符串创建了DateTimeFormatter对象,这样DateTimeFormatter即可按该格式字符串将日期、时间字符串解析成LocalDateTime对象。编译、运行该程序,即可看到两个日期、时间字符串都被成功地解析成LocalDateTime。
第七章结束了,关于第七章学习可能比较复杂和艰难,其实我也是,我也由于时间少,所以第七章的学习并没有做到每个都认真学习,这也是现在的不足,第七章的学习应该是无限的,作为合格的程序员我认为JavaAPI文档的掌握程序70%是最基础的要求,或许可以不怀疑自己现在为什么第七章学的这么痛苦,代码量和学习本身就是日积月累,我们一定要慢慢学慢慢积累它,这些并不主要的东西可以慢慢学习,后面的章节就都是Java很重要的内容了,第七章是以后的先行,加油吧后面的篇章都是作为Java入门最后的内容了,学习完,就可以出发去学习数据库、前端、框架等等内容了,也就在正真正追梦的路上了,我也希望我们始终不忘初心将Java的学习作为自己的人生乐趣而不是为了单纯的赚钱

致敬编程人生的你!

第八章见 2022.3.15
Bye

你可能感兴趣的:(java,开发语言,后端)