浮点数

其表示形式为:

$$
\text{value} = (-1)^S \times 1.M \times 2^{(E-1023)}
$$

M是二进制小数,省略了前导 1 节省空间。

E用于移动小数点

十进制到二进制转换

整数:除2取余,相当于移动小数点 向左,或数值 右移

小数: 乘2取余,相当于移动小数点 向右,或数值 左移

运算

优先级 运算符 结合性
1 ( ) 从左向右
2 ~ - + (强制类型转换) ++ – 从右向左
3 * / % 从左向右
4 + - 从左向右
5 << >> >>> 从左向右
6 > < >= >= 从左向右
7 == != 从左向右
8 & 从左向右
9 ^ 从左向右
10 | 从左向右
11 && 从左向右
12 || 从左向右
13 ? : 从右向左
14 = += -= *= /= %= &=|= ^= <<= >>= >>>= 从右向左

运算符结合性

右结合

1
2
3
a = b = c
// 等效于
a = (b = c)

左结合

1
2
3
a + b + c
// 等效于
(a + b) + c

强制类型转换

如果空间不够会发生截断

访问权限

当前类 同一个包下的类 不同包下的子类 不同包下的类
public
protected
默认
private

instanceof 返回bool,判断是否是对应类/子类

所有class继承自Object

abstract 抽象,相当于C++的纯虚函数,可以声明 抽象类、抽象接口

[!Note]

你甚至可以将构造函数 private ,让调用者必须通过你public的static方法创建实例

接口

interface 默认定义的方法为public abstract 可以省略

接口用implements 实现,类用extends 继承

接口可以通过extends 继承其他接口

接口用default 可以默认实现方法

Cloneable接口

1
2
3
4
@Override
public Object clone() throws CloneNotSupportedException { //提升clone方法的访问权限
return super.clone(); //因为底层是C++实现,我们直接调用父类的实现就可以了
}

注意为 浅拷贝

枚举类

关键字 enum

