Java学习笔记

变量的作用域:

  全局变量:

定义方法(函数)和代码块之外的变量

局部变量:

定义在方法或者代码之间的变量

静变量

数据类型:

整型、长整形、短整型 、单精度浮点型、 双、布尔、 字节、 字符型、

数据类型转换

     Boolean类型不参与转换

char和short可以相互转换(互相强转)

小类型转换成大类型(自动转换)

强制类型转换:

当大类型数据转换成小类型需要强制转换(类型强转)

 语法:小类型 变量名 =(需要转换的类型)大类型

引用 数据类型不能与基本数据类型相互转换;两种数据类型要彼此兼容,否则不能互相转换。

引用数据类型转换需是继承关系,

任何数据类型与字符串做+运算都会变成字符串。

Java算术运算符

        1、一元运算:

                  -、++、--、(++在前,先自增再赋值)

        2、二元运算符:

                  加、减、乘、除、取余、

        3、逻辑或不论后面是真是假都会往后走。

  Java关系运算符

           1、基本类型的变量、值不能与引用类型的变量、值使用==进行作比较;

            2、

           3、

栈 通常用来存放方法

equals(obj)方法和==的区别:

          equals方法比较的是两个对象的内容是否相等

         ==比较的是两个 对象在内存中存放的地址是否为同一个;

  位移运算符

只有<<(向左位移)          >>(向右位移)   。无<<<或者>>>等符号

next()与nextLine()方法的区别;

1.当遇到空格或者回车的时候,next()停止读取

2.当遇到回车的时候nextLine()方法停止读取,读取整行数据

一、int类型的直接量

在程序中直接给出的整型数值,可分为二进制、十进制、八进制和十六进制4种,其中二进制需要以0B或Ob开头,八进制需要以

0开头,十六进制需要以0x或OX开头。例如123、012(对应十进制的10)、0x12(对应十进制的18)等。

二、long 类型的直接量

在整型数值后添加 | 或 L后就变成了long 类型的直接量。例如3L、0x12L(对应十进制的18L),

三、float 类型的直接量

在一个浮点数后添加f 或 F 就变成了 float 类型的直接量,这个浮点数可以是标准小数形式,也可以是科学计数法形式。例如

5.34F、3.14E5f。

四、double 类型的直接量

直接给出一个标准小数形式或者科学计数法形式的浮点数就是 double 类型的直接量。例如5.34、3.14E5。

五、boolean 类型的直接量

这个类型的直接量只有true 和 false。

char类型的直接量

char 类型的直接量有三种形式,分别是用单引号括起来的字符、转义字符和 Unicode 值表示的字符。例如'a',\n'和\u0061'。

    Java课堂笔记

Java常量

常量是指在程序的整个运行过程中值保持不变的量。

常量值

常量值又称为字面常量,它是通过数据直接表示的,因此有很多种数据类型,像整型和字符串型等。

整型常量值

整型常量值主要有如下3种形式。

十进制数形式:如54、-67、 0。

八进制数形式: Java 中的八进制常数的表示以0开头,如0125表示十进制数85, -013 表示十进制数-11。

十六进制数形式: Java中的十六进制常数的表示以0x或0X开头,如0x100表示十进制数256, -0x16 表示十进制数-22。

整型(int) 常量默认在内存中占32位。

实型常量值

Java的实型常量值主要有如下两种形式。

十进制数形式:由数字和小数点组成,且必须有小数点,如12.34、-98.0。

科学记数法形式:如1.75e5或32&E3,中e或E之前必须有数字,且e或E之后的数字必须为整数。

Java实型常量默认在内存中占64位,是具有双精度型(double) 的值。

单精度型数值一般要在该常数后面加 F或f,如69.7f, 表示-个float型实数,它在内存中占32位。

布尔型常量值

Java的布尔型常量只有两个值,即false (假)和true (真)。

Java变量的作用域

变量的作用域规定了变量所能使用的范围,只有在作用域范围内才能被使用。根据变量的声明地点不同,变量的作用域也不同

变量又分为成员变量与局部变量

成员变量分为

  1. 全局变量:无static修饰访问对象名变量名
  2. 静态变量:用static修饰,访问类名变量名

局部变量分为

  1. 方法参数变量:在整个方法内有下效
  2. 方法局部变量:从定义这个变量开始到方法结束这一段时间内有效
  3. 代码块局部变量:从定义这个变量开始到代码结束这一段时间内有效

局部变量在使用前必须被程序员主动初始化值

声明变量

首字符必须是字母、下划线、美元符号、人民币符号

标识符由数字大写字母小写字母下划线美元符号人民币符号以及所有在十六进制Oxco前的ASCLL码组成,不能吧关键字保留字作为标识符

定义常量

常量不同于常量值,它可以在程序中用符号来代替常量值使用,因此在使用前必须先定义。常量与变量类似也需要初始化,即在声明 常量的同时要赋予一个初始值。常量一旦初始化就不可以被修改。

final dataType variab1eName = value

其中,final 是定义常量的关键字,dataType 指明常量的数据类型,variableName 是变量的名称, value 是初始值。final关键字表示最终的,它可以修改很多元素,修饰变量就变成了常量。例如,以下语句使用final关键字声明常量。

Java中的数据类型:

 1、基本数据类型(八种)

byte(字节型)、short(短整型)、int(整形)、long(长整型)、float(单精度浮点型)、double(双精度浮点型)char(字符型)、boolean(布尔型)

字节型:byte by = 127;

 // System.out.println((byte) (by + 1));

 短整型:short sh = 56;

 整形: int num = 78946;

 长整型,注意: 需要早长整型末尾添加L或者l:long number = 56L;

 单精度浮点型, 注意: 需要早长整型末尾添加F或者f;float fl = 3.14F

 System.out.println(fl1);

 双精度浮点型,java中小数默认是double类型;double db = 3.14159;

 字符型, 字符型的值使用单引号引起来,当值为整形时不需要单引号

        char ch1 = 3;

        char ch2 = 'a';

        char ch3 = 97;

        System.out.println(ch1);

        System.out.println(ch3);

 布尔值类型,注意:Java中布尔值类型只有true和false两个值

        boolean bool1 = true;

        boolean bool2 = false;

2、引用数据类型

除了基本数据类型,其他全部是引用数类型,如:String、数组等

Java逻辑运算符

  1. &&   短路与:a && b  (a和b同时为真时为真)

  1. ||     短路或:a && b (a和b有一个为真时为真)

  1. &    逻辑与:同&&
  2. |     逻辑或:同||
  3. !    逻辑非:!a      (a为真时,值为假;a为假时,值为真)

短路与、短路或和逻辑与、逻辑或的区别:

  1. a && b :如果 a 为 false,则不计算 b(因为不论 b 为何值,结果都为 false)

 

  1. a || b :如果 a 为 true,则不计算 b(因为不论 b 为何值,结果都为 true)

  1. a & b :  a无论是true或false,b都会进行计算

  1. a | b :  a无论是true或false,b都会进行计算

创建扫描器对象

Scanner scan = new Scanner(System.in);

System.out.println("请输入您的名字:");

name = scan.next();

next()nextLine()方法的区别:

    1、当遇到空格或者回车的时候 next()方法停止读取

2、当遇到回车的时候 nextLine()方法停止读取,读取整行数据

1.二位数组用数据类型 [][] = new 数据类型[][];

例如: int[][] arr = new int[2][3];

2.遍历二位数组;

可以用两个for-each,例如for(int i:arr[])for(int j:arr);遍历二维数组;

3.访问二维数组的语法:arr[x][y] = 1;就是给第x + 1行第y + 1列的值赋成1;

4.二维数组的存储,大概是arr[x][y]中,x存的是二维数组的索引,也就是数组的地址,x存在栈,y存在堆区;static修饰的存在方法区;在类中,static修饰可以直接用类名访问,用对象访问会报警告;       

5.System.arraycopy,src:原数组;srcPos: 原数组起始位置(从这个位置开始复制);dest:目标数组;destPos:目标数组粘贴的起始位置;length: 复制的个数

6.方法的重载是方法名相同,参数列表不同,返回值不做要求;

7.Arrays.copyOf(arr[],7);即arr【】为目标数组,7为新复制的数组长度;

8.数组的扩容与缩容量,使用int aee1[] = Arrays.copyOf(arr,5);

其中arr为原来的数组,5位新数组的大小;增容的操作也类同;这里并没有销毁原来的数组,准确的应该算复制;

9.Arrays.sort能对数组进行排序,返回值为int,返回数据为目标值的下标,二分查找的使用的前提是数组必须排好序;

10,使用equals能比较两个数组是否相等;两个参数分别是一个数组和另一个数组,如果相同返回true,如果不同返回false;

if语句

if双分枝结构:为条件表达语句,当语句为ture时执行语句,当语句为false,执行else语句。

嵌套switch语句

if语句与switch语句都表示条件语句,可以使用效率与实用性两方面加以区分。

从效率上看

对同一个变量的不同值作为条件判断时,当判断条件较多时可以使用switch语句

从实用性上看

当判断条件不多时使用if语句更加方便

while循环与do...while循环的区别:

 1、while是先判断后执行,do...while循环是先执行然后再判断

  2、do...while循环至少执行一次

  while循环与do...while的特点:都是循环执行某一语句,循环次数素不固定

   while循环:

   语法:

  while (循环条件) {

   循环体}

   案例:计算1+到100的和

  1+2+3+4+5+6+7+....+100

        int num = 1;

        int sum = 0;

        while (num <= 100) {

            sum += num;

            num ++;

        }

        System.out.println(sum);

 do.while循环

      语法:

     do{

           循环体

        } while (循环条件);

    案例:计算1+到100的和

       1+2+3+4+5+6+7+....+100

        int num2 = 1;

        int sum2 = 0;

        do {

            sum2 += num2;

            num2 ++;

        } while (num2 <= 100);

        System.out.println(sum2);

  for循环嵌套

如果循环嵌套层数超过了三层,那么一定是逻辑出了问题

 数组定义:

    用来存放相同类型的一组数据

     数组下标从0开始,对数组元素进行操作是通过数组的下标(索引)

     数组一旦被创建,则数组的长度不可被修改

       语法:

          静态创建数组

           数据类型[] 数组名 = {值1,值2....}

            数据类型 数组名[] = {值1,值2....}

 数组元素必须是相同类型的,不允许出现混合类型。

数组的默认初始化、

int a2[] = new int[2];//默认值0,0

boolean[] b = new boolean[2];//默认值 false,false

String[] s = new String[2];//默认值null

随机数:

     1Random random = new Random();

           生成指定范围内的随机数 random.nextInt(10)

     2double random = Math.random();

             生成随机数的范围 0.0-1.0,但是永远到不了 

       动态创建数组

    数据类型[] 数组名 = new 数据类型[数组长度]

        数据类型 数组名[] = new 数据类型[数组长度]

      

    // 静态创建数组,特点:创建数组的同时给数组元素赋值

      int arr[] = {1, 2, 4, 8, 9};

   // 动态创建数组,特点创建数组时指定数组的长度

    long[] arr1 = new long[5];

 // 数组元素必须是相同类型的,不允许出现混合类型。

    // dArr[2] = "hello array"; 数组元素类型与数组类型不匹配,语法报错

     二维数组的动态初始化: .

数据类型数组名00 = new数据类型[m][n]数据类型00数组名= new数据类型[m][n]

数据类型0数据名0 = new数据类型[m]In]数组的复制:浅拷贝

System.arrarcoplarr,start,dist,index.length)

Arrays,copofrr,length)

4、Arrays类 具有以下常用的功能

1、给数组赋值:通过调用fill方法

2、对数组排序:通过sort方法

3、比较数组:通过equals方法比较数组中元素的值是否相等

5、数组的扩容于缩容

实质:就是创建一个新的数组,新数组避原来的数组(大,为扩容,小,为缩容)打印数组Arrays. toStingarr)方法的作用是将数组- -字符串的形式输出

给数组赋值:

通常用il方法

Arrays binarySercharr9);该方法的返回值为查找到的元素下标的值

Java面向对象- 0OP

一、什么是面向对象

面向过程与面向对象:

面向对象过程思想:

1、步骤清晰简单

2、面向过程合适处理一些较为简单的问题

面向对象思想的本质就是:以类的方式组织代码,以对象的组织(封装) 数据类是对象的抽象,对象是类的实例。

什么是类:类型/类别,代表-类个体

对象:真实存在的单个个体;

类中包含: 1、所有对象所共有的属性/特征-- -成员变量

2、所有对象所共有的行- -方法

一个类可以创建多个对象

同一类型所创建的对象,结构相同,数据类型不同。

类是对象的模板,对象是类的具体实例

面向对象的三大特征:

封装,继承,多态类的定义:,

修饰符class 类名{成员变量类型变量名 ;

访问对象成员变量和方法,语法:

对象名.成员变量名字/方法名字,能点出什么东西全看类有扫描东西

00A :面向对象分析法

面向对象设计

一个Java文件中只能有一个public修饰的Java类,并且这个被public修饰的Java类名必须与Java文件名字-致。

创建对象的语法:

数据类型

引用类型变量

指向

new关键字对象

Student

new

造成空指针异常的原因:是因为对象为null定义类中的方法:

class类名{

修饰符返回值类型 方法名(参数列表) {

创建并且使用对象:

使用new关键字创建对象:new的运算语法为:

new类名()

# Java面向对象——OOP

# 补充:

包机制的作用:

​为了更好的组织类,Java提供了包机制,用于区分类的命名空间,包语句的语法格式为:

```java

package  包名1.包名2.……

```

一般利用公司的域名倒置作为包名。

```java

import  完整的包名.类名;

```

**注:**

package语句必须位于Java源文件中的第一行,否则编译不通过。

# 一、什么是面向对象

**面向过程与面向对象:**

​**面向过程思想:**

​ 1、 步骤清晰简单,第一步要做什么,第二步要做什么……

​ 2、 面向过程适合处理一些较为简单的问题

​ **面向对象思想:**

​ 1、 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考,最后,才对某个分类下的细节进行面向过程的思索。

​ 2、 面向对象适合处理复杂的问题,适合处理需要多人协作的问题

**面向过程的结构化程序设计:**

结构化程序设计的弊端

​        1、缺乏对数据的封装

​ 2、数据与方法(操作数据)的分离

**什么是面向对象:**

​ 面向对象的本质就是:以类的方式组织代码,以对象的组织(封装)数据。

**什么是抽象的数据类型:**

​ 所谓抽象数据类型可以理解为:将不同类型的数据的集合组成一个整体,用来描述一种新的事务。如:对象类型数据

**什么是?什么是对象?**

​  1、现实世界是由很多很多对象组成的

   基于对象抽出了类

​  2、对象:真实存在的单个的个体

​   类:类型/类别,代表一类个体

​  3、类中可以包含:

   3.1、所有对象所共有的属性/特征------------成员变量

   3.2、所有对象所共有的行为-----------------方法

​ 4、一个类可以创建多个对象

​ 同一类型所创建的对象,结构相同,数据不同

5、类是对象的模板,对象是类的具体的实例

**面向对象的三大特征:**

封装、继承、多态

<b style='color: red'>补充:</b>

​ **OOA**

​ **Object-Oriented Analysis:面向对象分析法**

  指的是在一个系统的开发过程中进行了系统业务调查以后,按照面向对象的思想来分析问题。OOA与结构化分析有较大的区别。OOA所强调的是在系统调查资料的基础上,针对OO方法所需要的素材进行的归类花分析和整理,而不是对管理业务现状和方法的分析。

OOA(面向对象的分析)模型由5个层次(主题层、对象类层、结构层、属性层和服务层)和5个活动(标识对象类、标识结构、定义主题、定义属性和定义服务)组成。在这种方法中定义了两种对象类之间的结构,一种称为分类结构,一种称为组装结构。分类结构就是所谓的一般与特殊的关系。组装结构则反映了对象之间的整体与部分的关系

​ OOA在定义属性的同时,要识别实例连接。实例连接是一个示例与另一个实例的映射关系。

​ OOA在定义服务的同时要识别消息连接。当一个对象需要向另一个对象发送消息时,它们之间就存在消息连接。

​ OOA中的5个层次和5个活动继续贯穿在OOD(面向对象设计)过程中。OOD模型由4各部分组成。它们分别是设计问题域部分、设计人机交互部分、设计任务管理部分、和设计数据管理部分。

**一、OOA的主要原则。**

​ 1)**抽象:**从许多食物中舍弃个别的、非本质的特征,抽取共同的、本质性的特征,就叫做抽象。愁乡石形成概念的必须手段。

​ 抽象原则有两个方面的意义:第一,尽管问题域中的事物是很复杂的,但是分析员并不需要了解和描述它们的一切,只需要分析 其中与系统目标有关的事物及其本质性特征。第二,通过舍弃个体事物在细节上的差异,抽取其共同特性而得到一批事物的抽象概 念。

​ 抽象是面向对象方法中使用最为广泛的原则。抽象原则包括过程抽象和数据抽象两个方面。

​ **过程抽象**是指,任何一个完成确定功能的操作序列,其使用者都可以把它看做一个单一的实体,尽管实际上它可能是由一系列更 低级的操作完成的。

​  **数据抽象**是根据施加于数据之上的操作来定义数据类型,并限定数据的值只能由这些操作来修改和观察。数据抽象是OOA的核 心原则。它强调把数据(属性)和操作(服务)结合为一个不可分的系统单位(即对象),对象的外部只需要知道它做什么,而不必 知道它如何做。

​ 2)**封装**就是把对象的属性和服务结合为一个不可分的系统单位,并尽可能隐蔽对象的内部细节。

​ 3)**继承:**特殊类的对象拥有的其一般类的全部属性与服务,称作特殊类对一般类的继承。(父子

​ 在OOA中运用继承原则,就是在每个由一般类和特殊类形成的一半----特殊结构总,把一般类的对象实例和所有特殊类的对象实 例都共同具有的属性和服务,一次性的在一般类中进行显式的定义。在特殊类中不在重复的定义一般类中已定义的东西,但是在语义 上,特殊类却自动的、隐含地拥有它的一般类(以及所有更上层的一般类)中定义的全部属性和服务。继承原则的好处是:是系统模 型比较简练也比较清晰。

​ 4)**分类:**就是把具有相同属性和服务的对象划分为一类,用类作为这些对象的抽象描述。分类原则实际上是抽象原则运用于对象描 述时的一种表现形式。

​ 5)**聚合:**又称组装,其原则是:把一个复杂的事物看成若干比较简单的事物组装体,从而简化对复杂事物的描述。

​ 6)**关联:**是人类思考问题时经常运用的思想方法:通过一个事物联想到另外的事物。能使人发生联想的原因是事物之间确实存在着某些联系。

​ 7)**消息通信:**这一原则要求对象之间只能通过消息进行通信,而不允许在对象之外直接地存取对象内部的属性。通过消息进行通信是由于封装原则而引起的。在OOA中要求消息连接表示出对象之间的动态联系。

​ 8)**粒度控制:**一般来讲,人在面对一个复杂的问题域时,不可能在同一时刻既能纵观全局,又能洞察秋毫。因此需要控制自己的视野:考虑全局时,注意其大的组成部分,暂时不详查每一部分的具体的细节:考虑某部分的细节时则暂时撇开其余的部分。着就是粒度控制原则

​ 9)**行为分析:**显示世界中事物的行为是复杂的。由大量的事物所构成的问题域中各种行为旺旺相互依赖交织

**二、面向对象分析产生三种模型**

​ 1、**对象模型:**对用例模型进行分析,把系统分解成互相协作的分析类,通过类图\对象图描述对象\对象的属性\对象间的关系,是系统的静态模型

​ 2、**动态模型:**描述系统的动态行为,通过时序图/协作图/描述对象的交互,以揭示对象间如何协作来完成每个具体的用例。单个对象的状态变化/动态行为可以通过状态图来表示。

​ 3、**功能模型**(即用例模型à作为输入)

​​ ​

封装也叫作信息封装,确保组件不会以不可预期的方式改变其它组件内部状态,只有在那些提供了内部状态改变方法的组建中,才可以访问其内部状态。每类组件都提供了一个与其它组件联系的接口,并规定了其它组件进行调用的方法。

多态性:组件的引用和类集会涉及到其它许多不同类型的组件,而且引用组件所产生的的结果得依据实际调用的类型。

继承性:允许在现存的组件基础上创建子类组件,着统一并强调了多态性和封装性。典型的来说就是用类来对组件进行分组,而且还可以定义新类为现存的类的扩展,这样就可以将类组织成树形或网状结构,这体现了动作的通用性。

# 二、类与对象的创建

**定义一个:**

​ **定义类的成员变量:**

​        1、类的定义包括“成员变量”的定义和“方法”的定义,其中“成员变量”用于描述对象  共同的数据结构,“方法”则是所有对象共同的行为。

​        2、Java语言中,类的成员变量的定义可以使用如下的语法:

```java

修饰符 class  类名 {

      成员变量类型   变量名;

      ………

}

```

​ 3、对象创建后,其成员变量可以按照默认的方式初始化,初始化对象成员变量时,其默认值的规则如下表所示:

| **成员变量类型**                                  | **默认初始值** |

| ------------------------------------------------- | -------------- |

| 数值类型(byte、short、int、long、float、double) | 0              |

| boolean类型                                       | false          |

| char类型                                          | /u0000         |

| 引用类型                                          | null           |

**定义类的方法:**

类中除了定义成员变量,还可以定义方法,用于描述对象的行为,封装对象的功能,Java语言中,可以按照如下方式定义类中的方法:

```java

class  类名 {

    修饰符  返回值类型   方法名(参数列表){

         方法体………

}

……………

}

```

**创建并且使用对象:**

**使用new关键字创建对象:**

​ 类定义完成之后,可以使用new关键字创建对象,创建对象的过程通常称为实例化对象,new的运算语法为:

```java

new   类名();

如:new  JFrame(); //可以创建一个窗体对象

创建对象语法:

数据类型   引用类型变量   指向  new关键字        对象

Student       zs        =    new           Student();

```

**引用类型变量:**

​ 为了能够实例化对象进行访问控制,需要使用一个特殊的变量——引用。

​ 引用类型变量可以存放该类对象的地址信息,通常称为“指向该类的对象”,当一个引用类型变量指向该类的对象时,就可以通过这个变量对对象实施访问。

除了8种基本数据类型之外,用类、接口、数组等声明的变量都称为引用类型变量,简称“引用”。

**访问对象的成员变量,调用方法:**

​ 可以通过引用访问对象的成员变量或调用方法。

```java

创建cell类:

public class Cell {

// 定义属性

int row;

int col;

//定义方法

public void drop() {}

public void moveLeft(int len) {}

public String getCellInfo() {

return "";

}

}

创建Cell类并访问成员变量及方法

Cell  c  =  new  Cell();

c.row  =  3;

c.col   =  2;

c.drop();

c.moveLeft(2);

String   str   =   c.getCellInfo();

```

**引用类型变量的赋值:**

​ 1、引用类型变量存储的是对象的地址信息,相同类型的引用类型变量之间也可以互相赋值。

​ 2、引用类型变量之间的赋值不会创建新的对象,但有可能会使两个以上的引用指向同一个对象。例如:

```java

Emp  e1  =  new  Emp();

Emp  e2  =  e1; // 将e1的值(对象地址信息)赋给e2,此时e1和e2指向相同对象

**null和NullPointerException:**

​ 1、对于引用类型变量。可以对其赋值为null,null的含义为“空”,表示还没有指向任何对象,例如:

```java

Emp  emp  =  null; //emp为的值null,没有指向任何对象

emp  =  new  Emp(); //emp指向了一个Emp对象

```

​ 2、当一个引用变量的值为null的时候,如果通过引用访问对象成员变量或者调用方法不符合逻辑时,会产生NullPointerException,例如:

```java

int[]  arr  =  null;

arr.length;

# 四、Java内存分析

## 1、堆、栈、方法区:

**堆:** new出来的对象(包括实例变量)

**栈:** 局部变量(包括方法的参数)

**方法区:** .class字节码文件(包括方法、静态变量)

## 2、堆,栈,方法区,常量池 的位置分布图:

<img src="06-面向对象(OOP).assets\image-20220913225751369.png" alt="image-20220913225751369" style="zoom:80%;" />

## 3、内存区域类型

**1.寄存器:**最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;

**2. 堆:**存放所有new出来的对象;

**3. 栈**:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(对象可能在常量池里)(字符串常量对象存放在常量池中。);

