编写更好的Java的4种技巧

日复一日,我们编写的大多数Java都使用了该语言全部功能的一小部分。我们实例化的每个实例和我们为实例变量加上前缀的每个注释都足以实现我们的大多数目标。但是,有时候我们必须诉诸语言中那些很少使用的部分:语言的隐藏部分有特定的用途。
本文探讨了四种可在绑定中使用并引入代码库中的技术,这些技术可同时提高开发的易用性和可读性。并非所有这些技术都适用于每种情况,甚至大多数情况。例如,可能只有少数方法适合于协变量返回类型,或者只有少数适​​合使用交叉通用类型的模式的通用类,而其他诸如最终方法和类以及try-with-resources块,将提高大多数代码库的可读性和意图的清晰度。无论哪种情况,重要的是不仅要知道这些技术的存在,而且要知道何时明智地应用它们。

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应用程序中常用的技术,但是在重写方法时可以采取的知名度较低:更改返回类型。尽管这似乎是重写方法的开放式方法,但是对重写方法的返回类型有一些严重的限制。
其中将return-type-substitutable (同上,第240页)定义为
1.如果R 1为空,则R 2为空
2.如果R 1是原始类型,则R 2与R 1相同
3.如果R 1是引用类型,则下列条件之一为真:
1.适应d 2类型参数的R 1是R 2的子类型。
2.R 1可以通过未经检查的转换转换为R 2的子类型
3.d 1与d 2具有不同的签名,并且R 1 = | R 2 |
可以说,最有趣的情况是规则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,我们可以调用getModel()我们的克隆Vehicle (没有进行明确的转换),因为我们已经覆盖的返回类型Vehicle::clone为Vehicle。这样就无需进行混乱的强制类型转换,因为我们知道我们正在寻找的返回类型是Vehicle,即使将其声明为是Object(根据先验信息,这是安全的强制类型转换,但严格来说是不安全的):
Vehicle clonedVehicle = (Vehicle) originalVehicle.customClone();

请注意,我们仍然可以将车辆的类型声明为a Object,并且返回类型将恢复为其原始类型Object:
Object clonedVehicle = originalVehicle.customClone();
System.out.println(clonedVehicle.getModel()); // ERROR: getModel not a method of Object

请注意,返回类型不能针对泛型参数进行重载,但可以针对泛型类进行重载。例如,如果基类或接口方法返回a List,则子类的返回类型可能会被覆盖为ArrayList,但可能不会被覆盖为List。

2.交叉通用类型

创建泛型类是创建以相似方式与组合对象进行交互的一组类的一种极好的方法。例如,List简单地存储和检索类型的对象,T而无需了解其包含的元素的性质。在某些情况下,我们希望将通用类型参数(T)约束为具有特定特征。例如,给定以下界面
public interface Writer {
public void write();
}

我们可能要Writers使用Composite Pattern创建以下的特定集合:
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遇到的特定对象是独立的Writer(叶子)还是的集合Writers(复合)。如果我们还希望我们的复合材料充当读者和作家的复合材料怎么办?例如,如果我们有以下界面
public interface Reader {
public void read();
}

我们如何才能将自己修改WriterComposite为ReaderWriterComposite?一种技术是创建一个新的接口,ReaderWriter将Reader和Writer接口融合在一起:
public interface ReaderWriter extends 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界面,我们现在需要创建ReaderModifier,WriterModifier和ReaderWriter接口。请注意,这些接口没有添加任何功能:它们只是合并现有接口。
要消除这种膨胀,我们需要能够指定我们ReaderWriterComposite接受泛型类型参数当且仅当它们都是Reader和Writer。交叉通用类型允许我们做到这一点。为了指定通用类型参数必须同时实现Reader和Writer接口,我们&在通用类型约束之间使用运算符:
public class ReaderWriterComposite<T extends Reader & Writer> 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现在extends的Writer抽象类和implements的Reader界面,而不是执行两种):
public class ReaderWriterComposite<T extends Writer & Reader> extends Writer implements Reader {
// Same class body as before
}

同样重要的是要注意,这种交叉通用类型可以用于两个以上的接口(或一个抽象类和一个以上的接口)。例如,如果我们希望我们的组合也包含Modifier接口,则可以如下编写类定义:
public class ReaderWriterComposite<T extends Reader & Writer & Modifier> 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();
}
}
}

