第4章:类和接口

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

15.1 封装

15.1.1 什么是封装

一个模块对于外部的其他模块而言,隐藏了自身内部数据和其他实现细节,将自身的API与它的内部实现清晰地隔离开,叫做封装

15.1.2 封装的优点
  1. 加快开发速度:有效地解除各个模块之间的耦合关系,使各模块可以独立地开发、测试、优化、使用、理解和修改
  2. 减轻维护负担:修改模块时不会影响其他模块
  3. 封装不会提升系统性能
  4. 类的导出API:protected、public,有责任永远支持,保持不同版本兼容性
  5. 类实现的一部分:default、private。注意同包中可以访问中wsh.lxt和wsh并不是同一个包
  6. 包级私有的类或接口变动,不会损害现有的客户端
  7. 让每个类或成员尽可能地不可访问
  8. 个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的静态内部类
  9. 减少不必要的public比减少default的类更重要
  10. 如果类实现Serializable接口,其私有成员可能“泄漏(leak)”到导出的API中。
  11. protected成员,也必须永远支持
  12. 子类重写父类,由于其权限必须大于父类的,如果里面又调用了父类方法,相当于扩大了父类的访问权限
  13. 为了测试可以将私有成员指定为包级私有,更高就不行,也没有必要,可以将测试类放在与被测试类同一包下
  14. 公共类,如果public修饰非final的实例域,或指向一个可变对象的final域,缺点
    1. 域修改时,失去了对他采取任何行动的能力,比如无法对存储在这个域中的值进行限制。也无法将包含这种域的类设置成线程安全,因为没法将set方法进行synchronize
    2. 就相当于放弃了强制这个域不可变的能力
    3. 就算final修饰不可变对象,也会导致放弃了灵活性,因为客户端如果直接用变量,那么有一点,我想反回这个变量的子对象,便改
  15. 常量:final修饰的变量叫常量,对于公共类,如果是static、final修饰的不可变对象或基本变量,是可以public暴露在外的,这些属性的名字由大写字母组成,字母用下划线分隔。因为这种对象
public final Apple a = new Apple();
main
b =a ;
//此时a的name也变化
b.name = "dfdf";

public static final指向可变对象时缺点

//不安全,客户端虽然无法修改values的指向,但可以其指向的对象中的内容
public static final Thing[] VALUES = { ... };
//做法1:使公共数组私有并添加一个公共的不可变列表
private static final Thing[] PRIVATE_VALUES = { ... };

public static final List<Thing> VALUES =

Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
//做法2:将数组设置为private,并添加一个返回私有数组拷贝的公共方法private static final Thing[] PRIVATE_VALUES = { ... };

public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}
//例如说,我要是写一个简单的类包装数组的话:
public class ArrayWrapper<T> {  
    private final T[] array;  
    public final int length;  
      
    public ArrayWrapper(T[] originalArray) {  
        this.array = originalArray;  
        this.length = originalArray.length;  
    }  
    // ...  
}  
//length是暴露出来了,结果后来我发现我的程序里创建了很多这个类的实例,内存吃紧,我想时间换空间,却发现换不了了;只要我删除length域,使用该类的程序就需要重新编译才能继续使用。
//如果最开始这样写
public class ArrayWrapper<T> {  
    private final T[] array;  
    private final int length;  
      
    public ArrayWrapper(T[] originalArray) {  
        this.array = originalArray;  
        this.length = originalArray.length;  
    }  
      
    public int getLength() {  
        return this.length;  
    }  
    // ...  
}  
//修改后的length表达,此时对客户端无影响
public class ArrayWrapper<T> {  
    private final T[] array;  
      
    public ArrayWrapper(T[] originalArray) {  
        this.array = originalArray;  
    }  
      
    public int getLength() {  
        return this.array.length;  
    }  
    // ...  
}  

第16条:在public类中使用访问方法而不是public属性

16.1 public修饰类中使用public修饰属性的问题
//1.无法不修改API就改变它的数据表示法:比如开始x表示体重,但是有一天,不想用x表示体重了,而是重新定义了一个z表示体重,但由于客户端都使用Point.x,因此无法修改,但如果有访问方法,可以直接getX()中return y即可
//2.无法强加任何约束条件:例如想限制x,y的值有一定范围
//3.java中Dimension和Point类就有这种问题
package chapter4.number14;

public class Point {
	public double x;
	public double y;
}

16.2 修改方案
package chapter4.number14;

public class Point1 {
	private double x;
	private double y;
	public double getX() {
		return x;
	}
	public void setX(double x) {
		//此处可以加一些限制,不满足限制的x赋值时报错
		this.x = x;
	}
	public double getY() {
		return y;
	}
	public void setY(double y) {
		this.y = y;
	}
	
}

16.3 在default类或私有类中的嵌套类中使用公有域本质上没有错误,甚至比起访问方法,视觉上更简洁

主要原因就是这种代码无法被包外的客户端使用

16.4 在public类中定义不可变的属性危害较小,也尽量不用
package chapter4.number14;

public final class Time {
	public final int hour;
	public final int minute;
	public Time(int hour,int minute){
		//这里可以加上限制条件,对于定义非final可变属性不能这么做,因为即使这么做,还是可以直接修改
		//例:Time.hour=50;
		//但是如果不改变类的API还是无法改变属性的表示方法,例如改hour为hours,因为客户端代码已经遍布各处
		if(hour<0||hour>=24){
			throw new IllegalArgumentException();
		}
		if(minute<0||minute>=60){
			throw new IllegalArgumentException();
		}
		this.hour=hour;
		this.minute=minute;
	}
}

第17条:使可变性最小

  1. 不可变类简单来说是它的实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化
  2. 不可变对象内的属性,从创建开始到销毁,其值都不会发生变化
  3. java自带不可变类:String、基本类型包装类、BigInteger、BigDecimal
  4. 不可变类优点:易于设计、实现、使用,且安全
17.1 创建不可变类规则
//创建不可变类规则(1-5)
//1. 对于外部可见的属性,不提供setter方法:防止外界修改该属性。所谓外部可见,比如之前PhoneNumber方法,private int hashCode,只是为了在内部缓存hashCode值,外部根本无法访问该属性,也就没必要final修饰,也没必要提供setter方法
//2. 保证类不被继承:由于里氏替换原则,所有使用父类的地方都能使用子类,但父类无法保证子类是否可变,那就可能与客户端代码需要的内容不符,例如这里realPart方法给客户端返回re属性的值,但如果其子类重写该方法,返回常量3.15,那么会让客户端以为这个类改变了,也会影响客户端原本想实现的逻辑
//BigInteger、BigDecimal刚编写出来时,对该说法没有得到广泛理解, 他们是可以被继承的,那么当你编写一个类/方法,它的安全性来自于BigInteger、BigDecimal对象的不可变性,就必须进行检查
//public static BigInteger safeInstance(BigInteger val) {
	//return val.getClass() == BigInteger.class ?val : new BigInteger(val.toByteArray());
//}

