(尊重劳动成果,转载请注明出处:https://blog.csdn.net/qq_25827845/article/details/85016496冷血之心的博客)
系列文章:
Effective Java经典学习(一)
目录
第二章:创建和销毁对象
(1)使用静态工厂方法代替构造器
(2)遇到多个构造器参数时要考虑用构建器
(3)用私有构造器或者枚举类型强化Singleton属性
(4)通过私有构造器强化不可实例化的能力
(5)避免创建不必要的对象
(6)消除过期的对象引用
(7)避免使用终结方法
第三章:对于所有对象都通用的方法
(8)覆盖equals时请遵守通用约定
(9)覆盖equals时总要覆盖hashCode
(10)始终要覆盖toString方法
(11)谨慎地覆盖clone
(12)考虑实现Comparable接口
第四章:类和接口
(13)使类和成员的可访问性最小化
(14)在公有类中使用访问方法而不是公有域
(15)使可变性最小化
(16)复合优先于继承
(17)要么为继承而设计,并提供文档说明,要么就禁止继承
(18)接口优先于抽象类
(19)接口只用于定义类型
(20)类层次优先于标签类
(21)用函数对象表示策略
(22)优先考虑静态成员类
总结
博主最近在研究《Effective Java》这本书,也学习了一些新的知识点。下边我们一起来学习总结该书第2,3,4章节中所讲解的案例。
一个类可以提供一个公有的构造器来让客户端获取其实例,但是我们更应该考虑使用静态工厂方法来代替公有构造器。
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
静态工厂方法创建实例的常用方法名:valueOf,of,genInstance,newInstance,getType,newType
相信在我们的日常开发中,肯定遇到过一个场景,那就是一个类的构造器中有多个参数。有些参数是必选的,其余则是可选的。这种情况下,我们一般使用重叠构造器模式:即创建了若干个构造器,构造器之间进行相互调用。
重叠构造器模式是我们最常见的一种写法,但是我们应该考虑builder模式,即:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器,得到一个builder对象,然后客户都在builder对象上调用类似于setter的方法,来设置每个可选的参数。
这一条主要讲述了如何更加优雅高效的实现单例模式,那就是使用枚举,你需要编写一个包含单个元素的枚举类型。
比如一些工具类,我们提供了静态方法,不希望这个类被私有化。很简单,我们可以将构造器私有化。产生的副作用:该类不可以被子类化。因为所有的构造器都必须显示或者隐示的调用超类构造器。
先来看一个极端的方面例子,该语句每次被执行都创建了一个新的String实例。
String s = new String("stringette");
改进后如下:
String s = "stringette";
再来看一个关于创建无用对象所带来的问题。我们都知道自动装箱/拆箱,这个功能使得基本类型和装箱基本类型之间的差别变得模糊起来。但是,还是有差别的,案例如下:
package com.pak8;
public class AutoBoxTest {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
Long sum = 0L;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum +=i;
}
System.out.println("时间差1:"+(System.currentTimeMillis()-startTime));
long startTime2 = System.currentTimeMillis();
long sum2 = 0;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum2 +=i;
}
System.out.println("时间差2:"+(System.currentTimeMillis()-startTime2));
}
}
运行结果如下:
在上边的计算中,我们先定义了Long sum = 0L 导致,程序构造了大约2的31次方个多余的Long实例。当我们定义了long sum = 0L之后,减少了无用对象的创建,计算时间从13205ms减少到了1263ms。
结论:优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
这里并不是告诉大家创建对象的代价很昂贵,相反,由于小对象的构造器只做很少量的显示工作。所以小对象的创建和回收动作是非常廉价的。通过创建附加的对象,提升程序的清晰性,简洁性和功能性,这通常是件好事。
我们先来看一个简单的自定义栈的实现:
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);
}
}
}
实际上,这段程序中并没有很明显的错误。无论如何测试,它都会成功地运行通过每一项测试,但这个程序中隐藏着一个问题。不严格地讲,这段程序有一个”内存泄漏“, 随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄露会导致磁盘交换,甚至程序失败,但这种情况比较少见。
在我们的stack例子中,凡是在elements数组的”活动范围“之外的任何引用都是过期的,这里的活动部分指的是elements中下标小于size的那些元素。内存泄漏发生在pop方法中。如何解决stack中的过期引用问题?一旦对象引用已经过期,我们只需清空这些引用即可。
public Object pop(){
if (size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
//清空引用
elements[size] = null;
return result;
}
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能等问题。Java语言规范不仅不保证终结方法会被及时执行,而且根本就不保证它们会被执行
我们什么使用需要覆盖Object.equals呢?如果类具有自己特有的“逻辑相等”概念,而且超类还没有覆盖equals以实现期望的行为,这时我们就需要覆盖equals方法。但是必须遵守通用的约定:
覆盖equals时总要覆盖hashCode,也就是相等的对象必须要有相等的散列码(hash code)这样该类才可以结合所有给予散列的集合(Hashmap,HashSet和Hashtable)一起正常工作。
在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。
克隆对象,谨慎覆盖。
一个模块不需要知道其它模块内部的工作情况,叫做信息的隐藏或者封装。封装也是面向对象的三大特性之一。对于成员(域,方法,嵌套类或者嵌套接口)有四种可能的访问级别,下面按照可访问性的递增顺序罗列出来:
结论:尽可能的降低可访问性,公有类不应该包含公有域
比如一个实体类,下边的定义导致该类的数据域被直接访问,没有提供封装的功能。
class People{
public static String name;
public static int age;
}
封装之后,我们不直接对外暴露数据域,而是提供一个公有的访问方法。
class People {
private static String name;
private static int age;
public People(String name, int age) {
this.name = name;
this.age = age;
}
public static String getName() {
return name;
}
public static void setName(String name) {
People.name = name;
}
public static int getAge() {
return age;
}
public static void setAge(int age) {
People.age = age;
}
}
不可变类只是其实例不可以被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。不可变类必须遵循的五条规则如下:
不可变对象(比如说String)本质上是线程安全的,不要求同步。不可变对象可以被自由的共享。
总之,坚决不要为每一个get方法编写一个相应的set方法,除非你有很好的理由要让类成为可变的类。(第14条中就是同时提供了set和get方法)
面向对象的三大特性:封装,继承与多态。但是,在扩展一个类的功能时,我们的第一选择并不是继承而是复合。跨越包的继承是一种非常危险的行为,继承打破了封装性。子类依赖于其超类中特定功能的实现细节,超类的实现可能会随着发行版本的不同而有所变化,子类可能会遭到破坏。
案例如下,我们创建一个自定义的HashSet,并且统计一共被添加过多少个元素。
package com.pak10;
import java.util.*;
public class Main {
public static void main(String[] args) {
MyHashSet set = new MyHashSet<>();
set.addAll(Arrays.asList("lisi", "zhangsan", "wangliu"));
System.out.println(set.getCount());
}
}
class MyHashSet extends HashSet {
private int count = 0;
public MyHashSet() {
}
public MyHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
public boolean add(E e) {
count++;
return super.add(e);
}
public boolean addAll(Collection extends E> c) {
count += c.size();
return super.addAll(c);
}
public int getCount() {
return count;
}
}
输出结果如下:
由于子类覆盖了add和addAll方法,我们期待返回的是3,结果却是6,该程序没有正常执行。因为addAll方法内部调用了add方法,也就是我们的每一个元素都被添加计数了两次。我们应该去掉addAll方法。
package com.pak10;
import java.util.*;
public class Main {
public static void main(String[] args) {
MyHashSet set = new MyHashSet<>();
set.addAll(Arrays.asList("lisi", "zhangsan", "wangliu"));
System.out.println(set.getCount());
}
}
class MyHashSet extends HashSet {
private int count = 0;
public MyHashSet() {
}
public MyHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
public boolean add(E e) {
count++;
return super.add(e);
}
public int getCount() {
return count;
}
}
结果如下:
说完了继承的缺点,我们再来看看何为复合?
复合就是在新的类中增加一个私有域,它可以引用现有类的一个实例,现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法。这样得到的新类,即使现有类中增加了新的方法也不会影响到新的类。
文档中必须明确说明每个方法如果被覆盖会产生哪些影响。好的API文档应该描述一个给定的方法做了说明工作,而不是描述它是如何做到的。
为了允许继承,必须遵循的一个约束:构造器决不能调用可被覆盖的方法。
import java.util.Date;
public class Sub extends Super {
private Date date;
Sub() {
date = new Date();
}
@Override
public void overrideMe() {
System.out.println(date);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
class Super {
public Super() {
overrideMe();
}
// 父类中的普通方法
public void overrideMe() {
}
}
结果如下:
我们期望该程序可以打印出两次时间,但是第一个打印的却是null 因为第一次执行父类的构造器,子类的date还没有被初始化。
接口一旦被公开发行,并且已经被官方实现,再想改变这个接口几乎是不可能的。接口通常是定义允许多个实现的类型的最佳途径。但是,当演变的容易醒比灵活性和功能更加重要的时候,我们应该使用抽象类定义类型。
接口不应该用来导出常量。
多定义一些子类,而不是定义一些既可以表示圆又可以表示矩形的标签类。
函数指针的主要用途就是实现策略模式。
嵌套类:
指被定义在另一个类的内部的类。嵌套类存在的目的应该只是为他的外围类(enclosing class)提供服务。如果嵌套类将来可以回用于其他的某个环境中,他就应该是顶层类(top-level class)。
嵌套类分为四种:
内部类:非静态成员类(nonstatic member class)、匿名类(anonymous class)和局部类(local class)。
静态成员类:一般情况下做为外围类的公有辅助类,与外围类一起使用时才有意义。
public class StaticClassTest {
// 定义了一个内部的静态枚举类
public static enum Code{
Success(0),
Failed(1);
private int value;
Code(int value) {
this.value = value;
}
}
}
非静态成员类:
从语法上讲,静态成员类和非静态成员类之间唯一的区别是:静态成员类的声明中包含修饰符static。尽管他们语法非常相似,但是这两种嵌套类有很大的不同。
非静态成员类的每个实例都隐含着与外围类的一个外围实例(enclosing instance)想关联。在非静态成员类的实例方法内部,可以调用外围实例上的方法,或者利用修饰过的this构造获得外围实例的引用[JLS, 15.8.4]。如果嵌套类的实例可以在他外围实例的情况下,要想创建非静态成原来的实例是不可能的。
匿名类:
不同于Java程序设计语言中的其他任何语法单元。正如你所想象的,匿名类没有名字。它不是外围类的一个成员。它并不与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员。
局部类:
是四种嵌套类中用的最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,只有当局部类实在非静态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须简短以便不会影响到可读性。
小结:
自此,完成了Effective Java这本名著的前四个章节的简单学习和总结。虽然只是蜻蜓点水,但是我依然收获了很多,毕竟这只是一个开始,接下来我会继续认真研读该书,致力于写出高效而又优雅的代码。
如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,我会持续更新后续学习笔记,如果有什么问题,可以进群824733818一起交流学习哦~