Loading

Java从小白到大牛—笔记

第一章 Java基础语法

1.1 标识符、关键字和保留字

1.1.1 标识符

标识符就是变量、常量、方法、枚举、类、接口等由程序员指定的名字。

命名规范:

  1. 所有的标识符都应该以字母(A-Z 或者 a-z),美元符($)、或者下划线(_)开始
  2. 首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下划线(_)或数字的任何字符组合
  3. 关键字不能用作标识符
  4. 标识符是大小写敏感的

1.1.2 关键字

关键字是类似于标识符的保留字符序列,由语言本身定义好的,不能挪作他用

private protected public abstract class extends final null
implements interface native new static strictfp synchronized true
transient volatile break continue return do while false
if else for instanceof switch case default super
try catch throw throws import package boolean this
byte char double float int long short void

1.1.3 保留字

Java中有一些字符序列既不能当作标识符使用,也不是关键字,也不能在程序中使用,这些字符序列称为保留字。Java语言中的保留字只有两个goto和const:

  1. goto:在其他语言中叫做“无限跳转”语句,在Java语言中不再使用goto语句,因为“无限跳转”语句会破坏程序结构。在Java语言中goto的替换语句可以通过break、continue和return实现“有限跳转”。
  2. const:在其他语言中是声明常量关键字,在Java语言中声明常量使用public static final 方式声明。

1.2 Java分隔符

在Java源代码中,有一些字符被用作分隔,称为分隔符。分隔符主要有:分号(;)、左右大括号({})和空白。

1.3 变量

变量和常量是构成表达式的重要部分,变量所代表的内部是可以被修改的。变量名要遵守用标识符命名规范。变量包括变量名和变量值,变量的声明格式为:

数据类型 变量名 [=初始值];

根据作用域不同分为:成员变量和局部变量。

public class HelloWorld {

    // 声明int型成员变量
    int y; ①

    public static void main(String[] args) {

        // 声明int型局部变量
        int x; ②
        // 声明float型变量并赋值
        float f = 4.5f; ③

        // x = 10;
        System.out.println("x = " + x);// 编译错误,局部变量 x未初始化 ④
        System.out.println("f = " + f);

        if (f < 10) {
            // 声明型局部变量
            int m = 5; ⑤

        }
        System.out.println(m); // 编译错误 ⑥
    }
}

1.4 常量

常量事实上是那些内容不能被修改的变量,常量与变量类似也需要初始化,即在声明常量的同时要赋予一个初始值。常量一旦初始化就不可以被修改。它的声明格式为:

final 数据类型 变量名 = 初始值;

常量有三种类型:静态常量、成员常量和局部常量。

public class HelloWorld {

    // 静态常量,替代保留字const
    public static final double PI = 3.14; ①

    // 声明成员常量
    final int y = 10;    ②

    public static void main(String[] args) {
        // 声明局部常量
        final double x = 3.3; ③
    }
}

第二章 Java编码规范

2.1 命名规范

Java编码规范命名方法采用驼峰法,下面分类说明一下。

  • 包名:包名是全小写字母,中间可以由点分隔开。作为命名空间,包名应该具有唯一性,推荐采用公司或组织域名的倒置,如com.apple.quicktime.v2。但Java核心库包名不采用域名的倒置命名,如java.awt.event。
  • 类和接口名:采用大驼峰法,如SplitViewController。
  • 文件名:采用大驼峰法,如BlockOperation.java。
  • 变量:采用小驼峰法,如studentNumber。
  • 常量名:全大写,如果是由多个单词构成,可以用下划线隔开,如YEAR和WEEK_OF_MONTH。
  • 方法名:采用小驼峰法,如balanceAccount、isButtonPressed等。

命名规范示例如下:

package com.a51work6;

public class Date extends java.util.Date {

    private static final int DEFAULT_CAPACITY = 10;

    private int size;

    public static Date valueOf(String s) {

        final int YEAR_LENGTH = 4;
        final int MONTH_LENGTH = 2;

        int firstDash;
        int secondDash;
        ...
    }

    public String toString () {
        int year = super.getYear() + 1900;
        int month = super.getMonth() + 1;
        int day = super.getDate();
        ...
    }
}

2.2 注释规范

Java中注释的语法有三种:单行注释(//)、多行注释(/.../)和文档注释(/**...*/)。

2.2.1 文件注释

文件注释就是在每一个文件开头添加注释。文件注释通常包括如下信息:版权信息、文件名、所在模块、作者信息、历史版本信息、文件内容和作用等。

下面看一个文件注释的示例:

/*
* 版权所有 2015 北京智捷东方科技有限公司
* 许可信息查看LICENSE.txt文件
* 描述:
*   实现日期基本功能
* 历史版本:
*   2015-7-22: 创建 关东升
*   2015-8-20: 添加socket库
*   2015-8-22: 添加math库
*/

2.2.2 文档注释

文档注释就是指这种注释内容能够生成API帮助文档,JDK中javadoc命令能够提取这些注释信息并生成HTML文件。文档注释主要对类(或接口)、实例变量、静态变量、实例方法和静态方法等进行注释。

提示 文档是要给别人看的帮助文档,一般注释的实例变量、静态变量、实例方法和静态方法都应该是非私有的,那些只给自己看的内容可以不用文档注释。

标签 描述
@author 作者名
@version 版本号
@since 指明需要最早使用的jdk版本
@param 参数名
@return 返回值情况
@throws 异常抛出情况

2.2.3 单行注释

单行注释示例:(//)

public class  online{
    public static void main(String[] args) {
        //这是一个单行注释
        System.out.println("Hello World!");
    }
}

2.2.4 多行注释

多行注释示例:(/.../)

public class  online{
    public static void main(String[] args) {
        /*
        这是一个多行注释
        */
        System.out.println("Hello World!");
    }
}

第三章 数据类型

基本类型表示简单的数据,基本类型分为4大类,共8种数据类型。

  • 整数类型:byte、short、int和long
  • 浮点类型:float和double
  • 字符类型:char
  • 布尔类型:boolean

3.1 整数类型

Java中整数类型包括:byte、short、int和long ,它们之间的区别仅仅是宽度和范围的不同。Java中整数都是有符号,与C不同没有无符号的整数类型。

Java的数据类型是跨平台的(与平台无关),无论你计算机是32位的还是64位的,byte类型整数都是一个字节(8位)。

整数类型 宽度 取值范围
byte 1个字节(8位) -128 — -127
short 2个字节(16位) -215 — 215 -1
int 4个字节(32位) -231 — 231 -1
long 8个字节(64位) -263 — 263 -1

3.2 浮点类型

浮点类型主要用来储存小数数值,也可以用来储存范围较大的整数。它分为浮点数(float)和双精度浮点数(double)两种,双精度浮点数所使用的内存空间比浮点数多,可表示的数值范围与精确度也比较大。

浮点类型 宽度
float 4个字节(32位)
double 8个字节(64位)

3.3 数字表示方式

3.3.1 进制数字表示

  • 二进制数:以 0b 或0B为前缀,注意0是阿拉伯数字,不要误认为是英文字母o。

  • 八进制数:以0为前缀,注意0是阿拉伯数字。

  • 十六进制数:以 0x 或0X为前缀,注意0是阿拉伯数字。

    例如下面几条语句都是表示int整数28。

    int decimalInt = 28;
    int binaryInt1 = 0b11100;
    int binaryInt2 = 0B11100;
    int octalInt = 034;
    int hexadecimalInt1 = 0x1C;
    int hexadecimalInt2 = 0X1C;
    

3.3.1 指数表示

进行数学计算时往往会用到指数表示的数值。如果采用十进制表示指数,需要使用大写或小写的e表示幂,e2表示102 。

采用十进制指数表示的浮点数示例如下:

double myMoney = 3.36e2;
double interestRate = 1.56e-2;

其中3.36e2表示的是3.36×102 ,1.56e-2表示的是1.56×10-2 。

3.4 字符类型

字符类型表示单个字符,Java中char声明字符类型,Java中的字符常量必须用单引号括起来的单个字符,如下所示:

char c = 'A';

转义字符

字符 意义 字符 意义 字符 意义
' 单引号 \t 制表符 \b 退格
" 双引号 \r 回车 \f 换页
\ 反斜杠 \n 换行 \0 空字符

3.5 布尔类型

在Java语言中声明布尔类型的关键字是boolean,它只有两个值:true和false。

提示 在C语言中布尔类型是数值类型,它有两个取值:1和0。而在Java中的布尔类型取值不能用1和0替代,也不属于数值类型,不能与int等数值类型之间进行数学计算或类型转化。

示例代码如下:

boolean isMan = true;
boolean isWoman = false;

3.6 数据类型转换

3.6.1 自动转换

自动类型转换,也称隐式类型转换,是指不需要书写代码,由系统自动完成的类型转换。由于实际开发中这样的类型转换很多,所以Java语言在设计时,没有为该操作设计语法,而是由JVM自动完成。

从存储范围小的类型到存储范围大的类型。 具体规则为:
byte→short(char)→int→long→float→double 也就是说byte类型的变量可以自动转换为short类型,示例代码:
byte b = 10;
short sh = b; 这里在赋值时,JVM首先将b的值转换为short类型,然后再赋值给sh。 在类型转换时可以跳跃。示例代码:
byte b1 = 100;
int n = b1;

注意: 在整数之间进行类型转换时,数值不发生改变,而将整数类型,特别是比较大的整数类型转换成小数类型时,由于存储方式不同,有可能存在数据精度的损失。

3.6.2 强制转换

强制类型转换,也称显式类型转换,是指必须书写代码才能完成的类型转换。该类类型转换很可能存在精度的损失,所以必须书写相应的代码,并且能够忍受该种损失时才进行该类型的转换。

示例代码:

//int型变量
int i = 10;
//把int变量i强制转换为byte
byte b = (byte) i;

3.6.3 引用数据类型

在Java中除了8种基本数据类型外,其他数据类型全部都是引用(reference)数据类型,引用数据类型用了表示复杂数据类型,包含:接口数组声明的数据类型。

第四章 运算符

4.1 算数运算符

Java中的算术运算符主要用来组织数值类型数据的算术运算,按照参加运算的操作数的不同可以分为一元运算符和二元运算符。

4.1.1 一元运算符

Java中的算术运算符主要用来组织数值类型数据的算术运算,按照参加运算的操作数的不同可以分为一元运算符和二元运算符。

运算符 名称 说明 例子
~ 取反符号 取反运算 b = ~a
++ 自加一 先取值直再加一,或先加一再取值 a++或++a
-- 自减一 先取值直再减一,或先减一再取值 a--或--a

示例代码:

int a = 12;
System.out.println(-a);            //-12
int b = a++;                        
System.out.println(b);	           //12
b = ++a;                            
System.out.println(b);             //14

4.1.2 二元运算符

元运算符包括:+、-、*、/和%,这些运算符对数值类型数据都有效。

运算符 名称 说明 例子
+ 求a加b的和,还可用String类型,进行字符串
连接操作
a + b
- 求a减b的差 a - b
* 求a乘以b的积 a * b
/ 求a除以b的商 a/ b
% 取余 求a初一b的余数 a % b

4.1.3 算数赋值运算符

算术赋值运算符只是一种简写,一般用于变量自身的变化,

运算符 名称 例子
+= 加赋值 a += b、a += b + 3
-= 减赋值 a -= b
*= 乘赋值 a *= b
/= 除赋值 a /= b
%= 取余赋值 a %= b

4.2关系运算符

关系运算是比较两个表达式大小关系的运算,它的结果是布尔类型数据,即true或false。关系运算符有6种:==、!=、>、<、>=和<=。

运算符 名称 说明 例子
== 等于 a等于b时返回true,否则返回false。可以应用于基本
数据类型和引用数据类型
a == b
!= 不等于 与=相反 a != b
> 大于 a大于b时返回true,否则返回false,只应用于基本数
据类型
a > b
< 小于 a小于b时返回true,否则返回false,只应用于基本数
据类型
a < b
>= 大于等于 a大于等于b时返回true,否则返回false,只应用于基
本数据类型
a >= b
<= 小于等于 a小于等于b时返回true,否则返回false,只应用于基
本数据类型
a <= b

4.3 逻辑运算符

逻辑运算符是对布尔型变量进行运算,其结果也是布尔型。

运算符 名称 说明 例子
& 逻辑与 ab全为true时,计算结果为true,否则为false a & b
| 逻辑或 ab全为false时,计算结果为false,否则为true a | b
逻辑非 a为true时,值为false,a为false时,值为true !a
&& 短路与 ab全为true时,计算结果为true,否则为false。&&与&区
别:如果a为false,则不计算b(因为不论b为何值,结果
都为false)
a && b
|| 短路或 ab全为false时,计算结果为false,否则为true。II与区别:
如果a为true,则不计算b(因为不论b为何值,结果都为
true)
a || b

提示 短路与(&&)和短路或(||)能够采用最优化的计算方式,从而提高效率。在实际编程时,应该优先考虑使用短路与和短路或。

4.4 位运算符

位运算是以二进位(bit)为单位进行运算的,操作数和结果都是整型数据。位运算符有如下几个运算符:&、|、^、~、>>、<<和>>>,以及相应的赋值运算符

运算符 名称 例子 说明
~ 位反 -x 将x的只按位取反
& 位与 x & y x与y位进行位与运算
| 位或 x | y x与y位进行位或运算
^ 位异或 x ^ y x与y位进行位异或运算
>> 有符号又移 x >> a x右移a位,高位采用符号位补位
<< 左移 x << a x左移a位,低位用0补位
>>> 无符号右移 x >>> a x右移a位,低位用0补位
&= 位于等于 a &= b 等价于a = a & b
|= 位或等于 a |= b 等价于 a = a | b
^= 位异或等于 a ^= b 等价于 a = a ^ b
<<= 左移等于 a <<= b 等价于 a = a << b
>>= 右移等于 a >>= b 等价于 a = a >> b
>>>= 右移等于 a >>>= b 等价于 a = a >>> b

注意 无符号右移>>>运算符仅被允许用在int和long整数类型, 如果用于short或byte数据, 则数据在位移之前,转换为int类型后再进行位移计算。

4.5 其他运算符

  • 三元运算符(? :)。例如x?y:z;,其中x、y和z都为表达式。

  • 小括号。起到改变表达式运算顺序的作用,它的优先级最高。

  • 中括号。数组下标。

  • 引用号(.)。对象调用实例变量或实例方法的操作符,也是类调用静态变量或静态方法的操作符。

  • 赋值号(=)。赋值是用等号运算符(=)进行的。

  • instanceof。判断某个对象是否为属于某个类。

  • new。对象内存分配运算符。

  • 箭头(->)。Java 8新增加的,用来声明Lambda表达式。

  • 双冒号(::)。Java 8新增加的,用于Lambda表达式中方法的引用。

    示例代码如下:

    import java.util.Date;
    
    public class HelloWorld {
    
        public static void main(String[] args) {
    
            int score = 80;
            String result = score > 60 ? "及格" : "不及格"; // 三元运算符(? : )
            System.out.println(result);
    
            Date date = new Date();                 // new运算符可以创建Date对象
            System.out.println(date.toString());    //通过.运算符调用方法
    
        }
    }
    

4.6 运算符优先级

优先级 运算符
1 .(引用号)小括号中括号
2 ++--(数值取反)~(位反)!(逻辑非)类型转换小括号
3 * / %
4 + -
5 << >> >>
6 <  > <= >=  instanceof
7 == !=
8 &(逻辑与、位与)
9 ^(位异或)
10 |(逻辑或、位或)
11 &&
12 ||
13 ?:
14 ->
15 = *= /= %= += -= <<= >>= >>>= &= ^= |=

总结 运算符优先级大体顺序,从高到低是:算术运算符→位运算符→关系运算符→逻辑运算符→赋值运算符。

第五章 控制语句

程序设计中的控制语句有三种,即顺序、分支和循环语句。Java程序通过控制语句来管理程序流,完成一定的任务。程序流是由若干个语句组成的,语句可以是一条单一的语句,也可以是一个用大括号({})括起来的复合语句。Java中的控制语句有以下几类:

  • 分支语句:if和switch
  • 循环语句:while、do-while和for
  • 跳转语句:break、continue、return和throw

5.1 分支语句

5.1.1 if语句

如果条件表达式为true就执行语句组,否则就执行if结构后面的语句。如果语句组只有一条语句,可以省略大括号,当从编程规范角度不要省略大括号,省略大括号会是程序的可读性变差。语法结构如下:

if (条件表达式) {
    语句组
}

if-else结构

if (条件表达式) {
   语句组1
} else {
   语句组2
}

else-if结构

if (条件表达式) {
   语句组1
} else if{
   语句组2
} else {
   语句组3
}

5.1.2 switch语句

switch提供多分支程序结构语句。

switch (表达式) {
    case 值1:
        语句组1
    case 值2:
        语句组2
    case 值3:
        语句组3
            ...
    case 判断值n:
        语句组n
    default:
        语句组n+1
}

switch语句中“表达式”计算结果只能是int、byte、short和char类型,不能是long更不能其他的类型。每个case后面只能跟一个int、byte、short和char类型的常量,default语句可以省略。

5.2 循环语句

5.2.1 while语句

while语句是一种先判断的循环结构,格式如下:

while (循环条件) {
    语句组
}

while循环没有初始化语句,循环次数是不可知的,只要循环条件满足,循环就会一直进行下去。

  • 死循环
while(true){
//循环体
}

5.2.2 do-while语句

do-while语句的使用与while语句相似,不过do-while语句是事后判断循环条件结构,语句格式如下:

do {
    语句组
} while (循环条件)

5.2.3 for语句

for语句是应用最广泛、功能最强的一种循环语句。一般格式如下:

for (初始化; 循环条件; 迭代) {
    语句组
}

提示 初始化、循环条件以及迭代部分都可以为空语句(但分号不能省略),三者均为空的时候,相当于一个无限循环。代码如下:

for (; ;) {
    ...
}

另外,在初始化部分和迭代部分,可以使用逗号语句来进行多个操作。逗号语句是用逗号分隔的语句序列,如下程序代码所示:

int x;
int y;

for (x = 0, y = 10; x < y; x++, y--) {
    System.out.printf("(x,y) = (%d, %d)", x, y);
    // 打印一个换行符,实现换行
    System.out.println();
}
输出结果:
(x,y) = (0,10)
(x,y) = (1,9)
(x,y) = (2,8)
(x,y) = (3,7)
(x,y) = (4,6)

5.2.4 for-each 语句

Java 5之后提供了一种专门用于遍历集合的for循环——for-each循环。使用for-each循环不必按照for的标准套路编写代码,只需要提供一个集合就可以遍历。语法格式如下:

for(元素类型t 元素变量x : 遍历对象obj){
引用了x的java语句;
}

假设有一个数组,采用for语句遍历数组的方式如下:

// 声明并初始化int数组
int[] numbers = { 43, 32, 53, 54, 75, 7, 10 };

System.out.println("------for语句------");
for (int i = 0; i < numbers.length; i++) {
    System.out.println("Count is:" + numbers[i]);
}

System.out.println("----for each语句----");
for (int item : numbers) {
    System.out.println("Count is:" + item);
}

5.3 跳转语句

break、continue、return
  1. break是跳出当前所在的循环,执行循环后面的语句;
  2. continue是跳出当前这次循环,继续下一次循环。
  3. return跳出当前所在的方法,是返回方法调用者一个数据。

第六章 数组

6.1 一维数组

当数组中每个元素都只带有一个下标时,这种数组就是“一维数组”。数组是引用数据类型,引用数据类型在使用之前一定要做两件事情:声明和初始化。

6.1.1 数组声明

数组的声明就宣告这个数组中元素类型,数组的变量名。

注意 数组声明完成后,数组的长度还不能确定,JVM(Java虚拟机)还没有给元素分配内存空间。

数组声明语法如下:

元素数据类型[] 数组变量名;
元素数据类型 数组变量名[];

6.1.2 数组初始化

静态初始化:在定义数组的同时就为数组元素分配空间并赋值。

int a[] = new int[]{ 3, 9, 8};
int[] a = {3,9,8};

动态初始化:数组声明且为数组元素分配空间与赋值的操作分开进行

int[] arr = new int[3];
arr[0] = 3;
arr[1] = 9;
arr[2] = 8;

6.1.3 案例: 合并数组

public class HelloWorld {

    public static void main(String[] args) {

        // 两个待合并数组
        int array1[] = { 20, 10, 50, 40, 30 };
        int array2[] = { 1, 2, 3 };

        // 动态初始化数组,设置数组的长度是array1和array2长度之和
        int array[] = new int[array1.length + array2.length];

        // 循环添加数组内容
        for (int i = 0; i < array.length; i++) {

            if (i < array1.length) {                     ①
                array[i] = array1[i];                    ②
            } else {
                array[i] = array2[i - array1.length];    ③
            }
        }

        System.out.println("合并后:");
        for (int element : array) {
            System.out.printf("%d ", element);
        }

    }
}

6.2 多维数组

6.2.1 二维数组声明

Java中声明二维数组需要有两个中括号,具体有三种语法如下:

元素数据类型[][] 数组变量名;
元素数据类型 数组变量名[][];
元素数据类型[] 数组变量名[];

6.2.2 二维数组初始化

二维数组的初始化也可以分为静态初始化和动态初始化。

格式1(动态初始化):int[][] arr = new int[3][2];
格式2(动态初始化):int[][] arr = new int[3][];
格式3(静态初始化):int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};

第七章 字符串

7.1 Java中的字符串

Java中的字符串是由双引号括起来的多个字符,下面示例都是表示字符串常量:

"Hello World"                                                            ①
"\u0048\u0065\u006c\u006c\u006f\u0020\u0057\u006f\u0072\u006c\u0064"     ②
"世界你好"                                                               ③
"A"                                                                      ④
""                                                                       ⑤

注意 字符串还有一个极端情况,就代码第⑤行的""表示空字符串,双引号中没有任何内容,空字符串不是null,空字符串是分配内存空间,而null是没有分配内存空间。

Java SE提供了三个字符串类:String、StringBuffer和StringBuilder。String是不可变字符串,StringBuffer和StringBuilder是可变字符串。

7.2 使用API

Java中很多类,每一个类又有很多方法和变量,通过查看Java API文档能够知道这些类、方法和变量如何使用。

Java官方提供了Java 8在线API文档,网址是http://docs.oracle.com/javase/8/docs/api/

提示 很多读者希望能够有离线的中文Java API文档,但Java官方只提供了Java 6的中文API文档,该文件下载地址是http://download.oracle.com/technetwork/java/javase/6/docs/zh/api.zip ,下载完成后解压api.zip文件,找到其中的index.html文件,双击就会在浏览器中打开API文档了。

查询API的一般流程是:找包→找类或接口→查看类或接口→找方法或变量。读者可以尝试查找一下String、StringBuffer和StringBuilder这些字符串类的API文档,熟悉一下这些类的用法。

7.3 不可变字符串

很多计算机语言都提供了两种字符串,即不可变字符串和可变字符串,它们区别在于当字符串进行拼接等修改操作时,不可变字符串会创建新的字符串对象,而可变字符串不会创建新对象。

7.3.1 String

Java中不可变字符串类是String,属于java.lang包,它也是Java非常重要的类。

提示 java.lang包中提供了很多Java基础类,包括Object、Class、String和Math等基本类。在使用java.lang包中的类时不需要引入(import)该包,因为它是由解释器自动引入的。当然引入java.lang包程序也不会有编译错误。

创建String对象可以通过构造方法实现,常用的构造方法:

  • String():使用空字符串创建并初始化一个新的String对象。
  • String(String original):使用另外一个字符串创建并初始化一个新的 String 对象。
  • String(StringBuffer buffer):使用可变字符串对象(StringBuffer)创建并初始化一个新的 String 对象。
  • String(StringBuilder builder):使用可变字符串对象(StringBuilder)创建并初始化一个新的 String 对象。
  • String(byte[] bytes):使用平台的默认字符集解码指定的byte数组,通过byte数组创建并初始化一个新的 String 对象。
  • String(char[] value):通过字符数组创建并初始化一个新的 String 对象。
  • String(char[] value, int offset, int count):通过字符数组的子数组创建并初始化一个新的 String 对象;offset参数是子数组第一个字符的索引,count参数指定子数组的长度。

创建字符串对象示例代码如下:

// 创建字符串对象
String s1 = new String();
String s2 = new String("Hello World");
String s3 = new String("\u0048\u0065\u006c\u006c\u006f\u0020\u0057\u006f\u0072\u006c\u0064");
System.out.println("s2 = " + s2);
System.out.println("s3 = " + s3);

char chars[] = { 'a', 'b', 'c', 'd', 'e' };
// 通过字符数组创建字符串对象
String s4 = new String(chars);
// 通过子字符数组创建字符串对象
String s5 = new String(chars, 1, 4);
System.out.println("s4 = " + s4);
System.out.println("s5 = " + s5);

byte bytes[] = { 97, 98, 99 };
// 通过byte数组创建字符串对象
String s6 = new String(bytes);
System.out.println("s6 = " + s6);
System.out.println("s6字符串长度 = " + s6.length());

输出结果;

s2 = Hello World
s3 = Hello World
s4 = abcde
s5 = bcde
s6 = abc
s6字符串长度 = 3

7.3.2 字符串池

字符串池有助于为Java Runtime节省大量空间,尽管创建String需要更多时间。

当使用双引号创建一个String时,它首先在String池中查找具有相同值的String,如果存在那么只是返回引用,否则它在池中创建一个新String,然后返回引用。

但是,使用new运算符,那么将强制String类在堆空间中创建一个新的String对象。可以使用intern()方法将它放入池中,或者从具有相同值的字符串池中引用另一个String对象。

下面是String池的java示例程序:

public class StringPool {

    /**
     * Java String Pool example
     * @param args
     */
    public static void main(String[] args) {
        String s1 = "Cat";
        String s2 = "Cat";
        String s3 = new String("Cat");

        System.out.println("s1 == s2 :"+(s1==s2));
        System.out.println("s1 == s3 :"+(s1==s3));
    }

}

执行上面示例示例代码,得到以下结果 -

s1 == s2 :true
s1 == s3 :false

字符串池中创建了多少个字符串?

有时在java面试中,会被问到字符串池的问题。例如,在下面的语句中创建了多少个字符串?

String str = new String("Cat");
Java

在上面的语句中,将创建12个字符串。如果池中已存在字符串文字 - Cat,则池中只会创建一个字符串 - str。如果池中没有字符串文字 - Cat,那么它将首先在池中创建,然后在堆空间中创建,因此将创建总共2个字符串对象。

7.3.3 字符拼接

​ String字符串虽然是不可变字符串,但也可以进行拼接只是会产生一个新的对象。String字符串拼接可以使用+运算符或String的concat(String str)方法。+运算符优势是可以连接任何类型数据拼接成为字符串,而concat方法只能拼接String类型字符串。

字符串拼接示例如下:

String s1 = "Hello";
// 使用+运算符连接
String s2 = s1 + " ";                         ①
String s3 = s2 + "World";                     ②
System.out.println(s3);

String s4 = "Hello";
// 使用+运算符连接,支持+=赋值运算符
s4 += " ";                                    ③
s4 += "World";                                ④
System.out.println(s4);

String s5 = "Hello";
// 使用concat方法连接
s5 = s5.concat(" ").concat("World");          ⑤
System.out.println(s5);

int age = 18;
String s6= "她的年龄是" + age + "岁。";       ⑥
System.out.println(s6);

char score = 'A';
String s7= "她的英语成绩是" + score;          ⑦
System.out.println(s7);

java.util.Date now = new java.util.Date();    ⑧
//对象拼接自动调用toString()方法
String s8= "今天是:" + now;                  ⑨
System.out.println(s8);

输出结果:

Hello World
Hello World
Hello World
她的年龄是18岁。
她的英语成绩是A
今天是:Thu May 25 16:25:40 CST 2017

​ 上述代码第①②行使用+运算符进行字符串的拼接,其中产生了三个对象。代码第③④行是使用+=赋值运算符,本质上也是+运算符进行拼接。

​ 代码第⑤行采用concat方法进行拼接,该方法的完整定义如下:public String concat(String str),它的参数和返回值都是String,因此代码第⑤行可以连续调用该方法进行多个字符串的拼接。

​ 代码第⑥和第⑦行是使用+运算符,将字符串与其他类型数据进行的拼接。代码第⑨行是与对象可以进行拼接,Java中所有对象都有一个toString()方法,该方法可以将对象转换为字符串,拼接过程会调用该对象 toString()方法,将该对象转换为字符串后再进行拼接。代码第⑧行的java.util.Date类是Java SE提供的日期类。

7.3.4 字符查找

在给定的字符串中查找字符或字符串是比较常见的操作。在String类中提供了indexOf和lastIndexOf方法用于查找字符或字符串,返回值是查找的字符或字符串所在的位置,-1表示没有找到。这两个方法有多个重载版本:

  • int indexOf(int ch):从前往后搜索字符ch,返回第一次找到字符ch所在处的索引。
  • int indexOf(int ch, int fromIndex):从指定的索引开始从前往后搜索字符ch,返回第一次找到字符ch所在处的索引。
  • int indexOf(String str):从前往后搜索字符串str,返回第一次找到字符串所在处的索引。
  • int indexOf(String str, int fromIndex):从指定的索引开始从前往后搜索字符串str,返回第一次找到字符串所在处的索引。
  • int lastIndexOf(int ch):从后往前搜索字符ch,返回第一次找到字符ch所在处的索引。
  • int lastIndexOf(int ch, int fromIndex):从指定的索引开始从后往前搜索字符ch,返回第一次找到字符ch所在处的索引。
  • int lastIndexOf(String str):从后往前搜索字符串str,返回第一次找到字符串所在处的索引。
  • int lastIndexOf(String str, int fromIndex):从指定的索引开始从后往前搜索字符串str,返回第一次找到字符串所在处的索引。

提示 字符串本质上是字符数组,因此它也有索引,索引从零开始。String的charAt(int index)方法可以返回索引index所在位置的字符。

字符串查找示例代码如下:

String sourceStr = "There is a string accessing example.";

//获得字符串长度
int len = sourceStr.length();
//获得索引位置16的字符
char ch = sourceStr.charAt(16);

//查找字符和子字符串
int firstChar1 = sourceStr.indexOf('r');
int lastChar1 = sourceStr.lastIndexOf('r');
int firstStr1 = sourceStr.indexOf("ing");
int lastStr1 = sourceStr.lastIndexOf("ing");
int firstChar2 = sourceStr.indexOf('e', 15);
int lastChar2 = sourceStr.lastIndexOf('e', 15);
int firstStr2 = sourceStr.indexOf("ing", 5);
int lastStr2 = sourceStr.lastIndexOf("ing", 5);

System.out.println("原始字符串:" + sourceStr);
System.out.println("字符串长度:" + len);
System.out.println("索引16的字符:" + ch);
System.out.println("从前往后搜索r字符,第一次找到它所在索引:" + firstChar1);
System.out.println("从后往前搜索r字符,第一次找到它所在索引:" + lastChar1);
System.out.println("从前往后搜索ing字符串,第一次找到它所在索引:" + firstStr1);
System.out.println("从后往前搜索ing字符串,第一次找到它所在索引:" + lastStr1);
System.out.println("从索引为15位置开始,从前往后搜索e字符,第一次找到它所在索引:" + firstChar2);
System.out.println("从索引为15位置开始,从后往前搜索e字符,第一次找到它所在索引:" + lastChar2);
System.out.println("从索引为5位置开始,从前往后搜索ing字符串,第一次找到它所在索引:" + firstStr2);
System.out.println("从索引为5位置开始,从后往前搜索ing字符串,第一次找到它所在索引:" + lastStr2);

7.3.5 字符比较

字符串比较是常见的操作,包括比较相等、比较大小、比较前缀和后缀等。

  1. 比较相等

    String提供的比较字符串相等的方法:

    • boolean equals(Object anObject):比较两个字符串中内容是否相等。
    • boolean equalsIgnoreCase(String anotherString):类似equals方法,只是忽略大小写。
  2. 比较大小

    有时不仅需要知道是否相等,还要知道大小,String提供的比较大小的方法:

    • int compareTo(String anotherString):按字典顺序比较两个字符串。如果参数字符串等于此字符串,则返回值 0;如果此字符串小于字符串参数,则返回一个小于 0 的值;如果此字符串大于字符串参数,则返回一个大于 0 的值。
    • int compareToIgnoreCase(String str):类似compareTo,只是忽略大小写。
  3. 比较前缀和后缀

    • boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束。
    • boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始。

字符串比较示例代码如下:

String s1 = new String("Hello");
String s2 = new String("Hello");
// 比较字符串是否是相同的引用
System.out.println("s1 == s2 : " + (s1 == s2));
// 比较字符串内容是否相等
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));

