《Effective Java》读书笔记 —— 类和接口

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

访问修饰符:

  • private
  • protected
  • public

顶层的(非嵌套)类和接口,两种访问级别:

  • 包级私有(package-private)
  • public

成员(域、方法、嵌套类和嵌套接口)

  • private:只有在声明该成员的顶层类内部才可以访问
  • package-private:声明该成员的包内部的任何类都可以访问,是默认访问级别
  • protected:在声明类和子类中可以访问
  • public:任何地方可以访问
规则一:尽可能使每个类或者成员不被外界访问

如果一个包级私有的顶层类只是在某一个类的内部使用,就应该考虑使它成为唯一使用它的那个类的私有嵌套类。

规则二:如果方法覆盖了超类中第一个方法,子类中的访问级别就不允许低于超类的访问级别,确保任何可使用超类的地方都可以使用子类
规则三:接口中的所有方法都必须是public
规则四:实例域不能是公有的

如果域时非final的,或者是一个指向可变对象的final引用,那么一旦使这个域称为公有,就放弃了在这个域中的值进行限制的能力,也就放弃了这个域的不可变能力

包含公有可变域的类并不是线程安全的。

规则四:静态域不要是公有的(除了暴露静态常量)

要对外暴露静态域,必须是基本类型的值,或者是不可变对象。

长度非零的数组总是可变的,所以,类具有公有的静态final数组域,或者返回这种域的方法,总是不正确的。

以下错误:

public static final Tings[] VALUES = {...};

解决方案:公有数组私有化,并增加一个公有的不可变列表

private static final Tings[] VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(VALUES));

或者:添加公有方法,返回私有数组的拷贝

private static final Tings[] VALUES = {...};
public static final Tings[] values() {
    return VALUES.clone;
}

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

公有类永远不要暴露可变的类。

3.使可变性最小化

不可变类:其实例不能被修改的类。具体来说,每个实例中包含的所有信息都必须在创建该实例的时候就提供,并在对象的整个生命周期内固定不变。

Java 平台类库中的不可变类:String、基本类型的包装类、BigInteger、BigDecimal。不可变类对应配套的可变类:StringBuilder、BitSet。本应该是不可变,但却是可变的类:Date、Point。

不可变类的优点:

  • 易于设计、实现和使用
  • 不可变对象很简单、只有一种状态,即被创建时的状态
  • 不容易出错、更加安全
  • 线程安全,不要求同步,可以被自由的共享

不可变类的缺点:

  • 在特定的情况下,存在潜在的性能问题,比如执行一个多步骤操作,每个步骤都会产生一个新的对,但除了最后的结果之外其他的对象最终都会被丢弃,就会有性能问题
  • 所以应该使一些小的值对象成为不可变的
  • 如果发生了性能问题,才应该为不可变的类提供公有的可变配套版

String对象不可变性的优缺点

  • 字符串常量池的需要.
  • 线程安全考虑
  • 类加载器要用到字符串,不可变性提供了安全性,以便正确的类被加载
  • 支持hash映射和缓存

使类成为不可变,遵循的规则:

  • 不要提供任何会修改对象状态(属性)的方法
  • 保证类不会被扩展,不会有子类,破坏该类的不可变行为
    • 如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。使所有的域都是final的。
  • 使所有的域(属性)都是final
  • 使所有的域都是private,防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象
  • 确保对于任何可变组件的互斥访问
    • 如果类具有指向可变对象的域,必须确保该类的客户端无法获得执行这些对象的引用
    • 在构造器中,永远不要用客户端提供的对象引用来初始化这样的域
    • 在访问方法中,也不要返回该对象引用
      • 普通对象,直接new 一个新的对象
      • 数组这类复杂对象,可使用 clone方法
    • 在构造器、访问方法和 readObject 方法中请使用保护性拷贝技术

通过构造器初始化所有成员,构造器初始化成员时,需要进行深浅拷贝,如果构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值

     public final class ImmutableDemo {  
         private final int[] myArray;  
         public ImmutableDemo(int[] array) {  
             this.myArray = array; // wrong  
         }  
     }
     这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。
     为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:
     public final class MyImmutableDemo {  
         private final int[] myArray;  
         public MyImmutableDemo(int[] array) {  
             this.myArray = array.clone();   
         }   
     }

