我们通常仅使用Java语言的小部分功能来编写大部分代码,我们实例化的每个Stream以及每个作为实例前缀的@Autowired注释都足以完成我们的大部分目标。然而,有时候,我们必须转向该语言中很少被使用的部分:该语言的隐藏部分,该部分语言通常用于特定目的。
在本文中,我们将探讨4种技巧以帮助提高开发简便性和可读性。并非所有这些技巧都适用于每种情况,或者大多数情况。例如,可能只有少数集中方法适用于协变返回类型或者只有几个通用类适合使用交叉通用类型的模式,而其他方法可能可提高大多数代码库意图的可读性和清晰度。无论在何种情况下,重要的是不仅要了解这些技巧,而且要知道何时应用它们。
1.协变返回类型
即使是最引人入胜的Java操作手册也都会包含有关继承、接口、抽象类和方法覆盖的介绍,但却很少探讨覆盖方法时更复杂的可能性。例如,下面的代码片段对于即使是Java开发新手也不会惊讶:
public interface Animal {
public String makeNoise();
}
public class Dog implements Animal {
@Override
public String makeNoise() {
return "Woof";
}
}
public class Cat implements Animal {
@Override
public String makeNoise() {
return "Meow";
}
}
这是多态的基本概念:对象的方法可根据其接口(Animal::makeNoise)调用但该方法调用的实际行为取决于实现类型(Dog::makeNoise)。例如,下面方法的输出将会改变,取决于Dog对象或Cat对象是否传递到该方法:
public class Talker {
public static void talk(Animal animal) {
System.out.println(animal.makeNoise());
}
}
Talker.talk(new Dog()); // Output: Woof
Talker.talk(new Cat()); // Output: Meow
虽然这是很多Java应用中常用的技巧,但在覆盖方法时可能会采用另一种操作:更改返回类型。尽管这可能是无限制的覆盖方法的方式,但对覆盖方法的返回类型有着严格限制。根据Java 8 SE语言规范(第248页):
如果一种方法声明d 1 (包含返回类型R 1)覆盖或隐藏另一种方法d 2 (包含返回类型R 2)的声明,那么d 1 必须是的返回类型替代,否则将发生编译时错误。
其中return-type-substitutable (同上,第240页)被定义为:
如果R1 无效,则R2无效
如果R1 是原始类型,则R2 与R1相同
如果R1 是引用类型,则符合以下条件之一:
a. R1 适用于d2 类型参数,它是R2.的子类型
b. R1 可通过未经检查的转换被转换为R2 的子类型
c. d1 不具有与d2 相同的签名,且R1 = |R2|
可以说,最有趣的例子是Rules 3.a.和3.b.:当覆盖方法时,返回类型的子类型可被声明作为覆盖返回类型,例如:
public interface CustomCloneable {
public Object customClone();
}
public class Vehicle implements CustomCloneable {
private final String model;
public Vehicle(String model) {
this.model = model;
}
@Override
public Vehicle customClone() {
return new Vehicle(this.model);
}
public String getModel() {
return this.model;
}
}
Vehicle originalVehicle = new Vehicle("Corvette");
Vehicle clonedVehicle = originalVehicle.customClone();
System.out.println(clonedVehicle.getModel());
虽然clone()的原始返回类型是Object,但我们可在clonedVehicle调用getModel() (没有显式转换),因为我们已经将Vehicle :: clone的返回类型重写为Vehicle。这消除了对乱码的必要,我们知道我们寻找的返回类型是Vehicle,即使它被声明为Object(相当于基于先验信息的安全投递,但严格来说不安全):
- Vehicle clonedVehicle = (Vehicle) originalVehicle.customClone();
请注意我们仍然可将Vehicle的类型声明为Object,返回类型将恢复到Object的原始类型:
Object clonedVehicle = originalVehicle.customClone();
System.out.println(clonedVehicle.getModel()); // ERROR: getModel not a method of Object
请注意,对于通用参数,返回类型不能重载,但对于通用类,则可重载。例如,如果基类或接口方法返回 List,子类的返回类型可能覆盖到 ArrayList,但可能不会覆盖到List。
2.交叉通用类型
创建通用类是很好的方法来创建一组类与组合对象进行交互。例如,List 仅存储和检索类型T的对象,而不了解其包含的元素的性质,在某些情况下,我们想要限制我们的通用类型参数(T)具有特定特征。例如,以下接口:
public interface Writer {
public void write();
}
我们可能想要创建特定Writers组合,在下面Composite Patterns:
public class WriterComposite implements Writer {
private final List writers;
public WriterComposite(List writers) {
this.writers = writer;
}
@Override
public void write() {
for (Writer writer: this.writers) {
writer.write();
}
}
}
我们现在可遍历Writers树,不知道我们遇到的具体Writer是standalongWriter还是Writer组合。如果我们也希望我们的组合作为reader和writer的组合怎么办?例如,如果我们有以下接口:
public interface Reader {
public void read();
}
我们如何将我们的WriterComposite修改为ReaderWriterComposite?有种技巧可创建新的接口ReaderWriter,来融合Reader和Writer接口:
- public interface ReaderWriter implements Reader, Writer {}
然后,我们可修改现有的WriterComposite为以下内容:
public class ReaderWriterComposite implements ReaderWriter {
private final List readerWriters;
public WriterComposite(List readerWriters) {
this.readerWriters = readerWriters;
}
@Override
public void write() {
for (Writer writer: this.readerWriters) {
writer.write();
}
}
@Override
public void read() {
for (Reader reader: this.readerWriters) {
reader.read();
}
}
}
虽然这样做完成了我们的目标,但我们在代码中制造了膨胀:我们创建了一个接口,其唯一目的是将两个现有接口合并在一起。随着越来越多接口出现,我们会看到膨胀的组合爆炸。例如,如果我们创建新的Modifier接口,我们现在会需要createReaderModifier、WriterModifier和ReaderWriter接口。请注意,这些接口并不增加任何功能:它们只是合并现有接口。
为了消除这个膨胀,我们需要能够指定我们的 ReaderWriterComposite 接受通用类型参数,仅当它们都是Reader和Writer时。交叉通用类型允许我们这样做,为了指定通用类型参数,必须同时部署reader和writer接口,我们在通用类型约束之间使用&运算符:
public class ReaderWriterComposite implements Reader, Writer {
private final List readerWriters;
public WriterComposite(List readerWriters) {
this.readerWriters = readerWriters;
}
@Override
public void write() {
for (Writer writer: this.readerWriters) {
writer.write();
}
}
@Override
public void read() {
for (Reader reader: this.readerWriters) {
reader.read();
}
}
}
如果没有膨胀继承树,我们现在可约束通用类型参数来部署多个接口。请注意,相同限制还可制定其中一个接口是抽象类或者具体类。例如,如果我们将writer接口更改为抽象类,类似以下:
public abstract class Writer {
public abstract void write();
}
我们仍然可限制我们的通用类型参数为Reader和Writer,但Writer(由于它是抽象类而不是接口)必须先被指定(还要注意,我们的ReaderWriterComposite现在扩展Writer抽象类并部署Reader接口,而不是实现两者)
public class ReaderWriterComposite extends Writer implements Reader {
// Same class body as before
}
同样重要的是,这种交叉通用类型可用于两个以上接口(或者一个抽象类和多个接口),例如,如果我们想要我们的组合还包含Modifierinterface,我们可按以下编写我们的类定义:
public class ReaderWriterComposite implements Reader, Writer, Modifier {
private final List things;
public ReaderWriterComposite(List things) {
this.things = things;
}
@Override
public void write() {
for (Writer writer: this.things) {
writer.write();
}
}
@Override
public void read() {
for (Reader reader: this.things) {
reader.read();
}
}
@Override
public void modify() {
for (Modifier modifier: this.things) {
modifier.modify();
}
}
}
虽然可执行上述,但这可能是代码嗅觉的迹象(Reader、Writer和Modifier对象可能是更具体的东西,例如File)
有关交叉通用类型的更多信息,请参阅Java 8语言规范。
3.自动关闭类
创建资源类是一种常见做法,但保持该资源的完整性具有挑战性,特别是当涉及异常处理时。例如,假设我们创建一个资源类,Resource,并希望对该资源执行操作,这可能会引发异常(实例化过程也可能会引发异常):
public class Resource {
public Resource() throws Exception {
System.out.println("Created resource");
}
public void someAction() throws Exception {
System.out.println("Performed some action");
}
public void close() {
System.out.println("Closed resource");
}
}
在任一情况下(引发异常或者没有引发),我们要关闭我们的资源以确保没有资源泄漏。正常的过程是在finally块中封闭我们的 close() 方法,确保无论发生什么情况,我们的资源在封闭执行范围完成前关闭:
Resource resource = null;
try {
resource = new Resource();
resource.someAction();
}
catch (Exception e) {
System.out.println("Exception caught");
}
finally {
resource.close();
}
通过简单的检查,我们发现很多样板代码从Resource对象someAction()的执行可读性减损。为了弥补这种情况,Java 7引入try-with-resources声明,resource可在try声明中创建,并在离开try执行范围前自动关闭。为了让类可使用try-with-resources,必须部署自动关闭接口:
public class Resource implements AutoCloseable {
public Resource() throws Exception {
System.out.println("Created resource");
}
public void someAction() throws Exception {
System.out.println("Performed some action");
}
@Override
public void close() {
System.out.println("Closed resource");
}
}
我们的Resource类现在采用自动可关闭接口,我们可清理代码以确保资源在离开try执行范围之前关闭。
try (Resource resource = new Resource()) {
resource.someAction();
}
catch (Exception e) {
System.out.println("Exception caught");
}
与非try-with-resource技术相比,这个过程没有那么混乱,并保持相同安全性(在try执行范围完成前resource始终关闭)。如果执行上述try-with-resource,我们获得以下输出:
Created resource
Performed some action
Closed resource
为了展示这种try-with-resource技术的安全性,我们可改变someAction()为抛出Exception:
public class Resource implements AutoCloseable {
public Resource() throws Exception {
System.out.println("Created resource");
}
public void someAction() throws Exception {
System.out.println("Performed some action");
throw new Exception();
}
@Override
public void close() {
System.out.println("Closed resource");
}
}
如果我们重新运行try -with-resources声明,我们可获得以下输出:
Created resource
Performed some action
Closed resource
Exception caught
请注意,即使在执行someAction()方法时抛出Exception,我们的资源仍然关闭,则Exception被捕获。这可确保在离开try执行范围前,我们的资源保证关闭。同样重要的是要注意,resource可部署Closeable接口,仍然可使用try-with-resources声明。部署自动关闭接口和可关闭接口之间的区别在于从 close() 方法签名抛出的Exception类型:Exceptionand IOException。在我们的例子中,我们简单地改变了 close() 方法的签名,不会引发异常。
4.最终类和方法
在几乎所有情况下,我们创建的类可由另一位开发人员进行扩展,并根据其需求进行自定义(我们可扩展自己的类),即使我们不希望扩展我们的类。虽然这在大多数情况下是足够的,但有时候我们不希望方法被覆盖,或者让我们的类被扩展。例如,我们创建File类来封装文件系统中文件的读取和写入,我们可能不希望任何子类覆盖我们的 read(int bytes) 和write(String data)方法(如果这些方法的裸机被改变,可能会导致文件系统损坏)。在这种情况下,我们标记不可扩展方法作为final:
public class File {
public final String read(int bytes) {
// Execute the read on the file system
return "Some read data";
}
public final void write(String data) {
// Execute the write to the file system
}
}
现在,如果另一个类希望覆盖读取或写入方法,则会引发编译错误:无法从File覆盖最终方法。我们不仅记录我们的方法不应该被覆盖,编译器也确保这个意图在编译时不会执行。
在将这个想法扩展到整个类时,可能有时候我们不希望我们的类被扩展。这不仅会使类的每个方法不可执行,还会让无法创建类的子类型。例如,如果我们在创建安全框架来使用密钥生成器,我们可能不会想要任何外部开发人员扩展我们的密钥生成器以及覆盖生成算法(自定义功能可能会影响系统):
public final class KeyGenerator {
private final String seed;
public KeyGenerator(String seed) {
this.seed = seed;
}
public CryptographicKey generate() {
// ...Do some cryptographic work to generate the key...
}
}
通过将我们的KeyGenerator类作为最终类,编译器可确保没有类可扩展我们的类以及将其传递到我们的框架作为有效的加密密钥生成器。尽管简单地标记thegenerate() 方法为最终似乎已经足够,但这并不会阻止开发人员创建自定义密钥生成器并将其作为有效生成器。由于我们的系统为安全导向,所以应该尽可能不要信任外部世界(聪明的开发人员可通过改变KeyGenerator类中其他方法的功能来改变生成算法)。
虽然这似乎是对开放/封闭原则的公开否认,但这样做有很好的理由。从我们上面安全示例中可以看出,很多时候,我们无法让外部开发人员对我们的应用程序做想做的事情,我们必须对集成非常仔细地做决定。这个概念无处不在,例如C#语言默认一个类作为final(不能被扩展),并且,它必须被开发人员指定为开放。此外,我们应该非常慎重地确定哪些类可以被扩展,哪些方法可被覆盖。
结论
尽管我们仅使用Java小部分功能来编写大多数代码,但这足以解决我们遇到的大部分问题。有时候,我们需要深入挖掘那些被遗忘或者未知的语言部分来解决特定问题。协变返回类型和交叉通用类型等技术可用于一次性的情况,而自动关闭资源和最终方法及类的方法则可用于产生更可读和更准确的代码。你可将这些技巧与日常编程实践结合起来,这可帮助你更好地编写Java代码。
注:加群要求 学习交流群:642830685
1、想学习JAVA这一门技术, 对JAVA感兴趣零基础,想从事JAVA工作的。
2、工作1-5年,感觉自己技术不行,想提升的
3、如果没有工作经验,但基础非常扎实,想提升自己技术的。
4、还有就是想一起交流学习的。
5.小号加群一律不给过,谢谢。
转发此文章请带上原文链接,否则将追究法律责任