JAVA基础

笔记来源:本人在B站观看尚硅谷宋红康老师的Java教程视频,在宋老师的笔记基础上补充了一些自己的笔记,仅供参考

java语言概述

java语言概述

image-20220329213805753

1.基础常识

软件:即一系列按照特定顺序组织的计算机数据和指令的集合。分为:系统软件 和 应用软件
系统软件:windows , mac os , linux ,unix,android,ios,....
应用软件:word ,ppt,画图板,...
人机交互方式: 图形化界面 vs 命令行方式
应用程序 = 算法 + 数据结构
常用DOS命令:

image-20220329214106684

2.计算机语言的发展迭代史

第一代:机器语言
第二代:汇编语言
第三代:高级语言

面向过程:C,Pascal、Fortran
面向对象:Java,JS,Python,Scala,...

3.Java语言版本迭代概述

1991年 Green项目,开发语言最初命名为Oak (橡树)
1994年,开发组意识到Oak 非常适合于互联网
1996年,发布JDK 1.0,约8.3万个网页应用Java技术来制作
1997年,发布JDK 1.1,JavaOne会议召开,创当时全球同类会议规模之最
1998年,发布JDK 1.2,同年发布企业平台J2EE
1999年,Java分成J2SE、J2EE和J2ME,JSP/Servlet技术诞生
2004年,发布里程碑式版本:JDK 1.5,为突出此版本的重要性,更名为JDK 5.0
2005年,J2SE -> JavaSE,J2EE -> JavaEE,J2ME -> JavaME
2009年,Oracle公司收购SUN,交易价格74亿美元
2011年,发布JDK 7.0
2014年,发布JDK 8.0,是继JDK 5.0以来变化最大的版本
2017年,发布JDK 9.0,最大限度实现模块化
2018年3月,发布JDK 10.0,版本号也称为18.3
2018年9月,发布JDK 11.0,版本号也称为18.9

4.Java语言应用的领域:

Java Web开发:后台开发

大数据开发:

Android应用程序开发:客户端开发

5.Java语言的特点

面向对象性:
两个要素:类、对象
三个特征:封装、继承、多态
健壮性:① 去除了C语言中的指针 ②自动的垃圾回收机制 -->仍然会出现内存溢出、内存泄漏
跨平台型:write once,run anywhere:一次编译,到处运行
功劳归功于:JVM

image-20220329214034339

开发环境搭建

1.1 JDK(Java Development Kit)、JRE(Java Runtime Environment)、JVM(ava Virtual Machine)的关系

image-20220329214238446

1.2 JDK的下载、安装

下载:官网,github

安装:傻瓜式安装:JDK 、JRE
注意问题:安装软件的路径中不能包含中文、空格。

1.3 path环境变量的配置

1.3.1 为什么配置path环境变量?

path环境变量:windows操作系统执行命令时所要搜寻的路径

为什么要配置path:希望java的开发工具(javac.exe,java.exe)在任何的文件路径下都可以执行成功。

1.3.2 如何配置?

image-20220329214314421

第一个Java程序

1.开发体验——HelloWorld

image-20220329214415241

1.1 编写

创建一个java源文件:HelloWorld.java

class HelloChina{
public static void main(String[] args){
System.out.println("Hello,World!");
}
}
1.2 编译:

javac HelloChina.java

1.3 运行:

java HelloChina

2.常见问题的解决

image-20220329214508454

image-20220329214516537

3.总结第一个程序

  1. java程序编写-编译-运行的过程
    编写:我们将编写的java代码保存在以".java"结尾的源文件中
    编译:使用javac.exe命令编译我们的java源文件。格式:javac 源文件名.java
    运行:使用java.exe命令解释运行我们的字节码文件。 格式:java 类名

  2. 在一个java源文件中可以声明多个class。但是,只能最多有一个类声明为public的。
    而且要求声明为public的类的类名必须与源文件名相同。

  3. 程序的入口是main()方法。格式是固定的。

  4. 输出语句:
    System.out.println():先输出数据,然后换行
    System.out.print():只输出数据

  5. 每一行执行语句都以";"结束。

  6. 编译的过程:编译以后,会生成一个或多个字节码文件。字节码文件的文件名与java源文件中的类名相同。

注释与API文档

1.注释:Comment

分类:
单行注释://
多行注释:/* /
文档注释:/
* */
作用:
① 对所写的程序进行解释说明,增强可读性。方便自己,方便别人
② 调试所写的代码
特点:
单行注释和多行注释,注释了的内容不参与编译
换句话说,编译以后生成的.class结尾的字节码文件中不包含注释掉的信息
② 注释内容可以被JDK提供的工具 javadoc 所解析,生成一套以网页文件形式体现的该程序的说明文档。
③ 多行注释不可以嵌套使用

2.Java API 文档:

API:application programming interface。习惯上:将语言提供的类库,都称为api.
API文档:针对于提供的类库如何使用,给的一个说明书。类似于《新华字典》

3.良好的编程风格

image-20220329214840784

基本语法

关键字与标识符

1.java关键字的使用

定义:被Java语言赋予了特殊含义,用做专门用途的字符串(单词)
特点:关键字中所有字母都为小写

具体哪些关键字:

image-20220329215929112

image-20220329215937314

2.保留字

现Java版本尚未使用,但以后版本可能会作为关键字使用。

具体哪些保留字:goto 、const
注意:自己命名标识符时要避免使用这些保留字

3.标识符的使用

定义:凡是自己可以起名字的地方都叫标识符
涉及到的结构:
包名、类名、接口名、变量名方法名、常量名
规则:(必须要遵守。否则,编译不通过)

image-20220329220050172

规范:(可以不遵守,不影响编译和运行。但是要求大家遵守)

image-20220329220122757

注意点: 在起名字时,为了提高阅读性,要尽量意义,“见名知意”。

代码整洁之道(了解即可)

第2章 有意义的命名
2.1 介绍
软件中随处可见命名。我们给变量、函数、参数、类和包命名。我们给源代码及源代码所在目录命名。
这么多命名要做,不妨做好它。下文列出了取个好名字的几条简单规则。

2.2 名副其实,见名知意
变量名太随意,haha、list1、ok、theList 这些都没啥意义

2.3 避免误导
包含List、import、java等类名、关键字或特殊字;
字母o与数字0,字母l与数字1等
提防使用不同之处较小的名称。比如:XYZControllerForEfficientHandlingOfStrings与XYZControllerForEfficientStorageOfStrings

2.4 做有意义的区分
反面教材,变量名:a1、a2、a3
避免冗余,不要出现Variable、表字段中避免出现table、字符串避免出现nameString,直接name就行,知道是字符串类型
再比如:定义了两个类:Customer类和CustomerObject类,如何区分?
定义了三个方法:getActiveAccount()、getActiveAccounts()、getActiveAccountInfo(),如何区分?

2.5 使用读得出来的名称
不要使用自己拼凑出来的单词,比如:xsxm(学生姓名);genymdhms(生成日期,年、月、日、时、分、秒)
所谓的驼峰命名法,尽量使用完整的单词

2.6 使用可搜索的名称
一些常量,最好不直接使用数字,而指定一个变量名,这个变量名可以便于搜索到.
比如:找MAX_CLASSES_PER_STUDENT很容易,但想找数字7就麻烦了。

2.7 避免使用编码
2.7.1 匈牙利语标记法
即变量名表明该变量数据类型的小写字母开始。例如,szCmdLine的前缀sz表示“以零结束的字符串”。
2.7.2 成员前缀
避免使用前缀,但是Android中一个比较好的喜欢用m表示私有等,个人感觉比较好
2.7.3 接口和实现
作者不喜欢把接口使用I来开头,实现也希望只是在后面添加Imp

2.8 避免思维映射
比如传统上惯用单字母名称做循环计数器。所以就不要给一些非计数器的变量命名为:i、j、k等

2.9 类名
类名与对象名应该是名词与名词短语。如Customer、WikiPage、Account和AddressParser。避免使用Data或Info这样的类名。
不能使动词。比如:Manage、Process

2.10 方法名
方法名应当是动词或者动词短语。如postPayment、deletePage或save

2.11 别扮可爱
有的变量名叫haha、banana
别用eatMyShorts()表示abort()

2.12 每个概念对应一个词
项目中同时出现controllers与managers,为什么不统一使用其中一种?
对于那些会用到你代码的程序员,一以贯之的命名法简直就是天降福音。

2.13 别用双关语
有时可能使用add并不合适,比例insert、append。add表示完整的新添加的含义。

2.14 使用解决方案领域名称
看代码的都是程序员,所以尽量用那些计算机科学术语、算法名、模式名、数学术语,
依据问题所涉领域来命名不算是聪明的做法。

2.15 使用源自所涉问题领域的名称
如果不能用程序员熟悉的术语来给手头的工作命名,就采用从所涉问题领域而来的名称吧。
至少,负责维护代码的程序员就能去请教领域专家了。

2.16 添加有意义的语境
可以把相关的变量放到一个类中,使用这个类来表明语境。

2.17 不要添加没用的语境
名字中带有项目的缩写,这样完全没有必要。比如有一个名为“加油站豪华版”(Gas Station Deluxe)的项目,
在其中给每个类添加GSD前缀就不是什么好策略。

2.18 最后的话
取好名字最难的地方在于需要良好的描述技巧和共有文化背景。

变量的使用(重点)

1.变量的分类

1.1 按数据类型分类

image-20220329220338460

详细说明:

//1. 整型:byte(1字节=8bit) \ short(2字节) \ int(4字节) \ long(8字节)
//① byte范围:-128 ~ 127
// ② 声明long型变量,必须以"l"或"L"结尾
// ③ 通常,定义整型变量时,使用int型。
//④整型的常量,默认类型是:int型
//2. 浮点型:float(4字节) \ double(8字节)
//① 浮点型,表示带小数点的数值
//② float表示数值的范围比long还大
//③ 定义float类型变量时,变量要以"f"或"F"结尾
//④ 通常,定义浮点型变量时,使用double型。
//⑤ 浮点型的常量,默认类型为:double
//3. 字符型:char (1字符=2字节)
//① 定义char型变量,通常使用一对'',内部只能写一个字符
//② 表示方式:1.声明一个字符 2.转义字符 3.直接使用 Unicode 值来表示字符型常量
//4.布尔型:boolean
//① 只能取两个值之一:true 、 false
//② 常常在条件判断、循环结构中使用
1.2 按声明的位置分类(了解)

image-20220329220445766

2.定义变量的格式:

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

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

3.变量使用的注意点:

① 变量必须先声明,后使用

② 变量都定义在其作用域内。在作用域内,它是有效的。换句话说,出了作用域,就失效了

③ 同一个作用域内,不可以声明两个同名的变量

4.基本数据类型变量间运算规则

4.1 涉及到的基本数据类型:除了boolean之外的其他7种
4.2 自动类型转换(只涉及7种基本数据类型)

结论:当容量小的数据类型的变量与容量大的数据类型的变量做运算时,结果自动提升为容量大的数据类型。
byte 、char 、short --> int --> long --> float --> double
特别的:当byte、char、short三种类型的变量做运算时,结果为int型
说明:此时的容量大小指的是,表示数的范围的大和小。比如:float容量要大于long的容量

4.3 强制类型转换(只涉及7种基本数据类型):自动类型提升运算的逆运算。

1.需要使用强转符:()
2.注意点:强制类型转换,可能导致精度损失。

4.4 String与8种基本数据类型间的运算
  1. String属于引用数据类型,翻译为:字符串

  2. 声明String类型变量时,使用一对""

  3. String可以和8种基本数据类型变量做运算,且运算只能是连接运算:

  4. 运算的结果仍然是String类型
    避免:

String s = 123;//编译错误
String s1 = "123";
int i = (int)s1;//编译错误

进制

1.编程中涉及的进制及表示方式:

image-20220329220856384

2.二进制的使用说明:

2.1 计算机底层的存储方式:所有数字在计算机底层都以二进制形式存在
2.2 二进制数据的存储方式:所有的数值,不管正负,底层都以补码的方式存储
2.3 原码、反码、补码的说明:
正数:三码合一
负数:

image-20220329220949050

3.进制间的转换:

3.1 图示:

image-20220329221015548

3.2 图示二进制转换为十进制:

image-20220329221034381

image-20220329221040131

image-20220329221047123

3.3 图示十进制转换为二进制:

image-20220329221114717

3.4 二进制与八进制、十六进制间的转换:

image-20220329221141840

image-20220329221149248

运算符

1.算术运算符:

+ - + - * / % (前)++ (后)++ (前)-- (后)-- +

【典型代码】

//除号:/
int num1 = 12;
int num2 = 5;
int result1 = num1 / num2;
System.out.println(result1);//2
// %:取余运算
//结果的符号与被模数的符号相同
//开发中,经常使用%来判断能否被除尽的情况。
int m1 = 12;
int n1 = 5;
System.out.println("m1 % n1 = " + m1 % n1);
int m2 = -12;
int n2 = 5;
System.out.println("m2 % n2 = " + m2 % n2);
int m3 = 12;
int n3 = -5;
System.out.println("m3 % n3 = " + m3 % n3);
int m4 = -12;
int n4 = -5;
System.out.println("m4 % n4 = " + m4 % n4);
//(前)++ :先自增1,后运算
//(后)++ :先运算,后自增1
int a1 = 10;
int b1 = ++a1;
System.out.println("a1 = " + a1 + ",b1 = " + b1);
int a2 = 10;
int b2 = a2++;
System.out.println("a2 = " + a2 + ",b2 = " + b2);
int a3 = 10;
++a3;//a3++;
int b3 = a3;
//(前)-- :先自减1,后运算
//(后)-- :先运算,后自减1
int a4 = 10;
int b4 = a4--;//int b4 = --a4;
System.out.println("a4 = " + a4 + ",b4 = " + b4);

【特别说明的】
1.//(前)++ :先自增1,后运算
//(后)++ :先运算,后自增1

2.//(前)-- :先自减1,后运算
//(后)-- :先运算,后自减1

3.连接符:+:只能使用在String与其他数据类型变量之间使用。

2.赋值运算符:

= += -= *= /= %=

【典型代码】

int i2,j2;
//连续赋值
i2 = j2 = 10;
//***************
int i3 = 10,j3 = 20;
int num1 = 10;
num1 += 2;//num1 = num1 + 2;
System.out.println(num1);//12
int num2 = 12;
num2 %= 5;//num2 = num2 % 5;
System.out.println(num2);
short s1 = 10;
//s1 = s1 + 2;//编译失败
s1 += 2;//结论:不会改变变量本身的数据类型
System.out.println(s1);

【特别说明的】

1.运算的结果不会改变变量本身的数据类型
2.//开发中,如果希望变量实现+2的操作,有几种方法?(前提:int num = 10;)

​ //方式一:num = num + 2;
​ //方式二:num += 2; (推荐)
​ //开发中,如果希望变量实现+1的操作,有几种方法?(前提:int num = 10;)
​ //方式一:num = num + 1;
​ //方式二:num += 1;
​ //方式三:num++; (推荐)

3.比较运算符(关系运算符):

== != > < >= <= instanceof
【典型代码】

int i = 10;
int j = 20;
System.out.println(i == j);//false
System.out.println(i = j);//20
boolean b1 = true;
boolean b2 = false;
System.out.println(b2 == b1);//false
System.out.println(b2 = b1);//true

【特别说明的】
1.比较运算符的结果是boolean类型
2.> < >= <= :只能使用在数值类型的数据之间。

3.== 和 !=: 不仅可以使用在数值类型数据之间,还可以使用在其他引用类型变量之间。

Account acct1 = new Account(1000);
Account acct2 = new Account(1000);
boolean b1 = (acct1 == acct2);//比较两个Account是否是同一个账户。
boolean b2 = (acct1 != acct2);//

4.逻辑运算符:

&(短路与) && | || ! ^

【典型代码】

//区分& 与 &&
//相同点1:& 与 && 的运算结果相同
//相同点2:当符号左边是true时,二者都会执行符号右边的运算
//不同点:当符号左边是false时,&继续执行符号右边的运算。&&不再执行符号右边的运算。
//开发中,推荐使用&&
boolean b1 = true;
b1 = false;
int num1 = 10;
if(b1 & (num1++ > 0)){
System.out.println("我现在在北京");
}else{
System.out.println("我现在在南京");
}
System.out.println("num1 = " + num1);
boolean b2 = true;
b2 = false;
int num2 = 10;
if(b2 && (num2++ > 0)){
System.out.println("我现在在北京");
}else{
System.out.println("我现在在南京");
}
System.out.println("num2 = " + num2);
// 区分:| 与 ||
//相同点1:| 与 || 的运算结果相同
//相同点2:当符号左边是false时,二者都会执行符号右边的运算
//不同点3:当符号左边是true时,|继续执行符号右边的运算,而||不再执行符号右边的运算
//开发中,推荐使用||
boolean b3 = false;
b3 = true;
int num3 = 10;
if(b3 | (num3++ > 0)){
System.out.println("我现在在北京");
}else{
System.out.println("我现在在南京");
}
System.out.println("num3 = " + num3);
boolean b4 = false;
b4 = true;
int num4 = 10;
if(b4 || (num4++ > 0)){
System.out.println("我现在在北京");
}else{
System.out.println("我现在在南京");
}
System.out.println("num4 = " + num4);

【特别说明的】
1.逻辑运算符操作的都是boolean类型的变量。而且结果也是boolean类型

5.位运算符:

<< >> >>> & | ^ ~

《 左移1位,相对于在原来的数值基础上乘2
》右移1位,相对于在原来的数值基础上除以2 另一种解释:带符号右移。正数右移高位补0,负数右移高位补1
>>> 无符号右移。无论是正数还是负数,高位通通补0
& 与运算符,两个操作数中位都为1,结果才为1,否则结果为0
| 或运算符,两个位只要有一个为1,那么结果就是1,否则就为0
^ 异或运算符,两个操作数的位中,相同则结果为0,不同则结果为1
~ 非运算符,如果位为0,结果是1,如果位为1,结果是0

【典型代码】

int i = 21;
i = -21;
System.out.println("i << 2 :" + (i << 2));
System.out.println("i << 3 :" + (i << 3));
System.out.println("i << 27 :" + (i << 27));
int m = 12;
int n = 5;
System.out.println("m & n :" + (m & n));
System.out.println("m | n :" + (m | n));
System.out.println("m ^ n :" + (m ^ n));

【面试题】 你能否写出最高效的2 * 8的实现方式?
答案:2 << 3 或 8 << 1

【特别说明的】

  1. 位运算符操作的都是整型的数据

  2. << 在一定范围内,每向左移1位,相当于 * 2

    >>:在一定范围内,每向右移1位,相当于 / 2

6.三元运算符:

(条件表达式)? 表达式1 : 表达式2
【典型代码】
1.获取两个整数的较大值
2.获取三个数的最大值
【特别说明的】

  1. 说明
    ① 条件表达式的结果为boolean类型
    ② 根据条件表达式真或假,决定执行表达式1,还是表达式2.
    如果表达式为true,则执行表达式1。
    如果表达式为false,则执行表达式2。
    ③ 表达式1 和表达式2要求是一致的。
    ④ 三元运算符可以嵌套使用
  2. 凡是可以使用三元运算符的地方,都可以改写为if-else
    反之,不成立。
  3. 如果程序既可以使用三元运算符,又可以使用if-else结构,那么优先选择三元运算符。原因:简洁、执行效率高。

控制流程

分支结构

1.if-else条件判断结构

1.1.

**结构一**:
if(条件表达式){
执行表达式
}
**结构二**:二选一
if(条件表达式){
执行表达式1
}else{
执行表达式2
}
**结构三**:n选一
if(条件表达式){
执行表达式1
}else if(条件表达式){
执行表达式2
}else if(条件表达式){
执行表达式3
}
...
else{
执行表达式n
}

1.2.说明:

  1. else 结构是可选的。

  2. 针对于条件表达式:

    如果多个条件表达式之间是“互斥”关系(或没有交集的关系),哪个判断和执行语句声明在上面还是下面,无所谓。

    如果多个条件表达式之间有交集的关系,需要根据实际情况,考虑清楚应该将哪个结构声明在上面。

    如果多个条件表达式之间有包含的关系,通常情况下,需要将范围小的声明在范围大的上面。否则,范围小的就没机会执行了。

  3. if-else结构是可以相互嵌套的。

  4. 如果if-else结构中的执行语句只有一行时,对应的一对{}可以省略的。但是,不建议大家省略。

2.switch-case选择结构
switch(表达式){
case 常量1:
执行语句1;
//break;
case 常量2:
执行语句2;
//break;
...
default:
执行语句n;
//break;
}

2.说明:
① 根据switch表达式中的值,依次匹配各个case中的常量。一旦匹配成功,则进入相应case结构中,调用其执行语句。
当调用完执行语句以后,则仍然继续向下执行其他case结构中的执行语句,直到遇到break关键字或此switch-case结构
末尾结束为止。
② break,可以使用在switch-case结构中,表示一旦执行到此关键字,就跳出switch-case结构
switch结构中的表达式,只能是如下的6种数据类型之一: byte 、short、char、int、枚举类型(JDK5.0新增)、String类型(JDK7.0新增)
case 之后只能声明常量。不能声明范围。
⑤ break关键字是可选的。
⑥ default:相当于if-else结构中的else.
default结构是可选的,而且位置是灵活的。

3.如果switch-case结构中的多个case的执行语句相同,则可以考虑进行合并。
4.break在switch-case中是可选的

循环结构

1.循环结构的四要素

① 初始化条件
② 循环条件 --->是boolean类型
③ 循环体
④ 迭代条件
说明:通常情况下,循环结束都是因为②中循环条件返回false了。

2.三种循环结构:

2.1 for循环结构
for(①;②;④){

}
执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ... - ②

2.2 while循环结构

while(②){
③;
④;
}
执行过程:① - ② - ③ - ④ - ② - ③ - ④ - ... - ②
说明:
写while循环千万小心不要丢了迭代条件。一旦丢了,就可能导致死循环!

2.3 do-while循环结构

do{
③;
④;
}while(②);
执行过程:① - ③ - ④ - ② - ③ - ④ - ... - ②

for和while循环总结:

  1. 开发中,基本上我们都会从for、while中进行选择,实现循环结构。
  2. for循环和while循环是可以相互转换的!
    区别:for循环和while循环的初始化条件部分的作用范围不同。
  3. 我们写程序,要避免出现死循环。
  4. do-while循环至少会执行一次循环体!
  5. 开发中,使用for和while更多一些。较少使用do-while
3.“无限循环”结构: while(true) 或 for(;😉

总结:如何结束一个循环结构?
方式一:当循环条件是false时
方式二:在循环体中,执行break

4.嵌套循环

1.嵌套循环:将一个循环结构A声明在另一个循环结构B的循环体中,就构成了嵌套循环
内层循环:循环结构A
外层循环:循环结构B
2.说明:
① 内层循环结构遍历一遍,只相当于外层循环循环体执行了一次
② 假设外层循环需要执行m次内层循环需要执行n次。此时内层循环的循环体一共执行了m * n
③ 外层循环控制行数,内层循环控制列数

补充:衡量一个功能代码的优劣:
1.正确性
2.可读性
3.健壮性
4.高效率与低存储:时间复杂度 、空间复杂度 (衡量算法的好坏)

关键字break和continue

image-20220329223643907

Scanner类的使用

/*
如何从键盘获取不同类型的变量:需要使用Scanner类
具体实现步骤:
1.导包:import java.util.Scanner;
2.Scanner的实例化:Scanner scan = new Scanner(System.in);
3.调用Scanner类的相关方法(next() / nextXxx()),来获取指定类型的变量
注意:
需要根据相应的方法,来输入指定类型的值。如果输入的数据类型与要求的类型不匹配时,会报异常:InputMisMatchException
导致程序终止。
*/
//1.导包:import java.util.Scanner;
import java.util.Scanner;
class ScannerTest{
public static void main(String[] args){
//2.Scanner的实例化
Scanner scan = new Scanner(System.in);
//3.调用Scanner类的相关方法
System.out.println("请输入你的姓名:");
String name = scan.next();
System.out.println(name);
System.out.println("请输入你的芳龄:");
int age = scan.nextInt();
System.out.println(age);
System.out.println("请输入你的体重:");
double weight = scan.nextDouble();
System.out.println(weight);
System.out.println("你是否相中我了呢?(true/false)");
boolean isLove = scan.nextBoolean();
System.out.println(isLove);
//对于char型的获取,Scanner没有提供相关的方法。只能获取一个字符串
System.out.println("请输入你的性别:(男/女)");
String gender = scan.next();//"男"
char genderChar = gender.charAt(0);//获取索引为0位置上的字符
System.out.println(genderChar);
}
}

数组

数组的概述

image-20220329224236992

数据结构:
1.数据与数据之间的逻辑关系:集合、一对一、一对多、多对多
2.数据的存储结构:
线性表:顺序表(比如:数组)、链表、栈、队列
树形结构:二叉树
图形结构:

算法:
排序算法:
搜索算法:

一维数组

1.一维数组的声明与初始化

正确的方式:

int num;//声明
num = 10;//初始化
int id = 1001;//声明 + 初始化

一维数组命名方式:

int[] ids;//声明
//1.1 静态初始化:数组的初始化和数组元素的赋值操作同时进行
ids = new int[]{1001,1002,1003,1004};
//1.2动态初始化:数组的初始化和数组元素的赋值操作分开进行
String[] names = new String[5];
//1.3 类型推断
int[] arr4 = {1,2,3,4,5};//类型推断
错误的方式:
// int[] arr1 = new int[];
// int[5] arr2 = new int[5];
// int[] arr3 = new int[3]{1,2,3};

2.一维数组元素的引用:通过角标的方式调用。

image-20220329224704994

3.数组的属性:length

System.out.println(names.length);//5
System.out.println(ids.length);

注意:
数组一旦初始化,其长度就是确定的。arr.length
数组长度一旦确定,就不可修改。

4.一维数组的遍历

for(int i = 0;i < names.length;i++){
System.out.println(names[i]);
}

5.一维数组元素的默认初始化值

image-20220329224844438

6.一维数组的内存解析

image-20220329224901967

二维数组

1.如何理解二维数组?

数组属于引用数据类型
数组的元素也可以是引用数据类型
一个一维数组A的元素如果还是一个一维数组类型的,则此数组A称为二维数组。

2.二维数组的声明与初始化

正确的方式:

int[] arr = new int[]{1,2,3};//一维数组
//静态初始化
int[][] arr1 = new int[][]{{1,2,3},{4,5},{6,7,8}};
//动态初始化1
String[][] arr2 = new String[3][2];
//动态初始化2
String[][] arr3 = new String[3][];
//也是正确的写法:
int[] arr4[] = new int[][]{{1,2,3},{4,5,9,10},{6,7,8}};
int[] arr5[] = {{1,2,3},{4,5},{6,7,8}};//类型推断

错误的方式:

// String[][] arr4 = new String[][4];
// String[4][3] arr5 = new String[][];
// int[][] arr6 = new int[4][3]{{1,2,3},{4,5},{6,7,8}};

3.如何调用二维数组元素:

System.out.println(arr1[0][1]);//2
System.out.println(arr2[1][1]);//null
arr3[1] = new String[4];
System.out.println(arr3[1][0]);
System.out.println(arr3[0]);//

4.二维数组的属性:

System.out.println(arr4.length);//3
System.out.println(arr4[0].length);//3
System.out.println(arr4[1].length);//4

5.遍历二维数组元素

for(int i = 0;i < arr4.length;i++){
for(int j = 0;j < arr4[i].length;j++){
System.out.print(arr4[i][j] + " ");
}
System.out.println();
}

6.二维数组元素的默认初始化值

image-20220329225947591

7.二维数组的内存结构

image-20220329230002901

数组的常见算法

1.数组的创建与元素赋值:
杨辉三角(二维数组)、回形数(二维数组)、6个数,1-30之间随机生成且不重复。
2.针对于数值型的数组:
最大值、最小值、总和、平均数等
3.数组的赋值与复制
int[] array1,array2;
array1 = new int[]{1,2,3,4};
3.1 赋值:
array2 = array1;
如何理解:将array1保存的数组的地址值赋给了array2,使得array1和array2共同指向堆空间中的同一个数组实体。

image-20220329230438650

3.2 复制

array2 = new int[array1.length];
for(int i = 0;i < array2.length;i++){
array2[i] = array1[i];
}

image-20220329230517062

如何理解:我们通过new的方式,给array2在堆空间中新开辟了数组的空间。将array1数组中的元素值一个一个的赋值到array2数组中。

4.数组元素的反转

//方法一:
// for(int i = 0;i < arr.length / 2;i++){
// String temp = arr[i];
// arr[i] = arr[arr.length - i -1];
// arr[arr.length - i -1] = temp;
// }
//方法二:
// for(int i = 0,j = arr.length - 1;i < j;i++,j--){
// String temp = arr[i];
// arr[i] = arr[j];
// arr[j] = temp;
// }

5.数组中指定元素的查找:搜索、检索

5.1 线性查找:
实现思路:通过遍历的方式,一个一个的数据进行比较、查找。
适用性:具有普遍适用性。
5.2 二分法查找:
实现思路:每次比较中间值,折半的方式检索。
适用性:(前提:数组必须有序)

6.数组的排序算法

image-20220329230719570

理解:
1)衡量排序算法的优劣:
时间复杂度、空间复杂度、稳定性