public final class Complex {
	//3. 把所有属性设置为final:表示该属性无法被修改
	//4. 把所有属性设置为private:参考15、16条的好处
	private final double re;
	private final double im;
	//5. 如果属性为可变类型,需要在构造方法,getter方法和readObject、readResolve,方法中进行防御性拷贝:防止外界获取不可变类中可变的属性的引用,甚至从不可变类,创建可变的实例(item 88)
	public Complex(double re, double im) {
		this.re = re;
		this.im = im;
	}

	public double realPart() {
		return re;
	}

	public double imaginaryPart() {
		return im;
	}
	//1. 该方法返回一个新的Complex实例,而不是修改这个实例(this.re = re+c.re(过程/命令方法)),这种方法也叫函数式方法
	//2. 方法名称为介词plus,表示不会改变对象内属性的值,而是新产生一个对象。add一般表示改变对象内属性值
	//3. 很可惜,BigDecimal和BigInteger没有遵守这一命名习惯,导致很多错误用法
	public Complex plus(Complex c) {
		return new Complex(re + c.re, im + c.im);
	}
}
17.2 不可变类优势
  1. 不可变对象简单
  2. 不可变对象只有一种状态,就是被创建时的状态
  3. 不可变类构造器中如果限定了其属性值得约束,那么该对象永远不会违反这个约束(可变类如果想设置约束,至少在setter方法+构造器)
  4. 由于可变对象中属性的值可以是任何值,那么如果文档中没有提供对所有属性值的精确描述,无法可靠使用
  5. 不可变对象线程安全,不需要同步,因此也可以被共享
//由于可以被共享,因此也没必要每次都创建一个新的
//1. 可以考虑提供public static final的常量,用于缓存不可变对象
public static final Complex ZERO = new Complex(0, 0);
public static final Complex ONE  = new Complex(1, 0);
public static final Complex I    = new Complex(0, 1);
//2. 也可以考虑用静态工厂方法提供缓存了的不可变实例
  1. 甚至可以共享不可变对象的内部属性
//1. 由于signum是不可变类BigInteger的属性,因此客户端其实无法获取该值引用,也无法修改该值,那么根据其创建出的BigInteger对象(或者某方法可以创建一个可变对象)的接收signum的属性,不会由于原BigInteger对象中属性值被修改而变动
public BigInteger negate() {
	return new BigInteger(this.mag, -this.signum);
}
  1. 不可变对象不需要保护性拷贝,不可变类也不需要提供clone方法
  2. 不可变对象为其他对象提供了大量的构件:比如String作为map和set的key,不需要担心对象的改变导致摧毁map和set的不可变性
  3. 不可变对象内部属性值不可变,因此不存在临时不一致的可能性(原子性)
17.3 不可变类缺点
  1. 如果需要"改变"对象内属性值,必须重新创建一个对象。如果该不可变对象创建成本很高,会有性能问题,通常为其添加一个可变的"配套类"。例如StringBuilder与String的关系,StringBuilder这个配套类,是可变的,而且调用方法时,不会新创建一个对象。
17.4 创建不可变类规则中"保证类不被继承"的方案
  1. final类
  2. private/protected修饰构造器,并提供创建对象的public静态工厂方法
//1. 由于构造器被私有化,实际上类无法继承该类,因为创建子类对象时,需要先调用父类的构造器,但父类构造器对子类隐藏了,因此该类根本无法继承
//2. 如果使用default构造器,实际上虽然该类可以被继承,但只能在包内被继承,也就防止了包外客户端恶意继承该类,并修改子类不可变性,而包内的继承,是由包的作者完成的,是不会故意创建破坏不可变性的子类的
private Complex(double re, double im) {
	this.re = re;
	this.im = im;
}

public static Complex valueOf(double re, double im) {
	return new Complex(re, im);
}

17.5 使用原则
  1. 除非有充分的理由使类成为可变类,否则类应该是不可变的
  2. 只有当确认有必要实现令人满意的性能时,才应该为不可变类提供公有的可变配套类
  3. 如果类不能被做成不可变的,也应尽可能限制它的可变性
  4. 除非有令人信服的理由使域变成非final的,否则每个域都应该是private final的
  5. 构造器应该创建完全初始化的对象,并建立起所有的约束关系

第18条:复合优先于继承

18.1 继承
  1. 这里的继承指的只是类继承类,不包括类实现接口和接口继承接口
  2. 包内的继承(子类和超类实现都是一个人写的)和专门为被继承而设计且具有很好文档说明的父类被继承,是安全的
18.2 继承缺点
  1. 继承会打破封装:子类中如果重写父类方法A,而父类方法B的实现又依赖于A方法,那么此时调用子类的B方法时,由于多态,这个B方法内部依赖子类重写后的A方法,相当于修改了父类方法的实现细节
package chapter4.number16;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

public class InstrumentedHashSet<E> extends HashSet<E> {
	private int addCount = 0;
	public InstrumentedHashSet(){
		
	}
	public InstrumentedHashSet(int initCap,float loadFactor){
		super(initCap,loadFactor);
	}
	@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 int getAddCount(){
		return addCount;
	}
	public static void main(String[] args) {
		InstrumentedHashSet s = new InstrumentedHashSet();
		s.addAll(Arrays.asList("Snap","Crackle","Pop"));
		//1.我们以为会返回3,实际上返回6,因为在HashSet内部,addAll是基于add方法实现,即先调用add使addCount增加3,又在调用addAll时增加了3
		//2.如果想子类功能正常时间,那么需要根据父类这种实现细节对自身addAll方法改写,不再调用代码addCount += c.size(),但一旦超类中这个实现细节进行修改,例如addAll不再多次调用add方法完成,那么子类的计数器又无法正常工作
		System.out.println(s.getAddCount());
	}
}

  1. 父类新增子类中不存在的方法导致子类功能被破坏:例如某集合子类功能为在添加元素之前,先判断该元素是否满足某条件,如果满足才能被添加,那么该子类中应该覆盖所有父类中添加元素的方法以确保在加入每个元素前满足这个先决条件。但是一旦超类增加了添加元素的新方法,子类对象一旦调用该方法会导致非法元素被添加
  2. 父类新增子类中已存在的方法导致子类被破坏
    1. 父类新增与子类方法名、形参列表相同,返回值完全不同方法,此时子类想重写,但返回值类型不小于父类的返回值类型,编译报错
    2. 父类新增与子类形参列表相同,返回值也相同的方法,子类原有的方法不一定能遵守父类新增方法的约定
18.3 通过复合与转发解决继承带来的问题
  1. 先创建一个可以重用的转发类(forwarding class)
//本例实际上为装饰者模式,提供包装类,为原对象增加功能
package chapter4.number16;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
//1. 必须得实现Set接口,否则"包装"后的这个ForwardingSet类无法作为Set类型被使用
public class ForwardingSet<E> implements Set<E> {
	
