ITKeyword,专注技术干货聚合推荐

注册 | 登录

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

hzy38324 分享于 2017-06-11

推荐:Java本地接口规范设计概述

平台相关代码是通过调用 JNI 函数来访问 Java 虚拟机功能的。JNI 函数可通过接口指针来获得。接口指针是指针的指针,它指向一个指针数组,而指针数组中的每个元

2019阿里云双11返场狂欢继续,
地址https://www.aliyun.com/1111/2019/home

本文结合《Effective Java》第四章《类和接口》和自己的理解及实践,讲解了设计Java类和接口的优秀指导原则,文章发布于专栏Effective 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所讲的缺陷,那么就不要轻易提供继承的能力。

禁止继承的两种方法:

推荐:对面向对象设计的理解—Java接口和Java抽象类

在没有好好地研习面向对象设计的设计模式之前,我对Java接口和Java抽象类的认识还是很模糊,很不可理解。 刚学Java语言时,就很难理解为什么要有接口这个概念,

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抽象类与接口的设计理念差异

Java抽象类与接口都可以实现功能与实现的分离,都对多态提供了很好的支持,那么我们什么时候应该使用抽象类或接口呢?在以前的一篇文章初探Java抽象类与接口中谈

本文结合《Effective Java》第四章《类和接口》和自己的理解及实践,讲解了设计Java类和接口的优秀指导原则,文章发布于专栏Effective Java,欢迎读者订阅。 清单1:使类和成员的可访问性最小化

相关阅读排行


用户评论

游客

相关内容推荐

最新文章

×

×

请激活账号

为了能正常使用评论、编辑功能及以后陆续为用户提供的其他产品,请激活账号。

您的注册邮箱: 修改

重新发送激活邮件 进入我的邮箱

如果您没有收到激活邮件,请注意检查垃圾箱。