java开发必备基础

Java总复习

java知识总复习

第一章:java概述:

1:Java的历史:

​ Java诞生于SUN(Stanford University Network),09年SUN被Oracle(甲骨文)收购。

​ 1996年Java之父是詹姆斯.高斯林(James Gosling)发布了第一个版本JDK1.0,java诞生!

2:Java语言的特点:

​ 特点一: 面向对象的:基本的概念是:类与对象。

​ 三个特性:继承,封装,多态。

​ 特点二:健壮性:java吸收了C/C++语言的优点(代码少,运行速度快,功能强大),去掉了影响程序健壮性的部分比如,指针,内存的申请和手动的释放,提供了一个相对的内存管理机制(虚拟机的自动内存管理机制)。

​ 特点三:跨品台性:用为有了Java虚拟机的存在,Java的语言可以在任意得平台运行!

3:Java开发环境的搭建:
1): 概念:JDK,JRE,JVM

​ jdk:java的工具包。

​ jre:java的运行环境。

​ jvm:java的虚拟机。

2):各个概念之间的关系:

​ JDK = JRE+开发工具组成。

​ JRE = JVM+核心的类库组成。

3):java环境的搭建:

​ 安装JDK,将jdk的开发工具配置到电脑的开发环境path中,配置在path是因为为了使用jdk中的javac.exe工具。

4:java的第一个程序:
class HelloWorld{
    public static void main(String[] args){
        System.out.print("Hello Java!");
    }
}

1):java的开发步骤:一共就3步骤那个的:

​ 第一步:编写源代码的文件,要求必须是 .java的文件。

​ 第二步:把源文件编译为字节码的文件.class,因为虚拟机只认识字节码,使用编译工具就是javac.exe工具。

​ 第三步运行:使用的工具是 java.exe。

2):java程序的结构:
{
   			 	方法{
      			  语句;
   			 }
		}
3):java的入口方法:
		public static void main(String[] args){
    
		}
4):java的注释:

​ 单行注释:// 。 多行注释:// 文档注释/*/

5:java编程是的注意事项:
1):字符编码问题

​ 当cmd命令行窗口的字符编码与.java源文件的字符编码不一致,如何解决?

​ 解决方案一:

​ 在Notepad++等编辑器中,修改源文件的字符编码。

​ 解决方案二:

​ 在使用javac命令式,可以指定源文件的字符编码。

​ javac -encoding utf-8 Review01.java。

2):大小写问题

​ (1)源文件名:

​ 不区分大小写,我们建议大家还是区分。

​ (2)字节码文件名与类名 区分大小写。

​ (3)代码中 区分大小写。

3):源文件名与类名一致问题?
(1)源文件名是否必须与类名一致?public呢?

​ 如果这个类不是public,那么源文件名可以和类名不一致。

​ 如果这个类是public,那么要求源文件名必须与类名一致。

​ 我们建议大家,不管是否是public,都与源文件名保持一致,而且一个源文件尽量只写一个类,目的是为了好维护。

(2)一个源文件中是否可以有多个类?public呢?

​ 一个源文件中可以有多个类,编译后会生成多个.class字节码文件。

​ 但是一个源文件只能有一个public的类。

(3)main必须在public的类中吗? =====>不是。

第二章:java的语法基础:

1:标识符:

​ 简单的说,凡是程序员自己命名的部分都可以称之为程序的标识符,即给类,方法,变量,包命名的字符序列都可以称之为标识符。

1):标识符的命名规则

​ (1)Java的标识符只能使用26个英文字母大小写,0-9的数字,下划线_,美元符号$

​ (2)不能使用Java的关键字(包含保留字)和特殊值

​ (3)数字不能开头

​ (4)不能包含空格

​ (5)严格区分大小写

2):标识符的命名规范

​ (1)见名知意

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

​ 例如:HelloWorld,String,System等

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

​ 例如:age,name,bookName,main

​ (4)包名等:每一个单词都小写,单词之间使用点.分割,形式:xxx.yyy.zzz,

​ 例如:java.lang

​ (5)常量名等:每一个单词都大写,单词之间使用下划线_分割,形式:XXX_YYY_ZZZ,

​ 例如:MAX_VALUE,PI

2:变量:
1):变量的概念:

​ 用来存储数据,代表内存的一块存储区域,变量中的值是可以改变的。

2):变量的三要素:

​ (1)数据类型

​ (2)变量名

​ (3)值

3):变量使用应该注意的事项:

​ (1)先声明,后使用。

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

​ (3)注意变量的作用域,在同一个作用域不能出现两个相同 的变量名称。

4):变量的声明赋值和使用方式:

​ (1):变量的声明的语法规则:

数据类型  变量名;
例如:
int age;
String name;
double weight;
char gender;
boolean isMarry;

​ (2):变量的赋值的语法格式

变量名 =;
例如:
age = 18;
name = "柴林燕"; //字符串的值必须用""
weight = 44.4;
gender = '女';//单字符的值必须使用''
isMarry = true;

​ (3):变量的使用的语法格式:

通过变量名直接引用

例如:
(1)输出变量的值
System.out.print(name);
System.out.print("姓名:" + name);//""中的内容会原样显示
System.out.print("name = " + name);
(2)计算
age = age + 1;
3:数据类型:
1):基础数据类型的分类:

​ 1、整型系列

​ (1)byte:字节类型:占内存:1个字节。

​ (2)short:短整型类型:占内存:2个字节。

​ (3)int:整型:占内存:4个字节。

​ (4)long:整型:占内存:8个字节。

​ 2、浮点型系列(小数)

​ (1)float:单精度浮点型:占内存:4个字节

​ (2)double:双精度浮点型:占内存:8个字节

​ 3、单字符类型

​ (1)char:字符类型占内存:2个字节

​ 注意:字符的三种表现方式:普通的一个字符 :‘A’。转译字符:\n。十六进制字符:\u5c1a。

​ 4、布尔类型:boolean:只能存储true或false

2):进制:

​ 1、进制的分类:

​ (1)十进制

​ 数字组成:0-9:进位规则:逢十进一

​ (2)二进制

​ 数字组成:0-1进位规则:逢二进一

​ (3)八进制

​ 数字组成:0-7:进位规则:逢八进一

​ (4)十六进制

​ 数字组成:0-9,af(或AF):进位规则:逢十六进一

3):基本数据类型的装换:

​ (1):自动类型转化:

​ 把存储小范围的值,赋值给大范围值的时候。

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

int i = 'A';//char自动升级为int
double d = 10;//int自动升级为double

​ 当存储范围小的数据类型与存储范围大的数据类型一起混合运算时,会按照其中最大的类型运算。

int i = 1;
byte b = 1;
double d = 1.0;
double sum = i + b + d;//混合运算,升级为double

​ 当byte,short,char数据类型进行算术运算时,按照int类型处理。boolean类型不参与计算。

byte b1 = 1;
byte b2 = 2;
byte b3 = (byte)(b1 + b2);//b1 + b2自动升级为int

char c1 = '0';
char c2 = 'A';
System.out.println(c1 + c2);//113 

​ (2):强制类型装换:

​ 当把存储范围大的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围小的变量时,需要强制类型转换。这样转化有风险,可能会损失精度或溢出。

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

double d = 1.2;
int num = (int)d;//损失精度

int i = 200;
byte b = (byte)i;//溢出

​ 当某个值想要提升数据类型时,也可以使用强制类型转换,这个情况的强制类型转换是没有风险的。boolean类型不参与。

int i = 1;
int j = 2;
double shang = (double)i/j;

3):特殊数据类型的转换

​ (1): 任意数据类型的数据与String类型进行“+”运算时,结果一定是String类型。

System.out.println("" + 1 + 2);//12

​ (2)但是String类型不能通过强制类型()转换,转为其他的类型

String str = "123";
int num = (int)str;//错误的

4):引用数据类型

​ String、数组,类,接口

4:运算符:
1):Java基本数据类型的运算符:

​ (1)算术运算符

​ (2)赋值运算符

​ (3)比较运算符

​ (4)逻辑运算符

​ (5)条件运算符

​ (6)位运算符(难)

2):按照操作数个数的分类:

​ (1)一元运算符:操作数只有一个

​ 例如:正号(+),负号(-),自增(++),自减(–),逻辑非(!),按位取反(~)。

​ (2)二元运算符:操作数有两个

​ 例如:加(+),减(-),乘(),除(/),模(%) 大于(>),小于(<),大于等于(>=),小于等于(<=),等于(==),不等于(!=),赋值(=,+=,-=,=,/=,%=,>>=,<<=。。。),逻辑与(&),逻辑或(|),逻辑异或(),短路与(&&),短路或(||),左移(<<),右移(>>),无符号右移(>>>),按位与(&),按位或(|),按位异或(

​ (3)三元运算符:操作数三个

​ 例如: ? :

3):算术运算符

​ 加法:+

​ 减法:-

​ 乘法:*

​ 除法:/

注意:整数与整数相除,只保留整数部分

​ 取模:% 取余

注意:取模结果的正负号只看被模数

​ 正号:+

​ 负号:-

​ 自增:++

​ 自减:–

原则:自增与自减
++/–在前的,就先自增/自减,后取值
++/–在后的,就先取值,后自增/自减
整个表达式的扫描,是从左往右扫描,如果后面的先计算的,那么前面的就暂时先放到“操作数栈”中
int i = 1;
i++;//i=2

int j = 1;
++j;//j=2

int a = 1;
int b = a++;//(1)先取a的值“1”放操作数栈(2)a再自增,a=2(3)再把操作数栈中的"1"赋值给b,b=1

int m = 1;
int n = ++m;//(1)m先自增,m=2(2)再取m的值“2”放操作数栈(3)再把操作数栈中的"2"赋值给n,n=2

int i = 1;
int j = i++ + ++i * i++;
/*
从左往右加载
(1)先算i++
①取i的值“1”放操作数栈
②i再自增 i=2
(2)再算++i
①i先自增 i=3
②再取i的值“3”放操作数栈
(3)再算i++
①取i的值“3”放操作数栈
②i再自增 i=4
(4)先算乘法
用操作数栈中3 * 3 = 9,并把9压会操作数栈
(5)再算求和
用操作数栈中的 1 + 9 = 10
(6)最后算赋值
j = 10
*/

4):赋值运算符
			基本赋值运算符:=。扩展赋值运算符:+=,-=,*=,/=,%=。
注意:所有的赋值运算符的=左边一定是一个变量。
   扩展赋值运算符=右边的计算结果的类型如果比左边的大的话会强制类型转换,所以结果可能有风险。
   扩展赋值运算符的计算:(1)赋值最后算(2)加载数据的顺序是把左边的变量的值先加载,再去与右边的表达式进行计算
int i = 1;
int j = 5;
j *= i++ + j++;//j = j *(i++ + j++);
/*
(1)先加载j的值“5”
(2)在计算i++
①先加载i的值“1”
②再i自增,i=2
(3)再计算j++
①先加载j的值"5"
②再j自增,j=6
(4)算  加法
i + 5 = 6
(5)算乘法
5 * 6 = 30
(6)赋值
j = 30
*/
5):比较运算符

​ 大于:>小于:<大于等于:>=小于等于:<=等于:== 注意区分赋值运算符的=,不等于:!=

注意:比较表达式的运算结果一定只有true/false
比较表达式可以作为(1)条件(2)逻辑运算符的操作数
6):逻辑运算符

逻辑与:&
​ 运算规则:只有左右两边都为true,结果才为true。
​ 例如:true & true 结果为true
​ false & true 结果为false
​ true & false 结果为false
​ false & false 结果为false
逻辑或:|
​ 运算规则:只要左右两边有一个为true,结果就为true。
​ 例如:true | true 结果为true
​ false | true 结果为true
​ true | false 结果为true
​ false | false 结果为false
逻辑异或:^
​ 运算规则:只有左右两边不同,结果才为true。
​ 例如:true ^ true 结果为false
​ alse ^ true 结果为true
​ true ^ false 结果为true
​ false ^ false 结果为false

逻辑非:!
​ 运算规则:布尔值取反
​ 例如:!true 为false
​ !false 为true

短路与:&&
​ 运算规则:只有左右两边都为true,结果才为true。
​ 例如:true & true 结果为true
​ true & false 结果为false
​ false & ? 结果就为false
​ 它和逻辑与不同的是当&&左边为false时,右边就不看了。

短路或:||
​ 运算规则:只要左右两边有一个为true,结果就为true。
​ 例如:true | ? 结果为treu
​ false | true 结果为true
​ false | false 结果为false
​ 它和逻辑或不同的是当||左边为true时,右边就不看了

开发中一般用短路与和短路或比较多
&&当左边为false,右边不计算
&不管左边是true还是false,右边都要计算

7):条件运算符

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

运算规则:

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

1boolean类型
boolean marry = true;
System.out.println(marry? "已婚" : "未婚");2)求最值
int i = 3;
int j = 5;
int max = i>=j ? i : j;
//当i>=j时,max就赋值为i的值,否则就赋值为j的值

8):位运算符

左移:<<

​ 运算规则:左移几位就相当于乘以2的几次方

右移:>>

​ 运算规则:右移几位就相当于除以2的几次方

无符号右移:>>>

​ 运算规则:往右移动后,左边空出来的位直接补0,不看符号位

按位与:&

​ 运算规则:

​ 1 & 1 结果为1

​ 1 & 0 结果为0

​ 0 & 1 结果为0

​ 0 & 0 结果为0

按位或:|

​ 运算规则:

​ 1 | 1 结果为1

​ 1 | 0 结果为1

​ 0 | 1 结果为1

​ 0 & 0 结果为0

按位异或:^

​ 运算规则:

​ 1 ^ 1 结果为0

​ 1 ^ 0 结果为1

​ 0 ^ 1 结果为1

​ 0 ^ 0 结果为0

按位取反:~

​ 运算规则:~0就是1

​ ~1就是0

如何区分&,|,^是逻辑运算符还是位运算符?

如果操作数是boolean类型,就是逻辑运算符,如果操作数是整数,那么就位运算符。
9):运算符操作数类型说明

1、算术运算符

数字和单个字符可以使用算术运算符。

其中+,当用于字符串时,表示拼接。

2、赋值运算符

右边的常量值、表达式的值、变量的值的类型必须与左边的变量一致或兼容(可以实现自动类型转换)或使用强制类型转换可以成功。

3、比较运算符

其他的比较运算符都是只能用于8种基本数据类型。

其中的==和!=可以用于引用数据类型的比较,用于比较对象的地址

int i = 10;
int j = 10;
System.out.println(i==j);//true

char c1 = '帅';
char c2 = '帅';
System.out.println(c1 == c2);//true

4、逻辑运算符

逻辑运算符的操作数必须是boolean值

5、条件运算符

?前面必须是条件,必须是boolean值

结果表达式1和结果表达式2要保持类型一致或兼容

6、位运算符

一般用于整数系列