**4. 静态域:**存放静态成员(static定义的);

**5. 常量池:**存放字符串常量和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中 ;

**6. 非RAM存储:**硬盘等永久存储空间

1.内存管理:由JVM来管理的

​ **1) 堆:**

​ 1.1) 存储new出来的对象(包括实例变量)

​ 1.2) 垃圾: 没有任何引用所指向的对象

​     **垃圾回收器(GC)**不定时到内存中清理垃圾,

​     回收过程是透明的(看不到的),不一定一发现垃圾就立刻回收,

​     调用System.gc()可以建议虚拟机尽快调度GC来回收

   1.3) 内存泄漏: 不再使用的内存没有被及时的回收

     建议: 对象不再使用时及时将引用设置为null

   1.4) 实例变量的生命周期:

​ 在创建对象时存储在堆中,对象被回收时一并被回收

  **2) 栈:**

   2.1) 存储正在调用的方法中的所有局部变量(包括方法的参数)

   2.2) 调用方法时,会在栈中为该方法分配一块对应的栈帧,

​     栈帧中存储方法中的局部变量(包括参数),

​     方法调用结束时,栈帧被清除,局部变量一并被清除

   2.3) 局部变量的生命周期:

​ 方法被调用时存储在栈中,方法结束时与栈帧一并被清除

  **3) 方法区:**

​ 3.1) 存储.class字节码文件(包括静态变量、方法)

​ 3.2) 方法只有一份,通过this来区分具体的调用对象

# 六、访问控制修饰符

## 1、public和private

private修饰的成员变量和方法仅仅只能在本类中访问调用。

public修饰的成员变量和方法可以在任何地方调用,public修饰的内容是对外提供可以被调用的功能,需要相对稳定,private修饰的内容是对类实现的封装,如果“公开”会增加维护的成本。

## 2、 protected和默认访问控制

​ 1)用protected修饰的成员变量和方法可以被子类及同一个包中的类使用。

​ 2)默认的访问控制即不书写任何访问修饰符,默认访问控制的成员变量和方法可以被同一个包中的类调用。

## 3、访问控制修饰类

​ 1)对于类的修饰可以使用public和默认的方式,public修饰的类可以被任意一个类使用,默认方法控制的类只能被同包中的类使用。

​ 2)protected和private可以用于修饰内部类

# 七、封装

封装,简单的说就是该露的露,该藏的藏我们在设计程序是要追求“高内聚,低耦合”,其中,高内聚指的是类的内部数据操作细节由自己完成,不允许外部干涉。低耦合指的是仅暴露少量的方法给外部调用(使用get/set方法)。

​ 封装(对数据的隐藏),通常来说,应禁止直接访问应该对象中数据的实际表示,而是应该通过操作接口来访问,这种称为信息隐藏。

**封装的意义:**

1、对外提供可调用的,稳定的功能。

2、封装容易变化的,具体的细节,外界不可访问

这样封装的意义在于:

​        a. 降低代码出错的可能性,便于维护。

​ b. 当内部的实现细节改变时,只要保证对外的功能定义不变,其他的模块就不会因此而受到牵连。

# 八、继承

## 1、泛化的过程

## 2、extends关键字

1、通过extends关键字可以实现类的继承

​ 2、子类可以继承父类的成员变量及成员方法,同时也可以定义自己的成员变量和成员方法。

​ 3、Java语言不支持多重继承,一个类只能继承一个父类,但是一个父类可以有多个子类。

例如:

<img src="06-面向对象(OOP).assets\image-20220913231357154.png" alt="image-20220913231357154" style="zoom:80%;" />

## 3、继承中的构造方法

​ 1、子类的构造方法中必须通过super关键字调用父类的构造方法,这样可以妥善的初始化继承自父类的成员变量。

​ 2、如果子类的构造方法中没有调用父类的构造方法,Java编译器会自动的加入对父类的无参构造方法的调用(如果父类没有无参构造方法,则会有编译错误)。

## 4、父类的引用指向子类的对象

​ 1、一个子类的对象可以向上造型为父类的类型,即,定义父类型的引用可以指向子类型的对象。

​ 2、 父类的引用可以指向子类的对象,但是通过父类的引用只能访问父类所定义的成员,不能访问子类扩展的部分。

例如:

# 九、super关键字

**在java里面,对于super关键字通常有两种用法:**

​ 1. 用在子类的构造方法里(初始化用),主要是调用父类的默认构造方法,如果父类有不止一个构造方法,可以通过super指定具体的构造函数,比如 super(paras)。

​ 2. 用在子类里调用隐藏或重写的属性或行为,比如 super.onDestroy()等等。

​ 对于第1种需要注意,super表示当前类的父类对象,super()调用的是父类默认的构造方法,即这样可以对父类进行初始化。如果没有对父类进行初始化,当子类调用父类的方法时,便会从逻辑上出现错误,因为没对父类初始化,父类的方法和属性便没有内存空间。

 **关于super 与 this 关键字的对比(区别):**

1. super(参数):调用基类中的某一个构造函数(应该位于子类构造函数中的第一条语句)。

2. this(参数):调用本类中的构造函数(应该位于构造函数中的第一条语句)。

3. super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)。

4. this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)

5、调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。

6、super()和this()类似,区别是,super()从子类中调用父类的构造方法,this()在同一类内调用其它构造方法。

7、super()和this()均需放在构造方法内第一行。

8、尽管可以用this调用一个构造器,但却不能调用两个。

9、this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。

10、 this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。

11、 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

# 十、方法重写

**方法的重写(Override):**

​ 1、发生在父子类中,方法名称相同,参数列表相同,方法体不同

​ 2、重写方法被调用时,看对象的类型

​ 3、遵循"两同两小一大"原则:------------了解

​ 3.1、两同:

​ 3.1.1、方法名称相同

​ 3.1.2、参数列表相同

3.2、两小:

​ 3.2.1、派生类方法的返回值类型小于或等于超类方法的

​ a、void时,必须相同

​ b、基本数据类型时,必须相同

​ c、引用数据类型时,小于或等于

​ 3.2.2、派生类方法抛出的异常小于或等于超类方法的

​ 3.3、一大:

​ 3.3.1、派生类方法的访问权限大于或等于超类方法的

**重写与重载的区别:**

​ 1、重写(Override):

​ 1.1、发生在父子类中,方法名相同,参数列表相同,方法体不同

​ 1.2、"运行期绑定",看对象的类型绑定方法

​ 2、重载(Overload):

​ 2.1、发生在一个类中,方法名相同,参数列表不同,方法体不同

​ 2.2、"编译期绑定",看参数/引用的类型绑定方法

<p style="color: red">补充:</p>

​ 编译期:.java源文件,经过编译,生成.class字节码文件

​ 运行期:JVM加载.class并运行.class

​ 编译错误:只是检查语法

# 十一、多态

​ 多态指的是同一方法可以根据发送对象的不同而采用多种不同的行为方式。

​ 一个对象的实际类型是确定的,但是可以指向对象的引用的类型有很多。

​ 多态存在的条件:

​ 1、 有继承关系

​ 2、 子类重写父类的方法

​ 3、 父类引用指向子类对象

**多态的意义:**

​ 1、行为的多态(所有抽象方法都是多态的)

​ 2、对象的多态(所有对象都是多态的)

**多态的表现形式:**

​ 1、重写:根据对象的不同来表现多态

​ 2、重载:根据参数的不同来表现多态

注:多态是方法的多态性,属性没有多态性。

多态

  • 多态: 是指同一行为,具有多个不同表现形式。
  • 多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。

父类类型 变量名 = new 子类/实现类构造器;

变量名.方法名();

引用类型转换

向上转型:多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。当父类引用指向一个子类对象时,便是向上转型。

使用格式:

父类类型  变量名 = new 子类类型();

向下转型:父类类型向子类类型向下转换的过程,这个过程是强制的。

一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。

使用格式:

子类类型 变量名 = (子类类型) 父类变量名;

Final关键字

fina:  不可改变,最终的含义。可以用于修饰类、方法和变量。

    1. 类:被修饰的类,不能被继承。
    2. 方法:被修饰的方法,不能被重写。
    3. 变量:被修饰的变量,有且仅能被赋值一次。

  1. final修饰的类,不能被继承。

格式如下:final class 类名 {

}

  1. final修饰的方法,不能被重写。

格式如下:

修饰符 final 返回值类型 方法名(参数列表){

方法体

}

抽象类

父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了(因为子类对象会调用自己重写的方法)。换句话说,父类可能知道子类应该有哪个功能,但是功能具体怎么实现父类是不清楚的(由子类自己决定),父类只需要提供一个没有方法体的定义即可,具体实现交给子类自己去实现。我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类就是抽象类。

抽象方法: 没有方法体的方法。

抽象类:包含抽象方法的类。

abstract使用格式

abstract是抽象的意思,用于修饰方法方法和类,修饰的方法是抽象方法,修饰的类是抽象类。

抽象方法

使用`abstract` 关键字修饰方法,该方法就成了抽象方法,抽象方法只包含一个方法名,而没有方法体。

定义格式:

修饰符 abstract 返回值类型 方法名 (参数列表);

抽象类

如果一个类包含抽象方法,那么该类必须是抽象类。注意:抽象类不一定有抽象方法,但是有抽象方法的类必须定义成抽象类。

定义格式:

abstract class 类名字 {    

}

接口

定义格式

//接口的定义格式:

interface 接口名称{

    // 抽象方法

}

// 接口的声明:interface

// 接口名称:首字母大写,满足“驼峰模式”

内部类

按定义的位置来分

1. 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)

2. 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)

3. 局部内部类,类定义在方法内

4. 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

内部类的使用格式:

外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类

获取成员内部类对象的方式:

外部类.内部类 变量 = new 外部类().new 内部类();

枚举

声明枚举时必须使用 enum 关键字,然后定义枚举的名称、可访问性、基础类型和成员等。

任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。

例 1

下面代码定义了一个表示性别的枚举类型 SexEnum 和一个表示颜色的枚举类型 Color。

    

public enum SexEnum{

    male,female;

}

public enum Color{

    RED,BLUE,GREEN,BLACK;

}

接口(Interface)

接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过实现(implements)接口的方式,从而来实现接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。

除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。

接口与类相似点:

1)一个接口可以有多个方法。

2)接口文件保存在 .java 结尾的文件中,文件名使用接口名。

3)接口的字节码文件保存在 .class 结尾的文件中。

4)接口相应的字节码文件必须在与包名称相匹配的目录结构中。

接口与类的区别:

1)接口不能用于实例化对象。

2)接口没有构造方法。

3)接口中所有的方法必须是抽象方法。

4)接口不能包含成员变量,除了 static 和 final 变量。

5)接口不是被类继承了,而是要被类实现。

6)接口支持多继承(接口不能继承类,接口只能继承接口)。

接口特性:

1)接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。

2)接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。

3)接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

package com.zpark.oop.day07;

public interface  Demo05 {

//    public Demo05(); 接口中不能有构造器

    // 在接口中定义的变量默认为 public final static修饰

    public final static String NAME = "大锤";

    // 接口中的方法必须全部是抽象方法

    public abstract void a();

    int b();

}

抽象类和接口的区别:

1. 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。

2. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。

3. 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法。

4. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

内部类

java内部类的几种类型:成员内部类,静态内部类,方法内部类,匿名内部类。

成员内部类:成员内部类是类内部的非静态类。成员内部类不能定义静态方法和变量(final修饰的除外)。这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。

成员内部类的使用方法:

1、 Inner 类定义在 Outer 类的内部,相当于 Outer 类的一个成员变量的位置,Inner 类可以使用任意访问控制符,如 public 、 protected 、 private 等

2、 Inner 类中定义的 test() 方法可以直接访问 Outer 类中的数据,而不受访问控制符的影响,如直接访问 Outer 类中的私有属性a

3、 定义了成员内部类后,必须使用外部类对象来创建内部类对象,而不能直接去 new 一个内部类对象,即:内部类 对象名 = 外部类对象.new 内部类( );

静态内部类:

静态内部类是 static 修饰的内部类,这种内部类的特点是:

​1、 静态内部类不能直接访问外部类的非静态成员,但可以通过 new 外部类().成员 的方式访问。

​2、 如果外部类的静态成员与内部类的成员名称相同,可通过“类名.静态成员”访问外部类的静态成员;如果外部类的静态成员与 内部类的成员名称不相同,则可通过“成员名”直接调用外部类的静态成员。

​3、 创建静态内部类的对象时,不需要外部类的对象,可以直接创建 内部类 对象名= new 内部类();

方法内部类(局部内部类):

方法内部类就是内部类定义在外部类的方法中,方法内部类只在该方法的内部可见,即只在该方法内可以使用。

需要注意:由于方法内部类不能在外部类的方法以外的地方使用,因此方法内部类不能使用访问控制符和 static 修饰符。

枚举

枚举是一个被命名的整型常数的集合,用于声明一组带标识符的常数。类似这种当一个变量有几种固定可能的取值时,就可以将它定义为枚举类型。

声明枚举

声明枚举时必须使用 enum 关键字,然后定义枚举的名称、可访问性、基础类型和成员等。

任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔。

提示:如果没有显式地声明基础类型的枚举,那么意味着它所对应的基础类型是 int。

枚举类

Java 中的每一个枚举都继承自 java.lang.Enum 类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例,这些枚举成员默认都被 final、public, static 修饰,当使用枚举类型成员时,直接使用枚举名称调用成员即可。所有枚举实例都可以调用 Enum 类的方法。

为枚举添加方法

Java 为枚举类型提供了一些内置的方法,同时枚举常量也可以有自己的方法。此时要注意必须在枚举实例的最后一个成员后添加分号,而且必须先定义枚举实例。

Java 中的 enum 还可以跟 Class 类一样覆盖基类的方法。

字符串String处理:直接定义和使用String类定义;

将字符数组转为字符串:char[] ch = new char[]{‘H’,’e’,’l’,’l’,’o’}

创建建字符串

        String str = new String(ch);

分配一个新字符串,该字符串包含字符数组参数的子数组中的字符:

      offset参数是子数组第一个字符的索引,count参数指定子数组的长度。

      复制子数组的内容;字符数组的后续修改不会影响新创建的字符串。

   char[] ch1 = new char[]{'H', 'e', 'l', 'l', 'o', ',', 'w', 'o', 'r', 'l', 'd'};

   String str1 = new String(ch1, 2, 8);

   System.out.println(str1);

分配一个新字符串,该字符串包含来自Unicode码点数组参数子数组的字符:

offset参数是子数组第一个编码点的索引,count参数指定子数组的长度。

子数组的内容被转换为字符;int数组的后续修改不会影响新创建的字符串。

        int[] arr = new int[]{65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75};

        String str2 = new String(arr, 0, 5);

        System.out.println(str2);

    }

将字符串转为int类型的值前提是字符内容必须是纯数字,使用包装类进行转换。

String转换为int:

Integer.parselnt(str)

Integer.valueOf(str).intValue();

Int转换为String:

String s = String.valueOf(i);

String s = Integer.toString(i);

String s = “”+i;

将int类型数据转为String类型

        int num1 = 56;

        String str = num1 + "";

        System.out.println(str + 1);

valueOf()方法将数据的内部格式转换为可读的形式。是一种静态方法,对于所有内置的类型,在字符串被重载,以便每一种类型都能被转换成字符串。

parse()

parseXxx(String)这种形式,是指把字符串转换为数值型;

toString()可以把一个引用类型转换为String字符串类型。

concat()方法:String类的concat()方法实现了将一个字符串连接到另外一个字符串后面。语法:字符串1.concat(字符串2);

其他类型数据直接使用“+”拼接;

获取字符串长度:

使用length(),语法:字符串名.length();

1、获取字符串长度

2、将字符串转大小写

3、去除字符串两端的空白

String str = "Hello Java";

// 使用length()函数获取字符串长度

System.out.println(str.length());

// 将字符串转为大写

System.out.println(str.toUpperCase());

// 将字符串转小写

System.out.println(str.toLowerCase());

String str1 = "              hello                     ";

System.out.println(str1.length());

// 取出字符串两端空白

String str2 = str1.trim();

System.out.println(str2.length());

字符串截取(substring()):

         * substring(int start): 从指定位置开始截取,start表示开始位置下标

         * substring(int start, int end):截取指定范围内的字符串,含前不含尾

  • 分割字符串(spilt())​ String 类的 split() 方法可以按指定的分割符对目标字符串进行分割,分割后的内容存放在字符串数组中。

九、字符串的替换 1、replace() 方法​ replace() 方法用于将目标字符串中的指定字符(串)替换成新的字符(串),其语法格式如下:```java字符串.replace(String oldChar, String newChar)```​ 其中,oldChar 表示被替换的字符串;newChar 表示用于替换的字符串。replace() 方法会将字符串中所有 oldChar 替换成 newChar

十、字符串比较​ 字符串比较是常见的操作,包括比较相等、比较大小、比较前缀和后缀串等。在 Java 中,比较字符串的常用方法有 3 个:equals() 方法、equalsIgnoreCase() 方法、 compareTo() 方法。## 1、equals() 方法​ equals() 方法将逐个地比较两个字符串的每个字符是否相同。如果两个字符串具有相同的字符和长度,它返回 true,否则返回 false。

十一、1、equalsIgnoreCase() 方法​ equalsIgnoreCase() 方法的作用和语法与 equals() 方法完全相同,唯一不同的是 equalsIgnoreCase() 比较时不区分大小写。当比较两个字符串时,它会认为 A-Z 和 a-z 是一样的。

2、equals()与==的比较​ 理解 equals() 方法和`==`运算符执行的是两个不同的操作是重要的。如同刚才解释的那样,equals() 方法比较字符串对象中的字符。而`==`运算符比较两个对象引用看它们是否引用相同的实例

  1. 4、compareTo() 方法​ 通常,仅仅知道两个字符串是否相同是不够的。对于排序应用来说,必须知道一个字符串是大于、等于还是小于另一个。一个字符串小于另一个指的是它在字典中先出现。而一个字符串大于另一个指的是它在字典中后出现。字符串(String)的 compareTo() 方法实现了这种功能。​ compareTo() 方法用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值。
  2. **提示:**如果两个字符串调用 equals() 方法返回 true,那么调用 compareTo() 方法会返回 0。

  • 字符串查找​

 在给定的字符串中查找字符或字符串是比较常见的操作。字符串查找分为两种形式:一种是在字符串中获取匹配字符(串)的索引值,另一种是在字符串中获取指定索引位置的字符。## 1、根据字符查找​ String 类的 indexOf() 方法和 lastlndexOf() 方法用于在字符串中获取匹配字符(串)的索引值。

    1. indexOf() 方法​ indexOf() 方法用于返回字符(串)在指定字符串中首次出现的索引位置,如果能找到,则返回索引值,否则返回 -1。
    2. fromIndex 表示查找时的起始索引,如果不指定 fromIndex,则默认从指定字符串中的开始位置(即 fromIndex 默认为 0)开始查找。
    3. lastlndexOf() 方法​ lastIndexOf() 方法用于返回字符(串)在指定字符串中最后一次出现的索引位置,如果能找到则返回索引值,否则返回 -1。
  • StringBuffer 类是可变字符串类,创建 StringBuffer 类的对象后可以随意修改字符串的内容。每个 StringBuffer 类的对象都能够存储指定容量的字符串,如果字符串的长度超过了 StringBuffer 类对象的容量,则该对象的容量会自动扩大
  • 删除字符串​ StringBuffer 类提供了 deleteCharAt() 和 delete() 两个删除字符串的方法,下面详细介绍。**1、deleteCharAt() 方法**​ deleteCharAt() 方法用于移除序列中指定位置的字符,该方法的语法格式如下:```javaStringBuffer 对象.deleteCharAt(int index);```​ deleteCharAt() 方法的作用是删除指定位置的字符,然后将剩余的内容形成一个新的字符串
  • StringBuilder 和 StringBuffer 功能基本相似,方法也差不多。不同的是,StringBuffer 是线程安全的,而 StringBuilder 则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用 StringBuilder 类。

1、静态常量​ Math 类中包含 E 和 PI 两个静态常量,正如它们名字所暗示的,它们的值分别等于 e(自然对数)和 π(圆周率)。

2、生成随机数(random()和Random类)​ 在 Java 中要生成一个指定范围之内的随机数字有两种方法:一种是调用 Math 类的 random() 方法,一种是使用 Random 类。​ Random 类提供了丰富的随机数生成方法,可以产生 boolean、int、long、float、byte 数组以及 double 类型的随机数,这是它与 random() 方法最大的不同之处。random() 方法只能产生 double 类型的 0~1 的随机数。​ Random 类位于 java.util 包中,该类常用的有如下两个构造方法。​ **Random():**该构造方法使用一个和当前系统时间对应的数字作为种子数,然后使用这个种子数构造 Random 对象。​ **Random(long seed):**使用单个 long 类型的参数创建一个新的随机数生成器。​ Random 类提供的所有方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的概率是均等的,在表 1 中列出了 Random 类中常用的方法

3、Math 类的 random() 方法没有参数,它默认会返回大于等于 0.0、小于 1.0 的 double 类型随机数,即 0<=随机数<1.0。对 random() 方法返回的数字稍加处理,即可实现产生任意范围随机数的功能。

4、BigInteger 类型的数字范围较 Integer 类型的数字范围要大得多。BigInteger 支持任意精度的整数,也就是说在运算中 BigInteger 类型可以准确地表示任何大小的整数值。​ 除了基本的加、减、乘、除操作之外,BigInteger 类还封装了很多操作,像求绝对值、相反数、最大公约数以及判断是否为质数等。​ 要使用 BigInteger 类,首先要创建一个 BigInteger 对象。BigInteger 类提供了很多种构造方法,其中最直接的一种是参数以字符串形式代表要处理的数字。

5、igDecimal 类​ BigInteger 和 BigDecimal 都能实现大数字的运算,不同的是 BigDecimal 加入了小数的概念。一般的 float 和 double 类型数据只能用来做科学计算或工程计算,但由于在商业计算中要求数字精度比较高,所以要用到 BigDecimal 类。BigDecimal 类支持任何精度的浮点数,可以用来精确计算货币值。​ BigDecimal 常用的构造方法如下。​ 1、BigDecimal(double val):实例化时将双精度型转换为 BigDecimal 类型。​ 2、BigDecimal(String val):实例化时将字符串形式转换为 BigDecimal 类型。​ BigDecimal 类的方法可以用来做超大浮点数的运算,像加、减、乘和除等。在所有运算中,除法运算是最复杂的,因为在除不尽的情况下,末位小数的处理方式是需要考虑的。

6、Java时间日期的处理​ 在 Java 中获取当前时间,可以使用 java.util.Date 类和 java.util.Calendar 类完成。其中,Date 类主要封装了系统的日期和时间的信息,Calendar 类则会根据系统的日历来解释 Date 对象。下面详细介绍这两个类的具体使用。## 1、Date 类​ Date 类表示系统特定的时间戳,可以精确到毫秒。Date 对象表示时间的默认顺序是星期、月、日、小时、分、秒、年。#### 1.1、构造方法​ Date 类有如下两个构造方法。​ 1、Date():此种形式表示分配 Date 对象并初始化此对象,以表示分配它的时间(精确到毫秒),使用该构造方法创建的对象可 以获取本地的当前时间。​ 2、Date(long date):此种形式表示从 GMT 时间(格林尼治时间)1970 年 1 月 1 日 0 时 0 分 0 秒开始经过参数 date 指定的毫 秒数。

7、Calendar 类​ Calendar 类是一个抽象类,它为特定瞬间与 YEAR、MONTH、DAY_OF—MONTH、HOUR 等日历字段之间的转换提供了一些方法,并为操作日历字段(如获得下星期的日期) 提供了一些方法。​ 创建 Calendar 对象不能使用 new 关键字,因为 Calendar 类是一个抽象类,但是它提供了一个 getInstance() 方法来获得 Calendar类的对象。getInstance() 方法返回一个 Calendar 对象,其日历字段已由当前日期和时间初始化

