第七章 Java基础类库

第七章 Java基础类库

Oracle为java提供了丰富的基础类库,java8提供了4000多个基础类(包括下一章将要介绍的集合框架),通过这些基础类库可以提高开发效率,降低开发难度。对于合格的Java程序员而言,至少要熟悉 Java SE中70%以上的类(当然本书并不是让读者去背诵 Java API文档),但在反复查阅API文档 的过程中,会自动记住大部分类的功能、方法,因此程序员一定要多练,多敲代码。

Java提供了 String、 StringBuffer和 StringBuilder来理字符串,它们之间存在少许差别,本章会详细介绍它们之间的差别,以及如何选择合适的字符串类。Java还提供了Date和 Calendar来处理日期、 时间,其中Date是一个已经过时的API,通常推荐使用 Calendar来处理日期、时间。

正则表达式是一个强大的文本处理工具,通过正则表达式可以对文本内容进行查找、替换、分割等 操作。从K1.4以后,Java也增加了对正则表达式的支持,包括新增的 Pattem和 Matcher两个类,并 改写了 String类,让 String类增加了正则表达式支,增加了正则表达式功能后的 String类更加强大。

Java还提供了非常简单的国际化支持, Java使用 Locale对象封装一个国家、语言环境,再使用 ResourceBundle根据 Locale加载语言资源包,当 ResourceBundle加载了指定 Locale对应的语言资源文件后, ResourceBundle对象就可调用 getString()方法来取出指定key所对应的消息字符串。

7.1 与用户交互

本节主要介绍如何获得用户的键盘输入

7.1.1 运行Java程序的参数

main()方法的方法签名

//Java程序入口:main()方法
public static void main(String[] args)
  • public 修饰符:Java类由JVM调用,为了让JVM可以自由调用这个main()方法,所以用public修饰符把这个方法暴露出来。
  • static 修饰符:JVM调用这个主方法时,会创建该主类的对象,然后通过对象来调用该主方法。JVM直接通过该类来调用主方法,因此使用static修饰该方法。
  • void 返回值:因为主方法被JVM调用,该方法的返回值将返回给JVM,这没有任何意义,因此main()方法没有返回值。

根据方法调用规则:谁调用方法,谁负责为形参赋值。也就是说main()方法由JVM调用,即args形参应该由JVM负责赋值,但JVM怎么知道如何为args赋值呢,看如下:

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

上面程序输出0,表明args数组是一个长度为0的数组——这是合理的。因为计算机是没有思考能力的,他只能忠实地执行用户交给它的任务,既然程序没有给args数组设定参数值,那么JVM就不知道args数组的元素,所以JVM将args数组设置成一个长度为0的数组。

改为一下命令运行此程序:

java ArgsTest.java Java Spring

即可看到如下结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QPA2xcmC-1618031098508)(C:\Users\MingyangLiu\Desktop\张智超\image\Snipaste_2021-03-19_16-17-33.png)]

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BocDUIY6-1618031098510)(C:\Users\MingyangLiu\Desktop\张智超\image\P10319-162517(1).jpg)

如果某参数本身包含了空格,则应该将该参数用双引用引用,否则JVM会把这个空格当成参数分隔符,而不是参数本身。

7.1.2 使用Scanner获取键盘输入

使用Scanner类可以很方便地获取用户键盘输入,Scanner是一个基于正则表达式的文本扫描器,他可以用文件,输入流,字符串作为数据源,用于文件、输入流、字符串中解析数据。

Scanner主要看以下两个方法进行扫描输入:

  1. hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果只是判断是否包含下一个字符串。则直接用hasNext()。
  2. nextXxx():获取下一个输入项。Xxx的含义与前一个方法中的Xxx相同。

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

import java.util.Scanner;

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

上面的程序可以不断地输入输出。

如果希望改变Scanner的分隔符(不使用空白作为分隔符),例如,程序需要每次读取一行,不管这一行中是否包含空格,Scanner都把它当成一个输入项。在这种需求下,可以把Scanner的分隔符设置为回车符,不在使用默认的空白作为分隔符。

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

为Scanner设置分隔符使用useDelimiter(String pattern)方法即可,该方法的参数应该是一个正则表达式,,只要把上面程序中粗体字代码行的注释去掉,该程序就会把键盘的每行输入当成一个输入项,不会以空格、TAB空白等作为分隔符。事实上,Scanner提供了两个简单的方法来逐行读取。

  • boolean hasNextLine():返回输入源中是否还有下一行。
  • String nextLine():返回输入源中下一行的字符串。

Scanner不仅可以获取字符串输入项,也可以获取任何基本类型的输入项:

import java.util.Scanner;

public class ScannerLongTest {
public static void main(String[] args) {
	var sc = new Scanner(System.in) ;
    //控制输入的格式
while(sc.hasNextLong())//粗
	{
		System.out.println("键盘输入的是:"
                           +sc.nextLong());		//粗
	}
}
}

注意粗,正如通过hasNextLong()和nextLong()两个方法,Scanner可以直接从输入流中获得long型证书输入项。与此类似,如果需要输入其他基本类型的输入项,则可以使用相应的方法。

==上面的程序不如ScannerKeyBoardTest程序适应性强,因为这个程序要求必须输入的格式,否则退出。

Scanner不仅能读取用户的键盘键入,还可以读取文件输入。只要在创建Scanner对象时传入了一个File对象作为参数,就可以让Scanner读取该文件的内容。

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

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

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

因为上面程序涉及文件输入,可能引发文件IO相关异常,故主程序声明throws Exception表明main方法不处理任何异常。

7.2 系统相关

Java提供了System类和Runtime类来与程序的运行平台进行交互。

7.2.1 System类

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

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

注意: 加载文件和动态链接库主要对 native方法有用,对于一些特殊的功能(如访问操作系统底层硬件设备等)Java程序无法实现,必须借助C语言来完成,此时需要使用C语言为Java方法提供实现。其实现步骤如下: ①Java程序中声明 native修饰的方法,类似于abstract方法,只有方法签名,没有实现。使用带h选项的 Javac命令编译该Java程序,将生成一个class文件和一个h头文件 ②写一个.cpp文件实现 native方法,这一步需要包含1步产生的.h文件(这个h 文件中又包含了JDK带的jni.h文件) ③将第2步的.cpp文件编译成动态链接库文件 ④在Java中用 System类的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 SystemTest 
{
	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();
		props.store(new FileOutputStream("props.txt"), "System Properties");
		//输出特定的系统属性
		System.out.println(System.getProperty("os.name"));
	}
}

该程序运行结束之后还会在当前路径下生产一个Props.txt文件,该文件中记录了当前平台所有系统属性。