以上运算符都是针对基本数据类型设计的。
能够用于引用数据类型只有基本的赋值运算符=,和比较运算符中的==和!=。其他运算符都不能用于引用数据类型。
其中字符串类型还有一个+,表示拼接。

第三章:流程控制语句结构

​ 流程控制语句结构分为:

​ 1、顺序结构:从上到下依次执行

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

​ 3、循环结构:重复执行某些代码

1:顺序结构:
1):输出语句:从上到下顺序执行
#输出常量
System.out.print(1);
System.out.print('尚');
System.out.print(44.4);
System.out.print(true);
System.out.print("尚硅谷");

#输出变量
int a = 1;
char c = '尚';
double d = 44.4;
boolean b = true;
String school = "尚硅谷";
System.out.print(a);
System.out.print(c);
System.out.print(d);
System.out.print(b);
System.out.print(school);

#输出拼接结果
System.out.print("a = " + a);
System.out.print("c = " + c);
System.out.print("d = " + d);
System.out.print("b = " + b);
System.out.print("school = " + school);
2):输入语句:键盘输入代码的三个步骤

​ 1、准备Scanner类型的变量

​ 2、提示输入xx

​ 3、接收输入内容

//1、准备Scanner类型的变量
java.util.Scanner input = new java.util.Scanner(System.in);//System.in默认代表键盘输入

//2、提示输入xx
System.out.print("请输入一个整数:");

//3、接收输入内容
int num = input.nextInt();

//列出各种数据类型的输入
int num = input.nextInt();
long bigNum = input.nextLong();
double d = input.nextDouble();
boolean b = input.nextBoolean();
String s = input.next();
char c = input.next().charAt(0);//先按照字符串接收,然后再取字符串的第一个字符(下标为0)
2: 分支结构:

​ 分支结构:根据条件选择性的执行某些代码

1):条件判断:

​ (1):单分支结构

if(条件表达式){
    当条件表达式成立(true)时需要执行的语句块;
}

注意:
(1)if(条件表达式)中的条件表达式的结果必须是boolean类型

(2)当{}中的语句只有一个语句(简单的语句,也可以是一个复合语句)时,可以省略{},但是我们不建议省略
		//省略{}的情况
		if(score<0 || score>100)
			System.out.println("输入有误!");//简单的语句
		else
			//复合语句
			if(score==100){
				System.out.println("满分");
			}else if(score>=80){
				System.out.println("优秀");
			}else if(score>=60){
				System.out.println("及格");
			}else{
				System.out.println("不及格");
			}

int year = 2019;
int days = 28;
if(year%4==0 && year%100!=0 || year%400==0){
    days= 29;
}

​ (2):双分支结构

​ 语法规则:

if(条件表达式){
    当条件表达式成立(true)时需要执行的语句块1;
}else{
    当条件表达式不成立(false)时需要执行的语句块2;
}

执行过程:
 当条件表达式成立(true)时执行语句块1,否则执行语句块2
注意:
(1)if(条件表达式)中的条件表达式的结果必须是boolean类型
(2)当{}中的语句只有一个语句(简单的语句,也可以是一个复合语句)时,可以省略{},但是我们不建议
int num = 10;
if(num%2==0){
    System.out.println(num + "是偶数")}else{
     System.out.println(num + "是奇数")}

​ (3):多分支结构

​ 语法规则:

if(条件表达式1){
    当条件表达式1成立的时候,执行的语句块1;
}else if(条件表达式2){
    当条件表达式1不成立,
      条件表达式2成立的时候,执行的语句块2;
}else if(条件表达式3){
    当条件表达式1不成立,
       条件表达式2也不成立,
      条件表达式3成立的时候,执行的语句块3;
}
。。。
【else{
	当以上所有的条件表达式都不成立,需要执行的语句块n+1;
}】

执行过程:
(1)多个条件顺序往下判断,如果上面有一个条件成立了,下面的条件就不看了
(2)多个分支也只会执行其中的一个
注意:

(1)每一个条件表达式都必须是boolean值
(2)当{}中只有一个语句时,也可以省略{},但不建议省略
(3)当多个条件是“互斥”关系(没有重叠部分),顺序可以随意;
当多个条件是“包含”关系(有重叠部分),顺序不能随意,小的在上,大的在下面
			int score = 78;
			if(score==100){
				System.out.println("满分");
			}else if(score>=80){
				System.out.println("优秀");
			}else if(score>=60){
				System.out.println("及格");
			}else{
				System.out.println("不及格");
			}

2):选择结构:

​ 语法格式:

switch(表达式){
    case 常量值1:
        语句块1;
        【break;】
    case 常量值2:
        语句块2;
        【break;】   
    。。。
   【default:
        语句块n+1;
        【break;】
     】
}
执行过程:
(1)入口
①当switch(表达式)的值与case后面的某个常量值匹配,就从这个case进入;
②当switch(表达式)的值与case后面的所有常量值都不匹配,寻找default分支进入;
(2)一旦从“入口”进入switch,就会顺序往下执行,直到遇到“出口”
(3)出口
①自然出口:遇到了switch的结束}
②中断出口:遇到了break等

注意:
(1)switch(表达式)的值的类型,只能是:4种基本数据类型(byte,short,int,char),两种引用数据类型(枚举、String)
(2)case后面必须是常量值,而且不能重复
int month = 4;
switch(month){
    case 3:
    case 4:
    case 5:
        System.out.println("春季");
        break;
    case 6:
    case 7:
    case 8:
        System.out.println("夏季");
        break;
    case 9:
    case 10:
    case 11:
        System.out.println("秋季");
        break;
    case 12:
    case 1:
    case 2:
        System.out.println("冬季");
        break;
    default:
        System.out.println("输入有误!");
}

3:循环结构:

​ 循环结构,“重复”执行某些代码。

​ 循环结构的分类:

​ 1、for循环 。2、while循环。3、do…while循环

1): for循环

​ 语法格式

for(;;){
    循环体语句块;
    if(条件表达式){
    	break;
    }
}
for(初始化表达式; 循环条件; 迭代表达式){
    循环体语句块;(需要重复执行的代码)
}


执行过程:
(1)初始化表达式;
(2)判断循环条件;
(3)如果循环条件成立,先执行循环体语句块;然后执行迭代表达式,再回到(2)…
(4)如果循环条件不成立,会结束for;

 或者在当前循环中遇到break语句,也会结束当前for循环;

注意:
(1)for(;;)中的两个;是不能多也不能少
(2)循环条件必须是boolean类型
//遍历1-100之间的偶数
for(int i=1; i<=100; i++){//每次循环的步幅是1
    if(i%2==0){
        System.out.println(i);
    }
}

//遍历1-100之间的偶数
for(int i=2; i<=100; i+=2){//每次循环的步幅是2
    System.out.println(i);
}

2): while循环

​ 语法格式:

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

经典的形式:
while(true){
	循环体语句块;
    if(条件表达式){
    	break;
    }
}
执行过程:
(1)先判断循环条件
(2)如果循环条件成立,就执行循环体语句块;然后回到(1)
(3)如果循环条件不成立,就结束while循环;
 如果在循环体语句块中,遇到break,也会结束while循环;

注意:
(1)while(循环条件)中循环条件必须是boolean类型

//遍历1-100之间的偶数
int num = 2;
while(num<=100){
    System.out.println(num);
    num+=2;
}

3):do…while循环

语法规则:

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


执行过程:
(1)先执行一次循环体语句块;
(2)判断循环条件
(3)如果循环条件成立,再次执行循环体语句块;然后回到(2)…
(4)如果循环条件不成立,就结束do…while循环;

 如果在循环体语句块中,遇到break,也会结束do…while循环;

注意:
(1)while(循环条件)中循环条件必须是boolean类型
(2)do{}while();最后有一个分号
(3)do…while结构的循环体语句是至少会执行一次,这个和for和while是不一样的
//从键盘输入整数,统计正数、负数的个数,输入0结束
java.util.Scanner input = new java.util.Scanner(System.in);

int num;
int positive = 0;
int negative = 0;
do{
    System.out.print("请输入整数(0结束):");
    num = input.nextInt();
    
    if(num > 0){
        positive++;
    }else if(num < 0){
        negatvie++;
    }
}while(num!=0);

System.out.println("正数的个数:" + positive);
System.out.println("负数的个数:" + negatvie);

4):三种循环的选择

原则:三种循环之间是可以互相转换的,都能实现循环的功能

建议(习惯上):当我们次数比较明显的时候,或者说从几循环到几的时候,一般先考虑for;

​ 当循环体语句块至少要执行一次的时候,一般先考虑do…while;

​ 当循环条件比较明显,但是次数不明显,循环体语句块也不是至少执行一次,那么可以考虑while结构;

三种循环结构都具有四要素:

(1)循环变量的初始化表达式

(2)循环条件

(3)循环变量的修改的迭代表达式

(4)循环体语句块

5):跳转语句

​ 1、break

​ 用于:(1)switch结构

​ 作用:结束switch结构

​ (2)循环结构

​ 作用:结束当前循环

​ 2、continue

​ 用于:只能用于循环结构

​ 作用:提前结束本次循环,继续下一次循环

​ 3、return:

​ 用于:循环和分支结构都可以。

​ 作用:返回结果集。

第四章:数组

1:数组的相关概念和名词

​ 1、数组(array): 一组具有相同数据类型的数据的按照一定顺序排列的集合。

​ 把有限的几个相同类型的变量使用一个名称来进行统一管理。

​ 2、数组名:

​ (1)这个数组名,代表的是一组数

​ (2)这个数组名中存储的整个数组的“首地址”

​ 3、下标(index): 我们使用编号、索引、下标来区别表示一组数当中某一个。

​ 范围:[0,数组长度-1]

			例如:for(int i = 0; i<arr.length; i++){}

​ 4、元素(element): 这一组中的的每一个数据都是元素。

​ 如何表示数组元素? 数组名[下标]

​ 5、数组的长度(length), 数组中元素的总个数。

			如何获取数组长度? 数组名.length
2:数组的相关语法
1):数组的声明
 //推荐
元素的数据类型[] 数组名;

 //也对,但是不推荐
元素的数据类型  数组名[];

/要存储一组整数
int[] array;

//要存储一组单字符
char[] array;

//要存储一组字符串
String[] array;
2):数组的初始化

初始化的目的:(1)确定数组的长度(2)为元素赋值

两种初始化方式:

1、动态初始化

语法格式:

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

//为元素赋值
数组名[下标] =; //这个值可以是个常量值,也可以是个表达式的计算结果,也可以是键盘输入的

//如果每个元素的赋值比较有规律,通常使用for循环赋值
for(int i=0; i<长度; i++){
    数组名[下标] =;
}

注意:即使刚声明的数组,也有默认值。

2、静态初始化

语法格式:

数组名 = new 元素的数据类型[]{值列表};
//int[] arr = new int[5]{1,2,3,4,5};//错误的
//更简洁
//当声明与静态初始化一起完成时,可以简化
元素的数据类型[] 数组名 = {值列表};


适用场合:
  当数组的元素是已知的有限个时,可以使用静态初始化。
String[] weeks = {"monday","tuesday","wednesday","thursday","friday","saturday","sunday"};

int[] daysOfMonths = {31,28,31,30,31,30,31,31,30,31,30,31};

char[] letters = {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'};

3):数组的遍历

for循环遍历数组:

for(int i=0; i<数组名.lenght; i++){
    //或赋值
    数组名[i] =;
    //或显示
    System.out.println(数组名[i])//或其他操作
    //例如:判断是否是偶数
    if(数组名[i]%2==0){
          //...
    }
}

​ 4):数组的内存分析

元素是基本数据类型的一维数组内存分析:

int[] arr = {1,2,3,4,5};
int[] arr = new int[5];
for(int i=0; i<arr.length; i++){
    arr[i] = i+1;
}
3:数组的相关算法
1):数组中找最值

思路:

(1)先假设第一个元素最大/最小

(2)然后用max/min与后面的元素一一比较

示例代码:

int[] arr = {4,5,6,1,9};
//找最大值
int max = arr[0];
for(int i=1; i<arr.length; i++){
    if(arr[i] > max){
        max = arr[i];
    }
}
2):数组中找最值及其下标

情况一:找最值及其第一次出现的下标

思路:

(1)先假设第一个元素最大/最小

(2)然后用max/min与后面的元素一一比较

示例代码:

int[] arr = {4,5,6,1,9};
//找最大值
int max = arr[0];
int index = 0;
for(int i=1; i<arr.length; i++){
    if(arr[i] > max){
        max = arr[i];
        index = i;
    }
}

情况二:找最值及其所有最值的下标(即可能最大值重复)

思路:

(1)先找最大值

①假设第一个元素最大

②用max与后面的元素一一比较

(2)遍历数组,看哪些元素和最大值是一样的

示例代码:

int[] arr = {4,5,6,1,9};
//找最大值
int max = arr[0];
for(int i=1; i<arr.length; i++){
    if(arr[i] > max){
        max = arr[i];
    }
}

//遍历数组,看哪些元素和最大值是一样的
for(int i=0; i<arr.length; i++){
    if(max == arr[i]){
        System.out.print(i+"\t");
    }
}
3): 数组统计:求总和、均值、统计偶数个数等

思路:遍历数组,挨个的累加,判断每一个元素

示例代码:

int[] arr = {4,5,6,1,9};
//求总和、均值
int sum = 0;//因为0加上任何数都不影响结果
for(int i=0; i<arr.length; i++){
    sum += arr[i];
}
double avg = (double)sum/arr.length;

int[] arr = {4,5,6,1,9};

//求总乘积
long result = 1;//因为1乘以任何数都不影响结果
for(int i=0; i<arr.length; i++){
    result *= arr[i];
}

int[] arr = {4,5,6,1,9};
//统计偶数个数
int even = 0;
for(int i=0; i<arr.length; i++){
    if(arr[i]%2==0){
        even++;
    }
}
4):反转

方法有两种:

1、借助一个新数组

2、首尾对应位置交换

第一种方式示例代码:

int[] arr = {1,2,3,4,5,6,7,8,9};

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

//(2)复制元素
int len = arr.length;
for(int i=0; i<newArr.length; i++){
    newArr[i] = arr[len -1 - i];
}

//(3)舍弃旧的,让arr指向新数组
arr = newArr;//这里把新数组的首地址赋值给了arr

//(4)遍历显示
for(int i=0; i<arr.length; i++){
    System.out.println(arr[i]);
}

第二种方式示例代码:

int[] arr = {1,2,3,4,5,6,7,8,9};

//(1)计算要交换的次数:  次数 = arr.length/2
//(2)首尾交换
for(int i=0; i<arr.length/2; i++){//循环的次数就是交换的次数
    //首  与  尾交换
    int temp = arr[i];
    arr[i] = arr[arr.length-1-i];
	arr[arr.length-1-i] = temp;
}

//(3)遍历显示
for(int i=0; i<arr.length; i++){
    System.out.println(arr[i]);
}