String s3 = "HELlo";
// 忽略大小写比较字符串内容是否相等
System.out.println("s1.equalsIgnoreCase(s3) : " + (s1.equalsIgnoreCase(s3)));

// 比较大小
String s4 = "java";
String s5 = "Swift";
// 比较字符串大小 s4 > s5
System.out.println("s4.compareTo(s5) : " + (s4.compareTo(s5)));                        ①
// 忽略大小写比较字符串大小 s4 < s5
System.out.println("s4.compareToIgnoreCase(s5) : " + (s4.compareToIgnoreCase(s5)));    ②

// 判断文件夹中文件名
String[] docFolder = { "java.docx", " JavaBean.docx", "Objecitve-C.xlsx", "Swift.docx " };
int wordDocCount = 0;
// 查找文件夹中Word文档个数
for (String doc : docFolder) {
    // 去的前后空格
    doc = doc.trim();                        ③
    // 比较后缀是否有.docx字符串
    if (doc.endsWith(".docx")) {
        wordDocCount++;
    }
}
System.out.println("文件夹中Word文档个数是: " + wordDocCount);

int javaDocCount = 0;
// 查找文件夹中Java相关文档个数
for (String doc : docFolder) {
    // 去的前后空格
    doc = doc.trim();
    // 全部字符转成小写
    doc = doc.toLowerCase();                 ④
    // 比较前缀是否有java字符串
    if (doc.startsWith("java")) {
        javaDocCount++;
    }
}
System.out.println("文件夹中Java相关文档个数是:" + javaDocCount);

输出结果:

s1 == s2 : false
s1.equals(s2) : true
s1.equalsIgnoreCase(s3) : true
s4.compareTo(s5) : 23
s4.compareToIgnoreCase(s5) : -9
文件夹中Word文档个数是: 3
文件夹中Java相关文档个数是:2

上述代码第①行的compareTo方法按字典顺序比较两个字符串,s4.compareTo(s5)表达式返回结果大于0,说明s4大于s5,字符在字典中顺序事实上就它的Unicode编码,先比较两个字符串的第一个字符j和S,j的Unicode编码是106,S的Unicode编码是83,所以可以得出结论s4 > s5。代码第②行是忽略大小写时,要么全部当成小写字母进行比较,要么当前成全部大写字母进行比较,无论哪种比较结果都是一样的s4 < s5。

代码第③行trim()方法可以去除字符串前后空白。代码第④行toLowerCase()方法可以将此字符串全部转化为小写字符串,类似的方法还有toLowerCase()方法,可将字符串全部转化为小写字符串。

7.3.6 字符截取

Java中字符串String截取方法主要的方法如下:

  • String substring(int beginIndex):从指定索引beginIndex开始截取一直到字符串结束的子字符串。
  • String substring(int beginIndex, int endIndex):从指定索引beginIndex开始截取直到索引endIndex - 1处的字符,注意包括索引为beginIndex处的字符,但不包括索引为endIndex处的字符。

字符串截取方法示例代码如下:

String sourceStr = "There is a string accessing example.";
// 截取example.子字符串
String subStr1 = sourceStr.substring(28);            ①
// 截取string子字符串
String subStr2 = sourceStr.substring(11, 17);        ②
System.out.printf("subStr1 = %s%n", subStr1);
System.out.printf("subStr2 = %s%n",subStr2);

// 使用split方法分割字符串
System.out.println("-----使用split方法-----");
String[] array = sourceStr.split(" ");               ③
for (String str : array) {
    System.out.println(str);
}

输出结果:

subStr1 = example.
subStr2 = string
-----使用split方法-----
There
is
a
string
accessing
example.

上述sourceStr字符串索引参考图10-6所示。代码第①行是截取example.子字符串,从图10-6可见e字符索引是28, 从索引28字符截取直到sourceStr结尾。代码第②行是截取string子字符串,从图10-6可见,s字符索引是11,g字符索引是16,endIndex参数应该17。

另外,String还提供了字符串分割方法,见代码第③行split(" ")方法,参数是分割字符串,返回值String[]。

7.4 可变字符串

可变字符串在追加、删除、修改、插入和拼接等操作不会产生新的对象。

7.4.1 StringBuffer和StringBuilder

Java提供了两个可变字符串类StringBuffer和StringBuilder,中文翻译为“字符串缓冲区”。

StringBuffer是线程安全的,它的方法是支持线程同步1 ,线程同步会操作串行顺序执行,在单线程环境下会影响效率。StringBuilder是StringBuffer单线程版本,Java 5之后发布的,它不是线程安全的,但它的执行效率很高。

1 线程同步是一个多线程概念,就是当多个线程访问一个方法时,只能由一个优先级别高的线程先访问,在访问期间会锁定该方法,其他线程只能等到它访问完成释放锁,才能访问。有关多线程问题将在后面章节详细介绍。

StringBuffer和StringBuilder具有完全相同的API,即构造方法和普通方法等内容一样。StringBuilder的中构造方法有4个:

  • StringBuilder():创建字符串内容是空的StringBuilder对象,初始容量默认为16个字符。
  • StringBuilder(CharSequence seq):指定CharSequence字符串创建StringBuilder对象。CharSequence接口类型,它的实现类有:String、StringBuffer和StringBuilder等,所以参数seq可以是String、StringBuffer和StringBuilder等类型。
  • StringBuilder(int capacity):创建字符串内容是空的StringBuilder对象,初始容量由参数capacity指定的。
  • StringBuilder(String str):指定String字符串创建StringBuilder对象。

上述构造方法同样适合于StringBuffer类,这里不再赘述。

提示 字符串长度和字符串缓冲区容量区别。字符串长度是指在字符串缓冲区中目前所包含字符串长度,通过length()获得;字符串缓冲区容量是缓冲区中所能容纳的最大字符数,通过capacity()获得。当所容纳的字符超过这个长度时,字符串缓冲区自动扩充容量,但这是以牺牲性能为代价的扩容。

字符串长度和字符串缓冲区容量示例代码如下:

// 字符串长度length和字符串缓冲区容量capacity
StringBuilder sbuilder1 = new StringBuilder();
System.out.println("包含的字符串长度:" + sbuilder1.length());
System.out.println("字符串缓冲区容量:" + sbuilder1.capacity());

StringBuilder sbuilder2 = new StringBuilder("Hello");
System.out.println("包含的字符串长度:" + sbuilder2.length());
System.out.println("字符串缓冲区容量:" + sbuilder2.capacity());

// 字符串缓冲区初始容量是16,超过之后会扩容
StringBuilder sbuilder3 = new StringBuilder();
for (int i = 0; i < 17; i++) {
    sbuilder3.append(8);
}
System.out.println("包含的字符串长度:" + sbuilder3.length());
System.out.println("字符串缓冲区容量:" + sbuilder3.capacity());

输出结果:

包含的字符串长度:0
字符串缓冲区容量:16
包含的字符串长度:5
字符串缓冲区容量:21
包含的字符串长度:17
字符串缓冲区容量:34

7.4.2 字符串追加

StringBuilder在提供了很多修改字符串缓冲区的方法,追加、插入、删除和替换等,这一节先介绍字符串追加方法。字符串追加方法是append,append有很多重载方法,可以追加任何类型数据,它的返回值还是StringBuilder。StringBuilder的追加法与StringBuffer完全一样,这里不再赘述。

字符串追加示例代码如下:

//添加字符串、字符
StringBuilder sbuilder1 = new StringBuilder("Hello");  ①
sbuilder1.append(" ").append("World");                 ②
sbuilder1.append('.');                                 ③
System.out.println(sbuilder1);

StringBuilder sbuilder2 = new StringBuilder();
Object obj = null;
//添加布尔值、转义符和空对象
sbuilder2.append(false).append('\t').append(obj);      ④
System.out.println(sbuilder2);

//添加数值
StringBuilder sbuilder3 = new StringBuilder();
for (int i = 0; i < 10; i++) {
    sbuilder3.append(i);
}
System.out.println(sbuilder3);

运行结果:

Hello World.
false  null
0123456789

上述代码第①行是创建一个包含Hello字符串StringBuilder对象。代码第②行是两次连续调用append方法,由于所有的append方法都返回StringBuilder对象,所有可以连续调用该方法,这种写法比较简洁。如果连续调用append方法不行喜欢,可以append方法占一行,见代码第③行。

代码第④行连续追加了布尔值、转义符和空对象,需要注意的是布尔值false转换为false字符串,空对象null也转换为"null"字符串。

7.4.3 字符串插入、删除和替换

StringBuilder中实现插入、删除和替换等操作的常用方法说明如下:

  • StringBuilder insert(int offset, String str):在字符串缓冲区中索引为offset的字符位置之前插入str,insert有很多重载方法,可以插入任何类型数据。
  • StringBuffer delete(int start, int end):在字符串缓冲区中删除子字符串,要删除的子字符串从指定索引start开始直到索引end - 1处的字符。start和end两个参数与substring(int beginIndex, int endIndex)方法中的两个参数含义一样。
  • StringBuffer replace(int start, int end, String str)字符串缓冲区中用str替换子字符串,子字符串从指定索引start开始直到索引end - 1处的字符。start和end同delete(int start, int end)方法。

以上介绍的方法虽然是StringBuilder方法,但StringBuffer也完全一样,这里不再赘述。

示例代码如下:

// 原始不可变字符串
String str1 = "Java C";
// 从不可变的字符创建可变字符串对象
StringBuilder mstr = new StringBuilder(str1);

// 插入字符串
mstr.insert(4, " C++");                      ①
System.out.println(mstr);

// 具有追加效果的插入字符串
mstr.insert(mstr.length(), " Objective-C");  ②
System.out.println(mstr);

// 追加字符串
mstr.append(" and Swift");
System.out.println(mstr);

// 删除字符串
mstr.delete(11, 23);                         ③
System.out.println(mstr);

输出结果:

Java C++ C
Java C++ C Objective-C
Java C++ C Objective-C and Swift
Java C++ C and Swift

第八章 面向对象基础

面向对象是Java最重要的特性。Java是彻底的、纯粹的面向对象语言,在Java中“一切都是对象”。

8.1 面向对象概述

面向对象的编程思想:按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。

例如:在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作。学生只是抽象的描述,这个抽象的描述称为“类”。在学校里活动是学生个体,即:张同学、李同学等,这些具体的个体称为“对象”,“对象”也称为“实例”。

在现实世界有类和对象,面向对象软件世界也会有,只不过它们会以某种计算机语言编写的程序代码形式存在,这就是面向对象编程(Object Oriented Programming,OOP)。作为面向对象的计算机语言——Java,具有定义类和创建对象等面向对象能力。

8.2 面向对象三个基本特征

面向对象思想有三个基本特性:封装性、继承性和多态性。

(1)封装:保护内部的操作不被破坏;
(2)继承:在原本的基础之上继续进行扩充;
(3)多态:在一个指定的范围之内进行概念的转换。

对于面向对象的开发来讲也分为三个过程:OOA(面向对象分析)、OOD(面向对象设计)、OOP(面向对象编程)。

8.2.1 封装性

在现实世界中封装的例子到处都是。

例如:一台计算机内部极其复杂,有主板、CPU、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、CPU主频、硬盘和内存的大小,于是计算机制造商将用机箱把计算机封装起来,对外提供了一些接口,如鼠标、键盘和显示器等,这样当用户使用计算机就变非常方便。

8.2.2 继承性

在现实世界中继承也是无处不在。

例如:轮船与客轮之间的关系,客轮是一种特殊轮船,拥有轮船的全部特征和行为,即数据和操作。在面向对象中轮船是一般类,客轮是特殊类,特殊类拥有一般类的全部数据和操作,称为特殊类继承一般类。在Java语言中一般类称为“父类”,特殊类称为“子类”。

8.2.3 多态性

多态性是指在父类中成员变量和成员方法被子类继承之后,可以具有不同的状态或表现行为。

8.3 类

是抽象的概念集合,表示的是一个共性的产物,类之中定义的是属性和行为(方法);

8.3.1 类声明

在Java中定义类,使用关键字class完成。语法如下:

修饰符 class  类名 {
	属性声明;
	方法声明;
}

范例:定义一个Person类

class Person {     // 类名称首字母大写
	//成员变量
    String name ;
    int age ;
    //成员方法
    public void tell() {        // 没有static
          System.out.println("姓名:" + name + ",年龄:" + age) ;
         }
}

成员变量:和以前定义变量几乎一样的。只不过位置发生了改变。在类中,方法外。

成员方法:和以前定义方法几乎是一样的。只不过把static去掉。

8.4 包

为了更好地组织类,Java 提供了包机制,用于区别类名的命名空间。

8.4.1 包的作用

  • 1、把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
  • 2、如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
  • 3、包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。

8.4.2 包定义和引用

Java 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类(class)、接口、枚举(enumerations)和注释(annotation)等。

包语句的语法格式为:

package pkg1[.pkg2[.pkg3…]];

例如,一个Something.java 文件它的内容

package net.java.util;
public class Something{
   ...
}

8.4.3 常用包

Java SE提供一些常用包,其中包含了Java开发中常用的基础类。这些包有:java.lang、java.io、java.net、java.util、java.text、java.awt和javax.swing。

  1. java.lang包

    java.lang包含中包含了Java语言的核心类,如Object、Class、String、包装类和Math等,还有包装类Boolean、Character、Integer、Long、Float和Double。使用java.lang包中的类型,不需要显示使用import语句引入,它是由解释器自动引入。

  2. java.io包

    java.io包含中提供多种输入/输出流类,如InputStream、OutputStream、Reader和Writer。还有文件管理相关类和接口,如File和FileDescriptor类以及FileFilter接口。

  3. java.net包

    java.net包含进行网络相关的操作的类,如URL、Socket和ServerSocket等。

  4. java.util包

    java.util包含一些实用工具类和接口,如集合、日期和日历相关类和接口。

  5. java.text包

    java.text包中提供文本处理、日期式化和数字格式化等相关类和接口。

  6. java.awt和javax.swing包

    java.awt和javax.swing包提供了Java图形用户界面开发所需要的各种类和接口。java.awt提供是一些基础类和接口,javax.swing提供了一些高级组件。

8.5 方法重载(Overload)

先来看下方法重载(Overloading)的定义:如果有两个方法的方法名相同,但参数不一致,哪么可以说一个方法是另一个方法的重载。 具体说明如下:

  • 方法名相同
  • 方法的参数类型,参数个不一样
  • 方法的返回类型可以不相同
  • 方法的修饰符可以不相同
  • main 方法也可以被重载

以下实例演示了如何重载 MyClass 类的 info 方法:

// MethodOverloading.java文件
package com.a51work6;

class MethodOverloading {

    void receive(int i) {                            ①
        System.out.println("接收一个int参数");
        System.out.println("i = " + i);
    }

    void receive(int x, int y) {                     ②
        System.out.println("接收两个int参数");
        System.out.printf("x = %d, y = %d \r", x, y);
    }

    int receive(double x, double y) {                ③
        System.out.println("接收两个double参数");
        System.out.printf("x = %f, y = %f \r", x, y);
        return 0;
    }
}

// HelloWorld.java文件调用MethodOverloading
package com.a51work6;

public class HelloWorld {
    public static void main(String[] args) {

        MethodOverloading mo = new MethodOverloading();

        //调用void receive(int i)
        mo.receive(1);                          ④

        //调用void receive(int x, int y)
        mo.receive(2, 3);                       ⑤

        //调用void receive(double x, double y)
        mo.receive(2.0, 3.3);                   ⑥
    }

运行结果如下

接收一个int参数
i = 1
接收两个int参数
x = 2, y = 3
接收两个double参数
x = 2.000000, y = 3.300000

8.6 封装性与访问控制

Java面向对象的封装性是通过对成员变量和方法进行访问控制实现的,访问控制分为4个等级:私有、默认、保护和公有。

控制等级 同一个类 同一个包 不同包的子类 不同包非子类
私有(private) Yes
默认(default) Yes Yes
保护(protected) Yes Yes Yes
公有(public) Yes Yes Yes Yes

修饰类

  1. 默认访问权限(包访问权限):用来修饰类的话,表示该类只对同一个包中的其他类可见。

  2. public:用来修饰类的话,表示该类对其他所有的类都可见。

修饰类的方法和变量

  1. 默认访问权限(包访问权限):如果一个类的方法或变量被包访问权限修饰,也就意味着只能在同一个包中的其他类中显示地调用该类的方法或者变量,在不同包中的类中不能显示地调用该类的方法或变量。
  2. private:如果一个类的方法或者变量被private修饰,那么这个类的方法或者变量只能在该类本身中被访问,在类外以及其他类中都不能显示地进行访问。
  3. protected:如果一个类的方法或者变量被protected修饰,对于同一个包的类,这个类的方法或变量是可以被访问的。对于不同包的类,只有继承于该类的类才可以访问到该类的方法或者变量。
  4. public:被public修饰的方法或者变量,在任何地方都是可见的。

拓展:

  Java中的包主要是为了防止类文件命名冲突以及方便进行代码组织和管理;

  对于一个Java源代码文件,如果存在public类的话,只能有一个public类,且此时源代码文件的名称必须和public类的名称完全相同,另外,如果还存在其他类,这些类在包外是不可见的。如果源代码文件没有public类,则源代码文件的名称可以随意命名。

8.7 静态变量和静态方法

有一个Account(银行账户)类,假设它有三个成员变量:amount(账户金额)、interestRate(利率)和owner(账户名)。在这三个成员变量中,amount和owner会因人而异,对于不同的账户这些内容是不同的,而所有账户的interestRate都是相同的。

amount和owner成员变量与账户个体有关,称为“实例变量”,interestRate成员变量与个体无关,或者说是所有账户个体共享的,这种变量称为“静态变量”或“类变量”。

静态变量和静态方法示例代码如下:

// Account.java文件
package com.a51work6;

public class Account {

    // 实例变量账户金额
    double amount = 0.0;                    ①
    // 实例变量账户名
    String owner;                           ②

    // 静态变量利率
    static double interestRate = 0.0668;    ③

    // 静态方法
    public static double interestBy(double amt) {        ④
        //静态方法可以访问静态变量和其他静态方法
        return interestRate * amt;                       ⑤
    }

    // 实例方法
    public String messageWith(double amt) {              ⑥
        //实例方法可以访问实例变量、实例方法、静态变量和静态方法
        double interest = Account.interestBy(amt);       ⑦
        StringBuilder sb = new StringBuilder();
        // 拼接字符串
        sb.append(owner).append("的利息是").append(interest);
        // 返回字符串
        return sb.toString();
    }
}

static修饰的成员变量是静态变量,见代码第③行。staitc修饰的方法是静态方法,见代码第④行。相反,没有static修饰的成员变量是实例变量,见代码第①行和第②行;没有staitc修饰的方法是实例方法,见代码第⑥行。

注意 静态方法可以访问静态变量和其他静态方法,例如访问代码第⑤行中的interestRate静态变量。实例方法可以访问实例变量、其他实例方法、静态变量和静态方法,例如访问代码第⑦行interestBy静态方法。

调用Account代码如下:

// HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {
        // 访问静态变量
        System.out.println(Account.interestRate);           ①
        // 访问静态方法
        System.out.println(Account.interestBy(1000));       ②

        Account myAccount = new Account();
        // 访问实例变量
        myAccount.amount = 1000000;                         ③
        myAccount.owner = "Tony";                           ④
        // 访问实例方法
        System.out.println(myAccount.messageWith(1000));    ⑤

        // 通过实例访问静态变量
        System.out.println(myAccount.interestRate);         ⑥
    }
}

8.8 静态代码块

静态代码块:执行优先级高于非静态的初始化块,它会在类初始化的时候执行一次,执行完成便销毁,它仅能初始化类变量,即static修饰的数据成员。

静态代码块写法,

static{

}

构造代码块:java类中使用{ }声明的代码块(和静态代码块的区别是少了static关键字)

{

}

举例

 public class codeBlock {
     static {
         System.out.println("静态代码块");
     }
     {
         System.out.println("构造代码块");
     }
 }

第九章 对象

9.1 创建对象

创建对象包括两个步骤:声明和实例化。

语法格式如下:

类名 对象名 = new 类名()

9.2 空对象

一个引用变量没有通过new分配内存空间,这个对象就是空对象,Java使用关键字null表示空对象。

示例代码如下:

String name = null;
name = "Hello World";

9.3 构造方法

Java构造方法的特点:

  1. 构造方法名必须与类名相同。
  2. 构造方法没有任何返回值,包括void。
  3. 构造方法只能与new运算符结合使用。

构造方法示例代码如下:

//Rectangle.java文件
package com.a51work6;

// 矩形类
public class Rectangle {

    // 矩形宽度
    int width;
    // 矩形高度
    int height;
    // 矩形面积
    int area;

    // 构造方法
    public Rectangle(int w, int h) {        ①
        width = w;
        height = h;
        area = getArea(w, h);
    }
    ...
}

9.3.1 默认构造方法

Java虚拟机为没有构造方法的类,提供一个无参数的默认构造方法,默认构造方法其方法体内无任何语句,默认构造方法相当于如下代码:

//默认构造方法
public User() {
}

9.3.2 构造方法重载

在一个类中可以有多个构造方法,它们具体有相同的名字(与类名相同),参数列表不同,所以它们之间一定是重载关系。

构造方法重载示例代码如下:

//Person.java文件
package com.a51work6;

import java.util.Date;

public class Person {

    // 名字
    private String name;
    // 年龄
    private int age;
    // 出生日期
    private Date birthDate;

    public Person(String n, int a, Date d) {     ①
        name = n;
        age = a;
        birthDate = d;
    }

    public Person(String n, int a) {             ②
        name = n;
        age = a;
    }

    public Person(String n, Date d) {            ③
        name = n;
        age = 30;
        birthDate = d;
    }

    public Person(String n) {                     ④
        name = n;
        age = 30;
    }

    public String getInfo() {
        StringBuilder sb = new StringBuilder();
        sb.append("名字: ").append(name).append('\n');
        sb.append("年龄: ").append(age).append('\n');
        sb.append("出生日期: ").append(birthDate).append('\n');
        return  sb.toString();
    }
}

9.3.3 构造方法封装

构造方法也可以进行封装,访问级别与普通方法一样。示例代码如下:

//Person.java文件
package com.a51work6;

import java.util.Date;

public class Person {

    // 名字
    private String name;
    // 年龄
    private int age;
    // 出生日期
    private Date birthDate;

    // 公有级别限制
    public Person(String n, int a, Date d) {    ①
        name = n;
        age = a;
        birthDate = d;
    }

    // 默认级别限制
    Person(String n, int a) {                   ②
        name = n;
        age = a;
    }

    // 保护级别限制
    protected Person(String n, Date d) {        ③
        name = n;
        age = 30;
        birthDate = d;
    }

    // 私有级别限制
    private Person(String n) {                  ④
        name = n;
        age = 30;
    }

    ...
}

单例模式是一种常用的软件设计模式,单例模式可以保证系统中一个类只有一个实例。

9.4 this关键字

this指向对象本身,一个类可以通过this来获得一个代表它自身的对象变量。this使用在如下三种情况中:

  1. 调用实例变量。
  2. 调用实例方法。
  3. 调用其他构造方法。

使用this变量的示例代码:

//Person.java文件
package com.a51work6;

import java.util.Date;

public class Person {

    // 名字
    private String name;
    // 年龄
    private int age;
    // 出生日期
    private Date birthDate;

    // 三个参数构造方法
    public Person(String name, int age, Date d) {       ①
        this.name = name;                               ②
        this.age = age;                                 ③
        birthDate = d;
        System.out.println(this.toString());            ④
    }

    public Person(String name, int age) {
        // 调用三个参数构造方法
        this(name, age, null);                          ⑤
    }

    public Person(String name, Date d) {
        // 调用三个参数构造方法
        this(name, 30, d);                              ⑥
    }

    public Person(String name) {
        // System.out.println(this.toString());
        // 调用Person(String name, Date d)构造方法
        this(name, null);                               ⑦
    }

    @Override
    public String toString() {
        return "Person [name=" + name                   ⑧
                + ", age=" + age                        ⑨
                + ", birthDate=" + birthDate + "]";
    }
}

9.5 对象销毁

对象不再使用时应该销毁。C++语言对象是通过delete语句手动释放,Java语言对象是由垃圾回收器(Garbage Collection)收集然后释放,程序员不用关心释放的细节。自动内存管理是现代计算机语言发展趋势,例如:C#语言的垃圾回收,Objective-C和Swift语言的ARC(内存自动引用计数管理)。

垃圾回收器(Garbage Collection)的工作原理是:当一个对象的引用不存在时,认为该对象不再需要,垃圾回收器自动扫描对象的动态内存区,把没有引用的对象作为垃圾收集起来并释放。

第十章 继承与多态

10.1 继承的概念

继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

10.2 类的继承格式

class 父类 {
}
 
class 子类 extends 父类 {
}

10.3 继承的特征

  • 子类拥有父类非 private 的属性、方法。
  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
  • 子类可以用自己的方式实现父类的方法。
  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

10.4 继承关键字

继承可以使用 extends 和 implements 这两个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的两个关键字,则默认继承object(这个类在 java.lang 包中,所以不需要 import)祖先类。

10.4.1 extends关键字

在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。

public class Animal { 
    private String name;   
    private int id; 
    public Animal(String myName, String myid) { 
        //初始化属性值
    } 
    public void eat() {  //吃东西方法的具体实现  } 
    public void sleep() { //睡觉方法的具体实现  } 
} 
 
public class Penguin  extends  Animal{ 
}

10.4.2 implements关键字

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

public interface A {
    public void eat();
    public void sleep();
}
 
public interface B {
    public void show();
}
 
public class C implements A,B {
}

10.4.3 super 与 this 关键字

super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

this关键字:指向自己的引用。

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}
 
class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    this.eat();   // this 调用自己的方法
    super.eat();  // super 调用父类方法
  }
}
 
