【无标题】

标题Java笔记

Java是一门面向对象的编程语言,而C语言是一个面向过程的语言,Java语言具有功能强大和简单易用两个特征,Java是静态面向对象编程语言代表,Java具有简单性、面向对象、分布式、健壮性、安全性、平台独立与可移植性、多线程、动态性等特点。
Java是强类型的语言,必须先定义,后使用;指定类型的变量只能接受类型与之匹配的值。

Java入门及环境搭建
Java分为三个体系:JavaSE、JavaEE、JavaME
Java还是一个平台,Java平台有虚拟机和Java应用编程接口构成。

Java是一种面向对象的语言,它对对象中的类、对象、继承、封装、多态、接口、包装均有很好的支持。
为了简单,Java只支持类之间的单继承,但是可以使用接口来进行多继承。

Package com.zpark;
//定义类
Public class HelloWord {
//定义程序入口(主函数)
Public static void main(String arg[]{
System.out.println(“Hello word java”) ;
}
}

java源程序可以使用文本编译器编写:记事本
保存文件后修改文件后缀为.java
打开cmd,输入文件名.java继续编译
运行编译的.class文件,输入Java+文件名

Java命名规则
编程规范是对编程的一种约定,主要作用是增强代码的可读性和可维护性,便于代码重用。
Java类的命名规则:
类的首字母大写,如果由多个单词组成,则每个单词首字母大写(大驼峰命名法) 如:HelloWorld Demo01
首个单词的首字母小写,后面的单词首字母大写 (小驼峰命名法) 如:helloWorld dayMyYou

常量:是形式化的表现形式
常量值:是常量的具体和直观的表现形式;
常量值的分类:
整型常量值:十进制(12) 、 八进制(0125) 、十六进制(0x345)
实型常量值:十进制(12.56) 、 科学计数法(1.75e5 或 32E8)
布尔型常量值:只有两个值 true 和 false;

常量命名规则:
1.常量名全用大写;(如:PI , PRICE)
2.当常量名由多个单词组成时采用下划线分割。(如:HTTP_CODE, HTTP_STATUS_CODE)

标识符:由数字、字母、下划线(-)及$组成。如:d12abc;
数字不能作为标识符的开始。如:12as;
用户自定义标识符:是由用户按标识符的规则生成的非保留字的标识符。如:abc , name 就是一个标识符, myint , MString , intString(也是标识符)。

基本数据类型(八种)
byte 字节型(-128–127)、
short 短整型 (-32768–32767) 、int 整型 、 long 长整型 、
float 单精度浮点型 (6–7个有效位)
double 双精度浮点型 (15个有效位) 、 char 字符型 、
Boolean 布尔型

变量的作用域:
全局变量:定义在方法(函数)和代码块之外的变量;
局部变量:定义在方法或者代码块之间的变量。

整形常量默认在内存中占32位
长整形类型在内存占64位
常量一但被赋值则不可再被修改

引用数据类型:包括 数组 、类 和 接口
除了基本数据类型,其他全部是引用数据类型 如:String 、数组 等。
null类型:null可以转换任何引用类型
引用数据类型
引用数据类型建立在基本数据类型的基础上,包括数组、类和接口。引用数据类型是由用户自定义,用来限制其他数据的类型。
数据类型的转换可以分为隐式转换(自动类型转换)和显式转换(强制类型转换)两种。
如果以下 2 个条件都满足,那么将一种类型的数据赋给另外一种类型变量的时,将执行自动类型转换(automatic type conversion)。 ​ 1、两种数据类型彼此兼容 ​ 2、目标类型的取值范围大于源数据类型(低级类型数据转换成高级类型数据)

byte 1字节;short 2字节; char 2字节; int 4字节;
long 8字节; float 4字节; double 8字节;
类型转换:1、小类型数据转大类型数据(自动转换)
byte–>short–>int–>long–>float–>double
char
2、大类型数据转小类型数据(强制转换)
强制转换方法:{小类型 变量名 = (需要转换的类型)大类型}
注意:大类型转换为小类型数据可能造成数据进度丢失和溢出。
Double–>float–>long–>int–>short–>byte
Char
short和char互相强转

引用数据类型和基本数据类型不能转换,两种数据类型彼此兼容,否则不能互相转换,
引用数据类型中,除非是继承关系,否则是平等关系无大小之分
数据之间进行运算,结果会往大类型转换
Java算术运算符
一元运算符:
-:取反符合;
++:自加一(先取值再加一);
–:自减一(先取值再减一);
-a 是对 a 取反运算,a++ 或 a-- 是在表达式运算完后,再给 a 加一或减一。而 ++a 或 --a 是先给 a 加一或减一,然后再进行表达式运算。

二元运算符:
+:加,求a加b的和,还可用于String的类型,进行字符串连接操作
-:减,求 a 减 b 的差
*:乘,求 a 乘以 b 的积
/:除,求 a 除以 b 的商
%:取余,求 a 除以 b 的余数

赋值运算符:
+=:加赋值
-=:减赋值
*=:乘赋值
=:出赋值
%=:取余赋值

Java逻辑运算符:
&&:短路与,ab 全为 true 时,计算结果为 true,否则为 false
||:短路或,ab 全为 false 时,计算结果为 false,否则为 true
!:逻辑非,a 为 true 时,值为 false,a 为 false 时,值为 true
|:逻辑或,ab 全为 false 时,计算结果为 false,否则为 true
&:逻辑与,ab 全为 true 时,计算结果为 true,否则为 false
· && 与 & 区别:如果 a 为 false,则不计算 b(因为不论 b 为何值,结果都为 false
· || 与 | 区别:如果 a 为 true,则不计算 b(因为不论 b 为何值,结果都为 true)

关系运算符:

: 只支持左右两边操作数是数值类型。如果前面变量的值大于后面变量的值, 则返回 true
=:只支持左右两边操作数是数值类型。如果前面变量的值大于等于后面变量的值, 则返回 true
<: 只支持左右两边操作数是数值类型。如果前面变量的值小于后面变量的值,则返回 true。
<=:只支持左右两边操作数是数值类型。如果前面变量的值小于等于后面变量的值, 则返回 true。
==:如果进行比较的两个操作数都是数值类型,无论它们的数据类型是否相同,只要它们的值相等,也都将返回 true。 如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同一个对象就会返回 true。 Java 也支持两个 boolean 类型的值进行比较。
!=:如果进行比较的两个操作数都是数值类型,无论它们的数据类型是否相同,只要它们的值不相等,也都将返回 true。 如果两个操作数都是引用类型,只有当两个引用变量的类型具有父子关系时才可以比较,只要两个引用指向的不是同一个对象就会返回 true

自增和自减运算符:
i++:将 i 的值先使用再加 1 赋值给 i 变量本身
++i:将 i 的值先加 1 赋值给变量 i 本身后再使用
i–:将 i 的值先使用再减 1 赋值给变量 i 本身
–i:将 i 的值先减 1 后赋值给变量 i 本身再使用

& 按位进行与运算(AND)
| 按位进行或运算(OR)
^ 按位进行异或运算(XOR)
~ 按位进行取反运算(NOT)
位与运算符为&,其运算规则是:参与运算的数字,低位对齐,高位不足的补零,如果对应的二进制位同时为 1,那么计算结果才为 1,否则为 0。

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

equals(obj)方法和==的区别:
==比较的是两个对象在内存中存放的地址是否为同一个
equals(obj)比较的是内容是否相同

next()与nextLine()方法的区别:
1、当遇到空格或者回车的时候next()方法停止读取
2、当遇到回车的时候nextLink()停止读取,读取整行数据

位移运算符:
» 右移位运算符
« 左移位运算符
左移位运算符为«,其运算规则是:按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。
右位移运算符为»,其运算规则是:按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补零。

运算符优先级:
1 ()、[]、{}
2 !、+、-、~、++、–
3 、/、%
4 +、-
5 «、»、>>>
6 <、<=、>、>=、instanceof
7 ==、!=
8 &
9 ^
10 |
11 &&
12 ||
13 ?:
14 =、+=、-=、
=、/=、&=、|=、^=、~=、«=、»=、>>>=

直接量类型:
1)int 类型的直接量
在程序中直接给出的整型数值,可分为二进制、十进制、八进制和十六进制 4 种,其中二进制需要以 0B 或 0b 开头,八进制需要以 0 开头,十六进制需要以 0x 或 0X 开头。例如 123、012(对应十进制的 10)、0x12(对应十进制的 18)等。
2)long 类型的直接量
在整型数值后添加 l 或 L 后就变成了 long 类型的直接量。例如 3L、0x12L(对应十进制的 18L)。
3)float 类型的直接量
在一个浮点数后添加 f 或 F 就变成了 float 类型的直接量,这个浮点数可以是标准小数形式,也可以是科学计数法形式。例如 5.34F、3.14E5f。
4)double 类型的直接量
直接给出一个标准小数形式或者科学计数法形式的浮点数就是 double 类型的直接量。例如 5.34、3.14E5。
5)boolean 类型的直接量
这个类型的直接量只有 true 和 false。
6)char 类型的直接量
char 类型的直接量有三种形式,分别是用单引号括起来的字符、转义字符和 Unicode 值表示的字符。例如‘a’,‘\n’和‘\u0061’。
7)String 类型的直接量
一个用双引号括起来的字符序列就是 String 类型的直接量。
在大多数其他语言中,包括 C/C++,字符串作为字符的数组被实现。然而,在 Java 中并非如此。在 Java 中,字符串实际上是对象类型。在教程后面你将看到,因为 Java 对字符串是作为对象实现的,因此,它有广泛的字符串处理能力,而且功能既强又好用。
8)null 类型的直接量
这个类型的直接量只有一个值,即 null。
在上面的 8 种类型的直接量中,null 类型是一种特殊类型,它只有一个值:null。而且这个直接量可以赋给任何引用类型的变量,用以表示这个引用类型变量中保存的地址为空,即还未指向任何有效对象。

Java扫描器:用户从控制台输入数据,后台java程序接收
扫描器:(Scanner)
编译期:是指把源码交给编译器编译成计算机可以执行的文件的过程。在Java中也就是把Java代码编成class文件的过程.编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。
运行期:是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来,在Java中把磁盘中的代码放到内存中就是类加载过程,类加载是运行期的开始部分。

Java语句格式
1、语句编写方式
在 Java 中,语句是最小的组成单位,每个语句必须使用分号作为结束符。
2、空语句
3、表达式语句
4、复合语句
它的执行规则如下:
1、如果语句块是空的,控制转到语句块的结束点。
2、如果语句块不是空的,控制转到语句列表。当控制到达语句列表的结束点时,控制转到语句的结束点。
if else语句
1、if结构
if 语句是使用最多的条件分支结构,它属于选择语句,也可以称为条件语句。if 选择结构是根据条件判断之后再做处理的一种语法结构。默认情况下,if 语句控制着下方紧跟的一条语句的执行。不过,通过语句块,if 语句可以控制多个语句。

条件表达式:条件表达式可以是任意一种逻辑表达式,最后返回的结果必须是一个布尔值。取值可以是一个单纯的布尔变量或常 量,也可以是使用关系或布尔运算符的表达式。如果条件为真,那么执行语句块;如果条件为假,则语句块将被绕过而不被执行。 ​ 语句块:该语句块可以是一条语句也可以是多条语句。如果仅有一条语句,可省略条件语句中的大括号 {}。当从编程规范角度不 要省略大括号,省略大括号会使程序的可读性变差。
2、if-else结构
单 if 语句仅能在满足条件时使用,而无法执行任何其他操作(停止)。而结合 else 语句的 if 可以定义两个操作,此时的 if…else 语句表示“如果条件正确则执行一个操作,否则执行另一个操作”。
3、多条件if-else-if语句
if 语句的主要功能是给程序提供一个分支。有时候程序中仅仅多一个分支是远远不够的,甚至有时候程序的分支会很复杂,这就需要使用多分支的 if…else if 语句。
if(表达式1) {
语句块1;
} else if(表达式2) {
语句块2;

} else if(表达式n) {
语句块n;
} else {
语句块n+1;
}
4、嵌套if的使用
if(表达式1) {
if(表达式2) {
语句块1;
} else {
语句块2;
}
} else {
if(表达式3) {
语句块3;
} else if(表达式4) {
语句块4;
} else {
if(表达式n) {
语句块n;
} else {
语句块n+1;
}
}
}

