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可代表任何合法的字符) |


