Java se 基础上

第1章 Java语言概述与开发环境

1.1 Java语言的发展简史

JDK1.0 :

Sun在1996年年初发布了JDK 1.0,该版本包括两部分:运行环境(即JRE)和开发环境(即JDK)。运行环境包括核心API、集成API、用户界面API、发布技术、Java虚拟机(JVM)5个部分;开发环境包括编译Java程序的编译器(即javac命令)。

JDK1.1 :

Sun在1997年2月18日发布了JDK 1.1,该版本增加了JIT(即时编译)编译器。

JDK 1.2 :

1998年12月Sun发布了JDK 1.2,伴随JDK 1.2一同发布的还有JSP/Servlet、EJB等规范,并将Java分成了J2EE、J2SE和J2ME三个版本。JDK 1.2还把它的API分成了三大类:核心API、可选API、特殊API。

JDK1.4 :

2002年2月,Sun发布了JDK 1.4。

JDK1.5 :

2004年10月,Sun发布了JDK 1.5,将JDK 1.5改名为Java SE 5.0,J2EE、J2ME也相应地改名为Java EE和Java ME。JDK 1.5增加了诸如泛型、增强的for语句、可变数量的形参、注释(Annotations)、自动拆箱和装箱等功能;推出了EJB 3.0规范、推出了自己的MVC框架规范:JSF。

JDK 1.6 :

2006年12月,Sun公司发布了JDK 1.6(也被称为Java SE 6)。2009年4月20日,Oracle收购Sun.

JDK 1.7 :

2011年7月28日,Oracle发布Java SE 7,该版本引入了二进制整数、支持字符串的switch语句、菱形语法、多异常捕捉、自动关闭资源的try语句等新特性。

JDK 1.8:

2014年3月18日,Oracle公司发布了Java SE 8,该版本带来了全新的Lambda表达式、流式编程等大量新特性

JDK1.9 :

2017年9月22日,Oracle公司发布了Java SE 9,这次版本升级强化了Java的模块化系统,而且采用了更高效、更智能的G1垃圾回收器,并在核心类库上进行了大量更新。

JDK1.10:

2018年3月如约发布Java 10

JDK1.11 :

2018年9月如约发布Java 11。

1.2 Java程序运行机制

Java语言具有解释型语言、编译型语言的特征,因为Java程序要经过先编译、后解释两个步骤。

1.2.1 高级语言的运行机制

计算机高级语言按程序的执行方式可以分为编译型和解释型两种。

1.2.2 Java程序的运行机制和JVM

Java程序的执行过程必须经过先编译、后解释两个步骤,如图1.1所示。
在这里插入图片描述

Java语言里负责解释执行字节码文件的是Java虚拟机,即JVM(Java Virtual Machine)。JVM是Java程序跨平台的关键部分,只要为不同平台实现了相应的虚拟机,编译后的Java字节码就可以在该平台上运行。JVM是一个抽象的计算机,和实际的计算机一样,它具有指令集并使用不同的存储区域。它负责执行指令,还要管理数据、内存和寄存器。

Oracle公司制定的Java虚拟机规范在技术上规定了JVM的统一标准,具体定义了JVM的如下细节: 指令集、寄存器、 类文件的格式、 栈、 垃圾回收堆、存储区,制定这些规范的目的是为了提供统一的标准,最终实现Java程序的平台无关性。

1.3 开发Java的准备

下载和安装Java 11的JDK、设置PATH环境变量

1.4 第一个Java程序

package InnerClass;

public class demo1 {
	public static void main(String[] args) {
		A a=new A();
		A.B b= a.x();
		b.returnx().printxx();
	}

}


class A{
	private int n;
	public A() {
		this.n=0;
	}
	void printxx() {
		System.out.println("测试"+n);
	}
	
	public B x() {
		return new B();
	}
	class B{
		public A returnx() {
			
			return A.this;
		}
	}
}

1.5 Java程序的基本规则

Java程序必须以类(class)的形式存在,类(class)是Java程序的最小程序单位。

Java程序源文件的扩展名必须是.java,不能是其他文件扩展名。

在通常情况下,Java程序源文件的主文件名可以是任意的。如果Java程序源代码里定义了一个public类,则该源文件的主文件名必须与该public类(也就是该类定义使用了public关键字修饰)的类名相同。

通常有如下建议:一个Java源文件通常只定义一个类,不同的类使用不同的源文件定义。 让Java源文件的主文件名与该源文件中定义的public类同名。

1.6 交互式工具:jshell

1.7 Java 11改进的垃圾回收器

第2章 理解面向对象

Java完全支持面向对象的三种基本特征:继承、封装和多态。Java语言完全以对象为中心,Java程序的最小程序单位是类,整个Java程序由一个一个的类组成。

面向对象的方式由OOA(面向对象分析)、OOD(面向对象设计)和OOP(面向对象编程)组成,其中,OOA和OOD的结构需要使用一种方式来描述并记录,目前业界统一采用UML(统一建模语言)来描述并记录OOA和OOD的结果。

2.1 面向对象

程序的三种基本结构:顺序结构、选择结构、循环结构。

面向对象程序设计方法的基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计。

成员变量(状态数据)+方法(行为)= 类定义

2.1.4 面向对象的基本特征

面向对象方法具有三个基本特征:封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。

封装:指将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能;

继承:是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为特殊的父类,将直接获得父类的属性和方法;

多态:指子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。

除此之外,抽象也是面向对象的重要部分,抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。

2.2 UML(统一建模语言)介绍

UML图大致上可分为静态图和动态图两种,UML 2.0的组成如图2.9所示:
在这里插入图片描述

2.3 Java的面向对象特征

2.3.1 一切都是对象

在Java语言中,除8个基本数据类型值之外,一切都是对象,而对象就是面向对象程序设计的中心。Java通过为对象定义成员变量来描述对象的状态;Java通过为对象定义方法来描述对象的行为。

2.3.2 类和对象

具有相同或相似性质的一组对象的抽象就是类,类是对一类事物的描述,是抽象的、概念上的定义;对象是实际存在的该类事物的个体,也称为实例(instance)。对象的抽象化是类,类的具体化就是对象,也可以说类的实例是对象。

类通常有如下两种主要的结构关系:

一般→特殊关系:这种关系就是典型的继承关系,Java语言使用extends关键字来表示这种继承关系,Java的子类是一种特殊的父类。因此,这种一般→特殊的关系其实是一种“is a”关系。

整体→部分结构关系:也被称为组装结构,这是典型的组合关系,Java语言通过在一个类里保存另一个对象的引用来实现这种组合关系。因此,这种整体→部分结构关系其实是一种“has a”关系。

第3章 数据类型和运算符

Java语言是一门强类型语言,强类型的含义:① 所有的变量必须先声明、后使用;② 指定类型的变量只能接受类型与之匹配的值。

3.1 注释

Java语言的注释共有3种类型:单行注释(使用 //)、多行注释(使用 /* …… /)、 文档注释(使用 /* …… */)。

3.2 标识符和关键字

Java语言里的分隔符: 分号(;)、花括号({})、方括号([])、圆括号(())、空格、圆点(.)。

标识符就是用于给程序中变量、类、方法命名的符号。Java语言的标识符必须以字母、下画线()、美元符( ) 开 头 , 后 面 可 以 跟 任 意 数 目 的 字 母 、 数 字 、 下 画 线 ( ) 和 美 元 符 ( )开头,后面可以跟任意数目的字母、数字、下画线(_)和美元符( 线)。Java语言是区分大小写的,从Java 9开始,不允许使用单独的下画线()作为标识符。

使用标识符时,需要注意如下规则:

➢ 标识符可以由字母、数字、下画线(_)和美元符($)组成,其中数字不能打头。

➢ 标识符不能是Java关键字和保留字,但可以包含关键字和保留字。

➢ 标识符不能包含空格。

➢ 标识符只能包含美元符($),不能包含@、#等其他特殊字符。

Java的所有关键字都是小写的,TRUE、FALSE和NULL都不是Java关键字。
在这里插入图片描述

不仅如此,Java还提供了三个特殊的直接量(literal):true、false和null;Java语言的标识符也不能使用这三个特殊的直接量。从Java 10开始引入的var并不是关键字,它相当于一个可变的类型名(后面会详述),因此var依然可作为标识符。

3.3 数据类型分类

声明变量的语法:type varName [=初始值];

Java语言支持的类型分为两类:基本类型(Primitive Type)和引用类型(Reference Type)。

基本类型:包括boolean类型和数值类型。数值类型有整数类型和浮点类型。整数类型包括byte、short、int、long、char,浮点类型包括float和double。

引用类型:包括类、接口和数组类型,还有一种特殊的null类型。

3.4 基本数据类型

在这里插入图片描述

Java只包含这8种基本数据类型,字符串不是基本数据类型,字符串是一个类,也就是一个引用数据类型。

Java中整数值有4种表示方式:十进制、二进制(Java7 开始支持)、八进制、十六进制,其中二进制的整数以0b或0B开头;八进制的整数以0开头;十六进制的整数以0x或者0X开头,其中10~15分别以a~f(此处的a~f不区分大小写)来表示。

所有数字在计算机底层都是以二进制形式存在的,原码是直接将一个数值换算成二进制数。但计算机以补码的形式保存所有的整数。补码的计算规则:正数的补码和原码完全相同,负数的补码是其反码加1;反码是对原码按位取反,只是最高位(符号位)保持不变。

字符型值有三种表示形式:

➢ 直接通过单个字符来指定字符型值,例如’A’、'9’和’0’等。

➢ 通过转义字符表示特殊字符型值,例如’\n’、’\t’等。

➢ 直接使用Unicode值来表示字符型值,格式是’\uXXXX’,其中XXXX代表一个十六进制的整数。

Java语言中常用的转义字符如表3.2所示。
在这里插入图片描述

char类型的值也可直接作为整型值来使用。

Java语言中的单引号、双引号和反斜线都有特殊的用途,如果一个字符串中包含了这些特殊字符,则应该使用转义字符的表示形式。例如,在Java程序中表示一个绝对路径:“c:\codes”,但这种写法得不到期望的结果,因为Java会把反斜线当成转义字符,所以应该写成这种形式:“c:\codes”,只有同时写两个反斜线,Java才会把第一个反斜线当成转义字符,和后一个反斜线组成真正的反斜线。

Java的浮点数遵循IEEE 754标准,采用二进制数据的科学计数法来表示浮点数,对于float型数值,第1位是符号位,接下来8位表示指数,再接下来的23位表示尾数;对于double类型数值,第1位也是符号位,接下来的11位表示指数,再接下来的52位表示尾数。

Java 7引入了一个新功能:程序员可以在数值中使用下画线,不管是整型数值,还是浮点型数值,都可以自由地使用下画线。

boolean类型的值或变量主要用做旗标来进行流程控制,主要有: if条件控制语句、 while循环控制语句、do while循环控制语句、for循环控制语句、三目运算符(?:)。

Java 10开始支持使用var定义局部变量:var相当于一个动态类型,使用var定义的局部变量的类型由编译器自动推断—定义变量时分配了什么类型的初始值,那该变量就是什么类型。因此,使用var定义局部变量时,必须在定义局部变量的同时指定初始值,否则编译器无法推断该变量的类型。

3.5 基本类型的类型转换

Java中类型转换分为:自动类型转换、强制类型转换。
在这里插入图片描述

强制类型转换的语法格式是:(targetType)value,强制类型转换的运算符是圆括号(())。

当一个算术表达式中包含多个基本类型的值时,整个算术表达式的数据类型将发生自动提升。Java定义了如下的自动提升规则。➢ 所有的byte类型、short类型和char类型将被提升到int类型。➢ 整个算术表达式的数据类型自动提升到与表达式中最高等级操作数同样的类型。操作数的等级排列如图3.10所示,位于箭头右边类型的等级高于位于箭头左边类型的等级。

3.6 直接量

直接量(literal value,也被直译为字面值)是指在程序中通过源代码直接给出的值,例如在int a=5;这行代码中,为变量a所分配的初始值5就是一个直接量。

并不是所有的数据类型都可以指定直接量,能指定直接量的通常只有三种类型:基本类型、字符串类型和null类型。对于null类型的直接量的值只有null一个。

关于字符串直接量有一点需要指出,当程序第一次使用某个字符串直接量时,Java会使用常量池(constantpool)来缓存该字符串直接量,如果程序后面的部分需要用到该字符串直接量时,Java会直接使用常量池中的字符串直接量。

常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括关于类、方法、接口中的常量,也包括字符串直接量。

3.7 运算符

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。

Java语言中的运算符可分为如下几种:

➢ 算术运算符(+ - * / %)

➢ 赋值运算符( = )

➢ 比较运算符(> >= < <= != ==)

➢ 逻辑运算符(&& & || | ! ^)

➢ 位运算符( | ~ ^ << >> >>>)

➢ 类型相关运算符

赋值运算符可与算术运算符、位移运算符结合,扩展成功能更加强大的运算符。扩展后的赋值运算符如下。+= -= *= /= %= &= |= ^= <<= >>= >>>=

三目运算符只有一个 ?:

在这里插入图片描述

第4章 流程控制与数组

4.1 顺序结构

4.2 分支结构

Java提供了两种常见的分支控制结构:if语句和switch语句。

switch语句由一个控制表达式和多个case标签组成,和if语句不同的是,switch语句后面的控制表达式的数据类型只能是byte、short、char、int四种整数类型,枚举类型和java.lang.String类型(从Java 7才允许),不能是boolean类型。

switch语句的语法格式:
在这里插入图片描述

Java 11编译器做了一些改进,如果开发者忘记了case块后面的break语句,Java 11编译器会生成警告:“[fallthrough]可能无法实现case”。这个警告以前需要为javac指定-X:fallthrough选项才能显示出来。

4.3 循环结构

while循环的语法格式如下:
在这里插入图片描述

do while循环的语法格式如下:
在这里插入图片描述

for循环的基本语法格式如下:
在这里插入图片描述

4.4 控制循环结构

Java提供了continue和break来控制循环结构。除此之外,return可以结束整个方法,当然也就结束了一次循环。

break用于完全结束一个循环,跳出循环体。不管是哪种循环,一旦在循环体中遇到break,系统将完全结束该循环,开始执行循环之后的代码。break语句不仅可以结束其所在的循环,还可以直接结束其外层循环。此时需要在break后紧跟一个标签,这个标签用于标识一个外层循环。Java中的标签就是一个紧跟着英文冒号(:)的标识符。与其他语言不同的是,Java中的标签只有放在循环语句之前才有作用。

continue只是忽略本次循环剩下语句,接着开始下一次循环,并不会终止循环;而break则是完全终止循环本身。与break类似的是,continue后也可以紧跟一个标签,用于直接跳过标签所标识循环的当次循环的剩下语句,重新开始下一次循环。

return关键字并不是专门用于结束循环的,return的功能是结束一个方法。

4.5 数组类型

一个数组里只能存储一种数据类型的数据,而不能存储多种数据类型的数据。

数组也是一种数据类型,它本身是一种引用类型。

Java语言支持两种语法格式来定义数组:
在这里插入图片描述

Java语言中数组必须先初始化,然后才可以使用。所谓初始化,就是为数组的数组元素分配内存空间,并为每个数组元素赋初始值。

数组的初始化有如下两种方式。

➢ 静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度。
在这里插入图片描述
在这里插入图片描述

➢ 动态初始化:初始化时程序员只指定数组长度,由系统为数组元素分配初始值。
在这里插入图片描述

从Java 5之后,Java提供了一种更简单的循环:foreach循环,这种循环遍历数组和集合。foreach循环的语法格式如下:
在这里插入图片描述

Java提供的Arrays类里包含的一些static修饰的方法可以直接操作数组。

第5章 面向对象(上)

5.1 类和对象

类可以当成一种自定义类型,可以使用类来定义变量,这种类型的变量统称为引用变量。

类是某一批对象的抽象,可以把类理解成某种概念;对象才是一个具体存在的实体。Java语言是面向对象的程序设计语言,类和对象是面向对象的核心。Java语言提供了对创建类和创建对象简单的语法支持。

Java语言里定义类的简单语法如下:
在这里插入图片描述

在上面的语法格式中,修饰符可以是public、final、abstract,或者完全省略这三个修饰符,类名只要是一个合法的标识符即可,建议Java类名必须是由一个或多个有意义的单词连缀而成的,每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。

对一个类定义而言,可以包含三种最常见的成员:构造器、成员变量和方法,static修饰的成员不能访问没有static修饰的成员。成员变量用于定义该类或该类的实例所包含的状态数据,方法则用于定义该类或该类的实例的行为特征或者功能实现。构造器用于构造该类的实例,Java语言通过new关键字来调用构造器,从而返回该类的实例。构造器是一个类创建对象的根本途径,如果一个类没有构造器,这个类通常无法创建实例。因此,Java语言提供了一个功能:如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为一个类提供了构造器,系统将不再为该类提供构造器。

定义成员变量的语法格式如下:
在这里插入图片描述

➢ 修饰符:修饰符可以省略,也可以是public、protected、private、static、final,其中public、protected、private三个最多只能出现其中之一,可以与static、final组合起来修饰成员变量。

➢ 类型:类型可以是Java语言允许的任何数据类型,包括基本类型和现在介绍的引用类型。

➢ 成员变量名:成员变量名只要是一个合法的标识符即可,建议成员变量名应该由一个或多个有意义的单词连缀而成,第一个单词首字母小写,后面每个单词首字母大写,其他字母全部小写,单词与单词之间不要使用任何分隔符。

➢ 默认值:定义成员变量还可以指定一个可选的默认值。

定义方法的语法格式如下:
在这里插入图片描述

➢ 修饰符:修饰符可以省略,也可以是public、protected、private、static、final、abstract,其中public、protected、private三个最多只能出现其中之一;abstract和final最多只能出现其中之一,它们可以与static组合起来修饰方法。

➢ 方法返回值类型:返回值类型可以是Java语言允许的任何数据类型,包括基本类型和引用类型;如果声明了方法返回值类型,则方法体内必须有一个有效的return语句,该语句返回一个变量或一个表达式,这个变量或者表达式的类型必须与此处声明的类型匹配。除此之外,如果一个方法没有返回值,则必须使用void来声明没有返回值。

➢ 方法名:方法名的命名规则与成员变量的命名规则基本相同,但由于方法用于描述该类或该类的实例的行为特征或功能实现,因此通常建议方法名以英文动词开头。

➢ 形参列表:形参列表用于定义该方法可以接受的参数,形参列表由零组到多组“参数类型 形参名”组合而成,多组参数之间以英文逗号(,)隔开,形参类型和形参名之间以英文空格隔开。一旦在定义方法时指定了形参列表,则调用该方法时必须传入对应的参数值——谁调用方法,谁负责为形参赋值。

定义构造器的语法格式如下:
在这里插入图片描述

➢ 修饰符:修饰符可以省略,也可以是public、protected、private其中之一。

➢ 构造器名:构造器名必须和类名相同。

➢ 形参列表:和定义方法形参列表的格式完全相同

创建对象的根本途径是构造器,通过new关键字来调用某个类的构造器即可创建这个类的实例。

创建对象之后,接下来即可使用该对象了,Java的对象大致有如下作用:

➢ 访问对象的实例变量

➢ 调用对象的方法。

有这样一行代码:Person p=new Person(); 这行代码创建了一个Person实例,也被称为Person对象,这个Person对象被赋给p变量。在这行代码中实际产生了两个东西:一个是p变量,一个是Person对象。

Java提供了一个this关键字,this关键字总是指向调用该方法的对象。

根据this出现位置的不同,this作为对象的默认引用有两种情形:

➢ 构造器中引用该构造器正在初始化的对象。

➢ 在方法中引用调用该方法的对象。

this关键字最大的作用就是让类中一个方法,访问该类里的另一个方法或实例变量。

5.2 方法详解

Java语言里,方法不能独立存在,方法必须属于类或对象。一旦将一个方法定义在某个类的类体内,如果这个方法使用了static修饰,则这个方法属于这个类,否则这个方法属于这个类的实例。

Java里方法的参数传递方式只有一种:值传递。所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。

从JDK 1.5之后,Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的形参。如果在定义方法时,在最后一个形参的类型后增加三点(…),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

注意:个数可变的形参只能处于形参列表的最后。一个方法中最多只能包含一个个数可变的形参。个数可变的形参本质就是一个数组类型的形参,因此调用包含个数可变形参的方法时,该个数可变的形参既可以传入多个参数,也可以传入一个数组。

一个方法体内调用它自身,被称为方法递归。只要一个方法的方法体实现中再次调用了方法本身,就是递归方法。递归一定要向已知方向递归。

如果同一个类中包含了两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。方法重载的要求就是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值类型、修饰符等,与方法重载没有任何关系。

5.3 成员变量和局部变量

成员变量指的是在类里定义的变量,也就是前面所介绍的field;局部变量指的是在方法里定义的变量。变量名称建议第一个单词首字母小写,后面每个单词首字母大写。Java程序中的变量划分如图5.9所示。
在这里插入图片描述

成员变量无须显式初始化,只要为一个类定义了类变量或实例变量,系统就会在这个类的准备阶段或创建该类的实例时进行默认初始化。

局部变量除形参之外,都必须显式初始化。也就是说,必须先给方法局部变量和代码块局部变量指定初始值,否则不可以访问它们。

当系统加载类或创建该类的实例时,系统自动为成员变量分配内存空间,并在分配内存空间后,自动为成员变量指定初始值。

局部变量定义后,必须经过显式初始化后才能使用,系统不会为局部变量执行初始化。这意味着定义局部变量后,系统并未为这个变量分配内存空间,直到等到程序为这个变量赋初始值时,系统才会为局部变量分配内存,并将初始值保存到这块内存中。与成员变量不同,局部变量不属于任何类或实例,因此它总是保存在其所在方法的栈内存中。如果局部变量是基本类型的变量,则直接把这个变量的值保存在该变量对应的内存中;如果局部变量是一个引用类型的变量,则这个变量里存放的是地址,通过该地址引用到该变量实际引用的对象或数组。栈内存中的变量无须系统垃圾回收,往往随方法或代码块的运行结束而结束。

5.4 隐藏和封装

封装实际上有两个方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两个方面都需要通过使用Java提供的访问控制符来实现。

Java提供了3个访问控制符:private、protected和public,分别代表了3个访问控制级别,另外还有一个不加任何访问控制符的访问控制级别,提供了4个访问控制级别。Java的访问控制级别由小到大如图5.14所示:
在这里插入图片描述
在这里插入图片描述

关于访问控制符的使用,存在如下几条基本原则。

➢ 类里的绝大部分成员变量都应该使用private修饰,只有一些static修饰的、类似全局变量的成员变量,才可能考虑使用public修饰。除此之外,有些方法只用于辅助实现该类的其他方法,这些方法被称为工具方法,工具方法也应该使用private修饰。

➢ 如果某个类主要用做其他类的父类,该类里包含的大部分方法可能仅希望被其子类重写,而不想被外界直接调用,则应该使用protected修饰这些方法。

➢ 希望暴露出来给其他类自由调用的方法应该使用public修饰。因此,类的构造器通过使用public修饰,从而允许在其他地方创建该类的实例。因为外部类通常都希望被其他类自由使用,所以大部分外部类都使用public修饰。

package语句必须作为源文件的第一条非注释性语句,一个源文件只能指定一个包,即只能包含一条package语句,该源文件中可以定义多个类,则这些类将全部位于该包下。如果没有显式指定package语句,则处于默认包下。

import可以向某个Java文件中导入指定包层次下某个类或全部类,import语句应该出现在package语句(如果有的话)之后、类定义之前。

JDK 1.5以后更是增加了一种静态导入的语法,它用于导入指定类的某个静态成员变量、方法或全部的静态成员变量、方法。静态导入使用import static语句,静态导入也有两种语法,分别用于导入指定类的单个静态成员变量、方法和全部静态成员变量、方法

package 静态导入;
import static java.lang.Math.abs;
import static java.lang.Math.max;
import static java.lang.Math.pow;
public class StaticImportDemo4
{
	public static void main(String[] args)
	{
		System.out.println(abs(-10));
		System.out.println(pow(2,3));
		System.out.println(max(23, 34));	
	}
}

Java的核心类都放在java包以及其子包下,Java扩展的许多类都放在javax包以及其子包下。

下面几个包是Java语言中的常用包:

➢ java.lang:这个包下包含了Java语言的核心类,如String、Math、System和Thread类等,使用这个包下的类无须使用import语句导入,系统会自动导入这个包下的所有类。

➢ java.util:这个包下包含了Java的大量工具类/接口和集合框架类/接口,例如Arrays和List、Set等。

➢ java.net:这个包下包含了一些Java网络编程相关的类/接口。

➢ java.io:这个包下包含了一些Java输入/输出编程相关的类/接口。

➢ java.text:这个包下包含了一些Java格式化相关的类。

➢ java.sql:这个包下包含了Java进行JDBC数据库编程的相关类/接口。

➢ java.awt:这个包下包含了抽象窗口工具集的相关类/接口,这些类主要用于构建图形用户界面(GUI)程序。

➢ java.swing:这个包下包含了Swing图形用户界面编程的相关类/接口,这些类可用于构建平台无关的GUI程序。

5.5 深入构造器

构造器最大的用处就是在创建对象时执行初始化。

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。

使用this调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用this调用重载的构造器时,系统会根据this后括号里的实参来调用形参列表与之对应的构造器。

5.6 类的继承

Java的继承通过extends关键字来实现,实现继承的类被称为子类,被继承的类被称为父类,有的也称其为基类、超类。父类和子类的关系,是一种一般和特殊的关系。(子类是一个特殊父类,is a关系)

注意:子类只能从被扩展的父类获得成员变量、方法和内部类(包括内部接口、枚举),不能获得构造器和初始化块。

如果定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。因此,java.lang.Object类是所有类的父类,要么是其直接父类,要么是其间接父类。因此所有的Java对象都可调用java.lang.Object类所定义的实例方法。

子类包含与父类同名方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。

方法的重写要遵循“两同两小一大”规则,“两同”即方法名相同、形参列表相同;“两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;“一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。尤其需要指出的是,覆盖方法和被覆盖方法要么都是类方法,要么都是实例方法,不能一个是类方法,一个是实例方法。
————————————————————————————————————————————————
类方法
用static修饰的方法。

由于类方法是属于整个类的,所以类方法的方法体中不能有与类的对象有关的内容。
即类方法体有如下限制:
1.类方法中不能引用对象变量;
2.类方法中不能调用类的对象方法;
3.在类方法中不能调使用super,this关键字;
4.类方法不能被覆盖。

实例方法
当一个类创建了一个对象后,这个对象就可以调用该类的方法(对象方法)。

1.实例方法中可以引用对象变量,也可以引用类变量;
2.实例方法中可以调用类方法;
3.对象方法中可以使用super,this关键字。
————————————————————————————————————————————————

当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类中被覆盖的方法,则可以使用super(被覆盖的是实例方法)或者父类类名(被覆盖的是类方法)作为调用者来调用父类中被覆盖的方法。

如果父类方法具有private访问权限,则该方法对其子类是隐藏的,因此其子类无法访问该方法,也就是无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。

重载主要发生在同一个类的多个同名方法之间,而重写发生在子类和父类的同名方法之间。

super是Java提供的一个关键字,super用于限定该对象调用它从父类继承得到的实例变量或方法。

5.7 多态

Java引用变量有两个类型:一个是编译时类型,一个是运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就可能出现所谓的多态(Polymorphism)。

相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态。与方法不同的是,对象的实例变量则不具备多态性。

instanceof运算符的前一个操作数通常是一个引用类型变量,后一个操作数通常是一个类(也可以是接口,可以把接口理解成一种特殊的类),它用于判断前面的对象是否是后面的类,或者其子类、实现类的实例。如果是,则返回true,否则返回false。

在使用instanceof运算符时需要注意:instanceof运算符前面操作数的编译时类型要么与后面的类相同,要么与后面的类具有父子继承关系,否则会引起编译错误。

5.8 继承与组合

继承是实现类复用的重要手段,但继承带来了一个最大的坏处:破坏封装。相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供更好的封装性。

子类扩展父类时,子类可以从父类继承得到成员变量和方法,如果访问权限允许,子类可以直接访问父类的成员变量和方法,相当于子类可以直接复用父类的成员变量和方法。

为了保证父类有良好的封装性,不会被子类随意改变,设计父类通常应该遵循如下规则。

➢ 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private访问类型,不要让子类直接访问父类的成员变量。

➢ 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private访问控制符修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则必须以public修饰,但又不希望子类重写该方法,可以使用final修饰符(该修饰符后面会有更详细的介绍)来修饰该方法;如果希望父类的某个方法被子类重写,但不希望被其他类自由访问,则可以使用protected来修饰该方法。

➢ 尽量不要在父类构造器中调用将要被子类重写的方法。

如果想把某些类设置成最终类,即不能被当成父类,则可以使用final修饰这个类,例如JDK提供的java.lang.String类和java.lang.System类。除此之外,使用private修饰这个类的所有构造器,从而保证子类无法调用该类的构造器,也就无法继承该类。对于把所有的构造器都使用private修饰的父类而言,可另外提供一个静态方法,用于创建该类的实例。

如果需要复用一个类,除把这个类当成基类来继承之外,还可以把该类当成另一个类的组合成分,从而允许新类直接复用该类的public方法。

5.9 初始化块

一个类里可以有多个初始化块,相同类型的初始化块之间有顺序:前面定义的初始化块先执行,后面定义的初始化块后执行。

在 Java 语言中,当实例化对象时,对象所在类的所有成员变量首先要进行初始化,只有当所有类成员完成初始化后,才会调用对象所在类的构造函数创建象。

初始化一般遵循3个原则:

静态对象(变量)优先于非静态对象(变量)初始化,静态对象(变量)只初始化一次,而非静态对象(变量)可能会初始化多次;
父类优先于子类进行初始化;
按照成员变量的定义顺序进行初始化。 即使变量定义散布于方法定义之中,它们依然在任何方法(包括构造函数)被调用之前先初始化;
加载顺序

父类(静态变量、静态语句块)
子类(静态变量、静态语句块)
父类(实例变量、普通语句块)
父类(构造函数)
子类(实例变量、普通语句块)
子类(构造函数)

5.10 static关键字

在类中,用static声明的成员变量为静态成员变量,也成为类变量。类变量的生命周期和类相同,在整个应用程序执行期间都有效。

这里要强调一下:

  • static修饰的成员变量和方法,从属于类

  • 普通变量和方法从属于对象

  • 静态方法不能调用非静态成员,编译会报错

static关键字的用途
一句话描述就是:方便在没有创建对象的情况下进行调用(方法/变量)。

显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

static可以用来修饰类的成员方法、类的成员变量,另外也可以编写static代码块来优化程序性能

static方法
static方法也成为静态方法,由于静态方法不依赖于任何对象就可以直接访问,因此对于静态方法来说,是没有this的,因为不依附于任何对象,既然都没有对象,就谈不上this了,并且由于此特性,在静态方法中不能访问类的非静态成员变量和非静态方法,因为非静态成员变量和非静态方法都必须依赖于具体的对象才能被调用。

虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法和静态成员变量。
在这里插入图片描述
从上面代码里看出:

  • 静态方法test2()中调用非静态成员变量address,编译失败。这是因为,在编译期并没有对象生成,address变量根本就不存在。

  • 静态方法test2()中调用非静态方法test1(),编译失败。这是因为,编译器无法预知在非静态成员方法test1()中是否访问了非静态成员变量,所以也禁止在静态方法中调用非静态成员方法

  • 非静态成员方法test1()访问静态成员方法test2()/变量name是没有限制的

所以,如果想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。最常见的静态方法就是main方法,这就是为什么main方法是静态方法就一目了然了,因为程序在执行main方法的时候没有创建任何对象,只有通过类名来访问。

特别说明:static方法是属于类的,非实例对象,在JVM加载类时,就已经存在内存中,不会被虚拟机GC回收掉,这样内存负荷会很大,但是非static方法会在运行完毕后被虚拟机GC掉,减轻内存压力

static变量
static变量也称为静态变量,静态变量和非静态变量的区别:

  • 静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化
  • 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响

static成员变量初始化顺序按照定义的顺序来进行初始化

static块
构造方法用于对象的初始化。静态初始化块,用于类的初始化操作。

在静态初始化块中不能直接访问非staic成员。

static块的作用
静态初始化块的作用就是:提升程序性能。

为什么说静态初始化块能提升程序性能,代码示例如下:

class Person{
    private Date birthDate;

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    boolean isBornBoomer() {
        Date startDate = Date.valueOf("1946");
        Date endDate = Date.valueOf("1964");
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行

静态初始化块可以置于类中的任何地方,类中可以有多个静态初始化块。
在类初次被加载时,会按照静态初始化块的顺序来执行每个块,并且只会执行一次。

static关键字的误区
static关键字会改变类中成员的访问权限吗?

有些初学的朋友会将java中的static与C/C++中的static关键字的功能混淆了。在这里只需要记住一点:与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。看下面的例子就明白了:


public class Person {

    public String name = "李四";

    private static String address = "中国";
}

在这里插入图片描述

说明static关键字不能改变变量和方法的访问权限

能通过this访问静态成员变量吗?

public class Main {  
    static int value = 33;

    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }

    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

这段代码输出结果为:33

this代表什么?this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

static能作用于局部变量么?

static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定

static常见笔试面试题
1、下面这段代码的输出结果是什么?

public class Test extends Base{

    static{
        System.out.println("test static");
    }

    public Test(){
        System.out.println("test constructor");
    }

    public static void main(String[] args) {
        new Test();
    }
}

class Base{

    static{
        System.out.println("base static");
    }

    public Base(){
        System.out.println("base constructor");
    }
}

输出结果为

base static
test static
base constructor
test constructor

分析下这段代码的执行过程:

找到main方法入口,main方法是程序入口,但在执行main方法之前,要先加载Test类

加载Test类的时候,发现Test类继承Base类,于是先去加载Base类

加载Base类的时候,发现Base类有static块,而是先执行static块,输出base static结果

Base类加载完成后,再去加载Test类,发现Test类也有static块,而是执行Test类中的static块,输出test static结果

Base类和Test类加载完成后,然后执行main方法中的new Test(),调用子类构造器之前会先调用父类构造器

调用父类构造器,输出base constructor结果

然后再调用子类构造器,输出test constructor结果

2、这段代码的输出结果是什么?

public class Test {
    Person person = new Person("Test");
    static{
        System.out.println("test static");
    }

    public Test() {
        System.out.println("test constructor");
    }

    public static void main(String[] args) {
        new MyClass();
    }
}

class Person{
    static{
        System.out.println("person static");
    }
    public Person(String str) {
        System.out.println("person "+str);
    }
}


class MyClass extends Test {
    Person person = new Person("MyClass");
    static{
        System.out.println("myclass static");
    }

    public MyClass() {
        System.out.println("myclass constructor");
    }
}

结果为

test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

为什么输出结果是这样的?我们来分析下这段代码的执行过程:

找到main方法入口,main方法是程序入口,但在执行main方法之前,要先加载Test类

加载Test类的时候,发现Test类有static块,而是先执行static块,输出test static结果

然后执行new MyClass(),执行此代码之前,先加载MyClass类,发现MyClass类继承Test类,而是要先加载Test类,Test类之前已加载

加载MyClass类,发现MyClass类有static块,而是先执行static块,输出myclass static结果

然后调用MyClass类的构造器生成对象,在生成对象前,需要先初始化父类Test的成员变量,而是执行Person person = new Person(“Test”)代码,发现Person类没有加载

加载Person类,发现Person类有static块,而是先执行static块,输出person static结果

接着执行Person构造器,输出person Test结果

然后调用父类Test构造器,输出test constructor结果,这样就完成了父类Test的初始化了

再初始化MyClass类成员变量,执行Person构造器,输出person MyClass结果

最后调用MyClass类构造器,输出myclass constructor结果,这样就完成了MyClass类的初始化了

3、这段代码的输出结果是什么?

public class Test {

    static{
        System.out.println("test static 1");
    }
    public static void main(String[] args) {

    }

    static{
        System.out.println("test static 2");
    }
}

结果为

test static 1
test static 2

虽然在main方法中没有任何语句,但是还是会输出,原因上面已经讲述过了。另外,static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。

第6章 面向对象(下)

6.1 包装类

在这里插入图片描述

JDK 1.5提供了自动装箱(Autoboxing)和自动拆箱(AutoUnboxing)功能。

包装类还可实现基本类型变量和字符串之间的转换。

把字符串类型的值转换为基本类型的值有两种方式。

➢ 利用包装类提供的parseXxx(String s)静态方法(除Character之外的所有包装类都提供了该方法。

public class MyDemo7 {
    public static void main(String[] args) {
        boolean a = Boolean.parseBoolean("true");
        System.out.println(a);  //true
        int i = Integer.parseInt("123");
        System.out.println(i); //123
        byte abc = Byte.parseByte("46");
        System.out.println(abc);  //46
        long l = Long.parseLong("1515661515");
        System.out.println(l);  //1515661515
        //......
    }
}

➢ 利用包装类提供的valueOf(String s)静态方法。

        String markString = "45.9";
		
		double m1 = Double.parseDouble(markString);
		System.out.println(m1);
		
		double m2 = Double.valueOf(markString);
		System.out.println(m2);
		
		double m3 = new Double(markString);
		System.out.println(m3);

String类也提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串。

         int num = 12;
		String numsString = String.valueOf(num);
		System.out.println("number = " + numsString);

Java 7增强了包装类的功能,Java 7为所有的包装类都提供了一个静态的compare(xxx val1,xxx val2)方法,这样开发者就可以通过包装类提供的compare(xxx val1,xxx val2)方法来比较两个基本类型值的大小,包括比较两个boolean类型值,两个boolean类型值进行比较时,true>false。

6.2 处理对象

Object类提供的toString()方法总是返回该对象实现类的“类名+@+hashCode”值,这个返回值并不能真正实现“自我描述”的功能,因此如果用户需要自定义类能实现“自我描述”的功能,就必须重写Object类的toString()方法。

public class test {
    private String name = "123";
    private int id = 1;
    private boolean married = false;
 
    @Override
    public String toString() {
        return "test{" +
                "name='" + name + '\'' +
                ", id=" + id +
                ", married=" + married +
                '}';
    }
 
    public static void main(String[] args) {
        test t =new test();
        System.out.println(t);
        String s= "This is a String:" + t;
        System.out.println(s);
    }
}

Java程序中测试两个变量是否相等有两种方式:一种是利用运算符,另一种是利用equals()方法。当使用来判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),则只要两个变量的值相等,就将返回true。但对于两个引用类型变量,只有它们指向同一个对象时,==判断才会返回true。==不可用于比较类型上没有父子关系的两个对象。

6.3 类成员

在Java类里只能包含成员变量、方法、构造器、初始化块、内部类(包括接口、枚举)5种成员。

6.4 final修饰符

对于final修饰的成员变量而言,一旦有了初始值,就不能被重新赋值,如果既没有在定义成员变量时指定初始值,也没有在初始化块、构造器中为成员变量指定初始值,那么这些成员变量的值将一直是系统默认分配的0、’\u0000’、false或null,这些成员变量也就完全失去了存在的意义。因此Java语法规定:final修饰的成员变量必须由程序员显式地指定初始值。

归纳起来,final修饰的类变量、实例变量能指定初始值的地方如下:

➢ 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。

➢ 实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在三个地方的其中之一指定。

系统不会对局部变量进行初始化,局部变量必须由程序员显式初始化。因此使用final修饰局部变量时,既可以在定义时指定默认值,也可以不指定默认值。如果final修饰的局部变量在定义时没有指定默认值,则可以在后面代码中对该final变量赋初始值,但只能一次,不能重复赋值;如果final修饰的局部变量在定义时已经指定默认值,则后面代码中不能再对该变量赋值。final修饰的形参因为形参在调用该方法时,由系统根据传入的参数来完成初始化,因此使用final修饰的形参不能被赋值。

当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变。但对于引用类型变量而言,它保存的仅仅是一个引用,final只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。

对一个final变量来说,不管它是类变量、实例变量,还是局部变量,只要该变量满足三个条件,这个final变量就不再是一个变量,而是相当于一个直接量。➢ 使用final修饰符修饰。➢ 在定义该final变量时指定了初始值。➢ 该初始值可以在编译时就被确定下来。

注意:final修饰符的一个重要用途就是定义“宏变量”。当定义final变量时就为该变量指定了初始值,而且该初始值可以在编译时就确定下来,那么这个final变量本质上就是一个“宏变量”,编译器会把程序中所有用到该变量的地方直接替换成该变量的值。

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

final修饰的类不可以有子类,例如java.lang.Math类就是一个final类,它不可以有子类。

不可变(immutable)类的意思是创建该类的实例后,该实例的实例变量是不可改变的。Java提供的8个包装类和java.lang.String类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变。

如果需要创建自定义的不可变类,可遵守如下规则。

➢ 使用private和final修饰符来修饰该类的成员变量。

➢ 提供带参数的构造器(或返回该实例的类方法),用于根据传入参数来初始化类里的成员变量。

➢ 仅为该类成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量。

➢ 如果有必要,重写Object类的hashCode()和equals()方法(关于重写hashCode()的步骤可参考8.3.1节)。equals()方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()方法判断为相等的对象的hashCode()也相等。

6.5 抽象类

抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法。

抽象方法和抽象类的规则如下:

➢ 抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。

➢ 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。

➢ 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。

➢ 含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。

定义抽象方法只需在普通方法上增加abstract修饰符,并把普通方法的方法体(也就是方法后花括号括起来的部分)全部去掉,并在方法后增加分号即可。

定义抽象类只需在普通类上增加abstract修饰符即可。甚至一个普通类(没有包含抽象方法的类)增加abstract修饰符后也将变成抽象类。

当使用abstract修饰类时,表明这个类只能被继承;当使用abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。而final修饰的类不能被继承,final修饰的方法不能被重写。因此final和abstract永远不能同时使用

注意:abstract不能用于修饰成员变量,不能用于修饰局部变量,即没有抽象变量、没有抽象成员变量等说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器。

public abstract 返回值类型 方法名(参数);
抽象类定义的格式:
public abstract class 类名 {
}
看如下代码:
//员工 
public abstract class Employee{
	public abstract void work();//抽象函数。需要abstract修饰,并分号;结束
}

//manager
public class Teacher extends Employee {
	public void work() {
		System.out.println("正在赋予权限");
	}
}

//customer
public class Assistant extends Employee {
	public void work() {
		System.out.println("正在使用该系统");
	}
}

//开发人员
public class Manager extends Employee {
	public void work() {
		System.out.println("正在维护此系统");
	}
}

6.6 Java 9改进的接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”—接口(interface)。

Java 8对接口进行了改进,允许在接口中定义默认方法和类方法,默认方法和类方法都可以提供方法实现,Java 9为接口增加了一种私有方法,私有方法也可提供方法实现。

接口定义使用interface关键字,接口定义的基本语法如下:

在这里插入图片描述

➢ 修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符。

➢ 接口名应与类名采用相同的命名规则

➢ 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法、默认方法或私有方法)、内部类(包括内部接口、枚举)定义。

接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。

接口主要有如下用途:➢ 定义变量,也可用于进行强制类型转换。➢ 调用接口中定义的常量。➢ 被其他类实现。

一个类可以实现一个或多个接口,继承使用extends关键字,实现则使用implements关键字。因为一个类可以实现多个接口,这也是Java为单继承灵活性不足所做的补充。类实现接口的语法格式如下:

接口与抽象类异同点:

接口和抽象类很像,它们都具有如下特征(同):

➢ 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。

➢ 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

接口和抽象类在用法上也存在如下差别:

➢ 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法。

➢ 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。

➢ 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。

➢ 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。

➢ 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口。

➢ 接口是一种规范,抽象类是模板模式。

6.7 内部类

Java从JDK 1.1开始引入内部类,内部类主要有如下作用:

➢ 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。

➢ 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。

➢ 匿名内部类适合用于创建那些仅需要一次使用的类。

从语法角度来看,定义内部类与定义外部类的语法大致相同,内部类除需要定义在其他类里面之外,还存在如下两点区别:

➢ 内部类比外部类可以多使用三个修饰符:private、protected、static—外部类不可以使用这三个修饰符

非静态内部类不能拥有静态成员

6.7.1 非静态内部类

内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员;局部内部类和匿名内部类则不是类成员。

成员内部类分为两种:静态内部类和非静态内部类,使用static修饰的成员内部类是静态内部类,没有使用static修饰的成员内部类是非静态内部类。

成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClass.class。

当在非静态内部类的方法内访问某个变量时,系统优先在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则到该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果依然不存在,系统将出现编译错误:提示找不到该变量。因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。

非静态内部类的成员可以访问外部类的实例成员,但反过来就不成立了。如果外部类需要访问非静态内部类的实例成员,则必须显式创建非静态内部类对象来调用访问其实例成员。

根据静态成员不能访问非静态成员的规则,外部类的静态方法、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。

Java不允许在非静态内部类里定义静态成员,非静态内部类里不能有静态方法、静态成员变量、静态初始化块。

class Outer {
    private String name = "test";
    public  static int age =20;

    class Inner{
        //public static int num =10;错误,成员内部类不能有静态变量
        public void fun()
        {
            System.out.println(name);
            System.out.println(age);
        }
    }
}
public class Test{
    public static void main(String [] args)
    {}
}

6.7.2 静态内部类

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。

class Outer {
    public String name = "test";
    private static int age =20;

    static class Inner{
        private String name;
        public void fun()
        {
            //System.out.println(name);错误,引用了外部类的非静态成员方法
            System.out.println(age);
        }
    }
}
public class Test{
    public static void main(String [] args)
    {
        Outer.Inner in = new Outer.Inner();
    }
}

6.7.3 使用内部类

1.在外部类内部使用内部类

从前面程序中可以看出,在外部类内部使用内部类时,与平常使用普通类没有太大的区别。一样可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。唯一存在的一个区别是:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。在外部类内部定义内部类的子类与平常定义子类也没有太大的区别。

class A{
    public A(){
    }
    B b=new B();
    private class B{
        public B(){
        }
    }
}

2.在外部类以外使用非静态内部类

如果希望在外部类以外的地方访问内部类(包括静态和非静态两种),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。

对于使用其他访问控制符修饰的内部类,则能在访问控制符对应的访问权限内使用:

➢ 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类所访问。

➢ 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。

➢ 使用public修饰的内部类,可以在任何地方被访问。

在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:

在这里插入图片描述

由于非静态内部类的对象必须寄生在外部类的对象里,因此创建非静态内部类对象之前,必须先创建其外部类对象。在外部类以外的地方创建非静态内部类实例的语法如下:
在这里插入图片描述

public class demo1 {
	public static void main(String[] args) {
		A a=new A();
		A.B b= a.x();
		b.returnx().printxx();
		A.B b1=new A().new B();
		A a2=new A();
		A.B b2=a2.new B();
		
	}

}



class A{
	private int n;
	public A() {
		this.n=0;
	}
	void printxx() {
		System.out.println("测试"+n);
	}
	
	public B x() {
		return new B();
	}
	public class B{
		public A returnx() {
			
			return A.this;
		}
		
	}
}

3.在外部类以外使用静态内部类

因为静态内部类是外部类类相关的,因此创建静态内部类对象时无须创建外部类对象。在外部类以外的地方创建静态内部类实例的语法如下:

在这里插入图片描述

class Outer {
    public String name = "test";
    private static int age =20;

    static class Inner{
        private String name;
        public void fun()
        {
            System.out.println(name);
            System.out.println(age);
        }
    }
}
public class Test{
    public static void main(String [] args)
    {
        Outer.Inner in = new Outer.Inner();
    }
}

6.7.4 局部内部类

如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此**局部内部类也不能使用访问控制符和static修饰符修饰。**方法内部类如果想要使用方法形参,该形参必须使用final声明(JDK8形参变为隐式final声明)

class Outer{
    private int num =5;
    public void dispaly(final int temp)
    {
        //方法内部类即嵌套在方法里面
        class Inner{
        System.out.println(temp);//如果temp没有被final修饰,则不能引用,否则报错
        }
    }
}
public class Test{
    public static void main(String[] args)
    {}
}

6.7.5 匿名内部类

匿名内部类适合创建那种只需要一次使用的类,定义匿名内部类的格式如下:
在这里插入图片描述
1.匿名内部类必须继承一个抽象类或者实现一个接口。
2.匿名内部类没有类名,因此没有构造方法。

//匿名内部类
//声明一个接口
interface MyInterface {
    //接口中方法没有方法体
    void test();
}
class Outer{
    private int num = 5;
    public void dispaly(int temp)
    {
        //匿名内部类,匿名的实现了MyInterface接口
        //隐藏的class声明
        new MyInterface()
        {
            public void test()
            {
                System.out.println("匿名实现MyInterface接口");
                System.out.println(temp);
            }
        }.test();
    }
}
public class Test{
    public static void main(String[] args)
    {
        Outer out = new Outer();
        out.dispaly(3);
    }
}

关于匿名内部类还有如下两条规则:

➢ 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。

➢ 匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

6.8 Java 11增强的Lambda表达式

当使用Lambda表达式代替匿名内部类创建对象时,Lambda表达式的代码块将会代替实现抽象方法的方法体,Lambda表达式就相当一个匿名方法。

从上面语法格式可以看出,Lambda表达式的主要作用就是代替匿名内部类的烦琐语法,它由三部分组成:

➢ 形参列表。形参列表允许省略形参类型。如果形参列表中只有一个参数,甚至连形参列表的圆括号也可以省略。

➢ 箭头(->)。必须通过英文中画线和大于符号组成。

➢ 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号,那么这条语句就不要用花括号表示语句结束。Lambda代码块只有一条return语句,甚至可以省略return关键字。Lambda表达式需要返回值,而它的代码块中仅有一条省略了return的语句,Lambda表达式会自动返回这条语句的值。

// 1. 不需要参数,返回值为 5  
() -> 5  
  
// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  
  
// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  
  
// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  
  
// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

Lambda表达式的类型,也被称为“目标类型(target type)”,Lambda表达式的目标类型必须是“函数式接口(functional interface)”。函数式接口代表只包含一个抽象方法的接口。函数式接口可以包含多个默认方法、类方法,但只能声明一个抽象方法。

前面已经介绍过,如果Lambda表达式的代码块只有一条代码,程序就可以省略Lambda表达式中代码块的花括号。不仅如此,如果Lambda表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。方法引用和构造器引用可以让Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。Lambda表达式支持如表6.2所示的几种引用方式:

在这里插入图片描述

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

Lambda表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用。

Lambda表达式与匿名内部类存在如下相同点。

➢ Lambda表达式与匿名内部类一样,都可以直接访问“effectively final”的局部变量,以及外部类的成员变量(包括实例变量和类变量)。

➢ Lambda表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认方法。

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

➢ 匿名内部类可以为任意接口创建实例—不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可;但Lambda表达式只能为函数式接口创建实例。

➢ 匿名内部类可以为抽象类甚至普通类创建实例;但Lambda表达式只能为函数式接口创建实例。

➢ 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但Lambda表达式的代码块不允许调用接口中定义的默认方法。

1.用lambda表达式实现Runnable
lambda表达式替换了原来匿名内部类的写法,没有了匿名内部类繁杂的代码实现,而是突出了,真正的处理代码。最好的示例就是 实现Runnable 的线程实现方式了: 用() -> {}代码块替代了整个匿名类

    @Test
    public void test(){
        //old
        new Thread((new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类 实现线程");
            }
        })).start();
        //lambda
        new Thread( () -> System.out.println("java8 lambda实现线程")).start();
    }