Switch case语句
1、switch语句格式
switch 语句是 Java 的多路分支语句。它提供了一种基于一个表达式的值来使程序执行不同部分的简单方法。
1.1、switch
表示“开关”,这个开关就是 switch 关键字后面小括号里的值,小括号里要放一个整型变量或字符型变量。表达式必须为 byte,short,int,char类型。
Java7 增强了 switch 语句的功能,允许 switch 语句的控制表达式是 java.lang.String 类型的变量或表达式。只能是 java.lang.String 类型,不能是 StringBuffer 或 StringBuilder 这两种字符串的类型。
1.2、case
表示“情况,情形”,case 标签可以是: 1、类型为 char、byte、 short 或 int 的常量表达式。 2、枚举常量。 3、从 Java SE 7 开始, case 标签还可以是字符串字面量。
注意:重复的 case 值是不允许的。
1.3、default
表示“默认”,即其他情况都不满足。default 后要紧跟冒号,default 块和 case 块的先后顺序可以变动,不会影响程序执行结果。通常,default 块放在末尾,也可以省略不写。
1.4、break
表示“停止”,即跳出当前结构。
2、嵌套switch语句
可以将一个 switch 语句作为一个外部 switch 语句的语句序列的一部分,这称为嵌套 switch 语句。
1.1、switch 语句不同于 if 语句的是 switch 语句仅能测试相等的情况,而 if 语句可计算任何类型的布尔表达式。也就是 switch 语 句只能寻找 case 常量间某个值与表达式的值相匹配。
2.1、在同一个 switch 语句中没有两个相同的 case 常量。当然,外部 switch 语句中的 case 常量可以和内部 switch 语句中的 case 常量相同。 ​ 3、switch 语句通常比一系列嵌套 if 语句更有效。
3、if语句和switch语句的区别
if 和 switch 语句都表示条件语句,可以从使用效率和实用性两方面加以区分。

  1. 从使用效率上区分
    从使用效率上区分,在对同一个变量的不同值作条件判断时,既可以使用 switch 语句,也可以使用 if 语句。使用 switch 语句的效率更高一些,尤其是判断的分支越多,越明显。
  2. 从实用性上区分
    从语句的实用性角度区分,switch 语句不如 if 条件语句,if 语句是应用最广泛和最实用的语句。
  3. 何时使用 if 语句和 switch 语句
    在程序开发的过程中,何时使用 if 语句和 switch 语句,需要根据实际情况而定,应尽量做到物尽其用。不能因为 switch 语句的效率高就一直使用,也不能因为 if 语句常用就不用 switch 语句。需要根据实际情况,具体问题具体分析,使用最适合的条件语句。
    一般情况下,对于判断条件较少的,可以使用 if 条件语句,但是在实现一些多条件的判断中,最好使用 switch 语句。
    while和do while循环
    循环语句可能包含如下 4 个部分。 ​ 初始化语句(init statement): 一条或多条语句,这些语句用于完成一些初始化工作,初始化语句在循环开始之前执行。 ​ 循环条件(test_expression):这是一个 boolean 表达式,这个表达式能决定是否执行循环体。 ​ 循环体(body_statement):这个部分是循环的主体,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一 行语句,则这个代码块的花括号是可以省略的。 ​ 迭代语句(iteration_statement):这个部分在一次循环体执行结束后,对循环条件求值之前执行,通常用于控制循环条件中 的变量,使得循环在合适的时候结束。
    1、while语句
    while 语句是 Java 最基本的循环语句,是一种先判断的循环结构,可以在一定条件下重复执行一段代码。
    2、do while语句
    如果 while 循环一开始条件表达式就是假的,那么循环体就根本不被执行。
    3、while和do while的比较
    while 循环和 do-while 循环的相同处是:都是循环结构,使用 while(循环条件) 表示循环条件,使用大括号将循环操作括起来。
    while 循环和 do-while 循环的不同处如下: ​ 语法不同:与 while 循环相比,do-while 循环将 while 关键字和循环条件放在后面,而且前面多了 do 关键字,后面多了一个分 号。 ​ 执行次序不同:while 循环先判断,再执行。do-while 循环先执行,再判断。 ​ 一开始循环条件就不满足的情况下,while 循环一次都不会执行,do-while 循环则不管什么情况下都至少执行一次。

for循环
for 语句是应用最广泛、功能最强的一种循环语句。大部分情况下,for 循环可以代替 while 循环、do while 循环。
for循环嵌套
如果循环层次超过三层,就是逻辑出现问题。
foreach语句的用法
foreach 循环语句是 Java 1.5 的新特征之一,在遍历数组、集合方面,foreach 为开发者提供了极大的方便。foreach 循环语句是 for 语句的特殊简化版本,主要用于执行遍历功能的循环。
break语句
某些时候需要在某种条件出现时强行终止循环,而不是等到循环条件为 false 时才退出循环。此时,可以使用 break 来完成这个功能。break 用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到 break,系统将完全结束该循环,开始执行循环之后的代码。
Break goto语句
注意:通常紧跟 break 之后的标签,必须在 break 所在循环的外层循环之前定义才有意义。
在 switch 语句中终止一个语句序列
在 switch 语句中终止一个语句序列,就是在每个 case 子句块的最后添加语句“break;”
Continue语句
continue 语句是跳过循环体中剩余的语句而强制执行下一次循环,其作用为结束本次循环,即跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判定。
continue 语句类似于 break 语句,但它只能出现在循环体中。它与 break 语句的区别在于:continue 并不是中断循环语句,而是中止当前迭代的循环,进入下一次的迭代。简单来讲,continue 是忽略循环语句的当次循环。
注意:continue 语句只能用在 while 语句、for 语句或者 foreach 语句的循环体之中,在这之外的任何地方使用它都会引起语法错误。
在循环体中使用 continue 语句有两种方式可以带有标签,也可以不带标签。

选择结构和循环结构
Java 提供了 if 和 switch 两种分支语句,并提供了 while、do while 和 for 三种循环语句。
一般写循环语句时,分以下三步走:
1、定义初始值
2、设置判断条件
3、初始值变化

方法的定义和调用
首先方法包含一个方法头和一个方法体。下面是一个方法的所有部分:
修饰符:修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型。
返回值类型 :方法可能会返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需的操作,但没有返回值。在这种情况下,returnValueType 是关键字void。
方法名:是方法的实际名称。方法名和参数表共同构成方法签名。
参数类型:参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。
方法体:方法体包含具体的语句,定义该方法的功能。
方法的作用及特点:
1、封装一段特定的业务逻辑功能
2、尽可能独立,一个方法只干一件事
3、方法可以被反复多次的调用
4、减少代码的重复,有利于代码的维护,有利于团队的协作
方法调用:
调用方法:对象名.方法名(实参列表),类名.方法名()。
Java支持两种调用方法的方式,根据方法是否有返回值来选择:
(1)当方法有返回值时,方法调用通常被当作一个值
(2)如果方法返回值类型是void时,方法调用一定是一条语句

方法的重载
重载就是在一个类中,有相同的函数名称,但参数列表不相同的方法。
方法重载的规则:
方法名字必须相同
参数列表必须不同(参数个数不同、类型不同、参数排列顺序不同等)
方法的返回值可以相同,也可以不同(方法的重载与返回值类型无关)。
仅仅只是返回类型不同的话,则不足以称为方法的重载。
方法签名:
方法名+参数列表
重载方法调用规则:
编译器在编译时会根据方法的签名自动绑定调用的方法
可变参数
Java从JDK 1.5开始支持传递同类型的可变参数给一个方法。在方法声明中,通过给知道参数类型后面加一个省略号,从而达到传递可变参数的效果。一个方法中只能指定一个可变参数,并且它必须位于参数列表的最后一位,其余的普通参数都必须在其之前声明。
递归算法:在函数或子过程的内部,直接或者间接地调用自己的算法。
递归算法解决问题的特点:  
(1)递归就是在过程或函数里调用自身。  
(2)在使用递增归策略时,必须有一个明确的递归结束条件,称为递归出口。  
(3)递归算法解题通常显得很简洁,但递归算法解题的运行效率较低。所以一般不提倡用递归算法设计程序。  
(4) 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储。递归次数过多容易造成栈溢出等。所以一般不提倡用递归算法设计程序。
递归的两个条件:
1、可以通过递归调用来缩小问题规模,且新问题与原问题有着相同的形式。(自身调用)
2、存在一种简单情境,可以使递归在简单情境下退出。(递归出口)
递归三要素:
1、一定有一种可以退出程序的情况;
2、总是在尝试将一个问题化简到更小的规模
3、父问题与子问题不能有重叠的部分

数组的声明
Int[]arr;
Int arr[];
//静态创建数组,特点:创建数组同时赋值
int arr[] = {1,2,4,8,9}
//动态创建数组,特点创建数组时指定数组长度
Long[] arr = new long[5];
数组的四个基本特点:
1、 数组一旦被创建,它的大小将不可以在被改变。
2、 数组元素必须是相同类型的,不允许出现混合类型。
3、 数组中的元素可以是任何类型,包括基本类型与引用类型。
4、 数组属于引用类型,数组可以看成是一个对象,数组中的每一个元素相当于该对象的成员变量,因为数组本身就是对象,Java中对象是存放在堆中的,因此数组无论保存原始类型还是其他类型,数组对象本身都是在堆中。
数组三种初始化
1.静态初始化
2.动态初始化
3.数组的默认初始化
数组的使用
遍历数组:
遍历数组的意思就是将数组的元素逐个取出来,通常我们使用循环进行遍历。
数组的使用还有很多,例如:查找数组元素中最大的值,数组反序,将数组作为方法的返回值,将数组作为参数等。

二维数组:即“数组的数组”,类型说明符 数组名 [常量表达式] [常量表达式]。 二维数组又称为矩阵,行列数相等的矩阵称为方阵。
二维数组
数据类型[][] = new 数据类型[m][n]
本质:数组的元素还是数组。
int [][] arr= new int[4][3];
System.out.println(arr.length);
1.访问二维数组的元素,语法arr[x][y]
2.遍历二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + “\t”); }

数组的复制:
System.arraycopy(arr, start, dist, index, length)

数组扩容与Arrays类
由于数组对象本身并没有什么方法可以供我们调用,但是API中提供了一个根据类Arrays供我们使用,从而可以对数据对象进行一些基本操作。
Arrays类中的方法都是static修饰的静态方法,在使用的时候可以直接使用类名进行调用,而“不用”使用对象类调用(注意是“不用”,而不是“不能”)。
Arrays类具有以下常用的功能:
​ 给数组赋值:通过调用fill方法。
​ 对数组进行排序:通过sort方法。
比较数组:通过equals方法比较数组中元素的值是否相等。
查找数组元素:通过binarySearch方法能对排序好的数组进行二分查找法操作。
将数组转为字符串输出:通过toString方法实现。
缩容的实质:就是创建一个新的数组,新数组的长度比原来的数组(大,
扩容,小,缩容)。
打印数组,Arrays.toString(arr)方法的作用是将数组以字符串的形式输出
System.out.println(Arrays.toString(arr));
对数组进行缩容:
int arr1[] = Arrays.copyOf(arr, 5);
System.out.println(Arrays.toString(arr1));
扩容,将arr数组长度扩容到15:
int[] arr2 = Arrays.copyOf(arr, 15);
System.out.println(Arrays.toString(arr2));

排序算法
十种常见排序算法可以分为两大类:
​ 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
相关概念
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
排序方法:插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序、桶排序、基数排序。

稀疏数组
稀疏数组介绍:
当一个数组中大部分元素为0时,或者为同一值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方式是:
1、 记录数组一共有几行几列,有多少个不同的值。
2、 把具体有不同值的元素和行列及值记录在一个小规模的数组中,从而缩小程序的规模。
稀疏数组定义:
稀疏数组可以看做是普通数组的压缩,但是这里说的普通数组是值无效数据量远大于有效数据量的数组。

类是对象的抽象,抽象是类的特性。
一个类可以创建多个对象
类是对象的模板,对象是类的具体实例
面向对象的三大特征:
封装、继承、多态
Java面向对象
什么是面向对象
面向过程与面向对象:
面向过程:
1、步骤清晰简单,第一步要做什么,第二 步要做.么…
2、面向过程适合处理- 一些较为简单的问题
面向对象:
物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考,最后,才对某个分类下的细节进行面向过程的思索。
面向对象适合处理复杂的问题,适合处理需要多人协作的问题

面向过程的结构化程序设计:
结构化程序设计的弊端:
1、缺乏对数据的封装
2、数据与方法(操作数据)的分离

什么是面向对象:
面向对象的本质是:一类的方法组织代码,以对象的组织(封装)数据。

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

对象:真实存在的单个的个体
类:类型/类别,代表-类个体

类中可以包含:
所有对象所共有的属性/特征–成员量

所对象所共的行----__方法

1、一个类可以创建多个对象
同一类型所创建的对象,结构相同,数据不同
2、类是对象的模板,对象是类的具体的实例

面向对象的三大特征:
封装、继承、多态

二、类与对象的创建

定义一个类:

定义类的成员变量:

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