2)排序的分类:内部排序 与 外部排序(需要借助于磁盘)

3)不同排序算法的时间复杂度

image-20220329230754387

4)手写冒泡排序

int[] arr = new int[]{43,32,76,-98,0,64,33,-21,32,99};
//冒泡排序
for(int i = 0;i < arr.length - 1;i++){
for(int j = 0;j < arr.length - 1 - i;j++){
if(arr[j] > arr[j + 1]){
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}

数组的常见异常

1.数组角标越界异常:ArrayIndexOutOfBoundsException

int[] arr = new int[]{1,2,3,4,5};
// for(int i = 0;i <= arr.length;i++){
// System.out.println(arr[i]);
// }
// System.out.println(arr[-2]);
// System.out.println("hello");

2.空指针异常:NullPointerException

//情况一:
// int[] arr1 = new int[]{1,2,3};
// arr1 = null;
// System.out.println(arr1[0]);
//情况二:
// int[][] arr2 = new int[4][];
// System.out.println(arr2[0][0]);
//情况:
String[] arr3 = new String[]{"AA","BB","CC"};
arr3[0] = null;
System.out.println(arr3[0].toString());

小知识:一旦程序出现异常,未处理时,就终止执行。

JVM内存结构

编译完源程序以后,生成一个或多个字节码文件。
我们使用JVM中的类的加载器和解释器对生成的字节码文件进行解释运行。意味着,需要将字节码文件对应的类加载到内存中,涉及到内存解析

image-20220329232155811

虚拟机栈,即为平时提到的栈结构。我们将局部变量存储在栈结构中
,我们将new出来的结构(比如:数组、对象)加载在堆空间中。补充:对象的属性(非static的)加载在堆空间中。
方法区:类的加载信息、常量池、静态域

面向对象

类与对象

1.面向对象学习的三条主线:

image-20220329231201320

2.面向对象与面向过程(理解)

image-20220329231332158

3.完成一个项目(或功能)的思路:

image-20220329231348760

4.面向对象中两个重要的概念:

:对一类事物的描述,是抽象的、概念上的定义
对象:是实际存在的该类事物的每个个体,因而也称为实例(instance)

面向对象程序设计的重点是类的设计;设计类,就是设计类的成员。

二者的关系:
对象,是由类new出来的,派生出来的。

5.面向对象思想落地实现的规则一

  • 1.创建类,设计类的成员
  • 2.创建类的对象
  • 3.通过“对象.属性”或“对象.方法”调用对象的结构

补充:几个概念的使用说明

  • 属性 = 成员变量 = field = 域、字段
  • 方法 = 成员方法 = 函数 = method
  • 创建类的对象 = 类的实例化 = 实例化类

6.对象的创建与对象的内存解析

典型代码:

Person p1 = new Person();
Person p2 = new Person();
Person p3 = p1;//没有新创建一个对象,共用一个堆空间中的对象实体。

说明:
如果创建了一个类的多个对象,则每个对象都独立的拥有一套类的属性。(非static的) 意味着:如果我们修改一个对象的属性a,则不影响另外一个对象属性a的值。

内存解析:

image-20220329231727955

image-20220329231738709

7.匿名对象:我们创建的对象,没显式的赋给一个变量名。即为匿名对象

特点:匿名对象只能调用一次。

举例:

new Phone().sendEmail();
new Phone().playGame();
new Phone().price = 1999;
new Phone().showPrice();//0.0

应用场景:

PhoneMall mall = new PhoneMall();
//匿名对象的使用
mall.show(new Phone());
其中,
class PhoneMall{
public void show(Phone phone){
phone.sendEmail();
phone.playGame();
}
}

8.理解"万事万物皆对象"

image-20220329231919013

类的结构之一:属性

对比:属性 vs 局部变量

1.相同点:

  • 1.1 定义变量的格式:`数据类型 变量名 = 变量值`
  • 1.2 先声明,后使用
  • 1.3 变量都有其对应的作用域

2.不同点:
2.1 在类中声明的位置的不同

  • 属性:直接定义在类的一对{}内

  • 局部变量:声明在方法内、方法形参、代码块内、构造器形参、构造器内部的变量

  • 2.2 关于权限修饰符的不同

  • 属性:可以在声明属性时,指明其权限,使用权限修饰符。

  • 常用的权限修饰符:private、public、缺省、protected --->封装性

  • 目前,大家声明属性时,都使用缺省就可以了。

  • 局部变量:不可以使用权限修饰符。

  • 2.3 默认初始化值的情况

  • 属性:类的属性,根据其类型,都默认初始化值

  • 整型(byte、short、int、long:0)

  • 浮点型(float、double:0.0)

  • 字符型(char:0 (或'\u0000'))

  • 布尔型(boolean:false)

  • 引用数据类型(类、数组、接口:null)

  • 局部变量:没默认初始化值

  • 意味着,我们在调用局部变量之前,一定要显式赋值

  • 特别地:形参在调用时,我们赋值即可。

  • 2.4 在内存中加载的位置

  • 属性:加载到堆空间中 (非static)

  • 局部变量:加载到栈空间

补充:回顾变量的分类:
方式一:按照数据类型:

image-20220329232713421

方式二:按照在类中声明的位置:

image-20220329232727480

类的结构之二:方法

方法介绍

方法:描述类应该具的功能。

比如:

Math类:sqrt()\random() ...

Scanner类:nextXxx() ...

Arrays类:sort() \ binarySearch() \ toString() \ equals() \ ...

1.举例:

public void eat(){}
public void sleep(int hour){}
public String getName(){}
public String getNation(String nation){}
  1. 方法的声明
权限修饰符 返回值类型 方法名(形参列表){
方法体
}

注意:static、final、abstract 来修饰的方法,后面再讲。

  1. 说明:

    3.1 关于权限修饰符:默认方法的权限修饰符先都使用public

    ​ Java规定的4种权限修饰符:private、public、缺省、protected -->封装性再细说

    image-20220329233339881

return关键字:

image-20220329233429840

方法的重载

1.方法的重载的概念

定义:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。

总结:"两同一不同(同类同名不同参)":同一个类、相同方法名
参数列表不同:参数个数不同,参数类型不同

2.构成重载的举例:

//如下的4个方法构成了重载
public void getSum(int i,int j){
System.out.println("1");
}
public void getSum(double d1,double d2){
System.out.println("2");
}
public void getSum(String s ,int i){
System.out.println("3");
}
public void getSum(int i,String s){
System.out.println("4");
}
//不构成重载的举例:
//如下的3个方法不能与上述4个方法构成重载
// public int getSum(int i,int j){
// return 0;
// }
// public void getSum(int m,int n){
//
// }
// private void getSum(int i,int j){
//
// }
  1. 如何判断是否构成方法的重载?
  2. 严格按照定义判断:两同一不同。跟方法的权限修饰符、返回值类型、形参变量名、方法体都没关系!

面试题

方法的重载与重写的区别?

throws\throw

1throws出现在方法函数头;而throw出现在函数体
2throws表示出现异常的一种可能性,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象
3、两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理

String\StringBuffer\StringBuilder三者之间的区别:

相同点:都是final类,都不允许被继承;
不同点:
String类长度是不可变的,StringBuffer和StringBuilder类长度是可以改变的;
StringBuffer类是线程安全的,StringBuilder不是线程安全的

Collection\Collections

1java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式
2java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架

final\finally\finalize

解释:
https://www.cnblogs.com/ktao/p/8586966.html

抽象类、接口

sleep() / wait()

1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

可变个数形参的方法

1.使用说明:

image-20220329234651693

2.举例说明:

public void show(int i){
}
public void show(String s){
System.out.println("show(String)");
}
public void show(String ... strs){
System.out.println("show(String ... strs)");
for(int i = 0;i < strs.length;i++){
System.out.println(strs[i]);
}
}
//不能与上一个方法同时存在
// public void show(String[] strs){
//
// }
//调用时:
test.show("hello");
test.show("hello","world");
test.show();
test.show(new String[]{"AA","BB","CC"});

java的值传递机制

1.规则:
如果变量是基本数据类型,此时赋值的是变量所保存的数据值。
如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值。

2.针对于方法的参数概念
形参:方法定义时,声明的小括号内的参数
实参:方法调用时,实际传递给形参的数据

3.java中参数传递机制:值传递

规则:

  • 如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值。
  • 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值。

image-20220329234958772

4.典型例题与内存解析:

【例题1】

image-20220329235010810

【例题2】

image-20220329235023548

面向对象的特征一:封装与隐藏

1.为什么要引入封装性?

我们程序设计追求“高内聚,低耦合”。
高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
低耦合 :仅对外暴露少量的方法用于使用。

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想

2.问题引入:

当我们创建一个类的对象以后,我们可以通过"对象.属性"的方式,对对象的属性进行赋值。这里,赋值操作要受到属性的数据类型和存储范围的制约。除此之外,没其他制约条件。但是,在实际问题中,我们往往需要给属性赋值加入额外的限制条件。这个条件就不能在属性声明时体现,我们只能通过方法进行限制条件的添加。(比如:setLegs()同时,我们需要避免用户再使用"对象.属性"的方式对属性进行赋值。则需要将属性声明为私有的(private).
-->此时,针对于属性就体现了封装性。

3.封装性思想具体的代码体现:

体现一:将类的属性xxx私化(private),同时,提供公共的(public)方法来获取(getXxx)和设置(setXxx)此属性的值

private double radius;
public void setRadius(double radius){
this.radius = radius;
}
public double getRadius(){
return radius;
}

体现二:不对外暴露的私有的方法

体现三:单例模式(将构造器私有化)

体现四:如果不希望类在包外被调用,可以将类设置为缺省的。

4.Java规定的四种权限修饰符

4.1 权限从小到大顺序为:private < 缺省 < protected < public

4.2 具体的修饰范围:

image-20220329235609687

4.3 权限修饰符可用来修饰的结构说明:

4种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类, 修饰类的话,只能使用:缺省、public

类的结构之三:构造器

1.构造器(或构造方法):Constructor

构造器的作用:

①.创建对象

②.初始化对象的信息

2.使用说明:

1.如果没显式的定义类的构造器的话,则系统默认提供一个空参的构造器
2.定义构造器的格式:权限修饰符 类名(形参列表){}
3.一个类中定义的多个构造器,彼此构成重载
4.一旦我们显式的定义了类的构造器之后,系统就不再提供默认的空参构造器
5.一个类中,至少会有一个构造器。

3.举例:

//构造器
public Person(){
System.out.println("Person().....");
}
public Person(String n){
name = n;
}
public Person(String n,int a){
name = n;
age = a;
}

属性赋值顺序

image-20220330000257530

javaBean概念

所谓JavaBean,是指符合如下标准的Java类:
>类是公共的
>一个无参的公共的构造器
>属性,且对应的get、set方法

关键字:this

1.可以调用的结构:属性、方法;构造器

2.this调用属性、方法:

this理解为:当前对象 或 当前正在创建的对象

2.1 在类的方法中,我们可以使用"this.属性"或"this.方法"的方式,调用当前对象属性或方法。但是,通常情况下,我们都选择省略"this."。特殊情况下,如果方法的形参和类的属性同名时,我们必须显式的使用"this.变量"的方式,表明此变量是属性,而非形参。

2.2 在类的构造器中,我们可以使用"this.属性"或"this.方法"的方式,调用当前正在创建的对象属性或方法。但是,通常情况下,我们都选择省略"this."。特殊情况下,如果构造器的形参和类的属性同名时,我们必须显式的使用"this.变量"的方式,表明此变量是属性,而非形参。

3.this调用构造器:

① 我们在类的构造器中,可以显式的使用"this(形参列表)"方式,调用本类中指定的其他构造器
② 构造器中不能通过"this(形参列表)"方式调用自己
③ 如果一个类中有n个构造器,则最多有 n - 1构造器中使用了"this(形参列表)"
④ 规定:"this(形参列表)"必须声明在当前构造器的首行
⑤ 构造器内部,最多只能声明一个"this(形参列表)",用来调用其他的构造器

关键字:package/import

  1. package的使用

1.1 使用说明:

image-20220330000826316

1.2 举例:

举例一:
某航运软件系统包括:一组域对象、GUI和reports子系统

image-20220330000927745

举例二:MVC设计模式

image-20220330000936813

1.3 JDK中的主要包介绍:

image-20220330000948681

2.import的使用:
import:导入
* 1. 在源文件中显式的使用import结构导入指定包下的类、接口
* 2. 声明在包的声明和类的声明之间
* 3. 如果需要导入多个结构,则并列写出即可
* 4. 可以使用"xxx.*"的方式,表示可以导入xxx包下的所结构
* 5. 如果使用的类或接口是java.lang包下定义的,则可以省略import结构
* 6. 如果使用的类或接口是本包下定义的,则可以省略import结构
* 7. 如果在源文件中,使用了不同包下的同名的类,则必须至少一个类需要以全类名的方式显示。
* 8. 使用"xxx.*"方式表明可以调用xxx包下的所结构。但是如果使用的是xxx子包下的结构,则仍需要显式导入
*
* 9. import static:导入指定类或接口中的静态结构:属性或方法。

面向对象的特征二:继承性

1.为什么要有类的继承性?(继承性的好处)

① 减少了代码的冗余,提高了代码的复用性

② 便于功能的扩展

③ 为之后多态性的使用,提供了前提

image-20220330001304441

2.继承性的格式:

class A extends B{}

​ A:子类、派生类、subclass

​ B:父类、超类、基类、superclass

3.子类继承父类以后有哪些不同?

3.1体现:一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的属性和方法

特别的,父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私的结构。只因为封装性的影响,使得子类不能直接调用父类的结构而已。

3.2 子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的拓展。

4.Java中继承性的说明

1.一个类可以被多个子类继承。
2.Java中类的单继承性:一个类只能有一个父类
3.子父类是相对的概念。
4.子类直接继承的父类,称为:直接父类。间接继承的父类称为:间接父类
5.子类继承父类以后,就获取了直接父类以及间接父类中声明的属性和方法

图示:

image-20220330001625196

5.java.lang.Object类的理解

  1. 如果我们没显式的声明一个类的父类的话,则此类继承于java.lang.Object类
  2. 所有的java类(除java.lang.Object类之外都直接或间接的继承于java.lang.Object类
  3. 意味着,所的java类具有java.lang.Object类声明的功能。

方法重写

1.什么是方法的重写(override 或 overwrite)?

子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作.

2.应用:

重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法。

3.重写的规则:

方法的声明:

权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型{
//方法体
}

约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法

① 子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同

② 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符

特殊情况:子类不能重写父类中声明为private权限的方法

③ 返回值类型:

​ 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void

父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类

​ 父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(必须也是double)

④ 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型(具体放到异常处理时候讲)

子类和父类中的同名同参数的方法要么都声明为非static的,要么都声明为static的(声明为static的方法不能说是重写)。

4.面试题:

区分方法的重写和重载?

① 二者的概念:
② 重载和重写的具体规则
③ 重载:不表现为多态性。
重写:表现为多态性。

重载,是指允许存在多个同名方法,而这些方法的参数不同。编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。Java的重载是可以包括父类和子类的,即子类可以重载父类的同名不同参数的方法。
所以:对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”

而对于多态,只等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”

引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”

关键字:super

1.super 关键字可以理解为:父类的

2.可以用来调用的结构:
属性、方法、构造器

3.super调用属性、方法:

3.1 我们可以在子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
3.2 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性。
3.3 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。

4.super调用构造器:
4.1 我们可以在子类的构造器中显式的使用"super(形参列表)"的方式,调用父类中声明的指定的构造器
4.2 "super(形参列表)"的使用,必须声明在子类构造器的首行
4.3 我们在类的构造器中,针对于"this(形参列表)"或"super(形参列表)"只能二选一,不能同时出现
4.4 在构造器的首行,没显式的声明"this(形参列表)"或"super(形参列表)",则默认调用的是父类中空参的构造器:super()
4.5 在类的多个构造器中,至少一个类的构造器中使用了"super(形参列表)",调用父类中的构造器

子类对象实例化全过程(理解即可)

1.从结果上看:继承性

①子类继承父类以后,就获取了父类中声明的属性或方法。

创建子类的对象,在堆空间中,就会加载所有父类中声明的属性

2.从过程上看

当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,...直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中父类中的结构,子类对象才可以考虑进行调用。

图示:

image-20220330003434065

3.强调说明

虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。

image-20220330003504309

面向对象的特征三:多态性

1.多态性的理解:可以理解为一个事物的多种形态。

2.何为多态性:

对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
举例:

Person p = new Man();
Object obj = new Date();

3.多态性的使用:虚拟方法调用

①有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。

②总结:编译,看左边;运行,看右边。

4.多态性的使用前提:

① 类的继承关系 ② 方法的重写

5.多态性的应用举例:

//举例一:
public void func(Animal animal){//Animal animal = new Dog();
animal.eat();
animal.shout();
}
//举例二:
class Driver{
public void doData(Connection conn){//conn = new MySQlConnection(); // conn = new OracleConnection();
//规范的步骤去操作数据
// conn.method1();
// conn.method2();
// conn.method3();
}
}

6.多态性使用的注意点:

对象的多态性,只适用于方法,不适用于属性(编译和运行都看左边)

7.关于向上转型与向下转型:

7.1 向上转型:多态
7.2 向下转型:
7.2.1 为什么使用向下转型:

有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。如何才能调用子类特的属性和方法?使用向下转型。

7.2.2 如何实现向下转型:
使用强制类型转换符:()
7.2.3 使用时的注意点:
① 使用强转时,可能出现ClassCastException的异常。
② 为了避免在向下转型时出现ClassCastException的异常,我们在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。

7.2.4 instanceof的使用:
a instanceof A:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false。
② 如果 a instanceof A返回true,则 a instanceof B也返回true.其中,类B是类A的父类。

③ 要求a所属的类与类A必须是子类和父类的关系,否则编译错误。

image-20220330004239951

  1. 面试题:

8.1 谈谈你对多态性的理解?
① 实现代码的通用性。
② Object类中定义的public boolean equals(Object obj){ }
JDBC:使用java程序操作(获取数据库连接、CRUD)数据库(MySQL、Oracle、DB2、SQL Server)
③ 抽象类、接口的使用肯定体现了多态性。(抽象类、接口不能实例化)
8.2 多态是编译时行为还是运行时行为?
运行时行为

Object类的使用

1.java.lang.Object类的说明:

  • Object类是所Java类的根父类
  • 如果在类的声明中未使用extends关键字指明其父类,则默认父类为java.lang.Object类
  • Object类中的功能(属性、方法)就具通用性。
  • 方法:equals() / toString() / getClass() /hashCode() / clone() / finalize()、wait() 、 notify()、notifyAll()
  • Object类只声明了一个空参的构造器

2.equals()方法

2.1 equals()的使用:

1. 是一个方法,而非运算符
2. 只能适用于引用数据类型
3. Object类中equals()的定义:
public boolean equals(Object obj) {
return (this == obj);
}
`说明:Object类中定义的equals()和==的作用是相同的:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体`
4.StringDateFile、包装类等都重写了Object类中的equals()方法。重写以后,比较的不是
两个引用的地址是否相同,而是比较两个对象的"实体内容"是否相同。
5. 通常情况下,我们自定义的类如果使用equals()的话,也通常是比较两个对象的"实体内容"是否相同。那么,我们
就需要对Object类中的equals()进行重写.
重写的原则:比较两个对象的实体内容是否相同.

2.2 如何重写equals()

2.2.1 手动重写举例:

class User{
String name;
int age;
//重写其equals()方法
public boolean equals(Object obj){
if(obj == this){
return true;
}
if(obj instanceof User){
User u = (User)obj;
return this.age == u.age && this.name.equals(u.name);
}
return false;
}
}

2.2.2 开发中如何实现:自动生成的

2.3 回顾 == 运算符的使用:

== :运算符

1. 可以使用在基本数据类型变量和引用数据类型变量中
2. `如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同)`
`如果比较的是引用数据类型变量:比较两个对象的地址值是否相同.即两个引用是否指向同一个对象实体`
补充: == 符号使用时,必须保证符号左右两边的变量类型一致。
  1. toString()方法

    3.1 toString()的使用:

    1. 当我们输出一个对象的引用时,实际上就是调用当前对象的toString()
    2. Object类中toString()的定义:
      public String toString() {
      return getClass().getName() + "@" + Integer.toHexString(hashCode());
      }
    3. 像String、Date、File、包装类等都重写了Object类中的toString()方法。
      使得在调用对象的toString()时,返回"实体内容"信息
    4. 自定义类也可以重写toString()方法,当调用此方法时,返回对象的"实体内容"

    3.2 如何重写toString()

    //自动实现
    @Override
    public String toString() {
    return "Customer [name=" + name + ", age=" + age + "]";
    }

    4.面试题:

    ① final、finally、finalize的区别?
    ② == 和 equals() 区别

包装类的使用

1.为什么要有包装类(或封装类)
为了使基本数据类型的变量具有类的特征,引入包装类。

2.基本数据类型与对应的包装类:

image-20220330005537928

3.需要掌握的类型间的转换:(基本数据类型、包装类、String)

image-20220330005551965

简易版:
基本数据类型<--->包装类:JDK 5.0 新特性:自动装箱 与自动拆箱
基本数据类型、包装类--->String:调用String重载的valueOf(Xxx xxx)、包装类对象的toString()、+ "" 方法进行转化
String--->基本数据类型、包装类:调用包装类的parseXxx(String s)
注意:转换时,可能会报NumberFormatException
应用场景举例:
① Vector类中关于添加元素,只定义了形参为Object类型的方法:
v.addElement(Object obj); //基本数据类型 --->包装类 --->使用多态

关键字:static

1.可以用来修饰的结构:主要用来修饰类的内部结构

属性、方法、代码块、内部类

2.static修饰属性:静态变量(或类变量)

image-20220330005923241

3.静态变量内存解析:

image-20220330005950252

4.static修饰方法:静态方法、类方法

image-20220330010049633

5.static的注意点:

5.1 在静态的方法内,不能使用this关键字、super关键字
5.2 关于静态属性和静态方法的使用,大家都从生命周期的角度去理解。

6.如何判定属性和方法应该使用static关键字:

6.1 关于属性

属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
类中的常量也常常声明为static

6.2 关于方法

操作静态属性的方法,通常设置为static的
工具类中的方法,习惯上声明为static的。 比如:Math、Arrays、Collections

单例模式

1.设计模式的说明

1.1 理解

设计模式是在大量的实践中总结和理论化之后优化的代码结构、编程风格、以及解决问题的思考方式。

1.2 常用设计模式 --- 23种经典的设计模式 GOF

创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共7种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共11种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

2.单例模式

2.1 要解决的问题:
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例
2.2 具体代码的实现:

饿汉式1:

class Bank{
//1.私化类的构造器
private Bank(){
}
//2.内部创建类的对象
//4.要求此对象也必须声明为静态的
private static Bank instance = new Bank();
//3.提供公共的静态的方法,返回类的对象
public static Bank getInstance(){
return instance;
}
}

饿汉式2:使用了静态代码块

class Order{
//1.私化类的构造器
private Order(){
}
//2.声明当前类对象,没初始化
//4.此对象也必须声明为static的
private static Order instance = null;
static{
instance = new Order();
}
//3.声明public、static的返回当前类对象的方法
public static Order getInstance(){
return instance;
}
}

懒汉式:

class Order{
//1.私化类的构造器
private Order(){
}
//2.声明当前类对象,没初始化
//4.此对象也必须声明为static的
private static Order instance = null;
//3.声明public、static的返回当前类对象的方法
public static Order getInstance(){
if(instance == null){
instance = new Order();
}
return instance;
}
}

2.3 两种方式的对比:

饿汉式:
坏处:对象加载时间过长。
好处:饿汉式是线程安全的
懒汉式:好处:延迟对象的创建。
目前的写法坏处:线程不安全。--->到多线程内容时,再修改

main方法的使用说明

main()方法作为程序的入口

main()方法也是一个普通的静态方法

main()方法可以作为我们与控制台交互的方式。(之前:使用Scanner)

​ 交互方式1:

鼠标右键---》run as --->run configurations--->Arguments

image-20220330102349368

​ 交互方式2:

将控制台获取的数据传给形参:String[] args
运行时:java 类名 "参数1" "参数2" "参数3" "参数4"

小结:一叶知秋

image-20220330103010121

类的结构:代码块

1.代码块的作用:用来初始化类、对象的信息
2.分类:代码块要是使用修饰符,只能使用static

3.分类:静态代码块 vs 非静态代码块

静态代码块:

内部可以输出语句
随着类的加载而执行,而且只执行一次
作用:初始化类的信息
如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行
静态代码块的执行要优先于非静态代码块的执行
静态代码块内只能调用静态的属性、静态的方法,不能调用非静态的结构

非静态代码块:

内部可以输出语句
随着对象的创建而执行
每创建一个对象,就执行一次非静态代码块
作用:可以在创建对象时,对对象的属性等进行初始化
如果一个类中定义了多个非静态代码块,则按照声明的先后顺序执行
非静态代码块内可以调用静态的属性、静态的方法,或非静态的属性、非静态的方法

4.实例化子类对象时,涉及到父类、子类中静态代码块、非静态代码块、构造器的加载顺序:

由父及子,静态先行 : 父类静态代码块---》子类静态代码块---》(调用子类构造函数时)---》父类非静态代码块---》父类构造函数---》子类非静态代码块---》子类构造函数。

5.属性的赋值顺序

①默认初始化
②显式初始化/⑤在代码块中赋值(二者是同级,谁在前谁先执行)
③构造器中初始化
④有了对象以后,可以通过"对象.属性""对象.方法"的方式,进行赋值
执行的先后顺序:① - ② / ⑤ - ③ - ④

关键字:final

1.可以用来修饰:类、方法、变量

2.具体的:

2.1 final 用来修饰一个类:此类不能被其他类所继承。比如:String类、System类、StringBuffer类

2.2 final 用来修饰方法:表明此方法不可以被重写,比如:Object类中getClass();

2.3 final 用来修饰变量:此时的"变量"就称为是一个常量

  • final修饰属性:可以考虑赋值的位置:显式初始化、代码块中初始化、构造器中初始化
  • final修饰局部变量:尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。

static final 用来修饰属性:全局常量

关键字:abstract

1.可以用来修饰:类、方法

2.具体的

abstract修饰类:抽象类

  • 此类不能实例化
  • 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)
  • 开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作 --->抽象的使用前提:继承性

abstract修饰方法:抽象方法

  • 抽象方法只有方法的声明,没方法体
  • 包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的。
  • 若子类重写了父类中的所的抽象方法后,此子类方可实例化
  • 若子类没重写父类中的所的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰

3.注意点:

  • 1.abstract不能用来修饰:属性、构造器等结构
  • 2.abstract不能用来修饰私方法、静态方法、final的方法、final的类

4.abstract的应用举例:

abstract class GeometricObject{
public abstract double findArea();
}
class Circle extends GeometricObject{
private double radius;
public double findArea(){
return 3.14 * radius * radius;
};
}

5.应用:模板方法的设计模式

1.解决的问题

在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,部分可以抽象出来,供不同子类实现。这就是一种模板模式

2.举例

abstract class Template{
//计算某段代码执行所需要花费的时间
public void spendTime(){
long start = System.currentTimeMillis();
this.code();//不确定的部分、易变的部分
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
public abstract void code();
}
class SubTemplate extends Template{
@Override
public void code() {
for(int i = 2;i <= 1000;i++){
boolean isFlag = true;
for(int j = 2;j <= Math.sqrt(i);j++){
if(i % j == 0){
isFlag = false;
break;
}
}
if(isFlag){
System.out.println(i);
}
}
}
}
  1. 应用场景

    image-20220330104651830

匿名子类匿名对象

匿名子类:父类是抽象类,子类只需要用一次,那么不需要引用具体的子类
匿名对象: new 出来的对象没有引用去承接

假如 子类Work继承了抽象类Person

abstract class Person{
public void eat();
public void breath()
}

匿名子类匿名对象: new Person(){ ... }
匿名子类非匿名对象: Person p = new Person(){ ...}
非匿名子类匿名对象: new Work().method();
非匿名子类非匿名对象 Person p = new Work();

匿名实现类非匿名对象:没有实现类去实例化,用接口去实例化,(例如接口A A a = new A(){ methods... } )

interface Person{
public void eat();
public void breath()
}

匿名实现类匿名对象:(例如接口A new A(){ methods... } )
非匿名实现类匿名对象:(例如实现类B实现了接口A new B() )
非匿名实现类非匿名对象:(例如实现类B实现了接口A B b = new B() )

image-20220330104852246

image-20220330104857622

image-20220330104909165

abstract class Person{
public void eat();
public void breath()
}
class Worker extends Person{
@Override
public void eat() {
}
@Override
public void breath() {
}
}
public class PersonTest {
public static void main(String[] args) {
}
//创建匿名子类的匿名对象
method1(new Person(){
@Override
public void eat() {
System.out.println("吃好吃东西");
}
@Override
public void breath() {
System.out.println("好好呼吸新鲜空气");
}
});
//创建了一匿名子类的非匿名对象:p
Person p = new Person(){
@Override
public void eat() {
System.out.println("吃东西");
}
@Override
public void breath() {
System.out.println("好好呼吸");
}
};
Worker worker = new Worker();
method1(worker);//非匿名的类非匿名的对象
}
****************匿名接口************************
/*
* 接口的使用
* 1.接口使用上也满足多态性
* 2.接口,实际上就是定义了一种规范
* 3.开发中,体会面向接口编程!
*
*/
public class USBTest {
public static void main(String[] args) {
Computer com = new Computer();
//1.创建了接口的非匿名实现类的非匿名对象
Flash flash = new Flash();
com.transferData(flash);
//2. 创建了接口的非匿名实现类的匿名对象
com.transferData(new Printer());
//3. 创建了接口的匿名实现类的非匿名对象
USB phone = new USB(){
@Override
public void start() {
System.out.println("手机开始工作");
}
@Override
public void stop() {
System.out.println("手机结束工作");
}
};
com.transferData(phone);
//4. 创建了接口的匿名实现类的匿名对象
com.transferData(new USB(){
@Override
public void start() {
System.out.println("mp3开始工作");
}
@Override
public void stop() {
System.out.println("mp3结束工作");
}
});
}
}
class Computer{
public void transferData(USB usb){//USB usb = new Flash();
usb.start();
System.out.println("具体传输数据的细节");
usb.stop();
}
}
interface USB{
//常量:定义了长、宽、最大最小的传输速度等
void start();
void stop();
}
class Flash implements USB{
@Override
public void start() {
System.out.println("U盘开启工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
}
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开启工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}

关键字:interface

1.使用说明:

    1. 接口使用interface来定义
    1. Java中,接口和类是并列的两个结构
    1. 如何定义接口:定义接口中的成员
    • ​ 3.1 JDK7及以前:只能定义全局常量和抽象方法

      • 全局常量:public static final的.但是书写时,可以省略不写

      • 抽象方法:public abstract的

      • ​ 3.2 JDK8:除了定义全局常量和抽象方法之外,还可以定义静态方法、默认方法(略

    1. 接口中不能定义构造器的!意味着接口不可以实例化
    1. Java开发中,接口通过让类去实现(implements)的方式来使用.

    ​ 如果实现类覆盖了接口中的所抽象方法,则此实现类就可以实例化

    ​ 如果实现类没覆盖接口中所的抽象方法,则此实现类仍为一个抽象类

    1. Java类可以实现多个接口 --->弥补了Java单继承性的局限性

    ​ 格式:class AA extends BB implements CC,DD,EE

    1. 接口与接口之间可以继承,而且可以多继承
    1. 接口的具体使用,体现多态性
    1. 接口,实际上可以看做是一种规范

2.举例:

image-20220330110132615

class Computer{
public void transferData(USB usb){//USB usb = new Flash();
usb.start();
System.out.println("具体传输数据的细节");
usb.stop();
}
}
interface USB{
//常量:定义了长、宽、最大最小的传输速度等
void start();
void stop();
}
class Flash implements USB{
@Override
public void start() {
System.out.println("U盘开启工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
}
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开启工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}

体会:

  • 1.接口使用上也满足多态性
  • 2.接口,实际上就是定义了一种规范
  • 3.开发中,体会面向接口编程!

3.体会面向接口编程的思想

image-20220330110222382

面向接口编程:我们在应用程序中,调用的结构都是JDBC中定义的接口,不会出现具体某一个数据库厂商的API。

4.Java8中关于接口的新规范

//知识点1:接口中定义的静态方法,只能通过接口来调用。
//知识点2:通过实现类的对象,可以调用接口中的默认方法。
//如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法
//知识点3:如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没重写此方法的情况下,默认调用的是父类中的同名同参数的方法。-->类优先原则
//知识点4:如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,
//那么在实现类没重写此方法的情况下,报错。-->接口冲突。
//这就需要我们必须在实现类中重写此方法
//知识点5:如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
public void myMethod(){
method3();//调用自己定义的重写的方法
super.method3();//调用的是父类中声明的
//调用接口中的默认方法
CompareA.super.method3();
CompareB.super.method3();
}

代理模式

1.解决的问题

代理模式是Java开发中使用较多的一种设计模式。代理设计就是为其他对象提供一种代理以控制对这个对象的访问。

2.举例

interface NetWork{
public void browse();
}
//被代理类
class Server implements NetWork{
@Override
public void browse() {
System.out.println("真实的服务器访问网络");
}
}
//代理类
class ProxyServer implements NetWork{
private NetWork work;
public ProxyServer(NetWork work){ //NetWork work = new Server();
this.work = work;
}
public void check(){
System.out.println("联网之前的检查工作");
}
@Override
public void browse() {
check();
work.browse();
}
}

3.应用场景

image-20220330110801483

工厂的设计模式

  1. 解决的问题
    实现了创建者与调用者的分离,即将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。

  2. 具体模式

简单工厂模式:用来生产同一等级结构中的任意产品。(对于增加新的产品,需要修改已有代码)
工厂方法模式:用来生产同一等级结构中的固定产品。(支持增加任意产品)
抽象工厂模式:用来生产不同产品族的全部产品。(对于增加新的产品,无能为力;支持增加产品族)

类的结构:内部类

内部类:类的第五个成员
1.定义:Java中允许将一个类A声明在另一个类B中,则类A就是内部类,类B称为外部类.
2.内部类的分类:

成员内部类(静态、非静态 ) vs 局部内部类(方法内、代码块内、构造器内)

3.成员内部类的理解:
一方面,作为外部类的成员:

  • 调用外部类的结构
  • 可以被static修饰
  • 可以被4种不同的权限修饰

另一方面,作为一个类:

  • 类内可以定义属性、方法、构造器等
  • 可以被final修饰,表示此类不能被继承。言外之意,不使用final,就可以被继承
  • 可以被abstract修饰

4.成员内部类:

4.1如何创建成员内部类的对象?(静态的,非静态的)

//创建静态的Dog内部类的实例(静态的成员内部类):
Person.Dog dog = new Person.Dog();
//创建非静态的Bird内部类的实例(非静态的成员内部类):
//Person.Bird bird = new Person.Bird();//错误的
Person p = new Person();
Person.Bird bird = p.new Bird();

4.2如何在成员内部类中调用外部类的结构?

class Person{
String name = "小明";
public void eat(){}
//非静态成员内部类
class Bird{
String name = "杜鹃";
public void display(String name){
System.out.println(name);//方法的形参
System.out.println(this.name);//内部类的属性
System.out.println(Person.this.name);//外部类的属性
//Person.this.eat();
}
}
}

5.局部内部类的使用:

//返回一个实现了Comparable接口的类的对象
public Comparable getComparable(){
//创建一个实现了Comparable接口的类:局部内部类
//方式一:
// class MyComparable implements Comparable{
//
// @Override
// public int compareTo(Object o) {
// return 0;
// }
//
// }
//
// return new MyComparable();
//方式二:匿名内部类匿名对象
return new Comparable(){
@Override
public int compareTo(Object o) {
return 0;
}
};
}

注意点:

在局部内部类的方法中(比如:show如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,要求此局部变量声明为final的。

jdk 7及之前版本:要求此局部变量显式的声明为final的
jdk 8及之后的版本:可以省略final的声明

/*
* 在局部内部类的方法中(比如:show)如果调用局部内部类所声明的方法(比如:method)中的局部变量(比如:num)的话,
* 要求此局部变量声明为final的。
*
* jdk 7及之前版本:要求此局部变量显式的声明为final的
* jdk 8及之后的版本:可以省略final的声明
*
*/
public void method() {
//局部变量
int num = 10; //省略了final final int num = 10
class AA {
public void show() {
System.out.println(num);
}
}
}

总结:
成员内部类和局部内部类,在编译以后,都会生成字节码文件。
格式:成员内部类:外部类$内部类名.class
局部内部类:外部类$数字 内部类名.class

异常处理

异常

1.java.lang.Throwable
|-----java.lang.Error:一般不编写针对性的代码进行处理。
|-----java.lang.Exception:可以进行异常的处理
|------编译时异常(checked)
|-----IOException
|-----FileNotFoundException
|-----ClassNotFoundException
|------运行时异常(unchecked,RuntimeException)
|-----NullPointerException
|-----ArrayIndexOutOfBoundsException
|-----ClassCastException
|-----NumberFormatException
|-----InputMismatchException
|-----ArithmeticException

image-20220330135946608

2.从程序执行过程,看编译时异常和运行时异常

image-20220330140040228

编译时异常:执行javac.exe命名时,可能出现的异常
运行时异常:执行java.exe命名时,出现的异常

3.常见的异常类型,请举例说明:

以下是运行时异常*********

//ArithmeticException
@Test
public void test6(){
int a = 10;
int b = 0;
System.out.println(a / b);
}
//InputMismatchException
@Test
public void test5(){
Scanner scanner = new Scanner(System.in);
int score = scanner.nextInt();
System.out.println(score);
scanner.close();
}
//NumberFormatException
@Test
public void test4(){
String str = "123";
str = "abc";
int num = Integer.parseInt(str);
}
//ClassCastException
@Test
public void test3(){
Object obj = new Date();
String str = (String)obj;
}
//IndexOutOfBoundsException
@Test
public void test2(){
//ArrayIndexOutOfBoundsException
// int[] arr = new int[10];
// System.out.println(arr[10]);
//StringIndexOutOfBoundsException
String str = "abc";
System.out.println(str.charAt(3));
}
//NullPointerException
@Test
public void test1(){
// int[] arr = null;
// System.out.println(arr[3]);
String str = "abc";
str = null;
System.out.println(str.charAt(0));
}

以下是编译时异常*********

@Test
public void test7(){
// File file = new File("hello.txt");
// FileInputStream fis = new FileInputStream(file);
//
// int data = fis.read();
// while(data != -1){
// System.out.print((char)data);
// data = fis.read();
// }
//
// fis.close();
}

异常的处理、常见面试题

1.java异常处理的抓抛模型

image-20220330140355313

2.异常处理方式一:try-catch-finally

2.1 使用说明:

try{
//可能出现异常的代码
}catch(异常类型1 变量名1){
//处理异常的方式1
}catch(异常类型2 变量名2){
//处理异常的方式2
}catch(异常类型3 变量名3){
//处理异常的方式3
}
....
finally{
//一定会执行的代码
}

说明:

1. finally是可选的。
2. 使用try将可能出现异常代码包装起来,在执行过程中,一旦出现异常,就会生成一个对应异常类的对象,根据此对象的类型,去catch中进行匹配
3. 一旦try中的异常对象匹配到某一个catch时,就进入catch中进行异常的处理。一旦处理完成,就跳出当前的try-catch结构(在没写finally的情况。继续执行其后的代码
4. catch中的异常类型如果没子父类关系,则谁声明在上,谁声明在下无所谓。
catch中的`异常类型如果满足子父类关系,则要求子类一定声明在父类的上面`。否则,报错
5. `常用的异常对象处理的方式: ① String getMessage() ② printStackTrace()`
6. `在try结构中声明的变量,出了try结构以后,就不能再被调用`
7. try-catch-finally结构可以嵌套

总结:如何看待代码中的编译时异常和运行时异常?

体会1:使用try-catch-finally处理编译时异常,使得程序在编译时就不再报错,但是运行时仍可能报错。相当于我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。

体会2:开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了。针对于编译时异常,我们说一定要考虑异常的处理。

2.2:finally的说明

  • finally是可选的
  • finally中声明的是一定会被执行的代码。即使catch中又出现异常了,try中return语句,catch中return语句等情况。
  • 像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。
3.异常处理方式二:

"throws + 异常类型"写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。
一旦当方法体执行时,出现异常,仍会在异常代码处生成一个异常类的对象,此对象满足throws后异常类型时,就会被抛出。异常代码后续的代码,就不再执行!

对比两种处理方式
  • try-catch-finally:真正的将异常给处理掉了。
  • throws的方式只是将异常抛给了方法的调用者。并没真正将异常处理掉。

体会开发中应该如何选择两种处理方式?

  • 如果父类中被重写的方法没用throws方式处理异常,则子类重写的方法也不能使用throws,意味着如果子类重写的方法中异常,必须使用try-catch-finally方式处理。

  • 执行的方法a中,先后又调用了另外的几个方法,这几个方法是递进关系执行的。我们建议这几个方法使用throws的方式进行处理。而执行的方法a可以考虑使用try-catch-finally方式进行处理。

补充:
方法重写的规则之一:
子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型

[面试题]

final、finally、finalize三者的区别?
final用于声明属性、方法和类,分别表示属性为常量、方法不可覆盖、类不可被继承(不能再派生出新的子类)
finally作为异常处理的一部分,它只能用在try/catch语句中
finalize是Object类中的一个方法,在垃圾收集器执行的时候会调用被回收对象的finalize()方法

类似:
throw 和 throws
throw:表示在方法内手动抛出某种异常对象
throws:异常处理的一种方式,写在方法的声明处。指明此方法执行时,可能会抛出的异常类型。谁调用我我就抛给谁

Collection 和 Collections
Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法,List,Set,Queue接口都继承Collection。
Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排 序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

String 、StringBuffer、StringBuilder
String:不可变的字符序列;底层使用char[]存储
StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[]存储
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储

ArrayList 、 LinkedList
ArrayList和Vector使用了数组的实现,可以认为ArrayList或者Vector封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。
LinkedList使用了循环双向链表数据结构。与基于数组ArrayList相比,这是两种截然不同的实现技术,这也决定了它们将适用于完全不同的工作场景。

HashMap 、LinkedHashMap

重写、重载

结构不相似的:
抽象类、接口

image-20220330140818745

== 、 equals()
:基本类型比较的是值,引用类型比较的是地址值
equals:Object类的一个方法,默认实现了
的功能;但是多数包装类都重写了equals方法,通用用来判断对象中的属性值是否相等(比较的是值)

sleep()、wait()
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:

1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

手动抛出异常对象

1.使用说明

在程序执行中,除了自动抛出异常对象的情况之外,我们还可以手动的throw一个异常类的对象。

2.[面试题]

throw 和 throws区别:
throw 表示抛出一个异常类的对象,生成异常对象的过程。声明在方法体内。
throws 属于异常处理的一种方式,声明在方法的声明处。

3.典型例题
class Student{
private int id;
public void regist(int id) throws Exception {
if(id > 0){
this.id = id;
}else{
//手动抛出异常对象
// throw new RuntimeException("您输入的数据非法!");
// throw new Exception("您输入的数据非法!");
throw new MyException("不能输入负数");
}
}
@Override
public String toString() {
return "Student [id=" + id + "]";
}
}

自定义异常类

/*
* 如何自定义异常类?
* 1. 继承于现有的异常结构:RuntimeException 、Exception
* 2. 提供全局常量:serialVersionUID
* 3. 提供重载的构造器
*
*/
public class MyException extends Exception{
static final long serialVersionUID = -7034897193246939L;
public MyException(){
}
public MyException(String msg){
super(msg);
}
}

多线程

程序、进程、线程的理解

  1. 程序(programm)
    概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码

  2. 进程(process)
    概念:程序的一次执行过程,或是正在运行的一个程序
    说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

  3. 线程(thread)
    概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。
    说明:线程作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小

image-20220330142239126

补充:
内存结构:

image-20220330142251584

进程可以细化为多个线程。
每个线程,拥有自己独立的:栈、程序计数器
多个线程,共享同一个进程中的结构:方法区、堆。

并行与并发

  1. 单核CPU与多核CPU的理解
    单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过,那么CPU就好比收费人员。如果某个人不想交钱,那么收费人员可以把他“挂起”(晾着他,等他想通了,准备好了钱,再去收费。)但是因为CPU时间单元特别短,因此感觉不出来。
    如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
    一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
  2. 并行与并发的理解
    并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
    并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、一个人做多件事

创建多线程的四种方式

方式一:继承Thread类的方式:

1. 创建一个继承于Thread类的子类
2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中
3. 创建Thread类的子类的对象
4. 通过此对象调用start():①启动当前线程 ② 调用当前线程的run()

说明两个问题:
问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().

方式二:实现Runnable接口的方式:

1. 创建一个实现了Runnable接口的类
2. 实现类去实现Runnable中的抽象方法:run()
3. 创建实现类的对象
4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5. 通过Thread类的对象调用start()

继承Threadh和实现Runnable两种方式的对比:

开发中:优先选择:实现Runnable接口的方式
原因:

  1. 实现的方式没有类的单继承性的局限性

  2. 实现的方式更适合来处理多个线程共享数据的情况。

联系:public class Thread implements Runnable

相同点:

两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
目前两种方式,要想启动线程,都是调用的Thread类中的start()。

方式三:实现Callable接口(了解)。 --- JDK 5.0新增

//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

说明:

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

    1. call()可以返回值的。
    1. call()可以抛出异常,被外面的操作捕获,获取异常的信息
    1. Callable是支持泛型的

方式四:使用线程池(了解)

class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}

说明:

  • 1.提高响应速度(减少了创建新线程的时间)
  • 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 3.便于线程管理
  • corePoolSize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程没任务时最多保持多长时间后会终止

Thread类中的常用方法

  1. start():启动当前线程;调用当前线程的run()
  2. run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
  3. currentThread():静态方法,返回执行当前代码的线程
  4. getName():获取当前线程的名字
  5. setName():设置当前线程的名字
  6. yield():释放当前cpu的执行权
  7. join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
  8. stop():已过时。当执行此方法时,强制结束当前线程。
  9. sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
  10. isAlive():判断当前线程是否存活

线程的优先级:
1.
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级

2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。

补充:线程的分类
一种是守护线程,一种是用户线程

Thread的生命周期

图示:

image-20220330143944260

说明:
1.生命周期关注两个概念:状态、相应的方法
2.关注:状态a-->状态b:哪些方法执行了(回调方法)
某个方法主动调用:状态a-->状态b
3.阻塞:临时状态,不可以作为最终状态
死亡:最终状态。

线程的同步机制

1.背景

例子:创建个窗口卖票,总票数为100张.使用实现Runnable接口的方式

1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题

2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

2.Java解决方案:同步机制

在Java中,我们通过同步机制,来解决线程的安全问题。

方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}

例子:

实现Runnable接口的线程类

class Window1 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {//此时的this:唯一的Window1的对象
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

继承Thread类的线程类

class Window2 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while(true){
//正确的
// synchronized (obj){
synchronized (Window2.class){//Class clazz = Window2.class,Window2.class只会加载一次
//错误的方式:this代表着t1,t2,t3三个对象
// synchronized (this){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 t1 = new Window2();
Window2 t2 = new Window2();
Window2 t3 = new Window2();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

说明:
1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁

实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器

继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器

方式二:同步方法

​ 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

同步方法声明:

private synchronized void show() {//同步监视器:this
...
}

例子:

实现Runnable接口的线程类,此时的同步监视器为this

class Window3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show() {//同步监视器:this
//synchronized (this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
//}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

继承Thread类的线程类,此时的同步监视器为Class对象

注意点:此时的同步方法需要写成静态方法

class Window4 extends Thread {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private static synchronized void show() {//同步监视器:Window4.class
//private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
Window4 t1 = new Window4();
Window4 t2 = new Window4();
Window4 t3 = new Window4();
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

关于同步方法的总结

  1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

  2. 非静态的同步方法,同步监视器是:this (适合用在实现Runnable接口的线程)

    静态的同步方法,同步监视器是:当前类本身 (适合用在继承Thread类的线程)

方式三:Lock锁 --- JDK5.0新增
class Window implements Runnable {
private int ticket = 100;
//1.实例化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//2.调用锁定方法lock()
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
} else {
break;
}
} finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}

面试题:synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())

使用的优先顺序

Lock ---> 同步代码块(已经进入了方法体,分配了相应资源 ) --->同步方法(在方法体之外)

3.利弊

同步的方式,解决了线程的安全问题。---好处
操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

4.面试题:

Java是如何解决线程安全问题的,有几种方式?并对比几种方式的不同

解决方案:同步机制,同步代码块,同步方法,Lock锁 ,synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())

面试题:synchronized和Lock方式解决线程安全问题的对比

线程安全的单例模式(懒汉式)

使用同步机制将单例模式中的懒汉式改写为线程安全的。

class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
//
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}

死锁问题

1.死锁的理解:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

2.说明:

  • 1出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续
  • 2我们使用同步时,要避免出现死锁。

3.举例:

public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}

线程通信

1.线程通信涉及到的三个方法:
  • wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
  • notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
  • notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
2.说明:
  • 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
  • 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
  • 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
class Number implements Runnable {
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
obj.notify();
if (number <= 100) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}

错误示范

class Number implements Runnable {
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
this.notify(); //这里的this指Number类对象,但是同步监视器是obj,必须要用监视器对象调用wait/notify/notifyAll,否则报错
...
}
}
}

image-20220330222020114

3.面试题:

面试题:sleep() 和 wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:

​ 1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
​ 2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
​ 3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

4.小结释放锁/不释放的操作:

image-20220330143944260

image-20220330175823514

image-20220330175834166

Java常用类

String类

1.概述

String:字符串,使用一对""引起来表示。
1.String声明为final的,不可被继承
2.String实现了Serializable接口:表示字符串是支持序列化的。
实现了Comparable接口:表示String可以比较大小
3.String内部定义了final char[] value用于存储字符串数据
4.通过字面量的方式(区别于new给一个字符串赋值,此时的字符串值声明在字符串常量池中)。
5.字符串常量池中是不会存储相同内容(使用String类的equals()比较,返回true)的字符串的。

2.String的不可变性

2.1 说明

1.当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值。
2.当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
3.当调用String的replace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。

2.2 代码举例
String s1 = "abc";//字面量的定义方式
String s2 = "abc";
s1 = "hello";
System.out.println(s1 == s2);//比较s1和s2的地址值
System.out.println(s1);//hello
System.out.println(s2);//abc
System.out.println("*****************");
String s3 = "abc";
s3 += "def";
System.out.println(s3);//abcdef
System.out.println(s2);
System.out.println("*****************");
String s4 = "abc";
String s5 = s4.replace('a', 'm');
System.out.println(s4);//abc
System.out.println(s5);//mbc
2.3 String对象的创建方式

image-20220331110000052

2.4 图示

image-20220331110035643

3.String实例化的不同方式

方式一:通过字面量定义的方式
//通过字面量定义的方式:此时的s1和s2的数据"javaEE"声明在方法区中的字符串常量池中。
String s1 = "javaEE";
String s2 = "javaEE";

此时的s1和s2的数据"javaEE"声明在方法区中的字符串常量池中。

方式二:通过new + 构造器的方式
//通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。
String s3 = new String("javaEE");
String s4 = new String("javaEE");
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//false
System.out.println(s1 == s4);//false
System.out.println(s3 == s4);//false

通过new + 构造器的方式:此时的s3和s4保存的地址值,是数据在堆空间中开辟空间以后对应的地址值。

面试题

String s = new String("abc");方式创建对象,在内存中创建了几个对象?
两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:"abc"

图示

image-20220331110512937

4. 字符串拼接方式赋值的对比

4.1 说明

1.常量与常量的拼接结果在常量池。且常量池中不会存在相同内容的常量。
2.只要其中一个是变量,结果就在堆中。
3.如果拼接的结果调用intern()方法,返回值就在常量池中

4.2 代码举例
String s1 = "javaEE";
String s2 = "hadoop";
String s3 = "javaEEhadoop";
String s4 = "javaEE" + "hadoop";
String s5 = s1 + "hadoop";
String s6 = "javaEE" + s2;
String s7 = s1 + s2;
System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//false
System.out.println(s3 == s7);//false
System.out.println(s5 == s6);//false
System.out.println(s5 == s7);//false
System.out.println(s6 == s7);//false
String s8 = s6.intern();//返回值得到的s8使用的常量值中已经存在的“javaEEhadoop”
System.out.println(s3 == s8);//true
****************************
String s1 = "javaEEhadoop";
String s2 = "javaEE";
String s3 = s2 + "hadoop";
System.out.println(s1 == s3);//false
final String s4 = "javaEE";//s4:常量
String s5 = s4 + "hadoop";
System.out.println(s1 == s5);//true

5.常用方法:

int length():返回字符串的长度: return value.length
char charAt(int index): 返回某索引处的字符return value[index]
boolean isEmpty():判断是否是空字符串:return value.length == 0
String toLowerCase():使用默认语言环境,将 String 中的所字符转换为小写
String toUpperCase():使用默认语言环境,将 String 中的所字符转换为大写
String trim():返回字符串的副本,忽略前导空白和尾部空白
boolean equals(Object obj):比较字符串的内容是否相同
boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
int compareTo(String anotherString):比较两个字符串的大小
String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
注:indexOf和lastIndexOf方法如果未找到都是返回-1
替换:
String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所 oldChar 得到的。
String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所匹配字面值目标序列的子字符串。
String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所匹配给定的正则表达式的子字符串。
String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。
匹配:
boolean matches(String regex):告知此字符串是否匹配给定的正则表达式。
切片:
String[] split(String regex):根据给定正则表达式的匹配拆分此字符串。
String[] split(String regex, int limit):根据匹配给定的正则表达式来拆分此字符串,最多不超过limit个,如果超过了,剩下的全部都放到最后一个元素中。

6. String与其它结构的转换

6.1 与基本数据类型、包装类之间的转换

String --> 基本数据类型、包装类:调用包装类的静态方法:parseXxx(str)
基本数据类型、包装类 --> String:调用String重载的valueOf(xxx)

@Test
public void test1(){
String str1 = "123";
// int num = (int)str1;//错误的
int num = Integer.parseInt(str1);
String str2 = String.valueOf(num);//"123"
String str3 = num + "";
System.out.println(str1 == str3);
}
6.2 与字符数组之间的转换

String --> char[]:调用String的toCharArray()
char[] --> String:调用String的构造器

@Test
public void test2(){
String str1 = "abc123"; //题目: a21cb3
char[] charArray = str1.toCharArray();
for (int i = 0; i < charArray.length; i++) {
System.out.println(charArray[i]);
}
char[] arr = new char[]{'h','e','l','l','o'};
String str2 = new String(arr);
System.out.println(str2);
}
6.3 与字节数组之间的转换

编码:String --> byte[]:调用String的getBytes()
解码:byte[] --> String:调用String的构造器

编码:字符串 -->字节 (看得懂 --->看不懂的二进制数据)
解码:编码的逆过程,字节 --> 字符串 (看不懂的二进制数据 ---> 看得懂

说明:解码时,要求解码使用的字符集必须与编码时使用的字符集一致,否则会出现乱码。

@Test
public void test3() throws UnsupportedEncodingException {
String str1 = "abc123中国";
byte[] bytes = str1.getBytes();//使用默认的字符集,进行编码。
System.out.println(Arrays.toString(bytes));
byte[] gbks = str1.getBytes("gbk");//使用gbk字符集进行编码。
System.out.println(Arrays.toString(gbks));
System.out.println("******************");
String str2 = new String(bytes);//使用默认的字符集,进行解码。
System.out.println(str2);
String str3 = new String(gbks);
System.out.println(str3);//出现乱码。原因:编码集和解码集不一致!
String str4 = new String(gbks, "gbk");
System.out.println(str4);//没出现乱码。原因:编码集和解码集一致!
}
6.4 与StringBuffer、StringBuilder之间的转换

String -->StringBuffer、StringBuilder: 调用StringBuffer、StringBuilder构造器
StringBuffer、StringBuilder -->String: ①调用String构造器;②StringBuffer、StringBuilder的toString()

7. JVM中字符串常量池存放位置说明:

jdk 1.6 (jdk 6.0 ,java 6.0):字符串常量池存储在方法区(永久区)
jdk 1.7:字符串常量池存储在堆空间
jdk 1.8:字符串常量池存储在方法区(元空间)

StringBuffer、StringBuilder

1.String、StringBuffer、StringBuilder三者的对比

String:不可变的字符序列;底层使用char[]存储
StringBuffer:可变的字符序列;线程安全的,效率低;底层使用char[]存储
StringBuilder:可变的字符序列;jdk5.0新增的,线程不安全的,效率高;底层使用char[]存储

2.StringBuffer与StringBuilder的内存解析

以StringBuffer为例:

String str = new String();//char[] value = new char[0];
String str1 = new String("abc");//char[] value = new char[]{'a','b','c'};
StringBuffer sb1 = new StringBuffer();//char[] value = new char[16];底层创建了一个长度是16的数组。
System.out.println(sb1.length());//
sb1.append('a');//value[0] = 'a';
sb1.append('b');//value[1] = 'b';
StringBuffer sb2 = new StringBuffer("abc");//char[] value = new char["abc".length() + 16];
//问题1. System.out.println(sb2.length());//3

扩容机制:

如果要添加的数据底层数组盛不下了,那就需要扩容底层的数组。默认情况下,扩容为原来容量的2倍 + 2,同时将原数组中的元素复制到新的数组中。

指导意义:开发中建议大家使用:StringBuffer(int capacity) 或 StringBuilder(int capacity)

3.对比String、StringBuffer、StringBuilder三者的执行效率

从高到低排列:StringBuilder > StringBuffer > String

4.StringBuffer、StringBuilder中的常用方法

增:append(xxx)
删:delete(int start,int end)
改:setCharAt(int n ,char ch) / replace(int start, int end, String str)
查:charAt(int n )
插:insert(int offset, xxx)
长度:length();
*遍历:for() + charAt() / toString(

JDK 8之前日期时间API

示意图

image-20220331190944930

image-20220331191000205

1.获取系统当前时间

System类中的currentTimeMillis():返回时间戳

long time = System.currentTimeMillis();
//返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。
//称为时间戳

2. java.util.Date类与java.sql.Date类

关系:子父类的关系

java.util.Date类
|---java.sql.Date类

方法的使用

构造器的使用(java.util.Date)
构造器一:创建一个对应当前时间的Date对象 Date date = new Date()

​ 构造器二:new Date(时间戳):创建指定毫秒数的Date对象 Date date = new Date(79834523054L)

构造器的使用(java.sql.Date)

​ 构造器一:new Date(时间戳):创建指定毫秒数的Date对象 Date date = new Date(79834523054L)

​ 两个方法的使用(既适合java.util.Date 又适合java.sql.Date)
toString():显示当前的年、月、日、时、分、秒
getTime():获取当前Date对象对应的毫秒数。(时间戳)

java.sql.Date对应着数据库中的日期类型的变量

例子
@Test
public void test2() {
//构造器一:Date():创建一个对应当前时间的Date对象
Date date1 = new Date();
System.out.println(date1.toString());//Sat Feb 16 16:35:31 GMT+08:00 2019
System.out.println(date1.getTime());//1550306204104
//构造器二:创建指定毫秒数的Date对象
Date date2 = new Date(155030620410L);
System.out.println(date2.toString());
//创建java.sql.Date对象
java.sql.Date date3 = new java.sql.Date(35235325345L);
System.out.println(date3);//1971-02-13
}
如何将java.util.Date对象转换为java.sql.Date对象

思路:

image-20220331192523273

//情况一:
// Date date4 = new java.sql.Date(2343243242323L);
// java.sql.Date date5 = (java.sql.Date) date4;
//情况二:
Date date6 = new Date();
java.sql.Date date7 = new java.sql.Date(date6.getTime()); //sql.data(时间戳)

3. java.text.SimpleDataFormat类

SimpleDateFormat对日期Date类的格式化解析

1.两个操作

1.1 格式化:日期 --->字符串
1.2 解析:格式化的逆过程,字符串 ---> 日期

2.SimpleDateFormat的实例化:new + 构造器
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//格式化
String format1 = sdf1.format(date);
System.out.println(format1);//2019-02-18 11:48:27
//解析:要求字符串必须是符合SimpleDateFormat识别的格式(通过构造器参数体现),
//否则,抛异常
Date date2 = sdf1.parse("2020-02-18 11:48:27");
System.out.println(date2)

image-20220331193139003

4.Calendar类:日历类、抽象类

@Test
public void testCalendar(){
//1.实例化
//方式一:创建其子类(GregorianCalendar)的对象
//方式二:调用其静态方法getInstance()
Calendar calendar = Calendar.getInstance();
// System.out.println(calendar.getClass());
//2.常用方法
//get()
int days = calendar.get(Calendar.DAY_OF_MONTH); //这个月的第几天
System.out.println(days);
System.out.println(calendar.get(Calendar.DAY_OF_YEAR));//这一年的第几天
//set()
//calendar可变性
calendar.set(Calendar.DAY_OF_MONTH,22); //设置这个月的天数
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
//add()
calendar.add(Calendar.DAY_OF_MONTH,-3);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
//getTime():日历类---> Date
Date date = calendar.getTime();
System.out.println(date);
//setTime():Date ---> 日历类
Date date1 = new Date();
calendar.setTime(date1);
days = calendar.get(Calendar.DAY_OF_MONTH);
System.out.println(days);
}

注意:
获取月份时:一月是0,二月是1,以此类推,12月是11
获取星期时:周日是1,周二是2 , 。。。。周六是7

JDK8中新日期时间API

1.日期时间API的迭代:

第一代:jdk 1.0 Date类
第二代:jdk 1.1 Calendar类,一定程度上替换Date类
第三代:jdk 1.8 提出了新的一套API

2.前两代存在的问题举例:

可变性:像日期和时间这样的类应该是不可变的。
偏移性:Date中的年份是从1900开始的,而月份都从0开始。

@Test
public void testDate(){
//偏移量
Date date1 = new Date(2020 - 1900,9 - 1,8); //2020-09-08
System.out.println(date1);//Tue Sep 08 00:00:00 GMT+08:00 2020
}

格式化:格式化只对Date用,Calendar则不行。
此外,它们也不是线程安全的;不能处理闰秒等。

3.java 8 中新的日期时间API涉及到的包

image-20220331223534978

4.本地日期、本地时间、本地日期时间的使用:LocalDate / LocalTime / LocalDateTime

4.1 说明:
① 分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的本地日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
② LocalDateTime相较于LocalDate、LocalTime,使用频率要高
③ 类似于Calendar

4.2 常用方法:

image-20220331223617645

/*
LocalDate、LocalTime、LocalDateTime 的使用
说明:
1.LocalDateTime相较于LocalDate、LocalTime,使用频率要高
2.类似于Calendar
*/
@Test
public void test1() {
//now():获取当前的日期、时间、日期+时间
LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDate);
System.out.println(localTime);
System.out.println(localDateTime);
//of():设置指定的年、月、日、时、分、秒。没有偏移量
LocalDateTime localDateTime1 = LocalDateTime.of(2020, 10, 6, 13, 23, 43);
System.out.println(localDateTime1);
//getXxx():获取相关的属性
System.out.println(localDateTime.getDayOfMonth());
System.out.println(localDateTime.getDayOfWeek());
System.out.println(localDateTime.getMonth());
System.out.println(localDateTime.getMonthValue());
System.out.println(localDateTime.getMinute());
//体现不可变性
//withXxx():设置相关的属性
LocalDate localDate1 = localDate.withDayOfMonth(22);
System.out.println(localDate);
System.out.println(localDate1);
LocalDateTime localDateTime2 = localDateTime.withHour(4);
System.out.println(localDateTime);
System.out.println(localDateTime2);
//不可变性
LocalDateTime localDateTime3 = localDateTime.plusMonths(3);
System.out.println(localDateTime);
System.out.println(localDateTime3);
LocalDateTime localDateTime4 = localDateTime.minusDays(6);
System.out.println(localDateTime);
System.out.println(localDateTime4);
}

5.时间点:Instant

5.1 说明:
① 时间线上的一个瞬时点。 概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC开始的秒数。)
② 类似于 java.util.Date类

image-20220331223746348

/*
Instant的使用
类似于 java.util.Date类
*/
@Test
public void test2(){
//now():获取本初子午线对应的标准时间
Instant instant = Instant.now();
System.out.println(instant);//2019-02-18T07:29:41.719Z
//添加时间的偏移量
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);//2019-02-18T15:32:50.611+08:00
//toEpochMilli():获取自1970年1月1日0时0分0秒(UTC)开始的毫秒数 ---> Date类的getTime()
long milli = instant.toEpochMilli();
System.out.println(milli);
//ofEpochMilli():通过给定的毫秒数,获取Instant实例 -->Date(long millis)
Instant instant1 = Instant.ofEpochMilli(1550475314878L);
System.out.println(instant1);
}

6.日期时间格式化类:DateTimeFormatter

6.1 说明:

① 格式化或解析日期、时间
② 类似于SimpleDateFormat

6.2 常用方法:

① 实例化方式
预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
本地化相关的格式。如:ofLocalizedDateTime(FormatStyle.LONG)
自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

② 常用方法:

image-20220331224048934

@Test
public void test3() {
// 方式一:预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
//格式化:日期-->字符串
LocalDateTime localDateTime = LocalDateTime.now();
String str1 = formatter.format(localDateTime);
System.out.println(localDateTime);
System.out.println(str1);//2019-02-18T15:42:18.797
//解析:字符串 -->日期
TemporalAccessor parse = formatter.parse("2019-02-18T15:42:18.797");
System.out.println(parse);
// 方式二:
// 本地化相关的格式。如:ofLocalizedDateTime()
// FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);
//格式化
String str2 = formatter1.format(localDateTime);
System.out.println(str2);//2019年2月18日 下午03时47分16秒
// 本地化相关的格式。如:ofLocalizedDate()
// FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDate
DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
//格式化
String str3 = formatter2.format(LocalDate.now());
System.out.println(str3);//2019-2-18
// 重点: 方式三:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
//格式化
String str4 = formatter3.format(LocalDateTime.now());
System.out.println(str4);//2019-02-18 03:52:09
//解析
TemporalAccessor accessor = formatter3.parse("2019-02-18 03:52:09");
System.out.println(accessor);
}

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

// 重点:自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
//格式化
String str4 = formatter3.format(LocalDateTime.now());
System.out.println(str4);//2019-02-18 03:52:09
//解析
TemporalAccessor accessor = formatter3.parse("2019-02-18 03:52:09");
System.out.println(accessor);

7.其它API的使用

7.1 带时区的日期时间:ZonedDateTime / ZoneId

举例:

// ZoneId:类中包含了所的时区信息
@Test
public void test1(){
//getAvailableZoneIds():获取所的ZoneId
Set<String> zoneIds = ZoneId.getAvailableZoneIds();
for(String s : zoneIds){
System.out.println(s);
}
System.out.println();
//获取“Asia/Tokyo”时区对应的时间
LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(localDateTime);
}
//ZonedDateTime:带时区的日期时间
@Test
public void test2(){
//now():获取本时区的ZonedDateTime对象
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);
//now(ZoneId id):获取指定时区的ZonedDateTime对象
ZonedDateTime zonedDateTime1 = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(zonedDateTime1);
}

7.2 时间间隔:Duration--用于计算两个“时间”间隔,以秒和纳秒为基准

image-20220331225653152

举例:

@Test
public void test3(){
LocalTime localTime = LocalTime.now();
LocalTime localTime1 = LocalTime.of(15, 23, 32);
//between():静态方法,返回Duration对象,表示两个时间的间隔
Duration duration = Duration.between(localTime1, localTime);
System.out.println(duration);
System.out.println(duration.getSeconds());
System.out.println(duration.getNano());
LocalDateTime localDateTime = LocalDateTime.of(2016, 6, 12, 15, 23, 32);
LocalDateTime localDateTime1 = LocalDateTime.of(2017, 6, 12, 15, 23, 32);
Duration duration1 = Duration.between(localDateTime1, localDateTime);
System.out.println(duration1.toDays());
}

7.3 日期间隔:Period --用于计算两个“日期”间隔,以年、月、日衡量

image-20220331225724408

举例:

@Test
public void test4(){
LocalDate localDate = LocalDate.now();
LocalDate localDate1 = LocalDate.of(2028, 3, 18);
Period period = Period.between(localDate, localDate1);
System.out.println(period);
System.out.println(period.getYears());
System.out.println(period.getMonths());
System.out.println(period.getDays());
Period period1 = period.withYears(2);
System.out.println(period1);
}

7.4 日期时间校正器:TemporalAdjuster

举例:

@Test
public void test5(){
//获取当前日期的下一个周日是哪天?
TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);
LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster);
System.out.println(localDateTime);
//获取下一个工作日是哪天?
LocalDate localDate = LocalDate.now().with(new TemporalAdjuster(){
@Override
public Temporal adjustInto(Temporal temporal) {
LocalDate date = (LocalDate)temporal;
if(date.getDayOfWeek().equals(DayOfWeek.FRIDAY)){
return date.plusDays(3);
}else if(date.getDayOfWeek().equals(DayOfWeek.SATURDAY)){
return date.plusDays(2);
}else{
return date.plusDays(1);
}
}
});
System.out.println("下一个工作日是:" + localDate);
}

Java比较器

1.Java比较器的使用背景:

Java中的对象,正常情况下,只能进行比较:== 或 != 。不能使用 > 或 < 的
但是在开发场景中,我们需要对多个对象进行排序,言外之意,就需要比较对象的大小
如何实现?使用两个接口中的任何一个:Comparable 或 Comparator

2.自然排序:使用Comparable接口

2.1 说明
  • 像String、包装类等实现了Comparable接口,重写了compareTo(obj)方法,给出了比较两个对象大小的方式。
  • 像String、包装类重写compareTo()方法以后,进行了从小到大的排列
  • 重写compareTo(obj)的规则:
    • 如果当前对象this大于形参对象obj,则返回正整数,
    • 如果当前对象this小于形参对象obj,则返回负整数,
    • 如果当前对象this等于形参对象obj,则返回零。
  • 对于自定义类来说,如果需要排序,我们可以让自定义类实现Comparable接口,重写compareTo(obj)方法。在compareTo(obj)方法中指明如何排序
2.2 自定义类实现Comparable接口代码举例:
public class Goods implements Comparable{
private String name;
private double price;
public Goods() {
}
public Goods(String name, double price) {
this.name = name;
this.price = price;
}
...
//指明商品比较大小的方式:按照价格从低到高排序,再按照产品名称从高到低排序
@Override
public int compareTo(Object o) {
// System.out.println("**************");T
if(o instanceof Goods){
Goods goods = (Goods)o;
//方式一:
if(this.price > goods.price){
return 1;
}else if(this.price < goods.price){
return -1;
}else{
// return 0;
return -this.name.compareTo(goods.name);
}
//方式二:
// return Double.compare(this.price,goods.price);
}
// return 0;
throw new RuntimeException("传入的数据类型不一致!");
}
}

3.定制排序:使用Comparator接口

3.1 说明

1.背景:
当元素的类型没实现java.lang.Comparable接口而又不方便修改代码,或者实现了java.lang.Comparable接口的排序规则不适合当前的操作,那么可以考虑使用 Comparator 的对象来排序

2.重写compare(Object o1,Object o2)方法,比较o1和o2的大小:

  • 如果方法返回正整数,则表示o1大于o2;
  • 如果返回0,表示相等;
  • 返回负整数,表示o1小于o2
3.2 实现Comparator接口的代码举例:
@Test
public void test3(){
String[] arr = new String[]{"AA","CC","KK","MM","GG","JJ","DD"};
Arrays.sort(arr,new Comparator(){
//按照字符串从大到小的顺序排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof String && o2 instanceof String){
String s1 = (String) o1;
String s2 = (String) o2;
return -s1.compareTo(s2);
}
// return 0;
throw new RuntimeException("输入的数据类型不一致");
}
});
System.out.println(Arrays.toString(arr));
}
@Test
public void test4(){
Goods[] arr = new Goods[6];
arr[0] = new Goods("lenovoMouse",34);
arr[1] = new Goods("dellMouse",43);
arr[2] = new Goods("xiaomiMouse",12);
arr[3] = new Goods("huaweiMouse",65);
arr[4] = new Goods("huaweiMouse",224);
arr[5] = new Goods("microsoftMouse",43);
Arrays.sort(arr, new Comparator() {
//指明商品比较大小的方式:按照产品名称从低到高排序,再按照价格从高到低排序
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof Goods && o2 instanceof Goods){
Goods g1 = (Goods)o1;
Goods g2 = (Goods)o2;
if(g1.getName().equals(g2.getName())){
return -Double.compare(g1.getPrice(),g2.getPrice());
}else{
return g1.getName().compareTo(g2.getName());
}
}
throw new RuntimeException("输入的数据类型不一致");
}
});
System.out.println(Arrays.toString(arr));
}
两种排序方式对比
  • Comparable接口的方式一旦一定,保证Comparable接口实现类的对象在任何位置都可以比较大小。
  • Comparator接口属于临时性的比较。

