java基础 - 个人笔记

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
全局流量管理 GTM,标准版 1个月
简介: java基础 - 个人笔记

入门基础

Java程序的运行过程

  1. 编写,在java开发环境中进行java代码的输入,最终形成后缀名为.java的java源文件。
  2. 编译,指Java编译器对源文件进行错误排查的过程,最终将.java的源文件生成.class的字节码文件。
  3. 运行,使用Java解释器将字节码文件翻译成机器代码,执行并显示结果。

字节码文件是一种和任何具体机器及操作系统环境无关的中间代码,是一种二进制文件,是由Java源文件经过Java编译器编译后生成的目标代码文件。Java虚拟机(JVM)是运行Java程序的软件环境,Java解释器是Java虚拟机的一部分。

JVM是Java平台架构的基础,Java的跨平台特性正是通过在JVM中运行Java程序实现的。

不同操作系统上的JVM是不同的。

Java程序的运行流程

JVM工作方式

JDK、JRE、JVM的区别与联系

  • JDK(Java Development Kid,Java开发开源工具包):是针对开发人员的产品,包括了Java运行环境JRE、Java工具和Java基础类库。
  • JRE(Java Runtime Environment,Java运行环境):是运行Java程序所必须的环境的集合,包含JVM标准实现及Java核心类库。
  • JVM(Java Virtual Machine,Java虚拟机):是整个Java跨平台最核心的部分,能够运行以Java语言编写的软件程序。

由图中可以看出以下几点:

  • JDK=JRE+多种Java开发工具
  • JRE=JVM+各种类库
  • 这三者的关系是一层层的嵌套关系。JDK>JRE>JVM

程序设计基础

Java标志符与关键字

标志符

Java 中标识符是为方法、变量或其他用户定义项所定义的名称。标识符可以有一个或多个字符。

标志符构成规则

  • 标志符由数字、字母、美元符号($)、下划线(_)以及Unicode字符集中符号大于0xC0的所有符号构成
  • 以字母、下划线、美元符号开头
  • Java区分大小写,myvar 与 MyVar是两个不同的标志符
  • 不能以数字开头,不能以Java关键字作为标志符

关键字

关键字(或者保留字)是对编译器有特殊意义的固定单词,不能在程序中做其他目的使用。关键字具有专门的意义和用途,和自定义的标识符不同,不能当作一般的标识符来使用。

Java 的关键字对 Java 编译器有特殊的意义,它们用来表示一种数据类型,或者表示程序的结构等。保留字是为 Java 预留的关键字,它们虽然现在没有作为关键字,但在以后的升级版本中有可能作为关键字。

Java 语言目前定义了 51 个关键字,这些关键字不能作为变量名、类名和方法名来使用。以下对这些关键字进行了分类。

  1. 数据类型:boolean、int、long、short、byte、float、double、char、class、interface。
  2. 流程控制:if、else、do、while、for、switch、case、default、break、continue、return、try、catch、finally。
  3. 修饰符:public、protected、private、final、void、static、strict、abstract、transient、synchronized、volatile、native(凡是带了native关键字的,说明java的作用已经达不到了,会去调用底层库。内存中有一块专门开辟的区域:Native Method Stack,登记Native方法)。
  4. 动作:package、import、throw、throws、extends、implements、this、supper、instanceof、new。
  5. 保留字:true、false、null、goto、const。

Java注释

 // 单行注释

 /*
  *多行注释
  */

 /**
  *文档注释
  */

Javadoc标签

Javadoc 工具可以识别文档注释中的一些特殊标签,这些标签一般以@开头,后跟一个指定的名字,有的也以{@开头,以}结束。Javadoc 可以识别的标签如下表所示:

标签 描述 示例
@author 标识一个类的作者,一般用于类注释 @author description
@deprecated 指名一个过期的类或成员,表明该类或方法不建议使用 @deprecated description
{@docRoot} 指明当前文档根目录的路径 Directory Path
@exception 可能抛出异常的说明,一般用于方法注释 @exception exception-name explanation
{@inheritDoc} 从直接父类继承的注释 Inherits a comment from the immediate surperclass.
{@link} 插入一个到另一个主题的链接 {@link name text}
{@linkplain} 插入一个到另一个主题的链接,但是该链接显示纯文本字体 Inserts an in-line link to another topic.
@param 说明一个方法的参数,一般用于方法注释 @param parameter-name explanation
@return 说明返回值类型,一般用于方法注释,不能出现再构造方法中 @return explanation
@see 指定一个到另一个主题的链接 @see anchor
@serial 说明一个序列化属性 @serial description
@serialData 说明通过 writeObject() 和 writeExternal() 方法写的数据 @serialData description
@serialField 说明一个 ObjectStreamField 组件 @serialField name type description
@since 说明从哪个版本起开始有了这个函数 @since release
@throws 和 @exception 标签一样. The @throws tag has the same meaning as the @exception tag.
{@value} 显示常量的值,该常量必须是 static 属性。 Displays the value of a constant, which must be a static field.
@version 指定类的版本,一般用于类注释 @version info

对两种标签格式的说明:

  • @tag 格式的标签(不被{ }包围的标签)为块标签,只能在主要描述(类注释中对该类的详细说明为主要描述)后面的标签部分(如果块标签放在主要描述的前面,则生成 API 帮助文档时会检测不到主要描述)。
  • {@tag} 格式的标签(由{ }包围的标签)为内联标签,可以放在主要描述中的任何位置或块标签的注释中。

Javadoc 标签注意事项:

  • Javadoc标签必须从一行的开头开始,否则将被视为普通文本。
  • 一般具有相同名称的标签放在一起。
  • Javadoc 标签区分大小写,代码中对于大小写错误的标签不会发生编译错误,但是在生成 API 帮助文档时会检测不到该注释内容。

常量

常量:是指在整个程序运行期间值保持不变的量。是形式化的表现。使用final定义的,声明的同时需要赋予一个初始值

常量值:是常量的具体和直观的表现形式

在定义常量时,需要注意如下内容:

  • 在定义常量时就需要对该常量进行初始化。
  • final 关键字不仅可以用来修饰基本数据类型的常量,还可以用来修饰对象的引用或者方法。
  • 为了与变量区别,常量取名一般都用大写字符。
  • 一般常量的值不允许更改,如果更改其值将提示不能重复赋值错误。

Java中常用的转义字符

转义字符 说明
\ddd 1~3 位八进制数所表示的字符
\uxxxx 1~4 位十六进制数所表示的字符
\' 单引号字符
\" 双引号字符
\\ 双斜杠字符
\r 回车
\n 换行
\b 退格
\t 横向跳格

变量

Java是强类型语言,强类型的含义有以下两点:

  • 所有变量必须先定义后使用
  • 指定类型的变量只接受与之类型匹配的值

变量的声明设计四个内容:

  1. 变量类型

  2. 标志符,也称变量名称

  3. 声明变量时的值

  4. 多个同类型的变量可以同时声明同时初始化,多个变量之间需要使用逗号分开,声明结束时用分号分隔。

    String username,address,phone,tel;    // 声明多个变量
    int num1=12,num2=23,result=35;    // 声明并初始化多个变量
    

变量的赋值:

  1. 声明时直接赋值
  2. 先声明,后赋值

变量的作用域:规定了变量所能使用的范围,只有在作用域范围内的变量才能被使用。根据作用域的不同一般分为成员变量局部变量

成员变量:定义在方法体和语句块之外,作用域是整个类。又分为两种,全局变量和静态变量(类变量)

名称 修饰 访问 生命周期
全局变量(实例变量) 无 static 修饰 对象名.变量名 只要对象被当作引用,实例变量就将存在
静态变量(类变量) 用 static 修饰 类名.变量名或对象名.变量名 其生命周期取决于类的生命周期。类被垃圾回收机制彻底回收时才会被销毁

局部变量:是指在方法或者方法代码块中定义的变量,其作用域是其所在的代码块。可分为以下三种:

  • 方法参数变量(形参):在整个方法内有效。
  • 方法局部变量(方法内定义): 从定义这个变量开始到方法结束这一段时间内有效。
  • 代码块局部变量(代码块内定义):从定义这个变量开始到代码块结束这一段时间内有效。

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

数据类型

分为基本数据类型引用数据类型

基本数据类型

关键字 类型名称 占用内存 取值范围
boolean 布尔型 1 字节 true 或 false
byte 字节型 1 字节 -128~127
char 字符型 2 字节 ISO 单一字符集
short 短整型 2 字节 -32768~32767
int 整型 4 字节 -2147483648~2147483647
long 长整型 8 字节 -9223372036854775808L~9223372036854775807L
float 单精度浮点型 4 字节 +/-3.4E+38F(6~7 个有效位)
double 双精度浮点型 8 字节 +/-1.8E+308 (15 个有效位)

提示:char也是一种整数类型,相当于无符号整数类型

基本数据类型又可以分为四大类:整数类型、浮点类型、布尔类型和字符型

  1. 整数类型

    包含四种:字节型(byte)、短整型(short)、整形(int)、长整形(long)。这些值都是有符号的值,正数和负数。

    | 类型 | 描述 |
    | ----- | ------------------------------------------------------------ |
    | byte | 最小的整数类型,处理数据流或者未加工的二进制数据时,此类型非常有用。 |
    | short | short 类型限制数据的存储为先高字节,后低字节,这样在某些机器中会出错,因此该类型很少被使用。 |
    | int | 最常使用的一种整数类型 |
    | long | 对于超出int类型所表示的范围就要使用long类型 |

  2. 浮点类型

    也称实型,包括单精度浮点型和双精度浮点型,代表有小数精度要求的数字。单精度与双精度主要的区别是所占内存大小不同,float占用4个字节,double占用8个字节,双精度比单精度具有更大的表示范围和更高的精度

    一个只要真正的被看做float,必须以f(或F)结尾,否则会被当做double类型。而double类型的值,以d(或D)结尾不是必须的。

  3. 布尔类型

    java使用保留字true和false来表示逻辑运算中的真和假,boolean类型的变量或者表达式只能取值其一。布尔类型的值不能被转换为任何类型。

  4. 字符类型

    char使用Unicode编码表示,使用单引号或者数字对char型赋值。只能赋单个字符。char类型可以运算是因为在ASCII等字符集编码表中有对应的数值,char类型运行时直接当做ASCII表对应的整数来对待。

引用数据类型

引用类型建立在基本类型的基础上,包括数组、类和接口。

引用类型还有一种特殊的 null 类型。所谓引用数据类型就是对一个对象的引用,对象包括实例和数组两种。实际上,引用类型变量就是一个指针,只是 Java 语言里不再使用指针这个说法。

空类型(null type)就是 null 值的类型,这种类型没有名称。因为 null 类型没有名称,所以不可能声明一个 null 类型的变量或者转换到 null 类型。

空引用(null)是 null 类型变量唯一的值。空引用(null)可以转换为任何引用类型。

在实际开发中,程序员可以忽略 null 类型,假定 null 只是引用类型的一个特殊直接量。

注意:空引用(null)只能被转换成引用类型,不能转换成基本类型,因此不要把一个 null 值赋给基本数据类型的变量。

数据类型转换

数据类型的转换就是所赋值的数值类型与被变量接收的数据类型不一致时发生的,它需要从一种数据类型转换为另一种数据类型。分为隐式转换和显式转换。

隐式转换(自动类型转换)

满足条件:

  • 两种数据类型彼此兼容
  • 目标类型取值范围大于源数据类型(低级类型数据转换为高级数据类型)

转换规则如下:

  • 数值型数据的转换:byte→short→int→long→float→double。
  • 字符型转换为整型:char→int。

显式转换(强制类型转换)

当两种数据类型不兼容,或者目标类型的取值范围小于源类型取值范围时,自动转换就无法进行

在强制类型转换中,如果是将浮点类型的值转换为整数,直接去掉小数点后边的所有数字;而如果是整数类型强制转换为浮点类型时,将在小数点后面补零。

运算符

Java中的运算符具有优先级、结合性的特点。运算符按照操作数的数量可以分为单目运算符、双目运算符、三目运算符。

最基本的运算符包括:算数运算符、赋值运算符、逻辑运算符、关系运算符。

算术运算符

算数一元运算符分为3个:-++--

运算符 名称 说明 例子
- 取反 去反符号 b = -a
++ 自增一 先取值再增一,或者先增一再取值 a++或++a
-- 自减一 先取值再减一,或者先减一再取值 a--或--a
运算符 含义 实例 结果
i++ 将 i 的值先使用,再加 1 赋值给 i 变量本身 int i=1; int j=i++; i=2 j=1
++i 将 i 的值先加 1 赋值给变量 i 本身后,再使用 int i=1; int j=++i; i=2 j=2
i-- 将 i 的值先使用,再减 1 赋值给变量 i 本身 int i=1; int j=i--; i=0 j=1
--i 将 i 的值先减 1 后赋值给变量 i 本身,再使用 int i=1; int j=--i; i=0 j=0

二元运算符

加(+)、减(-)、乘(*)、除(\)、取余(%),和我们平时接触的数学运算相同。加(+)还可以用于String类型的拼接操作。乘(*)、除(\)、取余(%)具有相同的优先级且高于加(+)、减(-

算术赋值运算符

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

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

赋值运算符

符号为“=”,它是双目运算符,左边必须是变量,不能是常量或者表达式。

赋值运算符的优先级低于算数运算符,结合方向是自右向左

逻辑运算符

把各个运算的关系表达式连接起来组成一个复杂的逻辑表达式,判断结果为true或false

运算法 用法 含义 说明
&& a&&b 短路与 ab 全为 true 时,计算结果为 true,否则为 false。若a为false则不会计算b,因为b不论为何值表达式都为false。
\ \ a\ \ b 短路或 ab 全为 false 时,计算结果为 false,否则为 true。若a为true则不计算b,因为b不论为何值表达式结果都为true。
! !a a 为 true 时,值为 false,a 为 false 时,值为 true
& a&b 逻辑与 ab 全为 false 时,计算结果为 false,否则为 true
\ a\ b 逻辑或 ab 全为 true 时,计算结果为 true,否则为 false

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

逻辑运算符的优先级:!的优先级最高,&&的优先级高于||!的优先级高于算数运算符,而&&||则低于关系运算符。结合方向是!具有右结合性,逻辑与和逻辑或具有左结合性

关系运算符

关系与算符是二元运算符,运算结果是boolean类型

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

注意:

   1. 基本类型的变量、值不能和引用类型的变量、值使用`==`进行比较;boolean类型的变量、值不能与其他任意类型的变量、值使用`==`进行比较如果两个引用类型之间没有父子继承关系不能使用`==`比较。
   2. `==`和`!=`可以应用于基本数据类型和引用类型,但是用于引用类型比较时,比较的是两个引用是否指向同一个对象,实际开发中一般是比较对象的内容是否相等,可使用equals方法。
   3. 关系运算符的优先级`>、<、>=、<= `具有相同的优先级且大于具有相同优先级的` !=、==`,关系运算符的优先级高于赋值运算符低于算术运算符结合方向自左向右。

位运算符

位逻辑运算包含四个:&(与)、|(或)、~(非)、^(异或)

运算符 含义 实例 结果
& 转换成二进制数,按位进行与运算(AND) 4 & 5 4
\ 转换成二进制数,按位进行或运算(OR) 4 \ 5 5
^ 转换成二进制数,按位进行异或运算(XOR) 4 ^ 5 1
~ 转换成二进制数,按位进行取反运算(NOT) ~ 4 -5

位移运算符

运算符 含义 实例 结果
» 右移位运算符。按二进制形式把所有的数字向右移动对应的位数,低位移出(舍弃),高位的空位补零。 8»1 4
« 左移位运算符。按二进制形式把所有的数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。 9«2 36

条件运算符(三目运算符)

result = <expression> ? <statement1> : <statement3>;

运算符优先级

所有的数学运算都认为是从左向右运算的,Java 语言中大部分运算符也是从左向右结合的,只有单目运算符、赋值运算符和三目运算符例外,其中,单目运算符、赋值运算符和三目运算符是从右向左结合的,也就是从右向左运算。

乘法和加法是两个可结合的运算,也就是说,这两个运算符左右两边的操作数可以互换位置而不会影响结果。运算符有不同的优先级,所谓优先级就是在表达式运算中的运算顺序。

一般而言,单目运算符优先级较高,赋值运算符优先级较低。算术运算符优先级较高,关系和逻辑运算符优先级较低。多数运算符具有左结合性,单目运算符、三目运算符、赋值运算符具有右结合性。

Java 语言中运算符的优先级共分为 14 级,其中 1 级最高,14 级最低。在同一个表达式中运算符优先级高的先执行。

优先级 运算符 结合性
1 ()、[]、{} 从左向右
2 !、+、-、~、++、-- 从右向左
3 *、/、% 从左向右
4 +、- 从左向右
5 «、»、>>> 从左向右
6 <、<=、>、>=、instanceof 从左向右
7 ==、!= 从左向右
8 & 从左向右
9 ^ 从左向右
10 \ 从左向右
11 && 从左向右
12 \ \ 从左向右
13 ?: 从右向左
14 =、+=、-=、*=、/=、&=、\ =、^=、~=、«=、»=、>>>= 从右向左

流程控制语句

选择结构(分支结构)

if结构
/* -------------------------分隔符----------------------------- */
if (条件表达式) {
   
   
    语句块;
}
/* -------------------------分隔符----------------------------- */
if (表达式) {
   
   
    语句块1;
} else {
   
   
    语句块2;
}
/* -------------------------分隔符----------------------------- */
if(表达式1) {
   
   
    语句块1;
} else if(表达式2) {
   
   
    语句块2;
...
} else if(表达式n) {
   
   
    语句块n;
} else {
   
   
    语句块n+1;
}
/* -------------------------分隔符----------------------------- */
if(表达式1) {
   
   
    if(表达式2) {
   
   
        语句块1;
    } else {
   
   
        语句块2;
    }
} else {
   
   
    if(表达式3) {
   
   
        语句块3;
    } else if(表达式4) {
   
   
        语句块4;
    } else {
   
   
        if(表达式n) {
   
   
            语句块n;
        } else {
   
   
            语句块n+1;
        }
    }
}
switch结构

注意:重复的 case 值是不允许的。

switch(表达式) {
   
   
    case1:
        语句块1;
        break;
    case2:
        语句块2;
        break;case 值n:
        语句块n;
        break;
    default:
        语句块n+1;
    break;
}
/* -------------------------分隔符----------------------------- */
switch (count) {
   
   
        case 1:
            switch (target) {
   
   
            case 0:
                System.out.println("target is zero");
                break;
            case 1:
                System.out.println("target is one");
                break;
            }
            break;
        case 2: // ...
    }
  • switch 语句不同于 if 语句的是 switch 语句仅能测试相等的情况,而 if 语句可计算任何类型的布尔表达式。也就是 switch 语句只能寻找 case 常量间某个值与表达式的值相匹配。
  • 在同一个 switch 语句中没有两个相同的 case 常量。当然,外部 switch 语句中的 case 常量可以和内部 switch 语句中的 case 常量相同。
  • switch 语句通常比一系列嵌套 if 语句更有效。switch 语句将比与之等效的 if-else 语句快得多。
if语句与switch语句的区别
1. 从使用效率上区分

从使用效率上区分,在对同一个变量的不同值作条件判断时,既可以使用 switch 语句,也可以使用 if 语句。使用 switch 语句的效率更高一些,尤其是判断的分支越多,越明显。

2. 从实用性上区分

从语句的实用性角度区分,switch 语句不如 if 条件语句,if 语句是应用最广泛和最实用的语句。

3. 何时使用 if 语句和 switch 语句

在程序开发的过程中,何时使用 if 语句和 switch 语句,需要根据实际情况而定,应尽量做到物尽其用。不能因为 switch 语句的效率高就一直使用,也不能因为 if 语句常用就不用 switch 语句。需要根据实际情况,具体问题具体分析,使用最适合的条件语句。

循环结构

使程序代码重复执行,适用于需要重复一段代码直到满足特定条件为止的情况。主要有 while、do-while 和 for。另外 Java 5 之后推出了 for-each 循环语句,for-each 循环是 for 循环的变形,它是专门为集合遍历而设计的。for-each 并不是一个关键字。

循环语句可能包含如下 4 个部分。

  • 初始化语句(init statement): 一条或多条语句,这些语句用于完成一些初始化工作,初始化语句在循环开始之前执行。
  • 循环条件(test_expression):这是一个 boolean 表达式,这个表达式能决定是否执行循环体。
  • 循环体(body_statement):这个部分是循环的主体,如果循环条件允许,这个代码块将被重复执行。如果这个代码块只有一行语句,则这个代码块的花括号是可以省略的。
  • 迭代语句(iteration_statement):这个部分在一次循环体执行结束后,对循环条件求值之前执行,通常用于控制循环条件中的变量,使得循环在合适的时候结束。
while语句

while 循环语句的语法结构如下:

while(条件表达式) {
   
   
    语句块;
}
do-while 语句

如果 while 循环一开始条件表达式就是假的,那么循环体就根本不被执行。do-while 循环语句也是 Java 中运用广泛的循环语句,它由循环条件和循环体组成,但它与 while 语句略有不同。do-while 循环语句的特点是先执行循环体,然后判断循环条件是否成立。

do-while 语句的语法格式如下:

do {
   
   
    语句块;
}while(条件表达式);
while和do-while的比较

while 循环和 do-while 循环的相同处是:都是循环结构,使用 while(循环条件) 表示循环条件,使用大括号将循环操作括起来。

while 循环和 do-while 循环的不同处如下:

  • 语法不同:与 while 循环相比,do-while 循环将 while 关键字和循环条件放在后面,而且前面多了 do 关键字,后面多了一个分号。
  • 执行次序不同:while 循环先判断,再执行。do-while 循环先执行,再判断。
  • 一开始循环条件就不满足的情况下,while 循环一次都不会执行,do-while 循环则不管什么情况下都至少执行一次。
for循环

for 语句是一种在程序执行前就要先判断条件表达式是否为真的循环语句。假如条件表达式的结果为假,那么它的循环语句根本不会执行。for 语句通常使用在知道循环次数的循环中。

for 语句语法格式如下所示。

for(条件表达式1;条件表达式2;条件表达式3) {
   
   
    语句块;
}
表达式 形式 功能 举例
条件表达式 1 赋值语句 循环结构的初始部分,为循环变量赋初值 int i=1
条件表达式 2 条件语句 循环结构的循环条件 i>40
条件表达式 3 迭代语句,通常使用 ++ 或 -- 运算符 循环结构的迭代部分,通常用来修改循环 变量的值 i++

for 关键字后面括号中的 3 个条件表达式必须用“;”隔开。

for 循环语句执行的过程为:首先执行条件表达式 1 进行初始化,然后判断条件表达式 2 的值是否为 true,如果为 true,则执行循环体语句块;否则直接退出循环。最后执行表达式 3,改变循环变量的值,至此完成一次循环。接下来进行下一次循环,直到条件表达式 2 的值为 false,才结束循环。

for 循环和 while、do while 循环不一样:由于 while、do while 循环的循环迭代语句紧跟着循环体,因此如果循环体不能完全执行,如使用 continue 语句来结束本次循环,则循环迭代语句不会被执行。但 for 循环的循环迭代语句并没有与循环体放在一起,因此不管是否使用 continue 语句来结束本次循环,循环迭代语句一样会获得执行。

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

名称 概念 适用场景 特点
for 根据循环次数限制做多少次重复操作 适合循环次数是已知的操作 初始化的条件可以使用局部变量和外部变量使用局部变量时,控制执行在 for 结束后会自动释放,提高内存使用效率。且变量在 for 循环结束后,不能被访问。先判断,再执行
while 当满足什么条件的时候,才做某种操作 适合循环次数是未知的操作 初始化的条件只能使用外部变量,且变量在 while 循环结束后可以访问先判断,再执行
do-while 先执行一次,在判断是否满足条件 适合至少执行一次的循环操作 在先需要执行一次的情况下,代码更加简洁。先执行一次,再判断
foreach(增强for循环)

foreach 循环语句是 for 语句的特殊简化版本,主要用于执行遍历功能的循环。在遍历数组、集合方面,foreach 为开发者提供了极大的方便。

foreach 循环语句的语法格式如下:

for(类型 变量名:集合) {
   
   
    语句块;
}

其中,“类型”为集合元素的类型,“变量名”表示集合中的每一个元素,“集合”是被遍历的集合对象或数组。每执行一次循环语句,循环变量就读取集合中的一个元素

return语句

return 关键字并不是专门用于结束循环的,return 语句用于终止函数的执行或退出类的方法,并把控制权返回该方法的调用者。如果这个方法带有返回类型,return 语句就必须返回这个类型的值;如果这个方法没有返回值,可以使用没有表达式的 return 语句。

break语句

break 用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到 break,系统将完全结束该循环,开始执行循环之后的代码。

在Java中,break 语句有 3 种作用,分别是:在 switch 语句中终止一个语句序列、使用 break 语句直接强行退出循环和使用 break 语句实现 goto 的功能。

在内部循环中的 break 语句仅仅终止了所在的内部循环,外部循环没有受到任何的影响。

break 除了具有 goto 退出深层循环嵌套作用外,还保留了一些程序结构化的特性。

标签 break 语句的通用格式如下:

break label;

label 是标识代码块的标签。当执行这种形式的 break 语句时,控制权被传递出指定的代码块。被加标签的代码块必须包围 break 语句,但是它不需要直接包围 break 的块。也就是说,可以使用一个加标签的 break 语句来退出一系列的嵌套块,但是不能使用 break 语句将控制权传递到不包含 break 语句的代码块。

用标签(label)可以指定一个代码块,标签可以是任何合法有效的 Java 标识符,后跟一个冒号。加上标签的代码块可以作为 break 语句的对象,使程序在加标签的块的结尾继续执行。

public class GotoDemo {
   
   
    public static void main(String[] args) {
   
   
        label: for (int i = 0; i < 10; i++) {
   
   
            for (int j = 0; j < 8; j++) {
   
   
                System.out.println(j);
                if (j % 2 != 0) {
   
   
                    break label;
                }
            }
        }
    }
}

这里的 label 是标签的名称,可以为 Java 语言中任意合法的标识符。标签语句必须和循环匹配使用,使用时书写在对应的循环语句的上面,标签语句以冒号结束。如果需要中断标签语句对应的循环,可以采用 break 后面跟标签名的方式。通常紧跟 break 之后的标签,必须在 break 所在循环的外层循环之前定义才有意义。

continue语句

continue 语句是跳过循环体中剩余的语句而强制执行下一次循环,其作用为结束本次循环,即跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判定。continue 语句类似于 break 语句,但它只能出现在循环体中。它与 break 语句的区别在于:continue 并不是中断循环语句,而是中止当前迭代的循环,进入下一次的迭代。简单来讲,continue 是忽略循环语句的当次循环。

注意:continue 语句只能用在 while 语句、for 语句或者 foreach 语句的循环体之中,在这之外的任何地方使用它都会引起语法错误。

在循环体中使用 continue 语句有两种方式可以带有标签,也可以不带标签。语法格式如下:

continue; //不带标签
continue label; //带标签,label是标签名

默认情况下,continue 只会跳出最近的内循环,如果要跳出外循环,可以为外循环添加一个标签 label1,在 continue 语句后面指定这个标签 label1,这样当条件满足执行 continue 语句时,程序就会跳转出外循环。

字符串

Java 没有内置的字符串类型,而是在标准 Java 类库中提供了一个 String 类来创建和操作字符串。

  • 直接使用双引号定义字符串

    String str = "我是一只小小鸟"; // 结果:我是一只小小鸟
    String word;
    word = "I am a bird"; // 结果:I am a bird
    
  • 使用String类定义字符串

    String str1 = new String();
    String str1 = new String("Hello Java");
    String str1 = new String(char[] value);
    String str1 = new String(char[] value,int offset,int count);
    

String 字符串转整型 int 有以下两种方式:

  • Integer.parseInt(str)
  • Integer.valueOf(str).intValue()

整型 int 转 String 字符串类型有以下 3 种方法:

  • String s = String.valueOf(i);
  • String s = Integer.toString(i);
  • String s = "" + i;

valueOf() 、parse()和toString()

1)valueOf()

valueOf() 方法将数据的内部格式转换为可读的形式。它是一种静态方法,对于所有 Java 内置的类型,在字符串内被重载,以便每一种类型都能被转换成字符串。valueOf() 方法还被类型 Object 重载,所以创建的任何形式类的对象也可被用作一个参数。这里是它的几种形式:

2)parse()

