学习路线
- **Java零基础入门必刷视频2024版(上)**√
- **Java零基础入门必刷视频2024版(中)**√
- **Java零基础入门必刷视频2024版(下)**√
- 老杜MySQL(2024版)
- JDBC
- web基础
- VUE
- JavaWeb
- Maven
- MyBatis
- MyBatisPlus
- Spring6
- SpringMVC
- SpringBoot3
- Git
- Redis7
- Dubbo
- SpringCloud Alibaba
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka
- Docker
- Kubernetes/K8S
- SpringSecurity
- Java大厂面试题
- JVM面试题
- Java多线程与JUC并发编程
Java零基础上部(2024版)–>Java零基础中部(2024版)–>Java零基础下部(2024版)–> MySQL(2024版)–>JDBC(2024版) ->Vue–> JavaWeb -->Ajax–>Maven入门到精通(2024版)–>MyBatis–>Spring–>SpringMVC(2024版)–>Linux(2024版)–> Git–>SpringBoot3 -->Redis7 -->Spring Security–> MyBatis Plus–>RabbitMQ–>RocketMQ -->Dubbo ->SpringCloud–>SpringCloudAlibaba(深入版) -->Docker-> Kubernetes/K8S --Java多线程与并发编程–>JVM面试题–>Java大厂面试题
一、Java知识点
数据元素是数据最小的描述单位,但是下面还有数据项
数据元素描述一个对象,数据项是具体的属性,多个属性组成的数据项成为组合数据项。
算法要有有穷性、确定性:相同的输入一定获得相同的输出。可行性:有限次执行。输入和输出。
线性表(linear list)是具有相同数据类型的n个数据元素的有限序列
位序:从1开始
为什么要实现对数据结构的基本操作?
- 封装后方便调用
顺序表:用顺序存储的方式实现线性表的顺序存储
静态分配:存储空间是静态的,表长无法修改。
动态分配:需要指示动态分配数组的指针
顺序表的特点:随机访问(可以在0(1)的时间找到任意一个元素)、存储密度高、扩展容量不方便、插入删除数据元素不方便
链表:单链表、双链表、循环列表、静态链表
不要求大片连续空间、改变容量方便。但是访问数据不方便
java是由c++编写的。
.java----javac----.class。包含两个阶段:编译阶段和运行阶段,这两个阶段可以在不同的操作系统上完成
JDK 包含 JRE 包含 JVM
java长期支持版本:8、11、17、21
-
java8引入了Lambda表达式、Stream API、新的日期、时间API
-
java11引入了HTTP Client
-
java17引入了Sealed、Pattern类、Pattern Matching for switch、Records
classpath环境变量是隶属于java的,不是windows系统。如果没有配置,会默认从当前路径下找。在配置classpath路径时,通常用
.;路径
.的作用是先从当前路径下找
用;隔开,没找到的话从配置路径找
三种注释:单行、多行、javadoc注释
一个java文件中可以定义多个类,但是只能定义一个public类,和源文件类名一致。文件中public类可以没有。每个类中都可以定义main方法,只用使用java A执行A类的main方法。
二、Java基础语法
标识符
一个java源程序中可以自己取名的,都叫为标识符,可以用来标识变量名、方法名、类型名、包名、常量名…
命名规则(语法):
- 只能由数字、字母(任何一个国家的文字)、_ 、$组成
- 不能以数字开头
- 严格区分大小写
- 关键字不能做标识符
- 没有长度限制
命名规范(常识):
- 见名知意
- 驼峰命名法
- 类名:首字母大写,后续没法单词首字母大写
- 变量名:首字母小写,后续没法单词首字母大写
- 常量名:全部大写,_分割
- 包名:全部小写
关键字
在java语言中,所有关键字都是全部小写,一共50个,不能再用来做标识符
保留字:goto、const。目前没有用,但是开发者不能使用
public class static void 等等
字面量
程序中直接使用的数据,最基本的表达式,无需转换、直接使用。
eg:10,3.14、false、true
整数型、浮点型、布尔型、字符型、字符串型
变量
变量是内存中的一块空间,是计算机中存储数据最基本的单元。
三要素:数据类型、变量名、变量值。
过程:声明、赋值、访问。访问包含读和修改两种情况
作用:1、便于代码的维护。2、增强代码的可读性。
一行代码中可以同时声明多个变量。
在一个作用域中,变量名不能重名、可以重新赋值。一个{}就是一个域。作用域就是变量的有效范围
变量的分类:
- 局部变量
- 在方法体中定义的变量一定是局部变量
- 成员变量:
- 静态变量:带static
- 实例变量
二进制
权值:二进制中,每个位所代表的数字大小
十进制转换未二进制:除2取余,一直到商0为止。
在数字前面加上0b,程序会将其作为二进制来识别
八进制与十六进制
十六进制:1、2、3、4、5、6、7、8、9、10、a、b、c、d、e、f
在数字前面加上0x,程序会将其作为十六进制来识别
在数字前面加上0,程序会将其作为八进制来识别
二进制转换为十六进制
- 将二进制树从右往左,每四位为一组,不足四位的补0.
原码反码补码
byte=8bit
在计算机中,数据通常以字节(byte)为单位进行存储和传输,而比特(bit)是表示数据的最小单位。
1KB=1024byte
-
原码反码补码是计算机二进制的三种表示形式
-
计算机底层是采用补码形式表示
-
最高位是符号位,0为正数,1为负数
-
正数的源码反码补码相同
-
负数的原码:绝对值转化为二进制后,最高位改为1
-
负数的反码:以原码为参考,符号位不变,其他位取反
-
负数的补码:以反码为参考,符号位不变,加1
-
已知负数的补码,符号位不变,其他位取反再加1,得到原码
为什么用补码:
- 简化电路
- 原码中有+0和-0,但是补码中只有一个0
数据类型
整数型
基本数据类型(8种):byte(8)、short(16)、int(32)、long(64)、float(单精度,7位小数,32)、double(双精度,15位小数,64)、boolean(8)、char(16)
引用数据类型:类(String、Object…)、接口、数组、枚举…
java种任务一个整数型字面量都会默认当作int类型来处理
自动类型转换:小容量可以自动赋值给大容量的变量
byte<short<int<long<float<double
//int最大为2147483647
long e = 2147483648;
print(e);
//会报错,因为在赋值前默认为int,超出了范围,加一个L
long e = 2147483648L;
强制类型转换:需要添加强制类型转换符,是否会损失精度看数值大小
long x = 1000L;
int y = (int)x;
在赋值byte、short时,如果没有超出范围,也可以编译通过,因为内部做了优化.
如果强制转化又超出了范围,会按照补码进行截取
byte b = 1;
- 两个int类型数据运算后仍然是int类型
- 多种数据类型混合运算时,会先转换为最大容量类型进行运算。
- byte和short运算时会先转换为int
浮点型
默认当作double来处理,如果想使用float,需要在字面量后加F/f
//报错
float x = 3.14;
//需要
float x = 3.14F;
float y = (float)3.14;
表示方法:十进制、科学计数法
double x = 1e-3;
double x = 1.23;
double x = .23;
字符型
char类型采用统一的字符编码方式:Unicode编码
char可以存储一个汉字,默认值为\u0000
转义字符:
\t \n \' \" \\
字符编码是认为规定的文字和二进制之间的转换关系(美国发明的)
ASCII码采用1个字节编码,实际上ASCII码表只用了128个,需要记住3个:
- a(97):01100001
- A(66):01000010
- 0(49):00110001
- 乱码一定是编码和解码过程中使用的码表不同造成的
- 常见的字符编码:ASCII、Latin-1、ANSI、Unicode(可表示所有语言的字符,采用16进制,占用2个字节或4个字节)、UTF-8(Unicode的可变长度字符编码,最常用)、UTF-16、UTF-32、GB2312(小)、GBK(中、针对中文设计的一个汉字编码方式)、GB18030
- java中允许将一个整数赋值给char类型,但是会按照ascii编码来进行赋值,所以可以使用
char c = 'a';
print(c+1);//会输出b
布尔型
int类型无法转换为boolean
boolean值只用true、false,没有0和1的说法
运算符
一元运算符、二元运算符(符号两边都有数)
算数运算符:+、-、*、/、%、++、–
关系运算符:>、>=、<、<=、==、!=。关系运算符的运算结果一定是true或false
逻辑运算符:&、|、!、^、&&、||(与、或、非、异或、短路与、短路或)
按位运算符:<<、>>、>>>、&、|、^、~。作用在二进制位级别上,
- 左移n位,结果是乘2的n次方
- 移除的会被截断,右侧补0
- 右移的话,对于正数左补0,对于负数左补1
- 无符号右移(>>>):不考虑符号位,都是左补0,所以都会变回正数
- 按位与可以用来判断一个数是不是奇数
- 异或主要用于加密解密上,它具有自反性,对同一个数进行两次异或,相当于不变。
- 按位取反:位清除操作,将指定位上的数变为0
//将低位第4个清0
int value = 0b11111111
int flag = (1<<3);
value = value & (~flasg);
赋值运算符:=、+=、-=、/=、*=、>>=…
条件运算符:三元运算符,布尔表达式?表达式1:表达式2。为true时执行1,false执行2.
为什么System.out.println()中传入不同类型的值都能够进行输出,因为在java原码中对其进行了不同方法的创建。这是一个final定义的类,其次,该类的构造器是以private权限进行定义的。根据这两情况可以说明,该类即不能被继承也无法实例化成对象,同时需注意一点,就是这个类里定义的很多变量和方法都是static来定义的,即这些类成员都是属于类而非对象。每个有传参的p rintln方法里,其最后调用的方法都是print()与newLine()。
值得注意一点,这些带有传参的println方法当中,里面都是通过同步synchronized来修饰,这说明System.out.println其实是线程安全的。同时还有一点需注意,在多线程情况下,当大量方法执行同一个println打印时,其synchronized同步性能效率都可能出现严重性能问题。因此,在实际生产上,普遍是用log.info()类似方式来打印日志而不会用到System.out.println。
控制语句
用于控制程序的执行流程
分支语句
- if(4种)
if(){
}
if(){
}else{
}
if(){
}else if(){
}
if(){
}else if(){
}else(){
}
- switch
default语句不是必须的,但是要记得加上break,节省时间
jdk7之前支支持int、枚举类型,jdk7之后,增加了String类型
//express需要时int、String、枚举类型的
switch(express){
case value1:
.......
break;
case value1:
.......
break;
default:
}
java12之后switch引入了新特性
switch(x){
case 1,2,3,4->print();
case 5,6,7,8->print();
default ->
}
循环语句
for
while
do…while
跳转语句
break
continue
方法
-
语法格式:【修饰符列表】返回值类型 方法名(形式参数列表){}
-
当返回值类型是void时,可以不写return
-
实参于形参要一一对应
-
方法如果只定义,不调用是不会分配内存空间的。(java8开始,方法的字节码指令在元空间metaspace中,元空间使用的是本地内存)
-
方法调用的瞬间,会在栈内存中分配活动场所,此时发生压栈操作
-
方法重载(overload),编译阶段的机制,在编译阶段已经完成了方法的绑定
- 在一个类中
- 方法名一致/
- 形式参数列表不同(类型、顺序、个数)
- 好处:不用为了对不同的参数类型或参数个数,而写多个函数。
7.递归:方法自己调用自己。能用循环尽量不适用递归,太占内存,一直压栈
包机制
- 只能出现在代码第一行且只能写一行
- 所有的包名都是小写
- 命名规范:公司域名倒序 + 项目名 + 模块名 + 功能名
import
- 静态导入:会导入该类的所有方法和变量(以.*结尾)
三、面向对象
面向过程(PO):关注点在实现功能的步骤上。c
面向对象(OO):关注点在实现功能需要哪些对象的参与。java、c#、python
- 对于简单的流程适合使用面向过程的方式进行,复杂的流程不适合使用面好像过程的开发方式。
- 面向对象开发方式耦合度低,扩展能力强。
类
-
类实际上是人类大脑撕烤总结的一个模板,是一个抽象的概念。
-
类 = 属性 + 方法
-
状态在程序中对应属性,属性通常用变量来表示
-
象为在程序中对应方法,用方法来描述行为动作
-
定义的类属于引用类型
对象
- 对象是实际存在的个体,又称为实例(instanse)
- 通过类可以实例化n个对象
- 对象中的属性又成为实例变量,实例变量属于成员变量
元空间是java8之后引入的,存储的是类的元信息,字节码等,可以看作是JVM规范中方法区的实现。方法区是JVM规范中的叫法,元空间是对方法区的实现。在java8之前,方法区的实现叫做永久代。
JVM是一套规范,各个厂家根据这个规范实现具体的java虚拟机。
所有使用运算符new的对象,都存储在堆内存中。
当方法被调用是,会给该方法分配空间,在VM Stack压栈。
对象在堆中,引用在VM Stack中。引用在本质上是一个变量,这个变量中保存了java对象的内存地址。
局部变量存储在栈中,实例变量存储在JVM堆内存的java对象内部。
空指针异常,访问某个属性、方法时是null。
形参属于局部变量,参数传递时时将变量中的值复制一份进行传递
三大特性:封装、继承、多态
封装
实现封装的步骤:
- 属性的私有化,使用private,私有属性只能在本类中进行访问
- 提供公开的访问入口,读和改
构造方法Constructor
- 构造方法的执行分为两个阶段:对象的创建和对象的初始化
- 在使用new时,在内存中会创建一个新的对象,虽然对象已经被创建出来了,但是还没有被初始化
- 构造方法名和类名一致
- 调用方法为使用new来调用
- 不提供返回值类型。但是构造方法执行结束之后,会自动将创建的对象的内存地址返回
- 在java中,如果一个类没有显式地进行定义,系统会默认提供一个无参的构造方法
- 一个类可以有多个构造方法,这些构造方法自动构成了方法的重载
- 构造代码块,语法为{},在执行构造方法前会自动执行,new的时候会分配内存空间,附默认值,之后执行构造代码块,执行构造方法
this关键字
- 出现在实例方法中,代表的是当前对象
- this本质上是一个引用,保存的是对象的内存地址。在代码中this.是可以省略的
- 大部分情况下可以省略,但是在区分实例变量和局部变量是不能省略
- this不能出现在static静态方法中
static关键字
- 修饰的变量叫做静态变量,修饰的方法叫做静态方法
- 在类加载时初始化,存储在堆中
- 使用类名.进行访问,不需要new对象
- 当一个属性是对象级别的,这个属性通常定义为实例变量。如果所有对象都有一个相同的属性值,可以将其定义为static,节省空间
- jdk8之后,静态变量存储在堆内存之中,类加载时进行初始化
- 静态变量亦可以使用“引用.”来访问,但是运行时与对象无关,一个空引用访问实例相关的,都会出现空指针异常
- 使用“引用.”访问静态相关的,即使引用时null,也不会报空指针异常
- 静态代码块在类加载时执行(类加载时刻),可以有多个,会按照自上而下的顺序执行
JVM规范(6块)
程序计数器:记录正在执行的虚拟机字节码指令地址
java虚拟机栈:存储局部变量表、操作数栈、动态链接、方法出口等
堆:java虚拟机管管理的最大的一块内存,存放java实例对象以及属猪,垃圾收集器收集垃圾的主要区域
方法区:存储以被虚拟机加载的类信息、常量
运行时常量池:方法区的一本分,存放编译生成的各种字面量和符号引用
本地方法栈:在本地方法的执行过程中,会使用本地方法栈。存放native方法。
jdk6的HotSpot:
年轻代:刚new出来的对象
老年代:经过垃圾回收之后仍然存活的对象
永久代的垃圾收集和老年代捆绑在一起,无论谁满了,都会出发永久代和老年代的垃圾收集
jdk7的HotSpot:
类的静态变量转移到堆中
字符串常量转移到堆中
符号引用转移到本地内存中
jdk8的HotSpot:
设计模式
设计模式就是可以重复运用的解决方案
- GoF设计模式(23个),一般情况下默认的设计模式
- 架构设计模式:含MVC、微服务等
- 企业级设计模式:SOA、EIP
- 领域驱动模式
- 并发设计模式
- 数据访问模式
GoF设计模式可以分为3种,
继承(extends)
作用:代码复用。有了继承,才有了方法覆盖和多态机制
java只支持单继承,c++支持多继承。但是可以多层继承
字类继承父类,构造方法不支持继承,私有的不继承
覆盖(override)
当从父类种继承过来的方法无法满足使用需求时进行覆盖重写该方法
当子类将父类对象覆盖之后,字类调用会调用覆盖后的方法
什么条件下,构成覆盖:
- 存在继承关系
- 具有相同的方法名
- 具有相同的形参列表
- 具有相同的返回值类型,如果返回值类型时引用类型,可以是他的子类型
@Override,jdk5引入的,被标注的方法必须是重写父类的方法,否则,编译器会报错。只在编译阶段有用,和运行期无关
-
访问权限不能变低,可以更高。public proteded
-
抛出异常不能更多,可以更少
-
私有方法以及构造方法不能继承,因此也不会覆盖
-
针对的是实例方法,和静态static方法无关
-
针对的是实例方法,和实例变量无关
多态
作用:降低耦合,提高程序扩展力。尽量使用多态,面向抽象编程
父类型绑定了子类型的方法
向上转型(子–>父)与向下转型(父–>子)
小容量转换成大容量,是自动类型转换
大容量转换成小容量,是强制类型转换
在引用类型中,不叫自动类型转换和强制类型转换,叫做向上转型与向下转型
Animal a2 = new Cat();
在编译阶段会找Animal进行绑定,此时发生静态绑定。在运行阶段会找Cat的方法执行
在向下转型时,容易出现(ClassCastException)类型转换异常,使用instanceof来避免
if(x instanceof Bird){
Bird y = (Bird)x;
}
实例变量不存在多态,只和编译阶段有关
抽象方法abstract必须重写
super关键字
-
super代表的时当前对象中的父类型特征
-
super不能使用在静态上下文中
-
super大部分情况下是可以省略的,但是当父类和字类中定义了相同的属性或方法时,如果需要在子类中访问父类的属性或方法时,super.不能省略
-
this.可以单独输出,super.不能单独输出。因为this是一个引用,super是代表了父类型特征,编译会报错
-
通过子类的构造方法调用了父类的构造方法,目的是完成父类型特征的初始化
-
当一个构造方法的第一行没有写this(),也没有写super(),他会默认调用super()。如果不写无参构造方法,会影响子类对象的创建。
final
- 表示最终的,不可变的
- final修饰的类无法被继承,无法被覆盖
- 一旦复制,不能修改
- final修饰的实例变量,必须在构造函数前被赋值
- final修饰的实例变量一般和static联合起来使用
- final修饰的引用,一旦指向了某个对象,不能再指向其他对象。指向的内存地址指向的对象信息可以修改
抽象类
如果类中有些方法无法实现或者没有意义
- 一个非抽象的类继承了一个抽象类之后,必须把所有的抽象方法都实现
- 抽象类有构造方法,但是无法实例化
- 抽象类中不一定有抽象方法,但是如果有抽象方法,则必须是抽象类
- abstract关键字不能额和private,final,static关键字共存
接口(interface)
接口在java中表示一种规范或契约,定义了一组抽象方法和常量,用来描述一些实现这个接口的类应该具有哪些行为和属性,为了降低程序的耦合度
- 抽象类是半抽象,接口是完全抽象。接口没有构造方法,也无法实例化
- 接口中只能定义常量+抽象方法(jdk8之前),接口中的static final可以省略,abstract可以省略,接口中所有的方法都是public定义的。
- 接口和接口之间可以多继承,一个类可以继承多个接口
- 类和接口之间的关系叫做实现
- 一个非抽象的类必须实现继承的接口中的全部方法
- jdk8之后,允许接口中出现默认方法(default)和静态方法,但是静态方法只能通过该接口名进行调用。
- jdk9之后允许定义私有的静态方法,给默认方法使用
接口和抽象类如何选择?
两者虽然在代码角度都能够达到同样的效果,但是使用场景不同。
-
抽象类主要使用于公共代码的提取
-
接口主要用于功能的扩展
访问控制权限
private:同一个类
缺省的:同一个类、同一个包
protected:同一个类、同一个包、子类
pubic:都可以
Object类
是所有类的超类
toString
equals():比较的是地址,String类中对equals()方法进行了重写,会比较两个字符串的值
hasCode:返回一个对象的哈希值,通常作为在哈希表中查找该对象的键值。hashCode()方法是为了HashMap、Hashtable、HashSet等集合类进行优化而设置的,以便于快速查找和存储对象。
finalize():销毁用的,在jdk9已经被取消了
clone():
浅克隆(指向同一个地址)
-
对象的拷贝,protected修饰。
-
通常在开发中需要保护对象的数据结构,所以复制一份生成新对象.
-
需要在字类中重写clone方法
-
凡是要参加clone的对象,必须要实现一个标志接口java.lang.Cloneable
深克隆(在创建一个内存空间)
内部类
定义在一个类中的类
- 静态内部类:等同于静态变量
静态内部类中,无法直接访问外部的实例变量和实例方法
- 实例内部类:等同于实例变量
实例内部类可以访问外部的实例成员和静态成员
- 局部内部类:等同于局部变量
局部内部类能不能访问外部类的实例变量和实例方法,取决于局部内部类所在的方法
局部内部类不能使用修饰符
jdk8开始,不需要添加final
- 匿名内部类:没有名字的类,只能使用一次
和继承配合使用,直接继承接口并重写方法
computer.conn(new Usb(){
@Override
public void read(){
}
})
interface Usb{
void read();
}
什么时候用?
- 一个类用到了另外一个类,两个类的关系很密切,如果分开定义不利于阅读理解
- 内部类可以访问外部类的私有成员。这样可以将相关的类和接口隐藏在外部类的内部,从而提高封装性
软件开发的7大原则:
- 开闭原则:对扩展开放,对修改关闭。
- 单一职责原则:只负责单一的职责
- 里氏原则:子类对象可以替换基类对象在任何地方
- 接口隔离原则:客户端不应该依赖他不需要的接口
- 依赖倒置原则:高层模块不应该依赖底层模块,应该依赖于抽象接口
- 迪米特法则:一个对象应该对其他对象保持最少的了解,及一个类应该对自己需要耦合或者调用的类知道的最少
- 合成复用原则:尽量使用对象组合和据合,而不是继承来达到服用的目的
四、数组
在java中,数组是一种用于存储多个相同数据类型元素的容器
是一种引用数据类型,隐式继承Object
数组对象存放在堆内存中
分类:
- 一维、二维
- 基本类型数组、引用类型数组
- 静态数组、动态数组
特点:
- 数组长度一旦确定不可改变
- 数组中元素数据类型类型相同,每个元素占用空间大小相同
- 数组中每个元素在空间存储上,内存地址是连续的
- 每个元素有索引,首元素索引0,以1递增
- 数组中第一个元素的内存地址作为数组对象的内存地址
优点:
- 查询效率相同,时间复杂度为O(1),因为每个元素所占的空间是相同的,所以可以通过下标计算出每个元素的内存地址
缺点:
- 随机增删元素的速度较慢,时间复杂度为O(n),
- 无法存储大量数据,很难在内存上找到一块非常大的连续内存
一维数组
初始化:
静态初始化方式
数据类型[] name= new 数据类型[]{1,2,3,};
数据类型[] name= {1,2,3,};
增强for循环(for-each循环),jdk5引入
优点是代码简洁,可读性强,缺点是没有下标
for(数据中元素的数据类型 name:数组名){
}
动态初始化:当创建数组时,不知道数组中具体存储哪些元素,可以动态初始化
数据类型[] name= new 数据类型[长度];
main方法中的args
- 接收命令行参数
- JSM负责调用ArrayTest.main()方法,JVM负责给main方法准备一个String[]一维数组的对象
- JVM会对参数字符串进行拆分,生成一个新的数组对象
- 用处:在使用系统时,需要用户名密码,比如mysql
- 可变长度参数【int… nums】,可变参数只能有一个,并在只能在参数列表的末尾出现。可变参数可以当作数组来使用
一维数组扩容
- 只能创建一个更大的数组,将原数组中的数据全部拷贝到新数组中
- 使用System.arraycopy()方法完成数组的拷贝
- 数组扩容会影响程序的执行效率,因此尽可能预测数据量,创建一个接近数量的数组,减少扩容次数。
System.araycopy(old_data,lod_start,new_data,new_start,length);
二维数组
二维数组是一个特殊的一维数组,特殊在这个一维数组中每个元素是一个一维数组
初始静态化
数据类型[][] name= new 数据类型[][]{{},{},{},{}};
数据类型[][] name= {{},{},{},{}};
动态初始化
等长
数据类型[][] name= new 数据类型[3][4];
不等长
数据类型[][] nums= new 数据类型[3][];
nums[0] = new int[]{1,2,3,4};
JUnit单元测试
一个项目是巨大的,只有保证每一块都是正确的,整个项目才是正确的
JUnit在JDK中没有,需要额外引入
单元测试类名:XxxTest
需要用@Test注解标注
返回值为void
形参个数为0
数据结构与算法
数据结构是指用来存储和组织数据的一种方式
分为数据的逻辑结构与物理结构
逻辑结构是指数据元素之间的逻辑关系,他是从抽象的角度描述数据元素之间的关系,不涉及具体的存储方式或实现细节,结构主要关注问题的本质、特点和抽象模型。
物理结构是指数据结构在计算机内存中实际存储和组织的方式。
逻辑结构的划分:集合结构、线性结构、树形结构、图形结构或网状结构
物理结构的划分:顺序存储结构、链式存储结构、散列存储结构
冒泡排序
比较相邻两个数的大小,并进行交换
for(int i=0;i<n-1;i++){
for(int j=0;j<n-i-1;j++){
if(nums[j]<nums[j+1]){
swap(nums[j],nums[j+1]);
}
}
}
//优化
for(int i=n-1; i>=0;i--){
boolean flag = true;
for(int j=0;j<i;j++){
if(nums[j] < nums[j+1]){
swap(nums[j],nums[j+1]);
flag = false;
}
}
if(flag){
break;
}
}
选择排序
冒泡排序的交换次数太多,为了解决这个问题,提出选择排序,每一次选出最大或最小的来进行交换
for(int i=0;i<n-1;i++){
int min = i;
for(int j=i+1;i<n-1;j++){
if(nums[j]<nums[min]){
min = j;
}
}
swap(nums[i],nums[min]);
}
查找算法
线性查找
二分法查找:要求数据已经排好了顺序
Arrays工具类
- Arrays.toString(nums):将数组转化为String字符串
- Arrays.deepToString(nums):将数组转化为String字符串,适合于二维及多维数组
- Arrays.equals(nums1,nums2):判断两个数组是否相等
- Arrays.deepEquals(nums1,nums2):判断两个数组是否相等,适合于二维及多维数组
- Arrays.sort():排序,根据字典的顺序。如果数组中存储的是对象,则无法排序,如果想要使用,需要继承Comparable接口,实现compareTo方法,在这个方法中编写比较规则。
- Arrays.parallelSort():启用多核CPU进行s排序,JDK8引入,数据量较小时不建议使用
- Arrays.binarySearch():二分查找
- Arrays.fill(nums,data):填充
- Arrays.copyOf(nums,length):拷贝,可指定长度
- Arrays.copyOfRange(nums,start,end):拷贝,指定起始和结束
五、异常
java中的异常是指程序运行时出现了错误或异常情况,导致程序无法正常执行
异常机制用于提高程序健壮性,在程序出现不正常情况时,对用户进行提示
异常在java中以类和对象的形式存在
所有的编译时异常在程序编写阶段都需要进行处理
所有的运行时异常在程序员编写程序阶段,可以选择处理,也可以选择不处理
异常都是在运行时发生的,编译异常是因为该异常必须在编译阶段处理,不处理会报错
异常的发生需要经历2个阶段:创建异常对象,让异常发生(手动抛出对象)
自定义异常
步骤:
- 写一个类继承Execption或RuntimeException
- 写无参和有参的构造方法
处理异常的两种方式:抛出,捕捉
throws xxx
try{
..如果异常在这里出现,则try块后续的代码不再执行
}catch(异常类型名1 变量名){
}catch(异常类型名2 变量名){
}finally{
}
异常的常用方法:
getMessage():获取当时创建异常构造方法时传递的message参数值
printStachTrace():打印异常堆栈信息
看异常信息主要看栈顶信息(最开始的信息)
finally语句块中的代码是一定会执行的,不同单独使用,需要至少配合try语句块使用。
通常在finally语句块中完成资源的释放
final、finally、finalize区分:
- final是一个关键字,修饰的类无法继承,修饰的方法无法覆盖
- finally是一个关键字
- finalize是一个标识符
父类方法重写之后,不能抛出更多的异常,可以更少
六、常用类
String
String在java中属于引用数据类型,代表字符串
java专门在堆中为字符串准备了一个字符串常量池,因为字符串使用比较频繁,java8之后字符串常量池在堆中,java8之前在永久代中。
在java程序中,凡是带有双引号的字符串,在编译阶段就已经完全确定了,这些字符串字面量将会放在字符串常量池中。
在JVM启动时,会进行一系列的初始化,其中就包括字符串常量池的初始化,在初始化字符串常量池的时候,会将字符串字面量全部提前创建好,放到字符串常量池中。在执行java程序的过程中,如果需要这个字面量对象,直接从字符串常量池中话取,提高执行效率。(常量池属于一种缓存技术)
字符串一旦创建是不可变的,x是可以变的,是字面量不可变
使用+进行拼接产生的新的字符串不会被放到字符串常量池中,在堆中。
实际上在进行 + 时,会创建一个StringBuilder对象,堆字符串进行拼接,最后再转换成String对象
字符串数量的常量池在确定之后仍然可以添加 String s5 = s3.intern();
常用String构建方法
char[] chars = new char[]{'a','b','c'};
//根据字符数组转换字符串
String s1 = new String(chars);
//将char[]数组的一部分转换成字符串
String s2 = new String(chars,0,2);
byte[] bytes = {1,2,3,4};
//根据字节数组转换字符串
String s3 = new String(bytes);
//将byte[]数组的一部分转换成字符串(解码的过程,采用平台默认的字符编码方式进行解码)
String s4 = new String(bytes,0,2);
//造成乱码
byte[] bs = "一段测试".getBytes("GBK");
String s5 = new String(bs,"UTF-8");
System.out.println(s5);
在不知道平台编码方式时,可以动态获取平台的编码方式
.getBytes(Charset.defaultCharset())
concat和+的区别:
- +既可以进行字符串拼接,也可以进行求和。拼接时会创建StringBuilder对象,最后调用toString()方法获取拼接之后的字符串,拼接null时会出现空指针异常
- concat方法只能操作字符串类型,拼接时不会创建StringBuilder对象,拼接null时会出现空指针异常
- 需要进行大量的字符串拼接操作时,建议使用StringBuilder
trim()方法可以去除ASCII码的空白以及制表符tab。无法去除全角空白
strip()可以去除所有的空白。jdk11新增
正则表达式
regular expression,是一种用于描述特定模式的表达式,可以匹配,查找,替换文本中与该模式匹配的内容
应用:
- 验证输入内容格式
- 在文本编辑器中进行搜索和替换
- 数据挖掘与信息提取
- 编写脚本语言
- 服务器端编程
StringBuffer和StringBuilder
这两个类都是专门为频繁进行字符串拼接而准备,可变字符串
StringBuilder在jdk5新增
StringBuilder是线程安全的,不需要考虑线程安全问题
底层是byte[]数组,这个byte[]数组没有被final修饰,说明如果byte[]数组满了,可以创建更大的byte[]
优化策略:创建StringBuilder时预估字符串长度,减少扩容次数
StringBuilder默认初始化容量时16,扩容策略是max(每次的2倍+2,需要的长度)
包装类
都在long包下,对应8种基本数据类型,包装类都是引用数据类型,其中Integer使用最多
8种包种类中的6个数字类型都继承了Number,因此这些类中都有以下方法:
- byteValue()
- shortValue()
- intValue()
- longValue()
- floatValue()
- doubleValue()
Boolean的拆箱方法:booleanValue()
char的拆箱方法:charValue()
常量-最大值最小值
Integer.MAX_VALUE
Integer.MIN_VALUE
构造方法在jdk9之后已过期
jdk5新特性:自动装箱、自动拆箱。属于编译阶段的功能。该机制是为了方便写代码存在的机制
// 自动装箱
Integer x = 1000;
// 自动拆箱
int num = x;
[-128~127]在整数型常量池中提前建好了
Integer x = 1000;
Integer y = 1000;
print(x==y)//false
Integer x = 127;
Integer y = 126;
print(x==y)//true
如果查过long,可以用java.math.BigInter,其父类为Number
如果超过double,可以用java.math.BigDecimal,其父类为Number(经常用在财务软件中)
日期API
java8推出了新的日期API,解决传统日期API存在的线程安全问题
LocalDate、LocalTime、LocalDateTime
枚举
jdk5新增
- 是一种引用数据类型
- 合理使用可以使代码更清洗、可读性更高
- 使用情况:数据有限,能够一一列举出来。枚举类型是安全的(做了类型限定和检查)
- 所有枚举类型都默认继承java.lang.Enum,所以不能再继承其他类,所有枚举类型都被final修饰,无法被继承
- 所有枚举值都是常量
- 所有枚举值都可以通过一个数组获取
- 枚举类的构造方法是私有的
- 如果一个枚举类型中定义了普通类的内容,必须指定枚举值
- 每一个枚举值相当于一个枚举的实例
public enum Season{
SPRING,SUMMER,AUTUMN,WINNER
}
七、集合
集合是一种容器,用来组织和管理数据
每一个集合类底层采用的数据结构不同,例如ArrayList底层采用数组,LinkedList底层采用了双向链表,TreeMap底层采用了红黑树
集合中存储的是引用,不是把堆中的对象存储到集合中,是把对象的地址存储到集合中。
默认情况下,如果不使用泛型的话,集合中可以存储任何类型的引用
集合框架相关的类都在java,util包下
集合框架包括两部分:Collection结构(元素以单个形式存储),Map结构(元素以键值对形式存储)
- SequencedCollection和SequencedSet都是jdk21新增的
- List家族元素都是可重复的,set家族元素都是不可重复的
- 集合可以分为两个部分:Map和Collection
Collection通用方法
boolean add(E e);//向集合中添加元素
int size()//获取集合中元素个数
boolean addAll(Collection c)//将参数集合中的所有元素加入当前集合
boolean contains(Object o)//判断集合中是否包含元素o,会调用equals方法,equals方法重写了
boolean remove(Object 0)//删除元素o
void claer()//清空集合
boolean isEmpty()//判断集合是否为空
Object[] toArray()//将集合转化为一维数组
Collection集合的通用迭代方式/遍历
- 获取集合依赖的迭代器对象
- 判断当前光标指向的位置是否存在元素
- 去除当前光标指向位置的元素,并将光标指向下一个
//如果不是ArrayList,是其他的类也可以
Collection collection = new ArrayList();
collection.add("zhangsan");
collection.add("lisi");
collection.add("wangwu");
collection.add("3.14");
Iterator it = collection.iterator();
while(it.hasNext()){
Object obj = it.next();
System.out.println(obj);
}
SequencedCollection通用方法
- jdk21新增
- addFirst(E)
- addLast()
- getFirst()
- getLast()
- removeFirst()
- removeLast()
- reversed():反转
SequencedCollection col = new ArrayList();
泛型是jdk5的新特性,属于编译阶段的功能
可以让开发者在编写代码时指定集合中存储的数据类型
作用:类型安全(编译器会在编译时进行类型检查,避免了在运行时类型出错的问题)、代码简洁(避免了类型转换)
看程序中是否有<>
钻石表达式是jdk7新特性Collection col = new ArrayList<>();
Collection<User> col = new ArrayList<User>();
col.add(user1);
Iterator<User> it = col.iterator();
while(it.hasNext()){
User user = it.next();
System.out.println(user);
}
泛型擦除:程序编译阶段将泛型删掉,为了兼容1.4、1.5
泛型补偿:程序运行时通过获取元素的实际类型进行强转
如果想在静态方法上使用泛型,必须提前进行定义
public static <T> void shop(T type){
}
接口中的泛型使用方法和类相同
泛型通配符:
- 无限定通配符 <?>,此处?可以引用任意数据类型
- 上限通配符 <? extends Number>,此处?可以引用Number及其子类
- 下限通配符 <? super Number>,此处?可以引用Number及其父类
迭代时删除元素会产生问题
在删除集合元素时,要使用迭代器的remove方法去删,而不是使用集合对象的remove方法(会造成并发修改异常fail-fast机制)。因为在迭代时使用的时迭代器,如果用集合的方法会造成多线程冲突
fail-fast机制:
- 集合中设置了一个modCount属性,用来记录修改次数
- 获取迭代去对象时,会给迭代器对象初始化一个exceptedModCount属性,并且将exceptedModCount初始化为modCount的值,后续当这两个值相等,就不会报异常
- 虽然当前的程序是单线程程序,但是通过迭代器去遍历的同时使用集合去删除元素,会被认为是并发
List
特点:有序(每个元素都有下标,从零开始)可重复
常见实现类:ArrayList、Vector、LinkedList
除了Collection中的方法,还有新的方法,都与下标有关
- void add(int index, E element)
- E set(int index, E element)
- E get(int index)
- indexOf(Object o)
- lastIndexOf(Object 0)
- remove(int index)
- List subList(int start, int end)左闭右开
- sort
List<Integer> nums = new ArrayList<>();
特有的迭代方式
ListIterator
//boolean hasNext()
//E next() 将当前光标指向的元素返回,然后将光标向下移动一位
//void remove() 删除的是上一次next()方法返回的数据
//void add() 添加元素,之后将光标移动下一位
//boolean hasPrevious()
//E previous()将光标上移一位,并将光标指向的元素返回
//nextIndex()
//previousIndex()
//void set(E),修改的是上一次next()方法返回的数据,set调用的前提是之前调用了next()或者previous()
persons.sort(new Comparator<Person>{
@Override
public int compare(Person p1, Person p2) {
return 0;
}
})
ArrayList
底层是数组实现的,所有有数组的优缺点
当调用无参构造方法时,初始化容量0,当第一次调用add方法时,容量加10
ArrayList扩容之后的增长容量是原先的1.5倍
Vector中所有的方法都是线程安全的,但是整体执行效率较低,不建议使用,现在有新的保证线程安全的方法。
Vector扩容后的容量是原来的2倍
LinkedList
底层是一个双向链表
适合于需要频繁进行增删的场景
调用无参数构造方法会创建一个空链表
添加元素时,如果没有指定下标,会默认加到末尾
set方法也有返回值,会返回修改前的值
remove方法也有返回值,会返回被删除的值
栈
Stack、ArrayDeque、LinkedList都实现了栈数据结构
- push():压栈
- pop():弹栈
- peek():查看栈顶元素
- search():返回数字,会从栈顶向下数
队列
先进先出,进行插入的端是队尾,删除的端为队头
接口Queue是队列,Deque是双端队列
类ArrayDeque、LinkedList实现了队列
-
offer():入队
-
poll():出队
环形数组:当检索到最后一个元素时,下标能回到最开始。实际上就是加了一个%
Queue
创建队列对象
Queue<String> queue = new ArrayDeque<>();
queue.offer("1");
queue.poll();
//创建双端队列
Deque<String> deque = new ArrayDeque<>();
deque.offerLast("1");
deque.pollFirst();
deque.offerFirst("2");
deque.pollLast();
Set&&Map
HashSet:无序(没有下标,且不可排序)不可重复
TreeSet和LinkedHashSet都是有序的,TreeSet可以排序,LinkedHashSet有下标
map常用方法:
- V put()
- void putAll()
- V get()
- boolean containsKey()
- boolean containsValue()
- V remove()
- void clear()
- int size()
- boolean isEmpty()
- Collection values()
- Set keySet()
- set<Map.Entry<K,V>> entrySet()
- static <K,V> Map<K,V> of(k,)
Map<Integer,String> maps =new HashMap<>();
maps.put(120,"zhangsan");
maps.put(118,"lisi");
maps.put(119,"李四");
Map<Integer,String> newMaps =new HashMap<>();
newMaps.put(110,"wangwu");
newMaps.putAll(maps);
System.out.println("键值对个数:"+newMaps.size());
System.out.println(maps.get(11));//map中不包含这个key时返回null
//判断集合中是否包含某个key(底层会调用equals方法)
System.out.println(maps.containsKey(120));
//判断集合中是否包含某个value
System.out.println(maps.containsValue("lisi"));
//清空map中的值
newMaps.clear();
//通过key删除key-value键值对
maps.remove(120);
//判断是否为空
System.out.println(newMaps.isEmpty());
//获取所有的value
Collection<String> values = maps.values();
for(String value:values){
System.out.println(value);
}
//静态方法of
Map<Integer, String> userMap = Map.of(1,"zhangsan",2,"lisi");
//遍历Map集合
Set<Integer> keys = maps.keySet();
Iterator<Integer> it = keys.iterator();
while(it.hasNext()){
Integer key = it.next();
String value = maps.get(key);
System.out.println(value);
}
//遍历方法2
Set<Map.Entry<Integer,String>> entrys= maps.entrySet();
Iterator<Map.Entry<Integer, String>> its= entrys.iterator();
HashMap
HashMap集合特点
- key无序,存进去的顺序和去除的顺序不一定相同
- 不可重复:具有唯一性
产生哈希冲突的两种情况:
- 不同的key获得的哈希值相同,那么计算得到的索引位置相同
- 通过key得到不同的哈希值,但是通过“哈希值%数组长度”的结果相同
添加键值对的两种情况;
- 如果索引位置没有储存元素,则将键值对封装为Node对象,然后存储到该索引位置中。
- 如果索引位置有存储元素,就先遍历整合单链表,如果遍历出存在有相同的key,就进行覆盖操作,否则进行添加。jdk8之前是头插法,之后是尾插法
当key中存的是自定义类时,无法保证key是唯一的,会产生冲突,需要重写equals和hashCode方法
如果equals方法返回的结果是true,那么hashCode方法返回的的结果必须相同,这个样才能保证key是不重复的
key可以是null,但是只能有一个,后续会覆盖,null会默认存储到hashcode为0的位置
map.put(null,"abc");
HashMap在jdk8进行的改进:
- 初始化时间:jdk8之前,构造方法执行时初始化table数组。jdk8之后在第一次调用put方法时初始化table数组
- 插入法:头插法改为尾插法
- 数据结构:jdk8之前是数组+单项链表,jdk8之后是数组+单向链表+红黑树
- 最开始使用单向链表解决哈希冲突。如果节点数量>=8,并且table的长度>=64.单向链表转换为红黑树。当删除红黑树上的节点时,节点数量<=6时,红黑树转换为单向链表。
HashMap的容量永远是2的次幂
原因:
- 提高哈希计算的效率(位运算比%取模操作速度快)
- 减少哈希冲突,让散列分布更加均匀
随着哈希表中的元素越来越多,散列碰撞的纪律也越来越高,从而导致单链表过长
HashMap集合扩容成本较高,因为扩容后length会变,哈希计算int index=hash%length会变。所以在初始赋值时要预估元素数量,加载因子0.75,真实容量是容量*加载因子
LinkedHashMap
是HashMap的子类,几乎和HashMap的用法是一样的
可以保证元素插入顺序,所以是有序不可重复
集合的key也需要同时重写hashCode和equals方法
底层数据结构是:哈希表+双向链表
Hashtable
线程安全的
key和value不能为null
初始化容量是11,扩容策略是2倍,加载因子是0.75
Properties
一般叫做属性类,继承于Hashtable,所以也是线程安全的。
一般与java程序中的属性配置文件联合使用,属性配置文件的扩展名:xxxx.properties
不支持泛型,key和value都是String类型
Properties properties = new Properties();
properties.setProperty("jdbc.driver","com.mysql.jdbc.Driver");
properties.setProperty("jdbc.user","root");
String driver = properties.getProperty("jdbc.driver");
String user = properties.getProperty("jdbc.user");
System.out.println(driver);
//获取所有的key
Enumeration<?> names = properties.propertyNames();
while(names.hasMoreElements()){
String name = (String)names.nextElement();
String value = properties.getProperty(name);
System.out.println(name+"="+value);
}
TreeMap
二叉树:由1个结点及两颗互不相交的、分别成为这个根的左子树和右子树的二叉树组成
排序二叉树:
采用左小右大原则存储,按照中序遍历方式,自动就是排好序的
排序二叉树本身实现了排序的功能,可以快速检索,但是如果插入的结点本身就是有序的,那么会严重失衡,最后得到的排序二叉树将变为普通的链表,检索效率较差
平衡二叉树(AVL)
为了避免上述出现的一边倒的存储
在平衡二叉树中任何两个子树的高度差最大为1
结点的平衡因子是它的左子树-右子树的差。带有平衡因子1、0、-1的结点被认为是平衡的
红黑二叉树
是一棵自平衡的排序二叉树
要求:
- 每个结点要么红色,要么黑色
- 根节点永远是黑色
- 所有的叶节点都是空结点,且为黑色
- 每个红色结点的两个子结点是黑色(从每个叶子结点到根结点的路径上不会有两个连续的红色结点)
- 从任意结点到其子树中每个叶子结点的路径都包含相同数量的黑色结点
- 每次插入新结点时,颜色是红色。插入后会根据红黑树的约束条件进行调整
TreeMap集合的key部分是可排序的
key也需要重写hashCode和equals方法
底层采用了红黑二叉树
如果key是自定义类型,默认情况下是无法进行 ,需要重写compare方法。实现接口或创建一个比较器
key不能为null,value可以是null
哪些集合不能为null?
Hashtable的key和value不能为null
Properties的key不能为null
TreeMap的key不能为null
TreeSet不能添加null
HashSet
无序且不可重复
放在HashSet中的元素需要重写hashCode和equals方法
LinkedHashSet
有序(插入顺序有保障)且不可重复
TreeSet
不能存储null
有序(可以排序)且不可重复
hashCode+equals需要重写
Collections工具类
- 针对List提供了sort排序方法
- 混排,打乱顺序:shuffle
- 反转:reverse
- 替换:fill
八、IO流
IO流指的是程序中数据的流动
分类:
根据数据流向分类,输入和输出是相对于内存而言的,
-
输入流:从硬盘到内存(read)
-
输出流:从内存到硬盘(write)
根据读写数据形式
- 字节流:一次读取一个字节。适合读取非文本数据
- 字符类:一次读取一个字符。只适合读取普通文本
Java中所有IO流只要是以Stream结尾的都是字节流。以Reader和Writer结尾的都是字符流
根据流在IO操作中的作用和实现方式分类
- 节点流:节点流负责数据源和数据目的地的连接,是IO中最基本的组成部分
- 处理流(包装流):处理流对节点流进行装饰/包装,提供更多高级处理操作,方便用户进行数据处理
所有的流都实现了Closeable接口,使用完成后需要close
FileInputStream
文件字节输入流,负责读
是一个万能流,任何文件都能读,但是建议读取二进制文件。图片、声音等
FileInputStream也可以读取普通文件,但是一次读取一个字节,容易出现乱码
- int read():调用一次read()方法可以读取一个字节,返回读到的字节本身,如果没有读到任何数据,则返回-1
- int read(byte[] b):一次最多可以督导b.length个字节,返回读到的字节数量,如果没有读到任何数据,则返回-1
- int read(byte[] b, int off, int len):一次读取len个字节,将读到的数据从byte数组的off处开始放
- long skip(long n):跳过n个字节
- int available():返回流中剩余的估计字节数
FileInputStream in = null;
try{
in = new FileInputStream("");
while(true){
int readByte = in.read();
if(readByte == -1) {
System.out.println(readByte);
}
}
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
finally {
if(in != null){
try{
in.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
FileOutputStream
构造方法
FileOutputStream(String name)
FileOutputStream(String name, boolean append)
//没有append属性时,会清空原文件重新写入
FileOutputStream out = new FileOutputStream("");
//当append为true时,表示追加。append为false时,表示清空原文件重新写入
FileOutputStream out = new FileOutputStream("",true);
常用方法:
- close
- flush
- write(int b):写一个字节
- write(byte[] b):将整个byte字节数组写出
- write(byte[] b, int off, int len):将byte字节数组的一部分写出
jdk7新特性:try-with-resources(资源自动关闭)
凡是实现了AutoCloseable接口的流都可以使用try-with-resources,能自动关闭
//最后一个可以不加;
try(FileInputStream in = new FileInputStream("");
FileOutputStream out = new FileOutputStream("")){
}catch (Exception e){
}
实现文件的复制
try(FileInputStream in = new FileInputStream("");
FileOutputStream out = new FileOutputStream("")){
byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = in.read(bytes)) != -1){
out.write(bytes, 0 , readCount);
}
}catch (Exception e){
throw new RuntimeException(e);
}
Reader和Writer
当读取的数据是普通文本内容时,使用FileReader和FileWriter(不能复制ppt等非普通文本文件)
一边读一边写
try(FileReader reader = new FileReader("test.txt");
FileWriter writer = new FileWriter("test1.txt")){
//一边读一边写
char[] chars = new char[1024];
int readCount = 0;
while((readCount = reader.read(chars)) != -1){
writer.write(chars, 0, readCount);
}
//刷新
writer.flush();
}catch (FileNotFoundException e){
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
关于IO流中的文件路径问题
相对路径一定是从当前路径开始,在idea中,相对路径是从项目的根出发,而不是本文件的位置
从类路径当中加载资源,如果这个资源时放在类路径之外,这个方法是不合适的(找不到)
更换操作系统或者环境的话,这个方法也适用
//自动从类路径当中加载资源
String path = Thread.currentThread().getContextClassLoader().getResource("log").getPath();
System.out.println(path);
在内存中的这个大的byte数组就是一个缓冲区,不需要我们维护,由BufferedInputStream和BufferedOutputStream自动维护
包装流和节点流只需要关闭一个即可。关闭包装流时,节点流会自动关闭
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try{
// //创建节点流
// FileInputStream in = new FileInputStream("text");
// //创建包装流
// BufferedInputStream bis = new BufferedInputStream(in);
//组合起来进行创建
bis = new BufferedInputStream(new FileInputStream("test.txt"));
bos = new BufferedOutputStream(new FileOutputStream("test1.txt"));
byte[] bytes = new byte[1024];
int readCount = 0;
while((readCount = bis.read(bytes)) != -1){
bos.write(bytes,0,readCount);
}
bos.flush();
}catch (Exception e){
e.printStackTrace();
}finally{
if(bis != null){
try {
bis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
使用BufferedReader和BufferedWriter每次对一行进行操作
BufferedReader br = new BufferedReader(new FileReader("test.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("test1.txt"));
BufferedWriter bw2 = new BufferedWriter(new FileWriter("test2.txt"));
String s = null;
while((s = br.readLine()) != null){
String[] strs = s.split(",");
System.out.println(strs[0]);
System.out.println(strs[1]);
bw.write(strs[0]);
bw2.write(strs[1]);
bw.flush();
bw2.flush();
}
br.close();
bw.close();
bw2.close();
文件中实际上存储的是二进制,解码的时候需要指定字符集,如果使用FileReader的时候没有指定字符集,会默认为使用utf-8的字符集进行解码
使用FileWriter向文件中写数据时,如果没有使用字符集,也会采用平台指定的字符集,也是UTF-8
FileReader是一个解码的过程
FileWriter是一个编码的过程
InputStreamReader和OutputStreamWriter是转换流,可以用于解决乱码问题
InputStreamReader是输入流,用于解决解码产生的乱码
InputStreamReader常用的构造方法:
- InputStreamReader(InputStream in) 采用平台默认的字符集进行解码
- InputStreamReader(InputStream in, String charsetName) 采用指定的字符集进行解码
FileReader是InputStreamReader的子类,因此也是一个包装流
//InputStreamReader isr = new InputStreamReader(new FileInputStream("test.txt"),"GBK");
//以上代码太长了,在IO流的继承体系结构中,IO流又给InputStringReader提供了一个子类:FileReader
FileReader isr = new FileReader("test.txt", Charset.forName("GBK"));
int readCount = 0;
char[] chars = new char[1024];
while((readCount=isr.read(chars)) != -1){
System.out.println(chars);
}
isr.close();
DataInputStream和DataOutputStream
数据流,将java程序中的数据直接写入到文件,写到文件中的数据就是二进制。
写的效率很高,因为写的过程不需要转码,但是写到文件中的数据只能用DataInputStream进行读取
且读取的顺序要和写入的顺序一致
也就是要先写再读
ObjectOutputStream和ObjectInputStream
可以将对象进行保存(序列化)和读取(反序列化)
readObject()方法
凡是参与序列化和反序列化的对象必须实现java.io.Serializable接口
反序列化:将文件中的Student字节序列恢复到内存中,变成Student对象
当java程序中类实现了Serializable接口,编译器会自动给该类添加一个“序列化版本号”
序列化版本号:serialVersionUID
序列化版本号有什么用?
- 判断是否是同一个类,在java中首先通过类的名字,然后通过序列化版本号
- 可以将serialVersionUID写死,这样如果过程中对对象进行了修改,也可以识别之前的类
private static final long serialVersionUID = 1231237868L;
//如果希望某个属性不进行序列话,可以加transient关键字
private transient int num;
PrintWriter和PrintStream
打印流,专门负责进行打印,提供便捷的打印方法和格式化输出
常用方法:
- println
直接输出各种数据类型
自动刷新和换行
支持转意
自动编码
标准输入流
System.in
从控制台获取数据,普通输入流是从文件或网络获得数据
是一个全局的输入流
标准输出流
System.out
向控制台打印
是一个全局的书v互留,不需要手动关闭,JVM会负责关闭这个流
标准输出流也是可以改变输出方向的
System.setOut(new PrintStream("log"));
//在使用sout方法回想文件中输出
System.out.println("zhangsan1");
File
- File是路径的抽象表示形式
- File和IO流没有关系,弗雷什Object,通过File不能完成文件的读和写
- File可能是一个文件,也可能是一个路径
File file = new File("test.txt");
if(file.exists()){
//获取绝对路径
System.out.println("文件的绝对路径"+file.getAbsoluteFile());
//获取名字
System.out.println("文件名:"+file.getName());
//获取父路径
System.out.println("文件父路径:"+file.getParent());
//判断该路径是否是绝对路径
System.out.println(file.isAbsolute()?"绝对路径":"不是绝对路径");
//判断该路径是否是文件
System.out.println(file.isFile()?"是文件":"不是文件");
//判断该路径是否是隐藏文件
System.out.println(file.isHidden()?"是隐藏文件":"不是隐藏文件");
//获取最后修改时间
long l = file.lastModified();
Date time = new Date(l);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String str = sdf.format(time);
System.out.println("文件最后修改时间:"+ str);
//文件大小,总字节数
System.out.println("文件的字节数"+file.length());
System.out.println("=======================");
File file1 = new File("D:\\Desktop\\工作准备");
//获取所有的子文件和目录
File[] files = file1.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
//return false;
return name.endsWith(".md");
}
});
for(File f:files){
System.out.println(f.getName());
}
//删除文件
file.delete();
}
//如果不存在
if(!file.exists()){
//以目录的形式创建
file.mkdir();
//以文件的方式新建
file.createNewFile();
//以多重目录的形式创建
file.mkdirs();
}
JDK中提供了资源绑定器来绑定属性配置文件
要使用资源绑定器,该文件必须放在类的根路径下
ResourceBundle bundle = ResourceBundle.getBundle("");
String driver = bundle.getString("xxx.driver");
装饰器设计模式
IO流中大量的使用了装饰器模式
主要目标:在松耦合的前提下,能够完成功能的扩展
可以在不修改原有代码的情况下完成功能扩展
在装饰器设计模式中有两个重要的角色,分别为装饰者,被装饰者。
要求装饰者与被装饰者应该实现同一些接口,继承同一个抽象类。使用装饰者的时候就像使用被装饰者一样
装饰者中含有被装饰者的引用
所有的装饰者应该有一个共同的父类(抽象类)
GZIPInputStream和GZIPOuputStream
压缩流,GZIPOuputStream专门进行文件压缩的,生成的文件格式是xxx.gz
gzip.finish()进行生成压缩文件
节点流关闭时会自动刷新,包装流需要手动刷新
ByteArrayInputStream和ByteArrayOutputStream
节点流
ByteArrayOutputStream是向数组中写,ByteArrayInputStream从数组中读。都是在内存中进行操作,不与硬盘进行交互
- 因为都是内存操作流,不需要打开和关闭文件等操作
- 一般不会直接使用这个方法,会和其他的流结合使用(装饰器模式)
//节点流
ByteArrayOutputStream baos =new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeDouble(3.14);
oos.writeUTF("yhk");
byte[] bytes = baos.toByteArray();
//使用ByteArrayInputStream对bytes进行恢复
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
System.out.println(ois.read());
可以完成对象的深克隆
克隆的几个方法
- 调用Object的clone方法,默认是浅克隆,深克隆的话需要重写clone方法
- 序列化和反序列化
- ByteArrayInputStream和ByteArrayOutputStream完成深克隆
九、多线程
进程与线程的区别?
- 进程是操作系统中的一段程序,是一个正在执行中的程序实例。一个进程可以执行多个线程,
- 线程是进程中的一个执行单元,是进程中的一部分,它负责在进程中执行程序代码。每个线程都有自己的栈和程序计数器。
- 进程之间的内存独立的,线程的内存是共享的
GC:垃圾回收
作用:提高处理效率
JVM规范中规定:
- 堆内存、方法区是线程共享的
- 虚拟机栈、本地方法栈、程序计数器是每个线程私有的
并发:使用单核cpu的时候,同一时刻只能有一条指令运行
并行:使用多核cpu,同一时刻有多条指令执行
在cpu比较繁忙时,如果开启了多个线程,则只能为一个线程分配仅有的cpu资源,线程之间竞争cpu资源
在cpu资源比较充足时,一个进程内的多个线程可以分配到不同的cpu资源
线程的调度策略
分时调度模型:轮流使用cpu的执行权
抢占式调度模型:让优先级高的线程以较大的概率优先获得CPU的执行权(Java采用的)
在java中,实现线程有两种方式
实现线程的方法一:
- 编写一个类继承Thread
- 重写run方法
- new线程对象
- 调用线程对象的start方法来启动线程,start方法的任务就是启动一个新线程,分配一个新的栈空间。无论是run方法还是start方法,不结束main方法是不会继续进行的。
一个启动线程的例子
public class ThreadTest01 {
public static void main(String[] args) {
//创建线程对象
MyThread mt = new MyThread();
//调用start()方法,启动线程
//直接调用run()方法,不会启动新的线程
//如果是run()方法,就必须要等它执行结束后,才能接着执行后续的代码
mt.start();
for (int i =0; i<100; i++){
System.out.println("main-->" + i);
}
}
}
//自定义一个线程类
//java.lang.Threa本身就是一个线程
//MyThread继承Thread,因此它本身也是一个线程
class MyThread extends Thread{
@Override
public void run() {
for (int i =0; i<100; i++){
System.out.println("MyThread-->" + i);
}
}
}
实现线程的方法二
- 编写一个类实现java.lang.Runnable接口(可运行的接口)
- 实现接口中的run方法
- new线程对象
- 调用线程中的start方法启动线程
public class ThreadTest02 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnalbe());
t.start();
for (int i =0; i<100; i++){
System.out.println("main-->" + i);
}
}
}
class MyRunnalbe implements Runnable{
@Override
public void run() {
for (int i =0; i<100; i++){
System.out.println("MyThread-->" + i);
}
}
}
通过内部类方式实现线程启动
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i =0; i<100; i++){
System.out.println("Thread1-->" + i);
}
}
});
t.start();
//进一步简化
new Thread(new Runnable() {
@Override
public void run() {
for (int i =0; i<100; i++){
System.out.println("Thread2-->" + i);
}
}
}).start();
3个常用方法
//获取当前线程对象
Thread mainThread = Thread.currentThread();
//获取当前线程名字
System.out.println("获取当前线程的名字:"+mainThread.getName());
//修改线程名字
mainThread.setName("t1");
System.out.println("获取当前线程的名字:"+mainThread.getName());
线程生命周期
- NEW:新建状态
- RUNNALBE:可运行状态
- 就绪状态
- 运行状态
- BLOCKED:阻塞状态(遇到锁之后进入阻塞状态)
- WAITING:等待状态
- TIMED_WAITING:超时等待状态
- TERMINATED:终止状态
线程的sleep方法:static void sleep(long millis)
静态方法:没有返回值,参数是一个毫秒
作用:让当前线程进入休眠,放弃占有的CPU时间片,让其进入阻塞状态
当前线程:这个代码出现在哪个线程中,就是哪个线程
run方法在重写的时候,不能再方法声明位置通过throws抛出异常,因为子类不能抛出更多异常
sleep可以模拟每隔一段时间调用一次程序
怎么中断一个线程(t)的睡眠?
- t.interrupt()方法,利用异常机制,通过抛出异常终止t线程的睡眠
怎么让一个运行的线程(t)终止?
- t.stop(),从java2开始就不建议使用了,此方式会导致内存中的数据丢失
- 打标记,在线程类中添加一个标记值,当它为true时才会执行程序,需要终止时将其改为false
线程分为两类:用户线程和守护线程
JVM中,有一个隐藏的守护线程一直在守护,他是GC线程
守护线程特点:所有的用户线程结束后,守护线程自动退出/结束
如何将一个线程设置为守护线程?
-
MyThread myThread = new MyThread(); //在启动线程前,设置线程为守护线程 myThread.setDaemon(true);
jdk中提供的定时任务
java.util.Timer:定时器
java.utile.TimerTask:定时任务
定时器+定时任务可以帮助我们在程序中完成:没隔多久执行一次某段程序
public class ThreadTest06 {
public static void main(String[] args) throws Exception {
Timer timer = new Timer();
//指定定时任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date firstTime = sdf.parse("2024-1-27 10:22:00");
//timer.schedule(new LogTimmerTask(),firstTime,1000);
//匿名内部类实现
timer.schedule(new TimerTask() {
int count = 0;
@Override
public void run() {
Date now = new Date();
String strTime = sdf.format(now);
System.out.println(strTime+":"+count++);
}
},firstTime,1000);
for(int i=0;i<10;i++){
Thread.sleep(1000);
}
}
}
class LogTimmerTask extends TimerTask {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int count = 0;
@Override
public void run() {
Date now = new Date();
String strTime = sdf.format(now);
System.out.println(strTime+":"+count++);
}
}
t.join():实例方法,将t线程加入到当前线程,当前线程进入阻塞状态,直到t线程结束,当前线程阻塞解除
//如果设置时间内,线程执行完成了,就不会再继续阻塞了
t.join(1000*10)
优先级
JVM采用的是抢占式调度模型,优先级高的获得CPU时间片的概率高一些
线程可以设置优先级,默认为5,最大为10(Thread.MAX_PRIORITY)
MAX_PRIORITY=10
MIN_PRIORITY=1
NORM_PRIORITY=5
让位
从运行状态转化为就绪状。不会让其进入阻塞状态
静态方法 Thread.yield()
只能保证大方向的,大概率到了某个点让位一次
线程安全
什么情况下需要考虑?
- 多线程的并发环境下
- 有共享的数据
- 共享数据涉及修改操作
一般情况下局部变量不存在线程安全问题,基本数据类型不存在线程安全问题,引用数据类型存在
实例变量和静态变量可能存在线程安全问题,他们在堆中,堆是多线程共享的
线程同步:让线程排队执行。效率低,可以保证数据的安全问题
线程异步:不需要线程排队执行。可能存在安全问题。
同步代码块
synchronized (共享对象obj){
//需要同步的代码
}
//注意同步代码块的范围不能无故扩大,范围越小,效率越高
对象锁,类锁
在静态方法上添加synchronized后,线程会占用类锁,一个类只有一个类锁
死锁
使用的synchronized过多,导致多个线程相互等待资源
比如嵌套的synchronized
synchronized又被称为互斥锁
线程通信
三个方法,都是Object类的方法
- wait(),重载了3个,
- wait():调用此方法,线程进入“等待状态“,释放对象锁
- wait(毫秒):调用此方法,线程进入“超时等待状态“,释放对象锁
- wait(毫秒,纳秒):调用此方法,线程进入“超时等待状态“,释放对象锁
- notify():唤醒一个优先级最高的
- notifyAll():唤醒所有的线程
wait方法和notify相关方法不是通过线程对象去调用,而是通过共享对象去调用
//obj是多线程共享的对象
//当调用了obj.wait()之后,在obj对象上活跃的所有线程进入无期限等待。直到调用了该共享对象的obj.notify()方法进行唤醒
obj.wait()
只能在同步代码块或同步方法中使用,且必须是共享的锁对象
wait()和sleep的区别?
- wait是Object的实例方法,sleep是Thread的静态方法
- wait只能在同步代码块中使用,sleep随意
- wait方法执行会释放对象锁,sleep不会
- wait结束时机是notify,或到达指定时间,sleep只能到指定时间
单例模式
懒汉式:在第一次调用方法时才会初始化对象
饿汉式:构造方法初始化对象
懒汉式的单例模式有线程安全问题,可能会new两个对象。
public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
}
return singleton;
}
使用Lock来实现线程安全,Lock接口从jdk5引入,Lock接口下有一个可实现类:可重入锁(ReentrantLock)
要想使用ReentrantLock达到线程安全,假设要让tq,t2,t3线程同步,就需要让他们共享同一个lock
public static Singleton getSingleton(){
if(singleton == null){
try{
lock.lock();
if(singleton == null){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}finally {
lock.unlock();
}
}
return singleton;
}
实现线程的方法三
实现Callable接口,实现call方法
public class ThreadTest07 {
public static void main(String[] args) {
//创建未来任务对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//处理业务....
return 1;
}
});
//创建线程对象
Thread t = new Thread(task);
t.setName("t");
t.start();
try {
//获取未来任务线程的返回值
//阻塞当前线程,等待未来任务结束并返回值
//拿到返回值,当前线程的阻塞才会接触,继续执行
Integer i = task.get();
System.out.println(i);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
实现线程的方法四
使用线程池技术
线程池本质上是一个缓存:cache
一般都是服务器在启动的时候初始化线程池,创建多个线程对象
public class ThreadTest08 {
public static void main(String[] args) {
//创建一个线程池对象,有3个线程
ExecutorService executorService = Executors.newFixedThreadPool(3);
//将任务交给线程池
executorService.submit(new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
}
});
//关闭线程池
executorService.shutdown();
}
}
十、反射(reflect)
反射机制是jdk中的一套类库,可以帮助我们操作class文件
大量的java框架底层都是基于反射机制实现的
反射机制可以让程序更加灵活
反射机制最核心的几个类
- java.lang.Class:Class类型的实例代表硬盘上的某个class文件
- java.lang.reflect.Field:Field类型的实例代表类中的属性/字段
- java.lang.reflect.Constructor:Constructor类型的实例代表类中的构造方法
- java.lang.reflect.Method:Method类型的实例代表类中的方法
Class
获取Class的三种方法
//第一种方式
//全限定类型是带有包名的
//如果类不存在,在运行时会报异常:java.lang.ClassNotFoundException
//这个方法的执行会导致类的加载动作的产生
Class stringClass = Class.forName("java.lang.String");
Class cuserClass = Class.forName("reflectLearn.User");
String s1 = "动力节点";
//第二种方式
Class stringClass2 = s1.getClass();
//某种类型的字节码文件在内存中只有一份
//stringClass和stringClass2代表了同一种类型
System.out.println(stringClass2 == stringClass);//true
//第三种方式
Class intClass = int.class;
实例化
//通过userClass来实例化User类型的对象
//底层原理是:调用了User类的无参数构造方法完成对象的实例化
//要使用这个方法实例化对象的话,必须保证这个类中是存在无参数构造方法的。如果没有无参数构造方法,则出现异常java.lang.InstantionException
User user = (User)userClass.newInstance();
反射机制是灵活的,可以实现对象的动态创建
在修改时只修改配置文件
Field
如何给属性赋值
Vip vip = new Vip();
Class clazz = Class.forName("reflectLearn.Vip");
//获取对应的Field
Field ageFidld = clazz.getDeclaredField("age");
//调用方法打破封装
ageFidld.setAccessible(true);
//修改属性值
//给对象属性赋值三要素:给哪个对象 的 哪个属性 赋什么值
ageFidld.set(vip,30);
System.out.println(ageFidld.get(vip));
Constructor
//获取所有的构造方法
Constructor[] cons = clazz.getDeclaredConstructors();
通过反射机制来创建对象
Class clazz = Class.forName("reflectLearn.UserService");
Constructor defaultCon = clazz.getDeclaredConstructor();
Object obj = defaultCon.newInstance();
System.out.println(obj);
//获取三个参数的构造方法
Constructor threeArgsCon = clazz.getDeclaredConstructor(String.class, double.class, String.class);
//调用
Object obj1 = threeArgsCon.newInstance("asdasd",67.0,"wqea");
Method
Class clazz = Class.forName("reflectLearn.UserService");
// 获取所有的方法,包括私有的方法
Method[] methods = clazz.getDeclaredMethods();
for(Method method:methods){
//方法修饰符
System.out.println(Modifier.toString(method.getModifiers()));
// 方法返回值类型
System.out.println(method.getReturnType().getName());
//方法名
System.out.println(method.getName());
}
通过反射机制调用method
调用一个方法需要四要素:哪个对象,哪个方法,传什么参数,返回什么值
UserService userService = new UserService();
Class clazz = Class.forName("reflectLearn.UserService");
//获取login方法
Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);
//调用
Object retValue = loginMethod.invoke(userService, "admin", "123456");
System.out.println(retValue);
类加载器
类加载的过程
- 装载
- 类加载器负责将类的class文件读入内存(启动类加载器、平台类加载器)
- 链接
- 验证:确保加载类的信息符合JVM规范
- 准备:正式为静态变量在方法区中开辟空间
- 解析:将虚拟机常量池内的符号引用替换
- 初始化
- 静态变量赋值
获取class的第四种方法
//获取类加载器对象(获取的是系统类加载器)
ClassLoader syatemClassLoader = ClassLoader.getSystemClassLoader();
//加载类,但是这个加载过程只是将类加载过程中的前两步完成了,第三步的初始化没做
//当这个类真正的被第一次使用的时候会进行初始化
Class<?> aclass = syatemClassLoader.loadClass("reflectLearn.UserService");
虚拟机内部提供了三种类加载器(java9+)
- 启动类加载器(BootstrapClassLoader):负责加载核心类库
- 平台类加载器(PlatformClassLoader):负责加载扩展类库
- 应用类加载器(AppClassLoader):负责加载classpath
- 自定义加载器
可以通过getParent()方法一级一级获取
//通过自定义的类获取类加载器:应用类加载器
ClassLoader appClassLoader = ReflectTest06.class.getClassLoader();
//获取应用类加载器
ClassLoader appClassLoader2 = ClassLoader.getSystemClassLoader();
//获取应用类加载器
ClassLoader appClassLoader3 = Thread.currentThread().getContextClassLoader();
//获取平台类加载器
System.out.println("平台类加载器:"+appClassLoader.getParent());
//获取启动类加载器
//启动类加载器负责加载的是JDK核心类库,这个类加载器的名字看不到。直接输出的结果是null
System.out.println("启动类加载器:"+appClassLoader.getParent().getParent());
双亲委派机制
- 某个类加载器接收到加载类的任务时,通常委托给“父 类加载”完成加载
- 最“父 类加载器”无法加载时,一级一级向下委托加载任务
- 作用:
- 保护程序的安全
- 防止类加载重复
泛型
//获取类
Class<User> userClass = User.class;
//获取父类泛型
Type genericSuperclass = userClass.getGenericSuperclass();
//获取接口上的泛型
Type[] genericInterfaces = userClass.getGenericInterfaces();
//获取属性上的泛型,需要先获取到属性
Field mapField = userClass.getDeclaredField("");
Type genericType = mapField.getGenericType();
//获取方法上的泛型,需要先获取到方法
Method mMethod = userClass.getDeclaredMethod("m", List.class);
Type[] genericParameterTypes = mMethod.getGenericParameterTypes();
//获取方法返回值上的泛型
Type genericReturnType = mMethod.getGenericReturnType();
//获取构造方法参数的泛型
Constructor<User> con = userClass.getDeclaredConstructor(Map.class);
Type[] genericParameterTypes1 = con.getGenericParameterTypes();
//如果父类使用了泛型
if(genericSuperclass instanceof ParameterizedType){
//转型为参数化类型
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
//获取泛型数组
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
//遍历
for (Type a : actualTypeArguments) {
System.out.println(a.getTypeName());
}
}
十一、注解
注解是jdk1.5引入的
注解可以标注在类,属性,方法上扽
注解可以做到在不改变代码逻辑的前提下在代码中嵌入补充信息
注解与注释:
- 注释是给程序员看的,编译器编译时会忽略注释
- 注解是给编译器看的,程序根据有没有这个注解来决定不同的处理方法
框架=反射+注解+设计模式
Java预置注解
java自带的注解
- @Deprecated:用来标记过时的元素,在编译阶段遇到会发出提示警告,告诉开发者调用了一个已经过时的方法或类
- since属性值表示从哪个版本开始过时
- forRemoval属性为trye表示已移除
- @Override:标注实例方法,被标注的方法必须是重写父类的方法。如果没有重写则报错
- @SuppressWarnings:抑制警告的注解
- @SuppressWarnings(“rawtypes”):抑制未使用泛型的警告
- @SuppressWarnings(“resourse”):抑制未关闭资源的警告
- @SuppressWarnings(“deprecation”):抑制使用了已过时资源的警告
- @SuppressWarnings(“all”):抑制所有警告
- @FunctionalInterface:“函数式接口”的注解,jdk1.8引入,专门用来标注接口,该接口有且只能有1个抽象方法,允许有多个默认方法和静态方法
自定义注解
使用@interface来定义注解
新建一个Annotation
注解也可以定义属性,但是属性名后面必须加括号
所有自定义的注解父类是Annotation
如果注解中有属性,那么使用时必须给属性赋值,否则报错。除非有默认值。
如果属性只有一个,并且属性名为“value”,则属性名可以不写
如果属性为数组,使用数组时数组中只有一个值,则“{}”可以省略
属性的类型只能是
- byte,short,int,long,float,double,boolean,char
- String,Class,枚举,注解
- 以及上述的一维数组
public @interface MyAnnotation{
//在注解中,这不是方法,是属性
String dirver() default "123456";//用default关键字来指定默认值
Class clazz();
Season season();
MyAnnotation1 myAnnotation();
String[] names();
}
元注解
用来标注注解的注解
常用的
- @Retention:设置注解的保持性
- SOURCE:注解保留在源码中
- CLASS:注解保留在字节码中,不能被反射
- RUNTIME:注解保留在运行时阶段,并且运行时可以被反射
- @Target:设置注解可以出现的位置
- @Documented:可以使用javac生成帮助文档
- @Interited:如果该注解标注了一个父类,则子类也被标注了
- @Repeatable:可重复出现,jdk8出现
十二、网络编程
指利用计网实现程序之间通信的一种编程方式。在网络编程中,程序需要通过网络协议(如TCP/IP)来进行通信,以实现不同计算机之间的数据传输和共享。
三要素
ip地址
用于唯一标识网络中的每一台计算机。
ipv4格式:xxx.xxx.xxx.xxx 每个xxx表示一个8位的二进制数(0-255),组合起来可表示2的32次方个不同的ip地址
ipv6(十六进制,128位):xxxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
ip地址被分为网络地址和主机地址两部分,前三个字节表示网络
192.168.0.0-192.168.255.255为私有地址,属于非注册地址,转为组织机构内部使用
域名:是ip地址的一种符号化方案,用来代替数字型的ip地址
DNS:是进行域名解析的服务器。机器识别ip地址,人记忆域名,DNS负责进行转换。
端口号port
定位计算机上的某个进程
- 在计算机中,不同的应用程序是通过端口号区分的
- 端口号使用两个字节(无符号)表示的,取值范围是0~65535,可以分为3类
- 公认端口:0~1023,被预先定义的服务通信占用(HTTP:80 FTP:21 Telnet:23)
- 注册端口:1024~49151.分配给用户进程或应用程序(Tomcat:8080 MySQL:3306 Oracle:1521),写程序一般用这里的
- 私有(动态)端口:49152~65535
- to南广场情况下,服务器程序使用固定的端口号来监听客户端的请求,而客户端使用随机端口连接服务器
- 必须同时指定ip地址和端口才能够进行通信
通信协议:
通过ip和port定位后,保证数据可靠高效的传输
位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则
这些连接和通信的规则被称为网络通信歇息,对数据的传输格式、传输速率、传输步骤做了统一规定
常见有TCP,UDP,HTTP,FTP
TCP:传输控制协议:可靠的面向连接的
UDP:用户数据报协议:不可靠的面向无连接的。比如视频
OSI参考模型(OSI七层协议)
TCP/IP模型:传输控制协议/因特网互联协议
是一个开放的网络协议簇,定义了设备怎样连入互联网
InetAddress类
该类封装了计算机的IP地址和DNS,他包括一个主机名和IP地址,
常用静态方法
- getLocalHost():得到本机的InetAddress
- getByName(String host):传入主机名称得到对应的InetAddress对象
常用实例方法
- getHostAddress():获取ip地址
- getHostName():获取主机名
InetAddress ia = InetAddress.getLocalHost();
String ip = ia.getHostAddress();
System.out.println("本机ip:"+ip);
String name = ia.getHostName();
System.out.println("本机主机名:"+name);
InetAddress ia2 = InetAddress.getByName("www.baidu.com");
System.out.println(ia2.getHostAddress());
System.out.println(ia2.getHostName());
什么是URL?
统一资源定位符,互联网上的每一个文件都有唯一的url,
由四部分组成:协议、主机域名、端口、资源名称
//创建url对象
URL url =new URL("https://www.bilibili.com/video/BV1p7421N7XT?p=144&spm_id_from=pageDriver&vd_source=08b57a28f8f2dae01a8dea63d78a6ccd");
String protocol = url.getProtocol();
System.out.println("协议:"+protocol);
String path = url.getPath();
System.out.println("资源路径:"+path);
int port = url.getPort();//端口
String host = url.getHost();//主机地址
String querty = url.getQuery();//数据
String file = url.getFile();//获取资源
//创建一个流读取网页信息
InputStream is = url.openStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
我们开发的网络应用程序位于应用层,TCP和UDP属于传输层协议
基于TCP协议的编程
- 使用tcp协议,需先建立TCP连接,形成数据传输通道
- 传输前,采用“三次握手”方式,属于点对点通信,是面向连接的,效率低
- 仅支持
三次握手和四次挥手的区别?
三次握手过程
- 客户端发送SYN(同步)数据包
- 服务器收到SYN数据包后,发送SYN-ACK(同步确认)数据包
- 客户端收到SYN-ACK数据包后,发送ACK(确认)数据包
基于UDP协议的编程
十三、Lambda表达式
函数式编程思想
函数式接口
TreeSet<User> sets = new TreeSet<>((o1,o2) -> o1.getAge()-o2.getAge());
匿名内部类和Lambda表达式的区别?
- 所需类别不同:匿名内部类可以是抽象类,也可以是接口。Lambda表达类只能是接口
- 使用限制不同:匿名内部类不做限制。Lambda表达式使用的接口只能有一个抽象方法
- 实现原理不同:匿名内部类编译后会生成一个单独的.class文件。Lambda表达式不会
Lambda表达式的使用
语法
(形参列表)->{方法体;}
public class LambdaTest01 {
public static void main(String[] args) {
OneParameterNoReturn oneParameterNoReturn = (Integer value)->{
System.out.println("值是:"+value);
};
oneParameterNoReturn.test(123);
}
}
@FunctionalInterface
interface OneParameterNoReturn{
void test(Integer value);
}
Lambda表达式的方法引用
Lambda在集合中的使用
十四、Stream API
jdk8引入,可以更方便地操作集合,允许在不改变原始数据地情况下对集合进行操作,类似于使用SQL执行的数据库查询
Stream和Collection的区别
Collection是静态的内存数据结构,强调的是数据
Stream API是跟集合相关的计算操作,强调的是计算
Collection面向的是内存,Stream API面向的是CPU
操作步骤
- 创建Stream:通过数据源来获取一个Stream对象
- 中间操作:对数据源的数据进行处理,该操作会返回一个Stream对象,可以进行链式操作
- 终止操作:真正执行中间操作,并返回一个计算完毕的结果
特点
- Stream不会存储元素,只能对元素进行计算
- Stream不会改变数据对象,会返回一个持有结果的新Stream
- Stream上的擦欧总属于延迟执行,只有等到用户真正需要结果的时候才会执行
- Stream一旦执行了终止操作,就不能再调用其他的中间操作或终止操作
创建方式
- 使用Collection
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//通过Stream对象可以对集合中的元素进行计算
Stream<Integer> stream = list.stream();
System.out.println(stream);
//这是一个并行流,底层自动启动多线程
//在计算的时候会自动启动多线程去运算
//当数据量很大时可以使用
Stream<Integer> stream2 = list.parallelStream();
- 使用Arrays
String[] names = {"zhangsan","lisi","wangwu"};
Stream<String> stream3 = Arrays.stream(names);
System.out.println(stream3);
- Stream本身的of方法
Stream<String> stream4 = Stream.of("zhangsan","lisi","wangwu");
System.out.println(stream4);
顺序流和并行流
//创建一个顺序流
Stream<String> stream5 = Stream.of("zhangsan","lisi","wangwu");
//将Stream转化为并行流
Stream<String> stream6 = stream5.parallel();
//直接创建并行流Stream对象
Stream<Integer> stream7 = list.parallelStream();
中间操作
- 筛选(filter)
//筛选出年龄大于20的学生对象
StudentService.getStudents().stream().filter(new Predicate<Student>() {
@Override
public boolean test(Student student) {
return student.getAge() > 20;
}
}).forEach(new Consumer<Student>() {
@Override
public void accept(Student student) {
System.out.println(student);
}
});
//Lambda表达式
StudentService.getStudents().stream().filter(student -> student.getAge() > 20).forEach(System.out::println);
- 映射(map)
Stream.of("abc","def").map(new Function<String,String>() {
@Override
public String apply(String s) {
return s.toUpperCase();
}
}).forEach(System.out::println);
//Lambda表达式
Stream.of("abc","def").map(s -> s.toUpperCase()).forEach(System.out::println);
//筛选出集合中性别为男的学生名字
StudentService.getStudents().stream().filter(student -> student.getSex().equals("男")).map(Student::getName).forEach(System.out::println);
Stream<List<Integer>> stream1 = Stream.of(list1, list2);
stream1.flatMap(new Function<List<Integer>, Stream<?>>() {
@Override
public Stream<?> apply(List<Integer> integers) {
return integers.stream();
}
}).forEach(System.out::println);
//Lambda表达式
stream1.flatMap(List::stream).forEach(System.out::println);
- 去重(distinct)
//去除重复元素
Stream.of(1,1,1,1,1,1,2).distinct().forEach(System.out::println);
//去除年龄相同的学生(除虫后输出学生年龄)
StudentService.getStudents().stream().map(student -> student.getAge()).distinct().forEach(System.out::println);
- 排序(sort)
//排序
Stream.of(2,4,3,5,1,3).sorted().forEach(System.out::println);//默认是升序
//按照学生年龄执行升序(排序后输出学生对象)
StudentService.getStudents().stream().sorted().forEach(System.out::println);
//按照学生年龄执行升序(排序后输出学生年龄)
StudentService.getStudents().stream().sorted().map(student -> student.getAge()).forEach(System.out::println);
- 合并(concat)
//合并,将两个Stream合并成一个Stream
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<Integer> integerStream1 = Stream.of(4, 5, 6);
Stream.concat(integerStream, integerStream1).forEach(System.out::println);
- 判断和跳过
//跳过skip
//截断limit
Stream.of(1,2,3,4,5,67,8,9).skip(3).limit(3).forEach(System.out::println);
终止操作
终止操作执行完后会返回计算结果,且该Stream失效
-
遍历(foreach)
-
匹配(match)
-
归约(reduce)
将流中所有的数据按照指定的规则计算出一个结果
//数字求和
System.out.println(Stream.of(1, 2, 3, 4).reduce(new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer + integer2;
}
}));
System.out.println(Stream.of(1,2,3,4).reduce((x,y) ->x+y).get());
System.out.println(Stream.of(1,2,3,4).reduce((x, y) -> Math.addExact(x,y)).get());
System.out.println(Stream.of(1, 2, 3, 4).reduce(Integer::sum));
count()
max()
- 收集(collect)
//获取年龄最大的学生
System.out.println(StudentService.getStudents().stream().max((s1,s2)->s1.getAge()-s2.getAge()).get());
//收集为List集合
List<String> list = Stream.of("zhangsan","lisi").collect(Collectors.toList());
System.out.println(list);
List<String> list2 = Stream.of("zhangsan","lisi").collect(Collectors.toCollection(LinkedList::new));
归集
统计
//收集为List集合
List<String> list = Stream.of("zhangsan","lisi").collect(Collectors.toList());
//也可以收集为Set或map
//使用toCollection可以指定集合的类型
List<String> list2 = Stream.of("zhangsan","lisi").collect(Collectors.toCollection(LinkedList::new));
十五、新特性
new Consumer() {
@Override
public void accept(Student student) {
System.out.println(student);
}
});
//Lambda表达式
StudentService.getStudents().stream().filter(student -> student.getAge() > 20).forEach(System.out::println);
2. 映射(map)
```java
Stream.of("abc","def").map(new Function<String,String>() {
@Override
public String apply(String s) {
return s.toUpperCase();
}
}).forEach(System.out::println);
//Lambda表达式
Stream.of("abc","def").map(s -> s.toUpperCase()).forEach(System.out::println);
//筛选出集合中性别为男的学生名字
StudentService.getStudents().stream().filter(student -> student.getSex().equals("男")).map(Student::getName).forEach(System.out::println);
Stream<List<Integer>> stream1 = Stream.of(list1, list2);
stream1.flatMap(new Function<List<Integer>, Stream<?>>() {
@Override
public Stream<?> apply(List<Integer> integers) {
return integers.stream();
}
}).forEach(System.out::println);
//Lambda表达式
stream1.flatMap(List::stream).forEach(System.out::println);
- 去重(distinct)
//去除重复元素
Stream.of(1,1,1,1,1,1,2).distinct().forEach(System.out::println);
//去除年龄相同的学生(除虫后输出学生年龄)
StudentService.getStudents().stream().map(student -> student.getAge()).distinct().forEach(System.out::println);
- 排序(sort)
//排序
Stream.of(2,4,3,5,1,3).sorted().forEach(System.out::println);//默认是升序
//按照学生年龄执行升序(排序后输出学生对象)
StudentService.getStudents().stream().sorted().forEach(System.out::println);
//按照学生年龄执行升序(排序后输出学生年龄)
StudentService.getStudents().stream().sorted().map(student -> student.getAge()).forEach(System.out::println);
- 合并(concat)
//合并,将两个Stream合并成一个Stream
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<Integer> integerStream1 = Stream.of(4, 5, 6);
Stream.concat(integerStream, integerStream1).forEach(System.out::println);
- 判断和跳过
//跳过skip
//截断limit
Stream.of(1,2,3,4,5,67,8,9).skip(3).limit(3).forEach(System.out::println);
终止操作
终止操作执行完后会返回计算结果,且该Stream失效
-
遍历(foreach)
-
匹配(match)
-
归约(reduce)
将流中所有的数据按照指定的规则计算出一个结果
//数字求和
System.out.println(Stream.of(1, 2, 3, 4).reduce(new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer + integer2;
}
}));
System.out.println(Stream.of(1,2,3,4).reduce((x,y) ->x+y).get());
System.out.println(Stream.of(1,2,3,4).reduce((x, y) -> Math.addExact(x,y)).get());
System.out.println(Stream.of(1, 2, 3, 4).reduce(Integer::sum));
count()
max()
- 收集(collect)
//获取年龄最大的学生
System.out.println(StudentService.getStudents().stream().max((s1,s2)->s1.getAge()-s2.getAge()).get());
//收集为List集合
List<String> list = Stream.of("zhangsan","lisi").collect(Collectors.toList());
System.out.println(list);
List<String> list2 = Stream.of("zhangsan","lisi").collect(Collectors.toCollection(LinkedList::new));
归集
统计
//收集为List集合
List<String> list = Stream.of("zhangsan","lisi").collect(Collectors.toList());
//也可以收集为Set或map
//使用toCollection可以指定集合的类型
List<String> list2 = Stream.of("zhangsan","lisi").collect(Collectors.toCollection(LinkedList::new));