1
2
3
4
5
6
7
8
9
10
11
12
public enum Status {
RUNNING("跑步"), STUDYING("学习")

private final String name;
public Status(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

包装类

5c3a6a27-6370-4c60-9bbc-8039e11e752d

特殊:BigInteger BigDecimal

BigInteger 主要使用一个 int[] 数组来存储数值,而 BigDecimal 结合了 BigInteger 和标度来表示高精度的定点浮点数。

内部类

成员内部类

image-20220924123600217

内部类的创建需要一个外部类的实例作为载体,如

1
2
3
Test b = new Test("小红");
Test.Inner inner2 = b.new Inner(); //依附于b创建的对象,那么就是b的
inner2.test();

同名成员访问可以使用外部类名和this,如:

1
2
3
4
// 假设都有name成员
name // 形参
this.name // 内部类成员
Outer.this.name // 外部类成员

静态内部类

可以直接newnew Test.Inner
但是在 静态上下文 下,只能访问静态内容

局部内部类

在方法中声明,仅限方法作用域访问

匿名内部类

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Study study = new Study() {
@Override
public void study() {
System.out.println("我是学习方法!");
}
};
study.study();
}

可以实例化一个 接口/类 的子类,在实例化的同时声明这个匿名类相关的成员。
通常用于 抽象接口/类,临时使用对其进行初始化工作。

如果抽象类/接口 只有一个 抽象方法,可以用lambda表达式简写。

1
2
3
4
public static void main(String[] args) {
Study study = () -> System.out.println("我是学习方法!"); //是不是感觉非常简洁!
study.study();
}
1
2
3
4
5
6
7
public interface Study {
int sum(int a, int b); //待实现的求和方法
}
public static void main(String[] args) {
Study study = (a, b) -> Integer.sum(a, b); //直接使用Integer为我们通过好的求和方法
System.out.println(study.sum(10, 20));
}

使用 双冒号 进行方法引用,格式为类名::方法名

1
2
3
4
public static void main(String[] args) {
Study study = Integer::sum; //使用双冒号来进行方法引用,静态方法使用 类名::方法名 的形式
System.out.println(study.sum(10, 20));
}

字符串

比较用equals ,常量字符串地址一致,创建不同的用new

Stringchar[]可以互转,用toCharArray()

StringBuilder 进行编辑操作。

数组

声明:type[] = new type[len]{......}

数组 不支持 自动装/拆箱

Lambda表达式

语法(类似箭头函数)

1
([type arg1]...) -> {body} | exp

利用lambda表达式作为抽象方法实现

异常

基类:Excpetion

运行时异常:RuntimeExcpetion

抛出

语法thorw (exp实例)

编译器强制调用者显式处理这些异常(通过 try-catch 或继续使用 throws 声明)

一般来说,必须抛出受检异常。比如I/O操作可能因为外部原因失败,这类异常必须被处理。

异常捕获

1
2
3
4
5
6
7
8
9
try {

} catch (Excpetion1 | Excpetion2 ...) {

} catch (Throwable ...) {

} finally {

}

注意catch按顺序执行,匹配项后面的catch不会被执行。

finally 为始终执行的语句

断言

1
2
3
4
assert (exp) : (msg)
// 如
int a = 10;
assert a > 10 : "我是错误信息";

如果exp == false 程序将throw AssertionExcpetion

断言用于测试。

常用工具类

数学工具类

方法名 例子 输出
Math.abs(int a) Math.abs(-5) 5
Math.max(int a, int b) Math.max(3, 7) 7
Math.min(int a, int b) Math.min(3, 7) 3
Math.pow(double a, double b) Math.pow(2, 3) 8.0
Math.sqrt(double a) Math.sqrt(16) 4.0
Math.random() Math.random() 0.01.0 之间的一个随机数
Math.round(float a) Math.round(5.5f) 6
Math.ceil(double a) Math.ceil(5.3) 6.0
Math.floor(double a) Math.floor(5.7) 5.0
Math.sin(double a) Math.sin(Math.PI / 2) 1.0

数组工具类

多维数组的方法大多带deep前缀

方法名 例子 输出
Arrays.sort(int[] a) int[] arr = {5, 3, 8}; Arrays.sort(arr); [3, 5, 8]
Arrays.binarySearch(int[] a, int key) int[] arr = {1, 3, 5, 7}; Arrays.binarySearch(arr, 5); 2
Arrays.fill(int[] a, int val) int[] arr = new int[3]; Arrays.fill(arr, 7); [7, 7, 7]
Arrays.equals(int[] a, int[] b) int[] arr1 = {1, 2, 3}; int[] arr2 = {1, 2, 3}; Arrays.equals(arr1, arr2); true
Arrays.toString(int[] a) int[] arr = {1, 2, 3}; Arrays.toString(arr); [1, 2, 3]
Arrays.asList(T... a) String[] arr = {"a", "b"}; List<String> list = Arrays.asList(arr); [a, b]
Arrays.copyOf(int[] original, int newLength) int[] arr = {1, 2, 3}; int[] newArr = Arrays.copyOf(arr, 5); [1, 2, 3, 0, 0]
Arrays.copyOfRange(int[] original, int from, int to) int[] arr = {1, 2, 3, 4}; int[] newArr = Arrays.copyOfRange(arr, 1, 3); [2, 3]
Arrays.deepToString(Object[] a) int[][] arr = {{1, 2}, {3, 4}}; Arrays.deepToString(arr); [[1, 2], [3, 4]]
Arrays.stream(int[] array) int[] arr = {1, 2, 3}; int sum = Arrays.stream(arr).sum(); 6

泛型

泛型类

泛型没办法存放基本类型,只能用引用类型替代。

1
2
3
4
5
6
7
public static void main(String[] args) {
Test<int> test = new Test<>(); // 不行
Test<Integer> test = new Test<>(); // 用Integer替代

// 当然数组可以,因为数组是引用类型。
Test<int[]> test = new Test<>();
}

泛型在进行实现或继承时,子类可以选择保持泛型或指定类型

1
2
static class A extends B<String> {...} // 确定类型
class A<T> implements B<T> {...} // 保持泛型

泛型方法

1
public <T> T test(T a)  // 需要放在返回类型 前面

泛型的界限

img
1
2
<T extends Number> // 限定T为number或其子类
<? extends Integer> // 泛型通配符也支持

在对通配符使用上界后,对应的类型会变成 上界 的类型

下界

泛型的下界只适用于通配符,使用super 关键字

4aa52791-73f4-448f-bab3-9133ea85d850.jpg
1
Score<? super Number> score = new Score<>("数据结构与算法基础", "EP074512", 10);

类型擦除

实际上,泛型是通过强制类型转换,以及Object的使用完成的。

而子类对父类泛型方法的重写,是通过编译器创建一个中转(桥接)方法,调用子类的“新”“重写”方法完成的。

这一对早期版本的兼容,造成了如下限制:

  • instanceof 不能判断具体的泛型