十七、

  1.  实现 int 和 Integer 的相互转换**​ 可以通过 Integer 类的构造方法将 int 装箱,通过 Integer 类的 intValue 方法将 Integer 拆箱。
    2、 将字符串转换为数值类型**​ 在 Integer 和 Float 类中分别提供了以下两种方法:​ ① Integer 类(String 转 int 型)```javaint parseInt(String s);```​ s 为要转换的字符串。​ ② Float 类(String 转 float 型)```javafloat parseFloat(String s)```​ **注意:**使用以上两种方法时,字符串中的数据必须由数字组成,否则转换时会出现程序错误。


    3、将整数转换为字符串**​ Integer 类有一个静态的 toString() 方法,可以将整数转换为字符串
    4、Object类​ Object 是 Java 类库中的一个特殊类,也是所有类的父类。也就是说,Java 允许把任何类型的对象赋给 Object 类型的变量。当一个类被定义后,如果没有指定继承的父类,那么默认父类就是 Object 类。因此,以下两个类表示的含义是一样的。


    5、toString() 方法​ toString() 方法返回该对象的字符串,当程序输出一个对象或者把某个对象和字符串进行连接运算时,系统会自动调用该对象的 toString() 方法返回该对象的字符串表示。​ Object 类的 toString() 方法返回“运行时类名@十六进制哈希码”格式的字符串,但很多类都重写了 Object 类的 toString() 方法,用于返回可以表述该对象信息的字符串。


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


7、getClass() 方法​ getClass() 方法返回对象所属的类,是一个 Class 对象。通过 Class 对象可以获取该类的各种信息,包括类名、父类以及它所实现接口的名字等。

8、Integer类​ Integer 类在对象中包装了一个基本类型 int 的值。Integer 类对象包含一个 int 类型的字段。此外,该类提供了多个方法,能在 int 类型和 String 类型之间互相转换,还提供了处理 int 类型时非常有用的其他一些常量和方法。## 1、Integer 类的构造方法​ Integer 类中的构造方法有以下两个:​ **Integer(int value):**构造一个新分配的 Integer 对象,它表示指定的 int 值。​ **Integer(String s):**构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。

9、Integer 类的常量​ Integer 类包含以下 4 个常量。​ **MAX_VALUE:**值为 231-1 的常量,它表示 int 类型能够表示的最大值。​ **MIN_VALUE:**值为 -231 的常量,它表示 int 类型能够表示的最小值。​ **SIZE:**用来以二进制补码形式表示 int 值的比特位数。​ **TYPE:**表示基本类型 int 的 Class 实例。


10、Float 类的构造方法​ Float 类中的构造方法有以下 3 个。​ **Float(double value):**构造一个新分配的 Float 对象,它表示转换为 float 类型的参数。​ **Float(float value):**构造一个新分配的 Float 对象,它表示基本的 float 参数。​ **Float(String s):**构造一个新分配的 Float 对象,它表示 String 参数所指示的 float 值。


11、Float 类的常用常量​ 在 Float 类中包含了很多常量,其中较为常用的常量如下。​ **MAX_VALUE:**值为 1.4E38 的常量,它表示 float 类型能够表示的最大值。​ **MIN_VALUE:**值为 3.4E-45 的常量,它表示 float 类型能够表示的最小值。​ **MAX_EXPONENT: **有限 float 变量可能具有的最大指数。​ **MIN_EXPONENT:**标准化 float 变量可能具有的最小指数。​ **MIN_NORMAL:**保存 float 类型数值的最小标准值的常量,即 2-126。​ **NaN:**保存 float 类型的非数字值的常量。​ **SIZE:**用来以二进制补码形式表示 float 值的比特位数。​ **TYPE:**表示基本类型 float 的 Class 实例。


12、Double类​ Double 类在对象中包装了一个基本类型 double 的值。Double 类对象包含一个 double 类型的字段。此外,该类还提供了多个方法,可以将 double 类型与 String 类型相互转换,同时 还提供了处理 double 类型时比较常用的常量和方法。## 1、Double 类的构造方法​ Double 类中的构造方法有如下两个。​ **Double(double value):**构造一个新分配的 Double 对象,它表示转换为 double 类型的参数。​ **Double(String s):**构造一个新分配的 Double 对象,它表示 String 参数所指示的 double 值。

13、Number类​ Number 是一个抽象类,也是一个超类(即父类)。Number 类属于 java.lang 包,所有的包装类(如 Double、Float、Byte、Short、Integer 以及 Long)都是抽象类 Number 的子类。

14、在 validateUser() 方法中,使用 for 循环遍历用户输入的用户名、密码和年龄,对其每个字符进行验证,判断其是否符合要求。在验证的过程中,分别使用了 Character 类的 isLetter() 方法、isLetterOrDigit() 方法和 isDigit() 方法。


15、Byte 类的构造方法​ Byte 类提供了两个构造方法来创建 Byte 对象。

File类

1.创建File实例

在 Java 中,File 类是 java.io 包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等。

public static void main(String[] args) {

        // 创建File对象

        File f1 = new File("C:\\WorkAndStudy\\java\\a\\c");

        // 删除空的目录

        boolean b1 = f1.delete();

        System.out.println(b1);

        /**

         * 删除目录

         * 任务:

         *      删除java目录下的a目录

         */

        File f2 = new File("C:\\WorkAndStudy\\java\\a");

        // 注意:删除目录时,目录必须为空,否则删除失败

        boolean b2 = f2.delete();

        System.out.println(b2);

    }

 2.File类常用方法

方法名称 说明

boolean canRead() 测试应用程序是否能从指定的文件中进行读取

boolean canWrite() 测试应用程序是否能写当前文件

boolean delete() 删除当前对象指定的文件

boolean exists() 测试当前 File 是否存在

String getAbsolutePath() 返回由该对象表示的文件的绝对路径名

String getName() 返回表示当前对象的文件名或路径名(如果是路径,则返回最后一级子路径名)

String getParent() 返回当前 File 对象所对应目录(最后一级子目录)的父目录名

boolean isAbsolute() 测试当前 File 对象表示的文件是否为一个绝对路径名。该方法消除了不同平台的差异,可以直接判断 file 对象是否为绝对路径。在 UNIX/Linux/BSD 等系统上,如果路径名开头是一条斜线/,则表明该 File 对象对应一个绝对路径;在 Windows 等系统上,如果路径开头是盘符,则说明它是一个绝对路径。

boolean isDirectory() 测试当前 File 对象表示的文件是否为一个路径

boolean isFile() 测试当前 File 对象表示的文件是否为一个“普通”文件

long lastModified() 返回当前 File 对象表示的文件最后修改的时间

long length() 返回当前 File 对象表示的文件长度

String[] list() 返回当前 File 对象指定的路径文件列表

String[] list(FilenameFilter) 返回当前 File 对象指定的目录中满足指定过滤器的文件列表

boolean mkdir() 创建一个目录,它的路径名由当前 File 对象指定

boolean mkdirs() 创建一个目录,它的路径名由当前 File 对象指定

boolean renameTo(File) 将当前 File 对象指定的文件更名为给定参数 File 指定的路径名

字节流

        字节流是由字节组成的,字符流是由字符组成的. Java里字符由两个字节组成.字节流是最基本的,所有的InputStream和OutputStream的子类都是,主要用在处理二进制数据。

1.常用方法:

        InputStream:

                read() 读取一个字节

                read(byte[]) 读取若干(数组长度)字节

                available() 获取可读的字节数

                close() 关闭流, 释放资源

        OutputStream:\nwrite(int) 写出一个字节

                write(byte[]) 写出数组中的所有字节

                write(byte[],start,len);

                close() 关闭流, 释放资源

使用文件字节流对象实现复制功能

public static void main(String[] args) throws Exception {

        // 创建读取文件字节流对象

        FileInputStream fis = new FileInputStream("dir/a.txt");

        // 创建输出文件字节流对象

        FileOutputStream fos = new FileOutputStream("dir/a_back.dat");

        // 定义每次读取的大小

        byte[] data = new byte[1024 * 8];

        int len = -1;

        // 循环读取和写入文件

        while ((len = fis.read(data)) != -1) {

            // 写入数据

            fos.write(data, 0, len);

        }

        // 关闭流

        fis.close();

        fos.close();

    }

FileInputStream: 文件字节输入流

FileInputStream从文件系统中的文件获取输入字节。 什么文件可用取决于主机环境。

FileInputStream用于读取诸如图像数据的原始字节流。

构造方法

FileInputStream(File file)

     通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的 File对象 file命名。

FileInputStream(FileDescriptor fdObj)

     通过使用文件描述符 fdObj创建 FileInputStream ,该文件描述符表示与文件系统中的实际文件的现有连接。

FileInputStream(String name)

     通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name命名。

public static void main(String[] args) throws Exception {

        // 创建fileInputStream对象

        FileInputStream fis = new FileInputStream("dir/a.txt");

        // 常用方法

        for (int i = 0; i < 10; i++) {

            int read = fis.read();

            System.out.println(fis.read(new byte[1024]));

            System.out.println(read);

            // 将输入流的指针返回到设置标记的起始处

            // fis.reset();

        }

        // 关闭文件

        fis.close();

    }

public static void main(String[] args) throws Exception {

        // 创建字节输出流对象

        FileOutputStream fos = new FileOutputStream("dir/fos.txt", true);

        // 写入数据

        String str = "晓看天色,暮看云....";

        fos.write(str.getBytes("utf-8"));

        // 关闭文件

        fos.close();

    }

字符流

BufferedInputStream为另一个输入流添加了功能,即缓冲输入并支持mark和reset方法的功能。创建BufferedInputStream将创建一个内部缓冲区数组。 当从流中读取或跳过字节时,内部缓冲区将根据需要从所包含的输入流中重新填充,一次很多字节。 mark操作会记住输入流中的一个点,并且reset操作会导致从最近的mark操作读取的所有字节在从包含的输入流中取出新字节之前重新读取。

构造方法    Constructor 描述

BufferedInputStream(InputStream in)

     创建一个 BufferedInputStream并保存其参数,输入流 in供以后使用。

BufferedInputStream(InputStream in, int size)

     创建具有指定缓冲区大小的 BufferedInputStream ,并保存其参数,输入流 in供以后使用。

public static void main(String[] args) throws Exception {

        // 创建字节输入流对象

        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("dir/bos.txt"));

        // 创建读缓冲区大小

        byte[] data = new byte[1024 * 8];

        // 将读取数据转出字符串输出

        bis.read(data);

        String result = new String(data, "utf-8");

        System.out.println(result);

        // 关闭流

        bis.close();

    }

public class BufferedOutputStream extends FilterOutputStream该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节,而不必为写入的每个字节导致底层系统的调用。构造方法摘要

构造方法    Constructor 描述

BufferedOutputStream(OutputStream out)

     创建一个新的缓冲输出流,以将数据写入指定的底层输出流。

BufferedOutputStream(OutputStream out, int size)

     创建一个新的缓冲输出流,以便以指定的缓冲区大小将数据写入指定的底层输出流。

public static void main(String[] args) throws Exception {

        // 创建流对象

        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dir/bos.txt"));

        // 创建需要写入的字符串

        String str = "该类实现缓冲输出流。 通过设置这样的输出流,应用程序可以向底层输出流写入字节";

        // 写入数据

        bos.write(str.getBytes("utf-8"));

        // 关闭流

        bos.close();

    }

BufferedReader 类主要用于辅助其他字符输入流,它带有缓冲区,可以先将一批数据读到内存缓冲区。接下来的读操作就可以直接从缓冲区中获取数据,而不需要每次都从数据源读取数据并进行字符编码转换,这样就可以提高数据的读取效率。

public static void main(String[] args) throws Exception {

        // 创建字节流对象

        FileInputStream fis = new FileInputStream("dir/osw.txt");

        // 创建字符流对象

        InputStreamReader isr = new InputStreamReader(fis, "utf-8");

        // 创建缓冲流对象

        BufferedReader br = new BufferedReader(isr);

        // 读

        String s = br.readLine();

        System.out.println(s);

        String s1 = br.readLine();

        System.out.println(s1);

        // 读取到文件末尾结果为null

        String s2 = br.readLine();

        System.out.println(s2);

        // 关闭资源

        fis.close();

        isr.close();

        br.close();

————————————————

多线程

一、多线程基础

现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务,例如:

CPU执行代码都是一条一条顺序执行的,但是,即使是单核cpu,也可以同时运行多个任务。因为操作系统执行多任务实际上就是让CPU对多个任务轮流交替执行。

例如,假设我们有语文、数学、英语3门作业要做,每个作业需要30分钟。我们把这3门作业看成是3个任务,可以做1分钟语文作业,再做1分钟数学作业,再做1分钟英语作业:

这样轮流做下去,在某些人眼里看来,做作业的速度就非常快,看上去就像同时在做3门作业一样

类似的,操作系统轮流让多个任务交替执行,例如,让浏览器执行0.001秒,让QQ执行0.001秒,再让音乐播放器执行0.001秒,在人看来,CPU就是在同时执行多个任务。

即使是多核CPU,因为通常任务的数量远远多于CPU的核数,所以任务也是交替执行的。

1、进程

在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。

某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

多进程模式(每个进程只有一个线程):

多线程模式(一个进程有多个线程):

多进程+多线程模式(复杂度最高):

2、进程 vs 线程

进程和线程是包含关系,但是多任务既可以由多进程实现,也可以由单进程内的多线程实现,还可以混合多进程+多线程。具体采用哪种方式,要考虑到进程和线程的特点。

和多线程相比,多进程的缺点在于:

1、创建进程比创建线程开销大,尤其是在Windows系统上;

2、进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。

而多进程的优点在于:

多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃 会直接导致整个进程崩溃。

3、多线程

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。

因此,对于大多数Java程序来说,我们说多任务,实际上是说如何使用多线程实现多任务。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。

Java多线程编程的特点又在于:

  • 多线程模型是Java程序最基本的并发模型;
  • 网络、数据库、Web开发等都依赖Java多线程模型。

二、创建新线程

Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。

要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法:

public class Main {
   public static void main(String[] args) {
       Thread t = new Thread();
       t.start(); // 启动新线程
  }
}

但是这个线程启动后实际上什么也不做就立刻结束了。我们希望新线程能执行指定的代码,有以下几种方法:

1、通过继承Thread来创建线程

Thread派生一个自定义类,然后覆写run()方法:

public class Main {
   public static void main(String[] args) {
       Thread t = new MyThread();
       t.start(); // 启动新线程
  }
}


class MyThread extends Thread {
   @Override
   public void run() {
       System.out.println("start new thread!");
  }
}

执行上述代码,注意到start()方法会在内部自动调用实例的run()方法。

2、实现 Runnable 接口创建线程

创建Thread实例时,传入一个Runnable实例:

public class Main {
   public static void main(String[] args) {
       Thread t = new Thread(new MyRunnable());
       t.start(); // 启动新线程
  }
}


// 创建线程方式二,实现Runnable接口
class MyRunnable implements Runnable {
   @Override
   public void run() {
       System.out.println("start new thread!");
  }
}

或者用Java8引入的lambda语法进一步简写为:

public class Main {
   public static void main(String[] args) {
       Thread t = new Thread(() -> {
           System.out.println("start new thread!");
      });
       t.start(); // 启动新线程
  }
}

有同学会问,使用线程执行的打印语句,和直接在main()方法执行有区别吗?

区别大了去了。我们看以下代码:

public class Main {
   public static void main(String[] args) {
       System.out.println("main start...");
       
       Thread t = new Thread() {
           public void run() {
               System.out.println("thread run...");
               System.out.println("thread end.");
          }
      };
       
       t.start();
       
       System.out.println("main end...");
  }
}

主线程main线程,main线程执行的代码有4行,首先打印main start,然后创建Thread对象,紧接着调用start()启动新线程。当start()方法被调用时,JVM就创建了一个新线程,我们通过实例变量t来表示这个新线程对象,并开始执行。

接着,main线程继续执行打印main end语句,而t线程在main线程执行的同时会并发执行,打印thread runthread end语句。

run()方法结束时,新线程就结束了。而main()方法结束时,主线程也结束了。

我们再来看线程的执行顺序:

1、main线程肯定是先打印main start,再打印main end

2、t线程肯定是先打印thread run,再打印thread end

但是,除了可以肯定,main start会先打印外,main end打印在thread run之前、thread end之后或者之间,都无法确定。因为从t线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序。

要模拟并发执行的效果,我们可以在线程中调用Thread.sleep(),强迫当前线程暂停一段时间:

public class Main {
   public static void main(String[] args) {
       System.out.println("main start...");
       Thread t = new Thread() {
           public void run() {
               System.out.println("thread run...");
               try {
                   Thread.sleep(10);
              } catch (InterruptedException e) {}
               System.out.println("thread end.");
          }
      };
       t.start();
       try {
           Thread.sleep(20);
      } catch (InterruptedException e) {}
       System.out.println("main end...");
  }
}

sleep()传入的参数是毫秒。调整暂停时间的大小,我们可以看到main线程和t线程执行的先后顺序。要特别注意:直接调用Thread实例的run()方法是无效的:

public class Main {
   public static void main(String[] args) {
       Thread t = new MyThread();
       t.run();
  }
}


class MyThread extends Thread {
   public void run() {
       System.out.println("hello");
  }
}

直接调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码实际上是在main()方法内部又调用了run()方法,打印hello语句是在main线程中执行的,没有任何新线程被创建。

必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

3、线程的优先级

可以对线程设定优先级,设定优先级的方法是:

Thread.setPriority(int n) // 1~10, 默认值5

JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。

三、线程的状态

JDK中用Thread.State类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:  新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态  就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源  运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能  阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU并临时中止自己的执行,进入阻塞状态  死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:

  • New:新创建的线程,尚未执行;
  • Runnable:运行中的线程,正在执行run()方法的Java代码;
  • Blocked:运行中的线程,因为某些操作被阻塞而挂起;
  • Waiting:运行中的线程,因为某些操作在等待中;
  • Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待;
  • Terminated:线程已终止,因为run()方法执行完毕。

用一个状态转移图表示如下:

当线程启动后,它可以在RunnableBlockedWaitingTimed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。

线程终止的原因有:

  • 线程正常终止:run()方法执行到return语句返回;
  • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
  • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

一个线程还可以等待另一个线程直到其运行结束。例如,main线程在启动t线程后,可以通过t.join()等待t线程结束后再继续运行:

public class Main {
   public static void main(String[] args) throws InterruptedException {
       Thread t = new Thread(() -> {
           System.out.println("hello");
      });
       System.out.println("start");
       t.start();
       t.join();
       System.out.println("end");
  }
}

main线程对线程对象t调用join()方法时,主线程将等待变量t表示的线程运行结束,即join就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是main线程先打印startt线程再打印hellomain线程最后再打印end

如果t线程已经结束,对实例t调用join()会立刻返回。此外,join(long)的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。

小结

1、Java线程对象Thread的状态包括:NewRunnableBlockedWaitingTimed WaitingTerminated

2、通过对另一个线程对象调用join()方法可以等待其执行结束;

3、可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;

4、对已经运行结束的线程调用join()方法会立刻返回。

四、中断线程

如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。

我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

我们还是看示例代码:

public class Main {
   public static void main(String[] args) throws InterruptedException {
       Thread t = new MyThread();
       t.start();
       Thread.sleep(1); // 暂停1毫秒
       t.interrupt(); // 中断t线程
       t.join(); // 等待t线程结束
       System.out.println("end");
  }
}


class MyThread extends Thread {
   public void run() {
       int n = 0;
       while (! isInterrupted()) {
           n ++;
           System.out.println(n + " hello!");
      }
  }
}

仔细看上述代码,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

如果线程处于等待状态,例如,t.join()会让main线程进入等待状态,此时,如果对main线程调用interrupt()join()方法会立刻抛出InterruptedException,因此,目标线程只要捕获到join()方法抛出的InterruptedException,就说明有其他线程对其调用了interrupt()方法,通常情况下该线程应该立刻结束运行。

我们来看下面的示例代码:

public class Main {
   public static void main(String[] args) throws InterruptedException {
       Thread t = new MyThread();
       t.start();
       Thread.sleep(1000);
       t.interrupt(); // 中断t线程
       t.join(); // 等待t线程结束
       System.out.println("end");
  }
}

class MyThread extends Thread {
   public void run() {
       Thread hello = new HelloThread();
       hello.start(); // 启动hello线程
       try {
           hello.join(); // 等待hello线程结束
      } catch (InterruptedException e) {
           System.out.println("interrupted!");
      }
       hello.interrupt();
  }
}

class HelloThread extends Thread {
   public void run() {
       int n = 0;
       while (!isInterrupted()) {
           n++;
           System.out.println(n + " hello!");
           try {
               Thread.sleep(100);
          } catch (InterruptedException e) {
               break;
          }
      }
  }
}

main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。如果去掉这一行代码,可以发现hello线程仍然会继续运行,且JVM不会退出。

另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束:

public class Main {
   public static void main(String[] args)  throws InterruptedException {
       HelloThread t = new HelloThread();
       t.start();
       Thread.sleep(1);
       t.running = false; // 标志位置为false
  }
}

class HelloThread extends Thread {
   public volatile boolean running = true;
   public void run() {
       int n = 0;
       while (running) {
           n ++;
           System.out.println(n + " hello!");
      }
       System.out.println("end!");
  }
}

注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。

为什么要对线程间共享的变量用关键字volatile声明?这涉及到Java的内存模型。在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!

这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量a = true,线程1执行a = false时,它在此刻仅仅是把变量a的副本变成了false,主内存的变量a还是true,在JVM把修改后的a回写到主内存之前,其他线程读取到的a的值仍然是true,这就造成了多线程之间共享的变量不一致。

因此,volatile关键字的目的是告诉虚拟机:

1、每次访问变量时,总是获取主内存的最新值;

2、每次修改变量后,立刻回写到主内存。

volatile关键字解决的是可见性问题:当一个线程修改了某个共享变量的值,其他线程能够立刻看到修改后的值。

如果我们去掉volatile关键字,运行上述程序,发现效果和带volatile差不多,这是因为在x86的架构下,JVM回写主内存的速度非常快,但是,换成ARM的架构,就会有显著的延迟。

小结

对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException

目标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程;

通过标志位判断需要正确使用volatile关键字;

volatile关键字解决了共享变量在线程间的可见性问题。

六、守护线程

Java程序入口就是由JVM启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束。但是有一种线程的目的就是无限循环,例如,一个定时触发任务的线程:

class TimerThread extends Thread {
   @Override
   public void run() {
       while (true) {
           System.out.println(LocalTime.now());
           try {
               Thread.sleep(1000);
          } catch (InterruptedException e) {
               break;
          }
      }
  }
}

如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?然而这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,怎么办?答案是使用守护线程(Daemon Thread)。守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。因此,JVM退出时,不必关心守护线程是否已结束。

如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

Thread t = new MyThread();
t.setDaemon(true);
t.start();

在守护线程中,编写代码要注意:守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

七、线程同步

当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。

这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。我们来看一个例子:

public class Main {
   public static void main(String[] args) throws Exception {
       var add = new AddThread();
       var dec = new DecThread();
       add.start();
       dec.start();
       add.join();
       dec.join();
       System.out.println(Counter.count);
  }
}


class Counter {
   public static int count = 0;
}


class AddThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) { Counter.count += 1; }
  }
}


class DecThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) { Counter.count -= 1; }
  }
}

上面的代码很简单,两个线程同时对一个int变量进行操作,一个加10000次,一个减10000次,最后结果应该是0,但是,每次运行,结果实际上都是不一样的。

这是因为对变量进行读取和写入时,结果要正确,必须保证是原子操作。原子操作是指不能被中断的一个或一系列操作。例如,对于语句:

n = n + 1;

看上去是一行语句,实际上对应了3条指令:

ILOAD
IADD
ISTORE

我们假设n的值是100,如果两个线程同时执行n = n + 1,得到的结果很可能不是102,而是101,原因在于:

如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102

这说明多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:

通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。

可见,保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用synchronized关键字对一个对象进行加锁:

synchronized(lock) {
   n = n + 1;
}

synchronized保证了代码块在任意时刻最多只有一个线程能执行。我们把上面的代码用synchronized改写如下:

public class Main {
   public static void main(String[] args) throws Exception {
       var add = new AddThread();
       var dec = new DecThread();
       add.start();
       dec.start();
       add.join();
       dec.join();
       System.out.println(Counter.count);
  }
}


class Counter {
   public static final Object lock = new Object();
   public static int count = 0;
}


class AddThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock) {
               Counter.count += 1;
          }
      }
  }
}


class DecThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock) {
               Counter.count -= 1;
          }
      }
  }
}

注意到代码:

synchronized(Counter.lock) { // 获取锁
  ...
} // 释放锁

