4. 类和接口

15. 使类和成员的可访问性最小化

把API与实现清晰地隔离开,组件间通过API进行通信,不需要知道其他模块的内部工作情况,这称为:实现信息隐藏或封装

解耦系统中的各个组件

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

成员(域、方法、嵌套类、嵌套接口)的四种可能访问级别:

  • 私有(private)
  • 包级私有(package-private)
  • 受保护(protected) 被称为“缺省” default
  • 公有(public)

如果一个缺省的顶级类或接口只在某一类内部用到,就应该考虑使它成为哪个类的私有嵌套类

子类覆盖父类方法,访问级别不能低于父类

公有类的实例域绝对不能是公有的,如果是final或者可变对象的final引用,公有就等于放弃了对存储在这个域中值的限制,除非是是为了暴露静态final常量

注意!

常见问题:长度非零的数组总是可变的,用静态final数组域存储也是错误的,解决:

  1. 使用Collections.unmodifiableList() 返回不可变列表
  2. 用clone() 返回私有数组的拷贝

16. 要在公有类而非公有域中使用访问方法

如果类可以在它所在的包之外访问,就提供方法而非暴露数据域,以保留将来改变类内部表示法的灵活性

如果是缺省或私有的嵌套类,直接暴露数据域就没有本质的错误

Java类库反例:java.awt的Point类和Dimension类

17. 使可变性最小化

Java类库不可变类:String、基本类型包装类、BigInteger、BigDecimal

不可变类要遵守规则:

  1. 不提供任何会修改对象状态的方法
  2. 保证类不会被拓展 (final class)
  3. 申明所有的域都是final
  4. 申明所有的域都是私有的
  5. 确保对于任何可变组件的互斥访问

    如果该不可变类具有指向可变对象的域,要确保该类的客户端无法获得指向这些对象的引用,并且不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用

方法结果返回新的实例而不是修改这个实例:这称为函数方法(与之对应的是:过程或命令式方法)

这些 函数方法 方法命名使用介词而非动词,强调不会改变对象值

不可变对象本质上是线程安全的,不要求同步,不可变对象可以自由共享,鼓励尽可能重复用现有的实例:为频繁用到的值,提供公有静态final常量

可以提供静态工厂:把频繁请求的实例缓存起来,用静态工厂提供代替公有构造器可以让以后有添加缓存的灵活性,而不影响客户端

不需要为不可变类提供clone方法或拷贝构造器,Java类库反例:String有拷贝构造器(应尽量少用)

不可变类可以共享内部信息,例如:BigInterger negate方法

不可变类为其他对象提供大量构件:Map Set

不可变类无偿提供失败的原子性,不存在临时不一致的可能性

缺点:每个不同的值都需要一个单独对象,创建大量对象会影响性能,解决:

  1. 用基本类型代替其中一些创建大量对象的多步骤操作
  2. 提供可变配套类:例如 String 对应的 StringBuilder

注意:BigInteger、BigDecimal 最初对于不可变类必须为final没有广泛理解,导致如果编写的一个类安全性依赖于不可信客户端的BigInteger或者BigDecimal参数的不可变性,必须检查是否为真实的而非不可信任子类实例,进行保护性拷贝

注意如果让自己的不可实现类实现Serializable接口,要提供显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectOutputStream.readUnshared方法

Java类库反例:Date、Point 本来应该是不可变类

构造器应该创建完全初始化的对象,并建立起所有的约束关系

18. 复合优先于继承 (此条不适用于接口继承)

包的内部使用继承非常安全,但是普通的具体类进行跨包的继承非常危险

与方法调用不同的是,继承打破了封装性:子类依赖于其父类中特定功能的实现细节,父类的实现可能随着版本的不同而变化,子类会遭到破坏

父类方法:自用性是实现细节,不是承诺

非重写父类方法也有风险:父类后续的发行版可能与你的子类提供签名相同返回值不同或者是签名和返回值都相同的方法

复合(composition):不依赖父类的实现细节,不拓展现有的类,而实在新的类中增加一个私有域,它引用现有类的实例

实现包括两部分:转发类(包含了所有转发方法),以及类本身