    1
    2
    a instanceof Test<String> // 不行,不支持泛型判断
    a instanceof Test // 可以
  • 不支持泛型数组:

    1
    2
    3
    4
    5
    Test<String>[] test = new Test<String>[19]; // 不行
    Test[] test = new Test[19]; // 可以

    // 不过可以这样代替
    Test<String>[] test = new Test[19];

函数式接口

语法为

1
@FuntionalInterface // 装饰器

这些接口是专用于Lambda表达式的接口,以下介绍主要四个:

Supplier供给型接口

1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
T get(); // 实现此方法
}

例子:供给学生

1
2
3
4
5
6
//专门供给Student对象的Supplier
private static final Supplier<Student> STUDENT_SUPPLIER = Student::new;
public static void main(String[] args) {
Student student = STUDENT_SUPPLIER.get();
student.hello();
}

Consumer消费型接口

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); //这个方法就是用于消费的,没有返回值

default Consumer<T> andThen(Consumer<? super T> after) { //这个方法便于我们连续使用此消费接口
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}

andThen的链式调用会返回一个新的Consumer,它会顺序执行传入的accept内容。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
//专门消费Student对象的Consumer
private static final Consumer<Student> STUDENT_CONSUMER = student -> System.out.println(student+" 真好吃!");
public static void main(String[] args) {
Student student = new Student();
STUDENT_CONSUMER.accept(student);


STUDENT_CONSUMER //我们可以提前将消费之后的操作以同样的方式预定好
.andThen(stu -> System.out.println("我是吃完之后的操作!"))
.andThen(stu -> System.out.println("好了好了,吃饱了!"))
.accept(student); //预定好之后,再执行
}

Function函数型接口

这个接口消费一个对象,然后会向外供给一个对象(前两个的融合体)

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
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); //这里一共有两个类型参数,其中一个是接受的参数类型,还有一个是返回的结果类型

default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

static <T> Function<T, T> identity() {
return t -> t;
}
}

public class FunctionExample {

// Function接口,接受一个Integer,返回一个String
private static final Function<Integer, String> INTEGER_STRING_FUNCTION = Object::toString;

public static void main(String[] args) {

// 使用apply方法,将传入的int参数转换为字符串
String str = INTEGER_STRING_FUNCTION.apply(10);
System.out.println(str); // 输出: 10

// 使用compose方法,将指定函数式的结果作为当前函数式的实参
// 首先执行传入的lambda表达式 (String s) -> s.length() ,将字符串"lbwnb"转换为其长度5
// 然后执行INTEGER_STRING_FUNCTION,将5转换为字符串"5"
String composedStr = INTEGER_STRING_FUNCTION
.compose((String s) -> s.length()) // 传入一个Function<String, Integer>
.apply("lbwnb"); // 执行compose中的lambda表达式,返回字符串长度,再作为INTEGER_STRING_FUNCTION的输入
System.out.println(composedStr); // 输出: 5

// 使用andThen方法,将当前函数式的返回值进行进一步处理
// 首先执行INTEGER_STRING_FUNCTION,将10转换为字符串"10"
// 然后执行String::isEmpty方法,判断字符串"10"是否为空,返回false
Boolean result = INTEGER_STRING_FUNCTION
.andThen(String::isEmpty) // 传入一个Function<String, Boolean>
.apply(10); // 执行INTEGER_STRING_FUNCTION得到"10",再作为andThen中lambda表达式的输入
System.out.println(result); // 输出: false

// 使用identity方法,返回传入参数的原样
Function<String, String> identityFunction = Function.identity(); // 原样返回
String identityStr = identityFunction.apply("不会吧不会吧,不会有人听到现在还是懵逼的吧");
System.out.println(identityStr); // 输出: 不会吧不会吧,不会有人听到现在还是懵逼的吧
}
}

Predicate断言型函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); //这个方法就是我们要实现的

default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}

default Predicate<T> negate() {
return (t) -> !test(t);
}

default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}

static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PredicateExample {

public static void main(String[] args) {
Predicate<String> isHello = Predicate.isEqual("Hello");

System.out.println(isHello.test("Hello")); // 输出: true
System.out.println(isHello.test("World")); // 输出: false

Predicate<String> isNull = Predicate.isEqual(null);

System.out.println(isNull.test(null)); // 输出: true
System.out.println(isNull.test("Hello")); // 输出: false
}
}

判空包装

语法

1
2
3
Optional.ofNullable(exp)
.ifPresent( exp -> "非空") // lambda
.orElse( exp -> "为空") // lambda