System类提供了通知系统进行垃圾回收的gc()方法,以及通知系统进行资源清理的 runFinalization(方法。关于这两个方法的用法请参考本书6.10节的内容

System类还有两个获取系统当前时间的方法 : currentTimeMillis()和nanoTime(),它们都返回一个long型整数。实际上它们都返回当前时间与UTC1970年1月1日午夜的时间差,前者以毫秒作为单位, 后者以纳秒作为单位。必须指出的是,这两个方法返回的间粒度取决于底层操作系统,可能所在的操作系统根本不支持以毫秒、纳秒作为计时单位。例如,许多操作系统以几十毫秒为单位测量时间,currentTimeMillis()方法不可能返回精确的毫秒数,而nanoTime()方法很少用,因为大部分操作系统都不支持使用纳秒作为计时单位。

除此之外, System类的in、out和err分别代表系统的标准输入(通常是键盘)、标准输出(通常是 显示器)和错误输出流,并提供了 setIn(),setout()和setErr()方法来改变系统的标准输入、标准输出和标准错误输出流。关于如何改变系统的标准输入、输出的方法,可以参考本书第15章的内容。

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

public class IdentityHashCodeTest 
{
	public static void main(String[] args) 
	{
//		下面程序中的s1和s2是两个不同的对象
		var s1 = new String("Hello");
		var s2 = new String("Hello");
//		String重写了hasCode()方法——改为根据字符序列计算hashCode值
//		因为S1和S2的字符序列相同,所以他们的hasCode()方法返回值相同
		System.out.println(s1.hashCode()+"-----"+s2.hashCode());
//		s1和s2的不同的字符串对象,所以他们的identityHashCode值不同
		System.out.println(System.identityHashCode(s1) + "----"+System.identityHashCode(s2));
		var s3 = "Java";
		var s4 = "Java";
//		s3和s4是相同的字符串对象,所以他们的identityHashCode值相同。
		System.out.println(System.identityHashCode(s3)+"---"+System.identityHashCode(s4));
	}
}

通过 identityHashCode(Object x)方法可以获得对象的 identityHashCode值,这个特殊的identityHashCode值可以唯一地标识该对象。因为identityHashCode值是根据对象的地址计算得到的, 所以任何两个对象的 identityHashCode值总是不相等。

7.2.2 Runtime类与java9的 ProcessHandle

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

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

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

public class RuntimeTest 
{
	public static void main(String[] args) 
	{
//		获取Java程序关联的运行时的环境
		var rt = Runtime.getRuntime();
		System.out.println("处理器数量:"+rt.availableProcessors());
		System.out.println("空闲内存数:"+rt.freeMemory());
		System.out.println("总内存数:"+rt.totalMemory());
		System.out.println("可用最大内存数:"+rt.maxMemory());
	}
}

Runtime类还有一个功能——他可以直接启动一个进程来运行操作系统的命令:

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

上面程序中粗体字代码将启动 Windows系统里的“记事本”程序。 Runtime提供了一系列 exec() 来运行操作系统命令,关于它们之间的细微差别,请读者自行查阅API文档。

通过exec启动平台上的命令之后,它就变成了一个进程,Java使用 Process来代表进程java9还新增了一个ProcessHandle接口,通过该接口可获取进程的ID、父进程和后代进程:通过该接口的onExit() 方法可在进程结束时完成某些行为。

ProcessHandle还提供了一个 ProcessHandle.Info类,用于获取进程的命令、参数、启动时间、累计运行时间、用户等信息。下面程序示范了通过 ProcessHandle获取进程的相关信息。

import java.util.concurrent.CompletableFuture;

public class ProcessHandleTest 
{
	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信息
		ProcessHandle.Info info = ph.info();
//		通过ProcessHandle.Info信息获取进程相关信息
		System.out.println("进程命令:"+info.command());
		System.out.println("进程参数:"+info.arguments());
		System.out.println("进程启动时间:"+info.startInstant());
		System.out.println("进程累计运行时间:"+info.totalCpuDuration());
//		通过CompletableFuture在进程结束后运行某个任务
		CompletableFuture<ProcessHandle>cf = ph.onExit();
		cf.thenRunAsync(()->{
		System.out.println("程序退出");});
		Thread.sleep(5000);
	}
}

通过粗体字获取Process对象的ProcessHandle对象,接下来即可通过ProcessHandle对象来获取进程相关信息。

7.3 常用类

7.3.1 Object类

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

  • boolean equals( Object obj):判断指定对象该对象是否相等。此处相等的标准是,两个对象是 同一个对象,因此该 equals方法通常没有太大的实用价值。
  • protected void finalize():当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源。
  • Class<?>getClass():返回该对象的运行时类,该方法在本书第18章还有更详细的介绍。
  • int hashCode():返回该对象的 hashCode()值。在默认情况下, Object类的hashCode()方法根据该对象的地址来计算(即与 System.identityHashCode( Object x)方法的计算结果相同)。但很多类都重写了 Object 类的 hashCode()方法,不再根据地址计算其 hashCode()方法值。
  • String toString()返回该对象的字符串表示,当程序使用System.out.printlm()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的 toString()方法返回该对象的字符串表示。Object类的toString()方法返回”运行时类名@十六进制 hash Code值”格式的字符串,但很多类都重写了Object类的 toString()方法,用于返回可以表述该对象信息的字符串。

除此之外, Object类还提供了 wait()、 notify()、notifyAll()几个方法,通过这几个方法可以控制线程的暂停和运行。

Java还提供了一个 protected修饰的 clone()方法,该方法用于帮助其他对象来实现“自我克隆”,所谓“自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。由于Object类提供的clone()方法使用了protected修饰,因此该方法只能被子类重写或调用。

自定义类实现“克隆”的步骤如下:

​ ①自定义类实现 Cloneable接口。这是一个标记性的接口,实现该接口的对象可以实现“自我克隆”,接口里没有定义任何方法。

​ ②自定义类实现自己的 clone()方法。

​ ③实现 clone()方法时通过super.clone();调用 Object实现的clone()方法来得到该对象的副本,并返回该对象的副本。如下程序示范了如何实现“自我克隆”。

class Address
{
	String detail;
	public Address(String detail)
	{
		this.detail = detail;
	}
}
//实现Cloneable接口
class User implements Cloneable
{
	int age;
	Address address;
	public User(int age)
	{
		this.age = age;
		address = new Address("广州天河"); 
	}
	//通过调用supre.clone()来实现clone()方法
	public User clone()
			throws CloneNotSupportedException
	{
	return(User) super.clone();
	}
}
public class CloneTest 
{
	public static void main(String[] args) 
		throws CloneNotSupportedException
	{
		var u1 = new User(29);
//		clone得到u1对象的副本
		var u2 = u1.clone();
//		判断u1和u2是否相等
		System.out.println(u1 == u2);  //①
//		判断u1和u2的address是否相等
		System.out.println(u1.address = u2.address); //②
	}
}

上面程序让User类实现了Cloneable接口,而且实现clone()方法,因此User对象就可实现“自我克隆”——克隆出来的对象只是原有对象的副本。程序在①号粗体字代码处判断原有的User对象与克隆出来的User对象是否想到,程序返回false。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EbouxrSf-1618031098513)(C:\Program Files\Typora\image\de3267f363f41df5b666c846be80dfd.jpg)]

Object类提供的clone()方法不仅能简单地处理“复制”对象的问题,而且这之前“自我克隆”的机制十分高效。比如clone一个包含100个元素的int[]数组,用系统默认的clone方法比静态copy方法快近2倍。

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

7.3.2 操作对象的Object工具类

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

提示:Java为工具类的命名习惯是提供一个字母s,比如操作系统的工具类是Arrays,操作集合的工具类是Collections。

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.hash(obj));
//		输出一个null对象的toString,输出null
		System.out.println(Objects.toString(obj));
//		要求obj不能为null,如果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.requireNull(bar)
}

7.3.3 Java 9改进的String、StringBuffer和StringBuilder类

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

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

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

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

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

Java 9改进了字符串(包括 String、StringBulider,、StringBuilder) 的实现。在Java 9以前字符串采用 char[ ]数组来保存字符,因此字符串的每个字符占2字节;而Java 9及更新版本的JDK的字符串采用byte[ ] 数组再加一个 encoding-flag字段来保存字符,因此字符串的每个字符只占1字节。所以Java 9及更新版本的JDK的字符串更加节省空间,但字符串的功能方法没有受到任何影响。

