You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

206 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 16 | 改进的废弃,怎么避免使用废弃的特性?
你好我是范学雷。今天我们讨论Java公开接口的废弃。
像所有的事物一样,公开接口也有生命周期。要废弃那些被广泛使用的、或者还有人使用的公开接口,是一个非常痛苦的过程。该怎么废弃一个公开接口,该怎么减少废弃接口对我们的影响呢?这是这一次我们要讨论的话题。
我们先来看看阅读案例。
## 阅读案例
在 JDK 中,一个公开的接口,可能会因为多种多样的原因被废弃。比如说,这个接口的设计是危险的,或者有了更新的、更好的替代接口。不管是什么原因,废弃接口的使用者们都需要尽快迁移代码,转换到替代方案上来。
在JDK中公开接口的废弃需要使用两种不同的机制也就是“Deprecated” 注解annotation和“Deprecated”文档标记JavaDoc tag
Deprecated的注解会编译到类文件里并且可以在运行时查验。这就允许像javac这样的工具检测和标记已废弃接口的使用情况了。
Deprecated文档标记用于描述废弃接口的文档中。除了标记接口的废弃状态之外一般情况下我们还要描述废弃的原因和替代的方案。
下面的这段代码就是使用Java注解和文档标记来废弃一个公开接口的例子。
```java
public sealed abstract class Digest {
/**
* -- snipped
*
* @deprecated This method is not performance friendly. Use
* {@link #digest(byte[], byte[]) instead.
*/
@Deprecated
public abstract byte[] digest(byte[] message);
// snipped
public void digest(byte[] message, byte[] digestValue) {
// snipped
}
}
```
如果一段程序使用了废弃接口编译的时候就会提出警告。但是有很多编译环境的配置把编译警告看作是编译错误。为了解决这样的问题JDK还提供了“消除使用废弃接口的编译警告”的选项。也就是SuppressWarnings注解。
```java
@SuppressWarnings("deprecation")
public static void main(String[] args) {
try {
Digest.of("SHA-256")
.digest("Hello, world!".getBytes());
} catch (NoSuchAlgorithmException ex) {
// ignore
}
}
```
公开接口的废弃机制是在JDK 1.5的时候发布的。 这种机制像一座设计者和使用者之间的沟通桥梁,减轻了双方定义或者使用废弃接口的痛苦。
遗憾的是直到现在公开接口的废弃依然是一个复杂、痛苦的过程。一个公开的接口从声明废弃到彻底删除是一个漫长的过程。在JDK中还存在着大量废弃了20多年都无法删除的公开接口。
为什么删除废弃的公开接口这么困难呢?如果从废弃机制本身的角度来思考,下面几个问题延迟了废弃接口使用者的迁移意愿和努力。
第一个问题也是最重要的问题就是SuppressWarnings注解的使用。SuppressWarnings注解的本意是消除编译警告保持向后的编译兼容性。可是一旦编译警告消除SuppressWarnings注解也就抵消了Deprecated注解的功效。代码的维护者一旦使用了SuppressWarnings注解就很难再有更合适的工具让自己知道还在使用的废弃接口有哪些了。不知道当然就不会有行动。
第二个问题就是废弃接口的使用者并不担心使用废弃接口。虽然我们都知道不应该使用废弃的接口但是因为一些人认为没有紧急迁移的必要性也不急着制定代码迁移的时间表所以倾向于先使用SuppressWarnings注解把编译警告消除了以后再说迁移的事情。然后就掉入了第一个问题的陷阱。
第三个问题,就是废弃接口的使用者并不知道接口废弃了多久。在接口使用者的眼里,废弃了十年,和废弃了一年的接口,没有什么区别。可是,在接口维护者的眼里,废弃了十年的接口,应该可以放心地删除了。然而,使用者并没有感知到这样的区别。没有感知,当然也就没有急迫感了。
一旦一个接口被声明为废弃,它的问题也就再难进入接口维护者的任务列表里了。所以,这个接口的实现可能充满了风险和错误。于是局面就变成了,接口维护者难以删除废弃的接口,接口的使用者又不能获得必要的提示,这种情况实在有点尴尬。
## 改进的废弃
上面这些问题在JDK 9的接口废弃机制里有了重大的改进。
第一个改进是添加了一个新的工具jdeprscan。有了这个工具就可以扫描编译好的Java类或者包看看有没有使用废弃的接口了。即使代码使用了SuppressWarnings注解jdeprscan的结果也不受影响。这个工具解决了我们在阅读案例里提到的第一个问题。
另外如果我们使用第三方的类库或者已经编译好的类库发现对废弃接口的依赖关系很重要。如果将来废弃接口被删除使用废弃接口的类库将不能正常运行。而jdeprscan允许我们在使用一个类库之前进行废弃依赖关系检查提前做好风险的评估。
第二个改进是给Deprecated注解增加了一个“forRemoval”的属性。如果这个属性设置为“true",那就表示这个废弃接口的删除已经提上日程了。两到三个版本之后,这个废弃的接口就会被删除。这样的改进,强调了代码迁移的紧急性,它给了使用者一个明确的提示。这个改进,解决了我们在阅读案例里提到的第二个问题。
第三个改进是给Deprecated注解增加了一个“since”的属性。这个属性会说明这个接口是在哪一个版本废弃的。如果我们发现一个接口已经废弃了三年以上就要考虑尽最大努力进行代码迁移了。这样的改进给了废弃接口的使用者一个时间上的概念也方便开发者安排代码迁移的时间表。这个改进解决了我们在阅读案例里提到的第三个问题。
下面的这段代码,就是一个使用了这两种属性的例子。
```java
public sealed abstract class Digest {
/**
* -- snipped
*
* @deprecated This method is not performance friendly. Use
* {@link #digest(byte[], byte[]) instead.
*/
@Deprecated(since = "1.4", forRemoval = true)
public abstract byte[] digest(byte[] message);
// snipped
public void digest(byte[] message, byte[] digestValue) {
// snipped
}
}
```
如果在Deprecated注解里新加入“forRemoval”属性并且设置为“true"那么以前的SuppressWarnings就会失去效果。要想消除掉编译警告我们需要使用新的选项。就像下面的例子这样。
```java
@SuppressWarnings("removal")
public static void main(String[] args) {
try {
Digest.of("SHA-256")
.digest("Hello, world!".getBytes());
} catch (NoSuchAlgorithmException ex) {
// ignore
}
}
```
当一个废弃接口的删除提上日程的时候添加“forRemoval”属性让我们又有一次机会在代码编译的时候重新审视还在使用的废弃接口了。
## 废弃三部曲
有了JDK 9的废弃改进我们就能够看到接口废弃的一般过程了。
第一步,废弃一个接口,标明废弃的版本号,并且描述替代方案;
第二步添加“forRemoval”属性把删除的计划提上日程
第三步,删除废弃的接口。
对于接口的使用者,我们应该尽量在第一步就做好代码的迁移;如果我们不能在第一步完成迁移,当看到第二步的信号时,我们也要把代码迁移的工作提高优先级,以免影响后续的版本升级。
对于接口的维护者,我们需要尽量按照这个过程退役一个接口,给接口的使用者充分的时间和信息,让他们能够完成代码的迁移。
## 总结
好,到这里,我来做个小结。刚才,我们讲了接口废弃的现实问题,以及接口废弃的三部曲。总体来说,我们要管理好废弃的接口。接口的废弃要遵守程序,有序推进;代码的迁移要做好计划,尽快完成。
另外我们要使用好jdeprscan这个新的工具。在使用一个类库之前要有意识地进行废弃依赖关系检查提前做好代码风险的评估。
如果面试中聊到了接口废弃的问题你可以聊一聊接口废弃的三部曲以及每一步应该使用的Java注解形式。
## 思考题
今天的思考题,我们来练习一下接口废弃的过程。前面,我们练习过表示形状的封闭类。假设要废弃表示正方形的许可类,我们该怎么做呢?代码该怎么改动呢?
为了方便你阅读,我把表示形状的封闭类的代码拷贝到了下面。请再一次阅读“废弃三部曲”这一小节,然后试着修改下面的代码。
```plain
package co.ivi.jus.retire.review.xuelei;
public abstract sealed class Shape {
public final String id;
public Shape(String id) {
this.id = id;
}
public abstract double area();
public static final class Circle extends Shape {
public final double radius;
public Circle(String id, double radius) {
super(id);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public static final class Square extends Shape {
public final double side;
public Square(String id, double side) {
super(id);
this.side = side;
}
@Override
public double area() {
return side * side;
}
}
// Here is your code for Rectangle.
// Here is the test for circle.
public static boolean isCircle(Shape shape) {
// Here goes your update.
return (shape instanceof Circle);
}
// Here is the code to run your test.
public static void main(String[] args) {
// Here is your code.
}
}
```
欢迎你在留言区留言、讨论,分享你的阅读体验以及你的设计和代码。我们下节课见!
注:本文使用的完整的代码可以从[GitHub](https://github.com/XueleiFan/java-up/tree/main/src/main/java/co/ivi/jus/retire)下载,你可以通过修改[GitHub](https://github.com/XueleiFan/java-up/tree/main/src/main/java/co/ivi/jus/retire)上[review template](https://github.com/XueleiFan/java-up/blob/main/src/main/java/co/ivi/jus/retire/review/xuelei/Shape.java)代码,完成这次的思考题。如果你想要分享你的修改或者想听听评审的意见,请提交一个 GitHub的拉取请求Pull Request并把拉取请求的地址贴到留言里。这一小节的拉取请求代码请在[接口废弃专用的代码评审目录](https://github.com/XueleiFan/java-up/tree/main/src/main/java/co/ivi/jus/retire/review)下建一个以你的名字命名的子目录代码放到你专有的子目录里。比如我的代码就放在retire/review/xuelei的目录下面。