public class Test {
  public static void main(String[] args) {
    Animal a = new Animal();
    a.eat();
    Dog d = new Dog();
    d.eatTest();
  }
}

10.4.4 final关键字

final 关键字声明类可以把类定义为不能继承的,即最终类;或者用于修饰方法,该方法不能被子类重写:

  • 声明类:

    final class 类名 {//类体}
    
  • 声明方法:

    修饰符(public/private/default/protected) final 返回值类型 方法名(){//方法体}
    

:实例变量也可以被定义为 final,被定义为 final 的变量不能被修改。被声明为 final 类的方法自动地声明为 final,但是实例变量并不是 final

10.5 多态

多态是同一个行为具有多个不同表现形式或形态的能力。

多态就是同一个接口,使用不同的实例而执行不同操作,

10.5.1 多态的优点

  1. 消除类型之间的耦合关系
  2. 可替换性
  3. 可扩充性
  4. 接口性
  5. 灵活性
  6. 简化性

10.5.2 多态存在的三个必要条件

  1. 继承
  2. 重写
  3. 父类引用指向子类对象:Parent p = new Child();

10.6 重写

当子类对象调用重写的方法时,调用的是子类的方法,而不是父类中被重写的方法。

要想调用父类中被重写的方法,则必须使用关键字 super

Employee.java 文件代码:

/* 文件名 : Employee.java */
public class Employee {
   private String name;
   private String address;
   private int number;
   public Employee(String name, String address, int number) {
      System.out.println("Employee 构造函数");
      this.name = name;
      this.address = address;
      this.number = number;
   }
   public void mailCheck() {
      System.out.println("邮寄支票给: " + this.name
       + " " + this.address);
   }
   public String toString() {
      return name + " " + address + " " + number;
   }
   public String getName() {
      return name;
   }
   public String getAddress() {
      return address;
   }
   public void setAddress(String newAddress) {
      address = newAddress;
   }
   public int getNumber() {
     return number;
   }
}

Salary.java 文件代码:

/* 文件名 : Salary.java */
public class Salary extends Employee
{
   private double salary; // 全年工资
   public Salary(String name, String address, int number, double salary) {
       super(name, address, number);
       setSalary(salary);
   }
   public void mailCheck() {
       System.out.println("Salary 类的 mailCheck 方法 ");
       System.out.println("邮寄支票给:" + getName()
       + " ,工资为:" + salary);
   }
   public double getSalary() {
       return salary;
   }
   public void setSalary(double newSalary) {
       if(newSalary >= 0.0) {
          salary = newSalary;
       }
   }
   public double computePay() {
      System.out.println("计算工资,付给:" + getName());
      return salary/52;
   }
}

VirtualDemo.java 文件代码:

/* 文件名 : VirtualDemo.java */
public class VirtualDemo {
   public static void main(String [] args) {
      Salary s = new Salary("员工 A", "北京", 3, 3600.00);
      Employee e = new Salary("员工 B", "上海", 2, 2400.00);
      System.out.println("使用 Salary 的引用调用 mailCheck -- ");
      s.mailCheck();
      System.out.println("\n使用 Employee 的引用调用 mailCheck--");
      e.mailCheck();
    }
}

以上实例编译运行结果如下:

Employee 构造函数
Employee 构造函数
使用 Salary 的引用调用 mailCheck -- 
Salary 类的 mailCheck 方法 
邮寄支票给:员工 A ,工资为:3600.0

使用 Employee 的引用调用 mailCheck--
Salary 类的 mailCheck 方法 
邮寄支票给:员工 B ,工资为:2400.0

10.7 多态的实现方式

  1. 方式一:重写
  2. 方式二:接口
  3. 方式三:抽象类和抽象方法

第十一章 抽象类与接口

11.1 抽象类

11.1.1 抽象类的概念

  • 在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
  • 抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
  • 由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。也是因为这个原因,通常在设计阶段决定要不要设计抽象类。
  • 父类包含了子类集合的常见的方法,但是由于父类本身是抽象的,所以不能使用这些方法。
  • 在Java中抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口。

11.1.2 抽象类的声明和实现

在Java中抽象类和抽象方法的修饰符是abstract,声明抽象类Figure示例代码如下:

//Figure.java文件
package com.a51work6;

public abstract class Figure {            ①
    // 绘制几何图形方法
    public abstract void onDraw();        ②
}

设计抽象方法目的就是让子类来实现的,否则抽象方法就没有任何意义,实现抽象类示例代码如下:

//Ellipse.java文件
package com.a51work6;

//几何图形椭圆形
public class Ellipse extends Figure {

    //绘制几何图形方法
    @Override
    public void onDraw() {
        System.out.println("绘制椭圆形...");
    }
}

//Triangle.java文件
package com.a51work6;

//几何图形三角形
public class Triangle extends Figure {

    // 绘制几何图形方法
    @Override
    public void onDraw() {
        System.out.println("绘制三角形...");
    }
}

上述代码声明了两个具体类Ellipse和Triangle,它们实现(覆盖)了抽象类Figure的抽象方法onDraw。
调用代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // f1变量是父类类型,指向子类实例,发生多态
        Figure f1 = new Triangle();
        f1.onDraw();

        // f2变量是父类类型,指向子类实例,发生多态
        Figure f2 = new Ellipse();
        f2.onDraw();
    }
}

上述代码中实例化两个具体类Triangle和Ellipse,对象f1和f2是Figure引用类型。

注意 抽象类不能被实例化,只有具体类才能被实例化。

11.2 接口

11.2.1 接口的概念

  • 接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
  • 接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
  • 除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
  • 接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

11.2.2 接口声明和实现

声明格式:

[可见度] interface 接口名称 [extends 其他的接口名] {
        // 声明变量
        // 抽象方法
}

接口实现:

...implements 接口名称[, 其他接口名称, 其他接口名称..., ...] ...

11.2.3 接口实例

Animal.java 文件代码:

/* 文件名 : Animal.java */
interface Animal {
   public void eat();
   public void travel();
}

MammalInt.java 文件代码:

/* 文件名 : MammalInt.java */
public class MammalInt implements Animal{
 
   public void eat(){
      System.out.println("Mammal eats");
   }
 
   public void travel(){
      System.out.println("Mammal travels");
   } 
 
   public int noOfLegs(){
      return 0;
   }
 
   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

以上实例编译运行结果如下:

Mammal eats
Mammal travels

11.3 接口的继承

一个接口能继承另一个接口,和类之间的继承方式比较相似。接口的继承使用extends关键字,子接口继承父接口的方法。
下面的Sports接口被Hockey和Football接口继承:

// 文件名: Sports.java
public interface Sports
{
   public void setHomeTeam(String name);
   public void setVisitingTeam(String name);
}
 
// 文件名: Football.java
public interface Football extends Sports
{
   public void homeTeamScored(int points);
   public void visitingTeamScored(int points);
   public void endOfQuarter(int quarter);
}
 
// 文件名: Hockey.java
public interface Hockey extends Sports
{
   public void homeGoalScored();
   public void visitingGoalScored();
   public void endOfPeriod(int period);
   public void overtimePeriod(int ot);
}

Hockey接口自己声明了四个方法,从Sports接口继承了两个方法,这样,实现Hockey接口的类需要实现六个方法。
相似的,实现Football接口的类需要实现五个方法,其中两个来自于Sports接口。

11.4 接口的多继承

在Java中,类的多继承是不合法,但接口允许多继承。
在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示:

public interface Hockey extends Sports, Event

以上的程序片段是合法定义的子接口,与类不同的是,接口允许多继承,而 Sports及 Event 可能定义或是继承相同的方法

11.3 抽象类和接口的区别

  • 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
  • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  • 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

:JDK 1.8 以后,接口里可以有静态方法和方法体了。

第十二章 枚举类

12.1 枚举概述

枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。枚举在曰常生活中很常见,例如一个人的性别只能是“男”或者“女”,一周的星期只能是 7 天中的一个等。类似这种当一个变量有几种固定可能的取值时,就可以将它定义为枚举类型。

12.2 枚举类声明

Java中是使用enum关键词声明枚举类,具体定义放在一对大括号内,枚举的语法格式如下:

[public] enum 枚举名 {
     枚举常量列表
}

12.2.1 最简单形式的枚举类

如果采用枚举类来表示工作日,最简单枚举类WeekDays具体代码如下:

//WeekDays.java文件
package com.a51work6;

public enum WeekDays {
    // 枚举常量列表
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY
}

在枚举类WeekDays中定义了5个常量,使用枚举类WeekDays代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {
        // day工作日变量
        WeekDays day = WeekDays.FRIDAY;         ①
        System.out.println(day);                ②

        switch (day) {                          ③
        case MONDAY:                            ④
            System.out.println("星期一");
            break;
        case TUESDAY:
            System.out.println("星期二");
            break;
        case WEDNESDAY:
            System.out.println("星期三");
            break;
        case THURSDAY:
            System.out.println("星期四");
            break;
        default: //case FRIDAY:                 ⑤
            System.out.println("星期五");
        }
    }
}

输出结果:

FRIDAY
星期五

12.2.2 枚举类中成员变量和成员方法

枚举类可以像类一样包含成员变量和成员方法,成员变量可以是实例变量也可以是静态变量,成员方法可以是实例方法,也可以是静态方法,但不能是抽象方法。

示例代码如下:

//WeekDays.java文件
package com.a51work6;

public enum WeekDays {
    // 枚举常量列表
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY;        ①

    // 实例变量
    private String name;
    private int index;

    // 静态变量
    private static int staticVar = 100;

    // 覆盖父类中的toString()方法
    @Override
    public String toString() {                           ②
        StringBuilder sb = new StringBuilder();
        sb.append(name);
        sb.append('-');
        sb.append(index);
        return sb.toString();
    }

    // 实例方法
    public String getInfo() {
        // 调用父类中toString()方法
        return super.toString();
    }

    // 静态方法
    public static int getStaticVar() {
        return staticVar;
    }
}

12.2.3 枚举类构造方法

在12.2.2节示例中实例变量name和index,都是没有初始化,在类中成员变量的初始化是通过构造方法实现的,而在枚举类中也是通过构造方法初始化成员变量的。

为12.2.2节示例添加构造方法,代码如下:

//WeekDays.java文件
package com.a51work6;

public enum WeekDays {
    // 枚举常量列表
    MONDAY("星期一", 0),  TUESDAY("星期二", 1),  WEDNESDAY("星期三", 2),
            THURSDAY("星期四", 3),  FRIDAY("星期五", 4);            ①

    // 实例变量
    private String name;
    private int index;

    // 静态变量
    private static int staticVar = 100;

    private WeekDays(String name, int index) {                      ②
        this.name = name;
        this.index = index;
    }

    // 覆盖父类中的toString()方法
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(name);
        sb.append('-');
        sb.append(index);
        return sb.toString();
    }

    // 实例方法
    public String getInfo() {
        // 调用父类中toString()方法
        return super.toString();
    }

    // 静态方法
    public static int getStaticVar() {
        return staticVar;
    }
}

12.3 枚举常用方法

所有枚举类都继承java.lang.Enum类,Enum中定义了一些枚举中常用的方法:

  • int ordinal():返回枚举常量的顺序。这个顺序根据枚举常量声明的顺序而定,顺序从零开始。
  • 枚举类型[] values():静态方法,返回一个包含全部枚举常量的数组。
  • 枚举类型 valueOf(String str):静态方法,str是枚举常量对应的字符串,返回一个包含枚举类型实例。

WeekDays枚举类代码如下:

//WeekDays.java文件
package com.a51work6;

public enum WeekDays {
    // 枚举常量列表
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY
}

使用枚举常用方法示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 返回一个包含全部枚举常量的数组
        WeekDays[] allValues = WeekDays.values();                ①
        // 遍历枚举常量数值
        for (WeekDays value : allValues) {
            System.out.printf("%d - %s\n", value.ordinal(), value);    ②
        }

        // 创建WeekDays对象
        WeekDays day1 = WeekDays.FRIDAY;
        WeekDays day2 = WeekDays.valueOf("FRIDAY");              ③

        System.out.println(day1 == WeekDays.FRIDAY);             ④
        System.out.println(day1.equals(WeekDays.FRIDAY));        ⑤
        System.out.println(day1 == day2);                        ⑥

    }
}

第十三章 Java常用类

13.1 java根类—Object

第一个应该介绍的常用类就是java.lang.Object类,它是Java所有类的根类,Java所有类都直接或间接继承自Object类,它是所有类的“祖先”。Object类属于java.lang包中的类型,不需要显示使用import语句引入,它是由解释器自动引入。

Object类有很多方法,常用的几个方法:

  • String toString():返回该对象的字符串表示。
  • boolean equals(Object obj):指示其他某个对象是否与此对象“相等”。

这些方法都是需要在子类中用来覆盖的,下面就详细解释一下它们的用法。

13.1.1 toString()方法

为了日志输出等处理方便,所有的对象都可以以文本方式表示,需要在该对象所在类中覆盖toString()方法。如果没有覆盖toString()方法,默认的字符串是“类名@对象的十六进制哈希码1

1 哈希码(hashCode),每个Java对象都有哈希码(hashCode)属性,哈希码可以用来标识对象,提高对象在集合操作中的执行效率。

13.1.2 对象比较方法

在前面学习字符串比较的时,曾经介绍过有两种比较方法:运算符和equals()方法,运算符是比较两个引用变量是否指向同一个实例,equals()方法是比较两个对象的内容是否相等,通常字符串的比较,只是关心的内容是否相等。

13.2 包装类

在Java中8种基本数据类型不属于类,不具备“对象”的特征,没有成员变量和方法,不方便进行面向对象的操作。为此,Java提供包装类(Wrapper Class)来将基本数据类型包装成类,每个Java基本数据类型在java.lang包中都有一个相应的包装类,每个包装类对象封装一个基本数据类型数值。

本数据类型与包装类对应关系

基本数据类型 包装类
boolean Boolean
byte Byte
char Character
short Short
int Integer
long Long
float Float
double Double

13.2.1 数值包装类

这些数值包装类(Byte、Short、Integer、Long、Float和Double)都有一些相同特点。

  1. 构造方法类似

    每一个数值包装类都有两个构造方法,以Integer为例,Integer构造方法如下:

    • Integer(int value):通过指定一个数值构造Integer对象。
    • Integer(String s):通过指定一个字符串s构造对象,s是十进制字符串表示的数值。
  2. 共同的父类

    这6个数值包装类有一个共同的父类——Number,Number是一个抽象类,除了这6个子类还有:AtomicInteger、AtomicLong、BigDecimal和BigInteger,其中BigDecimal和BigInteger后面还会详细介绍。Number是抽象类,要求它的子类必须实现如下6个方法:

    • byte byteValue():将当前包装的对象转换为byte类型的数值。
    • double doubleValue():将当前包装的对象转换为double类型的数值。
    • float floatValue():将当前包装的对象转换为float类型的数值。
    • int intValue():将当前包装的对象转换为int类型的数值。
    • long longValue():将当前包装的对象转换为long类型的数值。
    • short shortValue():将当前包装的对象转换为short类型的数值。

    通过这6个方法数值包装类可以互相转换这6种数值,但是需要注意的是大范围数值转换为小范围的数值,如果数值本身很大,可以会导致精度的丢失。

  3. compareTo()方法

    每一个数值包装类都有int compareTo(数值包装类对象)方法,可以进行包装对象的比较。方法返回值是int,如果返回值是0,则相等;如果返回值小于0,则此对象小于参数对象;如果返回值大于0,则此对象大于参数对象。

  4. 字符串转换为基本数据类型

    每一个数值包装类都提供一些静态parseXXX()方法将字符串转换为对应的基本数据类型,以Integer为例,方法定义如下:

    • static int parseInt(String s):将字符串s转换有符号的十进制整数。
    • static int parseInt(String s, int radix):将字符串s转换有符号的整数,radix是指定基数,基数用来指定进制。注意这种指定基数的方法在浮点数包装类(Double和Float)中没有的。
  5. 基本数据类型转换为字符串

    每一个数值包装类都提供一些静态toString()方法实现将基本数据类型数值转换为字符串,以Integer为例,方法定义如下:

    • static String toString(int i):将该整数i转换为有符号的十进制表示的字符串。
    • static String toString(int i, int radix):将该整数i转换为有符号的特定进制表示的字符串,radix是基数可以指定进制。注意这种指定基数的方法在浮点数包装类(Double和Float)中没有的。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 1.构造方法
        //创建数值为80的Integer对象
        Integer objInt = new Integer(80);
        //创建数值为80.0的Double对象
        Double objDouble = new Double(80.0);
        //通过"80.0"字符串创建数值为80.0的Float对象
        Float objFloat = new Float("80.0");
        //通过"80"字符串创建数值为80的Long对象
        Long objLong = new Long("80");

        // 2.Number类方法
        //Integer对象转换为long数值
        long longVar = objInt.longValue();
        //Double对象转换为int数值
        int intVar = objDouble.intValue();
        System.out.println("intVar = " + intVar);
        System.out.println("longVar = " + longVar);

        // 3.compareTo()方法
        Float objFloat2 = new Float(100);
        int result = objFloat.compareTo(objFloat2);
        // result = -1,表示objFloat小于objFloat2
        System.out.println(result);

        // 4.字符串转换为基本数据类型
        // 10进制"100"字符串转换为10进制数为100
        int intVar2 = Integer.parseInt("100");
        // 16进制"ABC"字符串转换为10进制数为2748
        int intVar3 = Integer.parseInt("ABC", 16);
        System.out.println("intVar2 = " + intVar2);
        System.out.println("intVar3 = " + intVar3);

        // 5.基本数据类型转换为字符串
        // 100转换为10进制字符串
        String str1 = Integer.toString(100);
        // 100转换为16进制字符串结果是64
        String str2 = Integer.toString(100, 16);
        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);

    }
}

13.2.2 Character类

Character类是char类型的包装类。Character类常用方法如下:

  • Character(char value):构造方法,通过char值创建一个新的Character对象。
  • char charValue():返回此Character对象的值。
  • int compareTo(Character anotherCharacter):方法返回值是int,如果返回值是0,则相等;如果返回值小于0,则此对象小于参数对象;如果返回值大于0,则此对象大于参数对象。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 创建数值为'A'的Character对象
        Character objChar1 = new Character('A');
        // 从Character对象返回char值
        char ch = objChar1.charValue();

        // 字符比较
        Character objChar2 = new Character('C');
        int result = objChar1.compareTo(objChar2);
        // result = -2,表示objChar1小于objChar2
        if (result < 0) {
            System.out.println("objChar1小于objChar2");
        }
    }
}

13.2.3 Boolean类

Boolean类是boolean类型的包装类。

  1. 构造方法

    Boolean类有两个构造方法,构造方法定义如下:

    • Boolean(boolean value):通过一个boolean值创建Boolean对象。
    • Boolean(String s):通过字符串创建Boolean对象。s不能为null,s如果是忽略大小写"true"则转换为true对象,其他字符串都转换为false对象。
  2. compareTo()方法

    Boolean类有int compareTo(Boolean包装类对象)方法,可以进行包装对象的比较。方法返回值是int,如果返回值是0,则相等;如果返回值小于0,则此对象小于参数对象;如果返回值大于0,则此对象大于参数对象。

  3. 字符串转换为boolean类型

    Boolean包装类都提供静态parseBoolean()方法实现将字符串转换为对应的boolean类型,方法定义如下:

    static boolean parseBoolean(String s):将字符串转换为对应的boolean类。s不能为null,s如果是忽略大小写"true"则转换为true,其他字符串都转换为false。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 创建数值为true的Character对象true
        Boolean obj1 = new Boolean(true);
        // 通过字符串"true"创建Character对象true
        Boolean obj2 = new Boolean("true");
        // 通过字符串"True"创建Character对象true
        Boolean obj3 = new Boolean("True");
        // 通过字符串"TRUE"创建Character对象true
        Boolean obj4 = new Boolean("TRUE");
        // 通过字符串"false"创建Character对象false
        Boolean obj5 = new Boolean("false");
        // 通过字符串"Yes"创建Character对象false
        Boolean obj6 = new Boolean("Yes");
        // 通过字符串"abc"创建Character对象false
        Boolean obj7 = new Boolean("abc");


        boolean b1 = Boolean.parseBoolean("true");
        boolean b2 = Boolean.parseBoolean("True");
        boolean b3 = Boolean.parseBoolean("TRUE");
        boolean b4 = Boolean.parseBoolean("false");
        boolean b5 = Boolean.parseBoolean("Yes");
        boolean b6 = Boolean.parseBoolean("abc");
        ...
    }
}

13.2.4 自动装箱、拆箱

自动装箱( autoboxing ),装箱能够自动地将基本数据类型的数值自动转换为包装类对象,而不需要使用构造方法。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        Integer objInt = new Integer(80);
        Double objDouble = new Double(80.0);
        //自动拆箱
        double sum = objInt + objDouble;

        //自动装箱
        //自动装箱'C'转换为Character对象
        Character objChar = 'C';
        //自动装箱true转换为Boolean对象
        Boolean objBoolean = true;
        //自动装箱80.0f转换为Float对象
        Float objFloat = 80.0f;

        //自动装箱100转换为Integer对象
        display(100);

        //避免出现下面的情况
        Integer obj = null;                                          ①
        int intVar = obj;    //运行期异常NullPointerException        ②

    }

    /**
     * @param objInt Integer对象
     * @return int数值
     */
    public static int display(Integer objInt) {

        System.out.println(objInt);

        //return objInt.intValue();
        //自动拆箱Integer对象转换为int
        return objInt;
    }
}

在自动装箱和拆箱时,要避免空对象,代码第①行obj是null,则代码第②行会发生运行期NullPointerException异常,这是因为拆箱的过程本质上是调用intValue()方法实现的,试图访问空对象的方法和成员变量,就会抛出运行期NullPointerException异常。

13.3 Math类

Java语言是彻底地面向对象语言,哪怕是进行数学运算也封装到一个类中的,这个类是java.lang.Math,Math类是final的不能被继承。Math类中包含用于进行基本数学运算的方法,如指数、对数、平方根和三角函数等。这些方法分类如下:

  1. 舍入方法

    • static double ceil(double a):返回大于或等于a最小整数。
    • static double floor(double a):返回小于或等于a最大整数。
    • static int round(float a):四舍五入方法。
  2. 最大值和最小值

    • static int min(int a, int b):取两个int整数中较小的一个整数。
    • static int min(long a, long b):取两个long整数中较小的一个整数。
    • static int min(float a, float b):取两个float浮点数中较小的一个浮点数。
    • static int min(double a, double b):取两个double浮点数中较小的一个浮点数。

    max方法取两个数中较大的一个数,max方法与min方法参数类似也有4个版本,这里不再赘述。

  3. 绝对值

    • static int abs(int a):取int整数a的绝对值。
    • static long abs(long a):取long整数a的绝对值。
    • static float abs(float a):取float浮点数a的绝对值。
    • static double abs(double a):取double浮点数a的绝对值。
  4. 三角函数:

    • static double sin(double a):返回角的三角正弦。
    • static double cos(double a):返回角的三角余弦。
    • static double tan(double a):返回角的三角正切。
    • static double asin(double a):返回一个值的反正弦。
    • static double acos(double a):返回一个值的反余弦。
    • static double atan(double a):返回一个值的反正切。
    • static double toDegrees(double angrad):将弧度转换为角度。
    • static double toRadians(double angdeg):将角度转换为弧度。
  5. 对数运算:static double log(double a),返回a的自然对数。

  6. 平方根:static double sqrt(double a),返回a的正平方根。

  7. 幂运算:static double pow(double a, double b),返回第一个参数的第二个参数次幂的值。

  8. 计算随机值:static double random(),返回大于等于 0.0 且小于 1.0随机数。

  9. 常量

    • 圆周率PI
    • 自然对数的底数E。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        double[] nums = { 1.4, 1.5, 1.6 };

        // 测试最大值和最小值
        System.out.printf("min(%.1f, %.1f) = %.1f\n", nums[1], nums[2], Math.min(nums[1], nums[2]));
        System.out.printf("max(%.1f, %.1f) = %.1f\n", nums[1], nums[2], Math.max(nums[1], nums[2]));
        System.out.println();

        // 测试三角函数
        // 1π弧度 = 180°
        System.out.printf("toDegrees(0.5π)    = %f\n", Math.toDegrees(0.5 * Math.PI));
        System.out.printf("toRadians(180/π) = %f\n", Math.toRadians(180 / Math.PI));
        System.out.println();

        // 测试平方根
        System.out.printf("sqrt(%.1f) = %f\n", nums[2], Math.sqrt(nums[2]));
        System.out.println();

        // 测试幂运算
        System.out.printf("pow(8, 3) = %f\n", Math.pow(8, 3));
        System.out.println();

        // 测试计算随机值
        System.out.printf("0.0~1.0之间的随机数 = %f\n", Math.random());
        System.out.println();

        // 测试舍入
        for (double num : nums) {
            display(num);
        }

    }

    // 测试舍入方法
    public static void display(double n) {
        System.out.printf("ceil(%.1f)    = %.1f\n", n, Math.ceil(n));
        System.out.printf("floor(%.1f)     = %.1f\n", n, Math.floor(n));
        System.out.printf("round(%.1f)     = %d\n", n, Math.round(n));
        System.out.println();
    }
}

运行结果如下:

min(1.5, 1.6) = 1.5
max(1.5, 1.6) = 1.6

toDegrees(0.5π)    = 90.000000
toRadians(180/π) = 1.000000

sqrt(1.6) = 1.264911

pow(8, 3) = 512.000000

0.0~1.0之间的随机数 = 0.881115

ceil(1.4)    = 2.0
floor(1.4)     = 1.0
round(1.4)     = 1

ceil(1.5)    = 2.0
floor(1.5)     = 1.0
round(1.5)     = 2

ceil(1.6)    = 2.0
floor(1.6)     = 1.0
round(1.6)     = 2

13.4 大数值

对货币等大值数据进行计算时,int、long、float和double等基本数据类型已经在精度方面不能满足需求了。为此Java提高了两个大数值类:BigInteger和BigDecimal,这里两个类都继承自Number抽象类。

13.4 日期时间

java.util 包提供了 Date 类来封装当前的日期和时间。 Date 类提供两个构造函数来实例化 Date 对象。

13.4.1 获取当前日期时间

import java.util.Date;
  
public class DateDemo {
   public static void main(String args[]) {
       // 初始化 Date 对象
       Date date = new Date();
        
       // 使用 toString() 函数显示日期时间
       System.out.println(date.toString());
   }
}

以上实例编译运行结果如下:

Mon May 04 09:51:52 CDT 2013

13.4.2 日期格式化

import java.util.Date;
 
public class DateDemo {
 
  public static void main(String args[]) {
     // 初始化 Date 对象
     Date date = new Date();
 
     //c的使用  
    System.out.printf("全部日期和时间信息:%tc%n",date);          
    //f的使用  
    System.out.printf("年-月-日格式:%tF%n",date);  
    //d的使用  
    System.out.printf("月/日/年格式:%tD%n",date);  
    //r的使用  
    System.out.printf("HH:MM:SS PM格式(12时制):%tr%n",date);  
    //t的使用  
    System.out.printf("HH:MM:SS格式(24时制):%tT%n",date);  
    //R的使用  
    System.out.printf("HH:MM格式(24时制):%tR",date);  
  }
}

以上实例译运行结果如下:

全部日期和时间信息:星期一 九月 10 10:43:36 CST 2012  
年-月-日格式:2012-09-10  
月/日/年格式:09/10/12  
HH:MM:SS PM格式(12时制):10:43:36 上午  
HH:MM:SS格式(24时制):10:43:36  
HH:MM格式(24时制):10:43  

第十四章 内部类

Java中还有一种内部类技术,简单说就是在一个类的内部定义一个类。内部类看起来很简单,但是当你深入其中,你会发现它是极其复杂的。事实上Java应用程序开发过程中内部类使用的地方不是很多,一般在图形用户界面开发中用于事件处理。

提示 内部类技术虽然使程序结构变得紧凑,但是却在一定程度上破坏了Java面向对象思想。

14.1 内部类概述

Java语言中允许在一个类(或方法、代码块)的内部定义另一个类,后者称为“内部类”(Inner Classes),也称为“嵌套类”(Nested Classes),封装它的类称为“外部类”。内部类与外部类之间存在逻辑上的隶属关系,内部类一般只用在封装它的外部类或代码块中使用。

14.1.1 内部类的作用

内部类的作用如下:

  1. 封装。将不想公开的实现细节封装到一个内部类中,内部类可以声明为私有的,只能在所在外部类中访问。
  2. 提供命名空间。静态内部类和外部类能够提供有别于包的命名空间。
  3. 便于访问外部类成员。内部类能够很方便访问所在外部类的成员,包括私有成员也能访问。

14.1.2 内部类的分类

按照内部类在定义的时候是否给它一个类名,可以分为:有名内部类和匿名内部类。有名内部类又按照作用域不同可以分为:局部内部类和成员内部类,成员内部类又分为:实例内部类和静态内部类。

14.2 成员内部类

成员内部类类似于外部类的成员变量,在外边类的内部,且方法体和代码块之外定义的内部类。

14.2.1 实例内部类

实例内部类与实例变量类似,可以声明为公有级别、私有级别、默认级别或保护级别,即4种访问级别都可以,而外部类只能声明为公有或默认级别。

实例内部类示例代码如下:

//Outer.java文件
package com.a51work6;