char charAt(int index):获取字符串中指定位置的字符,其中index指的是字符串的序数,字符串的序数从0开始到length()-1。

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

int compareTo(String anotherString):比较两个字符串的大小。如果两个字符串的字符串序列相等,则返回0,;不相等时,从两个字符串的第0个字符开始比较,返回第一个不相等的字符差。另一种情况,较长的字符串的前面部分刚好是较短的字符串,返回长度差。

		var s1 = "abcdefghigklmnopq";
		var s2 = "abcdefghig";
		var s3 = "abcdefghigrstuvw";
		System.out.println("s1.compareTo(s2):"+s1.compareTo(s2)); //返回长度差
		System.out.println("s1.compareTo(s3):"+s1.compareTo(s3)); //返回'k'-'r'

​ boolean ends With(String suffix):返回String是否一suffix结尾。

		var s4 = "fkit.org";
		var s5 = ".org";
		System.out.println("s4.endsWith(s5):"+s4.endsWith(s5));

void getChar(int scrBegin,int scrEnd,char [ ] dst,int dstBegin):该方法将字符串中从scrBegin开始,到scrEnd结束的字符串复制到dst字符串数组中,其中dstBegin为目标字符数组的起始复制位置。

		char [] s6 = {'I',' ','l','o','v','e',' ','J','a','v','a'};
		var s7 = "ejb";
		s7.getChars(0, 3, s6, 7);  //s6 = I love ejba
		System.out.println(s6);

int indexOf(String str,int fromIndex):找出str子字符串在该字符串中从fromIndex开始后第一次出现的位置。

		var s8 = "www.fkit.org";
		var s9 = "it";
		System.out.println("s8.indexOf('r'):"+s8.indexOf('r'));
		System.out.println("s8.indexOf('r',2):"+s8.indexOf('r',2));
		System.out.println("s8.indexOf(s9):"+s8.indexOf(s9));

剩下的读者可以查阅API自己学习。

因为String是不可变的,多以会额外产生很多零食变量,使用StringBuffer或StringBulider就可以避免这个问题。

StringBulider提供了一系列的插入、追加、改变该字符串里包含的字符序列的方法。而StringBuffer与其用法完全相同,只是StringBuffer是线程安全的。

StringBulider、StringBuffer有两个属性:length和capaity,其中lengith,其中length属性表示其包含的字符序列的长度。与String对象的length对象的length不同的是,StringBulider、StringBuffer的length是可以改变的,可以通过length()、setLength(int len)方法来访问和修改其字符序列的长度。capaity属性表示StringBuilder类的用法。

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

上面程序中粗体字部分师范看StringBulider类的追加、插入、替换、删除等操作,这些操作改变了StringBulider里的字符序列,这就是StringBulider与String之间最大的区别:StringBulider的字符序列是可变的。从程序可看到StringBulider的Length()方法返回其字符序列的长度,而capacity()返回值则比length()返回值大。

7.3.4 Math类

Java提供了的基本的+、-、*、/、%等基本算术运算的运算符,但对于更复杂的数学运算,例如,三角函数、对数运算、指数运算等无能为力。Java提供了Math工具类来完成这些复杂的运算,Math类是一个工具类、它的构造器被定义成private的,因此无法创建Math类的对象:Math类中的所有方法都是类方法,可以直接通过类名来调用他们。Math类提供了大量静态方法之外,还提供了两个类变量:PI和E,指π和e。

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));
//		计算反正切,返回角度范围在-pi/2到pi/2之间
		System.out.println(Math.atan(2.3));
//		计算三角余弦
		System.out.println(Math.cos(1.57));
//		计算双曲余弦
		System.out.println(Math.cosh(1.2));
//		计算正弦
		System.out.println(Math.sin(1.57));
//		计算双曲正弦
		System.out.println(Math.sin(1.2));
//		计算三角正切
		System.out.println(Math.tan(0.8));
//		计算双曲正切
		System.out.println(Math.tanh(2.1));
//		将矩形坐标(x,y)转换成极坐标(r,thet)
		System.out.println(Math.atan2(0.1,0.2));
		/*----------下面是取整运算-----------*/
//		取整,返回小于目标数的最大整数
		System.out.println(Math.floor(-1.2));
//		取整,返回大于目标数的最小整数
		System.out.println(Math.ceil(1.2));
//		四舍五入取整
		System.out.println(Math.round(2.3));
		/*----------下面是乘方、开方、指数运算-----------*/
//		计算平方根
		System.out.println(Math.sqrt(2.3));
//		其余Math功能省略,读者可查阅Math API自行学习
	}
}

7.3.5 ThreadLocalRandom与Random

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

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

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

可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,也可以指定生成随机数的范围。下面关于Random类的用法:

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

public class RandomTest 
{
	public static void main(String[] args) 
	{
		var rand = new Random();
//		从这个随机数生成器的序列中返回下一个伪随机、均匀分布的布尔值。
//		nextBoolean的一般约定是,伪随机生成并返回一个布尔值。
//		值true和false产生的概率(大约)相等。
		System.out.println(rand.nextBoolean());
		var buffer = new byte[16];
//		生成随机字节并将它们放入用户提供的字节数组中。
//		产生的随机字节数等于字节数组的长度。
		rand.nextBytes(buffer);
		System.out.println(Arrays.toString(buffer));
//		生成0.0~1.0之间的伪随机double数
		System.out.println(rand.nextDouble());
//		生成0.0~1.0之间的伪随机float数
		System.out.println(rand.nextFloat());
//		生成平均值是0.0,标准差是1.0的伪高斯数
		System.out.println(rand.nextGaussian());
//		生成一个处于int整数取值范围的伪随机数
		System.out.println(rand.nextInt());
//		生成0~26之间的伪随机数
		System.out.println(rand.nextInt(26));
//		生成一个处于long整数须知范围的伪随机整数
		System.out.println(rand.nextLong());
	}
}

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());
		System.out.println(r1.nextDouble());
		System.out.println(r1.nextFloat());
		System.out.println(r1.nextGaussian());
		var r2 = new Random(50);
		System.out.println("第二个种子为50的Random对象");
		System.out.println(r2.nextBoolean());
		System.out.println(r2.nextDouble());
		System.out.println(r2.nextFloat());
		System.out.println(r2.nextGaussian());
		var r3 = new Random(100);
		System.out.println("种子为100的Random对象");
		System.out.println(r3.nextBoolean());
		System.out.println(r3.nextDouble());
		System.out.println(r3.nextFloat());
		System.out.println(r3.nextGaussian());
	}
}

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

为了避免两个Random对象产生相同的数字序列,通常建议使用当前时间作为Random对象的种子:

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

在多线程环境下使用ThreadLocalRandom的方式使用Random基本类似,

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

7.3.6 BigDecimal类

前面介绍double、fioat两种基本浮点类型时已经指出,这两个基本类型的浮点数容易丢失精度。

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("123.3/100 = "+(123.3/100));
		System.out.println("4.015*100 = " + (4.015*100));
	}
}

上面程序运行结果表明,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 BigDecimall(0.1)将创建 个 BigDecimal,它正好等于预期的0.1。因此通常建议优先使用基于 String的构造器。

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

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

import java.math.BigDecimal;

