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自动升级为intchar 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=2int j = 1;
++j;//j=2int a = 1;
int b = a++;//(1)先取a的值“1”放操作数栈(2)a再自增,a=2(3)再把操作数栈中的"1"赋值给b,b=1int m = 1;
int n = ++m;//(1)m先自增,m=2(2)再取m的值“2”放操作数栈(3)再把操作数栈中的"2"赋值给n,n=2int 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的值

(1)boolean类型
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);//truechar 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++){//每次循环的步幅是1if(i%2==0){System.out.println(i);}
}//遍历1-100之间的偶数
for(int i=2; i<=100; i+=2){//每次循环的步幅是2System.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=4j的起点是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/setpublic 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换成isreturn 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@Overridepublic 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是否可以捕获异常,也不管try和catch中是不是有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());  //获取异常信息,就是上面传的messagee.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() {@Overridepublic 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() {@Overridepublic 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或新特性(例如泛型),那么基本数据类型的数据就需要用包装类来包装。

序号 基本数据类型 包装类
1 byte Byte
2 short Short
3 int Integer
4 long Long
5 float Float
6 double Double
7 char Character
8 boolean Boolean
9 void Void
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);//trueInteger i = 128;
Integer j = 128;
System.out.println(i == j);//falseInteger i = new Integer(1);//新new的在堆中
Integer j = 1;//这个用的是缓冲的常量对象,在方法区
System.out.println(i == j);//falseInteger i = new Integer(1);//新new的在堆中
Integer j = new Integer(1);//另一个新new的在堆中
System.out.println(i == j);//falseInteger 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方法:结果在常量池

 @Testpublic 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);//trueSystem.out.println(s3 == s5);//true}@Testpublic 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);//trueSystem.out.println(s3 == s5);//trueSystem.out.println(s3 == s6);//true}@Testpublic 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);//falseSystem.out.println(s3 == s5);//falseSystem.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:年当中的天数
