本部分介绍一些与运行环境相关的类
Sun为Java提供了丰富的基础类库,Java SE有三千多个基础类,要多敲代码,多练。
1.main()方法解析
如果一个程序总是按规定的流程运行,无需处理用户动作,也就是没有通过用户方面接受信息,但实际上大多数情况下,都需要与用户进行交互,这里先介绍从键盘获得用户输入,在图形用户接口(GUI)章节将会介绍用户通过GUI输入数据。
1.1 运行Java程序的参数
主方法解析
1 public static void main(String[] args){ 2 3 }
主方法这样设计的原因是:
public 修饰符:
Java类由JVM调用,为了让JVM可以自由调用这个main方法,可以使用public修饰符把这个方法暴露出来
static修饰符:
JVM调用这个主方法时,不可能先创建该主类的对象,然后通过对象来调用该主方法。JVM直接通过主类来调用主方法,因此使用static修饰主方法。
void返回值:
主方法被JVM调用,该方法的返回值返回给JVM,这没有意义,main方法没有返回值。
方法中包含一个字符串数组形参,根据方法调用原则:谁调用方法,谁负责形参赋值。main方法由JVM调用,args形参由JVM赋值。
package chapter1; public class TestArgs { public static void main(String[] args){ //输出由JVM调用的main参数args数组信息 //输出数组长度 System.out.println(args.length); //输出数组包含的元素 for(String i:args){ System.out.println(i); } } }
备注:foreach循环里,循环变量就是数组元素变量,和for循环里的循环变量是有区别的,foreach循环里循环变量是一个map集合的key,是实实在在的,而for循环里面循环数组里的变量(通常是i,j,k)并不是真实存在的,只是数组元素的下标。
上面这个程序输出JVM调用main方法传递的参数数组args的长度及遍历args数组元素。使用java TestArgs命令运行上面的程序只输出一个0,也就是args数组是一个长度为0的空数组。这是因为用户运行程序时候没有给args数组设定参数值(显然JVM并没有胆子自己随意加个字符串给它),那么JVM不知道args数组的值,所以JVM将args数组设置成一个长度为0的数组
按下面的命令运行 java TestArgs Hello World 输出结果 2 Hello World
如果运行Java程序时在类名后紧跟一个或多个字符串(多个字符串之间以空格隔开),JVM就会把这些字符串依次赋给args数组元素,运行Java程序时的参数与args数组之间的对应关系如下:
执行命令时候输入的参数: 第一个参数 第二个参数 第三个参数....
args数组: 第一个数组元素 第二个数组元素 第三个数组元素
主方法的参数: 第一个参数 第二个参数 第三个参数....
上面三组参数一一对应
如果某参数本身包含了空格,需要将参数用双引号""括起来,否则JVM会把这个空格当成参数分隔符,而不是餐费本身
java TestArgs "Java Spring"
输出结果:长度是1,数组元素是一个Java Spring
2. 使用Scanner获取键盘输入
通常运行Java程序时候是在程序运行之前就定义几个参数,通过一些类,可以与用户进行交互,程序就可以在运行过程中取得输入。
Scanner类是一个基于正则表达式的文本扫描器,它可以从文件、输入流、字符串中解析出基本类型值和字符串值。Scanner类提供了多个构造器,不同的构造器可以接受文件、输入流、字符串作为数据源,用于从文件、输入流、字符串中解析数据。
Scanner主要提供了两个方法来扫描输入:
hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串,如果需要判断是否包含下一个字符串,可以省略掉Xxx
nextXxx():获取下一个输入项。Xxx的含义与hasNextXxx()中的Xxx相同
默认情况下Scanner使用空白(包括空格、Tab空白)作为多个输入项的分隔符。
package chapter1; import java.util.Scanner; public class TestScannerKeyBoard { public static void main(String[] args){ //System.in代表标注输入,标注输入就是键盘输入 Scanner scanner = new Scanner(System.in); //限制输入时的参数分隔符 scanner.useDelimiter(" "); for(;scanner.hasNext();){ //输出输入的东西 System.out.println("从键盘输入的东西是:+ scanner.next()); } } }
上面使用的分隔符(分隔的是参数)是回车,也就是说每次回车一下,Scanner就当作一次参数输入。用户可能在将一系列数据输入到一行末尾回车,也可能只输入一个字符便回车,无论哪一种,里面是否包含空格,都以回车作为一个输入项。当然如果想用回车作为参数之间的分隔符,只需要注释掉上面的useDelimiter()方法。
Scanner的读取操作可能被阻塞(当前执行程序顺序流暂停)来等待信息的输入,如果输入没有结束,Scanner又读不到更多输入项时,Scanner的hasNext和next方法都有可能阻塞,hasNext方法是否阻塞与其相关的next方法是否阻塞无关
除了以空格、Tab、回车作为参数分隔符外,Scanner提供了两个简单的方法来逐行读取,也就是说每一行作为一个参数
boolean hasNextLine():返回输入源中是否还有下一行
String nextLine():返回输入源中下一行的字符串
Scanner不仅可以获取字符串输入项,也可以获取任何基本类型的输入项。
package chapter1; import java.util.Scanner; public class TestScannerKeyBoard { public static void main(String[] args){ //System.in代表标注输入,标注输入就是键盘输入 Scanner scanner = new Scanner(System.in); //限制输入时的参数分隔符 //scanner.useDelimiter(" "); for(;scanner.hasNextLong();){ //输出输入的东西 System.out.println("从键盘输入的东西是:" + scanner.nextLong()); } } }
上面通过hasNextLong和nextLong两个方法来接收和输出Long型对象,如果不是Long型的将退出,显然这样的程序不够健壮。
Scanner不仅可以处理从标准设备的输入,也可以处理从文件的输入。
3. 使用BufferedReader获取键盘输入
Scanner可以非常方便获取键盘输入,在JDK1.5 之前还有一个获取输入的类:BufferedReader类
BufferedReader是JavaIO流中的一个字符包装流,也就是说它必须建立在另一个字符流基础上,但标准输入:System.in是字节流,程序需要使用转换流InputStreamReader将其包装成字符流。所以程序中用于获取键盘输入的BufferedReader对象采用如下代码创建:
1 BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
一旦获得BufferedReader对象之后,就可以调用该对象的readLine()方法逐行读取键盘输入:如果没有键盘输入,readLine方法会阻塞等待键盘输入。
package chapter1; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class TestBufferedReader { public static void main(String[] args) throws IOException{ //创建字符流输入 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); //提示用户输入 System.out.println("请输入:"); //读取输入 String str = br.readLine(); System.out.print(str); } }
使用BufferedReader可以逐行读取用户的键盘输入,每次用户的键盘输入都被BufferedReader当成String对象。与Scanner不同的是,BufferedReader即便读取的是基本类型的输入项,也会当成String处理
备注1:字节流与字符流区别
字符流处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,而字节流处理单元为1个字节, 操作字节和字节数组。所以字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,所以它对多国语言支持性比较好!如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好点.
所有文件的储存是都是字节(byte)的储存,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成字节序列.
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串; 2. 字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
字节流是读取的是字节,也就是机器存储时的源码,任何软件在计算机中存储的形式都是二进制,所以说,字节可以读取任何软件,而字符只是针对某个字,而且容易出现乱码,而字节流就不会
用字节流时采用二进制的编码直接传输,用字符流则牵涉到本地系统的编码问题,java io中的部分api会根据操作系统或者jvm的参数配置自行进行字符流转换,这样会简化部分的编程过程,但如果是在网络通讯中,强烈建议使用byte字节流方式,减少程序因编码转换造成的种种问题
区别:
1. 字节流是最基本的,所有的InputStream和OutputStream的子类都是,主要用在处理二进制数据,它是按字节来处理的。但实际中很多的数据是文本,又提出了字符流的概念,它是按虚拟机的encode来处理,也就是要进行字符集的转化这两个之间通过InputStreamReader,OutputStreamWriter来关联,实际上是通过byte[]和String来关联。在实际开发中出现的汉字问题实际上都是在字符流和字节流之间转化不统一而造成的,在从字节流转化为字符流时,实际上就是byte[]转化为String时,public String(bytebytes[],String charsetName)
2. 有一个关键的参数字符集编码,通常我们都省略了,那系统就用操作系统的lang。而在字符流转化为字节流时,实际上是String转化为byte[]时,byte[]String.getBytes(StringcharsetName)也是一样的道理。至于java.io中还出现了许多其他的流,按主要是为了提高性能和使用方便,如BufferedInputStream,PipedInputStream等
3. 字符流和字节流是根据处理数据的不同来区分的。字节流按照8位传输,字符流按照16位传输。由于字符流使用Unicode字符集,支持多国文字,因此若流要跨越多种平台传输,应使用字符流。按kilojin的说法,字符流的传输效率比字节流的高.
备注2:硬盘知识
传统硬盘实际上就是一个高密度的磁盘,它是在一块硬质基板上涂覆了磁粉,通过读写磁头产生的磁场改变磁盘上的每一个磁道记录单元内磁体方向的变化进行读写处理。说白了就是一张类似原来的3寸、5寸磁盘,不过是密度、可靠性等大大提高而已。传统的硬盘上的数据只要没有强磁作用,其数据的维持期理论上更久,之所以硬盘容易坏,主要因为其机械机构、电路部分故障所致。相信你也常听说10几年前的老硬盘,如今依然工作这样的故事。当然,如果想长期保存数据的话,刻制光盘并定期复制会更好一些。
2. 系统相关
Java程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java提供了System类和Runtime类来与程序运行的平台进行交互。
2.1 System类
System类代表当前Java程序的运行平台(平台类),程序不能创建System类对象,所以它提供了一些类属性和类方法,允许直接通过System类名来调用这些属性和方法
System类提供了代表标准输入、标准输出和错误输出的类属性;并提供了一些静态方法用于访问环境变量、系统属性的方法
package chapter1; import java.util.*; public class TestSystem { public static void main(String[] args){ //创建一个Map集合用于存放系统信息,使用系统类直接调用 Map<String,String> env = System.getenv(); //以key为循环变量输出集合信息 for(String key:env.keySet()){ System.out.println(key + "------->" + key); } //获取指定变量的值 System.out.println(System.getenv("JAVA_HOME")); //获取所有系统属性 System.out.println(System.getProperties()); //获取某个属性 System.out.println(System.getProperty("os.name"));//如果不是变量,请带上双引号~~ } }
System类还提供了垃圾回收的gc()方法
System类还提供了获取当前时间与UTC 1970年1月1日 午夜的时间差两个方法。一个是currentTimeMillis()和nanoTime(),他们都返回一个long型整数,前者是毫秒为单位,后者以微秒为单位。返回值的粒度取决于底层操作系统。
输入输出方法:
in 系统的标准输入流(键盘)
out 标准输出流(显示器)
err 错误输出流
另外也提供了setIn,setOut,setErr来修改标准输入流
System类还提供了一个identityHashCode(Object x)方法,该方法返回指定对象的hashCode()值,也就是根据对象的地址计算得到的hashCode值。当某个类的hashCode方法被重写后,该类实例的hashCode方法不能唯一地标识该对象,通过identityHashCode方法返回的hashCode依然是根据该对象的地址计算得到的hashCode值,所以如果两个identityHashCode值相同,则两个对象绝对是同一个对象。
1 package chapter1; 2 3 public class TestIdentityHashCode { 4 public static void main(String[] args){ 5 String str1 = new String("Hello World!"); 6 String str2 = new String("Hello World!"); 7 //String重写了hashCode方法,使用equals方法比较,比较内容 8 System.out.println("str1的hashCode " + str1.hashCode() 9 +"-------" + "str2的hashCode " + str2.hashCode()); 10 /** 11 * identityHashCode比较采用的是堆内存地址的比较 12 * 由于采用的是String共享池设计,所以地址相同,比较结果相同 13 * err用红色输出 14 */ 15 System.err.println("str1的identityHashCode " + System.identityHashCode(str1) 16 +"-------" + "str2的identityHashCode " + System.identityHashCode(str1)); 17 /** 18 * 创建两个整数对象,对比String的identityHashCode方法结果,可以 19 * 发现String对于内容相同的字符串序列比较结果相同,而整数对象是不同的 20 * 因为String是共享池设计,而整数对象不是 21 */ 22 Integer integer1 = new Integer(5); 23 Integer integer2 = new Integer(5); 24 System.out.println("integer1的hashCode " + integer1.hashCode() 25 +"-------" + "integer2的hashCode " + integer2.hashCode()); 26 System.err.println("integer1的identityHashCode " + System.identityHashCode(integer1) 27 +"-------" + "integer2的identityHashCode " + System.identityHashCode(integer2)); 28 String str3 = "Java"; 29 String str4 = "Java"; 30 System.out.println("str3的hashCode " + str3.hashCode() 31 +"-------" + "str4的hashCode " + str4.hashCode()); 32 System.err.println("str3的identityHashCode " + System.identityHashCode(str3) 33 +"-------" + "str4的identityHashCode " + System.identityHashCode(str4)); 34 } 35 36 }
输出结果:
str1的hashCode -969099747-------str2的hashCode -969099747
str1的identityHashCode 33263331-------str2的identityHashCode 33263331
integer1的hashCode 5-------integer2的hashCode 5
integer1的identityHashCode 6413875-------integer2的identityHashCode 21174459
str3的hashCode 2301506-------str4的hashCode 2301506
str3的identityHashCode 827574-------str4的identityHashCode 827574
2.2 Runtime类
Runtime类代表Java程序的运行时环境,每个Java程序都有一个与之相应的Runtime实例,通过这个实例与运行时环境交互。应用程序自己不能创建自己的RunTime实例,但是可以通过getRuntime()方法获取与之关联的Runtime对象
与System类类似的是:Runtime类也提供了gc()方法和runFinalization()方法来通知系统进行垃圾回收。注意:System类是属于JVM的,而Runtime类是与Java程序相关联的。
Runtime类代表Java程序的运行时环境,可以访问JVM的相关信息,如处理器、内存信息。
Runtime类还提供了一个方法exec()用于与操作系统打交道
1 package chapter1; 2 3 import java.io.IOException; 4 5 public class TestRuntime { 6 public static void main(String[] args) throws IOException{ 7 //获得一个与当前程序相关的Runtime实例与JVM交互 8 Runtime rt = Runtime.getRuntime(); 9 System.out.println("总内存数:" + rt.totalMemory()); 10 System.out.println("最大内存数:" + rt.maxMemory()); 11 System.out.println("处理器数理:" + rt.availableProcessors()); 12 System.out.println("空闲内存数:" + rt.freeMemory()); 13 //通过exec()方法与操作系统交互 14 rt.exec("notepad.exe"); 15 } 16 }
注意:没有最小内存方法。
3 常用类
下面介绍一些常用类,如String、Math、BigDecimal等的用法。
3.1 Object类
Object类是所有类、数组、枚举类的父类,也就是说,Java允许把所有任何类型的对象赋给Object类型的变量。当定义一个类时没有使用extends关键字为它显示指定父类,则该类默认继承Object父类。 因为所有Java类都是Object类的子类,所以任何Java对象都可以调用Object类的方法,Object提供了如下几个常用方法: 1. boolean equals(Object obj):判断指定对象与该对象是否相等。此处相等的标准是:两个对象是同一个对象,因此该equals方法通常没有太大的实用价值。 2. protected void finalize():当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资源 3. Class<?>getClass():该方法返回该对象的运行时类。 4. int hashCode():返回该对象的hashCode值,默认情况下,如果Object类hashCode()方法根据该对象的地址来计算(即与System.identityHashCode(Object x)方法的计算结果相同)。但很多类都重写Object类的hashCode()方法,不再根据地址来计算其hashCode()方法值。 5. String toString():返回该对象的字符串表示,当我们使用System.out.println方法输出一个对象,或者把某个对象和字符串进行连接时,系统会自动调用该对象的toString方法返回该对象的字符串表示。Object的toString方法返回"运行时类名@十六进制hashCode值"格式的字符串,但很多类都重写了Object类的toString方法用于返回可以表述该对象信息的字符串。 除此之外,Object还提供了wait、notify、notifyAll几个方法,通过这几个方法用于控制线程的暂停和运行。
3.2 String、StringBuffer和StringBuilder类
字符串就是一连串的字符序列,Java提供了String和StringBuffer两个类来封装字符串,并提供了系列方法来操作字符串对象。 String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。 StringBuffer 对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append、insert、reverse、setCharAt、setLength等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString方法将其转换为一个String对象。 JDK1.5 又新增了一个StringBuilder类,它也代表了字符串对象,实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同,不同的是,StringBuffer是线程安全的,而StringBuilder没有实现线程安全功能,所以性能略高,因此通常情况下,如果需要创建一个内容可变的字符串对象,应该优先考虑使用StringBuilder类。 String类提供了大量构造器来创建String对象。
String类是不可变的,String的实例一旦生成就不会再改变了,例如如下代码:String str = "java" + "struts" + "spring" 上面程序除了使用4字符串直接量之外,还会生成额外的2个字符串常量,按照运算顺序,java和struts生成了javastruts保存在内存中,接着javastruts与spring生成了javastrutsspring字符串,并将它赋给str变量。因为String是不可变的,所以会产生很大临时变量,使用StringBuffer或StringBuilder就可以避免这个问题。 String还有一个非常容易迷惑的地方,就是hello和new String("hello")的区别。为了介绍hello的存储,我们先介绍常量池这个简单的概念:常量池指的是在编译期被确定、并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口中的常量,也包括字符串常量。看如下程序:
String s0 = "hello"; String s1 = "hello"; String s2 = "he" + "llo"; System.out.println(s0 == s1); System.out.println(s0 == s2); System.out.println(new String("hello" == new String("hello"));
运行结果为:true true false
最后一行比较容易理解,因为两次new出来的不是同一个对象,所以使用==比较返回false。Java会确保每个字符串常量只有一个,不会产生多个副本,s0和s1中的"hello"都是字符串常量,它们在编译期被确定了,所以是true,而he和llo也是字符串常量,当一个字符串由多个字符串常量连接而成时,它本身也是字符串常量,所以s2同样在编译期被解析为一个字符串常量,所以s2也是常量池中hello的引用,所以s0==s2返回true。 用new String()创建的字符串不是常量,不能在编译器就确定,所以new String()创建字符串不会放入常量池中,它们都有自己的地址空间。 StringBuilder、StringBuffer则提供了系列插入、追加、改变该字符串里包含的字符串序列,而StringBuffer与其用法完全相同,只是StringBuffer是线程安全的。
3.3 Math类
Java提供了基本的+、-、*、/、%等级别算术运算的运算符,但对于更复杂的数学运算,例如三角函数、对数运算、指数运算等则无能为力。Java提供了Math工具类来完成这项复杂的运算,Math类是一个工具类,它的构造器被定义成private,因此无法创建Math类的对象;Math类中所有方法都是类方法,可以直接通过类名来调用它们。Math除了提供大量静态方法之外,还提供了两个静态属性:PI和E,正如它们名字暗示的,它们的值分别等于π和e
3.4 Random类
Random类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子,另一个构造器需要程序员显式传入一个long型整数的种子。 相对于Math的random()方法而言,Random类提供了更多方法来生成各种伪随机数,它不仅可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,还可以指定生成随机数的范围.
3.5 BigDecimal类
前面介绍float、double两种基本浮点类型时已经指出,这两个基本类型的浮点数容易引起精度丢失,用BigDecimal不会发生这样的事情。
3.6 处理日期的类
Java还提供了一系列用于处理日期、时间的类,包括创建日期、时间对象,获取系统当前日期、时间等操作。
Date类
Java提供了Date类来处理日期、时间(此处的Date是指java.util包下的Date类,而不是java.sql包下的Date类),这个Date类从JDK1.0就开始存在了,但因为时间久远,所以大部分构造器、方法都不再推荐使用。 Date类提供了6个构造器,但4个已经被Deprecated(Java不再推荐使用,使用不再推荐的方法时编译器会提出警告信息,并导致程序性能、安全性等方面的问题),剩下的两个构造器分别为: Date():生成一个代表当前日期时间的Date对象,该方法在底层调用System.currentTimeMillis()获得long整数作为日期参数。 Date(long date):根据指定的long型整数来生成一个Date对象。
Calendar类
因为Date类的设计上存在一些缺陷,所以Java提供了Calendar类来更好地处理日期和时间。Calendar是一个抽象类,它用于表示日历。 Calendar本身是一个抽象类,它是所有日历类的模板,并提供了一些所有日历通用的方法,但它本身不能直接实例化,程序只能创建Calendar子类的实例,Java本身提供了一个GregorianCalendar类,一个代表GregorianCalendar的子类,它代表了我们通常所说的公历。当然,也可以创建自己的Calendar子类,然后将它作为Calendar对象使用。
4 正则表达式
正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作,String类里也提供了如下几个特殊的方法: boolean matches(String regex):判断该字符串是否匹配指定正则表达式 String replaceAll(String regex, String replacement):返回该字符串中所有匹配正则表达式的子串替换成replacement后的新字符串 String replaceFirst(String regex, String replacement):返回该字符串中第一个匹配正则表达式的子串替换成replacement后的新字符串 String[] split(String regex):根据给定正则表达式拆分该字符串后得到的字符串数组。 上面这些方法依赖于Java提供的正则表达式支持,除此之外,Java提供了Pattern和Matcher两个类专门用于提供正则表达式支持。
4.1 创建正则表达式
正则表达式是一个模板,创建正则表达式就是创建一个特殊的字符串。
4.2 使用正则表达式
一旦在程序中定义了正则表达式之后,就可以使用Pattern和Matcher来使用正则表达式。Pattern对象是正则表达式编译后在内存中的表示形式,因此,正则表达式字符串必须先被编译为Pattern对象,然后再利用该Pattern对象创建对应的Matcher对象。执行匹配所涉及的状态保留在Matcher对象中,多个Matcher对象可共享同一个Pattern对象。 典型的调用顺序如下:
//将一个字符串编译成Pattern对象 Pattern p = Pattern.compile("a*b"); //使用Pattern对象创建Matcher对象 Matcher m = p.matcher("aaaaab"); boolean b = m.matches();//返回true
上面定义的Pattern对象可以多次重复使用,如果某个正则表达式仅需一次使用,可直接使用Pattern类的静态matches方法,此方法自动把指定字符串编译成匿名的Pattern对象,并执行匹配,如下所示: boolean b = Pattern.maches("a*b", "aaaaab");