public class BigDecimalTest
{
	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("...............");
		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));
	}
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2hUOSCr-1618031098517)(C:\Program Files\Typora\image\image-20210328100130103.png)]

上面程序中f1和f3都是基于0.05创建的BigDecimal对象,其中f1是基于“0.05”字符串,但是f3是基于0.05的double浮点数。所以创建BigDecimal对象时,一定要使用String对象作为参数构造器,而不是直接使用double数字。

如果程序要求对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 d1,double d2)
	{
//		使用double . tostring (double)方法提供的规范的double字符串表示形式,将double转换为BigDecimal。
		var b1 = BigDecimal.valueOf(d1);
		var b2 = BigDecimal.valueOf(d2);
//		将此BigDecimal转换为double。
		return b1.add(b2).doubleValue();
	}
//	提供精确的减法运算
	public static double sub(double d3,double d4)
	{
		var b3 = BigDecimal.valueOf(d3);
		var b4 = BigDecimal.valueOf(d4);
		return b3.subtract(b4).doubleValue();
	}
//	提供精确的乘法运算
	public static double mul(double d1,double d2)
	{
		var b1 = BigDecimal.valueOf(d1);
		var b2 = BigDecimal.valueOf(d2);
//		将此BigDecimal转换为double。
		return b1.multiply(b2).doubleValue();
	}
//	提供(相对)精确的除法运算,当发生除不尽时
//	精确到小数点后10位数字后四舍五入
	public static double div(double d3,double d4)
	{
		var b3 = BigDecimal.valueOf(d3);
		var b4 = BigDecimal.valueOf(d4);
/*		四舍五入模式向“最近的邻居”舍入,除非两个邻居的距离相等,这种情况下取四舍五入。
		表现为舍入模式。
		discardedfraction≥0.5则为UP;
		否则,表现为舍入模式。down。
		注意,这是学校里常用的舍入模式。
		该模式对应于IEEE 754-2019 round - dingattribute roundTiesToAway。
*/
		return b3.divide(b4,DEF_DIV_SCALE,RoundingMode.HALF_UP).doubleValue();
	}
	public static void main(String[] args)
	{
		System.out.println(Arith.add(0.05, 0.01));
		System.out.println(Arith.sub(1.0, 0.42));
		System.out.println(Arith.mul(4.015, 100));
		System.out.println(Arith.div(123.4, 100));
	}
}

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

7.4 Java 8的日期、时间类

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

7.4.1Date类

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对象的时间。
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是一个设计相当糟糕的类,因此Java官方推荐尽量少用Date的构造器和方法。如果需要对日期、时间进行加减运算,或者获取指定时间的年、月、日、时、分、秒信息,可使用Calendar工具类。

7.4.2 Calendar类

因为Date类在设计上存在一些缺陷,所以Java提供了Calendar来更好的处理日期和时间。Calendar是一个抽象类,他用来表示日历。

全世界通常选择最普及、最通用的日历:Gregorian Calendar,也就是日常介绍年份时常用的“公元几几年” 。

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

当然,也可以创建自己的 Calendar子类,然后将它作为 Calendar对象使用(这就是多态)。因为篇幅关系,本章不会详细介绍如何扩展 Calendar子类,读者可通过互联网查看 Calendar各子类的源码来学习。

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

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

//创建一个默认的Calendar对象
var calendar = Calendar.getInstance();
//从Calendar中取出Date对象
var date = .getTime();
//通过Date对象获取对应的Calendar对象
//因为Calendar/GregorianCalendar没有构造函数可以接收Date对象
//所以必须创建一个Calendar实例,然后调用其setTime()方法。
var calendar2 = Calendar.getInstance();
calendar2.setTime(date);

Calender类提供了大量修改日期时间的方法,常用方如下:

  • void add(int field,int amount ): 根据日历的规则,为给定的日历字段添加或减去指定的时间量。
  • Int get(int field): 返回指定日历字段的值。
  • int getActualMaximum(int field):返回指定日历字段可能拥有的最大值。例如月,最大值为11
  • int getActualMinimum(int field):返回指定日历字段可能拥有的最小值。例如月,最小值为0
  • void roll(int field, Int amount):与add()方法类似,区别在于加上 amount后超过了该字段所能表示的最大范围时,也不会向上一个字段进位。
  • void set(int field, int value):将给定的日历字段设置为给定值 。
  • void set(int year, int month, int date):设置 Calendar对象的年、月、日三个字段的值。
  • void set(int year, int month, int date, int hourOfDay, int minute, Int second):设置 Calendar对象的年、 月、日、时 分、秒6个字段的值。

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

import java.util.Calendar;

public class CalendarTest
{
	private static final int YEAR = 0;
	private static final int MONTH = 0;
	private static final int DATE = 0;

	public static void main(String[] args)
	{
		var c = Calendar.getInstance();
//		取出年
		System.out.println(c.get(YEAR));//粗
		System.out.println(c.get(MONTH));//粗
		System.out.println(c.get(DATE));//粗
		c.set(2003,10,23,12,32,23);//粗//2003-11-23 12.32.23
		System.out.println(c.getTime());
		c.add(YEAR, -1);//粗//2002-11-23 12.32.23
		System.out.println(c.getTime());
		c.roll(MONTH, -8);
		System.out.println(c.getTime());
	}
}

上面程序中粗体字代码示范了Calendar类的用法,Calendar可以很灵活的改变它对应的日期。

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

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

1.add与roll的区别

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

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

  • 当被修改的字段超出它允许的范围的时候,会发生进位,即上一级字段也会增大:
var call = Calendar.getInstance();
call.set(2003,7,23,0,0,0);//2003-8-23
call.add(MONTH,6);//2004-2-23
  • 如果下一级字段也需要改变,那么该字段会修正到变化最小的值:
var cal2 = Calendar.getInstance();
cal2.set(1003,7,31,0,0,0);
//因为进位后月份改为2月,2月没有31日,自动变成29日
val2.add(MONTH,6);//2003-8-31 → 2004-2-29

对于上面的例子,8-31就会变成2-29.因为MOUTH的下一级字段是DATE,从31到29改变最小,所以上面2003-8-31的MOUTH字段增加6后,不是变成2004-3-2,而是变成2004-2-29。

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

var cal3 = Calendar.getInstance();
cal3.set(2003,7,23,0,0,0);
//MOUTH字段“进位”,但是YEAR并不增加
val3.roll(MOUTH,6);//2003-8-23 → 2003-2-23

下一级字段的处理规则用户add()相似;

var cal4 = Calendar.getInstance();
cal3.set(2003,7,31,0,0,0);
//MOUTH字段“进位”,但是YEAR并不增加
val3.roll(MOUTH,6);//2003-8-23 → 2003-2-28
2.设置Calendar的容错率

调用Calendar对象的set()方法来改变指定字符串的值时,有可能传入一个不合法的参数,例如为MOUTH字段设置13,这将会导致如下后果

import java.util.Calendar;

public class LenientTest
{
	private static final int MOUTH = 0;

	public static void main(String[] args)
	{
		Calendar ca1 = Calendar.getInstance();
//		如果是字段YEAR字段加一,MOUTH字段为1(2月)
		ca1.set(MOUTH,13);//①
		System.out.println(ca1.getTime());
//		关闭容错性
		ca1.setLenient(false);
//		导致运行时异常
		ca1.set(MOUTH, 13);//②
		System.out.println(ca1.getTime());
	}
}