	private final Set<E> s;
	//4. 对于之前继承的方案,HashSet有多少个构造器,它就得有多少个构造器,但本方案不需要,并且之前的方案中,只设计出了一个具有计数功能的集合,但这种方案,使所有Set的子类,都可以具备计数器功能
	public ForwardingSet(Set<E> s){
		this.s = s;
	}
	//2. 其所有实例方法,都调用现有的类型Set的实例s中对应的方法,并返回它的结果,这叫做转发。这些新类中的方法,叫做转发方法
	//3. 这种做法不依赖于现有类(Set)的实现细节,即使在现有类中(Set)添加了新方法,也不会影响新的类
	@Override
	public int size() {
		// TODO Auto-generated method stub
		return s.size();
	}

	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return s.isEmpty();
	}

	@Override
	public boolean contains(Object o) {
		// TODO Auto-generated method stub
		return s.contains(o);
	}

	@Override
	public Iterator<E> iterator() {
		// TODO Auto-generated method stub
		return s.iterator();
	}

	@Override
	public Object[] toArray() {
		// TODO Auto-generated method stub
		return s.toArray();
	}

	@Override
	public <T> T[] toArray(T[] a) {
		// TODO Auto-generated method stub
		return s.toArray(a);
	}

	@Override
	public boolean add(E e) {
		// TODO Auto-generated method stub
		return s.add(e);
	}

	@Override
	public boolean remove(Object o) {
		// TODO Auto-generated method stub
		return s.remove(o);
	}

	@Override
	public boolean containsAll(Collection<?> c) {
		// TODO Auto-generated method stub
		return s.containsAll(c);
	}

	@Override
	public boolean addAll(Collection<? extends E> c) {
		// TODO Auto-generated method stub
		return s.addAll(c);
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		// TODO Auto-generated method stub
		return s.retainAll(c);
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		// TODO Auto-generated method stub
		return s.removeAll(c);
	}

	@Override
	public void clear() {
		s.clear();
	}

}


  1. 创建一个包装类继承这个转发类
package chapter4.number16;

import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class InstrumentedHashSet1<E> extends ForwardingSet<E>{
	private int addCount = 0;
	public InstrumentedHashSet1(Set<E> s) {
		super(s);
	}
	public boolean add(E e){
		addCount++;
		return super.add(e);
	}
	public boolean addAll(Collection<? extends E>c){
		addCount+=c.size();
		return super.addAll(c);
	}
	public int getAddCount(){
		return addCount;
	}
	public static void main(String[] args) {
		//由于每一个InstrumentedHashSet1实例都把Set实例包装起来了,所以InstrumentedHashSet1也称为包装类(wrapper class)
		Set<Date> s  = new InstrumentedHashSet1(new TreeSet<Date>());
		Set<String> s2  = new InstrumentedHashSet1<String>(new HashSet<String>());
	}
}

18.4 使用继承的场景
  1. 回调框架中:包装类不适用于回调框架
//1. 回调框架使用场景:类A想让类B在某个时间段(sometime)完成某样事情(dosomething),类B完成某事后,再通知(callback)类A执行后续该执行的事情。
//2. 在A类中,调用B对象的方法c,而方法c在处理完一系列逻辑后,又调用A对象的方法d,这个d方法就叫做回调函数
//3. 此处回调函数作用为:让闹钟在10*1ms后响铃,通知我"起床去上班了"
public class Me implements CallBack {
	private Clock clock;

	public Me(Clock clock) {
		this.clock = clock;
		// 1.A类Me,调用了B类Clock的c方法excute
		// 2. 而c方法又调用了A对象的d方法doSomeThing
		// 3. 其中doSomeThing为回调方法,为了被回调,A对象将自身传给B类中的方法c,用于后续被回调
		this.clock.excute(this);
	}

	@Override
	public void doSomeThing() {
		System.out.println("起床去上班了");
	}

	public static void main(String[] args) {
		Me me = new Me(new Clock());
	}
}

interface CallBack {
	void doSomeThing();
}

public class Clock {
	public void excute(final CallBack callback) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				int i = 0;
				while (i < 10) {
					try {
						Thread.sleep(1);
						i++;
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				callback.doSomeThing();
			}
		}).start();
	}

}

public class A implements CallBack {

	public void test() {
		System.out.println("test方法");
		C c = new C();
		c.testnew(this);
	}

	@Override
	public void doSomeThing() {
		System.out.println("10分钟后起床");
	}
}

class C {
	public void testnew(CallBack a) {
		a.doSomeThing();
	}
}


public class B implements CallBack{
	//a为被包装的对象,它的test方法,本身具有回调功能,可以回调A类中的doSomeThing方法
	//当我实现包装类B时,将B的test方法,转发给对象a的test方法进行处理,但我的本意,肯定是希望其回调我B的doSomeThing方法,但事与愿违,只能回调A的方法
	//所谓的包装类不适合用于回调框架,其实是说,包装类,想通过使用其包装的对象A的方法test,来回调自身(B)中的方法doSomeThing,但做不到
	static A a = new A();
	public void test(A a) {
		a.test();
	}
	public static void main(String[] args) {
		B b= new B();
		b.test(a);
	}
	@Override
	public void doSomeThing() {
		System.out.println("20分钟后起床");
	}
}

  1. 两个类A和B之间确实存在is-a关系
18.5 jdk中错误使用继承导致的问题
  1. 栈(stack)并不是向量(vector),但jdk中Stack扩展了Vector
  2. 属性列表(Properties)不是散列表(Hashtable),但Properties扩展了Hashtable
//设计者设计Properties只是为了允许字符串作为key和value,不应该用继承,继承后导致,使用Properties对象p调用父类的put方法,而不是使用子类的setProperty方法,来放入元素,会导致子类依赖于放入key-value为字符串的方法load失效
Properties p = new Properties();
p.put(new Object(), new Object());
p.load(new FileInputStream(new File("E:\\test.properties")));
p.store(new FileOutputStream(new File("E:\\test.properties1")),"注释");

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

  1. java注释三种方法:
    1. 单行注释
    2. 多行注释
    3. 文档注释
  2. javadoc工具可以提取java文件中的文档注释,整理成一个文档
javadoc 选项 Java源文件/
  1. java文档注释方法:/**开始,*/结束
package lee;
/**
 * Description:
 * <br>网站: <a href="http://www.crazyit.org">疯狂Java联盟</a>
 * <br>Copyright (C), 2001-2016, Yeeku.H.Lee
 * <br>This program is protected by copyright laws.
 * <br>Program Name:
 * <br>Date:
 * @author Yeeku.H.Lee [email protected]
 * @version 1.0
 */
public class JavadocTest
{
	/**
	 * 简单测试成员变量
	 */
	protected String name;
	/**
	 * 主方法,程序的入口
	 */
	public static void main(String[] args)
	{
		System.out.println("Hello World!");
	}
}
19.1 对于专门为继承而设计的类,需要在文档说明的内容
  1. 它可被重写的方法的自用性:比如之前例子中HashSet中的addAll是基于add方法实现,这个add方法被自己使用了,即该方法有自用性
    1. 对于public、protected方法/构造器, 文档必须指明该方法/构造器调用了哪些可被重写的方法,以什么顺序调用,每个调用结果如何影响后续处理
    2. 如果确实有方法调用了可覆盖的方法,一般在文档注释中使用@implSpec来描述该方法的内部工作情况,在使用javadoc生成的文档中,自动生成Implementation Requirements项
