public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
EnumSet 没有公有的构造器,只有静态工厂方法。 在 OpenJDK 实现中, 它们返回两种子类之一的一个实例,具体则取决于底层枚举类型的大小:如果它的元素有 64 个或者更少,就像大多数枚举类型一样,静态工厂方法就会返回一个 RegalarEumSet 实例, 用单个 long 进行支持;如果枚举类型有 65 个或者更多元素,工厂就返回 JumboEnumSet 实例,用一个 long 数组进行支持。
/**
* Creates an empty enum set with the specified element type.
*
* @param The class of the elements in the set
* @param elementType the class object of the element type for this enum
* set
* @return An empty enum set of the specified type.
* @throws NullPointerException if elementType is null
*/
public static > EnumSet noneOf(Class elementType) {
Enum>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe);
else
return new JumboEnumSet<>(elementType, universe);
}
在 Java 8 中仍要求接口的所有静态成员都必须是 公有的。 在 Java 9 中允许接口有私有的静态方法,但是静态域和静态成员类仍然需要是公 有的。
Type 表示工厂方法所返回的对象类型,例如:
public static BufferedReader newBufferedReader(Path path, Charset cs)
throws IOException
{
CharsetDecoder decoder = cs.newDecoder();
Reader reader = new InputStreamReader(newInputStream(path), decoder);
return new BufferedReader(reader);
}
/**
* Returns an array list containing the elements returned by the
* specified enumeration in the order they are returned by the
* enumeration. This method provides interoperability between
* legacy APIs that return enumerations and new APIs that require
* collections.
*
* @param the class of the objects returned by the enumeration
* @param e enumeration providing elements for the returned
* array list
* @return an array list containing the elements returned
* by the specified enumeration.
* @since 1.4
* @see Enumeration
* @see ArrayList
*/
public static ArrayList list(Enumeration e) {
ArrayList l = new ArrayList<>();
while (e.hasMoreElements())
l.add(e.nextElement());
return l;
}
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数 ,
1.程序员一向习惯采用重叠构造器( telescoping constructor)模式,
简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难缩写, 并且仍然较难以阅读。
2.JavaBeans 模式,先调用一个无参构造器来创建对象,然后再调用 setter 方法来设置每个必要的参 数,以及每个相关的可选参数:
JavaBeans 模式自身有着很严重的缺点。
因为构造过程被分到了几个调用中,在构造过程中 JavaBean 可能处于不一致的状态。 类无法仅仅通过检验构造器参数的有效性来保证一致性。 试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码 大相径庭,因此调试起来十分困难。 与此相关的另一点不足在于, JavaBeans 模式使得把 类做成不可变的可能性不复存在 .
package com.liruilong.common.demo;
/**
* @Auther Liruilong
* @Date 2020/8/4 12:34
* @Description:
*/
public class Demo {
private final int a;
private final int d;
private Demo(Bulider bulider) {
a = bulider.a;
d = bulider.d;
}
public static class Bulider {
private int a;
private int d;
public Bulider(int a) {
this.a = a;
}
public Demo build() {
return new Demo(this);
}
public Bulider setA(int a) {
this.a = a;
return this;
}
public Bulider setD(int d) {
this.d = d;
return this;
}
}
public static void main(String[] args) {
Demo build = new Demo.Bulider(3).setD(4).build();
}
}
注意Demo是不可变的,所有的默认参数值都单独放在个地方。builder的设值方法返回builder本身,以便把调用链接起来,得到一个流式的API。 BuiIde模式模拟了具名的可选参数。
为了简洁起见,示例中省略了有效性检查。要想尽快侦测到无效的参数,可以在builder的构造器和方法中检查参数的有效性。查看不可变量,包括build方法调用的构造器中的多个参数。为了确保这些不变量免受攻击,从builder复制完参数之后,要检查对象域(详见第50条)。如果检查失败,就抛出Il leg a lArgumen tExcept ion (详见第72条),其中的详细信息会说明哪些参数是无效的(详见第75条)
package com.liruilong.common;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
/**
* @Auther Liruilong
* @Date 2020/8/4 20:04
* @Description:
*/
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set toppings;
abstract static class Builder>{
// TODO: 2020/8/4 noneOf(class elementType) : 使用指定的元素类型创建一个空的枚举集
EnumSet toppings= EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
// TODO: 2020/8/4 检查指定的对象引用不是 null : 检查指定的对象引用不是 null (抛异常)
toppings.add(Objects.requireNonNull(topping));
return self();
}
protected abstract T self();
}
Pizza (Builder> builder){
toppings = builder.toppings.clone();
}
}
package com.liruilong.common;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
/**
* @Auther Liruilong
* @Date 2020/8/4 20:04
* @Description: 通过内部抽象类来构建成员。
*/
public abstract class Pizza {
public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
final Set toppings;
abstract static class Builder> {
// TODO: 2020/8/4 noneOf(class elementType) : 使用指定的元素类型创建一个空的枚举集
EnumSet toppings = EnumSet.noneOf(Topping.class);
public T addTopping(Topping topping) {
// TODO: 2020/8/4 检查指定的对象引用不是 null : 检查指定的对象引用不是 null (抛异常)
toppings.add(Objects.requireNonNull(topping));
return self();
}
protected abstract T self();
abstract Pizza build();
}
Pizza(Builder> builder) {
toppings = builder.toppings.clone();
}
}
Pizza.Builder的类型是泛型(generictype),带有一个递归类型参数(recursivetype parameter),详见第30条。它和抽象的self方法一样,允许在子类中适当地进行方法链接,不需要转换类型。这个针对Java缺乏self类型的解决方案,被称作模拟的self类型(simulatedself-type)。
package com.liruilong.common;
/**
* @Auther Liruilong
* @Date 2020/8/5 19:58
* @Description:
*/
public class Calzone extends Pizza {
private final boolean sauceInside;
public static class Builder extends Pizza.Builder {
private boolean sauceInside = false; //default
public Builder sauceInside() {
sauceInside = true;
return this;
}
// TODO 方法重写 return 可以是子类型
@Override
public Calzone build() {
return new Calzone(this);
}
@Override
protected Builder self() {
return this;
}
}
private Calzone(Builder builder){
super(builder);
sauceInside = builder.sauceInside;
}
}
-------------------------------------
package com.liruilong.common;
import java.util.Objects;
/**
* @Auther Liruilong
* @Date 2020/8/5 19:47
* @Description:
*/
public class NyPizza extends Pizza {
public enum Size {SMALL, LARGE}
private final Size size;
public static class Builder extends Pizza.Builder {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
@Override
protected Builder self() {
return this;
}
@Override
NyPizza build() {
return new NyPizza(this);
}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
public static void main(String[] args) {
NyPizza pizza = new NyPizza.Builder(Size.SMALL).addTopping(Topping.ONION)
.addTopping(Topping.ONION).build();
Calzone calzone = new Calzone.Builder()
.addTopping(Topping.ONION).sauceInside().build();
}
}
在该方法中,子类方法声明返回超级类中声明的返回类型的子类型,这被称作协变返回类型(covariant return type)。它允许客户端无须转换类型就能使用这些构建器
如果类的构造器或者静态工厂中具有多个参数,设计这种类时,Builde模式就是一种不错的选择,特别是当大多数参数都是可选或者类型相同的时候。与使用重叠构造器模式相比,使用Builder模式的客户端代码将更易于阅读和编写,构建器也比JavaBeans更加安全。
Singleton是指仅仅被实例化一次的类。Singleton通常被用来代表一个无状态的对象,如函数(详见第24条),或者那些本质上唯一的系统组件。使类成为Singleton会使它的害户端测试变得十分困难
package com.liruilong.common;
/**
* @Auther Liruilong
* @Date 2020/8/6 19:48
* @Description:
*/
public class Singleton {
public static final Singleton sin = new Singleton();
// todo 私有状态,保证全局唯一性。
/***
AccessibleObject.setAccessible方法,通过反射机制(详见第65条)调用私有构造器
* @Description
* @author Liruilong
* @Date 2020年08月06日 20:08:39
**/
private Singleton(){}
public static Singleton getSin() {
return sin;
}
}
使用静态工厂的一个优势是,可以通过方法引用(method reference)作为提供者
序列化状态 添加 transient 是实例域为,并提供一个readResolve 方法
package com.liruilong.common;
import java.io.Serializable;
/**
* @Auther Liruilong
* @Date 2020/8/6 19:48
* @Description:
*/
public class Singleton implements Serializable {
//todo 序列化状态 添加 transient 是实例域为,并提供一个readResolve 方法
public transient static final Singleton sin = new Singleton();
// todo 私有状态,保证全局唯一性。
/***
AccessibleObject.setAccessible方法,通过反射机制(详见第65条)调用私有构造器
* @Description
* @author Liruilong
* @Date 2020年08月06日 20:08:39
**/
private Singleton(){}
public static Singleton getSin() {
return sin;
}
public Singleton readResolve(){
return sin;
}
}
实现Singleton的第三种方法是声明一个包含单个元素的枚举类型:单元素的枚举类型经常成为实现Singleton的最佳方法。
package com.liruilong.singleton;
/**
* @Author: Liruilong
* @Date: 2019/7/20 17:55
*/
// final 不允许被继承
public final class Singleton {
// 实例变量
private byte[] bate = new byte[1024];
// 私有的构造函数,即不允许外部 new
private Singleton(){ }
/**
* @Author Liruilong
* @Description 基于枚举类线程安全
* 枚举类型不允许被继承,同样线程安全的,且只能被实例化一次。
* @Date 17:33 2019/7/26
* @Param []
* @return com.liruilong.singleton.Singleton
**/
private enum Singtetonss {
SINGTETONSS; //实例必须第一行,默认 public final static修饰
private Singleton singleton;
Singtetonss() { //构造器。默认私有
this.singleton = new Singleton();
}
public static Singleton getInstance() {
return SINGTETONSS.singleton;
}
}
public static Singleton getInstance3(){
return Singtetonss.getInstance();
}
企图通过将类做成抽象类来强制该类不可被实例化是行不通的。该类可以被子类化,并且该子类也可以被实例化。
因此只要让这个类包含一个私有构造器,它就不能被实例化:
package com.liruilong.common;
/**
* @Auther Liruilong
* @Date 2020/8/6 20:31
* @Description:
*/
public class NoNewClass {
private NoNewClass(){
//
throw new AssertionError();
}
}
有许多类会依赖一个或多个底层的资源。例如,拼写检查器需要依赖词典。
不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注入,它极大地提升了类的灵活性、可重用性和可测试性
一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。
如果对象是不可变的(immutable)它就始终可以被重用。
对于同时提供了静态工厂方法(staticfactory method) 和构造器的不可变类,通常优先使用静态工厂方法而不是构造器
注意构造器Boolean(String)在Java9中已经被废弃了。
构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。
public static void main(String[] args) {
// Boolean.valueOf(""); 优于 new Boolean("");
Boolean.valueOf("");
new Boolean("");
}
对于字符串的正则匹配。虽然String.matches方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用
它在内部为正则表达式创建了一个Pattern实例,却只用了一次,之后就可以进行垃圾回收了。创建Patter口实例的戚本很高,因为需要将正则表达式编译成一个有限状态机(finitestate machine)。
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]D?C{0,3})" +
"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s){
// 正确的写法:
ROMAN.matcher(s).matches();
return s.matches("^(?=.)M*(C[MD]D?C{0,3})" +
"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(BoxedPrimitive Type)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除 。
public static long sum(){
// long sum = 0; 比 Long 快很多
// 要优先使用基本类型,而不是封装类型,要当心无意识的使用自动装箱
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量(详见第57条),这种情形就会自然而然地发生
package com.liruilong.common;
import java.util.Arrays;
import java.util.EmptyStackException;
import java.util.Objects;
/**
* @Auther Liruilong
* @Date 2020/8/6 20:54
* @Description:
*/
public class Stack {
private Object[] element;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.element = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object object){
ensureCapactity();
element[size++] = object;
}
public Object pop(){
if (size == 0){
throw new EmptyStackException();
}
return element[--size];
}
// TODO 程序中哪里发生了内存泄漏呢?如果一个战先是增长,然后再收缩,
// 那么,从校中弹出来的对象将不会被当作垃圾回收,
// 即使使用校的程序不再引用这些对象,它们也不会被回收。
// 这是因为校内部维护着对这些对象的过期引用(obsolete reference)。
// 所谓的过期引用,是指永远也不会再被解除的引用。
// 在本例中,凡是在elements数组的“活动部分”(active portion)之外的任何引用都是过期的。
// 活动部分是指elements中下标小于size的那些元素
private void ensureCapactity() {
if(element.length == size){
element = Arrays.copyOf(element, 2 * size + 1);
}
}
}
在支持垃圾回收的语言中,内存泄漏是很隐蔽的(称这类内存泄漏为“无意识的对象保持”(unintentionalobject retention)更为恰当)。如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不仅不会处理这个对象.
public Object pop(){
if (size == 0){
throw new EmptyStackException();
}
Object result = element[--size];
element[size] = null;
return result;
}
WeakHashMap:
public class WeakHashMapextends AbstractMapimplements Map
以弱键 实现的基于哈希表的 Map。在 WeakHashMap 中,当某个键不再正常使用时,将自动移除其条目。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。丢弃某个键时,其条目从映射中有效地移除,因此,该类的行为与其他的 Map 实现有所不同。
null 值和 null 键都被支持。该类具有与 HashMap 类相似的性能特征,并具有相同的效能参数初始容量 和加载因子。
像大多数 collection 类一样,该类是不同步的。可以使用
Collections.synchronizedMap
方法来构造同步的 WeakHashMap。该类主要与这样的键对象一起使用,其 equals 方法使用 == 运算符来测试对象标识。一旦这种键被丢弃,就永远无法再创建了,所以,过段时间后在 WeakHashMap 中查找此键是不可能的,不必对其项已移除而感到惊讶。该类十分适合与 equals 方法不是基于对象标识的键对象一起使用,比如,String 实例。然而,对于这种可重新创建的键对象,键若丢弃,就自动移除 WeakHashMap 条目,这种表现令人疑惑。
WeakHashMap 类的行为部分取决于垃圾回收器的动作,所以,几个常见的(虽然不是必需的)Map 常量不支持此类。因为垃圾回收器在任何时候都可能丢弃键,WeakHashMap 就像是一个被悄悄移除条目的未知线程。特别地,即使对 WeakHashMap 实例进行同步,并且没有调用任何赋值方法,在一段时间后 size 方法也可能返回较小的值,对于 isEmpty 方法,返回 false,然后返回 true,对于给定的键,containsKey 方法返回 true 然后返回 false,对于给定的键,get 方法返回一个值,但接着返回 null,对于以前出现在映射中的键,put 方法返回 null,而 remove 方法返回 false,对于键 set、值 collection 和条目 set 进行的检查,生成的元素数量越来越少。
WeakHashMap 中的每个键对象间接地存储为一个弱引用的指示对象。因此,不管是在映射内还是在映射之外,只有在垃圾回收器清除某个键的弱引用之后,该键才会自动移除。
实现注意事项:WeakHashMap 中的值对象由普通的强引用保持。因此应该小心谨慎,确保值对象不会直接或间接地强引用其自身的键,因为这会阻止键的丢弃。注意,值对象可以通过 WeakHashMap 本身间接引用其对应的键;这就是说,某个值对象可能强引用某个其他的键对象,而与该键对象相关联的值对象转而强引用第一个值对象的键。处理此问题的一种方法是,在插入前将值自身包装在 WeakReferences 中,如:m.put(key, new WeakReference(value)),然后,分别用 get 进行解包。
collection 的 iterator 方法所返回的迭代器(由该类所有“collection 视图方法”返回)均是快速失败的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出
ConcurrentModificationException
。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测 bug。
此类是 Java Collections Framework 的成员。
public static void main(String[] args) {
System.out.println(isRomanNumeral("IV"));
WeakHashMap weakHashMap = new WeakHashMap(){
{
for (int i = 0; i < 0 ; i++) {
put(i+"","==========="+i);
}
}
};
weakHashMap.forEach((o1,o2) -> System.out.println("原来的数据:"+o1+":"+o2));
//调用垃圾回收后没有数据
Runtime.getRuntime().gc();
System.out.println("额 ,数据呢:"+weakHashMap.get("1"));
weakHashMap.forEach((o1,o2) -> System.out.println("现在的数据:"+o1+":"+o2));
}
缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是ScheduledThreadPoolExecutor)[在给定延时后执行异步任务或者周期性执行任务]来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref
如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weakreference),例如,只将它们保存成WeakHashMap中的键.
package com.liruilong.common.util;
import java.util.Map;
import java.util.Objects;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author Liruilong
* @Date 2020/8/11 09:37
* @Description: 基于 WeakHashMap 的缓存实现
*/
public class WeakHashMapCache {
private final int size;
private final Map eden;
private final Map longterm;
private WeakHashMapCache(Builder builder){
this.size = builder.size;
this.eden = builder.eden;
this.longterm = builder.longterm;
}
public static class Builder{
private volatile int size;
private volatile Map eden;
private volatile Map longterm;
public Builder(int size){
this.size = rangeCheck(size,Integer.MAX_VALUE,"缓存容器初始化容量异常");
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}
private static int rangeCheck(int val, int i, String arg) {
if (val < 0 || val > i) {
throw new IllegalArgumentException(arg + ":" + val);
}
return val;
}
public WeakHashMapCache build(){
return new WeakHashMapCache(this);
}
}
public V get(K k){
V v = this.eden.get(k);
if (Objects.isNull(v)){
v = this.longterm.get(k);
if (Objects.nonNull(v)){
this.eden.put(k,v);
}
}
return v;
}
public void put(K k,V v){
if (this.eden.size() >= size){
this.longterm.putAll(this.eden);
this.eden.clear();
}
this.eden.put(k,v);
}
public static void main(String[] args) {
WeakHashMapCache cache = new WeakHashMapCache.Builder(3).build();
for (int i = 0; i < 5; i++) {
cache.put(i+"",i+"");
}
System.gc();
for (int i = 0; i < 5; i++) {
System.out.println(cache.get(i + ""));
}
}
}
https://github.com/apache/tomcat/blob/3e5ce3108e2684bc25013d9a84a7966a6dcd6e14/java/org/apache/tomcat/util/collections/ConcurrentCache.java
--- Ajax请求之后的数据会一直保存在内存中,等待下一次请求覆盖掉,还是会直接被js回收掉??
借助于Heap剖析工具(HeapProfiler)才能发现内存泄漏问题
终结方法(finalize「)通常是不可预测的,也是很危险的,一般情况下是不必要的,
在Java9 中用清除方法(cleaner)代替了终结方法。清除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的
终结方法和清除方法的缺点在于不能保证会被及时执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。这意味着,注重时间的任务不应该由终结方法或者清除方法来完成
终结方法线程的优先级比该应用程序的其他线程的优先级要低得多。
永远不应该依赖终结方法或者清除方法来更新重要的持久状态,不要被System.gc和System.runFinalization这两个方法所诱惑。
当Java7引人try-with刊sources语句。要使用这个构造的资源,必须先实现AutoCloseable接口,其中包含了单个返回void的close方法。Java类库与第三方类库中的许多类和接口,现在都实现或扩展了AutoCloseable接口。如果编写了一个类,它代表的是必须被关闭的资源,那么这个类也应该实现AutoCloseable。
public class Stack extends Exception implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("资源关闭");
}
public static void main(String[] args) {
try(Stack stack = new Stack()){
stack.push("嘻嘻,加油");
System.out.println(stack.pop());
} catch (Exception e) {
e.printStackTrace();
}
}
}
嘻嘻,加油
资源关闭
Object所有的非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定(general contract), 因为它们设计成是要被覆盖(override)的。任何一个类,它在覆盖这些方法的时·候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运作。
什么时候应该覆盖equals方法呢?
有一种“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”的类。枚举类型(详见第34条)就属于这种类。对于这样的类而言,逻辑相同与对象等同是一回事,因此Object的equals方法等同于逻辑意义上的equals方法
如果类具有自己特有的“逻辑相等”(logicalequality)概念(不同于对象等同的概念),而且超类还没有覆盖equals。这通常属于“值类”(value class)的情形。值类仅仅是一个表示值的类。
五个要求:
在equals方法中用getClass测试代替instanceof测试,可以扩展可实例化的类和增加新的值组件,同时保留equals约定的不可行性。
我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定
方法:
注意:
package com.liruilong.common;
import java.util.Objects;
/**
* @Author Liruilong
* @Date 2020/8/8 17:25
* @Description:
*/
public class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeChek(areaCode, 999, "area code");
this.prefix = rangeChek(prefix,999,"pre fix");
this.lineNum = rangeChek(lineNum,999,"lineNum");
}
private static short rangeChek(int val, int i, String arg) {
if (val < 0 || val > i) {
throw new IllegalArgumentException(arg + ":" + val);
}
return (short) val;
}
/**
* @param o
* @return boolean
* @Description
* 使用==操作符检查“参数是否为这个对象的引用”
* 使用instanceof操作符检查“参数是否为正确的类型”为空。
* 把参数转换成正确的类型
* 对于该类中的每个“关键”(significant )域,检查参数中的域是否与该对象中对应的域相匹配。
* @author Liruilong
* @Date 2020年08月08日 17:08:46
**/
@Override
public boolean equals(Object o) {
// 使用==操作符检查“参数是否为这个对象的引用”
if (this == o) {
return true;
}
// 使用instanceof操作符检查“参数是否为正确的类型”为空。测试等同性的同时,测试非空性。
if (!(o instanceof PhoneNumber)) {
return false;
}
// 把参数转换成正确的类型
PhoneNumber that = (PhoneNumber) o;
// 对于该类中的每个“关键”(significant )域,检查参数中的域是否与该对象中对应的域相匹配。
return areaCode == that.areaCode &&
prefix == that.prefix &&
lineNum == that.lineNum;
}
@Override
public int hashCode() {
return Objects.hash(areaCode, prefix, lineNum);
}
}
在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。如果不这样做的话,就会违反hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这类集合包括HashMap和HashSet。
因没有覆盖hashCode而违反的关键约定是第二条:相等的对象必须具有相等的散到码(hashcode )。
@Override
public int hashCode() {
return Objects.hash(areaCode, prefix, lineNum);
}
//---------------------------
public static int hash(Object... values) {
return Arrays.hashCode(values);
}
//---------------------------
public static int hashCode(Object a[]) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
提供好的t。String实现可以便类用起来更加舒适,使用了这个类的系统也更易于调试。
@Override
public String toString() {
return "PhoneNumber{" +
"areaCode=" + areaCode +
", prefix=" + prefix +
", lineNum=" + lineNum +
'}';
}
public static void main(String[] args) {
PhoneNumber phoneNumber = new PhoneNumber(12,23,34);
System.out.println(phoneNumber);
}
com.liruilong.common.PhoneNumber@a456
PhoneNumber{areaCode=12, prefix=23, lineNum=34}
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息,
public static void main(String[] args) {
PhoneNumber phoneNumber = new PhoneNumber(12,212323,34);
System.out.println(phoneNumber);
}
要在你编写的每一个可实例化的类中覆盖Object的toString实现,除非已经在超类中这么做了。这样会使类使用起来更加舒适,也更易于调试。toString方法应该以美观的格式返回一个关于对象的简洁、有用的描述。
Exception in thread "main" java.lang.IllegalArgumentException: pre fix:212323
at com.liruilong.common.PhoneNumber.rangeChek(PhoneNumber.java:21)
at com.liruilong.common.PhoneNumber.(PhoneNumber.java:15)
at com.liruilong.common.PhoneNumber.main(PhoneNumber.java:70)
制定格式:
// 无论是否决定指定格式,都应该在文档中明确地表明你的意图
@Override
public String toString() {
return String.format("%03d-%03d-%04d",areaCode,prefix,lineNum);
}
012-045-0034
Cloneable接口的目的是作为一个标注接口。表明该类的实例允许克隆。
事实上,实现Cloneable接口的类是为了提供一个功能适当的公有的clone方法。
package com.liruilong.common;
import java.util.Objects;
/**
* @Author Liruilong
* @Date 2020/8/8 17:25
* @Description:
*/
public class PhoneNumber implements Cloneable{
@Override
protected PhoneNumber clone() throws CloneNotSupportedException {
try {
return ((PhoneNumber) super.clone());
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
}
因为Java支持协变返回类型(covariant return type)。换句话说,目前覆盖方法的返回类型可以是被覆盖方法的返回类型的子类了.
/* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
*/
protected native Object clone() throws CloneNotSupportedException;
对super.clone方法的调用应当包含在一个try-catch块中。这是因为Object声明其clone方法抛出CloneNotSupportedException,这是一个受检异常(checkedexception)。由于PhoneNumber实现了Cloneable接口
对于域中包含引用类的对象。
实际上,clone方法就是另一个构造器;必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件
Cloneable架构与引用可变对象的final域的正常用法是不相兼容的,
对象拷贝的更好的办法是提供一个拷贝构造器(copyconstructo「)或拷贝工厂(copy factory )。
总之,复制功能最好由构造器或者工厂提供。这条规则最绝对的例外是数组,最好利用clone方法复制数组。
compareTo方法并没有在Object类中声明。相反,它是Comparable接口中唯一的方法。将这个对象与指定的对象进行比较。当该对象小于、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException异常
public interface Comparable {
/* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws NullPointerException if the specified object is null
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this object.
*/
public int compareTo(T o);
}
compareTo方法不但允许进行简单的等同性比较,而且允许执行顺序比较,除此之外,它与Object的equals方法具有相似的特征,它还是个泛型(generic)。
类实现了Comparable接口,就表明它的实例具有内在的排序关系( natural ordering)。为实现Comparable接口的对象数组进行排序就这么简单。
Arrays.sort(new PhoneNumber[]{new PhoneNumber(12,23,34)});
在compareTo方法中使用关系操作符〈和〉是非常烦琐的,并且容易出错,因此不再建议使用。
public static void main(String[] args) {
Demo build = new Bulider(3).setD(4).build();
Set s = new TreeSet();
Collections.addAll(s,args);
}
Java 平台类库中的所有值类( value classes), 以及所有的枚 举类型(详见第 34 条)都实现了 Comparable 接口 。 如果你正在编写一个值类,它具有非 常明显的内在排序关系,比如按字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现 Comparable 接口:
在 Java 8 中, Comparator 接口配置了一组 比较器构造方法 ( comparator construction methods),使得 比较器的构造工作变得非常流畅。 之后,按照 Comparable 接口 的要求, 这些比较器可以用来实现一个 compareTo 方法
static Comparator
private static final Comparator COMPARATOR = Comparator
.comparingInt((PhoneNumber phoneNumber) ->phoneNumber.areaCode)
.thenComparingInt((phoneNumber) ->phoneNumber.lineNum);
区分一个组件设计得好不好,唯一重要的因素在于,它对于外部的其他组件而言,是 否隐藏了其内部数据和其他实现细节。
设计良好的组件会隐藏所有的实现细节, 把API 与 实现清晰地隔离开来。 然后,组件之间只通过 API 进行通信,一个模块不需要知道其他模 块的内部工作情况。
这个概念被称为信息隐藏( info1mation hiding)或封装( encapsulation), 是软件设计的基本原则之一。
它可以有效地解除组成系统 的各组件之间的藕合关系,即解相( decouple),使得这些组件可以独立地开发、 测试、优 化、使用、理解和修改。 因为这些组件可以并行开发,
Java 提供了许多机制(facility)来协助信息隐藏。 访问控制(access control)机制决定了类、接口和成员的可访问性( accessibility)
实体的可访问性是由该实体声明所 在的位置,以及该实体声明中所出现的访问修饰符( private 、 protected 和 public) 共同决定的。
尽可能地使每个类或者成员不被外界访问。 换句话说,应该使用与你 在编写的软件的对应功能相一致的、 尽可能最小的访问级别。
对于顶层的(非嵌套的)类和接口,只有两种可能的访问级别:包级私有的( packageprivat巳)和公有的( public)。 如果你用 public 修饰符声明了顶层类或者接口,那它就是公有 的;否则,它将是包级私有的。
包级私有:如果类或者接口能够被做成包级私有的,它就应该被做成包 级私有。 通过把类或者接口做成包级私有,它实际上成了这个包的实现的一部分,而不是该包导出的 API 的一部分,在以后的发行版本中,可以对它进行修改、替换或者删除,而无 须担心会影响到现有的客户端程序。 如果把它做成公有的,你就有责任永远支持它,以保持 它们的兼容性。
私有嵌套类:如果一个包级私有的顶层类(或者接口) 只是在某一个类的内部被用到,就应该考虑使 它成为唯一使用它的那个类的私有嵌套类(详见第 24 条)。 这样可以将它的可访问范围从包 中的所有类缩小到使用它的那个类。
降低不必要公有类的可访问性,比降低包级私有 的顶层类的可访问性重要得多:
对于成员(域、方法、嵌套类和嵌套接口)有四种可能的访问级别,下面按照可访问性 的递增顺序罗列出来:
当你仔细地设计了类的公有 API 之后,可能觉得应该把所有其他的成员都变成私有 的。 其实,只有当同一个包内的另一个类真正需要访问一个成员的时候,你才应该删除 private 修饰符,使该成员变成包级私有的。
私有成员和包级私有成员都是一个类的实现中的一部分,一般不会影响导出 的 API。 然而,如果这个类实现了 Serializable 接口(详见第 86 条和第 87 条),这些域就有可能会被“泄漏”(leak)到导出的 API 中。
对于公有类的成员,当访问级别从包级私有变成保护级别时,会大大增强可访问性。 受保护的成员是类的导出的 API 的一部分,必须永远得到支持。 导出的类的受保护成员也 代表了该类对于某个实现细节的公开承诺(详见第四条)。 应该尽量少用受保护的成员 。
有一条规则限制了降低方法的可访问性的能力。 如果方法覆盖了超类中的一个方法, 子类中的访问级别就不允许低于超类中的访问级别 。 这样可以确保任何可使用超类的实例的地方也都可以使用子类的实例(里氏替换原则,详见第 10 条)。 如果违反 了这条规则,那么当你试图编译该子类的时候,编译器就会产生一条错误消息。
这条规则有 一个特例:如果一个类实现了一个接口,那么接口中所有的方法在这个类中也都必须被声明 为公有的。
如果实例域是非 final 的,或者是一 个指向可变对象的 final 引用, 那么一旦使这个域成为公有的,就等于放弃了对存储在这个 域中的值进行限制的能力;这意味着,你也放弃了强制这个域不可变的能力。 同时,当这个 域被修改的时候,你也失去了对它采取任何行动的能力。
因此, 包含公有可变域的类通常并 不是线程安全的。 即使域是 final 的,并且引用不可变的对象,但当把这个域变成公有的时 候,也就放弃了“切换到一种新的内部数据表示法”的灵活性。
如果 final 域包含可变对 象的引用,它便具有非 final 域的所有缺点。 虽然引用本身不能被修改,但是它所引用的对 象却可以被修改,这会导致灾难性的后果。 注意,长度非零的数组总是可变的,所以让类具有公有的静态 final 数组域,或者返回 这种域的访问方法,这是错误的。 如果类具有这样的域或者访问方法,客户端将能够修改数 组中的内容。 这是安全漏洞的一个常见根源:
修正这个问题有两种方法。 可以使公有数组变成私有的,并增加一个公有的不可变 列表:
另一种方法是,也可以使数组变成私有的,并添加一个公有方法,它返回私有数组的 一个拷贝 :
package com.liruilong.common.util;
import java.util.Arrays;
import java.util.List;
/**
* @author Liruilong
* @Date 2020/8/22 14:02
* @Description:
*/
public class Demo {
// 长度为零的数组总数可变的
public static final String[] VALUE ={};
// 方法一
private static final String [] PRAVATE_VALUES = {};
public static final List VALUES = Arrays.asList(PRAVATE_VALUES);
// 方法二
public static final String[] value(){
return PRAVATE_VALUES.clone();
}
}
从 Java 9 开始,又新增了两种隐式访问级别,作为模块系统( module system)的一部 分。
一个模块就是一组包,就像一个包就是一组类一样。 模块可以通过其模块声明( module declaration)中的导出声明 ( export declaration) 显式地导出它的一部分包(按照惯例,这包含在名为 module-info.java 的源文件中)。
模块中未被导出的包在模块之外是不可访问 的;在模块内部,可访问性不受导出声明的影响。 使用模块系统可以在模块内部的包之间共 享类,不用让它们对全世界都可见。 未导出的包中公有类的公有成员和受保护的成员都提 高了两个隐式访问级别,这是正常的公有和受保护级别在模块内部的对等体( intramodular analogues)。 对于这种共享的需求相对罕见,经常通过在包内部重新安排类来解决。
总而言之,应该始终尽可能(合理)地降低程序元素的可访问性。 在仔细地设计了一个 最小的公有 API 之后,应该防止把任何散乱的类、接口或者成员变成 API 的一部分。 除了 公有静态 final 域的特殊情形之外(此时它们充当常量),公有类都不应该包含公有域,并且 要确保公有静态 final 域所引用的对象都是不可变的。
如果类可以在 它所在的包之外进行访问,就提供访问方法,。
如果类是包级私有的,或者是私有的嵌套类, 直接暴露它的数据域并没有本质 的错误。
公有类永远都不应该暴露可变的域。 虽然还是有问题,但是让公有类暴露 不可变的域,其危害相对来说比较小。 但有时候会需要用包级私有的或者私有的嵌套类来暴 露域,无论这个类是可变的还是不可变的。
不可变类是指其实例不能被修改的类。
每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期( lifetime)内固定不变。 Java 平台类库中包含许 多不可变的类,
有 String 、基本类型的包装类、 Biginteger 和 BigDecimal。 存在不可变的类有许多理由:不可变的类比可变类更加易于设计、实现和使用。 它们不容易出 错,且更加安全。
为了使类成为不可变,要遵循下面五条规则:
1. 不要提供任何会修改对象状态的方法 (也称为设值方法)。
2. 保证类不会被扩展。
3. 声明所有的域都是 final 的。 通过系统的强制方式可以清楚地表明你的意图。
4. 声明所有的域都为私有的。 这样可以防止客户端获得访问被域引用的可变对象的权限,
5.确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该 类的客户端无法获得指向这些对象的引用。
不可变对象本质上是线程安全的,它们不要求同步。 当多个线程并发访问这样的对象 时它们不会遭到破坏。 这无疑是获得线程安全最容易的办法。 实际上,没有任何线程会注意到其他线程对于不可变对象的影响。 所以, 不可变对象可以被自由地共享。 不可变类应该 充分利用这种优势,鼓励客户端尽可能地重用现有的实例。
除非有很好的理由要让类 成为可变的类,否则它就应该是不可变的。
如果类不能被做成不可变的,仍然应该 尽可能地限制它的可变性。
除非有令人信服的理由要 使域变成是非 final 的,否则要使每个域都是 private final 的。 构造器应该创建完全初始化的对象,并建立起所有的约束关系。
继承( inheritanc巳)是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具。
与方法调用不同的是,继承打破了封装性[ Snyder86 ]。 换句话说,子类依赖于其超 类中特定功能的实现细节。 超类的实现有可能会随着发行版本的不同而有所变化,如果真的 发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。 因而,子类必须要跟着其 超类的更新而演变,除非超类是专门为了扩展而设计的,并且具有很好的文挡说明。 为了说明得更加具体一点,我们假设有一个程序使用了 HashSet。 为了调优该程序的性能,需要查询 HashSet ,看一看自从它被创建以来添加了多少个元素(不要与它当前的 元素数目泪淆起来, 它会随着元素的删除而递减)。 为了提供这种功能,我们得编写一个 HashSet 变体,定义记录试图插入的元素的数量 addCount,井针对该计数值导出一个访问 方法。 HashSet 类包含两个可以增加元素的方法: add 和 addAll ,因此这两个方法都要 被覆盖:
package com.liruilong.common.util;
import java.util.*;
/**
* @author Liruilong
* @Date 2020/8/22 14:02
* @Description:
*/
public class InstrumentedHashSet extends HashSet{
private int addCount = 0;
public InstrumentedHashSet() {
}
public InstrumentedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, 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;
}
}
在 HashSet 的 内 部, addAll 方法是基于它的 add 方法来实现的
即不扩展现有的类,而是在新 的类中增加一个私有域,它引用现有类的一个实例。 这种设计被称为“复合”(composition), 因为现有的类变成了新类的一个组件。 新类中的每个实例方法都可以调用被包含的现有类 实例中对应的方法,并返回它的结果。 这被称为转发( forwarding)
类本身和可重用的转发类( forwarding class),其中包含了所有的转发方法,没有任何其 他的方法:
package com.liruilong.common.util.ziptar;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
/**
* @author Liruilong
* @Date 2020/8/22 17:02
* @Description:
*/
public class ForwardingSet implements Set {
private final Set s;
public ForwardingSet(Set 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);
}
package com.liruilong.common.util;
import com.liruilong.common.util.ziptar.ForwardingSet;
import java.util.*;
/**
* @author Liruilong
* @Date 2020/8/22 14:02
* @Description:
*/
public class InstrumentedHashSet extends ForwardingSet {
private int addCount = 0;
public InstrumentedHashSet(Set 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 int getAddCount(){
return addCount;
}
}
因为每一个 InstrumentedSet 实例都把另一个 Set 实例包装起来了,所以 InstrumentedSet 类被称为包装类(wrapper class)。 这也正是 Decorator(修饰者)模式 。
因为 InstrumentedSet 类对一个集合进行了修饰,为它增加了计数特性。 有时复合和转 发的结合也被宽松地称为“委托” ( d巳legatio口)。 从技术的角度而言,这不是委托,除非包装 对象把自身传递给被包装的对象
包装类几乎没有什么缺点。 需要注意的一点是, 包装类不适合用于回调框架。Guava 就为所有的集合接口提供了转发类 。
只有当子类真正是超类的子类型( subtyp巳)时,才适合用继承。 换句话说,对于两个类 A 和 B,只有当两者之间确实存在“ is-a”关系的时候,类 B 才应该扩展类 A。
继承的功能非常强大,但是也存在诸多问题,因为它违背了封装原则。 只 有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。 即使如此,如果子 类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性 ( fragility)。 为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当 的接口可以实现包装类的时候。 包装类不仅比子类更加健壮,而且功能也更加强大。
该类的文档必须精确地描述覆盖每个方法所带来的影响。 换句话说, 该类必须 有文档说明它可覆盖( ove「「idable )的方法的自用性( se协use )。
好的 API 文档应该描述一个给定的方法做了什么工作,而不 是描述它是如何做到的。
对于为了继承而设计的类,唯一的测试方法就是编写子类。
必须在发布类之前先编写子类对类进行测试。
为了允许继承,类还必须遵守其他一些约束。 构造器决不能调用可被覆盖的方法, 无论是直接调用还是间接调用。 如果违反了这条规则,很有可能导致程序失败。 超类的构造器 在子类的构造器之前运行,所以,子类中覆盖版本的方法将会在子类的构造器运行之前先被 调用。 如果该覆盖版本的方法依赖于子类构造器所执行的任何初始化工作,该方法将不会如 预期般执行。
构造器决不能调用可被覆盖的方法,
package com.liruilong;
/**
* @author Liruilong
* @Date 2020/8/29 19:31
* @Description:
*/
public class Super {
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
package com.liruilong.fserve;
import com.liruilong.Super;
import java.time.Instant;
/**
* @author Liruilong
* @Date 2020/8/29 19:32
* @Description:
*/
public final class Sub extends Super {
private final Instant instant;
Sub() {
instant = Instant.now();
}
@Override
public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
无论是 clone 还是readObject, 都不可以调用可覆盖的方 法,不管是以直接还是间接的方式。
对于 readObject 方法,覆盖的方法将在子类的状态 被反序列化 ( deserialized)之前先被运行;而对于 clone 方法,覆盖的方法则是在子类的 clone 方法有机会修正被克隆对象的状态之前先被运行。
如果你决定在一个为了继承而设计的类中实现 Serializable 接口,并且该类 有一个 readResolve 或者 writeReplace 方法,就必须使 readResolve 或者 wr工teReplace 成为受保护的方法,而不是私有的方法。 如果这些方法是私有的,那么子类将会 不声不响地忽略掉这两个方法。 这正是“为了允许继承, 而把实现细节变成一个类的 API 的一部分”的另一种情形。
为了继承而设计类,对这个类会有一些实质性的限 制。
这个问题的最佳解决方案是,对于那些并非为了安全地进行子类化而设计和编写文 档的类,要禁止子类化。
专门为了继承而设计类是一件很辛苦的工作。 你必须建立文档说明其所有的自用模式,并且一旦建立了文档,在这个类的整个生命周期中都必须遵守。 如果没有做到,子类就会依赖超类的实现细节,如果超类的实现发生了变化,它就有可能遭到破坏。 为 了允许其他人能编写出高效的子类,还你必须导出一个或者多个受保护的方法。 除非知道 真正需要子类,否则最好通过将类声明为 final,或者确保没有可访问的构造器来禁止类被 继承。
Java 提供了两种机制,可以用来定义允许多个实现的类型 : 接口和抽象类。
自从 Java 8 为继承引入了缺省方法( default method),这两种机制都允许为某些实例方法提供实现。
主要的区别在于,为了实现由抽象类定义的类型,类必须成为抽象类的一个子类。 因为 Java 只允许单继承,所以用抽象类作为类型定义受到了限制。
任何定义了所有必要的方法并遵守通用约定的类,都允许实现一个接口,无论这个类是处在类层次结构中的什么位置。
骨架实现类被称为 Abs tr a c ti nterface,这里的 Interface 是指所实现的接 口的名字。 例如, Collections Framework 为每个重要的集合接口都提供了一个骨架实现,包 括 AbstractCollection 、 AbstractSet 、 AbstractList 和 AbstractMap。 将它 们称作 SkeletalCollection, SkeletalSet, SkeletalList 和 SkeletalMap 也 是有道理的,但是现在 Abstract 的用法已经根深蒂固。 如果设计得当 ,骨架实现(无论 是单独一个抽象类,还是接口中唯一包含的缺省方法) 可以使程序员非常容易地提供他们自 己的接口实现。 例如,下面是一个静态工厂方法,除 AbstractList 之外,它还包含了一个完 整的、功能全面的 List 实现:
虽然 Java 编译器允许在一个源文件中定义多个顶级类,但这么做并没有什么好处,只会带来巨大的风险。
因为在一个源文件中定义多个顶级类,可能导致给一个类提供多个定义。 哪一个定义会被用到,取决于源文件被传给编译器的顺序。
链接:https://pan.baidu.com/s/1tks8TZGeO6P1yPYmCZoUwQ
提取码:n742