①和②完全相似,但他们的结果不同:①处代码可以正常运行,因为设置MOUTH字段的值为13,将导致YEAR字段加一;②处代码将会导致运行时异常,因为设置的MOUTH字段的值超出了MOUTH字段允许的范围。关键在于程序粗体字的运行,Calendar设置了一个setLenient()用于设置它的容错率,Calendar默认支持好的容错性,通过setLenient(false)可以关闭它的容错率,让他进行严格的参数检查。

Calendar有两种日历模式:

​ lenient模式:每个字段可接受超出它允许范围的值

​ non-lenient模式时,如果某个时间字段设置超出了它允许的取值范围,程序将抛出异常。

3.set()方法延迟修改

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

import java.util.Calendar;

public class LazyTest
{
	public static void main(String[] args)
	{
		Calendar cal = Calendar.getInstance();
		cal.set(2003,7,31);
//		将月份设置为9月,但是9月没有31日
//		如果立即修改,系统将会把cal自动调整到10月1日
		cal.set(Calendar.MONTH, 8);
//		System.out.println(cal.getTime());//①
//		设置DATE字段为5
		cal.set(Calendar.DATE,5);//②
		System.out.println(cal.getTime());//③
	}
}

程序将①注释了,因为Calendar的set()方法具有延迟修改的特性,即调用set()方法后Calendar实际上并未计算真实的日期,他只是使用内部成员变量表记录MOUTH字段被修改为8,接着程序设置DATE字段值为5,程序内部在此记录DATE——就是9月5日,因此③输出2003-9-5。

7.4.3新的日期、时间包

Jva8专门新增了一个java.time包,该包下包含了如下常用的类。

  • Clock:该类用于获取指定时区的当前日期、时间。该类可取代 System类的currentTimeMillis() 方法,而且提供了更多方法来获取当前日期、时间。该类提供了大量静态方法来获取 Clock对象。

  • Duration:该类代表持续时间。该类可以非常方便地获取一段时间。

  • Instant:代表一个具体的时刻,可以精确到纳秒。该类供了静态的now()方法来获取当前时刻, 也提供了静态的 now(Clock clock)方法来获取 clock对应的时刻。除此之外,它还提供了一系列 minusXXX() 方法在当前时刻基础上减去一段时间,也提供了 plusXxx()方法在当前时刻基础上加上一段时间。

  • LocalDate:该类代表不带时区的日期,例如2007-12-03。该类提供了静态的now方法来获取 当前日期,也提供了静态的now( Clock cloc)方法来获取 clock对应的日期。除此之外,它还提 供了 minusXxx()方法在当前年份基础上减去几年、几月、几周或几日等,也提供了 plusXxx() 方法在当前年份基础上加上几年、几月、几周或几日等

  • LocalTime:该类代表不带时区的时间,例如10:15:30。该类提供了静态的now方法来获取当 前时间,也提供了静态的now( (Clock clock)方法来获取 clock对应的时间。除此之外,它还提供 了 minus Xxx()方法在当前年份基础上减去几小时、几分、几秒等,也提供了 plusXxx()方法在当 前年份基础上加上几小时、几分、几秒等。

  • LocaIDate Time:该类代表不带时区的日期、时间,例如2007-12-07T10:15:30。该类提供了静态的now方法来获取当前日期、时间,也提供了静态的now( Clock clock)方法来获取 clock对应的日期、时间。除此之外,它还提供了 minusXxx()方法在当前年份基础上减去几年、几月、几 日、几小时、几分、几秒等,也提供了 plusXxx()方法在当前年份基础上加上几年、几月、几日、 几小时、几分、几秒等。

  • MonthDay:该类仅代表月日,例如-04-12。该类提供了静态的now方法来获取当前月日,也提供了静态的 now(Clock clock)方法来获取 clock对应的月日

  • Year:该类仅代表年,例如2014。该类提供了静态的now方法来获取当前年份,也提供了静态的now (Clock clock)方法来获取 clock对应的年份。除此之外,它还提供了 minusYears()方法 在当前年份基础上减去几年,也提供了 plusYears()方法在当前年份基础上加上几年。

  • Year Month:该类仅代表年月,例如2014-04。该类提供了静态的now方法来获取当前年月, 也提供了静态的 now(Clock clock)方法来获取clok对应的年月。除此之外,它还提供了minusXxx()方法在当前年月基础上减去几年、几月,也提供了 plusXxx()方法在当前年月基础上 加上几年、几月。

  • ZonedDateTime:该类代表一个时区化的日期、时间。

  • Zoneld:该类代表一个时区。

  • DayOfWeek:这是一个枚举类,定义了周日到周六的枚举值

  • Month:这也是一个枚举类,定义了一月到十二月的枚举值。

下面通过一个简单的程序来示范这些类的用法。

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.MonthDay;
import java.time.Year;

public class NewDatePackageTest
{
	public static void main(String[] args)
	{
//		-------下面关于Clock的用法---------
//		获取当前的Clock
		var clock = Clock.systemUTC();
//		通过Clock获取当前时刻
		System.out.println("当前时刻为"+clock.instant());
//		获取Clock对应的毫秒数,与System.currentTimeMills()输出相同
		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
//		offset:获取一个时钟,从添加了指定持续时间的指定时钟返回瞬时值
		var  clock2 = Clock.offset(clock, d);
//		可以看出clock与clock2相差的时间
		System.out.println("当前时刻加6000秒为:"+clock2.instant());
//		-------下面关于Instant的用法---------
//		获取当前时间
		var instant = Instant.now();
		System.out.println(instant);
		var instant2 = instant.plusSeconds(6000);
		System.out.println(instant2);
//		根据字符串解析instant对象
		var instant3 = Instant.parse("2014-02-03T20:18:09Z");
		System.out.println(instant3);
		var instant4 = instant3.plus(Duration.ofHours(1).plusMinutes(4));
		System.out.println(instant4);
//		获取Instant4 5天前的时刻
		var instant5 = instant4.minus(Duration.ofDays(5));
//		-------下面关于LoaclDate的用法---------
		var localdate = LocalDate.now();
		System.out.println(localdate);
//		获取2021年的第219天
		localdate = LocalDate.ofYearDay(2021, 219);
		System.out.println(localdate);
//		设置为2021年5月21日
		localdate = LocalDate.of(2021, Month.MAY, 21);
		System.out.println(localdate);
//		-------下面关于LocalTime的用法---------
		var localtime = LocalTime.now();
		System.out.println(localtime)
		localtime = LocalTime.of(22, 01);
		System.out.println(localtime);
//		返回一天中的5503秒
		localtime = LocalTime.ofSecondOfDay(5503);
		System.out.println(localtime);
//		-------下面关于LocalDateTime的用法---------
		var localdatetime = LocalDateTime.now();
		System.out.println(localdatetime);
		var future = localdatetime.plusHours(25).plusMinutes(3);
		System.out.println(future);
//		-------下面关于Year,YearMonth,MonthDay的用法---------
		var year = Year.now();
		System.out.println(year);
		var futureyear = year.plusYears(5);
		System.out.println(futureyear);
//		根据指定月份获取YearMonth
		var ym = year.atMonth(10);
		System.out.println(ym);
		ym = ym.plusYears(5).minusYears(3);
		System.out.println(ym);
		var md = MonthDay.now();
		System.out.println(md);
		var md2 = MonthDay.of(5, 22);
		System.out.println(md2);
	}
}

该程序就是这些常用类的用法示例,这些API和他们的方法都非常简单。

7.5 正则表达式

正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作。String类里也提供了几个特殊的方法:

  • boolean matches(String regex):判断该字符串是否匹配指定的正则表达式。
  • String replaceAll(String regex,String replacement):将该字符串中所匹配regex的子串替换成replacement。
  • String replaceFirst(String regex,String replacement):将该字符串中第一个匹配regex的子串替换成replacement。
  • String[ ] split(String regex):以regex作为分隔符,把该字符串分割成多个子串。

Java还提供Pattern和Matcher两个类专门用于提供正则表达式支持。

Matcher类:一种引擎,通过解释模式对字符序列执行匹配操作。

匹配器是通过调用模式的匹配器方法来创建的。一旦创建,匹配器可用于执行三种不同的匹配操作:

•matcher():方法尝试将整个输入序列与模式匹配。

•lookingAt():方法尝试匹配输入序列,从开头开始,与模式匹配。

•find():方法扫描输入序列,寻找下一个匹配模式的子序列。

每个方法都返回一个指示成功或失败的布尔值。关于成功匹配的更多信息可以通过查询匹配器的状态来获得。

匹配器在称为region的输入子集中查找匹配项。默认情况下,该区域包含匹配器的所有输入。可以通过region方法修改region,通过regionStart和regionEnd方法查询region。区域边界与某些模式结构交互的方式可以改变。

这个类还定义了用新字符串替换匹配的子序列的方法,如果需要,可以从matchresult计算新字符串的内容。
可以同时使用appendReplacement和appendTail方法,以便将结果收集到现有的字符串缓冲区或字符串生成器中。
或者,可以使用更方便的replaceAll方法创建一个字符串,其中输入序列中的每个匹配子序列都被替换。

7.5.1 创建正则表达式

正则表达式所支持的合法字符

字符解释
x字符x(x可代表任何合法字符)
\0mnn八进制数0mnn所表示的字符
\xhh十六进制xhh所表示的字符
\uhhhh十六进制uhhhh所表示的字符
\t制表符
\n换行符
\r回车符
\f换页符
\a报警符
\eEscape符
\cxx对应的控制符。例如,\cM匹配ctrl+M。x必须为A~Z或a ~ z之一。

正则表达式中的特殊字符

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

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

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

注意:可能有读者觉得第一个正则表达式中怎么有那么多反斜杠啊?这是由于Java字符串中反斜杠本身需要转义,因此两个反斜杠(\\)实际上相当于一个(前一个用于转义)。

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

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

提示:上面的7个预定义字符其实很容易记忆,d是dit的意思,代表数字;s是 space 的意思,代表空白;w是word的意思,代表单词。d、s、w的大写形式恰好匹配与之相反的字符。
有了上面的预定义字符后,接下来就可以创建更强大的正则表达式了。例如:

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

在一些特殊情况下,例如,若只想匹配a~f的母,或者匹配除ab之外的所有小写字母,或者匹配中文字符,上面这些预定义字符就无能为力了,此时就需要使用方括号表达式,方括号表达式有如下表所示的几种形式。

方括号表达式

方括号表达式说明
表示枚举例如[abc],表示a、b、c其中任意一个字符;[gz],表示g,z其中任意一个字符
表示范 围:-例如[a-f],表示a~f范围内的任意字符;[\\u0041-\\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]]范围内的所有字符,除m~p范围之外的字符,即[a-lq-z]
表示“并”运算并运算与前面的枚举类似。例如[a-d[m-p]],表示[a-dm-p]即a~d,m ~p。