public class ForwardingSet<E> implements Set<E> {
private final Set<E> s; public ForwardingSet(Set<E> s) {
this.s = s;
} @Override
public int size() {
return s.size();
} @Override
public boolean isEmpty() {
return s.isEmpty();
} @Override
public boolean contains(Object o) {
return s.contains(o);
} @Override
public Iterator<E> iterator() {
return s.iterator();
} @Override
public Object[] toArray() {
return s.toArray();
} @Override
public <T> T[] toArray(T[] a) {
return s.toArray(a);
} @Override
public boolean add(E e) {
return s.add(e);
} @Override
public boolean remove(Object o) {
return s.remove(o);
} @Override
public boolean containsAll(Collection<?> c) {
return s.containsAll(c);
} @Override
public boolean addAll(Collection<? extends E> c) {
return s.addAll(c);
} @Override
public boolean retainAll(Collection<?> c) {
return s.retainAll(c);
} @Override
public boolean removeAll(Collection<?> c) {
return s.removeAll(c);
} @Override
public void clear() {
s.clear();
}
}
public class InstrumentedHashSet<E> extends ForwardingSet<E> {

    private int addCount = 0;

    public InstrumentedHashSet(Set<E> s) {
super(s);
} @Override
public boolean add(E e) {
addCount++;
return super.add(e);
} @Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
} public static void main(String[] args) {
InstrumentedHashSet<String> strings = new InstrumentedHashSet<>(new TreeSet<>());
strings.addAll(Arrays.asList("1", "2", "3"));
System.out.println(strings.addCount); // 3 }
}

InstrumentedHashSet实例把另一个Set实例包装起来了,所以 InstrumentedHashSet类被成为包装类,这就是:Decorator(修饰者)模式

注意!包装类不适合用于回调框架:回调框架中,对象把自生的引用传递给其他对象用于后续回调,包装类不知道自己是一个包装类,传递了指向自生(this)的引用,回调时避开了外面的包装对象(SELF问题)

只有当子类真正是父类的子类型才适合用继承(is-a关系)

Java类库反例:Stack继承Vector Properties继承Hashtable

暴露内部实现细节,客户端可以直接访问内部细节,直接修改父类,从而破坏子类约束

19. 要么设计继承并提供文档说明,要么禁止继承

对于专门为了继承而设计的类,必须精确地描述覆盖每个方法所带来的影响

关于程序文档的一句格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述他是如何做到的

为了继承的类必须精心挑选受保护(protected)的方法的形式,提供适当的钩子(hook),以便进入其内部工作

允许继承的类的构造器不能直接或间接地调用可能被覆盖的方法,因为父类构造器在子类构造器之前运行,子类中覆盖的方法会在子类构造器之前运行

而通过构造器调用私有方法、final方法、静态是安全的,因为它们都是不可被覆盖的方法

在为了继承而设计的类中实现Cloneable和Serializable接口,要注意clone和readObject方法,类似于构造器,不能直接或间接调用可覆盖方法

所以对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化

  1. final修饰类
  2. 私有或包级私有构造器,并增加公有静态工厂

20. 接口优于抽象类

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

Java 8 为继承引入了缺省方法

一般来说,无法更新现有的类来拓展新的抽象类,如过希望两个类拓展一个抽象类,就必须把抽象类放到类型层次的高度,使其成为两个类的祖先

接口的定义 mixin(混合类型)的理想选择,接口允许构造非层次结构的类型框架

接口使得安全地增强类的功能成为可能,如果是抽象类,只能通过继承来增加功能

接口可以使用缺省方法,但是不允许给Object方法(equals hashCode) 提供缺省方法,而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)

可以通过接口提供一个抽象的骨架实现类,把接口和抽象类的有点结合,接口负责定义类型,还可以提供缺省方法,骨架类实现非基本类型接口方法,这就是模板方法模式

实现这个接口的类,可以把对于接口方法的调用转发到内部私有类的实例上,内部私有类拓展了骨架实现类,别称为:模拟多重继承

对于骨架实现类,好的文档非常必要

21. 为后代设计接口

Java 8 增加了缺省方法,目的是允许给现有接口添加方法,Java 8 在核心类库增加了许多新的缺省方法,为了便于使用Lambda

应尽量避免这种用法,缺省的方法实现可能会破坏现有的接口实现

22. 接口只用于定义类型

常量接口是反例,如果实现类以后不需要这些常量了,依然需要实现这个接口,以确保二进制兼容,非final类实现了常量接口,它的所有名字的命名空间会被这些常量“污染”

解决方案:

  1. 添加常量在与之紧密相关的类或接口上
  2. 这些常量最好被看作枚举类成员就用枚举
  3. 不可实例化的工具类(private 构造器)