定义类的方法:
类中除了定义成员变量,还可以定义方法,用于描述对象的行为封装对象
功能, Java语言中,可以按照如下方式定义类中的方法:
class 类名{
修饰符返回值类型 方法名(参数列表){
法体…
创建并且使用对象:
使用new关键字创建对象:
定义完成之后,可以使用new关键字创建对象,创建对象的过程通常称为实例化对象, new的运算语法为:
类名();
如: new JFrame(); //可以创建一- 个窗体对象 创建对象语法:

引用类型变量:
为了能够实例化对象进行访问控制,需要使用一个特殊的变量–引用。
引用类型变量可以存放该类对象的地址信息,通常称为"指向该类的对象”,当一个引用类型变量指向该类的对象时,就可以通过这个变量对对象实施访问。
除了8种基本数据类型之外,类、接口、数组等声明的变量都称为引用类型变量,简称引用”。

引用类型变量的赋值:

1、引用类型变量存储的是对象的地址信息,相同类型的引用类型变之间也可以互相赋值。
2、引用类型变之间的赋值不会创建新的对象,但有可能会使两个以上的引用指向同一个对象

null和NullPointerException:
1、对于引用类型变量。可以对其赋值为null, null的含义为"空",表示还没有指向任何对象。
2、当一个引用变量的值为nul的时候,如果通过引用访问对象成员变量或者调用方法不符合逻辑时,产生NullPointerException。

面向对象:面向对象的本质就是:以类的方式组织代码,以对象的组织(封装)数据。
对象:现实世界抽象出来的;
类是对象的模板,而对象是类的实例;
OOA:面向对象分析
OOD:面向对象设计
OOP:面向对象编程
成员变量类型 默认初始值
数值类型(byte、short、int、long、float、double) 0
boolean类型 false
char类型 /u0000
引用类型 null

创建对象语法:
数据类型 引用类型变量 = new关键字对象
Student zs = new Student();
对象的创建:为了方便使用对象为我们提供的方法;
引用类型变量:
为了能够实例化对象进行访问控制,需要使用一个特殊的变量——引用。
创建cell类:
public class Cell {
// 定义属性
int row;
int col;

//定义方法
public void drop() {}
public void moveLeft(int len) {}
public String getCellInfo() {
	return "";
}

}
构造方法语法结构:
构造方法是在类中定义的方法,不同于其他的方法,构造方法的定义有如下两点规则:
1)构造方法的名称必须与类名完全相同。
2)构造方法没有返回值,连void关键字有没有

构造方法(构造函数):
每一个Java类必须包含构造方法,如果程序员没有手动添加构造方法,则编译器会在编译时添加一个默认的

  • 无参构造方法,关于构造方法的特点:1、构造方法的方法名字必须与类名完全相同(包括大小写必须相同),2、
  • 构造方法没有任何返回值,连void关键字也没有。
    构造方法的语法:
    修饰符 类名(参数){
    方法体…
    }
    构造方法的作用:用于创建类的对象(实例化对象),还可以初始化成员比啊量的初始值
    注意:当我们手动添加构造器(构造方法)之后,编译器将不在为我们添加默认的无参构造器

1)任何一个类都必须含有构造方法。
2)如果源程序中没有定义构造方法,编译器在编译时会为其添加一个无参的构造方法(称为默认无参构造器)。
3)当定义了构造方法之后,Java编译器不再添加默认的无参构造器。

this关键字的使用:
this关键字用在方法体中,用于指向调用该方法的当前对象,简单来说,那个对象调用方法,this就指的是那个对象,严格来讲在方法中需要通过this关键字指明当前对象。
当我们没有使用this关键字的时候,默认给我们补上this关键字
一个构造方法可以通过this关键字调用另外一个重载的构造方法。
在构造器中使用this关键字调用另外一个重载构造器是this要放在第一行
1.寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制;
2. 堆:存放所有new出来的对象;
3. 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈中,而是存放在堆(new 出来的对象)或者常量池中(对象可能在常量池里)(字符串常量对象存放在常量池中。);
4. 静态域:存放静态成员(static定义的);
5. 常量池:存放字符串常量和基本类型常量(public static final)。有时,在嵌入式系统中,常量本身会和其他部分分割离开(由于版权等其他原因),所以在这种情况下,可以选择将其放在ROM中 ;
6. 非RAM存储:硬盘等永久存储空间

数组是对象:
1、在Java中,数组属于引用数据类型。
2、数组对象存储在堆中,数组变量属于引用类型,存储数组对象的地址信息,指向数组对象。
3、数组的元素可以看成数组对象的成员变量(只不过类型全部相同)
引用类型数组的声明:
数组的元素可以是任意类型,当然也包括引用类型。
Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
内存管理:由JVM来管理的
存储new出来的对象(包括实例变量)
*存储正在调用的方法中的所有局部变量(包括方法的参数)
*调用方法时,会在栈中为该方法分配一块对应的栈帧,
栈帧中存储方法中的局部变量(包括参数),方法调用结束时,栈帧被清除,局部变量一并被清除
*局部变量的生命周期:
方法被调用时存储在栈中,方法结束时与栈帧一并被清除

引用类型数组的初始化:
1)引用类型数组的默认初始值但是null。
2)如果希望每一个元素都指向具体的对象,需要针对每一个数组元素进行new运算。

封装:
封装,简单的说就是该露的露,该藏的藏。我们在设计程序是要追求“高内聚,低耦合”,其中,高内聚指的是类的内部数据操作细节由自己完成,不允许外部干涉。低耦合指的是仅暴露少量的方法给外部调用(使用get/set方法)。
封装(对数据的隐藏),通常来说,应禁止直接访问应该对象中数据的实际表示,而是应该通过操作接口来访问,这种称为信息隐藏。
封装意义:
1、对外提供可调用的,稳定的功能。
2、封装容易变化的,具体的细节,外界不可访问,这样封装的意义在于:
a. 降低代码出错的可能性,便于维护。
b. 当内部的实现细节改变时,只要保证对外的功能定义不变,其他的模块就不会因此而受到牵连。
1、通过extends关键字可以实现类的继承。
2、子类可以继承父类的成员变量及成员方法,同时也可以定义自己的成员变量和成员方法。
3、Java语言不支持多重继承,一个类只能继承一个父类,但是一个父类可以有多个子类。

为属性提供统一的访问方法 get():获取属性值 ,set():设置属性值
get、set方法的语法特点,get属性名字(),如:getName(), set属性名字(参数),如::setName(String name)

调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。​
super()和this()类似,区别是,super()从子类中调用父类的构造方法,this()在同一类内调用其它构造方法。​ 7、super()和this()均需放在构造方法内第一行。​
可以用this调用一个构造器,但却不能调用两个。
this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。​
从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
方法的重写(Override):
发生在父子类中,方法名称相同,参数列表相同,方法体不同​
重写方法被调用时,看对象的类型​
遵循"两同两小一大"原则:------------了解​
两同:方法名称相同,参数列表相同​
void时,必须相同​
基本数据类型时,必须相同​
两小:派生类方法的返回值类型小于或等于超类方法的​
引用数据类型时,小于或等于​
派生类方法抛出的异常小于或等于超类方法的​
一大:派生类方法的访问权限大于或等于超类方法的

重写与重载的区别:​
1、重写(Override):​ 1.1、发生在父子类中,方法名相同,参数列表相同,方法体不同​ 1.2、“运行期绑定”,看对象的类型绑定方法
2、重载(Overload):
发生在一个类中,方法名相同,参数列表不同,方法体不同​
多态:​
多态指的是同一方法可以根据发送对象的不同而采用多种不同的行为方式。​ 一个对象的实际类型是确定的,但是可以指向对象的引用的类型有很多。​
多态存在的条件:
1、 有继承关系​
2、 子类重写父类的方法​
3、 父类引用指向子类对象
十一、多态的意义:​
1、行为的多态(所有抽象方法都是多态的)​
2、对象的多态(所有对象都是多态的)
多态的表现形式: 1、重写:根据对象的不同来表现多态​ 2、重载:根据参数的不同来表现多态:
多态是方法的多态性,属性没有多态性。
instanceof 和类型转换​ instanceof 是 Java 的一个二元操作符,类似于 ==,>,< 等操作符。​
instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例(实例指的是“类”在实例化之后叫做一个“实例”。实例化:通常把用类创建对象的过程称为实例化),返回 boolean 的数据类型。
向上造型:
超类型的引用指向派生类的对象​
能点出来什么,看引用的类型**
向下造型:(容易出现类型转换异常)
1)派生类的引用指向超类对象​
2)向下造型需要进行强转​

static和final关键字
1、static修饰成员变量
1、用static修饰的成员变量不属于对象的数据结构,属于类的数据结构
2、static变量是属于类的变量,通常可以通过类名来引用static成员。
3、static成员变量和类的信息一起存储在方法区,而不是在堆中,一个类的static变量只有一份,无论这个类创建了多少个对象。
static修饰方法
​ 1、通常的方法都会涉及到对具体对象的操作,这些方法在调用时需要隐式传递对象的引用(this)。
​ 2、static修饰的方法则不需要针对某些对象进行操作,其运行结果仅仅与输入的参数有关,调用时直接用类名引用。
​ 3、static在调用时没有具体的对象,因此在static方法中不能对非static成员进行访问,static方法的作用在于提供一些“工具方法”和“工厂方法”等。
3、static静态块
​ Static块属于类的代码块,在类加载期间指向代码块,只执行一次,可以用来在软件中加载静态资源。
final修饰变量
​1、final关键字修饰成员变量,表示变量不可被改变。
​2、final修饰成员变量的两种方式初始化:
​ a. 声明的同时初始化
​ b. 构造函数中初始化
​3、final关键字也可以修饰局部变量,使用之前初始化即可。

final修饰方法
​ 1、final关键字修饰的方法不可被重写。
​ 2、使一个方法不能被重写的意义在于:防止子类在定义新方法使造成“不间意”重写。

final修饰类
​ 1、final关键字修饰的类不可被继承。
​ 2、 JDK中的一些基础类库被定义为final的,例如:String、Math、Integer、Double等。
​ 3、使一个类不能继承的意义在于:可以保护类不被继承修改,可以控制滥用继承对系统造成的危害。

static final常量
​ 1、static final修饰的成员变量称为常量,必须声明同时初始化,不可被改变。
​ 2、static final常量会在编译期被替换。

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

抽象方法:

​ 1、由abstract修饰

​ 2、只有方法的定义,没有具体的实现(连{ }都没有)

​ 如果一个方法使用 abstract 来修饰,则说明该方法是抽象方法,抽象方法只有声明没有实现。需要注意的是 abstract 关键字只能用于普通方法,不能用于 static 方法或者构造方法中。
​ 抽象方法的 3 个特征如下:
​ (1)抽象方法没有方法体
​ (2)抽象方法必须存在于抽象类中
​ (3)子类重写父类时,必须重写父类所有的抽象方法

​ 注意:在使用 abstract 关键字修饰抽象方法时不能使用 private 修饰,因为抽象方法必须被子类重写,而如果使用了 private 声明,则子类是无法重写的。

抽象类:
​ 1、由abstract修饰
​ 2、包含抽象方法的类必须是抽象类
不包含抽象方法的类也可以声明为抽象类。
​ 3、抽象类不能被实例化
​ 4、抽象类是需要被继承的,派生类:
​ 4.1、重写所有抽象方法。
​ 4.2、也声明为抽象类。
​ 5、抽象类的意义:
​ 5.1、封装派生类所共有的属性和行为。​
5.2、给所有派生类提供统一的类型。
​ 5.3、可以包含抽象方法,为所有派生类提供统一的入口
注意:派生类的具体行为不同,但入口是一致的。

接口(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)接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

抽象类和接口的区别:
​ 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() 方法比较字符串对象中的字符。而==运算符比较两个对象引用看它们是否引用相同的实例
3、compareTo() 方法​ 通常,仅仅知道两个字符串是否相同是不够的。对于排序应用来说,必须知道一个字符串是大于、等于还是小于另一个。一个字符串小于另一个指的是它在字典中先出现。而一个字符串大于另一个指的是它在字典中后出现。字符串(String)的 compareTo() 方法实现了这种功能。​ compareTo() 方法用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值。

**提示:**如果两个字符串调用 equals() 方法返回 true,那么调用 compareTo() 方法会返回 0。

字符串查找​
在给定的字符串中查找字符或字符串是比较常见的操作。字符串查找分为两种形式:一种是在字符串中获取匹配字符(串)的索引值,另一种是在字符串中获取指定索引位置的字符。1、根据字符查找​ String 类的 indexOf() 方法和 lastlndexOf() 方法用于在字符串中获取匹配字符(串)的索引值。
1.1、indexOf() 方法​ indexOf() 方法用于返回字符(串)在指定字符串中首次出现的索引位置,如果能找到,则返回索引值,否则返回 -1。
1.2、fromIndex 表示查找时的起始索引,如果不指定 fromIndex,则默认从指定字符串中的开始位置(即 fromIndex 默认为 0)开始查找。
1.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 类提供的所有方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的概率是均等的。

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

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

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

5、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() 方法是比较两个对象的内容是否相等,通常字符串的比较只是关心内容是否相等。

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

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

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

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

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 实例。

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

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

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

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

System类
System 类位于 java.lang 包,代表当前 Java 程序的运行平台,系统级的很多属性和控制方法都放置在该类的内部。由于该类的构造方法是 private 的,所以无法创建该类的对象,也就是无法实例化该类。
System 类提供了一些类变量和类方法,允许直接通过 System 类来调用这些类变量和类方法。
System 类的成员变量
System 类有 3 个静态成员变量,分别是 PrintStream out、InputStream in 和 PrintStream err。
Lambda 表达式
Java 中 Lambda 表达式
1、Lambda 简介
Lambda 表达式(Lambda expression)是一个匿名函数,基于数学中的λ演算得名,也可称为闭包(Closure)。现在很多语言都支持 Lambda 表达式,如 C++、C#、Java、 Python 和 JavaScript 等。
// 可计算接口
public interface Calculable {
// 计算两个int数值
int calculateInt(int a, int b);
}

