Effective Java 读书笔记(3)

2017-12-06  本文已影响11人  Lin_Shao

类和接口

类和接口是Java程序设计语言的核心,它们也是Java语言的基本抽象单元,Java语言提供了许多强大的基本元素,供程序员用来设计类和接口。以下指导原则,可以帮助你更好地利用这些元素,设计出更好用、健壮和灵活的类和接口。

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

对于模块化开发,设计良好的模块会隐藏所有的实现细节,把它的API与实现分离,模块之间通过API通信,无需关注实现细节,从而降低各个模块的耦合度。

Java提供了许多的机制类协助我们隐藏实现细节。访问控制机制(access control)决定了类、接口和成员的可访问性,我们可以通过访问控制尽可能最小化我们开发的类和成员的可访问性,隐藏其实现细节,对外仅仅暴露接口即可。

对外暴露的API、protected成员和方法,必须永远得到支持。

实例域绝不能是public的,包含public实例域的类并不是线程安全的,因为你无法对这个域实施有效的控制。

对于类中的常量,我们通常用public static final修饰,按照惯例,这些常量的名称通常由大写字母加下划线构成。

**注意,如果public static final修饰的是一个可变域的引用,如数组引用,那么这个域跟实例域一样,不是线程安全的。假设我们有
···
public static final String PRIVATE_VALUES[] = {"abc","bcd","cde"};
···
对于长度非零的数组的引用,values本身是不能修改的,即以下是非法的:

PRIVATE_VALUES = {"abcde"};

但是,对于数组里面的内容,我们是可以修改的:

PRIVATE_VALUES[0] = "abcde";

对于这个问题,我们可以选择将values设置为private,然后增加一个公有的不可变列表:

public static final List<String> tmp = Coolections.unmodifiableList(Arrays.asList(PRIVATE_VALUES))

java 的访问控制机制,即访问修饰符:
1、private 私有的,类内部才能访问(包括内部类)
2、package-private 包级私有的,缺省的访问级别,同一包下的类可以访问
3、protected 受保护的,子类及包内部的类可以访问
4、public 公有的,在任何地方都可以访问

总而言之,我们应该始终尽可能地降低可访问性。在仔细设计了一个最小的公有API之后,应该防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情况外,公有域都不应该包含公有域。并且必须确保公有静态域所引用的对象都是不可变的。

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

有时候,我们需要写一些用来集中实例的退化类。这些并没有什么作用,只是作为实例对象的一个聚合。由于这种类的数据域是可以被直接访问的,所以很少会被封装。根据面向对象程序设计的原则,我们应该尽量对其用geter/setter进行封装

第15条:使可变性最小化

不可变类是其实例不能被修改的类。每个实例中包含的信息都应该在创建时提供,其后不再提供修改的方法。不可变类,相比于可变类,更加容易设计、实现和使用,不易出错,且更加安全。

为了使类成为不可变的,需要遵循以下五条规则:
1、不要提供任何会修改对象状态的方法;
2、保证类不会被扩展(在类中添加final);
3、使所有的域都是final的,保证线程安全;
4、使所有的域都是private的。对于可变对象的final引用,客户端仍然可以对其进行修改。对于不可变域,声明为private可以保持类内部表达形式的灵活性;
5、确保任何可变组件之间的互斥访问。若类内部有可变对象的引用,确保外部无法访问这些引用。不要直接用客户端传入的引用初始化可变对象的引用,也不要返回这些引用,请使用保护性拷贝技术。

不可变对象本质上是线程安全的,不需要进行同步。而且,不可变对象比较简单,它只有一种状态,不需要我们花费精力在其生命周期的维护上。

对于某些类,本身就要求是可变的,我们也需要让其可变性尽可能小。

第16条:复合优于继承

继承(implement)是实现代码重用的有力手段。在包的内部使用继承是非常安全的,子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计,并且具有良好的文档支持的类来说,使用继承(0包外)也是非常安全的。然而,对于普通的实现类,进行跨越包边界的继承,而是十分危险的。

与方法调用不同,子类继承打破了封装。换句话说,子类依赖于其超类的实现细节。一个普通的实现类,没有在发布的时候有详细的文档说明其可以被继承,那么它随时有可能在后面的分布版本中修改其实现方式。这样,客户端盲目继承的子类可能会遭到破坏。

在这里,我们举一个例子:假设我们对HashSet进行继承,添加一个功能来计算它自创建以来被添加了几个元素。HashSet包含了两个添加元素的方法:add和addAll,我们实现一个HashSet的子类,并覆盖两个方法:

@Override
public boolean add(E e){
  addCount++;
  return super.add(e);
}
@Override
public boolean addAll(Colllection<? extends E> c){
  addCount += c.size();
  return super.addAll(c);
}

这样看起来非常合理。这是它是错的

在HashSet内部,addAll是通过调用多次add实现的,这导致了当调用addAll时,addCount被加多了一倍。

把addAll中的addCount+=c.size()删掉不就好了

真的吗?你怎么能确保在以后的发行版本中,addAll还会调用add实现呢?

导致子类脆弱的一个相关原因是,超类可以在后续的版本中获取新的方法。假设HashSet新增了一个方法,addSet(Set),这样的话,我们的统计就是不正确的了。

幸运的是,我们可以通过复合解决这个问题。我们不再扩展HashSet,而是将其作为这个实例域,这样我们可以有效地控制这个类。对于这个组件,我们可以用新类的方法封装组件的方法:
···
public boolean add(E e){
addCount++;
return mySet.add(e);
}
···
这样,这个组件的访问完全在我们的控制之内。我们之依赖于HashSet对外发布的add方法,不关心其内部实现,即使后来的发行版本中修改了实现方法,只要其功能不对,我们的新类也不会有问题。
这样行为,称之为转发(forwarding);这样的类,称之为转发类,封装的访问称之为转发方法

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

当我们设计一个可继承类时,应该保证以下两点:

对于普通的类,我们要禁止其实例化,,方法有两个:

第18条:接口优于抽象类

Java提供了两种机制用来定义多个实现的类型:接口抽象类。这两种机制之间的最明显的区别在于:抽象类中允许方法实现java只允许单继承,抽象类位于类层次结构中,这使得抽象类作为类型定义收到了极大的限制。而接口脱离于类层次结构,更具扩展性

以下是接口优于抽象类的几个原因:

上一篇下一篇

猜你喜欢

热点阅读