5):复制

应用场景:

1、扩容

2、备份

3、截取

示例代码:扩容

int[] arr = {1,2,3,4,5,6,7,8,9};

//如果要把arr数组扩容,增加1个位置
//(1)先创建一个新数组,它的长度 = 旧数组的长度+1
int[] newArr = new int[arr.length + 1];

//(2)复制元素
//注意:i<arr.length   因位arr比newArr短,避免下标越界
for(int i=0; i<arr.length; i++){
    newArr[i] = arr[i];
}

//(3)把新元素添加到newArr的最后
newArr[newArr.length-1] = 新值;

//(4)如果下面继续使用arr,可以让arr指向新数组
arr = newArr;

//(4)遍历显示
for(int i=0; i<arr.length; i++){
    System.out.println(arr[i]);
}

示例代码:备份

int[] arr = {1,2,3,4,5,6,7,8,9};

//1、创建一个长度和原来的数组一样的新数组
int[] newArr = new int[arr.length];

//2、复制元素
for(int i=0; i<arr.length; i++){
    newArr[i] = arr[i];
}

//3、遍历显示
for(int i=0; i<arr.length; i++){
    System.out.println(arr[i]);
}

示例代码:截取

int[] arr = {1,2,3,4,5,6,7,8,9};

int start = 2;
int end = 5;

//1、创建一个新数组,新数组的长度 = end-start + 1;
int[] newArr = new int[end-start+1];

//2、赋值元素
for(int i=0; i<newArr.length; i++){
    newArr[i] = arr[start + i];
}

//3、遍历显示
for(int i=0; i<newArr.length; i++){
    System.out.println(newArr[i]);
}

6):查找

查找分为两种:

1、顺序查找:挨个看

​ 对数组没要求

2、二分查找:对折对折再对折

​ 对数组有要求,元素必须有大小顺序的

顺序查找示例代码:

int[] arr = {4,5,6,1,9};
int value = 1;
int index = -1;

for(int i=0; i<arr.length; i++){
    if(arr[i] == value){
        index = i;
        break;
    }
}

if(index==-1){
    System.out.println(value + "不存在");
}else{
    System.out.println(value + "的下标是" + index);
}

二分查找示例代码:

/*
2、编写代码,使用二分查找法在数组中查找 int value = 2;是否存在,如果存在显示下标,不存在显示不存在。
已知数组:int[] arr = {1,2,3,4,5,6,7,8,9,10};
*/
class Exam2{
	public static void main(String[] args){
		int[] arr = {1,2,3,4,5,6,7,8,9};//数组是有序的
		int value = 2;
		
        int index = -1;
		int left = 0;
        int right = arr.length - 1;
        int mid = (left + right)/2;
        while(left<=right){
            //找到结束
            if(value == arr[mid]){
                index = mid;
                break;
            }//没找到
            else if(value > arr[mid]){//往右继续查找
                //移动左边界,使得mid往右移动
                left = mid + 1;
            }else if(value < arr[mid]){//往左边继续查找
                right = mid - 1;
            }
            
            mid = (left + right)/2;
        }
        
        if(index==-1){
    		System.out.println(value + "不存在");
		}else{
    		System.out.println(value + "的下标是" + index);
		}
        
	}
}

使用for

class Exam2{
	public static void main(String[] args){
		int[] arr = {1,2,3,4,5,6,7,8,9};//数组是有序的
		int value = 2;
		
        int index = -1;
        
        for(int left=0,right=arr.length-1,mid = (left+right)/2; left<=right; mid = (left + right)/2){
             //找到结束
            if(value == arr[mid]){
                index = mid;
                break;
            }//没找到
            else if(value > arr[mid]){//往右继续查找
                //移动左边界,使得mid往右移动
                left = mid + 1;
            }else if(value < arr[mid]){//往左边继续查找
                right = mid - 1;
            }
        }

        if(index==-1){
    		System.out.println(value + "不存在");
		}else{
    		System.out.println(value + "的下标是" + index);
		}
        
	}
}
7):排序

数组的排序算法有千万种,我们只讲了两种:

1、冒泡排序

2、简单的直接排序

示例代码:冒泡:从小到大,从左到右两两比较

int[] arr = {4,5,6,3,1};
for(int i=1; i<arr.length; i++){//外循环的次数 = 轮数 = 数组的长度-1
    /*
    第1轮,i=1,从左到右两两比较,arr[0]与arr[1]。。。。。arr[3]与arr[4]
    第2轮,i=2,从左到右两两比较,arr[0]与arr[1]。。。。。arr[2]与arr[3]
    ...
    				arr[j]与arr[j+1]比较
    找两个关键点:(1)j的起始值:0(2)找j的终止值,依次是3,2,1,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;
        }
    }
}

例代码:从大到小,从右到左

char[] arr = {'h','e','l','l','o','j','a','v','a'};
for(int i=1; i<arr.length; i++){//外循环的次数 = 轮数 = 数组的长度-1
    /*
    第1轮,i=1,从右到左两两比较,arr[8]与arr[7],arr[7]与arr[6]....arr[1]与arr[0]
    第2轮,i=2,从右到左两两比较,arr[8]与arr[7],arr[7]与arr[6]....arr[2]与arr[1]
    ...
    第8轮,i=8,从右到左两两比较,arr[8]与arr[7]
    		   arr[j]与arr[j-1]
    找两个关键点:(1)j的起始值:8(2)找j的终止值,依次是1,2,3,。。。8,得出j>=i
    */
    for(int j=8; j>=i; j--){
        //从大到小,后面的元素 > 前面的元素,就交换
        if(arr[j]>arr[j-1]){
            int temp = arr[j];
            arr[j] = arr[j-1];
            arr[j-1] = temp;
        }
    }
}

示例代码:简单的直接选择排序

int[] arr = {3,2,6,1,8};
for(int i=1; i<arr.length; i++){//外循环的次数 = 轮数 = 数组的长度-1
    //(1)找出本轮未排序元素中的最值
    /*
    未排序元素:
    第1轮:i=1,未排序,[0,4]
    第2轮:i=2,未排序,[1,4]
    ...  
    每一轮未排序元素的起始下标:0,1,2,3,正好是i-1的
    未排序的后面的元素依次:
    第1轮:[1,4]  j=1,2,3,4
    第2轮:[2,4]  j=2,3,4
    第3轮:[3,4]  j=3,4
    第4轮:[4,4]  j=4
    j的起点是i,终点都是4
    */
    int max = arr[i-1];
    int index = i-1;
    for(int j=i; j<arr.length; j++){
        if(arr[j] > max){
            max = arr[j];
            index = j;
        }
    }
  
    //(2)如果这个最值没有在它应该在的位置,就与这个位置的元素交换
    /*
    第1轮,最大值应该在[0]
    第2轮,最大值应该在[1]
    第3轮,最大值应该在[2]
    第4轮,最大值应该在[3]
    正好是i-1的值
    */
    if(index != i-1){
        //交换arr[i-1]与arr[index]
        int temp = arr[i-1];
        arr[i-1] = arr[index];
        arr[index] = temp;
    }
}

//显示结果
for(int i=0; i<arr.length; i++){
	System.out.print(arr[i]);
}

4:二维数组
1):相关的表示方式

​ (1)二维数组的长度/行数:

​ 二维数组名.length

​ (2)二维数组的其中一行:

​ 二维数组名[行下标]

​ 行下标的范围:[0, 二维数组名.length-1]

​ (3)每一行的列数:

​ 二维数组名[行下标].length

​ 因为二维数组的每一行是一个一维数组

​ (4)每一个元素

​ 二维数组名[行下标][列下标]

2):二维数组的声明和初始化

​ 1、二维数组的声明

  //推荐
  元素的数据类型[][] 二维数组的名称;

 //不推荐
 元素的数据类型  二维数组名[][];
 //不推荐
  元素的数据类型[]  二维数组名[];

​ 2、二维数组的初始化

静态:

二维数组名 = new 元素的数据类型[][]{
			{第一行的值列表}, 
			{第二行的值列表},
			...
			{第n行的值列表}
		};
		
//如果声明与静态初始化一起完成
元素的数据类型[][] 二维数组的名称 = {
			{第一行的值列表}, 
			{第二行的值列表},
			...
			{第n行的值列表}
		};

动态:

//(1)先确定总行数
二维数组名 = new 元素的数据类型[总行数][];

//(2)再确定每一行的列数
二维数组名[行下标] = new 元素的数据类型[该行的总列数];

//(3)再为元素赋值
二维数组名[行下标][列下标] =;

3):二维数组的遍历
for(int i=0; i<二维数组名.length; i++){
    for(int j=0; j<二维数组名[i].length; j++){
        System.out.print(二维数组名[i][j]);
    }
    System.out.println();
}

第五章:面向对象的基本特征

​ 面向对象的基本特征:

​ 1、封装 2、继承 3、多态

1: 封装
1):好处?

​ (1)隐藏实现细节,方便使用者使用

​ (2)安全,可以控制可见范围

2):如何实现封装?

​ (1)通过权限修饰符

面试题:请按照可见范围从小到大(从大到小)列出权限修饰符?

修饰符		本类		本包		其他包的子类		任意位置
private		√		  ×			   ×				 ×
缺省	       √	     √		      ×	                ×
protected	√	       √		   √	              ×
public	    √	       √	       √	              √

权限修饰符可以修饰什么?

类(类、接口等)、属性、方法、构造器、内部类

类(外部类):public和缺省

属性:4种

方法:4种

构造器:4种

内部类:4种

3):通常属性的封装是什么样的?

​ 当然属性的权限修饰符可以是private、缺省、protected、public。但是我们大多数时候,见到的都是private,然后给它们配上get/set方法。

示例代码:标准Javabean的写法

public class Student{
    //属性私有化
    private String name;
    private int age;
    private boolean marry;
    
    //公共的get/set
    public void setName(String n){
        name = n;//这里因为还没有学习this等,可能还会优化
    }
    public String getName(){
        return name;
    }
    public void setAge(int a){
        age = a;
    }
    public int getAge(){
        return age;
    }
    public void setMarry(boolean m){
        marry = m;
    }
    public boolean isMarry(){//boolean类型的属性的get方法,习惯使用把get换成is
        return marry;
    }
}

4):构造器

​ 1、构造器的作用:
​ (1)和new一起使用创建对象。

//调用无参构造创建对象
类名 对象名 = new 类名();

//调用有参构造创建对象
类名 对象名 = new 类名(实参列表);

​ (2)可以在创建对象的同时为属性赋值。

public class Circle{
    private double radius;
    
    public Circle(){
        
    }
    public Circle(double r){
        radius = r;//为radius赋值
    }
}

​ 2、声明构造器的语法格式:

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

​ 3、构造器的特点:

​ (1)所有的类都有构造器。

​ (2)如果一个类没有显式/明确的声明一个构造器,那么编译器将会自动添加一个默认的无参构造。

​ (3)如果一个类显式/明确的声明了构造器,那么编译器将不再自动添加默认的无参构造,如果需要,那么就需要手动添加。

​ (4)构造器的名称必须与类名相同。

​ (5)构造器没有返回值类型。

​ (6)构造器可以重载。

public class Circle{
    private double radius;
    
    public Circle(){
        
    }
    public Circle(double r){
        radius = r;//为radius赋值
    }
}

​ 5):关键字this

​ 1、this关键字:意思:当前对象。

​ (1)如果出现在构造器中:表示正在创建的对象。

​ (2)如果出现在成员方法中:表示正在调用这个方法的对象。

​ 2、this的用法:

​ (1)this.属性:当局部变量与成员变量同名时,那么可以在成员变量的而前面加“this.”用于区别。

​ (2)this.方法:调用当前对象的成员方法,完全可以省略“this.”

​ (3)this()或this(实参列表)

​ this()表示调用本类的无参构造

​ this(实参列表)表示调用本类的有参构造

​ this()或this(实参列表)要么没有,要么必须出现在构造器的首行

public class Student{
    private String name;
    private int score;
    
    public Student(){
        
    }
    public Student(String name){
        this.name = name;
    }
    public Student(String name, int score){
        this(name);
    }
    public void setName(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
    public void setScore(int score){
        this.score = score;
    }
    public int getScore(){
        return score;
    }
}
5):成员变量与局部变量的区别

​ (1)声明的位置不同

​ 成员变量:类中方法外。

​ 局部变量:方法中或代码。

​ (2)运行时在内存中的存储位置不同

​ 成员变量:堆

​ 局部变量:栈

基本数据类型的变量在栈中,引用数据类型的变量在堆中:不准确。

​ (3)修饰符

​ 成员变量:有很多修饰符,例如:权限修饰符。

​ 局部变量:不能加权限修饰符,唯一的能加的是final。

​ (4)初始化

​ 成员变量:有默认值。

​ 局部变量:没有默认值,必须手动初始化。

​ (5)生命周期

​ 成员变量:随着对象的创建而创建,随着对象被回收而消亡,即与对象同生共死。每一个对象都是独立的。

​ 局部变量:方法调用时才分配,方法运行结束就没有了。每一次方法调用,都是独立的.

2:继承
1):为什么要继承?继承的好处?(理解)

​ (1)代码的复用

​ (2)代码的扩展

2):如何实现继承?

​ 语法格式:

【修饰符】 class 子类  extends 父类{
    
}
3):继承的特点

​ (1)子类会继承父类的所有特征(属性、方法),但是,私有的在子类中是不能直接使用的。

​ (2)子类不会继承父类的构造器,因为,父类的构造器是用于创建父类的对象的。

​ (3)子类的构造器中又必须去调用父类的构造器,在创建子类对象的同时,为从父类继承的属性进行初始化用,可以借助父类的构造器中的代码为属性赋值。

​ (4)Java只支持单继承:一个子类只能有一个“直接”父类

​ (5)Java又支持多层继承:父类还可以有父类,特征会代代相传

​ (6)一个父类可以同时拥有很多个子类

4):关键字super的使用

​ super关键字:引用父类的,找父类的xx。

​ 用法:

​ (1)super.属性:当子类声明了和父类同名的成员变量时,那么如果要表示某个成员变量是父类的,那么可以加“super.”。

​ (2)super.方法:当子类重写了父类的方法,又需要在子类中调用父类被重写的方法,可以使用"super."。

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

​ super():表示调用父类的无参构造。

​ super(实参列表):表示调用父类的有参构造。

注意:

(1)如果要写super()或super(实参列表),必须写在子类构造器的首行。

(2)如果子类的构造器中没有写:super()或super(实参列表),那么默认会有 super()。

(3)如果父类没有无参构造,那么在子类的构造器的首行“必须”写super(实参列表)。

5):方法的重写

​ 1、方法的重写(Override)

​ 当子类继承了父类的方法时,又觉得父类的方法体的实现不适合于子类,那么子类可以选择进行重写。

​ 2、方法的重写的要求

​ (1)方法名:必须相同。

​ (2)形参列表:必须相同。

