文章目录
- 一、基础语法
- 1.1 环境变量配置
- 1.2 JDK、JRE、JVM
- 1.3 基本数据类型
- 1.4 运算符
- 1.5 保留字
- 二、面向对象
- 2.1 数组
- 2.2 字符串
- 2.3 构造器
- 2.4 继承与多态
- 2.5 super 和 this
- 2.6 == 和 equals()
- 2.7 重写和重载
- 2.8 可变参数
- 2.9 类字段与类方法
- 2.10 代码块和类加载
- 2.11 单例设计模式
- 2.12 final
- 2.13 抽象类
- 2.14 接口
- 2.15 内部类
- 三、枚举与注解
- 3.1 自定义枚举类
- 3.2 enum 枚举类
- 3.3 Enum 类成员方法
- 3.4 注解介绍
- 3.5 三个常用注解
- 3.6 四个元注解
- 四、异常与泛型
- 4.1 异常体系图
- 4.2 try-catch-finally-return
- 4.3 自定义异常
- 4.4 throws 和 throw
- 4.5 泛型介绍
- 4.6 泛型类型
- 4.7 自定义泛型
- 五、常用类
- 5.1 包装类
- 5.2 String
- 5.3 StringBuffer
- 5.4 Arrays
- 5.5 BigDecimal
- 5.6 日期类
- 六、集合类
- 6.1 Collection
- 6.2 Iterator
- 6.3 List
- 6.4 Map
- 6.5 Set
- 6.6 集合遍历
- 6.7 集合选择
- 6.8 Collections
- 七、多线程
- 7.1 线程概述
- 7.2 线程创建
- 7.3 start 与 run
- 7.4 Thread
- 7.5 线程插队与礼让
- 7.6 守护线程
- 7.7 线程的七大状态
- 7.8 线程同步机制
- 7.9 线程死锁
- 7.10 多线程卖票
- 八、IO
- 8.1 File
- 8.2 IO 流的分类
- 8.3 字节流与字符流
- 8.4 节点流与处理流
- 8.5 对象处理流
- 8.6 标准输入输出流
- 8.7 转换流
- 8.8 Properties
- 九、网络编程
- 9.1 IP 与域名
- 9.2 端口与协议
- 9.3 InetAddres
- 9.4 TCP、UDP 与 Socket
- 9.5 TCP 编程
- 9.6 UDP 编程
- 9.7 netstat
- 十、反射
- 10.1 反射机制
- 10.2 程序三阶段
- 10.3 Class
- 10.4 类加载
- 10.5 反射爆破
- 十一、JDBC
- 11.1 JDBC 原理
- 11.2 五种连接方式
- 11.3 ResultSet
- 11.4 SQL 注入
- 11.5 PreparedStatement
- 11.6 JDBC 事务操作
- 11.7 批处理
- 11.8 数据库连接池
- 11.9 DBUtils
一、基础语法
1.1 环境变量配置
- Windows 环境变量配置:
- JAVA_HOME =
D:\Java\jdk1.8.0_131
- Path =
%JAVA_HOME%\bin
- CLASSPATH =
.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
- Linux 环境变量配置:
export JAVA_HOME=/usr/local/java/jdk1.8.0_311
export PATH=$JAVA_HOME/bin:$PATH
1.2 JDK、JRE、JVM
-
JDK
= JRE + Java 开发工具(java、javac、javap、javadoc ······) -
JRE
= JVM + Java 核心类库(dt.jar、tools.jar) -
JVM
是一个虚拟的计算机,具有指令集并使用不同的存储区域。负责执行指令,管理和寄存器,包含在 JRE 中
1.3 基本数据类型
- Java 数据类型中
没有无符号类型
- Java 浮点类型默认为 double 类型,定义 float 类型时需要在末尾加上 F 或 f,如
3.14F
- 浮点数组成:符号位 + 指数位 + 尾数位
- Java 对布尔类型的存储并没有做规定,因为理论上存储布尔类型只需要
1 bit
,但通常 JVM 内部会把 boolean 表示为4 字节
整数 - 八大基本数据类型的数据范围:最高位用作符号位
类型 | 字节数 | 范围 |
byte | 1 | [-27, 27-1] => [-128, 127] |
boolean | 1 | true or false |
char | 2 | ISO 单一字符 |
short | 2 | [-215, 215-1] => [-32768, 32767] |
int | 4 | [-231, 231-1] = > [-2147483648, 2147483647] |
float | 4 | -3.403E38 - 3.403E38 |
long | 8 | [-263, 263-1] => [-9223372036854774808, 9223372036854774807] |
double | 8 | -1.798E308 - 1.798E308 |
- 自动类型转换:
- byte 自动类型:byte -> short -> int -> long -> float -> double
- char 自动类型:char -> int -> long -> float -> double(
char 直转为 int
) - byte、char、short 在进行运算时,当作 int 处理
- 浮点数运算和整数运算相比,只能进行加减乘除这些数值计算,不能做
位运算和移位运算
- 整数运算在除数为 0 时会报错,而
浮点数
运算在除数为 0 时不会报错,但会返回以下几个特殊值(NaN:Not a Number、Infinity:无穷大、-Infinity:负无穷大)
// java.lang.ArithmeticException
int i = 10 / 0;
// j = Infinity
double j = 10.0 / 0;
- 如果强制类型转换后超过了整型能表示的最大范围,将返回整型的最大值
double d = Double.MAX_VALUE;
// max = Integer.MAX_VALUE = 2147483647
int max = (int) d;
- 如果要进行四舍五入,可以对浮点数加上 0.5 再强制转型
double a = 1.2;
// 对浮点数四舍五入
a += 0.5;
int b = (int) a;
1.4 运算符
- 自增、自减运算符
int a = 1;
int b = 1;
// the value changed at 'a++' is never used
a = a++;
b = ++b;
// output: a = 1, b = 2
System.out.println("a = " + a + ", b = " + b);
int a = 3;
// b = 3 + 5
int b = a++ + ++a;
// output: a = 5, b = 8
System.out.println("a = " + a + ", b = " + b);
- 三元运算符
Object object = true ? new Integer(1) : new Double(2.0);
// 三元运算符需要看作一个整体,故输出 1.0;if-else 分支结构则输出 1
// output: 1.0
System.out.println(object);
- 位运算
- 算术右移(>>):低位丢弃,符号位不变,高位补符号位
- 算术左移(<<):符号位不变,低位补 0
- 无符号右移(>>>):低位丢弃,符号位不变,高位补 0
- 运算符优先级:
自上而下优先级依次降低
运算顺序 | 操作符 |
. () {} ; , | |
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 | >>>= &= ^= |= |
1.5 保留字
- assert:断言
- const:预留关键字
- goto:预留关键字
- transient:使用 transient 修饰的字段在对象序列化时该字段不会被序列化
- strictfp:
strict float point
(精确浮点) ,strictfp 关键字可应用于类、接口、方法。使用 strictfp 关键字声明一个方法时,该方法中所有的 float 和 double 表达式都严格遵守 FP-strict 的限制,符合 IEEE-754 规范
strictfp double add(double a, double b) {
return a + b;
}
- volatile:JVM 提供的轻量级同步机制。作用是:保证可见性、禁止指令重排、不保证原子性
- switch:switch 的 case 后跟常量或常量表达式,switch 中的表达式只能是以下类型中的一种:
byte、short、char、int、enum、String
二、面向对象
2.1 数组
- 静态初始化方式创建数组
int[] arr = new int[]{1, 2, 3};
int[][] matrix = new int[][]{{1, 2}, {3, 4, 5}, {6, 7, 8, 9}};
// output:[[1, 2], [3, 4, 5], [6, 7, 8, 9]]
System.out.println(Arrays.deepToString(matrix));
- 数组默认值:new 方式创建数组,若没有赋值则有默认值,由此可看出数组是引用类型
float[] arr = new float[5];
// output: [0.0, 0.0, 0.0, 0.0, 0.0]
System.out.println(Arrays.toString(arr));
2.2 字符串
- Java 使用
Unicode
字符集,所以一个英文字符和一个中文字符都用一个char
类型表示,占用2
字节 - 常用字符集占用的字节数:
字符集 | 字符占用字节数 | 汉字占用字节数 |
ASCII | 1 | 不支持中文 |
Unicode | 2 | 2 |
UTF - 8 | 1 | 3 |
GBK | 1 | 2 |
2.3 构造器
- 调用构造器时堆中已经分配了对象的空间,构造器只负责对象的初始化,并不创建对象
- 字段默认值:类的属性有默认值,局部变量没有默认值(
数组除外
) - 对象创建流程:
- 加载类信息,生成
.class
对象 - 在堆中为对象分配空间
- 栈:一般存放基本数据类型(局部变量)
- 堆:存放对象(数组)
- 方法区:常量池(字符串)、类加载信息
- 完成对象的默认初始化
- 构造器完成对象的初始化
2.4 继承与多态
- 子类继承了父类所有的方法和属性,但子类不能直接访问父类的私有属性
- 当创建子类的对象时,无论调用了子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类中使用
super(...)
显式地调用父类的构造器完成父类的初始化工作,否则编译通不过
super()
和this()
都只能放在构造器的第一行
,因此这两个方法不能同在一个构造器中instanceof
实际上判断的是一个变量所指向的实例是否是指定类型,或者这个类型的子类型
- 多态:针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法(谓之动态绑定机制)
2.5 super 和 this
区别点 | this | super |
属性 | 访问本类属性,没有则逐层查找父类 | 查找父类属性 |
方法 | 访问本类方法,没有则逐层查找父类 | 查找父类方法 |
构造器 | 行首调用本类其它构造器 | 行首调用父类指定构造器 |
特殊点 | 表示当前对象 | 表示子类中访问父类的对象 |
- 不能使用 super 访问父类的
private
成员 - this 不能在类定义的外部使用,只能在类定义的方法中使用
2.6 == 和 equals()
作用 | |
== | 判断基本类型时判断值是否相等;判断引用类型时判断是否引用到同一个对象 |
equals() | 只能判断引用类型是否引用到同一个对象,子类往往重写从而实现判断内容相等 |
2.7 重写和重载
- 重写:类实现接口或子类继承父类方法是发生重写
- 重写时返回类型可以是父类或父类的子类
- 重写时不能缩小父类方法的访问权限
- 重载:方法重载需保证方法签名不同(返回值不是方法签名的一部分)
2.8 可变参数
- 可变参数的
实参
可以是 0 个或是多个,其本质就是数组 - 方法的形参列表中可以同时有普通形参和一个可变参数,但必须保证可变参数是形参列表的最后一个参数
public double add(double a, int b, int... args) {
double sum = a + b;
for (int c : args) {
sum += c;
}
return sum;
}
2.9 类字段与类方法
- 类变量也即静态变量,类方法也即静态方法
- JDK8 以前类变量放在
方法区的静态域
中,JDK8 以后放在堆中类的 class 对象尾部
- 静态代码块、静态方法属于
类
而不属于类实例,只能调用静态成员;普通方法和普通代码块可以调用任何成员 - 当方法中不涉及到与任何对象相关的成员时,则可以将方法设计为静态方法以提高运行效率
2.10 代码块和类加载
- 代码块可以理解为只有方法体的方法,它没有方法名、返回值、形参列表,不能通过对象或类显式调用,只有在加载类时自动隐式调用
- 代码块的修饰符只能是 static 或无修饰符,块体中的语句可以是任何正确逻辑语句
- 如果只是调用类的静态成员,代码块并不会执行(类未加载)
public class Test {
public static void main(String[] args) {
// output: 2
System.out.println(GirlFriend.AGE);
}
}
class GirlFriend {
public static final int AGE = 2;
// 当使用到 GirlFriend.AGE 时,类未加载,代码块不会被执行
static {
System.out.println("I am your only girlfriend");
}
{
System.out.println("I am your second girlfriend");
}
}
- 类代码块的执行顺序优先于构造器
public class Test {
public static void main(String[] args) {
/*
* output:
* I am your only girlfriend
* I am your second girlfriend
* I am your third girlfriend, my name is name
*/
new GirlFriend("Alice");
}
}
class GirlFriend {
public static final int AGE = 2;
public GirlFriend(String name) {
System.out.println("I am your third girlfriend, my name is " + name);
}
// 当使用到 GirlFriend.AGE 时,类未加载,代码块不会被执行
static {
System.out.println("I am your only girlfriend");
}
{
System.out.println("I am your second girlfriend");
}
}
- 类加载时机:创建该类的对象时、创建子类的对象时父类也会加载
- Java 代码执行顺序:
- 父类的静态代码块和静态属性
- 子类的静态代码块和静态属性
- 父类的普通代码块与普通属性
- 父类的构造器
- 子类的普通代码块与普通属性
- 子类的构造器
2.11 单例设计模式
- 设计模式:在大量的实践中总结和理论化后优选的代码结构、编程风格以及解决问题的思考方式。设计模式好比经典的棋谱,不同的棋局采用不同的棋谱,免去再次思考和摸索的过程
- 类的单例设计模式:采取一定的方法使得在整个软件系统中,某个类有且仅有一个对象实例,并且该类只提供一个取得该对象的方法
- 单例设计模式分为饿汉式和懒汉式:
- 饿汉式在类加载时就创建实例,不存在线程安全问题
- 懒汉式在使用到对象时才创建,存在线程安全问题
- 单例模式的实现:
- 构造器私有化
- 在类的内部创建对象
- 向外部提供一个静态的公共方法取得该对象
/**
* 单例设计模式:饿汉式
*/
class GirlFriend {
private GirlFriend() {
}
private static final GirlFriend girlFriend = new GirlFriend();
public static GirlFriend getGirlFriend() {
return girlFriend;
}
}
/**
* 单例设计模式:懒汉式
*/
class BoyFriend {
private BoyFriend() {
}
private static BoyFriend boyFriend;
public static BoyFriend getBoyFriend() {
if (boyFriend == null) {
boyFriend = new BoyFriend();
}
return boyFriend;
}
}
java.lang.Runtime
类就是经典的懒汉式单例模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
2.12 final
- final 的使用场景:
- 不希望某个类被继承
- 不希望父类的方法被子类重写
- 不希望类的成员值被修改
- 不希望局部变量的值被修改
- final 修饰的属性必须初始化,初始化后不能更改。初始化可在三个地方进行:
- 定义时初始化
- 构造器中初始化
- 代码块中初始化
public static final int MAX = 9;
public static final int MIN;
static {
MIN = -1;
}
- 如果非 final 类中含有 final 修饰的方法,则该方法可以被继承但不能被重写
- final 和 static 搭配使用效率更高,因为底层编译器做了优化处理,不会导致类加载
2.13 抽象类
- 当父类的某些方法需要被声明,但又不确定如何实现时,可以将其声明为抽象方法,那么这个类就是抽象类
有抽象方法的类一定是抽象类,抽象类不一定有抽象方法
,abstract 只可以修饰类和方法。抽象方法没有方法体
public abstract class Node {
public int getMax() {
return 100;
}
}
- 如果一个类继承自抽象类,则必须实现父类的所有抽象方法,除非它也是一个抽象类
- 抽象方法不能使用 private、final、static 来修饰,因为这些修饰符与重写相违背
2.14 接口
- JDK8 之前的版本,接口中所有的方法都没有方法体即全是抽象方法。JDK8 及之后的版本,接口中可以有静态字段(默认是 public static final)、默认方法
interface Study {
/*
* 类实现接口时拥有此默认方法
*/
default void english() {
System.out.println("Study english");
}
static void math() {
System.out.println("Study math");
}
}
- 抽象类实现接口可以不用实现方法、接口继承接口时可以不用实现方法
- 接口对比:
- Java 的接口特指
interface
的定义,表示一个接口类型和一组方法签名 - 编程接口泛指接口规范,如方法签名,数据格式,网络协议等
- 继承与接口对比:
- 继承解决代码的复用性与可维护性
- 接口用来设计规范方法,一定程度上实现代码解耦
2.15 内部类
- 局部内部类:定义在外部类的局部位置且有类名,比如方法或代码块中。具有以下特点:
- 局部内部类可以理解为方法的局部变量,不可以添加访问修饰符,但可以添加 final
- 内部类可以直接访问外部类的所有成员,包括私有成员
- 如果外部类和局部内部类的成员重名时,默认遵循就近原则。若想访问外部类的成员,可以使用
外部类名.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();
}
}
- 匿名内部类:定义在外部类的局部位置且没有类名,比如方法或代码块。好比一个没有名字的局部变量。经典使用场景是在方法参数位置直接实现接口当作实参传递,简洁高效不冗余
- 匿名内部类本身既是一个类的定义同时也是一个对象,因而从语法层面看,它既有类的特征也有对象的特征
public class Anonymous {
public static void main(String[] args) {
Cry cry = new Cry() {
@Override
public void cry() {
System.out.println("cry~~~");
}
};
cry.cry();
System.out.println(cry.getClass());
new Cry() {
@Override
public void cry() {
System.out.println("cry~~~");
}
}.cry();
}
}
interface Cry {
void cry();
}
- 成员内部类:成员内部类可以理解为类的成员
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);
}
}
}
- 静态内部类:静态内部类可以理解类的静态成员,可以直接访问外部类的所有静态成员,但不能访问非静态成员
三、枚举与注解
3.1 自定义枚举类
- 枚举:枚举是一组常量的集合,属于一种特殊的类,里面只包含一组有限的特定对象
- 自定义枚举类:
- 构造器私有化
- 不提供 setXxx() 方法
- 对枚举属性使用
statc + final
修饰以实现底层优化
class Season {
private String name;
private 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;
}
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
3.2 enum 枚举类
enum 修饰的类默认继承 Enum
类并且是一个 final
类,故不能继承其它类,但可以实现接口
enum Season {
/**
* 必须位于行首且以逗号间隔,分号结尾。如果使用的是无参构造器创建枚举对象,则括号可以省略
*/
SPRING("春天", "温暖"),
SUMMER("夏天", "炎热"),
AUTUMN("秋天", "凉爽"),
WINTER("冬天", "寒冷");
private final String name;
private final String description;
Season(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
'}';
}
}
3.3 Enum 类成员方法
方法 | 功能 |
toString() | 返回当前对象名,子类可重写 |
name() | 返回当前对象常量名,子类不可重写 |
ordinal() | 返回当前对象的索引号,默认从 0 开始 |
values() | 返回当前枚举类中的所有常量 |
valueOf() | 将所给字符串包装为枚举对象,若该枚举对象不存在则抛异常 |
compareTo() | 比较两个枚举常量的索引号 |
3.4 注解介绍
- 注解(
Annotation
)也称为元数据(Metadata
),用于修饰解释包、类、方法、属性、构造器、局部变量等数据信息。与注释一样,注解不影响程序逻辑,但可以被编译或运行,相当于嵌入在代码中的补充信息 - 在 Java SE 中,注解的使用目的比较简单,例如标记过时的方法、忽略警告等。但注解在 Java EE 中注解占据重要角色,例如用来配置应用程序的任何切面、代替 Java EE 旧版中所遗留的冗杂代码和 XML 配置等
3.5 三个常用注解
- @Override:重写方法时如果添加了
@Override
注解,则编译器就会检查是否真的重写了父类的方法,如果未重写则编译报错
// @Target(ElementType.METHOD) 是修饰注解的注解,称为元注解,用于说明此注解作用位置
@Target(ElementType.METHOD)
// @Retention(RetentionPolicy.SOURCE) 也是元注解,说明此注解的作用域(源码、运行时···)
@Retention(RetentionPolicy.SOURCE)
// @interface 并不指此类是接口,而是说明此类是注解类,在 JDK1.5 之后加入
public @interface Override {
}
- @Deprecated:标记该方法已过时
- @SupressWarning:抑制编译器警告
3.6 四个元注解
- @Retention:指定注解的作用域,如
SOURCE、CLASS、RUNTIME
- @Target:指定注解的使用位置,如
TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
- @Documented:指定注解在 javadoc 中体现,使用 @Documented 元注解的注解 @Retention 必须为
RUNTIME
- @Inherited:指定子类可以继承父类的注解
四、异常与泛型
4.1 异常体系图
-
Error
(错误):Java 虚拟机无法解决的严重问题,如 JVM 系统内部错误、资源耗尽等严重情况 -
Exception
(异常):其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。分为编译时异常和运行时异常
4.2 try-catch-finally-return
- 如果出现异常,则 try 块中发生异常语句后的语句不再执行
- 若在 finally 块中返回,则 try 或 catch 块中的 return 必定不会执行
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 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 3。 i = 3 时将结果存入临时空间,finally 执行完后返回临时空间的值 3
return ++i;
} finally {
++i;
}
}
4.3 自定义异常
- 如果继承 Exception,则属于编译异常,必须显式处理或抛出
- 如果继承 RuntimeException,则属于运行时异常,如果处理则默认 throws
- 子类重写父类的方法时,子类抛出的异常需小于等于父类定义的异常类型
4.4 throws 和 throw
含义 | 位置 | 抛出内容 | |
throws | 异常处理的一种方式 | 方法声明处 | 异常类型 |
throw | 手动生成异常对象 | 方法体中 | 异常对象 |
4.5 泛型介绍
- 泛型的优点:
- 不使用泛型的弊端:不能对加入到集合中的数据类型进行约束(不安全)、遍历集合时需要进行显式类型转换
- 使用泛型的好处:编译时检查元素的类型提高了安全性、减少了类型转换的次数提高了运行效率
- 泛型定义:泛型又称为
参数化类型
,是 jdk5.0 出现的新特性,解决数据类型的安全性问题,只需在类声明或实例化时指定具体需要的类型即可,编译期即可确定类型。Java 泛型保证在编译期没有发出警告的代码,在运行阶段就不会发生类转换异常,使得代码更加简洁与健壮 - 泛型的使用规则:可以在类声明时通过一个标识符表示类中某个属性的类型,或是某个方法的返回值类型,或是参数类型。指定泛型类型时只能是
引用类型
。在给泛型指定具体类型后,可以传入该类型或其子类类型。省略指定泛型类型时,默认填充Object
4.6 泛型类型
-
<?>
:支持任意泛型类型 -
<? extends A>
:规定了泛型的上限,支持 A 类以及 A 的子类 -
<? super A>
:规定了泛型的下限,支持 A 类以及 A 的直接或间接父类
4.7 自定义泛型
- 自定义泛型的注意事项:
- 静态方法、静态字段中不能使用类的泛型
- 使用泛型的数组,不能进行初始化
- 泛型不具备继承性,即以下代码语法错误
List<Object> lists = new ArrayList<String>();
- 泛型方法可以定义在普通类中、也可以定义在泛型类中
/**
* 泛型类
*
* @author Spring-_-Bear
* @datetime 5/28/2022 8:57 AM
*/
public class Generic<R> {
private R r;
public R getR() {
return r;
}
// 泛型方法
public <T> T getT(T t) {
return t;
}
}
- 泛型接口的类型,在继承接口或是实现接口时确定
interface IGeneric<T> {
T getT();
}
// 实现接口时指定接口的泛型
class GenericImpl implements IGeneric<Integer> {
@Override
public Integer getT() {
return 3;
}
}
// 继承接口时指定接口的泛型
interface IGeneric2 extends IGeneric<String> {
}
五、常用类
5.1 包装类
- 八大包装类都是 final 类。除 Boolean 和 Character 外,其余包装类均继承父类 Number
- 装箱与拆箱:jdk5 以前是手动装箱和拆箱。自动装箱底层调用的是对应包装类的 valueOf() 方法
// 手动装箱
Integer integer = Integer.valueOf(23);
// 手动拆箱
int a = integer.intValue();
- String 与包装类:
- String 转包装类
String str = "123";
// 方式 1
Integer i = Integer.parseInt(str);
// 方式 2
Integer i = new Integer(str);
- 包装类转 String
Integer i = 100;
// 方式 1
String str = i + "";
// 方式 2
String str = i.toString();
// 方式 3
String str = String.valueOf(i);
- Integer 的创建机制:自动装箱机制,在
[-128,127]
范围内直接返回,否则 new Integer(i)
/**
* Integer 包装类装箱源码
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/* 经典例题 */
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);
// 只要有基本数据类型参与比较,判断的是值是否相等
Integer j = 128;
int jj = 128;
// true
System.out.println(j == jj);
5.2 String
- String 是一个线程不安全的 final 类,其字段 value[] 是 final 类型的
- String 的创建机制:
-
String s1 = "lcx";
先查找常量池是否有 “lcx” 数据空间,如果有则直接将栈变量 s1 指向 “lcx”;没有则新建后指向,s1 最终指向的是常量池的空间地址 -
String s2 = new String("lcx");
先在堆中创建空间,维护了 String 类字段value[]
,然后判断常量池中是否有 “lcx” 的数据空间。如果常量池没有 “lcx”,则新建后使用 value 指向;如果有,则 value 直接指向。s2 最终指向的是堆中 value 的地址
- String 创建机制经典例题:
String a = "lcx";
String b = new String("lcx");
// true:a 指向常量池中 "lcx" 的地址,b.intern() 返回常量池中 "lcx" 的地址
System.out.println(a == b.intern());
// false:b 返回 String 类的字段 value[] 的地址,b.intern() 返回常量池中 "lcx" 的地址
System.out.println(b == b.intern());
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 的相加:常量相加看池,变量相加看堆
// 只创建了一个字符串常量 "hello123"
String a = "hello" + "123";
// b -> 常量池的 "hello"
String b = "hello";
// c -> 常量池的 "123"
String c = "123";
// d -> 堆中 value,value 指向常量池中的 "hello123";b + c 的底层实现:调用 StringBuilder 的 append 方法连接两次,然后再 new String() 返回
String d = b + c;
public class Test {
// str 指向堆中的 value,value 指向常量池中的 "lcx"
String str = new String("lcx");
// 字符数组对象存放于堆中,final 表示 ch 的指向不能改变
final char[] ch = {'j','a','v','a'};
public void change(String str, char[] ch) {
// 在常量池中新建 "java" 的数据空间,change 方法栈中的 str 指向 "java"
str = "java";
// 方法栈中的 ch 指向堆中的 final char[]
ch[0] = 'h';
}
public static void main(String[] args) {
Test test = new Test();
test.change(test.str, test.ch);
// Output: lcx and hava
System.out.print(test.str + " and " + test.ch);
}
}
5.3 StringBuffer
- StringBuffer 是一个线程安全的 final 类,其字段 value[] 不是 final 类型的
- String 保存的是字符串常量,value 引用到常量池
- StringBuffer 保存的是字符串变量,数据存放在堆中
- StringBuilder 线程不安全,用法与 StringBuffer 基本一致
- StringBuffer value[] 无参构造器默认初始化容量为 16;若构造器传入的是字符串,则初始化容量为字符串长度加上 16
- StringBuffer 的使用注意事项:
String str = null;
StringBuffer stringBuffer = new StringBuffer();
// append 方法可以追加 null
stringBuffer.append(str);
// Output:null
System.out.println(stringBuffer);
// 构造器不允许传入空,否则抛出 java.lang.NullPointerException
stringBuffer = new StringBuffer(str);
System.out.println(stringBuffer);
5.4 Arrays
- 使用
Arrays.binarySearch()
方法对有序数组进行二分查找时,若不存在该元素,则返回该元素应在数组中位置下标的负值
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
long midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
Arrays.asList()
可以将数组转换为 List 类型的集合,运行类型为Arrays$ArrayList
,即 Arrays 类中的静态内部类 ArrayList
List<Integer> list = Arrays.asList(1, 2, 3);
// output: class java.util.Arrays$ArrayList
System.out.println(list.getClass());
Integer[] array = new Integer[]{1, 2, 3};
List<Integer> integerList = Arrays.asList(array);
int[] arr = new int[]{1, 2, 3};
List<int[]> ints = Arrays.asList(arr);
- 定制排序:
public class CustomizationSort {
public static void main(String[] args) {
Integer[] arrays = new Integer[]{1, 31, 523, 452, 13, 64, 23, 75};
CustomizationSort sort = new CustomizationSort();
// 面向接口编程 + 动态绑定(多态)
sort.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;
}
}
}
}
}
5.5 BigDecimal
BigDecimal bigDecimal = new BigDecimal("242131.24321335243234123");
// 为避免结果为无限循环小数,可对结果指定精度以解决 ArithmeticException
BigDecimal res = bigDecimal.divide(new BigDecimal("1.1"), BigDecimal.ROUND_CEILING);
System.out.println(res);
5.6 日期类
- JDK1.0 中出现的
java.util.Date
类,大多数方法在 jdk1.1 引入的Calendar
类中已被弃用,第二代日期类 Calendar 也存在着以下一些问题:
- 可变性:像日期和时间这样的类应该是不可变的
- 偏移性:年份从 1900 开始,月份从 0 开始
- 格式化:不能对 Calendar 进行格式化
- 线程不安全,不能处理闰秒(每隔两天,多出 1s)
- JDK8 引入了第三代日期类:LocalDate(日期)、LocalTime(时间)、LocalDateTime(日期时间)
LocalDate localDate = LocalDate.now();
// output: 2022-09-22
System.out.println(localDate);
LocalTime localTime = LocalTime.now();
// output: 11:31:36.591
System.out.println(localTime);
LocalDateTime localDateTime = LocalDateTime.now();
// output: 2022-09-22T11:32:13.159
System.out.println(localDateTime);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// output: 2022-09-22 11:33:05
System.out.println(formatter.format(localDateTime));
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// output: 2022-09-22 11:34:14
System.out.println(simpleDateFormat.format(date));
六、集合类
6.1 Collection
6.2 Iterator
- Iterator 称为迭代器,主要用于遍历 Collection 集合中的元素。所有实现了 Collection 接口的集合类都有一个
iterator()
方法,用来返回一个迭代器
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iterator = list.iterator();
// 判断是否还有下一个元素
while (iterator.hasNext()) {
// 取出当前元素
System.out.println(iterator.next());
// 从集合中移除当前元素
iterator.remove();
}
// output: 0
System.out.println(list.size());
- Iterator 对象仅用于遍历集合,本身并不存放数据对象。使用增强 for 循环遍历集合时,底层仍然使用的 Iterator 进行迭代遍历
6.3 List
- 实现 List 接口的集合类元素添加与取出顺序一致,允许元素重复,支持使用对应的索引值直接获取元素的值,可动态扩充容量
- ArrayList:
- 底层机制:
transient Object[] elementData;
- 扩容机制:当超过 elementData 容量时,扩容为原有大小的
1.5
倍
- 无参构造器:elementData 初始容量为 0,第一次添加元素后容量为
10
- 有参构造器:elementData 的初始容量为指定大小
// overflow-conscious code
int oldCapacity = elementData.length;
// 在原有大小基础上加上原有大小的一半为新容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
- ArrayList 线程不安全,可以添加多个 null 元素
- Vector:
- 底层机制:
protected Object[] elementData;
- 扩容机制:当超过 elementData 容量时,扩容为原有大小的
2
倍
- 无参构造器:elementData 初始容量为 0,第一次添加元素后容量为
10
- 有参构造器:elementData 的初始容量为指定大小
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) capacityIncrement : oldCapacity);
- Vector 线程安全,可以添加多个 null 元素
- LinkedList:
- 底层机制:双向链表和双端队列(Queue)。类中有两个属性
transient Node<E> first
和transient Node<E> last
分别指向头节点和尾节点,每个节点中又维护了 prev、next、item 三个属性 - 特点:LinkedList 线程不安全,可以添加多个 null 元素;具有双端队列的特点可头插、头删、尾插、尾删等
6.4 Map
- Map 与 Collection 接口并列存在,用于保存具有映射关系的键值对 key - value,Map 中的 key 和 value 可以是任何引用类型。Map 具有以下特点:
- key 不允许重复,value 可以重复,key 相同时用新的 value 替换旧的 value
- key 最多有一个 null,而 value 可以有多个 null
- HashMap:
- 底层机制:JDK1.8 版本的 HashMap 底层是【数组 + 链表 + 红黑树】,而 JDK1.7 版本的 HashMap 底层是【数组 + 链表】
- 扩容机制:默认初始化容量为
16
,加载因子loadFactor = 0.75
,临界值threshold = 12
- 树化条件:哈希表长度
不小于 64
且某颗链表的长度不小于 8
就将该颗链表树化为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
static final int TREEIFY_THRESHOLD = 8;
- 剪枝:若某颗红黑树元素个数较少,则会触发剪枝行为即将红黑树重新转换为链表
- HashMap 线程不安全,key-val 封装在
HashMap$Node
中;key 最多有一个 null,而 value 可以有多个 null
- HashMap 元素的存放过程:
- 先通过 HashMap 类的静态方法 hash(Object) 获得本次元素的 hash 值
// 确定元素 hash 值的算法
static final int hash(Object key) {
int h;
// key 的 hashCode 与 key 的 hashCode 无符号右移 16 位的值做按位异或运算得到元素的 hash 值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 将 hash 值与本次哈希表大小
-1
的值进行按位与运算获得本次添加的元素在哈希表中的位置号,若该位置上没有其它元素则直接存放
// 用本次哈希表长度减 1 与本次元素的 hash 值进行按位与运算获得元素在哈希表中的位置号
if ((p = tab[i = (n - 1) & hash]) == null)
// 位置号上未存储元素则直接存放
tab[i] = newNode(hash, key, value, null);
- 若位置上已经存在元素,则遍历该条链表判断是否已经存在相同 key,若已存在则返回,否则创建新的节点连接到链表尾(树化判断)
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 类型的数组 transient Node<K,V>[] table;
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;
}
- Hashtable:
- 底层机制:使用方法与 HashMap 基本一致,散列表类型为
Hashtable$Entry
- 扩容机制:默认初始化容量为
11
,,加载因子0.75
,按原有容量的2 倍加 1
的机制进行扩容
// overflow-conscious code
int newCapacity = (oldCapacity << 1) + 1;
- Hashtable 线程安全,
键和值都不能为 null
,否则抛出 NullPointerException
Hashtable<String, Integer> hashtable = new Hashtable<>();
// java.lang.NullPointerException
hashtable.put(null, 2);
// java.lang.NullPointerException
hashtable.put("Spring-_-Bear", null);
- LinkedHashMap:
- 底层机制:继承自 HashMap。数组的类型是
HashMap$Node[]
,其中存放的元素是LinkedHashMap$Entry
类型。在 HashMap 的基础之上,增加了一个双向链表
用来记录元素的添加顺序,使得元素看起来是以插入顺序保存的
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
- 特点:LinkedHashMap 线程不安全,key 最多有一个 null,而 value 可以有多个 null;遍历 LinkedHashMap 时可以使元素的输出顺序与插入顺序一致
- TreeMap:
- 底层机制:TreeMap 实现了
SortedMap
接口,即 TreeMap 中插入的元素是有顺序的 - 使用
TreeMap
时,放入的 key 必须实现Comparable
接口以自定义元素比较规则从而实现元素排序存放 - 去重机制:
- 如果传入了 Comparator 匿名对象,则使用实现的 compare 方法中的比较方式去重,compare 方法返回 0 则认为是相同的元素
- 如果没有传入匿名比较器对象,则以所要添加的对象实现的 Comparaeable 接口的 compareTo 方法比较机制去重
6.5 Set
- Set 中的元素无序(hash 后确定存储位置),不存在索引。Set 不允许重复元素,故最多只包含一个 null
// 1. 增强 for 遍历 Set
for (Integer integer : set) {
System.out.println(integer);
}
// 2. Iterator 遍历 Set
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
- HashSet:底层采用 HashMap,value 为系统给定的
PRESENT
Object 对象
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
- LinkedHashSet:底层使用 LinkedHashMap,底层维护双向链表使得元素取出顺序与添加顺序一致
- TreeSet:底层使用 TreeMap,实现元素添加后有序存放
6.6 集合遍历
- List 遍历
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 方式一:普通 for
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 方式二:增强 for
for (Integer integer : list) {
System.out.println(integer);
}
// 方式三:iterator
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
iterator.remove();
}
- Map 遍历
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 0);
map.put(1, 1);
map.put(2, 2);
// 方式一:entrySet + 增强 for:key-value 的值实际存放到 HashMap$Node 中,而 HashMap$Node 实现了 Map.Entry<K,V> 接口,所以可通过 EntrySet 拿到每一个 Entry,再从 Entry 中依次取出 key 和 value
Set<Map.Entry<Integer, Integer>> entries = map.entrySet();
for (Map.Entry<Integer, Integer> entry : entries) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 方式二:entrySet + iterator
Iterator<Map.Entry<Integer, Integer>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, Integer> entry = iterator.next();
System.out.println(entry.getKey() + " -> " + entry.getValue());
}
// 方式三:Lambda
map.forEach((k, v) -> System.out.println(k + " -> " + v));
// 单独遍历 key 和 value:key-value 的值实际存放到 HashMap$Node 中,为方便单独遍历 key 或 value,使用 KeySet(Set 类型) 引用到 Node 中所有的 key,使用 Values (Collection 类型)引用到所有的 value
Set<Integer> keySet = map.keySet();
for (Integer integer : keySet) {
System.out.println(integer);
}
Collection<Integer> values = map.values();
for (Integer value : values) {
System.out.println(value);
}
- Set 遍历
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
// 方式一:增强 for
for (Integer integer : set) {
System.out.println(integer);
}
// 方式二:iterator
Iterator<Integer> iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
6.7 集合选择
- 存储单个对象且允许重复选择 List
应用场景 | 集合选择 | 底层结构 | 备注 |
改查多 | ArrayList | Object[] | 默认 size = 0,第一次添加 size = 10;按 1.5 倍扩容 |
增删多 | LinkedList | 双端队列 | |
多线程 | Vector | Object[] | 默认 size = 0,第一次添加 size = 10;按 2 倍扩容 |
- 存储单个对象且不允许重复选择 Set
应用场景 | 集合选择 | 底层使用 |
无顺序要求 | HashSet | HashMap |
添加、取出顺序一致 | LinkedHashSet | LinkedHashMap |
元素有序排列存放 | TreeSet | TreeMap |
- 键值对 key-value 选择 Map
应用场景 | 集合选择 | 底层结构 | 备注 |
键顺序无要求 | HashMap | 数组+链表+红黑树 | size = 16,loadFactor = 0.75,按 2 倍扩容 |
插入、取出顺序一致 | LinkedHashMap | 数组 + 双向链表 | 继承自 HashMap |
多线程 | Hashtable | 数组+链表+红黑树 | size = 11,loadFactor = 0.75,按 2 倍 + 1 扩容,k-v 不允许 null |
配置文件 | Properties | 数组+链表+红黑树 | 继承自 Hashtable |
键有序存放 | TreeMap | 红黑树 |
6.8 Collections
Collections 是一个操作 Set、List、Map 等集合的工具类,提供了一系列的静态方法对集合进行操作
方法 | 功能 |
reverse(List) | 反转 List 中的元素顺序 |
shuffle(List) | 对 List 中的元素进行随机排序 |
sort(List) | 按元素自然顺序方式对 List 中的元素排序 |
sort(List,Comparator) | 根据 Comparator 的顺序对 List 中的元素排序 |
swap(List,int,int) | 对 List 中的两个元素交换顺序 |
max(Collection) | 自然顺序找出集合中的最大元素 |
max(Collection,Comparator) | 指定排序找出集合中的最大元素 |
frequency(Collection, Object) | 某个元素在集合中的出现次数 |
copy(List,List) | List 拷贝 |
replaceAll(List,Object,Object) | 用新的 Object 替换指定的 Object |
七、多线程
7.1 线程概述
- 进程:进程是程序的一次执行过程,是一个动态过程,有其自身的产生、存在和消亡历程
- 线程:线程是由进程创建的,是进程的一个实体,一个进程可以拥有多个线程
- 并发:同一时刻多个任务交替执行,造成一种 “貌似同时” 的错觉。简单地说,单核 CPU 实现的多任务就是并发
- 并行:同一时刻多个任务同时执行,多核 CPU 实现的多任务就是并行
- Java 获取 JVM 可用处理器个数:
// 获取 JVM 可用处理器个数
Runtime runTime = Runtime.getRuntime();
System.out.println(runTime.availableProcessors());
7.2 线程创建
- 继承 Thread 类创建线程:
/**
* @author Spring-_-Bear
* @version 2021-11-21 19:53
*/
public class ThreadEx extends Thread {
// 当启动程序时理解为开启了一个进程,进程开启了 main 线程
public static void main(String[] args) {
// 在 main 线程中可以开启其它线程,只有当所有线程都消亡时,进程才结束
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();
}
}
}
}
- 实现 Runnable 接口创建线程:
- Java 是单继承机制,若某个类已经存在父类,这时可以通过实现 Runnable 接口实现线程
- 实现 Runnable 接口的类不能直接调用
start()
方法,可以将对象作为new Thread(Runnable)
的参数,从而调用 start() 方法(静态代理设计模式) - 实现 Runnable 接口方式更加适合
多个线程共享某个资源
的情况,并且避免了单继承的局限
/**
* @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();
}
}
}
}
7.3 start 与 run
new Dog().run()
与 new Dog().start()
的区别:
- 前者只是单纯地调用 run() 方法
- 后者启动了线程,在 start() 方法中实际调用了 start0() 方法,这是个 native 修饰的方法,真正实现了线程启动。start() 方法调用了 start0() 方法后,该线程并不一定会马上执行,只是将线程变成了就绪状态,具体什么时候执行,取决于操作系统处理机调度
7.4 Thread
方法 | 功能 |
setName(String) | 设置线程名 |
getName() | 获取线程名 |
start() | 启动线程 |
run() | 调用线程对象的 run() 方法 |
setPriority(int) | 设置线程优先级(1->5->10) |
getPriority() | 获得线程优先级 |
sleep(long) | 休眠线程 |
interrupt() | 中断线程,线程并未消亡 |
7.5 线程插队与礼让
方法名 | 功能 |
yield() | 线程礼让,让出 CPU 资源,礼让时间不确定,是否礼让成功不确定,取决于 OS |
join() | 线程插队,一旦插队成功,则必须执行完插队线程的所有任务 |
/**
* @author Spring-_-Bear
* @version 2021-11-21 22:49
*/
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
Thread task = new Thread(new Task());
task.start();
for (int i = 1; i <= 20; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
Thread.sleep(1000);
if (i == 3) {
// task 线程插队,一旦插队成功则需先执行完所有 task 任务
// task.join();
// main 线程礼让,不一定礼让成功
Thread.yield();
}
}
}
}
class Task 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();
}
}
}
}
7.6 守护线程
- 用户线程:也称工作线程,当线程的任务执行完成后结束或以通知方式结束
- 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束(经典守护线程:垃圾回收机制)
/**
* @author Spring-_-Bear
* @version 2021-11-22 19:10
*/
public class ThreadDaemon {
public static void main(String[] args) throws InterruptedException {
Thread task = new Thread(new Task());
// 设置为 main 线程的守护线程
task.setDaemon(true);
task.start();
Thread.sleep(5000);
// 工作线程结束,守护线程自动终止
System.out.println("妈妈回家了,小明结束写作业!");
}
}
class Task implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("小明写作业中···");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
7.7 线程的七大状态
状态 | 说明 |
NEW | 尚未启动的线程处于此状态。 该状态线程对象被创建,但还未调用 start 方法 |
READY | 就绪状态,等待 JVM 的调度 |
RUNNABLE | 可运行状态,细分为 |
BLOCKED | 被阻塞等待监视器锁的线程处于此状态。处于该状态的线程正在等待获取一个监视器锁进入同步代码或方法;也可能是在调用了 |
WAITING | 线程处于等待状态;处于该状态的线程可能是因为调用了 |
TIMED_WAITING | 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态;处于这个状态的线程可能是调用了具有指定时间的 |
TERMINATED | 已退出的线程处于此状态 |
7.8 线程同步机制
- 线程互斥:当有一个线程正在对内存进行操作时,其它线程都不可以对这个内存进行操作,直到该线程完成操作其它线程才能对该内存进行操作,也即同一时刻只允许一个线程操作内存
- 对象互斥锁:Java 语言中引入了对象互斥锁来保证共享数据操作的完整性。每个对象都对应一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻有且仅有一个线程可以访问该对象,用关键字
synchronized
来实现对象的互斥锁 - 同步代码块与同步方法:
- 同步代码块:将 synchronized 加在某段代码上,表明此段代码是同步代码
- 同步方法:得到对象的锁才可以操作对象的代码。可以将 synchronized 加在方法声明中,表示整个方法为同步方法
- 静态同步方法与非静态同步方法:
- 静态同步方法的锁是当前类本身(
ClassName.class
)
/**
* 静态方法中的互斥锁加在类上
*/
class SellTicket implements Runnable {
public static void getTicket() {
synchronized (SellTicket.class) {
System.out.println("售出一张票");
}
}
}
- 非静态同步方法的锁可以是它本身 this,也可以是其它对象(要求是同一个对象)
class SellTicket implements Runnable {
private int ticketNum = 100;
private boolean hasTicket = true;
private final Object object = new Object();
public void sell() {
// 锁 this 与锁 object 等价
synchronized (object) {
if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + " 售票结束...");
hasTicket = false;
return;
}
System.out.println(Thread.currentThread().getName() + " 售出一张票" + ",余票 = " + (--ticketNum));
}
// 释放锁后再睡觉
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (hasTicket) {
sell();
}
}
}
7.9 线程死锁
- 线程死锁:两个或两个以上的线程在执行过程中同时被阻塞,它们中的某个或者全部都在等待某个资源被释放,由于线程被无限期的阻塞,系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程被称为线程死锁
- 产生死锁必须满足的四个条件:
- 互斥条件:只有对需要互斥使用的资源的争夺才会发生死锁
- 不剥夺条件:进程所获得的资源在未使用完之前,不能由其它进程强行夺走,只能主动释放
- 请求和保持条件:进程已经保持了至少一个资源并持有不放,但又提出了新的资源请求,而该资源被其它进程占有,此时请求进程将被阻塞
- 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求(循环等待未必死锁,死锁一定有循环等待)
- 线程死锁 Java 代码示例:
/**
* @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 final Object object1 = new Object();
static final 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() + " 获得 object1 的锁,尝试获取 object2 的锁···");
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + " 成功获得 object2 的锁!");
}
}
} else {
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + " 获得 object2 的锁,尝试获取 object1 的锁···");
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + " 成功获得 object1 的锁!");
}
}
}
}
}
- 线程释放锁的情况:
- 当前线程的同步方法、同步代码块中
执行结束
会释放锁 - 当前线程在同步方法、同步代码块中遇到
break、return
会释放锁 - 当前线程在同步方法、同步代码块中出现了
未处理的 Error 或 Exception 导致结束
会释放锁 - 当前线程在同步方法、同步代码块中执行了线程
对象的 wait() 方法,当前线程暂停并释放锁
- 不释放锁的情况:
- 当前线程在同步方法、同步代码块中调用了
Thread.sleep()、Thread.yield()
方法暂停当前线程的执行不会释放锁 - 线程执行同步代码块时,其它线程调用了该线程的
suspend() 方法将该方法挂起
,该线程不会释放锁(suspend()、resume() 方法均已过时,不再推荐使用)
7.10 多线程卖票
- 未使用同步锁出现超卖现象
/**
* @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 = 10;
@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));
}
}
}
- 锁方法,抱着锁睡觉,未均匀卖票,Why?执行任意一行输出即可均匀卖票?
/**
* 锁方法,抱着锁睡觉,未均匀卖票,Why?执行任意一行输出即可均匀卖票?
*
* @author Spring-_-Bear
* @datetime 2022/4/10 15:43
*/
public class Ticket {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
class SellTicket implements Runnable {
private int ticketNum = 100;
private boolean hasTicket = true;
public synchronized void sell() {
if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + " 售票结束...");
hasTicket = false;
return;
}
System.out.println(Thread.currentThread().getName() + " 售出一张票" + ",余票 = " + (--ticketNum));
try {
// 抱着锁睡觉,睡醒后释放锁不公平竞争?
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (hasTicket) {
// System.out.println(Thread.currentThread().getName());
sell();
}
}
}
- 锁代码块,线程不抱着锁睡觉,10 个线程均匀售票
/**
* 锁代码块,未抱着锁睡觉,10 个线程均匀卖票
*
* @author Spring-_-Bear
* @datetime 2022/4/10 15:43
*/
public class Ticket {
public static void main(String[] args) {
SellTicket sellTicket = new SellTicket();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
class SellTicket implements Runnable {
private int ticketNum = 100;
private boolean hasTicket = true;
public void sell() {
synchronized (this) {
if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + " 售票结束...");
hasTicket = false;
return;
}
System.out.println(Thread.currentThread().getName() + " 售出一张票" + ",余票 = " + (--ticketNum));
}
// 释放锁后再睡觉
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (hasTicket) {
sell();
}
}
}
八、IO
8.1 File
方法名 | 功能 |
new File(String) | 根据路径构建一个 File 对象 |
new File(File,String) | 根据父目录文件 + 子路径构建 |
new File(String,String) | 根据父路径 + 子路径构建 |
getName() | 获取文件名,包含文件扩展名 |
getAbsolutePath() | 获取文件绝对路径 |
getParent() | 获取文件父级目录 |
length() | 获取文件大小,以字节为单位 |
exists() | 判断是否存在 |
isFile() | 判断是否是文件 |
isDirectory() | 判断是否是目录 |
mkdir() | 创建一级目录 |
mkdirs() | 创建多级目录 |
delete() | 删除空目录或文件 |
8.2 IO 流的分类
- 流的定义:数据在数据源(文件)和程序(内存)之间经历的路径。以程序(内存)为参照,流入内存为输入流,流出内存为输出流
- IO 流分类:
- IO 流的四大抽象基类:
- InputStream(字节输入流)
- OutputStream(字节输出流)
- Reader(字符输入流)
- Writer(字符输出流)
8.3 字节流与字符流
- 字节流文件拷贝:
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);
}
fileOutputStream.close();
fileInputStream.close();
- 字符流使用:字符流使用完毕后必须
close()
或手动flush()
,否则数据只是暂存在内存缓冲区中,并未写入文件。字符流底层调用字节流实现具体功能
// 字符流追加写入文件
FileWriter fileWriter = new FileWriter("c:/users/admin/desktop/test.txt", true);
fileWriter.write("Hello World");
fileWriter.write(" World");
fileWriter.close();
8.4 节点流与处理流
- 节点流:节点流是底层流,直接对数据源进行操作,可以从一个特定的数据源读写数据。如 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 | - | - | 处理流 |
8.5 对象处理流
- 序列化与反序列化:
- Java 序列化是指把 Java 对象转换为字节序列的过程
- Java 反序列化是指把字节序列恢复为 Java 对象的过程
- 序列化接口:若某个对象需支持序列化机制,则必须让其类是可序列化的,也即必须实现
Serializable
或Externalizable
接口之一
- Serializable 是标记接口,无需实现任何方法
- Externalizable 继承自 Serializable,需要实现方法
- 序列化机制:
- 序列化的类建议添加
SerialVersionUID
序列化版本 ID 以提高版本的兼容性 - 序列化对象时,默认将除了
static、transient
修饰的成员以外的所有成员进行序列化保存 - 序列化要求类的所有字段类型也需要实现序列化接口
- 序列化具备可继承性,即子类继承已序列化的父类,则子类也可以序列化
File file = new File("c:/users/admin/desktop/object.dat");
if (!file.exists()) {
file.createNewFile();
}
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
objectOutputStream.writeObject(new User(1, "spring", "spring"));
objectOutputStream.writeObject(new User(2, "bear", "bear"));
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
Object o = objectInputStream.readObject();
System.out.println(o);
o = objectInputStream.readObject();
System.out.println(o);
objectOutputStream.close();
class User implements Serializable {
private static final long serialVersionUID = 362498820763181265L;
private Integer id;
private String username;
private transient String password;
public User(Integer id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
- 对象序列化追加保存需要注意的问题:java.io.StreamCorruptedException: invalid type code: AC
8.6 标准输入输出流
- 标准输入流:
System.in
编译类型是 InputStream,运行类型是 BufferedInputStream
,标准输入设备默认为键盘
public class System {
/**
* The "standard" input stream. This stream is already
* open and ready to supply input data. Typically this stream
* corresponds to keyboard input or another input source specified by
* the host environment or user.
*/
public final static InputStream in = null;
}
- 标准输出流:
System.out
编译类型是PrintStream
,运行类型也是PrintStream
,标准输出设备默认为屏幕
public class System {
/**
* The "standard" output stream. This stream is already
* open and ready to accept output data. Typically this stream
* corresponds to display output or another output destination
* specified by the host environment or user.
* <p>
* For simple stand-alone Java applications, a typical way to write
* a line of output data is:
* <blockquote><pre>
* System.out.println(data)
* </pre></blockquote>
* <p>
*/
public final static PrintStream out = null;
}
- 打印流
PrintStream
:涉及字符输出到文件时需要手动close()
或flush()
,否则内容不会写入到文件
// 设置打印流为标准输出
PrintStream printStream = System.out;
// print 方法底层调用了 write 方法,因此以下两种方式等价
printStream.print("Hello World!");
printStream.write("Hello Java!".getBytes(StandardCharsets.UTF_8));
printStream.close();
// 重定向输出设备为文件
System.setOut(new PrintStream("C:\\Users\\Admin\\Desktop\\printStream.txt"));
System.out.println("Spring-_-Bear");
8.7 转换流
- 输入转换流:
InputStreamReader -> Reader
:可以将 InputStream(字节流输入流)包装成 Reader(字符输入流),同时指定编码
// 将 InputStream 转换为 BufferedReader
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c:\\users\\admin\\desktop\\temp.txt"), StandardCharsets.UTF_8));
String str;
while ((str = bufferedReader.readLine()) != null) {
System.out.println(str);
}
bufferedReader.close();
- 输出转换流:
OutputStreamWriter -> Writer
:可以将 OutputStream(字节输出流)包装成 Writer (字符输出流),同时指定编码
// 将 OutputStream 转换为 OutputWriter
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("c:/users/admin/desktop/out.txt")));
bufferedWriter.write("Hello World!");
bufferedWriter.close();
8.8 Properties
方法名 | 功能 |
load(InputStream | Reader) | 加载配置文件 |
list(PrintStream | PrintWriter) | 将数据显示到指定设备 |
getProperty(String) | 根据键获取值 |
setProperty(String,String) | 设置键值对到 Properties 对象 |
store(OutputStream | Writer,String) | 保存到配置文件,含有中文默认保存 Unicode 码值 |
Properties properties = new Properties();
// 加载配置文件
properties.load(new FileInputStream("c:/users/admin/desktop/jdbc.properties"));
// 打印所有 key-val 到控制台
properties.list(System.out);
// 根据 key 获取 val
System.out.println(properties.getProperty("url"));
// 根据 key 替换 val
properties.setProperty("username", "bear");
// 存储 properties 到新文件
properties.store(new OutputStreamWriter(new FileOutputStream("c:/users/admin/desktop/tmp.properties")), "Copy from jdbc.properties");
九、网络编程
9.1 IP 与域名
- IP 地址:用于唯一标识网络中的每台计算机(主机)或路由器的各接口
- Windows 查看 IP 地址:
ipconfig
- Linux 查看 IP 地址:
ifconfig
-
IPV4
用 4 字节共 32 位标识一个 IP 地址(点分十进制) -
IPV6
用 8 字节共 128 位标识一个 IP 地址
- IP 组成及分类:
网络地址 + 主机地址
分类 | 表示方法 | 范围 |
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 地址,一个域名与一个 IP 地址唯一对应
9.2 端口与协议
- 端口:用于标识主机特定的网络程序,以整数形式表示,范围是
0 ~ 65535
,其中0 ~ 1024
已被一些知名程序占用 - TCP/IP 四层网络体系结构:网络接口层、网际层、运输层、应用层
TCP/IP模型 | 对应协议 |
应用层 | HTTP、FTP、Telnet、DNS··· |
传输层 | TCP、UDP··· |
网络层 | IP、ICMP、ARP··· |
物理 + 数据链路层 | Link··· |
9.3 InetAddres
方法名 | 功能 |
getLocalHost | 获取本机 InetAddress 对象 |
getByName | 根据指定主机名 / 域名获取 InetAddress 对象 |
getHostName | 获取 InetAddress 对象的主机名 |
getHostAddress | 获取 InetAddress 对象的 ip 地址 |
// 获取本机的 InetAddress 对象
InetAddress localHost = InetAddress.getLocalHost();
// Output: DESKTOP-BEAR/10.134.220.95
System.out.println(localHost);
// 通过计算机名获取本机的 InetAddress 对象
InetAddress desktop = InetAddress.getByName("DESKTOP-BEAR");
// Output: DESKTOP-BEAR/10.134.220.95
System.out.println(desktop);
// 获取 InetAddress 对象的主机名
System.out.println(localHost.getHostName());
// 获取 InetAddress 对象的 ip 地址
System.out.println(desktop.getHostAddress());
// 根据指定主机名/域名获取 InetAddress 对象
InetAddress csdn = InetAddress.getByName("www.csdn.net");
// Output: www.csdn.net/39.106.226.142
System.out.println(csdn);
// Output: www.csdn.net
System.out.println(csdn.getHostName());
// Output: 39.106.226.142
System.out.println(csdn.getHostAddress());
9.4 TCP、UDP 与 Socket
- TCP(
Transmission Control Protocol
):传输控制协议。在使用 TCP 协议前,通信双方须建立 TCP 连接(三报文握手)形成传输数据通道,建立成功后可进行大量数据的传输,数据传输完毕需释放已建立的连接(四报文挥手) - UDP(
User Datagram Protocol
):用户数据报协议。将数据、源地址、目的地封装成数据报,数据传输前不需要建立连接,数据传输完成无需释放资源
- 每个数据报的大小限制在
64K
以内 - 不可靠传输服务,数据传输速度较 TCP 快、效率高
- Socket(套接字):开发网络应用程序时被广泛使用,以至于成为事实上的标准。通信的两端都要有 Socket,实际上就是实现两台机器间通信的端口。Socket 允许程序把网络连接当作是一个流,数据在两个 Socket 间通过 IO 传输。一般发起通信的应用程序为客户端,等待通信请求的为服务器
- 当客户端与服务端建立连接后,实际上客户端也是通过一个临时端口与服务端进行通信的,这个端口号是根据 TCP / IP 协议由系统随机分配的
9.5 TCP 编程
- TCP 文件上传:
Server.java
/**
* @author Spring-_-Bear
* @version 2021-11-16 09:40
*/
public class Server {
public static void main(String[] args) throws IOException {
// 监听端口,等待连接
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器已启动,等待连接···");
Socket socket = serverSocket.accept();
// 文件大小,单位为字节
long fileSize = 0;
// 文件保存路径
File fileSavePath = new File("C:\\Users\\Admin\\Desktop\\temp.jpg");
// 从数组通道读取文件字节数组
int readLen;
byte[] buffer = new byte[1024];
InputStream inputStream = socket.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
System.out.println("文件读取中···");
while ((readLen = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, readLen);
fileSize += readLen;
}
System.out.println("文件读取成功,总大小为 " + fileSize + " bytes!");
// 将 byteArrayOutputStream 中的数据转换为字节数组(文件二进制数据)
byte[] fileByteArray = byteArrayOutputStream.toByteArray();
// 将文件字节数组保存到磁盘
System.out.println("文件保存到本地中···");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(fileSavePath));
bufferedOutputStream.write(fileByteArray);
System.out.println("文件保存到本地成功!保存路径为:" + fileSavePath);
// 向客户端反馈文件下载保存成功信息
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
System.out.print("发送反馈信息到客户端··· ");
bufferedWriter.write("文件接收成功,下载保存完成!");
bufferedWriter.newLine();
bufferedWriter.flush();
System.out.println("消息发送成功!");
bufferedWriter.close();
bufferedOutputStream.close();
byteArrayOutputStream.close();
inputStream.close();
socket.close();
serverSocket.close();
}
}
Client.java
/**
* @author Spring-_-Bear
* @version 2021-11-16 09:40
*/
public class Client {
public static void main(String[] args) throws IOException {
// 文件大小,以字节为单位
int fileSize = 0;
// 文件路径
File filePath = new File("C:\\Users\\Admin\\Desktop\\fishing.jpg");
// 从磁盘读取文件,并存入文件字节数组
int readLen;
byte[] buf = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
System.out.println("文件读入中···");
while ((readLen = bufferedInputStream.read(buf)) != -1) {
// 将从文件读取到的二进制数据写进字节数组输出流(byteArrayOutputStream)
byteArrayOutputStream.write(buf, 0, readLen);
fileSize += readLen;
}
System.out.println("文件读入成功,文件大小为:" + fileSize + " bytes!");
// 将得到的 byteArrayOutputStream 转换为一个字符数组
byte[] fileByteArray = byteArrayOutputStream.toByteArray();
// 将字节数组写入数据通道
Socket socket = new Socket(InetAddress.getLocalHost(), 6666);
OutputStream outputStream = socket.getOutputStream();
System.out.println("文件发送到服务器中···");
outputStream.write(fileByteArray);
socket.shutdownOutput();
System.out.println("文件发送到服务器成功!");
// 接送服务器端发送的反馈信息并打印
System.out.print("接收到的服务器信息:");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println(bufferedReader.readLine());
bufferedReader.close();
outputStream.close();
byteArrayOutputStream.close();
bufferedInputStream.close();
socket.close();
}
}
- TCP 文件下载:
Server.java
/**
* 服务端:将客户端请求的文件传输到客户端
*
* @author Spring-_-Bear
* @version 2021-11-02 09:57
*/
public class Server {
byte[] fileData = null;
String requestFileName = null;
ArrayList<String> fileNameList = new ArrayList<>();
File filePath = new File("C:\\Users\\Admin\\Desktop\\sourceFile");
Socket socket = null;
ServerSocket serverSocket = null;
InputStream inputStream = null;
BufferedInputStream bufferedInputStream = null;
public static void main(String[] args) {
Server server = new Server();
server.createAndReadFiles();
server.startServer();
server.receiveRequestNameFromClient();
server.readFileFromDisk();
server.transferFileToClient();
try {
server.socket.close();
server.serverSocket.close();
server.inputStream.close();
server.bufferedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取指定路径下的所有文件
*/
public void createAndReadFiles() {
System.out.println("正在进行文件夹初始化···");
// 若文件夹不存在,则先创建文件夹
if (!(filePath.exists() && filePath.isDirectory())) {
filePath.mkdirs();
}
// 读取文件夹中的所有文件,并将文件名存进集合
File[] files = filePath.listFiles();
for (File file : files) {
if (file.isFile()) {
fileNameList.add(file.getName());
}
}
System.out.println("初始化完成!");
}
/**
* 启动服务器
*/
public void startServer() {
try {
serverSocket = new ServerSocket(9999);
System.out.println("服务器已启动,等待连接···");
socket = serverSocket.accept();
System.out.println("服务端 - 客户端建立连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取客户端请求的文件名
*/
public void receiveRequestNameFromClient() {
try {
int readLen;
byte[] buf = new byte[2048];
inputStream = socket.getInputStream();
System.out.println("正在接收客户端请求的文件名···");
while ((readLen = inputStream.read(buf)) != -1) {
requestFileName = new String(buf, 0, readLen);
}
socket.shutdownInput();
System.out.println("成功接收到客户端请求的文件名!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从磁盘中读取文件内容到内存
*/
public void readFileFromDisk() {
try {
int readLen = 0;
byte[] buf = new byte[1024];
File requestFile = new File(filePath + "\\" + requestFileName);
bufferedInputStream = new BufferedInputStream(new FileInputStream(requestFile));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
System.out.println("正在从磁盘读取文件···");
while ((readLen = bufferedInputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, readLen);
}
fileData = byteArrayOutputStream.toByteArray();
System.out.println("从磁盘读取文件完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将文件字节数组传输到客户端
*/
public void transferFileToClient() {
try {
System.out.println("正在传送文件到客户端···");
OutputStream outputStream1 = socket.getOutputStream();
outputStream1.write(fileData);
System.out.println("文件传输到客户端完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
// /**
// * 发送文件中的文件夹中的所有文件名到客户端
// */
// public void sendFilesNameToClient() {
// System.out.println("正在发送文件目录到客户端···");
// outputStream = socket.getOutputStream();
// bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
// for (String fileName : fileArrayList) {
// bufferedWriter.write((fileName));
// outputStream.flush();
// }
// bufferedWriter.newLine();
// outputStream.flush();
// System.out.println("文件目录发送到客户端成功!");
// }
}
Client.java
/**
* 服务端:发送想要下载的文件名称到客户端,得到客户端回应并将文件保存到本地
*
* @author Spring-_-Bear
* @version 2021-11-02 09:58
*/
public class Client {
byte[] fileData = null;
Socket socket = null;
InputStream inputStream = null;
OutputStream outputStream = null;
BufferedOutputStream bufferedOutputStream = null;
Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
Client client = new Client();
try {
client.socket = new Socket(InetAddress.getLocalHost(), 9999);
client.sendFileNameToServer();
client.receiveFileData();
client.saveFile();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
client.socket.close();
client.inputStream.close();
client.outputStream.close();
client.bufferedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 发送想要下载的文件名到服务器端
*/
public void sendFileNameToServer() {
try {
System.out.print("请输入您想要下载的文件名(含文件拓展名):");
String choice = scanner.next();
outputStream = socket.getOutputStream();
outputStream.write(choice.getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 接收从服务端传送的数据
*/
public void receiveFileData() {
try {
int readLen = 0;
byte[] buf = new byte[1024];
inputStream = socket.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
System.out.println("正在接收服务器传输的文件···");
while ((readLen = inputStream.read(buf)) != -1) {
byteArrayOutputStream.write(buf, 0, readLen);
}
socket.shutdownInput();
fileData = byteArrayOutputStream.toByteArray();
System.out.println("文件接收成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将从服务器端接收到的文件保存到本地
*/
public void saveFile() {
try {
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream("C:\\Users\\Admin\\Desktop\\temp.jpg"));
System.out.println("正在将文件保存到本地···");
bufferedOutputStream.write(fileData);
System.out.println("文件保存到本地完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
// /**
// * 读取服务端发送的文件名并显示到控制台
// */
// public void receiveFilesName() {
// bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// System.out.println("正在接收服务端发送的文件目录···");
// String fileName = "";
// System.out.println(bufferedReader.readLine());
// while ((fileName = bufferedReader.readLine()) != null) {
// System.out.println(fileName);
// }
// System.out.println("接收服务端发送的文件目录成功!");
// }
}
9.6 UDP 编程
UDP 数据报通过数据报套接字 DatagramSocket
实现发送和接收,系统不保证 UDP 数据报一定送达目的地,也不确定什么时候可以送达即传输不可靠。DatagramSocket 对象封装了 UDP 数据报,在数据报中封装了数据、发送端和接收端的 IP 地址以及端口信息
Sender.java
/**
* @author Spring-_-Bear
* @datetime 2022-09-25 23:02 Sunday
*/
public class Sender {
public static void main(String[] args) throws IOException {
// 监听端口
DatagramSocket datagramSocket = new DatagramSocket(8888);
// 装包:封装数据
byte[] data = "Hello, receiver!".getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, 0, data.length, InetAddress.getLocalHost(), 7777);
// 发送包
datagramSocket.send(packet);
byte[] buf = new byte[1024];
packet = new DatagramPacket(buf, buf.length);
// 接收包并从包中获取数据
datagramSocket.receive(packet);
int dataLen = packet.getLength();
data = packet.getData();
System.out.println(new String(data, 0, dataLen));
// 关闭资源
datagramSocket.close();
}
}
Receiver.java
/**
* @author Spring-_-Bear
* @datetime 2022-09-25 23:02 Sunday
*/
public class Receiver {
public static void main(String[] args) throws IOException {
// 监听端口
DatagramSocket socket = new DatagramSocket(7777);
byte[] buf = new byte[1024];
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
// 接收包并从包中获取数据
socket.receive(datagramPacket);
int dataLen = datagramPacket.getLength();
byte[] data = datagramPacket.getData();
System.out.println(new String(data, 0, dataLen));
// 装包:封装数据
buf = "Hello, sender!".getBytes(StandardCharsets.UTF_8);
datagramPacket = new DatagramPacket(buf, 0, buf.length, InetAddress.getLocalHost(), 8888);
// 发送包
socket.send(datagramPacket);
// 关闭资源
socket.close();
}
}
9.7 netstat
-
netstat -an | more
:查看当前主机网络情况,包括端口监听和网络连接情况 -
netstat -anb
:以管理员身份运行此命令可以查看监听端口的具体程序
十、反射
10.1 反射机制
- 反射:允许程序在运行时借助于
Reflection API
取得类的任何内部信息(如成员变量、构造器、成员方法等),并能操作对象的属性及方法。反射在设计模式和框架底层广泛使用 - Class 对象:类加载完毕之后,在堆中就产生了一个
Class
类型(Class 本身也是一个类)的对象(一个类只有一个 Class 对象,因为类只加载一次),这个对象包含了类的完整结构信息,通过这个对象就可以得到类的所有信息 - 反射的作用:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象,获取其属性或调用其方法
- 生成动态代理
- 反射的优缺点:
- 优点:可以动态创建和使用对象(框架底层核心),没有反射框架就失去了灵魂
- 缺点:使用反射代码基本是解释执行,对执行速度有一定影响
- 优化:关闭访问检查,Method、Field、Constructor 类都有一个
setAccessible(boolean)
方法,作用是启动和禁用访问安全检查的开关,传入参数 true 表示反射的对象在使用时取消安全检查,提高反射效率,但提高效果并不明显
- 反射案例:
// 根据类名加载类信息
Class<?> aClass = Class.forName("cn.edu.whut.springbear.User");
// 创建类实例
Object o = aClass.newInstance();
// 获得类成员方法
Method method = aClass.getMethod("hello");
// 调用对象的方法
method.invoke(o);
10.2 程序三阶段
- 代码阶段(编译阶段):将源码
.java
文件通过javac
编译得到.class
字节码文件 - Class 类阶段(加载阶段):通过类加载器(ClassLoader)将 .class 字节码文件加载到堆中生成 Class 类的对象,该对象记录了 .class 中类的字段 Field[] fields、构造器 Constructor[] constructors、方法 Method[] methods 等信息
- Runtime(运行阶段):通过 new 创建出的对象知道自己与哪个 Class 类型的对象相关联
10.3 Class
- Class 对象:
java.lang.Class
代表一个类,继承自 Object 类。Class 对象代表某个类的字节码文件被类加载器加载后在堆中生成的 Class 类型的对象
- 类的元数据:加载类在堆里生成 Class 对象的同时也会在方法区生成对应类的
字节码的二进制数据
,也称为类的元数据,该二进制数据引用到 Class 对象 - Class 对象的生成:Class 类对象并不是 new 出来的,而是 JVM 创建的,通过 ClassLoader 类的 loadClass 方法创建。对于某个类的 Class 对象,在堆中有且仅有一份,因为类只加载一次
- 哪些类型有 Class 对象:外部类、内部类、接口、数组、枚举、注解、基本数据类型、void
- Class 类常用 API
方法 | 功能 |
static Class forName(String) | 获得指定类名的 Class 对象 |
Object newInstance() | 调用缺省构造函数,获得 Class 类的一个实例 |
getFields | 获取所有 public 修饰的字段,本类以及父类 |
getDeclaredFidles | 获取本类中的所有字段 |
getMethods | 获取所有 public 修饰的方法,本类以及父类 |
getDeclaredMethods | 获取本类中的所有方法 |
getConstructors | 获取所有 public 修饰的构造器,只包含本类 |
getDeclaredConstructors | 获取本类中的所有构造器 |
getPackage | 以 Package 形式返回包信息 |
getSuperClass | 以 Class 形式返回父类信息 |
getAnnotations | 以 Annotation[] 形式返回注解信息 |
Class[] getInterfaces | 获得 Class 对象的接口 |
ClassLoder getClassLoder() | 获得类的加载器 |
getName | 获取全类名 |
getSimpleName | 获取简单类名 |
- 获取类的 Class 对象:
- 编译阶段:
Class.forName(String);
多用于从配置文件读取到类全路径,而后加载类 - 类加载阶段:
ClassName.class;
多用于参数传递,此方式最为安全可靠,程序性能最高 - 运行阶段:
object.getClass();
通过创建好的类的实例来获取 Class 对象 - 类加载器:
object.getClass().getClassLoader().loadClass("ClassFullPath");
Car car = new Car();
// 获得类对应的类加载器
ClassLoader classLoder = car.getClass().getClassLoader();
// 获得 Class 对象
Class<?> aClass = classLoder.loadClass("com.springbear.Car");
- 基本数据类型:
Class clazz = int.class;
- 包装类:
Class cls = Integer.TYPE;
10.4 类加载
- 静态加载与动态加载 :
- 静态加载:编译时加载相关的类,如果不存在相关类则报错,依赖性很强
- 动态加载:运行过程中在需要时加载该类,如果运行时未使用到该类则不报错,降低了依赖性。反射机制是 Java 实现动态的关键
- 引起类加载的情况:
- new 创建对象时
- 子类被加载时其父类也被加载
- 访问到类的静态成员时(
非 static final
) - 通过反射加载
- 类加载流程:加载 -> 连接 -> 初始化
- 加载(Loading):类加载器完成将类的 .class 文件读入内存,并为之创建一个 Class 类型的对象
- 连接(Linking):将类的二进制数据(类的元数据)合并到
JRE
中。细分为验证、准备和解析三个子阶段:
- 验证(Verification):目的是为了确保 .class 文件的字节流中包含的信息符合当前虚拟机的要求,不会危害到虚拟机自身的安全。验证包括对文件格式验证(是否以魔数 oxcafebabe 开头)、元数据验证、字节码验证和符号引用验证等。可以考虑使用
-Xverify:none
参数来关闭大部分的类验证措施,缩短虚拟机加载时间 - 准备(Preparation):JVM 会在此阶段给
静态变量
分配内存并执行默认初始化,这些变量所使用的内存都会在方法区
中进行分配
// 在连接的准备子阶段先默认初始化为 0,后在初始化阶段再显式初始化为 1
public static int num1 = 1;
// 在连接的准备子阶段直接初始化为 2
public static final int num2 = 2;
- 解析(Resolution):JVM 将常量池内的符号引用替换为直接引用的过程
- 初始化(Initialization):真正执行类中定义的 Java 代码,此阶段是执行
<clinit>()
方法的过程。<clinit>()
方法的执行过程是由编译器按代码在源文件中的出现顺序,依次收集类中所有的静态变量的赋值动作和静态代码块中的语句
,并自动进行合并。JVM 保证一个类的<clinit>()
方法在多线程环境中会被正确地加锁、同步
static {
System.out.println("静态代码块被执行!");
num = 300;
}
public static int num = 100;
// 经 <clinit>() 方法收集合并后如下
System.out.println("静态代码块被执行!");
num = 100;
- 类加载后内存布局情况:
- 在堆中存放类的 Class 对象(数据结构)
- 在方法区存放类的字节码的二进制数据(元数据),元数据引用到对应的 Class 对象
10.5 反射爆破
- 调用类的指定构造器:
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));
- 操作类的方法:
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);
- 最佳实践:在指定目录下创建文件
// 加载类信息
Class<?> fileClass = Class.forName("java.io.File");
// 获得指定的构造器
Constructor<?> constructor = fileClass.getConstructor(String.class);
Object file = constructor.newInstance("c:/users/admin/desktop/reflection.txt");
// 获得指定方法
Method createNewFile = fileClass.getMethod("createNewFile");
// 调用方法
createNewFile.invoke(file);
十一、JDBC
11.1 JDBC 原理
- JDBC(Java Database Connectivity):JDBC 为访问不同的数据库定义了统一的接口,具体实现由各数据库厂商完成。Java 程序员使用 JDBC API 可以连接任何提供了 JDBC 驱动程序的数据库系统,从而完成对数据库的各种操作,、关的接口和类在 java.sql 和 javax.sql 包中
- JDBC 步骤:
- 注册驱动:加载 Driver 类
- 获取连接:获得 Connection 对象
- 执行 SQL:获得 Statement 对象
- 释放数据库连接
11.2 五种连接方式
- 静态加载驱动类:
Properties properties = new Properties();
properties.setProperty("user", "admin");
properties.setProperty("password", "admin");
/*
* 加载驱动类: mysql5.* new com.mysql.jdbc.Driver()
* 加载驱动类: mysql8.* new com.mysql.cj.jdbc.Driver()
*/
Driver driver = new com.mysql.cj.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
// 获得数据库连接
Connection connection = driver.connect(url, properties);
System.out.println(connection);
connection.close();
- 反射加载驱动类:
// 连接到的数据库: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 connection = driver.connect(url, properties);
System.out.println(connection);
connection.close();
- 使用
DriverManager
手动注册驱动类:
// 加载类信息
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);
System.out.println(connection);
connection.close();
- 使用
DriverManager
自动注册驱动类:
// 加载类信息,在加载 Driver 的过程中自动完成注册
Class.forName("com.mysql.jdbc.Driver");
/*
* JDK 底层自动注册驱动类,交给 DriverManager 进行管理
* 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);
System.out.println(connection);
connection.close();
- 从配置文件读取配置信息:
# jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
jdbc.username=admin
jdbc.password=admin
// 从配置文件中读取数据库信息
Properties properties = new Properties();
properties.load(new FileInputStream("jdbc.properties"));
String driver = properties.getProperty("jdbc.driver");
String url = properties.getProperty("jdbc.url");
String username = properties.getProperty("jdbc.username");
String password = properties.getProperty("jdbc.password");
/*
* mysql-connector-java 驱动文件 5.1.6 版本之后无需手动获取驱动类,
* 即省略 Class.forName(“com.mysql.jdbc.Driver”) 也可以获得数据库连接。
* 原因:从 jdk5 以后使用了 jdbc4,会自动调用 jar 包下的 META-INF\services\java.sql.Driver 文本中的类名称进行注册
*/
// 加载类信息,自动注册驱动(可省略)
// Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println(connection);
11.3 ResultSet
ResultSet:表示从数据库读取到的数据表结果集。ResultSet 对象保持一个光标 指向当前的数据行
。最初,光标位于第一行之前。其有一个 next 方法将光标移动到下一行,并且由于在 ResultSet 对象中没有更多行时返回 false,因此常用 while 循环来遍历结果集
// 从配置文件中读取数据库信息
Properties properties = new Properties();
properties.load(new FileInputStream("jdbc.properties"));
String user = properties.getProperty("jdbc.username");
String password = properties.getProperty("jdbc.password");
String driver = properties.getProperty("jdbc.driver");
String url = properties.getProperty("jdbc.url");
// 加载类信息,自动注册驱动(可省略)
Class.forName(driver);
Connection connection = DriverManager.getConnection(url, user, password);
// Sql 语句
String sql = "SELECT * from t_user";
Statement statement = connection.createStatement();
// 执行 SELECT 语句
ResultSet resultSet = statement.executeQuery(sql);
// 遍历结果集
while (resultSet.next()) {
// column index begin with 1
System.out.println(resultSet.getString(1));
System.out.println(resultSet.getString(2));
}
statement.close();
connection.close();
11.4 SQL 注入
- SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入的数据中注入非法的 SQL 语句段或命令,恶意攻击数据库
/**
* @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:存在 SQL 注入
- PreparedStatement:预处理(解决 SQL 注入)
- CallableStatement:用于执行数据库存储过程
11.5 PreparedStatement
PreparedStatement
执行的 SQL 语句中的参数用问号(?)来表示,调用 PreparedStatement
对象的 setXxx() 方法来设置这些参数。setXxx() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引,从 1
开始,第二个参数是设置 SQL 语句中的参数的值
String select = "SELECT * FROM admin WHERE name = ? AND pwd= ?";
// SQL 语句预处理
PreparedStatement preparedStatement = connection.prepareStatement(select);
// column index begin with 1
preparedStatement.setString(1, userName);
preparedStatement.setString(2, pwd);
// 执行 SQL 语句,得到结果集
ResultSet resultSet = preparedStatement.executeQuery();
接口名 | 方法名 | 功能 |
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) | 获得指定列的值 |
11.6 JDBC 事务操作
- 自动提交事务:JDBC 程序中当一个 Connection 对象被创建后,默认情况下自动提交事务,即每执行一条 SQL 语句时,如果执行成功就会向数据库自动提交保存,不能进行回滚
- 开启事务:可以调用 Connection 接口的
setAutoCommit(false)
方法关闭自动提交事务,在所有的 SQL 语句都执行成功后调用commit()
方法提交事务,在其中某个操作失败或出现异常时调用rollback()
方法回滚事务
public void transactional() throws SQLException {
String url = "jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai";
String username = "admin";
String password = "admin";
Connection connection = DriverManager.getConnection(url, username, password);
// 关闭自动提交即开启事务
connection.setAutoCommit(false);
PreparedStatement preparedStatement = null;
try {
String add = "UPDATE account SET balance = balance + 100 WHERE id = 2";
preparedStatement = connection.prepareStatement(add);
preparedStatement.executeUpdate();
int tmp = 1 / 0;
String sub = "UPDATE account SET balance = balance - 100 WHERE id = 1";
preparedStatement = connection.prepareStatement(sub);
preparedStatement.executeUpdate();
// 提交事务
connection.commit();
} catch (ArithmeticException e) {
e.printStackTrace();
// 发生异常,撤销操作,事务回滚
connection.rollback();
} finally {
connection.close();
Objects.requireNonNull(preparedStatement).close();
}
}
11.7 批处理
- 批处理机制:当需要批量插入或者更新记录时,可以采用 Java 的批处理机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单条语句提交处理效率更高。若需开启批处理功能则需要在 url 后加入
rewriteBatchedStatements=true
jdbc.url=jdbc:mysql://localhost:3306/temp?rewriteBatchedStatements=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
- 批处理往往和
PreparedStatement
一起搭配使用,既可以减少编译次数又可以减少运行次数,效率大大提高
String url = "jdbc:mysql://localhost:3306/images_gather?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true";
String username = "admin";
String password = "admin";
Connection connection = DriverManager.getConnection(url, username, password);
String sql = "insert into t_user (username, password) values (?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 1; i <= 1000; i++) {
preparedStatement.setString(1, "batch " + i);
preparedStatement.setString(2, String.valueOf(i));
preparedStatement.addBatch();
if (i % 50 == 0) {
// 批量发送执行
preparedStatement.executeBatch();
// 清空批处理包
preparedStatement.clearBatch();
}
}
preparedStatement.close();
connection.close();
- 批处理源码剖析:
/**
* 第一次会创建 ArrayList<elementData> 会存放预处理后的 SQL 语句,当 elementDate 满后会按照 1.5 倍扩容
* 当达到指定的容量之后,就会发送给 MySQL 执行,批处理会减少发送 SQL 语句的网络开销,减少编译次数,从而提高效率
*/
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));
}
}
11.8 数据库连接池
- 传统 JDBC 的弊端:
- 传统的 JDBC 数据库连接使用
DriverManager
来获取,每次建立数据库连接的时都需要将Connection
加载到内存中,再验证 IP 地址、用户名、密码(耗时0.05s ~ 1s)是否正确 - 当需要连接数据库时就向数据库请求一个连接,频繁的请求操作将占用过多的系统资源,容易造成服务器崩溃
- 每一次数据库连接使用完后都得及时释放,如果程序出现异常导致未能正常关闭数据库连接,过多的数据库连接将导致数据库内存泄漏,最终导致数据库崩溃
- 数据库连接池原理:预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从 “缓冲池” 中取出一个,使用完毕归还给缓冲池(并不断开与数据库的连接)。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个,大大减轻了数据库的压力
- DataSource:JDBC 的数据库连接池使用
javax.sql.DataSource
来表示,DataSource 只是一个接口,具体实现留给第三方
连接池 | 特点 |
C3P0 | 速度相对较慢,稳定性不错(Hibernate、Spring 框架底层均采用) |
Druid | 阿里巴巴提供的数据库连接池,集 DBCP、C3P0、Proxool 优点于一身 |
Proxool | 有监控连接池状态的功能,稳定性较 C3P0 略差 |
BoneCP | 速度快 |
DBCP | 速度较 C3P0 快,但不稳定 |
- C3P0 数据库连接池:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
- 方式一:
jdbc.properties
// 创建数据源对象
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 设置相关信息
comboPooledDataSource.setDriverClass("com.mysql.cj.jdbc.Driver");
comboPooledDataSource.setJdbcUrl("jdbc:mysql://localhost:3306/temp");
comboPooledDataSource.setUser("admin");
comboPooledDataSource.setPassword("admin");
comboPooledDataSource.setInitialPoolSize(10);
comboPooledDataSource.setMaxPoolSize(50);
// 获取一个数据库连接
Connection connection = comboPooledDataSource.getConnection();
System.out.println(connection);
connection.close();
- 方式二:
c3p0-config.xml
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
<default-config>
<property name="driverClass">com.mysql.cj.jdbc.Driver</property>
<property name="jdbcUrl"><![CDATA[jdbc:mysql://localhost:3306/temp?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8]]></property>
<property name="user">admin</property>
<property name="password">admin</property>
<property name="initialPoolSize">10</property>
<property name="maxIdleTime">30</property>
<property name="maxPoolSize">100</property>
<property name="minPoolSize">10</property>
</default-config>
</c3p0-config>
// 将数据库配置信息写入到 c3p0-config.xml 配置文件中,并将其拷贝到类路径下,C3P0 将会自动加载并读取此配置文件
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
Connection connection = comboPooledDataSource.getConnection();
System.out.println(connection);
connection.close();
- Druid 数据库连接池:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
# druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/temp?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
characterEncoding=utf-8
username=admin
password=admin
initialSize=5
maxActive=10
maxWait=3000
validationQuery=SELECT 1
testWhileIdle=true
// 加载配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("druid.properties"));
// 创建一个的连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
11.9 DBUtils
- ResultSet 存在的问题:关闭 connection 后,resultSet 结果集无法继续使用,然而很多时候我们希望关闭 connection 连接后仍然可以继续使用查询到的数据;且 resultSet 存储查询结果的方式不利于数据管理,从 resultSet 结果集中获取数据时操作方法不够明确,getXxx() 方法容易出错,含义模糊
- JavaBean:定义一个类与数据库表的字段一一对应,这样的类一般称作
JavaBean 或 Domain 或 POJO 或 Entity
,将返回的结果集的字段值封装到自定义的类的对象中,将若干个这样的对象放进集合中,就可以直接访问集合从而获得数据库表的查询结果
- 在创建 JavaBean 时类的字段的数据类型强制使用八大基本数据类型对应的包装类,因为 MySQL 数据库表中的字段值可能为空,而 Java 只有引用数据类型才有 NULL 值。创建 JavaBean 类的时候一定要给一个无参构造器和对应的 getter、setter 方法,以方便通过反射机制获取该类信息
- commons-dbutils:
commons-dbutils
是 Apache 组织提供的一个开源 JDBC 工具类,它是对 JDBC 的封装,使用 dbutils 能极大简化 JDBC 编码量
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.3</version>
</dependency>
// 获得连接
Connection connection = DruidUtils.getConnection();
// 获得 Apache 实现的查询对象
QueryRunner queryRunner = new QueryRunner();
String select = "SELECT * FROM cost WHERE id >= ? AND id <= ?";
// 执行 SQL 语句获取查询结果
List<Fishing> fishings = queryRunner.query(connection, select, new BeanListHandler<>(Fishing.class), 1, 10);
for (Fishing fishing : fishings) {
System.out.println(fishing);
}
connection.close();
方法 | 功能 |
ArrayHandler | 将结果集中的第一行数据转换成对象数组 |
ArrayListHandler | 将结果集中的每一行都转换成一个数组,再存放到 List 中 |
BeanHandler | 将结果集中的第一行数据封装到一个对应的 JavaBean 实例中 |
BeanListHandler | 将结果集中的每一行数据封装到对应的 JavaBean 实例中,存放到 List |
ColumnListHandler | 将结果集中的某一列数据存放到 List 中 |
KeyedHandler(name) | 将每行数据封装到 Map 中,再将 map 存入另一个 Map 中 |
MapHandler | 将结果集的第一行数据封装到 Map 中,key 是列名,value 是对应值 |
MapListHandler | 将结果集中的每一行数据封装到 Map 中,再存放到 List 里 |
- BaseDao:为每张数据库表设计一个 JavaBean ,同时为每一张数据库表设计一个专门操作该表的数据访问对象 Dao(
Data Access Object
),将所有的具体 Dao 类中的共有部分抽象出父类 BaseDao,以更好地利用多态完成数据库操作
/**
* @author Spring-_-Bear
* @datetime 2022/3/16 18:54
*/
public abstract class BaseDao {
private final QueryRunner queryRunner = new QueryRunner();
/**
* 执行 insert、update、delete 语句
*
* @param sql sql
* @param params sql 实参
* @return 受影响的行数
*/
public int update(String sql, Object... params) {
Connection connection = null;
try {
connection = JdbcUtil.getConnection();
return queryRunner.update(connection, sql, params);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.close(connection);
}
return -1;
}
/**
* 查询数据库表的一条记录
*
* @param clazz JavaBean class 对象
* @param sql sql
* @param params sql 实参
* @param <T> 返回类型泛型
* @return 一条记录 or null
*/
public <T> T getRecord(Class<T> clazz, String sql, Object... params) {
Connection connection = null;
try {
connection = JdbcUtil.getConnection();
return queryRunner.query(connection, sql, new BeanHandler<>(clazz), params);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.close(connection);
}
return null;
}
/**
* 查询返回多条数据库表记录
*
* @param clazz JavaBean 的 class 对象
* @param sql sql
* @param params sql 实参
* @param <T> 返回类型的泛型
* @return 多条记录 or null
*/
public <T> List<T> listRecord(Class<T> clazz, String sql, Object... params) {
Connection connection = null;
try {
connection = JdbcUtil.getConnection();
return queryRunner.query(connection, sql, new BeanListHandler<>(clazz), params);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.close(connection);
}
return null;
}
/**
* 查询返回单个数值
*
* @param sql sql
* @param params sql 实参
* @return 单个数值 or null
*/
public Object getSingleValue(String sql, Object... params) {
Connection connection = null;
try {
connection = JdbcUtil.getConnection();
return queryRunner.query(connection, sql, new ScalarHandler(), params);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtil.close(connection);
}
return null;
}
}