案例:

 @Testpublic 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);}@Testpublic 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);}@Testpublic void test8(){String[] all = TimeZone.getAvailableIDs();for (int i = 0; i < all.length; i++) {System.out.println(all[i]);}}@Testpublic void test7(){TimeZone t = TimeZone.getTimeZone("America/Los_Angeles");//getInstance(TimeZone zone)Calendar c = Calendar.getInstance(t);System.out.println(c);}@Testpublic 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);//...}@Testpublic void test5(){long time = Long.MAX_VALUE;Date d = new Date(time);System.out.println(d);}@Testpublic void test4(){long time = 1559807047979L;Date d = new Date(time);System.out.println(d);}@Testpublic void test3(){Date d = new Date();long time = d.getTime();System.out.println(time);//1559807047979}@Testpublic void test2(){long time = System.currentTimeMillis();System.out.println(time);//1559806982971//当前系统时间距离1970-1-1 0:0:0 0毫秒的时间差,毫秒为单位}@Testpublic 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]位置插入一个元素valuepublic 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;//因为数据可以是任意类型的对象,所以设计为ObjectNode 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)创建一个数组,长度为totalObject[] 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元素的个数增1elementData[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 codeint oldCapacity = elementData.length;//获取目前数组的长度//如果capacityIncrement增量是0,新容量 = oldCapacity的2倍//如果capacityIncrement增量是不是0,新容量 = oldCapacity + capacityIncrement增量;int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);//如果按照上面计算的新容量还不够,就按照你指定的需要的最小容量来扩容minCapacityif (newCapacity - minCapacity < 0)newCapacity = minCapacity;//如果新容量超过了最大数组限制,那么单独处理if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);//把旧数组中的数据复制到新数组中,新数组的长度为newCapacityelementData = 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;//默认初始容量10private 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自增1elementData[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 codeint 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 workreturn 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对应的结点xfor (Node<E> x = first; x != null; x = x.next) {if (x.item == null) {unlink(x);//删除x结点return true;}}} else {//找到o对应的结点xfor (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]中

java开发必备基础相关推荐

  1. 博学谷java题库判断_博学谷Java开发面试基础笔试题及答案分享

    博学谷Java开发面试基础笔试题分享:char 型变量中能不能存贮一个中文汉字?为什么?"=="和 equals 方法究竟有什么区别?静态变量和实例变量的区别?是否可以从一个 st ...

  2. eclipse java开发插件_10大Java开发必备的Eclipse插件

    原标题:10大Java开发必备的Eclipse插件 今天小编来给大家介绍10个Java开发人员必备的Eclipse 插件,它们有各自的优势,你可以从中选择适合你的那款. 1. EGit EGit是 J ...

  3. Java开发必备技巧

    1 调试技巧 一般的IDE工具都有以下调试功能,本文以 IntelliJ IDEA 为例 1.1 计算表达式 以下是实际开发中很容易遇到的一种场景:调试下面的代码时,validate() 返回值为tr ...

  4. 超超超级详细的java入门必备基础知识

    一.JAVA运行机制 Java程序的运行必须经过编写.编译.运行三个步骤. 编写是指在Java开发环境中进行程序代码的输入,最终形成后缀名为.java的Java源文件. 编译是指使用Java编译器(J ...

  5. Java开发环境基础配置

    开发环境基础配置 概述 安装JDK 配置JDK(选做) 如何配置 为什么要配置 第一个Java程序 hello world案例 安装IDEA IDEA的使用(重要) 第一行IDEA代码 创建Proje ...

  6. 4 万字超强总结!Java 这些必备基础知识不可少

    点击「关注」公众号,回复"1024"获取2TB学习资源! 什么是Java Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承.指针等概 ...

  7. JAVA开发语言基础

    很多时候我们都不知道,为什么android最开始要用java作为基础语言 看看知乎上都是怎么回答的 为什么java最初成为android开发的基础语言 ------------------------ ...

  8. java 置顶_[置顶]java开发之基础篇2

    一.java开发环境的搭建 下载和安装jdk.版本自己看着办! 1 JAVA_HOME C:\Program Files\Java\jdk1.7.0_25 2 path C:\Program File ...

  9. 高级JAVA开发必备技能:java8 新日期时间API((一)JSR-310:ZoneId 时区和偏移量)(JAVA 小虚竹)

    技术活,该赏 点赞再看,养成习惯 大家好,我是小虚竹.之前有粉丝私聊我,问能不能把JAVA8 新的日期时间API(JSR-310)知识点梳理出来.答案是肯定的,谁让我宠粉呢.由于内容偏多(超十万字了) ...

最新文章

  1. ZeroMq实现跨线程通信
  2. http://alvinalexander.com/java/jwarehouse/hibernat
  3. iscsi发起程序找不到目标_3分钟学会程序员“面试回答规范”,不怕找不到工作的里面请...
  4. zoj 1152 A Mathematical Curiosity
  5. SleuthQL 一个自动化执行导出扫描结果的库
  6. Android 实现系统更新功能
  7. 互信息的数学解释以及matlab编程
  8. html css js编程顺序,html css js先学哪个呢?
  9. java 遍历属性文件路径_Java项目中读取properties文件,以及六种获取路径的方法...
  10. Android P 开发者预览版
  11. 阿里第三财季:下沉市场再立功 疫情之下祸福相依
  12. oracle物理索引结构,oracle学习笔记(6)oracle物理结构--索引
  13. 关于局云混合的新思路
  14. 课程设计旅游景点咨询系统
  15. 动易自定义标签HTML输出,动易标签【ArticleList】
  16. Python压缩解压–zipfile
  17. maven 使用assembly 进行打包
  18. 西安电子科技大学和东北大学计算机,西安电子科技大学和东北大学比较,哪个好,特别是计算机软件方面...
  19. python爬虫---拉勾网与前程无忧网招聘数据获取(多线程,数据库,反爬虫应对)
  20. 麻将判断胡牌 java_麻将胡牌逻辑 java

热门文章

  1. json与xml的相互转换
  2. 让荣之学告诉你Shopee选品思路及运营方法!
  3. 安卓手机玩游戏卡顿怎么解决_安卓手机卡顿如何解决?教你四招,流畅度立刻飙升!...
  4. 利用好 git bisect 这把利器,帮助你快速定位疑难 Bug
  5. java集成微信支付
  6. 35岁技术人如何转型做管理?mysql删除数据语句
  7. java陆小凤的游戏_陆小凤之金鹏王朝游戏
  8. 手游开发攻防——二、基础篇
  9. 基于Docker的交互式人脸识别应用
  10. 基于Vue的图片裁剪 vue-cropper