​ (3)修饰符: 权限修饰符: >=。

​ (4)返回值类型:如果是基本数据类型和void:必须相同

​ 如果是引用数据类型:<=。

​ 在Java中我们认为,在概念范围上:子类 <父类

​ 3、重载(Overload)与重写(Override)的区别

​ 重载(Overload):在同一个类中,方法名相同,形参列表不同,和返回值类型无关的两个或多个方法。

​ 重写(Override):在父子类之间。对方法签名的要求见上面。

特殊的重载:

public class TestOverload {
	public static void main(String[] args) {
		B b = new B();
		//b对象可以调用几个a方法
		b.a();
		b.a("");//从b对象同时拥有两个方法名相同,形参不同的角度来说,算是重载
	}
}
class A{
	public void a(){
		//...
	}
}
class B extends A{
	public void a(String str){
		
	}
}

6):非静态代码块

​ (1):语法格式

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

​ (2):作用目的:在创建的过程中,为对象属性赋值,协助完成实例初始化的过程

​ (3):什么时候执行?

​ (1)每次创建对象时都会执行。

​ (2)优先于构造器执行。

​ (3)静态代码块比非静态代码块先执行。

3:多态
1):多态

​ 语法格式:

​ 父类 引用/变量 = 子类的对象;

2):前提

​ (1)继承

​ (2)方法的重写

​ (3)多态引用

3):现象

​ 编译时看左边/“父类”,运行时看右边/“子类”。

​ 编译时,因为按父类编译,那么只能父类有的方法,子类扩展的方法是无法调用的;

​ 执行时一定是运行子类重写的过的方法体。
实例代码:

class Person{
	public void eat(){
		System.out.println("吃饭");
	}
	public void walk(){
		System.out.println("走路");
	}
}
class Woman extends Person{
	public void eat(){
		System.out.println("细嚼慢咽的吃饭");
	}
	public void walk(){
		System.out.println("婀娜多姿走路");
	}
	public void shop(){
		System.out.println("买买买...");
	}
}
class Man extends Person{
	public void eat(){
		System.out.println("狼吞虎咽的吃饭");
	}
	public void walk(){
		System.out.println("大摇大摆的走路");
	}
	public void smoke(){
		System.out.println("吞云吐雾");
	}
}
class Test{
    public static void main(String[] args){
        Person p = new Woman();//多态引用
        p.eat();//执行子类重写
        p.walk();//执行子类重写
        //p.shop();//无法调用
    }
}

4):应用场景

​ (1)多态参数:形参是父类,实参是子类对象

​ (2)多态数组:数组元素类型是父类,元素存储的是子类对象

​ 示例代码:多态参数

class Test{
    public static void main(String[] args){
        test(new Woman());//实参是子类对象
        test(new Man());//实参是子类对象
    }
    public static void test(Person p){//形参是父类类型
        p.eat();
        p.walk();
    }
}

​ 示例代码:多态数组

class Test{
    public static void main(String[] args){
        Person[] arr = new Person[2];//多态数组
        arr[0] = new Woman();
        arr[1] = new Man();
        
        for(int i=0; i<arr.length; i++){
            all[i].eat();
            all[i].walk();
        }
    }
}
5):向上转型与向下转型:父子类之间的转换

​ (1)向上转型:自动类型转换

	当把子类的对象赋值给父类的变量时(即多态引用时),在编译时,这个对象就向上转型为父类。此时就看不见子类“特有、扩展”的方法。

​ (2)向下转型:强制转换。有风险,可能会报ClassCastException异常。

​ 当需要把父类的变量赋值给一个子类的变量时,就需要向下转型。

​ 要想转型成功,必须保证该变量中保存的对象的运行时类型是<=强转的类型

class Person{
	//方法代码省略...
}
class Woman extends Person{
    //方法代码省略...
}
class ChineseWoman extends Woman{
	//方法代码省略...
}



 public class Test{
     public static void main(String[] args){
		//向上转型
		Person p1 = new Woman();
		//向下转型
		Woman m = (Woman)p1; 
		//p1变量中实际存储的对象就是Woman类型,和强转的Woman类型一样

		//向上转型
		Person p2 = new ChineseWoman();
		//向下转型
		Woman w2 = (Woman) p2; 
		//p2变量中实际存储的对象是ChineseWoman类型,强制的类型是Woman,ChineseWoman<Woman类型     
     }
 }

6):instanceof

​ 作用:用来判断这个对象是否属于这个类型,或者说,是否是这个类型的对象或这个类型子类的对象。

示例代码:

class Person{
	//方法代码省略...
}
class Woman extends Person{
    //方法代码省略...
}
class ChineseWoman extends Woman{
	//方法代码省略...
}

 public class Test{
     public static void main(String[] args){
         Person p = new Person();
         Woman w = new Woman();
         ChineseWoman c = new ChineseWoman();
         
         if(p instanceof Woman){//false
             
         }
         if(w instanceof Woman){//true
             
         }
         if(c instanceof Woman){//true
             
         }
     }
 }

第六章:面向对象的高级特性

​ 修饰符的学习围绕三个问题:

​ (1)单词的意思

​ (2)可以修饰什么?

​ (3)用它修饰后有什么不同?

1):关键字:final

​ final:最终的

​ 用法:

​ (1)修饰类(包括外部类、内部类类):表示这个类不能被继承,没有子类。

​ (2)修饰方法:表示这个方法不能被重写。

​ (3)修饰变量(成员变量(类变量、实例变量),局部变量):表示这个变量的值不能被修改。

​ 注意:如果某个成员变量用final修饰后,也得手动赋值,而且这个值一旦赋完,就不能修改了,即没有set方法。

2):关键字:native

​ 用法:只能修饰方法.

​ 表示这个方法的方法体代码不是用Java语言实现的。

	但是对于Java程序员来说,可以当做Java的方法一样去正常调用它,或者子类重写它。
3):关键字:static

static:静态的

用法:

1、成员方法:我们一般称为静态方法或类方法

(1)不能被重写

(2)被使用

本类中:其他方法中可以直接使用它

其他类中:可以使用“类名.方法"进行调用,也可以使用"对象名.方法",推荐使用“类名.方法"

(3)在静态方法中,我们不能出现:this,super,非静态的成员

2、成员变量:我们一般称为静态变量或类变量

(1)静态变量的值是该类所有对象共享的

(2)静态变量存储在方法区

(3)静态变量对应的get/set也是静态的

(4)静态变量与局部变量同名时,就可以使用“类名.静态变量"进行区分

3、内部类:后面讲

4、代码块:静态代码块

5、静态导入(JDK1.5引入)

4):静态代码块

语法格式:

【修饰符】 class 类名{
    static{
        静态代码块;
    }
}

2、作用:

协助完成类初始化,可以为类变量赋值。

3、类初始化()

类的初始化有:

①静态变量的显式赋值代码

②静态代码块中代码

其中①和②按顺序执行

注意:类初始化方法,一个类只有一个

4、类的初始化的执行特点:

(1)每一个类的()只执行一次

(2)如果一个子类在初始化时,发现父类也没有初始化,会先初始化父类

(3)如果既要类初始化又要实例化初始化,那么一定是先完成类初始化的

5):变量的分类与区别

​ 1、变量按照数据类型分:

​ (1)基本数据类型的变量,里面存储数据值

​ (2)引用数据类型的变量,里面存储对象的地址值

int a = 10;//a中存储的是数据值

Student stu = new Student();//stu存储的是对象的地址值
int[] arr = new int[5];//arr存储的是数组对象的地址值
String str = "hello";//str存储的是"hello"对象的地址值

2、变量按照声明的位置不同:

​ (1)成员变量

​ (2)局部变量

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

​ (1)声明的位置不同

​ 成员变量:类中方法外

​ 局部变量:(1)方法的()中,即形参(2)方法体的{}的局部变量(3)代码块{}中

​ (2)存储的位置不同

​ 成员变量:

​ 如果是静态变量(类变量),在方法区中

​ 如果是非静态的变量(实例变量),在堆中。

​ 局部变量:栈。

​ (3)修饰符不同。

​ 成员变量:4种权限修饰符、static、final。

​ 局部变量:只有final。

​ (4)生命周期。

​ 成员变量:

​ 如果是静态变量(类变量),和类相同。

​ 如果是非静态的变量(实例变量),和所属的对象相同,每一个对象是独立。

​ 局部变量:每次执行都是新的。

(5)作用域

​ 成员变量:

​ 如果是静态变量(类变量),在本类中随便用,在其他类中使用“类名.静态变量"。

​ 如果是非静态的变量(实例变量),在本类中只能在非静态成员中使用,在其他类中使用“对象名.非静态的变量"。

​ 局部变量:有作用域

6):根父类

​ 1、java.lang.Object类是类层次结构的根父类。包括数组对象。

​ (1)Object类中声明的所有的方法都会被继承到子类中,那么即所有的对象,都拥有Object类中的方法。

​ (2)每一个对象的创建,最终都会调用到Object实例初始化方法()。

​ (3)Object类型变量、形参、数组,可以存储任意类型的对象。

​ 2、Object类的常用方法

​ (1)public String toString():

​ ①默认情况下,返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"。

​ ②通常是建议重写,如果在eclipse中,可以用Alt +Shift + S–>Generate toString()。

​ ③如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()。

​ (2)public final Class<?> getClass():获取对象的运行时类型。

​ (3)protected void finalize():当对象被GC确定为要被回收的垃圾,在回收之前由GC帮你调用这个方法。而且这个方法只会被调用一次。子类可以选择重写。

​ (4)public int hashCode():返回每个对象的hash值。

​ 规定: ①如果两个对象的hash值是不同的,那么这两个对象一定不相等;

​ ②如果两个对象的hash值是相同的,那么这两个对象不一定相等。

​ 主要用于后面当对象存储到哈希表等容中时,为了提高性能用的。

​ (5)public boolean equals(Object obj):用于判断当前对象this与指定对象obj是否“相等”

​ ①默认情况下,equals方法的实现等价于与“==”,比较的是对象的地址值

​ ②我们可以选择重写,重写有些要求:

A:如果重写equals,那么一定要一起重写hashCode()方法,因为规定:

​ a:如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;

​ b:如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;

​ c:如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false

B:如果重写equals,那么一定要遵循如下几个原则:

​ a:自反性:x.equals(x)返回true

​ b:传递性:x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true

​ c:一致性:只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致

​ d:对称性:x.equals(y)与y.equals(x)结果应该一样

​ e:非空对象与null的equals一定是false

7):关键字:abstract

​ 1、什么时候会用到抽象方法和抽象类?

当声明父类的时候,在父类中某些方法的方法体的实现不能确定,只能由子类决定。但是父类中又要体现子类的共同的特征,即它要包含这个方法,为了统一管理各种子类的对象,即为了多态的应用。

那么此时,就可以选择把这样的方法声明为抽象方法。如果一个类包含了抽象方法,那么这个类就必须是个抽象类。

抽象类的语法格式:

【权限修饰符】 abstract class 类名{
    
}
【权限修饰符】 abstract class 类名 extends 父类{
    
}

抽象方法的语法格式

【其他修饰符】 abstract 返回值类型  方法名(【形参列表】);

​ 4、抽象类的特点

​ (1)抽象类不能直接实例化,即不能直接new对象。

​ (2)抽象类就是用来被继承的,那么子类继承了抽象类后,必须重写所有的抽象方法,否则这个子类也得是抽象类。

​ (3)抽象类也有构造器,这个构造的作用不是创建抽象类自己的对象用的,给子类在实例化过程中调用。

​ (4)抽象类也可以没有抽象方法,那么目的是不让你创建对象,让你创建它子类的对象。

​ (5)抽象类的变量与它子类的对象也构成多态引用。

5、不能和abstract一起使用的修饰符?

​ (1)final:和final不能一起修饰方法和类。

​ (2)static:和static不能一起修饰方法。

​ (3)native:和native不能一起修饰方法。

​ (4)private:和private不能一起修饰方法。

8): 接口

​ 1、接口的概念

​ 接口是一种标准。注意关注行为标准(即方法)。

​ 面向对象的开发原则中有一条:面向接口编程。

​ 2、接口的声明格式

【修饰符】 interface 接口名{
    接口的成员列表;
}

​ 3、类实现接口的格式

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

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

​ 4、接口继承接口的格式

【修饰符】 interface 接口名 extends 父接口们{
    接口的成员列表;
}

​ 5、接口的特点

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

​ (2)只能创建接的实现类对象,那么接口与它的实现类对象之间可以构成多态引用。

​ (3)实现类在实现接口时,必须重写所有抽象的方法,否则这个实现类也得是抽象类。

​ (4)Java规定类与类之间,只能是单继承,但是Java的类与接口之间是多实现的关系,即一个类可以同时实现多个接口。

​ (5)Java还支持接口与接口之间的多继承。

​ 6、接口的成员

​ JDK1.8之前:

​ (1)全局的静态的常量:public static final,这些修饰符可以省略

​ (2)公共的抽象方法:public abstract,这些修饰符也可以省略

​ JDK1.8之后:

​ (3)公共的静态的方法:public static ,这个就不能省略了

​ (4)公共的默认的方法:public default,这个就不能省略了

​ 9、常用的接口

​ (1)java.lang.Comparable接口:自然排序

​ 抽象方法:int compareTo(Object obj)

​ (2)java.util.Comparator接口:定制排序

​ 抽象方法:int compare(Object obj1 ,Object obj2)

如果员工类型,默认顺序,自然顺序是按照编号升序排列,那么就实现Comparable接口

class Employee implements Comparable{
    private int id;
    private String name;
    private double salary;
    
    //省略了构造器,get/set,toString
    
    @Override
    public int compareTo(Object obj){
        return id - ((Employee)obj).id;
    }
}

如果在后面又发现有新的需求,想要按照薪资排序,那么只能选择用定制排序,实现Comparator接口

class SalaryComparator implements Comparator{
    public int compare(Object o1, Object o2){
        Employee e1 = (Employee)o1;
        Employee e2 = (Employee)o2;
        if(e1.getSalary() > e2.getSalary()){
            return 1;
        }else if(e1.getSalary() < e2.getSalary()){
            return -1;
        }
        return 0;
    }
}

第七章:枚举与注解

1):枚举(JDK1.5引入的)

​ 枚举类型的对象是有限、固定的几个常量对象。

2):语法格式
//形式一:枚举类型中只有常量对象列表
【修饰符】 enum 枚举类型名{
    常量对象列表
}

//形式二:枚举类型中只有常量对象列表
【修饰符】 enum 枚举类型名{
    常量对象列表;
    
    其他成员列表;
}

说明:常量对象列表必须在枚举类型的首行

3):在其他类中如何获取枚举的常量对象
//获取一个常量对象
枚举类型名.常量对象名

//获取一个常量对象
枚举类型名.valueOf("常量对象名")
    
//获取所有常量对象
枚举类型名[] all = 枚举类型名.values();

4):枚举类型的特点

