首页 > 其他 > 详细

知识点

时间:2020-10-19 09:45:57      阅读:38      评论:0      收藏:0      [点我收藏+]

第一章 Java概述

1、Java的发展历史

Java语言诞生于SUN(Stanford University Network),现在是属于Oracle(甲骨文)。

Java于1996年初正式发布开发版1.0。

Java最新版本是14,目前使用最广的稳定版本8。

Java之父詹姆斯.高斯林。

2、Java的特点

Java是面向对象、健壮的、安全的、支持分布式开发、跨平台的。

跨平台的本质是靠JVM来实现。

JVM:Java Virtual Machine,Java虚拟机。它只识别字节码文件。

JRE:Java Runtime Environment,Java运行环境。JRE = JVM + 核心类库。

JDK:Java Development Kit‘s,Java开发工具集。JDK = JRE + 开发工具。

第二章 Java基础语法

2.1 注释

单行注释:// 注释内容

多行注释:/* 注释内容 */,不能嵌套

文档注释:/** 注释内容 */

2.2 关键字

Java的关键字:50个

Java的保留字:const,goto,在关键字范围内。

Java的特殊值:true, false, null

技术分享图片

2.3 标识符

标识符:凡是自己命名的部分都是标识符。例如:变量、类、方法等。

标识符的命名规则:

(1)必须由26个英文字母大小写、0-9的数字、下划线_、美元符号$组成

(2)不能数字开头

(3)不能包含空格

(4)不能使用关键字和特殊值

(5)严格区分大小写

标识符的命名规范:

(1)类名、接口名等:每一个单词的首字母大写,形式:XxxYyyZzz

(2)变量、方法名等:从第二个单词开始,首字母大写,形式:xxxYyyZzz

(3)包名:所有单词都小写,形式:xxx.yyy.zzz

(4)常量名:所有单词都大写,形式:XXX_YYY_ZZZ

(5)见名知意

2.4 初识数据类型

分为两大类:

1、基本数据类型(8种)

byte,short,int,long,float,double,char,boolean

2、引用数据类型

类、接口、数组、枚举、注解等

String类型属于类。

2.5 常量

分为两大类:

1、字面常量:

例如:"hello",15,true等

2、声明常量

例如:

final double PI = 3.1415926;
final double MAX_AGE = 120;

3、常量值

字符串常量值:例如:"hello",必须加双引号
单字符常量值:例如:‘a‘,必须加单引号
float常量值:例如:1.2F,必须加F或f
long常量值:例如:123456L,如果超过int的范围必须加L或l
整数常量值:
	如果在byte范围内,可以直接赋值给byte类型;
	如果在short范围,可以直接赋值给short类型;
	否则就是默认int类型。

2.6 变量

1、变量的本质

代表了一块内存区域,这块内存区域中的值是会变的。

2、变量三要素

(1)数据类型

(2)变量名

(3)变量值

3、变量使用有几个要求

(1)先声明后使用

声明的格式:

数据类型  变量名;

(2)在使用之前必须初始化

变量名 = 变量值;

很多时候声明和初始化在一句完成:

数据类型  变量名 = 变量值;

(3)变量有作用域,在作用域范围不能重复声明,出了作用域不能使用

2.7 两种输出语句

1、输出并换行

System.out.println(输入内容);

2、输出不换行

System.out.print(输入内容);

3、示例

//输出常量
System.out.println(常量);
System.out.println("hello");
System.out.println(true);

//输出变量
System.out.println(变量名);
int age = 18;
System.out.println(age);

//常量和变量拼接输出
System.out.println(常量和变量使用+拼接);
System.out.println("柴老师的年龄是" + age +"岁");

2.8 计算机存储

2.8.1 进制

二进制:数字是0-1,逢二进一,计算机中识别,在程序中表示在数字前面加0B或0b

八进制:数字是0-7,逢八进一,在程序中表示在数字前面加0

十进制:数字是0-9,逢十进一,生活熟悉,在程序中正常表示

十六进制:数字0-9,字母a-f(A-F),逢十六进一,在程序中表示在数字前面加0X或0x

2.8.2 计算机存储单位

最小单位:位(bit)

最基本单位:字节(byte)

1KB=1024Byte

1MB=1024KB

1GB=1024MB

1TB=1024GB

1PB=1024TB

2.8.3 Java的基本数据类型的存储宽度

byte:1个字节,范围:-128~127,其中的-128是特殊规定

short:2个字节,范围:-32768~32767

int:4个字节

long:8个字节,当某个常数整数超过int范围,要加L

float:4个字节,但是它比long的存储范围大,在数字后面加F或f

double:8个字节

char:2个字节,范围:0~65535

boolean:1位,只有true/false,底层就是1和0

补充:char

每一个字符都对应有一个唯一的编码值,早期是ASCII码,现在是Unicode字符集。

‘0‘:48

‘A‘:65

‘a‘:97

几个特殊的转义字符:

\n:换行

\t:制表符(Tab)

\b:退格键

\r:回车键

\\:表示斜杆本身

\‘:表示单引号

\":表示双引号

2.8.4 计算机底层存储规则

1、最高位是符号位:0表示正数、1表示负数

2、都是补码形式存储

正数:原码、反码、补码是一样的

负数:原码、反码、补码是不同的

? 反码:原码的符号位不变,其余位取反

? 补码:补码=反码+1

3、小数

(1)先把小数转为二进制:整数部分是除2倒取余数、小数部分是乘2取整数位

(2)用科学计数法表示该二进制,整部是1,小数部分是尾数,指数部分是表示小数点移动了几位,小数点往左移是正,小数点往右移是负

(3)符号位+指数值+尾数

2.9 基本数据类型的转换(重要)

1、自动类型转换

byte->short->int->long->float->double

? char->

(1)当把存储范围小的变量/常量赋值给存储范围大的变量时,会自动类型提升

(2)当byte、short、char类型进行计算时,会自动升级为int

(3)当很多类型一起混合运算时,结果取他们中最大的

(4)boolean不参与

2、强制类型转换

double->float->long->int->short->byte

? ->char

(1)当把存储范围大的变量/常量赋值给存储范围小的变量时,需要强制类型转换,有风险,可能是溢出或损失精度

(2)当我们需要刻意提升某个常量/变量的数据类型时,也可以使用强制类型转换,没有风险

(3)boolean不参与

强制类型转换的格式
(需要转为的类型)需要转换的变量/常量/(表达式)

例如:
int num = 12;
byte b = (byte)num;

int code = 97;
char c = (char)code;

int x = 3;
int y = 65;
char letter = (char)(x+y);

3、基本数据类型与字符串

(1)任意数据类型与字符串做了“+”拼接,结果都是字符串

(2)字符串不能通过强制类型转换(),转为基本数据类型,如果需要转,需要使用后面的包装类来实现

例如:Integer.parseInt("字符串")等

2.10 运算符

1、按照操作数的个数分:

一元运算符、二元运算符、三元运算符

三元运算符:? :

一元运算符:正号、负号、逻辑非、按位取反、自增、自减等只需要一个操作数

二元运算符:其他都是二元运算符,例如:加、减、大于、逻辑与....

2、按照功能分

(1)算术运算符

加:+,特别注意,字符串参与的+表示拼接
减:-
乘:*
除:/,当两个整数相除结果是只保留整数部分
模:%,模数的结果的符号只看被模数
正号:+
负号:-
自增:++
自减:--
	(1)如果自增/自减表达式是独立的,那么自增/自减在前在后都一样
	(2)如果自增/自减表达式不是独立的,那么自增/自减在前,先自增/自减,然后取值,然后用取出来的值做其他运算
	                             那么自增/自减在前,先取值,然后自增/自减,然后用取出来的值做其他运算

(2)赋值运算符

基本赋值运算符:=
扩展的赋值运算符:+=,-=,*=,/=,%=,>>=,<<=。。。
	注意:=左边一定是一个变量,并且该变量的类型是>=右边的值或结果;
	     一定是把右边的结果赋值给左边的变量,而且赋值是最后计算的。
	     如果是扩展的赋值运算符,如果右边的结果计算完的类型超过左边的变量,会隐含强制类型转换,所以可能结果是溢出或损失精度。	     

(3)比较运算符/关系运算符

大于:>
小于:<
大于等于:>=
小于等于:<=
等于:==
不等于:!=

所有关系运算符的表达式结果是boolean类型

(4)逻辑运算符

逻辑与:&
	只有两边都为true,结果才为true
逻辑或:|
	只要有一边为true,结果就为true
逻辑异或:^
	只有两边不同,一个是true,一个为false,结果才为true
逻辑非:!
	!true为false,!false为true
短路与:&&
	只有两边都为true,结果才为true;
	当左边为false,右边的表达式不看/不算。
短路或:||
	只要有一边为true,结果就为true;
	当左边为true,右边的表达式不看/不算。
	
逻辑运算符两边都是boolean值、表达式,结果也是boolean值;	

(5)条件运算符

条件表达式 ? 结果表达式1 : 结果表达式

当条件表达式为true时,整个表达式是是取结果表达式1 的值,否则取结果表达式2的值

(6)位运算符

左移:<<
	快速运算口诀:左移几位,乘以2的几次方
右移:>>
	快速运算口诀:右移几位,除以2的几次方
无符号右移:>>>
	正数:无符号右移和右移一样
	负数:无符号右移会变为正数
按位与:&
	对应位都是1,结果是1
按位或:|
	对应位只要有1,结果就是1
按位异或:^
	对应位一个1一个0,结果为1
按位去反:~
	原来1变为0,0变为1

注意:结果还是运算过程中都是基于“补码”,给然看结果要转为原码

技术分享图片

提示说明:

(1)表达式不要太复杂

(2)先算的使用()

大体的排序:算术->位-->比较-->逻辑-->三元-->赋

第三章 流程控制语句结构

流程控制语句结构分为三大类:

1、顺序结构:最基本的结构,程序从上往下依次执行

2、分支结构:从多个分支中选择其中一个分支执行

(1)条件判断:if...else

(2)选择结构:switch...case

3、循环结构:重复执行某些语句

(1)for

(2)while

(3)do...while

一、输入输出

1、输出

System.out.println(输出内容); //输出xx并换行
System.out.print(输出内容);//输出xx不换行,除非在输出内容中包含了\n才会换行
System.out.printf("输出内容",...);

例如:
System.out.printf("xx%d,xx%.2f,xx%c,xx%s",变量1,变量2,变量3,变量4);
变量1:对应%d,是整型
变量2:对应%f,是浮点型
变量3:对应%c,是字符型
变量4:对应%s,是字符串类型

2、输入

步骤:

java.util.Scanner input = new java.util.Scanner(System.in);

System.out.print("提示输入xx:");
变量 = input.nextXxx();

int类型:input.nextInt();
double类型:input.nextDouble();
boolean类型:input.nextBoolean();
long类型:input.nextLong();
String类型:input.next();
char类型:input.next().charAt(索引);  索引从0开始

二、分支结构

1、条件判断

(1)单分支

语法格式:

if(条件表达式){
	语句块; //语句块的意思可以是一条或多条语句;
}

执行过程:

如果if()中的条件表达式成立,就执行{}中的语句块; 否则就不执行

注意点:

(1)if()中的条件表达式必须是boolean类型

(2)当{}中只有一条语句时,可以省略{}

(2)双分支

语法格式:

if(条件表达式){
	语句块1; //语句块的意思可以是一条或多条语句;
}else{
	语句块2;
}

执行过程:

如果if()中的条件表达式成立,就执行{}中的语句块1; 否则就执行语句块2;

注意点:

(1)if()中的条件表达式必须是boolean类型

(2)当{}中只有一条语句时,可以省略{}

(3)多分支

语法格式:

if(条件表达式1){
	语句块1; //语句块的意思可以是一条或多条语句;
}else if(条件表达式2){
	语句块2;
}else if(条件表达式3){
	语句块3;
}
....
【else{
	语句块n+1;
}】

执行过程:

(1)多个条件表达式是从上往下的顺序判断

(2)如果上面的某个条件表达式成立了,下面的条件表达式就不看了

(3)多个分支最终只会执行其中的一个,如果没有最后独立的else,可能会一个分支都不执行。

注意点:

(1)if()中的条件表达式必须是boolean类型

(2)当{}中只有一条语句时,可以省略{}

(3)else if中间有空格,只有在if后面可以写条件

(4)嵌套

外面的分支满足条件了,才会看里面嵌套的分支

2、选择结构

语法格式:

switch(表达式){
	case 常量值1:
		语句块1;
		【break;】
	case 常量值2:
		语句块2;
		【break;】
	case 常量值3:
		语句块3;
		【break;】
	...
	【default:
		语句块n+1;
		break;】
	】
}

执行过程:

(1)入口

①当switch(表达式)中表达式的值与某个case后面的常量值匹配/相等了,就从这个case进入;

②当switch(表达式)中表达式的值与所有case后面的值都不匹配,如果有default,从default进入;

先看case,如果case没有满足的,才会看default,不管default的位置在哪里。

(2)出口

①自然出口:switch结构的结束}

②中断出口:break,return等

(3)贯穿

从入口进入之后,在遇到出口之前,会从一个case贯穿到另一个case执行。

注意点:

(1)switch(表达式)中表达式结果的类型必须是byte,short,int,char,枚举(JDK1.5),字符串(JDK1.7)

(2)case后面必须是常量值,而且不能重复,case与常量值之间有空格,常量值后面是:

三、循环结构

1、什么情况下使用循环

当某些语句需要“重复”执行的时候,就可以考虑使用循环结构

2、循环结构的形式

1、for循环

2、while循环

3、do...while循环

区别与联系:

(1)他们的功能是一样,都是可以实现重复执行某些代码的功能,所以三种循环可以互换

(2)但是习惯上

  • 当循环的次数比较明显,例如:从几循环到几,一般先考虑使用for;
  • 当循环的次数不明显,但是循环条件比较清晰,一般考虑使用while;
  • 当循环的次数不明显,循环体至少执行一次,一般考虑使用do...while;

3、for循环

语法格式:

for(【初始化表达式】; 【循环条件表达式】; 【迭代表达式】){
	循环体语句块;
}

执行过程:

(1)先执行【初始化表达式】;

(2)判断【循环条件表达式】;

(3)如果循环条件表达式为true,执行循环体语句块;,然后执行 【迭代表达式】,再回到(2)

? 如果循环条件表达式为false,直接结束for

注意点:

(1)for(;;)两个;不能多也不能少

(2)for循环结构的循环体语句块可能一次都不执行,即当第一次判断循环条件时就不成立

(3)for(;;)它没有break等配合的话,就是死循环

4、while循环

语法格式:

while(循环条件表达式){ 
	循环体语句块;
}

执行过程:

(1)判断循环条件表达式

(2)如果循环条件表达式为true,执行{循环体语句块;},然后回到(1)

? 如果循环条件表达式为false,直接结束while

注意点:

(1)循环条件表达式不能省略,如果要表示永远成立,可以写while(true)

(2)while循环结构的循环体语句块可能一次都不执行,即当第一次判断循环条件时就不成立

(3)while(true)它没有break等配合的话,就是死循环

5、do...while循环

语法格式:

do{ 
	循环体语句块;
}while(循环条件表达式);

执行过程:

(1)先不管三七二十一,即不看循环条件,执行第一次的{循环体语句块;}

(2)再判断循环条件表达式

(3)如果循环条件表达式为true,再次执行{循环体语句块;},然后回到(2)

? 如果循环条件表达式为false,直接结束do...while

注意点:

(1)循环条件表达式不能省略,如果要表示永远成立,可以写while(true);

(2)do...while循环结构的循环体语句块至少执行一次

(3)do...while(true)它没有break等配合的话,就是死循环

(4)do{} while(循环条件表达式)的最后一定要有;

四、跳转语句

1、break

用法:

(1)在switch结构中,结束当前的switch

(2)在循环结构,结束当前的循环

2、continue

用法:

只能用在循环结构中,用于表示提前结束本次循环,continue下面的循环体语句块本次就不执行了,进入下一次循环的准备

3、return后面再讲

return用于结束当前方法

第四章 数组

4.1 数组的概念

数组:表示一组相同类型的数据/变量的集合,使用统一的名称来管理它们,这个统一的名称就是“数组名”,再通过编号来区分每一个数据,这个编号我们称为“下标/索引”,因为这组数据是有限个,它们的总个数称为“数组的长度”。

特点:

(1)数组的长度一旦确定就不能修改

换句话说,如果需要修改数组的长度,那么就意味着要建新的数据,然后把数据复制过去。

(2)数组在内存中是开辟连续的存储空间,数组名中记录的这块连续存储空间的“首地址”

(3)数组存取速度很快,可以根据“首地址” + “下标” 可以快速的定位到某个元素的位置

4.2 一维数组

1、声明

元素的数据类型[] 数组名;//标准格式

元素的数据类型  数组名[];//非标准形式

说明:声明了数组,并没有在堆中申请空间来存储元素的值

2、初始化

(1)静态初始化

数组名 = new 元素的数据类型[]{元素1,元素2,元素3。。。。};

//简化形式:声明和静态初始化在一句完成时才能简化
元素的数据类型[] 数组名 = {元素1,元素2,元素3。。。。};

(2)动态初始化

数组名 = new 元素的数据类型[长度];//这个new意味着在堆中申请了连续的存储空间,用来存储元素的值,此时元素都是默认值

数组名[下标] = 值; //这个值可以是一个常量,可以是键盘输入的,可以是随机产生的,。。。。

3、数组的遍历

for(int i=0; i<数组名.length; i++){
	.....
}

4、注意

  • 一维数组的长度:数组名.length
  • 一维数组的元素:数组名[下标],下标的范围:[0, 数组名.length-1]
  • 数组元素的默认值:
    • byte,short,int,long:0
    • flloat,double:0.0
    • char:\u0000,是编码值为0的字符
    • boolean:false
    • 引用数据类型:null

4.3 二维数组

1、声明

元素的数据类型[][] 数组名;//标准格式

元素的数据类型  数组名[][];//非标准形式

元素的数据类型[] 数组名[];//非标准形式

说明:声明了数组,没有在堆中申请空间来表示行的空间,也没有申请空间来存储元素的值,即表示该二维数组还不能使用,必须初始化才能使用

2、初始化

(1)静态初始化

数组名 = new 元素的数据类型[][]{{第一行的元素1,第一行的元素2,第一行的元素3。。。。}, {第二行的元素1,第二行的元素2,第二行的元素3。。。。},....};

//简化形式:声明和静态初始化在一句完成时才能简化
元素的数据类型[][] 数组名 = {{第一行的元素1,第一行的元素2,第一行的元素3。。。。}, {第二行的元素1,第二行的元素2,第二行的元素3。。。。},....};

(2)动态初始化

每一行的元素个数是相同:

数组名 = new 元素的数据类型[行的总数][每一行的元素的总个数];//这个new意味着在堆中申请了存储空间,这个存储空间首先有多个行元素的存储空间,然后每一行又有独立的存储空间用来存储元素的值

数组名[行下标][列下标] = 值; //这个值可以是一个常量,可以是键盘输入的,可以是随机产生的,。。。。

技术分享图片

每一行的元素个数不同:

数组名 = new 元素的数据类型[行的总数][];//这个new只是申请行元素的空间,该空间用来存储每一行的首地址用的,默认值是null

数组名[行下标] = new 元素的数据类型[该行的元素总个数];//有几行,就需要new几次,这个new是申请每一行的独立的存储空间用来存储元素的值,这个时候元素的默认值看元素的数据类型

数组名[行下标][列下表] = 值; //这个值可以是一个常量,可以是键盘输入的,可以是随机产生的,。。。。

技术分享图片

3、数组的遍历

for(int i=0; i<数组名.length; i++){
	for(int j=0; j<数组名[i].length; j++){
    	//....
    }
}

4、注意

  • 二维数组名:存储的是行元素的首地址
  • 二维数组的总行数:数组名.length
  • 二维数组每一行的元素个数/长度:数组名[行下标].length
  • 二维数组的每一行表示方式:数组名[行下标]
    • 行下标的范围:[0, 数组名.length-1]
    • 数组名[行下标]中存储的是该行的首地址
  • 二维数组的元素:数组名[行下标][列下标]
    • 行下标的范围:[0, 数组名.length-1]
    • 列下标的方位:[0, 数组名[行下标].length-1]
  • 二维数组的行元素的默认值:null
  • 二维数组元素的默认值:
    • byte,short,int,long:0
    • flloat,double:0.0
    • char:\u0000,是编码值为0的字符
    • boolean:false
    • 引用数据类型:null

4.4 一维数组的基础算法(第一部分)

1、统计元素的情况

例如:总分、平均分、偶数的个数、素数的个数、xx倍数的个数

步骤:遍历每一个元素,然后累加或者挨个做判断并统计
//例如:判断某个整型数组中元素偶数的个数
int[] arr = {....};

int count = 0;
for(int i=0; i<arr.length; i++){
	if(arr[i]%2==0){
		count++;
	}
}

2、找最大值/最小值

步骤:
第一步:假设第一个元素最大/最小
第二步:用后面的元素一一与这个变量比较,如果有比它大/小的修改变量

//例如:找某个整型数组的最大值
int[] arr = {....};

int max = arr[0];
for(int i=1; i<arr.length; i++){
	if(arr[i] > max){
        max = arr[i];
    }
}

3、找最大值/最小值及其下标

步骤:
第一步:假设第一个元素最大/最小,记录最大或最小值的下标
第二步:用后面的元素一一与这个变量比较,如果有比它大/小的修改变量
    
//例如:找某个整型数组的最大值及其下标
int[] arr = {....};

int max = arr[0];
int index = 0;
for(int i=1; i<arr.length; i++){
	if(arr[i] > max){
        max = arr[i];
        index = i;
    }
} 

或
int index = 0;
for(int i=1; i<arr.length; i++){
	if(arr[i] > arr[index]){
        index = i;
    }
}   
//最后最大值就是arr[index]

4、顺序查找

步骤:挨个判断元素是否是要查找的值

//例如:在某个整型数组中查找value是否存在
int[] arr = {....};
int value = ?;

//看value在arr数组中是否存在
int index = -1;
for(int i=0; i<arr.length; i++){
    if(arr[i] == value){
        index = i;
        break;
    }
}
if(index == -1){
    System.out.println("不存在");
}else{
    System.out.println("存在,它的下标是" + index);
}

5、冒泡排序

关键词:

(1)相邻元素比较

(2)比较很多轮

int[] arr = {....};

//从小到大,每一轮都是从左到右去比较
for(int i=1; i<arr.length; i++){
    /*
   	假设arr.length=5
   	当i=1, j=0,1,2,3,比较4次
   		arr[0]与arr[1]
   		arr[1]与arr[2]
   		arr[2]与arr[3]
   		arr[3]与arr[4]
   	当i=2,j=0,1,2,比较3次
   		arr[0]与arr[1]
   		arr[1]与arr[2]
   		arr[2]与arr[3]
   	。。。。
   	j=0,j<arr.length-i
    */
	for(int j=0; j<arr.length-i; j++){
        if(arr[j] > arr[j+1]){
            int temp = arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = temp;
        }
    }
}

int[] arr = {....};

//从小到大,每一轮都是从右到左去比较
for(int i=1; i<arr.length; i++){
    /*
    假设arr.length=5
    当i=1,比较4次,j=4,3,2,1
    	arr[4]与arr[3]
    	arr[3]与arr[2]
    	arr[2]与arr[1]
    	arr[1]与arr[0]
    当i=2,比较3次,j=4,3,2
    	arr[4]与arr[3]
    	arr[3]与arr[2]
    	arr[2]与arr[1]
    ...
    j=arr.length-1; j>=i
    */
	for(int j=arr.length-1; j>=i; j--){
        if(arr[j] < arr[j-1]){
            int temp = arr[j];
            arr[j] = arr[j-1];
            arr[j-1] = temp;
        }
    }
}

第五章 面向对象基础(上)

5.1 面向对象编程思想的概述

面向对象是指一种编程思想,它是以“类/对象”为中心。

5.2 类与对象

1、类与对象的定义与关系

类:一类具有相同特性的事物的抽象描述。

对象是这类事物一个具体的实在的个体。

类是抽象的,对象是具体的,类是创建对象的模板。

2、如何声明一个类

语法格式:

【修饰符】 class 类名{
	....各种成员
}

3、如何创建对象

语法格式:

new 类名(); //如果没有用变量接收这个对象,那么它就是一个匿名对象

常见的形式:
变量 = new 类名();

类名  变量名 = new 类名();

5.3 类的第一个成员:成员变量

1、作用:存储类或对象的数据

2、声明格式:

【修饰符】 class 类名{
	【修饰符】 数据类型  成员变量名;
}

3、分类

根据有没有static修饰来划分:

(1)静态变量,有static修饰

(2)实例变量,没有static修饰

它俩的区别:

(1)在内存中存储的位置

静态变量:方法区

实例变量:堆

(2)生命周期

静态变量:和类一样,生命周期很长(生的早死的晚)

实例变量:和对象一样,每一个对象是独立的,随着new在堆内存分配空间,随着对象被GC回收而消失;

(3)值的特点

静态变量:该类所有对象共享,即一个对象修改了,其他对象跟着修改

实例变量:每一个对象都是独立的,即一个对象修改了实例变量,其他对象的实例变量不会受到影响;

(4)访问方式

在其他类中:

? 静态变量:类名.静态变量(推荐) 或 对象名.静态变量

? 实例变量:对象名.实例变量

在本类中:

? 静态变量:在任意方法和代码块中都可以访问;

? 实例变量:只有在非静态方法中和代码块中才能访问它;

? 换句话说:静态的方法和代码块是不允许访问非静态的实例变量。

(5)初始化的时机是不同(后面补充)

4、成员变量有默认值

和数组的元素的默认值规则一样:

  • byte,short,int,long:0
  • float,double:0.0
  • char:\u0000
  • boolean:false
  • 引用数据类型:null

5.4 包

1、作用:

(1)避免类的重名

(2)可以限制某些类的可见性访问

(3)可以按照不同的功能模块划分不同的包,来管理一个项目中众多的类

2、如何声明一个包?

要求:

  • 在.java源文件的首行,不管这个源文件中写了多少个类,多少代码;
  • 一个.java源文件只能有一个package语句;

格式:

package 包名;

包名的命名习惯:

(1)所有单词都小写

(2)多个单词直接使用.分割

(3)习惯用公司域名倒置 + xx

3、如何使用其他包的类

(1)在代码中使用全名称,例如:

java.util.Scanner input = new java.util.Scanner(System.in);

(2)在代码中使用简名称 + import语句,例如:

package 包名;

import java.util.Scanner;

【修饰符】 class 类名{
	方法{
		Scanner input = new Scanner(System.in);
	}
}

说明:import语句

(1)必须在package语句与class声明之间

(2)import 包.类名;

? import 包.*;

5.7 类的第二个成员:成员方法

1、什么是方法?

一个方法(method)代表了一个独立的可复用功能,又称为函数(function)。

2、如何声明一个方法?

位置:在类中方法外

语法格式:

【修饰符】 class 类名{
	
	【修饰符】 返回值类型 方法名(【形参列表】){
		方法体:实现功能的代码;
	}
}

说明:一个完整的方法 = 方法头 + 方法体;

? 方法头:【修饰符】 返回值类型 方法名(【形参列表】),又称为方法签名。对于调用者来说,我们关心的是方法头;

3、如何调用方法?

(1)本类中

静态方法:在本类的其他方法、代码块中直接调用。

非静态方法:只能在本类的非静态方法、代码块中直接调用。

(2)其他类中

静态方法:类名.静态方法

非静态方法:对象名.非静态方法

4、方法的参数传递机制

规则:

(1)实参给形参传值

形参:方法声明处的()中的参数,格式是(数据类型1 形参名1, 数据类型2 形参名2 。。。)

实参:方法调用处的()中的参数,格式是(变量名/常量/表达式1, 变量名/常量/表达式2 。。。)

(2)实参必须与形参的类型、个数、顺序一一对应

(3)实参给形参传什么值

基本数据类型:实参给形参的是数据值,或者说是数据值的副本

引用数据类型:实参给形参的是地址值,或者说,让形参和实参指向同一个对象

影响规则:

(1)基本数据类型:形参值的修改与实参是无关

(2)引用数据类型:通过形参对象修改了成员变量的值,会影响实参对象,因为本质上,他们就是指向同一个对象。

注意陷阱:当形参重新指向新的对象时,和实参就无关了。

示例一:基本数据类型的形参

public class TestPassParam {
    //swap交换
    static void swap(int a, int b){
        int temp = a;
        a = b;
        b = temp;
    }

    public static void main(String[] args) {
        int x = 1;
        int y = 2;

        swap(x, y);

        System.out.println("x = " + x);
        System.out.println("y = " + y);
    }
}

技术分享图片

示例二:引用数据类型

public class TestPassParam2 {
    static void change(MyData my){
        int temp = my.a;
        my.a = my.b;
        my.b = temp;
    }

    public static void main(String[] args) {
        //创建一个MyData对象
        MyData myData = new MyData();
        //给myData这个对象的a,b赋值
        myData.a = 1;
        myData.b = 2;

        //调用上面的change方法
        change(myData);

        System.out.println("myData.a = " + myData.a);//2
        System.out.println("myData.b = " + myData.b);//1
    }
}

class MyData{
    int a;
    int b;
}

技术分享图片

示例3:陷阱,形参指向新的对象

public class TestPassParam3 {
    static void change(MyData my) {
        //加入一句代码
        my = new MyData();//意味着修改了my中存储的地址值,相当于my指向了新对象,和实参对象无关

        int temp = my.a;
        my.a = my.b;
        my.b = temp;
    }

    public static void main(String[] args) {
        //创建一个MyData对象
        MyData myData = new MyData();
        //给myData这个对象的a,b赋值
        myData.a = 1;
        myData.b = 2;

        //调用上面的change方法
        change(myData);

        System.out.println("myData.a = " + myData.a);//1
        System.out.println("myData.b = " + myData.b);//2
    }
}

class MyData{
    int a;
    int b;
}

技术分享图片

示例4:形参是引用数据类型,而且形参重新指向新的对象,但是我们通过形参修改的是静态变量,那么会影响实参,因为形参和实参是同一种数据类型的,静态变量是共享的

public class TestPassParam {
    static void change(MyData my) {
        //加入一句代码
        my = new MyData();//意味着修改了my中存储的地址值,相当于my指向了新对象,和实参对象无关

        int temp = my.a;
        my.a = my.b;
        my.b = temp;
    }

    public static void main(String[] args) {
        //创建一个MyData对象
        MyData myData = new MyData();
        //给myData这个对象的a,b赋值
        myData.a = 1;
        myData.b = 2;

        //调用上面的change方法
        change(myData);

        System.out.println("myData.a = " + myData.a);//2
        System.out.println("myData.b = " + myData.b);//1
    }
}

class MyData{
   static int a;//注意:静态
   static int b;
}

技术分享图片

5、方法调用的内存分析

方法没调用时,它的指令等信息在方法区;

方法调用时,在栈开辟空间,而且每一次调用都会开辟独立的栈空间,

  • ? 调用时在栈中开辟空间,称为“入栈”
  • ? 调用结束后,自动释放该栈空间,称为“出栈”

6、方法的分类

静态方法:一般该功能和对象无关,习惯上用于工具类,方便调用

非静态方法:一般该功能和对象是有关,习惯上用于实体类、管理类等

7、可变参数

(1)什么情况下用可变参数?

当我们可以确定形参的数据类型,但是不能确定它们的个数时,可以使用可变参数

(2)可变参数的声明格式

数据类型... 可变参数名

(3)如何使用可变参数

  • 在声明它的方法中:当做数组使用即可
  • 在调用该方法时:
    • ①当成数组也可以
    • ②传入0~n个对应类型的实参元素

(4)可变参数的要求

  • 一个方法最多只能有一个可变参数
  • 可变参数必须是形参列表的最后一个

(5)示例

/*
练习:
    (1)求n个整数的和,n>=0
    (2)求n个整数的最小值,而且要求n>=1
 */
public class TestVarParam2 {
    //(1)求n个整数的和,n>=0
    static long sum(int... nums){
        long he = 0;
        for (int i=0; i<nums.length; i++){
            he += nums[i];
        }
        return he;
    }

    public static void main(String[] args) {
        System.out.println(sum());
        System.out.println(sum(1));
        System.out.println(sum(1,2,3,4,5));

        System.out.println("--------------------------");
//        System.out.println(min());//错误,形参a没有赋值
        System.out.println(min(5));
        System.out.println(min(5,6,2,8,1));
    }

    //(2)求n个整数的最小值,而且要求n>=1
    //int a,不属于可变参数,调用方法时,必须传入一个值给a
    static int min(int a, int... nums){
        int min = a;
        for(int i=0; i<nums.length; i++){
            if(nums[i] < min){
                min = nums[i];
            }
        }
        return min;
    }
}

8、return

return关键字:
1、用法
(1)return 返回值;
(2)return ;

2、区别
(1)return 返回值;
用于方法的返回值类型不是void,表示返回结果,并且结束当前方法。这种方法的正常结束的最后一条语句一定是return 返回值;

(2)return ;
用于方法的返回值类型是void,表示没有返回结果,提前结束当前方法。

public class TestReturn {
    static int max(int a, int b){
        if(a > b){
            return a;
        }
        return b;
    }

    /*
    line:行,colomn:列, sign:符号
     */
    static void print(int line, int column, char sign){
        if(line <=0){
            return;
        }
        if(column <= 0){
            return;
        }

        for(int i=0; i<line; i++){
            for(int j=0; j<column; j++){
                System.out.print(sign);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        print(3,5,‘#‘);
        System.out.println("----------------------");
        print(-3,5,‘#‘);
    }
}

9、成员变量与局部变量的区别

(1)声明的位置不同

成员变量:在类中,方法或代码块的外面

局部变量:在方法(),方法体,代码块中

(2)在内存中的存储位置不同

成员变量:

? 静态变量:方法区

? 实例变量:堆

局部变量:栈

(3)生命周期

成员变量:

? 静态变量:和类同生共死

? 实例变量:和某个对象同生共死

局部变量:每一个线程每一次调用局部变量都是独立的

(4)作用域

成员变量:

? 静态变量:本类中随便用,跨类的话,建议使用“类名."使用,也可以通过”对象."使用

? 实例变量:本来的非静态方法、代码块中随意用,跨类的话,只能通过”对象."使用

局部变量:有严格的作用域,从声明处开始,到所属的}结束,出了作用域不能使用。

(5)初始值

成员变量:有默认值

? byte,short,int,long:0,

? float,double:0.0,

? char:\u0000的空字符

? boolean:false

? 引用数据类型:null

局部变量:没有默认值,必须手动赋值,如果是形参,通过实参给它赋值

(6)修饰符

成员变量:有很多修饰符,比如:static等

局部变量:除了final没有其他的

10、重载

(1)什么是重载

某个类或某个对象中有两个或更多个,“方法名相同”,“形成列表不同”的方法,称为重载的方法。和返回值类型无关。

(2)如何匹配多个重载方法中是哪个方法?

依据:看形参列表

①:先看实参的类型与个数与形参列表最匹配,即数据类型和个数一样

②:再看实参的类型与个数与形参列表相兼容,即实参的数据类型<形参的数据类型,或者个数匹配可变参数

③:如果找不到能够接受的方法,编译就报错

示例:

class MyMath{
	static double max(double a, double b){
        System.out.println("2个double");
        return a > b ? a :b;
    }

    static int max(int a, int b){
        System.out.println("2个int");
        return a>b?a:b;
    }

    static int max(int a, int b, int c){
        System.out.println("3个int");
        return max(max(a,b),c);
    }

    static int max(int... nums){
        System.out.println("n个int");
        if(nums == null || nums.length==0){//如果传入的是数据不符合要求,抛异常
            throw new RuntimeException("不正常");
        }
        int max = nums[0];
        for (int i = 0; i < nums.length; i++) {
            if(max < nums[i]){
                max = nums[i];
            }
        }
        return max;
    }
}

MyMath.max(4,5); //调用 2个int的max
MyMath.max(4L,5);//调用 2个double的max
MyMath.max(2,3,4);//调用3个int的max
MyMath.max(4,5,6,7,8);//调用n个int的max
MyMath.max(3.4, 2,3); //编译报错

(3)陷阱

陷阱1:重载方法声明编译不通过

public class TestOverloadTrap2 {
    //求n个(n可以是0个,可以是多个)整数的和
    static int sum(int... nums){
        int sum  = 0;
        for(int i=0; i<nums.length; i++){
            sum += nums[i];
        }
        return sum;
    }

    ////求n个(n可以是0个,可以是多个)整数的和
    static int sum(int[] nums){
        int sum  = 0;
        for(int i=0; i<nums.length; i++){
            sum += nums[i];
        }
        return sum;
    }
}

编译器会认为int...和int[]是一样。

陷阱2:重载方法声明编译通过,但是调用编译会有问题

public class TestOverloadTrap1 {
    public static void main(String[] args) {
//        System.out.println(sum(1,2));//错误
        /*
        ambiguous method call.Both
        ambiguous:模糊不清的,引起歧义,通俗的讲,就是两个方法都匹配,无法区分是哪一个。
         */
    }

    /*
    以下两个方法是重载
     */
    //求n个(至少一个)整数的和
    static int sum(int a, int... nums){
        int sum  = a;
        for(int i=0; i<nums.length; i++){
            sum += nums[i];
        }
        return sum;
    }

    //求n个(n可以是0个,可以是多个)整数的和
    static int sum(int... nums){
        int sum  = 0;
        for(int i=0; i<nums.length; i++){
            sum += nums[i];
        }
        return sum;
    }
}

11、命令行参数(了解)

(1)什么是命令行参数?

给main方法传入的实参称为命令行参数。

(2)在命令行中如何传递

java 主类名  参数值1  参数值2 。。。

(3)在IDEA或Eclipse等工具中

通过:在运行菜单中,选择要运行的主类,并且在Program Arguments:传入参数

12、递归

(1)什么是递归?

一个方法直接或间接的自己调用自己,就是递归。

(2)递归方法的要求

①必须有终结条件,即当满足xx条件,就不进行递归。

②递归的次数不宜太多

(3)问题

①如果没有结束条件,一定会发生栈内存异常,StackOverflewError

②如果递归的次数太多,也可能发生栈内存异常,StackOverflewError

示例:

public class TestRecursion {
    static void a(){
        System.out.println("a被调用了");
        a();//Exception in thread "main" java.lang.StackOverflowError
    }

    public static void main(String[] args) {
        //a();

        System.out.println(sum(100));

        System.out.println(jieCheng((10)));

        System.out.println(f(5));
    }

    //(1)计算1-100之间所有自然数的和
    //不考虑负数
    static int sum(int n){
        if(n==1){
            return 1;
        }
        return sum(n-1) + n;
        /*
        n=1, sum(1)=>  return 1;
        n=2, sum(2)=>  return sum(1) + 2
        n=3, sum(3)=>  return sum(2) + 3
        ...
        n=100, sum(100) => return sum(99) + 100;
         */
    }

    //(2)n!
    static int jieCheng(int n){
        if(n==1){
            return 1;
        }
        return jieCheng(n-1) * n;
    }

    //(3)示例三:计算斐波那契数列(Fibonacci)的第n个值
    /*
    规律:一个数等于前两个数之和,
?	f(0) = 1,
?	f(1) = 1,
?	f(2) = f(0) + f(1) = 2,
?	f(3) = f(1) + f(2) = 3,
?	f(4) = f(2) + f(3) = 5,
    f(5) = f(3) + f(4) = 8,
?	...
?	f(n) = f(n-2) + f(n-1);
     */
    static int f(int n){
        if(n==0 || n==1){
            return 1;
        }
        return f(n-2) + f(n-1);
    }
}


5.8 对象数组

1、什么是对象数组?

数组的元素类型是引用数据类型。

数组的元素中存储的是对象,或者说是对象的首地址。

2、如何声明对象数组?

//一维数组
元素的数据类型[] 数组名;
对象的类型[] 数组名;

3、如何初始化?

(1)静态初始化

数组名 = new 元素的数据类型[]{对象1, 对象2 。。。。};

简化:当声明和静态初始化一起时
元素的数据类型[] 数组名 = {对象1, 对象2 。。。。};
 

(2)动态初始化

数组名 = new 元素的数据类型[长度];

//给每一个元素创建的对象
数组名[下标] = 对象;
或
数组名[下标] = new 元素的数据类型();


(3)访问或使用数组的元素

//通过数组的元素对象,操作对象的成员变量或成员方法
数组名[下标].成员变量
数组名[下标].成员方法

4、示例代码

import java.util.Scanner;

//存储5个Teacher对象,Teacher对象有姓名和薪资,按照薪资从高到低排序
//姓名和薪资从键盘输入
public class TestObjectArrayUse2 {
    public static void main(String[] args) {
        //第二步:声明数组,并初始为长度为5的数组
        Teacher[] teachers = new Teacher[5];

        //第三步:创建Teacher对象,并且从键盘输入姓名和薪资
        Scanner scanner = new Scanner(System.in);//ctrl + alt + V
        for(int i=0; i<teachers.length; i++){
            //创建Teacher对象
            teachers[i] = new Teacher();

            //输入姓名和薪资
            System.out.print("请输入姓名:");
            teachers[i].name = scanner.next();
            System.out.print("请输入薪资:");
            teachers[i].salary = scanner.nextDouble();
        }

        //统一显示
        for(int i=0; i<teachers.length; i++){
            System.out.println(teachers[i].getInfo());
        }

        //排序
        for(int i=1; i<teachers.length; i++){
            for(int j=0; j<teachers.length-i; j++){
                //比较两个元素,如果不满足,就交换
//                if(teachers[j] < teachers[j+1]){//对象不能直接比较大小
                if(teachers[j].salary < teachers[j+1].salary){//对象不能直接比较大小
                    //交换:teachers[j].salary 与 teachers[j+1].salary
                    //还是
                    //交换:teachers[j] 与 teachers[j+1]  √
                   Teacher temp = teachers[j];
                    teachers[j] = teachers[j+1];
                    teachers[j+1] = temp;
                }
            }
        }

        //统一显示
        System.out.println("排序后:");
        for(int i=0; i<teachers.length; i++){
            System.out.println(teachers[i].getInfo());
        }
    }

}

//第一步:声明Teacher类
class Teacher{
    String name;
    double salary;

    String getInfo(){
        return "姓名:" + name +",薪资:" + salary;
    }


第六章 面向对象基础(中)

6.1 封装

1、为什么要封装?

  • 安全:可以控制类、成员的可见性范围
  • 便捷:隐藏了很多使用者不需要知道的内部细节,通过它暴露的方法或部分信息来使用即可

2、如何实现封装?

使用权限修饰符/访问控制修饰符来控制类、成员的可见性范围。

修饰符 本类 本包 其他包子类 其他包非子类
private × × ×
缺省 × ×
protected ×
public

3、访问控制修饰符可以修饰什么

类:public和缺省

成员(成员方法、成员变量等):4种

4、如何使用其他包的类?

前提:这个类必须是public的

需要:导包或者写全名称

5、习惯上成员变量私有化

第一步:成员变量加private修饰

第二步:提供get/set方法

  • 静态变量:get/set也是静态的,静态变量与局部变量重名时,使用“类名."进行区别
  • 实例变量:get/set也是非静态的,实例变量与局部变量重名时,使用“this."进行区别
  • 当成员变量是boolean时,它的get方法是is开头,不是get开头。

示例:

  1. 使用 private 修饰成员变量
private 数据类型 变量名 ;

代码如下:

class Chinese{
    private static String country;
    private String name;
    private String tel;
    private int age;
    private boolean marry;
}

  1. 提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下:
class Chinese{
    private static String country;
    private String name;
    private String tel;
    private int age;
    private boolean marry;
    
    public static String getCountry() {
        return country;
    }

    public static void setCountry(String country) {
        Chinese.country = country;//因为this在静态方法中是不允许出现的,所以当局部变量与静态变量重名时,使用“类名.”
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;//当非静态成员变量与局部变量重名时,使用“this."进行区别,因为这个方法由对象调用,所以有当前对象this
    }

    public String getTel() {
        return tel;
    }

    public void setTel(String tel) {
        this.tel = tel;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean isMarry() {
        return marry;
    }

    public void setMarry(boolean marry) {
        this.marry = marry;
    }
}

6.2 this关键字

1、this:当前对象

(1)在构造器或非静态代码块中出现的this,表示正在new的对象

(2)在非静态方法中出现的this,表示调用当前方法的对象

2、用法

(1)this.成员变量

当在非静态方法中,成员变量与局部变量重名时,在成员变量前面加this.进行区分。

(2)this.成员方法

当在非静态方法,调用当前对象的其他方法时,可以使用,也完全可以省略this.。

(3)this() 或 this(...)

位置:必须在构造器的首行

表示调用本类的其他构造器。

this():调用本类的无参构造

this(...):调用本类的有参构造

3、注意

this是不允许出现在静态方法、静态代码块中。

因为在静态的方法或静态代码块中,还没有当前类的对象。

4、this的追溯

(1)this() 或 this(...):仅限于本类中查找和匹配

(2)this.成员变量 和 this.成员方法,先从本类中查找,如果没有,会去父类查找,当然要是父类在子类中可见的。

6.3 构造器

1、作用

(1)和new一起,用来创建对象

new 类名(【实参列表】);  本质上就是  new  构造器(【实参列表】);

(2)在创建这个对象的同时,为对象的实例变量初始化

2、语法格式/特点

【修饰符】 class 类名{
	【修饰符】 类名(){//无参构造
		....
	}
	【修饰符】 类名(形参列表){//有参构造
		....
	}
}

语法格式特点:

(1)构造器的名字必须和类名一致,包括大小写

(2)构造器没有返回值类型,不要写返回值类型,包括void,一旦写了返回值类型就是普通方法

(3)修饰符只能是权限修饰符:public,protected,缺省,private

3、其他特点

(1)所有类都有构造器

(2)如果我们没有手动编写构造器,那么编译器会自动添加一个默认的无参构造,并且该构造器的权限修饰符和当前类的权限修饰符一致;

? 如果我们手动编写了构造器,那么编译器就不会自动添加默认的无参构造,如果需要无参构造,需要手动添加;

(3)一个类可以重载多个构造器。建议大家尽量保留无参构造。

(4)子类继承父类时,不会继承父类的构造器,但是必须调用父类的构造器:

? 默认情况下,子类的构造器会自动调用父类的无参构造;

? 如果父类没有无参构造,必须子类中手动编写构造器,并且在子类构造器中手动去调用父类的有参构造。

? 可以用super(),表示调用父类的无参构造;可以省略;

? 可以用super(实参列表),表示调用父类的有参构造;

6.4 继承

1、什么情况下需要继承?或者说继承的好处?

  • 代码的复用:即子类会延续父类的一些特征(成员变量、成员方法等)
  • 代码的扩展:即子类可以扩展父类的一些特征(增加成员变量、增加成员方法、重写成员方法等)
  • 表示了is-a的关系,即派生出子类别,我们在事物的表示范围角度来说,子类<父类

2、语法格式

【修饰符】 class 子类名 extends 父类{
	...
}

3、特点和要求

(1)子类继承父类时,会继承父类的所有成员变量和成员方法,但是私有的在子类中不能直接使用,

缺省的成员变量和成员方法,跨包的话,在子类中也不能直接使用。

(2)子类继承父类时,不会继承父类的构造器,但是必须调用父类的构造器:

? 默认情况下,子类的构造器会自动调用父类的无参构造;

? 如果父类没有无参构造,必须子类中手动编写构造器,并且在子类构造器中手动去调用父类的有参构造。

? 可以用super(),表示调用父类的无参构造;可以省略;

? 可以用super(实参列表),表示调用父类的有参构造;

(3)Java只支持单继承,即直接父类只有一个,extends后面的父类只能写一个

(4)Java支持多层继承,父类还可以有父类

(5)一个父类可以同时有很多个子类

4、重写(Override)

当子类继承了父类的成员方法时,该方法的实现(方法体)不适用于子类,那么子类可以选择进行重写。

重写的要求:

  • 方法名:必须相同
  • 形参列表:必须相同,如果形参列表不同,就是重载关系
  • 返回值类型:
    • ? 基本数据类型和void:必须相同
    • ? 引用数据类型:<=
  • 修饰符:
    • ? 权限修饰符:>=
    • ? 其他修饰符:被重写的方法不能是static,private等
  • 异常:后面补充

6.5 super关键字

1、super:引用父类的xx

2、用法

(1)super.成员变量

当子类声明了和父类同名的成员变量时,可以使用super.成员变量表示引用的是父类的成员变量。

但是在实际开发中是要尽量避免。

(2)super.成员方法

当子类重写了父类的某个成员方法,但是在子类中又想要调用父类被重写的成员方法,那么可以使用super.成员方法。

(3)super()或super(实参列表)

位置:必须在子类的构造器的首行

表示:调用直接父类的构造器

super():表示调用父类的无参构造,可以省略,省略的话,默认也是找父类的无参构造

super(实参列表):表示调用父类的有参构造,不能省略

3、super关键字的使用要求

(1)不能出现在静态方法、静态代码块中

(2)super(),super(实参列表),super.成员变量,super.成员方法,所访问的构造器、成员变量、成员方法必须在子类在中可见。

4、super的追溯

(1)先在直接父类中查找,

(2)如果没有找到

super(),super(实参列表):不会往上找了,

super.成员变量,super.成员方法:继续往上追溯,直到根父类为止。

6.6 就近原则和追根溯源原则

1、变量

(1)没有类名.,没有this.,也没有super.,先从局部变量开始查找,如果没有,找本类的成员变量,如果没有,找父类(可见)的成员变量

(2)有this.,即this.成员变量,从本类的成员变量,如果没有,找父类(可见)的成员变量

(3)有super.,即super.成员变量,从直接父类(可见)的成员变量开始找,如果没有,找父类的父类

2、方法

(1)没有类名.,没有this.,也没有super.,先从本类开始查找,如果没有,找父类(可见)的成员方法

(2)有this.,即this.成员方法,先从本类开始查找,如果没有,找父类(可见)的成员方法

(3)有super.,即super.成员方法,从直接父类(可见)的成员方法开始找,如果没有,找父类的父类

3、构造器

(1)this() 或 this(...):仅限于本类中查找和匹配

(2)super(),super(实参列表):仅限于直接父类查找和匹配

6.7 成员变量初始化

1、不管静态变量还是实例变量都先有默认值,然后在默认值的基础上可以进行初始化。

2、静态变量初始化

(1)什么时候初始化

  • 和类初始化一起,每一个类只会初始化一次。
  • 如果一个子类初始化的时候,发现它的父类还没有初始化的话,那么会先初始化父类。当然如果父类已经初始化过了,就不会初始化两次。

(2)和哪些代码有关

  • 静态变量的显式赋值语句
  • 静态代码块

它俩按照编写的顺序执行。

本质上这两部分代码是合并到一个()类初始化方法中,由类加载器在类初始化时调用。

3、实例变量初始化

(1)什么时候初始化

  • 在new对象时,而且每次new对象都会发生实例初始化
  • 在new子类对象的时候,一定会调用父类相关的实例初始化代码,因为在子类的构造器中一定就会有super()或super(实参列表)语句
  • 如果在new对象时,这个类还没有初始化,那么肯定先完成类的初始化,才能进行实例初始化

(2)和哪些代码有关

  • ①super()或super(实参列表) :原来构造器首行是什么,合并到实例初始化方法中,它就是什么
  • ②实例变量的显式赋值语句
  • ③非静态代码块
  • ④构造器中剩下的代码

其中的①一定是最早,④是最后,②和③在中间并且按照编写的顺序

本质上,这四部分代码是合并到一个一个的()实例初始化方法中,一个类有几个实例初始化方法,看程序员编写了几个构造器,如果一个都没有编写,那么有一个默认的无参构造对应的实例初始化方法。

4、代码块

(1)静态代码块

作用:为静态变量初始化,在类初始化时执行

【修饰符】 class 类{
	static{
		//...静态代码块
	}
}

(2)非静态代码块

作用:为实例变量初始化,在new对象时执行

【修饰符】 class 类{
	{
		//...非静态代码块
	}
}

6.8 多态

1、多态引用的形式

//经典形式
父类类型  变量 =  new 子类(【实例列表】);

//变形
(1)多态数组:
父类类型[] 数组名 = new 父类类型[长度];
数组名[下标] = new 子类(【实例列表】);

(2)多态参数:
 //方法声明
【修饰符】 返回值类型  方法名(父类类型  形参名){
    ....
}
//方法调用
方法名(子类对象);

(3)多态返回值
  //方法声明
【修饰符】 父类型  方法名(形参类型  形参名){
    ....
    return 子类对象;
}   

2、多态的好处

代码更灵活,功能更强大

3、多态引用后有什么现象

原因:多态引用后,我们的变量/数组的元素/形参/返回值结果,它们表现出来“编译时类型”和“运行时类型”不一致。

现象:

(1)成员变量:只看“.”前面的变量/数组的元素等的编译时类型

(2)成员方法:

非虚方法:静态方法等不能重写的方法:只看“.”前面的变量/数组的元素等的编译时类型

虚方法:可能被重写的方法

  • 编译时看“.”前面的变量/数组的元素等的编译时类型,在编译时类型中查找匹配的方法;
  • 运行是看“."前面的变量/数组的元素等的运行时类型,如果对上面找的方法进行了重写,一定是执行重写后的方法,如果没有重写,还是执行刚刚找到的方法。

说明:实际开发中,多态引用基本上是针对“虚方法”,而且子类都会“虚方法“进行重写。

4、类型转换

注意:向上转型与向下转型都只是针对“编译时类型”,和运行时类型无关。

(1)向上转型:自动发生,当把子类对象赋值给父类的变量/元素/形参等时自动就发生了。

(2)向下转型:强制进行,当把父类的变量/元素/形参等重新赋值给子类类型的变量时,需要强制类型转换。

如果要成功:

(1)向上转型与向下转型的类型必须是父子类关系

(2)向上转型: =左边的变量/元素的编译时类型 > 右边的变量/对象的编译时类型;

? 向下转型:

? 编译通过:=左边的变量/元素的编译时类型 >= 右边的()中的类型

? 运行通过:=左边的变量/元素的类型 >= 右边的变量/对象的运行时类型;

结论:向上转型来说,编译通过运行就可以,但是向下转型,编译通过不代表运行可以通过,运行可能发生ClassCastException类型转换异常。

class Animal{
	...
}
class Dog extends Animal{
	...
}
class Cat extends Animal{

}
class TaiDiDog extends Dog{
	...
}

Animal a1 = new Dog(); //向上转型  左边的变量a1的编译时类型 > 右边对象Dog类型
Animal a2 = new TaiDiDog(); //向上转型  左边的变量a2的编译时类型 > 右边对象Dog类型

Dog d1 = (Dog)a1; //向下转型  编译是否通过  =左边的Dog类型 >= 右边()中的类型
							运行是否通过  =左边的Dog类型 >= 右边a1的运行时类型Dog
							
Dog d2 = (Dog)a2; //向下转型  编译是否通过  =左边的Dog类型 >= 右边()中的类型
							运行是否通过  =左边的Dog类型 >= 右边a2的运行时类型TaiDiDog			

Dog d3 = (TaiDiDog)a2; //向下转型  编译是否通过  =左边的Dog类型 >= 右边()中的类型
							      运行是否通过  =左边的Dog类型 >= 右边a2的运行时类型TaiDiDog	
                                      
                                      
Animal a3 = new Cat();//向上转型  左边的变量a3的编译时类型 > 右边对象Cat类型
Dog d4 = (Dog)a3;    //向下转型   编译是否通过  =左边的Dog类型 >= 右边()中的类型
							     运行是否通过 =左边的Dog类型 与 右边a3的运行时类型Cat 没有>=的关系,所以运行报错
                                     
Animal a4 = new Animal();//没有向上或向下
Dog d5 = (Dog)a4;       //向下转型	编译是否通过  =左边的Dog类型 >= 右边()中的类型
								   运行是否通过  =左边的Dog类型 < 右边a4的运行时类型Animal,所以运行报错

5、关键字:instanceof

用于向下转型之前的判断,如果它判断返回true,说明向下转型是安全的,否则就不能进行向下转型。

语法格式:

对象/变量  instanceof 类型

//经典写法
if(对象/变量  instanceof 类型){
	....
}

6.9 native关键字

1、native:本地的、原生的

2、native是一个修饰符,用来修饰方法

3、native修饰的方法,它的方法体不是Java语言实现的,而是由C/C++等语言实现。但是在Java中可以当成是Java的方法一样去使用,如果子类需要并且允许,子类也可以重写native的方法。

6.10 final关键字

1、final:最终的

2、final是一个修饰符,用来修饰类、方法、变量

3、final修饰类:表示该类不能被继承,例如:String,System,Math,Class等

4、final修饰方法:表示该方法虽然可以被继承到子类,但是不能被子类重写。

5、final修饰变量:不管是成员变量还是局部变量,都不能修改值,即常量,并且要明确初始化。

6.11 根父类Object

1、Object在java.lang包下

2、Object是所有类的根父类,所有对象都会拥有它的方法,它的变量可以接受任意类型的对象,Object[]虽然不能接收基本数据类型的数组,例如:int[],但是可以接受所有的对象数组,例如:String[],Student[]。

3、Object的方法(一共11个,目前简单介绍了5个)

(1)public String toString():

  • ? 默认返回的是 对象的运行时类型@对象的hashCode值的十六进制值
  • ? 建议重写,尽量返回简单易懂的字符串形式,来表示对象的信息
  • ? 当打印一个对象或对象与字符串进行拼接时,默认会自动调用对象的toString()
  • ? IDEA中快速重写的快捷键是Alt + Insert

(2)public final Class<?> getClass():返回对象的运行时类型

(3)protected void finalize():当对象给GC回收时,由GC自动调用,用于彻底释放资源。每一个对象的finalize方法只会被调用一次。

(4)public boolean equals(Object obj)

  • 默认返回的结果和“==”是一样的,比较的是两个对象的地址
  • IDEA中快速重写的快捷键是Alt + Insert,一般和hashCode()方法一起重写,选择相同的成员变量
  • 常见的类型都重写了equals方法,例如:String等
  • equals如果是手动重写,那么应该遵循几个原则:
    • 自反性:x.equals(x)一定是true
    • 对称性:x.equals(y)和y.equals(x)返回的结果一样
    • 传递性:x.equals(y)是true,y.equals(z)是true,那么x.equals(z)也一定是true
    • 一致性:两个对象参与equals比较的成员变量的值如果没有修改过,比较的结果是不能变
    • 非空对象与null比较一定是false:x.equals(null)是false

(5)public int hashCode():

  • 每一个对象都有一个hashCode值,理想状态下,不同的对象,hashCode一定是不同,但是现实中,不同的对象的hashCode值可能相同;
  • IDEA中快速重写的快捷键是Alt + Insert,一般和equals()方法一起重写,选择相同的成员变量
  • hashCode方法如果是手动重写,那么应该遵循常规协定:
    • 如果两个对象调用equals返回了true,那么他们的hashCode值一定要相同
    • 如果两个对象的hashCode值不同,那么他们调用equals方法一定是false
    • 如果两个对象的hashCode值相同,那么他们调用equals方法的结果可能是true,也可能是false,例如:Aa和BB字符串
  • hashCode值的计算过程中,习惯性选择31作为表达式的一个因子,因为31是一个不大不小的素数,而且是2的n次方-1的一个素数。

第七章 面向对象基础(下)

7.1 抽象类

1、什么情况下需要用到抽象类

情形一:当我们声明某个父类时,某个方法的方法体无法给出具体的实现,需要在子类中才能给出合理的具体的实现,那么这样的方法在父类中只能声明为抽象方法,而包含抽象方法的类必须是抽象类。在父类中又必须包含该方法,应该类是代表一类事物的共同特征,而且从多态的角度来说,父类拥有该方法也比较方便。

情形二:当我们某个父类虽然没有抽象方法,但是我们不希望你创建该类的对象,而且希望你创建它子类的对象,这样的父类也会声明为抽象类。

2、如何声明抽象类

【其他修饰符】 abstract class 类{
	...
}

【其他修饰符】 abstract class 类  extends 父类{
	...
}
问:父类可以是抽象类,可以是非抽象类吗?都可以
    
【其他修饰符】 abstract class 类  extends 父类  implements 接口们{
	...
}

3、如何声明抽象方法

//在抽象类中
【其他修饰符】 abstract class 类{
	【其他修饰符】 abstract  返回值类型   方法名(【形参列表】);
}

//在接口中
【其他修饰符】 interface 接口{
	public abstract  返回值类型   方法名(【形参列表】);//并且public abstract可以省略
}

4、抽象类的特点

(1)不能直接new对象,即不能直接实例化

(2)包含抽象方法的类必须是抽象类,但是反过来抽象类可以不包含抽象方法

(3)抽象类是用来被继承,子类继承抽象类,必须重写/实现抽象父类的所有抽象方法,否则这个子类也得是抽象类

(4)抽象类是有构造器的,给子类调用的

(5)抽象类也可以包含静态方法,因为静态方法的调用不需要对象

问:抽象类与非抽象类有什么区别?

(1)抽象类有abstract修饰,里面可能包含抽象方法

? 非抽象类没有abstract修饰,里面一定没有抽象方法

(2)抽象类不能直接new对象

? 非抽象类可以直接new对象

(3)抽象类不能是final的

? 非抽象类可以是final的

7.2 接口

1、什么情况下需要用到接口?

接口是代表一种标准,行为规范。

当我们有多个类,他们之间没有同属于某个父类的关系,也没有is-a的关系,但是他们之间有相同的“方法/功能/行为能力”,我们可以提取这个公共的行为为一个接口,这个时候,接口的类型都可以统一管理这些类的对象,实现多态。

2、如何声明接口

【权限修饰符】 interface 接口名{
	...
}

【权限修饰符】 interface 接口名 extends 接口们{
	...
}

3、类如何实现接口

【修饰符】 class  实现类 implements 接口们{

}

【修饰符】 class  实现类 extends 父类 implements 接口们{

}

4、接口的特点

(1)接口不能直接new对象,即不能直接实例化

(2)接口中没有构造器、代码块

(3)接口是用来实现的,实现类在实现接口时,必须重写接口的所有抽象方法,否则该实现类就得是抽象类

(4)接口的成员变量必须是public static final的,其中public static final可以省略

(5)接口的成员方法:

JDK1.8之前:只能是公共的抽象方法,public abstract可以省略

JDK1.8之后:增加了公共的静态方法和公共的默认方法,static或default不能省略。

JDK1.9之后:增加了私有的方法,即可以是静态的私有方法,或者非静态的私有方法,但是不能是默认方法。

5、接口的静态方法

接口的静态方法的调用,只能通过“接口名.静态方法”的形式调用,不会继承到实现类中。

6、接口的默认方法

(1)接口的默认方法的调用,只能通过“实现类的对象.默认方法”的形式调用

(2)默认方法会继承到实现类中,实现类可以选择进行重写,但是重写时,就不要在加default。

(3)冲突问题

  • 当父类有一个方法,与父接口的默认方法的方法签名相同,那么子类中默认保留的是父类的(亲爹原则),当然我们也可以选择保留父接口,在重写的方法体中用“父接口名.super.方法”的形式实现,也可以现在完全重写;
  • 当一个实现类实现了多个接口,多个接口中存在方法签名相同的默认方法,在实现类中必须做出选择,即必须重写默认方法,可以保留其中一个接口的实现,用“接口名1.super.方法”的形式实现,也可以现在完全重写;

7、经典接口

(1)java.lang.Comparable接口:自然排序/比较接口

? 抽象方法:int compareTo(Object o)

? 返回值:当.前面的this对象 大于 ()中的指定对象o时,返回正整数

? 当.前面的this对象 小于 ()中的指定对象o时,返回负整数

? 当.前面的this对象 等于 ()中的指定对象o时,返回0

(2)java.util.Comparator接口:定制排序/比较接口

抽象方法:int compare(Object o1, Object o2)

返回值:o1对象 大于 o2对象时,返回正整数

? o1对象 小于 o2对象时,返回负整数

? o1对象 等于 o2对象时,返回0

7.3 包装类

1、为什么要用包装类?

Java延续了C语言的8种基本数据类型和void,但是Java是面向对象的语言,很多API以及新特性只支持对象,不支持基本数据类型,所以需要用到包装类来与基本数据类型对接。

2、包装类有哪些?

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

3、数据类型的转换

(1)装箱:把基本数据类型转为包装类的对象

手动:使用构造器或是使用valueOf(..)

自动:JDK1.5之后

(2)拆箱:把包装类的对象转为基本数据类型

手动:xxValue()

自动:JDK1.5之后

注意:自动的装箱与拆箱要注意,每一种基本数据类型和包装类只与自己对应的类型自动装箱与拆箱,例如:int只与Integer自动装箱与拆箱。

4、缓存对象

Byte:-128~127

Short:-128~127

Integer:-128~127

Long:-128~127

Character:0~127,最早的ASCII码的范围。

Boolean:true和false

Float和Double没有缓存对象。

注意:(1)自动装箱的会用到缓存对象(2)valueOf()也是用缓存对象,

? 如果是自己new 类型对象的就不是用缓存对象。

5、方法

(1)和字符串进行转换的方法

基本数据类型 --> 字符串 : 与字符串拼接

字符串 --> 基本数据类型/包装类: (1)构造器(2)parseXxx(字符串)

(2)获取某个类型的最大/小值

例如:Integer.MAX_VALUE 和 Integer.MIN_VALUE

(3)比较两个基本数据类型的值

例如:Double.compare(值1,值2),返回值类型是int。

(4)转大小写

例如:Character.toUpperCase(字符)

? Character.toLowerCase(字符)

(5)转不同的进制值

例如:Integer.toBinaryString(int)转二进制

? Integer.toHexString(int)转二进制

? Integer.toOctalString(int)转二进制

6、面试题

(1)和不同的值比较

如果是两个引用数据类型的对象比较==,那么按照地址比较;

如果是一个引用数据类型对象与基本数据类型比较==,一定是先自动拆箱,然后比较。

如果是两个引用数据类型的对象比较>,<等,那么一定是先自动拆箱,然后比较。

如果是一个引用数据类型对象与基本数据类型比较>,<等,那么一定是先自动拆箱,然后比较。

(2)方法的参数传递

如果形参是包装类的类型,那么形参的修改和实参无关。因为包装类的对象是“不可变对象”,和字符串是一样。

7.4 枚举

1、什么是枚举?或者什么情况用枚举?

当某个类型的对象是固定的有限的几个常量对象。

2、JDK1.5如何声明枚举?

【修饰符】 enum 枚举类型{
	常量对象列表
}

【修饰符】 enum 枚举类型{
	常量对象列表;
    
    其他成员列表
}

注意:

(1)常量对象列表必须在枚举类型的首行

(2)如果常量对象列表后面还有其他成员,那么需要在常量对象列表最后加;

(3)如果枚举类型要手动编写构造器,所有构造器会自动私有化。如果没有写构造器,那么默认产生的无参构造也是私有的。

(4)枚举类型不能显式的extends其他的父类,它的直接父类java.lang.Enum类

(5)枚举类型可以实现接口,既可以统一实现,也可以每一个常量对象单独实现。

3、Enum类中有几个特殊的地方

(1)它只有一个有参构造

protected Enum(String name, int ordinal):其中name是枚举对象的常量名,ordinal是枚举对象的序号/索引,从0开始。这个构造器不用程序员手动调用,是自动调用。

(2)Enum类重写了Object类的toString方法,返回的常量对象名称,我们自己枚举类型可以自己再次重写。

(3)常用的方法

String name():返回常量对象的名称

int ordinal():返回常量对象的索引

static 枚举类型 valueOf("常量对象名"):根据常量对象名获取某个常量对象

static 枚举类型[] values():返回该枚举的所有常量对象

7.5 内部类

1、什么样的类是内部类?

声明在另一个类的里面的类称为内部类。

2、内部类的分类

(1)成员内部类

静态内部类:有static

非静态内部类:没有static

(2)局部内部类

有名字的局部内部类

匿名的内部类

3、静态内部类

(1)语法格式

【修饰符】 class 外部类{
	【其他修饰符】 static class 静态内部类{
		
	}
}

【修饰符】 class 外部类  【extends 父类】 【implements 父接口们】{
	【其他修饰符】 static class 静态内部类 【extends 父类】 【implements 父接口们】{
		
	}
    
    
}

(2)特点

  • 静态内部类的权限修饰符:4种

  • 静态内部类也是一个类

    • 静态内部类有自己的字节码文件:外部类名$静态内部类.class
    • 静态内部类的成员没有限制
    • 静态内部类可以有自己的父类和父接口
    • 静态内部类可以是abstract,也可以是final的
  • 在静态内部类中使用外部类的成员

    • 私有的可以用
    • 外部类的非静态成员不能在静态内部类中直接使用
  • 在外部类中使用静态内部类的成员

    • 私有的可以用
    • 使用静态内部类的静态成员:直接用静态内部类名. 静态成员
    • 使用静态内部类的非静态成员:(1)先创建静态内部类的对象(2)通过对象.进行调用
  • 在外部类的外面使用静态内部类的成员

    • 使用静态内部类的静态成员:直接用外部类名.静态内部类名. 静态成员

    • 使用静态内部类的非静态成员:(1)先创建静态内部类的对象:格式:外部类名.静态内部类名 变量 = new 外部类名.静态内部类构造器(...);

      ? (2)通过对象.进行调用

  • 当静态内部类中的某个静态成员与外部类的某个静态成员同名时,如何区分,如果访问外部类的静态成员,那么加“外部类名."

public class TestReview {
    public static void main(String[] args) {
        Outer.Inner.method();
        Outer.Inner.test(30);
    }
}
class Outer{
    static int a = 10;
    static class Inner{
        static int a = 20;

        public static void method(){
            System.out.println("外部类的a = " + Outer.a);
            System.out.println("内部类的a = " + a);
        }

        public static void test(int a){
            System.out.println("外部类的a = " + Outer.a);
            System.out.println("内部类的a = " + Inner.a);
            System.out.println("局部变量的a =" + a);
        }

    }
}

4、非静态内部类

(1)语法格式

【修饰符】 class 外部类{
	【其他修饰符】  class 非静态内部类{
		
	}
}

【修饰符】 class 外部类  【extends 父类】 【implements 父接口们】{
	【其他修饰符】  class 非静态内部类 【extends 父类】 【implements 父接口们】{
		
	}
    
    
}

(2)特点

  • 非静态内部类的权限修饰符:4种
  • 非静态内部类也是一个类
    • 非静态内部类有自己的字节码文件:外部类名$非静态内部类.class
    • 非静态内部类的成员有限制
      • 不能声明自己的静态成员,除非是静态的常量(static final)
      • 或者是从父类继承的
    • 非静态内部类可以有自己的父类和父接口
    • 非静态内部类可以是abstract,也可以是final的
  • 在非静态内部类中使用外部类的成员
    • 私有的可以用
    • 外部类的静态和非静态成员都能在非静态内部类中直接使用
  • 在外部类中使用非静态内部类的成员
    • 私有的可以用
    • 使用非静态内部类的非静态成员:(1)先创建非静态内部类的对象(2)通过对象.进行调用
  • 在外部类的外面使用非静态内部类的成员
    • 使用非静态内部类的非静态成员:
      • (1)先创建外部类的对象:格式:外部类名 out = new 外部类构造器(..);
      • (2)在创建非静态内部类的对象:格式:外部类名.非静态内部类名 变量 = out.new 非静态内部类构造器(...)
      • (3)通过非静态内部类的对象.进行调用
    • 在实际开发中,可以在外部类中提供一个方法,返回非静态内部类的对象
      • (1)先创建外部类的对象:格式:外部类名 out = new 外部类构造器(..);
      • (2)通过外部类的对象.方法获取非静态内部类的对象:格式:外部类名.非静态内部类名 变量 = out.方法(..);
      • (3)通过非静态内部类的对象.进行调用
  • 当非静态内部类中的某个非静态成员与外部类的某个非静态成员同名时,如何区分,如果访问外部类的非静态成员,那么加“外部类名.this.非静态成员"
public class TestReview2 {
    public static void main(String[] args) {
        Wai wai = new Wai();
        wai.a = 100;
        Wai.Nei nei = wai.new Nei();
        nei.method();

        nei.test(30);
    }
}
class Wai{
    int a = 10;
    class Nei{
        int a = 20;

        public void method(){
            System.out.println("外部类的a = " + Wai.this.a);
            System.out.println("内部类的a = " + a);
        }

        public void test(int a){
            System.out.println("外部类的a = " + Wai.this.a);
            System.out.println("内部类的a = " + this.a);
            System.out.println("局部的a = " + a);
        }
    }
}

5、局部内部类(非常不重要)

(1)语法格式

【修饰符】 class 外部类{
    【修饰符】 返回值类型 方法名(【形参列表】){
        【abstract/final】  class 局部内部类{
		
		}
    }
	
}

【修饰符】 class 外部类  【extends 父类】 【implements 父接口们】{
	【修饰符】 返回值类型 方法名(【形参列表】){
        【abstract/final】  class 局部内部类 【extends 父类】 【implements 父接口们】{
		
		}
    }
}

(2)特点

  • 局部内部类的权限修饰符:没有
  • 局部内部类也是一个类
    • 局部内部类有自己的字节码文件:外部类名$数字编号局部内部类.class
    • 局部内部类的成员有限制
      • 不能声明自己的静态成员,除非是静态的常量(static final)
      • 或者是从父类继承的
    • 局部内部类可以有自己的父类和父接口
    • 局部内部类可以是abstract,也可以是final的
  • 在局部内部类中使用外部类的成员
    • 私有的可以用
    • 外部类的静态可以在局部内部类中直接使用
    • 外部类的非静态成员是否能在局部内部类中使用,看局部内部类的所在的方法,如果这个方法是静态的就不行,是非静态的就可以
  • 在外部类中使用局部内部类的成员
    • 私有的可以访问
    • 但是使用该内部类有作用域
    • 使用局部内部类的非静态成员:(1)先创建局部内部类的对象(2)通过对象.进行调用
  • 在外部类的外面使用局部内部类:不能
  • 在外部类的外面获取局部内部类的对象:可以
    • 如何返回:通过方法的返回值传回
    • 如何接收:用它的父类或父接口的变量接收
  • 在局部内部类中使用它所在外部类的方法的局部变量时,该局部变量必须加final
    • JDK1.8之前必须手动加final
    • JDK1.8之后自动加final

6、匿名内部类

1、声明的格式

//形式一:
new 父类(){
	//成员,基本上是重写父类的方法
}

//形式二:
new 父类(实参列表){
	//成员,基本上是重写父类的方法
}

//形式三:
new 父接口(){
	//成员,基本上是重写父类的方法
}
形式三的直接父类是Object

2、使用匿名内部类的格式

//形式一:用匿名内部类的匿名对象直接调用方法
new 父类/父接口(【实参列表】){
	。。。
}.成员方法(...);

//形式二:把匿名内部类的对象赋值给他的父类或父接口
父类/父接口 变量 = new 父类/父接口(【实参列表】){
	。。。
};
变量.成员方法(...);

//形式三:把匿名内部类的匿名对象直接作为“实参”传给另一个方法
方法(new 父类/父接口(【实参列表】){
	。。。
});

3、匿名内部类的特点

(1)匿名内部类没有名字,必须在声明类的同时就创建它唯一的对象

(2)匿名内部类也是一个类

  • 它有自己的字节码文件:外部类名$编号.class
  • 它没有修饰符:权限修饰符、static、abstract、final等没有
  • 它不能手动声明构造器,但是编译器是会自动生成一个无参的()实例初始化方法
  • 它不能声明静态成员,除非是static final的常量,或者是从父类继承的静态成员
  • 它可以声明自己的其他成员,但是习惯上我们在匿名内部类中都是“重写/实现”父类或父接口的方法,很少扩展自己的方法;

(3)匿名内部类也是局部内部类的一种,它也可以使用所在方法的局部变量的,如果要用该局部变量就必须final修饰该局部变量。

7.6 注解

1、注解长什么样?

//形式一:
@注解名

//形式二:
@注解名(...)

2、注解是干什么

注解也是注释的一种形式,它是用代码去注释代码。一个完整的注解应该包含三个部分:

(1)注解的声明

例如:@Override

package java.lang;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

(2)注解的使用

public class TestOverride {
    @Override
    public String toString() {
        return "TestOverride{}";
    }
}

(3)注解的读取

例如:@Override注解的读取是由编译器(javac.exe等)程序来读取。

如果我们自己写的自定义注解,那么我们就需要编写一段程序来读取它,否则没有读取就没有意义。

3、三个最最基本的注解

(1)@Override

用在重写的方法上面,表示让编译器检查该方法是否遵循了重写的要求;

(2)@SuppressWarnings

用在需要抑制警告的位置,例如:类型声明上面、构造器上面、方法上面等

(3)@Deprecated

用在需要标记为“已过时”的位置,例如:类上面,方法上面,构造器上面,包上面等

4、文档注释

(1)语法格式

/**
文档注释

*/

(2)一些注解配合文档注释

@author:作者

@version:版本

@since:从xx版本开始

@see:另请参看

在方法上:

@param:参数,格式:@param 参数名称 参数的数据类型 关于该参数的描述,如果方法有几个形参,就写几个@param,如果无参,就不写。

@return:返回值,格式:@return 返回值类型 返回值的描述,如果方法的返回值类型不是void,就写@return,如果是void,就不写。

@throws:抛出的异常,格式:@throws 异常类型 什么时候会抛出该异常,如果方法没有throws的声明就不用写,如果有多个throws,那么就写多个。

5、JUnit的非常重要一个注解@Test

这个注解必须标记在一个测试方法上,这个方法有要求:

(1)所在的类必须是public

(2)方法本身也是public

(3)所在的类必须只有一个公共的无参构造

(4)方法本身必须是无参

(5)方法本身必须是void

(6)方法本身不能是static

(7)如果项目中有用@Test注解,自己的类尽量不要命名为Test

第八章 异常

8.1 什么是异常

以下不是异常:

(1)语法错误

(2)逻辑错误

什么是异常?

正常情况下,程序是可以正常运行的,结果也是正确的。但是某些时候,因为一些其他的因素,例如:

(1)用户的输入、传参不合法

(2)用户的网络中断

(3)磁盘已满

....

导致我们的程序无法正常的运行。

8.2 异常的体系结构

1、异常的根类型:java.lang.Throwable

2、Throwable有两大子类:

(1)Error:严重的错误,例如:VirtualMachieError(StackOverflewError栈溢出,OutOfMemoryError堆内存溢出),这种错误一般的合理的应用程序不应该通过异常处理(try..catch)去处理,而且应该停下来,修正代码或者升级硬件等。

(2)Exception:一般的异常。合理的应用程序应该给出合理的处理。

面试题:编写一段代码,该代码会发生StackOverflewError?

public void a(){
	a();
}

面试题:编写一段代码,该代码会发生OutOfMemoryError?

public static void main(String[] args){
	int[] arr = new int[Integer.MAX_VALUE];
}

3、Exception又分为两大类

(1)运行时异常:RuntimeException及其子类

? 这种异常,编译器是无法检测到,只会在运行过程中发现。

(2)编译时异常:除了运行时异常,剩下的都是编译时异常,包括Exception父类本身。

这种异常,只要你的代码中可能抛出throws或手动throw,那么编译器就会强制加异常处理的代码,否则编译不通过。

面试题:请列举出至少5种常见的异常/编译时异常/运行时异常?

//编译时异常
Excecption
FileNotFoundException
IOException
SQLException
InterrupttedException
    ...

//运行时异常
ArrayIndexOutOfBoundsException
NullPointerException
ClassCastException
ArithmeticException
NumberFormatException
InputMismatchException
....

8.3 5个关键字

1、try...catch

try{
	可能发生异常的代码
}catch(异常的类型1 e){
	处理异常的代码
	打印异常的信息的代码
}catch(异常的类型2 e){
	处理异常的代码
	打印异常的信息的代码
}。。。

说明:

如果有多个catch,当try中发生异常时,判断的顺序是从上到下,如果上面匹配了,下面就不看了。如果多个catch的类型没有父子类关系,那么顺序无所谓;如果多个catch的类型有父子类关系,那么子上父下。

2、try...catch...finally

try{
	可能发生异常的代码
}catch(异常的类型1 e){
	处理异常的代码
	打印异常的信息的代码
}catch(异常的类型2 e){
	处理异常的代码
	打印异常的信息的代码
}。。。
finally{
	无论try中是否有异常;
	无论catch是否能够捕获异常;
	无论try和catch中是否有return语句;
	finally块都要执行。
}

说明:可能没有catch,成try...finally结构

3、throws

【修饰符】 返回值类型  方法名(【形参列表】) throws 异常列表{
}

throws后面可以是一个或多个异常类型。

方法重写时,有要求:

  • 方法名:必须相同
  • 形参列表:必须相同,如果形参列表不同,就是重载关系
  • 返回值类型:
    • ? 基本数据类型和void:必须相同
    • ? 引用数据类型:<= 子类重写方法的返回值类型 <= 父类被重写的方法的返回值类型
  • 修饰符:
    • ? 权限修饰符:>=
    • ? 其他修饰符:被重写的方法不能是static,private,final等
  • throws异常:<=

4、throw

throw 异常对象;
throw new 异常构造器(...);

  • throw既可以抛出系统预定义的异常类型,也可以抛出我们自定义的异常类型。而且自定义异常类型必须由throw抛出。
  • throw抛出的异常,如果是编译时异常,那么编译器也可以检测到,如果当前方法可以解决,就try...catch,不能解决必须用throws抛给调用者。
  • throw语句可以和return语句一样,如果没有处理该异常,会结束当前方法,并且带给调用者一个异常对象,而不是正常的返回值。

8.4 自定义异常

1、必须继承Throwable类,或它的子类,一般都是继承Exception或RuntimeException

【修饰符】 class 自定义异常类  extends Exception或RuntimeException等{

}

说明:自定义异常的名称非常重要,见名知意

2、尽量要保留两个构造器

(1)无参构造

(2)可以给message赋值的构造器

【修饰符】 class 自定义异常类  extends Exception或RuntimeException等{
	【修饰符】 自定义异常类(){
	
	}
	【修饰符】 自定义异常类(String message){
		super(message);
	}
}

3、尽量实现序列化接口

第九章 多线程

9.1 几个名词(了解)

  • 程序:为了完成一个功能或目的,选择一种编程语言编写的一组指令的集合。它如果没有运行是静态的,存储在硬盘上的一组指令。
  • 进程:程序的一次运行,一个程序多次运行会有多个进程。它是动态的,需要操作系统给这个进程分配独立内存空间等资源。
  • 线程:是进程中的一条执行路径,如果进程中有多个线程,那么多个线程之间可能是“同时”运行。一个进程的多个线程可以有共享的内存。线程是CPU调度的最小单位。
  • 并发:同一个进程的多个线程之间“同时”运行,从宏观角度是“同时”运行的,从微观的角度,其实CPU是“交替”运行多个线程。
  • 并行:同一个进程的多个线程之间“同时”运行,无论从宏观角度还是微观角度都是“同时”运行,要求必须有多个CPU。

面试题:并行和并发?

生活中:
并行:泡面,一边烧水,一边拆调理包,准备...
并发:一边吃饭一边接电话,看起来是同时进行的,但是微观角度,说的时候不吃,吃的时候不说

9.2 在Java程序中编写多线程的几种方式

1、4种:

(1)继承Thread类

(2)实现Runnable接口

(3)实现Callable接口

(4)线程池

2、继承Thread类

步骤:

(1)自定义线程类,继承Thread类

(2)必须重写public void run()方法

(3)创建自定义线程类的对象,调用start()方法启动

public class TestExtendsThread {
    public static void main(String[] args) {
        MyThread my = new MyThread();
        my.start();//从父类Thread继承的   一旦该线程被启动了,就准备好和main线程强CPU了,“同时”运行
//        my.run();//不要手动调用run(),否则就不是多线程了

        //在main线程中打印1-100之间的奇数
        for (int i=1; i<=100; i+=2){
            System.out.println("main:" + i);
        }
    }
}

class MyThread extends Thread{
    //例如:我要在这个线程中打印1-100之间的偶数
    @Override
    public void run(){
        for (int i=2; i<=100; i+=2){
            System.out.println(i);
        }
    }
}

3、实现Runnable接口

步骤:

(1)自定义线程类,实现Runnable接口

(2)必须重写public void run()方法

(3)创建自定义线程类的对象

(4)创建Thread类的对象,并把自定义线程类的对象传给它

(5)调用Thread类的对象的start()

public class TestImplRunnable {
    public static void main(String[] args) {
        MyRunnable my = new MyRunnable();
//        my.start();//没有这个方法

        Thread thread = new Thread(my);
        thread.start();

        //在main线程中打印1-100之间的奇数
        for (int i=1; i<=100; i+=2){
            System.out.println("main:" + i);
        }
    }
}

class MyRunnable implements Runnable{
    //例如:我要在这个线程中打印1-100之间的偶数
    @Override
    public void run(){
        for (int i=2; i<=100; i+=2){
            System.out.println(i);
        }
    }
}

public class TestAnoymous {
    public static void main(String[] args) {
        //一个线程打印奇数
        new Thread(){
            public void run(){
                for(int i=1; i<=100; i+=2){
                    System.out.println("奇数:" + i);
                }
            }
        }.start();

        //一个线程打印偶数
/*        Runnable r = new Runnable(){
            public void run(){
                for(int i=2; i<=100; i+=2){
                    System.out.println(i);
                }
            }
        };
        Thread t = new Thread(r);
        t.start();*/

        new Thread(new Runnable(){
            public void run(){
                for(int i=2; i<=100; i+=2){
                    System.out.println(i);
                }
            }
        }).start();
    }
}

9.3 Thread类的常用方法

1、构造器

  • Thread()
  • Thread(Runnable target)
  • Thread(String name)
  • Thread(Runnable target ,String name)
  • ...

2、方法们

  • public void start():启动线程
  • public void run():必须重写,编写线程要完成的任务代码
  • public static void sleep(时间):线程休眠
  • public static void yield():暂停当前线程(哪个线程执行该语句,那么就是哪个线程暂停)
  • public void join() : 形式xx.join(),哪个线程(例如:main)运行了该句代码,哪个线程(例如:main)就被xx线程阻塞了,要等到xx线程结束才能继续。和其他的线程无关。
  • public void join(时间):形式xx.join(时间),哪个线程(例如:main)运行了该句代码,哪个线程(例如:main)就被xx线程阻塞多少时间了,等时间到了才能继续。和其他的线程无关。
  • public String getName():获取线程名称
  • public static Thread currentThread():获取当前线程对象,获取的线程对象是执行该句代码的线程。
  • public int getPriority():获取线程优先级
  • public void setName(String name):设置线程名称
  • public void setPriority(int priority):设置线程优先级,线程的优先级有范围的要求:1-10。可以使用常量:MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY
  • public boolean isAlive():是否处于活动状态。必须已启动但是还未死亡。
  • public void setDaemon(boolean flag):如果设置为true,说明是守护线程

9.4 关键字:volatile

标记在成员变量的前面,希望程序来获取该成员变量的值的时候,从主存获取,而不是从缓存获取,保证变量的值的一致性。

9.5 线程安全

1、什么情况下可能发生线程安全问题?

(1)多个线程

(2)共享数据

(3)多个线程对共享数据有读有写

2、如何解决?

加锁:

(1)同步锁

(2)Lock(后面juc讲)

同步锁的使用有两种形式:

(1)同步代码块

synchronized(锁对象){
	需要加锁的代码
}

这里的锁对象我们自己选择的,有什么要求?

  • 必须保证有竞争关系(使用共享数据的多个线程)必须使用同一个锁对象,至于对象的类型不重要,任意类型都可以
  • 锁的代码范围要注意

(2)同步方法

【其他修饰符】 synchronized 返回值类型 方法名(【形参列表】)【throws 异常列表】{
}

这里的锁对象不是我们自己选择的:

  • 静态方法:默认选择当前类的Class对象
  • 非静态方法:默认选择this(谨慎)

9.6 线程通信

1、解决什么问题?

解决生产者与消费者问题。

描述:

当有多个线程一起工作,其中一些线程往”数据缓冲区“添加数据,我们称为生产者线程,另一些线程从”数据缓冲区“消耗数据,我们称为消费者线程。因为数据缓冲区的大小是有限制的,那么当数据缓冲区”满“时,生产者线程应该”等待“,直到消费者线程消耗了数据之后再”唤醒/通知“它继续工作,反过来,当数据缓冲区”空“时,消费者线程应该”等待“,直到生产者线程生产了数据之后再”唤醒/通知“它继续工作。

核心问题:

(1)线程安全问题:必须加锁解决

(2)线程通信问题:等待与唤醒机制解决

2、依赖的方法?

  • wait(),wait(时间):等待
  • notify(),notifyAll():唤醒/通知

它们在Object类中声明。

9.7 线程的生命周期

1、观点1:5个状态

新建、就绪、运行、阻塞、死亡

技术分享图片

2、观点2:6个状态

新建(NEW)、就绪和运行(RUNNABLE)、BLOCKED(等锁阻塞)、WAITING(无限等待)、TIMED_WAITING(限时等待)、死亡(TERMINATED).

技术分享图片

9.8 死锁

1、什么情况可能发生死锁?

当两个或更多个线程互相持有对方想要的资源不放手,那么可能就会出现死锁。

2、示例

public class TestDeadLock {
    public static void main(String[] args) {
        Object money = new Object();
        Object girl = new Object();

        Boy boy = new Boy("开发",money ,girl);
        Bang bang = new Bang("来首臣",money ,girl);

        boy.start();
        bang.start();
    }
}

class Boy extends Thread{
    private Object money;
    private Object girl;

    public Boy(String name, Object money, Object girl) {
        super(name);
        this.money = money;
        this.girl = girl;
    }

    @Override
    public void run() {
        synchronized (money){
            System.out.println("你先放了翠花,我给你500万");
            synchronized (girl) {
                System.out.println("当接到翠花后,给了对方500万");
            }
        }

    }
}
class Bang extends Thread{
    private Object money;
    private Object girl;

    public Bang(String name, Object money, Object girl) {
        super(name);
        this.money = money;
        this.girl = girl;
    }

    @Override
    public void run() {
        synchronized (girl) {
            System.out.println("你先给我500万,我放了翠花。");
            synchronized (money){
                System.out.println("当收到500万,会放了翠花。");
            }
        }

    }
}

9.9 面试题:sleep和wait区别

1、sleep是在Thread类中声明的,静态的方法,通过Thread.sleep(时间)进行调用。

? wait是在Object类中声明的,非静态方法,必须通过“锁/监视器”对象进行调用。

2、sleep(时间)必须指定休眠的时间,时间到或者中途被interrupt,会从休眠的状态恢复到就行状态。

? wait():没有指定时间,那么必须被notify()/notifyAll()唤醒,或者中途被interrupt

? wait(时间):指定等待时间,等时间到或者中途被interrupt,恢复

3、sleep方法不会让当前线程失去锁,即不释放锁,

? wait方法会让当前线程释放锁,让其他线程去执行任务。

第十章 常用API和基础算法

10.1 和数学相关

1、java.lang.Math类

  • abs(x):求绝对值
  • sqrt(x):求平方根
  • pow(x,y):求x的y次方
  • ceil(x):向上取整
  • floor(x):向下取整
  • round(x):四舍五入
  • random():得到[0,1)范围的随机值
  • max(x,y):求x,y的最大值
  • min(x,y):求x,y的最小值
  • PI:圆周率
  • ...

2、java.math包

  • BigInteger:不可变对象,任意精度的整数

  • BigDecimal:不可变对象,任意精度的小数

  • 它们要new对象来表示数据,要通过add,subtract,multiply,divide等方法来进行算术运算

3、java.util.Random

Random r = new Random();

boolean b = r.nextBoolean();//true/false
double d = r.nextDouble();//[0,1)
int i = r.nextInt();//int范围内
int j = r.nextInt(x);//[0,x)范围内的整数

10.2 日期时间相关

10.2.1 java.util.Date

  • new Date():获取系统日期时间
  • new Date(long 毫秒):根据毫秒值来获取日期时间
  • long getTime():获取该日期时间对应的毫秒值,距离1970-1-1 0:0:0

10.2.2 java.util.Calendar

如何创建/获取Calendar的对象?

(1)创建子类对象:GregorianCalendar

(2)获取指定时区的日历对象:

  • getInstance()
  • getInstance(TimeZone 时区) 或 getInstance(Locale 语言环境)
  • getInstance(TimeZone, Locale)

(3)get(字段名)

例如:int year = get(Calendar.YEAR);

10.2.3 java.text.DateFormat 日期时间格式化

  • DateFormat.getDateInstance()
  • DateFormat.getDateInstance(int style)
  • DateFormat.getDateInstance(int style, Locale aLocale)
  • DateFormat.getDateTimeInstance()
  • DateFormat.getDateTimeInstance(int dateStyle, int timeStyle)
  • DateFormat.getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale)
    • dateStyle - 给定的日期格式化风格。例如,SHORT 用于 US 语言环境中的 "M/d/yy"。
    • timeStyle - 给定的时间格式化风格。例如,SHORT 用于 US 语言环境中的 "h:mm a"。
  • DateFormat.getTimeInstance()
  • DateFormat.getTimeInstance(int style)
  • DateFormat.getTimeInstance(int style, Locale aLocale)
  • 使用它的子类:SimpleDateFormat
    • y:年
    • M:月
    • d:日
    • D:一年中的第几天
    • H:24小时制的时
    • h:12小时制的时
    • m:分
    • s:秒
    • S:毫秒
    • E:星期
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

//把日期转字符串
String str = sf.format(Date的日期对象);
//把字符串解析为日期
Date d = sf.parse(字符串);

10.2.4 java.time包

1、本地化日期/时间:LocalDate,LocalTime,LocalDateTime

方法 描述
now() / now(ZoneId zone) 静态方法,根据当前时间创建对象/指定时区的对象
of(...) 静态方法,根据指定日期/时间创建对象
getDayOfMonth()/getDayOfYear() 获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek() 获得星期几(返回一个 DayOfWeek 枚举值)
getMonth() 获得月份, 返回一个 Month 枚举值
getMonthValue() / getYear() 获得月份(1-12) /获得年份
getHours()/getMinute()/getSecond() 获得当前对象对应的小时、分钟、秒
withDayOfMonth()/withDayOfYear()/withMonth()/withYear() 将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
with(TemporalAdjuster t) 将当前日期时间设置为校对器指定的日期时间
plusDays(), plusWeeks(), plusMonths(), plusYears(),plusHours() 向当前对象添加几天、几周、几个月、几年、几小时
minusMonths() / minusWeeks()/minusDays()/minusYears()/minusHours() 从当前对象减去几月、几周、几天、几年、几小时
plus(TemporalAmount t)/minus(TemporalAmount t) 添加或减少一个 Duration 或 Period
isBefore()/isAfter() 比较两个 LocalDate
isLeapYear() 判断是否是闰年(在LocalDate类中声明)
format(DateTimeFormatter t) 格式化本地日期、时间,返回一个字符串
parse(Charsequence text) 将指定格式的字符串解析为日期、时间

2、指定时区日期时间:ZonedDateTime

import java.time.ZoneId;
import java.time.ZonedDateTime;

public class TestZonedDateTime {
	public static void main(String[] args) {
		ZonedDateTime t = ZonedDateTime.now();
		System.out.println(t);
		
		ZonedDateTime t1 = ZonedDateTime.now(ZoneId.of("America/New_York"));
		System.out.println(t1);
	}
}

3、阶段日期或时间:间隔日期Period和持续时间Duration

between(x,y):两个日期或时间

Period:用于计算两个“日期”间隔

public static void main(String[] args) {
		LocalDate t1 = LocalDate.now();
		LocalDate t2 = LocalDate.of(2018, 12, 31);
		Period between = Period.between(t1, t2);
		System.out.println(between);
		
		System.out.println("相差的年数:"+between.getYears());//1年
		System.out.println("相差的月数:"+between.getMonths());//又7个月
		System.out.println("相差的天数:"+between.getDays());//零25天
		System.out.println("相差的总数:"+between.toTotalMonths());//总共19个月
	}

Duration:用于计算两个“时间”间隔

	public static void main(String[] args) {
		LocalDateTime t1 = LocalDateTime.now();
		LocalDateTime t2 = LocalDateTime.of(2017, 8, 29, 0, 0, 0, 0);
		Duration between = Duration.between(t1, t2);
		System.out.println(between);
		
		System.out.println("相差的总天数:"+between.toDays());
		System.out.println("相差的总小时数:"+between.toHours());
		System.out.println("相差的总分钟数:"+between.toMinutes());
		System.out.println("相差的总秒数:"+between.getSeconds());
		System.out.println("相差的总毫秒数:"+between.toMillis());
		System.out.println("相差的总纳秒数:"+between.toNanos());
		System.out.println("不够一秒的纳秒数:"+between.getNano());
	}

4、格式化:DateTimeFormatter

该类提供了三种格式化方法:

预定义的标准格式。如:ISO_DATE_TIME;ISO_DATE

本地化相关的格式。如:ofLocalizedDate(FormatStyle.MEDIUM)

自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

@Test
	public void test10(){
		LocalDateTime now = LocalDateTime.now();
		
//		DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);//2019年6月6日 下午04时40分03秒
		DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);//19-6-6 下午4:40
		String str = df.format(now);
		System.out.println(str);
	}
	@Test
	public void test9(){
		LocalDateTime now = LocalDateTime.now();
		
		DateTimeFormatter df = DateTimeFormatter.ISO_DATE_TIME;//2019-06-06T16:38:23.756
		String str = df.format(now);
		System.out.println(str);
	}
	
	@Test
	public void test8(){
		LocalDateTime now = LocalDateTime.now();
		
		DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒  SSS毫秒  E 是这一年的D天");
		String str = df.format(now);
		System.out.println(str);
	}

public class TestReview {
    public static void main(String[] args) {
        LocalDateTime d = LocalDateTime.now();
        DateTimeFormatter df = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.of("Asia/Shanghai"));
        System.out.println(df.format(d));//2020年9月12日 星期六 上午08时46分09秒 CT
    }
}

10.3 系统相关类

1、java.lang.System类:系统工具类

static long currentTimeMillis():获取系统时间的毫秒值

static void exit(x):退出JVM

static void arraycopy(原数组, 原数组的起始下标, 目标数组, 目标数组的起始下标,一共复制几个元素)

static void gc():通知垃圾回收器工作

static String getProperty(系统属性名)

2、java.lang.Runtime类:JVM运行环境

static Runtime getRuntime():单例类的对象

long totalMemory()

long freeMemory()

void gc()

10.4 数组相关的算法

10.4.1 反转

//方式一:借一个数组
int[] arr = {1,2,3,4,5};

//(1)先创建一个新数组,长度和原来的数组一样
int newArr = new int[arr.length];
//(2)把原数组的元素,倒置放到新数组中
for(int i=0; i<arr.length; i++){
    newArr[i] = arr[arr.length-1-i];
}
//(3)指向新数组
arr = newArr;

//方式二:首尾交换
int[] arr = {1,2,3,4,5};

int start = 0;
int end = arr.length;
//反转[start, end)
//交换次数:(end-start)/2
for(int i=0; i< (end-start)/2; i++){//循环次数代表交换的次数
    int temp = arr[start+i];
    arr[start+i] = arr[end-1-i];
    arr[end-1-i] = temp;
}

//方式二:首尾交换
int[] arr = {1,2,3,4,5};

int start = 0;
int end = arr.length;
//反转[start, end)
for(int left=start, right=end-1; left<=right; left++,right--){//left代表左边元素的下标,right代表右边元素的下标
    int temp =  arr[left];
    arr[left] = arr[right];
    arr[right] = temp;
}

10.4.2 扩容

步骤:

(1)先创建一个新数组

  • 可能在原数组基础上 + 一定长度
  • 可能原数组长度翻倍

(2)把旧数组的数据复制到新数组中

(3)放入新增元素

(4)指向新数组

int[] arr = {1,2,3,4,5};

//(1)先创建一个新数组
int[] newArr = new int[arr.length+1];

//(2)复制元素
for(int i=0; i<arr.length; i++){//这里i<arr.length,注意越界
    newArr[i] = arr[i];
}

//(3)指向新数组
arr = newArr;

//(4)把新元素放入
arr[arr.length-1] = 新值;

10.4.3 插入

步骤:

(1)判断数组是否需要扩容

如果需要,先扩容

(2)把[index]位置和它后面的元素往右移动

(3)在[index]位置放入新元素

int[] arr = {....};

//1、先扩容
//(1)先创建一个新数组
int[] newArr = new int[arr.length+1];

//(2)复制元素
for(int i=0; i<arr.length; i++){//这里i<arr.length,比较越界
    newArr[i] = arr[i];
}

//(3)指向新数组
arr = newArr;

//2、移动元素
System.arraycopy(arr, index, arr, index+1, 移动元素的个数);

//3、在[index]位置放入新元素
arr[index] = 新值;

10.4.4 删除

步骤:

(1)把[index+1]位置和它后面的元素往左移动

(3)把当前数组的最后一个元素还原

int[] arr = {....};

//移动元素
System.arraycopy(arr, index+1, arr, index, 移动元素的个数);

arr[最后元素的下标] = 0;//引用数据类型的数组,就是为null

10.4.5 二分查找

int[] arr = {....};//前提是有序的

int left = 0;
int right = arr.length-1;
int mid = (left + right)/2;
int index = -1;
int value = ?;

while(left<=right){
	if(arr[mid] == value){
        index = mid;
        break;
    }else if(arr[mid] > value){ //如果数组是从小到大,往左移动
        //修改右边界
        right = mid -1 ;
    }else{
        //修改左边界
        left = mid +1;
    }
    //因为left或right变了,mid也得重新计算
    mid = (left + right)/2;
}

if(index==-1){
    //没有
}

int[] arr = {....};//前提是有序的

int index = -1;
int value = ?;

for(int left = 0, right = arr.length-1; left<=right;){
    int mid = (left + right)/2;
	if(arr[mid] == value){
        index = mid;
        break;
    }else if(arr[mid] > value){ //如果数组是从小到大,往左移动
        //修改右边界
        right = mid -1 ;
    }else{
        //修改左边界
        left = mid +1;
    }
}

if(index==-1){
    //没有
}

10.4.5 选择排序

int[] arr = {....};

//轮数 = arr.length-1
for(int i=0; i<arr.length-1; i++){
    //(1)找出本轮最小值
    int minIndex = i;
    for(int j=i+1; j<arr.length-1; j++){
        if(arr[minIndex] > arr[j]){
            minIndex = j;
        }
    }
    
    //(2)如果本轮最小值不在它应该在的位置
    if(minIndex != i){
        int temp = arr[minIndex];
        arr[minIndex] = arr[i];
        arr[i] = temp;
    }
}

10.4.6 数组工具类java.util.Arrays

序号 方法签名 方法功能简介
1 static int binarySearch(数据类型[] a, 数据类型 key) 使用二分查找法在a数组中查找key的下标
2 static 数据类型[] copyOf(数据类型[] original, int newLength) 根据original复制一个新数组长度为newLength
3 static 数据类型[] copyOfRange(数据类型[] original, int from, int to) 根据original的[from,to)部分复制一个新数组
4 static boolean equals(数据类型[] a1, 数据类型[] a2) 比较两个数组内容是否一致
5 static void fill(数据类型[] a, 数据类型val) 用val的值填充a数组
6 static void sort(数据类型[] a) 将a数组按照自然排序规则实现升序排列
7 static void sort(数据类型[] a, Comparator c) 将a数组按照c指定的定制比较规则实现升序排列
8 static String toString(数据类型[] a) 将数组的元素拼接为一个字符串返回

10.5 String类

10.5.1 String类的特点

1、String类是final修饰的,不能被继承

2、String类的底层使用数组存储

JDK1.9之前:char[] value

JDK1.9之后:byte[] value

3、String类的对象不可变

(1)字符串常量池中存储字符串常量,可以共享

(2)每次修改都会产生新对象,频繁修改的话效率不高

如果涉及到大量的字符串修改操作,建议使用StringBuffer或StringBuilder

如何实现不可变的?

(1)类本身不能继承,没有子类会重写

(2)底层存储的value数组都是final修饰

(3)底层存储的value数组私有化

(4)对外提供的所有修改字符串的方法,都返回一个新字符串对象

4、在Java程序代码中任意使用“”引起来的部分都是字符串的对象

10.5.2 String对象的创建

1、字面常量值

String str = "hello";

2、使用构造器

String str = new String();
String str = new String("hello");
char[] arr = {...};
String str = new String(arr);
byte[] arr = {...};
String str = new String(arr);
//....

3、静态方法

String str = String.copyValueOf(xx);
String str = String.valueOf(xx);

4、xx.toString()

Student stu = new Student(xx);
String str = stu.toString();

StringBuffer s = new StringBuffer(xx);
String str = s.toString();

5、和字符串的拼接+

int a = 10;
String str = a + "";

10.5.3 String对象的个数

String str1 = "hello";//1个
String str2 = "hello";//和上面是同1个

String str = new String("atguigu");//2个,一个在常量池一个在堆

String s1 = "hello";//1个
String s2 = "world";//1个
String s3 = s1 + s2 + "java";//"java"1个,s1 + s2拼接结果1个,最后结果1个

10.5.4 String对象的拼接

1、+

(1)常量池中的字符串常量 + 常量池中的字符串常量 :结果是在常量池中

(2)变量 + xx:结果都在堆

(3)指向堆中的字符串常量 + xx:结果都在堆

(4)xx拼接结果.intern():结果都在常量池

2、concat:拼接的结果都是新的字符串,都在堆中

10.5.5 String对象的比较

1、==:比较地址,只有都是常量池中的字符串对象,内容相同地址也相同,其他的只要是不同的对象,地址都不同。

2、equals(xx):比较字符串内容,严格区分大小写

3、equalsIgnoreCase(xx):比较字符串内容,不区分大小写

4、compareTo(xx):比较字符串的大小,按照字符编码值比较,严格区分大小写

5、compareToIgnoreCase(xx):比较字符串的大小,按照字符编码值比较,不区分大小写

6、java.text.Collator:文本校对器比较大小,按照指定语言环境的自然语言顺序比较大小(字典排序)

10.5.6 空字符串与空字符串的判断

if(str !=null && str.isEmpty())
if(str !=null && str.length()==0)
if(str !=null && str.equals(""))
if("".equals(str))

10.5.7 String类的常用API

序号 方法签名 方法功能简介
1 String() 创建空字符串
2 String(String original) 根据original创建一个新字符串
3 static String valueOf(xx value) 根据value内容创建一个字符串
4 String intern() 将字符串的内容存入常量池
5 String concat() 字符串拼接
6 boolean equals(Object obj) 判断当前字符串与指定字符串内容是否已在,严格区分大小写
7 boolean equalsIgnoreCase(String obj) 判断当前字符串与指定字符串内容是否已在,不区分大小写
8 int compareTo(String str) 比较当前字符串与指定字符串的大小,严格区分大小写
9 int compareToIgnoreCase(String str) 比较当前字符串与指定字符串的大小,不区分大小写
10 boolean isEmpty() 判断当前字符串是否为空
11 int length() 返回当前字符串的长度
12 String toLowerCase() 将当前字符串转为小写
13 String toUpperCase() 将当前字符串转为大写
14 String trim() 去掉当前字符串前后空白符
15 boolean contains(xx) 判断当前字符串中是否包含xx
16 int indexOf(xx) 在当前字符串中查找xx第一次出现的下标
17 int lastIndexOf(xx) 在当前字符串中查找xx最后一次出现的下标
18 String substring(int beginIndex) 从当前字符串的[beginIndex, 最后]截取一个子串
19 String substring(int beginIndex, int endIndex) 从当前字符串的[beginIndex, endIndex)截取一个子串
20 char charAt(index) 返回当前字符串[index]位置字符
21 char[] toCharArray() 将当前字符串的内容用一个字符数组返回
22 String(char[] value) 用value字符数组的元素构建一个新字符串
23 String(char[] value,int offset, int count) 用value字符数组的[offset]开始的count个字符构建一个新字符串
24 static String copyValueOf(char[] data) 用data字符数组的元素构建一个新字符串
25 static String copyValueOf(char[] data, int offset, int count) 用data字符数组的[offset]开始的count个字符构建一个新字符串
26 static String valueOf(char[] data) 用data字符数组的元素构建一个新字符串
27 static String valueOf(char[] data, int offset, int count) 用data字符数组的[offset]开始的count个字符构建一个新字符串
28 byte[] getBytes() 将当前字符串按照平台默认字符编码方式编码为字节序列
29 byte[] getBytes(字符编码方式) 将当前字符串按照指定字符编码方式编码为字节序列
30 String(byte[] bytes) 将bytes字节序列按照平台默认字符编码方式解码为字符串
31 String(byte[] bytes,String charsetName) 将bytes字节序列按照指定字符编码方式解码为字符串
32 boolean startsWith(xx) 判断当前字符串是否以xx开头
33 boolean endsWith(xx) 判断当前字符串是否以xx结尾
34 boolean matchs(xx) 判断当前字符串是否满足xx正则
35 String replace(xx,yy) 将当前字符串中所有xx替换为yy
36 String replaceFirst(xx,value) 将当前字符串中第一个满足xx正则的字符替换为value
37 String repalceAll(xx, value) 将当前字符串中所有满足xx正则的字符替换为value
38 String[] split(xx) 将当前字符串按照xx正则拆分为多个字符串
39 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 将当前字符串的[srtBegin,srcEnd)部分字符复制到dst字符数组中,dst数组从[dstBegin]开始存储

10.7 StringBuffer和StringBuilder

10.7.1 String类与StringBuffer和StringBuilder区别

String类的对象是不可变字符序列,StringBuffer和StringBuilder的对象是可变字符序列。

StringBuffer:老点,线程安全的,因为它的方法有synchronized修饰

StringBuilder:JDK1.5之后引入的,线程不安全,单线程情况下推荐使用。

10.7.2 StringBuffer和StringBuilder的API

String==>StringBuffer或StringBuilder,用构造器

StringBuffer或StringBuilder==>String,用toString方法

序号 方法签名 方法区功能简介
1 StringBuffer() 创建一个空的可变字符序列,默认长度16
2 StringBuffer(String str) 用字符串str内容创建一个可变字符序列
3 StringBuffer append(数据类型 b) 在当前字符序列后面追加b
4 StringBufferinsert(int index, 数据类型 s) 在当前字符序列[index]插入s
5 StringBuffer delete(int start, int end) 删除当前字符序列[start,end)部分字符
6 StringBuffer deleteCharAt(int index) 删除当前字符序列[index]位置字符
7 void setLength(int newLength) 修改当前字符序列的长度为newLength
8 void setCharAt(int index, char ch) 替换当前字符序列[index]位置字符为ch
9 StringBuffer reverse() 将当前字符序列内容反转
10 StringBuffer replace(int start, int end, String str) 替换当前字符序列[start,end)部分字符为str
11 int indexOf(String str) 在当前字符序列中开始查找str第一次出现的下标
12 int indexOf(String str, int fromIndex) 在当前字符序列[fromIndex]开始查找str第一次出现的下标
13 int lastIndexOf(String str) 在当前字符序列中开始查找str最后一次出现的下标
14 int lastIndexOf(String str, int fromIndex) 在当前字符序列[fromIndex]开始查找str最后一次出现的下标
15 String substring(int start) 截取当前字符序列[start,最后]部分构成一个字符串
16 String substring(int start, int end) 截取当前字符序列[start,end)部分构成一个字符串
17 String toString() 将当前可变字符序列的内容用String字符串形式表示
18 void trimToSize() 如果缓冲区大于保存当前字符序列所需的存储空间,则将重新调整其大小,以便更好地利用存储空间。
19 int length() 返回当前字符序列的长度
20 char charAt(int index) 返回当前字符序列[index]位置字符
21 void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 将当前字符串的[srtBegin,srcEnd)部分字符复制到dst字符数组中,dst数组从[dstBegin]开始存储

第11章 泛型

泛型是JDK1.5之后引入的。

11.1 泛型

泛型:参数化的类型,即把数据类型当做参数来传递

? 又有的地方又称为泛化的类型,用一个单个大写字母,例如来代表任意类型,这个T就是泛化的类型。

泛型的好处:

(1)表示某个变量的类型更灵活

(2)安全:有了泛型,在编译期间就可以避免不符合类型的数据赋值

(3)避免数据类型转换

11.2 泛型类或泛型接口

1、语法格式

【修饰符】 class 类名<类型变量列表/泛型类型形参列表>{

}
【修饰符】 interface 接口名<类型变量列表/泛型类型形参列表>{

}

2、注意

(1)<类型变量列表/泛型类型形参列表>:使用单个的大写字母表示,例如:,...

(2)<类型变量列表/泛型类型形参列表>:可以多个,每个之间使用,分割,例如:<K,V>

(3)如果要指定<类型变量列表/泛型类型形参列表>的实际类型,必须是引用数据类型,不能是基本数据类型

(4)<类型变量列表/泛型类型形参列表>可能有上限,上限的要求:

< T extends 上限1 & 上限2 ...>

上限中类只能有一个,如果有必须在最左边,接口的话可以多个。

多个上限之间是&(与)的关系。

(5)在类或接口上面声明的泛型是不能用于静态成员

3、什么时候才能确定<类型变量列表/泛型类型形参列表>的实际类型

(1)创建对象时

Collection<String> c = new ArrayList<String>();

(2)继承

class Student<T>{
	//....
}
class ChineseStudent extends Student<String>{

}

(3)实现接口

class Circle implements Comparable<Circle>{
	public int compareTo(Circle c){
		//...
	}
}

4、泛型的擦除

如果在使用泛型类或泛型接口时,没有指定它的具体类型,那么泛型被擦除,泛型的类型就会自动按照它的第一个上限处理,如果没有指定上限,默认就是Object类型

class Student<T>{
	//....
}

Student stu = new Student();//这里T按照Object

class Student<T extends Number & Comparable & ....>{
	//....
}

Student stu = new Student();//这里T按照Number

11.3 泛型方法

1、语法格式

【修饰符】 <类型变量列表/泛型类型形参列表> 返回值类型  方法名(【数据形参列表】)【throws 异常列表】

2、注意

(1)<类型变量列表/泛型类型形参列表>:使用单个的大写字母表示,例如:,...

(2)<类型变量列表/泛型类型形参列表>:可以多个,每个之间使用,分割,例如:<K,V>

(3)如果要指定<类型变量列表/泛型类型形参列表>的实际类型,必须是引用数据类型,不能是基本数据类型

(4)<类型变量列表/泛型类型形参列表>可能有上限,上限的要求:

< T extends 上限1 & 上限2 ...>

上限中类只能有一个,如果有必须在最左边,接口的话可以多个。

多个上限之间是&(与)的关系。

(5)每一个泛型方法的<类型变量列表/泛型类型形参列表>是独立的,和别的方法无关,和类上面的泛型也无关。

3、什么时候确定泛型方法的<类型变量列表/泛型类型形参列表>具体类型?

方法被调用,根据方法的实参的类型自动推断。

11.4 泛型通配符

当我们在使用泛型类或泛型接口时,无法确定泛型的具体类型,可以使用通配符。

形式:

(1)泛型类/接口名<?>:?代表任意引用数据类型

(2)泛型类/接口名<? extends 上限>:?代表的是该上限或上限的子类类型

(3)泛型类/接口名<? super 下限>:?代表的是该下限或下限的父类类型

import org.junit.Test;

import java.util.ArrayList;
import java.util.Collection;

public class TestGeneric {
    @Test
    public void test01(){
//        <?>的任意类型,不是指可以添加任意值,而是右边对应<>中可以写任意类型
        Collection<?> c1 = new ArrayList<>();//不写,默认是?
        Collection<?> c2 = new ArrayList<Object>();//写,只要是引用数据类型即可
        Collection<?> c3 = new ArrayList<String>();//写,只要是引用数据类型即可
        Collection<?> c4 = new ArrayList<Integer>();//写,只要是引用数据类型即可

        c1.add(null);
//        c1.add("hello");//错误
//        c1.add(1);//错误
//
//        c2.add("hello");//错误,编译时按左边来,左边<?>是未知的任意类型
    }

    @Test
    public void test02(){
        //<? extends Number> 表示右边对应的<>中只能写Number或Number的子类
        Collection<? extends Number> c1 = new ArrayList<>();//不写,和左边一样
//        Collection<? extends Number> c2 = new ArrayList<Object>();
//        Collection<? extends Number> c3 = new ArrayList<String>();
        Collection<? extends Number> c4= new ArrayList<Number>();
        Collection<? extends Number> c5= new ArrayList<Integer>();

        c1.add(null);
//        c1.add(1);//错误   ?可能是Nubmer,可能是Integer,可能是Double等

    }
    @Test
    public void test03(){
        //<? super Number>表示右边的<>中,只能写Number或Number的父类
        Collection<? super Number> c1 = new ArrayList<>();//不写,和左边一样
        Collection<? super Number> c2 = new ArrayList<Object>();
//        Collection<? super Number> c3 = new ArrayList<String>();
        Collection<? super Number> c4 = new ArrayList<Number>();
//        Collection<? super Number> c5 = new ArrayList<Integer>();

        c1.add(null);
        c1.add(1);//因为?一定是Number或Number的父类,那么可以接收Number子类的对象
        c1.add(1.0);
    }
}

11.5 泛型的其他小问题

1、<>在左右两边 类型必须一致

@Test
	public void test3() {
		//(1)两个<>中的类型一致  (对)
		Collection<String> c1 = new ArrayList<String>();
		
		//(2)两个<>中的类型不一致  (错)
//		Collection<Object> c2 = new ArrayList<String>();
		//因为Object是String的父类
		//Collection是ArrayList的父接口
//		但是Collection<Object>不是ArrayList<String>的父类
	}

2、JDK1.7之后允许右边<>里面空着,根据左边的自动推断

@Test
	public void test4() {
		//JDK1.7之后,允许右边<>里面是空的
		Collection<String> c1 = new ArrayList<String>();//手动指定
		Collection<String> c2 = new ArrayList<>();//编译器认为是自动按照左边的处理
//		Collection<String> c3 = new ArrayList();//编译器认为它没有指定泛型,编译有警告
	}

3、try...catch的catch里面不能使用T这种来代表任意异常类型。

@Test
	public void test5() {
		try {
			
		}catch(T t) {
			
		}
	}

4、泛型类不能创建数组对象

@Test
	public void test6() {
		T[] arr;
		//T[]是一种数组类型,它是要在运行时,由编译器根据T的实际类型,动态编译出一种新的数据类型
//		arr = new T[5];//因为T编译期间不确定,那么编译器找对应的构造器是找不到
	}

第12章 集合

12.1 集合的概念

集合:

? 是一种容器,用来装对象的容器,不能装基本数据类型。如果装基本数据类型的值,会自动装箱。

? 数组也是容器,可以用来装基本数据类型,也可以用来装对象。

? 本质上,集合需要用对应的数据结构来实现。

集合分为两大类:

(1)Collection系列:存储一组对象,每个对象之间是独立的

(2)Map系列:存储一组映射关系(key,value),即存储对值。

12.2 Collection系列

12.2.1 java.util.Collection接口

Collection接口:是Collection系列的集合的根接口,它没有提供任何的直接的实现,而是提供更具体的子接口(例如:List,Set,Queue等)的实现。有一些是可重复的,有一些时不可重复的,有一些时有序的,有一些是无序的。

12.2.2 Collection接口API

序号 归类 方法签名 方法描述
1 添加 add(E e) 添加一个元素
2 添加 addAll(Collection c) 添加多个元素,把c集合的所有元素都添加到当前集合中,this = this ∪ c;
3 删除 clear() 清空集合
4 删除 remove(Object obj) 删除一个元素,根据元素的equals()来判断是否是要被删除的元素,如果元素的类型没有重写equals方法,那么等价于==,如果重写了equals,那么就按照equals的规则来比较,一般比较内容。
5 删除 removeAll(Collection c) 删除多个元素,把当前集合中和c共同的元素删除,即this = this - this ∩ c;
6 删除 retainAll(Collection c) 删除多个元素,在当前集合中保留和c的共同的元素,即this = this ∩ c;
7 int size() 获取元素的个数
8 boolean contains(Object obj) 是否包含某个元素。根据元素的equals()来判断是否是要被删除的元素,如果元素的类型没有重写equals方法,那么等价于==,如果重写了equals,那么就按照equals的规则来比较,一般比较内容。
9 boolean containsAll(Collection c) 是否包含多个元素。判断当前集合中是否包含c集合的所有元素,即c是否是this的子集。
10 boolean isEmpty() 集合是否为空
11 遍历 Object[] toArray() 将集合中的元素用数组返回
12 遍历 Iterator iterator() 返回一个迭代器对象,专门用于遍历集合

12.2.3 Collection系列的集合遍历

1、使用数组

	@Test
	public void test01() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		Object[] array = coll.toArray();
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i]);
		}
	}

2、使用foreach

foreach的语法格式
for(元素的类型  元素名 :  容器名){

}

容器名:数组名,或者Collection系列的集合名称, 或者说实现了Iterable接口的其他容器名

	@Test
	public void test02() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		for (String str : coll) {
			System.out.println(str);
		}
	}

	@Test
	public void test03() {
		String[] coll = {"hello","world","java"};
		
		for (String str : coll) {
			System.out.println(str);
		}
	}

3、使用迭代器

java.util.Iterator类型,接口类型。

(1)boolean hasNext():判断当前游标后面是否还有下一个元素

(2)E next():取出下一个元素

(3)remove():删除刚刚迭代/取过的那个元素

	@Test
	public void test04() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		Iterator<String> iterator = coll.iterator();
		while(iterator.hasNext()) {
			String next = iterator.next();
			System.out.println(next);
		}
	}

12.2.4 删除Collection元素

1、明确删除

如果集合中有重复元素,只会删除第一个满足条件的

	@Test
	public void test07() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		coll.remove(new String("hello"));//可以
		coll.remove("hello");//可以,因为是根据元素的equals方法,String重写了equals
		
		for (String str : coll) {
			System.out.println(str);
		}
	}

2、根据条件删除

可以删除多个

@Test
	public void test08() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		Iterator<String> iterator = coll.iterator();
		while(iterator.hasNext()) {
			String next = iterator.next();
			if(next.length()<=4) {
				iterator.remove();
			}
		}
		
		for (String string : coll) {
			System.out.println(string);
		}
	}

3、以下两种错误不能犯

	@Test
	public void test09() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		Iterator<String> iterator = coll.iterator();
		while(iterator.hasNext()) {
			String next = iterator.next();
			if(next.length()<=4) {
				coll.remove(next);//错误
			}
		}
		
		for (String string : coll) {
			System.out.println(string);
		}
	}

	@Test
	public void test10() {
		Collection<String> coll = new ArrayList<String>();
		coll.add("hello");
		coll.add("world");
		coll.add("java");
		
		for (String string : coll) {
			if(string.length()<=4) {
				coll.remove(string);//错误的
			}
		}
	}

12.2.5 java.util.Iterator和java.lang.Iterable

java.lang.Iterable:表示可迭代的,它的抽象方法:Iterator iterator(),实现这个接口意味着可以使用foreach遍历。Java集合框架中Collection系列的集合都实现了这个接口,意味着可以使用foreach循环。而Map系列集合没有实现这个接口,不能直接使用foreach,需要通过entrySet(), keySet(), values()进行转换,然后再使用foreach。

java.util.Iterator:表示迭代器。每一种实现了Iterable接口的集合内部,都会有一个内部类,例如:ArrayList的内部有一个Itr内部类,实现Iterator接口,用于集合的迭代。

java.util.Iterator的抽象方法:

(1)boolean hasNext()

(2)E next()

(3)void remove()

java.util.Iterator的子接口:java.util.ListIterator。List系列的集合内部有提供listIterator()方法可以获取ListIterator对象,在List系列的集合内部有内部类实现了ListIterator接口。它的抽象方法:

(1)boolean hasNext()

(2)E next()

(3)void remove()

(4)boolean hasPrevious()

(5)E previous()

(6)void add(E e)

(7)void set(E e)

(8)int nextIndex()

(9)int previousIndex()

	@Test
	public void test14() {
		List<String> list = new ArrayList<>();
		list.add("hello");
		list.add("world");
		list.add("java");
		list.add("chai");
		
//		ListIterator<String> listIterator = list.listIterator();//默认游标仍然在最开始
		ListIterator<String> listIterator = list.listIterator(list.size());//游标指向最后
		while(listIterator.hasPrevious()) {
			String str = listIterator.previous();
			System.out.println(str);
		}
	}

12.3 List系列

12.3.1 List系列的集合特点

(1)有序,可以通过索引/下标进行操作

(2)可重复的:调用equals相同的元素

12.3.2 List集合的API

在Collection接口的API基础之上增加如下方法:

1、添加

add(int index, E e):在[index]位置添加一个

addAll(int index , Collection c):在[index]位置添加多个

2、删除

E remove(int index):删除[index]位置的元素,返回被删除的元素

3、修改

E set(int index, E e):替换[index]位置的元素,返回被替换的元素

4、查询

E get(int index):返回[index]位置的元素

List subList(int start, int end):截取[start,end)部分的元素

int indexOf(Object obj):返回obj在当前集合中第一次出现的下标

int lastIndexOf(Object obj):返回obj在当前集合中最后一次出现的下标

5、遍历

在原来Iterator和foreach遍历的基础上增加了:

ListIterator listIterator():默认游标在[0]开始

ListIterator listIterator(int index):默认游标在[index]位置

12.3.3 List接口的常用实现类

1、ArrayList:动态数组

2、Vector:动态数组

3、LinkedList:双向链表

4、Stack:栈

面试题:ArrayList与Vector的区别

ArrayList:新一点, 线程不安全,扩容的机制默认为原来的1.5倍。

? ArrayList在JDK1.7之后new ArrayList()初始化数组为一个空数组。

Vector:最古老的动态数组,线程安全的,扩容的机制默认为原来的2倍,还多支持了一个旧版的迭代器Enumeration迭代。

面试题:动态数组与链表的区别

区别一:
动态数组的元素:数据本身
双向链表的元素:结点

结点包含:
①前一个结点的首地址:prev
②数据本身
③后一个结点的首地址:next

区别二:
动态数组的元素是连续存储,提前开辟一整块连续的内存空间。
双向链表的元素不要求连续存储,来一个申请一个连接一个。

连续的好处:查询快,可以根据数组的首地址,快速的定位到某个元素。
不连续的缺点:只能从头或尾挨个查找,定位到对应的位置。

连续的坏处:插入,删除时,需要移动元素,插入时可能考虑扩容。
不连续的好处:不用提前申请空间,插入和删除只需要修改前后元素的prev和next

面试题:Stack栈结构的特点

(1)先进后出

(2)Stack是Vector的子类,比Vector增加了几个方法:push,pop,peek等来体现先进后出的特点

面试题:Queue/Deque(双端队列)队列结构的特点

(1)先进先出

(2)代表:LinkedList等

12.4 Set系列

12.4.1 Set系列的集合的特点

(1)不可重复

(2)TreeSet:按大小顺序,LinkedHashSet:按照添加的顺序,HashSet:无序

12.4.2 Set接口的实现类们

HashSet/LinkedHashSet:

? 如何区别元素的不可重复。依赖于元素的hashCode和equals方法

TreeSet:

? 如何区别元素的不可重复以及如何保证元素的大小顺序?

? 要么元素实现java.lang.Comparable接口,重写int compareTo(T t)方法,

? 要么指定java.util.Comparator接口的实现类对象,重写int compare(T t1, T t2)方法

? 如果希望保持一致性,在重写compareTo时,一般也会重写equals方法。不是语法要求,而且逻辑意义问题。

12.5 Map系列

12.5.1 Map系列的集合的特点

1、存储键值对(key,value),也称为映射关系,键值对是Map.Entry接口的实现类对象。

2、所有存储到Map中的key不能重复

? 提示:存储到Map中的key最后是不可变,不要修改key。

3、所有存储到Map中的value可以重复

12.5.2 Map接口的API

1、添加

put(K key, V value):两个形参,一对键值对,同一个key如果put两次,第二次会覆盖上次的value

putAll(Map m):将另一个map中的所有键值对添加到当前map中

2、删除

void clear():清空

V remove(Object key):根据key删除一整对键值对(key,value)

3、查询

int size():键值对的数量

boolean containsKey(Object key):是否包含某个key

boolean containsValue(Object value):是否包含某个value

V get(Object key):根据key获取value值

4、遍历

(1)遍历所有的键值对: Set<Entry<K,V>> entrySet()

(2)遍历所有的key:Set keySet()

(3)遍历所有的value:Collection values()

12.5.3 Map的实现类们

1、Hashtable:哈希表

2、HashMap:哈希表

3、TreeMap:按照key的大小顺序排序

? key类型实现了Comparable接口,重写int compareTo方法

? 或者在创建TreeMap时,指定Comparator接口的实现类对象,重写int compare()方法

4、LinkedHashMap:它是HashMap的子类,记录(key,value)添加的顺序

5、Properties:是Hashtable子类,它的key,value的类型是String类型。添加时用setProperty(),获取时用getProperty()

面试题:HashMap与Hashtable的区别:

Hashtable旧,线程安全的,不允许key和value为null。

HashMap相对新,线程不安全的,允许key与value为null。HashMap在并发的情况下可能发生死循环。

ConcurrentHashMap可以解决HashMap的并发问题,比Hashtable效率高一点。

12.5.4 Map与Set的关系

1、HashSet:内部维护了一个HashMap,

2、TreeSet:内部类维护了一个TreeMap

3、LinkedHashSet,内部维护了一个LinkedHashMap

添加到Set中的元素作为底层Map的key,value选择一个Object类型的常量对象RRESENT。

12.6 集合框架集

技术分享图片

12.7 集合工具类Collections

参考操作数组的工具类:Arrays。

Collections 是一个操作 Set、List 和 Map 等集合的工具类。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法:

  • public static boolean addAll(Collection<? super T> c,T... elements)将所有指定元素添加到指定 collection 中。
  • public static int binarySearch(List<? extends Comparable<? super T>> list,T key)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且必须是可比较大小的,即支持自然排序的。而且集合也事先必须是有序的,否则结果不确定。
  • public static int binarySearch(List<? extends T> list,T key,Comparator<? super T> c)在List集合中查找某个元素的下标,但是List的元素必须是T或T的子类对象,而且集合也事先必须是按照c比较器规则进行排序过的,否则结果不确定。
  • public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)在coll集合中找出最大的元素,集合中的对象必须是T或T的子类对象,而且支持自然排序
  • public static T max(Collection<? extends T> coll,Comparator<? super T> comp)在coll集合中找出最大的元素,集合中的对象必须是T或T的子类对象,按照比较器comp找出最大者
  • public static void reverse(List<?> list)反转指定列表List中元素的顺序。
  • public static void shuffle(List<?> list) List 集合元素进行随机排序,类似洗牌
  • public static <T extends Comparable<? super T>> void sort(List list)根据元素的自然顺序对指定 List 集合元素按升序排序
  • public static void sort(List list,Comparator<? super T> c)根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
  • public static void swap(List<?> list,int i,int j)将指定 list 集合中的 i 处元素和 j 处元素进行交换
  • public static int frequency(Collection<?> c,Object o)返回指定集合中指定元素的出现次数
  • public static void copy(List<? super T> dest,List<? extends T> src)将src中的内容复制到dest中
  • public static boolean replaceAll(List list,T oldVal,T newVal):使用新值替换 List 对象的所有旧值
  • Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
  • Collections类中提供了多个unmodifiableXxx()方法,该方法返回指定 Xxx的不可修改的视图。

12.8 迭代器与modCount

当使用foreach或Iterator迭代器遍历集合时,同时调用迭代器自身以外的方法修改了集合的结构,例如调用集合的add和remove方法时,就会报ConcurrentModificationException。

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class TestForeach {
	public static void main(String[] args) {
		Collection<String> list = new ArrayList<>();
		list.add("hello");
		list.add("java");
		list.add("atguigu");
		list.add("world");
		
		Iterator<String> iterator = list.iterator();
		while(iterator.hasNext()){
			list.remove(iterator.next());
		}
	}
}

在集合的add,remove等方法中会有modCount的值的修改。

如果在Iterator、ListIterator迭代器创建后的任意时间从结构上修改了集合(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。

这样设计是因为,迭代器代表集合中某个元素的位置,内部会存储某些能够代表该位置的信息。当集合发生改变时,该信息的含义可能会发生变化,这时操作迭代器就可能会造成不可预料的事情。因此,果断抛异常阻止,是最好的方法。这就是Iterator迭代器的快速失败(fail-fast)机制。

注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测 bug。例如:

	@Test
	public void test02() {
		ArrayList<String> list = new ArrayList<>();
		list.add("hello");
		list.add("java");
		list.add("atguigu");
		list.add("world");
		
        //以下代码没有发生ConcurrentModificationException异常
		Iterator<String> iterator = list.iterator();    
        
		while(iterator.hasNext()){
			String str = iterator.next();
			
			if("atguigu".equals(str)){
				list.remove(str);
			}
		}
	}

那么如何实现快速失败(fail-fast)机制的呢?

  • 在ArrayList等集合类中都有一个modCount变量。它用来记录集合的结构被修改的次数。
  • 当我们给集合添加和删除操作时,会导致modCount++。
  • 然后当我们用Iterator迭代器遍历集合时,创建集合迭代器的对象时,用一个变量记录当前集合的modCount。例如:int expectedModCount = modCount;,并且在迭代器每次next()迭代元素时,都要检查 expectedModCount != modCount,如果不相等了,那么说明你调用了Iterator迭代器以外的Collection的add,remove等方法,修改了集合的结构,使得modCount++,值变了,就会抛出ConcurrentModificationException。

下面以AbstractList和ArrayList.Itr迭代器为例进行源码分析:

AbstractList类中声明了modCount变量:

    /**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

modCount是这个list被结构性修改的次数。结构性修改是指:改变list的size大小,或者,以其他方式改变他导致正在进行迭代时出现错误的结果。

这个字段用于迭代器和列表迭代器的实现类中,由迭代器和列表迭代器方法返回。如果这个值被意外改变,这个迭代器将会抛出 ConcurrentModificationException的异常来响应:next,remove,previous,set,add 这些操作。在迭代过程中,他提供了fail-fast行为而不是不确定行为来处理并发修改。

子类使用这个字段是可选的,如果子类希望提供fail-fast迭代器,它仅仅需要在add(int, E),remove(int)方法(或者它重写的其他任何会结构性修改这个列表的方法)中添加这个字段。调用一次add(int,E)或者remove(int)方法时必须且仅仅给这个字段加1,否则迭代器会抛出伪装的ConcurrentModificationExceptions错误。如果一个实现类不希望提供fail-fast迭代器,则可以忽略这个字段。

Arraylist的Itr迭代器:

   private class Itr implements Iterator<E> {
        int cursor;      
        int lastRet = -1; 
        int expectedModCount = modCount;//在创建迭代器时,expectedModCount初始化为当前集合的modCount的值

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();//校验expectedModCount与modCount是否相等
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
       	final void checkForComodification() {
            if (modCount != expectedModCount)//校验expectedModCount与modCount是否相等
                throw new ConcurrentModificationException();//不相等,抛异常
        }
}

第13章 数据结构与算法

13.1 数据结构的概述

数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。

逻辑结构:

(1)集(数学中的集的概念):同属于一个集体

(2)线性结构:一对一

(3)树型结构:一对多

(3)图形结构:多对多

物理结构:

(1)顺序结构:连续存储空间的数组

(2)链式结构:链表、树、图

(3)索引结构:

(4)哈希结构:

在数据结构基础之上的操作:

(1)增

(2)删

(3)改

(4)查

(5)排序

(6)遍历

....

13.2 动态数组

13.2.1 JDK中的常用动态数组:ArrayList和Vector

面试题:ArrayList与Vector的区别

ArrayList:新一点, 线程不安全,扩容的机制默认为原来的1.5倍

Vector:最古老的动态数组,线程安全的,扩容的机制默认为原来的2倍,还多支持了一个旧版的迭代器Enumeration迭代。

13.2.2 自定义动态数组

动态数组的实现:

package com.atguigu.test07;

import java.util.Arrays;
import java.util.Iterator;

/*
MyArrayList是我们自己定义的集合类型,用来存储对象的容器

问题1:这个容器用来存储什么类型的对象?未知的 ==》
    思路1:Object类型,不方便,没有类型检查,需要类型转换
    思路2:泛型

问题2:容器中,把对象真正存储哪里?数组

问题3:这个数组初始化长度为多少? 例如:5个

问题4:如何表示实际存储多少个元素?用total

问题5:扩容的机制? 例如:1.5倍
 */
public class MyArrayList<T> implements Iterable<T>{
    private Object[] arr = new Object[5];
    private int total;//实际存储元素的个数,默认是0

    public void add(T element){
        //考虑扩容
       /* if(total >= arr.length){
            arr = Arrays.copyOf(arr, arr.length + arr.length >> 1);
        }*/
        grow();

        //把元素放进数组,并且total增加
        arr[total++] = element;
    }

    public void add(int index, T element){
        //检查index的合法性,合法范围:[0, total]
        if(index <0 || index>total){
            throw new IndexOutOfBoundsException("index:" + index + ",total=" + total);
        }

        //是否需要扩容
        grow();

        //把[index]以及之后的元素后移,腾出了[index]位置
        /*
        假设total = 5, index = 1 , 有效元素的范围[0,4],要移动的[1][2][3][4],移动个数total-index=4个
         */
        System.arraycopy(arr, index, arr, index+ 1, total - index);

        //把新元素放到[index]位置
        arr[index] = element;

        //元素个数增加
        total++;
    }

    private void grow() {
        if(total >= arr.length){
            arr = Arrays.copyOf(arr, arr.length + arr.length >> 1);
        }
    }

    //查询,获取[index]位置的元素
    public T get(int index){
        //检查[index]的合法性,合法位置[0, total-1]
        checkExistIndex(index);

        //返回index位置的元素
        return (T)arr[index];
    }

    private void checkExistIndex(int index) {
        if(index <0 || index >= total){
            throw new IndexOutOfBoundsException("index:" + index + ",total=" + total);
        }
    }

    //在当前动态数组中,查询obj的下标
    public int indexOf(Object obj){
        if(obj == null){
            for (int i = 0; i < total; i++) {
                if (obj == arr[i]) { //if(arr[i] == null)
                    return i;
                }
            }
        }else {
            //一共total个,看这个total中是否有和obj“相等”
            for (int i = 0; i < total; i++) {
                if (obj.equals(arr[i])) {
                    return i;
                }
            }
        }
        return -1;

/*        for (int i = 0; i < total; i++) {
            if ((obj==null && arr[i] == null) || (obj!=null && obj.equals(arr[i]))) {
                return i;
            }
        }
        return -1;*/
    }

    public int lastIndexOf(Object obj){
        if(obj == null){
            for (int i = total-1; i >=0; i--) {
                if (obj == arr[i]) { //if(arr[i] == null)
                    return i;
                }
            }
        }else {
            //一共total个,看这个total中是否有和obj“相等”
            for (int i = total-1; i >=0; i--) {
                if (obj.equals(arr[i])) {
                    return i;
                }
            }
        }
        return -1;
    }

    public boolean contains(Object obj){
        return indexOf(obj) != -1;
    }

    //删除第一个匹配的
    public void remove(Object obj){
        //先找到obj的index位置
        int index = indexOf(obj);

        if(index != -1){//存在obj
            remove(index);
        }
    }

    public void remove(int index){
        //先判断index的合法性:[0, total-1]
        checkExistIndex(index);

        //把[index]及其后面的元素向前移动
            /*
            假设:total=5, index=1,要移动[2][3][4]三个元素,total-index-1
             */
        System.arraycopy(arr, index+1, arr, index, total-index-1);

//            //把[total-1]位置置为null
//            arr[total-1] = null; //让GC尽早的回收该位置的内存
//
//            //元素个数减少
//            total--;

        arr[--total] = null;
    }

    public void set(int index , T element){
        //检查index, [0,total-1]
        checkExistIndex(index);

        //替换[index]位置的元素
        arr[index] = element;
    }

    public int size(){
        return total;
    }

    public boolean isEmpty(){
        return total == 0;
    }

    public void clear(){
        //遍历[0,total]范围,置为null,让GC可以及时回收内存
        /*for (int i = 0; i < total; i++) {
            arr[i] = null;
        }
        total=0;*/

        arr = new Object[5];
        total=0;
    }

    @Override
    public Iterator<T> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<T>{
        private int cursor;//游标,默认是0

        @Override
        public boolean hasNext() {
            //合法的元素下标范围[0, total-1]
            return cursor < total;
        }

        @Override
        public T next() {
            return (T)arr[cursor++];
        }
    }
}


13.3 链式结构

经典代表有两类:

1、链表:单链表、双链表、循环单链表

2、二叉树:满二叉树、完全二叉树、平衡二叉树。。。

13.3.1 单链表

1、结点类型

class Node<E>{
	E data;
	Node<E> next;
}

2、单链表的实现

package com.atguigu.test10;

import java.util.Iterator;

/*
OneWayLinkedList:自定义容器,单链表

问题1:
    底层的数据存储的方式:数据必须封装到“结点”中

问题2:单链表的结点

class Node{
    Object data;  //存储数据本身
    Node next; //next是一个变量,该变量存储某个对象地址,这个对象的类型是Node类型。记录的是下一个Node的地址
}

问题3:
    如何用结点表示单链表呢?
    需要记录头结点,根据头结点,可以找到后面的其他结点

问题4:数据的类型未知,可以用Object,或用泛型

问题5:有效元素个数怎么表示? total
 */
public class OneWayLinkedList<T> implements Iterable<T>{
    private Node head;//头结点  当新创建OneWayLinkedList单链表时,没有任何结点head也是null
    private int total;//记录有效元素个数,其实就是结点的个数

    public void add(T data){
        //数据存储到某个结点中
        Node newNode = new Node(data, null);//新结点的下一个结点是没有null

        //把新结点和原来的结点链接起来
        //(1)单链表是空的,如何判断?head==null
        if(head == null){
            head = newNode;//新结点就是头结点
        }else{
            //(2)找到最后一个结点
            Node node = head;
            while(node.next != null){
                node = node.next;
            }
            //出循环,node.next==null成立  node就是最后一个结点
            //让现在的最后一个结点的next指向新结点,即新结点称为最后一个结点
            node.next = newNode;
        }

        //结点个数增加
        total++;
    }

    public int size(){
        return total;
    }

    //根据结点的数据项中的data匹配是否要删除该结点
    public void remove(Object data){
        //单链表是空的,就不删除了,直接返回
        if(head == null){
            return;
        }

        if(data == null){
            //单链表非空,但是删除的是head结点
            if (data == head.data) {
                Node node = head;//原来的head
                head = head.next;

                node.data = null;
                node.next = null;
                total--;
            }else{
                /*(1)找到被删除结点的上一个prevNode 和被删除结点node
                    (2)prevNode.next = node.next
                    (3)node.data=null node.next=null*/
                Node node = head;
                Node prevNode = null;//head没有前一个结点
                while(node.data != null){
                    prevNode = node;
                    node = node.next;
                    if(node == null){
                        break;
                    }
                }

               // node==null//从break出来,没找到
                 if(node!=null){//找到了被删除结点
                     prevNode.next = node.next;

                     node.data = null;
                     node.next = null;
                     total--;
                }
            }
        }else {
            //单链表非空,但是删除的是head结点
            if (data.equals(head.data)) {
                Node node = head;//原来的head
                head = head.next;

                node.data = null;
                node.next = null;
                total--;
            }else{
                /*(1)找到被删除结点的上一个prevNode 和被删除结点node
                    (2)prevNode.next = node.next
                    (3)node.data=null node.next=null*/
                Node node = head;
                Node prevNode = null;//head没有前一个结点
                while(!data.equals(node.data)){
                    prevNode = node;
                    node = node.next;
                    if(node == null){
                        break;
                    }
                }

                // node==null//从break出来,没找到
                if(node!=null){//找到了被删除结点
                    prevNode.next = node.next;

                    node.data = null;
                    node.next = null;
                    total--;
                }
            }
        }

    }

    @Override
    public Iterator<T> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<T>{
        private Node cursor = head;

        @Override
        public boolean hasNext() {
            return cursor != null;
        }

        @Override
        public T next() {
            T data = (T) cursor.data;
            cursor = cursor.next;
            return data;
        }
    }

    /*
    定义为内部类:
    (1)这个Node在别的地方没用
    (2)对于外部使用者来说,是不需要了解我们的内部实现结构
    所以用内部类来表示Node
     */
    private class Node{
        Object data;
        Node next;

        Node(Object data, Node next) {
            this.data = data;
            this.next = next;
        }
    }
}


13.3.2 双链表:LinkedList

1、结点类型

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;
        }
    }

2、双链表实现

int size = 0;
Node<E> first;//记录第一个结点的位置
Node<E> last;//记录最后一个结点的位置

    public boolean add(E e) {
        linkLast(e);//默认把新元素链接到链表尾部
        return true;
    }
    void linkLast(E e) {
        final Node<E> l = last;//用l 记录原来的最后一个结点
        
        //创建新结点
        final Node<E> newNode = new Node<>(l, e, null);
        //现在的新结点是最后一个结点了
        last = newNode;
        
        //如果l==null,说明原来的链表是空的
        if (l == null)
            //那么新结点同时也是第一个结点
            first = newNode;
        else
            //否则把新结点链接到原来的最后一个结点的next中
            l.next = newNode;
        //元素个数增加
        size++;
        //修改次数增加
        modCount++;
    }

    public boolean remove(Object o) {
        //分o是否为空两种情况
        if (o == null) {
            //找到o对应的结点x
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);//删除x结点
                    return true;
                }
            }
        } else {
            //找到o对应的结点x
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);//删除x结点
                    return true;
                }
            }
        }
        return false;
    }
    E unlink(Node<E> x) {//x是要被删除的结点
        // assert x != null;
        final E element = x.item;//被删除结点的数据
        final Node<E> next = x.next;//被删除结点的下一个结点
        final Node<E> prev = x.prev;//被删除结点的上一个结点

        //如果被删除结点的前面没有结点,说明被删除结点是第一个结点
        if (prev == null) {
            //那么被删除结点的下一个结点变为第一个结点
            first = next;
        } else {//被删除结点不是第一个结点
            //被删除结点的上一个结点的next指向被删除结点的下一个结点
            prev.next = next;
            //断开被删除结点与上一个结点的链接
            x.prev = null;//使得GC回收
        }

        //如果被删除结点的后面没有结点,说明被删除结点是最后一个结点
        if (next == null) {
            //那么被删除结点的上一个结点变为最后一个结点
            last = prev;
        } else {//被删除结点不是最后一个结点
            //被删除结点的下一个结点的prev执行被删除结点的上一个结点
            next.prev = prev;
            //断开被删除结点与下一个结点的连接
            x.next = null;//使得GC回收
        }
		//把被删除结点的数据也置空,使得GC回收
        x.item = null;
        //元素个数减少
        size--;
        //修改次数增加
        modCount++;
        //返回被删除结点的数据
        return element;
    }

13.3.3 链表与动态数组的区别

动态数组底层的物理结构是数组,因此根据索引访问的效率非常高,但是根据索引的插入和删除效率不高,因为涉及到移动元素,而且添加操作时可能涉及到扩容问题,那么就会增加时空消耗。

链表底层的物理结构是链表,因此根据索引访问的效率不高,但是插入和删除的效率高,因为不需要移动元素,只需要修改前后元素的指向关系即可,而链表的添加不会涉及到扩容问题。

13.3.4 二叉树

二叉树实现基本结构

class Node{
	Node parent;
	Node left;
	Object data;
	Node right;
	public Node(Node parent,Node left, Object data, Node right) {
		this.parent = parent;
		this.left = left;
		this.data = data;
		this.right = right;
	}
}

二叉树

public class BinaryTree<E>{
    private Node<E> root;
    private int total;
    
    private static class Node<E>{
        Node<E> parent;
        Node<E> left;
        E data;
        Node<E> right;
        
        public Node(Node<E> parent, Node<E> left, E data, Node<E> right) {
            this.parent = parent;
            this.left = left;
            this.data = data;
            this.right = right;
        }
	}
}

二叉树分类

  • 满二叉树: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点的二叉树。 第n层的结点数是2的n-1次方,2的n次方-1

    技术分享图片

  • 完全二叉树: 叶结点只能出现在最底层的两层,且最底层叶结点均处于次底层叶结点的左侧。

    技术分享图片

  • 平衡二叉树:平衡二叉树(Self-balancing binary search tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树, 但不要求非叶节点都有两个子结点 。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci(斐波那契)数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。

例如:斐波那契数列(Fibonacci):1,1,2,3,5,8,13.....

规律:除了第一个和第二个数以外,后面的数等于前两个数之和,

? f(0) =1,

? f(1) = 1,

? f(2) = f(0) + f(1) =2,

? f(3) = f(1) + f(2) = 3,

? f(4) = f(2) + f(3) = 5

? ...

? f(n) = f(n-2) + f(n-1);

技术分享图片

二叉树的遍历

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

技术分享图片

前序遍历:ABDHIECFG

中序遍历:HDIBEAFCG

后序遍历:HIDEBFGCA

13.4 栈和队列

13.4.1 栈

特点:先进后出或后进先出

在JDK中的代表:Stack和LinkedList

如何体现栈的特点?依据方法:

(1)E peek():只看栈顶元素不取走

(2)E pop():取走栈顶元素

(3)void push(E e):把元素添加到栈顶

13.4.2 队列

特点:先进先出

在JDK中的代表:LinkedList

队列的接口:Queue 和 双端队列(Deque)

如何体现队列的特点?依据方法:

Queue除了基本的 Collection操作外,队列还提供其他的插入、提取和检查操作。每个方法都存在两种形式:一种抛出异常(操作失败时),另一种返回一个特殊值(nullfalse,具体取决于操作)。Queue 实现通常不允许插入 元素,尽管某些实现(如 )并不禁止插入 。即使在允许 null 的实现中,也不应该将 插入到 中,因为 也用作 方法的一个特殊返回值,表明队列不包含元素。

抛出异常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
检查 element() peek()

Deque,名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。此接口定义在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(nullfalse,具体取决于操作)。

第一个元素(头部) 最后一个元素(尾部)
抛出异常 特殊值 抛出异常 特殊值
插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e)
移除 removeFirst() pollFirst() removeLast() pollLast()
检查 getFirst() peekFirst() getLast() peekLast()

13.5 哈希表

13.5.1 代表

1、JDK中的代表:Hashtable和HashMap

2、它们的区别

(1)Hashtable:旧,线程安全,不支持key和value为null值

(2)HashMap:新,线程不安全,支持key和value为null值

13.5.2 HashMap

1、JDK1.7实现

数组+链表

(1)几个关键的常量和变量值的作用:

初始化容量:

int DEFAULT_INITIAL_CAPACITY = 1 << 4;//16

①默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

②阈值:扩容的临界值

int threshold;

threshold = table.length * loadFactor;

③负载因子

final float loadFactor;

负载因子的值大小有什么关系?

如果太大,threshold就会很大,那么如果冲突比较严重的话,就会导致table[index]下面的结点个数很多,影响效率。

如果太小,threshold就会很小,那么数组扩容的频率就会提高,数组的使用率也会降低,那么会造成空间的浪费。

    public HashMap() {
    	//DEFAULT_INITIAL_CAPACITY:默认初始容量16
    	//DEFAULT_LOAD_FACTOR:默认加载因子0.75
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor) {
        //校验initialCapacity合法性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
        //校验initialCapacity合法性                                       initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //校验loadFactor合法性
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
		//加载因子,初始化为0.75
        this.loadFactor = loadFactor;
        // threshold 初始为初始容量                                  
        threshold = initialCapacity;
        init();
    }

public V put(K key, V value) {
        //如果table数组是空的,那么先创建数组
        if (table == EMPTY_TABLE) {
            //threshold一开始是初始容量的值
            inflateTable(threshold);
        }
        //如果key是null,单独处理
        if (key == null)
            return putForNullKey(value);
        
        //对key的hashCode进行干扰,算出一个hash值
        int hash = hash(key);
        
        //计算新的映射关系应该存到table[i]位置,
        //i = hash & table.length-1,可以保证i在[0,table.length-1]范围内
        int i = indexFor(hash, table.length);
        
        //检查table[i]下面有没有key与我新的映射关系的key重复,如果重复替换value
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //添加新的映射关系
        addEntry(hash, key, value, i);
        return null;
    }
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);//容量是等于toSize值的最接近的2的n次方
		//计算阈值 = 容量 * 加载因子
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //创建Entry[]数组,长度为capacity
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }
	//如果key是null,直接存入[0]的位置
    private V putForNullKey(V value) {
        //判断是否有重复的key,如果有重复的,就替换value
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        //把新的映射关系存入[0]的位置,而且key的hash值用0表示
        addEntry(0, null, value, 0);
        return null;
    }
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //判断是否需要库容
        //扩容:(1)size达到阈值(2)table[i]正好非空
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //table扩容为原来的2倍,并且扩容后,会重新调整所有映射关系的存储位置
            resize(2 * table.length);
            //新的映射关系的hash和index也会重新计算
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
		//存入table中
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        //原来table[i]下面的映射关系作为新的映射关系next
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;//个数增加
    }

步骤:

(1)在new HashMap():内部的table数组是一个空数组,长度为0

(2)put过程

A:检查table是否为空数组,如果是空数组,创建一个长度为16的Map.Entry类型的数组

B:检查key是否为null,如果为null,hash是0,[index]也是[0]。

? 检查table[0]下面是否有已经存在的(key,value)的key也为null,如果是,就替换value。如果不存在,添加到table[0]下面作为链表的头,在添加之前还要判断是否需要扩容:①size是否达到threshold阈值②table[0]是否不为null,如果两个条件同时满足就扩容,扩容时,会重新计算所有(key,value)的[index]。

C:用key的hashCode值调用hash()函数算出一个经过处理的hash值

D:用hash值与table.length-1做&运算,计算[index]

E:检查table[index]下面是否有重复的key,如果有,替换value。如果不存在,添加到table[index]下面作为链表头。在添加之前还要判断是否需要扩容:①size是否达到threshold阈值②table[0]是否不为null,如果两个条件同时满足就扩容,扩容时,会重新计算所有(key,value)的[index]

2、JDK1.8实现

数组+链表/红黑树

几个常量和变量:
(1)DEFAULT_INITIAL_CAPACITY:默认的初始容量 16
(2)MAXIMUM_CAPACITY:最大容量  1 << 30
(3)DEFAULT_LOAD_FACTOR:默认加载因子 0.75
(4)TREEIFY_THRESHOLD:默认树化阈值8,当链表的长度达到这个值后,要考虑树化
(5)UNTREEIFY_THRESHOLD:默认反树化阈值6,当树中的结点的个数达到这个阈值后,要考虑变为链表
(6)MIN_TREEIFY_CAPACITY:最小树化容量64
		当单个的链表的结点个数达到8,并且table的长度达到64,才会树化。
		当单个的链表的结点个数达到8,但是table的长度未达到64,会先扩容
(7)Node<K,V>[] table:数组
(8)size:记录有效映射关系的对数,也是Entry对象的个数
(9)int threshold:阈值,当size达到阈值时,考虑扩容
(10)double loadFactor:加载因子,影响扩容的频率

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
        // all other fields defaulted,其他字段都是默认值
    }

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
	//目的:干扰hashCode值
    static final int hash(Object key) {
        int h;
		//如果key是null,hash是0
		//如果key非null,用key的hashCode值 与 key的hashCode值高16进行异或
		//		即就是用key的hashCode值高16位与低16位进行了异或的干扰运算
		
		/*
		index = hash & table.length-1
		如果用key的原始的hashCode值  与 table.length-1 进行按位与,那么基本上高16没机会用上。
		这样就会增加冲突的概率,为了降低冲突的概率,把高16位加入到hash信息中。
		*/
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; //数组
		Node<K,V> p; //一个结点
		int n, i;//n是数组的长度   i是下标
		
		//tab和table等价
		//如果table是空的
        if ((tab = table) == null || (n = tab.length) == 0){
            n = (tab = resize()).length;
            /*
			tab = resize();
			n = tab.length;*/
			/*
			如果table是空的,resize()完成了①创建了一个长度为16的数组②threshold = 12
			n = 16
			*/
        }
		//i = (n - 1) & hash ,下标 = 数组长度-1 & hash
		//p = tab[i] 第1个结点
		//if(p==null) 条件满足的话说明 table[i]还没有元素
		if ((p = tab[i = (n - 1) & hash]) == null){
			//把新的映射关系直接放入table[i]
            tab[i] = newNode(hash, key, value, null);
			//newNode()方法就创建了一个Node类型的新结点,新结点的next是null
        }else {
            Node<K,V> e; 
			K k;
			//p是table[i]中第一个结点
			//if(table[i]的第一个结点与新的映射关系的key重复)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))){
                e = p;//用e记录这个table[i]的第一个结点
			}else if (p instanceof TreeNode){//如果table[i]第一个结点是一个树结点
                //单独处理树结点
                //如果树结点中,有key重复的,就返回那个重复的结点用e接收,即e!=null
                //如果树结点中,没有key重复的,就把新结点放到树中,并且返回null,即e=null
				e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            }else {
				//table[i]的第一个结点不是树结点,也与新的映射关系的key不重复
				//binCount记录了table[i]下面的结点的个数
                for (int binCount = 0; ; ++binCount) {
					//如果p的下一个结点是空的,说明当前的p是最后一个结点
                    if ((e = p.next) == null) {
						//把新的结点连接到table[i]的最后
                        p.next = newNode(hash, key, value, null);
						
						//如果binCount>=8-1,达到7个时
                        if (binCount >= TREEIFY_THRESHOLD - 1){ // -1 for 1st
                            //要么扩容,要么树化
							treeifyBin(tab, hash);
						}
                        break;
                    }
					//如果key重复了,就跳出for循环,此时e结点记录的就是那个key重复的结点
            if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))){
                        break;
					}
                    p = e;//下一次循环,e=p.next,就类似于e=e.next,往链表下移动
                }
            }
			//如果这个e不是null,说明有key重复,就考虑替换原来的value
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null){
                    e.value = value;
				}
                afterNodeAccess(e);//什么也没干
                return oldValue;
            }
        }
        ++modCount;
		
		//元素个数增加
		//size达到阈值
        if (++size > threshold){
            resize();//一旦扩容,重新调整所有映射关系的位置
		}
        afterNodeInsertion(evict);//什么也没干
        return null;
    }	
	
   final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//oldTab原来的table
		//oldCap:原来数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
		
		//oldThr:原来的阈值
        int oldThr = threshold;//最开始threshold是0
		
		//newCap,新容量
		//newThr:新阈值
        int newCap, newThr = 0;
        if (oldCap > 0) {//说明原来不是空数组
            if (oldCap >= MAXIMUM_CAPACITY) {//是否达到数组最大限制
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY){
				//newCap = 旧的容量*2 ,新容量<最大数组容量限制
				//新容量:32,64,...
				//oldCap >= 初始容量16
				//新阈值重新算 = 24,48 ....
                newThr = oldThr << 1; // double threshold
			}
        }else if (oldThr > 0){ // initial capacity was placed in threshold
            newCap = oldThr;
        }else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//新容量是默认初始化容量16
			//新阈值= 默认的加载因子 * 默认的初始化容量 = 0.75*16 = 12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;//阈值赋值为新阈值12,24.。。。
		
		//创建了一个新数组,长度为newCap,16,32,64.。。
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
		
		
        if (oldTab != null) {//原来不是空数组
			//把原来的table中映射关系,倒腾到新的table中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {//e是table下面的结点
                    oldTab[j] = null;//把旧的table[j]位置清空
                    if (e.next == null)//如果是最后一个结点
                        newTab[e.hash & (newCap - 1)] = e;//重新计算e的在新table中的存储位置,然后放入
                    else if (e instanceof TreeNode)//如果e是树结点
						//把原来的树拆解,放到新的table
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
						/*
						把原来table[i]下面的整个链表,重新挪到了新的table中
						*/
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }	
	
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
		//创建一个新结点
	   return new Node<>(hash, key, value, next);
    }

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; 
		Node<K,V> e;
		//MIN_TREEIFY_CAPACITY:最小树化容量64
		//如果table是空的,或者  table的长度没有达到64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();//先扩容
        else if ((e = tab[index = (n - 1) & hash]) != null) {
			//用e记录table[index]的结点的地址
            TreeNode<K,V> hd = null, tl = null;
			/*
			do...while,把table[index]链表的Node结点变为TreeNode类型的结点
			*/
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;//hd记录根结点
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
			
            //如果table[index]下面不是空
            if ((tab[index] = hd) != null)
                hd.treeify(tab);//将table[index]下面的链表进行树化
        }
    }	

1、添加过程

A. 先计算key的hash值,如果key是null,hash值就是0,如果为null,使用(h = key.hashCode()) ^ (h >>> 16)得到hash值;

B. 如果table是空的,先初始化table数组;

C. 通过hash值计算存储的索引位置index = hash & (table.length-1)

D. 如果table[index]==null,那么直接创建一个Node结点存储到table[index]中即可

E. 如果table[index]!=null

? a) 判断table[index]的根结点的key是否与新的key“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),如果是那么用e记录这个根结点

? b) 如果table[index]的根结点的key与新的key“不相同”,而且table[index]是一个TreeNode结点,说明table[index]下是一棵红黑树,如果该树的某个结点的key与新的key“相同”(hash值相同并且(满足key的地址相同或key的equals返回true)),那么用e记录这个相同的结点,否则将(key,value)封装为一个TreeNode结点,连接到红黑树中

? c) 如果table[index]的根结点的key与新的key“不相同”,并且table[index]不是一个TreeNode结点,说明table[index]下是一个链表,如果该链表中的某个结点的key与新的key“相同”,那么用e记录这个相同的结点,否则将新的映射关系封装为一个Node结点直接链接到链表尾部,并且判断table[index]下结点个数达到TREEIFY_THRESHOLD(8)个,如果table[index]下结点个数已经达到,那么再判断table.length是否达到MIN_TREEIFY_CAPACITY(64),如果没达到,那么先扩容,扩容会导致所有元素重新计算index,并调整位置,如果table[index]下结点个数已经达到TREEIFY_THRESHOLD(8)个并table.length也已经达到MIN_TREEIFY_CAPACITY(64),那么会将该链表转成一棵自平衡的红黑树。

F. 如果在table[index]下找到了新的key“相同”的结点,即e不为空,那么用新的value替换原来的value,并返回旧的value,结束put方法

G. 如果新增结点而不是替换,那么size++,并且还要重新判断size是否达到threshold阈值,如果达到,还要扩容。

第14章 File与IO流

14.1 java.io.File类

14.1.1 File类概述

File类的对象用来表示文件和文件夹的对象。

通过指定文件或文件夹(目录)的路径名来表示。

如果这个路径名对应的文件或目录不存在,那么在堆中的File对象的成员变量就是默认值,除了name和path是根据构造器指定的来赋值。。

如果这个路径名对应的文件或目录存在,那么在堆中的File对象就根据这个路径找到对应的文件或目录,然后将一些信息获取到为File对象的成员变量赋值,例如:length(),修改时间等。

14.1.2 File类API

序号 方法签名 方法功能
1 File(String pathName) 根据文件、目录的路径名构建File对象
2 public File(String parent, String child) 根据文件、目录的路径名构建File对象,路径名通过指定父目录与子目录方式来指定
3 String getName() 获取文件名
4 long length() 获取文件大小,无法直接获取目录大小
5 String getPath() 获取构造路径
6 String getAbsolutePath() 获取绝对路径
7 String getCanonicalPath() 获取规范路径
8 long lastModified() 获取最后修改时间
9 boolean isFile() 判断是否是文件
10 boolean isDirectory() 判断是否是目录
11 boolean exists() 判断是否存在
12 boolean isHidden() 判断是否隐藏
13 createNewFile() 创建文件
14 mkdir()或mkdirs() 创建目录
15 delete() 删除文件或空目录
16 renameTo(File dest) 重命名文件或目录
17 String[] list() 获取目录里面的文件或子目录
18 File[] listFiles() 获取目录里面的文件或子目录
19 File[] listFiles(FileFilter filter) 根据过滤条件获取目录里面的文件或子目录

14.2 IO流

1、分类

1、按照方向分:输入流和输出流

2、按照数据处理方式分:字节流和字符流

字节流:适用于所有类型的数据

字符流:只适用于纯文本数据(字符串)

3、按照IO流角色不同:节点流和处理流

处理流在其他节点流或其他处理流的基础上增加辅助功能,例如:缓冲,转码等功能

2、四个基类

InputStream:字节输入流

OutputStream:字节输出流

Reader:字符输入流

Writer:字符输出流

3、四个基类的API

1、InputStream:字节输入流

  • int read():读一个字节,如果流中没有数据了,返回-1。
  • int read(byte[] data):一次读取多个字节,最多读取data.length个,把读取的数据放到data中,从data[0]开始存储,如果流中没有data.length个,那么有几个读取几个,返回实际读取的字节的个数。如果流中没有数据了,返回-1。
  • int read(byte[] data, int offset, int count):一次读取多个字节,最多读取count个,把读取的数据放到data中,从data[offset]开始存储,如果流中没有count个,那么有几个读取几个,返回实际读取的字节的个数。如果流中没有数据了,返回-1。
  • void close()

2、OutputStream:字节输出流

(1)void write(int b):输出一个字节

(2)void write(byte[] data):输出一个字节数组的全部

(3)void write(byte[] data, int offset, int len):输出一个字节数组的部分

(4)void flush():刷新

(5)void close():关闭

3、Reader:字符输入流

(1)int read():读取一个字符,如果已经到达流末尾,没有数据可读了,返回-1.

(2)int read(char[] data):读取多个字符到data数组中,从data[0]开始存储,最多读取data.length个字符。返回的是实际读取的字符数。如果已经到达流末尾,没有数据可读了,返回-1.

(3)int read(char[] data, int offset, int len):读取多个字符到data数组中,从data[offset]开始存储,最多读取len个字符。返回的是实际读取的字符数。如果已经到达流末尾,没有数据可读了,返回-1.

(4)void close();关闭IO流

4、Writer:字符输出流

(1)void write(int b):输出一个字符

(2)void write(char[] data):输出一个字符数组的全部

(3)void write(char[] data, int offset, int len):输出一个字符数组的部分

(4)void flush():刷新

(5)void close():关闭

(6)void write(String str):输出整个字符串

(7)void write(String str ,int offset, int count):输出字符串的部分

4、操作IO流的步骤

(1)创建合适的IO流的对象

(2)读、写

(3)关闭IO流

? 要么只关闭最外层的IO流,要是都关的话,注意顺序,先关外面的再关里面的。

14.3 文件IO流

FileInputStream:文件字节输入流,可以用于读取任意类型的文件

FileOutputStream:文件字节输出流,用于输出数据到任意类型的文件

FileReader:文件字符输入流,只能用于读取纯文本文件,并且只能按照平台默认的字符编码进行文件读取。所以如果文件的编码与程序的编码不一致,会出现乱码。

FileWriter:文件字符输出流,只能把数据输出到纯文本文件中,并且只能按照平台默认的字符编码进行输出。所以如果文件的编码与程序的编码不一致,会出现乱码。

FileInputStream

	@Test
	public void test01() throws IOException {
		//相对路径,相对于当前项目的根目录
		//(1)创建FileInputStream对象
		//可以把FileInputStream看成一个数据的管道,和1.txt文件连接上了
		//编译期间不会检查1.txt这个文件是否存在,运行时,1.txt文件又可能不在,所以报异常
		FileInputStream fis = new FileInputStream("1.txt");
		
		//(2)开始读取
//		System.out.println(fis.read());//读取一个字节
		
		/*
		 * 
		 * 1.txt文件中的数据:FileInputStreamhelloworld
		 * 第一次读取:尝试读取10个字节  FileInputS  
		 * 		data字节数组:FileInputS[70, 105, 108, 101, 73, 110, 112, 117, 116, 83]
		 * 		我们显示时new String(data,0,len) len是10
		 * 第二次读取:尝试读取10个字节  treamhello  
		 * 		data字节数组:treamhello[116, 114, 101, 97, 109, 104, 101, 108, 108, 111]
		 * 		我们显示时new String(data,0,len) len是10
		 * 第三次读取:尝试读取10个字节  world  只读了5个  
		 * 		data字节数组:worldhello[119, 111, 114, 108, 100, 104, 101, 108, 108, 111]
		 * 		我们显示时new String(data,0,len) len是5
		 * 第四次读取:尝试读取10个字节  没有了  读取了-1
		 * 
		 * 文件中的数据:尚硅谷是一家靠谱的培训机构   假设文件是UTF-8格式
		 * 第一次读取:尝试读取10个字节  尚硅谷?  
		 * 		data字节数组:尚硅谷?[-27, -80, -102, -25, -95, -123, -24, -80, -73, -26]
		 * 		我们显示时new String(data,0,len) len是10
		 * 第二次读取:尝试读取10个字节  ??一家?? 
		 * 		data字节数组:??一家??[-104, -81, -28, -72, -128, -27, -82, -74, -23, -99]
		 * 		我们显示时new String(data,0,len) len是10
		 * 第三次读取:尝试读取10个字节  ?谱的培  
		 * 		data字节数组:?谱的培[-96, -24, -80, -79, -25, -102, -124, -27, -97, -71]
		 * 		我们显示时new String(data,0,len) len是10
		 * 第四次读取:尝试读取10个字节  训机构 只读取了9个字节   
		 * 		data字节数组:训机构[-24, -82, -83, -26, -100, -70, -26, -98, -124, -71]
		 * 		我们显示时new String(data,0,len) len是9
		 * 第五次读取:尝试读取10个字节  没有了  读取了-1
		 * 
		 */
		byte[] data = new byte[10];
		while(true) {
			//从fis读取数据,存储到data字节数组中,返回本次实际读取的字节的个数
			//因为fis流中,可能么有data.length个
			int len = fis.read(data);
			if(len==-1) {//fis流没有数据,fis.read方法没有读取到数据
				break;
			}
			//如果读取的数据,想要在控制台打印,可以把字节数组转为字符串显示
			System.out.print(new String(data,0,len));
		}		
		//(3)关闭,即和1.txt文件断开连接
		fis.close();
	}

FileReader

	@Test
	public void test01() throws IOException {
		//相对路径,相对于当前项目的根目录
		//(1)创建FileReader对象
		//可以把FileReader看成一个数据的管道,和1.txt文件连接上了
		//编译期间不会检查1.txt这个文件是否存在,运行时,1.txt文件又可能不在,所以报异常
		FileReader fis = new FileReader("1.txt");
		
		//(2)开始读取
//		System.out.println(fis.read());//读取一个字符
/*		char[] data = new char[2];
		int len = fis.read(data);
		System.out.println("len="+len);
		System.out.println(Arrays.toString(data));*/
		
		char[] data = new char[10];
		while(true) {
			//从fis读取数据,存储到data字节数组中,返回本次实际读取的字节的个数
			//因为fis流中,可能么有data.length个
			int len = fis.read(data);
			if(len==-1) {//fis流没有数据,fis.read方法没有读取到数据
				break;
			}
			//如果读取的数据,想要在控制台打印,可以把字节数组转为字符串显示
			System.out.print(new String(data,0,len));
		}
		
		//(3)关闭,即和1.txt文件断开连接
		fis.close();
	}

FileOutputStream

	@Test
	public void test01() throws IOException {
		//(1)创建FileOutputStream的对象
		//把FileOutputStream看成数据流通道,与1.txt文件连接
		FileOutputStream fw = new FileOutputStream("1.txt");
		
		//(2)写数据
		fw.write("柴林燕是美女".getBytes());//把字符串转为字节数组,然后写出
		
		//(3)关闭
		fw.close();
	}

FileWriter

	@Test
	public void test01() throws IOException {
		//(1)创建FileWriter的对象
		//把FileWriter看成数据流通道,与1.txt文件连接
		FileWriter fw = new FileWriter("1.txt");
		
		//(2)写数据
		fw.write("柴林燕是美女");
		
		//(3)关闭
		fw.close();
	}
	
	@Test
	public void test02() throws IOException {
		//(1)创建FileWriter的对象
		//把FileWriter看成数据流通道,与1.txt文件连接
		FileWriter fw = new FileWriter("1.txt",true);//true表示已追加内容的方式写入文件
		
		//(2)写数据
		fw.write("柴林燕是超级大美女");
		
		//(3)关闭
		fw.close();
	}

文件复制

public class FileTools {
	public static void main(String[] args) throws IOException {
		copy("1.txt", "2.txt");
	}
	/*
	 * 从srcPathName复制文件内容到destPathName文件中
	 */
	public static void copy(String srcPathName, String destPathName) throws IOException{
		//(1)创建IO流
		//FileInputStream和FileOutputStream,这些选择字节流的原因,是因为copy是要用于复制所有类型的文件,不仅限于文本文本
		FileInputStream fis = new FileInputStream(srcPathName);
		FileOutputStream fos = new FileOutputStream(destPathName);
		
		//(2)从fis流中读取数据,写到fos流中
		byte[] data = new byte[10];
	/*	while(true) {
			int len = fis.read(data);
			if(len==-1) {
				break;
			}
			//把本次读取的数据,写到fos中
			fos.write(data, 0, len);//这里要写(0,len),因为最后一次可能没有10个
		}*/
		//换一种写法
		int len;
		while((len=fis.read(data))!=-1) {
			fos.write(data, 0, len);
		}
		
		
		//(3)关闭
		fis.close();
		fos.close();
	}
}

知识点

原文:https://www.cnblogs.com/zhaoyongbin0203/p/13838233.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!