//对于AbstractCollection的remove方法,@implSpec描述如下
//清楚的说明了覆盖iterator方法会影响这个remove方法,并详细描述了会如何影响remove方法
//javadoc工具会自动忽略@implSpec,除非加上参数-tag"apiNote:a:API Note:"
/**
  * {@inheritDoc}
  *
  * @implSpec
  * This implementation iterates over the collection looking for the
  * specified element.  If it finds the element, it removes the element
  * from the collection using the iterator's remove method.
  *
  * <p>Note that this implementation throws an
  * {@code UnsupportedOperationException} if the iterator returned by this
  * collection's iterator method does not implement the {@code remove}
  * method and this collection contains the specified object.
  *
  * @throws UnsupportedOperationException {@inheritDoc}
  * @throws ClassCastException            {@inheritDoc}
  * @throws NullPointerException          {@inheritDoc}
  */
public boolean remove(Object o) {
     Iterator<E> it = iterator();
     if (o==null) {
         while (it.hasNext()) {
             if (it.next()==null) {
                 it.remove();
                 return true;
             }
         }
     } else {
         while (it.hasNext()) {
             if (o.equals(it.next())) {
                 it.remove();
                 return true;
             }
         }
     }
     return false;
 }
  1. 类内部精心挑选的作为钩子的protected方法
//AbatractList中的clear内调用的removeRange方法,就是一个钩子,子类实现该方法可以使clear方法更快。
//原方法中,clear方法所需要的时间为remove方法的时间*集合中元素的个数,效率较低
/**
  * Removes from this list all of the elements whose index is between
  * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
  * Shifts any succeeding elements to the left (reduces their index).
  * This call shortens the list by {@code (toIndex - fromIndex)} elements.
  * (If {@code toIndex==fromIndex}, this operation has no effect.)
  *
  * <p>This method is called by the {@code clear} operation on this list
  * and its subLists.  Overriding this method to take advantage of
  * the internals of the list implementation can <i>substantially</i>
  * improve the performance of the {@code clear} operation on this list
  * and its subLists.
  *
  * @implSpec
  * This implementation gets a list iterator positioned before
  * {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
  * followed by {@code ListIterator.remove} until the entire range has
  * been removed.  <b>Note: if {@code ListIterator.remove} requires linear
  * time, this implementation requires quadratic time.</b>
  *
  * @param fromIndex index of first element to be removed
  * @param toIndex index after last element to be removed
  */
 protected void removeRange(int fromIndex, int toIndex) {
     ListIterator<E> it = listIterator(fromIndex);
     for (int i=0, n=toIndex-fromIndex; i<n; i++) {
         it.next();
         it.remove();
     }
 }

19.2 测试为继承而设计的类的方法

编写子类,每当这种类源码发生变化,也都应该在发布前,编写子类进行测试

19.3 与正常类文档的不同
  1. 正常类的文档信息是为了告诉程序员如何创建该类的实例,而为继承设计的类是想告诉你如何更好的继承它
  2. 目前无法将正常的类的API与为继承设计的类的文档区分开
19.4 为继承设计的类的注意事项
  1. 构造器不能调用可被重写的方法
public class Super {
	public Super() {
		overrideMe();
	}

	public void overrideMe() {
	}
}
import java.time.Instant;

public final class Sub extends Super {
	private final Instant instant;

	Sub() {
		instant = Instant.now();
	}

	@Override
	public void overrideMe() {
		System.out.println(instant.getNano());
	}

	public static void main(String[] args) {
		//子类初始化前,会调用父类构造器,而父类构造器如果调用了被子类覆盖的方法,那么此时会执行子类的该方法,但该方法overrideMe中用到了子类中的成员变量instant,由于该成员变量还未初始化(因为子类构造器还没执行),此时该值为null,因此会出现空指针异常
		//调用私有方法、final方法、static方法是安全的,因为这些方法都不会被重写
		Sub sub = new Sub();
		sub.overrideMe();
	}
}
  1. 为继承而设计的类如果实现Clonable和Serializable接口,其clone方法和readObject方法行为上类似构造器,因此同样适用于上面的规则,即在clone和readObject方法中不要调用可被子类重写的方法。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class TransientTest {
	public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
		User user = new User();
		ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("C:\\Users\\含低调\\Desktop\\111.txt"));
		os.writeObject(user);
		os.flush();
		os.close();
		ObjectInputStream is = new ObjectInputStream(new FileInputStream("C:\\Users\\含低调\\Desktop\\111.txt"));
		//子类被反序列化之前,会调用父类的readObject(ObjectInputStream ois) 方法,该方法中调用了overwrite方法,而由于重写,实际上调用了子类的该方法,因此报错空指针异常
		user = (User) is.readObject();
		is.close();
	}
}

class User extends Person implements Serializable {
	String username;

	@Override
	public void overwrite() {
		System.out.println(username.compareTo("1234"));
	}
}

class Person implements Serializable {
	private void writeObject(ObjectOutputStream oos) {
	}

	private void readObject(ObjectInputStream ois) {
		overwrite();
	}

	public void overwrite() {
	}
}
import java.io.FileNotFoundException;
import java.io.IOException;

public class TransientTest {
	public static void main(String[] args)
			throws FileNotFoundException, IOException, ClassNotFoundException, CloneNotSupportedException {
		User u = new User();
		//clone方法是Object的方法,实际上u是可以调用的,但调用时,由于User没定义clone方法,因此会找其父类的该方法,但其父类中clone方法调用了overwrite方法,导致空指针
		u.clone();
	}
}

class User extends Person {
	String username;

	@Override
	public void overwrite() {
		System.out.println(username.compareTo("1234"));
	}
}

class Person implements Cloneable {
	@Override
	protected Person clone() throws CloneNotSupportedException {
		overwrite();
		return (Person) super.clone();

	}

	public void overwrite() {
	}
}
  1. 尽量不要实现Cloneable和Serializable接口,因为会把一些负担转嫁给扩展这个子类的程序员身上,应允许子类实现者自主选择是否实现这些接口
  2. 为继承设计的类如果实现了Serializable接口,且该类有readResolve或writeReplace方法,那么需要将其至少定义为protected以上,否则子类相当于没继承到这两个方法,那么在被序列化和反序列化时,也不会调用这两个方法
  3. 对于不是为了继承而设计的类,应禁止其子类化,因为每次对这种类进行修改,都可能破坏由这个类扩展的客户类。禁止子类化方法
    1. final修饰类
    2. private/default修饰类,并提供静态工厂方法替代构造器
  4. 如果非要继承不是为继承而设计的类,那么设计该类时,确保该类永远不会调用它的任何可覆盖的方法,并在文档中说明,这样可以完全消除可覆盖方法的自用性。
  5. 如果某个方法中必须使用其另一个可覆盖方法的功能,可以将另一个方法的逻辑,封装成一个private方法(辅助方法), 并调用该private方法,来消除自用性
