Skip to content

Commit 4ce6bcc

Browse files
committed
docs: 优化 Java 核心知识文档
1 parent 2d652cd commit 4ce6bcc

63 files changed

Lines changed: 765 additions & 806 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/java/basis/bigdecimal.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ System.out.println(a == b);// false
2626

2727
**为什么浮点数 `float``double` 运算的时候会有精度丢失的风险呢?**
2828

29-
这个和计算机保存小数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就解释了为什么十进制小数没有办法用二进制精确表示
29+
这个和计算机保存小数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的。许多十进制小数转换为二进制后会无限循环,只能舍入为有限位数,因此存在精度损失的风险。不过,像 0.5、0.25 这样能够表示为有限二进制小数的值可以被精确表示
3030

3131
就比如说十进制下的 0.2 就没办法精确转换成二进制小数:
3232

@@ -45,7 +45,7 @@ System.out.println(a == b);// false
4545

4646
## BigDecimal 介绍
4747

48-
`BigDecimal` 可以实现对小数的运算,不会造成精度丢失
48+
`BigDecimal` 可以精确表示十进制数,并提供可显式指定精度和舍入规则的运算。不过,使用有限精度的 `MathContext`、执行需要舍入的除法,或将结果转换为 `float``double` 时,仍可能发生舍入
4949

5050
通常情况下,大部分需要小数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。
5151