Tips:Java 7 开始数字的字面量中可以使用下划线区分

可以使用静态导入:import static 全限定类名.值

23. 类层次优于标签类

类层次例子:

public abstract class Figure {
abstract double area();
} class Circle extends Figure {
final double radius; public Circle(double radius) {
this.radius = radius;
} @Override
double area() {
return Math.PI * (radius * radius);
}
} class Rectangle extends Figure {
final double length;
final double width; public Rectangle(double length, double width) {
this.length = length;
this.width = width;
} @Override
double area() {
return length * width;
}
}

多种风格的实例使用标签类的问题:过于冗长、容易出错、效率低下

使用类层次:为标签类中的每个方法都定义一个包含抽象方法的抽象类,所有的方法都用到了某些数据域就放到这个抽象类;为每种原始标签类都定义根类的具体子类

24. 静态成员类优于非静态成员类

嵌套类(4种):在另一个类内部的类,目的是为了外围类提供服务

  1. 静态成员类:看作普通类,可以访问外围类所有成员,它是外围类的一个静态成员;常见用法:作为公有的辅助类。私有静态成员:常见用法:外围类所代表对象的组件,比如Map的Entry对象,它不需要访问Map,使用非静态成员类很浪费
  2. 非静态成员类:它的每个实例都隐含地与外围类的一个外围实例相关联;常见用法:定义Adapter,它允许外部类的实例被看作是另一个不相关类的实例,例如:Map的keySet、entrySet、values以及Set和List的迭代器
  3. 匿名内部类:出现在非静态环境中才有外围实例,即使是在静态环境中,也不能拥有任何静态成员,拥有的常数变量是final基本类型或者被初始化成常量表达式的字符串域;还有许多限制:无法实例化、不能instanceof、无法实现多个接口或拓展类,除了从父类继承,无法调用任何成员;常见用法:静态工厂方法的内部
  4. 局部类:出现在可以声明局部变量的地方,有名字可重复利用、在非静态环境下定义才有外围实例、不能包含静态成员

除了静态成员类,其他3个都称为内部类

嵌套类中只有静态成员类可以在它的外围类之外独立存在

如果声明的成员类不要求访问外围实例,那就把它定义成静态成员类,而不是非静态成员类,这样可避免每个实例都包含一个额外指向外围对象的引用,这可能造成内存泄漏

25. 限制源文件为单个顶级类

一个源文件只定义多个顶级类,可能导致给一个类提供多个定义,具体使用哪个定义取决于源文件被传递给编译器的顺序

一个源文件放多个顶级类,考虑使用静态成员类

5. 泛型

26. 请不要使用原生态类型

每一种泛型都定义一个原生态类型(raw type):不带任何实际类型参数的泛型,主要为了兼容之前代码(移植兼容性)

List 对应的就是 List

List 逃避了泛型检查,而List明确告诉编译器可以持有任何类型对象

泛型子类型化规则:List 可以传递给List,但是不能传递给List

不确定或者不在乎集合的元素类型,可以使用无限制的通配符类型,来代替原生态类型,例如:Set<?>,但是不能将任何除了null以外的元素放入到其中

必须用原生类型的情况:

  1. 类文字:List.class、String[].class、int.class
  2. instanceof,泛型可以在运行时被擦除,在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的,下面为正确做法:
if(o instanceof Set){
Set<?>S=(Set<?>) o;
}

27. 消除非受检的警告

尽可能消除每一条非受检警告,这意味着不会再运行时出现ClassCastException(这是RuntimeException);

禁用警告:@SuppressWarnings("unchecked"),应该在尽可能小的范围内使用,在局部变量上声明

28. 列表优于数组

数组是协变(covariant),例子:Sub为Super的子类型,Sub[] 就是Super[]的子类型

泛型是不变的(invariant),对于两个不同类型Type1,Type2,List 和List 之间既不是子类型关系,也不是超类型关系

泛型只在编译时强化他们的类型信息,并在运行时丢弃(或者擦除)他的元素类型信息

泛型和数组不能很好地混合使用,泛型数组、参数化类型或者类型参数的数组都是非法的,非法的例子:List[]、new List[]和new E[]

消除未受检转换警告,可以使用列表代替数组,虽然速度可能会慢一点,但是更安全

29. 优先考虑泛型

编写泛型类,例子:Stack泛型类