它表示用Counter.lock实例作为锁,两个线程在执行各自的synchronized(Counter.lock) { ... }代码块时,必须先获得锁,才能进入代码块进行。执行结束后,在synchronized语句块结束会自动释放锁。这样一来,对Counter.count变量进行读写就不可能同时进行。上述代码无论运行多少次,最终结果都是0。

使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

我们来概括一下如何使用synchronized

  1. 找出修改共享变量的线程代码块;
  2. 选择一个共享实例作为锁;
  3. 使用synchronized(lockObject) { ... }

在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁:

public void add(int m) {
   synchronized (obj) {
       if (m < 0) {
           throw new RuntimeException();
      }
       this.value += m;
  } // 无论有无异常,都会在此释放锁
}

我们再来看一个错误使用synchronized的例子:

public class Main {
   public static void main(String[] args) throws Exception {
       var add = new AddThread();
       var dec = new DecThread();
       add.start();
       dec.start();
       add.join();
       dec.join();
       System.out.println(Counter.count);
  }
}


class Counter {
   public static final Object lock1 = new Object();
   public static final Object lock2 = new Object();
   public static int count = 0;
}


class AddThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock1) {
               Counter.count += 1;
          }
      }
  }
}


class DecThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock2) {
               Counter.count -= 1;
          }
      }
  }
}

结果并不是0,这是因为两个线程各自的synchronized锁住的不是同一个对象!这使得两个线程各自都可以同时获得锁:因为JVM只保证同一个锁在任意时刻只能被一个线程获取,但两个不同的锁在同一时刻可以被两个线程分别获取。

因此,使用synchronized的时候,获取到的是哪个锁非常重要。锁对象如果不对,代码逻辑就不对。我们再看一个例子:

public class Main {
   public static void main(String[] args) throws Exception {
       var ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };
       for (var t : ts) {
           t.start();
      }
       for (var t : ts) {
           t.join();
      }
       System.out.println(Counter.studentCount);
       System.out.println(Counter.teacherCount);
  }
}


class Counter {
   public static final Object lock = new Object();
   public static int studentCount = 0;
   public static int teacherCount = 0;
}


class AddStudentThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock) {
               Counter.studentCount += 1;
          }
      }
  }
}


class DecStudentThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock) {
               Counter.studentCount -= 1;
          }
      }
  }
}


class AddTeacherThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock) {
               Counter.teacherCount += 1;
          }
      }
  }
}


class DecTeacherThread extends Thread {
   public void run() {
       for (int i=0; i<10000; i++) {
           synchronized(Counter.lock) {
               Counter.teacherCount -= 1;
          }
      }
  }
}

上述代码的4个线程对两个共享变量分别进行读写操作,但是使用的锁都是Counter.lock这一个对象,这就造成了原本可以并发执行的Counter.studentCount += 1Counter.teacherCount += 1,现在无法并发执行了,执行效率大大降低。实际上,需要同步的线程可以分成两组:AddStudentThreadDecStudentThreadAddTeacherThreadDecTeacherThread,组之间不存在竞争,因此,应该使用两个不同的锁,即:

AddStudentThreadDecStudentThread使用lockStudent锁:

synchronized(Counter.lockStudent) {
  ...
}

AddTeacherThreadDecTeacherThread使用lockTeacher锁:

synchronized(Counter.lockTeacher) {
  ...
}

这样才能最大化地提高执行效率。

不需要synchronized的操作

JVM规范定义了几种原子操作:

  • 基本类型(longdouble除外)赋值,例如:int n = m
  • 引用类型赋值,例如:List<String> list = anotherList

longdouble是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把longdouble的赋值作为原子操作实现的。

单条原子操作的语句不需要同步。例如:

public void set(int m) {
   synchronized(lock) {
       this.value = m;
  }
}

就不需要同步。对引用也是类似。例如:

public void set(String s) {
   this.value = s;
}

上述赋值语句并不需要同步。但是,如果是多行赋值语句,就必须保证是同步操作,例如:

class Pair {
   int first;
   int last;
   public void set(int first, int last) {
       synchronized(this) {
           this.first = first;
           this.last = last;
      }
  }
}

有些时候,通过一些巧妙的转换,可以把非原子操作变为原子操作。例如,上述代码如果改造成:

class Pair {
   int[] pair;
   public void set(int first, int last) {
       int[] ps = new int[] { first, last };
       this.pair = ps;
  }
}

就不再需要同步,因为this.pair = ps是引用赋值的原子操作。而语句:

int[] ps = new int[] { first, last };

这里的ps是方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步。

小结

1、多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;

2、同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;

3、注意加锁对象必须是同一个实例;

4、对JVM定义的单个原子操作不需要同步。

八、同步方法

我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。

让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。例如,我们编写一个计数器如下:

public class Counter {
   private int count = 0;

   public void add(int n) {
       synchronized(this) {
           count += n;
      }
  }

   public void dec(int n) {
       synchronized(this) {
           count -= n;
      }
  }

   public int get() {
       return count;
  }
}

这样一来,线程调用add()dec()方法时,它不必关心同步逻辑,因为synchronized代码块在add()dec()方法内部。并且,我们注意到,synchronized锁住的对象是this,即当前实例,这又使得创建多个Counter实例的时候,它们之间互不影响,可以并发执行:

var c1 = Counter();
var c2 = Counter();

// 对c1进行操作的线程:
new Thread(() -> {
   c1.add();
}).start();

new Thread(() -> {
   c1.dec();
}).start();

// 对c2进行操作的线程:
new Thread(() -> {
   c2.add();
}).start();

new Thread(() -> {
   c2.dec();
}).start();

现在,对于Counter类,多线程可以正确调用。

如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe),上面的Counter类就是线程安全的。Java标准库的java.lang.StringBuffer也是线程安全的。

还有一些不变类,例如StringIntegerLocalDate,它们的所有成员变量都是final,多线程同时访问时只能读不能写,这些不变类也是线程安全的。

最后,类似Math这些只提供静态方法,没有成员变量的类,也是线程安全的。

除了上述几种少数情况,大部分类,例如ArrayList,都是非线程安全的类,我们不能在多线程中修改它们。但是,如果所有线程都只读取,不写入,那么ArrayList是可以安全地在线程间共享的。

我们再观察Counter的代码:

public class Counter {
   public void add(int n) {
       synchronized(this) {
           count += n;
      }
  }
  ...
}

当我们锁住的是this实例时,实际上可以用synchronized修饰这个方法。下面两种写法是等价的:

public void add(int n) {
   synchronized(this) { // 锁住this
       count += n;
  } // 解锁
}

public synchronized void add(int n) { // 锁住this
   count += n;
} // 解锁

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁。我们再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?

public synchronized static void test(int n) {
  ...
}

对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。上述synchronized static方法实际上相当于:

public class Counter {
   public static void test(int n) {
       synchronized(Counter.class) {
          ...
      }
  }
}

我们再考察Counterget()方法:

public class Counter {
   private int count;

   public int get() {
       return count;
  }
  ...
}

它没有同步,因为读一个int变量不需要同步。然而,如果我们把代码稍微改一下,返回一个包含两个int的对象:

public class Counter {
   private int first;
   private int last;

   public Pair get() {
       Pair p = new Pair();
       p.first = first;
       p.last = last;
       return p;
  }
  ...
}

就必须要同步了。

小结

1、用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this

2、通过合理的设计和数据封装可以让一个类变为“线程安全”;

3、一个类没有特殊说明,默认不是thread-safe;

4、多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。

九、死锁

Java的线程锁是可重入的锁。什么是可重入的锁?我们还是来看例子:

public class Counter {
   private int count = 0;

   public synchronized void add(int n) {
       if (n < 0) {
           dec(-n);
      } else {
           count += n;
      }
  }

   public synchronized void dec(int n) {
       count += n;
  }
}

观察synchronized修饰的add()方法,一旦线程执行到add()方法内部,说明它已经获取了当前实例的this锁。如果传入的n < 0,将在add()方法内部调用dec()方法。由于dec()方法也需要获取this锁,现在问题来了:

对同一个线程,能否在获取到锁以后继续获取同一个锁?

答案是肯定的。JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。

由于Java的线程锁是可重入锁,所以,获取锁的时候,不但要判断是否是第一次获取,还要记录这是第几次获取。每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。

死锁

一个线程可以获取一个锁后,再继续获取另一个锁。例如:

public void add(int m) {
   synchronized(lockA) { // 获得lockA的锁
       this.value += m;
       synchronized(lockB) { // 获得lockB的锁
           this.another += m;
      } // 释放lockB的锁
  } // 释放lockA的锁
}

public void dec(int m) {
   synchronized(lockB) { // 获得lockB的锁
       this.another -= m;
       synchronized(lockA) { // 获得lockA的锁
           this.value -= m;
      } // 释放lockA的锁
  } // 释放lockB的锁
}

在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()dec()方法时:

  • 线程1:进入add(),获得lockA
  • 线程2:进入dec(),获得lockB

随后:

  • 线程1:准备获得lockB,失败,等待中;
  • 线程2:准备获得lockA,失败,等待中。

此时,两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去,这就是死锁。

死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

因此,在编写多线程应用时,要特别注意防止死锁。因为死锁一旦形成,就只能强制结束进程。

那么我们应该如何避免死锁呢?答案是:线程获取锁的顺序要一致。即严格按照先获取lockA,再获取lockB的顺序,改写dec()方法如下:

public void dec(int m) {
   synchronized(lockA) { // 获得lockA的锁
       this.value -= m;
       synchronized(lockB) { // 获得lockB的锁
           this.another -= m;
      } // 释放lockB的锁
  } // 释放lockA的锁
}

十、wait和notify

在Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:

class TaskQueue {
   Queue<String> queue = new LinkedList<>();

   public synchronized void addTask(String s) {
       this.queue.add(s);
  }
}

但是synchronized并没有解决多线程协调的问题。仍然以上面的TaskQueue为例,我们再编写一个getTask()方法取出队列的第一个任务:

class TaskQueue {
   Queue<String> queue = new LinkedList<>();

   public synchronized void addTask(String s) {
       this.queue.add(s);
  }

   public synchronized String getTask() {
       while (queue.isEmpty()) {
      }
       return queue.remove();
  }
}

上述代码看上去没有问题:getTask()内部先判断队列是否为空,如果为空,就循环等待,直到另一个线程往队列中放入了一个任务,while()循环退出,就可以返回队列的元素了。

但实际上while()循环永远不会退出。因为线程在执行while()循环时,已经在getTask()入口获取了this锁,其他线程根本无法调用addTask(),因为addTask()执行条件也是获取this锁。

因此,执行上述代码,线程会在getTask()中因为死循环而100%占用CPU资源。

如果深入思考一下,我们想要的执行效果是:

  • 线程1可以调用addTask()不断往队列中添加任务;
  • 线程2可以调用getTask()从队列中获取任务。如果队列为空,则getTask()应该等待,直到队列中至少有一个任务时再返回。

因此,多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。

对于上述TaskQueue,我们先改造getTask()方法,在条件不满足时,线程进入等待状态:

public synchronized String getTask() {
   while (queue.isEmpty()) {
       this.wait();
  }
   return queue.remove();
}

当一个线程执行到getTask()方法内部的while循环时,它必定已经获取到了this锁,此时,线程执行while条件判断,如果条件成立(队列为空),线程将执行this.wait(),进入等待状态。

这里的关键是:wait()方法必须在当前获取的锁对象上调用,这里获取的是this锁,因此调用this.wait()

调用wait()方法后,线程进入等待状态,wait()方法不会返回,直到将来某个时刻,线程从等待状态被其他线程唤醒后,wait()方法才会返回,然后,继续执行下一条语句。

有些仔细的童鞋会指出:即使线程在getTask()内部等待,其他线程如果拿不到this锁,照样无法执行addTask(),肿么办?

这个问题的关键就在于wait()方法的执行机制非常复杂。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。

因此,只能在锁对象上调用wait()方法。因为在getTask()中,我们获得了this锁,因此,只能在this对象上调用wait()方法:

public synchronized String getTask() {
   while (queue.isEmpty()) {
       // 释放this锁:
       this.wait();
       // 重新获取this锁
  }
   return queue.remove();
}

当一个线程在this.wait()等待时,它就会释放this锁,从而使得其他线程能够在addTask()方法获得this锁。

现在我们面临第二个问题:如何让等待的线程被重新唤醒,然后从wait()方法返回?答案是在相同的锁对象上调用notify()方法。我们修改addTask()如下:

public synchronized void addTask(String s) {
   this.queue.add(s);
   this.notify(); // 唤醒在this锁等待的线程
}

注意到在往队列中添加了任务后,线程立刻对this锁对象调用notify()方法,这个方法会唤醒一个正在this锁等待的线程(就是在getTask()中位于this.wait()的线程),从而使得等待线程从this.wait()方法返回。

我们来看一个完整的例子:

import java.util.*;  

public class Main {
   public static void main(String[] args) throws InterruptedException {
       var q = new TaskQueue();
       var ts = new ArrayList<Thread>();
       for (int i=0; i<5; i++) {
           var t = new Thread() {
               public void run() {
                   // 执行task:
                   while (true) {
                       try {
                           String s = q.getTask();
                           System.out.println("execute task: " + s);
                      } catch (InterruptedException e) {
                           return;
                      }
                  }
              }
          };
           t.start();
           ts.add(t);
      }
       var add = new Thread(() -> {
           for (int i=0; i<10; i++) {
               // 放入task:
               String s = "t-" + Math.random();
               System.out.println("add task: " + s);
               q.addTask(s);
               try { Thread.sleep(100); } catch(InterruptedException e) {}
          }
      });
       add.start();
       add.join();
       Thread.sleep(100);
       for (var t : ts) {
           t.interrupt();
      }
  }
}

class TaskQueue {
   Queue<String> queue = new LinkedList<>();

   public synchronized void addTask(String s) {
       this.queue.add(s);
       this.notifyAll();
  }

   public synchronized String getTask() throws InterruptedException {
       while (queue.isEmpty()) {
           this.wait();
      }
       return queue.remove();
  }
}

这个例子中,我们重点关注addTask()方法,内部调用了this.notifyAll()而不是this.notify(),使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在getTask()方法内部的wait()中等待,使用notifyAll()将一次性全部唤醒。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。

但是,注意到wait()方法返回时需要重新获得this锁。假设当前有3个线程被唤醒,唤醒后,首先要等待执行addTask()的线程结束此方法后,才能释放this锁,随后,这3个线程中只能有一个获取到this锁,剩下两个将继续等待。

再注意到我们在while()循环中调用wait(),而不是if语句:

public synchronized String getTask() throws InterruptedException {
   if (queue.isEmpty()) {
       this.wait();
  }
   return queue.remove();
}

这种写法实际上是错误的,因为线程被唤醒时,需要再次获取this锁。多个线程被唤醒后,只有一个线程能获取this锁,此刻,该线程执行queue.remove()可以获取到队列的元素,然而,剩下的线程如果获取this锁后执行queue.remove(),此刻队列可能已经没有任何元素了,所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断:

while (queue.isEmpty()) {
   this.wait();
}

所以,正确编写多线程代码是非常困难的,需要仔细考虑的条件非常多,任何一个地方考虑不周,都会导致多线程运行时不正常。

小结

waitnotify用于多线程协调运行:

  • synchronized内部可以调用wait()使线程进入等待状态;
  • 必须在已获得的锁对象上调用wait()方法;
  • synchronized内部可以调用notify()notifyAll()唤醒其他等待线程;
  • 必须在已获得的锁对象上调用notify()notifyAll()方法;
  • 已唤醒的线程还需要重新获得锁后才能继续执行。

十一、ReentrantLock

从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。

我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。

java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁,我们来看一下传统的synchronized代码:

public class Counter {
   private int count;

   public void add(int n) {
       synchronized(this) {
           count += n;
      }
  }
}

如果用ReentrantLock替代,可以把代码改造为:

public class Counter {
   private final Lock lock = new ReentrantLock();
   private int count;

   public void add(int n) {
       lock.lock();
       try {
           count += n;
      } finally {
           lock.unlock();
      }
  }
}

因为synchronized是Java语言层面提供的语法,所以我们不需要考虑异常,而ReentrantLock是Java代码实现的锁,我们就必须先获取锁,然后在finally中正确释放锁。

顾名思义,ReentrantLock是可重入锁,它和synchronized一样,一个线程可以多次获取同一个锁。和synchronized不同的是,ReentrantLock可以尝试获取锁:

if (lock.tryLock(1, TimeUnit.SECONDS)) {
   try {
      ...
  } finally {
       lock.unlock();
  }
}

上述代码在尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false,程序就可以做一些额外处理,而不是无限等待下去。

所以,使用ReentrantLock比直接使用synchronized更安全,线程在tryLock()失败的时候不会导致死锁。

小结

1、ReentrantLock可以替代synchronized进行同步;

2、ReentrantLock获取锁更安全;

3、必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁;

4、可以使用tryLock()尝试获取锁。

十二、Condition

使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。

但是,synchronized可以配合waitnotify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写waitnotify的功能呢?

答案是使用Condition对象来实现waitnotify的功能。

我们仍然以TaskQueue为例,把前面用synchronized实现的功能通过ReentrantLockCondition来实现:

class TaskQueue {
   private final Lock lock = new ReentrantLock();
   private final Condition condition = lock.newCondition();
   private Queue<String> queue = new LinkedList<>();

   public void addTask(String s) {
       lock.lock();
       try {
           queue.add(s);
           condition.signalAll();
      } finally {
           lock.unlock();
      }
  }

   public String getTask() {
       lock.lock();
       try {
           while (queue.isEmpty()) {
               condition.await();
          }
           return queue.remove();
      } finally {
           lock.unlock();
      }
  }
}

可见,使用Condition时,引用的Condition对象必须从Lock实例的newCondition()返回,这样才能获得一个绑定了Lock实例的Condition实例。

Condition提供的await()signal()signalAll()原理和synchronized锁对象的wait()notify()notifyAll()是一致的,并且其行为也是一样的:

  • await()会释放当前锁,进入等待状态;
  • signal()会唤醒某个等待线程;
  • signalAll()会唤醒所有等待线程;
  • 唤醒线程从await()返回后需要重新获得锁。

此外,和tryLock()类似,await()可以在等待指定时间后,如果还没有被其他线程通过signal()signalAll()唤醒,可以自己醒来:

if (condition.await(1, TimeUnit.SECOND)) {
   // 被其他线程唤醒
} else {
   // 指定时间内没有被其他线程唤醒
}

可见,使用Condition配合Lock,我们可以实现更灵活的线程同步。

小结

1、Condition可以替代waitnotify

2、Condition对象必须从Lock对象获取。

十三、ReadWriteLock

前面讲到的ReentrantLock保证了只有一个线程可以执行临界区代码:

public class Counter {
   private final Lock lock = new ReentrantLock();
   private int[] counts = new int[10];

   public void inc(int index) {
       lock.lock();
       try {
           counts[index] += 1;
      } finally {
           lock.unlock();
      }
  }

   public int[] get() {
       lock.lock();
       try {
           return Arrays.copyOf(counts, counts.length);
      } finally {
           lock.unlock();
      }
  }
}

但是有些时候,这种保护有点过头。因为我们发现,任何时刻,只允许一个线程修改,也就是调用inc()方法是必须获取锁,但是,get()方法只读取数据,不修改数据,它实际上允许多个线程同时调用。

实际上我们想要的是:允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待:

允许

不允许

不允许

不允许

使用ReadWriteLock可以解决这个问题,它保证:

  • 只允许一个线程写入(其他线程既不能写入也不能读取);
  • 没有写入时,多个线程允许同时读(提高性能)。

ReadWriteLock实现这个功能十分容易。我们需要创建一个ReadWriteLock实例,然后分别获取读锁和写锁:

public class Counter {
   private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
   private final Lock rlock = rwlock.readLock();
   private final Lock wlock = rwlock.writeLock();
   private int[] counts = new int[10];

   public void inc(int index) {
       wlock.lock(); // 加写锁
       try {
           counts[index] += 1;
      } finally {
           wlock.unlock(); // 释放写锁
      }
  }

   public int[] get() {
       rlock.lock(); // 加读锁
       try {
           return Arrays.copyOf(counts, counts.length);
      } finally {
           rlock.unlock(); // 释放读锁
      }
  }
}

把读写操作分别用读锁和写锁来加锁,在读取时,多个线程可以同时获得读锁,这样就大大提高了并发读的执行效率。

使用ReadWriteLock时,适用条件是同一个数据,有大量线程读取,但仅有少数线程修改。

例如,一个论坛的帖子,回复可以看做写入操作,它是不频繁的,但是,浏览可以看做读取操作,是非常频繁的,这种情况就可以使用ReadWriteLock

小结

使用ReadWriteLock可以提高读取效率:

  • ReadWriteLock只允许一个线程写入;
  • ReadWriteLock允许多个线程在没有写入时同时读取;
  • ReadWriteLock适合读多写少的场景。

十四、StampedLock

前面介绍的ReadWriteLock可以解决多线程同时读,但只有一个线程能写的问题。

如果我们深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。

要进一步提升并发执行效率,Java 8引入了新的读写锁:StampedLock

StampedLockReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入!这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁。

乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

我们来看例子:

public class Point {
   private final StampedLock stampedLock = new StampedLock();

   private double x;
   private double y;

   public void move(double deltaX, double deltaY) {
       long stamp = stampedLock.writeLock(); // 获取写锁
       try {
           x += deltaX;
           y += deltaY;
      } finally {
           stampedLock.unlockWrite(stamp); // 释放写锁
      }
  }

   public double distanceFromOrigin() {
       long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
       // 注意下面两行代码不是原子操作
       // 假设x,y = (100,200)
       double currentX = x;
       // 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
       double currentY = y;
       // 此处已读取到y,如果没有写入,读取是正确的(100,200)
       // 如果有写入,读取是错误的(100,400)
       if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
           stamp = stampedLock.readLock(); // 获取一个悲观读锁
           try {
               currentX = x;
               currentY = y;
          } finally {
               stampedLock.unlockRead(stamp); // 释放悲观读锁
          }
      }
       return Math.sqrt(currentX * currentX + currentY * currentY);
  }
}

ReadWriteLock相比,写入的加锁是完全一样的,不同的是读取。注意到首先我们通过tryOptimisticRead()获取一个乐观读锁,并返回版本号。接着进行读取,读取完成后,我们通过validate()去验证版本号,如果在读取过程中没有写入,版本号不变,验证成功,我们就可以放心地继续后续操作。如果在读取过程中有写入,版本号会发生变化,验证将失败。在失败的时候,我们再通过获取悲观读锁再次读取。由于写入的概率不高,程序在绝大部分情况下可以通过乐观读锁获取数据,极少数情况下使用悲观读锁获取数据。

可见,StampedLock把读锁细分为乐观读和悲观读,能进一步提升并发效率。但这也是有代价的:一是代码更加复杂,二是StampedLock是不可重入锁,不能在一个线程中反复获取同一个锁。

StampedLock还提供了更复杂的将悲观读锁升级为写锁的功能,它主要使用在if-then-update的场景:即先读,如果读的数据满足条件,就返回,如果读的数据不满足条件,再尝试写。

小结

1、StampedLock提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;

2、StampedLock是不可重入锁。

十五、Semaphore

前面我们讲了各种锁的实现,本质上锁的目的是保护一种受限资源,保证同一时刻只有一个线程能访问(ReentrantLock),或者只有一个线程能写入(ReadWriteLock)。

还有一种受限资源,它需要保证同一时刻最多有N个线程能访问,比如同一时刻最多创建100个数据库连接,最多允许10个用户下载等。

这种限制数量的锁,如果用Lock数组来实现,就太麻烦了。

这种情况就可以使用Semaphore,例如,最多允许3个线程同时访问:

public class AccessLimitControl {
   // 任意时刻仅允许最多3个线程获取许可:
   final Semaphore semaphore = new Semaphore(3);

   public String access() throws Exception {
       // 如果超过了许可数量,其他线程将在此等待:
       semaphore.acquire();
       try {
           // TODO:
           return UUID.randomUUID().toString();
      } finally {
           semaphore.release();
      }
  }
}

使用Semaphore先调用acquire()获取,然后通过try ... finally保证在finally中释放。

调用acquire()可能会进入等待,直到满足条件为止。也可以使用tryAcquire()指定等待时间:

if (semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
   // 指定等待时间3秒内获取到许可:
   try {
       // TODO:
  } finally {
       semaphore.release();
  }
}

