Java学习笔记(1)——基础语法
吃水不忘挖井人,我是根据http://www.iteedu.com/plang/java/javadiary/2.php学习的,所以大部分内容也都是这个网站上的。
1、第一个Java程序
1 public class HelloWorld { 2 public static void main(String[] args){ 3 System.out.println("Hello World!I am CheeseZH!"); 4 } 5 }
写Java程序通常都是由定义「类别」开始,"class"是Java 用来定义类别的关键词,类别的名称是HelloWorld,这与您所编辑的档案(HelloWorld.java)主档名必须相同,在编写Java程序 时,一个档案中可撰写数个类别,但是只能有一个"public"类别, 而且档案主档名必须与这个"public"类别的名称相同。
接下来看看 main() 方法(Method),它是Java程序的「进入点」(Entry point), 程序的执行是由进入点开始的:
1)、它一定是个"public"成员(Member), 这样它才可以被呼叫;
2)、它不需要产生对象就要能被执行,所以它必须是个"static"成员。
"void"表示这个方法执行结束后不传回任何值,Java程序的主 方法不需传回任何值,所以一律使用void;main()是Java程序的 主方法名称,其中"String[] args"是命令列自变量 (Command line argument),可以在执行程序时取得使用者指定的相关参数,目前虽然您不使用,但仍要撰写它,这是规定。
System.out.println("Hello World!I am CheeseZH!");
在这个程序中使用了java.lang套件下的System类别,使用它的公开成员 out 对象,它是一个 PrintStream 对象,您使用了它所提供的println()方法,将当中指定的字符串(String) "Hello World!I am CheeseZH!"输出至 Console 上。
注意在Java中字符串要使用""包括,println()表示输出字符串后自动断行,如果使用print()的话,则输出字符串后程序并不会自动断行;注意陈 述结束要用 ';' 。
2、C风格输出printf
public class HelloWorld { public static void main(String[] args){ System.out.printf("%s\n", "Hello World!I am CheeseZH!"); } }
3、获取用户输入
1)、System.in.read()使用不方便
当在文字模式下要输入数据至程序中时,您可以使用标准输入串流对象System.in,然而我们很少直接使用它,因为System.in对象所提供的 read()方法,是从输入串流取得一个字节的数据,并传回该字节的整数值。
在文字模式下的输入是以字符的方式传送给程序,所以直接使用read()方法取得的是字符的ASCII编码整数,通常要取得的使用者输入会是一个字符串,或是一组数字,所以 System.in对象的read()方法一次只读入一个字节数据的方式并不适用。
2)、使用java.util.Scanner
import java.util.Scanner; public class ScannerInput { public static void main(String[] args){ Scanner scanner = new Scanner(System.in); System.out.print("Please input your name:"); System.out.printf("Hello, %s.\n", scanner.next()); System.out.print("Please input a number:"); System.out.printf("%d\n", scanner.nextInt()); } }
"new"表示新增一个Scanner对象,在新增一个 Scanner对象时需要一个System.in对象,因为实际上还是System.in在取得使用者的输入,您可以将Scanner看作是 System.in对象的支持者,System.in取得使用者输入之后,交给Scanner作一些处理(实际上,这是 Decorator 模式 的一个应用)。
简单的说,您告诉执行环境新增一个Scanner对象,然后使用它的next()方法来取得使用者的输入字符串,使用 Scanner对象的nextInt()方法取得数字。同样的,您还可以使用Scanner的nextFloat()、nextBoolean()等方法来取得使用者的输入,并转换为正确的 数据型态。
要注意的是,Scanner取得输入的依据是空格符,举凡按下空格键、tab键或是enter键,Scanner就会传回下一个输入,如果您想要取得包 括空格符的输入,比较简单的方法是 使用 BufferedReader 类别取得输入。
3)、使用 BufferedReader 类别取得输入
(1)、BufferedReader类别,它是java.io套件中所提供的一个类别,所以使用这个类别时必须先import java.io套件;
(2)、使用BufferedReader对象的readLine()方法必须处理IOException例外(exception),例外处理机制是Java提 供给程序设计人员捕捉程序中可能发生的错误所提供的机制,现阶段您处理IOException的方法是在main()方法后,加上 throws IOException,这在以后会再详细讨论为何要这么作。
BufferedReader在建构时接受一个Reader对象,在读取标准输入串流时,会使用InputStreamReader,它 继承了Reader类别,您使用以下的方法来为标准输入串流建立缓冲区对象:
import java.io.*; public class BuffRdr { public static void main(String[] args) throws IOException{ BufferedReader buf = new BufferedReader( new InputStreamReader(System.in)); System.out.print("Please input a sentence:"); String text = buf.readLine(); System.out.println("Your sentece is:"+text); } }
"new"关键词表示您要建构一个对象为您所用,BufferedReader buf表示宣告一个型态为BufferedReader的对象变量,而new BufferedReader()表示以BufferedReader类别建构一个对象,new InputStreamReader(System.in)表示接受一个System.in对象来建构一个InputStreamReader物件。
readLine()方法会传回使用者在按下Enter键之前的所有字符输入,不包括最后按下的 Enter返回字符。
4)、标准输入输出串流
在之前的HelloWorld程序中,您使用了System类别中的静态对象out,它提供标准输出串流(Stream),会在程序开始执行之后自动开启 并准备接受指定的资料,它通常对应至显示输出(Console、终端机输出)或其它的输出目的地,它可以被重新导向至一个档案,您可以在执行程序时使用 '>>'将输出结果导向至指定的档案【需要在控制台中执行】,例如:
java HelloWorld >> output.txt
上面的执行会将结果导向至output.txt,而不会在屏幕上显示"Hello! World!",output.txt中将会有输出结果"Hello! World"!。
除了标准输出串流out之外,Java程序在执行之后,还会开启标准输入串流in与标准错误输出串流err,下面先说明标准输入串流in。
标准输入串流in也是用System类别所提供的静态对象,在程序开 始之后它会自动开启,对应至键盘或其它的输入来源,准备接受使用者或其它来源的输入,您可以使用read()方法来读取输入,不过通常很少直接使用它,而 会使用一个Scanner对象为输入串流作后处理,方法在 取得使用者输入 简介过了。
标准错误输出串流err也是在程序执行后自动开启,它会将指定的字符串 输出至显示装置或其它指定的装置,与标准输出串流out不同的是,它会立即显示指定的(错误)讯息给使用者知道,例如即使您指定程序将结果重新导向至文件 案,err输出串流的讯息并不会被重新导向,而仍会显示在指定的显示装置上。
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello! World!"); System.err.println("Error Message Test"); } }
运行:
java HelloWorld >> output.txt
Error Message Test
开启output.txt之后,您会发现当中只有"Hello! World!"讯息,而Error Message Test讯息并没有被导向至档案中,而是直接显示在Console(或终端机)中。
要 重新导向标准输出是用 '>', 标准输入则是 '<',而 '>>' 除了重导标准输出之外,还有附加的功能,也就是会把输出附加到被导向的目标档案后头,如果目标档案本来不存在,那么效果就和 '>' 一样。(重导——重定向)
5)、Console 输入格式控制
标准输出通常是文字模式为主的主控台(终端机),这边介绍几个格式控制技巧,在主控台显示时可以协助输出的显示格式。
首先介绍格式字符控制,先表列一些常用的控制字符:
\\ | 反斜线 |
\' | 单引号 ' |
\" | 双引号 " |
\uxxxx | 以16进位数指定Unicode字符输出 |
\dxxx | 以8进位数指定Unicode字符输出 |
\b | 倒退一个字符 |
\f | 换页 |
\n | 换行 |
\r | 游标移至行首 |
\t | 跳格(一个Tab键) |
下面这段程序告诉您如何指定字符编码来输出"Hello"这段文字:
System.out.println("\u0048\u0065\u006C\u006C\u006F");
在输出数值时,预设都会以十进制的方式来显示数值,下面这几段程序可以让您分别以不同进位制来显示数值:
int x = 19; // 转成二进制 10011 System.out.println(Integer.toBinaryString(x)); // 转成十六进制 13 System.out.println(Integer.toHexString(x)); // 转成八进位 23 System.out.println(Integer.toOctalString(x));
您可以使用 System.out.printf() 作简单的输出格式设定,例如:
// 输出 19 的十进制表示 System.out.printf("%d%n", 19); // 输出 19 的八进位表示 System.out.printf("%o%n", 19); // 输出 19 的十六进制表示 System.out.printf("%x%n", 19);
'%d'表示将指定的数值以十进制表示,'%o'是八进位表示,而'%x'是十六进制表示,'%n'是指输出平台特定的换行字符,如果是在Windows下实际上会置换为 '/r/n',如果是Linux下则会置换为'/n'。
下表简单列出了一些常用的转换符号:
%% | 在字符串中显示% |
%d | 以10进位整数方式输出,提供的数必须是Byte、 Short、 Integer、Long、或BigInteger |
%f | 将浮点数以10进位方式输出,提供的数必须是Float、 Double或 BigDecimal |
%e, %E | 将浮点数以10进位方式输出,并使用科学记号,提供的数必须是 Float、 Double或BigDecimal |
%a, %A | 使用科学记号输出浮点数,以16进位输出整数部份,以10进位 输出指数部份, 提供的数必须是Float、Double、BigDecimal |
%o | 以8进位整数方式输出,提供的数必须是Byte、Short、 Integer、Long、或BigInteger |
%x, %X | 将浮点数以16进位方式输出,提供的数必须是Byte、 Short、 Integer、Long、或BigInteger |
%s, %S | 将字符串格式化输出 |
%c, %C | 以字符方式输出,提供的数必须是Byte、Short、 Character或 Integer |
%b, %B | 将"true"或"false"输出(或"TRUE"、 "FALSE",使用 %B)。另外,非null值输出是"true",null值输出是"false" |
%t, %T | 输出日期/时间的前置,详请看在线API文件 |
您可以在输出浮点数时指定精度,例如:
System.out.printf("example:%.2f%n", 19.234);
执行结果会输出:
example:19.23
您也可以指定输出时,至少要预留的字符宽度,例如:
System.out.printf("example:%6.2f%n", 19.234);
由于预留了6个字符宽度,不足的部份要由空格符补上,所以执行结果会输出如下(19.23只占五个字符,所以补上一个空白在前端):
example: 19.23
以上只是简短的列出一些常用的输出转换符号,事实上,这些功能都是由 java.util.Formatter 所提供的,如果您需要更多关于输出格式的控制,您可以看看在线API文件以查询相关设定。
4、类型
整数
只储存整数数值,可细分为「短整数」(short)(占2个字节)、整数(int)(占4个字节)与长整数(long)(占8个字节)。
位
Java提供有byte数据型态,专门储存位数据,例如影像位资 料,一个byte数据型态占一个字节。
浮点数
主要用来储存小数数值,也可以用来储存范围更大的整数,可分为浮点数(float) (占4个字节)与倍精度浮点数(double)(占8个字节)。
字符
用来储存字符,Java的字符采Unicode编码,其中前128个 字符编码与ASCII编码兼容;每个字符数据型态占两个字节,可储存的字符范围由\ u0000到\uFFFF,由于Java的字符采用Unicode编码,一个中文字与一个英文字母在Java中同样都是用一个字符来表示。
布尔数
占内存2个字节,可储存true与false两 个数值,分别表示逻辑的「真」与「假」。
使用下面这个程序获得数值的储存范围:
public class DataTypeMaxMin { public static void main(String[] args) { System.out.printf("short range: %d ~ %d\n", Short.MAX_VALUE, Short.MIN_VALUE); System.out.printf("int range: %d ~ %d\n", Integer.MAX_VALUE, Integer.MIN_VALUE); System.out.printf("long range: %d ~ %d\n", Long.MAX_VALUE, Long.MIN_VALUE); System.out.printf("byte range: %d ~ %d\n", Byte.MAX_VALUE, Byte.MIN_VALUE); System.out.printf("float range: %e ~ %e\n", Float.MAX_VALUE, Float.MIN_VALUE); System.out.printf("double range: %e ~ %e\n", Double.MAX_VALUE, Double.MIN_VALUE); } }
其中Byte、Integer、Long、Float、Double都 是java.lang套件下的类别名称,而 MAX_VALUE与MIN_VALUE则是各类别中所定义的静态常数成员,分别表示该数据型态可储 存的数值最大与最小范围,%e表示用科学记号显示,执行结果如下所示:
short range: 32767 ~ -32768 int range: 2147483647 ~ -2147483648 long range: 9223372036854775807 ~ -9223372036854775808 byte range: 127 ~ -128 float range: 3.402823e+38 ~ 1.401298e-45 double range: 1.797693e+308 ~ 4.900000e-324
其中浮点数所取得是正数的最大与最小范围,加上负号即为负数的最大与最小范围。
5、变量、常量
变量(Variable)是一个指向数据储存空间的参考,您将数据指定给变量,就是将数据储存至对应的内存空间,呼叫变量, 就是呼叫对应的内存空间的数据供您使用。
在过去曾流行过匈牙利命名法,也就是在变量名称前加上变量的数据型态名称缩写,例如intNum用来表示这个变量是int整数数据型态,fltNum表示 一个float数据型态,然而随着现在程序的发展规模越来越大,这种命名方式已经不被鼓励。
过去的程序在撰写时,变量名称的长度会有所限制,但现在已无这种困扰,因而现在比较鼓励用清楚的名称来表明变量作用,通常会以小写字母作为开始,并在每个单字开始时第一个字母使用大写,例如:
int ageForStudent; int ageForTeacher;
像这样的名称可以让人一眼就看出这个变量的作用,这样的命名方式,在Java程序设计领域中是最常看到的一种。
变量名称可以使用底线作为开始,通常使用底线作为开始的变量名称,表示它是私用的 (Private),只在程序的某个范围使用,外界并不需要知道有这个变量的存在,通常这样的变量名称常用于对象导向程序设计中类别的私有成员(Private member),这样的命名方式在Java中偶而也 会看到(比较常见于C++的程序撰写中),一个宣告的例子如下:
double _window_center_x; double _window_center_y;
当您在Java中宣告一个变量,就会配置一块内存空间给它,这块空间中原先可能就有数据,也因此变量在宣告后的值是不可预期的,Java对于安全性的要 求极高,您不可以宣告变量后,而在未指定任何值给它之前就使用它,编译器在编译时会回报这个错误,例如若宣告变量var却没有指定值给它,则会显示以下讯 息:
variable var might not have been initialized
可以的话,尽量在变量宣告后初始其值,您可以使用「指定运算子」 (Assignment operator)=来指定变量的值,例如:
int ageForStudent = 0; double scoreForStudent = 0.0; char levelForStudent = 'A';
上面这段程序在宣告变量的时候,同时指定变量的储存值,而您也看到如何指定字符给字符变量,字符在指定时需使用引号 ' ' 来包括;在指定浮点数时,会习惯使用小数的方式来指定,如0.0,在Java中写下0.0这么一个常数的话,其预设为double数据型态。
在宣告变量之后,您可以直接呼叫变量名称来取得其所储存的值,下面这个程序是个简单的示范:
public class UseVar { public static void main(String[] args){ int ageForStudent = 5; double scoreForStudent = 95.0; char lvlForStudent = 'A'; System.out.println("Age\t Score\t Rank"); System.out.printf("%3d\t %5.1f\t %4c", ageForStudent, scoreForStudent, lvlForStudent); } }
在Java中写下一个数值,称之为字面常量(Literal constant), 它会存在内存的某个位置,您无法改变它的值;而在使用变量的时候,也会使用一种叫「常数」的变量,严格来说它并不是常数,只不过指定数值给这个变量之 后,就不可再改变其值,有人为了区分其与常数的差别,还给了它一个奇怪的名称:「常数变数」。
先不要管「常数变量」这个怪怪的名称,其实它终究是个变量而已,只是在宣告变量名称的同时,加上"final"来限定,只不过这个变量一但指定了值,就不可以再改变它的值,如 果程序中有其它程序代码试图改变这个变量,编译器会先检查出这个错误,例如:
final int maxNum = 10; maxNum =20;
这一段程序代码中的maxNum变量使用final来限定,所以它在指定为10之后,就不可以再指定值给它,所以第二次指定会被编译器指出错误:
cannot assign a value to final variable maxNum
使用final来限定的变量,目的通常就是不希望其它的程序代码来变动它的值,例如用于循环计数次数的指定(循环之后就会学到),或是像圆周率PI的指定。
6、类型转换
Java对于程序的安全性要求极高,型态转换在某些情况一定要明确指定,就是在使用指定运算子时,将精确度大的指定给精确度小的变量时,由于在精确度上会 有遗失的现象,编译器会认定这是一个错误,例如:
int testInteger = 0; double testDouble = 3.14; testInteger = testDouble; System.out.println(testInteger);
这段程序在编译时会出现以下的错误讯息:
possible loss of precision found?: double required: int testInteger = testDouble ^ 1 error
如果您确定这是您要的结果,您必须明确加上转换的限定字:
testInteger = (int) testDouble;
7、比较、条件运算符
使用 == 运算时要注意的是,对于对象来说,两个对象参考之间使用 == 作比较时,是比较其名称是否参考至同一对象,而不是比较其内容。
Java中的「条件运算子」 (Conditional operator),它的使用方式如下:
条件式 ? 成立传回值 : 失败传回值
import java.util.Scanner; public class UseConditionOperator { public static void main(String[] args){ Scanner scanner = new Scanner(System.in); System.out.print("Input a number:"); int inputedNumber = scanner.nextInt(); System.out.println(inputedNumber + "是不是奇数?" + (inputedNumber%2 == 0 ? '否' : '是')); } }
8、逻辑、位运算
ps:原网站上说的补码符~,实际是反码。
显示各个运算的结果:
public class BitwiseOperator { public static void main(String[] args) { System.out.println("AND运算:"); System.out.println("0 AND 0\t\t" + (0 & 0)); System.out.println("0 AND 1\t\t" + (0 & 1)); System.out.println("1 AND 0\t\t" + (1 & 0)); System.out.println("1 AND 1\t\t" + (1 & 1)); System.out.println("\nOR运算:"); System.out.println("0 OR 0\t\t" + (0 | 0)); System.out.println("0 OR 1\t\t" + (0 | 1)); System.out.println("1 OR 0\t\t" + (1 | 0)); System.out.println("1 OR 1\t\t" + (1 | 1)); System.out.println("\nXOR运算:"); System.out.println("0 XOR 0\t\t" + (0 ^ 0)); System.out.println("0 XOR 1\t\t" + (0 ^ 1)); System.out.println("1 XOR 0\t\t" + (1 ^ 0)); System.out.println("1 XOR 1\t\t" + (1 ^ 1)); } }
9、递增、递减运算符【略】
10、if语句【略】
11、switch语句【略】
12、for语句【略】
13、while,do-while语句【略】
14、break、continue语句【略】
15、装箱boxing、拆箱unboxing
自动装箱运用的方法如下:
int i = 10; Integer integer = i;
也可以使用更一般化的Number,例如:
Number number = 3.14f;
3.14f会先被自动装箱为Float,然后指定给number。
J2SE 5.0中可以自动装箱,也可以自动拆箱(Unboxing),例如下面这样写是可以的:
Integer i = 10; System.out.println(i + 10); System.out.println(i++);
上例中会显示20与10,编译器会自动帮您进行自动装箱与拆箱,即10会先被装箱,然后在i + 10时会先拆箱,进行加法运算;i++该行也是先拆箱再进行递增运算。再来看一个例子:
Boolean boo = true; System.out.println(boo && false);
同样的,先将boo拆箱,再与false进行AND运算,结果会显示false。
16、一维数组
在Java中可以这么宣告一个数组并初始数组内容:
int[] score = {90, 85, 55, 94, 77};
上面这个程序片段宣告了一个score数组,它的内容包括90、85、55、94与77这五个元素,我们要存取数组时,必须使用索引来指定存取数组中的哪个元素,在Java中,数组的索引是由0开始,也就是说索引0的位置储存90、索引1的位置储存85、索引2的位置储存55,依此类推。
下面这个程序可以让您使用for循环来取出数组中的元素值:
public class ScoreArray { public static void main(String[] args) { int[] score = {90, 85, 55, 94, 77}; for(int i = 0; i < score.length; i++) System.out.print(score[i] + " "); System.out.println(); } }
在这个程序中,您使用了length这个属性成员,这边涉及到一个观念,在Java中,数组是一个对象,而不是单纯的数据集合,当您宣告一个数组时,其实就是在配置一个数组对象,上面的程序只是数组宣告与初始化成员的一个简易宣告方式,数组对象的length属性成员可以取回这个数组的长度,也就是元素个数。
其实在Java中,对象都是以new来配置内存空间,数组的使用也不例外,一个完整的数组宣告方式如下所示:
int[] arr = new int[10];
在上面的宣告中,会为arr配置10个整数的数组元素,索引为0到9,初始值预设为0,在Java中配置数组之后,若还没有指定初值,则依数据型态的不同,会预设有不同的初值,如下所示:
数据型态 | 初值 |
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | \u0000 |
boolean | false |
由于数组的内存空间是使用new配置而来,这意味着您也可以使用动态的方式来宣告数组长度,而不用在程序中事先决定数组大小,例如:
import java.util.Scanner; public class DynamicAnnounceArray { public static void main(String[] args){ Scanner scanner = new Scanner(System.in); System.out.print("Input the size of Array:"); int length = scanner.nextInt(); int[] arr = new int[length]; System.out.println("The contents of Array:"); for(int i=0;i<arr.length;i++){ System.out.printf("%3d",arr[i]); } } }
17、二维数组、不规则数组
public class TwoDimArray { public static void main(String[] args){ int[][] arr = {{1,2,3}, {4,5,6}}; for (int i = 0; i<arr.length; i++ ){ for (int j = 0; j<arr[0].length; j++){ System.out.printf("%3d",arr[i][j]); } System.out.println(" "); } } }
在使用二维数组对象时,注意length所代表的长度,数组名直接加上length,所指的是有几列(Row),而使用索引加上length,指的是该列所拥有的元素,也就是行(Column)数目,要了解为何是这样,必须从对象配置的角度来说明。
以对象的方式来配置一个二维数组对象,您使用以下的语法:
int[][] arr = new int[2][3];
在上面的程序片段中,其实arr[0]、arr[1]是两个一维数组对象,其长度为3,而arr的型态为int[][],内容值为arr[0]与arr[1],其关系如下:
从上图中您可以看到,arr名称参考至int[][]型态的对象,而arr[0]与arr[1]再分别参考至一个一维数组对象,所以这也就是为何上面的程序中,使 用的length所表示的长度意义,您也可以知道,在Java中,一个二维数组的配置,各个元素的内存位置并不是连续的。
同样的道理,您也可以宣告三维以上的数组,如果要宣告同时初始元素值,可以使用以下的语法:
int[][][] arr = { {{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}} };
不规则数组。数组的维度不一定要是四四方方的,您也可以制作一个二维数组,而每个维度的长度并不相同,例如:
public class TwoDimArray { public static void main(String[] args) { int arr[][]; arr = new int[2][]; arr[0] = new int[3]; arr[1] = new int[5]; for(int i = 0; i < arr.length; i++) { for(int j = 0; j < arr[i].length; j++) arr[i][j] = j + 1; } for(int i = 0; i < arr.length; i++) { for(int j = 0; j < arr[i].length; j++) System.out.print(arr[i][j] + " "); System.out.println(); } } }
18、慎用boxing
自动装箱与拆箱是编译器在编译时期为您作好一切的事情,是编译蜜糖(Compiler sugar),这很方便,但在运行阶段您还是了解Java的语义,例如下面的程序是可以通过编译的:
Integer i = null; int j = i;
语法是在编译时期是合法的,但是在运行时期会有错误,因为null表示 i 没有参考至任何的对象实体,它可以合法的指定给对象参考名称,但null值对于基本型态 j 的指定是不合法的,上面的写法在运行时会出现NullPointerException的错误。
再来看一个,先看看程序,您以为结果是如何?
Integer i1 = 100; Integer i2 = 100; if (i1 == i2) System.out.println("i1 == i2"); else System.out.println("i1 != i2");
以自动装箱与拆箱的机制来看,我想您会觉得结果是显示"i1 == i2",您是对的!那么下面这个您觉得结果是什么?
Integer i1 = 200; Integer i2 = 200; if (i1 == i2) System.out.println("i1 == i2"); else System.out.println("i1 != i2");
结果是显示"i1 != i2",这有些令人讶异,语法完全一样,只不过改个数值而已,结果却相反。
其实这与'=='运算子的比较有关,'=='可用来比较两个基本型态的变量值是否相等,事实上'=='也用于判断两个对象变量名称是否参考至同一个对象。
所以'=='可以比较两个基本型态的变量值是否相等,也可以判断两个对象变量的参考对象是否相同,当您如前两个程序的方式撰写时,编译器不知道您实际上要比较的是哪一种?所以对于值从-128到127之间的值,它们被装箱为Integer对象后,会存在内存之中被重用,所以当值在100,使用'=='进行比较时,i1 与 i2实际上参考至同一个对象。
如果超过了从-128到127之间的值,被装箱后的Integer对象并不会被重用,即相当于每次都新建一个Integer对象,所以当值在 200,使用'=='进行比较时,i1与i2参考的是不同的对象。
所以不要过份依赖自动装箱与拆箱,您还是必须知道基本型态与对象的差异,上面的程序最好还是依正规的方式来写,而不是依赖编译蜜糖(Compiler sugar),例如当值为200时,必须改写为以下才是正确的。
Integer i1 = 200; Integer i2 = 200; if (i1.equals(i2)) System.out.println("i1 == i2"); else System.out.println("i1 != i2");
结果这次是显示"i1 == i2"了,使用这样的写法,相信您也会比较放心一些,总之一个原则:如果您不确定就不要用。
19、数组复制
可以使用System.arraycopy()方法来进行数组复制,这个方式必须明确自行新建立一个数组对象。在JDK 6中,Arrays 类别 新增了copyOf()方法,可以直接传回一个新的数组对象,而当中包括复制的内容,例如:
import java.util.Arrays; public class ArrayCopy { public static void main(String[] args){ int[] arr1 = {1,2,3,4,5}; int[] arr2 = new int[5]; int[] arr3 = Arrays.copyOf(arr1,arr1.length); System.arraycopy(arr1, 0, arr2, 0, arr1.length); for (int i=0; i<arr1.length; i++){ System.out.printf("%3d", arr1[i]); } System.out.println(" "); for (int j=0; j<arr2.length; j++){ System.out.printf("%3d",arr2[j]); } System.out.println(" "); for (int j=0; j<arr3.length; j++){ System.out.printf("%3d",arr3[j]); } } }
Arrays的copyOf()方法传回的数组是新的数组对象,所以您改变传回数组中的元素值,也不会影响原来的数组。
copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值(超出部分为0、0L...)。
20、Arrarys类
数组中基本操作的排序、搜寻与比较等动作是很常见的,在Java中提供了Arrays类别可以协助您作这几个动作,Arrays类别位于java.util套件中,它提供了几个静态方法可以直接呼叫使用。
sort() |
这个方法可以帮助您对指定的数组排序,所使用的是快速排序法 |
binarySearch() |
这个方法可以让您对已排序的数组进行二元搜寻,如果找到指定的值就传回该值所 在的索引,否则就么回负值 |
fill() |
当我们配置一个数组之后,其会依数据型态来给定默认值,例如整数数组就初始为 0,您可以使用Arrays.fill()方法来将所有的元素设定为指定的值 |
equals() |
比较两个数组中的元素值是否全部相等,如果是将传回true,否则传回 false |
下面这个程序示范数组的排序与搜寻:
import java.util.Arrays; import java.util.Scanner; public class UseArrays { public static void main(String[] args){ Scanner scanner = new Scanner(System.in); int[] arr = {93, 5, 3, 55, 57, 7, 2, 73, 41, 91}; System.out.print("排序前:"); for (int i=0; i<arr.length; i++){ System.out.print(arr[i]+" "); } System.out.println(); Arrays.sort(arr); System.out.print("排序后:"); for (int i=0; i<arr.length; i++){ System.out.print(arr[i]+" "); } System.out.println(); System.out.print("输入搜索值:"); int key = scanner.nextInt(); int find = -1; if((find = Arrays.binarySearch(arr,key))>-1){ System.out.println("找到值于索引"+find+"位置。"); }else{ System.out.println("找不到指定值。"); } } }
下面这个程序示范数组的填充与比较:
import java.util.Arrays; public class UseArrays2 { public static void main(String[] args){ int[] arr1 = new int[10]; int[] arr2 = new int[10]; int[] arr3 = new int[10]; Arrays.fill(arr1, 5); Arrays.fill(arr2, 5); Arrays.fill(arr3, 10); System.out.print("arr1 :"); for (int i=0; i<arr1.length; i++){ System.out.print(arr1[i]+" "); } System.out.println("\narr1 = arr2 ? " + Arrays.equals(arr1, arr2)); System.out.println("arr1 = arr3 ? " + Arrays.equals(arr1, arr3)); } }
请注意到,您不可以用==来比较两个数组的元素值是否相等,==使用于对象比对时,是用来比对两个对象名称是否参考至同一个对象,下面这个程序是个简单的 示范:
public class TestEqual { public static void main(String[] args){ int[] arr1 = new int[5]; int[] arr2 = new int[5]; int[] tmp = arr1; System.out.println(arr1 == tmp); System.out.println(arr2 == tmp); } }
事实上,J2SE 5.0 对Arrays类别作了不少的修改与新增,由此可见数组操作在程序中的重要性,这边介绍Arrays新增的两个方法:deepEquals()与deepToString()。
deepEquals() |
对数组作深层比较,简单的说,您可以对二维仍至三维以上的数组进行比较是否相 等 |
deepToString() |
将数组值作深层输出,简单的说,您可以对二维仍至三维以上的数组输出其字符串值 |
直接来看个程序比较清楚:
import java.util.Arrays; public class UseArrays { public static void main(String args[]) { int[][] arr1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int[][] arr2 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int[][] arr3 = {{0, 1, 3}, {4, 6, 4}, {7, 8, 9}}; System.out.println("arr1 equals arr2? " + Arrays.deepEquals(arr1, arr2)); System.out.println("arr1 equals arr3? " + Arrays.deepEquals(arr1, arr3)); System.out.println("arr1 deepToString()\n\t" + Arrays.deepToString(arr1)); } }
执行结果:
arr1 equals arr2? true arr1 equals arr3? false arr1 deepToString() [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
如果您有数组操作方面的相关需求,可以先查查 java.util.Arrays 的API文件。
21、进阶的数组观念
藉由对数组对象的进一步探讨,您可以稍微了解Java对对象处理的一些作法,首先来看看一个一维数组参考名称的宣告:
int[] arr;
在这个宣告中,arr表示一个可以参考至一维数组对象的名称,但是目前还没有指定它参考至任何的对象,在Java中,=运算子用于基本数据型态时,是 将值复制给变量,但当它用于对象时,则是将对象指定给参考名称来参考,您也可以将同一个对象指定给两个参考名称,当对象的值藉由其中一个参考名称变更 时,另一个参考名称所参考到的值也会更动。
在宣告int[] arr之后,arr表示它是一个一维数组对象的参考名称,所以它可以参考至任何长度的一维数组对象。
您了解到在Java中数组是一个对象,而使用 = 指定时是将对象指定给数组名来参考,而不是数组复制,如果您想将整个数组的值复制给另一个数组该如何作呢?您可以使用循环,将整个数组的元素值走访一遍,并指定给另一个数组相对应的索引位置,例如下面这个例子:
public class AdvancedArray { public static void main(String[] args) { int[] arr1 = {1, 2, 3, 4, 5}; int[] arr2 = new int[5]; for(int i = 0; i < arr1.length; i++) arr2[i] = arr1[i]; for(int i = 0; i < arr2.length; i++) System.out.print(arr2[i] + " "); System.out.println(); } }
另一个更简便的方法是使用System类别所提供的静态方法arraycopy(),其语法如下:
System.arraycopy(来源, 起始索引, 目的, 起始索引, 复制长度);
22、对象数组
就如同Java的基本数据型态可以宣告为数组,Java中的对象也可以使用数组来加以管理,但两者的索引存取意义有些不同。
使用Java的基本数据型态来宣告数组,每一个数组的索引位置都可以储存一个数值,这不用怀疑,但是如果是对象数组,其每一个索引位置是用来参考至一个对象,例如宣告一个字符串对象数组:
String[] names = {"caterpillar", "momor", "beckyday","bush"};
在这个例子中,names中的每个索引位置其真正意义为参考,如下所示:
names[0] => 参考至"caterpillar"对象
names[1] => 参考至"momor"对象
names[2] => 参考至"beckyday"对象
names[3] => 参考至"bush"对象
如果您作了下面的指定:
names[1] = names[2];
则索引位置的参考会变成如下:
names[0] => 参考至"caterpillar"对象
names[1] => 都参考至"beckyday"对象 <= names[2]
names[3] => 参考至"bush"对象
如果是基本数据型态的话,就不是如此,例如:
int arr[] = {1, 2, 3, 4}; arr[1] =arr[2];
经过以上的宣告与指定后,arr[2]的值会复制给arr[1],也就是说它们两个拥有各自的值,虽然arr[1]的值等于3,arr[2]的值也等于 3,但储存在不同的内存位置,彼此不相互干扰。
23、foreach语句
foreach的语法,它可以应用于数组的循序存取上,其语法如下:
for(type element : array) { System.out.println(element).... }
例如循序存取一个数组的方式如下:
int[] arr = {1, 2, 3, 4, 5}; for(int element : arr) System.out.println(element);
注意:element的宣告必须在for()之中,例如以下就无法通过编译:
int[] arr = {1, 2, 3, 4, 5}; int element = 0; for(element : arr) System.out.println(element);
那么二维数组呢?基本上您要是了解数组本身就是个对象,您自然就会知道如何存取,举个例子:
int[][] arr = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; for(int[] row : arr) for(int element : row) System.out.println(element);
24、String类
由字符所组成的一串文字符号,称之为字符串,在Java中字符串可以使用String类别来建构,例如您可以使用以下的方式来宣告并初始一个字符串变量:
String text = "字符串的使用";
注意字符串的直接指定必须使用"",而字符串是使用Unicode字符来建构,在建构一个字符串对象变量之后,您可以直接在输出串流中指定变量名称来输出字符串,例如:
System.out.println(text);
字符串的串接在Java中可以直接使用 + 运算子,+ 本来是加法运算子,而它被重载(Override)为可以直接用于字符串的串接,例如您可以这么使用字符串:
String msg = "哈啰!"; msg = msg + "Java Programming!"; System.out.println(msg);
这一段程序代码会在主控台上显示 "哈啰!Java Programming!"。
用于生成字符串对象的String类别拥有几个操作字符串的方法,在这边先介绍几个常用的:
length() |
取得字符串的字符长度 |
equals() |
判断原字符串中的字符是否相等于指定字符串中的字符 |
toLowerCase() |
转换字符串中的英文字符为小写 |
toUpperCase() |
转换字符串中的英文字符为大写 |
下面这个程序介绍以上的几个操作字符串方法的使用与结果:
public class UseString { public static void main(String[] args){ String text = "Hello"; System.out.println("字符串内容:"+text); System.out.println("字符串长度:"+text.length()); System.out.println("等于Hello?" + text.equals("Hello")); System.out.println("转换为大写:"+text.toUpperCase()); System.out.println("转换为小写:"+text.toLowerCase()); } }
如果您要将输入的字符串转换为整数、浮点数等等数据型态,您可以使用以下各类别所提供的各个静态剖析方法:
Byte.parseByte(字符串) |
将字符串剖析为位 |
Short.parseShort(字符串) |
将字符串剖析为short整数 |
Integer.parseInt(字符串) |
将字符串剖析为integer整数 |
Long.parseLong(字符串) |
将字符串剖析为long整数 |
Float.parseFloat(字符串) |
将字符串剖析为float浮点数 |
Double.parseDouble(字符串) |
将字符串剖析为double浮点数 |
注意如果指定的字符串无法剖析为指定的数据型态数值,则会发生NumberFormatException例外。
之前宣告字符串时,都是以这样的样子来宣告:
String str = "caterpillar";
这样的宣告方式看来像是基本数据型态宣告,但事实上String并不是Java的基本数据型态,String是java.lang套件下所提供的类别,如果以配置对象的观念来宣告字符串,应该是这样的:
String str = new String("caterpillar");
不过事实上它与下面这段是有差别的:
String str = "caterpillar";
一个字符串其实是由字符数组所组成,所以使用String类别宣告字符串后,该字符串会具有数组索引的性质,以下介绍几个与索引相关的方法:
char charAt(int index) |
传回指定索引处的字符 |
int indexOf(int ch) |
传回指定字符第一个找到的索引位置 |
int indexOf(String str) |
传回指定字符串第一个找到的索引位置 |
int lastIndexOf(int ch) |
传回指定字符最后一个找到的索引位置 |
String substring(int beginIndex) |
取出指定索引处至字符串尾端的子字符串 |
String substring(int beginIndex, int endIndex) |
取出指定索引范围子字符串 |
char[] toCharArray() |
将字符串转换为字符Array |
下面这个程序是个简单的示范:
public class UseString { public static void main(String[] args) { String text = "Your left brain has nothing right.\n" + "Your right brain has nothing left.\n"; System.out.println("字符串内容: "); for(int i = 0; i < text.length(); i++) System.out.print(text.charAt(i)); System.out.println("\n第一个left: " + text.indexOf("left")); System.out.println("最后一个left: " + text.lastIndexOf("left")); char[] charArr = text.toCharArray(); System.out.println("\n字符Array内容: "); for(int i = 0; i < charArr.length; i++) System.out.print(charArr[i]); } }
在建构字符串对象时,除了直接指定字符串常数之外,您也可以使用字符数组来建构,例如:
char[] name = {'c', 'a', 't', 'e', 'r', 'p', 'i', 'l', 'l', 'a', 'r'}; String str = new String(name);
上面这个程序片段使用字符数组name,建构出一个内容为"caterpillar"的字符串。
除了以上所介绍的几个方法之外,您应该查查API手册,了解更多有关于String类别的方法,例如我们可以使用endsWith()方法来过滤文件名称,下面这个程序过滤出档案类型为jpg的档案:
public class UseString { public static void main(String[] args) { String[] filenames = {"caterpillar.jpg", "cater.gif", "bush.jpg", "wuwu.jpg", "clockman.gif"}; System.out.print("过滤出jpg档案: "); for(int i = 0; i < filenames.length; i++) if(filenames[i].endsWith("jpg")) System.out.print(filenames[i] + " "); System.out.println(""); } }
25、不可变的(immutable)字符串
一个字符串对象一旦被配置,它的内容就是固定不可变的(immutable),例如下面这个宣告:
String str = "caterpillar";
这个宣告会配置一个长度为11的字符串对象,您无法改变它的内容;别以为下面这个宣告就是改变一个字符串对象的内容:
String str = "just";
str = "justin";
事实上,在这个程序片段中,会有两个字符串对象,一个是"just",长度为4,一个是"justin",长度为6,它们两个是不同的字符串对象,您并不是在 "just"字符串后加上"in"字符串,而是让str名称参考至新的字符串对象,如下所示:
原来参考至此
str --------> "just"
重新指定后
"just" 等待回收
str ---------> "justin"
参考新的字符串对象
在Java中,使用 = 将一个字符串对象指定给一个名称,其意义为改变名称的参考对象,原来的字符串对象若没有其它名称来参考它,就会在适当的时机被Java的「垃圾回收」(Garbage collection)机制回收,在Java中,程序设计人员通常不用关心无用对象的资源释放问题,Java会检查对象是否不再被参考,如果没有任何名称参考的对象将会被回收。
如果您在程序中使用下面的方式来宣告,则实际上是指向同一个字符串对象:
String str1 = "flyweight"; String str2 = "flyweight"; System.out.println(str1 == str2);
程序的执行结果会显示true,在Java中,会维护一个String Pool,对于一些可以共享的字符串对象,会先在String Pool中查找是否存在相同的String内容(字符相同),如果有就直接传回,而不是直接创造一个新的String对象,以减少内存的耗用。
再来个一看例子,String的intern()方法,来看看它的API说明的节录:
Returns a canonical representation for the string object.
A pool of strings, initially empty, is maintained privately by the class String.
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.
这段话其实说明了 Flyweight 模式 的运作方式,来用个实例来说明会更清楚:
public class StringIntern { public static void main(String[] args) { String str1 = "fly"; String str2 = "weight"; String str3 = "flyweight"; String str4; str4 = str1 + str2; System.out.println(str3 == str4); str4 = (str1 + str2).intern(); System.out.println(str3 == str4); } }
在程序中第一次比较str3与str4对象是否为同一对象时,您知道结果会是false,而intern()方法会先检查 String Pool中是否存在字符部份相同的字符串对象,如果有的话就传回,由于程序中之前已经有"flyweight"字符串对象,intern()在String Pool中发现了它,所以直接传回,这时再进行比较,str3与str4所指向的其实是同一对象,所以结果会是true。
注意到了吗?== 运算在Java中被用来比较两个名称是否参考至同一对象,所以不可以用==来比较两个字符串的内容是否相同,例如:
String str1 = new String("caterpillar"); String str2 = new String("caterpillar"); System.out.println(str1 == str2);
上面会显示false的结果,因为str1与str2是分别参考至不同的字符串对象,如果要比较两个(字符串)对象是否相同,您要使用equals()方法,例如:
String str1 = new String("caterpillar"); String str2 = new String("caterpillar"); System.out.println(str1.equals(str2));
26、分离字符串
将字符串依所设定的条件予以分离是很常见的操作,例如指令的分离,文本文件的数据读出等等,以后者而言,当您在文本文件中储存以下的资料时,在读入档案后,将可以使用String的split()来协助每一格的资料分离。
假设在文本文件中有如下的内容,每笔数据中是以tab分开:
cater 64/5/26 093900230 25433343
momor 67/3/26 0939123456 5434233
下面这个程序是一个简单的范例,假设String对象的数据就是档案中的一行文字数据:
public class StringSplit { public static void main(String[] args){ String strOfReaded1 = "cater\t64/5/26\t0939002302\t5433343"; String[] tokens = strOfReaded1.split("\t"); for (String token : tokens){ System.out.print(token+"<token>"); } System.out.println(); } }
执行结果:
cater<token>64/5/26<token>0939002302<token>5433343<token>
split()依您所设定的分隔设定,将字符串分为数个子字符串并以String数组传回。
27、使用正则表达式(Regular expression)
正则表示式最早是由数学家Stephen Kleene于1956年提出,主要使用在字符字符串的格式比对,后来在信息领域广为应用,现在已经成为ISO(国际标准组织)的标准之一。
您可以在API文件的 java.util.regex.Pattern 类别中找到支持的正则表示式相关信息。
如果您使用String类别来配置字符串对象,您可以使用简易的方法来使用正则表示式,并应用于字符串的比对或取代等动作上,以下先介绍几个简单的正则表示式。
例如一些常用的范围,我们可以使用预先定义的字符类别:
. |
符合任一字符 |
\d |
等于 [0-9] 数字 |
\D |
等于 [^0-9] 非数字 |
\s |
等于 [ \t\n\x0B\f\r] 空格符 |
\S |
等于 [^ \t\n\x0B\f\r] 非空格符 |
\w |
等于 [a-zA-Z_0-9] 数字或是英文字 |
\W |
等于 [^a-zA-Z_0-9] 非数字与英文字 |
符合任一字符。例如有一字符串abcdebcadxbc,使用.bc来比对的话,符合的子字符串有abc、ebc、xbc三个;如果使用..cd,则符合的子字符串只有abcd。
以上的例子来根据字符比对,您也可以使用「字符类」(Character class)来比较一组字符范围,例如:
[abc] |
a、b或c |
[^abc] |
非a、b、c的其它字符 |
[a-zA-Z] |
a到z或A到Z(范围) |
[a-d[m-p]] |
a到d或m到p(联集) |
[a-z&&[def]] |
d、e或f(交集) |
[a-z&&[^bc]] |
a到z,除了b与c之外(减集) |
[a-z&&[^m-p]] |
a到z且没有m到p(a-lq-z)(减集) |
一次只指定一个字符不过瘾,也可以用Greedy quantifiers来指定字符可能出现的次数:
X? |
X出现一次或完全没有 |
X* |
X出现零次或多次 |
X+ |
X出现一次或多次 |
X{n} |
X出现n次 |
X{n,} |
X出现至少n次 |
X{n,m} |
X出现至少n次,但不超过m次 |
另外还有Reluctant quantifiers、Possessive quantifiers等的指定,您可以自行参考 java.util.regex.Pattern 类别中的说明。
在String类别中,
matches()方法可以让您验证字符串是否符合指定的正规表示式,这通常用于验证使用者输入的字符串数据是否正确,例如 电话号码格式;
replaceAll()方法可以将符合正规表示式的子字符串置换为指定的字符串;
split()方法可以让您依指定的正规表示式,将符合的子 字符串分离出来,并以字符串数组传回。
import java.util.Scanner; public class UseRegularExpression { public static void print(String str){ System.out.print(str); } public static void println(String str){ System.out.println(str); } public static void main(String[] args){ Scanner scanner = new Scanner(System.in); String str = "abcdefgabcabc"; println(str.replaceAll(".bc", "###")); print("请输入手机号:"); str = scanner.next(); //简单格式验证 if(str.matches("[0-9]{4}-[0-9]{6}")){ println("格式正确。"); }else{ println("格式错误。"); } print("输入href标签:"); //Scanner.next()以空白区隔 str = scanner.next()+" "+scanner.next(); //验证href标签 if(str.matches("<a.+href*=*['\"]?.*?['\"]?.*?>")){ println("格式正确。"); }else{ println("格式错误。"); } print("输入电子邮件:"); str = scanner.next(); //验证电邮格式 if(str.matches("^[_a-z0-9-]+([.][_a-z0-9-]+)*@[a-z0-9-]+([.][a-z0-9-]+)*$")){ println("格式正确"); }else{ println("格式错误"); } } }
28、Pattern、Matcher
String上的正则表示式,实际上是利用了Pattern与Matcher的功能,当您呼叫String的matches()方法时,实际上是呼叫Pattern的静态(static)方法matches(),这个方法会传回boolean值,表示字符串是否符合正则表示式。
如果您想要重复使用您的正则表示式,则您可以使用Pattern的静态方法compile()进行编译,它会传回一个Pattern的实例,代表您的正则表示式,之后您就可以重复使用这个实例的matcher()方法来进行字符串比对,这个方法会传回一个Matcher的实例, Matcher上有一些寻找符合正则式条件的方法可供操作。
import java.util.regex.*; public class UsePatternMather { public static void main(String[] args){ String phones1 = "Justin's phone number: 0939-100391\n" + "momor's phone number: 0939-666888\n"; Pattern pattern = Pattern.compile(".*0939-\\d{6}"); Matcher matcher = pattern.matcher(phones1); while(matcher.find()){ System.out.println(matcher.group()); } String phones2 = "caterpillar's phone number: 0952-600391\n" + "bush's phone number: 0939-550391"; matcher = pattern.matcher(phones2); while(matcher.find()){ System.out.println(matcher.group()); } } }
这个程序会寻找手机号码为0939开头的号码,假设您的号码来源不只一个(如phones1、phones2),我们可以编译好正则表示式并传回一个 Pattern对象,之后就可以重复使用。
29、StringBuilder 类别
一个String对象的长度是固定的,您不能改变它的内容,或者是附加新的字符至String对象中。
在 J2SE 5.0 提供StringBuilder类别,使用这个类别所产生的对象预设会有16个字符的长度,您也可以自行指定初始长度,如果附加至对象的字符超出可容纳的长度,则StringBuilder对象会自动增加长度。
在StringBuilder中,length()可传回目前对象中的字符长度,而capacity()可传回该对象目前可容纳的字符容量,下面这个程序是个简单的示范:
public class UseStringBuilder { public static void main(String[] args) { StringBuilder strBuilder = new StringBuilder("Knowledge is power!"); System.out.println("内容: " + strBuilder); System.out.println("长度: " + strBuilder.length()); System.out.println("容量: " + strBuilder.capacity()); } }
执行结果:
内容: Knowledge is power! 长度: 19 容量: 35
StringBuilder拥有几个操作字符串的方法,例如insert()方法可以将字符插入指定的位置,如果该位置以后有字符,则将所有的字符往后移,deleteChar()方法可以删除指定位置的字符,而reserve()方法可以反转字符串,详细的使用可以查询看看 java.lang.StringBuilder 的API说明。
您可能会问 java.lang.StringBuffer 呢?事实上,StringBuilder被设计为与StringBuffer相同的操作接口,但不考虑多执行绪下同步的问题,所以在单执行绪下,您可以将以前使用StringBuffer撰写的程序,通通换为StringBuilder而仍可以运作,并可以获得较好的效能;如果您的程序是在多执行绪下操作,则可以使用StringBuffer,让这个类别自行管理同步问题。【执行绪——线程】
30、命令行参数(Command line argument)
在使用主控台启动一个Java程序时,我们可以一并指定一些参数,以让程序进行相对应的功能,例如:
$java 类别名称 -compare a.java b.java
像这样的功能,您可以使用命令列自变量(Command line argument)来达到,在我们撰写主程序时,会在自变量列撰写String[] args,它就是用来接受一个自变量指定的字符串数组,您只要使用索引取出args中的元素值,就可以取出程序运行时的参数,下面这个程序是个简单的示范:
public class CommandLineArg { public static void main(String[] args) { System.out.print("读入的自变量: "); for(int i = 0; i < args.length; i++) System.out.print(args[i] + " "); System.out.println(""); } }
执行结果:
$ java CommandLineArg -d /mnt/win_d/sample/
读入的自变量: -d /mnt/win_d/sample/
args索引0的值是从程序名称后第一个自变量开始,以空白为区隔依序储存在args数组中,当然,您可以使用 J2SE 5.0 的foreach来改写上面的程序:
public class CommandLineArg { public static void main(String[] args) { System.out.print("读入的自变量: "); for(String arg : args) System.out.print(arg + " "); System.out.println(); } }
接下来介绍一些处理命令列自变量的技巧,由于命令列自变量是储存在数组中,取出这些自变量的最好方式当然就是使用for循环,而我们通常使用一个前导字符, 例如'-'来指定自变量的选项功能,由于arg是个字符数组,自然的您可以使用switch来比对前导字符,例如:
for(String arg : args) { switch(arg.charAt(0)) { case '-': // 处理参数,执行选项,例如-o、-p、-r等等 default: // 执行对应功能 } }
在判断执行选项的case中,您可以进一步检查第二个字符,例如:
switch(arg.charAt(1)) { case 'o': // 选项o的处理 break; case 'p': // 选项p的处理 break; case 'r': // 选项r的处理 break; default: // 选项错误处理或其它处理 }
以上是命令列自变量处理时的大致流程,当然不同的程序会有不同的处理方式,不过大致上不离以上的架构。
31、使用Class类型【快速浏览】
对象导向设计中,对象并不是凭空产生的,您必须先定义您的对象,您要一个规格书,这个规格书称之为类别(Class)。
在Java中使用"class"关键词来书写类别(规格书),您使用类别来定义一个对象(object)时,您考虑这个对象可能拥有的「属性」(Property,在Java中则是用Field)与「方法」(Method)。属性是参与对象内部运算的数据成员,而方法则是对象与外界互动的动态操作。
您使用类别定义出对象的规格书,之后根据这个规格书来建构对象,然后透过对象所提供的操作接口来与程序互动。
举个例子来说,您可以定义一个对象:「球」。
考虑球有各种不同的颜色(或名称),以及球最基本的球半径信息,您想到这些信息应该可以取得,并可以进一步取得球的体积,当您在Java中要定义这些信息时,您可以如下进行定义:
public class Ball { private double radius;//半径 private String name;//名称 //无参数构造函数 public Ball(){ this(0.0,"no name"); } //有参数构造函数 public Ball(double radius, String name) { this.radius = radius; this.name = name; } //获取属性 public double getRadius(){ return radius; } public String getName(){ return name; } //设置属性 public void setRadius(double radius){ this.radius = radius; } public void setName(String name){ this.name = name; } }
一个定义良好的类别,即使在不看程序代码实作的情况下,也可以从定义中所提供的公开(public)方法看出这个类别的大致功能。
在类别中的运算参与数据(Field)及互动方法(Method),我们统称其为 类别成员(Class member)。
上例中的radius、name成员是field成员,getRadius()与getName()是method成员。注意到"public"这个关键 字,它表示所定义的成员可以使用宣告的对象名称加上 '.' 运算子直接呼叫,也称之为「公用成员」或「公开成员」。而private这个关键词用来定义一个「私用成员」,它不可以透过参考名称直接呼叫,又称之为 「私有成员」。
在定义类别时,有一个基本原则是:信息的最小化公开。也就是说尽量透过方法来操作对象,而不是直接存取其内部运算参与数据(也就是field成员)。
信息的最小化公开原则是基于安全性的考虑,避免程序设计人员随意操作field成员而造成程序的错误,您可以在日后的程序设计中慢慢来体会;在稍后的实作中,您将可以看到,我们将不会radius与name两个私用成员直接进行存取,而是透过公开的方法来进行设定。
一个类别中的field成员,若宣告为"private",则其可视范围(Scope)为整个类别,由于外界无法直接存取私用成员,所以您使用两个公开方法 getRadius()与getName()分别传回其这两个成员的值。
与类别名称同名的方法称之为 建构方法 Cconstructor),也有人称之为「建构子」,它没有传回值。顾名思义,建构方法的作用是让您建构对象可以设定一些必要的建构信息,它可 以被重载(Overload),以满足对象生成时不同的设定条件。
您在实作中重载了建构方法,在不指定参数的情况下,会将radius设定为0.0,而name设定为 "no name",另一个建构方法则可以指定参数,this()方法用于对象内部,表示呼叫对象的建构方法,另一个就是this,它表示对象本身,您可以在 关于 this 进一步了解其作用。
定义好类别之后,您就可根据这个类别(规格)来建构对象,建构对象时使用new关键词,顾名思义,就是根据所指定的类别(规格书)「新建」一个对象:
Ball ball1 = new Ball(); Ball ball2 = new Ball(3.5, "red ball");
在上例中配置了ball1与ball2两个对象,ball1对象在建立时并不指定任何参数,所以根据之前对Ball类别的定义,ball1的radius 将设定为0.0,name设定为"no name";ball2则给定两个参数,所以ball2的radius设定为3.5,而ball2的name设定为"red ball"。
您可以透过公开成员来操作对象或取得对象信息,方法是使用对象名称加上「.」运算子,例如:
ball1.getRadius(0.1);
ball1.setName("GBall");
以下先看个简单的程序:
public class SimpleClass { public static void main(String[] args) { Ball b1 = new Ball(18.4, "red ball"); System.out.println("名称: " + b1.getName()); System.out.println("半径: " + b1.getRadius()); } }
类别与对象这两个名词会经常混于书籍与文件之中,例如「您可以使用Scanner类别」、「您可以使用Scanner对象」,这两句在某些场合其语义是相 同的,不过要细究的话,两句的意思通常都是「您可以使用根据Scanner类别所建构出来的对象」,不过写这么长很烦,难免就省略了一些字眼。
Java会将参与内部运算的数据命名为field,其实是蛮有道理的,field在英文中有事件的参与者的意义,有限定范围的意思。基本上,在定义对象 时,field成员其作用范围要限定于对象之中,对对象内部数据的变更,都要透过公开方法来进行,避免field成员的作用范围离开了对象之外。
32、类成员
在Java中,类别的存取权限修饰词有"public"、"protected"、"private"三个,如果在宣告成员时不使用存取修饰词,则预设以套件 (package)为存取范围,也就是说在package外就无法存取,这些存取修饰,之后在 套件(package) 还会见到说明。
方法的参数列用来告知方法成员执行时所需的数据,如果传入的自变量是基本数据型态(Primitive data type),则会将值复制至参数列上的变量,如果传入的自变量是一个对象,则会将参数列上的变量参考至指定的对象。
Math.PI是由Java所提供的功能变量,它定义了圆周率3.14159......,在Math类别中还包括有许多公用的数学功能函式,您可以自行查询 java.lang.Math 在线说明文件以得知这些功能。
另外可以注意到,autoboxing、 unboxing 在方法的参数列中是可以作用的,也就是说如果您的方法中是这样设计的:
public class SomeClass { .... public void someMethod(Integer integer) { ...... } .... }
您可以使用这样的方式来设定自变量:
SomeClass someObj = new SomeClass(); someObj.someMethod(1);
autoboxing、unboxing会自动作用,但记得要小心使用这个功能。
一般在命名类别时,类别名称首字会大写,而方法名称首字是小写,名称命名时以一目了解名称的作用为原则,上面所采取的都是骆驼式的命名方式,也就是每个单字的首字予以适当的大写,例如someMethodOfSomeClass这样的方式,这是常见的一种命名惯例。
为field成员设定setXXX()或getXXX()这类的方法时,XXX名称最好与field名称相对应,例如name这个field 成员对应的方法,可以命名为setName()与getName(),而radius这个成员,则对应于setRadius()与getRadius() 这样的名称,如此阅读程序时可以一目了解setter与getter方法的存取对象。
33、static成员
![字节跳动内推](https://img2020.cnblogs.com/blog/323808/202004/323808-20200415172728571-1992447742.jpg)
找我内推: 字节跳动各种岗位
作者:
ZH奶酪(张贺)
邮箱:
cheesezh@qq.com
出处:
http://www.cnblogs.com/CheeseZH/
*
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。