使用Object[] 存储数据,将原来方法入参或者返回类型改为 E,会出现无法创建泛型数组的问题,两种解决方案:

  1. 使用E[] elements存储数据,实例化Object数组,使用 (E[]) 转换为泛型,编译器提示警告,实践中优先使用,但它会导致堆污染
  2. 使用Object[] elements存储数据,在需要元素时使用 (E) 转换为泛型

类型参数没有限制,例子:Stack、Stack<int[]>、Stack<List>,但是不能用基本类型

30. 优先考虑泛型方法

在方法的修饰符和其返回值之间生命类型参数的类型参数列表

public static <E> Set<E> union(Set<E> s1, Set<E> s2);

TODO p107 泛型单例工厂

31. 利用有限制通配符来提升API的灵活性

参数化类型时不变的(invariant)

特殊的参数化类型:有限制的通配符类型(bounded wildcard type)

// 将一系列元素放入堆栈中
// 可以放 E 及其子类
public void pushAll(Iterable<? extends E> src); // 将所有元素弹出到传入容器中
// 这个容器应该是 E 及其父类
public void popAll(Collection<? super E> dst);

PECS:producer-extends,consumer-super

生产者T就用:<? extends T>

消费者E就用:<? super E>

注意!返回值不要用通配符类型

Comparable<? super T> 优先于 Comparable,类似的Comparator<? super T> 优先于 Comparator

两种静态方法声明:

// 1. 无限制的类型参数
public static <E> void swap(List<E> list,int i,int j);
// 2. 无限制的通配符
public static void swap(List<?> list,int i,int j);

方式2存在问题:无法把除了null意外的元素放入到List<?>中,解决:编写私有辅助方法来捕捉通配符类型

    public static void swap(List<?> list, int i, int j) {
// 报错 List<?> 不允许放除了null以外的值
swapHelper(list, i, j);
} private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(i, list.get(j)));
}

32. 谨慎并用泛型和可变参数

调用一个可变参数方法,会创建一个数组来存放可变类型,可变类型有泛型或者参数化类型时,编译警告信息会产生混乱

当一个参数化类型的变量指向一个不是该类型的对象时,会产生堆污染,导致编辑器的自动生成类型转换失败

将值保存在泛型可变参数数组中是不安全的,如果只是从中读取它是安全的

Java 7 增加@SafeVarargs注解,禁用泛型可变参数警告,只在静态方法和final实例方法中合法,Java 9 在私有实例方法也合法

泛型可变参数在都满足下列条件下是安全的:

  1. 它没有在可变数组中保存任何值
  2. 它没有对不被信任的代码开放该数组(或者其克隆程序)

可以使用List结合静态工厂List.of 代替可变参数(伪数组),缺点是:代码略繁琐,运行慢一点

33. 优先考虑类型安全的异构容器

TODO p119

    private Map<Class<?>, Object> favorites = new HashMap<>();

    public <T> void putFavorite(Class<T> type, T instance) {
// 使用动态转换,确保不会违背类型约束
favorites.put(type,type.cast(instance) );
} public <T> T getFavorite(Class<T> type) {
return type.cast(favorites.get(type));
} public static void main(String[] args) {
Favorites f = new Favorites();
f.putFavorite(String.class, "Java");
f.putFavorite(Integer.class, 0xcafebabe);
f.putFavorite(Class.class, Favorites.class); String favoriteString = f.getFavorite(String.class);
Integer favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoriteClass = f.getFavorite(Class.class); System.out.printf("%s %x %s %n",favoriteString,favoriteInteger,favoriteClass);
}

注意 Map<Class<?>, Object> 这里的无限制通配符 并非只能放null