Semaphore本质上就是一个信号计数器,用于限制同一时间的最大访问数量。

小结

如果要对某一受限资源进行限流访问,可以使用Semaphore,保证同一时间最多N个线程访问受限资源。

十六、Concurrent集合

我们在前面已经通过ReentrantLockCondition实现了一个BlockingQueue

public class TaskQueue {
   private final Lock lock = new ReentrantLock();
   private final Condition condition = lock.newCondition();
   private Queue<String> queue = new LinkedList<>();

   public void addTask(String s) {
       lock.lock();
       try {
           queue.add(s);
           condition.signalAll();
      } finally {
           lock.unlock();
      }
  }

   public String getTask() {
       lock.lock();
       try {
           while (queue.isEmpty()) {
               condition.await();
          }
           return queue.remove();
      } finally {
           lock.unlock();
      }
  }
}

BlockingQueue的意思就是说,当一个线程调用这个TaskQueuegetTask()方法时,该方法内部可能会让线程变成等待状态,直到队列条件满足不为空,线程被唤醒后,getTask()方法才会返回。

因为BlockingQueue非常有用,所以我们不必自己编写,可以直接使用Java标准库的java.util.concurrent包提供的线程安全的集合:ArrayBlockingQueue

除了BlockingQueue外,针对ListMapSetDeque等,java.util.concurrent包也提供了对应的并发集合类。我们归纳一下:

interface

non-thread-safe

thread-safe

List

ArrayList

CopyOnWriteArrayList

Map

HashMap

ConcurrentHashMap

Set

HashSet / TreeSet

CopyOnWriteArraySet

Queue

ArrayDeque / LinkedList

ArrayBlockingQueue / LinkedBlockingQueue

Deque

ArrayDeque / LinkedList

LinkedBlockingDeque

使用这些并发集合与使用非线程安全的集合类完全相同。我们以ConcurrentHashMap为例:

Map<String, String> map = new ConcurrentHashMap<>();
// 在不同的线程读写:
map.put("A", "1");
map.put("B", "2");
map.get("A", "1");

因为所有的同步和加锁的逻辑都在集合内部实现,对外部调用者来说,只需要正常按接口引用,其他代码和原来的非线程安全代码完全一样。即当我们需要多线程访问时,把:

Map<String, String> map = new HashMap<>();

改为:

Map<String, String> map = new ConcurrentHashMap<>();

就可以了。

java.util.Collections工具类还提供了一个旧的线程安全集合转换器,可以这么用:

Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);

但是它实际上是用一个包装类包装了非线程安全的Map,然后对所有读写方法都用synchronized加锁,这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用。

小结

使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程:

多线程同时读写并发集合是安全的;

尽量使用Java标准库提供的并发集合,避免自己编写同步代码。

十七、Atomic

Java的java.util.concurrent包除了提供底层锁、并发集合外,还提供了一组原子操作的封装类,它们位于java.util.concurrent.atomic包。

我们以AtomicInteger为例,它提供的主要操作有:

  • 增加值并返回新值:int addAndGet(int delta)
  • 加1后返回新值:int incrementAndGet()
  • 获取当前值:int get()
  • 用CAS方式设置:int compareAndSet(int expect, int update)

Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。

如果我们自己通过CAS编写incrementAndGet(),它大概长这样:

public int incrementAndGet(AtomicInteger var) {
  int prev, next;
  do {
      prev = var.get();
      next = prev + 1;
  } while ( ! var.compareAndSet(prev, next));
  return next;
}

CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。

我们利用AtomicLong可以编写一个多线程安全的全局唯一ID生成器:

class IdGenerator {
  AtomicLong var = new AtomicLong(0);

  public long getNextId() {
      return var.incrementAndGet();
  }
}

通常情况下,我们并不需要直接用do ... while循环调用compareAndSet实现复杂的并发操作,而是用incrementAndGet()这样的封装好的方法,因此,使用起来非常简单。

在高度竞争的情况下,还可以使用Java 8提供的LongAdderLongAccumulator

小结

使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:

  • 原子操作实现了无锁的线程安全;
  • 适用于计数器,累加器等。

十八、线程池

Java语言虽然内置了多线程支持,启动一个新线程非常方便,但是,创建线程需要操作系统资源(线程资源,栈空间等),频繁创建和销毁大量线程需要消耗大量时间。

如果可以复用一组线程:

那么我们就可以把很多小任务让一组线程来执行,而不是一个任务对应一个新线程。这种能接收大量小任务并进行分发处理的就是线程池。

简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理。

Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:

// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);

因为ExecutorService只是接口,Java标准库提供的几个常用实现类有:

  • FixedThreadPool:线程数固定的线程池;
  • CachedThreadPool:线程数根据任务动态调整的线程池;
  • SingleThreadExecutor:仅单线程执行的线程池。

创建这些线程池的方法都被封装到Executors这个类中。我们以FixedThreadPool为例,看看线程池的执行逻辑:

import java.util.concurrent.*;

public class Main {
   public static void main(String[] args) {
       // 创建一个固定大小的线程池:
       ExecutorService es = Executors.newFixedThreadPool(4);
       for (int i = 0; i < 6; i++) {
           es.submit(new Task("" + i));
      }
       // 关闭线程池:
       es.shutdown();
  }
}

class Task implements Runnable {
   private final String name;

   public Task(String name) {
       this.name = name;
  }

   @Override
   public void run() {
       System.out.println("start task " + name);
       try {
           Thread.sleep(1000);
      } catch (InterruptedException e) {
      }
       System.out.println("end task " + name);
  }
}

我们观察执行结果,一次性放入6个任务,由于线程池只有固定的4个线程,因此,前4个任务会同时执行,等到有线程空闲后,才会执行后面的两个任务。

线程池在程序结束的时候要关闭。使用shutdown()方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow()会立刻停止正在执行的任务,awaitTermination()则会等待指定的时间让线程池关闭。

如果我们把线程池改为CachedThreadPool,由于这个线程池的实现会根据任务数量动态调整线程池的大小,所以6个任务可一次性全部同时执行。

如果我们想把线程池的大小限制在4~10个之间动态调整怎么办?我们查看Executors.newCachedThreadPool()方法的源码:

public static ExecutorService newCachedThreadPool() {
   return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                   60L, TimeUnit.SECONDS,
                                   new SynchronousQueue<Runnable>());
}

因此,想创建指定动态范围的线程池,可以这么写:

int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(min, max,
       60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());

ScheduledThreadPool

还有一种任务,需要定期反复执行,例如,每秒刷新证券价格。这种任务本身固定,需要反复执行的,可以使用ScheduledThreadPool。放入ScheduledThreadPool的任务可以定期反复执行。

创建一个ScheduledThreadPool仍然是通过Executors类:

ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);

我们可以提交一次性任务,它会在指定延迟后只执行一次:

// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);

如果任务以固定的每3秒执行,我们可以这样写:

// 2秒后开始执行定时任务,每3秒执行:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);

如果任务以固定的3秒为间隔执行,我们可以这样写:

// 2秒后开始执行定时任务,以3秒为间隔执行:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);

注意FixedRate和FixedDelay的区别。FixedRate是指任务总是以固定时间间隔触发,不管任务执行多长时间:

而FixedDelay是指,上一次任务执行完毕后,等待固定的时间间隔,再执行下一次任务:

因此,使用ScheduledThreadPool时,我们要根据需要选择执行一次、FixedRate执行还是FixedDelay执行。

细心的童鞋还可以思考下面的问题:

1、在FixedRate模式下,假设每秒触发,如果某次任务执行时间超过1秒,后续任务会不会并发执行?

2、如果任务抛出了异常,后续任务是否继续执行?

Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer

一、Java 网络编程

网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。

java.net 包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP:TCP(英语:Transmission Control Protocol,传输控制协议) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP 层是位于 IP 层之上,应用层之下的中间层。TCP 保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。
  • UDP:UDP (英语:User Datagram Protocol,用户数据报协议),位于 OSI 模型的传输层。一个无连接的协议。提供了应用程序之间要发送数据的数据报。由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。

1、Socket 编程

套接字使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。

以下步骤在两台计算机之间使用套接字建立TCP连接时会出现:  1、服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。  2、服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。  3、服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。  4、Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够 与服务器进行通信。  5、在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket。

连接建立后,通过使用 I/O 流在进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。

TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送.以下是一些类提供的一套完整的有用的方法来实现 socket。

Java的网络编程主要涉及到的内容是Socket编程。Socket,套接字,就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

Socket,实际上是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的关系,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现,只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:

“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”

实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。socket是对端口通信开发的工具,它要更底层一些。

2、Socket整体流程

Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。

客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。

2、ServerSocket 类的方法

服务器应用程序通过使用 java.net.ServerSocket 类以获取一个端口,并且侦听客户端请求。ServerSocket 类有四个构造方法:

方法

方法描述

public ServerSocket(int port) throws IOException

创建绑定到特定端口的服务器套接字。

public ServerSocket(int port, int backlog) throws IOException

利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。

public ServerSocket(int port, int backlog, InetAddress address) throws IOException

使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。

public ServerSocket() throws IOException

创建非绑定服务器套接字。

创建非绑定服务器套接字。 如果 ServerSocket 构造方法没有抛出异常,就意味着你的应用程序已经成功绑定到指定的端口,并且侦听客户端请求。

这里有一些 ServerSocket 类的常用方法:

方法

方法描述

public int getLocalPort()

返回此套接字在其上侦听的端口。

public Socket accept() throws IOException

侦听并接受到此套接字的连接。

public void setSoTimeout(int timeout)

通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。

public void bind(SocketAddress host, int backlog)

将 ServerSocket 绑定到特定地址(IP 地址和端口号)。


3、Socket 类的方法

java.net.Socket 类代表客户端和服务器都用来互相沟通的套接字。客户端要获取一个 Socket 对象通过实例化 ,而 服务器获得一个 Socket 对象则通过 accept() 方法的返回值。

Socket 类有五个构造方法.

方法

方法描述

public Socket(String host, int port) throws UnknownHostException, IOException.

创建一个流套接字并将其连接到指定主机上的指定端口号。

public Socket(InetAddress host, int port) throws IOException

创建一个流套接字并将其连接到指定 IP 地址的指定端口号。

public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException.

创建一个套接字并将其连接到指定远程主机上的指定远程端口。

public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException.

创建一个套接字并将其连接到指定远程地址上的指定远程端口。

public Socket()

通过系统默认类型的 SocketImpl 创建未连接套接字

当 Socket 构造方法返回,并没有简单的实例化了一个 Socket 对象,它实际上会尝试连接到指定的服务器和端口。

下面列出了一些感兴趣的方法,注意客户端和服务器端都有一个 Socket 对象,所以无论客户端还是服务端都能够调用这些方法。

方法

方法描述

public void connect(SocketAddress host, int timeout) throws IOException

将此套接字连接到服务器,并指定一个超时值。

public InetAddress getInetAddress()

返回套接字连接的地址。

public int getPort()

返回此套接字连接到的远程端口。

public int getLocalPort()

返回此套接字绑定到的本地端口。

public SocketAddress getRemoteSocketAddress()

返回此套接字连接的端点的地址,如果未连接则返回 null。

public InputStream getInputStream() throws IOException

返回此套接字的输入流。

public OutputStream getOutputStream() throws IOException

返回此套接字的输出流。

public void close() throws IOException

关闭此套接字。


4、InetAddress 类的方法

这个类表示互联网协议(IP)地址。下面列出了 Socket 编程时比较有用的方法:

方法

方法描述

static InetAddress getByAddress(byte[] addr)

在给定原始 IP 地址的情况下,返回 InetAddress 对象。

static InetAddress getByAddress(String host, byte[] addr)

根据提供的主机名和 IP 地址创建 InetAddress。

static InetAddress getByName(String host)

在给定主机名的情况下确定主机的 IP 地址。

String getHostAddress()

返回 IP 地址字符串(以文本表现形式)。

String getHostName()

获取此 IP 地址的主机名。

static InetAddress getLocalHost()

返回本地主机。

String toString()

将此 IP 地址转换为 String。


5、Socket 客户端实例

如下的 GreetingClient 是一个客户端程序,该程序通过 socket 连接到服务器并发送一个请求,然后等待一个响应。

import java.net.*;
import java.io.*;

public class GreetingClient
{
  public static void main(String [] args)
  {
     String serverName = args[0];
     int port = Integer.parseInt(args[1]);
     try
    {
        System.out.println("连接到主机:" + serverName + " ,端口号:" + port);
        Socket client = new Socket(serverName, port);
        System.out.println("远程主机地址:" + client.getRemoteSocketAddress());
        OutputStream outToServer = client.getOutputStream();
        DataOutputStream out = new DataOutputStream(outToServer);

        out.writeUTF("Hello from " + client.getLocalSocketAddress());
        InputStream inFromServer = client.getInputStream();
        DataInputStream in = new DataInputStream(inFromServer);
        System.out.println("服务器响应: " + in.readUTF());
        client.close();
    }catch(IOException e)
    {
        e.printStackTrace();
    }
  }
}

6、Socket 服务端实例

如下的GreetingServer 程序是一个服务器端应用程序,使用 Socket 来监听一个指定的端口。

import java.net.*;
import java.io.*;

public class GreetingServer extends Thread
{
  private ServerSocket serverSocket;
 
  public GreetingServer(int port) throws IOException
  {
     serverSocket = new ServerSocket(port);
     serverSocket.setSoTimeout(10000);
  }

  public void run()
  {
     while(true)
    {
        try
        {
           System.out.println("等待远程连接,端口号为:" + serverSocket.getLocalPort() + "...");
           Socket server = serverSocket.accept();
           System.out.println("远程主机地址:" + server.getRemoteSocketAddress());
           DataInputStream in = new DataInputStream(server.getInputStream());
           System.out.println(in.readUTF());
           DataOutputStream out = new DataOutputStream(server.getOutputStream());
           out.writeUTF("谢谢连接我:" + server.getLocalSocketAddress() + "\nGoodbye!");
           server.close();
        }catch(SocketTimeoutException s)
        {
           System.out.println("Socket timed out!");
           break;
        }catch(IOException e)
        {
           e.printStackTrace();
           break;
        }
    }
  }
  public static void main(String [] args)
  {
     int port = Integer.parseInt(args[0]);
     try
    {
        Thread t = new GreetingServer(port);
        t.run();
    }catch(IOException e)
    {
        e.printStackTrace();
    }
  }
}

一、Java 网络编程

网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。

java.net 包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

java.net 包中提供了两种常见的网络协议的支持:

TCP:TCP(英语:Transmission Control Protocol,传输控制协议) 是一种面向连接的、可靠的、基于字节流的传输层通信协议,TCP 层是位于 IP 层之上,应用层之下的中间层。TCP 保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP / IP。

UDP:UDP (英语:User Datagram Protocol,用户数据报协议),位于 OSI 模型的传输层。一个无连接的协议。提供了应用程序之间要发送数据的数据报。由于UDP缺乏可靠性且属于无连接协议,所以应用程序通常必须容许一些丢失、错误或重复的数据包。

1、Socket 编程

套接字使用TCP提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。当连接建立时,服务器会创建一个 Socket 对象。客户端和服务器现在可以通过对 Socket 对象的写入和读取来进行通信。java.net.Socket 类代表一个套接字,并且 java.net.ServerSocket 类为服务器程序提供了一种来监听客户端,并与他们建立连接的机制。

以下步骤在两台计算机之间使用套接字建立TCP连接时会出现: ​ 1、服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。 ​ 2、服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。 ​ 3、服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。 ​ 4Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够 与服务器进行通信。 ​ 5、在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket

连接建立后,通过使用 I/O 流在进行通信,每一个socket都有一个输出流和一个输入流,客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。

TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送.以下是一些类提供的一套完整的有用的方法来实现 socket。

Java的网络编程主要涉及到的内容是Socket编程。Socket,套接字,就是两台主机之间逻辑连接的端点。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。

应用层通过传输层进行数据通信时,TCP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了套接字(Socket)接口。应用层可以和传输层通过Socket接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

Socket,实际上是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。实际上,Socket跟TCP/IP协议没有必然的关系,Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以说,Socket的出现,只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口,比如create、listen、accept、send、read和write等等。网络有一段关于socket和TCP/IP协议关系的说法比较容易理解:

“TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。这个就像操作系统会提供标准的编程接口,比如win32编程接口一样,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。”

实际上,传输层的TCP是基于网络层的IP协议的,而应用层的HTTP协议又是基于传输层的TCP协议的,而Socket本身不算是协议,就像上面所说,它只是提供了一个针对TCP或者UDP编程的接口。socket是对端口通信开发的工具,它要更底层一些。

2、Socket整体流程

Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,我们可以选择任意一个当前没有被其他进程使用的端口。

客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。

2、ServerSocket 类的方法

服务器应用程序通过使用 java.net.ServerSocket 类以获取一个端口,并且侦听客户端请求。ServerSocket 类有四个构造方法:

方法 方法描述

public ServerSocket(int port) throws IOException 创建绑定到特定端口的服务器套接字。

public ServerSocket(int port, int backlog) throws IOException 利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。

public ServerSocket(int port, int backlog, InetAddress address) throws IOException 使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。

public ServerSocket() throws IOException 创建非绑定服务器套接字。

创建非绑定服务器套接字。 如果 ServerSocket 构造方法没有抛出异常,就意味着你的应用程序已经成功绑定到指定的端口,并且侦听客户端请求。

这里有一些 ServerSocket 类的常用方法:

方法 方法描述

public int getLocalPort() 返回此套接字在其上侦听的端口。

public Socket accept() throws IOException 侦听并接受到此套接字的连接。

public void setSoTimeout(int timeout) 通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。

public void bind(SocketAddress host, int backlog) 将 ServerSocket 绑定到特定地址(IP 地址和端口号)。

3、Socket 类的方法

java.net.Socket 类代表客户端和服务器都用来互相沟通的套接字。客户端要获取一个 Socket 对象通过实例化 ,而 服务器获得一个 Socket 对象则通过 accept() 方法的返回值。

Socket 类有五个构造方法.

方法 方法描述

public Socket(String host, int port) throws UnknownHostException, IOException. 创建一个流套接字并将其连接到指定主机上的指定端口号。

public Socket(InetAddress host, int port) throws IOException 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。

public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException. 创建一个套接字并将其连接到指定远程主机上的指定远程端口。

public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException. 创建一个套接字并将其连接到指定远程地址上的指定远程端口。

public Socket() 通过系统默认类型的 SocketImpl 创建未连接套接字

当 Socket 构造方法返回,并没有简单的实例化了一个 Socket 对象,它实际上会尝试连接到指定的服务器和端口。

下面列出了一些感兴趣的方法,注意客户端和服务器端都有一个 Socket 对象,所以无论客户端还是服务端都能够调用这些方法。

方法 方法描述

public void connect(SocketAddress host, int timeout) throws IOException 将此套接字连接到服务器,并指定一个超时值。

public InetAddress getInetAddress() 返回套接字连接的地址。

public int getPort() 返回此套接字连接到的远程端口。

public int getLocalPort() 返回此套接字绑定到的本地端口。

public SocketAddress getRemoteSocketAddress() 返回此套接字连接的端点的地址,如果未连接则返回 null。

public InputStream getInputStream() throws IOException 返回此套接字的输入流。

public OutputStream getOutputStream() throws IOException 返回此套接字的输出流。

public void close() throws IOException 关闭此套接字。

4、InetAddress 类的方法

这个类表示互联网协议(IP)地址。下面列出了 Socket 编程时比较有用的方法:

方法 方法描述

static InetAddress getByAddress(byte[] addr) 在给定原始 IP 地址的情况下,返回 InetAddress 对象。

static InetAddress getByAddress(String host, byte[] addr) 根据提供的主机名和 IP 地址创建 InetAddress。

static InetAddress getByName(String host) 在给定主机名的情况下确定主机的 IP 地址。

String getHostAddress() 返回 IP 地址字符串(以文本表现形式)。

String getHostName() 获取此 IP 地址的主机名。

static InetAddress getLocalHost() 返回本地主机。

String toString() 将此 IP 地址转换为 String。

5、Socket 客户端实例

如下的 GreetingClient 是一个客户端程序,该程序通过 socket 连接到服务器并发送一个请求,然后等待一个响应。

import java.net.*;

import java.io.*;

public class GreetingClient

{

   public static void main(String [] args)

   {

      String serverName = args[0];

      int port = Integer.parseInt(args[1]);

      try

      {

         System.out.println("连接到主机:" + serverName + " ,端口号:" + port);

         Socket client = new Socket(serverName, port);

         System.out.println("远程主机地址:" + client.getRemoteSocketAddress());

         OutputStream outToServer = client.getOutputStream();

         DataOutputStream out = new DataOutputStream(outToServer);

         out.writeUTF("Hello from " + client.getLocalSocketAddress());

         InputStream inFromServer = client.getInputStream();

         DataInputStream in = new DataInputStream(inFromServer);

         System.out.println("服务器响应: " + in.readUTF());

         client.close();

      }catch(IOException e)

      {

         e.printStackTrace();

      }

   }

}

6、Socket 服务端实例

如下的GreetingServer 程序是一个服务器端应用程序,使用 Socket 来监听一个指定的端口。

import java.net.*;

import java.io.*;

public class GreetingServer extends Thread

{

   private ServerSocket serverSocket;

   

   public GreetingServer(int port) throws IOException

   {

      serverSocket = new ServerSocket(port);

      serverSocket.setSoTimeout(10000);

   }

   public void run()

   {

      while(true)

      {

         try

         {

            System.out.println("等待远程连接,端口号为:" + serverSocket.getLocalPort() + "...");

            Socket server = serverSocket.accept();

            System.out.println("远程主机地址:" + server.getRemoteSocketAddress());

            DataInputStream in = new DataInputStream(server.getInputStream());

            System.out.println(in.readUTF());

            DataOutputStream out = new DataOutputStream(server.getOutputStream());

            out.writeUTF("谢谢连接我:" + server.getLocalSocketAddress() + "\nGoodbye!");

            server.close();

         }catch(SocketTimeoutException s)

         {

            System.out.println("Socket timed out!");

            break;

         }catch(IOException e)

         {

            e.printStackTrace();

            break;

         }

      }

   }

   public static void main(String [] args)

   {

      int port = Integer.parseInt(args[0]);

      try

      {

         Thread t = new GreetingServer(port);

         t.run();

      }catch(IOException e)

      {

         e.printStackTrace();

      }

   }

}

注解

一、注解简介

从 Java 5 版本之后可以在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation),是 Java 平台中非常重要的一部分。注解都是 @ 符号开头的,例如我们在学习方法重写时使用过的 @Override 注解。同 Class 和 Interface 一样,注解也属于一种类型。

Annotation 可以翻译为“注解”或“注释”,一般翻译为“注解”,因为“注释”一词已经用于说明“//”、“/*.../”和“/.../”等符号了,这里的“注释”是英文 Comment 翻译。

注解并不能改变程序的运行结果,也不会影响程序运行的性能。有些注解可以在编译时给用户提示或警告,有的注解可以在运行时读写字节码文件信息。

注解可以元数据这个词来描述,即一种描述数据的数据。所以可以说注解就是源代码的元数据。例如以下代码:

@Override
public String toString() {
   return "中关村";
}

上面的代码重写了 Object 类的 toString() 方法并使用了 @Override 注解。如果不使用 @Override 注解标记代码,程序也能够正常执行。那么这么写有什么好处吗?事实上,使用 @Override 注解就相当于告诉编译器这个方法是一个重写方法,如果父类中不存在该方法,编译器便会报错,提示该方法没有重写父类中的方法。这样可以防止不小心拼写错误造成麻烦。

例如,在没有使用 @Override 注解的情况下,将 toString() 写成了 toStrring(),这时程序依然能编译运行,但运行结果会和所期望的结果大不相同。

注解常见的作用有以下几种:  1、生成帮助文档。这是最常见的,也是 Java 最早提供的注解。常用的有 @see、@param 和 @return 等;  2、跟踪代码依赖性,实现替代配置文件功能。比较常见的是 Spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量;  3、在编译时进行格式检查。如把 @Override 注解放在方法前,如果这个方法并不是重写了父类方法,则编译时就能检查出。

无论是哪一种注解,本质上都一种数据类型,是一种接口类型。到 Java 8 为止 Java SE 提供了 11 个内置注解。其中有 5 个是基本注解,它们来自于 java.lang 包。有 6 个是元注解,它们来自于 java.lang.annotation 包,自定义注解会用到元注解。

