栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

JavaSE:第三阶段 Java常用API

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

JavaSE:第三阶段 Java常用API

1.与用户互动

运行Java程序的参数

回忆Java程序的入口——main()方法的方法签名:

public static void main(String[] args)

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

public修饰符:Java类由JVM调用,为了让JVM可以自由调用这个main()方法,所以使用public修饰符把这个方法暴露出来。

static修饰符:JVM调用这个主方法时,不会先创建该主类的对象,然后通过对象来调用该主方法,JVM直接通过该类来调用主方法,因此使用static修饰该主方法。

void返回值:因为主方法被JVM调用,该方法的返回值将返回给JVM,这没有任何意义,因此main()方法没有返回值。

方法中还包括一个字符串数组形参args,根据方法调用的规则:谁调用方法,谁负责为形参赋值,也就是说,main()方法由JVM调用,即args形参应该由JVM负责赋值。

但JVM怎么知道如何为args数组赋值呢?先看下面程序:

//example01
public class ArgsTest
{
	public static void main(String[] args)
	{
		//输出args数组的长度
		System.out.println(args.length);
		//遍历args数组的每个元素
		for(var arg:args)
		{
			System.out.println(arg);
		}
	}
}

使用javac ArgsTest命令运行上面程序,看到程序仅仅输出一个0,这表明args数组是一个长度为0的数组。程序没有给args数组设定参数值,那么JVM就不知道args数组的元素,所以JVM将args数组设置成一个长度为0的数组.

改为如下命令来运行上面程序:

java ArgsTest Java Spring

将看到下图所示的运行结果:

从图中可以看出,如果运行Java程序时在类名后紧跟一个或多个字符串(多个字符串之间以空格隔开),JVM就会把这些字符串依次赋给args数组元素。运行Java程序时的参数与args数组之间的对应关系如下图所示:

如果某参数本身包含了空格,则应该将该参数用双引号("")括起来,否则JVM会把这个空格当成参数分隔符,而不是参数本身。例如,采用如下命令来运行上面程序:

java ArgsTest "Java Spring"

看到args数组的长度是1,只有一个数度元素,其值是Java Spring。

使用Scanner获取键盘输入

使用Scanner类可以很方便地获取用户的键盘输入, Scanner是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本类型值和字符串值,。Scanner类提供了多个构造器,不同的构造器可以接收文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。

Scanner主要提供了两个方法来扫描输入:

hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果只是判断是否包含下一个字符串,则直接使用hasNext()。 

nextXxx():获取下一个输入项。Xxx含义与前一个方法中的Xxx相同。

在默认情况下Scanner使用空白(包括空格、Tab空白、回车)作为多个输入项之间的分隔符。

下面程序使用Scanner来获得用户的键盘输入:

//example02-1
import java.util.Scanner;
public class ScannerKeyBoardTest
{
	public static void main(String[] args)
	{
		//System.in代表标准输入,就是键盘输入
		Scanner sc=new Scanner(System.in);
		//增加下面一行将只把回车作为分隔符
		sc.useDelimiter("n");
		//判断是否还有下一个输入项
		while(sc.hasNext())
		{
			//输出输入项
			System.out.println("键盘输入的内容是:"+sc.next());
		}
	}
}

Scanner的读取操作可能被阻塞(当前执行顺序流暂停)来等待信息的输入。如果输入源没有结束,Scanner又读不到更多输入项时(尤其在键盘输入时比较常见),Scanner的hasNext()和next()方法都有可能阻塞,hasNext()方法是否阻塞与和其相关的next()方法是否阻塞无关。

为Scanner设置分隔符使用useDelimiter(String pattern)方法即可,该方法的参数应该是一个正则表达式。

Scanner提供了两个简单的方法,来逐行读取:

boolean hasNextLine():返回输入源中是否还有下一行。

String nextLine():返回输入源中下一行的字符串。

Scanner不仅可以获取字符串输入项,也可以获取任何基本类型的输入项。如下程序所示:

//example02-2
import java.util.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读取该文件的内容,如下程序所示:

//example03
import java.util.Scanner;
import java.io.*;
public class ScannerFileTest
{
	public static void main(String[] args) throws Exception
	{
		//将一个File对象作为Scanner的构造器参数,Scanner读取文件内容
		Scanner sc=new Scanner(new File("ScannerFileTest.txt"));
		System.out.println("ScannerFileTest.txt文件内容如下:");
		//判断是否还有下一行
		while(sc.hasNextLine())
		{
			//输出文件中的下一行
			System.out.println(sc.nextLine());
		}
		sc.close();
	}
}

上面程序传入一个File对象作为参数,将会读取ScannerFileTest.txt文件中的内容。程序使用了hasNextLine()和nextLine()两个方法,表明该程序将逐行读取ScannerFileTest.txt文件的内容。

因为上面程序涉及文件输入,可能引发文件IO相关异常,故主程序声明throws Exception表明main方法不处理任何异常。关于异常的内容后续博客再讲解。

2.系统相关

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

System类

System类代表当前Java程序的运行平台,程序不能创建System类的对象,System类提供了一些类变量和类方法,允许直接通过System类来调用这些类变量和类方法。

System类提供了代表标准输入、标准输出和错误输出的类变量,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库的方法。

加载文件和动态链接库主要对native方法有用,对于一些特殊的功能(如访问操作系统底层硬件设备等) Java程序无法实现,必须借助C语言来完成.此时需要使用C语言为Java方法提供实现.其实现步骤如下:
(1)Java程序中声明native修饰的方法,类似于abstract方法,只有方法签名,没有实现。使用javah命令编译该Java程序,将生成一个.class文件和一个.h头文件。
(2)写一个.cpp文件实现native方法,这一步需要包含第1步产生的.h文件(这个.h文件中又包含了JDK带的jni.h文件)。
(3)将第2步的.cpp文件编译成动态链接库文件。
(4)在Java中用System类的loadLibrary..()方法或Runtime类的loadLibrary()方法加载第3步产生的动态链接库文件,Java程序中就可以调用这个native方法了。

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

