1.创建和销毁对象
第1条 : 用静态工厂方法代替构造器
静态工厂方法的一个优点是,不像构造方法,它们是有名字的。
一个类只能有一个给定签名的构造方法。 程序员知道通过提供两个构造方法来解决这个限制,这两个构造方法的参数列表只有它们的参数类型的顺序不同。 这是一个非常糟糕的主意。 这样的 API 用户将永远不会记得哪个构造方法是哪个,最终会错误地调用。 阅读使用这些构造方法的代码的人只有在参考类文档的情况下才知道代码的作用。
静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。这种技术类似于 Flyweight 模式,如果经常请求等价对象,那么它可以极大地提高性能,特别是如果在创建它们非常昂贵的情况下。
静态工厂方法的第三个优点是,与构造方法不同,它们可以返回原类型的任何子类型的对象。 这为你在选择返回对象的类时提供了很大的灵活性。
静态工厂方法的第四个优点是,返回对象的类可以根据输入参数的不同而不同。声明的返回类型的任何子类都是允许的。 返回对象的类也可以随每次发布而不同。
静态工厂方法的第五个优点是,在编写包含该方法的类时,返回的对象的类可以不存在(即返回接口)。这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 API(JDBC),DriverManager.getConnection()。
静态工厂方法的第一个缺点是,类如果不含公有的或者受保护的构造器,就不能被子类化。
静态工厂方法的第二个缺点是,程序员很难发现他们。
下面是一些静态工厂方法的常用名称:
from——类型转换方法,它接受单个参数并返回此类型的相应实例,例如:
Date d = Date.from(instant);
of——聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:
Set faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf——from 和 to 更为烦琐的替代方式,例如:
BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:
StackWalker luke = StackWalker.getInstance(options);
create 或 newInstance——与 instance 或 getInstance 类似,但该方法保证每个调用返回一个新的实例,例如:
Object newArray = Array.newInstance(classObject, arrayLen);
getType——与 getInstance 类似,但是在工厂方法处于不同的类中使用。Type 是工厂方法返回的对象类型,例如:
FileStore fs = Files.getFileStore(path);
newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:
BuweredReader br = Files.newBuweredReader(path);
type—— getType 和 newType 简洁的替代方式,例如:
List litany = Collections.list(legacyLitany);
总之,静态工厂方法和公共构造方法都有它们的用途,我们要理解它们各自的长处。静态工厂经常更加合适,因此切忌第一反应就是提供公有的构造器、而不先考虑静态工厂。
第2条 : 遇到多个构造器参数时要考虑使用构造器
静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过 20个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品都有非零值,只有少数几个可选属性。
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了重叠(telescoping constructor)构造器模式,在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。
简而言之,可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。读者不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用 setter 方法来设置每个必需的参数和可选参数。
不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean可能处于不一致的状态,JavaBeans 模式排除了让类不可变的可能性。
幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder模式。
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在性能关键的情况下可能会出现问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,将来你可能需要添加更多的参数。如果一开始就使用构造方法或静态工厂开始,当类演化到参数数量失控的时候,过时的构造方法或静态工厂就会面临尴尬的处境。因此,所以,最好从一开始就创建一个 builder。
第3条 : 用私有构造方法或枚类强化 Singleton 属性
第一种方法中,成员是 final 修饰的属性
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton(){}
}
但是这种会受到反射机制的攻击,例如:
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clazz = Singleton.class;
Constructor<?> constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance = (Singleton) constructor.newInstance();
System.out.println(instance);
}
}
如果需要防御此攻击,请修改构造函数,使其在请求创建第二个实例时抛出异常。
第二种方法中,公共成员是一个静态的工厂方法:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance(){
return INSTANCE;
}
private Singleton(){}
}
这样做的好处是:
- 1.工厂方法的返回可以修改。
- 2.如果你的应用程序需要它,可以编写一个泛型单例工厂
- 3.可以用 supplier
为了将利用上述方法实现的Singleton类变成是可序列化的(Serializable),仅仅将 implements Serializable 添加到声明中是不够的,为了维护单例的保证,声明所有的实例属性为 transient ,并提供一个 readResolve 方法,完整代码如下
public class Singleton implements Serializable{
private transient static final long serialVersionUID = 1L;
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance(){
return INSTANCE;
}
private Singleton(){}
private Object readResolve(){
return INSTANCE;
}
}
验证代码如下:
public class Test {
public static void main(String[] args) throws Exception {
Singleton singleton = Singleton.getInstance();
ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(singleton);
ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(in);
Singleton readObject = (Singleton) ois.readObject();
System.out.println(singleton);
System.out.println(readObject);
}
}
实现一个单例的第三种方法是声明单一元素的枚举类:
public enum Singleton {
INSTANCE;
public void someMethod(){}
}
这种方式类似于公共属性方法,但更简洁,提供了免费的序列化机制,并提供了针对多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是单一元素枚举类通常是实现单例的最佳方式。注意,如果单例必须继承 Enum 以外的父类 (尽管可以声明一个 Enum 来实现接口),那么就不能使用这种方法。
第4条 : 使用私有构造方法强化不可实例化的能力
试图通过创建抽象类来强制执行非实例化是行不通的。 该类可以被子类化,子类可以被实例化,不过,有一个简单的方法来确保非实例化。只有当类不包含显式构造方法时,才会生成一个默认构造方法,因此可以通过包含一个私有构造方法来实现的非实例化。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
} ... // Remainder omitted
}
因为显式构造方法是私有的,所以在类之外是不可访问的。 AssertionError 异常不是严格要求的,但是它提供了一种保证,以防在类中意外地调用构造方法。它保证类在任何情况下都不会被实例化。这个习惯用法有点违反直觉,好像构造方法就是设计成不能调用的一样。
因此,如前面所示,添加注释是种明智的做法。这种习惯有一个副作用,阻止了类的子类化。所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访问的父类构造方法来调用。
第5条 : 优先使用依赖注入来引用资源
许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此拼写检查器类实现为静态实用工具类并不少见
// Inappropriate use of static utility - inflexible & untestable!
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // Noninstantiable
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
同样地,将它们实现为单例也并不少见
// Inappropriate use of singleton - inflexible & untestable!
public class SpellChecker {
private final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static INSTANCE = new SpellChecker(...);
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
这两种方法都不令人满意,因为他们都假定只有一本字典可用,静态工具类和Singleton类不适合于需要引用底层资源的类。
满足这一需求的简单模式是在创建新实例时将资源传递到构造方法中。这是依赖项注入(dependency injection)的一种形式。
// Dependency injection provides flexibility and testability
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
虽然我们的拼写检查器的例子只有一个资源(字典),但是依赖项注入适用于任意数量的资源,以及任意的依赖形式。依赖注入的对象资源具有不可变性,因此多个客户端可以共享依赖对象。依赖注入同样适用于构造方法,静态工厂和builder模式。
第6条 : 避免创建不必要的对象
一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个「昂贵的对象」,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:
// Performance can be greatly improved!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
这个实现的问题在于它依赖于 String.matches 方法。 虽然 String.matches 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在注重性能的情况下重复使用。 问题是它在内部为正则表达式创建一个Pattern 实例,却只使用了一次,之后就可以进行垃圾回收了。 创建 Pattern 实例是昂贵的,因为它需要
将正则表达式编译成有限状态机(finite state machine)。
为了提高性能,应该显式的将正则表达式显式编译为一个 Pattern 实例(不可变),缓存它,每当调用 isRomanNumeral 方法的时候就重用同一个实例:
// Reusing expensive object for improved performance
public class RomanNumerals {
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) {
return ROMAN.matcher(s).matches();
}
}
例如,Map 接口的 keySet 方法返回 Map 对象的 Set 视图,包含 Map 中的所有 key。乍看之下,似乎每次调用keySet 都必须创建一个新的 Set 实例,但是对给定 Map 对象的 keySet 的每次调用都返回相同的 Set 实例。 尽管返回的 Set 实例通常是可变的,但是所有返回的对象在功能上都是相同的:当其中一个返回的对象发生变化时,所有其他对象也都变化,因为它们全部由相同的 Map 实例支持。 虽然创建 keySet 视图对象的多个实例基本上是无害的,但这是没有必要的,也没有任何好处。
另一种创建多余对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱使得基本类型和装箱类型自己的差别变得模糊不清,但不会消除它们之间的区别。考虑下面的方法:
// Hideously slow! Can you spot the object creation?
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i; //等于Long sum = Long.valueOf(sum.longValue()+i);
return sum;
}
结论很明显:优先使用基本类型而不是装箱的基本类型,避免无意识的自动装箱。
不要错误的认为本条目所介绍的内容暗示着“创建对象的代价非常昂贵,应该尽量避免创建对象”。 相反,由于小对象的构造方法只会做很少的显示工作,所以使用构造方法创建和回收小的对象是非常廉价,尤其是在现代 JVM 实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性,通常是件好事。
相反,通过维护自己的对象池来避免对象创建是一个坏主意,除非对象池中中的对象是非常重量级的。对象池的典型例
子就是数据库连接。建立连接的成本非常高,因此重用这些对象是有意义的。但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代 JVM 实现具有高度优化的垃圾收集器,它们在轻量级对象上轻松胜过此类对象池。
第7条 : 消除过期的对象引用
考虑以下简单的堆栈实现:
//Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集(因为栈中还存着对象),即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是很隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 Stack 类的情景下,只要从栈中弹出,元素的引用就设置为过期。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
清空对象引用应该是例外而不是规范。消除过期引用的最好方法是让包含引用的变量结束其生命周期。如果在最近的作用域范围内定义每个变量,这种自然就会出现这种情况。
那么什么时候应该清空一个引用呢? Stack 类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池(storage pool)由 elements 数组的元素组成 (对象引用单元,而不是对象本身)。数组中活动部分的元素 (如前面定义的) 是已分配的(allocated),而数组其他部分的元素则是自由的(free)。但是垃圾收集器并不知道这一点;对于垃圾收集器而言,elements 数组中的所有对象引用都同样有效。
一般来说,当一个类自己管理内存时,程序员应该警惕内存泄漏问题。 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
内存泄漏的另一个常见的来源是缓存。 一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它不再使用之后,仍然保留在缓存中。可以用 WeakHashMap 来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,WeakHashMap 才有用。
第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个 API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在 WeakHashMap 的键(key)中。
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
第8条 : 避免使用 Finalizer 和 Cleaner 机制
第9条 :使用 try-with-resources 语句替代 try-finally 语句
Java 类库中包含许多必须通过调用 close 方法手动关闭的资源。 比如 InputStream , OutputStream 和
java.sql.Connection 。try-finally 语句在多个需要关闭资源的操作时并不能很有效率的捕捉异常。
当使用try-with-resources 语句时资源必须实现 AutoCloseable 接口,以下是我们的第一个使用 try-with-resources 的示例:
// try-with-resources on multiple resources - short and sweet
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
使用 try-with-resources 语句替代 try-finally 语句。 生成的代码更简洁,更清晰,并且生成的异常更有用。
第10条 :重写 equals 方法时遵守通用约定
重写 equals 方法看起来很简单,但是有很多方式会导致重写出错,避免此问题的最简单方法是不覆盖 equals 方法,在这种情况下,类的每个实例只与自身相等。如果满足以下任一下条件,则说明不应该覆盖equals方法:
- 每个类的实例都是固有唯一的。 如Thread。
- 类不需要提供一个“逻辑相等(logical equality)”的测试功能。
- 父类已经重写了 equals 方法,则父类行为完全适合于该子类。
- 类是私有的或包级私有的,可以确定它的 equals 方法永远不会被调用。如果你非常厌恶风险,可以重写 equals
方法,以确保不会被意外调用:
@Override
public boolean equals(Object o) {
throw new AssertionError(); // Method is never called
}
当你重写 equals 方法时,必须遵守它的通用约定。Object 的规范如下: equals 方法实现了一个等价关系
(equivalence relation)。它有以下这些属性:
- 自反性: 对于任何非空引用 x, x.equals(x) 必须返回 true。
- 对称性: 对于任何非空引用 x 和 y,如果且仅当 y.equals(x) 返回 true 时 x.equals(y) 必须返回 true。
- 传递性: 对于任何非空引用 x、y、z,如果 x.equals(y) 返回 true, y.equals(z) 返回 true,则x.equals(z) 必须返回 true。
- 一致性: 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 x.equals(y) 的多次调用
- 必须始终返回 true 或始终返回 false。
- 对于任何非空引用 x, x.equals(null) 必须返回 false。
综合起来,以下是编写高质量 equals 方法的配方(recipe):
- 1. 使用 == 运算符检查参数是否为该对象的引用
- 2. 使用 instanceof 运算符来检查参数是否具有正确的类型。
- 3. 参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功
- 4. 对于类中的每个“重要”的属性,请检查该参数属性是否与该对象对应的属性相匹配。
当你完成编写完 equals 方法时,问你自己三个问题:它是对称的吗?它是传递吗?它是一致的吗?除此而外,编写
单元测试加以排查
以下是一些最后提醒:
- 1. 当重写 equals 方法时,同时也要重写 hashCode 方法
- 2. 不要让 equals 方法试图太聪明。
- 3. 在 equal 时方法声明中,不要将参数 Object 替换成其他类型。
第11条 :覆盖 equals 时总要覆盖hashCode
第12条 :始终要覆盖toString
每个实例化的类中重写 Object 的 toString 实现。 它使得类更加舒适地使用和协助调试。 toString 方法应该以一种美观的格式返回对象的简明有用的描述。
第13条 :谨慎地重写 clone 方法
如果对象包含引用可变对象的属性,则前面显示的简单 clone 实现可能是灾难性的。
第14条 :考虑实现 Comparable 接口
第15条 :使类和成员的可访问性最小化
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,组件将其内部数据和其他组件的其他实现细节隐藏起来。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则。
信息隐藏很重要有很多原因,其中大部分来源于它将组成系统的组件分离开来,允许它们被独立地开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以并行开发。它减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件。虽然信息隐藏本身并不会导致良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题,则可以优化这些组件,而不会影响别人的正确的组件。
经验法则很简单:尽可能的让每个类或成员不被外界访问。
总之,公共类不应该暴露可变属性。 公共类暴露不可变属性的危害虽然仍然存在问题,但其危害较小。 然而,
有时需要包级私有或私有内部类来暴露属性,无论此类的域是否是可变的。
第16条 :使可变性最小化
不可变的类是指其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供、并在对象的整个生命周期内固定不变。java里面有许多不可变的类,包括String、基本类型的包装类、BigInteger、BigDecimal等,不可变的类比可变的类更加易于设计、实现和使用。
为了使类成为不可变、要遵循下面五条规则:
- 不要提供修改对象状态的方法(也称为 set方法)。
- 确保这个类不能被继承。
- 把所有属性设置为 final。
- 把所有的属性设置为 private
- 确保对任何可变组件的互斥访问。 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。
不可变对象的优点:
- 不可变对象本质上是线程安全的,它们不要求同步。不可变对象可以被线程自由的共享。
- 不可变对象无偿提供了失败的原子性
缺点:
- 对于每个不同的值都需要一个单独的对象
总之,坚决不要为每个get方法编写一个相应的set方法。除非有很好的理由要让类成为可变的类,否则它就应该是不可变的。
对于某些类而言,其不可变性是不切实际的。如果类不能被做成不可变的,仍然应该尽可能地限制它的可变性。
除非有令人信服的理由要使域变成非final的,否则要使每个域都是private final 的。
构造器应该创建完全初始化的对象,并建立起所有的约束关系。不要在构造器或者静态工厂之外再提供公有的初始化方法。
第17条 :接口优于抽象类
Java 有两种机制来定义允许多个实现的类型:接口和抽象类。 由于在 Java 8 [JLS 9.4.3] 中引入了接口的默认方法(default methods ),因此这两种机制都允许为某些实例方法提供实现。 一个主要的区别是要实现由抽象类定义的类型,类必须是抽象类的子类。 因为 Java 只允许单一继承,所以对抽象类的这种限制严格限制了它们作为类型定义的使用。
接口的优势:
- 现有的类可以很容易的被更新,以实现新的接口
- 接口是定义mixin(混合类型)的理想选择、如Comparable
- 接口允许构造非层次结构的类型框架
接口的劣势:
- 接口提供的默认实现方法有限制
- 接口不能包含实例属性
- 接口不能包含私有静态方法(jdk9中可以)
但是,你可以通过提供一个抽象的骨架实现类(abstract skeletal implementation class)来与接口一起使用,将接口和抽象类的优点结合起来。 接口定义了类型,可能提供了一些默认的方法,而骨架负责实现除基本类型接口方法之外的方法。 这就是模板方法设计模式。
总而言之,一个接口通常是定义允许多个实现的类型的最佳方式。 如果你导出一个重要的接口,应该强烈考提供一个骨架的实现类。 在可能的情况下,应该通过接口上的默认方法提供骨架实现,以便接口的所有实现者都可以使用它。 也就是说,对接口的限制通常要求骨架实现类采用抽象类的形式。