第4章 类和接口

Posted by 程序亦非猿 on 2016-06-20

第4章 类和接口

类和接口是Java程序设计语言的核心,它们也是Java语言的基本抽象单元

初学Java的时候感觉类和接口都好简单,后续慢慢发现类和接口的设计并非是想象总的那么简单的,还有好多需要学习。

而这一章作者就阐述了一些指导原则,指导我们设计出更加有用、健壮和灵活的类和接口,很有意义。

第13条 使类和成员的可访问性最小化

设计良好的模块对外部而言总是隐藏了所有的细节

模块之间中通过它们的API进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏封装,是软件设计的基本原则之一(还是封装好听啊,信息隐藏好low啊)

封装可以有效解除各个模块之间的耦合关系,是现在模块化开发的基础,使模块可以独立地开发、测试、优化、使用、理解和修改

尽可能地使每个类或者成员不被外界访问。

Java中可以通过访问修饰符(private protected public)控制类、接口和成员的可访问性(accessibility)

除了应该暴露的API之外,我们应该尽可能少的开放访问权。
因为一旦暴露给外界,可能会有风险,另外还需要保证一直维护与兼容。

可以想象一下,当我们使用一个第三方库的时候,如果它暴露了一个不该暴露的类(假设为BitmapUtil),而我们恰好用了它里面的方法,结果它一升级,把方法改了,或者把方法去掉了,那我们不是懵了?

所以不要去暴露那些不该暴露的类、接口、成员

小结

作者在最后给了小结:应该始终尽可能地降低可访问性。应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形之外,共有类都不应该包含公有域。并且要确保公有静态final域所引用的对象都不是可变的

第14条 在共有类中使用访问方法而非公有域

假设有一个类:

1
2
3
4
public class Point{
public double x;
public double y;
}

作者的意思是不应该直接暴露x y,要为它们提供gettersetter方法,这样有利于添加约束条件,辅助行为。

本人表示道理我懂,但是现在一般的类都不愿意去写getter setter了,真心觉得好烦啊,虽然方法可以用AS自动生成,我还是不太愿意去写,直接public就是那么任性

第15条 使可变性最小化

不可变类:实例不能被修改的类每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。如:String、基本类型的包装类、BigInteger和BigDecimal
不可变类比可变类更加易于设计、实现和使用。它们不容易出错,且更加安全。

不可变类的五条规则:

  1. 不要提供任何会修改对象状态的方法。
  2. 保证类不会被扩展。 一般用final修饰
  3. 使所有的域都是final的。
  4. 使所有的域都成为私有的。(降低访问权限)
  5. 确保对于任何可变组件的互斥访问。(什么意思呢?)

不可变类的优缺点

线程安全

不要求同步,无惧多线程并发访问

不可变对象可以被自由地共享

所以不需要保护性拷贝(如String类的拷贝构造器)
也可以重复利用,如:Boolean.FALSE/TRUE

不可变对象为其他对象提供了大量的构建(building blocks)

不是很懂

唯一的缺点:对于每个不同的值都需要一个单独的对象

如FALSE TRUE ,不过如果值少,到也没什么关系
但是如String这样的不可变类,我们需要注意,多用StringBuilder(可变,性能好)

小结

除非有很好的理由要让类成为可变的类,否则就应该是不可变的。
如果类不能被做成是不可变的,仍然应该尽可能地限制它的可变性。(降低出错的可能性)

第16条 复合优先于继承

在读HeadFirstDesignPattern的时候已经看到太多次了

这里的继承是指 实现继承(implementation inheritance)也即extends 而不是接口继承 复合(composition)也应该是常听到的组合

继承打破了封装性 子类依赖于超类中特定功能的实现细节 当超类发生改变,子类可能会遭到破坏

比如随着版本的发布,超类需要新增方法,但是这些方法不是所有子类需要的,那么就破坏了子类!

复合(组合)

复合,即使用包装类(wrapper class),其实这也就是设计模式中的装饰者模式

另外值得一提的是,复合以及转发并不是委托(delegation)

装饰者模式的优缺点不多说了,可以看设计模式的笔记

小结

继承功能确实强大,但它也存在很多问题,比如违背了封装原则(是不是很矛盾?),对于两个类,它们确实有is-a的关系时候才使用继承!

所以使用继承的时候要考虑清楚

第17条 要么为继承而设计,并提供文档说明,要么就禁止继承

好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的  

如标题,恩,我可是连注释都懒得写的人,怎么会写文档。。。

第18条 接口优于抽象类

Java提供两种机制用来定义允许多个实现的类型:接口抽象类

区别

接口和抽象类的区别有很多,其中 最为明显 的区别是: 抽象类可以包含某些方法的实现,而接口不允许,即接口都是抽象方法

而另外还有一个 更重要 的区别是:为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。而Java是单继承的,所以抽象类作为类型定义受到了极大的限制,而接口没有这个限制。

接口的优势

  • 现有的类可以很容易被更新,以实现新的接口
    当需要增加方法的时候只需要implements具体的接口即可,非常方便,而如果通过抽象类来实现,则需要在抽象类里新增方法,而这会导致其他继承该抽象类的类也被强制加上额外的方法!

  • 接口是定义mixin(混合类型)的理想选择
    mixin是指主要的类型: 类除了实现它的”基本类型”之外,还可以实现这个mixin类型(一脸懵逼!这翻译的什么玩意?) Comparable是一个mixin接口

