如何写出高质量代码

 

1、Java 设计类和接口的八条优秀实践清单 

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

类之间,隐藏内部数据和实现细节,只通过API进行通信。

信息隐藏的好处:模块可独立开发测试优化,并行开发,降低大型系统的风险等。

 

清单2: final不一定不可变
很多人容易把final跟不可变划上等号,但是,final限制的只是引用不可变,

也就是说,一个final数组,你不能把它指向另一个数组,但是你可以修改数组的元素。

看下面这段代码,TestFinal提供了一个final的数组,然后以为final了就无敌了,自以为是的加了public修饰符


public class TestFinal {
public static final String[] VALUES = {"1","2","3"};
}

接着,Test类来调用了

public class Test {
public static void main(String[] args) {
String[] arr = {"1","2","3"};
// TestFinal.VALUES = arr; // cannot be assigned because of final
TestFinal.VALUES[0] = "11"; // but u can change the sub item
}
}

它修改了TestFinal的final数组的角标为0的元素,而且还修改成功了。
那么,要怎样做,才能既对外提供这个数组的访问权限,又让外界不能修改数组的子元素呢?

一种方法是使用Collections.unmodifiableList暴露一个不可修改的List


public class TestFinal {
private static final String[] PRIVATE_VALUES = {"1","2","3"};
public static final List<String> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
}

这样外界在修改的时候会抛出java.lang.UnsupportedOperationException
另一种方法是提供一个get方法,返回一个clone对象


public class TestFinal {
private static final String[] PRIVATE_VALUES = {"1","2","3"};
public String[] getValues()
{
return PRIVATE_VALUES.clone();
}
}

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

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

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

2) 把类声明为final,保证不被扩展

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

4) 使所有域都是private

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

 

清单4 复合优先于继承
继承有一个天然的缺陷,子类依赖于超类的特定功能,和清单1所提到的封装相违背,而包装模式的复合,则可以解决这个问题。

关于这条清单的详细说明,请读者移步到专栏的另一篇文章,Java继承的天然缺陷和替代方案

 

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

禁止继承的两种方法:

1)把类声明为final

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

 

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

有个类违法了这个原则:


public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}

然后下面这个子类覆盖了overrideMe方法:

import java.util.*;

public final class Sub extends Super {
private final Date date; // Blank final, set by constructor

Sub() {
date = new Date();
}

// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(date);
}

public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}

由于超类的构造器会在子类构造器之前执行,因此会有两次打印,而且第一次打印的是null,因为父类构造器先于子类构造器执行,如果这里调用了date的方法,那么就会导致NullPointer异常。

 


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

而这种标签的方式,在面向对象的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 = length;
this.width = width;
}

double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError();
}
}
}

可以看到,代码里充斥这各种枚举和条件语句,一旦要新增类型,修改时很容易遗漏。
用Java面向对象的思维,改造一下:

// Class hierarchy replacement for a tagged class
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 = width;
}
double area() { return length * width; }
}

改造后的代码,简单清楚,而且很容易扩展。


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

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

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

2)接口可以实现非层次结构的类型框架

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

你可以这样做:


public interface Singer {
String sing();
}

public interface Singer {
String sing();
}

public interface SingerSongwriter extends Singer, SongWriter {

}

当然,抽象类也有它的优势:
1)抽象类可以包含一些方法的具体实现,接口不行。 如果使用接口,一般都要提供一个骨架实现类,客户端可以去继承这个骨架实现类来使用方法的具体实现。

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

 

总结一下:

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

 

Java 设计方法的五条优秀实践清单 

清单1: 检查参数的有效性
在每个方法的开头检查方法的参数,遵循“应该在发生错误之后尽快检测出错误”这一原则。

对于公有的方法,对于校验失败的入参,抛出异常,常见的有IllegalArgumentException(非法参数异常)、Arithmeticexception(运算条件异常)等,并在Javadoc里进行说明。

对于私有方法,不像public方法需要防范外界的不可信任性,private方法是给创建者自己使用的,因此遵循的是一种契约关系,因此对于参数的校验,可以使用assert断言。使用断言的好处是,在开发阶段,可以使用-ea来开启断言,保证调用者入参的准确性,在生产环境,可以禁用断言,去掉参数检查,提高性能。关于java assert的更多信息,可以参考这篇博客,断言绝对不是鸡肋

当然,有时候,有效性检查已经在方法后续的执行过程中完成,比如Collection.sort方法,会在排序中校验对象是不是可以互相比较,那么就不必在调用sort方法之前进行检查了。

 

