1、Java概述
1.1 Java语言特点
- 简单易用
- 安全可靠
- 跨平台(JVM实现)
- 面向对象
- 支持多线程(内置多线程控制)
1.2 Java的运行机制?以及JDK、JRE、JVM的关系?
- 运行机制:Java源文件通过Javac工具进行编译,生成 .class 的字节码文件,然后Java虚拟机(JVM)将字节码文件进行解释执行,并将结果显示出来。
- JDK:Java语言的软件开发核心工具包。包括Java编译器、Java运行环境(JRE)、Java文档生成工具、Java打包工具等等。
- JRE:Java运行环境,包括JVM、基础类库等。
- JVM:Java核心虚拟机,通过Java虚拟机,Java程序可以在不同的操作系统上运行,实现跨平台。
1.3 Java与C++的区别
- Java是存粹的面向对象语言,所有的对象都继承自Object类,C++兼容C,不但支持面向对象,还支持面向过程。
- Java通过虚拟机实现跨平台的特性,C++依赖于特定的平台。
- Java没有指针,它的引用可以理解为安全指针,C++具有与C一样的指针。
- Java支持自动回收,C++需要手动回收。
- Java不支持多重继承,只能通过实现多个接口来达到相同的目的,C++支持多重继承。
1.4 Java是解释型语言还是编译性语言?
- 编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。C、C++
- 解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束。js、python、php
总结:Java会先通过Javac编译成字节码文件,再通过JVM将字节码文件进行解释执行,即解释运行和编译运行配合使用,称为混合型或半编译型。
2、Java编程基础
2.1 数据类型
- 基本数据类型(8个)
- 整数类型
- byte:8位 1个字节 -2^7——2^7-1
- short:16位 2个字节 -2^15——2^15-1
- int:32位 4个字节 -2^31——2^31-1
- long:64位 8个字节 -2^63——2^63-1 long类型所有赋值后面都要加上L或l
- 浮点类型
- float:32位 4个字节 float类型所有赋值后面都要加上F或f
- double:64位 8个字节 double比float更精确
- 字符类型
- char:存储一个单一字符,一个字符占用2个字节,用 单引号 引起来
- 布尔类型
- boolean:true和false,不参与类型转换。
- 引用数据类型:包括基础类、接口、数组、枚举等
- 基本数据类型自动转换(低精度->高精度):
2.2 switch语句注意
- switch条件语句的控制表达式结果类型只能是byte、short、、char、int、枚举、String类型。
- 当有多个case目标值会执行同样的语句时,可以多个case并列书写,只写一次执行语句。
2.3 数组初始化
* 初始默认值为0
- 类型[] 数组名 = new 类型[长度]
- 类型[] 数组名 = new 类型[]{元素0、元素1...}
- 类型[] 数组名 = {元素0、元素1...}
2.4 冒泡排序
- 原理:不断的比较相邻两个元素的大小,较大元素向后排(交换两个位置),直到最后交换完为止。
- 核心逻辑
// 两层循环:外层为比较轮数,内层为第i轮比较的两个数,并完成交换,较大排在后
int[] nums = {6, 8, 9, 4, 2};
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < nums.length - i; j++) {
if (nums[j] > nums[j + 1]) {
// 交换位置
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
3、Java面向对象(上)
3.1 面向对象特点
* Java中一切皆为对象
- 封装:面向对象的核心思想,将对象的属性和行为封装起来,不需要让外界知道具体的实现细节。有效避免类被外界随意访问而产生的问题。
- 继承:描述类与类之间的关系,通过继承可以在无需重新编写原有类的情况下,对原有类的功能进行扩展。
- 多态:当一个类中定义的属性和功能被其他类继承后,当把子类对象直接赋值给父类引用变量时,相同引用类型调用同一个方法所呈现出的多种不同行为特性。
3.2 修饰符访问级别
访问范围 |
private |
default |
protected |
public |
同一类中 |
√ |
√ |
√ |
√ |
同一包中 |
√ |
√ |
√ |
|
子类中 |
√ |
√ |
||
全局范围 |
√ |
3.3 方法重载与方法重写
- 方法重载:在同一个类中,方法名相同,参数列表不同,与返回值类型无关。例:
int add(int x,int y)、int add(int x,int y,int z)、double add(int x,int y)
- 父类方法重写:需要和父类方法具有相同的方法名、参数列表、返回值类型。重写可以在某种意义上理解为覆盖。
3.4 构造方法需注意
修饰符 方法名(与类名一致) (无参或有参) { //方法体 }
- 方法名与类名相同
- 方法名前面没有返回值类型声明
- 方法中不能使用return返回一个值,可以单独return结束
- new实例化对象时,系统会自动调用该类的默认无参构造
- 该类若定义了构造方法,系统将不再调用默认的无参构造
3.5 this关键字
- 常见用法:
- 通过this关键字调用成员变量,解决与局部变量名称冲突问题。
- 通过this关键字调用成员方法。
- 通过this关键字调用构造方法。
- this关键字调用构造方法时需注意:
- 只能在构造方法中使用this调用其他的构造方法,不能在成员方法中使用this调用构造。
- 在构造方法中,使用this调用其他构造方法的语句必须是该方法的第一条执行语句,并且只能出现一次。
- 不能在一个类的两个构造方法中使用this互相调用。
3.6 static关键字
- static关键字常见用法
- 静态变量:static关键字修饰的成员变量,它不能用于修饰局部变量。静态变量属于类本身,不是类的具体实例。
- 静态方法:static关键字修饰的成员方法,在静态方法中,只能访问static修饰的成员变量或方法。
- 静态变量和静态方法都属于类本身的属性,它们不归属于类的具体实例,所以在类没有创建实例的时候也可以进行调用。类名.静态成员(静态方法)
- 静态代码块:static关键字修饰的一段代码块。static{ // 代码块 }
- 静态内部类:static修饰内部类的类名。static class 类名{}静态内部类是一个嵌套类,但它不是非静态嵌套类的一个实例。可以直接通过类名来访问静态内部类,而不需要创建外部类的实例。
- 执行顺序:静态代码块 > 构造代码块 > 构造方法
- static关键字需注意
- 静态变量和静态代码块在类的第一次使用时才会被加载,并且只会加载一次,即使类被多次加载。
- 静态变量、静态方法、静态内部类通常在什么情况下使用?
- 静态变量:通常用于存储不需要随着对象状态改变而改变的值,配合final使用。例如配置参数或全局常量
- 静态方法:通常用于执行不依赖于对象状态的操作,例如工具方法或辅助函数。
- 静态内部类:通常用于定义与外部类相关但不依赖于外部类实例的逻辑或组件。
- 为什么静态变量是线程安全的?
- 静态变量是类级别的变量,它们被所有实例共享。在多线程环境中,当多个线程访问同一个静态变量时,它们实际上访问的是同一个内存地址中的变量,因此不会出现并发问题。
- Java虚拟机提供了对内存模型的保证,确保对共享变量的访问是原子的,从而保证了静态变量的线程安全性。
4、Java面向对象(下)
4.1 继承
[修饰符] class 子类名 extends 父类名 {}
- 子类在继承父类的时候,会自动拥有父类所有的公共成员属性。私有属性不能在子类中直接访问,子类需要通过父类的公共方法去访问父类的私有属性。
- 关于继承需注意:
- Java类只支持单继承,不允许多重继承。(特例:接口与接口之间可以多继承)
- 多个类可以继承同一个父类。
- 可以多层继承。A->B->C
4.2 super关键字
- super关键字使用
- 调用父类的成员变量和成员方法。super.成员变量 super.成员方法()
- 调用父类的构造方法时,必须位于子类的构造方法的第一行,并且只能出现一次。super()
- 使用super时需注意:
- 如果父类有多个构造方法,则需要在子类中使用super显式地指定要调用的父类构造方法,完成父类的初始化,否则编译不通过,默认情况下为隐式调用(当子类被实例化时),父类会被自动初始化。
- 使用super调用构造方法时,必须放在构造方法的第一行。
- super()和this()都只能放在构造方法的第一行,因此两个方法不能共存。
4.3 super与this的比较
区别点 |
this |
super |
访问属性 |
访问本类的属性,如果本类中没有,则从父类中继续查找 |
访问父类属性 |
调用方法 |
访问本类中的方法,如果本类中没有,则从父类中继续查找 |
访问父类中的方法 |
调用构造方法 |
调用本类的构造方法,必须放在构造方法的第一行 |
调用父类的构造方法,必须放在子类构造方法的第一行 |
特殊 |
表示当前对象 |
子类中访问父类对象 |
4.4 Object类常用的方法
方法 |
含义 |
boolean equals(obj) |
比较两个对象是否相等,默认情况下比较的是两个对象在内存中的位置 |
int hashCode() |
返回该对象的哈希值 |
String toString() |
对象转化为字符串返回 |
void finalize() |
垃圾回收器,此方法清理没有被任何引用变量所引用对象的资源 |
4.5 final关键字
- 特性
- final修饰的类不能被继承。
- final修饰的方法不能被子类重写。
- final修饰的变量是常量,只能赋值一次。
- final修饰方法的参数,表示这个参数是只读,不能在方法内部修改它。
- 使用final关键字可以帮助提高代码的可读性和可维护性,因为它明确地指示了某个元素是固定的、不可变的。但是,过度使用final关键字也可能导致代码的灵活性降低。
4.6 接口需要注意
- interface修饰的类,一个类可以实现多个接口
- 接口与接口之间可以多继承
- 接口中可以包含抽象方法、默认方法(default修饰,有方法体)、静态方法(static修饰,有方法体)
- instanceof:判断一个对象是否为某个类(或接口)的实例或子类实例。
if(student instanceof school){ }
- 抽象类与接口的区别
- 实现方式:抽象类需要通过继承来使用,接口是特殊的抽象类,需要实现接口来使用。
- 方法类型:抽象类中的方法可以抽象或非抽象,而接口中都是抽象方法(1.8以后引入了默认方法和静态方法)
- 成员变量:抽象类中可以有普通成员变量、静态成员变量,而接口中只能存在常量。
- 构造方法:抽象类中可以存在构造方法,而接口中不能存在构造方法。
4.7 异常分类
- 定义:程序运行时磁盘不足、网络中断、类不被加载等非正常情况。
- 异常类都继承自java.lang.Throwable类,继承体系:
- Error类为错误类,Java运行时产生的系统内部错误或资源耗尽的错误,仅靠修改程序本身无法恢复。
- Exception为异常类,表示程序本身可以处理的错误。常用的两个方法:
- String getMessage() 返回此异常的详细信息
- void printStackTrace() 将此异常及其追踪输出至标准错误流
- 异常类型
- 编译时异常:除了RuntimeException及其子类都属于编译时异常。
- 运行时异常:RuntimeException及其子类,Java虚拟机(JVM)自动捕获。
- ArithmeticException 算术异常
- IndexOutofBoundsException 下标越界
- ClassCastException 类型转换异常
- NullPointerException 空指针异常
- NumberFormatException 数字格式化异常
4.8 异常处理
- 异常捕获:
try { // 可能异常语句 }catch(Exception e){ // 异常处理 }finally{ // 是否异常都会执行 }
- 当执行了System.exit(0)程序退出当前虚拟机,finally不再执行。
- throws关键字抛出异常:从当前方法中抛出,后续调用者使用时进行异常处理。
- 例:
public int divicle(int x,int y) throws Exception { }
- throw关键字与throws的区别
- throw使用:throw new Exception 类或子类构造方法
- throw用于方法体内,并且抛出一个异常类对象,而throws用在方法声明中,用来指明方法可能抛出多个异常。
4.9 垃圾回收机制
- Java虚拟机会自动回收垃圾对象所占用的内存空间。[Java GC 垃圾回收机制]
- 一个对象在堆内存运行时的状态:可用状态、可恢复状态、不可用状态。
- 回收流程图
- 一个对象在彻底失去引用后会暂时的保留在内存中,堆积到一定程度时,虚拟机会启动垃圾回收器,将垃圾对象从内存中释放。
- 当一个对象在内存中被释放时,它的finalize()方法会被自动调用进行资源清理。
- 只有当程序认为需要更多的额外内存时,垃圾回收器才会自动进行垃圾回收。
- 主动强制系统垃圾回收
- 调用System类的gc静态方法:System.gc()
- 调用Runtime对象的gc实例方法:Runtime.getRuntime().gc()
4.10 多态特性
- 不同类的对象在调用同一个方法时所呈现出的多种行为。
- 通过多态消除了类之间的耦合关系,大大提高了程序的可扩展性和可维护性。
- 多态是由类的继承、方法重写、父类引用指向子类对象体现的。
5、Java常用类
5.1 String类
- 构造方法中不常用的一个:
new String(char[] array)
- String重写了Object类的equals()方法,用于比较值是否相等。
- String中的"=="与equals()的区别:
- "=="用于比较两个字符串的内存地址是否相同。对于基本数据类型,比较的是值。
- equals()用于比较两个字符串的值是否相等。默认下比较的是两个对象的内存地址,重写后比较的是值。
5.2 StringBuffer类
- StringBuffer类似一个字符容器,当在其中添加或删除字符时,所操作的都是这个字符容器,不会产生新的StringBuffer对象。
- 常用方法:
方法 |
含义 |
append(char c) |
末尾追加 |
insert(int index,String s) |
指定位置加 |
deleteCharAt(int index) |
指定位置删 |
delete(int begin,int end) |
指定范围删 |
replace(int begin,int end,String s) |
指定范围替换 |
toString() |
转换为String类型 |
reverse() |
反转 |
- String与StringBuffer的区别
- String类定义的字符串是常量,一旦被创建后,内容和长度不可改变;StringBuffer是字符容器,内容与长度可随时修改。
- String重写了Object类的equals方法,StringBuffer没有重写。
- String对象可用"+"进行连接,而StringBuffer不能。
- StringBuilder与StringBuffer用法与功能基本相同。
5.3 String、StringBuilder、StringBuffe的区别
- 性能方面:StringBuilder > StringBuffer > String
- 线程安全:StringBuffer、String是线程安全的;StringBuilder是线程不安全的。
- StringBuffer线程安全原因:内部方法通过使用synchroized关键字(同步机制)处理,保证线程安全。synchroized关键字可以确保在同一时刻只有一个线程可以访问。
- StringBuilder线程不安全:内部方法没有进行同步处理,多线程环境下对同一个StringBuilder对象进行操作,可能出现数据不一致问题。
- 功能方面:String可以通过compare to方法进行比较,其他两者不可以。
- 可变性:String创建后其值和长度不可改变,其他两者可以改变。
- 应用场景
- String用于少量字符串操作的情况。
- StringBuilder适用于单线程、字符缓冲区大量操作下。
- StringBuffer适用于多线程(线程安全)、字符缓冲区大量操作下。
5.4 键盘输入操作
Scanner sc = new Scanner(System.in);int i = sc.nextInt();
5.5 自动装箱和自动拆箱
- 自动装箱:将基本数据类型的变量赋值给对应的包装类变量。
- 自动拆箱:将包装类对象直接赋值给一个对应的基本数据类型变量。
- 包装类一般都重写了equals()方法,用于比较两个对象的值是否相等。
6、集合
6.1 集合分类和结构图
- 单列集合:根接口Collection,有List、Set两个子接口,结构图:
- List集合特点:元素有序且重复,允许插入多个null元素。
- Set集合特点:元素无序且不可重复,没有索引,只允许插入一个null元素。
- 双列集合:根接口Map,主要的实现类有HsahMap,TreeMap,每个元素都包含 key -> value映射,结构图:
6.2 ArrayList集合实现类
- 特点:有序可重复,允许插入多个null值。
- 底层:数据结构是长度可变的数组,是线程不安全的,内存地址是连续的,便于元素查找。
- 自动扩容机制:当存入元素超过数组的长度,ArrayList会进行自动扩容,按照1.5倍扩容,会在内存中分配一个更大的数组来存储这些元素。
- 由于底层结构是数组,内存地址中是连续的,可通过索引直接查找与定位元素,因此它的查询和修改效率较高。在增加和删除元素时,需要创建新数组进行扩容和移动元素位置,因此在增加和删除元素的效率较低。
6.3 LinkedList集合实现类
- 特点:有序可重复,允许插入多个null值。
- 底层:数据结构是双向循环链表,是线程不安全的,它的内存地址是不连续的。
- 内部包含有两个Node类型的first(结点的前驱指针)和last(结点的后继指针)属性指针,链表中的每一个元素都使用引用的方式来记住它的前一个元素和后一个元素,从而将所有元素都连接起来。链表
- 在进行增加和删除元素操作时,只需要修改前驱指针和后继指针即可,不需要创建新的内存和移动元素,因此LinkenList的插入和删除效率较高。
- 相对于ArrayList,LinkedList的查询效率较低。LinkedList在进行查找元素时,内存是不连续的,需要通过移动前驱和后继指针在链表中挨个查找,效率较低。
- 存储前驱和后继指针需要额外的内存空间,空间利用率较低。
6.4 List迭代器遍历
- 核心逻辑
List<String> list = new ArrayList<>();
Iterator it = list.iterator();
while(it.hasNext()){
String s = it.next();
// 删除集合中的某个元素,迭代器本身移除,此时不会产生异常
it.remove();
}
6.5 HashSet集合实现类
- 特点:元素无序且不可重复,只允许添加一个null值。
- 底层:底层是哈希表(Hash Table)实现的。
- 特性
- 唯一性:由哈希表的键唯一保证元素的唯一性,元素不可重复。
- 高效性:根据对象的哈希值来确定对象在集合中的存储位置,具有高效的存取和查找的性能。
- 线程不安全:如果多个线程同时修改HashSet,可能会导致数据不一致的并发问题,哈希表本就是线程不安全的。
- 若在多线程环境下使用Hashset,需要加同步锁(Synchronized),否则数据可能不一致。
- HashSet添加数据的原理流程
6.6 哈希表线程不安全原因
哈希表是通过散列算法将键映射到数组索引位置,从而存储和查找键值对,多线程情况下,散列算法可能会导致冲突,从而使多个线程操作同一个索引位置,出现并发问题。
6.7 TreeSet集合实现类
- 特点:元素无序且不可重复,只允许添加一个null值。
- 底层:TreeSet的底层结构是红黑树(自平衡二叉树),红黑树特性:
- 每个节点要么是红的,要么是黑的。
- 根节点是黑的。
- 每个叶节点是(NIL节点,空节点)黑的。
- 如果一个节点是红的,那么它的两个子节点都是黑的。
- 从任一节点到其每个叶节点的所有路径都包含相同数目的黑节点。
- TreeSet特性:
- 自动平衡:底层结构是红黑树,可以通过颜色调整保持树的平衡。在进行插入、删除和查询操作时,其效率相对较高。
- 有序性:TreeSet中的元素是按照一定的顺序排列的,可以是自然顺序也可以是自定义的顺序。
- 高效并发:TreeSet是线程安全的,内部是红黑树和排序结构,多个线程同时操作情况下,不会出现并发修改的问题。
- 高并发下的性能问题:高并发下由于需要使用锁(同步机制),对于TreeSet操作会比较耗时,产生性能问题。
6.8 重写equals()和hashCode()方法
- 什么场景下需要重写equals和hashCode方法?
- 当类(一般为自定义的类)的对象将被用作HashSet的元素和HashMap的键时,需要重写这两个方法,确保能正确的操作对象和查找操作。
- 当类(一般为自定义的类)的对象需要使用equals方法进行比较时,需要重写equals和hashCode方法,以确保两个对象在比较时具有相同的属性。
- 为什么要重写equals和hashCode方法?
- hashCode方法用于计算对象的哈希码值,该哈希值将对象映射到哈希表中,以此来快速定位对象在哈希表中的位置,若两个对象是相等(属性相同)的,则它们的哈希值相等,在哈希表中的位置也应该是相同的。重写hashCode确保相同属性的对象具有相等的哈希值,以此来确认这两个对象相等。
- equals方法默认情况下比较的是两个对象的引用,即对象在内存中的地址是否为同一个,重写equals方法是为了确保具有相同属性的对象是相等的,也就是确保对象的实际内容是相等的。
- 若不重写会有什么后果?
- 若不重写hashCode方法,则默认情况下使用对象的内存地址计算哈希值,即使对象的属性相同,它们也可能被认为是不相等的,从而导致无法正确的操作和查找对象。
- 若不重写equals方法,则默认情况下是对对象的内存地址进行比较,即使对象的属性相同,它们也可能被认为是不相等的,因为它们是不同的实例,导致结果异常或不正确。
6.9 HashMap双列集合实现类
- 特点:键和值都允许为null,键不能重复,元素是无序的。
- 底层:数组+链表的组合体。数组是主体结构,链表是解决哈希冲突的分支结构,查询效率较高。
- HashMap线程不安全,多线程下需要加同步锁。
- 扩容机制:当HashMap元素达到一定的阈值,就会自动扩容,所有元素会重新计算哈希值并重新放置,耗性能(空间换时间)。
- 初始化HashMap就指定合适的容量,减少扩容次数。
- LinkedHashMap是HashMap的子类,存入和取出的顺序一致。
- Map集合遍历常用
Map<String, Integer> map = new HashMap<>();
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
}
map.forEach((key, value) -> {
System.out.println(key + "--->" + value);
});
6.10 CurrentHashMap双列集合实现类
- 特性
- 高效的线程安全Map实现,支持高并发下的读写操作。
- 锁策略:采用分段式锁技术,将数据分成一段一段的,每段数据由一个锁保护,多个线程同时访问不同的段,实现并发访问。
- 线程安全:分段锁策略。
- 动态扩容:初始化时不需要指定大小,它会根据元素多少动态增加和减少存储数量,增加时,通过ReentrantLock(可重入锁)保护的代码块来保证线程安全。
6.11 CurrentHashMap和HashMap的区别
- Cu..Map线程是安全的,Ha..Map线程不安全。
- Cu..Map支持高并发的操作,Ha..Map不支持高并发操作。
- Cu..Map底层:Node+CAS+Synchronized(同步)实现,Ha..Map底层是数组+链表。
6.12 TreeMap双列集合实现类
- 特点:键和值都允许为null,键不能重复,元素是无序(存入与取出的顺序)的。
- 底层:红黑树(自平衡二叉树)结构。
- 特性
- 自动平衡:自动调整树的结构,保证树的平衡。插入和删除时通过颜色调整,保证树的平衡。
- 排序:默认按照键的自然顺序排序,通过比较左子树与右子树的大小。
- 高效性:线程安全,内部是红黑树与排序结构,由于键是唯一的,多个线程同时操作,不会出现并发修改问题。
- 高并发下可能存在性能问题,高并发需要时使用同步锁,操作耗时。
6.13 HashTable和Properties双列集合实现类
- HashTable是线程安全的,内部有锁的控制,操作效率较低。
- Properties是HashTable的子类,线程安全,相对于HashMap操作效率低。
- 主要用来存储字符串类型的键和值。
- 实际中主要用于存取 .properties 类型文件的配置项。
6.14 泛型的特性
- 类型安全:指导注意类型转换。
- 减少类的加载时间,编译时进行类型检查。
- 提高代码的复用。
6.15 Collections和Arrays工具类常用方法
- Collections:对集合操作
方法 |
含义 |
void swap(List list,int i,int j ) |
交换list中的两个元素 |
int binarySearch(List list, Obj) |
二分查找list中的元素索引,前提先排好序 |
Obj max(Collection col);Obj min(Collection col); |
查找集合中的最大最小元素 |
- Arrays:对数组操作
方法 |
含义 |
Arrays.binarySearch(arr,target) |
二分查找元素,前提先排好序 |
Arrays.copyofRange(int[] arr,int begin,int end) |
拷贝元素到新数组 |
6.16 二分查找
- 原理:使用双指针,每次将指定元素与中间位置元素比较,移动首尾指针位置,直到找到或者前指针不再小于等于后指针为止。前提是排好序(正序或倒序)
- 核心实现:
int mid = (start + end) >>> 1
public int binarySearch(int[] nums, int target) {
// 有序
Arrays.sort(nums);
// 定义开始与结尾索引的指针,便于移动
int start = 0;
int end = nums.length - 1;
// 当start>end时跳出循环
while (start <= end) {
int mid = (start + end) >>> 1; // 无符号右移动,相当于除2取整
// 当(start + end)超过范围时,其值为负,所以不使用 (start + end)/2
if (target < nums[mid]) {
// 目标值介于 nums[start]与nums[mid]之间
// 将end位置置于mid
end = mid - 1;
} else if (target > nums[mid]) {
start = mid + 1;
} else {
return mid;
}
}
return -1;
}
7、JDBC
7.1 基础类接口定义
- DriverManager类:用于加载JDBC驱动并创建数据库连接。
- Connection接口:Java程序和数据库连接的对象,只有获得该对象后,才能访问数据库并操作数据库。
- Statement接口:执行静态SQL语句,并返回一个结果对象。
- 常用的方法
方法 |
含义 |
boolean excute(String sql) |
查询 |
ResultSet getResultSet() |
查询结果集 |
int excuteUpdate(String sql) |
增删改,返回影响条数 |
ResultSet excuteQuery(String sql) |
查询并返回结果集 |
- ResultSet接口:查询返回结果集,封装在一个逻辑表中。
7.2 JDBC创建步骤
- 加载数据库驱动。
Class.forName("com.mysql.jdbc.Driver")
- DriverManager获取数据库连接。
Connection conn = DriverManager.getConnection(String url,String user,String password)
- 获取Statement对象
Statement st = conn.createStatement()
创建基本的对象。PreparedStatement pst = conn.prepareStatement(String sql)
根据sql创建PreparedStatement对象。CallableStatement cst = conn.prepareCall(String sql)
根据sql创建CallableStatement对象。
- 使用以上创建对象调用执行sql的方法,执行sql语句。
- 操作结果集ResultSet。
- 关闭连接,释放资源。后创建的先关闭。
7.3 JDBC创建核心逻辑
public void jdbcUtil() throws SQLException {
Connection conn = null;
Statement statement = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://127.0.0.1:3306/info";
conn = DriverManager.getConnection(url, "root", "123456");
statement = conn.createStatement();
rs = statement.executeQuery("select * from info");
while (rs.next()) {
int id = rs.getInt("id");
System.out.println("id:" + id);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) {
rs.close();
}
if (statement != null) {
statement.close();
}
if (conn != null) {
conn.close();
}
}
}
8、多线程
8.1 进程和线程基础定义
- 进程:在一个操作系统中,每个独立执行的程序都称为一个进程。"正在运行的程序"
- 计算机中,所有应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是只能执行一个进程。
- 线程:在一个进程中可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元称为线程。
- 单线程:就是一条顺序执行的线索,多线程是并发执行的多条线索,充分利用CPU资源。
8.2 进程和线程的区别
- 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
- 上下文切换:线程上下文切换比进程上下文切换要快得多。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
- 系统开销:创建或撤销进程时,系统都要为之分配或回收系统资源,系统所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。
8.3 多线程较单线程的好处
- 并发提升程序执行效率。
- 提高cpu利用率,访问和存储的时候可以切换线程来执行。
- 更快的响应速度,可以有专门的线程来监听用户请求和专门的线程来处理请求。
8.4 线程的创建
- Thread类实现多线程:继承Thread类,重写run()方法。
- 创建一个Thread线程类的子类,同时重写Thread类的run()方法。
- 创建该子类的实例对象,通过调用实例的start()方法启动线程。
// 继承Thread类,重写run()方法
public class MyThread extends Thread {
public MyThread(String name) {
super(name); // 通过有参构造把线程名带进去
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程" + Thread.currentThread().getName() + "执行run...");
}
}
public static void main(String[] args) {
MyThread thread1 = new MyThread("thread1");
MyThread thread2 = new MyThread("thread2");
thread1.start(); // 开启线程
thread2.start();
}
}
- 局限性:Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类实现多线程。
- Runnable接口实现多线程:实现Runnable接口,实现run()方法。
- 创建一个Runnable接口的实现类,实现run()方法。
- 使用Thread有参构造创建线程实例对象,并将Runnable接口的实现类实例对象作为参数传入构造方法中;调用start()方法启动线程。
- 缺陷:Thread和Runnable实现多线程的方法都重写了run()方法,没有返回值,无法返回多线程的结果。
// 实现Runnable接口,实现run()方法
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程" + Thread.currentThread().getName() + "执行run...");
}
}
public static void main(String[] args) {
// 创建实现类实例,作为参数传入Thread构造方法中
MyThread thread = new MyThread();
Thread thread1 = new Thread(thread, "thread1");
thread1.start();
Thread thread2 = new Thread(thread, "thread2");
thread2.start();
}
}
- Callable接口实现多线程:实现Callable接口,重写call()方法。
- 创建Callable接口的实现类,同时重写接口的call()的方法。
- 通过FutureTask线程结果处理类的有参构造封装Callable接口实现类对象。
- 将FutureTask对象作为Thread类的构造方法的参数传入,调用start()方法启动线程。
// 实现Callable接口,重写call()方法
public class MyThread implements Callable<Integer> {
// 可以接收返回值
@Override
public Integer call() throws Exception {
int count = 0;
for (int i = 0; i < 5; i++) {
System.out.println("线程" + Thread.currentThread().getName() + "执行run...");
count++;
}
return count;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread thread = new MyThread();
FutureTask<Integer> ft1 = new FutureTask<>(thread);
Thread thread1 = new Thread(ft1, "thread1");
thread1.start();
FutureTask<Integer> ft2 = new FutureTask<>(thread);
Thread thread2 = new Thread(ft2, "thread2");
thread2.start();
System.out.println("ft1返回:" + ft1.get() + ",ft2返回:" + ft2.get());
}
}
- 使用线程池创建:Executors
- Executors.newCachedThreadPool()
- executorService.submit()只能接收Runnable或者Callable的实现类实例对象。
// 线程池创建线程
public class MyThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程" + Thread.currentThread().getName() + "执行run...");
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
// 只能接收Runnable和Callable的实现类实例
executorService.submit(new MyThread());
executorService.submit(new MyThread());
executorService.shutdown();
}
}
8.5 Runnable和Callable创建多线程的区别
- Runnable接口实现的方法是run(),Callable接口实现的方法是call()。
- Callable接口call()方法有返回值,支持泛型;Runnable接口run()方法没有返回值和泛型。
- Callable接口call()方法允许抛出异常,Runnable接口run()方法不能继续向上抛异常。
8.6 线程的生命周期
- 初始状态(NEW):线程被创建,还没有调用start()。
- 运行状态(Runnable):包括操作系统的就绪和运行两种状态。
- 阻塞状态(Blocked):一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待资源释放将其唤醒。线程被阻塞会释放CPU,不释放内存。
- 等待状态(waiting):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 定时等待(timed_waiting):该状态不同于等待状态,它可以在指定的时间后自行返回。
- 终止(Terminated):表示该线程已执行完毕。
8.7 线程设置优先级
- 线程调度模型分为分时调度模型和抢占式调度模型,Java虚拟机默认采用抢占式调度模型。
- 线程优先级[1--10]:优先级越高,获得CPU执行的机会越大,并不一定就会执行。程序中通过
thread.setPriority(int p)
设置优先级。
- Thread.MAX_PRIORITY = 10
- Thread.MIN_PRIORITY = 1
- Thread.NORM_PRIORITY = 5
8.8 线程中的方法
- start():启动线程
- getPriority():获取线程优先级,线程的默认优先级为5。
- setPriority(int p):设置线程优先级。
- interrupt():中断当前线程。
- 如果线程处于阻塞状态,那么线程将立即退出阻塞状态,并抛出InterruptedException异常。
- 如果线程处于正常运行状态,那么线程的中断标志设置为true。
- join():等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,线程运行结束,当前线程再由阻塞状态转为就绪状态。
- yield():线程让步,让当前正在运行的线程失去CPU使用权,系统重新调度一次,所有线程重新抢夺CPU使用权,不能保证立即执行其他线程。
- sleep(long 毫秒):线程休眠,当其他线程都终止后并不代表当前休眠的线程会立即执行,而是必须当休眠时间结束后,线程才会转换到就绪状态。
8.9 wait()和sleep()方法的异同点
- 相同点
- 它们都可以使当前线程暂停运行,把机会交给其他线程。
- 任何线程在调用wait()和sleep()之后,在等待期间被中断都会抛出InterruptedException异常。
- 不同点
- wait()是object类中的方法,而sleep()是线程Thread类中的方法。
- 对锁的持有不同。wait()会释放锁,sleep()并不释放锁。
- 唤醒方法不完全相同。wait()依靠notify()或者notifyAll()方法、中断以及到达指定时间唤醒,sleep()到达指定时间才会被唤醒。
- 调用wait()需要先获取对象的锁,而Thread.sleep()不需要。
8.10 线程run()和start()方法的区别
- 当程序调用start()方法,将会创建一个新的线程去执行run()方法中的代码。run()就像一个普通方法一样,直接调用run()方法,不会产生一个新线程。
- 一个线程的start()方法只能调用一次,多次调用会抛出IllegalThreadStateException异常,run()方法没有限制。
8.11 线程死锁
- 概念:两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,会一直死锁下去。
- 线程死锁产生的条件
- 互斥:一个资源每次只能被一个线程使用。
- 请求与保持:一个线程因请求资源而阻塞时,不释放获得的资源。
- 不剥夺:线程已获得的资源,在未使用之前,不能强行剥夺。
- 循环等待:线程之间循环等待着资源。
- 避免死锁的方法
- 互斥条件不能破坏,因为加锁就是为了保证互斥。
- 一次性申请所有的资源,避免线程占有资源而且在等待其他资源。
- 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源。
- 按序申请资源。
8.12 synchronized关键字
- 概念:同步锁。处理多个线程同时处理共享资源所导致的并发问题。使用同步锁机制保证处理共享资源在任意时刻只能有一个线程访问。
- synchronized底层原理
- 其内部包含一个计数器,默认为0,计数器为0时,锁处于释放状态,线程可获取锁,当一个线程成功获取锁之后,锁会将计数器设置为1,处于被获取状态,当新的线程进来时,需要阻塞等待,只有当上一个线程执行完锁将计数器置为0时,锁被释放,新的线程才可重新进行获取。
- synchronized的特性
- 原子性:确保线程互斥的访问同步代码。
- 可见性:保证共享变量的修改能够及时可见。
- 有序性:有效解决重排序问题。
- synchronized的用法
- 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁,在某一时刻只允许一个线程访问。
- 修饰静态方法:作用于当前类,进入同步代码前,要获得当前类对象的锁。synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给Class类上锁。
- 修饰代码块:指定加锁对象,对给定的对象加锁,进入同步代码块前要获得给定对象的锁。
8.13 ReentrantLock(可重入锁Lock)和Synchronized的区别
- 使用Synchronized实现同步,线程执行完同步代码块后会自动释放锁,ReentrantLock需要手动释放锁。
- Synchronized是非公平锁,ReentrantLock默认是非公平锁,可以设置为公平锁。
- 公平锁:按照线程访问顺序来获取对象锁。
- ReentrantLock等待锁的线程是可中断的,线程可以放弃等待锁,而Synchronized会无限期的等待下去。
- ReentrantLock可以设置超时获取锁,在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
- ReentrantLock的tryLock()方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
// Lock锁的使用
final Lock lock = new ReentrantLock(); // 定义锁对象
lock.lock(); // 加锁
// 逻辑代码...
lock.unlock(); // 释放锁
lock.tryLock(); // 判断锁是否可用
注:通常在finally{}中释放锁,保证锁最终能够被释放。
8.14 悲观锁与乐观锁
- 悲观锁:每次访问资源都会加锁,执行完同步代码释放锁。Synchronized和ReentrantLock属于悲观锁。
- 乐观锁:不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试,乐观锁最常见的实现是CAS。
- 优势:避免了悲观锁独占对象的问题,提高了并发性能。
- 缺陷
- 乐观锁只能保证一个共享变量的原子操作。
- 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会给CPU带来很大的开销。
- ABA问题。CAS的原理是通过比对内存值与预期值是否一样而判断内存值是否被改过,但是会有以下问题:假如内存值原来是A, 后来被一条线程改为B,最后又被改成了A,则CAS认为此内存值并没有发生改变。可以引入版本号解决这个问题,每次变量更新都把版本号加一。
8.15 守护线程
- 概念:运行在后台的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或者等待处理某些发生的时间。在Java中,垃圾回收线程就是特殊的守护线程。