提示:方括号表达式比前面的预定义字符灵活多了,几乎可以匹配任何字符。例如,若需要 匹配所有的中文字符,就可以利用u0041-056形式因为所有中文字符的 Unicode 值是连续的,只要找出所有中文字符中最小、最大的 Unicode值,就可以利用上面形式来 匹配所有的中文字符。

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

除此之外,Java正则表达式还支持如下表的几个边界匹配符。

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

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

  • 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 ,world!";
    //贪婪模式的正则表达式
    System.out.println(str.replaceFirst("\\w*","    "));//输出    ,world!
    //勉强模式的正则表达式
    System.out.println(str.replaceFirst("\\w*?","    "));//输出  helle,world!
    

    当从“Hello,world!”字符串中查找匹配"//w*"子串时,因为“//w *”使用了贪婪模式,数量表示符( *)会一直匹配下去,所以该字符串前面的所有的单词字符都会被匹配到,直到遇到空格;如果使用勉强模式,数量表示符 ( *)会尽量匹配最少字符,即匹配0个字符。

    7.5.2 使用正则表达式

    **正则表达式字符串必须先被编译为Pattern对象,然后利用该Pattern对象创建对应的Matcher对象。**执行匹配所涉及的状态保留在Matcher对象中,多个Matcher可共享一个Pattern对象。

    //将一个字符串编译成Pattern对象
    Pattern p = Pattern.compile("a*b");
    //使用Pattern对象创建Matcher()对象
    //matcher()自动把指定字符串编译成匿名的Pattern对象,并执行匹配
    Matcher m = p.matcher("aaaab");
    /*matches():试图将整个区域与模式进行匹配。
    如果匹配成功,则可以通过start、end和group方法获得更多信息。
    当且仅当整个区域序列匹配此匹配器的模式时为真*/
    boolean b = m.matches(); //返回true
    

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

    boolean b = Pattern.matcher("a*b","aaaab");
    

    这句等效于以上三局,但采用这种语句都需要重新编译新的Pattern对象,不能重复利用以编译的Pattern对象,所以效率不高。

    Matcher类中的几种方法:

    • find():返回目标字符串中是否包含Pattern匹配的子串。
    • group():返回上一次与Pattern匹配的子串。
    • start():返回上一次与Pattern匹配的子串在目标字符串中的开始位置。
    • end():返回上一次与Pattern匹配的子串在目标字符串中的结束位置加1。
    • lookungAt():返回目标字符串前面部分与Pattern是否匹配。
    • matches():返回整个目标字符串与Pattern是否匹配。
    • reset():将现有的Matcher对象应用于一个新的字符串序列。

    使用Maycher类的find()和group()方法可以从目标字符串中依次取出特定子串(匹配这则表达式的子串),例如互联网的网络爬虫。他们可以自动从网页上识别出所有的电话号码。

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

public class FindGroup
{
	public static void main(String[] args)
	{
//		使用字符串模拟从网络上得到网页的源码
		var str = "我想求购一本《疯狂Java讲义》,尽快联系我15333634564"
				+"交朋友,电话号码是17333636698"
				+"美女的微信是13165489874"
				+"出售二手电脑,联系方式15633644562";
//		创建一个Pattern对象,并用它建立一个Matcher对象
//		该正则表达式要抓取17x和15x段的手机号
//		实际要抓取哪些代码只需要修改正则表达式即可
//		compile:将给定的正则表达式编译成模式。
//		matcher():创建一个匹配器,它将根据这个模式匹配给定的输入。
		Matcher m = Pattern.compile("((13\\d)|(15\\d))\\d{8}").matcher(str);
//		将所有符合正则表达式的子串全部输出
//		find():尝试查找与模式匹配的输入序列的下一个子序列。
		while(m.find())
		{
//			group():返回与前一个匹配项匹配的输入子序列。
			System.out.println(m.group());
		}
	}
}

find()方法依次查找字符串中与Pattern匹配的子串,一旦找到对应的子串,下次调用find()方法时将接着向下查找。

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());
		}
	}
}

上面程序使用find()、group()方法逐项取出字符串与指定正则表达式匹配的子串,并使用start()、end()方法返回子串在目标字符串中的位置。

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