提示:元注解就是负责注解其他的注解。

基本注解包括:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs 和 @FunctionalInterface。

二、@Override注解

Java 中 @Override 注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其它的元素。它可以强制一个子类必须重写父类方法或者实现接口的方法。

使用 @Override 注解示例代码如下:

public class Person {
   private String name = "";
   private int age;
  ...
   @Override
   public String t0String() { //toString()
       return "Person [name=" + name + ", age=" + age + "]";
  }
}

上述代码第 6 行是重写 Object 类的 toString() 方法,该方法使用 @Override 注解。如果 toString() 不小心写成了 t0String(),那么程序会发生编译错误。会有如下的代码提示:

类型为 Person 的方法t0String()必须覆盖或实现超类型方法

所以 @Override 的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。这样可以帮助程序员避免一些低级错误。

当然如果代码中的方法前面不加 @Override 注解,即便是方法编辑错误了,编译器也不会有提示。这时 Object 父类的 toString() 方法并没有被重写,将会引起程序出现 Bug(缺陷)。

三、@Deprecated注解

Java 中 @Deprecated 可以用来注解类、接口、成员方法和成员变量等,用于表示某个元素(类、方法等)已过时。当其他程序使用已过时的元素时,编译器将会给出警告。

使用 @Deprecated 注解示例代码如下:

@Deprecated
public class Person {
   @Deprecated
   protected String name;
   private int age;
   
   public String getName() {
       return name;
  }
   
   public void setName(String name) {
       this.name = name;
  }
   
   public int getAge() {
       return age;
  }
   
   public void setAge(int age) {
       this.age = age;
  }
   
   @Deprecated
   public void setNameAndAge(String name, int age) {
       this.name = name;
       this.age = age;
  }
   
   @Override
   public String toString() {
       return "Person [name=" + name + ", age=" + age + "]";
  }
}

上述代码第 2 行类 Person、第 4 行的成员变量 name 和第 24 行的 setNameAndAge 方法都被 @Deprecated 注解。在 Eclipse 中这些被注解的 API 都会被画上删除线。调用这些 API 代码也会有删除线,示例代码如下。

public class Demo07 {
   public static void main(String[] args) {
       Person p = new Person();
       p.setNameAndAge("java", 20);
       p.name = "Java教程";
  }
}

从图中可以看到代码中不仅有删除线,而且还有编译警告。

Java 9 为 @Deprecated 注解增加了以下两个属性:  forRemoval:该 boolean 类型的属性指定该 API 在将来是否会被删除。  since:该 String 类型的属性指定该 API 从哪个版本被标记为过时。

示例代码如下所示:

class Test {
   // since属性指定从哪个版本开始被标记成过时,forRemoval指定该API将来会被删除
   @Deprecated(since = "9", forRemoval = true)
   public void print() {
       System.out.println("这里是C语言中文网Java教程!");
  }
}

public class DeprecatedTest {
   public static void main(String[] args) {
       // 下面使用info()方法时将会被编译器警告
       new Test().print();
  }
}

上面程序的第 12 行代码使用了 Test 的 print() 方法,而 Test 类中定义 info() 方法时使用了 @Deprecated 修饰,表明该方法已过时,所以将会引起编译器警告。

@Deprecated 的作用与文档注释中的 @deprecated 标记的作用基本相同,但它们的用法不同,前者是 Java 5 才支持的注解,无须放在文档注释语法(/** ... */部分)中,而是直接用于修饰程序中的程序单元,如方法、类和接口等。

四、@SuppressWarnings:抑制编译器警告

Java 中的 @SuppressWarnings 注解指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告,且会一直作用于该程序元素的所有子元素。例如,使用 @SuppressWarnings 修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告。

@SuppressWarnings 注解主要用在取消一些编译器产生的警告对代码左侧行列的遮挡,有时候这样会挡住我们断点调试时打的断点。

如果你确认程序中的警告没有问题,可以不用理会。通常情况下,如果程序中使用没有泛型限制的集合将会引起编译器警告,为了避免这种编译器警告,可以使用 @SuppressWarnings 注解消除这些警告。

注解的使用有以下三种:  抑制单类型的警告:@SuppressWarnings("unchecked")  抑制多类型的警告:@SuppressWarnings("unchecked","rawtypes")  抑制所有类型的警告:@SuppressWarnings("unchecked")

抑制警告的关键字如下表所示。

关键字

用途

all

抑制所有警告

boxing

抑制装箱、拆箱操作时候的警告

cast

抑制映射相关的警告

dep-ann

抑制启用注释的警告

deprecation

抑制过期方法警告

fallthrough

抑制在 switch 中缺失 breaks 的警告

finally

抑制 finally 模块没有返回的警告

hiding

抑制相对于隐藏变量的局部变量的警告

incomplete-switch

忽略不完整的 switch 语句

nls

忽略非 nls 格式的字符

null

忽略对 null 的操作

rawtypes

使用 generics 时忽略没有指定相应的类型

restriction

抑制禁止使用劝阻或禁止引用的警告

serial

忽略在 serializable 类中没有声明 serialVersionUID 变量

static-access

抑制不正确的静态访问方式警告

synthetic-access

抑制子类没有按最优方法访问内部类的警告

unchecked

抑制没有进行类型检查操作的警告

unqualified-field-access

抑制没有权限访问的域的警告

unused

抑制没被使用过的代码的警告

使用 @SuppressWarnings 注解示例代码如下:

public class Demo07 {
   @SuppressWarnings({"deprecation"})
   public static void main(String[] args) {
       Person p = new Person();
       p.setNameAndAge("java", 20);
       p.name = "Java教程";
  }
}

上述代码第 2 行使用 @SuppressWarnings({ "deprecation" }) 注解了 main 方法。在@Deprecated注解中的 Person 代码中,这些 API 已经过时了,所以代码第 4 行~第 6 行是编译警告,但是在使用了 @SuppressWarnings 注解之后会发现程序代码的警告没有了。

五、@SafeVarargs注解

在介绍 @SafeVarargs 注解用法之前,先来看看如下代码:

public class HelloWorld {
   public static void main(String[] args) {
       // 传递可变参数,参数是泛型集合
       display(10, 20, 30);
       // 传递可变参数,参数是非泛型集合
       display("10", 20, 30); // 会有编译警告
  }
   
   public static <T> void display(T... array) {
       for (T arg : array) {
           System.out.println(arg.getClass().getName() + ":" + arg);
      }
  }
}

代码第 10 行声明了一种可变参数方法 display,display 方法参数个数可以变化,它可以接受不确定数量的相同类型的参数。可以通过在参数类型名后面加入...的方式来表示这是可变参数。可变参数方法中的参数类型相同,为此声明参数是需要指定泛型。

但是调用可变参数方法时,应该提供相同类型的参数,代码第 4 行调用时没有警告,而代码第 6 行调用时则会发生警告,这个警告是 unchecked(未检查不安全代码),就是因为将非泛型变量赋值给泛型变量所发生的。

可用 @SafeVarargs 注解抑制编译器警告,修改代码如下:

public class HelloWorld {
   public static void main(String[] args) {
       // 传递可变参数,参数是泛型集合
       display(10, 20, 30);
       // 传递可变参数,参数是非泛型集合
       display("10", 20, 30); // 没有@SafeVarargs会有编译警告
  }
   
   @SafeVarargs
   public static <T> void display(T... array) {
       for (T arg : array) {
           System.out.println(arg.getClass().getName() + ":" + arg);
      }
  }
}

上述代码在可变参数 display 前添加了 @SafeVarargs 注解,当然也可以使用 @SuppressWarnings("unchecked") 注解,但是两者相比较来说 @SafeVarargs 注解更适合。

注意:@SafeVarargs注解不适用于非 static 或非 final 声明的方法,对于未声明为 static 或 final 的方法,如果要抑制 unchecked 警告,可以使用 @SuppressWarnings 注解。

六、@FunctionalInterface注解

在学习 Lambda 表达式时,我们提到如果接口中只有一个抽象方法(可以包含多个默认方法或多个 static 方法),那么该接口就是函数式接口。@FunctionalInterface 就是用来指定某个接口必须是函数式接口,所以 @FunInterface 只能修饰接口,不能修饰其它程序元素。

函数式接口就是为 Java 8 的 Lambda 表达式准备的,Java 8 允许使用 Lambda 表达式创建函数式接口的实例,因此 Java 8 专门增加了 @FunctionalInterface。

例如,如下程序使用 @FunctionalInterface 修饰了函数式接口。

@FunctionalInterface
public interface FunInterface {
   static void print() {
       System.out.println("C语言中文网");
  }
   
   default void show() {
       System.out.println("我正在学习C语言中文网Java教程");
  }
   
   void test(); // 只定义一个抽象方法
}

编译上面程序,可能丝毫看不出程序中的 @FunctionalInterface 有何作用,因为 @FunctionalInterface 注解的作用只是告诉编译器检查这个接口,保证该接口只能包含一个抽象方法,否则就会编译出错。

@FunctionalInterface 注解主要是帮助程序员避免一些低级错误,例如,在上面的 FunInterface 接口中再增加一个抽象方法 abc(),编译程序时将出现如下错误提示:

“@FunctionInterface”批注无效;FunInterface不是functional接口

七、Java 元注解作用及使用

元注解是负责对其它注解进行说明的注解,自定义注解时可以使用元注解。Java 5 定义了 4 个注解,分别是 @Documented、@Target、@Retention 和 @Inherited。Java 8 又增加了 @Repeatable 和 @Native 两个注解。这些注解都可以在 java.lang.annotation 包中找到。下面主要介绍每个元注解的作用及使用。

1、@Documented

@Documented 是一个标记注解,没有成员变量。用 @Documented 注解修饰的注解类会被 JavaDoc 工具提取成文档。默认情况下,JavaDoc 是不包括注解的,但如果声明注解时指定了 @Documented,就会被 JavaDoc 之类的工具处理,所以注解类型信息就会被包括在生成的帮助文档中。

下面通过示例来了解它的用法,代码如下所示。

例 1:

@Documented
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface MyDocumented {
   public String value() default "这是@Documented注解";
}

测试类:

@MyDocumented
public class DocumentedTest {
   /**
    * 测试document
    */
   @MyDocumented
   public String Test() {
       return "C语言中文网Java教程";
  }
}

打开 Java 文件所在的目录,分别输入如下两条命令行:

javac MyDocumented.java DocumentedTest.java
javadoc -d doc MyDocumented.java DocumentedTest.java

运行成功后,打开生成的帮助文档,可以看到在类和方法上都保留了 MyDocument 的注解信息。如下图所示:

2、@Target

@Target 注解用来指定一个注解的使用范围,即被 @Target 修饰的注解可以用在什么地方。@Target 注解有一个成员变量(value)用来设置适用目标,value 是 java.lang.annotation.ElementType 枚举类型的数组,下表为 ElementType 常用的枚举常量。

名称

说明

CONSTRUCTOR

用于构造方法

FIELD

用于成员变量(包括枚举常量)

LOCAL_VARIABLE

用于局部变量

METHOD

用于方法

PACKAGE

用于包

PARAMETER

用于类型参数(JDK 1.8新增)

TYPE

用于类、接口(包括注解类型)或 enum 声明

例 2:

自定义一个 MyTarget 注解,使用范围为方法,代码如下所示。

@Target({ ElementType.METHOD })
public @interface MyTarget {
}

class Test {
   @MyTarget
   String name;
}

如上代码第 6 行会编译错误,错误信息为:

The annotation @MyTarget is disallowed for this location

提示此位置不允许使用注解 @MyDocumented,@MyTarget 不能修饰成员变量,只能修饰方法。

3、@Retention

@Retention 用于描述注解的生命周期,也就是该注解被保留的时间长短。@Retention 注解中的成员变量(value)用来设置保留策略,value 是 java.lang.annotation.RetentionPolicy 枚举类型,RetentionPolicy 有 3 个枚举常量,如下所示。  SOURCE:在源文件中有效(即源文件保留)  CLASS:在 class 文件中有效(即 class 保留)  RUNTIME:在运行时有效(即运行时保留)

生命周期大小排序为 SOURCE < CLASS < RUNTIME,前者能使用的地方后者一定也能使用。如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS 注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。  @Inherited  @Inherited 是一个标记注解,用来指定该注解可以被继承。使用 @Inherited 注解的 Class 类,表示这个注解可以被用于该 Class 类的子类。就是说如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解。

例 3:

创建一个自定义注解,代码如下所示:

@Target({ ElementType.TYPE })
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInherited {
}

测试类代码如下:

@MyInherited
public class TestA {
   public static void main(String[] args) {
       System.out.println(TestA.class.getAnnotation(MyInherited.class));
       System.out.println(TestB.class.getAnnotation(MyInherited.class));
       System.out.println(TestC.class.getAnnotation(MyInherited.class));
  }
}

class TestB extends TestA {
}

class TestC extends TestB {
}

运行结果为:

@MyInherited()
@MyInherited()
@MyInherited()

4、@Repeatable

@Repeatable 注解是 Java 8 新增加的,它允许在相同的程序元素中重复注解,在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。

例 4:

Java 8 之前的做法:

public @interface Roles {
   Role[] roles();
}

public @interface Roles {
   Role[] value();
}

public class RoleTest {
   @Roles(roles = {@Role(roleName = "role1"), @Role(roleName = "role2")})
   public String doString(){
       return "这是C语言中国网Java教程";
  }
}

Java 8 之后增加了重复注解,使用方式如下:

public @interface Roles {
   Role[] value();
}

@Repeatable(Roles.class)
public @interface Role {
   String roleName();
}

public class RoleTest {
   @Role(roleName = "role1")
   @Role(roleName = "role2")
   public String doString(){
       return "这是C语言中文网Java教程";
  }
}

不同的地方是,创建重复注解 Role 时加上了 @Repeatable 注解,指向存储注解 Roles,这样在使用时就可以直接重复使用 Role 注解。从上面例子看出,使用 @Repeatable 注解更符合常规思维,可读性强一点。

两种方法获得的效果相同。重复注解只是一种简化写法,这种简化写法是一种假象,多个重复注解其实会被作为“容器”注解的 value 成员的数组元素处理。

5、@Native

使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。

八、Java 自定义注解

声明自定义注解使用 @interface 关键字(interface 关键字前加 @ 符号)实现。定义注解与定义接口非常像,如下代码可定义一个简单形式的注解类型。

// 定义一个简单的注解类型

public @interface Test {}

上述代码声明了一个 Test 注解。默认情况下,注解可以在程序的任何地方使用,通常用于修饰类、接口、方法和变量等。

定义注解和定义类相似,注解前面的访问修饰符和类一样有两种,分别是公有访问权限(public)和默认访问权限(默认不写)。一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的注解。且源程序文件命名和公有访问权限的注解名一致。

不包含任何成员变量的注解称为标记注解,例如上面声明的 Test 注解以及基本注解中的 @Override 注解都属于标记注解。根据需要,注解中可以定义成员变量,成员变量以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。代码如下所示:

public @interface MyTag {    

// 定义带两个成员变量的注解    

// 注解中的成员变量以方法的形式来定义    

String name();    

int age();

}

以上代码中声明了一个 MyTag 注解,定义了两个成员变量,分别是 name 和 age。成员变量也可以有访问权限修饰符,但是只能有公有权限和默认权限。

如果在注解里定义了成员变量,那么使用该注解时就应该为它的成员变量指定值,如下代码所示。

public class Test {    

    // 使用带成员变量的注解时,需要为成员变量赋值    

    @MyTag(name="xx", age=6)    

    public void info() {        

        ...    

    }    

    ...

}

注解中的成员变量也可以有默认值,可使用 default 关键字。如下代码定义了 @MyTag 注解,该注解里包含了 name 和 age 两个成员变量。

public @interface MyTag {    

    // 定义了两个成员变量的注解    

    // 使用default为两个成员变量指定初始值    

    String name() default "中关村";    

    int age() default 7;}

如果为注解的成员变量指定了默认值,那么使用该注解时就可以不为这些成员变量赋值,而是直接使用默认值。

public class Test {    

    // 使用带成员变量的注解    

    // MyTag注释的成员变量有默认值,所以可以不为它的成员变量赋值    

    @MyTag    

    public void info() {        

        ...    

    }    

    ...

}

当然也可以在使用 MyTag 注解时为成员变量指定值,如果为 MyTag 的成员变量指定了值,则默认值不会起作用。

根据注解是否包含成员变量,可以分为如下两类。

  1. 标记注解:没有定义成员变量的注解类型被称为标记注解。这种注解仅利用自身的存在与否来提供信息,如前面介绍的 @Override、@Test 等都是标记注解。
  2. 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。

XML基础_Java解析XML

案例源码地址:Java XML解析: XML(可扩展标记语言)是一种很流行的简单的基于文本的语言来用作应用程序之间的通信模式。它被认为是传输标准装置和存储数据。JAVA提供了极好的支持和丰富的库来解析,修改或查询XML文档。

一、XML基础

1.1、XML是什么?

XML(可扩展标记语言)是一种很流行的简单的基于文本的语言来用作应用程序之间的通信模式。它被认为是传输标准装置和存储数据。JAVA提供了极好的支持和丰富的库来解析,修改或查询XML文档。

XML是一种简单的基于文本的语言,它被设计为储存和运输以纯文本格式的数据。它代表着可扩展标记语言。以下是一些XML的显着特征。  XML是一种标记语言。  XML是一种标记语言就像HTML一样。  XML标签不是像HTML那样预定义。  可以定义自己的标签,这就是为什么它被称为可扩展的语言。  XML标签被设计成自描述性的。  XML是W3C推荐用于数据存储和传输。

1.2、XML能干什么?

描述数据、存储数据、传输(交换)数据。

优缺点:

优势 以下是XML提供的优势: 技术无关 - 作为普通文本,XML是技术独立。它可以用于由任何技术进行数据的存储和传输的目的。 人类可读 - XML使用简单的文本格式。它是人类可读和可以理解的。 可扩展性 - 在XML,自定义标签可以创建和很容易使用。 允许验证 - 使用XSD,DTD和XML结构可以很容易地验证。 缺点 下面是使用XML的缺点: 冗余的语法 - 通常XML文件中包含大量的重复计算。 冗余 - 作为一个冗长的语言,XML文件大小增加了传输和存储成本。

1.3、XML与HTML区别

1、目的不一样  2、XML 被设计用来描述数据,其焦点是数据的内容。  3、HTML 被设计用来展示数据,其焦点是数据的外观。  4、HTML可以不关闭标签(即标签可以不成对出现),但XML必须关闭标签(即标签必须成对出现)。  5、HTML中的标签标识文本如何展示,而XML中的标签标识文本是什么含义(什么类型的文本)。 XML文档节点类型  文档(document)  元素(element)  属性(attribute)  文本(PCDATA--parsed character data)  注释(comment)  DOCTYPE :主要验证文档内容的正确性  实体(ENTITIES)  CDATA(character data)

1.4、XML语法

1、声明:<?xml version="1.0" encoding="UTF-8"?>  2、根节点:必须只能有一个根节点  3、标签:标签必须有结束且区分大小写,标签必须顺序嵌套  4、属性:必须引号引起值  5、空格会被保留,HTML空格最多保留一个  6、命名规则:命名必须见名知意 a)名字可包含字母、数字以及其他的字符 b)名字不能以数字或者标点符号开始 c)名字不能以字符“xml”(或者XML、Xml)开始  7、名字不能包含空格  8、 不应在 XML 元素名称中使用 ":" ,这是由于它用于命名空间(namespaces)的保留字。  9、标签优先于属性。  10、XML 命名空间可提供避免元素命名冲突的方法。  11、CDATA:字符数据,<![CDATA[字符数据]]> ,字符数据不进行转义  12、实体:&实体;

<?xml version='1.0' encoding='UTF-8' ?><!--文档声明  version='1.0'为必须字段-->
<users><!--根节点,有且只有一个根节点-->
    
    <!-- 子节点,随便写,符合规则即可 -->
    <user id='Z' number="zpark-001">
        <name>zhangsan</name>
        <age>23</age>
        <gender>nan</gender>
    </user>

    <user id='L' number="zpark-002">
        <name>lisi</name>
        <age>24</age>
        <gender>nv</gender>
    </user>
</users>

1.5、Xml约束

1.5.1、XML DTD 约束

DTD(DocType Definition 文档类型定义)的作用是定义 XML 文档的合法构建模块。它使用一系列的合法元素来定义文档结构。用于约定XML格式。

1、DTD引用方式  1.1、内部 <!DOCTYPE 根元素 [元素声明]>