清单2: 必要时进行保护性拷贝
我们设计的方法,往往会在不经意间,给外界提供修改对象内部状态的机会,破坏了类的不可变性(不可变性,参考专栏的另一篇文章Java 设计类和接口的八条优秀实践清单——清单3 使类的可变性最小化)。

比如下面这个类,它声称可以表示一段不可变的时间周期:


public final class Period {
private final Date start;
private final Date end;

/**
* @param start the beginning of the period
* @param end the end of the period; must not precede start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
this.start = start;
this.end = end;
}

public Date start() {
return start;
}
public Date end() {
return end;
}
}

虽然这个类给Date对象声明了final,但如同专栏之前说的,final只是引用不可变,客户端很容易去修改start和end:

Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78);

同时由于类提供了返回start和end的方法,客户端还能这样修改:

Date start = new Date();
Date end = new Date();
p = new Period(start, end);
p.end().setYear(78);

解决这个问题的关键在于进行保护性拷贝,给客户端返回一个新的对象,使得客户端在修改它获取到的对象时,不会影响原来的对象,加入保护性拷贝后的类如下:
public final class Period {
private final Date start;
private final Date end;

public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());

if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(start + " after " + end);
}

public Date start() {
return new Date(start.getTime());
}

public Date end() {
return new Date(end.getTime());
}

public String toString() {
return start + " - " + end;
}
}

修改后的类,在构造器中,不直接使用外部传入的对象,而是进行一次拷贝;同时在每个返回内部属性的方法,不直接返回原有对象,也做一次拷贝,这样,这个类才是真正的不可变类。
这里的构造器,为什么要先拷贝再校验呢?不能这样写吗:


public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);

this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}

原因是,如果这样写,就会有一个漏洞——在执行完校验和进行拷贝的这段时间,外部传入的start和end对象有可能被修改,而不再满足我们的校验条件。这种在计算机安全术语中,叫做Time-Of-Check/Time-Of-Use或者TOCTOU攻击。
到这里,我们终于把这个类做到不可变了,但我们回头想想,如果一开始我们不使用Date对象,而是直接使用Date.getTime()的long基本类型来表示的时间,不就不需要保护性拷贝了吗?由此得到另一个启示——尽量使用不可变对象作为对象内部的组件。

 

清单3: 避免过长的参数列表
好的方法,参数不能超过4个,超过4个,方法就不方便使用。

有三种方法可以缩短过长的参数列表:

1、把方法分解成多个独立功能的方法

很多时候,我们的方法参数过多,是因为实现的功能太复杂。比如,java的List并没有提供“在子列表中查找元素第一个索引和最后一个索引”的方法,如果提供,那么方法就需要三个参数:子列表的开始索引、子列表的结束索引、要查找的元素;List方法提供了subList、indexOf和lastIndexOf方法,客户端通过组合使用这三个防范,就能实现这个功能。

2、使用辅助类,存储原来的参数,方法入参改为辅助类即可

3、使用builder模式,参考专栏的另一篇文章  Java创建对象的方法清单 —— 原来还可以这样创建对象 


清单4: 返回0长度的数组或者集合,而不是null
如果一个方法声明返回的是一个数组或者集合,那么当你打算返回null时,请返回一个长度为0的数组或者集合,这样能让调用者省去null对象校验。

有人可能会担心每次都创建一个空的数组或者集合去返回会影响效率,那么完全可以先创建好不可变的空的数组或者集合。

对于数组,只需要加上final修饰符,那么,零长度的数组就是不可变的。

private static final String[] EMPTY_STR_ARRAY = new String[0];

对于集合,Collections类提供了emptyList、emptySet、emptyMap方法,同样可以返回一个不可变的空集合。

 

清单5: 为每个方法编写Javadoc文档注释
工作这段经历,让我深刻地体会到,没有注释,读懂代码很辛苦;良好且正确的注释,可以帮助更快更好地去读懂代码。

一个方法的注释应该包含以下几个部分:

方法概要描述:往往是一个简单的动词

方法使用后产生的一些副作用等详细描述:比如是否会启动了线程,是不是线程安全等

方法入参描述:使用@param

方法抛出的异常描述: 往往要在异常后面写上什么情况下会抛出

示例:


/**
* Returns the element at the specified position in this list.
*
* This method is thread-safe, and it will start a thread.
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* (<tt>index < 0 || index >= size()</tt>)
*/

关于Java注释的更多指导,可以参考Oracle最权威的指导文档  How to Write Doc Comments for the Javadoc Tool

 


上面两个分别介绍了在Java语言中,设计类和接口,以及设计方法的一些通用原则,而在了解完如何设计类接口和方法之后,就要去写它们的具体实现细节,这也就是本文将重点介绍的以下5个清单。

 

清单1: 将局部变量的作用域最小化
这条清单,其实是封装原则的一个衍生,我们都知道,封装是使得一个类的属性不被其他类所访问和篡改,那么在一个代码块里面,我们同样需要让代码块里的各个子代码块的局部变量,不互相影响。

要做到这条原则,有两个小原则:

在第一次使用局部变量的地方再去声明它;
使方法小而集中,如果把两个操作合并到一个方法,那么其中一个操作的局部变量就会有可能被另一个操作误用。
以下面这两个while循环为例,本意是遍历两个List,但是由于后面的一个while循环,是直接复制上面那个循环然后粘贴的,使用的是上面循环的迭代器,但是由于上面循环的iterator仍在有效范围,因此编译还通过了:


public class TestCycle {
public static void main(String[] args) {
testCycle();
}

private static void testCycle() {
String[] sArr = {"1","2","3"};
List<String> strings = Arrays.asList(sArr);
List<String> strings2 = Arrays.asList(sArr);

Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()) {
String string = (String) iterator.next();
System.out.println(string);
}

Iterator<String> iterator2 = strings2.iterator();
//copy from the while code above, make wrong!
while (iterator.hasNext()) {
String string = (String) iterator.next();
System.out.println(string);
}
}
}