//外部类
public class Outer {

    // 外部类成员变量
    private int x = 10;

    // 外部类方法
    private void print() {
        System.out.println("调用外部方法...");
    }

    // 测试调用内部类
    public void test() {
        Inner inner = new Inner();
        inner.display();
    }

    // 内部类
    class Inner {                       ①

        // 内部类成员变量
        private int x = 5;              ②

        // 内部类方法
        void display() {                ③

            // 访问外部类的成员变量x
            System.out.println("外部类成员变量 x = " + Outer.this.x);      ④
            // 访问内部类的成员变量x
            System.out.println("内部类成员变量 x = " + this.x);            ⑤
            System.out.println("内部类成员变量 x = " + x);                 ⑥

            // 调用外部类的成员方法
            Outer.this.print();         ⑦
            print();                    ⑧
        }
    }
}

14.2.2 静态内部类

静态内部类与静态变量类似,在声明的时候使用关键字static修饰,静态内部类只能访问外部类静态成员,所以静态内部类使用的场景不多。但可以提供有别于包的命名空间。

示例代码如下:

//View.java文件
package com.a51work6;

//外部类
public class View {

    // 外部类实例变量
    private int x = 20;                 ①
    // 外部类静态变量
    private static int staticX = 10;    ②

    // 静态内部类
    static class Button {               ③

        // 内部类方法
        void onClick() {                ④
            //访问外部类的静态成员
            System.out.println(staticX);            ⑤
            //不能访问外部类的非静态成员
            // System.out.println(x); //编译错误    ⑥
        }
    }
}

14.3 局部内部类

局部内部类就是在方法体或代码块中定义的内部类,局部内部类的作用域仅限于方法体或代码块中。局部内部类访问级别只能是默认的,不能是公有的、私有的和保护的访问级别,即不能使用public、private和protected修饰。局部内部类也不能是静态,即不能使用static修饰。局部内部类可以访问外部类所有成员。

示例代码如下:

//Outer.java文件
package com.a51work6;

//外部类
public class Outer {

    // 外部类成员变量
    private int value = 10;

    // 外部类方法
    public void add(final int x, int y) {     ①
        //局部变量
        int z = 100;

        // 定义内部类
        class Inner {                         ②
            // 内部类方法
            void display() {
                int sum = x + z + value;      ③
                System.out.println("sum = " + sum);
            }
        }

        // Inner inner = new Inner();
        // inner.display();
        //声明匿名对象
        new Inner().display();                ④
    }
}

14.4 匿名内部类

匿名内部类是没有名字的内部类,本质上是没有名的局部内部类,具有局部内部类所有特征。例如:可以访问外部类所有成员。如果匿名内部类在方法中定义,它所访问的参数需要声明为final的。

下面通过示例介绍一下匿名内部类。有如下一个View类:

//View.java文件
package com.a51work6;

//外部类
public class View {

    public void handler(OnClickListener listener) {        ①
        listener.onClick();
    }
}

第十五章 Java8函数式编程基础—Lambda表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
使用 Lambda 表达式可以使代码变的更加简洁紧凑。

lambda 表达式的语法格式如下:

(parameters) -> expression
或
(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

15.1 Lambda 表达式实例

Lambda 表达式的简单例子:

// 1. 不需要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)
public class Java8Tester {
   public static void main(String args[]){
      Java8Tester tester = new Java8Tester();
        
      // 类型声明
      MathOperation addition = (int a, int b) -> a + b;
        
      // 不用类型声明
      MathOperation subtraction = (a, b) -> a - b;
        
      // 大括号中的返回语句
      MathOperation multiplication = (int a, int b) -> { return a * b; };
        
      // 没有大括号及返回语句
      MathOperation division = (int a, int b) -> a / b;
        
      System.out.println("10 + 5 = " + tester.operate(10, 5, addition));
      System.out.println("10 - 5 = " + tester.operate(10, 5, subtraction));
      System.out.println("10 x 5 = " + tester.operate(10, 5, multiplication));
      System.out.println("10 / 5 = " + tester.operate(10, 5, division));
        
      // 不用括号
      GreetingService greetService1 = message ->
      System.out.println("Hello " + message);
        
      // 用括号
      GreetingService greetService2 = (message) ->
      System.out.println("Hello " + message);
        
      greetService1.sayMessage("Runoob");
      greetService2.sayMessage("Google");
   }
    
   interface MathOperation {
      int operation(int a, int b);
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
    
   private int operate(int a, int b, MathOperation mathOperation){
      return mathOperation.operation(a, b);
   }
}

执行以上脚本,输出结果为:

$ javac Java8Tester.java 
$ java Java8Tester
10 + 5 = 15
10 - 5 = 5
10 x 5 = 50
10 / 5 = 2
Hello Runoob
Hello Google

使用 Lambda 表达式需要注意以下两点:

  • Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
  • Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。

15.2 变量作用域

lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。

public class Java8Tester {
 
   final static String salutation = "Hello! ";
   
   public static void main(String args[]){
      GreetingService greetService1 = message -> 
      System.out.println(salutation + message);
      greetService1.sayMessage("Runoob");
   }
    
   interface GreetingService {
      void sayMessage(String message);
   }
}

执行以上脚本,输出结果为:

$ javac Java8Tester.java 
$ java Java8Tester
Hello! Runoob

我们也可以直接在 lambda 表达式中访问外层的局部变量:

public class Java8Tester {
    public static void main(String args[]) {
        final int num = 1;
        Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
        s.convert(2);  // 输出结果为 3
    }
 
    public interface Converter<T1, T2> {
        void convert(int i);
    }
}

lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改(即隐性的具有 final 的语义)

int num = 1;  
Converter<Integer, String> s = (param) -> System.out.println(String.valueOf(param + num));
s.convert(2);
num = 5;  
//报错信息:Local variable num defined in an enclosing scope must be final or effectively 
 final

在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

String first = "";  
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(), second.length());  //编译会出错 

第十五章 异常处理

15.1 Throwable类

所有的异常类都直接或间接地继承于java.lang.Throwable类,在Throwable类有几个非常重要的方法:

  • String getMessage():获得发生异常的详细消息。
  • void printStackTrace():打印异常堆栈跟踪信息。
  • String toString():获得异常对象的描述。

提示 堆栈跟踪是方法调用过程的轨迹,它包含了程序执行过程中方法调用的顺序和所在源代码行号。

15.2 Error和Exception

Throwable有两个直接子类:Error和Exception。

  1. Error

    Error是程序无法恢复的严重错误,程序员根本无能为力,只能让程序终止。例如:JVM内部错误、内存溢出和资源耗尽等严重情况。

  2. Exception

    Exception是程序可以恢复的异常,它是程序员所能掌控的。例如:除零异常、空指针访问、网络连接中断和读取不存在的文件等。本章所讨论的异常处理就是对Exception及其子类的异常处理。

15.3 捕捉异常

15.3.1 try-catch语句

捕获异常是通过try-catch语句实现的,最基本try-catch语句语法如下:

try{
    //可能会发生异常的语句
} catch(Throwable e){
    //处理异常e
}

15.3.2 多catch代码块

如果try代码块中有很多语句会发生异常,而且发生的异常种类又很多。那么可以在try后面跟有多个catch代码块。多catch代码块语法如下:

try{
    //可能会发生异常的语句
} catch(Throwable e){
    //处理异常e
} catch(Throwable e){
    //处理异常e
} catch(Throwable e){
    //处理异常e
}

15.3.3 多重捕获

多catch代码块客观上提高了程序的健壮性,但是程序代码量大大增加。如果有些异常虽然种类不同,但捕获之后的处理是相同的,看如下代码。

try{
    //可能会发生异常的语句
} catch (FileNotFoundException e) {
    //调用方法methodA处理
} catch (IOException e) {
    //调用方法methodA处理
} catch (ParseException e) {
    //调用方法methodA处理
}

三个不同类型的异常,要求捕获之后的处理都是调用methodA方法。是否可以把这些异常合并处理,Java 7推出了多重捕获(multi-catch)技术,可以帮助解决此类问题,上述代码修改如下:

try{
    //可能会发生异常的语句
} catch (IOException | ParseException e) {
    //调用方法methodA处理
}

15.4 释放资源

有时在try-catch语句中会占用一些非Java资源,如:打开文件、网络连接、打开数据库连接和使用数据结果集等,这些资源并非Java资源,不能通过JVM的垃圾收集器回收,需要程序员释放。为了确保这些资源能够被释放可以使用finally代码块或Java 7之后提供自动资源管理(Automatic Resource Management)技术。

15.4.1 finally代码块

try-catch语句后面还可以跟有一个finally代码块,try-catch-finally语句语法如下:

try{
    //可能会生成异常语句
} catch(Throwable e1){
    //处理异常e1
} catch(Throwable e2){
    //处理异常e2
} catch(Throwable eN){
    //处理异常eN
} finally{
    //释放资源
}

无论try正常结束还是catch异常结束都会执行finally代码块

15.4.2 自动资源管理

15.4.1节使用finally代码块释放资源会导致程序代码大量增加,一个finally代码块往往比正常执行的程序还要多。在Java 7之后提供自动资源管理(Automatic Resource Management)技术,可以替代finally代码块,优化代码结构,提高程序可读性。

自动资源管理是在try语句上的扩展,语法如下:

try (声明或初始化资源语句) {
    //可能会生成异常语句
} catch(Throwable e1){
    //处理异常e1
} catch(Throwable e2){
    //处理异常e1
} catch(Throwable eN){
    //处理异常eN
}

在try语句后面添加一对小括号“()”,其中是声明或初始化资源语句,可以有多条语句语句之间用分号“;”分隔。

15.5 throws与声明方法抛出异常

在一个方法中如果能够处理异常,则需要捕获并处理。但是本方法没有能力处理该异常,捕获它没有任何意义,则需要在方法后面声明抛出该异常,通知上层调用者该方法有可以发生异常。

方法后面声明抛出使用throws关键字,成员方法语法格式如下:

     class className {

             [public | protected | private ] [static] [final | abstract] [native] [synchronized]
                     type methodName([paramList]) [throws exceptionList] {
                         //方法体
            }
     }

其中参数列表之后的[throws exceptionList]语句是声明抛出异常。方法中可能抛出的异常(除了Error和RuntimeException及其子类外)都必须通过throws语句列出,多个异常之间采用逗号(,)分隔。

15.6 自定义异常类

有些公司为了提高代码的可重用性,自己开发了一些Java类库或框架,其中少不了自己编写了一些异常类。实现自定义异常类需要继承Exception类或其子类,如果自定义运行时异常类需继承RuntimeException类或其子类。

实现自定义异常类示例代码如下:

     package com.a51work6;

     public class MyException extends Exception {    ①

         public MyException() {                      ②

         }

         public MyException(String message) {        ③
             super(message);
         }

     }

15.7 throw与显式抛出异常

Java异常相关的关键字中有两个非常相似,它们是throws和throw,其中throws关键字前面19.5节已经介绍了,throws用于方法后声明抛出异常,而throw关键字用来人工引发异常。本节之前读者接触到的异常都是由于系统生成的,当异常发生时,系统会生成一个异常对象,并将其抛出。但也可以通过throw语句显式抛出异常,语法格式如下:

throw Throwable或其子类的实例

第十六章 对象容器—集合

集合本质是基于某种数据结构数据容器。常见的数据结构:数组(Array)、集(Set)、队列(Queue)、链表(Linkedlist)、树(Tree)、堆(Heap)、栈(Stack)和映射(Map)等结构。

16.1 集合概述

Java中提供了丰富的集合接口和类,它们来自于java.util包。如图20-1所示是Java主要的集合接口和类,从图中可见Java集合类型分为:Collection和Map,Collection子接口有:Set、Queue和List等接口。每一种集合接口描述了一种数据结构。

提示 学习Java中的集合,首先从两大接口入手,重点掌握List、Set和Map三个接口,熟悉这些接口中提供的方法。然后再熟悉这些接口的实现类,并了解不同实现类之间的区别。

16.2 List集合

提示 List集合关心的元素是否有序,而不关心是否重复,请大家记住这个原则。

List接口的实现类有:ArrayList 和 LinkedList。ArrayList是基于动态数组数据结构的实现,LinkedList是基于链表数据结构的实现。ArrayList访问元素速度优于LinkedList,LinkedList占用的内存空间比较大,但LinkedList在批量插入或删除数据时优于ArrayList。

16.2.1 常用方法

List接口继承自Collection接口,List接口中的很多方法都继承自Collection接口的。List接口中常用方法如下。

  1. 操作元素
    • get(int index):返回List集合中指定位置的元素。
    • set(int index, Object element):用指定元素替换List集合中指定位置的元素。
    • add(Object element):在List集合的尾部添加指定的元素。该方法是从Collection集合继承过来的。
    • add(int index, Object element):在List集合的指定位置插入指定元素。
    • remove(int index):移除List集合中指定位置的元素。
    • remove(Object element):如果List集合中存在指定元素,则从List集合中移除第一次出现的指定元素。该方法是从Collection集合继承过来的。
    • clear():从List集合中移除所有元素。该方法是从Collection集合继承过来的。
  2. 判断元素
    • isEmpty():判断List集合中是否有元素,没有返回true,有返回false。该方法是从Collection集合继承过来的。
    • contains(Object element):判断List集合中是否包含指定元素,包含返回true,不包含返回false。该方法是从Collection集合继承过来的。
  3. 查询元素
    • indexOf(Object o):从前往后查找List集合元素,返回第一次出现指定元素的索引,如果此列表不包含该元素,则返回-1。
    • lastIndexOf(Object o):从后往前查找List集合元素,返回第一次出现指定元素的索引,如果此列表不包含该元素,则返回-1。
  4. 其他
    • iterator():返回迭代器(Iterator)对象,迭代器对象用于遍历集合。该方法是从Collection集合继承过来的。
    • size():返回List集合中的元素数,返回值是int类型。该方法是从Collection集合继承过来的。
    • subList(int fromIndex, int toIndex):返回List集合中指定的 fromIndex(包括 )和 toIndex(不包括)之间的元素集合,返回值为List集合。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.util.ArrayList;
import java.util.List;

public class HelloWorld {

    public static void main(String[] args) {

        List list = new ArrayList();                     ①

        String b = "B";

        //向集合中添加元素
        list.add("A");
        list.add(b);                                     ②
        list.add("C");
        list.add(b);                                     ③
        list.add("D");
        list.add("E");

        //打印集合元素个数
        System.out.println("集合size = " + list.size());
        //打印集合
        System.out.println(list);

        //从前往后查找集合中的"B"元素
        System.out.println("indexOf(\"B\") = " + list.indexOf(b));
        //从后往前查找集合中的"B"元素
        System.out.println("lastIndexOf(\"B\") = " + list.lastIndexOf(b));

        //删除集合中第一个"B"元素
        list.remove(b);
        System.out.println("remove(3)前: " + list);
        //判断集合中是否包含"B"元素
        System.out.println("是否包含\"B\":" + list.contains(b));

        //删除集合第4个元素
        list.remove(3);
        System.out.println("remove(3)后: " + list);
        //判断集合是否为空
        System.out.println("list集合是空的:" + list.isEmpty());

        System.out.println("替换前:" + list);
        //替换集合第2个元素
        list.set(1, "F");
        System.out.println("替换后:" + list);

        //清空集合
        list.clear();                                    ④
        System.out.println(list);


        // 重新添加元素
        list.add(1);//发生自动装箱                       ⑤
        list.add(3);

        int item = (Integer)list.get(0);//发生自动拆箱   ⑥
    }
}

运行结果如下:

集合size = 6
[A, B, C, B, D, E]
indexOf("B") = 1
lastIndexOf("B") = 3
remove(3)前: [A, C, B, D, E]
是否包含"B":true
remove(3)后: [A, C, B, E]
list集合是空的:false
替换前:[A, C, B, E]
替换后:[A, F, B, E]
[]

16.2.2 遍历集合

集合最常用的操作之一是遍历,遍历就是将集合中的每一个元素取出来,进行操作或计算。List集合遍历有三种方法:

  1. 使用for循环遍历:List集合可以使用for循环进行遍历,for循环中有循环变量,通过循环变量可以访问List集合中的元素。
  2. 使用for-each循环遍历:for-each循环是针对遍历各种类型集合而推出的,笔者推荐使用这种遍历方法。
  3. 使用迭代器遍历:Java提供了多种迭代器,List集合可以使用Iterator和ListIterator迭代器。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class HelloWorld {

    public static void main(String[] args) {

        List list = new ArrayList();

        String b = "B";
        // 向集合中添加元素
        list.add("A");
        list.add(b);
        list.add("C");
        list.add(b);
        list.add("D");
        list.add("E");

        // 1.使用for循环遍历
        System.out.println("--1.使用for循环遍历--");
        for (int i = 0; i < list.size(); i++) {
            System.out.printf("读取集合元素(%d): %s \n", i, list.get(i));      ①
        }

        // 2.使用for-each循环遍历
        System.out.println("--2.使用for-each循环遍历--");
        for (Object item : list) {                                 ②
            String s = (String) item;                              ③
            System.out.println("读取集合元素: " + s);
        }

        // 3.使用迭代器遍历
        System.out.println("--3.使用迭代器遍历--");
        Iterator it = list.iterator();                             ④
        while (it.hasNext()) {                                     ⑤
            Object item = it.next();                               ⑥
            String s = (String) item;                              ⑦
            System.out.println("读取集合元素: " + s);
        }
    }
}

16.3 Set集合

16.3.3 常用方法

Set接口也继承自Collection接口,Set接口中大部分都是继承自Collection接口,这些方法如下。

  1. 操作元素
    • add(Object element):在Set集合的尾部添加指定的元素。该方法是从Collection集合继承过来的。
    • remove(Object element):如果Set集合中存在指定元素,则从Set集合中移除该元素。该方法是从Collection集合继承过来的。
    • clear():从Set集合中移除所有元素。该方法是从Collection集合继承过来的。
  2. 判断元素
    • isEmpty():判断Set集合中是否有元素,没有返回true,有返回false。该方法是从Collection集合继承过来的。
    • contains(Object element):判断Set集合中是否包含指定元素,包含返回true,不包含返回false。该方法是从Collection集合继承过来的。
  3. 其他
    • iterator():返回迭代器(Iterator)对象,迭代器对象用于遍历集合。该方法是从Collection集合继承过来的。
    • size():返回Set集合中的元素数,返回值是int类型。该方法是从Collection集合继承过来的。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.util.HashSet;
import java.util.Set;

public class HelloWorld {

    public static void main(String[] args) {

        Set set = new HashSet();                        ①

        String b = "B";

        // 向集合中添加元素
        set.add("A");
        set.add(b);                                      ②
        set.add("C");
        set.add(b);                                      ③
        set.add("D");
        set.add("E");

        // 打印集合元素个数
        System.out.println("集合size = " + set.size());        ④
        // 打印集合
        System.out.println(set);

        // 删除集合中第一个"B"元素
        set.remove(b);
        // 判断集合中是否包含"B"元素
        System.out.println("是否包含\"B\":" + set.contains(b));
        // 判断集合是否为空
        System.out.println("set集合是空的:" + set.isEmpty());

        // 清空集合
        set.clear();
        System.out.println(set);
    }
}

运行结果:

集合size = 5
[A, B, C, D, E]
是否包含"B":false
set集合是空的:false
[]

16.3.2 遍历集合

Set集合中的元素由于没有序号,所以不能使用for循环进行遍历,但可以使用for-each循环和迭代器进行遍历。事实上这两种遍历方法也是继承自Collection集合,也就是说所有的Collection集合类型都有这两种遍历方式。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class HelloWorld {

    public static void main(String[] args) {

        Set set = new HashSet();

        String b = "B";
        // 向集合中添加元素
        set.add("A");
        set.add(b);
        set.add("C");
        set.add(b);
        set.add("D");
        set.add("E");

        // 1.使用for-each循环遍历
        System.out.println("--1.使用for-each循环遍历--");
        for (Object item : set) {
            String s = (String) item;
            System.out.println("读取集合元素: " + s);
        }

        // 2.使用迭代器遍历
        System.out.println("--2.使用迭代器遍历--");
        Iterator it = set.iterator();
        while (it.hasNext()) {
            Object item = it.next();
            String s = (String) item;
            System.out.println("读取集合元素: " + s);
        }
    }
}

16.4 Map集合

Map(映射)集合表示一种非常复杂的集合,允许按照某个键来访问元素。Map集合是由两个集合构成的,一个是键(key)集合,一个是值(value)集合。键集合是Set类型,因此不能有重复的元素。而值集合是Collection类型,可以有重复的元素。Map集合中的键和值是成对出现的。

16.4.1 常用方法

Map集合中包含两个集合(键和值),所以操作起来比较麻烦,Map接口提供很多方法用来管理和操作集合

  1. 操作元素
    • get(Object key):返回指定键所对应的值;如果Map集合中不包含该键值对,则返回null。
    • put(Object key, Object value):指定键值对添加到集合中。
    • remove(Object key):移除键值对。
    • clear():移除Map集合中所有键值对。
  2. 判断元素
    • isEmpty():判断Map集合中是否有键值对,没有返回true,有返回false。
    • containsKey(Object key):判断键集合中是否包含指定元素,包含返回true,不包含返回false。
    • containsValue(Object value):判断值集合中是否包含指定元素,包含返回true,不包含返回false。
  3. 查看集合
    • keySet():返回Map中的所有键集合,返回值是Set类型。
    • values():返回Map中的所有值集合,返回值是Collection类型。
    • size():返回Map集合中键值对数。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.util.HashMap;
import java.util.Map;

public class HelloWorld {

    public static void main(String[] args) {

        Map map = new HashMap();                ①

        map.put(102, "张三");
        map.put(105, "李四");                    ②
        map.put(109, "王五");
        map.put(110, "董六");
        //"李四"值重复
        map.put(111, "李四");                    ③
        //109键已经存在,替换原来值"王五"
        map.put(109, "刘备");                    ④


        // 打印集合元素个数
        System.out.println("集合size = " + map.size());
        // 打印集合
        System.out.println(map);

        // 通过键取值
        System.out.println("109 - " + map.get(109));    ⑤
        System.out.println("108 - " + map.get(108));    ⑥

        // 删除键值对
        map.remove(109);
        // 判断键集合中是否包含109
        System.out.println("键集合中是否包含109:" + map.containsKey(109));
        // 判断值集合中是否包含 "李四"
        System.out.println("值集合中是否包含:" + map.containsValue("李四"));

        // 判断集合是否为空
        System.out.println("集合是空的:" + map.isEmpty());

        // 清空集合
        map.clear();
        System.out.println(map);
    }
}

运行结果如下:

集合size = 5
{102=张三, 105=李四, 109=王五, 110=董六, 111=刘备}
109 - 王五
108 - null
是否包含"B":false
值集合中是否包含:true
集合是空的:false
{}

16.4.2 遍历集合

Map集合遍历与List和Set集合不同,Map有两个集合,因此遍历时可以只遍历值的集合,也可以只遍历键的集合,也可以同时遍历。这些遍历过程都可以使用for-each循环和迭代器进行遍历。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class HelloWorld {

    public static void main(String[] args) {

        Map map = new HashMap();

        map.put(102, "张三");
        map.put(105, "李四");
        map.put(109, "王五");
        map.put(110, "董六");
        map.put(111, "李四");

        // 1.使用for-each循环遍历
        System.out.println("--1.使用for-each循环遍历--");
        // 获得键集合
        Set keys = map.keySet();                                    ①
        for (Object key : keys) {
            int ikey = (Integer) key; // 自动拆箱                   ②
            String value = (String) map.get(ikey); // 自动装箱      ③
            System.out.printf("key=%d - value=%s \n", ikey, value);
        }

        // 2.使用迭代器遍历
        System.out.println("--2.使用迭代器遍历--");
        // 获得值集合
        Collection values = map.values();                           ④
        // 遍历值集合
        Iterator it = values.iterator();
        while (it.hasNext()) {
            Object item = it.next();
            String s = (String) item;
            System.out.println("值集合元素: " + s);
        }

    }
}

第十七章 泛型

ava 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

17.1 泛型方法

你可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用。

下面是定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前(在下面例子中的)。
  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。
  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。
  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)。

下面的例子演示了如何使用泛型方法打印不同类型的数组元素:

public class GenericMethodTest
{
   // 泛型方法 printArray                         
   public static < E > void printArray( E[] inputArray )
   {
      // 输出数组元素            
         for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }
 
    public static void main( String args[] )
    {
        // 创建不同类型数组: Integer, Double 和 Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
 
        System.out.println( "整型数组元素为:" );
        printArray( intArray  ); // 传递一个整型数组
 
        System.out.println( "\n双精度型数组元素为:" );
        printArray( doubleArray ); // 传递一个双精度型数组
 
        System.out.println( "\n字符型数组元素为:" );
        printArray( charArray ); // 传递一个字符型数组
    } 
}

编译以上代码,运行结果如下所示:

整型数组元素为:
1 2 3 4 5 

双精度型数组元素为:
1.1 2.2 3.3 4.4 

字符型数组元素为:
H E L L O 

17.2 泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。

和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。

如下实例演示了我们如何定义一个泛型类:

public class Box<T> {
   
  private T t;
 
  public void add(T t) {
    this.t = t;
  }
 
  public T get() {
    return t;
  }
 
  public static void main(String[] args) {
    Box<Integer> integerBox = new Box<Integer>();
    Box<String> stringBox = new Box<String>();
 
    integerBox.add(new Integer(10));
    stringBox.add(new String("菜鸟教程"));
 
    System.out.printf("整型值为 :%d\n\n", integerBox.get());
    System.out.printf("字符串为 :%s\n", stringBox.get());
  }
}

编译以上代码,运行结果如下所示:

整型值为 :10
字符串为 :菜鸟教程

17.3 类型通配符

  1. 类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类。
import java.util.*;
 
public class GenericTest {
     
    public static void main(String[] args) {
        List<String> name = new ArrayList<String>();
        List<Integer> age = new ArrayList<Integer>();
        List<Number> number = new ArrayList<Number>();
        
        name.add("icon");
        age.add(18);
        number.add(314);
 
        getData(name);
        getData(age);
        getData(number);
       
   }
 
   public static void getData(List<?> data) {
      System.out.println("data :" + data.get(0));
   }
}

输出结果为:

data :icon
data :18
data :314

解析: 因为getData()方法的参数是List类型的,所以name,age,number都可以作为这个方法的实参,这就是通配符的作用

  1. 类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。
import java.util.*;
 
public class GenericTest {
     
    public static void main(String[] args) {
        List<String> name = new ArrayList<String>();
        List<Integer> age = new ArrayList<Integer>();
        List<Number> number = new ArrayList<Number>();
        
        name.add("icon");
        age.add(18);
        number.add(314);
 
        //getUperNumber(name);//1
        getUperNumber(age);//2
        getUperNumber(number);//3
       
   }
 
   public static void getData(List<?> data) {
      System.out.println("data :" + data.get(0));
   }
   
   public static void getUperNumber(List<? extends Number> data) {
          System.out.println("data :" + data.get(0));
       }
}j

输出结果:

data :18
data :314

17.4 自定义泛型类

//Queue.java文件
package com.a51work6;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义的泛型队列集合
 */
public class Queue<T> {                         ①

    // 声明保存队列元素集合items
    private List<T> items;                      ②

    // 构造方法初始化是集合items
    public Queue() {
        this.items = new ArrayList<T>();        ③
    }

    /**
     * 入队方法
     * @param item 参数需要入队的元素
     */
    public void queue(T item) {                 ④
        this.items.add(item);
    }

    /**
     * 出队方法
     * @return 返回出队元素
     */
    public T dequeue(){                         ⑤
        if (items.isEmpty()) {
            return null;
        } else {
            return this.items.remove(0);        ⑥
        }
    }

    @Override
    public String toString() {
        return items.toString();
    }

17.5 自定义泛型接口

自定义泛型接口与自定义泛型类类似,定义的方式完全一样。

//IQueue.java文件
package com.a51work6;

/**
 * 自定义的泛型队列集合
 */
public interface IQueue<T> {                    ①

    /**
     * 入队方法
     * @param item 参数需要入队的元素
     */
    public void queue(T item);                  ②

    /**
     * 出队方法
     * @return 返回出队元素
     */
    public T dequeue();                         ③

}

第十八章 文件管理与I/O流

18.1 文件管理

Java语言使用File类对文件和目录进行操作,查找文件时需要实现FilenameFilter或FileFilter接口。另外,读写文件内容可以通过FileInputStream、FileOutputStream、FileReader和FileWriter类实现,它们属于I/O流。这些类和接口全部来源于java.io包。

18.1.1 File类

File类表示一个与平台无关的文件或目录。File类名很有欺骗性,初学者会误认为是File对象只是一个文件,但它也可能是一个目录。

File类中常用的方法如下。