其他类

1.System类

System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包。
由于该类的构造器是private的,所以无法创建该类的对象,也就是无法实例化该类。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。

成员变量

System类内部包含in、out和err三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。

成员方法

native long currentTimeMillis()
该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。
void exit(int status)
该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表异常退出。 使用该方法可以在图形界面编程中实现程序的退出功能等。

void gc()
该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。
String getProperty(String key): :
该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表示:

image-20220401004705929

2.Math类

java.lang.Math提供了一系列静态方法用于科学计算。其方法的参数和返回值类型一般为double型。

3.BigInteger类、BigDecimal类

说明:
① java.math包的BigInteger可以表示不可变的任意精度的整数。
② 要求数字精度比较高,用到java.math.BigDecimal类

代码举例:

image-20220401004353809

枚举类和注解

枚举类的使用

1.枚举类的说明:

  • 1.枚举类的理解:类的对象只有有限个,确定的。我们称此类为枚举类
  • 2.当需要定义一组常量时,强烈建议使用枚举类
  • 3.如果枚举类中只有一个对象,则可以作为单例模式的实现方式。

2.如何自定义枚举类?步骤:

  1. 私有化类的构造器,保证不能在类的外部创建其对象
  2. 在类的内部创建枚举类的实例。声明为:public static final
  3. 对象如果有实例变量,应该声明为private final,并在构造器中初始化
