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的值
(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);//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是否可以捕获异常,也不管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()); //获取异常信息,就是上面传的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或新特性(例如泛型),那么基本数据类型的数据就需要用包装类来包装。
序号 | 基本数据类型 | 包装类 |
---|---|---|
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);//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]中