//example04
import java.lang.System;
import java.io.*;
import java.util.Map;
import java.util.Properties;
public class SystemTest
{
	public static void main(String[] args) throws Exception
	{
		//获取系统所有的环境变量
		Map 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类的getnv()、getProperties()、getProperty()等方法来访问程序所在平台的环境变量和系统属性,程序运行的结果会输出操作系统所有的环境变量值,并输出JAVA_HOME环境变量以及os.name系统属性的值。该程序运行结束后,还会在当前路径下生成一个props.txt文件,该文件中记录了当前平台的所有属性。

运行结果如下图所示;

System类还有两个获取系统当前时间的方法:currentTimeMillis()【以毫秒作为单位】和nanoTime()【以纳秒作为单位】,它们都返回一个long型整数。实际上它们都返回当前时间与UTC1970年1月1日午夜的时间差。这两个方法返回的时间粒度取决于底层操作系统,可能所在的操作系统根本不支持以毫秒、纳秒作为计时单位。

 System类的in、out和err分别代表系统的标准输入(通常是键盘)、标准输出(通常是显示器)和错误输出流,并提供了setIn()、 setOut()和setErr()方法来改变系统的标准输入、标准输出和标准错误输出流。

System类还提供了一个indentityHashCode(Object x)方法,该方法返回指定对象的精确hashCode值,也就是根据该对象的地址计算得到的hashCode值。当某个类的hashCode()方法被重写后,该类实例的hashCode()方法就不能唯一地标识该对象;但通过indentityHashCode()方法返回的hashCode值,依然是根据该对象的地址计算得到的hashCode值,所以如果两个对象的indentityHashCode值相同,则两个对象绝对是同一个对象。

如下程序所示:

//example05
public class IdentityHashCodeTest
{
	public static void main(String[] args) throws Exception
	{
		//下面程序中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));
	}
}

输出结果为:

99162322----99162322
321001045----791452441
834600351----834600351

Runtime类

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

Runtime类也提供了gc()方法和runFinalization()方法来通知系统进行垃圾回收、清理系统资源,并提供了load(String filename)和loadLibrary(String filename)方法来加载文件和动态链接库。

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

//example06
public class RuntimeTest
{
	public static void main(String[] args) throws Exception
	{
		//获取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类还有一个功能——它可以直接单独启动一个进程来运行操作系统的命令。

如下程序所示:

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

Runtime提供了一系列exec()方法来运行操作系统命令,关于它们之间的细微差别,请读者自行查阅API文档。

通过exce启动平台上的命令之后,它就变成了一个进程,Java使用process来代表进程。

3.常用类

Object类

Object的类是所有类、数组、枚举类的父类,Java允许把任何类型的对象赋给Object类型的变量。当定义一个类时,如果没有使用extends关键字为它显式指定父类,则该类默认继承Object父类。

任何Java的对象都可以调用Object类的方法,Object类提供了如下几个常用方法:
boolean equals(Object obj):判断指定对象与该对象是否相等。此处相等的标准是两个对象是同一个对象,因此该方法通常没有太大的实用价值。
protected void finalize():当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源。
Class getCass():返回该对象的运行时类型,该方法在后续中还有更详细的介绍。
int hashCode():返回该对象的hashCode值,在默认情况下Object类的hashCode()方法根据该对象的地址来计算,但很多类都重写了Object类的hashCode()方法,不再根据地址来计算其hashCode()方法值。
String 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()方法来得到该对象的副本,并返回该副本。

如下程序示范了如何实现“自我克隆”:

//example08
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);
		//clona得到u1对象的副本
		User u2=u1.clone();
		//判断u1、u2是否相同
		System.out.println(u1==u2);//(1)输出false
		//判断u1、u2的address是否相同
		System.out.println(u1.address==u2.address);//(2)输出true
	}
}

 Object类提供的clone机制,只是对对象里各实例变量进行简单复制,如果实例变量的类型是引用类型,Object的clone机制也只是简单的复制这个引用变量,这样原有对象的引用类型的实例变量与克隆对象的引用类型的实例变量依然指向内存中的同一个实例,所以上面程序在(2)号代码处输出true。上面程序“克隆”出来的u1、u2所指向的对象在内存中的存储示意图如下图所示:

Object类提供的clone()方法的“自我克隆”机制十分高效,比如clone一个包含100个元素的int[]数组,用系统默认的clone方法比静态copy方法快近2倍。

Object类的clone()方法虽然简单、易用,但是它不会对引用类型的成员变量值所引用的对象进行克隆。如果开发者需要对对象进行“深克隆”,则需要开发者自己进行“递归”克隆,保证所有引用类型的成员变量值所引用的对象都被复制了。

操作对象的Objects工具类

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

Java为工具类的命名习惯是添加一个字母s,比如操作数组的工具类是Arrays,操作集合的工具类是Collections。

如下程序示范了Objects工具类的用法:

//example09
import java.util.Objects;
public class ObjectsTest
{
	//定义一个obj变量,它的默认值是null
	static ObjectsTest obj;
	public static void main(String[] args)
	{
		//输出一个null对象的hashCode值,输出为0
		System.out.println(Objects.hashCode(obj));
		//输出一个null对象的toString,输出为null
		System.out.println(Objects.toString(obj));
		//要求obj不能为null,否则会引发异常
		System.out.println(Objects.requireNonNull(obj,"obj参数不能是null!"));
	}
}

Objects提供的requireNonNull()方法,当传入的参数不为null时,该方法返回参数本身;否则将会引发NullPointerException异常。该方法主要用来对方法形参进行输入校验,例如如下代码:

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

String、StringBuffer和StringBuilder类

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

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

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的 append()、insert()、reverse()、setChar()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。

StringBuilder类是JDK1.5新增的类,它也代表可变字符串对象。实际上StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。因此,在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用StringBuilder类。

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

类提供了大量构造器来创建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 charseName):使用特定的默认字符集将指定的byte[]数组,从offset开始、长度为length的子数组解码成一个新的String对象。
String(byte[] bytes,String charseName):使用指定的字符集将指定的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类也提供了大量方法来操作字符串对象,关于这些方法的详细介绍,请读者自行查阅API文档。

StringBuilder、StringBuffer有两个属性:length和capacity,其中length属性表示其包含的字符序列的长度。与String对象的length不同的是:SringBuffer的length是可以改变的,可以通过length()、setLength(int len)方法来访问和修改其字符序列的长度。capacity属性表示StringBuilder的容量,capacity通常比length大,程序通常无需关心capacity属性。

如下程序示范了StringBuilder类的用法:

//example10
public class StringBuilderTest
{
	public static void main(String[] args)
	{
		StringBuilder sb=new StringBuilder();
		//追加字符串
		sb.append("java");
		//插入
		sb.insert(0,"hello ");
		//替换
		sb.replace(5,6,",");
		//删除
		sb.delete(5,6);
		//反转
		sb.reverse();
		System.out.println(sb);
		System.out.println(sb.length());//输出9
		System.out.println(sb.capacity());//输出16
		//改变StringBuilder的长度,将只保留前面部分
		sb.setLength(5);
		System.out.println(sb);
	}
}

Math类

Math类是一个工具类,它的构造器是被定义成private的,因此无法创建Math类的对象;Math类中的所有方法都是类方法,可以直接通过类名来调用他们。Math类除提供了大量静态方法之外,还提供了两个类变量:PI和E,正如它们名字所暗示的,它们的值分别等于Π和e。

Math类的所有方法名都明确标识了该方法的作用,读者可自行查阅API来了解Math类各个方法的说明。下面程序示范了Math类的用法:

//example11
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.6)="+Math.round(2.6));
		
		//计算平方根
		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(-5)="+Math.abs(-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(5.0,9.1)="+Math.max(5.0,9.1));
		//计算最小值
		System.out.println("Math.min(1.2,0.5)="+Math.min(1.2,0.5));
		//返回第一个参数和第二个参数之间与第一个参数相邻的浮点数
		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类的用法几乎覆盖了Math类的所有数学计算功能,读者可参考上面程序来学习Math类的用法。

ThreadLocalRandom与Random

ThreadLocalRandom类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以当前时间作为种子),另一个构造器需要程序员显式传入一个long型整数的种子。

ThreadLocalRandom类是Java7新增的一个类,它是Random的增强版。在并发访问的环境下使用ThreadLocalRandom来代替Random,可以减少多线程资源竞争,最终保证系统具有更好的线程安全性。

ThreadLocalRandom类的用法与Random类的用法基本相似,它提供了一个静态的current方法来获取ThreadLocalRandom对象,获取该对象之后即可调用各种nextXxx()方法来获取伪随机数了。

ThreadLocalRandom与Random都比Math的random()方法提供了更多的方式来生成各种伪随机数,可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,还可以指定生成随机数的范围。

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

//example12
import java.util.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);
		//生成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使用一个48位的种子,如果这个类的两个实例是用同一个种子创建的,对它们以相同的顺序调用方法,则它们会产生相同的数字序列.

下面就对上面的介绍做一个实验,可以看到当两个Random对象种子相同时,它们会产生相同的数字序列。值得指出的,当使用默认的种子构造Random对象时,它们属于同一个种子。

//example13
import java.util.Random;
public class SeedTest
{
	public static void main(String[] args)
	{
		Random rand1=new Random(50);
		System.out.println("第一个种子为50的Random对象");
		System.out.println("rand1.nextBoolean():t"+rand1.nextBoolean());
		System.out.println("rand1.nextInt():t"+rand1.nextInt());
		System.out.println("rand1.nextDouble():t"+rand1.nextDouble());
		System.out.println("rand1.nextGaussian():t"+rand1.nextGaussian());
		Random rand2=new Random(50);
		System.out.println("第二个种子为50的Random对象");
		System.out.println("rand2.nextBoolean():t"+rand2.nextBoolean());
		System.out.println("rand2.nextInt():t"+rand2.nextInt());
		System.out.println("rand2.nextDouble():t"+rand2.nextDouble());
		System.out.println("rand2.nextGaussian():t"+rand2.nextGaussian());
		Random rand3=new Random(100);
		System.out.println("第三个种子为100的Random对象");
		System.out.println("rand3.nextBoolean():t"+rand3.nextBoolean());
		System.out.println("rand3.nextInt():t"+rand3.nextInt());
		System.out.println("rand3.nextDouble():t"+rand3.nextDouble());
		System.out.println("rand3.nextGaussian():t"+rand3.nextGaussian());
	}
}

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

第一个种子为50的Random对象
rand1.nextBoolean():    true
rand1.nextInt():        -1727040520
rand1.nextDouble():     0.6141579720626675
rand1.nextGaussian():   2.377650302287946
第二个种子为50的Random对象
rand2.nextBoolean():    true
rand2.nextInt():        -1727040520
rand2.nextDouble():     0.6141579720626675
rand2.nextGaussian():   2.377650302287946
第三个种子为100的Random对象
rand3.nextBoolean():    true
rand3.nextInt():        -1139614796
rand3.nextDouble():     0.19497605734770518
rand3.nextGaussian():   0.6762208162903859

从上面运行结果来看,只要两个Random对象的种子相同,而且方法的调用顺序也相同,它们就会产生相同的数字序列,也就是说,Random产生的数字并不是真正随机的,而是一种伪随机。

为了避免两个Random对象产生相同的数字序列,通常推荐使用当前时间作为Random对象的种子。如下代码所示:

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

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

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

BigDecimal类

Java的double类型会发生精度丢失,尤其在进行算术运算时更容易发生这种情况。不仅是Java,很多编程语言也存在这样的问题。

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

查看BigDecimal类的BigDecimal(double val)构造器的详细说明时,可以看到不推荐使用该构造器的说明,主要是因为使用该构造器时有一定的不可预知性。当程序使用new BigDecimal(0.1)来创建一个BigDecimal对象时,它的值并不是0.1,它实际上等于一个近似0.1的数,这是因为0.1无法准确地表示为double浮点数,所以传入构造器的值不会正好等于0.1(虽然表面上等于该值)。

如果使用BigDecimal(String val)构造器的结果是可预知的——写入new BigDecimal("0.1");将创建一个BigDecimal,它正好等于预期的0.1。因此通常建议优先使用基于String的构造器。

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

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

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

//example14
import java.math.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.06000000000000000277555756156289135105907917022705078125
0.05-0.01= 0.04000000000000000277555756156289135105907917022705078125
0.05*0.01= 0.0005000000000000000277555756156289135105907917022705078125
0.05/0.01= 5.000000000000000277555756156289135105907917022705078125

创建BigDecimal对象时,不要直接使用double浮点数作为构造器参数来调用BigDecimal构造器,否则同样会发生精度丢失的问题。

如果程序中要求对double浮点数进行加、减、乘、除基本运算,则需要先将double类型数值包装成BigDecimal对象,调用BigDecimal对象的方法执行运算后再将结果转换成double型变量。这是比较繁琐的过程,可以考虑以BigDecimal为基础定义一个Arith工具类,该工具类代码如下:

//example15
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)
	{
		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("0.05-0.01= "+Arith.sub(0.05,0.01));
		System.out.println("0.05*0.01= "+Arith.mul(0.05,0.01));
		System.out.println("0.05/0.01= "+Arith.div(0.05,0.01));
	}
}

4.Java8的日期、时间类

Date类

Java提供了Date类来处理日期、时间(此处的Date是指java.util包下的Date类,Date对象,既包含日期也包含时间,但是他的大部分构造器方法都已经过时了,不再推荐使用了

 Date类提供了6个构造器,其中4个已经不再推荐使用,剩下的两个构造器如下:
Date():生成一个代表当前日期时间的Date对象,该构造器在底层调用System.currentTime Millis()获得long整数作为日期参数。
Date(long date):根据指定的long型整数来生成一个Date对象。该构造器的参数表示创建的Date对象和GMT1970年1月1日00:00:00之间的时间差,以毫秒作为计时单位。

与Date构造器相同的是,Date对象大部分方法也已经不再推荐使用了,剩下为数不多的几个方法如下:
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是一个设计相当糟糕的类,因此Java官方推荐,尽量少用Date的构造器和方法,如果需要对日期时间进行加、减运算或获取指定时间的年、月、日、时、分、秒信息,可使用Calendar工具类。

Calendar类

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

也可以创建自己的Calendar子类,然后将它作为Calendar对象使用(这就是多态)。

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

Calendar与Date都是表示日期的工具类,它们直接可以自由转换。

Calendar类提供了大量访问、修改时日期时间的方法,常用方法请读者自行查阅API文档。

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

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

//example16
import java.util.Calendar;
import static java.util.Calendar.YEAR;
import static java.util.Calendar.MONTH;
import static java.util.Calendar.DATE;
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(2021,10,19,16,30,23);//2021-11-19 16:30:30
		System.out.println(c.getTime());
		//将Calendar的年前推1年
		c.add(YEAR,-1);//2020-11-19 16:30:30
		System.out.println(c.getTime());
		//将Calendar的月前推11个月
		c.roll(MONTH,-11);
		System.out.println(c.getTime());
	}
}

上面程序使用了静态导入,它导入了Calendar类里的所有类变量,所以上面程序可以直接使用Calendar类的YEAR、MONTH、DATE等类变量。

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

1.add和roll的区别

add(int field,int amount)的功能非常强大,add主要用于改变Calendar的特定字段的值。如果需要增加某字段的值,则让amount为正数;如果需要减少某字段的值,则让amount为负数即可。

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

当被修改的字段超出它允许的范围时,会发生进位,即上一级字段也会增大。例如:

var cal1=Calender.getInatance();
cal1.set(2003,7,23,0,0,0);//2003-8-23
cal1.add(MONTH,6);//2003-8-23=>2004-2-29

如果下一级字段也需要改变,那么该字段会修正到变化最小的值。例如:

var cal2=Calender.getInatance();
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

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

var cal3=Calender.getInatance();
cal3.set(2003,7,23,0,0,0);//2003-8-23
//MONTH字段“进位”,但YEAR字段并不增加
cal3.add(MONTH,6);//2003-8-23=>2004-2-23

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

var cal4=Calender.getInatance();
cal4.set(2003,7,31,0,0,0);//2003-8-31
//MONTH字段“进位”后变成2,2月没有31日
//YEAR字段不会改变,2003年2月只有28天
cal4.add(MONTH,6);//2003-8-31=>2003-2-28

2.设置Calendar的容错性

调用Calendar对象的set()方法来改变指定时间字段的值时,有可能传入一个不合法的参数,Calendar提供了一个setLenient()方法用于设置它的容错性,Calendar默认支持较好的容错性,通过setLenient(false)可以关闭Calendar的容错性,让它进行严格的参数检查。

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

3.set()方法延迟修改

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

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

//example17
import static java.util.Calendar.DATE;
import java.util.Calendar;
public class LazyTest
{
	public static void main(String[] args)
	{
		Calendar c=Calendar.getInstance();
		c.set(2003,7,31);//2003-8-31
		//将月份设为9,但8月31日不存在
		//如果立即修改,系统将会把c自动调整到10月1日
		c.set(MONTH,8);
		//下面代码输出10月1日
		//System.out.println(c.getTime());//(1)
		//设置DATE字段位5
		c.set(DATE,5);///(2)
		System.out.println(c.getTime());//(3)
	}
}

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

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

新的日期、时间包

Java8专门新增了一个java.time包,该包下包含了包含了一些常用的类,请读者自行查阅API使用。

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还提供了Pattern和Matcher两个类专门用于提供正则表达式支持。

创建正则表达式

正则表达式就是一个用于匹配字符串的模板,可以匹配一批字符串,所以创建正则表达式就是创建一个特殊的字符串。

正则表达式所支持的合法字符如下表所示:

正则表达式所支持的合法字符
字符解释
x字符x(x可代表任何合法的字符)
mnn八进制数0mnn所表示的字符
xhh十六进制0xhh所表示的字符
uhhhh十六进制0xhhhh所表示的Unicode字符
t制符表('u0009')
n新行(换行)符('u000A')
r回车符('u000D')
f换页符('u000C')
a报警(bell)符('u0007')
eEscape符('u001B')
cxx对应的控制符。例如,cM匹配Ctrl-M。x值必须为A~Z或a~z之一。

除此之外,正则表达式中有一些特殊字符,这些特殊字符在正则表达式中有其特殊的用途,如果需要匹配这些特殊字符,就必须首先将这些字符转义,也就是在前面添加一个反斜线()。

正则表达式中的特殊字符如下表所示:

正则表达式中的特殊字符
预定义字符说明
$匹配一行的结尾。要匹配$字符本身,请使用$
^匹配一行的开头。要匹配^字符本身,请使用^
()标记子表达式的开始和结束位置。要匹配这些字符,请使用(和)
[]用于确定中括号表达式的开始和结束位置。要匹配这些字符,请使用[和]
{}用于标记前面子表达式出现的频度。要匹配这些字符,请使用{和}
*指定前面子表达式可以出现零次或多次。要匹配*字符本身,请使用*
+指定前面子表达式可以出现一次或多次。要匹配+字符本身,请使用+
?指定前面子表达式可以出现零次或一次。要匹配?字符本身,请使用?
.匹配除换行符n之外的任何单字符。要匹配.字符本身,请使用.
用于转义下一个字符,或指定八进制、十六进制字符。要匹配字符本身,请使用\
|指定两项之间任选一项。要匹配|字符本身,请使用|

