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成员



posted @ 2012-11-23 09:39  ZH奶酪  阅读(3179)  评论(1编辑  收藏  举报