//原情況,test方法中调用可被覆盖的notOverride方法
public class Parent {
	public void test(){
		notOverride();
	}
	protected void notOverride() {
		System.out.println("必须使用的功能");
	}
}	
//进行如下调整,这样就不再调用可被覆盖的方法
public class Parent {
	public void test(){
		notOverrideHelp();
	}
	protected void notOverride() {
		System.out.println("必须使用的功能");
	}
	private void notOverrideHelp() {
		System.out.println("必须使用的功能");
	}
}	

第20条:接口优于抽象类

  1. java8的default方法:java 8 之前,接口与其实现类之间的 耦合度 太高,当需要为一个接口添加方法时,所有的实现类都必须随之修改
interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}
 
class ClassA implements InterfaceA {
}
 
public class Test {
    public static void main(String[] args) {
        new ClassA().foo(); // 打印:“InterfaceA foo”
    }
}
  1. default方法的多继承
interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}
 
interface InterfaceB {
    default void bar() {
        System.out.println("InterfaceB bar");
    }
}
 
interface InterfaceC {
    default void foo() {
        System.out.println("InterfaceC foo");
    }
    
    default void bar() {
        System.out.println("InterfaceC bar");
    }
}
 
class ClassA implements InterfaceA, InterfaceB {
}
 
// 1. 错误,因为InterfaceB和InterfaceC都有bar方法,ClassB不知道使用哪个bar方法, 因此必须重写该方法
//class ClassB implements InterfaceB, InterfaceC {
//}
 
class ClassB implements InterfaceB, InterfaceC {
    @Override
    public void bar() {
    	//2. 调用 InterfaceB 的 bar 方法
        InterfaceB.super.bar(); 
        //3. 调用 InterfaceC 的 bar 方法
        InterfaceC.super.bar(); 
        System.out.println("ClassB bar"); // 做其他的事
    }
}
interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
}
 
interface InterfaceB extends InterfaceA {
    @Override
    default void foo() {
        System.out.println("InterfaceB foo");
    }
}
 
//1. 虽然InterfaceA和InterfaceB都具有foo方法,但由于B中foo方法覆盖了A中的foo方法,因此ClassA就只继承B中的foo方法, 不会造成歧义
class ClassA implements InterfaceA, InterfaceB {
}
 
class ClassB implements InterfaceA, InterfaceB {
    @Override
    public void foo() {
    	//错误,因为InterfaceA中的foo方法被覆盖了,没法被调用
		//InterfaceA.super.foo(); 
        InterfaceB.super.foo();
    }
}
  1. 继承的类的方法,优先于实现的接口的默认方法,无论该方法是具体的还是抽象的
interface InterfaceA {
    default void foo() {
        System.out.println("InterfaceA foo");
    }
 
    default void bar() {
        System.out.println("InterfaceA bar");
    }
}
 
abstract class AbstractClassA {
    public abstract void foo();
 
    public void bar() {
        System.out.println("AbstractClassA bar");
    }
}
 
class ClassA extends AbstractClassA implements InterfaceA {
	//1. 继承的类AbstractClassA的方法优先于实现的接口InterfaceA的方法
	//2. 两者都有foo方法,以类的为主,类中foo方法为抽象,所以必须提供实现。
	//3. 对于bar方法,默认继承了类AbstractClassA中的bar方法,会打印"AbstractClassA bar"
    @Override
    public void foo() {
        InterfaceA.super.foo();
    }
}
 
public class Test {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.foo(); // 打印:“InterfaceA foo”
        classA.bar(); // 打印:“AbstractClassA bar”
    }
}

20.1 接口的优势

接口与抽象类都可以为子类提供实现,抽象类使用非抽象方法,接口使用default方法

  1. Java只允许单继承,但可以实现多个接口,因此如果想获取多个不同的实现时,只能通过实现多个接口的方式
  2. 现有的类通过实现接口获取功能,要比通过继承类来获取功能更方便
//例如有一个继承树,A、B都继承C,现如果希望B获得D中的一个实现
//1. 如果D是一个接口,那么B只需要增加一个implements子句即可
//2. 如果D是一个类,由于B原本已继承C,所以只能让C去继承D来获取它的实现(default方法),但此时C就也拥有了D的功能,这个功能是多余的
  1. 接口更适合用于定义mixin类型:原因与上面相同
    1. 类无法多继承
    2. 类层次结构中没有适当的地方插入mixin
//mixin类型:类除了实现它的"基本类型"之外,还可以实现的,一种表明它提供了某些可供选择的行为的类型,例如Comparable就是一种mixin类型,表示实现它的类,可以与其他的实现该接口的类的对象进行比较、排序
  1. 接口允许构造非层次结构的类型框架
//例如一个接口代表singer、一个接口代表songwriter,他们实际上并属于层次结构,他们是并列的,在现实生活中一个人既是singer又是songwriter,但如果singer和songwriter都是泪,就无法构建出这种非层次关系,因为Java不允许多继承
//如果想使用继承来表达这种非层次关系,以上面为例,比如想得到一个singer,用类A表示,那么A类需继承singer,想得到一个songwriter,用类B表示,B继承songwriter,如果A这个singer想获得songwriter的能力,需要让singer继承songwriter,得到类C,如果需要B这个songwriter的能力,又要让songwriter继承singer,得到类D,如果有n个属性(singer、songwriter为两个属性),那么就需要2的n次个类。这叫做组合爆炸
  1. 接口使得安全地增强类的功能称为可能
这一点没有理解,对于之前的包装类,如果没有接口,可以通过继承一个抽象类(AbstractSet),然后在组合一个该抽象类类型的对象达到包装类的效果

20.2 接口的劣势

  1. 无法在接口中为Object的equals和hashCode方法提供default方法
  2. 接口中无法包含实例域,或非公有的静态域(静态方法除外)
  3. 无法为不受你控制的接口添加缺省方法:例如想写一个集合WusihanList,实现了Collection接口,你想为WusihanList增加实现,没法通过修改Collection接口,只能在WusihanList中增加,那么如果有n个类中都实现该接口,那么一旦想为这10个类,都添加新功能,就无法做到

20.3 骨架实现类

  1. 骨架实现类:指为接口中的定义提供一个抽象的类,该类实现了一些方法,保留了一些方法未被实现,它可以将接口和抽象类的优点结合起来。骨架实现类一般命名为AbstractInterface,其中Interface代表所实现的接口名称
  2. 我理解骨架实现类就是只实现了一个接口的骨架,并没有血肉,实现的不完整,但具备了基本的功能,其它功能等待子类补充
  3. 骨架实现:指骨架实现类中的非抽象方法
20.3.1 一般做法
  1. 创建接口A
  2. 创建抽象类B,实现该接口A,并实现公共方法,这个B叫做骨架实现类
  3. 子类C实现接口A,并创建一个私有内部类D,继承抽象类B。子类C中组合一个B类型的对象,将A中需要被实现的方法,委托给组合的抽象类对象,这叫做模拟多重继承