parseXxx(String) 这种形式,是指把字符串转换为数值型,其中 Xxx 对应不同的数据类型,然后转换为 Xxx 指定的类型,如 int 型和 float 型。

3)toString()

toString() 可以把一个引用类型转换为 String 字符串类型,是 sun 公司开发 Java 的时候为了方便所有类的字符串操作而特意加入的一个方法。

字符串拼接

  • 使用+连接符,是最简单、最快捷,也是使用最多的字符串连接方式。
  • 使用 concat() 方法,字符串 1.concat(字符串 2);

字符串长度

字符串名.length();

字符串大小写转换

字符串.toLowerCase(); // 将字符串中的字母全部转换为小写,非字母不受影响
字符串.toUpperCase(); // 将字符串中的字母全部转换为大写,非字母不受影响

去除字符串中的空格

字符串名.trim()

截取字符串(索引从0开始,左含右不含)

  1. substring(int beginIndex)

    从指定的开始索引位置(beginIndex)截取至字符串的最后一个字符。

    1. substring(int beginIndex, int endIndex)

    从指定的开始索引位置(beginIndex)截取至指定的结束索引位置(endIndex)的前一个位置。

sunstring() 是按照字符截取的不是按照字节截取的。

        String str = "1234567";
        System.out.println(str.substring(0)); //1234567
        System.out.println(str.substring(1)); //234567
        System.out.println(str.substring(6)); //7
        System.out.println(str.substring(7)); //
        System.out.println(str.substring(7).length()); //0
        System.out.println(str.substring(8)); //报错,StringIndexOutOfBoundsException
        System.out.println(str.substring(0,3)); //123
        System.out.println(str.substring(1,3)); //23
        System.out.println(str.substring(0,7)); //1234567
        System.out.println(str.substring(1,7)); //234567
        System.out.println(str.substring(0,8)); //报错,StringIndexOutOfBoundsException

分割字符串

String 类的spilt()方法可以按照指定的分隔符对字符串进行分割

两种重载方式

str.spilt(String sign);
str.spilt(String sign, int limit);
  • str表示要分割的字符串
  • sign表示以什么字符进行分割
  • limit表示分割后申城字符串的限制个数,如果不指定则直至整个字符串分割完为止。

使用分隔符注意如下:

1)“.”和“|”都是转义字符,必须得加“\”。

  • 如果用“.”作为分隔的话,必须写成String.split("\\."),这样才能正确的分隔开,不能用String.split(".")
  • 如果用“|”作为分隔的话,必须写成String.split("\\|"),这样才能正确的分隔开,不能用String.split("|")

2)如果在一个字符串中有多个分隔符,可以用“|”作为连字符,比如:“acount=? and uu =? or n=?”,把三个都分隔出来,可以用String.split("and|or")

字符串替换

String类提供了3种字符串替换方式,分别是replace()、replaceFirst()、replaceAll()

replace()

replace()用于将目标字符串中指定字符(串)替换为新的字符(串)

str.replace(String oldStr, String newStr)

其中,oldStr表示被替换的字符串;newStr表示用于替换的字符串。replace() 方法会将字符串中所有 oldStr替换成 newStr。

replaceFirst() 方法

replaceFirst() 方法用于将目标字符串中匹配某正则表达式的第一个子字符串替换成新的字符

字符串.replaceFirst(String regex, String replacement)

其中,regex 表示正则表达式;replacement 表示用于替换的字符串

replaceAll() 方法

replaceAll() 方法用于将目标字符串中匹配某正则表达式的所有子字符串替换成新的字符串

字符串.replaceAll(String regex, String replacement)

其中,regex 表示正则表达式,replacement 表示用于替换的字符串

字符串比较

常用方法:equals()、compareTo()、equalsIgnoreCase()

equals()

equals() 方法将逐个地比较两个字符串的每个字符是否相同。字符的大小写也在检查范围之内

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

equalsIgnoreCase()

equalsIgnoreCase() 方法的作用和语法与 equals() 方法完全相同,唯一不同的是 equalsIgnoreCase() 比较时不区分大小写

compareTo()

用于按字典顺序比较两个字符串的大小,该比较是基于字符串各个字符的 Unicode 值

1.返回参与比较的前后两个字符串的ASCII码的差值,如果两个字符串首字母不同,则该方法返回首字母的ASCII码的差值。

String a1 = "a";
String a2 = "c";
System.out.println(a1.compareTo(a2));//结果为-2

2.参与比较的两个字符串如果首字符相同,则比较下一个字符,直到有不同的为止,返回该不同的字符的asc码差值。

String a1 = "aa";
String a2 = "ad";        
System.out.println(a1.compareTo(a2));//结果为-3

3.如果两个字符串不一样长,可以参与比较的字符又完全一样,则返回两个字符串的长度差值。

String a1 = "aa";
String a2 = "aa12345678";        
System.out.println(a1.compareTo(a2));//结果为-8

4.返回为正数表示a1>a2, 返回为负数表示a1<a2, 返回为0表示a1==a2。

5.int型可以直接比较,所以没有用到compareTo比较,如果声明的是Date、String、Integer、或者其他的,可以直接使用compareTo比较。

Integer n1 = 5;
Integer n2 = 6;
System.out.println(n1.compareTo(n2));//-1

字符串查找

indexOf()

indexOf() 方法用于返回字符(串)在指定字符串中首次出现的索引位置,如果能找到,则返回索引值,否则返回 -1

str.indexOf(value)
str.indexOf(value,int fromIndex)

其中,str 表示指定字符串;value 表示待查找的字符(串);fromIndex 表示查找时的起始索引,如果不指定 fromIndex,则默认从指定字符串中的开始位置(即 fromIndex 默认为 0)开始查找。

lastlndexOf()

lastIndexOf() 方法用于返回字符(串)在指定字符串中最后一次出现的索引位置,如果能找到则返回索引值,否则返回 -1

str.lastIndexOf(value)
str.lastlndexOf(value, int fromIndex)

注意:lastIndexOf() 方法的查找策略是从右往左查找,如果不指定起始索引,则默认从字符串的末尾开始查找。

charAt()

String 类的 charAt() 方法可以在字符串内根据指定的索引查找字符,

字符串名.charAt(索引值)

String、StringBuild、StringBuffer的区别

  • String类是不可变的,对象被创建后,包含在这个对象中的字符序列是不可改变的,直至这个对象销毁,所以拼接字符串时候会产生很多无用的中间对象,如果频繁的进行这样的操作对性能有所影响。
  • StringBuild、StringBuffer可变字符串类,中文翻译为字符串缓冲区
  • StringBuffer、StringBuilder、String 中都实现了 CharSequence 接口。CharSequence 是一个定义字符串操作的接口,它只包括 length()、charAt(int index)、subSequence(int start, int end) 这几个 API。

  • StringBuffer 就是为了解决大量拼接字符串时产生很多中间对象问题而提供的一个类。它提供了 append 和 add 方法,可以将字符串添加到已有序列的末尾或指定位置,它的本质是一个线程安全的可修改的字符序列。

线程安全:

StringBuffer:线程安全
StringBuilder:线程不安全

速度:

一般情况下,速度从快到慢为 StringBuilder > StringBuffer > String,当然这是相对的,不是绝对的。

使用环境:

操作少量的数据使用 String。
单线程操作大量数据使用 StringBuilder。
多线程操作大量数据使用 StringBuffer。

问题

String str="i"的方式,java虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中

  • String s = "abc" 方式创建的对象,存储在字符串常量池中,在创建字符串对象之前,会先在常量池中检查是否存在 abc 对象。如果存在,则直接返回常量池中 abc 对象的引用,不存在则创建该对象,并将该对象的引用返回给对象 s 。
  • String s = new String("abc") 这种方式,实际上 abc 本身就是字符串常量池中的一个对象,在运行 new String() 时,把字符串常量池中的字符串 abc 复制到堆中,因此该方式不仅会在常量池中,还会在堆中创建 abc 字符串对象。 最后把 java 堆中对象的引用返回给 s 。

正则表达式(java.util.regex )

又称正规表示法、常规表示法,在代码中常简写为 regex、regexp 或 RE,它是计算机科学的一个概念。

String 类里也提供了如下几个特殊的方法。

  • boolean matches(String regex):判断该字符串是否匹配指定的正则表达式。
  • String replaceAll(String regex, String replacement):将该字符串中所有匹配 regex 的子串替换成 replacement。
  • String replaceFirst(String regex, String replacement):将该字符串中第一个匹配 regex 的子串替换成 replacement。
  • String[] split(String regex):以 regex 作为分隔符,把该字符串分割成多个子串。

语法