(1)枚举类型有一个公共的基本的父类,是java.lang.Enum类型,所以不能再继承别的类型

(2)枚举类型的构造器必须是私有的

(3)枚举类型可以实现接口

interface MyRunnable{
    void run();
}
enum Gender implements MyRunnable{
    NAN,NV;
    public void run(){
        //...
    }
}
//或
enum Gender implements MyRunnable{
    NAN{
        public void run(){
       	 //...
    	}
    },NV{
        public void run(){
        //...
   		}
    };
    
}
5):注解

​ 1、注解:它是代码级别的注释

​ 2、标记符号:@

​ 3、系统预定义的三个最基本的注解:

​ (1)@Override:表示某个方法是重写的方法

​ 它只能用在方法上面,会让编译器对这个方法进行格式检查,是否满足重写的要求

​ (2)@SuppressWarnings(xx):抑制警告

​ (3)@Deprecated:表示xx已过时
​ 4、和文档注释相关的注解

​ (1)文档注释

/**

文档注释
*/

​ (2)常见的文档注释

​ @author:作者

​ @since:从xx版本加入的

​ @see:另请参考

​ @param:形参

​ @return:返回值

​ @throws或@exception:异常

​ 5、JUnit相关的几个注解

​ (1)@Test:表示它是一个单元测试方法

​ 这个方法需要是:public void xxx(){}

​ (2)@Before:表示在每一个单元测试方法之前执行

​ 这个方法需要是:public void xxx(){}

​ (3)@After:表示在每一个单元测试方法之后执行

​ 这个方法需要是:public void xxx(){}

​ (4)@BeforeClass:表示在类初始化阶段执行,而且只执行一次

​ 这个方法需要是:public static void xxx(){}

​ (3)@AfterClass:表示在类的“卸载”阶段执行,而且只执行一次

​ 这个方法需要是:public static void xxx(){}

​ 6、自定义注解

@元注解(@Target(xx))
【修饰符】 @interface 注解名{
    
}

@元注解(@Target(xx))
【修饰符】 @interface 注解名{
    配置参数列表
}

配置参数的语法格式

数据类型  配置参数名();

或

数据类型  配置参数名() default 默认值;

元注解:

(1)@Target(xx):用它标记的注解能够用在xx位置

(xx):由ElementType枚举类型的10个常量对象指定,例如:TYPE,METHOD,FIELD等

@Target(ElementType.TYPE)
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})


import static java.lang.annotation.ElementType.*;


@Target({TYPE,METHOD,FIELD})

2)@Retention(xx):用它标记的注解可以滞留到xx阶段

(xx):由RetentionPolicy枚举类型的3个常量对象指定,分别是:SOURCE,CLASS,RUNTIME

唯有RUNTIME阶段的注解才能被反射读取到

例如:

@Retention(RetentionPolicy.RUNTIME)

(3)@Documentd:用它标记的注解可以读取到API中

(4)@Inherited:用它标记的注解可以被子类继承

关于配置参数:

(1)配置参数的类型有要求:

八种基本数据类型、String、枚举、Class类型、注解、它们的数组。

(2)如果自定义注解声明了配置参数,那么在使用这个注解时必须为配置参数赋值,除非它有默认值

@自定义注解名(配置参数名1=值,配置参数名2=值。。。)

//如果配置参数类型是数组,那么赋值时,可以用{}表示数组
@自定义注解名(配置参数名1={},配置参数名2=值。。。)

(3)如果配置参数只有一个,并且名称是value,那么赋值时可以省略value=

(4)如果读取这个注解时,要获取配置参数的值的话,可以当成方法一样来访问

案例:


@Retention(RetentionPolicy.RUNTIME) // 让程序执行不同的阶段
@Target(value = { ElementType.METHOD }) //限定某个自定义注解能够被应用在哪些Java元素上面的
@Documented //是否能随着被定义的java文件生成到JavaDoc文档当中
public @interface Test {
	 	String name();
	    int age() default 18;
	    int[] score();
	    
}

public class Student {
	@Test(name = "cherry-peng",age = 23,score = {99,66,77})
	 public static void study(int times){
	        for(int i = 0; i < times; i++){
	            System.out.println("Good Good Study, Day Day Up!");
	        }
	    }
}

第八章:异常

1):异常的类型的体系结构

1、异常系列的超父类:java.lang.Throwable

(1)只有它或它子类的对象,才能被JVM或throw语句“抛”出

(2)也只有它或它子类的对象,才能被catch“捕获”

2、Throwable分为两大派别

(1)Error:严重的错误,需要停下来重新设计、升级解决这个问题

(2)Exception: 一般的异常,可以通过判断、检验进行避免,或者使用try…catch进行处理

3、Exception又分为两大类

(1)运行时异常:

​ 它是RuntimeException或它子类的对象。

​ 这种类型的异常,编译器不会提醒你,要进行throws或try…catch进行处理,但是运行时可能导致崩溃。

(2)编译时异常:

​ 异常除了运行时异常以外的都是编译时异常。

​ 这种类型的异常,编译器是强制要求你,throws或try…catch进行处理,否则编译不通过。

4、列出常见的异常类型

(1)运行时异常

RuntimeException、NullPointerException(空指针异常),ClassCastException(类型转换异常),ArithmeticException(算术异常),NubmerFormatException(数字格式化异常),IndexOutOfBoundsException(下标越界异常)(ArrayIndexOutOfBoundsException(数组下标越界异常)、StringIndexOutOfBoundsException(字符串下标越界异常))、InputMisMatchException(输入类型不匹配异常)。。。。

(2)编译时异常

FileNotFoundException(文件找不到异常)、IOException(输入输出异常)、SQLException(数据库sql语句执行异常)。。

2):异常的处理

1、在当前方法中处理:try…catch…finally

//形式一:try...catch
try{
    可能发生异常的代码
}catch(异常类型  异常名e){
    处理异常的代码(一般都是打印异常的信息的语句)
}catch(异常类型  异常名e){
    处理异常的代码(一般都是打印异常的信息的语句)
}。。。


//形式二:try...finally
try{
    可能发生异常的代码
}finally{
    无论try中是否有异常,也不管是不是有return,都要执行的部分
}

//形式三:try..catch..finally
try{
    可能发生异常的代码
}catch(异常类型  异常名e){
    处理异常的代码(一般都是打印异常的信息的语句)
}catch(异常类型  异常名e){
    处理异常的代码(一般都是打印异常的信息的语句)
}。。。
finally{
    无论try中是否有异常,也不管catch是否可以捕获异常,也不管trycatch中是不是有return,都要执行的部分
}

执行特点:

(1)如果try中的代码没有异常,那么try中的代码会正常执行,catch部分就不执行,finally中会执行

(2)如果try中的代码有异常,那么try中发生异常的代码的后面就不执行了,找对应的匹配的catch分支执行,finally中会执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kOQFABsU-1594124252804)(imgs/1559610439025.png)]

2、finally与return混合使用时

(1)如果finally中有return,一定从finally中的return返回。

此时try和catch中的return语句,执行了一半,执行了第一个动作。所以,finally中的return语句会覆盖刚刚的返回值

return 返回值; 语句有两个动作:(1)把返回值放到“操作数栈”中,等当前方法结束后,这个“操作数栈”中的值会返回给调用处(2)结束当前方法的执行

(2)如果finally中没有return,finally中的语句会执行,但是不影响最终的返回值

即try和catch中的return语句两步拆开来走,先把(1)把返回值放到“操作数栈”中,(2)然后走finally中的语句(3)再执行return后半个动作,结束当前方法

3、在当前方法中不处理异常,明确要抛给调用者处理,使用throws

语法格式:

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

此时调用者,就知道需要处理哪些异常。

方法的重写的要求:

(1)方法名:相同

(2)形参列表:相同

(3)返回值类型:

​ 基本数据类型和void:相同

​ 引用数据类型:<=

(4)修饰符:

​ 权限修饰符:>=

​ 其他修饰符:static,final,private不能被重写

(5)throws:<=

方法的重载:

(1)方法名:相同

(2)形参列表:必须不同

(3)返回值类型:无关

(4)修饰符:无关

(5)throws:无关

3): 手动抛出异常:throw

throw 异常对象;

//例如:
throw new AccountException(“xxx”);

throw抛出来的异常对象,和JVM抛出来的异常对象一样,也要用try…catch处理或者throws。

如果是运行时异常,编译器不会强制要求你处理,如果是编译时异常,那么编译器会强制要求你处理。

4):自定义异常

1、必须继承Throwable或它的子类

我们见到比较多的是继承RuntimeException和Exception.

如果你继承RuntimeException或它的子类,那么你自定义的这个异常就是运行时异常。编译器就不会提醒你处理。

如果你继承Exception,那么它属于编译时异常,编译器会强制你处理。

2、建议大家保留两个构造器

//无参构造
public 自定义异常名(){
    
}

//有参构造
public 自定义异常名(String message){
    super(message);
}

3、自定义异常对象,必须手动抛出,用throw抛出,如果自定义异常是为了提示,一定要用try…catch,不要直接用throw往外抛。这样只能被框架捕获。

案例:

自定义异常,继承Exception,代码如下

public class NoMappingParamString extends Exception {
    /*无参构造函数*/
    public NoMappingParamString(){
        super();
    }
    
    //用详细信息指定一个异常
    public NoMappingParamString(String message){
        super(message);
    }
    
    //用指定的详细信息和原因构造一个新的异常
    public NoMappingParamString(String message, Throwable cause){
        super(message,cause);
    }
    
    //用指定原因构造一个新的异常
    public NoMappingParamString(Throwable cause) {
        super(cause);
    }
}

使用自定义异常:

/*结果resultType字段set方法*/
    public void setResultType(String resultType) {
        this.resultType = resultType == null ? null : resultType.trim();  //resultType结果封装
        
        //自定义一个resultTypeString字段,用来根据resultType的值(1,2,3)自动生成对应的文本
        /*这样写的好处是集中管理,后台代码中只在此处管理,不好的地方是运营时突然加一个值就会返回未知类型,要更新要把后端重新编译发布,也就意味着要重启(把值传给前端让前端判断是不用重启的)。这种方案只适用于类型固定的字段*/
        if (resultType!=null) {
            int rt = Integer.parseInt(resultType);   //将flag转换为int值
            switch (rt) {  //判断属于那种类型,就给resultTypeString赋予对应的值
            case 1:
                resultTypeString ="未处理";
                break;
            case 2:
                resultTypeString ="自动解除";
                break;
            case 3:
                resultTypeString ="已解除";
                break;
            default:
                resultTypeString = "未知类型";
                /*这里一定要try catch异常,因为这是set方法,throw出去大部分情况是被框架获取*/
                try {   
                    /*实例化自定义异常*/
                    NoMappingParamString exception = new NoMappingParamString("resultType类型未完善"+"未知resultType:"+resultType);
                    /*抛出异常*/
                    throw exception;
                } catch (NoMappingParamString e) { //捕获异常
                    System.err.println("异常信息:"+e.getMessage());  //获取异常信息,就是上面传的message
                    e.printStackTrace();  //把栈信息打印出来
                }
                break;
            }
        }else {
            resultTypeString = "";  //如果flag为null返回为空
        }
    }

4、仅仅为了提示,又不想自定义一个Exception,可以用RuntimeException。这个可以抛出异常,并准确定位,缺点是不能处理这个异常自定义异常的话可以捕获并且处理

public Queue(int initialSize) {
        if (initialSize >= 0) {
            this.maxSize = initialSize;
            data = new Object[initialSize];
        }else {
            throw new RuntimeException("初始化大小不能小于0:"+initialSize);
        }
    }
5):关于异常的几个方法

(1)e.printStackTrace():打印异常对象的详细信息,包括异常类型,message,堆栈跟踪信息。这个对于调试,或者日志跟踪是非常有用的

(2)e.getMessage():只是获取异常的message信息

关于异常信息的打印:

用System.err打印和用e.printStackTrace()都是会标记红色的突出。

用System.out打印,当成普通信息打印。

这两个打印是两个独立的线程,顺序是不能精确控制的。

第九章:多线程

1: 相关的概念
1):程序(Program)

​ 为了实现一个功能,完成一个任务而选择一种编程语言编写的一组指令的集合。

2):进程(Process)

​ 程序的一次运行。操作系统会给这个进程分配资源(内存)。

​ 进程是操作系统分配资源的最小单位。

		进程与进程之间的内存是独立,无法直接共享。

		最早的DOS操作系统是单任务的,同一时间只能运行一个进程。后来现在的操作系统都是支持多任务的,可以同时运行多个进程。进程之间来回切换。成本比较高。
3):线程(Thread)

​ 线程是进程中的其中一条执行路径。一个进程中至少有一个线程,也可以有多个线程。有的时候也把线程称为轻量级的进程。

​ 同一个进程的多个线程之间有些内存是可以共享的(方法区、堆),也有些内存是独立的(栈(包括虚拟机栈和本地方法栈)、程序计数器)。 线程之间的切换相对进程来说成本比较低。

4):并行: 多个处理器同时可以执行多条执行路径。
5):并发:多个任务同时执行,但是可能存在先后关系
2:两种实现多线程的方式
1):继承Thread类

​ 步骤:

​ (1)编写线程类,去继承Thread类

​ (2)重写public void run(){}

​ (3)创建线程对象

​ (4)调用start()

​ 案例:

class MyThread extends Thread {
    public void run(){
        //...
    }
}
class Test{
    public static void main(String[] args){
        MyThread my = new MyThread();
        my.start();//有名字的线程对象启动
        
        new MyThread().start();//匿名线程对象启动
        
        //匿名内部类的匿名对象启动
        new Thread(){
            public void run(){
                //...
            }
        }.start();
        
        //匿名内部类,但是通过父类的变量多态引用,启动线程
        Thread t =  new Thread(){
            public void run(){
                //...
            }
        };
        t.start();
    }
}

2):实现Runnable接口

步骤:

(1)编写线程类,实现Runnable接口

(2)重写public void run(){}

(3)创建线程对象

(4)借助Thread类的对象启动线程

class MyRunnable implements Runnable{
    public void run(){
        //...
    }
}
class Test {
    public static void main(String[] args){
        MyRunnable my = new MyRunnable();
        Thread t1 = new Thread(my);
        Thread t2 = new Thread(my);
        t1.start();
        t2.start();
        
        //两个匿名对象
        new Thread(new MyRunnable()).start();
        
        //匿名内部类的匿名对象作为实参直接传给Thread的构造器
        new Thread(new Runnable(){
            public void run(){
                //...
            }
        }).start();
            
    }
}

3):Thread的相关API

1、构造器

Thread()
Thread(String name)
Thread(Runnable target)
Thread(Runnable target, String name)
2、其他方法

(1)public void run()

(2)public void start()

(3)获取当前线程对象:Thread.currentThread()

(4)获取当前线程的名称:getName()

(5)设置或获取线程的优先级:set/getPriority()