@Data
@ToString
@Accessors(chain = true)
public class Person {
    private String name;
    private int age;
    private String sex;
}
 
 
private List<Person> getPersonList(){
        Person p1 = new Person().setName("liu").setAge(22).setSex("male");
        Person p2 = new Person().setName("zhao").setAge(21).setSex("male");
        Person p3 = new Person().setName("li").setAge(18).setSex("female");
        Person p4 = new Person().setName("wang").setAge(21).setSex("female");
        List<Person> list = Lists.newArrayList();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        return list;
    }

2.forEach 遍历集合
使用 forEach方法,直接通过一行代码即可完成对集合的遍历:

    @Test
    public void test1(){
        List<Person> list = getPersonList();
        list.forEach(person -> System.out.println(person.toString()));
    }
 
结果
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)
    Person(name=li, age=18, sex=female)
    Person(name=wang, age=21, sex=female)

双冒号:: 表示方法引用,可以引用其他方法

    @Test
    public void test2(){
        List<Person> list = getPersonList();
        Consumer<Person> changeAge = e -> e.setAge(e.getAge() + 3);
        list.forEach(changeAge);
        list.forEach(System.out::println);
    }
 
    结果:
    Person(name=liu, age=25, sex=male)
    Person(name=zhao, age=24, sex=male)
    Person(name=li, age=21, sex=female)
    Person(name=wang, age=24, sex=female)

3.filter 对集合进行过滤
filter 可以根据传入的 Predicate 对象,对集合进行过滤操作,Predicate 实质就是描述了过滤的条件:

    @Test
    public void test3(){
        List<Person> list = getPersonList();
        list.stream().filter(e -> e.getAge() > 20)
                     .forEach(e -> System.out.println(e.toString()));
    }
       
    结果:
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)
    Person(name=wang, age=21, sex=female)

当需要通过 多个过滤条件对集合进行过滤时,可以采取两种方式:

   1.可以通过调用多次filter 通过传入不同的 Predicate对象来进行过滤

   2.也可以通过 Predicate 对象的 and  or 方法,对多个Predicate 对象进行 且 或 操作
    @Test
    public void test4(){
        List<Person> list = getPersonList();
 
        Predicate<Person> ageFilter = e -> e.getAge() > 20;
        Predicate<Person> sexFilter = e -> e.getSex().equals("male");
 
        //多条件过滤
        list.stream().filter(ageFilter)
                     .filter(sexFilter)
                     .forEach(e -> System.out.println(e.toString()));
        System.out.println("----------------------------");
        // Predicate : and or
        list.stream().filter(ageFilter.and(sexFilter))
                     .forEach(e -> System.out.println(e.toString()));
    }
 
 
    结果:
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)
    ----------------------------
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)

4.limit 限制结果集的数据量
limit 可以控制 结果集返回的数据条数:返回三条数据,返回年龄>20的前两条数据

    @Test
    public void  test5(){
        List<Person> list = getPersonList();
        list.stream().limit(3).forEach(e -> System.out.println(e.toString()));
 
        System.out.println("----------------------------");
        list.stream().limit(2).filter(e -> e.getAge() > 20)
                     .forEach(e -> System.out.println(e.toString()));
    }
 
 
    结果:
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)
    Person(name=li, age=18, sex=female)
    ----------------------------
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)

5. sorted 排序
通过sorted,可以按自定义的规则,对数据进行排序,可以用两种写法,分别按 年龄 和 姓名排序

    @Test
    public void test6(){
        List<Person> list = getPersonList();
        //年龄排序
        list.stream().sorted((p1,p2) -> (p1.getAge() - p2.getAge()))
                     .forEach(e -> System.out.println(e.toString()));
        //姓名排序
        System.out.println("----------------------------");
        list.stream().sorted(Comparator.comparing(Person::getName))
                     .forEach(e -> System.out.println(e.toString()));
    }
 
 
    结果:
    Person(name=li, age=18, sex=female)
    Person(name=zhao, age=21, sex=male)
    Person(name=wang, age=21, sex=female)
    Person(name=liu, age=22, sex=male)
    ----------------------------
    Person(name=li, age=18, sex=female)
    Person(name=liu, age=22, sex=male)
    Person(name=wang, age=21, sex=female)
    Person(name=zhao, age=21, sex=male)

6.max min 获取结果中 某个值最大最小的的对象
max min 可以按指定的条件,获取到最大、最小的对象,当集合里有多个满足条件的最大最小值时,只会返回一个对象。

如: 返回年龄最大的人

    @Test
    public void test7(){
        List<Person> list = getPersonList();
        // 如果 最大最小值 对应的对象有多个 只会返回第一个
        Person oldest = list.stream().max(Comparator.comparing(Person::getAge)).get();
        System.out.println(oldest.toString());
    }
 
 
    结果:
    Person(name=liu, age=22, sex=male)

7.map 与 reduce也是两个十分重要的方法,
map:对集合中的每个元素进行遍历,并且可以对其进行操作,转化为其他对象,如将集合中的每个人的年龄增加3岁

    @Test
    public void test8(){
        List<Person> list = getPersonList();
        //将 每人的年龄 +3
        System.out.println("修改前:");
        list.forEach(e -> System.out.println(e.toString()));
        System.out.println("修改后:");
        list.stream().map(e -> e.setAge(e.getAge() + 3 ))
                     .forEach(e -> System.out.println(e.toString()));
 
    }
 
    结果:
    修改前:
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)
    Person(name=li, age=18, sex=female)
    Person(name=wang, age=21, sex=female)
    修改后:
    Person(name=liu, age=25, sex=male)
    Person(name=zhao, age=24, sex=male)
    Person(name=li, age=21, sex=female)
    Person(name=wang, age=24, sex=female)

reduce:也是对所有值进行操作,但它是将所有值,按照传入的处理逻辑,将结果处理合并为一个

如:将集合中的所有整数相加,并返回其总和

    @Test
    public void test9(){
        //第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,
        // 这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。
        //要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素
 
        //将所有人的年龄加起来 求和
        List<Integer> ages = Arrays.asList(2,5,3,4,7);
        int totalAge = ages.stream().reduce((sum,age) -> sum + age).get();
 
        System.out.println(totalAge);
        //带 初始值的计算, 如果list没有元素 即stream为null 则直接返回初始值
        int totalAge1 = ages.stream().reduce(0,(sum,age) -> sum+age);
        List<Integer> initList = Lists.newArrayList();
        int initTotalAge = initList.stream().reduce(0,(sum,age) -> sum+age);
 
        System.out.println("totalAge1: "+ totalAge1 + " initTotalAge: " + initTotalAge);
    }
 
    结果:
    21
    totalAge1: 21 initTotalAge: 0

8.collect方法以集合中的元素为基础,生成新的对象
在实际中,我们经常会以集合中的元素为基础,取其中的数据,来生成新的结果集,例如 按照过滤条件,返回新的List,

或者将集合转化为 Set 或Map等操作,通过collect方法实现是十分简便的:

    @Test
    public void test10(){
        List<Person> list = getPersonList();
        //排序过滤等一系列操作之后的元素 放入新的list
        List<Person> filterList = list.stream().filter(e -> e.getAge() >20).collect(Collectors.toList());
        filterList.forEach(e -> System.out.println(e.toString()));
 
        //将 name 属性用" , ",连接拼接成一个字符串
        String nameStr = list.stream().map(Person::getName).collect(Collectors.joining(","));
        System.out.println(nameStr);
        //将name 放入到新的 set 集合中
        Set<String> nameSet = list.stream().map(person -> person.getName()).collect(Collectors.toSet());
        nameSet.forEach(e -> System.out.print(e + ","));
 
        System.out.println();
        System.out.println("map--------");
        Map<String,Person> personMap = list.stream().collect(Collectors.toMap(Person::getName,person -> person));
        personMap.forEach((key,val) -> System.out.println(key + ":" + val.toString()));
    }
 
    结果:
    Person(name=liu, age=22, sex=male)
    Person(name=zhao, age=21, sex=male)
    Person(name=wang, age=21, sex=female)
    liu,zhao,li,wang
    wang,zhao,liu,li,
    map--------
    wang:Person(name=wang, age=21, sex=female)
    zhao:Person(name=zhao, age=21, sex=male)
    liu:Person(name=liu, age=22, sex=male)
    li:Person(name=li, age=18, sex=female)

9.summaryStatistics 计算集合元素的最大、最小、平均等值

 @Test
    public void test11(){
        List<Integer> ages = Arrays.asList(2,5,3,4,7);
        IntSummaryStatistics statistics = ages.stream().mapToInt(e -> e).summaryStatistics();
        System.out.println("最大值: " + statistics.getMax());
        System.out.println("最小值: " + statistics.getMin());
        System.out.println("平均值: " + statistics.getAverage());
        System.out.println("总和: " + statistics.getSum());
        System.out.println("个数: " + statistics.getCount());
    }
 
    结果:
    最大值: 7
    最小值: 2
    平均值: 4.2
    总和: 21
    个数: 5

补充:
1.lambda表达式
Java8最值得学习的特性就是Lambda表达式和Stream API,如果有python或者javascript的语言基础,对理解Lambda表达式有很大帮助,因为Java正在将自己变的更高(Sha)级(Gua),更人性化。--------可以这么说lambda表达式其实就是实现SAM接口的语法糖。

lambda写的好可以极大的减少代码冗余,同时可读性也好过冗长的内部类,匿名类。

先列举两个常见的简化(简单的代码同样好理解)

创建线程

排序

lambda表达式配合Java8新特性Stream API可以将业务功能通过函数式编程简洁的实现。(为后面的例子做铺垫)

例如:

这段代码就是对一个字符串的列表,把其中包含的每个字符串都转换成全小写的字符串。注意代码第四行的map方法调用,这里map方法就是接受了一个lambda表达式。

1.1lambda表达式语法
1.1.1lambda表达式的一般语法
(Type1 param1, Type2 param2, …, TypeN paramN) -> {
statment1;
statment2;
//…
return statmentM;
}
这是lambda表达式的完全式语法,后面几种语法是对它的简化。

1.1.2单参数语法
param1 -> {
statment1;
statment2;
//…
return statmentM;
}
当lambda表达式的参数个数只有一个,可以省略小括号

例如:将列表中的字符串转换为全小写

List proNames = Arrays.asList(new String[]{“Ni”,“Hao”,“Lambda”});
List lowercaseNames1 = proNames.stream().map(name -> {return name.toLowerCase();}).collect(Collectors.toList());

1.1.3单语句写法
param1 -> statment

当lambda表达式只包含一条语句时,可以省略大括号、return和语句结尾的分号

例如:将列表中的字符串转换为全小写

List proNames = Arrays.asList(new String[]{“Ni”,“Hao”,“Lambda”});

List lowercaseNames2 = proNames.stream().map(name -> name.toLowerCase()).collect(Collectors.toList());

1.1.4方法引用写法
(方法引用和lambda一样是Java8新语言特性,后面会讲到)

Class or instance :: method

例如:将列表中的字符串转换为全小写

List proNames = Arrays.asList(new String[]{“Ni”,“Hao”,“Lambda”});

List lowercaseNames3 = proNames.stream().map(String::toLowerCase).collect(Collectors.toList());

1.2lambda表达式可使用的变量
先举例:

//将为列表中的字符串添加前缀字符串
String waibu = “lambda :”;
List proStrs = Arrays.asList(new String[]{“Ni”,“Hao”,“Lambda”});
ListexecStrs = proStrs.stream().map(chuandi -> {
Long zidingyi = System.currentTimeMillis();
return waibu + chuandi + " -----:" + zidingyi;
}).collect(Collectors.toList());
execStrs.forEach(System.out::println);

输出:

lambda :Ni -----:1474622341604
lambda :Hao -----:1474622341604
lambda :Lambda -----:1474622341604

变量waibu :外部变量

变量chuandi :传递变量

变量zidingyi :内部自定义变量

lambda表达式可以访问给它传递的变量,访问自己内部定义的变量,同时也能访问它外部的变量。

不过lambda表达式访问外部变量有一个非常重要的限制:变量不可变(只是引用不可变,而不是真正的不可变)。

当在表达式内部修改waibu = waibu + " ";时,IDE就会提示你:

Local variable waibu defined in an enclosing scope must be final or effectively final

编译时会报错。因为变量waibu被lambda表达式引用,所以编译器会隐式的把其当成final来处理。

以前Java的匿名内部类在访问外部变量的时候,外部变量必须用final修饰。现在java8对这个限制做了优化,可以不用显示使用final修饰,但是编译器隐式当成final来处理。

1.3lambda表达式中的this概念
在lambda中,this不是指向lambda表达式产生的那个SAM对象,而是声明它的外部对象。

例如:

public class WhatThis {

 public void whatThis(){
       //转全小写
       List<String> proStrs = Arrays.asList(new String[]{"Ni","Hao","Lambda"});
       List<String> execStrs = proStrs.stream().map(str -> {
             System.out.println(this.getClass().getName());
             return str.toLowerCase();
       }).collect(Collectors.toList());
       execStrs.forEach(System.out::println);
 }

 public static void main(String[] args) {
       WhatThis wt = new WhatThis();
       wt.whatThis();
 }

}

输出:

com.wzg.test.WhatThis
com.wzg.test.WhatThis
com.wzg.test.WhatThis
ni
hao
lambda

2.方法引用和构造器引用
本人认为是进一步简化lambda表达式的声明的一种语法糖。

前面的例子中已有使用到: execStrs.forEach(System.out::println);

2.1方法引用
objectName::instanceMethod

ClassName::staticMethod

ClassName::instanceMethod

前两种方式类似,等同于把lambda表达式的参数直接当成instanceMethod|staticMethod的参数来调用。比如System.out::println等同于x->System.out.println(x);Math::max等同于(x, y)->Math.max(x,y)。

最后一种方式,等同于把lambda表达式的第一个参数当成instanceMethod的目标对象,其他剩余参数当成该方法的参数。比如String::toLowerCase等同于x->x.toLowerCase()。

可以这么理解,前两种是将传入对象当参数执行方法,后一种是调用传入对象的方法。

2.2构造器引用
构造器引用语法如下:ClassName::new,把lambda表达式的参数当成ClassName构造器的参数 。例如BigDecimal::new等同于x->new BigDecimal(x)。

3.Stream语法
两句话理解Stream:

1.Stream是元素的集合,这点让Stream看起来用些类似Iterator;
2.可以支持顺序和并行的对原Stream进行汇聚的操作;

大家可以把Stream当成一个装饰后的Iterator。原始版本的Iterator,用户只能逐个遍历元素并对其执行某些操作;包装后的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于10的字符串”、“获取每个字符串的首字母”等,具体这些操作如何应用到每个元素上,就给Stream就好了!原先是人告诉计算机一步一步怎么做,现在是告诉计算机做什么,计算机自己决定怎么做。当然这个“怎么做”还是比较弱的。

例子:

//Lists是Guava中的一个工具类
List nums = Lists.newArrayList(1,null,3,4,null,6);
nums.stream().filter(num -> num != null).count();

上面这段代码是获取一个List中,元素不为null的个数。这段代码虽然很简短,但是却是一个很好的入门级别的例子来体现如何使用Stream,正所谓“麻雀虽小五脏俱全”。我们现在开始深入解刨这个例子,完成以后你可能可以基本掌握Stream的用法!

图片就是对于Stream例子的一个解析,可以很清楚的看见:原本一条语句被三种颜色的框分割成了三个部分。红色框中的语句是一个Stream的生命开始的地方,负责创建一个Stream实例;绿色框中的语句是赋予Stream灵魂的地方,把一个Stream转换成另外一个Stream,红框的语句生成的是一个包含所有nums变量的Stream,进过绿框的filter方法以后,重新生成了一个过滤掉原nums列表所有null以后的Stream;蓝色框中的语句是丰收的地方,把Stream的里面包含的内容按照某种算法来汇聚成一个值,例子中是获取Stream中包含的元素个数。如果这样解析以后,还不理解,那就只能动用“核武器”–图形化,一图抵千言!

使用Stream的基本步骤:

1.创建Stream;
2.转换Stream,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换);
3.对Stream进行聚合(Reduce)操作,获取想要的结果;

3.1怎么得到Stream
最常用的创建Stream有两种途径:

1.通过Stream接口的静态工厂方法(注意:Java8里接口可以带静态方法);
2.通过Collection接口的默认方法(默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法)–stream(),把一个Collection对象转换成Stream