表达式 说明
. 匹配除换行符\n之外的任何单字符。要匹配.字符本身,请使用\.
\d 匹配0~9的所有数字
\D 匹配所有的非数字
\s 匹配所有的空白字符
\S 匹配所有的非空白字符
\w 匹配所有的单词字符,包括0-9、a-z、下划线_
\W 匹配所有的非单词字符
^ 匹配字符串的开始,要匹配 ^ 字符本身,请使用\^
$ 匹配字符串的结束,要匹配 $ 字符本身,请使用\$
* 指定前面子表达式可以出现零次或多次。要匹配 字符本身,请使用`\`。如果要匹配包括换行符在内的所有字符,一般使用[\s\S]。相当于{0,}
+ 指定前面子表达式可以出现一次或者多次,要匹配 + 字符本身,请使用\+,相当于{1,}
? 指定前面子表达式可以出现零次或一次,要匹配 ? 字符本身,请使用\?,相当于{0,1}
() 标记子表达式的开始和结束位置。要匹配这些字符,请使用\(\)
[] 用于确定中括号表达式的开始和结束位置。要匹配这些字符,请使用\[\]
{} 用于标记前面子表达式的出现频度。要匹配这些字符,请使用\{ \}
\ 用于转义下一个字符,或指定八进制、十六进制字符。如果需匹配\字符,请用\\
\ 指定两项之间任选一项。如果要匹配字符本身,请使用`\ `

方括号表达式

方括号表达式 说明
表示枚举 例如[abc]表示匹配a、b、c任意一个字符
表示范围:- 例如[a-f]表示 a~f 范围内的任意字符;[\\u0041-\\u0056]表示十六进制字符 \u0041 到 \u0056 范围的字符。范围可以和枚举结合使用,如[a-cx-z],表示 a~c、x~z 范围内的任意字符
表示求否:^ 例如[^abc]表示非 a、b、c 的任意字符;[^a-f]表示不是 a~f 范围内的任意字符
表示“与”运算:&& 例如 [a-z&&[def]]是 a~z 和 [def] 的交集,表示 d、e[a-z&&^bc]]是 a~z 范围内的所有字符,除 b 和 c 之外[ad-z] [a-z&&[m-p]]是 a~z 范围内的所有字符,除 m~p 范围之外的字符
表示“并”运算 并运算与前面的枚举类似。例如[a-d[m-p]]表示[a-dm-p]

实际上,正则表达式还提供了数量标识符,正则表达式支持的数量标识符有如下几种模式。

  • Greedy(贪婪模式):数量表示符默认采用贪婪模式,除非另有表示。贪婪模式的表达式会一直匹配下去,直到无法匹配为止。如果你发现表达式匹配的结果与预期的不符,很有可能是因为你以为表达式只会匹配前面几个字符,而实际上它是贪婪模式,所以会一直匹配下去。
  • Reluctant(勉强模式):用问号后缀(?)表示,它只会匹配最少的字符。也称为最小匹配模式。
  • Possessive(占有模式):用加号后缀(+)表示,目前只有 Java 支持占有模式,通常比较少用。

三种模式的数量表示符

贪婪模式 勉强模式 占用模式 说明
X? X?? X?+ X表达式出现零次或一次
X* X*? X*+ X表达式出现零次或多次
X+ X+? X++ X表达式出现一次或多次
X{n} X{n}? X{n}+ X表达式出现 n 次
X{n,} X{n,}? X{n,}+ X表达式最少出现 n 次
X{n,m} X{n,m}? X{n,m}+ X表达式最少出现 n 次,最多出现 m 次
字符 解释
X 字符x(x 可代表任何合法的字符)
\0mnn 八进制数 0mnn 所表示的字符
\xhh 十六进制值 0xhh 所表示的字符
\uhhhh 十六进制值 0xhhhh 所表示的 Unicode 字符
\t 制表符(“\u0009”)
\n 新行(换行)符(‘\u000A’)
\r 回车符(‘\u000D’)
\f 换页符(‘\u000C’)
\a 报警(bell)符(‘\u0007’)
\e Escape 符(‘\u001B’)
\cx x 对应的的控制符。例如,\cM匹配 Ctrl-M。x 值必须为 A~Z 或 a~z 之一。

Java类的Patter类和Matcher类的使用

Pattern 对象是正则表达式编译后在内存中的表示形式,因此,正则表达式字符串必须先被编译为 Pattern 对象,然后再利用该 Pattern 对象创建对应的 Matcher 对象。执行匹配所涉及的状态保留在 Matcher 对象中,多个 Matcher 对象可共享同一个 Pattern 对象。

因此,典型的调用顺序如下:

// 将一个字符串编译成 Pattern 对象
Pattern p = Pattern.compile("a*b");
// 使用 Pattern 对象创建 Matcher 对象
Matcher m = p.matcher("aaaaab");
boolean b = m.matches(); // 返回 true

上面定义的 Pattern 对象可以多次重复使用。如果某个正则表达式仅需一次使用,则可直接使用 Pattern 类的静态 matches() 方法,此方法自动把指定字符串编译成匿名的 Pattern 对象,并执行匹配,如下所示。

boolean b = Pattern.matches ("a*b","aaaaab");  // 返回 true

Matcher 类提供了几个常用方法

名称 说明
find() 返回目标字符串中是否包含与 Pattern 匹配的子串
group() 返回上一次与 Pattern 匹配的子串
start() 返回上一次与 Pattern 匹配的子串在目标字符串中的开始位置
end() 返回上一次与 Pattern 匹配的子串在目标字符串中的结束位置加 1
lookingAt() 返回目标字符串前面部分与 Pattern 是否匹配
matches() 返回整个目标字符串与 Pattern 是否匹配
reset() 将现有的 Matcher 对象应用于一个新的字符序列。

Java中的Math类

Math工具类提供了三角函数、对数运算、指数运算等等复杂运算。

位于java.lang包,Math类的构造方法是private修饰的,所以无法创建Math类的对象,但是Math类中的所有方法都是static修饰的

Math包含E和PI两个静态常量,分别等于e(自然对数)和 π(圆周率)

System.out.println("E 常量的值:" + Math.E); // E 常量的值:2.718281828459045
System.out.println("PI 常量的值:" + Math.PI); // PI 常量的值:3.141592653589793
方法 说明
static int abs(int a) 返回 a 的绝对值
static long abs(long a) 返回 a 的绝对值
static float abs(float a) 返回 a 的绝对值
static double abs(double a) 返回 a 的绝对值
static int max(int x,int y) 返回 x 和 y 中的最大值
static double max(double x,double y) 返回 x 和 y 中的最大值
static long max(long x,long y) 返回 x 和 y 中的最大值
static float max(float x,float y) 返回 x 和 y 中的最大值
static int min(int x,int y) 返回 x 和 y 中的最小值
static long min(long x,long y) 返回 x 和 y 中的最小值
static double min(double x,double y) 返回 x 和 y 中的最小值
static float min(float x,float y) 返回 x 和 y 中的最小值
static double ceil(double a) 返回大于或等于 a 的最小整数
static double floor(double a) 返回小于或等于 a 的最大整数
static double rint(double a) 返回最接近 a 的整数值,如果有两个同样接近的整数,则结果取偶数
static int round(float a) 将参数加上 1/2 后返回与参数最近的整数
static long round(double a) 将参数加上 1/2 后返回与参数最近的整数,然后强制转换为长整型
static double sin(double a) 返回角的三角正弦值,参数以孤度为单位
static double cos(double a) 返回角的三角余弦值,参数以孤度为单位
static double asin(double a) 返回一个值的反正弦值,参数域在 [-1,1],值域在 [-PI/2,PI/2]
static double acos(double a) 返回一个值的反余弦值,参数域在 [-1,1],值域在 [0.0,PI]
static double tan(double a) 返回角的三角正切值,参数以弧度为单位
static double atan(double a) 返回一个值的反正切值,值域在 [-PI/2,PI/2]
static double toDegrees(double angrad) 将用孤度表示的角转换为近似相等的用角度表示的角
staticdouble toRadians(double angdeg) 将用角度表示的角转换为近似相等的用弧度表示的角
static double exp(double a) 返回 e 的 a 次幂
static double pow(double a,double b) 返回以 a 为底数,以 b 为指数的幂值
static double sqrt(double a) 返回 a 的平方根
static double cbrt(double a) 返回 a 的立方根
static double log(double a) 返回 a 的自然对数,即 lna 的值
static double log10(double a) 返回以 10 为底 a 的对数

生成随机数

两种方法:一种是调用 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 类提供的所有方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的概率是均等的

方法 说明
boolean nextBoolean() 生成一个随机的 boolean 值,生成 true 和 false 的值概率相等
double nextDouble() 生成一个随机的 double 值,数值介于 [0,1.0),含 0 而不包含 1.0
int nextlnt() 生成一个随机的 int 值,该值介于 int 的区间,也就是 -231~231-1。如果 需要生成指定区间的 int 值,则需要进行一定的数学变换
int nextlnt(int n) 生成一个随机的 int 值,该值介于 [0,n),包含 0 而不包含 n。如果想生成 指定区间的 int 值,也需要进行一定的数学变换
void setSeed(long seed) 重新设置 Random 对象中的种子数。设置完种子数以后的 Random 对象 和相同种子数使用 new 关键字创建出的 Random 对象相同
long nextLong() 返回一个随机长整型数字
boolean nextBoolean() 返回一个随机布尔型值
float nextFloat() 返回一个随机浮点型数字
double nextDouble() 返回一个随机双精度值

数字格式化

DecimalFormat 是 NumberFormat 的一个子类,用于格式化十进制数字。DecimalFormat 类包含一个模式和一组符号

符号 说明
0 显示数字,如果位数不够则补 0
# 显示数字,如果位数不够不发生变化
. 小数分隔符
- 减号
, 组分隔符
E 分隔科学记数法中的尾数和小数
% 前缀或后缀,乘以 100 后作为百分比显示
? 乘以 1000 后作为千进制货币符显示。用货币符号代替。如果双写,用国际货币符号代替; 如果出现在一个模式中,用货币十进制分隔符代替十进制分隔符

下面编写一个Java 程序,演示如何使用 DecimalFormat 类将数字转换成各种格式,实现代码如下。

import java.text.DecimalFormat;
import java.util.Scanner;

public class Test08 {
   
   
    public static void main(String[] args) {
   
   
        // 实例化DecimalFormat类的对象,并指定格式
        DecimalFormat df1 = new DecimalFormat("0.0");
        DecimalFormat df2 = new DecimalFormat("#.#");
        DecimalFormat df3 = new DecimalFormat("000.000");
        DecimalFormat df4 = new DecimalFormat("###.###");
        Scanner scan = new Scanner(System.in);
        System.out.print("请输入一个float类型的数字:");
        float f = scan.nextFloat();
        // 对输入的数字应用格式,并输出结果
        System.out.println("0.0 格式:" + df1.format(f));
        System.out.println("#.# 格式:" + df2.format(f));
        System.out.println("000.000 格式:" + df3.format(f));
        System.out.println("###.### 格式:" + df4.format(f));
    }
}

执行上述代码,输出结果如下所示:

请输入一个float类型的数字:5487.45697
0.0 格式:5487.5
#.# 格式:5487.5
000.000 格式:5487.457
###.### 格式:5487.457
请输入一个float类型的数字:5.0
0.0 格式:5.0
#.# 格式:5
000.000 格式:005.000
###.### 格式:5

Java大数字运算

java提供大数字运算的类即 java.math.BigInteger 类和 java.math.BigDecimal 类。用于高精度计算。

BigInteger可以精确的表示任意范围的整数。

BigDecimal支持任何精度的浮点数,可以用来精确计算货币值。

BigInteger

除了基本的加、减、乘、除操作之外,BigInteger 类还封装了很多操作,像求绝对值、相反数、最大公约数以及判断是否为质数等。

方法名称 说明
add(BigInteger val) 做加法运算
subtract(BigInteger val) 做减法运算
multiply(BigInteger val) 做乘法运算
divide(BigInteger val) 做除法运算
remainder(BigInteger val) 做取余数运算
divideAndRemainder(BigInteger val) 做除法运算,返回数组的第一个值为商,第二个值为余数
pow(int exponent) 做参数的 exponent 次方运算
negate() 取相反数
shiftLeft(int n) 将数字左移 n 位,如果 n 为负数,则做右移操作
shiftRight(int n) 将数字右移 n 位,如果 n 为负数,则做左移操作
and(BigInteger val) 做与运算
or(BigInteger val) 做或运算
compareTo(BigInteger val) 做数字的比较运算
equals(Object obj) 当参数 obj 是 Biglnteger 类型的数字并且数值相等时返回 true, 其他返回 false
min(BigInteger val) 返回较小的数值
max(BigInteger val) 返回较大的数值

BigDecimal

BigDecimal 常用的构造方法如下。

  • BigDecimal(double val):实例化时将双精度型转换为 BigDecimal 类型。
  • BigDecimal(String val):实例化时将字符串形式转换为 BigDecimal 类型。
BigDecimal add(BigDecimal augend); // 加法
BigDecimal subtract(BigDecimal substrahend); // 减法
BigDecimal multiply(BigDecimal multiplieand); // 乘法
BigDecimal divide(BigDecimal divide, int scale, int roundingModel); // 除法
// divide() 方法的 3 个参数分别表示除数、商的小数点后的位数、近似值处理模式(例如四舍五入)。

roundingMode 参数支持的处理模式。

模式名称 说明
BigDecimal.ROUND_UP 商的最后一位如果大于 0,则向前进位,正负数都如此
BigDecimal.ROUND_DOWN 商的最后一位无论是什么数字都省略
BigDecimal.ROUND_CEILING 商如果是正数,按照 ROUND_UP 模式处理;如果是负数,按照 ROUND_DOWN 模式处理
BigDecimal.ROUND_FLOOR 与 ROUND_CELING 模式相反,商如果是正数,按照 ROUND_DOWN 模式处理; 如果是负数,按照 ROUND_UP 模式处理
BigDecimal.ROUNDHALF DOWN 对商进行五舍六入操作。如果商最后一位小于等于 5,则做舍弃操作,否则对最后 一位进行进位操作
BigDecimal.ROUND_HALF_UP 对商进行四舍五入操作。如果商最后一位小于 5,则做舍弃操作,否则对最后一位 进行进位操作
BigDecimal.ROUND_HALF_EVEN 如果商的倒数第二位是奇数,则按照 ROUND_HALF_UP 处理;如果是偶数,则按 照 ROUND_HALF_DOWN 处理

时间日期

Date()类

Date 类有如下两个构造方法。

  • Date():此种形式表示分配 Date 对象并初始化此对象,以表示分配它的时间(精确到毫秒),使用该构造方法创建的对象可以获取本地的当前时间。
  • Date(long date):此种形式表示从 GMT 时间(格林尼治时间)1970 年 1 月 1 日 0 时 0 分 0 秒开始经过参数 date 指定的毫秒数。

Date 类的无参数构造方法获取的是系统当前的时间,显示的顺序为星期、月、日、小时、分、秒、年。

Date 类带 long 类型参数的构造方法获取的是距离 GMT 指定毫秒数的时间,60000 毫秒是一分钟,而 GMT(格林尼治标准时间)与 CST(中央标准时间)相差 8 小时,也就是说 1970 年 1 月 1 日 00:00:00 GMT 与 1970 年 1 月 1 日 08:00:00 CST 表示的是同一时间。 因此距离 1970 年 1 月 1 日 00:00:00 CST 一分钟的时间为 1970 年 1 月 1 日 00:01:00 CST,即使用 Date 对象表示为 Thu Jan 01 08:01:00 CST 1970。

Date 类提供了许多与日期和事件相关的方法。

方法 描述
boolean after(Date when) 判断此日期是否在指定日期之后
boolean before(Date when) 判断此日期是否在指定日期之前
int compareTo(Date anotherDate) 比较两个日期的顺序
boolean equals(Object obj) 比较两个日期的相等性
long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来,此 Date 对象表示的毫秒数
String toString() 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy。 其中 dow 是一周中的某一天(Sun、Mon、Tue、Wed、Thu、Fri 及 Sat)

Calendar类

抽象类。创建 Calendar 对象不能使用 new 关键字,因为 Calendar 类是一个抽象类,但是它提供了一个 getInstance() 方法来获得 Calendar类的对象。

Calendar 类的常用方法

方法 描述
void add(int field, int amount) 根据日历的规则,为给定的日历字段 field 添加或减去指定的时间量 amount
boolean after(Object when) 判断此 Calendar 表示的时间是否在指定时间 when 之后,并返回判断结果
boolean before(Object when) 判断此 Calendar 表示的时间是否在指定时间 when 之前,并返回判断结果
void clear() 清空 Calendar 中的日期时间值
int compareTo(Calendar anotherCalendar) 比较两个 Calendar 对象表示的时间值(从格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒至现在的毫秒偏移量),大则返回 1,小则返回 -1,相等返回 0
int get(int field) 返回指定日历字段的值
int getActualMaximum(int field) 返回指定日历字段可能拥有的最大值
int getActualMinimum(int field) 返回指定日历字段可能拥有的最小值
int getFirstDayOfWeek() 获取一星期的第一天。根据不同的国家地区,返回不同的值
static Calendar getInstance() 使用默认时区和语言坏境获得一个日历
static Calendar getInstance(TimeZone zone) 使用指定时区和默认语言环境获得一个日历
static Calendar getInstance(TimeZone zone, Locale aLocale) 使用指定时区和语言环境获得一个日历
Date getTime() 返回一个表示此 Calendar 时间值(从格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒至现在的毫秒偏移量)的 Date 对象
long getTimeInMillis() 返回此 Calendar 的时间值,以毫秒为单位
void set(int field, int value) 为指定的日历字段设置给定值
void set(int year, int month, int date) 设置日历字段 YEAR、MONTH 和 DAY_OF_MONTH 的值
void set(int year, int month, int date, int hourOfDay, int minute, int second) 设置字段 YEAR、MONTH、DAY_OF_MONTH、HOUR、 MINUTE 和 SECOND 的值
void setFirstDayOfWeek(int value) 设置一星期的第一天是哪一天
void setTimeInMillis(long millis) 用给定的 long 值设置此 Calendar 的当前时间值

Calendar 类中定义了许多常量,分别表示不同的意义。

  • Calendar.YEAR:年份。
  • Calendar.MONTH:月份。
  • Calendar.DATE:日期。
  • Calendar.DAY_OF_MONTH:日期,和上面的字段意义完全相同。
  • Calendar.HOUR:12小时制的小时。
  • Calendar.HOUR_OF_DAY:24 小时制的小时。
  • Calendar.MINUTE:分钟。
  • Calendar.SECOND:秒。
  • Calendar.MILLISECOND:毫秒
  • Calendar.DAY_OF_WEEK:星期几。

如果整型变量 month 的值是 0,表示当前日历是在 1 月份;如果值是 11,则表示当前日历在 12 月份。

Calendar 对象可以调用 set() 方法将日历翻到任何一个时间,当参数 year 取负数时表示公元前。Calendar 对象调用 get() 方法可以获取有关年、月、日等时间信息,参数 field 的有效值由 Calendar 静态常量指定。

时间日期格式化

DateFormat 是日期/时间格式化子类的抽象类,它以与语言无关的方式格式化并解析日期或时间。日期/时间格式化子类(如 SimpleDateFormat)允许进行格式化(也就是日期→文本)、解析(文本→日期)和标准化日期。

在创建 DateFormat 对象时不能使用 new 关键字,而应该使用 DateFormat 类中的静态方法 getDateInstance(),示例代码如下:

DateFormat df = DateFormat.getDatelnstance();

在创建了一个 DateFormat 对象后,可以调用该对象中的方法来对日期/时间进行格式化。

方法 描述
String format(Date date) 将 Date 格式化日期/时间字符串
Calendar getCalendar() 获取与此日期/时间格式相关联的日历
static DateFormat getDateInstance() 获取具有默认格式化风格和默认语言环境的日期格式
static DateFormat getDateInstance(int style) 获取具有指定格式化风格和默认语言环境的日期格式
static DateFormat getDateInstance(int style, Locale locale) 获取具有指定格式化风格和指定语言环境的日期格式
static DateFormat getDateTimeInstance() 获取具有默认格式化风格和默认语言环境的日期/时间 格式
static DateFormat getDateTimeInstance(int dateStyle,int timeStyle) 获取具有指定日期/时间格式化风格和默认语言环境的 日期/时间格式
static DateFormat getDateTimeInstance(int dateStyle,int timeStyle,Locale locale) 获取具有指定日期/时间格式化风格和指定语言环境的 日期/时间格式
static DateFormat getTimeInstance() 获取具有默认格式化风格和默认语言环境的时间格式
static DateFormat getTimeInstance(int style) 获取具有指定格式化风格和默认语言环境的时间格式
static DateFormat getTimeInstance(int style, Locale locale) 获取具有指定格式化风格和指定语言环境的时间格式
void setCalendar(Calendar newCalendar) 为此格式设置日历
Date parse(String source) 将给定的字符串解析成日期/时间

格式化样式主要通过 DateFormat 常量设置。将不同的常量传入到表所示的方法中,以控制结果的长度。DateFormat 类的常量如下。

  • SHORT:完全为数字,如 12.5.10 或 5:30pm。
  • MEDIUM:较长,如 May 10,2016。
  • LONG:更长,如 May 12,2016 或 11:15:32am。
  • FULL:是完全指定,如 Tuesday、May 10、2012 AD 或 11:l5:42am CST。

SimpleDateFormat 类

如果使用 DateFormat 类格式化日期/时间并不能满足要求,那么就需要使用 DateFormat 类的子类——SimpleDateFormat。

SimpleDateFormat 是一个以与语言环境有关的方式来格式化和解析日期的具体类,它允许进行格式化(日期→文本)、解析(文本→日期)和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期/时间格式的模式。

SimpleDateFormat 类主要有如下 3 种构造方法。

  • SimpleDateFormat():用默认的格式和默认的语言环境构造 SimpleDateFormat。
  • SimpleDateFormat(String pattern):用指定的格式和默认的语言环境构造 SimpleDateF ormat。
  • SimpleDateFormat(String pattern,Locale locale):用指定的格式和指定的语言环境构造 SimpleDateF ormat。

SimpleDateFormat 自定义格式中常用的字母及含义

字母 含义 示例
y 年份。一般用 yy 表示两位年份,yyyy 表示 4 位年份 使用 yy 表示的年扮,如 11; 使用 yyyy 表示的年份,如 2011
M 月份。一般用 MM 表示月份,如果使用 MMM,则会 根据语言环境显示不同语言的月份 使用 MM 表示的月份,如 05; 使用 MMM 表示月份,在 Locale.CHINA 语言环境下,如“十月”;在 Locale.US 语言环境下,如 Oct
d 月份中的天数。一般用 dd 表示天数 使用 dd 表示的天数,如 10
D 年份中的天数。表示当天是当年的第几天, 用 D 表示 使用 D 表示的年份中的天数,如 295
E 星期几。用 E 表示,会根据语言环境的不同, 显示不 同语言的星期几 使用 E 表示星期几,在 Locale.CHINA 语 言环境下,如“星期四”;在 Locale.US 语 言环境下,如 Thu
H 一天中的小时数(0~23)。一般用 HH 表示小时数 使用 HH 表示的小时数,如 18
h 一天中的小时数(1~12)。一般使用 hh 表示小时数 使用 hh 表示的小时数,如 10 (注意 10 有 可能是 10 点,也可能是 22 点)
m 分钟数。一般使用 mm 表示分钟数 使用 mm 表示的分钟数,如 29
s 秒数。一般使用 ss 表示秒数 使用 ss 表示的秒数,如 38
S 毫秒数。一般使用 SSS 表示毫秒数 使用 SSS 表示的毫秒数,如 156

Java包装类、拆箱、装箱

Java 中的数据类型分为基本数据类型和引用数据类型。于是 Java 为每种基本数据类型分别设计了对应的类,称之为包装类

包装类和基本数据类型的关系

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

基本数据类型转换为包装类的过程称为装箱

包装类变为基本数据类型的过程称为拆箱

Object类

Object 是Java类库中的一个特殊类,也是所有类的父类

方法 说明
Object clone() 创建与该对象的类相同的新对象
boolean equals(Object) 比较两对象是否相等
void finalize() 当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法
Class getClass() 返回一个对象运行时的实例类
int hashCode() 返回该对象的散列码值
void notify() 激活等待在该对象的监视器上的一个线程
void notifyAll() 激活等待在该对象的监视器上的全部线程
String toString() 返回该对象的字符串表示
void wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待

Number类

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

Number 类定义了一些抽象方法,以各种不同数字格式返回对象的值。如 xxxValue() 方法,它将 Number 对象转换为 xxx 数据类型的值并返回

方法 说明
byte byteValue(); 返回 byte 类型的值
double doubleValue(); 返回 double 类型的值
float floatValue(); 返回 float 类型的值
int intValue(); 返回 int 类型的值
long longValue(); 返回 long 类型的值
short shortValue(); 返回 short 类型的值

抽象类不能直接实例化,而是必须实例化其具体的子类。

Integer类

构造器

  • Integer(int value);构造一个新分配的 Integer 对象,它表示指定的 int 值。
  • Integer(String value);构造一个新分配的 Integer 对象,它表示 String 参数所指示的 int 值。

在 Integer 类常用的方法。

方法 返回值 功能
byteValue() byte 以 byte 类型返回该 Integer 的值
shortValue() short 以 short 类型返回该 Integer 的值
intValue() int 以 int 类型返回该 Integer 的值
toString() String 返回一个表示该 Integer 值的 String 对象
equals(Object obj) boolean 比较此对象与指定对象是否相等
compareTo(Integer anotherlnteger) int 在数字上比较两个 Integer 对象,如相等,则返回 0; 如调用对象的数值小于 anotherlnteger 的数值,则返回负值; 如调用对象的数值大于 anotherlnteger 的数值,则返回正值
valueOf(String s) Integer 返回保存指定的 String 值的 Integer 对象
parseInt(String s) int 将数字字符串转换为 int 数值

Integer 类包含以下 4 个常量。

  • MAX_VALUE:值为 2^31^-1 的常量,它表示 int 类型能够表示的最大值。
  • MIN_VALUE:值为 -2^31^ 的常量,它表示 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 操作有关的方法

方法 返回值 功能
byteValue() byte 以 byte 类型返回该 Float 的值
doubleValue() double 以 double 类型返回该 Float 的值
floatValue() float 以 float 类型返回该 Float 的值
intValue() int 以 int 类型返回该 Float 的值(强制转换为 int 类型)
longValue() long 以 long 类型返回该 Float 的值(强制转换为 long 类型)
shortValue() short 以 short 类型返回该 Float 的值(强制转换为 short 类型)
isNaN() boolean 如果此 Float 值是一个非数字值,则返回 true,否则返回 false
isNaN(float v) boolean 如果指定的参数是一个非数字值,则返回 true,否则返回 false
toString() String 返回一个表示该 Float 值的 String 对象
valueOf(String s) Float 返回保存指定的 String 值的 Float 对象
parseFloat(String s) float 将数字字符串转换为 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 value):构造一个新分配的 Double 对象,它表示转换为 double 类型的参数。
  • Double(String s):构造一个新分配的 Double 对象,它表示 String 参数所指示的 double 值。

在 Double 类内部包含一些和 double 操作有关的方法

方法 返回值 功能
byteValue() byte 以 byte 类型返回该 Double 的值
doubleValue() double 以 double 类型返回该 Double 的值
fioatValue() float 以 float 类型返回该 Double 的值
intValue() int 以 int 类型返回该 Double 的值(强制转换为 int 类型)
longValue() long 以 long 类型返回该 Double 的值(强制转换为 long 类型)
shortValue() short 以 short 类型返回该 Double 的值(强制转换为 short 类型)
isNaN() boolean 如果此 Double 值是一个非数字值,则返回 true,否则返回 false
isNaN(double v) boolean 如果指定的参数是一个非数字值,则返回 true,否则返回 false
toString() String 返回一个表示该 Double 值的 String 对象
valueOf(String s) Double 返回保存指定的 String 值的 Double 对象
parseDouble(String s) double 将数字字符串转换为 Double 数值

Double 类的常用常量

在 Double 类中包含了很多常量,其中较为常用的常量如下。

  • MAX_VALUE:值为 1.8E308 的常量,它表示 double 类型的最大正有限值的常量。
  • MIN_VALUE:值为 4.9E-324 的常量,它表示 double 类型数据能够保持的最小正非零值的常量。
  • NaN:保存 double 类型的非数字值的常量。
  • NEGATIVE_INFINITY:保持 double 类型的负无穷大的常量。
  • POSITIVE_INFINITY:保持 double 类型的正无穷大的常量。
  • SIZE:用秦以二进制补码形式表示 double 值的比特位数。
  • TYPE:表示基本类型 double 的 Class 实例。

Character类

Character 类是字符数据类型 char 的包装类。Character 类的对象包含类型为 char 的单个字段,这样能把基本数据类型当对象来处理,

方法 描述
void Character(char value) 构造一个新分配的 Character 对象,用以表示指定的 char 值
char charValue() 返回此 Character 对象的值,此对象表示基本 char 值
int compareTo(Character anotherCharacter) 根据数字比较两个 Character 对象
boolean equals(Character anotherCharacter) 将此对象与指定对象比较,当且仅当参数不是 null,而 是一个与此对象 包含相同 char 值的 Character 对象时, 结果才是 true
boolean isDigit(char ch) 确定指定字符是否为数字,如果通过 Character. getType(ch) 提供的字 符的常规类别类型为 DECIMAL_DIGIT_NUMBER,则字符为数字
boolean isLetter(int codePoint) 确定指定字符(Unicode 代码点)是否为字母
boolean isLetterOrDigit(int codePoint) 确定指定字符(Unicode 代码点)是否为字母或数字
boolean isLowerCase(char ch) 确定指定字符是否为小写字母
boolean isUpperCase(char ch) 确定指定字符是否为大写字母
char toLowerCase(char ch) 使用来自 UnicodeData 文件的大小写映射信息将字符参数转换为小写
char toUpperCase(char ch) 使用来自 UnicodeData 文件的大小写映射信息将字符参数转换为大写

CompareTo() 方法将这个字符与其他字符比较,并且返回一个整型数组,这个值是两个字符比较后的标准代码差值。当且仅当两个字符相同时,equals() 方法的返回值才为 true。

Boolean类

Boolean 类有以下两种构造形式:

  • new Boolean(boolean boolValue);
  • new Boolean(String boolString);

在 Boolean 类内部包含了一些和 Boolean 操作有关的方法

方法 返回值 功能
booleanValue() boolean 将 Boolean 对象的值以对应的 boolean 值返回
equals(Object obj) boolean 判断调用该方法的对象与 obj 是否相等。当且仅当参数不是 null,且与调用该 方法的对象一样都表示同一个 boolean 值的 Boolean 对象时,才返回 true
parseBoolean(String s) boolean 将字符串参数解析为 boolean 值
toString() string 返回表示该 boolean 值的 String 对象
valueOf(String s) boolean 返回一个用指定的字符串表示的 boolean 值

Boolean 类的常用常量

在 Boolean 类中包含了很多的常量,其中较为常用的常量如下。

  • TRUE:对应基值 true 的 Boolean 对象。
  • FALSE:对应基值 false 的 Boolean 对象。
  • TYPE:表示基本类型 boolean 的 Class 对象。

Byte类

构造器

  • Byte(byte value)
  • Byte(String s)

在 Byte 类内部包含了一些和 Byte 操作有关的方法

方法 返回值 功能
byteValue() byte 以一个 byte 值返回 Byte 对象
compareTo(Byte bytel) int 在数字上比较两个 Byte 对象
doubleValue() double 以一个 double 值返回此 Byte 的值
intValue() int 以一个 int 值返回此 Byte 的值
parseByte(String s) byte 将 String 型参数解析成等价的 byte 形式
toStringO String 返回表示此 byte 值的 String 对象
valueOf(String s) Byte 返回一个保持指定 String 所给出的值的 Byte 对象
equals(Object obj) boolean 将此对象与指定对象比较,如果调用该方法的对象与 obj 相等 则返回 true,否则返回 false

Byte 类的常用常量

在 Byte 类中包含了很多的常量,其中较为常用的常量如下。

  • MIN_VALUE:byte 类可取的最小值。
  • MAX_VALUE:byte 类可取的最大值。
  • SIZE:用于以二进制补码形式表示的 byte 值的位数。
  • TYPE:表示基本类 byte 的 Class 实例。

System类

成员变量

  • printStream out

    标准输出流。此流已打开并准备接收输出数据。通常,此流对应于显示器输出或者由主机环境或用户指定的另一个输出目标。

  • InputStrean in

    标准输入流。此流已打开并准备提供输入数据。通常,此流对应于键盘输入或者由主机环境或用户指定的另一个输入源。

  • PrintStream err

    标准的错误输出流。其语法与 System.out 类似,不需要提供参数就可输出错误信息。也可以用来输出用户指定的其他信息,包括变量的值。

System 类的成员方法

System 类中提供了一些系统级的操作方法,常用的方法有 arraycopy()、currentTimeMillis()、exit()、gc() 和 getProperty()。

  1. arraycopy() 方法

    该方法的作用是数组复制,即从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。该方法的具体定义如下:

    public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length)
    

    其中,src 表示源数组,srcPos 表示从源数组中复制的起始位置,dest 表示目标数组,destPos 表示要复制到的目标数组的起始位置,length 表示复制的个数。

    1. currentTimeMillis()方法

      该方法的作用是返回当前的计算机时间,时间的格式为当前计算机时间与 GMT 时间(格林尼治时间)1970 年 1 月 1 日 0 时 0 分 0 秒所差的毫秒数。一般用它来测试程序的执行时间

    2. exit() 方法

      该方法的作用是终止当前正在运行的 Java 虚拟机,具体的定义格式如下:

      public static void exit(int status)
      

      其中,status 的值为 0 时表示正常退出,非零时表示异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

    3. gc() 方法

      该方法的作用是请求系统进行垃圾回收,完成内存中的垃圾清除。至于系统是否立刻回收,取决于系统中垃圾回收算法的实现以及系统执行时的情况。定义如下:

      public static void gc()
      
    4. getProperty() 方法

      该方法的作用是获得系统中属性名为 key 的属性对应的值,具体的定义如下:

      public static String getProperty(String key)
      

Java数组

最常见的一种数据结构,用一个标志符将相同类型的基本数据类型或者对象序列封装到一起。数组使用统一的数组名和不同的下标来唯一确定数组中的元素,是一个简单地线性序列,因此访问速度很快。

三个基本特征

  1. 一致性:数组只能保存相同数据类型元素,数据类型的任意的相同数据类型。
  2. 有序性:数组中的元素时有序的,通过下标访问。
  3. 不可变性:数组一旦初始化,长度不可变。

特点

  • 数组可以是一维数组、二维数组或多维数组
  • 数值型数组默认值为0,引用类型数组默认值为 null
  • 数组索引从0开始,n个元素的数组,索引为0 - (n-1)
  • 数组元素可以是任意类型,包括数组类型
  • 数组是抽象基类Arrays派生的引用类型

初始化

// 一维数组
type[] arrayName = new type[size];
type[] arrayName = new type[]{
   
   1,2,3,4,• • •,值 n};
type[] arrayName = {
   
   1,2,3,...,值 n};

// 二维数组
type[][] arrayName = new type[][]{
   
   1,2,3,,值 n};
type[][] arrayName = new type[size1][size2];
type[][] arrayName = new type[size][];

Arrays工具类

  1. void sort(type[] a)

    对数组a进行排序

  2. void sort(type[] a, int fromIndex, int toIndex)

    对数组a下标fromIndex到toIndex的元素进行排序

  3. String toString(type[] a)

    将数组转换成一个字符串,使用英文逗号和空格隔开

  4. boolean equals(type[] a, type[] a2)

    两数组的长度相同,且数组元素也一一相等,则该方法返回true

  5. XxxStream stream(xxx[] array)

    将数组转换为stream,Stream是java8新增的流式编程法人API

  6. XxxStream stream(xxx[] array, int startInclusive, int endExclusive)

    该方法与上一个方法相似,区别是该方法仅将 fromIndex 到 toIndex 索引的元索转换为 Stream。

  7. static <T> List<T> asList(T... a)

    将任意类型对象的数组转换为List。第一个T是泛型(可以是任意类型),第二个T是返回List的类型,第三个参数的类型

Java类和对象

Java面向对象

类:一组具有相同特性(属性)和相同行为(方法)的一组对象的集合

对象:真实的模型,一个具体的实体

面向对象简称OO(Object Oriented),面向对象分析(OOA)、面向对象设计(OOD)、面向对象程序设计(OOP)。

类是Java中一种重要的引用类型

三大核心特性

  • 封装:目的在于保护信息。封装的基本单位是类。Java提供私有和公有的访问模式。
  • 继承:子类拥有父类的全部特征和行为。Java只支持单继承。
  • 多态:父类中定义的属性和方法被子类继承后,可以有不同的属性和表现形式

类的定义

[public][abstract|final]<class|interface><class_name>[extends<class_name>][implements<interface_name>] {
   
   
    // 定义属性部分
    <property_type><property1>;
    <property_type><property2>;
    <property_type><property3>;// 定义方法部分
    function1();
    function2();
    function3();}

提示:上述语法中,中括号“[]”中的部分表示可以省略,竖线“|”表示“或关系”,例如 abstract|final,说明可以使用 abstract 或 final 关键字,但是两个关键字不能同时出现。

public:公有。被该修饰符修饰,可以被其他类和程序访问。每个Java程序的主类必须是public

abstract:抽象类。不能被实例化,可以有抽象方法(abstract修饰)和具体方法,继承该类必须实现类中所有的抽象方法,除非子类也是抽象类。

final:final修饰的类不可被继承。

extends:表示继承其他类

implements:表示实现其他类

class:类的关键字

命名规则

  • 以字母或者下划线_开头
  • 第一个字母最好大写,如有多个单词,每个单词的首字母最好大写
  • 不能包含除_$之外的特殊字符

类的属性(成员变量)

[public|protected|private][static][final]<type><variable_name>;

各参数的含义如下。

  • public、protected、private:用于表示成员变量的访问权限。
  • static:表示该成员变量为类变量,也称为静态变量。
  • final:表示将该成员变量声明为常量,其值无法更改。
  • type:表示变量的类型。
  • variable_name:表示变量名称。

初始化的默认值如下:

  • 整数型(byte、short、int 和 long)的基本类型变量的默认值为 0。
  • 单精度浮点型(float)的基本类型变量的默认值为 0.0f。
  • 双精度浮点型(double)的基本类型变量的默认值为 0.0d。
  • 字符型(char)的基本类型变量的默认值为 “\u0000”。
  • 布尔型的基本类型变量的默认值为 false。
  • 数组引用类型的变量的默认值为 null。如果创建了数组变量的实例,但没有显式地为每个元素赋值,则数组中的元素初始化值采用数组数据类型对应的默认值。

类的成员方法

完整的方法应包括方法名、方法主体、方法参数、方法返回值

public class Test {
   
   
    [public|private|protected][static]<void|return_type><method_name>([paramList]) {
   
   
        // 方法体
    }
}

各修饰符的含义如下。

  • public、private、protected:表示成员方法的访问权限。
  • static:表示限定该成员方法为静态方法。
  • final:表示限定该成员方法不能被重写或重载。
  • abstract:表示限定该成员方法为抽象方法。抽象方法不提供具体的实现,并且所属类型必须为抽象类。
  • paramList:参数列表

this关键字

调用static修饰的方法可省略this关键字,static修饰的方法中不能使用this关键字,静态成员不能访问非静态成员。

this.属性名:访问成员变量

this.方法名:访问成员方法

this():访问构造方法。只能写在构造方法中且必须是第一条语句。

创建对象

  • new 关键字
  • 调用java.lang.Class或者java.lang.reflect.Constructor的newInstance()的实例方法创建。
  • 调用对象的clone方法。使用该方法创建对象时,要实例化的类必须实现java.lang.Cloneable接口
  • 调用java.io.ObjectInputStream对象的readObject()方法
public class student implenents Cloneable{
   
   
    // 实现 Cloneable
    private String name;
    private int age;
    public static void main(String[] args)throws Exception {
   
   
        // 使用 new 关键字
        Student s = new Student();

        // 使用java.lang.Class 的 newInstance()
        Class c = Class.forname("com.xxx.Student");
        Student s = c.newInstance();

        // 调用对象的clone()方法
        Student s1 = (Student)s.clone();        
    }
}
  • 使用new关键字和newInstance()(默认无参构造)方法都会调用类的构造方法
  • clone()方法不会调用构造方法,他会复制一个对象,它和原对象具有相同的属性,但是有不同的内存地址。如果不实现Cloneable接口调用clone方法会报错 java.lang.CloneNotSupportedException。

通过反射创建新的类示例,有两种方式:
Class.newInstance()
Constructor.newInstance()

以下对两种调用方式给以比较说明:
Class.newInstance() 只能够调用无参的构造函数,即默认的构造函数;
Constructor.newInstance() 可以根据传入的参数,调用任意构造构造函数。

Class.newInstance() 抛出所有由被调用构造函数抛出的异常。

Class.newInstance() 要求被调用的构造函数是可见的,也即必须是public类型的;
Constructor.newInstance() 在特定的情况下,可以调用私有的构造函数。

Java虚拟机创建对象的步骤

  1. 给对象分配内存
  2. 将对象的实例变量自动初始化为其变量类型的默认值
  3. 初始化对象,给实例变量赋予正确的初始值

每个对象都是相互独立的,在内存中战友独立的内存地址,并且每个对象都有自己的生命周期。当一个对象的生命周期结束时对象就变成了垃圾,由Java虚拟机自带的垃圾回收机制处理。

匿名对象

就是没有明确给出名字的对象,是对象的一种简写方式。一般只是使用,匿名对象只在堆内存中开辟空间,不存在栈内存中的引用。又因为栈内存中没有任何引用,所以此对象使用一次后就等待GC回收。

匿名对象实际就是个堆内存,不管是不是匿名对象,都必须开辟堆内存后才可以使用。

对象的销毁

对象使用完后需要对其进行清除进而释放对象占用的内存,清除对象时由系统自动进行内存回收不需要用户额外处理。

Java语言的内存自动回收机制称为垃圾回收机制(GC)

Java的Object类提供一个protected类型的finalize()方法。因此任何Java类都可覆盖此方法,这个方法中进行释放对象所占有的相关资源的操作

Java虚拟机的堆区中,对象的三种状态

  1. 可触及状态:当一个对象被创建后,只要程序中还有引用它的变量,他就始终处于可触及状态。
  2. 可复活状态:当程序中不再有变量引用该对象,该对象就进入可复活状态,GC会准备释放所有可复活对象的内存,再释放之前会调用处于可复活状态对象的finalize()方法,这些finalize()有可能使该对象回到可触及状态。
  3. 不可触及状态:当Java虚拟机执行完所有可复活对象的finalize()方法后,如果这些对象没有恢复到可触及状态,GC才会真正回收它占用的内存。

System.gc()和Runtime.gc()方法不能保证回收操作一定执行,只是提高了Java垃圾回收期尽快回收垃圾的可能性。

java注释(类、方法、字段)

类注释:一般放在import后面,类定义前面

/**
 * @projectName(项目名称): project_name
 * @package(包): package_name.file_name
 * @className(类名称): type_name
 * @description(类描述): 一句话描述该类的功能
 * @author(创建人): user 
 * @createDate(创建时间): datetime  
 * @updateUser(修改人): user 
 * @updateDate(修改时间): datetime
 * @updateRemark(修改备注): 说明本次修改内容
 * @version(版本): v1.0
 */

方法注释

/**
 * @param num1: 加数1
 * @param num2: 加数2
 * @return: 两个加数的和
 * @throws:异常类描述:表示这个方法有可能抛出异常。
 */
/**
 * @description: 构造方法
 * @param name: 学生姓名
 * @param age: 学生年龄
 */

字段注释

/**
 * 用户名
 */
public String name;

/**用户名*/
public String name;

Java访问修饰符

Java提供多个作用域修饰符,常用的有public、private、protected、final、abstract、static、transient、volatile这些修饰符有类修饰符、变量修饰符、方法修饰符。

信息隐藏是OOP最重要的功能,也是使用访问修饰符的原因。

好处:

  • 防止对封装数据的未授权访问
  • 有助于保证数据的完整性
  • 当类的私有实现细节必须改变时,可以限制发生在整个应用程序中的连锁反应。

类的访问修饰符只能是空或者public

方法和属性的访问修饰符有4种:public、protected、friendiy(没有访问修饰符时的默认情况)、private

访问范围 private friendly(默认) protected public
同一个类 可访问 可访问 可访问 可访问
同一包中的其他类 不可访问 可访问 可访问 可访问
不同包中的子类 不可访问 不可访问 可访问 可访问
不同包中的非子类 不可访问 不可访问 不可访问 可访问
  1. private

    private 修饰的类成员只能被该类本身的方法访问和修改,不能被任何其他类包括子类访问和引用。private具有最高的保护级别。

  2. friendly(默认)

    一个类没有访问控制符,说明它具有吗,默认的访问控制符。只能被同一个包中的类范文和引用,不能被其他包访问,即使其他包中有该类的子类。同理,类的成员没写也说明他们具有包访问特性

  3. protected

    保护访问控制符,允许该类本身、一个包下、及其他包下的子类访问,主要作用是允许子类访问父类的属性及方法,否则使用默认访问符即可。

  4. public

    可被其他包进行访问。每个人Java的主类必须是public类。

Java static关键字

static修饰的成员变量称为静态变量、常量称为静态常量、方法称为静态方法,统称为静态成员,为类所有,不需要依赖对象进行访问,只要类被加载Java虚拟机就可通过类名找到他们。

静态方法不能调用非静态成员,否则编译时报错

静态变量

在类加载的过程中完成静态变量的内存分配

在类内部任何方法都可访问静态变量,在其他类中通过类名访问该类中的静态变量

静态变量可以被类的所有实例共享,可以看做实例之间的共享数据

静态方法

不需要所属类的实例就可调用,静态方法中不能使用this、super关键字,不能访问所属类的非静态变量

静态代码块

static{}主要用于初始化类,为类的静态变量符初始值。

  • 类似一个方法,但是不可存在与任何一个方法体中
  • 可以位于类地任何地方,一个类中可以有多个静态代码块
  • 每个静态代码块只会被执行一次
  • 和静态方法一样,不能直接访问类的实例变量和实例方法
  • 静态代码块在类加载的时候执行,非静态代码块在创建对象的时候执行

Java import static 静态导入

有两种语法:导入指定类的单个静态成员变量、方法和全部的静态变量和方法

import static的位置

// 导入单个静态成员,| 是或者的意思
import static package.ClassName.fieldOrMmethodName;
// 导入全部的静态成员,* 只能代表静态成员
import static package.ClassName.*;

Java final修饰符

final应用于类、方法、变量时意义不同,但本质都表示不可改变

  • final修饰的类不可被继承

  • final修饰的方法不可被重写

    • 如需要子类可以可以重写父类final修饰的方法,需将父类final修饰方法的访问修饰符修改为private
  • final修饰的变量一旦被初始化便不可被改变

    • final修饰的变量不能被赋值的说法是错误的,严格说final修饰的变量的值不可以被改变,一旦获得初始值就不能被重新赋值。
    • final修饰的局部变量必须在使用前赋值
    • final修饰的成员变量在声明时没有赋值的叫做空白final变量,此变量必须在构造方法(所有的构造方法)或者静态代码块(静态常量)中初始化
    • final修饰基本类型变量时,不能重新赋值,因为基本类型变量不可以改变。
    • final修饰引用类型变量时,引用保存的仅仅只是对象的地址,所以只需要保证一直引用同一对象,而这个对象完全可以发生改变
    • final声明变量时要求变量名全部大写

main()方法

main方法的定义必须是public static void main(String args[]),其中参数字符串数组名可以随意命名,但根据使用习惯一般命名为Java规范的agrs

一个类中只能有一个main方法。

main方法是以字符串数组的形式接收名两行参数。

Java方法的可变参数

语法格式

methodName({
   
   paramList},paramType…paramName)

methodName表示方法名,paramList表示固定参数列表,paramType表示可变参数类型,...是声明可变参数的标识,paramName标识可变参数的参数名称,可变参数必须定义在参数列表的最后。

说明:

  • 可变参数是指在调用方法时传入长度不确定的参数,本质上是基于数组的实现。
  • 可变参数必须是参数列表的最后一个,一个方法最多只能有一个可变参数。
  • 可变参数在编译为字节码文件后,在方法签名中是以数组的形态出现的,如果两个方法的签名在编译后是一致的(重载),则编译不通过
  • 可变参数可以接受数组类型的数据或者多个数据
  • 调用一个被重载的方法是,如果此调用既能调用固定长度的重载方法,又能调用可变参数的重载方法,优先调用固定长度的重载方法
  • 避免传入空值或null,当两个方法的方法名和固定参数列表都相同,只有可变参数不同时,传入空值或null会报错(修改方法:让编译器知道传入的null是什么类型的,即传入一个指向null的引用)

构造方法

类的特殊方法,用来初始化类的新对象。在创建对象(new关键字)之后自动调用,每一个类都有一个默认的无参构造,并且可以有一个以上的构造方法

  • 方法名和类名相同
  • 可以有0到多个参数
  • 没有返回值类型,包括void
  • 只能与new运算符结合使用
  • 如果类中没有定义任何的构造方法,则Java会自动为该类生成一个方法体为空的无参构造,如果类中显式定义了一个或多个构造方法,则Java不在提供默认的构造方法

如果给构造方法加上返回值类型或者void,编译不会报错,但是会将构造方法当做普通方法来处理

析构方法

与构造方法相反,当对象脱离其作用域时,系统自动执行析构方法。析构方法往往用来做清理垃圾碎片的工作

Java包(package)

提供了类的多层命名空间,解决类的命名冲突、类文件管理等问题。控制访问范围。

定义语法格式

package 包名;

规则:

  • 放在源文件的第一行,每个源文件中有且只能有一个包定义语句
  • 全部小写字母
  • 多个层次.分割
  • 自定义包不能由java开头

如果没有定义包,那么类、接口、枚举、注释类型文件将会放进一个无名的包

导入包

import 包名.类名;
import 包名.*;

系统包

说明
java.lang Java的核心类库,包含运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、异常处理和线程类,系统默认加载
java.lang.reflect 提供用于反射对象的类库
java.io Java语言的标准输入、输出类库,如基本输入、输出流,文件输入、输出流,过滤输入、输出流等
java.util 工具类,如处理时间的Date类,处理动态数组的Vector类,以及Stack和HashTable类
java.util.zip 实现文件压缩
java.security 提供安全性方面的有关支持
java.sql 实现JDBC的类库
java.awt 构建用户图形界面的类库
java.awt.image 处理和操纵来自网上的图片的类库
java.awt.datatransfer 处理数据传输的工具类,包括剪切板、字符串发送器等。
java.wat.peer 很少在程序中直接用到,使得同一个Java程序在不同的软硬件平台上运行
java.net 实现网络功能的类库
java.rmi 提供远程连接与载入的支持

Java的封装、继承、多态

Java类的封装

封装是将类的某些信息隐藏在类的内部,不允许外部程序直接访问,只能通过该类提供的方法来实现对隐藏信息的操作和访问。

  • 只能通过规定的方法访问数据
  • 隐藏类的实现细节,方便修改和实现

步骤:

  1. 修改属性的访问修饰符来限制对属性的访问,一般设为private。
  2. 为每个属性创建一对赋值(set)和取值(get)方法,一般为public,用于属性的读写
  3. 可以在赋值与取值方法中加入属性控制语句

Java的继承

在已有类的基础上进行扩展,从而产生新的类,已经存在的类称为父类、基类、超类,新产生的类称为子类、派生类,子类中不仅包含父类的属性和方法,还可以增加新的属性和方法。

使用extends关键字

public class Cat extends Animal{
   
   }
  • 类的继承并不改变类成员的访问权限。
  • 子类不能获得父类的构造方法
  • 如果父类中存在有参构造而没有无参构造,那么子类中必须含有有参构造,因为如果子类中不包含构造方法,会默认生成一个无参构造,而默认的无参构造中的第一行调用了super();,但是父类中并没有无参构造,所以会报错。
  • Java只支持单继承,一个类只能有一个直接父类,但是可以有多个间接父类
  • 父类中private的成员在子类中是不可见的

Java super关键字

子类不能继承父类的构造方法,但是可以使用super关键字来访问父类构造方法、普通方法、属性。

super访问父类构造方法

super();必须在子类构造方法的第一行

显式的调用父类构造方法,格式:

super(paramList);

super访问父类成员

当子类成员变量或者方法与父类重名,可是使用super关键字访问,super关键字与this关键字类似,只不过他引用的是子类的父类。

super与this的区别

  • this是当前对象的引用,super是当前对象的父对象的引用。
  • 如果构造方法的第一行不是this()或者super(),则系统默认添加super()
  • this 和super不能同时出现在一个构造方法里面,因为this必然会调用其他的构造方法,而其他的构造方法必然会有super语句的存在,同一个方法里重复出现了super()语句。
  • this() 和 suepr() 都是指的对象,所以均不可在static环境中使用
  • 本质上,this是一个指向对象的指针,而super是一个Java关键字。

Java类型转换

对象类型转换,是指存在继承关系的对象,不是任意类型的对象。当不存在继承关系的对象进行强制类型转换时,会抛出Java强制类型转换异常(java.lang.ClassCastException)

向上类型转换:父类引用指向子类对象

  • 不需要强制
  • 不能调用子类特有成员,但是子类重写的方法是有效的

向下类型转换:子类引用指向父类对象

  • 需要强制
  • 可以调用子类特有成员
  • 必须是先向上转型再向下转型,否则会出现ClassCastException异常,可使用instanceof进行类型判断

通过引用类型变量访问引用类型对象的属性与方法时,采用的绑定机制:

  • 实例方法与引用变量实际引用的对象(堆中存储的内容)进行绑定,这种属于动态绑定
  • 静态方法与引用变量的声明类型(栈中存储的内容)所绑定,这种属于静态绑定
  • 成员变量(包括静态变量与实例变量)与引用变量的声明类型的成员绑定,属于静态绑定

方法的重载

指同一个类中包含两个或两个以上方法同名的方法,但是参数列表不同(参数顺序也算)。

重载至于参数列表相关,和返回值类型、修饰符没有任何关系

口诀:同名不同参,返回值无关

方法的重写

子类中创建了一个与父类相同名称、相同返回值类型、相同参数列表的方法,只是方法体不同这种方式称为重写又称覆盖。

规则:

  1. 参数列表必须和被重写的方法相同
  2. 返回值类型必须和被重写方法返回值类型相同(1.5版本之前相等,之后版本小于等于即可)
  3. 访问权限大于等于被重写方法的访问权限
  4. 抛出的异常小于等于被重写方法抛出的异常
  5. 可以使用@Override标志重写的方法
  6. 声明为final的方法不可被重写
  7. 声明为static的方法不可被重写,但是能够被再次重新声明
  8. 如果方法不能继承,则方法不能别重写(访问权限修饰符public 、protected、default、private的访问级别)

子类成员变量的类型、名称都与父类中的相同,称之为变量隐藏

口诀:两同两小一大

  1. 方法名、参数列表相同;
  2. 重写后的方法的返回值类型小于等于重写前的方法
  3. 重写后的方法可抛出的异常小于等于重写前的方法;
  4. 重写后的方法访问权限大于等于重写之前的方法。

Java的多态性

多态分为运行时多态和编译时多态,编译时多态是静态的,主要指方法的重载。运行时多态是动态的通过动态绑定来实现的。

Java多态的3个必要条件:继承、重写、向上转型

instanceof关键字

双目运算符,用来判断一个对象是否为一个类(接口、抽象类、父类)的实例

abstract关键字

abstract修饰的类是抽象类,修饰的方法是抽象方法,抽象方法只有声明没有实现,只能用于普通方法,不能用于static或构造方法,因为抽象方法可以重写,而static方法和构造方法不可以被继承自然不可被重写。

抽象类:

  • 不可被创建对象,也就是不能使用new关键字
  • 抽象类可以有0~n个抽象方法,以及0~n个实例方法

抽象方法:

  • 没有方法体
  • 必须存在于抽象类中
  • 子类继承父类时必须重写所有的抽象方法

interface关键字

特殊的类,成员没有执行体,由全局常量和公共的抽象方法组成。

特征:

  • 接口可以有多个直接父类,但是接口只能继承接口,不能继承类。
  • 方法的声明不需要其他的修饰符,接口中声明的方法隐式的声明为public 和 abstract
  • 接口中声明的变量其实都是常量,将隐式的声明为public、static 、final,所以接口中的变量必须初始化
  • 接口没有构造方法,不能够实例化。

Java类的实现

使用implements关键字

注意:

  • 实现与继承类似,一样可以获得接口中定义的常量和方法
  • Java允许多实现。多个接口之间使用逗号隔开
  • implements必须放在extends后面
  • 实现一个或多个接口后,必须将接口中的所有抽象方法实现,否则必须将实现类定义为抽象类
  • 实现的方法访问修饰符必须为public,因为接口中的方法隐式的声明为public

Java内部类

在类的内部可以定义可定义成员变量和方法,也可以在类得内部定义另一个类,如果在Outer类每内部定义一个类Inner,Inner类称为内部类(或嵌套类),

类Outer称为外部类(宿主类)

内部类可以很好的隐藏,非内部类不允许有private和protected权限的,内部类可以,且内部类拥有外部类所有元素的访问权限

分类:

  • 实例内部类
  • 静态内部类
  • 成员内部类

内部类的特点:

  • 内部类是一个独立的类,在编译后会被编译成独立的.class文件,但是前面会冠以外部类的类名以及$符号

  • 内部类不能以普通的方式访问,内部类是外部类的一个成员,内部类访问外部类成员不受访问修饰符控制

  • 内部类声明成静态的就不能随便的访问外部成员变量,只能访问外部类的静态的成员变量

  • 外部类只有良好总访问级别public、default;内部类有4种访问权限public、protected、default、private

  • 内部类不能与外部类重名

  • 需要通过完整类名访问内部类

    Outer.Inner class = new Outer().new Inner();
    

实例内部类

指没有使用static修饰的内部类,也称非静态内部类

特点:

  1. 在外部类的静态方法和其他类中访问内部类需要通过外部类的实例创建内部类的实例,否则就可以直接创建内部类的实例(new关键字创建)
  2. 可以访问外部类的所有成员。如果有多层嵌套可以访问所有外部类的成员
  3. 外部类不能访问内部类成员,必须通过内部类的实例进行访问
  4. 外部类与内部类是一对多的关系
  5. 内部类不能使用static,除非同时使用final和static

静态内部类

指使用static修饰的内部类。

特点:

  1. 创建内部类的实例时,不需要创建外部类的实例

    public class Outer {
         
         
        static class Inner {
         
         
        }
    }
    class OtherClass {
         
         
        Outer.Inner oi = new Outer.Inner();
    }
    
  2. 静态内部类可单独使用static,可以定义静态成员和实例成员

  3. 静态内部类可直接访问外部类的静态成员,若要访问实例成员需要创建外部类的实例进行访问

局部内部类

指在方法中定义的内部类。

特点:

  1. 局部内部类和局部变量一样,不能使用访问权限修饰符和static修饰
  2. 局部内部类只在当前方法中生效
  3. 不能定义static成员
  4. 局部内部类可以包含内部类,同样不能使用访问权限修饰符和static修饰
  5. 局部内部类可以访问外部类的所有成员
  6. 局部内部类只可以访问当前方法中的final类型的参数与变量

Java匿名类

指没有类名的内部类,必须在创建时使用new关键字来声明类。

new <类或接口>() {
   
   
    // 类的主体,声明的对象若是有抽象方法,则这里必须实现
};

匿名内部类有两种实现方式:

  • 继承一个类,重写其方法。
  • 实现一个接口(可以是多个),实现其方法。

特点:

  1. 匿名类和局部内部类一样,可以访问外部类的所有成员。若是匿名类在一个方法中只能的访问final类型的局部变量和参数。在Java 8及其后版本中一个非final的局部变量或者方法参数在初始化后就未更改,那个该变量就是Effectively final的。
  2. 匿名类中允许使用非静态代码块进行成员初始化。
  3. 匿名类的非静态代码块会在父类的构造方法后执行。

Java 8新特性:Effectively final

Java中要求局部内部类和匿名内部类访问的局部变量要求必须是final修饰的,以保证内部类和外部类数据的一致性。Java 8之后我们可以不加final修饰符,有系统默认添加。这个功能称之为 Effectively final。

从Java 8 开始,他不要求程序员必须将访问的局部变量显式的声明为final,只要该变量不被重新赋值即可。

一个非final的局部变量或者方法参数,其值在初始化后就未改变,那么该变量就是effectively final。

在lambda表达式和匿名内部类中非常有用。

Java Lambda表达式

Lambda表达式是一个匿名函数,基于数学的λ演算得名,可称之为闭包。它允许把函数当做一个方法的参数(函数作为参数传递进方法中)。

==Lambda表达式本质就是为了简写接口实现而存在的语法糖==

标准语法:

(参数列表) -> {
   
   
    // Lambda表达式主体
}

->被称为箭头操作符或者Lambda操作符,箭头操作符将函数分割为两部分

  • 左侧:Lambda表达式的参数列表
  • 右侧:Lambda表达式中需要执行的功能,使用{}包裹起来

优点:

  • 代码简洁,开发迅速
  • 方便函数式编程
  • 进行并行计算
  • 引入了Stream API,改善了集合操作

缺点:

  • 可读性差
  • 在非并行计算中,未必有for的性能高
  • 不容易调试

Lambda实现的接口不能是普通的接口,而是函数式接口。

如果在一个接口中有且只有一个抽象方法(Object类中的方法除外),那么这个接口就可以看做函数式接口。如果接口中声明多个抽象方法,那么Lambda表达式会发生编译错误。

为了防止函数式接口声明多个抽象方法,Java 8 提供一个声明函数式接口的注解@FunctionalInterface,该注解可用于一个接口的定义上,一旦使用编译器会强制检查接口是否确实有且仅有一个抽象方法,否则会报错。即使不使用该注解,满足函数式接口的定义这仍是一个函数式接口。

使用@FunctionalInterface超过一个抽象方法会报错,但是可以添加默认方法和静态方法。

Lambda表达式是一个匿名方法代码,因为匿名方法必须声明在类或者接口中,所以Lambda表达式所实现的匿名方法就是在函数式接口中声明的。

函数式接口特点:

  • 接口有且仅有一个抽象方法
  • 允许定义静态方法
  • 允许定义默认方法
  • 允许java.lang.Object中的public方法

JDK 8 之后新增了一个函数接口包 java.util.function 这里面包含了我们常用的一些函数式接口

接口 参数 返回值 说明
Function T R 接受一个输入参数 T,返回一个结果 R
BiFunction T, u R 接受两个输入参数 TU,返回一个结果 R
Predicate T boolean 接受一个输入参数 T,返回一个布尔值结果
Supplier None T 无参数,返回一个结果,结果类型为 T
Consumer T void 代表了接受一个输入参数 T 并且无返回的操作
UnaryOperator T T 接受一个参数为类型 T,返回值类型也为 T
BinaryOperator (T,T) T 代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果

使用Lambda表达式

作为参数使用Lambda表达式

需要声明参数的类型声明为函数式接口类型。

访问变量

Lambda可以访问所在外层作用域定义的变量,包括成员变量和局部变量。

Lambda与普通方法一样可以读取成员变量,也可以修改成员变量。成员变量与局部变量重名时也可使用this关键字

访问局部变量时,变量必须是final类型的(不可改变的),且不可修改局部变量。

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

lambda表达式只可以读取局部变量,但是可以读写成员变量(包括静态的)

方法引用

Java 8之后增加了双冒号::运算符,该运算符用于“方法引用”,而不是方法调用。方法引用虽然没有直接使用Lambda表达式,但是与之有关,与函数式接口有关。

ObjectClass::methodName    // ObjectClass是类名,methodName是方法名
  • 静态方法引用

    调用类的静态方法

    1. 被引用的方法的参数列表和接口中的方法的参数相同
    2. 接口方法没有返回值的,引用方法可以有返回值也可以没有返回值
    3. 接口方法有返回值的,引用方法必须有返回值
    public interface Finder {
         
         
        public int find(String s1, String s2);
    }
    //创建一个带有静态方法的类
    public class StaticMethodClass{
         
         
        public static int doFind(String s1, String s2){
         
         
            return s1.lastIndexOf(s2);
        }
    }
    Finder finder = StaticMethodClass :: doFind;
    //调用find方法
    int findIndex = finder.find("abc","bc");
    

    在这里Finder 接口的 find 方法和类 StaticMethodClassdoFind 方法有相同的输入参数(参数个数和类型)完全相同,又因为 doFind 方法是一个静态方法,于是我们就可以使用静态方法引用了。此时,Finder 接口引用了 StaticMethodClass 的静态方法 doFind。

  • 参数方法引用

    可以将参数的一个方法引用到Lambda表达式中

    1. 接口方法和引用方法必须有相同的参数和返回值
    public interface Finder {
         
         
        public int find(String s1, String s2);
    }
    // 参数方法引用
    // Finder finder =(s1,s2)-> s1.indexOf(s2);
    Finder finder = String :: indexOf;
    
    // 调用find方法
    int findIndex = finder.find("abc","bc")
    // 输出find结果。
    System.out.println("返回结果:"+findIndex);
    

    我们希望 Finder 接口搜索参数 s1 的出现参数 s2 的位置,这个时候我们会使用 Java String 的 indexOf 方法 String.indexOf 来进行查询,我们发现,接口 Finderfind 方法与 String.indexOf 有着相同的方法签名(相同的输入和返回值),那么我们就可以使用参数方法引用来进一步简化

  • 实例方法引用

    直接调用实例的方法

    1. 接口方法和实例方法必须具有相同的参数和返回值
    public interface Serializer {
         
         
        public int serialize(String v1);
    }
    public class StringConverter {
         
         
        public int convertToInt(String v1){
         
         
            return Integer.valueOf(v1);
        }
    }
    StringConverter stringConverter = new StringConverter(); 
    Serializer serializer = stringConverter::convertToInt;
    

构造器引用

ObjectClass::new
public interfact MyFactory{
   
   
    public String create(char[] chars)
}
MyFactory myfactory =  String::new;
// MyFactory myfactory = chars->new String(chars);

Lambda表达式与匿名内部类的联系与区别

Lambda 表达式与匿名内部类主要存在如下区别。

  • 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。
  • 匿名类的this、super指针指向的是其自身的实例,而Lambda表达式的this、super指针指向的是创建这个Lambda表达式的类对象的实例。
  • 匿名类会编译出父类名$1.class的class文件,而Lambda表达式不会编译出class文件。编译器不认为Lambda表达式是一个完全的类(或者说他是一个特殊的类对象)

Java异常处理

异常简介

异常(exception)是在运行程序时产生的一种异常情况。又称例外,它中断正在执行程序的正常指令流。为了能够及时有效的处理程序中的运行错误,必须使用异常类,可以让程序具有更好的容错性更加健壮。

异常产生的原因:

  1. Java内部错误发生异常,Java虚拟机产生的异常。
  2. 编写的程序发生的异常。
  3. 通过throw语句手动生成的异常,用来告知该方法的调用方一些必要信息。

Java通过面向对象的方式处理异常,在一个方法的运行过程中如果发生了异常,则这个方法或产生一个代表该异常的对象,并把它交给运行时的系统,运行时系统寻找相应的代码来处理这一异常。

我们把生成异常对象并把它提交给运行时系统的过程称之为==抛出异常==(throw)。运行时系统在方法的调用栈中查找,直到找到能够处理该类异常的对象,这一过程称之为==捕获异常==(catch)。

graph BT;
java.lang.Error-->java.lang.Throwable;
java.lang.Exception-->java.lang.Throwable;
\(RuntimeException   运行时异常)-->java.lang.Exception;
非运行时异常-->java.lang.Exception;

由上图可知,Java中的所有异常类型都是内置类Java.lang.Throwable类的子类,Throwable有两个子类Exception(异常)和Error(错误)。其中Exception又分为运行时异常和非运行时异常。

==在Java中只有继承了Throwable类才可以被throw或catch==

  • Exception类用于用户程序可能出现的异常情况,他也是用来创建自定义异常类型类的类。是程序运行过程中可以预料到的意外情况,并且应该被捕获并进行响应的处理。
  • Error定义了在通常情况下不希望被程序捕获的异常,一般值JVM出错,如堆栈溢出。正常情况不会出现的情况,大多数Error都会导致程序处于非正常、不可恢复状态。所以不需要捕获。无法通过技术处理解决,属于未检查类型

Exception:

  • 运行时异常:RuntimeException类及其子类,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理,一般是由于程序逻辑错误引起。

    | 异常类型 | 说明 |
    | ----------------------------- | ----------------------------------------------------- |
    | NullPointerException | 尝试访问 null 对象成员,空指针异常 |
    | IndexOutOfBoundsException | 某种类型的索引越界 |
    | ArraylndexOutOfBoundException | 数组索引越界 |
    | ClassCastException | 类型转换异常 |
    | ArithmeticException | 算术错误异常,如以零做除数 |
    | ArrayStoreException | 向类型不兼容的数组元素赋值 |
    | IllegalArgumentException | 使用非法实参调用方法 |
    | lIIegalStateException | 环境或应用程序处于不正确的状态 |
    | lIIegalThreadStateException | 被请求的操作与当前线程状态不兼容 |
    | NegativeArraySizeException | 再负数范围内创建的数组 |
    | NumberFormatException | 数字转化格式异常,比如字符串到 float 型数字的转换无效 |
    | TypeNotPresentException | 类型未找到 |

  • 非运行时异常:指RuntimeException类以外,类型上都属于Exception类及其子类的异常。是必须处理的异常,否则编译不能通过。

    | 异常类型 | 说明 |
    | ---------------------------- | -------------------------- |
    | ClassNotFoundException | 没有找到类 |
    | IllegalAccessException | 访问类被拒绝 |
    | InstantiationException | 试图创建抽象类或接口的对象 |
    | InterruptedException | 线程被另一个线程中断 |
    | NoSuchFieldException | 请求的域不存在 |
    | NoSuchMethodException | 请求的方法不存在 |
    | ReflectiveOperationException | 与反射有关的异常的超类 |

异常处理机制及处理基本结构

Java异常的处理通过5个关键字:try、catch、throw、throws、finally。try catch语句用于捕获并处理异常,finally用于在任何情况下都必须执行的代码,throw语句用于抛出异常。throws语句用于声明可能会出现的异常。

异常的处理机制

  • 在方法中使用try catch捕获异常并处理,catch可以有多个用来匹配多个异常
  • 对于处理不了的异常或者要转型的异常,在方法的声明处通过throws语句抛出异常,由调用者处理。
try{
   
   
    // 逻辑代码块
} catch (ExceptionType1 e){
   
   
    // 处理代码块1
} catch (ExceptionType2 e){
   
   
    // 处理代码块1
    throw(e)
} finally {
   
   
    // 释放资源代码块
}

try catch 语句详解

Java中通常采用try catch语句来捕获异常并处理。将可能引发异常的语句封装在try语句块中,用以捕获可能发生的异常。catch后的()中放匹配的异常类,指明catch语句可以处理的异常类型,发生异常时产生异常类的实例化对象。

如果try语句块中发生异常,那么一个相应的异常对象就会被抛出,然后catch语句会依据抛出的异常类型进行捕获并处理,处理后程序会跳过try语句块中剩余的语句并转到catch语句块中的第一条语句开始执行。

如果try语句块中没有发生异常,那么try语句块正常结束,后面的catch语句块被跳过,程序将从catch语句块后的第一条语句开始执行。

try catch中的花括号{}不可以省略。

catch代码块中输出异常信息的3个方法

  • printStackTrace()方法:指出异常的类型、性质、栈层级、程序中出现的位置
  • getMessage()方法:指出错误的性质
  • toString()方法:给出异常的类型和性质

多重catch语句

  • 当一个catch捕获一个异常后,其他的catch代码块就不再进行匹配。
  • 如果捕获的多个异常存在父子关系,捕获异常一般先捕获子类,再捕获父类。若是父类在子类前面先捕获,则子类永远也捕获不到。

finally

==无论发生异常与否,finally中的代码都会被执行,除非在try或catch中调用了退出虚拟机的方法System.exit()==

根据try catch语句的执行过程可知try语句和catch语句有可能不完全执行,而有些代码要求必须执行物理资源(数据库连接、网络连接、磁盘文件)的显示回收

Java的垃圾回收机制不会回收任何物理资源,只会回收堆内存中对象多占用的内存

为了确保一定能回收try中打开的物理资源,异常处理机制提供了finally代码块,并且Java 7之后提供了自动资源管理技术

使用try-catch-finally的注意:

  1. 异常处理语法结构中只有try块是必须的,catch块和finally块都是可选的,但是catch块和finally块至少出现一个(==使用了自动资源管理的try块除外==),也可以同时出现。
  2. 可以有多个catch块,捕获的父类异常必须在子类异常后面
  3. 顺序必须是先 try 再 catch 后 finally

Java中finally和return的执行顺序

  1. finally语句在return语句执行之后return返回之前执行的

  2. finally中的return语句会覆盖try中的return返回

  3. 如果finally语句中没有return语句覆盖返回值,但是原来的返回值可能会因为finally里的修改而改变

    常量存储的是常量值,常量值不会修改;变量存的是地址,地址不会修改,但是地址所指向的内容修改了

自动资源管理(Automatic Resource Management)

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

当try块结束时,自动释放资源。不需要在显示的调用close()方法,也称为带资源的try语句

注意:

  • try语句中声明的资源被隐式的声明为final,资源的作用局限于带资源的try语句
  • 一条try语句可以初始化多个资源,每个资源以应为分号分隔;
  • 需要关闭的资源必须实现了AutoCloseable或Closeable接口
  • 自动关闭资源的try语句相当于隐式的包含了finally块,因此这个try块可以既没有catch有没有finally。如果程序需要,自动关闭资源的try块后面可以带多个catch块和一个finally块

Closeable 是 AutoCloseable 的子接口,Closeable 接口里的 close() 方法声明抛出了 IOException,因此它的实现类在实现 close() 方法时只能声明抛出 IOException 或其子类;AutoCloseable 接口里的 close() 方法声明抛出了 Exception,因此它的实现类在实现 close() 方法时可以声明抛出任何异常。

Java throws和throw:声明和抛出异常

Java中的异常处理除了捕获和处理异常外。还包括声明异常和抛出异常。通过throws关键字在方法上声明要抛出的异常,然后再方法内部通过throw抛出异常对象。

public AjaxResult reject(Map map) throws Exception {
   
   }
  • 如果有多个异常类可以使用,分开。如Exception 1,Exception2
  • 这些异常类可以是方法中调用了可能抛出异常的方法而产生的异常,也可以是方法体生成并抛出的异常。
  • 使用throws抛出异常的使用思路。当前方法不知道如何处理这类异常,将异常抛给上一层调用者处理;如果main方法也不知道如何处理,也可以使用throws抛出,将该异常交给JVM处理,JVM对异常的处理方式是打印异常跟踪栈信息,终止程序运行。
  • 重写时,子类抛出的异常类应小于等于父类抛出的异常类(小于 ---> 子类),父类的throws可以限制子类的行为
throw ExceptionObject;
  • ExceptionObject必须是Throwable类或其子类的==对象==。
  • 在throw语句执行时,他后面的语句将不执行,此时程序转向调用者,直到找到异常程序处理程序,终止程序并打印调用栈情况。

throws关键字和throw关键字的区别

  • throws用来声明一个方法可能抛出的所有异常信息,表示异常出现的可能,不一定会发生。throw是抛出一个具体的异常类型,执行throw则一定抛出了某种异常信息。
  • throws通常不显示的捕获异常,可由系统自动的将所有捕获的异常信息抛给调用程序;throw则需要用户自己捕获相关的异常对其进行包装后,再将异常信息抛出。

多异常捕获

Java 7新特性

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

上面两个try catch的用法相同

注意:

  • 捕获多种类型异常时,用竖线|隔开
  • 捕获多种类型异常时,异常类型不能存在父子关系。
  • 捕获多种类型异常时,异常变量由隐式final修饰,因此程序不可对异常变量重新赋值

自定义异常

当Java提供的内置异常类型不能满足程序设计的需求室,我们可以自己设计Java类库或框架,其中包括异常类型。实现自定义异常类需要继承Exception类或其子类,如果实现运行时自定义异常类需要继承RuntimeException类及其子类。

  • 一般包含两个构造方法,无参构造和一个接收字符串参数构造方法,用以将消息传递给超类的构造方法。
  • 自定义异常继承自Exception类,因此包含父类中的所有方法和属性。

异常处理规则

==异常处理机制的效率比正常的流程控制效率差==

  1. 不要过度使用异常
  2. 不要使用过于庞大的try块
  3. 避免使用catch (Throwsble t)catch (Exception e)语句
  4. 不要忽略捕获到的异常

Java集合、泛型、枚举

Java集合详解

为了保存数量不确定的数据,以及具有映射关系的数据(也称关联数组),Java提供了集合类。集合类主要负责盛装、保存其他数据,因此集合类也被称为容器类。==所有的集合类都位于java.util包下==

集合类和数组不一样,数组元素既可以是基本类型的值,也可以是对象(实际上保存的是对象的引用)。集合里只能保存对象(实际也是对象的引用变量)。

Java集合类型分为Collection和Map,他们是Java集合根接口,它们包含一些子接口或实现类。

graph TB;
Java集合-->java.lang.Iterable;
java.lang.Iterable-->java.util.Collection;
java.util.Collection-->java.util.Queue;
java.util.Queue-->id1([PriorityQueue]);
java.util.Queue-->Dueue;
Dueue-->id3([ArrayDueue]);
Dueue-->id2([LinkedList]);
java.util.Collection-->java.util.List;
java.util.List-->id2([LinkedList]);
java.util.List-->id4([ArrayList]);
java.util.List-->id5([Vector]);
id5([Vector])-->id6([Stack]);
java.util.Collection-->java.util.Set;
java.util.Set-->id7([TreeSet]);
java.util.Set-->id8([HashSet]);
id8([HashSet])-->id10([LinkedHashSet]);
java.util.Set-->id9([EnumSet]);
id9([EnumSet])-->id11([JumboEnumSet]);
id9([EnumSet])-->id12([RegularEnumSet]);
Java集合-->java.util.Map;
java.util.Map-->id13([HashMap]);
id13([HashMap])-->id14([LinkedHashMap]);
java.util.Map-->id15([WeakHashMap]);
java.util.Map-->id16([IdentityHashMap]);
java.util.Map-->id17([Hashtable]);
id17([Hashtable])-->id18([Properties]);
java.util.Map-->id19([TreeMap]);
java.util.Map-->id20([EnumMap]);

==矩形块为集合的接口,椭圆形块为集合的实现类==

简单记忆线程安全的集合类: 喂! SHE!C! 喂是指 vector S 是指 stack H 是指 hashtable E 是指: EenumerationC指的是ConcurrentHashmap、Collections、CopyOnWirteArrayList

ListsynArrayList = Collections.synchronizedList(new ArrayList());
SetsynHashSet = Collections.synchronizedSet(new HashSet());
MapsynHashMap = Collections.synchronizedMap(new HashMap());

集合接口的作用

接口名称 作用
Iterable接口 意为可迭代的,Collection继承了Iterable接口。接口中定义了一个抽象方法Iterator<T> iterator();用于获取迭代器。Java 8之后定义了两个默认方法forEach:用于内部元素的遍历,spliterator:提供了一个可以并行遍历元素的迭代器。
Iterator 接口 迭代器。定义了4个方法boolean hasNext();E next();void remove()void forEachRemaining()
Collection 接口 是 List、Set 和 Queue 的父接口,是存放一组单值的最大接口。所谓的单值是指集合中的每个元素都是一个对象。一般很少直接使用此接口直接操作。
Queue 接口 Queue 是 Java 提供的队列实现,有点类似于 List。
Dueue 接口 是 Queue 的一个子接口,为双向队列。
List 接口 是最常用的接口。是有序集合,允许有相同的元素。使用 List 能够精确地控制每个元素插入的位置,用户能够使用索引(元素在 List 中的位置,类似于数组下标)来访问 List 中的元素,与数组类似。
Set 接口 无序集合。不能包含重复的元素。
Map 接口 是存放一对值的最大接口,即接口中的每个元素都是一对,以 key➡value 的形式保存。

集合实现类的作用

类名称 作用
HashSet 类 为优化查询速度而设计的Set。它基于HashMap实现,底层使用HashMap保存所有元素。
TreeSet 类 一个有序的Set。
ArrayList 一个用数组实现的List,能够快速的随机访问,效率高且大小可变的数组。
LinkedList 对顺序访问进行优化,但是随机访问效率先对较慢,可以把它当做栈(Stack)和队列(Queue)使用。
ArrayDueue 一个基于数组实现的双端队列,按照先进先出的方式操作集合元素
HsahMap 按照哈希算法来存取键对象
TreeMap 可以对键对象进行排序

Java Collection接口

Collection接口是list、Set和Queue接口的父接口,通常情况不被直接使用。

Collection接口的常用方法

方法名称 说明
boolean add(E e); 向集合中添加一个元素,如果集合对象被添加操作改变了,则返回true。E是数据元素的类型。
boolean addAll(Collection c); 向集合中添加集合c的所有元素,如果集合对象被添加操作改变了,则返回true。
void clear(); 清除集合中所有的元素,将集合的长度变为0。
boolean contains(object o); 判断集合中是否包含指定元素
boolean containsAll(Collection c); 判断集合是否包含集合c中的所有元素。
boolean isEmpty(); 判断集合是否为空
Iterator iterator(); 返回一个Iterator对象(迭代器),用于遍历集合中的元素
boolean remove(Object o); 从集合中删除一个指定元素,当集合中包含了一个或多个元素 o 时,该方法只删除第一个符合条件的元素,该方法将返回true。
boolean removeAll(Collection c); 从集合中删除所有在集合c中出现的元素,如果该操作改变了调用该方法的集合,则返回true。
boolean retainAll(Collection c); 从集合中删除集合c不包含的元素(相当于求交集),如果该操作改变了调用该方法的集合,则返回true。
int size(); 返回集合中元素的个数。
Object[] toArray(); 将集合转换为数组,集合元素改变为数组元素。
Java List集合

List是一个有序、可重复的集合。每个元素都有对应的顺序索引。可以通过索引访问指定位置元素。常用的实现类有ArrayList类和LinkedList类。

List集合方法

方法名称 说明
replaceAll(UnaryOperator<E> operator) 对数组中的元素统一进行某种操作(例如加减数值),具体的操作由lambda表达式实现UnaryOperator函数式接口自定义。
sort(Comparator<? super E> c) 排序,传入自定义Lambda表达式,与Collections.sort(List<T> list, Comparator<? super T> c)用法相同
E get(int index) 返回列表中指定位置的元素
E set(int index, E element); 替换列表中指定位置的元素
int indexOf(Object o); 返回元素 o 第一次出现的位置(下标),不存在返回-1
int lastIndexOf(Object o); 返回元素 o 最后一次出现的位置(下标),不存在返回-1
List<E> subList(int fromIndex, int toIndex); 截取数组,参数为开始和结束位置(下标,左含右不含)

ArrayList类

实现了可变数组的大小,存储在内的数据称为元素。提供了快速基于索引访问元素的方式,对尾部成员的增加删除友好,可以快速的随机访问,插入与删除速度相对较慢。

构造方法:

  • ArrayList():构造一个初始容量为10的空列表。
  • ArrayList(int initialCapacity):构造一个具有初始容量的列表
  • ArrayList(Collection<? extends E> c):构造一个列表,包含指定集合的元素。按照Collection的迭代器返回的顺序排列。

LinkedList类

采用链表结构保存对象,便于插入或删除,随机访问速度慢。

构造方法:

  • LinkedList():构造一个空列表。双向链表,没有初始大小
  • LinkedList(Collection<? extends E> c):构造一个列表,包含指定集合的元素。按照Collection的迭代器

LinkedList特别提供的方法

方法名 作用
void addFirst(E e) 将指定元素添加到此集合的开头
void addLast(E e) 将指定元素添加到此集合的末尾
E getFirst() 返回此集合的第一个元素
E getLast() 返回此集合的最后一个元素
E removeFirst() 删除此集合中的第一个元素
E removeLast() 删除此集合中的最后一个元素

ArrayList与LinkedList 类的区别

  • 都是 List 接口的实现类,因此都实现了 List 的所有未实现的方法,只是实现的方式有所不同。
  • ArrayList 是基于动态数组数据结构的实现,随机访问速度快。
  • LinkedList 是基于链表数据结构的实现,占用的内存空间比较大,但在批量插入或删除数据时较快。
Java Set集合

Set无序、不能重复、最多含有一个null元素。

HashSet类

是Set集合的典型实现,大多数时候使用Set集合就是使用的这个实现类。HashSet是按照Hash算法来存储集合中的元素的,具有很好的存取和查找性能。

特点:

  • 无序
  • HashSet不是同步的,如果多个线程同时访问或修改一个HashSet,则必须通过代码控制保证其同步
  • 可以存null,且只能有一个

当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的 hashCode 值,然后根据该 hashCode 值决定该对象在 HashSet 中的存储位置。如果有两个元素通过 equals() 方法比较返回的结果为 true,但它们的 hashCode 不相等,HashSet 将会把它们存储在不同的位置,依然可以添加成功。

在 HashSet 类中实现了 Collection 接口中的所有方法。HashSet 类的常用构造方法重载形式如下。

  • HashSet():构造一个新的空的 Set 集合。
  • HashSet(Collection<? extends E>c):构造一个包含指定 Collection 集合元素的新 Set 集合。其中,“< >”中的 extends 表示 HashSet 的父类,即指明该 Set 集合中存放的集合元素类型。c 表示其中的元素将被存放在此 Set 集合中。

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 的值。

JDK中实现了Comparable的类,以及他们比较大小的方式

比较方式
包装类(BigDecimal、BigInteger、Byte、Double、Float、Integer、Long 及 Short) 按数字大小比较
Character 按字符的 Unicode 值的数字大小比较
String 按字符串中字符的 Unicode 值的数字大小比较

reeSet 类除了实现 Collection 接口的所有方法之外,还提供了如表所示的方法。

方法名称 说明
E first() 返回此集合中的第一个元素。其中,E 表示集合中元素的数据类型
E last() 返回此集合中的最后一个元素
E poolFirst() 获取并移除此集合中的第一个元素
E poolLast() 获取并移除此集合中的最后一个元素
SortedSet subSet(E fromElement,E toElement) 返回一个新的集合,新集合包含原集合中 fromElement 对象与 toElement 对象之间的所有对象。包含 fromElement 对象,不包含 toElement 对象
SortedSet headSet<E toElement〉 返回一个新的集合,新集合包含原集合中 toElement 对象之前的所有对象。 不包含 toElement 对象
SortedSet tailSet(E fromElement) 返回一个新的集合,新集合包含原集合中 fromElement 对象之后的所有对 象。包含 fromElement 对象

Java Map详解

Map是一种键值对(key-value)集合用于保存具有映射关系的数据。key和value可以是任何引用类型的数据。key不可以重复,value可以重复。key与value是一一对应的。

Map主要有两个实现类:HashMap和TreeMap。HashMap类按照哈希算法来存取键对象,TreeMap可以对键对象进行排序。

常用方法:

方法名称 说明
int size() Map集合的长度(键值对的个数)
boolean isEmpty() 如果这个Map不包含键值对则返回true
boolean containsKey(Object key) Map集合如果包含指定的key则返回true
boolean containsValue(Object value) Map集合如果包含指定的value则返回true
V get(Object key) 获取集合中指定key的值,V 表示值的数据类型
V put(K key, V value) 向 Map 集合中添加键-值对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。
V remove(Object key) 删除集合中指定key的键值对
void putAll(Map<? extends K, ? extends V> m) 将指定Map的键值对复制进本Map
void clear() 清空Map集合
Set keySet() 返回 Map 集合中所有键对象的 Set 集合
Collection values() 返回该 Map 里所有 value 组成的 Collection
Set> entrySet() 返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry(内部类)

==TreeMap的使用方法与HasnMap的使用方法相同,唯一不同的是TreeMap可以对进对象进行排序==

Java 8 除了为 Map 增加了 remove(Object key, Object value) 默认方法之外,还增加了如下方法。

名称 说明
Object compute(Object key, BiFunction remappingFunction) 该方法使用 remappingFunction 根据原 key-value 对计算一个新 value。只要新 value 不为 null,就使用新 value 覆盖原 value;如果原 value 不为 null,但新 value 为 null,则删除原 key-value 对;如果原 value、新 value 同时为 null,那么该方法不改变任何 key-value 对,直接返回 null。
Object computeIfAbsent(Object key, Function mappingFunction) 如果传给该方法的 key 参数在 Map 中对应的 value 为 null,则使用 mappingFunction 根据 key 计算一个新的结果,如果计算结果不为 null,则用计算结果覆盖原有的 value。如果原 Map 原来不包括该 key,那么该方法可能会添加一组 key-value 对。
Object computeIfPresent(Object key, BiFunction remappingFunction) 如果传给该方法的 key 参数在 Map 中对应的 value 不为 null,该方法将使用 remappingFunction 根据原 key、value 计算一个新的结果,如果计算结果不为 null,则使用该结果覆盖原来的 value;如果计算结果为 null,则删除原 key-value 对。
void forEach(BiConsumer action) 该方法是 Java 8 为 Map 新增的一个遍历 key-value 对的方法,通过该方法可以更简洁地遍历 Map 的 key-value 对。
Object getOrDefault(Object key, V defaultValue) 获取指定 key 对应的 value。如果该 key 不存在,则返回 defaultValue。
Object merge(Object key, Object value, BiFunction remappingFunction) 该方法会先根据 key 参数获取该 Map 中对应的 value。如果获取的 value 为 null,则直接用传入的 value 覆盖原有的 value(在这种情况下,可能要添加一组 key-value 对);如果获取的 value 不为 null,则使用 remappingFunction 函数根据原 value、新 value 计算一个新的结果,并用得到的结果去覆盖原有的 value。
Object putIfAbsent(Object key, Object value) 该方法会自动检测指定 key 对应的 value 是否为 null,如果该 key 对应的 value 为 null,该方法将会用新 value 代替原来的 null 值。
Object replace(Object key, Object value) 将 Map 中指定 key 对应的 value 替换成新 value。与传统 put() 方法不同的是,该方法不可能添加新的 key-value 对。如果尝试替换的 key 在原 Map 中不存在,该方法不会添加 key-value 对,而是返回 null。
boolean replace(K key, V oldValue, V newValue) 将 Map 中指定 key-value 对的原 value 替换成新 value。如果在 Map 中找到指定的 key-value 对,则执行替换并返回 true,否则返回 false。
replaceAll(BiFunction function) 该方法使用 BiFunction 对原 key-value 对执行计算,并将计算结果作为该 key-value 对的 value 值。

Java Collections类

ollections 类是 Java 提供的一个操作 Set、List 和 Map 等集合的工具类。Collections 类提供了许多操作集合的静态方法,借助这些静态方法可以实现集合元素的排序、查找替换和复制等操作。下面介绍 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 个元素“整体”移到后面。该方法不会改变集合的长度。

Collections 还提供了如下常用的用于查找、替换集合元素的方法。

  • int binarySearch(List list, Object key):使用二分搜索法搜索指定的 List 集合,以获得指定对象在 List 集合中的索引。如果要使该方法可以正常工作,则必须保证 List 中的元素已经处于有序状态。
  • Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。
  • Object max(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最大元素。
  • Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。
  • Object min(Collection coll, Comparator comp):根据 Comparator 指定的顺序,返回给定集合中的最小元素。
  • void fill(List list, Object obj):使用指定元素 obj 替换指定 List 集合中的所有元素。
  • int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。
  • int indexOfSubList(List source, List target):返回子 List 对象在父 List 对象中第一次出现的位置索引;如果父 List 中没有出现这样的子 List,则返回 -1。
  • int lastIndexOfSubList(List source, List target):返回子 List 对象在父 List 对象中最后一次出现的位置索引;如果父 List 中没有岀现这样的子 List,则返回 -1。
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值 newVal 替换 List 对象的所有旧值 oldVal。

Collections 类的 copy() 静态方法用于将指定集合中的所有元素复制到另一个集合中。执行 copy() 方法后,目标集合中每个已复制元素的索引将等同于源集合中该元素的索引。

copy() 方法的语法格式如下:

void copy(List <? super T> dest,List<? extends T> src)

其中,dest 表示目标集合对象,src 表示源集合对象。

注意:目标集合的长度至少和源集合的长度相同,如果目标集合的长度更长,则不影响目标集合中的其余元素。如果目标集合长度不够而无法包含整个源集合元素,程序将抛出 IndexOutOfBoundsException 异常。==创建集合对象时,目标集合初始化容量大于源集合长度也会报错,必须真正包含元素,也就是两个集合的size()方法返回值相同。==

遍历集合

  1. 使用Lambda表达式遍历集合

    Java 8 为 Iterable 接口新增了一个 forEach(Consumer action) 默认方法,该方法所需参数的类型是一个函数式接口,而 Iterable 接口是 Collection 接口的父接口,因此 Collection 集合也可直接调用该方法。

    当程序调用 Iterable 的 forEach(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。正因为 Consumer 是函数式接口,因此可以使用 Lambda 表达式来遍历集合元素。

    objects.forEach(obj -> System.out.println(obj));
    // 或者
    objects.forEach(System.out::println);
    
  2. 使用迭代器遍历集合

    Iterator(迭代器)是一个接口,它的作用就是遍历容器的所有元素,也是 Java 集合框架的成员,但它与 Collection 和 Map 系列的集合不一样,Collection 和 Map 系列集合主要用于盛装其他对象,而 Iterator 则主要用于遍历(即迭代访问)Collection 集合中的元素。

    Iterator 接口隐藏了各种 Collection 实现类的底层细节,向应用程序提供了遍历 Collection 集合元素的统一编程接口。Iterator 接口里定义了如下 4 个方法。

    • boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回 true。
    • Object next():返回集合里的下一个元素。
    • void remove():删除集合里上一次 next 方法返回的元素。
    • void forEachRemaining(Consumer action):这是 Java 8 为 Iterator 新增的默认方法,该方法可使用 Lambda 表达式来遍历集合元素。
              Iterator it = objs.iterator();
            while (it.hasNext()) {
         
         
                // it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
                String obj = (String) it.next();
                System.out.println(obj);
                if (obj.equals("C语言中文网C语言教程")) {
         
         
                    // 从集合中删除上一次next()方法返回的元素
                    it.remove();
                    // 使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常“java.util.ConcurrentModificationException”
                    objs.remove(obj);
                }
                // 对book变量赋值,不会改变集合元素本身
                obj = "C语言中文网Python语言教程";
            }
    

    Iterator 仅用于遍历集合,Iterator 必须依赖于Collection对象,若有一个 Iterator 对象,则必然有一个与之关联的 Collection 对象。Iterator 提供了两个方法来迭代访问 Collection 集合里的元素,并可通过 remove() 方法来删除集合中上一次 next() 方法返回的集合元素。对iterator的操作不会影响集合本身。Iterator 并不是把集合元素本身传给了迭代变量,而是把集合元素的值传给了迭代变量,所以修改迭代变量的值对集合元素本身没有任何影响。当使用 Iterator 迭代访问 Collection 集合元素时,Collection 集合里的元素不能被改变,否则会引发异常“java.util.ConcurrentModificationException”

  3. Lambda表达式遍历迭代器

    Java 8 为 Iterator 引入了一个 forEachRemaining(Consumer action) 默认方法,该方法所需的 Consumer 参数同样也是函数式接口。当程序调用 Iterator 的 forEachRemaining(Consumer action) 遍历集合元素时,程序会依次将集合元素传给 Consumer 的 accept(T t) 方法(该接口中唯一的抽象方法)。

    java.util.function 中的 Function、Supplier、Consumer、Predicate 和其他函数式接口被广泛用在支持 Lambda 表达式的 API 中。“void accept(T t);”是 Consumer 的核心方法,用来对给定的参数 T 执行定义操作。

    Iterator it = objs.iterator();
    // 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
    it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
    
  4. 使用foreach遍历集合

    for (Object obj : objs) {
         
         
        // 此处的obj变量也不是集合元素本身
        String obj1 = (String) obj;
        System.out.println(obj1);
        if (obj1.equals("C语言中文网Java教程")) {
         
         
            // 下面代码会引发 ConcurrentModificationException 异常
            objs.remove(obj);
        }
    }
    System.out.println(objs);
    
  5. 使用Stream操作集合

    Java 8 还新增了 Stream、IntStream、LongStream、DoubleStream 等流式 API,这些 API 代表多个支持串行和并行聚集操作的元素。上面 4 个接口中,Stream 是一个通用的流接口,而 IntStream、LongStream、 DoubleStream 则代表元素类型为 int、long、double 的流。

    Java 8 还为上面每个流式 API 提供了对应的 Builder,例如 Stream.Builder、IntStream.Builder、LongStream.Builder、DoubleStream.Builder,开发者可以通过这些 Builder 来创建对应的流。

    独立使用 Stream 的步骤如下:

    1. 使用 Stream 或 XxxStream 的 builder() 类方法创建该 Stream 对应的 Builder。
    2. 重复调用 Builder 的 add() 方法向该流中添加多个元素。
    3. 调用 Builder 的 build() 方法获取对应的 Stream。
    4. 调用 Stream 的聚集方法。

    Stream 提供了大量的方法进行聚集操作,这些方法既可以是“中间的”(intermediate),也可以是 "末端的"(terminal)。

    • 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。上面程序中的 map() 方法就是中间方法。中间方法的返回值是另外一个流。
    • 末端方法:末端方法是对流的最终操作。当对某个 Stream 执行末端方法后,该流将会被“消耗”且不再可用。上面程序中的 sum()、count()、average() 等方法都是末端方法。

除此之外,关于流的方法还有如下两个特征。

  • 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。
  • 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。

    Stream 常用的中间方法。

    | 方法 | 说明 |
    | ------------------------------ | ------------------------------------------------------------ |
    | filter(Predicate predicate) | 过滤 Stream 中所有不符合 predicate 的元素 |
    | mapToXxx(ToXxxFunction mapper) | 使用 ToXxxFunction 对流中的元素执行一对一的转换,该方法返回的新流中包含了 ToXxxFunction 转换生成的所有元素。 |
    | peek(Consumer action) | 依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法主要用于调试。 |
    | distinct() | 该方法用于排序流中所有重复的元素(判断元素重复的标准是使用 equals() 比较返回 true)。这是一个有状态的方法。 |
    | sorted() | 该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。 |
    | limit(long maxSize) | 该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。 |

Stream 常用的末端方法。

方法 说明
forEach(Consumer action) 遍历流中所有元素,对每个元素执行action
toArray() 将流中所有元素转换为一个数组
reduce() 该方法有三个重载的版本,都用于通过某种操作来合并流中的元素
min() 返回流中所有元素的最小值
max() 返回流中所有元素的最大值
count() 返回流中所有元素的数量
anyMatch(Predicate predicate) 判断流中是否至少包含一个元素符合 Predicate 条件。
allMatch(Predicate predicate) 判断流中是否每个元素都符合 Predicate 条件
noneMatch(Predicate predicate) 判断流中是否所有元素都不符合 Predicate 条件
findFirst() 返回流中的第一个元素
findAny() 返回流中的任意一个元素除此之外,Java 8 允许使用流式 API 来操作集合,Collection 接口提供了一个 stream() 默认方法,该方法可返回该集合对应的流,接下来即可通过流式 API 来操作集合元素。由于 Stream 可以对集合元素进行整体的聚集操作,因此 Stream 极大地丰富了集合的功能。

Java 9 新增的不可变集合

Java 9 版本以前,假如要创建一个包含 6 个元素的 Set 集合,程序需要先创建 Set 集合,然后调用 6 次 add() 方法向 Set 集合中添加元素。Java 9 对此进行了简化,程序直接调用 Set、List、Map 的 of() 方法即可创建包含 N 个元素的不可变集合,这样一行代码就可创建包含 N 个元素的集合。

菱形语法的增强

Java 7 之前

// 构造器
Map<String, Integer> scores = new HashMap<String, Integer>();
// 内部类
List<Integer> integers = new ArrayList<Integer>() {
   
   }

Java 7 之后,Java 9 之前

// 构造器
Map<String, Integer> scores = new HashMap<>();
// 内部类
List<Integer> integers = new ArrayList<Integer>() {
   
   }

Java 9 之后

// 构造器
Map<String, Integer> scores = new HashMap<>();
// 内部类
List<Integer> integers = new ArrayList<>() {
   
   }

Java泛型

Java有一个缺点,就是把一个对象丢进集合里,集合就会忘记这个对象的数据类型,再次取出该对象时,对象的编译类型就变为Object类型了(运行时类型不变)

Java 集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性,但这样做带来如下两个问题:

  1. 集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存 Dog 对象的集合,但程序也可以轻易地将 Cat 对象“丢”进去,所以可能引发异常。
  2. 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是 Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发 ClassCastException 异常。

为了解决上述问题,Java 1.5开始提供了泛型,泛型可以在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率。

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 泛型通配符,表示不确定的 java 类型
泛型集合

泛型本质上是提供类型的“类型参数”,也就是参数化类型。我们可以为类,接口,方法指定一个类型参数,通过这个参数限制操作的数据类型,从而保证类型转换的绝对安全。

==如果使用了泛型通配符,那么该集合变量只能读取、删除元素,不能增加元素。==

Map<String, Integer> scores = new HashMap<>();

List<Integer> integers = new ArrayList<>()
泛型类

泛型类一般用于类中属性不确定的情况

public class Students<N, A, S> {
   
   
    private N name; // 姓名
    private A age; // 年龄
    private S sex; // 性别
    // ... 省略get/set以及toString方法
    public static void main(String[] args) {
   
   
        Students<String, Integer, Character> stu = new Students<String, Integer, Character>();
    }
}
泛型方法

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

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

泛型同样可以在类中包含参数化的方法,而方法所在的类可以是泛型类,也可以不是泛型类。也就是说,==是否拥有泛型方法,与其所在的类是不是泛型没有关系。==

泛型方法使得该方法能够独立于类而产生变化。如果使用泛型方法可以取代类泛型化,那么就应该只使用泛型方法。另外,对一个 static 的方法而言,无法访问泛型类的类型参数。因此,==如果 static 方法需要使用泛型能力,就必须使其成为泛型方法==。

public static <T> List find(Class<T> cs,int userId){
   
   }

一般来说编写 Java 泛型方法,其返回值类型至少有一个参数类型应该是泛型,而且类型应该是一致的,如果只有返回值类型或参数类型之一使用了泛型,那么这个泛型方法的使用就被限制了。

泛型的高级用法

包括限制泛型的可用类型、使用类型通配符、继承泛型类和实现泛型接口。

  1. 限制泛型可用类型

    Java中默认使用任意类型实例化一个泛型类对象,也可以对泛型实例的类型进行限制。

    public class ListClass<T extends List> {
         
         
         public static void main(String[] args) {
         
         
            // 实例化使用ArrayList的泛型类ListClass,正确
            ListClass<ArrayList> lc1 = new ListClass<ArrayList>();
            // 实例化使用LinkedList的泛型类LlstClass,正确
            ListClass<LinkedList> lc2 = new ListClass<LinkedList>();
            // 实例化使用HashMap的泛型类ListClass,错误,因为HasMap没有实现List接口
            // ListClass<HashMap> lc3=new ListClass<HashMap>();
        }
    }
    

    extends可以是类,也可以是接口。所以在进行泛型限制时,无论是接口还是类,都必须使用extends关键字,==且extends关键字后面跟这个是类型的上界==

  2. 使用类型通配符

    类型通配符的作用是在创建一个泛型类对象时(==创建对象时==),限制这个泛型类的类型必须继承或实现类某个类或接口

    泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

    // ?代表可以接收任意类型
    // 泛型不存在继承、多态关系,泛型左右两边要一样
    //ArrayList<Object> list = new ArrayList<String>();这种是错误的
    //泛型通配符?:左边写<?> 右边的泛型可以是任意类型
    A<?> a = new A<Object>();
    A<?> b = new A<String>();
    A<?> c = new A<Integer>();
    

    类型通配符的上界:

    • 格式类型名称 <? extends 类 > 对象名称
    • 意义只能接收该类型及其子类
    // ArrayList<? extends Animal> list = new ArrayList<Object>();//报错
    List<? extends Animal> list2 = new ArrayList<Animal>();
    List<? extends Animal> list3 = new ArrayList<Dog>();
    List<? extends Animal> list4 = new ArrayList<Cat>();
    

    类型通配符的下界:

    • 格式类型名称 <? super 类 > 对象名称

    • 意义只能接收该类型及其父类型

    ArrayList<? super Animal> list5 = new ArrayList<Object>();
    ArrayList<? super Animal> list6 = new ArrayList<Animal>();
    // ArrayList<? super Animal> list7 = new ArrayList<Dog>();//报错
    // ArrayList<? super Animal> list8 = new ArrayList<Cat>();//报错
    
  3. 继承泛型类和实现泛型接口

    // 继承
    public class FatherClass<T1>{
         
         }
    public class SonClass<T1,T2,T3> extents FatherClass<T1>{
         
         }
    
    // 实现
    interface interface1<T1>{
         
         }
    public class SubClass<T1,T2,T3> implements Interface1<T2>{
         
         }
    

    如果要在 SonClass 类继承 FatherClass 类时保留父类的泛型类型,需要在继承时指定,否则直接使用 extends FatherClass 语句进行继承操作,此时 T1、T2 和 T3 都会自动变为 Object,所以一般情况下都将父类的泛型类型保留。

Java 枚举(enum)

枚举是一个被命名的整形常数的集合,用于声明一组带有常数的集合。

自定义类实现枚举

  • 将构造器私有化,目的是防止被new出对象
  • 去掉 setXxxx() 方法,防止属性被修改
  • 在Season内部,直接创建固定对象
  • 对外暴露对象(通过为对象添加 public static final 修饰符)
public class Demo03 {
   
   
    public static void main(String[] args) {
   
   
        System.out.println(Season.AUTUMN);
        System.out.println(Season.SUMMER);
    }
}
class Season{
   
   
    private String name;
    private String desc;
    //定义了四个对象
    //加final是为了使引用不能被修改
    public static final Season SPRING = new Season("春天", "温暖");
    public static final Season WINTER = new Season("冬天", "寒冷");
    public static final Season SUMMER = new Season("夏天", "炎热");
    public static final Season AUTUMN = new Season("秋天", "凉爽");

    private Season(String name, String desc) {
   
   
        this.name = name;
        this.desc = desc;
    }

    public String getName() {
   
   
        return name;
    }
    public String getDesc() {
   
   
        return desc;
    }
    @Override
    public String toString() {
   
   
        return "Season{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

声明枚举

  • 使用 enum 关键字代替 class

  • 常量对象名(实参列表)

  • public static final Season2 SPRING = new Season2("春天", "温暖"); 等价于 SPRING("春天", "温暖");

  • 如果有多个对象,需要使用 ,间隔

  • 如果使用 enum 关键字来实现枚举,要求将定义的常量对象写在最前面

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

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

public class Demo04 {
   
   
    public static void main(String[] args) {
   
   
        System.out.println(Season2.SPRING);
        System.out.println(Season2.SUMMER);
    }
}
enum  Season2{
   
   
    SPRING("春天", "温暖"),WINTER("夏天", "炎热"),SUMMER("夏天", "炎热"),AUTUMN("秋天", "凉爽");

    private String name;
    private String desc;

    private Season2(String name, String desc) {
   
   
        this.name = name;
        this.desc = desc;
    }
    public String getName() {
   
   
        return name;
    }
    public String getDesc() {
   
   
        return desc;
    }
    @Override
    public String toString() {
   
   
        return "Season{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}

==默认会继承Enum类;而且该枚举类是一个final类。如果没有显式地声明基础类型的枚举,那么意味着它所对应的基础类型是 int。==

  • 使用enum关键字创建的枚举类,就不能再继承其它类了,因为使用enum创建的枚举类会隐式的继承Enum类,而Java是单继承机制
  • 枚举类和普通类一样,可以实现接口

枚举类

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

所有枚举实例都可以调用 Enum 类的方法,常用方法如表所示。

方法名称 描述
values() 以数组形式返回枚举类型的所有成员
valueOf() 将普通字符串转换为枚举实例
compareTo() 比较两个枚举成员在定义时的顺序
ordinal() 获取枚举成员的索引位置
name() 返回当前对象名(常量名),子类中不能重写
enum WeekDay {
   
   
    Mon("Monday"),Tue("Tuesday"),Wed("Wednesday"),Thu("Thursday"),Fri("Friday"),Sat("Saturday"),Sun("Sunday");
    // 以上是枚举的成员,必须先定义,而且使用分号结束
    private final String day;
    private WeekDay(String day) {
   
   
        this.day = day;
    }
    public static void printDay(int i) {
   
   
        switch(i) {
   
   
            case 1:
                System.out.println(WeekDay.Mon);
                break;
            case 2:
                System.out.println(WeekDay.Tue);
                break;
            case 3:
                System.out.println(WeekDay.Wed);
                break;
            case 4:
                System.out.println(WeekDay.Thu);
                break;
            case 5:
                System.out.println(WeekDay.Fri);
                break;
            case 6:
                System.out.println(WeekDay.Sat);
                break;
            case 7:
                System.out.println(WeekDay.Sun);
                break;
            default:
                System.out.println("wrong number!");
        }
    }
    public String getDay() {
   
   
        return day;
    }
    public static void main(String[] args) {
   
   
        for(WeekDay day : WeekDay.values()) {
   
   
            System.out.println(day+"====>" + day.getDay());
        }
        WeekDay.printDay(5);
    }
    /** 结果
    Mon====>Monday
    Tue====>Tuesday
    Wed====>Wednesday
    Thu====>Thursday
    Fri====>Friday
    Sat====>Saturday
    Sun====>Sunday
    Fri
    */
}

EnumMap 与 EnumSet

为了更好地支持枚举类型,java.util 中添加了两个新类:EnumMapEnumSet。使用它们可以更高效地操作枚举类型。

EnumMap 类

EnumMap 是专门为枚举类型量身定做的 Map 实现。虽然使用其他的 Map(如 HashMap)实现也能完成枚举类型实例到值的映射,但是使用 EnumMap 会更加高效。

HashMap 只能接收同一枚举类型的实例作为键值,并且由于枚举类型实例的数量相对固定并且有限,所以 EnumMap 使用数组来存放与枚举类型对应的值,使得 EnumMap 的效率非常高。

EnumSet 类

EnumSet 是枚举类型的高性能 Set 实现,它要求放入它的枚举常量必须属于同一枚举类型。EnumSet 提供了许多工厂方法以便于初始化,如表 2 所示。

方法名称 描述
allOf(Class element type) 创建一个包含指定枚举类型中所有枚举成员的 EnumSet 对象
complementOf(EnumSet s) 创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象, 并包含所有 s 中未包含的枚举成员
copyOf(EnumSet s) 创建一个与指定 EnumSet 对象 s 相同的枚举类型 EnumSet 对象, 并与 s 包含相同的枚举成员
noneOf( elementType) 创建指定枚举类型的空 EnumSet 对象
of(E first,e...rest) 创建包含指定枚举成员的 EnumSet 对象
range(E from ,E to) 创建一个 EnumSet 对象,该对象包含了 from 到 to 之间的所有枚 举成员

EnumSet 作为 Set 接口实现,它支持对包含的枚举常量的遍历。

Java 反射机制

编译期:指把源码交给编译器编译成计算机可以执行的文件的过程,在Java中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。

运行期:是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来

Java的反射机制实在运行状态中,对于任意一个类,都能够直到这个类的所有属性和方法,对于任意一个对象,对于任意一个对象,都能够调用它的任意方法和属性。这种动态获取信息以及动态调用对象方法的功能称为Java的反射机制。

简单来说,反射机制指的是程序运行期间能够获取自身的信息,Java中只要给定类的名字,就可以通过反射机制来获取类的所有信息。

Java 反射机制在服务器程序和中间件程序中得到了广泛运用。在服务器端,往往需要根据客户的请求,动态调用某一个对象的特定方法。此外,在 ORM 中间件的实现中,运用 Java 反射机制可以读取任意一个 JavaBean 的所有属性,或者给这些属性赋值。

Java 反射机制主要提供了以下功能,这些功能都位于java.lang.reflect包。

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。
  • 生成动态代理。

要想知道一个类的属性和方法,必须先获取到该类的字节码文件对象。获取类的信息时,使用的就是 Class 类中的方法。所以先要获取到每一个字节码文件(.class)对应的 Class 类型的对象.

Java 反射机制的优缺点

优点:

  • 能够运行时动态获取类的实例,大大提高系统的灵活性和扩展性。
  • 与 Java 动态编译相结合,可以实现无比强大的功能。
  • 对于 Java 这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

缺点:

  • 反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
  • 反射调用方法时可以忽略权限检查,获取这个类的私有方法和属性,因此可能会破坏类的封装性而导致安全问题。

反射实现原理

第一步:首先调用了 java.lang.Class 的静态方法,获取类信息
主要是先获取 ClassLoader, 然后调用 native方法,获取信息。
class类信息获取到之后开始实例化,有两种(一:无参构造函数,二:有参构造函数)

第二步(无参构造函数): 调用 newInstance() 的实现方式

权限检测,如果不通过直接抛出异常;
查找无参构造器,并将其缓存起来;
调用具体方法的无参构造方法,生成实例并返回
第二步(有参构造函数):获取所有的构造器主要步骤

先尝试从缓存中获取
如果缓存没有,则从jvm中重新获取,并存入缓存,缓存使用软引用进行保存,保证内存可用
jvm获取 — getConstructor0() 为获取匹配的构造方器
先获取所有的constructors, 然后通过进行参数类型比较
找到匹配后,通过 ReflectionFactory copy一份constructor返回
否则抛出 NoSuchMethodException;

Java.lang.class类

对于一个字节码文件.class,虽然表面上我们对该字节码文件一无所知,但该文件本身却记录了许多信息。Java在将.class字节码文件载入时,JVM将产生一个java.lang.Class对象代表该.class字节码文件,从该Class对象中可以获得类的许多基本信息,这就是反射机制。所以要想完成反射操作,就必须首先认识Class类。

反射机制所需的类主要有java.lang包中的Class类和java.lang.reflect包中的Constructor类、Field类、Method类和Parameter类。Class类是一个比较特殊的类,它是反射机制的基础,Class类的对象表示正在运行的Java程序中的类或接口,也就是任何一个类被加载时,即将类的.class文件(字节码文件)读入内存的同时,都自动为之创建一个java.lang.Class对象。Class类没有公共构造方法,其对象是JVM在加载类时通过调用类加载器中的defineClass()方法创建的,因此不能显式地创建一个Class对象。

Class类的常用方法

方法名称 返回值类型 说明
public Package getPackage() Package对象 获取Class对象对应类的存放路径
public String getName() String 对象 获取该类的名称
public static Class<?> forName(String className) Class对象 返回名称为className的类(或接口)的class对象
public native Class<? super T> getSuperclass() Class对象 返回该类的父类的Class对象
public Class<?>[] getInterfaces() Class型数组 返回该类实现的接口的Class对象数组
public A getAnnotation(Class annotationClass) 继承自Annotation的泛型 根据annotationClass返回该类使用的注解类
public Annotation[] getAnnotations() Annotation型数组 返回该类上所有注解的Annotation数组
public Constructor getConstructor(Class<?>... parameterTypes) Constructor对象 返回该类的指定参数列表的构造方法
public Constructor getDeclaredConstructor(Class<?>... parameterTypes) Constructor对象 返回该类的指定参数列表的构造方法,与访问权限无关
public Constructor<?>[] getConstructors() Constructor型数组 返回所有权限为 public 的构造方法
public Constructor<?>[] getDeclaredConstructors() Constructor型数组 获取当前对象的所有构造方法,与访问权限无关
public Field getField(String name) Field对象 返回该该类名称为name的public成员变量的Field对象
public Field getDeclaredField(String name) Field对象 返回该该类名称为name的成员变量的Field对象,与访问权限无关
public Field[] getFields() Field型数组 获取所有权限为 public 的成员变量,包含从超类中继承到的成员变量
public Field[] getDeclaredFields() Field型数组 获取本类中的所有成员变量,不包含从父类中继承到的成员变量
public Method getMethod(String name, Class<?>... parameterTypes) Method对象 返回该类名称为name参数列表为parameterTypes的public的方法的Method对象
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) Method对象 返回该类名称为name参数列表为parameterTypes的方法的Method对象,与访问权限无关
public Method[] getMethods() Method型数组 获取所有权限为 public 的方法,包括从父类继承得到的成员
public Method[] getDeclaredMethods() Method型数组 获取本类中的所有方法,不包含从父类中继承到的成员方法
public Class<?>[] getClasses() Class型数组 获取所有权限为 public 的内部类
public Class<?>[] getDeclaredClasses() Class型数组 获取所有内部类
public Class<?> getDeclaringClass() Class对象 如果该类为内部类,则返回它的成员类,否则返回 null

每个类被加载之后,系统都会为该类生成一个对应的Class对象,通过Class对象就可以访问到JVM中该类的信息,一旦类被加载到JVM中,同一个类将不会被再次载入。被载入JVM的类都有一个唯一标识就是该类的全名,即包括包名和类名。在Java中程序获得Class对象有如下3种方式。

(1)使用Class类的静态方法forName(String className),其中参数className表示所需类的全名。如“Class cObj=Class.forName("java.lang.String");”。另外,forName()方法声明抛出ClassNotFoundException异常,因此调用该方法时必须捕获或抛出该异常。

(2)用类名调用该类的class属性来获得该类对应的Class对象,即“类名.class”。如,语句“ClasscObj=Cylinder.class;”将返回Cylinder类所对应的Class对象赋给cObj变量。

(3)用对象调用getClass()方法来获得该类对应的Class对象,即“对象.getClass()”。该方法是Object类中的一个方法,因此所有对象调用该方法都可以返回所属类对应的Class对象。如例8.8中的语句“Person per=new Person(“张三”);”可以通过以下语句返回该类的Class对象:Class cObj=per.getClass();

通过类的class属性获得该类所对应的Class对象,会使代码更安全,程序性能更好,因此大部分情况下建议使用第二种方式。但如果只获得一个字符串,例如获得String类对应的Class对象,则不能使用String.class方式,而是使用Class.forName("java.lang.String")。注意:如果要想获得基本数据类型的Class对象,可以使用对应的打包类加上.TYPE,例如,Integer.TYPE可获得int的Class对象,但要获得Integer.class的Class对象,则必须使用Integer.class。在获得Class对象后,就可以取得Class对象的基本信息。

java.lang.reflect 包

java.lang.reflect 包提供了反射中用到类,主要的类说明如下:

  • Constructor 类:提供类的构造方法信息。

    常用方法

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

  • Field 类:提供类或接口中成员变量信息。

    常用方法如表 1 所示

    | 方法名称 | 说明 |
    | --------------------------------- | ------------------------------------------------------------ |
    | 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() | 获得可以解析出该方法所采用修饰符的整数 |

  • Method 类:提供类或接口成员方法信息。

    常用方法

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

  • Array 类:提供了动态创建和访问 Java 数组的方法。

  • Modifier 类:提供类和成员访问修饰符信息。

    常用静态方法。

    | 静态方法名称 | 说明 |
    | -------------------- | -------------------------------------------------------- |
    | 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) | 以字符串形式返回所有修饰符 |

Java输入输出流

Java的I/O(输入/输出)技术可以将数据保存到文本文件和二进制文件中,已达到永久保存数据的目的。Java流相关的操作都封装在java.io包中,每个数据流都是一个对象

输入输出概念(读入写出)

输入就是将数据从输入设备(键盘)中读取到内存中,输出是将数据写入到各种输出设备(显示器、磁盘)。

数据流式Java进行I/O操作的对象,按照不同的标准可以分为不同的类别

  • 按照流的方向主要分为输入流和输出流两大类
  • 数据流按照数据单位的不同可以分为字符流和字节流
  • 按照功能可以分为节点流和功能流
graph LR;
文件-->输入流;
网络-->输入流;
数据库-->输入流;
其他数据源-->输入流;
输入流-->目的地;
源-->输出流;
输出流-->文件1;
输出流-->网络1;
输出流-->数据库1;
输出流-->其他数据源1;

输入流

所有的输入流都是InputStream抽象类(字节输入流)和Reader抽象类(字符输入流)的子类。

InputStream 类中所有方法遇到错误时都会引发 IOException 异常。如下是该类中包含的常用方法。

名称 作用
int read() 从输入流读入一个 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 个方法一般会结合在一起使用,首先使用 markSupported() 判断,如果可以重复读取,则使用 mark(int readLimit) 方法进行标记,标记完成之后可以使用 read() 方法读取标记范围内的字节数,最后使用 reset() 方法使输入流重新定位到标记的位置,继而完成重复读取操作。

Java 中的字符是 Unicode 编码,即双字节的,而 InputerStream 是用来处理单字节的,在处理字符文本时不是很方便。这时可以使用 Java 的文本输入流 Reader 类,该类是字符输入流的抽象类,即所有字符输入流的实现都是它的子类,该类的方法与 InputerSteam 类的方法类似,这里不再介绍。

输出流

在 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() 方法的简单形式如下:

void write(int byteval) throws IOException

该方法通过 byteval 参数向文件写入指定的字节。在实际操作中,print() 方法和 println() 方法比 write() 方法更常用。

注意:尽管它们通常用于对控制台进行读取和写入字符,但是这些都是字节流。因为预定义流是没有引入字符流的 Java 原始规范的一部分,所以它们不是字符流而是字节流,但是在 Java 中可以将它们打包到基于字符的流中使用。

Java字符编码介绍

计算机中,任何的文字都是以指定的编码方式存在的,在 Java 程序的开发中最常见的是 ISO8859-1、GBK/GB2312、Unicode、 UTF 编码。

Java 中常见编码说明如下:

  • ISO8859-1:属于单字节编码,最多只能表示 0~255 的字符范围。
  • GBK/GB2312:中文的国标编码,用来表示汉字,属于双字节编码。GBK 可以表示简体中文和繁体中文,而 GB2312 只能表示简体中文。GBK 兼容 GB2312。
  • Unicode:是一种编码规范,是为解决全球字符通用编码而设计的。UTF-8 和 UTF-16 是这种规范的一种实现,此编码不兼容 ISO8859-1 编码。Java 内部采用此编码。
  • UTF:UTF 编码兼容了 ISO8859-1 编码,同时也可以用来表示所有的语言字符,不过 UTF 编码是不定长编码,每一个字符的长度为 1~6 个字节不等。一般在中文网页中使用此编码,可以节省空间。

File类

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

File 类不能访问文件内容本身,如果需要访问文件内容本身,则需要使用输入/输出流。

File 类提供了如下三种形式构造方法。

  1. File(String path):如果 path 是实际存在的路径,则该 File 对象表示的是目录;如果 path 是文件名,则该 File 对象表示的是文件。
  2. File(String path, String name):path 是路径名,name 是文件名。
  3. File(File dir, String name):dir 是路径对象,name 是文件名。

使用任意一个构造方法都可以创建一个 File 对象,然后调用其提供的方法对文件进行操作。在表 1 中列出了 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 下指;。例如 java -cp test.jar;abc.jar HelloWorld
  • public static final String separator:用来分隔同一个路径字符串中的目录的,Windows 下指/。例如 C:/Program Files/Common Files

注意:可以看到 File 类的常量定义的命名规则不符合标准命名规则,常量名没有全部大写,这是因为 Java 的发展经过了一段相当长的时间,而命名规范也是逐步形成的,File 类出现较早,所以当时并没有对命名规范有严格的要求,这些都属于 Java 的历史遗留问题。

Windows 的路径分隔符使用反斜线“\”,而 Java 程序中的反斜线表示转义字符,所以如果需要在 Windows 的路径下包括反斜线,则应该使用两条反斜线或直接使用斜线“/”也可以。Java 程序支持将斜线当成平台无关的路径分隔符。

Java字节流的使用:字节输入/输出流、文件输入/输出流、字节数组输入/输出流

InputStream 是 Java 所有字节输入流类的父类,OutputStream 是 Java 所有字节输出流类的父类,它们都是一个抽象类,因此继承它们的子类要重新定义父类中的抽象方法。

下面首先介绍上述两个父类提供的常用方法,然后介绍如何使用它们的子类输入和输出字节流,包括 ByteArrayInputStream 类、ByteArrayOutputStream 类、FileInputStream 类和 FileOutputStream 类。

字节输入流

InputStream 类及其子类的对象表示字节输入流,InputStream 类的常用子类如下。

  • ByteArrayInputStream 类:将字节数组转换为字节输入流,从中读取字节。
  • FileInputStream 类:从文件中读取数据。
  • PipedInputStream 类:连接到一个 PipedOutputStream(管道输出流)。
  • SequenceInputStream 类:将多个字节输入流串联成一个字节输入流。
  • ObjectInputStream 类:将对象反序列化。

使用 InputStream 类的方法可以从流中读取一个或一批字节。

方法名及返回值类型 说明
int read() 从输入流中读取一个 8 位的字节,并把它转换为 0~255 的整数,最后返回整数。 如果返回 -1,则表示已经到了输入流的末尾。为了提高 I/O 操作的效率,建议尽量 使用 read() 方法的另外两种形式
int read(byte[] b) 从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。 该方法返回 读取的字节数。如果返回 -1,则表示已经到了输入流的末尾
int read(byte[] b, int off, int len) 从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。其中,off 指 定在字节数组中开始保存数据的起始下标;len 指定读取的字节数。该方法返回实际 读取的字节数。如果返回 -1,则表示已经到了输入流的末尾
void close() 关闭输入流。在读操作完成后,应该关闭输入流,系统将会释放与这个输入流相关 的资源。注意,InputStream 类本身的 close() 方法不执行任何操作,但是它的许多 子类重写了 close() 方法
int available() 返回可以从输入流中读取的字节数
long skip(long n) 从输入流中跳过参数 n 指定数目的字节。该方法返回跳过的字节数
void mark(int readLimit) 在输入流的当前位置开始设置标记,参数 readLimit 则指定了最多被设置标记的字 节数
boolean markSupported() 判断当前输入流是否允许设置标记,是则返回 true,否则返回 false
void reset() 将输入流的指针返回到设置标记的起始处

注意:在使用 mark() 方法和 reset() 方法之前,需要判断该文件系统是否支持这两个方法,以避免对程序造成影响。

字节输出流

OutputStream 类及其子类的对象表示一个字节输出流。OutputStream 类的常用子类如下。

  • ByteArrayOutputStream 类:向内存缓冲区的字节数组中写数据。
  • FileOutputStream 类:向文件中写数据。
  • PipedOutputStream 类:连接到一个 PipedlntputStream(管道输入流)。
  • ObjectOutputStream 类:将对象序列化。

利用 OutputStream 类的方法可以从流中写入一个或一批字节。表 2 列出了 OutputStream 类的常用方法。

方法名及返回值类型 说明
void write(int b) 向输出流写入一个字节。这里的参数是 int 类型,但是它允许使用表达式, 而不用强制转换成 byte 类型。为了提高 I/O 操作的效率,建议尽量使用 write() 方法的另外两种形式
void write(byte[] b) 把参数 b 指定的字节数组中的所有字节写到输出流中
void write(byte[] b,int off,int len) 把参数 b 指定的字节数组中的若干字节写到输出流中。其中,off 指定字节 数组中的起始下标,len 表示元素个数
void close() 关闭输出流。写操作完成后,应该关闭输出流。系统将会释放与这个输出 流相关的资源。注意,OutputStream 类本身的 close() 方法不执行任何操 作,但是它的许多子类重写了 close() 方法
void flush() 为了提高效率,在向输出流中写入数据时,数据一般会先保存到内存缓冲 区中,只有当缓冲区中的数据达到一定程度时,缓冲区中的数据才会被写 入输出流中。使用 flush() 方法则可以强制将缓冲区中的数据写入输出流, 并清空缓冲区

字节数组输入流

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 常用的构造方法主要有如下两种重载形式。

  1. FileInputStream(File file):通过打开一个到实际文件的连接来创建一个 FileInputStream,该文件通过文件系统中的 File 对象 file 指定。
  2. FileInputStream(String name):通过打开一个到实际文件的链接来创建一个 FileInputStream,该文件通过文件系统中的路径名 name 指定。

注意:FileInputStream 类重写了父类 InputStream 中的 read() 方法、skip() 方法、available() 方法和 close() 方法,不支持 mark() 方法和 reset() 方法。

文件输出流

FileOutputStream 类继承自 OutputStream 类,重写和实现了父类中的所有方法。FileOutputStream 类的对象表示一个文件字节输出流,可以向流中写入一个字节或一批字节。在创建 FileOutputStream 类的对象时,如果指定的文件不存在,则创建一个新文件;如果文件已存在,则清除原文件的内容重新写入。

FileOutputStream 类的构造方法主要有如下 4 种重载形式。

  1. FileOutputStream(File file):创建一个文件输出流,参数 file 指定目标文件。
  2. FileOutputStream(File file,boolean append):创建一个文件输出流,参数 file 指定目标文件,append 指定是否将数据添加到目标文件的内容末尾,如果为 true,则在末尾添加;如果为 false,则覆盖原有内容;其默认值为 false。
  3. FileOutputStream(String name):创建一个文件输出流,参数 name 指定目标文件的文件路径信息。
  4. 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 异常。

技巧:在创建 FileOutputStream 对象时,如果将 append 参数设置为 true,则可以在目标文件的内容末尾添加数据,此时目标文件仍然可以暂不存在。

Java字符流的使用:字符输入/输出流、字符文件和字符缓冲区的输入/输出流

字符输入流

Reader 类是所有字符流输入类的父类,该类定义了许多方法,这些方法对所有子类都是有效的。

Reader 类的常用子类如下。

  • CharArrayReader 类:将字符数组转换为字符输入流,从中读取字符。
  • StringReader 类:将字符串转换为字符输入流,从中读取字符。
  • BufferedReader 类:为其他字符输入流提供读缓冲区。
  • PipedReader 类:连接到一个 PipedWriter。
  • InputStreamReader 类:将字节输入流转换为字符输入流,可以指定字符编码。

与 InputStream 类相同,在 Reader 类中也包含 close()、mark()、skip() 和 reset() 等方法,这些方法可以参考 InputStream 类的方法。下面主要介绍 Reader 类中的 read() 方法,如表 1 所示。

方法名及返回值类型 说明
int read() 从输入流中读取一个字符,并把它转换为 0~65535 的整数。如果返回 -1, 则表示 已经到了输入流的末尾。为了提高 I/O 操作的效率,建议尽量使用下面两种 read() 方法
int read(char[] cbuf) 从输入流中读取若干个字符,并把它们保存到参数 cbuf 指定的字符数组中。 该方 法返回读取的字符数,如果返回 -1,则表示已经到了输入流的末尾
int read(char[] cbuf,int off,int len) 从输入流中读取若干个字符,并把它们保存到参数 cbuf 指定的字符数组中。其中, off 指定在字符数组中开始保存数据的起始下标,len 指定读取的字符数。该方法返 回实际读取的字符数,如果返回 -1,则表示已经到了输入流的末尾

字符输出流

与 Reader 类相反,Writer 类是所有字符输出流的父类,该类中有许多方法,这些方法对继承该类的所有子类都是有效的。

Writer 类的常用子类如下。

  • CharArrayWriter 类:向内存缓冲区的字符数组写数据。
  • StringWriter 类:向内存缓冲区的字符串(StringBuffer)写数据。
  • BufferedWriter 类:为其他字符输出流提供写缓冲区。
  • PipedWriter 类:连接到一个 PipedReader。
  • OutputStreamReader 类:将字节输出流转换为字符输出流,可以指定字符编码。

与 OutputStream 类相同,Writer 类也包含 close()、flush() 等方法,这些方法可以参考 OutputStream 类的方法。下面主要介绍 Writer 类中的 write() 方法和 append() 方法,如表 2 所示。

方法名及返回值类型 说明
void write(int c) 向输出流中写入一个字符
void write(char[] cbuf) 把参数 cbuf 指定的字符数组中的所有字符写到输出流中
void write(char[] cbuf,int off,int len) 把参数 cbuf 指定的字符数组中的若干字符写到输出流中。其中,off 指定 字符数组中的起始下标,len 表示元素个数
void write(String str) 向输出流中写入一个字符串
void write(String str, int off,int len) 向输出流中写入一个字符串中的部分字符。其中,off 指定字符串中的起 始偏移量,len 表示字符个数
append(char c) 将参数 c 指定的字符添加到输出流中
append(charSequence esq) 将参数 esq 指定的字符序列添加到输出流中
append(charSequence esq,int start,int end) 将参数 esq 指定的字符序列的子序列添加到输出流中。其中,start 指定 子序列的第一个字符的索引,end 指定子序列中最后一个字符后面的字符 的索引,也就是说子序列的内容包含 start 索引处的字符,但不包括 end 索引处的字符

注意:Writer 类所有的方法在出错的情况下都会引发 IOException 异常。关闭一个流后,再对其进行任何操作都会产生错误。

字符文件输入流

为了读取方便,Java 提供了用来读取字符文件的便捷类——FileReader。该类的构造方法有如下两种重载形式。

  1. FileReader(File file):在给定要读取数据的文件的情况下创建一个新的 FileReader 对象。其中,file 表示要从中读取数据的文件。
  2. FileReader(String fileName):在给定从中读取数据的文件名的情况下创建一个新 FileReader 对象。其中,fileName 表示要从中读取数据的文件的名称,表示的是一个文件的完整路径。

在用该类的构造方法创建 FileReader 读取对象时,默认的字符编码及字节缓冲区大小都是由系统设定的。要自己指定这些值,可以在 FilelnputStream 上构造一个 InputStreamReader。

注意:在创建 FileReader 对象时可能会引发一个 FileNotFoundException 异常,因此需要使用 try catch 语句捕获该异常。

字符流和字节流的操作步骤相同,都是首先创建输入流或输出流对象,即建立连接管道,建立完成后进行读或写操作,最后关闭输入/输出流通道。

字符文件输出流

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 语句捕获该异常。

字符缓冲区输入流

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

BufferedReader 类的构造方法有如下两种重载形式。

  1. BufferedReader(Reader in):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流。
  2. BufferedReader(Reader in,int size):创建一个 BufferedReader 来修饰参数 in 指定的字符输入流,参数 size 则用于指定缓冲区的大小,单位为字符。

除了可以为字符输入流提供缓冲区以外,BufferedReader 还提供了 readLine() 方法,该方法返回包含该行内容的字符串,但该字符串中不包含任何终止符,如果已到达流末尾,则返回 null。readLine() 方法表示每次读取一行文本内容,当遇到换行(\n)、回车(\r)或回车后直接跟着换行标记符即可认为某行已终止。

字符缓冲区输出流

BufferedWriter 类主要用于辅助其他字符输出流,它同样带有缓冲区,可以先将一批数据写入缓冲区,当缓冲区满了以后,再将缓冲区的数据一次性写到字符输出流,其目的是为了提高数据的写效率。

BufferedWriter 类的构造方法有如下两种重载形式。

  1. BufferedWriter(Writer out):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流。
  2. BufferedWriter(Writer out,int size):创建一个 BufferedWriter 来修饰参数 out 指定的字符输出流,参数 size 则用于指定缓冲区的大小,单位为字符。

该类除了可以给字符输出流提供缓冲区之外,还提供了一个新的方法 newLine(),该方法用于写入一个行分隔符。行分隔符字符串由系统属性 line.separator 定义,并且不一定是单个新行(\n)符。

提示:BufferedWriter 类的使用与 FileWriter 类相同,这里不再重述。

Java注解

声明自定义注解使用 @interface 关键字(interface 关键字前加 @ 符号)实现。定义注解与定义接口非常像,如下代码可定义一个简单形式的注解类型。

定义注解和定义类相似,注解前面的访问修饰符和类一样有两种,分别是公有访问权限(public)和默认访问权限(默认不写)。一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的注解。且源程序文件命名和公有访问权限的注解名一致。

不包含任何成员变量的注解称为标记注解,例如上面声明的 Test 注解以及基本注解中的 @Override 注解都属于标记注解。根据需要,注解中可以定义成员变量,成员变量以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。代码如下所示:

public @interface MyTag {
   
   
    // 定义带两个成员变量的注解
    // 注解中的成员变量以方法的形式来定义
    String name();
    int age();
    String name() default "C语言中文网";
    int age() default 7;
}

Java多线程

如果一次只完成一件事情,很容易实现。但是现实生活中很多事情都是同时进行的,所以在 Java 中为了模拟这种状态,引入了线程机制。简单地说,当程序同时完成多件事情时,就是所谓的多线程程序。多线程的应用相当广泛,使用多线程可以创建窗口程序、网络程序等。

线程是操作系统能够进行运算调度的最小单位

一个进程可以包含多个线程

Windows 系统是多任务操作系统,它以进程为单位。

系统可以分配给每个进程一段有限的执行 CPU 的时间(也称为 CPU 时间片),CPU 在这段时间中执行某个进程,然后下一个时间段又跳到另一个进程中去执行。由于 CPU 切换的速度非常快,给使用者的感受就是这些任务似乎在同时运行,所以使用多线程技术后,可以在同一时间内运行更多不同种类的任务。

单任务的特点就是排队执行,也就是同步,就像在 cmd 中输入一条命令后,必须等待这条命令执行完才可以执行下一条命令一样。这就是单任务环境的缺点,即 CPU 利用率大幅降低。

并发与并行

  • 并发:单cpu。系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。
  • 并行:多cpu。系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行

创建线程的方式

  1. 继承Thread类,重写run方法

    使用继承 Thread 类的方式实现多线程,最大的局限就是不支持多继承

    Thread 类的常用构造方法如下:

    • Thread():创建一个线程对象;
    • Thread(String name):创建一个指定名称的线程对象;
    • Thread(Runnable target):创建一个基于 Runnable 接口实现类的线程对象;
    • Thread(Runnable target, String name):创建一个基于 Runnable 接口实现类,并具有指定名称的线程对象。

    Thread类的常用方法

    | 方法名 | 说明 |
    | ------------------------------------------------------------ | ------------------------------------------------------------ |
    | public void run() | 如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。 |
    | public synchronized void start() | 导致此线程开始执行; Java虚拟机调用此线程的run方法。 |
    | public static native Thread currentThread(); | 返回当前执行的线程对象的引用 |
    | public long getId() | 返回此线程的标识符。 |
    | public final String getName() | 返回此线程的标名称。 |
    | public final native boolean isAlive(); | 测试这个线程是否活着。 |
    | public final boolean isDaemon() | 测试这个线程是否是守护线程。 |
    | public final synchronized void join(long millis, int nanos) | 等待此线程结束或者指定的毫秒数,调用Object中的wait方法,阻塞主线程,等待此线程执行完毕或者到达等待时间,才会调用notify或者notifyAll()方法唤醒。如果想要join方法正常生效,调用join方法的线程对象必须已经调用了start()方法并且未进入终止状态。 |
    | public final synchronized void setName(String name) | 将此线程的名称更改为等于参数 name 。 |
    | public static native void sleep(long millis) | 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。 |
    | public static native void yield(); | 线程让步,从运行状态变为就绪状态,让其他具有相同优先级的其他线程获得运行机会 |

    equals, finalize, getClass, hashCode, notify, notifyAll, wait,toString,clone等是Object类的方法

    | 方法 | 说明 |
    | ----------- | ------------------------------------------------------------ |
    | sleep() | 当前线程休眠一段时间其他线程有机会执行,时间到了就会进入就绪队列。会释放cpu资源,但是不会释放锁(类锁和对象锁) |
    | yield() | 当前线程立即进入就绪状态。会释放cpu资源,但是不会释放锁(类锁和对象锁) |
    | join() | 哪个线程调用,其他线程都会等待此线程执行完毕或者指定的毫秒数。例如在线程b中获取到了线程a并调用了线程a的join(),那么线程b会调用wait方法进入阻塞队列。等待此线程执行完毕或者到达等待时间,才会调用notify或者notifyAll()方法唤醒。如果想要join方法正常生效,调用join方法的线程对象必须已经调用了start()方法并且未进入终止状态。 |
    | wait() | 用于协调多个线程对于共享数据的存取。所以必须在synchronized语句块中。会释放cpu资源以及锁(类锁和对象锁)。调用wait后必须调用notify或notifyAll,线程才会从等待池进入到锁池,当我们的线程竞争到同步锁以后会重新进入就绪状态等待cpu分配资源 |
    | notify() | 将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。 |
    | notifyAll() | 从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中 |

    join方法演示

    ```java
    class JoinDemo extends Thread {

    int i;
    Thread previousThread; //上一个线程
    
    public JoinDemo(Thread previousThread, int i) {
        this.previousThread = previousThread;
        this.i = i;
    }
    
    @Override
    public void run() {
    

    // try {
    // //调用上一个线程的join方法,大家可以自己演示的时候可以把这行代码注释掉
    // previousThread.join();
    // } catch (InterruptedException e) {
    // e.printStackTrace();
    // }

        System.out.println("num:" + i);
    }
    

    }
    class Test {

    public static void main(String[] args) {
        Thread previousThread=Thread.currentThread();
        for(int i=0;i<10;i++){
            JoinDemo joinDemo=new JoinDemo(previousThread,i);
            joinDemo.start();
            previousThread=joinDemo;
            System.out.println("----------");
        }
    }
    

    }




   ==sleep没有释放锁,而wait释放了锁==