//自定义枚举类
class Season{
//1.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;
//2.私化类的构造器,并给对象属性赋值
private Season(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//3.提供当前枚举类的多个对象:public static final的
public static final Season SPRING = new Season("春天","春暖花开");
public static final Season SUMMER = new Season("夏天","夏日炎炎");
public static final Season AUTUMN = new Season("秋天","秋高气爽");
public static final Season WINTER = new Season("冬天","冰天雪地");
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
//4.其他诉求1:提供toString()
@Override
public String toString() {
return "Season{" +
"seasonName='" + seasonName + '\'' +
", seasonDesc='" + seasonDesc + '\'' +
'}';
}
}

3. jdk 5.0 新增使用enum定义枚举类。步骤:

使用说明

  • 使用 enum 定义的枚举类默认继承了 java.lang.Enum类,因此不能再继承其他类
  • 枚举类的构造器只能使用 private 权限修饰符
  • 枚举类的所有实例必须在枚举类中显式列出(, 分隔 ; 结尾)。列出的实例系统会自动添加 public static final 修饰
  • 必须在枚举类的第一行声明枚举类对象

了解:JDK 1.5 中可以在 switch 表达式中使用Enum定义的枚举类的对象作为表达式, case 子句可以直接使用枚举值的名字, 无需添加枚举类作为限定

//使用enum关键字枚举类
enum Season1 {
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
//相当于 public static final Season SPRING = new SPRING("春天","春暖花开");
SPRING("春天","春暖花开"),
SUMMER("夏天","夏日炎炎"),
AUTUMN("秋天","秋高气爽"),
WINTER("冬天","冰天雪地");
//2.声明Season对象的属性:private final修饰
private final String seasonName;
private final String seasonDesc;
//2.私化类的构造器,并给对象属性赋值
private Season1(String seasonName,String seasonDesc){
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
//4.其他诉求1:获取枚举类对象的属性
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}

4.使用enum定义枚举类之后,枚举类常用方法:(继承于java.lang.Enum类

  • values() 方法:返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值。
  • valueOf(String str):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。
  • toString():返回当前枚举类对象常量的名称
public static void main(String[] args) {
Season1 summer = Season1.SUMMER;
//toString():返回枚举类对象的名称
System.out.println(summer.toString());
//values():返回所有的枚举类对象构成的数组
Season1[] values = Season1.values();
for (int i = 0; i < values.length; i++) {
System.out.println(values[i]);
values[i].show();
}
//valueOf(String objName):返回枚举类中对象名是objName的对象。
Season1 winter = Season1.valueOf("WINTER");
//如果没有objName的枚举类对象,则抛异常:IllegalArgumentException
// Season1 winter = Season1.valueOf("WINTER1");
System.out.println(winter);
}

5. 使用enum定义枚举类之后,如何让枚举类对象分别实现接口:

interface Info{
void show();
}
//使用enum关键字枚举类
enum Season1 implements Info{
//1.提供当前枚举类的对象,多个对象之间用","隔开,末尾对象";"结束
SPRING("春天","春暖花开"){
@Override
public void show() {
System.out.println("春天在哪里?");
}
},
SUMMER("夏天","夏日炎炎"){
@Override
public void show() {
System.out.println("宁夏");
}
},
AUTUMN("秋天","秋高气爽"){
@Override
public void show() {
System.out.println("秋天不回来");
}
},
WINTER("冬天","冰天雪地"){
@Override
public void show() {
System.out.println("大约在冬季");
}
};
}

注解的使用

1. 注解的理解

① jdk 5.0 新增的功能

② Annotation 其实就是代码里的特殊标记, 这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用Annotation,序员可以在不改变原逻辑的情况下, 在源文件中嵌入一些补充信息

③在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中,注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码和XML配置等。

框架 = 注解 + 反射机制 + 设计模式

2.注解的使用示例

示例一:生成文档相关的注解

/**
*@author 标明开发该类模块的作者,多个作者之间使用,分割
*@version 标明该类模块的版本
*@see 参考转向,也就是相关主题
*@since 从哪个版本开始增加的
*@param 对方法中某参数的说明,如果没有参数就不能写
*@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
*@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
*/

示例二:在编译时进行格式检查(JDK内置的个基本注解)的注解

  • @Override: 限定重写父类方法, 该注解只能用于方法
  • @Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的择

image-20220401115553165

  • @SuppressWarnings: 抑制编译器警告

示例三:跟踪代码依赖性,实现替代配置文件功能

Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。

image-20220401115239795

image-20220401115300723

3. 如何自定义注解:参照@SuppressWarnings定义

  • ① 注解声明为:@interface
  • ② 内部定义成员,通常使用value表示
  • ③ 可以指定成员的默认值,使用default定义
  • ④ 如果自定义注解没成员,表明是一个标识作用。
public @interface MyAnnotation {
//设置默认值
// String value() default "hello";
String value();
}

说明:

  • 如果注解有成员,在使用注解时,需要指明成员的值。
  • 自定义注解必须配上注解的信息处理流程(使用反射)才意义。
  • 自定义注解通常都会指明两个元注解:Retention、Target

理解:

image-20220401120508031

4. 元注解 :对现有的注解进行解释说明的注解

  • Retention:指定所修饰的 Annotation 的生命周期:SOURCE`CLASS(默认行为)\RUNTIME`, 只声明为RUNTIME生命周期的注解,才能通过反射获取。

image-20220401143149836

  • Target:用于指定被修饰的 Annotation 能用于修饰哪些程序元素

image-20220401143228621

  • Documented:表示所修饰的注解在被javadoc解析时,保留下来。
  • Inherited:被它修饰的 Annotation 将具继承性。

image-20220401143308100

类比:元数据的概念:String name = "Tom"; 其中String 和 name是用来修饰“Tom”的

image-20220401143326344

5.如何获取注解信息:通过反射来进行获取、调用。

前提:要求此注解的元注解Retention中声明的生命周期状态为:RUNTIME.

6.JDK8中注解的新特性:可重复注解、类型注解

6.1可重复注解:

① 在MyAnnotation上声明@Repeatable,成员值为MyAnnotations.class(MyAnnotatoins也是一个注解类,个人理解:MyAnnotation是MyAnnotations的父类,MyAnnotations是子类)

② MyAnnotation的Target和Retention等元注解与MyAnnotations相同。

image-20220401144150310

6.2 类型注解:

ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。
ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, TYPE_PARAMETER, TYPE_USE})
public @interface MyAnnotation {
String value() default "hello";
}
class Generic<@MyAnnotation T> {
public void show() throws @MyAnnotation RuntimeException {
ArrayList<@MyAnnotation String> list = new ArrayList<>();
int num = (@MyAnnotation int) 10L;
}
}

image-20220401144821514

集合

集合与数组概述

1.集合与数组存储数据概述:
集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)