​ Calculable 接口只有一个方法 calculateInt,参数是两个 int 类型,返回值也是 int 类型。
// 实现加法计算Calculable对象
Calculable f1 = calculate(‘+’);
// 实现减法计算Calculable对象
Calculable f2 = calculate(‘-’);
// 调用calculateInt方法进行加法计算
System.out.println(n1 + “+” + n2 + “=” + f1.calculateInt(n1, n2));
// System.out.printf(“%d + %d = %d \n”, n1, n2, f1.calculateInt(n1, n2));
// 调用calculateInt方法进行减法计算
System.out.println(n1 + “-” + n2 + “=” + f1.calculateInt(n1, n2));
// System.out.printf(“%d - %d = %d \n”, n1, n2, f2.calculateInt(n1, n2));
}
常见的输出形式:
1、printf 主要继承了C语言中 printf 的一些特性,可以进行格式化输出。
2、print 就是一般的标准输出,但是不换行。
3、println 和 print 基本没什么差别,就是最后会换行。
箭头操作符:
->被称为箭头操作符或 Lambda 操作符,箭头操作符将 Lambda 表达式拆分成两部分:

  • 左侧:Lambda 表达式的参数列表。
  • 右侧:Lambda 表达式中所需执行的功能,用{ }包起来,即 Lambda 体。
    2、Java Lambda 表达式的优缺点
    优点:
  1. 代码简洁,开发迅速
  2. 方便函数式编程
  3. 非常容易进行并行计算
  4. Java 引入 Lambda,改善了集合操作(引入 Stream API)
    缺点:
  5. 代码可读性变差
  6. 在非并行计算中,很多计算未必有传统的 for 性能要高
  7. 不容易进行调试
    3、函数式接口
    Lambda 表达式实现的接口不是普通的接口,而是函数式接口。如果一个接口中,有且只有一个抽象的方法(Object 类中的方法不包括在内),那这个接口就可以被看做是函数式接口。这种接口只能有一个方法。如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误。
    Lambda表达式的使用
    1、作为参数使用Lambda表达式:Lambda 表达式一种常见的用途就是作为参数传递给方法,这需要声明参数的类型声明为函数式接口类型。
    2、访问变量:Lambda 表达式可以访问所在外层作用域定义的变量,包括成员变量和局部变量。
    2.1、访问成员变量:成员变量包括实例成员变量和静态成员变量。在 Lambda 表达式中可以访问这些成员变量,此时的 Lambda 表达式与普通方法一样,可以读取成员变量,也可以修改成员变量。
    2.2、访问局部变量:对于成员变量的访问 Lambda 表达式与普通方法没有区别,但是访问局部变量时,变量必须是 final 类型的(不可改变)。
    3、方法引用
    方法引用可以理解为 Lambda 表达式的快捷写法,它比 Lambda 表达式更加的简洁,可读性更高,有很好的重用性。如果实现比较简单,复用的地方又不多,推荐使用 Lambda 表达式,否则应该使用方法引用。
    Jav异常处理:
    异常简介
    Java 中的异常又称为例外,是一个在程序执行期间发生的事件,它中断正在执行程序的正常指令流。为了能够及时有效地处理程序中的运行错误,必须使用异常类,这可以让程序具有极好的容错性且更加健壮。
    在 Java 中一个异常的产生,主要有如下三种原因:
    1、Java 内部错误发生异常,Java 虚拟机产生的异常。
    2、编写的程序代码中的错误所产生的异常,例如空指针异常、数组越界异常等。
    3、通过 throw 语句手动生成的异常,一般用来告知该方法的调用者一些必要信息。
    Java 通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法会产生代表该异常的一个对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。我们把生成异常对象,并把它提交给运行时系统的过程称为拋出(throw)异常。运行时系统在方法的调用栈中查找,直到找到能够处理该类型异常的对象,这一个过程称为捕获(catch)异常。
    异常类型
    在 Java 中所有异常类型都是内置类 java.lang.Throwable 类的子类,即 Throwable 位于异常类层次结构的顶层。Throwable 类下有两个异常分支 Exception 和 Error。
    Throwable 类是所有异常和错误的超类,下面有 Error 和 Exception 两个子类分别表示错误和异常。其中异常类 Exception 又分为运行时异常和非运行时异常,这两种异常有很大的区别,也称为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。
    ​ 1、Exception 类用于用户程序可能出现的异常情况,它也是用来创建自定义异常类型类的类。
    ​ 2、Error 定义了在通常环境下不希望被程序捕获的异常。一般指的是 JVM 错误,如堆栈溢出。
    Exception 类型的异常处理。
    运行时异常都是 RuntimeException 类及其子类异常,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生。
    非运行时异常是指 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、ClassNotFoundException 等以及用户自定义的 Exception 异常(一般情况下不自定义检查异常)。
    Error和Exception的异同
    Error(错误)和 Exception(异常)都是 java.lang.Throwable 类的子类,在 Java 代码中只有继承了 Throwable 类的实例才能被 throw 或者 catch。
    Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类,Exception 是程序正常运行过程中可以预料到的意外情况,并且应该被开发者捕获,进行相应的处理。Error 是指正常情况下不大可能出现的情况,绝大部分的 Error 都会导致程序处于非正常、不可恢复状态。所以不需要被开发者捕获。
    Error 错误是任何处理技术都无法恢复的情况,肯定会导致程序非正常终止。并且 Error 错误属于未检查类型,大多数发生在运行时。Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源码里必须显示的进行捕获处理,这里是编译期检查的一部分。不检查异常就是所谓的运行时异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求。
    常见的 Error 和 Exception:
    (1)运行时异常(RuntimeException):
    ​ NullPropagation:空指针异常;
    ​ ClassCastException:类型强制转换异常
    ​ IllegalArgumentException:传递非法参数异常
    ​ IndexOutOfBoundsException:下标越界异常
    ​ NumberFormatException:数字格式异常
    (2)非运行时异常:
    ​ ClassNotFoundException:找不到指定 class 的异常
    ​ IOException:IO 操作异常
    (3)错误(Error):
    ​ NoClassDefFoundError:找不到 class 定义异常
    ​ StackOverflowError:深递归导致栈被耗尽而抛出的异常
    ​ OutOfMemoryError:内存溢出异常
    异常处理机制
    Java 的异常处理通过 5 个关键字来实现:try、catch、throw、throws 和 finally。try catch 语句用于捕获并处理异常,finally 语句用于在任何情况下(除特殊情况外)都必须执行的代码,throw 语句用于拋出异常,throws 语句用于声明可能会出现的异常。
    异常处理的机制如下:
    ​ 1、在方法中用 try catch 语句捕获并处理异常,catch 语句可以有多个,用来匹配多个异常。
    ​ 2、对于处理不了的异常或者要转型的异常,在方法的声明处通过 throws 语句拋出异常,即由上层的调用方法来处理。
    tre catch语句:
    try {
    逻辑程序块
    } catch(ExceptionType1 e) {
    处理代码块1
    } catch (ExceptionType2 e) {
    处理代码块2
    throw(e); // 再抛出这个"异常"
    } finally {
    释放资源代码块}
    在以上语法中,把可能引发异常的语句封装在 try 语句块中,用以捕获可能发生的异常。catch 后的( )里放匹配的异常类,指明 catch 语句可以处理的异常类型,发生异常时产生异常类的实例化对象。
    如果 try 语句块中发生异常,那么一个相应的异常对象就会被拋出,然后 catch 语句就会依据所拋出异常对象的类型进行捕获,并处理。处理之后,程序会跳过 try 语句块中剩余的语句,转到 catch 语句块后面的第一条语句开始执行。
    如果 try 语句块中没有异常发生,那么 try 块正常结束,后面的 catch 语句块被跳过,程序将从 catch 语句块后的第一条语句开始执行。
    多重try catch语句:
    在多个 catch 代码块的情况下,当一个 catch 代码块捕获到一个异常时,其它的 catch 代码块就不再进行匹配。
    注意:捕获的多个异常类之间存在父子关系时,捕获异常时一般先捕获子类,再捕获父类。所以子类异常必须在父类异常的前面,否则子类捕获不到。
    try catch finally语句
    try catch 语句的执行过程,try 语句块和 catch 语句块有可能不被完全执行,而有些处理代码则要求必须执行。例如,程序在 try 块里打开了一些物理资源(如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。 Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只回收堆内存中对象所占用的内存。 所以为了确保一定能回收 try 块中打开的物理资源,异常处理机制提供了 finally 代码块。
    try {
    // 可能会发生异常的语句
    } catch(ExceptionType e) {
    // 处理异常语句
    } finally {
    // 清理代码块
    }
    对于以上格式,无论是否发生异常(除特殊情况外),finally 语句块中的代码都会被执行。此外,finally 语句也可以和 try 语句匹配使用:
    try {
    // 逻辑代码块
    } finally {
    // 清理代码块
    }
    使用 try-catch-finally 语句时需注意以下几点:
    1、异常处理语法结构中只有 try 块是必需的,也就是说,如果没有 try 块,则不能有后面的 catch 块和 finally 块;
    2、catch 块和 finally 块都是可选的,但 catch 块和 finally 块至少出现其中之一,也可以同时出现;
    3、可以有多个 catch 块,捕获父类异常的 catch 块必须位于捕获子类异常的后面;
    4、不能只有 try 块,既没有 catch 块,也没有 finally 块;
    5、多个 catch 块必须位于 try 块之后,finally 块必须位于所有的 catch 块之后。
    6、finally 与 try 语句块匹配的语法格式,此种情况会导致异常丢失,所以不常见。

声明和抛出异常
1、throws 声明异常
当一个方法产生一个它不处理的异常时,那么就需要在该方法的头部声明这个异常,以便将该异常传递到方法的外部进行处理。使用 throws 声明的方法表示此方法不处理异常。
returnType method_name(paramList) throws Exception 1,Exception2,…{…}
returnType 表示返回值类型;method_name 表示方法名;paramList 表示参数列表;Exception 1,Exception2,… 表示异常类。

集合:集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。Java 所有的集合类都位于 java.util 包下,提供了一个表示和操作对象集合的统一构架,包含大量集合接口,以及这些接口的实现类和操作它们的算法。
集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用变量),而集合里只能保存对象(实际上只是保存对象的引用变量)。
Collection接口
Collection 接口是 List、Set 和 Queue 接口的父接口,通常情况下不被直接使用。Collection 接口定义了一些通用的方法,通过这些方法可以实现对集合的基本操作。定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
由于 Collection 是接口,不能对其实例化,所以上述代码中使用了 Collection 接口的 ArrayList 实现类来调用 Collection 的方法。add() 方法可以向 Collection 中添加一个元素,而调用 addAll() 方法可以将指定 Collection 中的所有元素添加到另一个 Collection 中。
List集合
List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List 集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List 集合默认按元素的添加顺序设置元素的索引,第一个添加到 List 集合中的元素的索引为 0,第二个为 1,依此类推。