2. **实现Runnable接口,实现run方法**

   Runnable接口是函数式接口,只有一个没有返回值的run方法

   通过实现 `Runnable` 接口的方案来创建线程,要优于继承 `Thread` 类的方案,主要有以下原因:

   1. Java 不支持多继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了 `Thread` 类就无法继承其它类,这不利于扩展;
   2. 继承 `Thread` 类通常只重写 `run()` 方法,其他方法一般不会重写。继承整个 `Thread` 类成本过高,开销过大。

   步骤如下:

   1. 定义 `Runnable` 接口的实现类,并实现该接口的 `run()` 方法。这个 `run()` 方法的方法体同样是该线程的线程执行体;
   2. 创建 `Runnable` 实现类的实例,并以此实例作为 `Thread` 的 `target` 来创建 `Thread` 对象,该 `Thread` 对象才是真正的线程对象;
   3. 调用线程对象的 `start` 方法来启动该线程。

3. **实现Callable接口(带泛型的函数式接口),实现call方法,cal方法有返回值,可声明异常**

   **继承 Thread 类和实现 Runnable 接口这两种创建线程的方式都没有返回值**。所以,线程执行完毕后,无法得到执行结果。为了解决这个问题,Java 5 后,提供了 `Callable` 接口和 `Future` 接口,通过它们,可以在线程执行结束后,返回执行结果。

   通过实现 `Callable` 接口创建线程步骤如下:

   1. 创建 `Callable` 接口的实现类,并实现 `call()` 方法。这个 `call()` 方法将作为线程执行体,并且有返回值;
   2. 创建 `Callable` 实现类的实例,使用 `FutureTask` 类来包装 `Callable` 对象,这个 `FutureTask` 对象封装了该 `Callable` 对象的 `call()` 方法的返回值;
   3. 使用 `FutureTask` 对象作为 `Thread` 对象的 target 创建并启动新线程;
   4. 调用 `FutureTask` 对象的 `get()` 方法来获得线程执行结束后的返回值。

   ```java
   import java.util.concurrent.Callable;
   import java.util.concurrent.ExecutionException;
   import java.util.concurrent.FutureTask;

   /**
    * @author colorful@TaleLin
    */
   public class CallableDemo1 {

       static class MyThread implements Callable<String> {

           @Override
           public String call() { // 方法返回值类型是一个泛型,在上面 Callable<String> 处定义
               return "我是线程中返回的字符串";
           }

       }

       public static void main(String[] args) throws ExecutionException, InterruptedException {
           // 常见实现类的实例
           Callable<String> callable = new MyThread();
           // 使用 FutureTask 类来包装 Callable 对象
           FutureTask<String> futureTask = new FutureTask<>(callable);
           // 创建 Thread 对象
           Thread thread = new Thread(futureTask);
           // 启动线程
           thread.start();
           // 调用 FutureTask 对象的 get() 方法来获得线程执行结束后的返回值
           String s = futureTask.get();
           System.out.println(s);
       }

   }
  1. 使用线程池

参考 笔记.md文件

为什么使用线程池

  • 降低资源消耗:线程的创建合销毁会一定程度上造成时间与空间上的消耗,线程池可以让我们重复利用已创建的线程
  • 提高响应速度:线程池已经创建好了线程,当任务到达时可以不需要等待线程创建就可以立即执行。
  • 提高线程的可管理性:线程是稀缺资源,不可能无线创建,使用线程池进行统一分配、调优和监控。
  • 提供更强大的功能:线程池具备可扩展性,允许开繁荣恩怨象棋中添加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

核心参数

ThreadPoolExecutor(int corePoolSize, 
                   int maximumPoolSize, 
                   long keepAliveTime, 
                   TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue, 
                   ThreadFactory threadFactory, 
                   RejectedExecutionHandler handler);
参数 说明
corePoolSize 核心线程数量,线程池维护线程的最少数量
maximumPoolSize 线程池维护线程的最大数量,最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常
keepAliveTime 非核心线程的最长空闲时间,超过该时间的空闲线程会被销毁
unit keepAliveTime的单位,有NANOSECONDS(纳秒)、MICROSECONDS(微秒)、MILLISECONDS(毫秒)、SECONDS(秒)
workQueue 任务缓冲队列(阻塞队列)
threadFactory 线程工厂,用于创建线程,一般用默认的即可
handle 线程池对拒绝任务的处理策略

将线程池设置为守护线程

public static void main(String[] args) throws InterruptedException {
   
   
    // 线程工厂(设置守护线程)
    ThreadFactory threadFactory = new ThreadFactory() {
   
   
        @Override
        public Thread newThread(Runnable r) {
   
   
            Thread thread = new Thread(r);
            // 设置为守护线程
            thread.setDaemon(true);
            return thread;
        }
    };
    // 创建线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10,
                                                           0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), threadFactory);
    threadPool.submit(new Runnable() {
   
   
        @Override
        public void run() {
   
   
            System.out.println("ThreadPool 线程类型:" +
                               (Thread.currentThread().isDaemon() == true ? "守护线程" : "用户线程"));
        }
    });
    Thread.sleep(2000);
}

==阻塞队列:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。==

1.ArrayBlockingQueue:有界、数组结构、FIFO
2.LinkedBlockingQueue:有界、单链表结构、FIFO、默认长度Integer.MAX_VALUE
3.SynchronousQueue:不存储元素、每个put操作必须等待take操作,否则阻塞状态
4.PriorityBlockingQuene:无界、数组的平衡二叉堆、支持线程优先级排序、默认自然序、同优先级不能保证顺序
5.DelayQueue:无界、基于PriorityBlockingQuene、以时间作为比较基准的优先级队列,这个时间即延迟时间

ThreadPoolExecutor提供了四种拒绝策略:

  1. AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认)
  2. CallerRunsPolicy:由调用线程处理该任务( 常用)
  3. DiscardPolicy:丢弃任务,但是不抛出异常。
  4. DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

ThreadPoolExecutor的运行状态:

运行状态 状态描述
RUNNING 能接受新提交的任务、并且也能处理阻塞队列中的任务
SHUTDOWN 关闭状态,不再接受新提交的任务,但可以继续处理阻塞队列中已保存的任务
STOP 不能接受新提及的任务,也不处理队列中的任务,会中断正在处理任务的线程
TIDYING 所有的任务都已经终止了,workerCount(有效线程数)为0
TERMINATED 在terminated()方法执行完后进入该状态

线程池的创建方式:

  1. 通过Executor线程工厂类创建(不推荐)

    • newFixedThreadPool()方法:该方法返回一个固定数量的线程池,该方法的线程数始终不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中等待有空闲的线程去执行。

    • newSingleThreadExecutor ()方法:创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务列队中。创建单个线程数的线程池,它可以保证先进先出的执行顺序。

    • newCachedThreadPool()方法:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若有任务,则创建线程,若无任务则不创建线程。如果没有任务则线程在60s后自动回收(空闲时间60s)。若线程数不够,则会新建线程。是根据短时间的任务量来决定创建的线程数量的,所以它适合短时间内有突发大量任务的处理场景。

    • newScheduledThreadPool()方法:该方法返回一个SchededExecutorService对象,但该线程池可以指定线程的数量。

      public static void scheduledThreadPool() {
             
             
          // 创建线程池
          ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
          // 添加定时执行任务(1s 后执行)
          System.out.println("添加任务,时间:" + new Date());
          threadPool.schedule(() -> {
             
             
              System.out.println("任务被执行,时间:" + new Date());
              try {
             
             
                  TimeUnit.SECONDS.sleep(1);
              } catch (InterruptedException e) {
             
             
              }
          }, 1, TimeUnit.SECONDS);
      }
      

上面四种方式都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

  1. 通过newThreadPoolExecutor自定义创建(推荐)

    ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 
                                                     20, 
                                                     60, 
                                                     TimeUnit.SECONDS, 
                                                     new LinkedBlockingQueue<>(200));
    

线程池使用规范(阿里巴巴)

  • 创建线程或者线程池时指定有意义的名称,方便溯源。
  • 线程资源必须使用线程池,不允许自行显示的在程序中创建线程。 自行创建线程,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题。线程池不允许使用 Executors工厂类去创建,而是通过new ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

SpringBoot项目中使用线程池

  1. 配置自定义线程池并开启异步任务

    @Configuration 
    @EnableAsync // 开启异步任务支持 
    public class ExecutorConfig {
         
          
    
        // 声明线程池 
        @Bean("taskExecutor") 
        public Executor taskExecutor() {
         
          
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 
            executor.setCorePoolSize(5); 
            executor.setMaxPoolSize(20); 
            executor.setQueueCapacity(2000); 
            executor.setThreadNamePrefix("taskExecutor-"); 
            executor.setRejectedExecutionHandler(new 
                               ThreadPoolExecutor.CallerRunsPolicy()); 
            //执行初始化 
            executor.initialize(); 
            return executor; 
        }
    
    }
    
  2. 在@Async中使用自定义线程池

    @Service 
    public class TaskService {
         
          
    
        // @Async声明方法为异步方法并自定使用自定义线程池 
        @Async("taskExecutor") 
        public void task1() {
         
          
            // 具体业务 
        } 
    
    }
    
  3. @Async失效(本质是代理没有生效)

    • 异步方法使用而了static修饰
    • 异步方法所在类没有使用@Component或@Service注解进行注释,导致spring无法扫描到异步类
    • 异步方法类应使用@Autowired或@Resource等注解自动注入到使用类中,不能自己手动new对象
    • 没有在启动类或配置类中增加@EnableAsync注解启动异步任务支持
    • 异步方法不能由本类内其他方法调用,必须是外部使用者调用,如果内部方法调用会出现代理绕过的问题,会变成同步操作

ThreadPoolTaskExecutor和ThreadPoolExecutor区别:

  • ThreadPoolExecutor是JDK提供的线程池类,也是几种线程池创建的底层实现
  • ThreadPoolTaskExecutor是spring提供的

判断线程池是否执行完毕

  1. 使用executor.studown()方法和executor.isTerminated()方法。

    利用线程池的终止状态(TERMINATED)来判断线程池的任务是否已经全部执行完,但想要线程池的状态发生改变,我们就需要调用线程池的 shutdown 方法,不然线程池一直会处于 RUNNING 运行状态,那就没办法使用终止状态来判断任务是否已经全部执行完了

    缺点:需要关闭线程池。如果不调用线程池的关闭方法,那么线程池会一直处于 RUNNING 运行状态。

     public void insertAutoId() {
         
         
    
            ExecutorService pool1 = Executors.newCachedThreadPool();
            List<ToolFilePdf> list = new ArrayList<ToolFilePdf>(){
         
         {
         
         
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
            }};
            List<Future<Integer>> futures = new ArrayList<>();
            for (ToolFilePdf filePdf : list) {
         
         
                futures.add(pool1.submit(() -> {
         
         
                    int i = 0;
                    System.out.println("save前"+filePdf);
                    i += mapper.insertAutoId(filePdf);
                    System.out.println("task"+i);
                    System.out.println("save后"+filePdf);
                    return i;
                }));
            }
            pool1.shutdown();
            while(!pool1.isTerminated()) {
         
         
    
            }
            try {
         
         
                System.out.println("开始主线程");
                futures.stream().map(el -> {
         
         
                    try {
         
         
                        return el.get();
                    } catch (InterruptedException e) {
         
         
                        e.printStackTrace();
                    } catch (ExecutionException e) {
         
         
                        e.printStackTrace();
                    }
                    return 666;
                }).forEach(el -> System.out.println("result"+el));
            } catch (Exception e) {
         
         
                e.printStackTrace();
            }
        }
    
  2. 使用executor.getCompletedTaskCount()方法与executor.getTaskCount()方法

    此线程池的类型须为ThreadPoolExecutor类型,下面说的四种创建线程池的方位创建的线程池为ExecutorService类型,ThreadPoolExecutor为ExecutorService的子类,须向下强转类型。

    • getTaskCount():返回计划执行的任务总数。由于任务和线程的状态可能在计算过程中动态变化,因此返回的值只是一个近似值。
    • getCompletedTaskCount():返回完成执行任务的总数。因为任务和线程的状态可能在计算过程中动态地改变,所以返回的值只是一个近似值,但是在连续的调用中并不会减少。

    缺点:此实现方法的优点是无需关闭线程池。它的缺点是 getTaskCount() 和 getCompletedTaskCount() 返回的是一个近似值,因为线程池中的任务和线程的状态可能在计算过程中动态变化,所以它们两个返回的都是一个近似值

     @Test
        public void insertAutoId() {
         
         
    
            ThreadPoolExecutor pool1 = (ThreadPoolExecutor)Executors.newCachedThreadPool();
            List<ToolFilePdf> list = new ArrayList<ToolFilePdf>(){
         
         {
         
         
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
            }};
            List<Future<Integer>> futures = new ArrayList<>();
            for (ToolFilePdf filePdf : list) {
         
         
                futures.add(pool1.submit(() -> {
         
         
                    int i = 0;
                    System.out.println("save前"+filePdf);
                    i += mapper.insertAutoId(filePdf);
                    System.out.println("task"+i);
                    System.out.println("save后"+filePdf);
                    return i;
                }));
            }
            while(pool1.getTaskCount() != pool1.getCompletedTaskCount()) {
         
         
                System.out.println(pool1.getTaskCount());
                System.out.println(pool1.getCompletedTaskCount());
            }
            try {
         
         
                System.out.println("开始主线程");
                futures.stream().map(el -> {
         
         
                    try {
         
         
                        return el.get();
                    } catch (InterruptedException e) {
         
         
                        e.printStackTrace();
                    } catch (ExecutionException e) {
         
         
                        e.printStackTrace();
                    }
                    return 666;
                }).forEach(el -> System.out.println("result"+el));
            } catch (Exception e) {
         
         
                e.printStackTrace();
            }
        }
    
  3. 使用CountDownLatch类的countDown()和awiat()方法

    CountDownLatch 可以理解为一个计数器,我们创建了一个包含 N 个任务的计数器,每个任务执行完计数器 -1,直到计数器减为 0 时,说明所有的任务都执行完了,就可以执行下一段业务的代码了。提交任务(executor.submit())需要放在循环中,而countDown()方法需要放在executor.submit()方法中,否则计数器会生效但不是在任务执行完去减一,从而导致任务失效。同时CountDownLatch(new Integer())的构造方法的整型参数必须和任务数量,否则任务会一直卡在await()方法。CountDownLatch 写法很优雅,且无需关闭线程池,但它的缺点是只能使用一次,CountDownLatch 创建之后不能被重复使用,也就是说 CountDownLatch 可以理解为只能使用一次的计数器。

        @Test
        public void insertAutoId() {
         
         
    
            ThreadPoolExecutor pool1 = (ThreadPoolExecutor)Executors.newCachedThreadPool();
            List<ToolFilePdf> list = new ArrayList<ToolFilePdf>(){
         
         {
         
         
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
                add(new ToolFilePdf());
            }};
            CountDownLatch c = new CountDownLatch(list.size());
            List<Future<Integer>> futures = new ArrayList<>();
            for (ToolFilePdf filePdf : list) {
         
         
                futures.add(pool1.submit(() -> {
         
         
                    int i = 0;
                    System.out.println("save前"+filePdf);
                    i += mapper.insertAutoId(filePdf);
                    System.out.println("task"+i);
                    System.out.println("save后"+filePdf);
                    c.countDown();
                    return i;
                }));
            }
            try {
         
         
                c.await();
                System.out.println("开始主线程");
                futures.stream().map(el -> {
         
         
                    try {
         
         
                        return el.get();
                    } catch (InterruptedException e) {
         
         
                        e.printStackTrace();
                    } catch (ExecutionException e) {
         
         
                        e.printStackTrace();
                    }
                    return 666;
                }).forEach(el -> System.out.println("result"+el));
            } catch (Exception e) {
         
         
                e.printStackTrace();
            }
        }
    
  4. 使用CyclicBarrier类的构造方法和await()方法

    CyclicBarrier 和 CountDownLatch 类似,它可以理解为一个可以重复使用的循环计数器,CyclicBarrier 可以调用 reset 方法将自己重置到初始状态,CyclicBarrier 具体实现代码如下:

    @Test
        public void list() {
         
         
                ExecutorService executorService = Executors.newCachedThreadPool();
                List<McmsAssetRepair> list = new ArrayList<McmsAssetRepair>() {
         
         {
         
         
                    add(new McmsAssetRepair().setCreateUser(1L).setCreateTime(new Date()).setIsDeleted(1).setStatus(1));
                    add(new McmsAssetRepair().setCreateUser(1L).setCreateTime(new Date()).setIsDeleted(1).setStatus(1));
                    add(new McmsAssetRepair().setCreateUser(1L).setCreateTime(new Date()).setIsDeleted(1).setStatus(1));
                    add(new McmsAssetRepair().setCreateUser(1L).setCreateTime(new Date()).setIsDeleted(1).setStatus(1));
                    add(new McmsAssetRepair().setCreateUser(1L).setCreateTime(new Date()).setIsDeleted(1).setStatus(1));
                    add(new McmsAssetRepair().setCreateUser(1L).setCreateTime(new Date()).setIsDeleted(1).setStatus(1));
                }};
                List<Future<Integer>> futures = new ArrayList<>();
                CyclicBarrier c = new CyclicBarrier(list.size(), () -> System.out.println("开始主线程11111111111111111111111111"));
                for (McmsAssetRepair mcmsAssetRepair : list) {
         
         
                    futures.add(executorService.submit(() -> {
         
         
                        int i = 0;
                        Thread.sleep(2000);
                        System.out.println("save前" + mcmsAssetRepair);
                        i += mcmsAssetRepairMapper.insert(mcmsAssetRepair);
                        System.out.println("task" + i);
                        System.out.println("save后" + mcmsAssetRepair);
                    c.await();
                        return i;
                    }));
                }
            System.out.println("开始主线程333333333333333333333333333333333333");
            futures.stream().map(el -> {
         
         
                try {
         
         
                    return el.get();
                } catch (InterruptedException e) {
         
         
                    e.printStackTrace();
                } catch (ExecutionException e) {
         
         
                    e.printStackTrace();
                }
                return 666;
            }).forEach(el -> System.out.println("result" + el));
            System.out.println("开始主线程2222222222222222222222222222222222222");
        }
    
    /**
        输出结果为:
            开始主线程333333333333333333333333333333333333
            save与task交替输出
            开始主线程11111111111111111111111111
            result
            开始主线程2222222222222222222222222222222222222
    */
    

jdk自带的四种线程池创建方式

// 第一种线程池:固定个数的线程池,可以为每个CPU核绑定一定数量的线程数
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(processors * 2);
// 缓存线程池,无上限
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 单一线程池,永远会维护存在一条线程
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
// 固定个数的线程池,可以执行延时任务,也可以执行带有返回值的任务。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

线程池的运作流程。

java.lang.Thread.State 枚举类中定义了 6 种不同的线程状态:

  • NEW新建状态,尚未启动的线程处于此状态。
  • RUNNABLE可运行状态,Java虚拟机中的执行的线程处于此状态,就绪和运行中的统称
  • BLOCK阻塞状态,等待监视器锁而被阻塞线程处于此状态
  • WAITING等待状态,等待另一线程的执行特定操作的线程处于此状态
  • TIME_WAITING超时等待状态,等待时间内等待另一线程执行特定操作的线程处于此状态
  • TERMINATED结束状态,已退出的线程处于此状态

一个线程在给定的时间点只能处于一种状态。这些状态是不反映任何操作系统线程状态的虚拟机状态

Java线程的优先级和执行顺序

每个线程都具有优先级,虚拟机根据线程的优先级决定线程的执行顺序,这样使多线程合理共享 CPU 资源而不会产生冲突。

在 Java 语言中,线程的优先级范围是 1~10,值必须在 1~10,否则会出现异常;优先级的默认值为 5。优先级较高的线程会被优先执行,当执行完毕,才会轮到优先级较低的线程执行。如果优先级相同,那么就采用轮流执行的方式。

Java的线程安全问题与同步机制

线程安全问题主要是指对个线程对同一个对象的同一变量进行操作时会出现值被更改,值不同步的情况,进而影响程序的执行流程。

为了处理这种资源共享竞争,可以使用同步机制。

所谓同步机制,就是指两个线程同时作用在一个对象上,应该保持对象数据的同一性和整体性。Java提供synchronized关键字,为了防止资源冲突提供内置支持。资源共享一般是文件、输入/输出端口或打印机。

synchronized的使用

  • 同步方法

    public synchronized void synMethod(){
         
         
    //方法体
    }
    
  • 同步代码块

    synchronized(this)
    {
         
         
        //代码
    }
    synchronized(TestClass.class)
    {
         
         
        //代码
    }
    

    后面括号里是一对象,此时,线程获得的是对象锁。如果线程进入,则得到当前对象锁,那么别的线程在该类所有对象上的任何操作都不能进行.在对象级使用锁通常是一种比较粗糙的方法。

    synchronized后面括号里是类,此时,线程获得的是对象锁。如果线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步,我们通常用4来加锁。

  • 修饰静态方法

    public synchronized static void method() {
         
         
       // todo
    }
    

使用总结

  • 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
  • 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
  • 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
  • 给class加锁和上例的给静态方法加锁是一样的,所有对象公用一把锁。

并发编程

定义: 所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件,多个事件在同一时间间隔发生。

意义:开发者通过使用不同的语言,实现并发编程,充分的利用处理器(CPU)的每一个核,以达到最高的处理性能,提升服务器的资源利用率,提升数据的处理速度。

什么是进程?
官方定义: 进程(baiProcess)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

什么是线程?
官方定义: 线程是操作系统能够进行资源调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,每个线程执行的都是进程代码的某个片段,特定的线程总是在执行特定的任务。

串行:顺序执行,按步就搬。在 A 任务执行完之前不可以执行 B。

并行:同时执行,多管齐下。指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同 CPU 核心上同时执行。

并发:穿插执行,减少等待。指多个线程轮流穿插着执行,并发的实质是一个物理 CPU 在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。

三大特性:

  1. 原子性;
  2. 可见性;
  3. 有序性。

Java 的内存模型

定义: Java 内存模型(即 Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

线程的私有线程的意义

程序计数器:记录当前方法执行到了哪里,以便 CPU 切换回来之后能够继续执行上次执行到的位置,而不会进行重复执行或者遗漏。

局部变量:局部变量是方法中的变量,只供当前方法使用。

方法参数:Java 方法会定义自己的入参,入参的真实值也会记录到内存空间供当前线程使用。

主内存操作共享变量需要注意的事项

  • 确定是否是多线程环境:多线程环境下操作共享变量需要考虑线程的安全性;
  • 确定是否有增删改操作:多线程环境下,如果对共享数据有增加,删除或者修改的操作,需要谨慎。为了保证线程的同步性,必须对该共享数据进行加锁操作,保证多线程环境下,所有的线程能够获取到正确的数据。如生产者与消费者模型,售票模型。这些会在后续章节进行代码实战演练;
  • 多线程下的读操作:如果是只读操作,对共享数据不需要进行锁操作,因为数据本身未发生增删改操作,不会影响获取数据的准确性。

线程的上下文切换

概述:在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时-刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。

定义:当前线程使用完时间片后,就会处于就绪状态并让出 CPU,让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。

线程上下文切换时机: 当前线程的 CPU 时间片使用完或者是当前线程被其他线程中断时,当前线程就会释放执行权。那么此时执行权就会被切换给其他的线程进行任务的执行,一个线程释放,另外一个线程获取,就是我们所说的上下文切换时机。

线程死锁

定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

死锁的必备要素

  • 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;
  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放,如 yield 释放 CPU 执行权);
  • 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放;
  • 循环等待条件:指在发生死锁时,必然存在一个线程请求资源的环形链,即线程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,以此类推,Tn 正在等待己被 T0 占用的资源。

避免线程死锁

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。

造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可避免死锁。

守护线程与用户线程

Java 中的线程分为两类,分别为 daemon 线程(守护线程〉和 user 线程(用户线程)

在 JVM 启动时会调用 main 函数, main 函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。

守护线程定义:所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程。比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。

因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程定义:某种意义上的主要用户线程,只要有用户线程未执行完毕,JVM 虚拟机不会退出。

区别:在本质上,用户线程和守护线程并没有太大区别,唯一的区别就是当最后一个非守护线程结束时,JVM 会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响 JVM 的退出。

Java 守护进程的最主要的特点有:

  • 守护线程是运行在程序后台的线程;
  • 守护线程创建的线程,依然是守护线程;
  • 守护线程不会影响 JVM 的退出,当 JVM 只剩余守护线程时,JVM 进行退出;
  • 守护线程在 JVM 退出时,自动销毁。

创建方式:将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon (true) 方法来实现。

创建细节

  • thread.setDaemon (true) 必须在 thread.start () 之前设置,否则会跑出一个 llegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程;
  • 在 Daemon 线程中产生的新线程也是 Daemon 的;
  • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

应用场景

  • 为其它线程提供服务支持的情况,可选用守护线程;
  • 根据开发需求,程序结束时,这个线程必须正常且立刻关闭,就可以作为守护线程来使用;
  • 如果一个正在执行某个操作的线程必须要执行完毕后再释放,否则就会出现不良的后果的话,那么这个线程就不能是守护线程,而是用户线程;
  • 正常开发过程中,一般心跳监听,垃圾回收,临时数据清理等通用服务会选择守护线程

ThreadLocal 的使用

当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中 “Local” 所要表达的意思。

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

ThreadLocal set 方法

public class DemoTest{
   
   
    public static void main(String[] args){
   
   
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
    }
}

set 方法可以设置任何类型的值,无论是 String 类型 ,Integer 类型,Object 等类型,原因在于 set 方法的 JDK 源码实现是基于泛型的实现,此处只是拿 String 类型进行的举例。

ThreadLocal 中只能设置一个变量值,因为多次 set 变量的值会覆盖前一次 set 的值,我们之前提出过,ThreadLocal 其实是使用 ThreadLocalMap 进行的 value 存储,那么多次设置会覆盖之前的 value,这是 get 方法无需入参的原因,因为只有一个变量值。

ThreadLocal get 方法

public class DemoTest{
   
   
    public static void main(String[] args){
   
   
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
        System.out.println(localVariable.get());
    }
}

ThreadLocal remove 方法

remove 方法是为了清除 ThreadLocal 变量,清除成功后,该 ThreadLocal 中没有变量值。

public class DemoTest{
   
   
    public static void main(String[] args){
   
   
        ThreadLocal<String> localVariable = new ThreadLocal<> () ;
        localVariable.set("Hello World");
        System.out.println(localVariable.get());
        localVariable.remove();
        System.out.println(localVariable.get());
    }
}

remove 方法同 get 方法一样,是没有任何入参的,因为 ThreadLocal 中只能存储一个变量值,那么 remove 方法会直接清除这个变量值。

多线程的操作原则

多线程 AVO 原则

A:即 Atomic,原子性操作原则。对基本数据类型的变量读和写是保证原子性的,要么都成功,要么都失败,这些操作不可中断。

V:即 volatile,可见性原则。后续的小节会对 volatile 关键字进行深入的讲解,此处只需要理解概念性问题即可。使用 volatile 关键字,保证了变量的可见性,到主存拿数据,不是到缓存里拿。

O:即 order, 就是有序性。代码的执行顺序,在代码编译前的和代码编译后的执行顺序不变。

为什么要进行多线程并发

多核 CPU 时代的到来打破了单核 CPU 对多线程效能的限制。 多个 CPU 意味着每个线程可以使用自己的 CPU 运行,这减少了线程上下文切换的开销。

但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

共享变量:非线程私有的变量,共享变量存放于主内存中,所有的线程都有权限对变量进行增删改查操作。

内存可见性:由于数据是存放于内存中的,内存可见性意味着数据是公开的,所有线程都可对可见性的数据进行增删改查操作。

Java 内存模型规定,将所有的变量都存放在主内存(共享内存)中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,也就是我们所说的线程私有内存,线程读写变量时操作的是自己工作内存中的变量。

当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

synchronized关键字

synchronized 同步块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。

内置锁:即排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。

线程的执行:代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在==正常退出同步代码块==或者==抛出异常后==或者==在同步块内调用了该内置锁资源的 wait 系列方法时==释放该内置锁。

synchronized 的使用就会导致上下文切换。 Lock 接口和 ReadWriteLock 接口,能在一定场景下很好地避免 synchronized 关键字导致的上下文切换问题。

synchronized 的三种使用方式

  • 普通同步方法(实例方法):锁是当前实例对象 ,进入同步代码前要获得当前实例的锁;

    public class DemoTest extends Thread {
         
         
        //共享资源
        static int count = 0;
    
        /**
         * synchronized 修饰实例方法
         */
        public synchronized void increase() throws InterruptedException {
         
         
            sleep(1000);
            count++;
            System.out.println(Thread.currentThread().getName() + ": " + count);
        }
        @Override
        public void run() {
         
         
            try {
         
         
                increase();
            } catch (InterruptedException e) {
         
         
                e.printStackTrace();
            }
        }
        public static void main(String[] args) throws InterruptedException {
         
         
            DemoTest test = new DemoTest();
            Thread t1 = new Thread(test);
            Thread t2 = new Thread(test);
            t1.setName("threadOne");
            t2.setName("threadTwo");
            t1. start();
            t2. start();
        }
    
  • 静态同步方法:锁是当前类的 class 对象 ,进入同步代码前要获得当前类对象的锁;

    public static synchronized void increase() throws InterruptedException {
         
         
            System.out.println(Thread.currentThread().getName() + "获取到锁,其他线程在我执行完毕之前,不可进入。" );
            sleep(1000);
            count++;
            System.out.println(Thread.currentThread().getName() + ": " + count);
        }
    
  • 同步方法块:锁是括号里面的对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

        /**
         * synchronized 修饰实例方法
         */
        static final Object objectLock = new Object(); //创建一个对象锁
        public static void increase() throws InterruptedException {
         
         
            System.out.println(Thread.currentThread().getName() + "获取到锁,其他线程在我执行完毕之前,不可进入。" );
            synchronized (objectLock) {
         
         
                sleep(1000);
                count++;
                System.out.println(Thread.currentThread().getName() + ": " + count);
            }
        }
    

volatile关键字

volatile 关键字解决内存可见性问题,是一种弱形式的同步。==该关键字可以确保当一个线程更新共享变量时,更新操作对其他线程马上可见==。当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。

在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 sychronized 关键字更轻量级的同步机制。

volatile 与 synchronized 的区别

相似处:volatile 的内存语义和 synchronized 有相似之处,具体来说就是,当线程写入了 volatile 变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存),读取 volatile 变量值时就相当于进入 synchronized 同步块( 先清空本地内存变量值,再从主内存获取最新值)。

区别:使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。具体区别如下:

  • volatile 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
  • volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的;
  • volatile 仅能实现变量的修改可见性,不能保证原子性;而 synchronized 则可以保证变量的修改可见性和原子性;
  • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞;
  • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化

CAS 操作

CAS(Compare And Swap 比较和交换)解决了 volatile 不能保证原子性的问题。从而 CAS 操作即能够解决锁的效率问题,也能够保证操作的原子性。

同步锁是一种悲观策略,CAS 是一种乐观策略

JAVA 多线程锁

悲观锁

定义:悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改(很悲观),所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。

悲观锁的实现:开发中常见的悲观锁实现往往依靠数据库提供的锁机制,即在数据库中,在对数据记录操作前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁。

实例:Java 中的 synchronized 关键字就是一种悲观锁,一个线程在操作时,其他的线程必须等待,直到锁被释放才可进入方法进行执行,保证了线程和数据的安全性,同一时间,只能有一条线程进入执行。

存在的问题

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
  • 一个线程持有锁会导致其它所有需要此锁的线程挂起;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

乐观锁

定义:乐观锁是相对悲观锁来说的,它认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新的时候,才会正式对数据冲突与否进行检测。

乐观锁的实现:依旧拿数据库的锁进行比较介绍,乐观锁并不会使用数据库提供的锁机制, 一般在表中添加 version 宇段或者使用业务状态来实现。 乐观锁直到提交时才锁定,所以不会产生任何死锁。

Java 中的乐观锁:我们之前所学习的 CAS 原理即是乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

公平锁与非公平锁

公平锁:表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。

非公平锁:非公平锁则在运行时闯入,不遵循先到先执行的规则。

ReentrantLock:ReentrantLock 提供了公平和非公平锁的实现。我们本节只做介绍,后续章节会对 ReentrantLock 进行深入的讲解。

独占锁与共享锁

独占锁:保证任何时候都只有一个线程能得到锁,ReentrantLock 就是以独占锁方式实现的。

共享锁:则可以同时由多个线程持有,例如 ReadWriteLock 读写锁,它允许一个资源可以被多线程同时进行读操作。

独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取。共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

并发锁之 Lock 接口

Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。

Lock 相对于 synchronized 关键字而言更加灵活,你可以自由得选择你想要加锁的地方。当然更高的自由度也带来更多的责任。

使用示例:我们通常会在 try catch 模块中使用 Lock 关键字,在 finally 模块中释放锁。

     Lock lock = new ReentrantLock(); //通过子类进行创建,此处以ReentrantLock进行举例
     lock.lock(); //加锁
     try {
   
   
         // 对上锁的逻辑进行操作
     } finally {
   
   
         lock.unlock(); //释放锁
     }
// 在Lock中,不能使用wait和notify,取而代之的为:

// Condition   它的功能类似于Object.wait()和Object.notify()的功能。

Condition condition = lock.newCondition();
condition.await();//相当于wait
condition.signal();//相当于notify

方法介绍

  1. void lock():获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态;
  2. void lockInterruptibly():如果当前线程未被中断,则获取锁;
  3. boolean tryLock():仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false;
  4. boolean tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁;
  5. void unlock():释放锁。在等待条件前,锁必须由当前线程保持。调用 Condition.await () 将在等待前以原子方式释放锁,并在等待返回前重新获取锁;
  6. Condition newCondition():返回绑定到此 Lock 实例的新 Condition 实例。

JUC 包

java.util.concurrent (缩写 JUC)并发编程包是专门为 Java 并发编程设计的

包路径 主要内容 典型类型
java.util.concurrent 提供很多种最基本的并发工具类,包括对各类数据结构的并发封装,并发框架主要接口 CountDownLatch,CyclicBarrier,Semaphore,Exchanger,Phaser,BlockingQueue,ConcurrentHashMap,ThreadPoolExecutor,ForkJoinPool
java.util.concurrent.atomic 提供各类原子操作工具类 AtomicInteger, DoubleAdder,LongAccumulator,AtomicReference
java.util.concurrent.locks 提供各类锁工具 Lock,ReadWriteLock,ReentrantLock,StampedLock

锁(locks)部分:提供适合各类场合的锁工具;
原子变量(atomic)部分:原子变量类相关,是构建非阻塞算法的基础;
并发框架(executor)部分:提供线程池相关类型;
并发容器(collections) 部分:提供一系列并发容器相关类型;
同步工具(tools)部分:提供相对独立,且场景丰富的各类同步工具,如信号量、闭锁、栅栏等功能;

Java内存模型 与 JVM内存模型

Java内存模型

是Java语言在多线程并发情况下对于共享变量读写(实际是共享变量的内存操作)的规范,主要为了解决多线程可见性,原子性的问题,解决共享变量的多线程操作冲突问题。规定是所有的变量都存在在主存中,每个线程都有自己的工作内存。线程对内存的操作必须在工作内存中进行,不能对主存进行操作,每个线程不能访问其他线程的工作内存。

Java内存模型的Volatile关键字,原子性、可见性、有序性

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图所示。

主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。

原子性、可见性与有序性

原子性(Atomicity):一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。通过 synchronized、lock实现,适用于多写。

可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。 通过volatile实现,适用于一写多读。

有序性(Ordering):一个线程中的所有操作必须按照程序的顺序来执行。通过volatile实现,会插入内存屏障使得volatile声明的变量前后顺序不会发生变化

多线程编程的普遍问题是:

  • 所见非所得
  • 无法肉眼检测程序的准确性
  • 不同的运行平台表现不同
  • 错误很难复现

故JVM规范规定了Java虚拟机对多线程内存操作的一些规则,主要集中体现在volatile和synchronized这两个关键字。

  • volatile 是JVM提供的对共享变量在多线程读写时的可见性保证,主要作用是对volatile修饰的共享变量禁止被缓存(这里跟CPU的高速缓存和缓存一致性协议有关),不做重排序(重排序:在CPU处理速度远大于内存读写速度的现状下为了提高性能而进行的优化),但是并不保证共享变量操作的原子性。
  • synchronized 是JVM提供的锁机制,通过锁的特性和内存屏障保证锁住区域操作的原子性、可见性、有序性。
  • 锁争抢的是对象(static锁的是类对象,非static锁的是当前对象,即this,锁方法块锁的是自定义对象)在堆内存中对象头的一块内存的“主权”,只有一个线程能获取该“主权”,即排他性,通过锁的排他性保证对锁住区域的操作的原子性
  • 通过在代码前后加入加载屏障(Load Barrier)和存储屏障(Store Barrier),能保证锁住代码块或者方法中对共享变量的操作的可见性
  • 通过在代码前后加入获取屏障(Acquire Barrier)和释放屏障(Release Barrier),能保证锁住代码块或者方法中对共享变量的操作的有序性

JVM内存模型

  • 虚拟机栈:随着线程的创建而创建。用来存放局部变量、堆区对象的引用、常量池对象的引用,对象本身不存放在栈中。线程的私有区域
  • 方法区:存放类的信息、常量、静态变量、编译后的代码,Java7的方法区放在永代区,Java8的方法区放在元空间,并通过GC管理。线程的共享区域。
  • 堆:在虚拟机启动的时候就已经创建。存放的是对象实例、数组也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代
  • 本地方法栈:类似虚拟机栈,是为虚拟机使用native本地方法而准备的。具体实现由虚拟机厂商来实现。HotSpot虚拟机中实现与虚拟机栈一致,同时超出大小抛StackOverFlowError。
  • 程序计数器:记录当前线程执行字节码的位置,存储的是字节码指令地址,如果native方法,则为空。CPU同一时间只能执行一条线程中的指令,线程切换后通过程序计数器来恢复正确的执行位置。

新生代、老年代、永代区、元空间

  • 新生代:存在于堆内存中,存放刚创建的对象。大约占据堆空间的三分之一

    • eden区:新创建的对象绝大部分存在eden区。当eden内存不够时触发一次MinorGC(复制算法)进行垃圾回收。
    • survivor Form区和survivor To区:GC开始的时候对象只存在eden和Form区,一次MinorGC过后存活的对象会转入到To区并清空eden和from区,同时对象的年龄+1,GC结束时To区和Form区功能互换。如此循环往复,当对象的年龄达到15时,则直接分配到老年代。

    三个区的默认比例为8:1:1

  • 老年代:存在于堆内存中,存放生命周期比较长的对象。老年代比较稳定不会频繁的进行MaiorGC。在进行MaiorGC前会进行一次MinorGC,使新生代的对象进入老年代并且老年代内存不够时才会触发。当无法找到足够大的连续空间分配给新创建的较大对象也会提前触发一次MajorGC进行垃圾回收腾出空间。老年代中采取清除算法(:首先扫描一次所有老年代里的对象,标记出存活的对象,然后回收没有标记的对象。MajorGC的耗时比较长。因为要扫描再回收。MajorGC会产生内存碎片,当老年代也没有内存分配给新来的对象的时候,就会抛出OOM(Out of Memory)异常)。

  • 永代区:永久保存区域。主要存放Class和Meta(元数据)的信息。Classic在被加载的时候被放入永久区域,它和存放的实例的区域不同。Java8中永代区取消了,取而代之的是元空间(元数据区)。元空间和永久代类似,都是对JVM中规范中方法的实现。元空间与永代区的区别:元空间不存在于JVM虚拟机中,而是放在本地内存。默认情况下,元空间的大小受限于本地内存的大小

  • 元空间:方法区的实现,由Java1.7的永代区变为的Java1.8的元空间。元空间存在于本地内存,意味着只要本地内存足够,它不会出现像永久代中“java.lang.OutOfMemoryError: PermGen space”这种错误。默认情况下元空间是可以无限使用本地内存的,但为了不让它如此膨胀,JVM 同样提供了参数来限制它使用的使用。

    -XX:MetaspaceSize,class metadata的初始空间配额,以bytes为单位,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当的降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize(如果设置了的话),适当的提高该值。
    -XX:MaxMetaspaceSize,可以为class metadata分配的最大空间。默认是没有限制的。
    -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为class metadata分配空间导致的垃圾收集。
    -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为class metadata释放空间导致的垃圾收集。

    采用元空间而不使用永代区的原因

    • 为了解决永久代的OOM(OutOfMemoryError)问题,元数据和class对象存放在永久代中,容易出现性能问题和内存溢出。
    • 类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
    • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

Java数据库编程

JDBC

JDBC是Java DataBase Connectivity的缩写,他是Java的标准库,定义了客户访问数据库的API。

连接不同的数据库需要不同的数据库驱动,数据库驱动是由厂商提供的,需要我们引入,标准库编写了一天数据库访问代码,因此不需要标准库代码的改动,只需要加载不同的驱动,就可以访问不同的数据库。

在 JDBC 出现之前,数据库驱动程序由不同的数据库厂商提供,程序员想要操作不同的数据库,就不得不学习各类不同驱动的用法,驱动的学习成本和代码的维护成本都非常高。此定义了一套标准的访问数据库的 API(即 JDBC),不同厂商按照这个 API 提供的统一接口,实现驱动,这保证了数据库操作的统一性。程序员也不需要再去学习不同厂商提供的五花八门的 API,只需要学习 JDBC 标准 API 即可。

连接数据库

使用maven依赖,以mysql为例,

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.49</version>
</dependency>

JDBC 的简单使用,以下实例代码有这样几个步骤:

  1. 加载数据库驱动;
  2. 建立连接;
  3. 创建 Statement 对象,用于向数据库发送 SQL 语句;
  4. 获取 ResultSet 对象,取出数据,此对象代表结果集;
  5. 释放资源,断开与数据库的连接。
import com.mysql.jdbc.Driver;

import java.sql.*;

public class JDBCDemo1 {
   
   

    public static void main(String[] args) throws SQLException {
   
   
        // 1. 加载数据库驱动
        DriverManager.registerDriver(new Driver());
        // 2. 建立连接
        final String url = "jdbc:mysql://localhost:3306/jdbcdemo";  // 数据库 url
        final String user = "root"; // 数据库用户名
        final String password = "123456"; // 数据库密码
        Connection connection = DriverManager.getConnection(url, user, password);
        // 3. 创建 Statement 对象,用于向数据库发送 SQL 语句
        String sql = "SELECT * FROM `user`";
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery(sql);
        // 4. 获取 ResultSet 对象,取出数据,此对象代表结果集
        while (resultSet.next()) {
   
   
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            String nickname = resultSet.getString("nickname");
            String pwd = resultSet.getString("password");
            System.out.println("id=" + id + "; username=" + username + "; nickname=" + nickname + "; password=" + pwd + '\r');
        }
        // 5. 释放资源,断开与数据库的连接(调用close()方法)
        // 5.1 释放 ResultSet
        resultSet.close();
        // 5.2 释放 Statement
        statement.close();
        // 5.3 释放 Connection
        connection.close();
    }
}

JDBC类详解

DriverManager

驱动管理类,用于加载驱动和获取链接

// 一般使用此方法注册驱动,registerDriver属于静态方法,在类加载的时候会加载一次,通过此方法会加载两次驱动
DirverManager.registerDiver(new Driver);
// 一般通过反射获取类,去加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");

加载了 Driver 类,其静态代码块就会执行,因此也就注册了驱动。

除了获得驱动,我们还可以调用 getConnection(url, user, password) 方法来获得连接,其中 url 这个参数不是很好理解:

String url = "jdbc:mysql://localhost:3306/jdbcdemo";

其中 jdbc 是协议,mysql 是子协议,localhost 是主机名,3306 是端口号,jdbcdemo 是数据库名。/ 这里的协议是固定地写法,连接不同类型地数据库需要不同地协议。

Connection

Connection 是连接对象,它可以创建执行 SQL 语句的对象,还可以进行事务的管理。

下面列举了 Connection 类的常用实例方法:

  • Statement createStatement():创建执行 SQL 语句对象,有 SQL 注入风险;
  • PrepareStatement prepareStatement(String sql):预编译 SQL 语句,解决 SQL 注入的漏洞;
  • CallableStatement prepareCall(String sql):执行 SQL 中存储过程;
  • setAutoCommit(boolean autoCommit):设置事务是否自动提交;
  • commit():事务提交;
  • rollback():事务回滚。
Statement

Statement 是执行 SQL 语句的对象,下面列举了 Statement 类的常用实例方法:

  • boolean execute(String sql):执行 SQL 语句,如果返回的第一个结果为 ResultSet 对象,则返回 true,如果其为更新计数或者不存在任何结果,则返回 false。该方法不常用;
  • ResultSet executeQuery(String sql):执行 SQL 中的 select 语句;
  • int executeUpdate(String sql):执行 SQL 中的 insertupdatedelete 语句,返回影响的行数。

Statement 对象有如下常用的用于批量操作的方法:

  • void addBatch(String sql):将给定的 SQL 命令添加到此 Statement 对象的当前命令列表中;
  • int[] executeBatch():将一批命令提交给数据库来执行,如果全部命令执行成功,则返回更新计数组成的数组;
  • void clearBatch():清空此 Statement 对象的当前 SQL 命令列表。
ResultSet

ResultSet 是结果集对象,它是 select 语句查询的结果的封装。下面列举了 ResultSet 类的常用实例方法:

  • boolean next():将光标从当前位置向前移一行,判断是否有下一行记录;
  • getString(String columnLable):以 Java 语言中 String 的形式获取此 ResultSet 对象的当前行中指定的值;
  • getInt(String columnLable):以 Java 语言中 int 的形式获取此 ResultSet 对象的当前行中指定的值;
  • getXXX():对于不同类型的数据,可以使用 getXXX() 来获取数据(例如 getString()getInt()),另外还有一个通用的 getObject() 方法,用于获取所有 Object 类型的数据。

JDBC资源的释放

JDBC 程序运行完成后,一定要记得释放程序在运行过程中,创建的那些与数据库进行交互的对象,这些对象通常是 ResultSetStatementConnection 对象。特别是 Connection 对象,它是非常稀有的资源,用完后必须马上释放,如果此对象不能及时、正确的关闭,极易导致系统的宕机。Connection 对象的使用原则是尽量晚创建,尽量早释放。

Optional类

空指针异常(NUllPointerExecptions)是Java中最常见的异常,一方面,程序员不得不在代码中写很多null的检查逻辑,让代码看起来非常臃肿;另一方面,由于其属于运行时异常,是非常难以预判的。

为了预防空指针异常,在Java 8中也引入了Optional类。

Optional 类位于java.util包下,是一个可以为 null容器对象,如果值存在则isPresent()方法会返回 true ,调用 get() 方法会返回该对象,可以有效避免空指针异常。下面我们来学习如何实例化这个类,以及这个类下提供了哪些常用方法。

创建Optional对象

  • Optional.of(T t);创建一个 Optional 对象,参数 t 必须非空;
  • Optional.empty();创建一个空的Optional实例;
  • Optional.ofNullable(T t);创建一个Optional对象,参数t 可以为 null

Optional<T>类提供了如下常用方法:

  • booean isPresent():判断是否包换对象;
  • void ifPresent(Consumer<? super T> consumer):如果有值,就执行 Consumer 接口的实现代码,并且该值会作为参数传递给它;
  • T get():如果调用对象包含值,返回该值,否则抛出异常;
  • T orElse(T other):如果有值则将其返回,否则返回指定的other 对象;
  • T orElseGet(Supplier<? extends T other>):如果有值则将其返回,否则返回由Supplier接口实现提供的对象;
  • T orElseThrow(Supplier<? extends X> exceptionSupplier):如果有值则将其返回,否则抛出由Supplier接口实现提供的异常。
目录
相关文章
|
7月前
|
存储 缓存 Java
【Java】《2小时搞定多线程》个人笔记
【Java】《2小时搞定多线程》个人笔记
146 0
|
7月前
|
消息中间件 缓存 NoSQL
【Java】《2小时搞定多线程》个人笔记(二)
【Java】《2小时搞定多线程》个人笔记
92 0
【Java】《2小时搞定多线程》个人笔记(二)
|
7月前
|
存储 缓存 安全
【Java】《2小时搞定多线程》个人笔记(一)
【Java】《2小时搞定多线程》个人笔记
67 0
【Java】《2小时搞定多线程》个人笔记(一)
|
SQL XML 前端开发
JAVA个人笔记
JAVA个人笔记
|
12天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
20天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
3天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
3天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
24 1
|
11天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
|
11天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####