尽管执行上述操作是合法的,但这可能是代码气味的征兆(a Reader,a Writer和a的Modifier对象可能更具体一些,例如a 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”);
}
}

无论哪种情况(如果引发或未引发异常),我们都希望关闭资源以确保没有资源泄漏。正常过程是将我们的close()方法封装在一个finally块中,以确保无论发生什么情况,在封闭的执行范围完成之前,我们的资源都是关闭的:
Resource resource = null;
try {
resource = new Resource();
resource.someAction();
}
catch (Exception e) {
System.out.println(“Exception caught”);
}
finally {
resource.close();
}

通过简单的检查,有很多样板代码降低了someAction()在我们的Resource对象上执行代码的可读性。为了解决这种情况,Java 7引入了try-with-resources语句,从而可以在该try语句中创建资源,并在try离开执行范围之前自动关闭资源。为了使一个类能够使用try-with-resources,它必须实现以下AutoCloseable接口:
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类现在实现AutoCloseable接口,我们可以清理代码以确保在离开try执行范围之前关闭资源:
try (Resource resource = new Resource()) {
resource.someAction();
}
catch (Exception e) {
System.out.println(“Exception caught”);
}

与非资源尝试技术相比,此过程更加混乱,并保持相同的安全性(资源总是在try执行范围完成后关闭)。如果执行了上述try-with-resources语句,我们将获得以下输出:
Created resource
Performed some action
Closed 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

即使一个通知,Exception在执行被抛出someAction()的方法,我们的资源被关闭,然后将Exception被抓住了。这样可以确保在离开try执行范围之前,确保我们的资源已关闭。同样重要的是要注意,资源可以实现Closeable接口并且仍然使用try-with-resources语句。实现AutoCloseable接口与Closeable接口之间的区别在于分别从close()方法签名引发的异常类型:Exception和IOException。在我们的案例中,我们仅更改了close()方法的签名以不引发异常。

4.期末课程和方法

在几乎所有情况下,我们创建的类都可以由其他开发人员扩展并进行定制,以适应该开发人员的需求(我们可以扩展自己的类),即使扩展我们的类不是我们的意图。尽管这在大多数情况下就足够了,但有时我们不希望重写某个方法,或更广泛地说,是扩展我们的一个类。例如,如果我们创建一个File封装文件系统上文件读写的类,则我们可能不希望任何子类覆盖我们的read(int bytes)and 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
}
}

现在,如果另一个类希望重写read或write方法,则会引发编译错误:Cannot override the final method from 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类final,编译器将确保没有任何类可以扩展我们的类并将其作为有效的加密密钥生成器传递给我们的框架。尽管将generate()方法简单地标记为似乎足够了final,但这并不能阻止开发人员创建自定义密钥生成器并将其作为有效生成器传递。考虑到我们的系统是面向安全性的,因此最好不要与外界信任(聪明的开发人员可以通过更改KeyGenerator类中其他方法的功能来更改生成算法,如果这些方法当下)。
尽管这似乎公然无视了“ 开放/封闭原则”(确实如此),但这样做是有充分理由的。从上面的安全示例可以看出,很多时候我们没有允许外部世界使用我们的应用程序执行它想要做的事情的奢望,我们在决策继承时必须非常谨慎。诸如Josh Bolch之类的作家甚至认为,应该故意将一个类设计为可扩展的,或者应该为扩展而明确关闭该类(Effective Java)。尽管他故意夸大了这个想法(请参见记录继承或禁止继承)),他提出了一个很重要的观点:我们应该非常仔细地考虑应该扩展哪些类,以及哪些方法可以覆盖。

结论

虽然我们编写的大多数代码仅利用Java的部分功能,但足以解决我们遇到的大多数问题。有时候,我们需要更深入地研究该语言,并清除掉那些被遗忘或未知的语言部分,以解决特定的问题。其中一些技术(例如协变返回类型和交集通用类型)可以在一次性情况下使用,而其他技术(例如可自动关闭的资源以及最终方法和类)可以​​并且应该更经常用于产生更易读和可理解的信息。更精确的代码。将这些技术与日常编程实践相结合不仅有助于更好地理解我们的意图,而且有助于更好地编写更好的Java。

最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值