例如:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE beans [
        <!ELEMENT beans (bean*)>
        <!ELEMENT bean (name,author,money)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ELEMENT money (#PCDATA)>
]>

<beans>
    <bean>
        <name>张三</name>
        <author>56</author>
        <money>3500</money>
    </bean>
    ...
</beans>

1.2、外部私有的 SYSTEM 一般是我们自己定义的,可能只是一个公司内部使用,<!DOCTYPE 根元素 SYSTEM "dtd文件位置">

例如:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE 书架 SYSTEM "book.dtd">
<beans>
    <bean>
        <name>张三</name>
        <author>56</author>
        <money>3500</money>
    </bean>
    ...
</beans>

1.3、外部公有的 PUBLIC 一般是一些标准,可能非常多的人用,<!DOCTYPE 根元素 PUBLIC "命名空间" "dtd文件位置">,首先根据“命名空间”去问环境要相应的dtd文件,如果有,直接提供,如果没有再根据dtd文件位置找。

例如:

<!DOCTYPE web-app PUBLIC
               "-//SunMicrosystems, Inc.//DTD Web Application 2.3//EN"
               "http://java.sun.com/dtd/web-app_2_3.dtd">

<?xml version="1.0"?>
<!DOCTYPE note [
  <!ELEMENT note (to,from,heading,body)>
  <!ELEMENT to      (#PCDATA)>
  <!ELEMENT from    (#PCDATA)>
  <!ELEMENT heading (#PCDATA)>
  <!ELEMENT body    (#PCDATA)>
]>
<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend</body>
</note>

相关参考文档及DTD教程:DTD 教程 | 菜鸟教程

1.5.2、XML Schema 约束

XML Schema 是基于 XML 的 DTD 替代者。XML Schema 描述 XML 文档的结构。XML Schema 语言也称作 XML Schema 定义(XML Schema Definition,XSD)。  DTD不是通过XML语法定义文档结构, 不能定义数据类型和限制Schema通过XML语法定义文档结构,可以定义数据类型和限制

约定XML格式 定义可出现在文档中的元素 定义可出现在文档中的属性 定义哪个元素是子元素 定义子元素的次序 定义子元素的数目 定义元素是否为空,或者是否可包含文本 定义元素和属性的数据类型 定义元素和属性的默认值以及固定值 1、为何使用Schema XML Schema 是 DTD 的继任者 XML Schema 可针对未来的需求进行扩展 XML Schema 更完善,功能更强大 XML Schema 基于 XML 编写 XML Schema 支持数据类型和限制 XML Schema 支持命名空间

2、Schema引用方式

<users xmlns="命名空间"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="命名空间 Schema位置">

如何找Schema?和DTD一样,首先根据命名空间问环境要,找不到再根据Schema位置找。

例子:

<?xml version="1.0" encoding="UTF-8"?>  
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"  
            <!--xs="http://www.w3.org/2001/XMLSchema" 声名了w3c的名称空间,方便下面调用 -->    
              
             targetNamespace="http://www.zhong.cn"  
                      elementFormDefault="qualified">      
<!--  
    schema 是根元素  
      
    xmlns:xs="http://www.w3.org/2001/XMLSchema"                    
    指明了在schema中使用的元素和数据种类来自http://www.w3.org/2001/XMLSchema名称空间(namespace)。  
    它也指定了来自"http://www.w3.org/2001/XMLSchema"名称空间(namespace)的元素和数据种类必须带前缀“xs:”    
      
    targetNamespace="http://www.zhong.cn"(将全部元素绑定给这个名称空间)  
    暗示了由这份schema(shiporder, orderperson, shipto, ....)定义的元素来自"http://www.zhong.com"名称空间  
      
    xmlns="http://www.w3schools.com"  
    指明了默认名称空间(namespace)是http://www.w3schools.com.  
      
    elementFormDefault="qualified" (“unqualified”)将根节点绑定到名称空间  
        将所有元素绑定到名称空间  
  -->                    
                        
    <!--xs:element  指的是element这个元素来自于xs名称空间 -->                  
    <xs:element name="shiporder"> <!-- 定义一个元素 shiporder -->  
     <xs:complexType>             <!-- 类型是:复合类型(里面包含元素或者属性) -->  
      <xs:sequence>                   <!-- 元素要有顺序 -->  
       <!-- 定义一个元素 orderperson 类型为:字符串 -->  
       <xs:element name="orderperson" type="xs:string"/>          
       <!-- 定义一个元素 shipto 最少出现1次,最多出现1次  -->  
       <xs:element name="shipto" minOccurs="1" maxOccurs="1"> 
        <xs:complexType> <!-- shipto元素也是复合类型 -->  
         <xs:sequence>   <!-- 元素要有顺序 -->  
          <xs:element name="name" type="xs:string"/> <!-- 在shipto元素中定义一个元素 name 类型为:字符串 -->  
          <xs:element name="address" type="xs:string"/>  
          <xs:element name="city" type="xs:string"/>  
          <xs:element name="country" type="xs:string"/>  
         </xs:sequence>  
        </xs:complexType>  
       </xs:element>  
          
       <!-- 在shiporder元素中定义一个元素 item 出现次数可以无限次 -->  
       <xs:element name="item" maxOccurs="unbounded">  
        <xs:complexType>  
         <xs:sequence>  
          <xs:element name="title" type="xs:string"/>  
          <xs:element name="note" type="xs:string" minOccurs="0"/>  
          <xs:element name="quantity" type="xs:positiveInteger"/>  
          <xs:element name="price" type="xs:decimal"/>  
         </xs:sequence>  
        </xs:complexType>  
       </xs:element>  
      </xs:sequence>  
      <xs:attribute name="orderid" type="xs:string" use="required"/>  
     </xs:complexType>  
    </xs:element>  
</xs:schema>  

schema教程及相关参考文档:XML Schema 教程 | 菜鸟教程

二、Java XML 教程

1、Java XML 解析器

1.1、什么是XML解析?

解析XML是指将通过XML文档访问数据或修改数据的一个操作或方法。

Java库中提供了两种XML解析器:  1、像文档对象模型(Document Object Model,DOM)解析器这的树型解析器(tree parse),它们将读入的XML文档转换 成树结构。  2、像XML简单API(Simple API for XML,SAX)解析器这样的流机制解析器(streaming parser),它们在读入XML文档时生 成相应的事件。

1.2、XML解析器是什么?

XML解析器提供方法来访问或修改XML文档中的数据。 Java提供了多种选择来解析XML文档。以下是各种类型解析器其通常用于解析XML文档。  Dom解析器 - 解析通过加载该文件的全部内容,并创建其完整分级树中存储的文件。  SAX解析器 - 解析基于事件触发器的文档。不完整(部分)的文件加载到存储器中。  JDOM解析器 - 解析以类似的方式,以DOM解析器但更简单的方法的文档。  StAX解析器 - 解析以类似的方式,以SAX解析器但在更高效的方式的文档。  XPath解析器 - 解析基于表达式XML并广泛选择使用XSLT。  DOM4J解析器 - Java库来解析XML,XPath和使用Java集合框架XSLT,为DOM,SAX和JAXP的支持。

2、Java DOM解析器

2.1、DOM解析器简介

文档对象模型是万维网联盟(W3C)的官方推荐。它定义了一个接口,使程序能够访问和更新样式,结构和XML文档的内容。支持DOM实现该接口的XML解析器。

何时使用? 在以下几种情况时,应该使用DOM解析器: 1、需要知道很多关于文档的结构 2、需要将文档的部分周围(例如,可能需要某些元素进行排序) 3、需要使用的文件中的信息超过一次

会得到什么? 当使用DOM 解析器解析一个XML文档,会得到一个树形结构,其中包含的所有文档的元素。 DOM提供了多种可用于检查文档的内容和结构的函数。

优势 DOM是用于处理文档结构的通用接口。它的一个设计目标是Java代码编写一个DOM兼容的解析器,运行在任何其他的DOM兼容的解析器不会有变化。

DOM接口 DOM定义了几个Java接口。这里是最常见的接口: 1、节点 - DOM的基本数据类型。 2、元素 - 要处理的对象绝大多数是元素。 3、Attr - 代表元素的属性。 4、文本 - 元素或Attr的实际内容。 5、文档 - 代表整个XML文档。文档对象是通常被称为DOM树。

常见的DOM方法 当正在使用DOM,有经常用到的几种方法: 1、Document.getDocumentElement() - 返回文档的根元素。 2、Node.getFirstChild() - 返回给定节点的第一个子节点。 3、Node.getLastChild() - 返回给定节点的最后一个子节点。 4、Node.getNextSibling() - 这些方法返回一个特定节点的下一个兄弟节点。 5、Node.getPreviousSibling() - 这些方法返回一个特定节点的前一个兄弟节点。 6、Node.getAttribute(attrName) - 对于给定的节点,则返回所请求的名字的属性。

2.2、Java DOM解析器 - 解析XML文档

使用DOM的步骤 以下是在使用DOM解析器解析文档使用的步骤。 1、导入XML相关的软件包。 2、创建DocumentBuilder 3、从文件或流创建一个文档 4、提取根元素 5、检查属性 6、检查子元素

document对象: 要操作XML,先就得有Document对象,把一个XML文件加载进内存的时候,在内存中形成所谓的一种树状结构,我们把这一个结构称之为DOM树.

注意: 我们在Java代码中所做的增/删/改/查操作,都仅仅是操作的是内存中的Document对象,和磁盘中的XML文件没有关系。比如:删除一个联系人信息之后,XML文件中数据依然存在,此时出现内存中的数据和磁盘文件中的数据不同步。所以,对于增删改操作,我们需要做同步操作(把内存中的数据和磁盘的XML文件数据保持一致)。 DOM:在第一次的时候就会把XML文件加载进内存,如果XML文件过大,可能会造成内存的溢出. DOM:在做增删改查操作的时候比较简单,,但是性能却不高(线性搜索).

获取Document对象:

演示示例:

步骤1:创建 ’Java-DOM解析器‘ 工程,并且在resources目录下创建domDemo.xml文件

<?xml version="1.0" encoding="utf-8" ?>
<class>
    <student rollno="393">
        <firstname>dinkar</firstname>
        <lastname>kad</lastname>
        <nickname>dinkar</nickname>
        <marks>85</marks>
    </student>
    <student rollno="493">
        <firstname>Vaneet</firstname>
        <lastname>Gupta</lastname>
        <nickname>vinni</nickname>
        <marks>95</marks>
    </student>
    <student rollno="593">
        <firstname>jasvir</firstname>
        <lastname>singn</lastname>
        <nickname>jazz</nickname>
        <marks>90</marks>
    </student>
</class>

步骤2:创建dom解析类 ‘com.zpark.DOMParseDemo’

package com.zpark;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

public class DOMParseDemo {
    public static void main(String[] args) throws Exception {
        /*
            1):表示出需要被操作的XML文件的路径,注意是文件的路径,不是文件所在的目录.
                File f = new File(...);
            2):根据DocumentBuilderFactory类,来获取DocumentBuilderFactory对象.
                注意:工厂设计模式往往体现着单例设计模式.
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance() ;
            3):根据DocumentBuilderFactory对象,构建DocumentBuilder对象.
                注意:XxxFactory,就是用来创建Xxx对象的.
                DocumentBuilder build = factory .newDocumentBuilder();
            4):根据DocumentBuidler对象,构建Document对象.
                Document doc = build.parse(f);
         */
        // 获取xml文件路径
        String path = DOMParseDemo.class.getClassLoader().getResource("domDemo.xml").getPath();
        // 由于路径中包含中文,所以需要对路径解析解码
        path = URLDecoder.decode(path, "utf-8");

        File file = new File(path);
        // 获取DocumentBuilderFactory对象
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        // 根据DocumentBuilderFactory对象,构建DocumentBuilder对象
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        // 根据DocumentBuidler对象,构建Document对象
        Document document = documentBuilder.parse(path);

        // 获取给定标签名元素内容
        NodeList student = document.getElementsByTagName("student");
        for (int i = 0; i < student.getLength(); i++) {
            // 获取节点对象
            Node node = student.item(i);
            // 将node转为element,node为element父接口
            Element element = (Element)node;
            // 根据标签名字获取标签的text值
            String firstname = element.getElementsByTagName("firstname").item(0).getTextContent();
            System.out.println(firstname);
        }
    }
}

程序运行结果:

3、Java SAX解析器

3.1、Java SAX解析器简介

SAX解析器在解析XML输入数据的各个组成部分时会报告事件,但不会以任何方式存储文档,而是由事件处理器建立相应的数据结构。实际上DOM解析器是在SAX解析器的基础上构建的,它在接收到解析器事件时构建DOM树。

SAX(针对XML的简单API)是基于事件为XML文档的解析器。不像DOM解析器,SAX解析器创建没有解析树。 SAX是一个流接口用于XML的,这意味着使用SAX应用接收事件通知有关XML文档被处理的元素,属性,在按顺序每次开始在文档的顶部,并与所述闭合结束根元素。  1、读取XML文件从上到下,构成一个结构完整的XML文档的标记  2、令牌以相同的顺序进行处理,它们出现在文档中  3、报告应用程序,因为它们所出现解析器遇到标记的特性  4、应用程序提供了必须的解析器注册的“事件”处理程序  5、作为标记标识,在处理程序回调方法相关信息调用

什么时候使用? 应该使用SAX解析器的时候: 1、可以在XML文档从上往下处理以线性方式 2、该文件并不深层次嵌套 3、处理一个非常大的XML文档,DOM树会占用太多的内存。典型DOM的实现使用10字节的存储器以表示XML的一个字节 4、解决的问题涉及的XML文档的一部分 5、数据是可用的,只要它是由解析器看出,这样的SAX可以很好地用于到达流的XML文档

SAX的缺点 1、它是在一个只进入处理随机访问方式XML文档 2、如果需要跟踪的数据分析器已经看到或更改项目的顺序,必须自已编写代码和数据存储

ContentHandler接口 此接口指定SAX解析器用来通知XML文档,已经看到部件应用程序的回调方法。

方法

方法描述

void startDocument()

调用在一个文件的开头。

void endDocument()

调用在一个文件的末尾。

void startElement(String uri, String localName, String qName, Attributes atts)

调用在一个元素的开头

void endElement(String uri, String localName,String qName)

调用在一个元件的末端。

void characters(char[] ch, int start, int length)

字符数据出现时调用。

void ignorableWhitespace( char[] ch, int start, int length)

当DTD是当前和忽略空白遇到时调用。

void processingInstruction(String target, String data)

当处理指令的认可时调用。

void setDocumentLocator(Locator locator))

提供可用于识别文档中的位置的定位器。

void skippedEntity(String name)

一个尚未解决实体遇到时调用。

void startPrefixMapping(String prefix, String uri)

当一个新的命名空间的映射定义调用。

void endPrefixMapping(String prefix)

当一个命名空间定义结束其范围时调用。

属性接口 这种接口指定用于处理连接到一个元素的属性的方法。 int getLength() - 返回属性的数目。 String getQName(int index) String getValue(int index) String getValue(String qname)

3.2、Java SAX解析器 - 解析XML文档

在使用SAX解析器时,需要一个处理器来为各种解析器事件定义事件动作。DefaultHandler接口定义了若干个在解析文档时解析器会调用的回调方法。下面是最重要的几个方法:  1、startElement和endElement在每当遇到起始或终止标签时调用。  2、characters在每当遇到字符数据时调用。  3、startDocument和endDocument分别在文档开始和结束时各调用一次。

例如,在解析以下片段时:

<person>
    <name type="string">韩信</name>
    <age>25</age>
</person>

解析器会产生以下回调: 1)startElement,元素名:person 2)startElement,元素名:name ,属性:type="string" 3)characters,内容:韩信 4)endElement,元素名:name 5)startElement,元素名:age 6)characters,内容:25 7)endElement,元素名:age 8)endElement,元素名:person

处理器必须覆盖这些方法,让它们执行在解析文件时我们想让它们执行的动作。下面通过一个简单的demo体会SAX解析XML的过程。

准备xml

首先准备person.xml,内容如下

<?xml version="1.0" encoding="UTF-8" ?>
<persons>
    <person>
      <name>韩信</name>
      <age>25</age>
   </person>
   <person>
      <name>李白</name>
      <age>23</age>
   </person>
</persons>

解析代码 1、获取解析工厂 2、从解析工厂获取解析器 3、得到解读器 4、设置内容处理器 5、读取xml的文档内容

package com.zpark;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.net.URLDecoder;

/**
 * @PackageName: com.zpark
 * @ClassName: SAXParseFactoryDemo
 * @Description:  SAX方式解析xml文件
 * @author: RZS
 * @date: 2021/8/15  15:37
 */
public class SAXParseFactoryDemo {
    public static void main(String[] args) throws Exception{
        // 获取xml文件路径
        String path = SAXParseFactoryDemo.class.getClassLoader().getResource("sax-parse.xml").getPath();
        path = URLDecoder.decode(path, "utf-8");
        // SAX解析
        // 1.获取解析工厂
        SAXParserFactory factory = SAXParserFactory.newInstance();
        // 2.从解析工厂获取解析器
        SAXParser parse = factory.newSAXParser();
        // 3.得到解读器
        XMLReader reader=parse.getXMLReader();
        // 4.设置内容处理器
        reader.setContentHandler(new PHandler());
        // 5.读取xml的文档内容
        reader.parse(path);

    }

}

class PHandler extends DefaultHandler {
    /**
     * @author lastwhisper
     * @desc 文档解析开始时调用,该方法只会调用一次
     * @param
     * @return void
     */
    @Override
    public void startDocument() throws SAXException {
        System.out.println("----解析文档开始----");
    }

    /**
     * @author lastwhisper
     * @desc 每当遇到起始标签时调用
     * @param uri xml文档的命名空间
     * @param localName 标签的名字
     * @param qName 带命名空间的标签的名字
     * @param attributes 标签的属性集
     * @return void
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        System.out.println("标签<"+qName + ">解析开始");
    }

    /**
     * @author lastwhisper
     * @desc 解析标签内的内容的时候调用
     * @param ch 当前读取到的TextNode(文本节点)的字节数组
     * @param start 字节开始的位置,为0则读取全部
     * @param length 当前TextNode的长度
     * @return void
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contents = new String(ch, start, length).trim();
        if (contents.length() > 0) {
            System.out.println("内容为-->" + contents);
        } else {
            System.out.println("内容为-->" + "空");
        }
    }
    /**
     * @author lastwhisper
     * @desc 每当遇到结束标签时调用
     * @param uri xml文档的命名空间
     * @param localName 标签的名字
     * @param qName 带命名空间的标签的名字
     * @return void
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        System.out.println("标签</"+qName + ">解析结束");
    }
    /**
     * @author lastwhisper
     * @desc 文档解析结束后调用,该方法只会调用一次
     * @param
     * @return void
     */
    @Override
    public void endDocument() throws SAXException {
        System.out.println("----解析文档结束----");
    }
}

运行main函数,查看运行结果:

分析解析过程

----解析文档开始---- 调用startDocument
标签<persons>解析开始 调用startElement
内容为-->空 调用characters,因为<persons></persons>之间只有标签没有内容,所以为空。
标签<person>解析开始 调用startElement
内容为-->空 调用characters,因为<person></person>之间只有标签没有内容,所以为空。
标签<name>解析开始 调用startElement
内容为-->韩信 调用characters,<name>韩信</name>之间内容为:韩信
标签</name>解析结束 调用endElement
内容为-->空 调用characters,每次执行完调用endElement之后会调用一次characters
标签<age>解析开始 调用startElement
内容为-->25 调用characters,<age>25</age>之间内容为:25
标签</age>解析结束 调用endElement
内容为-->空 调用characters,每次执行完调用endElement之后会调用一次characters
标签</person>解析结束 调用endElement
内容为-->空 调用characters,每次执行完调用endElement之后会调用一次characters
...
标签<person>解析开始
内容为-->空
标签<name>解析开始
内容为-->李白
标签</name>解析结束
内容为-->空
标签<age>解析开始
内容为-->23
标签</age>解析结束
内容为-->空
标签</person>解析结束
内容为-->空
标签</persons>解析结束
----解析文档结束---- 调用endDocument,xml解析结束

解析xml到pojo对象

pojo对象:

package com.zpark.pojo;

public class Person {
    private String name;
    private Integer age;

    // get()/set()/toString()
}

xml转换为pojo的代码

package com.zpark;

import com.zpark.pojo.Person;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;

/**
 * @PackageName: com.zpark
 * @ClassName: SAXParseXmlToPojoDemo
 * @Description: 将xml数据解析到pojo中
 * @author: RZS
 * @date: 2021/8/15  15:46
 */
public class SAXParseXmlToPojoDemo {
    public static void main(String[] args) throws ParserConfigurationException, SAXException, IOException {
        // 获取xml文件路径
        String path = SAXParseFactoryDemo.class.getClassLoader().getResource("sax-parse.xml").getPath();
        path = URLDecoder.decode(path, "utf-8");
        // SAX解析
        // 1.获取解析工厂
        SAXParserFactory factory = SAXParserFactory.newInstance();
        // 2.从解析工厂获取解析器
        SAXParser parse = factory.newSAXParser();
        // 3.得到解读器
        XMLReader reader = parse.getXMLReader();
        // 4.设置内容处理器
        PersonHandler personHandler = new PersonHandler();
        reader.setContentHandler(personHandler);
        // 5.读取xml的文档内容
        reader.parse(path);

        List<Person> persons = personHandler.getPersons();
        for (Person person : persons) {
            System.out.println("姓名:" + person.getName() + " 年龄:" + person.getAge());
        }
    }

}

class PersonHandler extends DefaultHandler {
    private List<Person> persons;
    private Person person;
    private String tag; // 存储操作标签

    /**
     * @author lastwhisper
     * @desc 文档解析开始时调用,该方法只会调用一次
     * @param
     * @return void
     */
    @Override
    public void startDocument() throws SAXException {
        persons = new ArrayList<Person>();
    }

    /**
     * @author lastwhisper
     * @desc 标签(节点)解析开始时调用
     * @param uri xml文档的命名空间
     * @param localName 标签的名字
     * @param qName 带命名空间的标签的名字
     * @param attributes 标签的属性集
     * @return void
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        tag = qName;
        if ("person".equals(tag)) {
            person = new Person();
        }
    }

    /**
     * @author lastwhisper
     * @desc 解析标签的内容的时候调用
     * @param ch  字符
     * @param start 字符数组中的起始位置
     * @param length 要从字符数组使用的字符数
     * @return void
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        String contents = new String(ch, start, length).trim();
        if ("name".equals(tag)) {
            person.setName(contents);
        } else if ("age".equals(tag)) {
            if (contents.length() > 0) {
                person.setAge(Integer.valueOf(contents));
            }
        }
    }

    /**
     * @author lastwhisper
     * @desc 标签(节点)解析结束后调用
     * @param uri xml文档的命名空间
     * @param localName 标签的名字
     * @param qName 带命名空间的标签的名字
     * @return void
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        if ("person".equals(qName)) {
            persons.add(person);
        }
        tag = null; //tag丢弃了
    }

    /**
     * @author lastwhisper
     * @desc 文档解析结束后调用,该方法只会调用一次
     * @param
     * @return void
     */
    @Override
    public void endDocument() throws SAXException {
    }

    public List<Person> getPersons() {
        return persons;
    }
}

运行结果:

4、Java StAX解析器

4.1、Java StAX解析器简介

StAX即Streaming API for XML,当前最有效的XML处理方法,因此特别适合于处理复杂流程,比如数据库绑定和SOAP消息。StAX创建的信息集是非常小,可以直接作为垃圾收集的候选对象。这让XML处理任务占用较小的空间,使得它不仅适用于小型堆设备,比如移动电话,而且适用于长期运行的服务器端应用程序。  与SAX不同,StAX能够对XML文档进行写操作,这减少了需要处理的API数量。  StAX提供两种不同的解析数据模型:光标模型和迭代器模型。

环境设置 为了使用StAX的解析器,应该准备好stax.jar在应用程序的类路径中。下载 stax-1.2.0.jar,以下是StAX API的功能: 1、读取XML文件从上到下,认识构成一个结构完整的XML文档的标记 2、令牌是以相同的顺序进行处理,它们出现在文档中 3、报告应用程序,因为解析器遇到标记的特性 4、应用程序提供了一个“事件”读取器充当了事件,以获得所需信息的迭代器和迭代。可另一个读取器是“光标”充当一个指向XML 节点。 5、由于事件被识别,XML元素可以从事件对象进行检索,并且可以进一步处理。

什么情况下使用?

应该使用的StAX解析器的时候:  1、可以处理在自上而下线性方式的XML文档。  2、文件并不深入嵌套。  3、处理一个非常大的XML文档的DOM树会占用太多的内存。典型的DOM的实现使用10字节的存储器以表示XML的一个字节。  4、要解决的问题涉及XML文档的一部分。  5、数据是可用的,只要它是由解析器处理,这样StAX可以很好地用于所收到超过数据流的XML文档。

SAX的缺点

1、因为它是在一个处理的方式,而不是随机访问XML文档。  2、如果需要跟踪的数据分析器已经看到或更改项目的顺序,必须编写代码和数据存储以自己方式处理。

XMLEventReader类

因为在解析XML文档时该类提供可用于迭代事件事件迭代器  1、StartElement asStartElement() - 用于检索值和元素的属性。  2、EndElement asEndElement() - 调用元件的端部。  3、Characters asCharacters() - 可用于获得字符,例如一个CDATA,空白等。

XMLEventWriter类

此接口指定创建事件的方法。  add(Event event) - 添加包含元素XML事件。

XMLStreamReader Class

因为在解析XML文档时该类提供可用于迭代事件事件迭代器  1、int next() - 用于检索下一个事件。  2、boolean hasNext() - 用于检查其他事件的存在与否  3、String getText() - 用于获取一个元素的文本  4、String getLocalName() - 用于获取一个元素的名称

XMLStreamWriter类

此接口指定创建事件的方法  1、writeStartElement(String localName) - 加入定名称开始元素。  2、writeEndElement(String localName) - 添加指定名称的结束元素。  3、writeAttribute(String localName, String value) - 编写属性到元素。

4.2、Java StAX解析器 - 解析XML文档

步骤1:创建stax.xml文件,文件内容如下:

<?xml version="1.0"?>
<class>
    <student rollno="393">
        <firstname>dinkar</firstname>
        <lastname>kad</lastname>
        <nickname>dinkar</nickname>
        <marks>85</marks>
    </student>
    <student rollno="493">
        <firstname>Vaneet</firstname>
        <lastname>Gupta</lastname>
        <nickname>vinni</nickname>
        <marks>95</marks>
    </student>
    <student rollno="593">
        <firstname>jasvir</firstname>
        <lastname>singn</lastname>
        <nickname>jazz</nickname>
        <marks>90</marks>
    </student>
</class>

步骤2:创建stax的Java解析类com.zpark.StAXParseFileDemo

package com.zpark;

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.*;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;

/**
 * @PackageName: com.zpark
 * @ClassName: StAXParseFileDemo
 * @Description:
 * @author: RZS
 * @date: 2021/8/15  20:01
 */
