Java技术栈 1-1 Java基础
恁爹说:不积跬步无以至千里,不积小流无以成江海。Java学习虽是工科活,但在我看来就是个文科,始终离不开一个背字。与君共勉,背就完事了!
本文尚需完善内容:
1)String.format()方法
2)集合、for-each语句底层原理
3)异或运算符、原码、反码、补码(雪花算法中要用到,首次出处:https://blog.csdn.net/qq_26222859/article/details/123689230)
目录
- Java技术栈 1-1 Java基础
- 一、Java语言
- 二、字节码指令
- 三、Java语法
- 四、类与接口
- 五、Java常用API
- 六、Java反射和代理
- 七、Java异常处理
- 八、Java IO/NIO及序列化
- 九、网络编程
- 十、Java注解
- 十一、Java函数式编程
- 十二、Java容器(集合)
一、Java语言
1.何为编程
编程就是让计算机为解决某个问题而使用某种程序设计语言编写程序代码,并最终得到结果的过程。
2.什么是java
- Java是一门面向对象编程语言,不仅吸收了
C++
语言的各种优点,还摒弃了C++
里难以理解的多继承、 指针等概念,因此Java语言具有功能强大和简单易 用两个特征 - Java语言作为静态面向对象编程语言的 代表,极好地实现了面向对 象理论,允许程序员以优雅的思维方式进行复杂的编程。
3.Java语言特点
3.1 平台无关性( Java 虚拟机实现平台无关性)
- 定义:又称跨平台性,是指java语言编写的程序,一次编译后,可以在多个系统平台上 运行。
- 实现原理:Java程序是通过java虚拟机在系统平台上运行的,而Java虚拟机识别的Java字节码是相同的,因此只要该系统可以安装相应的java虚拟机,该系统就可以运行java程序
3.2 面向对象(封装,继承,多态)
- 面向对象:是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。
- 优势:性能较高
- 应用:单片机、嵌入式开发等一般采用面向过程开发
1.1 面向对象的三大特性:
1)封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
2)继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以 增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
- 通过使用继承可以提高代码复用性。
- 继承是多态的前提。
3)多态:是指类中定义的引用类型成员变量的具体类型以及其方法调用在编程时不确定,在程序运行过程中才确定下来。
- 实现方式:继承(多个子类对同一方法的重写以及方法重载)和抽象(实现抽象方法以及方法重载)
1.2 面向对象5大基本原则:
实际上参考了设计模式中的七大原则
1)单一职责原则SRP(Single Responsibility Principle):类的功能要单一。
2)开放封闭原则OCP(Open-Close Principle):对扩展开放,对修改关闭
3)里氏替换原则LSP(the Liskov Substitution Principle):继承类应该继承超类的特性,而不是重写超类的特性
4)依赖倒置原则:代码依赖抽象,而不是依赖抽象的具体实现。(细节就是子类、实现类、类中方法) 。例如:跨部门沟通时,员工a只要找到各个部门的领导,由领导通知属下对应员工办事即可,而不是由员工a找到这些部门的员工直接协调他们做事。→符合高内聚低耦合。
5)接口隔离原则:将臃肿的接口拆分为更小的更具体的接口,使接口中的方法职责足够单一,方便实现类进行单一实现。→是针对接口提出的单一原则,是对函数式编程的解释,符合高内聚低耦合
- 面向过程:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。
- 优势:面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展,从而可以设计出低耦合的系统。
- 缺陷:性能上比面向过程要低。
3.3 可靠安全、支持多线程
3.4 编译与解释并存
1. 热点代码:源代码中经常需要被调⽤的⽅法和代码块。
2. Java程序执行过程
1)源代码经过JDK中的javac工具编译成.class字节码文件。
2)JVM中类加载器⾸先加载字节码⽂件,然后通过解释器逐⾏解释成机器可执行的二进制机器码。
3)操作系统执行二进制机器码
3. 缘由
上述这种方式的执⾏速度会相对较慢。而通过引入JIT 编译器(运行时编译)以及JDK 9出现的新的编译模式 AOT(Ahead of Time Compilation),在源代码第一次编译后,除了生成字节码外,也会将字节码对应的机器码保存下来,下次可以直接使⽤。⽽我们知道,机器码的运⾏效率肯定是⾼于 Java 解释器的,因此热点代码执⾏的次数越多,它的速度就越快。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔。
4.编码
- Java语言采用何种编码方案?有何特点?
- Java语言采用Unicode编码标准:
Unicode(标准码)
,它为每个字符制订了一 个唯一的数值,因此在任何的语言、平台、程序都可以放心的使用。
5.JVM、JRE和JDK关系
- JVM(Java Virtual Machine):是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果,从而实现跨平台。
1.1 什么是字节码?采用字节码的好处是什么?
1)定义:JVM 可以理解的代码就叫做字节码(即扩展名为.class
的⽂件),它不⾯ 向任何特定的处理器,只⾯向虚拟机。
2)好处:
- a.使得Java语言运行高效:Java 语⾔通过字节码的⽅式,在⼀定程度上解决了传统解释型语⾔执⾏效率低的问题,同时⼜保留了解释型语⾔可移植的特点。
- b.跨平台性:由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重新编译便可在多种不同操作系统的计算机上运⾏(即一次编译,处处运行)。
1.2 虚拟机的种类
1)HotSpot JVM:最初是Sun公司开发的JVM,后随Oracle收购后成该公司的一种JVM。在JDK1.7版本之前永久代未移除前和JRockit有区别,现已合并JRockit
2)JRockit JVM:Oracle公司自研的JVM,现已和HotSpot合并
- JRE(Java Runtime Environment):包括Java虚拟机和Java程序所需的核心类库等
- 核 心类库:主要是
java.lang
包,包含了运行Java程序必不可少的系统类,如基本数 据类型、基本数学函数、字符串处理、线程、异常处理类等。系统缺省加载这个包。
如果想要运行一个开发好的Java程序,计算机只需安装JRE即可。
- JDK(Java Development Kit):是提供给Java开发人员使用,其中包含了Java的开发 工具,也包括了JRE。
安装了JDK,就无需再单独安装JRE
- 开发工 具:编译工具(javac.exe),打包工具(jar.exe)等
- Oracle JDK 和 OpenJDK 的对⽐:
- a. OpenJDK 是⼀个参考模型并且是完全开源的,⽽ Oracle JDK 是 OpenJDK 的⼀个实现,并不是完全开源的。
- b. Oracle JDK ⽐ OpenJDK 更稳定,更适合企业级应用:OpenJDK 和 Oracle JDK 的代码⼏乎相同,但 Oracle JDK 有更多的类和⼀些错误修复。
- c. Oracle JDK 根据⼆进制代码许可协议获得许可,⽽ OpenJDK 根据 GPL v2 许可获得许可JDK 有更多的类和⼀些错误修复。
- 关系图:
6.JDK1.5之后的三大版本
- Java SE(J2SE,Java 2 Platform Standard Edition,标准版): 以前称为 J2SE,包含了支持 Java Web 服务开发的类,并为Java EE和Java ME提供基础。
- Java EE(J2EE,Java 2 Platform Enterprise Edition,企业版):以前称为 J2EE。 是在 Java SE 的基础上构建的,它提供 Web 服务、组件模型、 管理和通信 API,可以用来实现企业级的面向服务体系结构(service-oriented architecture,SOA)和 Web2.0应用程序。
2.1 单片架构、SOA架构和微服务架构的区别:
1)单片架构:类似于大容器,其中应用程序的所有软件组件都紧密封装在一起
2)SOA(Service-Oriented Architecture 面向服务架构) :是一种相互通信的服务集合。通信可以是简单的数据传递,也可以是协调两个或多个活动的服务。
3)微服务架构是一种架构风格,它将应用程序构建为以业务域为模型的小型自治服务集合。
- Java ME(J2ME,Java 2 Platform Micro Edition,微型版) :Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视 机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。
7.Java和C++的区别
- 都是⾯向对象的语⾔,都⽀持封装、继承和多态。
- Java 不提供指针来直接访问内存,程序内存更加安全。
- Java 的类是单继承的,
C++
⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多继承。 - Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存。
- 在 C 语⾔中,字符串或字符数组最后都会有⼀个额外的字符
‘\0’
来表示结束。但是,Java 语言中没有结束符这⼀概念。
5.1 Java中无需结束符的原因:
Java里面一切都是对象,是对象的话,字符串肯定就有长度,即然有长度,编译器就可以确定要输出的字符个数,当然也就没有必要去浪费那1字节的空间用以标明字符串的结束了。(比如,数组对象里有一个属性length,就是数组的长度,String类里面有方法length()
可以确定字符串的长度,因此对于输出函数来说,有直接的大小可以判断字符串的边界,编译器就没必要再去浪费一个空间标识字符串的结束)
8.Java应用程序和小程序有哪些差别
- Java应用程序:
- 在Java应用程序中,可以有多个类,但只能有一个类是主类(Java 程序执行的入口点)。这个主类是指包含
main()
方法的类。 - 应用程序的主类不一定要求是
public
类。
- 小程序:
- 在 Java 小程序中,也可以有多个类,同样只能有一个类是主类,但这个主类是一个继承自系统类 JApplet 或 Applet 的子类,通过嵌入浏览器页面并调用
init()
或者run()
来启动。 - 小程序的主类要求必须是
public
类。
9.JDK1.8 十大新特性
9.1 接口的默认方法
9.2 Lambda 表达式
9.3 Lambda 作用域
9.4 函数式接口
9.5 访问局部变量
9.6 方法与构造函数引用
9.7 访问对象字段与静态变量
9.8 访问接口的默认方法
9.9 Date API
9.10 Annotation注解
10.Java 9 十大新特性
10.1 modularity System模块系统
10.2 HTTP/2
10.3 JShell
10.4 不可变集合工厂方法
10.5 私有接口方法
10.6 HTML5风格的Java帮助文档
10.7 多版本兼容JAR
10.8 统一JVM日志
10.9 Java9的垃圾收集机制
10.10 I/O流新特性
11.Java 包
import java
和import javax
有什么区别?
刚开始的时候Java API所必需的包是
java
开头的包,javax
当时只是扩展API包来使用。
然而随着时间的推移,javax
逐渐地扩展成为Java API的组成部分。但是,将扩展从javax
包移动到java
包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定javax
包将成为标准 API 的一部分。
所以,实际上java
和javax
没有区别,都是一样的作用。
- JDK中常用的包:
java.lang
:存放系统的基础类;java.io
:存放所有输入输出有关的类,比如文件操作等;java.nio
:为了完善io
包中的功能,提高io
包中性能而写的一个新包;java.net
:存放与网络有关的类;java.util
:存放系统辅助类,特别是集合类;java.sql
:存放数据库操作的类。
二、字节码指令
1.介绍
1:定义:JVM用来控制CPU执行的一套程序指令集
2:组成:字节码指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码Opcode
)以及跟随其后的零至多个代表此操作所需参数(称为操作数Operands
)而构成。
废话与吹逼:
对于JVM来说,程序的执行过程就是入栈出栈的过程。在这个过程中伴随着从局部变量表存值取值,然后压栈,然后对栈内数据做计算(出栈)。此外,可以通过程序流程控制指令(改变程序计数器),达到循环、分支、跳转的效果。如果程序需要改变对象(堆),需要用到对象操作指令。如果变量类型是兼容的,编译器还会加入转换指令(基本类型)。
- 分类:按照指令的用途划分:
- 加载与存储指令
- 算术指令
- 类型转换指令
- 对象的创建与访问指令
- 方法调用与返回指令
- 操作数栈管理指令
- 比较控制指令
- 异常处理指令
- 同步控制指令
为了探究代码的内存加工逻辑(例如String对象的内存分配),我们有时候需要反汇编.class文件来查看具体的字节码指令。
2. 如何查看字节码指令?
- 使用
javap -v xxx.class
命令:在当前.class文件的目录,打开控制台输入该命令,即可看到翻译后的汇编代码(包括:字节码指令、行号、局部变量表、常量池等信息):
- 源代码:
public class StringTest {
public StringTest() {
}
public static void main(String[] args) {
String a = "1";
}
}
- 反汇编后的结果:
Last modified 2022-7-20; size 422 bytes
MD5 checksum 49327255721de2f988d8838e6161276c
Compiled from "StringTest.java"
public class StringTest
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = String #21 // 1
#3 = Class #22 // StringTest
#4 = Class #23 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 LStringTest;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 a
#17 = Utf8 Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 StringTest.java
#20 = NameAndType #5:#6 // "<init>":()V
#21 = Utf8 1
#22 = Utf8 StringTest
#23 = Utf8 java/lang/Object
{
public StringTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LStringTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 1
2: astore_1
3: return
LineNumberTable:
line 3: 0
line 15: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 a Ljava/lang/String;
}
SourceFile: "StringTest.java"
通常我们只关注指定方法的字节码指令步骤,比方说这边的main方法的String实例a构造过程,对应的指令如下:
0: ldc #2 // String 1
2: astore_1
3: return
- 使用idea插件Jclasslib bytecode viewer:
- File→Settings打开设置页面,选择Plugins,在搜索框内输入jclasslib
- 选择安装,之后重启idea
- Build→Build Project,然后打开编译后的.class文件,选择 View→Show Bytecode With jclasslib,即可打开反汇编视图。
- 在idea的 File→Settings→Tools→External Tool里自定义一个扩展插件:
- 使用时直接右击代码,选择External Tool→show byte codes即可!
3.常见字节码指令及释义
本笔记只略做说明,详细学习之后再说吧,,
3.1 new
- 功能:在java堆上为特定类型的对象分配内存空间,并创建一个instanceOopDesc(Java对象)实例,最后将Java对象实例的地址(引用)压入操作数栈的栈顶。
- 典型应用:
new
一个对象
3.2 ldc
- 功能:分为两大类:
- 将
int
、float
、或者一个类、方法类型或方法句柄的符号引用压入操作数栈的栈顶 - 将指定字符串常量的String对象实例(String常量)的引用从常量池中压入操作数栈栈顶。
- 典型应用:String对象内存分配
3.3 astore
- 功能:弹出操作数栈的栈顶元素,赋值给指定索引(
index
)的局部变量。
index
对应的变量必须位于局部变量表中;astore
指令只能操作栈顶的returnAddress
类型或reference
类型的数据
- 典型应用:任何以对象实例赋值的场景
3.4 dup
- 功能:复制操作数栈顶值,并将其压入栈顶,此时操作数栈上有连续相同的两个对象地址
- 典型应用:
new
一个对象
3.5 invokespecial、invokevirtual
- 功能:
- 依次从操作数栈的栈顶取出待执行方法的各项参数(
args
)、对象引用(objectref
),并调用对象引用所指实例的该方法。 - 执行完方法后,除非该方法无返回值(构造方法和返回
void
的方法),否则将返回值压入操作数栈的栈顶(因为Java是值传递,所以压入栈的只能是数值或引用)
1.1 注意事项
1)执行这两个指令前需确保 对象引用、要执行的方法的各项参数(左先右后)已经被其他指令依次压入操作数栈中。
2)invokespecial通常用来执行实例方法,尤其是实例的初始化方法(<init>:()V
);而invokevirtual则是用来执行特定非私有的实例方法
更多指令使用请参考:www.cnblogs.com/mongotea/p/11980087.html
- 典型应用:
invokespecial
:new
一个对象、调用私有方法、调用父类方法invokevirtual
:调用实例的公共方法
3.6 aload
- 功能:将指定索引(
index
)的局部变量压入操作数栈的栈顶。 - 典型应用:
new
一个对象
三、Java语法
1.标识符与关键字
1.1 标识符
- 定义:是指在程序中,我们自己定义的内容,譬如,类的名字,方法名称以及变量名称等等,都是标识符。
- 命名规则:硬性要求
- 标识符可以包含英文字母,
0-9
的数字,$
以及_
; - 标识符不能以数字开头
- 自定义标识符不能是关键字
- 命名规范:非硬性要求
- 类名规范:首字符大写,后面每个单词首字母大写(大驼峰式)。
- 变量名规范:首字母小写,后面每个单词首字母大写(小驼峰式)。
- 方法名规范:同变量名。
- 关键字的定义:关键字是被赋予特殊含义的标识符,只能用于特定的地方。
- 常见关键字:
访问控制 | private | protected | public | ||||
---|---|---|---|---|---|---|---|
类,方法 和变量修 饰符 | abstract | class | extends | final | implements | interface | native |
new | static | strictfp | synchronized | transient | volatile | ||
程序控制 | break | continue | return | do | while | if | else |
for | instanceof | switch | case | default | |||
错误处理 | try | catch | throw | throws | finally | resources | |
包相关 | import | package | |||||
基本类型 | boolean | byte | char | double | float | int | long |
short | null | true | false | ||||
变量引用 | super | this | void | ||||
保留字 | goto | const |
2.数据类型
2.1 定义
Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类 型,在内存中分配了不同大小的内存空间。
2.2 分类
2.2.1 基本数据类型(primitive types)
- 定义:又叫原生数据类型
- 分类:
- 数值型(numberic Types):整数类型(
byte
,short
,int
,long
)、浮点类型(float
,double
)、字符型(char
) - 布尔型(boolean Type):
boolean
- returnAddress类型:
扩展:关于returnAddress类型
1)returnAddress
类型的值是指向字节码的指针。
2)不管是物理机还是虚拟机,运行时内存中的数据总归可分为两类:代码,数据。对于冯诺依曼结构的计算机,指令数据和数值数据都存储在内存中,而哈弗结构的计算机,将程序指令与数据分开存储。对于 JVM来说,程序就是存储在方法区的字节码指令,而returnAddress
类型的值就是指向特定指令内存地址的指针。
3)JVM支持多线程,每个线程有自己的程序计数器(pc register),而 pc中的值就是当前指令所在的内存地址,即returnAddress
类型的数据,当线程执行native
方法时,pc中的值为undefined
。
4)returnAddress
类型会被jsr
、ret
、jsr_w
指令使用,主要被用来实现finally
语句块,但后来改为finally
块代码的方式实现;到JDK1.7,虚拟机不再允许.class文件出现这几条指令,故returnAddress
也用不到了。
- 各数据类型占用内存、取值范围、默认值以及对应封装类:
1字节=8 bit
类型 | 类型名称 | 关键字 | 占用内存 | 取值范围 | 默认值 | 封装类 |
---|---|---|---|---|---|---|
整形 | 字节型 | byte | 1字节 | -27~ 27-1 | 0 | Byte |
短整型 | short | 2字节 | -215~215-1 | 0 | Short | |
整形 | int | 4字节 | -231~231-1 | 0 | Integer | |
长整型 | long | 8字节 | -263~263-1 | 0L | Long | |
浮点型 | 单精度浮点型 | float | 4字节 | -3.403E38~3.403E38 | 0.0F | Float |
双精度浮点型 | double | 8字节 | -1.798E308~1.798E308 | 0.0D | Double | |
字符型 | 字符型 | char | 2字节 | 任意用单引号括的字符 | ‘\u0000’ | Character |
布尔型 | 布尔型 | boolean | - | true、false | false | Boolean |
- switch关键字的作用域:
1)在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。
2)从 Java5 开始,Java 中引入 了枚举类型,expr 也可以是 enum 类型
3)从 Java 7 开始,expr 还可以是字符串(String)
4)但是长整 型(long)在目前所有的版 本中都是不可以的。
loat f=3.4;
是否正确?
不正确。
3.4
是双精度数,将双精度型(double
)赋值给浮点型(float
)属于下转型(down-casting
,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4;
或者写成float f =3.4F;
(3*0.1 == 0.3)
返回值是什么?
false,因为有些浮点数不能完全精确的表示出来。
- boolean型的特殊之处
1)在Java虚拟机中没有任何供
boolean
值专用的字节码指令,Java语言表达式所操作的boolean
值,在编译之后都使用Java
虚拟机中的int数据类型来代替(占32位),而boolean
数组将会被编码成Java虚拟机的byte
数组,每个元素占8位。
2)因此,boolean类型单独使用是4个字节,在数组中又是1个字节。
使用int的原因是,对于当下32位的CPU来说,一次处理数据采用32位(这里不是指的是32/64位系统,而是指CPU硬件层面)时具有高效存取的特点。
2.2.1 引用数据类型(reference types)
- 分类:类(
class
)、接口(interface
)、数组([]
)、泛型 - 引用类型的实现方式:
JVM 规范中并没有详细规定引用类型的实现细节,比如引用应该通过何种方式去定位、访问堆中的对象,具体的对象访问方式取决于虚拟机的具体实现,比如HotSpot有其自己的实现方案。
- 主流的对象访问方式:
- 使用句柄:
使用句柄访问的最大好处就是对象引用中存储的是稳定的句柄地址,在对象被移动(比如垃圾回收时,整理内存空间,会移动对象的存储位置)时只会改变句柄中示例数据的指针,而对象引用本身不需要修改。
- 使用直接指针:
使用直接指针访问的方式,类似于 C++ 中的虚表(虚表就是指向对象类型数据的指针),其最大好处就是速度更快,节省了一次内存寻址的时间开销。
2.3 基本数据类型和引用数据类型的区别
- 基本数据类型在声明时系统会自动给它分配空间;
- 引用类型声明时只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。
3.访问修饰符
- 定义:Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问的标识符
- 分类:Java 支持 4 种不同的访问权限,按照由低到高分别是private、default、protected 、public
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部 类)
- default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用 任何修饰符。使用对象:类、接口、变量、方法。
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意: 不能修饰类(外部类)
- public : 对所有类可见。使用对象:类、接口、变量、方法
4.final修饰符
- 作用域:用于修饰类、属性和方法
- 特性:
- 被
final
修饰的类不可以被继承 - 被
final
修饰的方法不可以被重写(覆盖),但是可以被重载 - 被
final
修饰的变量不可以被改变,被final
修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
- final、finally和finalize区别:
final
可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写,修饰变量表示该变量是一个常量不能被重新赋值。finally
一般作用在try-catch
代码块中,在处理异常的时候,通常我们将一定要执行的代码方法放入finally
代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
仅当遇到System.exit(-1)时,finally块不执行
finalize
:表示系统回收垃圾做的一些处理工作,一般在调用System.gc()
方法或者Runtime.getRunTime().gc
时可能会调用finalize()
方法。
5.static关键字
- static特性/设置的意义:
1)被static修饰的变量或者方法是独立于该类的任何对象。以致于即使没有创建对象,也能使用属性和调用方法。
2)在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行 初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
3)static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。
4)被static修饰的变量或者方法或者类是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问
- static关键字用法:
- 修饰成员变量
- 修饰成员方法
- 静态代码块
- 修饰类(只能修饰内部类也就是静态内部类)
- 静态导包:
import static
2.1 静态导包:
1)何时引入:import static
是在JDK 1.5之后引入
2)用法:用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名
3)示例:
import static java.lang.Math.*;
public class Test{
public static void main(String[] args){
//System.out.println(Math.sin(20));传统做法
System.out.println(sin(20));
}
}
- 注意事项:
- 静态只能访问静态。
- 非静态既可以访问非静态的,也可以访问静态的。
6.instanceof 关键字
- 定义:是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例
- 用法:
- obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false:
boolean result = obj instanceof Class;
- 在 JavaSE规范 中对
instance of
运算符的规定就是:如果 obj 为 null,那么将返回false:
System.out.println(null instanceof Object);// 返回false
- 注意事项:判断对象必须是引用类型,不能是基本类型:
int i = 0;
System.out.println(i instanceof Integer);// 编译不通过
System.out.println(i instanceof Object);// 编译不通过
7.变量引用:this关键字与super关键字
7.1 this关键字
- 定义:
this
是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针(引用)。 - 用法:
- 普通的直接引用:
this
相当于是指向当前对象本身 - 形参与成员名字重名,用
this
来区分 - 引用本类的构造函数:
this()
、this(参数1,参数2,...)
注意事项:被构造函数中调用时,应该为构造函数中的第一条语句
7.2 super关键字
- 定义:
super
可以理解为指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
注意:
super
本质上只是个关键字!
- 用法:
- 普通的直接引用:
super
相当于是指向当前对象的父类的引用,这样就可以用super.xxx
来引用父类的成员 - 子类中的成员变量或方法与父类中的成员变量或方法同名时,用
super
进行区分 - 引用父类构造函数:
super()
、super(参数1,参数2,...)
注意:
1)super
方法被构造函数中调用时,应该为构造函数中的第一条语句。
2)类构造函数首行缺省了super()
调用。显示调用super()
、super(参数1,参数2,...)
将覆盖缺省调用。→因此子类构造函数首行缺省super()
时,父类需保证有缺省的无参构造函数或自己定义的无参构造函数。
7.3 其他特性:
- 尽管可以在构造方法中用
this
调用一个构造器,但却不能调用两个。 this
和super
不能同时出现在一个构造函数里面。
缘由:因为
this
必然会调用其它的构造函数,其它的构造函数必然也会有super
语句的存在。再加上子类也有super
,就造成super
方法重复,编译不通过
this()
和super()
都指的是对象,所以均不可以在static环境中使用。包括: static变量、static方法、static语句块。
8.运算符
8.1 逻辑运算符
- 种类:
&
(按位与)和&&
(逻辑与)、|
(按位或)和||
(逻辑或)运算符 &
(按位与)和&&
(逻辑与)两者联系和区别:
1)
&
(按位与)和&&
(逻辑与)都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true。
2)但存在区别:&&
之所以称为短路运算,是因为如果&&
左边的表达式的值是 false,右边的表达式会被直 接短路掉,不会进行运算
3)|
(按位或)和||
(逻辑或)同理
8.2 自增自减运算符
- 种类:可以放在操作数之前,也可以放在操作数之后:
i++
、++i
、i--
、--i
- 用法:
- 当运算符放在操作数之前时,先自增/减,再赋值。
- 当运算符放在操作数之后时,先赋值,再自增/减。
口诀:符号在前就先加/减,符号在后就后加/减
8.3 移位运算符
- 种类:
<<
:左移预算符,等同于乘2的n次方>>
:右移预算符,等同于除2的n次方>>>
:无符号右移运算符,不管移动前最高位是0
还是1
,右移后左侧产生的空位部分都以0
来填充。与>>
类似
- 示例1:最有效率的方法计算 2 乘以 8:
2 << 3
原理:计算机底层采用二进制存储数据,左移 3 位相当于乘以 2 的 3 次方,右移 3 位相当于除以 2 的 3 次 方
- 位移运算符相比于普通运算符的运算优势:
对于大数据的2进制运算,位移运算符比那些普通运算符的运算要快很多,因为程序仅仅移动一下而已,不去计算,这样提高了效率,节省了资源。
8.4 复合赋值运算符
- 种类:
+=
、-=
… short s1 = 1; s1 = s1 + 1;
有错吗?
会编译报错。由于 1 是 int 类型,因此
s1+1
运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
short s1 = 1;
s1 += 1;
有错吗?
不报错,会正确编译。因为
s1+= 1;
相当于s1 = (short(s1 + 1);
其中有隐含的强制类型转换。
a=a+b
与a+=b
有什么区别吗?
+=
操作符会进行隐式自动类型转换,此处a+=b
隐式的将加操作的结果类型强制转换为持有结果的类型,a=a+b
则不会自动进行类型转换
8.5 条件运算符
1.种类:==
、!=
、<>
、>
、<
、>=
、<=
2. ==
和equals()
的区别:
==
:它的作用是判断两个对象的值不是相等(基本数据类型比较的是值,引用类型变量存的值是对象的地址)equals()
:它的作用也是判断两个对象的值是否相等,但不能用于比较基本数据类型的变量。equals()
方法存在于Object
类中,详见Object
类相关章节的介绍。
9.类型转换以及类型向下转型(down-casting 窄化)
9.1 基本数据类型
- 自动类型转换:一般是指向上类型转换,多个变量作算术运算时最终表达式的类型会和这几个变量中类型最大的看齐:
Byte->short->int->long->float->double
(char
类型有点特殊,char
和其他类型相加是int
型)
使用复合赋值运算符可能会出现隐式自动类型转换
- 强制类型转换:又称为类型下转型(
down-casting
窄化),是由高类型(如双精度浮点类型)转化成低类型(单精度浮点类型),会出现精度丢失的情况。转化方式:在等式右侧声明下转型类型:float f =3.4F;
9.2 引用类型
- 自动类型转换:一般是指向上类型转换,即:父类引用指向子类实例对象。
- 形式:等式左侧对象是右侧对象的超类时(父子或实现关系),等式右侧会自动类型转换成父类引用:
- 强制类型转换:又称为类型下转型(
down-casting
窄化)。
- 形式:等式右侧对象通过在左侧括号声明等式左侧对象的类类型,来完成类型转换的方式:
子类类型 子类对象=(子类类型)父类对象
- 可能会出现类型转换异常
ClassCastException
:当等式右侧引用类型的具体引用和左侧没有父子或实现关系时)。
10.Java注释
- 定义:用于解释说明程序的文字
- 分类:
- 单行注释 格式:
// 注释文字
- 多行注释 格式:
/* 注释文字 */
- 文档注释 格式:
/** 注释文字 */
- 好处:
- 适当地加入注释可以增加程序的可读性,有利于程序的修改、调试和交流。
- 注释的内容在程序编译的时候会被忽视,不会产生目标代码,注释的部分不会对程序的执行结果 产生任何影响。
注意事项:多行和文档注释都不能嵌套使用
11.流程控制语句
11.1 break ,continue ,return 的区别及作用
break
:结束当前的循环体continue
:跳出本次循环,继续执行下次循环return
:结束当前的方法并直接返回
11.2 在 Java 中,如何跳出当前的多重嵌套循环?
- 使用
return
关键字
不推荐,尤其后续还想执行逻辑的话
- 在想跳出多重循环的最外层循环语句前,定义一个标号,然后在里层循环体的代码中使用带有标号的break语句,即可跳出外层循环:
public static void main(String[] args) {
ok:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i=" + i + ",j=" + j);
if (j == 5) {
break ok;
}
}
}
}
12.Java泛型(Generics)
12.1 泛型(钻石操作符)
- 定义:Java 泛型(Generics)又叫钻石操作符,是JDK 1.5中引入的特性, 提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
1.1 扩展:叫钻石操作符是因为语法中包含
<>
符号,形似钻石。
- 本质:泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
- 泛型参数:也被称为一个类型变量,是用于指定一个泛型类型名称的标识符(
T
、E
等)。
12.2 类型通配符
- 定义:类型通配符一般是使用
?
代 替 具 体 的 类 型 参 数 。 例 如List<?>
在逻辑上是List<String>
,List<Integer>
等所有 List<具体类型实参>的父类。 - 其他用法:
<? extends T>
表示该通配符所代表的类型是T
类型的子类。<? super T>
表示该通配符所代表的类型是T
类型的父类。
12.3 类型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。
- 定义:使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉,并以 Object。或指定的类型参数的上界类 来替换泛型参数,这便是类型擦除。
- 举例:如在代码中定义的
List<Object>
和List<String>
等类型,在编译之后都会变成List
。JVM 看到的只是List
,而由泛型附加的类型信息对 JVM 来说是不可见的。 - 特性:JDK1.9之前,钻石操作符与匿名内部类不能共存,从JDK1.9开始了支持。
12.4 泛型的用法
12.4.1 泛型方法(<E>
)
可以写一个泛型方法,该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,编译器适当地处理每一个方法调用:
// 泛型方法 printArray
public static < E > void printArray( E[] inputArray ) {
for ( E element : inputArray ) {
System.out.printf( "%s ", element );
}
}
12.4.2 泛型类
- 泛型类和非泛型类、泛型方法的区别:
- 泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。
- 和泛型方法一样,泛型类的类型参数声明部分也包含一个或多个泛型参数,参数间用逗号隔开。
因为接受一个或多个参数,泛型类也被称为参数化的类或参数化的类型
- 示例:
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
12.4.3 泛型匿名内部类
- 介绍:
- JDK1.8开始,钻石操作符支持与匿名内部类共存,且可以重写非抽象类、非接口的类中方法;
重写抽象类、接口中方法JDK1.8之前就行了,此处不关注
- JDK1.9开始,泛型匿名内部类可以不用声明泛型具体类型
- 版本要求:JDK1.8开始
- 本质:泛型匿名内部类首先是匿名内部类,因此一定是声明类的子类;
- 使用示例:
import java.util.ArrayList;
import java.util.List;
public class NewGenericsTest {
public static void main(String[] args) {
// JDK1.9支持无需声明具体类型的泛型内部匿名类(可自行判断):ArrayList无String也没事
// List<String> strList = new ArrayList<>() {
// JDK1.8仅支持声明具体类型的泛型匿名内部类:ArrayList必须要有String
List<String> strList = new ArrayList<String>() {
@Override
public boolean add(String e) {
System.out.println("重写父类方法,我读到你了:" + e);
return true;
}
};
strList.add("gnm");
}
}
运行结果:
重写父类方法,我读到你了:gnm
12.5 如何表示一个既有继承又有实现的泛型?
- 形式:
T extends Object & Comparable<? super T>
四、类与接口
1.Java变量与方法
1.1 变量、成员变量、局部变量、静态变量、实例变量
- 变量:在程序执行的过程中,在某个范围内其值可以发生改变的量
从本质上讲,变量其实是内存中的一小块区域
- 成员变量:方法外部,类内部定义的变量
- 作用域:针对整个类有效
- 存储位置:存储在堆内存中
- 生命周期:随着对象的创建而存在,随着对象的消失而消失
- 初始值:有默认初始值。
- 局部变量:类的方法中的变量
- 作用域:只在某个范围内有效。(一般指的就是方法,语句体内)
- 存储位置:在方法被调用,或者语句被执行的时候存在,存储在栈内存中。当方法调用完或者语句结束后,就自动释放。
- 生命周期:在方法被调用,或者语句被执行的时候存在,当方法调用完,或者语句结束后,就自动释放。
- 初始值:没有默认初始值,使用前必须赋值。
1.1 使用变量时需要遵循的原则:就近原则,首先在局部范围找,有就使用;接着在成员位置找
- 静态变量:是成员变量,且属于类而不属于任何实例对象,但被所有的对象所共享,所以在内存中只会有一份。它当且仅当在类初次加载时会被初始化,JVM只为静态变量分配一次内存空间。
- 多个静态变量时的初始化顺序:按照定义的顺序进行初始化。
4.1 静态变量内存位置的变化:
1)在JDK1.7之前,HotSpot VM把静态变量存在InstanceKlass的末尾;
2)JDK1.7之后,为了配合PermGen移除的工作,静态变量被挪到Java mirror(Class对象)的末尾了。
- 实例变量:是成员变量,且属于实例对象。每次创建对象,都会为每个对象分配实例变量内存空间,各个对象的实例变量间互不影响。
1.2 方法、构造方法
- 什么是方法的返回值?返回值的作用是什么?
- 返回值:获取到的某个方法体中的代码执行后产生的结果
- 作用:接收结果,使得它可以用于后续操作。
- 构造方法的作用:是完成对类对象的初始化工作
- 没有声明构造方法的类能否正确执行?
可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法(缺省的构造方法)。
- 构造方法的特性:
- 名字与类名相同;
- 没有返回值,但不能用
void
声明构造函数; - 生成类的对象时自动执行,无需调用。
- 构造方法可以被重载,在不同构造方法中,可以通过
this(参数1,参数2,...)
或this()
调用其他构造方法。
注意
this()
、this(参数1,参数2,...)
方法必须放在构造方法体内的第一行
- 若主动声明有参构造方法,则Java编译时视作只有有参构造方法,无缺省无参构造方法;除非再主动声明无参构造方法
- 定义一个无参构造方法的作用:
Java程序在执行子类的构造方法之前,如果没有显示用
super()
、super(参数1,参数2,...)
来调用父类特定的构造方法,则会调用父类中的无参构造方法(缺省调用了super()
)。因此父类中没有缺省的无参构造方法 或 定义了有参构造方法却没定义无参构造方法时,子类编译报错。
- 调用子类构造方法时首行必须先调用父类构造方法(缺省调用super()或显示调用super):
- 目的:帮助子类做初始化工作
1.3 静态方法、实例方法
- 静态方法
- 调用方式:①
类名.方法名
②对象名.方法名
- 访问范围:静态方法访问本类的成员时,只允许访问静态成员(即静态成员变量、静态方法、静态内部类)和接口,而不允许访问实例成员变量和实例方法
- 实例方法
- 调用方式:①
对象名.方法名
- 访问范围:无限制
- 静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静 态变量,也不可以访问非静态变量成员。
1.4 值传递、引用传递
1)编程语言中参数传递给方法的两种方式:值传递、引用传递。
2)编程语言中,一个方法可以修改引用传递所传的外部参数变量的变量值,而不能修改值传递所传的外部参数变量的值!
3)Java 程序设计语言总是采用值传递,方法得到的是所有参数值的一个拷贝,因此方法不能修改传递给它的任何外层参数变量的内容。
- 值传递(call by value):也叫按值调用,表示方法接收的是调用者提供的值(拷贝的是外层变量的值,传递后内外两个值互不影响了,方法不能修改传递给它的任何参数变量的内容)
- 引用传递(call by reference):也叫按引用调用,表示方法接收的是调用者提供的变量地址(拷贝的是外层变量的引用,传递后内部使用的就是外部参数的地址,方法可以修改传递给它的任何参数变量的内容)。
- 值传递示例:
- 方法外部参数是基本数据类型:
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
结果如下:
a = 20
b = 10
num1 = 10
num2 = 20
解析:
1)图示:
2)说明:
在swap()
方法中,a
、b
的值进行交换,并不会影响到 num1
、num2
。因为a
、 b
中的值,只是从num1
、num2
复制过来的。也就是说,a
、b
相当于 num1
、num2
的副本,副本的内容无论怎么修改,都不会影响到原件本身
- 方法外部参数是引用数据类型:
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
结果如下:
1
0
解析:
1)图示:
2)说明:方法参数数组array
对象对外部调用数组arr
的值进行拷贝,而arr
因为是引用类型,其值为数组空间的引用,因此array
和arr
都指向同一片内存空间,对array[0]
的修改就是对arr[0]
的修改
4. 引用传递示例:还拿上面的数组案例为例,如果是引用传递的话,对象array的值就成了00…00,也就是数组引用arr的内存地址了。这种情况下对象array就不再是数组了,而是数组引用的引用(数组指针),指向的是一个数组引用,本身却无法提供数组的功能。
1)图示:
1.5 方法重载、方法重写
方法重载和方法重写是实现Java多态的两种手段,另外一种是实现抽象方法
1.5.1 方法重载:
- 定义:定义方法名相同、参数列表不同的方法称作方法重载。
【注意】:
1)参数列表不同:参数类型不同或参数个数不同
2)重载与方法的返回值无关,重载必须发生在同类或继承、实现关系的子类中
- 侧重于编译时的多态性
1.5.2 方法重写:
- 定义:子类对父类的方法体进行覆写称作方法重写。
- 在调用父类引用子类对象时,会调用子类重写的方法。
- 侧重于运行时的多态性
在继承关系中,父类引用是子类实例对象的情况下,用父类引用是无法调用到子类中重写之后又重载的方法的。→父类框定了范围
构造方法(构造器 Constructor)不能被 override(重写),但是可以overload(重载)
- 使用条件:
- 发生在继承关系下且被重写的方法是可继承的:诸如父类中
pivate
方法无法被重写。 - 方法的访问修饰符权限不能严于父类。
- 返回值类型、方法名、方法的参数列表要和父类相同。
- 子类不能抛出比父类更多的检查时异常(空指针异常等运行时异常除外)。
- 不能被重写的方法:
- 无法被继承的方法:
private
修饰的方法、默认权限但不在同包的方法、构造方法 - 静态方法
final
修饰的方法。
1.5.3 方法重写和方法重载的区别:
- 重载的方法是新增的方法,重写的方法还是同一个方法。
- 重载不考虑返回值类型,而重写要求返回值类型必须相同。
- 重载形参列表必须不同,而重写要求形参列表必须相同。
- 重载不考虑访问修饰符权限,而重写要求访问修饰符权限不能降级。
- 重写必定发生在继承关系(两个类中)中,而重载只需要发生在继承、实现关系或本类中即可(两个类或干脆一个类)。
1.5.4 Java方法调用优先级顺序:
顺序:重写>自身>重载>对象的上转型>下转型
- 示例1:
1)存在方法
A(M,N)
,方法A(m,n)
重载了方法A(M,N)
,方法A(M,N)
的参数M
、N
恰好是方法A(m,n)
对应参数m
、n
的父类(或是接口),那么在方法A(M,N)
内部调用A(m,n)
,则这边的A(m,n)
是重载的方法,而不是将m
、n
对象上转型调用A(M,N)
2)不存在A(m,n)
这个方法的话,会出现参数的上转型然后就调用A(M,N)
自身了,出现递归调用
- 示例2:写出Test的运行结果
public interface A {
public void a();
public void b();
}
public class B implements A{
public void a() {}
public void b() {}
}
public class C {
int k=0;
public void method(A a){
B b=(B)a;
k++;
if(k>2){
return;
}
System.out.println("method(A a) has invoked!");
method(b);
}
public void method(B b){
A a=b;
System.out.println("method(B b) has invoked!");
method(a);
}
}
public class Test {
public static void main(String[] args) {
C c=new C();
A a=new B();
c.method(a);
}
}
运行结果:
method(A a) has invoked!
method(B b) has invoked!
method(A a) has invoked!
method(B b) has invoked!
- 示例3:写出Test的运行结果
public interface A {
public void a();
public void b();
}
public class B implements A{
public void a() {}
public void b() {}
}
public class C {
int k=0;
public void method(A a){
B b=(B)a;
k++;
if(k>4){
return;
}
System.out.println("method(A a) has invoked!");
method(b);
}
public void method(B b){
A a=b;
k++;
if(k>4){
return;
}
System.out.println("method(B b) has invoked!");
method(b);
}
}
public class Test {
public static void main(String[] args) {
C c=new C();
A a=new B();
c.method(a);
}
}
运行结果:
method(A a) has invoked!
method(B b) has invoked!
method(B b) has invoked!
method(B b) has invoked!
- 示例4:写出Test的运行结果
public interface A {
public void a();
public void b();
}
public class B implements A{
public void a() {}
public void b() {}
}
public class C {
int k=0;
public void method(A a){
B b=(B)a;
k++;
if(k>2){
return;
}
System.out.println("C->method(A a) has invoked!");
method(b);
}
public void method(B b){
A a=b;
k++;
if(k>4){
return;
}
System.out.println("C->method(B b) has invoked!");
method(b);
}
}
public class D extends C {
@Override
public void method(B b){
A a=b;
System.out.println("D->method(B b) has invoked!");
method(a);
}
}
public class Test {
public static void main(String[] args) {
C c=new D();
A a=new B();
c.method(a);
}
}
运行结果:
C->method(A a) has invoked!
D->method(B b) has invoked!
C->method(A a) has invoked!
D->method(B b) has invoked!
【java中方法调用优先级】最终总结:
能够找到重写的就找重写的>能够直接找到调用的就直接调用>有重载的就找重载的>能够对象上转型能用的就对象上转型>能够强制类型转(向下转型)后能用的就强制类型转换
1.6 可变参(…)
- 语法:方法名(数据类型 … 参数名)
数据类型支持所有基本数据类型和引用类型(含泛型)
- 本质:可变参在编译器中识别为数组数据类型
- 特点:
- 传递个数不限
- 类型统一
- 可以不传值:不写或者填null
想传null值需要做强转型
参数名
只能是方法的最后一个参数- 支持数组操作:可传数组
2.Java对象、继承、多态
2.1 Java对象(OOP)
2.1.1 OOP-Klass对象模型
HotSpot JVM 并没有根据Java实例对象直接通过虚拟机映射到新建的
C++
对象,而是设计了一个OOP-Klass模型。
- 定义:OOP-Klass对象模型是HotSpot JVM实现的、用来描述Java对象和
C++
对象间关系的模型。 - 设计理念:no virtual functions allowed 虚函数禁止:
- HotSopt JVM的设计者不想让每个对象中都含有一个
vtable
(虚函数表),所以就把对象模型拆成Klass和OOP。 - 其中OOP中不含有任何虚函数,而Klass就含有虚函数表,可以进行 method dispatch。
该模型参照了Strongtalk VM底层的对象模型
2.1.2 OOP(Ordinary Object Pointer)
- 定义:普通对象指针,它用来表示对象的实例信息。
- 创建时机:是在new的时候创建的。
- 组成:
- 对象头:
Mark Word
:主要存储对象运行时记录信息,如hashCode
、GC分代年龄、锁状态标志(偏向锁、轻量级锁、重量级锁)、线程持有的锁、偏向线程ID、偏向时间戳等。metadata
:元数据指针,即指向方法区的instanceKlass
实例(Klass对象)。
- 对象字段数据:
field data
:存储Java源代码中定义的各种类型字段内容,具体包括父类继承以及子类定义的对象字段。
- 对齐补充:
padding
:非必须,只起到对实例数据的补齐,无具体含义。
HotSpot JVM要求对象大小必须是8字节(64 bit)的整数倍,对象头是8字节的整数倍。
- 底层C++源码:
- oopDesc类:是各种oop类的共同基类:
class oopDesc
{
friend class VMStructs;
private:
volatile markOop _mark; // 对象头
union _metadata
{
wideKlassOop _klass;//普通指针
narrowOop _compressed_klass;//压缩类指针
}
_metadata;
private:
// field addresses in oop
void* field_base(int offset) const;
jbyte* byte_field_addr(int offset) const;
jchar* char_field_addr(int offset) const;
jboolean* bool_field_addr(int offset) const;
jint* int_field_addr(int offset) const;
jshort* short_field_addr(int offset) const;
jlong* long_field_addr(int offset) const;
jfloat* float_field_addr(int offset) const;
jdouble* double_field_addr(int offset) const;
address* address_field_addr(int offset) const;
}
- instanceOopDesc类:是oopDesc的一个子类,表示堆中一个具体的Java对象实例:
class instanceOopDesc : public oopDesc {
public:
static int header_size()
{
return sizeof(instanceOopDesc)/HeapWordSize;
}
static int base_offset_in_bytes()
{
return UseCompressOops ? klass_gap_offset_in_bytes(): sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size)
{
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};
2.1.3 Klass对象和Class对象的关系
- Klass对象:即
instanceKlass
实例,是Java类在HotSpot JVM中的C++
对等体,用来描述Java类,代表一个类的元数据。
- 一般JVM在加载
.class
文件时,会在方法区创建instanceKlass
实例,表示其元数据,包括常量池、字段、方法等。 instanceKlass
实例是HotSpot JVM内部用的,JVM并不把instanceKlass
实例暴露给 Java层(开发者)使用。
- Class对象:是HotSpot VM内部用的Klass对象的“镜像”(
Java mirror
):JVM把InstanceKlass
实例包装了一层来暴露给Java层(开发者)使用。
- 主要用途:提供反射访问
- 完成创建时机:加载->连接->初始化的类加载阶段
- 内存位置:在堆内存中
- 包含内容:
ClassLoader
(类加载器类)
2.1.4 OOP(Java对象)和Klass对象、Class对象的关系
- 图示:
- 每个Java对象的对象头里,
_klass
字段会指向一个JVM内部用来记录类的元数据用的InstanceKlass
实例。 - klass与mirror之间的双向引用:
1)
insanceKlass
里_java_mirror
字段指向该类所对应的Class对象(Java mirror
);
2)HotSpot VM会给Class对象注入一个隐藏字段klass
(注意不是_klass
字段!!),用于指回到其对应的InstanceKlass
实例
- 静态变量内存位置的变化:
- JDK1.7之前,HotSpot VM把静态变量存在
InstanceKlass
实例的末尾; - JDK1.7之后,为了配合PermGen(永久代)移除的工作,静态变量被挪到Class对象(
Java mirror
)的末尾了。
2.1.5 使用new 类名()创建对象的底层加工过程
- 以类A为例,执行
new A()
前(执行invokespecial A::
),如果这个类没有被加载过,JVM会先进行类的加载,并在方法区创建一个instanceKlass
实例表示这个类的运行时元数据。 - 同时,在堆内存中会自动构造一个镜像的Class对象提供反射访问(它和
instanceKlass
实例持有着双向引用) - 加载完类后,JVM就会在堆分配内存,创建一个
instanceOopDesc
对象表示A的实例,并调用构造函数初始化A的实例
3.1 初始化实例包括以下动作:
1)进行Mark Word的填充;
2)将元数据指针(KlassPointer
)指向instanceKlass
实例
3)填充实例变量
- 返回该实例的指针到栈空间对应线程。
2.1.6 Java对象复制
- Java对象复制的方式:将一个对象的引用复制给另外一个对象,一共有三种方式:直接赋值、浅拷贝、深拷贝。
- 直接赋值:
A a1 = a2;
直接赋值实际上复制的是引用,也就是说
a1
和a2
指向的是同一个对象。因此,当a1
变化的时候,a2
里面的成员变量也会跟
着变化。
- 浅拷贝:在拷贝一个对象时,对对象的基本数据类型的成员变量进行拷贝,但对引用类型的成员变量只进行引用的传递,并没有创建一个新的对象,当对引用类型的内容修改会影响被拷贝的对象。
- 实现方式:Java中的
clone()
方法是一个浅拷贝,其拷贝出的对象实例内部引用类型成员依然在引用传递。 - 代码示例:
class Person implements Cloneable {
private int age;// 整形
private String name;// 引用类型
public Object clone() {
try {
// 仅仅拷贝了Person对象实例,内部的String类型成员依旧是之前的引用,未重新创建String成员实例
return (Person) super.clone();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
- 深拷贝:在拷贝一个对象时,除了对基本数据类型的成员变量进行拷贝,对引用类型的成员变量进行拷贝时,创建一个新的对象实例来保存引用类型的成员变量。
- 实现方式1:序列化该对象,然后反序列化回来,就能得到一个新的对象了。
- 实现方式2:继续利用
clone()
方法,对该对象的引用类型成员再实现一次clone()
方法:
class Person implements Cloneable {
private int age;// 整形
private String name;// 引用类型
public Object clone() {
Person p = null;
try {
// 先拷贝了Person对象实例
p = (Person) super.clone();
// 再拷贝内部的String引用类型成员
p.name = (String) name.clone();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
2.1.7 零拷贝
见并发编程篇章
- 定义:指应用程序需要把内核中的一块区域数据转移到另一块内核区域时,不需要经过【复制到用户空间,再转移到目标内核区域去】了,而是直接在内核中实现转移
- 场景示例:现在有一个应用程序(例如Netty),需要把一个文件写到Socket中去
- 经过用户空间转移:
- 从磁盘读取文件数据到内核缓冲区
- 把内核缓冲区的数据读到应用缓冲区(位于用户空间)
- 从应用缓冲区读取数据到内核的Socket缓冲区
- 最后将Socket缓冲区的数据写到网络缓冲区(NIC buffer)中去
- 零拷贝:
- 从磁盘读取文件数据到内核缓冲区
- 应用程序通过操作系统的api(例如
transferTo()
方法),通知操作系统将内核中内核缓冲区(例如JVM内存模型中提到的DirectBuffer直接内存)数据直接转移到Socket缓冲区 - 最后将Socket缓冲区的数据写到网络缓冲区(NIC buffer)中去
2.1.8 其他内容
- 创建Java对象用什么关键字?
new关键字,new创建对象实例。
- 对象引用和对象实例的区别:
1)对象实例在堆内存中,对象引用存放在栈内存中。
2)一个对象引用可以指向0个或1个对象实例,一个对象实例可以有n个引用指向它。
- 创建对象的4种方式:
- new创建新对象
- 通过反射机制:详见反射章节
- 采用clone机制
- 通过序列化机制
- 对象的上转型对象(父类引用指向子类对象实例):
- 不能访问子类相比父类新增出的成员
- 不能访问子类相比父类新增出的方法【重载方法包含在内】
- 能被强制转换为子类对象(下转型对象)
- 对象的相等与指向他们的引用相等,两者有什么不同?:
- 对象的相等:比的是内存中存放的内容是否相等。
- 引用相等:比的是他们指向的内存地址是否相等。
通常不特别指明的话,提到的对象就是指对象实例,而不需要考虑对象引用。
2.2 继承
- 使用继承的目的:减少代码量,扩展方便
- 语法:
class A extends B{}
// A继承了B
- 特性:
- 单根性:一个类只能有一个直接父类(
C++
允许多继承) - 传递性:B继承A,C继承B,那么C继承A
Object类是所有类的直接或间接父类【跟接口没有关系】
- 子类不能继承父类哪些成员?
private
修饰的成员- 父类使用默认访问权限的,子类和父类不同包
- 构造方法不能被继承
- 继承关系下的程序执行顺序:
1)父类静态代码块
2)子类静态代码块
3)父类属性初始化(没有显式初始化就系统默认初始化)
4)父类构造方法调用(子类没有在构造方法第一行显示用super()
调用父类的构造方法,那么系统默认添加super()
去调用父类的缺省构造函数)
5)子类属性初始化(没有显式初始化就系统默认初始化)
6)子类构造方法调用(子类构造的部分)
- 先继承后实现:子类所继承的父类和实现的接口存在同样的方法声明时,该类无需重写该接口方法:
- 示例1:
public class ExtendAndImplementTest {
public static void main(String[] args) {
Child child = new Child();
child.sayHello();
child.test();
}
static class Parent {
public void test() {
System.out.println("这是父类的test方法");
}
}
interface Inter {
void sayHello();
void test();
}
static class Child extends Parent implements Inter{
// 仅仅需要实现Inter接口的sayHello()方法,因为先继承后实现,test()方法可以从
@Override
public void sayHello() {
System.out.println("hello");
}
}
}
运行结果
hello
这是父类的test方法
- 示例2:最佳应用Comparator比较器接口
import java.util.*;
public class ComparatorTest {
public static void main(String[] args) {
int res = Collections.binarySearch(Arrays.asList(1, 2, 3, 4), 2,
// 写法1:按照参数要求写匿名内部类,可见匿名内部类只要实现compare方法即可
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// 1.1 值为null时的比较,当操作数不是封箱类而是普通类时相对有用
/*return Comparator.nullsFirst(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return Integer.compare((Integer)o1, (Integer)o2);
}
}).compare(o1, o2);*/
// 1.2 因为整形,不考虑null值,属于整形的通用比较
return Integer.compare(o1, o2);
}
}
// 写法2:最容易想到的lambda表达式版本
// (x,y)-> {return x.compareTo(y);}
// 写法3:写法2的改良版本,更简洁,编译后同写法2
// (x, y) -> x.compareTo(y)
// 写法4:写法3的优化版本,因为写法符合实例方法引用的语法糖,因此改成方法引用形式
// Integer::compareTo
// 写法5:使用Integer的公共静态方法compare
// (x, y) -> Integer.compare(x, y)
// 写法6:写法5符合静态方法引用的语法糖
// Integer::compare
// 写法7:写法5是Comparator.comparingInt方法的简化版本,自然可以用更全面的版本了
// Comparator.comparingInt(o -> o)
// 写法8:运用枚举类的单例特性,Comparator自身提供了 供两个实现Comparable接口的操作数进行比较 的静态方法naturalOrder
// Comparator.naturalOrder()
);
System.out.println(res);
}
}
2.3 多态
- 定义:指同一个事物的不同形态。
- 发生条件:
- 必须在继承或实现关系下
- 必须要有方法重写或方法实现
- 形式为父类引用指向子类对象实例【对象的上转型对象】(里氏代换原则):
父类类型 对象1 = new 子类类型();
具体应用:
1)将父类作为方法的参数,实际使用的是子类的对象
2)将父类作为方法的返回值
3.抽象与接口
- 抽象类和普通类的对比:
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
- 抽象类能使用
final
修饰吗:
不能,定义抽象类就是让其他类继承的,如果定义为
final
该类就不能被继承, 这样彼此就会产生矛盾,所以final
不能修饰抽象类。
- 抽象类和接口的对比:
- 抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
- 从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为的抽象,是一种行为的规范。
- 相同点:
1)接口和抽象类都不能实例化
2)都位于树的顶端,用于被其他类实现或继承
3)都包含抽象方法,其非抽象子类都必须实现这些抽象方法
- 不同点:
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者protected |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final的 |
Java 8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间 的差异.
- 接口和抽象类的选择原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
- 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能
关于接口、抽象类设计的个人见解 :
1)从个人的产品研发经历来看,接口就是做行为规范,规定其树下子类必须要做的内容。比方说一个动物接口,规定实现的子类无论狗还是猫都要会叫,一定要会跑,但是具体怎么叫、怎么跑,能不能边跑边叫以及长啥样、是啥种类却不会去约束。
2)产品研发阶段,应明确禁止接口内部的默认方法提供,这会破坏接口的职责,即仅定义行为规范而不定义逻辑规范。仅当后续版本接口要升级时才可对接口内添加默认方法。
2)而抽象类,则是行为逻辑以及对象属性(状态)的规范,它通常要先实现接口,在此基础上再对接口的行为做进一步的深化要求,显示出接口方法的联系性,是对接口的补充。
3)同时,抽象类作为类是可以描述类特征的,因此不同的实现接口的类要有其特异性所在。
4)通用设计思想:①针对产品中一类cv代码(可能是接口、可能是批量等),提取出其中抛开强业务属性的行为模式。拿我们这边金融产品的交易接口来说,不考虑具体的业务属性,所有的接口其实都是 接收请求(校验保温)→处理请求→响应请求这三个步骤,这是一个最基本的行为规范,因此可以将其定义为接口,同时每个方法间光从接口上看不出彼此关系。②然后继续考虑接口层面,联系上业务属性,此处是要做交易接口,首先肯定要继承通用处理接口,然后再抽象出交易的处理流程:创建检查交易→创建交易→加载交易→执行交易,自然就有了创建检查交易抽象方法、创建交易方法、执行交易方法,同样从接口上看不出方法间关系。③以上是接口方面的设计,接下来就到了抽象类层次,对应最上层的通用接口,可以定义抽象类来组合接口的几个方法,来规定具体一个接口的实现所需具备的行为逻辑规范,然后再对非业务属性相关的方法做一些实现,对于定死不想让人重写的就用final声明(final方法其实很好的反应了当前类及其子类的特异性)。④业务相关的抽象类同理,实现交易接口的同时也要继承通用抽象类,如此在属性和行为上都有了约束。⑤后续集体的交易处理接口实现类,直接继承之前的业务相关抽象类即可。
4.内部类
4.1 介绍:
- 定义:定义在类内部的类叫做内部类。
- 分类:
- 静态内部类
- 成员内部类
- 局部内部类
- 匿名内部类
- 优点:
- 一个内部类对象可以访问创建它的外部类对象的内容(包括私有数据);
- 内部类不为同一包的其他类所见,具有很好的封装性;
- 内部类有效实现了“多重继承”,优化 java 单继承的缺陷;
- 匿名内部类可以很方便的定义回调。
- 应用场景:
- 一些多算法场合
- 解决一些非面向对象的语句块。
- 适当使用内部类,使得代码更加灵活和富有扩展性。
- 当某个类除了它的外部类,不再被其他的类使用时。
4.2 静态内部类
- 定义:定义在类内部的静态类
- 示例:
public class Out {
private static int a;
private int b;
public static class Inner {
public void print() {
System.out.println(a);
}
}
}
- 特性:静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量。
- 创建方式:
new 外部类.静态内部类();
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
4.3 成员内部类
- 定义:定义在类内部,成员位置上的非静态类
- 示例:
public class Outer {
private static int radius = 1;
private int count =2;
class Inner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}
- 特性:成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。
- 创建方式:成员内部类依赖于外部类的实例:
外部类实例.new 内部类();
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
4.3 局部内部类
- 定义:定义在方法中的内部类
- 示例:
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}
- 特性:
- 定义在实例方法中的局部类可以访问外部类的所有变量和方法;
- 定义在静态方法 中的局部类只能访问外部类的静态变量和方法。
- 创建方式:在对应方法内,
new 内部类();
public static void testStaticFunctionClass(){
class Inner {}
Inner inner = new Inner();
}
4.4 匿名内部类
- 定义:没有名字的内部类,其本质是创建一个new后类/接口的子类的实例。
- 示例1:
public class Outer {
private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
JDK1.8之前,匿名内部类必须继承一个已有抽象类或实现一个已有接口:
interface Service{
void method();
}
- 特性:
- 匿名内部类必须继承一个抽象类或者实现一个接口。
- JDK1.8开始,匿名内部类内部可以重写非抽象类、非接口的方法,视作该类的子类。
- 匿名内部类不能定义任何静态成员和静态方法。
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为
final
。 - 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
- 创建方式:
new 类/接口{
// 匿名内部类实现部分
}
- 示例2:以泛型匿名内部类为例
import java.util.ArrayList;
import java.util.List;
public class NewGenericsTest {
public static void main(String[] args) {
// JDK1.9支持无需声明具体类型的泛型内部匿名类(可自行判断):ArrayList无String也没事
// List<String> strList = new ArrayList<>() {
// JDK1.8仅支持声明具体类型的泛型匿名内部类:ArrayList必须要有String
List<String> strList = new ArrayList<String>() {
@Override
public boolean add(String e) {
System.out.println("重写父类方法,我读到你了:" + e);
return true;
}
};
strList.add("gnm");
}
}
运行结果:
重写父类方法,我读到你了:gnm
4.5 相关问题
- 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上
final
?
public class Outer {
void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}
缘由:
1)是因为方法内局部变量和局部内部类(匿名内部类)的生命周期不一致。
2)局部变量直接存储在栈中,当方法执行结束后,非final
的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。
3)给局部变量加了final
,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。
另一个回答:
1)局部内部类/匿名内部类和外部类处于同一级别,但局部内部类/匿名内部类不会因为定义在方法中就随着方法的执行完毕而销毁(仅当没有人引用局部内部类/匿名内部类时才会死亡)
2)局部变量直接存储在栈中,当外部方法执行结束后,非final
的局部变量就被销毁。而局部内部类/匿名内部类对局部变量的引用依然存在,如果局部内部类/匿名内部类要访问非final
的局部变量时,就会出错。
3)因此JVM会将final
修饰的方法的局部变量复制一份作为局部内部类/匿名内部类的成员变量(也就是说访问的是局部变量的浅拷贝),从而不会受外部方法执行结束导致的局部变量死亡的影响
4)同时final
关键字也能保证局部内部类/匿名内部类成员变量的值和局部变量保持一致。
五、Java常用API
1.Java自动装箱与拆箱
1.1 装箱
- 定义:是自动将基本数据类型转换为包装器类型(
int→Integer
) - 调用方法:
Integer
的valueOf(int)
方法 - 发生时机:调用方法并进行传参时
1.2 拆箱
- 定义:是自动将包装器类型转换为基本数据类型(
Integer→int
) - 调用方法:
Integer
的intValue
方法
1.3 包装器类特性
- 整形
int
对应包装器类Integer
,在用valueOf(int)
方法创建对象时,如果数值在[-128,127]
之间,便返回指向IntegerCache.cache
中已经存在的对象的引用;否则创建一个新的Integer
对象:
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2);// true,因为100在范围内,且i1时已经创建,i2返回即可
System.out.println(i3==i4);// false
Integer类valueOf(int)方法源码:
public static Integer valueOf(int i) {
if(i >= -128 && i <= IntegerCache.high)
return IntegerCache.cache[i + 128];
else
return new Integer(i);
}
其中IntegerCache类的实现为:
private static class IntegerCache {
static final int high;
static final Integer cache[];
static {
final int low = -128;
// high value may be configured by property
int h = 127;
if (integerCacheHighPropValue != null) {
// Use Long.decode here to avoid invoking methods that
// require Integer's autoboxing cache to be initialized
int i = Long.decode(integerCacheHighPropValue).intValue();
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - -low);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
- 在某个范围内的浮点数个数是无限的,因此不会有常量池供浮点数类型对应包装器使用:
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);// false
System.out.println(i3==i4);// false
2.BigDecimal
2.1 BigDecimal由来
- 定义:BigDecimal是Java在
java.math
包中提供的API类,用来对超过16位有效位的数进行精确的运算。 - 产生背景:计算机底层无法精确表示浮点类型数,且常用的
float
和double
类型的计算采用的是二进制浮点运算,是为了在广域数值范围上提供较为精确的快速近似计算而设计的,只适用于科学计算和工程计算,并不适用于计算精确的数值。
【注意】以下内容扩展了解即可
2.1 浮点数存在精度偏差示例:此处由于Java版本问题,虚拟机对浮点数类型的运算做了精度处理,只能使用
BigDecimal(double)
构造方法暴露其中的double
型数据实际情况。
public static void main(String[] args) {
double cc = 1000 * 0.06 / 365;
BigDecimal cc_1 = new BigDecimal(cc);
System.out.println("double的加法运算:" + 1.2 + 0.03);
System.out.println("BigDecimal(double):" + new BigDecimal(1.2 + 0.03).toString());
System.out.println("BigDecimal(String):" + new BigDecimal(Double.toString(1.2 + 0.03)));
System.out.println("double的乘法运算:" + cc * 365);
System.out.println("BigDecimal的乘法运算:" + cc_1.multiply(new BigDecimal(365)));
}
- 计算结果:
double的加法运算:1.20.03
BigDecimal(double):1.229999999999999982236431605997495353221893310546875
BigDecimal(String):1.23
double的乘法运算:60.0
BigDecimal的乘法运算:59.9999999999999966693309261245303787291049957275390625
2.2 为什么使用浮点类型会出现精度偏差?
1)计算机底层所有数据的表现形式都为二进制形式,而浮点类型又由整数和尾数两部分组成。
2)整数部分:一般情况下,二进制转为十进制所使用的方法是按权相加法,十进制转二进制是除2取余的逆序排列法:以二级制10010为例
- 二进制到十进制:
10010: 0 * 20 + 1 * 21 + 0 * 22 + 0 * 23 + 1 * 24 = 18- 十进制到二进制:
18 / 2 =9。 余0
9 / 2 = 4。 余1
4 / 2 = 2。 余0
2 / 2 = 1。 余0
1 / 2 = 0。 余1
从而得到结果100103)尾数(小数)部分:二进制小数到十进制小数还是按权相加法,十进制转二进制是乘2取整的顺序排列法:
3.1)这边先以二机制0.01为例
- 二进制到十进制:以0.01为例
0.01 = 1 * 2-2 + 0 * 2-1 + 0 * 20 = 0.25- 十进制到二进制:
0.25 * 2 = 0.5 … 0
0.5 * 2 = 1 … 1
结果等于0.013.2)但是如果是十进制0.1这种小数位乘2一直有尾数的呢?
- 十进制到二进制:
0.1 * 2 = 0.2 … 0
0.2 * 2 = 0.4 … 0
0.4 * 2 = 0.8 … 0
0.8 * 2 = 1.6 … 1
0.6 * 2 = 1.2 … 1
0.2 * 2 = 0.4 … 0
0.4 * 2 = 0.8 … 0
…这就0.0001100…无限循环了,而计算机在存储小数时是有长度限制的,所以会截取部分小数进行存储,从而导致计算机存储的数值只能是个大概的值,而不是精确的值。→计算机根本无法使用二进制来精确表示诸如2.1这样的小数,自然计算也会出问题。
2.2 BigDecimal的常见构造方法
BigDecimal(int)
:创建一个具有参数所指定整数值的对象。BigDecimal(double)
:创建一个具有参数所指定双精度值的对象(不建议使用)。
2.1 不建议使用
BigDecimal(double)
而是推荐用BigDecimal(String)
进行构造的原因:
1)BigDecimal(double)
的结果具有一定的不可预知性:大部分浮点数无法准确地表示为double,所以传入构造方法的值也就出现了误差;
2)BigDecimal(double)
作为运算类方法的参数时,有可能产生BigDecimal(double)
结果,使用System.out.println
进行输出时必定是无穷小数!具体产生时机见下文
3)而BigDecimal(String)
是完全可以预知到的,参数给的是什么,结果就是什么。
2.2 非要使用double作BigDecimal的源的方法:
可以使用BigDecimal.valueOf(double)
的方法来进行,因为该方法的底层,也是将double值转换为String类型,再进行运算的:
public static BigDecimal valueOf(double val) {
return new BigDecimal(Double.toString(val));
}
BigDecimal(String)
:创建一个具有参数所指定以字符串表示的数值的对象 。BigDecimal(long)
:创建一个具有参数所指定的长整数值的对象。
2.3 BigDecimal的常用方法
- 运算类:
add(BigDecimal)
:BigDecimal
对象中的值相加,返回BigDecimal
对象。subtract(BigDecimal)
:BigDecimal
对象中的值相减,返回BigDecimal
对象multiply(BigDecimal)
:BigDecimal
对象中的值相乘,返回BigDecimal
对象divide(BigDecimal)
:BigDecimal
对象中的值相除,返回BigDecimal
对象
1.1 关于
divide(BigDecimal)
方法的注意事项:
1)使用divide(BigDecimal)
当不能整除的时候就会报java.lang.ArithmeticException
异常:因为内部默认使用了ROUND_UNNECESSARY
的舍入模式,而该舍入模式的前提就是请求的操作具有精确的结果,否则报出java.lang.ArithmeticException
2)解决办法:换成使用divide(BigDecimal divisor, int scale, int roundingMode)
divisor
:表示除数;scale
:表示小数点后保留位数;roundingMode
:表示舍入模式;
1.2 关于运算类方法的结果:
两个数的加法、减法、乘法以及可以整除的情况下:
1)运算方法的参数的value如果是无法不丢精度的转化为整形的BigDecimal(double)
,则无论调用者是什么BigDecimal,其结果必定是BigDecimal(double)
,且System.out.println()
为无穷小数。
2)运算方法的参数如果是BigDecimal(int)
或BigDecimal(String)
或是value可以不丢精度的转化成整形的BigDecimal(double)
,则看调用者的情况:
- 调用者是
BigDecimal(int)
或BigDecimal(String)
或是value可以不丢精度的转化成整形的BigDecimal(double)
,则输出结果必定是BigDecimal(int)
或BigDecimal(String)
或BigDecimal(double)
,且System.out.println()
为整数或有限小数- 其他调用者情况,输出结果必定是
BigDecimal(double)
,且System.out.println()
为无穷小数
- 转化类:
abs()
:将BigDecimal
对象中的值转换成绝对值doubleValue()
:将BigDecimal
对象中的值转换成双精度数floatValue()
:将BigDecimal
对象中的值转换成单精度数longValue()
:将BigDecimal
对象中的值转换成长整数intValue()
:将BigDecimal
对象中的值转换成整数toString()
:将BigDecimal
对象中的值转换成字符串
- 比较类:
int compareTo(BigDecimal)
:将调用方和指定的BigDecimal
进行比较,前者比后者小返回-1,相等返回0,大于返回1.
1)两个
BigDecimal
对象的价值相等但具有不同的比例(如2.0和2.00)被认为是相等。
2)该方法优先于六个布尔比较运算符(<
,==
,>
,>=
,!=
,<=
)。
3)执行比较的语句建议是:x.compareTo(y) <op> 0
,其中<op>
是六个比较运算符之一。
- 格式化类:
setScale(int scale)
:用于格式化小数点;setScale(1)
表示保留一位小数,默认用四舍五入(ROUND_HALF_UP
)舍入模式;setScale(int scale, int roundingMode)
:用于格式化小数点;roundingMode
表示指定的舍入模式;
2.4 BigDecimal的八种舍入模式
ROUND_UP
:向远离0的方向舍入,始终对非零舍弃部位前面的数字+1,该方式就是只增不减。ROUND_DOWN
:向0方向舍入,在丢弃某部分之前,始终不增加数据(即截断),该方式是只减不加。ROUND_CEILING
:向正无穷方向舍入,如果数值为正,舍入方式与ROUND_UP
一致,如果为负,舍入方式与ROUND_DOWN
一致,该模式始终不会减少计算数值。ROUND_FLOOR
:向负无穷方向舍入,如果数值为正,舍入行为与ROUND_DOWN
相同;如果为负,则舍入行为与ROUND_UP
相同。该模式始终不会增加计算数值。ROUND_HALF_UP
:向“最接近的”数字舍入,也就是四舍五入。ROUND_HALF_DOWN
:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式,也就是五舍六入。ROUND_HALF_EVEN
:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与ROUND_HALF_UP
相同;如果为偶数,则舍入行为与ROUND_HALF_DOWN
相同。此模式也被称为“银行家舍入法”,主要在美国使用。例如:1.15->1.2
,1.25->1.2
。ROUND_UNNECESSARY
:计算结果是精确的,不需要舍入模式。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException
。
3.Object类相关
3.1 常见方法
3.1.1 equals()方法:
- 源码:
public boolean equals(Object obj) {
return (this == obj);
}
- 两种使用场景:
- 类没有覆盖
equals()
方法:则通过equals()
比较该类的两个对象时,等价于通过==
比较这两个对象→使用的是默认equals()
方法。 - 类重写了
equals()
方法。一般都是覆盖equals()
方法来比较两个对象的内容是否相等。若相等则返回true
。 - 示例:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
3.1.2 hashCode()方法:
- 作用:获取哈希码,也称为散列码。这个哈希码的作用是确定该对象在堆(是一个哈希表(散列表))中的索引位置(方法返回值是一个
int
整数)
散列表:存储的是键值对(
key-value
),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!
- 存在的意义:
- 在重复校验过程中可以大大减少
equals()
的调用次数,提高执行速度。
2.1 以HashSet添加元素的过程为例:
1)当你把对象加入 HashSet 时,HashSet 会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashcode
值作比较。
2)如果没有相符的hashcode
,HashSet会假设对象没有重复出现;但是如果发现有相同hashcode
值的对象,这时会调用元素的equals()
方法来检查hashcode
相等的对象是否真的相同。
如果两者相同,HashSet 就不会让其加入操作成功;如果不同的话,就会重新散列到其他位置。
这样我们就大大减少了equals()
的调用次数,提高了执行速度。
hashCode()
与equals()
的相关规定
- 如果两个对象相等,则
hashcode
一定也是相同的。 - 两个对象相等,对两个对象分别调用
equals()
应该都返回true。
两个对象有相同的
hashcode
值,它们也不一定是相等的!
equals()
被覆盖过,则hashCode()
也必须被覆盖。
hashCode()
的默认行为是对堆上的对象产生独特值。如果没有重写hashCode()
,则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
- 有没有可能两个不相等的对象有相同的hashcode?如果有该如何解决?
- 有可能.在产生
hash
冲突(hash
碰撞)时,两个不相等的对象就会有相同的hashcode
值。 - 出现
hash
冲突后解决方案:
1)拉链法:又叫开散列法、链地址法、开链法,每个哈希表节点都有一个
next
指针,多个哈希表节点可以用next
指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储。→哈希桶是该方法的经典实现。
2)开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
3)再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个….等哈希函数计算地址,直到无冲突。
3.2 其他方法
getClass()
:native方法,用于返回当前运行时对象的Class
对象,使用了final
关键字修饰,故不允许子类重写。clone()
:naitive方法,用于创建并返回当前对象的一份拷贝。
- 一般情况下,对于任何对象
x
,表达式x.clone() != x
为true,x.clone().getClass() == x.getClass()
为true。 - Object类本身没有实现
Cloneable
接口,所以不重写clone()
并且进行调用的话会发生CloneNotSupportedException
异常。
toString()
:返回类的名字@实例
的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。notify()
: native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。
- 如果有多个线程在等待只会任意唤醒一个。
notifyAll()
:native方法,并且不能重写。跟notify()
一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。wait(long timeout)
:native方法,并且不能重写。暂停线程的执行。timeout
是等待时间。
- 抛出异常:
InterruptedException
wait(long timeout, int nanos)
:多了nanos
参数,这个参数表示额外时间(以毫微秒为单位,范围是0-999999
)。 所以超时的时间还需要加上nanos
毫秒。wait()
:跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念。
注意:线程Thread类
sleep()
方法没有释放锁,而Object类的wait()
、wait(long timeout)
、wait(long timeout, int nanos)
方法释放了锁。
finalize()
:实例被垃圾回收器回收的时候触发的操作。
- 抛出异常:
Throwable
4.String相关
4.1 字符串常量
4.1.1 字符型常量和字符串常量的区别
参数 | 字符常量 | 字符串常量 |
---|---|---|
形式 | 单引号引起的一个字符 | 双引号引起的若干个字符 |
含义 | 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 | 字符串常量代表一个地址值(该字符串在内存中存放位置) |
占内存大小 | 1个字节 | 若干个字节(至少一个字符结束标志) |
4.1.2 在常量池中创建字符串常量
- 创建时机:加载->连接->初始化的类加载阶段
- 创建前提:该字符串常量可以在编译阶段即确定下来(例如小于等于127、大于等于-128的整数所在的静态常量池)。
- 例如:
String a = "1";
、String b = new String("2");
、String c = "3" + "4";
、String d = "5" + new String("6")
;,其中在编译阶段就能确定常量"1"
、"2"
、"34"
、"5"
、"6"
。
2.1 为什么
String c = "3" + "4";
确定的常量是"34"
,而不是"3"
和"4"
?
1)String c = "3" + "4";
在编译阶段就会被编译器优化成String c = "34" ;
,所以不会在常量池中单独创建常量"3"
和常量"4"
- 创建过程:类加载阶段,JVM会首先检查字符串常量池以及其他运行时常量池,如果该字符串(String对象实例)已经存在池中,则返回它的引用;如果不存在,则实例化一个字符串放到池中,并返回其引用。
详细请参考:4.2.4 查找/添加字符串常量逻辑
- 可配合使用的字节码指令:
lcd
- 可配合使用的String类方法:
intern()
4.2 字符串常量池
4.2.1 介绍
字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串。
4.2.2 在HotSpot JVM中的位置变迁
- JDK1.7 之前:位于永久代中
- JDK1.7 之后:位于堆内存中
- 变迁的原因:
- JDK1.7之前方法区(永久代)的大小通常人为指定且受到堆内存大小限制,且方法区进行gc的频率偏低,将字符串常量池放在永久代中很容易出现
java.lang.OutOfMemoryError: PermGen
永久代内存溢出异常。 - 堆空间更大,可以提升字符串常量池的操作性能。
- JRockit JVM没有永久代,为了促进HotSpot JVM和JRockit JVM的融合,有必要移除永久代,作为内容之一的字符串常量池自然要迁出。
4.2.3 底层结构
C++
源码:JVM是用C++写的,底层实现:StringTable的basic_add
方法
oop StringTable:basic_add(int index_arg, Handle string, jchar* name, int len, unsigned int hashValue_arg, TRAPS)
{
NoSafepointVerifier nsv;
unsigned int hashValue;
int index;
if (use_alternate_hashcode())
{
// 重新计算hash值,根据string字符串内容和长度,算出hash值
hashValue = alt_hash_string(name, len);
// 根据hash值算出数组下标位置
index = hash_to_index(hashValue);
}
else
{
hashValue = hashValue_arg;
index = index_arg;
}
// 通过index在数组中查询相应的字符串对应的oop,找到即返回
oop test = lookup_in_main_table(index, name, len, hashValue);
if (test != NULL)
{
return test;
}
// 如果没找到,创建一个HashtableEntry对象,key是hashValue,也就是通过String的内容和长度生成hash值
// value是由string()生成的,也就是instanceOopDesc (简称oop)
HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
add_entry(index, entry);
return string();
}
- 字符串常量池的底层是一个由StringTable管理的一片内存空间。
1.1 一些说明:不特别指明的话
1)常量池指的就是StringTable以及StringTable管理的一片String对象实例。
2)返回常量池中的引用指的就是StringTable注册的String对象实例的地址(即对应HashEntry的value)
- StringTable是字符串常量注册表,其数据结构为HashTable(散列表)。
- StringTable由一个个Hash桶组成,Hash桶值指向一个单向链表,该链表中线性存储了一个个HashTableEntry(以下简称HashEntry)。HashEntry存在key和value。key是散列码,value是instanceOopDesc(也就是oop)的引用
1.2 图示:
- StringTable默认大小:
1)JDK1.7之前:桶个数是默认值1009个,且无法修改。
2)JDK1.7之后:桶个数默认60013个,可通过-XX:StringTableSize:60013
参数进行修改。
- 为什么用哈希桶而不是直接存储HashEntry?
既然是散列表,那就可能出现hash碰撞问题,使用哈希桶可以通过开链法有效避免hash碰撞问题,更多参考Object类相关→hashCode()方法部分的说明
4.2.4 查找/添加字符串常量底层逻辑:
- 根据字符串常量的内容和长度计算字符串常量的散列码(
hashValue
) - 根据散列码确定HashEntry在散列表中的位置。
- 如果找到对应HashEntry,则取出其值(字符串对象实例的引用)并返回
- 如果未找到对应HashEntry,则创建一个HashEntry对象,键是散列码,并使用StringTable的
string()
方法实例化一个字符串对象实例,将其地址作为HashEntry对象的值。最后返回HashEntry对象的值。
4.3 String对象内存分配
4.3.1 直接赋值实例化
- 形式:
String a = "1";
- 样例代码:
public static void main(String[] args) {
String a = "1";
}
- 字节码指令:
0 ldc #2 <1>
2 astore_1
3 return
3.1 解析:
1)由【4.1.2 在常量池中创建字符串常量】小节可知,在上述代码尚未执行前的类加载阶段,已经在字符串常量池中创建了字符串常量"1"
。
2)第一行指令不是new
,说明没有在堆中创建String对象实例
3)ldc
指令的操作步骤之前提到过,返回常量池中指定字符串常量对应的String对象实例的引用,并压入操作数栈的栈顶。
3)astore
指令弹出操作数栈的栈顶的值给到局部变量表index为1的局部变量。main方法的args参数的index为0,String变量a的index为1,因此最终将字符串常量的引用返回给a
- 内存示意图:
- 相关问题:
- 一共创建了几个字符串对象?
1个String对象实例
- 一共创建了几个对象?
2个,一个String对象实例,还有一个字符数组对象实例
4.3.2 构造方法实例化
- 形式:
String b = new String("1");
- 样例代码:
public static void main(String[] args) {
String b = new String("1");
System.out.println(b); // 不加个引用b的语句,String b = new String("1") 就得在编译阶段被优化成 new String("1")了
}
- 字节码指令:只看
String b = new String("1");
这句
0 new #2 <java/lang/String>
3 dup
4 ldc #3 <1>
6 invokespecial #4 <java/lang/String.<init>>
9 astore_1
...
return
3.1 解析:
1)类加载阶段,创建字符串常量"1"
并放入字符串常量池。
2)执行阶段,new
指令在堆内分配内存,创建一个String对象实例并返回引用,压入操作数栈的栈顶。
3)dup
指令复制操作数栈的栈顶值,并压入操作数栈的栈顶
4)ldc
指令返回字符串常量池中的指定常量的引用,并压入操作数栈的栈顶。
5)invokespecial
指令依次从操作数栈栈顶弹出初始化所需的参数以及对象引用,并调用对象引用所指oop的指定参数构造方法。
6)astore
指令弹出操作数栈栈顶的引用,赋值给index为1的变量(b
)。
- 内存示意图:
- 相关问题:
- 一共创建了几个字符串对象?
1个String对象实例
- 一共创建了几个对象?
2个,一个String对象实例,还有一个字符数组对象实例
4.3.3 字符串常量拼接实例化
- 形式:
String c = "3" + "4";
- 样例代码:
public static void main(String[] args) {
String c = "3" + "4";
}
前文提到,
String c = "3" + "4";
在编译阶段会被优化成String c = "34";
,所以内存分配情况同【直接赋值实例化】
4.3.4 任意拼接实例化
- 形式:
- 字符串常量和String对象拼接:
String d = "5" + new String("6");
- String对象和String对象拼接:
String d = new String("5") + new String("6");
- 上述情况的任意组合
只要是非全常量,就要用到StringBuilder的
append()
方法进行拼接,最后toString()
输出拼接好的字符串。
- 样例代码:
public static void main(String[] args) {
String d = "5" + new String("6");
}
- 字节码指令:
0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 ldc #4 <5>
9 invokevirtual #5 <java/lang/StringBuilder.append>
12 new #6 <java/lang/String>
15 dup
16 ldc #7 <6>
18 invokespecial #8 <java/lang/String.<init>>
21 invokevirtual #5 <java/lang/StringBuilder.append>
24 invokevirtual #9 <java/lang/StringBuilder.toString>
27 astore_1
28 return
3.1 解析:
1)类加载阶段,创建字符串常量"5"
和字符串常量"6"
,并放入字符串常量池。
2)执行阶段,因为涉及到非全字符串常量的拼接,需要依赖StringBuilder类的append(String)
方法,所以,第一行先执行new
指令在堆内分配内存,创建一个StringBuilder对象实例并返回其引用,压入操作数栈的栈顶。
3)dup
指令复制操作数栈的栈顶值,并压入操作数栈的栈顶。
4)invokespecial
指令依次弹出操作数栈栈顶的参数(无参数)和对象引用(dup
指令复制的),并调用引用所指的StringBuilder对象实例。
5)ldc
指令返回字符串常量池中的指定常量("5"
)的引用,并压入操作数栈的栈顶。
6)invokevirtual
指令依次弹出操作数栈栈顶的参数(ldc
指令压入的字符串常量"5"
)和对象引用(一开始new
指令压入的StringBuilder对象引用),并调用引用所指的StringBuilder对象实例的append(String)
方法,拼接字符串常量"5"
到StringBuilder对象实例中。最后将append(String)
方法返回值(StringBuilder对象自身引用)压入操作数栈的栈顶。
7)new
指令在堆内分配内存,创建一个String对象实例并返回引用,压入操作数栈的栈顶。
8)dup
指令复制操作数栈的栈顶值,并压入操作数栈的栈顶。
9)ldc
指令返回字符串常量池中的指定常量("6"
)的引用,并压入操作数栈的栈顶。
10)invokespecial
指令依次从操作数栈栈顶弹出初始化所需的参数(常量"6"
的引用)以及对象引用(刚dup
指令压入操作数栈的String对象引用),并调用对象引用所指oop的初始化方法init()
。
11)invokevirtual
指令依次弹出操作数栈栈顶的参数(上次new
指令压入的String对象实例的引用)和对象引用(上次append(String)
进来的StringBuilder对象引用),并调用引用所指的StringBuilder对象实例的append(String)
方法,拼接字符串常量"6"
到StringBuilder对象实例中。最后将append(String)
方法返回值(StringBuilder对象自身引用)压入操作数栈的栈顶。
12)invokevirtual
指令依次弹出操作数栈栈顶的参数(无参)和对象引用(上次append(String)
进来的StringBuilder对象引用),并调用引用所指的StringBuilder对象实例的toString()
方法,在堆内存中开辟空间,创建一个String对象实例并完成初始化,最终将该实例的引用压入操作数栈的栈顶。
13)astore
指令弹出操作数栈栈顶的引用,赋值给index为1的变量(d
)。
- 内存示意图:
- 相关问题:
- 一共创建了几个字符串对象?
4个String对象实例,其中堆内存中有一个垃圾空间,可通过
System.gc()
手动回收。
- 一共创建了几个对象?
10个,4个String对象实例,5个字符数组对象实例,1个StringBuilder对象实例。
其中1个String对象实例,2个字符数组对象实例,1个StringBuilder对象实例是垃圾空间,可通过System.gc()
手动回收。
4.4 String对象特性和底层结构
4.4.1 特性
- 不变性:String是只读字符串,是一个典型的immutable(不可变)对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。
1.1 不变模式的主要作用:
在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
- 常量池优化:String对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
- 不可被继承:使用
final
来修饰String类,表示String类不能被继承,提高了系统的安全性。
4.4.2 底层结构
- String底层是
final
修饰的char
类型数组,用来存储字符:
/** The value is used for character storage. */
private final char value[];
- String真的是不可变的吗?
- String对象的内容一般是不可变的,但引用可以变:
// 假定代码执行前字符串常量池中空空如也
String str = "Hello";// 分配了2次内存,第一次:在字符串常量池中创建“Hello”字符串常量,第二次:在堆内存中创建一个String类型对象str
str = str + " World";// 分配了3次内存,第一次:在字符串常量池中创建“World”字符串常量,第二次:在字符串常量池中创建StringBuilder实例,第三次:StringBuilder对象的toString()方法构建了最终的String对象实例
System.out.println("str = " + str);// 结果:str = Hello World
1)原来String的内容是不变的,只是
str
的值由原来的"Hello"
对象的堆内存地址转为"Hello World"对象的堆内存地址。
2)期间发生5次内存分配,详细情况见代码示例中的注释。
- 通过反射可以修改所谓的“不可变”对象:
String s = "Hello World";
// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);
// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
// 改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); // Hello_World
4.4.3 如何将字符串反转?
- 使用StringBuilder的
reverse()
方法:
// StringBuilder reverse
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("abcdefg");
System.out.println(stringBuilder.reverse());// gfedcba
- 使用StringBuffer的
reverse()
方法:
// StringBuffer reverse
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("abcdefg");
System.out.println(stringBuffer.reverse());// gfedcba
4.4.4 String和StringBuffer、StringBuilder的区别
参数 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | String底层是final 修饰的char 类型数组,所以String对象是不可变的 | 父类AbstractStringBuilder类也使用字符数组保存字符串,但未用final 修饰,因此是可变的 | 同StringBuffer |
线程安全性 | 线程安全 | StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的 | StringBuilder并没有对方法进行加同步锁,所以是非线程安全的 |
性能 | 每次对String类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String对象,这会造成空间的浪费 | StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用,但性能比StringBuilder低 | StringBuilder也是对自身进行操作,相同情况下使用StirngBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,却要冒多线程不安全的风险 |
总结:操作少量数据优先用String,单线程操作大量数据优先使用StringBuilder,多线程操作大量数据优先使用StringBuffer。
4.5 String常见方法
4.5.1 equals()方法
- 功能:判断两个字符串对象内容是否相等。
- 示例:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
- 说明:
- String中的
equals()
方法是被重写过的,因为Object的equals()
方法是比较的对象的内存地址,而String的equals()
方法比较的是对象的字符串值。 - 当使用字符串常量直接赋值创建String对象时,JVM会在常量池的StringTable中查找有没有HashCode和待创建对象的字符串HashCode相匹配的HashEntry,如果有就把HashEntry对应的值赋给当前引用。如果没有就在常量池中重新创建一个 String对象实例并注册进StringTable,然后返回其引用。
4.5.2 intern()方法
- 功能:判断字符串常量池中是否有内容和调用对象一致的字符串常量(String对象实例)。如果没有,则将该String对象添加到字符串常量池中,并返回该String对象引用;如果有,则直接返回字符串常量池中指定常量的引用。
1.1 【添加到字符串常量池中】的含义:
是指先new
一个HashEntry实例,再将调用者(String对象)的内容对应hashCode
作为键,对应引用作为值初始化该HashEntry实例,然后将该实例放入StringTable指定索引的Hash桶中。
- 示例1:
public static void main(String[] args) {
String arr[] = new String[1000000000];
for (int i = 0; i < arr.length; i++) {
arr[i] = new String(new byte[1]);
// 分别单独执行上下两句,看内存占用情况
// arr[i] = new String(new byte[1]).intern();
}
}
- 使用
javac
工具查看后台内存占用情况:
- 执行完所有循环的
arr[i] = new String(new byte[1]);
时:
- 执行完所有循环的
arr[i] = new String(new byte[1]).intern();
时:
- 为什么执行
arr[i] = new String(new byte[1]).intern();
内存占用低了这么多?
- 因为
intern()
方法会将从第二次循环以后进来的数组元素引用,直接指向第一次循环时创建的String对象实例。
- 示例2:
String a = new String("Hell") + new String("o_World");
a.intern();
String b = "Hello_World";
System.out.println(a == b);// true
- 示例3:
String a = new String("Hell") + new String("o_World");
String b = "Hello_World";
a.intern();
System.out.println(a == b);// false
- 示例4:
String a = new String("Hell") + new String("o_World");
String b = "Hello_World";
a = a.intern();
System.out.println(a == b);// true
4.5.3 format()方法
自己参考百度网站整理:
https://blog.csdn.net/m0_57037182/article/details/124738711
https://www.cnblogs.com/JYB2021/p/14254890.html
https://www.cnblogs.com/zhongjunbo555/p/11383159.html
4.5.3 其他方法
lengh()
:返回字符串的长度。
注意数组用的是length,不带括号!
indexOf()
:返回指定字符的索引。charAt()
:返回指定索引处的字符。replace()
:字符串替换。
String提供的
replace()
方法仅能替换字符或匹配正则表达式的字符串,使用范围不够广,因此实际项目上通常会自己改写replace()
方法,以下提供一个小样以供参考:
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 字符串替换工具类
* @author majiayi5
* @date 2999-12-31
*/
public class ReplaceStringFunction {
public static void main(String[] args) {
String a = "我是{$dsb}!";
System.out.println(ReplaceStringFunction.replace(a, "{$dsb}", "大帅比"));
}
/**
* 替换字符串中指定字符串为指定字符串
* @param res 源字符串
* @param oldStr 老字符串
* @param newStr 新字符串
* @return 处理完的字符串
*/
public static String replace(String res, String oldStr, String newStr) {
if (res != null && res.length() < 256) {
for (int position = res.indexOf(oldStr, 0);
position != -1;
position = res.indexOf(oldStr, position + newStr.length())) {
res = res.substring(0, position) + newStr + res.substring(position + oldStr.length());
}
return res;
} else if (res == null) {
return null;
} else {
// 既然上面条件都不符合,只能走正则表达式那套了
Pattern pattern = Pattern.compile(dealEscapeCharacter(oldStr));
Matcher mm = pattern.matcher(res);
return mm.replaceAll(newStr);
}
}
/**
* 处理转义字符
* @param str 目标字符串
*/
public static String dealEscapeCharacter(String str) {
Hashtable hashtable = new Hashtable();
hashtable.put(new Character('\r'), new Character('r'));
hashtable.put(new Character('\n'), new Character('n'));
hashtable.put(new Character('\\'), new Character('\\'));
hashtable.put(new Character('{'), new Character('{'));
hashtable.put(new Character('['), new Character('['));
hashtable.put(new Character('$'), new Character('$'));
hashtable.put(new Character('('), new Character('('));
hashtable.put(new Character(')'), new Character(')'));
hashtable.put(new Character('+'), new Character('+'));
hashtable.put(new Character('*'), new Character('*'));
hashtable.put(new Character('^'), new Character('^'));
hashtable.put(new Character('|'), new Character('|'));
StringBuffer buffer = new StringBuffer();
char[] arr = str.toCharArray();
for(int i = 0; i < arr.length; ++i) {
Character character = new Character(arr[i]);
if (hashtable.containsKey(character)) {
buffer.append('\\');
buffer.append((Character) hashtable.get(character));
} else {
buffer.append(arr[i]);
}
}
return buffer.toString();
}
}
trim()
:去除字符串两端空白。split()
:分割字符串,返回一个分割后的字符串数组。getBytes()
:返回字符串的 byte 类型数组。toLowerCase()
:将字符串转成小写字母。toUpperCase()
:将字符串转成大写字符。substring()
:截取字符串。
4.6 其他问题
4.6.1 在使用HashMap的时候,用String做key有什么好处?
HashMap内部实现是通过key的
hashCode
来确定value的存储位置,因为字符串是不可变的,所以当创建字符串时,它的hashCode
被缓存下来,不需要再次计算,所以相比于其他对象更快。
5.Math类相关
5.1 Math类常用方法
Math.round(数值)
:数值+0.5,然后向负无穷取整。如果数值是float型则返回int,如果数值是double型则返回long。
Math.round(-5.6) = -6;
Math.round(-5.4) = -5;
Math.round(-5.5) = -5;
Math.round(5.6) = 6;
Math.round(5.5) = 6;
Math.round(5.4) = 5;
Math.ceil(数值)
:向正无穷取整,如果数值是double型则返回double。
Math.ceil(-5.5) = -5.0;
Math.ceil(-5.4) = -5.0;
Math.ceil(-5.6) = -5.0;
Math.ceil(5.5) = 6.0;
Math.ceil(5.6) = 6.0;
Math.ceil(5.4) = 6.0;
Math.floor(数值)
:向负无穷取整,如果数值是double型则返回double。
Math.ceil(-5.5) = - 6.0;
Math.ceil(-5.4) = - 6.0;
Math.ceil(-5.6) = - 6.0;
Math.ceil(5.5) = 5.0;
Math.ceil(5.6) = 5.0;
Math.ceil(5.4) = 5.0;
Math.random()
:返回[0,1)的小数
6.Files类相关
6.1 Files类常用方法
File f=new File("盘符"+File.separator+"路径");
:new
一个文件exists()
:检测文件路径是否存在。isDirectory()
:判断f是否是一个目录isFile()
:判断f是否是一个实体文件length()
:实体文件的话返回该文件的大小(字节数)【long
类型】getName()
:返回文件/路径的名字(含文件后缀)getAbsolutePath()
:返回文件的绝对路径getPath()
:返回文件的相对路径File[] fs=f.listFiles();
:获取当前目录f
下的一层文件(不管是目录还是实体文件),并放入fs
数组中String[] names=f.list();
:获取当前目录f
下的一层文件(不管是目录还是实体文件)的名字,并放入names
数组中createFile()
:创建文件。createDirectory()
:创建文件夹。mkdir()
:创建单层目录(不包含子目录)mkdirs()
:创建多层目录(包含子目录)delete()
:删除一个文件或目录。copy()
:复制文件。move()
:移动文件。size()
:查看文件个数。read()
:读取文件。write()
:写入文件。File.separator();
:系统路径分隔符
7.获取用键盘输入常用的两种方法
- 通过Scanner:
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
- 通过BufferedReader:
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
8.日期类
8.1 Date类
- 创建:
Date date = new Date();
// Thu Jul 28 17:16:06 CST 2022
System.out.println(date);
- Date对象表示时间的默认顺序是星期、月、日、小时、分、秒、年(年和星期倒过来)
8.2 SimpleDateFormat日期格式化类
- 将一个日期转换成字符串:
Date date=new Date();
//patern部分如果要添加ASCII字符,记得在它两边加上''
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String dateStr=sdf.format(date);
// 2022-07-28 05:19:53
System.out.println(dateStr);
- 将一个日期格式的字符串转换成日期格式:
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
try{
Date date2=sdf.parse("2022-07-28 05:19:53");
// Thu Jul 28 05:19:53 CST 2022
System.out.println(date2);
}catch(ParseException e){
e.printStackTrace();
}
8.3 Calendar类
Calendar是一个抽象类
- 创建方式:
Calendar calendar = Calendar.getInstance();
- 常用方法:
calendar.setTime(Date date);
:设置date
为当前日历的时间calendar.set(int year, int month, int date);
:设置日历时间为year
年month
月date
日calendar.set(int year, int month, int date, int hour, int minute);
:设置日历时间为year
年month
月date
日hour
时minute
分calendar.set(int year, int month, int date, int hour, int minute, int second);
:设置日历时间为year
年month
月date
日hour
时minute
分second
秒int year = calendar.get(Calendar.YEAR);
:获取年int month = calendar.get(Calendar.MONTH);
:获取月int day = calendar.get(Calendar.DAY_OF_MONTH);
:获取月中日int hour = calendar.get(Calendar.HOUR_OF_DAY);
:获取日中时int minute = calendar.get(Calendar.MINUTE);
:获取分
9.System类相关
9.1 System类常用方法
System.currentTimeMillis();
:获取系统当前时间(单位是毫秒,数据类型是long)System.gc();
:系统回收垃圾,等价于Runtime.getRunTime().gcSystem.exit(-1)
:退出当前程序System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
:数组拷贝
src
:待拷贝的数组srcPos
:待拷贝数组中的要被拷贝过来的元素的起始下标dest
:准备接受拷贝数组元素的数组destPos
:准备接受元素的数组的起始下标length
:拷贝过来的元素的个数
Properties properties = System.getProperties();
:获取系统属性
9.2 标准I/O
- 定义:
java.lang.System
是java系统自带的标准IO流。开发者不能使用System类创建对象,但是可以使用其内部包含的三个数据流:
- 标准输出流:
System.out
- 标准输入流:
System.in
- 标准错误流:
System.err
- 相关源码:
java.lang.System
public final class System extends Object {
static PrintStream err;// 标准错误流(输出)
static InputStream in;// 标准输入(键盘输入流)
static PrintStream out;// 标准输出流(显示器输出流)
}
上述三个流都是静态成员,因此main方法被执行时,就自动生成。
9.2.1 标准输出流 System.out
- 介绍:
System.out
向标准输出设备输出数据,其数据类型为PrintStream。 - 常用方法:
void print(参数)
:输出参数不换行void println(参数)
:输出参数并换行println(参数)
或print(参数)
方法都通过重载实现了输出基本数据类型的多个方法,包括输出参数类型为boolean
、char
、int
、long
、float
和double
。- 同时,也重载实现 了输出参数类型为
char[]
、String和Object的方法。其中,print(Object)
和println(Object)
方法在运行时将调用参数Object的toString()
方法
9.2.2 标准输入流 System.in
- 介绍:
System.in
读取标准输入设备数据(从标准输入获取数据,一般是键盘),其数据类型为InputStream。 - 常用方法:
int read()
:读入1个字节的数据,并返回ASCII码。若返回值为-1,说明没有读取到任何字节,读取工作结束。int read(byte[] b)
:读入多个字节到缓冲数组b中,并返回读入的字节数
- 代码示例:等待键盘输入,键盘输入什么,就打印出什么
import java.io.*;
public class StandardInputOutput {
public static void main(String args[]) {
int b;
try {
System.out.println("please Input:");
while ((b = System.in.read()) != -1) {
System.out.print((char) b);
}
} catch (IOException e) {
System.out.println(e.toString());
}
}
}
相比较而言,使用Scanner和BufferedReader包装System.in的形式就简洁多了,详情见【获取用键盘输入常用的两种方法】
9.2.3 标准错误流 System.err
System.err
输出标准错误,其数据类型为PrintStream。可查阅API获得详细说明
9.2.4 综合使用案例
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class StandardInputOutput {
public static void main(String args[]) {
String s;
// 创建缓冲区阅读器从键盘逐行读入数据
InputStreamReader ir = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(ir);
System.out.println("Unix系统: ctrl-d 或 ctrl-c 退出"
+ "\nWindows系统: ctrl-z 退出");
try {
// 读一行数据,并标准输出至显示器
s = in.readLine();
// readLine()方法运行时若发生I/O错误,将抛出IOException异常
while (s != null) {
System.out.println("Read: " + s);
s = in.readLine();
}
// 关闭缓冲阅读器
in.close();
} catch (IOException e) {// Catch any IO exceptions.
e.printStackTrace();
}
}
}
10.DecimalFormat类
- 介绍:它可以帮助你将数字格式化位你需要的样子,比如取2位小数
- 使用示例:
import java.text.DecimalFormat;
public class DecimalFormatTest {
public static void main(String[] args){
double pi=3.1415927;// 圆周率
// 取一位整数
System.out.println(new DecimalFormat("0").format(pi));// 3
// 取一位整数和两位小数
System.out.println(new DecimalFormat("0.00").format(pi));// 3.14
// 取两位整数和三位小数,整数不足部分以0填补。
System.out.println(new DecimalFormat("00.000").format(pi));// 03.142
// 取所有整数部分
System.out.println(new DecimalFormat("#").format(pi));// 3
// 以百分比方式计数,并取两位小数
System.out.println(new DecimalFormat("#.##%").format(pi));// 314.16%
long c=299792458;// 光速
// 显示为科学计数法,并取五位小数
System.out.println(new DecimalFormat("#.#####E0").format(c));// 2.99792E8
// 显示为两位整数的科学计数法,并取四位小数
System.out.println(new DecimalFormat("00.####E0").format(c));// 29.9792E7
// 每三位以逗号进行分隔。
System.out.println(new DecimalFormat(",###").format(c));// 299,792,458
// 将格式嵌入文本
System.out.println(new DecimalFormat("光速大小为每秒,###米").format(c)); // 光速大小为每秒299,792,458米
}
}
DecimalFormat类主要靠
#
和0
两种占位符号来指定数字长度。0
表示如果位数不足则以0
填充,#
表示只要有可能就把数字拉上这个位置。
11.Comparable接口
11.1 介绍
- 所在包:
java.lang.Comparable
- 作用:Comparable接口是自然排序接口。一个类实现了Comparable接口,就说明这个类是支持排序,由其组成的List可以通过调用
Collections.sort
或Arrays.sort
进行排序 - 应用:装箱类Integer、Double、Long、Short以及BigDecimal等数值类都实现了Comparable接口
- 源码:
public interface Comparable<T> {
int compareTo(T t);
}
11.2 使用示例
- 学生类:
/**
* 学生类,含分数信息
*/
public class Student implements Comparable<Student> {
// 学生考试分数
private int goals;
private String id;
private String name;
@Override
public int compareTo(Student o) {
return this.goals - o.goals;
}
public Student(int goals, String id, String name) {
this.goals = goals;
this.id = id;
this.name = name;
}
public int getGoals() {
return goals;
}
public void setGoals(int goals) {
this.goals = goals;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return (new StringBuilder()).append("当前学生编号:")
.append(id)
.append(",成绩为:")
.append(goals).toString();
}
}
- 测试类:
public class ComparableTest {
public static void main(String[] args) {
Student stu1 = new Student(96, "1", "haha");
Student stu2 = new Student(87, "2", "dada");
List students = Arrays.asList(stu1, stu2);
System.out.println("排序前==================================================");
// 流式打印当前学生列表
students.stream().forEach(System.out::println);
// 先升序
Collections.sort(students);
System.out.println("按成绩升序后=============================================");
// 流式打印当前学生列表
students.stream().forEach(System.out::println);
// 后反转(相当于反序,需要依赖升序)
Collections.reverse(students);
System.out.println("按成绩倒序后=============================================");
// 流式打印当前学生列表
students.stream().forEach(System.out::println);
}
}
运行结果:
排序前==================================================
当前学生编号:1,成绩为:96
当前学生编号:2,成绩为:87
按成绩升序后=============================================
当前学生编号:2,成绩为:87
当前学生编号:1,成绩为:96
按成绩倒序后=============================================
当前学生编号:1,成绩为:96
当前学生编号:2,成绩为:87
12.Comparator接口
12.1 介绍
- 定义:Comparator是比较器接口,当我们需要控制某个类的次序,而这个类是不支持排序的(没有实现Comparable接口),那我们需要实现Comparator接口来建立一个比较器,从而通过比较器来对这个类进行排序。
- 所在包:
java.util.Comparator
- 源码:
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
}
- 要实现Comparator接口,一定要实现
compare()
方法,但是可以不实现equals()
方法:这是因为所有类都继承于Object,而Object类中已有equals()
方法实现,且继承优先于实现,因此无需强制实现equals()
方法。
此处Comparator接口声明
equals()
方法只是告知使用者可以重写该方法。
compare()
方法通用返回值说明:假定o1
、o2
能直接加减,该返回值也就是o1 - o2
的值- 正数:
o1 > o2
- 零:
o1 == o2
- 负数:
o1 < o2
- 正数:
通常需要比较的是
o1
、o2
的内部中的数值成员。
12.2 使用示例
- 使用二分法查找列表中指定元素
public class ComparatorTest {
public static void main(String[] args) {
// 二分法查询列表中指定元素,并返回其索引号。若比当前列表所有元素还小,则返回-1,若比当前所有元素还大,则返回-(size+1)
int res = Collections.binarySearch(Arrays.asList(1, 2, 3, 4, 5), 2,
// 写法1:按照参数要求写匿名内部类,可见匿名内部类只要实现compare方法即可
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// 1.1 值为null时的比较,当操作数不是封箱类而是普通类时相对有用
/*return Comparator.nullsFirst(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return Integer.compare((Integer)o1, (Integer)o2);
}
}).compare(o1, o2);*/
// 1.2 因为整形,不考虑null值,属于整形的通用比较
return Integer.compare(o1, o2);
}
}
// 写法2:最容易想到的lambda表达式版本
// (x,y)-> {return x.compareTo(y);}
// 写法3:写法2的改良版本,更简洁,编译后同写法2
// (x, y) -> x.compareTo(y)
// 写法4:写法3的优化版本,因为写法符合实例方法引用的语法糖,因此改成方法引用形式
// Integer::compareTo
// 写法5:使用Integer的公共静态方法compare
// (x, y) -> Integer.compare(x, y)
// 写法6:写法5符合静态方法引用的语法糖
// Integer::compare
// 写法7:写法5是Comparator.comparingInt方法的简化版本,自然可以用更全面的版本了
// Comparator.comparingInt(o -> o)
// 写法8:运用枚举类的单例特性,Comparator自身提供了 供两个实现Comparable接口的操作数进行比较 的静态方法naturalOrder
// Comparator.naturalOrder()
);
System.out.println(res);
}
}
12.3 最佳运用
本节扩展了解即可,后续Java集合容器会继续学习:
- Collections类中静态方法:
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)
:使用二分法搜索指定列表中的指定元素,并返回其所在索引。- 若指定元素比列表中所有元素都要小,则返回
-1
;若比所有元素都大,则返回-(size+1)
- 若指定元素比列表中所有元素都要小,则返回
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
:返回集合中的最大元素public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> coll)
:返回集合中的最小元素public static <T> Comparator<T> reverseOrder(Comparator<T> cmp)
:返回指定比较器的强行逆转后的版本public static <T extends Comparable<? super T>> void sort(List<T> list)
:对列表进行升序排序
- Arrays类中静态方法:
public static <T> int binarySearch(T[] a, T key, Comparator<? super T> c)
:使用二分法搜索数组中的指定元素,并返回索引public static <T> int binarySearch(T[] a, int fromIndex, int toIndex, T key, Comparator<? super T> c)
:使用二分法搜索指定范围内的数组中的指定元素,并返回索引public static <T> void sort(T[] a, Comparator<? super T> c)
:对数组元素进行升序排序public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)
:对指定范围内的数组元素进行升序排序
11.Objects类
注意不是Object类!!
11.1 介绍
- 定义:Java中的Objects是操作对象的工具类,有如下功能:比较对象、计算
hashCode
、空指针报错等。 - 最佳运用:
Optional.of()
方法
11.2 常用方法
public static <T> int compare(T a, T b, Comparator<? super T> c)
:使用指定的比较器c
比较参数a
和参数b
的大小(相等返回0,a > b
返回正数,a < b
返回负数)
- 源码:
public static <T> int compare(T a, T b, Comparator<? super T> c) {
return (a == b) ? 0 : c.compare(a, b);
}
public static boolean equals(Object a, Object b)
:比较两个对象是否相等
- 首先比较内存地址,然后比较
a.equals(b)
,只要符合其中之一返回true
- 源码:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
public static boolean deepEquals(Object a, Object b)
:深度比较两个对象是否相等
- 首先比较内存地址,相同返回
true
;如果传入的是数组,则比较数组内的各个值是否相同 - 源码:
public static boolean deepEquals(Object a, Object b) {
if (a == b)
return true;
else if (a == null || b == null)
return false;
else
return Arrays.deepEquals0(a, b);
}
public static int hash(Object... values)
:返回传入可变参数的所有值的hashCode
的汇总
- 源码:
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
public static int hashCode(Object o)
:返回对象的hashCode
,若传入的为null
,返回0
- 源码:
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
public static <T> T requireNonNull(T obj)
:如果传入的obj
为null
抛出NullPointerException
,否者返回obj
- 源码:
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
Objects.requireNonNull(obj)
等价于Optional.of(obj)
,因为Optional.of(obj)
内部最底层调用的就是Objects.requireNonNull(obj)
:- Optional.of()方法源码:
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
...
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
public static String toString(Object o)
:返回对象的String
表示,若传入null
,返回null
字符串
public static String toString(Object o, String nullDefault) {
return (o != null) ? o.toString() : nullDefault;
}
public static String toString(Object o, String nullDefault)
:返回对象的String
表示,若传入null
,返回默认值nullDefault
- 源码:
public static String toString(Object o, String nullDefault) {
return (o != null) ? o.toString() : nullDefault;
}
六、Java反射和代理
1. 反射介绍
- 动态语言:指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结构上的变化。
- 举例:JavaScript、Ruby、Python都属于动态语言,C、
C++
不属于动态语言,Java属于半动态语言
- 反射机制定义:Java 中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法。
- 静态编译和动态编译的区别:
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
2. 反射API
2.1 Class类
2.1.1 创建Class类实例
- 方式:以类
Student
为例:
Class cls = Student.class;
Class cls = student.getClass();
Class cls = Class.forName(Student类的全名[包括包名]);
- Class类实例的
toString()
结果:class packageName.className
以下无特别说明,
cls
就是指代类的Class实例
2.1.2 常用方法
- 获取属性:
Filed[] fields = cls.getFields();
:获取cls
对应类的所有公共属性成员,放进Field数组里[包含继承的属性]→无顺序,无规则Filed[] fields = cls.getDeclaredFields();
:获取cls
对应类私有、默认、保护、公共的属性成员[不包含继承的属性]→无顺序,无规则Field[] fields = cls.getFields();
:获得所有的公共属性[包含继承属性]Filed field = cls.getDeclaredField(String name);
:获取指定名称的属性成员
- 获取非构造方法:
Method[] methods = cls.getMethods();
:获取cls
对应类的所有公共方法[包含继承的属性]→无顺序,无规则Method method = cls.getMethod(方法名, 参数1的类型.class, 参数2的类型.class,..);
:获取指定名称的公共方法[包含继承的方法]Method[] methods = cls.getDeclaredMethods();
:获取cls
对应类私有、默认、保护、公共的方法[只能是当前类声明的方法]→无顺序,无规则Method method = cls.getDeclaredMethod(方法名, 参数1的类型.class, 参数2的类型.class,..);
:获取指定名称的方法[只能是当前类声明的方法]
2.1 注意事项:
1)没有参数直接填null
或不填,想传null
请强转型
2)方法参数顺序与原方法参数顺序相同
- 获取构造方法:
Constructor con = cls.getConstructor(形参1类型.class, 形参2类型.class,...);
:获取指定方法名的public类型的构造方法【构造方法不能继承】Constructor con = cls.getDeclaredConstructor(形参1类型.class, 形参2类型.class,...);
:获取指定方法名的任意类型的构造方法Constructor[] cons = cls.getConstructors();
:获取cls
对应类的所有public类型的构造方法(含缺省)Constructor[] cons = cls.getDeclaredConstructors();
:获取cls
对应类中声明的构造方法
3.1 注意事项:
1)没有参数直接填null
或不填,想传null
请强转型
2)方法参数顺序与原方法参数顺序相同
- 获取周期是RUNTIME的注解:只能是用
@Retention
修饰且值为RetentionPolicy.RUNTIME
的注解
cls.getAnnotation(注解名.class)
:获取指定注解名的(自身和从父类继承)的注解cls.getAnnotations()
:获取自身和从父类继承的注解
4.1 注意事项:
1)只有当父类上的注解是用@Inherited
修饰的,子类的getAnnotation(注解名.class)
、getAnnotations()
才能获取得到父亲的注解以及自身的注解。
2)若注解被@Repeatable
修饰,则可以用getAnnotation(@Repeatable值对应注解名.class)
获取到@Repeatable
值对应注解【尽管源代码中根本没有声明该注解去修饰该类】
cls.getDeclaredAnnotations()
:只会获取自身声明的注解,无论如何都不会获取父亲的注解
更多注解使用详见【Java注解】章节
- 其他:
Class cls_1 = cls.getSuperClass();
:获取cls
对应类(接口)的直接父类的cls
实例Class[] clss = cls.getInterfaces();
:获取cls
对应类(接口)实现(继承)的接口,将它们放进一个Class
类型的数组里
2.2 Field类
2.2.1 创建Field类实例
- 方式:见Class类部分
- Field类实例的
toString()
结果:modifier type packageName.className.fieldName
type
可能是packageName.className
,或者是基本数据类型;modifier
可能是默认访问修饰符,这个时候用字符串形式输出自然就是空了。
以下无特别说明,
f
就是指代类的Field实例
2.2.2 常用方法
String name = f.getName();
:返回成员的名字(String类型);Class cls = f.getType();
:返回成员的类型(Class类型);int mod=f.getModifiers();
:返回成员的访问修饰符(int
类型)
注意,是
getModifiers()
不是getModifier()
!
String mod2 = Modifier.toString(mod);
:接上一个的mod
,返回成员的访问修饰符(String类型)f.setAccessible(true);
:让该属性可以被访问(允许修改其在某个对象中的值)
使用
f.setAccessible(true);
不会改变成员本身的访问修饰符
f.get(object);
:取得对象object
当前属性f的值f.set(object, 值);
:设置对象object
当前属性f
的值为值
2.2.3 设置对象成员属性值
假设存在Object类型对象obj,以其反射创建另一个Object类型对象并初始化其成员对象(成员对象可能私有)
- 方式1:依次获取成员的set方法,并对其赋值
Object returnObject=null;
try {
returnObject = obj.getClass().newInstance();
Field[] f1 = obj.getClass().getDeclaredFields();
for (Field f : f1) {
//获得属性名
String fieldName = f.getName();
//获得方法的名字
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
String setMethodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
//获取set和get方法
Method getMethod = obj.getClass().getDeclaredMethod(getMethodName, null); // 注意这里只能是obj.getClass
Method setMethod = obj.getClass().getDeclaredMethod(setMethodName, f.getType());
//设置属性值
Object value = getMethod.invoke(obj, null);
setMethod.invoke(returnObject, value);
}
} catch(..){..}
- 方式2:让该属性可以被访问,直接反射设值:
...
for (Field f : f1) {
// 先让当前属性的访问权限变为非private
f.setAccessible(true);
// 再获取obj对象中的f属性的值,并赋值给value中
Object value = f.get(obj);
// 最后设置returnObject对象的f属性值为value
f.set(returnObject, value);
}
2.3 Method类
2.3.1 创建Method类实例
- 方式:见Class类部分
- Method类实例的
toString()
结果:modifier returnType packageName.className.methodName([packageName.className/基本数据类型,...])
returnType
有可能是packageName.className
,或者是基本数据类型,或者干脆就是不显示,为空(void
)
- 获取参数列表为空的方法:
getMethod()
括号内参数列表部分可以填上null或者去掉逗号啥都不填:
Method method = cls.getDeclaredMethod("getName");
Method method = cls.getDeclaredMethod("getName", null);
单纯的null表示参数没有(空)、不填的意思
2.3.2 常用方法
String name = method.getName();
:返回方法名int mod = method.getModifiers();
:返回方法的访问修饰符号Class cls = method.getReturnType();
:获取方法的返回值类型Class[] clss = method.getParameterTypes();
:返回参数的类型,放进数组invoke()
方法:public Object invoke(Object object,Object..args);
obj
:表示该方法由谁去调用args
:表示这个方法的实际参数- 示例:
// 1.调用无参方法
Method m1=Student.class.getDeclaredMethod("get",null);
m1.invoke(stu,null);// 这边写成m1.invoke(stu);也对
// 2.调用有参方法
Method m2=Student.class.getDeclaredMethod("set",String.class,int.class,String.class);
Object value=m2.invoke(stu,"mazai",10,"haha");
System.out.println(value);// 输出value时如果方法的返回值为void,那么输出null
2.4 Constructor类
2.4.1 创建Constructor类(构造方法)实例
- 方式:见Class类部分
- 注意:
在获取指定的无参构造方法时,形参列表可以不填写,也可以写成null,即
cls.getConstructor(null)
和cls.getConstructor()
2.4.2 常用方法
- 创建对象实例:
Object obj = con.newInstance(null);
:使用无参构造方法实例化obj
Object obj = con.newInstance(实际参数列表);
:使用有参构造方法实例化obj
obj不强制类型转换也可用sysout输出,输出结果等同于con实际来源的类的输出[子类的输出]
2.4.3 其他
- 什么情况下执行以下这句不会报错:
Object obj = con.newInstance(null);
- A.
Constructor con = cls.getDeclaredConstructor(String.class);
- B.
Constructor con = cls.getDeclaredCOnstructor(null);
1.1 解析:
1)这边只能选B,你可能会问为什么不能选A,因为A的形参是String类型,你可以传递一个null给构造方法,但是要注意了我们说过在反射机制中单纯的null代表空、没有、不填的意思,而要是选A本意却是传一个null的地址值,这就错了,出现非法参数异常。
2)解决办法:原题中Object obj=con.newInstance((String)null)
就能选A了
2.5 ReflectionUtils类
2.5.1 介绍
反射工具类ReflectionUtils会使用缓存等优化反射性能
2.5.2 常用API-获取方法
Method findMethod(Class<?> clazz, String name)
:在类中查找指定方法Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes)
:同上,额外提供方法参数类型作查找条件Method[] getAllDeclaredMethods(Class<?> leafClass)
:获得类中所有方法,包括继承而来的Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes)
:在类中查找指定构造方法boolean isEqualsMethod(Method method)
:是否是equals()
方法boolean isHashCodeMethod(Method method)
:是否是hashCode()
方法boolean isToStringMethod(Method method)
:是否是toString()
方法boolean isObjectMethod(Method method)
:是否是从Object
类继承而来的方法boolean declaresException(Method method, Class<?> exceptionType)
:检查一个方法是否声明抛出指定异常
2.5.3 常用API-执行方法
Object invokeMethod(Method method, Object target)
:执行方法(无参)Object invokeMethod(Method method, Object target, Object... args)
:执行方法(带参)void makeAccessible(Method method)
:取消Java权限检查。以便后续执行该私有方法void makeAccessible(Constructor<?> ctor)
:取消Java权限检查。以便后续执行私有构造方法
2.5.4 常用API-获取字段
Field findField(Class<?> clazz, String name)
:在类中查找指定属性Field findField(Class<?> clazz, String name, Class<?> type)
:在类中查找指定类型、指定名称的属性boolean isPublicStaticFinal(Field field)
:是否为一个public static final
属性
2.5.5 常用API-设置字段
Object getField(Field field, Object target)
:获取target
对象的field
属性值void setField(Field field, Object target, Object value)
:设置target
对象的field
属性值,值为value
void shallowCopyFieldState(Object src, Object dest)
:同类对象属性对等赋值void makeAccessible(Field field)
:取消Java的权限控制检查。以便后续读写该私有属性void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)
:对类的每个属性执行callback
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc, ReflectionUtils.FieldFilter ff)
:对类的指定属性执行callback
void doWithLocalFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)
:对类的每个属性(不包括继承而来的属性)执行callback
2.6 通过反射创建对象
- 通过Class类的
newInstance()
方法反向生成目标对象:
// 默认调用无参构造方法(缺省或不缺省)
Object obj = cls.newInstance();
Person p=(Person)obj;
- 通过构造方法来创建对象:
// cons.length返回的是public类型的构造方法个数
Constructor[] cons = cls.getConstructors();
// null表示是无参,想传一个null值的参数需对null强转型
Object obj=cons[0].newInstance(null);
3.反射应用场景及优缺点
3.1 应用场景:
- Java动态代理
- 在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序。
- Spring框架的xml配置模式。
3.1 Spring通过XML配置模式装载Bean的过程:
1)将程序内所有xml或properties配置文件加载入内存中;
2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
3)使用反射机制,根据这个字符串获得某个类的Class实例;
4)动态配置实例的属性。
3.2 优缺点:
- 优点:运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多。
4.Java代理
4.1 设计模式-代理模式
- 定义:代理模式是指以某一类对象为目标、提供对应的代理对象实例,代理目标对象的方法,并提供给外部调用。在有代理后,外部访问目标对象会被代理对象拦截。
- 代理模式三角色:
- 抽象角色(Subject):一般会采用接口或者抽象类,抽象角色的功能
- 真实主体(RealSubject):其实例又叫目标对象,完成主体功能
- 代理主体(ProxySubject):其实例又叫代理对象,完成目标对象不能搞定的操作
- 分类:
- 静态代理:在编译阶段就确定代理角色,并且明确代理类和目标类的关系
- 动态代理:基于Java反射机制,在JVM运行时动态创建和生成代理对象。
3.1 动态代理相比静态代理的优势:
相比于静态代理, 动态代理的优势在于可以很方便的对目标对象的方法进行统一的处理,而不用修改每个目标对象中的方法。
- UML图示:
代理类中一定包含一个目标对象的引用!
4.1 静态代理
- 抽象角色接口Subject:
public interface Subject {
public void giveMoney();
}
- 真实主体RealSubject:
public class RealSubject implements Subject {
@Override
public void giveMoney() {
System.out.println("给钱");
}
}
- 代理类ProxySubject:
public class ProxySubject implements Subject {
// 代理类中一定包含一个目标对象的引用
private Subject subject;
public ProxySubject(Subject subject) {
this.subject = subject;
}
/**
* 代理类前置操作
*/
private void before() {
System.out.println("调查背景");
System.out.println("带上假的手枪");
}
@Override
public void giveMoney() {
before();
subject.giveMoney();// 可以看出实际上还是真实类在进行目标操作
after();
}
/**
* 代理类后置操作
*/
private void after() {
System.out.println("收取佣金");
}
}
4.2 动态代理
4.2.1 两个核心类
InvocationHandler
:代理调用处理器类Proxy
:代理主体,通过Proxy.newProxyInstance()
方法来产生代理对象
4.2.2 典型案例
- 抽象角色接口Subject:
public interface Subject {
public void giveMoney(int money);
public void eat();
public void drink();
}
- 真实主体RealSubject:
public class RealSubject implements Subject {
@Override
public void giveMoney(int money) {
System.out.println("给我钱"+money);
}
@Override
public void eat() {
System.out.println("吃海鲜");
}
@Override
public void drink() {
System.out.println("喝五粮液");
}
}
- InvocationHanderl接口实现类:MyInvocationHandler
public class MyInovocationHandler implements InvocationHandler {
// 目标对象的引用(也可以是父类引用指向子类实例)
private Object object ;
public MyInovocationHandler(Object object) {
this.object = object;
}
// 获取一个代理对象
public Object getProxyObject() {// 该方法返回一个实现接口和真实主体相同的代理对象
return Proxy.newProxyInstance(object.getClass().getClassLoader(),// 获取真实主体的类加载器【关于类加载器详见反射机制】
object.getClass().getInterfaces(),// 获取真实主体所实现的接口,并将它们放进Class类型的数组里
this);// 传递一个代理调用处理器实例:因为每一个代理对象的产生都会绑定一个调用处理器
}
// 核心方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object value = null ;
if(object.getClass()==RealSubject.class) {
if("giveMoney".equals(method.getName())) {
System.out.println("带上枪支,炸弹。。");
method.invoke(object, args);
System.out.println("打完收工");
}else{
method.invoke(object, args);
}
}else if(object.getClass()==ArrayList.class) {
if("add".equals(method.getName())) {
if("a".equals(args[0])) {
return false ;
}else{
value = method.invoke(object, args);
}
}
}
return value;
//return null ;
}
}
MyInvocationHandler
是一个代理调用处理器类。- 每一个代理对象都与一个代理调用处理器类实例(以下简称代理调用处理器)相关联:通过将真实类的信息传递到代理调用处理器类中来初始化代理调用处理器,然后再通过
Proxy.newProxyInstance()
来构建一个代理对象。 - 代理对象调用上层接口声明的方法时,会触发调用 代理调用处理器 的
invoke()
方法,最后再在invoke()
方法中运用反射机制(指method.invoke(object, args);
)调用目标对象的相应方法。
- 测试类:Test
public class Test {
public static void main(String[] args) {
// 获取一个关于真实对象list的调用处理器
MyInovocationHandler invocationHandler = new MyInovocationHandler(list);
// 获取一个代理类
Object object = invocationHandler.getProxyObject();
System.out.println(object instanceof List);
List proxy = (List) object;
// 代理对象帮助真实完成附加操作
proxy.add("a");
proxy.add("a");
proxy.add("a");
proxy.add("b");
proxy.add("c");
proxy.add("d");
proxy.add("e");
System.out.println(list);
}
}
4.2.3 Proxy和InvocationHandler的其他内容
Proxy.newProxyInstance()
方法参数解析:public static Object newProxyInstance(ClassLoader loader, Class interfaces, InvocationHandler h);
:
loader
:真实主体的类加载器
如何获取类加载器?→
cls.getClassLoader();
interfaces
:真实主体实现的所有接口对应的Class类型数组
代理类需要实现真实主体所实现的所有接口
如何获取所有接口Class? →
cls.getInterfaces();
h
:相关联的代理调用处理器实例
- 对InvocationHandler的invoke()方法的理解:
Object invoke(Object proxy, Method method, Object[] args);
:代理对象执行实现的方法时回调该方法,返回值类型建议为真实类调用相同方法的返回值类型proxy
:代理对象method
:目标对象的待调用方法args
:目标对象的待调用方法中的参数
七、Java异常处理
1.介绍
1.1 异常机制
- Java异常是Java提供的一种识别及响应错误的一致性机制。
- Java异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。
- 在有效使用异常的情况下,异常能清晰的回答 what, where, why这3个问题:异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪”抛出,异常信息回答了“为什么”会抛出。
1.2 层次结构及分类
- 图示:
- 分类:
- Error(错误):程序中无法处理的异常。大多数错误与代码编写者执行的操作无关,而是与JVM的错误有关。
- Exception(异常):程序中可以处理的异常。
- CheckException(检查时异常):必须使用try…catch进行处理的异常
- RuntimeException(运行时异常):可以放在try…catch中处理,也可以直接由方法抛出。
- 10种常见运行时异常:
NullPointerException
:空指针异常ArrayStoreException
:存储元素类型不匹配异常ClassCastException
:类转换异常DOMException
:DOM异常NoSuchElementException
:Scanner抛出的类型不匹配异常【常见于迭代器next()方法输出空时】IndexOutOfBoundsException
:下标超出范围异常IllegalArgumentException
:非法参数异常StringIndexOutOfBoundsException
:字符串…(charAt(int index)方法调用时)ArrayIndexOutOfBoundsException
:数组…ArithmeticException
:运算条件异常(例如“除以零”)
- 抛出异常三种形式:
- 方法声明用throw
- 方法内部用throws
- 系统自动抛出异常
2.常用API
2.1 Throwable类
2.1.1 Throwable类常用方法
getMessage()
:返回异常发生时的简要描述toString()
:返回异常发生时的详细信息getLocalizedMessage()
:返回异常对象的本地化信息。
- 使用Throwable的子类覆盖这个方法,可以生成本地化信息。
- 如果子类没有覆盖该方法,则该方法返回的信息与
getMessage()
返回的结果相同
printStackTrace()
:在控制台上打印Throwable对象封装的异常信息
3.throw和throws
throws
:表示在方法的声明处抛出一个异常类型,作用在于告知调用者调用此方法可能会产生异常。throw
:方法内部程序自行抛出一个异常对象。
只能抛出包含throwable在内的异常类及子类!
- 两者区别:
角度 | throw | throws |
---|---|---|
位置 | 用在方法内,后面跟的是异常对象 | 用在方法上,后面跟的是异常类,可以跟多个 |
功能 | ①执行 throw 则一定抛出了某种异常对象②throw 抛出具体的问题对象,执行到 throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者 | ①throws表示出现异常的一种可能性,并不一定会发生这些异常②throws用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式 |
4.try-catch-finally
4.1 try块
用于捕获异常。其后可接零个或多个
catch
块,如果没有catch 块,则必须跟一个finally
块。
4.2 catch块
用于处理捕获到的异常,且仅当捕获到对应异常时才进入
catch
块。
- 多个
catch
块时,先捕获子类异常,再捕获父类异常,否则编译报错。 - 多个
catch
块时按照声明顺序从上往下判断,如果和某个catch
块代码匹配,则执行相应的异常处理,后面的catch
块不看了
4.3 finally块
try
块或者catch
块出现下列场景之一时:执行完 或 期间发生异常 或 方法要返回(return
语句),只要后续有finally
块,在抛出异常(如果有的话)、方法返回(如果有的话)前需先执行finally
块,再接着抛出异常(如果有的话)、方法返回(如果有的话)
- 无论是否捕获或处理异常,
finally
块里的语句都会被执行。 - 执行完finally块后,若不需要抛出异常且不需要返回方法,则继续执行后续代码:
2.1 示例代码:
try {
System.out.println("1");
throw new Exception("haha");
} catch (Exception e) {
System.out.println("2");
} finally {
System.out.println("3");
}
System.out.println("4");// 尽管try部分抛出异常了,但是因为catch给吞了,所以4正常输出
执行结果:
1
2
3
4
finally
块中主动执行return语句时,将会覆盖前面待return的值:
3.1 示例代码:
public static void main(String[] args) throws Exception {
int res = test();
System.out.println(res);// 结果0
}
public static int test() throws Exception {
try {
return 1;
} finally {
return 0;
}
}
finally
块中手动声明抛出异常、执行return
语句时,finally
块后续不能再有代码,否则编译报错。
4.1 示例代码:
try {
System.out.println("1");
throw new Exception("2222");
} catch (Exception e) {
System.exit(-1);
throw e;
} finally {
System.out.println("2");
throw new Exception("3333");
}
System.out.println("3");// 这行编译报错,原因也简单,因为无法达到
finally
块执行过程中抛出异常(主动或非主动都行) 或 执行return
语句后,无论执行finally
块前是否已经准备抛出别的异常,之前的异常直接吞掉不再抛出:
5.1 示例代码1:
try {
System.out.println("1");
throw new Exception("aa");
} catch (Exception e) {
System.out.println("2");
throw e;// 想抛出,看finally给不给你机会😜
} finally {
System.out.println("3");
throw new Exception("cc");// 直接把异常aa给吞掉!
}
执行结果:
1
2
3
Exception in thread "main" java.lang.Exception: cc
5.2示例代码2:
try {
System.out.println("1");
throw new Exception("aa");
} catch (Exception e) {
System.out.println("2");
throw e;// 想抛出,看finally给不给你机会😜
} finally {
System.out.println("3");
return ;// 直接把异常aa给吞掉!
}
执行结果:
1
2
3
4.4 执行流程
try
块中没有抛出异常,不会进入任何catch
块,后续有finally
块则执行finally
块。如果抛出异常,则依次向下匹配catch
块。- 如果和某个
catch
块匹配,则执行相应的异常处理,后面的catch
块不再判断。catch
块中抛出异常、执行return
语句前,需先执行finally
块,finally
块执行完后,有条件再在catch
块中抛出异常、执行return
语句。 - 如果出现异常且所有的
catch
块都不匹配,则看有没有finally
块。没有finally
块则向上抛出异常,有的话,则先执行finally
代码块,然后看情况。 - 如果
finally
块中抛出异常或执行return
语句,则直接覆盖之前的待抛出异常(如果有的话)、待执行return
语句(如果有的话),且是finally
块中主动抛出异常或执行return
语句时,finally
块后有代码就编译报错。 finally
块正常执行完,若之前有待抛出异常(如果有的话)、待执行return
语句(如果有的话),该抛出异常就抛出异常,该执行return
语句就执行return
语句。若没有,则接着执行finally
块后的代码。- 整个过程中任何一环节出现【
finally
块不会执行的3种情况】,finally
块都不会执行。
4.5 finally块不会执行的3种情况
- 在执行
finally
语句块前调用了System.exit(-1);
退出程序
1.1 示例代码:
try {
System.out.println("1");
throw new Exception("2222");
} catch (Exception e) {
System.exit(-1);
throw e;
} finally {
System.out.println("2");
}
执行结果:1
- 程序所在的线程死亡
- 关闭CPU
5.try-with-resources
5.1 介绍
- 定义:try-with-resources是Java从JDK1.7开始提供的一种语法糖,用来简化异常处理过程中的资源关闭操作,让代码变得更加美观。
1.1 关于语法糖:
是指在不增加语言额外功能模块的情况下,提供了一种便捷的写法,从而增强代码的编程效率和可读性
- 版本要求:JDK1.7起
- 语法:将创建资源的操作写在
try()
的括号中,try
块中可以使用括号中的资源变量,且无需声明资源对象的关闭:
File f = new File("D:"+ File.separator +"test2.txt");
try(InputStream is = new FileInputStream(f);
// 括号内多个语句时,中间正常用分号分割,最后语句末尾可省略分号
Reader isr = new InputStreamReader(is, "utf-8")
) {
char[] c = new char[1024];
int length = -1;
while (-1 != (length = isr.read(c))) {
String str=new String(c, 0, length);
System.out.print(str);// 输出:我是你爸
}
} catch (Exception e) {
throw e;
}
- 编译后的结果:
File f = new File("D:" + File.separator + "test2.txt");
try {
InputStream is = new FileInputStream(f);
Throwable var3 = null;
try {
Reader isr = new InputStreamReader(is, "utf-8");
Throwable var5 = null;
try {
char[] c = new char[1024];
boolean var7 = true;
int length;
while(-1 != (length = isr.read(c))) {
String str = new String(c, 0, length);
System.out.print(str);
}
} catch (Throwable var32) {
var5 = var32;
throw var32;
} finally {
if (isr != null) {
if (var5 != null) {
try {
isr.close();
} catch (Throwable var31) {
var5.addSuppressed(var31);
}
} else {
isr.close();
}
}
}
} catch (Throwable var34) {
var3 = var34;
throw var34;
} finally {
if (is != null) {
if (var3 != null) {
try {
is.close();
} catch (Throwable var30) {
var3.addSuppressed(var30);
}
} else {
is.close();
}
}
}
} catch (Exception var36) {
throw var36;
}
很明显,繁琐的很啊
- 需要注意的是,资源变量的定义必须在
try()
括号内,try
外部会编译报错。 try-with-resources
语句也可以有finally
块- JDK1.9后,
try()
括号内可以使用外部的final
修饰的静态变量资源。
5.2 异常处理
- 通过查看编译后的代码可知,和正常
try-catch-finally
相同
6.常见异常及处理
6.1 Compilation failed: internal java compiler error
- 场景:使用idea开发中编译时报
Error:java: Compilation failed: internal java compiler error
错误。 - 出错原因:JDK版本不匹配,分为两方面:①编译器版本不匹配②当前项目JDK版本不支持
- 解决方案:
- 查看项目的JDK版本:File→Project Structure→Project Settings→Project:如下所示,查看此两处是否与目标JDK一致:
- 查看工程的JDK版本:File→Project Structure→Project Settings→Modules,如下所示:查看此处是否与目标JDK一致:
- 查看IDEA编辑器的JDK版本:File→Settings→Build, Execution, Deployment→Compiler,点击Java Compiler查看如图所示(Target bytecode version)目标版本:
6.2 OOM(OutOfMemoryError)异常
6.2.1 OOM介绍
- 内存位置:除了程序计数器外,JVM内存的其他几个运行时区域都有发生OOM异常的可能。
6.2.2 相关异常以及解决方案
- Java堆溢出:
java.lang.OutOfMemoryError:Java heap spacess
- 原因:Java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
- 解决方案:
- 先先通过内存映像分析工具(如Eclipse Memory Analyzer)对dump出来的堆转存快照进行分析,重点是确认内存中的对象是否是必要的,先分清是因为内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
- 如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄漏对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收。
- 如果不存在泄漏,那就应该检查虚拟机的参数(-Xmx与-Xms)的设置是否适当。
- 虚拟机栈和本地方法栈溢出:
- 原因:虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常
- 解决方案:栈的大小越大可分配的线程数就越少,因此一般控制好线程数即可
- 方法区溢出:
java.lang.OutOfMemoryError:PermGenspace
3.1 注意事项:
1)由于JDK1.7开始字符串常量池搬出运行时常量池(方法区),因此不再存在【运行时常量池溢出】的异常场景
2)由于JDK1.8开始HotSpot虚拟机取消了永久代,对应方法区也从堆内存移动到了直接内存(直接内存大小是物理机定的,hen大),并改称为元数据区(Metaspace),因此不再存在【方法区溢出】的这个大异常场景
- 原因:
- 运行时常量池溢出:此处特指由于字符串常量池中字符串常量的增加导致的方法区的溢出。
- 类元数据溢出:方法区中保存的class对象没有被及时回收掉或者class信息占用的内存超过了我们配置
- 解决方案:
- 通过
-XX:PermSize
和-XX:MaxPermSize
参数限制方法区的大小 - 检查代码中是否存在动态生成大量Class的代码,进行减少优化
- 通过
八、Java IO/NIO及序列化
本章节需要额外具备多线程方面的知识储备,尤其是Java线程模型、系统调用,此处不作说明,详见我的【并发编程】篇笔记
1.操作系统层面的基本概念
1.1 文件描述符(fd)
- 定义:文件描述符(file descriptor)简称
fd
,是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。 - 底层结构:文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
- 应用场景:在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
1.2 缓存I/O(标准I/O)
- 定义:缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。
- 底层原理:在Linux的缓存I/O机制中, 操作系统会将I/O的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中, 然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
- 缓存I/O的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。
标准I/O的应用详见【System类相关】
1.3 同步和异步
- 同步请求:A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。
- 异步请求:A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知A。
- 两者区别:最大的区别就是被调用方的执行方式和返回时机:同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。
1.3.1 扩展:用热水壶烧开水的例子来解释一下同步、异步概念
1)同步:在很久之前,科技还没有这么发达的时候,如果我们要烧水, 需要把水壶放到火炉上,我们通过观察水壶内的水的沸腾程度来判断水有没有烧开。
2)异步:随着科技的发展,现在市面上的水壶都有了提醒功能,当我们把水壶插电之后,水壶水烧开之后会通过声音提醒我们水开了。
1.4 阻塞和非阻塞
- 阻塞请求:A调用B,A一直等着B的返回,别的事情什么也不干。
- 非阻塞请求:A调用B,A不用一直等着B的返回,先去忙别的事情了。
- 两者区别:最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待:阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。
1.4.1 扩展:用热水壶烧开水的例子来解释一下阻塞、非阻塞概念
1)阻塞:当你把水放到水壶里面,按下开关后,你可以坐在水壶前面,别的事情什么都不做, 一直等着水烧好。
2)非阻塞:可以先去客厅看电视,等着水开就好了。
1.5 阻塞、非阻塞和同步、异步的区别
- 针对的对象不同:阻塞、非阻塞说的是调用者。同步、异步说的是被调用者。
- 同步包含阻塞和非阻塞:
- 我们是用传统的水壶烧水。在水烧开之前我们一直做在水壶前面,等着水开。这就是阻塞的。
- 我们是用传统的水壶烧水。在水烧开之前我们先去客厅看电视了,但是水壶不会主动通知我们, 需要我们时不时的去厨房看一下水有没有烧开,这就是非阻塞的。
- 异步包含阻塞和非阻塞:
- 我们是用带有提醒功能的水壶烧水。在水烧发出提醒之前我们一直做在水壶前面,等着水开。这就是阻塞的。
- 我们是用带有提醒功能的水壶烧水。在水烧发出提醒之前我们先去客厅看电视了,等水壶发出声音提醒我们。这就是非阻塞的。
1.6 I/O访问过程
- 对于本地字节、字符流而言:以read操作举例,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。 所以说,当一个read操作发生时,它会经历两个阶段:
- 第一阶段:等待数据准备(
Waiting for the data to be ready
)。 - 第二阶段:将数据从内核拷贝到进程(用户态内存)中(
Copying the data from the kernel to the process
)。
- 对于socket流而言:
- 第一阶段:通常涉及等待网络上的数据分组到达,也就是被复制到内核的某个缓冲区。
- 第二阶段:把数据从内核缓冲区复制到应用进程缓冲区。
1.7 并发与并行
- 并发数:一段时间内可以同时进行的任务数(如同时服务的HTTP请求)
- 并行数:某一时刻可以同时工作的物理资源数量(如CPU核数)
- 并发相比于并行的优势:在高并发的情况下,采用并行的方式为每个任务(用户请求)创建一个进程或线程的开销是非常大的;而通过合理调度任务的不同阶段,并发数可以远远大于并行数,这就是区区几个CPU 可以支持上万个用户并发请求的奥秘。
2.Unix下五种I/O模型
2.1 同步阻塞I/O模型(BIO)
- 定义:最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
也就是说数据的读写必须阻塞在一个线程内等待其完成,无法立即完成则保持阻塞。
- 同步阻塞I/O分为如下两个阶段:
- 阶段1:等待数据就绪:用户线程发出IO请求(进行
recvform
系统调用)之后,负责IO的内核线程(以下简称内核,请知悉)会去查看fd对应数据是否就绪,如果没有就绪就会等待就绪,而用户线程就会处于阻塞状态(block
),用户线程交出CPU分片。
网络I/O的情况就是等待远端数据陆续抵达,也就是网络数据被复制到内核缓存区中,磁盘I/O的情况就是等待磁盘数据从磁盘上读取到内核态内存中。
- 阶段2:数据拷贝:出于系统安全,用户态的程序没有权限直接读取内核态内存,因此当数据就绪之后,内核负责把内核态内存中的数据拷贝一份到用户态内存中,并返回结果给用户线程,用户线程才解除
block
状态去进行相应的数据处理操作。 - 这两个阶段必须都完成后才能继续下一步操作。
- 示意图:
- 代码示例:
data = socket.read();// 如果数据没有就绪,就会一直阻塞在read方法
- 适用场景:在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。
- 缺陷:当面对十万甚至百万级连接的时候,传统的BIO模型无能为力。
- Java是否支持:支持,对应
java.io
包
2.2 同步非阻塞I/O模型(NIO)
同步非阻塞I/O模型又叫异步阻塞I/O模型:
1)说其【同步非阻塞】,是因为
a.用户线程发起IO请求后没有阻塞,继续做别的事;
b.而在进行真正IO操作时需要同步内核(负责IO的内核线程)进行;
2)说其【异步阻塞】,是因为a步骤是用户线程和内核异步的,b步骤是用户线程阻塞的
- 步骤:以读操作为例:
- 相比于BIO,用户线程在进行
recvform
系统调用之后,用户线程并没有被阻塞,内核马上返回给用户线程 fd对应数据报的准备状态(未准备好则返回error
)。 - 若未准备好,则后续开启轮询,循环往复的进行
recvform
系统调用。
这边开启轮询可能是再开一个用户线程作轮询,也可能是由已有内核线程作轮询,后者是多路复用IO模型的实现方式。
- 同时,在数据报准备好前,用户线程可进行别的操作。
- 轮询检查到内核数据报进入读就绪状态后,后续操作和BIO相同:内核从内核态内存拷贝数据到用户态内存,成功则通知用户线程进行数据处理【这个步骤对于用户线程是阻塞的】
- 示意图:以读操作为例:
non-blocking socket
执行读操作流程:
- 代码示例:
// 现实中除非是低负载,否则不会用新开用户线程内部while循环来模拟NIO→CPU占用率太高了
while(true){
data = socket.read();
if(data!= error){
处理数据
break;
}
}
- 适用场景:高负载、高并发的(网络)应用
- 缺陷:通过轮询的方式不断地去询问内核数据是否就绪,在高并发情况下会导致CPU占用率非常高。
- Java支持:支持,对应
java.nio
包。Java NIO实际实现的是多路复用IO模型,而不是上述这种低级的while循环实现,详见下文【2.3 多路复用I/O】小节
2.2.1 扩展了解:
1)在Java 1.4 中引入了NIO框架(New I/O
),提供了Channel
、Selector
、Buffer
等抽象。
2)NIO中的N
可以理解为Non-blocking
,不单纯是New
。它支持面向缓冲的、基于通道的I/O操作方法。
3)NIO提供了与传统BIO模型中的Socket
和ServerSocket
相对应的SocketChannel
和ServerSocketChannel
两种不同的套接字通道实现,两
种通道都支持阻塞和非阻塞两种模式。
2.3 多路复用I/O模型
- 介绍:又叫事件驱动IO模型,属于同步非阻塞I/O模型(NIO)中的一种。
- IO多路复用:是指内核一旦发现用户线程指定的一个或者多个IO通道(
socket
)上的fd
对应数据报进入读就绪状态,内核就从内核态内存拷贝数据到用户线程,成功则通知用户线程进行数据处理。 - 优点:相比普通的同步非阻塞I/O模型,其强大之处在于只需要一个用户线程就能轮询管理多个通道(
socket
)的状态(又叫事件),并且不需要建立、维护新的用户线程。只有当socket
真正有读写事件到达时,用户线程才真正进行数据处理操作,所以它大大减少了IO资源占用。
多路复用I/O属于同步非阻塞I/O,依旧没有解决高并发下CPU占用过高的问题。
- 原理:以读操作为例:
- 用户线程发出IO请求(执行
select
、pselect
、poll
、epoll
等系统调用),对应内核线程收到请求后马上返回接收成功,用户线程继续进行后续操作。
2.3.1 注意事项:
1)发出IO请求不是recvform
系统调用
2)在Java NIO 中,是通过selector.select()
实现进行select
、pselect
、poll
、epoll
等系统调用
- 内核线程将原本的需要【用户线程轮询执行
recvform
系统调用】下放到自身进行,由该内核线程轮询执行recvform
系统调用,监视多个通道(socket
)上fd
对应数据报的状态。
2.3.2 监视的三种方式:
select
、poll
、epoll
- 一旦轮询检查到某个
socket
上fd
对应数据报进入读就绪状态后,后续操作和BIO相同:内核从内核态内存拷贝数据到用户态内存,成功则通知用户线程进行数据处理【这个步骤对于用户线程是阻塞的】
- 示意图:
再来个具备迷惑性的,感觉画的不对,嘿嘿~
- 适用场景:适合一段时间内连接数比较多的情况,也就是高并发的IO
- Java支持:支持,通过NIO实现的Reactor模式即是I/O多路复用模型的实现
在Java NIO中,用户线程是通过 selector.select()系统调用去查询每个通道是否有到达事件,由于轮询下放到内核线程进行,因此除非有到达事件导致用户线程阻塞外,用户线程可以正常进行后续操作。
2.4 信号驱动I/O模型(SIGIO)
- 定义:在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的
socket
注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。 - 和多路复用IO、AIO的区别:信号驱动I/O模型在IO访问的第二阶段需要用户线程主动调用IO函数来触发实际的读写操作。
- Java支持:不支持
2.5 异步非阻塞I/O模型(AIO)
- 定义:AIO相比于多路复用IO,大体流程相同,不同之处在于IO访问的第二阶段,内核将数据从内核态内存拷贝到用户态内存的过程、内核通知用户线程数据拷贝完毕 这两个过程都不会造成用户线程的阻塞。用户线程只管发出IO请求,接收到通知就做数据处理即可。
- Java支持:支持,通过AIO实现的Proactor模式即是异步I/O模型的实现
3.Java IO与序列化
3.1 介绍
- 定义:Java的IO是实现输入和输出的基础,可以方便的实现数据的输入和输出操作。
- 流的概念:在Java中把不同的输入/输出源(键盘,文件,网络连接等)抽象表述为“流”(
stream
)。
- 通过流的形式允许Java程序使用相同的方式来访问不同的输入/输出源。
- 流是一种有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象。即数据在两个设备间的传输成为流,流的本质是数据传输。
3.2 常见的流类型
- 4大抽象类基类:
- InputStream/Reader:所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流
Java IO流的40多个类都是从上述4个抽象类基类中派生出来的,其中字符流是基于字节流进行使用的
- 按照数据传输单位划分:可以划分为字节流和字符流:
- 字节流:数据流中最小的数据单元是字节
- 字符流:数据流中最小的数据单元是字符。
Java中的字符是Unicode编码,一个字符占用2个字节(无论中文还是英文都是2个字节)
- 图例:
- 按照功能划分:可以划分为节点流和处理流:
- 节点流:可以从或向一个特定的地方(节点)读写数据,直接连接数据源。
如最常见的是文件的FileReader,还可以是数组、管道、字符串,关键字分别为ByteArray/CharArray、Piped、String
- 处理流:又名包装流,并不直接连接数据源,是对一个已存在的流的连接和封装,是一种典型的装饰器设计模式。包装流隐藏了底层节点流的差异,并对外提供了更方便的输入\输出功能,让我们只关心这个高级流的操作。
使用处理流的目的是为了更方便的执行输入输出工作,如PrintStream,输出功能很强大,又如BufferedReader提供缓存机制,推荐输出时都使用处理流包装
- 图示:
- 流的链接:是指一个流对象经过其他流的多次包装
- 转换流:又名转化控制流,转换流只有字节流转换为字符流,因为字符流使用起来更方便,我们只会向更方便使用的方向转化,如:InputStreamReader与OutputStreamWriter。
注意:一个IO流可以即是输入流又是字节流又或是以其他方式分类的流类型,是不冲突的。比如FileInputStream,它既是输入流又是字节流还是文件节点流。
3.4 字节流
3.4.1 继承关系
3.4.2 字节输入流(InputStream)
java.io.InputStream
抽象类是表示字节输入流的所有类的超类(父类),可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
- 子类实例化:
InputStream is= new FileInputStream(f);
:以文件实例f创建对应输入流InputStream is= new FileInputStream("C:" + File.separator + "haha.txt");
:以文件路径创建对应输入流
- 常用方法:
public void close()
:关闭此输入流并释放与此流相关联的任何系统资源
有多个流对象要关闭时,谁最后初始化的谁就最先关,就跟JDC编程的ResultSet、PrepareStatement、Connection一样,最后初始化ResultSet的,最先就要关它。
public abstract int read()
:从输入流读取数据的下一个字节public int read(byte[] b)
:从输入流尽可能读取字节数组b长度的字节,并保存到数组b中- 该方法从数组
b
的索引为0的位置开始存储,返回的int
值代表的是读取了多少个字节,读到几个返回几个,读取不到返回-1 - 每次读取只会覆盖元素,不会清空整个字节数组!
- 该方法从数组
- 代码示例:
// 使用文件名称创建流对象.
InputStream in = new FileInputStream("test.txt");
// 定义字节数组,作为装字节数据的容器
byte[] b = new byte[2];
// 初始化一次读取的长度
int len = -1;
while ((len = in.read(b)) != -1) {
// 每次读取后,把数组的有效字节部分,变成字符串打印
// len为每次读取的有效字节个数,加上len就能避免读不满数组却把之前读的输出的情况
System.out.println(new String(b,0,len));
}
// 关闭资源
in.close();
输出结果:
ab
cd
e
3.4.3 字节输出流(OutputStream)
java.io.OutPutStream
抽象类是表示字节输出流的所有类的超类(父类),将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
- 子类实例化:
OutputStream os = new FileOutputStream(f);
:根据文件实例f创建输出流对象,如果f对应文件不存在则先创建该文件,如果存在则覆盖已有文件OutputStream os = new FileOutputStream(f, true);
:根据文件实例f创建输出流对象,如果f对应文件不存在则先创建该文件,如果存在则根据第二个参数确定是否在已有文件上追加还是覆盖已有文件- 第二个参数true为追加,false为覆盖
OutputStream os = new FileOutputStream("C:" + File.separator + "haha.txt");
:根据指定路径创建输出流对象,如果路径对应文件不存在则先创建该文件,如果存在则覆盖已有文件
- 常用方法:
public void close()
:将流中缓冲区内容强制输出到关联的系统资源中,且关闭此输出流并释放与此流相关联的任何系统资源public void flush()
:将流中缓冲区内容强制输出到关联的系统资源中public void write(byte[] b)
:将字节数组数据写入此输出流public void write(byte[] b, int off, int len)
:将字节数组中指定索引开始、指定长度的字节写入此输出流public abstract void write(int b)
:将指定1个字节写入此输出流b
是整形其长度为4字节,此处只会写入最低位的一个字节:- 例如-100,其32位二进制代码是:
11111111_11111111_11111111_10011100
,那么write(-100)
最终只会将10011100
写入输出流
- 例如-100,其32位二进制代码是:
- 需注意该方法和DataOutputStream的
writeInt(int)
方法的区别,writeInt(int)
也是接收整形数据,但是在写入时是将整个32位二进制代码都写入了输出流,即writeInt(-100)
会将11111111_11111111_11111111_10011100
写入。
- 代码示例:
OutputStream out = new FileOutputStream("test.txt");
// 写入单个字节
out.write(97);
//换行
out.write("\r\n".getBytes());
// 写入字节数组
byte[] b = new String("abcde").getBytes();
out.write(b);
out.write("\r\n".getBytes());
// 写入指定长度的字节数组
out.write(b,1,2);
// 关闭资源
out.close();
3.5 字符流
3.5.1 使用原因介绍
- 原因:因为数据编码的不同,字节流直接读取数据会有乱码的问题,因而有了对字符进行高效操作的流对象——字符流
- 本质:字符流 = 字节流 + 编码表。
- 字符流的优势:二者处理的基本单位不同,字符流是专门处理字符串的,字节流有时候不适合处理字符串,因为有些编码是变长度的。比如当你要读取流中前三个字符,字符编码又是utf-8的话,你用字节流没办法确定要读取几个字节,字符流的话直接读取三个字符就可以了。
3.5.2 继承关系
3.5.3 字符输入流(Reader)
java.io.Reader
抽象类是字符输入流的所有类的超类(父类),可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
- 子类实例化:
- 使用转换输入流:
InputStream is = new FileInputStream(f);
Reader isr = new InputStreamReader(is, "utf-8");// 编码格式要和源的编码相同
- 使用文件操作字符流:
Reader isr = new FileReader("test.txt");
- 常用方法:
public void close()
:关闭此流并释放与此流相关联的任何系统资源public int read()
:从输入流读取数据的下一个字节public int read(char[] c)
:从输入流尽可能读取字符数组c
长度的字符,并保存到数组c
中- 该方法从数组
c
的索引为0的位置开始存储,返回的int
值代表的是读取了多少个字符,读到几个返回几个,读取不到返回-1 - 每次读取只会覆盖元素,不会清空整个字符数组!
- 该方法从数组
- 代码示例:
InputStream is = null;
InputStreamReader isr = null;
try {
is=new FileInputStream(f);
isr=new InputStreamReader(is, "utf-8");
char[] c=new char[1024];
int length = -1;
while (-1 != (length = isr.read(c))) {
String str = new String(c, 0, length);
System.out.print(str);
}
} finally {// 谁最后开谁最先关
if (isr == null) isr.close();
if (is == null) is.close();
}
3.5.4 字符输出流(Writer)
- 子类实例化:
- 使用转换输出流:
OutputStream os = new FileOutputStream(f);
Writer writer = new OutputStreamWriter(os, "utf-8");// 编码格式要和源的编码相同
- 使用文件操作字符流:
Writer writer = new FileWriter("test.txt");
- 常用方法:
public void close()
:将流中缓冲区内容强制输出到关联的系统资源中,且关闭此输出流并释放与此流相关联的任何系统资源public void flush()
:将流中缓冲区内容强制输出到关联的系统资源中public void write(char[] c)
:将字符数组数据写入此输出流public void write(char[] c, int off, int len)
:将字符数组中指定索引开始、指定长度的字符写入此输出流public abstract void write(String str)
:将字符串str
写入此输出流public abstract void write(String str, int off, int len)
:将字符串str
中指定索引开始、指定长度的字符串写入此输出流
- 代码示例:
// 创建流对象
Writer writer = new FileWriter("test.txt");
writer.write(97);
writer.write('b');
writer.write("fsdfs");
writer.close();
3.6 缓冲流
- 定义:缓冲流也叫高效流,它属于包装流。
- 目的:缓存作用,加快读取和写入数据的速度
- 分类:它是四个抽象IO流的增强,所以也是4个流,按照数据类型分类。
- 字节缓冲流:BufferedInputStream、BufferedOutputStream
- 字符缓冲流:BufferedReader、BufferedWriter
- 原理:缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO读取次数,从而提高读写的效率。
BufferedInputStream内部缓存数组大小为8192
- 代码示例:参考【标准IO-综合使用案例】
3.7 序列化流
3.7.1 序列化与反序列化
- 定义:
- 序列化:把对象转化为可传输的字节序列过程称为序列化;
详细篇:把堆内存中的Java对象数据,通过某种方式存储到磁盘文件中或者传递给其他网络节点(在网络上传输),这个过程称为序列化。通俗来说就是将数据结构或对象转换成二进制串的过程。
- 反序列化:把字节序列还原为对象的过程称为反序列化;
详细篇:把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
- 序列化的应用:
- 服务器钝化:如果服务器发现某些对象好久没活动了,那么服务器就会把这些内存中的对象持久化在本地磁盘文件中(Java对象转换为二进制文件);如果服务器发现某些对象需要活动时,先去内存中寻找,找不到再去磁盘文件中反序列化我们的对象数据,恢复成Java对象。这样能节省服务器内存。
- 网络传输:所有可在网络上传输的对象都必须是可序列化的,比如RMI(
remote method invoke
,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错。 - 进程间传递对象:Linux系统中不同进程之间的java对象是无法传输的,为了实现对象在Android应用程序进程和ActivityManagerService进程之间传输,需要对对象进行序列化。
综上所述,序列化就两大应用:①创建可复用的Java对象:保存(持久化)对象及其状态到内存或者磁盘;②进行网络、进程通信
- 序列化特性:
- 序列化对象以字节数组保持其状态,却不保存静态成员
这是因为对象序列化保存的对象的【状态 】指的是它的成员变量。
- 序列化要求对象里所有的对象成员都必须实现序列化接口
- 要想将父类对象也序列化,就需要让父类也实现Serializable接口
- 序列化版本号serialVersionUID:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是 本地class文件对应类 和 待反序列化对象的对应类 两个类的序列化ID是否一致(就是
private static final long serialVersionUID
是否相同)
- 序列化版本问题:完成序列化操作后,由于项目的升级或修改,可能我们会对序列化对象对应类进行修改,比如增加某个字段,那么我们在对历史对象的序列化文件进行反序列化就会报错(因为是依据本地的class文件来反序列化的)
- 解决方式:在Java Bean中使用serialVersionUID固定当前对象版本,后续反序列化就只看待序列化对象对应类结构:
private static final long serialVersionUID = 8656128222714547171L;// ID随机
4.1 扩展:
所有实现序列化的对象都必须要有个版本号,这个版本号可以由我们自己定义,当我们没定义的时候JDK工具会按照我们对象的属性生成一个对应的版本号。其实这个版本号就和我们平常软件的版本号一样,你的软件版本号和官方的服务器版本不一致的话就告诉你有新的功能更新了,主要用于提示用户进行更新。序列化也一样,我们的对象通常需要根据业务的需求变化要新增、修改或者删除一些属性,在我们做了一些修改后,就通过修改版本号告诉 反序列化的那一方对象有了修改你需要同步修改。
3.7.2 实现Java序列化的方式
- 通过实现Serializable接口确保可序列化:
public class Person implements Serializable{
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
}
- 通过序列化输出流(ObjectOutputStream)对对象进行序列化,并使用
writeObject()
方法自定义序列化策略:
OutputStream op = new FileOutputStream("C:" + File.separator + "a.txt");
ObjectOutputStream ops = new ObjectOutputStream(op);
ops.writeObject(new Person("vae",1));
ops.close();
如果新建的Person对象没有实现Serializable接口,那么上面的操作会报错:
java.io.NotSerializableException
- 通过序列化输入流(ObjectInputStream)对对象进行反序列化,并使用
readObject()
方法自定义序列化策略:
反序列化的对象必须要提供该对象的字节码文件(.class)
InputStream in = new FileInputStream("C:" + File.separator + "a.txt");
ObjectInputStream os = new ObjectInputStream(in);
byte[] buffer = new byte[10];
int len = -1;
Person p = (Person) os.readObject();
System.out.println(p);// Person [name=vae, age=1]
os.close();
- 如何阻止变量序列化:在变量前面加上
transient
关键字:
- 例如想阻止Person对象的
age
作序列化:
transient private int age;// 不需要序列化
3.8 内存流(数组流)
3.8.1 介绍
- 定义: 内存流又叫数组流,可以分为字节内存流、字符内存流和字符串流三种
- 底层原理:把数据先临时存在数组中,也就是内存中。
所以关闭内存流是无效的,关闭后还是可以调用这个类的方法,这也是为什么内存流底层源码的
close()
方法是一个空方法。
3.8.2 字节内存流
- 分类:
ByteArrayOutputStream
、ByteArrayInputStream
- 代码示例:
// 字节数组输出流:程序---》内存
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 将数据写入到内存中
bos.write("ABCD".getBytes());
// 创建一个新分配的字节数组。 其大小是此输出流的当前大小,缓冲区的有效内容已被复制到其中。
byte[] temp = bos.toByteArray();
System.out.println(new String(temp, 0, temp.length));
byte[] buffer = new byte[10];
///字节数组输入流:内存---》程序
ByteArrayInputStream bis = new ByteArrayInputStream(temp);
int len = -1;
while ((len=bis.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
//这里不写也没事,因为源码中的 close()是一个空的方法体
bos.close();
bis.close();
3.8.3 字符内存流
- 分类:
CharArrayReader
、CharArrayWriter
- 代码示例:
// 字符数组输出流
CharArrayWriter caw = new CharArrayWriter();
caw.write("ABCD");
// 返回内存数据的副本
char[] temp = caw.toCharArray();
System.out.println(new String(temp));
// 字符数组输入流
CharArrayReader car = new CharArrayReader(temp);
char[] buffer = new char[10];
int len = -1;
while ((len=car.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, len));
}
3.8.4 字符串流
把数据临时存储到字符串中
- 分类:StringReader、StringWriter
- 代码示例:
//字符串输出流,底层采用 StringBuffer 进行拼接
StringWriter sw = new StringWriter();
sw.write("ABCD");
sw.write("帅锅");
System.out.println(sw.toString());//ABCD帅锅
//字符串输入流
StringReader sr = new StringReader(sw.toString());
char[] buffer = new char[10];
int len = -1;
while((len=sr.read(buffer))!=-1){
System.out.println(new String(buffer,0,len));//ABCD帅锅
}
3.9 管道流
3.9.1 介绍
- 定义:管道流是用来在多线程间进行信息传递的IO流,是多线程间信息传输的一种有效手段
- 分类:对应四大抽象IO流,可以分为:
- 字节/字符管道输入流:
PipedInputStream
、PipedReader
- 字节/字符管道输出流:
PipedOutputStream
、PipedWriter
- 角色:
- 管道输入流是读取者/消费者/接收者,负责从管道读取数据;
- 管道输出流是写入者/生产者/发送者,负责向管道写入数据。
后续我们只分析字符管道流,字节管道流原理跟字符管道流一样,只不过底层一个是
char
数组存储 一个是byte
数组存储的。
- 使用注意事项:
- 管道流仅用于多个线程之间传递信息,若将管道输入、输出流用在同一个线程中可能会造成死锁。
- 管道流的输入输出是成对的,这样才能建立管道。一个输出流只能对应一个输入流,使用构造方法或者
connect()
方法进行连接。 - 管道依附于线程,因此若线程结束,则虽然管道流对象还在,仍然会报错(例如
read dead end
)
- 优势和缺点:
- 优势:管道流能够保证两个通信的线程同步进行数据传递
- 缺点:管道流必须依附线程对象,当线程对象已经失效而流未关闭时会出错。
3.9.2 构造方法
- PipedReader:
public PipedReader();
:创建一个字符管道输入流实例,其循环缓冲数组使用默认为大小1024public PipedReader(int pipeSize);
:创建一个字符管道输入流实例,并指定循环缓冲数组大小为pipeSize
public PipedReader(PipedWriter pw);
:创建一个字符管道输入流实例,使其连接到字符管道输出流pw
,其循环缓冲数组使用默认为大小1024public PipedReader(PipedWriter pw, int pipeSize);
:创建一个字符管道输入流实例,使其连接到字符管道输出流pw
,并指定循环缓冲数组大小为pipeSize
创建流对象,如:
PipedWriter out = new PipedWriter(in)
,in
为 PipedReader对象,必须先实例化使用,否则会报java.lang.NullPointerException
异常。
- PipedWriter:
public PipedWriter();
:创建一个字符管道输出流实例public PipedWriter(PipedReader pr);
:创建一个字符管道输出流实例,并连接到指定字符管道输入流pr
PipedWriter out = new PipedWriter(in);
与PipedReader in = new PipedReader(out);
是等价的,开发时传递的流对象作为参数必须实例化,然后进行传递。
3.9.3 常用方法
- PipedReader:
void close()
:关闭此管道流,并释放与流相关联的任何系统资源。void connect(PipedWriter pw)
:让当前管道输入流连接到管道输出流pw
。int read()
:从管道缓冲区数组中读取下一个字符int read(char[] c, int off, int len)
:从管道缓冲区数组指定索引off
开始,读取出指定长度len
的字符boolean ready()
:告诉这个流是否准备好读取
- PipedWriter:
void close()
:当前管道输出流释放锁资源并通知管道上的所有管道输入流将缓冲区数组中的字符全部读出,之后关闭此管道流,并释放与流相关联的任何系统资源。void connect(PipedReader pr)
:让当前管道输出流连接到管道输入流pr
。void write(int b)
:向管道写入一个字符(以字节形式)void write(char[] c, int off, int len)
:从数组指定索引off
开始,将其中指定长度len
的字符写入到管道中void flush()
:当前管道输出流释放锁资源并通知管道上的所有管道输入流将缓冲区数组中的字符全部读出
out.connect(in)
和in.connect(out)
是等价的,开发时只能选择其中的一个而不能两个connect()
同时调用,否则会抛出java.io.IOException: Already connected
异常
3.9.4 底层原理分析
- 建立连接:
- PipedReader中保存着连接标志:
boolean connected = false;
PipedWriter中保存着对PipedReader的引用:private PipedReader sink;
两个类中都有connect()
方法,而其实 PipedReader中的conncet()
方法最终也是通过调用PipedWriter中的connect()
方法。 connect()
方法做的事情:- 异常检查,包括:为空检查、已连接检查、已关闭检查(通过同步机制,保证这些检查的结果是可信的)
- 建立PipedWriter对PipedReader的引用
- 初始化管道位置标记
- 将连接标志置为 true
- 写入数据:
- PipedWriter有两个
write()
方法,最终都是调用PipedReader的receive()
方法实现的(其实这很自然,因为buffer
本身就是放在PipedReader中的),所以写入数据的核心方法是synchronized void receive(int c) throws IOException
。 write()
方法是同步的。如果不同步,那么可能导致不同线程写入的数据相互覆盖write()
方法是阻塞的。当管道已满却要写入数据的时候,会首先唤醒监听当前对象锁的所有线程(当然也包括读取线程)让他们进入就绪态,准备竞争锁资源;然后让当前执行写入的线程放弃对象锁,沉睡1秒。这样一来,才有可能让该对象锁上的读取线程从管道读取数据,将管道腾出部分或者全部的空间,当前线程才能继续写入数据,结束阻塞状态。
- 读取数据:
- PipedReader有两个
read()
方法,分别是读取单个字符和多个,大同小异。 read()
方法是同步且阻塞的。当管道已空却需要读取数据时,首先唤醒当前对象锁上的其它线程(包括写入线程);然后让当前执行读取的线程沉睡1秒,放弃锁资源,这样一来才有可能让该对象锁上的写入线程写入数据,当前线程才有数据可读,结束阻塞状态。
- 写入结束和关闭管道:
- 可以从Writer或者Reader任意一方关闭管道,
closeByReader
和closeByWriter
会标记从哪一方关闭的,并且若是从Writer 方关闭的,会调用PipedReader的synchronized void receivedLast()
方法,从而唤醒所有线程。
- 总结:管道的读写操作是互相阻塞的,当缓冲区为空时,读操作阻塞。当缓冲区满时,写操作阻塞。
3.9.5 代码示例
- PipedSender:发送者:
public class PipedSender implements Runnable{
private static char[] chs = new char[]
{'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z'};
PipedWriter wr = new PipedWriter();
public PipedWriter getPipedWriter() {
return wr;
}
public void run() {
sendOne(); // 写入较短数据
sendMove();// 写入较长数据
}
/**
* 写入较短数据
*/
private void sendOne() {
try {
wr.write("this is a PipedSender");
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
if (wr != null)
wr.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
/**
* 写入较长数据
*/
private void sendMove() {
try {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100; i++) {
sb.append("0123456789");
}
sb.append(new String(chs));
String str = sb.toString();
wr.write(chs);
wr.write(str);
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
if (wr != null)
wr.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
}
- PipedReceiver:接收者:
public class PipedReceiver implements Runnable {
PipedReader re = new PipedReader();
public PipedReader getPipedReader() {
return re;
}
@Override
public void run() {
readOne(); // 读取一次
readMove(); // 全部读取
}
/**
* 读取一次数据
*/
private void readOne() {
char[] buff = new char[2048];
int len = 0;
try {
len = re.read(buff);
System.out.println("readOne : " + new String(buff,0,len));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (re != null)
re.close();
} catch(Exception e) {
e.printStackTrace();
}
}
}
/**
* 全部读取
*/
private void readMove() {
char[] buff = new char[1024];
int len = 0;
try {
while(true) {
len = re.read(buff);
if(len == -1) break;
System.out.println("readMove : " + new String(buff,0,len));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (re != null)
re.close();
}catch(Exception e) {
e.printStackTrace();
}
}
}
}
- PipedDemo:测试类:
public class PipedDemo {
public static void main(String[] args) {
PipedSender se = new PipedSender();
PipedReceiver re = new PipedReceiver();
PipedWriter out = se.getPipedWriter();
PipedReader in = re.getPipedReader();
try {
in.connect(out);// 将输入流与输出流建立连接
// 开启线程
new Thread(se).start();
new Thread(re).start();
}catch(Exception e) {
e.printStackTrace();
}
}
}
4.标准I/O
参考【System类相关】
5.Java NIO
5.1 介绍
- 所在包:
java.nio
- 内部层次结构:
- 特性:Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
5.2 基础概念
- Java NIO网络模型:
- 传统IO和NIO的区别:IO是面向流的,NIO是面向缓冲区的
- 传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
- NIO中的缓冲区:
- Java IO流每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
- NIO的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。
但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
5.3 三大核心
5.3.1 Channel(通道)
- 定义:国内大多翻译成“通道”
- 和IO中的Stream的区别:Channel和IO中的Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream、OutputStream,而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
- 主要实现:
- 对应文件IO:
FileChannel
- 对应UDP :
DatagramChannel
- 对应TCP :SocketChannel、ServerSocketChannel
5.3.2 Buffer(缓冲区)
- 定义:Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
- 网络通信中数据传输过程:
上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入Buffer中,然后将Buffer中的内容写入通道。服务端这边接收数据必须通过Channel将数据读入到 Buffer中,然后再从Buffer 中取出数据来处理。
- 主要实现:ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、DoubleBuffer、FloatBuffer、ShortBuffer
5.3.3 Selector
- NIO为什么可以单线程管理多个通道?
- Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。
- 优势:只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
九、网络编程
1.实现案例
1.1 HttpClient实现get、post请求
import com.alibaba.fastjson.JSON;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class HttpClientUtils {
/**
* 有参数请求的get请求
* @param url 请求接口
* @param paramMap 请求参数Map对象
* @return
*/
public static JSONObject getParamMap(String url, Map<String, String> paramMap) {
CloseableHttpClient httpClient = HttpClients.createDefault();
try {
List<NameValuePair> pairs = new ArrayList<>();
for(Map.Entry<String,String> entry:paramMap.entrySet()){
pairs.add(new BasicNameValuePair(entry.getKey(),entry.getValue()));
}
CloseableHttpResponse response;
URIBuilder builder = new URIBuilder(url).setParameters(pairs);
// 执行get请求.
HttpGet httpGet = new HttpGet(builder.build());
response = httpClient.execute(httpGet);
if(response != null && response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
String jsonString= EntityUtils.toString(entity);
JSONObject jsonObject=new JSONObject(jsonString);
// System.out.println("get请求数据成功!\n"+jsonObject);
return jsonObject;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭连接,释放资源
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 发送post请求,参数用map接收
* @param url 地址
* @param object 请求的对象
* @return 返回值
*/
public static JSONObject postMap(String url,Object object) {
//获取json字符串
String json= JSON.toJSONString(object);
System.out.println(json);
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(url);
CloseableHttpResponse response;
try {
StringEntity stringEntity=new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(stringEntity);
response = httpClient.execute(httpPost);
if (response != null && response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
String jsonString= EntityUtils.toString(entity);
JSONObject jsonObject=new JSONObject(jsonString);
return jsonObject;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭连接,释放资源
try {
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
十、Java注解
1.基本概念
- 定义:Java注解又称Java标注,是在JDK1.5时引入的新特性。注解也被称为元数据,是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。
- 所在包:
java.lang.annotation
- 作用:Java注解提供了一种安全的类似注释的机制,用来将任何的信息或元数据(
metadata
)与程序元素(类、方法、成员变量等)进行关联。 - 应用:
- 生成文档:这是最常见的,也是Java最早提供的注解
- 在编译时进行格式检查:如
@Override
放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出 - 跟踪代码依赖性,实现替代配置文件功能:比较常见的是spring 2.5 开始的基于注解配置,,目的就是为了减少配置
- 通过反射获取注解对象:反射的
Class
、Method
、Field
等提供了很多关于Annotation的API,方便在反射中解析并使用注解
- 本质:是接口
- 分类:可以分为标准注解和自定义注解两大类。其中标准注解又包含了元注解。
- 元注解:负责修饰其他注解的注解,JDK1.5时引入了
@Retention
、@Target
、@Inherited
、@Documented
、@Repeatable
这些元注解
2.Java标准元注解
2.1 @Target
- 作用:用于描述注解的使用范围(即:被注解的注解可以用在什么地方)
- 源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
- 关于ElementType成员:
ElementType
是一个枚举类型,它定义了被@Target
修饰的注解可以应用的范围:
ElementType.TYPE
:应用于类、接口(包括注解类型)、枚举ElementType.CONSTRUCTOR
:应用于构造方法ElementType.PARAMETER
:应用于方法的参数ElementType.FIELD
:应用于类成员或属性ElementType.METHOD
:应用于方法ElementType.PACKAGE
:应用于包ElementType.LOCAL_VARIABLE
:应用于局部变量ElementType.TYPE_PARAMETER
:JDK1.8新增,应用于类型变量ElementType.TYPE_USE
:JDK1.8新增,应用于任何使用类型的语句中
2.2 @Retention
- 作用:@Retention用来定义该注解在哪一个级别可用,在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME)
- 源码:
@Documented@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
- 关于RetentionPolicy成员:
RetentionPolicy
是一个枚举类型,它定义了被@Retention
修饰的注解所支持的保留级别:
RetentionPolicy.SOURCE
:此注解类型的信息只会记录在源文件中,编译时将被编译器丢弃,也就是说不会保存在编译好的.class
文件中RetentionPolicy.CLASS
:编译器将注解记录在.class
文件中,但不会加载到JVM中。如果一个注解声明没指定范围,则系统默认值就是RetentionPolicy.CLASS
。RetentionPolicy.RUNTIME
:注解信息会保留在源文件、.class
文件中,在执行的时也加载到Java的JVM中,因此可以使用反射读取。
- 周期大小排序:
SOURCE(源码)
<CLASS (字节码)
<RUNTIME(运行)
2.3 @Inherited
- 作用:如果一个类用上了
@Inherited
修饰的注解,那么其子类也会继承这个注解。 - 无法继承的两个场景:
- 接口上用
@Inherited
修饰的注解,其实现类不会继承这个注解; - 父类的方法用了
@Inherited
修饰的注解,子类不会继承这个注解;
2.4 @Documented
@Documented
修饰的注解在生成java doc文档信息的时候被保留,从而对类作辅助说明
2.5 @Repeatable
- 作用:被
@Repeatable
修饰的注解可以在同一处重复使用,且都归属于@Repeatable
其值指定的注解(以下简称【父注解】) 的数组成员。 - 源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
Class<? extends Annotation> value();
}
- 使用注意事项:
@Repeatable
值中指定的注解(以下简称【父注解】) 需要声明一个名为value
、@Repeatable
所修饰的注解类型 的数组成员- 父注解的作用域必须大于等于子注解
- 父注解的周期必须小于等于子注解(注意:
SOURCE(源码)
<CLASS(字节码)
<RUNTIME(运行)
)
- 代码示例:
- 可重复使用的注解:视作子注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
/*指定的注解中必须声明名称为value的Person数组成员,否则编译报错*/
@Repeatable(Persons.class)
public @interface Person {
// 人的角色
String role();
}
- 归属注解:视作父注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Persons {
Person[] value();
}
- 测试类:
@Person(role = "学生")
@Person(role = "父亲")
@Person(role = "儿子")
public class PersonTest {
public static void main(String[] args) {
Person[] persons = PersonTest.class.getAnnotationsByType(Person.class);
for (Person person : persons) {
System.out.println("直接通过@Person获取:" + person);
}
System.out.println("================================================");
// 哦豁,没看到用@Persons修饰呀,怎么也能获取呢?
Persons annotation = PersonTest.class.getAnnotation(Persons.class);
Person[] value = annotation.value();
for (Person person : value) {
System.out.println("通过@Persons获取:" + person);
}
}
}
运行结果:
直接通过@Person获取:@annotation.Person(role=学生)
直接通过@Person获取:@annotation.Person(role=父亲)
直接通过@Person获取:@annotation.Person(role=儿子)
================================================
通过@Persons获取:@annotation.Person(role=学生)
通过@Persons获取:@annotation.Person(role=父亲)
通过@Persons获取:@annotation.Person(role=儿子)
通过测试代码,我们不禁好奇,明明没有在测试类上声明@Persons注解,怎么还能反射获取到呢?
编译后的.class告诉了答案:
/* 看到没,这边编译后优化成了父注解的形式,所以你不嫌麻烦也可以用这个方式声明多个@Person注解 */
@Persons({@Person(
role = "学生"
), @Person(
role = "父亲"
), @Person(
role = "儿子"
)})
public class PersonTest {
public PersonTest() {
}
public static void main(String[] args) {
Person[] persons = (Person[])PersonTest.class.getAnnotationsByType(Person.class);
Person[] var2 = persons;
int var3 = persons.length;
for(int var4 = 0; var4 < var3; ++var4) {
Person person = var2[var4];
System.out.println("直接通过@Person获取:" + person);
}
System.out.println("================================================");
Persons annotation = (Persons)PersonTest.class.getAnnotation(Persons.class);
Person[] value = annotation.value();
Person[] var10 = value;
int var11 = value.length;
for(int var6 = 0; var6 < var11; ++var6) {
Person person = var10[var6];
System.out.println("通过@Persons获取:" + person);
}
}
}
3.Java其他标准注解
3.1 @Override
检查该方法是否是重写方法,如果发现其父类(接口)并没有该方法时,会编译报错。
3.2 @Deprecated
用于标明被修饰的类或类成员、类方法已经废弃、过时,不建议使用
3.3 @SuppressWarnings
- 作用:用于关闭对类、方法、成员编译时产生的特定警告。
- 可关闭的警告对应参数及说明:
deprecation
:使用了不赞成使用的类或方法时的警告unchecked
:执行了未检查的转换时的警告,例如当使用集合时没有用泛型(Generics)来指定集合保存的类型fallthrough
:当switch
程序块直接通往下一种情况而没有break
时的警告path
:在类路径、源文件路径等中有不存在的路径时的警告serial
:当在可序列化的类上缺少serialVersionUID
定义时的警告finally
:任何finally
子句不能正常完成时的警告all
:所有的警告
3.4 @FunctionalInterface
- 作用:用于指示被修饰的接口是函数式接口,在JDK 1.8引入
- 特性:
@FunctionalInterface
只能用于注解接口而不能用在class以及枚举上- 被
@FunctionalInterface
修饰的符合规则的接口, 可以用Lambda表达式。
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。更多函数式接口使用详见【函数式编程】
4.自定义注解
如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处器。Java SE5 扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。下面实现一个注解处理器:
- 定义注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
- 使用注解:
public class Apple {
@FruitProvider(id = 1, name = "老巫婆", address = "法拉纳大森林")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
}
}
- 注解处理器:
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
/注解信息的处理地方
trFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
}
}
- 测试类:
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:老巫婆 供应商地址:法拉纳大森林
}
}
十一、Java函数式编程
1.介绍
- 函数式编程:函数式编程是一种编程范式,即一切都是数学函数。在Java面向对象编程中,程序是一系列相互作用(方法)的对象,而在函数式编程中,程序会是一个无状态的函数组合序列。
- 函数是“第一等公民”:“第一等公民”指的是函数和其他数据类型一样,处于平等的地位。可以赋值给变量、可以作为另一个函数的参数或者作为一个函数的返回值。
- 函数式编程和链式编程的区别:
- 链式编程思想:是将多个操作(多行代码)通过点号(
.
)链接在一起成为一句代码,使代码可读性好 - 函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用
2.默认方法
2.1 介绍
- 目的:为了解决修改接口的抽象方法或新增抽象方法需要修改所有的已有实现的问题。
- 定义:默认方法是接口自己的实现方法,而且不强制要求实现类去实现其方法。其特征是default前缀
注意和缺省的访问修饰符进行区分
- 语法结构:
[@FunctionalInterface]
public interface 接口名 {
[public ]default 返回类型 方法名称(参数类型 参数名...){
//具体实现
}
}
2.2 特性
- 一个类实现单一接口时,如果该接口不存在抽象方法(Object类的方法声明不算),可以直接空实现:
interface Poll {
public default void print() {
System.out.println("我是一辆车!");
}
@Override
public abstract boolean equals(Object obj);
}
//此处直接空实现
class Lop implements Poll {}
- 一个类实现多个接口时,如果这些接口存在复数的default方法,实现类必须重写该方法,否则报错:
具体想用哪个接口的default方法可以使用
接口名.super.方法名
的形式来调用
//以下两个接口都没有抽象方法,并非函数式接口!
interface Vehicle {
public default void print() {
System.out.println("我是一辆车!");
return ;
}
}
interface FourWheeler {
default void print() {
System.out.println("我是一辆四轮车!");
}
}
//此处Car报错:Duplicate default methods named print with the parameters () and () are inherited from the types FourWheeler and Vehicle
//class Car implements Vehicle, FourWheeler {}
class Car implements Vehicle, FourWheeler {
public void print() {
Vehicle.super.print();
FourWheeler.super.print();
System.out.println("我是一辆汽车!");
}
}
- 一个类可以实现的多个接口必须满足方法名相同的复数个抽象方法(Object类的方法不算)或
default
方法,其返回值类型也要相同,否则报错:
原因:因为第2点说了必须要实现。
可能你会觉得再实现一个不同类型的方法不就ok了?但是记住这是Java不是C++。
//以下两个接口都没有抽象方法,并非函数式接口!
interface Vehicle {
default String print() {
System.out.println("我是一辆车!");
return "";
}
}
interface FourWheeler {
default void print() {
System.out.println("我是一辆四轮车!");
}
}
class Car implements Vehicle, FourWheeler {
//即使print方法内部已经不用Vehicle的print了,依旧报错:print方法返回值类型不兼容Vehicle的print方法
public void print() {
FourWheeler.super.print();
FourWheeler.super.print();
System.out.println("我是一辆汽车!");
}
//直接报重复java方法的错误好吧,这不是C++
//public String print() {}
}
3.Lambda表达式
3.1 介绍
- 定义:Lambda表达式是JDK1.8推出的新特性,符合函数式编程的要求,其主要目的是为了简化函数式接口实例化的方式,代替了以往需要匿名类来实例化的场景。
- 语法结构:
- Lambda表达式作为参数:不能以
;
结尾:
(parameters) -> expression
// 或
(parameters) -> {statements1;statements2;...;statementsN;}
其中
parameters
对应接口抽象方法的参数,expression
是表达式(仅有一条),statements
为执行语句(0-n条语句),例子如下:
// 符合语法1
Thread thread = new Thread(() -> System.out.println("zz"));
// 符合语法2
Thread thread = new Thread(() -> {System.out.println("zz");});
- Lambda表达式作为赋值要素构成一条语句:必须以
;
结尾,具体例子第二小节将会介绍:
(parameters) -> statements;
// 或
(parameters) ->{statements1;statements2;...;statementsN;};
- Lambda表达式和内部类的区别:以Runnable这个线程接口为例:
- 使用匿名类的形式实例化Runnable:
Thread thread = new Thread(new Runnnable(){
@Override
public void run() {
System.out.println("zz");
}
})
- 使用Lamba形式实例化Runnable:
Thread thread = new Thread(() -> System.out.println("zz"));
// 或
Thread thread = new Thread(() -> {System.out.println("zz");});
相比与匿名类实例化,使用Lambda表达式要简化许多。
- 适用范围:只能实例化函数式接口
这也是Lambda表达式实例化接口的局限性
3.2 特性
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
- 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
- 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
- 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。
- Lambda表达式的右半部分表达式或语句中能引用的外部局部变量必须是
final
类型(或是隐式final类型,即该变量后面不允许发生更改),这点跟匿名类相同。
3.3 使用示例
public class LambdaTest {
public static void main(String[] args) {
//()省略的话直接编译报错!
Test1 test1_1 = () -> System.out.println("哈哈");
test1_1.run();//结果为哈哈
//多条语句时需要使用花括号括起来,表示其为一个语句块
Test1 test1_2_1 = () -> {System.out.println("哈哈");};
test1_2_1.run();//结果为哈哈
Test1 test1_2_2 = () -> {
System.out.println("哈哈");
System.out.println("哈哈哈哈");
};
test1_2_2.run();
//也可以是空实现
Test1 test1_2_3 = () -> {};
test1_2_2.run();
//即使是空实现,花括号也不能省略!此处->报错
//Test1 test1_2_4 = () -> ;
Test2 test2_1 = (c) -> System.out.println(c+7);
test2_1.run(1);//结果为8
//内部类实现的方法仅一个参数时,括号可有可无
Test2 test2_2 = a -> System.out.println(a+7);
test2_2.run(2);//结果为9
//可以主动声明参数的类型,但这样一来必须加括号!
Test2 test2_3 = (int k) -> System.out.println(k);
test2_3.run(3);//结果为3
//即使是只有一个参数,在你声明参数类型后也必须加上括号:此处int处编译报错!
//Test2 test2_4 = int c -> System.out.println(c+7);
String c = "haha";
//多个参数时可以不声明类型
Test3 test3_1 = (a, b) -> System.out.println(a+b);
test3_1.run(3,"4");//结果为34
//lambda表达式参数名称不能和局部变量相同!此处c报编译错误!
//Test3 test3_2 = (a,c) -> System.out.println(a+c);
Test3 test3_3 = (int a, String b) -> System.out.println(a+b);
test3_3.run(3,"5");//结果为35
//lambda表达式参数名称不能和已有局部变量相同,即使你的参数类型已经和局部变量不同,但使用的相同的名称,仍然报错!
//此处c报编译错误!
//Test3 test3_4 = (int c, String b) -> System.out.println(c+b);
//参数类型要么全部声明,要么全不声明,不能只声明一部分,即使所有参数都是一个类型。此处String报错
//Test4 test4_1 = (String a, b) -> System.out.println(a+b);
//lamba表达式只能引用final类型的局部变量,或是隐式final类型的局部变量(及该变量后续不能发生更改,否则编译报错)
Test4 test4_2 = (a,b) -> System.out.println(a+c);
test4_2.run("4","5");//结果为4haha
//如果此处给c重新赋值,则test4_2的lambda表达式就会报错,报错位置在c处。
//c="";
//只有函数式接口才能使用lambda表达式来实例化,此处等号右边编译报错!
//Test5 test5 = () -> {};
//三种形式使用runnable实例初始化Thread
Thread thread1 = new Thread(new Runnable(){
@Override
public void run() {
System.out.println("傻逼");
}
});
Thread thread2 = new Thread(() -> {System.out.println("哈哈");});
Thread thread3 = new Thread(() -> System.out.println("哈哈"));
//;处报错,lambda表达式作为参数时不能以;结尾!
//Thread thread4 = new Thread(() -> System.out.println("哈哈"););
}
interface Test1 {
public abstract void run();
}
interface Test2 {
public abstract void run(int a);
}
interface Test3 {
public abstract void run(int b, String c);
}
//函数式接口
@FunctionalInterface
interface Test4 {
public static String kk1 = "k";
public final String kk2 = "k";
public abstract void run(String b, String c);
public default void test(){
}
public static void test2() {
}
@Override
public abstract boolean equals(Object obj);
}
abstract class Test5 {
public void run() {
System.out.println("哈哈哈");
}
public abstract void test();
}
}
4.函数式接口
4.1 介绍
- 定义:仅有一个抽象方法的接口,是JDK1.8的新特性。
可以有默认方法
- 所在包:
java.util.function
- 语法结构:
[@FunctionalInterface]
interface 接口名
{
//可以定义公共静态变量
[public ]static 成员类型 成员名 = 成员值;
//可以定义公共常量
[public ]final 成员类型 成员名 = 成员值;
[[public ]abstract ]返回类型 方法名(参数类型 参数名...);
//可以定义公共静态方法(必须是public,即使省略了默认也是public)
[[public ]static 返回类型 方法名(参数类型 参数名...);]
//可以定义默认方法(必须是public,即使省略了默认也是public)
[[public ]default 返回类型 方法名(参数类型 参数名...);]
//可以用抽象方法的形式声明Object类的方法(@Override注解必须得加),但这些方法不被视作是抽象方法
[@Override
[[public abstract] boolean equals(Object obj);
]
}
- 其中
@FunctionalInterface
注解仅仅只是对当前接口的内容在编译阶段做一个校验,判断其是否符合函数式接口的标准。
没有该注解,只要该接口符合函数式接口的标准依旧是函数式接口。
- 实例化方式:函数式接口可以使用Lambda表达式和匿名类进行实例化。
- 特性:
- 函数式接口里允许定义默认方法(默认
public
类型) - 函数式接口里允许定义静态方法(默认
public
类型) - 函数式接口里允许定义
java.lang.Object
里的public
方法
原因前文Comparator接口说过,所有类都是Object的子类,因此可以声明Object类内部方法。
- 使用示例:
public class LambdaTest {
public static void main(String[] args) {
String c = "haha";
Test4 test4_2 = (a,b) -> System.out.println(a+c);
test4_2.run("4","5");// 结果为4haha
}
@FunctionalInterface
interface Test4 {
public static String kk1 = "k";
public final String kk2 = "k";
public abstract void run(String b, String c);
public default void test(){}
public static void test2() {}
@Override
public abstract boolean equals(Object obj);
}
}
- 六大函数式接口:
接口 | 参数 | 返回类型 | 表述 |
---|---|---|---|
Function<T, R> | T | R | 入参是T,出参是R的函数 |
Predicate | T | boolean | 用于判断操作函数 |
Supplier | T | 生成一个对象T的函数 | |
Consumer | T | void | 没有返回结果的函数 |
UnaryOperator | T | T | 入参、出参都是T的类型函数 |
BinaryOperator | (T,T) | T | 接收两个入参为T,出参也为T的函数 |
- 自定义通用函数式接口:
- 接口:入参是
T
类型的不定参,返回值是R
@FunctionalInterface
public interface FunctionPlus<T, R> {
R apply(T ... kk);
}
- 测试类:
public class FunctionPlusTest {
public static void main(String[] args) {
FunctionPlus<Integer, List<Integer>> func = Arrays::asList;
func = kk -> {
List<Integer> res1 = new ArrayList<>();
Collections.addAll(res1, kk);
return res1;
};
System.out.println(func.apply(1,2,3,4));
}
}
4.2 Function<T, R>接口
4.2.1 介绍
- 定义:函数型接口:有入参
T
,有返回值R
- 源码:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
...
}
- 应用示例:作为Optional类的
map()
方法参数 - 相关子接口:ToIntFunction、ToDoubleFunction、ToLongFunction
4.2.2 ToIntFunction接口
首次使用:Comparator比较器类
comparingInt()
方法
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}
4.3 Predicate<T>
接口
- 定义:断定型接口:有入参
T
,返回布尔值 - 用途:常用作集合类型的流的过滤条件
- 源码:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}
- 应用示例:作为Optional类的
filter()
方法参数
4.4 Supplier<T>
接口
- 定义:供给型接口:没有参数,只有返回值
T
。 - 用途:用来存储数据(或者是产生数据的规则),然后可以供其他方法使用
- 源码:
public interface Supplier<T> {
T get();
}
- 应用示例:作为Optional类的
orElseGet()
方法参数
4.5 Consumer<T>
接口
- 定义:消费型接口:有入参,没有返回值。
- 源码:
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
}
- 应用示例:作为Optional类的
ifPresent()
方法参数
4.6 UnaryOperator接口和BinaryOperator接口
都是Function接口的加强版,不作说明
5.方法引用
5.1 介绍
- 定义:方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法的语法范式,是一种语法糖,其本质其实是简化版的Lambda表达式。
- 语法:使用
::
- 不同方法类型对应的方法引用形式:
类型 | 方法引用 | 对应的Lambda表达式 |
---|---|---|
构造方法引用 | 类名::new | (args) -> new 类名(args) |
静态方法引用 | 类名:: 静态方法名 | (args) -> 类名.静态方法名(args) |
实例方法引用 | 类名::方法名 | (inst,args) -> inst.method(args) |
对象方法引用 | 对象::方法名 | (args) -> 对象.method(args) |
需注意对象方法引用中的对象指的是外部的静态对象(或隐式final类型,即该对象后面不允许发生更改)
假设存在类Person,代码如下:
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void say(Person person) {
System.out.println(person.getName());
}
public void equals(Person person) {
System.out.println(this.getName().equals(person.getName()));
}
public void eat(String food) {
System.out.println("eat " + food);
}
}
5.2 构造方法引用
Supplier<Person> supplier = Person::new;
// Lambda表达式写法
Supplier<Person> supplier = ()-> new Person();
5.3 静态方法引用
Consumer<Person> say = Person::say;
// Lambda表达式写法
Consumer<Person> say = person -> Person.say(person);
5.4 实例方法引用
BiConsumer<Person, Person> personPersonBiConsumer = Person::equals;
// Lambda表达式写法
BiConsumer<Person, Person> personPersonBiConsumer = (inst, args) -> inst.equals(args);
5.5 对象方法引用
Person person = new Person();
Consumer<String> eat = person::eat;
// Lambda表达式写法
Consumer<String> eat = food -> person.eat(food);
6.Optional<T>
类
6.1 介绍
- 定义:Optional 是个容器:它可以保存类型
T
的值,或者仅仅保存null - 底层数据结构:内部储存了一个真实的值,在构造的时候,就直接判断其值是否为空:
public final class Optional<T> {
// value为null的一个空实现
private static final Optional<?> EMPTY = new Optional<>();
// 内部储存的值
private final T value;
// 构造方法私有化,且无参构造方法默认value为null
private Optional() {
this.value = null;
}
// 有参构造函数要求入参value不得为null,否则报空指针异常
private Optional(T value) {
this.value = Objects.requireNonNull(value);
}
}
6.2 常用方法
6.2.1 创建Optional实例
Optional.empty()
:返回一个value为null
的Optional实例:
- 源码:
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
Optional.of(a)
:返回一个value为a
的Optional实例。如果a
为null
,则报空指针异常
- 源码:
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
Optional.ofNullable(a)
:返回一个value为a
的Optional实例。如果a
为null
,则返回一个EMPTY
实例;否则返回一个有值的实例
- 源码:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
6.2.2 获取Optional实例的value
T get()
:如果Optional实例中value不为null
,则返回value,否则抛出异常:NoSuchElementException
- 源码:
public T get() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
T orElse(T other)
:如果Optional实例中value不为null
,则返回value, 否则返回other。
注意,无论是否为空,
other
对应的语句必定执行→值传递
- 源码:
public T orElse(T other) {
return value != null ? value : other;
}
T orElseGet(Supplier<? extends T> other)
:如果Optional实例中value不为null
,则返回value, 否则调用Supplier实例other
的get()
方法。
- 源码:
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
<X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
:如果value不为null
则返回value,否则调用Supplier实例的get()
方法抛出异常。
- 源码:
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
6.2.3 加工Optional实例
Optional<T> filter(Predicate<? super <T> predicate)
:如果value不为null,且匹配predicate
的条件,则返回该Optional实例,否则返回EMPTY
实例
- 源码:
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
<U> Optional<U> map(Function<? super T, ? extends U> mapper)
:如果value为null
,则返回一个EMPTY
实例;否则对value使用映射函数加工,将返回结果用Optional封装后返回。
mapper
映射函数实例不得为null
,否则报空指针异常。
- 源码:
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));
}
}
<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper)
:如果value为null
,则返回一个EMPTY
实例;否则对value使用映射函数加工,并返回处理结果。
1)
mapper
映射函数实例不得为null
,否则报空指针异常。
2)映射函数的处理结果不得为null
,否则报空指针异常。
- 源码:
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
void ifPresent(Consumer<? super T> consumer)
:如果value不为null
则使用value调用consumer
, 否则不做任何事情
- 源码:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
boolean isPresent()
:如果value不为null
则返回true
,否则返回false
。
- 源码:
public boolean isPresent() {
return value != null;
}
6.2.4 重写Object的方法
boolean equals(Object obj)
:判断两个Optional实例的value或实例的地址是否相等。如果obj
不是Optional类型,直接返回false
。
- 源码:
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Optional)) {
return false;
}
Optional<?> other = (Optional<?>) obj;
return Objects.equals(value, other.value);
}
int hashCode()
:返回实例的value的hash值,如果value为null
则返回0
。
- 源码:
public int hashCode() {
return Objects.hashCode(value);
}
String toString()
:返回实例的字符串
- 源码:
public String toString() {
return value != null
? String.format("Optional[%s]", value)
: "Optional.empty";
}
6.3 使用示例
以水果礼盒为例
- 水果类:
public class Fruit {
// 构造次数
public static int createTimes = 0;
private Optional<String> status;
private int id;
public Fruit(Optional<String> status, int id) {
this.status = status;
this.id = id;
createTimes++;
System.out.println("Fruit全参构造方法已调用" + createTimes + "次!");
}
public Fruit() {
createTimes++;
System.out.println("Fruit无参构造方法已调用" + createTimes + "次!");
}
public void setStatus(Optional<String> status) {
this.status = status;
}
public void setId(int id) {
this.id = id;
}
public Optional<String> getStatus() {
return status;
}
public int getId() {
return id;
}
}
- 礼盒类:
public class FruitBox {
// 构造次数
public static int createTimes = 0;
private Optional<Fruit> fruit;
public FruitBox(Optional<Fruit> fruit) {
this.fruit = fruit;
createTimes++;
System.out.println("FruitBox全参构造方法已调用" + createTimes + "次!");
}
public FruitBox() {
createTimes++;
System.out.println("FruitBox无参构造方法已调用" + createTimes + "次!");
}
public Optional<Fruit> getFruits() {
return fruit;
}
public void setFruits(Optional<Fruit> fruit) {
this.fruit = fruit;
}
}
- 测试类:
public class OptinalTest {
public static void main(String[] args) {
System.out.println("验证orElse方法特性:======================");
Optional<FruitBox> fruitBoxOptional = Optional.ofNullable(new FruitBox());
// 这条编译不通过!原因见下文
// Optional<Fruit> fruitOptional = fruitBoxOptional.map(FruitBox::getFruits);
// 通过map方法源码可知,是将fruitBoxOptional中值作为入参,并将Function返回结果用Optional包装起来,就出现了下面这个奇葩
Optional<Optional<Fruit>> fruitOptional = fruitBoxOptional.map(FruitBox::getFruits);
// 问题1:要获取FruitBox实例里最里层的Fruit成员怎么办?
// 错误方法1:使用flatMap:直接运行报空指针异常:因为最初fruitBoxOptional就是无参实例化,
// FruitBox::getFruits获得的只能是null值的Optional<Fruit>实例,由源码可知直接被Objects.requireNonNull()抛空指针异常了
// Fruit a = fruitBoxOptional.flatMap(FruitBox::getFruits).orElse(new Fruit());
// 正确方法2:使用map+orElse:正确运行~
Fruit a = fruitBoxOptional.map(FruitBox::getFruits).orElse(Optional.of(new Fruit())).orElse(new Fruit());
/**
* 截止到目前为止,运行结果:
*
* FruitBox无参构造方法已调用1次!
* Fruit无参构造方法已调用1次!
* Fruit无参构造方法已调用2次!
*/
// 优化方法3:使用map+orElseGet:相比于orElse(),orElseGet()只会在value确实为空时才执行其中Supplier实例的get()方法,性能更佳
FruitBox fruitBox = new FruitBox(fruitBoxOptional.map(FruitBox::getFruits).orElseGet(() -> Optional.of(new Fruit())));
System.out.println("验证orElse方法特性:======================");
/* 为方便测试此处将FruitBox和Fruit构造次数清零 */
FruitBox.createTimes = 0;
Fruit.createTimes = 0;
Fruit b = Optional.ofNullable(fruitBox).map(FruitBox::getFruits).orElseGet(() -> Optional.of(new Fruit())).orElseGet(Fruit::new);
/**
* 截止到目前为止,运行结果:
*
* 验证orElse方法特性:======================
* FruitBox无参构造方法已调用1次!
* Fruit无参构造方法已调用1次!
* Fruit无参构造方法已调用2次!
* Fruit无参构造方法已调用3次!
* FruitBox全参构造方法已调用2次!
* 验证orElse方法特性:======================
*/
// 由此可见,value不为空,不会执行Supplier实例的get()方法
// 问题2:如何过滤掉没状态的Fruit(即Fruit实例的status为null的要过滤掉)
// 正确方法:使用filter()方法过滤掉status为null的水果
b = Optional.of(b).filter(x -> Optional.ofNullable(x.getStatus()).orElseGet(() -> Optional.ofNullable(null)).isPresent()).orElseGet(()->null);
System.out.println("水果b过滤后为:" + b);
}
}
7.流式编程(Stream<T>
接口)
7.1 介绍
- 定义:JDK1.8添加了一个新的抽象称为流Stream,其唯一实现是ReferencePipeline(管道)。
- 作用:将集合(Collection)、数组(Array)、I/O channel、产生器generator看作是一种流,并使其在管道中传输、处理。
流是一种元素序列
- 数据流的来源:集合(Collection)、数组(Array)、I/O channel、产生器
generator
(算法、函数) - 流式编程的三大特征:
- Pipelining:中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。同时使用了责任链模式,使得一个Stream可以轻易转换为另一个Stream。
这样做可以对操作进行优化, 比如延迟执行(laziness)和短路(short-circuiting)。
- 惰性计算:真正的计算通常只发生在最后的结果获取时。流式计算存在第一步、中间一步、最后一步的说法,只有当到达最后一步执行函数的时候,整个惰性函数才会执行。
4.1 关于懒惰节点(中间节点、转换方法)和终值节点(聚合方法):
1)代码后续没有终值节点,只有懒惰节点,则懒惰节点不执行;代码后续有终值节点,前面有懒惰节点,则按顺序执行懒惰节点、终值节点。
// 只用了懒惰节点filter()
public static void main(String[] args) {
List list = new ArrayList();
Stream.of(list).filter(a -> {
System.out.println("hello");
return true;
});// 没有打印结果
}
// 除了使用了懒惰节点filter(),最后还使用了终值节点toArray()
public static void main(String[] args) {
List list = new ArrayList();
Stream.of(list).filter(a -> {
System.out.println("hello");
return true;
}).toArray();// 打印输出hello
}
2)区分中间节点与终值节点的方法:进入Stream流的源码,按快捷键ctrl+f12,只要方法返回的是Stream,基本都是中间节点,其余就是终值节点
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式,显式的在集合外部进行迭代,这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
注意!流和迭代器类似,只能迭代一次!!
Stream<String> stream = list.stream().map(Person::getName).sorted().limit(10);
List<String> newList = stream.collect(Collectors.toList());
// 第三行会报错,因为第二行已经使用过这个流,这个流已经被消费掉了。
List<String> newList2 = stream.collect(Collectors.toList());
- 流式编程的好处:
- 使用类似sql语句的方式,可以更加直观的表达Java集合的运算
- Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
- 流无存储:流不是一种数据结构,流只是提供了一种数据视图
- Stream和InputStream/OutputStream的区别:
- 包不同:Stream在
java.util.stream
包中,InputStream/OutputStream在java.io
包中 - 功能不同:Stream代表的是任意Java对象的序列,InputStream/OutputStream代表的是数据流。
- Stream和List的区别:
- Stream不是 List,List中存储的元素是事先存在于内存中的Java对象,而Stream输出的元素可能并没有预先存储在内存中,而是通过实时计算出来的惰性对象。
- 其次,Stream在理论上能容纳无限对象,List不能。
- 三种基本类型流是什么?其出现的目的是什么?
- 分别是
IntStream
、LongStream
、DoubleStream
- 目的:
- 在Java中,因为Java泛型不支持基本类型,所以我们无法使用像
Stream<int>
这样的形式来保存int
,只能采用形如Integer
这样的形式。但是频繁装箱、拆箱操作会牺牲编译器的大量性能。 - 所以为了提高效率,Java标准库提供了三种使用基本类型的Stream,它们的使用和标准的Stream没有太大区别,直接使用:
- 在Java中,因为Java泛型不支持基本类型,所以我们无法使用像
// 1.
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 2. 将Stream<String>转换为LongStream:
LongStream s=List.of("1").stream().mapToLong(Long::parseLong);
7.2 常用方法
7.2.1 流的生成
of()
:也就是值创建流,是创建流的最简单的方法,其底层源码实际就是调用的Arrays.stream()
。
Stream<String> stream = Stream.of("a","b","c");
stream.forEach(System.out::println);
stream()
:为集合创建串行流。
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList())
parallelStream()
:为集合创建并行流,是流并行处理程序的代替方法。
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
Arrays.stream()
:数组创建流
Stream<String> s = Arrays.stream(new String[] {"A"});
此外,还可以规定只取数组的某部分,用到的是Arrays.stream(T[], int, int)
,且区间左闭右开:
只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);
// 打印 2 ,3
- 函数生成流:
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
:seed为首元素,该方法会依次对每个新生成的值应用函数:
//生成流,首元素为 0,之后依次加 2
Stream.iterate(0, n -> n + 2)
public static<T> Stream<T> generate(Supplier<T> s)
:基于Supplier实例创建的Stream实例会不断调用Supplier.get()
方法来生成下一个元素,这种Stream中数据源不是集合,而是算法:
Stream<Integer> my = Stream.generate(new MySup());
my.limit(10).forEach(System.out::println);
// 不断生成自然数的Supplier(范围在Integer之内)
class MySup implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
Stream.concat()
合并流:
Stream<String> s1 = ... ;
Stream<String> s2 = ... ;
Stream<String> s3 = Stream.concat(s1,s2);
- 其他方式:
- 文件生成流:Files类的
lines()
方法,常用于遍历文本文件。每个元素是给定文件的其中一行
Stream<String> stream = Files.lines(Paths.get("data.txt"));
- 正则表达式Pattern对象存在
splitAsStream()
方法,可以直接把一个长字符串分割成Stream序列而不是数组。
7.2.2 中间操作
Stream<T> filter(Predicate<? super T> predicate)
:用于通过设置的条件过滤出元素,其参数为Predicate<T>
接口实例,且泛型和流中元素类型属于同一继承树。
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
// 获取空字符串的数量
long count = strings.stream().filter(string -> string.isEmpty()).count();
Stream<T> sorted()
、Stream<T> sorted(Comparator<? super T> comparator)
:用于将元素进行顺序排列,支持无参(元素需实现Comparable<T>
接口)和Comparator<? super T>
接口 实例两类参数。
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
:用于映射每个元素到对应的结果,内部参数为Function<T,R>
实例。
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
// 获取对应的平方数
List<Integer> squaresList = numbers.stream().map(i -> i * i).distinct().collect(Collectors.toList());
Stream<T> limit(long maxSize)
:尽可能获取流中期望个数的元素(期望个数与数据源中元素个数无关,与数据源是否是集合或者是算法无关),并组成新的流。
Random random = new Random();
random.ints().limit(10).forEach(System.out::println);
【注意!】对于无限元素,如果直接调用
forEach()
或者count()
求最终值,会直接进入死循环,因为无限序列永远不可能被计算完。所以我们需要先将起转变为有序序列,例如limit(100)
。
Stream<T> distinct()
:保留流中的非重复元素,并组成新的流。
元素相等是通过
equals()
方法来判断的。
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1,7,3,5,9,2,4,5);
nums.stream().limit(100).distinct().sorted().forEach(System.out::print);// 结果1234579
}
Stream<T> skip(long n)
:尽可能去除流中的前n个元素,并将后续元素组成新的流。
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1,7,3,5,9,2,4,5);
nums.stream().limit(100).skip(3).distinct().sorted().forEach(System.out::print);// 结果2459
}
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
:将流中的每一个元素转化为一个流,再把每一个流连接成为一个新的流。
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());
Stream<T> peek(Consumer<? super T> action)
:对元素使用action
实例消费,不改变元素本身。和foreach()
的唯一区别在于一个是中间操作一个是最终操作:
public class StreamTest {
private static List<AppleStore> appleStores = new ArrayList();
static {
appleStores.add(new AppleStore(1,"red",1.2,"重庆"));
appleStores.add(new AppleStore(2,"red",1.5,"四川"));
appleStores.add(new AppleStore(3,"yellow",2.6,"重庆"));
appleStores.add(new AppleStore(4,"yellow",2.9,"四川"));
appleStores.add(new AppleStore(5,"red",3.2,"杭州"));
}
public static void main(String[] args) {
// peek()是执行每个节点的方法
// peek里的方法会挨个执行,也就是节点会挨个执行,也称作责任链模式
appleStores.stream().peek(appleStore -> System.out.println(appleStore.getColor()))// 打印苹果的颜色
.peek(appleStore -> System.out.println(appleStore.getWeight()))// 打印苹果的重量
.peek(appleStore -> appleStore.getOrigin())// 打印苹果的产地
.toArray();
}
}
Stream<T> unordered()
:生成一个与当前流等效的无序流。
如果流本身就是无序的话,那可能就会直接返回其本身。实际应用中发现该方法无论如何都会返回原本的流本身,无卵用。
7.2.3 最终操作
7.2.3.1 终值操作
void forEach(Consumer<? super T> action)
:和peek()
类似,对元素使用action
实例消费,不改变元素本身:
Random random = new Random();
random.ints().limit(10).sorted().forEach(System.out::println);
boolean anyMatch(Predicate<? super T> predicate)
:流中是否有一个元素匹配给定的条件,如果有则返回true:
boolean b = list.stream().anyMatch(person -> person.getAge() == 20);
boolean allMatch(Predicate<? super T> predicate)
:流中是否所有元素都匹配给定的条件,如果有则返回true:
boolean result = list.stream().allMatch(Person::isStudent);
boolean noneMatch(Predicate<? super T> predicate)
:流中是否没有元素匹配给定的条件,如果有则返回true:
boolean result = list.stream().noneMatch(Person::isStudent);
Optional<T> findAny()
:找到其中一个元素。
- 普通流找到的是第一个元素;
- 并行流找到的是其中一个元素;
Optional<T> findFirst()
:找到第一个元素。
7.2.3.2 采集操作
<R, A> R collect(Collector<? super T, A, R> collector)
:属于Stream的采集功能,通常配合Collectors.toList()
、Collectors.toSet()
、Collectors.toCollection(TreeSet::new)
、Collectors.toCollection(ArrayList::new)
、Collectors.groupingBy()
这几种方法使用。
List<Integer> nums = Arrays.asList(1,7,3,5,9,2,4,5);
List<Integer> newNums = nums.stream().limit(100).skip(3).distinct().sorted().unordered().collect(Collectors.toList());
System.out.println(newNums);// [2, 4, 5, 9]
关于Collectors类的使用详见【Java技术栈 1-2 Java进阶】第一章
7.2.3.3 归约操作
- 归约是将集合中的所有元素经过指定运算,折叠成一个元素输出,如:求最值、平均数等,这些操作都是将一个集合的元素折叠成一个元素输出。
- 在Stream中,reduce方法能实现归约,主要有以下三个方法:
Optional<T> reduce(BinaryOperator<T> accumulator)
:
1)accumulator
:累加器,用于将流中的元素进行归约操作。通常使用Lambda表达式来声明该实例。
2)BinaryOperator<T>
是函数式接口,继承于BiFunction<U,T,R>
,两者关系如下:
interface BinaryOperator<T> extends BiFunction<T,T,T> {}
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);//两个不同类型的参数返回另一个类型的值
}
// 等价于如下结构:注意非官方源码!!
@FunctionalInterface
public interface BinaryOperator<T> {
T apply(T t, T u);// 两个同类型的参数返回同类型的值
}
因此累加器的作用就是将流中元素依次归约得到和元素同类型的值。
3)由于累加器没有初始值,需要考虑在流为空时或元素不足两个时导致的结果可能不存在的情况,因此返回的是Optional
类型
4)执行原理:该reduce(accumulator)
方法接受一个BinaryOperator
累加器实例,实例的apply()
方法有两个参数,第一个参数是上次apply()
方法执行的返回值(也称为中间结果,首次执行取的是Stream的第一个元素),第二个参数是stream中的元素,这个方法把这两个值进行归约,得到的值会被赋值给下次执行这个apply()
方法的第一个参数,再取出Stream中的下一个元素作为第二个参数的值(如果后续没有元素则返回归约计算结果),之后依次类推。
5)应用实例:
public class StreamTest3 {
public static void main(String[] args) {
Test1.test1();
Test2.test2();
Test3.test3();
Test4.test4();
}
static class Test1 {
public static void test1() {
Optional<Stream<String>> stream = Optional.ofNullable(Stream.of(""));
test(stream);
// Test1执行结果:
// accResult:
//--------
}
}
static class Test2 {
public static void test2() {
Optional<Stream<String>> stream = Optional.ofNullable(Stream.of("3"));
test(stream);
// Test2执行结果:
// accResult: 3
//--------
}
}
static class Test3 {
public static void test3() {
Optional<Stream<String>> stream = Optional.ofNullable(Stream.of("3","4"));
test(stream);
// Test3执行结果:
// acc : 3
//item: 4
//acc+ : 34
//--------
//accResult: 34
//--------
}
}
static class Test4 {
public static void test4() {
Optional<Stream<String>> stream = Optional.ofNullable(Stream.of());
test(stream);
// 执行到accResult.get()时会报错:java.util.NoSuchElementException: No value present
}
}
public static void test(Optional<Stream<String>> stream) {
Optional accResult = stream.get()
.reduce((acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult.get());
System.out.println("--------");
}
}
T reduce(T identity, BinaryOperator<T> accumulator)
:
1)identity
:accumulator
累加器的初始值。
2)执行原理:如果流为空,该方法直接返回该初始值;如果流不为空,则首次执行时累加器的apply()
方法的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素,这个方法把这两个值进行归约,得到的值会被赋值给下次执行这个apply()
方法的第一个参数,再取出Stream中的下一个元素作为第二个参数的值(如果后续没有元素则返回归约计算结果),之后依次类推。
3)应用实例:
public class StreamTest4 {
public static void main(String[] args) {
Test1.test1();
}
static class Test1 {
public static void test1() {
Optional<Stream<Integer>> stream = Optional.ofNullable(Stream.of(1));
test(stream);
// Test1执行结果:
//acc : 0
//item: 1
//acc+ : 1
//--------
//accResult: 1
//--------
}
}
public static void test(Optional<Stream<Integer>> stream) {
int accResult = stream.get()
.reduce(0, (acc, item) -> {
System.out.println("acc : " + acc);
acc += item;
System.out.println("item: " + item);
System.out.println("acc+ : " + acc);
System.out.println("--------");
return acc;
});
System.out.println("accResult: " + accResult);
System.out.println("--------");
}
}
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
:
1)identity
:accumulator
累加器的初始值。需注意初始值类型可以是任意数据类型,不一定要和流的元素类型一致!
2)accumulator
:累加器,用于将流中的元素进行归约操作。此处的累加器和前两个方法的有很大区别,其apply()
方法的第一个参数的类型、方法的返回值类型需和初始值的类型保持一致,仅第二个参数才要求和流的元素同类型。如此大大加强了归约的功能。
3)combiner
:组合器。组合器只在并行流中起作用(可以使用parallel()
方法将普通流转换为并行流),否则不执行组合器的apply()
方法。在普通流中调用该方法只起到改变归约计算结果类型的作用。
4)执行原理:分为两种情况:
i.若是普通流,如果流为空,该方法直接返回该初始值;如果流不为空,则只会调用累加器的apply()
方法。首次执行时累加器第一次执行apply()
方法的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素,这个方法把这两个值进行归约,得到的值会被赋值给下次执行这个apply()
方法的第一个参数,再取出Stream中的下一个元素作为第二个参数的值(如果后续没有元素则返回归约计算结果),之后依次类推。
ii.若是并行流,如果流为空,该方法直接返回该初始值;如果流不为空,则会对流中任意元素分配给并发线程,线程先分别调用累加器的apply()
方法,再将累加器的结果两两作为组合器的apply()
方法的参数参与运算。
5)应用实例:
public class StreamTest2 {
public static void main(String[] args) {
Stream<Integer> iterate = Stream.iterate(1, (a) -> a + 1).limit(5);//1,2,3,4,5
System.out.println(iterate.parallel().unordered().reduce(0,(a,b)->{
System.out.println(a.getClass()+" "+Thread.currentThread().getName()+"a: "+a);
System.out.println(b.getClass()+" "+Thread.currentThread().getName()+"b: "+b);
return a+b;
},(c,d)->{
System.out.println(c.getClass()+" "+Thread.currentThread().getName()+"c: "+c);
System.out.println(d.getClass()+" "+Thread.currentThread().getName()+"d: "+d);
return c+d;
}));
/**
* class java.lang.Integer maina: 0
* class java.lang.Integer ForkJoinPool.commonPool-worker-13a: 0
* class java.lang.Integer ForkJoinPool.commonPool-worker-11a: 0
* class java.lang.Integer ForkJoinPool.commonPool-worker-9a: 0
* class java.lang.Integer ForkJoinPool.commonPool-worker-2a: 0
* class java.lang.Integer ForkJoinPool.commonPool-worker-9b: 1
* class java.lang.Integer ForkJoinPool.commonPool-worker-11b: 5
* class java.lang.Integer ForkJoinPool.commonPool-worker-13b: 4
* class java.lang.Integer mainb: 3
* class java.lang.Integer ForkJoinPool.commonPool-worker-2b: 2
* class java.lang.Integer ForkJoinPool.commonPool-worker-13c: 4
* class java.lang.Integer ForkJoinPool.commonPool-worker-2c: 1
* class java.lang.Integer ForkJoinPool.commonPool-worker-13d: 5
* class java.lang.Integer ForkJoinPool.commonPool-worker-2d: 2
* class java.lang.Integer ForkJoinPool.commonPool-worker-13c: 3
* class java.lang.Integer ForkJoinPool.commonPool-worker-13d: 9
* class java.lang.Integer ForkJoinPool.commonPool-worker-13c: 3
* class java.lang.Integer ForkJoinPool.commonPool-worker-13d: 12
* 15
*/
}
}
7.2.3.4 聚合操作
使用说明:
1)如果流是Stream<Integer>
、Stream<Double>
、Stream<Long>
类型,在求和、求平均值时,都需要先执行mapToInt()
、mapToDouble()
、mapToLong()
将流转化成基本数据类型流,才能后续执行sum()
、average()
方法。
2)如果流是IntStream
、LongStream
、DoubleStream
这类基本类型流,则可以直接使用sum()
、average()
方法。
long count()
:计算流中元素总个数
long l = list.stream().count();
T sum()
:求和
int sum = list.stream().mapToInt(Person::getAge).sum();
// 也可以使用reduce实现:
int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();
OptionalDouble average()
:求平均值
OptionalDouble average = list.stream().mapToInt(Person::getAge).average();
十二、Java容器(集合)
1.Arrays类
1.1 常见方法
public static void sort(int[] a)
: