读书笔记 - 剑指Java:核心原理与应用实践
全书分为19章,内容分为四大部分,
- 第一部分介绍了Java编程语言的基础知识,包括Java语言的基础语法、流程控制语句结构、数组,以及主流编程工具的使用等;
- 第二部分详细介绍了Java编程语言的核心知识,即面向对象编程基础/进阶/高级、异常和异常处理等;
- 第三部分重点讲解Java的各种应用场景,涉及常用类、集合、泛型、IO流、多线程、网络编程、反射等。
- 第四部分则是关注Java 8~Java 17版本的新特性,从语法层面变化、API层面增删变化、GC等底层设计的变化等四个方面进行阐述。
第1章 Java语言概述
Java的发展简史
1995年Sun(Stanford UniversityNetwork)公司开发了一门新的编程语言——Java。
Java语言是由詹姆斯·高斯林(James Gosling)和其Green Team小组成员共同开发的。
Java的最初名为Oak,由于Oak商标已经被注册,所以更名为Java。
1996年是Java语言的发展简史中具有里程碑意义的一年。在这一年里,Sun公司发布了Java开发人员熟悉的JDK 1.0版本,JDK 1.0版本包括Java虚拟机、网页应用小程序及可以嵌套在网页中运行的用户界面组件,开发人员通过用户界面组件可以开发窗口应用程序。
作为一个开放的平台,Java虚拟机负责解释执行字节码文件,即任何一种能够编译成字节码的编程语言都可以在Java虚拟机上运行,如Groovy、Scala、JRuby、Kotlin等编程语言,因此它们也是Java平台的一部分,Java平台也因为它们变得更加丰富多彩。
Java语言的技术体系平台
1998年,Sun公司根据应用的领域不同,把Java技术划归为三个平台,当时分别称为J2SE、J2EE和J2ME,2006年改名为Java SE、Java EE和Java ME。
Java SE是Java平台标准版(Java Platform,StandardEdition)的简称,允许用户在桌面和服务器上开发和部署Java应用程序。Java提供了当今应用程序所需要的丰富的用户界面、性能,具有通用性、可移植性和安全性。同时,Java SE为Java EE提供了基础。本书主要介绍的就是Java SE的技术。
Java EE是Java平台企业版(Java Platform,EnterpriseEdition)的简称,用于开发便于组装、健壮、可扩展、安全的服务器端Java应用。Java EE建立在Java SE之上,具有Web服务、组件模型,以及通信API等特性。这些为面向服务的架构(SOA),以及开发Web 2.0应用提供了支持。
Java ME是Java微版(Java Platform,Micro Edition)的简称,是一个技术和规范的集合,它为移动设备(消费类产品、嵌入式设备、高级移动设备等)提供了基于Java环境的开发与应用平台。Java ME在早期的诺基亚塞班手机系统中有很多应用,而现在随着使用iOS和Android操作系统的智能手机的兴起,Java ME渐渐退出了手机端应用开发的历史舞台。
Java语言的特点
-
平台无关性
Java语言的一个显著特点就是平台无关性。 -
面向对象性
Java语言是一门面向对象的语言。面向对象的世界观认为世界是由各种各样具有自己的运动规律和内部状态的对象组成的,不同对象之间的相互作用和通信构成了完整的现实世界。 -
支持分布式
java.net包提供了相应的类库用于网络应用编程,Java语言的远程方法调用(RMI)机制也是开发分布式应用的重要手段。 -
支持多线程
JVM被设计成采用轻量级进程(Light Weight Process,LWP)实现与操作系统的内核线程形成相互对应的映射关系。使用JVM就可以实现Java内部的多线程,并提供了相应的语法来进行编码。其实调用Java的多线程就是调用内核线程来执行的,所以说Java天生是支持多线程的语言。
java.lang包提供了Thread线程类来支持多线程编程,Java的线程支持包括一组同步原语。这组同步原语是基于监督程序和条件变量风范,由C.A.R.Hoare开发并广泛使用的同步化方案,如synchronized、volatile等关键字的使用。从JDK 1.5开始又增加了java.util.concurrent包,该包提供了大量高级工具,可以帮助开发人员编写高效、易维护、结构清晰的Java多线程程序。 -
健壮性
Java语言原来是用于编写消费类家用电子产品软件的语言,所以它被设计成可以编写高可靠和稳健的程序。Java会检查程序在编译、运行时的错误,并消除错误。
Java被设计为强类型语言,类型检查能帮助用户检查出许多在开发早期出现的错误。Java要求以显式的方法声明,它不支持C语言风格的隐式声明。这些严格的要求可以保证编译程序能捕捉调用错误。 -
安全性
Java通常被用在网络环境中,为此Java提供了一个安全机制以防恶意代码的攻击,类加载器的双亲委托工作模式、加载过程中对字节码的校验、分配不同的命名空间以防替代本地的同名类等设计都保证了Java程序的安全性。
Java的存储分配模型也是它防御恶意代码的主要方法之一。学过C语言的开发人员对内存的管理都很头痛。Java语言删除了类似C语言中的指针和内存释放等语法,由JVM自动分配内存,并且提供了强大的垃圾回收机制,人们在使用Java语言时不需要过多考虑内存情况,可以把精力更多专注在业务开发上。
Java语言的核心机制之JVM
Java最初风靡世界的原因是它有良好的跨平台性。而Java能够跨平台的核心机制在于它的虚拟机。
C语言程序针对不同指令集的处理方式
在Java出现之前,最为流行的编程语言是C语言和C++。如果我们想要在一台使用x86_64指令集的CPU的机器上运行一个C语言程序,那么就需要编写一个将C语言翻译成x86_64汇编语言的编译器。如果想要在一台使用arm指令集的CPU机器上运行一个C语言程序,那么同样需要编写一个将C语言翻译成arm汇编语言的编译器。这严重影响了C语言程序的跨平台性,因为针对特定的指令集编写编译器是一个难度非常大的工作。
Java程序针对不同指令集的处理方式
只要一台机器可以运行Java虚拟机,那么就能运行Java语言编写的程序。而不同的平台,需要安装不同的Java虚拟机程序。那么我们编写完Java程序之后,需要先将.java的源文件编译为.class的字节码文件,然后在Java虚拟机中执行这些字节码文件。
Java程序的编辑、编译、运行过程
Java语言的开发环境和运行环境
- JDK(Java Development Kits):Java开发工具包
- JRE(Java Runtime Environment):Java运行环境
- JVM(Java Virtual Machine):Java虚拟机
第2章 第一个Java程序:HelloWorld
略,这有啥看的?
第3章 基础语法
保留字
一部分关键字在Java中并没有使用,暂时没有赋予特殊含义,这部分称为保留字。
,还有一些单词在Java中没有正式使用,但是为了Java与底层系统(如C语言)的交互,Java保留了一些关键字,称为保留字。目前,保留字有如下两个。
(1)goto。goto语句在其他语言中叫作“无限跳转”语句。Java语言不再使用goto语句,这是因为goto语句会破坏程序结构。在Java语言中可以通过break、continue和return实现“有限跳转”。
(2)const。const在其他语言中是声明常量的关键字,在Java语言中使用final方式声明常量。
关键字
关键字是被Java语言赋予特殊含义,具有特殊用途的字符串(单词),如表3-1所示。关键字有一个特点是所有字母都为小写形式。
特殊值
三个特殊值:true、false、null。这三个特殊值看起来像是关键字,但实际上是字面量值。后面在给标识符命名时,同样要避开使用特殊值。
标识符
标识符是Java对各种变量、方法和类等要素命名时使用的字符序列。凡是需要命名的类名、接口名、变量名、方法名、包名等统称为标识符。
Java标识符的命名规则有如下几个。
(1)每个标识符只能包含数字、字母、下画线和美元符号。
(2)不能以数字开头。
(3)不能使用关键字和保留字作为标识符。
(4)标识符不能包含空格。
(5)严格区分大小写。
数据类型
Java是一门强类型语言,根据存储元素的需求不同,我们将数据类型划分为基本数据类型和引用数据类型,如图3-9所示。
第4章 流程控制语句结构
略
第5章 数组
一维数组内存分析
int arr = {10,20,30,40,50}
JVM会给内存中的每字节都编有地址值,但具体的地址值我们很难记忆也不需要关心,只要知道数组引用(数组名)arr中存储了第一个元素的地址值即可,其称为首地址。因为每个元素的宽度都是一样的,所以只要知道了首地址,加上距离首地址的偏移量,就可以找到数组的每个元素。
第6章 开发工具IntelliJ IDEA
快捷键
以下为常用的快捷键,可供参考。
·运行:Shift + F10。
·导入包和自动修正:Alt + Enter。
·向下复制选中行:Ctrl +D。
·删除选中行:Ctrl + Y。
·剪切选中行:Ctrl + X。
·行交换位置。与上面行交换位置:Ctrl + Shift + ↑。与下面行交换位置:Ctrl + Shift + ↓。
·当前代码行与下一行代码之间插入一个空行,光标现在处于新加的空行上:Shift+Enter。
·当前代码行与上一行代码之间插入一个空行,光标现在处于新加的空行上:Ctrl+Alt+Enter。
·自动生成某个类的Constructors、Getters、Setters、equals() and hashCode()、toString()等代码:Alt +Insert。
·重写父类的方法:Ctrl + O。
·实现接口的方法:Ctrl + I。
·自动生成具有环绕性质的代码,如if/else、for、do/while、try/catch、synchronized等,使用前要先选择好需要环绕的代码块:Ctrl + Alt + T。
·添加和取消注释。选中行加单行注释:Ctrl + /,再按一次取消;选中行加多行注释:Ctrl + Shift + /,再按一次取消。
·将方法调用的返回值自动赋值给变量:Ctrl + Alt + V。
·方法参数提示:Ctrl + P。
·代码模板提示:Ctrl + J。
·选中代码抽取封装为新方法:Ctrl+Alt+M。
·重命名某个类、变量等:Shift+F6。
·格式化代码:Ctrl+Alt + L。
·删除导入的没用的包:Ctrl + Alt + O。
·折叠/展开方法实现:Ctrl + Shift + -/+。
·缩进或不缩进一次所选择的代码段:Tab/Shift+Tab。
·查看某个方法的实现:Ctrl +Alt + B 或 Ctrl + 单击该方法名。
·查看继承快捷键:Ctrl + H。
·查看类的UML关系图:Ctrl + Alt + U。
·查看类的成员列表:Ctrl + F12。
·查找某个文件:Ctrl+Shift+N。
·打开最近编辑的文件:Ctrl+E。
·全文检索:双击Shift。
·选中内容转大小写:Ctrl + Shift + U。
第7章 面向对象编程基础
面向对象与面向过程
编程思想有很多种,最基础的就是面向对象和面向过程,因为很多人接触编程都是从C语言开始的,C语言是面向过程的编程语言,所以面向过程和面向对象经常被放在一起做比较。
面向过程(Procedure Oriented Programming,POP)是以“过程”为中心的,遇到问题时,想的是解决问题的步骤,然后用函数把步骤一一实现,最后再依次调用。面向过程更侧重于“怎么做”,以执行者的角度思考问题,比较适合解决小型问题或底层问题。
面向对象(Object Oriented Programming,OOP)以“对象”为中心,遇到问题时,想的是需要哪些对象一起来解决问题,然后找到这些对象,并把它们组织在一起,然后取各家之所长来共同解决一个问题。面向对象更侧重于“谁来做”,以指挥者的角度思考问题,比较适合解决中大型问题。
对象的内存分析
JVM将内存分为5个部分:方法区、堆、虚拟机栈、本地方法栈、程序计数器。
TestChinese类代码的内存示意图
图是JDK6版本中的结构。后续JVM内存结构的不同部分存储的数据做了相关调整(如字符串常量池存储位置在后续JDK版本中有所调整),但不影响其对实例变量、静态变量的理解。
成员变量与局部变量的区别
第8章 面向对象编程进阶
常用包介绍
JDK提供了很多核心类,按类的功能不同,将其放在不同的包下,扩展类都放在javax包及子包下。
Java提供了大量的基础类,因此Oracle也为这些基础类提供了相应的API(Application Programming Interface,应用程序编程接口)文档,用于告诉开发者如何使用这些类及这些类中包含的方法。
基础的常用包有以下几个。
(1)java.lang:包含Java的核心的基础类型,如String、Math、Integer、System类等。
(2)java.net:包含执行与网络相关的API。
(3)java.io:包含能表示文件和目录的File类及各种数据读/写相关的功能类。
(4)java.util:包含实用工具类,如集合框架类、数组工具类、旧版时间日期API等。
(5)java.time:包含新版日期时间API。
(6)java.text:包含Java文本格式化相关的API。
(7)java.lang.reflect:包含一些与反射相关的API。
(8)java.sql:包含Java进行JDBC数据库编程的相关API。
(9)java.util.function:是Java 8新增的包,包含与函数式编程相关的接口。
(10)java.util.stream:也是Java 8新增的包,包含与Stream相关的API。
当然JDK核心类库中的包远不止这些,在学习时需要循序渐进,先了解一部分,再延伸到其他部分。
向上转型与向下转型
“Animal animal = new Cat();”这种将子类对象赋值到父类变量的过程,称为向上转型(Upcasting),而“向上”(Up)这个名词来源于继承图的典型布局方式,通常基类在顶部,而子类在其下部散开。因此,转型为一个基类,也就是在继承图中向上移动,即“向上转型”,
instanceof关键字
我们在8.6.3节学习了向上转型和向下转型,当把子类对象/变量赋值给父类的变量时就自动发生向上转型,这个过程是安全的。但是当把父类的变量重新赋值给子类的变量时,就必须进行强制的向下转型操作,这个操作是有风险的。
类的初始化
我们定义完类之后,会被编译为对应的class字节码文件,这些字节码文件存放在硬盘的某个位置。当Java程序运行时,会将使用到的相关类加载到JVM的方法区内存中,这时系统会对类进行初始化操作。
类的初始化操作本质上是执行一个()的类初始化方法,这个方法是由编译器根据相关代码组装而成的,不是由程序员直接声明的。其中“cl”代表类,“init”代表初始化。
编译器在编译时,会将类中有关静态变量的显式赋值和静态代码块的代码按照顺序组装到()的类初始化方法中,每个类有且只有一个()的类初始化方法,它包含0~n
条语句。
当类加载器加载某个类后,会调用()类初始化方法对类进行初始化。()类初始化方法的执行特点有如下两个。
·每个类的初始化方法只会执行一次。
·如果子类在初始化过程中发现父类还未初始化,会先初始化父类,再初始化子类。
对象的初始化
每次在创建一个新对象时,JVM都会在堆中开辟一块内存空间,用来存储该对象的相关信息,并且完成对实例对象的初始化操作。非静态代码块也是用来进行对象初始化的。
对实例对象的初始化操作都是通过执行对应的方法来完成的,其中“init”代表初始化(initialize)。每个类都至少包含一个方法,具体有多少个方法是由程序员编写的构造器数量来决定的。
方法不是由程序手动定义的,它也是在编译器对类进行编译时,编译器根据类的相关结构自行组装而成的,每个构造器最终都会编译生成一个方法。当然,方法中的代码不只是构造器中的代码,而是由以下4个部分组成的。
(1)super()或super(实参列表):构造器首行的代码。
(2)非静态实例变量的显式赋值语句。
(3)实例初始化块。
(4)构造器中除super()或super(实参列表)外剩下的代码。
每个实例初始化方法中都会有一份(1)、(2)、(3),加上对应构造器中的代码构成对应的实例初始化方法,并且(1)永远在首行,(4)永远在最后,而(2)、(3)按照在Java类中的编写顺序,按原顺序组装。
特别需要说明的是,原来说super()和super(实参列表)代表父类的无参构造器和有参构造器,其实本质上是对应父类无参构造器和有参构造器对应的方法。
当使用new调用某个构造器创建实例对象时,其实就是执行该构造器对应的方法,而在子类构造器中首行的super()或super(实参列表)的执行就会导致父类对应方法被执行。每次new对象时,都会执行对应的实例初始化方法。
第9章 面向对象高级编程
本章我们将给大家介绍几个高级特性,分为以下几个主题:final和native关键字的使用、使用abstract声明抽象方法和抽象类、更抽象类型接口的声明与实现、类的另类成员之内部类的设计和语法,以及JDK 5引入的两种特殊Java类型之枚举和注解的使用。
final关键字
final代表最终,不可更改的意思。
(1)final修饰的变量值不能被修改。
(2)final修饰的方法可以被继承但不能被子类重写。
(3)final修饰的类不能被继承。
native关键字
native代表本地、原生。Java编写的程序属于应用层,中间隔着JVM和操作系统,是不能直接针对硬件编程的。所以,当Java程序需要与Java外面的环境进行交互,如Java需要与底层(如操作系统或某些硬件)交换信息,Java就需要调用一些本地方法来实现,有些本地方法是由JVM提供的,有些本地方法是由外部的动态链接库(External Dynamic Link Library)提供,然后被JVM调用。本地方法就是Java与外部环境交流的一种机制,它提供了一个非常简洁的接口,系统无须了解Java应用之外的烦琐细节,就能简单实现想要的功能。例如,无须知道键盘输入的数据如何被放到JVM的内存中,或者Java程序数据的内容如何在屏幕显示,等等,只要简单调用某些native方法就可以快速实现。
Object类、System类、Thread类等系统基础类中就有大量的native方法。
abstract关键字
当父类表现为更通用的概念类,以至于创建它的实例对象没有实际意义,那么这样的父类就算没有抽象方法,也应该设计为抽象类。
抽象方法
所谓的抽象方法,就是指没有方法体实现代码的方法,它仅具有一个方法签名。
抽象类
Java规定如果一个类中包含抽象方法,则该类必须设计成抽象类。当然,也并非所有的抽象类都包含抽象方法,当某个父类表现为更通用的概念类,以至于创建它的实例对象没有实际意义时,那么这样的父类就算没有抽象方法,也应该设计为抽象类。
抽象类与普通类还是有区别的,具体体现在以下几个方面。
(1)抽象类不能直接实例化,即不能直接创建抽象类的对象。这是因为抽象类中可能包含抽象方法,而抽象方法没有方法体可以执行。
(2)抽象类不能使用final修饰,因为抽象类是必须被子类继承的,否则它就失去了存在的意义,这与final正好矛盾。
(3)子类继承抽象类之后,如果子类不再是抽象类,那么子类必须重写抽象父类的所有抽象方法,否则编译报错。
接口
Java中的接口和抽象类代表的都是抽象类型,可以看作是抽象层的具体表现。
类、接口之间的关系有以下几点。
·类和类之间是单继承。
·接口和接口之间是多继承。
·类和接口之间是多实现。
·继承关系使用extends,实现接口关系使用implements。
Java 8对接口的改进
对于已经实现了这些接口的所有实现类来说,它们就会全部面临修改问题,即降低了可扩展性。
因此Java 8的编写人员决定对接口的语法进行改进,允许在接口中声明公共的静态方法和公共的默认方法。
(1)静态方法:使用public static关键字修饰,其中public可以省略,static不能省略。另外,接口中的静态方法,实现类不会继承,只能通过“接口名.静态方法”的方式进行调用。
(2)默认方法:使用public default关键字修饰,其中public可以省略,default不能省略。另外,默认方法只能通过接口的实现类对象来调用。实现类会继承接口的默认方法,而且如果需要可以选择对其进行重写。
内部类
因为类的定义是一类具有相同特性的事物的抽象描述。一般情况下我们会把类定义成一个独立的程序单元,但有的情况下会把一个类放在另一个类的内部。
·定义在其他类内部的类,称为内部类(或叫嵌套类)。
·包含内部类的类,称为外部类(或叫宿主类)。
内部类的作用有以下几点。
·提供了更好的封装,可以把内部类隐藏在外部类之内,让其他类按照要求的方式访问该类。
·内部类与外部类可以直接访问对方的私有成员。
根据声明位置和方式的不同,内部类可以分为两大类别,共有四种形式。
(1)在成员位置上:定义在类体中,方法的外面。
非静态成员内部类(没用static关键字修饰)。
静态成员内部类(使用static关键字修饰)。
(2)在局部位置上:定义在方法或代码块内部。
局部内部类(有类名)。
匿名内部类(无类名)。
局部内部类
定义在方法体或代码中的内部类称为局部内部类。局部内部类分为有名字的局部内部类和没有名字的局部内部类,
使用enum定义枚举类
关于新的枚举类声明格式,我们要做如下说明。
(1)枚举类的所有常量对象必须在枚举类的首行全部列出,建议使用大写形式来命名这些常量对象,它们其实就是使用public static final修饰的常量。
(2)枚举类本质上是一种类,它一样可以有自己的属性、方法等,有一个默认的私有构造器,如果要手动定义构造器,那么构造器的权限修饰符只能是private,其中private可以省略。
(3)枚举类中如果除了常量对象没有其他成员,那么常量对象列表后可以不用加分号,反之,如果后面还有其他成员,那么常量对象列表后必须加分号。
(4)枚举类隐式继承了java.lang.Enum类,根据Java单继承的特性,枚举类不能再继承其他类。当然枚举类可以实现自己需要的接口,同样支持多实现,并且如果需要,那么每个枚举对象还可以独立重写接口的抽象方法。
(5)Java 5.0之后,switch(表达式)开始支持枚举类型。
元注解
注解是标记在类、方法、变量等上的解释性元素。其中有一种特殊的注解,它是在声明注解时,标记在被声明的注解上,对声明的新注解做解释说明的,这种用于给注解类型做解释说明的注解,称为元注解。
DK 5.0之后提供了以下四个元注解。
(1)Retention:用于解释新声明注解的保留策略。使用Retention注解时必须用枚举类RetentionPolicy的三个常量对象之一来指定具体的保留策略。
·SOURCE:保留在源码阶段,编译器编译后直接丢弃该注解信息。
·CLASS:保留在字节码阶段,即该注解会由编译器记录在类文件中,但不需要在运行时由VM保留。这也是注解声明的默认保留策略,即如果某个注解声明时未加Retention,注解则默认保留策略是CLASS。
·RUNTIME:保留到运行期间,即在运行期间仍然可以读取该注解,程序员自定义的注解都使用这个策略,因为必须保证在程序运行期间使用反射读取到该注解信息。
(2)Target:用于解释新声明的注解可以使用在什么位置。使用Target注解时必须用枚举类的常量对象们来指定具体的位置。如果某个注解声明时没有加Target注解,则表示所有位置都可以。
·TYPE:代表类、接口、枚举。
·FIELD:属性。
·METHOD:方法。
·PARAMETER:形参。
·CONSTRUCTOR:构造器。
·LOCAL_VARIABLE:局部变量。
·ANNOTATION_TYPE:注解类型。
·PACKAGE:包。
·TYPE_PARAMETER:类型参数。
·TYPE_USE:类型使用。
(3)Documented:用于解释新声明注解用在某个包、类、方法等上面后,当使用javadoc工具提取文档注释生成的API文档时,是否将对应的注解信息也读取到API文档。加@Documented的注解其@Retention的RetentionPolicy值必须为RUNTIME才有意义。
(4)Inherited:用于解释新声明注解用在某个类、方法等上面后是否可以被其子类继承,即如果父类上面或父类的某个成员标记了@Inherited修饰的注解之后,该注解会被子类继承。
自定义注解
除了使用系统预定义的注解,还可以声明自己的注解类型。
在Java中注解被看作一种特殊的接口,使用@interface关键字进行声明。注解中可以没有任何成员,也可以声明一个或多个抽象方法。这里的抽象方法比较特殊,不能声明参数列表,返回值类型只能使用八大基本数据类型、String类型、枚举类型、Class类型及上述类型的数组类型,不能是Void类型或其他类型。
Java 8注解的新特性
在Java 8之前,注解只能在声明的地方使用,如声明类的上面、声明方法的上面等,Java 8扩展了注解的使用范围。为支持新特性,Java 8在ElementType中新增的两个常量对象(TYPE_USE和TYPE_PARAMETER)用来描述注解的新场合,这样注解就可以应用在任何地方。
代码演示1:在创建类实例时使用注解。
new@Internet MyObject();
代码演示2:在类型转换时使用注解。
myString = (@Notnull String) str;
代码演示3:在implements实现接口时使用注解。
class UnmodifiableList implements@Readonly List<@Readonly T>
代码演示4:在throw exception声明时使用注解(关于异常的throws参考第10章)
void monitorTemperature() throws@Critical TemperatureException {}
第10章 异常和异常处理
异常的分类
对于错误,一般有两种解决方法:一种是遇到错误就终止程序的运行;另一种是程序员在编写程序时,就先考虑错误的检测、错误消息的提示,以及错误的处理。
Java将程序执行时可能发生的错误(Error)或异常(Exception),都封装成了类,作为java.lang.Throwable的子类,即Throwable是所有错误或异常的超类。Throwable类中定义了子类通用的方法,当错误或异常发生时,则会创建对应异常类型的对象并且抛出。为什么子类又分为错误和异常呢?显然,二者的特点是不同的。
(1)错误:指的是Java虚拟机无法解决的严重问题,一般不编写针对性的代码进行处理。
(2)异常:指其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。
Java将这些异常归为运行时异常(RuntimeException)
常见的异常和错误类型
(1)ArrayIndexOutOfBoundsException:数组下标越界异常。
(2)NullPointerException:空指针异常。
(3)ClassCastException:类型转换异常。当试图将对象强制转换为它不属于的类型时,系统就会抛出该异常,所以在强制类型转换之前建议使用instanceof关键字进行判断,从而避免类型转换异常。
(4)ArithmeticException:算术异常。当进行一些数学运算时,如果违反了一些规则,就会发生算术异常。
(5)InputMismatchException:输入不匹配异常。
我们之前使用Scanner类不同的next方法接收键盘输入的各种数据类型数据,但是Java语言是强类型语言,每种数据类型的宽度(字节数)是不同的,所以如果你输入的数据类型与要接收数据的类型不一致,那么将会发生输入不匹配异常。
(6)NumberFormatException:数字格式化异常。
当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,就会抛出数据格式化异常。在实际开发中,通常使用文本框接收用户输入的数据,而文本框中输入的数据不管是文字还是数字,都只能按字符串处理,然后在程序中需要转换为需要的数据类型,如转换为整数类型。
(7)StackOverflowError:栈内存溢出错误。
方法在调用时会有一个“入栈”的过程,即需要在栈中开辟一块独立的内存空间用来存储该方法的局部变量等信息,直到方法运行结束才会“出栈”,即释放该内存空间。当方法调用层次太多,特别是递归调用时,就容易发生栈内存溢出错误。一个极端的例子就是无限递归。
(8)OutOfMemoryError:内存溢出错误。因为在内存溢出或没有可用的内存提供给垃圾回收器时,Java 虚拟机无法再给一个新对象分配内存,所以将会抛出内存溢出错误。
throw 关键字
throw 关键字用于在方法体内部手动抛出一个异常对象。当程序在执行过程中遇到某种错误情况,需要立即终止当前操作并抛出异常时,可以使用 throw 语句。
throw new IllegalArgumentException(“年龄不能为负数”);
throws 关键字
throws 关键字用于在方法声明中指定该方法可能会抛出的异常类型。它表示该方法本身不处理这些异常,而是将异常抛给调用者处理。
public static void readFile(String filePath) throws FileNotFoundException { }
Java 7对异常处理的改进
一个是一个catch分支可以捕获多个异常类型;另一个是增加了一种新的try-catch语法形式以支持自动关闭资源。了try-with-resource
第11章 常用类
本章将给大家介绍几个简单实用的API,主要包括:Object类(根父类)、包装类、String类、可变字符序列、Arrays类(数组工具类)、自然排序接口Comparable和定制排序接口Comparator、数学相关类、日期类等。
Object类
java.lang.Object类是类层次结构的根类,每个类(除了Object类本身)都使用Object类作为超类。一个类如果没有显式声明继承另一个类,则相当于默认继承了Object类。
Object类只有一个默认的空参构造器,所有类的对象创建最终都会通过super()语句调用到Object类的无参构造器中。
toString方法
对于初学者来说,Object类中最实用的方法应该是toString方法,作用是返回对象的字符串形式。
equals方法
比较两个基本数据类型的值是否相等应使用“==”,
而比较两个字符串的内容是否相等应使用equals方法。
Object类中默认实现的equals方法也是通过“”判断对象是否相等。而对于引用类型变量,“”判断的不是属性信息,而是二者引用的是否是同一个对象,也就是地址是否相等。一般来讲,开发人员希望判断的是两个对象的属性内容是否相等,所以往往需要重写equals方法。
为什么要重写 equals 方法?在实际开发中,我们通常更关心的是两个对象的属性内容是否相等,而不是它们是否指向同一个内存地址。例如,有两个 Person 对象,它们的姓名、年龄等属性都相同,我们就认为这两个对象是相等的,尽管它们可能是不同的实例。此时,就需要重写 equals 方法来实现基于属性内容的比较。
hashCode方法
重写equals方法时,一般都会带上hashCode方法,
hashCode方法的说明有以下几点:
(1)hashCode方法用于返回对象的哈希码值。支持此方法是为了提高哈希表(如java.util.Hashtable提供的哈希表)的性能,因此,目前大家只要知道hashCode值是一个整数值即可,至于哈希表等概念在后面的集合章节会讲解。
(2)hashCode在Object类中有native修饰,是本地方法,该方法的方法体不是Java实现的,是由C/C++实现的,最后编译为.dll文件,然后由Java调用。
hashCode方法重写时要满足如下几个要求。
规则(1):如果两个对象调用 equals 方法返回 true,那么要求这两个对象 hashCode 值一定是相等的这是为了保证在使用基于哈希的集合(如 HashMap、HashSet)时,当我们认为两个对象相等(equals 方法返回 true),它们在哈希表中应该被视为同一个元素。如果 equals 为 true 但 hashCode 不同,可能会导致这些集合将它们视为不同的元素,从而破坏集合的逻辑。
规则(2):如果两个对象的 hashCode 值不相等,那么要求这两个对象调用 equals 方法一定是 false这是规则(1)的逆否命题。hashCode 是用于快速查找和初步判断对象是否可能相等的一个值,如果 hashCode 都不同,那么对象肯定不相等。
规则(3):如果两个对象的 hashCode 值相等,那么这两个对象调用 equals 方法可能是 true,也可能是 false这是因为 hashCode 方法通常会将对象的属性映射到一个有限范围的整数值,可能会出现不同的对象属性计算出相同 hashCode 的情况,即哈希冲突。所以 hashCode 相等时,还需要通过 equals 方法进一步比较对象的属性来确定它们是否真正相等。
一个类new出两个对象:
- 未重写 hashCode 方法
如果一个类没有重写 Object 类的 hashCode 方法,那么默认情况下,hashCode 方法返回的是对象的内存地址经过某种转换后的整数值。在这种情况下,两个通过 new 创建的不同对象,由于它们在内存中占据不同的地址,所以它们的 hashCode 值通常是不一样的。 - 重写了 hashCode 方法
当一个类重写了 hashCode 方法,并且根据对象的属性来计算 hashCode 值时,两个对象的 hashCode 值是否相同取决于这些属性的值。
属性值相同:如果两个对象的用于计算 hashCode 的属性值都相同,那么它们的 hashCode 值会相同。
属性值不同:如果两个对象的用于计算 hashCode 的属性值不同,那么它们的 hashCode 值通常也不同,但也存在哈希冲突的情况,即不同的属性值计算出了相同的 hashCode 值。
clone方法
开发中如果需要复制一个对象,则可以使用Object类提供的clone方法。
finalize方法
对于初学者来说,finalize方法是最没机会接触到的方法,简单了解一下即可。
finalize方法是Object类中的protected方法,子类可以重写该方法以实现资源清理工作,GC在回收对象之前会调用该方法,即该方法不是由开发人员手动调用的。
当对象变成不可达时,即对象成为需要被回收的垃圾对象时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。若对象未执行过finalize方法,则将其放入F-Queue队列,由一个低优先级线程执行该队列中对象的finalize方法。执行完finalize方法后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则对象复活,复活后的对象下次回收时,将不再放入F-Queue队列,即不再执行其finalize方法。
包装类
包装类主要分为三种不同类型:数值类型(Byte、Short、Integer、Long、Float和Double)、Character类型、Boolean类型。
为什么Java语言当初不直接使用包装类来代替基本数据类型,使其成为纯面向对象的语言呢?Java语言最初保留基本数据类型的主要考量是性能,而且基本数据类型不涉及垃圾回收的问题;从运行时间来看,测试同样的算法代码时基本数据类型明显快很多。
基本数据类型和包装类的内存
数值类型
数值类型包括Byte、Short、Integer、Long、Float和Double,它们有一些共同点。
- 数值类型的包装类都有共同的父类
数值类型的包装类都继承自Number类,Number类是抽象类,要求它的子类必须实现如下六个方法。
·byte byteValue():将当前包装的对象转换为byte类型的数值。
·short shortValue():将当前包装的对象转换为short类型的数值。
·int intValue():将当前包装的对象转换为int类型的数值。
·long longValue():将当前包装的对象转换为long类型的数值。
·float floatValue():将当前包装的对象转换为float类型的数值。
·double doubleValue():将当前包装的对象转换为double类型的数值。 - 创建对象的方式相同
包装类是引用数据类型,那么应如何创建包装类的对象呢?数值类型的包装类创建对象的方式通常有两种。
方式一:通过调用构造器
方式二:从JDK 1.5之后,可以通过调用包装类的valueOf静态方法,将一个基本数据类型的值或字符串转换为数值类型的包装类对象。 - 基本数据类型和String类型之间的转换
方式一:直接拼接空字符串。
调用String类型的valueOf静态方法。 - 其他常量与方法
·MIN_VALUE:表示某数值类型的最小值。
·MAX_VALUE:表示某数值类型的最大值。
·static int compare(int x,int y):Integer类中用于比较两个int值大小的静态方法,如果x大于y,则返回正整数;如果x小于y,则返回负整数;如果x等于y,则返回0。
·static int compare(double d1,double d2)
·static String toBinaryString(int i):Integer类中用于返回某int值的二进制值。
·static String toOctalString(int i):Integer类中用于返回某int值的八进制值。
·static String toHexString(int i):Integer类中用于返回某int值的十六进制值。
Character类型
Character类型是char类型的包装类,用于处理字符数据。
- 创建对象
方式一:通过构造器Character(char value)创建一个新的Character对象。
方式二:通过调用静态方法Character valueOf(charvalue)返回该char值的Character对象。 - char类型和String类型之间的转换
方式一:直接拼接空字符串来实现
方式二:通过调用String类型的valueOf方法来实现
在开发中经常有关于字符的判断、大小写转换等操作,Character类型给我们提供了以下几种现成的方法。
·MIN_VALUE:表示最小编码值的字符。
·MAX_VALUE:表示最大编码值的字符。
·char charValue():返回此Character类型对象的值。
·static boolean isLetter(char ch):判断该字符是否为字母。
·static boolean isDigit(char ch):判断该字符是否为数字。
·static boolean isUpperCase(char ch):判断是否为字母的大写形式。
·static boolean isLowerCase(char ch):判断是否为字母的小写形式。
·static char toLowerCase(char ch):将字符转为小写形式。
·static char toUpperCase(char ch):将字符转为大写形式。
Boolean类型
Boolean类型是booelan类型的包装类,用来处理布尔值“true”或“false”。
- 创建对象
将一个boolean值或“true”和“false”字符串转为Boolean类型的对象有以下两种方式。
方式一:通过构造器创建一个Boolean类型对象。 - 创建对象
方式一:通过构造器创建一个Boolean类型对象。
方式二:通过Boolean类型的静态方法valueOf将一个boolean值或“true”和“false”字符串转为Boolean类型对象。 - boolean类型和String类型之间的转换
方式一:通过拼接空字符串.
方式二:通过调用String类型的静态方法valueOf来实现.
包装类的缓存对象
包装类是引用数据类型,而且对象不可变,也就意味着如果在计算过程中值变了,就会产生新对象,那么这势必会增加空间和时间成本,为了节省内存、提高性能,Java 5引入了包装类的缓存机制。例如:在[-128,127]范围内的Integr对象,就可以使用共享缓存对象以便减少重复的对象。
String类
(1)正是因为String类使用太频繁,所以Java底层给String类做了很多特殊的支持。首先,String类除可以像其他普通类一样使用构造器和静态方法valueOf来创建对象,还可以像基本数据类型那样直接赋值。
(2)从API或源码中可以看到String类本身是final修饰的,这就意味着String类不能被继承。
(3)String类的对象用于表示一串字符序列,这串字符序列其实是用一个私有的数组char[ ] value进行存储的,而且该数组的声明也加了final声明,那么这就意味着我们无法对数组进行扩容等操作。
(4)正因为字符串对象是不可变的,所以JVM专门为字符串提供了一个常量池,凡是放在常量池中的字符串对象都可以共享.
那么哪些字符串对象是放在常量池中的呢?
(1)直接"…"得到的字符串对象放在常量池。
(2)直接"…" + "…"拼接的字符串对象放在常量池。
(3)两个指向"…"的final常量拼接结果放在常量池。
(4)所有字符串对象.intern()方法得到的结果放在常量池。
除以上四种方式,其他方式得到的字符串结果都在堆中。
字符串对象的内存分析
图11-9 共享同一个字符串常量的内存分析,s1和s2是共享常量池中的同一个字符串对象
图11-10 两个字符串常量的内存分析,本质上有两个字符串对象,new出来的在堆中,"hello"在常量池中,一个在常量池一个在堆的字符串内
图11-11 一个在常量池一个在堆的字符串内存分析,"hello"在常量池中是可以共享的。
图11-12 两个在堆一个在字符串常量池的内存分析
Arrays类
瑞士计算机科学家尼古拉斯·沃斯在1984年获得图灵奖时说过一句话“Programs =Algorithm+Data Structures”,即程序=算法+数据结构,而丰富的数据结构的实现,其底层物理结构只有两种,一种是线性存储结构(最经典的就是数组),另一种是链式存储结构。
1、toString方法:转换字符串,使用数组存储数据之后,查看所有数组元素是最基本的需求,之前我们不得不使用for循环进行遍历。
2、sort方法:自然排序,所谓自然排序,是指基本数据类型的数组就是按照数值本身的大小进行排序;对象数组的自然排序就是元素本身已经实现java.lang.Comparabhe接口的compareTo方法,即对象本身具有了可比较性,所以在排序时,按着元素本身的比较规则(compareTo方法的实现)进行排序。该方法为重载方法,支持除boolean类型的任意类型元素。
3、sort方法:定制排序,所谓定制排序,是指不管数组元素本身是否已经实现Comparable接口的compareTo方法,在排序时都使用定制比较器的比较规则进行排序。定制比较器是指java.util.Comparator接口的实现类对象,包含抽象方法int compare(Object obj1,Object obj2),定制排序器只支持对象数组。
4、binarySearch方法:二分查找,当然该方法返回正确结果的前提是待查找的数组已经排好序,否则结果是不确定的。对象数组要求元素必须支持自然排序或指定了定制比较器对象。
5、copyOf方法:数组复制,Arrays类的copyOf方法和copyOfRange方法可以满足不同场合的排序。
6、equals方法:判断数组的元素是否相等,如果需要比较两个数组的元素是否完全相等,那么可以直接使用Arrays类的equals方法来比较,该方法为重载方法,参数类型支持任意类型的数组。如果是基本数据类型,则直接比较数组的长度和元素值;如果是引用数据类型,则比较数组的长度及每个元素的equals方法。
Math类
Math类中包含了一些数学常量值,以及用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数等,其方法的参数和返回值类型一般为double类型。
BigDecimal类
一般的Float类和Double类可以用来做一般的科学计算或工程计算,但在商业计算等对求数字精度要求比较高的场合中,就不能使用Float类和Double类,可以考虑使用BigDecimal类。BigDecimal类支持任何精度的定点数。
Random类
在Math类中已经提供了random()方法来获取一个随机值,但是该方法只能返回[0,1)的小数,如果要获取一个其他基本数据类型的随机值,则需要做相应的换算等操作,比较麻烦。JDK在java.util包中单独提供了一个Random类供开发人员使用,该类提供了获取多种数据类型的随机值方法,Random类的部分方法如表11-9所示。
日期类
第一代日期类
- Date类
第一代日期时间API相对较简朴,主要有java.util.Date及和日期时间格式化有关的java.text.DateFormat及其子类。 - SimpleDateFormat类
通过刚才Date类的代码演示,我们可以发现取得的时间是一个非常精确的时间。但是因为其显示的格式没有考虑国际化问题,如该格式不符合中国人查看时间的格式习惯,因此需要对其进行格式化操作。java.text.SimpleDateFormat类可以实现格式化操作,它是java.text.DateFormat的子类。
第二代日期类
java.util.Calendar类是一个抽象类,它为特定瞬间与一组诸如YEAR、MONTH、DAY_OF_MONTH、HOUR 等日历字段之间的转换提供了一些方法,并为操作日历字段
第三代日期类
前面我们介绍了第一代日期类(Date类)和第二代日期类(Calendar类)。但发现依然有很多问题。
·可变性:像日期和时间这样的类应该是不可变的,某一个日期时间对象都只能代表某一个特定的瞬间。
·偏移性:Date类中的年份是从1900开始的,月份都是从0开始的,这不符合常规编程习惯。
·格式化:用于日期格式化及解析的SimpleDateFormat只对Date类有用,Calendar类则不行。
·它们也不是线程安全的。
·不能处理闰秒等。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),所以在世界时(民用时)和原子时之间相差超过到±0.9秒时,人们就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒)。目前,全球已经进行了27次闰秒,均为正闰秒。最近一次闰秒是北京时间2017年1月1日7时59分59秒(时钟显示07:59:60)。
- LocalDate、LocalTime、LocalDateTime类
java.time.LocalDate类:代表一个只包含年、月、日的日期对象,如2007-12-03。
java.time.LocalTime类:代表一个只包含小时、分钟、秒的日期对象,如13:45.30.123456789。
java.time.LocalDateTime类:代表一个包含年、月、日、小时、分钟、秒的日期对象,如2007-12-03T10:15:30。
-
Instant类
在处理时间和日期时,我们通常会想到年、月、日、时、分、秒。然而,这只是时间的一个模型,是面向人类的。第二种通用模型是面向计算机的,在此模型中,时间线中的一个点表示一个整数,这有利于计算机处理。在UNIX中这个数从1970年开始,以秒为单位;同样在Java中也是从1970年开始的,但以毫秒为单位。
java.time包通过值类型Instant提供机器视图,不提供处理人类意义上的时间单位。Instant类表示时间线上的一点,不需要任何上下文信息,例如,时区。从概念上讲,它只是简单地表示自1970年1月1日0时0分0秒(UTC)开始的秒数。因为java.time包是基于纳秒计算的,所以Instant类的精度可以达到纳秒级。(1 ns = 10-9 s)1秒= 1000毫秒=106微秒=109纳秒。 -
DateTimeFormatter类
第12章 集合
Java集合大致可以分为Set、List和Map三种体系。集合相较于数组来讲,可以被理解成一种更高级的容器,更适合存储数量不确定的多个对象,并可以实现常用的数据结构,如栈、队列等,还可用于保存具有映射关系的关联数组,满足各种数据存储需求。
数组回顾
数组可用于存储一组基本数据类型,也可以用于存储一组对象。元素之间的逻辑关系是线性的,底层的物理结构是顺序结构,即元素是依次按顺序存储的。当我们创建一个数组容器的对象时,会一次申请一大段连续的空间,所有数据存储在这个连续的空间中紧密排布,不能有间隔。在整个数组使用期间,数组的长度是固定的,是不能修改的,除非创建新的数组。
集合框架集
集合是为了满足用户多样化数据的逻辑关系而设计的,一系列不同于数组的,可变的聚合抽象数据类型,这些接口和类都在java.util包中,其类型很丰富,因此通常把这一组API统称为集合框架集。
集合框架集大致分为两大系列:一个是Collection系列,另一个是Map系列。
Collection接口提供三种Collection视图
Collection集合框架中的接口和类主要用于存储和操作一个一个的对象,称为单列集合。java.util.Collection是该系列中的根接口,提供了一系列方法供继承或实现。JDK不提供此接口的任何直接实现,而是提供了更具体的子接口(如Set和List、Queue)实现。此接口类型通常用来传递集合,并在需要最大普遍性的地方操作这些集合。
(1)List:有序的Collection(也称序列)。此接口的用户可以对列表中每个元素的插入位置进行精确控制。用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
(2)Queue:队列通常以FIFO(先进先出)的方式排序各个元素。不过优先级队列和LIFO队列(或堆栈)除外,前者根据系统提供的比较器或元素的自然顺序对元素进行排序,后者按LIFO(后进先出)的方式对元素进行排序。
(3)Set:一个不包含重复元素的Collection。更确切地讲,Set不包含满足e1.equals(e2) 结果为true的元素对象e1和e2,并且最多包含一个null元素。正如其名,此接口模仿了数学上的Set抽象(高中的集合概念)。其中SortedSet进一步提供了关于元素的总体排序的Set,这些元素使用其自然顺序进行排序,或者根据通常在创建有序Set时提供的Comparator进行排序。该Set 的迭代器将按元素升序遍历Set,并提供了一些附加的操作来实现这种排序。
Map接口提供三种Collection视图
允许以键集、值集或键—值映射关系集的形式查看某个映射的内容。一些映射实现可明确保证其顺序,如TreeMap类;另一些映射实现则不保证其顺序,如 HashMap 类。SortedMap进一步提供关于键的总体排序的Map,该映射是根据键的自然顺序进行排序的,或者根据通常在创建有序映射时提供的Comparator进行排序。
List集合
Collection接口没有提供直接的实现类,而是提供了更加具体的子接口的实现类,其中一个最常用的子接口就是List接口。
List接口的实现类
- ArrayList类:动态数组。
2· LinkedList:双向链表,JDK 1.6之后又实现了双端队列Deque接口,双端队列也可用作LIFO(后进先出)堆栈。如果要使用堆栈的集合,那么可以考虑使用LinkedList类,而不是Deque接口
3· Vector类:动态数组。Vector类是STL(标准模板库)中最常见的容器,也是动态数组数据结构的实现。
4· Stack类:堆栈。Stack类是Vector的子类,用于表示后进先出(LIFO)的对象堆栈
Set集合
Set集合支持的遍历方式也和Collection集合一样,使用foreach和Iterator进行遍历。
常用实现类有:HashSet、LinkedHashSet、TreeSet。
HashSet和LinkedHashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能(具体原因和存储结构分析请看12.6.1节和12.6.5节)。HashSet和LinkedHashSet集合判断两个元素相等的标准是两个对象通过hashCode方法比较,并且两个对象的equals方法返回值也相等。因此,存储到HashSet和LinkedHashSet的元素要注意是否可以重写hashCode和equals方法。
LinkedHashSet是HashSet的子类,它在HashSet的基础上,在结点中增加两个属性(before和after)以维护结点的前后添加顺序。LinkedHashSet插入性能略低于HashSet,但在迭代访问Set中的全部元素时有很好的性能。
SortedSet是Set接口的一个子接口,支持排序类Set集合,TreeSet是SortedSet接口的实现类,即TreeSet可以确保集合元素处于排序状态。对象的排序要么是对象本身支持自然排序,即实现java.lang.Comparable接口,要么在创建set集合对象时提供定制排序接口java.util.Comparator的实现类对象。
Map集合
Map是地图、映射的意思。生活中地图上的某个点可以映射到实际地理环境中的某个位置,这种映射关系可以用(key,value)的键值对来表示。
既然Map是用来存储Entry类的(key,value)键值对的,那么Map接口中自然也封装了所有键值对的通用操作方法:增、删、改、查、遍历。
Map集合的遍历:
(1)分开遍历:又存在两种情况,即单独遍历所有key和单独遍历所有value。
(2)成对遍历:遍历的是映射关系Map.Entry。Map.Entry是Map接口的内部接口。每种Map内部都有自己的Map.Entry的实现类。
Map接口的常用实现类有HashMap、TreeMap、LinkedHashMap和Properties。
LinkedHashMap是HashMap的子类,此实现与HashMap的不同之处在于,LinkedHashMap维护了一个双向链表,此链表定义了迭代顺序,此迭代顺序通常就是将键插入映射中的顺序。
TreeMap的集合是基于红黑树(Red-Black tree)的可导航NavigableMap实现的。TreeMap中的映射关系要么根据其key键的自然顺序进行排序,要么根据创建TreeMap对象时提供给key键的定制排序Comparator接口实现类进行排序,具体取决于使用的构造方法。
TreeMap使用key的自然排序的示例代码(其中String类实现了Comparable接口):
Properties是Hashtable的子类,Properties中存储的数据可保存在流中或从流中加载。Properties的特殊之处在于,它的每个key及其对应的value都是一个字符串。在存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法。
源码分析
Set的源码分析
Set的内部实现其实是一个Map,即HashSet的内部实现是一个HashMap,TreeSet的内部实现是一个TreeMap,LinkedHashSet的内部实现是一个LinkedHashMap。
HashSet的源码分析
HashSet核心构造器的源码摘要如下所示(基于JDK 8):
public HashSet() {
map = new HashMap<>();
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
原来是把add添加到Set中的元素e作为内部实现map的key,然后用一个常量对象PRESENT对象作为value,iterator遍历时通过底层Map的keySet()返回所有key再迭代。这是因为Set的元素不可重复和Map的key不可重复有相同特点,所以就在Map的基础上轻易地封装出了Set系列的集合类型,这也是代码复用得很好的例证。
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
Iterator的源码分析
迭代器(Iterator)有时又称游标(Cursor),它会提供一种方法可以访问一个容器(Container)对象中的各个元素,而又不需要暴露容器对象的内部细节。
每种集合的底层实现方式都不同,有数组、链表等,不同的结构其遍历方式肯定不同,所以无法有一个统一的迭代器实现类。但是它们遍历数据的过程都有相同的操作,如需要判断是否还有下一个元素hashNext(),获取下一个元素next()等,所以抽取了迭代器接口,但是接口的实现类交给各个集合自己实现,每种集合就用内部类的形式实现自己的迭代器,这种设计称为迭代器设计模式。迭代器用内部类的实现方式既可以让其直接访问集合容器中私有的内部成员,又可以对外隐藏实现细节,便于后期维护。迭代器实现公共的迭代器接口可以让用户用统一的方式遍历所有支持该迭代器的集合,降低了耦合性,也大大降低了程序员的学习成本,即使你不知道迭代器的内部实现细节也不妨碍你使用迭代器接口遍历集合,这个设计是不是很妙!
其他源码分析
其他的源代码分析,参考类似思路,观察如何构造、添加、删除、迭代等。
new HashSet<>();
new TreeSet<>();
new LinkedHashSet<>();
new ArrayList<>();
new LinkedList<>();
new HashMap<>();
new Properties();
第13章 泛型
泛型的概念
泛型(Generics)指的就是泛化的类型,即用等形式来表示一个未确定的类型。
常见的泛型形参字母有T、K、V、E,它们一般是某个单词的首字母,这些单词通常表示该泛型表示的数据类型,或者仅是类型的意思。
·T:Type。
·K,V:Key、Value。
·E:Element
泛型的示例
上面的示例代码暴露了以下两个问题。
问题1:集合对元素类型没有任何限制,这样可能会导致本来只想存储字符串对象的,却不小心把Integer对象轻易放进去的问题,因为编译期间没有类型检查。
问题2:由于把对象“丢进”集合后,集合就忘记了对象的实际类型,集合只知道它装的是Object类型,因此取出集合元素是Object类型(其在实际的运行时类型没变,但是编译时只能按照Object类型处理),如果想使用对象的实际类型那么还需要进行强制类型转换。这种强制类型转换既会增加编程的复杂度,也可能引发ClassCastException。
示例代码:
ArrayList类代码片段。
Iterator接口代码片段。
Map接口代码片段。
从上面的代码片段中可以看出,我们可以在定义接口和类时声明泛型形参,如代表未知的集合元素类型、<K,V>代表未知的key和value的类型。当使用这些集合时,就可以为、<K,V>指定具体的泛型实参。
示例代码:
设定泛型形参的上限
在声明泛型类或泛型接口时,<泛型>是可以指定上限的,同样在声明泛型方法时,<泛型>也可以指定上限,这两种的语法格式和要求是一样的。如果没有指定上限,则默认上限为Object,如果有多个上限,则用“&”连接,并且父类在前,父接口在后,至多只能指定一个父类上限。
类型通配符
当声明一个方法的某个形参类型是一个泛型类或泛型接口,但是不确定该泛型的实际类型时,如某个方法的形参类型是ArrayList,实参集合元素可能是任意类型,即此时形参无法将具体化。Java提供了类型通配符用来解决这个问题。使用泛型类或泛型接口的类型声明其他变量时也是如此。
类型通配符的上限(限制自己)
类型通配符的下限(限制对方)
可以通过<? super 下限>的方式指定其下限。
案例需求:
假设需要声明一个处理两个Collection集合的静态方法,它可以将src集合中的元素剪切到dest集合中,并且返回被剪切的最后一个元素。
方案一:public static T cut(Collectiondest,Collection<? extends T> src)。可以表示依赖关系,不管T是什么类型,src集合元素类型都是T或T的子类。但是这种方案有一个缺点,那就是src元素的实际类型是不确定的,但肯定是T或T的子类,所以方法的返回值类型只能用T来笼统表示。如果此时dest的泛型是,src的泛型是,那么cut方法返回的结果类型只能是Object,也就是说,程序在复制集合元素的过程中,丢失了src集合元素的String类型。
方案二:public static T cut(Collection<? superT> dest,Collection src)。也可以表示依赖关系,不管src集合元素类型中的T是什么,只要dest集合元素的类型是T或T的父类即可。而且如果此时dest的泛型是,src的泛型是,那么cut方法返回的结果是String类型,完美地记录了源集合src的元素类型。
泛型方法与类型通配符
根据前面几节的学习,我们可以得出以下几个结论。
<?>可以代表任意类型,
<? extends Type>可以代表Type或Type的子类,
<? super Type>可以代表Type或Type的父类。
可以代表任意类型,
可以代表Type或Type的子类。
泛型擦除
如果没有为泛型类的<泛型>指定具体类型,则该类被称作原始类型,此时<泛型>自动按照<泛型>的第一个上限类型处理。如果某个泛型没有指定上限,则默认上限是Object,即泛型擦除后,<泛型>自动按照第一个上限处理。
第14章 IO流
I是Input,代表数据的输入和读取;O是Output,代表数据的输出和写出。Java将数据的输入/输出(I/O)操作当作“流”来处理,“流”是一组有序的数据序列,Java的IO技术支持通过java.io包下的类和接口来完成。
Windows的路径分隔符用“\”表示,而Java程序中的“\”表示转义字符,所以在Windows中需要用“\”表示路径,或者直接用“/”表示。Java程序支持将“/”当成与平台无关的路径分隔符,或者直接用File.separator常量值表示路径分隔符。
路径中如果出现“…”,则表示上一级目录,路径名如果以“/”开头,则表示从根目录下开始导航。
IO流类的体系结构
Java在java.io包下提供了很多IO流的类型,用于通过各种方式在多种场景下读/写数据,其实不管有多少种IO流,它们都是从基本的四个IO流中延伸出来的,以下是IO流的四个超级父类、抽象基类。
·InputStream:字节输入流,以字节的方式读取数据。
·OutputStream:字节输出流,以字节的方式输出数据。
·Reader:字符输入流,以字符的方式读取数据。
·Writer:字符输出流,以字符的方式输出数据。
IO流类整个体系结构的设计选用了装饰者设计模式,即IO流分为两大类,分别为被装饰的组件和装饰的组件。
以InputStream为例,其中FileInputStream、ByteArrayInputStream等是被装饰的组件,依次用来连接和读取文件、内存中的字节数组等;
而BufferedInputStream、DataInputStream、ObjectInputStream等是用来装饰的组件,负责给其他InputStream的IO流提供装饰的辅助功能,如可以增加提高效率的缓冲功能、按照Java数据类型读取数据的能力、读取并恢复Java对象的能力等。
常用的IO流
抽象基类的常用方法
很多初学Java的读者一看到那么多的IO流类型就被吓住了。如果你把每种IO流都单独来学习,确实难度很大。但是,我们知道所有的IO流都是从四大抽象基类中扩展出来的,因此掌握四大抽象基类基本方法的使用将会大大降低学习其他IO流的难度。在学习方法上,切记不要死记硬背,要善于比较和观察它们之间的区别,这样可以事半功倍。
1. InputStream:字节输入流
(1)int read()。
从输入流中读取数据的下一字节,返回 0 到255内的 int字节值。如果因为已经到达流末尾而没有可用的字节,则返回-1。
(2)int read(byte[ ] b)。
从此输入流中将最多b.length字节的数据读入一个byte数组中。如果因为已经到达流末尾而没有可用的字节,则返回-1,否则以整数形式返回实际读取的字节数。
(3)int read(byte[ ] b,int off,int len)。
将输入流中最多len个数据字节读入byte数组。尝试读取len字节,但读取的字节也可能小于该值,以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回-1。
(4)public void close() throws IOException。
关闭此输入流并释放与该流相关的所有系统资源。
2. OutputStream:字节输出流
(1)void write(int b)。
将指定的字节写入此输出流。write的常规协定是向输出流中写入1字节。要写入的字节是参数b的8个低位。b的24个高位将被忽略,即写入0~255。
(2)void write(byte[ ] b)。
将b.length字节从指定的byte数组中写入此输出流。write(b)的常规协定是应该与调用write(b,0,b.length)的效果完全相同。
(3)void write(byte[ ] b,int off,int len)。
将指定byte数组中从偏移量off开始的len字节写入此输出流。
(4)public void flush()throws IOException。
刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标。
(5)public void close() throws IOException。
关闭此输出流并释放与该流相关的所有系统资源。
3. Reader:字符输入流
(1)int read()。
读取单个字符,作为整数读取的字符,范围在0~65535(0x00~0xffff)(2字节的Unicode码),如果已到达流的末尾,则返回-1。
(2)int read(char[ ] cbuf)。
将字符读入数组。如果已到达流的末尾,则返回-1。否则返回本次读取的字符数。
(3)int read(char[ ] cbuf,int off,int len)。
将字符读入数组的某个部分。存到数组cbuf中,从off处开始存储,最多读取len个字符。如果已到达流的末尾,则返回-1。否则返回本次读取的字符数。
(4)public void close() throws IOException。
关闭此输入流并释放与该流相关的所有系统资源。
4. Writer:字符输出流
(1)void write(int c)和Writer append(char c)。
写入单个字符。要写入的字符包含在给定整数值的16个低位中,16个高位被忽略,即写入0~65535的Unicode码。
(2)void write(char[ ] cbuf)和Writerappend(CharSequence csq)。
写入字符数组。
(3)void write(char[ ] cbuf,int off,int len)和Writer append(CharSequence csq,int start,intend)。
写入字符数组的某个部分。从off开始,写入len个字符。
(4)void write(String str)。
写入字符串。
(5)void write(String str,int off,int len)。
写入字符串的某个部分。
(6)void flush()。
刷新该流的缓冲,则立即将它们写入预期目标。
(7)public void close() throws IOException。
关闭此输出流并释放与该流相关的所有系统资源。
Java序列化与反序列化
Java中输出对象的过程称为序列化,读取对象的过程称为反序列化。
序列化的过程需要使用ObjectOutputStream,它有一个writeObject(obj)方法可以输出对象,即将对象的完整信息转换为字节流数据。
反序列化的过程需要使用ObjectInputStream,它有一个readObject()方法可以读取对象,即从字节流数据中读取信息并重构一个Java对象。
不是所有对象都可以直接序列化的,需要序列化的对象类型或其父类必须已经实现了java.io.Serializable接口,而且如果对象的某个属性是引用数据类型,并且这个属性也要参与到序列化的过程,那么这个属性的类型或其父类也要实现java.io.Serializable接口,否则就会报java.io.NotSerializableException。
serialVersionUID,在实际开发中,序列化后的数据可能被保存在磁盘的某个文件中,然后很久之后才会出现被反序列化的过程,这时可能系统中的类早已经被维护更新了。又或者,序列化后的数据从网络的一端传输到网络的另一端,而另一端中关于该对象的类没有及时更新。也就是说,序列化所使用的类与反序列化所使用的类,并不完全一致,那么在反序列化时就会报java.io.InvalidClassException。
类中static修饰的静态变量值是不会被序列化的。
第15章 多线程
线程的创建和启动
在Java中,我们可以通过java.lang.Thread类实现多线程。所有的线程对象都必须是Thread类或其子类的对象。每个线程的作用是完成一定的任务,实际上就是执行一段代码,称之为线程执行体。Java使用run方法来封装这段代码,即run方法的方法体就是线程执行体。
继承Thread类
在Java中,线程是Thread类的对象,如果要创建和启动自己的线程,那么就可以直接继承Thread类。
启动线程用start()方法,而不是run()方法。调用start()方法来启动线程,系统会把run()方法当成线程执行体来处理。但是如果直接调用run()方法,系统就会把线程对象当成一个普通对象处理,run()方法就是一个普通方法。
实现Runnable接口
Java有单继承的限制,所以除了可以直接继承Thread类,Java还提供了实现java.lang.Runnable接口的方式来创建自己的线程类。实现Runnable接口来创建并启动多线程有以下几个步骤。
(1)定义Runnable接口的实现类,并重写该接口的run()方法。
(2)创建Runnable接口实现类的对象。
(3)创建Thread类的对象,并将Runnable接口实现类的对象作为target。该Thread类的对象才是真正的线程对象。当JVM调用线程对象的run()方法时,如果target不为空,那么就会调用target的run()方法。
(4)调用线程对象的start()方法启动线程。
线程的生命周期
,一个完整的线程的生命周期通常要经历五种状态,这是从操作系统层面来描述的:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
线程的控制
Thread类提供了以下方法,可以控制线程的执行。
·public static void sleep(long millis) throwsInterruptedException:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器、调度程序精度和准确性的影响。
·public static void yield():它可以让当前正在执行的线程暂停,但它不会阻塞该线程,只是将该线程转入就绪状态。
·public final void join()throwsInterruptedException:插入另一个线程之前,使另一个线程暂停直到当前线程结束后再继续执行。
·public final void join(long millis) throwsInterruptedException:插入另一个线程之前,使另一个线程暂停直到毫秒后再继续执行。
·public final void stop():强迫线程停止执行(但是该方法已经过时不建议使用)。
·public final void suspend():挂起当前线程(但是该方法已经过时不建议使用)。
·public final void resume():重新开始执行挂起的线程(但是该方法已经过时不建议使用)。
·public void interrupt():中断线程。如果线程在调用Object类的wait()、wait(long) 或 wait(long,int)方法,或者Thread类的join()、join(long)、join(long,int)、sleep(long)或sleep(long,int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个InterruptedException。
·public static boolean interrupted():测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用就会返回“false”。
·public boolean isInterrupted():测试线程是否已经中断。线程的中断状态不受该方法的影响。
·public final void setDaemon(boolean on):将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException。
·public final boolean isDaemon():判断线程是否是守护线程。
同步代码块
为了解决线程安全问题,Java提供了synchronized关键字。当我们使用synchronized关键字时,一定要有一个锁对象配合工作。synchronized关键字的使用形式有两种:同步代码块和同步方法。
Java的同步锁可以是任意类型的对象。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot虚拟机的对象头(Object Header)又包括两部分信息,
第一部分用于存储对象自身运行时的数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
另一部分是类型指针,HotSpot虚拟机通过这个指针来确定这个对象是哪个类的实例。
所以同步锁看起来锁的是代码,其实本质上锁的是对象,即在同步锁对象中有锁标记,能够明确知道现在是哪个线程在占用锁。任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。所以,我们必须保证竞争共享资源的这几个线程,选的是同一个同步监视器对象,否则无法实现同步效果。
第16章 网络编程
JDK 8在java.net和javax.net包中提供了大量的API以支持程序员编写网络通信相关的程序。
按照网络的拓扑结构不同,可以分为星型网络、总线型网络、环型网络、网型网络、树型网络、混合型网络等.
网络的分类
按照网络模式不同,可以分为局域网、城域网和广域网。
(1)局域网(Local Area Network,LAN):是指在某个区域中由多台计算机互连而成的计算机组,区域一般是方圆
几千米以内。局域网可以实现文件管理、应用软件共享、打印机共享、工作组内的日程安排、电子邮件和传真通信服务等功能。
(2)城域网(Metropolitan Area Network,MAN):是指在一个城市范围内建立的计算机通信网。由于其采用具有有源交换元件的局域网技术,所以网络传输时延较小。城域网的传输媒介主要采用光缆,传输速率在100兆比特/秒以上。城域网的一个重要用途就是用作骨干网,通过它可以将位于同一个城市内不同地点的主机、数据库,以及局域网等互相连接起来,这与广域网的用途有相似之处,但两者在实现方法与性能上有很大差别。
(3)广域网(Wide Area Network,WAN):又称为外网、公网,是连接不同地区局域网或城域网的远程网。广域网通常可以跨越很大的物理范围,所覆盖的范围从几十千米到几千千米,能连接多个地区、城市和国家,或者横跨几个洲,并且能提供远距离通信,形成国际性的远程网络。
网络协议
(1)网络层。
IP是一种低级的路由协议,负责把数据从源地址传送到目的地址。IP在源地址和目的地址之间传送一种叫数据包的东西,还提供对数据包大小的重新组装功能,以适应不同网络对数据包大小的要求,但是无法保证所有数据包都能抵达目的地址,也不能保证数据包抵达的顺序。
IP地址是逻辑地址,在网络底层的物理传输过程中,是通过物理地址来识别主机的,这个物理地址也是全球唯一的,称为MAC地址。MAC地址前24位是厂家编号,由IEEE分配给厂家,后24位是序列号,由厂家自行分配。
地址解析协议(Address Resolution Protocol,ARP)是将IP地址解析为48位的以太网地址(MAC地址)的协议;而反向地址解析(ReverseAddress Resolution Protocol,RARP)则将48位以太网地址(MAC地址)解析为IP地址的协议。
因特网控制报文协议(Internet Control MessageProtocol,ICMP)用在IP主机和路由器之间传递控制消息。控制消息指的是主机是否可达、网络通不通、路由是否可用等消息,这些控制消息虽然并不传输用户数据,但是对用户数据的传输起着重要作用。
IGMP(Internet Group Management Protocol)是一个组播协议,运行在主机和组播路由器之间。
(2)传输层。
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,如果某些数据包没有收到,则会重发,并对数据包内容的准确性进行检查,最后按照数据包原有的顺序对信息进行重组。
UDP(User Datagram Protocol,用户数据报协议)是一种不可靠的、无连接的数据报协议,源主机在传送数据包前不需要和目标主机建立连接,数据附加了源端口号和目标端口号等UDP报头字段后,直接发往目的地址,因为实现没有建立连接,所以无法保证接收方收到消息,但是它比TCP更高效。
(3)位于应用层的协议。
·HTTP(Hyper Text Transfer Protocol,超文本传输协议):提供访问超文本信息功能,是WWW浏览器和WWW服务器之间的应用层通信协议。
·HTTPS(Hyper Text Transfer Protocol Secure,超文本传输安全协议):是HTTP与SSL的组合,用以提供加密通信及对网络服务器身份的鉴定。
·FTP(File Transfer Protocol,文件传输协议);用于在因特网上控制文件的双向传输。
·SNMP(Simple Network Management Protocol,简单网络管理协议):用在应用层上进行网络设备间通信。
·Telnet(Terminal Emulation Protocol,终端仿真协议):用于实现远程登录、远程管理交换机和路由器。
还有以下几种常见的电子协议。
·SMTP(Simple Mail Transfer Protocol,简单邮件传输协议):主要负责底层的邮件系统,将邮件从一台设备发送到另一台设备。
·POP(Post Office Protocol,邮局协议):目前的版本是POP3,负责把邮件从邮件服务器下载到本机。
·IMAP(Internet Message Access Protocol,因特网邮件访问协议):是POP 3的替代协议,提供了邮件检索和邮件的新功能。
因为应用层协议繁多,这里就不一一介绍了。
TCP Socket网络编程
这是因为当一台计算机需要与另一台远程计算机连接时,TCP会采用三次握手的方式让它们建立一个连接,用于发送和接收数据的虚拟链路。数据传输完毕TCP会采用四次挥手的方式断开连接。
TCP三次握手
第一次握手:客户则主动连接服务器端,并且发送“SYN”(同步序列号),假设序列号为“J”,则服务器端被动打开。
第二次握手:服务器端在收到“SYN”后,会发送一个“SYN”及一个“ACK”(应答)给客户端,“ACK”的序列号是J+1,表示给“SYN J”的应答,新发送的“SYN”序列号是K。
第三次握手:客户端在收到新“SYN K,ACK J+1”后,也回应“ACK K+1”,表示收到了,然后两边就可以开始发送数据了。
TCP四次挥手
第一次挥手:客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经发送过来的数据的最后一字节的序号加1),此时,客户端进入FINWAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
第二次挥手:服务器端收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务器端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器端通知高层的应用进程,客户端向服务器端的方向释放,这时候处于半关闭状态,即客户端已经没有数据要发送,但是服务器端若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
客户端收到服务器端的确认请求后,此时,客户端进入FIN-WAIT-2(终止等待2)状态,等待服务器端发送连接释放报文(在这之前还需要接收服务器端发送的最后数据)。
第三次挥手:服务器端将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于处在半关闭状态,服务器端很可能又发送了一些数据,假设此时的序列号为seq=w,服务器端就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到服务器端的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号为seq=u+1,此时,客户端进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过两个MSL后,当客户端撤销相应的TCB后,才会进入CLOSED状态。MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,它是任何报文在网络上存在的最长时间,超过这个时间则报文将被丢弃。
服务器端只要收到了客户端发出的确认,就会立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器端结束TCP连接的时间要比客户端早一些。
Socket介绍
Socket可以分为以下几种。
·流套接字(Stream Socket):使用TCP提供可依赖的字节流服务,包括ServerSocket和Socket。
·数据报套接字(Datagram Socket):使用UDP提供“尽力而为”的数据报服务,如DatagramSocket。
基于TCP的网络通信程序结构
ava基于套接字的TCP编程分为服务器端编程和客户端编程,TCP通信模型如图16-7所示。
(1)服务器端编程的工作过程包含以下五个基本的步骤。
① 使用 ServerSocket(int port):创建一个服务器端套接字,并绑定到指定端口上,用于监听客户端的请求。
② 调用accept()方法:监听连接请求,如果客户端请求连接,则接收连接,创建与该客户端的通信套接字的对象,否则该方法将一直处于等待状态。
③ 调用该Socket对象的getOutputStream()方法和getInputStream()方法:获取输出流和输入流,开始网络数据的发送和接收。
④ 关闭Socket对象:某客户端访问结束,关闭与之通信的套接字。
⑤ 关闭ServerSocket:如果不再接收任何客户端的连接,则调用close()方法进行关闭。
基于UDP的网络编程
UDP是一个无连接的传输层协议,提供面向事务的简单不可靠的信息传送服务,类似于短信。
基于UDP的网络编程仍然需要在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送、接收数据报的对象。Java提供了DatagramSocket对象作为UDP的Socket,DatagramPacket代表DatagramSocket发送、接收的数据报。
DatagramSocket类的常用方法
DatagramPacket类的常用方法
MulticastSocket多点广播
Datagram只允许数据报发送给指定的目标地址,而MulticastSocket可以将数据报以广播的方式发送到数量不等的多个客户端,多点广播IP地址如图16-10所示。IP为多点广播提供了一批特殊的IP地址,这些IP地址的范围是224.0.0.0~239.255.255.255。
MulticastSocket的常用方法
第17章 反射
类的加载、链接和初始化
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载或类初始化。
图17-1 类初始化过程
类的加载
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
(1)从本地系统直接读取.class文件,这是绝大部分类的加载方法。
(2)从zip、jar等归档文件中加载.class文件,这种方式也是很常见的。
(3)通过网络下载.class文件或数据。
(4)从专有数据库中提取.class数据。
(5)将Java源文件数据上传到服务器中,动态编译为.class数据,并执行加载。
类的链接
当类被加载之后,系统为其生成一个对应的Class对象,接着将会进入链接阶段,链接阶段负责把类的二进制数据合并到JVM的运行状态之中。类链接又可以分为如下三个阶段。
(1)验证:确保加载的类信息符合JVM规范,如以cafebabe开头,没有安全方面的问题。
(2)准备:正式为类变量(Static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配。
(3)解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
类的初始化
类的初始化主要就是对静态的类变量进行初始化,有以下几种。
(1)执行类构造器()方法的过程。类构造器()方法是由编译器自动收集类中所有类变量的显式赋值动作和静态代码块中的语句合并产生的。
(2)当初始化一个类时,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
(3)虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。
四种类加载器
Java的类加载器主要分为以下四种。
(1)引导类加载器(Bootstrap Classloader)又称为根类加载器。
它负责加载Java的核心库(JAVA_HOME/jre/lib/rt.jar等或sun.boot.class.path路径下的内容),是用原生代码(C/C++)来实现的,并不继承java.lang.ClassLoder,所以通过Java代码获取引导类加载器对象将会得到null。
(2)扩展类加载器(Extension ClassLoader)。
它是由sun.misc.Launcher
E
x
t
C
l
a
s
s
L
o
a
d
e
r
实现的,是
j
a
v
a
.
l
a
n
g
.
C
l
a
s
s
L
o
a
d
e
r
的子类,负责加载
J
a
v
a
的扩展库(
J
A
V
A
H
O
M
E
/
j
r
e
/
e
x
t
/
∗
.
j
a
r
或
j
a
v
a
.
e
x
t
.
d
i
r
s
路径下的内容)。(
3
)应用程序类加载器(
A
p
p
l
i
c
a
t
i
o
n
C
l
a
s
s
l
o
a
d
e
r
)。它是由
s
u
n
.
m
i
s
c
.
L
a
u
n
c
h
e
r
ExtClassLoader实现的,是java.lang.ClassLoader的子类,负责加载Java的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)。 (3)应用程序类加载器(Application Classloader)。 它是由sun.misc.Launcher
ExtClassLoader实现的,是java.lang.ClassLoader的子类,负责加载Java的扩展库(JAVAHOME/jre/ext/∗.jar或java.ext.dirs路径下的内容)。(3)应用程序类加载器(ApplicationClassloader)。它是由sun.misc.LauncherAppClassLoader实现的,是java.lang.ClassLoader的子类,负责加载Java应用程序类路径(classpath、java.class.path)下的内容。
(4)自定义类加载器。
开发人员可以通过继承java.lang.ClassLoader类的方式来实现自己的类加载器,以满足一些特殊的需求,如对字节码进行加密来避免class文件被反编译,或者加载特殊目录下的字节码数据等,在这些场景下,我们需要使用自定义类加载器。
双亲委托模型
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被载入了。
在JVM中,一个类用其全限定类名和其类加载器作为唯一标识。换句话说,同一个类如果用两个类加载器分别加载,那么JVM会将它们视为不同的类,互不兼容。
Java虚拟机的设计者们通过一种被称为“双亲委托模型(Parent Delegation Model)”的委派机制来约定类加载器的加载机制。
按照双亲委托模型的规则,除可以引导类加载器,程序中的每个类加载器都应该拥有一个父类加载器,如ExtClassLoader的父类加载器是引导类加载器,AppClassLoader的父类加载器是ExtClassLoader,自定义类加载器的父类加载器是AppClassLoader,类加载器执行类加载过程如图17-2所示。
自定义类加载器
在实际开发中,我们会遇到需要的类没有存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径)的情况,对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader。甚至有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就涉及一些加密和解密操作,那么此时就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
反射的根源
在Java程序中,所有的对象都有两种类型,即编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致。
例如,某些变量或形参的类型是Object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是Object类型中的方法,那么应该如何解决呢?
为了解决这些问题,程序需要在运行时发现对象和类的真实信息,现在有以下两种方案。
方案1:在编译和运行时都完全知道类型的具体信息,在这种情况下,可以直接先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量。
方案2:编译时我们根本无法预知该对象和类的真实信息,程序只能依靠运行时的信息来发现该对象和类的真实信息,这就必须使用反射。
Class类剖析
Class 类的实例常用来表示正在运行的Java程序中的类和接口。事实上,所有的类都可以表示为Class类的实例对象。
(1)class:外部类,内部类。
(2)interface:接口。
(3)[ ]:数组,所有具有相同元素类型和维数的数组共享同一个Class对象。
(4)enum:枚举。
(5)annotation:注解@interface。
(6)primitive type:八种基本数据类型。
(7)void:空,无返回值。
在Java程序中我们可以通过以下四种方式获得某种类的Class对象。
(1)类型名.class:仅适用于编译期间已知的任意类型,包括基本数据类型、void、数组、类、接口、枚举、注解等。如果某个类编译期间是已知的,则优先考虑这种方式,代码更安全,效率更高。另外,基本数据类型和void也只能通过这种方式获得Class对象。
(2)调用任意对象的getClass()方法,可以获取该对象的运行时类型的Class对象,适用于任意引用数据类型。
(3)使用Class类的forName(String name)静态方法,该方法需要传入一个字符串参数,该参数是某个类的全限定名(完整的包.类型名),该方法适用于数组以外的任意引用数据类型。如果运行时获取不到对应的Class对象,则会报ClassNotFoundException。
(4)调用类加载对象的loadClass(String name)方法,该方法需要传入一个字符串参数,该参数是某个类的全限定名,该方法适用于数组以外的任意引用数据类型。
获取类信息
java.lang.Class类提供了大量实例方法来获取该Class对象所对应类的详细信息,如包、修饰符、类名、父类、父接口、注解,以及成员(属性、构造器、方法)等,反射其他相关的API在java.lang.reflect包下。
java.lang.Class类提供了大量实例方法来获取该Class对象所对应类的详细信息,如包、修饰符、类名、父类、父接口、注解,以及成员(属性、构造器、方法)等,反射其他相关的API在java.lang.reflect包下。
1. 获取某个类的加载器
public ClassLoader getClassLoader():返回该类的类加载器。
2. 获取包名和类型名
public Package getPackage():获取此类的包,可以通过Package实例对象的getName()获取包名。
public String getName():以String的形式返回此Class对象所表示的实体(类、接口、数组类、基本数据类型或void)名称。
3. 获取类型修饰符
public int getModifiers():返回此类或接口以整数编码的Java语言修饰符。
修饰符由Java虚拟机的public、protected、private、final、static、abstract和interface对应的常量组成,它们应当使用Modifier类的方法来解码。
如果底层类是数组类,则其public、private和protected修饰符与其组件类型的修饰符相同。
4. 获取父类或父接口
public Class<? super T> getSuperclass():返回表示此Class所表示的实体(类、接口、基本数据类型或void)的超类的Class。如果此Class表示Object类型、一个接口、一个基本数据类型或void,则返回null。如果此对象表示一个数组类型,则返回表示该Object类型的Class对象。
public Class<?>[ ] getInterfaces():确定此对象所表示的类型或实现的接口。如果此对象表示一个类,则返回值是一个数组,它包含了表示该类所实现的所有接口的对象。
5. 获取内部类或外部类信息
public Class<?>[ ] getClasses():返回所有公共内部类和内部接口,包括从超类中继承的公共类和接口成员及该类声明的公共类和接口成员。
public Class<?>[ ] getDeclaredClasses():返回Class对象的一个数组,这些对象反映声明为此Class对象所表示的类的成员的所有类和接口,包括该类所声明的公共、保护、默认(包)访问及私有类和接口,但不包括继承的类和接口。
public Class<?> getDeclaringClass():如果此Class对象所表示的类或接口是一个内部类或内部接口,则返回它的外部类或外部接口,否则返回null。
6. 获取属性
以下四种方法均可用于访问Class对应类所包含的属性(Field)。
(1)public Field[ ] getFields():返回一个包含某些Field对象的数组,这些对象反映此Class对象所表示的类或接口的所有可访问公共字段。返回数组中的元素没有排序,也没有任何特定的顺序,包括继承的公共字段。
(2)public Field getField(String name):返回一个Field对象,它反映此Class对象所表示的类或接口的指定公共成员字段,包括继承的公共字段。name参数是一个String类,用于指定所需要字段的简称。
(3)public Field[ ] getDeclaredFields():返回Field对象的一个数组,这些对象反映此Class对象所表示的类或接口所声明的所有字段,包括公共、保护、默认(包)访问和私有字段,但不包括继承的字段。返回数组中的元素没有排序,也没有任何特定的顺序。
(4)public Field getDeclaredField(String name):返回一个Field对象,该对象反映此Class对象所表示的类或接口的指定已声明字段。
7. 获取构造器
(1)public ConstructorgetDeclaredConstructor(Class<?>…parameterTypes):构造器名称不需要指定,因为它和类名一致。parameterTypes参数是Class对象的一个数组,它按声明顺序标识构造方法的形参类型。如果此Class对象表示非静态上下文中声明的内部类,则形参类型作为第一个参数包括显示封闭的实例。
(2)public Constructor<?>[ ]getDeclaredConstructors():公共、保护、默认(包)访问和私有构造方法。
(3)public ConstructorgetConstructor(Class<?>…parameterTypes):指定公共构造方法。
(4)public Constructor<?>[ ]getConstructors():所有公共构造方法。
8. 获取方法
(1)public Method getDeclaredMethod(Stringname,Class<?>…parameterTypes):name参数是一个String类,它指定所需方法的简称;parameterTypes参数是Class对象的一个数组或0~n
个Class对象,它按声明顺序标识该方法的形参类型。如果是无参方法,那么parameterTypes可以不传或传null。因为可能存在重载的方法,所以在一个类中唯一确定一个方法,需要方法名和形参类型列表。
(2)public Method[ ] getDeclaredMethods():包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
(3)public Method getMethod(String name,Class<?>…parameterTypes):指定的公共成员方法,包括继承的公共方法。
(4)public Method[ ] getMethods():所有公共成员方法,包括继承的公共方法。
9. 获取泛型父类
JDK 1.5引入了泛型,为了通过反射操作这些泛型,新增了ParameterizedType、GenericArrayType、TypeVariable和WildcardType这几种类型来代表不能被归到Class中但是又和原始类型齐名的类型,获取泛型父类如图17-5所示。
public Type getGenericSuperclass():返回表示此Class类所表示的实体(类、接口、基本数据类型或 void)的直接超类的 Type。
10. 获取注解信息
可以通过反射API获得相关的注解信息,有如下几种方法。
(1)public Annotation[ ] getAnnotations():返回此元素上存在的所有注释。
(2)public Annotation[ ]getDeclaredAnnotations():获取某元素上存在的所有注释,该方法将忽略继承的注释。
(3)public TgetAnnotation(Class annotationClass):如果存在该元素的指定类型的注释,则返回这些注释,否则返回null。
反射的应用
动态创建对象
通过反射生成对象有如下两种方式。
方式一:使用Class对象的newInstance()方法创建该Class对象对应类的实例,这种方式要求该Class对象的对应类有无参构造器,而执行newInstance()方法时实际上是利用默认构造器创建该类的实例。
方式二:先使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance(Object…args)方法创建该Class对象对应类的实例。
动态操作属性
通过Class对象的getFields()等方法可以获取该类所包括的全部Field或指定Field。而Field类除提供获取属性的修饰符、属性类型、属性名等方法,还提供了如下几个方法来协助我们获取和设置属性值。
(1)public xxx getXxx(Object obj):获取obj对象的Field的属性值。此处的Xxx对应八种基本数据类型,如果该属性的类型是引用数据类型,则直接使用get(Object obj)方法。
(2)public void setXxx(Object obj,Xxx value):设置obj对象的Field的属性值为value。此处的Xxx对应八种基本数据类型,如果该属性的类型是引用数据类型,则直接使用set(Object obj,Object value)方法。
(3)public void setAccessible(boolean flag):启动和禁用访问安全检查的开关。值为true则指示反射的对象在使用时应该取消Java语言访问检查,可以提高反射的效率。如果代码中必须用反射,而该句代码需要频繁地被调用,那么请设置为true。
动态调用方法
java.lang.reflect包下还提供了一个Array类,Array类可以代表所有的数组。程序可以通过使用Array类来动态地创建数组和操作数组元素等。
Array类提供了如下几种方法。
(1)public static Object newInstance(Class<?>componentType,int…dimensions):创建一个具有指定组件类型和维度的新数组。
(2)public static void setXxx(Object array,intindex,xxx value):将Array数组中[index]元素的值修改为value。此处的Xxx对应8种基本数据类型,如果该属性的类型是引用数据类型,则直接使用set(Object array,int index,Object value)方法。
(3)public static xxx getXxx(Object array,intindex,xxx value):返回Array数组中[index]元素的值。此处的Xxx对应八种基本数据类型,如果该属性的类型是引用数据类型,则直接使用get(Object array,int index)方法。
第18章 Lambda表达式与Stream API
Java 8为Java语言、编译器、类库、开发工具与JVM带来了大量新特性,其中十大主要新特性如下。
新特性1:Lambda表达式。
新特性2:函数式接口。
新特性3:方法引用。
新特性4:数组引用和构造器引用。
新特性5:Stream API。
新特性6:Optional类的引入,为了减少空指针异常。
新特性7:Nashone引擎的使用,在jvm上运行js。
新特性8:新日期API。
新特性9:接口中可以定义默认方法和静态方法。
新特性10:重复注解。
新特性1:Lambda表达式语法
Lambda表达式是一个匿名函数,可以理解其为一段可以传递的代码。Lambda语法将代码像数据一样传递,可以代替大部分匿名内部类,使用它可以写出更简洁、更灵活的代码。Lambda表达式作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
新特性2:函数式接口
前面两个案例中的Lambda表达式都是作为接口实现类的实例出现的,但并不是所有的接口实现都可以使用Lambda表达式。能使用Lambda表达式的接口要求只有一个抽象方法。我们把只包含一个抽象方法的接口称为函数式接口。
Java建议在一个函数式接口声明上方使用@FunctionalInterface注解,这样做可以明确它是一个函数式接口。同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。
简单地说,Java 8中Lambda表达式就是一个函数式接口的实例,这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。以前用匿名内部类表示的现在大多可以用Lambda表达式来写。
Java 8除了给之前满足函数式接口定义的接口加了@FunctionalInterface注解,并且给建议使用Lambda表达式进行赋值的接口也加了@FunctionalInterface注解,如java.lang.Runnable接口、java.util.Comparator接口等。Java 8还在java.util.function包下定义了更丰富的函数式接口供我们使用,Java内置函数式接口如表18-1所示。
表18-1中的前四个为核心的函数式接口,其余都是它们的变形。因此,java.util.function包下的函数式接口主要分为以下四大类。
(1)消费型接口:其抽象方法有参无返回值,用“有去无回”的纯消费行为比喻。
(2)供给型接口:其抽象方法无参有返回值,用“无私奉献”的行为比喻。
(3)函数型接口:其抽象方法有参有返回值,参数类型与返回值类型可以不一致,也称为功能型接口。
(4)断定型接口:其抽象方法有参有返回值,但返回值类型是boolean,在Lambda体中是对传入的参数做条件判断,并返回判断结果。
在Java 8中,原来java.util包中的集合API也得到了大量改进,在很多接口中增加了静态方法和默认方法,并且这些静态方法和默认方法的形参类型也使用了函数式接口。
新特性3:方法引用。
方法引用也是Lambda表达式,就是通过方法的名字来指向一个方法,可以认为它是Lambda表达式的一个语法糖。当要传递给Lambda体的操作是调用一个现有的方法来实现时,就可以使用方法引用。
语法糖(Syntactic Sugar)也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,是计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
总结,当Lambda表达式满足以下三个要求时,才能使用方法引用进行简化。
(1)Lambda体中只有一句话。
(2)Lambda体中只有这句话为方法调用。
(3)调用的方法参数列表和返回类型与接口中抽象方法的参数列表和返回类型完全一致。
如果是类名::普通方法,则需要满足调用方法的调用者必须是抽象方法的第一个参数。调用方法的参数列表和抽象方法的其他参数一致。
新特性4:数组引用和构造器引用。
数组引用
与方法引用类似,Lambda体中如果引用的是一个构造器,且参数列表和抽象方法的参数列表一致,则可以使用构造器引用。当Lambda表达式满足如下三个要求时,就可以使用构造器引用来进行简化。
(1)Lambda体中只有一个语句。
(2)仅有的这个语句还是一个通过new调用构造器的return语句。
(3)抽象方法的参数列表和调用的构造器参数列表完全一致,并且抽象方法返回的正好是通过构造器创建的对象。
Java 8在java.util包中增加了一个工具类Optional(这个类的详细讲解可以参考第19章),这个类中有一个方法:T orElseGet(Supplier<? extends T>other)。该方法的作用是返回Optional对象中包含的值,如果该值为null,则用Supplier的get方法返回值代替。
数组构造引用
与方法引用类似,Lambda体中如果是通过new关键字创建数组,且数组的长度正好是抽象方法的实参,抽象方法返回的正好是该新数组对象,则可以使用数组引用。当Lambda表达式满足如下三个要求时,就可以使用数组构造引用来进行简化。
(1)Lambda体中只有一句话。
(2)只有的这句话为创建一个新数组。
(3)抽象方法的参数列表和新数组的长度一致,并且抽象方法的返回正好为该新数组对象。
新特性5:Stream API。
Java 8中有两大最为重要的改变,一个是Lambda表达式;另一个则是Stream API。
Stream API(java.util.stream)把真正的函数编程风格引入Java。这是目前为止对Java类库最好的补充,Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
Stream API不是用于处理IO的,而是用于处理集合的。使用Stream API对集合数据进行操作,就类似于使用SQL执行的数据库查询。Stream API可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作,也可以使用Stream API来并行执行操作。简言之,Stream API提供了一种高效且易于使用的处理数据方式。
Stream是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲究的是数据,Stream讲究的是计算。
步骤1:开始操作,根据一个数据源,如集合、数组等,获取一个Stream流。
步骤2:中间操作,对Stream流中的数据进行处理。
步骤3:终止操作,获取或查看最终结果。
Stream的特点有如下几点。
(1)Stream讲究的是计算,可以处理数据,但不能更新数据。
(2)Stream创建后可以有零个或多个操作处理数据,每次处理都会返回一个新的Stream,这些操作称为中间操作。
(3)Stream属于惰性操作,必须等终止操作执行后,前面的中间操作或开始操作才会处理。
(4)Stream只能终结一次,一旦终结,就不能再次使用,除非重新创建Stream对象,因为终结操作后返回值就不再是Stream类型了。
(5)Stream相当于一个更强大的Iterator,可以处理更加复杂的数据,并且实现并行化,效率更高。
新特性6:Optional类的引入,为了减少空指针异常。
新特性7:Nashone引擎的使用,在jvm上运行js。
新特性8:新日期API。
新特性9:接口中可以定义默认方法和静态方法。
新特性10:重复注解。
第19章 Java 9~Java 17新特性
图19-2 Java版本新特性数量示意
Java 9提供了超过150种新功能特性
包括备受期待的模块化系统、可交互的REPL工具(Jshell)、JDK编译工具、Java公共API和私有代码,以及安全增强、扩展提升、性能管理改善等。可以说Java 9是一个庞大的系统工程,完全做了一个整体的改变。
Java 10一共定义了109种新特性
其中包含12个JDK特性加强提议(JDK Enhancement Proposal,JEP),如局部变量的类型推断var关键字、并行全垃圾回收器G1、垃圾收集器接口和从JDK中移除javah工具等。Java 10的升级幅度其实主要还是以优化为主,并没有带来太多让使用者惊喜的特性。
Java 11一共包含了17个JEP
Java 11是发布周期变化后的第一个长期支持版本,非常值得关注,Java 11带来了ZGC、Http Client等重要特性,一共包含了17个JEP。对于企业来说,选择Java 11将意味着拥有长期的、可靠的、可预测的技术路线图。从JVM GC的角度来说,JDK 11引入了两种新的GC,其中包括划时代意义的ZGC。虽然ZGC在Java 11中还是实验特性(后面版本已经转正),但是从性能来看,这是JDK的一个巨大突破,为特定生产环境的苛刻需求提供了一个可能的选择。例如,对于部分企业核心存储等产品,如果能够保证不超过10ms的GC暂停,则可靠性会上一个大的台阶,这是在过去版本中进行GC调优几乎做不到的。
Java 12不是LTS版本,总共有8个新的JEP
Java 12重新拓展了switch语句,让它具备了新的能力。通过扩展现有的switch语句,可将其作为增强版的switch语句或称为switch 表达式来写出更加简化的代码。switch语句也是作为预览语言功能的第一个语言改动引入新版Java中来的,预览语言功能的想法是在2018年初引入Java中的,在本质上讲,这是一种引入新特性的测试版的功能。预览语言功能能够根据用户反馈进行升级、更改,在极端情况下,如果没有被很好地接纳,则可以完全删除该功能。预览语言功能的关键在于它们没有包含在JavaSE规范中。Java 12还引入了一个新的垃圾收集器,即Shenandoah,它是一种低停顿时间的垃圾收集器,其工作原理是通过与Java应用程序中的执行线程同时运行,用以执行其垃圾收集和内存回收任务,通过这种运行方式,给虚拟机带来短暂的停顿时间。
Java 13也不是LTS版本,总共有5个新的JEP
在语法层面,Java 13改进了switch Expressions,新增了TextBlocks;在API层面,Java 13主要使用NioSocketImpl来替换JDK 1.0的PlainSocketImpl;在GC层面,Java 13则改进了ZGC,以支持Uncommit Unused Memory。
Java 14也不是LTS版本,总共16种新特性
此版本包含的JEP比Java 12和Java 13加起来的JEP还要多,包括两个孵化器模块、三个预览特性、两个弃用功能和两个删除功能。例如,instanceof模式匹配(预览)、更详细的NullPointerException异常提示、record(预览)、switch表达式(二次预览)、文本块(二次预览)、扩展ZGC在macOS和Windows上的应用和移除CMS垃圾回收器等。孵化器模块是指将尚未定稿的API和工具先交给开发者使用,以获得反馈,并用这些反馈进一步改进Java平台的质量。
Java 15也不是LTS版本,此版本包括14种新特性,
其中包括一个孵化器模块、三个预览功能、两个不推荐使用的功能,以及两个删除功能。例如,密封类(预览)、隐藏类、instanceof模式匹配(二次预览)、record(二次预览)、移除Nashorn JavaScript引擎、移除 Solaris和SPARC端口等。Java 15从整体来看在新特性方面并不算很亮眼,它主要是对之前版本预览特性的功能做了确定,如文本块、ZGC等。
Java 16仍然不是LTS版本,此版本包含17种新特性。
其中几种值得重点关注的特性是全并发的ZGC、弹性元空间能力、instanceof的模式匹配和record正式交付使用等,另外还有三个孵化器模块和一个预览特性等。ZGC是从Java 11开始引入的新一代垃圾收集器,经过了几个版本的迭代,在Java 15中成为正式特性。Java 15将其进行了进一步改进,将线程栈的处理从安全点移到了并发阶段,这样ZGC在扫描根时就不用stop-the-world了。JDK 16修改了元空间实现,可以快速将未使用的元空间内存返回给操作系统,以减少内存占用,简化了元空间代码,降低了维护成本。record是作为预览特性在Java 14中引入的,在Java 16中正式交付。向量API孵化器提供了一个 API 的初始迭代以表达一些向量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳向量硬件指令,从而获得优于同等标量计算的性能,充分利用单指令多数据(SIMD)技术(大多数现代 CPU 上都可以使用的一种技术),该 API 将使开发人员能够轻松地用 Java 编写可移植的高性能向量算法。外部链接器API孵化器提供静态类型、纯 Java 访问原生代码的特性,该API 将大大简化绑定原生库的原本复杂且容易出错的过程。外部存储器访问API最早在 Java 14 和 Java 15 中作为孵化器 API引入,它使Java 程序能够安全有效地对各种外部存储器(如本机存储器、持久性存储器、托管堆存储器等)进行操作,还提供了外部链接器 API 的基础。密封类的二次预览可以限制哪些类或接口可以扩展或实现它们,允许类或接口的作者控制负责实现它的代码,它还提供了比访问修饰符更具声明性的方式来限制对超类的使用。
Java17是目前最新的长期支持版本此版本删除了2个功能,弃用了2个功能,增加了10个新功能
Java17是目前最新的长期支持版本,一问世就备受关注,很多框架技术纷纷开始尝试转向Java17,虽然这个过程还需经历一段时间,但是我们需要提前了解它们,并时刻准备好迎接它的普及。此版本删除了2个功能,弃用了2个功能,增加了10个新功能,进一步提升了Java的性能、稳定性和安全性,提高了开发人员的生产力。Java17正式删除了在Java15中明确标记已过时不推荐开发人员使用的远程方法调用(RMI) 激活机制,以及几乎没有使用过的实验性 AOT 和 JIT 编译器,明确弃用并准备在后续版本删除Applet API和安全管理器。在Java15引入的密封类在Java17正式转正。很多程序员常以损害安全性和可维护性的方式使用JDK的内部元素,比如一些非public类、方法和字段,为了继续提高 JDK 的安全性和可维护性,Java17默认强封装JDK除了sun.misc.Unsafe等关键内部API的所有内部元素,从而限制对它们的访问。随着always-strict 浮点语义的恢复,浮点运算将变得始终严格,而不是同时具有严格的浮点语义(strictfp)和细微不同的默认浮点语义,这一特性表示Java正式和一个搁置了25年的浮点规范漏洞说再见了。将模式匹配扩展到switch,允许对表达式进行测试,每个模式都有特定的操作,以便可以简洁而安全地表达复杂的面向数据的查询。允许应用程序通过调用 JVM范围的过滤器工厂来配置特定于上下文和动态选择的反序列化过滤器,以便为每个序列化操作选择一个过滤器。开发平台方面扩展了macOS渲染管道和macOS Aarch64端口。在外部函数和内存API 引入了一个孵化器阶段,允许 Java 程序与 Java 运行时之外的代码和数据进行互操作,与平台无关的矢量 API 作为孵化 API 集成到 JDK16 中,将在 JDK 17 中再次孵化,提供一种机制来表达矢量计算,这些计算在运行时可靠地编译为支持的 CPU 架构上的最佳矢量指令,这相比等效标量计算具有更好的性能。