《Effective Java》笔记 4~5的更多相关文章

  1. Effective Java笔记一 创建和销毁对象

    Effective Java笔记一 创建和销毁对象 第1条 考虑用静态工厂方法代替构造器 第2条 遇到多个构造器参数时要考虑用构建器 第3条 用私有构造器或者枚举类型强化Singleton属性 第4条 ...

  2. Effective java笔记(二),所有对象的通用方法

    Object类的所有非final方法(equals.hashCode.toString.clone.finalize)都要遵守通用约定(general contract),否则其它依赖于这些约定的类( ...

  3. effective java笔记之单例模式与序列化

    单例模式:"一个类有且仅有一个实例,并且自行实例化向整个系统提供." 单例模式实现方式有多种,例如懒汉模式(等用到时候再实例化),饿汉模式(类加载时就实例化)等,这里用饿汉模式方法 ...

  4. effective java笔记之java服务提供者框架

    博主是一名苦逼的大四实习生,现在java从业人员越来越多,面对的竞争越来越大,还没走出校园,就TM可能面临失业,而且对那些增删改查的业务毫无兴趣,于是决定提升自己,在实习期间的时间还是很充裕的,期间自 ...

  5. Effective java笔记(一),创建与销毁对象

    1.考虑用静态工厂方法代替构造器 类的一个实例,通常使用类的公有的构造方法获取.也可以为类提供一个公有的静态工厂方法(不是设计模式中的工厂模式)来返回类的一个实例.例如: //将boolean类型转换 ...

  6. Effective java笔记(四),泛型

    泛型为集合提供了编译时类型检查. 23.不要在代码中使用原生态类型 声明中具有一个或多个类型参数的类或接口统称为泛型.List<E>是一个参数化类,表示元素类型为E的列表.为了提供兼容性, ...

  7. Effective java笔记(九),并发

    66.同步访问共享的可变数据 JVM对不大于32位的基本类型的操作都是原子操作,所以读取一个非long或double类型的变量,可以保证返回的值是某个线程保存在该变量中的,但它并不能保证一个线程写入的 ...

  8. Effective java笔记(八),异常

    57.只针对异常的情况才使用异常 try { int i = 0; while(true) range[i++].climb(); }catch(ArrayIndexOutOfBoundsExcept ...

  9. Effective java笔记(七),通用程序设计

    45.将局部变量的作用域最小化 将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性. Java允许在任何可以出现语句的地方声明变量(C语言中局部变量要在代码块开头声明),要使 ...

  10. Effective java笔记(六),方法

    38.检查参数的有效性 绝大多数方法和构造器对于传递给它们的参数值都会有限制.如,对象引用不能为null,数组索引有范围限制等.应该在文档中指明所有这些限制,并在方法的开头处检查参数,以强制施加这些限 ...

随机推荐

  1. Python中保存字典类型数据到文件

    三种方法: 1.在 Python 中使用 pickle 模块的 dump 函数将字典保存到文件中import pickle my_dict = { 'Apple': 4, 'Banana': 2, ' ...

  2. Linux Vim操作看这篇文章就够了

    一.什么是Vim Vim是一个类似于Vi的著名的功能强大.高度可定制的文本编辑器,在Vi的基础上改进和增加了很多特性.代码补全.编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用.和Emac ...

  3. OAuth2 Authorization Server

    基于Spring Security 5 的 Authorization Server的写法 先看演示 pom.xml <?xml version="1.0" encoding ...

  4. 【Unity3D】协同程序

    1 简介 ​ 1)协程概念 ​ 协同程序(Coroutine)简称协程,是伴随主线程一起运行的程序片段,是一个能够暂停执行的函数,用于解决程序并行问题.协程是 C# 中的概念,由于 Unity3D 的 ...

  5. 解决Springboot发起https请求报错:sun.sec urity.validator.ValidatorException: PKIX path building failed

    问题描述 最近开发项目中在springboot接口中调用第三方https接口,后台日志报错: sun.sec urity.validator.ValidatorException: PKIX path ...

  6. 数仓的等待视图中,为什么会有Hashjoin-nestloop

    本文分享自华为云社区<GaussDB(DWS)等待视图之Hashjoin-nestloop>,作者:Arrow0lf. 1. 业务场景 众所周知,GaussDB(DWS)中有3种常见的jo ...

  7. zynq7000 I2C RTC 与 串口使用

    RS485 串口 测试 硬件上2路串口,其中UART 1对应PS STD IN/OUT,UART 0对应RS485: 图 ‑1 RS485电路,自动转换输入.输出方向 可参考 https://blog ...

  8. 微信小程序:接手项目,修bug

    好家伙,   问题描述如下: 小程序主界面,选择快速上传会议记录 选择快速 其中,没有2022-2023第二学期,所以,新的会议记录无法上传 于是,我自愿修复这个bug 由于我们没有产品文档 我只能由 ...

  9. [Node] nvm 安装 node 和 npm

    Node JS 安装 安装 node version manager (nvm) Windows: https://github.com/coreybutler/nvm-windows/release ...

  10. 【Azure 服务总线】使用Azure Service Bus 时,出现证书错误: 所使用的证书具有无法验证的信任链

    问题描述 在Azure中连接 Service Bus 服务发送消息时发生证书错误,抛出证书异常消息: 或 The X.509 certificate CN=servicebus.chinaclouda ...