Java基础知识点
2021-04-26 06:06:13 1 举报
AI智能生成
Java编程语言基础知识点
作者其他创作
大纲/内容
面向对象 OOP
基础知识
类是模板,可以通过类模板创建多个实例,这些实例具有相同的方法、属性,例如: class Person{ ... }
实例是类具体化的结果,例如: Person xiaoming = new Person()
属性/字段是类的一些通用特征,例如:
class Person {
int age;
}
class Person {
int age;
}
方法是类内部用于处理事务的代码,例如:
class Person {
int age;
public int getAge() {
return this.age;
}
}
如果方法没有返回值,则需要将方法返回类型设置为void,同时省略return语句
class Person {
int age;
public int getAge() {
return this.age;
}
}
如果方法没有返回值,则需要将方法返回类型设置为void,同时省略return语句
可变参数,定义格式:类型...,可变参数表示元素为指定类型的数组
如果方法的参数定义为可变参数,如果传入null,则会报错,传入空参数则会得到一个空数组
this是类内部的隐含变量,始终指向当前实例,访问类内字段时需要加this,在字段命名不重复的情况下可以省略this
任何子类的构造函数,都必须先调用父类的构造函数,如果未主动调用,则系统会自动调用super()方法
向上转型:从Java15开始,可以使用sealed来修饰class,并用permits来指定哪些子类可以继承该类,这样可以避免滥用继承,示例代码:
public sealed class Shape permits Rect, Circle, Triangle {
...
}
public sealed class Shape permits Rect, Circle, Triangle {
...
}
向上转型:子类可赋值给父类变量,就是向上转型,原因是子类实现了父类的全部方法,所以可以赋值
Person p = new Student();
Person p = new Student();
向下转型:将子类强制转型为父类就是向下转型,存在失败的可能,因此需要通过instanceof来判断要转型的实例是否为指定类型
Java14开始简化instanceof的使用方式,在判断instanceof的同时,可同时进行转型,例如:
if (obj instanceof String as str) {
System.out.print(s)
}
if (obj instanceof String as str) {
System.out.print(s)
}
多态,某个类型的方法调用,其真正执行的方法取决于运行期间实际类型的方法
final
如果某个类不希望被继承,则需要使用final来修饰该类;
当父类中的某些方法不希望被子类所覆盖(@Override),则需要将这些访问标准为final,子类覆盖final方法时会抛出异常;
当类中的某些字段不希望被修改,则需要使用final来修改该字段,该字段初始化后将不能被修改。通常final字段是在构造函数中进行初始化;
如果某个类不希望被继承,则需要使用final来修饰该类;
当父类中的某些方法不希望被子类所覆盖(@Override),则需要将这些访问标准为final,子类覆盖final方法时会抛出异常;
当类中的某些字段不希望被修改,则需要使用final来修改该字段,该字段初始化后将不能被修改。通常final字段是在构造函数中进行初始化;
抽象类
抽象类需要使用关键字abstract来修饰;
抽象类无法被实例化;
抽象类可以有字段和非抽象方法;
抽象类是用来被继承的,因此可以在抽象类中定义方法规范,强制其子类实现各抽象方法的代码;
面向抽象编程,尽量使用高层次类型,避免使用具体子类型的编程方式,其本质是:
1. 上层代码只定义规范(例如:abstract class Person);
2. 不需要子类就可以实现业务逻辑(正常编译);
3. 具体的业务逻辑由不同的子类实现,调用者并不关心。
抽象类需要使用关键字abstract来修饰;
抽象类无法被实例化;
抽象类可以有字段和非抽象方法;
抽象类是用来被继承的,因此可以在抽象类中定义方法规范,强制其子类实现各抽象方法的代码;
面向抽象编程,尽量使用高层次类型,避免使用具体子类型的编程方式,其本质是:
1. 上层代码只定义规范(例如:abstract class Person);
2. 不需要子类就可以实现业务逻辑(正常编译);
3. 具体的业务逻辑由不同的子类实现,调用者并不关心。
接口
如果一个抽象类没有实例字段(但可以有静态字段),且所有方法都是抽象方法,则该类可以定义为一个接口
如果一个抽象类没有实例字段(但可以有静态字段),且所有方法都是抽象方法,则该类可以定义为一个接口
技巧:实例化的永远是一个子类,但总是通过接口变量去引用这个实例
接口中可以定义default方法,继承该接口的类可以不必实现default方法,也可以根据实际需要去重写default方法,default方法的存在意义是,当要给接口增加一个方法时,会导致所有子类都需要修改代码,而增加一个default方法,就不需要改动所有子类了
default void func() { ... }
default void func() { ... }
接口中只能包含静态字段,编译器会自动将接口中定义的字段编译为public static final 类型
静态方法和静态字段
使用static修饰的方法和字段就是静态方法和静态字段,静态方法和静态字段是所有类实例所共有的,
访问时需使用类名.方法名或类名.字段名的形式来访问
使用static修饰的方法和字段就是静态方法和静态字段,静态方法和静态字段是所有类实例所共有的,
访问时需使用类名.方法名或类名.字段名的形式来访问
包
用于解决类名冲突问题,可以理解成类文件的存储目录
用于解决类名冲突问题,可以理解成类文件的存储目录
包没有父子关系,例如:java.util和java.util.zip没有任何关系,是两个独立的包
一个class如果没有定义包,编译器会使用默认包,容易引起命名冲突,因此尽量定义包
包作用域:在同一个包内,不用public, protected, private 修饰的方法和字段就是包作用域,可以直接引用
import java.util.*; 表示将java.util包下的所有类都引入进来
当编译器遇到一个class名时,查找这个类位置的顺序是:
1. 查找当前包中是否有这个class;
2. 查找import中是否有这个class;
3. 查找java.lang中是否有这个class;
如果以上都没有,则编译器会报错。
1. 查找当前包中是否有这个class;
2. 查找import中是否有这个class;
3. 查找java.lang中是否有这个class;
如果以上都没有,则编译器会报错。
包命名的建议,将域名倒写作为报名,例如:com.abc.project
作用域
public 可被任何类访问
private 无法被其他类访问,如果private类存在内部嵌套类,则嵌套类有权限访问private方法
protected 作用于继承关系,protected方法和字段可以被子类访问
package 包作用域是指一个类,允许访问同包中未用public、private、protected修饰的类、方法、字段
final final修饰的class不能被继承,final修饰的方法不能被覆盖,final修饰的字段和局部变量不能被重新赋值
最佳实践
1. 不确定是否需要使用public修饰时,尽量不用public
2. 把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。
3. 一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。
1. 不确定是否需要使用public修饰时,尽量不用public
2. 把方法定义为package权限有助于测试,因为测试类和被测试类只要位于同一个package,测试代码就可以访问被测试类的package权限方法。
3. 一个.java文件只能包含一个public类,但可以包含多个非public类。如果有public类,文件名必须和public类的名字相同。
classpath
JVM通过环境变量classpath决定搜索class的路径和顺序
不推荐通过设置环境变量classpath,始终推荐通过-cp传入路径
jar包
是一个zip压缩包,生成zip压缩包后修改扩展名为.jar即可
包含多个class文件,方便传递和使用
MANIFEST.MF可配置jar包信息,如果包含Main-Class则可以直接运行jar包,命令为:java -jar abc.jar
模块 Module
Java9开始引入模块,模块用于管理依赖关系
module-info.java用于描述当前模块运行所依赖的其他模块
生成模块过程:
1. 创建oop-module项目;
2. 在module-info.java文件中添加依赖模块,使用requires关键字;
3. 在module-info.java文件中用exports控制本模块中类的访问权限;
4. 编辑生成class文件;
5. 打包生成jar包;
6. 使用jmod命令将jar包转为jmod包;
1. 创建oop-module项目;
2. 在module-info.java文件中添加依赖模块,使用requires关键字;
3. 在module-info.java文件中用exports控制本模块中类的访问权限;
4. 编辑生成class文件;
5. 打包生成jar包;
6. 使用jmod命令将jar包转为jmod包;
jmod不可以直接允许,主要是用于打包jre
使用jlink命令打包jre,仅将用到的模块打包到jre中,可以有效的对jre进行瘦身
Java核心类
String 字符串
字符串是引用类型,其内部实现为一个char[]
字符串是不可修改的,任何对字符串的变更操作实际上都是复制了一个新的字符串对象
==比较的是字符串内存地址,equals()比较的是字符串内容,equalsIgnoreCase()是比较字符串内容时忽略大小写
常用方法
equals() 比较字符串内容是否相同
equalsIgnoreCase() 比较字符串内容是否相同,忽略大小写
contains() 是否包含子字符串
indexOf() 子字符串首次出现位置
lastIndexOf() 子字符串最后出现位置
startsWith() 以指定字符串开头
endsWith() 以指定字符串结束
substring() 提取子字符串
trim() 去掉首尾空白字符,默认空白字符包括:\t \r \n
strip() 与trim类似可以去掉首尾空白字符,包括中文空格字符\u3000
stripLeading() 去掉字符串左侧空白字符
stripTrailing() 去掉字符串右侧空白字符
isEmpty() 判断字符串是否为空,即长度为0
isBlank() 判断字符串是否只包含空白字符,空白字符包括:\t\r\n
replace() 通过字符替换字符串中子串
replaceAll() 通过正则表达式替换字符串中子串
split() 分割字符串,本方法参数为正则表达式
join() 使用自定字符串将一个字符串数组拼接为一个字符串
format()和formatted() 用于格式化字符串
%s 显示字符串
%d 显示整数
%x 显示十六进制数
%f 显示浮点数
valueOf() 将任意类型转为字符串类型
toCharArray() 将字符串转为字符数组
new String(char[]) 将字符数组转为字符串
getBytes() 转换字符串的编码,例如:"Hello".getBytes("UTF-8"),转换后返回的是byte[],如需字符串,可通过new String(byte[], "编码格式")将byte[]转为字符串
Java的String和Char在内存中始终以Unicode编码表示
早期的JDK中,String总是以char[]来存储,较新的JDK中总以byte[]来存储String
StringBuilder
使用String处理字符串,每次变动字符串内容都会产生新的字符串对象,浪费内存影响性能,因此Java提供了StringBuilder可变对象
StringBuilder支持链式操作,实现链式操作的关键是调用的方法返回的是对象自身this,可以按照这个思路实现链式操作
StringBuffer是StringBuilder的线程安全版本,但现在很少使用了
对字符串进行迭代拼接时,用StringJoiner更方便,如果不需要定义字符串的头尾,可以直接使用String.join()这个静态方法来拼接字符串,这个静态方法也是通过StringJoiner实现的
JavaBean
JavaBean是一种符合命名规范的class,它通过getter和setter来定义属性;
使用Introspector.getBeanInfo()可以获取属性列表
枚举类型
与int相比的好处
枚举类型自带类型信息,编译器可以做类型检查
不可能引用非枚举的值
不同类型枚举值之间不能进行比较和赋值
枚举类型虽然是引用类型,但由于每个枚举常量在JVM中都是唯一的实例,因此可以使用 == 对枚举值进行相等判断,当然equals也是可以使用的
enum与普通class相比具有的特点
定义的enum总是继承自java.lang.Enum,且无法被继承
只能通过定义来创建实例,而不能通过new来创建实例
定义的每个实例都是引用类型的唯一示例
可将enum实例用于switch语句
enum类型经过编译器编译后也是一个普通的class,只不过构造函数被设置为private,防止通过new来创建实例。
可通过覆盖enum的构造函数来给枚举值添加参数,例如:
enum {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
enum {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}
可通过覆盖enum的toString()方法来控制枚举类型调用toString()时的输出格式
记录类
可通过record关键字来定义记录类,记录类是不变类,从Java14开始提供record
记录类中可以定义静态方法
记录类中可定义Compact Constructor验证参数
BigInteger类
用于表示任意大小的整数
BigInteger是不变类,继承自Number
BigInteger转换为基本类型时,可以通过longValueExact()来保证结果的准确性
BigInteger的运行速度要低于基本数据类型的运行速度
BigDecimal类
表示任意大小的并且精度准确的浮点数
比较BigDecimal是否相等只能使用compareTo(),而不能使用equals()
常用工具类
Math 数学计算类
abs 求绝对值
max 求最大值
pow 求x的y次方
sqrt 求开方
exp 求e的x次方
log 求e为底数的对数
log10 求10为底数的对数
三角函数 sin(), cos(), tan(), asin(), acos()
数学常量:Math.PI、Math.E
random 创建0到1之间的随机数
StrictMath 提供与Math几乎一模一样的方法,该类与Math的不同是:
对于浮点运算,Math在不同平台(例如:x86和ARM)上的运算结果可能不同,
而StrictMath则保证在各种平台下的运算结果是相同的
对于浮点运算,Math在不同平台(例如:x86和ARM)上的运算结果可能不同,
而StrictMath则保证在各种平台下的运算结果是相同的
Random 伪随机数类
是指只要给定一个初始的种子,产生的随机数序列是完全一样的
Math.random() 也是调用Random类来实现的,只是无法指定种子
不指定种子时,Random通常会以系统时间戳作为种子
常用方法
r.nextInt() 返回随机序列中的下一个整数
r.nextInt(10) 返回一个0-10之间的随机整数
r.nextLong() 返回一个长整型数
r.nextFloat() 返回一个单精度浮点数
r.nextDouble() 返回一个双精度浮点数
SecureRandom 安全随机数类
无法指定种子
java.security.SecureRandom
需要使用安全随机数的时候,必须使用SecureRandom,绝不能使用Random!
注解
注解是JAVA语言用于工具处理的标注
注解可以配置参数,如果没有配置参数则表示参数都使用默认值
如果参数名称是value,且只有一个参数,那么可以省略参数名称
元注解:用于修饰其它注解的注解称为元注解
JAVA基础
程序结构及规范
类名要求:必须以英文字母开头(建议使用大写字母),后面为英文、数字、下划线的组合
类的入口方法必须是静态方法,方法名称必须是main,方法参数必须是字符串数组,示例:public static main (String[] args)
注释
行注释 //
多行注释 /* */
文档注释 /** */
修饰符
public
定义类时不写public修饰符不影响编译,但该类将无法在命令行执行
static
final
final修饰变量就变成了常量,常量定义后不可修改,否则编译时会报错
关键字
var 定义自动识别类型的变量,例如:var sb = new StringBuilder(); // 编译器会自动推断sb的类型是StringBuilder
变量
基础类型变量(值变量):变量始终指向一个固定的内存地址,对值变量赋值,会覆盖掉该内存地址上之前的值
引用类型变量(引用变量):变量会指向赋值变量的内存地址,对赋值或被赋值变量修改时,会同时影响两者的变量值
基础类型
整型
byte 字节型,8bit,范围:-128 ~ 127
short 短整型,16bit,范围:-32768 ~ 32767
int 整型,32bit,范围:-2147483648 ~ 2147483647
long 长整型,64bit,范围:-9223372036854775808 ~ 9223372036854775807
有效的定义方式
int i = 2147483647;
int i = -2147483648;
int i = 2_000_000_000;
int i = 0xff0000;
int i = 0b1000000000;
long l = 900000000000000L;
特别注意:同一数的不同进制的表示是完全相同的,例如:15 = 0xf = 0b111
浮点型
float 单精度浮点型,范围:3.4x10^38
double 双精度浮点型,范围:1.79x10^308
有效的定义方式
float f = 3.14f;
float f = 3.14e38f;
double d = 1.79e308;
double d = -1.79e308;
double d = 4.9e-324;
由于浮点数常常无法精确表示,所以浮点运算的结果会存在误差
判断两个浮点数是否相等,需要判断两个浮点数之差的绝对值是否小于某一非常小的数值,而不能直接用==判断
boolean 布尔型
JVM中,布尔类型表示为4字节整数
char 字符型
可表示标准的ASCII,也可以表示Unicode
注意:char类型使用单引号,需要与双引号的字符串类型区分开
Java在内存中总是用Unicode表示字符,因此一个英文字母和一个中文汉字,都用一个char类型表示,都占用两个字节
Unicode编码:将char型字符赋值给int变量就可以获得该字符的Unicode编码,例如:int c = '中';
可以直接用转义字符\u+Unicode编码来表示一个字符,例如:\u0041表示A,0041是16进制数
对整数强制转型为char会获取到该整数对应的char字符,例如:(char)72 返回H
基础类型(值类型)变量间判断是否相同,除了浮点型外都可以使用==来判断
引用类型
String 字符串类型
+号可连接多个字符串,字符串连接数字时,会先将数字转为字符串后再进行连接
从Java13开始支持多行字符串,用""" ... """表示,多行字符串前面共同的空格会被去掉
字符串不可变,每次对字符串赋值都会在内存中开辟新的空间来存储新的内容,再将变量的指针指向该字符串的起始位置
null和“”是不同的,""表示一个空字符串对象,null则表示该变量没有指向到任何对象
数组类型
定义数组方式一:TYPE[] a = new TYPE[length],指定数组长度
定义数组方式二:TYPE[] a = new TYPE[] {1, 2, 3, 4, ...},指定元素初始值,由编译器自动推算数组长度
定义数组方式二简化版:TYPE[] a = {1, 2, 3, 4, ...};
数组所有元素初始化为默认值,整型是0,浮点型是0.0,布尔型是false,数组创建后,大小不可改变
为数组变量指定新的数组时,并不会覆盖原来的数组,而是在内存中开辟新的空间存储新数组,再将数组变量指向到新数组
引用类型变量间判断是否相等,需要使用equals()来判断,例如:s1.equals(s2)
运算符
常用运算符:+、-、x 、/、%
整数移位运算符
>> / <<:带符号右移或左移
>>>:不带符号右移
整数位操作符
& 按位与运算
| 按位或运算
~ 按位非运算
^ 按位异或运算
运算优先级(从上至下)
( )
! - ++ --
* / %
+ -
<< >> >>>
&
|
+= -= *= /=
类型提升:不同类型间的运算结果会是较大类型,例如:short和int运算,结果为int,int与浮点数运算,结果会是浮点数
整数除以0在编译时会报错,浮点数除以0在编译时不会报错,但运行时会返回溢出结果:NaN(非数字) / Infinity(无穷大) / -Infinity(负无穷大)
四舍五入:浮点型强制转型为整型时会直接舍弃小数位,不进行四舍五入,如果需要四舍五入则需要+0.5后再强制转型
布尔运算
布尔运算优先级(从上至下)
!
>, >=, <, <=
==, !=
&&
||
短路运算
&&运算中,当第一个值为false时,后面的值不会进行运算,直接返回false结果
||运算中,当第一个值为true时,后面的值也不会进行运算,直接返回true
三元运算:b ? x : y
流程控制
输入与输出
System.out.printf() 控制输出内容的格式
%d 格式化输出十进制整数
%x 格式化输出16进制整数
%f 格式化输出浮点数
%e 格式化输出科学计数法表示的浮点数
%s 格式化字符串
System.util.Scanner 对象用来控制从控制台的输入
读取控制台输入的字符串用scanner.nextLine()
读取控制台输入的整数用scanner.nextInt()
if ... else ...
如果if条件后只有一行代码,则可以省略{},但不推荐这么做,不利于代码阅读
switch ... case ... default ...
switch语句具有穿透性,当某一个case满足条件开始执行时,如果该case中没有break,
则该case执行完后会继续执行后续的case,直到遇到break或switch执行结束
则该case执行完后会继续执行后续的case,直到遇到break或switch执行结束
default 不要漏掉
从Java12开始支持“switch表达式”,不需要写break了
流程控制,示例:
switch (a) {
case 1 -> System.out.println(a);
case 2 -> System.out.println(a);
default -> System.out.println("OK");
}
switch (a) {
case 1 -> System.out.println(a);
case 2 -> System.out.println(a);
default -> System.out.println("OK");
}
赋值,示例:
int a = switch(b) {
case "apple" -> 1;
case "orange" -> 2;
default -> 0;
}
int a = switch(b) {
case "apple" -> 1;
case "orange" -> 2;
default -> 0;
}
yield表示在switch语句中返回值,示例:
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> {
int code = fruit.hashCode();
yield code; // switch语句返回值
}
};
while { ... }
先判断循环条件,再执行循环语句,存在一次都不执行的可能
do { ... } while( ... )
先执行循环语句,再判断循环条件,至少要执行一次
for ( 计数器初始化; 循环条件判断; 计数器更新; )
不设置循环条件
for (int i = 0; ; i++) { ... }
for (int i = 0; ; i++) { ... }
不设置循环条件和计数器更新
for (int i = 0; ; ) { ... }
for (int i = 0; ; ) { ... }
什么都不设置
for ( ; ; ;) { ... }
for ( ; ; ;) { ... }
for each语句,用于便利可迭代对象中的每个元素,代码格式为:for (type i : ns) { ... }
break 和 continue
break 只会跳出当前循环
continue 会提前结束本次循环,并开始执行下次循环
异常处理
JAVA中异常的体系
Throwable
Error
严重错误,程序通常对此无能为力
不需要捕获
Exception
RuntimeException
不需要捕获
非RuntimeException
必须捕获
声明异常
定义方法时,可通过throws关键字来说明本方法可能会抛出的异常,
强制要求调用本方法的程序必须捕获本异常,否则编译时会报错,示例:
public byte[] getBytes(String charName) throws UnsupportedEncodingException { ... }
强制要求调用本方法的程序必须捕获本异常,否则编译时会报错,示例:
public byte[] getBytes(String charName) throws UnsupportedEncodingException { ... }
如果在调用方的throws中抛出了被调用方抛出的异常,则调用方不需要捕获该异常,示例:
public byte[] toGBK(String str) throws UnsupportedEncodingException {
return str.getBytes("GBK");
}
public byte[] toGBK(String str) throws UnsupportedEncodingException {
return str.getBytes("GBK");
}
捕获异常
只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。
所有未捕获的异常,最终也必须在main()方法中捕获,否则在编译时会报错。
所有未捕获的异常,最终也必须在main()方法中捕获,否则在编译时会报错。
所有异常都可以通过e.printStackTrace()输出异常堆栈
多个catch语句中,异常子类必须放在父类的前面
finally语句中的代码,无论是否发生异常都会执行
catch语句中可以同时处理多个异常,如果多个异常的处理程序是一样的,可以用如下形式来处理:
try {
...
} catch (IOException | NumberFormatException e) { ... }
try {
...
} catch (IOException | NumberFormatException e) { ... }
抛出异常
异常会逐级上传,直至Main方法,如果一直未被捕获,则编译报错
捕获异常时需要尽量保留原始异常信息,可将原始异常实例传入最终抛出的异常实例中
包含finally的try语句执行顺序为:发生异常时先执行catch中语句,然后执行finally中语句,最后抛出异常
自定义异常
建议自定义异常的“根异常”派生自RuntimeException
“根异常”需要提供多种构造方法,以适应多种场景下的使用需要
记录异常
JDK Logging
Java标准库提供了java.util.logging来实现日志功能
使用比较繁琐,且在main运行时无法修改
使用不广泛
Commons Logging
使用广泛的日志“接口”,可以挂载多个日志系统,例如:JDK Logging、Log4j
API非常简单
可自动检测其它日志模块,并自行调用
最佳实现,建议始终用Commons Logging的API来输出日志,利用其可随意切换日志系统的特性来根据需要选择日志系统
Log4j
流行的日志框架及系统
可将日志信息按照级别输出到不同目的地
可通过log4j2.xml配置文件来控制日志的输出格式及输出规则
需要更换掉Log4j时,只需将log4j2.xml和相关的jar移除即可,无需修改代码
SLF4J + Logback
可以取代Commons Logging + Log4j的组合
逐渐开始流行
反射
什么是反射?反射是指在程序运行时可以拿到对象的全部信息。
JVM反射的实现方式是:JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息
JVM反射的实现方式是:JVM为每个加载的class及interface创建了对应的Class实例来保存class及interface的所有信息
为什么用反射?反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
Class
获取class的Class实例有三种方法
通过class的静态变量class获取,例如:
Class cls = String.class;
Class cls = String.class;
通过实例变量的getClass()方法获取,例如:
String s = "Hello";
Class cls = s.getClass();
String s = "Hello";
Class cls = s.getClass();
通过class的完整类名获取,例如:
Class cls = Class.forName("java.lang.String");
Class cls = Class.forName("java.lang.String");
创建对象实例
可通过Class.newInstance()来创建对象实例
反射创建实例的局限是,只能调用对象的public无参数构造方法,非public或带参数构造方法则无法调用
示例:
Class cls = String.class;
String s = (String) cls.newInstance();
Class cls = String.class;
String s = (String) cls.newInstance();
动态加载
JVM在执行Java程序的时候,并不是一次性把所有用到的class全部加载到内存,而是第一次需要用到class时才加载
动态加载机制使我们可以根据条件加载不同的实现类,例如:Commons Logging在Log4j存在时会自动加载Log4j,不存在时会自动加载JDK Logging
获取Class对象实例cls信息的方法
cls.getName() 获取完整类名
cls.getSimpleName() 获取简单类名
cls.getPackage().getName() 获取包名
cls.isInterface() 是否接口类型
cls.isEnum() 是否枚举类型
cls.isArray() 是否数据类型
cls.isPrimitive() 是否为基本类型
Field
获取Class对象字段信息的方法
注意:以下方法返回的类型为Field或Fieldp[]
注意:以下方法返回的类型为Field或Fieldp[]
cls.getField() 根据名称获取对象public字段信息,包括继承自父类的字段
cls.getDeclaredField() 获取当前实例指定字段的信息,不包括继承自父类的字段
cls.getFields() 获取对象全部public字段信息,包括继承自父类的字段
cls.getDeclaredFields() 获取当前实例所有字段信息,不包括继承自父类的字段
Field对象提供的方法
getName() 获取字段名称
getType() 获取字段类型
getModifiers() 返回字段的修饰符,它是一个int,不同的bit表示不同的含义
获取字段信息的示例代码:
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]类型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
通过Field对象获取或设置字段值
通过Field.get(Object)可获取对象示例指定字段的值,Field.set(Object, Value)则可以设置字段值
当字段修饰符为private时,会由于没有权限读取字段内容而报错,此时可通过Field.setAccessible(true)来允许字段被访问。
但如果JVM运行时存在SecurityManager,则setAccessible会失败。
但如果JVM运行时存在SecurityManager,则setAccessible会失败。
获取字段值的示例代码:
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
设置字段值的示例代码:
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
Method
获取Method的几种方法
Method Class.getMethod() 根据名称获取public的方法,包括继承自父类的方法
Method Class.getDeclaredMethod() 根据名称获取全部方法,不包括继承自父类的方法
Method[] Class.getMethods() 获取实例全部public方法,包括继承自父类的方法
Method[] Class.getDeclaredMethods() 获取实例全部方法,不包括继承自父类的方法
Method提供的方法
method.getName() 获取方法名称
method.getReturnType() 获取方法返回值类型
method.getParameterTypes() 获取方法参数值类型,返回类型数组
method.getModifiers() 返回方法修饰符
执行Method
method.invoke(Object, Parameter)
Object 是要执行该方法的对象实例,当method是静态方法时,该参数必须为null
Parameter 是执行该方法是需要传入的参数,参数类型必须符合方法定义,如果该方法没有参数,则可省略该参数
Object 是要执行该方法的对象实例,当method是静态方法时,该参数必须为null
Parameter 是执行该方法是需要传入的参数,参数类型必须符合方法定义,如果该方法没有参数,则可省略该参数
调用非public方法时,需要先执行setAccessible(true),但如果JVM运行时存在SecurityManager,则会报错
使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)
Constructor
由于class.newInstance的局限性,为了调用任意构造方法,Java的反射API中提供了Constructor对象,它包含了构造方法的全部信息,并始终返回实例
获取Constructor的方法
getConstructor() 获取某个public构造方法,JVM会根据传入的参数类型来确定返回哪个构造方法
getDeclaredConstructor() 获取某个构造方法
getConstructors() 获取全部public构造方法
getDeclaredConstructors() 获取全部构造方法
调用Constructor创建实例的示例代码
// 获取构造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
Constructor cons1 = Integer.class.getConstructor(int.class);
// 调用构造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 获取构造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
继承关系
获取继承关系的方法
Class cls.getSuperclass() 获取实例的父类Class
Class[] cls.getInterfaces() 获取实例实现的全部接口
如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()
示例代码:
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer
动态代理
没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK提供的动态创建接口对象的方式,就叫动态代理
在运行期动态创建一个interface实例的方法如下:
1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
1. 使用的ClassLoader,通常就是接口类的ClassLoader;
2. 需要实现的接口数组,至少需要传入一个接口进去;
3. 用来处理接口方法调用的InvocationHandler实例。
3. 将返回的Object强制转型为接口。
1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
1. 使用的ClassLoader,通常就是接口类的ClassLoader;
2. 需要实现的接口数组,至少需要传入一个接口进去;
3. 用来处理接口方法调用的InvocationHandler实例。
3. 将返回的Object强制转型为接口。
代码实例:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
泛型
泛型就是编写模板代码来适应任意类型
泛型的好处是使用时不必对类型进行强制转换,它通过编译器对类型进行检查
泛型向上转型时要保持T不变
编写泛型时,需要定义泛型类型<T>
静态方法不能引用泛型类型<T>,必须定义其他类型(例如<K>)来实现静态泛型方法
泛型可以同时定义多种类型,例如Map<K, V>
Java使用擦拭法实现泛型,泛型<T>:
不能是基本类型
不能获取带泛型类型的Class
不能判断带泛型类型的类型
不能实例化T类型
泛型方法要防止重复定义方法
子类可以获取父类的泛型类型<T>
extends通配符
使用<? extends Class>修饰符定义的泛型称为上界通配符(Upper Bounds Wildcards)
使用上界通配符的泛型,支持传入限定类型的子类型,例如:<? extends Number>支持传入Integer/Double/BigDecimal
上界通配符定义的泛型,仅支持对子类型的读取,禁止对子类型的写入,可通过此特性编写只读泛型方法
可以通过extends来限制泛型接收的类型范围
super通配符
使用<? super Class>通配符定义的泛型只能接收Class的父级类型
<? super Class>定义的泛型只能写入,不能读取
对于extends和super的使用需要遵循PECS原则(Producer Extends Consumer Super),生产使用extends,消费使用super
<?>无限定通配符
使用<?>定义的泛型既不能读也不能写,只能用于是否为null的判断
无限定通配符很少使用,通常可以使用<T>来替代
<?>是所有<T>的超类
集合
Collection接口
List接口
常用方法
boolean add(E e) 在列表末尾添加元素,允许添加重复的值,接收null值
boolean add(int index, E e) 在指定索引位置添加元素,允许添加重复的值,接收null值
int remove(int index) 删除指定索引位置的元素
int remove(Object e) 删除指定元素
E get(int index) 获取指定索引位置的元素
int size() 获取列表长度
List.of(T...) 根据参数快速创建列表,不接受null值
toArray() 将List转为Array,此方式会丢失数据类型信息,很少使用
toArray(T[]) 将List转为Array,并携带数据类型信息
toArray(IntFunction<T[]> generator) 将List转为Array,并携带数据类型信息,且传入的数组长度正合适
常用子类
ArrayList 数组列表,查询效率高,占用空间小,优先于LinkedList使用
LinkedList 链表,查询效率低且占用空间大
遍历
Iterator(迭代器) 是遍历效率最高的方式,应始终坚持用迭代器遍历可迭代类型
Java中的for each 循环语句可以自动帮助我们使用合适的迭代器。实现了Iterable接口的集合类都可以使用for each来遍历
编写equals
如果需要调用List的contains()、indexOf()这些方法时,需要在放入List的元素中编写equals方法
正确编写equals的步骤:
1. 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
2. 用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false;
3. 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
1. 先确定实例“相等”的逻辑,即哪些字段相等,就认为实例相等;
2. 用instanceof判断传入的待比较的Object是不是当前类型,如果是,继续比较,否则,返回false;
3. 对引用类型用Objects.equals()比较,对基本类型直接用==比较。
Set接口
用于存储不重复的元素集合
常用实现类
HashSet 仅实现了Set接口,未实现SortedSet接口,因此元素无序
TreeSet 实现了Set接口和SortedSet接口,因此有序
Queue接口
是实现了一个先进先出(FIFO:First In First Out)的有序表
队列接口提供的会抛出异常的方法
boolean add(<E>) 添加元素到队尾
E remove() 获取队首元素并删除
E element() 获取队首元素不删除
队列接口提供的会返回null或false的方法
boolean offer(<E>) 添加元素到队尾
E poll() 获取队首元素并删除
E peek() 获取队首元素不删除
要避免将null放入队列中,这会导致无法确定是否队列已空
常用实现类
LinkedList
PriorityQueue 优先级队列
从队首取出的元素都是优先级最高的元素
默认按元素比较的顺序排序(必须实现Comparable接口),也可以通过Comparator自定义排序算法(元素就不必实现Comparable接口)
Deque接口
双端队列接口,可从队首和队尾添加/读取元素
常用实现类
LinkedList
ArrayDeque
提供的方法
addFirst(<E>) / offerFist(<E>) 将元素添加到队首
E removeFirst() / E pollFirst() 从队首读取元素并删除
E getFirst() / E peekFirst() 从队首读取元素不删除
addLast(<E>) / offerLast(<E>) 将元素添加到队尾
E removeLast() / E pollLast() 从队尾读取元素并删除
E getLast() / E peekLast() 从队尾读取元素不删除
Stack接口
由于历史原因,Java中以前的Stack接口已停止使用,目前的Stack是通过Deque来模拟实现的
提供的方法
push(<E>) 入栈
E pop() 出栈并删除
E peek() 出栈不删除
Iterator对象
是一种抽象的数据访问模型
使用Iterator模式进行迭代的好处有
对任何集合都采用同一种访问模型
调用者对集合内部结构一无所知
集合类返回的Iterator对象知道如何迭代
一个集合类要实现for each遍历,需满足的条件
集合类实现Iterable接口,该接口要求返回一个Iterator对象
用Iterator对象迭代集合内部数据
Java提供了标准的迭代器模型:java.util.Iterable接口,返回java.util.Iterator实例
Collections类
JDK提供的工具类,提供了一系列静态方法,方便操作各种集合类
常用方法
创建空集合
List<T> emptyList()
Map<K, V> emptyMap()
Set<T> emptySet()
创建单元素不可变集合
List<T> singletonList(T o)
Map<K, V> singletonMap(K key, V value)
Set<T> singleton(T o)
sort() 对List进行排序
shuffle() 对List进行洗牌
创建不可变集合
List<T> unmodifiableList(List<? extends T> list)
Set<T> unmodifiableSet(Set<? extends T> set)
Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)
Map接口
Map是一种键值对集合,可以通过键K快速查找到值V,键K是无序的
遍历
keySet() 获取Map键列表,通过for each键来获取值
entrySet() 获取Map键值列表,通过for each获取Map元素对象
实现
HashMap 最常用的Map实现方式,需引用 java.util.HashMap
Map的实现原理:
1. 用一个大数组存储Value,通过一定的Hash算法将Key换算成Value在数组中存放的索引值。
2. 存放Value 的数组初始长度为16,以后每次自动扩容都是16的倍数。
3. 不同Key值通过Hash运算,可能会得到相同的索引值,此时该索引对应的Value数组元素会保存为一个List,List元素为Value,再通过循环判断具体需要取的是哪个Value.
1. 用一个大数组存储Value,通过一定的Hash算法将Key换算成Value在数组中存放的索引值。
2. 存放Value 的数组初始长度为16,以后每次自动扩容都是16的倍数。
3. 不同Key值通过Hash运算,可能会得到相同的索引值,此时该索引对应的Value数组元素会保存为一个List,List元素为Value,再通过循环判断具体需要取的是哪个Value.
EnumMap 当MAP的键为枚举型时,建议使用EnumMap,此类型查询效率最高,且不浪费空间
SortedMap 是对Key进行了排序的Map,并严格按照Key的排序遍历元素,对于String类型的Key,默认是按照字母升序排列,可以通过重写Comparable接口来实现自定义排序规则,最常用的实现为TreeMap
Properties
专门用于读写配置文件的接口,其本质上是一个Hashtable
Java默认以.properties扩展名文件为配置文件,每行是一个String类型的键值对
读取配置文件分三步:
1. 创建Properties实例;
2. 通过load()载入配置文件;
3. 通过getProperty()获取配置数据;
1. 创建Properties实例;
2. 通过load()载入配置文件;
3. 通过getProperty()获取配置数据;
load() 的参数可以是配置文件路径、classpath、内存数据,默认是以ASCII编码来读写数据,在实际使用中需要使用load(reader)来将编码格式转为UTF8
多次调用load(),后读取的key-value会覆盖先读取的key-value
由于Properties派生自Hashtable,因此继承了Hashtable的get、set方法,不要使用从Hashtable继承的方法,使用getProperty()和setProperty()来读取和修改配置数据,store()则是写入配置文件方法
IO
常用的接口
java.io
java.io
以字节byte为单位
InputStream
OutStream
以字符char为单位
Reader
Writer
File对象
构造File对象
使用相对路径 File f = new File("./dir/file.txt");
使用绝对路径 File f = new File("/var/www/dir/file.txt");
File既可以表示文件,也可以表示目录
文件路径的表达方式
getPath() 返回构造方法传入的路径
getAbsolutePath() 返回绝对路径
getCanonicalPath() 返回规范路径,规范路径就是将路径中的.或..变为完整的真实文件夹名称
当前系统分隔符 File.separator
常用方法
boolean isFile() 判断文件是否存在
boolean isDirectory() 判断目录是否存在
boolean canRead() 文件或目录是否可读
boolean canWrite() 文件或目录是否可写
boolean canExecute() 文件或目录是否可执行,对于目录来说,可执行表示是否可以列出该目录下的子目录和文件
long length() 文件字节数
boolean createNewFile() 创建新文件
boolean delete() 删除文件
File createTempFile() 创建临时文件
deleteOnExit() JVM退出时自动删除临时文件
File[] list() 遍历子目录及文件
File[] listFiles() 遍历子目录及文件,与list()不同的是,该方法提供了一系列重载方法,可以过滤不想要的文件和目录
boolean mkdir() 创建当前File对象的目录
boolean mkdirs() 创建当前File对象的目录,如果父目录不存在则也会自动创建
Path对象
存在于java.nio.file包
功能与File相似
如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便
InputStream 抽象类
方法
int read()
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了
这个方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了
子类
FileInputStream 文件输入流
ByteArrayInputStream 将byte[]转为InputStream
注意事项
无论是输入流还是输出流,使用完后一定要记得使用close()方法关闭流
所有与IO操作相关的代码都必须正确处理IOException
需要用try ... finally来保证流操作在无论是否发生IO错误的时候都能够正确地关闭
建议利用Java 7引入的新的try(resource)的语法,只需要编写try语句,让编译器自动为我们关闭资源
OutputStream 抽象类
方法
void write() 将流内容写入到缓冲区,缓冲区满后会自动调用flush()方法将内容写入到目标介质中
void flush() 强制输出缓冲区内容
子类
FileOutputStream
ByteArrayOutputStream
Filter模式(装饰器模式)
Java的IO标准库使用Filter模式为InputStream和OutputStream增加功能
可以把一个InputStream和任意个FilterInputStream组合
可以把一个OutputStream和任意个FilterOutputStream组合
Filter模式可以在运行期动态增加功能(又称Decorator模式)
classpath
Class对象的getResourceAsStream()可以从classpath中读取指定资源
根据classpath读取资源时,需要检查返回的InputStream是否为null
Serializable接口
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组
可序列化的Java对象必须实现java.io.Serializable接口,类似Serializable这样的空接口被称为“标记接口”(Marker Interface)
反序列化时不调用构造方法,可设置serialVersionUID作为版本号(非必需)
Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON
Java本身提供的基于对象的序列化和反序列化机制存在安全性问题,也存在兼容性问题,更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息
Reader接口
java.io.Reader是所有字符输入流的超类
常用方法
int read(),读取字符流的下一个字符
int read(char[]) 设置一个缓冲区,一次性将若干字符读取到缓冲数组中,返回值为实际读取的字符长度,最大不超过char[]的长度
实现类
FileReader
打开文件并获取Reader
建议使用try(...)语句来保证文件在IO异常时可以正常关闭
创建FileReader时需要制定编码来避免乱码问题
CharArrayReader
把一个char[]数组变成一个Reader
可在内存中模拟一个Reader,便于测试
StringReader
把一个String变成一个Reader
InputStreamReader
把InputStream转为Reader
Reader read = new InputStreamReader(new FileInputStream(...), "UTF-8")
Writer 接口
方法
void write(char c ) 写入单个字符
void write(char[] c) 写入字符数组
void write(String s) 写入String表示的所有字符
实现类
CharArrayWriter
可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组
可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组
StringWriter
也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口
也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口
OutputStreamWriter
是一个将任意的OutputStream转换为Writer的转换器
是一个将任意的OutputStream转换为Writer的转换器
Files及Paths工具类
存在于java.io包中,提供大量方便IO操作的静态方法
适用于小文件的简便操作
日期与时间
时区表示形式
GMT/UTC
GMT和UTC基本上是等价的,UTC更精确一些
以伦敦为0时区,其它时区需要添加偏移时区,例如:UTC+08:00表示东8区,UTC-02:00表示西2区
CST
既可以表示中国标准时间,也可以表示美国中部时间,容易混淆,不建议使用该表示方式
洲/城市
例如:Asia/Shanghai
夏令时
计算夏令时请使用标准库提供的相关类,不要试图自己计算夏令时
本地化
通常使用Locale表示一个国家或地区的日期、时间、数字、货币等格式
Locale由语言_国家的字母缩写构成,例如,zh_CN表示中文+中国,en_US表示英文+美国。语言使用小写,国家使用大写
Epoch Time 时间戳
表示从从1970年1月1日零点GMT时区到该时刻一共经历了多少秒
Java中通常使用long来存储时间戳,共有13位,前10位表示秒数,后三位表示毫秒数
获取当前时间错的常用方法:System.currentTimeMillis()
标准库API
旧API库 java.util
Date
常用方法
getYear() 获取当前年份,需要加上1900
getMonth() 获取当前月份,返回0~11,需要加上1
getDate() 获取当前日期,返回1~31
toString() 返回String字符串
toGMTString() 返回GMT时区字符串
toLocaleString() 返回本地时区字符串
java.text.SimpleDateFormat 控制输出格式
yyyy 年份
MM 月
dd 日
HH 小时
mm 分钟
ss 秒
示例:
Date date = new Date();
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dt = sdf.format(date);
Date date = new Date();
var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dt = sdf.format(date);
Date存在的问题
不能转换时区,除了toGMTString()可以按GMT+0:00输出外,Date总是以当前计算机系统的默认时区为基础进行输出
很难对日期和时间进行加减,计算两个日期相差多少天,计算某个月第一个星期一的日期等
Calender
用于获取并设置年、月、日、时、分、秒,它和Date比,主要多了一个可以做简单的日期和时间运算的功能
方法
get(int field) 获取年月日时分秒等信息,field表示字段类型,取值为:
Calendar.YEAR 年份
Calendar.MONTH 月份
Calendar.DAY_OF_MONTH 日期
Calendar.DAY_OF_WEEK 周几,返回的星期要特别注意,1~7分别表示周日,周一,……,周六
Calendar.HOUR_OF_DAY 小时
Calendar.MINUTE 分
Calendar.SECOND 秒
Calendar.MILLISECOND 毫秒
getTime()可以将一个Calendar对象转换成Date对象,然后就可以用SimpleDateFormat进行格式化了
getInstance() 获取Calender实例,Calender只能通过该方法创建实例
转换时区,需要借助TimeZone对象完成,本质上时区转换只能通过SimpleDateFormat在显示的时候完成,步骤如下:
1. 清除所有字段
2. 设定指定时区
3. 设定日期和时间
4. 创建SimpleDateFormat并设定目标时区
5. 格式化获取的Date对象(注意Date对象无时区信息,时区信息存储在SimpleDateFormat中)
TimeZone
时区对象
方法
getTimeZone(String zoneId) 获取指定时区对象,zoneId可以是GTM+09:00或Asia/Shanghai等格式
新API库 java.time
LocalDateTime / LocalDate / LocalTime
方法
now() 静态方法,获取当前时间,返回LocalDateTime实例
toLocalDate() 实例方法,将LocalDateTime对象转为LocalDate对象
toLocalTime() 实例方法,将LocalDateTime对象转为LocalTime对象
of(int year, int month, int day, int hour, int minute, int second) 静态方法,通过指定的日期和时间创建LocalDateTime
parse(String str) 静态方法,将字符串转换为LocalDateTime实例,参数str格式要求如下:
日期:yyyy-MM-dd
时间:HH:mm:ss
带毫秒的时间:HH:mm:ss.SSS
日期和时间:yyyy-MM-dd'T'HH:mm:ss
带毫秒的日期和时间:yyyy-MM-dd'T'HH:mm:ss.SSS
日期:yyyy-MM-dd
时间:HH:mm:ss
带毫秒的时间:HH:mm:ss.SSS
日期和时间:yyyy-MM-dd'T'HH:mm:ss
带毫秒的日期和时间:yyyy-MM-dd'T'HH:mm:ss.SSS
LocalDateTime plusYears(int n) / plusMonths(int n) / plusDays(int n) / ... 对年/月/日等时间单位进行加操作
LocalDateTime minusYears(int n) / minusMonths(int n) / minusDays(int n) / ... 对年/月/日等时间单位进行减操作
LocalDateTime withXxx(int n) 表示对指定时间单位进行调整,例如:withMonth(9) 将月份调整为9月
boolean isBefore(LocalDateTime ldt) / isAfter(LocalDateTime ldt) 比较两个LocalDateTime实例的先后
Duration和Period
Duration 表示两个时刻时间的间隔
Period 表示两个日期间的间隔
以P...T...形式表示日期和时间的间隔,P后面的代表日期间隔,T后面的代表时间间隔
仅表示本地的时间和日期,不带有时区
ZonedDateTime
带有时区的日期和时间,可以理解为LocalDateTime + ZoneId
方法
ZonedDateTime.now() 返回当前时间,时区默认
ZonedDateTime.now(ZoneId.of("Asia/Shanghai")) 返回当前时间,指定时区
LocalDateTime实例的atZone()方法也可以创建ZonedDateTime,例如:ldt.atZone(ZoneId.of("Asia/Shanghai"))
withZoneSameInstant(ZoneId) 时区转换
toLocalDateTime() 将ZonedDateTime转换为LocalDateTime
同样提供了plusXxxs和minusXxxs方法来进行日期时间的加减运算
DateTimeFormatter
存在于java.time.format包中,是线程安全的
自定义输出的格式,或者要把一个非ISO 8601格式的字符串解析成LocalDateTime
自定义输出格式
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dtf.format(LocalDateTime.now()));
用自定义格式解析
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
System.out.println(dt2);
LocalDateTime dt2 = LocalDateTime.parse("2019/11/30 15:16:17", dtf);
System.out.println(dt2);
在创建DateTimeFormatter实例时,可同时指定Locale来设置当地使用习惯,后续的输出将依照当地使用习惯来格式化输出结果,Locale存在于java.util.Locale中
Instant
用于表示系统时间戳的类型
方法
long getEpochSecond() 获取以秒表示的时间戳
long getEpochMilli() 获取以毫秒表示的时间戳
ZonedDateTime atZone(ZoneId) 为时间戳添加时区获得ZonedDateTime实例
修正了旧版API中存在的问题
Month的范围用1~12表示1月到12月
Week的范围用1~7表示周一到周日
单元测试
JUnit
项目中引入JUnit:Project - Properties - Java Build Path - Libraries中添加JUnit
测试方法需要添加@Test注解
方法
assertEquals()
assertTrue()
assertFalse()
assertNotNull()
assertArrayEquals()
assertThrows() 测试捕获指定的异常
Fixture
@BeforeEach 方法用于每个@Test执行前初始化测试资源
@AfterEach 方法用于每个@Test执行后清理测试资源
@BeforeAll 用于初始化静态资源,仅会执行一次
@AfterAll 用户清理静态资源,仅会执行一次
条件注解
@Disabled 禁止运行注解的测试方法
@EnabledOnOs 限定测试方法运行的操作系统,例如:@EnabledOnOs(OS.WINDOWS) / @EnabledOnOs({ OS.LINUX, OS.MAC })
@DisabledOnOs 禁止在指定的操作系统上运行测试方法
@DisabledOnJre 限制测试方法运行的Java最低版本,例如:@DisabledOnJre(JRE.JAVA_8)
@EnabledIfSystemProperty 根据指定系统条件执行测试方法,例如:@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*") 表示只能在64位系统运行测试方法
@EnabledIfEnvironmentVariable 根据指定环境变量执行测试方法,例如:@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true") 需要传入环境变量DEBUG=true才能执行的测试
多线程
基础知识
一个进程可以包含一个或多个线程,但至少会有一个线程
实现多任务的模式
多进程(每个进程中只有一个线程)
单进程多线程
多进程多线程
进程 VS 线程
创建进程比创建线程开销大,尤其是在Windows系统上
进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程
多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃
一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,又可以启动多个线程
一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。
线程状态
New:新创建的线程,尚未执行
Runnable:运行中的线程,正在执行run()方法的Java代码
Blocked:运行中的线程,因为某些操作被阻塞而挂起
Waiting:运行中的线程,因为某些操作在等待中
Timed Waiting:运行中的线程,因为执行sleep()方法正在计时等待
Terminated:线程已终止,因为run()方法执行完毕
创建新线程的方式
继承Thread类,覆写run()方法
创新新线程时传入一个实现了Runnable接口的实例
从java8开始引入了lambda,可以使用函数式来创建新线程
方法
sleep(int s) 静态方法,暂停线程执行,参数为毫秒数
setPriority(int t) 静态方法,设置线程优先级,取值范围1-10,默认值为5
start() 实例方法,启动线程
join(long s) 实例方法,其它线程调用指定线程t.join()后,发起调用的线程将等待实例线程t执行完后再继续执行,参数可选,表示发起调用线程的等待毫秒数,超过这个时间则发起调用线程继续执行
interrupt() 实例方法,线程实例调用本方法后,向线程提交一个中断请求,线程内部需要不断的调用isInterrupted()方法来判断当前线程是否被中断,如果发现中断请求则立即结束执行
中断线程
通过调用interrupt()实现中断
通过设置线程间共享变量实现中断,定义线程共享变量需要使用volatile关键字
守护线程
守护线程是为其他线程服务的线程
所有非守护线程都执行完毕后,虚拟机退出,但守护线程会继续执行
守护线程不能持有需要关闭的资源(如打开文件等)
创建守护线程
Thread t = new Thread();
t.setDaemon(t);
t.start();
Thread t = new Thread();
t.setDaemon(t);
t.start();
线程同步
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码
synchronized(Object lock) { ... }用于给代码块加锁,执行完后会自动解锁,加锁与解锁之间的代码块成为临界区
使用synchronized的时候,不必担心抛出异常。因为无论是否有异常,都会在synchronized结束处正确释放锁
JVM中规范了几种原子操作,进行这些操作时不需要同步
但由多行原子操作组成的代码块是需要同步的
但由多行原子操作组成的代码块是需要同步的
单个基本类型(double和long除外)赋值,例如:int n = m;
单个引用类型赋值,例如:List<String> lst = otherList;
可通过一些技巧将非原子操作转换为原子操作,示例如下:
class Pair {
int[] pair;
public void set(int first, int last) {
// 方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步
int[] ps = new int[] { first, last };
// 原子操作,不需要同步
this.pair = ps;
}
}
class Pair {
int[] pair;
public void set(int first, int last) {
// 方法内部定义的局部变量,每个线程都会有各自的局部变量,互不影响,并且互不可见,并不需要同步
int[] ps = new int[] { first, last };
// 原子操作,不需要同步
this.pair = ps;
}
}
volatile VS 线程同步
volatile只能保证线程共享变量的可见性,不能保证操作的原子性
线程同步则既保证可见性,又保证操作的原子性
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe)
常见的线程安全类型
StringBuffer
不变类 Stirng/Integer/LocalDate等,由于创建后只能读不能修改,所以是线程安全的
类似于Math只提供静态方法,没有成员变量,也是线程安全的
一个类没有特殊说明的话,默认都是非线程安全的
如果需要加锁的是类的实例,则可以使用synchronized来修饰这个方法,示例:
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
用synchronized修饰静态方法时,则表示将该类的Class对象作为锁对象了
线程死锁
Java线程锁是可重入锁,即JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁
死锁就是两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成了双方无限等待下去
应该尽量避免死锁的发生,原则是:线程获取锁的顺序要一致
wait和notify
wait() 只能被锁对象调用,表示释放当前线程获取到的锁对象,同时当前线程进入等待状态,等待被再次唤醒
调用wait()后该方法并不直接返回,而是进入等待notify()唤醒的状态。
notify()及notifyAll() 只能被锁对象调用,调用后将会唤醒单个或全部由于该锁对象而进入等待状态的线程
通常来说notifyAll()比notify()更安全,可以避免部分线程永远等待下去而不被唤醒
被唤醒的线程只有获取到锁后才能继续执行,没有获取到锁的线程将继续处于等待状态
如果多个线程被同时唤醒,其中只有一个线程能获取到锁对象,其余线程继续进入等待状态
由操作系统来决定由哪个线程获取到锁对象,具有一定的随机性
高级并发处理包 java.util.concurrent
使用synchronized加锁,这种锁一是很重,二是未获取到锁的线程会一直等待,而没有主动尝试获取锁的机会
ReentrantLock (java.util.concurrent.locks)
ReentrantLock可以替代synchronized进行同步
ReentrantLock获取锁更安全
必须先获取到锁,再进入try {...}代码块,最后使用finally保证释放锁
可以使用tryLock()尝试获取锁
Condition
可以替代wait和notify
await()会释放当前锁,进入等待状态
signal()会唤醒某个等待线程
signalAll()会唤醒所有等待线程
唤醒线程从await()返回后需要重新获得锁
await()可以在等待指定时间后,如果还没有被其他线程通过signal()或signalAll()唤醒,可以自己醒来
ReadWriteLock
悲观读锁,即读的过程中不允许写入
只允许一个线程写入
允许多个线程在没有写入时同时读取
适合读多写少的场景
StampedLock
乐观读锁,即读的过程中也允许写入
是不可重入锁
进一步提升了读取效率
获取读锁时会返回当前版本号,返回前需要判断版本号是否发生变动,如果发生变动则需要重新获取数据后再返回
线程安全的并发集合类
CopyOnWriteArrayList
ConcurrentHashMap
CopyOnWriteArraySet
ArrayBlockingQueue / LinkedBlockingQueue
LinkedBlockingDeque
Atomic
存在于java.util.concurrent.atomic包中
提供了一组原子操作的封装类
AtomicIneger
int addAndGet(int delta)
int get()
int incrementAndGet()
int compareAndSet(int expect, int update)
AtomicLong
CAS (Compare And Set)
在一次操作中,如果当前值是prev,那么就更新为next,返回true。如果当前值不是prev,就什么也不干,返回false
线程池
Java标准库提供了ExecutorService接口表示线程池
Java标准库提供的几个常用实现类
FixedThreadPool:线程数固定的线程池
CachedThreadPool:线程数根据任务动态调整的线程池
SingleThreadExecutor:仅单线程执行的线程池
ScheduledThreadPool:重复执行线程的线程池
Future接口
Runable 接口没有返回值
Callable 接口可以返回执行类型的值
一个线程池提交Callable任务后,会得到一个Future对象
一个Future<V>接口表示一个未来可能会返回的结果
方法
get():获取结果(可能会等待)
get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间
cancel(boolean mayInterruptIfRunning):取消当前任务
isDone():判断任务是否已完成
使用Future获得异步执行结果时,要么调用阻塞方法get(),要么轮询看isDone()是否为true
CompletableFuture
针对Future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法
异步任务结束时,会自动回调某个对象的方法
异步任务出错时,会自动回调某个对象的方法
主线程设置好回调后,不再关心异步任务的执行
多个CompletableFuture可以串行或并行执行
方法
thenAccept()处理正常结果
exceptional()处理异常结果
thenApplyAsync()用于串行化另一个CompletableFuture
anyOf()和allOf()用于并行化多个CompletableFuture
Fork/Join
原理:判断一个任务是否足够小,如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务
ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction
ThreadLocal
表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的
ThreadLocal实例通常总是以静态字段初始化
特别注意ThreadLocal在线程关闭前一定要清除,否则本线程返回线程池后,可能会对下次线程调用产生影响
为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try (resource) {...}结构,让编译器自动为我们关闭。
可以将ThreadLocal封装成一个实现了AutoCloseable接口的类,然后通过try(resource) { ... } 创建该类的实例,执行完毕后系统会自动清除ThreadLocal。
可以将ThreadLocal封装成一个实现了AutoCloseable接口的类,然后通过try(resource) { ... } 创建该类的实例,执行完毕后系统会自动清除ThreadLocal。
Maven
依赖管理
依赖关系
compile 编译时需要用到该jar包(默认),这种类型的依赖会被直接放到classpath中
test 编译Test时需要用到该jar包
runtime 编译时不需要,但运行时需要用到
provided 编译时需要用到,但运行时由JDK或某个服务器提供
Maven只需要3个变量即可唯一确定某个jar包
groupId:属于组织的名称,类似Java的包名
artifactId:该jar包自身的名称,类似Java的类名
version:该jar包的版本
某个jar包一旦被Maven下载过,即可永久地安全缓存在本地。只有以-SNAPSHOT结尾的版本号会被Maven视为开发版本,开发版本每次都会重复下载,这种SNAPSHOT版本只能用于内部私有的Maven repo,公开发布的版本不允许出现SNAPSHOT
配置国内镜像站
进入项目的.m2目录,创建settings.xml文件,配置内容如下:
<settings>
<mirrors>
<mirror>
<id>aliyun</id>
<name>aliyun</name>
<mirrorOf>central</mirrorOf>
<!-- 国内推荐阿里云的Maven镜像 -->
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>
</settings>
<settings>
<mirrors>
<mirror>
<id>aliyun</id>
<name>aliyun</name>
<mirrorOf>central</mirrorOf>
<!-- 国内推荐阿里云的Maven镜像 -->
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
</mirrors>
</settings>
Maven编译命令:mvn clean package
构建流程
Maven的生命周期
default 生命周期
validate
initialize
generate-sources
process-sources
generate-resources
process-resources
compile
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile
process-test-classes
test
prepare-package
package
pre-integration-test
integration-test
post-integration-test
verify
install
deploy
initialize
generate-sources
process-sources
generate-resources
process-resources
compile
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile
process-test-classes
test
prepare-package
package
pre-integration-test
integration-test
post-integration-test
verify
install
deploy
clean 生命周期
pre-clean
clean
post-clean
clean
post-clean
实际开发中常用的命令
mvn clean:清理所有生成的class和jar (仅执行clean生命周期)
mvn clean compile:先清理,再执行到compile(先执行clean生命周期再执行default生命周期,下同)
mvn clean test:先清理,再执行到test
mvn clean package:先清理,再执行到package
default生命周期中常用的phase
clean:清理
compile:编译
test:运行测试
package:打包
一个phase对应着一个或多个goal,可以把goal看做类中的方法,是实际执行操作的单元。
通常phase都是执行默认的goal即可,只有少数情况需要指明要执行哪个goal。
通常phase都是执行默认的goal即可,只有少数情况需要指明要执行哪个goal。
通常phase都是通过插件来实现的
模块管理
Maven 支持将一个大项目拆分为多个模块进行管理,拆分通过pom.xml进行,每个模块拥有自己的pom.xml文件,项目本身有一个总的pom.xml文件来记录模块pom.xml文件。
各模块pom.xml中重复的内容,可以提取出来放到parent.xml中,方便维护
网络编程
TCP编程
是一种基于流的协议
可用的端口方范围:0~65535
使用Socket模型,需要先创建服务器端与客户端的连接
服务器端使用ServerSocket监听指定端口
客户端使用Socket(InetAddress, port)连接服务器
服务端使用accept()方法接收连接并返回Socket对象
两端通过Socket打开InputStream/OutStream进行数据读写
服务端通常通过多线程来同时处理多个客户端的访问,利用线程池会提高处理效率
OutStream的flush()方法用于将缓冲区强制输出到网络
UDP编程
数据传递是通过数据包,不是通过流来实现的
可用端口范围:0~65535,与TCP协议端口是相互独立的,因此TPC协议占用的某端口,并不影响UDP协议同时使用本端口
数据传递由客户端发起,不需要建立与服务器的连接,直接将数据包发送到指定IP及端口的服务器端即可
服务器端接口到客户端发送过来的数据包后,需要立即返回信息,表示访问已收到
DatagramSocket用于发送和接口数据包,没有IO流接口,数据被直接写入byte[]缓冲区
DatagramPacket 用于生成数据包
Http编程
Http是基于TCP的请求-响应式协议
Http请求的格式是固定的,分为:Http Header和Http Body
早期JDK通过HttpURLConnection访问HTTP,先已被HttpClient替代
HttpClient使用链式调用并通过内置的BodyPublishers和BodyHandlers来更方便地处理数据,其内部使用线程池优化HTTP连接
RMI远程调用
RMI通过自动生成stub和skeleton实现网络调用,客户端只需要查找服务并获得接口实例,服务器端只需要编写实现类并注册为服务
RMI的序列化和反序列化可能会造成安全漏洞,因此调用双方必须是内网互相信任的机器,不要把通讯端口暴露在公网上作为对外服务
JDBC编程
java.sql 提供JDBC接口
JDBC驱动就是JDBC接口对于不同数据库的实现类
在Maven中添加JDBC驱动时,要将scope设置为runtime,不要设置为compile
JDBC连接
Connection代表一个JDBC连接,它相当于Java程序到数据库的连接(通常是TCP连接)。打开一个Connection时,需要准备URL、用户名和口令,才能成功连接到数据库。
URL格式(以MySQL为例)
jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2
jdbc:mysql://<hostname>:<port>/<db>?key1=value1&key2=value2
创建Connection
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
...
}
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
...
}
JDBC查询
执行查询分三步
第一步,通过Connection提供的createStatement()方法创建一个Statement对象,用于执行一个查询;
第二步,执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集;
第三步,反复调用ResultSet的next()方法并读取每一行结果。
注意事项
Statment和ResultSet都是需要关闭的资源,因此嵌套使用try (resource)确保及时关闭
rs.next()用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet时当前行不是第一行);
ResultSet获取列时,索引从1开始而不是0;
必须根据SELECT的列的对应位置来调用getLong(1),getString(2)这些方法,否则对应位置的数据类型不对,将报错。
查询返回的结构总是ResultSet类型的
注入攻击
使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码!
PreparedStatement可以通过Connection的实例方法prepareStatement()来获取
java.sql.Types定义了一组常量来表示与SQL数据类型的映射
BIT, BOOL -> boolean
INTEGER -> int
BIGINT -> long
REAL -> float
FLOAT, DOUBLE -> double
CHAR, VARCHAR -> String
DECIMAL -> BigDecimal
DATE -> java.sql.Date, LocalDate
TIME -> java.sql.Time,LocalTime
INTEGER -> int
BIGINT -> long
REAL -> float
FLOAT, DOUBLE -> double
CHAR, VARCHAR -> String
DECIMAL -> BigDecimal
DATE -> java.sql.Date, LocalDate
TIME -> java.sql.Time,LocalTime
通过PreparedStatement的executeQuery()方法来执行查询
JDBC增删改操作
通过PreparedStatement的executeUpdate()方法来执行增删改
如果要获取新增记录的自增ID,需要在创建PreparedStatement时第二个参数必须传入常量Statement.RETURN_GENERATED_KEYS
执行executeUpdate()方法后,必须调用getGeneratedKeys()获取一个ResultSet对象,这个对象包含了数据库自动生成的主键的值
执行executeUpdate()方法后,必须调用getGeneratedKeys()获取一个ResultSet对象,这个对象包含了数据库自动生成的主键的值
JDBC事务
ACID
Atomicity:原子性
Consistency:一致性
Isolation:隔离性
Durability:持久性
事务定义了四个隔离级别
Read Uncommitted
Read Committed
Repeatable Read
Serializable
方法
setAutoCommit(boolean b) 实例方法,用于关闭/开启SQL操作的自动提交
commit() 实例方法,提交一个或多个SQL操作
rollback() 实例方法,回滚SQL操作
setTransactionIsolation() 实例方法,设置事务的隔离级别
JDBC Batch
把同一个SQL但参数不同的若干次操作合并为一个批量执行
方法
addBatch() 实例方法,将一组SQL操作参数加入到批量操作中
int[] executeBatch() 实例方法,执行批量操作
JDBC 连接池
常用的连接池
HikariCP
C3P0
BoneCP
Druid
数据库连接池是一种复用Connection的组件,它可以避免反复创建新连接,提高JDBC代码的运行效率
可以配置连接池的详细参数并监控连接池
Lambda编程
也称函数式编程,是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。
单方法接口被称为FunctionalInterface,此类接口使用@FunctionalInterface注解
接收FunctionalInterface作为参数的时候,可以把实例化的匿名类改写为Lambda表达式,能大大简化代码
Lambda表达式的参数和返回值均可由编译器自动推断
FunctionalInterface允许传入
接口的实现类(传统写法,代码较繁琐)
Lambda表达式(只需列出参数名,由编译器推断类型)
符合方法签名的静态方法
符合方法签名的实例方法(实例类型被看做第一个参数类型)
符合方法签名的构造方法(实例类型被看做返回类型)
FunctionalInterface不强制继承关系,不需要方法名称相同,只要求方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同
流式API Stream
流式API
位于java.util.stream包中
Stream API提供了一套新的流式处理的抽象序列
Stream API支持函数式编程和链式操作
Stream可以表示无限序列,并且大多数情况下是惰性求值的
一个Stream转换为另一个Stream时,实际上只存储了转换规则,并没有任何计算发生,只有在最后调用一个求值方法时才会真正计算
创建Stream
方法
Stream.of() 静态方法,参数为一组可变参数,返回一个Stream实例
Arrays.stream() 静态方法,基于数组获取Stream
对于Collection(List、Set、Queue等),直接调用stream()方法就可以获得Stream
例如:List.of("X", "Y", "Z").stream()
例如:List.of("X", "Y", "Z").stream()
Stream.generate(Supplier<T> sp) 静态方法
会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列
会不断调用Supplier.get()方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列
File.lines() 静态方法,将一个文件内容转换为Stream,Stream中的每个元素就是文件中的一行内容
Stream st = Pattren.compile(String str).splitAsStream() 实例方法,将字符串按照正则式拆分为Stream
基本类型Stream
由于Java泛型不支持基本类型,所以无法使用类似Stream<int>类型,只能用Stream<Integer>来替代,但频繁的装箱拆箱会导致效率降低,因此Java标准库中提供了IntStream、LongStream和DoubleStream这三种使用基本类型的Stream,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率
map()
实例方法,将一个Stream通过一系列操作,转化为另一个Stream
接收的参数对象是一个Function接口对象,它定义了一个apply()方法,负责对每个元素进行转换操作
filter()
对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
接收的参数对象是Predicate接口对象,它定义了一个test()方法,负责判断元素是否符合条件
reduce()
是Stream的一个聚合方法,它可以把一个Stream的所有元素按照聚合函数聚合成一个结果
传入的参数对象是BinaryOperator接口,它定义了一个apply()方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果
如果不设置初始值,reduce()将返回一个Optional(<T>)对象
collect()
将Stream中的元素收集到一个指定的集合对象中
可接受的参数
Collectors.toSet()
Collectors.toList()
Collectors.toMap()
Collectors.groupingBy() 分组输出
toArray()
将Stream输出为数组
sorted()
对Stream元素进行操作,不会进行实际计算,只会返回Stream
distinct()
去重
skip()
跳过指定数量的元素
limit()
截取指定数量的元素
concat()
静态方法,将连个Stream合并成一个Stream
flatMap
是指把Stream的每个元素映射为Stream,然后合并成一个新的Stream
parallel()
将普通Stream转换为可并行处理的Stream,只要可能就会使用并行方式处理Stream
聚合操作
reduce(),collect(),count(),max(),min(),sum(),average()
测试操作
boolean allMatch(Predicate<? super T>):测试是否所有元素均满足测试条件
boolean anyMatch(Predicate<? super T>):测试是否至少有一个元素满足测试条件
设计模式
创建型模式
工厂模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类
工厂方法的目的是使得创建对象和使用对象是分离的,并且客户端总是引用抽象工厂和抽象产品
工厂方法可以隐藏创建产品的细节,且不一定每次都会真正创建产品,完全可以返回缓存的产品,从而提升速度并减少内存消耗
总是引用接口而非实现类,能允许变换子类而不影响调用方,即尽可能面向抽象编程
实际更常用的是更简单的静态工厂方法,它允许工厂内部对创建产品进行优化
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
抽象工厂模式是为了让创建工厂和一组产品与使用相分离,并可以随时切换到另一个工厂以及另一组产品
抽象工厂模式实现的关键点是定义工厂接口和产品接口,但如何实现工厂与产品本身需要留给具体的子类实现,客户端只和抽象工厂与抽象产品打交道
原型模式
根据一个现有对象实例复制出一个新的实例,复制出的类型和属性与原实例相同
Object提供的clone()方法虽然也可以复制实例,但过程较为繁琐
更好的方式是创建copy()方法,明确返回类型
原型模式应用不是很广泛,因为很多实例会持有类似文件、Socket这样的资源,而这些资源是无法复制给另一个对象共享的,只有存储简单类型的“值”对象可以复制
单例模式
是为了保证一个程序的运行期间,某个类有且只有一个全局唯一实例
既可以严格实现,也可以以约定的方式把普通类视作单例
两种实现方式
常规单例模式
只有private构造方法,确保外部无法实例化
通过private static变量持有唯一实例,保证全局唯一性
通过public static方法返回此唯一实例,使外部调用方能获取到实例
通过enum实现单例
Java保证枚举类的每个枚举都是单例,只需要编写一个只有一个枚举的类即可
通过enum定义单例,示例:
public enum World {
// 唯一枚举:
INSTANCE;
private String name = "world";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
public enum World {
// 唯一枚举:
INSTANCE;
private String name = "world";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
引用enum的单例
String name = World.INSTANCE.getName();
String name = World.INSTANCE.getName();
构造型模式
适配器模式 Adapter
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
Adapter模式可以将一个A接口转换为B接口,使得新的对象符合B接口规范
编写Adapter实际上就是编写一个实现了B接口,并且内部持有A接口的类:
public BAdapter implements B {
private A a;
public BAdapter(A a) {
this.a = a;
}
public void b() {
a.a();
}
}
public BAdapter implements B {
private A a;
public BAdapter(A a) {
this.a = a;
}
public void b() {
a.a();
}
}
在Adapter内部将B接口的调用“转换”为对A接口的调用
只有A、B接口均为抽象接口时,才能非常简单地实现Adapter模式
桥接模式
桥接模式通过分离一个抽象接口和它的实现部分,使得设计可以按两个维度独立扩展
组合模式
Composite模式使得叶子对象和容器对象具有一致性,从而形成统一的树形结构,并用一致的方式去处理它们
装饰器模式
是一种在运行期动态给某个对象的实例增加功能的方法
使用Decorator模式,可以独立增加核心功能,也可以独立增加附加功能,二者互不影响
外观模式
Facade模式是为了给客户端提供一个统一入口,并对外屏蔽内部子系统的调用细节
RestApi就是外观模式的一个具体应用
享元模式(Flyweight)
如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度
享元模式的设计思想是尽量复用已创建的对象,常用于工厂方法内部的优化
代理模式(Proxy)
代理模式通过封装一个已有接口,并向调用方返回相同的接口类型,能让调用方在不改变任何代码的前提下增强某些功能(例如,鉴权、延迟加载、连接池复用等)
使用Proxy模式要求调用方持有接口,作为Proxy的类也必须实现相同的接口类型
行为型模式
责任链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
责任链模式是一种把多个处理器组合在一起,依次处理请求的模式
责任链模式的好处是添加新的处理器或者重新排列处理器非常容易
责任链模式经常用在拦截、预处理请求等
命令模式
命令模式的设计思想是把命令的创建和执行分离,使得调用者无需关心具体的执行过程
通过封装Command对象,命令模式可以保存已执行的命令,从而支持撤销、重做等操作
解释器模式
解释器模式通过抽象语法树实现对用户输入的解释执行
解释器模式的实现通常非常复杂,且一般只能解决一类特定问题,例如:正则表达式
迭代器模式
Iterator模式常用于遍历集合,它允许集合提供一个统一的Iterator接口来遍历元素,同时保证调用者对集合内部的数据结构一无所知,从而使得调用者总是以相同的接口遍历各种不同类型的集合
实现Iterator模式的关键是返回一个Iterator对象,该对象知道集合的内部结构
中介/调停者模式
用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互
中介模式是通过引入一个中介对象,把多边关系变成多个双边关系,从而简化系统组件的交互耦合度
备忘录模式
备忘录模式是为了保存对象的内部状态,并在将来恢复,大多数软件提供的保存、打开,以及编辑过程中的Undo、Redo都是备忘录模式的应用
观察者模式
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
观察者模式,又称发布-订阅模式,是一种一对多的通知机制,使得双方无需关心对方,只关心通知本身
状态模式
状态模式的设计思想是把不同状态的逻辑分离到不同的状态类中,从而使得增加新状态更容易
状态模式的实现关键在于状态转换。简单的状态转换可以直接由调用方指定,复杂的状态转换可以在内部根据条件触发完成
策略模式
策略模式是为了允许调用方选择一个算法,从而通过不同策略实现不同的计算结果,例如:会员折扣策略
通过扩展策略,不必修改主逻辑,即可获得新策略的结果
模板模式
模板方法是一种高层定义骨架,底层实现细节的设计模式,适用于流程固定,但某些步骤不确定或可替换的情况
父类定义骨架,子类实现某些细节
访问者模式
访问者模式是为了抽象出作用于一组复杂对象的操作,并且后续可以新增操作而不必对现有的对象结构做任何改动
Web开发
Servlet
J2EE提供了Servlet API,用于处理TCP连接,解析HTTP协议这些底层工作
一个基础的Servlet包括
注解:@WebServlet(urlPatterns = "/")
Servlet总是继承自HttpServlet,然后覆写doGet()或doPost()方法
doGet/doPost方法传入了HttpServletRequest和HttpServletResponse两个对象
还需要在工程目录下创建一个web.xml描述文件,放到src/main/webapp/WEB-INF目录下(固定目录结构,不要修改路径,注意大小写)。文件内容可以固定如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
Servlet打包文件类型为war(Java Web Application Archive)
Tomcat之类的Web服务器也称为Servlet容器
Servlet的特点
无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例
Servlet容器只会给每个Servlet类创建唯一实例
Servlet容器会使用多线程执行doGet()或doPost()方法
在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全
HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题
在doGet()或doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用
开发Servlet时,推荐使用main()方法启动嵌入式Tomcat服务器并加载当前工程的webapp,便于开发调试,且不影响打包部署,能极大地提升开发效率
HttpServletRequest
封装了一个HTTP请求,它实际上是从ServletRequest继承而来
常用方法
getMethod():返回请求方法,例如,"GET","POST";
getRequestURI():返回请求路径,但不包括请求参数,例如,"/hello";
getQueryString():返回请求参数,例如,"name=Bob&a=1&b=2";
getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
getContentType():获取请求Body的类型,例如,"application/x-www-form-urlencoded";
getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串"";
getCookies():返回请求携带的所有Cookie;
getHeader(name):获取指定的Header,对Header名称不区分大小写;
getHeaderNames():返回所有Header名称;
getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
getReader():和getInputStream()类似,但打开的是Reader;
getRemoteAddr():返回客户端的IP地址;
getScheme():返回协议类型,例如,"http","https";
HttpServletResponse
封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。
常用方法
setStatus(sc):设置响应代码,默认是200;
setContentType(type):设置Body的类型,例如,"text/html";
setCharacterEncoding(charset):设置字符编码,例如,"UTF-8";
setHeader(name, value):设置一个Header的值;
addCookie(cookie):给响应添加一个Cookie;
addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header
流写入完毕后,必须调用flush()方法,将缓存中的数据发送出去
由于目前的Web服务器都基于Http/1.1协议,会复用TCP连接,因此在发送数据后不要调用close()方法,这会导致无法复用TCP连接
Filter
Filter是一种对HTTP请求进行预处理的组件,它可以构成一个处理链,使得公共处理代码能集中到一起
Filter适用于日志、登录检查、全局设置等
设计合理的URL映射可以让Filter链更清晰
0 条评论
下一页