不可变类的设计

对于访问方法,一般会返回一个新的实例,而不是修改这个实例,大多数不可变类使用这种模式,称为函数的做法。

不可变的类一般会提供一些静态工厂,它们把频繁被请求的实例缓存起来,使客户端之间可以共享这些实例,而不用创建新的实例,降低内存占用和垃圾回收成本。所有基本类型的包装类和BigInteger都有这样的静态工厂。

不可变对象可以被自由共享,所以根本不需要做任何拷贝,因为拷贝始终等于原始的对象,所以不需要为不可变的类提供clone方法或者拷贝构造器。

不仅可以共享不可变对象,也可以共享它们的内部信息。

不可变对象为其他对象提供了大量的构件。

有关序列化,如果让自己的不可变类实现序列化,就必须显式提供 readObject 或者 readResolve,否则反序列化可能会产生新的实例。

尽量使用不可变类,不要为每个get方法编写一个相应的set方法

举例

说明:这个类表示一个复数,加减运算都是返回一个新的实例,而不是在原来的实例上修改,称为函数的做法。

public final class Complex {
    private final double re;
    private final double im;
    
    public Complex(double re, double im) {
        this.re = re;
        this.im = im;
    }
    
    public double realPart() {
        return re;
    }
    
    public double imaginaryPart() {
        return im;
    }
    
    public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }
    
    public Complex sub(Complex c) {
        return new Complex(re - c.re, im - c.im);
    }
}

将构造函数改为私有的,并添加静态工厂来替代公有构造器

public final class Complex {
    private final double re;
    private final double im;
    
    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);
    }
    ...
}

4.复合优先于继承

与方法调用不同的是,继承打破了封装性。子类依赖于其超类中特定功能的实现细节。所以子类必须要跟着其超类的更新而演变,导致子类很脆弱。

继承

  • 继承打破了封装性
  • 父类内部细节对于子类是可见的,继承的代码复用是一种白盒式代码复用,如果基类的实现发生改变,那么派生类也将随之改变,导致子类的行为不可预知
  • 只有两者存在is-a的关系,才使用继承,如果不是,则使用组合

组合

  • 在新的类中增加一个私有域,它引用现有类的一个实例
  • 继承必须在编译器确定继承哪个类,组合可以采用面向接口编程,类的组合关系可以在运行期确定

5.要么为继承而设计,并提供文档说明,要么就禁止继承

类的文档必须精确描述覆盖每个方法所带来影响,也就是说,覆盖的方法必须说明其自用性

类必须通过某种形式提供适当的钩子(hook),以便能够进入它的内部工作流程中,这种形式可以是精心选择的受保护的方法

6.接口优于抽象类

接口优点

  • 现有的类可以很容易被更新,以实现新的接口
  • 接口是定义mixin(混合类型)的理想选择
    • 类不可能有一个以上的父类,类层次结构中也没有适当的地方插入mixin
  • 接口允许我们构造非层次接口的类型框架

接口和抽象类区别

  • 接口里不能定义静态方法;抽象类里可以定义静态方法。
  • 接口里不包含构造器,抽象类可以包含构造器。抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
  • 接口里不能包含初始化块,但抽象类可以包含初始化块。
  • 接口里不包含已经提供实现的方法,只能包含抽象方法,;抽象类则完全可以包含普通方法。
  • 接口里只能定义静态常量,不能定义其他变量。抽象类既可以定义普通变量,也可以定义静态常量。
  • 注意:在接口里定义的接口、枚举类、变量默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static对他们进行修饰,同理,在抽象类里,会默认使用public abstract修饰方法。

骨架实现类

虽然接口不允许包含默认实现,但是,可通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,但是骨架实现类接管了所有与接口实现相关的工作