3.1.1 使用Stream静态方法来创建Stream

  1. of方法:有两个overload方法,一个接受变长参数,一个接口单一值

Stream integerStream = Stream.of(1, 2, 3, 5);
Stream stringStream = Stream.of(“taobao”);

  1. generator方法:生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象)

Stream.generate(new Supplier() {
@Override
public Double get() {
return Math.random();
}
});

Stream.generate(() -> Math.random());
Stream.generate(Math::random);
三条语句的作用都是一样的,只是使用了lambda表达式和方法引用的语法来简化代码。每条语句其实都是生成一个无限长度的Stream,其中值是随机的。这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Stream的limit()方法来用。

  1. iterate方法:也是生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环

Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
这段代码就是先获取一个无限长度的正整数集合的Stream,然后取出前10个打印。千万记住使用limit方法,不然会无限打印下去。

3.1.2通过Collection子类获取Stream
Collection接口有一个stream方法,所以其所有子类都都可以获取对应的Stream对象。

public interface Collection extends Iterable {
//其他方法省略
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
}

3.2转换Stream
转换Stream其实就是把一个Stream通过某些行为转换成一个新的Stream。Stream接口中定义了几个常用的转换方法,下面我们挑选几个常用的转换方法来解释。

  1. distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;

  2. filter: 对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素;

  3. map: 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;

  4. flatMap:和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

flatMap给一段代码理解:

Stream<List> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream outputStream = inputStream.
flatMap((childList) -> childList.stream());
flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 output 的新 Stream 里面已经没有 List 了,都是直接的数字。

  1. peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;

  2. limit: 对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;

  3. skip: 返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

整体调用例子:

List nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).distinct().mapToInt(num -> num * 2).peek(System.out::println).skip(2).limit(4).sum());

这段代码演示了上面介绍的所有转换方法(除了flatMap),简单解释一下这段代码的含义:给定一个Integer类型的List,获取其对应的Stream对象,然后进行过滤掉null,再去重,再每个元素乘以2,再每个元素被消费的时候打印自身,在跳过前两个元素,最后去前四个元素进行加和运算(解释一大堆,很像废话,因为基本看了方法名就知道要做什么了。这个就是声明式编程的一大好处!)。大家可以参考上面对于每个方法的解释,看看最终的输出是什么。

2
4
6
8
10
12
sum is:36

可能会有这样的疑问:在对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在汇聚操作(见下节)的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在汇聚操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。

3.3汇聚(Reduce)Stream
汇聚操作(也称为折叠)接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果。比如查找一个数字列表的总和或者最大值,或者把这些数字累积成一个List对象。Stream接口有一些通用的汇聚操作,比如reduce()和collect();也有一些特定用途的汇聚操作,比如sum(),max()和count()。注意:sum方法不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。

下面会分两部分来介绍汇聚操作:

可变汇聚:把输入的元素们累积到一个可变的容器中,比如Collection或者StringBuilder;
其他汇聚:除去可变汇聚剩下的,一般都不是通过反复修改某个可变对象,而是通过把前一次的汇聚结果当成下一次的入参,反复如此。比如reduce,count,allMatch;

3.3.1可变汇聚
可变汇聚对应的只有一个方法:collect,正如其名字显示的,它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。先看一下最通用的collect方法的定义(还有其他override方法):

R collect(Supplier supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
先来看看这三个参数的含义:Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。看晕了?来段代码!

List nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
List numsWithoutNull = nums.stream().filter(num -> num != null).
collect(() -> new ArrayList(),
(list, item) -> list.add(item),
(list1, list2) -> list1.addAll(list2));
上面这段代码就是对一个元素是Integer类型的List,先过滤掉全部的null,然后把剩下的元素收集到一个新的List中。进一步看一下collect方法的三个参数,都是lambda形式的函数。

第一个函数生成一个新的ArrayList实例;
第二个函数接受两个参数,第一个是前面生成的ArrayList对象,二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。第二个函数被反复调用直到原stream的元素被消费完毕;
第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把第二个ArrayList全部加入到第一个中;
但是上面的collect方法调用也有点太复杂了,没关系!我们来看一下collect方法另外一个override的版本,其依赖Collector

<R, A> R collect(Collector<? super T, A, R> collector);
这样清爽多了!Java8还给我们提供了Collector的工具类–Collectors,其中已经定义了一些静态工厂方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中。这样的静态方法还有很多,这里就不一一介绍了,大家可以直接去看JavaDoc。下面看看使用Collectors对于代码的简化:

List numsWithoutNull = nums.stream().filter(num -> num != null).
collect(Collectors.toList());

3.3.2其他汇聚
– reduce方法:reduce方法非常的通用,后面介绍的count,sum等都可以使用其实现。reduce方法有三个override的方法,本文介绍两个最常用的。先来看reduce方法的第一种形式,其方法定义如下:

Optional reduce(BinaryOperator accumulator);
接受一个BinaryOperator类型的参数,在使用的时候我们可以用lambda表达式来。

List ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(“ints sum is:” + ints.stream().reduce((sum, item) -> sum + item).get());
可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional,这是Java8防止出现NPE的一种可行方法,后面的文章会详细介绍,这里就简单的认为是一个容器,其中可能会包含0个或者1个对象。
这个过程可视化的结果如图:

reduce方法还有一个很常用的变种:

T reduce(T identity, BinaryOperator accumulator);
这个定义上上面已经介绍过的基本一致,不同的是:它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值。下面直接给出例子,就不再做说明了。

List ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(“ints sum is:” + ints.stream().reduce(0, (sum, item) -> sum + item));
– count方法:获取Stream中元素的个数。比较简单,这里就直接给出例子,不做解释了。

List ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(“ints sum is:” + ints.stream().count());

– 搜索相关
– allMatch:是不是Stream中的所有元素都满足给定的匹配条件
– anyMatch:Stream中是否存在任何一个元素满足匹配条件
– findFirst: 返回Stream中的第一个元素,如果Stream为空,返回空Optional
– noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件
– max和min:使用给定的比较器(Operator),返回Stream中的最大|最小值
下面给出allMatch和max的例子,剩下的方法读者当成练习。

查看源代码打印帮助
List<Integer> ints = Lists.newArrayList(1,2,3,4,5,6,7,8,9,10);
System.out.println(ints.stream().allMatch(item -> item < 100));
ints.stream().max((o1, o2) -> o1.compareTo(o2)).ifPresent(System.out::println);

6.9 枚举类

Java 5新增了一个enum关键字(它与class、interface关键字的地位相同),用以定义枚举类。正如前面看到的,枚举类是一种特殊的类,它一样可以有自己的成员变量、方法,可以实现一个或者多个接口,也可以定义自己的构造器。一个Java源文件中最多只能定义一个public访问权限的枚举类,且该Java源文件也必须和该枚举类的类名相同。

但枚举类终究不是普通类,它与普通类有如下简单区别:

➢ 枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。

➢ 使用enum定义、非抽象的枚举类默认会使用final修饰。

➢ 枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;如果强制指定访问控制符,则只能指定private修饰符。由于枚举类的所有构造器都是private的,而子类构造器总要调用父类构造器一次,因此枚举类不能派生子类。

➢ 枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final修饰,无须程序员显式添加。枚举类默认提供了一个values()方法,该方法可以很方便地遍历所有的枚举值。

package enumcase;

public enum SeasonEnum {
    SPRING("春天"),SUMMER("夏天"),FALL("秋天"),WINTER("冬天");
    
    private final String name;
    
    private SeasonEnum(String name)
    {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

一、什么情况下使用枚举类?

有的时候一个类的对象是有限且固定的,这种情况下我们使用枚举类就比较方便?

二、为什么不用静态常量来替代枚举类呢?

public static final int SEASON_SPRING = 1;
public static final int SEASON_SUMMER = 2;
public static final int SEASON_FALL = 3;
public static final int SEASON_WINTER = 4;

枚举类更加直观,类型安全。使用常量会有以下几个缺陷:

1. 类型不安全。若一个方法中要求传入季节这个参数,用常量的话,形参就是int类型,开发者传入任意类型的int类型值就行,但是如果是枚举类型的话,就只能传入枚举类中包含的对象。

2. 没有命名空间。开发者要在命名的时候以SEASON_开头,这样另外一个开发者再看这段代码的时候,才知道这四个常量分别代表季节。

三、枚举类入门

先看一个简单的枚举类。

package enumcase;

public enum SeasonEnum {
    SPRING,SUMMER,FALL,WINTER;
}

enum和class、interface的地位一样
使用enum定义的枚举类默认继承了java.lang.Enum,而不是继承Object类。枚举类可以实现一个或多个接口。
枚举类的所有实例都必须放在第一行展示,不需使用new 关键字,不需显式调用构造器。自动添加public static final修饰。
使用enum定义、非抽象的枚举类默认使用final修饰,不可以被继承。
枚举类的构造器只能是私有的。
四、枚举类介绍

枚举类内也可以定义属性和方法,可是是静态的和非静态的。

复制代码

package enumcase;

public enum SeasonEnum {
    SPRING("春天"),SUMMER("夏天"),FALL("秋天"),WINTER("冬天");
    
    private final String name;
    
    private SeasonEnum(String name)
    {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

复制代码
  实际上在第一行写枚举类实例的时候,默认是调用了构造器的,所以此处需要传入参数,因为没有显式申明无参构造器,只能调用有参数的构造器。

构造器需定义成私有的,这样就不能在别处申明此类的对象了。枚举类通常应该设计成不可变类,它的Field不应该被改变,这样会更安全,而且代码更加简洁。所以我们将Field用private final修饰。

五、枚举类实现接口

枚举类可以实现一个或多个接口。与普通类一样,实现接口的时候需要实现接口中定义的所有方法,若没有完全实现,那这个枚举类就是抽象的,只是不需显式加上abstract修饰,系统化会默认加上。

复制代码

package enumcase;

public enum Operation {
    PLUS{

        @Override
        public double eval(double x, double y) {
            return x + y;
        }
        
    },
    MINUS{

        @Override
        public double eval(double x, double y) {
            return x - y;
        }
        
    },
    TIMES{

        @Override
        public double eval(double x, double y) {
            return x * y;
        }
        
    },
    DIVIDE{

        @Override
        public double eval(double x, double y) {
            return x / y;
        }
        
    };
    
    /**
     * 抽象方法,由不同的枚举值提供不同的实现。
     * @param x
     * @param y
     * @return
     */
    public abstract double eval(double x, double y);
    
    public static void main(String[] args) {
        System.out.println(Operation.PLUS.eval(10, 2));
        System.out.println(Operation.MINUS.eval(10, 2));
        System.out.println(Operation.TIMES.eval(10, 2));
        System.out.println(Operation.DIVIDE.eval(10, 2));
    }
}

复制代码
  Operatio类实际上是抽象的,不可以创建枚举值,所以此处在申明枚举值的时候,都实现了抽象方法,这其实是匿名内部类的实现,花括号部分是一个类体。我们可以看下编译以后的文件。

共生成了五个class文件,这样就证明了PLUS,MINUS,TIMES,DIVIDE是Operation的匿名内部类的实例。

六、switch语句里的表达式可以是枚举值

Java5新增了enum关键字,同时扩展了switch。

复制代码

package enumcase;

public class SeasonTest {
    public void judge(SeasonEnum s)
    {
        switch(s)
        {
        case SPRING:
            System.out.println("春天适合踏青。");
            break;
        case SUMMER:
            System.out.println("夏天要去游泳啦。");
            break;
        case FALL:
            System.out.println("秋天一定要去旅游哦。");
            break;
        case WINTER:
            System.out.println("冬天要是下雪就好啦。");
            break;
        }
    }
    
    public static void main(String[] args) {
        SeasonEnum s = SeasonEnum.SPRING;
        SeasonTest test = new SeasonTest();
        test.judge(s);
    }
}

复制代码
  case表达式中直接写入枚举值,不需加入枚举类作为限定。

6.10 对象与垃圾回收

6.10.1 对象在内存中的状态

当一个对象在堆内存中运行时,根据它被引用变量所引用的状态,可以把它所处的状态分成如下三种:

➢ 可达状态:当一个对象被创建后,若有一个以上的引用变量引用它,则这个对象在程序中处于可达状态,程序可通过引用变量来调用该对象的实例变量和方法。

➢ 可恢复状态:如果程序中某个对象不再有任何引用变量引用它,它就进入了可恢复状态。在这种状态下,系统的垃圾回收机制准备回收该对象所占用的内存,在回收该对象之前,系统会调用所有可恢复状态对象的finalize()方法进行资源清理。如果系统在调用finalize()方法时重新让一个引用变量引用该对象,则这个对象会再次变为可达状态;否则该对象将进入不可达状态。

➢ 不可达状态:当对象与所有引用变量的关联都被切断,且系统已经调用所有对象的finalize()方法后依然没有使该对象变成可达状态,那么这个对象将永久性地失去引用,最后变成不可达状态。只有当一个对象处于不可达状态时,系统才会真正回收该对象所占有的资源。

6.10.2 强制垃圾回收

当一个对象失去引用后,系统何时调用它的finalize()方法对它进行资源清理,何时它会变成不可达状态,系统何时回收它所占有的内存,对于程序完全透明。程序只能控制一个对象何时不再被任何引用变量引用,绝不能控制它何时被回收。程序无法精确控制Java垃圾回收的时机,但依然可以强制系统进行垃圾回收—这种强制只是通知系统进行垃圾回收,但系统是否进行垃圾回收依然不确定。大部分时候,程序强制系统垃圾回收后总会有一些效果。

强制系统垃圾回收有如下两种方式:

➢ 调用System类的gc()静态方法:System.gc()。

➢ 调用Runtime对象的gc()实例方法:Runtime.getRuntime().gc()。

这种强制只是建议系统立即进行垃圾回收,系统完全有可能并不立即进行垃圾回收,垃圾回收机制也不会对程序的建议完全置之不理:垃圾回收机制会在收到通知后,尽快进行垃圾回收。

6.10.3 finalize方法

finalize()方法具有如下4个特点。

➢ 永远不要主动调用某个对象的finalize()方法,该方法应交给垃圾回收机制调用。

➢ finalize()方法何时被调用,是否被调用具有不确定性,不要把finalize()方法当成一定会被执行的方法。

➢ 当JVM执行可恢复对象的finalize()方法时,可能使该对象或系统中其他对象重新变成可达状态。

➢ 当JVM执行finalize()方法时出现异常时,垃圾回收机制不会报告异常,程序继续执行。

6.10.4 对象的软、弱和虚引用

Java语言对对象的引用有如下4种方式:

1.强引用(StrongReference)这是Java程序中最常见的引用方式。程序创建一个对象,并把这个对象赋给一个引用变量,程序通过该引用变量来操作实际的对象,前面介绍的对象和数组都采用了这种强引用的方式。当一个对象被一个或一个以上的引用变量所引用时,它处于可达状态,不可能被系统垃圾回收机制回收。

2.软引用(SoftReference)软引用需要通过SoftReference类来实现,当一个对象只有软引用时,它有可能被垃圾回收机制回收。对于只有软引用的对象而言,当系统内存空间足够时,它不会被系统回收,程序也可使用该对象;当系统内存空间不足时,系统可能会回收它。软引用通常用于对内存敏感的程序中。

3.弱引用(WeakReference)弱引用通过WeakReference类实现,弱引用和软引用很像,但弱引用的引用级别更低。对于只有弱引用的对象而言,当系统垃圾回收机制运行时,不管系统内存是否足够,总会回收该对象所占用的内存。当然,并不是说当一个对象只有弱引用时,它就会立即被回收—正如那些失去引用的对象一样,必须等到系统垃圾回收机制运行时才会被回收。

4.虚引用(PhantomReference)虚引用通过PhantomReference类实现,虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,虚引用必须和引用队列(ReferenceQueue)联合使用。

第7章 Java基础类库

7.1 与用户互动

Scanner主要提供了两个方法来扫描输入。

➢ hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果只是判断是否包含下一个字符串,则直接使用hasNext()。

➢ nextXxx():获取下一个输入项。Xxx的含义与前一个方法中的Xxx相同。

import java.util.Scanner;//导包,除了lang包之外其他都需要导包

public class scanner {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int i = sc.nextInt();
        System.out.println("输入的数字是"+i);
        System.out.println("=================");
        String str = sc.next();
        System.out.println("输入的字符串是"+str);
        /*键盘输入为1空格2
        * 输出结果直接就是:(这里键盘只输入了一次)
        * 
        * 输入的数字是1 
        * ============
        * 输入的字符串是2 
        * 
        * 这样我们就可以看到 当从键盘输入检测的时候空格是作为分隔符的  
        * 两个空格之间为一个完整标记*/
        
    }

7.2 系统相关

Java程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java提供了System类和Runtime类来与程序的运行平台进行交互。

7.2.1、System 系统类

1、作用:主要用于获取系统的属性数据。
2、System类常用的方法:
1)arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
src - 源数组。
srcPos - 源数组中的起始位置。
dest - 目标数组。
destPos - 目标数据中的起始位置。
length - 要复制的数组元素的数量。
在这里插入图片描述
2、getenv(String name) 根据环境变量的名字获取环境变量。
3、currentTimeMillis() 获取当前系统系统。(当前时间与协调世界时 1970 年 1 月 1 日午夜之间的时间差(以毫秒为单位测量))。
在这里插入图片描述
4、exit(int status) 退出jvm 如果参数是0表示正常退出jvm,非0表示异常退出jvm。
System.exit(0);
jvm退出,注意:
1)0或者非0的数据都可以退出jvm。对于用户而言没有任何区别。
2)对操作系统有区别,因为jvm要在操作系统的基础上执行(如windows),如果是异常退出,windows会做成错误报告反馈回系统开发人员。而正常退出则不用。
3)对于程序员而言,在try-catch块中,习惯于在try中0退出,catch中非0退出。

5、gc() 建议jvm赶快启动垃圾回收器回收垃圾。(为什么是建议呢?因为对于绝大多数电脑而言,在同一时间点CPU只能执行一个程序,所以可能jvm要启动垃圾回收器时,CPU正在执行另一个程序,所以只能等待获取CPU的执行权)
6、finalize() 如果一个对象被垃圾回收器回收的时候,会先调用对象的finalize()方法。
在这里插入图片描述
7、getProperty(key) 获取指定键指示的系统属性。
8、getProperties() 确定当前的系统属性
在这里插入图片描述

7.2.2、RunTime:

该类类主要代表了应用程序运行的环境。
1、一个java程序只有一个运行环境——所以使用到了单例设计模式
2、常用方法:
1)getRuntime() 返回当前应用程序的运行环境对象。
2)exec(String command) 根据指定的路径执行对应的可执行文件。需要有异常处理的原因:担心所写的路径不存在。
3)freeMemory() 返回 Java 虚拟机中的空闲内存量,以字节为单位(jvm默认管理内存64MB)。
4)maxMemory() 返回 Java 虚拟机试图使用的最大内存量。 jdk7.0之前一次性要所有内存,但是如果仅用3MB,剩下的浪费内存。jdk7.0之后,仅仅管理10+MB的内存,如果不够使用了就类似StringBuffer的方式自动再向内存要些内存。
5)totalMemory() 返回 Java 虚拟机中的内存总量

在这里插入图片描述

7.3 常用类

本节将介绍Java提供的一些常用类,如String、Math、BigDecimal等的用法。

Object类的clone()方法虽然简单、易用,但它只是一种“浅克隆”——它只克隆该对象的所有成员变量值,不会对引用类型的成员变量值所引用的对象进行克隆。如果开发者需要对对象进行“深克隆”,则需要开发者自己进行“递归”克隆,保证所有引用类型的成员变量值所引用的对象都被复制了。
浅拷贝

@Data
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Address address;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
 
@Test
public void testShallowCopy() throws Exception{
  Person p1=new Person();
  p1.setAge(31);
  p1.setName("Peter");
 
  Person p2=(Person) p1.clone();
  System.out.println(p1==p2);//false
  p2.setName("Jacky");
  System.out.println("p1="+p1);//p1=Person [name=Peter, age=31]
  System.out.println("p2="+p2);//p2=Person [name=Jacky, age=31]
}

深拷贝

@Data
public class Address {
    private String type;
    private String value;
}
@Test
public void testShallowCopy() throws Exception{
  Address address=new Address();
  address.setType("Home");
  address.setValue("北京");
 
  Person p1=new Person();
  p1.setAge(31);
  p1.setName("Peter");
  p1.setAddress(address);
 
  Person p2=(Person) p1.clone();
  System.out.println(p1==p2);//false
 
  p2.getAddress().setType("Office");
  System.out.println("p1="+p1);
  System.out.println("p2="+p2);
}

输出结果

false
p1=Person(name=Peter, age=31, address=Address(type=Office, value=北京))
p2=Person(name=Peter, age=31, address=Address(type=Office, value=北京))

address只拷贝了引用

更改clone

@Data
public class Person implements Cloneable {
    private String name;
    private Integer age;
    private Address address;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Object obj=super.clone();
        Address a=((Person)obj).getAddress();
        ((Person)obj).setAddress((Address) a.clone());
        return obj;
    }
}

Java 7新增了一个Objects工具类,它提供了一些工具方法来操作对象,这些工具方法大多是“空指针”安全的。

String类是不可变类,即一旦一个String对象被创建以后,包含在这个对象中的字符序列是不可改变的,直至这个对象被销毁。StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法可以改变这个字符串对象的字符序列。一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。StringBuilder类是JDK 1.5新增的类,它也代表可变字符串对象。实际上,StringBuilder和StringBuffer基本相似,两个类的构造器和方法也基本相同。不同的是,StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能,所以性能略高。因此在通常情况下,如果需要创建一个内容可变的字符串对象,则应该优先考虑使用StringBuilder类。

Java 9改进了字符串(包括String、StringBuffer、StringBuilder)的实现。在Java 9以前字符串采用char[]数组来保存字符,因此字符串的每个字符占2字节;而Java 9及更新版本的JDK的字符串采用byte[]数组再加一个encoding-flag字段来保存字符,因此字符串的每个字符只占1字节。所以Java 9及更新版本的JDK的字符串更加节省空间,但字符串的功能方法没有受到任何影响。

Random类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以当前时间作为种子),另一个构造器需要程序员显式传入一个long型整数的种子。ThreadLocalRandom类是Java 7新增的一个类,它是Random的增强版。在并发访问的环境下,使用ThreadLocalRandom来代替Random可以减少多线程资源竞争,最终保证系统具有更好的线程安全性。

  //创建随机数对象
        Random random = new Random();

        //随机产生一个int类型取值范围内的数字。
        int num1 = random.nextInt();
        System.out.println(num1);

        //产生一个[0-100]之间的随机数
        int num2 = random.nextInt(101);
        System.out.println(num2);//不包括101

为了能精确表示、计算浮点数,Java提供了BigDecimal类,该类提供了大量的构造器用于创建BigDecimal对象,包括把所有的基本数值型变量转换成一个BigDecimal对象,也包括利用数字字符串、数字字符数组来创建BigDecimal对象。

7.4 Java 8的日期、时间类

7.4.1Date

        Date date = new Date();
        System.out.println("毫秒:"+date.getTime());//输入毫秒
 
        //时间转字符串
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = sdf.format(date);
        System.out.println("时间转字符串:"+time);
 
        //利用字符串来转时间格式(记得抛出异常)
        String time02 = "2018-09-05";
        SimpleDateFormat  sdf2 = new SimpleDateFormat ("yyyy-MM-dd");
        Date date2 = sdf2.parse(time02);
        System.out.println("字符串转时间格式:"+date2);

7.4.2Calendar

        // 使用默认时区和语言环境获得一个日历
        Calendar cal = Calendar.getInstance();
        // 赋值时年月日时分秒常用的6个值,注意月份下标从0开始,所以取月份要+1
        System.out.println("年:" + cal.get(Calendar.YEAR));
        System.out.println("月:" + (cal.get(Calendar.MONTH) + 1));
        System.out.println("日:" + cal.get(Calendar.DAY_OF_MONTH));
        System.out.println("时:" + cal.get(Calendar.HOUR_OF_DAY));
        System.out.println("分:" + cal.get(Calendar.MINUTE));
        System.out.println("秒:" + cal.get(Calendar.SECOND));
 
        //手动设置某个日期
        Calendar cal02 = Calendar.getInstance();
        //注意,设置时间的时候月份的下标是在0开始的
        //设置时间不一定要这6个参数3个参数也是可以的
        cal02.set(2018,9,1,12,0,0);//二零一八年十月一号十二点
        System.out.println(cal02.getTime());
 

相互转换

1CalendarDate
Calendar cal = Calendar.getInstance();
Date date = cal.getTime();
Date date2 = new Date();
Calendar cal2 = Calendar.getInstance();
cal2.setTime( date );

7.5 正则表达式

正则表达式是一个强大的字符串处理工具,可以对字符串进行查找、提取、分割、替换等操作。

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

➢ boolean matches(String regex):判断该字符串是否匹配指定的正则表达式。

➢ String replaceAll(String regex, String replacement):将该字符串中所有匹配regex的子串替换成replacement。

➢ String replaceFirst(String regex, String replacement):将该字符串中第一个匹配regex的子串替换成replacement。

➢ String[] split(String regex):以regex作为分隔符,把该字符串分割成多个子串。

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

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

Pattern是不可变类,可供多个并发线程安全使用。

Matcher类提供了如下几个常用方法:

➢ find():返回目标字符串中是否包含与Pattern匹配的子串。

➢ group():返回上一次与Pattern匹配的子串。

➢ start():返回上一次与Pattern匹配的子串在目标字符串中的开始位置。

➢ end():返回上一次与Pattern匹配的子串在目标字符串中的结束位置加1。

➢ lookingAt():返回目标字符串前面部分与Pattern是否匹配。

➢ matches():返回整个目标字符串与Pattern是否匹配。

➢ reset():将现有的Matcher对象应用于一个新的字符序列。

一、正则表达式术语

1)元字符 : 非一般字符,具有某种意义的字符。如 : \bX : \b边界符, 以 X开始的单词

2)正则表达式语法大全
在这里插入图片描述
二、Pattern类与Matcher类详解

    java.util.regex是一个用正则表达式所订制的模式来对字符串进行匹配工作的类库包。它包括两个类:Pattern和Matcher Pattern 一个Pattern是一个正则表达式经编译后的表现模式。 Matcher 一个Matcher对象是一个状态机器,它依据Pattern对象做为匹配模式对字符串展开匹配检查。 首先一个Pattern实例订制了一个所用语法与PERL的类似的正则表达式经编译后的模式,然后一个Matcher实例在这个给定的Pattern实例的模式控制下进行字符串的匹配工作。

以下我们就分别来看看这两个类:

捕获组的概念

捕获组可以通过从左到右计算其开括号来编号,编号是从1 开始的。例如,在表达式 ((A)(B©))中,存在四个这样的组:

((A)(B(C)))
(A)
(B(C))
(C)

组零始终代表整个表达式。 以 (?) 开头的组是纯的非捕获 组,它不捕获文本,也不针对组合计进行计数。

   与组关联的捕获输入始终是与组最近匹配的子序列。如果由于量化的缘故再次计算了组,则在第二次计算失败时将保留其以前捕获的值(如果有的话)例如,将字符串"aba" 与表达式(a(b)?)+ 相匹配,会将第二组设置为 "b"。在每个匹配的开头,所有捕获的输入都会被丢弃。

详解Pattern类和Matcher类

   java正则表达式通过java.util.regex包下的Pattern类与Matcher类实现(建议在阅读本文时,打开java API文档,当介绍到哪个方法时,查看java API中的方法说明,效果会更佳). 
   Pattern类用于创建一个正则表达式,也可以说创建一个匹配模式,它的构造方法是私有的,不可以直接创建,但可以通过Pattern.complie(String regex)简单工厂方法创建一个正则表达式, 
Pattern p=Pattern.compile("\\w+"); 
p.pattern();//返回 \w+ 

pattern() 返回正则表达式的字符串形式,其实就是返回Pattern.complile(String regex)的regex参数

1.Pattern.split(CharSequence input)

     Pattern有一个split(CharSequence input)方法,用于分隔字符串,并返回一个String[],我猜String.split(String regex)就是通过Pattern.split(CharSequence input)来实现的. 
Pattern p=Pattern.compile("\\d+"); 
String[] str=p.split("我的QQ是:456456我的电话是:0532214我的邮箱是:aaa@aaa.com"); 

结果:str[0]=“我的QQ是:” str[1]=“我的电话是:” str[2]=“我的邮箱是:aaa@aaa.com”

2.Pattern.matcher(String regex,CharSequence input)是一个静态方法,用于快速匹配字符串,该方法适合用于只匹配一次,且匹配全部字符串.

Pattern.matches("\\d+","2223");//返回true 
Pattern.matches("\\d+","2223aa");//返回false,需要匹配到所有字符串才能返回true,这里aa不能匹配到 
Pattern.matches("\\d+","22bb23");//返回false,需要匹配到所有字符串才能返回true,这里bb不能匹配到 

3.Pattern.matcher(CharSequence input)

     说了这么多,终于轮到Matcher类登场了,Pattern.matcher(CharSequence input)返回一个Matcher对象.
     Matcher类的构造方法也是私有的,不能随意创建,只能通过Pattern.matcher(CharSequence input)方法得到该类的实例. 
     Pattern类只能做一些简单的匹配操作,要想得到更强更便捷的正则匹配操作,那就需要将Pattern与Matcher一起合作.Matcher类提供了对正则表达式的分组支持,以及对正则表达式的多次匹配支持. 
Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("22bb23"); 
m.pattern();//返回p 也就是返回该Matcher对象是由哪个Pattern对象的创建的 

4.Matcher.matches()/ Matcher.lookingAt()/ Matcher.find()

    Matcher类提供三个匹配操作方法,三个方法均返回boolean类型,当匹配到时返回true,没匹配到则返回false 
    matches()对整个字符串进行匹配,只有整个字符串都匹配了才返回true 
Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("22bb23"); 
m.matches();//返回false,因为bb不能被\d+匹配,导致整个字符串匹配未成功. 
Matcher m2=p.matcher("2223"); 
m2.matches();//返回true,因为\d+匹配到了整个字符串

我们现在回头看一下Pattern.matcher(String regex,CharSequence input),它与下面这段代码等价
Pattern.compile(regex).matcher(input).matches()
lookingAt()对前面的字符串进行匹配,只有匹配到的字符串在最前面才返回true

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("22bb23"); 
m.lookingAt();//返回true,因为\d+匹配到了前面的22 
Matcher m2=p.matcher("aa2223"); 
m2.lookingAt();//返回false,因为\d+不能匹配前面的aa 

find()对字符串进行匹配,匹配到的字符串可以在任何位置

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("22bb23"); 
m.find();//返回true 
Matcher m2=p.matcher("aa2223"); 
m2.find();//返回true 
Matcher m3=p.matcher("aa2223bb"); 
m3.find();//返回true 
Matcher m4=p.matcher("aabb"); 
m4.find();//返回false 

5.Mathcer.start()/ Matcher.end()/ Matcher.group()

当使用matches(),lookingAt(),find()执行匹配操作后,就可以利用以上三个方法得到更详细的信息. 

start()返回匹配到的子字符串在字符串中的索引位置.
end()返回匹配到的子字符串的最后一个字符在字符串中的索引位置.
group()返回匹配到的子字符串

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("aaa2223bb"); 
m.find();//匹配2223 
m.start();//返回3 
m.end();//返回7,返回的是2223后的索引号 
m.group();//返回2223 
 
Mathcer m2=m.matcher("2223bb"); 
m.lookingAt();   //匹配2223 
m.start();   //返回0,由于lookingAt()只能匹配前面的字符串,所以当使用lookingAt()匹配时,start()方法总是返回0 
m.end();   //返回4 
m.group();   //返回2223 
 
Matcher m3=m.matcher("2223bb"); 
m.matches();   //匹配整个字符串 
m.start();   //返回0,原因相信大家也清楚了 
m.end();   //返回6,原因相信大家也清楚了,因为matches()需要匹配所有字符串 
m.group();   //返回2223bb 

说了这么多,相信大家都明白了以上几个方法的使用,该说说正则表达式的分组在java中是怎么使用的.
start(),end(),group()均有一个重载方法它们是start(int i),end(int i),group(int i)专用于分组操作,Mathcer类还有一个groupCount()用于返回有多少组.

Pattern p=Pattern.compile("([a-z]+)(\\d+)"); 
Matcher m=p.matcher("aaa2223bb"); 
m.find();   //匹配aaa2223 
m.groupCount();   //返回2,因为有2组 
m.start(1);   //返回0 返回第一组匹配到的子字符串在字符串中的索引号 
m.start(2);   //返回3 
m.end(1);   //返回3 返回第一组匹配到的子字符串的最后一个字符在字符串中的索引位置. 
m.end(2);   //返回7 
m.group(1);   //返回aaa,返回第一组匹配到的子字符串 
m.group(2);   //返回2223,返回第二组匹配到的子字符串 

现在我们使用一下稍微高级点的正则匹配操作,例如有一段文本,里面有很多数字,而且这些数字是分开的,我们现在要将文本中所有数字都取出来,利用java的正则操作是那么的简单.

Pattern p=Pattern.compile("\\d+"); 
Matcher m=p.matcher("我的QQ是:456456 我的电话是:0532214 我的邮箱是:aaa123@aaa.com"); 
while(m.find()) { 
     System.out.println(m.group()); 
} 
 
//输出: 
 
456456 
0532214 
123 
 
 
//如将以上while()循环替换成 
 
while(m.find()) { 
     System.out.println(m.group()); 
     System.out.print("start:"+m.start()); 
     System.out.println(" end:"+m.end()); 
} 
 
//则输出: 
 
456456 
start:6 end:12 
0532214 
start:19 end:26 
123 
start:36 end:39 