优先级的范围:[1,10],Thread类中有三个常量:MAX_PRIORITY(10),MIN_PRIORITY(1),NORM_PRIORITY(5)

优先级只是影响概率。

(6)线程休眠:Thread.sleep(毫秒)

(7)打断线程:interrupt()

(8)暂停当前线程:Thread.yield()

(9)线程要加塞:join()

xx.join()这句代码写在哪个线程体中,哪个线程被加塞,和其他线程无关。

(10)判断线程是否已启动但未终止:isAlive()

3、线程的几个常见状态:

创建状态-----启动线程------->就绪状态 -----获取资源------->运行状态------执行完毕/干扰-------->死亡状态

​ --------等待用户/休眠------>阻塞状态

4):关键字:volatile

volatile:易变,不稳定,不一定什么时候会变

修饰:成员变量

作用:当多个线程同时去访问的某个成员变量时,而且是频繁的访问,再多次访问时,发现它的值没有修改,Java执行引擎就会对这个成员变量的值进行缓存。一旦缓存之后,这个时候如果有一个线程把这个成员变量的值修改了,Jav执行引擎还是从缓存中读取,导致这个值不是最新的。如果不希望Java执行引擎把这个成员变的值缓存起来,那么就可以在成员变量的前面加volatile,每次用到这个成员变量时,都是从主存中读取。

5):关键字:synchronized(同步)

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

(1)多个线程

(2)共享数据

(3)多个线程的线程体中,多条语句再操作这个共享数据时

2、如何解决线程安全问题?同步锁

​ 形式一:同步代码块

​ 形式二:同步方法

3、同步代码块

synchronized(锁对象){
    //一次任务代码,这其中的代码,在执行过程中,不希望其他线程插一脚
}

锁对象:

(1)任意类型的对象

(2)确保使用共享数据的这几个线程,使用同一个锁对象

案例:

package demo1;
public class SellTickets {
    static int count = 10;

    //任何对象都可以充当一个对象锁
    static Object obj = new Object();
    static Runnable r = new Runnable() {
      @Override
      public void run() {
          while(count > 0) {
              System.out.println(Thread.currentThread().getName()+"--");
              synchronized(obj){
                  count--;
                  if(count <= 0) {
                      return;
                  }
                  System.out.println(Thread.currentThread().getName() + "售出了 一张票,剩余" + count);
                  
              }
          }
      }
  };
    public static void main(String[] args) {
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);
       // t1.setPriority(8);
        t1.start();
        t2.start();
        t3.start();
      }
}

4、同步方法

synchronized 【修饰符】 返回值类型  方法名(【形参列表】)throws 异常列表{
    //同一时间,只能有一个线程能进来运行
}

锁对象:

(1)非静态方法:this(谨慎)

(2)静态方法:当前类的Class对象

案例:

package demo1;

public class SellTickets {
    static int count = 100;
    //任何对象都可以充当一个对象锁
    //static Object obj = new Object();
    static Runnable r = new Runnable() {
      @Override
      public void run() {
          while(count > 0) {
           sellTickets();
          }
      }
        //同步方法,作用和同步代码块一样
        public synchronized void sellTickets() {
            if (count <= 0) {
                return;
            }
            count--;
            System.out.println("售票员" + Thread.currentThread().getName() + "售出一张票,余额 为" + count);
        }
    };
    public static void main(String[] args) {
        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        Thread t3 = new Thread(r);
       // t1.setPriority(8);
        t1.start();
        t2.start();
        t3.start();
      }
}
6): 线程通信

1、为了解决“生产者与消费者问题”。

当一些线程负责往“数据缓冲区”放数据,另一个线程负责从“数据缓冲区”取数据。

问题1:生产者线程与消费者线程使用同一个数据缓冲区,就是共享数据,那么要考虑同步

问题2:当数据缓冲区满的时候,生产者线程需要wait(), 当消费者消费了数据后,需要notify或notifyAll

​ 当数据缓冲区空的时候,消费者线程需要wait(), 当生产者生产了数据后,需要notify或notifyAll

2、java.lang.Object类中声明了:

(1)wait():必须由“同步锁”对象调用

(2)notfiy()和notifyAll():必须由“同步锁”对象调用

3、面试题:sleep()和wait的区别

(1)sleep()不释放锁,wait()释放锁

(2)sleep()在Thread类中声明的,wait()在Object类中声明

(3)sleep()是静态方法,是Thread.sleep(),wait()是非静态方法,必须由“同步锁”对象调用

(4)sleep()方法导致当前线程进入阻塞状态后,当时间到或interrupt()醒来

​ wait()方法导致当前线程进入阻塞状态后,由notify或notifyAll()

4、哪些操作会释放锁?

(1)同步代码块或同步方法正常执行完一次自动释放锁

(2)同步代码块或同步方法遇到return等提前结束

(3)wait()

5、不释放锁

(1)sleep()

(2)yield()

(3)suspend()

第十章:常用类

1,包装类
1):包装类汇总

​ 当要使用只针对对象设计的API或新特性(例如泛型),那么基本数据类型的数据就需要用包装类来包装。

序号基本数据类型包装类
1byteByte
2shortShort
3intInteger
4longLong
5floatFloat
6doubleDouble
7charCharacter
8booleanBoolean
9voidVoid
2): 装箱与拆箱

​ JDK1.5之后,可以自动装箱与拆箱。

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

Integer i = 1;
Double d = 1;//错误的,1是int类型

​ 装箱:把基本数据类型转为包装类对象。

​ 作用:转为包装类的对象,是为了使用专门为对象设计的API和特性。

​ 拆箱:把包装类对象拆为基本数据类型。

​ 转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。比较、算术等。

​ 总结:对象(引用数据类型)能用的运算符有哪些?

​ (1)instanceof。

​ (2)=:赋值运算符。

​ (3)==和!=:用于比较地址,但是要求左右两边对象的类型一致或者是有父子类继承关系。

​ (4)对于字符串这一种特殊的对象,支持“+”,表示拼接。

3):包装类的一些API

​ 1、基本数据类型和字符串之间的转换

​ (1)把基本数据类型转为字符串

int a = 10;
//String str = a;//错误的
//方式一:
String str = a + "";
//方式二:
String str = String.valueOf(a);

​ (2)把字符串转为基本数据类型

int a = Integer.parseInt("整数的字符串");
double a = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");

​ 2、数据类型的最大最小值

Integer.MAX_VALUE和Integer.MIN_VALUE
Long.MAX_VALUE和Long.MIN_VALUE
Double.MAX_VALUE和Double.MIN_VALUE

​ 3、转大小写

Character.toUpperCase('x');
Character.toLowerCase('X');

​ 4、转进制

Integer.toBinaryString(int i) 
Integer.toHexString(int i)
Integer.toOctalString(int i)
Integer i = 1;
Integer j = 1;
System.out.println(i == j);//true

Integer i = 128;
Integer j = 128;
System.out.println(i == j);//false

Integer i = new Integer(1);//新new的在堆中
Integer j = 1;//这个用的是缓冲的常量对象,在方法区
System.out.println(i == j);//false

Integer i = new Integer(1);//新new的在堆中
Integer j = new Integer(1);//另一个新new的在堆中
System.out.println(i == j);//false

Integer i = new Integer(1);
int j = 1;
System.out.println(i == j);//true,凡是和基本数据类型比较,都会先拆箱,按照基本数据类型的规则比较
2,字符串
1):字符串的特点

​ 1、字符串String类型本身是final声明的,意味着我们不能继承String。

​ 2、字符串的对象也是不可变对象,意味着一旦进行修改,就会产生新对象。

​ 我们修改了字符串后,如果想要获得新的内容,必须重新接受,如果程序中涉及到大量的字符串的修改操作,那么此时的时空消耗比较高。可能需要考虑使用StringBuilder或StringBuffer。

​ 3、String对象内部是用字符数组进行保存的。

​ JDK1.9之前有一个char[] value数组,JDK1.9之后byte[]数组。

​ 4、String类中这个char[] values数组也是final修饰的,意味着这个数组不可变,然后它是private修饰,外部不能直接操作它,String类型提供的所有的方法都是用新对象来表示修改后内容的,所以保证了String对象的不可变。

​ 5、就因为字符串对象设计为不可变,那么所以字符串有常量池来保存很多常量对象。常量池在方法区。

​ 如果细致的划分:

​ (1)JDK1.6及其之前:方法区

​ (2)JDK1.7:堆

​ (3)JDK1.8:元空间

2): 字符串对象的比较

​ 1、==:比较是对象的地址

​ 只有两个字符串变量都是指向字符串的常量对象时,才会返回true

String str1 = "hello";
String str2 = "hello";
str1 == str2//true

​ 2、equals:比较是对象的内容,因为String类型重写equals,区分大小写

只要两个字符串的字符内容相同,就会返回true。

String str1 = new String("hello");
String str2 = new String("hello");
str1.equals(strs) //true

​ 3、equalsIgnoreCase:比较的是对象的内容,不区分大小写。

String str1 = new String("hello");
String str2 = new String("HELLO");
str1.equalsIgnoreCase(strs) //true

​ 4、compareTo:String类型重写了Comparable接口的抽象方法,自然排序,按照字符的Unicode编码值进行比较大小的,严格区分大小写。

String str1 = "hello";
String str2 = "world";
str1.compareTo(str2) //小于0的值

​ 5、compareToIgnoreCase:不区分大小写,其他按照字符的Unicode编码值进行比较大小

String str1 = new String("hello");
String str2 = new String("HELLO");
str1.compareToIgnoreCase(str2)  //等于0
3):空字符的比较

​ 1、哪些是空字符串,空字符串:长度为0

String str1 = "";
String str2 = new String();
String str3 = new String("");

​ 2、如何判断某个字符串是否是空字符串

if("".equals(str))
if(str!=null  && str.isEmpty())
if(str!=null && str.equals(""))
if(str!=null && str.length()==0)
4): 字符串的对象的个数

​ 1、字符串常量对象

String str1 = "hello";//1个,在常量池中

​ 2、字符串的普通对象

String str2 = new String();
String str22 = new String("");
//两个对象,一个是常量池中的空字符串对象,一个是堆中的空字符串对象

​ 3、字符串的普通对象和常量对象一起

String str3 = new String("hello");
//str3首先指向堆中的一个字符串对象,然后堆中字符串的value数组指向常量池中常量对象的value数组
5):字符串拼接结果

​ 原则:

​ (1)常量+常量:结果是常量池

​ (2)常量与变量 或 变量与变量:结果是堆

​ (3)拼接后调用intern方法:结果在常量池

	@Test
	public void test06(){
		String s1 = "hello";
		String s2 = "world";
		String s3 = "helloworld";
		
		String s4 = (s1 + "world").intern();//把拼接的结果放到常量池中
		String s5 = (s1 + s2).intern();
		
		System.out.println(s3 == s4);//true
		System.out.println(s3 == s5);//true
	}
	
	@Test
	public void test05(){
		final String s1 = "hello";
		final String s2 = "world";
		String s3 = "helloworld";
		
		String s4 = s1 + "world";//s4字符串内容也helloworld,s1是常量,"world"常量,常量+ 常量 结果在常量池中
		String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是常量,常量+ 常量 结果在常量池中
		String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
		
		System.out.println(s3 == s4);//true
		System.out.println(s3 == s5);//true
		System.out.println(s3 == s6);//true
	}
	
	@Test
	public void test04(){
		String s1 = "hello";
		String s2 = "world";
		String s3 = "helloworld";
		
		String s4 = s1 + "world";//s4字符串内容也helloworld,s1是变量,"world"常量,变量 + 常量的结果在堆中
		String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是变量,变量 + 变量的结果在堆中
		String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
		
		System.out.println(s3 == s4);//false
		System.out.println(s3 == s5);//false
		System.out.println(s3 == s6);//true
	}
6):字符串的API

​ 1)boolean isEmpty()

​ (2)int length()

​ (3)String concat(xx):拼接,等价于+

​ (4)boolean contanis(xx)

​ (5)int indexOf():从前往后找,要是没有返回-1

​ (6)int lastIndexOf():从后往前找,要是没有返回-1

​ (7)char charAt(index)

​ (8)new String(char[] ) 或new String(char[] ,int, int)

​ (9)char[] toCharArray()

​ (10)byte[] getBytes():编码,把字符串变为字节数组,按照平台默认的字符编码进行编码

​ byte[] getBytes(字符编码方式):按照指定的编码方式进行编码

​ (11)new String(byte[] ) 或 new String(byte[], int, int):解码,按照平台默认的字符编码进行解码

​ new String(byte[],字符编码方式 ) 或 new String(byte[], int, int,字符编码方式):解码,按照指定的编码方式进行解码

​ (12)String subString(int begin):从[begin]开始到最后

String subString(int begin,int end):从[begin, end)

​ (13)boolean matchs(正则表达式)

​ (14)String replace(xx,xx):不支持正则

String replaceFirst(正则,value):替换第一个匹配部分

String repalceAll(正则, value):替换所有匹配部分

​ (15)String[] split(正则):按照某种规则进行拆分

​ (16)boolean startsWith(xx):是否以xx开头

boolean endsWith(xx):是否以xx结尾

​ (17)String trim():去掉前后空白符,字符串中间的空白符不会去掉

​ (18)String toUpperCase():转大写

​ (19)String toLowerCase():转小写

7):能够更改的字符传的方法

​ 1、可变字符序列:StringBuilder和StringBuffer

​ StringBuffer:老的,线程安全的(因为它的方法有synchronized修饰)

​ StringBuilder:线程不安全的

​ 2、面试题:String和StringBuilder、StringBuffer的区别?

​ String:不可变对象,不可变字符序列

​ StringBuilder、StringBuffer: 可变字符序列

​ 3、常用的API,StringBuilder、StringBuffer的API是完全一致的

​ (1)append(xx):拼接,追加

​ (2)insert(int index, xx):插入

​ (3)delete(int start, int end)

​ deleteCharAt(int index)

​ (4)set(int index, xx)

​ (5)reverse():反转

​ … 替换、截取、查找…

8):日期时间API

​ 1、java.util.Date

new Date():当前系统时间

long getTime():返回该日期时间对象距离1970-1-1 0.0.0 0毫秒之间的毫秒值

new Date(long 毫秒):把该毫秒值换算成日期时间对象

2、java.util.Calendar:

(1)getInstance():得到Calendar的镀锡

(2)get(常量)

3、java.text.SimpleDateFormat:日期时间的格式化

y:表示年

M:月

d:天

H: 小时,24小时制

h:小时,12小时制

m:分

s:秒

S:毫秒

E:星期