20.3.2 自定义骨架实现类
  1. 认真研究接口,确定哪些方法是最基本的,即其他方法可以根据它们来实现。将这些基本方法作为骨架实现类中的抽象方法
  2. 在接口中为所有可以在基本方法之上直接实现的方法提供default方法,其内会调用抽象方法
  3. 接口中不能定义与Object的equals、hashCode、toString方法签名相同的default方法,这些应放在骨架实现类中
package chapter4.number18;

import java.util.Map;

public abstract  class AbstractMapEntry<K,V> implements Map.Entry<K,V>{
	//1.认真研究接口,确定getKey与getValue方法是最基本的,其他方法可以根据他们来实现,将这些方法作为抽象方法放在骨架实现中
	@Override
	public abstract K getKey();

	@Override
	public abstract V getValue();

	@Override
	public V setValue(V value) {
		throw new UnsupportedOperationException();
	}
	//2.类中可以为Object的equals、hashCode、toString提供实现,而接口中的default方法不行
	@Override
	public boolean equals(Object o){
		if(o==this)
			return true;
		if(!(o instanceof Map.Entry))
			return false;
		Map.Entry<?, ?> arg = (Map.Entry)o;
		return equals(getKey(),arg.getKey())&&equals(getValue(),arg.getValue());
		
	}
	private static boolean equals(Object o1,Object o2){
		return o1==null?o2==null:o1.equals(o2);
	}
	@Override
	public int hashCode(){
		return hashCode(getKey())^hashCode(getValue());
	}
	private static int hashCode(Object obj){
		return obj == null?0:obj.hashCode();
	}

}

/*
1. 使用骨架实现思维过程
此处SimulatedMultipleIn类想使用Map.Entry接口的功能,但不想单独写hashCode、toString、equals等方法,以及一些复杂的功能,只想实现自己独特的getKey和getValue方法。那么考虑到可以继承已经对Map.Entry有部分实现的AbstractMapEntry类,但由于SimulatedMultipleIn已经继承了一个类,一次可以考虑内部组合一个AbstractMapEntry,只实现其getKey和getValue方法,然后Map.Entry的所有方法都转发给这个组合的AbstractMapEntry对象
*/

import java.util.Map;

public class SimulatedMultipleIn<K,V> implements Map.Entry<K,V>{
	AbstractMapEntry am = new AbstractMapEntry(){

		@Override
		public Object getKey() {
			// TODO Auto-generated method stub
			return null;
		}

		@Override
		public Object getValue() {
			// TODO Auto-generated method stub
			return null;
		}
		
	};
	
	@Override
	public K getKey() {
		// TODO Auto-generated method stub
		return (K) am.getKey();
	}

	@Override
	public V getValue() {
		// TODO Auto-generated method stub
		return (V) am.getValue();
	}

	@Override
	public V setValue(V value) {
		// TODO Auto-generated method stub
		return (V) am.setValue(value);
	}
	
}

20.3.3 优势
  1. 该子类C还可以继承其他类,同时C又拥有类B的功能,相当于多继承
  2. 子类C不需要自主实现所有接口A的方法, 大部分实现可以转发给骨架实现类B
  3. 多个类实现同一接口时,如果该接口不是自己定义的,无法被修改,那么无法统一为实现该接口的类新增功能,但如果开始就自定义一个骨架实现类来实现这个接口,其他子类继承这个骨架实现类,那么后期可以通过为这个骨架实现类增加方法,从而为其所有子类提供功能
  4. 可以在骨架实现类中定义Object的equals、hashCode方法的具体实现

20.4 简单实现

简单实现就像个骨架实现,区别在于它不是抽象的,它是接口的最简单的有可能的有效实现,AbstractMap.SimpleEntry就是个例子。我们可以直接使用它,或将它子类化,这样不用自己实现接口中的方法

20.5 最佳实践

  1. 对于自己设计的重要的接口,都应为其提供骨架实现类
  2. 在这个接口中,应尽可能地通过新增default方法来提供骨架实现,以便所有接口的实现类都能使用

21 为后代设计接口

21.1 default方法

  1. Java8之前,为接口添加方法,其所有实现编译都会报错
  2. Java8以后提供了default方法,可以为接口添加新方法
  3. 但仍然无法确保default方法可以在以前存在的实现中都可以良好运行