在选择抽象类和接口时,并不是二选一的答案,或干脆枪毙掉抽象类。其实,你可以把接口和抽象类的优点结合起来,对于你希望导出(对外提供)的每一个重要接口都提供一个抽象类(骨架实现类)。接口的作用仍然是定义类型,骨架实现类负责所有与接口实现相关的工作

7.接口只用于定义类型

当类实现接口时,接口就充当可以引用这个类的实例的类型。类实现了接口,就表明客户端对这个类的实例实施某些动作。

接口应该只被用来定义类型,不应该被用来导出常量

接口用于导出常量

常量接口:只包含静态的final域。导出常量的一种形式。常量接口模式是对接口的不良使用

缺点:

  • 实现常量接口,会导致把这样的实现细节泄漏到该类导出的API中
  • 如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所”污染“。

常量接口的例子

public interface PhysicalConstants {
    static final double AAA = 0.1;
    static final double BBB = 0.1;
    static final double CCC = 0.1;
}

导出常量的合理方案:

  • 使用枚举类型导出
  • 使用不可实例化的工具类导出

工具类的方式:

public class PhysicalConstants {
    private PhysicalConstants() {};
    static final double AAA = 0.1;
    static final double BBB = 0.1;
    static final double CCC = 0.1;
}

8.类层次优于标签类

有时,可能遇到带有两种甚至更多风格的实例的类,并包含表示实例风格的标签域。

下面例子,此类表示圆形或者矩形

demo

标签类缺点:

  • 充斥样板代码,包括枚举声明、标签域以及条件语句
  • 破坏了可读性
  • 内存占用也增加
  • 实例承担着其他风格不相关的域

解决方案:子类化

定义一个包含抽象方法的抽象类,公共方法定义在抽象类

demo

9.用函数对象表示策略(策略模式)

函数指针(引用)的主要用途是实现策略模式,在Java中实现策略模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类,当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并且通过公有的静态final域被导出,其类型为该策略接口。

举例:比较器函数代表一种为元素排序的策略。

Java没有提供函数指针,可以用对象引用实现此功能。

比较器实例

StringLengthComparator 实例就是用于字符串长度比较的具体策略。

StringLengthComparator 是无状态的(没有域),所以单例比较合适。

class StringLengthComparator implements Comparator<String> {
    private StringLengthComparator() {};
    private static final StringLengthComparator INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

定义一个策略接口

public interface Comparator<T> {
    public int compare(T t1, T t2);
}

使用比较策略

Arrays.sort(stringArray, new Comparator(String)(){
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
})

10.优先考虑静态成员类

嵌套类:被定义在另一个类的内部的类。嵌套类存在的目的只是为了它的外围类提供服务。

嵌套类包括:

  • 静态成员类(内部类)
  • 非静态成员类(内部类)
  • 匿名类
  • 局部类

静态成员类

最简单的一种嵌套类,可看作是普通的类,可以访问外围类的所有成员,包括私有成员。

公有静态成员类常见用法,是作为公有的辅助类,仅当与它的外部类一起使用时才有意义。

私有静态成员类常见用法,用来代表外围类所代表的对象的组件。例如,Map实例,Map的内部都有一个Entry对象,对应于Map的key和value。

非静态成员类(内部类)

必须和外围类的一个实例相关联,可以用this来访问

常见用法:Adapter

如果成员类不要求访问外围实例,就要声明成静态成员类,不然每个实例都会包含一个额外的指向外围对象的引用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 类与接口是Java语言的核心,设计出更加有用、健壮和灵活的类与接口很重要。 13、使类和成员的可访问性最小化 设计...
    Alent阅读 657评论 0 2
  • 类和接口 一、使类和成员的可访问性最小化 首先我们要了解一个 软件设计基本原则:封装 模块隐藏所有的实现细节,只通...
    dooze阅读 430评论 0 0
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,296评论 18 399
  • 一:java概述:1,JDK:Java Development Kit,java的开发和运行环境,java的开发工...
    ZaneInTheSun阅读 2,582评论 0 11
  • 第2章 创建和销毁对象 第1条:考虑用静态工厂方法代替构造方法 静态工厂方法与构造方法的不同 优点: 静态工厂方法...
    QM阅读 579评论 0 51