zoukankan      html  css  js  c++  java
  • 《Effective Java》笔记 4~5

    4. 类和接口

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

    把API与实现清晰地隔离开,组件间通过API进行通信,不需要知道其他模块的内部工作情况,这称为:实现信息隐藏或封装
    解耦系统中的各个组件
    尽可能地使每个类或者成员不被外界访问

    成员(域、方法、嵌套类、嵌套接口)的四种可能访问级别:

    • 私有(private)
    • 包级私有(package-private)
    • 受保护(protected) 被称为“缺省” default
    • 公有(public)

    如果一个缺省的顶级类或接口只在某一类内部用到,就应该考虑使它成为哪个类的私有嵌套类
    子类覆盖父类方法,访问级别不能低于父类

    公有类的实例域绝对不能是公有的,如果是final或者可变对象的final引用,公有就等于放弃了对存储在这个域中值的限制,除非是是为了暴露静态final常量
    注意!
    常见问题:长度非零的数组总是可变的,用静态final数组域存储也是错误的,解决:

    1. 使用Collections.unmodifiableList() 返回不可变列表
    2. 用clone() 返回私有数组的拷贝

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

    如果类可以在它所在的包之外访问,就提供方法而非暴露数据域,以保留将来改变类内部表示法的灵活性
    如果是缺省或私有的嵌套类,直接暴露数据域就没有本质的错误
    Java类库反例:java.awt的Point类和Dimension类

    17. 使可变性最小化

    Java类库不可变类:String、基本类型包装类、BigInteger、BigDecimal

    不可变类要遵守规则:

    1. 不提供任何会修改对象状态的方法
    2. 保证类不会被拓展 (final class)
    3. 申明所有的域都是final
    4. 申明所有的域都是私有的
    5. 确保对于任何可变组件的互斥访问
      如果该不可变类具有指向可变对象的域,要确保该类的客户端无法获得指向这些对象的引用,并且不要用客户端提供的对象引用来初始化这样的域,也不要从任何访问方法中返回该对象引用

    方法结果返回新的实例而不是修改这个实例:这称为函数方法(与之对应的是:过程或命令式方法)
    这些 函数方法 方法命名使用介词而非动词,强调不会改变对象值

    不可变对象本质上是线程安全的,不要求同步,不可变对象可以自由共享,鼓励尽可能重复用现有的实例:为频繁用到的值,提供公有静态final常量
    可以提供静态工厂:把频繁请求的实例缓存起来,用静态工厂提供代替公有构造器可以让以后有添加缓存的灵活性,而不影响客户端

    不需要为不可变类提供clone方法或拷贝构造器,Java类库反例:String有拷贝构造器(应尽量少用)
    不可变类可以共享内部信息,例如:BigInterger negate方法
    不可变类为其他对象提供大量构件:Map Set
    不可变类无偿提供失败的原子性,不存在临时不一致的可能性

    缺点:每个不同的值都需要一个单独对象,创建大量对象会影响性能,解决:

    1. 用基本类型代替其中一些创建大量对象的多步骤操作
    2. 提供可变配套类:例如 String 对应的 StringBuilder

    注意:BigInteger、BigDecimal 最初对于不可变类必须为final没有广泛理解,导致如果编写的一个类安全性依赖于不可信客户端的BigInteger或者BigDecimal参数的不可变性,必须检查是否为真实的而非不可信任子类实例,进行保护性拷贝

    注意如果让自己的不可实现类实现Serializable接口,要提供显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshared和ObjectOutputStream.readUnshared方法

    Java类库反例:Date、Point 本来应该是不可变类

    构造器应该创建完全初始化的对象,并建立起所有的约束关系

    18. 复合优先于继承 (此条不适用于接口继承)

    包的内部使用继承非常安全,但是普通的具体类进行跨包的继承非常危险
    与方法调用不同的是,继承打破了封装性:子类依赖于其父类中特定功能的实现细节,父类的实现可能随着版本的不同而变化,子类会遭到破坏

    父类方法:自用性是实现细节,不是承诺
    非重写父类方法也有风险:父类后续的发行版可能与你的子类提供签名相同返回值不同或者是签名和返回值都相同的方法

    复合(composition):不依赖父类的实现细节,不拓展现有的类,而实在新的类中增加一个私有域,它引用现有类的实例
    实现包括两部分:转发类(包含了所有转发方法),以及类本身

    public class ForwardingSet<E> implements Set<E> {
        private final Set<E> s;
    
        public ForwardingSet(Set<E> 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);
        }
    
        @Override
        public Iterator<E> iterator() {
            return s.iterator();
        }
    
        @Override
        public Object[] toArray() {
            return s.toArray();
        }
    
        @Override
        public <T> T[] toArray(T[] a) {
            return s.toArray(a);
        }
    
        @Override
        public boolean add(E e) {
            return s.add(e);
        }
    
        @Override
        public boolean remove(Object o) {
            return s.remove(o);
        }
    
        @Override
        public boolean containsAll(Collection<?> c) {
            return s.containsAll(c);
        }
    
        @Override
        public boolean addAll(Collection<? extends E> c) {
            return s.addAll(c);
        }
    
        @Override
        public boolean retainAll(Collection<?> c) {
            return s.retainAll(c);
        }
    
        @Override
        public boolean removeAll(Collection<?> c) {
            return s.removeAll(c);
        }
    
        @Override
        public void clear() {
            s.clear();
        }
    }
    
    public class InstrumentedHashSet<E> extends ForwardingSet<E> {
    
        private int addCount = 0;
    
        public InstrumentedHashSet(Set<E> 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 static void main(String[] args) {
            InstrumentedHashSet<String> strings = new InstrumentedHashSet<>(new TreeSet<>());
            strings.addAll(Arrays.asList("1", "2", "3"));
            System.out.println(strings.addCount); // 3
    
        }
    }
    

    InstrumentedHashSet实例把另一个Set实例包装起来了,所以 InstrumentedHashSet类被成为包装类,这就是:Decorator(修饰者)模式
    注意!包装类不适合用于回调框架:回调框架中,对象把自生的引用传递给其他对象用于后续回调,包装类不知道自己是一个包装类,传递了指向自生(this)的引用,回调时避开了外面的包装对象(SELF问题)

    只有当子类真正是父类的子类型才适合用继承(is-a关系)
    Java类库反例:Stack继承Vector Properties继承Hashtable
    暴露内部实现细节,客户端可以直接访问内部细节,直接修改父类,从而破坏子类约束

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

    对于专门为了继承而设计的类,必须精确地描述覆盖每个方法所带来的影响
    关于程序文档的一句格言:好的API文档应该描述一个给定的方法做了什么工作,而不是描述他是如何做到的
    为了继承的类必须精心挑选受保护(protected)的方法的形式,提供适当的钩子(hook),以便进入其内部工作

    允许继承的类的构造器不能直接或间接地调用可能被覆盖的方法,因为父类构造器在子类构造器之前运行,子类中覆盖的方法会在子类构造器之前运行
    而通过构造器调用私有方法、final方法、静态是安全的,因为它们都是不可被覆盖的方法
    在为了继承而设计的类中实现Cloneable和Serializable接口,要注意clone和readObject方法,类似于构造器,不能直接或间接调用可覆盖方法

    所以对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化

    1. final修饰类
    2. 私有或包级私有构造器,并增加公有静态工厂

    20. 接口优于抽象类

    Java提供两种机制可以用来定义允许多个实现的类型:接口和抽象类
    Java 8 为继承引入了缺省方法
    一般来说,无法更新现有的类来拓展新的抽象类,如过希望两个类拓展一个抽象类,就必须把抽象类放到类型层次的高度,使其成为两个类的祖先

    接口的定义 mixin(混合类型)的理想选择,接口允许构造非层次结构的类型框架

    接口使得安全地增强类的功能成为可能,如果是抽象类,只能通过继承来增加功能

    接口可以使用缺省方法,但是不允许给Object方法(equals hashCode) 提供缺省方法,而且接口中不允许包含实例域或者非公有的静态成员(私有的静态方法除外)

    可以通过接口提供一个抽象的骨架实现类,把接口和抽象类的有点结合,接口负责定义类型,还可以提供缺省方法,骨架类实现非基本类型接口方法,这就是模板方法模式
    实现这个接口的类,可以把对于接口方法的调用转发到内部私有类的实例上,内部私有类拓展了骨架实现类,别称为:模拟多重继承
    对于骨架实现类,好的文档非常必要

    21. 为后代设计接口

    Java 8 增加了缺省方法,目的是允许给现有接口添加方法,Java 8 在核心类库增加了许多新的缺省方法,为了便于使用Lambda
    应尽量避免这种用法,缺省的方法实现可能会破坏现有的接口实现

    22. 接口只用于定义类型

    常量接口是反例,如果实现类以后不需要这些常量了,依然需要实现这个接口,以确保二进制兼容,非final类实现了常量接口,它的所有名字的命名空间会被这些常量“污染”
    解决方案:

    1. 添加常量在与之紧密相关的类或接口上
    2. 这些常量最好被看作枚举类成员就用枚举
    3. 不可实例化的工具类(private 构造器)

    Tips:Java 7 开始数字的字面量中可以使用下划线区分
    可以使用静态导入:import static 全限定类名.值

    23. 类层次优于标签类

    类层次例子:

    public abstract class Figure {
        abstract double area();
    }
    
    class Circle extends Figure {
        final double radius;
    
        public Circle(double radius) {
            this.radius = radius;
        }
    
        @Override
        double area() {
            return Math.PI * (radius * radius);
        }
    }
    
    class Rectangle extends Figure {
        final double length;
        final double width;
    
        public Rectangle(double length, double width) {
            this.length = length;
            this.width = width;
        }
    
        @Override
        double area() {
            return length * width;
        }
    }
    

    多种风格的实例使用标签类的问题:过于冗长、容易出错、效率低下
    使用类层次:为标签类中的每个方法都定义一个包含抽象方法的抽象类,所有的方法都用到了某些数据域就放到这个抽象类;为每种原始标签类都定义根类的具体子类

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

    嵌套类(4种):在另一个类内部的类,目的是为了外围类提供服务

    1. 静态成员类:看作普通类,可以访问外围类所有成员,它是外围类的一个静态成员;常见用法:作为公有的辅助类。私有静态成员:常见用法:外围类所代表对象的组件,比如Map的Entry对象,它不需要访问Map,使用非静态成员类很浪费
    2. 非静态成员类:它的每个实例都隐含地与外围类的一个外围实例相关联;常见用法:定义Adapter,它允许外部类的实例被看作是另一个不相关类的实例,例如:Map的keySet、entrySet、values以及Set和List的迭代器
    3. 匿名内部类:出现在非静态环境中才有外围实例,即使是在静态环境中,也不能拥有任何静态成员,拥有的常数变量是final基本类型或者被初始化成常量表达式的字符串域;还有许多限制:无法实例化、不能instanceof、无法实现多个接口或拓展类,除了从父类继承,无法调用任何成员;常见用法:静态工厂方法的内部
    4. 局部类:出现在可以声明局部变量的地方,有名字可重复利用、在非静态环境下定义才有外围实例、不能包含静态成员

    除了静态成员类,其他3个都称为内部类
    嵌套类中只有静态成员类可以在它的外围类之外独立存在
    如果声明的成员类不要求访问外围实例,那就把它定义成静态成员类,而不是非静态成员类,这样可避免每个实例都包含一个额外指向外围对象的引用,这可能造成内存泄漏

    25. 限制源文件为单个顶级类

    一个源文件只定义多个顶级类,可能导致给一个类提供多个定义,具体使用哪个定义取决于源文件被传递给编译器的顺序
    一个源文件放多个顶级类,考虑使用静态成员类

    5. 泛型

    26. 请不要使用原生态类型

    每一种泛型都定义一个原生态类型(raw type):不带任何实际类型参数的泛型,主要为了兼容之前代码(移植兼容性)
    List 对应的就是 List
    List 逃避了泛型检查,而List明确告诉编译器可以持有任何类型对象
    泛型子类型化规则:List 可以传递给List,但是不能传递给List

    不确定或者不在乎集合的元素类型,可以使用无限制的通配符类型,来代替原生态类型,例如:Set<?>,但是不能将任何除了null以外的元素放入到其中

    必须用原生类型的情况:

    1. 类文字:List.class、String[].class、int.class
    2. instanceof,泛型可以在运行时被擦除,在参数化类型而非无限制通配符类型上使用instanceof操作符是非法的,下面为正确做法:
    if(o instanceof Set){
        Set<?>S=(Set<?>) o;
    }
    

    27. 消除非受检的警告

    尽可能消除每一条非受检警告,这意味着不会再运行时出现ClassCastException(这是RuntimeException);
    禁用警告:@SuppressWarnings("unchecked"),应该在尽可能小的范围内使用,在局部变量上声明

    28. 列表优于数组

    数组是协变(covariant),例子:Sub为Super的子类型,Sub[] 就是Super[]的子类型
    泛型是不变的(invariant),对于两个不同类型Type1,Type2,List 和List 之间既不是子类型关系,也不是超类型关系
    泛型只在编译时强化他们的类型信息,并在运行时丢弃(或者擦除)他的元素类型信息
    泛型和数组不能很好地混合使用,泛型数组、参数化类型或者类型参数的数组都是非法的,非法的例子:List[]、new List[]和new E[]
    消除未受检转换警告,可以使用列表代替数组,虽然速度可能会慢一点,但是更安全

    29. 优先考虑泛型

    编写泛型类,例子:Stack泛型类
    使用Object[] 存储数据,将原来方法入参或者返回类型改为 E,会出现无法创建泛型数组的问题,两种解决方案:

    1. 使用E[] elements存储数据,实例化Object数组,使用 (E[]) 转换为泛型,编译器提示警告,实践中优先使用,但它会导致堆污染
    2. 使用Object[] elements存储数据,在需要元素时使用 (E) 转换为泛型

    类型参数没有限制,例子:Stack、Stack<int[]>、Stack<List>,但是不能用基本类型

    30. 优先考虑泛型方法

    在方法的修饰符和其返回值之间生命类型参数的类型参数列表

    public static <E> Set<E> union(Set<E> s1, Set<E> s2);
    

    TODO p107 泛型单例工厂

    31. 利用有限制通配符来提升API的灵活性

    参数化类型时不变的(invariant)
    特殊的参数化类型:有限制的通配符类型(bounded wildcard type)

    // 将一系列元素放入堆栈中
    // 可以放 E 及其子类
    public void pushAll(Iterable<? extends E> src);
    
    // 将所有元素弹出到传入容器中
    // 这个容器应该是 E 及其父类
    public void popAll(Collection<? super E> dst);
    

    PECS:producer-extends,consumer-super
    生产者T就用:<? extends T>
    消费者E就用:<? super E>

    注意!返回值不要用通配符类型
    Comparable<? super T> 优先于 Comparable,类似的Comparator<? super T> 优先于 Comparator

    两种静态方法声明:

    // 1. 无限制的类型参数 
    public static <E> void swap(List<E> list,int i,int j);
    // 2. 无限制的通配符
    public static void swap(List<?> list,int i,int j);
    

    方式2存在问题:无法把除了null意外的元素放入到List<?>中,解决:编写私有辅助方法来捕捉通配符类型

        public static void swap(List<?> list, int i, int j) {
            // 报错 List<?> 不允许放除了null以外的值
            swapHelper(list, i, j);
        }
    
        private static <E> void swapHelper(List<E> list, int i, int j) {
            list.set(i, list.set(i, list.get(j)));
        }
    

    32. 谨慎并用泛型和可变参数

    调用一个可变参数方法,会创建一个数组来存放可变类型,可变类型有泛型或者参数化类型时,编译警告信息会产生混乱
    当一个参数化类型的变量指向一个不是该类型的对象时,会产生堆污染,导致编辑器的自动生成类型转换失败
    将值保存在泛型可变参数数组中是不安全的,如果只是从中读取它是安全的
    Java 7 增加@SafeVarargs注解,禁用泛型可变参数警告,只在静态方法和final实例方法中合法,Java 9 在私有实例方法也合法

    泛型可变参数在都满足下列条件下是安全的:

    1. 它没有在可变数组中保存任何值
    2. 它没有对不被信任的代码开放该数组(或者其克隆程序)

    可以使用List结合静态工厂List.of 代替可变参数(伪数组),缺点是:代码略繁琐,运行慢一点

    33. 优先考虑类型安全的异构容器

    TODO p119

        private Map<Class<?>, Object> favorites = new HashMap<>();
    
        public <T> void putFavorite(Class<T> type, T instance) {
            // 使用动态转换,确保不会违背类型约束
            favorites.put(type,type.cast(instance) );
        }
    
        public <T> T getFavorite(Class<T> type) {
            return type.cast(favorites.get(type));
        }
    
        public static void main(String[] args) {
            Favorites f = new Favorites();
            f.putFavorite(String.class, "Java");
            f.putFavorite(Integer.class, 0xcafebabe);
            f.putFavorite(Class.class, Favorites.class);
    
            String favoriteString = f.getFavorite(String.class);
            Integer favoriteInteger = f.getFavorite(Integer.class);
            Class<?> favoriteClass = f.getFavorite(Class.class);
            
            System.out.printf("%s %x %s %n",favoriteString,favoriteInteger,favoriteClass);
        }
    

    注意 Map<Class<?>, Object> 这里的无限制通配符 并非只能放null

  • 相关阅读:
    unexpected inconsistency;run fsck manually esxi断电后虚拟机启动故障
    centos 安装mysql 5.7
    centos 7 卸载mysql
    centos7 在线安装mysql5.6,客户端远程连接mysql
    ubuntu 14.04配置ip和dns
    centos7 上搭建mqtt服务
    windows eclipse IDE打开当前类所在文件路径
    git 在非空文件夹clone新项目
    eclipse中java build path下 allow output folders for source folders 无法勾选,该如何解决 eclipse中java build path下 allow output folders for source folders 无法勾选,
    Eclipse Kepler中配置JadClipse
  • 原文地址:https://www.cnblogs.com/aaronlinv/p/14376174.html
Copyright © 2011-2022 走看看