//1. 例如在java8中的Collection接口中增加了default方法removeIf,该方法可以遍历集合中的所有元素,将元素作为传入的Predicate对象的test方法的参数,根据该方法返回的真假,来删除集合中的元素
//2. 这里谈到了断言的概念,实际上,断言就是指一个返回值为boolean的表达式
default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}
//2. 但这种default方法,对于其他类库中的实现,可能会造成影响,比如org.apache.commons.collections.collection.SynchronizedCollection类,它来自Apache Commons类库,它是一个包装类,提供了可以根据客户端提供的对象,进行同步的功能,代码片段如下
//构造器
protected SynchronizedCollection(Collection collection, Object lock) {
	if (collection == null) {
		throw new IllegalArgumentException("Collection must not be null");
	}
	this.collection = collection;
	this.lock = lock;
}
//根据传入的lock对象进行同步,并转发给内部的collection对象
public boolean add(Object object) {
	synchronized (this.lock) {
		return this.collection.add(object);
}
//3. 该类也获得了removeIf这个default方法,但很明显,removeIf方法在该类中无法做到同步,违反了这个类当初设定的目的
//4. 而对于JDK自己的类库中的同步集合Collections.SynchronizedCollection,它覆盖了removeIf方法,因此让该方法满足了同步的要求

21.2 最佳实践

  1. 尽量避免利用default方法在现有的接口上添加新方法,除非特殊需要,即使非这么做,也应慎重考虑,该default方法的实现,是否会破坏现有的接口实现
  2. 发布新接口前,应以至少三种方法实现接口,并测试。编写多个客户端程序,利用新接口的实例执行不同的任务

22 接口只用于定义类型

22.1 反模式:常量接口

  1. 接口是不能阻止被实现或继承的,也就是说子接口或实现中是能够覆盖掉常量的定义,这样通过父、子接口(或实现) 去引用常量是可能不一致的,同时也会造成子类命名空间被污染,比如继承树中可以用大量的接口、类或实例去引用同一个常量。但对于类中定义常量就不同,可以通过final修饰类,保证该类不被继承,常量值就不会被覆盖,也不存在继承树
  2. 如果有一天实现该接口的子类不再需要这些常量,它依然必须实现这个接口,以确保二进制兼容性。所谓二进制兼容:指在版本升级某文件的时,不必要做重新编译使用该文件的可执行文件,也能保证程序功能不被破坏
//1 例如如下代码,由于B类中使用了一个常量,在编译时,实际上代码A.name直接被替换为"bright"字符串,因此当A修改name值,并只重新编译A后,B中仍输出"Class A's name = bright",除非对B也重新编译,才会输出新的结果
public interface A {
  String name = "bright";
}
//file B.java
public class B {
  public static void main(String[] args) {
    System.out.println("Class A's name = " + A.name);
  }
}
//2 修改方案
public class A {
  private static final String name = "bright";
  public static String getName() {
    return name;
  }
}

public class B {
  public static void main(String[] args) {
    System.out.println("Class A's name = " + A.getName());
  }
}
//3. 本文中我理解为,实际上客户端代码会有如下情况,那么即使有一天你不想使用A中变量,就必须重新编写客户端C,还得重新编译
public class C implements A{
	public static void main(String[] args) {
		A a = new C();
	}
}
  1. 类内部使用某些常量,纯粹属于实现细节,如果类内定义一个常量,可以将其权限设置为private,外界无法访问,但如果在接口中定义常量,那么由于类实现了该接口,且接口中成员默认为public,外界也可以访问到这个成员
//虽然A接口,权限为default,正常客户类D在其他包下,是无法使用A中的name变量的,但由于D拥有使用C的权限,而C又从A中获得了name变量,导致D有方法可以直接访问到A中的name。实际上对于继承也有这个问题,但类可以final定义,或私有化构造器,以防止被继承
public interface A {
	String name = "bright";
}
public class C implements A{
	
}
public class D {
	public static void main(String[] args) {
		System.out.println(new C().name);
	}
}
  1. 实现常量接口对于代码阅读起来有困难,程序员一般认为实现接口是为了具备某种功能,而不是只为使用里面的一些常量

22.2 导出常量的合理方案

  1. 如果常量和某个现有类或接口紧密相关,可以将这些常量添加到这些类或接口中。例如Integer和Double的MIN_VALUE、MAX_VALUE
  2. 如果这些常量最好被看做枚举类型的成员,应该使用枚举类型导出这些常量
  3. 如果不满足以上两点,应提供不可实例化的工具类导出常量
package kjj.jkjk;
public class PhysicalConstants {
	private PhysicalConstants() {
	}
	//1. "_"是java7中引入的,可以提长升数字的可读性,一般一个数字中包含连续5个或以上的数字,就应考虑增加"_",一般3个数字一组,用"_"隔开,表示1000的正负倍数
	//2. 对于这种工具类,通常客户端引用为如下方式PhysicalConstants.AVOGADROS_NUMBER,如果觉得麻烦,可以使用静态导入
	public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
	public static final double BOLTZMANN_CONST = 1.380_648_52e-23;
	public static final double ELECTRON_MASS = 9.109_383_56e-31;
}

23 类层次优于标签类

23.1 标签类

class Figure {
	//1. Figure意思是图形,图形类里面有一个域,存放着Shape,表示它的形状
	//2. Figure类可以根据Shape的对象的不同,表示不同的风格,比如说三角形风格的Figure、圆形风格的Figure
	enum Shape {
		RECTANGLE, CIRCLE
	};
	//3. 而我们把存放该类的某个对象,具体属于哪种风格的实例变量,叫做标签,因为它标记了类所属的风格
	//4. 根据类中成员变量的值不同,而表示不同风格的这种类,我们叫做标签类
	final Shape shape;
	//4. 下面两个域只能在风格是三角形时,才应该使用
	double length;
	double width;
	//5. 只能在风格是圆形时,才应该使用
	double radius;

	//6. 对应圆形风格的构造器
	Figure(double radius) {
		shape = Shape.CIRCLE;
		this.radius = radius;
	}

	//7. 对应三角形风格的构造器
	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(shape);
		}
	}
}

23.2 标签类缺点

  1. 标签类充斥着样板代码
    1. 样板代码”是任何看似重复的代码,它一次又一次地出现,以便得到一些似乎应该简单得多的结果,但又不得不写,这是一个主观的定义
    2. 比如本例中枚举声明、标签域、条件语句
  2. 标签类使得多个实现挤在同一个类中,破坏可读性
  3. 实例中存在属于其他风格的不相关的域,产生更多样板代码
    1. 比如例子中,如果生成的实例为CIRCLE风格,那么专属于三角形的length、width域其实没必要存在,但由于必须存在,增加了内存
  4. 域无法做成final的,除非构造器初始化那些不相干的域,例如,如果把length、width、radius都设置成final,那么构造CIRCLE风格的类时,就不得不在构造器中也将length、width进行初始化
  5. 如果想为标签域添加一种风格,必须修改源码,同时对每个条件都要考虑到,不像继承可以新增个风格,继承原类就可以了
  6. 创建的Figure实例,我们不知道它内部的风格是什么
  7. 构造器没法利用编译器,检查出自己是否设定了一个自己想要的风格,并只对该风格所需的成员变量进行初始化。比如继承那种类层次,不同的风格实际上会对应不同的类,所以不会用混,而对于不同类中的构造器,压根不会初始化其他风格的类中的成员,如果想初始化其他风格类中的成员,编译器会报错,因为访问不到

23.3 将标签类修改为类层次

//1. 首先定义一个抽象类
//2. 将标签类中那些可以根据不同标签的值而拥有不同功能的方法(原Figure标签类中只有一个这样的方法area,它可以根据不同标签的值获得不同的功能)以抽象方法形式放到这个抽象类中
//3. 将标签类中,那些不依赖标签的值而决定行为的方法,直接放入抽象类中(Figure中不存在)
//4. 将标签类中,在不同方法下,不同标签的值的情况下,都需要用到的数据域,也放到抽象类中(Figure中不存在)
//5. 用这个抽象类作为类层次的根类
abstract class Figure {
	abstract double area();
}

//1. 为不同的标签值都定义根类的具体子类,Circle和Rectangle子类
class Circle extends Figure {
	//2. 在每个子类中包含特定于该标签值的成员变量,radius只有圆形有
	final double radius;

	Circle(double radius) {
		this.radius = radius;
	}
	//3. 在每个子类中包括针对根类中每个抽象方法的特定实现(根类Figure中只有area)
	@Override
	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;
	}

	@Override
	double area() {
		return length * width;
	}
}

23.4 类层次的优势

  1. 解决了标签类所有缺点
  2. 可以用来反映类型之间本质关系,增强灵活性
//例如如下代码,很好的反映了正方形是一种特殊的矩形的事实
class Square extends Rectangle {
	Square(double side) {
		super(side, side);
	}
}

23.5 最佳实践

  1. 编写包含一个标签域的类时,考虑改造成类层次替代
  2. 遇到包含标签域的现有类时,考虑将它重构到一个层次结构中

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

24.1 嵌套类分类

  1. 静态成员类:也可以叫做static嵌套类,可以用嵌套这个词来理解它,即我跟你没关系,自己可以完全独立存在,但是我就想借你的壳用一下,来隐藏一下我自己。这个嵌套指的是静态成员类
  2. 内部类:也可以叫做non-static嵌套类,可以用内部这个词理解它,我是你的一部分,我了解你,我知道你的全部,没有你就没有我。(所以内部类对象是以外部类对象存在为前提的)
    在这里插入图片描述

24.2 内部类使用场景

  1. 一个类A如果只对某一个类B有用,那么可以将该A作为B的内部类,使A作为B的辅助类,这样能让他们所在的包结构更简单
  2. 增加了封装,因为如果B想访问A中成员,正常情况一定要将A中的成员变量至少设置为default,但一旦将B作为A的静态内部类,B可以直接访问A中的private元素
  3. 可以提升代码的可读性和可维护性,因为在顶级类中嵌套小类会使代码更靠近使用位置(因为这个类B中的方法其实大部分是为了在类A中使用,而不是在客户端中)

