编写更好的Java的4种技巧
日复一日,我们编写的大多数Java都使用了该语言全部功能的一小部分。我们实例化的每个实例和我们为实例变量加上前缀的每个注释都足以实现我们的大多数目标。但是,有时候我们必须诉诸语言中那些很少使用的部分:语言的隐藏部分有特定的用途。
本文探讨了四种可在绑定中使用并引入代码库中的技术,这些技术可同时提高开发的易用性和可读性。并非所有这些技术都适用于每种情况,甚至大多数情况。例如,可能只有少数方法适合于协变量返回类型,或者只有少数适合使用交叉通用类型的模式的通用类,而其他诸如最终方法和类以及try-with-resources块,将提高大多数代码库的可读性和意图的清晰度。无论哪种情况,重要的是不仅要知道这些技术的存在,而且要知道何时明智地应用它们。
1.协变量返回类型
即使是最入门的Java入门手册也将包括有关继承,接口,抽象类和方法重写的资料页面,但是即使是高级文本也很少探讨重写方法时更复杂的可能性。例如,即使是最新手的Java开发人员,以下代码片段也不会感到惊讶:
这是多态性的基本概念:可以根据对象的接口(Animal::makeNoise
)调用对象上的方法,但是该方法调用的实际行为取决于实现类型(Dog::makeNoise
)。例如,以下方法的输出将根据是否将Dog
对象或 Cat
对象传递给该方法而改变:
尽管这是许多Java应用程序中常用的技术,但是在重写方法时可以采取的知名度较低:更改返回类型。尽管这似乎是重写方法的开放式方法,但是对重写方法的返回类型有一些严重的限制。根据Java 8 SE语言规范(第248页):
其中将return-type-substitutable(同上,第240页)定义为
- 如果R 1为空,则R 2为空
- 如果R 1是原始类型,则R 2与R 1相同
- 如果R 1是引用类型,则下列条件之一为真:
- 适应d 2类型参数的R 1是R 2的子类型。
- R 1可以通过未经检查的转换转换为R 2的子类型
- d 1与d 2具有不同的签名,并且R 1 = | R 2 |
可以说,最有趣的情况是规则3.a。和3.b .:重写方法时,可以将返回类型的子类型声明为重写的返回类型。例如:
虽然原来的返回类型clone()
是Object
,我们可以调用getModel()
我们的克隆Vehicle
(没有进行明确的转换),因为我们已经覆盖的返回类型Vehicle::clone
为Vehicle
。这样就无需进行混乱的强制类型转换,因为我们知道我们正在寻找的返回类型是Vehicle
,即使将其声明为是Object
(根据先验信息,这是安全的强制类型转换,但严格来说是不安全的):
请注意,我们仍然可以将车辆的类型声明为a Object
,并且返回类型将恢复为其原始类型Object
:
请注意,返回类型不能针对泛型参数进行重载,但可以针对泛型类进行重载。例如,如果基类或接口方法返回a List<Animal>
,则子类的返回类型可能会被覆盖为ArrayList<Animal>
,但可能不会被覆盖为List<Dog>
。
2.交叉通用类型
创建泛型类是创建以相似方式与组合对象进行交互的一组类的一种极好的方法。例如,List<T>
简单地存储和检索类型的对象,而无需了解其包含的元素的性质。在某些情况下,我们希望将通用类型参数()约束为具有特定特征。例如,给定以下界面
我们可能要Writers
使用Composite Pattern创建以下的特定集合:
现在,我们可以遍历的树Writers
,而不必知道Writer
遇到的特定对象是独立的Writer
(叶子)还是的集合Writers
(复合)。如果我们还希望我们的复合材料充当读者和作家的复合材料怎么办?例如,如果我们有以下界面
我们如何才能将自己修改WriterComposite
为ReaderWriterComposite
?一种技术是创建一个新的接口,ReaderWriter
将Reader
和Writer
接口融合在一起:
然后,我们可以将现有内容修改WriterComposite
为以下内容:
尽管这确实实现了我们的目标,但是我们在代码中创建了膨胀:我们创建接口的唯一目的是将两个现有接口合并在一起。随着接口越来越多,我们可以开始看到膨胀的组合爆炸。例如,如果我们创建了一个新的Modifier界面,我们现在需要创建ReaderModifier,WriterModifier和ReaderWriter接口。请注意,这些接口没有添加任何功能:它们只是合并现有接口。
要消除这种膨胀,我们需要能够指定我们ReaderWriterComposite接受泛型类型参数当且仅当它们都是Reader和Writer。交叉通用类型允许我们做到这一点。为了指定通用类型参数必须同时实现Reader和Writer接口,我们&在通用类型约束之间使用运算符:
现在无需膨胀我们的继承树,我们现在就可以约束我们的通用类型参数以实现多个接口。请注意,如果同样的约束可以指定一个接口是一个抽象类或具体类。例如,如果我们将Writer
接口更改为类似于以下内容的抽象类
我们仍然可以约束我们的泛型类型参数是既Reader
和Writer
,但Writer
(因为它是一个抽象类,而不是一个接口)必须先指定(另请注意,我们ReaderWriterComposite
现在extends
的Writer
抽象类和implements
的Reader
界面,而不是执行两种):
同样重要的是要注意,这种交叉通用类型可以用于两个以上的接口(或一个抽象类和一个以上的接口)。例如,如果我们希望我们的组合也包含Modifier
接口,则可以如下编写类定义:
尽管执行上述操作是合法的,但这可能是代码气味的征兆(a Reader,a Writer和a的Modifier对象可能更具体一些,例如a File)。
有关交叉通用类型的更多信息,请参见Java 8语言规范。
3.自动关闭类
创建资源类是一种常见的做法,但是维护该资源的完整性可能是充满挑战的前景,尤其是在涉及异常处理时。例如,假设我们创建一个资源类,Resource
并且想要对该资源执行可能引发异常的操作(实例化过程也可能引发异常):
无论哪种情况(如果引发或未引发异常),我们都希望关闭资源以确保没有资源泄漏。正常过程是将我们的close()
方法封装在一个finally
块中,以确保无论发生什么情况,在封闭的执行范围完成之前,我们的资源都是关闭的:
通过简单的检查,有很多样板代码降低了someAction()
在我们的Resource
对象上执行代码的可读性。为了解决这种情况,Java 7引入了try-with-resources语句,从而可以在该try
语句中创建资源,并在try
离开执行范围之前自动关闭资源。为了使一个类能够使用try-with-resources,它必须实现以下AutoCloseable
接口:
通过我们的Resource
类现在实现AutoCloseable
接口,我们可以清理代码以确保在离开try执行范围之前关闭资源:
与非资源尝试技术相比,此过程更加混乱,并保持相同的安全性(资源总是在try
执行范围完成后关闭)。如果执行了上述try-with-resources语句,我们将获得以下输出:
为了证明这种尝试资源技术的安全性,我们可以更改someAction()
方法以抛出Exception
:
如果再次重新运行try-with-resources语句,则会获得以下输出:
即使一个通知,Exception
在执行被抛出someAction()
的方法,我们的资源被关闭,然后将Exception
被抓住了。这样可以确保在离开try
执行范围之前,确保我们的资源已关闭。同样重要的是要注意,资源可以实现Closeable
接口并且仍然使用try-with-resources语句。实现AutoCloseable
接口与Closeable
接口之间的区别在于分别从close()
方法签名引发的异常类型:Exception
和IOException
。在我们的案例中,我们仅更改了close()
方法的签名以不引发异常。
4. 最终类和方法
在几乎所有情况下,我们创建的类都可以由其他开发人员扩展并进行定制,以适应该开发人员的需求(我们可以扩展自己的类),即使扩展我们的类不是我们的意图。尽管这在大多数情况下就足够了,但有时我们不希望重写某个方法,或更广泛地说,是扩展我们的一个类。例如,如果我们创建一个File
封装文件系统上文件读写的类,则我们可能不希望任何子类覆盖我们的read(int bytes)
and write(String data)
方法(如果这些方法中的逻辑发生了变化,则可能导致文件系统损坏)。在这种情况下,我们将不可扩展的方法标记为final
:
现在,如果另一个类希望重写read或write方法,则会引发编译错误:Cannot override the final method from File。我们不仅记录了我们的方法不应被覆盖,而且编译器还确保了在编译时强制执行此意图。
将此思想扩展到整个类时,有时我们不希望扩展自己创建的类。这不仅使我们类的每个方法都是不可扩展的,而且还确保了永远都不能创建我们类的子类型。例如,如果我们正在创建使用密钥生成器的安全框架,则我们可能不希望任何外部开发人员扩展密钥生成器并覆盖生成算法(自定义功能在密码学上可能会劣于系统,并损害系统):
通过创建我们的KeyGenerator
类final
,编译器将确保没有任何类可以扩展我们的类并将其作为有效的加密密钥生成器传递给我们的框架。尽管将generate()
方法简单地标记为似乎足够了final
,但这并不能阻止开发人员创建自定义密钥生成器并将其作为有效生成器传递。考虑到我们的系统是面向安全性的,因此最好不要与外界信任(聪明的开发人员可以通过更改KeyGenerator
类中其他方法的功能来更改生成算法,如果这些方法当下)。
尽管这似乎公然无视了“ 开放/封闭原则”(确实如此),但这样做是有充分理由的。从上面的安全示例可以看出,很多时候我们没有允许外部世界使用我们的应用程序执行它想要做的事情的奢望,我们在决策继承时必须非常谨慎。诸如Josh Bolch之类的作家甚至认为,应该故意将一个类设计为可扩展的,或者应该为扩展而明确关闭该类(Effective Java)。尽管他故意夸大了这个想法(请参见记录继承或禁止继承)),他提出了一个很重要的观点:我们应该非常仔细地考虑应该扩展哪些类,以及哪些方法可以覆盖。
结论
虽然我们编写的大多数代码仅利用Java的部分功能,但足以解决我们遇到的大多数问题。有时候,我们需要更深入地研究该语言,并清除掉那些被遗忘或未知的语言部分,以解决特定的问题。其中一些技术(例如协变返回类型和交集通用类型)可以在一次性情况下使用,而其他技术(例如可自动关闭的资源以及最终方法和类)可以并且应该更经常用于产生更易读和可理解的信息。更精确的代码。将这些技术与日常编程实践相结合不仅有助于更好地理解我们的意图,而且有助于更好地编写更好的Java。