​ List 实现了 Collection 接口,它主要有两个常用的实现类:ArrayList 类和 LinkedList 类。
1、ArrayList 类
ArrayList 类实现了可变数组的大小,存储在内的数据称为元素。它还提供了快速基于索引访问元素的方式,对尾部成员的增加和删除支持较好。使用 ArrayList 创建的集合,允许对集合中的元素进行快速的随机访问,不过,向 ArrayList 中插入与删除元素的速度相对较慢。
ArrayList 类的常用构造方法有如下两种重载形式:
​1、ArrayList():构造一个初始容量为 10 的空列表。
​2、ArrayList(Collection<?extends E>c):构造一个包含指定 Collection 元素的列表,这些元素是按照该 Collection 的迭代器返回它们的顺序排列的。
2、LinkedList类
LinkedList 类采用链表结构保存对象,这种结构的优点是便于向集合中插入或者删除元素。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高,但是 LinkedList 类随机访问元素的速度则相对较慢。这里的随机访问是指检索集合中特定索引位置的元素。
3、ArrayList 类和 LinkedList 类的区别
ArrayList 与 LinkedList 都是 List 接口的实现类,因此都实现了 List 的所有未实现的方法,只是实现的方式有所不同。
ArrayList 是基于动态数组数据结构的实现,访问元素速度优于 LinkedList。LinkedList 是基于链表数据结构的实现,占用的内存空间比较大,但在批量插入或删除数据时优于 ArrayList。
对于快速访问对象的需求,使用 ArrayList 实现执行效率上会比较好。需要频繁向集合中插入和删除元素时,使用 LinkedList 类比 ArrayList 类效果高。
不同的结构对应于不同的算法,有的考虑节省占用空间,有的考虑提高运行效率,对于程序员而言,它们就像是“熊掌”和“鱼肉”,不可兼得。高运行速度往往是以牺牲空间为代价的,而节省占用空间往往是以牺牲运行速度为代价的。
Set集合
Set 集合类似于一个罐子,程序可以依次把多个对象“丢进”Set 集合,而 Set 集合通常不能记住元素的添加顺序。也就是说 Set 集合中的对象不按特定的方式排序,只是简单地把对象加入集合。Set 集合中不能包含重复的对象,并且最多只允许包含一个 null 元素。
Set 实现了 Collection 接口,它主要有两个常用的实现类:HashSet 类和 TreeSet类。
1、HashSet 类
HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 是按照 Hash 算法来存储集合中的元素。因此具有很好的存取和查找性能。
HashSet 具有以下特点:
​1、不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
​2、HashSet 不是同步的,如果多个线程同时访问或修改一个 HashSet,则必须通过代码来保证其同步。
​3、集合元素值可以是 null。
HashSet 类的常用构造方法重载形式如下。
​1、HashSet():构造一个新的空的 Set 集合。
​2、HashSet(Collection<? extends E>c):构造一个包含指定 Collection 集合元素的新 Set 集合。其中,“< >”中的 extends 表示HashSet 的父类,即指明该 Set 集合中存放的集合元素类型。c 表示其中的元素将被存放在此 Set 集合中。
2、TreeSet 类
TreeSet 类同时实现了 Set 接口和 SortedSet 接口。SortedSet 接口是 Set 接口的子接口,可以实现对集合进行自然排序,因此使用 TreeSet 类实现的 Set 接口默认情况下是自然排序的,这里的自然排序指的是升序排序。
TreeSet 只能对实现了 Comparable 接口的类对象进行排序,因为 Comparable 接口中有一个 compareTo(Object o) 方法用于比较两个对象的大小。例如 a.compareTo(b),如果 a 和 b 相等,则该方法返回 0;如果 a 大于 b,则该方法返回大于 0 的值;如果 a 小于 b,则该方法返回小于 0 的值。
Map集合
Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键(key)对象和一个值(value)对象。用于保存具有映射关系的数据。
Map 集合里保存着两组值,一组值用于保存 Map 里的 key,另外一组值用于保存 Map 里的 value,key 和 value 都可以是任何引用类型的数据。Map 的 key 不允许重复,value 可以重复,即同一个 Map 对象的任何两个 key 通过 equals 方法比较总是返回 false。
Map 中的 key 和 value 之间存在单向一对一关系,即通过指定的 key,总能找到唯一的、确定的 value。从 Map 中取出数据时,只要给出指定的 key,就可以取出对应的 value。
Map 接口主要有两个实现类:HashMap 类和 TreeMap 类。其中,HashMap 类按哈希算法来存取键对象,而 TreeMap 类可以对键对象进行排序。
遍历Map集合
Map 集合的遍历与 List 和 Set 集合不同。Map 有两组值,因此遍历时可以只遍历值的集合,也可以只遍历键的集合,也可以同时遍历。Map 以及实现 Map 的接口类(如 HashMap、TreeMap、LinkedHashMap、Hashtable 等)都可以用以下几种方式遍历。
1)在 for 循环中使用 entries 实现 Map 的遍历(最常见和最常用的)。
2)使用 for-each 循环遍历 key 或者 values,一般适用于只需要 Map 中的 key 或者 value 时使用。性能上比 entrySet 较好。
3)使用迭代器(Iterator)遍历。
4)通过键找值遍历,这种方式的效率比较低,因为本身从键取值是耗时的操作。
Collections类
Collections 类是 Java 提供的一个操作 Set、List 和 Map 等集合的工具类。Collections 类提供了许多操作集合的静态方法,借助这些静态方法可以实现集合元素的排序、查找替换和复制等操作。
Collections 提供了如下方法用于对 List 集合元素进行排序。

  • void reverse(List list):对指定 List 集合元素进行逆向排序。
  • void shuffle(List list):对 List 集合元素进行随机排序(shuffle 方法模拟了“洗牌”动作)。
  • void sort(List list):根据元素的自然顺序对指定 List 集合的元素按升序进行排序。
  • void sort(List list, Comparator c):根据指定 Comparator 产生的顺序对 List 集合元素进行排序。
  • void swap(List list, int i, int j):将指定 List 集合中的 i 处元素和 j 处元素进行交换。
  • void rotate(List list, int distance):当 distance 为正数时,将 list 集合的后 distance 个元素“整体”移到前面;当 distance 为负数时,将 list 集合的前 distance 个元素“整体”移到后面。该方法不会改变集合的长度。
    Lambda表达式遍历Collection集合
    Java 8 为 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数的类型是一个函数式接口,而 Iterable 接口是 Collection 接口的父接口,因此 Collection 集合也可直接调用该方法。
    当程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式来遍历集合元素。
    Iterator(迭代器)
    Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素,也是 Java 集合框架的成员,但它与 Collection 和 Map 系列的集合不一样,Collection 和 Map 系列集合主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问)Collection 集合中的元素。
    Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的统一编程接口。Iterator 接口里定义了如下 4 个方法。
    ​ 1、boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回 true。
    ​ 2、Object next():返回集合里的下一个元素。
    ​ 3、void remove():删除集合里上一次 next 方法返回的元素。
    ​ 4、void forEachRemaining(Consumer action):这是 Java 8 为 Iterator 新增的默认方法,该方法可使用 Lambda 表达式来遍历集合元素。
    注意:Iterator 必须依附于 Collection 对象,若有一个 Iterator 对象,则必然有一个与之关联的 Collection 对象。Iterator 提供了两个方法来迭代访问 Collection 集合里的元素,并可通过 remove() 方法来删除集合中上一次 next() 方法返回的集合元素。
    当使用 Iterator 迭代访问 Collection 集合元素时,Collection 集合里的元素不能被改变,只有通过 Iterator 的 remove() 方法删除上一次 next() 方法返回的集合元素才可以。

泛型
1、泛型集合​ 泛型本质上是提供类型的“类型参数”,也就是参数化类型。我们可以为类、接口或方法指定一个类型参数,通过这个参数限制操作的数据类型,从而保证类型转换的绝对安全。
2、泛型类​ 除了可以定义泛型集合之外,还可以直接限定泛型类的类型参数。语法格式如下:javapublic class class_name<data_type1,data_type2,…>{}​ 其中,class_name 表示类的名称,data_ type1 等表示类型参数。Java 泛型支持声明一个以上的类型参数,只需要将类型用逗号隔开即可。​ 泛型类一般用于类中的属性类型不确定的情况下。在声明属性时,使用下面的语句:javaprivate data_type1 property_name1;private data_type2 property_name2;​ 该语句中的 data_type1 与类声明中的 data_type1 表示的是同一种数据类型。例 2​ 在实例化泛型类时,需要指明泛型类中的类型参数,并赋予泛型类属性相应类型的值。
3、泛型方法​ 到目前为止,我们所使用的泛型都是应用于整个类上。泛型同样可以在类中包含参数化的方法,而方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是不是泛型没有关系。​ 泛型方法使得该方法能够独立于类而产生变化。如果使用泛型方法可以取代类泛型化,那么就应该只使用泛型方法。另外,对一个 static 的方法而言,无法访问泛型类的类型参数。因此,如果 static 方法需要使用泛型能力,就必须使其成为泛型方法。
4、泛型的高级用法​ 泛型的用法非常灵活,除在集合、类和方法中使用外,本节将从三个方面介绍泛型的高级用法,包括限制泛型可用类型、使用类型通配符、继承泛型类和实现泛型接口。1. 限制泛型可用类型​ 在 Java 中默认可以使用任何类型来实例化一个泛型类对象。当然也可以对泛型类实例的类型进行限制,语法格式如下:javaclass 类名称<T extends anyClass>​ 其中,anyClass 指某个接口或类。使用泛型限制后,泛型类的类型必须实现或继承 anyClass 这个接口或类。无论 anyClass 是接口还是类,在进行泛型限制时都必须使用 extends 关键字。
I/O流
在 Java 中所有数据都是使用流读写的。流是一组有序的数据序列,将数据从一个地方带到另一个地方。根据数据流向的不同,可以分为输入(Input)流和输出(Output)流两种。
1、什么是输入/输出流​ Java 程序通过流来完成输入/输出,所有的输入/输出以流的形式处理。因此要了解 I/O 系统,首先要理解输入/输出流的概念。​ 输入就是将数据从各种输入设备(包括文件、键盘等)中读取到内存中,输出则正好相反,是将数据写入到各种输出设备(比如文件、显示器、磁盘等)。例如键盘就是一个标准的输入设备,而显示器就是一个标准的输出设备,但是文件既可以作为输入设备,又可以作为输出设备。数据流是 Java 进行 I/O 操作的对象,它按照不同的标准可以分为不同的类别。按照流的方向主要分为输入流和输出流两大类。​
(1)数据流按照数据单位的不同分为字节流和字符流。​
(2)按照功能可以划分为节点流和处理流。​ 数据流的处理只能按照数据序列的顺序来进行,即前一个数据处理完之后才能处理后一个数据。数据流以输入流的形式被程序获取,再以输出流的形式将数据输出到其它设备。
2、输入流​ Java 流相关的类都封装在 java.io 包中,而且每个数据流都是一个对象。所有输入流类都是 InputStream 抽象类(字节输入流)和 Reader 抽象类(字符输入流)的子类。其中 InputStream 类是字节输入流的抽象类,是所有字节输入流的父类,InputStream 类中所有方法遇到错误时都会引发 IOException 异常。如下是该类中包含的常用方法。
从输入流读入一个 8 字节的数据,将它转换成一个 0~ 255 的整数,返回一个整数,如果遇到输入流的结尾返回-1
int read(byte[] b)从输入流读取若干字节的数据保存到参数 b 指定的字节数组中,返回的字节数表示读取的字节数,如果遇到输入流的结尾返回-1
int read(byte[] b,int off,int len)从输入流读取若干字节的数据保存到参数 b 指定的字节数组中,其中 off 是指在数组中开始保存数据位置的起始下标,len 是指读取字节的位数。返回的是实际读取的字节数,如果遇到输入流的结尾则返回-1
void close()关闭数据流,当完成对数据流的操作之后需要关闭数据流
int available()返回可以从数据源读取的数据流的位数。
skip(long n)从输入流跳过参数 n 指定的字节数目
boolean markSupported()判断输入流是否可以重复读取,如果可以就返回true
void mark(int readLimit)如果输入流可以被重复读取,从流的当前位置开始设置标记,readLimit 指定可以设置标记的字节数
void reset()使输入流重新定位到刚才被标记的位置,这样可以重新读取标记过的数据

3、输出流​ 在 Java 中所有输出流类都是 OutputStream 抽象类(字节输出流)和 Writer 抽象类(字符输出流)的子类。其中 OutputStream 类是字节输出流的抽象类,是所有字节输出流的父类,

​ OutputStream 类是所有字节输出流的超类,用于以二进制的形式将数据写入目标设备,该类是抽象类,不能被实例化。OutputStream 类提供了一系列跟数据输出有关的方法,如下所示。 int write(b) 将指定字节的数据写入到输出流, int write (byte[] b)将指定字节数组的内容写入输出流, int write (byte[] b,int off,int len)将指定字节数组从 off 位置开始的 len 字节的内容写入输出流 ,close()关闭数据流,当完成对数据流的操作之后需要关闭数据流 ,flush()刷新输出流,强行将缓冲区的内容写入输出流
二、系统流​ 每个 Java 程序运行时都带有一个系统流,系统流对应的类为 java.lang.System。Sytem 类封装了 Java 程序运行时的 3 个系统流,分别通过 in、out 和 err 变量来引用。这 3 个系统流如下所示:​ System.in:**标准输入流,默认设备是键盘。​ **System.out:**标准输出流,默认设备是控制台。​ **System.err:**标准错误流,默认设备是控制台。​ 以上变量的作用域为 public 和 static,因此在程序的任何部分都不需引用 System 对象就可以使用它们。

​ System.in 是 InputStream 类的一个对象,因此上述代码的 System.in.read() 方法实际是访问 InputStream 类定义的 read() 方法。该方法可以从键盘读取一个或多个字符。对于 System.out 输出流主要用于将指定内容输出到控制台。​ System.out 和 System.error 是 PrintStream 类的对象。因为 PrintStream 是一个从 OutputStream 派生的输出流,所以它还执行低级别的 write() 方法。因此,除了 print() 和 println() 方法可以完成控制台输出以外,System.out 还可以调用 write() 方法实现控制台输出。​ write() 方法的简单形式如下:javavoid write(int byteval) throws IOException​ 该方法通过 byteval 参数向文件写入指定的字节。在实际操作中,print() 方法和 println() 方法比 write() 方法更常用。​ **注意:**尽管它们通常用于对控制台进行读取和写入字符,但是这些都是字节流。因为预定义流是没有引入字符流的 Java 原始规范的一部分,所以它们不是字符流而是字节流,但是在 Java 中可以将它们打包到基于字符的流中使用。

四、File类(文件操作类)
File类简介:​ 在 Java 中,File 类是 java.io 包中唯一代表磁盘文件本身的对象,也就是说,如果希望在程序中操作文件和目录,则都可以通过 File 类来完成。File 类定义了一些方法来操作文件,如新建、删除、重命名文件和目录等。​ File 类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。​ File 类提供了如下三种形式构造方法。​ **File(String path):**如果 path 是实际存在的路径,则该 File 对象表示的是目录;如果 path 是文件名,则该 File 对象表示的是文 件。​ **File(String path, String name):**path 是路径名,name 是文件名。​ **File(File dir, String name):**dir 是路径对象,name 是文件名。​ 使用任意一个构造方法都可以创建一个 File 对象,然后调用其提供的方法对文件进行操作。在下表中列出了 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 指定的路径名
File 类中有以下两个常用常量: **public static final String pathSeparator:**指的是分隔连续多个路径字符串的分隔符,Windows 下指;。**public static final String separator:**用来分隔同一个路径字符串中的目录的,Windows 下指/。
**注意:**可以看到 File 类的常量定义的命名规则不符合标准命名规则,常量名没有全部大写,这是因为 Java 的发展经过了一段相当长的时间,而命名规范也是逐步形成的,File 类出现较早,所以当时并没有对命名规范有严格的要求,这些都属于 Java 的历史遗留问题。