24.3 静态成员类

24.3.1 特征
  1. 最好看成一个普通的类,只是碰巧被声明在另一个类的内部,它可以访问外围类的所有成员,包括那些私有的成员
24.3.2 使用场景
  1. 一般作为公有类的辅助类
  2. private的静态成员类一般用于表示其外围类代表的东西的一个组件
//例如Map实例中,表示将key和value关联起来的组件Entry,而这个Entry对象并不需要访问Map对象,因此不使用非静态内部类(浪费)

24.4 非静态成员类

24.4.1 特征
  1. 每个实例都与外围类的一个实例相关联
  2. 在非静态成员类的实例方法内部,可以调用外围实例上的方法,或利用外部类.this,获得外部实例的引用
  3. 没有外围实例,无法创建非静态成员类的实例
  4. 当在外围类的某个实例方法内部调用非静态成员类的构造器时,或外围类实例.new 非静态成员类()时(很少使用,因为嵌套类一般为外围类服务),外围实例与内部实例的关系被建立起来,且不能修改。
  5. 这种关联关系消耗非静态成员类实例的空间,并且增加构造的时间
24.4.2 使用场景
  1. 一般用非静态成员类来作为(定义)适配器,它可以将外部类的对象适配成另一种类的对象
//1. Map接口的实现,往往使用非静态成员类,将这个Map实现转为其他的集合的对象,例如HashMap中的keySet方法返回的非静态内部类KeySet,相当于将Map对象转为了一个Set对象,这个KeySet类实际上就是一个适配器(Adapter),而其内使用的Map的对象,就是被适配者(Adaptee)
//2. 书中管KeySet叫做视图,我理解意思实际上换个说法可以叫做适配器,作者应该想表达的是,KeySet的很多内容是根据Map的实现得到的,只不过换了一种展现方式,所以叫视图
final class KeySet extends AbstractSet<K> {
        public final int size(){ 
        	//3. 其实就是返回了Map对象的size属性,只不过由于适配器KeySet在类的内部,所以不用在KeySet中存放一个Map的引用,直接可以访问到,这也是为什么一般用非静态成员类作为适配器(因为不需要人为创建Adaptee的引用并存放了)
        	return size;
        }
        //其他方法
        ......
}
  1. 非静态成员类也可以用来实现外部类的迭代器(某个)的功能
import java.util.AbstractSet;
import java.util.Iterator;

public class MySet<E> extends AbstractSet<E> {
	... 
	@Override
	public Iterator<E> iterator() {
		return new MyIterator();
	}

	private class MyIterator implements Iterator<E> {
		...
	}
}

24.5 匿名类

  1. 匿名类中变量默认且必须final修饰
  2. 匿名类对象无法使用instanceof
  3. 匿名类无法实现多个接口,或扩展类的同时实现接口
  4. 匿名类必须保持简短(10行或更少),否则影响可读性
  5. 匿名类的客户端无法调用匿名类中,那些不是从其超类型中继承的成员
public class ExtendClass {
	public int a = 10;
	public static void main(String[] args) {
		ExtendClass a = new ExtendClass() {
			private String b = "handidiao";
		};
		System.out.println(a.a);
		//编译报错
//		System.out.println(a.b);
	}
}

  1. 优先使用lambda表达式替代匿名内部类,创建function object和process object
  2. 匿名类出现在非静态实例中,才持有外围实例,无论出现在哪种环境中(static or non-static),匿名类中都无法定义static成员,只能拥有常数变量
public class ExtendClass {
	public int a = 10;

	public static void main(String[] args) {
		//匿名内部类就
		ExtendClass a = new ExtendClass() {
			//不允许定义静态成员,无论是静态环境中使用还是非静态环境中使用,只能是final变量,final可以不写,默认final
			int a = 10;
			public void test1() {
				//匿名内部类里面相当于定义一个类,因此没法直接调用方法
				//System.out.println();
				//静态环境的main中,没有持有其外部类ExtendClass的引用,因此test()会报错
				//不过由于其不持有外部类的引用,也不会导致外部类对象垃圾回收失败
				//test();
			}
		};
	}

	void newTest() {
		ExtendClass a = new ExtendClass() {
			public void test1() {
				//非静态环境的newTest()中,就持有了ExtendClass的引用,可以直接调用其方法
				test();
			}
		};
	}
	

	void test() {
		
	}
}

24.6 局部类

  1. 可以在任何能够声明局部变量的地方声明局部类,这种类遵守同样的作用域规则
  2. 局部类有名字,可以被重用
  3. 与匿名类相同,出现在非静态实例中,才持有外围实例,无论出现在哪种环境中(static or non-static),匿名类中都无法定义static成员
  4. 与匿名类相同,必须非常简短,否则影响可读性

24.7 最佳实践

  1. 如果嵌套类不需要访问外围类的实例,应使用静态成员类,因为非静态成员类持有外围对象的引用,消耗时间空间,还会导致外围类实例符合垃圾回收条件时,却不能被清理,导致内存泄露
  2. 如果嵌套类是public或protected修饰的,必须慎重选择其是static或non-static,因为一旦发布,后续无法更改(客户端已经使用,变了客户端代码会报错)
  3. 如果一个类只属于一个方法的内部,可以使用匿名类或局部类,且如果只想创建一个实例,同时已经有一个预置的类型可以说明这个类的特征,就可以使用匿名类

25 一个Java源文件中不要包含多个顶级类

25.1 反例

文章中的例子我没有还原出来,我怀疑是我的编译器和他的不同,不再纠结,换一个例子

  1. 先创建一个Main.java
public class Main {
	public static void main(String[] args) {
		System.out.println(Utensil.NAME + Dessert.NAME);
	}
}
  1. 再创建一个Utensil.java
class Utensil {
	static final String NAME = "pan";
}

class Dessert {
	static final String NAME = "cake";
}
  1. 此时编译javac Main.java Utensil.java,成功,java Main打印"pancake"
  2. 再创建一个Dessert.java
class Utensil {
	static final String NAME = "pot";
}

class Dessert {
	static final String NAME = "pie";

}
  1. javac Dessert.java,编译成功,java Main打印"potpie"
  2. Dessert和Utensil不可以同时编译,但可以分别编译,且编译他们不同的类后,Main结果不同,这是不应被允许的

25.2 如何避免

  1. 可以把顶级类分别放入独立的源文件
  2. 如果非要放入同一个源文件,可以考虑使用静态内部类
public class Test {
	public static void main(String[] args) {
		System.out.println(Utensil.NAME + Dessert.NAME);
	}
	//因为这样,Utensil对应的class就叫做Test$Utensil,永远不会和别人重复
	private static class Utensil {
		static final String NAME = "pan";
	}

	private static class Dessert {
		static final String NAME = "cake";
	}
}
发布了32 篇原创文章 · 获赞 0 · 访问量 937

猜你喜欢

转载自blog.csdn.net/hanzong110/article/details/102750155