现在大家应该知道,每次执行匹配操作后start(),end(),group()三个方法的值都会改变,改变成匹配到的子字符串的信息,以及它们的重载方法,也会改变成相应的信息.
注意:只有当匹配操作成功,才可以使用start(),end(),group()三个方法,否则会抛出java.lang.IllegalStateException,也就是当matches(),lookingAt(),find()其中任意一个方法返回true时,才可以使用.
三、常用的正则表达式:

(1) “^\d+$”  //非负整数(正整数 + 0)

(2) “1[1-9][0-9]$”  //正整数

(3) “^((-\d+)|(0+))$”  //非正整数(负整数 + 0)

(4) “^-[0-9][1-9][0-9]$”  //负整数

(5) “^-?\d+$”    //整数

(6) “^\d+(.\d+)?$”  //非负浮点数(正浮点数 + 0)

(7) “^(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9]))$”  //正浮点数

(8) “^((-\d+(.\d+)?)|(0+(.0+)?))$”  //非正浮点数(负浮点数 + 0)

(9) “^(-(([0-9]+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9])))$”  //负浮点数

(10) “^(-?\d+)(.\d+)?$”  //浮点数

(11) “2+$”  //由26个英文字母组成的字符串

(12) “3+$”  //由26个英文字母的大写组成的字符串

(13) “4+$”  //由26个英文字母的小写组成的字符串

(14) “5+$”  //由数字和26个英文字母组成的字符串

(15) “^\w+$”  //由数字、26个英文字母或者下划线组成的字符串

(16) “6+(.[\w-]+)*@[\w-]+(.[\w-]+)+$”    //email地址

(17) “7+://(\w+(-\w+))(.(\w+(-\w+)))(?\S)?$”  //url

(18) /^(d{2}|d{4})-((0([1-9]{1}))|(1[1|2]))-((0-2)|(3[0|1]))$/ // 年-月-日

(19) /^((0([1-9]{1}))|(1[1|2]))/((0-2)|(3[0|1]))/(d{2}|d{4})$/ // 月/日/年

(20) “^([w-.]+)@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.)|(([w-]+.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(]?)$” //Emil

(21) /^((+?[0-9]{2,4}-[0-9]{3,4}-)|([0-9]{3,4}-))?([0-9]{7,8})(-[0-9]+)?$/ //电话号码

(22) “^(d{1,2}|1dd|2[0-4]d|25[0-5]).(d{1,2}|1dd|2[0-4]d|25[0-5]).(d{1,2}|1dd|2[0-4]d|25[0-5]).(d{1,2}|1dd|2[0-4]d|25[0-5])$” //IP地址

(23)

(24) 匹配中文字符的正则表达式: [\u4e00-\u9fa5]

(25) 匹配双字节字符(包括汉字在内):[^\x00-\xff]

(26) 匹配空行的正则表达式:\n[\s| ]*\r

(27) 匹配HTML标记的正则表达式:/<(.)>.</\1>|<(.*) />/

(28) 匹配首尾空格的正则表达式:(^\s*)|(\s*$)

(29) 匹配Email地址的正则表达式:\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*

(30) 匹配网址URL的正则表达式:8+://(\w+(-\w+))(\.(\w+(-\w+)))(\?\S)?$

(31) 匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):9[a-zA-Z0-9_]{4,15}$

(32) 匹配国内电话号码:(\d{3}-|\d{4}-)?(\d{8}|\d{7})?

(33) 匹配腾讯QQ号:10[1-9][0-9]$

(34) 元字符及其在正则表达式上下文中的行为:

(35) \ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个后向引用、或一个八进制转义符。

(36) ^ 匹配输入字符串的开始位置。如果设置了 RegExp 对象的Multiline 属性,^ 也匹配 ’\n’ 或 ’\r’ 之后的位置。

(37) $ 匹配输入字符串的结束位置。如果设置了 RegExp 对象的Multiline 属性,$ 也匹配 ’\n’ 或 ’\r’ 之前的位置。

(38) * 匹配前面的子表达式零次或多次。

(39) + 匹配前面的子表达式一次或多次。+ 等价于 {1,}。

(40) ? 匹配前面的子表达式零次或一次。? 等价于 {0,1}。

(41) {n} n 是一个非负整数,匹配确定的n 次。

(42) {n,} n 是一个非负整数,至少匹配n 次。

(43) {n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。在逗号和两个数之间不能有空格。

(44) ? 当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。

(45) . 匹配除 “\n” 之外的任何单个字符。要匹配包括 ’\n’ 在内的任何字符,请使用象 ’[.\n]’ 的模式。

(46) (pattern) 匹配pattern 并获取这一匹配。

(47) (?:pattern) 匹配pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。

(48) (?=pattern) 正向预查,在任何匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。

(49) (?!pattern) 负向预查,与(?=pattern)作用相反

(50) x|y 匹配 x 或 y。

(51) [xyz] 字符集合。

(52) [^xyz] 负值字符集合。

(53) [a-z] 字符范围,匹配指定范围内的任意字符。

(54) [^a-z] 负值字符范围,匹配任何不在指定范围内的任意字符。

(55) \b 匹配一个单词边界,也就是指单词和空格间的位置。

(56) \B 匹配非单词边界。

(57) \cx 匹配由x指明的控制字符。

(58) \d 匹配一个数字字符。等价于 [0-9]。

(59) \D 匹配一个非数字字符。等价于 [^0-9]。

(60) \f 匹配一个换页符。等价于 \x0c 和 \cL。

(61) \n 匹配一个换行符。等价于 \x0a 和 \cJ。

(62) \r 匹配一个回车符。等价于 \x0d 和 \cM。

(63) \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。

(64) \S 匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。

(65) \t 匹配一个制表符。等价于 \x09 和 \cI。

(66) \v 匹配一个垂直制表符。等价于 \x0b 和 \cK。

(67) \w 匹配包括下划线的任何单词字符。等价于’[A-Za-z0-9_]’。

(68) \W 匹配任何非单词字符。等价于 ’[^A-Za-z0-9_]’。

(69) \xn 匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。

(70) \num 匹配 num,其中num是一个正整数。对所获取的匹配的引用。

(71) \n 标识一个八进制转义值或一个后向引用。如果 \n 之前至少 n 个获取的子表达式,则 n 为后向引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。

(72) \nm 标识一个八进制转义值或一个后向引用。如果 \nm 之前至少有is preceded by at least nm 个获取得子表达式,则 nm 为后向引用。如果 \nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的后向引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 \nm 将匹配八进制转义值 nm。

(73) \nml 如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。

(74) \un 匹配 n,其中 n 是一个用四个十六进制数字表示的Unicode字符。

(75) 匹配中文字符的正则表达式: [u4e00-u9fa5]

(76) 匹配双字节字符(包括汉字在内):[^x00-xff]

(77) 匹配空行的正则表达式:n[s| ]*r

(78) 匹配HTML标记的正则表达式:/<(.)>.</1>|<(.*) />/

(79) 匹配首尾空格的正则表达式:(^s*)|(s*$)

(80) 匹配Email地址的正则表达式:w+([-+.]w+)@w+([-.]w+).w+([-.]w+)*

(81) 匹配网址URL的正则表达式:http://([w-]+.)+[w-]+(/[w- ./?%&=]*)?

(82) 利用正则表达式限制网页表单里的文本框输入内容:

(83) 用正则表达式限制只能输入中文:οnkeyup=“value=value.replace(/[^u4E00-u9FA5]/g,’’)” onbeforepaste=“clipboardData.setData(‘text’,clipboardData.getData(‘text’).replace(/[^u4E00-u9FA5]/g,’’))”

(84) 用正则表达式限制只能输入全角字符: οnkeyup=“value=value.replace(/[^uFF00-uFFFF]/g,’’)” onbeforepaste=“clipboardData.setData(‘text’,clipboardData.getData(‘text’).replace(/[^uFF00-uFFFF]/g,’’))”

(85) 用正则表达式限制只能输入数字:οnkeyup="value=value.replace(/[^d]/g,’’) "onbeforepaste=“clipboardData.setData(‘text’,clipboardData.getData(‘text’).replace(/[^d]/g,’’))”

(86) 用正则表达式限制只能输入数字和英文:οnkeyup="value=value.replace(/[W]/g,’’) "onbeforepaste=“clipboardData.setData(‘text’,clipboardData.getData(‘text’).replace(/[^d]/g,’’))”

(87) 整理:

(88) 匹配中文字符的正则表达式: [\u4e00-\u9fa5]

(89) 匹配双字节字符(包括汉字在内):[^\x00-\xff]

(90) 匹配空行的正则表达式:\n[\s| ]*\r

(91) 匹配HTML标记的正则表达式:/<(.)>.</\1>|<(.*) />/

(92) 匹配首尾空格的正则表达式:(^\s*)|(\s*$)

(93) 匹配IP地址的正则表达式:/(\d+).(\d+).(\d+).(\d+)/g //

(94) 匹配Email地址的正则表达式:\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*

(95) 匹配网址URL的正则表达式:http://(/[\w-]+.)+[\w-]+(/[\w- ./?%&=]*)?

(96) sql语句:^(select|drop|delete|create|update|insert).*$

(97) 非负整数:^\d+$

(98) 正整数:11[1-9][0-9]$

(99) 非正整数:^((-\d+)|(0+))$

(100) 负整数:^-[0-9][1-9][0-9]$

(101) 整数:^-?\d+$

(102) 非负浮点数:^\d+(.\d+)?$

(103) 正浮点数:^((0-9)+.[0-9][1-9][0-9])|([0-9][1-9][0-9].[0-9]+)|([0-9][1-9][0-9]))$

(104) 非正浮点数:^((-\d+.\d+)?)|(0+(.0+)?))$

(105) 负浮点数:^(-((正浮点数正则式)))$

(106) 英文字符串:12+$

(107) 英文大写串:13+$

(108) 英文小写串:14+$

(109) 英文字符数字串:15+$

(110) 英数字加下划线串:^\w+$

(111) E-mail地址:16+(.[\w-]+)*@[\w-]+(.[\w-]+)+$

(112) URL:17+://(\w+(-\w+))(.(\w+(-\w+)))(?\s)?$

或:http://[A-Za-z0-9]+.[A-Za-z0-9]+[/=?%-&_~`@[]’:+!]*([<>""])*$

(113) 邮政编码:18\d{5}$

(114) 中文:19+$

(115) 电话号码:^((\d2,3\d2,3)|(\d{3}-))?(0\d2,30\d2,3|0\d{2,3}-)?[1-9]\d{6,7}(-\d{1,4})?$

(116) 手机号码:^((\d2,3\d2,3)|(\d{3}-))?13\d{9}$

(117) 双字节字符(包括汉字在内):^\x00-\xff

(118) 匹配首尾空格:(^\s*)|(\s*$)(像vbscript那样的trim函数)

(119) 匹配HTML标记:<(.)>.</\1>|<(.*) />

(120) 匹配空行:\n[\s| ]*\r

(121) 提取信息中的网络链接:(h|H)(r|R)(e|E)(f|F) *= *(’|")?(\w|\|/|.)+(’|"| *|>)?

(122) 提取信息中的邮件地址:\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*

(123) 提取信息中的图片链接:(s|S)(r|R)(c|C) *= *(’|")?(\w|\|/|.)+(’|"| *|>)?

(124) 提取信息中的IP地址:(\d+).(\d+).(\d+).(\d+)

(125) 提取信息中的中国手机号码:(86)013\d{9}

(126) 提取信息中的中国固定电话号码:(\d3,4\d3,4|\d{3,4}-|\s)?\d{8}

(127) 提取信息中的中国电话号码(包括移动和固定电话):(\d3,4\d3,4|\d{3,4}-|\s)?\d{7,14}

(128) 提取信息中的中国邮政编码:[1-9]{1}(\d+){5}

(129) 提取信息中的浮点数(即小数):(-?\d*).?\d+

(130) 提取信息中的任何数字 :(-?\d*)(.\d+)?

(131) IP:(\d+).(\d+).(\d+).(\d+)

(132) 电话区号:/^0\d{2,3}$/

(133) 腾讯QQ号:20[1-9][0-9]$

(134) 帐号(字母开头,允许5-16字节,允许字母数字下划线):21[a-zA-Z0-9_]{4,15}$

(135) 中文、英文、数字及下划线:22+$

7.6 变量处理和方法处理

Java 9引入了一个新的VarHandle类,并增强了原有的MethodHandle类。通过这两个类,允许Java像动态语言一样引用变量、引用方法,并调用它们。

7.6.1 Java 9增强的MethodHandle

MethodHandle为Java增加了方法引用的功能,方法引用的概念有点类似于C的“函数指针”。这种方法引用是一种轻量级的引用方式,它不会检查方法的访问权限,也不管方法所属的类、实例方法或静态方法,MethodHandle就是简单代表特定的方法,并可通过MethodHandle来调用方法。

为了使用MethodHandle,还涉及如下几个类:

➢ MethodHandles:MethodHandle的工厂类,它提供了一系列静态方法用于获取MethodHandle。

➢ MethodHandles.Lookup:Lookup静态内部类也是MethodHandle、VarHandle的工厂类,专门用于获取MethodHandle和VarHandle。

➢ MethodType:代表一个方法类型。MethodType根据方法的形参、返回值类型来确定方法类型。

下面程序示范了MethodHandle的用法。

package org.zwc.methodhandletest;
 
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
 
 
/**
 * Created by zhangwenchao on 2018/1/3.
 */
public class MHTest {
 
 
    public String toString(String s) {
 
        return "hello," + s + "MethodHandle";
    }
 
 
    public static void main(String[] args) {
        MHTest mhTest = new MHTest();
        MethodHandle mh = getToStringMH();  //获取方法句柄
       
        try {
            // 1.调用方法:
            String result = (String) mh.invokeExact(mhTest, "ssssss");  //根据方法句柄调用方法----注意返回值必须强转
            System.out.println(result);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
 
 
        // 2.or like this:
        try {
            MethodHandle methodHandle2 = mh.bindTo(mhTest);
            String toString2 = (String) methodHandle2.invokeWithArguments("sssss");
            System.out.println(toString2);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
 
 
        // 得到当前Class的不同表示方法,最后一个最好。一般我们在静态上下文用SLF4J得到logger用。
        System.out.println(MHTest.class);
        System.out.println(mhTest.getClass());
        System.out.println(MethodHandles.lookup().lookupClass()); // like getClass()
 
 
    }
 
    /**
     * 获取方法句柄
     * @return
     */
    public static MethodHandle getToStringMH() {
 
        MethodType mt = MethodType.methodType(String.class, String.class);  //获取方法类型 参数为:1.返回值类型,2方法中参数类型
 
        MethodHandle mh = null;
        try {
            mh = MethodHandles.lookup().findVirtual(MHTest.class, "toString", mt);  //查找方法句柄
        } catch (NoSuchMethodException | IllegalAccessException e) {
            e.printStackTrace();
        }
 
        return mh;
    }
}

7.6.2 Java 9增加的VarHandle

VarHandle主要用于动态操作数组的元素或对象的成员变量。VarHandle与MethodHandle非常相似,它也需要通过MethodHandles来获取实例,接下来调用VarHandle的方法即可动态操作指定数组的元素或指定对象的成员变量。

下面程序示范了VarHandle的用法。

package org.zwc.methodhandletest;
 
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
 
 
/**
 * Created by zhangwenchao on 2018/1/3.
 */
public class MHTest {
 
 
    public String toString(String s) {
 
        return "hello," + s + "MethodHandle";
    }
 
 
    public static void main(String[] args) {
        MHTest mhTest = new MHTest();
        MethodHandle mh = getToStringMH();  //获取方法句柄
       
        try {
            // 1.调用方法:
            String result = (String) mh.invokeExact(mhTest, "ssssss");  //根据方法句柄调用方法----注意返回值必须强转
            System.out.println(result);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
 
 
        // 2.or like this:
        try {
            MethodHandle methodHandle2 = mh.bindTo(mhTest);
            String toString2 = (String) methodHandle2.invokeWithArguments("sssss");
            System.out.println(toString2);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
 
 
        // 得到当前Class的不同表示方法,最后一个最好。一般我们在静态上下文用SLF4J得到logger用。
        System.out.println(MHTest.class);
        System.out.println(mhTest.getClass());
        System.out.println(MethodHandles.lookup().lookupClass()); // like getClass()
 
 
    }
 
    /**
     * 获取方法句柄
     * @return
     */
    public static MethodHandle getToStringMH() {
 
        MethodType mt = MethodType.methodType(String.class, String.class);  //获取方法类型 参数为:1.返回值类型,2方法中参数类型
 
        MethodHandle mh = null;
        try {
            mh = MethodHandles.lookup().findVirtual(MHTest.class, "toString", mt);  //查找方法句柄
        } catch (NoSuchMethodException | IllegalAccessException e) {
            e.printStackTrace();
        }
 
        return mh;
    }
}

7.7 Java 11改进的国际化与格式化

Java程序的国际化主要通过如下三个类完成。

➢ java.util.ResourceBundle:用于加载国家、语言资源包。

➢ java.util.Locale:用于封装特定的国家/区域、语言环境。

➢ java.text.MessageFormat:用于格式化带占位符的字符串。

为了实现程序的国际化,必须先提供程序所需要的资源文件。资源文件的内容是很多key-value对,其中key是程序使用的部分,而value则是程序界面的显示字符串。

资源文件的命名可以有如下三种形式。

➢ baseName_language_country.properties

➢ baseName_language.properties

➢ baseName.properties

其中baseName是资源文件的基本名,用户可随意指定;而language和country都不可随意变化,必须是Java所支持的语言和国家。

Locale类的getAvailableLocales()方法,该方法返回一个Locale数组,该数组里包含了Java所支持的国家和语言。

MessageFormat是抽象类Format的子类,Format抽象类还有两个子类:NumberFormat和DateFormat,它们分别用以实现数值、日期的格式化。NumberFormat、DateFormat可以将数值、日期转换成字符串,也可以将字符串转换成数值、日期。SimpleDateFormat是DateFormat的子类,正如它的名字所暗示的,它是“简单”的日期格式器。

7.8 Java 8新增的日期、时间格式器

一 获取DateTimeFormatter对象的三种方式

直接使用静态常量创建DateTimeFormatter格式器
使用代码不同风格的枚举值来创建DateTimeFormatter格式器
根据模式字符串来创建DateTimeFormatter格式器
二 DateTimeFormatter完成格式化
1 代码示例

import java.time.*;
import java.time.format.*;
 
public class NewFormatterTest
{
	public static void main(String[] args)
	{
		DateTimeFormatter[] formatters = new DateTimeFormatter[]{
			// 直接使用常量创建DateTimeFormatter格式器
			DateTimeFormatter.ISO_LOCAL_DATE,
			DateTimeFormatter.ISO_LOCAL_TIME,
			DateTimeFormatter.ISO_LOCAL_DATE_TIME,
			// 使用本地化的不同风格来创建DateTimeFormatter格式器
			DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL, FormatStyle.MEDIUM),
			DateTimeFormatter.ofLocalizedTime(FormatStyle.LONG),
			// 根据模式字符串来创建DateTimeFormatter格式器
			DateTimeFormatter.ofPattern("Gyyyy%%MMM%%dd HH:mm:ss")
		};
		LocalDateTime date = LocalDateTime.now();
		// 依次使用不同的格式器对LocalDateTime进行格式化
		for(int i = 0 ; i < formatters.length ; i++)
		{
			// 下面两行代码的作用相同
			System.out.println(date.format(formatters[i]));
			System.out.println(formatters[i].format(date));
		}
	}
}
 

2 运行结果

2016-09-04
2016-09-04
12:18:33.557
12:18:33.557
2016-09-04T12:18:33.557
2016-09-04T12:18:33.557
201694日 星期日 12:18:33
201694日 星期日 12:18:33
下午121833秒
下午121833秒
公元2016%%九月%%04 12:18:33
公元2016%%九月%%04 12:18:33

3 代码说明

上面代码使用3种方式创建了6个DateTimeFormatter对象,然后程序中使用不同方式来格式化日期。

三 DateTimeFormatter解析字符串
1 代码示例

import java.time.*;
import java.time.format.*;
 
public class NewFormatterParse
{
	public static void main(String[] args)
	{
		// 定义一个任意格式的日期时间字符串
		String str1 = "2014==04==12 01时06分09秒";
		// 根据需要解析的日期、时间字符串定义解析所用的格式器
		DateTimeFormatter fomatter1 = DateTimeFormatter
			.ofPattern("yyyy==MM==dd HH时mm分ss秒");
		// 执行解析
		LocalDateTime dt1 = LocalDateTime.parse(str1, fomatter1);
		System.out.println(dt1); // 输出 2014-04-12T01:06:09
		// ---下面代码再次解析另一个字符串---
		String str2 = "2014$$$四月$$$13 20小时";
		DateTimeFormatter fomatter2 = DateTimeFormatter
			.ofPattern("yyy$$$MMM$$$dd HH小时");
		LocalDateTime dt2 = LocalDateTime.parse(str2, fomatter2);
		System.out.println(dt2); // 输出 2014-04-13T20:00
	}
}
 

2 运行结果

2014-04-12T01:06:09
2014-04-13T20:00

3 代码说明

上面代码定义了两个不同格式日期、时间字符串。为了解析他们,代码分别使用对应的格式字符串创建了DateTimeFormatter对象,这样DateTimeFormatter即可按照格式化字符串将日期、时间字符串解析成LocalDateTime对象。

第8章 Java集合

8.1 Java集合概述

集合类主要负责保存、盛装其他数据,因此集合类也称为容器类。所有的集合类都位于java.util包下,后来为了处理多线程环境下的并发安全问题,Java 5还在java.util.concurrent包下提供了一些多线程支持的集合类。

Java的集合类主要由两个接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。

8.2 Java 11增强的Collection和Iterator接口

Collection接口里定义了如下操作集合元素的方法。

➢ boolean add(Object o):该方法用于向集合里添加一个元素。

➢ boolean addAll(Collection c):该方法把集合c里的所有元素添加到指定集合里。

➢ void clear():清除集合里的所有元素,将集合长度变为0。

➢ boolean contains(Object o):返回集合里是否包含指定元素。

➢ boolean containsAll(Collection c):返回集合里是否包含集合c里的所有元素。

➢ boolean isEmpty():返回集合是否为空。当集合长度为0时返回true,否则返回false。

➢ Iterator iterator():返回一个Iterator对象,用于遍历集合里的元素。

➢ boolean remove(Object o):删除集合中的指定元素o,当集合中包含了一个或多个元素o时,该方法只删除第一个符合条件的元素,该方法将返回true。

➢ boolean removeAll(Collection c):从集合中删除集合c里包含的所有元素(相当于用调用该方法的集合减集合c),如果删除了一个或一个以上的元素,则该方法返回true。

➢ boolean retainAll(Collection c):从集合中删除集合c里不包含的元素(相当于把调用该方法的集合变成该集合和集合c的交集),如果该操作改变了调用该方法的集合,则该方法返回true。

➢ int size():该方法返回集合里元素的个数

Java 11为Collection新增了一个toArray(IntFunction)方法,使用该方法的主要目的就是利用泛型。对于传统的toArray()方法而言,不管Collection本身是否使用泛型,toArray()的返回值总是Object[];但新增的toArray(IntFunction)方法不同,当Collection使用泛型时,toArray(IntFunction)可以返回特定类型的数组。

8.2.1 使用Lambda表达式遍历集合

Java 8为Iterable接口新增了一个forEach(Consumer action)默认方法,该方法所需参数的类型是一个函数式接口。正因为Consumer是函数式接口,因此可以使用Lambda表达式来遍历集合元素。

        //使用com.google.guava包创建集合
		List<String> list =Lists.newArrayList("a","b","c","d");

        //遍历1  其中anyThing可以用其它字符替换

        list.forEach((anyThing)->System.out.println(anyThing));

        //遍历2

        list.forEach(any->System.out.println(any));

        //匹配输出 : "b"

        list.forEach(item->{

            if("b".equals(item)){

                System.out.println(item);

            }

        });

8.2.2 使用Iterator遍历集合元素

Iterator则主要用于遍历(即迭代访问)Collection集合中的元素,Iterator对象也被称为迭代器。

Iterator接口里定义了如下4个方法。

➢ boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true。

➢ Object next():返回集合里的下一个元素。

➢ void remove():删除集合里上一次next方法返回的元素。

➢ void forEachRemaining(Consumer action),这是Java 8为Iterator新增的默认方法,该方法可使用Lambda表达式来遍历集合元素。

Iterator迭代器采用的是快速失败(fail-fast)机制,一旦在迭代过程中检测到该集合已经被修改(通常是程序中的其他线程修改),程序立即引发ConcurrentModificationException异常,而不是显示修改后的结果,这样可以避免共享资源而引发的潜在问题。

 1 public static void main(String[] args) {
 2         List<String> list = new ArrayList<String>();
 3         list.add("张三1");
 4         list.add("张三2");
 5         list.add("张三3");
 6         list.add("张三4");
 7         
 8         List<String> linkList = new LinkedList<String>();
 9         linkList.add("link1");
10         linkList.add("link2");
11         linkList.add("link3");
12         linkList.add("link4");
13         
14         Set<String> set = new HashSet<String>();
15         set.add("set1");
16         set.add("set2");
17         set.add("set3");
18         set.add("set4");
19         //使用迭代器遍历ArrayList集合
20         Iterator<String> listIt = list.iterator();
21         while(listIt.hasNext()){
22             System.out.println(listIt.next());
23         }
24         //使用迭代器遍历Set集合
25         Iterator<String> setIt = set.iterator();
26         while(setIt.hasNext()){
27             System.out.println(listIt.next());
28         }
29         //使用迭代器遍历LinkedList集合
30         Iterator<String> linkIt = linkList.iterator();
31         while(linkIt.hasNext()){
32             System.out.println(listIt.next());
33         }
34 }

8.2.3 使用Lambda表达式遍历Iterator

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

import java.util.*;
 
public class IteratorEach
{
	public static void main(String[] args)
	{
		
		Collection books = new HashSet();
		books.add("Java EE");
		books.add("Java");
		books.add("Android");
		// 获取books集合对应的迭代器
		Iterator it = books.iterator();
		// 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
		it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
	}
}

8.2.4 使用foreach循环遍历集合元素

Java 5提供的foreach循环迭代访问集合元素更加便捷。
在这里插入图片描述

8.2.5 使用Predicate操作集合

Java 8为Collection集合新增了一个removeIf(Predicate filter)方法,该方法将会批量删除符合filter条件的所有元素。该方法需要一个Predicate(谓词)对象作为参数,Predicate也是函数式接口,因此可使用Lambda表达式作为参数。

public class ForeachTest {
    public static void main(String[] args) {
        // 创建一个集合
        Collection objs = new HashSet();
        objs.add(new String("中文百度搜索Java教程"));
        objs.add(new String("中文百度搜索C++教程"));
        objs.add(new String("中文百度搜索C语言教程"));
        objs.add(new String("中文百度搜索Python教程"));
        objs.add(new String("中文百度搜索Go教程"));
        // 统计集合中出现“中文百度搜索”字符串的数量
        System.out.println(calAll(objs, ele -> ((String) ele).contains("中文百度搜索")));
        // 统计集合中出现“Java”字符串的数量
        System.out.println(calAll(objs, ele -> ((String) ele).contains("Java")));
        // 统计集合中出现字符串长度大于 12 的数量
        System.out.println(calAll(objs, ele -> ((String) ele).length() > 12));
    }

    public static int calAll(Collection books, Predicate p) {
        int total = 0;
        for (Object obj : books) {
            // 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
            if (p.test(obj)) {
                total++;
            }
        }
        return total;
    }
}

8.2.6 使用Stream操作集合

Java 8还新增了Stream、IntStream、LongStream、DoubleStream等流式API,这些API代表多个支持串行和并行聚集操作的元素。

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

独立使用Stream的步骤如下:

①使用Stream或XxxStream的builder()类方法创建该Stream对应的Builder。

②重复调用Builder的add()方法向该流中添加多个元素。

③调用Builder的build()方法获取对应的Stream。

④调用Stream的聚集方法。

在上面4个步骤中,第4步可以根据具体需求来调用不同的方法,Stream提供了大量的聚集方法供用户调用,具体可参考Stream或XxxStream的API文档。对于大部分聚集方法而言,每个Stream只能执行一次。

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

➢ 中间方法:中间操作允许流保持打开状态,并允许直接调用后续方法。

➢ 末端方法:末端方法是对流的最终操作。

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

➢ 有状态的方法:这种方法会给流增加一些新的属性,比如元素的唯一性、元素的最大数量、保证元素以排序的方式被处理等。有状态的方法往往需要更大的性能开销。

➢ 短路方法:短路方法可以尽早结束对流的操作,不必检查所有的元素。

下面简单介绍一下Stream常用的中间方法。

➢ filter(Predicate predicate):过滤Stream中所有不符合predicate的元素。

➢ mapToXxx(ToXxxFunction mapper):使用ToXxxFunction对流中的元素执行一对一的转换,该方法返回的新流中包含了ToXxxFunction转换生成的所有元素。

➢ peek(Consumer action):依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元素。该方法用于调试。

➢ distinct():该方法用于排序流中所有重复元素(判断元素重复的标准是使用equals()比较返回true),这是一个有状态的方法。

➢ sorted():该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。

➢ limit(long maxSize):该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个有状态的、短路方法。

下面简单介绍一下Stream常用的末端方法。

➢ forEach(Consumer action):遍历流中所有元素,对每个元素执行action。

➢ toArray():将流中所有元素转换为一个数组

➢ reduce():该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。

➢ min():返回流中所有元素的最小值。

➢ max():返回流中所有元素的最大值。

➢ count():返回流中所有元素的数量。

➢ anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合Predicate条件。

package com.xiaobu.demo;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author xiaobu
 * @version JDK1.8.0_171
 * @date on  2019/4/23 9:34
 * @description V1.0
 */
public class StreamDemo {
    public static void main(String[] args) {
        test3();
        test1();
        List<String> list1 = new ArrayList<String>();
        list1.add("1");
        list1.add("2");
        list1.add("3");
        list1.add("5");
        list1.add("6");

        List<String> list2 = new ArrayList<String>();
        list2.add("2");

        System.out.println("===差集===");
        List<String> reduce1 = list1.stream().filter(item -> !list2.contains(item)).collect(Collectors.toList());
        //并行流
        reduce1.parallelStream().forEach(System.out::println);

        System.out.println("===交集===");
        List<String> reduce2 = list1.stream().filter(list2::contains).collect(Collectors.toList());
        reduce2.forEach(System.out::println);

        System.out.println("===去重并集===");
        list1.addAll(list2);
        List<String> reduce3 = list1.stream().distinct().collect(Collectors.toList());
        System.out.println("reduce3 = " + reduce3);

        list1 = list1.stream().distinct().sorted().collect(Collectors.toList());
        System.out.println("list1 = " + list1);

        //Collectors.joining() 按倒序拼接成字符串  listStr = 65321
        String listStr = list1.stream().distinct().sorted(Comparator.reverseOrder()).collect(Collectors.joining());
        System.out.println("listStr = " + listStr);
        //按正序拼接成字符串   listStr = [1,2,3,5,6]
        listStr = list1.stream().distinct().sorted().collect(Collectors.joining(",", "[", "]"));
        System.out.println("listStr = " + listStr);
    }


    public static void test1() {
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        list1.add(2);
        list1.add(3);

        List<Integer> list2 = new ArrayList<>();
        list2.add(3);
        list2.add(4);
        list2.add(5);

        System.out.println("====求交集===");

        List<Integer> list = list1.stream().filter(list2::contains).collect(Collectors.toList());
        list.forEach(System.out::println);

        System.out.println("====求差集===");
        list = list1.stream().filter(t -> !list2.contains(t)).collect(Collectors.toList());
        list.forEach(System.out::println);

        System.out.println("====求去重并集===");
        list.addAll(list1);
        list.addAll(list2);
        list = list.stream().distinct().collect(Collectors.toList());
        list.forEach(System.out::println);
    }


    public static void test3() {
        List<String> names1 = new ArrayList<String>();
        names1.add("Google ");
        names1.add("Runoob ");
        names1.add("Taobao ");
        names1.add("Baidu ");
        names1.add("Sina ");
        //逆序
        Collections.sort(names1, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.compareTo(o1);
            }
        });
        System.out.println("names1 = " + names1);
    }
}



8.3 Set集合

Set集合不允许包含相同的元素,如果试图把两个相同的元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。

8.3.1 HashSet类

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

HashSet具有以下特点。

➢ 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。

➢ HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步。

➢ 集合元素值可以是null。

HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,且两个对象的hashCode()值也相等。

原理

1、HashSet的底层结构
这个问题在打开源码的一瞬间就找到了答案,注释里面作了清楚的说明:This class implements the Set interface, backed by a hash table (actually a HashMap instance) ,HashSet内部包含了一个HashMap的实例,对HashSet的操作实际都是对HashMap实例的操作。比较有意思的地方在于HashSet还定义了一个成员变量PRESENT作为所有添加到HashSet中元素的值,即以添加到HashSet中的元素为Key,以此处定义的成员变量PRESENT为Value,组成Key-Value键值对,然后添加到HashMap实例中。

//用于储存对象
private transient HashMap<E,Object> map;//transient将可以序列化的对象在序列化的时候不被序列化
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

//HashSet.add(E e)方法
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

2、HashSet如何实现去重
这个问题的答案也在add方法中,不过具体得看HashMap的put()方法,源码如下:
HashMap#put

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

HashMap#putVal

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

对于该方法中有关添加元素的逻辑判断等不作展开,此处需要关注的是下面这段代码:

//省略其他代码。。。
//p为HashMap中key索引位置原来存在的元素
if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;

//省略其他代码,在满足上面条件的情况下,会跳转到下面的代码块
 if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //@param onlyIfAbsent if true, don't change existing value
                if (!onlyIfAbsent || oldValue == null)  
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

从上面代码可以看出,在HashMap中,当添加元素的Key已经存在时,HashMap会替换key所对应的value,同时返回oldvalue。而当添加元素的Key不存在时,则正常添加,最后返回null。
回到HashSet中,由于添加到HashSet中的元素在其内部HasMap的实例中充当的角色是Key,所以当重复添加时map.put(e,PRSENT)==null值为false,表示添加失败。
3、迭代器问题
在HashSet的类注释中有这么一段话

Iterating over this set requires time proportional to the sum of
 the <tt>HashSet</tt> instance's size (the number of elements) plus the
"capacity" of the backing <tt>HashMap</tt> instance (the number of
 buckets).  Thus, it's very important not to set the initial capacity too
 high (or the load factor too low) if iteration performance is important.

常用

<span style="font-size:18px;">HashSet hashset=new HashSet();</span>

HashSet添加元素

<span style="font-size:18px;">//向hashset中添加一个字符串
hashset.add("abc");
//向hashset中添加一个整数
hashset.add(1);
//向hashset中添加一个字符
hashset.add('a');
//向hashset中添加一个数组
int[] abc={10,11,12};
hashset.add(abc);
//向hashset中添加一个自定义对象
Cat cat1=new Cat("asd", 2);
hashset.add(cat1);//向hashset中添加一个对象</span>

三、遍历HashSet

<span style="font-size:18px;">//遍历HashSet
		Iterator it = hashset.iterator();
		while(it.hasNext())
		{
			Object obj = it.next();
			if(obj instanceof Integer)
			{
				 System.out.println("Integer:"+obj);
			}
			if(obj instanceof String)
			{
				 System.out.println("String:"+obj);
			}
			if(obj instanceof Character)
			{
				 System.out.println("Character:"+obj);
			}
			if(obj instanceof int[])
			{
				System.out.print("int[]:");
				for(int i=0;i<abc.length;i++)
				{
					System.out.print(abc[i]+" ");
				}
			}
		}</span>

笔试题:

对于一个字符串,请设计一个高效算法,找到第一次重复出现的字符。
给定一个字符串(不一定全为字母)A及它的长度n。请返回第一个重复出现的字符。保证字符串中有重复字符,字符串的长度小于等于500。
测试样例:
“qywyer23tdd”,11
返回:y

import java.util.*;
public class FirstRepeat {
	public static char findFirstRepeat(String A, int n) {
	
	char[] a=A.toCharArray();
	HashSet hs=new HashSet<>();
	for(int i=0; i<n;i++) 
	{
		if (!hs.add(a[i])) 
		{
			return a[i];
		}
	}
	return 0;
	}
 