这个例子说明了不控制好局部变量作用域的代价,要修复这个隐藏的缺陷,我们可以使用for循环或者foreach循环:


for(Iterator<String> iterator = strings.iterator(); iterator.hasNext();)
{
String string = (String) iterator.next();
System.out.println(string);
}


使用for循环可以很好的将局部变量的作用域限制在特定的代码块中,当然,我们更推荐使用foreach,为什么?请看下一条清单。


清单2: for-each循环优先于传统for循环
从上一条清单的例子,我们知道,for循环比while好,但是它也并非完美,for循环暴露了迭代器和索引变量,会造成一些混乱,比如下面这个双层for循环,在第二层for循环中调用了第一层for循环的迭代器的next方法,破坏了整个循环的顺序:


for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));


代码原意应该是这样:


for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
{
Suit suit = i.next();
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(suit, j.next()));
}


这个例子说明了暴露迭代器的后果,使用for-each可以完美的隐藏迭代器和索引变量:


for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));


for-each循环不仅仅可以遍历集合和数组,凡是实现了iterable接口的对象,都可以使用for-each去遍历,因此,如果你正在编写的类型表示的是一组元素,那么如果你给它实现了iterable接口,允许用户使用for-each去遍历,会令用户感激不尽的!
当然,由于隐藏了迭代器和索引,因此在一些需要使用到迭代器或者索引的场合,比如遍历并删除(参考  Java集合遍历删除),那么for-each是不适用的,但是大多数情况下,for-each可以满足我们的需要并且实现十分简洁和高质量的代码。

 

清单3: 了解和使用类库  不要重复制造轮子
JDK提供了很多有用的类库,比如随机函数Random.nextInt(int),集合框架,并发工具类库等。

在代码实现中,我们应该优先使用这些提供好的类库,而不是自己去实现。

使用jdk类库的好处在于:

这些方法,经过了大量的测试发行和成千上万程序员的使用,可靠性得到保障;
不必浪费时间提供和业务功能无关的解决方案;
这些方法的性能会随着每一次jdk的版本更新而提升;
使自己代码融入主流,更易读,更加容易维护;
从经济学的角度来看,使用已有类库的功能,是一种成本最小的方法。

 

清单4: 基本类型优先于装箱类型
经常发现有代码这样写:

private Long mills = ....

大概他是忘了Long还有一个基本类型long吧。

装箱类型和基本类型相比,有两个不同点:

在进行 == 比较时,装箱类型比较的是两个对象是不是同一对象,而不是比较值是不是相同;
装箱类型可以为null;
这两点都会给正常的运算带来不必要的麻烦,当然从性能上看,基本类型也比装箱类型快。

所以,除非你是在需要使用装箱类型的场合(比如List<Long>),否则基本类型总是优先于装箱类型。

 

转载于:https://www.cnblogs.com/yhxb/p/11004919.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值