例子

1
2
3
4
5
6
7
8
9
10
private static void test(String str){
Optional
.ofNullable(str) //将传入的对象包装进Optional中
.ifPresent(s -> System.out.println("字符串长度为:"+s.length()));
//如果不为空,则执行这里的Consumer实现
.orElse("备选方案")

// 原本需要进行判空,现在更优雅了
if (str == null) return;
}
1
2
3
4
Integer i = Optional
.ofNullable(str)
.map(String::length) //使用map来进行映射,将当前类型转换为其他类型,或者是进行处理
.orElse(-1);

数据结构

平衡二叉树AVL

目的:解决二叉查找树的痛点。

image-20220815113242191

平衡因子(Balance Factor):左子树高度 - 右子树高度

image-20220815210652973

失衡调整:失衡分为4种类型,LL LR RL RR

image-20220815115836604

在线动画地址

简记:

正为L,负为R

image-20240726161707869

[!Important]

以下所有情况为插入情况,如果是删除请依次检查父节点的失衡情况!

1.LL右旋

条件:失衡节点 平衡因子>1,左孩子 平衡因子=1

首先通过平衡因子计算,找到最小不平衡子树:

image-20240726161053776

进行右旋操作,即将中间节点作为根节点。

image-20240726161113563

2.RR左旋

条件:失衡节点 平衡因子<-1,右孩子 平衡因子=-1

和上面一样,中间节点作为根节点。

image-20220815214408651

3.RL右左

条件:失衡节点 平衡因子<-1,右孩子 平衡因子=1

要插入的是16,通过计算得到最小不平衡子树15 20 17

image-20220815214859501

子树的形状是RL。

image-20220815215929303

4.LR左右

条件:失衡节点 平衡因子>1,左孩子 平衡因子=-1

如图

image-20220815220609357

子树形状LR

image-20220815221349044

红黑树RedBlack

在线动画

image-20220815222810537

  • 规则1:是二叉搜索树
  • 规则2:根结点和叶子节点(NULL)一定是黑色。
  • 规则3:红色结点的父结点和子结点不能为红色,也就是说不能有两个连续的红色
  • 规则4:任一节点到叶节点所有路径的黑色节点数量相同

具有以下性质:

  • 1.最长路径不超过最短路径的两倍
    因为最短路径为 全黑节点,最长路径为 黑红交替节点
    也就是 任一节点左右子树高度差不超过两倍
  • 2.插入节点必定是 红色
    插入节点为黑色,必然导致路径的黑色数量不匹配

插入策略

  • 插入根节点 —— 变黑

当违反了 红红 原则时,应用:

  • 插入点叔叔为红 —— 叔叔父亲爷爷变色,从爷爷继续判定
  • 插入点叔叔为黑(NULL/黑)—— (LL,LR,RL,RR)旋转,然后 旋转点,中心点 变色

删除策略

核心:保持黑路同

下称经过该节点会减少一个黑色计数的节点为 缺失节点

  • 无子节点(叶节点)
    • 红节点——直接删除
    • 黑节点——删除后会路径会少一个黑色
      • 兄弟是黑色
        • 兄弟红孩子 (LL, LR, RL, RR)变色+旋转
        • 兄弟红孩子
      • 兄弟是红色:兄父变色,朝缺失节点旋转。此时缺失节点的兄弟是黑色。
  • 只有左/右子树——代替后变黑
    该情况只可能存在 黑父-红子,因为 黑路同不红红 两条规则
  • 都有左右子树——寻找直接前驱/后继,代替后,转移到直接前驱/后继进行删除处理。
[兄弟是黑色]兄弟有红孩子

此处r为红节点,s为兄弟节点,p为父节点(相对于删除节点)

按如下策略变色+旋转后,即可结束。

  1. LL RR型
    变色:r->s, s->p, p->黑
  2. LR RL型
    变色:r->p, p->黑
    r->p:旋转后r是s的父节点,应当和原树结构保持一致(不变会破坏p的子树的黑色计数,以及p兄弟的子树的黑色计数)
    p->黑:p会和s当兄弟,而兄弟是黑色,所以P应该是黑色以保持黑色计数一致
[兄弟是黑色]兄弟无红孩子