D:年当中的天数
案例:

	@Test
	public void test10() throws ParseException{
		String str = "2019年06月06日 16时03分14秒 545毫秒  星期四 +0800";
		SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒  E Z");
		Date d = sf.parse(str);
		System.out.println(d);
	}
	
	@Test
	public void test9(){
		Date d = new Date();

		SimpleDateFormat sf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒  E Z");
		//把Date日期转成字符串,按照指定的格式转
		String str = sf.format(d);
		System.out.println(str);
	}
	
	@Test
	public void test8(){
		String[] all = TimeZone.getAvailableIDs();
		for (int i = 0; i < all.length; i++) {
			System.out.println(all[i]);
		}
	}
	
	@Test
	public void test7(){
		TimeZone t = TimeZone.getTimeZone("America/Los_Angeles");
		//getInstance(TimeZone zone)
		Calendar c = Calendar.getInstance(t);
		System.out.println(c);
	}
	
	@Test
	public void test6(){
		Calendar c = Calendar.getInstance();
		System.out.println(c);
		
		int year = c.get(Calendar.YEAR);
		System.out.println(year);
		
		int month = c.get(Calendar.MONTH)+1;
		System.out.println(month);
		
		//...
	}
	
	@Test
	public void test5(){
		long time = Long.MAX_VALUE;
		Date d = new Date(time);
		System.out.println(d);
	}
	
	@Test
	public void test4(){
		long time = 1559807047979L;
		Date d = new Date(time);
		System.out.println(d);
	}
	@Test
	public void test3(){
		Date d = new Date();
		long time = d.getTime();
		System.out.println(time);//1559807047979
	}
	
	@Test
	public void test2(){
		long time = System.currentTimeMillis();
		System.out.println(time);//1559806982971
		//当前系统时间距离1970-1-1 0:0:0 0毫秒的时间差,毫秒为单位
	}
	
	@Test
	public void test1(){
		Date d = new Date();
		System.out.println(d);
	}

第十一章:集合

1,概念

​ 数据结构:存储数据的某种结构

(1)底层的物理结构

​ ①数组:开辟连续的存储空间,每一个元素使用[下标]进行区别

​ ②链式:不需要开辟连续的存储空间,但是需要“结点”来包装要存储的数据,结点包含两部分内容:

​ A、数据

​ B、记录其他结点的地址,例如:next,pre,left,right,parent等

(2)表现出来的逻辑结构:动态数组、单向链表、双向链表、队列、栈、二叉树、哈希表、图等

2, 手动实现一些逻辑结构

​ 1):动态数组

​ 包含:

​ (1)内部使用一个数组,用来存储数据

​ (2)内部使用一个total,记录实际存储的元素的个数

public class MyArrayList {
	//为什么使用Object,因为只是说这个容器是用来装对象的,但是不知道用来装什么对象。
	private Object[] data;
	private int total;
	
	public MyArrayList(){
		data = new Object[5];
	}
	
	//添加一个元素
	public void add(Object obj){
		//检查是否需要扩容
		checkCapacity();
		data[total++] = obj;
	}

	private void checkCapacity() {
		//如果data满了,就扩容为原来的2倍
		if(total >= data.length){
			data = Arrays.copyOf(data, data.length*2);
		}
	}
	
	//返回实际元素的个数
	public int size(){
		return total;
	}
	
	//返回数组的实际容量
	public int capacity(){
		return data.length;
	}
	
	//获取[index]位置的元素
	public Object get(int index){
		//校验index的合理性范围
		checkIndex(index);
		return data[index];
	}

	private void checkIndex(int index) {
		if(index<0 || index>=total){
			throw new RuntimeException(index+"对应位置的元素不存在");
//			throw new IndexOutOfBoundsException(index+"越界");
		}
	}
	
	//替换[index]位置的元素
	public void set(int index, Object value){
		//校验index的合理性范围
		checkIndex(index);
		
		data[index] = value;
	}
	
	//在[index]位置插入一个元素value
	public void insert(int index, Object value){
		/*
		 * (1)考虑下标的合理性
		 * (2)总长度是否够
		 * (3)[index]以及后面的元素往后移动,把[index]位置腾出来
		 * (4)data[index]=value  放入新元素
		 * (5)total++  有效元素的个数增加
		 */
		
		//(1)考虑下标的合理性:校验index的合理性范围
		checkIndex(index);
		
		//(2)总长度是否够:检查是否需要扩容
		checkCapacity();
		
		//(3)[index]以及后面的元素往后移动,把[index]位置腾出来
		/*
		 * 假设total = 5, data.length= 10, index= 1
		 * 有效元素的下标[0,4]
		 * 移动:[1]->[2],[2]->[3],[3]->[4],[4]->[5]
		 * 移动元素的个数:total-index
		 */
		System.arraycopy(data, index, data, index+1, total-index);
		
		//(4)data[index]=value  放入新元素
		data[index] = value;
		
		//(5)total++  有效元素的个数增加
		total++;
	}
	
	//返回所有实际存储的元素
	public Object[] getAll(){
		//返回total个
		return Arrays.copyOf(data, total);
	}
	
	//删除[index]位置的元素
	public void remove(int index){
		/*
		 * (1)校验index的合理性范围
		 * (2)移动元素,把[index+1]以及后面的元素往前移动
		 * (3)把data[total-1]=null  让垃圾回收器尽快回收
		 * (4)总元素个数减少 total--
		 */
		
		//(1)考虑下标的合理性:校验index的合理性范围
		checkIndex(index);
		
		//(2)移动元素,把[index+1]以及后面的元素往前移动
		/*
		 * 假设total=8, data.length=10, index = 3
		 * 有效元素的范围[0,7]
		 * 移动:[4]->[3],[5]->[4],[6]->[5],[7]->[6]
		 * 移动了4个:total-index-1
		 */
		System.arraycopy(data, index+1, data, index, total-index-1);
		
		//(3)把data[total-1]=null  让垃圾回收器尽快回收
		data[total-1] = null;
		
//		(4)总元素个数减少 total--
		total--;
	}
	
	//查询某个元素的下标
	public int indexOf(Object obj){
		if(obj == null){
			for (int i = 0; i < total; i++) {
				if(data[i] == null){//等价于 if(data[i] == obj)
					return i;
				}
			}
		}else{
			for (int i = 0; i < data.length; i++) {
				if(obj.equals(data[i])){
					return i;
				}
			}
		}
		return -1;
	}
	
	//删除数组中的某个元素
	//如果有重复的,只删除第一个
	public void remove(Object obj){
		/*
		 * (1)先查询obj的[index]
		 * (2)如果存在,就调用remove(index)删除就可以
		 */
		
		//(1)先查询obj的[index]
		int index = indexOf(obj);
		
		if(index != -1){
			remove(index);
		}
		//不存在,可以什么也不做
		//不存在,也可以抛异常
		//throw new RuntimeException(obj + "不存在");
	}
	
	public void set(Object old, Object value){
		/*
		 * (1)查询old的[index]
		 * (2)如果存在,就调用set(index, value)
		 */
		
//		(1)查询old的[index]
		int index = indexOf(old);
		if(index!=-1){
			set(index, value);
		}
		
		//不存在,可以什么也不做
	}
}

2、单向链表

包含:

(1)包含一个Node类型的成员变量first:用来记录第一个结点的地址

如果这个链表是空的,还没有任何结点,那么first是null。

最后一个结点的特征:就是它的next是null

(2)内部使用一个total,记录实际存储的元素的个数

(3)使用了一个内部类Node

private class Node{
    Object data;
    Node next;
}
public class SingleLinkedList {
	//这里不需要数组,不需要其他的复杂的结构,我只要记录单向链表的“头”结点
	private Node first;//first中记录的是第一个结点的地址
	private int total;//这里我记录total是为了后面处理的方便,例如:当用户获取链表有效元素的个数时,不用现数,而是直接返回total等
	
	/*
	 * 内部类,因为这种Node结点的类型,在别的地方没有用,只在单向链表中,用于存储和表示它的结点关系。
	 * 因为我这里涉及为内部类型。
	 */
	private class Node{
		Object data;//因为数据可以是任意类型的对象,所以设计为Object
		Node next;//因为next中记录的下一个结点的地址,因此类型是结点类型
		//这里data,next没有私有化,是希望在外部类中可以不需要get/set,而是直接“结点对象.data","结点对象.next"使用
		Node(Object data, Node next){
			this.data = data;
			this.next = next;
		}
	}
	
	public void add(Object obj){
		/*
		 * (1)把obj的数据,包装成一个Node类型结点对象
		 * (2)把新结点“链接”当前链表的最后
		 * ①当前新结点是第一个结点
		 * 如何判断是否是第一个   if(first==null)说明暂时还没有第一个
		 * ②先找到目前的最后一个,把新结点链接到它的next中
		 * 如何判断是否是最后一个   if(某个结点.next == null)说明这个结点是最后一个
		 */
//		(1)把obj的数据,包装成一个Node类型结点对象
		//这里新结点的next赋值为null,表示新结点是最后一个结点
		Node newNode = new Node(obj, null);
		
		//①当前新结点是第一个结点
		if(first == null){
			//说明newNode是第一个
			first = newNode;
		}else{
			//②先找到目前的最后一个,把新结点链接到它的next中
			Node node = first;
			while(node.next != null){
				node = node.next;
			}
			//退出循环时node指向最后一个结点
			
			//把新结点链接到它的next中
			node.next = newNode;
		}
		
		total++;
	}
	
	public int size(){
		return total;
	}
	
	public Object[] getAll(){
		//(1)创建一个数组,长度为total
		Object[] all = new Object[total];
		
		//(2)把单向链表的每一个结点中的data,拿过来放到all数组中
		Node node = first;
		for (int i = 0; i < total; i++) {
//			all[i] = 结点.data;
			all[i] = node.data;
			//然后node指向下一个
			node = node.next;
		}
		
		//(3)返回数组
		return all;
	}
	
	public void remove(Object obj){
		if(obj == null){
			//(1)先考虑是否是第一个
			if(first!=null){//链表非空
				
				//要删除的结点正好是第一个结点
				if(first.data == null){
					//让第一个结点指向它的下一个
					first = first.next;
					total--;
					return;
				}
				
				//要删除的不是第一个结点
				Node node = first.next;//第二个结点
				Node last = first;
				while(node.next!=null){//这里不包括最后一个,因为node.next==null,不进入循环,而node.next==null是最后一个
					if(node.data == null){
						last.next = node.next;
						total--;
						return;
					}
					last = node;
					node = node.next;
				}
				
				//单独判断最后一个是否是要删除的结点
				if(node.data == null){
					//要删除的是最后一个结点
					last.next = null;
					total--;
					return;
				}
			}
		}else{
			//(1)先考虑是否是第一个
			if(first!=null){//链表非空
				
				//要删除的结点正好是第一个结点
				if(obj.equals(first.data)){
					//让第一个结点指向它的下一个
					first = first.next;
					total--;
					return;
				}
				
				//要删除的不是第一个结点
				Node node = first.next;//第二个结点
				Node last = first;
				while(node.next!=null){//这里不包括最后一个,因为node.next==null,不进入循环,而node.next==null是最后一个
					if(obj.equals(node.data)){
						last.next = node.next;
						total--;
						return;
					}
					last = node;
					node = node.next;
				}
				
				//单独判断最后一个是否是要删除的结点
				if(obj.equals(node.data)){
					//要删除的是最后一个结点
					last.next = null;
					total--;
					return;
				}
			}
		}
	}

	public int indexOf(Object obj){
		if(obj == null){
			Node node = first;
			for (int i = 0; i < total; i++) {
				if(node.data == null){
					return i;
				}
				node = node.next;
			}
		}else{
			Node node = first;
			for (int i = 0; i < total; i++) {
				if(obj.equals(node.data)){
					return i;
				}
				node = node.next;
			}
		}
		return -1;
	}
}
3,Collection

​ 因为集合的类型很多,那么我们把它们称为集合框架。

​ 集合框架分为两个家族:Collection(一组对象)和Map(一组映射关系、一组键值对)。

Collection是代表一种对象的集合。它是Collection系列的根接口。

它们虽然:有些可能是有序的,有些可能是无序的,有些可能可以重复的,有些不能重复的,但是它们有共同的操作规范,因此这些操作的规范就抽象为了Collection接口。

常用方法:

(1)boolean add(Object obj):添加一个

(2)boolean addAll(Collection c):添加多个

(3)boolean remove(Object obj):删除一个

(4)boolean removeAll(Collection c ): 删除多个

(5)boolean contains(Object c):是否包含某个

(6)boolean containsAll(Collection c): 是否包含所有

(7)boolean isEmpty():是否为空

(8)int size():获取元素个数

(9)void clear():清空集合

(10)Object[] toArray():获取所有元素

(11)Iterator iterator(): 获取遍历当前集合的迭代器对象

(12)retainAll(Collection c):求当前集合与c集合的交集

4,List
1):List概述

List:是Collection的子接口。

List系列的集合:有序的、可重复的

List系列的常用集合:ArrayList、Vector、LinkedList、Stack

2):List的API

常用方法:

(1)boolean add(Object obj):添加一个

(2)boolean addAll(Collection c):添加多个

(3)void add(int index, Object obj):添加一个,指定位置添加

(4)void addAll(int index, Collection c):添加多个

(5)boolean remove(Object obj):删除一个

(6)Object remove(int index):删除指定位置的元素,并返回刚刚删除的元素

(7)boolean removeAll(Collection c ): 删除多个

(8)boolean contains(Object c):是否包含某个

(9)boolean containsAll(Collection c): 是否包含所有

(10)boolean isEmpty():是否为空

(11)int size():获取元素个数

(12)void clear():清空集合

(13)Object[] toArray():获取所有元素

(14)Iterator iterator(): 获取遍历当前集合的迭代器对象

(15)retainAll(Collection c):求当前集合与c集合的交集

(16)ListIterator listIterator():获取遍历当前集合的迭代器对象,这个迭代器可以往前、往后遍历

(17)ListIterator listIterator(int index):从[index]位置开始,往前或往后遍历

(18)Object get(int index):返回index位置的元素

(19)List subList(int start, int end):截取[start,end)部分的子列表

3):List的实现类们的区别

ArrayList、Vector、LinkedList、Stack

(1)ArrayList、Vector:都是动态数组

Vector是最早版本的动态数组,线程安全的,默认扩容机制是2倍,支持旧版的迭代器Enumeration

ArrayList是后增的动态数组,线程不安全的,默认扩容机制是1.5倍

(2)动态数组与LinkedList的区别

动态数组:底层物理结构是数组

​ 优点:根据[下标]访问的速度很快

​ 缺点:需要开辟连续的存储空间,而且需要扩容,移动元素等操作

LinkedList:底层物理结构是双向链表

​ 优点:在增加、删除元素时,不需要移动元素,只需要修改前后元素的引用关系

​ 缺点:我们查找元素时,只能从first或last开始查找

(3)Stack:栈

是Vector的子类。比Vector多了几个方法,能够表现出“先进后出或后进先出”的特点。

①Object peek():访问栈顶元素

②Object pop():弹出栈顶元素

③push():把元素压入栈顶

(4)LinkedList可以作为很多种数据结构使用

单链表:只关注next就可以