将上面多个字符拼起来,就可以创建一个正则表达式。例如:

"u0041\\"  //匹配A
"u0061t"//匹配a<制表符>
"\?\["  //匹配?[

由于Java字符串中,反斜杠本身需要转译,因此两个反斜杠(\)实际上相当于一个(前一个反斜杠用于转义)。

上面的正则表达式依然只能匹配单个字符,这是因为还未在正则表达式中使用“通配符”,“通配符”是可以匹配多个字符的特殊字符。正则表达式中的“通配符”远远超出了普通通配符的功能,它被称为预定义字符。

正则表达式支持的预定义字符如下表所示:

预定义字符
预定义字符说明
.可以匹配任何字符
d匹配0~9的所有数字
D匹配非数字
s匹配所有空白字符,包括空格、制表符、回车符、换页符、换行符等
S匹配所有的非空白字符
w匹配所有的单词字符,包括0~9所有数字、26个英文字母和下画线(_)
W匹配所有的非单词字符

有了上面的预定义字符后,接下来就可以创建更强大的正则表达式了。例如:

c\wt  //可以匹配cat、cbt、cct、c0t、c9t等一批字符串
\d\d\d-\d\d\d-\d\d\d\d  //匹配000-000-0000形式的电话号码

在一些特殊情况下,需要使用方括号表达式。方括号表达式有下表所示的几种形式:

方括号表达式
方括号表达式说明
表示枚举例如[abc],表示a、b、c其中任意一个字符;[gz],表示g、z其中任意一个字符
表示范围:-例如[a-f],表示a~f范围内的任意字符;[\u004-\u0056],表示十六进制字符u0041到u0056范围的字符。范围可以和枚举结合使用,如[a-cx-z]表示a~c、x~z范围内的任意字符
表示求否:^例如[^abc],表示非a、b、c的任意字符;[^a~f]表示不是a~f范围内的任意字符
表示“与”运算:&&

例如[a-z&&[def]],求a~z和[def]的交集,表示d、e或f

[a-z&&[^bc]],a~z范围内的所有字符,除b和c之外,即[ad-z]

[a-z&&[^m-p]],a~z范围内的所有字符,除m~p范围之外的字符,即[a-lq-z]

表示“并”运算并运算与前面的枚举类似。例如[a-d[m-p]],表示[a-dm-p]

正则表达式还支持圆括号表达式,用于将多个表达式组成一个子表达式,圆括号中可以使用或运算符(|),例如,正则表达式"((public)|(proteced)|(privete))"用于匹配Java的三个访问控制符其中之一。

Java正则表达式还支持下表所示的几个边界匹配符:

边界匹配符
边界匹配符说明
^行的开头
$行的结尾
b单词的边界
B非单词的边界
A输入的开头
G前一个匹配的结尾
Z输入的结尾,仅用于最后的结束符
z输入的结尾

正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式:
Greedy(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,直到无法匹配为止,如果你发现表达式匹配的结果与预期的不符,很有可能是因为——你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去。
Reluctant(勉强模式):用问号后缀(?)表示,它只会匹配最少的字符。也称为最小匹配模式。
 Possessive(占有模式):用加号后缀(+)表示,目前只有Java支持占有模式。通常比较少用。

三种模式的数量表示符如下表所示:

三种模式的数量标识符
贪婪模式勉强模式占有模式说明
X?X??X?+X表达式出现零次或一次
X*X*?X*+X表达式出现零次或多次
X+X+?X++X表达式出现一次或多次
X{n}X{n}?X{n}+X表达式出现n次
X{n,}X{n,}?X{n,}+X表达式最少出现n次
X{n,m}X{n,m}?X{n,m}+X表达式最少出现n次,最多出现m次

关于贪婪模式和勉强模式的对比,看如下代码:

String str="hello , java!";
//贪婪模式的正则表达式
System.out.println(str.replaceFirst("\w*","■"));//输出■ , java!
//勉强模式的正则表达式
System.out.println(str.replaceFirst("\w*?","■"));//输出■hello , java!

当从"hello , java!"串中查找匹配"\w*"子串时,因为"\w*"使用了贪婪模式,数量表示符(*)会一直匹配下去,所以该字符串前面的所有单词字符都被它匹配到,直到遇到空格,所以替换后的效果"■ , java!";如果使用勉强模式,数量标识符(*)会尽量匹配最少字符,即匹配零个字符,所以替换后的结果是"■hello , java!"。

使用正则表达式

一旦在程序中定义了正则表达式,就可以使用Pattern和Matcher来使用正则表达式。

Pattern对象是正则表达式编译后在内存中的表示形式,因此,正则表达式字符串必须先被编译为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对象,所以效率不高。

Pattern是不可变类,可供多个并发线程安全使用。

Matcher类提供了如下几个常用方法:
find():返回目标字符串中是否包含与Pattern匹配的子串。
group():返回上一次与Pattern匹配的子串。
start():返回上一次与Pattern匹配的子串在目标字符串中的开始位置。
end():返回上一次与Pattern匹配的子串在目标字符串中的结束位置加1。
lookingAt():返回目标字符串前面部分与Pattern是否匹配。
matches()://返回true:返回整个目标字符串与Pattern是否匹配。
reset():将现有的Matcher对象应用于一个新的字符序列。

在Pattern、Matcher类的介绍中,经常会看到一个CharSequence接口,该接口代表一个字符序列,其中CharBuffer、String、StringBuffer、StringBuilder都是它的实现类。简单的说,CharSequence代表一个各种表示形式的字符串。

通过Matcher类的find()和group()方法可以从目标字符串中依次取出特定子串(匹配正则表达式的子串)。

下面程序示范了如何从大段的字符串中找出电话号码:

//example19
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FindGroup
{
	public static void main(String[] args)
	{
		//使用字符串模拟从网络上得到的网页源码
		String str="我想买一本《从你的全世界路过》,尽快联系我13584695203"+"交朋友,电话是13645896758"+"出售二手电脑,电话是15948762314";
		//创建一个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()方法主要用于确定子串在目标字符串中的位置,如下程序所示:

//example20
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.start()+",子串的结束位置:"+m.end());
		}
	}
}