//import java.util.regex.Pattern;
import java.util.regex.*;

public class MatchesTest
{
	public static void main(String[] args)
	{
		String[] mails = 
			{
					"kidhfkd@163.com",
					"kidhfkd@gmail.com",
					"sjndbbcdv.org",
					"whfioavl.xx"
			};
		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
			{
//				reset:用一个新的输入序列重置这个匹配器。
				matcher.reset(mail);
			}
			String result = mail +(matcher.matches()?"是":"不是")
					+"一个有效的邮件地址";
			System.out.println(result);
		}
	}
}

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

对目标字符串进行分割、查找、替换等操作:

import java.util.regex.Pattern;
import java.util.regex.*;

public class ReplaceTest
{
	public static void main(String[] args)
	{
		String[] msgs =
		{ "Java hsa 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]);
			}
//			String replaceAll():将该字符串中所有匹配regex的子串退换成replacement
			System.out.println(matcher.replaceAll("哈哈:)"));
		}
	}
}

上面程序使用了Matcher类提供的replaceAll()把字符串中所有与正则表达式匹配的子串替换成 哈哈:)“,实际上,Mathcer类还提供了一个replaceFirst(),把方法只替换第一个匹配的子串,运行上面程序,会看到字符串中所有以”re“开头的单词都会替换成 哈哈:)”。

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

import java.util.Arrays;

public class StringReg
{
	public static void main(String[] args)
	{
		String[] mags =
		{ "Java hsa regular expressions in 1.4", "regular expressions now expressing in Java",
				"Java represses oracular expressions" };
		for(var mag : mags)
		{
			System.out.println(mag.replaceFirst("re\\w*", "哈哈"));
			System.out.println(Arrays.toString(mag.split(" ")));
		}
	}
}

上面程序只使用String类的replaceFirst()和split()方法对目标对目标字符串进行了一次替换和分割。

7.6 变量处理和方法处理

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

7.6.1 Java 9 增强的MethodHandle

这种方法引用是一种轻量级的引用方式,他不会检查方法的访问权限,也不会管方法所属的类、实例方法或静态方法,MethodHandle就是简单代表特定的方法,并可通过MethodHandle来调用方法。

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.regex.*;

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,不带形参的方法类型。
		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",
//						指定获取返回值为String、形参为String的方法类型
						MethodType.methodType(String.class,String.class));
//		通过MethodType执行方法,传入主调对象和参数
		System.out.println(mtd2.invoke(new MethodHandleTest(),"孙悟空"));
	}
}

从上面三行粗体字代码中可以看出,程序使用MethodHandles.lookup对象根据类、方法名,方法类型来获取MethodHandle对象。由于此处的方法名只是一个字符串,而该字符串可以来自变量、配置文件等,这意味着MethodHandle可以让Java动态的调用某个方法。

7.6.2 Java 9 新增的varHandle

用于动态的操作数组或者对象的成员变量。

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

class User7
{
	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);//粗体·
//		比较并设置:2表示第三个元素,如果第三个元素时Go,则被设置为Lua
		var r = avh.compareAndSet(sa,2,"Go","Lua");//粗体·
		System.out.println(r);
		System.out.println(Arrays.toString(sa));
//		获取并设置:
		System.out.println(avh.getAndSet(sa,2,"Swift"));
		System.out.println(Arrays.toString(sa));
		
//		用findVarHandle方法获取uesr7类中名为name
//		类型为String的实例变量
		var vh1 = MethodHandles.lookup().findVarHandle(User7.class, 
				"name",String.class);  //粗体·
		var user = new User7();
//		通过varHandle获取实例变量的值,需要传入对象作为调用者
		System.out.println(vh1.get(user));//粗体·
//		通过varHandle获取实例变量的值,需要传入对象作为调用者
		vh1.set(user,"孙悟空");//粗体·
//		输出user的name实例的值
		System.out.println(user.name);
//		用findVarHandle方法获取User类中名为MAX_AGE
//		类型为Integer的类变量
		var vh2 = MethodHandles.lookup().findStaticVarHandle(User7.class, 
				"MAX_AGE", int.class);
		System.out.println(vh2.get());
		vh2.set(88);
		System.out.println(User7.MAX_AGE);
	}
}

粗可以看出,程序调用MethodHandle类的静态方法可获取操作数组的VarHandle对象,接下来程序可以通过VarHandle对象来操作数组的方法,包括比较并设置数组元素、获取并这只数组元素等,VarHandle具体支持哪些方法则可参考API文档。

后三粗示范了使用VarHandle操作实例变量的情景,由于实例变量需要使用对象来访问,因此使用VarHandle操作实例变量时需要传入一个User对象。

操作类变量和实例变量差别不大,区别只是类变量不需要对象,而实例变量需要对象,因此VarHandle操作类变量是无须传入对象作为参数。

当程序通过MethodHandles.Lookup来获取成员变量时,可根据字符串名称来获取成员变量,这个字符串名称同样是可以动态改变的。

7.7 Java 11改进的国际化与格式化

国际化(Internationaliaztion)简称I18N。

7.7.2 Java支持的国家或语言

调用Locale类的getAvailableLocales()方法,该方法返回一个Locale数组,该数组中包含了Java所支持的国家和语言。

import java.util.Locale;

public class LocaleList
{
	public static void main(String[] args)
	{
//		返回Java所支持的全部国家和语言的数组
		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());
		}
	}
}

7.7.3 完成程序国际化

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

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

myMess_en_US.properties

msg = Hello,{0}!Today is {1}.
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变量
		Locale currentLocale = null;
//		运行程序指定了两个参数
		if(args.length == 2)
		{
//			使用运行时的两个参数构造Locale实例
			currentLocale = new Locale(args[0],args[1]);
		}
		else
		{
//			否则直接用系统默认的Locale
			currentLocale = Locale.getDefault(Locale.Category.FORMAT);
		}
//		根据Locale加载语言资源
		var bundle = ResourceBundle.getBundle("myMess",currentLocale);
//		取得已加载中的资源文件中的msg对应消息
		var msg = bundle.getString("msg");
//		使用MessageFormat为带占位符的字符串传入参数
		System.out.println(MessageFormat.format(msg, "Mr.zhang",new Date()));
	}
}

对于占位符字符串,只需要使用MessageFormat类的format()方法为消息中的占位符指定参数即可。

7.7.5 使用类文件代替资源文件

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

使用类文件来代替资源文件必须满足如下条件。

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

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

上面文件是一个简体中文语言环境的资源文件,该文件可以代替myMess_zh_CN.properties 文件;如果需要代替美国英语语言环境的资源文件,则还应该提供一个myMess_en_US 类。

如果系统同时存在资源文件、类文件,系统将以类文件为主,而不会调用资源文件。对于简体中文的 Locale,ResourceBundle 搜索资源文件的顺序是:
(1)baseName_zh_CN.class
(2)baseName_zh_CN.properties(3)baseName_zh.class
(4)baseName_zh.properties
(5)baseName.class

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

7.7.6 Java 9新增的日志API

7.7.7 使用NumberFormat格式化数字

MessageFormat是抽象类Format的子类,Format抽象类还有两个子类:NumberFormat和DateFormat,两个子类中提供了两个方法:format()和parse()方法,format()用于将数值、日期转换为字符串,parse()用于将字符串装换为数值和日期。

import java.text.NumberFormat;
import java.util.Locale;

