第01章 内容介绍
-
内容介绍
-
学习前的话
-
就业方向
-
开发场景
-
应用领域
-
内容梳理
第02章 Java概述
-
程序举例
-
Java故事
-
Java特性
-
Sublime
-
Jdk介绍
- JVM 是一个虚拟的计算机,具有指令集并使用不同存储区域。负责执行指令,管理数据、内存、寄存器,包含在 JDK 中
- JDK = JRE + Java 开发工具(java、javac、javap、javadoc ······)
- JRE = JVM + Java 核心类库
-
我的电脑
-
Jdk安装
-
环境变量配置
-
Win7安装Jdk
-
Mac安装Jdk
-
快速入门
-
运行机制
-
开发细节
-
学习方法
-
转义字符
-
易犯错误
-
注释介绍
-
多行注释
-
文档注释
-
代码规范
-
DOS原理
-
路径详解
-
DOS指令1
-
DOS指令2
-
DOS指令3
-
本章作业1
-
本章作业2
-
内容梳理
第03章 变量
-
变量原理
-
变量概念
-
变量入门
-
变量细节
-
加号使用
-
数据类型
-
整型使用
类型 | 字节数 | 范围 |
---|---|---|
byte | 1 | -128 - 127 |
short | 2 | -32768 - 32767 |
int | 4 | -231 - 231-1 |
long | 8 | -263 - 263-1 |
-
整型细节
-
浮点数使用
类型 | 字节数 | 范围 |
---|---|---|
float | 4 | -3.403E38 - 3.403E38 |
double | 8 | -1.798E308 - 1.798E308 |
- 浮点数组成:符号位 + 指数位 + 尾数位
- Java 浮点类型默认为 double 类型,定义 float 类型时需要在末尾加上 F 或 f
-
浮点数细节1
-
浮点数细节2
-
Java文档
-
字符型使用
类型 | 字节数 | 范围 |
---|---|---|
char | 2 | ISO 单一字符集 |
-
字符型细节
-
字符型本质
-
常用编码
类型 | 字符占用字节数 | 汉字占用字节数 |
---|---|---|
ASCII | 1 | |
Unicode | 2 | 2 |
UTF - 8 | 1 | 3 |
GBK | 1 | 2 |
- 布尔类型
类型 | 字节数 | 范围 |
---|---|---|
boolean | 1 | true or false |
- 自动类型转换基础
- char -> int -> long -> float -> double
- byte -> short -> int -> long -> float -> double
- 自动类型转换基础细节1
- (byte,short) 和 char 之间不会自动类型转换
- byte、char、short 在进行运算时,当作 int 处理
-
自动类型转换基础细节2
-
强制类型转换基础
-
强制类型转换细节
- char 类型可以保存 int 类型的常量值,但不能保存 int 型的变量值,需要强转
-
类型转换练习
-
String和基本类型转换
-
String转基本类型细节
-
本章作业1
-
本章作业2
-
本章小结
第04章 运算符
-
算术运算符介绍
-
算术运算符使用
int i = 1;
i = i++;
// output:1
System.out.println(i);
int i = 1;
i = ++i;
// output:2
System.out.println(i);
-
算术运算符练习1
-
算术运算符练习2
-
算术运算符练习3
-
算术运算符练习4
-
关系运算符介绍
-
关系运算符使用
-
逻辑运算符介绍
-
短路与逻辑与
- && 与 &
- 短路或逻辑或
- || 与 |
- 逻辑非逻辑异或
- ! 与 ^
- 逻辑运算符练习
- 赋值运算符介绍
- 赋值运算符细节
- 三元运算符介绍
Object object = true ? new Integer(1) : new Double(2.0);
// 三元运算符需要看作一个整体,故输出 1.0;if - else 分支结构则输出 1
System.out.println(object);
- 三元运算符细节
- 三元运算符练习
- 运算符优先级
运算顺序 | 操作符 |
---|---|
. () {} ; , | |
R -> L | ++ – ~ ! |
L -> R | * / % |
L -> R | + - |
L -> R | << >> >>> |
L -> R | < > <= >= instanceof |
L -> R | == != |
L -> R | & |
L -> R | ^ |
L -> R | | |
L -> R | && |
L -> R | || |
L -> R | ? : |
R -> L | = *= /= %= |
R -> L | += -= <<= >>= |
R -> L | >>>= &= ^= |= |
-
标识符规则
-
标识符练习
-
标识符规范
-
关键字保留字
- strictfp:strict float point (精确浮点) - strictfp关键字可应用于类、接口、方法。 使用 strictfp 关键字声明一个方法时,该方法中所有的 float 和 double 表达式都严格遵守 FP-strict 的限制,符合 IEEE-754 规范
- volatile 是 jvm 提供的轻量级同步机制。作用是: 1. 保证可见性 2. 禁止指令重排 3. 不保证原子性
-
键盘输入
-
四种进制介绍
-
2进制转10进制
-
8进制转10进制
-
16进制转10进制
-
10进制转2进制
-
10进制转8进制
-
10进制转16进制
-
2进制转8进制
-
2进制转16进制
-
8进制转2进制
-
16进制转2进制
-
位运算思考题
-
原码、反码和补码
-
位运算详解1
-
位运算详解2
- 算术右移:>> :低位丢弃,符号位不变,高位补符号位
- 算术左移:<< :符号位不变,低位补0
- 无符号右移:>> :低位丢弃,高位补 0
-
本章作业
-
内容梳理
第05章 程序控制结构
-
顺序控制
-
单分支使用
-
单分支流程图
-
双分支使用
-
双分支流程图
-
双分支练习题
-
多分支使用
-
多分支练习1
-
多分支练习2
-
嵌套分支
-
嵌套分支课后练习
-
switch基本语法
-
switch流程图
-
switch快速入门
-
switch细节
- switch 的 case 后跟常量或常量表达式,switch 中的表达式只能是以下类型中的一种:byte、short、char、int、enum、String
-
switch课堂练习1
-
switch课堂练习2
-
switch和if选择
-
for基本语法
-
for执行流程
-
for细节
-
for编程思想1
-
for编程思想2
-
while基本语法
-
while执行流程
-
while课堂练习
-
do-while语法
-
do-while执行流程
-
do-while练习1
-
do-while练习2
-
多重循环执行流程
-
多重循环练习1
-
多重循环练习2
-
空心金字塔
-
break需求
-
break执行流程
-
break快速入门
-
break细节
-
break课堂练习1
-
break课堂练习2
-
continue执行流程
-
continue快速入门
-
continue细节
-
return使用说明
-
本章作业1
-
本章作业2
-
本章作业3
-
本章作业4
-
本章作业5
-
本章作业6
-
内容梳理
-
听懂和会做
第06章 数组、排序和查找
-
数组必要性
-
数组快速入门
- 数组是引用类型,创建数组时使用了 new 关键字
-
数组使用1
-
数组使用2
-
数组使用3
- 数组静态初始化
int[] array = {
2,54,3,23};
- 数组注意事项
- 创建数组后,若没有赋值,则有默认值
int[] a = new int[3];
// output: [0, 0, 0]
System.out.println(Arrays.toString(a));
-
数组练习1
-
数组练习2
-
数组赋值机制1
- 基本数据类型赋值,赋值的是具体的数据
- 引用类型赋值,传递的是数组的地址
-
数组赋值机制2
-
数组拷贝
-
数组翻转1
-
数组翻转2
-
数组扩容1
-
数组扩容2
-
数组缩减
-
排序介绍
- 内部排序:将需要处理的所有数据都加载到内部存储器中进行排序(交换式排序法、选择式排序法和插入式排序法)
- 外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序(合并排序法和直接合并排序法)
-
冒泡排序思路
-
冒泡排序实现
-
查找
-
二维数组入门
-
二维数组内存布局
-
二维数组使用1
-
二维数组使用2
- Java 中二维数组每行的元素个数可以不一致
int[][] array = new int[3][];
array[0] = new int[3];
array[1] = new int[2];
array[2] = new int[6];
-
二维数组使用3
-
二维数组练习1
-
杨辉三角
-
二维数组细节
-
二维数组练习2
-
本章作业1
-
本章作业2
-
本章作业3
-
本章作业4
-
本章作业5
-
内容梳理
-
专业和编程
第07章 面向对象编程(基础部分)
-
类与对象引出
-
类与对象概述
-
面向对象快速入门
-
对象内存布局
-
属性概念
-
属性注意细节
-
创建对象访问属性
-
对象分配机制
-
对象创建过程
- 栈:一般存放基本数据类型(局部变量)
- 堆:存放对象(数组)
- 方法区:常量池(字符串)、类加载信息
-
对象机制练习
-
方法快速入门1
-
方法快速入门2
-
方法调用机制
-
方法的妙用
-
方法的定义
-
方法的使用细节1
-
方法的使用细节2
- 方法中不能嵌套定义方法
-
方法的使用细节3
-
方法练习题1
-
方法传参机制1
-
方法传参机制2
-
方法传参机制3
-
克隆对象
-
递归解决什么问题
-
递归执行机制1
-
递归执行机制2
-
递归执行机制3
-
递归执行机制4
-
递归斐波那契
-
猴子吃桃
-
老鼠出迷宫1
-
老鼠出迷宫2
-
老鼠出迷宫3
-
老鼠出迷宫4
-
汉诺塔
-
八皇后
-
重载介绍
-
重载快速入门
-
重载使用细节
- 方法重载方法签名必须不同,返回值不是方法签名的一部分
-
重载课堂练习1
-
重载课堂练习2
-
重载课堂练习3
-
可变参数使用
-
可变参数细节
- 可变参数的实参可以是 0 个或是多个,其本质就是数组
- 形参列表中可以同时有普通形参和可变参数,但必须保证可变参数是形参列表的最后一个参数
- 一个方法的形参列表只能出现一个可变参数
-
可变参数练习
-
作用域基本使用
- 类的属性有默认值,局部变量没有默认值
-
作用域使用细节1
-
作用域使用细节2
-
构造器基本介绍
-
构造器快速入门
-
构造器使用细节1
- 构造器完成对象的初始化,并不是创建对象;调用构造器时堆中已经分配了对象的空间,构造器只是负责对对象进行初始化
-
构造器使用细节2
-
构造器课堂练习
-
对象创建流程详解
- 加载类信息,生成 .class 对象
- 在堆中为对象分配空间
- 完成对象的默认初始化
- 构造器完成对象的初始化
-
引出this
-
this入门
- JVM 会给每个对象分配 this,代表当前对象;哪个对象调用,this 指向它
-
this本质
-
this小结
-
this使用细节
- this 不能在类定义的外部使用,只能在类定义的方法中使用
-
this课堂练习
-
本章作业1
-
本章作业2
-
本章作业3
-
本章作业4
-
本章作业5
-
本章作业6
-
本章作业7
-
本章作业8
-
本章作业9
-
本章作业10
-
本章作业11
-
内容梳理
第08章 面向对象编程(中级部分)
-
IDEA介绍
-
IDEA下载安装
-
IDEA使用1
-
IDEA使用2
-
IDEA使用3
-
IDEA快捷键1
- 自动补全 alt + /
-
IDEA快捷键2
-
IDEA快捷键3
- 查看类结构:ctrl + H
-
IDEA模板
-
包基本结束
-
包原理
-
包快速入门
-
包命名
-
常用的包
-
包的使用细节
-
访问修饰符规则
- 修饰符只能用来修饰类或类的成员
-
访问修饰符细节
-
封装介绍
-
封装步骤
-
封装快速入门
-
封装与构造器
-
封装课堂练习
-
为什么需要继承
-
继承原理图
-
继承快速入门
-
继承使用细节1
- 子类继承了父类所有的方法和属性,但父类的私有属性不能在子类中直接访问
- 继承使用细节2
- 当创建子类的对象时,无论使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类中使用 super 去显式调用父类的构造器完成父类的初始化工作,否则编译不会通过
- 继承使用细节3
- super() 和 this() 都只能放在构造器的第一行,因此这两个方法不能同时存在一个构造器中
- 继承使用细节4
- Object 类的所有方法
-
继承使用细节5
-
继承本质详解
-
继承课堂练习1
-
继承课堂练习2
-
继承课堂练习3
-
super基本语法
- super 代表父类的引用,用于访问父类的属性、方法、构造器
- 使用 super 不能访问父类的 private 成员
-
super使用细节1
-
super使用细节2
-
super使用细节3
区别点 | this | super |
---|---|---|
方法 | 访问本类属性,没有则依次查找父类 | 直接依次查找父类属性 |
属性 | 访问本类方法,没有则依次查找父类 | 直接依次查找父类方法 |
构造器 | 行首调用本类其它构造器 | 行首调用父类指定构造器 |
特殊 | 表示当前对象 | 表示子类中访问父类的对象 |
-
方法重写介绍
-
方法重写细节
- 子类重写父类的方法时,返回类型可以是父类返回类型的子类
- 子类不能缩小父类方法的访问权限
-
重写课堂练习1
-
重写课堂练习2
-
养宠物引出多态
-
方法的多态
- 重载与重写也体现了多态
-
对象的多态
-
多态快速入门
-
向上转型
-
向下转型
-
属性重写问题
-
多态课堂练习1
-
多态课堂练习2
-
动态绑定机制
- 当调用对象方法的时候,该方法会和该对象的内存地址 / 运行类型动态绑定
- 当调用对象属性时,不存在动态绑定,在哪儿声明在哪使用
-
多态数组1
-
多态数组2
-
多态参数
-
运算符
作用 | |
---|---|
== | 判断基本类型时判断值是否相等;判断引用类型时判断是否引用到同一个对象 |
equals() | 只能判断引用类型是否引用到同一个对象,子类往往重写 |
-
查看Jdk源码
-
子类重写equals
-
equals课堂练习1
-
equals课堂练习2
-
equals课堂练习3
-
hashCode
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>Whenever it is invoked on the same object more than once during
* an execution of a Java application, the {@code hashCode} method
* must consistently return the same integer, provided no information
* used in {@code equals} comparisons on the object is modified.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
- toString
/**
* Returns a string representation of the object. In general, the
* {@code toString} method returns a string that
* "textually represents" this object. The result should
* be a concise but informative representation that is easy for a
* person to read.
* It is recommended that all subclasses override this method.
* <p>
* The {@code toString} method for class {@code Object}
* returns a string consisting of the name of the class of which the
* object is an instance, the at-sign character `{@code @}', and
* the unsigned hexadecimal representation of the hash code of the
* object. In other words, this method returns a string equal to the
* value of:
* <blockquote>
* <pre>
* getClass().getName() + '@' + Integer.toHexString(hashCode())
* </pre></blockquote>
*
* @return a string representation of the object.
*/
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
- finalize
/**
* Called by the garbage collector on an object when garbage collection
* determines that there are no more references to the object.
* A subclass overrides the {@code finalize} method to dispose of
* system resources or to perform other cleanup.
* <p>
* The general contract of {@code finalize} is that it is invoked
* if and when the Java™ virtual
* machine has determined that there is no longer any
* means by which this object can be accessed by any thread that has
* not yet died, except as a result of an action taken by the
* finalization of some other object or class which is ready to be
* finalized. The {@code finalize} method may take any action, including
* making this object available again to other threads; the usual purpose
* of {@code finalize}, however, is to perform cleanup actions before
* the object is irrevocably discarded. For example, the finalize method
* for an object that represents an input/output connection might perform
* explicit I/O transactions to break the connection before the object is
* permanently discarded.
* <p>
* The {@code finalize} method of class {@code Object} performs no
* special action; it simply returns normally. Subclasses of
* {@code Object} may override this definition.
* <p>
* The Java programming language does not guarantee which thread will
* invoke the {@code finalize} method for any given object. It is
* guaranteed, however, that the thread that invokes finalize will not
* be holding any user-visible synchronization locks when finalize is
* invoked. If an uncaught exception is thrown by the finalize method,
* the exception is ignored and finalization of that object terminates.
* <p>
* After the {@code finalize} method has been invoked for an object, no
* further action is taken until the Java virtual machine has again
* determined that there is no longer any means by which this object can
* be accessed by any thread that has not yet died, including possible
* actions by other objects or classes which are ready to be finalized,
* at which point the object may be discarded.
* <p>
* The {@code finalize} method is never invoked more than once by a Java
* virtual machine for any given object.
* <p>
* Any exception thrown by the {@code finalize} method causes
* the finalization of this object to be halted, but is otherwise
* ignored.
*
* @throws Throwable the {@code Exception} raised by this method
* @see java.lang.ref.WeakReference
* @see java.lang.ref.PhantomReference
* @jls 12.6 Finalization of Class Instances
*/
protected void finalize() throws Throwable {
}
-
断点调试介绍
-
断点调试案例1
-
断点调试案例2
-
断点调试案例3
-
断点调试案例4
-
断点调试案例5
-
断点调试案例6
-
零钱通介绍
-
零钱通菜单
-
零钱通明细
-
零钱通收益
-
零钱通消费
-
零钱通退出确认
-
零钱通金额校验
-
零钱通OPP版
-
本章作业1
-
本章作业2
-
本章作业3
-
本章作业4
-
本章作业5
-
本章作业6
-
本章作业7
-
本章作业8
-
本章作业9
-
本章作业10
-
本章作业11
-
本章作业12
-
本章作业13
-
本章作业14
-
本章作业15
-
本章作业16
-
本章作业17
-
内容梳理
-
不要让堕性毁了你
第09章 房屋出租系统
-
房屋出租需求
-
房屋出租设计
-
房屋出租工具类
-
房屋出租House类
-
房屋出租主菜单
-
房屋出租列表
-
房屋出租添加
-
房屋出租删除
-
房屋出租退出
-
房屋出租查找
-
房屋出租修改
-
第一阶段结束语
第10章 面向对象编程(高级部分)
-
类变量引出
-
类变量快速入门
-
类变量内存剖析
- JDK 8 以前类变量放在方法区的静态域中,JDK 8 以后放在堆的对应类的 class 对象尾部
- 类变量定义访问
- 类变量也即静态变量
-
类变量使用细节
-
类方法快速入门
- 类方法也即静态方法
- 类方法最佳实践
- 当方法中不设计到与任何对象相关的成员时,则可以将方法设计为静态方法以提高运行效率
- 类方法注意事项
- 静态方法只能访问静态成员;非静态方法,可以访问静态和非静态成员
-
类成员课堂练习
-
main语法说明
-
main特别说明
-
main动态传值
-
代码块快速入门
- 代码块可以理解为只有方法体的方法,它没有方法名、返回值、参数,不能通过对象或类显式调用,只有在加载类或是创建对象时隐式调用
- 代码块的修饰符只能是 static,方法体中的语句可以是任何正确逻辑语句
- 类代码块的调用顺序优先于构造器
- 代码块使用细节1
- 类加载时机:创建该类的对象时;创建子类的对象时父类也会加载;使用类的静态成员时
- 如果只是调用类的静态成员,普通代码块并不会执行
- 代码块使用细节2
- 普通代码块可以看作是构造器的一种补充机制,所以在调用类的静态成员但没有创建类的对象时,构造器并未被调用,因而类的普通代码块不会被执行
- 代码块使用细节3
- 创建类的对象时,执行顺序
- 静态代码块与静态成员优先级一致,按定义先后顺序依次执行
- 普通代码块与普通成员优先级一致,按定义先后顺序依次执行
- 调用构造器
- 理解:构造器的开头隐含了 super() 和 调用普通代码块
class Temp {
public Temp() {
super();
{
}
}
}
- 代码块使用细节4
- 代码执行顺序
- 父类的静态代码块和静态属性按定义顺序依次执行
- 子类的静态代码块和静态属性按定义顺序依次执行
- 父类的普通代码块与普通属性按定义顺序依次执行
- 父类的构造器
- 子类的普通代码块与普通属性按定义顺序依次执行
- 子类的构造器
- 静态代码块、静态方法只能调用静态成员;普通方法和普通代码块可以调用任何成员
-
代码块课堂练习
-
单例模式饿汉式
- 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。设计模式好比经典的棋谱,不同的棋局采用不同的棋谱,免去再次思考和摸索的过程
- 类的单例设计模式:采取一定的方法在整个软件系统中,某个类有且仅有一个对象实例,并且该类只提供一个取得该对象的方法
- 单例模式的实现:构造器私有化;在类的内部创建对象;向外部提供一个静态的公共方法取得该对象
// 单例设计模式饿汉式
class GirlFriend {
private GirlFriend() {
}
private static final GirlFriend girlFriend = new GirlFriend();
public static GirlFriend getGirlFriend() {
return girlFriend;
}
}
- 单例模式懒汉式
// 单例设计模式懒汉式
class GirlFriend {
private GirlFriend() {
}
private static GirlFriend girlFriend;
public static GirlFriend getGirlFriend() {
if (girlFriend == null) {
girlFriend = new GirlFriend();
}
return girlFriend;
}
}
- 饿汉式与懒汉式的区别:创建时机不同 - 饿汉式在类加载是就创建了对象而懒汉式在使用到对象时才创建
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题
- java.lang.Runtime 类就是经典的单例模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {
}
}
- final基本使用
- final 的使用场景:不希望某个类被继承;不希望父类的方法被子类重写;不希望类的成员值被修改;不希望局部变量的值被修改
- final使用细节1
- final 修饰的属性必须初始化,此后不能更改,初始化可在三个地方进行:定义时初始化;构造器中初始化;代码块中初始化
- 如果 final 修饰的属性同时是 static 的,那么只能在两个地方初始化:定义时初始化;静态代码块中初始化
- 如果非 final 类中含有 final 修饰的方法,则该方法可以被继承不能被重写
- final使用细节2
- final 往往和 static 搭配使用,效率更高,因为底层编译器做了优化处理,不会导致类的加载
class GirlFriend {
public static final int AGE = 2;
// 当使用到 GirlFriend.AGE 时,此静态代码块不会执行即此类未加载
static {
System.out.println("I am your only girlfriend");
}
}
- 八大包装类都是 final 类型的
-
final课堂练习
-
抽象类引出
- 当父类的某些方法需要被声明,但又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类
- 有抽象方法的类一定是抽象类,抽象类不一定有抽象方法
- 抽象方法没有方法体即 {}
- 抽象类细节1
- abstract 只可以修饰类和方法
- 抽象类细节2
- 抽象类的本质也是类,可以有任何普通类有的属性
- 如果一个类继承自抽象类,则必须实现父类的所有抽象方法,除非它也是一个抽象类
- 抽象方法不能使用 private、final、static 来修饰,因为这些修饰符与重写相违背
-
抽象类课堂练习
-
抽象模板模式
-
接口快速入门
-
接口基本介绍
- JDK 7 及之前的版本,所有的方法都没有方法体及抽象方法
- JDK 8 及之后的版本,接口类中可以有静态方法、默认方法即接口中的方法可以有具体实现
interface Study{
default public void english() {
System.out.println("Study english");
}
public static void math() {
System.out.println("Study math");
}
}
-
接口应用场景
-
接口使用细节1
- 抽象类实现接口可以不用实现方法;接口继承接口时可以不用实现方法
- 接口使用细节2
- 接口中的属性能且仅能是 public static final 修饰符
-
接口课堂练习
-
接口VS继承
- 继承的价值:解决代码的复用性与可维护性
- 接口的价值:设计规范方法,一定程度上实现代码解耦即接口规范性 + 多态
-
接口多态特性
-
接口多态传递
-
接口课堂练习
-
四种内部类
- 匿名内部类、局部内部类、成员内部类、静态内部类
- 局部内部类1
- 局部内部类定义在外部类的局部位置,比如方法或代码块中,并且有类名,有以下几个特点
- 可以直接访问外部类的所有成员,包含私有的
- 不可以添加访问修饰符,可以添加 final
- 局部内部类可以理解为方法的局部变量
class Outer {
private String name = "Spring-_-Bear";
public void m1() {
class Inner {
private void test() {
// 直接调用外部类的私有成员
System.out.println(name);
}
}
// 调用局部内部类的方法
new Inner().test();
}
}
- 局部内部类2
- 如果外部类和局部内部类的成员重名时,默认遵循就近原则。如果想访问外部类的成员,可以使用 外部类名.this.成员 的方式去访问
class Outer {
private String name = "Spring-_-Bear";
public void m1() {
class Inner {
private String name = "springbear";
private void test() {
// 调用局部内部类的成员
System.out.println(name);
// 调用外部类的成员
System.out.println(Outer.this.name);
}
}
// 调用局部内部类的方法
new Inner().test();
}
}
- 匿名内部类本质
- 匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名,同时还是一个对象
/**
* 基于接口的匿名内部类
*
* @author Spring-_-Bear
* @date 2021-12-12 08:50
*/
public class C02 {
public static void main(String[] args) {
// 方式1
I03 i03 = new I03() {
@Override
public void cry() {
System.out.println("呜呜呜~~~");
}
};
i03.cry();
// 匿名内部类的类名:I03
System.out.println(i03.getClass());
}
// 方式2
new I03() {
@Override
public void cry() {
System.out.println("呜呜呜~~~");
}
}.cry();
}
interface I03{
void cry();
}
-
匿名内部类使用
-
匿名内部类细节
- 匿名内部类本身既是一个类的定义,同时它本身也是一个对象,因而从语法层面看,它既有类定义类的特征也有创建对象的特征
- 只能定义在方法或代码块中,匿名内部类的地位就好比一个没有名字的局部变量
- 匿名内部类实践
- 经典使用场景:将匿名内部类直接当作实参传递,简洁高效
- 成员内部类1
- 成员内部类可以理解为类的成员
- 成员内部类2
/**
* 成员内部类的使用
*
* @author Spring-_-Bear
* @date 2021-12-12 08:50
*/
public class C02 {
public static void main(String[] args) {
new Outer().new Inner().m();
}
}
class Outer {
private String name = "Spring-_-Bear";
// 成员内部类
public class Inner {
public void m() {
System.out.println(name);
}
}
}
- 静态内部类1
- 静态内部类可以直接访问外部类的所有静态成员,但不能访问非静态成员
- 静态内部类可以理解类的静态成员
-
静态内部类2
-
我亦无他唯手熟尔
第11章 枚举和注解
-
枚举类引出
-
自定义枚举类
- 枚举是一组常量的集合,属于一种特殊的类,里面只包含一组有限的特定的对象
- 自定义枚举类:构造器私有化;不提供 setXxx() 方法;对枚举属性使用 statc + final 修饰以实现底层优化
class Season {
private final String name;
private final String description;
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("冬天", "寒冷");
private Season(String name, String description) {
this.name = name;
this.description = description;
}
}
- enum枚举类1
enum Season {
// 必须位于行首且以逗号间隔,分号结尾
SPRING("春天", "温暖"),
SUMMER("夏天", "炎热"),
AUTUMN("秋天", "凉爽"),
WINTER("冬天", "寒冷");
private final String name;
private final String description;
private Season(String name, String description) {
this.name = name;
this.description = description;
}
}
- enum枚举类2
- 使用 enum 关键字开发一个枚举类时,默认会继承 Enum 类,而且是一个 final 类
- 使用 Javap 反编译 enum 类
- 如果使用的是无参构造器创建枚举对象,则括号可以省略
-
enum枚举类3
-
Enum成员方法
方法名 | 功能 |
---|---|
toString() | 返回当前对象名,子类可重写 |
name() | 返回当前对象常量名,子类不可重写 |
ordinal() | 返回当前对象的位置号,默认从 0 开始 |
vales() | 返回当前枚举类中的所有常量 |
valueOf() | 将字符串转换枚举对象 |
compareTo() | 比较两个枚举常量的位置号 |
-
Enum课堂练习
-
Enum使用细节
- enum 作为一个特殊的类,依然可以实现接口,但已经继承了 Enum 类,所以不能继承其它类
- Override注解
- 注解(Annotation)也成为元数据(Metadata),用于修饰解释包、类、方法、属性、构造器、局部变量等数据信息
- 与注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息
- 在 Java SE 中,注解的使用目的比较简单,例如标记过时的方法、忽略警告等;在 Java EE 中注解占据重要角色,例如用来配置应用程序的任何切面、代替 Java EE 旧版中所遗留的冗杂代码和 XML 配置等
- 重写方法时如果添加了 @Override 注解,则编译器就会检查是否真的重写了父类的方法,如果未重写则编译通不过
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
- @interface 并不指此类是接口,而是说明此类是注解类,在 JDK 1.5 之后加入
- @Target(ElementType.METHOD) 是修饰注解的注解,成为元注解,用于说明此注解作用的范围
- Deprecated注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={
CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
- SupressWarnings注解
@Target({
TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
- 四种元注解
@Retention // 指定注解的作用域:SOURCE、CLASS、RUNTIME
@Target // 指定注解使用的使用位置
@Documented // 指定注解在 javadoc 中体现
@Inherited // 指定子类可以继承父类的注解
- 使用 @Documented 元注解的注解 @Retention 作用域必须为 RUNTIME
-
家庭作业1
-
家庭作业2
/**
* 匿名内部类
*
* @author Spring-_-Bear
* @version 2021-12-08 11:33
*/
public class Homework02 {
public static void main(String[] args) {
Phone phone = new Phone();
// 匿名内部类,实现接口的方法,面向接口编程,动态绑定
phone.executeWork(new ICalculate() {
@Override
public double cal(double num1, double num2) {
return num1 - num2;
}
}, 12, 64);
}
}
interface ICalculate {
public double cal(double num1, double num2);
}
class Phone {
public void executeWork(ICalculate iCalculate, double num1, double num2) {
System.out.println(iCalculate.cal(num1, num2));
}
}
- 家庭作业3
package charpter11;
/**
* 局部内部类
*
* @author Spring-_-Bear
* @version 2021-12-08 11:41
*/
public class Homework03 {
public static void main(String[] args) {
new OuterClass().method();
}
}
class OuterClass {
private String name = "Spring-_-Bear";
public void method() {
class InnerClass {
private String name = "lcx";
public void show() {
System.out.println("Inner name = " + name + " Outer name = " + OuterClass.this.name);
}
}
new InnerClass().show();
}
}
-
家庭作业4
-
家庭作业5
-
家庭作业6
-
内容梳理
第12章 异常
-
异常处理入门
-
异常基本介绍
- 执行过程中发生异常事件可分为两大类:
- Error(错误):Java 虚拟机无法解决的严重问题,如 JVM 系统内部错误、资源耗尽等严重情况
- Exception(异常):其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理;分为编译时异常和运行时异常
- 异常体系图
-
五大运行时异常
-
异常课堂练习
-
异常处理机制
- 处理了异常,程序继续执行,若有可能抛出异常未处理,则由 JVM 打印异常信息后退出程序
-
try-catch
-
try-catch练习
public int method() {
int i = 1;
try {
i++;
String[] names = new String[3];
if (names[i].equals("lcx")) {
}
return i;
} catch (NullPointerException e) {
return ++i;
} finally {
// return result: 4
return ++i;
}
}
public int method1() {
int i = 1;
try {
i++;
String[] names = new String[3];
if (names[i].equals("lcx")) {
}
return i;
} catch (NullPointerException e) {
// return result: 3
return ++i;
} finally {
++i;
}
}
- 如果出现异常,则 try 中发生异常语句后的语句不再执行
-
try-catch最佳实践
-
throws入门案例
- 如果一个方法中的语句执行时可能生成某种异常,但不能确定如何处理这种异常,则可以在方法声明处显式地抛出异常,由调用者自行处理
- throws使用细节
- 编译异常必须处理,对于运行时异常,如不处理则默认 throws
- 子类重写父类的方法时,子类抛出异常的类型要么和父类抛出的一致,要么只能是父类抛出异常的子类
- 自定义异常
- 如果继承 Exception,则属于编译异常,必须显式处理或抛出
- 如果继承 RuntimeException,则属于运行时异常,可以默认 throws 处理,较为方便
- throw VS throws
意义 | 位置 | 抛出内容说明 | |
---|---|---|---|
throws | 异常处理的一种方式 | 方法声明处 | 异常类型 |
throw | 手动生成异常对象 | 方法体中 | 异常对象 |
-
课后作业1
-
课后作业2
-
异常处理小结
第13章 常用类
- 八大Wrapper类
- 除 Boolean 和 Character 外,其余包装类均继承父类 Number
- 装箱和拆箱
- jdk 5 以前是手动装箱和拆箱,自动装箱底层调用的是对应包装类的 valueOf() 方法
// 手动装箱
Integer integer = Integer.valueOf(i);
// 手动拆箱
int ii = integer.intValue();
- 包装类测试
Object object = true ? new Integer(1) : new Double(2.0);
// 三元运算符需要看作一个整体,故输出 1.0;if - else 分支结构则输出 1
System.out.println(object);
- 包装类方法
/* 包装类转 String */
Integer i = 100;
// 方式 1
String str = i + "";
// 方式 2
String str = i.toString();
// 方式 3
String str = String.valueOf(i);
/* String 转包装类 */
String str = "123";
// 方式 1
Integer i = Integer.parseInt(str);
// 方式 2
Integer i = new Integer(str);
- Integer创建机制
Integer i = new Integer(1);
Integer ii = new Integer(1);
// false
System.out.println(i == ii);
Integer j = 1;
Integer jj = 1;
// true
System.out.println(j == jj);
Integer k = 128;
Integer kk = 128;
// false
System.out.println(k == kk);
- 自动装箱机制,在 [-128,127] 范围内直接返回,否则 new Integer(i)
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
- Integer面试题
- 只要有基本数据类型与包装类型进行比较时,判断的是值是否相等
Integer i = 127;
int ii = 127;
// true
System.out.println(i == ii);
Integer j = 128;
int jj = 128;
// true
System.out.println(j == jj);
- String结构剖析
- 字符串的字符使用 Unicode 字符编码,一个字符占两个字节(不区分字母还是汉字)
- UTF - 8 编码一个字母占 1 个字节,一个汉字占 3 个字节
- String创建剖析
// 方式 1
String s1 = "lcx";
// 方式 2
String s2 = new String("lcx");
- 方式 1:先从常量池查找是否有 “lcx” 数据空间,如果有,直接将 s1 指向 “lcx”;没有则新建后指向,s1 最终指向的是常量池的空间地址
- 方式 2:先在堆中创建空间,维护了 String 的字段 value[] 属性,value 指向常量池的 “lcx” 空间;如果常量池没有 “lcx”,则新建;如果有,则通过 value 指向;s2 最终指向的是堆中的空间地址即 value 的地址
- String测试题1
String a = "lcx";
String b = new String("lcx");
// true:a 指向常量池中 "lcx" 的地址,b.intern() 返回常量池中 "lcx" 的地址
System.out.println(a == b.intern());
// false:b 返回 String 类的字段 value[] 的地址
System.out.println(b == b.intern());
- String测试题2
String s1 = new String("abc");
String s2 = new String("abc");
// false:s1,s2 -> 不同的 value[]
System.out.println(s1 == s2);
// true:引用到常量池的同一个地址
System.out.println(s1.intern()==s2.intern());
- String对象特性1
// 只创建了一个字符串常量 "hello123"
String a = "hello" + "123";
// b -> 常量池的 "hello"
String b = "hello";
// c -> 常量池的 "123"
String c = "123";
// d -> 堆中 value,value 指向常量池中的 "hello123"
// 调用 StringBuilder 的 append 方法连接两次,然后再 new String()
String d = b + c;
- 常量相加看池,变量相加看堆
- String对象特性2
// str 指定堆中的 value,value 指向常量池中的 "lcx"
String str = new String("lcx");
// 字符数组对象存放于堆中
final char[] ch = {
'j','a','v','a'};
public void change(String str, char[] ch) {
str = "java";
ch[0] = 'h';
}
public static void main(String[] args) {
Test test = new Test();
test.change(test.str, test.ch);
// output result: lcx and hava
System.out.print(test.str + " and ");
System.out.println(test.ch);
}
- 内存布局图
-
String常用方法1
-
String常用方法2
- 调用字符串的 replace() 方法若忽略返回值,则原字符串并未发生变化
- 调用 compartTo() 方法时,若其中一个字符串是另一个字符串的子串,则返回长度之差
-
String常用方法3
-
StringBuffer结构剖析
- StringBuffer 也是一个 final 类,但其字段 value[] 不是 final 类型的;StringBuffer 线程安全
- String 保存的是字符串常量,value 引用到常量池;而 StringBuffer 保存的是字符串变量,数据存放在堆中
- StringBuffer转换
- StringBuffer value[] 无参构造器默认初始化容量为 16;若构造器传入的是字符串,则初始化容量为字符串长度加上 16
- StringBuffer方法
String str = null;
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str);
// output:null
System.out.println(stringBuffer);
// NullPointerException
StringBuffer stringBuffer1 = new StringBuffer(str);
System.out.println(stringBuffer1);
-
StringBuffer练习
-
StringBuilder结构剖析
- StringBuilder 线程不安全,与 StringBuffer 使用方法基本一致
-
StringBuilder应用
-
Math方法
-
Arrays排序源码解读
Integer[] arrays = new Integer[]{
1, 31, 523, 452, 13, 64, 23, 75};
Arrays.sort(arrays, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(Arrays.toString(arrays));
- Arrays模拟排序
import java.util.Arrays;
import java.util.Comparator;
/**
* @author Spring-_-Bear
* @version 2021-12-07 20:06
*/
public class Test {
public static void main(String[] args) {
Integer[] arrays = new Integer[]{
1, 31, 523, 452, 13, 64, 23, 75};
Test test = new Test();
// 面向接口编程 + 动态绑定(多态)
test.bubble(arrays, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println(Arrays.toString(arrays));
}
public void bubble(Integer[] arrays, Comparator<Integer> comparator) {
int len = arrays.length;
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - 1 - i; j++) {
if (comparator.compare(arrays[j], arrays[j + 1]) > 0) {
int temp = arrays[j];
arrays[j] = arrays[j + 1];
arrays[j + 1] = temp;
}
}
}
}
}
- Arrays其他方法
- 使用 Arrays.binarySearch() 方法对有序数组进行二分查找时,若不存在该元素,则返回该元素应在数组中位置下标的负值
- Arrays.asList() 可以将数组转换为 List 类型的集合,运行类型为 Arrays$ArrayList,即 Arrays 类中的静态内部类 ArrayList
-
Arrays课堂练习
-
System方法
-
大数处理方案
BigDecimal bigDecimal = new BigDecimal("242131.24321335243234123");
// 为避免结果为无限循环小数,可对结果指定精度以解决 ArithmeticException
BigDecimal res = bigDecimal.divide(new BigDecimal(1.1), BigDecimal.ROUND_CEILING);
System.out.println(res);
-
Date介绍
-
Date应用案例
-
Calendar介绍
- Calendar 构造器私有,可通过 getInstance() 方法获取到一个实例
-
Calendar应用实例
-
第三代日期使用
- JDK 1.0 中出现的 java.util.Date 类,大多数方法在 jdk 1.1 引入的 Calendar 类中已被弃用,但Calendar 也存在着以下一些问题
1. 可变性:像日期和时间这样的类应该是不可变的
2. 偏移性:年份从 1900 开始,月份从 0 开始
3. 格式化:不能对 Calendar 进行格式化
4. 线程不安全,不能处理闰秒(每隔两天,多出 1s)
- JDK 8 引入的第三代日期类:LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期时间)
- 第三代日期方法
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss");
System.out.println(formatter.format(now));
-
String翻转
-
注册处理题
-
字符串统计
-
String内存布局测试题
-
常用类阶段梳理
第14章 集合
-
集合介绍
-
集合体系图
- Set 集合体系图
- List 集合体系图
- Map 集合体系图
- Collection方法
- Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的
// 只要是 Collection 接口的是实现类都可以做实参
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean containsAll(Collection<?> c);
- 迭代器遍历
-
Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素
-
所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象即可以返回一个迭代器
-
Iterator 对象仅用于遍历集合,本身并不存放数据对象
// 得到某个集合的迭代器
Iterator iterator = collection.iterator();
// 判断是否还有下一个元素
while(iterator.hasNext()){
System.out.println(iterator.next());
}
- 集合增强for
- 使用增强 for 循环遍历集合时,底层仍然使用的 Iterator 进行遍历
-
测试题
-
List接口方法
- 实现 List 的集合类中的元素有序(加入顺序与取出顺序一致),允许存在重复
- 支持使用对应的索引值直接获取元素的值
// 在索引为 index 的位置插入 element,后续元素后移
void add(int index, E element);
// 指定起始索引的子集合 [fromIndex,toIndex)
List<E> subList(int fromIndex, int toIndex);
-
List接口练习
-
List三种遍历方式
-
List排序练习
-
ArrayList注意事项
- ArrayList 可以添加多个空元素,底层由 Object[] 数组实现来存储数据
- ArrayList 基本等同于 Vector,除了 ArrayList 是线程不安全的,执行效率比 Vector 高,多线程条件下最好不使用 ArrayList
- ArrayList扩容机制
- ArrayList 中维护了一个 Object 类型的数组 elementData 即 transient Object[] elementData
- 若创建 ArrayList 对象时使用的是无参构造器,则 elementData 初始容量为 0,第 1 次添加后设置容量为 10,此后按照 elementData 容量的 1.5 倍进行扩容
- 若使用的是指定容量大小的构造器,则 elementData 的初始容量为指定大小,需要扩容时按照 1.5 进行扩容
- ArrayList底层源码1
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
-
ArrayList底层源码2
-
Vector注意事项
- Vector 底层存储数据时是一个对象数组即 protected Object[] elementData
- Vector 是线程安全的,在开发中,需要考虑线程同步安全时,考虑使用 Vector 而不是 ArrayList
- Vector源码解读
- 创建 Vector 的对象时若使用无参构造器,则默认初始化容量为 0,当第 1 次添加元素时设置容量大小为 10,此后按 2 倍扩容;若使用了有参构造器,则初始容量为指定大小,此后按照 2 倍扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 双向链表模拟
-
LinkedList 具有双向链表和双端队列特点;可以添加多个 null 元素,允许重复;线程不安全
-
LinkedList 底层维护了一个双向链表,类中有两个属性 transient Node first 和 transient Node last 分别指向头节点和尾节点
// LinkedList 的内部类 Node 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; } }
-
每个节点中又维护了 prev、next、item 三个属性,所以 LinkedList 元素的增加与删除效率较高
- LinkedList源码图解
// 添加新节点到表尾
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
// 移除头节点
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
-
List集合选择
-
Set接口方法
- Set 中的元素无序,没有索引,因而只有两种遍历方式
- Set 不允许重复元素,故最多只包含一个 null
- HashSet全面说明
- HashSet 的底层使用的是 HashMap,HashMap 底层是(数组 + 链表 + 红黑树)
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
- 数组链表模拟
// 确定元素 hash 值的算法
static final int hash(Object key) {
int h;
// key 的 hashCode 与 key 无符号右移 16 位的值进行按位异或 ^ 运算得到元素得 hash 值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 元素存放过程:先通过 hash(Object) 方法获得本次元素的 hash 值,将 hash 值与本次哈希表大小 -1 的值进行按位与运算获得此元素在哈希表中的位置号,如果该位置上没有其它元素则直接存放;如果存在,则遍历链表判断是否已经存在相同元素,如果存在则不添加并且返回元素值,否则创建新的节点连接到链表尾(判断两个元素是否相同的条件:当前元素的 hash 值与哈希表位置号上元素的 hash 值相同且值相同(引用相同)或者 要添加的元素不为空且 equals 比较相同)
- HashSet扩容机制
- 在 Java 8 中,如果哈希表的大小 >= 64 且某条链表的长度 >= 8 则会将该条链表树化为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
static final int TREEIFY_THRESHOLD = 8;
- HashSet源码解读1
- HashMap 为键值对,所以在 HashSet 采用 HaspMap 实现存储时,value 为系统给定的 PRESENT
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
- HashSet源码解读2
- HashMap 实现添加元素的方法
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table 为 HashMap 属性,Node 类型的数组,进行第一次扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 用本次哈希表长度减 1 与本次元素的 hash 值进行按位与运算获得元素在哈希表中的位置号
if ((p = tab[i = (n - 1) & hash]) == null)
// 位置号上未存储元素则直接存放
tab[i] = newNode(hash, key, value, null);
else {
// 判断是否存在相同的元素,不存在则存放到此条链表的末尾
Node<K,V> e; K k;
// 如果当前元素的 hash 值与哈希表位置号上元素的 hash 值相同且值相同(引用相同)或者 要添加的元素不为空且 equals 比较相同,则不添加
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断当前位置号是否是红黑树数据结构,是则按照红黑树方式添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 遍历此位置号上的链表元素,判断是否与当前需要加入的元素相同
for (int binCount = 0; ; ++binCount) {
// 不相同,添加到链表尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断该条链表上的元素个数是否 >= 8个,是则进行是否树化判断
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果存在相同的元素则不添加
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 存在相同元素,则不添加,返回旧值
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// size 加入到哈希表中的元素个数
// 判断加入的元素个数是否不小于临界值,是则对哈希表进行扩容
if (++size > threshold)
resize();
// HashMap 的空方法,留给子类实现以扩展功能
afterNodeInsertion(evict);
// 返回 null 代表元素添加成功
return null;
}
- HashSet源码解读3
- HashMap 实现扩容的方法:第一次添加时 table 扩容到 DEFAULT_INITIAL_CAPACITY 16,加载因子为 loadFactor 0.75,临界值为 threshold 12,如果 table 已添加 threshold 个元素,则 table 按照 2 倍方式扩容到 32,临界值为 24,依次类推
- 如果 table 大小不小于 MIN_TREEIFY_CAPACITY 64 且某条链表的长度不小于 TREEIFY_THRESHOLD 8,则将该条链表树化为红黑树
- HashSet源码解读4
// 判断加入的元素个数是否不小于临界值,是则对哈希表进行扩容
if (++size > threshold)
resize();
-
HashSet最佳实践
-
HashSet思考题
-
LinkedHashSet介绍
- LinkedHashSet 底层使用 LinkedHashMap,底层维护了一个 数组 + 双向链表
- LinkedHashSet 根据元素的 hashCode 值来决定元素都得存储位置,同时使用链表维护元素的次序,使得元素看起来是以插入顺序保存的;不允许添加重复元素
- LinkedHashSet 有 head 和 tail 指针,使用使用头尾指针遍历 linkedHashSet 时取出元素的顺序与插入元素的顺序一致
- LinkedHashSet 在添加一个元素时,先求 hash 值,再求在哈希表中的索引号,然后判断元素是否添加
- LinkedHashSet源码解读
- LinkedHashSet 底层数组的类型是 HashMap$Node[],其中存放的元素是 LinkedHashMap$Entry 类型
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V>
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V>
-
LinkedHashSet课堂练习
-
Map接口特点1
- Map 与 Collection 并列存在,用于保存具有映射关系的键值对:key - value
- Map 中的 key 和 value 可以是任何引用类型,k - v 会封装到 HashMap$Node 对象中,因为 Node 实现了 Entry 接口
- Map 中的 key 不允许重复,value 可以重复;key 最多有一个 null,value 可以有多个 null;当 key 相同而 value 不同时,用新的 value 替换旧的 value
- Map接口特点2
- key - value 的值存放到 HashMap$Node 中,为单独方便遍历 key 或 value,使用 KeySet(Set 类型) 引用到 Node 中所有的 key,使用 Values (Collection 类型)引用到所有的 value;将对应的 key - value 封装到 Entry 中,再将 Entry 存放到 EntrySet 中
// EntrySet
final class EntrySet extends AbstractSet<Map.Entry<K,V>>
// KeySet
final class KeySet extends AbstractSet<K>
// Values
final class Values extends AbstractCollection<V>
- EntrySet 使用示例
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("李春雄", 21);
// 通过 Entry 遍历
Set<Map.Entry<Object, Object>> entries = hashMap.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
System.out.println(entry);
System.out.println("key = " + entry.getKey());
System.out.println("value = " + entry.getValue());
}
// 通过 KeySet 遍历所有的 key
Set<Object> keySet = hashMap.keySet();
for (Object object : keySet) {
System.out.println("key = " + object);
}
// 通过 Values 遍历所有的 value
Collection<Object> values = hashMap.values();
for (Object value : values) {
System.out.println(value);
}
-
Map接口方法
-
Map六大遍历方式
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("李春雄", 21);
// 使用增强 for 的地方也可以使用迭代器
// 1. 通过 EntrySet 遍历
Set<Map.Entry<Object, Object>> entrySet = hashMap.entrySet();
for (Map.Entry<Object, Object> entry : entrySet) {
System.out.println(entry);
}
// 2. 通过 KeySet 遍历
Set<Object> keySet = hashMap.keySet();
for (Object key : keySet) {
System.out.println(key + " - " + hashMap.get(key));
}
// 3. 使用 Map.Entry 的 getKey() 和 getValue() 方法
for (Map.Entry<Object, Object> entry : entrySet) {
System.out.println(entry.getKey() + " - " + entry.getValue());
}
-
Map课堂练习
-
HashMap阶段小结
- key - value 封装在 HashMap$Node;HashMap 线程不安全
// key 相同而 value 不同时的替换 value 的机制
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
- HashMap底层机制
- JDK 7.0 的 HashMap 底层实现是(哈希表 + 链表),JDK 8.0 是(哈希表 + 链表 + 红黑树)
- 若某颗红黑树元素个数较少,则会触发剪枝行为即将红黑树转换为链表
-
HashMap源码解读
-
HashMap扩容树化触发
-
Hashtable使用
- Hashtable 的键和值都不能为 null,为 null 则抛出 NullPointerException,使用方法与 HashMap 基本一致;Hashtable 线程安全,HashMap 线程不安全
- Hashtable扩容
- Hashtable 初始化容量大小为 11,数组类型为 Hashtable$Entry;加载因子 loadFactor 0.75;按原有容量的 2 倍加 1 的机制进行扩容
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
-
Properties
-
集合选型规则
- TreeSet源码解读
// 排序使用示例
TreeSet<String> treeSet = new TreeSet<>(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o2).length() - ((String) o1).length();
}
});
treeSet.add("a");
treeSet.add("bb");
treeSet.add("ccc");
System.out.println(treeSet);
- TreeSet 实现排序
// TreeMap 的 put(K,V) 方法
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
-
TreeMap源码解读
-
Collections工具类1
- Collections 是一个操作 Set、List、Map 等集合的工具类,提供了一系列的静态方法对集合进行操作
方法名 | 功能 |
---|---|
reverse(List) | 反转 List 中的元素顺序 |
shuffle(List) | 对 List 中的元素进行随机排序 |
sort(List) | 按元素自然顺序升序方式对 List 中的元素排序 |
sort(List,Comparator) | 根据 Comparator 的顺序对 List 中的元素排序 |
swap(List,int,int) | 对 List 中的两个元素交换顺序 |
- Collections工具类2
方法名 | 功能 |
---|---|
max(Collection) | 自然顺序找出集合中的最大元素 |
max(Collection,Comparator) | 指定排序找出集合中的最大元素 |
frequency(Collection, Object) | 某个元素在集合中的出现次数 |
copy(List,List) | List 拷贝 |
replaceAll(List,Object,Object) | 用新的 Object 替换指定的 Object |
-
集合家庭作业1
-
集合家庭作业2
-
集合家庭作业3
-
集合家庭作业4
- HashSet 的去重机制:获得对象的 hashCode 值,经过 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16) 运算得到该对象的 hash 值,用 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 判断是否添加
- TreeSet 的去重机制:如果传入了 Comparator 匿名对象,则使用实现的 compare 方法中的比较方式去重,compare 方法返回 0 则认为是相同的元素;如果没有传入匿名对象,则以所添加的对象实现的 Comparaeable 接口的 compareTo 方法比较机制去重
- 集合家庭作业5
import java.util.HashSet;
import java.util.Objects;
/**
* @author Spring-_-Bear
* @version 2021-12-06 20:50
*/
public class Homework05 {
public static void main(String[] args) {
HashSet<Person> personHashSet = new HashSet<>();
// aa hash 值 34072,aa.setName("CC") 后 hash 值不发生变化
Person aa = new Person(1001, "AA");
Person bb = new Person(1002, "BB");
personHashSet.add(aa);
personHashSet.add(bb);
// remove 时 aa(1001,"CC") hash 值 34136,故删除失败
personHashSet.remove(aa);
// new Person(1001, "CC") hash 值:34136 故添加成功
personHashSet.add(new Person(1001, "CC"));
// 集合中已此元素,故添加成功
personHashSet.add(new Person(1001, "AA"));
System.out.println(personHashSet);
}
}
class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return id == person.id && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
- 集合内容梳理
第15章 泛型
- 泛型引入
- 不使用泛型的弊端:
- 不能对加入到集合中的数据类型进行约束(不安全)
- 遍历集合时,需要进行显式类型转换,如果集合中数据量很大,效率低下
- 使用泛型的好处:
- 编译时,检查元素的类型,提高了安全性
- 减少了类型转换的次数,提高了运行效率
-
泛型入门
-
泛型说明
- 泛型又称参数化类型,是 jdk 5.0 出现的新特性,解决数据类型的安全性问题
- 只需在类声明或实例化时执行具体需要的类型即可,编译期即可确定类型
- Java 泛型保证在编译期没有发出警告的代码,在运行阶段就不会发生类转换异常,使得代码更加简洁与健壮
- 泛型的作用:可以在类声明时通过一个标识符表示类中某个属性的类型,或者是某个方法的返回值类型,或者是参数类型
class Temp<E> {
private E e;
public Temp(E e) {
this.e = e;
}
public E m() {
return e;
}
}
- 泛型应用实例
import java.util.*;
/**
* @author Spring-_-Bear
* @version 2021-11-23 22:00
*/
public class GenericEx01 {
public static void main(String[] args) {
HashMap<String, Student> hashMap = new HashMap<>(5);
hashMap.put("mary", new Student("mary", 2));
hashMap.put("kim", new Student("kim", 23));
Set<Map.Entry<String, Student>> entries = hashMap.entrySet();
Iterator<Map.Entry<String, Student>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Student> next = iterator.next();
System.out.println(next.getKey() + " - " + next.getValue());
}
}
}
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
- 泛型使用细节1
- 指定泛型类型时只能是引用类型
- 在给泛型指定具体类型后,可以传入该类型或其子类类型
- 省略指定泛型类型时,默认填充 Object
-
泛型使用细节2
-
泛型课堂练习
-
自定义泛型类
class ClassName<T,R,E···>{
}
- 普通成员可以使用泛型(属性、方法)
- 使用泛型的数组,不能进行初始化
- 静态方法、静态字段中不能使用类的泛型
- 泛型类的类型,是在创建对象时确定的
- 如果在创建对象时,没有指定类型,默认为 Object
- 自定义泛型接口
- 泛型接口的类型,在继承接口或是实现接口时确定
- 自定义泛型方法
- 泛型方法可以定义在普通类中、也可以定义在泛型类中:调用方法时确定泛型类型
-
泛型方法练习
-
泛型继承和通配
- 泛型不具备继承性,即以下代码语法错误
// List<Object> lists = new ArrayList<String>();
- <?>:支持任意泛型类型
- <? extends A>:支持 A 类以及 A 的子类,规定了泛型的上限
- <? super A>:支持 A 类以及 A 的父类,不限定于直接父类,规定了泛型的下限
-
Junit使用
-
泛型家庭作业
-
泛型内容梳理
第16章 坦克大战1
-
坦克大战介绍
-
Java坐标体系
-
绘图入门和机制
-
绘图方法
-
绘制坦克游戏区域
-
绘制坦克
-
小球移动案例
-
事件处理机制
-
绘制坦克上右下左
-
坦克动起来
-
绘制敌人坦克
第17章 多线程编程
- 程序进程线程
- 进程是程序的一次执行过程,或是正在进行的一个程序。是动态过程,有其自身的产生、存在和消亡过程
- 线程是由进程创建的,是进程的一个实体;一个进程可以拥有多个线程
- 并发并行
- 并发:同一个时刻,多个任务交替执行,造成一种 “貌似同时” 的错觉,简单地说,单核 CPU 实现的多任务就是并发
- 并行:同一个时刻,多个任务同时执行,多核 CPU 实现的多任务就是并行
// 获取系统可用处理器个数
Runtime runTime = Runtime.getRuntime();
System.out.println(runTime.availableProcessors());
- 继承Thread类创建线程
- 创建线程的两种方式:继承 Thread 类并重写 run() 方法;实现 Runnable 接口,重写 run() 方法
/**
* @author Spring-_-Bear
* @version 2021-11-21 19:53
*/
public class ThreadEx extends Thread {
public static void main(String[] args) {
new Dog().start();
for (int i = 1; i <= 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + i);
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Dog extends Thread {
@Override
public void run() {
while (true) {
try {
System.out.println(Thread.currentThread().getName() + ":汪汪汪~~~");
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 当启动程序时理解为开启了一个进程,进程开启了 main 线程,在 main 线程中可以开启其它线程,只有当所有线程都消亡时,进程才结束
-
多线程机制
-
为什么是start
- new Dog().run() 与 new Dog().start() 的区别:前者只是单纯地调用 run() 方法,而后者启用了线程,在 start() 方法中实际调用了 start0() 方法,这是个 native 修饰的方法,真正实现了启动线程
- start() 方法调用了 start0() 方法后,该线程并不一定会马上执行,只是将线程变成了可运行状态,具体什么时候执行,取决于 CPU
- Runnable创建线程
- Java 是单继承机制,若某个类已经继承了某个父类,这时只能通过实现 Runnable 接口实现线程
- 实现 Runnable 接口的类不能直接调用 start() 方法,可以将对象作为 Thread(Runnable) 的参数,从而调用 start() 方法,也即使用了静态代理设计模式
/**
* @author Spring-_-Bear
* @version 2021-11-21 20:50
*/
public class Thread02 {
public static void main(String[] args) {
// 静态代理设计模式
new Thread(new Cat()).start();
}
}
class Cat implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("喵喵喵~~~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 多个子线程案例
- 实现 Runnable 接口方式更加适合多个线程共享某个资源的情况,并且避免了单继承的显示
Cat cat = new Cat();
Thread thread1 = new Thread(cat);
Thread thread2 = new Thread(cat);
- 多线程售票问题
/**
* @author Spring-_-Bear
* @version 2021-11-21 21:48
*/
public class Ticket {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
Thread thread = new Thread(sellTicket);
Thread thread1 = new Thread(sellTicket);
Thread thread2 = new Thread(sellTicket);
thread.start();
thread1.start();
thread2.start();
}
}
class SellTicket implements Runnable{
private int ticketNum = 100;
@Override
public void run() {
while (true) {
if (ticketNum <= 0) {
System.out.println("售票结束···");
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + "售出一张票,余票:" + (--ticketNum));
}
}
}
- 通知线程退出
- 通过使用变量来控制 run 方法退出的方式停止线程,即通知方式
- 线程中断
方法名 | 功能 |
---|---|
setName(String) | 设置线程名 |
getName() | 获取线程名 |
start() | 启动线程 |
run() | 调用线程对象的 run() 方法 |
setPriority(int) | 设置线程优先级 |
getPriority() | 获得线程优先级 |
sleep(long) | 休眠线程 |
interrupt() | 中断线程,线程并未消亡 |
- 线程优先级
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
- 线程插队
方法名 | 功能 |
---|---|
yield() | 线程礼让,让出 CPU 资源,但礼让时间不确定,是否礼让成功不确定,取决于 os |
join() | 线程插队,一旦插队成功,则必须执行完插入线程的所有任务 |
- 线程插队练习
/**
* @author Spring-_-Bear
* @version 2021-11-21 22:49
*/
public class JoinEx {
public static void main(String[] args) throws InterruptedException {
Thread temp = new Thread(new Temp());
temp.start();
for (int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(1000);
if (i == 5) {
temp.join();
// main 线程礼让,不一定礼让成功
// Thread.yield();
}
}
}
}
class Temp implements Runnable{
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 守护线程
- 用户线程:也叫工作线程,当线程的任务执行完成或以通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束。经典守护线程:垃圾回收机制
/**
* @author Spring-_-Bear
* @version 2021-11-22 19:10
*/
public class MyDaemon {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Daemon());
// 设置为 main 线程的守护线程
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("妈妈回家了,小明结束写作业!");
}
}
class Daemon implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("小明写作业中···");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 线程7大状态
状态 | 说明 |
---|---|
NEW | 尚未启动的线程处于此状态。 该状态线程对象被创建,但还未调用start方法 |
RUNNABLE | 可运行线程的线程状态,细分为 Running 和 Ready 两个状态。处于可运行状态的线程可能正在 Java 虚拟机中执行,也可能正在等待来自操作系统的其他资源 |
BLOCKED | 被阻塞等待监视器锁的线程处于此状态。处于该状态的线程正在等待获取一个监视器锁进入同步代码或方法;也可能是在调用了Object.wait后等待一个监视器锁重新进入同步代码或方法 |
WAITING | 线程处于等待状态;处于该状态的线程可能是因为调用了Object.wait()、Thread.join()、LockSupport.park() 中的某一个方法;处于该状态的线程正在等待其他线程完成一些特定的操作 |
TIMED_WAITING | 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。线程处于定时等待状态,这个等待是有具有指定时间的;处于这个状态的线程可能是调用了具有指定时间的 Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil() |
TERMINATED | 已退出的线程处于此状态 |
- 线程同步机制
-
线程同步:当有一个线程在对内存进行操作时,其它线程都不可以对这个内存进行操作,直到该线程完成操作,其它线程才能对该内存进行操作,也即同一时刻只允许一个线程操作
-
得到对象的锁,才可以操作对象的代码;也可以将 synchronized 加在方法声明中,表示整个方法为同步方法
- 互斥锁
- Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻,有且仅有一个线程可以访问该对象。用关键字 synchronized 来与对象的互斥锁联系
- 同步的局限性:程序的执行效率降低
- 非静态同步方法的锁可以是它本身 this,也可以是其它对象(要求是同一个对象)
- 静态同步方法的锁是当前类本身(ClassName.class)
/**
* @author Spring-_-Bear
* @version 2021-11-21 21:48
*/
public class Ticket {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
Thread thread1 = new Thread(sellTicket);
Thread thread2 = new Thread(sellTicket);
Thread thread3 = new Thread(sellTicket);
thread1.start();
thread2.start();
thread3.start();
}
}
class SellTicket implements Runnable {
private int ticketNum = 100;
Object object = new Object();
@Override
public void run() {
sell();
}
public void sell() {
while (true) {
synchronized (this) {
if (ticketNum <= 0) {
System.out.println("售票结束···");
break;
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票,余票:" + (--ticketNum));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/*
* 由于主方法中只创建了一个 SellTicket 对象,所以线程 0、1、2 操作的都是同一个对象
* 因而将锁加在对象本身等价于将锁将在此对象的其它对象(字段),也即下面的两段代码等价
* synchronized (this){} <=> synchronized (object){}
*/
- 静态方法中的互斥锁加在类上
class SellTicket implements Runnable {
public static void getTicket() {
synchronized (SellTicket.class) {
System.out.println("售出一张票");
}
}
}
- 线程死锁
/**
* @author Spring-_-Bear
* @version 2021-11-22 21:02
*/
public class DeadLockDemo {
public static void main(String[] args) {
new DeadLock(true).start();
new DeadLock(false).start();
}
}
class DeadLock extends Thread {
static Object object1 = new Object();
static Object object2 = new Object();
boolean flag;
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + " 获得对象 1 的锁,尝试获取对象 2 的锁···");
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + " 成功获得对象 2 的锁!");
}
}
} else {
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + " 成功获得对象 2 的锁,尝试获取对象 1 的锁···");
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + " 成功获得对象 1 的锁!");
}
}
}
}
}
- 释放锁
- 当前线程的同步方法、同步代码块执行结束会释放锁
- 当前线程在同步方法、同步代码块中遇到 break、return 会释放锁
- 当前线程在同步方法、同步代码块中出现了未处理的 Error 或 Exception 导致结束会释放锁
- 当前线程在同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
- 当前线程在执行同步代码块、同步方法的过程中调用了 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行,不会释放锁
- 线程执行同步代码块时,其它线程调用了该线程的 suspend() 方法将该方法挂起,该线程不会释放锁。suspend()、resume() 方法均已过时,不再推荐使用
-
线程家庭作业1
-
线程家庭作业2
第18章 坦克大战2
-
坦克发子弹思路
-
我方发射子弹1
-
我方发射子弹2
-
敌方发射子弹
-
敌方坦克消失
-
坦克爆炸
-
敌方坦克自由移动
-
控制坦克移动范围
-
坦克发多颗子弹
-
敌方移动发射
-
我方被击中爆炸
第19章 IO流
- 文件基础知识
- 流:数据在数据源(文件)和程序(内存)之间经历的路径
- 以程序(内存)为参照,流入内存为输入流,流出内存为输出流
- 创建文件
方法名 | 功能 |
---|---|
new File(String) | 根据路径构建一个 File 对象 |
new File(File,String) | 根据父目录文件 + 子路径构建 |
new File(String,String) | 根据父目录 + 子路径构建 |
- 获取文件信息
方法名 | 功能 |
---|---|
getName() | 获取文件名,包含文件扩展名 |
getAbsolutePath() | 获取文件绝对路劲 |
getParent() | 获取文件父级目录 |
length() | 获取文件大小,以字节为单位 |
exists() | 判断是否存在 |
isFile() | 判断是否是文件 |
isDirectory() | 判断是否是目录 |
- 目录操作
方法名 | 功能 |
---|---|
mkdir() | 创建一级目录 |
mkdirs() | 创建多级目录 |
delete() | 删除空目录或文件 |
- IO流原理和分类
- 分类:
- 按操作数据单位不同分为字节流和字符流
- 按数据流的流向不同分为输入流和输出流
- 按流的角色的不同分为字节流和处理流(包装流)
- 抽象基类:InputStream(字节输入流)、 OutputStream(字节输出流)、Reader(字符输入流)、 Writer(字符输出流)
- FileInputStream
- InputStream 的常用子类:FileInputStream、BufferedInputStream、ObjectInputSteam
- FileOutputStream
- OutputStream 的常用子类:FileOutputStream、BufferedOutputStream、ObjectOutputStream
- 文件拷贝
String src = "C:\\Users\\Admin\\Desktop\\BeFree.jpg";
String dst = "C:\\Users\\Admin\\Desktop\\BeFree.png";
FileInputStream fileInputStream = new FileInputStream(src);
// 无需追加写入,因为并未再次打开文件
FileOutputStream fileOutputStream = new FileOutputStream(dst);
int readLen;
byte[] buf = new byte[1024];
while ((readLen = fileInputStream.read(buf)) != -1) {
fileOutputStream.write(buf, 0, readLen);
}
fileInputStream.close();
fileOutputStream.close();
- 文件字符流说明
- FileWriter 使用后,必须 close() 或 flush(),否则数组只是写在缓冲区,未写入文件
- 字符流底层调用字节流实现具体功能
-
FileReader
-
FileWriter
-
节点流处理流
- 节点流可以从一个特定的数据源读写数据,如 FileReader、FileWriter
- 处理流(包装流)是 “连接” 在已存在的流,为程序提供了更为强大的读写功能,如 BufferedReader、BufferedWriter
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputSteam | ByteArrayOutputStram | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PirntStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
- 处理流设计理念体现了修饰器模式。列表中前 5 行为节点流,其余行对应的流为包装流
- 处理流设计模式
- 节点流是底层流,直接对数据源进行操作
- 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更加方便的方法来完成文件操作,底层调用节点流实现具体功能。优点主要有以下两点:1. 性能的提高:主要以增加缓冲的方式来提高输入输出的效率;2. 操作的便捷:提供了一系列便捷的方法来一次输入、输出大批量的数据(比如 BufferedReader 可以一次读取一行,而 FileReader 最多以字符数组形式进行读取)
- 使用处理流时,只需关闭外层流即可,对应的节点流会自动关闭
- BufferedReader
String src = "C:\\Users\\Admin\\Desktop\\Java.txt";
BufferedReader bufferedReader = new BufferedReader(new FileReader(src));
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
// 无需关闭节点流,底层自动关闭
bufferedReader.close();
-
BufferedWriter
-
Buffered拷贝
-
Buffered字节处理流
-
字节处理流拷贝文件
- BufferedInputStream(new FileInputStream(String)) 缓冲处理本质上与 FileInputStream(String) 一致,并无特殊提高读写效率之处
- 对象处理流
- 序列化与反序列化:Java 序列化 是指把 Java 对象转换为字节序列的过程;而 Java 反序列化 是指把字节序列恢复为 Java 对象的过程
- 若某个对象需支持序列化机制,则必须让其类是可序列化的,也即必须实现 Serializable 或 Externalizable 接口其中之一;其中 Serializable 是标记接口,无需实现任何方法;而 Externalizable 继承自 Serializable,需要实现方法
-
ObjectOutputStream
-
ObjectInputStream
String path = "C:\\Users\\Admin\\Desktop\\out.dat";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(path));
objectOutputStream.writeUTF("Spring-_-Bear");
objectOutputStream.writeObject(new People(11, "小明"));
objectOutputStream.close();
// 注意读入顺序应与写入顺序一致
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
String str = objectInputStream.readUTF();
Object people = objectInputStream.readObject();
System.out.println(str);
System.out.println(people);
objectInputStream.close();
- 对象处理流使用细节
- 序列化的类中建议添加 SerialVersionUID,为了提高版本的兼容性:private static final long SerialVersionUID = 1L
- 序列化对象时,默认将除了 static、transient(让某些被修饰的成员不被序列化) 修饰的成员以外的所有成员进行序列化保存
- 序列化对象时,要求类的所有属性的类型也需要实现序列化接口
- 序列化具备可继承性,实现的序列化接口的父类,其子类默认可序列化
- 标准输入输出流
- 标准输入:System.in 编译类型是 InputStream,运行类型是 BufferedInputStream
public final static InputStream in = null;
// class java.io.BufferedInputStream
System.out.println(System.in.getClass());
- 标准输出:System.out 编译类型是 PrintStream,运行类型也是 PrintStream
public final static PrintStream out = null;
// class java.io.PrintStream
System.out.println(System.out.getClass());
-
乱码引出转换流
-
InputStreamReader
- InputStreamReader -> Reader:可以将 InputStream(字节流)包装成 Reader(字符流),同时指定编码
- OutputStreamWriter -> Writer:可以将 OutputStream(字节流)包装成 Writer (字符流),同时指定编码
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c:\\users\\admin\\desktop\\temp1.txt"), StandardCharsets.UTF_8));
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
bufferedReader.close();
-
OutputStreamWriter
-
PrintStream
PrintStream printStream = System.out;
printStream.print("Hello World!");
// print 方法底层调用了 write 方法,因此等价
printStream.write("Hello Java!".getBytes(StandardCharsets.UTF_8));
printStream.close();
// 重定向输出设备
System.setOut(new PrintStream("c:\\users\\admin\\desktop\\1.txt"));
System.out.println("Spring-_-Bear");
- PrintWriter
- 设计字符输出到文件时需要 close() 或 flush(),否则内容不会写入到文件
-
配置文件引出Propeties
-
Propeties读文件
- 键值对不需要有空格,值不需要引号,默认是 String
方法名 | 功能 |
---|---|
load(InputStream | Reader) | 加载配置文件的键值对 |
list(PrintStream | PrintWriter) | 将数据显示到指定设备 |
getProperty(String) | 根据键获取值 |
setProperty(String,String) | 设置键值对到 Propertiles 对象 |
store(OutputStream | Writer,String) | 保存到配置文件,含有中文默认保存 unicode 码值 |
-
Propeties修改文件
-
家庭作业1
-
家庭作业2
-
家庭作业3
第20章 坦克大战3
-
防止坦克重叠思路
-
击中坦克爆炸完善
-
防止敌人坦克重叠1
-
防止敌人坦克重叠2
-
防止敌人坦克重叠3
-
记录玩家成绩1
-
记录玩家成绩2
-
记录敌人坦克信息
-
继续上局游戏1
-
继续上局游戏2
-
坦克大战音乐
-
坦克大战1内容梳理
-
线程基础内容梳理
-
坦克大战2内容梳理
-
IO流内容梳理
-
坦克大战3内容梳理
-
第二阶段结束语
第21章 网络编程
-
网络相关概念
-
IP地址
- IP 地址概念: 用于唯一标识网络中的每台计算机(主机)
- DOS 查看 IP 地址:ipconfig
- IPV4 用 4 字节共 32 位标识一个 IP 地址(点分十进制)
- IPV6 用 8 字节共 128 位标识一个 IP 地址
- IP 地址的组成:网络地址 + 主机地址
- IPV4 的分类:127.0.0.1 表示本机地址
分类 | 表示方法 | 范围 |
---|---|---|
A | 0 + 7 位网络号 + 24 位主机号 | 0.0.0.0 ~ 127.255.255.255 |
B | 10 + 14 位网络号 + 16 位主机号 | 128.0.0.0 ~ 191.255.255.255 |
C | 110 + 21 位网络号 + 8 位主机号 | 192.0.0.0 ~ 223.255.255.255 |
D | 1110 + 28 位多播组号 | 224.0.0.0 ~ 239.255.255.255 |
E | 11110 + 27 位留待后用 | 240.0.0.0 ~ 247.255.255.255 |
- 域名和端口
- 域名:将 IP 地址映射成域名(http 协议)
- 端口:用于标识计算机上特定的网络程序,以整数形式表示,范围是 0 ~ 65535,其中 0 ~ 1024 已被占用。常见的网络程序端口:ssh 22;ftp 21;smtp 25;http 80;tomcat 8080;mysql 3306;oracle 1521;sql server 1433
- 网络协议
- TCP / IP(Transmission Control Protocol / Internet Protocol):传输控制协议 / 因特网互联协议,又名网络通讯协议
TCP/IP模型 | 对应协议 |
---|---|
应用层 | http、ftp、telnet、DNS··· |
传输层 | TCP、UDP··· |
网络层 | IP、ICMP、ARP··· |
物理 + 数据链路层 | Link |
- TCP和UDP
- TCP(Transmission Control Protocol):在使用 TCP 协议前,须先建立 TCP 连接,形成传输数据通道;传输前,采用 ”三次握手“ 方式建立连接,传输可靠;TCP 协议进行通信的两个应用程序:客户端、服务端;在连接中可进行大量数据的传输;传输完毕,需释放已建立的连接,效率较低
- UDP(User Data Protocol):将数据、源、目的封装成数据包,不需要建立连接,传输不可靠,无需释放资源,速度快;每个数据包的大小限制在 64K 以内
- InetAddress
方法名 | 功能 |
---|---|
getLocalHost | 获取本机 InetAddress 对象 |
getByName | 根据指定主机名 / 域名获取 InetAddress 对象 |
getHostName | 获取 InetAddress 对象的主机名 |
getHostAddress | 获取 InetAddress 对象的 ip 地址 |
// 获取本机的 InetAddress 对象
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
// 根据指定主机名/域名获取 InetAddress 对象
InetAddress csdn = InetAddress.getByName("www.csdn.net");
System.out.println(csdn);
InetAddress springBear = InetAddress.getByName("SpringBear");
System.out.println(springBear);
// 获取 InetAddress 对象的主机名
System.out.println(localHost.getHostName());
System.out.println(csdn.getHostName());
// 获取 InetAddress 对象的 ip 地址
System.out.println(springBear.getHostAddress());
System.out.println(csdn.getHostAddress());
- Socket
- Socket(套接字)开发网络应用程序时被广泛使用,以至于成为事实上的标准;通信的两端都要有 Socket,是两台机器间通信的端点;网络通信其实就是 Socket 间的通信;Socket 允许程序把网络连接当作是一个流,数据在两个 Socket 间通过 IO 传输;一般发起通信的应用程序属客户端,等待通信请求的为服务端
- TCP字节流编程1
- Server
// 监听端口
ServerSocket serverSocket = new ServerSocket(4444);
// 等待连接
Socket socket = serverSocket.accept();
// 从数据通道中读取数据
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen;
while ((readLen = inputStream.read(buf)) != -1) {
System.out.println(new String(buf, 0, readLen));
}
// 关闭资源
inputStream.close();
socket.close();
serverSocket.close();
- Client
// 建立连接
Socket socket = new Socket(InetAddress.getLocalHost(), 4444);
// 写数据到数据通道
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello,Server!".getBytes(StandardCharsets.UTF_8));
// 关闭资源
outputStream.close();
socket.close();
- TCP字节流编程2
- Server
// 监听端口
ServerSocket serverSocket = new ServerSocket(5555);
// 等待连接
Socket socket = serverSocket.accept();
// 从数据通道读取数据
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen;
while ((readLen = inputStream.read(buf)) != -1) {
System.out.println(new String(buf, 0, readLen));
}
// 关闭 socket 的输入流
socket.shutdownInput();
// 写数据到数据通道
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello,Client!".getBytes(StandardCharsets.UTF_8));
// 关闭资源
outputStream.close();
inputStream.close();
socket.close();
serverSocket.close();
- Client
// 建立连接
Socket socket = new Socket(InetAddress.getLocalHost(), 5555);
// 写数据到数据通道
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello,Server!".getBytes(StandardCharsets.UTF_8));
// 关闭 socket 的输出流
socket.shutdownOutput();
// 从数据通道读取数据
InputStream inputStream = socket.getInputStream();
byte[] buf = new byte[1024];
int readLen;
while ((readLen = inputStream.read(buf)) != -1) {
System.out.println(new String(buf, 0, readLen));
}
// 关闭资源
inputStream.close();
outputStream.close();
socket.close();
- TCP字符流编程
- Server
// 监听端口
ServerSocket serverSocket = new ServerSocket(6666);
// 等待连接
Socket socket = serverSocket.accept();
// 从数据通道读取信息
InputStream inputStream = socket.getInputStream();
// 字节输入流转换成字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println(bufferedReader.readLine());
// 写数据到数据通道
OutputStream outputStream = socket.getOutputStream();
// 字节输出流转换成字符输出流
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("Hello,Client!");
// 行分隔符,写入结束标志
bufferedWriter.newLine();
// 刷新缓冲区,写入数据通道
bufferedWriter.flush();
// 关闭资源
bufferedWriter.close();
bufferedReader.close();
socket.close();
serverSocket.close();
- Client
// 建立连接
Socket socket = new Socket(InetAddress.getLocalHost(), 6666);
// 写入数据到数据通道
OutputStream outputStream = socket.getOutputStream();
// 字符输出流转换从字节输出流
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("Hello,Server!");
// 行分隔符,写入结束标记
bufferedWriter.newLine();
// 刷新缓冲区,写入数据通道
bufferedWriter.flush();
// 从数据通道读取数据
InputStream inputStream = socket.getInputStream();
// 字节输入流转换成字符输入流
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
System.out.println(bufferedReader.readLine());
// 关闭资源
bufferedReader.close();
bufferedWriter.close();
socket.close();
-
网络上传文件1
-
网络上传文件2
-
网络上传文件3
- Server
File saveFilePath = new File("C:\\Users\\Admin\\Desktop\\temp.jpg");
// 从数据通道中读取源文件字节数组
ServerSocket serverSocket = new ServerSocket(5555);
Socket socket = serverSocket.accept();
InputStream socketInputStream = socket.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen;
byte[] buf = new byte[1024];
while ((readLen = socketInputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, readLen);
}
buf = byteArrayOutputStream.toByteArray();
socket.shutdownInput();
// 将接收到的字节数组写入磁盘
FileOutputStream fileOutputStream = new FileOutputStream(saveFilePath);
fileOutputStream.write(buf);
// 向客户端反馈消息
OutputStream socketOutputStream = socket.getOutputStream();
socketOutputStream.write("Receive file successfully!".getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput();
socketOutputStream.close();
fileOutputStream.close();
byteArrayOutputStream.close();
socketInputStream.close();
socket.close();
serverSocket.close();
- Client
File sourceFilePath = new File("C:\\Users\\Admin\\Desktop\\fishing.jpg");
// 从磁盘中读取源文件存入字节数组
int readLen;
byte[] buf = new byte[1024];
FileInputStream fileInputStream = new FileInputStream(sourceFilePath);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
while ((readLen = fileInputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, readLen);
}
buf = byteArrayOutputStream.toByteArray();
// 发送源文件字节数组到数据通道
Socket socket = new Socket(InetAddress.getLocalHost(), 5555);
OutputStream socketOutputStream = socket.getOutputStream();
socketOutputStream.write(buf);
socket.shutdownOutput();
// 从数据通道读取服务器反馈的消息
InputStream socketInputStream = socket.getInputStream();
while ((readLen = socketInputStream.read(buf)) != -1) {
System.out.println(new String(buf, 0, readLen));
}
socketInputStream.close();
socketOutputStream.close();
socket.close();
byteArrayOutputStream.close();
fileInputStream.close();
- netstat
-
netstat -an:可以查看当前主机网络情况,包括端口监听和网络连接情况:
-
netstat -anb:管理员身份运行可以查看监听端口的程序
-
命令后跟参数 | more 可以进行分页显示;ctrl + c(double) 可以退出查看
- TCP链接秘密
- 当客户端与服务端建立连接后,实际上客户端也是通过一个端口与服务端进行通信的,这个端口号是根据 TCP / IP 由系统随机分配的
Proto | Local Address | Foreign Address | State |
---|---|---|---|
TCP | 10.138.141.42:5555 | 10.138.141.42:52820 | ESTABLISHED |
- UDP原理
- UDP 编程没有明确的服务端和客户端,因此无需建立连接
- UDP 数据包通过数据包套接字 DatagramSocket 实现发送和接收,系统不保证 UDP 数据包一定送达目的地,也不确定什么时候可以送达
- DatagramSocket 对象封装了 UDP 数据包,在数据包中封装了数据、发送端和接收端的 IP 地址以及端口信息
-
UDP网络编程1
-
UDP网络编程2
- Receive
// 监听端口
DatagramSocket socket = new DatagramSocket(7777);
// 接收包
// 用户保存数据包的缓冲区
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
// 拆包,获取数据
int dataLen = packet.getLength();
byte[] data = packet.getData();
System.out.println(new String(data, 0, dataLen));
// 装包,封装数据
buf = "Hello, sender!".getBytes(StandardCharsets.UTF_8);
packet = new DatagramPacket(buf, 0, buf.length, InetAddress.getLocalHost(), 8888);
// 发送包
socket.send(packet);
// 关闭资源
socket.close();
- Send
// 监听端口
DatagramSocket socket = new DatagramSocket(8888);
// 装包,封装数据
byte[] data = "Hello, receiver!".getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, 0, data.length, InetAddress.getLocalHost(), 7777);
// 发送包
socket.send(packet);
// 接收包
// 用于保存数据包的缓冲区
byte[] buf = new byte[1024];
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
// 拆包,获取数据
int dataLen = packet.getLength();
data = packet.getData();
System.out.println(new String(data, 0, dataLen));
// 关闭资源
socket.close();
-
网络编程作业1
-
网络编程作业2
-
TCP文件下载1
-
TCP文件下载2
-
网络编程梳理
第22章 多用户即时通信系统
-
多用户通信系统演示
-
项目开发流程
-
多用户通信需求
-
通信系统整体分析
-
QQ用户登录1
-
QQ用户登录2
-
QQ用户登录3
-
QQ用户登录4
-
QQ用户登录5
-
QQ用户登录6
-
QQ用户登录7
-
拉取在线用户1
-
拉取在线用户2
-
拉取在线用户3
-
无异常退出系统1
-
无异常退出系统2
-
私聊思路分析
-
私聊代码实现1
-
私聊代码实现2
-
群发分析实现
-
发文件思路分析
-
发文件实现1
-
发文件实现2
-
服务端推送新闻
-
离线留言和发文件
-
通信系统梳理
第23章 反射
- 反射机制问题
- 设计模式的 OCP(开闭) 原则:通过外部文件配置,在不修改源码的情况下,来控制程序;所谓开指功能开放,所谓闭指源码封闭
- 反射快速入门
// 加载配置文件,读取配置文件信息
Properties properties = new Properties();
properties.load(new FileInputStream("reflection.properties"));
String classPath = properties.getProperty("classPath");
String methodName = properties.getProperty("method");
// 加载类信息
Class<?> aClass = Class.forName(classPath);
// 获得类的一个实例
Object o = aClass.newInstance();
// 获得类中方法实例
Method method = aClass.getMethod(methodName);
// 调用方法
method.invoke(o);
- 反射原理图
- 反射机制允许程序在执行期借助于 Reflection API 取得任何类的内部信息(如成员变量、构造器、成员方法等),并能操作对象的属性及方法,反射在设计模式和框架底层广泛使用
- 加载完类之后,在堆中就产生了一个 Class 类型(Class 本身也是一个类)的对象(一个类只有一个 Class 对象,因为类只加载一次),这个对象包含了类的完整结构信息,通过这个对象就可以得到类的所有信息
- Java 程序的三个阶段:
- 代码阶段(编译阶段):将源码 .java 文件通过 javac 编译得到 .class 字节码文件
- Class 类阶段(加载阶段):通过类加载器(ClassLoader)将 .class 字节码文件加载到堆中生成 Class 类的对象,该对象记录了 .class 中类的字段 Field[] fields、构造器 Constructor[] constructors、方法 Method[] methods 等信息
- Runtime(运行阶段):通过 new 创建出的对象知道自己与哪个 Class 类型的对象关联
- 综上分析,通过 new 出来的对象也可以拿到它所关联的 Class 类型的对象;也可以直接拿到某个类的 Class 类型的对象进而操纵该类,比如创建该类的对象、调用该类的方法等等
- 反射相关类
- 反射机制的作用:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时得到任意一个类所具有的字段和方法
- 在运行时调用任意一个类所具有的字段和方法
- 生成动态代理
- java.lang.Class 代表一个类,Class 对象代表某个类的字节码文件被类加载器加载后在堆中生成的 Class 类型的对象
// 得到类的公有字段
Field ageField = aClass.getField("age");
// 获取字段的值
System.out.println(ageField.get(o));
// 得到类的公有构造器
Constructor<?> constructor = aClass.getConstructor(String.class, int.class);
// 调用构造器创建对象
constructor.newInstance("大黄", 12);
- 反射调用优化
- 反射优点:可以动态创建和使用对象(框架底层核心),使用灵活,没有反射机制框架就失去了灵魂
- 反射缺点:使用反射代码基本是解释执行,对执行速度有一定影响
- 反射调用优化:关闭访问检查;Method、Field、Constructor 类都有一个 setAccessible(boolean) 方法,作用是启动和禁用访问安全检查的开关;传入参数 true 表示反射的对象在使用时取消安全检查,提高反射效率,提高效果并不明显
- Class类分析
- Class 类本身也是类,继承自 Object 类
- Class 类对象不是 new 出来的,而是系统创建的,通过 ClassLoader 类的 loadClass 方法创建
- 对于某个类的 Class 对象,在堆中有且仅有一份,因为类只加载一次
- 每个类的实例(new 创建)都明确知道自己是与哪一个 Class 对象关联:obj.getClass()
- 通过一个类对应的 Class 对象,可以获取到该类的完整结构
- 加载类在堆里生成 Class 对象的同时也会在方法区生成对应类的字节码的二进制数据,也称为类的元数据,该二进制数据引用到 Class 类型对象
- Class常用方法
方法 | 功能 |
---|---|
static Class forName(String) | 获得指定类名的 Class 对象 |
Object newInstance() | 调用缺省构造函数,获得 Class 对象的一个实例 |
getName() | 获得 Class 对象所表示的实体名称 |
Class getSuperClass() | 获得当前 Class 对象的父类的 Class 对象 |
Class[] getInterfaces | 获得 Class 对象的接口 |
ClassLoder getClassLoder() | 获得类的加载器 |
Constructor[] getConstructors() | 获得非私有构造器对象 |
Field[] getDeclaredFields() | 获得所有字段对象 |
Method getMethod() | 获得一个非私有 Method 对象 |
- 使用示例:
String classPath = "com.springbear.Car";
Class<?> clazz = Class.forName(classPath);
// 输出此 Class 对象是哪个类的 Class 对象
System.out.println(clazz);
// 获得运行类型
System.out.println(clazz.getClass());
// 获得包名
System.out.println(clazz.getPackage().getName());
// 获得全类名
System.out.println(clazz.getName());
// 获得实例
Object obj = clazz.newInstance();
System.out.println(obj);
// 获取公有属性
Field bland = clazz.getField("bland");
System.out.println(bland.get(obj));
// 给属性设置值
bland.set(obj, "宝马");
System.out.println(bland.get(obj));
- 获取Class对象六种方式
- 编译阶段:Class.forName(String);多用于配置文件,读取类全路径,加载类
- 类加载阶段:类.class;多用于参数传递,此方式最为安全可靠,程序性能最高
- 运行阶段:对象.getClass();通过创建好的类的对象来获取 Class 对象也即运行类型
- 类加载器:
Car car = new Car();
// 获得类对应的类加载器
ClassLoader classLoder = car.getClass().getClassLoader();
// 获得 Class 对象
Class<?> aClass = classLoder.loadClass("com.springbear.Car");
- 八大基本数据类型(byte、boolean、char、short、int、float、long、double)可直接使用:Class cls = int.class
- 基本数据类型对应的包装类,可以通过 .TYPE获得 Class 对象:Class cls = Integer.TYPE
- 哪些类型有Class对象
- 外部类、内部类、接口、数组、枚举、注解、基本数据类型、void
- 动态和静态加载
- 反射机制是 Java 实现动态语言的关键,也就是通过反射实现类动态加载
- 静态加载:编译时加载相关的类,如果没有相关类则报错,依赖性很强
- 动态加载:只在运行时加载需要的类,如果运行时未使用到该类,则不报错,降低了依赖性
- 类加载的时机:
- new 创建对象时
- 其子类被加载时,父类也被加载
- 访问到类的静态成员时(非 static final)
- 通过反射加载
- 类加载流程图
- .java 文件通过 javac 编译得到 .class 文件,通过 java 运行 .class 文件时发生类的加载,类加载分为三个阶段:加载(loading)-> 连接(linking)-> 初始化(initialization)
- 加载(loading):将类的 .class 文件读入内存,并为之创建一个 Class 类型的对象,由类加载器完成
- 连接(linking):将类的二进制数据合并到 JRE 中。连接(linking)又分为验证(verification)、准备(preparation)、解析(resolution)三个子阶段
- 验证(verification):对文件的安全性进行校验,比如说文件格式是否正确、元数据验证是否正确、字节码是否正确、符号引用是否正确
- 准备(preparation):静态变量分配内存并完成默认初始化
- 解析(resolution):JVM 将常量池中的符号引用替换成直接引用
- 初始化(initialization):JVM 负责对类进行初始化(显示初始化),主要指静态成员
- 类加载后内存布局情况:在方法区存放类的字节码的二进制数据(元数据);在堆中存放类的 Class 对象(数据结构);且方法区的二进制数据引用到对应的 Class 对象
- 类加载五个阶段1
- 加载阶段:将字节码从不同的数据源(可能是 .class 文件、也可能是 jar 包、甚至是网络)转化为二进制字节流加载到内存(方法区)中,并生成一个代表该类的 java.lang.Class 对象
- 连接之验证阶段:目的是为了确保 .class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;验证包括对文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证;可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机加载时间
- 连接之准备阶段:JVM 会在此阶段给静态变量分配内存并执行默认初始化,这些变量所使用的内存都会在方法区中进行分配
// 在连接的准备子阶段先默认初始化为 0,后在初始化阶段再显式初始化为 1
public static int num1 = 1;
// 在连接的准备子阶段直接初始化为 2
public static final int num2 = 2;
- 连接之解析阶段:JVM 将常量池内的符号引用替换为直接引用的过程
- 类加载五个阶段2
- 初始化阶段:真正执行类中定义的 Java 代码,此阶段是执行 () 方法的过程
- () 方法是由编译器按语句在源文件中的出现顺序,依次自动收集类中所有的静态变量的赋值动作和静态代码块中的语句,并进行合并
static {
System.out.println("静态代码块被执行!");
num = 300;
}
public static int num = 100;
// 经 <clinit>() 方法收集合并后如下
System.out.println("静态代码块被执行!");
num = 100;
- JVM 保证一个类的 () 方法在多线程环境中会被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程会去执行 () 方法,其它线程都需要阻塞等待,直到活动线程执行完 () 方法
- 获取类结构信息1
方法名 | 功能 |
---|---|
getName | 获取全类名 |
getSimpleName | 获取简单类名 |
getFields | 获取所有 public 修饰的字段,本类以及父类 |
getDeclaredFidles | 获取本类中的所有字段 |
getMethods | 获取所有 public 修饰的方法,本类以及父类 |
getDeclaredMethods | 获取本类中的所有方法 |
getConstructors | 获取所有 public 修饰的构造器,只包含本类 |
getDeclaredConstructors | 获取本类中的所有构造器 |
getPackage | 以 Package 形式返回包信息 |
getSuperClass | 以 Class 形式返回父类信息 |
getAnnotations | 以 Annotation[] 形式返回注解信息 |
- 获取类结构信息2
类名 | 方法名 | 功能 |
---|---|---|
java.lang.reflect.Field | getModifiers() | 以 int 形式返回修饰符:默认修饰符是 0,public 是 1,private 是 2,protected 是 4,static 是 8,final 是 16;多重修饰符则相加 |
getType() | 以 Class 形式返回类型 | |
getName() | 返回字段名 | |
java.lang.reflect.Method | getModifiers | 以 int 形式返回修饰符 |
getReturnType | 以 Class 类型获取方法返回类型 | |
getName | 返回方法名 | |
getParameterTypes | 以 Class[] 形式返回参数类型数组 | |
java.lang.reflect.Constructor | getModifiers | 以 int 形式返回修饰符 |
getName | 返回构造器的全类名 | |
getParameterTypes | 以 Class[] 形式返回参数类型数组 |
- 反射爆破创建实例
- 通过反射创建对象方式一:调用类中的 public 修饰的无参构造器
Class<?> tempClass = Class.forName("com.springbear.Temp");
Object o = tempClass.newInstance();
- 通过反射创建对象方式一:调用类中指定的构造器
Class<?> userClass = Class.forName("com.springbear.User");
// 获得指定构造器
Constructor<?> userConstructor = userClass.getDeclaredConstructor(int.class, String.class);
// 爆破
userConstructor.setAccessible(true);
// 调用构造器
Object mary = userConstructor.newInstance(10, "mary");
System.out.println(mary);
- 反射爆破操作属性
Class<?> userClass = Class.forName("com.springbear.User");
Object userObject = userClass.newInstance();
// 获得指定字段
Field name = userClass.getDeclaredField("name");
// 爆破
name.setAccessible(true);
// 获得值
System.out.println(name.get(userObject));
// 设置值
name.set(userObject, "Spring-_-Bear");
System.out.println(name.get(userObject));
- 如果获取的是静态属性,则 set 和 get 方法中传入的对象参数可以是 null
- 反射爆破操作方法
Class<?> userClass = Class.forName("com.springbear.User");
Object userObject = userClass.newInstance();
// 获取指定的方法
Method loginMethod = userClass.getDeclaredMethod("login", String.class, String.class);
// 爆破
loginMethod.setAccessible(true);
// 调用方法
Object springbear = loginMethod.invoke(userObject, "springbear", "123");
System.out.println(springbear);
- 如果是静态方法,则 invoke 的对象参数可以写成 null
- 反射课后练习
- 利用反射机制在指定目录下创建文件
// 加载类信息
Class<?> fileClass = Class.forName("java.io.File");
// 获取所有构造器
Constructor<?>[] declaredConstructors = fileClass.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
declaredConstructor.setAccessible(true);
System.out.println(declaredConstructor);
}
// 获得指定的构造器
Constructor<?> constructor = fileClass.getConstructor(String.class);
Object file = constructor.newInstance("d:\\aa.txt");
// 获得指定方法
Method createNewFile = fileClass.getMethod("createNewFile");
// 调用方法
createNewFile.invoke(file);
- 反射梳理
第24章 零基础学MySQL
-
数据库的使用
-
MySQL5.7安装配置
- 删除 MySQL 服务:sc delete mysql
- 命令行连接到MySQL
- 启动服务:net start mysql
- 关闭服务:net stop mysql
- 登录:mysql -h 主机名 -P 端口 -u 用户名 -p 密码
-
Navicat安装和使用
-
SQLyog安装和使用
-
MySQL三层结构
-
mysqld 程序在端口进行监听
-
MySQL 数据库表中一行称为一条记录,一列称为一个字段。在 Java 程序中,一行记录往往使用对象表示
-
SQL语句分类:
- DDL(数据定义语句)[DEFINE]
- DML(数据操作语句)[INSERT、UPDATE、DELETE]
- DQL(数据查询语句)(SELECT)
- DCL(数据控制语句)[GRANT REVOKE]
- Java操作MySQL
// 加载驱动
Driver driver = new com.mysql.jdbc.Driver();
// 设置用户名、密码以及连接到的数据库
Properties properties = new Properties();
properties.setProperty("user", "userName");
properties.setProperty("password", "password");
String url = "jdbc:mysql://localhost:3306/fishing_cost";
// 获得 Java 程序与数据的链接对象
Connection connection = driver.connect(url,properties);
// 获得执行 SQL 语句的对象
Statement statement = connection.createStatement();
- 创建数据库
CREATE DATABASE [IF NOT EXISTS] db_name [create_specification]···
-- 指定数据库采用的字符集,默认为 utf8
[DEFAULT] CHARACTER SET char_set_name
-- 指定数据库字符集的校对规则,默认为 utf8_general_ci(不区分大小写),常用的还有 utf8_bin(区分大小写)
[DEFAULT] COLLATE collation_name
- 查询数据库
- 为了规避关键字,可以使用反引号 `` 解决
-- 显示所有数据库
SHOW DATABASES;
-- 显示数据库的创建语句
SHOW CREATE DATABASE db_name
-- 删除数据库
DROP DATABASE [IF EXISTS] db_name
- 备份恢复数据库
-- 备份数据库:在 DOS 命令行执行(文件名前可带路径)
mysqldump -u 用户名 -p -B 数据库1 数据库2 数据库n > 文件名.sql
-- 备份表:在 DOS 命令行执行(文件名前可带路径)
mysqldump -u 用户名 -p 数据库 表1 表2 表n > 文件名.sql
-- 恢复数据库:登录 MySQL 后在 MySQL 命令行执行(文件名前可带路径)
Source 文件名.sql
- 创建表
CREATE TABLE table_name (
field1 datatype,
field2 datatype,
fieldn datatype
) CHARACTER SET 字符集 COLLATE 校对规则 ENGINE 引擎;
- 如果表没有指定字符集、校对规则、引擎,则以数据库的设置为准
- 列类型有哪些
数据类型 | 说明 |
---|---|
BIT(M) | 位类型。M 指定位数,8位一个字节,默认值是 1,范围[1,64],按二进制显示 |
TINYINT [UNSIGNED] | 占 1 个字节,带符号[-128,127],无符号[0,255],默认带符号 |
SMALLINT [UNSIGNED] | 占 2 个字节,带符号[215,215-1],无符号[0,216-1],默认带符号 |
MEDIUMINT [UNSIGNED] | 占 3 个字节,带符号[223,223-1],无符号[0,224-1],默认带符号 |
INT [UNSIGNED] | 占 4 个字节,带符号[231,231-1],无符号[0,232-1],默认带符号 |
BIGINT [UNSIGNED] | 占 8 个字节,带符号[263,263-1],无符号[0,264-1],默认带符号 |
FLOAT [UNSIGNED] | 浮点数,占 4 个字节 |
DOUBLE [UNSIGNED] | 浮点数,占 8 个字节 |
DECIMAL(M,D) [UNSIGNED] | 定点数,M 指定长度,D 指定小数点位数 |
CHAR(size) | 定长字符串,size 最大 255 个字符 |
VARCHAR(size) | 变长字符串,size 最大 65535 个字节 |
BLOB | 二进制数据,存储位数[0,216-1] |
LONGBLOB | 二进制数据,存储位数[0,232-1] |
TEXT | 文本数据,[0,216-1] |
LONGTEXT | 文本数据,[0,232-1] |
DATE | 日期类型(YYYY-MM-DD),占3个字节 |
TIME | 时间类型(HH:MM:SS),占3个字节 |
YEAR | 年,占1个字节 |
DATETIME | 日期时间类型(YYYY-MM-DD HH:mm:ss),占8个字节 |
TIMESTAMP | 时间戳,可自动记录 INSERT、UPDATE操作时间,占4个字节 |
-
列类型之整型
-
列类型之bit
- 数据查询显示时,以二进制位的方式显示
- 列类型值小数型
- DECIMAL(M,D):若省略 M,则默认值为 10;若省略 D,默认为 0
- 列类型之字符串1
- CHAR(size):定长字符串,最大 255 个字符
- VARCHAR(size):变长字符串,最大 65535 个字节,实际上不能存放这么多
- Unicode编码中一个字符占 3 个字节,GBK编码中一个字符占 2 个字节
- UTF8 编码最大存放 21844 个字符,需要用 1~3 个字节用于记录字段大小即(65535字节 - 3字节)/ 3字节每字符 )
- GBK编码最大存放 32766 个字符,需要用 1~3 个字节用于记录字段大小即(65535字节 - 3字节)/ 2字节每字符 )
- 列类型之字符串2
- CHAR(4) 数字 4 表示字符数,不管是中文还是英文都存放 4 个;VARCHAR(4) 数字 4 表示字符数,不管是字符还是中文都以定义好的表的编码来存放数据。即不管是中文还是英文字母,最多存放 4 个字符。其中VARCHAR(size) :如果编码为 UTF8,则 size 最大为 21844;如果编码是 GBK,则 size 最大为 32766。CHAR(size) 最大范围是 255 。
- CHAR(4) 所谓定长,指的是分配的空间是固定的,哪怕你只是插入了 “aa”,占用的空间也是 4 个字符的空间。
- VARCHAR(4) 所谓变长,指的是占用的空间是动态大小,即根据实际占用的空间大小进行分配。VARCHAR 本身还需要占用 1~3 个字节来存放内容长度信息。
- CHAR 的查询速度高于 VARCHAR
- 在存放文本时,也可以使用 TEXT 数据类型,可以将 TEXT 列视为 VARCHAR 列,注意 TEXT 不能有默认值。大小范围为 [0,216-1] 字节。如果希望存放更多字符,可以使用 MEDIUMTEXT[0,224-1] 或 LONGTEXT[0,232-1]
- 日期类型
-- 在插入数据或更新数据时自动更新时间戳
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- 创建表练习
CREATE TABLE t01 (
id INT PRIMARY KEY,
name VARCHAR (32),
sex CHAR(1),
birthday DATE,
entry_date DATE,
job VARCHAR (32),
salary DOUBLE,
`resume` TEXT
) CHARSET utf8 COLLATE utf8_bin ENGINE INNODB ;
- 修改表
-- 添加列
ALTER TABLE `table_name` ADD column1 datatype,columnn datatype
-- 添加列到指定列之后
ALTER TABLE `table_name` ADD column1 datatype AFTER column_name
-- 修改列属性
ALTER TABLE `table_name` MODIFY column1 datatype,columnn datatype
-- 删除列
ALTER TABLE `table_name` DROP column1,columnn
-- 修改列名
ALTER TABLE `table_name` CHANGE old_column_name new_column_name datatype
-- 修改表的字符集
ALTER TABLE `table_name` CHARSET character_set_name
-- 查看表结构
DESC `table_name`
-- 重命名表名
RENAME TABLE old_table_name TO new_table_name
- insert基本使用
- CRUD:create、read、update、delete
- insert注意事项
-- 可以使用下列形式添加多条记录
INSERT INTO t01 VALUES (),(),()
- 如果是给表中所有字段添加数据,则可以省略字段名称
- 当不给某个字段添加值时,如果有默认值则自动添加默认值,否则报错
- update语句
UPDATE `table_name` SET col_name = exp1,col_name = exp2 WHERE ···
- delete语句
DELETE FROM `table_name` WHERE ···
- 使用 DELETE 语句不能删除某一列的值,可用 UPDATE 语句将该列设为 NULL
- 使用 DELETE 语句仅删除记录,不能删除表本身。如要删除表,使用 DROP TABLE table_name 语句
-
select语句1
-
select语句2
-
select语句3
- WHERE 子句中常用的运算符:
表达式 | 作用 |
---|---|
BETWEEN···AND··· | 显示在某一区间的值,闭区间 |
IN(set) | 显示在 IN 列表中的值 |
LIKE | 模糊查询 |
NOT LIKE | 模糊查询 |
IS NULL | 是否为空 |
AND | 与 |
OR | 或 |
NOT | 非 |
- select语句4
- ORDER BY 对指定的列进行排序,排序的列既可以是表中的列名,也可以是 SELECT 语句后指定的别名
- ASC:升序 DESC:降序
- 统计函数
- COUNT(*) 返回满足条件的的记录的行数;COUNT(列) 统计满足条件的某列有多少个,会排除 NULL
- 分组统计
- 使用 HAVING 子句可以对分组后的结果进行筛选
SELECT col1,col2,coln FROM `table_name` GROUP BY col_name HAVING exp
- 字符串函数
函数名 | 作用 |
---|---|
CHARSET(str) | 返回字符串字符集 |
CONCAT(str1,str2···) | 连接字符串 |
INSTR(str,substr) | 返回 substr 在 str 中的出现位置,没有出现返回 0 |
UCASE(str) | 转换成大写 |
LCASE(str) | 转换成小写 |
LEFT(str,len) | 从 str 中的左边起取 len 长度个字符 |
LENGTH(str) | str 的长度,以字节为单位 |
REPLACE(str,search_str,replace_str) | 在 str 中用 replace_str 替换 search_str |
STRCMP(str1,str2) | 逐字符比较字符串大小 |
SUBSTRING(str,pos,len) | 在 str 中从pos开始取 len 个字符,pos 从1开始 |
LTRIM | 去除前端空格 |
RTRIM | 去除后端空格 |
TRIM | 去除前后端空格 |
- SUBSTRING(str,pos,len):如果省略 len,则从 pos 位置开始截取完所有字符。例如:SUBSTRING(‘SpringBear’,1,6) 从位置 1 开始截取 6 个字符,返回结果为:Spring
- DUAL 表是系统的亚元表,在无表可用的情况下可以用 DUAL 来进行测试
-
字符串函数练习
-
数学函数
函数名 | 功能 |
---|---|
ABS(num) | 绝对值函数 |
BIN(decimal_number) | 十进制转二进制 |
CEILING(num) | 向上取整,得到比 num 大的最小整数 |
CONV(num,from_base,to_base) | 进制转换,从指定的 from 进制转换到 to 进制 |
FLOOR(num) | 向下取整,得到比 num 小的最大整数 |
FORMAT(num,decimal_places) | 四舍五入保留指定的小数位数 |
HEX(num) | 转十六进制 |
LEAST(num1,num2···) | 求最小值 |
MOD(numerator,denominator) | 求余 |
RAND([seed]) | 随机数[0,1] |
- 日期函数1
函数名 | 功能 |
---|---|
CURRENT_DATE() | 当前日期 |
CURRENT_TIME() | 当前时间 |
CURRENT_TIMESTAMP() | 当前时间戳 |
DATE(datetime) | 返回 datetime 的日期部分 |
DATE_ADD(date,INTERVAL d_value d_type) | 在 date上加上一个日期或时间 |
DATE_SUB(date,INTERVAL d_value d_type) | 在 date 上减去一个时间 |
DATEDIFF(date1,date2) | 日期差,结果是天 |
TIMEDIFF(time1,time2) | 时间差,结果是时分秒 |
NOW() | 当前时间,年月日时分秒 |
YEAR(datetime) | 年 |
MONTH(datetime) | 月 |
UNIX_TIMESTAMP() | 返回 1970-1-1 到当前时间的秒数 |
FROM_UNIXTIME() | 将一个秒数转换成指定格式,例如年月日 |
- 日期函数2
- DATE_ADD() 、DATE_SUB() 函数中的 date 可以是 date、datetime、timestamp,d_type 可以是年月日时分秒
- 日期函数3
-- 日期格式化:2021-11-06 21:39:34
SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(),'%Y-%m-%d %H:%i:%s') FROM DUAL
- 加密函数
-- 查看当前用户
SELECT USER() FROM DUAL
-- 查询当前使用的数据库
SELECT DATABASE() FROM DUAL
-- 为字符串创建一个MD5算法加密的密码,32位字符
SELECT MD5(str) FROM DUAL
-- 为字符串创建一个MySQL加密算法创建的密码
SELECT PASSWORD(str) FROM DUAL
-- 查询某个数据库的用户
SELECT * FROM 'db_name'.user
- 流程控制函数
-- 如果exp1为true,则返回exp2,否则返回exp3
IF(exp1,exp2,exp3)
-- 如果exp1为空,则返回exp2,否则返回exp1
IFNULL(exp1,exp2)
-- 如果exp1为true,则返回exp2;如果exp3为true,则返回exp4;否则返回exp5
SELECT CASE WHEN exp1 THEN exp2 WHEN exp3 THEN exp4 ELSE exp5 END;
- 判断是否为空不使用 = NULL,使用 IS NULL
- 查询增强
- % 表示 0 到多个字符;_ 表示单个任意字符
- 分页查询
- SELECT ··· LIMIT start, rows 从 start + 1 行开始,取出 rows 行,start 从 0 开始计算
-
分组增强
-
多子句查询
SELECT col1,col2,coln
FROM table_name
GROUP BY col
HAVING `condition`
ORDER BY col
LIMIT `start`, `rows`
- 多表笛卡尔集
- 多表查询的条件不能少于表的个数 -1,否则会出现笛卡尔集
-
多表查询
-
自连接
SELECT * FROM `table_name` alias1, `table_name` alias2
- 多行子查询
- 子查询是指嵌入在其它 SQL 语句中的 SELECT 语句,也叫嵌套查询。
- 单行子查询:只返回一行数据的子查询语句
- 多行子查询:返回多行数据的子查询,使用关键字 IN 进行连接
-
子查询临时表
-
all和any
- all:满足所有条件
- any:满足一个条件
- 多列子查询
- 多列子查询:返回多列数据的子查询
SELECT ··· WHERE (字段1,字段2···) = (SELECT 字段1,字段2 FROM ···)
- 子查询练习
- table_name.* 表是把该表的所有列信息都显示出来
- 表复制和去重
- 自我复制也称蠕虫复制,为了对某条 SQL 语句进行效率测试,当我们需要海量数据时,可以进行蠕虫复制。
-- 蠕虫复制
INSERT INTO `table_name` (field1,field2···) SELECT field1,field2 FROM `table_name`
-- 复制表结构
CREATE TABLE `table_name` LIKE `table`
- 去除表中的重复记录:创建一张与原表结构相同的表,从原表中将记录去重后插入到临时表,清除原表中的所有记录,再将临时表中的记录插入到原表,删除临时表
- 合并查询
- 为了合并多个 SELECT 语句的结果,可以使用集合操作符号 UNION,UNION ALL
- UNION ALL 操作符用于取得两个结果集的并集,不会去除重复行,UNION会自动去重
- 外连接需求
- 为了显示查询结果中某些不满足匹配条件的记录
- 左外连右外连
- 左外连接:左侧的表完全显示;
- 右外连接:右侧的表完全显示
- 用 ON 关键字进行条件过滤
-- 左外连接
SELECT ··· FROM table1 LEFT JOIN table2 ON condition1
-- 右外连接
SELECT ··· FROM table1 RIGHT JOIN table2 ON condition1
- 主键
- 约束:确保数据库的数据满足特定的规则
- MySQL 中的约束条件共有 5 个:NOT NULL、UNIQUE、PRIMARY KEY、FOREIGN KEY、CHECK
- 主键不能重复且不能为空,一张表中最多有一个主键,可以是复合主键(多个字段构成主键)
- unique
- 当指定过滤 UNIQUE 约束后,该列值是不能重复的。如果没有指定 NOT NULL,则 UNIQUE 字段可以有多个 NULL
- 外键介绍
- 外键:用于定义主表和从表之间的关系:外键约束要定义在从表上,主表则必须具有主键约束或是 UNIQUE 约束,当定义了外键约束后,要求外键列数据必须在主表的主键列存在或是为 NULL
-- 外键约束放在建表语句最后
FOREIGN KEY (本表字段名) REFERENCES 主表名(主键名或UNIQUE字段名)
- 外键使用细节
- 表的引擎为 INNODB,这样的表才支持外键
- 外键字段的类型要和主键字段的类型一致
- 外键字段的值,必须在主键字段中出现过或是为 NULL,前提是外键字段允许为 NULL
- 一旦建立了主外键关系,数据的删除原则应遵循先删外键所在从表的记录,才能删除主表中对应的记录
- check
- MySQL5.7 目前还不支持 check,只做语法校验,但不会生效。在 MySQL 中实现 check 的功能,一般是在程序中控制或是通过触发器完成
- 商店表设计
-- sex只能是男、女中的一个
sex ENUM('男','女') NOT NULL
- 自增长
-- 置空
INSERT INTO `table_name` (字段1,字段2,···) VALUES (NULL,值2,···)
-- 省略字段
INSERT INTO `table_name` (字段2,字段3,···) VALUES (值2,值3,···)
-- 省略字段表达式且置空
INSERT INTO `table_name` VALUES (NULL,值2,值3,···)
- 一般来说自增长是和主键配合使用的,也可以单独使用但需要配合一个 UNIQUE,一般情况只用于整数
-- 修改自增长的默认值
ALTER TABLE `table_name` AUTO_INCREMENT = digit
- 如果添加数据过程中存在指定的自增长值,则下一条记录从指定值之后开始自增
- 索引优化速度
-- 创建索引
CREATE INDEX `index_name` ON `table_name`(fidld_name)
- 创建索引后,只在查询有索引的列时速度明显提高
- 索引机制
- 索引的代价:1. 磁盘空间占用; 2. 对 DML(update,delete,insert) 语句的效率影响,因为会重新构建索引所需数据结构
- 没有创建索引查询时会进行全表扫描,创建索引之后会使用创建的数据结构进行查询,查询效率明显提高
- 创建索引
- 主键索引:主键自动为主索引
- 唯一索引:UNIQUE
- 普通索引:INDEX
- 全文索引:FULLTEXT,适用于 MyISAM
- 在开发中一般不使用 MySQL 自带的全文索引,考虑使用全文搜索框架 Solr 和 ElasticSearch
-- 查询表中是否存在索引
SHOW INDEXES FROM `table_name`
-- 添加索引
CREATE [UNIQUE] INDEX index_name ON `table_name` (col_name)
ALTER TABLE `table_name` ADD INDEX [index_name] (index_col_name)
-- 添加主键索引
ALTER TABLE `table_name` PRIMARY KEY (col_name)
- 索引选择:如果某列保证不可能重复,则优先考虑唯一索引
- 如果在建表语句中指定了 PRIMARY KEY 或 UNIQUE,则会自动创建索引;若没有指定,则可以在建表完成后通过添加索引语句创建索引
- 删除索引
-- 删除索引
DROP INDEX index_name ON `table_name`
-- 删除主键索引
ALTER TABLE `table_name` DROP PRIMARY KEY
- 创建索引规则
- 较频繁的作为查询添加的字段应该创建索引
- 唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件
- 更新非常频繁的字段不适合创建索引
- 事务有什么用
- 事务:用于保证数据的一致性,它由一组相关的 DML 语句组成,该组的 DML 语句要么全部成功,要么全部失败
- 事务和锁:当执行事务操作时( DML 语句),MySQL 会在表上加锁,防止其它用户改表的数据
-- 开始一个事务
start transaction
-- 设置保存点
savepoint point_name
-- 回退事务
rollback to point_name
-- 回退全部事务
rollback
-- 提交事务,一经提交,立即生效,不能回退
commit
- 事务操作
- 事务操作只能回退,不能回退后再前进
- 事务注意事项
- 如果不开启事务,默认情况下,DML 操作是自动提交的,不能回滚
- MySQL 的事务机制需要 INNODB 的存储引擎,MyISAM 不可用
-- 开启事务
start transaction
set autocommit = off
- 4种隔离级别
- 事务隔离级别:多个连接开启各自事务操作数据库中的数据时,DBMS 要负责隔离操作,以保证各个连接在获取数据时的准确性,如果不考虑隔离性,可能会引发如下问题:脏读、不可重复读、幻读
- 脏读(dirty read):当一个事务读取到另一个事务尚未提交的操作时产生脏读
- 不可重复读(nonrepeatable read):同一查询在同一事务中多次进行,由于提交事务所做的修改或删除,每次返回不同的结果集,此时发生不可重复读
- 幻读(phantom read):同一查询在同一事务中多次进行,由于提交事务所做的插入操作,每次返回不同的结果集,此时发生幻读
问题 | 描述 |
---|---|
脏读(dirty read) | 一个事务读取到另一个事务尚未提交的操作 |
不可重复读(nonrepeatable read) | 一个事务读取到另一个事务已提交的删除或修改操作 |
幻读(phantom read) | 一个事务读取到另一个事务已提交的插入操作 |
- 事务隔离级别:MySQL 隔离级别定义了事务与事务之间的隔离程度
MySQL隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
读未提交(read uncommintted) | Y | Y | Y | N |
读已提交(read committed) | N | Y | Y | N |
可重复读(repeatable read) | N | N | N | N |
可串行化(serializable) | N | N | N | Y |
- Y:可能出现 N:不出现 | ||||
- 加锁:有且仅有一个事务正在处理数据 |
- 隔离级别演示1
- 正常情况:查询的数据是登录时刻的数据才算正常情况
- 隔离级别演示2
- 可串行化:有一张表正在被另一个事务操作没有提交时,当前事务会停下等待,直到另一个事务提交操作时当前事务才可继续操作
- 设置隔离
-- 查看当前会话隔离级别
SELECT @@tx_isolation
-- 查看系统当前隔离级别(所有用户)
SELECT @@global.tx_isolation
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL isolation_name
-- 设置系统当前会话隔离级别(所有用户)
SET GLOBAL SESSION TRANSACTION ISOLATION LEVEL isolation_name
- MySQL 默认的事务隔离级别是 REPEATABLE READ,没有特殊情况一般不修改
- MySQL 事务的 ACID 特性
名称 | 描述 |
---|---|
原子性(atomicity) | 事务是一个不可分割的工作单位,要么都发生,要么都失败 |
一致性(consistency) | 事务保证数据库从一个一致性状态转换到另一个一致性状态 |
隔离性(isolation) | 多个并发事务之间根据隔离级别不同相互隔离 |
持久性(durability) | 事务一旦提交,对数据的改变就是永久性的 |
- 存储引擎1
- MySQL的表类型由存储引擎决定,常用的是 InnoDB、MyISAM、Memory
- MySQL数据表主要支持六种类型,分别是:InnoDB、MyISAM、Memory、CSV、Archive、MRG_MyISAM
- 六种类型又分为两类:事务安全型 (transaction-safe) 和非事务安全型 (non-transaction-safe)。除了 InnoDB 是事务安全型,其余的都是非事务安全型(不支持事务)
-- 查看所有的存储引擎
SHOW ENGINES
特点 | InnoDB | MyISAM | Memory | Archive |
---|---|---|---|---|
批量插入速度 | 低 | 高 | 高 | 非常高 |
事务安全 | 支持 | |||
全文索引 | 支持 | |||
锁机制 | 行锁 | 表锁 | 表锁 | 行锁 |
存储限制 | 64TB | 无限制 | 有限制 | 无限制 |
B树索引 | 支持 | 支持 | 支持 | |
哈希索引 | 支持 | 支持 | ||
集群索引 | 支持 | |||
数据缓存 | 支持 | 支持 | ||
索引缓存 | 支持 | 支持 | 支持 | |
数据可压缩 | 支持 | 支持 | ||
空间使用 | 高 | 低 | 非常低 | |
内存使用 | 高 | 低 | 中等 | 低 |
支持外键 | 支持 |
- MyISAM 不支持事务,不支持外键,但其访问速度快,对事务完整性没有要求
- InnoDB 提供了具有提交、回滚和崩溃恢复能力的事务安全。但是比起 MyISAM 存储引擎,InnoDB 写入数据的处理效率相对较低并且会占用更多的磁盘空间以保留数据和索引
- Memory 存储引擎使用内存来创建表。每个 Memory 表实际上值对应一个磁盘文件。表访问速度非常快,因为它的数据存放于内存中,并且默认使用 Hash 索引。一旦 MySQL 服务关闭,表中所有数据都会丢失,但表的结构仍然存在
- 存储引擎2
- 存储引擎的选择:如果不需要事务操作,只是处理基本的 CRUD 操作,MyISAM 是不二之选;如果需要支持事务,InnoDB 非选不可;Memory 经典用法:用户的在线状态
-- 修改存储引擎
ALTER TABLE `table_name` ENGINE = engine_name
- 视图原理
- 视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含列,其数据映射自对应的真实表(基表)
- 基表与视图的修改会相互影响,视图中的数据并不物理保存,视图只是映射基表中的数据
- 视图使用细节
-- 创建视图
CREATE VIEW view_name AS SELECT field1··· FROM `table_name`
-- 修改视图
ALTER VIEW view_name AS SELECT field1··· FROM `table_name`
-- 查看视图
SHOW CREATE VIEW view_name
-- 删除视图
DROP VIEW view_name
- 创建视图后,磁盘中只会存在一个视图结构文件,不存在数据存储文件,因为视图中的数据映射自基表
- 视图中可以再创建视图
- 视图应用案例
- 视图应用最佳实践:
- 安全:创建视图让用户只能通过视图查询到表中部分字段的数据,隐藏保密字段
- 性能:关系数据库的数据常常会分表存储,使用外键维护这些表之间的关系。查询时难免用到连接(JOIN)。这样做不仅麻烦,效率也比较低。如果建立视图,将相关的表和字段组合在视图中,就可以避免使用 JOIN 查询数据
- 灵活:如果系统中有一些旧的表,这些表由于设计的问题,即将被废弃。然而,很多应用都依赖于这些表,不易修改。这时就可以建立一张视图,视图中的数据映射到这些表。这样,就可以少做很多改动,也达到了升级数据表的目的
- MySQL用户管理
- MySQL中的用户,都存储在系统数据库 mysql 中的 user 表中
- user 表字段说明:Host:允许登录的位置
-- 创建用户
CREATE USER user_name @ip_addr IDENTIFIED BY pwd
-- 删除用户
DROP user_name @ip_addr
-- 修改自己的密码
SET PASSWORD = PASSWORD(pwd)
-- 修改别人的密码(需要权限)
SET PASSWORD FOR user_name @ip_addr = PASSWORD(new_pwd)
- MySQL权限管理
权限 | 内容 |
---|---|
ALL | 设置除 GRANT OPTION 之外的所有简单权限 |
ALTER | ALTER TABLE |
ALTER ROUTINE | 更改或取消已存储的子程序 |
CREATE | CREATE TABLE |
CREATE ROUTINE | 允许已存储的子程序 |
CREATE TEMPORARY TABLES | CREATE TEMPORARY TABLES |
CREATE USER | CREATE USER、DROP USER、RENAME USER、REVOKE ALL PRIVILEGES |
CREATE VIEW | CREATE VIEW |
DELETE | DELETE |
DROP | DROP TABLE |
EXECUTE | 语序用户运行已存储的子程序 |
FILE | SELECT···INTO OUTFILE、LOAD DATA INFILE |
INDEX | CREATE INDEX、DROP INDEX |
INSERT | INSERT |
LOCK TABLES | 对拥有 SELECT 权限的表使用 LOCK TABLES |
PROCESS | SHOW ALL PROCESSLIST |
REFERENCES | 未被实施 |
RELOAD | FLUSH |
REPLICATION CLIENT | 允许用户询问从属服务器或主服务器的地址 |
SELECT | SELECT |
SHOW DATABASES | SHOW DATABASES |
SHOW VIEW | SHOW CREATE VIEW |
SHUTDOWN | 允许使用 mysqladmin shutdown |
SUPER | CHANGE MASTER、KILL、PURGE MASTER LOGS、SET GLOBAL、mysqladmin debug;允许您连接(一次),即使已达到max_connections |
UPDATE | UPDATE |
USAGE | 无权限 |
GRANT OPTION | 允许授予权限 |
-- 给用户授权
GRANT 权限列表 ON 库.对象 TO 用户名 @登录位置 [IDENTIFIED BY pwd]
-- 1. 权限列表,多个权限用逗号分隔
GRANT SELECT,DELETE··· ON ···
-- 2. 除GRANG OPTION外的所有权限
GRANT ALL ON ···
-- 3. *.* 代表本DBMS中的所有数据库的所有对象
-- 4. 库.* 代表指定数据库的所有对象
-- 5. [IDENTIFIED BY pwd] 如果用户存在,则修改密码; 用户不存在则创建用户
-- 回收权限
REVOKE 权限列表 ON 库.对象名 FROM 用户名 @登录位置
-- 权限生效指令
FLUSH PRIVILEGES
- MySQL管理细节
- 在创建用户的时候,如果不指定 Host,则为 %,% 表示所有 IP 都有连接权限
- 在删除用户的时候,如果 Host 不是 %,则需要明确指定 user@ip_addr
-
MySQL作业1
-
MySQL作业2
-
MySQL作业3
-
MySQL作业4
-
MySQL作业5
-
MySQL作业6
-
MySQL作业7
-
MySQL作业8
-
MySQL内容梳理
第25章 JDBC和数据库连接池
- JDBC原理示意图
-
JDBC 为访问不同的数据库提供了统一的接口,为使用者屏蔽了使用细节。Java 程序员使用 JDBC API 可以连接任何提供了 JDBC 驱动程序的数据库系统,从而完成对数据库的各种操作
-
Java 设计者定义了操作数据库的接口规范,由各自数据库厂商具体实现。Java 程序员只需要面向这套接口编程即可。
-
JDBC模拟实现
-
JDBC快速入门
-
JDBC API 是一系列的接口,它统一和规范了应用程序与数据库的连接、执行 SQL 语句,并得到返回结果等各类操作,相关的接口和类在 java.sql 和 javax.sql 包中
-
JDBC 程序编写步骤:
- 注册驱动:加载 Driver 类
- 获取连接:获得 Connection 对象
- 执行 SQL:获得 Statement 对象
- 释放资源
- 数据库连接方式1:
package p823;
import com.mysql.jdbc.Driver;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/**
* 静态加载方式连接到数据库
* @author Spring-_-Bear
* @version 2021-11-08 20:56
*/
public class Jdbc01 {
public static void main(String[] args) throws SQLException {
// 连接到的数据库:jdbc:mysql://主机IP地址:端口/db_name
String url = "jdbc:mysql://localhost:3306/temp";
// 设置用户名与密码
Properties properties = new Properties();
properties.setProperty("user", "springbear");
properties.setProperty("password","123");
// 加载驱动: new com.mysql.jdbc.Driver()
Driver driver = new Driver();
// 获得连接
Connection connect = driver.connect(url, properties);
// SQL 语句
String insert = "INSERT INTO actor VALUES (NULL,'张三','男','1970-01-01','10086');";
// 得到一个执行静态 SQL 语句并返回其生成的结果的对象
Statement statement = connect.createStatement();
// 返回受影响的行数
int rows = statement.executeUpdate(insert);
System.out.println("返回受影响的行数 = " + rows);
statement.close();
connect.close();
}
}
- 数据库连接方式2
/**
* 方式1属于静态加载,灵活性差,依赖性强,考虑方式二使用反射机制
*/
public void connect02() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException {
// 连接到的数据库:jdbc:mysql://主机IP地址:端口/db_name
String url = "jdbc:mysql://localhost:3306/temp";
// 设置用户名与密码
Properties properties = new Properties();
properties.setProperty("user", "springbear");
properties.setProperty("password","123");
// 加载类信息
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
// 获得类实例
Driver driver = (Driver)aClass.newInstance();
// 获得连接
Connection connect = driver.connect(url, properties);
}
- 数据库连接方式3
/**
* 使用 DriverManager 替代 Driver,进行统一管理
*/
public void connect03() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException {
// 加载类信息
Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
// 获得类实例
Driver driver = (Driver)aClass.newInstance();
String url = "jdbc:mysql://localhost:3306/temp";
String user = "springbear";
String pwd = "123";
// 注册驱动
DriverManager.registerDriver(driver);
// 获得连接
Connection connection = DriverManager.getConnection(url, user, pwd);
}
- 数据库连接方式4
/**
* 使用 Class.forName 自动完成注册驱动
*/
public void connect04() throws ClassNotFoundException, SQLException {
// 加载类信息,在加载 Driver 的过程中自动完成注册
Class.forName("com.mysql.jdbc.Driver");
/*
* static {
* try {
* DriverManager.registerDriver(new Driver());
* } catch (SQLException var1) {
* throw new RuntimeException("Can't register driver!");
* }
* }
*/
String url = "jdbc:mysql://localhost:3306/temp";
String user = "springbear";
String pwd = "123";
// 获得连接
Connection connection = DriverManager.getConnection(url, user, pwd);
}
- 数据库连接方式5
- mysql-connector-java-5.1.37-bin.jar 驱动文件 5.1.6 之后无需 Class.forName(“com.mysql.jdbc.Driver”) 也可以直接获得连接。原因:从 jdk5 以后使用了 jdbc4,不再需要显式调用 Class.forName() 注册驱动,而是自动调用驱动 jar 包下的 META-INF\services\java.sql.Driver 文本中的类名称去注册
/**
* 在方式4的基础上使用配置文件进行连接,更加灵活
*/
public void connect05() throws IOException, ClassNotFoundException, SQLException {
// 从配置文件中读取信息
Properties properties = new Properties();
properties.load(new FileInputStream("src\\mysql.properties"));
String user = properties.getProperty("user");
String password = properties.getProperty("password");
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
// 加载类信息,自动注册驱动,获得连接
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);
}
- ResultSet底层
- ResultSet:表示从数据库读取到的数据表的结果集。ResultSet 对象保持一个光标指向当前的数据行。最初,光标位于第一行之前。其有一个 next 方法将光标移动到下一行,并且由于在 ResultSet 对象中没有更多行时返回 false,因此常用 while 循环来遍历结果集
- com.mysql.jdbc.JDBC42 ResultSet 类下有一个 RowData(接口) 类型的字段,rowData 中有一个 ArrayList 类型的集合 rows,rows 中又有 byte[] 类型的 internalRowData,数据真正存储的位置
- SQL注入
- Statement 对象,用于执行静态 SQL 语句并返回其生成结果的对象
- 在建立连接之后,想要对数据库进行操作,一般有以下三种方式:
- Statement:存在 SQL 注入
- PreparedStatement:预处理
- CallableStatement:用于执行数据库存储过程
- SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入的数据中注入非法的 SQL 语句段或命令,恶意攻击数据库
package p823;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
/**
* @author Spring-_-Bear
* @version 2021-11-09 09:48
*/
public class SqlInjection {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
// 获取用户想要查询的用户名和密码
// Input userName = 1' or
// Input pwd = or '1' = '1
Scanner scanner = new Scanner(System.in);
System.out.print("Input the name that you want to query:");
String userName = scanner.nextLine();
System.out.print("Input the password that you want to query:");
String pwd = scanner.nextLine();
// 加载配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("config\\temp.properties"));
// 加载驱动类信息,自动注册驱动
Class.forName(properties.getProperty("driver"));
// 获得连接
Connection connection = DriverManager.getConnection(properties.getProperty("url"), properties);
Statement statement = connection.createStatement();
String select = "SELECT * FROM admin WHERE name='" + userName + "' AND pwd= '" + pwd + "'";
ResultSet resultSet = statement.executeQuery(select);
while (resultSet.next()) {
userName = resultSet.getString(1);
pwd = resultSet.getString(2);
System.out.println(userName + "\t" + pwd);
}
resultSet.close();
statement.close();
connection.close();
}
}
-
Statement
-
预处理查询
- PreparedStatement 执行的 SQL 语句中的参数用问号(?)来表示,调用 PreparedStatement 对象的 setXxx() 方法来设置这些参数。setXxx() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引,从 1 开始,第二个参数是设置 SQL 语句中的参数的值
- 预处理的好处:
String select = "SELECT * FROM admin WHERE name = ? AND pwd= ?";
// SQL 语句预处理
PreparedStatement preparedStatement = connection.prepareStatement(select);
preparedStatement.setString(1, userName);
preparedStatement.setString(2, pwd);
// 执行 SQL 语句,得到结果集
ResultSet resultSet = preparedStatement.executeQuery();
-
预处理DML
-
JDBC API
接口名 | 方法名 | 功能 |
---|---|---|
Connection | createStatement() | 创建执行静态 SQL 语句的对象 |
createPreparedStatement(sql) | 获得 SQL 语句预处理对象 | |
Statement | executeUpdate(sql) | 执行 DML 语句,返回受影响行数 |
executeQuery(sql) | 执行 DQL 语句,返回结果集 | |
execute(sql) | 执行任意 SQL 语句,返回布尔值 | |
PreparedStatement | executeUpdate(sql) | 执行 DML 语句,返回受影响行数 |
executeQuery(sql) | 执行 DQL 语句,返回结果集 | |
execute(sql) | 执行任意 SQL 语句,返回布尔值 | |
setXxx(index,value) | 设置 SQL 语句中的值 | |
setObject(index,value) | 设置 SQL 语句中的值 | |
ResultSet | next() | 向下移动一行,到表尾返回 false |
previous() | 向上移动一行,到表头返回 false | |
getXxx(index || colLabel) | 获得指定列的值 | |
getObject(index || colLabel) | 获得指定列的值 |
- JDBC Utils开发
package utils;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
/**
* 数据库连接、资源关闭工具类
*
* @author Spring-_-Bear
* @version 2021-11-09 11:40
*/
public class JdbcUtils {
private static String driver;
private static String url;
private static String user;
private static String password;
private static String path = "config\\temp.properties";
/**
* 读取文件信息,初始化字段
*/
static {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(path));
driver = properties.getProperty("driver");
url = properties.getProperty("url");
user = properties.getProperty("user");
password = properties.getProperty("password");
} catch (IOException e) {
// 实际开发中,会将此类编译异常转换成运行异常,由调用方自行处理,较为方便
throw new RuntimeException(e);
}
}
/**
* 获得对数据库的连接
*
* @return 数据库连接对象
*/
public static Connection getConnection() {
try {
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
// 实际开发中,会将此类编译异常转换成运行异常,由调用方自行处理,较为方便
throw new RuntimeException(e);
}
}
/**
* 关闭对应资源
*
* @param resultSet none
* @param statement none
* @param connection none
*/
public static void close(ResultSet resultSet, Statement statement, Connection connection) {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
// 实际开发中,会将此类编译异常转换成运行异常,由调用方自行处理,较为方便
throw new RuntimeException(e);
}
}
}
-
JDBC Utils DML
-
JDBC Utils 查询
-
事务介绍
- JDBC 程序中当一个 Connectioon 对象被创建时,默认情况下是自动提交事务:每次执行一条 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
- 可以调用 Connection 接口的 setAutoCommit(false) 方法取消自动提交事务
- 在所有的 SQL 语句都执行成功后,调用 commit() 方法提交事务;在其中某个操作失败或出现异常时,调用 rollback() 方法回滚事务
- 事务处理
public void transaction() {
Connection connection = null;
PreparedStatement preparedStatement = null;
String sub = "UPDATE account SET balance = balance - 100 WHERE id = 1";
String add = "UPDATE account SET balance = balance + 100 WHERE id = 2";
try {
// 获得连接
connection = JdbcUtils.getConnection();
// 关闭自动提交即开启事务
connection.setAutoCommit(false);
// 执行 SQL
preparedStatement = connection.prepareStatement(add);
preparedStatement.executeUpdate();
int temp = 1 / 0;
preparedStatement = connection.prepareStatement(sub);
preparedStatement.executeUpdate();
// 提交事务
connection.commit();
System.out.println("所有 SQL 操作成功,提交事务!");
} catch (SQLException | ArithmeticException e) {
try {
// 发生异常,撤销操作,事务回滚
System.out.println("程序执行发生了异常,回滚所有操作!!!");
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
e.printStackTrace();
} finally {
JdbcUtils.close(null, preparedStatement, connection);
}
}
- 批处理应用
- 当需要成批插入或者更新记录时,可以采用 Java 的批处理更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率
方法名 | 功能 |
---|---|
addBatch() | 添加需要批量处理的 SQL 语句或参数 |
executeBatch() | 批量发送执行 |
clearBatch() | 清空批处理包 |
- JDBC 连接 MySQL 时,如果需要使用批处理功能,需要在 url 中添加参数:
url = "jdbc:mysql://localhost:3306/temp?rewriteBatchedStatements=true"
- 批处理往往和 PreparedStatement 一起搭配使用,既可以减少编译次数,又可以减少运行次数,效率大大提高
preparedStatement.addBatch();
if (i % 1000 == 0) {
preparedStatement.executeBatch();
preparedStatement.clearBatch();
}
- 批处理源码分析
/**
* 第一次会创建 ArrayList<elementData>
* elementData => Object[] 会存放预处理后的 SQL 语句
* 当 elementDate 满后会按照 1.5 倍扩容
* 当达到指定的容量之后,就会发送给 MySQL 执行
* 批处理会减少发送 SQL 语句的网络开销,减少编译次数,从而提高效率
* @throws SQLException none
*/
public void addBatch() throws SQLException {
synchronized(this.checkClosed().getConnectionMutex()) {
if (this.batchedArgs == null) {
this.batchedArgs = new ArrayList();
}
for(int i = 0; i < this.parameterValues.length; ++i) {
this.checkAllParametersSet(this.parameterValues[i], this.parameterStreams[i], i);
}
this.batchedArgs.add(new com.mysql.jdbc.PreparedStatement.BatchParams(this.parameterValues, this.parameterStreams, this.isStream, this.streamLengths, this.isNull));
}
}
- 传统链接弊端分析
- 传统的 JDBC 数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都需要将 Connection 加载到内存中,再验证 IP 地址、用户名、密码(耗时0.05s ~ 1s)是否正确。需要向数据库连接的时候,就向数据库请求一个连接,频繁的请求操作将占用过多的系统资源,容易造成服务器崩溃
- 每一次数据库连接,使用完后都得及时断开,如果程序出现异常而导致未能正常关闭,将导致数据库内存泄漏,最终导致数据库崩溃重启
- 传统获取连接的方式,不能控制创建连接的数量,如果连接过多,也可能导致内存泄漏,从而导致 MySQL 崩溃
- 数据库连接池原理
- 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕归还给缓冲池(并不断开与数据库的连接)
- 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个
- 当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列
- JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口具体实现留给第三方
连接池 | 特点 |
---|---|
C3P0 | 速度相对较慢,稳定性不错(hibernate、spring底层均采用) |
Druid | 阿里巴巴提供的数据库连接池,集 DBCP、C3P0、Proxool 优点于一身 |
Proxool | 有监控连接池状态的功能,稳定性较 C3P0 略差 |
BoneCP | 速度快 |
DBCP | 速度较 C3P0 快,但不稳定 |
- C3P0方式1
// 创建数据源对象
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 设置相关信息
comboPooledDataSource.setDriverClass(driver);
comboPooledDataSource.setJdbcUrl(url);
comboPooledDataSource.setUser(user);
comboPooledDataSource.setPassword(pwd);
comboPooledDataSource.setInitialPoolSize(10);
comboPooledDataSource.setMaxPoolSize(50);
// 获得连接
Connection connection = comboPooledDataSource.getConnection();
connection.close();
- C3P0方式2
// 将 c3p0-config.xml 文件拷贝到工程 src 目录下
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("spring_bear");
Connection connection = comboPooledDataSource.getConnection();
connection.close();
- 德鲁伊连接池
// 加载配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("config\\druid.properties"));
// 创建一个的连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 获得连接
Connection connection = dataSource.getConnection();
connection.close();
- 德鲁伊工具类
package utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/**
* Druid 连接池
*
* @author Spring-_-Bear
* @version 2021-11-09 22:17
*/
public class JdbcUtilsByDruid {
static DataSource dataSource;
static String path = "config\\druid.properties";
static {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(path));
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从 Druid 连接池中返回一个数据库连接
*
* @return Connection
* @throws SQLException none
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 关闭连接,将连接归还给连接池
*
* @param resultSet none
* @param statement none
* @param connection none
*/
public static void close(ResultSet resultSet, Statement statement, Connection connection) {
try {
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- ApDBUtils引出
- 关闭 connection 后,resultSet 结果集无法继续使用;然而很多时候我们希望关闭 connection 连接后仍然可以继续使用查询到的数据,resultSet 存储查询结果的方式不利于数据管理;从 resultSet 结果集中获取数据时操作方法不够明确,getXxx() 方法容易出错,含义模糊
- 定义一个类与数据库表的字段一一对应,这样的类一般称作 JavaBean 或 PoJo 或 Domain
- 将返回的结果集的字段值封装到自定义的类的对象中,将若干个这样的对象放进集合中,就可以直接访问集合从而获得数据库表的查询结果
-
土办法完成封装
-
ApDBUtils查询
- commons-dbutils 是 Apache 组织提供的一个开源 JDBC 工具类,它是对 JDBC 的封装,使用 dbutils 能极大简化 JDBC 编码量
- QueryRunner 类封装了 SQL 的执行,是线程安全的 ,可以实现增、删、改、查和批处理
- RessultSetHandler 接口用于处理 java.sql.ResultSet,将查询到的数据转换为另一种形式
接口名 | 功能 |
---|---|
ArrayHandler | 将结果集中的第一行数据转换成对象数组 |
ArrayListHandler | 将结果集中的每一行都转换成一个数组,再存放到 List 中 |
BeanHandler | 将结果集中的第一行数据封装到一个对应的 JavaBean 实例中 |
BeanListHandler | 将结果集中的每一行数据封装到对应的 JavaBean 实例中,存放到 List |
ColumnListHandler | 将结果集中的某一列数据存放到 List 中 |
KeyedHandler(name) | 将每行数据封装到 Map 中,再将 map 存入另一个 map 中 |
MapHandler | 将结果集的第一行数据封装到 Map 中,key 是列名,value 是对应值 |
MapListHandler | 将结果集中的每一行数据封装到 Map 中,再存放到 List 里 |
public void testApache() throws SQLException {
// 获得连接
Connection connection = JdbcUtilsByDruid.getConnection();
// 获得 Apache 实现的查询对象
QueryRunner queryRunner = new QueryRunner();
String select = "SELECT * FROM cost WHERE id >= ? AND id <= ?";
List<Fishing> fishings = queryRunner.query(connection, select, new BeanListHandler<>(Fishing.class), 1, 10);
for (Fishing fishing : fishings) {
System.out.println(fishing);
}
JdbcUtilsByDruid.close(null, null, connection);
}
- ApDBUtils源码分析
- 在创建 JavaBean 类时类的字段的数据类型强制使用八大基本数据类型对应的包装类,因为 MySQL 数据库表中的字段值可能为空,而 Java 只有引用数据类型才有 NULL 值
-
ApDBUtils查询2
-
ApDBUtilsDML
-
BasicDAO问题
- Apache-dbutils + Druid-connectionPoll 简化了 JDBC 开发,但仍有以下不足:
1. SQL 语句是固定的,不能通过参数传入,通用性不好,需进行改进,以方便 CRUD 操作
2. 对于查询 SELECT 操作,如果有返回值,返回类型不能确定,需要使用泛型解决
3. 未来数据库中的表会有很多,业务需求复杂,不可能只靠一个 Java 类完成
- 解决方案:为每个表设计一个 JavaBean 类,同时为每一张数据库表设计一个专门操作它的类 Dao,将所有的具体 Dao 类中的共有部分抽象出父类 BasciDao,以更好地利用多态完成功能
- DAO(data access object):访问数据库数据的对象
- 创建 JavaBeam 类的时候一定要给一个无参构造器,以方便反射机制获取该类信息
-
BasicDAO分析
-
BasicDAO实现1
package dao.dao;
import dao.utils.JdbcUtilsByDruid;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
/**
* 封装对数据库表的操作
*
* @author Spring-_-Bear
* @version 2021-11-10 11:03
*/
public class BasicDao<T> {
QueryRunner queryRunner = new QueryRunner();
/**
* DML操作
*
* @param sql SQL 语句
* @param params SQL 中参数的值
* @return 受影响的行数
*/
public int update(String sql, Object... params) {
Connection connection = null;
try {
connection = JdbcUtilsByDruid.getConnection();
return queryRunner.update(connection, sql, params);
} catch (SQLException e) {
// 将编译异常转换为运行异常,方便调用者处理
throw new RuntimeException(e);
} finally {
JdbcUtilsByDruid.close(null, null, connection);
}
}
/**
* 查询多行
*
* @param sql SQL 语句
* @param clazz 具体类的 Class 对象
* @param params SQL 语句中的具体值
* @return 从数据库查询到的结果集,封装进 ArrayList
*/
public List<T> getMultiRows(String sql, Class<T> clazz, Object... params) {
Connection connection = null;
try {
connection = JdbcUtilsByDruid.getConnection();
return queryRunner.query(connection, sql, new BeanListHandler<T>(clazz), params);
} catch (SQLException e) {
// 将编译异常转换为运行异常,方便调用者处理
throw new RuntimeException(e);
} finally {
JdbcUtilsByDruid.close(null, null, connection);
}
}
/**
* 查询一行
*
* @param sql SQL 语句
* @param clazz 具体类的 Class 对象
* @param params SQL 语句中的具体值
* @return 从数据库查询到的一行数据
*/
public T getOneRow(String sql, Class<T> clazz, Object... params) {
Connection connection = null;
try {
connection = JdbcUtilsByDruid.getConnection();
return queryRunner.query(connection, sql, new BeanHandler<T>(clazz), params);
} catch (SQLException e) {
// 将编译异常转换为运行异常,方便调用者处理
throw new RuntimeException(e);
} finally {
JdbcUtilsByDruid.close(null, null, connection);
}
}
/**
* 查询单行单列
*
* @param sql SQL 语句
* @param params SQL 语句中的具体值
* @return 从数据库查询到的一个单元格数据
*/
public Object getOneObj(String sql, Object... params) {
Connection connection = null;
try {
connection = JdbcUtilsByDruid.getConnection();
return queryRunner.query(connection, sql, new ScalarHandler(), params);
} catch (SQLException e) {
// 将编译异常转换为运行异常,方便调用者处理
throw new RuntimeException(e);
} finally {
JdbcUtilsByDruid.close(null, null, connection);
}
}
}
-
BasicDAO实现2
-
JDBC连接池梳理
第26章 满汉楼
-
满汉楼演示
-
满汉楼界面
-
满汉楼分层设计
-
满汉楼工具类
-
满汉楼菜单
-
满汉楼登录
-
满汉楼餐桌
-
满汉楼订座1
-
满汉楼订座2
-
满汉楼菜单
-
满汉楼点餐1
-
满汉楼点餐2
-
满汉楼点餐3
-
满汉楼账单
-
满汉楼结账1
-
满汉楼结账2
-
满汉楼多表处理
-
满汉楼多表细节1
-
满汉楼多表细节2
-
满汉楼扩展功能
第27章 正则表达式
- 正则快速入门
// 模式对象
Pattern pattern = Pattern.compile("[0-9]+");
// 匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环匹配
while (matcher.find()) {
System.out.println(matcher.group(0));
}
-
正则需求问题
-
正则底层实现1
-
正则底层实现2
// 匹配规则
String regExp = "\\d\\d\\d\\d";
// 模式对象
Pattern pattern = Pattern.compile(regExp);
// 匹配器对象
Matcher matcher = pattern.matcher(content);
// 循环匹配
while (matcher.find()) {
System.out.println(matcher.group(0));
}
/* return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString() */
- 根据指定的正则表达式,从给定的字符串中依次定位出满足匹配要求的子串
- 定位到子串时将子串首字符下标和尾字符下标 +1 记录到 matcher 对象的属性 int[] groups 的 group[0] 和 group[1] 中
- 同时记录 matcher 对象的属性 oldLast 的值为group[1],下一次匹配从 oldLast 开始
- 正则底层实现3
- 分组:在正则表达式中一对小括号表示一组,从 1 ······ n
String regExp = "(\\d)(\\d)(\\d)(\\d)";
- 分组过后,matcher 对象的属性 groups 数组的第 0,1 个元素依然记录子串首字符下标,尾字符下标 +1;下标从 2 开始往后的 groups 数组的元素,每相邻两个元素依次记录该组首字符下标,尾字符下标 +1 的下标值。比如说 2020 出现在给定字符串的第 323 ~ 326 位置,我们将字符串分成两组,则有:
groups[0] = 323、groups[1] = 327;
groups[2] = 323、groups[3] = 325;
groups[4] = 325、groups[5] = 327
- 正则转义符
- 正则表达式语法 - 元字符(Meta character)
1. 限定符
2. 选择匹配符
3. 分组组合和反向引用符
4. 特殊字符
5. 字符匹配符
6. 定位符
- 转义符 \ \ : 在我们需要使用正则表达式去检索某些特殊字符的时候,需要用到转义符号,否则检索不到结果。在 Java 的正则表达式中,两个 \ \ 代表其它语言中的一个 \ 。需要用到转义符的字符主要有以下几个:. * + ( ) $ / \ ? [ ] ^ { }
- 正则字符匹配
符号 | 意义 | 示例 | 解释 |
---|---|---|---|
[ ] | 可接收的字符列表 | [efgh] | e、f、g、h 中的一个字符 |
[^] | 不接收的字符列表 | [^abc] | 除 a、b、c 外的任意字符 |
- | 连字符 | A-Z | 任意单个大写字母 |
. | 除 \n 以外的任意字符 | a…b | a 开头,b 结尾,长度为 4 的字符串 |
\ \d | [0-9] | \ \d{3}(\ \d)? | 长度为 3 或 4 的数字字符串 |
\ \D | [^0-9] | \ \D(\ \d)* | 非数字开头,后接任意个数字字符 |
\ \w | [0-9a-zA-Z_] | \ \d{3}\ \w{4} | 3 个数字字符开头长度为 7 的数字字母字符串 |
\ \W | [^0-9a-zA-Z_] | \ \W+\ \d{2} | 至少 1 个非数字字母开头,2个数字字符结尾 |
\ \s | 任何空白字符 | \ \d\ \ s \ \D | 数字字符开头,夹一个空白字符,非数字结尾 |
\ \S | 任何非空白字符 | \ \S | 匹配所有的非空白字符 |
- 字符匹配案例1
- Java 正则表达式实现不区分大小写的两种方式:
1. (?i)abc:abc 都不区分大小写;a(?i)bc:bc 不区分大小写
2. Pattern pattern = Pattern.compile(regExp, Pattern.CASE_INSENSITIVE);
- 字符匹配案例2
- 选择匹配符
符号 | 意义 | 示例 | 解释 |
---|---|---|---|
| | 匹配 | 之前或之后的串 | ab|cd | ab 或 cd |
- 正则限定符
符号 | 意义 | 示例 | 解释 |
---|---|---|---|
* | 字符出现 0 次或 n 次 | (abc)* | 含有任意个 abc 的字符串 |
+ | 字符出现 1 次或 n 次 | m+(abc)* | 至少 1 个 m 开头,后接任意个 abc |
? | 字符出现 0 次或 1 次 | m+abc? | 至少一个 m 开头,后接 ab 或 abc |
{n} | 指定长度 | [abcd]{3} | abcd 中长度为 3 的子串 |
{n,} | 长度大于等于 n | [abcd]{3,} | abcd 中长度大于等于 3 的子串 |
{n,m} | 长度大于等于 n 小于等于 m | [abcd]{3,5} | abcd 中长度大于等于 3 小于等于 5 的子串 |
- Java 匹配模式默认贪婪匹配,尽量匹配长度较长的串。例如:str = aaaa regExp = “\ \ {3,4}”,则结果为 aaaa
- 正则定位符
符号 | 意义 | 示例 | 解释 |
---|---|---|---|
^ | 指定起始字符 | ^[0-9]+[a-z] | 至少 1 个数字开头,后接任意个小写字母 |
$ | 指定结束字符 | ^[0-9]\ \ -[a-z]+$ | 至少 1 个数字开头,夹 -,保证小写字母结尾 |
\ \b | 目标字符串边界 | han\ \b | 以字符串结尾或其后有空格,不需要括号 |
\ \B | 字符串非边界 | han\ \B | 字符串其后无空格或不是结尾字符串 |
- 捕获分组
- 非命名分组:
String regExp = "(\\d\\d)(\\d)(\\d)";
// matcher.group[0] = \\d\\d\\d\\d
// matcher.group[1] = \\d\\d
// matcher.group[2] = \\d
// matcher.group[3] = \\d
- 命名分组:将匹配的子字符串捕获到一个组名称或编号名称中。用于 name 的字符串不能包含任何标点符号并且不能以数字开头。可用单引号代替尖括号
String regExp = "(?<name>\\d\\d)(?<name>\\d\\d)";
// matcher.group[0] = \\d\\d\\d\\d
// matcher.group["one"] = \\d\\d
// matcher.group["two"] = \\d\\d
- 非捕获分组
- 不能使用 mather.group[1] 或 matcher.group[2···] 获取结果
"industr(?:y|ies)" <=> "industry|industries"
"windows(?=95|98|2000)":从 windows95 || windows98 || windows2000 中匹配出 windows
"windows(?!95|98|2000)":不从 windows95 || windows98 || windows2000 中匹配出 windows
- 非贪婪匹配
// 非贪婪匹配,匹配出长度尽可能短的字符串
String regExp = "1+?";
- 正则应用案例
// 匹配基本汉字
String regExp = "[\u4E00-\u9FA5]";
- 正则验证复杂URL
// 匹配是否是网址
String regExp = "(((https|http)?://)?([a-z0-9]+[.])|(www.))\\w+[.|\\/]([a-z0-9]{0,})?[[.]([a-z0-9]{0,})]+((/[\\S&&[^,;\u4E00-\u9FA5]]+)+)?([.][a-z0-9]{0,}+|/?)";
- Pattern类
// 整体匹配:判断传入的 content 内容是否符合 regExp 正则表达式要求
boolean isMatch = Pattern.matches(regExp,content);
- Matcher类
方法名 | 功能 |
---|---|
int start() | 返回匹配成功串的起始索引 |
int end() | 返回匹配成功串的结束索引 +1 |
String replaceAll(String) | 用参数替换正则表达式匹配到的字符串,并返回新字符串 |
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Spring-_-Bear
* @version 2021-11-11 20:28
*/
public class RegularExpression {
public static void main(String[] args) {
String content = "hello hell hello";
String regExp = "hello";
Pattern pattern = Pattern.compile(regExp, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.print(matcher.start() + "\t");
System.out.println(matcher.end());
}
// 原字符串 content 保持不变
String newString = matcher.replaceAll("李春雄");
System.out.println(newString);
}
}
- 反向引用
- 分组:我们可以用圆括号组成一个比较复杂的匹配模式,那么每个圆括号的部分我们可以看作是一个分组(也称为一个子表达式)
- 捕获:把正则表达式中分组匹配到的内容,保存到内存中以数字编号或显式命名的组里,方便后面引用。从左往右,以分组的左括号为标志,第一个出现的分组的组号为 1,第二个为 2,以此类推。编号为 0 的组代表整个正则式
- 圆括号的内容被捕获后,可以在这个括号后被使用,从而写出一个比较实用的匹配模式,我们称之为反向引用。这种引用既可以在正则表达式内部,也可以在正则表达式外部。正则表达式内部反向引用 \ \ 分组号,正则表达式外部反向引用 $ 分组号
- 反向引用案例
// 匹配 2 个连续相同的数字
String regExp = "(\\d)\\1";
// 匹配 5 个连续相同的数字
String regExp = "(\\d)\\1{4}";
// 匹配 4 位数的回文数,分别反向引用第 2 组,第 1 组
String regExp = "(\\d)(\\d)\\2\\1";
// 匹配类似 12321-333999111
String regExp = "\\d{5}-(\\d)\\1{2}(\\d)\\2{2}(\\d)\\3{2}";
- 结巴去重案例
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 结巴去重
*
* @author Spring-_-Bear
* @version 2021-11-11 20:28
*/
public class RegularExpression {
public static void main(String[] args) {
String content = "我...我...我...要要要..要要.学....Java!";
// 1. 先替换调所有的 .
String regExp = "\\.";
Pattern pattern = Pattern.compile(regExp);
Matcher matcher = pattern.matcher(content);
content = matcher.replaceAll("");
System.out.println(content);
// 2. 匹配出重复的汉字,重复 1 到 n 次
regExp = "(.)\\1+";
// content = Pattern.compile(regExp).matcher(content).replaceAll("$1");
pattern = Pattern.compile(regExp);
matcher = pattern.matcher(content);
// 3. 反向引用分组中的内容替换正则表达式匹配到的内容:group[1] = "我" -> "我我我"
content = matcher.replaceAll("$1");
System.out.println(content);
}
}
- 替换分割匹配
- String 类中使用正则表达式
- 替换功能:public String replaceAll(String regex,String replacement)
- 判断功能:public boolean matches(String regex)
- 分割功能:public String[] split(String regex)
- 本章练习1
- 本章练习2
- 本章练习3
- 正则内容梳理
第28章 算法优化体验课 - 骑士周游问题
-
马踏棋盘介绍
-
马踏棋盘实现1
-
马踏棋盘实现2
-
马踏棋盘实现3
-
马踏棋盘优化
-
第三阶段结束语