我的理解是:一个类,利用实现多个接口可以达到混合类型的目的,而利用抽象类只能继承一个类,则不能达到混合类型的效果!

  • 接口允许我们构造非层次结构的类型框架

第19条 接口只用于定义类型

我们知道当类实现接口时,我们可以把该类的类型当做是接口的类型来使用,这是我们定义接口的唯一目的,也即接口 只应该用来定义类型

看到这里,可能你会跟我一样奇怪,接口不就是用来定义类型的吗,还能用来干嘛?

有一种接口被称为 常量接口 ,就是没有方法,只有常量的接口,这常量接口模式是对接口的不良使用,因为它没什么卵用还会污染实现类

Java中有几个常量接口,如java.io.ObjectStreamConstants,千万别学啊!~
建议常量用工具类或者枚举或者@IntDef注解来实现

第20条 类层次优于标签类

标签类,书中对它的定义说得很拗口。
我的理解是一个类,拥有多个风格,通过一个属性来区分不同的风格,类里充斥着if else或者switch case

举个例子:

1
2
3
4
5
6
7
8
9
10
class Person{
boolean isMan;
String sayHi(){
if (isMan) {
return "Yo hi man!~";
}else{
return "Hello";
}
}
}

Person类,通过isMan属性来区分是男的还是女的,sayHi()方法针对男女有不同的表现,这个就是一个非常简单的标签类

标签类的缺点非常明显,当你要表现的风格非常多样的时候,你需要写大量的判断语句,非常容易出错,而且当你需要修改某一个风格的时候,你需要在一大堆代码里找出你要改的地方,很有可能引入bug,非常难以维护。

这个时候,将标签类转变成类层次就非常方便了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Person{
abstract String sayHi();
}
class Man extends Person{
@Override
String sayHi() {
return "Yo hi man!~";
}
}
class Woman extends Person{
@Override
String sayHi() {
return "Hello";
}
}

抽象出一个Person类,定义ManWoman类继承它,根据自己的需求实现sayHi()方法,当需要修改Man的行为时,你不需要也不用担心会破坏Woman的代码,代码可读性,可维护性一下子高了很多,有木有?!

第21条 用函数对象表示策略

函数对象的概念:如果一个类仅仅导出这样的一个方法(执行其他对象(这些对象被显示传递给这些方法)上的操作),它的实例实际上就等同于一个指向该方法的指针.
这样的实例被称为函数对象(function object).

如:

1
2
3
4
5
class StringLengthComparator{
public int compare(String s1,String s2){
return s1.length()-s2.length();
}
}

StringLengthComparator类即是一个典型的函数对象,指向StringLengthComparator对象的引用可以被当做是一个指向该比较器的函数指针(function pointer),它也是一个用于字符串比较操作的具体策略,这个策略策略模式中的策略,并且函数指针的主要用途就是实现策略模式.(关于策略模式这里就不多讲,推荐看<>,如果你不着急,可以等我出笔记~)

PS:指针的英语是 pointer

第22条 优先考虑静态成员类

嵌套类(nested class)是指被定义在另一个类的内部的类,它存在的目的应该只是为它的外围类(enclosing class)提供服务.

嵌套类分为四种:

  1. 静态成员类(static member class) (不是内部类
  2. 非静态成员类(nonstatic member class)
  3. 匿名类(anonymous class)
  4. 局部类(local class)

除了第一种之外,其他三种都被称为 内部类(inner class)

静态成员类 & 非静态成员类

最简单的嵌套类,最好把它看成普通的类,只是碰巧被声明在另一个类的内部而已(挺不错的解释),所以它可以脱离外部类单独存在

它可以访问外部类的所有成员,包括private修饰的,也可以被访问修饰符修饰,来控制它的可见性。

作用:静态成员类一般是用来辅助外部类的,比如CoordinatorLayout类中的Behavior类,它定义了一系列的行为用于辅助它的外部类CoordinatorLayout,例子有很多,不多说了。

静态成员类 & 非静态成员类 的差别

虽然它们只差了一个static修饰符,但是其实它们差别巨大。

非静态成员类的实例都隐含持有一个外部类的实例(enclosing instance)

这不仅仅会消耗更多的空间,还可能会导致外部类的实例泄漏,内存泄漏,而静态成员类并不会。

这在Android中很常见,比如我们使用Handler的时候,AS都会提示我们这可能造成内存泄漏,让我们使用静态成员类。

所以通常情况更推荐静态成员类,书中有一句话:如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中,使它成为静态成员类,而不是非静态成员类

匿名类 & 局部类

匿名类,没有名字,也不是外部类的成员,它是在使用的同时被声明和实例化

需要注意的是:当匿名类出现在非静态的环境中时,它会持有外部类的实例,所以它可能引起内存泄漏。

匿名类的作用:通常用于创建函数对象(见21条),比如ThreadRunnable

局部类非常少用,自己没用过,在源码里也没留意到它的存在,就不多写了。

小结

虽然本条推荐静态成员类,不过每个嵌套类都有自己的用途,还是得按实际情况去抉择。

本章小结

本章的内容在Java中非常重要,如果要提升架构能力,那么本章的学习必不可少!

本系列作品

所有的读书笔记
第2章 创建和销毁对象
第3章 对于所有对象都通用的方法
第4章 类和接口