  1. 构造方法
    • File(String path):如果path是实际存在的路径,则该File对象表示的是目录;如果path是文件名,则该File对象表示的是文件。
    • File(String path, String name):path是路径名,name是文件名。
    • File(File dir, String name):dir是路径对象,name是文件名。
  2. 获得文件名
    • String getName( ):获得文件的名称,不包括路径。
    • String getPath( ):获得文件的路径。
    • String getAbsolutePath( ):获得文件的绝对路径。
    • String getParent( ):获得文件的上一级目录名。
  3. 文件属性测试
    • boolean exists( ):测试当前File对象所表示的文件是否存在。
    • boolean canWrite( ):测试当前文件是否可写。
    • boolean canRead( ):测试当前文件是否可读。
    • boolean isFile( ):测试当前文件是否是文件。
    • boolean isDirectory( ):测试当前文件是否是目录。
  4. 文件操作
    • long lastModified( ):获得文件最近一次修改的时间。
    • long length( ):获得文件的长度,以字节为单位。
    • boolean delete( ):删除当前文件。成功返回 true,否则返回false。
    • boolean renameTo(File dest):将重新命名当前File对象所表示的文件。成功返回 true,否则返回false。
  5. 目录操作
    • boolean mkdir( ):创建当前File对象指定的目录。
    • String[] list():返回当前目录下的文件和目录,返回值是字符串数组。
    • String[] list(FilenameFilter filter):返回当前目录下满足指定过滤器的文件和目录,参数是实现FilenameFilter接口对象,返回值是字符串数组。
    • File[] listFiles():返回当前目录下的文件和目录,返回值是File数组。
    • File[] listFiles(FilenameFilter filter):返回当前目录下满足指定过滤器的文件和目录,参数是实现FilenameFilter接口对象,返回值是File数组。
    • File[] listFiles(FileFilter filter):返回当前目录下满足指定过滤器的文件和目录,参数是实现FileFilter接口对象,返回值是File数组。

对目录操作有两个过滤器接口:FilenameFilter和FileFilter。它们都只有一个抽象方法accept,FilenameFilter接口中的accept方法如下:

  • boolean accept(File dir, String name):测试指定dir目录中是否包含文件名为name的文件。

FileFilter接口中的accept方法如下:

  • boolean accept(File pathname):测试指定路径名是否应该包含在某个路径名列表中。

注意 路径中会用到路径分隔符,路径分隔符在不同平台上是有区别的,UNIX、Linux和macOS中使用正斜杠“/”,而Windows下使用反斜杠“\”。Java是支持两种写法,但是反斜杠“\”属于特殊字符,前面需要加转义符。例如C:\Users\a.java在程序代码中应该使用C:\Users\a.java表示,或表示为C:/Users/a.java也可以。

18.1.2 案例:文件过滤

为熟悉文件操作,本节介绍一个案例,该案例从指定的目录中列出文件信息。代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.io.File;
import java.io.FilenameFilter;

public class HelloWorld {

    public static void main(String[] args) {

        // 用File对象表示一个目录,.表示当前目录
        File dir = new File("./TestDir");                            ①
        // 创建HTML文件过滤器
        Filter filter = new Filter("html");                          ②

        System.out.println("HTML文件目录:" + dir);
        // 列出目录TestDir下,文件后缀名为HTML的所有文件
        String files[] = dir.list(filter); //dir.list();
        // 遍历文件列表
        for (String fileName : files) {
            // 为目录TestDir下的文件或目录创建File对象
            File f = new File(dir, fileName);
            // 如果该f对象是文件,则打印文件名
            if (f.isFile()) {
                System.out.println("文件名:" + f.getName());
                System.out.println("文件绝对路径:" + f.getAbsolutePath());
                System.out.println("文件路径:" + f.getPath());
            } else {
                System.out.println("子目录:" + f);
            }
        }

    }
}

// 自定义基于文件扩展名的文件过滤器
class Filter implements FilenameFilter {                             ③

    // 文件扩展名
    String extent;

    // 构造方法
    Filter(String extent) {
        this.extent = extent;
    }

    @Override
    public boolean accept(File dir, String name) {                   ④
        // 测试文件扩展名是否为extent所指定的
        return name.endsWith("." + extent);
    }
}

18.2 I/O流概述

Java将数据的输入输出(I/O)操作当作“流”来处理,“流”是一组有序的数据序列。“流”分为两种形式:输入流和输出流,从数据源中读取数据是输入流,将数据写入到目的地是输出流。

提示 以CPU为中心,从外部设备读取数据到内存,进而再读入到CPU,这是输入(Input,缩写I)过程;将内存中的数据写入到外部设备,这是输出(Output,缩写O)过程。所以输入输出简称为I/O。

18.3 字节流

Java将数据的输入输出(I/O)操作当作“流”来处理,“流”是一组有序的数据序列。“流”分为两种形式:输入流和输出流,从数据源中读取数据是输入流,将数据写入到目的地是输出流。

提示 以CPU为中心,从外部设备读取数据到内存,进而再读入到CPU,这是输入(Input,缩写I)过程;将内存中的数据写入到外部设备,这是输出(Output,缩写O)过程。所以输入输出简称为I/O。

18.3.1 InputStream抽象类

InputStream是字节输入流的根类,它定义了很多方法,影响着字节输入流的行为。下面详细介绍一下。

InputStream主要方法如下:

  • int read():读取一个字节,返回0到255范围内的int字节值。如果已经到达流末尾,而且没有可用的字节,则返回值-1。
  • int read(byte b[] ):读取多个字节,数据放到字节数组b中,返回值为实际读取的字节的数量,如果已经到达流末尾,而且没有可用的字节,则返回值-1。
  • int read(byte b[ ], int off, int len):最多读取len个字节,数据放到以下标off开始字节数组b中,将读取的第一个字节存储在元素b[off]中,下一个存储在b[off+1]中,依次类推。返回值为实际读取的字节的数量,如果已经到达流末尾,而且没有可用的字节,则返回值-1。
  • void close():流操作完毕后必须关闭。

上述所有方法都可能会抛出IOException,因此使用时要注意处理异常。

18.3.2 OutputStream抽象类

OutputStream是字节输出流的根类,它定义了很多方法,影响着字节输出流的行为。下面详细介绍一下。

OutputStream主要方法如下:

  • void write(int b):将b写入到输出流,b是int类型占有32位,写入过程是写入b 的8个低位,b的24个高位将被忽略。
  • void write(byte b[ ]):将b.length个字节从指定字节数组b写入到输出流。
  • void write(byte b[ ], int off, int len):把字节数组b中从下标off开始,长度为len的字节写入到输出流。
  • void flush():刷空输出流,并输出所有被缓存的字节。由于某些流支持缓存功能,该方法将把缓存中所有内容强制输出到流中。
  • void close( ):流操作完毕后必须关闭。

上述所有方法都声明抛出IOException,因此使用时要注意处理异常。

注意 流(包括输入流和输出流)所占用的资源,不能通过JVM的垃圾收集器回收,需要程序员自己释放。一种方法是可以在finally代码块调用close()方法关闭流,释放流所占用的资源。另一种方法通过自动资源管理技术管理这些流,流(包括输入流和输出流)都实现了AutoCloseable接口,可以使用自动资源管理技术,具体内容参考19.4.2节。

18.3.3 案例:文件复制

前面介绍了两种字节流常用的方法,下面通过一个案例熟悉一下它们的使用,该案例实现了文件复制,数据源是文件,所以会用到文件输入流FileInputStream,数据目的地也是文件,所以会用到文件输出流FileOutputStream。

FileInputStream和FileOutputStream中主要方法都是继承自InputStream和OutputStream前面两个节已经详细介绍了,这里不再赘述。下面介绍一下它们的构造方法,FileInputStream构造方法主要有:

  • FileInputStream(String name):创建FileInputStream对象,name是文件名。如果文件不存在则抛出FileNotFoundException异常。
  • FileInputStream(File file):通过File对象创建FileInputStream对象。如果文件不存在则抛出FileNotFoundException异常。

FileOutputStream构造方法主要有:

  • FileOutputStream(String name):通过指定name文件名创建FileOutputStream对象。如果name文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。
  • FileOutputStream(String name, boolean append):通过指定name文件名创建FileOutputStream对象,append参数如果为 true,则将字节写入文件末尾处,而不是写入文件开始处。如果name文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。
  • FileOutputStream(File file):通过File对象创建FileOutputStream对象。如果file文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。
  • FileOutputStream(File file, boolean append):通过File对象创建FileOutputStream对象,append参数如果为 true,则将字节写入文件末尾处,而不是写入文件开始处。如果file文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。

下面介绍如果将./TestDir/build.txt文件内容复制到./TestDir/subDir/build.txt。./TestDir/build.txt文件内容是AI-162.3764568,实现代码如下:

     //FileCopy.java文件
     package com.a51work6;

     import java.io.FileInputStream;
     import java.io.FileNotFoundException;
     import java.io.FileOutputStream;
     import java.io.IOException;

     public class FileCopy {

         public static void main(String[] args) {

             try (FileInputStream in = new FileInputStream("./TestDir/build.txt");
                     FileOutputStream out = new FileOutputStream("./TestDir/subDir/build.txt")) {    ①

                 // 准备一个缓冲区
                 byte[] buffer = new byte[10];                  ②
                 // 首先读取一次
                 int len = in.read(buffer);                     ③

                 while (len != -1) {                            ④
                     String copyStr = new String(buffer);       ⑤
                     // 打印复制的字符串
                     System.out.println(copyStr);
                     // 开始写入数据
                     out.write(buffer, 0, len);                 ⑥
                     // 再读取一次
                     len = in.read(buffer);                     ⑦
                 }

             } catch (FileNotFoundException e) {
                 e.printStackTrace();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }

控制台输出结果:

     AI-162.376
     456862.376

18.4 字符流

上一节介绍了字节流,本节详细介绍一下字符流的API。掌握字符流的API先要熟悉它的两个抽象类:Reader和Writer,了解它们有哪些主要的方法。

18.4.1 Reader抽象类

Reader是字符输入流的根类,它定义了很多方法,影响着字符输入流的行为。下面详细介绍一下。

Reader主要方法如下:

  • int read():读取一个字符,返回值范围在065535(0x000xffff)之间。如果因为已经到达流末尾,则返回值-1。
  • int read(char[] cbuf):将字符读入到数组cbuf中,返回值为实际读取的字符的数量,如果因为已经到达流末尾,则返回值-1。
  • int read(char[] cbuf, int off, int len):最多读取len个字符,数据放到以下标off开始字符数组cbuf中,将读取的第一个字符存储在元素cbuf[off]中,下一个存储在cbuf[off+1]中,依次类推。返回值为实际读取的字符的数量,如果因为已经到达流末尾,则返回值-1。
  • void close():流操作完毕后必须关闭。

上述所有方法都声明了抛出IOException,因此使用时要注意处理异常。

18.4.2 Writer抽象类

Writer是字符输出流的根类,它定义了很多方法,影响着字符输出流的行为。下面详细介绍一下。

Writer主要方法如下:

  • void write(int c):将整数值为c的字符写入到输出流,c是int类型占有32位,写入过程是写入c的16个低位,c的16个高位将被忽略。
  • void write(char[] cbuf):将字符数组cbuf写入到输出流。
  • void write(char[] cbuf, int off, int len):把字符数组cbuf中从下标off开始,长度为len的字符写入到输出流。
  • void write(String str):将字符串str中的字符写入输出流。
  • void write(String str,int off,int len):将字符串str 中从索引off开始处的len个字符写入输出流。
  • void flush():刷空输出流,并输出所有被缓存的字符。由于某些流支持缓存功能,该方法将把缓存中所有内容强制输出到流中。
  • void close( ):流操作完毕后必须关闭。

上述所有方法都可以会抛出IOException,因此使用时要注意处理异常。

注意 Reader和Writer都实现了AutoCloseable接口,可以使用自动资源管理技术自动关闭它们。

18.4.3 案例:文件复制

前面两节介绍了字符流常用的方法,下面通过一个案例熟悉一下它们的使用,该案例实现了文件复制,数据源是文件,所以会用到文件输入流FileReader,数据目的地也是文件,所以会用到文件输出流FileWriter。

FileReader和FileWriter中主要方法都是继承自Reader和Writer前面两个节已经详细介绍了,这里不再赘述。下面介绍一下它们的构造方法,FileReader构造方法主要有:

  • FileReader(String fileName):创建FileReader对象,fileName是文件名。如果文件不存在则抛出FileNotFoundException异常。
  • FileReader(File file):通过File对象创建FileReader对象。如果文件不存在则抛出FileNotFoundException异常。

FileWriter构造方法主要有:

  • FileWriter(String fileName):通过指定fileName文件名创建FileWriter对象。如果fileName文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。
  • FileWriter(String fileName, boolean append):通过指定fileName文件名创建FileWriter对象,append参数如果为 true,则将字符写入文件末尾处,而不是写入文件开始处。如果fileName文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。
  • FileWriter(File file):通过File对象创建FileWriter对象。如果file文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。
  • FileWriter(File file, boolean append):通过File对象创建FileWriter对象,append参数如果为 true,则将字符写入文件末尾处,而不是写入文件开始处。如果file文件存在,但如果是一个目录或文件无法打开则抛出FileNotFoundException异常。

注意 字符文件流只能复制文本文件,不能是二进制文件。

下面采用文件字符流重新实现22.3.3节文件复制案例,代码如下:

     //FileCopy.java文件
     package com.a51work6;

     import java.io.FileNotFoundException;
     import java.io.FileReader;
     import java.io.FileWriter;
     import java.io.IOException;

     public class FileCopy {

         public static void main(String[] args) {

             try (FileReader in = new FileReader("./TestDir/build.txt");
                     FileWriter out = new FileWriter("./TestDir/subDir/build.txt")) {

                 // 准备一个缓冲区
                 char[] buffer = new char[10];
                 // 首先读取一次
                 int len = in.read(buffer);

                 while (len != -1) {
                     String copyStr = new String(buffer);
                     // 打印复制的字符串
                     System.out.println(copyStr);
                     // 开始写入数据
                     out.write(buffer, 0, len);
                     // 再读取一次
                     len = in.read(buffer);
                 }

             } catch (FileNotFoundException e) {
                 e.printStackTrace();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }

控制台输出结果:

     AI-162.376
     456862.376

上述代码与22.3.3节非常相似,只是将文件输入流改为FileReader,文件输出流改为FileWriter,缓冲区使用的是字符数组。

18.4.4 使用字符缓冲流

BufferedReader和BufferedWriter称为字符缓冲流。BufferedReader特有方法和构造方法有:

  • String readLine():读取一个文本行,如果因为已经到达流末尾,则返回值null。
  • BufferedReader(Reader in):构造方法,通过一个底层输入流in对象创建缓冲流对象,缓冲区大小是默认的,默认值8192。
  • BufferedReader(Reader in, int size):构造方法,通过一个底层输入流in对象创建缓冲流对象,size指定的缓冲区大小,缓冲区大小应该是2的n次幂,这样可提高缓冲区的利用率。

BufferedWriter特有方法和构造方法主要有:

  • void newLine():写入一个换行符。
  • BufferedWriter(Writerout):构造方法,通过一个底层输出流out 对象创建缓冲流对象,缓冲区大小是默认的,默认值8192。
  • BufferedWriter(Writerout, int size):构造方法,通过一个底层输出流out对象创建缓冲流对象,size指定的缓冲区大小,缓冲区大小应该是2的n次幂,这样可提高缓冲区的利用率。

下面将22.4.3节的文件复制的案例改造成缓冲流实现,代码如下:

     //FileCopyWithBuffer.java文件
     package com.a51work6;

     import java.io.BufferedReader;
     import java.io.BufferedWriter;
     import java.io.FileNotFoundException;
     import java.io.FileReader;
     import java.io.FileWriter;
     import java.io.IOException;

     public class FileCopyWithBuffer {

         public static void main(String[] args) {

             try (FileReader fis = new FileReader("./TestDir/JButton.html");
                     BufferedReader bis = new BufferedReader(fis);
                     FileWriter fos = new FileWriter("./TestDir/subDir/JButton.html");
                     BufferedWriter bos = new BufferedWriter(fos)) {

                 // 首先读取一行文本
                 String line = bis.readLine();                ①

                 while (line != null) {
                     // 开始写入数据
                     bos.write(line);                    ②
                     //写一个换行符
                     bos.newLine();                    ③
                     // 再读取一行文本
                     line = bis.readLine();
                 }
                 System.out.println("复制完成");
             } catch (FileNotFoundException e) {
                 e.printStackTrace();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }

上述代码第①行是通过字节缓冲流readLine方法读取一行文本,当读取是文本为null时说明流已经读完了。代码第②行是写入文本到输出流,由于在输入流的readLine方法会丢掉一个换行符或回车符,为了保持复制结果完全一样,因此需要在写完一个文本后,调用输出流的newLine方法写入一个换行符。

18.4.5 字节流转换字符流

有时需要将字节流转换为字符流,InputStreamReader和OutputStreamWriter是为实现这种转换而设计的。

InputStreamReader构造方法如下:

  • InputStreamReader(InputStream in):将字节流in转换为字符流对象,字符流使用默认字符集。
  • InputStreamReader(InputStream in, String charsetName):将字节流in转换为字符流对象,charsetName指定字符流的字符集,字符集主要有:US-ASCII、ISO-8859-1、UTF-8和UTF-16。如果指定的字符集不支持会抛出UnsupportedEncodingException异常。

OutputStreamWriter构造方法如下:

  • OutputStreamWriter(OutputStream out):将字节流out转换为字符流对象,字符流使用默认字符集。
  • OutputStreamWriter(OutputStream out,String charsetName):将字节流out转换为字符流对象,charsetName指定字符流的字符集,如果指定的字符集不支持会抛出UnsupportedEncodingException异常。

下面将22.4.3节的文件复制的案例改造成缓冲流实现,代码如下:

     //FileCopyWithBuffer.java文件
     package com.a51work6;

     import java.io.BufferedReader;
     import java.io.BufferedWriter;
     import java.io.FileInputStream;
     import java.io.FileNotFoundException;
     import java.io.FileOutputStream;
     import java.io.IOException;
     import java.io.InputStreamReader;
     import java.io.OutputStreamWriter;

     public class FileCopyWithBuffer {

         public static void main(String[] args) {

             try ( // 创建字节文件输入流对象
                     FileInputStream fis = new FileInputStream("./TestDir/JButton.html");    ①
                     // 创建转换流对象
                     InputStreamReader isr = new InputStreamReader(fis);
                     // 创建字符缓冲输入流对象
                     BufferedReader bis = new BufferedReader(isr);

                     // 创建字节文件输出流对象
                     FileOutputStream fos = new FileOutputStream("./TestDir/subDir/JButton.html");
                     // 创建转换流对象
                     OutputStreamWriter osw = new OutputStreamWriter(fos);
                     // 创建字符缓冲输出流对象
                     BufferedWriter bos = new BufferedWriter(osw)) {                         ②

                 // 首先读取一行文本
                 String line = bis.readLine();

                 while (line != null) {
                     // 开始写入数据
                     bos.write(line);
                     // 写一个换行符
                     bos.newLine();
                     // 再读取一行文本
                     line = bis.readLine();
                 }
                 System.out.println("复制完成");
             } catch (FileNotFoundException e) {
                 e.printStackTrace();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }

第十九章 多线程编程

无论PC(个人计算机)还是智能手机现在都支持多任务,都能够编写并发访问程序。多线程编程可以编写并发访问程序。本章介绍多线程编程。

19.1 基础知识

那么线程究竟是什么?在Windows操作系统出现之前,PC上的操作系统都是单任务系统,只有在大型计算机上才具有多任务和分时设计。随着Windows、Linux等操作系统出现,把原本只在大型计算机才具有的优点,带到了PC系统中。

19.1.1 进程

一般可以在同一时间内执行多个程序的操作系统都有进程的概念。一个进程就是一个执行中的程序,而每一个进程都有自己独立的一块内存空间、一组系统资源。在进程的概念中,每一个进程的内部数据和状态都是完全独立的。在Windows操作系统下可以通过Ctrl+Alt+Del组合键查看进程,在UNIX和Linux操作系统下是通过ps命令查看进程的。

19.1.2 线程

线程与进程相似,是一段完成某个特定功能的代码,是程序中单个顺序控制的流程,但与进程不同的是,同类的多个线程是共享一块内存空间和一组系统资源。所以系统在各个线程之间切换时,开销要比进程小的多,正因如此,线程被称为轻量级进程。一个进程中可以包含多个线程。

19.1.3 主线程

Java程序至少会有一个线程,这就是主线程,程序启动后是由JVM创建主线程,程序结束时由JVM停止主线程。主线程它负责管理子线程,即子线程的启动、挂起、停止等等操作。图23-2所示是进程、主线程和子线程的关系,其中主线程负责管理子线程,即子线程的启动、挂起、停止等操作。

19.2 创建子线程

Java中创建一个子线程涉及到:java.lang.Thread类和java.lang.Runnable接口。Thread是线程类,创建一个Thread对象就会产生一个新的线程。而线程执行的程序代码是在实现Runnable接口对象的run()方法中编写的,实现Runnable接口对象是线程执行对象。

线程执行对象实现Runnable接口的run()方法,run()方法是线程执行的入口,该线程要执行程序代码都在此编写的,run()方法称为线程体。

提示 主线程中执行入口是main(String[] args)方法,这里可以控制程序的流程,管理其他的子线程等。子线程执行入口是线程执行对象(实现Runnable接口对象)的run()方法,在这个方法可以编写子线程相关处理代码。

19.2.1 实现Runnable接口

创建线程Thread对象时,可以将线程执行对象传递给它,这需要是使用Thread类如下两个构造方法:

  • Thread(Runnable target, String name):target是线程执行对象,实现Runnable接口。name为线程指定一个名字。
  • Thread(Runnable target):target是线程执行对象,实现Runnable接口。线程名字是由JVM分配的。

下面看一个具体示例,实现Runnable接口的线程执行对象Runner代码如下:

//Runner.java文件
package com.a51work6;

//线程执行对象
public class Runner implements Runnable {                              ①

    // 编写执行线程代码
    @Override
    public void run() {                                                ②
        for (int i = 0; i < 10; i++) {
            // 打印次数和线程的名字
            System.out.printf("第 %d次执行 - %s\n", i,
                            Thread.currentThread().getName());         ③

            try {
                // 随机生成休眠时间
                long sleepTime = (long) (1000 * Math.random());
                // 线程休眠
                Thread.sleep(sleepTime);                               ④
            } catch (InterruptedException e) {
            }
        }
        // 线程执行结束
        System.out.println("执行完成! " + Thread.currentThread().getName());
    }
}

19.2.2 继承Thread线程类

事实上Thread类也实现了Runnable接口,所以Thread类也可以作为线程执行对象,这需要继承Thread类,覆盖run()方法。

采用继承Thread类重新实现23.2.1节示例,自定义线程类MyThread代码如下:

//MyThread.java文件
package com.a51work6;

//线程执行对象
public class MyThread extends Thread {

    public MyThread() {                          ①
        super();                                 ②
    }

    public MyThread(String name) {               ③
        super(name);                             ④
    }

    // 编写执行线程代码
    @Override
    public void run() {                          ⑤
        for (int i = 0; i < 10; i++) {
            // 打印次数和线程的名字
            System.out.printf("第 %d次执行 - %s\n", i, getName());

            try {
                // 随机生成休眠时间
                long sleepTime = (long) (1000 * Math.random());
                // 线程休眠
                sleep(sleepTime);
            } catch (InterruptedException e) {
            }
        }
        // 线程执行结束
        System.out.println("执行完成! " + getName());
    }
}

19.2.3 使用匿名内部类和Lambda表达式实现线程体

如果线程体使用的地方不是很多,可以不用单独定义一个类。可以使用匿名内部类或Lambda表达式直接实现Runnable接口。Runnable中只有一个方法是函数式接口,可以使用Lambda表达式。

重新实现19.2.1节示例,代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 创建线程t1,参数是实现Runnable接口的匿名内部类
        Thread t1 = new Thread(new Runnable() {                ①
            // 编写执行线程代码
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    // 打印次数和线程的名字
                    System.out.printf("第 %d次执行 - %s\n", i, Thread.currentThread().getName());
                    try {
                        // 随机生成休眠时间
                        long sleepTime = (long) (1000 * Math.random());
                        // 线程休眠
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException e) {
                    }
                }
                // 线程执行结束
                System.out.println("执行完成! " + Thread.currentThread().getName());
            }

        });
        // 开始线程t1
        t1.start();

        // 创建线程t2,参数是实现Runnable接口的Lambda表达式
        Thread t2 = new Thread(() -> {                          ②
            for (int i = 0; i < 10; i++) {
                // 打印次数和线程的名字
                System.out.printf("第 %d次执行 - %s\n", i, Thread.currentThread().getName());
                try {
                    // 随机生成休眠时间
                    long sleepTime = (long) (1000 * Math.random());
                    // 线程休眠
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                }
            }
            // 线程执行结束
            System.out.println("执行完成! " + Thread.currentThread().getName());
        }, "MyThread");
        // 开始线程t2
        t2.start();
    }
}

19.3 线程的状态

在线程的生命周期中,线程会有几种状态,如图23-5所示,线程有5种状态。下面分别介绍一下。

  1. 新建状态

    新建状态(New)是通过new等方式创建线程对象,它仅仅是一个空的线程对象。

  2. 就绪状态

    当主线程调用新建线程的start()方法后,它就进入就绪状态(Runnable)。此时的线程尚未真正开始执行run()方法,它必须等待CPU的调度。

  3. 运行状态

    CPU的调度就绪状态的线程,线程进入运行状态(Running),处于运行状态的线程独占CPU,执行run()方法。

  4. 阻塞状态

    因为某种原因运行状态的线程会进入不可运行状态,即阻塞状态(Blocked),处于阻塞状态的线程JVM系统不能执行该线程,即使CPU空闲,也不能执行该线程。如下几个原因会导致线程进入阻塞状态:

    • 当前线程调用sleep()方法,进入休眠状态。
    • 被其他线程调用了join()方法,等待其他线程结束。
    • 发出I/O请求,等待I/O操作完成期间。
    • 当前线程调用wait()方法。

    处于阻塞状态可以重新回到就绪状态,如:休眠结束、其他线程加入、I/O操作完成和调用notify或notifyAll唤醒wait线程。

  5. 死亡状态

    线程退出run()方法后,就会进入死亡状态(Dead),线程进入死亡状态有可以是正常实现完成run()方法进入,也可能是由于发生异常而进入的。

19.4 线程管理

线程管理是比较头痛的事情,这是学习线程的难点。下面分别介绍一下。

19.4.1 线程优先级

线程的调度程序根据线程决定每次线程应当何时运行,Java提供了10种优先级,分别用1~10整数表示,最高优先级是10用常量MAX_PRIORITY表示;最低优先级是1用常量MIN_PRIORITY;默认优先级是5用常量NORM_PRIORITY表示。

Thread类提供了setPriority(int newPriority)方法可以设置线程优先级,通过getPriority()方法获得线程优先级。

设置线程优先级示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 创建线程t1,参数是一个线程执行对象Runner
        Thread t1 = new Thread(new Runner());
        t1.setPriority(Thread.MAX_PRIORITY);                    ①
        // 开始线程t1
        t1.start();

        // 创建线程t2,参数是一个线程执行对象Runner
        Thread t2 = new Thread(new Runner(), "MyThread");
        t2.setPriority(Thread.MIN_PRIORITY);                    ②
        // 开始线程t2
        t2.start();
    }
}

在代码第①行设置线程t1优先级最高,代码第②行设置线程t2优先级最低。

提示 多次运行上面的示例会发现,t1线程经常先运行,但是偶尔t2线程也会先运行。这些现象说明了:影响线程获得CPU时间的因素,除了受到的线程优先级外,还与操作系统有关。

19.4.2 等待线程结束

在介绍现在状态时提到过join()方法,当前线程调用t1线程的join()方法,则阻塞当前线程,等待t1线程结束,如果t1线程结束或等待超时,则当前线程回到就绪状态。

Thread类提供了多个版本的join(),它们定义如下:

  • void join():等待该线程结束。
  • void join(long millis):等待该线程结束的时间最长为millis毫秒。如果超时为0意味着要一直等下去。
  • void join(long millis, int nanos):等待该线程结束的时间最长为millis毫秒加nanos纳秒。

使用join()方法示例代码如下:

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    //共享变量
    static int value = 0;                                 ①

    public static void main(String[] args) throws InterruptedException {

        System.out.println("主线程 开始...");

        // 创建线程t1,参数是一个线程执行对象Runner
        Thread t1 = new Thread(() -> {                    ②
            System.out.println("ThreadA 开始...");
            for (int i = 0; i < 2; i++) {
                System.out.println("ThreadA 执行...");
                value++;                                  ③
            }
            System.out.println("ThreadA 结束...");

        }, "ThreadA");
        // 开始线程t1
        t1.start();
        // 主线程被阻塞,等待t1线程结束
        t1.join();                                        ④
        System.out.println("value = " + value);           ⑤
        System.out.println("主线程 结束...");
    }
}

运行结果如下:

主线程 开始...
ThreadA 开始...
ThreadA 执行...
ThreadA 执行...
ThreadA 结束...
value = 2
主线程 结束...

上述代码第①行是声明了一个共享变量value,这个变量在子线程中修改,然后主线程访问它。代码第②行是采用Lambda表达式创建线程,指定线程名为ThreadA。代码第③行是在子线程ThreadA中修改共享变量value。

代码第④行是在当前线程(主线程)中调用t1的join()方法,因此会导致主线程阻塞,等待t1线程结束,从运行结果可以看出主线程被阻塞了。代码第⑤行是打印共享变量value,从运行结果可见value = 2。

如果尝试将t1.join()语句注释掉,输出结果如下:

主线程 开始...
value = 0
主线程 结束...
ThreadA 开始...
ThreadA 执行...
ThreadA 执行...
ThreadA 结束...

提示 使用join()方法的场景是,一个线程依赖于另外一个线程的运行结果,所以调用另一个线程的join()方法等它运行完成。

19.4.3 线程让步

线程类Thread还提供一个静态方法yield(),调用yield()方法能够使当前线程给其他线程让步。它类似于sleep()方法,能够使运行状态的线程放弃CPU使用权,暂停片刻,然后重新回到就绪状态。与sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级高低都有机会运行。而yield()方法只给相同优先级或更高优先级线程机会。

示例代码如下:

//Runner.java文件
package com.a51work6;

//线程执行对象
public class Runner implements Runnable {