matches()和lookingAt()方法有点相似,只是matches()方法要求整个字符串和Pattern完全匹配时才返回true,而lookingAt()只要字符串以Pattern开头就会返回true。reset()方法可将现有的Matcher对象应用于新的字符序列。看如下例子程序:

//example21
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MatchesTest
{
	public static void main(String[] args)
	{
		String[] mails=
		{
			"fdfgr@asd.com",
			"hyjdrs@asd.com",
			"hngc@asd.org",
			"dsd@asd.xx"
		};
		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);
		}
	}
}

上面程序创建了一个邮件地址的Pattern,接着用这个Pattern与多个邮件地址进行匹配,当程序中的Matcher为null时,程序要用matcher()方法来创建一个Matcher对象,一旦Matcher对象被创建,程序就调用的reset()方法,将该Matcher应用于新的字符序列。

从某个角度来看,Matcher的matches()、lookingAt()和String类的equals()、startsWith()有点相似,区别是String类的equals()和startsWith()都是与字符串进行比较,而Matcher的matches()和lookingAt()则是与正则表达式进行匹配。

事实上,String类里也提供了matches()方法,该方法返回该字符串是否匹配指定的正则表达式。例如:

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

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

//example22
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReplaceTest
{
	public static void main(String[] args)
	{
		String[] msgs=
		{
			"I Love You",
			"Java is my only love",
			"Java is so easy!"
		};
		Pattern p=Pattern.compile("ve\w*");
		Matcher matcher=null;
		for(int i=0;i 

上面程序使用了Matcher类提供的replaceAll()把字符串中所有与正则表达式匹配的子串替换成“******”,实际上,Matcher类还提供了一个replaceFirst(),该方法只替换第一个匹配的子串。

String类中也提供了replaceAll()、replaceFirst()、split()等方法。下面的例子程序直接使用String类提供的正则表达式功能来进行替换和分割。

//example23
public class StringReg
{
	public static void main(String[] args)
	{
		String[] msgs=
		{
			"I Love You",
			"Java is my only love",
			"Java is so easy!"
		};
		for(String msg:msgs)
		{
			System.out.println(msg.replaceFirst("ve\w*","******"));
			System.out.println(msg.split(" "));
		}
	}
}

上面程序只使用String类的replaceFirst()和split()方法对目标字符串进行了一次替换和分割。运行上面程序会看到下图所示的运行效果:

正则表达式是一个功能非常灵活的文本处理工具,增加了正则表达式支持后的Java,可以不再使用StringTokenizer类(也是一个处理字符串的工具,但功能远不如正则表达式强大)即可进行复杂的字符串处理。

6.变量处理和方法处理

MethodHandle

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

为了使用MethodHandle,还涉及如下几个类:
MethodHandles:MethodHandle的工厂类,它提供了一系列静态方法用于获取MethodHandle。
 MethodHandles.Lookup:Lookup静态内部类也是MethodHandle的工厂类,专门用于获取MethodHandle。
MethodType:代表一个方法类型。MethodType根据方法的形参、返回值类型来确定方法类型。

下面程序示范了它的用法:

//example24
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获取类方法
		var mtd1=MethodHandles.Lookup().findStatic(MethodHandleTest.class,"hello",type);
		//通过MethodHandle执行方法
		mtd1.invoke();
		//使用MethodHandles.Lookup的findVirtual获取实例方法
		var mtd2=MethodHandles.Lookup().findVirtual(MethodHandleTest.class,"hello");
		//通过MethodHandle执行方法,传入主调对象和参数
		System.out.println(mtd2.invoke(new MethodHandleTest(),"孙悟空"));
	}
}

7.国际化与格式化

全球化的Internet需要全球化的软件,软件的全球化意味着国际化和本地化.

国际化是指应用程序运行时,可根据客户端请求来自的国家/地区、语言的不同,而显示不同的界面。引入国际化的目的是为了提供自适应更友好的用户界面,并不需要改变程序的逻辑功能,国际化的英文单词是Internationalization,简称I18N。

一个国际化支持很好的应用,在不同的区域使用时会呈现出本地语言的提示,这个过程也被称为Localization,即本地化,本地化简称为L10N。

一个优秀的全球化软件产品对国际化和本地化的要求远远不止于此,甚至还包括用户提交数据的国际化和本地化。

Java国际化的思路

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所支持的国家和语言:

//example25
public class LocaleList
{
	public static void main(String[] args)
	{
		//返回Java所支持的全部国家和语言的数组
		Locale[] localeList=Locale.getAvailableLocales();
		//遍历数组的每个元素,依次获取所支持的国家和语言
		for(int i=0;i 

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

完成程序国际化

为了让程序可以输出不同的字符串,即使字符串常量可以改变,可以将需要输出的各种字符串(不同的国家/语言环境对应不同的字符串)定义在资源包中。

提供如下两个文件:

第一个文件:mess_zh_CN.properties,该文件的内容为:

#资源文件的内容是key-value对

hello=你好!

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

#资源文件的内容是key-value对

hello=Welcome You!

这两份文件的文件名的baseName是相同的,通过如下程序进行示范:

//example26
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"));
	}
}

如果希望程序完成国际化,只需要将不同的国家/语言(Locale)的提示信息分别以不同的文件存放即可。

Java程序国际化的关键类是ResourceBundle,它有一个静态方法:getBundle(String baseName,Locale locale),该方法将根据Locale加载资源文件,而Locale封装了一个国家、语言。

从上面资源文件的命名中可以看出不同国家、语言环境的资源文件的baseName是相同的,即baseName为mess的资源文件有很多个,不同的国家、语言环境对应不同的资源文件。

Java程序国际化的关键类是ResourceBundle和Locale,ResourceBundle根据不同的Locale加载语言资源文件,再根据指定的key取得已加载语言资源文件中的字符串。

Windows是一个非常奇葩的操作系统,它保存文件默认采用GBK字符集,因此,在Windows平台上执行javac命令,默认也用GBK字符集读取Java源文件,但实际开发项目时采用GBK字符集会引起很多乱码问题,所以通常推荐源代码都使用UTF-8字符集保存,但如果使用UTF-8字符集保存Java源代码,在命令行编译源程序时需要为javac显式指定-encoding utf-8选项,用于告诉javac命令使用UTF-8字符集读取Java源文件。

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

如果需要输出的消息中必须包含动态的内容,可以使用带占位符的消息,例如,提供一个myMess_en_US.properties文件,该文件的内容如下:

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

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

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

上面的两个资源文件必须用UTF-8字符集保存。

当程序使用ResourceBundle的getString()方法来取出msg对应的字符串时,程序还需要为{0}和{1}两个占位符赋值,此时需要使用MessageFormat类,该类包含了一个有用的静态方法:

format(String pattern,Object... values):返回后面的多个参数值以填充前面的pattern字符串,其中pattern字符串不是正则表达式,而是一个带占位符的字符串。

借助于上面的MessageFormat类的帮助,将国际化程序语言改成如下形式:

//example27
import  java.util.Locale;
import  java.util.Date;
import java.util.ResourceBundle;
import java.text.MessageFormat;
public class HelloArg
{
	public static void main(String[] args)
	{
		//定义一个Locale变量
		Locale current1=null;
		//如果运行程序指定了两个参数
		if(args.length==2)
		{
			//使用运行程序的两个参数构造Locale实例
			current1=new Locale(args[0],args[1]);
		}
		else
		{
			//否则直接使用系统默认的Locale
			current1=Locale.getDefault(Locale.Category.FORMAT);
		}
		//根据Locale加载语言资源
		var bundle=ResourceBundle.getBundle("myMess",current1);
		//取得已加载的语言资源文件中msg对应消息
		var msg=bundle.getString("msg");
		//使用MessageFormat为带占位符的字符串传入参数
		System.out.println(MessageFormat.format(msg,"yeeku",new Date()));
	}
}

输出结果为:

你好,yeeku!今天是2021/11/20 下午4:51。

使用类文件代替资源文件

Java也允许使用类文件代替资源文件,即将所有的key-value对存入class文件,而不是属性文件。

使用类文件来代替资源文件必须满足如下条件:
该类的类名必须是baseName_language_country.properties,这与属性文件的命名相似。
该类必须继承ListResourceBundle,并重写getContents()方法,该方法返回Object数组,该数组的每一项都是key-value对。

下面的类文件可以代替上面的属性文件:

//example28
public class myMess_zh_CN extends ListResourceBundle
{
	//定义资源
	private final Object myData[][]=
	{
		{"msg","{0},你好!今天的日期是{1}"}
	};
	//重写getContents()方法
	public Object[][] getContents()
	{
		//该方法返回资源的key-value对
		return myData;
	}
}

如果系统同时存在资源文件、类文件,系统将以类文件为主,而不会调用资源文件。

对于简体中文的Locale,ResourceBundle搜索资源文件的顺序是:

(1)baseName_zh_CN.class

(2)baseName_zh_CN.properties

(3)baseName_zh.class

(4)baseName_zh.properties

(5)baseName.class

(6)baseName.properties

按系统按上面的顺序搜索资源文件,如果前面的文件不存在,才会使用下一个文件。如果一直找不到对应的文件,系统将抛出异常。

使用NumberFormat格式化数字

MessageFormat是抽象类Format的子类,Format抽象类还有两个子类:NumberFormat和DateFormat,它们分别用于实现数值、日期的格式化。NumberFormat、DateFormat可以将数值、日期转换成字符串,也可以将字符串转换成数值日期。

下图显示了NumberFormat和DateFormat的主要功能:

NumberFormat和DateFormat都包含了format()和parse()方法,其中format()用于将数值、日期格式化成字符串,parse()用于将字符串解析成数值、日期。

NumberFormat也是一个抽象基类,所以无法通过它的构造器来创建NumberFormat对象,它提供了如下几个类来得到NumberFormat对象:
getCurrencyInstance():返回默认Locale的货币格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的货币格式器。
getIntegerInstance():返回默认Locale的整数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的整数格式器。
getNumberInstance():返回默认Locale的通用数值格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的通用数值格式器。
getPercentInstance():返回默认Locale的百分数格式器。也可以在调用该方法时传入指定的Locale,则获取指定Locale的百分数格式器。

一旦取得了NumberFormat对象后,就可以调用它的format()方法来格式化数值,包括整数和浮点数。如下例子程序示范了NumberFormat的三种数字格式化器的用法:

//example29
import java.util.Locale;
import java.text.NumberFormat;
public class NumberFormatTest
{
	public static void main(String[] args)
	{
		//需要被格式化的数字
		double db=1234000.567;
		//创建四个Local,分别代表中国、日本、德国、美国
		Locale[] locales={Locale.CHINA,Locale.JAPAN,Locale.GERMAN,Locale.US};
		NumberFormat[] nf=new NumberFormat[12];
		//为上面四个Local创建12个NumberFormat对象
		//每个Locale分别有通用数值格式器、百分数格式器、货币格式器
		for(int i=0;i 

同样的数值在不同国家的写法是不同的,而NumberFormat的作用就是把数值转换成不同国家的本地写法,至于使用DateFormat类将字符串解析成数值的意义不大(因为可以使用Integer、Double等包装类完成这种解析),故此处不再赘述。

使用DateFormat格式化日期、时间

与NumberFormat相似的是DateFormat也是一个抽象类,它也提供了如下几个类方法用于获取DateFormat对象:
getDateInstance():返回一个日期格式器,它格式化后的字符串只有日期,没有时间。该方法可以传入多个参数,用于指定日期样式和Locale等参数;如果不指定这些参数,则使用默认参数。
getTimeInstance():返回一个时间格式器,它格式化后的字符串只有时间,没有日期。该方法可以传入多个参数,用于指定日期样式和Locale等参数;如果不指定这些参数,则使用默认参数。
getDateTimeInstance():返回一个日期、时间格式器,它格式化后的字符串既有日期,也有时间。该方法可以传入多个参数,用于指定日期样式和Locale等参数;如果不指定这些参数,则使用默认参数。

上面三个方法可以指定日期样式、时间样式参数,它们是DateFormat的4个静态常量:FULL、LONG、MEDIUM和SHORT,通过这4个样式参数可以控制生成的格式化字符串。看如下例子程序:

//example30
import java.text.ParseException;
import java.util.Locale;
import java.util.Date;
import java.text.DateFormat;
public class DateFormatTest
{
	public static void main(String[] args) throws ParseException
	{
		//需要被格式化的时间
		Date dt=new Date();
		//创建两个Local,分别代表中国、美国
		Locale[] locales={Locale.CHINA,Locale.US};
		DateFormat[] df=new DateFormat[16];
		//为上面两个Locale创建16个DateFormat对象
		for(int i=0;i 

DateFormat也具有国际化的能力,同一个日期使用不同的Locale格式器格式化的效果完全不同,格式化后的字符串正好符合Locale对应的本地习惯。

获得了DateFormat之后,还可以调用它的setLenient(boolean lenient)方法来设置该格式器是否采用严格语法。举例来说,如果采用不严格的日期语法(该方法参数为true),对于字符串"2004-2-31"将会转换成2004年3月2日;如果采用严格的日期语法,解析该字符串时将会抛出异常。

DateFormat的parse()方法可以把一个字符串解析成Date对象,但它要求被解析的字符串必须符合日期字符串的要求,否则可能抛出ParseException异常。例如如下代码片段:

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

因为"2007/10/07"是一个SHORT样式的日期字符串,必须用SHORT样式的DateFormat实例解析,否则将抛出异常。

使用SimpleDateFormat格式化日期

前面介绍的DateFormat的parse()方法可以把字符串解析成Date对象,但实际上DateFormat的parse()方法不够灵活,它要求被解析的字符串必须满足特定的格式,为了更好的格式化日期、解析日期字符串,Java提供了SimpleDateFormat类。

SimpleDateFormat是DateFormat的子类,它比DateFormat更简单、功能更强大。

SimpleDateFormat可以非常灵活的格式化Data,也可以用于解析各种格式的日期字符串。创建SimpleDateFormat对象时需要传入一个pattern字符串,这个pattern不是正则表达式,而是一个日期模板字符串。

看下面程序;

//example31
import java.text.ParseException;
import java.util.Date;
import java.text.SimpleDateFormat;
public class DateFormatTest
{
	public static void main(String[] args) throws ParseException
	{
		Date d=new Date();
		//创建一个SimpleDateFormat对象
		SimpleDateFormat sdf1=new SimpleDateFormat("Gyyyy年中第D天");
		//将d格式化成日期,输出:公元2021年中第324天
		String dateStr=sdf1.format(d);
		System.out.println(dateStr);
		//一个非常特殊的日期字符串
		String str="14###3月##21";
		SimpleDateFormat sdf2=new SimpleDateFormat("y###MMM##d");
		//将日期字符串解析成日期,输出:Fri Mar 21 00:00:00 CST 2014
		System.out.println(sdf2.parse(str));
	}
}

如果想知道SimpleDateFormat支持哪些日期、时间占位符,可以查阅API文档中的SimpleDateFormat类的说明,此处不再赘述。

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

Java8新增的日期、时间API里不仅包括了Instant、LocalDate、LocalDateTime、LocalTime等代表日期、时间的类,而且在java.time.format包下提供了一个DateTimeFormatter格式器类,它不仅可以将日期、时间对象格式化成字符串,也可以将特定格式的字符串解析成日期、时间对象。

为了使用DateTimeFormatter进行格式化或解析,必须先获取DateTimeFormatter对象,获取DateTimeFormatter对象有如下三种常见的方式:
(1)直接使用静态常量创建DateTimeFormatter格式器。DateTimeFormatter类中包含了大量形如ISO_LOCAL_DATE、ISO_LOCAL_TIME、ISO_LOCAL_DATE_TIME等静态常量,这些静态常量本身就是DateTimeFormatter实例。
(2)使用代表不同风格的枚举值来创建DateTimeFormatter格式器。在FormatStyle枚举类中定义了FULL、LONG、MEDIUM、SHORT四个枚举值,它们代表日期、时间的不同风格。
(3)根据模式字符串来创建DateTimeFormatter格式器。类似于SimpleDateFormat可以采用模式字符串来创建DateTimeFormatter,如果需要了解DateTimeFormatter支持哪些模式字符串,则需要参考该类的API文档。

使用完成DateTimeFormatter格式化

使用DateTimeFormatter将日期、时间(LocalDate、LocalDateTime、LocalTime等实例)格式化为字符串,可通过如下两种方式:
调用DateTimeFormatter的format(TemporalAccessor temporal)方法执行格式化,其中LocalDate、LocalDateTime、LocalTime等类都是接口的实现类。
调用LocalDate、LocalDateTime、LocalTime等日期、时间对象的format(DateTimeFormatter formatter)方法执行格式化。

上面两种方式的功能相同,用法也基本相似。如下程序示范了使用DateTimeFormatter来格式化日期、时间:

//example32
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
import java.time.format.FormatStyle;
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%%MM%%dd HH:mm:ss")
		};
		LocalDateTime date=LocalDateTime.now();
		//依次使用不同的格式器对LocalDateTime进行格式化
		for(int i=0;i 

上面程序使用三种方式创建了6个DateTimeFormatter对象代码,分别使用不同方式来格式化日期。运行上面程序会看到下图所示的效果:

使用DateTimeFormatter进行格式化时,不仅可按系统预置的格式对日期、时间进行格式化,也可使用模式字符串对日期、时间进行自定义格式化。

使用DateTimeFormatter解析字符串

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

如下程序示范了使用DateTimeFormatter解析日期、时间字符串:

//example33
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
public class NewFormatterParse
{
	public static void main(String[] args)
	{
		//定义一个任意格式的日期、时间字符串
		String str1="2014==04==12 01时06分09秒";
		//根据需要解析的日期、时间字符串定义解析所用的格式器
		DateTimeFormatter formatter1=DateTimeFormatter.ofPattern("yyyy==MM==dd HH时mm分ss秒");
		//执行解析
		LocalDateTime dt1=LocalDateTime.parse(str1,formatter1);
		System.out.println(dt1);//输出2014-04-12T01:06:09
		//下面代码再次解析另一个字符串
		String str2="2014$$$04月$$$13 20小时";
		DateTimeFormatter formatter2=DateTimeFormatter.ofPattern("yyyy$$$MM月$$$dd HH小时");
		LocalDateTime dt2=LocalDateTime.parse(str2,formatter2);
		System.out.println(dt2);//输出2014-04-13T20:00
	}
}


以上内容均属于博主自学的过程中记的笔记,故与博主所用教材比较类似。另外,文中所述内容如果有错误欢迎指出!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/571763.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号