兄弟变红(使得兄弟路径也减少黑色计数),将缺失节点放在父节点上

  • 父节点为红色/根节点:父节点变黑,结束
    此时,因为兄弟变红、自己被删,以父节点为根的子树的路径同时少了1个黑节点,此时将红转黑即可补上
    如果父节点为根节点,整个树都少了1个黑节点
  • 父节点为黑色:父节点为缺失节点,继续按策略处理

集合类

ArrayList

注意:

最好使用包装类添加/删除元素

有误认为下标的风险

Arrays.asList("A", "b", "c") 使用该工具类的方法可以快速生成一个 只读 列表

Iterator

方法 描述
boolean hasNext() 有下一个
E next()

LinkedList

可以当队列/双端队列使用

内部是由链表实现的

Queue

ArrayDeque 双端队列, PriorityQueue 优先队列

优先队列即堆

Set

和C++ STL的Set一样。

HashSet 底层用 HashMap 是无序的

有序可以使用 LinkedHashSet ,用链表实现

TreeSet 会在插入时按照给定条件排序,默认是升序

Map

方法 描述
put 放入键值对
putIfAbsent 如果无键,才放入
get 获取键,可能得到null
getOrDefault 获取键,或默认值
compute 计算保存新值
computeIfPresent 有Key则计算
computeIfAbsent 无Key则计算
replace 快速替换Key的值/对应Key-Value的值
remove 同上,支持Key和Key-Value匹配

HashMap 的底层使用哈希表维护,当发生的哈希碰撞达到一定阈值后,会转化为红黑树。

LinkedHashMap 底层用链表维护,有序

TreeMap 底层用红黑树维护,创建时给出排序规则即可。

关于Set的实现。

Set的实现基本都是创建一个空Object,作为Value。

然后使用Map保存Key,是很聪明的偷懒做法。

Stream流

image-20221003232832897

List可以通过stream()方法得到流。

流是链式操作,只有执行 collect 终止方法,才会

Collections工具类

这是一个用来操作集合的工具类。

方法 作用
binarySearch(list, val) 二分搜搜
fill(list, )
checked…(list, …) 生成一个集合,会动态检查元素是否全为xx类型
emptyList() 创建只读空集合
unmodifiableList(list) 创建只读集合
indexOfSubList(list, list2) 子集合位置

因为泛型的类型擦除,可能集合内会保存不符合预期的元素,可以使用checked…进行检查。

[!Important]

注意,使用Map和基于Map的类,需要注意重写equals和hashcode

Java I/O

语法糖,避免close的二次处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try (FileInputStream stream = new FileInputStream("......")) {}
// 等价于
FileInputStream stream = null;
try {
stream = new FileInputStream("...");
} catch (e) {
e.printStack();
} finally {
try {
if (stream != null) stream.close();
} catch (e) {
e.printStack();
}
}

常用方法

方法 描述
read(bytes, start, len) 读取,EOF返回-1。不传参默认读1字节
write(bytes, start, len) 写入
skip() 跳过指定字节的内容
available() 查看剩余字节(对于网络IO是预估量)
flush() 刷新缓存区

文件字节流

FileInputStream FileOutputStream

适用于二进制文件读取

文件字符流

类名为 FileReader FileWriter

适用于纯文本文件,可以方便的进行中英文读取,此处注意Java使用UTF-16编码,则一个Char为2字节

不同的是,FileWriterappend()方法可以很方便追加内容,并且支持链式调用

File类

封装了和文件对象有关的方法

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
File file = new File("test.txt"); //直接创建文件对象,可以是相对路径,也可以是绝对路径
System.out.println(file.exists()); //此文件是否存在
System.out.println(file.length()); //获取文件的大小
System.out.println(file.isDirectory()); //是否为一个文件夹
System.out.println(file.canRead()); //是否可读
System.out.println(file.canWrite()); //是否可写
System.out.println(file.canExecute()); //是否可执行
}
1
2
3
4
5
File file = new File("/");
System.out.println(Arrays.toString(file.list())); //快速获取文件夹下的文件名称列表
for (File f : file.listFiles()){ //所有子文件的File对象
System.out.println(f.getAbsolutePath()); //获取文件的绝对路径
}

File类可以作为文件流的传入参数

缓冲类

将磁盘文件暂存到内存中,在频繁读取时提升访问速度。

image-20221004125755217

缓冲字节流

BufferedInputStream

支持 reset()mark(limit)

mark() 会保存当前位置之后的limit个字符,使用reset会将读取位置重置到mark的位置

实际上,保存的内容大小是max(buffer_len, limit)

缓冲字符流

**BufferedReader **

传入一个Reader对象使用

可以按行读取得到字符串