public class StAXParseFileDemo {
    public static void main(String[] args) throws Exception {
        // 获取xml文件所在路径
        String path = StAXParseFileDemo.class.getClassLoader().getResource("stax.xml").getPath();
        // 由于路径包含中文,所以需要进行解码
        path = URLDecoder.decode(path, "utf-8");

        boolean bFirstName = false;
        boolean bLastName = false;
        boolean bNickName = false;
        boolean bMarks = false;

        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLEventReader eventReader = factory.createXMLEventReader(new FileReader(path));

        while(eventReader.hasNext()){
            XMLEvent event = eventReader.nextEvent();
            switch(event.getEventType()){
                case XMLStreamConstants.START_ELEMENT:
                    StartElement startElement = event.asStartElement();
                    String qName = startElement.getName().getLocalPart();
                    if (qName.equalsIgnoreCase("student")) {
                        System.out.println("Start Element : student");
                        Iterator<Attribute> attributes = startElement.getAttributes();
                        String rollNo = attributes.next().getValue();
                        System.out.println("Roll No : " + rollNo);
                    } else if (qName.equalsIgnoreCase("firstname")) {
                        bFirstName = true;
                    } else if (qName.equalsIgnoreCase("lastname")) {
                        bLastName = true;
                    } else if (qName.equalsIgnoreCase("nickname")) {
                        bNickName = true;
                    }
                    else if (qName.equalsIgnoreCase("marks")) {
                        bMarks = true;
                    }
                    break;
                case XMLStreamConstants.CHARACTERS:
                    Characters characters = event.asCharacters();
                    if(bFirstName){
                        System.out.println("First Name: "
                                + characters.getData());
                        bFirstName = false;
                    }
                    if(bLastName){
                        System.out.println("Last Name: "
                                + characters.getData());
                        bLastName = false;
                    }
                    if(bNickName){
                        System.out.println("Nick Name: "
                                + characters.getData());
                        bNickName = false;
                    }
                    if(bMarks){
                        System.out.println("Marks: "
                                + characters.getData());
                        bMarks = false;
                    }
                    break;
                case  XMLStreamConstants.END_ELEMENT:
                    EndElement endElement = event.asEndElement();
                    if(endElement.getName().getLocalPart().equalsIgnoreCase("student")){
                        System.out.println("End Element : student");
                        System.out.println();
                    }
                    break;
            }
        }
    }
}

运行结果:

5、Java XPath解析器

5.1、Java XPath解析器简介

XPath是万维网联盟(W3C)的官方推荐。它定义了一个语言在XML文件中查找信息。它被用于遍历XML文档的元素和属性。 XPath提供各种类型,可用于从XML文档查询相关的信息表现形式。

什么是XPath?

结构定义 - XPath定义像元素,属性,文本,命名空间,处理指令,注释和文档节点的XML文档部分  路径表达式 - XPath提供了强大的路径表达式选择的节点或在XML文档中的节点列表。  标准功能 - XPath提供了丰富的标准函数库操纵字符串值,数值,日期和时间比较,节操作,顺序操作,布尔值等。  XSLT重要组成部分 - XPath是在XSLT标准的主要元素之一,是必须有知识,以便使用XSLT的文档。  W3C推荐 - XPath是万维网联盟(W3C)的官方推荐。

XPath表达式

XPath使用路径表达式从XML文档中选择一个或多个节点的列表。以下是有用的路径和表达,从XML文档选择节点的任何节点/列表清单。

表达式

描述

node-name

选择具有给定名称的所有节点“nodename”

/

选择从根节点开始

//

选择从当前节点匹配开始的选择

.

选择当前节点

..

选择当前节点的父节点

@

选择属性

student

例如:选择名称为“student”的所有节点

class/student

例如:选择属于类的所有学生的子类元素

//student

选择文档中所有学生的元素

实例

路径表达式

结果

bookstore

选取 bookstore 元素的所有子节点。

/bookstore

选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!

bookstore/book

选取属于 bookstore 的子元素的所有 book 元素。

//book

选取所有 book 子元素,而不管它们在文档中的位置。

bookstore//book

选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。

谓词

谓词用于查找特定的节点或一个节点含有特定的值,并使用所定义 [...] .

表达式

结果

/class/student[1]

选择的是类元素的子第一个学生的元素

/class/student[last()]

选择的是类元素的子最后一个学生的元素

/class/student[last()-1]

选择的是类元素倒数的第二个学生的子元素

//student[@rollno='493']

选择一个名为rollno为'493'值的属性的学生元素

5.2、Java XPath解析器 - 解析XML文档

步骤1:创建xpath.xml文件,文件内容如下:

<?xml version="1.0"?>
<class>
    <student rollno="393">
        <firstname>dinkar</firstname>
        <lastname>kad</lastname>
        <nickname>dinkar</nickname>
        <marks>85</marks>
    </student>
    <student rollno="493">
        <firstname>Vaneet</firstname>
        <lastname>Gupta</lastname>
        <nickname>vinni</nickname>
        <marks>95</marks>
    </student>
    <student rollno="593">
        <firstname>jasvir</firstname>
        <lastname>singn</lastname>
        <nickname>jazz</nickname>
        <marks>90</marks>
    </student>
</class>

步骤2:创建解析的Java类

package com.zpark;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;

/**
 * @PackageName: com.zpark
 * @ClassName: XPathParseXmlFileDemo
 * @Description: XPath解析器
 * @author: RZS
 * @date: 2021/8/15  20:46
 */
public class XPathParseXmlFileDemo {
    public static void main(String[] args) throws Exception {
        // 获取xml文件路径
        String path = XPathParseXmlFileDemo.class.getClassLoader().getResource("xpath.xml").getPath();
        // 由于路径中包含中文,所以需要对路径解析解码
        path = URLDecoder.decode(path, "utf-8");
        // 获取DocumentBuilderFactory对象
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        // 根据DocumentBuilderFactory对象,构建DocumentBuilder对象
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
        // 根据DocumentBuidler对象,构建Document对象
        Document document = documentBuilder.parse(path);

        // 构建XPath
        XPath xPath = XPathFactory.newInstance().newXPath();

        // 准备路径表达式,并计算它
        String expression = "/class/student";
        NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(document, XPathConstants.NODESET);

        // 遍历节点列表
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node nNode = nodeList.item(i);
            System.out.println("\nCurrent Element :"
                    + nNode.getNodeName());
            if (nNode.getNodeType() == Node.ELEMENT_NODE) {
                Element eElement = (Element) nNode;
                System.out.println("Student roll no : "
                        + eElement.getAttribute("rollno"));
                System.out.println("First Name : "
                        + eElement
                        .getElementsByTagName("firstname")
                        .item(0)
                        .getTextContent());
                System.out.println("Last Name : "
                        + eElement
                        .getElementsByTagName("lastname")
                        .item(0)
                        .getTextContent());
                System.out.println("Nick Name : "
                        + eElement
                        .getElementsByTagName("nickname")
                        .item(0)
                        .getTextContent());
                System.out.println("Marks : "
                        + eElement
                        .getElementsByTagName("marks")
                        .item(0)
                        .getTextContent());
            }
        }
    }
}

运行结果:

6、Java DOM4J解析器

6.1、Java DOM4J解析器解析

DOM4J是一个开源的,基于Java的库来解析XML文档,它具有高度的灵活性,高性能和内存效率的API。这是java的优化,使用Java集合像列表和数组。它可以使用DOM,SAX,XPath和XSLT。它解析大型XML文档时具有极低的内存占用。

环境设置 为了使用DOM4J解析器,应该 dom4j-1.6.1.jar 和 jaxen.jar 在应用程序的类路径中。下载 dom4j-1.6.1.zip.

什么情况下使用? 应该考虑使用DOM4J解析器的时候: 1、需要知道很多关于文档的结构 2、需要将文档的部分围绕(例如,可能需要某些元素进行排序) 3、需要使用的文件中的信息超过一次 4、你是一个Java开发人员,并希望利用XML的Java的优化解析。

会得到什么? 当解析一个DOM4J解析XML文档,可以灵活地得到一个树形结构,其中包含所有文档的元素,而不会影响应用程序的内存占用。DOM4J提供了多种可用于检查的情况下文档内容和结构的实用功能是良好的结构,其结构是公知的。 DOM4J使用XPath表达式来浏览XML文档。

优势 DOM4J使Java开发的灵活性和XML解析代码易于维护。它是轻量级的,快速的API。

DOM4J 类 DOM4J定义了几个Java类。以下是最常见的类:

说明

Document

表示整个XML文档。文档Document对象是通常被称为DOM树。

Element

表示一个XML元素。 Element对象有方法来操作其子元素,它的文本,属性和名称空间。

Attribute

表示元素的属性。属性有方法来获取和设置属性的值。它有父节点和属性类型。

Node

代表元素,属性或处理指令

常见DOM4J的方法 当使用DOM4J,还有经常用到的几种方法:

方法

方法说明

SAXReader.read(xmlSource)()

构建XML源的DOM4J文档。

Document.getRootElement()

得到的XML的根元素。

Element.node(index)

获得在元素特定索引XML节点。

Element.attributes()

获取一个元素的所有属性。

Node.valueOf(@Name)

得到元件的给定名称的属性的值。

6.2、Java DOM4J解析器 - 解析XML文档

步骤1:添加dom4j依赖

<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>

步骤2:添加dom4j.xml文件,文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<exam>
    <student examid="111" idcard="123">
        <name>张三</name>
        <location>北京</location>
        <age>100</age>
    </student>
    <student examid="444" idcard="333">
        <name>李四</name>
        <location>深圳</location>
        <age>97</age>
    </student>
</exam>

步骤3:创建pojo类用于存放解析的数据

package com.zpark.pojo;

public class Student {
    private String name;
    private String location;
    private Integer age;

   // get()/set()/toString()
}

步骤4:创建解析的Java类

package com.zpark;

import com.zpark.pojo.Student;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;

/**
 * @PackageName: com.zpark
 * @ClassName: DOM4JParseXmlFileDemo
 * @Description: 使用dom4j解析xml
 * @author: RZS
 * @date: 2021/8/15  21:25
 */
public class DOM4JParseXmlFileDemo {
    /*
     * 使用dom4j解析xml的大致步骤
     * 1.创建SAXReader
     * 2.使用SAXReader读取xml文档并且生成Document对象。
     *    这一步也是dom解析耗资源的地方,因为首先要先将文档所有数据读取完毕,并且以一个Document对象
     *    的形式保存在内存当中
     * 3.通过Document对象获取根元素
     * 4.按照xml文档的结构从根元素开始逐级遍历xml文档数据的目的
     */
    public static void main(String[] args) throws Exception {
        // 获取xml文件路径
        String path = DOM4JParseXmlFileDemo.class.getClassLoader().getResource("dom4j.xml").getPath();
        // 由于路径中包含中文,所以需要对路径解析解码
        path = URLDecoder.decode(path, "utf-8");
        // 定义集合,用于存放xml取出的student信息
        List<Student> list = new ArrayList<Student>();
        SAXReader reader = new SAXReader();
        /*
         * Document提供了获取根元素的方法:Element getRootElement()
         *
         * 而element的每一个实例用于表示当前xml文件的一个元素(一对标签),它提供了获取
         * 其元素相关的方法:
         * 获取当前标签的名字:String getName()
         * 获取当前标签中间的文本:String getText()
         * 获取当前标签下指定名字的子标签:Element element(String name)
         * 获取当前标签下的所有子标签:List elements()
         * 获取当前标签下指定名字的子标签:List elements(String name)
         *
         * 获取当前标签下指定名字标签的属性:Attribute attribute(String name)
         * Attribute的每一个实例表示一个属性,它有两个方法:
         * 获取属性名:String getName()
         * 获取属性值:String getValue()
         */
        Document document = reader.read(path);
        // 获取根元素
        Element rootElement = document.getRootElement();
        // 获取根元素下的所有子标签
        List<Element> elements = rootElement.elements();
        for (Element element : elements) {
            Student student = new Student();
            // 将获取回来的值添加到Student对象中
            student.setName(element.elementText("name"));
            student.setAge(Integer.parseInt(element.elementText("age")));
            student.setLocation(element.elementText("location"));
            // 将student对象存入集合
            list.add(student);
        }
        System.out.println(list);
    }
}

运行结果:

使用DOM4J创建xml文件

package com.zpark;

import com.zpark.pojo.Student;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.XMLWriter;

import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @PackageName: com.zpark
 * @ClassName: DOM4JCreateXmlFileDemo
 * @Description: 使用DOM4J生成xml文件
 * @author: RZS
 * @date: 2021/8/15  22:00
 */
public class DOM4JCreateXmlFileDemo {
    /**
     * 生成xml文件
     * 其大致步骤为:
     *      1.创建一个Document对象,表示一个空白文档
     *      2.向Document中添加根元素
     *      3.按照xml文档结构从根标签开始逐级添加子标签以及对应的数据
     *      4.创建XmlWrite
     *      5.使用XmlWrite写出Document,生成文档
     */
    public static void main(String[] args) throws Exception {
        // 准备需要写入的数据
        List<Student> list = new ArrayList<Student>();
        list.add(new Student("大锤", "北京", 26));
        list.add(new Student("张三丰", "北京", 140));
        list.add(new Student("金轮法王", "西藏", 59));
        list.add(new Student("黄蓉", "杭州", 26));
        list.add(new Student("郭靖", "蒙古", 32));

        // 1.创建一个Document对象,表示一个空白文档
        Document document = DocumentHelper.createDocument();
        // 2.向Document中添加根元素, students表示根标签
        /**
         * Document提供了添加根元素的方法:
         * Element addElement(String name)
         * 添加后会根据标签以一个Element实例返回,以便于我们对其继续操作,
         * 注意,这个方法只能调用一次
         *
         * Element也提供了添加相关信息的方法:
         * Element addElement(String name),向当前标签中添加给定标签名的子标签
         *
         * Element addText(String text),向当前标签中添加指定的文本,返回的还是当前标签
         * (这样做的好处是 ,可以继续添加其他内容)
         */
        Element addElement = document.addElement("students");
        // 3.按照xml文档结构从根标签开始逐级添加子标签以及对应的数据
        for (Student student : list) {
            // 添加相应的子标签对应的属性和值
            Element element = addElement.addElement("student");// 添加student标签
            // 向student标签下添加子标签和值
            element.addElement("name").addText(student.getName());
            element.addElement("location").addText(student.getLocation());
            element.addElement("age").addText(student.getAge()+"");
        }
        /*
         * XMLWriter符合Java高级流的用法,它负责将Document对象以xml文档格式的形式写入到文件当中
         */
        XMLWriter xmlWriter = new XMLWriter(new FileOutputStream("Java-DOM4J解析/src/main/resources/students.xml"));
        xmlWriter.write(document);
        xmlWriter.close();

    }
}

student文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<students>
    <student>
        <name>大锤</name>
        <location>北京</location>
        <age>26</age>
    </student>
    <student>
        <name>张三丰</name>
        <location>北京</location>
        <age>140</age>
    </student>
    <student>
        <name>金轮法王</name>
        <location>西藏</location>
        <age>59</age>
    </student>
    <student>
        <name>黄蓉</name>
        <location>杭州</location>
        <age>26</age>
    </student>
    <student>
        <name>郭靖</name>
        <location>蒙古</location>
        <age>32</age>
    </student>
</students>

7、总结

XML解析方式分为两种:dom和sax dom:(Document Object Model, 即文档对象模型) 是 W3C 组织推荐的处理 XML 的一种方式。 sax: (Simple API for XML) 不是官方标准,但它是XML 社区事实上的标准,几乎所有的 XML 解析器都支持它。

XML解析器 Crimson、Xerces 、Aelfred2 XML解析开发包 Jaxp、Jdom、dom4j

1、DOM解析 DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作。由于它是基于信息层次的,因而DOM被认为是基于树或基于对象的。 DOM解析器把XML文档转化为一个包含其内容的树,并可以对树进行遍历。 DOM是拉模型,在遍历文档时,会把感兴趣的部分从读取器中拉出,不需要引发事件,允许我们选择性地处理节点。这大大提高了灵活性,以及整体效率。

2、DOM的基本对象有5个:Document,Node,NodeList,Element和Attr。 2.1、Document对象 代表了整个XML的文档,所有其它的Node,都以一定的顺序包含在Document对象之内,排列成一个树形的结构,程序员可以通过遍历这颗树来得到XML文档的所有的内容,这也是对XML文档操作的起点。我们总是先通过解析XML源文件而得到一个Document对象,然后再来执行后续的操作。此外,Document还包含了创建其它节点的方法,比如createAttribut()用来创建一个Attr对象。它所包含的主要的方法有: createAttribute(String):用给定的属性名创建一个Attr对象,并可在其后使用setAttributeNode方法来放置在某一个Element对 象上面。 createElement(String):用给定的标签名创建一个Element对象,代表XML文档中的一个标签,然后就可以在这个Element对象 上添加属性或进行其它的操作。 createTextNode(String):用给定的字符串创建一个Text对象,Text对象代表了标签或者属性中所包含的纯文本字符串。如果在 一个标签内没有其它的标签,那么标签内的文本所代表的Text对象是这个Element对象的唯一子对象。 getElementsByTagName(String):返回一个NodeList对象,它包含了所有给定标签名字的标签。 getDocumentElement():返回一个代表这个DOM树的根节点的Element对象,也就是代表XML文档根元素的那个对象。

2.2、Node对象 是DOM结构中最为基本的对象,代表了文档树中的一个抽象的节点。在实际使用的时候,很少会真正的用到Node这个对象,而是用到诸如Element、Attr、Text等Node对象的子对象来操作文档。Node对象为这些对象提供了一个抽象的、公共的根。虽然在Node对象中定义了对其子节点进行存取的方法,但是有一些Node子对象,比如Text对象,它并不存在子节点,这一点是要注意的。Node对象所包含的主要的方法有: appendChild(org.w3c.dom.Node):为这个节点添加一个子节点,并放在所有子节点的最后,如果这个子节点已经存在,则先 把它删掉再添加进去。 getFirstChild():如果节点存在子节点,则返回第一个子节点,对等的,还有getLastChild()方法返回最后一个子节点。 getNextSibling():返回在DOM树中这个节点的下一个兄弟节点,对等的,还有getPreviousSibling()方法返回其前一个兄弟节 点。 getNodeName():根据节点的类型返回节点的名称。 getNodeType():返回节点的类型。 getNodeValue():返回节点的值。 hasChildNodes():判断是不是存在有子节点。 hasAttributes():判断这个节点是否存在有属性。 getOwnerDocument():返回节点所处的Document对象。 insertBefore(org.w3c.dom.Node new,org.w3c.dom.Node ref):在给定的一个子对象前再插入一个子对象。 removeChild(org.w3c.dom.Node):删除给定的子节点对象。 replaceChild(org.w3c.dom.Node new,org.w3c.dom.Node old):用一个新的Node对象代替给定的子节点对象。

2.3、NodeList对象 顾名思义,就是代表了一个包含了一个或者多个Node的列表。可以简单的把它看成一个Node的数组,我们可以通过方法来获得列表中的元素: getLength():返回列表的长度。 item(int):返回指定位置的Node对象。

2.4、Element对象 代表的是XML文档中的标签元素,继承于Node,亦是Node的最主要的子对象。在标签中可以包含有属性,因而Element对象中有存取其属性的方法,而任何Node中定义的方法,也可以用在Element对象上面。 getElementsByTagName(String):返回一个NodeList对象,它包含了在这个标签中其下的子孙节点中具有给定标签名字的标 签。 getTagName():返回一个代表这个标签名字的字符串。 getAttribute(String):返回标签中给定属性名称的属性的值。在这儿需要主要的是,应为XML文档中允许*有实体属性出现,而 这个方法对这些实体属性并不适用。这时候需要用到getAttributeNodes()方法来得到一个Attr对象来进行进一步的操作。 getAttributeNode(String):返回一个代表给定属性名称的Attr对象。

2.5、Attr对象 代表了某个标签中的属性。Attr继承于Node,但是因为Attr实际上是包含在Element中的,它并不能被看作是Element的子对象,因而在DOM中Attr并不是DOM树的一部分,所以Node中的getParentNode(),getPreviousSibling()和getNextSibling()返回的都将是null。也就是说,Attr其实是被看作包含它的Element对象的一部分,它并不作为DOM树中单独的一个节点出现。这一点在使用的时候要同其它的Node子对象相区别。

3、SAX解析 SAX是Simple API forXML的缩写,它并不是由W3C官方所提出的标准,可以说是“民间”的事实标准。实际上,它是一种社区性质的讨论产物。虽然如此,在XML中对SAX的应用丝毫不比DOM少,几乎所有的XML解析器都会支持它。 与DOM比较而言,SAX是一种轻量型的方法。我们知道,在处理DOM的时候,我们需要读入整个的XML文档,然后在内存中创建DOM树,生成DOM树上的每个Node对象。当文档比较小的时候,这不会造成什么问题,但是一旦文档大起来,处理DOM就会变得相当费时费力。特别是其对于内存的需求,也将是成倍的增长,以至于在某些应用中使用DOM是一件很不划算的事(比如在applet中)。这时候,一个较好的替代解决方法就是SAX。 SAX在概念上与DOM完全不同。首先,不同于DOM的文档驱动,它是事件驱动的,也就是说,它并不需要读入整个文档,而文档的读入过程也就是SAX的解析过程。所谓事件驱动,是指一种基于回调(callback)机制的程序运行方法。(如果你对Java新的代理事件模型比较清楚的话,就会很容易理解这种机制了) 回调:由我们在组件中定义,而不由我们调用,由容器或框架调用 SAX是推模型,它是一种靠事件驱动的模型。当它每发现一个节点就引发一个事件,而我们需要编写这些事件的处理程序。这样的做法很麻烦,且不灵活。

一、StAX 解析 针对于XML的流式API(StAX),是在2004年3月的JSR 173规范中引入,这是一种针对XML的流式拉分析API。StAX是JDK 6.0提供的一种新特征。 一个推模型分析器不断地生成事件,直到XML文档被完全分析结束。但是,拉分析由应用程序进行调整;因此,分析事件是由应用程序生成的。这意味着,使用StaX,你可以推迟分析-在分析时跳过元素并且分析多个文档。在使用DOM API的时候,你必须把整个的XML文档分析成一棵DOM结构,这样也就降低了分析效率。而借助于StAX,在分析XML文档时生成分析事件。

二、DOM4J 虽然DOM4J代表了完全独立的开发结果,但最初,它是JDOM的一种智能分支。它合并了许多超出基本XML文档表示的功能,包括集成的XPath支持、 XML Schema支持以及用于大文档或流化文档的基于事件的处理。它还提供了构建文档表示的选项,它通过DOM4J API和标准DOM接口具有并行访问功能。从2000下半年开始,它就一直处于开发之中。 为支持所有这些功能,DOM4J使用接口和抽象基本类方法。DOM4J大量使用了API中的Collections类,但是在许多情况下,它还提供一些替 代方法以允许更好的性能或更直接的编码方法。直接好处是,虽然DOM4J付出了更复杂的API的代价,但是它提供了比JDOM大得多的灵活性。 在添加灵活性、XPath集成和对大文档处理的目标时,DOM4J的目标与JDOM是一样的:针对Java开发者的易用性和直观操作。它还致力于成为比 JDOM更完整的解决方案,实现在本质上处理所有Java/XML问题的目标。在完成该目标时,它比JDOM更少强调防止不正确的应用程序行为。 DOM4J是一个非常非常优秀的JavaXML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一个开放源代码的软件。如今你可以看到越来越多的Java软件都在使用DOM4J来读写XML,特别值得一提的是连Sun的JAXM也在用DOM4J.

3、DOM与SAX的区别

一、DOM:拉模型,把整个文档加载到内存中 优点:整个文档树在内存中,便于操作;支持删除、修改、重新排列等多种功能; 缺点:将整个文档调入内存(包括无用的节点),浪费时间和空间; 使用场合:一旦解析了文档还需多次访问这些数据;硬件资源充足(内存、CPU)

二、SAX:推模型,事件驱动编程,基于回调SAX ,事件驱动。当解析器发现元素开始、元素结束、文本、文档的开始或结束等时,发送事件,程序员编写响应这些事件的代码,保存数据。 优点:不用事先调入整个文档,占用资源少; 缺点:不是持久的;事件过后,若没保存数据,那么数据就丢了;无状态性;从事件中只能得到文本,但不知该文本属于哪个元素; 使用场合:数据量较大的XML文档,占用内存高,机器内存少,无法一次加载XML到内存;只需XML文档的少量内容,很少回头访问;

性能比较 1)DOM4J性能最好,连Sun的JAXM也在用DOM4J.目前许多开源项目中大量采用DOM4J,例如大名鼎鼎的hibernate也用DOM4J来读取XML配置文件。如果不考虑可移植性,那就采用DOM4J. 2)SAX表现较好,这要依赖于它特定的解析方式-事件驱动。一个SAX检测即将到来的XML流,但并没有载入到内存(当然当XML流被读入时,会有部分文档暂时隐藏在内存中)。

一、设计模式的分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

其实还有两类:并发型模式和线程池模式。

二、设计模式的六大原则

1、开闭原则(Open Close Principle)

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

2、里氏代换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科

3、依赖倒转原则(Dependence Inversion Principle)

这个是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

5、迪米特法则(最少知道原则)(Demeter Principle)

为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

6、合成复用原则(Composite Reuse Principle)

原则是尽量使用合成/聚合的方式,而不是使用继承。

认知范围:Java23设计模式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值