    // 编写执行线程代码
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            // 打印次数和线程的名字
            System.out.printf("第 %d次执行 - %s\n", i,
                        Thread.currentThread().getName());
            Thread.yield();                            ①
        }
        // 线程执行结束
        System.out.println("执行完成! " + Thread.currentThread().getName());
    }
}

代码第①行Thread.yield()能够使当前线程让步。

提示 yield()方法只能给相同优先级或更高优先级的线程让步,yield()方法在实际开发中很少使用,大多都使用sleep()方法,sleep()方法可以控制时间,而yield()方法不能。

19.4.4 线程停止

线程体中的run()方法结束,线程进入死亡状态,线程就停止了。但是有些业务比较复杂,例如想开发一个下载程序,每隔一段执行一次下载任务,下载任务一般会在由子线程执行的,休眠一段时间再执行。这个下载子线程中会有一个死循环,但是为了能够停止子线程,设置一个结束变量。

示例下面如下:

//HelloWorld.java文件
package com.a51work6;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class HelloWorld {

    private static String command = "";                        ①

    public static void main(String[] args) {

        // 创建线程t1,参数是一个线程执行对象Runner
        Thread t1 = new Thread(() -> {

            // 一直循环,直到满足条件在停止线程
            while (!command.equalsIgnoreCase("exit")) {        ②
                // 线程开始工作
                // TODO
                System.out.println("下载中...");
                try {
                    // 线程休眠
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                }
            }
            // 线程执行结束
            System.out.println("执行完成!");
        });
        // 开始线程t1
        t1.start();

        try (InputStreamReader ir = new InputStreamReader(System.in);     ③
                BufferedReader in = new BufferedReader(ir)) {
            // 从键盘接收了一个字符串的输入
            command = in.readLine();                                      ④
        } catch (IOException e) {
        }

    }
}

19.5 线程安全

在多线程环境下,访问相同的资源,有可以会引发线程不安全问题。本节讨论引发这些问题的根源和解决方法。

19.5.1 临界资源问题

多一个线程同时运行,有时线程之间需要共享数据,一个线程需要其他线程的数据,否则就不能保证程序运行结果的正确性。

19.5.2 多线程同步

为了防止多线程对临界资源的访问有时会导致数据的不一致性,Java提供了“互斥”机制,可以为这些资源对象加上一把“互斥锁”,在任一时刻只能由一个线程访问,即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象,这就多线程同步。线程同步保证线程安全的重要手段,但是线程同步客观上会导致性能下降。

可以通过两种方式实现线程同步,两种方式都涉及到使用synchronized关键字,一种是synchronized方法,使用synchronized关键字修饰方法,对方法进行同步;另一种是synchronized语句,使用synchronized关键字放在对象前面限制一段代码的执行。

第二十章 网络编程

20.1 网络基础

20.1.1 网络结构

客户端服务器(Client Server,缩写C/S)结构网络,是一种主从结构网络。服务器一般处于等待状态,如果有客户端请求,服务器响应请求建立连接提供服务。服务器是被动的,有点像在餐厅吃饭时候的服务员。而客户端是主动的,像在餐厅吃饭的顾客。

20.1.2 TCP/IP协议

网络通信会用到协议,其中TCP/IP协议是非常重要的。TCP/IP协议是由IP和TCP两个协议构成的,IP(Internet Protocol)协议是一种低级的路由协议,它将数据拆分成许多小的数据包中,并通过网络将它们发送到某一特定地址,但无法保证都所有包都抵达目的地,也不能保证包的顺序。

由于IP协议传输数据的不安全性,网络通信时还需要TCP协议,传输控制协议(Transmission Control Protocol,TCP)是一种高层次的协议,面向连接的可靠数据传输协议,如果有些数据包没有收到会重发,并对数据包内容准确性检查并保证数据包顺序,所以该协议保证数据包能够安全地按照发送时顺序送达目的地。

20.1.3 IP地址

为实现网络中不同计算机之间的通信,每台计算机都必须有一个与众不同的标识,这就是IP地址,TCP/IP使用IP地址来标识源地址和目的地址。最初所有的IP地址都是32位数字构成,由4个8位的二进制数组成,每8位之间用圆点隔开,如:192.168.1.1,这种类型的地址通过IPv4指定。而现在有一种新的地址模式称为IPv6,IPv6使用128位数字表示一个地址,分为8个16位块。尽管IPv6比IPv4有很多优势,但是由于习惯的问题,很多设备还是采用IPv4。不过Java语言同时指出IPv4和IPv6。

在IPv4地址模式中IP地址分为A、B、C、D和E等5类。

  • A类地址用于大型网络,地址范围:1.0.0.1~126.155.255.254。
  • B类地址用于中型网络,地址范围:128.0.0.1~191.255.255.254。
  • C类地址用于小规模网络,192.0.0.1~223.255.255.254。
  • D类地址用于多目的地信息的传输和作为备用。
  • E类地址保留仅作实验和开发用。

另外,有时还会用到一个特殊的IP地址127.0.0.1,127.0.0.1称为回送地址,指本机。主要用于网络软件测试以及本地机进程间通信,使用回送地址发送数据,不进行任何网络传输,只在本机进程间通信。

20.1.4 端口

一个IP地址标识这一台计算机,每一台计算机又有很多网络通信程序在运行,提供网络服务或进行通信,这就需要不同的端口进行通信。如果把IP地址比作电话号码,那么端口就是分机号码,进行网络通信时不仅要指定IP地址,还要指定端口号。

TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。小于1024的端口号保留给预定义的服务,如HTTP是80,FTP是21,Telnet是23,Email是25等,除非要和那些服务进行通信,否则不应该使用小于1024的端口。

20.2 TCP Socket低层次网络编程

20.2.1 TCP Socket通信概述

Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换。这个双向链路的一端称为一个Socket。Socket通常用来实现客户端和服务端的连接。Socket是TCP/IP协议的一个十分流行的编程接口,一个Socket由一个IP地址和一个端口号唯一确定,一旦建立连接Socket还会包含本机和远程主机的IP地址和远端口号

20.2.2 TCP Socket通信过程

服务器端监听某个端口是否有连接请求,服务器端程序处于阻塞状态,直到客户端向服务器端发出连接请求,服务器端接收客户端请求,服务器会响应请求,处理请求,然后将结果应答给客户端,这样就会建立连接。一旦连接建立起来,通过Socket可以获得输入输出流对象。借助于输入输出流对象就可以实现服务器与客户端的通信,最后不要忘记关闭Socket和释放一些资源(包括:关闭输入输出流)。

20.2.3 Socket类

java.net包为TCP Socket编程提供了两个核心类:Socket和ServerSocket,分别用来表示双向连接的客户端和服务器端。

本节先介绍一下Socket类,Socket常用的构造方法有:

  • Socket(InetAddress address, int port) :创建Socket对象,并指定远程主机IP地址和端口号。
  • Socket(InetAddress address, int port, InetAddress localAddr, int localPort):创建Socket对象,并指定远程主机IP地址和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。
  • Socket(String host, int port):创建Socket对象,并指定远程主机名和端口号,IP地址为null,null表示回送地址,即127.0.0.1。
  • Socket(String host, int port, InetAddress localAddr, int localPort):创建Socket对象,并指定远程主机和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。host主机名,IP地址为null,null表示回送地址,即127.0.0.1。

Socket其他的常用方法有:

  • InputStream getInputStream():通过此Socket返回输入流对象。
  • OutputStream getOutputStream():通过此Socket返回输出流对象。
  • int getPort():返回Socket连接到的远程端口。
  • int getLocalPort():返回Socket绑定到的本地端口。
  • InetAddress getInetAddress():返回Socket连接的地址。
  • InetAddress getLocalAddress():返回Socket绑定的本地地址。
  • boolean isClosed():返回Socket是否处于关闭状态。
  • boolean isConnected():返回Socket是否处于连接状态。
  • void close():关闭Socket。

注意 Socket与流类似所占用的资源,不能通过JVM的垃圾收集器回收,需要程序员释放。一种方法是可以在finally代码块调用close()方法关闭Socket,释放流所占用的资源。另一种方法通过自动资源管理技术释放资源,Socket和ServerSocket都实现了AutoCloseable接口。

20.2.4 ServerSocket类

ServerSocket类常用的构造方法有:

  • ServerSocket(int port, int maxQueue):创建绑定到特定端口的服务器Socket。maxQueue设置连接的请求最大队列长度,如果队列满时,则拒绝该连接。默认值是50。
  • ServerSocket(int port):创建绑定到特定端口的服务器Socket。最大队列长度是50。

ServerSocket其他的常用方法有:

  • InputStream getInputStream():通过此Socket返回输入流对象。
  • OutputStream getOutputStream():通过此Socket返回输出流对象。
  • boolean isClosed():返回Socket是否处于关闭状态。
  • Socket accept():侦听并接收到Socket的连接。此方法在建立连接之前一直阻塞。
  • void close():关闭Socket。

ServerSocket类本身不能直接获得I/O流对象,而是通过accept()方法返回Socket对象,通过Socket对象取得I/O流对象,进行网络通信。此外,ServerSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭ServerSocket。

20.2.5 案例:文件上传工具

下面看看案例服务器端UploadServer代码如下:

//UploadServer.java文件
package com.a51work6;
… …
public class UploadServer {

