七条优秀实践Java设计类和接口的清单

本文探讨了设计Java类和接口的几个关键原则,包括最小化可访问性、最小化可变性、优先使用复合、谨慎设计继承、避免构造器调用可覆盖方法、使用类层次优于标签类以及接口优于抽象类。这些原则有助于提高代码的封装性、可维护性和扩展性。
摘要由CSDN通过智能技术生成

写Java时,其实就是在写各种类和接口,那么在设计类和接口的时候,有哪些优秀实践呢?

本文结合《Effective Java》第四章《类和接口》和自己的理解及实践,讲解了设计Java类和接口的优秀指导原则,供您参考。

清单1 - 使类和成员的可访问性最小化
这个原则,其实就是我们常说的“封装”,也是软件设计的基本原则之一。

类与类之间,隐藏内部数据和实现细节,只通过API进行通信,我们常见的set、get方法都是非常好的例子。

信息隐藏的好处:各个模块可独立开发、团队成员可以并行开发、减少过多的暴露从而降低大型系统的风险等。

清单2 - 使类的可变性最小化
不可变类是实例不能被修改的类,这种类具有天然的线程安全特性,不需要同步,也不需要进行保护性拷贝。

设计一个不可变类的四条规则:

不提供任何修改对象状态的方法

把类声明为final,保证不被继承和扩展

把所有的域都声明为final,这样可以更清楚的表明意图

使所有域都是private

不可变类唯一的缺点就是,对于每个不同的值都需要创建一个单独的对象,性能差。比如String,因此,对于不可变类,我们一般都会提供一个可变配套类,比如String对应的可变配套类就是StringBuilder和StringBuffer。

清单3 - 复合优先于继承
继承有一个天然的缺陷,子类依赖于超类的特定功能,和清单1所提到的封装原则相违背,而采用基于包装模式的复合机制,则可以弥补继承的缺陷。

清单4 - 要么为继承而设计,并提供文档说明,要么就禁止继承
既然继承有清单3中所讲的缺陷,那么就不要轻易提供继承的能力。

禁止继承的两种方法:

1)把类声明为final

2)构造器私有或者包级私有

清单5 - 构造器不能调用可被覆盖的方法
为直观说明这个原则,下面举个例子:

有个类在它的构造器中调用了一个不是final的方法:

public class Super { public Super() { overrideMe(); } public void overrideMe() { }}
然后下面这个子类覆盖了overrideMe方法:

import java.util.*;public final class Sub extends Super { private final Date date; Sub() { date = new Date(); } @Override public void overrideMe() { System.out.println(date); } public static void main(String[] args) { Sub sub = new Sub(); http://xingkongmj.com/news/id/71.htmlsub.overrideMe(); }}
由于超类的构造器会在子类构造器之前执行,因此会有两次打印,而且第一次打印的是null,因为父类构造器先于子类构造器执行,如果这里调用了date的方法,比如:

date.getDay()
那么就会导致NullPointer异常。

清单6 类层次优于标签类
在面向过程的编码中,常常会使用标签,当标签等于某个值的时候,是一种代码逻辑,当标签等于另一个值的时候,执行另一套逻辑。

而这种标签的方式,在面向对象的Java里面,都应该被抽取为超类和子类。

举个简单的例子,下面是一个标签类,可以表示圆形或者矩形:

class Figure { enum Shape { RECTANGLE, CIRCLE }; // Tag field - the shape of this figure final Shape shape; // These fields are used only if shape is RECTANGLE double length; double width; // This field is used only if shape is CIRCLE double radius; // Constructor for circle Figure(double radius) { shape = Shape.CIRCLE; this.radius = radius; } // Constructor for rectangle Figure(double length, double width) { shape = Shape.RECTANGLE; this.length =http://xingkongmj.com/news/id/72.html length; this.width = width; } double area() { switch(shape) { case RECTANGLE: return length * width; case CIRCLE: return Math.PI * (radius * radius); default: throw new Asserti(); } }}
可以看到,代码里充斥这各种枚举和条件语句,一旦要新增类型,修改时很容易遗漏。

用Java面向对象的思维,改造一下:

抽象类:

abstract class Figure { abstract double area();}
圆形:

class Circle extends Figure { final double radius; Circle(double radius) { this.radius = radius; } double area() { return Math.PI * (radius * radius); }}
矩形:

class Rectangle extends Figure { final double length; final double width; Rectangle(double length, double width) { this.length = length; this.width =http://xingkongmj.com/news/id/73.html width; } double area() { return length * width; }}
改造后的代码,简单清楚,而且很容易扩展,以后如果需要新增形状,只需要新增一个类即可,不需要修改原来的代码。

清单7 - 接口优先于抽象类
接口和抽象类都可以让实现或者继承它们的类,具有某些特定的函数模板。

和抽象类相比,接口具有以下优势:

一个类可以实现多个接口,但是却只能继承一个类。想一下,假如Comparable接口当初被设计为一个抽象类了,那由于Java的单继承的特点,我们很多客户端的代码就都无法做到Comparable了。

接口可以实现非层次结构的类型框架。清单7里讲到的圆形和矩形是层次结构,因为一个图形不可能同时既是圆形又是矩形,但是,我们常常会遇到非层次结构的类型,比如歌唱家和作曲家,这俩就是非层次结构的,因为有的歌唱家本身也是作曲家。这就只能用接口来实现了,因为Java给了接口一个特权——接口可以多继承。

你可以这样做:

public interface Singer { String sing();} http://xingkongmj.com/news/id/74.htmlpublic interface SongWriter { String write();} public interface SingerAndSongWriter extends Singer,SongWriter {}
当然,抽象类也有它的优势:

抽象类可以包含一些方法的具体实现,而JDK1.8之前的接口则不行。 在JDK1.8之前,如果使用接口,一般都要提供一个骨架实现类,客户端可以去继承这个骨架实现类来使用方法的具体实现。在JDK1.8之后,可以使用default在接口中编写方法的默认实现。

抽象类的演变比接口的演变要容易得多。抽象类可以随意添加新的方法,但是接口不行,一旦接口新增了方法,之前实现了这个接口的类就无法编译通过。

因此,接口通常是定义允许多个实现的类型的最佳选择。但是,当演进的容易性被更重视,或者说,后续修改的可能性更大时,这种情况下,就应该使用抽象类。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值