Java
💡 【 Learn Java!】
Java程序基础
基础概念
整数运算
- 自增/自减(++/- -)
n++
先引用n再加1++n
先加1再引用n
- 移位运算
- 左移
(n<<m)
x2 右移(n>>m)
/2 - 无符号右移
>>>m
- 强制类型转换
(type)
(精度丢失)
- 左移
- 自增/自减(++/- -)
布尔运算
- 三元运算符:
n ? x : y
- 三元运算符:
字符和字符串
- 字符:
char''
基本数据类型 - 字符串
String""
引用类型 - 转义字符
- \n 换行
- \r 回车
- \t Tab
- 字符串拼接
- 单行:+
- 多行:
'''…'''
- 字符:
流程控制
输入和输出
格式化输出
1
2
3
4
5
6
7public class Main {
public static void main(String[] args) {
double d = 3.1415926;
System.out.printf("%.2f\n", d); // 显示两位小数3.14
System.out.printf("%.4f\n", d); // 显示4位小数3.1416
}
}- 占位符
- 占位符对应传入参数,参数类型要和占位符一致
- %d 整数
- %f 浮点数
- %s 字符串
- %e 科学计数法
- %x 十六进制
- 占位符
输入
1
2
3
4
5
6
7
8
9
10
11
12import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建Scanner对象
System.out.print("Input your name: ");
String name = scanner.nextLine(); // 读取一行输入并获取字符串
System.out.print("Input your age: ");
int age = scanner.nextInt(); // 读取一行输入并获取整数
System.out.printf("Hi, %s, you are %d\n", name, age); // 格式化输出
}
}
**if**
- 浮点数判断相等:差值小于某个临界值
- 引用类型判断相等:
equals()
- 如果
s1
为null
,s1.equals(s2)
报错:NullPointerException
- 采用
&&
判断 或将判断值作为对象调用equals()
- 如果
**switch**
lambda
的应用1
2
3
4
5
6
7
8
9
10
11public class Main {
public static void main(String[] args) {
String fruit = "apple";
int opt = switch (fruit) {
case "apple" -> 1;
case "pear", "mango" -> 2;
default -> 0;
}; // 注意赋值语句要以;结束
System.out.println("opt = " + opt);
}
}用
yield
返回一个值作为switch
语句的返回值
数组操作
面向对象
面向对象基础
方法
可变参数
可变参数用
Type...
定义,相当于数组类型示例
1
2
3
4
5
6
7
8class Group {
private String[] names;
public void setNames(String... names) { //setNames(String[] names)
this.names = names;
}
}
//可变参数改写为String[]类型,需要先构造String[]
参数绑定
- 基本类型参数的传递:是调用方值的复制
- 引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象
构造方法
- 构造方法的名称是类名
方法重载
- 方法名相同,功能类似,参数不同,称为方法重载
Overload
返回值类型通常相同
- 方法名相同,功能类似,参数不同,称为方法重载
继承
**extends**
关键字来实现继承protected
- 子类无法访问父类的
private
字段或者private
方法 protected
修饰的字段可以被子类访问(以及子类的子类)
- 子类无法访问父类的
**super**
- 子类不会继承任何父类的构造方法
- 阻止继承
- 用
sealed
修饰class,并通过permits
明确写出能够从该class继承的子类名称
- 用
- 向上转型
upcasting
- 子类类型安全地变为父类类型的赋值
- 向下转型
downcasting
instanceof
判断一个变量所指向的实例是否是指定类型及其子类
- 组合
- 继承是
is
关系,组合是has
关系
- 继承是
多态
Polymorphic
:实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型- 如不同
income
的tax
方法
- 如不同
- 继承可以允许子类
Override
父类的方法final
修饰的方法不能被覆写final
修饰的类不能被继承final
修饰的field在初始化后不能被修改final
修饰局部变量不能被重新赋值
抽象类
abstract class
只能用于被继承,由子类实现其定义的抽象方法
接口
一个具体的
class
去实现一个interface
时,需要使用implements
关键字一个类不能从多个类继承,只能继承自另一个,但一个类可以实现多个
interface
继承关系
接口比抽象类更抽象
实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它
1
2
3List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
default方法
- 抽象类的普通方法可以访问实例字段
interface
没有字段,default
方法无法访问字段
静态字段和静态方法
- 静态字段
- 静态字段为描述
class
本身的字段(非实例字段) - 所有实例共享一个静态字段
- 推荐用类名来访问静态字段
- 静态字段为描述
- 静态方法
- 静态方法属于
class
而不属于实例 - 用实例方法必须通过一个实例变量,而调用静态方法不需要实例变量,通过类名调用
- 静态方法属于
- 接口的静态字段
interface
是一个纯抽象类,所以它不能定义实例字段可以有静态字段的,并且静态字段必须为
final
类型interface
的字段只能是public static final
类型1
2
3
4
5public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
- 静态字段
包
- 如果有两个
class
名称相同,只能import
其中一个,另一个必须写完整类名 - 包名推荐使用倒置的域名
- 如果有两个
作用域
- 如果一个类内部还定义了嵌套类
nested class
,那么,嵌套类拥有访问private
的权限 - 包作用域:一个类允许访问同一个
package
- 没有
public
、private
修饰的类 - 没有
public
、protected
、private
修饰的字段和方法 - 同一个包,可以访问
package
权限的class
、field
和method
- 没有
- 局部变量
- 在方法内部定义的变量称为局部变量
- 局部变量作用域从变量声明处开始到对应的块结束
- 方法参数也是局部变量
- 尽可能缩小局部变量的作用域,尽可能延后声明局部变量
public
- 如果有
public
类,文件名必须和public
类的名字相同 - 一个
.java
文件只能包含一个public
类,但可以包含多个非public
类
- 如果有
- 如果一个类内部还定义了嵌套类
内部类
Inner Class
的实例不能单独存在,必须依附于一个Outer Class
的实例Outer.Inner inner = outer.**new** Inner();
Inner Class的作用域在Outer Class内部,所以能访问Outer Class的
private
字段和方法匿名类
Anonymous Class
asyncHello()
方法,我们在方法内部实例化了一个Runnable
Runnable
本身是接口,接口是不能实例化的,所以这里实际上是定义了一个实现了Runnable
接口的匿名类,并且通过new
实例化该匿名类,然后转型为Runnable
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class Main {
public static void main(String[] args) {
Outer outer = new Outer("Nested");
outer.asyncHello();
}
}
class Outer {
private String name;
Outer(String name) {
this.name = name;
}
void asyncHello() {
Runnable r = new Runnable() {
public void run() {
System.out.println("Hello, " + Outer.this.name);
}
};
new Thread(r).start();
}
}在定义匿名类的时候就必须实例化它,定义匿名类的写法如下
1
2
3Runnable r =new Runnable() {
// 实现必要的抽象方法...
};匿名类也完全可以继承自普通类
map1
是一个普通的HashMap
实例map2
是一个匿名类实例,只是该匿名类继承自HashMap
map3
也是一个继承自HashMap
的匿名类实例,并且添加了static
代码块初始化数据
1
2
3
4
5
6
7
8
9
10
11
12
13public class Main {
public static void main(String[] args) {
HashMap<String, String> map1 = new HashMap<>();
HashMap<String, String> map2 = new HashMap<>() {}; // 匿名类!
HashMap<String, String> map3 = new HashMap<>() {
{
put("A", "1");
put("B", "2");
}
};
System.out.println(map3.get("A"));
}
}
静态内部类
Static Nested Class
- 用
static
修饰的内部类和Inner Class有很大的不同,它不再依附于Outer
的实例,而是一个完全独立的类 - 因此无法引用
Outer.this
,但它可以访问Outer
的private
静态字段和静态方法 - 如果把
StaticNested
移到Outer
之外,就失去了访问private
的权限
- 用
Java核心类
字符串和编码
String
String
是一个引用类型,它本身也是一个class
- 可以直接用
"..."
这种字符串字面量表示方法 - 字符串不可变
字符串比较
- 使用
equals()
方法而不能用==
String
类还提供了多种方法来搜索子串、提取子串
- 使用
去除首尾空白字符
使用
trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
trim()
并没有改变字符串的内容,而是返回了一个新字符串
strip()
方法也可以移除字符串首尾空白字符- 和
trim()
不同的是,类似中文的空格字符\u3000
也会被移除
- 和
String
还提供了isEmpty()
和isBlank()
来判断字符串是否为空和空白字符串空和空白字符串
1
2
3
4"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换字串
- 根据字符或字符串替换
- 正则表达式
分割字符串
- 使用
split()
方法,并且传入的也是正则表达式
- 使用
拼接字符串
- 使用静态方法
join()
,它用指定的字符串连接字符串数组
- 使用静态方法
格式化字符串
formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串- 占位符
类型转换
- 任意基本类型或引用类型转换为字符串,使用静态方法
valueOf()
(重载方法) - 字符串转换为其他类型
- 任意基本类型或引用类型转换为字符串,使用静态方法
转换为char[]
String
和char[]
类型可以互相转换示例
1
2char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String如果修改了
char[]
数组,String
并不会改变
字符编码
**StringBuilder**
- 链式操作:类定义的
append()
方法会返回实例本身this
- 链式操作:类定义的
StringJoiner
- 用分隔符拼接数组
String
还提供了一个静态方法join()
包装类型
- 基本类型→引用类型 Auto Boxing + Auto unboxing
- 所有的包装类型都是不变类
- 实例的比较使用
equals
- 创建新对象时,优先选用静态工厂方法(
Integer.valueOf()
)而不是new操作符 - 进制转换:静态方法
parseInt()
可以把字符串解析成一个整数 - 整数和浮点数的包装类型都继承自
Number
JavaBean
- 一种符合命名规范的
class
,它通过getter
和setter
来定义属性 - 使用
Introspector.getBeanInfo()
可以获取属性列表
- 一种符合命名规范的
枚举类
- 使用
enum
定义枚举类型,编译器编译为final class Xxx extends Enum { … }
- 通过
name()
获取常量定义的字符串,不使用toString()
- 通过
ordinal()
返回常量定义的顺序 - 可以为
enum
编写构造方法、字段和方法enum
的构造方法要声明为private
,字段声明为final
enum
适合用在switch
语句中
- 使用
记录类
和
enum
类似,不能从Record
派生,只能通过record
关键字由编译器实现继承record Point(**int** x, **int** y) {}
把上述定义改写为class,相当于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32final class Point extends Record {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
public String toString() {
return String.format("Point[x=%s, y=%s]", x, y);
}
public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
}
/* 除了用final修饰class以及每个字段外
编译器还自动为我们创建了构造方法,和字段名同名的方法
以及覆写toString()、equals()和hashCode()方法
*/
构造方法
Point
的构造方法加上检查逻辑1
2
3
4
5
6
7public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}方法
public Point {...}
被称为常见的静态方法
of()
,用来创建Point
1
2
3
4
5
6
7
8
9
10
11public record Point(int x, int y) {
public static Point of() {
return new Point(0, 0);
}
public static Point of(int x, int y) {
return new Point(x, y);
}
}
var z = Point.of();
var p = Point.of(123, 456);
BigInteger
BigInteger
用于表示任意大小的整数;BigInteger
是不变类,并且继承自Number
;- 将
BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确
BigDecimal
BigDecimal
用于表示精确的小数,常用于财务计算;- 比较
BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
常用工具类
- Math:数学计算
- Random:生成伪随机数
- SecureRandom:生成安全的随机数
异常处理
Java的异常
- 异常是一种
class
,本身带有类型信息 - 异常可以在任何地方抛出,但只需要在上层捕获,这样就和方法调用分离了
Throwable
是异常体系的根,它继承自Object
Throwable
有两个体系:Error
和Exception
Error
表示严重的错误Exception
则是运行时的错误,它可以被捕获并处理
- 必须捕获的异常,包括
Exception
及其子类,但不包括RuntimeException
及其子类,这种类型的异常称为Checked Exception。 - 不需要捕获的异常,包括
Error
及其子类,RuntimeException
及其子类 - 捕获异常
使用
try...catch
语句,把可能发生异常的代码放到try {...}
中,然后使用catch
捕获对应的Exception
及其子类在方法定义的时候,使用
throws Xxx
表示该方法可能抛出的异常类型。调用方在调用的时候,必须强制捕获这些异常,否则编译器会报错只要是方法声明的Checked Exception,不在调用层捕获,也必须在更高的调用层捕获。所有未捕获的异常,最终也必须在
main()
方法中捕获1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Main {
public static void main(String[] args) {
try {
byte[] bs = toGBK("中文");
System.out.println(Arrays.toString(bs));
} catch (UnsupportedEncodingException e) {
System.out.println(e);
}
}
static byte[] toGBK(String s) throws UnsupportedEncodingException {
// 用指定编码转换String为byte[]:
return s.getBytes("GBK");
}
}
- 异常是一种
捕获异常
多catch语句
finally语句
捕获多种异常
1
2
3
4
5
6
7
8
9
10
11public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException | NumberFormatException e) { // IOException或NumberFormatException
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
抛出异常
异常的传播
- 当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个
try ... catch
被捕获为止
- 当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到某个
抛出异常
创建某个
Exception
的实例用
throw
语句抛出示例
1
2
3
4
5void process2(String s) {
if (s==null) {
throw new NullPointerException();
}
}
异常屏蔽(Suppressed Exception)
- 先用
origin
变量保存原始异常,然后调用Throwable.addSuppressed()
,把原始异常添加进来,最后在finally
抛出
- 先用
自定义异常
自定义一个
BaseException
,从RuntimeException
派生自定义的
BaseException
应该提供多个构造方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class BaseException extends RuntimeException {
public BaseException() {
super();
}
public BaseException(String message, Throwable cause) {
super(message, cause);
}
public BaseException(String message) {
super(message);
}
public BaseException(Throwable cause) {
super(cause);
}
}
**NullPointerException**
- 处理NullPointerException
成员变量在定义时初始化
1
2
3public class Person {
private String name = "";
}返回空字符串
""
、空数组而不是null
返回
Optional<T>
- 定位NullPointerException
- 处理NullPointerException
**Assertion**
- 单元测试与
JUnit
- 单元测试与
**Log**
JDK Logging
- 日志可以存档,便于追踪问题
- 按级别分类
java.util.logging
来实现日志功能
Commons Logging
在静态方法中引用
Log
,定义一个静态类型变量1
2
3
4
5
6
7public class Main {
static final Log log = LogFactory.getLog(Main.class);
static void foo() {
log.info("foo");
}
}在实例方法中引用
Log
,定义一个实例变量1
2
3
4
5
6
7public class Person {
protected final Log log = LogFactory.getLog(getClass());
void foo() {
log.info("foo");
}
}实例变量log的获取方式是
LogFactory.getLog(getClass())
子类可以直接使用该log
实例1
2
3
4
5public class Student extends Person {
void bar() {
log.info("bar");
}
}
Log4j
SLF4J和Logback
反射
反射
反射
Reflection
指程序在运行期可以拿到一个对象的所有信息Class类
- JVM为每个加载的
class
创建了对应的Class
实例,并在实例中保存了该class
的所有信息 - 通过
Class
实例获取class
信息的方法称为反射Reflection
- JVM为每个加载的
获取一个
class
的Class
实例通过一个
class
的静态变量class
1
Class cls = String.class;
一个实例变量,通过实例变量提供的
getClass()
方法1
2String s = "Hello";
Class cls = s.getClass();通过
class
的完整类名,静态方法Class.forName()
1
Class cls = Class.forName("java.lang.String");
Class
实例比较和instanceof
instanceof
不但匹配指定类型,还匹配指定类型的子类==
判断class
实例可以精确地判断数据类型,但不能作子类型比较通过反射获取该
Object
的class
信息1
2
3void printObjectInfo(Object obj) {
Class cls = obj.getClass();
}
动态加载
- 运行期根据条件来控制加载class
访问字段
- 通过
Class
实例获取所有Field
对象Field getField(name)
:根据字段名获取某个public
的field
(包括父类)Field getDeclaredField(name)
:根据字段名获取当前类的某个field
(不包括父类)Field[] getFields()
:获取所有public
的field
(包括父类)Field[] getDeclaredFields()
:获取当前类的所有field
(不包括父类)
- 首先获取
Student
的Class
实例,然后,分别获取public
字段、继承的public
字段以及private
字段getName()
:返回字段名称,例如,"name"
getType()
:返回字段类型,也是一个Class
实例,例如,String.class
getModifiers()
:返回字段的修饰符,它是一个int
,不同的bit表示不同的含义
- 字段值
- 获取字段值
- 先获取
Class
实例,再获取Field
实例,然后用Field.get(Object)
获取指定实例的指定字段的值
- 先获取
- 设置字段值
- 通过
Field.set(Object, Object)
实现的,其中第一个Object
参数是指定的实例,第二个Object
参数是待修改的值 - 修改非
public
字段,需要首先调用setAccessible(true)
- 通过
- 获取字段值
- 通过
调用方法
- 通过
Class
实例获取所有Method
信息Method getMethod(name, Class...)
:获取某个public
的Method
(包括父类)Method getDeclaredMethod(name, Class...)
:获取当前类的某个Method
(不包括父类)Method[] getMethods()
:获取所有public
的Method
(包括父类)Method[] getDeclaredMethods()
:获取当前类的所有Method
(不包括父类)
- 获取
Student
的Class
实例,然后,分别获取public
方法、继承的public
方法以及private
方法getName()
:返回方法名称,例如:"getScore"
getReturnType()
:返回方法返回值类型,也是一个Class实例,例如:String.class
getParameterTypes()
:返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
getModifiers()
:返回方法的修饰符,它是一个int
,不同的bit表示不同的含义
- 调用方法
- 对
Method
实例调用invoke
就相当于调用该方法,invoke
的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致
- 对
- 调用静态方法
- 如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以
invoke
方法传入的第一个参数永远为null
- 如果获取到的Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以
- 调用非public方法
- 和Field类似,对于非public方法,可以通过
Class.getDeclaredMethod()
获取该方法实例 Method.setAccessible(true)
允许其调用
- 和Field类似,对于非public方法,可以通过
- 多态
- 使用反射调用方法时,遵循多态原则:即总是调用实际类型的覆写方法(如果存在)
- 通过
调用构造方法
- 通过
Class
实例获取Constructor
的方法getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。
Java
反射API
提供了Constructor
对象,包含一个构造方法所有信息,可以创建一个实例Constructor
总是当前类定义的构造方法,和父类无关,因此不存在多态的问题- 调用非
public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问
- 通过
获取继承关系
获取父类的Class
有了
Class
实例,获取父类的Class
1
2Class i = Integer.class;
Class n = i.getSuperclass();
获取
interface
Class[] getInterfaces()
只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型- 获取接口的父接口要用
getInterfaces()
继承关系
- 判断一个实例是否是某个类型时,使用
instanceof
操作符 - 两个
Class
实例,要判断一个向上转型是否成立,调用isAssignableFrom()
- 判断一个实例是否是某个类型时,使用
动态代理
- Dynamic Proxy:在运行期动态创建某个
interface
的实例- 定义一个
InvocationHandler
实例,它负责实现接口的方法调用 - 通过
Proxy.newProxyInstance()
创建interface
实例,它需要3个参数- 使用的
ClassLoader
,通常就是接口类的ClassLoader
- 需要实现的接口数组,至少需要传入一个接口进去
- 用来处理接口方法调用的
InvocationHandler
实例
- 使用的
- 将返回的
Object
强制转型为接口
- 定义一个
- Dynamic Proxy:在运行期动态创建某个
注解
使用注解
- 注解
Annotation
是放在Java源码的类、方法、字段、参数前的一种特殊“注释”- 注释会被编译器忽略,注解被编译器打包进入class文件,是一种用作标注的“元数据”
- 注解作用
- 一、编译器使用的注解
@Override
:让编译器检查该方法是否正确地实现了覆写@SuppressWarnings
:让编译器忽略此处代码产生的警告- 这类注解不会被编译进入
.class
文件
- 二、工具处理
.class
文件使用的注解 - 三、在程序运行期能够读取的注解,它们在加载后一直存在于JVM中
- 定义一个注解时,还可以定义配置参数
- 一、编译器使用的注解
- 注解
定义注解
使用
@interface
语法来定义注解注解的参数类似无参数方法,
default
默认值。最常用的参数应当命名为value
1
2
3
4
5public Report {
int type() default 0;
String level() default "info";
String value() default "";
}
元注解
元注解
meta annotation
可以修饰其他注解元注解
@Target
定义Annotation
能够被应用于源码的哪些位置- 类或接口:
ElementType.TYPE
- 字段:
ElementType.FIELD
- 方法:
ElementType.METHOD
- 构造方法:
ElementType.CONSTRUCTOR
- 方法参数:
ElementType.PARAMETER
- 示例
定义注解
@Report
用在方法上,必须添加@Target(ElementType.METHOD
定义注解
@Report
可用在方法或字段上,可以把@Target
注解参数变为数组1
2
3
4
5
6
7
public Report {
...
}
- 类或接口:
元注解
@Retention
定义Annotation
的生命周期- 仅编译期:
RetentionPolicy.SOURCE
- 仅class文件:
RetentionPolicy.CLASS
- 运行期:
RetentionPolicy.RUNTIME
- default:
@Retention(RetentionPolicy.RUNTIME)
- 仅编译期:
元注解
@Repeatable
定义Annotation
是否可重复元注解
@Inherited
定义子类是否可继承父类定义的Annotation
@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效- 仅针对
class
的继承,对interface
的继承无效
处理注解
- 如何读取
RUNTIME
类型的注解- 注解定义后也是一种
class
,所有的注解都继承自java.lang.annotation.Annotation
- 读取注解,需要使用反射API
- 注解定义后也是一种
- 判断某个注解是否存在于
Class
、Field
、Method
或Constructor
Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
- 使用反射API读取Annotation
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
- 读取方法参数的注解,先用反射获取
Method
实例,然后读取方法参数的所有注解
- 如何读取
泛型
泛型
- 泛型是编写模板代码来适应任意类型,如
ArrayList<T>
,然后为用到的类创建对应的ArrayList<type>
ArrayList<T>
实现了List<T>
接口,可以向上转型为List<T>
- 泛型是编写模板代码来适应任意类型,如
使用泛型
省略后面的Number,编译器可以自动推断泛型类型
1
List<Number> list = new ArrayList<>();
除了
ArrayList<T>
使用了泛型,还可以在接口中使用泛型Arrays.sort(Object[])
可以对任意数组进行排序,但待排序的元素必须实现Comparable<T>
这个泛型接口
编写泛型
编写泛型类
按照某种类型,编写类
标记所有特定类型
特定类型
String
替换为T
,并申明<T>
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
静态方法
编写泛型类时,泛型类型
<T>
不能用于静态方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
// 静态泛型方法应该使用其他类型区分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}// 将静态方法的泛型类型和实例类型的泛型类型区分开多个泛型类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}
// 使用的时候,需要指出两种类型:
Pair<String, Integer> p = new Pair<>("test", 123);
//Java标准库的Map<K, V>就是使用两种泛型类型的例子
**Type Erasure**
泛型的局限
<T>
不能是基本类型- 实际类型是
Object
,Object
类型无法持有基本类型
- 实际类型是
无法取得带泛型的
Class
- 对
Pair<String>
和Pair<Integer>
类型获取Class
时,获取到的是同一个Class
,也就是Pair
类的Class
- 所有泛型实例,无论
T
的类型是什么,getClass()
返回同一个Class
实例,因为编译后它们全部都是Pair<Object>
- 对
无法判断带泛型的类型
不能实例化
T
类型要实例化
T
类型,我们必须借助额外的Class<T>
参数1
2
3
4
5
6
7publicclassPair<T> {private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}借助
Class<T>
参数并通过反射来实例化T
类型,使用的时候必须传入Class<T>
1
Pair<String> pair = new Pair<>(String.class);
覆写
泛型方法要防止重复定义方法,例如:
public boolean equals(T obj)
;定义的
equals(T t)
方法实际上会被擦拭成equals(Object t)
,而这个方法是继承自Object
1
2
3
4
5
6
7public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}
// 换个方法名,避开与Object.equals(Object)的冲突
// same(T t)
泛型继承
一个类可以继承自一个泛型类
1
2public class IntPair extends Pair<Integer> {
}在父类是泛型类型的情况下,编译器就必须把类型
T
保存到子类的class文件中在继承了泛型类型的情况下,子类可以获取父类的泛型类型
**extends**
通配符使用
<? extends Number>
的泛型定义称之为上界通配符Upper Bounds Wildcards
泛型类型
T
的上界限定在Number
方法参数签名
setFirst(? extends Number)
无法传递任何Number
的子类型给setFirst(? extends Number)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
// incompatible types: Pair<Integer> cannot be converted to Pair<Number>
// Pair<Integer>不是Pair<Number>的子类,add(Pair<Number>)不接受参数类型Pair<Integer>
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) { // add(Pair<Number> p)报错
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
class Pair<T> {...}
}作用
List<? extends Integer>
的限制:- 允许调用
get()
方法获取Integer
的引用; - 不允许调用
set(? extends Integer)
方法并传入任何Integer
的引用(null
除外) - 对参数
List<? extends Integer>
进行只读的方法
**
super
**通配符Number
和Object
是Integer
的父类,setFirst(Number)
和setFirst(Object)
实际上允许接受Integer
类型1
2
3
4void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}Pair<? super Integer>
表示,方法参数接受所有泛型类型为Integer
或Integer
父类的Pair
类型
作用
- 允许调用
set(? super Integer)
方法传入Integer
的引用 - 不允许调用
get()
方法获得Integer
的引用 - 唯一例外是可以获取
Object
的引用:Object o = p.getFirst()
- 方法内部代码对于参数只能写,不能读
- 允许调用
**extends**
和**super**
作为方法参数,
<? extends T>
类型和<? super T>
类型的区别在于:<? extends T>
允许调用读方法T get()
获取T
的引用,但不允许调用写方法set(T)
传入T
的引用(传入null
除外)<? super T>
允许调用写方法set(T)
传入T
的引用,但不允许调用读方法T get()
获取T
的引用(获取Object
除外)
Collections
类定义的copy()
方法第一个参数是
List<? super T>
,表示目标List
,第二个参数List<? extends T>
,表示要复制的List
1
2
3
4
5
6
7
8
9public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}
PECS原则
- Producer Extends Consumer Super
- 如果需要返回
T
,它是生产者(Producer),使用extends
通配符 - 如果需要写入
T
,它是消费者(Consumer),使用super
通配符
- 如果需要返回
- Producer Extends Consumer Super
无限定通配符
Unbounded Wildcard Type
1
2void sample(Pair<?> p) {
}不允许调用
set(T)
方法并传入引用(null
除外)不允许调用
T get()
方法并获取T
引用(只能获取Object
引用)只能做
null
判断可以用
<T>
替换,同时它是所有<T>
类型的超类
泛型和反射
- 部分反射API是泛型,如:
Class<T>
,Constructor<T>
- 可以声明带泛型的数组,但不能直接创建带泛型的数组,必须强制转型
- 可以通过
Array.newInstance(Class<T>, int)
创建T[]
数组,需要强制转型 - 谨慎使用泛型和可变参数
- 部分反射API是泛型,如:
集合
**Collection**
java.util
包提供了集合类:Collection
,它是除Map
外所有其他集合类的根接口List
:一种有序列表的集合Set
:一种保证没有重复元素的集合Map
:一种通过键值(key-value)查找的映射表集合
- 实现了接口和实现类相分离
- 有序表的接口是
List
,具体的实现类有ArrayList
,LinkedList
等 - 支持泛型,我们可以限制在一个集合中只能放入同一种数据类型的元素
- 有序表的接口是
- 集合使用统一的
Iterator
遍历
**List**
ArrayList
在内部使用了数组来存储所有元素List<E>
接口- 在末尾添加一个元素:
boolean add(E e)
- 在指定索引添加一个元素:
boolean add(int index, E e)
- 删除指定索引的元素:
E remove(int index)
- 删除某个元素:
boolean remove(Object e)
- 获取指定索引的元素:
E get(int index)
- 获取链表大小(包含元素的个数):
int size()
- 在末尾添加一个元素:
实现
List
接口ArrayList
通过数组实现LinkedList
通过链表实现ArrayList
和LinkedList
ArrayList LinkedList 获取指定元素 很快 从头查找 添加元素到末尾 很快 很快 指定位置添加或删除 需要移动元素 不需要移动元素 内存占用 少 较大
List
的特点List
内部的元素可以重复- 允许添加
null
创建
List
除了
ArrayList
和LinkedList
of()
方法(不接受null
)1
List<Integer> list = List.of(1, 2, 5);
遍历
List
for
循环根据索引配合get(int)
方法遍历Iterator
由
List
的实例调用iterator()
方法的时候创建1
2
3
4
5
6
7
8
9public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
String s = it.next();
System.out.println(s);
}
}
}for each
循环1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
List<String> list = List.of("apple", "pear", "banana");
for (String s : list) {
System.out.println(s);
}
}
}实现了
Iterable
接口的集合类都可以直接用for each
循环来遍历
List
和Array
转换调用
toArray()
方法直接返回一个Object[]
数组toArray(T[])
传入一个类型相同的Array
,List
内部自动把元素复制到传入的Array
Integer[] **array** = **list**.toArray(**new** Integer[**list**.size()]);
Integer[] **array** = **list**.toArray(Integer[]::**new**);
Array
-→List
**
List**<Integer> **list** = **List**.of(**array**);
(只读)
找出
List
缺失的数字1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19private static int findMissingNumber2(int start, int end, ArrayList<Integer> list) {
int sum1 = 0;
int max = start;
int min = end;
for (int n : list) {
sum1 += n;
if (n > max) max = n;
if (n < min) min = n;
}
int sum2 = (min + max) * (list.size() + 1) / 2;
if (start != min) return start;
if (end != max) return end;
return sum2 - sum1;
}
**equals**
List
提供boolean contains(Object o)
方法来判断List
是否包含某个指定元素int indexOf(Object o)
方法返回元素的索引,如果元素不存在,返回-1
- 放入的实例必须正确覆写
equals()
方法
equals()
方法要求自反性(Reflexive)
对称性(Symmetric)
传递性(Transitive)
一致性(Consistent)
对
null
的比较:即x.equals(null)
永远返回false
对于
Person
类1
2
3
4
5
6
7public boolean equals(Object o) {
if (o instanceof Person p) {
return this.name.equals(p.name) && this.age == p.age;
}
return false;
}
// 引用字段比较使用equals(),基本类型字段的比较使用==简化引用类型的比较,使用
Objects.equals()
静态方法1
2
3
4
5
6public boolean equals(Object o) {
if (o instanceof Person p) {
return Objects.equals(this.name, p.name) && this.age == p.age;
}
return false;
}
**Map**
Map<K, V>
是一种键-值映射表- 当调用
put(K key, V value)
方法时,就把key
和value
做了映射并放入Map
- 当我们调用
V get(K key)
时,就可以通过key
获取到对应的value
- 如果
key
不存在,则返回null
List
类似,Map
也是一个接口,最常用的实现类是HashMap
- 查询某个
key
是否存在,调用boolean containsKey(K key)
方法 Map
中不存在重复的key
,放入相同的key
,只会把原有的value
给替换掉
- 当调用
- 遍历
Map
for each
循环遍历Map
实例keySet()
方法返回的Set
集合,包含不重复的key
集合1
for (String key : map.keySet()) {...}
for each
循环遍历Map
对象的entrySet()
集合,包含每一个key-value
映射1
for (Map.Entry<String, Integer> entry : map.entrySet()) {};
编写
**equals**
和**hashCode**
Map
- 作为
key
的对象必须正确覆写equals()
方法,相等的两个key
实例调用equals()
必须返回true
- 作为
key
的对象还必须正确覆写hashCode()
方法,且hashCode()
方法要严格遵循以下规范- 如果两个对象相等,则两个对象的
hashCode()
必须相等 - 如果两个对象不相等,则两个对象的
hashCode()
尽量不要相等
- 如果两个对象相等,则两个对象的
- 编写
equals()
和hashCode()
遵循的原则是equals()
用到的用于比较的每一个字段,都必须在hashCode()
中用于计算equals()
中没有使用到的字段,绝不可放在hashCode()
中计算- 实现
hashCode()
方法可以通过Objects.hashCode()
辅助方法实现
- 创建
HashMap
时指定容量
- 作为
EnumMap
- 如果
Map
的key是enum
类型,使用EnumMap
- 如果
TreeMap
SortedMap
在内部会对Key进行排序,实现类是TreeMap
- 使用
TreeMap
时,放入的Key必须实现Comparable
接口如果作为Key的class没有实现
Comparable
接口,在创建TreeMap
时同时必须指定一个自定义排序算法严格按照
compare()
规范实现比较逻辑定义
Student
类,并用分数score
进行排序,高分在前1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import java.util.*;
public class Main {
public static void main(String[] args) {
Map<Student, Integer> map = new TreeMap<>(new Comparator<Student>() {
public int compare(Student p1, Student p2) {
if (p1.score == p2.score) {
return 0;
}
return p1.score > p2.score ? -1 : 1;
}// or: Integer.compare(int, int)
});
map.put(new Student("Tom", 77), 1);
map.put(new Student("Bob", 66), 2);
map.put(new Student("Lily", 99), 3);
for (Student key : map.keySet()) {
System.out.println(key);
}
System.out.println(map.get(new Student("Bob", 66))); // null?
}
}
class Student {
public String name;
public int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return String.format("{%s: score=%d}", name, score);
}
}
Properties
读取配置文件
创建
Properties
实例调用
load()
读取文件调用
getProperty()
获取配置(覆盖配置)1
2
3
4
5
6
7String f = "setting.properties";
Properties props = new Properties();
props.load(new java.io.FileInputStream(f));
// props.load(getClass().getResourceAsStream("/common/setting.properties"));
String filepath = props.getProperty("last_open_file");
String interval = props.getProperty("auto_save_interval", "120");
写入配置文件
setProperty()
修改了Properties
实例- 写入配置文件使用
store()
编码
使用重载方法
load(Reader)
读取1
2
3Properties props = new Properties();
props.load(new FileReader("settings.properties", StandardCharsets.UTF_8));
// InputStream和Reader的区别是一个是字节流,一个是字符流
Set
Set
用于存储不重复的元素集合- 将元素添加进
Set<E>
:boolean add(E e)
- 将元素从
Set<E>
删除:boolean remove(Object e)
- 判断是否包含元素:
boolean contains(Object e)
- 将元素添加进
- 放入
Set
的元素和Map
的key类似,都要正确实现equals()
和hashCode()
方法 - 最常用的
Set
实现类是HashSet
Set
接口并不保证有序HashSet
是无序的,实现了Set
接口,并没有实现SortedSet
接口TreeSet
是有序的,实现了SortedSet
接口- 使用
TreeSet
和使用TreeMap
的要求一样- 添加的元素必须正确实现
Comparable
接口 - 如果没有实现
Comparable
接口,创建TreeSet
时必须传入一个Comparator
对象
- 添加的元素必须正确实现
Queue
Queue
是实现了一个先进先出(FIFO)的有序表int size()
:获取队列长度boolean add(E)
/boolean offer(E)
:添加元素到队尾E remove()
/E poll()
:获取队首元素并从队列中删除E element()
/E peek()
:获取队首元素但并不从队列中删除
两个方法(要避免把
null
添加到队列)throw Exception 返回false或null 添加元素到队尾 add(E e) boolean offer(E e) 取队首元素并删除 E remove() E poll() 取队首元素但不删除 E element() E peek() LinkedList
即实现了List
接口,又实现了Queue
接口1
2
3
4// 这是一个List:
List<String> list = new LinkedList<>();
// 这是一个Queue:
Queue<String> queue = new LinkedList<>();
PriorityQueue
- 放入
PriorityQueue
的元素必须实现Comparable
接口,or通过Comparator
自定义排序算法,根据元素排序顺序决定出队优先级
- 放入
Deque
Double Ended Queue
Queue Deque 添加元素到队尾 add(E e) / offer(E e) addLast(E e) / offerLast(E e) 取队首元素并删除 E remove() / E poll() E removeFirst() / E pollFirst() 取队首元素但不删除 E element() / E peek() E getFirst() / E peekFirst() 添加元素到队首 无 addFirst(E e) / offerFirst(E e) 取队尾元素并删除 无 E removeLast() / E pollLast() 取队尾元素但不删除 无 E getLast() / E peekLast() Deque
接口实际上扩展自Queue
- 调用
xxxFirst()
/xxxLast()
以便与Queue
的方法区分开; - 避免把
null
添加到队列
- 调用
Deque
是一个接口,它的实现类有ArrayDeque
和LinkedList
Stack
- 栈(Stack)是一种后进先出(LIFO)的数据结构
- 把元素压栈:
push(E)
- 把栈顶的元素“弹出”:
pop()
- 取栈顶元素但不弹出:
peek()
- 把元素压栈:
Deque
可以实现Stack
的功能push(E)
/addFirst(E)
pop()
/removeFirst()
peek()
/peekFirst()
- 作用
- JVM在处理Java方法调用的时候就会通过栈这种数据结构维护方法调用的层次
- JVM会创建方法调用栈,每调用一个方法时
- 先将参数压栈,然后执行对应的方法
- 当方法返回时,返回值压栈,调用方法通过出栈操作获得方法返回值
- 嵌套调用过多会造成栈溢出,引发
StackOverflowError
- JVM会创建方法调用栈,每调用一个方法时
- 整数的进制转换
- 计算中缀表达式
- JVM在处理Java方法调用的时候就会通过栈这种数据结构维护方法调用的层次
- 栈(Stack)是一种后进先出(LIFO)的数据结构
Iterator
通过
Iterator
对象遍历集合的模式称为迭代器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import java.util.*;
public class Main {
public static void main(String[] args) {
ReverseList<String> rlist = new ReverseList<>();
rlist.add("Apple");
rlist.add("Orange");
rlist.add("Pear");
for (String s : rlist) {
System.out.println(s);
}
}
}
class ReverseList<T> implements Iterable<T> {
private List<T> list = new ArrayList<>();
public void add(T t) {
list.add(t);
}
public Iterator<T> iterator() {
return new ReverseIterator(list.size());
}
class ReverseIterator implements Iterator<T> {
int index;
ReverseIterator(int index) {
this.index = index;
}
public boolean hasNext() {
return index > 0;
}
public T next() {
index--;
return ReverseList.this.list.get(index);
}
}
}
Collections
addAll()
方法可以给一个Collection
类型的集合添加若干元素1
public static boolean addAll(Collection<? super T> c, T... elements) { ... }
创建空集合
创建空List:
List<T> emptyList()
创建空Map:
Map<K, V> emptyMap()
创建空Set:
Set<T> emptySet()
返回的空集合是不可变集合,无法向其中添加或删除元素
集合接口提供的
of(T...)
方法创建空集合1
2List<String> list1 = List.of();
List<String> list2 = Collections.emptyList();
创建单元素集合
- 单元素集合也是不可变集合
- 创建一个元素的List:
List<T> singletonList(T o)
- 创建一个元素的Map:
Map<K, V> singletonMap(K key, V value)
- 创建一个元素的Set:
Set<T> singleton(T o)
使用
List.of(T...)
,可以创建空集合,单元素集合,任意个元素的集合排序:
Collections.sort(list);
洗牌:
Collections.shuffle(list);
不可变集合
- 封装成不可变List:
List<T> unmodifiableList(List<? extends T> list)
- 封装成不可变Set:
Set<T> unmodifiableSet(Set<? extends T> set)
- 封装成不可变Map:
Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m)
- 对原始的可变
List
进行增删是可以的,并且会直接影响到封装后的“不可变”List
- 封装成不可变List:
IO
IO
- IO是指Input/Output
InputStream / OutputStream
- IO流以
byte
(字节)为最小单位,因此也称为字节流
- IO流以
Reader / Writer
- 字符流
- Java提供了
Reader
和Writer
表示字符流,传输的最小数据单位是char
- 同步和异步
- Java标准库的
java.io
包提供了同步IO功能 - 同步IO
- 读写IO时代码必须等待数据返回后才继续执行后续代码
- 优点是代码编写简单,缺点是CPU执行效率低
- 异步IO
- 读写IO时仅发出请求,然后立刻执行后续代码
- 优点是CPU执行效率高,缺点是代码编写复杂
- Java标准库的
File
File
对象- Java的标准库
java.io
提供了File
对象来操作文件和目录 - 构造File对象时,既可以传入绝对路径,也可以传入相对路径
- File对象有3种形式表示的路径
getPath()
返回构造方法传入的路径getAbsolutePath()
返回绝对路径getCanonicalPath
返回的是规范路径,和绝对路径类似
- Java的标准库
- 文件和目录
File
对象既可以表示文件,也可以表示目录- 构造一个
File
对象不会导致任何磁盘操作 - 调用
File
对象的某些方法时才真正进行磁盘操作
- 构造一个
File
对象获取到一个文件时,判断文件的权限和大小boolean canRead()
boolean canWrite()
boolean canExecute()
long length()
- 创建和删除文件
createNewFile()
创建一个新文件delete()
删除该文件createTempFile()
来创建一个临时文件deleteOnExit()
在JVM退出时自动删除该文件
- 遍历文件和目录
list()
和listFiles()
列出目录下的文件和子目录名- File对象如果表示一个目录
boolean mkdir()
:创建当前File对象表示的目录boolean mkdirs()
:创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来boolean delete()
:删除当前File对象表示的目录,当前目录必须为空才能删除成功
Path
Path
对象位于java.nio.file
包如果需要对目录进行复杂的拼接、遍历等操作,使用
Path
对象更方便1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import java.io.*;
import java.nio.file.*;
public class Main {
public static void main(String[] args) throws IOException {
Path p1 = Paths.get(".", "project", "study"); // 构造一个Path对象
System.out.println(p1);
Path p2 = p1.toAbsolutePath(); // 转换为绝对路径
System.out.println(p2);
Path p3 = p2.normalize(); // 转换为规范路径
System.out.println(p3);
File f = p3.toFile(); // 转换为File对象
System.out.println(f);
for (Path p : Paths.get("..").toAbsolutePath()) { // 可以直接遍历Path
System.out.println(" " + p);
}
}
}
InputStream
InputStream
Java标准库提供的最基本的输入流
位于package
java.io
,提供了所有同步IO的功能不是一个接口,而是一个抽象类,它是所有输入流的超类
抽象类定义的一个最重要的方法就是
int read()
方法签名
1
public abstract int read() throws IOException;
FileInputStream
是InputStream
的一个子类,从文件流中读取数据1
2
3
4
5
6
7
8
9
10
11
12public void readFile() throws IOException {
// 创建一个FileInputStream对象:
InputStream input = new FileInputStream("src/readme.txt");
for (;;) {
int n = input.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println(n); // 打印byte的值
}
input.close(); // 关闭流
}用
try ... finally
来保证InputStream
在无论是否发生IO错误的时候都能够正确地关闭try(resource)
1
2
3
4
5
6
7
8public void readFile() throws IOException {
try (InputStream input = new FileInputStream("src/readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
} // 编译器在此自动为我们写入finally并调用close()
}
缓冲
InputStream
提供了两个重载方法来支持读取多个字节int read(byte[] b)
:读取若干字节并填充到byte[]
数组,返回读取的字节数int read(byte[] b, int off, int len)
:指定byte[]
数组的偏移量和最大填充数
阻塞
在调用
InputStream
的read()
方法读取数据时,read()
方法是阻塞Blocking
1
2
3int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;
InputStream实现类
FileInputStream
从文件获取输入流ByteArrayInputStream
在内存中模拟一个InputStream
OutputStream
OutputStream
Java标准库提供的最基本的输出流
是抽象类,是所有输出流的超类
抽象类定义的一个最重要的方法就是
void write(int b)
方法签名
1
public abstract void write(int b) throws IOException;
缓冲区、
flush()
FileOutputStream
一次性写入若干字节,
OutputStream
提供的重载方法void write(byte[])
1
2
3
4
5public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("out/readme.txt");
output.write("Hello".getBytes("UTF-8")); // Hello
output.close();
}try(resource)
来保证OutputStream
在无论是否发生IO错误时都能正确地关闭1
2
3
4
5public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("out/readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}阻塞
- 和
InputStream
一样,OutputStream
的write()
方法也是阻塞的
- 和
OutputStream
实现类FileOutputStream
从文件获取输出流ByteArrayOutputStream
可以在内存中模拟一个OutputStream
**
Filter
**模式- 为了解决依赖继承会导致子类数量失控的问题,JDK将
InputStream
分为两大类- 一类是直接提供数据的基础
InputStream
,如:FileInputStream
ByteArrayInputStream
ServletInputStream
- 一类是提供额外附加功能的
InputStream
,如:BufferedInputStream
DigestInputStream
CipherInputStream
- 一个基础组件叠加各种附加功能组件的模式,称为Filter模式/装饰器模式Decorator
- 一类是直接提供数据的基础
- 编写
FilterInputStream
- 为了解决依赖继承会导致子类数量失控的问题,JDK将
操作**
Zip
**ZipInputStream
是一种FilterInputStream
,可以直接读取zip包的内容读取zip包
创建一个
ZipInputStream
,通常是传入一个FileInputStream
作为数据源,然后循环调用getNextEntry()
,直到返回null
,表示zip流结束一个
ZipEntry
表示一个压缩文件或目录,如果是压缩文件,我们就用read()
方法不断读取,直到返回-1
:1
2
3
4
5
6
7
8
9
10
11
12try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}
}
写入zip包
创建一个
ZipOutputStream
,通常是包装一个FileOutputStream
,然后,每写入一个文件前,先调用putNextEntry()
,然后用write()
写入byte[]
数据,写入完毕后调用closeEntry()
结束这个文件的打包1
2
3
4
5
6
7
8try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
File[] files = ...
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(Files.readAllBytes(file.toPath()));
zip.closeEntry();
}
}
读取
**classpath**
资源Java程序启动时需要从一个
.properties
文件中读取配置从classpath读取文件可以避免不同环境下文件路径不一致的问题
- 把
default.properties
文件放到classpath中,就不用关心它的实际存放路径
- 把
在classpath中的资源文件,路径总是以
/
开头,我们先获取当前的Class
对象,然后调用getResourceAsStream()
就可以直接从classpath读取任意的资源文件1
2
3
4
5try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
if (input != null) {
// TODO:
}
}
序列化
序列化
- 是指把一个Java对象变成二进制内容,本质上就是一个
byte[]
数组 - 序列化后可以把
byte[]
保存到文件中,或者把byte[]
通过网络传输到远程
- 是指把一个Java对象变成二进制内容,本质上就是一个
反序列化
- 把一个二进制内容(
byte[]
数组)变回Java对象 - 保存到文件中的
byte[]
数组又可以变回Java对象,或者从网络上读取byte[]
并把它变回Java对象
- 把一个二进制内容(
序列化
一个Java对象要能序列化,必须实现一个特殊的
java.io.Serializable
接口1
public interface Serializable {...}
Serializable
接口没有定义任何方法,是一个空接口,称为标记接口Marker Interface
一个Java对象变为
byte[]
数组,需要使用ObjectOutputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object,实现了Serializable接口的Object
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
}
反序列化
和
ObjectOutputStream
相反,ObjectInputStream
负责从一个字节流读取Java对象1
2
3
4
5try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}除了能读取基本类型和
String
类型外,调用readObject()
可以直接返回一个Object
对象。要把它变成一个特定类型,必须强制转型。readObject()
可能抛出的异常有:ClassNotFoundException
:没有找到对应的ClassInvalidClassException
:Class不匹配
反序列化时
- 由JVM直接构造出Java对象
- 不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行
Reader
java.io.Reader
是所有字符输入流的超类,最主要的方法是1
publicint read()throws IOException;
Reader
是Java的IO库提供的另一个输入流接口InputStream
是一个字节流,即以byte
为单位读取Reader
是一个字符流,即以char
为单位读取对比
InputStream Reader 字节流,以byte为单位 字符流,以char为单位 读取字节(-1,0~255):int read() 读取字符(-1,0~65535):int read() 读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)
FileReader
是
Reader
的一个子类,它可以打开文件并获取Reader
1
2
3
4
5
6
7
8
9
10
11
12
13public void readFile() throws IOException {
// 创建一个FileReader对象:
Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);
for (;;) {
int n = reader.read(); // 反复调用read()方法,直到返回-1
if (n == -1) {
break;
}
System.out.println((char)n); // 打印char
}
reader.close(); // 关闭流
}
// 创建FileReader时指定编码,避免乱码try (resource)
来保证Reader
在无论有没有IO错误的时候都能够正确地关闭一次性读取若干字符并填充到
char[]
数组1
public int read(char[] c) throws IOException
- 返回实际读入的字符个数,最大不超过
char[]
数组的长度 - 返回
-1
表示流结束
- 返回实际读入的字符个数,最大不超过
CharArrayReader
CharArrayReader
可以在内存中模拟一个Reader
实际上是把一个
char[]
数组变成一个Reader
,和ByteArrayInputStream
类似1
2try (Reader reader =new CharArrayReader("Hello".toCharArray())) {
}
StringReader
StringReader
可以直接把String
作为数据源,它和CharArrayReader
几乎一样1
2try (Reader reader =new StringReader("Hello")) {
}
InputStreamReader
可以把任何
InputStream
转换为Reader
构造
InputStreamReader
时,需要传入InputStream
,需要指定编码,得到Reader
对象通过
try (resource)
:1
2
3try (Reader reader =new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
// TODO:
}实际上就是
FileReader
的一种实现方式使用
try (resource)
结构时,当我们关闭Reader
时,它会在内部自动调用InputStream
的close()
方法,所以,只需要关闭最外层的Reader
对象即可。使用InputStreamReader,可以把一个InputStream转换成一个Reader
Writer
Writer
Reader
是带编码转换器的InputStream
,把byte
转换为char
Writer
就是带编码转换器的OutputStream
,把char
转换为byte
并输出
Writer
和OutputStream
OutputStream Writer 字节流,以byte为单位 字符流,以char为单位 写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c) 写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c) 无对应方法 写入String:void write(String s) Writer
是所有字符输出流的超类void write(int c)
void write(char[] c)
void write(String s)
FileWriter
FileWriter
就是向文件中写入字符流的Writer
。使用方法和FileReader
类似1
2
3
4
5try (Writer writer =new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H');// 写入单个字符
writer.write("Hello".toCharArray());// 写入char[]
writer.write("Hello");// 写入String
}
CharArrayWriter
CharArrayWriter
可以在内存中创建一个Writer
实际上是构造一个缓冲区,可以写入
char
,最后得到写入的char[]
数组,和ByteArrayOutputStream
非常类似1
2
3
4
5
6try (CharArrayWriter writer =new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray();// { 'A', 'B', 'C' }
}
StringWriter
StringWriter
也是一个基于内存的Writer
,和CharArrayWriter
类似StringWriter
在内部维护了一个StringBuffer
,并对外提供了Writer
接口
OutputStreamWriter
除
CharArrayWriter
和StringWriter
外,普通Writer实际上是基于OutputStream
构造的接收
char
,然后在内部自动转换成一个或多个byte
,并写入OutputStream
因此,
OutputStreamWriter
就是一个将任意的OutputStream
转换为Writer
的转换器1
2
3
4try (Writer writer =new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}上述代码实际上就是
FileWriter
的一种实现方式
PrintStream
和PrintWriter
PrintStream
- 一种
FilterOutputStream
,在OutputStream
接口上,提供写入各种数据类型的方法 - 写入
int
:print(int)
- 写入
boolean
:print(boolean)
- 写入
String
:print(String)
- 写入
Object
:print(Object)
,实际上相当于print(object.toString())
System.out
是标准输出;System.err
是标准错误输出
- 一种
PrintWriter
PrintStream
最终输出是byte数据,PrintWriter
则是扩展了Writer
接口,它的print()
/println()
方法最终输出的是char
数据
使用
**Files**
- 是
java.nio
包里面的类 - 对于简单的小文件读写操作,可以使用
Files
工具类
- 是
日期与时间
**Date & Time**
- 以
GMT
或者UTC
加时区偏移表示- 如:
GMT+08:00
或者UTC+08:00
表示东八区 - 本地化
Locale
表示一个国家或地区的日期、时间、数字、货币等格式Locale
由语言_国家
的字母缩写构成- 如,
zh_CN
表示中文+中国,en_US
表示英文+美国
- 如:
- 以
Date & Calendar
Epoch Time
时间戳,几种存储方式
- 以秒为单位的整数
- 以毫秒为单位的整数
- 以秒为单位的浮点数
通常是用
long
表示的毫秒数,即:1
long t = 1574208900123L;
获取当前时间戳,使用
System.currentTimeMillis()
标准库API
java.util
package- 主要包括
Date
、Calendar
和TimeZone
- 主要包括
java.time
package- 主要包括
LocalDateTime
、ZonedDateTime
、ZoneId
- 主要包括
Date
java.util.Date
是用于表示一个日期和时间的对象- 用法示例
Date
对象- 不能转换时区,除了
toGMTString()
可以按GMT+0:00
输出外 - 很难对日期和时间进行加减
- 不能转换时区,除了
Calendar
- 用于获取并设置年、月、日、时、分、秒
- 只有一种方式获取即
Calendar.getInstance()
,而且获取到是当前时间 Calendar.getTime()
可以将一个Calendar
对象转换成Date
对象
TimeZone
Calendar
和Date
相比,它提供了时区转换的功能
LocalDateTime
java.time
包提供了新的日期和时间API- 本地日期和时间:
LocalDateTime
,LocalDate
,LocalTime
- 带时区的日期和时间:
ZonedDateTime
- 时刻:
Instant
- 时区:
ZoneId
,ZoneOffset
- 时间间隔:
Duration
- 格式化类型:
DateTimeFormatter
- 本地日期和时间:
- LocalDateTime、DateTimeFormatter、Duration和Period
Duration
表示两个时刻之间的时间间隔、Period
表示两个日期之间的天数LocalDateTime
可以非常方便地对日期和时间进行加减,或者调整日期和时间,它总是返回新对象- 使用
isBefore()
和isAfter()
可以判断日期和时间的先后 - 使用
Duration
和Period
可以表示两个日期和时间的“区间间隔”
ZonedDateTime
- 带时区的日期和时间
LocalDateTime
加ZoneId
- 时区转换
ZonedDateTime
对象withZoneSameInstant()
将关联时区转换到另一个时区
- 带时区的日期和时间
DateTimeFormatter
- 不变对象、线程安全
- 对
ZonedDateTime
或LocalDateTime
进行格式化,需要使用DateTimeFormatter
DateTimeFormatter
可以通过格式化字符串和Locale
对日期和时间进行定制输出
Instant
System.currentTimeMillis()
返回的就是以毫秒表示的当前时间戳- 在
java.time
中以Instant
类型表示,用Instant.now()
获取当前时间戳
- 在
Instant
表示高精度时间戳,它可以和ZonedDateTime
以及long
互相转换
单元测试
编写**
JUnit
**测试单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试
每个单元测试应当互相独立,不依赖运行的顺序
测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为
0
,null
,空字符串""
等情况FactorialTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package com.itranswarp.learnjava;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class FactorialTest {
void testFact() {
assertEquals(1, Factorial.fact(1));
assertEquals(2, Factorial.fact(2));
assertEquals(6, Factorial.fact(3));
assertEquals(3628800, Factorial.fact(10));
assertEquals(2432902008176640000L, Factorial.fact(20));
}
}
使用Fixture
- 编写测试前准备、测试后清理的固定代码
- 对于实例变量,在
@BeforeEach
中初始化,在@AfterEach
中清理- 在各个
@Test
方法中互不影响,因为是不同的实例
- 在各个
- 对于静态变量,在
@BeforeAll
中初始化,在@AfterAll
中清理- 在各个
@Test
方法中均是唯一实例,会影响各个@Test
方法
- 在各个
- 对于实例变量,在
- 编写测试前准备、测试后清理的固定代码
异常测试
assertThrows()
期望捕获一个指定异常、第二个参数封装了要执行的会产生异常的代码1
2
3
4
5
6
void testNegative() {
assertThrows(IllegalArgumentException.class, () -> {
Factorial.fact(-1);
});
}
条件测试
- 根据某些注解在运行期让JUnit自动忽略某些测试
参数化测试
@ParameterizedTest
注解,进行参数化测试- 参数传入
@MethodSource
- 编写一个同名的静态方法来提供测试参数
@CsvSource
- 每一个字符串表示一行
@CsvFileSource
正则表达式
正则表达式
java.util.regex
包- 用字符串来描述规则,并用来匹配字符串
匹配规则
单字符
正则表达式 规则 可以匹配 A 指定字符 A \u548c 指定Unicode字符 和 . 任意字符 a,b,&,0 \d 数字0~9 0~9 \w 大小写字母,数字和下划线 a z,AZ,0~9,_\s 空格、Tab键 空格,Tab \D 非数字 a,A,&,_,…… \W 非\w &,@,中,…… \S 非\s a,A,&,_,…… 多个字符
正则表达式 规则 可以匹配 A* 任意个数字符 空,A,AA,AAA,…… A+ 至少1个字符 A,AA,AAA,…… A? 0个或1个字符 空,A A{3} 指定个数字符 AAA A{2,3} 指定范围个数字符 AA,AAA A{2,} 至少n个字符 AA,AAA,AAAA,…… A{0,3} 最多n个字符 空,A,AA,AAA
复杂匹配规则
- 匹配开头和结尾
^
表示开头,$
表示结尾
- 匹配指定范围
[...]
匹配范围内的字符[1-9]
[0-9a-fA-F]
、六位[0-9a-fA-F]{6}
- 不包含指定范围的字符
[^1-9]{3}
- 或规则匹配
|
连接的两个正则规则是或规则
- 使用括号
- 公共部分提出来,
(...)
把子规则括起来成learn\\s(java|php|go)
- 公共部分提出来,
- 匹配开头和结尾
分组匹配
- 引入
java.util.regex
包(...)
先分组,用Pattern
对象匹配- 匹配后获得一个
Matcher
对象 - 如果匹配成功,从
Matcher.group(index)
返回子串 Matcher.group(index)
方法的参数0即整个正则匹配到的字符串
1表示第一个子串,2表示第二个子串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import java.util.regex.*;
public class Main {
public static void main(String[] args) {
Pattern pattern = Pattern.compile("(\\d{3,4})\\-(\\d{7,8})");
pattern.matcher("010-12345678").matches(); // true
pattern.matcher("021-123456").matches(); // false
pattern.matcher("022#1234567").matches(); // false
// 获得Matcher对象:
Matcher matcher = pattern.matcher("010-12345678");
if (matcher.matches()) {
String whole = matcher.group(0); // "010-12345678", 0表示匹配的整个字符串
String area = matcher.group(1); // "010", 1表示匹配的第1个子串
String tel = matcher.group(2); // "12345678", 2表示匹配的第2个子串
System.out.println(area);
System.out.println(tel);
}
}
}
- 引入
非贪婪匹配
- 正则表达式匹配默认贪婪匹配,使用
?
表示对某一规则进行非贪婪匹配
- 正则表达式匹配默认贪婪匹配,使用
搜索和替换
分割字符串
String.split()
方法传入正则表达式
搜索字符串
获取到
Matcher
对象后,反复调用find()
方法,搜索能匹配上\\wo\\w
规则的子串1
2
3
4
5
6
7
8
9
10
11public class Main {
public static void main(String[] args) {
String s = "the quick brown fox jumps over the lazy dog.";
Pattern p = Pattern.compile("\\wo\\w");
Matcher m = p.matcher(s);
while (m.find()) {
String sub = s.substring(m.start(), m.end());
System.out.println(sub);
}
}
}
替换字符串
String.replaceAll()
(正则表达式,待替换的字符串)
反向引用
把任何4字符单词的前后用
<b>xxxx</b>
括起来" <b>$1</b> "
用匹配的分组子串([a-z]{4})
替换了$1
1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
String s = "the quick brown fox jumps over the lazy dog.";
String r = s.replaceAll("\\s([a-z]{4})\\s", " <b>$1</b> ");
System.out.println(r);
}
}
// the quick brown fox jumps <b>over</b> the <b>lazy</b> dog.
多线程
多线程基础
- 进程
- 一个任务称为一个进程
- 子任务称为线程
- 一个进程可以包含一个或多个线程,但至少会有一个线程
- 进程
创建新线程
实例化一个
Thread
实例,调用start()
方法一个线程对象只能调用一次
start()
方法- 线程的执行代码写在
run()
方法中 - 线程调度由操作系统决定,程序本身无法决定调度顺序
Thread.sleep()
可以把当前线程暂停一段时间
- 线程的执行代码写在
从
Thread
派生一个自定义类,然后覆写run()
方法1
2
3
4
5
6
7
8
9
10
11
12
13public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
public void run() {
System.out.println("start new thread!");
}
}创建
Thread
实例时,传入一个Runnable
实例1
2
3
4
5
6
7
8
9
10
11
12
13public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
public void run() {
System.out.println("start new thread!");
}
}lambda
1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
对线程设定优先级
1
Thread.setPriority(int n)// 1~10, 默认值5
线程状态
- 线程状态
- New:新创建的线程,尚未执行
- Runnable:运行中的线程,正在执行
run()
方法的Java代码 - Blocked:运行中的线程,因为某些操作被阻塞而挂起
- Waiting:运行中的线程,因为某些操作在等待中
- Timed Waiting:运行中的线程,因为执行
sleep()
方法正在计时等待 - Terminated:线程已终止,因为
run()
方法执行完毕
- 线程终止的原因有:
- 线程正常终止:
run()
方法执行到return
语句返回 - 线程意外终止:
run()
方法因为未捕获的异常导致线程终止 - 对某个线程的
Thread
实例调用stop()
方法强制终止
- 线程正常终止:
- 一个线程还可以等待另一个线程直到其运行结束
main
线程在启动t
线程后,通过t.join()
等待t
线程结束后再继续运行- 可以指定等待时间,超过等待时间线程仍然没有结束就不再等待
- 对已经运行结束的线程调用
join()
方法会立刻返回
- 线程状态
中断线程
- 在其他线程中对目标线程调用
interrupt()
方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行 running
标志位来标识线程是否应该继续运行HelloThread
的标志位boolean running
是一个线程间共享的变量线程间共享变量使用
volatile
关键字标记,确保每个线程都能读取到更新后的变量值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}volatile
关键字- 每次访问变量时,总是获取主内存的最新值
- 每次修改变量后,立刻回写到主内存
- 在其他线程中对目标线程调用
守护线程
- 守护线程(Daemon Thread)
- 守护线程是指为其他线程服务的线程
- 调用
start()
方法前,调用setDaemon(true)
把该线程标记为守护线程 - 守护线程不能持有任何需要关闭的资源
- 守护线程(Daemon Thread)
线程同步
通过加锁和解锁的操作,就能保证3条指令总是在一个线程执行期间,不会有其他线程会进入此指令区间
加锁和解锁之间的代码块我们称之为临界区(Critical Section),任何时候临界区最多只有一个线程能执行
synchronized
关键字对一个对象进行加锁1
2
3synchronized(lock) {
n = n + 1;
}- 找出修改共享变量的线程代码块
- 选择一个共享实例作为锁
- 使用
synchronized(lockObject) { ... }
无论是否有异常,都会在
synchronized
结束处正确释放锁不需要synchronized的操作
- JVM规范定义了几种原子操作
- 基本类型(
long
和double
除外)赋值,如:int n = m
- 引用类型赋值,如:
List<String> list = anotherList
- 基本类型(
- JVM规范定义了几种原子操作
多行赋值语句,就必须保证是同步操作
1
2
3
4
5
6
7
8
9
10classPoint {int x;
int y;
publicvoid set(int x,int y) {
synchronized(this) {
this.x = x;
this.y = y;
}
}
}- 写需要同步,读也需要同步
不可变对象无需同步
同步方法
synchronized
逻辑封装1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Counter {
private int count = 0;
public void add(int n) {
synchronized(this) {
count += n;
}
}
public void dec(int n) {
synchronized(this) {
count -= n;
}
}
public int get() {
return count;
}
}
// synchronized锁住的对象是this,即当前实例,使得创建多个Counter实例,互不影响,可以并发执行一个类被设计为允许多线程正确访问,我们就说这个类就是线程安全
thread-safe
java.lang.StringBuffer
- 不变类,例如
String
,Integer
,LocalDate
- 类似
Math
这些只提供静态方法,没有成员变量的类
锁住
this
实例=用synchronized
修饰这个方法synchronized
修饰的方法就是同步方法,表示整个方法都必须用this
实例加锁1
2
3
4
5
6
7
8
9public void add(int n) {
synchronized(this) { // 锁住this
count += n;
} // 解锁
}
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
static
方法添加synchronized
,锁住的是该类的Class
实例1
2
3
4
5
6publicclassCounter {publicstaticvoid test(int n) {
synchronized(Counter.class) {
...
}
}
}
死锁
- 可重入锁
- 同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁
- 每获取一次锁记录+1,每退出
synchronized
块记录-1,减到0时释放锁
- 死锁
- 两个线程各自持有不同的锁,各自试图获取对方的锁,造成了双方无限等待下去
- 死锁发生后,只能强制结束JVM进程
- 线程获取锁的顺序要一致
- 可重入锁
**wait**
和**notify**
wait
线程协调运行
- 当条件不满足时,线程进入等待状态
- 当条件满足时,线程被唤醒继续执行任务
wait()
方法必须在当前获取的锁对象上调用,这里获取的是this
锁1
2
3
4
5
6
7
8public synchronized String getTask() {
while (queue.isEmpty()) {
// 释放this锁:
this.wait();
// 重新获取this锁
}
return queue.remove();
}
notify
wait()
方法返回时需要重新获得this
锁1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54public class Main {
public static void main(String[] args) throws InterruptedException {
var q = new TaskQueue();
var ts = new ArrayList<Thread>();
for (int i=0; i<5; i++) {
var t = new Thread() {
public void run() {
// 执行task:
while (true) {
try {
String s = q.getTask();
System.out.println("execute task: " + s);
} catch (InterruptedException e) {
return;
}
}
}
};
t.start();
ts.add(t);
}
var add = new Thread(() -> {
for (int i=0; i<10; i++) {
// 放入task:
String s = "t-" + Math.random();
System.out.println("add task: " + s);
q.addTask(s);
try { Thread.sleep(100); } catch(InterruptedException e) {}
}
});
add.start();
add.join();
Thread.sleep(100);
for (var t : ts) {
t.interrupt();
}
}
}
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
ReentrantLock
java.util.concurrent.locks
包- 提供的
ReentrantLock
用于替代synchronized
加锁 ReentrantLock
是可重入锁- 和
synchronized
一样,一个线程可以多次获取同一个锁 - 和
synchronized
不同的是,ReentrantLock
可以尝试获取锁
- 和
- 提供的
Condition
Condition
对象来实现
ReentrantLock
和wait
、notify
使用
Condition
- 引用的
Condition
对象必须从Lock
实例的newCondition()
返回,获得一个绑定Lock
实例的Condition
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}- 引用的
Condition
提供的await()
、signal()
、signalAll()
synchronized
锁对象的wait()
、notify()
、notifyAll()
await()
会释放当前锁,进入等待状态signal()
会唤醒某个等待线程signalAll()
会唤醒所有等待线程- 唤醒线程从
await()
返回后需要重新获得锁
ReadWriteLock
ReadWriteLock
- 只允许一个线程写入(其他线程既不能写入也不能读取)
- 没有写入时,多个线程允许同时读(提高性能)
StampedLock
- 读的过程中也允许获取写锁后写入
Semaphore
- 保证同一时间最多N个线程访问受限资源
**
Concurrent
**集合java.util.concurrent
包也提供了对应的并发集合类interface non-thread-safe thread-safe List ArrayList CopyOnWriteArrayList Map HashMap ConcurrentHashMap Set HashSet / TreeSet CopyOnWriteArraySet Queue ArrayDeque / LinkedList ArrayBlockingQueue / LinkedBlockingQueue Deque ArrayDeque / LinkedList LinkedBlockingDeque
Atomic
- Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问
java.util.concurrent.atomic
提供的原子操作可以简化多线程编程- 原子操作实现了无锁的线程安全
- 适用于计数器,累加器等
线程池
线程池:接收大量小任务并进行分发处理
ExecutorService
接口表示线程池1
2
3
4
5
6
7
8// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);常用实现类
- FixedThreadPool:线程数固定的线程池
- CachedThreadPool:线程数根据任务动态调整的线程池
- SingleThreadExecutor:仅单线程执行的线程池
ScheduledThreadPool
- 定期反复执行
**Future**
- 对线程池提交一个
Callable
任务,可以获得一个Future
对象 - 可以用
Future
在将来某个时刻获取结果
- 对线程池提交一个
**CompletableFuture**
- 可以指定异步处理流程
thenAccept()
处理正常结果exceptional()
处理异常结果thenApplyAsync()
用于串行化另一个CompletableFuture
anyOf()
和allOf()
用于并行化多个CompletableFuture
- 可以指定异步处理流程
ForkJoin
- Fork/Join线程池:把一个大任务拆成多个小任务并行执行
- 基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果
ForkJoinPool
线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask
或RecursiveAction
- 使用Fork/Join模式可以进行并行计算以提高效率
ThreadLocal
ThreadLocal
- 表示线程的“局部变量”,确保每个线程的
ThreadLocal
变量都是各自独立的 - 适合在一个线程的处理流程中保持上下文(避免同一参数在所有方法中传递)
- 使用
ThreadLocal
要用try ... finally
结构,并在finally
中清除
- 表示线程的“局部变量”,确保每个线程的
虚拟线程
- 为了解决IO密集型任务的吞吐量,它可以高效通过少数线程去调度大量虚拟线程
函数式编程
- **
Lambda
**基础函数式编程
Functional Programming
把函数作为基本运算单元、变量,可以接收函数、返回函数
Lambda表达式
调用
Arrays.sort()
时传入一个Comparator
实例1
2
3
4
5
6
7
8
9
10
11import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, (s1, s2) -> {
return s1.compareTo(s2);
});
System.out.println(String.join(", ", array));
}
}Lambda表达式
1
2
3(s1, s2) -> {
return s1.compareTo(s2);
}- 参数是
(s1, s2)
-> { ... }
表示方法体- 参数类型可以省略,编译器自动推断出
String
类型
- 参数是
单方法接口,即一个接口只定义了一个方法,如
Comparator
定义了单方法的接口称之为
FunctionalInterface
,用注解@FunctionalInterface
标记Callable
接口1
2
3
4
public interfaceCallable<V> {
V call()throws Exception;
}Comparator
接口1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj);
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
default Comparator<T> thenComparing(Comparator<? super T> other) {
...
}
...
}Comparator
接口只有一个抽象方法int compare(T o1, T o2)
- 其他的方法都是
default
方法或static
方法 boolean equals(Object obj)
是Object
定义的方法,不算在接口方法内- 因此,
Comparator
也是一个FunctionalInterface
- 方法引用
指如果某个方法签名和接口恰好一致,就可以直接传入方法引用
在
Arrays.sort()
中直接传入了静态方法cmp
的引用,用Main::cmp
表示1
2
3
4
5
6
7
8
9
10
11
12
13import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, Main::cmp);
System.out.println(String.join(", ", array));
}
static int cmp(String s1, String s2) {
return s1.compareTo(s2);
}
}引用实例方法
1
2
3
4
5
6
7
8
9import java.util.Arrays;
public class Main {
public static void main(String[] args) {
String[] array = new String[] { "Apple", "Orange", "Banana", "Lemon" };
Arrays.sort(array, String::compareTo);
System.out.println(String.join(", ", array));
}
}其中,
String.compareTo()
实例方法1
2
3
4
5
6
7public final class String {
public int compareTo(String o) {
...
}
}
// String类的compareTo()方法在实际调用的时候,第一个隐含参数总是传入this,相当于静态方法:
public static int compareTo(String this, String o);
引用构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Bob", "Alice", "Tim");
List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
System.out.println(persons);
}
}
class Person {
String name;
public Person(String name) {
this.name = name;
}
public String toString() {
return "Person:" + this.name;
}
}
FunctionalInterface
- 不强制继承关系,不需要方法名称相同
- 方法参数(类型和数量)与方法返回类型相同,即认为方法签名相同
- 使用**
Stream
**Stream API
位于
java.util.stream
包,代表的是任意Java对象的序列不同于
java.io
java.io java.util.stream 存储 顺序读写的byte或char 用途 序列化至文件或网络 List
是操作一组已存在的Java对象,而Stream
实现的是惰性计算java.util.List java.util.stream 元素 已分配并存储在内存 可能未分配,实时计算 用途 操作一组已存在的Java对象 惰性计算 可以“存储”有限个或无限个元素
转换为另一个
Stream
,而不是修改原Stream
本身(只存储了转换规则)惰性计算
1
2
3
4Stream<BigInteger> naturals = createNaturalStream(); // 不计算
Stream<BigInteger> s2 = naturals.map(BigInteger::multiply); // 不计算
Stream<BigInteger> s3 = s2.limit(100); // 不计算
s3.forEach(System.out::println); // 计算链式
1
2
3
4
5int result = createNaturalStream() // 创建Stream
.filter(n -> n % 2 == 0) // 任意个转换
.map(n -> n * n) // 任意个转换
.limit(100) // 任意个转换
.sum(); // 最终计算结果
创建**
Stream
**Stream.of()
Stream<String> stream = Stream.of("A", "B", "C", "D");
基于数组或Collection
1
2
3
4
5
6
7
8
9
10
11
12
13import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
// 数组Arrays.stream()
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
//Collection调用stream()
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
}基于Supplier
Stream.generate()
方法,传入一个Supplier
对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import java.util.function.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
// 无限序列必须先变成有限序列再打印
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
其他
Files
类的lines()
方法把一个文件变成一个Stream
,每个元素代表文件一行内容- 正则表达式的
Pattern
对象有一个splitAsStream()
方法,把一个长字符串分割成Stream
序列而不是数组
基本类型
IntStream
、LongStream
和DoubleStream
使用基本类型的
Stream
,目的是提高运行效率1
2
3
4// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(newint[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
使用**
map
**Stream.map()
map()
方法接收的对象是Function
接口对象它定义了一个
apply()
方法,负责把一个T
类型转换成R
类型:1
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
其中,
Function
的定义1
2
3
4
publicinterfaceFunction<T,R> {// 将T类型转换为R:
R apply(T t);
}
数学计算、字符串操作、任何Java对象
1
2
3
4
5
6
7
8
9
10
11
12import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List.of(" Apple ", " pear ", " ORANGE", " BaNaNa ")
.stream()
.map(String::trim) // 去空格
.map(String::toLowerCase) // 变小写
.forEach(System.out::println); // 打印
}
}
Stream.filter()
filter()
方法接收的对象是Predicate
接口对象,它定义了一个test()
方法,负责判断元素是否符合条件1
2
3
publicinterfacePredicate<T> {// 判断元素t是否符合条件:boolean test(T t);
}常用于数值、任何Java对象
- 从一组给定的
LocalDate
中过滤掉工作日,以便得到休息日
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import java.time.*;
import java.util.function.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(ldt -> ldt.getDayOfWeek() == DayOfWeek.SATURDAY || ldt.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
}
}
class LocalDateSupplier implements Supplier<LocalDate> {
LocalDate start = LocalDate.of(2020, 1, 1);
int n = -1;
public LocalDate get() {
n++;
return start.plusDays(n);
}
}- 从一组给定的
使用**
reduce
**Stream.reduce()
map()
和filter()
都是Stream
的转换方法Stream.reduce()
则是Stream
的一个聚合方法,把一个Stream
的所有元素按照聚合函数聚合成一个结果聚合方法会立刻对
Stream
进行计算,对一个Stream
做聚合计算后,结果就不是一个Stream
,而是一个其他的Java对象reduce()
方法传入的对象是BinaryOperator
接口,它定义了一个apply()
方法,负责把上次累加的结果和本次的元素进行运算,并返回累加的结果1
2
3
4
publicinterfaceBinaryOperator<T> {// Bi操作:两个输入,一个输出
T apply(T t, T u);
}
输出集合
输出为List
- 调用
collect()
并传入Collectors.toList()
对象实际上是一个
Collector
实例,通过类似reduce()
的操作,把每个元素添加到一个收集器中(实际上是ArrayList
)1
2
3
4
5
6
7
8
9
10import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
}
}
- 调用
输出为数组
调用
toArray()
方法,并传入数组的“构造方法”1
2
3
4List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
// 构造方法是String[]::new,签名实际上是IntFunction<String[]>定义的String[] apply(int)
// 即传入int参数,获得String[]数组的返回值
输出为Map
指定两个映射函数,分别把元素映射为key和value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
// 把元素s映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
分组输出
Collectors.groupingBy()
提供两个函数- 一分组的key,这里使用
s -> s.substring(0, 1)
,表示只要首字母相同的String
分到一组 - 分组的value,这里直接使用
Collectors.toList()
,表示输出为List
1
2
3
4
5
6
7
8
9
10
11import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
}- 一分组的key,这里使用
其他
排序
调用
sorted()
方法,要求Stream
的每个元素必须实现Comparable
接口如果要自定义排序,传入指定的
Comparator
1
2
3
4List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());sorted()
只是一个转换操作,它会返回一个新的Stream
去重
distinct()
1
2
3
4List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList());// [A, B, C, D]
截取
- 截取操作常用于把一个无限的
Stream
转换成有限的Stream
skip()
用于跳过当前Stream
的前N个元素limit()
用于截取当前Stream
最多前N个元素1
2
3
4
5List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2)// 跳过A, B
.limit(3)// 截取C, D, E
.collect(Collectors.toList());// [C, D, E]截取操作也是一个转换操作,将返回新的
Stream
- 截取操作常用于把一个无限的
合并
将两个
Stream
合并为一个Stream
使用Stream
静态方法concat()
1
2
3
4
5Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
flatMap
Stream
转换为Stream<Integer>
,使用flatMap()
1
2Stream<Integer> i = s.flatMap(list -> list.stream());
把
Stream
的每个元素(这里是List
)映射为Stream
,合并成一个新Stream
并行
1
2
3
4Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);其他聚合方法
- 除了
reduce()
和collect()
外count()
:用于返回元素个数max(Comparator<? super T> cp)
:找出最大元素min(Comparator<? super T> cp)
:找出最小元素
- 针对
IntStream
、LongStream
和DoubleStream
sum()
:对所有元素求和average()
:对所有元素求平均数
- 用来测试
Stream
的元素是否满足以下条件boolean allMatch(Predicate<? super T>)
:是否所有元素均满足测试条件boolean anyMatch(Predicate<? super T>)
:是否至少一个元素满足测试条件
forEach()
- 循环处理
Stream
的每个元素,System.out::println
打印Stream
的元素
- 循环处理
- 除了
- Title: Java
- Author: Murphy Lee
- Created at : 2023-07-01 10:01:27
- Updated at : 2023-12-29 15:18:46
- Link: https://redefine.ohevan.com/2023/07/01/Java/
- License: This work is licensed under CC BY-NC-SA 4.0.