public class NumberFormatTest
{
	public static void main(String[] args)
	{
		var bd = 11230000.76;
		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(bd));
			System.out.println("百分比数值格式:"
					+nf[i*3+1].format(bd));
			System.out.println("货币数值格式:"
					+nf[i*3+2].format(bd));
		}
	}
}

7.7.8 使用DateFormat格式化日期、时间

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;

public class DateFormatTest
{
	public static void main(String[] args) throws ParseException
	{
//		需要格式化的时间
		var dt = new Date();
		Locale[] locales = {Locale.CHINA,Locale.US};
		var df = new DateFormat[16];
//		为上面两个Locale创建16个DateFormat对象
		for(var i = 0;i<locales.length;i++)
		{
			df[i*8] = DateFormat.getDateInstance(DateFormat.SHORT,locales[i]);
			df[i*8+1] = DateFormat.getDateInstance(DateFormat.MEDIUM,locales[i]);
			df[i*8+2] = DateFormat.getDateInstance(DateFormat.LONG,locales[i]);
			df[i*8+3] = DateFormat.getDateInstance(DateFormat.FULL,locales[i]);
			df[i*8+4] = DateFormat.getTimeInstance(DateFormat.SHORT,locales[i]);
			df[i*8+5] = DateFormat.getTimeInstance(DateFormat.MEDIUM,locales[i]);
			df[i*8+6] = DateFormat.getTimeInstance(DateFormat.LONG,locales[i]);
			df[i*8+7] = DateFormat.getTimeInstance(DateFormat.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("通用的SHORT格式:"
					+df[i*8+5].format(dt));
			System.out.println("通用的SHORT格式:"
					+df[i*8+6].format(dt));
			System.out.println("通用的SHORT格式:"
					+df[i*8+7].format(dt));
		}
		System.out.println("---------------下面为测试是否采用严格语法---------------");
		var str1 = "2021/2/31";
		var str2 = "2021年2月31日";
		System.out.println(DateFormat.getDateInstance().parse(str2));
		System.out.println(DateFormat.getDateInstance(DateFormat.SHORT).parse(str1));
        //引发异常,因为str1是一个SHORT字符串,必须使用SHORT样式的DateFormat实例解析
		System.out.println(DateFormat.getDateInstance().parse(str1));
	}
}

7.7.9 使用SimpleDateFormat格式化日期

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

SimpleDateFormat是DateFormat的子类,正如它的名字所暗示的,他是简单的日期格式器。很多读者对“简单的”格式器不屑一顾,实际上SimpleDateFormat 比DateFormat更简单,功能更强大。

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();
		var sdf1 = new SimpleDateFormat("Gyyyy年中的第D天");
//		将D格式化为日期
		var dateStr = sdf1.format(d);
		System.out.println(dateStr);
//		一个非常特殊的日期字符串
		var str = "14####3月##21";
		var sdf2 = new SimpleDateFormat("y####MMM##d");
//		将日期字符串解析成日期,输出:Fri Mar 21 00:00..........
		System.out.println(sdf2.parse(str));
	}
}

这样的字符串解析成日期,功能非常强大。格式化怎么样的字符串完全取决于创建该对象是指定的pattern参数,oattern是一个使用日期字段占位符的日期模板。

7.8 Java 8新增的日期、时间格式器

DateTimeFormatter类相当与前面介绍的DateFormat和SimpleDateFormatter的合体,功能十分强大。

使用DateTimeFormatter进行格式化或解析,必须进行格式化或解析,必须先获取DateTimeFormatter对象。

7.8.1 使用DateTimeFormatter完成格式化

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格式器
						DateTimeFormatter.ISO_LOCAL_DATE,
						DateTimeFormatter.ISO_LOCAL_TIME,
						DateTimeFormatter.ISO_LOCAL_DATE_TIME,
//						使用本地化的不同风格来创建DateTimeFormatter格式器
						DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM),
						DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG),
//						根据模式字符串来创建DateFormatter格式化
						DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
				};
		var date = LocalDateTime.now();
//		依次使用不同的格式器对LocalDateTime进行格式化
		for(var i = 0;i < formatters.length;i++)
		{
			System.out.println(date.format(formatters[i]));//粗
			System.out.println(formatters[i].format(date));//粗
		}
	}
}

粗从不同方式来格式化日期。

7.8.2 使用DateTimeFoematter解析字符串

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class NewFormatterParse
{
	public static void main(String[] args)
	{
//		定义一个任意格式的日期、时间字符串
		var str1 = "2012==04==12 01时07分09秒";
//		根据需要解析的日期、时间字符串定义解析所有的格式器
		var formatter1 = DateTimeFormatter.ofPattern("yyyy==MM==dd HH时jj分pp秒");
//		执行解析
		var dt1 = LocalDateTime.parse(str1,formatter1);
		System.out.println(dt1);
//省略第二个
		
	}
}

小结:

本章介绍了运行Java程序时的参数,并详细解释了main方法签名的含义。为了实现字符界面程序与晕乎交互功能,介绍了两种读取键盘输入的方法(hasNext(),next()),还介绍了System、Runtime、String、StringBuffer、StringBuilder、Math、BigDecimal、Random、Date、Calendar和TimeZone等常用类的用法。

重点介绍了正则表达式,以及使用Pattern、Matcher、String等类来使用正则。还介绍了程序国际化等,还介绍了新增的日期、时间包,以及新增的日期时间格式符。

SimpleDateFormatter的合体,功能十分强大。

使用DateTimeFormatter进行格式化或解析,必须进行格式化或解析,必须先获取DateTimeFormatter对象。

7.8.1 使用DateTimeFormatter完成格式化

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格式器
						DateTimeFormatter.ISO_LOCAL_DATE,
						DateTimeFormatter.ISO_LOCAL_TIME,
						DateTimeFormatter.ISO_LOCAL_DATE_TIME,
//						使用本地化的不同风格来创建DateTimeFormatter格式器
						DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL,FormatStyle.MEDIUM),
						DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG),
//						根据模式字符串来创建DateFormatter格式化
						DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
				};
		var date = LocalDateTime.now();
//		依次使用不同的格式器对LocalDateTime进行格式化
		for(var i = 0;i < formatters.length;i++)
		{
			System.out.println(date.format(formatters[i]));//粗
			System.out.println(formatters[i].format(date));//粗
		}
	}
}

粗从不同方式来格式化日期。

7.8.2 使用DateTimeFoematter解析字符串

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class NewFormatterParse
{
	public static void main(String[] args)
	{
//		定义一个任意格式的日期、时间字符串
		var str1 = "2012==04==12 01时07分09秒";
//		根据需要解析的日期、时间字符串定义解析所有的格式器
		var formatter1 = DateTimeFormatter.ofPattern("yyyy==MM==dd HH时jj分pp秒");
//		执行解析
		var dt1 = LocalDateTime.parse(str1,formatter1);
		System.out.println(dt1);
//省略第二个
		
	}
}

小结:

本章介绍了运行Java程序时的参数,并详细解释了main方法签名的含义。为了实现字符界面程序与晕乎交互功能,介绍了两种读取键盘输入的方法(hasNext(),next()),还介绍了System、Runtime、String、StringBuffer、StringBuilder、Math、BigDecimal、Random、Date、Calendar和TimeZone等常用类的用法。

重点介绍了正则表达式,以及使用Pattern、Matcher、String等类来使用正则。还介绍了程序国际化等,还介绍了新增的日期、时间包,以及新增的日期时间格式符。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值