持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情
1 与用户互动
1.1 运行Java程序的参数
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 (String arg : args)
{
System.out.println(arg);
}
}
}
因为计算机是没有思考能力的,它只能忠实地执行用户交给它的任务,既然我们没有给args数组设定参数值,那么JVM就不知道args数组的元素,所以JVM将args数组设置成一个长度为0的数组。
改为如下命令来运行上面程序:
java ArgsTest Java Spring
将看到如图所示的运行结果。
如果运行Java程序时在类名后紧跟一个或多个字符串(多个字符串之间以空格隔开),JVM就会把这些字符串依次赋给args数组元素。运行Java程序时的参数与args数组之间的对应关系如下图所示。
1.2 使用Scanner获取键盘输入
使用Scanner类可以很方便地获取用户的键盘输入,Scanner是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本类型值和字符串值。Scanner类提供了多个构造器,不同的构造器可以接收文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。
Scanner主要提供了两个方法来扫描输入。
- hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果需要判断是否包含下一个字符串,则可以省略Xxx。
- nextXxx():获取下一个输入项。Xxx的含义与前一个方法中的Xxx相同。在默认情况下,Scanner使用空白(包括空格、Tab空白、回车)作为多个输入项之间的分隔符。下面程序使用Scanner来获得用户的键盘输入。
在默认情况下,Scanner使用空白(包括空格、Tab空白、回车)作为多个输入项之间的分隔符。
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的分隔符设置为回车符,不再使用默认的空白作为分隔符。
Scanner的读取操作可能被阻塞(当前执行顺序流暂停)来等待信息的输入。如果输入源没有结束, Scanner又读不到更多输入项时(尤其在键盘输入时比较常见),Scanner的hasNext()和next()方法都有可能阻塞,hasNext()方法是否阻塞与和其相关的next()方法是否阻塞无关。
Scanner提供了两个简单的方法来逐行读取。
- boolean hasNextLine():返回输入源中是否还有下一行。
- String nextLine():返回输入源中下一行的字符串。
Scanner不仅能读取用户的键盘输入,还可以读取文件输入。只要在创建Scanner对象时传入一个File对象作为参数,就可以让Scanner读取该文件的内容。
public class ScannerFileTest
{
public static void main(String[] args)
throws Exception
{
//将一个File对象作为Scanner的构造器参数,Scanner读取文件内容
Scanner sc=new Scanner(new File("ScannerFileTest.java"));
System.out.println("ScannerFileTest.java文件内容如下:");
//判断是否还有下一行
while(sc.hasNextLine())
{
//输出文件中的下一行
System.out.println(sc.nextLine());
}
}
}
1.3 使用BufferedReader获取键盘输入
BufferedReader是Java IO流中的一个字符、包装流,它必须建立在另一个字符流的基础之上。但标准输入:System.in是字节流,程序需要使用转换流InputStreamReader将其包装成字符流。所以程序中用于获取键盘输入的BufferedReader对象采用如下代码创建:
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
使用BufferedReader可以逐行读取用户的键盘输入,用户的每次键盘输入都被BufferedReader当成String对象。与Scanner不同的是,BufferedReader不能读取基本类型输入项,它总是读取String对象。
2 系统相关
Java程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java提供了System类和Runtime类来与程序的运行平台进行交互
2.1 System类
System类代表当前Java程序的运行平台,程序不能创建System类的对象,System类提供了一些类Field和类方法,允许直接通过System类来调用这些Field和方法。System类提供了代表标准输入、标准输出和错误输出的类Field,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库的方法。
System类还提供了一个identityHashCode(Object x) 方法,该方法返回指定对象的精确hashCode值,也就是根据该对象的地址计算得到的hashCode值。当某个类的hashCode() 方法被重写后,该类实例的hashCode()方法就不能唯一地标识该对象;但通过identityHashCode()方法返回的hashCode值,依然是根据该对象的地址计算得到的hashCode值。所以,如果两个对象的identityHashCode值相同,则两个对象绝对是同一个对象。
public class IdentityHashCodeTest
{
public static void main(String[] args)
{
//下面程序中s1和s2是两个不同的对象
String s1=new String("Hello");
String s2=new String("Hello");
//String重写了hashCode()方法——改为根据字符序列计算hashCode值
//因为s1和s2的字符序列相同,所以它们的hashCode()方法返回值相同
System.out.println(s1.hashCode()
+ "----" + s2.hashCode());
//s1和s2是不同的字符串对象,所以它们的identityHashCode值不同
System.out.println(System.identityHashCode(s1)
+ "----" + System.identityHashCode(s2));
String s3="Java";
String s4="Java";
//s3和s4是相同的字符串对象,所以它们的identityHashCode值相同
System.out.println(System.identityHashCode(s3)
+ "----" + System.identityHashCode(s4));
}
}
2.2 Runtime类
Runtime类代表Java程序的运行时环境,每个Java程序都有一个与之对应的Runtime实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime实例,但可以通过getRuntime()方法获取与之关联的Runtime对象。
与System类似的是,Runtime类也提供了gc()方法和runFinalization()方法来通知系统进行垃圾回收、清理系统资源,并提供了load(String filename)和loadLibrary(String libname)方法来加载文件和动态链接库。
Runtime类代表Java程序的运行时环境,可以访问JVM的相关信息,如处理器数量、内存信息等。如下程序所示。
public class RuntimeTest
{
public static void main(String[] args)
{
//获取Java程序关联的运行时对象
Runtime rt=Runtime.getRuntime();
System.out.println("处理器数量:"
+ rt.availableProcessors());
System.out.println("空闲内存数:"
+ rt.freeMemory());
System.out.println("总内存数:"
+ rt.totalMemory());
System.out.println("可用最大内存数:"
+ rt.maxMemory());
}
}
3 常用类
3.1 Object类
Object类是所有类、数组、枚举类的父类,也就是说,Java允许把任何类型的对象赋给Object类型的变量。当定义一个类时没有使用extends关键字为它显式指定父类,则该类默认继承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.println()方法输出一个对象,或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。Object类的toString()方法返回“运行时类名@十六进制hashCode值”格式的字符串,但很多类都重写了Object类的toString()方法,用于返回可以表述该对象信息的字符串。
除此之外,Object类还提供了wait()、notify()、notifyAll()几个方法,通过这几个方法可以控制线程的暂停和运行。
Java还提供了一个protected修饰的clone()方法,该方法用于帮助其他对象来实现“自我克隆”,所谓“自我克隆”就是得到一个当前对象的副本,而且二者之间完全隔离。
3.2 Java 7新增的Objects类
Java为工具类的命名习惯是添加一个字母s,比如操作数组的工具类是Arrays,操作集合的工具类是Collections。
如下程序示范了Objects工具类的用法。
public class ObjectsTest
{
// 定义一个obj变量,它的默认值是null
static ObjectsTest obj;
public static void main(String[] args)
{
// 输出一个null对象的hashCode值,输出0
System.out.println(Objects.hashCode(obj));
// 输出一个null对象的toString,输出null
System.out.println(Objects.toString(obj));
// 要求obj不能为null,如果obj为null则引发异常
System.out.println(Objects.requireNonNull(obj
, "obj参数不能是null!"));
}
}
3.3 String、StringBuffer和StringBuilder类
字符串就是一连串的字符序列,Java提供了String和StringBuffer两个类来封装字符串,并提供了一系列方法来操作字符串对象。
String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。
StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
从JDK 1.5开始出现的StringBuilder类,也代表字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用StringBuilder类。
String类提供了大量构造器来创建String对象,其中如下几个有特殊用途。
- String():创建一个包含0个字符串序列的String对象(并不是返回null)。
- String(byte[] bytes, Charset charset):使用指定的字符集将指定的byte[]数组解码成一个新的String对象。
- String(byte[] bytes, int offset, int length):使用平台的默认字符集将从指定byte[]数组的offset开始、长度为length的子数组解码成一个新的String对象。
- String(byte[] bytes, int offset, int length, String charsetName):使用指定的字符集将指定的byte[]数组从offset开始、长度为length的子数组解码成一个新的String对象。
- String(byte[] bytes, String charsetName):使用指定的字符集将指定的byte[]数组解码成一个新的String对象。
- String(char[] value, int offset, int count):将指定的字符数组从offset开始、长度为count的
- String():创建一个包含0个字符串序列的String对象(并不是返回null)。
- String(String original):根据字符串直接量来创建一个String对象。也就是说,新创建的String对象是该参数字符串的副本。
- String(StringBuffer buffer):根据StringBuffer对象来创建对应的String对象
- String(StringBuilder builder):根据StringBuilder对象来创建对应的String对象。
String类也提供了大量方法来操作字符串对象,下面详细介绍这些常用方法。
- char charAt(int index):获取字符串中指定位置的字符。其中,参数index指的是字符串的序数,字符串的序数从0开始到length()-1
因为String是不可变的,所以会额外产生很多临时变量,使用StringBuffer或StringBuilder就可以避免这个问题。
StringBuilder提供了一系列插入、追加、改变该字符串里包含的字符序列的方法。而StringBuffer与其用法完全相同,只是StringBuffer是线程安全的。
StringBuilder、StringBuffer有两个属性:length和capacity,其中length属性表示其包含的字符序列的长度。与String对象的length不同的是,StringBuilder、StringBuffer的length是可以改变的,可以通过length()、setLength(int len)方法来访问和修改其字符序列的长度。capacity属性表示StringBuilder的容量,capacity通常比length大,程序通常无须关心capacity属性。如下程序示范了StringBuilder类的用法。
public class StringBuilderTest
{
public static void main(String[] args)
{
StringBuilder sb=new StringBuilder();
//追加字符串
sb.append("java");//sb="java"
//插入
sb.insert(0 , "hello "); //sb="hello java"
//替换
sb.replace(5, 6, ","); //sb="hello, java"
//删除
sb.delete(5, 6);//sb="hellojava"
System.out.println(sb);
//反转
sb.reverse();//sb="avajolleh"
System.out.println(sb);
System.out.println(sb.length()); //输出9
System.out.println(sb.capacity()); //输出16
//改变StringBuilder的长度,只保留前面部分
sb.setLength(5); //sb="avajo"
System.out.println(sb);
}
}
3.4 Math类
Java提供了基本的+、-、*、/、%等基本算术运算的运算符,但对于更复杂的数学运算,例如,三角函数、对数运算、指数运算等则无能为力。Java提供了Math工具类来完成这些复杂的运算,Math类是一个工具类,它的构造器被定义成private的,因此无法创建Math类的对象;Math类中的所有方法都是类方法,可以直接通过类名来调用它们。Math类除了提供了大量静态方法之外,还提供了两个静态Field:PI和E,正如它们名字所暗示的,它们的值分别等于π和e。
3.5 Java 7的ThreadLocalRandom与Random
Random类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以当前时间作为种子),另一个构造器需要程序员显式传入一个long型整数的种子。
ThreadLocalRandom类是Java 7新增的一个类,它是Random的增强版。在并发访问的环境下,使用ThreadLocalRandom来代替Random可以减少多线程资源竞争,最终保证系统具有较好的性能。
3.6 BigDecimal类
前面在介绍float、double两种基本浮点类型时已经指出,这两个基本类型的浮点数容易引起精度丢失。先看如下程序。
public class DoubleTest
{
public static void main(String args[])
{
System.out.println("0.05 + 0.01=" + (0.05 + 0.01));
System.out.println("1.0 - 0.42=" + (1.0 - 0.42));
System.out.println("4.015 * 100=" + (4.015 * 100));
System.out.println("123.3 / 100=" + (123.3 / 100));
}
}
程序输出结果是:
0.05 + 0.01=0.060000000000000005
1.0 - 0.42=0.5800000000000001
4.015 * 100=401.49999999999994
123.3 / 100=1.2329999999999999
为了能精确表示、计算浮点数,Java提供了BigDecimal类,该类提供了大量的构造器用于创建BigDecimal对象,包括把所有的基本数值型变量转换成一个BigDecimal对象,也包括利用数字字符串、数字字符数组来创建BigDecimal对象。
查看BigDecimal类的BigDecimal(double val)构造器的详细说明时,可以看到不推荐使用该构造器的说明,主要是因为使用该构造器时有一定的不可预知性。当程序使用new BigDecimal(0.1)来创建一个BigDecimal对象时,它的值并不是0.1,它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为double浮点数,所以传入BigDecimal构造器的值不会正好等于0.1(虽然表面上等于该值)。
BigDecimal类提供了add()、subtract()、multiply()、divide()、pow()等方法对精确浮点数进行常规算术运算。
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工具类,该工具类代码如下
public class Arith
{
//默认除法运算精度
private static final int DEF_DIV_SCALE=10;
//构造器私有,让这个类不能实例化
private Arith() {}
/**
* 提供精确的加法运算
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1,double v2)
{
BigDecimal b1=BigDecimal.valueOf(v1);
BigDecimal b2=BigDecimal.valueOf(v2);
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1,double v2)
{
BigDecimal b1=BigDecimal.valueOf(v1);
BigDecimal b2=BigDecimal.valueOf(v2);
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1,double v2)
{
BigDecimal b1=BigDecimal.valueOf(v1);
BigDecimal b2=BigDecimal.valueOf(v2);
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位的数字四舍五入
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
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
, BigDecimal.ROUND_HALF_UP).doubleValue();
}
public static void main(String[] args)
{
System.out.println("0.05 + 0.01="
+ Arith.add(0.05 , 0.01));
System.out.println("1.0 - 0.42="
+ Arith.sub(1.0 , 0.42));
System.out.println("4.015 * 100="
+ Arith.mul(4.015 , 100));
System.out.println("123.3 / 100="
+ Arith.div(123.3 , 100));
}
}