2.数组存储的特点:

  • 一旦初始化以后,其长度就确定了。
  • 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。比如:String[] arr;int[] arr1;Object[] arr2;

3.数组存储的弊端:

  • 一旦初始化以后,其长度就不可修改
  • 数组中提供的方法非常限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  • 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
  • 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足

4.集合存储的优点:

解决数组存储数据方面的弊端。

Collection接口

1.单列集合框架结构

|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储序的、可重复的数据。 -->“动态”数组
|----ArrayList、LinkedList、Vector

​ |----Set接口:存储无序的、不可重复的数据 -->高中讲的“集合”
​ |----HashSet、LinkedHashSet、TreeSet

对应图示:

image-20220402115014839

image-20220402115023319

2.Collection接口常用方法:

add(Object obj),
addAll(Collection coll),
size(),
isEmpty(),
clear();
contains(Object obj),
containsAll(Collection coll),
remove(Object obj),
removeAll(Collection coll),
retainsAll(Collection coll),
equals(Object obj);
hasCode(),
toArray(),
iterator();

3.Collection集合与数组间的转换

集合 --->数组:toArray()
//集合 --->数组:toArray()
Object[] arr = coll.toArray();
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
数组 --->集合:调用Arrays类的静态方法asList(T ... t)
//拓展:数组 --->集合:调用Arrays类的静态方法asList(T ... t)
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);
List arr1 = Arrays.asList(new int[]{123, 456});
System.out.println(arr1.size());//1
List arr2 = Arrays.asList(new Integer[]{123, 456});
System.out.println(arr2.size());//2

4.使用Collection集合存储对象,要求对象所属的类满足:
向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()

Iterator接口与foreach循环

1.遍历Collection的两种方式:

① 使用迭代器Iteratorforeach循环(或增强for循环)

2.java.utils包下定义的迭代器接口:Iterator

2.1说明

Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素
GOF给迭代器模式的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。迭代器模式,就是为容器而生

2.2作用

遍历集合Collectiton元素

2.3如何获取实例

Collectiton实例对象.iterator() 返回一个迭代器实例

2.4遍历的代码实现
Iterator iterator = coll.iterator();
//hasNext():判断是否还下一个元素
while(iterator.hasNext()){
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
2.5图示说明:

image-20220402115809420

2.6 remove()的使用

Iterator中的remove()

如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,再调用remove都会报IllegalStateException

内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove()

分析源码可知:

modCount存在于AbstractList中,记录集合被修改(add、remove)的次数;在迭代器中,进行list.iterator()时,首先会将修改次数modCount赋值给迭代器中的expectedModCount,用expectedModCount来存储当前集合修改次数;

private Itr() {
this.cursor = 0;
this.lastRet = -1;
this.expectedModCount = AbstractList.this.modCount;
}

而如果在迭代循环的过程中:
如果使用了list.remove方法,就会将modCount+1,而后再进行checkForComodification()方法时,就会发现modCount与expectedModCount不相等,就会抛出异常 ConcurrentModificationException;

如果使用了Iterator的remove方法,在Iterator的remove方法中可以发现,它将expectedModCount进行了更新: expectedModCount = modCount;expectedModCount 与modCount相等,就不会报错了;

image-20220402124259473

例子:

@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//删除集合中"Tom"
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
// iterator.remove();
Object obj = iterator.next();
if("Tom".equals(obj)){
iterator.remove();
// iterator.remove();
}
}
//遍历集合
iterator = coll.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}

3.jdk5.0新特性--增强for循环

适用范围:迭代访问 Collection和数组

1.遍历集合举例:

@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//for(集合元素的类型 局部变量 : 集合对象)
for(Object obj : coll){
System.out.println(obj);
}
}

说明:
内部仍然调用了迭代器。

2.遍历数组举例:

@Test
public void test2(){
int[] arr = new int[]{1,2,3,4,5,6};
//for(数组元素的类型 局部变量 : 数组对象)
for(int i : arr){
System.out.println(i);
}
}

Collection子接口:List接口

1.存储的数据特点:存储序的、可重复的数据。

2.常用方法:(记住)

增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:① Iterator迭代器方式
② 增强for循环
③ 普通的循环

3.常用实现类:

|----Collection接口:单列集合,用来存储一个一个的对象
|----List接口:存储有序的、可重复的数据。 -->“动态”数组,替换原的数组
|----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
|----LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
|----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

4.源码分析(难点)

4.1 ArrayList的源码分析:
/**
* jdk 7情况下
* ArrayList list = new ArrayList();//空参构造器底层默认创建了长度是10的Object[]数组elementData
* list.add(123);//elementData[0] = new Integer(123);
* ...
* list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。
* 默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。
*
* 结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
*
* jdk 8中ArrayList的变化:
* ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没创建长度为10的数组
*
* list.add(123);//第一次调用add()时,底层才创建了长度10的数组,并将数据123添加到elementData[0]
* ...
* 后续的添加和扩容操作与jdk 7 无异。
*
*
* 小结:jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象
* 的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
*/

image-20220402143334956

4.2 LinkedList的源码分析:

LinkedList list = new LinkedList(); 内部声明了Node类型的first(指向链表的头部)和last(指向链表的尾部)属性,默认值为null

list.add(123);//将123封装到Node中,创建了Node对象。

其中,Node定义为:体现了LinkedList的双向链表的说法

private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}

image-20220402143628239

4.3 Vector的源码分析:

jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组
在扩容方面,默认扩容为原来的数组长度的2倍

5.List接口的常用方法

/*
void add(int index, Object ele):在index位置插入ele元素
boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
Object get(int index):获取指定index位置的元素
int indexOf(Object obj):返回obj在集合中首次出现的位置
int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
Object remove(int index):移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
总结:常用方法
增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:① Iterator迭代器方式
② 增强for循环
③ 普通的循环
*/

存储的元素的要求
添加的对象,所在的类要重写equals()方法
原因:
List 还提供了boolean contains(Object o)方法来判断List是否包含某个指定元素。此外,int indexOf(Object o)方法可以返回某个元素的索引,如果元素不存在,就返回-1。List中的元素是引用类型,需要比较时,必须重写对应类中equals方法

image-20220402170811480

[面试题]

面试题:ArrayList、LinkedList、Vector者的异同?

  • 同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据

image-20220402163806538

Collection子接口:Set接口

HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时都使用这个实现类。
HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存取、查找、删除性能。

HashSet 具有以下特点
①不能保证元素的排列顺序
②HashSet 不是线程安全的
集合元素可以是 null

HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
对于存放在Set容器中的对象, 对应的类一定要重写equals() 和hashCode(Object obj) 方法,以实现对象相等规则 。即: “相等的对象必须具有相等的散列码

1.存储的数据特点:无序的不可重复的元素

具体的:
以HashSet为例说明:

  1. 无序性:不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。
  2. 不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个。

image-20220402145812753

2.元素添加过程:(以HashSet为例)

我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置,判断
数组此位置上是否已经元素:
如果此位置上没其他元素,则元素a添加成功。 --->情况1
如果此位置上其他元素b(或以链表形式存在的多个元素,则比较元素a与元素b的hash值:
如果hash值不相同,则元素a添加成功。--->情况2
如果hash值相同,进而需要调用元素a所在类的equals()方法:
equals()返回true,元素a添加失败
equals()返回false,则元素a添加成功。--->情况3

对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。
jdk 7 :元素a放到数组中,指向原来的元素(类似头插法)。
jdk 8 :原来的元素在数组中,指向元素a(类似尾插法
总结:七上八下

HashSet底层:数组+链表的结构。(前提:jdk7)

重写 hashCode() 方法的基本原则

  • 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
  • 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
  • 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。

重写 equals() 方法的基本原则

以自定义的Customer类为例,何时需要重写equals()?

  • 当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候,总是要改写hashCode(),根据一个类的equals方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据Object.hashCode()方法,它们仅仅是两个对象。
  • 因此,违反了“相等的对象必须具有相等的散列码”。
  • 结论:复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode 的对象的属性也应该参与到equals()

image-20220402164557967

3.常用方法

Set接口中没额外定义新的方法,使用的都是Collection中声明过的方法。

4.常用实现类:

|----Collection接口:单列集合,用来存储一个一个的对象
|----Set接口:存储无序的、不可重复的数据 -->高中讲的“集合”
|----HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
|----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的顺序遍历
在添加数据的同时,每个数据还维护了两个引用(指针),记录此数据前一个数据和后一个数添加的顺序遍据。对于频繁的遍历操作,LinkedHashSet效率高于HashSet.
|----TreeSet:可以按照添加对象的指定属性,进行排序。

5.存储对象所在类的要求:

HashSet/LinkedHashSet:

要求:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码
重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,也应该用这些属性来计算 hashCode 值。

6.TreeSet的使用

TreeSet:
1.自然排序(实现Comparable接口)中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals().
2.定制排序(实现Comparator接口)中,比较两个对象是否相同的标准为:compare()返回0.不再是equals().

6.1 使用说明:
1.向TreeSet中添加的数据,要求是相同类的对象
2.两种排序方式:自然排序(实现Comparable接口 和 定制排序(Comparator)

6.2 常用的排序方式:

public class User implements Comparable {
private String name;
private int age;
...
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if (o instanceof User) {
User user = (User) o;
// return -this.name.compareTo(user.name);
int compare = -this.name.compareTo(user.name);
if (compare != 0) {
return compare;
} else {
return Integer.compare(this.age, user.age);
}
} else {
throw new RuntimeException("输入的类型不匹配");
}
}
}
//方式一:自然排序
@Test
public void test1(){
TreeSet set = new TreeSet();
//失败:不能添加不同类的对象
// set.add(123);
// set.add(456);
// set.add("AA");
// set.add(new User("Tom",12));
//举例一:
// set.add(34);
// set.add(-34);
// set.add(43);
// set.add(11);
// set.add(8);
//举例二:
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
//方式二:定制排序
@Test
public void test2(){
Comparator com = new Comparator() {
//照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Mary",33));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}

Map接口

双列集合框架:Map

image-20220402151220013

1.常用实现类结构

|----Map:双列数据,存储key-value对的数据 ---类似于高中的函数:y = f(x)
|----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
|----LinkedHashMap:保证在遍历map元素时,可以照添加的顺序实现遍历
原因:在原来的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁的遍历操作,此类执行效率高于HashMap。
|----TreeMap:保证按照添加的key-value对进行排序,按照key值实现排序遍历。此时考虑key的自然排序或定制排序
底层使用红黑树
|----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
|----Properties:常用来处理配置文件。key和value都是String类型

HashMap的底层:

数组+链表 (jdk7及之前)
数组+链表+红黑树 (jdk 8)

[面试题]

1. HashMap的底层实现原理?
hashMap的底层:数组+链表 (jdk7及之前)
数组+链表+红黑树 (jdk 8)
2. HashMap 和 Hashtable的异同?
3. CurrentHashMap 与 Hashtable的异同?(暂时不讲)

2.存储结构的理解(这里需要多理解)

  • Map中的key:无序的、不可重复的,使用Set存储所的key ---> key所在的类要重写equals()和hashCode()记住:set集合一般都要重写hashCode()和equal()方法,以HashMap为例)

例:

boolean containsKey(Object key):是否包含指定的key
//需要根据key值做判断,所以要重写equals()和hashCode()
  • Map中的value:无序的、可重复的,使用Collection存储所的value --->value所在的类要重写equals()
boolean containsValue(Object value):是否包含指定的value
//需要根据value值做判断,所以要重写equals()
  • 一个键值对:key-value构成了一个Entry对象。Map中的entry:无序的、不可重复的,使用Set存储所的entry

图示:

image-20220402152420332

3.常用方法

添加、删除、修改操作
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据

元素查询的操作
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等

元视图操作的方法
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合

@Test
public void test5() {
Map map = new HashMap();
map.put("AA", 123);
map.put(45, 1234);
map.put("BB", 56);
//遍历所有的key集:keySet()
Set set = map.keySet();
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println();
//遍历所有的value集:values()
Collection values = map.values();
for (Object obj : values) {
System.out.println(obj);
}
System.out.println();
//遍历所有的key-value
//方式一:entrySet()
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()) {
Object obj = iterator1.next();
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
System.out.println();
//方式二:
Set keySet = map.keySet();
Iterator iterator2 = keySet.iterator();
while (iterator2.hasNext()) {
Object key = iterator2.next();
Object value = map.get(key);
System.out.println(key + "=====" + value);
}
}

4.内存结构说明:(难点)

4.1 HashMap在jdk7中实现原理:

HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table
...可能已经执行过多次put...
map.put(key1,value1):
首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1
如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

​ 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2
​ 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:

​ 如果equals()返回false:此时key1-value1添加成功。----情况3
​ 如果equals()返回true:使用value1替换value2。

补充:关于情况2和情况3:此时key1-value1和原来的数据以链表的方式存储。

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原来的数据复制过来。

4.2 HashMap在jdk8中相较于jdk7在底层实现方面的不同:
  1. new HashMap():底层没有马上创建一个长度为16的数组
  2. jdk 8底层的数组是:Node[],而非Entry[]
  3. 首次调用put()方法时,底层创建长度为16的数组
  4. jdk7底层结构:数组+链表。jdk8中底层结构:数组+链表+红黑树
    4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
    4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所有数据改为使用红黑树存储

image-20220402183153972

image-20220402183232191

4.3 HashMap底层典型属性的属性的说明

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量:16
DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

4.4 LinkedHashMap的底层实现原理(了解)

LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap.
区别就在于:LinkedHashMap内部提供了Entry,替换HashMap中的Node

image-20220402153834643

5.TreeMap的使用

向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
因为要照key进行排序:自然排序 、定制排序

自然排序

//向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
//因为要按照key进行排序:自然排序 、定制排序
//自然排序
@Test
public void test1() {
TreeMap map = new TreeMap();
User u1 = new User("Tom", 23);
User u2 = new User("Jerry", 32);
User u3 = new User("Jack", 20);
User u4 = new User("Rose", 18);
map.put(u1, 98);
map.put(u2, 89);
map.put(u3, 76);
map.put(u4, 100);
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()) {
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}
public class User implements Comparable{
private String name;
private int age;
...
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User)o;
// return -this.name.compareTo(user.name);
int compare = -this.name.compareTo(user.name);
if(compare != 0){
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}

定制排序

//定制排序
@Test
public void test2() {
TreeMap map = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof User && o2 instanceof User) {
User u1 = (User) o1;
User u2 = (User) o2;
return Integer.compare(u1.getAge(), u2.getAge());
}
throw new RuntimeException("输入的类型不匹配!");
}
});
User u1 = new User("Tom", 23);
User u2 = new User("Jerry", 32);
User u3 = new User("Jack", 20);
User u4 = new User("Rose", 18);
map.put(u1, 98);
map.put(u2, 89);
map.put(u3, 76);
map.put(u4, 100);
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()) {
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}

5.HashTable(了解)

Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap,Hashtable是线程安全的
Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询速度快,很多情况下可以互用。
与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value
与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。

6.使用Properties读取配置文件

Properties 类是 Hashtable 的子类,该对象用于处理属性(配置)文件
由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key和 value 都是字符串类型
存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

jdbc.properties文件

name=Tom
password=abc123

测试