@@ -92,7 +92,7 @@ System.out.println(a.divide(b));// 无法除尽,抛出 ArithmeticException 异
9292
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));// 1.11
9393
```
9494

95-
这里需要注意的是,在我们使用 `divide` 方法的时候尽量使用 3 个参数版本,并且 `RoundingMode` 不要选择 `UNNECESSARY`,否则很可能会遇到 `ArithmeticException`(无法除尽出现无限循环小数的时候),其中 `scale` 表示要保留几位小数,`roundingMode` 代表保留规则
95+
这里需要注意的是,应根据业务是否允许舍入来选择 `divide` 的重载。要求结果精确时,可以使用不指定舍入规则的版本;结果无法精确表示时会抛出 `ArithmeticException`。允许舍入时,应显式指定 `scale``roundingMode``RoundingMode.UNNECESSARY` 用于断言结果无需舍入,如果实际需要舍入同样会抛出 `ArithmeticException`
9696

9797
```java
9898
public BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) {
@@ -195,7 +195,7 @@ public class BigDecimalUtil {
195195
}
196196

197197
/**
198-
* 提供精确的加法运算
198+
* 使用 BigDecimal 进行加法运算,结果转换为 double 时仍可能发生舍入
199199
*
200200
* @param v1 被加数
201201
* @param v2 加数
@@ -208,7 +208,7 @@ public class BigDecimalUtil {
208208
}
209209

210210
/**
211-
* 提供精确的减法运算
211+
* 使用 BigDecimal 进行减法运算,结果转换为 double 时仍可能发生舍入
212212
*
213213
* @param v1 被减数
214214
* @param v2 减数
@@ -221,7 +221,7 @@ public class BigDecimalUtil {
221221
}
222222

223223
/**
224-
* 提供精确的乘法运算
224+
* 使用 BigDecimal 进行乘法运算,结果转换为 double 时仍可能发生舍入
225225
*
226226
* @param v1 被乘数
227227
* @param v2 乘数
@@ -265,7 +265,7 @@ public class BigDecimalUtil {
265265
}
266266

267267
/**
268-
* 提供精确的小数位四舍六入五成双处理
268+
* 使用 HALF_EVEN 规则处理指定小数位
269269
*
270270
* @param v 需要四舍六入五成双的数字
271271
* @param scale 小数点后保留几位
@@ -278,11 +278,11 @@ public class BigDecimalUtil {
278278
}
279279
BigDecimal b = BigDecimal.valueOf(v);
280280
BigDecimal one = new BigDecimal("1");
281-
return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();
281+
return b.divide(one, scale, RoundingMode.HALF_EVEN).doubleValue();
282282
}
283283

284284
/**
285-
* 提供精确的类型转换(Float)
285+
* 转换为 Float,超出 float 精度或范围时可能发生舍入或溢出
286286
*
287287
* @param v 需要被转换的数字
288288
* @return 返回转换结果
@@ -293,7 +293,7 @@ public class BigDecimalUtil {
293293
}
294294

295295
/**
296-
* 提供精确的类型转换(Int)不进行四舍六入五成双
296+
* 转换为 Int,不进行舍入;小数部分会被截断,超出范围时会丢失高位
297297
*
298298
* @param v 需要被转换的数字
299299
* @return 返回转换结果
@@ -304,7 +304,7 @@ public class BigDecimalUtil {
304304
}
305305

306306
/**
307-
* 提供精确的类型转换(Long)
307+
* 转换为 Long,不进行舍入;小数部分会被截断,超出范围时会丢失高位
308308
*
309309
* @param v 需要被转换的数字
310310
* @return 返回转换结果
@@ -362,7 +362,7 @@ public class BigDecimalUtil {
362362

363363
## 总结
364364

365-
浮点数没有办法用二进制精确表示,因此存在精度丢失的风险
365+
许多十进制小数无法用有限位二进制精确表示,因此使用 `float``double` 运算时存在精度丢失的风险
366366

367367
不过,Java 提供了 `BigDecimal` 来操作浮点数。`BigDecimal` 的实现利用到了 `BigInteger`(用来操作大整数), 所不同的是 `BigDecimal` 加入了小数位的概念。
368368

docs/java/basis/generics-and-wildcards.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,11 @@ printArray( stringArray );
115115

116116
### 什么是泛型擦除机制?为什么要擦除?
117117

118-
**Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除**
118+
**Java 泛型通过类型擦除实现:泛型实例在运行时不保留具体类型实参,但类文件仍可在 `Signature` 等属性中保留泛型声明信息,并可通过反射 API 读取**
119119

120120
编译器会在编译期间会动态地将泛型 `T` 擦除为 `Object` 或将 `T extends xxx` 擦除为其限定类型 `xxx`
121121

122-
因此,泛型本质上其实还是编译器的行为,为了保证引入泛型机制但不创建新的类型,减少虚拟机的运行开销,编译器通过擦除将泛型类转化为一般类
122+
类型擦除让泛型代码能够与引入泛型之前的 Java 类库和二进制代码保持兼容。编译器会通过必要的类型转换和桥方法维持类型安全与多态语义
123123

124124
这里说的可能有点抽象,我举个例子:
125125

@@ -193,13 +193,13 @@ class MyNode extends Node<Integer> {
193193

194194
泛型的限制一般是由泛型擦除机制导致的。擦除为 `Object` 后无法进行类型判断
195195

196-
- 只能声明不能实例化 `T` 类型变量
196+
- 可以声明 `T` 类型的变量,但不能直接通过 `new T()` 实例化类型参数
197197
- 泛型参数不能是基本类型。因为基本类型不是 `Object` 子类,应该用基本类型对应的引用类型代替。
198198
- 不能实例化泛型参数的数组。擦除后为 `Object` 后无法进行类型判断。
199199
- 不能实例化泛型数组。
200200
- 泛型无法使用 `instanceof` 对类型参数 T 做运行期判断;`getClass()` 在擦除后也无法区分不同泛型实参(如 `List<String>``List<Integer>` 均得到 `List.class`)。
201201
- 不能实现两个不同泛型参数的同一接口,擦除后多个父类的桥方法将冲突
202-
- 不能使用 `static` 修饰泛型变量
202+
- 类的 `static` 上下文不能引用该类声明的类型参数,但静态泛型方法可以声明并使用自己的类型参数
203203
- ……
204204

205205
### 以下代码是否能编译,为什么?
@@ -228,7 +228,7 @@ public class Singleton<T> {
228228
}
229229
```
230230

231-
无法编译,因为不能使用 `static` 修饰泛型 `T`
231+
无法编译,因为类的静态字段和静态方法不能引用类声明的类型参数 `T`。静态方法可以声明自己的类型参数,例如 `public static <T> T getInstance()`
232232

233233
## 通配符
234234

@@ -264,7 +264,7 @@ void testMethod(Person<?> p) {
264264
**`List<?>``List` 有区别吗?** 当然有!
265265

266266
- `List<?> list` 表示 `list` 的元素类型是**某个未知但固定的类型**(即「存在某一类型 `T`,list 是 `List<T>`」),因此编译器不允许向其中添加除 `null` 外的任何元素,以避免类型不安全。
267-
- `List list` 表示 `list` 持有的元素类型是 `Object`,因此可以添加任何类型的对象,但编译器会给出警告
267+
- `List list` 是原生类型(raw type),会绕过部分泛型类型检查,并不等同于 `List<Object>`。向其中添加元素通常会产生未检查警告,并可能把类型错误推迟到运行时
268268

269269
```java
270270
List<?> list = new ArrayList<>();
@@ -277,7 +277,7 @@ list2.add("sss");//警告信息
277277

278278
在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:**类型实参只准传入某种类型的父类或某种类型的子类**
279279

280-
**上边界通配符 `extends`** 可以实现泛型的向上转型即传入的类型实参必须是指定类型的子类型
280+
**上边界通配符 `extends`** 表示类型实参必须是指定类型或其子类型
281281

282282
举个例子:
283283

@@ -293,7 +293,7 @@ list2.add("sss");//警告信息
293293
<T extends XXX>
294294
```
295295

296-
**下边界通配符 `super`** 与上边界通配符 `extends` 刚好相反,它可以实现泛型的向下转型即传入的类型实参必须是指定类型的父类型
296+
**下边界通配符 `super`** 表示类型实参必须是指定类型或其父类型
297297

298298
举个例子:
299299

@@ -304,13 +304,13 @@ List<? super Employee>
304304

305305
**`? extends xxx``? super xxx` 有什么区别?**
306306

307-
两者接收参数的范围不同。并且,使用 `? extends xxx` 声明的泛型参数只能调用 `get()` 方法返回 `xxx` 类型,调用 `set()` 报错。使用 `? super xxx` 声明的泛型参数只能调用 `set()` 方法接收 xxx 类型,调用 `get()` 报错
307+
两者接收类型实参的范围不同。对于 `List<? extends Xxx>`,可以读取为 `Xxx`,但除 `null` 外不能安全写入;对于 `List<? super Xxx>`,可以写入 `Xxx` 及其子类型,读取结果只能安全地视为 `Object`
308308

309309
**PECS 原则(Producer Extends, Consumer Super)**:从数据结构****元素时用 `extends`(生产者,Producer);向数据结构****元素时用 `super`(消费者,Consumer)。例如:`List<? extends Number>` 只能从中读取 `Number`,不能写入;`List<? super Integer>` 可以写入 `Integer` 及其子类,读取时得到的是 `Object``Collections.copy(List<? super T> dest, List<? extends T> src)` 就是典型用法:从 `src` 读、往 `dest` 写。
310310

311311
**`T extends xxx``? extends xxx` 又有什么区别?**
312312

313-
`T extends xxx` 用于定义泛型类和方法,擦除后为 xxx 类型, `? extends xxx` 用于声明方法形参,接收 xxx 和其子类型
313+
`T extends xxx` 用于声明带上界的类型参数,擦除后为 `xxx``? extends xxx` 用于参数化类型中的通配符实参,可出现在字段、局部变量、方法参数和返回类型等位置
314314

315315
**`Class<?>``Class` 的区别?**
316316

0 commit comments

Comments
 (0)