	public static void main(String[] args)
	{
		System.out.println(findFirstRepeat("qywyer23tdd",11));
	}
}

8.3.2 LinkedHashSet类

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

原理
LinkedHashSet是Set集合的一个实现,具有set集合不重复的特点,同时具有可预测的迭代顺序,也就是我们插入的顺序。

并且linkedHashSet是一个非线程安全的集合。如果有多个线程同时访问当前linkedhashset集合容器,并且有一个线程对当前容器中的元素做了修改,那么必须要在外部实现同步保证数据的冥等性。

下面我们new一个新的LinkedHashSet容器看一下具体的源码实现。并分析师如何保证数据的插入顺序:

Set set = new LinkedHashSet<>();

跟进LinkedHashSet可以得到super一个父类初始化为一个容器为16大小,加载因子为0.75的Map容器。

构造一个空连接散列集合
在这里插入图片描述
在这里插入图片描述
实际创建的是一个LinkedHashMap带有制定大小和加载因子的容器。

在前面讲过一次,map的容器的大小必须是2的冥,那么在讲一次如何保证必须是2的冥,通过我们传入的参数在构建map集合的是通过位运算实现:

在这里插入图片描述
其中initialCapacity为我们传入的具体按容器的大小。

上面是我们描述的LinkedHashSet的具体构建过程,以及构建的具体内容。

由于LinkedHashSet是一个哈希表和链表的结合,且是一个双向链表,那么我们来看一下什么是双向连边?

双向链表是链表的一种,他的每个数据节点都有两个指针分别指向直接后继和直接前驱,所以从双向链表的任意一个节点开始都可以很方便的访问它的前驱节点和后继节点。这是双向链表的优点,那么有优点就有缺点,缺点是每个节点都需要保存当前节点的next和prev两个属性,这样才能保证优点。所以需要更多的内存开销,并且删除和添加也会比较费时间。

下面我们图示一个双向两表的节点:
在这里插入图片描述
多个节点相互连接,保证了数据录入的顺序。

那么我们源码分析一下具体的录入详情:

我们定义一个LinkedHashSet—LinkedHashSet set = new LinkedHashSet<>();

然后set.add();跟一下这个add是走的那个方法:
在这里插入图片描述
跟进来走的是put的方法:LinkedHashSet.class下的,这个是重写了超类中put的具体add方法。他会在新分配的元素在链表的末尾插入一条。
在这里插入图片描述
在这里插入图片描述
进来走的还是HashMap的put添加方法,在上面的判断和计算hash确定位置之后,由于LinkedHashSet重写了addEntry
在这里插入图片描述
在元素的后面添加新的元素。

整个过程就是LinkedHashSet在容器插入数据的过程。此过程主要由LinkedHashSet.class中重写超类的两个addEntry和createEntry 实现双向链表的结构。保证数据已我们录入的顺序遍历输出。

用法

import java.util.LinkedHashSet;
 
public class Test {
	public static void main(String[] args) {
		LinkedHashSet<String> hs=new LinkedHashSet<String>();
		
		hs.add("hello");
		hs.add("world");
		hs.add("java");
		
		for(String h:hs) {
			System.out.println(h);
		}
	}
}
 
输出:
 
hello
world
java
 
 
输出有序
import java.util.LinkedHashSet;
 
public class Test {
	public static void main(String[] args) {
		LinkedHashSet<String> hs=new LinkedHashSet<String>();
		
		hs.add("hello");
		hs.add("world");
		hs.add("java");
		hs.add("hello");
		hs.add("world");
		
		
		for(String h:hs) {
			System.out.println(h);
		}
	}
}
 
 
输出:
 
hello
world
java
 
 
唯一性

8.3.3 TreeSet类

TreeSet是SortedSet接口的实现类,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。

与HashSet集合相比,TreeSet还提供了如下几个额外的方法。

➢ Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,则返回null。

➢ Object first():返回集合中的第一个元素。

➢ Object last():返回集合中的最后一个元素。

➢ Object lower(Object e):返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,参考元素不需要是TreeSet集合里的元素)。

➢ Object higher(Object e):返回集合中位于指定元素之后的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)。

➢ SortedSet subSet(Object fromElement, Object toElement):返回此Set的子集合,范围从fromElement(包含)到toElement(不包含)。

➢ SortedSet headSet(Object toElement):返回此Set的子集,由小于toElement的元素组成。

➢ SortedSet tailSet(Object fromElement):返回此Set的子集,由大于或等于fromElement的元素组成。

与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。TreeSet支持两种排序方法:自然排序和定制排序。在默认情况下,TreeSet采用自然排序。

第1部分 TreeSet介绍

TreeSet简介
TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, java.io.Serializable接口。
TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
TreeSet 实现了Cloneable接口,意味着它能被克隆。
TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。
TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是fail-fast的。
 
TreeSet的构造函数

// 默认构造函数。使用该构造函数,TreeSet中的元素按照自然排序进行排列。
TreeSet()

// 创建的TreeSet包含collection
TreeSet(Collection<? extends E> collection)

// 指定TreeSet的比较器
TreeSet(Comparator<? super E> comparator)

// 创建的TreeSet包含set
TreeSet(SortedSet<E> set)

TreeSet的API

boolean                   add(E object)
boolean                   addAll(Collection<? extends E> collection)
void                      clear()
Object                    clone()
boolean                   contains(Object object)
E                         first()
boolean                   isEmpty()
E                         last()
E                         pollFirst()
E                         pollLast()
E                         lower(E e)
E                         floor(E e)
E                         ceiling(E e)
E                         higher(E e)
boolean                   remove(Object object)
int                       size()
Comparator<? super E>     comparator()
Iterator<E>               iterator()
Iterator<E>               descendingIterator()
SortedSet<E>              headSet(E end)
NavigableSet<E>           descendingSet()
NavigableSet<E>           headSet(E end, boolean endInclusive)
SortedSet<E>              subSet(E start, E end)
NavigableSet<E>           subSet(E start, boolean startInclusive, E end, boolean endInclusive)
NavigableSet<E>           tailSet(E start, boolean startInclusive)
SortedSet<E>              tailSet(E start)

说明:
(01) TreeSet是有序的Set集合,因此支持add、remove、get等方法。
(02) 和NavigableSet一样,TreeSet的导航方法大致可以区分为两类,一类时提供元素项的导航方法,返回某个元素;另一类时提供集合的导航方法,返回某个集合。
lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素,如果不存在这样的元素,则返回 null。

第2部分 TreeSet数据结构
TreeSet的继承关系

java.lang.Objectjava.util.AbstractCollection<E>java.util.AbstractSet<E>java.util.TreeSet<E>

public class TreeSet<E> extends AbstractSet<E>        
    implements NavigableSet<E>, Cloneable, java.io.Serializable{}

TreeSet与Collection关系如下图:
在这里插入图片描述
从图中可以看出:
(01) TreeSet继承于AbstractSet,并且实现了NavigableSet接口。
(02) TreeSet的本质是一个"有序的,并且没有重复元素"的集合,它是通过TreeMap实现的。TreeSet中含有一个"NavigableMap类型的成员变量"m,而m实际上是"TreeMap的实例"。

第3部分 TreeSet源码解析(基于JDK1.6.0_45)
为了更了解TreeSet的原理,下面对TreeSet源码代码作出分析。

  1 package java.util;
  2 
  3 public class TreeSet<E> extends AbstractSet<E>
  4     implements NavigableSet<E>, Cloneable, java.io.Serializable
  5 {
  6     // NavigableMap对象
  7     private transient NavigableMap<E,Object> m;
  8 
  9     // TreeSet是通过TreeMap实现的,
 10     // PRESENT是键-值对中的值。
 11     private static final Object PRESENT = new Object();
 12 
 13     // 不带参数的构造函数。创建一个空的TreeMap
 14     public TreeSet() {
 15         this(new TreeMap<E,Object>());
 16     }
 17 
 18     // 将TreeMap赋值给 "NavigableMap对象m"
 19     TreeSet(NavigableMap<E,Object> m) {
 20         this.m = m;
 21     }
 22 
 23     // 带比较器的构造函数。
 24     public TreeSet(Comparator<? super E> comparator) {
 25         this(new TreeMap<E,Object>(comparator));
 26     }
 27 
 28     // 创建TreeSet,并将集合c中的全部元素都添加到TreeSet中
 29     public TreeSet(Collection<? extends E> c) {
 30         this();
 31         // 将集合c中的元素全部添加到TreeSet中
 32         addAll(c);
 33     }
 34 
 35     // 创建TreeSet,并将s中的全部元素都添加到TreeSet中
 36     public TreeSet(SortedSet<E> s) {
 37         this(s.comparator());
 38         addAll(s);
 39     }
 40 
 41     // 返回TreeSet的顺序排列的迭代器。
 42     // 因为TreeSet时TreeMap实现的,所以这里实际上时返回TreeMap的“键集”对应的迭代器
 43     public Iterator<E> iterator() {
 44         return m.navigableKeySet().iterator();
 45     }
 46 
 47     // 返回TreeSet的逆序排列的迭代器。
 48     // 因为TreeSet时TreeMap实现的,所以这里实际上时返回TreeMap的“键集”对应的迭代器
 49     public Iterator<E> descendingIterator() {
 50         return m.descendingKeySet().iterator();
 51     }
 52 
 53     // 返回TreeSet的大小
 54     public int size() {
 55         return m.size();
 56     }
 57 
 58     // 返回TreeSet是否为空
 59     public boolean isEmpty() {
 60         return m.isEmpty();
 61     }
 62 
 63     // 返回TreeSet是否包含对象(o)
 64     public boolean contains(Object o) {
 65         return m.containsKey(o);
 66     }
 67 
 68     // 添加e到TreeSet中
 69     public boolean add(E e) {
 70         return m.put(e, PRESENT)==null;
 71     }
 72 
 73     // 删除TreeSet中的对象o
 74     public boolean remove(Object o) {
 75         return m.remove(o)==PRESENT;
 76     }
 77 
 78     // 清空TreeSet
 79     public void clear() {
 80         m.clear();
 81     }
 82 
 83     // 将集合c中的全部元素添加到TreeSet中
 84     public  boolean addAll(Collection<? extends E> c) {
 85         // Use linear-time version if applicable
 86         if (m.size()==0 && c.size() > 0 &&
 87             c instanceof SortedSet &&
 88             m instanceof TreeMap) {
 89             SortedSet<? extends E> set = (SortedSet<? extends E>) c;
 90             TreeMap<E,Object> map = (TreeMap<E, Object>) m;
 91             Comparator<? super E> cc = (Comparator<? super E>) set.comparator();
 92             Comparator<? super E> mc = map.comparator();
 93             if (cc==mc || (cc != null && cc.equals(mc))) {
 94                 map.addAllForTreeSet(set, PRESENT);
 95                 return true;
 96             }
 97         }
 98         return super.addAll(c);
 99     }
100 
101     // 返回子Set,实际上是通过TreeMap的subMap()实现的。
102     public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
103                                   E toElement,   boolean toInclusive) {
104         return new TreeSet<E>(m.subMap(fromElement, fromInclusive,
105                                        toElement,   toInclusive));
106     }
107 
108     // 返回Set的头部,范围是:从头部到toElement。
109     // inclusive是是否包含toElement的标志
110     public NavigableSet<E> headSet(E toElement, boolean inclusive) {
111         return new TreeSet<E>(m.headMap(toElement, inclusive));
112     }
113 
114     // 返回Set的尾部,范围是:从fromElement到结尾。
115     // inclusive是是否包含fromElement的标志
116     public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
117         return new TreeSet<E>(m.tailMap(fromElement, inclusive));
118     }
119 
120     // 返回子Set。范围是:从fromElement(包括)到toElement(不包括)。
121     public SortedSet<E> subSet(E fromElement, E toElement) {
122         return subSet(fromElement, true, toElement, false);
123     }
124 
125     // 返回Set的头部,范围是:从头部到toElement(不包括)。
126     public SortedSet<E> headSet(E toElement) {
127         return headSet(toElement, false);
128     }
129 
130     // 返回Set的尾部,范围是:从fromElement到结尾(不包括)。
131     public SortedSet<E> tailSet(E fromElement) {
132         return tailSet(fromElement, true);
133     }
134 
135     // 返回Set的比较器
136     public Comparator<? super E> comparator() {
137         return m.comparator();
138     }
139 
140     // 返回Set的第一个元素
141     public E first() {
142         return m.firstKey();
143     }
144 
145     // 返回Set的最后一个元素
146     public E first() {
147     public E last() {
148         return m.lastKey();
149     }
150 
151     // 返回Set中小于e的最大元素
152     public E lower(E e) {
153         return m.lowerKey(e);
154     }
155 
156     // 返回Set中小于/等于e的最大元素
157     public E floor(E e) {
158         return m.floorKey(e);
159     }
160 
161     // 返回Set中大于/等于e的最小元素
162     public E ceiling(E e) {
163         return m.ceilingKey(e);
164     }
165 
166     // 返回Set中大于e的最小元素
167     public E higher(E e) {
168         return m.higherKey(e);
169     }
170 
171     // 获取第一个元素,并将该元素从TreeMap中删除。
172     public E pollFirst() {
173         Map.Entry<E,?> e = m.pollFirstEntry();
174         return (e == null)? null : e.getKey();
175     }
176 
177     // 获取最后一个元素,并将该元素从TreeMap中删除。
178     public E pollLast() {
179         Map.Entry<E,?> e = m.pollLastEntry();
180         return (e == null)? null : e.getKey();
181     }
182 
183     // 克隆一个TreeSet,并返回Object对象
184     public Object clone() {
185         TreeSet<E> clone = null;
186         try {
187             clone = (TreeSet<E>) super.clone();
188         } catch (CloneNotSupportedException e) {
189             throw new InternalError();
190         }
191 
192         clone.m = new TreeMap<E,Object>(m);
193         return clone;
194     }
195 
196     // java.io.Serializable的写入函数
197     // 将TreeSet的“比较器、容量,所有的元素值”都写入到输出流中
198     private void writeObject(java.io.ObjectOutputStream s)
199         throws java.io.IOException {
200         s.defaultWriteObject();
201 
202         // 写入比较器
203         s.writeObject(m.comparator());
204 
205         // 写入容量
206         s.writeInt(m.size());
207 
208         // 写入“TreeSet中的每一个元素”
209         for (Iterator i=m.keySet().iterator(); i.hasNext(); )
210             s.writeObject(i.next());
211     }
212 
213     // java.io.Serializable的读取函数:根据写入方式读出
214     // 先将TreeSet的“比较器、容量、所有的元素值”依次读出
215     private void readObject(java.io.ObjectInputStream s)
216         throws java.io.IOException, ClassNotFoundException {
217         // Read in any hidden stuff
218         s.defaultReadObject();
219 
220         // 从输入流中读取TreeSet的“比较器”
221         Comparator<? super E> c = (Comparator<? super E>) s.readObject();
222 
223         TreeMap<E,Object> tm;
224         if (c==null)
225             tm = new TreeMap<E,Object>();
226         else
227             tm = new TreeMap<E,Object>(c);
228         m = tm;
229 
230         // 从输入流中读取TreeSet的“容量”
231         int size = s.readInt();
232 
233         // 从输入流中读取TreeSet的“全部元素”
234         tm.readTreeSet(size, s, PRESENT);
235     }
236 
237     // TreeSet的序列版本号
238     private static final long serialVersionUID = -2479143000061671589L;
239 }

总结:
(01) TreeSet实际上是TreeMap实现的。当我们构造TreeSet时;若使用不带参数的构造函数,则TreeSet的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。
(02) TreeSet是非线程安全的。
(03) TreeSet实现java.io.Serializable的方式。当写入到输出流时,依次写入“比较器、容量、全部元素”;当读出输入流时,再依次读取。

第4部分 TreeSet遍历方式
4.1 Iterator顺序遍历

for(Iterator iter = set.iterator(); iter.hasNext(); ) { 
    iter.next();
}   

4.2 Iterator顺序遍历
// 假设set是TreeSet对象

for(Iterator iter = set.descendingIterator(); iter.hasNext(); ) { 
    iter.next();
}

4.3 for-each遍历HashSet
// 假设set是TreeSet对象,并且set中元素是String类型

String[] arr = (String[])set.toArray(new String[0]);
for (String str:arr)
    System.out.printf("for each : %s\n", str);

TreeSet不支持快速随机遍历,只能通过迭代器进行遍历!

TreeSet遍历测试程序如下:

 1 import java.util.*;
 2 
 3 /**
 4  * @desc TreeSet的遍历程序
 5  *
 6  * @author skywang
 7  * @email kuiwu-wang@163.com
 8  */
 9 public class TreeSetIteratorTest {
10 
11     public static void main(String[] args) {
12         TreeSet set = new TreeSet();
13         set.add("aaa");
14         set.add("aaa");
15         set.add("bbb");
16         set.add("eee");
17         set.add("ddd");
18         set.add("ccc");
19 
20         // 顺序遍历TreeSet
21         ascIteratorThroughIterator(set) ;
22         // 逆序遍历TreeSet
23         descIteratorThroughIterator(set);
24         // 通过for-each遍历TreeSet。不推荐!此方法需要先将Set转换为数组
25         foreachTreeSet(set);
26     }
27 
28     // 顺序遍历TreeSet
29     public static void ascIteratorThroughIterator(TreeSet set) {
30         System.out.print("\n ---- Ascend Iterator ----\n");
31         for(Iterator iter = set.iterator(); iter.hasNext(); ) {
32             System.out.printf("asc : %s\n", iter.next());
33         }
34     }
35 
36     // 逆序遍历TreeSet
37     public static void descIteratorThroughIterator(TreeSet set) {
38         System.out.printf("\n ---- Descend Iterator ----\n");
39         for(Iterator iter = set.descendingIterator(); iter.hasNext(); )
40             System.out.printf("desc : %s\n", (String)iter.next());
41     }
42 
43     // 通过for-each遍历TreeSet。不推荐!此方法需要先将Set转换为数组
44     private static void foreachTreeSet(TreeSet set) {
45         System.out.printf("\n ---- For-each ----\n");
46         String[] arr = (String[])set.toArray(new String[0]);
47         for (String str:arr)
48             System.out.printf("for each : %s\n", str);
49     }
50 }

运行结果:

---- Ascend Iterator ----
asc : aaa
asc : bbb
asc : ccc
asc : ddd
asc : eee

---- Descend Iterator ----
desc : eee
desc : ddd
desc : ccc
desc : bbb
desc : aaa

---- For-each ----
for each : aaa
for each : bbb
for each : ccc
for each : ddd
for each : eee

第5部分 TreeSet示例
下面通过实例学习如何使用TreeSet

 1 import java.util.*;
 2 
 3 /**
 4  * @desc TreeSet的API测试
 5  *
 6  * @author skywang
 7  * @email kuiwu-wang@163.com
 8  */
 9 public class TreeSetTest {
10 
11     public static void main(String[] args) {
12         testTreeSetAPIs();
13     }
14     
15     // 测试TreeSet的api
16     public static void testTreeSetAPIs() {
17         String val;
18 
19         // 新建TreeSet
20         TreeSet tSet = new TreeSet();
21         // 将元素添加到TreeSet中
22         tSet.add("aaa");
23         // Set中不允许重复元素,所以只会保存一个“aaa”
24         tSet.add("aaa");
25         tSet.add("bbb");
26         tSet.add("eee");
27         tSet.add("ddd");
28         tSet.add("ccc");
29         System.out.println("TreeSet:"+tSet);
30 
31         // 打印TreeSet的实际大小
32         System.out.printf("size : %d\n", tSet.size());
33 
34         // 导航方法
35         // floor(小于、等于)
36         System.out.printf("floor bbb: %s\n", tSet.floor("bbb"));
37         // lower(小于)
38         System.out.printf("lower bbb: %s\n", tSet.lower("bbb"));
39         // ceiling(大于、等于)
40         System.out.printf("ceiling bbb: %s\n", tSet.ceiling("bbb"));
41         System.out.printf("ceiling eee: %s\n", tSet.ceiling("eee"));
42         // ceiling(大于)
43         System.out.printf("higher bbb: %s\n", tSet.higher("bbb"));
44         // subSet()
45         System.out.printf("subSet(aaa, true, ccc, true): %s\n", tSet.subSet("aaa", true, "ccc", true));
46         System.out.printf("subSet(aaa, true, ccc, false): %s\n", tSet.subSet("aaa", true, "ccc", false));
47         System.out.printf("subSet(aaa, false, ccc, true): %s\n", tSet.subSet("aaa", false, "ccc", true));
48         System.out.printf("subSet(aaa, false, ccc, false): %s\n", tSet.subSet("aaa", false, "ccc", false));
49         // headSet()
50         System.out.printf("headSet(ccc, true): %s\n", tSet.headSet("ccc", true));
51         System.out.printf("headSet(ccc, false): %s\n", tSet.headSet("ccc", false));
52         // tailSet()
53         System.out.printf("tailSet(ccc, true): %s\n", tSet.tailSet("ccc", true));
54         System.out.printf("tailSet(ccc, false): %s\n", tSet.tailSet("ccc", false));
55 
56 
57         // 删除“ccc”
58         tSet.remove("ccc");
59         // 将Set转换为数组
60         String[] arr = (String[])tSet.toArray(new String[0]);
61         for (String str:arr)
62             System.out.printf("for each : %s\n", str);
63 
64         // 打印TreeSet
65         System.out.printf("TreeSet:%s\n", tSet);
66 
67         // 遍历TreeSet
68         for(Iterator iter = tSet.iterator(); iter.hasNext(); ) {
69             System.out.printf("iter : %s\n", iter.next());
70         }
71 
72         // 删除并返回第一个元素
73         val = (String)tSet.pollFirst();
74         System.out.printf("pollFirst=%s, set=%s\n", val, tSet);
75 
76         // 删除并返回最后一个元素
77         val = (String)tSet.pollLast();
78         System.out.printf("pollLast=%s, set=%s\n", val, tSet);
79 
80         // 清空HashSet
81         tSet.clear();
82 
83         // 输出HashSet是否为空
84         System.out.printf("%s\n", tSet.isEmpty()?"set is empty":"set is not empty");
85     }
86 }

运行结果:

TreeSet:[aaa, bbb, ccc, ddd, eee]
size : 5
floor bbb: bbb
lower bbb: aaa
ceiling bbb: bbb
ceiling eee: eee
higher bbb: ccc
subSet(aaa, true, ccc, true): [aaa, bbb, ccc]
subSet(aaa, true, ccc, false): [aaa, bbb]
subSet(aaa, false, ccc, true): [bbb, ccc]
subSet(aaa, false, ccc, false): [bbb]
headSet(ccc, true): [aaa, bbb, ccc]
headSet(ccc, false): [aaa, bbb]
tailSet(ccc, true): [ccc, ddd, eee]
tailSet(ccc, false): [ddd, eee]
for each : aaa
for each : bbb
for each : ddd
for each : eee
TreeSet:[aaa, bbb, ddd, eee]
iter : aaa
iter : bbb
iter : ddd
iter : eee
pollFirst=aaa, set=[bbb, ddd, eee]
pollLast=eee, set=[bbb, ddd]
set is empty

8.3.4 EnumSet类

EnumSet是一个专为枚举类设计的集合类,EnumSet中的所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。

EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是进行批量操作(如调用containsAll() 和retainAll()方法)时,如果其参数也是EnumSet集合,则该批量操作的执行速度也非常快。

EnumSet集合不允许加入null元素,如果试图插入null元素,EnumSet将抛出NullPointerException异常。如果只是想判断EnumSet是否包含null元素或试图删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。

EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的类方法来创建EnumSet对象。

EnumSet类它提供了如下常用的类方法来创建EnumSet对象。

➢ EnumSet allOf(Class elementType):创建一个包含指定枚举类里所有枚举值的EnumSet集合。

➢ EnumSet complementOf(EnumSet s):创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,新EnumSet集合包含原EnumSet集合所不包含的、此枚举类剩下的枚举值(即新EnumSet集合和原EnumSet集合的集合元素加起来就是该枚举类的所有枚举值)。

➢ EnumSet copyOf(Collection c):使用一个普通集合来创建EnumSet集合。

➢ EnumSet copyOf(EnumSet s):创建一个与指定EnumSet具有相同元素类型、相同集合元素的EnumSet集合。

➢ EnumSet noneOf(Class elementType):创建一个元素类型为指定枚举类型的空EnumSet。

➢ EnumSet of(E first, E…rest):创建一个包含一个或多个枚举值的EnumSet集合,传入的多个枚举值必须属于同一个枚举类。

➢ EnumSet range(E from, E to):创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。

示例

package com.collection;
 
import java.util.EnumSet;
 
public class EnumSetTest {
 
    public static void main(String[] args) {
        //1.创建一个包含Session(枚举类)里所有枚举值的EnumSet集合
        EnumSet e1 = EnumSet.allOf(Session.class);
        System.out.println(e1);//[SPRING, SUMMER, FAIL, WINTER]
 
        //2.创建一个空EnumSet
        EnumSet e2 = EnumSet.noneOf(Session.class);
        System.out.println(e2);//[]
 
        //3. add()空EnumSet集合中添加枚举元素
        e2.add(Session.SPRING);
        e2.add(Session.SUMMER);
        System.out.println(e2);//[SPRING, SUMMER]
 
        //4. 以指定枚举值创建EnumSet集合
        EnumSet e3 = EnumSet.of(Session.SPRING,Session.FAIL);
        System.out.println(e3);//[SPRING, FAIL]
 
        //5.创建一个包含从from枚举值到to枚举值范围内所有枚举值的EnumSet集合。
        EnumSet e4 = EnumSet.range(Session.SPRING,Session.FAIL);
        System.out.println(e4);//[SPRING, SUMMER, FAIL]
 
        //6.创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合,
        //  新EnumSet集合包含原EnumSet集合所不包含的枚举值
        EnumSet e5 = EnumSet.complementOf(e4);
        System.out.println(e5);//[WINTER]
    }
}
 
//创建一个枚举
enum Session{
    SPRING,
    SUMMER,
    FAIL,
    WINTER
}

除此之外还可以复制另一个EnumSet集合中的所有元素来创建新的EnumSet集合,或者复制另一个Collection集合中的所有元素来创建新的EnumSet集合。

Collection c = new HashSet();
        c.clear();
        c.add(Session.SPRING);
        c.add(Session.FAIL);
        EnumSet e6 = EnumSet.copyOf(c);
        System.out.println(e6);//[SPRING, FAIL]

8.3.5 各Set实现类的性能分析

HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet。

HashSet还有一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略微慢一点,这是由维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快。

EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。

必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。

如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collections工具类的synchronizedSortedSet方法来“包装”该Set集合。

8.4 List集合

List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。

8.4.1 改进的List接口和ListIterator接口

List作为Collection接口的子接口,当然可以使用Collection接口里的全部方法。而且由于List是有序集合,因此List集合里增加了一些根据索引来操作集合元素的方法。

➢ void add(int index, Object element):将元素element插入到List集合的index处。

➢ boolean addAll(int index, Collection c):将集合c所包含的所有元素都插入到List集合的index处。

➢ Object get(int index):返回集合index索引处的元素。

➢ int indexOf(Object o):返回对象o在List集合中第一次出现的位置索引。

➢ int lastIndexOf(Object o):返回对象o在List集合中最后一次出现的位置索引。

➢ Object remove(int index):删除并返回index索引处的元素。

➢ Object set(int index, Object element):将index索引处的元素替换成element对象,返回被替换的旧元素。

➢ List subList(int fromIndex, int toIndex):返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素组成的子集合。所有的List实现类都可以调用这些方法来操作集合元素。与Set集合相比,List增加了根据索引来插入、替换和删除集合元素的方法。除此之外,Java 8还为List接口添加了如下两个默认方法。

➢ void replaceAll(UnaryOperator operator):根据operator指定的计算规则重新设置List集合的所有元素。➢ void sort(Comparator c):根据Comparator参数对List集合的元素排序。

Java 8为List集合增加了sort()和replaceAll()两个常用的默认方法,其中sort()方法需要一个Comparator对象来控制元素排序,程序可使用Lambda表达式来作为参数;而replaceAll()方法则需要一个UnaryOperator来替换所有集合元素,UnaryOperator也是一个函数式接口,因此程序也可使用Lambda表达式作为参数。

与Set只提供了一个iterator()方法不同,List还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法。

ListIterator接口在Iterator接口基础上增加了如下方法。

➢ boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素。

➢ Object previous():返回该迭代器的上一个元素。

➢ void add(Object o):在指定位置插入一个元素。

注意:

  (1)Iterator只能单向移动。

  (2)Iterator.remove()是唯一安全的方式来在迭代过程中修改集合;如果在迭代过程中以任何其它的方式修改了基本集合将会产生未知的行为。而且每调用一次next()方法,remove()方法只能被调用一次,如果违反这个规则将抛出一个异常。

ListIterator是一个功能更加强大的, 它继承于Iterator接口,只能用于各种List类型的访问。可以通过调用listIterator()方法产生一个指向List开始处的ListIterator, 还可以调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。ListIterator接口定义如下:

   add():在迭代器中新添加元素  

   remove():删除迭代器新返回的元素。

   set():在迭代器中修改元素  

   hasNext():向后遍历如果迭代器中还有元素,则返回true。

   next():返回迭代器中的下一个元素

   hasPrevious():向前遍历如果迭代器中还有元素,则返回true。

   previous():返回迭代器中的下一个元素

   nextIndex():  当前元素后一个元素的索引 

   previousIndex():当前元素前一个元素的索引
public static void main(String[] args) {
    ArrayList<String> a = new ArrayList<String>();
    a.add("aaa");
    a.add("bbb");
    a.add("ccc");
    System.out.println("Before iterate : " + a);
    ListIterator<String> it = a.listIterator()
    while (it.hasNext()) {
        System.out.println(it.next() + ", " + it.previousIndex() + ", " +it.nextIndex());
    }
    while (it.hasPrevious()) {
        System.out.print(it.previous() + " ");
    }
    System.out.println();
    //调用listIterator(n)方法创建一个一开始就指向列表索引为n的元素处的ListIterator。
    it = a.listIterator(1);
    while (it.hasNext()) {
        String t = it.next();
        System.out.println(t);
        if ("ccc".equals(t)) {
            it.set("nnn");
        } else {
            it.add("kkk");
        }
    }
    System.out.println("After iterate : " + a);
}

解释:

   第1行:新建一个ArrayList,命名为a;

   第2行、第3行和第4行分别一次往ArrayList里添加了aaa,bbb,ccc;

   第5行:输出ArrayList里的值:aaa,bbb,ccc

   第6行:调用了a的listIterator方法,并使ListIterator类型的it指向,也就是说ListIterator类型的it指向了ArrayList容器, 通过调用ArrayList的listIterator方法来进行容器内的遍历。

   第7行、8、9行,调用it的hasNext()方法进行判断容器中是否还有元素,如果有,则输出元素,当前元素前一个元素的索引,当前元素后一个元素的索引,所以会输出:aaa,0,1    bbb,1,2 ccc,2,3

   第10行,此时,it已经指向了ArrayList的最后一个元素,在这里调用了ListIterator的hasPrevious()方法,就是,开始往前遍历(上面是往后遍历) 在这个while循环中,会以此输出:ccc bbb  aaa。

   第13行:输出换行。

   第14行:现在it应该已经再一次指向ArrayList的开头。在这一行中,it又被用到了,同样的用到了ArrayList的listIteror方法,这一次不同,而是it指向了listIteror的第二个元素,因为是1,第一个元素的索引是0,也就是说it指向了ArrayList里的bbb。bbb是开头的元素。

  第15行:再一次是调用了ListIterator的hasnext()方法,来判断ArrayList里是否还有元素。

  第16行:调用了it的next()方法,所谓next方法,是指找到剩下元素的第一个元素,也就是bbb,并把它赋值了String 的 t;

  第17行:输出bbb

  第18行:19、20,21行,如果bbb与ccc相等则将bbb set成nnn,否则,add()来添加kkk,那么在哪里添加呢,是在next方法返回的元素之前,next方法返回的元素是ccc,也就是在bbb,和ccc之间添加kkk。现在容器中有aaa、bbb、kkk以及ccc。返回到第15行,再次以此往下执行,会进行if判断,然后把ccc设置nnn。

  第24行,最后输出ArrayList里的元素:aaa、bbb、kkk、nnn。