队列:先进先出,找对应的方法

双端队列(JDK1.6加入):两头都可以进出,找对应的方法

栈:先进后出,找对应的方法

建议:虽然LinkedList是支持对索引进行操作,因为它实现List接口的所有方法,但是我们不太建议调用类似这样的方法,因为效率比较低。

4):源码分析

​ (1)Vector

    public Vector() {
        this(10);//指定初始容量initialCapacity为10
    }
	public Vector(int initialCapacity) {
        this(initialCapacity, 0);//指定capacityIncrement增量为0
    }
    public Vector(int initialCapacity, int capacityIncrement增量为0) {
        super();
        //判断了形参初始容量initialCapacity的合法性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        //创建了一个Object[]类型的数组
        this.elementData = new Object[initialCapacity];//默认是10
        //增量,默认是0,如果是0,后面就按照2倍增加,如果不是0,后面就按照你指定的增量进行增量
        this.capacityIncrement = capacityIncrement;
    }

//synchronized意味着线程安全的   
	public synchronized boolean add(E e) {
        modCount++;
    	//看是否需要扩容
        ensureCapacityHelper(elementCount + 1);
    	//把新的元素存入[elementCount],存入后,elementCount元素的个数增1
        elementData[elementCount++] = e;
        return true;
    }

    private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        //看是否超过了当前数组的容量
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);//扩容
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;//获取目前数组的长度
        //如果capacityIncrement增量是0,新容量 = oldCapacity的2倍
        //如果capacityIncrement增量是不是0,新容量 = oldCapacity + capacityIncrement增量;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        
        //如果按照上面计算的新容量还不够,就按照你指定的需要的最小容量来扩容minCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        
        //如果新容量超过了最大数组限制,那么单独处理
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        
        //把旧数组中的数据复制到新数组中,新数组的长度为newCapacity
        elementData = Arrays.copyOf(elementData, newCapacity);
    }


    public boolean remove(Object o) {
        return removeElement(o);
    }
    public synchronized boolean removeElement(Object obj) {
        modCount++;
        //查找obj在当前Vector中的下标
        int i = indexOf(obj);
        //如果i>=0,说明存在,删除[i]位置的元素
        if (i >= 0) {
            removeElementAt(i);
            return true;
        }
        return false;
    }
    public int indexOf(Object o) {
        return indexOf(o, 0);
    }
    public synchronized int indexOf(Object o, int index) {
        if (o == null) {//要查找的元素是null值
            for (int i = index ; i < elementCount ; i++)
                if (elementData[i]==null)//如果是null值,用==null判断
                    return i;
        } else {//要查找的元素是非null值
            for (int i = index ; i < elementCount ; i++)
                if (o.equals(elementData[i]))//如果是非null值,用equals判断
                    return i;
        }
        return -1;
    }
    public synchronized void removeElementAt(int index) {
        modCount++;
        //判断下标的合法性
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        
        //j是要移动的元素的个数
        int j = elementCount - index - 1;
        //如果需要移动元素,就调用System.arraycopy进行移动
        if (j > 0) {
            //把index+1位置以及后面的元素往前移动
            //index+1的位置的元素移动到index位置,依次类推
            //一共移动j个
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        //元素的总个数减少
        elementCount--;
        //将elementData[elementCount]这个位置置空,用来添加新元素,位置的元素等着被GC回收
        elementData[elementCount] = null; /* to let gc do its work */
    }

​ (2)ArrayList

JDK1.6:

    public ArrayList() {
		this(10);//指定初始容量为10
    }
    public ArrayList(int initialCapacity) {
		super();
        //检查初始容量的合法性
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        //数组初始化为长度为initialCapacity的数组
		this.elementData = new Object[initialCapacity];
    }

JDK1.7

    private static final int DEFAULT_CAPACITY = 10;//默认初始容量10
	private static final Object[] EMPTY_ELEMENTDATA = {};
	public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;//数组初始化为一个空数组
    }
    public boolean add(E e) {
        //查看当前数组是否够多存一个元素
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {//如果当前数组还是空数组
            //minCapacity按照 默认初始容量和minCapacity中的的最大值处理
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
		//看是否需要扩容处理
        ensureExplicitCapacity(minCapacity);
    }
	//...

JDK1.8

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;//初始化为空数组
    }
    public boolean add(E e) {
        //查看当前数组是否够多存一个元素
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        
        //存入新元素到[size]位置,然后size自增1
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        //如果当前数组还是空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //那么minCapacity取DEFAULT_CAPACITY与minCapacity的最大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
		//查看是否需要扩容
        ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//修改次数加1

        // 如果需要的最小容量  比  当前数组的长度  大,即当前数组不够存,就扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;//当前数组容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);//新数组容量是旧数组容量的1.5倍
        //看旧数组的1.5倍是否够
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //看旧数组的1.5倍是否超过最大数组限制
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        
        //复制一个新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }


    public boolean remove(Object o) {
        //先找到o在当前ArrayList的数组中的下标
        //分o是否为空两种情况讨论
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {//null值用==比较
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {//非null值用equals比较
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    private void fastRemove(int index) {
        modCount++;//修改次数加1
        //需要移动的元素个数
        int numMoved = size - index - 1;
        
        //如果需要移动元素,就用System.arraycopy移动元素
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        
        //将elementData[size-1]位置置空,让GC回收空间,元素个数减少
        elementData[--size] = null; // clear to let GC do its work
    }
	




    public E remove(int index) {
        rangeCheck(index);//检验index是否合法

        modCount++;//修改次数加1
        
        //取出[index]位置的元素,[index]位置的元素就是要被删除的元素,用于最后返回被删除的元素
        E oldValue = elementData(index);
        
		//需要移动的元素个数
        int numMoved = size - index - 1;
        
        //如果需要移动元素,就用System.arraycopy移动元素
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //将elementData[size-1]位置置空,让GC回收空间,元素个数减少
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }


    public E set(int index, E element) {
        rangeCheck(index);//检验index是否合法

        //取出[index]位置的元素,[index]位置的元素就是要被替换的元素,用于最后返回被替换的元素
        E oldValue = elementData(index);
        //用element替换[index]位置的元素
        elementData[index] = element;
        return oldValue;
    }
    public E get(int index) {
        rangeCheck(index);//检验index是否合法

        return elementData(index);//返回[index]位置的元素
    }


    public int indexOf(Object o) {
        //分为o是否为空两种情况
        if (o == null) {
            //从前往后找
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }
    public int lastIndexOf(Object o) {
         //分为o是否为空两种情况
        if (o == null) {
            //从后往前找
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

​ (3),LinkedList

int size = 0;
Node<E> first;//记录第一个结点的位置
Node<E> last;//记录最后一个结点的位置

    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;
        }
    }


    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;
    }

5,Set概述

Set系列的集合:不可重复的

Set系列的集合,有有序的也有无序的。HashSet无序的,TreeSet按照元素的大小顺序遍历,LinkedHashSet按照元素的添加顺序遍历。

1):HashSet:

​ 底层是HashMap实现。添加到HashSet的元素是作为HashMap的key,value是一个Object类型的常量对象PRESENT。

​ 依赖于元素的hashCode()和equals()保证元素的不可重复,且是无序的,存储位置和hashCode()值有关,根据hashCode()来算出它在底层table数组中的[index]

2):TreeSet

​ 底层是TreeMap实现。添加到TreeSet的元素是作为TreeMap的key,value是一个Object类型的常量对象PRESENT。

​ 依赖于元素的大小,要么是java.lang.Comparable接口compareTo(Object obj),要么是java.util.Comparator接口的compare(Object o1, Object o2)来比较元素的大小。认为大小相等的两个元素就是重复元素。

3):LinkedHashSet

​ 底层是LinkedHashMap。添加到LinkedHashSet的元素是作为LinkedHashMap的key,value是一个Object类型的常量对象PRESENT。

​ LinkedHashSet是HashSet的子类,比父类多维护了元素的添加顺序。

​ 当且仅当,你既想要元素不可重复,又要保证元素的添加顺序时,再使用它。

6,Map
1):Map概述

​ 用来存储键值对,映射关系的集合。所有的Map的key都不能重复。

键值对、映射关系的类型:Entry类型。

Entry接口是Map接口的内部接口。所有的Map的键值对的类型都实现了这个接口。
HashMap中的映射关系,是有一个内部类来实现Entry的接口,JDK1.7是一个叫做Entry的内部类实现Entry接口。
JDK1.8是一个叫做Node的内部类实现Entry接口。
TreeMap中的映射关系,是有一个内部类Entry来实现Entry的接口
2): API

(1)put(Object key, Object value):添加一对映射关系

(2)putAll(Map m):添加多对映射关系

(3)clear():清空map

(4)remove(Object key):根据key删除一对

(5)int size():获取有效元素的对数

(6)containsKey(Object key):是否包含某个key

(7)containsValue(Object value):是否包含某个value

(8)Object get(Object key):根据key获取value

(9)遍历相关的几个方法

Collection values():获取所有的value进行遍历

Set keySet():获取所有key进行遍历

Set entrySet():获取所有映射关系进行遍历

3 ):Map的实现类们的区别

1)HashMap:

​ 依据key的hashCode()和equals()来保证key是否重复。

​ key如果重复,新的value会替换旧的value。

​ hashCode()决定了映射关系在table数组中的存储的位置,index = hash(key.hashCode()) & table.length-1

​ HashMap的底层实现:JDK1.7是数组+链表;JDK1.8是数组+链表/红黑树

(2)TreeMap

​ 依据key的大小来保证key是否重复。key如果重复,新的value会替换旧的value。

​ key的大小依赖于,java.lang.Comparable或java.util.Comparator。

(3)LinkedHashMap

​ 依据key的hashCode()和equals()来保证key是否重复。key如果重复,新的value会替换旧的value。

​ LinkedHashMap是HashMap的子类,比HashMap多了添加顺序

4):关于HashMap的面试问题

​ 1、HashMap的底层实现

答:JDK1.7是数组+链表,JDK1.8是数组+链表/红黑树

2、HashMap的数组的元素类型

答:java.util.Map$Entry接口类型。

JDK1.7的HashMap中有内部类Entry实现Entry接口

JDK1.8的HashMap中有内部类Node和TreeNode类型实现Entry接口

3、为什么要使用数组?

答:因为数组的访问的效率高

4、为什么数组还需要链表?或问如何解决hash或[index]冲突问题?

答:为了解决hash和[index]冲突问题

(1)两个不相同的key的hashCode值本身可能相同

(2)两个hashCode不相同的key,通过hash(key)以及 hash & table.length-1运算得到的[index]可能相同

那么意味着table[index]下可能需要存储多个Entry的映射关系对象,所以需要链表

5、HashMap的数组的初始化长度

答:默认的初始容量值是16

6、HashMap的映射关系的存储索引index如何计算

答:hash & table.length-1

7、为什么要使用hashCode()? 空间换时间

答:因为hashCode()是一个整数值,可以用来直接计算index,效率比较高,用数组这种结构虽然会浪费一些空间,但是可以提高查询效率。

8、hash()函数的作用是什么

答:在计算index之前,会对key的hashCode()值,做一个hash(key)再次哈希的运算,这样可以使得Entry对象更加散列的存储到table中

JDK1.8关于hash(key)方法的实现比JDK1.7要简洁。 key.hashCode() ^ key.Code()>>>16; 因为这样可以使得hashCode的高16位信息也能参与到运算中来

9、HashMap的数组长度为什么一定要是2的幂次方

答:因为2的n次方-1的二进制值是前面都0,后面几位都是1,这样的话,与hash进行&运算的结果就能保证在[0,table.length-1]范围内,而且是均匀的。

10、HashMap 为什么使用 &按位与运算代替%模运算?

答:因为&效率高

11、HashMap的数组什么时候扩容?

答:JDK1.7版:当要添加新Entry对象时发现(1)size达到threshold(2)table[index]!=null时,两个条件同时满足会扩容

JDK1.8版:当要添加新Entry对象时发现(1)size达到threshold(2)当table[index]下的结点个数达到8个但是table.length又没有达到64。两种情况满足其一都会导致数组扩容

而且数组一旦扩容,不管哪个版本,都会导致所有映射关系重新调整存储位置。

12、如何计算扩容阈值(临界值)?

答:threshold = capacity * loadfactor

13、loadFactor为什么是0.75,如果是1或者0.1呢有什么不同?

答:1的话,会导致某个table[index]下面的结点个数可能很长

0.1的话,会导致数组扩容的频率太高

14、JDK1.8的HashMap什么时候树化?

答:当table[index]下的结点个数达到8个但是table.length已经达到64

15、JDK1.8的HashMap什么时候反树化?

答:当table[index]下的树结点个数少于6个

16、JDK1.8的HashMap为什么要树化?

答:因为当table[index]下的结点个数超过8个后,查询效率就低下了,修改为红黑树的话,可以提高查询效率

17、JDK1.8的HashMap为什么要反树化?

答:因为因为当table[index]下树的结点个数少于6个后,使用红黑树反而过于复杂了,此时使用链表既简洁又效率也不错

18、作为HashMap的key类型重写equals和hashCode方法有什么要求

​ (1)equals与hashCode一起重写

​ (2)重写equals()方法,但是有一些注意事项;

自反性:x.equals(x)必须返回true。
对称性:x.equals(y)与y.equals(x)的返回值必须相等。
传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。
一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变。
非null:x不是null,y为null,则x.equals(y)必须为false。
​ (3)重写hashCode()的注意事项

如果equals返回true的两个对象,那么hashCode值一定相同,并且只要参与equals判断属性没有修改,hashCode值也不能修改;
如果equals返回false的两个对象,那么hashCode值可以相同也可以不同;
如果hashCode值不同的,equals一定要返回false;
hashCode不宜过简单,太简单会导致冲突严重,hashCode也不宜过于复杂,会导致性能低下;
19、为什么大部分 hashcode 方法使用 31?

答:因为31是一个不大不小的素数

20、请问已经存储到HashMap中的key的对象属性是否可以修改?为什么?

答:如果该属性参与hashCode的计算,那么不要修改。因为一旦修改hashCode()已经不是原来的值。
而存储到HashMap中时,key的hashCode()–>hash()–>hash已经确定了,不会重新计算。用新的hashCode值再查询get(key)/删除remove(key)时,算的hash值与原来不一样就不找不到原来的映射关系了。

21、所以为什么,我们实际开发中,key的类型一般用String和Integer

答:因为他们不可变。

22、为什么HashMap中的Node或Entry类型的hash变量与key变量加final声明?

答:因为不希望你修改hash和key值

23、为什么HashMap中的Node或Entry类型要单独存储hash?

答:为了在添加、删除、查找过程中,比较hash效率更高,不用每次重新计算key的hash值

24、请问已经存储到HashMap中的value的对象属性是否可以修改?为什么?

答:可以。因为我们存储、删除等都是根据key,和value无关。

25、如果key是null是如何存储的?

答:会存在table[0]中

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值