    public static void main(String[] args) {

        System.out.println("服务器端运行...");

        try ( // 创建一个ServerSocket监听8080端口的客户端请求
                ServerSocket server = new ServerSocket(8080);   ①
                // 使用accept()阻塞当前线程,等待客户端请求
                Socket socket = server.accept();                ②
                // 由Socket获得输入流,并创建缓冲输入流
                BufferedInputStream in = new BufferedInputStream(socket.getInputStream());  ③
                // 由文件输出流创建缓冲输出流
                FileOutputStream out = new FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg")) {④

            // 准备一个缓冲区
            byte[] buffer = new byte[1024];
            // 首次从Socket读取数据
            int len = in.read(buffer);
            while (len != -1) {
                // 写入数据到文件
                out.write(buffer, 0, len);
                // 再次从Socket读取数据
                len = in.read(buffer);
            }

            System.out.println("接收完成!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

上述代码第①行创建ServerSocket对象监听本机的8080端口,这是当前线程还没有阻塞,调用代码第②行的server.accept()才会阻塞当前线程,等待客户端请求。

提示 由于当前线程是主线程,所以server.accept()会阻塞主线程,阻塞主线程是不明智的,如果是在一个图形界面的应用程序,阻塞主线程会导致无法进行任何的界面操作,就是常见的“卡”现象,所以最好是把server.accept()语句放到子线程中。

代码第③行是从socket对象中获得输入流对象,代码第④行是文件输出流。下面输入输出代码读者可以参考第22章,这里不再赘述。

再看看案例客户端UploadClient代码如下:

//UploadClient.java文件
package com.a51work6;
… …
public class UploadClient {

    public static void main(String[] args) {

        System.out.println("客户端运行...");

        try ( // 向本机的8080端口发出请求
                Socket socket = new Socket("127.0.0.1", 8080);         ①
                // 由Socket获得输出流,并创建缓冲输出流
                BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());②
                // 创建文件输入流
                FileInputStream fin = new FileInputStream("./TestDir/coco2dxcplus.jpg");
                // 由文件输入流创建缓冲输入流
                BufferedInputStream in = new BufferedInputStream(fin)) {

            // 准备一个缓冲区
            byte[] buffer = new byte[1024];
            // 首次读取文件
            int len = in.read(buffer);
            while (len != -1) {
                // 数据写入Socket
                out.write(buffer, 0, len);
                // 再次读取文件
                len = in.read(buffer);
            }

            System.out.println("上传成功!");

        } catch (ConnectException e) {                                 ③
            System.out.println("服务器未启动!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述代码第①行创建Socket,指定远程主机的IP地址和端口号。代码第②行是从socket对象获得输出流。代码第③行是捕获ConnectException异常,这个异常引起的原因是在代码第①行向服务器发出请求时,服务器拒绝了客户端请求,这有两种可能性:一是服务器没有启动,服务器的8080端口没有打开;二是服务器请求队列已满(默认是50个)。

提示 案例测试时,先运行服务器,再运行客户端。测试Socket程序最好打开两个命令行窗口,通过java指令分别运行服务器程序和客户端程序,如图24-6和24-7所示,需要注意当前运行的路径是Eclipse工程根目录,需要指定类路径,命令的-cp .;./bin就是指定类路径,包括两个当前路径:其中点(.)表示当前路径,./bin表示bin目录,也可以写成.\bin。为什么要指定bin目录呢?是因为编译之后的字节码文件放在此目录中。另外,如果想在Eclipse中查看多个控制台信息,如图24-8所示,在控制台上面的工具栏中,单击“选择控制台”按钮实现切换。

20.2.6 案例:聊天工具

下面看看案例服务器端ChatServer代码如下:

// ChatServer.java文件
package com.a51work6;
… …
public class ChatServer {

    public static void main(String[] args) {

        System.out.println("服务器运行...");

        Thread t = new Thread(() -> {                                                         ①

            try ( // 创建一个ServerSocket监听端口8080客户请求
                    ServerSocket server = new ServerSocket(8080);
                    // 使用accept()阻塞等待客户端请求
                    Socket socket = server.accept();
                    DataInputStream in = new DataInputStream(socket.getInputStream());        ②
                    DataOutputStream out = new DataOutputStream(socket.getOutputStream());    ③
                    BufferedReader keyboardIn
                            = new BufferedReader(new InputStreamReader(System.in))) {         ④

                while (true) {
                    /* 接收数据 */
                    String str = in.readUTF();                                                ⑤
                    // 打印接收的数据
                    System.out.printf("从客户端接收的数据:【%s】\n", str);

                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 发送
                    out.writeUTF(keyboardInputString);                                        ⑥
                    out.flush();
                }
            } catch (Exception e) {
            }
            System.out.println("服务器停止...");
        });

        t.start();
    }
}

上述代码第①行是创建一个子线程,将网络通信放到子线程中处理是一种很好的做法,因为网络通信往往有线程阻塞过程,放到子线程中处理就不会阻塞主线程了。

代码第②行是从socket中获得数据输入流,代码第③行是从socket中获得数据输出流,数据流主要面向基本数据类型,本例中使用它们主要用来输入输出UTF编码的字符串,代码第⑤行readUTF()是数据输入流读取字符串。代码第⑥行writeUTF()是数据输出流写入字符串。代码第④行中的System.in是标准输入流,然后使用标准输入流创建缓冲输入流。

下面看看案例客户端ChatClient代码如下:

//ChatClient.java文件
package com.a51work6;
…
public class ChatClient {

    public static void main(String[] args) {

        System.out.println("客户端运行...");

        Thread t = new Thread(() -> {

            try ( // 向127.0.0.1主机8080端口发出连接请求
                    Socket socket = new Socket("127.0.0.1", 8080);
                    DataInputStream in = new DataInputStream(socket.getInputStream());
                    DataOutputStream out = new DataOutputStream(socket.getOutputStream());
                    BufferedReader keyboardIn
                        = new BufferedReader(new InputStreamReader(System.in))) {

                while (true) {
                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 发送
                    out.writeUTF(keyboardInputString);
                    out.flush();

                    /* 接收数据 */
                    String str = in.readUTF();
                    // 打印接收的数据
                    System.out.printf("从服务器接收的数据:【%s】\n", str);
                }
            } catch (ConnectException e) {
                System.out.println("服务器未启动!");
            } catch (Exception e) {
            }
            System.out.println("客户端停止!");
        });

        t.start();

    }
}

20.3 UDP Socket低层次网络编程

UDP(用户数据报协议)就像日常生活中的邮件投递,是不能保证可靠地寄到目的地。UDP是无连接的,对系统资源的要求较少,UDP可能丢包不保证数据顺序。但是对于网络游戏和在线视频等要求传输快、实时性高、质量可稍差一点的数据传输,UDP还是非常不错的。

UDP Socket网络编程比TCP Socket编程简单多,UDP是无连接协议,不需要像TCP一样监听端口,建立连接,然后才能进行通信。

20.3.1 DatagramSocket类

java.net包中提供了两个类:DatagramSocket和DatagramPacket用来支持UDP通信。这一节先介绍一下DatagramSocket类,DatagramSocket用于在程序之间建立传送数据报的通信连接。

先来看一下DatagramSocket常用的构造方法:

  • DatagramSocket():创建数据报DatagramSocket对象,并将其绑定到本地主机上任何可用的端口。
  • DatagramSocket(int port):创建数据报DatagramSocket对象,并将其绑定到本地主机上的指定端口。
  • DatagramSocket(int port, InetAddress laddr):创建数据报DatagramSocket对象,并将其绑定到指定的本地地址。

DatagramSocket其他的常用方法有:

  • void send(DatagramPacket p):从发送数据报包。
  • void receive(DatagramPacket p):接收数据报包。
  • int getPort():返回DatagramSocket连接到的远程端口。
  • int getLocalPort():返回DatagramSocket绑定到的本地端口。
  • InetAddress getInetAddress():返回DatagramSocket连接的地址。
  • InetAddress getLocalAddress():返回DatagramSocket绑定的本地地址。
  • boolean isClosed():返回DatagramSocket是否处于关闭状态。
  • boolean isConnected():返回DatagramSocket是否处于连接状态。
  • void close():关闭DatagramSocket。

DatagramSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭DatagramSocket。

20.3.2 DatagramPacket类

DatagramPacket用来表示数据报包,是数据传输的载体。DatagramPacket实现无连接数据包投递服务,每投递数据包仅根据该包中信息从一台机器路由到另一台机器。从一台机器发送到另一台机器的多个包可能选择不同的路由,也可能按不同的顺序到达,不保证包都能到达目的。

下面看一下DatagramPacket的构造方法:

  • DatagramPacket(byte[] buf, int length):构造数据报包,buf包数据,length是接收包数据的长度。
  • DatagramPacket(byte[] buf, int length, InetAddress address, int port):构造数据报包,包发送到指定主机上的指定端口号。
  • DatagramPacket(byte[] buf, int offset, int length):构造数据报包,offset是buf字节数组的偏移量。
  • DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):构造数据报包,包发送到指定主机上的指定端口号。

DatagramPacket常用的方法:

  • InetAddress getAddress():返回发往或接收该数据报包相关的主机的IP地址。
  • byte[] getData():返回数据报包中的数据。
  • int getLength():返回发送或接收到的数据(byte[])的长度。
  • int getOffset():返回发送或接收到的数据(byte[])的偏移量。
  • int getPort():返回发往或接收该数据报包相关的主机的端口号。

20.3.3 案例:文件上传工具

使用UDP Socket将20.1.5节文件上传工具重新实现一下。

下面看看案例服务器端UploadServer代码如下:

//UploadServer.java文件
package com.a51work6;
…
public class UploadServer {
    public static void main(String args[]) {

        System.out.println("服务器端运行...");

        // 创建一个子线程
        Thread t = new Thread(() -> {                                        ①

            try ( // 创建DatagramSocket对象,指定端口8080
                    DatagramSocket socket = new DatagramSocket(8080);        ②
                    FileOutputStream fout = new FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg"); 
                    BufferedOutputStream out = new BufferedOutputStream(fout)) {

                // 准备一个缓冲区
                byte[] buffer = new byte[1024];

                //循环接收数据报包
                while (true) {

                    // 创建数据报包对象,用来接收数据
                    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                    // 接收数据报包
                    socket.receive(packet);
                    // 接收数据长度
                    int len = packet.getLength();

                    if (len == 3) {                                          ③
                        // 获得结束标志
                        String flag = new String(buffer, 0, 3);
                        // 判断结束标志,如果是bye结束接收
                        if (flag.equals("bye")) {                            ④
                            break;
                        }
                    }
                    // 写入数据到文件输出流
                    out.write(buffer, 0, len);
                }
                System.out.println("接收完成!");
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 启动线程
        t.start();
    }
}

上述代码第①行是创建一个子线程,由于客户端上传的数据分为很多数据包,因此需要一个循环接收数据包,另外,调用后receive()方法会导致线程阻塞,因此需要将接收数据的处理放到一个子线程中。

代码第②行是创建DatagramSocket对象,并指定端口8080,作为服务器一般应该明确指定绑定的端口。

与TCP Socket不同UDP Socket无法知道哪些数据包已经是最后一个了,因此需要发送方发出一个特殊的数据包,包中包含了一些特殊标志。代码第③行~第④行是取出并判断这个标志。

再看看案例客户端UploadClient代码如下:

//UploadClient.java文件
package com.a51work6;
…
public class UploadClient {

    public static void main(String[] args) {

        System.out.println("客户端运行...");

        try (   // 创建DatagramSocket对象,由系统分配可以使用的端口
                DatagramSocket socket = new DatagramSocket();                    ①
                FileInputStream fin = new FileInputStream("./TestDir/coco2dxcplus.jpg");
                BufferedInputStream in = new BufferedInputStream(fin)) {

            // 创建远程主机IP地址对象
            InetAddress address = InetAddress.getByName("localhost");

            // 准备一个缓冲区
            byte[] buffer = new byte[1024];
            // 首次从文件流中读取数据
            int len = in.read(buffer);

            while (len != -1) {
                // 创建数据报包对象
                DatagramPacket packet = new DatagramPacket(buffer, len, address, 8080);
                // 发送数据报包
                socket.send(packet);
                // 再次从文件流中读取数据
                len = in.read(buffer);
            }

            // 创建数据报对象
            DatagramPacket packet = new DatagramPacket("bye".getBytes(), 3, address, 8080);
            // 发送结束标志
            socket.send(packet);                                                 ②
            System.out.println("上传完成!");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述是上传文件客户端,发送数据不会堵塞线程,因此没有使用子线程。代码第①行是创建DatagramSocket对象,由系统分配可以使用的端口,作为客户端DatagramSocket对象经常自己不指定了,而是有系统分配。

代码第②行是发送结束标志,这个结束标志是字符串bye,服务器端接收到这个字符串则结束接收数据包。

20.3.4 案例:聊天工具

使用UDP Socket将24.1.6节文件聊天工具重新实现一下。

下面看看案例服务器端ChatServer代码如下:

// ChatServer.java文件
package com.a51work6;
…
public class ChatServer {

    public static void main(String args[]) {

        System.out.println("服务器运行...");
        // 创建一个子线程
        Thread t = new Thread(() -> {                                       ①
            try ( // 创建DatagramSocket对象,指定端口8080
                    DatagramSocket socket = new DatagramSocket(8080);
                    BufferedReader keyboardIn
                        = new BufferedReader(new InputStreamReader(System.in))) {

                while (true) {
                    /* 接收数据报 */
                    // 准备一个缓冲区
                    byte[] buffer = new byte[128];
                    DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
                    socket.receive(packet);
                    // 接收数据长度
                    int len = packet.getLength();

                    String str = new String(buffer, 0, len);
                    // 打印接收的数据
                    System.out.printf("从客户端接收的数据:【%s】\n", str);

                    /* 发送数据 */
                    // 从客户端传来的数据包中得到客户端地址
                    InetAddress address = packet.getAddress();              ②
                    // 从客户端传来的数据包中得到客户端端口号
                    int port = packet.getPort();                            ③

                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 读取键盘输入的字节数组
                    byte[] b = keyboardInputString.getBytes();
                    // 创建DatagramPacket对象,用于向客户端发送数据
                    packet = new DatagramPacket(b, b.length, address, port);
                    // 向客户端发送数据
                    socket.send(packet);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 启动线程
        t.start();
    }
}

上述代码第①行是创建一个子线程,因为socket.receive(packet)方法会阻塞主线程了。服务器给客户端发数据包,也需要知道它的IP地址和端口号,代码第②行根据接收的数据包获得客户端的地址,代码第③行类似根据接收的数据包获得客户端的端口号。

下面看看案例客户端ChatClient代码如下:

//ChatClient.java文件
package com.a51work6;
…
public class ChatClient {

    public static void main(String[] args) {

        System.out.println("客户端运行...");
        // 创建一个子线程
        Thread t = new Thread(() -> {

            try ( // 创建DatagramSocket对象,由系统分配可以使用的端口
                    DatagramSocket socket = new DatagramSocket();
                    BufferedReader keyboardIn
                         = new BufferedReader(new InputStreamReader(System.in))) {

                while (true) {

                    /* 发送数据 */
                    // 准备一个缓冲区
                    byte[] buffer = new byte[128];
                    // 服务器IP地址
                    InetAddress address = InetAddress.getByName("localhost");
                    // 服务器端口号
                    int port = 8080;
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 退出循环,结束线程
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 读取键盘输入的字节数组
                    byte[] b = keyboardInputString.getBytes();
                    // 创建DatagramPacket对象
                    DatagramPacket packet = new DatagramPacket(b, b.length, address, port);
                    // 发送
                    socket.send(packet);

                    /* 接收数据报 */
                    packet = new DatagramPacket(buffer, buffer.length);
                    socket.receive(packet);

                    // 接收数据长度
                    int len = packet.getLength();
                    String str = new String(buffer, 0, len);
                    // 打印接收的数据
                    System.out.printf("从服务器接收的数据:【%s】\n", str);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        // 启动线程
        t.start();
    }
}

客户端ChatClient代码与服务器端ChatServer代码类似,这里不再赘述。这里需要注意的是ChatClient可以通过键盘输入bye,退出循环结束线程。

20.4 数据交换格式

数据交换格式就像两个人在聊天一样,采用彼此都能听得懂的语言,你来我往,其中的语言就相当于通信中的数据交换格式。有时候,为了防止聊天被人偷听,可以采用暗语。同理,计算机程序之间也可以通过数据加密技术防止“偷听”。

数据交换格式主要分为纯文本格式、XML格式和JSON格式,其中纯文本格式是一种简单的、无格式的数据交换方式。

20.4.1 JSON文档结构

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。所谓轻量级,是与XML文档结构相比而言的,描述项目的字符少,所以描述相同数据所需的字符个数要少,那么传输速度就会提高,而流量却会减少。

20.4.2 使用第三方JSON库

由于目前Java官方没有提供JSON编码和解码所需要的类库,所以需要使用第三方JSON库,笔者推荐JSON-java库,JSON-java库提供源代码,最重要的是不依赖于其他第三方库,需要再起找其他的库了。读者可以在https://github.com/stleary/JSON-java 下载源代码。API在线文档http://stleary.github.io/JSON-java/index.html 。

20.4.4 案例:聊天工具

为了进一步熟悉JSON数据交换格式,将24.2.6节的聊天工具修改为使用JSON进行数据交换。

客户端与服务器之间采用JSON数据交换格式,JSON格式内部结构是自定义的,代码如下:

{"message":"Hello","userid":"javaee","username":"关东升"}

下面看看案例服务器端ChatServer代码如下:

//ChatServer.java文件
package com.a51work6;
…
import org.json.JSONObject;

public class ChatServer {

    public static void main(String[] args) {

        System.out.println("服务器运行...");

        Thread t = new Thread(() -> {

            try ( // 创建一个ServerSocket监听端口8080客户请求
                    ServerSocket server = new ServerSocket(8080);
                    // 使用accept()阻塞等待客户端请求
                    Socket socket = server.accept();
                    DataInputStream in = new DataInputStream(socket.getInputStream());
                    DataOutputStream out = new DataOutputStream(socket.getOutputStream());
                    BufferedReader keyboardIn
                            = new BufferedReader(new InputStreamReader(System.in))) {

                while (true) {
                    /* 接收数据 */
                    String str = in.readUTF();
                    // JSON解码
                    JSONObject jsonObject = new JSONObject(str);                  ①
                    // 打印接收的数据
                    System.out.printf("从客户端接收的数据:%s\n", jsonObject);    ②

                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    // 编码
                    jsonObject = new JSONObject();                                ③
                    jsonObject.put("message", keyboardInputString);               ④
                    jsonObject.put("userid", "acid");                             ⑤
                    jsonObject.put("username", "赵1");                            ⑥
                    // 发送
                    out.writeUTF(jsonObject.toString());                          ⑦
                    out.flush();
                }
            } catch (Exception e) {
            }
            System.out.println("服务器停止...");
        });

        t.start();
    }
}

上述代码第①行是对,从服务器返回的字符串进行解码,并返回JSON对象,注意要解码的字符串应该是有效的JSON字符串。代码第②行是打印JSON对象。

代码第③行是创建JSON对象,代码第④行~第⑥行是添加JSON对象。代码第⑦行jsonObject.toString()语句是将JSON对象转换为JSON字符串。

下面看看案例客户端ChatClient代码如下:

//ChatClient.java文件
package com.a51work6;
…
import org.json.JSONObject;

public class ChatClient {

    public static void main(String[] args) {

        System.out.println("客户端运行...");

        Thread t = new Thread(() -> {

            try ( // 向127.0.0.1主机8080端口发出连接请求
                    Socket socket = new Socket("127.0.0.1", 8080);
                    DataInputStream in = new DataInputStream(socket.getInputStream());
                    DataOutputStream out = new DataOutputStream(socket.getOutputStream());
                    BufferedReader keyboardIn
                            = new BufferedReader(new InputStreamReader(System.in))) {

                while (true) {
                    /* 发送数据 */
                    // 读取键盘输入的字符串
                    String keyboardInputString = keyboardIn.readLine();
                    // 结束聊天
                    if (keyboardInputString.equals("bye")) {
                        break;
                    }
                    JSONObject jsonObject = new JSONObject();
                    jsonObject.put("message", keyboardInputString);
                    jsonObject.put("userid", "javaee");
                    jsonObject.put("username", "关东升");

                    // 发送
                    out.writeUTF(jsonObject.toString());
                    out.flush();

                    /* 接收数据 */
                    String str = in.readUTF();
                    jsonObject = new JSONObject(str);
                    // 打印接收的数据
                    System.out.printf("从服务器接收的数据:%s \n", str);
                }
            } catch (ConnectException e) {
                System.out.println("服务器未启动!");
            } catch (Exception e) {
            }
            System.out.println("客户端停止!");
        });

        t.start();

    }
}

客户端ChatClient代码与服务器端ChatServer代码类似,这里不再赘述。

20.5 访问互联网资源

Java的java.net包中还提供了高层次网络编程类——URL,通过URL类访问互联网资源。使用URL进行网络编程,不需要对协议本身有太多的了解,相对而言是比较简单的。

20.5.1 URL概念

互联网资源是通过URL指定的,URL是Uniform Resource Locator简称,翻译过来是“一致资源定位器”,但人们都习惯URL简称。

URL组成格式如下:

协议名://资源名

“协议名”指明获取资源所使用的传输协议,如http、ftp、gopher和file等,“资源名”则应该是资源的完整地址,包括主机名、端口号、文件名或文件内部的一个引用。例如:

http://www.sina.com/
http://home.sohu.com/home/welcome.html
http://www.51work6.com:8800/Gamelan/network.html#BOTTOM

20.5.2 HTTP/HTTPS协议

访问互联网大多都基于HTTP/HTTPS协议。下面介绍一下HTTP/HTTPS协议。

  1. HTTP协议

    HTTP是Hypertext Transfer Protocol的缩写,即超文本传输协议。HTTP是一个属于应用层的面向对象的协议,其简捷、快速的方式适用于分布式超文本信息的传输。它于1990年提出,经过多年的使用与发展,得到不断完善和扩展。HTTP协议支持C/S网络结构,是无连接协议,即每一次请求时建立连接,服务器处理完客户端的请求后,应答给客户端然后断开连接,不会一直占用网络资源。

    HTTP/1.1协议共定义了8种请求方法:OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE和CONNECT。在HTTP访问中,一般使用GET和HEAD方法。

    • GET方法:是向指定的资源发出请求,发送的信息“显式”地跟在URL后面。GET方法应该只用在读取数据,例如静态图片等。GET方法有点像使用明信片给别人写信,“信内容”写在外面,接触到的人都可以看到,因此是不安全的。
    • POST方法:是向指定资源提交数据,请求服务器进行处理,例如提交表单或者上传文件等。数据被包含在请求体中。POST方法像是把“信内容”装入信封中,接触到的人都看不到,因此是安全的。
  2. HTTPS协议

    HTTPS是Hypertext Transfer Protocol Secure,即超文本传输安全协议,是超文本传输协议和SSL的组合,用以提供加密通信及对网络服务器身份的鉴定。

    简单地说,HTTPS是HTTP的升级版,HTTPS与HTTP的区别是:HTTPS使用https://代替http://,HTTPS使用端口443,而HTTP使用端口80来与TCP/IP进行通信。SSL使用40位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。HTTPS和SSL支持使用X.509数字认证,如果需要的话,用户可以确认发送者是谁。

20.5.3 使用URL类

Java 的java.net.URL类用于请求互联网上的资源,采用HTTP/HTTPS协议,请求方法是GET方法,一般是请求静态的、少量的服务器端数据。

URL类常用构造方法:

  • URL(String spec):根据字符串表示形式创建URL对象。
  • URL(String protocol, String host, String file):根据指定的协议名、主机名和文件名称创建URL对象。
  • URL(String protocol, String host, int port, String file):根据指定的协议名、主机名、端口号和文件名称创建URL对象。

URL类常用方法:

  • InputStream openStream():打开到此URL的连接,并返回一个输入流。
  • URLConnection openConnection():打开到此URL的新连接,返回一个URLConnection对象。

下面通过一个示例介绍一下如何使用java.net.URL类,示例代码如下:

//HelloWorld.java文件
package com.a51work6;
...
public class HelloWorld {

    public static void main(String[] args) {
        // Web网址
        String url = "http://www.sina.com.cn/";

        URL reqURL;
        try {
            reqURL = new URL(url);                                 ①
        } catch (MalformedURLException e1) {
            return;
        }

        try ( // 打开网络通信输入流
                InputStream is = reqURL.openStream();              ②
                InputStreamReader isr = new InputStreamReader(is, "utf-8");
                BufferedReader br = new BufferedReader(isr)) {

            StringBuilder sb = new StringBuilder();
            String line = br.readLine();
            while (line != null) {
                sb.append(line);
                sb.append('\n');
                line = br.readLine();
            }
            // 日志输出
            System.out.println(sb);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上述代码第①行创建URL对象,参数是一个HTTP网址。代码第②行通过URL对象的openStream()方法打开输入流。

20.5.4 使用HttpURLConnection发送GET请求

由于URL类只能发送HTTP/HTTPS的GET方法请求,如果要想发送其他的情况或者对网络请求有更深入的控制时,可以使用HttpURLConnection类型。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HelloWorld {

    // Web服务网址
    static String urlString = "http://www.51work6.com/service/mynotes/WebService.php?"
            + "email=<换成你在51work6.com注册时填写的邮箱>&type=JSON&action=query"; ①

    public static void main(String[] args) {

        BufferedReader br = null;
        HttpURLConnection conn = null;

        try {
            URL reqURL = new URL(urlString);
            conn = (HttpURLConnection) reqURL.openConnection();            ②
            conn.setRequestMethod("GET");                                  ③

            // 打开网络通信输入流
            InputStream is = conn.getInputStream();                        ④
            // 通过is创建InputStreamReader对象
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            // 通过isr创建BufferedReader对象
            br = new BufferedReader(isr);

            StringBuilder sb = new StringBuilder();
            String line = br.readLine();
            while (line != null) {
                sb.append(line);
                line = br.readLine();
            }
            // 日志输出
            System.out.println(sb);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();                                         ⑤
            }
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上述代码第①行是一个Web服务网址字符串。

提示 发送GET请求时发送给服务器的参数是放在URL的“?”之后,参数采用键值对形式,例如:第①行的URL中type=JSON是一个参数,type是参数名,JSON是参数名,服务器端会根据参数名获得参数值。多个参数之间用“&”分隔,例如type=JSON&action=query就是两个参数。

代码第②行是用reqURL.openConnection()方法打开一个连接,返回URLConnection对象,由于本次连接是HTTP连接,所以返回的是HttpURLConnection对象。URLConnection是抽象类,HttpURLConnection是URLConnection的子类。

代码第③行conn.setRequestMethod("GET")是设置请求方法为GET方法。代码第④行是通过conn.getInputStream()打开输入流,上一节实例使用的URL的openStream()方法获得输入流。代码第⑤行conn.disconnect()是断开连接,这可以释放资源。

从服务器端返回的数据是JSON字符串,格式化后内容如下:

{
    "ResultCode": 0,
    "Record": [
        {
            "ID": 5238,
            "CDate": "2017-05-18",
            "Content": "欢迎来到智捷课堂。"
        },
        {
            "ID": 5239,
            "CDate": "2018-10-18",
            "Content": "Welcome to zhijieketang."
        }
    ]
}

提示 上述示例中URL所指向的Web服务是由作者所在的智捷课堂提供的,读者要想使用这个Web服务需要在www.51work6.com 进行注册,注册时需要提供自己有效的邮箱,这个邮箱用来激活用户。在网络请求时需要提交email参数,这个参数是注册时填写的邮箱。

20.5.5 使用HttpURLConnection发送POST请求

HttpURLConnection也可以发送HTTP/HTTPS的POST请求,下面介绍如何使用HttpURLConnection发送POST请求。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class HelloWorld {

    // Web服务网址
    static String urlString = "http://www.51work6.com/service/mynotes/WebService.php";    ①

    public static void main(String[] args) {

        BufferedReader br = null;
        HttpURLConnection conn = null;
        try {
            URL reqURL = new URL(urlString);
            conn = (HttpURLConnection) reqURL.openConnection();            ②
            conn.setRequestMethod("POST");                                 ③
            conn.setDoOutput(true);                                        ④

            String param = String.format("email=%s&type=%s&action=%s",
                             "<换成你在51work6.com注册时填写的邮箱>", "JSON", "query");  ⑤
            // 设置参数
            DataOutputStream dStream = new DataOutputStream(conn.getOutputStream());   ⑥
            dStream.writeBytes(param);            ⑦
            dStream.close();                    ⑧

            // 打开网络通信输入流
            InputStream is = conn.getInputStream();
            // 通过is创建InputStreamReader对象
            InputStreamReader isr = new InputStreamReader(is, "utf-8");
            // 通过isr创建BufferedReader对象
            br = new BufferedReader(isr);

            StringBuilder sb = new StringBuilder();
            String line = br.readLine();
            while (line != null) {
                sb.append(line);
                line = br.readLine();
            }
            // 日志输出
            System.out.println(sb);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上述代码第①行URL后面不带参数,这是因为要发送的是POST请求,POST请求参数是放在请求体中。代码第②行是通过reqURL.openConnection()是建立HTTP连接,代码第③行是设置HTTP请求方法为POST,代码第④行conn.setDoOutput(true)是设置请求过程可以传递参数给服务器。

代码第⑤是设置请求参数格式化字符串"email=%s&type=%s&action=%s",其中%s是占位符。

代码第⑥行~第⑧行是将请求参数发送给服务器,代码第⑥行中conn.getOutputStream()是打开输出流,new DataOutputStream(conn.getOutputStream())是创建基于数据输出流。代码第⑦行dStream.writeBytes(param)是向输出流中写入数据,第⑧行dStream.close()是关闭流,并将数据写入到服务器端。

20.5.6 实例:Downloader

为了进一步熟悉URL类,这一节介绍一个下载程序Downloader。Downloader.java代码如下:

//Downloader.java文件
package com.a51work6;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class Downloader {

    // Web服务网址
    private static String urlString = "https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/"
            + "static/superman/img/logo/bd_logo1_31bdc765.png";

    public static void main(String[] args) {
        download();
    }

    // 下载方法
    private static void download() {

        HttpURLConnection conn = null;

        try {
            // 创建URL对象
            URL reqURL = new URL(urlString);
            // 打开连接
            conn = (HttpURLConnection) reqURL.openConnection();                     ①

            try (// 从连接对象获得输入流
                    InputStream is = conn.getInputStream();                         ②
                    BufferedInputStream bin = new BufferedInputStream(is);          ③
                    // 创建文件输出流
                    OutputStream os = new FileOutputStream("./download.png");       ④
                    BufferedOutputStream bout = new BufferedOutputStream(os);) {    ⑤

                byte[] buffer = new byte[1024];
                int bytesRead = bin.read(buffer);
                while (bytesRead != -1) {
                    bout.write(buffer, 0, bytesRead);
                    bytesRead = bin.read(buffer);
                }
            } catch (IOException e) {
            }
            System.out.println("下载完成。");
        } catch (IOException e) {
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }
}

上述代码第①行打开连接获得HttpURLConnection对象。代码第②行是从连接对象获得输入流。代码第③行创建缓冲流输入流,使用缓冲流可以提高读写效率。

代码第④行是创建文件输出流,代码第⑤行是创建缓冲流输出流。

运行Downloader程序,如果成功会在当前目录获得一张图片。

第 21章 Swing图形用户界面编程

图形用户界面(Graphical User Interface,简称 GUI)编程对于某种语言来说非常重要。Java的应用主要方向是基于Web浏览器的应用,用户界面主要是HTML、CSS和JavaScript等基于Web的技术,这些介绍要到Java EE阶段才能学习到。

而本章介绍的Java图形用户界面技术是基于Java SE的Swing,事实上它们在实际应用中使用不多,因此本章的内容只做了解。

21.1 Java图形用户界面技术

Java图形用户界面技术主要有:AWT、Applet、Swing和JavaFX。

  1. AWT

    AWT(Abstract Window Toolkit)是抽象窗口工具包,AWT是Java 程序提供的建立图形用户界面最基础的工具集。AWT支持图形用户界面编程的功能包括:用户界面组件(控件)、事件处理模型、图形图像处理(形状和颜色)、字体、布局管理器和本地平台的剪贴板来进行剪切和粘贴等。AWT是Applet和Swing技术的基础。

  2. Applet

    Applet称为Java小应用程序,Applet基础是AWT,但它主要嵌入到HTML代码中,由浏览器加载和运行,由于存在安全隐患和运行速度慢等问题,已经很少使用了。

  3. Swing

    Swing是Java主要的图形用户界面技术,Swing提供跨平台的界面风格,用户可以自定义Swing的界面风格。Swing提供了比AWT更完整的组件,引入了许多新的特性。Swing API是围绕着实现AWT各个部分的API构筑的。Swing是由100%纯Java实现的,Swing组件没有本地代码,不依赖操作系统的支持,这是它与AWT组件的最大区别。本章重点介绍Swing技术。

  4. JavaFX

    JavaFX是开发丰富互联网应用程序(Rich Internet Application,缩写RIA)的图形用户界面技术,JavaFX期望能够在桌面应用的开发领域与Adobe公司的AIR、微软公司的Silverlight相竞争。传统的互联网应用程序基于Web的,客户端是浏览器。而丰富互联网应用程序试图打造自己的客户端,替代浏览器。

1> macOS是苹果计算机操作系统,它也是UNIX内核。

21.2 Swing技术基础

AWT是Swing的基础,Swing事件处理和布局管理都是依赖于AWT,AWT内容来自java.awt包,Swing内容来自javax.swing包。AWT和Swing作为图形用户界面技术包括了4个主要的概念:组件(Component)、容器(Container)、事件处理和布局管理器(LayoutManager),下面将围绕这些概念展开。

21.2.1 Swing类层次结构

容器和组件构成了Swing的主要内容,下面分别介绍一下Swing中容器和组件类层次结构。

21.2.2 Swing程序结构

图形用户界面主要是由窗口以及窗口中的组件构成的,编写Swing程序主要就是创建窗口和添加组件过程。Swing中的窗口主要是使用JFrame,很少使用JWindow。JFrame有标题、边框、菜单、大小和窗口管理按钮等窗口要素,而JWindow没有标题栏和窗口管理按钮。

21.3 事件处理模型

图形界面的组件要响应用户操作,就必须添加事件处理机制。Swing采用AWT的事件处理模型进行事件处理。在事件处理的过程中涉及三个要素:

  1. 事件:是用户对界面的操作,在Java中事件被封装称为事件类java.awt.AWTEvent及其子类,例如按钮单击事件类是java.awt.event.ActionEvent。
  2. 事件源:是事件发生的场所,就是各个组件,例如按钮单击事件的事件源是按钮(Button)。
  3. 事件处理者:是事件处理程序,在Java中事件处理者是实现特定接口的事件对象。

21.3.2 采用Lambda表达式处理事件

如果一个事件监听器接口只有一个抽象方法,则可以使用Lambda表达式实现事件处理,这些接口主要有:ActionListener、AdjustmentListener、ItemListener、MouseWheelListener、TextListener和WindowStateListener等。

     //MyFrame.java文件
     package com.a51work6;

     import java.awt.BorderLayout;
     import java.awt.event.ActionEvent;
     import java.awt.event.ActionListener;

     import javax.swing.JButton;
     import javax.swing.JFrame;
     import javax.swing.JLabel;

     public class MyFrame extends JFrame implements ActionListener {        ①

         // 声明标签
         JLabel label;

         public MyFrame(String title) {
             super(title);

             // 创建标签
             label = new JLabel("Label");
             // 添加标签到内容面板
             getContentPane().add(label, BorderLayout.NORTH);

             // 创建Button1
             JButton button1 = new JButton("Button1");
             // 添加Button1到内容面板
             getContentPane().add(button1, BorderLayout.CENTER);

             // 创建Button2
             JButton button2 = new JButton("Button2");
             // 添加Button2到内容面板
             getContentPane().add(button2, BorderLayout.SOUTH);

             // 设置窗口大小
             setSize(350, 120);
             // 设置窗口可见
             setVisible(true);

             // 注册事件监听器,监听Button2单击事件
             button2.addActionListener(this);                ②

             // 注册事件监听器,监听Button1单击事件
             button1.addActionListener((event) -> {          ③
                 label.setText("Hello World!");
             });
         }

         @Override
         public void actionPerformed(ActionEvent event) {    ④
             label.setText("Hello Swing!");
         }
     }

21.3.3 使用适配器

事件监听器都是接口,在Java中接口中定义的抽象方法必须全部是实现,哪怕你对某些方法并不关心,你也要给一对空的大括号表示实现。例如WindowListener是窗口事件(WindowEvent)监听器接口,为了在窗口中接收到窗口事件,需要在窗口中注册WindowListener事件监听器,示例代码如下:

     this.addWindowListener(new WindowListener() {

         @Override
         public void windowActivated(WindowEvent e) {
         }

         @Override
         public void windowClosed(WindowEvent e) {
         }

         @Override
         public void windowClosing(WindowEvent e) {            ①
             // 退出系统
             System.exit(0);
         }

         @Override
         public void windowDeactivated(WindowEvent e) {
         }

         @Override
         public void windowDeiconified(WindowEvent e) {
         }

         @Override
         public void windowIconified(WindowEvent e) {
         }

         @Override
         public void windowOpened(WindowEvent e) {
         }
     });

实现WindowListener接口需要提供它的7个方法的实现,很多情况下只是想在关闭窗口是释放一下资源,只需要实现代码第①行的windowClosing(WindowEvent e),其他的方法并不关心,但是也必须给出空的实现。这样的代码看起来很臃肿,为此Java还提供了一些与监听器相配套的适配器。监听器是接口,命名采用XXXListener,而适配器是类,命名采用XXX Adapter。在使用时通过继承事件所对应的适配器类,覆盖所需要的方法,无关方法不用实现。

采用适配器注册接收窗口事件代码如下:

     this.addWindowListener(new WindowAdapter(){
         @Override
         public void windowClosing(WindowEvent e) {
             // 退出系统
             System.exit(0);
         }
     });

可见代码非常的简洁。事件适配器提供了一种简单的实现监听器的手段,可以缩短程序代码。但是,由于Java的单一继承机制,当需要多种监听器或此类已有父类时,就无法采用事件适配器了。

并非所有的监听器接口都有对应的适配器类,一般定义了多个方法的监听器接口,例如WindowListener有多个方法对应多种不同的窗口事件时,才需要配套的适配器,主要的适配器如下:

  • ComponentAdapter:组件适配器。
  • ContainerAdapter:容器适配器。
  • FocusAdapter:焦点适配器。
  • KeyAdapter:键盘适配器。
  • MouseAdapter:鼠标适配器。
  • MouseMotionAdapter:鼠标运动适配器。
  • WindowAdapter:窗口适配器。

21.4 布局管理

Java为了实现图形用户界面的跨平台,并实现动态布局等效果,Java将容器内的所有组件布局交给布局管理器管理。布局管理器负责,如组件的排列顺序、大小、位置,当窗口移动或调整大小后组件如何变化等。

Java SE提供了7种布局管理器包括:FlowLayout、BorderLayout、GridLayout、BoxLayout、CardLayout、SpringLayout和GridBagLayout,其中最基础的是FlowLayout、BorderLayout和GridLayout布局管理器。下面重点介绍这三种布局。

21.4.1 FlowLayout布局

FlowLayout布局摆放组件的规律是:从上到下、从左到右进行摆放,如果容器足够宽,第一个组件先添加到容器中第一行的最左边,后续的组件依次添加到上一个组件的右边,如果当前行已摆放不下该组件,则摆放到下一行的最左边。

FlowLayout主要的构造方法如下:

  • FlowLayout(int align, int hgap, int vgap):创建一个FlowLayout对象,它具有指定的对齐方式以及指定的水平和垂直间隙,hgap参数是组件之间的水平间隙,vgap参数是组件之间的垂直间隙,单位是像素。
  • FlowLayout(int align):创建一个FlowLayout对象,指定的对齐方式,默认的水平和垂直间隙是5个单位。
  • FlowLayout():创建一个FlowLayout对象,它是居中对齐的,默认的水平和垂直间隙是5个单位。

上述参数align是对齐方式,它是通过FlowLayout的常量指定的,这些常量说明如下:

  • FlowLayout.CENTER:指示每一行组件都应该是居中的。
  • FlowLayout.LEADING:指示每一行组件都应该与容器方向的开始边对齐,例如,对于从左到右的方向,则与左边对齐。
  • FlowLayout.LEFT:指示每一行组件都应该是左对齐的。
  • FlowLayout.RIGHT:指示每一行组件都应该是右对齐的。
  • FlowLayout.TRAILING:指示每行组件都应该与容器方向的结束边对齐,例如,对于从左到右的方向,则与右边对齐。

示例代码如下:

     //MyFrame.java文件
     package com.a51work6;

     import java.awt.FlowLayout;

     import javax.swing.JButton;
     import javax.swing.JFrame;
     import javax.swing.JLabel;

     public class MyFrame extends JFrame {

         // 声明标签
         JLabel label;

         public MyFrame(String title) {
             super(title);

             setLayout(new FlowLayout(FlowLayout.LEFT, 20, 20));    ①
             // 创建标签
             label = new JLabel("Label");
             // 添加标签到内容面板
             getContentPane().add(label);                           ②

             // 创建Button1
             JButton button1 = new JButton("Button1");
             // 添加Button1到内容面板
             getContentPane().add(button1);                         ③

             // 创建Button2
             JButton button2 = new JButton("Button2");
             // 添加Button2到内容面板
             getContentPane().add(button2);                         ④

             // 设置窗口大小
             setSize(350, 120);
             // 设置窗口可见
             setVisible(true);

             // 注册事件监听器,监听Button2单击事件
             button2.addActionListener((event) -> {
                 label.setText("Hello Swing!");
             });

             // 注册事件监听器,监听Button1单击事件
             button1.addActionListener((event) -> {
                 label.setText("Hello World!");
             });
         }
     }

21.4.2 BorderLayout布局

BorderLayout布局是窗口的默认布局管理器,前面25.3节的示例就是采用BorderLayout布局实现。

BorderLayout 是JWindow、JFrame和JDialog的默认布局管理器。BorderLayout布局管理器把容器分成5个区域:North、South、East、West和Center,如图25-10所示每个区域只能放置一个组件。

BorderLayout主要的构造方法如下:

  • BorderLayout(int hgap, int vgap):创建一个BorderLayout对象,指定水平和垂直间隙,hgap参数是组件之间的水平间隙,vgap参数是组件之间的垂直间隙,单位是像素。
  • BorderLayout():创建一个BorderLayout对象,组件之间没有间隙。

BorderLayout布局有5个区域,为此BorderLayout中定义了5个约束常量,说明如下:

  • BorderLayout.CENTER:中间区域的布局约束(容器中央)。
  • BorderLayout.EAST:东区域的布局约束(容器右边)。
  • BorderLayout.NORTH:北区域的布局约束(容器顶部)。
  • BorderLayout.SOUTH:南区域的布局约束(容器底部)。
  • BorderLayout.WEST:西区域的布局约束(容器左边)。

示例代码如下:

     //MyFrame.java文件
     package com.a51work6;

     import java.awt.BorderLayout;
     import java.awt.Button;

     import javax.swing.JFrame;

     public class MyFrame extends JFrame {

         public MyFrame(String title) {
             super(title);

             // 设置BorderLayout布局
             setLayout(new BorderLayout(10, 10));                    ①

             // 添加按钮到容器的North区域
             getContentPane().add(new Button("北"), BorderLayout.NORTH);    ②
             // 添加按钮到容器的South区域
             getContentPane().add(new Button("南"), BorderLayout.SOUTH);    ③
             // 添加按钮到容器的East区域
             getContentPane().add(new Button("东"), BorderLayout.EAST);    ④
             // 添加按钮到容器的West区域
             getContentPane().add(new Button("西"), BorderLayout.WEST);    ⑤
             // 添加按钮到容器的Center区域
             getContentPane().add(new Button("中"), BorderLayout.CENTER);    ⑥

             setSize(300, 300);
             setVisible(true);
         }
     }

21.4.3 GridLayout布局

GridLayout布局以网格形式对组件进行摆放,容器被分成大小相等的矩形,一个矩形中放置一个组件。

GridLayout布局主要的构造方法如下:

  • GridLayout():创建具有默认值的GridLayout对象,即每个组件占据一行一列。
  • GridLayout(int rows, int cols):创建具有指定行数和列数的GridLayout对象。
  • GridLayout(int rows, int cols, int hgap, int vgap):创建具有指定行数和列数的GridLayout对象,并指定水平和垂直间隙。

示例代码如下:

     //MyFrame.java文件
     package com.a51work6;

     import java.awt.Button;
     import java.awt.GridLayout;

     import javax.swing.JFrame;

     public class MyFrame extends JFrame {

         public MyFrame(String title) {
             super(title);

             // 设置3行3列的GridLayout布局管理器
             setLayout(new GridLayout(3, 3));              ①

             // 添加按钮到第一行的第一格
             getContentPane().add(new Button("1"));        ②
             // 添加按钮到第一行的第二格
             getContentPane().add(new Button("2"));
             // 添加按钮到第一行的第三格
             getContentPane().add(new Button("3"));
             // 添加按钮到第二行的第一格
             getContentPane().add(new Button("4"));
             // 添加按钮到第二行的第二格
             getContentPane().add(new Button("5"));
             // 添加按钮到第二行的第三格
             getContentPane().add(new Button("6"));
             // 添加按钮到第三行的第一格
             getContentPane().add(new Button("7"));
             // 添加按钮到第三行的第二格
             getContentPane().add(new Button("8"));
             // 添加按钮到第三行的第三格
             getContentPane().add(new Button("9"));        ③

             setSize(400, 400);
             setVisible(true);
         }
     }

21.4.4 不使用布局管理器

如果要开发的图形用户界面应用不考虑跨平台,不考虑动态布局,窗口大小不变的,那么布局管理器就失去使用的意义。容器也可以不设置布局管理器,那么此时的布局是由开发人员自己管理的。

组件有三个与布局有关的方法setLocation()、setSize()和setBounds(),在设置了布局管理的容器中组件的这几个方法不起作用的,不设置布局管理时它们才起作用的。

这三个方法的说明如下:

  • void setLocation(int x, int y):方法是设置组件的位置。
  • void setSize(int width, int height):方法是设置组件的大小。
  • void setBounds(int x, int y, int width, int height):方法是设置组件的大小和位置。

示例代码如下:

     //MyFrame.java文件
     package com.a51work6;

     import javax.swing.JButton;
     import javax.swing.JFrame;
     import javax.swing.JLabel;
     import javax.swing.SwingConstants;

     public class MyFrame extends JFrame {

         public MyFrame(String title) {
             super(title);

             //设置窗口大小不变的
             setResizable(false);                                    ①

             // 不设置布局管理器
             getContentPane().setLayout(null);                       ②

             // 创建标签
             JLabel label = new JLabel("Label");
             // 设置标签的位置和大小
             label.setBounds(89, 13, 100, 30);                       ③
             // 设置标签文本水平居中
             label.setHorizontalAlignment(SwingConstants.CENTER);    ④
             // 添加标签到内容面板
             getContentPane().add(label);

             // 创建Button1
             JButton button1 = new JButton("Button1");
             // 设置Button1的位置和大小
             button1.setBounds(89, 59, 100, 30);                     ⑤
             // 添加Button1到内容面板
             getContentPane().add(button1);

             // 创建Button2
             JButton button2 = new JButton("Button2");
             // 设置Button2的位置
             button2.setLocation(89, 102);                           ⑥
             // 设置Button2的大小
             button2.setSize(100, 30);                               ⑦
             // 添加Button2到内容面板
             getContentPane().add(button2);

             // 设置窗口大小
             setSize(300, 200);
             // 设置窗口可见
             setVisible(true);

             // 注册事件监听器,监听Button2单击事件
             button2.addActionListener((event) -> {
                 label.setText("Hello Swing!");
             });

             // 注册事件监听器,监听Button1单击事件
             button1.addActionListener((event) -> {
                 label.setText("Hello World!");
             });
         }
     }

21.4.5 使用可视化设计工具

通过前面的学习,读者应该已经感受到了,通过代码实现界面布局工作量非常大。是否有可视化设计工具呢?各个主流的Java IDE工具都提供了可视化设计工具,IntelliJ IDEA和NetBeans IDE都内置了可视化设计工具,

Eclipse本身不提供可视化工具,但是可以安装其他可视化设计工具插件实现,目前流行的是WindowBuilder(http://www.eclipse.org/windowbuilder/ ),安装插件WindowBuilder网址是http://www.eclipse.org/windowbuilder/download.php ,找到适合你自己的Eclipse版本的在线安装地址.

21.5 Swing组件

Swing所有组件都继承自JComponent,主要有文本处理、按钮、标签、列表、面板、组合框、滚动条、滚动面板、菜单、表格和树等组件。下面介绍一下常用的组件。

21.5.1 标签和按钮

标签和按钮在前面示例中已经用到了,本节再深入地介绍一下它们。

Swing中标签类是JLabel,它不仅可以显示文本还可以显示图标,JLabel的构造方法如下:

  • JLabel():创建一个无图标无标题标签对象。
  • JLabel(Icon image):创建一个具有图标的标签对象。
  • JLabel(Icon image, int horizontalAlignment):通过指定图标和水平对齐方式创建标签对象。
  • JLabel(String text):创建一个标签对象,并指定显示的文本。
  • JLabel(String text, Icon icon, int horizontalAlignment):通过指定显示的文本、图标和水平对齐方式创建标签对象。
  • JLabel(String text, int horizontalAlignment):通过指定显示的文本和水平对齐方式创建标签对象。

上述构造方法horizontalAlignment参数是水平对齐方式,它的取值是SwingConstants 中定义的以下常量之一:LEFT、CENTER、RIGHT、LEADING 或 TRAILING。

Swing中的按钮类是JButton,JButton不仅可以显示文本还可以显示图标,JButton常用的构造方法有:

  • JButton():创建不带文本或图标的按钮对象。
  • JButton(Icon icon):创建一个带图标的按钮对象。
  • JButton(String text):创建一个带文本的按钮对象。
  • JButton(String text, Icon icon):创建一个带初始文本和图标的按钮对象。

示例代码如下:

     //MyFrame.java文件
     package com.a51work6;

     import javax.swing.Icon;
     import javax.swing.ImageIcon;
     import javax.swing.JButton;
     import javax.swing.JFrame;
     import javax.swing.JLabel;
     import javax.swing.SwingConstants;

     public class MyFrame extends JFrame {
         // 用于标签切换的图标
         private static Icon images[] = { new ImageIcon("./icon/0.png"),
                 new ImageIcon("./icon/1.png"),
                 new ImageIcon("./icon/2.png"),
                 new ImageIcon("./icon/3.png"),
                 new ImageIcon("./icon/4.png"),
                 new ImageIcon("./icon/5.png") };                ①

         // 当前页索引
         private static int currentPage = 0;                     ②

         public MyFrame(String title) {
             super(title);

             // 设置窗口大小不变的
             setResizable(false);

             // 不设置布局管理器
             getContentPane().setLayout(null);                   ③

             // 创建标签
             JLabel label = new JLabel(images[0]);
             // 设置标签的位置和大小
             label.setBounds(94, 27, 100, 50);
             // 设置标签文本水平居中
             label.setHorizontalAlignment(SwingConstants.CENTER);
             // 添加标签到内容面板
             getContentPane().add(label);

             // 创建向后翻页按钮
             JButton backButton = new JButton(new ImageIcon("./icon/ic_menu_back.png"));   ④
             // 设置按钮的位置和大小
             backButton.setBounds(77, 90, 47, 30);
             // 添加按钮到内容面板
             getContentPane().add(backButton);

             // 创建向前翻页按钮
             JButton forwardButton = new JButton(new ImageIcon("./icon/ic_menu_forward.png"));    ⑤
             // 设置按钮的位置和大小
             forwardButton.setBounds(179, 90, 47, 30);
             // 添加按钮到内容面板
             getContentPane().add(forwardButton);

             // 设置窗口大小
             setSize(300, 200);
             // 设置窗口可见
             setVisible(true);

             // 注册事件监听器,监听向后翻页按钮单击事件
             backButton.addActionListener((event) -> {
                 if (currentPage < images.length - 1) {
                     currentPage++;
                 }
                 label.setIcon(images[currentPage]);
             });

             // 注册事件监听器,监听向前翻页按钮单击事件
             forwardButton.addActionListener((event) -> {
                 if (currentPage > 0) {
                     currentPage--;
                 }
                 label.setIcon(images[currentPage]);
             });

         }
     }

21.5.2 文本输入组件

文本输入组件主要有:文本框(JTextField)、密码框(JPasswordField)和文本区(JTextArea)。文本框和密码框都只能输入和显示单行文本。当按下Enter键时,可以触发ActionEvent事件。而文本区可以输入和显示多行多列文本。

文本框(JTextField)常用的构造方法有:

  • JTextField():创建一个空的文本框对象。
  • JTextField(int columns):指定列数,创建一个空的文本框对象,列数是文本框显示的宽度,列数主要用于FlowLayout布局。
  • JTextField(String text):创建文本框对象,并指定初始化文本。
  • JTextField(String text, int columns):创建文本框对象,并指定初始化文本和列数。

JPasswordField继承自JTextField构造方法类似,这里不再赘述。

文本区(JTextArea)常用的构造方法有:

  • JTextArea():创建一个空的文本区对象。
  • JTextArea(int rows, int columns):创建文本区对象,并指定行数和列数。
  • JTextArea(String text):创建文本区对象,并指定初始化文本。
  • JTextArea(String text, int rows, int columns):创建文本区对象,并指定初始化文本、行数和列数。
    //MyFrame.java文件
     package com.a51work6;

     import java.awt.BorderLayout;

     import javax.swing.JFrame;
     import javax.swing.JLabel;
     import javax.swing.JPanel;
     import javax.swing.JPasswordField;
     import javax.swing.JTextArea;
     import javax.swing.JTextField;

     public class MyFrame extends JFrame {
         private JTextField textField;
         private JPasswordField passwordField;

         public MyFrame(String title) {
             super(title);

             // 设置布局管理BorderLayout
             getContentPane().setLayout(new BorderLayout());

             // 创建一个面板panel1放置TextField和Password
             JPanel panel1 = new JPanel();                            ①
             // 将面板panel1添加到内容视图
             getContentPane().add(panel1, BorderLayout.NORTH);        ②

             // 创建标签
             JLabel lblTextFieldLabel = new JLabel("TextField:");
             // 添加标签到面板panel1
             panel1.add(lblTextFieldLabel);

             // 创建文本框
             textField = new JTextField(12);                          ③
             // 添加文本框到面板panel1
             panel1.add(textField);


             // 创建标签
             JLabel lblPasswordLabel = new JLabel("Password:");
             // 添加标签到面板panel1
             panel1.add(lblPasswordLabel);

             // 创建密码框
             passwordField = new JPasswordField(12);                  ④
             // 添加密码框到面板panel1
             panel1.add(passwordField);

             // 创建一个面板panel2放置TextArea
             JPanel panel2 = new JPanel();                            ⑤
             getContentPane().add(panel2, BorderLayout.SOUTH);        ⑥

             // 创建标签
             JLabel lblTextAreaLabel = new JLabel("TextArea:");
             // 添加标签面板panel2
             panel2.add(lblTextAreaLabel);

             // 创建文本区
             JTextArea textArea = new JTextArea(3, 20);               ⑦
             // 添加文本区到面板panel2
             panel2.add(textArea);

             // 设置窗口大小
             pack();    // 紧凑排列,其作用相当于setSize()            ⑧

             // 设置窗口可见
             setVisible(true);

             textField.addActionListener((event)->{                   ⑨
                 textArea.setText("在文本框上按下Enter键");
             });
         }
     }

21.5.3 复选框和单选按钮

Swing中提供了用于多选和单选功能的组件。

多选组件是复选框(JCheckBox),复选框(JCheckBox)有时也单独使用,能提供两种状态的开和关。

单选组件是单选按钮(JRadioButton),同一组的多个单选按钮应该具有互斥特性,这也是为什么单选按钮也叫做收音机按钮(RadioButton),就是当一个按钮按下时,其他按钮一定抬起。同一组多个单选按钮应该放到同一个ButtonGroup对象,ButtonGroup对象不属于容器,它会创建一个互斥作用范围。

JCheckBox主要构造方法如下:

  • JCheckBox():创建一个没有文本、没有图标并且最初未被选定的复选框对象。
  • JCheckBox(Icon icon):创建有一个图标、最初未被选定的复选框对象。
  • JCheckBox(Icon icon, boolean selected):创建一个带图标的复选框对象,并指定其最初是否处于选定状态。
  • JCheckBox(String text):创建一个带文本的、最初未被选定的复选框对象。
  • JCheckBox(String text, boolean selected):创建一个带文本的复选框对象,并指定其最初是否处于选定状态。
  • JCheckBox(String text, Icon icon):创建带有指定文本和图标的、最初未被选定的复选框对象。
  • JCheckBox(String text, Icon icon, boolean selected):创建一个带文本和图标的复选框对象,并指定其最初是否处于选定状态。
    //MyFrame.java文件
     package com.a51work6;

     ...

     public class MyFrame extends JFrame implements ItemListener {          ①

         //声明并创建RadioButton对象
         private JRadioButton radioButton1 = new JRadioButton("男");        ②
         private JRadioButton radioButton2 = new JRadioButton("女");        ③

         public MyFrame(String title) {
             super(title);

             // 设置布局管理BorderLayout
             getContentPane().setLayout(new BorderLayout());

             // 创建一个面板panel1放置TextField和Password
             JPanel panel1 = new JPanel();
             FlowLayout flowLayout_1 = (FlowLayout) panel1.getLayout();
             flowLayout_1.setAlignment(FlowLayout.LEFT);
             // 将面板panel1添加到内容视图
             getContentPane().add(panel1, BorderLayout.NORTH);

             // 创建标签
             JLabel lblTextFieldLabel = new JLabel("选择你喜欢的编程语言:");
             // 添加标签到面板panel1
             panel1.add(lblTextFieldLabel);

             JCheckBox checkBox1 = new JCheckBox("Java");                   ④
             panel1.add(checkBox1);

             JCheckBox checkBox2 = new JCheckBox("C++");
             panel1.add(checkBox2);

             JCheckBox checkBox3 = new JCheckBox("Objective-C");
             //注册checkBox3对ActionLEvent事件监听
             checkBox3.addActionListener((event) -> {                       ⑤
                 // 打印checkBox3状态
                 System.out.println(checkBox3.isSelected());
             });
             panel1.add(checkBox3);

             // 创建一个面板panel2放置TextArea
             JPanel panel2 = new JPanel();
             FlowLayout flowLayout = (FlowLayout) panel2.getLayout();
             flowLayout.setAlignment(FlowLayout.LEFT);
             getContentPane().add(panel2, BorderLayout.SOUTH);

             // 创建标签
             JLabel lblTextAreaLabel = new JLabel("选择性别:");
             // 添加标签到面板panel2
             panel2.add(lblTextAreaLabel);

             //创建ButtonGroup对象
             ButtonGroup buttonGroup = new ButtonGroup();                   ⑥
             //添加RadioButton到ButtonGroup对象
             buttonGroup.add(radioButton1);
             buttonGroup.add(radioButton2);

             //添加RadioButton到面板panel2                                  ⑦
             panel2.add(radioButton1);
             panel2.add(radioButton2);

             //注册ItemEvent事件监听器
             radioButton1.addItemListener(this);                            ⑧
             radioButton2.addItemListener(this);

             // 设置窗口大小
             pack(); // 紧凑排列,其作用相当于setSize()

             // 设置窗口可见
             setVisible(true);
         }

         //实现ItemListener接口方法
         @Override
         public void itemStateChanged(ItemEvent) {                  ⑨

             if (e.getStateChange() == ItemEvent.SELECTED) {        ⑩
                JRadioButton button = (JRadioButton) e.getItem();
                System.out.println(button.getText());            
             }

         }
     }

21.5.4 下拉列表

Swing中提供了下拉列表(JComboBox)组件,每次只能选择其中的一项。

JComboBox常用的构造方法有:

  • JComboBox():创建一个下拉列表对象。
  • JComboBox(Object [] items):创建一个下拉列表对象,items设置下拉列表中选项。下拉列表中选项内容可以是任意类,而不再局限于String。
     //MyFrame.java文件
     package com.a51work6;
     ...
     public class MyFrame extends JFrame {

         // 声明下拉列表JComboBox
         private JComboBox choice1;
         private JComboBox choice2;

         private String[] s1 = { "Java", "C++", "Objective-C" };
         private String[] s2 = { "男", "女" };

         public MyFrame(String title) {
             super(title);

             getContentPane().setLayout(new GridLayout(2, 2, 0, 0));

             // 创建标签
             JLabel lblTextFieldLabel = new JLabel("选择你喜欢的编程语言:");
             lblTextFieldLabel.setHorizontalAlignment(SwingConstants.RIGHT);
             getContentPane().add(lblTextFieldLabel);

             // 实例化JComboBox对象
             choice1 = new JComboBox(s1);                                  ①
             // 注册Action事件侦听器,采用Lambda表达式
             choice1.addActionListener(e -> {                              ②
                 JComboBox cb = (JComboBox) e.getSource();                 ③
                 // 获得选择的项目
                 String itemString = (String) cb.getSelectedItem();        ④
                 System.out.println(itemString);
             });

             getContentPane().add(choice1);

             // 创建标签
             JLabel lblTextAreaLabel = new JLabel("选择性别:");
             lblTextAreaLabel.setHorizontalAlignment(SwingConstants.RIGHT);
             getContentPane().add(lblTextAreaLabel);

             // 实例化JComboBox对象,采用Lambda表达式
             choice2 = new JComboBox(s2);                                  ⑤
             // 注册项目选择事件侦听器
             choice2.addItemListener(e -> {                                ⑥
                 // 项目选择
                 if (e.getStateChange() == ItemEvent.SELECTED) {           ⑦
                     // 获得选择的项目
                     String itemString = (String) e.getItem();             ⑧
                     System.out.println(itemString);
                 }
             });
             getContentPane().add(choice2);

             // 设置窗口大小
             setSize(400, 150);

             // 设置窗口可见
             setVisible(true);
         }
     }

21.5.5 列表

Swing中提供了列表(JList)组件,可以单选或多选。

JList常用的构造方法有:

  • JList():创建一个列表对象。
  • JList(Object [] listData):创建一个列表对象,listData设置列表中选项。列表中选项内容可以是任意类,而不再局限于String。
    //MyFrame.java文件
     package com.a51work6;
     ...
     public class MyFrame extends JFrame {

         private String[] s1 = { "Java", "C++", "Objective-C" };

         public MyFrame(String title) {
             super(title);
             // 创建标签
             JLabel lblTextFieldLabel = new JLabel("选择你喜欢的编程语言:");
             getContentPane().add(lblTextFieldLabel, BorderLayout.NORTH);

             // 列表组件JList
             JList list1 = new JList(s1);                                        ①
             list1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);        ②
             // 注册项目选择事件侦听器,采用Lambda表达式。
             list1.addListSelectionListener(e -> {                               ③
                 if (e.getValueIsAdjusting() == false) {                         ④
                     // 获得选择的内容
                     String itemString = (String) list1.getSelectedValue();      ⑤
                     System.out.println(itemString);
                 }
             });
             getContentPane().add(list1);

             // 设置窗口大小
             setSize(300, 200);
             // 设置窗口可见
             setVisible(true);
         }

     }

21.5.6 分隔面板

Swing中提供了一种分隔面板(JSplitPane)组件,可以将屏幕分成左右或上下两部分。JSplitPane常用的构造方法有:

  • JSplitPane(int newOrientation):创建一个分隔面板,参数newOrientation指定布局方向,newOrientation取值是JSplitPane.HORIZONTAL_SPLIT水平或JSplitPane.VERTICAL_SPLIT垂直。
  • JSplitPane(int newOrientation, Component newLeftComponent, Component newRightComponent):创建一个分隔面板,参数newOrientation指定布局方向,newLeftComponent左侧面板组件,newRightComponent右侧面板组件。
     //MyFrame.java文件
     package com.a51work6;
     ...
     public class MyFrame extends JFrame {

         private String[] data = { "bird1.gif", "bird2.gif", "bird3.gif",
                 "bird4.gif", "bird5.gif", "bird6.gif" };

         public MyFrame(String title) {
             super(title);

             // 右边面板
             JPanel rightPane = new JPanel();
             rightPane.setLayout(new BorderLayout(0, 0));
             JLabel lblImage = new JLabel();
             lblImage.setHorizontalAlignment(SwingConstants.CENTER);
             rightPane.add(lblImage, BorderLayout.CENTER);

             // 左边面板
             JPanel leftPane = new JPanel();
             leftPane.setLayout(new BorderLayout(0, 0));
             JLabel lblTextFieldLabel = new JLabel("选择鸟儿:");
             leftPane.add(lblTextFieldLabel, BorderLayout.NORTH);

             // 列表组件JList
             JList list1 = new JList(data);
             list1.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
             // 注册项目选择事件侦听器,采用Lambda表达式。
             list1.addListSelectionListener(e -> {
                 if (e.getValueIsAdjusting() == false) {
                     // 获得选择的内容
                     String itemString = (String) list1.getSelectedValue();
                     String petImage = String.format("/images/%s", itemString);          ①
                     Icon icon = new ImageIcon(MyFrame.class.getResource(petImage));     ②
                     lblImage.setIcon(icon);
                 }
             });
             leftPane.add(list1, BorderLayout.CENTER);

             // 分隔面板
             JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
                                                     leftPane, rightPane);               ③
             splitPane.setDividerLocation(100);                                          ④

             getContentPane().add(splitPane, BorderLayout.CENTER);                       ⑤

             // 设置窗口大小
             setSize(300, 200);
             // 设置窗口可见
             setVisible(true);
         }

     }

21.5.7 使用表格

当有大量数据需要展示时,可以使用二维表格,有时也可以使用表格修改数据。表格是非常重要的组件。Swing提供了表格组件JTable类,但是表格组件比较复杂,它的表现形式与数据分离的,Swing的很多组件都是按照MVC2 设计模式进行设计的,JTable最有代表性,按照MVC设计理念JTable属于视图,对应的模型是javax.swing.table.TableModel接口,根据自己的业务逻辑和数据实现TableModel接口实现类。TableModel接口要求实现所有抽象方法,使用起来比较麻烦,有时只是使用很简单的表格,此时可以使用AbstractTableModel抽象类。实际开发时需要继承AbstractTableModel抽象类。

2 MVC是一种设计理念,将一个应用分为:模型(Model)、视图(View)和控制器(Controller),它将业务逻辑、数据、界面表示进行分离的方法组织代码,界面表示的变化不会影响到业务逻辑组件,不需要重新编写业务逻辑。

JTable类常用的构造方法有:

  • JTable(TableModel dm) :通过模型创建表格,dm是模型对象,其中包含了表格要显示的数据。
  • JTable(Object[][] rowData, Object[] columnNames):通过二维数组和指定列名,创建一个表格对象,rowData是表格中的数据,columnNames是列名。
  • JTable(int numRows, int numColumns):指定行和列数创建一个空的表格对象。
    //MyFrameTable.java文件
     package com.a51work6.array;
     ...
     public class MyFrameTable extends JFrame {

         // 获得当前屏幕的宽高
         private double screenWidth = Toolkit.getDefaultToolkit().getScreenSize().getWidth();      ①
         private double screenHeight = Toolkit.getDefaultToolkit().getScreenSize().getHeight();    ②

         private JTable table;

         public MyFrameTable(String title) {
             super(title);

             table = new JTable(rowData, columnNames);                           ③
             // 设置表中内容字体
             table.setFont(new Font("微软雅黑", Font.PLAIN, 16));
             // 设置表列标题字体
             table.getTableHeader().setFont(new Font("微软雅黑", Font.BOLD, 16));
             // 设置表行高
             table.setRowHeight(40);
             // 设置为单行选中模式
             table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
             // 返回当前行的状态模型
             ListSelectionModel rowSM = table.getSelectionModel();
             // 注册侦听器,选中行发生更改时触发
             rowSM.addListSelectionListener(new ListSelectionListener() {        ④

                 public void valueChanged(ListSelectionEvent e) {
                     //只处理鼠标按下
                     if (e.getValueIsAdjusting() == false) {
                         return;
                     }
                     ListSelectionModel lsm = (ListSelectionModel) e.getSource();
                     if (lsm.isSelectionEmpty()) {
                         System.out.println("没有选中行");
                     } else {
                         int selectedRow = lsm.getMinSelectionIndex();
                         System.out.println("第" + selectedRow + "行被选中");
                     }
                 }
             });                                                                 ⑤

             JScrollPane scrollPane = new JScrollPane();                         ⑥
             scrollPane.setViewportView(table);                                  ⑦
             getContentPane().add(scrollPane, BorderLayout.CENTER);

             // 设置窗口大小
             setSize(960, 640);
             // 计算窗口位于屏幕中心的坐标
             int x = (int) (screenWidth - 960) / 2;                              ⑧
             int y = (int) (screenHeight - 640) / 2;                             ⑨
             // 设置窗口位于屏幕中心
             setLocation(x, y);

             // 设置窗口可见
             setVisible(true);
         }

         // 表格列标题
         String[] columnNames = { "书籍编号", "书籍名称", "作者", "出版社", "出版日期", "库存数量" };
         // 表格数据
         Object[][] rowData = { { "0036", "高等数学", "李放", "人民邮电出版社", "20000812", 1 },
                 { "0004", "FLASH精选", "刘扬", "中国纺织出版社", "19990312", 2 },
                 { "0026", "软件工程", "牛田", "经济科学出版社", "20000328", 4 },
                 { "0015", "人工智能", "周未", "机械工业出版社", "19991223", 3 },
                 { "0037", "南方周末", "邓光明", "南方出版社", "20000923", 3 },
                 ...
                 { "0032", "SOL使用手册", "贺民", "电子工业出版社", "19990425", 2 } };

     }

第 22 章 反射

反射(Reflection)是程序的自我分析能力,通过反射可以确定类有哪些方法、有哪些构造方法以及有哪些成员变量。Java语言提供了反射机制,通过反射机制能够动态读取一个类的信息;能够在运行时动态加载类,而不是在编译期。反射可以应用于框架开发,它能够从配置文件中读取配置信息动态加载类、创建对象,以及调用方法和成员变量。

提示 Java反射机制在一般的Java应用开发中很少使用,即便是Java EE阶段也很少使用。除非你为了开发一个框架或出于兴趣对反射机制感兴趣,否则可以跳过本章内容。

22.1 Java反射机制API

Java反射机制API主要是 java.lang.Class类和java.lang.reflect包。

22.1.1 java.lang.Class类

java.lang.Class类是实现反射的关键所在,Class类的一个实例表示Java的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和void,void是“无类型”,主要用于方法返回值类型声明,表示不需要返回值。Class没有公有的构造方法,Class实例是由JVM在类加载时自动创建的。

在程序代码中获得Class实例可以通过如下代码实现;

//1.通过类型class静态变量
Class clz1 = String.class;
String str = "Hello";
//2.通过对象的getClass()方法
Class clz2 = str.getClass();

每一种类型包括类和接口等,都有一个class静态变量可以获得Class实例。另外,每一个对象都有getClass()方法可以获得Class实例,该方法是由Object类提供的实例方法。

Class类提供了很多方法可以获得运行时对象的相关信息,下面的程序代码展示了其中一些方法。

//HelloWorld.java文件
package com.a51work6;

public class HelloWorld {

    public static void main(String[] args) {

        // 获得Class实例
        // 1.通过类型class静态变量
        Class clz1 = String.class;

        String str = "Hello";
        // 2.通过对象的getClass()方法
        Class clz2 = str.getClass();
        //获得int类型Class实例
        Class clz3 = int.class;                         ①
        //获得Integer类型Class实例
        Class clz4 = Integer.class;                    ②

        System.out.println("clz2类名称:" + clz2.getName());
        System.out.println("clz2是否为接口:" + clz2.isInterface());
        System.out.println("clz2是否为数组对象:" + clz2.isArray());
        System.out.println("clz2父类名称:" + clz2.getSuperclass().getName());

        System.out.println("clz2是否为基本类型:" + clz2.isPrimitive());
        System.out.println("clz3是否为基本类型:" + clz3.isPrimitive());
        System.out.println("clz4是否为基本类型:" + clz4.isPrimitive());

    }
}

运行结果如下:

clz2类名称:java.lang.String
clz2是否为接口:false
clz2是否为数组对象:false
clz2父类名称:java.lang.Object
clz2是否为基本类型:false
clz3是否为基本类型:true
clz4是否为基本类型:false

注意上述代码第①行和第②行的区别,int和Integer的区别在于int是基本数据类型,所以输出结果为true,Integer是类,是引用类型。可见Class可以描述int等基本数据类型运行时实例。

22.1.2 java.lang.reflect包

java.lang.reflect包提供了反射中用到类,主要的类说明如下:

  • Constructor类:提供类的构造方法信息。
  • Field类:提供类或接口中成员变量信息。
  • Method类:提供类或接口成员方法信息。
  • Array类:提供了动态创建和访问Java数组的方法。
  • Modifier类:提供类和成员访问修饰符信息。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

public class HelloWorld {

    public static void main(String[] args) {

        try {
            // 动态加载xx类的运行时对象
            Class c = Class.forName("java.lang.String");      ①
            // 获取成员方法集合
            Method[] methods = c.getDeclaredMethods();        ②
            // 遍历成员方法集合
            for (Method method : methods) {                   ③
                // 打印权限修饰符,如public、protected、private
                System.out.print(Modifier.toString(method.getModifiers()));        ④
                // 打印返回值类型名称
                System.out.print(" " + method.getReturnType().getName() + " ");    ⑤
                // 打印方法名称
                System.out.println(method.getName() + "();");                      ⑥
            }
        } catch (ClassNotFoundException e) {                                       ⑦
            System.out.println("找不到指定类");
        }
    }
}

上述代码第①行是通过Class的静态方法forName(String)创建某个类的运行时对象,其中的参数是类全名字符串,如果在类路径中找不到这个类则抛出ClassNotFoundException异常,见代码第⑦行。

代码第②行是通过Class的实例方法getDeclaredMethods()返回某个类的成员方法对象数组。代码第③行是遍历成员方法集合,其中的元素是Method类型。

代码第④行的method.getModifiers()方法返回访问权限修饰符常量代码,是int类型,例如1代表public,这些数字代表的含义可以通过Modifier.toString(int)方法转换为字符串。代码第⑤行通过Method的getReturnType()方法获得方法返回值类型,然后再调用getName()方法返回该类型的名称。代码第⑥行method.getName()返回方法名称。

22.2 创建对象

Java反射机制提供了另外一种创建对象方法,Class类提供了一个实例方法newInstance(),通过该方法可以创建对象,使用起来比较简单,下面两条语句实现了创建字符串String对象。

Class clz = Class.forName("java.lang.String");
String str = (String) clz.newInstance();

这两条语句相当于String str = new String()语句。另外,需要注意newInstance()方法有可以会抛出InstantiationException和IllegalAccessException异常,InstantiationException不能实例化异常,IllegalAccessException是不能访问构造方法异常。

26.2.1 调用构造方法

调用方法newInstance()创建对象,这个过程中需要调用构造方法,上面的代码只是调用了String的默认构造方法。如果想要调用非默认构造方法,需要使用Constructor对象,它对应着一个构造方法,获得Constructor对象需要使用Class类的如下方法:

  • Constructor[] getConstructors():返回所有公有构造方法Constructor对象数组。
  • Constructor[] getDeclaredConstructors():返回所有构造方法Constructor对象数组。
  • Constructor getConstructor(Class... parameterTypes):根据参数列表返回一个共有Constructor对象。参数parameterTypes是Class数组,指定构造方法的参数列表。
  • Constructor getDeclaredConstructor(Class... parameterTypes):根据参数列表返回一个Constructor对象。参数parameterTypes同上。

示例代码如下:

//HelloWorld.java文件
package com.a51work6;

import java.lang.reflect.Constructor;

public class HelloWorld {

    public static void main(String[] args) {

        try {
            Class clz = Class.forName("java.lang.String");
            // 调用默认构造方法
            String str1 = (String) clz.newInstance();                    ①

            // 设置构造方法参数类型
            Class[] params = new Class[1];                               ②
            // 第一个参数是String
            params[0] = String.class;                                    ③

            // 获取与参数对应的构造方法
            Constructor constructor = clz.getConstructor(params);        ④
            // 为构造方法传递参数
            Object[] argObjs = new Object[1];                            ⑤
            // 第一个参数传递"Hello"
            argObjs[0] = "Hello";                                        ⑥

            // 调用非默认构造方法,构造方法第一个参数是String类型
            String str2 = (String) constructor.newInstance(argObjs);     ⑦
            System.out.println(str2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上述代码第①行是通过调用Class的newInstance()方法创建String对象,这个过程中使用String的默认构造方法public String()。

代码第②行~第⑦行通过反射机制调用String的public String(String original)构造方法。代码第②行和第③行是设置构造方法参数类型,参数有可能有多个需要Class数组类型。代码第④行是构造方法Constructor对象。代码第⑤行和第⑥行是为构造方法准备参数值,参数值放到Object数组中,与第②行的参数类型是一一对应的。

代码第⑦行是通过调用Constructor对象的newInstance(Object... initargs)方法创建String对象。

第23章 注解

第24章 数据库编程

第25章 项目实战

第26章 系统分析与涉及

posted @ 2021-02-03 20:36  北络  阅读(129)  评论(0编辑  收藏  举报