创建和删除文件:File 类不仅可以获取已知文件的属性信息,还可以在指定路径创建文件,以及删除一个文件。创建文件需要调用 createNewFile() 方法,删除文件需要调用 delete() 方法。无论是创建还是删除文件通常都先调用 exists() 方法判断文件是否存在。
Windows 中使用反斜杠\表示目录的分隔符。​ 2、Linux 中使用正斜杠/表示目录的分隔符。​ 那么既然 Java 程序本身具有可移植性的特点,则在编写路径时最好可以根据程序所在的操作系统自动使用符合本地操作系统要求的分隔符,这样才能达到可移植性的目的。要实现这样的功能,则就需要使用 File 类中提供的两个常量。
*注意:**在操作文件时一定要使用 File.separator 表示分隔符。在程序的开发中,往往会使用 Windows 开发环境,因为在 Windows 操作系统中支持的开发工具较多,使用方便,而在程序发布时往往是直接在 Linux 或其它操作系统上部署,所以这时如果不使用 File.separator,则程序运行就有可能存在问题。关于这一点我们在以后的开发中一定要有所警惕。
创建和删除目录​ File 类除了对文件的创建和删除外,还可以创建和删除目录。创建目录需要调用 mkdir() 方法,删除目录需要调用 delete() 方法。无论是创建还是删除目录都可以调用 exists() 方法判断目录是否存在。
遍历目录​ 通过遍历目录可以在指定的目录中查找文件,或者显示所有的文件列表。File 类的 list() 方法提供了遍历目录功能,该方法有如下两种重载形式。**1. String[] list()**​ 该方法表示返回由 File 对象表示目录中所有文件和子目录名称组成的字符串数组,如果调用的 File 对象不是目录,则返回 null。​ **提示:**list() 方法返回的数组中仅包含文件名称,而不包含路径。但不保证所得数组中的相同字符串将以特定顺序出现,特别是不保证它们按字母顺序出现。**2. String[] list(FilenameFilter filter)**​ 该方法的作用与 list() 方法相同,不同的是返回数组中仅包含符合 filter 过滤器的文件和目录,如果 filter 为 null,则接受所有名称。

RandomAccessFile类## 1、动态读取文件内容​ 所谓动态读取是指从文件的任意位置开始访问文件,而不是必须从文件开始位置读取到文件末尾。动态读取需要用到 Java 中的 RandomAccessFile 类。​ RandomAccessFile 是 Java 输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。由于 RandomAccessFile 可以从任意位置访问文件,所以在只需要访问文件部分内容的情况下,使用 RandonAccessFile 类是一个很好的选择。​ RandomAccessFile 对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个 RandomAccessFile 对象时,该对象的文件记录指针位于文件头(也就是 0 处),当读/写了 n 个字节后,文件记录指针将会向后移动 n 个字节。除此之外,RandonAccessFile 可以自由移动该记录指针,既可以向前移动,也可以向后移动。​ RandomAccessFile 类中提供了一些常用读取和写入数据的方法,如表 1 所示。
boolean readBoolean()从文件中读取一个 boolean 值
byte readByte()从文件中读取一个带符号位的字节
char readChar()从文件中读取一个字符
int readlnt()从文件中读取一个带符号位的整数
long readLong()从文件中读取一个带符号位的 long 值
String readLine()从文件中读取下一行文本
void seek(long pos)指定从文件起始位置开始的指针偏移量
void writeBoolean(boolean v)以字节的形式向文件中写入一个 boolean 值
void writeByte(int v)以单字节的形式向文件中写入一个 byte 值
void writeChar(int v)以双字节的形式向文件中写入一个 char 值
void writelnt(int v)以4字节的形式向文件中写入一个整数
writeLong(long v)以8字节的形式向文件中写入一个 long 值
void writeBytes(String s)以字节序列的形式向文件中写入一个字符串
void skipBytes(int n)以当前文件指针位置为起始点,跳过 n 字节
|RandomAccessFile 类的构造方法有如下两种重载形式。​
1、RandomAccessFile(File file, String mode):访问参数 file 指定的文件,访问形式由参数 mode 指定,mode 参数有两个常用的可选值 r 和 rw,其中 r 表示只读,rw 表示读写。​
2、RandomAccessFile(String name, String mode):访问参数 name 指定的文件,mode 参数的含义同上。​ **注意:**如果使用 rw 方式声明 RandomAccessFile 对象时,要写入的文件不存在,系统将自动进行创建。

字节流的使用​
InputStream 是 Java 所有字节输入流类的父类,OutputStream 是 Java 所有字节输出流类的父类,它们都是一个抽象类,因此继承它们的子类要重新定义父类中的抽象方法。
字节输入流​ InputStream 类及其子类的对象表示字节输入流,InputStream 类的常用子类如下。​ **ByteArrayInputStream 类:**将字节数组转换为字节输入流,从中读取字节。​ **FileInputStream 类:**从文件中读取数据。​ **PipedInputStream 类:**连接到一个 PipedOutputStream(管道输出流)。​ **SequenceInputStream 类:**将多个字节输入流串联成一个字节输入流。​ **ObjectInputStream 类:**将对象反序列化。
**注意:**在使用 mark() 方法和 reset() 方法之前,需要判断该文件系统是否支持这两个方法,以避免对程序造成影响。
字节输出流​ OutputStream 类及其子类的对象表示一个字节输出流。OutputStream 类的常用子类如下。​ **ByteArrayOutputStream 类:**向内存缓冲区的字节数组中写数据。​ **FileOutputStream 类:**向文件中写数据。​ **PipedOutputStream 类:**连接到一个 PipedlntputStream(管道输入流)。​ **ObjectOutputStream 类:**将对象序列化。
字节数组输入流ByteArrayInputStream 类可以从内存的字节数组中读取数据,该类有如下两种构造方法重载形式。​
1、ByteArrayInputStream(byte[] buf):创建一个字节数组输入流,字节数组类型的数据源由参数 buf 指定。​
2、ByteArrayInputStream(byte[] buf,int offse,int length):创建一个字节数组输入流,其中,参数 buf 指定字节数组类型的数据源,offset 指定在数组中开始读取数据的起始下标位置,length 指定读取的元素个数。
字节数组输出流​ ByteArrayOutputStream 类可以向内存的字节数组中写入数据,该类的构造方法有如下两种重载形式。​
1、ByteArrayOutputStream():创建一个字节数组输出流,输出流缓冲区的初始容量大小为 32 字节。​
2、ByteArrayOutputStream(int size):创建一个字节数组输出流,输出流缓冲区的初始容量大小由参数 size 指定。​
ByteArrayOutputStream 类中除了有前面介绍的字节输出流中的常用方法以外,还有如下两个方法。​
1、intsize():返回缓冲区中的当前字节数。​
2、byte[] toByteArray():以字节数组的形式返回输出流中的当前内容。

文件输入流​ FileInputStream 是 Java 流中比较常用的一种,它表示从文件系统的某个文件中获取输入字节。通过使用 FileInputStream 可以访问文件中的一个字节、一批字节或整个文件。​ 在创建 FileInputStream 类的对象时,如果找不到指定的文件将拋出 FileNotFoundException 异常,该异常必须捕获或声明拋出。​ FileInputStream 常用的构造方法主要有如下两种重载形式。​ **FileInputStream(File file):**通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。​ **FileInputStream(String name):**通过打开一个到实际文件的链接来创建一个 FileInputStream,该文件通过文件系统中的路 径名 name 指定。

文件输出流​ FileOutputStream 类继承自 OutputStream 类,重写和实现了父类中的所有方法。FileOutputStream 类的对象表示一个文件字节输出流,可以向流中写入一个字节或一批字节。在创建 FileOutputStream 类的对象时,如果指定的文件不存在,则创建一个新文件;如果文件已存在,则清除原文件的内容重新写入。FileOutputStream 类的构造方法主要有如下 4 种重载形式。 **FileOutputStream(File file):**创建一个文件输出流,参数 file 指定目标文件。 **FileOutputStream(File file,boolean append):**创建一个文件输出流,参数 file 指定目标文件,append 指定是否将数据添加到目标文件的内容末尾,如果为 true,则在末尾添加;如果为 false,则覆盖原有内容;其默认值为 false。 **FileOutputStream(String name):**创建一个文件输出流,参数 name 指定目标文件的文件路径信息。 **FileOutputStream(String name,boolean append):**创建一个文件输出流,参数 name 和 append 的含义同上。​ **注意:**使用构造方法 FileOutputStream(String name,boolean append) 创建一个文件输出流对象,它将数据附加在现有文件的末尾。该字符串 name 指明了原文件,如果只是为了附加数据而不是重写任何已有的数据,布尔类型参数 append 的值应为 true。对文件输出流有如下四点说明:
1、在 FileOutputStream 类的构造方法中指定目标文件时,目标文件可以不存在。
2、目标文件的名称可以是任意的,例如 D:\abc、D:\abc.de 和 D:\abc.de.fg 等都可以,可以使用记事本等工具打开并浏览这些文件中的内容。
3、目标文件所在目录必须存在,否则会拋出 java.io.FileNotFoundException 异常。
4、目标文件的名称不能是已存在的目录。例如 D 盘下已存在 Java 文件夹,那么就不能使用 Java 作为文件名,即不能使用 D:\Java,否则抛出 java.io.FileNotFoundException 异常。

字符流的使用​
尽管 Java 中字节流的功能十分强大,几乎可以直接或间接地处理任何类型的输入/输出操作,但利用它却不能直接操作 16 位的 Unicode 字符。这就要用到字符流。
1、字符输入流​ Reader 类是所有字符流输入类的父类,该类定义了许多方法,这些方法对所有子类都是有效的。​ Reader 类的常用子类如下。​ **CharArrayReader 类:**将字符数组转换为字符输入流,从中读取字符。​ **StringReader 类:**将字符串转换为字符输入流,从中读取字符。​ **BufferedReader 类:**为其他字符输入流提供读缓冲区。​ **PipedReader 类:**连接到一个 PipedWriter。​ **InputStreamReader 类:**将字节输入流转换为字符输入流,可以指定字符编码。
2、字符输出流​ 与 Reader 类相反,Writer 类是所有字符输出流的父类,该类中有许多方法,这些方法对继承该类的所有子类都是有效的。​ Writer 类的常用子类如下。​ **CharArrayWriter 类:**向内存缓冲区的字符数组写数据。​ **StringWriter 类:**向内存缓冲区的字符串(StringBuffer)写数据。​ **BufferedWriter 类:**为其他字符输出流提供写缓冲区。​ **PipedWriter 类:**连接到一个 PipedReader。​ **OutputStreamReader 类:**将字节输出流转换为字符输出流,可以指定字符编码。
3、字符文件输入流为了读取方便,Java 提供了用来读取字符文件的便捷类——FileReader。该类的构造方法有如下两种重载形式。​ 1、FileReader(File file):在给定要读取数据的文件的情况下创建一个新的 FileReader 对象。其中,file 表示要从中读取数据的文件。​ 2、FileReader(String fileName):在给定从中读取数据的文件名的情况下创建一个新 FileReader 对象。其中,fileName 表示要从中读取数据的文件的名称,表示的是一个文件的完整路径。​ 在用该类的构造方法创建 FileReader 读取对象时,默认的字符编码及字节缓冲区大小都是由系统设定的。要自己指定这些值,可以在 FilelnputStream 上构造一个 InputStreamReader。​ **注意:**在创建 FileReader 对象时可能会引发一个 FileNotFoundException 异常,因此需要使用 try catch 语句捕获该异常。​ 字符流和字节流的操作步骤相同,都是首先创建输入流或输出流对象,即建立连接管道,建立完成后进行读或写操作,最后关闭输入/输出流通道。
4、字符文件输出流Java 提供了写入字符文件的便捷类——FileWriter,该类的构造方法有如下4种重载形式。​
(1)FileWriter(File file):在指定 File 对象的情况下构造一个 FileWriter 对象。其中,file 表示要写入数据的 File 对象。​
(2)FileWriter(File file,boolean append):在指定 File 对象的情况下构造一个 FileWriter 对象,如果 append 的值为 true,则将字节写入文件末尾,而不是写入文件开始处。​
(3)FileWriter(String fileName):在指定文件名的情况下构造一个 FileWriter 对象。其中,fileName 表示要写入字符的文件名,表示的是完整路径。​
(4)FileWriter(String fileName,boolean append):在指定文件名以及要写入文件的位置的情况下构造 FileWriter 对象。其中,append 是一个 boolean 值,如果为 true,则将数据写入文件末尾,而不是文件开始处。​ 在创建 FileWriter 对象时,默认字符编码和默认字节缓冲区大小都是由系统设定的。要自己指定这些值,可以在 FileOutputStream 上构造一个 OutputStreamWriter 对象。​ FileWriter 类的创建不依赖于文件存在与否,如果关联文件不存在,则会自动生成一个新的文件。在创建文件之前,FileWriter 将在创建对象时打开它作为输出。如果试图打开一个只读文件,将引发一个 IOException 异常。​ **注意:**在创建 FileWriter 对象时可能会引发 IOException 或 SecurityException 异常,因此需要使用 try catch 语句捕获该异常。
5、字符缓冲区输入流​ BufferedReader 类主要用于辅助其他字符输入流,它带有缓冲区,可以先将一批数据读到内存缓冲区。接下来的读操作就可以直接从缓冲区中获取数据,而不需要每次都从数据源读取数据并进行字符编码转换,这样就可以提高数据的读取效率。BufferedReader 类的构造方法有如下两种重载形式。​ 1、BufferedReader(Reader in):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流。​ 2、BufferedReader(Reader in,int size):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流,参数 size 则用于指定缓冲区的大小,单位为字符。​ 除了可以为字符输入流提供缓冲区以外,BufferedReader 还提供了 readLine() 方法,该方法返回包含该行内容的字符串,但该字符串中不包含任何终止符,如果已到达流末尾,则返回 null。readLine() 方法表示每次读取一行文本内容,当遇到换行(\n)、回车(\r)或回车后直接跟着换行标记符即可认为某行已终止。