三,lterator和Listlterator的区别

 (1)ListIterator有add()方法,可以向List中添加对象,而Iterator不能

 (2)ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

 (3)ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。(4)都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。

   因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。

8.4.2 ArrayList和Vector实现类

ArrayList和Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能。

ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超出了该数组的长度时,它们的initialCapacity会自动增加。对于通常的编程场景,程序员无须关心ArrayList或Vector的initialCapacity。但如果向ArrayList或Vector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)方法一次性地增加initialCapacity。这可以减少重分配的次数,从而提高性能。如果开始就知道ArrayList或Vector集合需要保存多少个元素,则可以在创建它们时就指定initialCapacity大小。如果创建空的ArrayList或Vector集合时不指定initialCapacity参数,则Object[]数组的长度默认为10。

除此之外,ArrayList和Vector还提供了如下两个方法来重新分配Object[]数组。

➢ void ensureCapacity(int minCapacity):将ArrayList或Vector集合的Object[]数组长度增加大于或等于minCapacity值。

➢ void trimToSize():调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数。调用该方法可减少ArrayList或Vector集合对象占用的存储空间。

ArrayList和Vector在用法上几乎完全相同,ArrayList和Vector的显著区别是:

ArrayList是线程不安全的,当多个线程访问同一个ArrayList集合时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性;但Vector集合则是线程安全的,无须程序保证该集合的同步性。因为Vector是线程安全的,所以Vector的性能比ArrayList的性能要低。实际上,即使需要保证List集合线程安全,也同样不推荐使用Vector实现类。后面会介绍一个Collections工具类,它可以将一个ArrayList变成线程安全的。

Vector还提供了一个Stack子类,它用于模拟“栈”这种数据结构,“栈”通常是指“后进先出”(LIFO)的容器。最后“push”进栈的元素,将最先被“pop”出栈。

Stack类里提供了如下几个方法。

➢ Object peek():返回“栈”的第一个元素,但并不将该元素“pop”出栈。

➢ Object pop():返回“栈”的第一个元素,并将该元素“pop”出栈。

➢ void push(Object item):将一个元素“push”进栈,最后一个进“栈”的元素总是位于“栈”顶。需要指出的是,由于Stack继承了Vector,因此它也是一个非常古老的Java集合类,它同样是线程安全的、性能较差的,因此应该尽量少用Stack类。如果程序需要使用“栈”这种数据结构,建议使用后面将要介绍的ArrayDeque代替它。

集合
List接口: 有序的、不唯一
ArrayList:
特点: 有序、不唯一

数据结构: Object数组

ArrayList:包装类

作用一:ArrayList是基于Object[]实现的,所以该只能装引用数据类型,基本数据类型要想装进集合,需要将基本数据类型进行类的包装。

作用二:包装类中有将String类型转换为对应的基本数据类型的方法。

ArrayList的基本用法和特点

特点:
    有序的:按照添加的顺序
    不唯一:同一个元素可以装多次
1:如何创建泛型对象
ArrayList<泛型> list=new ArrayList<>();

2:如何添加元素:
一次添加一个元素:
list.add(元素);
一次添加多个元素:
Collections.addAll(集合,元素,元素,...);
3:得到集合元素的个数
list.size();
4:得到某一个元素
list.get(下标);
5:如何判断集合里面是否出现指定元素
list.contains();
6:遍历
for+下标
for(int x=0;x<list.size();x++){
    //x->下标
    //list.get(元素);
}
foreache
for(集合的泛型 x :list){
    //x->元素
}
迭代器********(重点)
for(得到迭代器对象;判断迭代器上面是否还有下一个元素;){
				取出下一个元素
			  }

for(Iterator<泛型>car=list.iterator();car.hasNext;){
    car.next();->元素
}


ArrayList 如何删除元素:

list.remove(int 下标);
下标指向谁就删除谁,如果下标不存在就抛出异常。
清空集合:list.clear();
list.remove(元素->参照物);
指定元素进行删除
*:一个remove只能删除一个对象。


1:ArrayList类里面的remove(元素)的方法
		底层需要尊重equals比较机制

		当我们想要删除一个元素的时候 底层拿着这个元素
		和集合里面的每一个元素做equals比较

2:谁主张谁举证
		要被删除的对象会主动调用他自己类的equals方法
		和集合里面的每一个元素做比较 

3:当我们使用迭代器遍历集合的过程中 不允许对集合
		的整体进行添加、删除操作 否则出发CME异常

		如果想要在遍历的过程中进行删除
		只能使用迭代器的删除方法:car.remove();

4:构造方法:
		ArrayList list = new ArrayList(int 数组空间大小);
		ArrayList list = new ArrayList();//数组默认开辟10块空间

		集合会自动扩容:
			jdk6.0及之前	x * 3 / 2 + 1
							10 -> 16 -> 25....

			jdk7.0及之后	x + (x >> 1)
							10 -> 15 -> 22....

		在项目开发的时候 尽量避免扩容:
			1:创建一个更大的新的数组对象
			2:将老数组里面的元素复制到新数组里面
			3:改变引用指向
			4:回收老数组对象
			5:继续添加元素

		扩容:list.ensureCapacity(数组空间)
		缩容:list.trimToSize()

*****
Vector语法和ArrayList一模一样的
*********************
面试题:
ArrayListVector之间的区别?
1:同步特性不同
Vector同一时间允许一个线程进行访问,效率较低,但是不会发生并发错误。
ArrayList同一时间允许多个线程进行访问,效率高,但是可能会发生并发错误。
***********************
jdk5.0开始 集合的工具类[Collections]里面提供一个方法synchronizedList
	   可以将线程不安全的ArrayList集合变成线程安全的集合对象
	   于是Vector渐渐被淘汰了
2:扩容不同
ArrayList:分版本
		jdk6.0及之前	x * 3 / 2 + 1
		jdk7.0及之后	x + (x >> 1)

Vector:分构造方法
		Vector(10) -> 2倍扩容	10 -20 -40...
		Vector(10,3) -> 定长扩容 10 -13 -16...

3:出现的版本不同
	  Vector:since jdk1.0
	  ArrayList:since jdk1.2
 **************************************
 LinkedList语法和ArrayList一模一样
	面试题: 
    ArrayListLinkedList之间的区别?
     ArrayListLinkedList底层数据结构不同 导致优劣势不同 
     ArrayList:底层是基于数组实现的
    优点:随机访问 遍历查找效率高
    缺点:添加删除元素的时候效率低
     LinkedList:底层是基于链表实现的

     优点:添加删除元素的时候效率高。
     缺点:随机访问 遍历查找效率低[从下标0开始找起]
**************************
Stack: 用数组模拟栈结构


arrayList代码实例

import java.util.*;
public class Daycare{
   public static void main(String[] args){
	   //ArrayList  Collections.addAll(集合,元素,....)
	   //创建一个集合对象装名字
	   ArrayList<String> list=new ArrayList<>();
	   //一次添加一个元素的方式:添加:Andy   Lee
	   Collections.addAll(list,"Andy","Lee");
	   //统计集合里面有几个人的姓名
	   System.out.println("人的个数:"+list.size());
	   //打印第一个人的姓名
	   System.out.println("第一个人的名字:"+list.get(0));
	   //判断集合里面是否出现Lee的名字
	   System.out.println(list.contains("Lee"));
	   //使用两种不同的方式打印 所有以A开头的名字
	   for(String name:list){
		   if(name.charAt(0)=='A'){
			   System.out.println(name);
			   }
		   }
       for(String name:list){
		   if(name.startsWith("A")){
			   System.out.println(name);
			   }
		   }
      //用迭代器
      for(Iterator<String> car=list.iterator();car.hasNext();){
		  String name=car.next();
		  if(name.startsWith("A")){
			  System.out.println(name);
			  }

		  }
	   }
}

import java.util.*;
public class Daycare{
   public static void main(String[] args){
	   //老筐 里面装的水果 有些重复的
	   ArrayList<Integer> list=new ArrayList<>();
	   Collections.addAll(list,56,77,77,90,56,28);
	   ArrayList<Integer>list1=new ArrayList<>();
	   //将重复元素去除
	   for(Integer i:list){
		   if(!(list1.contains(i))){
		       list1.add(i);
		   }
		 }
		System.out.println(list1);
	   }
}

Vector是矢量队列,它继承了AbstractList,实现了List、 RandomAccess, Cloneable, java.io.Serializable接口。

Vector接口依赖图:
在这里插入图片描述
Vector继承了AbstractList,实现了List,它是一个队列,因此实现了相应的添加、删除、修改、遍历等功能。
Vector实现了RandomAccess接口,因此可以随机访问。
Vector实现了Cloneable,重载了clone()方法,因此可以进行克隆。
Vector实现了Serializable接口,因此可以进行序列化。
Vector的 操作是线程安全的。

Vector的数据结构和ArrayList差不多,包含了3个成员变量:elementData,elementCount,capacityIncrement。
(1)elementData是Object[]的数组,初始大小为10,会不断的增长。
(2)elementCount是元素的个数。
(3)capacityIncrement是动态数组增长的系数。

Vector有四种遍历方式:
(1)第一种通过迭代器遍历,即通过Iterator去遍历

Integer value=null;
Iterator iter=vector.iterator();
while(iter.hasNext())
{
    value=(Interger)iter.next();
}

2)第二种随机访问,通过索引进行遍历

Integer value=null;
int size=vector.size();
for(int i=0;i<size;i++)
{
value=vector.get(i);
}

(3)第三种通过for循环的方式

Integer value=null;
for( Integer inte: vector)
{
value=inte;
}

(4)第四种,Enumeration遍历

Integer value=null;
Enumeration enu=vector.elements();
while(enu.hasMoreElements())
{
value=(Integer)enu.nextElement();
}

Vector示例代码:

public class Hello {

public static void main(String[] args) {
        Vector vec = new Vector();
//添加
vec.add("1");
vec.add("2");
vec.add("3");
vec.add("4");
vec.add("5");
//替换
vec.set(0, "100");
vec.add(2, "300");

System.out.println("vec:"+vec);
System.out.println("vec.indexOf(100):"+vec.indexOf("100"));
System.out.println("vec.lastIndexOf(100):"+vec.lastIndexOf("100"));
System.out.println("vec.firstElement():"+vec.firstElement());
System.out.println("vec.elementAt(2):"+vec.elementAt(2));
System.out.println("vec.lastElement():"+vec.lastElement());
System.out.println("size:"+vec.size());
System.out.println("capacity:"+vec.capacity());
System.out.println("vec 2 to 4:"+vec.subList(1, 4));
Enumeration enu = vec.elements();
        while(enu.hasMoreElements())
        {
            System.out.println("nextElement():"+enu.nextElement());
            Vector retainVec = new Vector();
            retainVec.add("100");
            retainVec.add("300");
            System.out.println("vec.retain():"+vec.retainAll(retainVec));
            System.out.println("vec:"+vec);
            String[] arr = (String[]) vec.toArray(new String[0]);
            for (String str:arr)
                System.out.println("str:"+str);
            vec.clear();
            vec.removeAllElements();
            System.out.println("vec.isEmpty():"+vec.isEmpty());
        }
    }
}

Stack
如果我们去查jdk的文档,我们会发现stack是在Java.util这个包里。它对应的一个大致的类关系图如下:

在这里插入图片描述
通过继承Vector类,Stack类可以很容易的实现他本身的功能。因为大部分的功能在Vector里面已经提供支持了。

Stack里面主要实现的有一下几个方法:
在这里插入图片描述
因为前面我们已经提到过,通过继承Vector,很大一部分功能的实现就由Vector涵盖了。Vector的详细实现我们会在后面分析。它实现了很多的辅助方法,给Stack的实现带来很大的便利。现在,我们按照自己的思路来分析每个方法的具体步骤,再和具体实现代码对比。

empty
从我们的思路来说,如果要判断stack是否为空,就需要有一个变量来计算当前栈的长度,如果该变量为0,则表示该栈为空。或者说我们有一个指向栈顶的变量,如果它开始的时候是设置为空的,我们可以认为栈为空。这部分的实现代码也很简单:

Java代码 收藏代码

public boolean empty() {
return size() == 0;
}
如果更进一步分析的话,是因为Vector已经实现了size()方法。在Vector里面有一个变量elementCount来表示容器里元素的个数。如果为0,则表示容器空。这部分在Vector里面的实现如下:

Java代码 收藏代码

public synchronized int size() {
return elementCount;
}

peek
peek是指的返回栈顶端的元素,我们对栈本身不做任何的改动。如果栈里有元素的话,我们就返回最顶端的那个。而该元素的索引为栈的长度。如果栈为空的话,则要抛出异常:

Java代码 收藏代码

public synchronized E peek() {
int len = size();

if (len == 0)  
    throw new EmptyStackException();  
return elementAt(len - 1);  

}
这个elementAt方法也是Vector里面的一个实现。在Vector里面,实际上是用一个elementData的Object数组来存储元素的。所以要找到顶端的元素无非就是访问栈最上面的那个索引。它的详细实现如下:

Java代码 收藏代码

public synchronized E elementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
}

return elementData(index);  

}

@SuppressWarnings(“unchecked”)
E elementData(int index) {
return (E) elementData[index];
}
pop
pop方法就是将栈顶的元素弹出来,如果栈里有元素,就取最顶端的那个,否则就要抛出异常:

Java代码 收藏代码

public synchronized E pop() {
E obj;
int len = size();

obj = peek();  
removeElementAt(len - 1);  

return obj;  

}
在这里,判断是否可以取栈顶元素在peek方法里实现了,也将如果栈为空则抛异常的部分包含在peek方法里面。这里有必要注意的一个细节就是,在通过peek()取到顶端的元素之后,我们需要用removeElementAt()方法将最顶端的元素移除。我们平时可能不太会留意到这一点。为什么要移除呢?我们反正有一个elementCount来记录栈的长度,不管它不是也可以吗?

实际上,这么做在程序运行的时候会有一个潜在的内存泄露的问题。因为在java里面,如果我们普通定义的类型属于强引用类型。比如这里vector就底层用的Object[]这个数组强类型来保存数据。强类型在jvm中做gc的时候,只要程序中有引用到它,它是不会被回收的。这就意味着在这里,只要我们一直在用着stack,那么stack里面所有关联的元素就都别想释放了。这样运行时间一长就会导致内存泄露的问题。那么,为了解决这个问题,这里就是用的removeElementAt()方法。

Java代码 收藏代码

public synchronized void removeElementAt(int index) {
modCount++;
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " +
elementCount);
}
else if (index < 0) {
throw new ArrayIndexOutOfBoundsException(index);
}
int j = elementCount - index - 1;
if (j > 0) {
System.arraycopy(elementData, index + 1, elementData, index, j);
}
elementCount–;
elementData[elementCount] = null; /* to let gc do its work */
}
这个方法实现的思路也比较简单。就是用待删除元素的后面元素依次覆盖前面一个元素。这样,就相当于将数组的实际元素长度给缩短了。因为这里这个移除元素的方法是定义在vector中间,它所面对的是一个更加普遍的情况,我们移除的元素不一定就是数组尾部的,所以才需要从后面依次覆盖。如果只是单纯对于一个栈的实现来说,我们完全可以直接将要删除的元素置为null就可以了。

push
push的操作也比较直观。我们只要将要入栈的元素放到数组的末尾,再将数组长度加1就可以了。

Java代码 收藏代码

public E push(E item) {
addElement(item);

return item;  

}
这里,addElement方法将后面的细节都封装了起来。如果我们更加深入的去考虑这个问题的话,我们会发现几个需要考虑的点。

  1. 首先,数组不会是无穷大的 ,所以不可能无限制的让你添加元素下去。当我们数组长度到达一个最大值的时候,我们不能再添加了,就需要抛出异常来。

  2. 如果当前的数组已经满了,实际上需要扩展数组的长度。常见的手法就是新建一个当前数组长度两倍的数组,再将当前数组的元素给拷贝过去。

前面讨论的这两点,都让vector把这份心给操了。我们就本着八卦到底的精神看看它到底是怎么干的吧:

Java代码 收藏代码

public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}

private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
看到这部分代码的时候,我不由得暗暗叹了口气。真的是拔了萝卜带出泥。本来想看看stack的细节实现,结果这些细节把vector都深深的出卖了。在vector中间有几个计数的变量,elementCount表示里面元素的个数,elementData是保存元素的数组。所以一般情况下数组不一定是满的,会存在着elementCount <= elementData.length这样的情况。这也就是为什么ensureCapacityHelper方法里要判断一下当新增加一个元素导致元素的数量超过数组长度了,我们要做一番调整。这个大的调整就在grow方法里展现了。

grow方法和我们所描述的方法有点不一样。他不一样的一点在于我们可以用一个capacityIncrement来指示调整数组长度的时候到底增加多少。默认的情况下相当于数组长度翻倍,如果设置了这个变量就增加这个变量指定的这么多。

search
search这部分就相当于找到一个最靠近栈顶端的匹配元素,然后返回这个元素到栈顶的距离。

Java代码 收藏代码

public synchronized int search(Object o) {
int i = lastIndexOf(o);

if (i >= 0) {  
    return size() - i;  
}  
return -1;  

}
对应在vector里面的实现也相对容易理解:

Java代码 收藏代码

public synchronized int lastIndexOf(Object o) {
return lastIndexOf(o, elementCount-1);
}

public synchronized int lastIndexOf(Object o, int index) {
if (index >= elementCount)
throw new IndexOutOfBoundsException(index + " >= "+ elementCount);

if (o == null) {  
    for (int i = index; i >= 0; i--)  
        if (elementData[i]==null)  
            return i;  
} else {  
    for (int i = index; i >= 0; i--)  
        if (o.equals(elementData[i]))  
            return i;  
}  
return -1;  

}
这个lastIndexOf的实现无非是从数组的末端往前遍历,如果找到这个对象就返回。如果到头了,还找不到对象呢?…不好意思,谁让你找不到对象的?活该你光棍,那就返回个-1吧。

8.4.3 固定长度的List

前面讲数组时介绍了一个操作数组的工具类:Arrays,该工具类里提供了asList(Object…a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例。Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素。

public static void main(String[] args) {
        //创建一个字符串数组
        String[] words = "And that is how we know the Earth to be banana-shaped".split(" ");
        //将该数组通过Arrays.asList方法,通过ArrayList的构造器传递给该list
        List<String> list = Arrays.asList(words);
        
        System.out.println("list改变前数组的第一个元素 : " + words[0]);
        System.out.println("list改变前的list" + list);
        
        /*
        *  改变 list 会影响原数组
        * */
        //将list中的元素换位
        String temp = list.get(0);
        list.set(0,list.get(1));
        list.set(1,temp);
        System.out.println("list改变后的list" + list);
        System.out.println("list改变后数组的第一个元素 : " + words[0]);
    }

输出结果:

list改变前数组的第一个元素 : And
list改变前的list[And, that, is, how, we, know, the, Earth, to, be, banana-shaped]
list改变后的list[that, And, is, how, we, know, the, Earth, to, be, banana-shaped]
list改变后数组的第一个元素 : that

原因:
asList()方法生成一个ArrayList,但是需要注意的是,这个ArrayList并不是ArrayList类,而是Arrays类里面的嵌套类Arrays.ArrayList类,该嵌套类的信息类标签和构造器如下:

private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable

private final E[] a;

ArrayList(E[] array) {
    a = Objects.requireNonNull(array);
}

可见Arrays.ArrayList有一个数组成员 E[] a。

只有这个Arrays.ArrayList的构造器能够接受数组作为构造器参数,ArrayList的构造器是只能接受Collection的。
Arrays.ArrayList的构造器直接让它的数组成员a指向了传进来的array。

也就是说,我们后面玩的ArrayList其实是Arrays.ArrayList,他是没有add()方法的,并且修改元素也是通过修改之前传递进去的固定长度数组来实现,这就是为什么修改它的元素会直接影响传进来的数组。

这个嵌套类虽然也是继承自AbstractList,但是它没实现add()方法,用的还是AbstractList方法,因此一旦调用add()方法会报错UnSupportedOperationException。

测试代码2
ArrayList中也有一个成员数组,对ArrayList的操作是通过对该数组的操作来实现的

transient Object[] elementData; 

但是如果将Arrays.asList(String…)方法用Collections.addAll(Collection,String…)代替,
对list的操作不会影响原数组,因为这个方法是把原数组的数据拷贝过来,而不是直接拷贝引用。

public static void main(String[] args) {
            //创建一个字符串数组
            String[] words = "And that is how we know the Earth to be banana-shaped".split(" ");
            //将该数组通过Arrays.asList方法,通过ArrayList的构造器传递给该list
            List<String> list = new ArrayList<>();
            Collections.addAll(list,words);
            
            System.out.println("list改变前数组的第一个元素 : " + words[0]);
            System.out.println("list改变前的list" + list);
            
            /*
            *  改变 list 不会影响原数组
            * */
            //将list中的元素换位
            String temp = list.get(0);
            list.set(0,list.get(1));
            list.set(1,temp);
            System.out.println("list改变后的list" + list);
            System.out.println("list改变后数组的第一个元素 : " + words[0]);
        }

输出结果:

list改变前数组的第一个元素 : And
list改变前的list[And, that, is, how, we, know, the, Earth, to, be, banana-shaped]
list改变后的list[that, And, is, how, we, know, the, Earth, to, be, banana-shaped]
list改变后数组的第一个元素 : And

总结
ArrayList是一个单独的类,Arrays.ArrayList是Arrays的一个嵌套类。前者的对象是我们常见的长度可变的容器,而后者是Arrays类利用asList()方法产生的一个长度不可变的容器 ( 因为没实现add()等方法 ),两者的实现是不同的。

8.5 Queue集合

Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。

队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。

Queue接口中定义了如下几个方法。

➢ void add(Object e):将指定元素加入此队列的尾部。

➢ Object element():获取队列头部的元素,但是不删除该元素。

➢ boolean offer(Object e):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。

➢ Object peek():获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null。

➢ Object poll():获取队列头部的元素,并删除该元素。如果此队列为空,则返回null。

➢ Object remove():获取队列头部的元素,并删除该元素。

Queue接口有一个PriorityQueue实现类。除此之外,Queue还有一个Deque接口,Deque代表一个“双端队列”,双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可当成队列使用,也可当成栈使用。Java为Deque提供了ArrayDeque和LinkedList两个实现类。

8.5.1 PriorityQueue实现类

PriorityQueue是一个比较标准的队列实现类。PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序。因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素。从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出(FIFO)。

PriorityQueue不允许插入null元素,它还需要对队列元素进行排序,PriorityQueue的元素有两种排序方式。

➢ 自然排序:采用自然顺序的PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。

➢ 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。PriorityQueue队列对元素的要求与TreeSet对元素的要求基本一致。
在堆排序这篇文章中千辛万苦的实现了堆的结构和排序,其实在Java 1.5版本后就提供了一个具备了小根堆性质的数据结构也就是优先队列PriorityQueue。下面详细了解一下PriorityQueue到底是如何实现小顶堆的,然后利用PriorityQueue实现大顶堆。

PriorityQueue的数据结构
在这里插入图片描述
PriorityQueue的逻辑结构是一棵完全二叉树,存储结构其实是一个数组。逻辑结构层次遍历的结果刚好是一个数组。

PriorityQueue的操作
①add(E e) 和 offer(E e) 方法

add(E e) 和 offer(E e) 方法都是向PriorityQueue中加入一个元素,其中add()其实调用了offer()方法如下:

public boolean add(E e) {
        return offer(e);
    }

下面主要看看offer()方法的作用:
在这里插入图片描述
如上图调用 offer(4)方法后,往堆中压入4然后从下往上调整堆为小顶堆。offer()的代码实现:

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
      //如果压入的元素为null 抛出异常      
        int i = size;
        if (i >= queue.length)
            grow(i + 1);
            //如果数组的大小不够扩充
        size = i + 1;
        if (i == 0)
            queue[0] = e;
            //如果只有一个元素之间放在堆顶
        else
            siftUp(i, e);
            //否则调用siftUp函数从下往上调整堆。
        return true;
    }

对上面代码做几点说明:
①优先队列中不能存放空元素。
②压入元素后如果数组的大小不够会进行扩充,上面的queue其实就是一个默认初始值为11的数组(也可以赋初始值)。
③offer元素的主要调整逻辑在 siftUp ( i, e )函数中。下面看看 siftUp(i, e) 函数到底是怎样实现的。

private void siftUpComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = queue[parent];
            if (key.compareTo((E) e) >= 0)
                break;
            queue[k] = e;
            k = parent;
        }
        queue[k] = key;
    }

上面的代码还是比较简明的,就是当前元素与父节点不断比较如果比父节点小就交换然后继续向上比较,否则停止比较的过程。

② poll() 和 remove() 方法
poll 方法每次从 PriorityQueue 的头部删除一个节点,也就是从小顶堆的堆顶删除一个节点,而remove()不仅可以删除头节点而且还可以用 remove(Object o) 来删除堆中的与给定对象相同的最先出现的对象。先看看poll()方法。下面是poll()之后堆的操作

删除元素后要对堆进行调整:
在这里插入图片描述
堆中每次删除只能删除头节点。也就是数组中的第一个节点
在这里插入图片描述
将最后一个节点替代头节点然后进行调整。

在这里插入图片描述
如果左右节点中的最小节点比当前节点小就与左右节点的最小节点交换。直到当前节点无子节点,或者当前节点比左右节点小时停止交换。

poll()方法的源码

public E poll() {
        if (size == 0)
            return null;
      //如果堆大小为0则返回null      
        int s = --size;
        modCount++;
        E result = (E) queue[0];
        E x = (E) queue[s];
        queue[s] = null;
//如果堆中只有一个元素直接删除        
        if (s != 0)
            siftDown(0, x);
//否则删除元素后对堆进行调整            
        return result;
    }

看看 siftDown(0, x) 方法的源码:

private void siftDownComparable(int k, E x) {
        Comparable<? super E> key = (Comparable<? super E>)x;
        int half = size >>> 1;        // loop while a non-leaf
        while (k < half) {
            int child = (k << 1) + 1; // assume left child is least
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0)
                c = queue[child = right];
            if (key.compareTo((E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = key;
    }

siftDown()方法就是从堆的第一个元素往下比较,如果比左右孩子节点的最小值小则与最小值交换,交换后继续向下比较,否则停止比较。
remove(4)的过程图:
在这里插入图片描述
先用堆的最后一个元素 5 代替4然后从5开始向下调整堆。这个过程和poll()函数一样,只不过poll()函数每次都是从堆顶开始。
remove(Object o)的代码:

 public boolean remove(Object o) {
        int i = indexOf(o);
        //先在堆中找到o的位置
        if (i == -1)
            return false;
        //如果不存在则返回false。    
        else {
            removeAt(i);
            //否则删除数组中第i个位置的值,调整堆。
            return true;
        }
    }

removeAt(int i)的代码

 private E removeAt(int i) {
        assert i >= 0 && i < size;
        modCount++;
        int s = --size;
        if (s == i) // removed last element
            queue[i] = null;
        else {
            E moved = (E) queue[s];
            queue[s] = null;
            siftDown(i, moved);
            if (queue[i] == moved) {
                siftUp(i, moved);
                if (queue[i] != moved)
                    return moved;
            }
        }
        return null;
    }

使用PriorityQueue实现大顶堆
PriorityQueue默认是一个小顶堆,然而可以通过传入自定义的Comparator函数来实现大顶堆。如下代码:

 private static final int DEFAULT_INITIAL_CAPACITY = 11;
PriorityQueue<Integer> maxHeap=new PriorityQueue<Integer>(DEFAULT_INITIAL_CAPACITY, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {                
            return o2-o1;
        }
    });

8.5.2 Deque接口与ArrayDeque实现类

Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。

➢ void addFirst(Object e):将指定元素插入该双端队列的开头。

➢ void addLast(Object e):将指定元素插入该双端队列的末尾。

➢ Iterator descendingIterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。

➢ Object getFirst():获取但不删除双端队列的第一个元素。

➢ Object getLast():获取但不删除双端队列的最后一个元素。

➢ boolean offerFirst(Object e):将指定元素插入该双端队列的开头。

➢ boolean offerLast(Object e):将指定元素插入该双端队列的末尾。

➢ Object peekFirst():获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null。

➢ Object peekLast():获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。

➢ Object pollFirst():获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null。

➢ Object pollLast():获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。

➢ Object pop()(栈方法):pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。

➢ void push(Object e)(栈方法):将一个元素push进该双端队列所表示的栈的栈顶。相当于addFirst(e)。

➢ Object removeFirst():获取并删除该双端队列的第一个元素。

➢ Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素o。

➢ Object removeLast():获取并删除该双端队列的最后一个元素。

➢ boolean removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素o。从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。

从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。

Deque的方法与Queue的方法对照表如表8.2所示。
在这里插入图片描述

Deque的方法与Stack的方法对照表如表8.3所示。
在这里插入图片描述

Deque接口提供了一个典型的实现类:ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建Deque时同样可指定一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16。

在这里插入图片描述
在这里插入图片描述

8.5.3 LinkedList实现类

LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成“栈”来使用,也可以当成队列使用。

ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色(只需改变指针所指的地址即可)。需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能(而且实现机制也不好),所以各方面性能都比较差。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.5.4 各种线性表的性能分析

Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。

Queue代表了队列,Deque代表了双端队列(既可作为队列使用,也可作为栈使用),接下来对各种实现类的性能进行分析。初学者可以无须理会ArrayList和LinkedList之间的性能差异,只需要知道LinkedList集合不仅提供了List的功能,还提供了双端队列、栈的功能就行。一般来说,由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。但总体来说,ArrayList的性能比LinkedList的性能要好,因此大部分时候都应该考虑使用ArrayList。

关于使用List集合有如下建议。

➢ 如果需要遍历List集合元素,对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素。

➢ 如果需要经常执行插入、删除操作来改变包含大量数据的List集合的大小,可考虑使用LinkedList集合。使用ArrayList、Vector集合可能需要经常重新分配内部数组的大小,效果可能较差。

➢ 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合。

8.6 增强的Map集合

Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。key和value之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value。从Map中取出数据时,只要给出指定的key,就可以取出对应的value。
在这里插入图片描述
在这里插入图片描述

8.6.1 Java 8为Map新增的方法

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

Object compute(Object key, BiFunction remappingFunction)、 Object computeIfAbsent(Object key, Function mappingFunction)、 Object computeIfPresent(Object key, BiFunction remappingFunction)、➢ void forEach(BiConsumer action)、➢ Object getOrDefault(Object key, V defaultValue)等。

8.6.2 改进的HashMap和Hashtable实现类

Java 8改进了HashMap的实现,使用HashMap存在key冲突时依然具有较好的性能。

此外,Hashtable和HashMap存在两点典型区别。

➢ Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。

➢ Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap可以使用null作为key或value。由于HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的key为null,但可以有无数多个key-value对的value为null。下面程序示范了用null值作为HashMap的key和value的情形。
在这里插入图片描述
在这里插入图片描述

8.6.3 LinkedHashMap实现类

HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类;LinkedHashMap也使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。

8.6.4 使用Properties读写属性文件

Properties类是Hashtable类的子类,正如它的名字所暗示的,该对象在处理属性文件时特别方便(Windows操作平台上的ini文件就是一种属性文件)。

Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型。该类提供了如下三个方法来修改Properties里的key、value值。

➢ String getProperty(String key):获取Properties中指定属性名对应的属性值,类似于Map的get(Object key)方法。

➢ String getProperty(String key, String defaultValue):该方法与前一个方法基本相似。该方法多一个功能,如果Properties中不存在指定的key时,则该方法指定默认值。

➢ Object setProperty(String key, String value):设置属性值,类似于Hashtable的put()方法。

除此之外,它还提供了两个读写属性文件的方法。

➢ void load(InputStream inStream):从属性文件(以输入流表示)中加载key-value对,把加载到的key-value对追加到Properties里(Properties是Hashtable的子类,它不保证key-value对之间的次序)。

➢ void store(OutputStream out, String comments):将Properties中的key-value对输出到指定的属性文件(以输出流表示)中。

在这里插入图片描述

8.6.5 SortedMap接口和TreeMap实现类

正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类。TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。

TreeMap也有两种排序方式。

➢ 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException异常。

➢ 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序。采用定制排序时不要求Map的key实现Comparable接口。类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认为这两个key是相等的。

如果使用自定义类作为TreeMap的key,且想让TreeMap良好地工作,则重写该类的equals()方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,它们通过compareTo()方法比较应该返回0。如果equals()方法与compareTo()方法的返回结果不一致,TreeMap与Map接口的规则就会冲突。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.6.6 WeakHashMap实现类

WeakHashMap与HashMap的用法基本相似。与HashMap的区别在于,HashMap的key保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap的所有key所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些key所对应的key-value对;但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,WeakHashMap也可能自动删除这些key所对应的key-value对。WeakHashMap中的每个key对象只持有对实际对象的弱引用,因此,当垃圾回收了该key所对应的实际对象之后,WeakHashMap会自动删除该key对应的key-value对。

8.6.7 IdentityHashMap实现类

在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等;对于普通的HashMap而言,只要key1和key2通过equals()方法比较返回true,且它们的hashCode值相等即可。

IdentityHashMap提供了与HashMap基本相似的方法,也允许使用null作为key和value。与HashMap相似:IdentityHashMap也不保证key-value对之间的顺序,更不能保证它们的顺序随时间的推移保持不变。

8.6.8 EnumMap实现类

EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。

EnumMap具有如下特征。

➢ EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑、高效。

➢ EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。当程序通过keySet()、entrySet()、values()等方法遍历EnumMap时可以看到这种顺序。

➢ EnumMap不允许使用null作为key,但允许使用null作为value。如果试图使用null作为key时将抛出NullPointerException异常。如果只是查询是否包含值为null的key,或只是删除值为null的key,都不会抛出异常。与创建普通的Map有所区别的是,创建EnumMap时必须指定一个枚举类,从而将该EnumMap和指定枚举类关联起来。

8.6.9 各Map实现类的性能分析

对于Map的常用实现类而言,虽然HashMap和Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashMap通常比Hashtable要快。

TreeMap通常比HashMap、Hashtable要慢(尤其在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。

LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value时的添加顺序。

IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用==而不是equals()方法来判断元素相等。

EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key。

8.7 HashSet和HashMap的性能选项

对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小。

hash表里可以存储元素的位置被称为“桶(bucket)”,在通常情况下,单个“桶”里存储一个元素,此时有最好的性能:hash算法可以根据hashCode值计算出“桶”的存储位置,接着从“桶”中取出元素。但hash表的状态是open的:在发生“hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。如图8.8所示是hash表保存各元素,且发生“hash冲突”的示意图。

因为HashSet和HashMap、Hashtable都使用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSet、HashMap的hash表包含如下属性。

➢ 容量(capacity):hash表中桶的数量。

➢ 初始化容量(initial capacity):创建hash表时桶的数量。HashMap和HashSet都允许在构造器中指定初始化容量。

➢ 尺寸(size):当前hash表中记录的数量。

➢ 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的hash表,依此类推。轻负载的hash表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)。除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。

HashSet和HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和HashMap、Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。

“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销。

程序员可以根据实际情况来调整HashSet和HashMap的“负载极限”值。如果开始就知道HashSet和HashMap、Hashtable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSet和HashMap、Hashtable所包含的最大记录数除以“负载极限”,就不会发生rehashing。使用足够大的初始化容量创建HashSet和HashMap、Hashtable时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得过高。

8.8 操作集合的工具类:Collections

Java提供了一个操作Set、List和Map等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序、查询和修改等操作,还提供了将集合对象设置为不可变、对集合对象实现同步控制等方法。

8.8.1 排序操作Collections

提供了如下常用的类方法用于对List集合元素进行排序。

➢ void reverse(List list):反转指定List集合中元素的顺序。

➢ void shuffle(List list):对List集合元素进行随机排序(shuffle方法模拟了“洗牌”动作)。

➢ void sort(List list):根据元素的自然顺序对指定List集合的元素按升序进行排序。

➢ void sort(List list, Comparator c):根据指定Comparator产生的顺序对List集合元素进行排序。

➢ void swap(List list, int i, int j):将指定List集合中的i处元素和j处元素进行交换。

➢ void rotate(List list, int distance):当distance为正数时,将list集合的后distance个元素“整体”移到前面;当distance为负数时,将list集合的前distance个元素“整体”移到后面。该方法不会改变集合的长度。

8.8.2 查找、替换操作

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

➢ int binarySearch(List list, Object key):使用二分搜索法搜索指定的List集合,以获得指定对象在List集合中的索引。如果要使该方法可以正常工作,则必须保证List中的元素已经处于有序状态。

➢ Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素。

➢ Object max(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最大元素。

➢ Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素。

➢ Object min(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最小元素。

➢ void fill(List list, Object obj):使用指定元素obj替换指定List集合中的所有元素。

➢ int frequency(Collection c, Object o):返回指定集合中指定元素的出现次数。

➢ int indexOfSubList(List source, List target):返回子List对象在父List对象中第一次出现的位置索引;如果父List中没有出现这样的子List,则返回-1。

➢ int lastIndexOfSubList(List source, List target):返回子List对象在父List对象中最后一次出现的位置索引;如果父List中没有出现这样的子List,则返回-1。

➢ boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值newVal替换List对象的所有旧值oldVal。

8.8.3 同步控制

Collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。Collections提供了多个类方法可以把它们包装成线程同步的集合。

8.8.4 设置不可变集合

Collections提供了如下三类方法来返回一个不可变的集合。

➢ emptyXxx():返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是SortedSet、Set,还可以是Map、SortedMap等。

➢ singletonXxx():返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象,此处的集合既可以是List,还可以是Map。

➢ unmodifiableXxx():返回指定集合对象的不可变视图,此处的集合既可以是List,也可以是Set、SortedSet,还可以是Map、SorteMap等。上面三类方法的参数是原有的集合对象,返回值是该集合的“只读”版本。通过Collections提供的三类方法,可以生成“只读”的Collection或Map。

8.8.5 Java 9新增的不可变集合

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

8.9 烦琐的接口:Enumeration
Enumeration接口是Iterator迭代器的“古老版本”,从JDK 1.0开始,Enumeration接口就已经存在了(Iterator从JDK 1.2才出现)。

Enumeration接口只有两个名字很长的方法。

➢ boolean hasMoreElements():如果此迭代器还有剩下的元素,则返回true。

➢ Object nextElement():返回该迭代器的下一个元素,如果还有的话(否则抛出异常)。通过这两个方法不难发现,Enumeration接口中的方法名称冗长,难以记忆,而且没有提供Iterator的remove()方法。

如果现在编写Java程序,应该尽量采用Iterator迭代器,而不是用Enumeration迭代器。
在这里插入图片描述
在这里插入图片描述

第9章 泛型

9.1 泛型入门

从Java 5以后,Java引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时指定集合元素的类型,如List,这表明该List只能保存字符串类型的对象。Java的参数化类型被称为泛型(Generic)。

在Java 7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型了。例如如下两条语句:
在这里插入图片描述

从Java 7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即上面两条语句可以改写为如下形式:
在这里插入图片描述

把两个尖括号并排放在一起非常像一个菱形,这种语法也就被称为“菱形”语法。

需要说明的是,当使用var声明变量时,编译器无法推断泛型的类型。因此,若使用var声明变量,程序无法使用“菱形”语法。

Java 9再次增强了“菱形”语法,它甚至允许在创建匿名内部类时使用菱形语法,Java可根据上下文来推断匿名内部类中泛型的类型。

9.2 深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。

Java 5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参,这就是在前面程序中看到的List和ArrayList两种类型。

9.2.1 定义泛型接口、类

在这里插入图片描述

注意:当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为Apple类定义构造器,其构造器名依然是Apple,而不是Apple!调用该构造器时却可以使用Apple的形式,当然应该为T形参传入实际的类型参数。Java 7提供了“菱形”语法,允许省略<>中的类型实参。

9.2.2 从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含泛型形参。例如,下面代码就是错误的:
在这里插入图片描述

如果想从Apple类派生一个子类,则可以改为如下代码:

在这里插入图片描述
在这里插入图片描述

像这种使用Apple类时省略泛型的形式被称为原始类型(raw type)。

9.2.3 并不存在泛型类

看下面代码的打印结果是什么?
在这里插入图片描述

运行上面的代码片段,可能有读者认为应该输出false,但实际输出true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。

不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量(它们都是类相关的)的声明和初始化中不允许使用泛型形参。

由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。例如,下面代码是错误的。
在这里插入图片描述

9.3 类型通配符

问号(?)被称为通配符,它的元素类型可以匹配任何类型。

在这里插入图片描述

现在使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object。

但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中。因为程序无法确定c集合中元素的类型,所以不能向其中添加对象。例如,如下代码将会引起编译错误。
在这里插入图片描述

9.3.2 设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类。

为了表示List集合的所有元素是Shape的子类,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:
在这里插入图片描述

List<? extends Shape>是受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound)。
在这里插入图片描述

使用普通通配符相似的是,shapes.add()的第二个参数类型是?extends Shape,它表示Shape未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型或其子类),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。

对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。比如Foo是Bar的子类,这样A就相当于A<? extends Bar>的子类,可以将A赋值给A<? extends Bar>类型的变量,这种型变方式被称为协变。

对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进!

9.3.3 设定类型通配符的下限

除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。指定通配符的下限就是为了支持类型型变。

比如Foo是Bar的子类,当程序需要一个A<? super Foo>变量时,程序可以将A、A赋值给A<? super Foo>类型的变量,这种型变方式被称为逆变。

对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。对于逆变的泛型而言,它只能调用泛型类型作为参数的方法;而不能调用泛型类型作为返回值类型的方法。口诀是:逆变只进不出!

假设自己实现一个工具方法:实现将src集合中的元素复制到dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。

9.3.4 设定泛型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

在这里插入图片描述

在一种更极端的情况下,程序需要为泛型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该泛型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。如下代码所示。
在这里插入图片描述

与类同时继承父类、实现接口类似的是,为泛型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为泛型形参指定类上限,类上限必须位于第一位。

9.4 泛型方法

前面介绍了在定义类、接口时可以使用泛型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普通类型来用。在另外一些情况下,定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,这也是可以的,Java 5还提供了对泛型方法的支持。

9.4.1 定义泛型方法

泛型方法,就是在声明方法时定义一个或多个泛型形参。泛型方法的语法格式如下:

在这里插入图片描述
在这里插入图片描述

9.4.2 泛型方法和类型通配符的区别

泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的泛型形参必须在对应方法中显式声明。

9.4.3 “菱形”语法与泛型构造器

正如泛型方法允许在方法签名中声明泛型形参一样,Java也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。

一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。
在这里插入图片描述

前面介绍过Java 7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。如下程序所示。
在这里插入图片描述

9.4.4 泛型方法与方法重载

9.4.5 类型推断

Java 8改进了泛型方法的类型推断能力,类型推断主要有如下两方面。➢ 可通过调用方法的上下文来推断泛型的目标类型。➢ 可在方法调用链中,将推断得到的泛型传递到最后一个方法。

9.5 擦除和转换

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。

当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。

9.6 泛型与数组

Java泛型有一个很重要的设计原则—如果一段代码在编译时没有提出“[unchecked] 未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,所以数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组。也就是说,只能声明List[]形式的数组,但不能创建ArrayList[10]这样的数组对象。
在这里插入图片描述
在这里插入图片描述

第10章 异常处理

Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字,其中try关键字后紧跟一个花括号扩起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常;而throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象。

Java 7进一步增强了异常处理机制的功能,包括带资源的try语句、捕获多异常的catch两个新功能,这两个功能可以极好地简化异常处理。

Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。

10.1 异常概述

10.2 异常处理机制

10.2.1 使用try…catch捕获异常

Java异常处理机制的语法结构:
在这里插入图片描述

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。

当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

10.2.2 异常类的继承体系

Java提供了丰富的异常类,这些异常类之间有严格的继承关系,图10.2显示了Java常见的异常类之间的继承关系,Java把所有的非正常情况分成两种:异常(Exception)和错误(Error),它们都继承Throwable父类。

Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义该方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

注意:捕获异常时,一定要记住先捕获小异常,再捕获大异常。

10.2.3 多异常捕获

Java 7开始,一个catch块可以捕获多种类型的异常。使用一个catch块捕获多种类型的异常时需要注意如下两个地方。

➢ 捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开。

➢ 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。

10.2.4 访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。

所有的异常对象都包含了如下几个常用方法。

➢ getMessage():返回该异常的详细描述字符串。

➢ printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。

➢ printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。

➢ getStackTrace():返回该异常的跟踪栈信息。

10.2.5 使用finally回收资源

有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。

提示:Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。完整的Java异常处理语法结构如下:

注意:除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现怎样的情况,异常处理的finally块总会被执行。

在通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,(throw语句将在后面介绍),一旦在finally块中使用了return或throw语句,将会导致try块、catch块中的return、throw语句失效。

10.2.6 异常处理的嵌套

10.2.7 Java 9增强的自动关闭资源的try语句

Java 7增强了try语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。

需要指出的是,为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法。

提示:Java 7几乎把所有的“资源类”(包括文件IO的各种类、JDBC编程的Connection、Statement等接口)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。

Java 9再次增强了这种try语句,Java 9不要求在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或者是有效的final(effectively final),Java 9允许将资源变量放在try后的圆括号内。

10.3 Checked异常和Runtime异常体系

Java的异常被分为两大类:Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。

Java认为Checked异常都是可以被处理(修复)的异常,所以Java程序必须显式处理Checked异常。

对于Checked异常的处理方式有如下两种。

➢ 当前方法明确知道如何处理该异常,程序应该使用try…catch块来捕获该异常,然后在对应的catch块中修复该异常

➢ 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常。

Runtime异常则更加灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try…catch块来实现。

10.3.1 使用throws声明抛出异常

throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间以逗号隔开。throws声明抛出的语法格式如下:

10.3.2 方法重写时声明抛出异常的限制

使用throws声明抛出异常时有一个限制,就是方法重写时“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。

10.4 使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成。

10.4.1 抛出异常

如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。throw语句的语法格式如下:

10.4.2 自定义异常类

用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。

10.4.3 catch和throw同时使用

10.4.4 使用throw语句抛出异常

10.4.5 异常链

10.5 Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,开发者可以找到异常的源头,并跟踪到异常一路触发的过程。

10.6 异常处理规则

不要过度使用异常; 不要使用过于庞大的try块; 避免使用Catch All语句; 不要忽略捕获到的异常
在这里插入图片描述
异常捕获:
在这里插入图片描述
异常返回
在这里插入图片描述
异常抛出
在这里插入图片描述
异常自定义
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第11章 AWT编程

第12章 Swing编程

第13章 MySQL数据库与JDBC编程

13.1 JDBC基础

JDBC的全称是Java Database Connectivity,即Java数据库连接,它是一种可以执行SQL语句的Java API。

程序可通过JDBC API连接到关系数据库,并使用结构化查询语言(SQL,数据库标准的查询语言)来完成对数据库的查询、更新。与其他数据库编程环境相比,JDBC为数据库开发提供了标准的API,所以使用JDBC开发的数据库应用可以跨平台运行,而且可以跨数据库(如果全部使用标准的SQL)。

13.1.1 JDBC简介

为了使JDBC程序可以跨平台,则需要不同的数据库厂商提供相应的驱动程序。图13.1显示了JDBC驱动示意图。
在这里插入图片描述

正是通过JDBC驱动的转换,才使得使用相同JDBC API编写的程序,在不同的数据库系统上运行良好。Sun提供的JDBC可以完成以下三个基本工作。

➢ 建立与数据库的连接。

➢ 执行SQL语句。

➢ 获得SQL语句的执行结果。

通过JDBC的这三个功能,应用程序即可访问、操作数据库系统。

13.1.2 JDBC驱动程序

数据库驱动程序是JDBC程序和数据库之间的转换层,数据库驱动程序负责将JDBC调用映射成特定的数据库调用。图13.2显示了JDBC示意图。
在这里插入图片描述

JDBC驱动通常有如下4种类型。

➢ 第1种JDBC驱动:称为JDBC-ODBC桥,这种驱动是最早实现的JDBC驱动程序,主要目的是为了快速推广JDBC。这种驱动将JDBC API映射到ODBC API。这种方式在Java 8中已经被删除了。

➢ 第2种JDBC驱动:直接将JDBC API映射成数据库特定的客户端API。这种驱动包含特定数据库的本地代码,用于访问特定数据库的客户端。

➢ 第3种JDBC驱动:支持三层结构的JDBC访问方式,主要用于Applet阶段,通过Applet访问数据库。

➢ 第4种JDBC驱动:是纯Java的,直接与数据库实例交互。这种驱动是智能的,它知道数据库使用的底层协议。这种驱动是目前最流行的JDBC驱动。

13.2 SQL语法

SQL语句是对所有关系数据库都通用的命令语句,而JDBC API只是执行SQL语句的工具,JDBC允许对不同的平台、不同的数据库采用相同的编程接口来执行SQL语句。

13.2.1 安装数据库

13.2.2 关系数据库基本概念和MySQL基本命令

数据库(Database)仅仅是存放用户数据的地方。当用户访问、操作数据库中的数据时,就需要数据库管理系统的帮助。数据库管理系统的全称是Database Management System,简称DBMS。习惯上常常把数据库和数据库管理系统笼统地称为数据库,通常所说的数据库既包括存储用户数据的部分,也包括管理数据库的管理系统。DBMS是所有数据的知识库,它负责管理数据的存储、安全、一致性、并发、恢复和访问等操作。DBMS有一个数据字典(有时也被称为系统表),用于存储它拥有的每个事务的相关信息,例如名字、结构、位置和类型,这种关于数据的数据也被称为元数据(metadata)。在数据库发展历史中,按时间顺序主要出现了如下几种类型的数据库系统。➢ 网状型数据库➢ 层次型数据库➢ 关系数据库➢ 面向对象数据库

查看数据库:

在这里插入图片描述

创建数据库:

在这里插入图片描述

删除数据库:

在这里插入图片描述

进如指定数据库:
在这里插入图片描述

查看数据库中数据表:
在这里插入图片描述

查看数据表结构:

在这里插入图片描述

启动MySQL命令行客户端:
在这里插入图片描述

13.2.3 SQL语句基础

SQL的全称是Structured Query Language,也就是结构化查询语言。SQL是操作和检索关系数据库的标准语言,标准的SQL语句可用于操作任何关系数据库。

标准的SQL语句通常可分为如下几种类型。

➢ 查询语句:主要由select关键字完成,查询语句是SQL语句中最复杂、功能最丰富的语句。

➢ DML(Data Manipulation Language,数据操作语言)语句:主要由insert、update和delete三个关键字完成。

➢ DDL(Data Definition Language,数据定义语言)语句:主要由create、alter、drop和truncate四个关键字完成。

➢ DCL(Data Control Language,数据控制语言)语句:主要由grant和revoke两个关键字完成。

➢ 事务控制语句:主要由commit、rollback和savepoint三个关键字完成。

SQL语句的关键字不区分大小写,也就是说,create和CREATE的作用完全一样。

在SQL命令中也可能需要使用标识符,标识符可用于定义表名、列名,也可用于定义变量等。这些标识符的命名规则如下。➢ 标识符通常必须以字母开头。➢ 标识符包括字母、数字和三个特殊字符(#_$)。➢ 不要使用当前数据库系统的关键字、保留字,通常建议使用多个单词连缀而成,单词之间以_分隔。➢ 同一个模式下的对象不应该同名,这里的模式指的是外模式。

13.2.4 DDL语句

DDL语句是操作数据库对象的语句,包括创建(create)、删除(drop)和修改(alter)数据库对象。
在这里插入图片描述

1.创建表的语法
在这里插入图片描述

在这里插入图片描述

2.修改表结构的语法

修改表结构包括增加列定义、修改列定义、删除列、重命名列等操作。

增加列定义的语法:
在这里插入图片描述

修改列定义的语法:

在这里插入图片描述

删除列的语法:

在这里插入图片描述

重命名数据表名的语法:
在这里插入图片描述

改变数据列名称:
在这里插入图片描述

3.删除表的语法

删除表的语法格式如下:
在这里插入图片描述

删除数据表的效果如下:

➢ 表结构被删除,表对象不再存在。

➢ 表里的所有数据也被删除。

➢ 该表所有相关的索引、约束也被删除。

4.truncate表

truncate被称为“截断”某个表—它的作用是删除该表里的全部数据,但保留表结构。相对于DML里的delete命令而言,truncate的速度要快得多,而且truncate不像delete可以删除指定的记录,truncate只能一次性删除整个表的全部记录。

truncate命令的语法如下:

在这里插入图片描述

MySQL对truncate的处理比较特殊—如果使用非InnoDB存储机制,truncate比delete速度要快;如果使用InnoDB存储机制,在MySQL 5.0.3之前,truncate和delete完全一样,在5.0.3之后,truncate table比delete效率高,但如果该表被外键约束所参照,truncate又变为delete操作。在5.0.13之后,快速truncate总是可用,即比delete性能要好。

13.2.5 数据库约束

约束是在表上强制执行的数据校验规则,约束主要用于保证数据库里数据的完整性。

大部分数据库支持下面5种完整性约束:

➢ NOT NULL:非空约束,指定某列不能为空。

➢ UNIQUE:唯一约束,指定某列或者几列组合不能重复。

➢ PRIMARY KEY:主键,指定该列的值可以唯一地标识该条记录。

➢ FOREIGN KEY:外键,指定该行记录从属于主表中的一条记录,主要用于保证参照完整性。

➢ CHECK:检查,指定一个布尔表达式,用于指定对应列的值必须满足该表达式。(MySql不支持,使用了也没效果)

虽然约束的作用只是用于保证数据表里数据的完整性,但约束也是数据库对象,并被存储在系统表中,也拥有自己的名字。根据约束对数据列的限制,约束分为如下两类。➢ 单列约束:每个约束只约束一列。➢ 多列约束:每个约束可以约束多个数据列。

1.NOT NULL约束

非空约束用于确保指定列不允许为空,它只能作为列级约束使用,只能使用列级约束语法定义。
在这里插入图片描述

2.UNIQUE约束

唯一约束用于保证指定列或指定列组合不允许出现重复值。当为某列创建唯一约束时,MySQL会为该列相应地创建唯一索引。如果不给唯一约束起名,该唯一约束默认与列名相同。

使用列级约束语法建立唯一约束非常简单,只要简单地在列定义后增加unique关键字即可。SQL语句如下:

在这里插入图片描述

如果需要为多列组合建立唯一约束,或者想自行指定约束名,则需要使用表级约束语法。表级约束语法格式如下:
在这里插入图片描述

3.PRIMARY KEY约束

主键约束相当于非空约束和唯一约束,如果对多列组合建立主键约束,则多列里包含的每一列都不能为空,但只要求这些列组合不能重复。主键列的值可用于唯一地标识表中的一条记录。每一个表中最多允许有一个主键,但这个主键约束可由多个数据列组合而成,主键是表中能唯一确定一行记录的字段或字段组合。

建立主键约束时既可使用列级约束语法,也可使用表级约束语法。如果需要对多个字段建立组合主键约束,则只能使用表级约束语法。使用表级约束语法来建立约束时,可以为该约束指定约束名。但不管用户是否为该主键约束指定约束名,MySQL总是将所有的主键约束命名为PRIMARY。

建表时创建主键约束,使用列级约束语法:

在这里插入图片描述

建表时创建主键约束,使用表级约束语法:
在这里插入图片描述

如果需要删除指定表的主键约束,则在alter table语句后使用drop primary key子句即可。SQL语句如下:
在这里插入图片描述

如果需要为指定表增加主键约束,既可通过modify修改列定义来增加主键约束,这将采用列级约束语法来增加主键约束;也可通过add来增加主键约束,这将采用表级约束语法来增加主键约束。SQL语句如下:
在这里插入图片描述

如果只是为单独的数据列增加主键约束,则可使用modify修改列定义来实现。SQL语句如下:
在这里插入图片描述

4.FOREIGN KEY约束

外键约束主要用于保证一个或两个数据表之间的参照完整性,外键是构建于一个表的两个字段或者两个表的两个字段之间的参照关系。外键确保了相关的两个字段的参照关系:子(从)表外键列的值必须在主表被参照列的值范围之内,或者为空(也可以通过非空约束来约束外键列不允许为空)。

采用列级约束语法建立外键约束直接使用references关键字,references指定该列参照哪个主表,以及参照主表的哪一列。SQL语句如下:
在这里插入图片描述

如果要使MySQL中的外键约束生效,则应使用表级约束语法。
在这里插入图片描述

5.CHECK约束

当前版本的MySQL支持建表时指定CHECK约束,但这个CHECK约束不会有任何作用。建立CHECK约束的语法很简单,只要在建表的列定义后增加check(逻辑表达式)即可。

13.2.6 索引

创建索引的唯一作用就是加速对表的查询,索引通过使用快速路径访问方法来快速定位数据,从而减少了磁盘的I/O。

创建索引有两种方式。

➢ 自动:当在表上定义主键约束、唯一约束和外键约束时,系统会为该数据列自动创建对应的索引。

➢ 手动:用户可以通过create index…语句来创建索引。

删除索引也有两种方式。

➢ 自动:数据表被删除时,该表上的索引自动被删除。

➢ 手动:用户可以通过drop index…语句来删除指定数据表上的指定索引。

创建索引的语法格式如下:

在这里插入图片描述

MySQL中删除索引需要指定表,采用如下语法格式:
在这里插入图片描述

索引的好处是可以加速查询,但索引也有如下两个坏处。

➢ 当数据表中的记录被添加、删除、修改时,数据库系统需要维护索引,因此有一定的系统开销。

➢ 存储索引信息需要一定的磁盘空间。

13.2.7 视图

视图看上去非常像一个数据表,但它不是数据表,因为它并不能存储数据。

视图只是一个或多个数据表中数据的逻辑显示。

使用视图有如下几个好处:

➢ 可以限制对数据的访问。

➢ 可以使复杂的查询变得简单。

➢ 提供了数据的独立性。

➢ 提供了对相同数据的不同显示。

创建视图的语法如下:

在这里插入图片描述

为了强制不允许改变视图的数据,MySQL允许在创建视图时使用with check option子句,使用该子句创建的视图不允许修改。

删除视图使用如下语句:

在这里插入图片描述

13.2.8 DML语句语法

与DDL操作数据库对象不同,DML主要操作数据表里的数据,使用DML可以完成如下三个任务。

➢ 插入新数据。➢ 修改已有数据。➢ 删除不需要的数据。

DML语句由insert into、update和delete from三个命令组成。

1.insert into语句
在这里插入图片描述

2.update语句
在这里插入图片描述

3.delete from语句

在这里插入图片描述

13.2.9 单表查询

单表查询的select语句的语法格式如下:

在这里插入图片描述

上面语法格式中的数据源可以是表、视图等。

13.2.10 数据库函数

根据函数对多行数据的处理方式,函数被分为单行函数和多行函数,单行函数对每行输入值单独计算,每行得到一个计算结果返回给用户;多行函数对多行输入值整体计算,最后只会得到一个结果。

13.2.11 分组和组函数

组函数也就是前面提到的多行函数,组函数将一组记录作为整体计算,每组记录返回一个结果,而不是每条记录返回一个结果。常用的组函数有如下5个:

➢ avg([distinct|all]expr):计算多行expr的平均值,其中,expr可以是变量、常量或数据列,但其数据类型必须是数值型。还可以在变量、列前使用distinct或all关键字,如果使用distinct,则表明不计算重复值;all用和不用的效果完全一样,表明需要计算重复值。

➢ count({|[distinct|all]expr}):计算多行expr的总条数,其中expr可以是变量、常量或数据列,其数据类型可以是任意类型;用星号()表示统计该表内的记录行数;distinct表示不计算重复值。

➢ max(expr):计算多行expr的最大值,其中expr可以是变量、常量或数据列,其数据类型可以是任意类型。

➢ min(expr):计算多行expr的最小值,其中expr可以是变量、常量或数据列,其数据类型可以是任意类型。

➢ sum([distinct|all]expr):计算多行expr的总和,其中expr可以是变量、常量或数据列,但其数据类型必须是数值型;distinct表示不计算重复值。

13.2.12 多表连接查询

13.3 JDBC的典型用法

13.3.1 JDBC 4.2常用接口和类简介

Java支持JDBC 4.2标准,JDBC 4.2在原有JDBC标准上增加了一些新特性。下面介绍这些JDBC API时会提到Java 8新增的功能。

➢ DriverManager:用于管理JDBC驱动的服务类。程序中使用该类的主要功能是获取Connection对象,该类包含如下方法。

• public static synchronized Connection getConnection(String url, String user,String pass) throws SQLException:该方法获得url对应数据库的连接。

➢ Connection:代表数据库连接对象,每个Connection代表一个物理连接会话。要想访问数据库,必须先获得数据库连接。该接口的常用方法如下。

• Statement createStatement() throws SQLExcetpion:该方法返回一个Statement对象。

• PreparedStatement prepareStatement(String sql) throws SQLExcetpion:该方法返回预编译的Statement对象,即将SQL语句提交到数据库进行预编译。

• CallableStatement prepareCall(String sql) throws SQLExcetpion:该方法返回CallableStatement对象,该对象用于调用存储过程。

Connection还有如下几个用于控制事务的方法:

➢ Savepoint setSavepoint():创建一个保存点。

➢ Savepoint setSavepoint(String name):以指定名字来创建一个保存点。

➢ void setTransactionIsolation(int level):设置事务的隔离级别。

➢ void rollback():回滚事务。

➢ void rollback(Savepoint savepoint):将事务回滚到指定的保存点。

➢ void setAutoCommit(boolean autoCommit):关闭自动提交,打开事务。

➢ void commit():提交事务。

Java 7为Connection新增了setSchema(String schema)、getSchema()两个方法,这两个方法用于控制该Connection访问的数据库Schema。Java 7还为Connection新增了setNetworkTimeout(Executor executor, int milliseconds)、getNetworkTimeout()两个方法来控制数据库连接的超时行为。

➢ Statement:用于执行SQL语句的工具接口。该对象既可用于执行DDL、DCL语句,也可用于执行DML语句,还可用于执行SQL查询。当执行SQL查询时,返回查询到的结果集。它的常用方法如下:

• ResultSet executeQuery(String sql) throws SQLException:该方法用于执行查询语句,并返回查询结果对应的ResultSet对象。该方法只能用于执行查询语句。

• int executeUpdate(String sql) throws SQLExcetion:该方法用于执行DML语句,并返回受影响的行数;该方法也可用于执行DDL语句,执行DDL语句将返回0。

• boolean execute(String sql) throws SQLException:该方法可执行任何SQL语句。如果执行后第一个结果为ResultSet对象,则返回true;如果执行后第一个结果为受影响的行数或没有任何结果,则返回false。

Java 7为Statement新增了closeOnCompletion()方法,如果Statement执行了该方法,则当所有依赖于该Statement的ResultSet关闭时,该Statement会自动关闭。Java 7还为Statement提供了一个isCloseOnCompletion()方法,该方法用于判断该Statement是否打开了“closeOnCompletion”。

Java 8为Statement新增了多个重载的executeLargeUpdate()方法,这些方法相当于增强版的executeUpdate()方法,返回值类型为long—也就是说,当DML语句影响的记录条数超过Integer.MAX_VALUE时,就应该使用executeLargeUpdate()方法。

➢ PreparedStatement:预编译的Statement对象。PreparedStatement是Statement的子接口,它允许数据库预编译SQL语句(这些SQL语句通常带有参数),以后每次只改变SQL命令的参数,避免数据库每次都需要编译SQL语句,因此性能更好。相对于Statement而言,使用PreparedStatement执行SQL语句时,无须再传入SQL语句,只要为预编译的SQL语句传入参数值即可。所以它比Statement多了如下方法。

• void setXxx(int parameterIndex,Xxx value):该方法根据传入参数值的类型不同,需要使用不同的方法。传入的值根据索引传给SQL语句中指定位置的参数。

➢ ResultSet:结果集对象。该对象包含访问查询结果的方法,ResultSet可以通过列索引或列名获得列数据。它包含了如下常用方法来移动记录指针。• void close():释放ResultSet对象。• boolean absolute(int row):将结果集的记录指针移动到第row行,如果row是负数,则移动到倒数第row行。如果移动后的记录指针指向一条有效记录,则该方法返回true。

• void beforeFirst():将ResultSet的记录指针定位到首行之前,这是ResultSet结果集记录指针的初始状态—记录指针的起始位置位于第一行之前。

• boolean first():将ResultSet的记录指针定位到首行。如果移动后的记录指针指向一条有效记录,则该方法返回true。

• boolean previous():将ResultSet的记录指针定位到上一行。如果移动后的记录指针指向一条有效记录,则该方法返回true。

• boolean next():将ResultSet的记录指针定位到下一行,如果移动后的记录指针指向一条有效记录,则该方法返回true。

• boolean last():将ResultSet的记录指针定位到最后一行,如果移动后的记录指针指向一条有效记录,则该方法返回true。

• void afterLast():将ResultSet的记录指针定位到最后一行之后。

13.3.2 JDBC编程步骤

JDBC编程大致按如下步骤进行:

① 加载数据库驱动。通常使用Class类的forName()静态方法来加载驱动。例如如下代码:

在这里插入图片描述
在这里插入图片描述

② 通过DriverManager获取数据库连接。DriverManager提供了如下方法:
在这里插入图片描述

③ 通过Connection对象创建Statement对象。

Connection创建Statement的方法有如下三个。

➢ createStatement():创建基本的Statement对象。

➢ prepareStatement(String sql):根据传入的SQL语句创建预编译的Statement对象。

➢ prepareCall(String sql):根据传入的SQL语句创建CallableStatement对象。

④ 使用Statement执行SQL语句。

所有的Statement都有如下三个方法来执行SQL语句。

➢ execute():可以执行任何SQL语句,但比较麻烦。

➢ executeUpdate():主要用于执行DML和DDL语句。执行DML语句返回受SQL语句影响的行数,执行DDL语句返回0。

➢ executeQuery():只能执行查询语句,执行后返回代表查询结果的ResultSet对象。

⑤ 操作结果集。

如果执行的SQL语句是查询语句,则执行结果将返回一个ResultSet对象,该对象里保存了SQL语句查询的结果。程序可以通过操作该ResultSet对象来取出查询结果。ResultSet对象主要提供了如下两类方法。

➢ next()、previous()、first()、last()、beforeFirst()、afterLast()、absolute()等移动记录指针的方法。

➢ getXxx()方法获取记录指针指向行、特定列的值。该方法既可使用列索引作为参数,也可使用列名作为参数。

⑥ 回收数据库资源,包括关闭ResultSet、Statement和Connection等资源。

13.4 执行SQL语句的方式

第14章 注解(Annotation)

从JDK 5开始,Java增加了对元数据(MetaData)的支持,也就是Annotation(即注解,偶尔也被翻译为注释)。

注意:注解是一个接口,程序可以通过反射来获取指定程序元素的java.lang.annotation.Annotation对象,然后通过java.lang.annotation.Annotation对象来取得注解里的元数据。

14.1 基本注解

注解必须使用工具来处理,工具负责提取注解里包含的元数据,工具还会根据这些元数据增加额外的功能。

Java提供的5个基本注解的用法——使用注解时要在其前面增加@符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。5个基本的注解如下:

➢ @Override➢ @Deprecated➢ @SuppressWarnings➢ @SafeVarargs➢ @FunctionalInterface

上面5个基本注解中的@SafeVarargs是Java 7新增的、@FunctionalInterface是Java 8新增的。这5个基本的注解都定义在java.lang包下。

14.1.1 限定重写父类方法:@Override

@Override就是用来指定方法覆载的,它可以强制一个子类必须覆盖父类的方法。

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

14.1.2 Java 9增强的@Deprecated

@Deprecated用于表示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。

Java 9为@Deprecated注解增加了如下两个属性。

➢ forRemoval:该boolean类型的属性指定该API在将来是否会被删除。

➢ since:该String类型的属性指定该API从哪个版本被标记为过时。

14.1.3 抑制编译器警告:@SuppressWarnings
@SuppressWarnings指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。

14.1.4 “堆污染”警告与Java 9增强的@SafeVarargs
在这里插入图片描述

Java把引发这种错误的原因称为“堆污染”(Heap pollution),当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种“堆污染”,如上①号粗体字代码所示。

对于形参个数可变的方法,该形参的类型又是泛型,这将更容易导致“堆污染”。

但在有些时候,开发者不希望看到这个警告,则可以使用如下三种方式来“抑制”这个警告。

➢ 使用@SafeVarargs修饰引发该警告的方法或构造器。Java 9增强了该注解,允许使用该注解修饰私有实例方法。

➢ 使用@SuppressWarnings(“unchecked”)修饰。

➢ 编译时使用-Xlint:varargs选项。

14.1.5 函数式接口与@FunctionalInterface

从Java 8开始:如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口就是函数式接口。@FunctionalInterface就是用来指定某个接口必须是函数式接口。

14.2 JDK的元注解

JDK除在java.lang下提供了5个基本的注解之外,还在java.lang.annotation包下提供了6个Meta注解(元注解),其中有5个元注解都用于修饰其他的注解定义。

14.2.1 使用@Retention

@Retention只能用于修饰注解定义,用于指定被修饰的注解可以保留多长时间。

@Retention包含一个RetentionPolicy类型的value成员变量,所以使用@Retention时必须为该value成员变量指定值。value成员变量的值只能是如下三个。

➢ RetentionPolicy.CLASS:编译器将把注解记录在class文件中。当运行Java程序时,JVM不可获取注解信息。这是默认值。

➢ RetentionPolicy.RUNTIME:编译器将把注解记录在class文件中。当运行Java程序时,JVM也可获取注解信息,程序可以通过反射获取该注解信息。

➢ RetentionPolicy.SOURCE:注解只保留在源代码中,编译器直接丢弃这种注解。

14.2.2 使用@Target

@Target也只能修饰注解定义,它用于指定被修饰的注解能用于修饰哪些程序单元。

@Target元注解也包含一个名为value的成员变量,该成员变量的值只能是如下几个。

➢ ElementType.ANNOTATION_TYPE:指定该策略的注解只能修饰注解。

➢ ElementType.CONSTRUCTOR:指定该策略的注解只能修饰构造器。

➢ ElementType.FIELD:指定该策略的注解只能修饰成员变量。

➢ ElementType.LOCAL_VARIABLE:指定该策略的注解只能修饰局部变量。

➢ ElementType.METHOD:指定该策略的注解只能修饰方法定义。

➢ ElementType.PACKAGE:指定该策略的注解只能修饰包定义。

➢ ElementType.PARAMETER:指定该策略的注解可以修饰参数。

➢ ElementType.TYPE:指定该策略的注解可以修饰类、接口(包括注解类型)或枚举定义。

14.2.3 使用@Documented

@Documented用于指定被该元注解修饰的注解类将被javadoc工具提取成文档,如果定义注解类时使用了@Documented修饰,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。

14.2.4 使用@Inherited

@Inherited元注解指定被它修饰的注解将具有继承性——如果某个类使用了@Xxx注解(定义该注解时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰。

14.3 自定义注解

14.3.1 定义注解

定义新的注解类型使用@interface关键字(在原有的interface关键字前增加@符号)定义一个新的注解类型与定义一个接口非常像,如下代码可定义一个简单的注解类型。

在这里插入图片描述

注解不仅可以是这种简单的注解,还可以带成员变量,成员变量在注解定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。如下代码可以定义一个有成员变量的注解。

在这里插入图片描述
在这里插入图片描述

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

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

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

14.3.2 提取注解信息

使用注解修饰了类、方法、成员变量等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取并处理注解信息。

Java使用java.lang.annotation.Annotation接口来代表程序元素前面的注解,该接口是所有注解的父接口。Java 5在java.lang.reflect包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素。该接口主要有如下几个实现类。

➢ Class:类定义。

➢ Constructor:构造器定义。

➢ Field:类的成员变量定义。

➢ Method:类的方法定义。

➢ Package:类的包定义。

java.lang.reflect包下主要包含一些实现反射功能的工具类,从Java 5开始,java.lang.reflect包所提供的反射API增加了读取运行时注解的能力。只有当定义注解时使用了@Retention(RetentionPolicy.RUNTIME)修饰,该注解才会在运行时可见,JVM才会在装载*.class文件时读取保存在class文件中的注解信息。

AnnotatedElement接口是所有程序元素(如Class、Method、Constructor等)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象(如Class、Method、Constructor等)之后,程序就可以调用该对象的如下几个方法来访问注解信息。

➢ A getAnnotation(Class annotationClass):返回该程序元素上存在的、指定类型的注解,如果该类型的注解不存在,则返回null。

➢ A getDeclaredAnnotation(Class annotationClass):这是Java 8新增的方法,该方法尝试获取直接修饰该程序元素、指定类型的注解。如果该类型的注解不存在,则返回null。

➢ Annotation[] getAnnotations():返回该程序元素上存在的所有注解。

➢ Annotation[] getDeclaredAnnotations():返回直接修饰该程序元素的所有注解。

➢ boolean isAnnotationPresent(Class< ?extends Annotation> annotationClass):判断该程序元素上是否存在指定类型的注解,如果存在则返回true,否则返回false。

➢ A[] getAnnotationsByType(Class annotationClass):该方法的功能与前面介绍的getAnnotation()方法基本相似。但由于Java 8增加了重复注解功能,因此需要使用该方法获取修饰该程序元素、指定类型的多个注解。

➢ A[] getDeclaredAnnotationsByType(ClassannotationClass):该方法的功能与前面介绍的getDeclaredAnnotations()方法基本相似。但由于Java 8增加了重复注解功能,因此需要使用该方法获取直接修饰该程序元素、指定类型的多个注解。

14.3.3 使用注解的示例

14.3.4 重复注解

Java 8允许使用多个相同类型的注解来修饰同一个类。

14.3.5 类型注解

Java 8为ElementType枚举增加了TYPE_PARAMETER、TYPE_USE两个枚举值,这样就允许定义注解时使用@Target(ElementType.TYPE_USE)修饰,这种注解被称为类型注解(TypeAnnotation),类型注解可用于修饰在任何地方出现的类型。

在Java 8以前,只能在定义各种程序元素(定义类、定义接口、定义方法、定义成员变量……)时使用注解。从Java 8开始,类型注解可以修饰在任何地方出现的类型。比如,允许在如下位置使用类型注解。➢ 创建对象(用new关键字创建)。➢ 类型转换。➢ 使用implements实现接口。➢ 使用throws声明抛出异常。上面这些情形都会用到类型,因此都可以使用类型注解来修饰。

14.4 编译时处理注解

APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件所包含的注解信息,然后针对注解信息进行额外的处理。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第15章 输入/输出

15.1 File类

File类是java.io包下代表与平台无关的文件和目录,值得指出的是,不管是文件还是目录都是使用File来操作的,File能新建、删除、重命名文件和目录,File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

15.1.1 访问文件和目录

File类提供了很多方法来操作文件和目录:

1.访问文件名相关方法

➢ String getName():返回此File对象所表示的文件名或路径名(如果是路径,则返回最后一级子路径名)。

➢ String getPath():返回此File对象所对应的路径名。

➢ File getAbsoluteFile():返回此File对象的绝对路径。

➢ String getAbsolutePath():返回此File对象所对应的绝对路径名。

➢ String getParent():返回此File对象所对应目录(最后一级子目录)的父目录名。

➢ boolean renameTo(File newName):重命名此File对象所对应的文件或目录,如果重命名成功,则返回true;否则返回false。

2.文件检测相关方法

➢ boolean exists():判断File对象所对应的文件或目录是否存在。

➢ boolean canWrite():判断File对象所对应的文件和目录是否可写。

➢ boolean canRead():判断File对象所对应的文件和目录是否可读。

➢ boolean isFile():判断File对象所对应的是否是文件,而不是目录。

➢ boolean isDirectory():判断File对象所对应的是否是目录,而不是文件。

➢ boolean isAbsolute():判断File对象所对应的文件或目录是否是绝对路径。

3.获取常规文件信息

➢ long lastModified():返回文件的最后修改时间。

➢ long length():返回文件内容的长度。

4.文件操作相关方法

➢ boolean createNewFile():当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,如果创建成功则返回true;否则返回false。

➢ boolean delete():删除File对象所对应的文件或路径。

➢ static File createTempFile(String prefix, String suffix):在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名

➢ static File createTempFile(String prefix, String suffix, File directory):在directory所指定的目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。

➢ void deleteOnExit():注册一个删除钩子,指定当Java虚拟机退出时,删除File对象所对应的文件和目录。

5.目录操作相关方法

➢ boolean mkdir():试图创建一个File对象所对应的目录,如果创建成功,则返回true;否则返回false。

➢ String[] list():列出File对象的所有子文件名和路径名,返回String数组。

➢ File[] listFiles():列出File对象的所有子文件和路径,返回File数组。

➢ static File[] listRoots():列出系统所有的根路径。这是一个静态方法,可以直接通过File类来调用。

15.1.2 文件过滤器

在File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

FilenameFilter接口里包含了一个accept(File dir, String name)方法,该方法将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

15.2 理解Java的IO流

15.2.1 流的分类

按照不同的分类方式:

1.输入流和输出流按照流的流向来分,可以分为输入流和输出流。

➢ 输入流:只能从中读取数据,而不能向其写入数据。

➢ 输出流:只能向其写入数据,而不能从中读取数据。

数据从内存到硬盘,通常称为输出流——也就是说,这里的输入、输出都是从程序运行所在内存的角度来划分的。

Java的输入流主要由InputStream和Reader作为基类,而输出流则主要由OutputStream和Writer作为基类。它们都是一些抽象基类,无法直接创建实例。

2.字节流和字符流

3.节点流和处理流按照流的角色来分,可以分为节点流和处理流。

15.2.2 流的概念模型

Java的IO流的40多个类都是从如下4个抽象基类派生的。

➢ InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。

➢ OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。

15.3 字节流和字符流

15.3.1 InputStream和Reader

InputStream和Reader是所有输入流的抽象基类:

在InputStream里包含如下三个方法。

➢ int read():从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型)。

➢ int read(byte[] b):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。

➢ int read(byte[] b, int off, int len):从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数。

在Reader里包含如下三个方法。

➢ int read():从输入流中读取单个字符,返回所读取的字符数据(字符数据可直接转换为int类型)。

➢ int read(char[] cbuf):从输入流中最多读取cbuf.length个字符数据,并将其存在字符数组cbuf中,返回实际读取的字符数。

➢ int read(char[] cbuf, int off, int len):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组cbuf中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数。

InputStream和Reader还支持如下几个方法来移动记录指针。

➢ void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark)。

➢ boolean markSupported():判断此输入流是否支持mark()操作,即是否支持记录标记。

➢ void reset():将此流的记录指针重新定位到上一次记录标记(mark)的位置。

➢ long skip(long n):记录指针向前移动n个字节/字符。

15.3.2 OutputStream和Writer

OutputStream和Writer都提供了如下三个方法。

➢ void write(int c):将指定的字节/字符输出到输出流中,其中c既可以代表字节,也可以代表字符。

➢ void write(byte[]/char[] buf):将字节数组/字符数组中的数据输出到指定输出流中。

➢ void write(byte[]/char[] buf, int off, int len):将字节数组/字符数组中从off位置开始,长度为len的字节/字符输出到输出流中。

因为字符流直接以字符作为操作单位,Writer里还包含如下两个方法。

➢ void write(String str):将str字符串里包含的字符输出到指定输出流中。

➢ void write(String str, int off, int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中。

15.4 输入/输出流体系

15.4.1 处理流的用法

处理流的功能,它可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入/输出方法。使用处理流时的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的I/O设备、文件交互。实际识别处理流非常简单,只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流;而所有节点流都是直接以物理IO节点作为构造器参数的。

15.4.2 输入/输出流体系

Java的输入/输出流体系提供了近40个类,表15.1显示了Java输入/输出流体系中常用的流分类。

在这里插入图片描述

注:表15.1中的粗体字标出的类代表节点流,必须直接与指定的物理节点关联;斜体字标出的类代表抽象基类,无法直接创建实例。

15.4.3 转换流

输入/输出流体系中还提供了两个转换流,这两个转换流用于实现将字节流转换成字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。

15.4.4 推回输入流

在输入/输出流体系中,有两个特殊的流与众不同,就是PushbackInputStream和PushbackReader,它们都提供了如下三个方法。

➢ void unread(byte[]/char[] buf):将一个字节/字符数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

➢ void unread(byte[]/char[] b, int off, int len):将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

➢ void unread(int b):将一个字节/字符推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

细心的读者可能已经发现了这三个方法与InputStream和Reader中的三个read()方法一一对应,这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread()方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read()所需的数组时才会从原输入流中读取。

当程序创建一个PushbackInputStream和PushbackReader时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1。如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflow的IOException异常。

15.5 重定向标准输入/输出

在System类里提供了如下三个重定向标准输入/输出的方法。

➢ static void setErr(PrintStream err):重定向 “标准”错误输出流。

➢ static void setIn(InputStream in):重定向“标准”输入流。

➢ static void setOut(PrintStream out):重定向 “标准”输出流。

15.6 Java虚拟机读写其他进程的数据

使用Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,Process对象代表由该Java程序启动的子进程。

Process类提供了如下三个方法,用于让程序和其子进程进行通信。

➢ InputStream getErrorStream():获取子进程的错误流。

➢ InputStream getInputStream():获取子进程的输入流。

➢ OutputStream getOutputStream():获取子进程的输出流。

15.7 RandomAccessFile

RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内容访问类,支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据。

RandomAccessFile的方法虽然多,但它有一个最大的局限,就是只能读写文件,不能读写其他IO节点。

RandomAccessFile对象也包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件记录指针位于文件头(也就是0处),当读/写了n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针,既可以向前移动,也可以向后移动。

RandomAccessFile包含了如下两个方法来操作文件记录指针。

➢ long getFilePointer():返回文件记录指针的当前位置。

➢ void seek(long pos):将文件记录指针定位到pos位置。

RandomAccessFile既可以读文件,也可以写,所以它既包含了完全类似于InputStream的三个read()方法,其用法和InputStream的三个read()方法完全一样;也包含了完全类似于OutputStream的三个write()方法,其用法和OutputStream的三个write()方法完全一样。

除此之外,RandomAccessFile还包含了一系列的readXxx()和writeXxx()方法来完成输入、输出。

RandomAccessFile类有两个构造器,其实这两个构造器基本相同,只是指定文件的形式不同而已—一个使用String参数来指定文件名,一个使用File参数来指定文件本身。

除此之外,创建RandomAccessFile对象时还需要指定一个mode参数,该参数指定RandomAccessFile的访问模式,该参数有如下4个值。

➢ “r”:以只读方式打开指定文件。如果试图对该RandomAccessFile执行写入方法,都将抛出IOException异常。

➢ “rw”:以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。

➢ “rws”:以读、写方式打开指定文件。相对于"rw"模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。

➢ “rwd”:以读、写方式打开指定文件。相对于"rw"模式,还要求对文件内容的每个更新都同步写入到底层存储设备。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

例子
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

15.8 Java 9改进的对象序列化

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点。其他程序一旦获得了这种二进制流(无论是从磁盘中获取的,还是通过网络获取的),都可以将这种二进制流恢复成原来的Java对象。

15.8.1 序列化的含义和意义

序列化机制允许将实现序列化的Java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。

序列化机制使得对象可以脱离程序的运行而独立存在。

对象的序列化(Serialize)指将一个Java对象写入IO流中,与此对应的是,对象的反序列化(Deserialize)则指从IO流中恢复该Java对象。

Java 9增强了对象序列化机制,它允许对读入的序列化数据进行过滤,这种过滤可在反序列化之前对数据执行校验,从而提高安全性和健壮性。

为了让某个类是可序列化的,该类必须实现如下两个接口之一。

➢ Serializable

➢ ExternalizableJava的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。

通常建议:程序创建的每个JavaBean类都实现Serializable。

15.8.2 使用对象流实现序列化

使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。一旦某个类实现了Serializable接口,该类的对象就是可序列化的,程序可以通过如下两个步骤来序列化该对象。

① 创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础之上。代码如下:
在这里插入图片描述

② 调用ObjectOutputStream对象的writeObject()方法输出可序列化对象。代码如下:
在这里插入图片描述

如果希望从二进制流中恢复Java对象,则需要使用反序列化。反序列化的步骤如下。

① 创建一个ObjectInputStream输入流,这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。代码如下:

在这里插入图片描述

② 调用ObjectInputStream对象的readObject()方法读取流中的对象,该方法返回一个Object类型的Java对象,如果程序知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型。代码如下:

在这里插入图片描述

15.8.3 对象引用的序列化

如果某个类的成员变量的类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。

Java序列化机制采用了一种特殊的序列化算法,其算法内容如下。

➢ 所有保存到磁盘中的对象都有一个序列化编号。

➢ 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出。

➢ 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。

15.8.4 Java 9增加的过滤功能

Java 9为ObjectInputStream增加了setObjectInputFilter()、getObjectInputFilter()两个方法,其中第一个方法用于为对象输入流设置过滤器。当程序通过ObjectInputStream反序列化对象时,过滤器的checkInput()方法会被自动激发,用于检查序列化数据是否有效。

使用checkInput()方法检查序列化数据时有3种返回值。

➢ Status.REJECTED:拒绝恢复。

➢ Status.ALLOWED:允许恢复。

➢ Status.UNDECIDED:未决定状态,程序继续执行检查。

ObjectInputStream将会根据ObjectInputFilter的检查结果来决定是否执行反序列化,如果checkInput()方法返回Status.REJECTED,反序列化将会被阻止;如果checkInput()方法返回Status.ALLOWED,程序将可执行反序列化。

15.8.5 自定义序列化

在一些特殊的场景下,如果一个类里包含的某些实例变量是敏感信息,例如银行账户信息等,这时不希望系统将该实例变量值进行序列化;或者某个实例变量的类型是不可序列化的,因此不希望对该实例变量进行递归序列化,以避免引发java.io.NotSerializableException异常。

提示:当对某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化,这种情况被称为递归序列化。

通过在实例变量前面使用transient(只能修饰实例变量)关键字修饰,可以指定Java序列化时无须理会该实例变量。

在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化。

➢ private void writeObject(java.io.ObjectOutputStream out) throws IOException

➢ private void readObject(java.io.ObjectInputStream in) throwsIOException,ClassNotFoundException;

➢ private void readObjectNoData() throws ObjectStreamException;

15.8.6 另一种自定义序列化机制

Java还提供了另一种序列化机制,这种序列化方式完全由程序员决定存储和恢复对象数据。要实现该目标,Java类必须实现Externalizable接口,该接口里定义了如下两个方法。

➢ void readExternal(ObjectInput in):需要序列化的类实现readExternal()方法来实现反序列化。该方法调用DataInput(它是ObjectInput的父接口)的方法来恢复基本类型的实例变量值,调用ObjectInput的readObject()方法来恢复引用类型的实例变量值。

➢ void writeExternal(ObjectOutput out):需要序列化的类实现writeExternal()方法来保存对象的状态。该方法调用DataOutput(它是ObjectOutput的父接口)的方法来保存基本类型的实例变量值,调用ObjectOutput的writeObject()方法来保存引用类型的实例变量值。

实际上,采用实现Externalizable接口方式的序列化与前面介绍的自定义序列化非常相似,只是Externalizable接口强制自定义序列化。

关于对象序列化,还有如下几点需要注意。

➢ 对象的类名、实例变量(包括基本类型、数组、对其他对象的引用)都会被序列化;方法、类变量(即static修饰的成员变量)、transient实例变量(也被称为瞬态实例变量)都不会被序列化。

➢ 实现Serializable接口的类如果需要让某个实例变量不被序列化,则可在该实例变量前加transient修饰符,而不是加static关键字。虽然static关键字也可达到这个效果,但static关键字不能这样用。

➢ 保证序列化对象的实例变量类型也是可序列化的,否则需要使用transient关键字来修饰该实例变量,要不然,该类是不可序列化的。

➢ 反序列化对象时必须有序列化对象的class文件。

➢ 当通过文件、网络来读取序列化后的对象时,必须按实际写入的顺序读取。

15.8.7 版本

Java序列化机制允许为序列化类提供一个private static final的serialVersionUID值,该类变量的值用于标识该Java类的序列化版本,也就是说,如果一个类升级后,只要它的serialVersionUID类变量值保持不变,序列化机制也会把它们当成同一个序列化版本。

可以通过JDK安装路径的bin目录下的serialver.exe工具来获得该类的serialVersionUID类变量的值。命令如下:
在这里插入图片描述

不显式指定serialVersionUID类变量的值的另一个坏处是,不利于程序在不同的JVM之间移植。因为不同的编译器对该类变量的计算策略可能不同,从而造成虽然类完全没有改变,但是因为JVM不同,也会出现序列化版本不兼容而无法正确反序列化的现象。如果类的修改确实会导致该类反序列化失败,则应该为该类的serialVersionUID类变量重新分配值。那么对类的哪些修改可能导致该类实例的反序列化失败呢?下面分三种情况来具体讨论。

➢ 如果修改类时仅仅修改了方法,则反序列化不受任何影响,类定义无须修改serialVersionUID类变量的值。

➢ 如果修改类时仅仅修改了静态变量或瞬态实例变量,则反序列化不受任何影响,类定义无须修改serialVersionUID类变量的值。

➢ 如果修改类时修改了非瞬态的实例变量,则可能导致序列化版本不兼容。

如果对象流中的对象和新类中包含同名的实例变量,而实例变量类型不同,则反序列化失败,类定义应该更新serialVersionUID类变量的值。如果对象流中的对象比新类中包含更多的实例变量,则多出的实例变量值被忽略,序列化版本可以兼容,类定义可以不更新serialVersionUID类变量的值;如果新类比对象流中的对象包含更多的实例变量,则序列化版本也可以兼容,类定义可以不更新serialVersionUID类变量的值;但反序列化得到的新对象中多出的实例变量值都是null(引用类型实例变量)或0(基本类型实例变量)。
在这里插入图片描述
在这里插入图片描述

15.9 NIO

15.9.1 Java新IO概述

新IO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。

Java中与新IO相关的包如下。

➢ java.nio包:主要包含各种与Buffer相关的类。

➢ java.nio.channels包:主要包含与Channel和Selector相关的类。

➢ java.nio.charset包:主要包含与字符集相关的类。

➢ java.nio.channels.spi包:主要包含与Channel相关的服务提供者编程接口。

➢ java.nio.charset.spi包:包含与字符集相关的服务提供者编程接口。

Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出系统是面向流的处理,则新IO则是面向块的处理。

Buffer可以被理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。

除Channel和Buffer之外,新IO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入/输出的Selector类。

15.9.2 使用Buffer

从内部结构上来看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作。

除ByteBuffer之外,对应于其他基本数据类型(boolean除外)都有相应的Buffer类:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。

这些Buffer类都没有提供构造器,通过使用如下方法来得到一个Buffer对象:

➢ static XxxBuffer allocate(int capacity):创建一个容量为capacity的XxxBuffer对象。

但实际使用较多的是ByteBuffer和CharBuffer,其他Buffer子类则较少用到。其中ByteBuffer类还有一个子类:MappedByteBuffer,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回。

在Buffer中有三个重要的概念:容量(capacity)、界限(limit)和位置(position)。

➢ 容量(capacity):缓冲区的容量(capacity)表示该Buffer的最大数据容量,即最多可以存储多少数据。

➢ 界限(limit):第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可被读,也不可被写。

➢ 位置(position):用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于IO流中的记录指针)。

当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。当刚刚新建一个Buffer对象时,其position为0;如果从Channel中读取了2个数据到该Buffer中,则position为2,指向Buffer中第3个(第1个位置的索引为0)位置。除此之外,Buffer里还支持一个可选的标记(mark,类似于传统IO流中的mark),Buffer允许直接将position定位到该mark处。这些值满足如下关系:

在这里插入图片描述

Buffer的主要作用就是装入数据,然后输出数据(其作用类似于前面介绍的取水的“竹筒”),开始时Buffer的position为0,limit为capacity,程序可通过put()方法向Buffer中放入一些数据(或者从Channel中获取一些数据),每放入一些数据,Buffer的position相应地向后移动一些位置。当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0,这就使得Buffer的读写指针又移到了开始位置。也就是说,Buffer调用flip()方法之后,Buffer为输出数据做好准备;当Buffer输出数据结束后,Buffer调用clear()方法,clear()方法不是清空Buffer的数据,它仅仅将position置为0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备。

15.9.3 使用Channel

Channel类似于传统的流对象,但与传统的流对象有两个主要区别。

➢ Channel可以直接将指定文件的部分或全部直接映射成Buffer。

➢ 程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。也就是说,如果要从Channel中取得数据,必须先用Buffer从Channel中取出一些数据,然后让程序从Buffer中取出这些数据;如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channel中。

Java为Channel接口提供了DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SelectableChannel、ServerSocketChannel、SocketChannel等实现类,本节主要介绍FileChannel的用法。根据这些Channel的名字不难发现,新IO里的Channel是按功能来划分的,例如Pipe.SinkChannel、Pipe.SourceChannel是用于支持线程之间通信的管道Channel;ServerSocketChannel、SocketChannel是用于支持TCP网络通信的Channel;而DatagramChannel则是用于支持UDP网络通信的Channel。

所有的Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。

Channel中最常用的三类方法是map()、read()和write(),其中map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer;而read()或write()方法都有一系列重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。

15.9.4 字符集和Charset

15.9.5 文件锁

文件锁在操作系统中是很平常的事情,如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件,所以现在的大部分操作系统都提供了文件锁的功能。

从JDK 1.4的NIO开始,Java开始提供文件锁的支持。

在NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件。lock()和tryLock()方法存在区别:当lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,该方法则返回该文件锁,否则将返回null。

如果FileChannel只想锁定文件的部分内容,而不是锁定全部内容,则可以使用如下的lock()或tryLock()方法。

➢ lock(long position, long size, boolean shared):对文件从position开始,长度为size的内容加锁,该方法是阻塞式的。

➢ tryLock(long position, long size, boolean shared):非阻塞式的加锁方法。参数的作用与上一个方法类似。当参数shared为true时,表明该锁是一个共享锁,它将允许多个进程来读取该文件,但阻止其他进程获得对该文件的排他锁。当shared为false时,表明该锁是一个排他锁,它将锁住对该文件的读写。程序可以通过调用FileLock的isShared来判断它获得的锁是否为共享锁。

15.10 NIO.2的功能和用法

Java 7对原有的NIO进行了重大改进,改进主要包括如下两方面的内容。

➢ 提供了全面的文件IO和文件系统访问支持。

➢ 基于异步Channel的IO。

第一个改进表现为Java 7新增的java.nio.file包及各个子包;第二个改进表现为Java 7在java.nio.channels包下增加了多个以Asynchronous开头的Channel接口和类。Java 7把这种改进称为NIO.2,本章先详细介绍NIO的第二个改进。

15.10.1 Path、Paths和Files核心API

NIO.2引入了一个Path接口,Path接口代表一个平台无关的平台路径。

除此之外,NIO.2还提供了Files、Paths两个工具类,其中Files包含了大量静态的工具方法来操作文件;Paths则包含了两个返回Path的静态工厂方法。

15.10.2 使用FileVisitor遍历文件和目录

在以前的Java版本中,如果程序要遍历指定目录下的所有文件和子目录,则只能使用递归进行遍历,但这种方式不仅复杂,而且灵活性也不高。

有了Files工具类的帮助,现在可以用更优雅的方式来遍历文件和子目录。Files类提供了如下两个方法来遍历文件和子目录。

➢ walkFileTree(Path start, FileVisitor<?super Path>visitor):遍历start路径下的所有文件和子目录。

➢ walkFileTree(Path start, Set options, int maxDepth, FileVisitor<?super Path>visitor):与上一个方法的功能类似。该方法最多遍历maxDepth深度的文件。

上面两个方法都需要FileVisitor参数,FileVisitor代表一个文件访问器,walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会“触发”FileVisitor中相应的方法。

FileVisitor中定义了如下4个方法。

➢ FileVisitResult postVisitDirectory(T dir, IOException exc):访问子目录之后触发该方法。

➢ FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs):访问子目录之前触发该方法。

➢ FileVisitResult visitFile(T file, BasicFileAttributes attrs):访问file文件时触发该方法。

➢ FileVisitResult visitFileFailed(T file, IOException exc):访问file文件失败时触发该方法。

上面4个方法都返回一个FileVisitResult对象,它是一个枚举类,代表了访问之后的后续行为。FileVisitResult定义了如下几种后续行为。

➢ CONTINUE:代表“继续访问”的后续行为。

➢ SKIP_SIBLINGS:代表“继续访问”的后续行为,但不访问该文件或目录的兄弟文件或目录。

➢ SKIP_SUBTREE:代表“继续访问”的后续行为,但不访问该文件或目录的子目录树。

➢ TERMINATE:代表“中止访问”的后续行为。

实际编程时没必要为FileVisitor的4个方法都提供实现,可以通过继承SimpleFileVisitor(FileVisitor的实现类)来实现自己的“文件访问器”,这样就根据需要、选择性地重写指定方法了。

15.10.3 使用WatchService监控文件变化

NIO.2的Path类提供了如下一个方法来监听文件系统的变化。

➢ register(WatchService watcher, WatchEvent.Kind<?>…events):用watcher监听该path代表的目录下的文件变化。events参数指定要监听哪些类型的事件。

在这个方法中WatchService代表一个文件系统监听服务,它负责监听path代表的目录下的文件变化。一旦使用register()方法完成注册之后,接下来就可调用WatchService的如下三个方法来获取被监听目录的文件变化事件。

➢ WatchKey poll():获取下一个WatchKey,如果没有WatchKey发生就立即返回null。

➢ WatchKey poll(long timeout, TimeUnit unit):尝试等待timeout时间去获取下一个WatchKey。

➢ WatchKey take():获取下一个WatchKey,如果没有WatchKey发生就一直等待。如果程序需要一直监控,则应该选择使用take()方法;如果程序只需要监控指定时间,则可考虑使用poll()方法。

15.10.4 访问文件属性

Java 7的NIO.2在java.nio.file.attribute包下提供了大量的工具类,通过这些工具类,开发者可以非常简单地读取、修改文件属性。这些工具类主要分为如下两类。

➢ XxxAttributeView:代表某种文件属性的“视图”。

➢ XxxAttributes:代表某种文件属性的“集合”,程序一般通过XxxAttributeView对象来获取XxxAttributes。

在这些工具类中,FileAttributeView是其他XxxAttributeView的父接口,下面简单介绍一下这些XxxAttributeView。

AclFileAttributeView:通过AclFileAttributeView,开发者可以为特定文件设置ACL(Access Control List)及文件所有者属性。它的getAcl()方法返回List对象,该返回值代表了该文件的权限集。通过setAcl(List)方法可以修改该文件的ACL。

BasicFileAttributeView:它可以获取或修改文件的基本属性,包括文件的最后修改时间、最后访问时间、创建时间、大小、是否为目录、是否为符号链接等。它的readAttributes()方法返回一个BasicFileAttributes对象,对文件夹基本属性的修改是通过BasicFileAttributes对象完成的。

DosFileAttributeView:它主要用于获取或修改文件DOS相关属性,比如文件是否只读、是否隐藏、是否为系统文件、是否是存档文件等。它的readAttributes()方法返回一个DosFileAttributes对象,对这些属性的修改其实是由DosFileAttributes对象来完成的。

FileOwnerAttributeView:它主要用于获取或修改文件的所有者。它的getOwner()方法返回一个UserPrincipal对象来代表文件所有者;也可调用setOwner(UserPrincipal owner)方法来改变文件的所有者。

PosixFileAttributeView:它主要用于获取或修改POSIX(Portable Operating SystemInterface of INIX)属性,它的readAttributes()方法返回一个PosixFileAttributes对象,该对象可用于获取或修改文件的所有者、组所有者、访问权限信息(就是UNIX的chmod命令负责干的事情)。这个View只在UNIX、Linux等系统上有用。

UserDefinedFileAttributeView:它可以让开发者为文件设置一些自定义属性。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


  1. 0-9 ↩︎

  2. A-Za-z ↩︎

  3. A-Z ↩︎

  4. a-z ↩︎

  5. A-Za-z0-9 ↩︎

  6. \w- ↩︎

  7. a-zA-z ↩︎

  8. a-zA-z ↩︎

  9. a-zA-Z ↩︎

  10. 1-9 ↩︎

  11. 0-9 ↩︎

  12. A-Za-z ↩︎

  13. A-Z ↩︎

  14. a-z ↩︎

  15. A-Za-z0-9 ↩︎

  16. \w- ↩︎

  17. a-zA-Z ↩︎

  18. 1-9 ↩︎

  19. \u0391-\uFFE5 ↩︎

  20. 1-9 ↩︎

  21. a-zA-Z ↩︎

  22. \u4e00-\u9fa5_a-zA-Z0-9 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值