//Properties:常用来处理配置文件。key和value都是String类型
public static void main(String[] args) {
FileInputStream fis = null;
try {
Properties pros = new Properties();
fis = new FileInputStream("jdbc.properties");
pros.load(fis);//加载流对应的文件
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Collections工具类的使用

1.作用:操作Collection和Map的工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法

Arrays和Collections作用相似,Arrays是操作数组的工具类

2.常用方法:

reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object min(Collection)
Object min(Collection,Comparator)
int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所旧值

同步控制:Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

image-20220402154427570

说明:ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转换为线程的。
使用synchronizedList(List list) 和 synchronizedMap(Map map)

3.面试题:

面试题:Collection 和 Collections的区别?

数据结构简述

1.数据结构概述

数据结构(Data Structure是一门和计算机硬件与软件都密切相关的学科,它的研究重点是在计算机的程序设计领域中探讨如何在计算机中组织和存储数据并进行高效率的运用,涉及的内容包含:数据的逻辑关系、数据的存储结构、排序算法(Algorithm)、查找(或搜索)等

2.数据结构与算法的理解

程序能否快速而高效地完成预定的任务,取决于是否选对了数据结构,而程序是否能清楚而正确地把问题解决,则取决于算法。

所以大家认为:“Algorithms + Data Structures = Programs”(出自:Pascal之父Nicklaus Wirth)

总结:算法是为了解决实际问题而设计的,数据结构是算法需要处理的问题载体。

3.数据结构的研究对象

3.1 数据间的逻辑结构

image-20220402154749929

3.2 数据的存储结构:

线性表(顺序表、链表、栈、队列)

说明:

习惯上把顺序表和链表看做基本数据结构(或真实数据结构)
习惯上把栈、队列、树、图看做抽象数据类型,简称ADT

泛型

1.泛型的概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时确定(即传入实际的类型参数,也称为类型实参)。

2.泛型的引入背景

集合容器类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以在JDK1.5之前只能把元素类型设计为Object,JDK1.5之后使用泛型来解决。因为这个时候除了元素的类型不确定,其他的部分是确定的,例如关于这个元素如何保存,如何管理等是确定的,因此时把元素的类型设计成一个参数,这个类型参数叫做泛型。Collection,List,ArrayList 这个就是类型参数,即泛型。

那么为什么要有泛型呢 , 直接Object 不是也可以存储数据吗 ?

  1. 解决元素存储的安全性问题,好比商品、药品标签,不会弄错。
  2. 解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别。

在集合中没有泛型时

image-20220403110323425

在集合中有泛型时

image-20220403110451473

Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮

在集合中使用泛型的例子

public class GenericTest {
//在集合中使用泛型之前的情况:
@Test
public void test1() {
ArrayList list = new ArrayList();
//需求:存放学生的成绩
list.add(78);
list.add(76);
list.add(89);
list.add(88);
//问题一:类型不安全
// list.add("Tom");
for (Object score : list) {
//问题二:强转时,可能出现ClassCastException
int stuScore = (Integer) score;
System.out.println(stuScore);
}
}
//在集合中使用泛型的情况:以ArrayList为例
@Test
public void test2() {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(78);
list.add(87);
list.add(99);
list.add(65);
//编译时,就会进行类型检查,保证数据的安全
// list.add("Tom");
//方式一:
// for(Integer score : list){
// //避免了强转操作
// int stuScore = score;
//
// System.out.println(stuScore);
//
// }
//方式二:
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
int stuScore = iterator.next();
System.out.println(stuScore);
}
}
//在集合中使用泛型的情况:以HashMap为例
@Test
public void test3() {
// Map<String,Integer> map = new HashMap<String,Integer>();
//jdk7新特性:类型推断
Map<String, Integer> map = new HashMap<>();
map.put("Tom", 87);
map.put("Jerry", 87);
map.put("Jack", 67);
// map.put(123,"ABC");
//泛型的嵌套
Set<Map.Entry<String, Integer>> entry = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entry.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> e = iterator.next();
String key = e.getKey();
Integer value = e.getValue();
System.out.println(key + "----" + value);
}
}
}

自定义泛型结构

image-20220403111414422

定义泛型要点

  1. 泛型类可能有多个参数,此时应将多个参数一起放在尖括号内。比如:<E1,E2,E3>

  2. 泛型类的构造器如下:public GenericClass(){}。而下面是错误的:public GenericClass<E>(){}

  3. 实例化后,操作原来泛型位置的结构必须与指定的泛型类型一致。

  4. 泛型不同的引用不能相互赋值。

    ​ 尽管在编译时ArrayList和ArrayList是两种类型,但是,在运行时只有一个ArrayList被加载到JVM中。

  5. 泛型如果不指定,将被擦除,泛型对应的类型均按照Object处理,但不等价于Object。 经验:泛型要使用一路都用。要不用,一路都不要用

  6. 如果泛型结构是一个接口或抽象类,则不可创建泛型类的对象。

  7. jdk1.7,泛型的简化操作:ArrayList flist = new ArrayList<>();

  8. 泛型的指定中不能使用基本数据类型,可以使用包装类替换

  9. 在类/接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型非静态方法的参数类型非静态方法的返回值类型。但在静态方法中不能使用类的泛型

  10. 异常类不能是泛型的

  11. 不能使用new E[]。但是可以:E[] elements = (E[])new Object[capacity];参考:ArrayList源码中声明:Object[] elementData,而非泛型参数类型数组。

  12. 父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

    • 子类不保留父类的泛型:按需实现

      • 没有类型 擦除
      • 具体类型
    • 子类保留父类的泛型:泛型子类

      • 全部保留
      • 部分保留

结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型

例5:

class GenericTest {
public static void main(String[] args) {
// 1、使用时:类似于Object,不等同于Object
ArrayList list = new ArrayList();
// list.add(new Date());//有风险
list.add("hello");
test(list);// 泛型擦除,编译不会类型检查
// ArrayList<Object> list2 = new ArrayList<Object>();
// test(list2);//一旦指定Object,编译会类型检查,必须按照Object处理
}
public static void test(ArrayList<String> list) {
String str = "";
for (String s : list) {
str += s + ",";
}
System.out.println("元素:" + str);
}
}

自定义泛型结构:泛型类

class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son1 extends Father {// 等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2 extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2> extends Father<Integer, T2> {
}
class Father<T1, T2> {
}
// 子类不保留父类的泛型
// 1)没有类型 擦除
class Son<A, B> extends Father{//等价于class Son extends Father<Object,Object>{
}
// 2)具体类型
class Son2<A, B> extends Father<Integer, String> {
}
// 子类保留父类的泛型
// 1)全部保留
class Son3<T1, T2, A, B> extends Father<T1, T2> {
}
// 2)部分保留
class Son4<T2, A, B> extends Father<Integer, T2> {
}
class Person<T> {
// 使用T类型定义变量
private T info;
// 使用T类型定义一般方法
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
// 使用T类型定义构造器
public Person() {
}
public Person(T info) {
this.info = info;
}
// 普通的static的方法中不能声明泛型,编译不通过,因为类的泛型参数要等到实例化后才能调用
// public static void show(T t) {
//}
// 泛型的static的方法可以编译通过,因为泛型方法只要调用的时候传入具体的类型即可,不需要实例化才能决定参数类型
public static <E> void show(E t) {
}
// 不能在try-catch中使用泛型定义
public void test() {
try {
} catch (T ex) {
}
}
}

自定义泛型结构:泛型方法

方法,也可以被泛型化,不管此时定义在其中的类是不是 泛型类。 在泛型方法中可以定义泛型参数,此时,参数的类型就是传入数据的 类型 。

泛型方法的格式:
[ 访问权限] < 泛型> 返回类型 方法名([ 泛型标识 参数名称])

public class DAO {
public <E> E get(int id, E e) {
E result = null;
return result;
}
}

泛型在继承上的体现

如果B是A的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G<B>并不是G<A>的子类型!二者是并列关系

比如:String是Object的子类,但是List并不是List的子类。

image-20220405150419964

public void testGenericAndSubClass() {
Person[] persons = null;
Man[] mans = null;
// 而 Person[] 是 Man[] 的父类.
persons = mans;
Person p = mans[0];
// 在泛型的集合上
List<Person> personList = null;
List<Man> manList = null;
// personList = manList;(报错)
}

通配符

1.通配符的使用

类A是类B的父类,G<A>和G<B>是没关系的,二者共同的父类是:G<?>

image-20220405150832255

3.1将任意元素加入到其中不是类型安全 的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 编译时错误
//获取(读取):允许读取数据,读取的数据类型为Object。
Object o = list.get(0);
System.out.println(o);

3.2因为我们不知道c的元素类型,我们不能向其中添加对象。add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。

3.3唯一 的例外 的是 是null ,它是所有引用类型成员的特殊值。

3.4另一方面,我们可以调用get() 方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个 个Object。

例子:

public static void main(String[] args) {
List<?> list = null;
list = new ArrayList<String>();
list = new ArrayList<Double>();
// list.add(3);//编译不通过
list.add(null);
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
l1.add("尚硅谷");
l2.add(15);
read(l1);
read(l2);
}
public static void read(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}

通配符使用的注意点

//注意点1:编译错误:不能用在泛型方法声明上,返回值类型前面<>不能使用?
public static <?> void test(ArrayList<?> list){
}
//注意点2:编译错误:不能用在泛型类的声明上
class GenericTypeClass<?>{
}
//注意点3:编译错误:不能用在创建对象上,右边属于创建集合对象
ArrayList<?> list2 = new ArrayList<?>();

2.有限制的通配符

1.<?>
允许所有泛型的引用调用

2.通配符指定上限

上限extends:使用时指定的类型必须是继承某个类,或者实现某个接口,即<=

3.通配符指定下限

下限super:使用时指定的类型不能小于操作的类,即>=

image-20220405151853063

有限制条件的通配符的使用

/*
限制条件的通配符的使用。
? extends A:
G<? extends A> 可以作为G<A>和G<B>的父类,其中B是A的子类
? super A:
G<? super A> 可以作为G<A>和G<B>的父类,其中B是A的父类
读取数据,选择大范围;
写入数据,选择小范围
*/

注:

读取数据,选择大范围;写入数据,选择小范围

例子

@Test
public void test4(){
List<? extends Person> list1 = null;
List<? super Person> list2 = null;
List<Student> list3 = new ArrayList<Student>();
List<Person> list4 = new ArrayList<Person>();
List<Object> list5 = new ArrayList<Object>();
list1 = list3;
list1 = list4;
// list1 = list5;
// list2 = list3;
list2 = list4;
list2 = list5;
//读取数据:返回类型要选择大范围 List<? extends Person>, ?的范围:(无穷小,Person]返回的类型>=Person
list1 = list3;
Person p = list1.get(0);
//编译不通过
//Student s = list1.get(0);
//返回类型要选择大范围 List<? super Person>, ?的范围[Person,Object),返回的类>=Object
list2 = list4;
Object obj = list2.get(0);
////编译不通过
// Person obj = list2.get(0);
//写入数据:写入的类型要选择小范围,List<? extends Person>, ?的范围:(无穷小,Person],小范围为无穷小,所以不能写入数据
//编译不通过
// list1.add(new Student());
//编译通过
//写入的类型要选择小范围,List<? super Person>, ?的范围:[Person,Object],小范围为Person,所以能写入Person及Person子类的数据
list2.add(new Person());
list2.add(new Student());
}

IO流

File类的使用

1.File类的理解

  • File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)
  • File类声明在java.io包下
  • File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
  • 后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"

2.File的实例化

2.1 常用构造器

File(String filePath): 以filePath为路径创建File对象,可以是 绝对路径或者相对路径
File(String parentPath,String childPath):以parent为父路径,child为子路径创建File对象。
File(File parentFile,String childPath):根据一个父File对象和子文件路径创建File对象

2.2 路径的分类

相对路径:相较于某个路径下,指明的路径。
绝对路径:包含盘符在内的文件或文件目录的路径

说明:

IDEA中:
如果大家开发使用JUnit中的单元测试方法测试,相对路径即为当前Module下
如果大家使用main()测试,相对路径即为当前的Project下
Eclipse中:
不管使用单元测试方法还是使用main()测试,相对路径都是当前的Project下。

2.3 路径分隔符

windows和DOS系统默认使用“\”来表示
UNIX和URL使用“/”来表示
为了解决这个隐患,File类提供了一个常量:
public static final String separator。根据操作系统,动态的提供分隔符

image-20220405220347163

3.File类的常用方法

image-20220405220432106

image-20220405220438007

image-20220405220621377

image-20220405220449282

IO流概述

1.流的分类

  • 操作数据单位:字节流(8bit)、字符流(16bit)
  • 操作数据单位:字节流(8bit)、字符流(16bit)
  • 流的角色:节点流、处理流

节点流:直接从数据源或目的地读写数据

image-20220405220818999

处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。

image-20220405220842359

图示:

image-20220405220902250

2.流的体系结构

image-20220405220940458

说明:红框对应的是IO流中的4个抽象基类。蓝框的流需要大家重点关注。

3.重点说明的几个流结构

image-20220405221026977

4.输入、输出的标准化过程

4.1 输入过程

① 创建File类的对象,指明读取的数据的来源。(要求此文件一定要存在)
② 创建相应的输入流,将File类的对象作为参数,传入流的构造器中
③ 具体的读入过程:
创建相应的byte[] 或 char[]。
④ 关闭流资源
说明:程序中出现的异常需要使用try-catch-finally处理。

4.2 输出过程
① 创建File类的对象,指明写出的数据的位置。(不要求此文件一定要存在)
② 创建相应的输出流,将File类的对象作为参数,传入流的构造器中
③ 具体的写出过程:
write(char[]/byte[] buffer,0,len)
④ 关闭流资源
说明:程序中出现的异常需要使用try-catch-finally处理。

InputStream & Reader
  • InputStream 和 Reader 是所有 输入流的基类。
  • InputStream(典型实现:FileInputStream)
    int read()
    int read(byte[] b)
    int read(byte[] b, int off, int len)
  • Reader(典型实现:FileReader)
    int read()
    int read(char [] c)
    int read(char [] c, int off, int len)
  • 程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该件 显式关闭文件 IO 资源。
  • FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream
    用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader
InputStream(了解)
  • int read()
    从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。
  • int read(byte[] b)
    从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。
  • int read(byte[] b, int off,int len)
    将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1。
  • public void close() throws IOException
    关闭此输入流并释放与该流关联的所有系统资源。
Reader
  • int read()
    读取单个字符。作为整数读取的字符,范围在 0 到 65535 之间 (0x00-0xffff)(2个字节的Unicode码),如果已到达流的末尾,则返回 -1
  • int read(char[] cbuf)
    将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
  • int read(char[] cbuf,int off,int len)
    将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
  • public void close() throws IOException
    关闭此输入流并释放与该流关联的所有系统资源。
OutputStream & Writer

OutputStream 和 Writer 也非常相似:

void write(int b/int c);

void write(byte[] b/char[] cbuf);

void write(byte[] b/char[] buff, int off, int len);

void flush();

void close(); 需要先刷新,再关闭此流

因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组,即以 String 对象作为参数

void write(String str);

void write(String str, int off, int len);

FileOutputStream 从文件系统中的某个文件中获得输出字节。FileOutputStream用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter

OutputStream

  • void write(int b)
    将指定的字节写入此输出流。write 的常规协定是:向输出流写入一个字节。要写入的字节是参数 b 的八个低位。b 的 24 个高位将被忽略。 即写入0~255范围的。
  • void write(byte[] b)
    将 b.length 个字节从指定的 byte 数组写入此输出流。write(b) 的常规协定是:应该与调用 write(b, 0, b.length) 的效果完全相同
  • void write(byte[] b,int off,int len)
    将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
  • public void flush()throws IOException
    刷新此输出流并强制写出所有缓冲的输出字节,调用此方法指示应将这些字节立即写入它们预期的目标
  • public void close() throws IOException
    关闭此输出流并释放与该流关联的所有系统资源。

Writer

  • void write(int c)
    写入单个字符。要写入的字符包含在给定整数值的 16 个低位中,16 高位被忽略。 即写入0 到 65535 之间的Unicode码。
  • void write(char[] cbuf)
    写入字符数组。
  • void write(char[] cbuf,int off,int len)
    写入字符数组的某一部分。从off开始,写入len个字符
  • void write(String str)
    写入字符串。
  • void write(String str,int off,int len)
    写入字符串的某一部分。
  • void flush()
    刷新该流的缓冲,则立即将它们写入预期目标。
  • public void close() throws IOException
    关闭此输出流并释放与该流关联的所有系统资源。

节点流(或文件流)

1.FileReader/FileWriter的使用:

1.1 FileReader的使用

  1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1
  2. 异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理
  3. 读入的文件一定要存在,否则就会报FileNotFoundException。