6、字符缓冲区输出流​ BufferedWriter 类主要用于辅助其他字符输出流,它同样带有缓冲区,可以先将一批数据写入缓冲区,当缓冲区满了以后,再将缓冲区的数据一次性写到字符输出流,其目的是为了提高数据的写效率。BufferedWriter 类的构造方法有如下两种重载形式。​ 1、BufferedWriter(Writer out):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流。​ 2、BufferedWriter(Writer out,int size):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流,参数 size 则用于指定缓冲区的大小,单位为字符。​ 该类除了可以给字符输出流提供缓冲区之外,还提供了一个新的方法 newLine(),该方法用于写入一个行分隔符。行分隔符字符串由系统属性 line.separator 定义,并且不一定是单个新行(\n)符。​ **提示:**BufferedWriter 类的使用与 FileWriter 类相同。

转换流​
正常情况下,字节流可以对所有的数据进行操作,但是有些时候在处理一些文本时我们要用到字符流,比如,查看文本的中文时就是需要采用字符流更为方便。所以 Java IO 流中提供了两种用于将字节流转换为字符流的转换流。​ InputStreamReader 用于将字节输入流转换为字符输入流,其中 OutputStreamWriter 用于将字节输出流转换为字符输出流。使用转换流可以在一定程度上避免乱码,还可以指定输入输出所使用的字符集。

对象序列化控制输入输出
​ 对象序列化(Serialize) :指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize) 则指从IO流中恢复该Java对象。如果想让某个Java对象能够序列化,则必须让它的类实现java.io.Serializable 接口。
…java
public interface Serializable {
}…
​ Serializable接口是一个空接口,实现该接口无须实现任何方法,它只是告诉JVM该类可以被序列化机制处理。通常建议程序创建的每个JavaBean类都实现Serializable。
1、序列化
​ ObjectOutputStream 类继承了OutputStream 类,同时实现了ObjectOutput接口,提供将对象序列化并写入流中的功能,该类的构造:

…java
public ObjectOutputStream(OutputStream out)