1
2
3
4
5
6
7
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){
System.out.println(reader.readLine()); //按行读取
}catch (IOException e) {
e.printStackTrace();
}
}

可以用lines()得到一个Stream<String>,然后进行流操作

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){
reader
.lines()
.limit(2)
.distinct()
.sorted()
.forEach(System.out::println);
}catch (IOException e) {
e.printStackTrace();
}
}

BufferedWriter

特殊API如下:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
try (BufferedWriter reader = new BufferedWriter(new FileWriter("output.txt"))){
reader.newLine(); //使用newLine进行换行
reader.write("汉堡做滴彳亍不彳亍"); //可以直接写入一个字符串
reader.flush(); //清空缓冲区
}catch (IOException e) {
e.printStackTrace();
}
}

转换类

InputStreamReader 和 OutputStreamWriter

可以传入字节流,变为字符流。

打印流

示意图

img

System.out就是一个打印流

Scanner

对应的,拥有一个扫描类Scanner。

可以扫描其他输入流

1
2
3
4
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); //系统输入流,默认是接收控制台输入
Scanner scanner = new Scanner(new FileInputStream("秘制小汉堡.txt")); //将文件内容作为输入流进行扫描
}

数据流

**DataInputStream **和 **DataOutputStream **

支持基本数据类型的读取和写入, 写入二进制数据

1
2
System.out.println(dataInputStream.readBoolean());   //直接将数据读取为任意基本数据类型
dataOutputStream.writeBoolean(false);

对象流

ObjectOutputStreamObjectInputStream

可以对实现了 Serializable 可序列化接口的类,进行二进制保存

1
2
3
4
5
 People people = new People("lbw");
outputStream.writeObject(people);
outputStream.flush();
people = (People) inputStream.readObject();
System.out.println(people.name);

对应的类在实现Serializable的时候,会添加版本标识,确保类改变后不会被错误的从文件反序列化

对于不想进行序列化的成员可以使用 transient 关键字标识

1
2
3
4
5
6
7
8
9
static class People implements Serializable{
private static final long serialVersionUID = 123456; //在序列化时,会被自动添加这个属性,它代表当前类的版本,我们也可以手动指定版本。

String name;
transient int age;
public People(String name){
this.name = name;
}
}

多线程

创建和启动

通过Thread类实现,传入一个Runnable接口,可以直接用lambda函数

1
2
3
Thread t1 = new Thread(() -> { sout("我是子线程") });
t1.start();

可以通过Thread.currentThread()获取当前进程的上下文

休眠和中断

方法 作用
sleep(ms) 休眠当前线程 单位ms
interrupt() 添加中断标记
interrupted() 去除中断标记
isInterrupted() 是否中断

sleep会抛出InterruptedException ,代表着进行长时间操作时,进程被中断

优先级

  • MIN_PRIORITY 最低优先级
  • MAX_PRIORITY 最高优先级
  • NOM_PRIORITY 常规优先级

通过t.setPriority设置优先级

yield()方法

表示线程建议把资源让给其他线程

join()方法

等待某个线程终止后,再继续运行,是一个阻塞方法

线程锁和线程同步

image-20221004203914215

这是JAVA内存模型,子线程会从主线程中拿相关内存到自己的工作内存,再复制回去。

高速缓存通过保存内存中数据的副本来提供更加快速的数据访问,但是如果多个处理器的运算任务都涉及同一块内存区域,就可能导致各自的高速缓存数据不一致,在写回主内存时就会发生冲突,这就是引入高速缓存引发的新问题,称之为:缓存一致性。

我们可以使用synchronized 关键字解决,将并行(异步)的部分代码,进行同步

synchronized 可以传入一个对象、类实例作为锁。

synchronized 可以作用在方法。

  • 静态方法:锁为类实例
  • 普通方法:锁为对象
1
2
3
synchronized (object) {...} // 方法锁
static int synchronized add() {....} // 类实例锁
int synchronized sub() {...} // 对象锁

在同步代码内,当一个线程拿到锁后,其他线程的同步代码会进行阻塞,确保异步下的资源竞争安全。

死锁

死锁即两个线程互相持有对方所需的锁,并且不愿意释放。

image-20221004205058223

可以通过jconsole检查死锁。

wait()和notify()

是属于 Object 的方法,只有 同步代码,可以调用该方法-

wait() notify() notifyAll() 需要配合Synchronized 使用。

不同的是,notify会随机唤醒一个进程,notifyAll会唤醒全部。