@Test
public void testFileReader1() {
FileReader fr = null;
try {
//1.File类的实例化
File file = new File("hello.txt");
//2.FileReader流的实例化
fr = new FileReader(file);
//3.读入的操作
//read(char[] cbuf):返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
char[] cbuf = new char[5];
int len;
while((len = fr.read(cbuf)) != -1){
//方式一:
//错误的写法
// for(int i = 0;i < cbuf.length;i++){
// System.out.print(cbuf[i]);
// }
//正确的写法
// for(int i = 0;i < len;i++){
// System.out.print(cbuf[i]);
// }
//方式二:
//错误的写法,对应着方式一的错误的写法
// String str = new String(cbuf);
// System.out.print(str);
//正确的写法
String str = new String(cbuf,0,len);
System.out.print(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fr != null){
//4.资源的关闭
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

1.2 FileWriter的使用

从内存中写出数据到硬盘的文件里。

说明:

  1. 输出操作,对应的File可以不存在的。并不会报异常
  2. File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。
    File对应的硬盘中的文件如果存在:
    如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原文件的覆盖
    如果流使用的构造器是:FileWriter(file,true):不会对原文件覆盖,而是在原文件基础上追加内容
@Test
public void testFileWriter() {
FileWriter fw = null;
try {
//1.提供File类的对象,指明写出到的文件
File file = new File("hello1.txt");
//2.提供FileWriter的对象,用于数据的写出
fw = new FileWriter(file,false);
//3.写出的操作
fw.write("I have a dream!\n");
fw.write("you need to have a dream!");
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.流资源的关闭
if(fw != null){
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

1.3 文本文件的复制:

@Test
public void testFileReaderFileWriter() {
FileReader fr = null;
FileWriter fw = null;
try {
//1.创建File类的对象,指明读入和写出的文件
File srcFile = new File("hello.txt");
File destFile = new File("hello2.txt");
//不能使用字符流来处理图片等字节数据
// File srcFile = new File("爱情与友情.jpg");
// File destFile = new File("爱情与友情1.jpg");
//2.创建输入流和输出流的对象
fr = new FileReader(srcFile);
fw = new FileWriter(destFile);
//3.数据的读入和写出操作
char[] cbuf = new char[5];
int len;//记录每次读入到cbuf数组中的字符的个数
while((len = fr.read(cbuf)) != -1){
//每次写出len个字符
fw.write(cbuf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭流资源
//方式一:
// try {
// if(fw != null)
// fw.close();
// } catch (IOException e) {
// e.printStackTrace();
// }finally{
// try {
// if(fr != null)
// fr.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
//方式二:
try {
if(fw != null)
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if(fr != null)
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

2.FileInputStream / FileOutputStream的使用:

对于文本文件(.txt,.java,.c,.cpp),使用字符流处理

对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),使用字节流处理

@Test
public void testFileInputOutputStream() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//1.造文件
File srcFile = new File("爱情与友情.jpg");
File destFile = new File("爱情与友情2.jpg");
//2.造流
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
//3.复制的过程
byte[] buffer = new byte[5];
int len;
while((len = fis.read(buffer)) != -1){
fos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fos != null){
//4.关闭流
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

缓冲流的使用

1.缓冲流涉及到的类:

BufferedInputStream

BufferedOutputStream

BufferedReader

BufferedWriter

2.说明

  • 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区
  • 当使用BufferedInputStream读取字节文件时,BufferedInputStream会一次性从文件中读取8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个8192个字节数组。
  • 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream才会把缓冲区中的数据一次性写到文件里。使用方法flush()可以强制将缓冲区的内容全部写入输出流
  • 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流
  • flush()方法的使用:手动将buffer中内容写入文件
  • 如果是带缓冲区的流对象的close()方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出

image-20220405224652899

image-20220405224701229

3.作用:

作用:提供流的读取、写入的速度
提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb

image-20220405223837584

4.典型代码

4.1 使用BufferedInputStream和BufferedOutputStream:处理非文本文件

//实现文件复制的方法
public void copyFileWithBuffered(String srcPath,String destPath){
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
//1.造文件
File srcFile = new File(srcPath);
File destFile = new File(destPath);
//2.造流
//2.1 造节点流
FileInputStream fis = new FileInputStream((srcFile));
FileOutputStream fos = new FileOutputStream(destFile);
//2.2 造缓冲流
bis = new BufferedInputStream(fis);
bos = new BufferedOutputStream(fos);
//3.复制的细节:读取、写入
byte[] buffer = new byte[1024];
int len;
while((len = bis.read(buffer)) != -1){
bos.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源关闭
//要求:先关闭外层的流,再关闭内层的流
if(bos != null){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(bis != null){
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略.
// fos.close();
// fis.close();
}
}

说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略.

4.2 使用BufferedReader和BufferedWriter:处理文本文件

@Test
public void testBufferedReaderBufferedWriter(){
BufferedReader br = null;
BufferedWriter bw = null;
try {
//创建文件和相应的流
br = new BufferedReader(new FileReader(new File("dbcp.txt")));
bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));
//读写操作
//方式一:使用char[]数组
// char[] cbuf = new char[1024];
// int len;
// while((len = br.read(cbuf)) != -1){
// bw.write(cbuf,0,len);
// // bw.flush();
// }
//方式二:使用String
String data;
while((data = br.readLine()) != null){
//方法一:
// bw.write(data + "\n");//data中不包含换行符
//方法二:
bw.write(data);//data中不包含换行符
bw.newLine();//提供换行的操作
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if(bw != null){
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(br != null){
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

转换流的使用

1.转换流涉及到的类(InputStreamReader、OutputStreamWriter):属于字符流

InputStreamReader:将一个字节的输入流转换为字符的输入流
解码:字节、字节数组 --->字符数组、字符串

OutputStreamWriter:将一个字符的输出流转换为字节的输出流
编码:字符数组、字符串 ---> 字节、字节数组

说明:

  • 编码决定了解码的方式
  • 很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能

2.作用:提供字节流与字符流之间的转换

3.图示:

image-20220405225104187

4.典型实现:

@Test
public void test1() throws IOException {
FileInputStream fis = new FileInputStream("dbcp.txt");
// InputStreamReader isr = new InputStreamReader(fis);//使用系统默认的字符集
//参数2指明了字符集,具体使用哪个字符集,取决于文件dbcp.txt保存时使用的字符集
InputStreamReader isr = new InputStreamReader(fis,"UTF-8");//使用系统默认的字符集
char[] cbuf = new char[20];
int len;
while((len = isr.read(cbuf)) != -1){
String str = new String(cbuf,0,len);
System.out.print(str);
}
isr.close();
}
/*
此时处理异常的话,仍然应该使用try-catch-finally
综合使用InputStreamReader和OutputStreamWriter
*/
@Test
public void test2() throws Exception {
//1.造文件、造流
File file1 = new File("dbcp.txt");
File file2 = new File("dbcp_gbk.txt");
FileInputStream fis = new FileInputStream(file1);
FileOutputStream fos = new FileOutputStream(file2);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
OutputStreamWriter osw = new OutputStreamWriter(fos,"gbk");
//2.读写过程
char[] cbuf = new char[20];
int len;
while((len = isr.read(cbuf)) != -1){
osw.write(cbuf,0,len);
}
//3.关闭资源
isr.close();
osw.close();
}

5.说明:
文件编码的方式(比如:GBK),决定了解析时使用的字符集(也只能是GBK)。

编码集

1.编码表的由来
计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表

2.常见的编码表

ASCII:美国标准信息交换码。用一个字节的7位可以表示。

ISO8859-1:拉丁码表。欧洲码表。用一个字节的8位表示。

GB2312:中国的中文编码表。最多两个字节编码所有字符

GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码

Unicode:国际标准码,融合了目前人类使用的所字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示(早期),后来再细分utf-8、utf-16、utf-32...

UTF-8:变长的编码方式,可用1-6个字节来表示一个字符,3个字节已经基本满足各国的字符的总和(特殊除外)。

在标准UTF-8编码中,超出基本多语言范围(BMP-BasicMutilingual Plane)的字符被编码为4字节格式;(了解)但是在修正的UT-8编码中,代理编码队在序列中分别重新编码。结果标准UTF-8编码中需要4个字节的字符,在修正后的UTF-8编码中将需要6个字节。

为了达到通用性,现在一般用UTF-8字符集

image-20220405225749301

补充:字符编码
1.Unicode不完美,早期的Unicode编码取俩个字节;这里就有三个问题,第一个,我们已经知道,英文字母只用一个字节表示就够了,用俩个字节就比较浪费内存;第二个问题是如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时间内无法推广,直到互联网的出现(直到出现utf编码,可看正Unicode编码的加强版)。

2.面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

3.Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。

image-20220406000714081

image-20220405230136749

其它的流的使用(了解)

1.标准的输入输出流

System.in:标准的输入流,默认从键盘输入
System.out:标准的输出流,默认从控制台输出

修改默认的输入和输出行为:
System类的setIn(InputStream is) / setOut(PrintStream ps)方式重新指定输入和输出的流。

/*
从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,
直至当输入“e”或者“exit”时,退出程序。
方法一:使用Scanner实现,调用next()返回一个字符串
方法二:使用System.in实现。System.in ---> 转换流 ---> BufferedReader的readLine()
*/
public static void main(String[] args) {
BufferedReader br = null;
try {
InputStreamReader isr = new InputStreamReader(System.in);
br = new BufferedReader(isr);
while (true) {
System.out.println("请输入字符串:");
String data = br.readLine();
if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
System.out.println("程序结束");
break;
}
String upperCase = data.toUpperCase();
System.out.println(upperCase);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

2.打印流:

PrintStream 和PrintWriter

说明:
提供了一系列重载的print()和println()方法,用于多种数据类型的输出
System.out返回的是PrintStream的实例

/*
2. 打印流:PrintStream 和PrintWriter
2.1 提供了一系列重载的print() 和 println()
2.2 练习:
*/
@Test
public void test2() {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));
// 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
ps = new PrintStream(fos, true);
if (ps != null) {// 把标准输出流(控制台输出)改成文件
System.setOut(ps);
}
for (int i = 0; i <= 255; i++) { // 输出ASCII字符
System.out.print((char) i);
if (i % 50 == 0) { // 每50个数据一行
System.out.println(); // 换行
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (ps != null) {
ps.close();
}
}
}

3.数据流:

DataInputStream 和 DataOutputStream

作用:
用于读取或写出基本数据类型的变量或字符串

示例代码:

/*
练习:将内存中的字符串、基本数据类型的变量写出到文件中。
注意:处理异常的话,仍然应该使用try-catch-finally.
*/
@Test
public void test3() throws IOException {
//1.
DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"));
//2.
dos.writeUTF("刘建辰");
dos.flush();//刷新操作,将内存中的数据写入文件
dos.writeInt(23);
dos.flush();
dos.writeBoolean(true);
dos.flush();
//3.
dos.close();
}
/*
将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量中。
注意点:读取不同类型的数据的顺序要与当初写入文件时,保存的数据的顺序一致!
*/
@Test
public void test4() throws IOException {
//1.
DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"));
//2.
String name = dis.readUTF();
int age = dis.readInt();
boolean isMale = dis.readBoolean();
System.out.println("name = " + name);
System.out.println("age = " + age);
System.out.println("isMale = " + isMale);
//3.
dis.close();
}

对象流的使用

对象流

ObjectInputStream 和 ObjectOutputStream

作用:用于存储和读取 基本数据类型数据或 对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来

ObjectOutputStream:内存中的对象--->存储中的文件、通过网络传输出去:序列化过程

ObjectInputStream:存储中的文件、通过网络接收过来 --->内存中的对象:反序列化过程

注意:ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量

对象的序列化

序列化:用ObjectOutputStream类 保存基本类型数据或对象的机制

反序列化:用ObjectInputStream类 读取基本类型数据或对象的机制

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象

序列化的好处在于可将任何实现了Serializable接口的对象转化为 字节数据,使其在保存和传输时可被还原

序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一(Serializable、Externalizable)。否则,会抛出NotSerializableException异常

如何序列化?

  • 若某个类实现了 Serializable 接口,该类的对象就是可序列化的

  • 创建一个 ObjectOutputStream

  • 调用 ObjectOutputStream 对象的 writeObject( 对象) 方法输出可

  • 注意写出一次,操作flush() 一次

如何反序列化?

  • 创建一个 ObjectInputStream
  • 调用 readObject()

强调:如果某个类的属性不是基本数据类型(默认情况下,基本数据类型可序列化)或 String 类型,而是另一个引用类型,那么这个引用类型必须是可序列化的,否则拥有该类型Field 的类也不能序列化

实现序列化的对象所属的类需要满足:

需要实现接口:Serializable

当前类提供一个全局常量:serialVersionUID

除了当前Person类需要实现Serializable接口之外,还必须保证其内部所属性也必须是可序列化的。(默认情况下,基本数据类型可序列化)

例子

public class Person implements Serializable {
public static final long serialVersionUID = 475463534532L;
private String name;
private int age;
private int id;
...
}
/*
序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去
使用ObjectOutputStream实现
*/
@Test
public void testObjectOutputStream(){
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("object.dat"));
oos.writeObject(new Person("王铭",23));
oos.flush();
oos.writeObject(new Person("张学良",23,1001));
oos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(oos != null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
反序列化:将磁盘文件中的对象还原为内存中的一个java对象
使用ObjectInputStream来实现
*/
@Test
public void testObjectInputStream() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("object.dat"));
Person p = (Person) ois.readObject();
Person p1 = (Person) ois.readObject();
System.out.println(p);
System.out.println(p1);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

面试题

谈谈你对java.io.Serializable 接口的理解,我们知道它用于序列化,是空方法接口,还有其它认识吗?

①实现了Serializable 接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。 这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。在 换句话说,可以先在Windows 机器上创台 建一个对象,对其序列化,然后通过网络发给一台Unix 机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

②由于大部分作为参数的类如String 、Integer 等都实现了java.io.Serializable 的接口,也可以利用多态的性质,作为参数使接口更灵活。

RandomAccessFile的使用

RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也可以写

RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意地方来 读、写文件

RandomAccessFile既可以作为一个输入流,又可以作为一个输出流

如果RandomAccessFile作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建。如果写出到的文件存在,则会对原文件内容进行覆盖。(默认情况下,从头开始覆盖)

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针:

  • long getFilePointer():获取文件记录指针的当前位置
  • void seek(long pos):将文件记录指针定位到 pos 位置

image-20220406104102361

注意:JDK1.6上面写的每次write数据时,"rw"模式,数据不会立即写到硬盘中;而"rwd",数据会被立即写入硬盘。如果写数据过程发生异常,"rwd"模式中已被wrie的数据被保存到磁盘,而"rw"则全部丢失。

用途:

我们可以用RandomAccessFile这个类,来实现一个多线程断点下载的功能,用过下载工具的朋友们都知道,下载前都会建立两个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能

例子

@Test
public void test1() {
RandomAccessFile raf1 = null;
RandomAccessFile raf2 = null;
try {
//1.
raf1 = new RandomAccessFile(new File("爱情与友情.jpg"),"r");
raf2 = new RandomAccessFile(new File("爱情与友情1.jpg"),"rw");
//2.
byte[] buffer = new byte[1024];
int len;
while((len = raf1.read(buffer)) != -1){
raf2.write(buffer,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//3.
if(raf1 != null){
try {
raf1.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(raf2 != null){
try {
raf2.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/*
使用RandomAccessFile实现数据的插入效果
*/
@Test
public void test3() throws IOException {
RandomAccessFile raf1 = new RandomAccessFile("hello.txt","rw");
raf1.seek(3);//将指针调到角标为3的位置
//保存指针3后面的所数据到StringBuilder中
StringBuilder builder = new StringBuilder((int) new File("hello.txt").length());
byte[] buffer = new byte[20];
int len;
while((len = raf1.read(buffer)) != -1){
builder.append(new String(buffer,0,len)) ;
}
//调回指针,写入“xyz”
raf1.seek(3);
raf1.write("xyz".getBytes());
//将StringBuilder中的数据写入到文件中
raf1.write(builder.toString().getBytes());
raf1.close();
}

Path、Paths、Files的使用

1.NIO的使用说明:

Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新的IO API,可以替代标准的Java IO API
NIO与原来的IO同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于通道的IO操作
NIO将以更加高效的方式进行文件的读写操作。
随着 JDK 7 的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。

2.Path的使用 ---jdk7提供

2.1 Path的说明:
Path替换原有的File类
2.2如何实例化:

image-20220406113322779

2.3常用方法:

image-20220406113338805

3.Files工具类 ---jdk7提供

3.1作用:
操作文件或文件目录的工具类
3.2常用方法:

image-20220406113359748

image-20220406113406948

网络编程

InetAddress类的使用

InetAddress类的使用

一、实现网络通信需要解决的两个问题
  • 如何准确地定位网络上一台或多台主机;定位主机上的特定的应用 ---》(IP和端口号)
  • 找到主机后如何可靠高效地进行数据传输 ---》提供网络通信协议:TCP/IP参考模型(应用层、传输层、网络层、物理+数据链路层)
二、通信要素一:IP和端口号

1.IP的理解

  • IP:唯一的标识 Internet 上的计算机(通信实体)
  • 在Java中使用InetAddress类的实例对象代表IP
  • IP分类方式1:IPv4 和 IPv6 ;
    • IPV4:4个字节组成,4个0-255。大概42亿,30亿都在北美,亚洲4亿。2011年初已经用尽。以点分十进制表示,如192.168.0.1
    • PV6:128位(16个字节),写成8个无符号整数,每个整数用四个十六进制位表示,数之间用冒号(:)分开,如:3ffe:3201:1401:1280:c8ff:fe4d:db39:1984
  • IP地址分类方式2: 公网地址( 万维网使用)和 私有地址( 局域网使用)。192.168.开头的就是私有址址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用
  • 域名: 例如:【www.baidu.com www.mi.com www.sina.com www.jd.com】
    • 域名解析:域名容易记忆,当在连接网络时输入一个主机的域名后,域名服务器(DNS)负责将域名转化成IP地址,这样才能和主机建立连接。 -------域名解析

2.InetAddress类:InetAddress 类没有提供公共的构造器,而是提供 了 如下几个 静态方法来获取InetAddress 实例

2.1 IP地址实例化

  • public static InetAddress getByName(String host) :根据主机名获取ip地址
  • public static InetAddress getLocalHost():获取本机的主机名和ip地址

2.2 InetAddress 提供了如下几个常用 的方法

  • public String getHostAddress() :返回 IP 地址字符串(以文本表现形式)
  • public String getHostName() :获取此 IP 地址的主机名
  • public boolean isReachable(int timeout): :测试是否可以达到该地址
public static void main(String[] args) {
try {
//IP地址实例化
InetAddress inet1 = InetAddress.getByName("192.168.10.14");
System.out.println(inet1);
InetAddress inet2 = InetAddress.getByName("www.atguigu.com");
System.out.println(inet2);
InetAddress inet3 = InetAddress.getByName("127.0.0.1");
System.out.println(inet3);
//获取本地ip
InetAddress inet4 = InetAddress.getLocalHost();
System.out.println(inet4);
//getHostName()
System.out.println(inet4.getHostName());
//getHostAddress()
System.out.println(inet4.getHostAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}

运行结果

image-20220406173333645

3.端口号:正在计算机上运行的进程。

  • 要求:不同的进程不同的端口号
  • 范围:被规定为一个 16 位的整数 0~65535。
  • 端口分类
    • 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,Telnet占用端口23)
    • 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)。
    • 动态/ 私有端口:49152~65535。
  • 端口号与IP地址的组合得出一个网络套接字:Socket
三、通信要素二:网络通信协议
  • 网络通信协议
    计算机网络中实现通信必须有一些约定,即通信协议,对速率、传输代码、代码结构、传输控制步骤、出错控制等制定标准。
  • 问题:网络协议太复杂
    计算机网络通信涉及内容很多,比如指定源地址和目标地址,加密解密,压缩解压缩,差错控制,流量控制,路由控制,如何实现如此复杂的网络协议呢?
  • 通信协议分层的思想
    在制定协议时,把复杂成份分解成一些简单的成份,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信、上一层可以调用下一层,而与再下一层不发生关系。各层互不影响,利于系统的开发和扩展。

TCP/IP协议簇

  • 传输层协议中有两个非常重要的协议:传输控制协议TCP(Transmission Control Protocol);用户数据报协议UDP(User Datagram Protocol)。
  • TCP/IP 以其两个主要协议:传输控制协议(TCP) 和网络互联协议(IP)而得名,实际上是一组协议,包括多个具有不同功能且互为关联的协议。
  • IP(Internet Protocol)协议是网络层的主要协议,支持网间互连的数据通信。
  • TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即物理链路层、IP 层、传输层和应用层

分型模型

image-20220406173747593

image-20220406173948871

2.TCP和UDP的区别

image-20220406173802839

image-20220406173810823

image-20220406173826911

3.TCP三次握手和四次挥手

image-20220406173840509

image-20220406173847511

image-20220406173856479

image-20220406173903656

Socket

  • 利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。
  • 网络上具有唯一标识的IP地址和端口号组合在一起才能构成唯一能识别的标识符套接字
  • 通信的两端都要有Socket,是两台机器间通信的端点。
  • 网络通信其实就是Socket间的通信。
  • Socket允许程序把网络连接当成一个流,数据在两个Socket间通过IO传输。
  • 一般主动发起通信的应用程序属客户端,等待通信请求的为服务端。
  • Socket分类:
    • 流套接字(stream socket):使用TCP提供可依赖的字节流服务
    • 数据报套接字(datagram socket):使用UDP提供“尽力而为”的数据报服务

image-20220406180515197

TCP网络编程

  • Java语言的基于套接字编程分为服务端编程和客户端编程,其通信模型如图所示:

image-20220406180655718

  • 客户端Socket 的工作过程包含以下四个基本的步骤

    • 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
    • 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输
    • 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
    • 关闭 Socket: :断开客户端到服务器的连接,释放线路
  • 客户端程序可以使用Socket类创建对象,创建的同时会自动向服务器方发起连接 接。Socket的构造器是:

    • Socket(String host,int port)throws UnknownHostException,IOException:向服务器(域名是host。端口号为port)发起TCP连接,若成功,则创建Socket对象,否则抛出异常。
    • Socket(InetAddress address,int port)throws IOException:根据InetAddress对象所表示的IP地址以及端口号port发起连接。
  • 客户端建立socketAtClient对象的过程就是向服务器发出套接字连接请求

image-20220406181325300

  • 服务器 程序的工作过程包含以下四个基本的 步骤:

    • 调用 ServerSocket(int port) : :创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
    • 调用 accept():监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
    • 调用 该Socket 类对象的 getOutputStream() 和 和 getInputStream ():获取输出流和输入流,开始网络数据的发送和接收。
    • 关闭ServerSocket 和Socket 对象:客户端访问结束,关闭通信套接字。
  • ServerSocket 对象负责等待客户端请求建立套接字连接,类似邮局某个窗口中的业务员。也就是说, 服务器必须事先建立一个等待客户请求建立套接字连接的ServerSocket 对象

  • 所谓“接收”客户的套接字请求,就是accept()方法会返回一个 Socket 对象

image-20220406181822097

//客户端
@Test
public void client() {
Socket socket = null;
OutputStream os = null;
try {
//1.创建Socket对象,指明服务器端的ip和端口号
InetAddress inet = InetAddress.getByName("192.168.14.100");
socket = new Socket(inet,8899);
//2.获取一个输出流,用于输出数据
os = socket.getOutputStream();
//3.写出数据的操作
os.write("你好,我是客户端mm".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.资源的关闭
if(os != null){
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务端
@Test
public void server() {
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
//1.创建服务器端的ServerSocket,指明自己的端口号
ss = new ServerSocket(8899);
//2.调用accept()表示接收来自于客户端的socket
socket = ss.accept();
//3.获取输入流
is = socket.getInputStream();
//不建议这样写,可能会有乱码
// byte[] buffer = new byte[1024];
// int len;
// while((len = is.read(buffer)) != -1){
// String str = new String(buffer,0,len);
// System.out.print(str);
// }
//4.读取输入流中的数据
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[5];
int len;
while((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据");
} catch (IOException e) {
e.printStackTrace();
} finally {
if(baos != null){
//5.关闭资源
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(socket != null){
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(ss != null){
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 实现TCP的网络编程
* 例题3:从客户端发送文件给服务端,服务端保存到本地。并返回“发送成功”给客户端。
* 并关闭相应的连接。
* @author shkstart
* @create 2019 下午 4:13
*/
public class TCPTest3 {
/*
这里涉及到的异常,应该使用try-catch-finally处理
*/
@Test
public void client() throws IOException {
//1.
Socket socket = new Socket(InetAddress.getByName("127.0.0.1"),9090);
//2.
OutputStream os = socket.getOutputStream();
//3.
FileInputStream fis = new FileInputStream(new File("beauty.jpg"));
//4.
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
//关闭数据的输出
socket.shutdownOutput();
//5.接收来自于服务器端的数据,并显示到控制台上
InputStream is = socket.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] bufferr = new byte[20];
int len1;
while((len1 = is.read(buffer)) != -1){
baos.write(buffer,0,len1);
}
System.out.println(baos.toString());
//6.
fis.close();
os.close();
socket.close();
baos.close();
}
/*
这里涉及到的异常,应该使用try-catch-finally处理
*/
@Test
public void server() throws IOException {
//1.
ServerSocket ss = new ServerSocket(9090);
//2.
Socket socket = ss.accept();
//3.
InputStream is = socket.getInputStream();
//4.
FileOutputStream fos = new FileOutputStream(new File("beauty2.jpg"));
//5.
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("图片传输完成");
//6.服务器端给予客户端反馈
OutputStream os = socket.getOutputStream();
os.write("你好,美女,照片我已收到,非常漂亮!".getBytes());
//7.
fos.close();
is.close();
socket.close();
ss.close();
os.close();
}
}

UDP网络编程

说明

  • 类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序。
  • UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达
  • DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
  • UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接。如同发快递包裹一样

DatagramSocket 类的常用方法

image-20220406182226451

DatagramPacket类的常用方法

image-20220406182309082

运行流 程:

  1. DatagramSocket与DatagramPacket
  2. 建立发送端,接收端
  3. 建立数据包
  4. 调用Socket的发送、接收方法
  5. 关闭Socket

测试

public class UDPTest {
//发送端
@Test
public void sender() throws IOException {
DatagramSocket socket = new DatagramSocket();
String str = "我是UDP方式发送的导弹";
byte[] data = str.getBytes();
InetAddress inet = InetAddress.getLocalHost();
DatagramPacket packet = new DatagramPacket(data, 0, data.length, inet, 9090);
socket.send(packet);
socket.close();
}
//接收端
@Test
public void receiver() throws IOException {
DatagramSocket socket = new DatagramSocket(9090);
byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
socket.receive(packet);
System.out.println(new String(packet.getData(), 0, packet.getLength()));
socket.close();
}
}

URL编程

1.URL(Uniform Resource Locator)的理解:

统一资源定位符,对应着互联网的某一资源地址,它表示 Internet 上 某一资源的地址

它是一种具体的URI,即URL可以用来标识一个资源,而且还指明了如何locate这个资源。

通过 URL 我们可以访问 Internet 上的各种网络资源,比如最常见的 www,ftp站点。浏览器通过解析给定的 URL 可以在网络上查找相应的文件或其他资源。

2.URL的5个基本结构:

image-20220406182833581

3.如何实例化:

为了表示URL,java.net 中实现了类 URL。我们可以通过下面的构造器来初始化一个 URL 对象:

  • public URL (String spec):通过一个表示URL地址的字符串可以构造一个URL对象。例如:URL url = new URL ("http://www. atguigu.com/");

  • public URL(URL context, String spec):通过基 URL 和相对 URL 构造一个 URL 对象。例如:URL downloadUrl = new URL(url, “download.html")

  • public URL(String protocol, String host, String file); 例如:new URL("http","www.atguigu.com", “download. html");

  • public URL(String protocol, String host, int port, String file); 例如: URL gamelan = new URL("http", "www.atguigu.com", 80, “download.html");

URL类的构造器都声明抛出非运行时异常,必须要对这一异常进行处理,通常是用 try-catch 语句进行捕获。

4.常用方法:

image-20220406182927322

5.针对HTTP 协议的URLConnection类

  • 若希望输出数据,例如向服务器端的 CGI公共网关接口-Common Gateway Interface-的简称,是用户浏览器和服务器端的应用程序进行连接的接口)程序发送一些数据,则必须先与URL建立连接,然后才能对其进行读写,此时需要使用URLConnection 。
    URLConnection:表示到URL所引用的远程对象的连接。当与一个URL建立连接时,首先要在一个 URL 对象上通过方法 openConnection() 生成对应的 URLConnection对象。如果连接过程失败,将产生IOException.
URL netchinaren = new URL ("http://www.atguigu.com/index.shtml");
URLConnectonn u = netchinaren.openConnection( );
  • 通过URLConnection对象获取的输入流和输出流,即可以与现有的CGI程序进行交互。

image-20220406184356980

6.可以读取、下载对应的url资源:

public class URLTest1 {
public static void main(String[] args) {
HttpURLConnection urlConnection = null;
InputStream is = null;
FileOutputStream fos = null;
try {
URL url = new URL("http://localhost:8080/examples/beauty.jpg");
urlConnection = (HttpURLConnection) url.openConnection(); //这里并没有访问url地址,只是获取连接对象
urlConnection.connect(); //这里才真正访问url地址
is = urlConnection.getInputStream();
fos = new FileOutputStream("day10\\beauty3.jpg");
byte[] buffer = new byte[1024];
int len;
while((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
}
System.out.println("下载完成");
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(fos != null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(urlConnection != null){
urlConnection.disconnect();
}
}
}
}

URI 、URL 和URN

image-20220406184534029

小 结

image-20220406184616256

Java反射机制

反射的概述

概述

  • Reflection(反射)是被视为动态语言的关键反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法

  • 加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为: 反射

Java反射机制提供的功能

在运行时判断任意一个对象所属的类
在运行时构造任意一个类的对象
在运行时判断任意一个类所具有的成员变量和方法
在运行时获取泛型信息
在运行时调用任意一个对象的成员变量和方法
在运行时处理注解
生成动态代理

反射相关的主要API

java.lang.Class: 代表一 个 类
java.lang.reflect.Method: 代表类 的 方法
java.lang.reflect.Field: 代表类的 成员 变量
java.lang.reflect.Constructor: 代表类 的 构造 器

动态语言 vs 静态语言

1.动态语言

是一类在运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化。通俗点说就是 在运行时代码可以根据某些条件改变自身结构

主要动态语言:Object-C、C#、JavaScript、PHP、Python、Erlang。

2 .静态语言

与动态语言相对应的,运行时结构不可变的语言就是静态语言。如Java、C、C++。

image-20220407113738611

体会反射的动态性

//体会反射的动态性
@Test
public void test2() {
for (int i = 0; i < 100; i++) {
int num = new Random().nextInt(3);//0,1,2
String classPath = "";
switch (num) {
case 0:
classPath = "java.util.Date";
break;
case 1:
classPath = "java.lang.Object";
break;
case 2:
classPath = "com.atguigu.java.Person";
break;
}
try {
Object obj = getInstance(classPath);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*
创建一个指定类的对象。
classPath:指定类的全类名
*/
public Object getInstance(String classPath) throws Exception {
Class clazz = Class.forName(classPath);
return clazz.newInstance();
}

Class类的理解与获取Class的实例

Class 类

  • 在Object类中定义了以下的方法,此方法将被所有子类继承:

public final Class getClass():返回值的类型是一个Class类,此类是Java反射的源头,实际上所谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名称

image-20220407114618037

  • 对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个 Class 对象包含了特定某个结构(class/interface/enum/annotation/primitive type/void/[])的有关信息。
    • Class本身也是一个类
    • Class 对象只能由系统建立对象
    • 一个加载的类在 JVM 中只会有一个Class实例
    • 一个Class对象对应的是一个加载到JVM中的一个.class文件
    • 每个类的实例都会记得自己是由哪个 Class 实例所生成
    • 通过Class可以完整地得到一个类中的所有被加载的结构
    • Class类是Reflection的根源,针对任何你想动态加载、运行的类,唯有先获得相应的Class对象

Class类的理解

1.类的加载过程:
程序经过javac.exe命令以后,会生成一个或多个字节码文件(.class结尾)。
接着我们使用java.exe命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为Class的一个实例
2.换句话说,Class的实例就对应着一个运行时类。
3.加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。

Class类的常用方法

image-20220407115012075

获取Class实例的几种方式:(前三种方式需要掌握)

//方式一:调用运行时类的属性:.class
Class clazz1 = Person.class;
System.out.println(clazz1);
//方式二:通过运行时类的对象,调用getClass()
Person p1 = new Person();
Class clazz2 = p1.getClass();
System.out.println(clazz2);
//方式三:调用Class的静态方法:forName(String classPath)
Class clazz3 = Class.forName("com.atguigu.java.Person");
//clazz3 = Class.forName("java.lang.String");
System.out.println(clazz3);
System.out.println(clazz1 == clazz2);
System.out.println(clazz1 == clazz3);
//方式四:使用类的加载器:ClassLoader (了解)
ClassLoader classLoader = ReflectionTest.class.getClassLoader();
Class clazz4 = classLoader.loadClass("com.atguigu.java.Person");
System.out.println(clazz4);
System.out.println(clazz1 == clazz4);

总结:创建类的对象的方式?

方式一:new + 构造器
方式二:要创建Xxx类的对象,可以考虑:Xxx、Xxxs、XxxFactory、XxxBuilder类中查看是否有静态方法的存在。可以调用其静态方法,创建Xxx对象。
方式三:通过反射

Class实例可以是哪些结构的说明

image-20220407115619871

image-20220407115715623

类的加载与ClassLoader的理解

类的加载过程

当程序主动使用某个类时,如果该类还未被加载到内存中,则系统会通过如下三个步骤来对该类进行初始化。

image-20220407115950493

image-20220407120218784

image-20220407120243802

了解: 什么时候会发生类初始化

image-20220407120937317

image-20220407121002495

类加载器的作用

image-20220407121218503

类的加载器的分类

类加载器作用是用来把类(class)装载进内存的。JVM 规范定义了如下类型的类的加载器

image-20220407121523555

Java类编译、运行的执行的流程

image-20220407121304891

测试不同的类由不同的加载器加载

public class ClassLoaderTest {
@Test
public void test1() {
//对于自定义类,使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);
//调用系统类加载器的getParent():获取扩展类加载器
ClassLoader classLoader1 = classLoader.getParent();
System.out.println(classLoader1);
//调用扩展类加载器的getParent():无法获取引导类加载器,值为null
//引导类加载器主要负责加载java的核心类库,无法加载自定义类的。
ClassLoader classLoader2 = classLoader1.getParent();
System.out.println(classLoader2);
}
}

运行结果

image-20220407122246949

使用Classloader加载src目录下的配置文件

/*
Properties:用来读取配置文件。
*/
@Test
public void test2() throws Exception {
Properties pros = new Properties();
//此时的文件默认在当前的module下。
//读取配置文件的方式一:
// FileInputStream fis = new FileInputStream("jdbc.properties");
// FileInputStream fis = new FileInputStream("src\\jdbc1.properties");
// pros.load(fis);
//读取配置文件的方式二:使用ClassLoader
//配置文件默认识别为:当前module的src下
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("jdbc1.properties");
pros.load(is);
String user = pros.getProperty("user");
String password = pros.getProperty("password");
System.out.println("user = " + user + ",password = " + password);
}

注:FileInputStream(文件路径)默认读取当前的module下的文件路径;getResourceAsStream(文件路径)默认读取当前module的src下文件路径

反射应用一:创建运行时类的对象

创建 类的对象:调用Class对象的newInstance()方法;内部调用了运行时类的空参的构造器

Class<Person> clazz = Person.class;
Person obj = clazz.newInstance();
System.out.println(obj);

要 求:

1)类必须有一个无参数的构造器。
2)类的构造器的访问权限需要足够。通常,设置为public

在javabean中要求提供一个public的空参构造器。原因:
1.便于通过反射,创建运行时类的对象
2.便于子类继承此运行时类时,默认调用super()时,保证父类有此构造器

难道没有无参的构造器就不能创建对象了吗?
不是!只要在操作的时候明确的调用类中的构造器,并将参数传递进去之后,才可以实例化操作。
步骤如下:

1)通过Class类的getDeclaredConstructor(Class … parameterTypes)取得本类的指定形参类型的构造器
2)向构造器的形参中传递一个对象数组进去,里面包含了构造器中所需的各个参数。
3)通过Constructor实例化对象。

image-20220407123242886

//1. 根据全类名获取对应的Class 对象
String name = “atguigu.java.Person";
Class clazz = null;
clazz = Class.forName(name);
//2. 调用指定参数结构的构造器,生成Constructor 的实例
Constructor con = clazz.getConstructor(String.class,Integer.class);
//3. 通过Constructor 的实例创建对应类的对象,并初始化类属性
Person p2 = (Person) con.newInstance("Peter",20);
System.out.println(p2);

反射应用二:获取运行时类的完整结构

我们可以通过反射,获取对应的运行时类中所有的属性、方法、构造器、父类、接口、父类的泛型、包、注解、异常等。。。。

  1. 实现的全部接口
    public Class<?>[] getInterfaces():确定此对象所表示的类或接口实现的接口

  2. 所继承的父类
    public Class<? Super T> getSuperclass():返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的Class。

  3. 全部的构造器
    public Constructor[] getConstructors():返回此 Class 对象所表示的类的所有public构造方法(包括父类的public 类型的构造器)。
    public Constructor[] getDeclaredConstructors():返回此 Class 对象表示的类声明的所有构造方法(仅包括自己的构造器,但不限于public,同时包含public、protected、缺省、private的构造器)。

​ Constructor类中:
​ 取得修饰符: public int getModifiers();
​ 取得方法名称: public String getName();
​ 取得参数的类型:public Class<?>[] getParameterTypes();

  1. 全部的方法
    public Method[] getDeclaredMethods():返回此Class对象所表示的类全部方法
    public Method[] getMethods():返回此Class对象所表示的类或父类的public的方法

​ Method类中:
​ public Class<?> getReturnType()取得全部的返回值

​ public Class[] getParameterTypes()取得全部的参数 ​ public int getModifiers()取得修饰符 ​ public Class[] getExceptionTypes()取得异常信息

  1. 全部的Field
    public Field[] getFields():返回此Class对象所表示的类或父类的public的Field。
    public Field[] getDeclaredFields():返回此Class对象所表示类的全部Field。

​ Field方法中:
​ public int getModifiers() 以整数形式返回此Field的修饰符
​ public Class<?> getType() 得到Field的属性类型
​ public String getName() 返回Field的名称。

  1. Annotation 相关
    get Annotation(Class annotationClass)
    getDeclaredAnnotations()

  2. 泛型相关
    获取带泛型的父类:Type getGenericSuperclass()
    泛型类型:ParameterizedType
    获取实际的泛型类型参数数组:getActualTypeArguments()

  3. 类所在的包 Package getPackage()

典型代码:

提前准备

public class Creature<T> implements Serializable {
private char gender;
public double weight;
private void breath(){
System.out.println("生物呼吸");
}
public void eat(){
System.out.println("生物吃东西");
}
}
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "hello";
}
public interface MyInterface {
void info();
}
@MyAnnotation(value = "hi")
public class Person extends Creature<String> implements Comparable<String>, MyInterface {
private String name;
int age;
public int id;
public Person() {
}
@MyAnnotation(value = "abc")
private Person(String name) {
this.name = name;
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
@MyAnnotation
private String show(String nation) {
System.out.println("我的国籍是:" + nation);
return nation;
}
public String display(String interests, int age) throws NullPointerException, ClassCastException {
return interests + age;
}
@Override
public void info() {
System.out.println("我是一个人");
}
@Override
public int compareTo(String o) {
return 0;
}
private static void showDesc() {
System.out.println("我是一个可爱的人");
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", id=" + id +
'}';
}
}

FiledTest.java

package com.atguigu.java2;
import com.atguigu.java1.Person;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
* 获取当前运行时类的属性结构
*
* @author shkstart
* @create 2019 下午 3:23
*/
public class FieldTest {
@Test
public void test1() {
Class clazz = Person.class;
//获取属性结构
//getFields():获取当前运行时类及其父类中声明为public访问权限的属性
Field[] fields = clazz.getFields();
for (Field f : fields) {
System.out.println(f);
}
System.out.println();
//getDeclaredFields():获取当前运行时类中声明的所有属性(包含私有属性)。(不包含父类中声明的属性)
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields) {
System.out.println(f);
}
}
//权限修饰符 数据类型 变量名
@Test
public void test2() {
Class clazz = Person.class;
Field[] declaredFields = clazz.getDeclaredFields();
for (Field f : declaredFields) {
//1.权限修饰符
int modifier = f.getModifiers();
System.out.print(Modifier.toString(modifier) + "\t");
//2.数据类型
Class type = f.getType();
System.out.print(type.getName() + "\t");
//3.变量名
String fName = f.getName();
System.out.print(fName);
System.out.println();
}
}
}

MethodTest.java

/**
* 获取运行时类的方法结构
*/
public class MethodTest {
@Test
public void test1() {
Class clazz = Person.class;
//getMethods():获取当前运行时类及其所有父类中声明为public权限的方法
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);
}
System.out.println();
//getDeclaredMethods():获取当前运行时类中声明的所有方法。(不包含父类中声明的方法)
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(m);
}
}
/*
@Xxxx
权限修饰符 返回值类型 方法名(参数类型1 形参名1,...) throws XxxException{}
*/
@Test
public void test2() {
Class clazz = Person.class;
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
//1.获取方法声明的注解
Annotation[] annos = m.getAnnotations();
for (Annotation a : annos) {
System.out.println(a);
}
//2.权限修饰符
System.out.print(Modifier.toString(m.getModifiers()) + "\t");
//3.返回值类型
System.out.print(m.getReturnType().getName() + "\t");
//4.方法名
System.out.print(m.getName());
System.out.print("(");
//5.形参列表
Class[] parameterTypes = m.getParameterTypes();
if (!(parameterTypes == null && parameterTypes.length == 0)) {
for (int i = 0; i < parameterTypes.length; i++) {
if (i == parameterTypes.length - 1) {
System.out.print(parameterTypes[i].getName() + " args_" + i);
break;
}
System.out.print(parameterTypes[i].getName() + " args_" + i + ",");
}
}
System.out.print(")");
//6.抛出的异常
Class[] exceptionTypes = m.getExceptionTypes();
if (exceptionTypes.length > 0) {
System.out.print("throws ");
for (int i = 0; i < exceptionTypes.length; i++) {
if (i == exceptionTypes.length - 1) {
System.out.print(exceptionTypes[i].getName());
break;
}
System.out.print(exceptionTypes[i].getName() + ",");
}
}
System.out.println();
}
}
}

Other.java

public class OtherTest {
/*
获取构造器结构
*/
@Test
public void test1() {
Class clazz = Person.class;
//getConstructors():获取当前运行时类中声明为public的构造器
Constructor[] constructors = clazz.getConstructors();
for (Constructor c : constructors) {
System.out.println(c);
}
System.out.println();
//getDeclaredConstructors():获取当前运行时类中声明的所有的构造器
Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor c : declaredConstructors) {
System.out.println(c);
}
}
/*
获取运行时类的父类
*/
@Test
public void test2() {
Class clazz = Person.class;
Class superclass = clazz.getSuperclass();
System.out.println(superclass);
}
/*
获取运行时类的带泛型的父类
*/
@Test
public void test3() {
Class clazz = Person.class;
Type genericSuperclass = clazz.getGenericSuperclass();
System.out.println(genericSuperclass);
}
/*
获取运行时类的带泛型的父类的泛型
代码:逻辑性代码 vs 功能性代码
*/
@Test
public void test4() {
Class clazz = Person.class;
Type genericSuperclass = clazz.getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) genericSuperclass;
//获取泛型类型
Type[] actualTypeArguments = paramType.getActualTypeArguments();
// System.out.println(actualTypeArguments[0].getTypeName());
System.out.println(((Class) actualTypeArguments[0]).getName());
}
/*
获取运行时类实现的接口
*/
@Test
public void test5() {
Class clazz = Person.class;
Class[] interfaces = clazz.getInterfaces();
for (Class c : interfaces) {
System.out.println(c);
}
System.out.println();
//获取运行时类的父类实现的接口
Class[] interfaces1 = clazz.getSuperclass().getInterfaces();
for (Class c : interfaces1) {
System.out.println(c);
}
}
/*
获取运行时类所在的包
*/
@Test
public void test6() {
Class clazz = Person.class;
Package pack = clazz.getPackage();
System.out.println(pack);
}
/*
获取运行时类声明的注解
*/
@Test
public void test7() {
Class clazz = Person.class;
Annotation[] annotations = clazz.getAnnotations();
for (Annotation annos : annotations) {
System.out.println(annos);
}
}
}

反射应用三:调用运行时类的指定结构

1.调用指定方法

通过反射,调用类中的方法,通过Method类完成。步骤:

1.通过Class类的getMethod(String name,Class…parameterTypes)方法取得一个Method对象,并设置此方法操作时所需要的参数类型

2.之后使用Object invoke(Object obj, Object[] args)进行调用,并向方法中传递要设置的obj对象的参数信息

Refection.java

image-20220407130209989

Object invoke(Object obj, Object … args)

说明:
1.Object 对应原方法的返回值,若原方法无返回值,此时返回null
2.若原方法若为静态方法,此时形参Object obj可为null
3.若原方法形参列表为空,则Object[] args为null
4.若原方法声明为private,则需要在调用此invoke()方法前,显式调用方法对象的setAccessible(true)方法,将可访问private的方法。

2.调用指定属性

在反射机制中,可以直接通过Field类操作类中的属性,通过Field类提供的set()和get()方法就可以完成设置和取得属性内容的操作。
public Field getField(String name) 返回此Class对象表示的类或父类的指定的public的Field。
public Field getDeclaredField(String name)返回此Class对象表示的类的指定的Field。
在Field 中:
public Object get(Object obj) 取得指定对象obj上此Field的属性内容
public void set(Object obj,Object value) 设置指定对象obj上此Field的属性内容

关于setAccessible方法的使用

Method和Field、Constructor对象都有setAccessible()方法。
setAccessible启动和禁用访问安全检查的开关
参数值为true则指示反射的对象在使用时应该取消Java语言访问检查。
提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为true。使得原本无法访问的私有成员也可以访问
参数值为false则指示反射的对象应该实施Java语言访问检查。

典型代码:

/**
* 调用运行时类中指定的结构:属性、方法、构造器
*/
public class ReflectionTest {
/*
不需要掌握
*/
@Test
public void testField() throws Exception {
Class clazz = Person.class;
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
//获取指定的属性:要求运行时类中属性声明为public
//通常不采用此方法
Field id = clazz.getField("id");
/*
设置当前属性的值
set():参数1:指明设置哪个对象的属性 参数2:将此属性值设置为多少
*/
id.set(p, 1001);
/*
获取当前属性的值
get():参数1:获取哪个对象的当前属性值
*/
int pId = (int) id.get(p);
System.out.println(pId);
}
/*
如何操作运行时类中的指定的属性 -- 需要掌握
*/
@Test
public void testField1() throws Exception {
Class clazz = Person.class;
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
//1. getDeclaredField(String fieldName):获取运行时类中指定变量名的属性
Field name = clazz.getDeclaredField("name");
//2.保证当前属性是可访问的
name.setAccessible(true);
//3.获取、设置指定对象的此属性值
name.set(p, "Tom");
System.out.println(name.get(p));
}
/*
如何操作运行时类中的指定的方法 -- 需要掌握
*/
@Test
public void testMethod() throws Exception {
Class clazz = Person.class;
//创建运行时类的对象
Person p = (Person) clazz.newInstance();
/*
1.获取指定的某个方法
getDeclaredMethod():参数1 :指明获取的方法的名称 参数2:指明获取的方法的形参列表
*/
Method show = clazz.getDeclaredMethod("show", String.class);
//2.保证当前方法是可访问的
show.setAccessible(true);
/*
3. 调用方法的invoke():参数1:方法的调用者 参数2:给方法形参赋值的实参
invoke()的返回值即为对应类中调用的方法的返回值。
*/
Object returnValue = show.invoke(p, "CHN"); //String nation = p.show("CHN");
System.out.println(returnValue);
System.out.println("*************如何调用静态方法*****************");
// private static void showDesc()
Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
//如果调用的运行时类中的方法没有返回值,则此invoke()返回null
// Object returnVal = showDesc.invoke(null);
Object returnVal = showDesc.invoke(Person.class);
System.out.println(returnVal);//null
}
/*
如何调用运行时类中的指定的构造器
*/
@Test
public void testConstructor() throws Exception {
Class clazz = Person.class;
//private Person(String name)
/*
1.获取指定的构造器
getDeclaredConstructor():参数:指明构造器的参数列表
*/
Constructor constructor = clazz.getDeclaredConstructor(String.class);
//2.保证此构造器是可访问的
constructor.setAccessible(true);
//3.调用此构造器创建运行时类的对象
Person per = (Person) constructor.newInstance("Tom");
System.out.println(per);
}
}

反射应用四:动态代理

1.代理模式的原理:

使用一个代理将对象包装起来, 然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

静态代理

代理类和目标对象的类都是在编译期间确定下来,不利于程序的扩展。
每一个代理类只能为一个接口服务,这样一来程序开发中必然产生过多的代理。

动态代理

动态代理是指客户通过代理类来调用被代理类对象的方法,并且是在程序运行时根据需要动态创建目标类的代理对象

动态代理相比于静态代理的优点

抽象角色中(接口)声明的所有方法都被转移到调用处理器一个集中的方法中处理,这样,我们可以更加灵活和统一的处理众多的方法。

2.Java 动态代理相关API

  • Proxy :专门完成代理的操作类,是所有动态代理类的父类。通过此类为一个或多个接口动态地生成实现类。

  • 提供用于创建动态代理类和动态代理对象的静态方法

    • static Class getProxyClass(ClassLoader loader, Class... interfaces) :创建一个动态代理类所对应的Class对象
    • static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) :直接创建一个动态代理对象

image-20220407184546428

动态代理步骤

1.创建一个实现接口InvocationHandler的类,它必须实现invoke方法(或者写成匿名实现类匿名对象),以完成代理的具体操作

image-20220407184739967

2.创建被代理的类以及接口

image-20220407184906463

3.通过Proxy的静态方法

newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 创建一个Subject 接口代理

RealSubject target = new RealSubject();
// Create a proxy to wrap the original implementation
DebugProxy proxy = new DebugProxy(target);
// Get a reference to the proxy through the Subject interface
Subject sub = (Subject) Proxy.newProxyInstance(Subject.class.getClassLoader(),new Class[] { Subject.class }, InvocationHandler实现类对象);
4.通过 Subject代理调用RealSubject实现类的方法
String info = sub.say(“Peter", 24);
System.out.println(info);

动态代理与AOP

  • 使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的意义。通常都是为指定的目标对象生成动态代理

  • 这种动态代理在AOP中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP 代理里的方法可以在执行目标方法之前、之后插入一些通用处理

动态代理代码实现

/**
* 动态代理的举例
*/
interface Human{
String getBelief();
void eat(String food);
}
//被代理类
class SuperMan implements Human{
@Override
public String getBelief() {
return "I believe I can fly!";
}
@Override
public void eat(String food) {
System.out.println("我喜欢吃" + food);
}
}
class HumanUtil{
public void method1(){
System.out.println("====================通用方法一====================");
}
public void method2(){
System.out.println("====================通用方法二====================");
}
}
class ProxyFactory{
//调用此方法,返回一个代理类的对象。解决问题一
public static Object getProxyInstance(Object obj){//obj:被代理类的对象
MyInvocationHandler handler = new MyInvocationHandler();
handler.bind(obj);
return Proxy.newProxyInstance(obj.getClass().getClassLoader(),obj.getClass().getInterfaces(),handler);
}
}
class MyInvocationHandler implements InvocationHandler{
private Object obj;//需要使用被代理类的对象进行赋值
public void bind(Object obj){
this.obj = obj;
}
//当我们通过代理类的对象,调用方法a时,就会自动的调用如下的方法:invoke()
//将被代理类要执行的方法a的功能就声明在invoke()中
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
HumanUtil util = new HumanUtil();
util.method1();
//method:即为代理类对象调用的方法,此方法也就作为了被代理类对象要调用的方法
//obj:被代理类的对象
Object returnValue = method.invoke(obj,args);
util.method2();
//上述方法的返回值就作为当前类中的invoke()的返回值。
return returnValue;
}
}
public class ProxyTest {
public static void main(String[] args) {
SuperMan superMan = new SuperMan();
//proxyInstance:代理类的对象
Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
//当通过代理类对象调用方法时,会自动的调用被代理类中同名的方法
String belief = proxyInstance.getBelief();
System.out.println(belief);
proxyInstance.eat("四川麻辣烫");
System.out.println("*****************************");
NikeClothFactory nikeClothFactory = new NikeClothFactory();
ClothFactory proxyClothFactory = (ClothFactory) ProxyFactory.getProxyInstance(nikeClothFactory);
proxyClothFactory.produceCloth();
}
}

Java8的新特性

Java8新特性概述

11111

特点

速度更快
代码更少(增加了新的语法:Lambda 表达式)
强大的 Stream API
便于并行
最大化减少空指针异常:Optional
Nashorn引擎,允许在JVM上运行JS应用

并行 流与 串行

并行流 就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。相比较串行的流,并行的流可以很大程度上提高程序的执行效率。
Java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel() 与 sequential() 在并行流与顺序流之间进行切换。

Lambda表达式

为什么使用 Lambda

Lambda 是一个 匿名函数,我们可以把 Lambda 表达式理解为是 一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

从匿名类到 Lambda 的转换

举例1

image-20220408215704660

举例2:

image-20220408215759508

Lambda 表达式:语法

Lambda 表达式:在Java 8 语言中引入的一种新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符箭 头操作符。它将 Lambda 分为两个部分:

左侧:指定了 Lambda 表达式需要的参数列表(其实就是接口中的抽象方法的形参列表)
右侧:指定了 Lambda 体 ,是抽象方法的实现逻辑(其实就是重写的抽象方法的方法体),也即Lambda 表达式要执行的功能。

Lambda 表达式:六种情况

image-20220408220333601

image-20220408220340641

类型推断

上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序
的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。

image-20220408220630855

总结六种情况:
左边:lambda形参列表的参数类型可以省略(类型推断);如果lambda形参列表只一个参数,其一对()也可以省略
右边:lambda体应该使用一对{}包裹;如果lambda体只一条执行语句(可能是return语句),省略这一对{}和return关键字

函数式接口

1.函数式接口的使用说明

如果一个接口中,只声明了一个抽象方法,则此接口就称为函数式接口

我们可以在一个接口上使用 @FunctionalInterface 注解,这样做可以检查它是否是一个函数式接口。

Lambda表达式的本质:作为函数式接口的实例

函数式接口举例

image-20220408221216880

自定义函数式接口

image-20220408221316970

作为递参数传递 Lambda image-20220408221848645

2.Java内置四大核心函数式接口

image-20220408222043475

其他接口

image-20220408222123562

方法引用

理解

Lambda体里面已经有实现的方法了,可以使用方法引用!

方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,可以认为是Lambda表达式的一个语法糖

格式

类(或对象) :: 方法名

分为如下的三种情况

情况1 对象 :: 非静态方法

情况2 类 :: 静态方法

情况3 类 :: 非静态方法

要求

  • 要求接口中的抽象方法的形参列表和返回值类型与方法引用的方法的形参列表和返回值类型相同!(针对于情况1和情况2)
  • 当函数式接口方法的第一个参数是需要引用方法的调用者,并且第二个参数是需要引用方法的参数(或无参数)时:ClassName::methodName(针对于情况3)

例如:

image-20220408225431311

image-20220408230158284

使用举例

image-20220408224703769

image-20220408224725362

public class MethodTest {
// 情况一:对象 :: 实例方法
//Consumer中的void accept(T t)
//PrintStream中的void println(T t)
@Test
public void test01() {
// 匿名实现类
Consumer<String> c1 = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
//lamda表达式
Consumer<String> c2 = s -> System.out.println(s);
//方法引用
Consumer<String> c3 = System.out::println;
}
//Supplier中的T get()
//Employee中的String getName()
@Test
public void test02() {
Employee emp = new Employee(1001, "Tom", 23, 5600);
//匿名实现类
Supplier s1 = new Supplier<String>() {
@Override
public String get() {
return emp.getName();
}
};
//lamda表达式
Supplier<String> s2 = () -> emp.getName();
//方法引用
Supplier<String> s3 = emp::getName;
}
// 情况二:类 :: 静态方法
//Comparator中的int compare(T t1,T t2)
//Integer中的int compare(T t1,T t2)
@Test
public void test03() {
//匿名实现类
Comparator c1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
//lamda表达式
Comparator<Integer> c2 = (o1, o2) -> Integer.compare(o1, o2);
//方法引用
Comparator<Integer> c3 = Integer::compare;
}
//Function中的R apply(T t)
//Math中的Long round(Double d)
@Test
public void test04() {
//匿名实现类
Function f1 = new Function<Double, Long>() {
@Override
public Long apply(Double aDouble) {
return Math.round(aDouble);
}
};
//lamda表达式
Function<Double, Long> f2 = aDouble -> Math.round(aDouble);
//方法引用
Function<Double, Long> f3 = Math::round;
}
// 情况三:类 :: 实例方法 (有难度)
// Comparator中的int comapre(T t1,T t2)
// String中的int t1.compareTo(t2)
@Test
public void test05() {
//匿名实现类
Comparator c1 = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
};
//lamda表达式
Comparator<Integer> c2 = (o1, o2) -> o1.compareTo(o2);
//方法引用
Comparator<Integer> c3 = Integer::compareTo;
}
//BiPredicate中的boolean test(T t1, T t2);
//String中的boolean t1.equals(t2)
@Test
public void test06() {
//匿名实现类
BiPredicate p1 = new BiPredicate<String, String>() {
@Override
public boolean test(String s, String s2) {
return s.equals(s2);
}
};
//lamda表达式
BiPredicate<String, String> p2 = (s, s2) -> s.equals(s2);
//方法引用
BiPredicate<String, String> p3 = String::equals;
}
// Function中的R apply(T t)
// Employee中的String getName();
@Test
public void test07() {
Employee employee = new Employee(1001, "Jerry", 23, 6000);
Function f1 = new Function<Employee, String>() {
@Override
public String apply(Employee emp) {
return emp.getName();
}
};
Function<Employee, String> f2 = emp -> emp.getName();
Function<Employee, String> f3 = Employee::getName;
}
}

构造器引用与数组引用

1.构造器引用格式

类名::new

2.构造器引用使用要求

和方法引用类似,函数式接口的抽象方法的形参列表和构造器的形参列表一致。抽象方法的返回值类型即为构造器所属的类的类型

3.构造器引用举例:
//Supplier中的T get()
//Employee的空参构造器:Employee()
@Test
public void test1(){
Supplier<Employee> sup = new Supplier<Employee>() {
@Override
public Employee get() {
return new Employee();
}
};
System.out.println("*******************");
Supplier<Employee> sup1 = () -> new Employee();
System.out.println(sup1.get());
System.out.println("*******************");
Supplier<Employee> sup2 = Employee :: new;
System.out.println(sup2.get());
}
//Function中的R apply(T t)
@Test
public void test2(){
Function<Integer,Employee> func1 = id -> new Employee(id);
Employee employee = func1.apply(1001);
System.out.println(employee);
System.out.println("*******************");
Function<Integer,Employee> func2 = Employee :: new;
Employee employee1 = func2.apply(1002);
System.out.println(employee1);
}
//BiFunction中的R apply(T t,U u)
@Test
public void test3(){
BiFunction<Integer,String,Employee> func1 = (id,name) -> new Employee(id,name);
System.out.println(func1.apply(1001,"Tom"));
System.out.println("*******************");
BiFunction<Integer,String,Employee> func2 = Employee :: new;
System.out.println(func2.apply(1002,"Tom"));
}
4.数组引用格式:

数组类型[] :: new

//Function中的R apply(T t)
@Test
public void test4(){
Function<Integer,String[]> func1 = length -> new String[length];
String[] arr1 = func1.apply(5);
System.out.println(Arrays.toString(arr1));
System.out.println("*******************");
Function<Integer,String[]> func2 = String[] :: new;
String[] arr2 = func2.apply(10);
System.out.println(Arrays.toString(arr2));
}

Stream API

1.Stream API的理解:

  • Stream API ( java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码
  • Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。 使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简言之,Stream API 提供了一种高效且易于使用的处理数据的方式
  • Stream关注的是对数据的运算,与CPU打交道;集合Collection关注的是数据的存储,与内存打交道

2.注意点:

①Stream 自己不会存储元素。
②Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
③Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行(遇到终止操作才执行)。

3.Stream的使用流程:

① Stream的实例化

② 一系列的中间操作(过滤、映射、...):每个中间操作(多个中间操作形成操作链),都会对数据源的数据进行处理并返回新的数据(源数据不变)

③ 终止操作:一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用

image-20220408232825348

4.步骤一:Stream实例化

//创建 Stream方式一:通过集合
@Test
public void test1(){
List<Employee> employees = EmployeeData.getEmployees();
// default Stream<E> stream() : 返回一个顺序流
Stream<Employee> stream = employees.stream();
// default Stream<E> parallelStream() : 返回一个并行流
Stream<Employee> parallelStream = employees.parallelStream();
}
//创建 Stream方式二:通过数组
@Test
public void test2(){
int[] arr = new int[]{1,2,3,4,5,6};
//调用Arrays类的static <T> Stream<T> stream(T[] array): 返回一个流
IntStream stream = Arrays.stream(arr);
Employee e1 = new Employee(1001,"Tom");
Employee e2 = new Employee(1002,"Jerry");
Employee[] arr1 = new Employee[]{e1,e2};
Stream<Employee> stream1 = Arrays.stream(arr1);
}
//创建 Stream方式三:通过Stream的of()
@Test
public void test3(){
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
}
//创建 Stream方式四:创建无限流
@Test
public void test4(){
// 迭代
// public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
//遍历前10个偶数
Stream.iterate(0, t -> t + 2).limit(10).forEach(System.out::println);
// 生成
// public static<T> Stream<T> generate(Supplier<T> s)
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}

5.步骤二:中间操作

image-20220408233212887

image-20220408233230775

image-20220408233241856

6.步骤三:终止操作

image-20220408233315760

image-20220408233322233

image-20220408233329312

image-20220408233341084

Collector需要使用Collectors提供实例;Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、Set、Map)。另外, Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表

image-20220408233623360

Optional类的使用

1.理解

  • 为了解决java中的空指针问题而生!

  • Optional 类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常

  • Optional类的Javadoc描述如下:这是一个可以为null的容器对象。如果值存在,则isPresent()方法会返回true,调用get()方法会返回该对象

2.常用方法:

image-20220408234044997

@Test
public void test1(){
//empty():创建的Optional对象内部的value = null
Optional<Object> op1 = Optional.empty();
if(!op1.isPresent()){//Optional封装的数据是否包含数据
System.out.println("数据为空");
}
System.out.println(op1);
System.out.println(op1.isPresent());
//如果Optional封装的数据value为空,则get()报错。否则,value不为空时,返回value.
// System.out.println(op1.get());
}
@Test
public void test2(){
String str = "hello";
// str = null;
//of(T t):封装数据t生成Optional对象。要求t非空,否则报错。
Optional<String> op1 = Optional.of(str);
//get()通常与of()方法搭配使用。用于获取内部的封装的数据value
String str1 = op1.get();
System.out.println(str1);
}
@Test
public void test3(){
String str = "beijing";
str = null;
//ofNullable(T t) :封装数据t赋给Optional内部的value。不要求t非空
Optional<String> op1 = Optional.ofNullable(str);
//orElse(T t1):如果Optional内部的value非空,则返回此value值。如果
//value为空,则返回t1.
String str2 = op1.orElse("shanghai");
System.out.println(str2);//
}

3.典型练习:

能保证如下的方法执行中不会出现空指针的异常。

//使用Optional类的getGirlName():
public String getGirlName2(Boy boy){
Optional<Boy> boyOptional = Optional.ofNullable(boy);
//此时的boy1一定非空
Boy boy1 = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));
Girl girl = boy1.getGirl();
Optional<Girl> girlOptional = Optional.ofNullable(girl);
//girl1一定非空
Girl girl1 = girlOptional.orElse(new Girl("古力娜扎"));
return girl1.getName();
}
@Test
public void test5(){
Boy boy = null;
boy = new Boy();
boy = new Boy(new Girl("苍老师"));
String girlName = getGirlName2(boy);
System.out.println(girlName);
}

posted @   努力&选择  阅读(95)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示