​ 该构造方法需要传入一个OutputStream对象,用来表示将对象二进制流写入到指定的OutputStream中 ​ 1) 创建一个ObjectOutputStream`对象
…java
//创建 ObjectOutputStream 输出流
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(“test.txt”));

​ 2)调用ObjectOutputStream对象的writeObject()方法输出可序列化对象。

…java
//将一个Person对象输出到输出流中
oos.writerObject(per);
2、反序列化
​ ObjectInputStream类继承了InputStream类,同时实现了ObjectInput接口,提供了将对象序列化并从流中读取出来的功能。该类方法的构造方法如下:
…java
public ObjectInputStream(InputStream in)

​ 该构造方法需要传入一个InputStream对象,用来创建从指定InputStream读取的ObjectInputStream
​ 1)创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。
…java
//创建一个ObjectInputStream输入流
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream(“object.txt”))

​ 2)调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型。
反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,否则将会引发ClassNotFoundException。
当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造方法,要么也是可序列化的,否则反序列化时将抛出 InvalidClassException异常。如果父类是不可序列化的,只是带有无参数的构造方法,则该父类中定义的成员变量值不会序列化到IO流中。
3、Java序列化编号
​ Java序列化机制是通过类的序列化编号(SerialVersionUID)来验证版本一致性的。在反序列化时,JVM会把传来字节流中的序列化编号和本地相应实体类的序列化编号进行比较,如果相同就认为一致,可以进行反序列化,否则会抛出InvalidCastException 异常。
​ 序列化编号有两种显示生成方式:
​ 1)、默认的1L,比如: private static final long serialVersionUID = 1L
​ 2)、根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段。

当实现Serializable接口的对象没有显式定义一个序列化编号时,Java序列化会根据编译的Class自动生成一个序列化编号,这种情况下只要class文件发生变化,序列化号就会改变,否则一致不变。

多线程
一)多线程基础
​ 现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务。
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 run和thread 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()方法执行完毕。
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:
线程正常终止:run()方法执行到return语句返回;
线程意外终止:run()方法因为未捕获的异常导致线程终止;
对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
小结
对目标线程调用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进程就无法结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用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
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。
我们来概括一下如何使用synchronized:
找出修改共享变量的线程代码块;
选择一个共享实例作为锁;
使用synchronized(lockObject) { … }。
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
不需要synchronized的操作
JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值,例如:int n = m;
引用类型赋值,例如:List list = anotherList。
long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
单条原子操作的语句不
小结
1、多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
2、同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
3、注意加锁对象必须是同一个实例;
4、对JVM定义的单个原子操作不需要同步。

同步方法
我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。
小结
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进程。

wait和notify
在Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:
class TaskQueue {
Queue queue = new LinkedList<>();

public synchronized void addTask(String s) {
this.queue.add(s);
}
}
但是synchronized并没有解决多线程协调的问题。仍然以上面的TaskQueue为例,我们再编写一个getTask()方法取出队列的第一个任务:
class TaskQueue {
Queue 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()方法才会返回,然后,继续执行下一条语句。
小结
wait和notify用于多线程协调运行:
在synchronized内部可以调用wait()使线程进入等待状态;
必须在已获得的锁对象上调用wait()方法;
在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
必须在已获得的锁对象上调用notify()或notifyAll()方法;
已唤醒的线程还需要重新获得锁后才能继续执行。

ReentrantLock
从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。
我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
小结
1、ReentrantLock可以替代synchronized进行同步;
2、ReentrantLock获取锁更安全;
3、必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
4、可以使用tryLock()尝试获取锁。

Condition
使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。
但是,synchronized可以配合wait和notify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?
答案是使用Condition对象来实现wait和notify的功能。
1、Condition可以替代wait和notify;
2、Condition对象必须从Lock对象获取。

多线程
一)多线程基础
​ 现代操作系统(Windows,macOS,Linux)都可以执行多任务。多任务就是同时运行多个任务。
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 run和thread 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()方法执行完毕。
当线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,直到最后变成Terminated状态,线程终止。
线程终止的原因有:
线程正常终止:run()方法执行到return语句返回;
线程意外终止:run()方法因为未捕获的异常导致线程终止;
对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。
中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
我们举个栗子:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
小结
对目标线程调用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进程就无法结束。
如何创建守护线程呢?方法和普通线程一样,只是在调用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
如果线程1在执行ILOAD后被操作系统中断,此刻如果线程2被调度执行,它执行ILOAD后获取的值仍然是100,最终结果被两个线程的ISTORE写入后变成了101,而不是期待的102。
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间。即使在执行期线程被操作系统中断执行,其他线程也会因为无法获得锁导致无法进入此指令区间。只有执行线程将锁释放后,其他线程才有机会获得锁并执行。这种加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行。
使用synchronized解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。
我们来概括一下如何使用synchronized:
找出修改共享变量的线程代码块;
选择一个共享实例作为锁;
使用synchronized(lockObject) { … }。
在使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
不需要synchronized的操作
JVM规范定义了几种原子操作:
基本类型(long和double除外)赋值,例如:int n = m;
引用类型赋值,例如:List list = anotherList。
long和double是64位数据,JVM没有明确规定64位赋值操作是不是一个原子操作,不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
单条原子操作的语句不
小结
1、多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
2、同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
3、注意加锁对象必须是同一个实例;
4、对JVM定义的单个原子操作不需要同步。

同步方法
我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。
让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装。更好的方法是把synchronized逻辑封装起来。
小结
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进程。

wait和notify
在Java程序中,synchronized解决了多线程竞争的问题。例如,对于一个任务管理器,多个线程同时往队列中添加任务,可以用synchronized加锁:
class TaskQueue {
Queue queue = new LinkedList<>();

public synchronized void addTask(String s) {
this.queue.add(s);
}
}
但是synchronized并没有解决多线程协调的问题。仍然以上面的TaskQueue为例,我们再编写一个getTask()方法取出队列的第一个任务:
class TaskQueue {
Queue 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()方法才会返回,然后,继续执行下一条语句。
小结
wait和notify用于多线程协调运行:
在synchronized内部可以调用wait()使线程进入等待状态;
必须在已获得的锁对象上调用wait()方法;
在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
必须在已获得的锁对象上调用notify()或notifyAll()方法;
已唤醒的线程还需要重新获得锁后才能继续执行。

ReentrantLock
从Java 5开始,引入了一个高级的处理并发的java.util.concurrent包,它提供了大量更高级的并发功能,能大大简化多线程程序的编写。
我们知道Java语言直接提供了synchronized关键字用于加锁,但这种锁一是很重,二是获取时必须一直等待,没有额外的尝试机制。
java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加锁
小结
1、ReentrantLock可以替代synchronized进行同步;
2、ReentrantLock获取锁更安全;
3、必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
4、可以使用tryLock()尝试获取锁。

Condition
使用ReentrantLock比直接使用synchronized更安全,可以替代synchronized进行线程同步。
但是,synchronized可以配合wait和notify实现线程在条件不满足时等待,条件满足时唤醒,用ReentrantLock我们怎么编写wait和notify的功能呢?
答案是使用Condition对象来实现wait和notify的功能。
1、Condition可以替代wait和notify;
2、Condition对象必须从Lock对象获取。

JAVA网络编程(Socket)
Socket编程
以下步骤在两台计算机之间使用套接字建立TCP连接时会出现:
1、服务器实例化一个 ServerSocket 对象,表示通过服务器上的端口通信。
2、服务器调用 ServerSocket 类的 accept() 方法,该方法将一直等待,直到客户端连接到服务器上给定的端口。
3、服务器正在等待时,一个客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
4、Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够 与服务器进行通信。
5、在服务器端,accept() 方法返回服务器上一个新的 socket 引用,该 socket 连接到客户端的 socket。

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 类的常用方法:
public int getLocalPort()返回此套接字在其上侦听的端口。
public Socket accept() throws IOException侦听并接受到此套接字的连接。
public void setSoTimeout(int timeout)通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。
public void bind(SocketAddress host, int backlog)将 ServerSocket 绑定到特定地址(IP 地址和端口号)。
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 创建未连接套接字

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关闭此套接字。

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。

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

和多线程相比,多进程的缺点在于:
1、创建进程比创建线程开销大,尤其是在Windows系统上;
2、进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。

和单线程相比,多线程编程的特点在于:多线程经常需要读写共享数据,并且需要同步。例如,播放电影时,就必须由一个线程播放视频,另一个线程播放音频,两个线程需要协调运行,否则画面和声音就不同步。因此,多线程编程的复杂度高,调试更困难。
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(); // 启动新线程
}
}
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!”);
}
}
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!”);
}
}

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

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

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

小结
1、Java线程对象Thread的状态包括:NewRunnableBlockedWaitingTimed WaitingTerminated
2、通过对另一个线程对象调用join()方法可以等待其执行结束;
3、可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;
4、对已经运行结束的线程调用join()方法会立刻返回。

中断线程
如果线程需要执行一个长时间任务,就可能需要能中断线程。中断线程就是其他线程给该线程发一个信号,该线程收到信号后结束执行run()方法,使得自身线程能立刻结束运行。
中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。
小结
对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException
目标线程检测到isInterrupted()true或者捕获了InterruptedException都应该立刻结束自身线程;
通过标志位判断需要正确使用volatile关键字;
volatile关键字解决了共享变量在线程间的可见性问题。
当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。
synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this
通过合理的设计和数据封装可以让一个类变为“线程安全”;
一个类没有特殊说明,默认不是thread-safe;
多线程能否安全访问某个非线程安全的实例,需要具体问题具体分析。
死锁
一个线程可以获取一个锁后,再继续获取另一个锁。
在获取多个锁的时候,不同线程获取多个不同对象的锁可能导致死锁。对于上述代码,线程1和线程2如果分别执行add()dec()方法时:
线程1:进入add(),获得lockA
线程2:进入dec(),获得lockB
随后:
线程1:准备获得lockB,失败,等待中;
线程2:准备获得lockA,失败,等待中。
死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。

反射
Java 反射机制是 Java 语言的一个重要特性。在学习 Java 反射机制前,先了解两个概念,编译期和运行期。
编译期是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。
运行期是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来。
Java 反射机制主要提供了以下功能,这些功能都位于java.lang.reflect包。
​ 1、在运行时判断任意一个对象所属的类。
​ 2、在运行时构造任意一个类的对象。
​ 3、在运行时判断任意一个类所具有的成员变量和方法。
​ 4、在运行时调用任意一个对象的方法。
​ 5、生成动态代理。
要想知道一个类的属性和方法,必须先获取到该类的字节码文件对象。获取类的信息时,使用的就是 Class 类中的方法。所以先要获取到每一个字节码文件(.class)对应的 Class 类型的对象.
表列出了通过反射可以访问的信息。
类型 访问方法 返回值类型 说明
包路径 getPackage() Package 对象 获取该类的存放路径
类名称 getName() String 对象 获取该类的名称
继承类 getSuperclass() Class 对象 获取该类继承的类
实现接口 getlnterfaces() Class 型数组 获取该类实现的所有接口
构造方法 getConstructors() Constructor 型数组 获取所有权限为 public 的构造方法
getDeclaredContruectors() Constructor 对象 获取当前对象的所有构造方法
方法 getMethods() Methods 型数组 获取所有权限为 public 的方法
getDeclaredMethods() Methods 对象 获取当前对象的所有方法
成员变量 getFields() Field 型数组 获取所有权限为 public 的成员变量
getDeclareFileds() Field 对象 获取当前对象的所有成员变量
内部类 getClasses() Class 型数组 获取所有权限为 public 的内部类
getDeclaredClasses() Class 型数组 获取所有内部类
内部类的声明类 getDeclaringClass() Class 对象 如果该类为内部类,则返回它的成员类,否则返回 null
在调用 getFields() 和 getMethods() 方法时将会依次获取权限为 public 的字段和变量,然后将包含从超类中继承到的成员变量和方法。而通过 getDeclareFields() 和 getDeclareMethod() 只是获取在本类中定义的成员变量和方法。
Java 反射机制的优缺点
优点:
能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
与 Java 动态编译相结合,可以实现无比强大的功能。
对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
缺点:
反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题。
java.lang.Class 类是实现反射的关键所在,Class 类的一个实例表示 Java 的一种数据类型,包括类、接口、枚举、注解(Annotation)、数组、基本数据类型和 void。Class 没有公有的构造方法,Class 实例是由 JVM 在类加载时自动创建的。
java.lang.reflect 包提供了反射中用到类,主要的类说明如下:

  • Constructor 类:提供类的构造方法信息。
  • Field 类:提供类或接口中成员变量信息。
  • Method 类:提供类或接口成员方法信息。
  • Array 类:提供了动态创建和访问 Java 数组的方法。
  • Modifier 类:提供类和成员访问修饰符信息。

为了能够动态获取对象构造方法的信息,首先需要通过下列方法之一创建一个 Constructor 类型的对象或者数组。
​ getConstructors()
​ getConstructor(Class<?>…parameterTypes) ​ getDeclaredConstructors() ​ getDeclaredConstructor(Class<?>…parameterTypes)

isVarArgs()查看该构造方法是否允许带可变数量的参数,如果允许,返回 true,否则返回 false
getParameterTypes()按照声明顺序以 Class 数组的形式获取该构造方法各个参数的类型
getExceptionTypes()以 Class 数组的形式获取该构造方法可能抛出的异常类型
newInstance(Object … initargs)通过该构造方法利用指定参数创建一个该类型的对象,如果未设置参数则表示 采用默认无参的构造方法
setAccessiable(boolean flag)如果该构造方法的权限为 private,默认为不允许通过反射利用 netlnstance() 方法创建对象。如果先执行该方法,并将入口参数设置为 true,则允许创建对象
getModifiers()获得可以解析出该构造方法所采用修饰符的整数

isStatic(int mod)如果使用 static 修饰符修饰则返回 true,否则返回 false
isPublic(int mod)如果使用 public 修饰符修饰则返回 true,否则返回 false
isProtected(int mod)如果使用 protected 修饰符修饰则返回 true,否则返回 false
isPrivate(int mod)如果使用 private 修饰符修饰则返回 true,否则返回 false
isFinal(int mod)如果使用 final 修饰符修饰则返回 true,否则返回 false
toString(int mod)以字符串形式返回所有修饰符
动态获取一个对象方法的信息,首先需要通过下列方法之一创建一个 Method 类型的对象或者数组。
​ getMethods()
​ getMethods(String name,Class<?> …parameterTypes) ​ getDeclaredMethods() ​ getDeclaredMethods(String name,Class<?>…parameterTypes)

getName()获取该方法的名称getParameterType()按照声明顺序以 Class 数组的形式返回该方法各个参数的类型
getReturnType()以 Class 对象的形式获得该方法的返回值类型
getExceptionTypes()以 Class 数组的形式获得该方法可能抛出的异常类型
invoke(Object obj,Object…args)利用 args 参数执行指定对象 obj 中的该方法,返回值为 Object 类型
isVarArgs()查看该方法是否允许带有可变数量的参数,如果允许返回 true,否则返回 false
getModifiers()获得可以解析出该方法所采用修饰符的整数

通过下列任意一个方法访问成员变量时将返回 Field 类型的对象或数组。
​ getFields()
​ getField(String name)
​ getDeclaredFields()
​ getDeclaredField(String name)
getName()获得该成员变量的名称
getType()获取表示该成员变量的 Class 对象
get(Object obj)获得指定对象 obj 中成员变量的值,返回值为 Object 类型
set(Object obj, Object value)将指定对象 obj 中成员变量的值设置为Value
getlnt(0bject obj)获得指定对象 obj 中成员类型为 int 的成员变量的值
setlnt(0bject obj, int i)将指定对象 obj 中成员变量的值设置为 i
setFloat(Object obj, float f)将指定对象 obj 中成员变量的值设置为 f
getBoolean(Object obj)获得指定对象 obj 中成员类型为 boolean 的成员变量的值
setBoolean(Object obj, boolean b)将指定对象 obj 中成员变量的值设置为 b
getFloat(Object obj)获得指定对象 obj 中成员类型为 float 的成员变量的值
setAccessible(boolean flag)此方法可以设置是否忽略权限直接访问 private 等私有权限的成员变量
getModifiers()获得可以解析出该方法所采用修饰符的整数

获取它的父类的Class
public class Main {
public static void main(String[] args) throws Exception {
Class i = Integer.class;
Class n = i.getSuperclass();
System.out.println(n);
Class o = n.getSuperclass();
System.out.println(o);
System.out.println(o.getSuperclass());
}
}
查询Integer实现的接口:
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
Integer实现的接口有:

  • java.lang.Comparable
  • java.lang.constant.Constable
  • java.lang.constant.ConstantDesc

判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
通过Class对象可以获取继承关系:
Class getSuperclass():获取父类类型;
Class[] getInterfaces():获取当前类实现的所有接口。
通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。

比较Java的classinterface的区别:
可以实例化class(非abstract);
不能实例化interface

在运行期动态创建一个interface实例的方法如下:
1、定义一个InvocationHandler实例,它负责实现接口的方法调用;
2、通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
​ 2.1、使用的ClassLoader,通常就是接口类的ClassLoader;
​ 2.2、需要实现的接口数组,至少需要传入一个接口进去;
​ 2.3、用来处理接口方法调用的InvocationHandler实例。
3、将返回的Object强制转型为接口。

小结
Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。

从JDK5开始,Java增加对元数据的支持,也就是注解,注解与注释是有一定区别的,可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。

注解常见的作用:

生成帮助文档,常用的有:@see、@param、@return等。
跟踪代码依赖性,实现替代配置文件功能。
在编译时进行格式检查。
基本注解包括:

@Override、@Deprecated、@Suppress Warnings、@SafeVarargs、@Functionallnterface。

二、@Override注解
该注解是用来指定方法重写的,只能修饰方法并且只能用于方法重写,不能修饰其他元素。

他的作用是:告诉编译器检查这个方法,保证父类要包括一个被该方法重写的方法,否则就会编译出错。

三、@Deprecated注解
该注解可以用来注解类,接口,成员方法和成员变量,用于表示某个元素已经过时。

@Depercated的作用与文档注释中的@deprecated标记的作用基本相同,但它们的用法不同而已,前者是Java5才支持的注解,直接用于修饰程序中的程序单元。

四、@Suppress Warnings:抑制编译器警告
该注解主要用在取消一些编译器产生的警告对代码左侧行列的遮挡,有时候这样会挡住我们断点调试时打的断点。

注解的使用有以下三种:

抑制单类型的警告:@SuppressWarnings(“unchecked”)
抑制多类型的警告:@SuppressWarnings(“unchecked”,“rawtypes”)
抑制所有类型的警告:@SuppressWarnings(“unchecked”)
抑制警告的关键字:

all:抑制所有警告

boxing:抑制装箱、拆箱操作时的警告

deprecation:抑制过期方法警告

fallthrough:抑制在switch中缺失breaks的警告

五、@SafeVarargs注解
@SafeVarargs注解不适用于非 static 或非 final 声明的方法,对于未声明为 static 或 final 的方法,如果要抑制 unchecked 警告,可以使用 @SuppressWarnings 注解。

六、自定义注解
声明自定义注解使用 @interface 关键字(interface 关键字前加 @ 符号)实现。

根据注解是否包含成员变量,可以分为如下两类。

  1. 标记注解:没有定义成员变量的注解类型被称为标记注解。这种注解仅利用自身的存在与否来提供信息,如前面介绍的 @Override、@Test 等都是标记注解。

  2. 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。

Java数据结构
在Java中的数据结构主要包括以下几种接口和类:

枚举、位集合、向量、栈、字典、哈希表、属性

一、枚举
枚举(The Enumeration)接口定义了一种从数据结构中取回连续元素的方式。

枚举声明的方法:

boolean hasMoreElements( ):测试此枚举是否包含更多的元素

Object nextElement( ): 如果此枚举对象至少还有一个可提供的元素,则返回此枚举的下一个元素

二、位集合
位集合定义了两个构造:

第一个构造方法创建一个默认的对象:

BitSet()

第二个方法允许用户指定初始大小,所有位初始化为0:

BitSet(int size)

三、队列
队列的六个方法分类:

压入元素(添加):add()、offer()

相同:未超出容量,从队尾压入元素,返回压入的那个元素。

区别:在超出容量时,add()方法会对抛出异常,offer()返回false

弹出元素(删除):remove()、poll()

相同:容量大于0的时候,删除并返回队头被删除的那个元素。

区别:在容量为0的时候,remove()会抛出异常,poll()返回false

获取队头元素(不删除):element()、peek()

相同:容量大于0的时候,都返回队头元素。但是不删除。

区别:容量为0的时候,element()会抛出异常,peek()返回null

offer,add 区别:

一些队列有大小限制,因此如果想在一个满的队列中加入一个新项,多出的项就会被拒绝。

这时新的 offer 方法就可以起作用了。它不是对调用 add() 方法抛出一个 unchecked 异常,而只是得到由 offer() 返回的 false

poll,remove 区别:

remove() 和 poll() 方法都是从队列中删除第一个元素。remove() 的行为与 Collection 接口的版本相似, 但是新的 poll() 方法在用空集合调用时不是抛出异常,只是返回 null。因此新的方法更适合容易出现异常条件的情况。

peek,element区别:

    element() 和 peek() 用于在队列的头部查询元素。与 remove() 方法类似,在队列为空时, element() 抛出一个异常,而 peek() 返回 null。

XML
XML(可扩展标记语言)是一种很流行的简单的基于文本的语言来用作应用程序之间的通信模式。它被认为是传输标准装置和存储数据。JAVA提供了极好的支持和丰富的库来解析,修改或查询XML文档。
XML可以用来描述数据、存储数据、传输(交换)数据。
优势
技术无关 - 作为普通文本,XML是技术独立。它可以用于由任何技术进行数据的存储和传输的目的。
人类可读 - XML使用简单的文本格式。它是人类可读和可以理解的。
可扩展性 - 在XML,自定义标签可以创建和很容易使用。
许验证 - 使用XSD,DTD和XML结构可以很容易地验证。
缺点
冗余的语法 - 通常XML文件中包含大量的重复计算。
冗余 - 作为一个冗长的语言,XML文件大小增加了传输和存储成本。

XML文档节点类型:文档(document)、元素(element)、属性(attribute)、文本(PCDATA–parsed character data)、注释(comment)、DOCTYPE :主要验证文档内容的正确性、实体(ENTITIES)、CDATA(character data)
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文件初级

XML解析:解析XML是指将通过XML文档访问数据或修改数据的一个操作或方法。
java库中提供了两种XML解析器:
1、像文档对象模型(Document Object Model,DOM)解析器这的树型解析器(tree parse),它们将读入的XML文档转换成树结构。
2、像XML简单API(Simple API for XML,SAX)解析器这样的流机制解析器(streaming parser),它们在读入XML文档时生成相应的事件。
XML解析器提供方法来访问或修改XML文档中的数据。 Java提供了多种选择来解析XML文档。以下是各种类型解析器其通常用于解析XML文档。
Dom解析器:解析通过加载该文件的全部内容,并创建其完整分级树中存储的文件。
SAX解析器:解析基于事件触发器的文档。不完整(部分)的文件加载到存储器中。
JDOM解析器:解析以类似的方式,以DOM解析器但更简单的方法的文档。
StAX解析器:解析以类似的方式,以SAX解析器但在更高效的方式的文档。
XPath解析器:解析基于表达式XML并广泛选择使用XSLT。
DOM4J解析:Java库来解析XML,XPath和使用Java集合框架XSLT,为DOM,SAX和JAXP的支持。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值