Java基础知识
知识来源:B站-尚硅谷-宋红康
Java基础编程
一、Java语言概述
1.1 基础常识
软件:即一系列按照特定顺序组织的计算机数据和指令的集合。分为:
系统软件,如:Windows、mac OS、Linux、Android、iOS
应用软件:word、ppt......
人机交互方式:图形化界面 VS 命令行方式
应用程序 = 算法 + 数据结构
常用的DOS命令
命令 | 含义 |
---|---|
dir | 列出当前目录下的文件以及文件夹 |
md | 创建目录 |
rd | 删除目录 |
cd | 进入指定目录 |
cd.. | 退回上一级目录 |
cd\ | 退回到根目录 |
del | 删除文件 |
exit | 退出dos命令行 |
1.2 计算机语言发展的迭代史
第一代:机器语言 第二代:汇编语言 第三代:高级语言
面向过程:C、Pascal、Fortran 面向对象:Java、JS、Python、Scala
1.3 Java语言版本迭代概述
年份 | 版本 |
---|---|
1991年 | Green项目,开发语言最初命名为Oak (橡树) |
1994年 | 开发组意识到Oak 非常适合于互联网 |
1996年 | 发布JDK 1.0,约8.3万个网页应用Java技术来制作 |
1997年 | 发布JDK 1.1,JavaOne会议召开,创当时全球同类会议规模之最 |
1998年 | 发布JDK 1.2,同年发布企业平台J2EE |
1999年 | Java分成J2SE、J2EE和J2ME,JSP/Servlet技术诞生 |
2004年 | 发布里程碑式版本:JDK 1.5,为突出此版本的重要性,更名为JDK 5.0 |
2005年 | J2SE -> JavaSE,J2EE -> JavaEE,J2ME -> JavaME |
2009年 | Oracle公司收购SUN,交易价格74亿美元 |
2011年 | 发布JDK7.0 |
2014年 | 发布JDK 8.0,是继JDK 5.0以来变化最大的版本 |
2017年 | 发布JDK9.0,最大限度实现了模块化 |
1.4 Java语言应用的领域
Java Web开发:后台开发
大数据开发
Android应用程序开发:客户端开发
1.5 Java语言的特点
面向对象性:
两个要素:类、对象
三个特征:封装、继承、多态
健壮性:
去除了C语言中的指针
自动的垃圾回收机制(但仍然会出现内存溢出、内存泄漏)
跨平台性:write once,run anywhere:一次编译,到处运行。归功于JVM
二、基本语法
2.1 关键字与标识符
2.1.1 Java关键字的使用
定义:被Java语言赋予了特殊含义,用做专门用途的字符串(单词)
特点:关键字中所字母都为小写
2.1.2 保留字
具体保留字:goto、const
现Java版本尚未使用,但以后版本可能会作为关键字使用,称为保留字。自己命名标识符的时候不仅要规避关键字,也要规避保留字。
2.1.3 标识符的使用
定义:凡是自己可以起名字的地方都叫标识符
规则:(必须遵守,否则编译不通过)
规范:(可以不遵守,不影响编译和运行。但为了良好的编程习惯,强制要求遵守)
在起名字时,为了提高阅读性,要尽量有意义,“见名知意”。
2.2 变量的使用
2.2.1 变量的分类
Ⅰ. 按数据类型分类
整型:byte(1字节=8bit) short(2字节) int(4字节) long(8字节)
① byte范围:-128~127
② 声明long型变量,必须以"l"或"L"结尾(推荐使用大写的"L")
③ 通常定义整型变量时,使用int型
④ 整型的常量,默认类型时:int型
浮点型:float(4字节) double(8字节)
① 浮点型,表示带小数的数值
② float表示的数值范围比long大
③ 定义float类型变量时,变量要以"f"或"F"结尾(推荐使用大写的"F")
④ 通常定义浮点型变量时,使用double型
⑤ 浮点型的常量,默认类型为:double型
字符型:char(1字符=2字节)
① 定义char型变量,通常使用一对'',内部只能写一个字符
② 表示方式:声明一个字符 转义字符 直接使用Unicode值来表示字符型常量
布尔型:boolean
① 只能取两个值之一:true false
② 常常在条件判断、循环结构中使用
Ⅱ. 按声明的位置分类(了解)
2.2.2 定义变量的格式
① 数据类型 变量名 = 变量值;
② 数据类型 变量名;
变量名 = 变量值;
2.2.3 变量使用的注意点
① 变量必须先声明,后使用
② 变量在其作用域内,它是有效的。换句话说,出了作用域,就失效了。
③ 同一个作用域内,不可以声明两个同名的变量。
2.2.4 基本数据类型变量间运算规则
① 涉及到的基本数据类型:除了boolean之外的其他七种
② 自动类型转换(只涉及7种基本数据类型)
byte、char、short ----> int ----> long ----> float ----> double
结论:当容量小的数据类型的变量与容量大的数据类型的变量做运算时,结果自动提升为容量大的数据类型。(说明:此时的容量大小指的是,表示数的范围的大和小。比如:float容量要大于long的容量)
特别的:当byte、char、short三种类型的变量做运算时,结果为int型。
③ 强制类型转换(只涉及7种基本数据类型):自动类型提升运算的逆运算
需要使用强转符:()
注意点:强制类型转换,可能导致精度损失。
④ String与8种基本数据类型间的运算
String属于引用数据类型,翻译为:字符串
声明String类型变量时,使用一对英文双引号 ""
String可以和8种基本数据类型变量做运算,且运算只能时连接运算:+
运算的结果仍然是String类型
避免以下错误:
String s = 123;//编译错误
String s1 = "123";
int i = (int) s1;//编译错误
2.3 运算符
2.3.1 算术运算符
取反 - 连接 + 加 + 减 - 乘 * 除 / 取余 % (前)++ (后)++ (前)-- (后)--
2.3.2 赋值运算符
= += -= *= /= %=
2.3.3 比较运算符(关系运算符)
== != > < >= <= instanceof
特别说明的:
① 比较运算符的结果是boolean类型。
② > < >= <= 只能使用在数值类型的数据之间。
③ == != 不仅可以使用在数值类型数据之间,还可以使用在其他引用类型变量之间。
Account acct1 = new Account(1000);
Account acct2 = new Account(1000);
boolean b1 = (acct1 == acct2);//比较两个Account是否是同一个账户。
boolean b2 = (acct1 != acct2);//比较两个Account是否不是同一个账户。
④ instanceof:a instanceof A:判断对象a是否是类A的实例。如果是,返回 true ;如果不是,返回 false。
2.3.4 逻辑运算符
& && | || ! ^
① 区分 & 与 &&
相同的1:& 与 &&的运算结果相同
相同点2:当符号左边是 true 时,二者都会执行符号右边的运算
不同点:当符号左边时 false 时,& 继续执行符号右边的运算。&& 不再执行符号右边的运算
开发中,推荐使用 &&
② 区分 | 与||
相同点1:| 与 || 的运算结果相同
相同点2:当符号左边是 false 时,二者都会执行符号右边的运算
不同点:当符号左边时 true 时,| 继续执行符号右边的运算,而 || 不在执行符号右边的运算
开发中,推荐使用 ||
③ 逻辑运算符操作的都是boolean类型的变量。而且结果也是boolean类型
2.3.5 位运算符
<< >> >>> & | ^ ~
你能否写出最高效的2 * 8的实现方式?
答案:2 << 3 或 8 << 1
【特别说明的】
① 位运算符操作的都是整型的数据
② << :在一定范围内,每向左移1位,相当于 * 2 >>:在一定范围内,每向右移1位,相当于 / 2
2.3.6 三元运算符
(条件表达式) ? 表达式1 : 表达式2
① 说明
条件表达式的结果为boolean类型
如果表达式为 true ,则执行表达式1。如果表达式为 false ,则执行表达式2。
三元运算符可以嵌套使用
② 凡是可以使用三元运算符的地方,都可以改写为 if-else 。反之,不成立。
③ 如果某段代码既可以使用三元运算符,又可以使用 if-else 结果,优先选择三元运算符。原因:执行效率高、简洁。
2.4 流程控制
2.4.1 分支结构
① if-else
//结构一:
if(条件表达式){
执行表达式
}
//结构二:二选一
if(条件表达式){
执行表达式1
}else{
执行表达式2
}
//结构三:n选一
if(条件表达式){
执行表达式1
}else if(条件表达式){
执行表达式2
}else if(条件表达式){
执行表达式3
}
...
else{
执行表达式n
}
如果多个条件表达式之间是“互斥”关系,哪个判断和执行语句申明在上面还是下面,无所谓。不影响最终结果。
如果多个条件表达式之间有交集的关系,需要根据实际情况,考虑清楚应该将哪个结构声明在上面。
如果多个条件之间有包含的关系,需要将范围小的声明在范围大的上面,否则,范围小的就没机会执行了。
if-else 结构是可以相互嵌套的。
如果 if-else 结构中执行语句只有一行时,对应的一对{}可以省略的。但是,可读性不好,不建议这样做。
② switch-case 选择结构
switch(表达式){
case 常量1:
执行语句1;
//break;
case 常量2:
执行语句2;
//break;
...
default:
执行语句n;
//break;
}
根据 switch 表达式中的值,依次匹配各个 case 中的常量。一旦匹配成功,则进入相应的 case 结构中,调用其执行语句。当调用完执行语句以后,则仍然继续向下执行其他 case 结构中的执行语句,直到遇到 break 关键字或 switch-case 结构末尾结束为止。
break:是可选的。在 switch-case 结构中,一旦执行到此关键字,就跳出 switch-case 结构。
switch 结构中的表达式,只能是以下6种数据类型之一:
byte、short、char、int、枚举类型(JDK5.0新增)、String类型(JDK7.0新增)
default:是可选的,而且位置是灵活的;但推荐每个switch块内,都包含一个default语句并且放在最后,即使它什么代码也没有。
2.4.2 循环结构
① 循环结构的四要素
- 初始化条件
- 循环条件 ---->是 boolean 类型的
- 循环体
- 迭代条件
通常情况下,循环结束都是因为循环条件返回 false 了。
② 三种循环结构
- for 循环结构
for(①;②;④){
③
}
//执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ... - ②
- while 循环结构
①
while(②){
③;
④;
}
//执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ... - ②
- do-while 循环结构
①
do{
③;
④;
}while(②);
//执行过程:① - ③ - ④ - ② - ③ - ④ - ... - ②
do-while 循环至少会执行一次循环体。
开发中,使用 for 和 while 更多一些,较少使用 do-while。
③ 循环嵌套
将一个循环结构A声明在另一个循环结构B的循环体中,就形成了嵌套循环。
//练习一:打印矩形
/*
******
******
******
******
*/
for(int j = 1;j <= 4;j++ ){
for(int i = 1;i <= 6;i++){
System.out.print('*');
}
System.out.println();
}
//练习二:打印三角形
/* i(行号) j(*的个数)
* 1 1
** 2 2
*** 3 3
**** 4 4
***** 5 5
*/
for(int i = 1;i <= 5;i++){//控制行数
for(int j = 1;j <= i;j++){//控制列数
System.out.print("*");
}
System.out.println();
}
//练习三:九九乘法表
for(int i = 1;i <= 9;i++){
for(int j = 1;j <= i;j++){
System.out.print(i + " × " + j + " = " + (i * j) + " ");
}
System.out.println();
}
2.4.3 关键字:break和continue
循环中使用的作用
关键字 | 使用范围 | 作用 | 注意点 |
---|---|---|---|
break | switch-case 和 循环结构中 | 结束当前循环 | 关键字后面不能声明执行语句 |
continue | 循环结构中 | 结束当次循环 | 关键字后面不能声明执行语句 |
return | 整个方法内 | 结束整个方法 | 关键字后面不能声明执行语句,可携带方法所需的返回值类型 |
三、数组
3.1 数组的概述
① 数组的理解:数组(Array),是多个相同数据类型的集合,并使用一个名字命名,通过编号的方式对这些数据进行统一的管理。
② 数组相关的概念:
- 数组名
- 元素
- 角标、下标、索引
- 数组的长度:元素的个数
③ 数组的特点:
- 数组是有序排列的
- 数组属于引用数据类型的变量。数组的元素,既可以是基本数据类型,也可以是引用数据类型
- 创建数组对象会在内存中开辟一整块连续的空间
- 数组一旦初始化,其长度就是确定的;数组的长度一旦确定,就不能修改长度
④ 数组的分类
- 按照维数:一维数组、二维数组、......
- 按照数组元素的类型:基本数据类型元素的数组、引用数据类型元素的数组
3.2 一维数组
3.2.1 一维数组的声明与初始化
//正确的方式:
int num;//声明
num = 10;//初始化
int id = 1001;//声明 + 初始化
int[] ids;//声明
//1.1 静态初始化:数组的初始化和数组元素的赋值操作同时进行
ids = new int[]{1001,1002,1003,1004};
//1.2动态初始化:数组的初始化和数组元素的赋值操作分开进行
String[] names = new String[5];
int[] arr4 = {1,2,3,4,5};//类型推断
// 错误的方式:
// int[] arr1 = new int[];
// int[5] arr2 = new int[5];
// int[] arr3 = new int[3]{1,2,3};
3.2.2 一维数组元素的引用
通过角标的方式调用。数组的角标从0开始,到数组长度减 1 结束。
3.2.3 数组的属性:length
通过 数组名.length 来获得数组的长度。
String[] names = new String[5];
System.out.println(names.length);//5
3.2.4 一维数组的遍历
String[] names = new String[5];
names[0] = "王铭";
names[1] = "王赫";
names[2] = "张学良";
names[3] = "孙居龙";
names[4] = "王宏志";
//普通 for 循环。快捷键:正序:names.fori 逆序:names.forr
for(int i = 0;i < names.length;i++){
System.out.println(names[i]);
}
//增强 for 循环。快捷键:names.iter 只能正序
for (String name : names) {
System.out.println(name);
}
3.2.5 一维数组元素的默认初始化值
- 数组元素是整型:0
- 数组元素是浮点型:0.0
- 数组元素是char型:0或'\u0000',而非'0'
- 数组元素是boolean型:false
- 数组元素是引用数据类型:null
3.2.6 一维数组的内存解析
3.3 二维数组
3.3.1 如何理解二维数组?
数组属于引用数据类型;数组的元素也可以是引用数据类型。
一个一维数组A的元素如果还是一个一维数组类型的,则此数组A称为二维数组。
3.3.2 二维数组的声明和初始化
//正确的方式:
int[] arr = new int[]{1,2,3};//一维数组
//静态初始化
int[][] arr1 = new int[][]{{1,2,3},{4,5},{6,7,8}};
//动态初始化1
String[][] arr2 = new String[3][2];
//动态初始化2
String[][] arr3 = new String[3][];
//也是正确的写法:
int[] arr4[] = new int[][]{{1,2,3},{4,5,9,10},{6,7,8}};
int[] arr5[] = {{1,2,3},{4,5},{6,7,8}};//类型推断
//错误的方式:
// String[][] arr4 = new String[][4];
// String[4][3] arr5 = new String[][];
// int[][] arr6 = new int[4][3]{{1,2,3},{4,5},{6,7,8}};
3.3.3 如何调用二维数组元素:
System.out.println(arr1[0][1]);//2
System.out.println(arr2[1][1]);//null
arr3[1] = new String[4];
System.out.println(arr3[1][0]);//null
System.out.println(arr3[0]);//null
3.3.4 二维数组的属性
System.out.println(arr4.length);//3
System.out.println(arr4[0].length);//3
System.out.println(arr4[1].length);//4
3.3.5 遍历二维数组
for(int i = 0;i < arr4.length;i++){
for(int j = 0;j < arr4[i].length;j++){
System.out.print(arr4[i][j] + " ");
}
System.out.println();
}
3.3.6 二维数组元素的默认初始化值
-
int[][] arr = new int[4][3]; //针对于此种方式一: //外层元素的初始化值:地址值 //内层元素的初始化值:与一维数组初始化值一致
-
int[][] arr = new int[4][]; //针对于此种方式二: //外层元素的初始化值:null //内层元素的初始化值:不能调用,否则报错。因为外层元素为null,相当于null调用会报错。
3.3.7 二维数组的内存解析
3.4 数组的常见算法
3.4.1元素的创建与元素的赋值:
- 杨辉三角(二维数组)
public static void main(String[] args) {
final int NMAX = 10;
int[][] odds = new int[NMAX + 1][];
for (int n = 0; n <= NMAX; n++) {
odds[n] = new int[n + 1];
}
for (int n = 0; n < odds.length; n++) {
for (int k = 0; k < odds[n].length; k++) {
int lotteryOdds = 1;
for (int i = 1; i <= k; i++) {
lotteryOdds = lotteryOdds * (n - i + 1) / i;
}
odds[n][k] = lotteryOdds;
}
}
for (int[] row : odds) {
for (int odd : row) {
System.out.printf("%4d", odd);
}
System.out.println();
}
}
- 回形数(二维数组)
public static void main(String[] args) {
int vol = 10;
int[][] arr = new int[vol][vol];
int x = 0, y = 0;
int num = 1;
while (arr[x][y] != (vol * vol)) {
arr[x][y] = num;
if (y != vol - 1 && arr[x][y + 1] == 0) {//向右
if (x > 0 && arr[x - 1][y] == 0) {
x--;//优先向上走
} else {
y++;//然后向右走
}
} else if (x != vol - 1 && arr[x + 1][y] == 0) {//向下
x++;
} else if (y != 0 && arr[x][y - 1] == 0) {//向左
y--;
} else if (arr[x - 1][y] == 0) {//向上
x--;
}
num++;
}
for (int[] ints : arr) {
for (int anInt : ints) {
System.out.print(anInt + "\t");
}
System.out.println();
}
}
3.4.2 针对数值型的数组:
求最大值、最小值、总和、平均数等
3.4.3 数组的赋值与复制
int[] array1,array2;
array1 = new int[]{1,2,3,4};
- 赋值:将array1保存的数组的地址值赋给了array2,使得array1和array2共同指向堆空间中的同一个数组实体。
array2 = array1;
- 复制:通过new的方式,给array2在堆空间中新开辟了数组的空间。将array1数组中的元素值一个一个的赋值到array2数组中。
array2 = new int[array1.length];
for(int i = 0;i < array2.length;i++){
array2[i] = array1[i];
}
3.4.4 数组元素的反转
//方法一:
for(int i = 0;i < arr.length / 2;i++){
String temp = arr[i];
arr[i] = arr[arr.length - i -1];
arr[arr.length - i -1] = temp;
}
//方法二:
for(int i = 0,j = arr.length - 1;i < j;i++,j--){
String temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
3.4.5 数组中指定元素的查找:搜索、检索
-
线性查找:
实现思路:通过遍历的方式,一个一个的数据进行比较、查找。
适用性:具有普遍适用性
-
二分法查找:
实现思路:每次比较中间值,折半的方式检索。
适用性:数组必须有序才适用
冒泡排序
int[] arr = new int[]{43,32,76,-98,0,64,33,-21,32,99};
//冒泡排序
for(int i = 0;i < arr.length - 1;i++){
for(int j = 0;j < arr.length - 1 - i;j++){
if(arr[j] > arr[j + 1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
3.5 Arrays工具类的使用
3.5.1 理解
Arrays 定义在 java.util 包下;提供了很多操作数组的方法。
3.5.2 使用
//1.boolean equals(int[] a,int[] b):判断两个数组是否相等。
int[] arr1 = new int[]{1,2,3,4};
int[] arr2 = new int[]{1,3,2,4};
boolean isEquals = Arrays.equals(arr1, arr2);
System.out.println(isEquals);
//2.String toString(int[] a):输出数组信息。
System.out.println(Arrays.toString(arr1));
//3.void fill(int[] a,int val):将指定值填充到数组之中。
Arrays.fill(arr1,10);
System.out.println(Arrays.toString(arr1));
//4.void sort(int[] a):对数组进行排序。
Arrays.sort(arr2);
System.out.println(Arrays.toString(arr2));
//5.int binarySearch(int[] a,int key)使用二分搜索法来搜索升序的数组指定元素的索引
int[] arr3 = new int[]{-98,-34,2,34,54,66,79,105,210,333};
int index = Arrays.binarySearch(arr3, 210);
if(index >= 0){
System.out.println(index);
}else{
System.out.println("未找到");
}
3.6 数组的常见异常
3.6.1 数组角标越界异常
ArrayIndexOutOfBoundsException
int[] arr = new int[]{1,2,3,4,5};
for(int i = 0;i <= arr.length;i++){
System.out.println(arr[i]);//角标越界
}
System.out.println(arr[-2]);//角标越界
3.6.2 空指针异常
NullPointerException
//情况一:
int[] arr1 = new int[]{1,2,3};
arr1 = null;
System.out.println(arr1[0]);
//情况二:
int[][] arr2 = new int[4][];
System.out.println(arr2[0][0]);
//情况三:
String[] arr3 = new String[]{"AA","BB","CC"};
arr3[0] = null;
System.out.println(arr3[0].toString());
四、面向对象-上
4.1 类与对象
4.1.0 几个概念的使用说明
-
万事万物皆对象
-
属性 = 成员变量 = field = 域、字段
-
方法 = 成员方法 = 函数 = method
-
创建类的对象 = 类的实例化 = 实例化类
4.1.1 面向对象学习的三条主线
- Java类及类的成员:属性、方法、构造器;代码块、内部类
- 面向对象的三大特征:封装性、继承性、多态性、(抽象性)
- 其他关键字:this、super、static、final、abstract、interface、package、import等
4.1.2 面向对象与面向过程的对比
- 面向过程:强调的是功能行为,以函数为最小单位,考虑怎么做。
- 面向对象:强调具备功能的对象,以类/对象为最小单位,考虑谁来做。
4.1.3 完成一个项目(或功能)的思路
根据问题需要,选择问题所针对的现实世界中的实体。
从实体中寻找解决问题相关的属性和功能,这些属性和功能就形成了概念世界中的类。
把抽象的实体用计算机语言进行描述,形成计算机世界中类的定义。即借助某种程序语言,把类构造成计算机能够识别和处理的数据结构。
将类实例化成计算机世界中的对象。对象是计算机世界中解决问题的最总工具。
4.1.4 面向对象中的两个重要的概念
- 类:对一类事物的描述,是抽象的、概念上的定义
- 对象:是实际存在的该类事物的每个个体,因而也称为实例(instance)
- 面向对象程序设计的重点是类的设计;设计类,就是设计类的成员
- 对象,是由类new出来的,派生出来的。
- 类是对象的抽象,对象是类的实例。
4.1.5 面向对象思想落地实现的规则一
- 创建类,设计类的成员
- 创建类的对象
- 通过 对象.属性 或 对象.方法 调用对象的结构
4.1.6 对象的创建与对象的内存解析
Person p1 = new Person();
Person p2 = new Person();
Person p3 = p1;//没有新创建一个对象,共用一个堆空间中的对象实体。
如果创建(new)了一个类的多个对象,则每个对象都独立的拥有一套类的属性(非 static 的)。意味着:如果修改一个对象的属性a,不影响另外一个对象的属性a的值。
4.1.7 匿名对象
- 我们创建的对象,没显示的赋给它一个变量名,即为匿名对象。
- 特点:匿名对象只能使用一次。
//举例:
new Phone().sendEmail();
new Phone().playGame();
new Phone().price = 1999;
new Phone().showPrice();//0.0
//应用场景:
PhoneMall mall = new PhoneMall();
//匿名对象的使用
mall.show(new Phone());
//其中
class PhoneMall{
public void show(Phone phone){
phone.sendEmail();
phone.playGame();
}
}
4.2 类的重要结构之一:属性
对比:属性 vs 局部变量
- 相同点:
- 定义变量的格式:数据类型 变量名 = 变量值;
- 先声明,后使用
- 变量都有其对应的作用域
- 不同点:
-
在类中的声明的位置的不同
属性:直接定义在类的一对{}内
局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量
-
关于权限修饰符的不同
属性:可以在声明属性是,指明其权限,使用权限修饰符。
常用的权限修饰符:private、public、缺省、protected局部变量:不可以使用权限修饰符。
-
默认初始化值的情况
属性:类的属性,根据其类型,都有默认初始化值。
-
整型(byte、short、int、long:0)
-
浮点型(float、double:0.0)
-
字符型(char:0 (或'\u0000'))
-
布尔型(boolean:false)
-
引用数据类型(类、数组、接口:null)
局部变量:没有默认初始化值。意味着,在调用局部变量之前,一定要显示赋值。特别的:形参在调用时赋值即可。
-
-
在内存中加载的位置
属性:加载到堆空间中(非static)
局部变量:加载到栈空间
4.3 类的重要结构之二:方法
方法:描述类应该具有的功能
4.3.1 举例
public void eat(){}
public void sleep(int hour){}
public String getName(){}
public String getNation(String nation){}
4.3.2 方法的声明
权限修饰符 返回值类型 方法名(形参列表) {
方法体
}
可以使用 static、final、abstract关键字来修饰方法。
4.3.3 说明
- 关于权限修饰符:
一般情况下使用public即可。 - 返回值类型:
有返回值 vs 没返回值- 如果方法有返回值,则必须在方法声明时,指定返回值的类型。同时在方法中需要使用 return 关键字来返回指定类型的变量或常量:return 数据;
- 如果方法没返回值,则方法声明时,使用 void 来表示。通常没返回值的方法中,不需要使用 return 。但是如果使用 return; 则表示结束此方法。
- 方法名:
属于标识符,遵守规则和规范,并且应“见名知意”。 - 形参列表:
方法可以声明0个、1个、或多个形参。- 格式:数据类型1 形参1, 数据类型2 形参2, ......
- 方法体:
方法功能的体现
4.3.4 方法的使用
-
方法在使用中,可以调用当前类的属性或方法。
-
特殊的:方法A中又调用了方法A:递归方法
-
方法中,不可以定义方法
4.4 面向对象的特征一:封装性
4.4.1 为什么要引入封装性?
- 程序设计追求“高内聚,低耦合”
- 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;
- 低耦合:仅对外暴露少量的方法用于使用
- 隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可维护性、可扩展性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
4.4.2 问题引入
当我们创建一个类的对象以后,我们可以通过 “对象.属性” 的方式,对对象的属性进行赋值。这里,赋值操作要受到属性的数据类型和存储范围的制约。除此之外,没其他的制约条件。但是,在实际问题中,我们往往需要给属性赋值加上额外的限制条件。这个条件就不能在属性声明时体现,我们只能通过方法进行限制条件的添加。比如:setName()同时,我们要避免用户再使用 “对象.属性” 的方式对属性进行赋值。则需要将属性声明为私有的(private)。
此时,针对于属性就体现了封装性。
4.4.3 封装性思想具体的代码体现
-
体现一:
将类的属性 xxx 私有化(private),同时提供公共的(public)方法来获取(getXxx)和设置(setXxx)此属性的值。private double radius; public void setRadius(double radius){ this.radius = radius; } public double getRadius(){ return radius; }
-
体现二:
不对外暴露的私有的方法 -
体现三:
单例设计模式(将构造器私有化) -
体现四:
如果不希望类在包外被调用,可以将类设置为缺省的。
4.4.4 Java 规定的四种权限修饰符
-
权限从小到大顺序为:private < 缺省 < protected(受保护的) < public
-
具体的可访问范围
修饰符 类内部 同一个包 不同包的子类 同一个工程 private yes (缺省) yes yes protected yes yes yes public yes yes yes yes -
权限修饰符可用来修饰的结构
4种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类
修饰类的话,只能使用:缺省、public
4.5 类的结构之三:构造器
构造器又称构造方法:Constructor
4.5.1 构造器的作用
- 创建对象
- 初始化对象的信息
4.5.2 使用说明
-
如果没有显式的定义类的构造器的话,系统默认提供一个空参的构造器
-
定义构造器的格式:权限修饰符 类名(形参列表){}
-
一个类种定义的多个构造器,彼此构成重载
-
一旦显式的定义了类的构造器之后,系统就不再提供默认的空参构造器
-
一个类中,至少会有一个构造器
-
举例
//构造器 public Person(){ System.out.println("Person()....."); } public Person(String name){ this.name = name; } public Person(String name,int age){ this.name = name; this.age = age; }
属性的赋值顺序
先后顺序为:
- 默认初始化
- 显式初始化
- 构造器中初始化
- 通过"对象.方法" 或 "对象.属性"的方式,赋值
JavaBean的概念
所谓JavaBean,是指符合如下标准的Java类:
- 类是公共的
- 一个公共的空参构造器
- 有属性,且有对应的get、set方法
4.6 关键字:this
this 理解为:当前对象 或 当前正在创建的对象
可以调用的结构:属性、方法;构造器
4.6.1 this 调用属性、方法
- 在类的方法 (或构造器) 中
可以使用 “this.属性” 或 “this.方法” 的方式,调用当前对象属性或方法。但是,通常情况下选择省略 “this.”。特殊情况下,如果方法 (或构造器) 的形参和类的属性同名时,必须显式的使用 “this.变量” 的方式,表明此变量是属性,而非形参。
4.6.2 this 调用构造器
- 在类的构造器中,可以显式的使用 “this(形参列表)” 的方式,调用本类中指定的其他构造器
- 构造器中不能调自己;构造器A调用了构造器B,则构造器B就不能调用构造器A
- 如果一个类中有n个构造器,则最多有 n - 1 个构造器中使用 “this(形参列表)”
- 规定:“this(形参列表)” 必须声明在构造器的首行
- 构造器的内部,最多只能使用一个 “this(形参列表)”,用来调用其他的构造器
4.7 关键字:package、import
4.7.1 package
-
使用说明:
- 为了更好的实现项目中类的管理,提供了包的概念
- 使用 package 声明类或接口所属的包,声明在源文件的首行
- 包,属于标识符,遵循标识符的命名规则、规范(xxxyyyzzz)、”见名知意“
- 每 ” . “ 一次,就代表一层文件目录。
-
JDK 中的主要包介绍
4.7.2 import的使用
import:导入
- 在源文件中显式的使用import结构导入指定包下的类、接口
- 声明在包的声明和类的声明之间
- 如果需要导入多个结构,则并列写出即可
- 如果使用 “xxx.*”的方式,表示可以导入xxx包下的所有结构
- 如果使用类或接口是 java.lang 包下定义的,则可以省略 import 关键字
- 如果使用类或接口时本包下定义的,则可以省略 import 关键字
- 如果在源文件中,使用了不同包下的同名的类,则必须有至少一个类需要以全类名的方式显示
- 使用 ”xxx.*” 方式表明可以调用xxx包下的所有结构。但是如果使用的是xxx子包下的结构,则仍需要显式导入
- import static :导入指定类或接口中的静态结构:属性或方法
五、面向对象-中
5.1 面向对象的特征二:继承性
5.1.1 为什么要有类的继承性?(继承性的好处)
- 减少代码的冗余,提高代码的复用性
- 便于功能的扩展
- 为多态的使用,提供了前提
5.1.2 继承性的格式
class A extends B{
//定义自己的东西
//重写需要的父类的方法
}
// A:子类、派生类、subclass
// B:父类、超类、基类、superclass
5.1.3 一个类继承另一个类之后有哪些变化
- 体现:一旦子类A继承父类B以后,子类A就获取了父类B中声明的所有的属性和虚方法表中的方法
虚方法表:非private、非static、非final - 父类中声明为private的属性,子类继承父类以后,仍然获取了父类的私有属性。只是因为封装性的影响,使得子类不能直接调用而已。
- 父类的构造器不能被子类继承
- 子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的扩展
子类和父类的关系,不同于子集和集合的关系
extends:延展、扩展
5.1.4 Java 中继承性的说明
- 一个类可以被多个类继承
- 一个类只能有一个父类:Java 中类的单继承性
- 字符类是相对的概念
- 子类直接继承的父类称为:直接父类。间接继承的父类称为:间接父类
- 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的可继承的属性和方法
5.1.5 java.lang.Object 类的理解
- 如过没有显式的声明一个类的父类的话,则此类继承于 java.lang.Object 类
- 所有的java类都直接或间接的继承于 java.lang.Object 类(除了它本身)
- 意味着,所有的java类都具有 java.lang.Object 类声明的功能
5.2 方法的重写
5.2.1 什么是方法的重写(override 或 overwrite)?
子类在继承父类以后,可以对父类中同名同参数的方法进行覆盖操作。
5.2.2 应用
重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法是,实际执行的是子类重写父类后的方法。
5.2.3 举例
class Circle{
public double findArea(){}//求面积
}
class Cylinder extends Circle{
public double findArea(){}//求表面积
}
***************
class Account{
public boolean withdraw(double amt){}
}
class CheckAccount extends Account{
public boolean withdraw(double amt){}
}
5.2.4 重写的规则
-
方法的声明:权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型{
方法体
} -
约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法
①子类重写的方法名和形参列表与父类被重写的方法的方法名和形参列表相同
②子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符
特殊情况:子类不能重写父类中声明为private权限的方法
③子类重写的方法的返回值类型,只能是父类被重写的方法的返回值类型或是其子类。
父类被重写的方法的返回值是void,则子类重写的方法的返回值只能是void。
父类被重写的方法的返回值是基本数据类型(如:double),则子类重写的方法的返回值类型必须是相同的返回值类型(必须也是double)
④子类重写的方法抛出的异常类型不能严于父类被重写的方法抛出的异常类型。
5.2.5 重写与重载的区别
-
方法重载:在同一个类中,方法名相同,形参列表不同,与返回值和访问修饰符无关。
-
方法重写:在子类中重写父类的方法,方法名、形参列表相同,返回值相同或是其子类,访问修饰符不能严于父类,抛出的异常类型不能严于父类被重写的方法抛出的异常类型。
5.3 关键字:super
-
可以理解为:父类的
-
可以用来调用的结构:属性、方法、构造器
-
super调用属性、方法
可以在子类的方法或构造器中。通过使用 “super.属性” 或 “super.方法” 的方式,显示的调用父类中声明的属性和方法。但是,通常情况下,习惯省略 “super.”
特殊情况1:当子类和父类定义了同名的属性时,要想在子类中调用父类声明的属性,则必须显示的使用 “super.属性” 的方式,表明调用的是父类中声明的属性。
特殊情况2:当子类重写了父类中的方法以后,要想在子类的方法中调用父类中被重写的方法时,则必须显示的使用 “super.方法” 的方式,表明调用的时父类中被重写的方法。
-
super调用构造器
可以在子类的构造器中显示的使用 “super(形参列表)” 的方式,调用父类中声明的指定的构造器。
“super(形参列表)” 的使用,必须声明在子类构造器的首行。
在类的构造器中,针对于 “this(形参列表)” 或 “super(形参列表)” 只能二选一,不能同时出现。
在类的构造器中,如果没有显示的声明 “this(形参列表)” 或 “super(形参列表)” ,则默认调用的是父类中空参的构造器:super() 。
-
5.4 子类实例化全过程
理解即可。
-
从结果上来看:继承性
子类继承父类以后,就获取了父类中声明的属性或方法。
创建对象的对象,在堆空间中,就会加载所有父类中声明的属性。
-
从过程上看:
当通过子类的构造器创建子类对象时,一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器......直到调用了 java.lang.Object 类中空参构造器为止。正因为加载过所有的父类的结构,子类对象才可以考虑进行调用。
-
强调说明:虽然创建对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为 new 的子类对象。
5.5 面向对象的特征三:多态性
5.5.1 多态性的理解
-
多态性的理解:可以理解为一个事物的多种形态
-
何为多态性:对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)举例:
Person p = new Man();
Object obj = new Date(); -
多态性的使用:虚拟方法调用
在编译器,只能调用父类中声明的方法,但在运行期,实际执行的使用子类重写的方法。
总结:编译看左边;运行看右边。
-
多态的使用前提:①类的继承关系 ②方法的重写
-
多态性的应用举例:使用父类作为方法的形参列表。使用父类作为方法返回值的类型。
-
多态使用的注意顶:对象的多态性,只适用于方法,不适用于属性(属性编译和运行都看左边)
-
关于向上转型与向下转型:
向上转型:多态
向下转型:
为什么使用向下转型:
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法,但是由于变量声明时为父类类型,导致编译时,只能调用父类中声明的属性和方法;子类特有的属性和方法不能调用,所以使用向下转型才能调用子类特有的属性和方法。如何实现向下转型:使用强制类型转换符:()
使用时的注意点:
①:使用强转时,可能出现 ClassCastException 的异常。
②:为了避免在向下转型时出现 ClassCastException 的异常,在向下转型之前,先进行 instanceof 的判断,返回 true 进行向下转型;返回 false 不进行向下转型。instanceof 的使用:
①:a instanceof A:判断对象a是否是类A的实例。如果是则返回true、否则返回false。
②:假设类B是类A的子类。如果 a instanceof A 返回true,则 a instanceof B 也返回true。③:要求 a 所属的类与类 A 必须是子类和父类的关系,否则编译错误。
5.5.2 谈谈你多多态性理解?
多态分为编译时多态(方法重载)和运行时多态(方法重写)。
要实现多态需要做两件事:一是子类继承父类并重写父类中的方法;二是用父类型引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象而表现出不同的行为。
5.6 Object 类的使用
5.6.1 java.lang.Object 类的说明
- Object 类是所有Java类的根父类
- 如果在类的声明中未使用 extends 关键字指明其父类,则默认父类未 java.lang.Object 类
- Object 类中的功能(属性、方法)就具有通用性
属性:无
方法:equals() 、toString()、getClass()、hashCode()、clone()、finalize()、wait()、notify()、notifyall()
5.6.2 equals()方法
① equals() 的使用:
-
是一个方法,而非运算符
-
只能适用于引用数据类型
-
Object类中 equals()的定义:
public boolean equals(Object obj) { return (this == obj); }
说明:Object 类中定义的 equals() 和 == 的作用是相同的:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体
-
像String、Date、File、包装类等重写了Object类中equals()方法。重写以后,比较的不是两个引用的地址是否相同,而是比较两个对象的 “实体内容” 是否相同。
-
通常情况下,自定义的类如果使用 equals() 的话,是比较两个对象的 “实体内容” 是否相同。那么就需要对 Object 类中的 equals() 进行重写。
重写的原则:比较两个对象的实体内容是否相同。
② 如何重写 equals()
-
手动重写举例:
class User{ String name; int age; //重写其equals()方法 public boolean equals(Object obj){ if(obj == this){ return true; } if(obj instanceof User){ User u = (User)obj; return this.age == u.age && this.name.equals(u.name); } return false; } }
-
开发中如何实现:自动生成的。
③ 回顾 == 运算符的使用
- ==:运算符
- 可以使用在基本数据类型变量和应用数据类型变量中
- 如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同,如 1 == 1.0)
- 如果比较的是引用数据类型变量:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体
- 补充:== 符号使用时,必须保存符号左右两边的变量类型一致(如 1 == “1” 就报错,基本数据类型跟基本数据类型比)
5.6.3 toString() 方法
① toString()的使用:
-
当我们输出一个对象的引用时,实际上就是调用当前对象的toString()
-
Object 类中 toString() 的定义:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
-
像String、Date、File、包装类等都重写了 Object 类中的 toString() 方法。使得在调用对象的 toString() 时,返回”实体内容“ 信息。
-
自定义类也可以重写 toString() 方法,当调用此方法时,返回对象的”实体内容“。
② 如何重写toString()
//自动实现
@Override
public String toString() {
return "Customer [name=" + name + ", age=" + age + "]";
}
5.7 包装类的使用
5.7.1 为什么要要有包装类(或封装类)
- 为了使基本数据类型的变量具有类的特征,引入包装类。
5.7.2 基本数据类型与对应的包装类
5.7.3 需要掌握的类型间的转换:(基本数据类型、包装类、String)
- 基本数据类型 <---->包装类:JDK5.0 新特性:自动装箱 与 自动拆箱
- 基本数据类型、包装类 ----> String:调用String重载的 valueOf (Xxx xxx)
- String ----> 基本数据类型、包装类:调用包装类的 parseXxx(String s)
- 注意点:转换时,可能会报NumberFormatException
单元测试
- 1.中当前工程 - 右键选择:build path - add libraries - JUnit 4 - 下一步
- 2.创建Java类,进行单元测试。此时的Java类要求:① 此类是public的 ②此类提供公共的无参的构造器
- 3.此类中声明单元测试方法。此时的单元测试方法:方法的权限是public,没返回值,没形参
- 4.此单元测试方法上需要声明注解:@Test,并在单元测试类中导入:import org.junit.Test;
- 5.声明好单元测试方法以后,就可以在方法体内测试相关的代码。
- 6.写完代码以后,左键双击单元测试方法名,右键:run as -JUnit Test
- 说明:
- 1.如果执行结果没任何异常:绿条
- 2.如果执行结果出现异常:红条
六、面向对象-下
6.1 关键字 static
-
可以用来修饰的结构:主要用来修饰类的内部结构
-
static修饰属性:静态变量(或类变量)
-
属性,是否使用static修饰,又分为:静态属性 vs 非静态属性(实例变量)
实例变量:创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。当修改其中一个对象的非静态属性时,不会导致其他对象中同样的属性值的修改。
静态变量:创建了类的多个对象,多个对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。 -
static 修饰属性的其他说明:
① 静态变量随着类的加载而加载。可以通过”类.静态变量“的方式进行调用
② 静态变量的加载要早于对象的创建。
③ 由于类只会加载一次,则静态变量在内存中也只会存在一份;存在方法区的静态域中。
④ 类、对象能否直接调用类变量 实例变量 类 能 不能 对象 能 能 -
静态属性举例:System.out 、Math.Pi
-
-
静态变量内存解析:
-
static修饰方法:静态方法、类方法
-
随着类的加载而加载,可以通过 ”类.静态方法“ 的方式进行调用
-
类、对象能否直接调用
类变量 实例变量 类 能 不能 对象 能 能 - 静态方法中,只能调用静态的方法或属性
非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性
-
-
static 的注意点
- 在静态的方法内,不能使用 this 关键字、super 关键字
- 关于静态属性和静态方法的使用,都从生命周期的角度去理解
-
如何判定属性和方法应该使用 static 关键字
- 关于属性:
属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
类中的常量也常常声明为 static。 - 关于方法
操作静态属性的方法,通常设置为 static 的
工具类中的方法,习惯上声明为 static 的。比如:Math、Arrays、Collections。
- 关于属性:
-
代码举例
-
举例一:Arrays、Math、Collections等工具类
-
举例二:单例模式
-
举例三
class Circle{ private double radius; private int id;//自动赋值 public Circle(){ id = init++; total++; } public Circle(double radius){ this(); // id = init++; // total++; this.radius = radius; } private static int total;//记录创建的圆的个数 private static int init = 1001;//static声明的属性被所对象所共享 public double findArea(){ return 3.14 * radius * radius; } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } public int getId() { return id; } public static int getTotal() { return total; } }
-
6.1 拓展--单例模式
① 设计模式的说明
-
理解
设计模式是在大量实践中总结和理论化之后优化的代码结构、编程风格、以及解决问题的思考方式 -
常用设计模式--22中经典的设计模式 GOF
模式类型 具体 创建型模式(共5种) 工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式 结构性模式(共7种) 适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式 行为型模式(共11种) 策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
② 单例模式
-
要解决的问题:
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例。 -
具体代码:
饿汉式1: class Bank{ //1.私化类的构造器 private Bank(){ } //2.内部创建类的对象 //4.要求此对象也必须声明为静态的 private static Bank instance = new Bank(); //3.提供公共的静态的方法,返回类的对象 public static Bank getInstance(){ return instance; } } 饿汉式2:使用了静态代码块 class Order{ //1.私化类的构造器 private Order(){ } //2.声明当前类对象,没初始化 //4.此对象也必须声明为static的 private static Order instance = null; static{ instance = new Order(); } //3.声明public、static的返回当前类对象的方法 public static Order getInstance(){ return instance; } } 懒汉式: class Order{ //1.私化类的构造器 private Order(){ } //2.声明当前类对象,没初始化 //4.此对象也必须声明为static的 private static Order instance = null; //3.声明public、static的返回当前类对象的方法 public static Order getInstance(){ if(instance == null){ instance = new Order(); } return instance; } }
-
两种方式的对比
- 饿汉式:
坏处:对象加载时间过长
好处:饿汉式是线程安全的 - 懒汉式:
好处:延迟对象的创建
目前的写法线程不安全
- 饿汉式:
6.2 main()的使用说明
- main()方法作为程序的入口
- main()方法也是一个普通的静态方法
- main()方法可以作为我们与控制台交互的方式
- 如何将控制台获取的数据传给形参:String[] args?
运行时:java 类名 "Tom" "Jerry" "123" "true" - sysout(args[0]);//"Tom"
sysout(args[3]);//"true" -->Boolean.parseBoolean(args[3]);
sysout(args[4]);//报异常 - 小结:一叶知秋
6.3 类的结构:代码块
- 类的成员之四:代码块(初始化块)(重要性较属性、方法、构造器差一些)
- 代码块的作用:用来初始化类、对象的的信息。
- 分类:静态代码块 vs 非静态代码块
代码块要是使用修饰符,只能使用 static- 静态代码块:
- 内部可以输出语句
- 随着类的加载而执行,而且只执行一次
- 作用:初始化类的信息
- 如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
- 静态代码块的执行要优先于非静态代码块的执行
- 静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构
- 非静态代码块:
- 内部可以输出语句
- 随着对象的创建而执行
- 每创建一个对象,就执行一次非静态代码块
- 作用:可以在创建对象时,对对象的属性等进行初始化
- 如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
- 非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法
- 静态代码块:
扩展 属性赋值的顺序
- ①默认初始化
- ②显示初始化 / ⑤在代码块中赋值
- ③构造器中初始化
- ④有了对象以后,可以通过 “对象.属性” 或 “对象。方法” 的方式,进行赋值
6.4 final:最终的
- 可以用来修饰:类、方法、变量
- 具体的
- final用来修饰一个类:此类不能被其他类所继承。
比如:String类、System类、StringBuffer类 - final用来修饰方法:表明此方法不可以被重写
比如:Object类中getClass(); - fianl用来修饰变量:此时的“变量”就称为是一个常量
- final修饰属性:可以考虑赋值的位置有:显示初始化、代码块中初始化、构造器中初始化
- final修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。
- final用来修饰一个类:此类不能被其他类所继承。
- static final 用来修饰属性:全局常量
6.5 abstract:抽象的
-
可以用来修饰:类、方法
-
具体的:
- abstract修饰类:抽象类
- 此类不能实例化
- 抽象类中一定有构造器。便于子类实例化时调用(涉及:子类对象实例化的全过程)
- 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作---->抽象的使用前提:继承性
- abstract修饰方法:抽象方法
- 抽象方法只有方法的声明,没有方法体
- 包含抽象方法的类,一定时一个抽象类。反之,抽象类中则可以没有抽象方法
- 若子类重写了父类中的所有的抽象方法,此子类方可实例化。
- 若子类没有重写父类中的所有的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰
- 注意点:
- abstract不能用来修饰:属性、构造器等结构
- abstract不能用来修饰私有方法、静态方法、final的方法、final的类
- abstract修饰类:抽象类
-
abstract 的应用举例
abstract class GeometricObject{ public abstract double findArea(); } class Circle extends GeometricObject{ private double radius; public double findArea(){ return 3.14 * radius * radius; } }
扩展 模板方法的设计模式
-
解决的问题
- 在软件开发中实现一个算法是,整体步骤很固定、通用,这些步骤已经在父类中写好了,但是某些部分易变,易变部分可以抽象出来,共不同子类实现。这就是一种模板模式。
-
举例:
abstract class Template{ //计算某段代码执行所需要花费的时间 public void spendTime(){ long start = System.currentTimeMillis(); this.code();//不确定的部分、易变的部分 long end = System.currentTimeMillis(); System.out.println("花费的时间为:" + (end - start)); } public abstract void code(); } class SubTemplate extends Template{ @Override public void code() { for(int i = 2;i <= 1000;i++){ boolean isFlag = true; for(int j = 2;j <= Math.sqrt(i);j++){ if(i % j == 0){ isFlag = false; break; } } if(isFlag){ System.out.println(i); } } } }
-
应用场景
- 模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有它的影子比如常见的有:
数据库访问的封装
Junit单元测试
JavaWeb的Servlet中关于doGet/doPost方法调用
Hibernate中模板程序
SpringzhongJDBCTemlate、HebernateTemplate等
- 模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有它的影子比如常见的有:
6.6 interface:接口
6.6.1 使用说明
- 接口使用 interface 来定义
- Java 中接口和类是并列的两个结构
- 如何定义接口:定义接口中的成员
- JDK7及以前:只能定义全局常量和抽象方法
全局常量:public static final修饰的,但是书写时,可以省略不写
抽象方法:public abstract的 - JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法。
- JDK7及以前:只能定义全局常量和抽象方法
- 接口中不能定义构造器的!意味着接口不可以被实例化
- Java 开发中,接口通过让类去实现(implements)的方式来使用。
如果实现类覆盖了接口中所有的抽象方法,则此实现类就可以实例化
如果实现类没有覆盖了接口中所有的抽象方法,则此实现类仍为一个抽象类 - Java 类可以实现多个接口 ---->弥补了 Java 单继承性的局限性
格式:class AA extends BB implements CC, DD, EE - 接口和接口之间可以继承,而且可以多继承
- 接口的具体使用,体现了多态性
- 接口,实际上可以看做是一种规范
6.6.2 示例
class Computer{
public void transferData(USB usb){//USB usb = new Flash();
usb.start();
System.out.println("具体传输数据的细节");
usb.stop();
}
}
interface USB{
//常量:定义了长、宽、最大最小的传输速度等
void start();
void stop();
}
class Flash implements USB{
@Override
public void start() {
System.out.println("U盘开启工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
}
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开启工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}
- 体会:
- 1.接口使用上也满足多态性
- 2.接口,实际上就是定义了一种规范
- 3.开发中,体会面向接口编程!
面向接口编程:我们在应用程序中,调用的结构都是JDBC中定义的接口,不会出现具体某一个数据库厂商的API。
6.6.3 Java8中关于接口的新规范
-
接口中定义的静态方法,只能通过接口来调用。
-
通过实现类的对象,可以调用接口中的默认方法。如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法。
-
如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下,默认调用的是父类中同名同参数的方法---->类优先原则。
-
如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错。---->接口冲突。意味着这必须在实现类中重写此方法。
-
如何在子类(或实现类)的方法中调用父类、接口中被重写的方法。
public void myMethod(){ method3();//调用自己定义的重写的方法 super.method3();//调用的是父类中声明的 //调用接口中的默认方法 CompareA.super.method3(); CompareB.super.method3(); }
6.6.4 抽象类与接口的异同
- 相同点:不能实例化,都可以包含抽象方法。
- 不同点:
- 抽象类使用 abstract 关键字修饰。接口使用 interface 关键字定义。
- 类与类之间:单继承关系。接口与接口之间:多继承关系。类与接口之间:多实现关系。
- 抽象类中一定有构造器,便于子类实例化时调用。接口中不能有构造器。
- 抽象类可以有抽象方法和具体方法;而接口中只能有抽象方法(并且只能是 public abstract 修饰)、JDK8 开始可以有默认方法、静态方法,JDK9 开始有 private 方法。
- 抽象类中的成员权限可以是:public、默认、protected(因为抽象类中抽象方法就是为了重写,所以不能被 private 修饰);而接口中的成员只能是 public 的(方法默认:public abstract( JDK9 开始有 private 方法)、成员变量默认:public static final)。
- 抽象类中可以包含静态方法;接口在 JDK8 开始可以定义静态方法(接口中定义的静态方法,只能通过接口来调用)。
扩展 代理模式
1 解决的问题
代理模式是 JAVA 开发中使用较多的一种设计模式。代理模式就是为其他对象提供一种代理以控制对这个对象的访问。
2 举例
interface NetWork{
public void browse();
}
//被代理类
class Server implements NetWork{
@Override
public void browse() {
System.out.println("真实的服务器访问网络");
}
}
//代理类
class ProxyServer implements NetWork{
private NetWork work;
public ProxyServer(NetWork work){
this.work = work;
}
public void check(){
System.out.println("联网之前的检查工作");
}
@Override
public void browse() {
check();
work.browse();
}
}
class test{
public static void main(String[] args) {
Server server = new Server();
ProxyServer proxyServer = new ProxyServer(server);
proxyServer.browse();
}
}
3 应用场景
扩展 工厂设计模式
1 解决的问题
实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。
2 具体模式
简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)
抽象工厂模式:用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持增加产品族)
6.7 类的结构:内部类
-
内部类:类的第五个成员
-
定义:Java中允许将一个类 A 声明在另一个类 B 中,则类 A 就是内部类,类 B 称为外部类。
-
内部类的分类:
成员内部类(静态、非静态)vs 局部内部类(方法内、代码块内、构造器内) -
成员内部类的理解
- 一方面:作为外部类的成员:
可以调用外部类的结构
可以被 static 修饰
可以被 4 种不同的权限修饰 - 另一方面:作为一个类:
类内可以定义属性、方法、构造器等
可以被final修饰,表示此类不能继承,言外之意,不使用 final 就可以被继承。
可以被 abstract 修饰
- 一方面:作为外部类的成员:
-
成员内部类的使用
-
如何创建成员内部类的对象?(静态的、非静态的)
//创建静态的Dog内部类的实例(静态的成员内部类): Person.Dog dog = new Person.Dog(); //创建非静态的Bird内部类的实例(非静态的成员内部类): //Person.Bird bird = new Person.Bird();//错误的 Person p = new Person(); Person.Bird bird = p.new Bird();
-
如何在成员内部类中调用外部类的结构?
class Person{ String name = "小明"; public void eat(){ } //非静态成员内部类 class Bird{ String name = "杜鹃"; public void display(String name){ System.out.println(name);//方法的形参 System.out.println(this.name);//内部类的属性 System.out.println(Person.this.name);//外部类的属性 Person.this.eat();//外部类的方法 } } }
-
-
局部内部类的使用 :
//返回一个实现了Comparable接口的类的对象 public Comparable getComparable(){ //创建一个实现了Comparable接口的类:局部内部类 //方式一: class MyComparable implements Comparable{ @Override public int compareTo(Object o) { return 0; } } return new MyComparable(); //方式二: return new Comparable(){ @Override public int compareTo(Object o) { return 0; } }; }
- 注意点:在局部内部类的方法中(比如:show)如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,要求此局部变量声明为 final 的。
JDK7 及之前的版本:要求此局部变量显示的声明为 final 的。
JDK8 开始:可以省略 final 的声明。 - 总结:
成员内部类和局部内部类,在编译以后,都会生成字节码文件。
格式:成员内部类:外部类$内部类名.class
局部内部类:外部类$数字 内部类名.class
- 注意点:在局部内部类的方法中(比如:show)如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,要求此局部变量声明为 final 的。
七、异常处理
7.1 异常
7.1.1 异常的体系结构
java.lang.Throwable
java.lang.Error:一般不编写针对性的代码进行处理。
java.lang.Exception:可以进行异常的处理
编译时异常(checked)
IOException
FileNotFoundException:两种情况:一是“拒绝访问”,二是“系统找不到指定路径
ClassNotFoundException:无法找到指定的类异常
运行时异常(unchecked,RuntimeException)
NullPointerException:空指针异常
ArrayIndexOutOfBoundsException:数组角标越界异常
ClassCastException:当试图将对象强制转换为不是实例的子类时,抛出该异常。
NumberFormatException:试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
InputMismatchException:输入不匹配异常,即输入的值数据类型与设置的值数据类型不能匹配。
ArithmeticException:算术异常。例如,一个整数“除以零”时。
7.1.2 从程序执行过程,看编译时异常和运行时异常
- 编译时异常:执行 javac.exe 命名时,可能出现的异常
- 运行时异常:执行 java.exe 命名时,出现的异常
7.1.3 常见异常举例
//******************以下是运行时异常***************************
//ArithmeticException
@Test
public void test6(){
int a = 10;
int b = 0;
System.out.println(a / b);
}
//InputMismatchException
@Test
public void test5(){
Scanner scanner = new Scanner(System.in);
int score = scanner.nextInt();
System.out.println(score); //输入字符串就会报异常
scanner.close();
}
//NumberFormatException
@Test
public void test4(){
String str = "123";
str = "abc";
int num = Integer.parseInt(str);
}
//ClassCastException
@Test
public void test3(){
Object obj = new Date();
String str = (String)obj;
}
//IndexOutOfBoundsException
@Test
public void test2(){
//ArrayIndexOutOfBoundsException
// int[] arr = new int[10];
// System.out.println(arr[10]);
//StringIndexOutOfBoundsException
String str = "abc";
System.out.println(str.charAt(3));
}
//NullPointerException
@Test
public void test1(){
// int[] arr = null;
// System.out.println(arr[3]);
String str = "abc";
str = null;
System.out.println(str.charAt(0));
}
//******************以下是编译时异常***************************
@Test
public void test7(){
// File file = new File("hello.txt");
// FileInputStream fis = new FileInputStream(file);
//
// int data = fis.read();
// while(data != -1){
// System.out.print((char)data);
// data = fis.read();
// }
// fis.close();
}
7.2 异常的处理
7.2.1 Java异常处理的抓抛模型
-
抛:
程序在正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应的异常类的对象。并将此对象抛出。一旦抛出对象以后,其后的代码就不再执行。
关于异常对象的产生:
- 系统自动生成的异常对象
- 手动的生成一个异常对象,并抛出 (使用关键字throw)
-
抓:
可以理解为异常的处理方式
- try-catch-finally
- throws
7.2.2 异常处理方式一:try-catch-finally
① 使用说明:
try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}catch(异常类型3 变量名3){
//处理异常的方式3
}
....
finally{
//一定会执行的代码
}
- catch 和 finally 都可以省略,但不能同时省略。但不建议省略 catch 。
- 使用 try 将可能出现异常的代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应的异常类对象,
根据此对象的类型,去catch 中进行匹配。 - 一旦 try 中的异常对象匹配到某一个 catch 时,就进入 catch 中进行异常的处理。
一旦处理完成,就跳出当前的try-catch结构(在没finally的情况下),继续执行其后的代码。 - catch 中的异常类型如果没有子父类关系,则谁声明在上,谁声明在下都行。
catch 中的异常类型如果满足子父类关系,则要求子类一定要声明在父类的上面。否则就会报错。 - 常用的异常对象处理的方式:String getMessage() 获取异常信息的字符串。printStackTrace() 打印异常堆栈信息。
- 在 try结构中申明的变量时局部变量,出了try结构以后,就不能在被调用。
- try-catch-finally 结构可以嵌套使用。
② finally 的再说明
- finally 是可以省略的
- finally 中声明的是一定会被执行的代码。即使 try 中有 return 语句;catch 中又出现了异常;catch 中有 return 语句等情况。
除非程序被中断,退出Java虚拟机,才会不执行。 - 像数据库连接、输入输出流、网络编程 Socket 等资源,JVM 是不能自动回收的,我们需要自己手动的进行资源的释放。
此时的资源释放,就需要声明在finally中。
7.2.3 异常处理方式二
- “throws + 异常类型” 写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。
- 当方法体执行时,一旦出现异常,会在异常代码处生成一个异常类的对象,此对象满足 throws 后异常类型时,就会被抛出。
异常代码后续的代码,就不再执行。
7.2.4 对比两种处理方式
- try-cath-finally :真正的将异常给处理掉了
- throws 的方式只是将异常抛给了方法的调用者;并没有真正的将异常处理掉。
7.2.5 体会开发中应该如何选择两种处理方式
- 如果父类中被重写的方法没用 throws 方式处理异常,则子类重写的方法也不能使用 throws。
意味着如果子类重写的方法中出现异常,必须使用 try-catch-finally 方式处理。 - 执行的方法 a 中,先后又调用了另外的几个方法,这几个方法时递进关系执行的。
建议这几个方法使用 throws 的方式进行处理;而在方法 a 中使用 try-catch-finally 方式进行处理。
补充:方法重写的规则之一:
子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型。
7.3 手动抛出异常对象
例题
class Student{
private int id;
public void regist(int id) throws Exception {
if(id > 0){
this.id = id;
}else{
//手动抛出异常对象
// throw new RuntimeException("您输入的数据非法!");
// throw new Exception("您输入的数据非法!");
throw new MyException("不能输入负数");
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}
7.4 自定义异常类
如何自定义异常类?
- 继承于现有的异常结构:RuntimeException、Exception
- 提供全局常量:serialVersionUID
- 提供重载的构造器
/*
* 如何自定义异常类?
* 1. 继承于现有的异常结构:RuntimeException 、Exception
* 2. 提供全局常量:serialVersionUID
* 3. 提供重载的构造器
*
*/
public class MyException extends Exception{
static final long serialVersionUID = -7034897193246939L;
public MyException(){}
public MyException(String msg){
super(msg);
}
}
Java高级编程
八、多线程
8.1 程序、进程、线程的理解
8.1.1 程序(programm)
- 概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
8.1.2 进程(process)
- 概念:程序一次执行过程,或是正在运行的一个程序。
- 说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
8.1.3 线程(thread)
- 概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 说明:线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
8.1.4 补充:
-
内存结构:
-
进程可以细化为多个线程。
-
每个线程,拥有自己独立的:栈、程序计数器
多个线程,共享同一个进程中的结构:方法区、堆
8.2 并行与并发
8.2.1 单核CPU与多核CPU的理解
- 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收费了才能通过,那么CPU就好比收费人员。如果某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了前,再去收费。)但是因为CPU时间单元特别短,因此感觉不出来。
- 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
- 一个Java应用程序java.exe,至少有三个线程,main()主线程、gc()垃圾回收线程、异常处理线程。当然,如果发现异常,会影响主线程。
8.2.2 并行与并发的理解
- 并行:
多个CPU同时执行多个任务。比如:多个人同时做不同的事。 - 并发:
一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
8.3 创建多线程的两种方式(JDK5之前)
8.3.1 方式一:继承Thread类的方式
- 1、创建一个继承于 Thread 类的子类
- 2、重写 Thread 类的 run() 方法 --> 将此线程执行的操作声明在 run() 中
- 3、创建 Thread 类的子类的对象
- 4、通过此对象调用 start() :
- ①启动当前线程
- ②调用当前线程的 run()
- 说明两个问题:
- 问题一:我们启动一个线程,必须调用 start(),不能调用 run() 的方式启动线程
- 问题二:如果再启动一个线程,必须重新创建一个 Thread 子类的对象,调用此对象的 start()。
8.3.2 方式二:实现 Runnable 接口的方式:
- 1、创建一个实现了 Runnable 接口的类
- 2、实现类去实现 Runnable 中的抽象方法:run()
- 3、创建实现类的对象
- 4、将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
- 5、通过 Thread 类的对象调用 start()
8.3.3 两种方式的对比:
- 开发中:
- 优先选择:实现 Runnable 接口的方式
- 原因:1、实现的方式没有类的单继承性的局限性
2、实现的方式更适合来处理多个线程共享数据的情况
- 联系:public class Thread implements Runnable
- 相同点:
- 两种方式都需要重写 run(),将线程要执行的逻辑声明在 run() 中。
- 目前两个方式,要想启动线程,都是调用的 Thread 类中的 start()
8.4 Thread类中的常用的方法
8.4.1 常用的方法
方法名 | 作用 |
---|---|
start() | 启动当前线程;调用当前线程的 run() |
run() | 通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中 |
currentThread() | 静态方法,返回执行当前代码的线程 |
getName() | 获取当前线程的名字 |
setName() | 设置当前线程的名字 |
yield() | 释放当前cpu的执行权 |
join() | 在线程 a 中调用线程 b 的 join(),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完成后,线程 a 才结束阻塞状态。 |
stop() | 已过时,当执行此方法时,强制结束当前线程 |
sleep(long m) | 让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒事件内,当前线程是阻塞状态。 |
isAlive() | 判断当前线程是否存活 |
8.4.2 线程的优先级
- 最大优先级:MAX_PRIORITY:10
- 最小优先级:MIN _PRIORITY:1
- 默认优先级:NORM_PRIORITY:5
- 如何获取和设置当前线程的优先级
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级 - 说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
8.4.3 补充
- 线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。
- 线程的分类
- 一种是守护线程
- 一种是用户线程
8.5 Thread 的生命周期
- 1、生命周期关注的两个概念:状态、相应的方法
- 2、关注:
状态 a --> 状态 b :哪些方法执行了(回调方法)
某个方法主动调用:状态 a --> 状态 b - 3、阻塞:临时状态,不可以作为最终状态
死亡:最终状态
8.6 线程的同步机制
8.6.1 背景
- 例子:创建了窗口卖票,总票数为100张,使用实现 Runnable 接口的方式。
- 1.问题:卖票过程中,出现了重票、错票-->出现了线程的安全问题。
- 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 3.如何解决:当一个线程 a 在操作 ticket(票) 的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket 。这种情况即使线程 a 出现了阻塞,也不能被改变。
8.6.2 Java解决方案:同步机制
-
在Java中,我们通过同步机制,来解决线程的安全问题。
-
方式一:同步代码块
* synchronized(同步监视器){ * //需要被同步的代码 * }
- 1.操作共享数据的代码,即为需要被同步的代码。-->不能包含代码多了,也不能包含代码少了。
- 2.共享数据:多个线程共同操作的变量。比如:ticket 就是共享数据。
- 3.同步监视器:俗称 锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用一把锁。 - 补充:
在实现 Runnable 接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器。
在继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器。
-
方式二:同步方法
- 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
- 关于同步方法的总结:
- 1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
- 2.非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
-
方式三:Lock锁 ---- JDK5.0新增
- synchronized 与 Lock 的异同(面试题)
- 相同:二者都可以解决线程安全问题
- 不同:synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器。Lock 需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())。
- 使用的优先顺序:Lock --> 同步代码块(已经进入了方法体,分配了相应资源)-->同步方法(在方法体之外)
- synchronized 与 Lock 的异同(面试题)
8.6.3 利弊
- 利:同步的方式,解决了线程的安全问题。
- 弊:操作同步代码块时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
8.6.补充1 线程安全的单例模式
-
使用同步机制将单例模式中的懒汉式改写为线程安全的。
-
代码示例:
class Bank{ private Bank(){} private static Bank instance = null; public static Bank getInstance(){ //方式一:效率稍差 // synchronized (Bank.class) { // if(instance == null){ // instance = new Bank(); // } // return instance; // } //方式二:效率更高 if(instance == null){ synchronized (Bank.class) { if(instance == null){ instance = new Bank(); } } } return instance; } } public class SingletonTest{ public static void main(String[] args){ Singleton s1=Singleton.getInstance(); Singleton s2=Singleton.getInstance(); System.out.println(s1==s2); } }
8.6.补充2 死锁
-
死锁的理解:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。 -
说明:
- 1.出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
- 2.我们使用同步时,要避免出现死锁。
-
举例:
public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); }
8.7 线程通信
8.7.1 线程通信设计到的三个方法:
- wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被 wait 的一个线程。如果多个线程被 wait,就唤醒优先级高的那个。
- notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。
8.7.2 说明:
- wait(),notify(),notifyAll() 三个方法必须使用在同步代码块或同步方法中。
- wait(),notify(),notifyAll() 三个方法的调用者必须时同步代码块或同步方法中的同步监视器。
- wait(),notify(),notifyAll() 三个方法是定义在 java.lang.Object 类中。
8.7.3 sleep() 和 wait()的异同?
- 相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
- 不同点:
- 两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明 wait()
- 调用的要求不同:sleep() 可以在任何需要的场景下调用。wait() 必须使用在同步代码块或同步方法中
- 关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁。
8.7.4 小结释放 / 不释放锁的操作
- 释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到 break、return 终止了改代码块。
- 当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。
- 不释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁。
应尽量避免使用 suspend() 和 resume() 来控制线程。
8.8 JDK5.0新增线程创建的方式
8.8.1 实现 Callable 接口。--JDK5.0新增
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 说明:
九、Java常用类
9.1 String类
9.1.1 概述
- String:字符串,使用一对”“引起来表示
- String声明为 final 的,不可被继承。
- String实现了 Serializable 接口:表示String是支持序列化的。
实现了Comparable接口:表示String是可以比较大小的。 - String内部定义了 final char[] value用于存储字符串数据
- 通过字面量的方式定义(区别于 new 给一个字符串赋值)此时的字符串值声明在字符串常量池中
- 字符串常量池中是不会存储相同的内容(使用String类的 equals() 方法比较,返回 true)的字符串的。
9.1.2 String的不可变性
-
说明
- 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的内存区域进行赋值。
- 当对现有的字符串进行连接操作时,也需要重新指定内存取悦赋值,不能使用原有的内存区域进行赋值。
- 当调用String的 replace() 方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的内存区域进行赋值。
-
代码示例
String s1 = "abc";//字面量的定义方式 String s2 = "abc"; s1 = "hello"; System.out.println(s1 == s2);//比较s1和s2的地址值,false System.out.println(s1);//hello System.out.println(s2);//abc System.out.println("*****************"); String s3 = "abc"; s3 += "def"; System.out.println(s3);//abcdef System.out.println(s2); System.out.println("*****************"); String s4 = "abc"; String s5 = s4.replace('a', 'm'); System.out.println(s4);//abc System.out.println(s5);//mbc
-
图示
9.1.3 String实例化的不同方法
-
方法说明
- 方式一:通过字面量定义的方式
- 方式二:通过new + 构造器的方式
-
代码举例
//通过字面量定义的方式:此时的s1和s2的数据javaEE声明在方法区中的字符串常量池中。 String s1 = "javaEE"; String s2 = "javaEE"; //通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。 String s3 = new String("javaEE"); String s4 = new String("javaEE"); System.out.println(s1 == s2);//true System.out.println(s1 == s3);//false System.out.println(s1 == s4);//false System.out.println(s3 == s4);//false
-
String s = new String("abc");方式创建对象,在内存中创建了几个对象?
两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:"abc"。 -
图示
9.1.4 字符串拼接方式赋值的对比
-
说明
- 常量与常量拼接的结果在常量池中,且常量池中不会存在相同内容的常量。
- 只要其中一个是变量,结果就在堆中。
- 如果拼接的结果调用 intern() 方法,返回值就在常量池中
-
代码举例
String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE" + "hadoop"; String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4);//true System.out.println(s3 == s5);//false System.out.println(s3 == s6);//false System.out.println(s3 == s7);//false System.out.println(s5 == s6);//false System.out.println(s5 == s7);//false System.out.println(s6 == s7);//false String s8 = s6.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop” System.out.println(s3 == s8);//true **************************** String s1 = "javaEEhadoop"; String s2 = "javaEE"; String s3 = s2 + "hadoop"; System.out.println(s1 == s3);//false final String s4 = "javaEE";//s4:常量 String s5 = s4 + "hadoop"; System.out.println(s1 == s5);//true
9.1.5 常用方法
方法名 | 作用 | 备注 |
---|---|---|
int length() | 返回字符串长度 | return value.length |
char charAt(int index) | 返回某索引处的字符 | return value[index] |
boolean isEmpty() | 判断是否是空字符串 | return value.length == 0 |
String toLowerCase() | 使用默认语言环境,将String中所有字符转换为小写 | |
String toUpperCase() | 使用默认语言环境,将String中所有字符转换为大写 | |
String trim() | 返回字符串的副本,忽略前部空白和尾部空白 | |
boolean equals(Object obj) | 比较字符串的内容是否相同 | |
boolean equalsIgnoreCase(String anotherString) | 忽略大小写比较字符串的内容是否相同 | |
String concat(String str) | 将指定字符串连接到此字符串的结尾。 | 等价于”+“ |
int compareTo(String anotherString) | 比较两个字符串的大小 | |
String substring(int beginIndex, int endIndex) | 返回一个新字符串,它是此字符串从beginIndex开始截取endIndex(不包含)的一个子字符串。 | |
boolean endsWith(String suffix) | 测试此字符串是否以指定的后缀结束 | |
boolean startsWith(String prefix) | 测试此字符串是否以指定的前缀开始 | |
boolean startsWith(String prefix, int toffset) | 测试此字符串从指定索引开始的子字符串是否以指定前缀开始 | |
boolean contains(CharSequence s) | 当且仅当此字符串包含指定的 char 值序列时,返回 true | |
int indexOf(String str) | 返回指定子字符串在此字符串中第一次出现处的索引 | |
int indexOf(String str, int fromIndex) | 返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始 | |
int lastIndexOf(String str) | 返回指定子字符串在此字符串中最右边出现处的索引 | |
int lastIndexOf(String str, int fromIndex) | 返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索 | 注:indexOf和lastIndexOf方法如果未找到都是返回-1 |
String replace(char oldChar, char newChar) | 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所 有oldChar 得到的。 | |
String replace(CharSequence target, CharSequence replacement) | 值替换序列替换此字符串所匹配字面值目标序列的子字符串。 | |
String replaceAll(String regex, String replacement) | 使用给定的 replacement 替换此字符串所匹配给定的正则表达式的子字符串。 | |
String replaceFirst(String regex, String replacement) | 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 | |
boolean matches(String regex) | 告知此字符串是否匹配给定的正则表达式。 | |
String[] split(String regex) | 根据给定正则表达式的匹配拆分此字符串。 | |
String[] split(String regex, int limit) | 根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。 |
9.1.6 String与其它结构的转换
-
与基本数据类型、包装类之间的转换
-
String --> 基本数据类型、包装类:调用包装类的静态方法:parseXxx(str)
-
基本数据类型、包装类 --> String:调用String重载的valueOf(xxx)
-
@Test public void test1(){ String str1 = "123"; //int num = (int)str1;//错误的 int num = Integer.parseInt(str1); String str2 = String.valueOf(num);//"123" String str3 = num + ""; System.out.println(str1 == str3); }
-
-
与字符数组之间的转换
-
String --> char[]:调用String的toCharArray()
-
char[] --> String:调用String的构造器
-
@Test public void test2(){ String str1 = "abc123"; //题目: a21cb3 char[] charArray = str1.toCharArray(); for (int i = 0; i < charArray.length; i++) { System.out.println(charArray[i]); } char[] arr = new char[]{'h','e','l','l','o'}; String str2 = new String(arr); System.out.println(str2); }
-
-
与字节数组之间的转换
-
编码:String ----> byte[] :调用String的getBytes()
-
解码:byte[] ----> String :调用String的构造器
-
说明
编码:字符串 -->字节 (看得懂 --->看不懂的二进制数据)
解码:编码的逆过程,字节 --> 字符串 (看不懂的二进制数据 ---> 看得懂。
解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码。 -
@Test public void test3() throws UnsupportedEncodingException { String str1 = "abc123中国"; byte[] bytes = str1.getBytes();//使用默认的字符集,进行编码。 System.out.println(Arrays.toString(bytes)); byte[] gbks = str1.getBytes("gbk");//使用gbk字符集进行编码。 System.out.println(Arrays.toString(gbks)); System.out.println("******************"); String str2 = new String(bytes);//使用默认的字符集,进行解码。 System.out.println(str2); String str3 = new String(gbks); System.out.println(str3);//出现乱码。原因:编码集和解码集不一致! String str4 = new String(gbks, "gbk"); System.out.println(str4);//没出现乱码。原因:编码集和解码集一致! }
-
-
与StringBuffer、StringBuilder之间的转换
- String -->StringBuffer、StringBuilder:调用StringBuffer、StringBuilder构造器
- StringBuffer、StringBuilder -->String:①调用String构造器;②StringBuffer、StringBuilder的toString()。
9.1.7 JVM中字符串常量池存放位置说明
jdk 1.6 (jdk 6.0 ,java 6.0):字符串常量池存储在方法区(永久区)
jdk 1.7:字符串常量池存储在堆空间
jdk 1.8:字符串常量池存储在方法区(元空间)
9.2 StringBuffer、StringBuilder
9.2.1 String、StringBuffer、StringBuilder三者的对比
- String:不可变的字符序列;底层使用 char[] 存储
- StringBuffer:可变的字符序列;线程安全的,效率低;底层使用 char[] 存储
- StringBuilder:可变的字符序列;JDK5.0 新增的,线程不安全的,效率高;底层 使用 char[] 存储
9.2.2 StringBuffer 与 StringBuilder 的内存解析
String str = new String();//char[] value = new char[0];
String str1 = new String("abc");//char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度是16的数组。
System.out.println(sb1.length());//
sb1.append('a');//value[0] = 'a';
sb1.append('b');//value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc");//char[] value = new char["abc".length() + 16];
//问题1. System.out.println(sb2.length());//3
//问题2. 扩容问题:如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。
// 默认情况下,扩容为原来容量的2倍 + 2,同时将原数组中的元素复制到新的数组中。
//指导意义:开发中建议大家使用:StringBuffer(int capacity) 或 StringBuilder(int capacity)
9.2.3 对比String、StringBuffer、StringBuilder三者的执行效率
- 从高到底排列:StringBuilder > StringBuffer > String
9.2.4 StringBufer、StringBuilder中的常用方法
- 增:append(xxx)
- 删:delete(int start, int end)
- 改:setCharAt(int n, char ch) / replace(int start, int end, String str)
- 查:charAt(int n)
- 插:insert(int offset, xxx)
- 长度:length()
- 遍历:for() + charAt() / toString()
9.3 JDK8之前日期时间API
9.3.1 获取系统当前时间
-
System 类中的 currentTimeMillis()
long time = System.currentTimeMillis();
返回当前时间与 1970年 1 月 1 日 0 时 0 分 0 秒之间以毫秒为单位的时间差。称为时间戳。
9.3.2 java.util.Date类 与 java.sql.Date 类
-
两个构造器的使用
- 构造器一:Date():创建一个对应当前时间的Date对象
- 构造器二:创建指定毫秒数的Date对象
-
两个方法的使用
- toString():显示当前的年、月、日、时、分、秒
- getTime():获取当前Date对象对应的毫秒数(时间戳)
-
java.sql.Date对应着数据库中的日期类型的变量
-
如何实例化
-
如何将 java.util.Date 对象转换为 java.sql.Date 对象
-
@Test public void test2(){ //构造器一:Date():创建一个对应当前时间的Date对象 Date date1 = new Date(); System.out.println(date1.toString());//Sat Feb 16 16:35:31 GMT+08:00 2019 System.out.println(date1.getTime());//1550306204104 //构造器二:创建指定毫秒数的Date对象 Date date2 = new Date(155030620410L); System.out.println(date2.toString()); //创建java.sql.Date对象 java.sql.Date date3 = new java.sql.Date(35235325345L); System.out.println(date3);//1971-02-13 //如何将java.util.Date对象转换为java.sql.Date对象 //情况一: Date date4 = new java.sql.Date(2343243242323L); java.sql.Date date5 = (java.sql.Date) date4; //情况二: Date date6 = new Date(); java.sql.Date date7 = new java.sql.Date(date6.getTime()); }
-
9.3.3 java.text.SimpleDataFormat类
-
SimpleDateFormat对日期Date类的格式化和解析
- 格式化:日期---->字符串
- 解析:格式化的逆过程,字符串---->日期
-
SimpleDateFormat的实例化:new + 构造器
- 按照指定的方式格式化和解析:调用带参的构造器
SimpleDateFormat sdf = new SimpleDateFormat("yyyyy.MMMMM.dd GGG hh:mm aaa"); SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); //格式化 String format1 = sdf1.format(date); System.out.println(format1);//2019-02-18 11:48:27 //解析:要求字符串必须是符合SimpleDateFormat识别的格式(通过构造器参数体现), //否则,抛异常 Date date2 = sdf1.parse("2020-02-18 11:48:27"); System.out.println(date2);
- 按照指定的方式格式化和解析:调用带参的构造器
-
练习
/* 练习一:字符串"2020-09-08"转换为java.sql.Date 练习二:"三天打渔两天晒网" 1990-01-01 xxxx-xx-xx 打渔?晒网? 举例:2020-09-08 ? 总天数 总天数 % 5 == 1,2,3 : 打渔 总天数 % 5 == 4,0 : 晒网 总天数的计算? 方式一:( date2.getTime() - date1.getTime()) / (1000 * 60 * 60 * 24) + 1 方式二:1990-01-01 --> 2019-12-31 + 2020-01-01 -->2020-09-08 */ @Test public void testExer() throws ParseException { String birth = "2020-09-08"; SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd"); Date date = sdf1.parse(birth); // System.out.println(date); java.sql.Date birthDate = new java.sql.Date(date.getTime()); System.out.println(birthDate); }
9.3.4 Calendar类:日历类、抽象类
//1.实例化
//方式一:创建其子类(GregorianCalendar的对象
//方式二:调用其静态方法getInstance()
Calendar calendar = Calendar.getInstance();
// System.out.println(calendar.getClass());
//2.常用方法
//get()
int days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
System.out.println(calendar.get(Calendar.DAY_OF_YEAR));
//set()
//calendar可变性
calendar.set(Calendar.DAY_OF_MONTH,22);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
//add()
calendar.add(Calendar.DAY_OF_MONTH,-3);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
//getTime():日历类---> Date
Date date = calendar.getTime();
System.out.println(date);
//setTime():Date ---> 日历类
Date date1 = new Date();
calendar.setTime(date1);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
9.4 JDK8中新日期时间API
9.4.1 日期时间API的迭代
- 第一代:JDK1.0 Date类
- 第二代:JDK1.1 Calendar类,一定程度上替换Date类
- 第三代:JDK1.8 提出了新的一套API
9.4.2 前两代存在的问题举例:
- 可变性:像日期和时间这样的类应该是不可变的。
- 偏移性:Date中的年份是从1900开始的,而月份都从0开始。
- 格式化:格式化只对Date有用,Calendar则不行。此外,他们不是线程安全的,不能处理闰秒等。
9.4.3 Java8中新的日期时间API涉及到的包
9.4.4 本地日期、本地时间、本地日期时间的使用
-
LocalDate / LocalTime / LocalDateTime
-
说明
- 分别表示使用 ISO-8601 日历系统的日期、时间、日期时间。踢门提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
- LocalDateTime 相较于LocalDate、LocalTime,使用频率要高。
- 类似于Calendar
-
常用方法
方法 描述 now() / now(Zoneld zone) 静态方法,更具当前时间创建对象 / 指定时区的对象 of() 静态方法,根据指定日期 / 时间创建对象 getDayOfMonth() / getDayOfYear() 获得月份天数(1-31) / 获得年份天数(1-366) getDayOfWeek() 获得星期几(返回一个 DayOfWeek枚举值) getMonth() 获得月份,返回一个 Month 枚举值 getMonthValue() / getYear() 获得月份(1-12)/ 获得年份 getHour() / getMinute() / getSecond() 获得当前对象对应的小时、分钟、秒 withDayOfMonth() / withDayOfYear() / withMonth() / withYear() 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象 plusDays() / plusWeeks() / plusMonths() / plusYears() / plusHours() 向当前对象添加几天、几周、几个月、几年、几小时 minusDays() / minusMonths() / minusWeeks() / minusDays() / minusYears() / minusHours() 向当前对象减去几天、几月、几周、几天、几年、几小时
9.4.5 时间点:Instant
-
说明:
- 时间线上的一个瞬时点。概念上讲,他只是简单的表示自1970年1月1日0时0分0秒(UTC开始的秒数)
- 类似于 java.util.Date类
-
常用方法
方法 描述 now() 静态方法,返回默认 UTC 时区的 Instant 类的对象 ofEpochMilli(long epochMilli) 静态方法,返回在 1970-01-01 00:00:00 基础上加上指定的毫秒数之后的 Instant 类的对象 atOffset(ZoneOffset offset) 结合即时的偏移来创建一个 OffsetDateTime toEpochMilli() 返回 1970-01-01 00:00:00 到当前时间的毫秒数,即为时间戳 时间戳是指格林威治时间 1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
9.4.6 日期时间格式化类:DateTimeFormatter
- 说明:
- 格式化或解析日期、时间
- 类似于 SimpleDateFormat
- 常用方法
-
实例化方法:
预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”) -
常用方法
方法 描述 ofPattern(String pattern) 静态方法,返回一个指定字符串格式的 DateTimeFormatter format(TemporalAccessor t) 格式化一个日期、时间,返回字符串 parse(CharSequence text) 将指定格式的字符串序列解析为一个日期、时间 特别的:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”) // 重点:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”) DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); //格式化 String str4 = formatter3.format(LocalDateTime.now()); System.out.println(str4);//2019-02-18 03:52:09 //解析 TemporalAccessor accessor = formatter3.parse("2019-02-18 03:52:09"); System.out.println(accessor);
-
9.4.7 其他 API 的使用
-
带时区的日期时间:ZonedDateTime / ZoneId
// ZoneId:类中包含了所有的时区信息 @Test public void test1(){ //getAvailableZoneIds():获取所有的ZoneId Set<String> zoneIds = ZoneId.getAvailableZoneIds(); for(String s : zoneIds){ System.out.println(s); } System.out.println(); //获取“Asia/Tokyo”时区对应的时间 LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo")); System.out.println(localDateTime); } //ZonedDateTime:带时区的日期时间 @Test public void test2(){ //now():获取本时区的ZonedDateTime对象 ZonedDateTime zonedDateTime = ZonedDateTime.now(); System.out.println(zonedDateTime); //now(ZoneId id):获取指定时区的ZonedDateTime对象 ZonedDateTime zonedDateTime1 = ZonedDateTime.now(ZoneId.of("Asia/Tokyo")); System.out.println(zonedDateTime1); }
-
时间间隔:Duration
用于计算两个“时间”间隔,以秒和纳秒为基准方法 描述 between(Temporal start, Temporal end) 静态方法,返回 Duration 对象,表示两个时间间隔 getNano() / getSeconds() 返回时间间隔的纳秒数 / 返回时间间隔的秒速 toDays() / toHours() / toMinutes() / toMillis() / toNanos() 返回时间间隔期间的天数、小时数、分钟数、毫秒数、纳秒数 @Test public void test3(){ LocalTime localTime = LocalTime.now(); LocalTime localTime1 = LocalTime.of(15, 23, 32); //between():静态方法,返回Duration对象,表示两个时间的间隔 Duration duration = Duration.between(localTime1, localTime); System.out.println(duration); System.out.println(duration.getSeconds()); System.out.println(duration.getNano()); LocalDateTime localDateTime = LocalDateTime.of(2016, 6, 12, 15, 23, 32); LocalDateTime localDateTime1 = LocalDateTime.of(2017, 6, 12, 15, 23, 32); Duration duration1 = Duration.between(localDateTime1, localDateTime); System.out.println(duration1.toDays()); }
-
日期间隔:Period
用于计算两个“日期”间隔,以年、月、日衡量方法 描述 between(LocalDate start, LocalDate end) 静态方法,返回 Period 对象,表示两个本地日期的间隔 getYears() / getMonths() / getDays() 返回此期间的年数、月数、天数 withYears(int years) / withMonths(int months) / withDays(int days) 返回设置间隔指定年、月、日数以后的 Period 对象 @Test public void test4(){ LocalDate localDate = LocalDate.now(); LocalDate localDate1 = LocalDate.of(2028, 3, 18); Period period = Period.between(localDate, localDate1); System.out.println(period); System.out.println(period.getYears()); System.out.println(period.getMonths()); System.out.println(period.getDays()); Period period1 = period.withYears(2); System.out.println(period1); }
-
日期时间校正器:TemporalAdjuster
@Test public void test5(){ //获取当前日期的下一个周日是哪天? TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY); LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster); System.out.println(localDateTime); //获取下一个工作日是哪天? LocalDate localDate = LocalDate.now().with(new TemporalAdjuster(){ @Override public Temporal adjustInto(Temporal temporal) { LocalDate date = (LocalDate)temporal; if(date.getDayOfWeek().equals(DayOfWeek.FRIDAY)){ return date.plusDays(3); }else if(date.getDayOfWeek().equals(DayOfWeek.SATURDAY)){ return date.plusDays(2); }else{ return date.plusDays(1); } } }); System.out.println("下一个工作日是:" + localDate); }
9.5 Java比较器
9.5.1 Java比较器的使用背景
- Java中的对象,正常情况下,只能进行比较:== 或 != 。不能使用 > 或 < 进行比较。但是在开发场景中,我们需要对多个对象进行排序,言外之意,就需要比较对象的大小。
- 如何实现?使用两个接口中的任何一个:Comparable 或 Comparator
9.5.2 自然排序:使用 Comparable 接口
-
说明
- 像String、包装类等实现了 Comparable 接口,重写了 compareTo(obj) 方法,给出了比较两个对象大小的方式
- 像String、包装类重写 compareTo() 方式以后,进行了从小到大的排列
- 重写 compareTo(obj) 的规则:
如果当前对象 this 大于形参对象 obj,则返回正整数,
如果当前对象 this 小于形参对象 obj,则返回负整数,
如果当前对象 this 等于形参对象 obj,则返回0。 - 对于自定义类来说,如果需要排序,我们可以让自定义类实现 Comparable 接口,重写 compareTo(obj) 方法。在compareTo(obj) 方法中指明如何排序
-
自定义类代码举例:
public class Goods implements Comparable{ private String name; private double price; //指明商品比较大小的方式:照价格从低到高排序,再照产品名称从高到低排序 @Override public int compareTo(Object o) { // System.out.println("**************"); if(o instanceof Goods){ Goods goods = (Goods)o; //方式一: if(this.price > goods.price){ return 1; }else if(this.price < goods.price){ return -1; }else{ // return 0; return -this.name.compareTo(goods.name); } //方式二: // return Double.compare(this.price,goods.price); } // return 0; throw new RuntimeException("传入的数据类型不一致!"); } // getter、setter、toString()、构造器:省略 }
9.5.3 定制排序:使用 Comparator 接口
-
说明
- 背景
当元素的类型没实现 java.lang.Comparable 接口而又不方便修改代码,或者实现了 java.lang.Comparable 接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序 - 重写 compare(Object o1, Object o2) 方法,比较 o1 和 o2 的大小:
如果方法返回正整数,则表示 o1 > o2
如果方法返回0,表示 o1 = o2
如果方法返回负整数,表示 o1 < o2
- 背景
-
代码举例
Comparator com = new Comparator() { //指明商品比较大小的方式:照产品名称从低到高排序,再照价格从高到低排序 @Override public int compare(Object o1, Object o2) { if(o1 instanceof Goods && o2 instanceof Goods){ Goods g1 = (Goods)o1; Goods g2 = (Goods)o2; if(g1.getName().equals(g2.getName())){ return -Double.compare(g1.getPrice(),g2.getPrice()); }else{ return g1.getName().compareTo(g2.getName()); } } throw new RuntimeException("输入的数据类型不一致"); } }
9.5.4 两种排序方式的对比
- Comparable 接口的方式一旦一定,保证 Comparable 接口实现类的对象在任何位置都可以比较大小
- Comparator 接口属于临时性的比较
9.6 其他类
9.6.1 System类
- System 类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于 java.lang 包。
- 由于该类的构造器是 private 的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是 static 的,所以也可以很方便的进行调用。
- 方法:
native long currentTimeMillis()
void exit(int status)
void gc()
String getProperty(String key)
9.6.2 Math类
- java,lang.Math 提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为 double 型。
9.6.3 BigInteger类、BigDecimal类
-
说明:
- java.math 包的 BigInteger可以表示不可变的任意精度的整数。
- 要求数字精度比较高,用 java.math.BigDecimal 类
-
代码举例:
public void testBigInteger(){ BigInteger bi new BigInteger("12433241123"); BigDecimal bd new BigDecimal("12435.351"); BigDecimal bd2 new BigDecimal("11"); system.out.println(bi); //System.out.println(bd.divide(bd2)); System.out.println(bd.divide(bd2,BigDecimal.ROUND HALF_UP)); System.out.println(bd.divide(bd2,15,BigDecimaL.ROUND_HALF_UP)); }
十、枚举类和注解
10.1 枚举类的使用
10.1.1 枚举类的说明
- 枚举类的理解:类的对象只有有限个,确定的。我们称此类为枚举类
- 当需要定义一组常量时,强烈建议使用枚举类
- 如果枚举类中只有一个对象,则可以作为单例模式的实现方式。
10.1.2 如何自定义枚举类?步骤
//自定义枚举类
class Season{
//1.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;
//2.私化类的构造器,并给对象属性赋值
private Season(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//3.提供当前枚举类的多个对象:public static final的
public static final Season SPRING = new Season("春天","春暖花开");
public static final Season SUMMER = new Season("夏天","夏日炎炎");
public static final Season AUTUMN = new Season("秋天","秋高气爽");
public static final Season WINTER = new Season("冬天","冰天雪地");
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
//4.其他诉求1:提供toString()
@Override
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonDesc='" + seasonDesc + '\'' +
'}';
}
10.1.3 jdk5.0新增使用 enum 定义枚举类。步骤
//使用enum关键字枚举类
enum Season1 {
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天","春暖花开"),
SUMMER("夏天","夏日炎炎"),
AUTUMN("秋天","秋高气爽"),
WINTER("冬天","冰天雪地");
//2.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;
//3.私化类的构造器,并给对象属性赋值
private Season1(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}
10.1.4 使用enum定义枚举类之后,枚举类常用方法(继承于java.lang.Enum类)
Season1 summer = Season1.SUMMER;
//toString():返回枚举类对象的名称
System.out.println(summer.toString());
//System.out.println(Season1.class.getSuperclass());
System.out.println("****************");
//values():返回所有的枚举类对象构成的数组
Season1[] values = Season1.values();
for(int i = 0;i < values.length;i++){
System.out.println(values[i]);
}
System.out.println("****************");
Thread.State[] values1 = Thread.State.values();
for (int i = 0; i < values1.length; i++) {
System.out.println(values1[i]);
}
//valueOf(String objName):返回枚举类中对象名是objName的对象。
Season1 winter = Season1.valueOf("WINTER");
//如果没objName的枚举类对象,则抛异常:IllegalArgumentException
//Season1 winter = Season1.valueOf("WINTER1");
System.out.println(winter);
10.1.5 使用enum定义枚举类之后,如何让枚举类对象分别实现接口
interface Info{
void show();
}
//使用enum关键字枚举类
enum Season1 implements Info{
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天","春暖花开"){
@Override
public void show() {
System.out.println("春天在哪里?");
}
},
SUMMER("夏天","夏日炎炎"){
@Override
public void show() {
System.out.println("宁夏");
}
},
AUTUMN("秋天","秋高气爽"){
@Override
public void show() {
System.out.println("秋天不回来");
}
},
WINTER("冬天","冰天雪地"){
@Override
public void show() {
System.out.println("大约在冬季");
}
};
}
10.2 注解的使用
10.2.1 注解的理解
- jdk5.0 新增的功能
- Annotation 其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过使用 Annotation ,程序员可以在不改变原逻辑的情况下,在源文件中嵌入一些补充信息。
- 在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaSE/Android 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 JavaEE 旧版中所遗留的繁冗代码和 XML 配置等。
- 框架 = 注解 + 反射机制 + 设计模式
10.2.2 注解的使用示例
- 示例一:生成文档相关的注解
- 示例二:在编译时进行格式检查(JDK内置的基本注解)
@Override:限定重写父类方法,该注解只能用于此方法
@Deprecated:用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择。
@SuppressWarnings:抑制编译器警告 - 示例三:跟踪代码依赖性,实现替代配置文件功能
10.2.3 如何使用自定义注解:参照@SuppressWarnings 定义
-
注解声明为:@interface
-
内部定义成员,通常使用 value 表示
-
可以指定成员的默认值,使用 default 定义
-
如果自定义注解没成员,表明是一个标识作用。
-
说明:
如果注解有成员,在使用注解时,需要指明成员的值。
自定义注解必须配上注解的信息处理流程(使用反射)才有意义
自定义注解通过都会指明两个注解:Retention、Target -
代码举例
@Inherited @Repeatable(MyAnnotations.class) @Retention(RetentionPolicy.RUNTIME) @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,TYPE_PARAMETER,TYPE_USE}) public @interface MyAnnotation { String value() default "hello"; }
10.2.4 元注解:对现有的注解进行解释说明的注解。
jdk 提供4种元注解
- Retention:指定所修饰的 Annatation 的生命周期:SOURCE \ CLASS(默认行为) \ RUNTIME;只有声明为 RUNTIME 生命周期的注解,才能通过反射获取。
- Target:用于指定被修饰的 Annaotation 能用于修饰哪些程序元素;出现的频率较低。
- Documented:表示所修饰的注解被 javadoc 解析时,保留下来。
- Inherited:被它修饰的 Annatation 将具有继承性。
- 类比:元数据的概念:String name = “Tom”;
10.2.5 如何获取注解信息
- 通过反射来进行获取、调用。
- 前提:要求此注解的元注解 Retention 中声明的声明周期状态为:RUNTIME。
10.2.6 JDK8中注解的新特性:可重复注解、类型注解
- 可重复注解
- 在 MyAnnotation 上声明 @Repeatable,成员值为 MyAnnotations.class
- MyAnnotation 的 Target 和 Retention 等元注解与 MyAnnpatations 相同。
- 类型注解:
- ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)
- ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中
十一、Java 集合
Java集合分为Collection和Map两种体系
Collection接口:单列数据,定义了存取一组对象的方法的集合
Map接口:双列数据,保存具有映射关系“key-value对”的集合
11.1 数组与集合
11.1.1 集合与数组存储数据概述
集合、数组都是对多个数据进行存储操作的结构,简称 Java 容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储。
11.1.2 数组存储的特点
- 一旦初始化以后,其长度就确定了
- 数组一旦定义好,其元素的类型也就确定了。也就只能操作定义时指定类型的数据了。
11.1.3 数据存储的弊端
- 一旦初始化以后,其长度就不可修改。
- 数组中提供的方法非常有限,对于添加、删除、插入等操作非常不便,同时效率不高。
- 获取数据中实际元素的个数的需求,数组没有现成的属性或方法可用
- 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
11.1.4 集合存储的优点
- 解决数组存储数据方面的弊端
11.2 Collection接口
单列数据,定义了存取一组对象的方法的集合
11.2.1 Collection接口继承树
11.2.2 Collection中常用的方法
方法作用 | 方法名 | 方法介绍 |
---|---|---|
添加 | add(Object obj) | 添加一个元素 |
addAll(Collection) | 添加多个元素 | |
获取有效元素的个数 | int size() | |
清空集合 | void clear() | |
是否为空集合 | boolean isEmpty() | |
是否包含某个元素 | boolean contains(Object obj) | 通过元素的equals方法来判断是否是同一个 |
boolean containsAll(Collection c) | 也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较 | |
删除 | boolean remove(Object obj) | 通过元素的equals挨个判断是否是要删除的元素。 只删除找到的第一个就停止。 |
boolean removeAll(Collection c) | 相当于取当前集合的差集 | |
取两个集合的交集 | boolean rerainAll(Collection c) | 把交集的集合存在调用者中,不影响c |
集合是否相等 | boolean equals(Objext obj) | |
转成对象数组 | Object[] toArray() | |
获取元素的哈希值 | hashCode() | |
遍历 | iterator() | 返回迭代器对象,用于集合遍历 |
11.2.3 Collection与数组间的转换
-
//集合 --->数组:toArray()
Object[] arr = coll.toArray(); for(int i = 0;i < arr.length;i++){ System.out.println(arr[i]); }
-
//拓展:数组 --->集合:调用Arrays类的静态方法asList(T ... t)
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"}); System.out.println(list.size);//3 List arr1 = Arrays.asList(new int[]{123, 456}); System.out.println(arr1.size());//1 把数组当成一个对象传入 List arr2 = Arrays.asList(new Integer[]{123, 456}); System.out.println(arr2.size());//2 Integer数组中有两个Interger对象
11.2.4 使用Collection集合存储对象的条件
- 向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()
11.3 Iterator接口与foreach循环
遍历集合的两种方式:① 使用迭代器 Iterator ② foreach 循环(增强 for 循环)
11.3.1 Iterator接口
-
说明:Iterator 是 java.utils 包下定义的迭代器接口
Iterator 对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的
GoF给迭代器模式的定义为:提供一种方法访问一个容器(Collection)对象中各个元素,而又不暴露该对象的内部细节。迭代器模式,就是为容器而生。
-
作用:遍历集合 Collection 元素
-
如何获取实例:coll.iterator(),返回一个迭代器实例
-
遍历的代码实现:
Iterator iterator = coll.iterator(); //hasNext():判断是否还下一个元素 while(iterator.hasNext()){ //next():进行两项操作:①指针下移 ②将下移以后集合位置上的元素返回 System.out.println(iterator.next()); }
-
迭代器执行原理
-
remove()的使用:
//测试Iterator中的remove() //如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会IllegalStateException。 //内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove() @Test public void test3(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person("Jerry",20)); coll.add(new String("Tom")); coll.add(false); //删除集合中"Tom" Iterator iterator = coll.iterator(); while (iterator.hasNext()){ // 下面这行代码,还未调用next(),报IllegalStateException。 // iterator.remove(); Object obj = iterator.next(); if("Tom".equals(obj)){ iterator.remove(); // 下面这行代码,上一次调用 next 方法之后已经调用了 remove 方法。报IllegalStateException。 // iterator.remove(); } } //遍历集合 iterator = coll.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } }
11.3.2 foreach循环
-
说明:foreach循环也叫增强for循环,是 JDK5.0 新增的。
-
遍历集合举例:(内部仍然调用了迭代器。)
public void test1(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person("Jerry",20)); coll.add(new String("Tom")); coll.add(false); //for(集合元素的类型 局部变量 : 集合对象) for(Object obj : coll){ System.out.println(obj); } }
-
遍历对象举例:
public void test2(){ int[] arr = new int[]{1,2,3,4,5,6}; //for(数组元素的类型 局部变量 : 数组对象) for(int i : arr){ System.out.println(i); } }
11.4 Collection 接口的子接口:List 接口
存储的数据特点:存储有序的、可重复的数据
11.4.1 常用方法:(牢记)
- 增:add(Object obj)(加在首位)
- 删:remove(int index) / remove(Object obj)
- 改:set(int index, Object ele)
- 查:get(int index)
- 插:add(int index, Object ele)
- 长度:size()
- 遍历:Iterator 迭代器方式 / 增强 for 循环 / 普通的循环
11.4.2 常用的实现类
Collection接口:单列集合,用于存储一个一个的对象
List接口:存储有序的、可重复的数据。-->“动态”数组,常用于替换数组
ArrayList:作为 List 接口的主要(常用)实现类;线程不安全的,效率高;底层使用 Object[] ele 存储
LinkedList:线程不安全的;对于频繁的插入、删除操作,效率高于 ArrayList;底层使用双向链表存储
Vector:作为 List 接口的古老实现类;线程安全的,效率低;底层使用 Object[] ele 存储;(不常用)。
11.4.3 新增方法(部分)
-
LinkedList:
方法作用 方法名 在首部添加 void addFirst(Object obj) 在尾部添加 void addLast(Object obj) 获取首位元素 Object getFirst() 获取尾位元素 Object getLast() 移除首位元素 Object removeFirst() 移除尾位元素 Object removerLast() -
Vector:
方法作用 方法名 将指定的组件添加到此向量的末尾,将其大小增加 1。 void addElement(Object obj) 将指定对象作为此向量中的组件插入到指定的 index 处。 void insetElementAt(Object obj, int index) 将此向量指定 index 处的组件设置为指定的对象。 void setElementAt(Objext obj, int index) 从此向量中移除变量的第一个(索引最小的)匹配项。 void removeElement(Object obj) 从此向量中移除全部组件,并将其大小设置为零。 void removeAllElements()
11.4.4 源码分析(难点)
-
ArrayList的源码分析:
-
JDK 7 情况下
ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData list.add(123);//elementData[0] = new Integer(123); ... list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity) -
JDK 8 中Arraylist的变化
ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组 list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0] ... //后续的添加和扩容操作与jdk 7 无异。
-
小结:
JDK 7 中的 ArrayList 的对象的创建类似于单例的饿汉式,而 JDK8 中的 ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
-
-
LinkedList的源码分析:
LinkedList list = new LinkedList(); //内部声明了Node类型的first和last属性,默认值为null list.add(123);//将123封装到Node中,创建了Node对象。 //其中,Node定义为:体现了LinkedList的双向链表的说法(双向链表没有扩容一说) private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
Vector的源码分析:
JDK 7 和 JDK 8 中通过 Vector() 构造器创建对象时,底层都创建了长度为10的数组。在扩容方面,默认扩容为原来的数组长度的2倍。
11.5 Collection的子接口:Set接口
11.5.1 存储的数据特点
无序的、不可重复的元素
以HachSet为例说明:
- 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
- 不可重复性:保证添加的元素按照 equals() 判断时,不能返回 true 。即:相同的元素只能添加一个。
11.5.2 元素添加过程:(以HashSet为例)
向HashSet中添加元素 a,首先调用元素 a 所在类的hashCode() 方法,计算元素 a 的哈希值,此哈希值接种通过某种算法计算出在 HashSet 底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
如果此位置上没有其他元素,则元素 a 添加成功 ---->情况1
如果此位置上有其他元素 b(或以链表形式存在的多个元素),则比较元素 a 和元素 b 的 hash 值:
如果 hash 值不相同,则元素 a 添加成功 ---->情况2
如果 hash 值相同,进而需要调用元素 a 所在类的 equals() 方法:
equals() 返回 false ,则元素 a 添加成功 ---->情况3
equals() 返回 true ,元素 a 添加失败
对于添加成功的情况 2 和情况 3 而言:元素 a 与已经存在指定索引位置上数据以链表的方法存储。
对于 JDK7 :元素 a 放在数组中,指向原来的元素。(挤进去)
对于 JDK8 :原来的元素在数组中,指向元素 a
HashSet底层:数组+链表的结构。(JDK7及以上)
11.5.3 常用实现类
Collextion接口:单列集合,用来存储一个一个的对象
Set接口:存储无序的、不可重复的数据。
HashSet:作为Set接口的主要(常用)实现类;线程不安全的;可以存储null值
LinkedHashSet:在HashSet的存储基础上同时使用双向链表维护元素的次序;
遍历其内部数据时,可以按照添加的顺序遍历;
对于频繁的遍历操作,效率高于HashSetTreeSet:可以按照添加对象的指定属性,进行排序;底层使用红黑树结构存储数据
11.5.4 存储对象所在类的要求
-
HashSet / LikedHashSet:
- 向Set中(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写 hashCode() 和 equals()
- 重写的hashCode() 和 equals() 尽可能保持一致性:相等的对象必须具有相等的散列码
重写的技巧:对象中用作 equals() 方法比较的 Filed(成员变量),都应该用来计算 hashCode 值
-
TreeSet:
-
向TreeSet中添加的数据,要求是相同类的对象
-
自然排序中,比较两个对象是否相同的标准为:compareTo() 返回 0,不再是 equals()
-
定制排序中,比较两个对象是否相同的标准为:compare() 返回 0,不再是 equals()
-
11.5.5 TreeSet 的使用
-
使用说明:
- 向TreeSet中添加的数据,要求是相同类的对象
- 两种排序方式:自然排序(实现Comparable接口)和 定制排序(Comparator接口来实现,重写compare(T o1,T o2)方法)
-
代码示例
-
自然排序
public void test1(){ TreeSet set = new TreeSet(); //失败:不能添加不同类的对象 // set.add(123); // set.add(456); // set.add("AA"); // set.add(new User("Tom",12)); //举例一: // set.add(34); // set.add(-34); // set.add(43); // set.add(11); // set.add(8); //举例二:其中user类实现Comparable接口重写了compareTo() /*//先按照姓名从大到小排列,年龄从小到大排列 @Override public int compareTo(Object o) { if(o instanceof User){ User user = (User)o; int compare = -this.name.compareTo(user.name); if(compare != 0){ return compare; }else{ return Integer.compare(this.age,user.age); } }else{ throw new RuntimeException("输入的类型不匹配"); } }*/ set.add(new User("Tom",12)); set.add(new User("Jerry",32)); set.add(new User("Jim",2)); set.add(new User("Mike",65)); set.add(new User("Jack",33)); set.add(new User("Jack",56)); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } }
-
定制排序
public void test2(){ //Comparator接口来实现,重写compare(T o1,T o2)方法 Comparator com = new Comparator() { //照年龄从小到大排列 @Override public int compare(Object o1, Object o2) { if(o1 instanceof User && o2 instanceof User){ User u1 = (User)o1; User u2 = (User)o2; return Integer.compare(u1.getAge(),u2.getAge()); }else{ throw new RuntimeException("输入的数据类型不匹配"); } } }; TreeSet set = new TreeSet(com); set.add(new User("Tom",12)); set.add(new User("Jerry",32)); set.add(new User("Jim",2)); set.add(new User("Mike",65)); set.add(new User("Mary",33)); set.add(new User("Jack",33)); set.add(new User("Jack",56)); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } }
-
11.6 Map接口
11.6.1 概述
- 双列数据,保存具有映射关系“key-value对”的集合。Map 与 Collection 并列存在。
- Map 中的 key 用 Set 来存放,不允许重复,即同一个 Map 对象所对应的类,必须重写 hashCode() 和 equals() 方法
- 常用 String 类作为 Map 的“键”
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value
- Map接口继承树:
11.6.2 常用实现类结构
Map:双列数据,存储 key-value 对的数据
HashMap:作为 Map 最主要(常用)的实现类;线程不安全的,效率高;允许存储 null 的 key 和 value
底层:JDK7 及之前:数组 + 链表 JDK8:数组 + 链表 + 红黑树LinkedHashMap:在原 HashMap底层结构基础上, 添加了一对指针,指向前一个和后一个元素。
保证在遍历 Map 元素时,可以按照添加的顺序实现遍历。
对于频繁的遍历操作时,效率高于HashMapTreeMap:不允许存储 null 的 key,但是允许存储 null 的值;线程不安全;底层使用红黑树;
存储 ”Key-Value 对“ 时,需要根据 ”key-value 对“ 进行排序;可以保证所有的 ”Key-Value对“ 处于有序状态Hashtable:作为Map的古老实现类;线程安全的,效率低;不能存储 null 的 key 和 value
Properties:常用来处理配置文件。key 和 value 都是 String 类型的。
11.6.3 存储结构的理解
- Map 中的 key:无序的、不可重复的;使用 Set 来存储所有的 key-->key 所在的类要重写 equals() 和 hashCode()---以HashMap为例
- Map 中的 value:无序的、不可重复的;使用 Collection 存储所有的 value -->value 所在的类要重写 equals()
- Map 中的 entry:无序的、不可重复的;使用 Set 存储所有的 entry。
11.6.4 常用方法
- 添加:put(Object key, Object value)
- 删除:remove(Object key)
- 修改:put(Object key, Object value)
- 长度:size()
- 遍历:keySet() / values() / entrySet()
11.6.5 内存结构说明:(难点)
①HashMap在jdk7中实现原理:
HashMap map = new HashMap(); 在实例化以后,底层创建了长度是16的一维数组Entry[] table.
map.put(key1, value1):
首先,调用key1所在类的 hashCode() 计算 key1 哈希值,此哈希值经过某种算法计算以后,得到在 Entry 数组中的存放位置
如果此位置上的数据为空,此时 key1-value1 添加成功。-->情况1
如果此位置上的数据不为空,意味着此位置上存在一个或多个数据(以链表形式存在);
比较 key1 和已经存在的一个或多个数据的哈希值如果 key1 的哈希值与已经存在的数据的哈希值都不 相同,此时 key1-value1添加成功。-->情况2
如果 key1 的哈希值和已经存在的某一数据 (key1-value1) 的哈希值相同,
继续比较:调用 key1 所在类的 equals(key2) 方法,比较如果 equals() 返回 false :此时 key1-value1 添加成功。-->情况3
如果 equals() 返回 true :使用 value1 替换 此位置上的 value。-->情况4
补充:关于情况2和情况3,此时 key1-value1 和原来的数据以链表的方式存储。
在不断的添加过程中,会涉及扩容问题,当超出临界值时,扩容。默认的扩容方式:扩容为原来容量的 2 倍,并将原来的数据复制过来。
②HashMap在jdk8中相较于jdk7在底层实现方面的不同:
- HashMap map = new HashMap(); 在实例化以后,底层没有创建一个长度为16的数组
- jdk8 底层的数组是:Node[] ,而非 Entry[]
- 首次调用 put() 方法时,底层才创建长度为16的数组
- jdk7 底层结构:数组+链表。jdk8 底层结构:数组+链表+红黑树
- 形成链表时:jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素。-->七上八下
- 当数组的某一索引位置上的元素以链表形式存在的数据个数 > 8 并且当前数组长度 > 64 时,
此时此索引位置上的所有数据改用红黑树存储。
③HashMap底层典型属性的属性的说明:
DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
④LinkedHashMap的底层实现原理
LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap。
区别在于:LinkedHashMap内部提供了Entry,替换了HashMap中的Node.
11.6.6 TreeMap的使用
- 向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
因为要照key进行排序:自然排序 、定制排序
11.6.7 使用Properties读取配置文件
//Properties:常用来处理配置文件。key和value都是String类型
public static void main(String[] args) {
FileInputStream fis = null;
try {
Properties pros = new Properties();
fis = new FileInputStream("jdbc.properties");
pros.load(fis);//加载流对应的文件
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
11.7 Collections工具类的使用
11.7.1 作用
- 操作 Collection 和 Map 的工具类
11.7.2 常用方法
方法 | 作用 |
---|---|
void reverse(List) | 反转 List 中元素的顺序 |
void shuffle(List) | 对 List 集合元素进行随机排序 |
void sort(List) | 根据元素的自然顺序对指定 List 集合元素升序排序 |
void sort(List, Comparator) | 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 |
swap(List, int, int) | 将指定 list 集合中的 i 处元素和 j 处元素进行交换 |
Object max(Collection) | 根据元素的自然顺序,返回给定集合中的最大元素 |
Object max(Collection, Comparator) | 根据 Comparator 指定的顺序,返回给定集合中的最大元素 |
Object min(Collection) | 根据元素的自然顺序,返回给定集合中的最小元素 |
Object min(Collection, Comparator) | 根据 Comparator 指定的顺序,返回给定集合中的最小元素 |
int frequency(Collection,Object) | 返回指定集合中指定元素的出现次数 |
void copy(List dest, List src) | 将src中的内容复制到dest中 |
boolean replaceAll(List list, Object oldVal, Object newVal) | 使用新值替换 List 对象的所旧值 |
ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转换为线程安全的。
使用synchronizedList(List list) 和 synchronizedMap(Map map)
11.7.3 面试题:Collection 和 Collections的区别?
Collection:是最基本的集合接⼝,⼀个 Collection 代表⼀组 Object,即 Collection 的元素。它的直接继承接⼝有 List,Set 和 Queue。
Collections:是不属于 Java 的集合框架的,它是集合类的⼀个⼯具类/帮助类。此类不能被实例化, 主要服务于 Java 的 Collection 框架。它包含有关集合操作的静态多态方法,实现对各种集合的搜索、排序、线程安全等操作。
十二、泛型
12.1 泛型的理解
12.1.1 泛型的概念
所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口),用这个类型声明变量、创建对象时确定(即传入实际的类型参数,也称为类型实参)。
12.1.2 泛型的引入背景
集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在 JDK1.5 之前只能把元素类型设计成 Object,JDK1.5 之后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分时确定的,例如关于这个元素如何保存,如何管理等是确定的,因此此时把元素的类型设计成一个参数,这个类型的参数叫做泛型。Collection
12.2 泛型在集合中的使用
12.2.1 在集合中使用泛型之前的例子
@Test
public void test1(){
ArrayList list = new ArrayList();
//需求:存放学生的成绩
list.add(78);
list.add(76);
list.add(89);
list.add(88);
//问题一:类型不安全
// list.add("Tom");
for(Object score : list){
//问题二:强转时,可能出现ClassCastException
int stuScore = (Integer) score;
System.out.println(stuScore);
}
}
-
图示:
12.2.2 在集合中使用泛型例子1
@Test
public void test2(){
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(78);
list.add(87);
list.add(99);
list.add(65);
//编译时,就会进行类型检查,保证数据的安全
// list.add("Tom");
//方式一:
// for(Integer score : list){
// //避免了强转操作
// int stuScore = score;
// System.out.println(stuScore);
// }
//方式二:
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
int stuScore = iterator.next();
System.out.println(stuScore);
}
}
- 图示
12.2.3 在集合中使用泛型例子2
//在集合中使用泛型的情况:以HashMap为例
@Test
public void test3(){
//Map<String,Integer> map = new HashMap<String,Integer>();
//jdk7新特性:类型推断
Map<String,Integer> map = new HashMap<>();
map.put("Tom",87);
map.put("Jerry",87);
map.put("Jack",67);
//map.put(123,"ABC");
//泛型的嵌套
Set<Map.Entry<String,Integer>> entry = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
while(iterator.hasNext()){
Map.Entry<String, Integer> e = iterator.next();
String key = e.getKey();
Integer value = e.getValue();
System.out.println(key + "----" + value);
}
}
12.2.4 集合中使用泛型总结:
- 集合接口或集合类在 jdk5.0 时都修改为带泛型的结构。
- 在实例化集合类时,可以指明具体的泛型类型
- 指明完以后,在集合类或接口中凡是定义类或接口时,内部结构(比如:方法、构造器、属性等)使用到类的泛型的位置,都指定为实例化的泛型类型。
- 注意点:泛型的类型必须是类。不能是基本数据类型。需要用到基本数据类型时,使用包装类替换。
- 如果实例化时,没指明泛型的类型。默认类型为 java.lang.Object 类型。
12.3 自定义泛型类、泛型接口、泛型方法
12.3.1 举例
【Order.java】
public class Order<T> {
String orderName;
int orderId;
//类的内部结构就可以使用类的泛型
T orderT;
public Order(){
//编译不通过
//T[] arr = new T[10];
//编译通过
T[] arr = (T[]) new Object[10];
}
public Order(String orderName,int orderId,T orderT){
this.orderName = orderName;
this.orderId = orderId;
this.orderT = orderT;
}
//泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没任何关系。
//换句话说,泛型方法所属的类是不是泛型类都没关系。
//泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在实例化类时确定。
public static <E> List<E> copyFromArrayToList(E[] arr){
ArrayList<E> list = new ArrayList<>();
for(E e : arr){
list.add(e);
}
return list;
}
//如下的几个方法都不是泛型方法
public T getOrderT(){
return orderT;
}
public void setOrderT(T orderT){
this.orderT = orderT;
}
@Override
public String toString() {
return "Order{" +
"orderName='" + orderName + '\'' +
", orderId=" + orderId +
", orderT=" + orderT +
'}';
}
//静态方法中不能使用类的泛型。
//public static void show(T orderT){
//System.out.println(orderT);
//}
public void show(){
//编译不通过
//try{
//
//}catch(T t){
//
//}
}
}
【SubOrder.java】
public class SubOrder extends Order<Integer> {//SubOrder:不是泛型类
public static <E> List<E> copyFromArrayToList(E[] arr){
ArrayList<E> list = new ArrayList<>();
for(E e : arr){
list.add(e);
}
return list;
}
}
//实例化时,如下的代码是错误的
SubOrder<Integer> o = new SubOrder<>();
【SubOrder1.java】
public class SubOrder1<T> extends Order<T> {//SubOrder1<T>:仍然是泛型类
}
【测试】
@Test
public void test1(){
//如果定义了泛型类,实例化没指明类的泛型,则认为此泛型类型为Object类型
//要求:如果大家定义了类是带泛型的,建议在实例化时要指明类的泛型。
Order order = new Order();
order.setOrderT(123);
order.setOrderT("ABC");
//建议:实例化时指明类的泛型
Order<String> order1 = new Order<String>("orderAA",1001,"order:AA");
order1.setOrderT("AA:hello");
}
@Test
public void test2(){
SubOrder sub1 = new SubOrder();
//由于子类在继承带泛型的父类时,指明了泛型类型。则实例化子类对象时,不再需要指明泛型。
sub1.setOrderT(1122);
SubOrder1<String> sub2 = new SubOrder1<>();
sub2.setOrderT("order2...");
}
@Test
public void test3(){
ArrayList<String> list1 = null;
ArrayList<Integer> list2 = new ArrayList<Integer>();
//泛型不同的引用不能相互赋值。
//list1 = list2;
Person p1 = null;
Person p2 = null;
p1 = p2;
}
//测试泛型方法
@Test
public void test4(){
Order<String> order = new Order<>();
Integer[] arr = new Integer[]{1,2,3,4};
//泛型方法在调用时,指明泛型参数的类型。
List<Integer> list = order.copyFromArrayToList(arr);
System.out.println(list);
}
12.3.2 注意点
- 1、泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1, E2, E3>
- 2、泛型类的构造器如下:public GenericClass(){}
而下面是错误的:public GenericClass(){} - 3、实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。
- 4、泛型不同的引用不能相互赋值。
尽管在编译时 ArrayList和 ArrayList 是两种类型,但是在运行时只有一个 ArrayList 被加载到 JVM 中。 - 5、泛型如果不指定,将被擦除,泛型对应的类型均按 Object 处理,但不等价于 Object 。经验:泛型要使用一路都用,要不用,一路都不要用。
- 6、如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。
- 7、jdk1.7 泛型的简化操作:ArrayList
flist = new ArrayList<>();类型推断。 - 8、泛型的指定中不能使用基本数据类型,可以使用包装类替换。
- 9、在类/接口上声明的泛型,在本类或接口中即代表某种类型,可以作为非静态属性的类型、分静态方法的参数类型,非静态方法的返回值类型。但在静态方法中不能使用类的泛型。
- 10、异常类不能是泛型的。
- 11、不能使用 new E[]。但是可以:E[] elements = (E[])new Objext[capacity];
参考:ArrayList 源码中声明:Object[] elementData,而非泛型参数类型数组。 - 12、父类有泛型,子类可以选择保留泛型也可以指定泛型类型:
- 子类不保留父类的泛型:按需实现
- 没有类型,擦除
- 具体类型
- 子类保留父类的泛型:泛型子类
- 全部保留
- 部分保留
- 结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
- 子类不保留父类的泛型:按需实现
12.3.3 应用场景举例
【DAO.java】:定义了操作数据库中的表的通用操作。ORM思想(数据库中的表和Java中的类对应)
public class DAO<T> {//表的共性操作的DAO
//添加一条记录
public void add(T t){
}
//删除一条记录
public boolean remove(int index){
return false;
}
//修改一条记录
public void update(int index,T t){
}
//查询一条记录
public T getIndex(int index){
return null;
}
//查询多条记录
public List<T> getForList(int index){
return null;
}
//泛型方法
//举例:获取表中一共有多少条记录?获取最大的员工入职时间?
public <E> E getValue(){
return null;
}
}
【CustomerDAO.java】:
public class CustomerDAO extends DAO<Customer>{//只能操作某一个表的DAO
}
【StudentDAO.java】:
public class StudentDAO extends DAO<Student> {//只能操作某一个表的DAO
}
12.4 泛型在继承上的体现
虽然类 A 是类 B 的父类,但是 G<A\>
和 G<B> 二者不具备子父类关系,二者是并列关系。
补充:类 A 是类 B 的父类,A<G> 是 B<G> 的父类。
@Test
public void test1(){
Object obj = null;
String str = null;
obj = str;
Object[] arr1 = null;
String[] arr2 = null;
arr1 = arr2;
//编译不通过
//Date date = new Date();
//str = date;
List<Object> list1 = null;
List<String> list2 = new ArrayList<String>();
//此时的list1和list2的类型不具子父类关系
//编译不通过
//list1 = list2;
/*
反证法:
假设list1 = list2;
list1.add(123);导致混入非String的数据。出错。
*/
show(list1);
show1(list2);
}
public void show1(List<String> list){
}
public void show(List<Object> list){
}
@Test
public void test2(){
AbstractList<String> list1 = null;
List<String> list2 = null;
ArrayList<String> list3 = null;
list1 = list3;
list2 = list3;
List<String> list4 = new ArrayList<>();
}
12.5 通配符
-
通配符的使用
通配符:?
类 A 是类 B 的父类,G<A>
和G<B>
是没关系的。二者共同的父类是:G<?>
@Test public void test3(){ List<Object> list1 = null; List<String> list2 = null; List<?> list = null; list = list1; list = list2; //编译通过 print(list1); print(list2); List<String> list3 = new ArrayList<>(); list3.add("AA"); list3.add("BB"); list3.add("CC"); list = list3; //添加(写入):对于List<?>就不能向其内部添加数据。 //除了添加null之外。 //list.add("DD"); //list.add('?'); list.add(null); //获取(读取):允许读取数据,读取的数据类型为Object。 Object o = list.get(0); System.out.println(o); } public void print(List<?> list){ Iterator<?> iterator = list.iterator(); while(iterator.hasNext()){ Object obj = iterator.next(); System.out.println(obj); } }
-
涉及通配符的集合的数据的写入和读取:见上
-
有限制条件的通配符的使用
- ? ectends A:
G<? extends A>
可以作为G<A>
和G<B>
的父类,其中 B 是 A 的子类。 - ? super A:
G<? super A>
可以作为G<A>
和G<B>
的父类,其中 B 是 A 的子类。
@Test public void test4(){ List<? extends Person> list1 = null; List<? super Person> list2 = null; List<Student> list3 = new ArrayList<Student>(); List<Person> list4 = new ArrayList<Person>(); List<Object> list5 = new ArrayList<Object>(); list1 = list3; list1 = list4; //list1 = list5; //list2 = list3; list2 = list4; list2 = list5; //读取数据: list1 = list3; Person p = list1.get(0); //编译不通过 //Student s = list1.get(0); list2 = list4; Object obj = list2.get(0); //编译不通过 //Person obj = list2.get(0); //写入数据: //编译不通过 //list1.add(new Student()); //编译通过 list2.add(new Person()); list2.add(new Student()); }
- ? ectends A:
十三、IO流
13.1 File类的使用
13.1.1 File类的理解
- File 类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)
- File 类声明在 Java.io 包下
- File 类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法;并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用 IO 流来完成。
- 后续 File 类的对象常会作为参数传递到流的构造器中,指明读取或写入的“终点”。
13.1.2 File 的实例化
- 常用构造器
- File(String filePath)
- File(String parentPath, String childPath)
- File(File parentFile, String childPath)
- 路径的分类
- 相对路径:相较于某个路径下,指明的路径。
- 绝对路径:包含盘符在内的文件或文件目录的路径
- 在 IDEA 中:
使用 JUnit 中的单元测试方法测试,相对路径即为当前 Module 下。使用 main() 测试,相对路径即为当前的 Project 中。 - Eclipse 中
不管使用单元测试方法还是使用 main() 测试,相对路径都是当前 Project 下。
- 在 IDEA 中:
- 路径分隔符
- windows 和 DOS 系统默认使用 “\” 来表示
- UNIX 和 URL 使用 “/” 来表示
13.1.3 File 类的常用方法
13.2 IO流概述
13.2.1 流的分类
- 操作数据单位:字节流、字符流
- 数据的流向:输入流、输出流
- 流的角色:节点流、处理流
13.2.2 流的体系结构
说明:红框对应的是 IO 流中的4个抽象基类
蓝框的流需要重点关注
13.2.3 重点说明的几个流结构
抽象基类 | 节点流(或文件流) | 缓冲流(处理流的一种) |
---|---|---|
InputStream | FileInputStream (read(byte[] buffer)) | BufferedInputStream (read(bute[] buffer)) |
OutputStream | FileOutputStream (write(byte[] buffer, 0, len)) | BufferedReader (write(byte[] buffer, 0, len)) / flush() |
Reader | FileReader (read(char[] cbuf)) | BufferedReader (read(char[] cbuf)) / readLine() |
Writer | FileWriter (write(char[] cbuf, 0, len)) | BufferedWriter (write(char[] cbuf, 0, len)) / flush() |
13.2.4 输入、输出的标准化过程
- 输入过程
- ① 创建 File 类的对象,指明读取的数据的来源。(要求此文件一定要存在)
- ② 创建相应的输入流,将 File 类的对象作为参数,传入流的构造器中。
- ③ 具体的读入过程:创建相应的 byte[] 或 char[]
- ④ 关闭流资源
- 说明:程序中出现的异常需要使用 try-catch-finally 处理
- 输出过程
- ① 创建 File 类的对象,指明写出的数据的位置。(不要求此文件一定要存在)
- ② 创建相应的输出流,将 File 类的对象作为参数,传入流的构造器中
- ③ 具体的写入过程:write(Char[] / byte[] buffer, 0, len)
- ④ 关闭流资源
- 说明:程序中出现的异常需要使用 try-catch-finally 处理
13.3 节点流(或文件流)
13.3.1 FileReader / FileWriter 的使用:
-
FileReader的使用
/* 将day09下的hello.txt文件内容读入程序中,并输出到控制台 说明点: 1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1 2. 异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理 3. 读入的文件一定要存在,否则就会报FileNotFoundException。 */ @Test public void testFileReader1() { FileReader fr = null; try { //1.File类的实例化 File file = new File("hello.txt"); //2.FileReader流的实例化 fr = new FileReader(file); //3.读入的操作 //read(char[] cbuf):返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1 char[] cbuf = new char[5]; int len; while((len = fr.read(cbuf)) != -1){ //方式一: //错误的写法 //for(int i = 0;i < cbuf.length;i++){ // System.out.print(cbuf[i]); //} //正确的写法 //for(int i = 0;i < len;i++){ // System.out.print(cbuf[i]); //} //方式二: //错误的写法,对应着方式一的错误的写法 //String str = new String(cbuf); //System.out.print(str); //正确的写法 String str = new String(cbuf,0,len); System.out.print(str); } } catch (IOException e) { e.printStackTrace(); } finally { if(fr != null){ //4.资源的关闭 try { fr.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
FileWriter的使用
/* 从内存中写出数据到硬盘的文件里。 说明: 1. 输出操作,对应的File可以不存在的。并不会报异常 2. File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。 File对应的硬盘中的文件如果存在: 如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原文件的覆盖 如果流使用的构造器是:FileWriter(file,true):不会对原文件覆盖,而是在原文件基础上追加内容 */ @Test public void testFileWriter() { FileWriter fw = null; try { //1.提供File类的对象,指明写出到的文件 File file = new File("hello1.txt"); //2.提供FileWriter的对象,用于数据的写出 fw = new FileWriter(file,false); //3.写出的操作 fw.write("I have a dream!\n"); fw.write("you need to have a dream!"); } catch (IOException e) { e.printStackTrace(); } finally { //4.流资源的关闭 if(fw != null){ try { fw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
-
文本文件的复制:
@Test public void testFileReaderFileWriter() { FileReader fr = null; FileWriter fw = null; try { //1.创建File类的对象,指明读入和写出的文件 File srcFile = new File("hello.txt"); File destFile = new File("hello2.txt"); //不能使用字符流来处理图片等字节数据 //File srcFile = new File("爱情与友情.jpg"); //File destFile = new File("爱情与友情1.jpg"); //2.创建输入流和输出流的对象 fr = new FileReader(srcFile); fw = new FileWriter(destFile); //3.数据的读入和写出操作 char[] cbuf = new char[5]; int len;//记录每次读入到cbuf数组中的字符的个数 while((len = fr.read(cbuf)) != -1){ //每次写出len个字符 fw.write(cbuf,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.关闭流资源 //方式一: //try { // if(fw != null) // fw.close(); //} catch (IOException e) { // e.printStackTrace(); //}finally{ // try { // if(fr != null) // fr.close(); // } catch (IOException e) { // e.printStackTrace(); // } //} //方式二: try { if(fw != null) fw.close(); } catch (IOException e) { e.printStackTrace(); } try { if(fr != null) fr.close(); } catch (IOException e) { e.printStackTrace(); } } }
13.3.2 FileInputStream / FileOutputStream 的使用
-
- 对于文本文件(.txt,.java,.c,.cpp),使用字符流处理
-
- 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),使用字节流处理
/*
实现对图片的复制操作
*/
@Test
public void testFileInputOutputStream() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1.造文件
File srcFile = new File("爱情与友情.jpg");
File destFile = new File("爱情与友情2.jpg");
//2.造流
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
//3.复制的过程
byte[] buffer = new byte[5];
int len;
while((len = fis.read(buffer)) != -1){
fos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos != null){
//4.关闭流
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 注意:相对路径在IDEA和Eclipse中使用的区别?
- IDEA:
如果使用单元测试方法,相对路径基于当前的Module的。
如果使用main()测试,相对路径基于当前Project的。 - Eclipse:
无论单元测试方法还是main(),相对路径都是基于当前Project的。
- IDEA:
13.4 缓冲流的使用
13.4.1 缓冲流涉及到的类:
- BufferedInputStream
- BufferedOutputStream
- BufferedReader
- BufferedWriter
13.4.2 作用
- 提供流的读取、写入的速度
提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb
13.4.3 典型代码
-
使用B使用BufferedInputStream和BufferedOutputStream 处理非文本文件
//实现文件复制的方法 public void copyFileWithBuffered(String srcPath,String destPath){ BufferedInputStream bis = null; BufferedOutputStream bos = null; try { //1.造文件 File srcFile = new File(srcPath); File destFile = new File(destPath); //2.造流 //2.1 造节点流 FileInputStream fis = new FileInputStream((srcFile)); FileOutputStream fos = new FileOutputStream(destFile); //2.2 造缓冲流 bis = new BufferedInputStream(fis); bos = new BufferedOutputStream(fos); //3.复制的细节:读取、写入 byte[] buffer = new byte[1024]; int len; while((len = bis.read(buffer)) != -1){ bos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { //4.资源关闭 //要求:先关闭外层的流,再关闭内层的流 if(bos != null){ try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } if(bis != null){ try { bis.close(); } catch (IOException e) { e.printStackTrace(); } } //说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略. //fos.close(); //fis.close(); } }
-
使用BufferedReader和BufferedWriter:处理文本文件
@Test public void testBufferedReaderBufferedWriter(){ BufferedReader br = null; BufferedWriter bw = null; try { //创建文件和相应的流 br = new BufferedReader(new FileReader(new File("dbcp.txt"))); bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt"))); //读写操作 //方式一:使用char[]数组 //char[] cbuf = new char[1024]; //int len; //while((len = br.read(cbuf)) != -1){ // bw.write(cbuf,0,len); // //bw.flush(); //} //方式二:使用String String data; while((data = br.readLine()) != null){ //方法一: //bw.write(data + "\n");//data中不包含换行符 //方法二: bw.write(data);//data中不包含换行符 bw.newLine();//提供换行的操作 } } catch (IOException e) { e.printStackTrace(); } finally { //关闭资源 if(bw != null){ try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } if(br != null){ try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
13.5 转换流的使用
13.5.1 转换流涉及到的类:属于字符流
- InputStreamReader : 讲一个字节的输入流转换为字符的输入流
解码:字节、字节数组 ---->字符数组、字符串 - OutputStreamWriter:将一个字符的输出流转换为字节的输出流
编码:字符数组、字符串 ----> 字节、字节数组 - 说明:编码决定了解码的方式;文件编码的方式(比如:GBK),决定了解析时使用的字符集(也只能是GBK)。
13.5.2 作用
- 提供字节流与字符流之间的转换
13.5.3 图示
13.5.4 典型实现
@Test
public void test1() throws IOException {
FileInputStream fis = new FileInputStream("dbcp.txt");
//InputStreamReader isr = new InputStreamReader(fis);//使用系统默认的字符集
//参数2指明了字符集,具体使用哪个字符集,取决于文件dbcp.txt保存时使用的字符集
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");//使用系统默认的字符集
char[] cbuf = new char[20];
int len;
while((len = isr.read(cbuf)) != -1){
String str = new String(cbuf,0,len);
System.out.print(str);
}
isr.close();
}
/*
此时处理异常的话,仍然应该使用try-catch-finally
综合使用InputStreamReader和OutputStreamWriter
*/
@Test
public void test2() throws Exception {
//1.造文件、造流
File file1 = new File("dbcp.txt");
File file2 = new File("dbcp_gbk.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");
//2.读写过程
char[] cbuf = new char[20];
int len;
while((len = isr.read(cbuf)) != -1){
osw.write(cbuf,0,len);
}
//3.关闭资源
isr.close();
osw.close();
}
补充:常用的编码表
名称 | 含义 | 字节数 |
---|---|---|
ASCII | 美国标准信息交换码 | 用一个字节的7位可以表示 |
ISO8859-1 | 拉丁码表,欧洲码表 | 用一个字节的8位表示 |
GB2312 | 中国的中文编码表 | 最多两个字节编码所有字符 |
GBK | 中国的中文编码表升级,融合了更多的中文文字符号 | 最多两个字节编码 |
Unicode | 国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码 | 所有的文字都用两个字节来表示 |
UTF-8 | 常用 | 可用1-4个字节来表示一个字符 |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步