注意

当进程被wait的时候,会释放拥有的所有锁。

ThreadLocal

可以使用ThreadLocal类将线程的专用变量存到线程的专用内存了,不同的线程访问ThreadLocal对象时,只能获取到当前线程所属的变量。

1
2
3
ThreadLocal<String> str = new ThreadLocal<>();
str.set("你干嘛")
str.get()

InheritableThreadLocal 可以让线程的子线程获取到父线程的变量

1
2
3
4
5
InheritableThreadLocal<String> str = new InheritableThreadLocal<>();
str.set("哈哈哎哟");
Thread t1 = new Thread(() -> {
sout("我能获取到" + str.getI())
})

定时器

我们可以使用Timer类来执行定时任务。

Timer类的实例化需要传入一个TimerTask为参数,可惜这是一个抽象类非接口,就无法使用lambda表达式

1
2
3
4
5
6
7
Timer timer = new TImer(new TimerTask(){
@override
public void run() {
// doSomething.............
}
}, sleep, period);
timer.cancel();

注意,timer必须通过 cancel() 方法取消,否则他会持续等待新的任务,不会关闭线程。

timer内部通过维护一个任务队列来实现消息通信。

守护线程

守护线程会在其他线程结束后,再结束

守护线程必须在其开始之前设置

1
2
3
Thread t = new Thread();
t.setDaemon(true); // 设置为守护线程
t.start(); // 开始之前设置

多线程下的集合类

数据 介绍
spliterator() 获取可拆分迭代器(用于多线程)
parallelStream() 获取并行流
forEachOrdered() 在并行流下进行单线程(默认的forEach是多线程)
Arrays.parallelSort(arr) 并行排序

以后在多线程环境使用集合类,就需要注意加锁避免资源竞争了。

反射

Java类加载机制

image-20221004213335479

在Java程序启动时,JVM会将一部分类(class文件)先加载(并不是所有的类都会在一开始加载),通过ClassLoader将类加载,在加载过程中,会将类的信息提取出来(存放在元空间中,JDK1.8之前存放在永久代),同时也会生成一个Class对象存放在内存(堆内存),注意此Class对象只会存在一个,与加载的类唯一对应!

为了方便各位小伙伴理解,你们就直接理解为默认情况下(仅使用默认类加载器)每个类都有且只有一个唯一的Class对象存放在JVM中,我们无论通过什么方式访问,都是始终是那一个对象。Class对象中包含我们类的一些信息,包括类里面有哪些方法、哪些变量等等。

Class类对象

可以通过以下三种方法获取

  • 类名.class
  • 对象.getClass();
  • Class.forName("包名");
1
2
3
4
5
public static void main(String[] args) throws ClassNotFoundException {
Class<String> clazz = String.class; //使用class关键字,通过类名获取
Class<?> clazz2 = Class.forName("java.lang.String"); //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
Class<?> clazz3 = new String("cpdd").getClass(); //通过实例对象获取
}

不过只有第一种能获取到具体的Class,其他两种都是用通配符作为返回值。

基本类型与包装类

基本类型也可以获取class,也可以通过其对应的包装类获取class

1
2
3
int.class // 直接获取
Integer.TYPE // 通过包装类获取
int.class == Integer.TYPE // 返回true

注意:包装类的class与基本类型的class是不一样

1
Integer.class != int.class // 返回true

数组也是一种类型,可以获取数组的class

1
2
3
4
5
6
7
8
public static void main(String[] args) {
Class<String[]> clazz = String[].class;
System.out.println(clazz.getName()); //获取类名称(得到的是包名+类名的完整名称)
System.out.println(clazz.getSimpleName());
System.out.println(clazz.getTypeName());
System.out.println(clazz.getClassLoader()); //获取它的类加载器
System.out.println(clazz.cast(new Integer("10"))); //强制类型转换
}

Class对象与多态

一些常用的Class类方法

方法 描述
asSubClass(Class<>); 进行类型转换,看看该类是否是对应类的子类,失败会异常
getSuperclass(); 获取父类的Class对象,基类返回null
getGenericSuperclass(); 获取父类的泛型类型的Type对象(比如ArrayList<String>)
基类返回null
父类非泛型返回对应的Class对象
getInterfaces(); 获取接口数组
getGenericInterfaces(); 获取泛型接口数组

getGenericSuperclass可能会得到 ParameterizedType、TypeVariable、GenericArrayType或WildcardType

其中,ParameterizedType是参数化类型,即需要传入参数的类型,如List<String>

TypeVariable 是类型变量,比如Student<T> 中的 T

GenericArrayType 是泛型数组,如T[]

WildcardType 是通配符类型,如? extends Student

创建对象

1.直接创建实例

通过Class<>.newInstance()创建调用的是无参构造

如果是private则无权访问

如果不具备无参构造则抛出异常

1
2
Class<Integer> integerClass = int.class;
Integer i = integerClass.newInstance();

该方法已在JAVA 9弃用

2.通过构造器

用法:

1
Constructor <Class<>> c = Class<>.getConstructor(Class<>...)

需要填写构造的参数的Class,如果没有对应的构造函数会抛出异常

可以获取从父类继承的构造函数

可以通过newInstance方法创建实例,需要传入对应的参数。

没有访问权限时

可以通过getDeclaredConstructor 获取

该方法可以获取类所有的构造函数,包括所有权限

不能获取从父类继承的构造函数

1
2
3
4
5
Class<Student> clazz = Student.class;
Constructor<Student> constructor = clazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true); //修改访问权限
Student student = constructor.newInstance("what's up");
student.test();

对应的还有getConstructorsgetDeclaredConstructors方法,会返回构造方法列表

调用方法

1
2
3
Method method = clazz.getMethod("test", String.class);   //通过方法名和形参类型获取类中的方法

method.invoke(instance, "what's up"); //通过Method对象的invoke方法来调用方法

通过getMethod获取public方法,invoke执行

包括从父类和接口继承的方法

需要传递方法名和参数列表

对于可变长参数,可以传入对应的数组类对象

注意的是,invoke需要传入一个类或对象作为执行载体

  • 静态方法:传入类对象
  • 成员方法:传入类实例

获取所有方法

getDeclaredMethod 可以获取所有权限的方法

不包括从父类和接口继承的方法

1
2
3
Method method = clazz.getDeclaredMethod("test", String.class);   //通过方法名和形参类型获取类中的方法
method.setAccessible(true);
method.invoke(instance, "what's up"); //通过Method对象的invoke方法来调用方法

获得字段

1
2
Field field = clazz.getField("i");   //获取类的成员字段i
field.set(instance, 100); //将类实例instance的成员字段i设置为100

通过getField获取public字段,set修改,get获取

同样需要传递类对象或类实例

获取所有字段

getDeclaredField 获取所有权限的字段

不包括从父类继承的字段

1
2
Field modifiersField = Field.class.getDeclaredField("modifiers");  //这里要获取Field类的modifiers字段进行修改
modifiersField.setAccessible(true);

修改final字段

如果要修改final字段,可以去除final修饰符再进行修改

field.getModifiers()&~Modifier.FINAL

1
2
3
Field modifiersField = Field.class.getDeclaredField("modifiers");  //这里要获取Field类的modifiers字段进行修改
modifiersField.setAccessible(true);
modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL); //去除final标记

类加载器

双亲委派机制

img

每个类的加载会通过三个加载器进行加载,每个加载器会将加载委托给自己的父加载器,进行递归委托加载。

这确保了JDK的类不会被篡改,保证了安全性。

手写ClassLoader

可以通过自定义的ClassLoader加载Class文件,用动态的方式从外部(文件或网络)加载类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义一个自己的ClassLoader
static class MyClassLoader extends ClassLoader{
public Class<?> defineClass(String name, byte[] b){
return defineClass(name, b, 0, b.length); //调用protected方法,支持载入外部class文件
}
}

public static void main(String[] args) throws IOException {
MyClassLoader classLoader = new MyClassLoader();
FileInputStream stream = new FileInputStream("Test.class");
byte[] bytes = new byte[stream.available()];
stream.read(bytes);
Class<?> clazz = classLoader.defineClass("com.test.Test", bytes); //类名必须和我们定义的保持一致
System.out.println(clazz.getName()); //成功加载外部class文件
}

注解

元注解

元注解是作用于注解上的注解,用于我们编写自定义的注解:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

注解的声明

1
2
3
4
@元注解
public @interface name {
String value() default "默认值";
}

当有且只有一个字段value时,在使用注解时可以不用写字段名,直接传值。

字段为数组时,也可以直接传一个值作参数。

1
2
@name("你好") // 数组可以用初始化列表传值,也可以直接传单个元素
@name({"你", "好"})

无论是方法、类、还是字段,都可以使用getAnnotations()方法(还有几个同名的)来快速获取我们标记的注解。