zoukankan      html  css  js  c++  java
  • 阅读《Effective Java》每条tips的理解和总结(2)

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

      一个好用的类的属性必须要隐藏起来,干净的将它与类的api分离开来,类之间只通过api相互使用,降低他们之间的耦合性。为了做到这一点,建议根据情况选择尽可能低的访问级别修饰符。

     public 修饰不可变属性时(final修饰的属性)只是暴露的读权限,危害不是很大。但要注意的是,对于成员属性是对象的情况,不能在用final修饰后就认为危害很小了而用public修饰它,final修饰表示引用指向的对象不可变了,但根据引用取修改对象的内容是可以的。

    16. 在公共类中使用访问方法而不是公共属性

      大量使用的getter、setter方法就是符合这条原则,这样不仅将属性的读、写分开了,还可以在访问属性前做一些事、访问完属性后做一些事,要灵活的多。而不像公共属性,读写权限完全同时暴露,一旦被访问就是无条件被使用。

    17. 最小化可变性

      我们都知道String类一旦实例化后那这个对象的值是不可变的,是一个典型的最小化可变性的例子。除它之外,还有一众包装类:Integer、Double、Float.....都是将不可变性最小化的例子。他们都有如下特性:

      (1)所有属性都是final修饰的不可变属性。如String类的 char[] 数组,用final修饰后就不能修改引用指向的数组

      (2)所有属性都用private修饰。如String类的char [] 数组,这样就不能通过引用访问数组修改每个元素的内容

      (3)类没有提供修改这些属性的方法。如String类的char[] 数组,用private修饰也没有对其的setter方法,保证了字符内容的不可变。

      (4)在以上基础上,为了防止有子类恶意的继承并修改类的可变性,需要用final修饰类,设置为不可继承。

    最小化类的可变性好处是:对象从创建后它的状态就永远不可能被改变,是天然线程安全的,可以被任意的共享。也有一些坏处:对于每一个值都需要创建一个新对象,不可以从旧对象的基础上修改而来。一般的,尽量将一个类的可变性最小化。

      

    18. 组合优于继承

      继承虽然是代码重用的好方法,但是一定不能随便用,因为继承可能会破坏类的封装性,父类做了修改往往子类也要随之修改。因此,需要代码重用时、或需要使用另一个类的方法时先考虑使用组合。

     只有在子类真的是父类的子类型的情况下,继承才是合适的。也就是只有在两个类之间存在「is-a」关系 的情况下,才能使用继承。如果你试图让B类继承A类时,问自己这个问题:每个B都是A吗?如果你不能回答这个问题,那么B就不应该继承A。如果答案是:不是,那么就说明A只是B的一个实现细节,应该使用组合让B持有A的对象。

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

      还是因为继承会破坏类的封装性,子类很有可能随父类更改而更改。但是如果父类经过精心设计,专门考虑别的子类继承自己的情况给出文档说明,子类可以放心继承父类而不用时时更改。设计可供继承的类主要注意:

    (1)仔细考虑该暴露哪些属性给子类,尽可能的少暴露。

    (2)父类编写完成后一定要由其他的人编写子类继承它,并测试。

    (3)注意父类的构造方法不要调用可供重写的方法,因为:子类在实例化时会先执行父类的构造方法,此时子类还没有实例化完成,如果这时调用子类的重写方法则会发生意料之外的结果。

    如果不能做到以上几点,不要让这个类可继承。

    20. 接口优于抽象类

      当一个类需要有某些特定的方法时,可以继承一个抽象类或者去实现一个接口,推荐实现接口。主要的原因是:一个类允许实现多个接口,但只能继承一个类,而且一个接口还可以继承多个接口,这就决定了接口比抽象类灵活的多。

      如果一个接口的方法有明显实现,还可以考虑为其提供默认实现(JDK8开始支持),默认方法可以减轻不少实现接口的任务。但是默认方法是有一些限制的:一般不允许为Object类的方法提供默认实现,默认实现中不允许有实例属性、非静态成员,不能给不受控制的接口添加默认实现方法。为此,常用做法是:额外创建一个骨架类,对一些方法做一些简单的实现,或者使用接口的默认方法实现。

    21. 为后代设计接口

      编写接口的方法时一定要考虑清楚,因为如果在接口投入使用后再往接口里添加方法就会导致实现类出错,因为实现类中并没有为这个新加的接口方法编写实现。当然,如果添加的方法是默认实现的方法,那是不会导致出错的,因为所有实现这个接口的类也自动有了这个默认实现的方法。但是,如上一条所说,默认接口方  法是由一些限制的,而且默认接口方法如果一旦出错,破坏性也是很大的毕竟影响了所有实现此接口的类。

      总之,不要寄希望后续添加默认方法来修改接口,而应该在发布一个接口前仔细考虑,测试后再发布。

    22. 接口仅用来定义类型

      接口通俗的讲就是一组规范、标准,实现了某个接口则一定会具有此接口的全部特性,包括含有的属性、能做的动作(声明的方法)。因此,接口引用可以指向实现了它的类的实例,可以说接口就是一种类型。

      但是,若一个接口只声明了常量属性而没有规定方法,那么这个接口也就毫无意义了。因为声明常量的工作完全可以放到类里面,或者放到某个工具类里面,而且常量声明再接口里总是public的,这会在继承时一层层“传下去”,污染子类。

      总之,接口表示一个类型,类型应该是属性+方法组成。因此,接口中只有属性是无意义的。

    23. 类层次结构优于标签类

        根据构造方法传入一个标签,根据此标签构造不同特性的对象,这就是标签类。虽然用起来感觉很方便,编写一个类实现了多种类型的对象,但是这样做其实不是一种好的做法。如下:

    class Figure {
      enum Shape { RECTANGLE, CIRCLE }; 
      final Shape shape;        //这个属性的值表明当前类是什么特性
      double radius;       
      double length;
      double width;
      ...
      double area () {
       switch(shape) {
            case RECTANGLE:               //如果当前对象的类型标签表明是一个长方形
                  return length*width;
            case CIRCLE:
                  return Math.PI*(radius*radius);     //如果当前对象的类型标签表明是一个园
        } 
      }
    }

    由于Figure类通过一个标签,可以让自己表现不同特性,因此这个类也必须有着多种特性的属性,每种方法又要根据标签通过swicth做不同的动作。比如上面计算面积函数,需要根据形状标签用不同的属性采用不同的计算。

    可以看出来:标签类十分冗长、易错、效率低,而且不符合对象的思想---一个类不应该表现出多种特性。最好的做法是使用继承,使其成为一个层次结构:先编写一个具备基本特性的基类,可以设为抽象类,再根据不同的特性需求在继承后编写子类自己的方法、属性。

    24. 使用静态成员类而不是非静态成员类

        定义在某个类中的类叫做嵌套类,有四种不同的嵌套类:静态成员类,非静态成员类,匿名类和局部类,后三种也叫做内部类(因为它们三个必须依赖宿主而存在)。每个都有它的用途:

        如果一个嵌套的类需要在一个方法之外可⻅,或者太⻓而不能很好地适应一个方法,使用一个成员类(即非局部类)。如果一个成员类的每个实例都需要一个对其宿主实例的引用,使其成为非静态的(即非静态成员类);否则,使其静态(静态成员类)。假设这个类属于一个方法内部,使用局部类。如果局部类只需要从一个地方创建实例,并且存在 一个预置类型来说明这个类的特征,那么把它作为一个匿名类。

        为什么提倡使用静态成员类呢:因为非静态成员类每个实例都会有一个隐藏的外部引用给它的宿主实例,存储这个引用需要占用时间和空间,并且会导致即使宿主类在满足垃圾回收的条件时却仍然驻留在内存中。如果由此产生的内存泄漏通常难以检测到,因为这个引用是不可见的嘛。 

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

        Java编译器允许在单个源文件中定义多个顶级类,但这样做没有任何好处,并且存在重大⻛险。因为在源文件中定义多个顶级类,使得可能为类提供了多个定义。使用哪个定义会受到源文件传递给编译器的顺序的影响:

    A.java文件================
    class A{
        ....
    } 
    class B{
        ...
    } 
    
    B.java文件================
    class B{
        ....
    } 
    class A{
        ...
    }

    如果这时执行 javac A.java B.java就会出现类名重复的错误。

    26.不要使用原始类型

        这里的原始类型指的是不加类型参数,也就是创建使用了泛型的类时要添加类型参数,如下:

    List<Integer> list = new ArrayList<>();   //要这样
    list.add("1");  //使用了类型参数,放入类型不一致的数据在编译时就会报错
    Integer i = list.get(0);  //有泛型翻译,list对象已经被标记。调用它返回值为泛型的方法时,自动将返回的Object对象强制转换为Integer,不需要手动强制转换
    =============================================================
    List list = new ArrayList();  //不要这样,因为它不对存放数据类型做限制
    list.add("1");   //编译时这里并不会报错,只是发出一个警告
    Integer i = list.get(0);  //这里去除元素时由于类型不一致才会报错

    使用了类型参数,在编译期间就能保证类型的安全性,否则我们只有在运行时才会发现类型不一致的错误,我们当然宁愿在编译期间发现错误。使用原始类型虽然是合法的,但是那样会丢失泛型的所有安全性和表达上的优势。

    注意:泛型仅仅在编译期间起一个约束的作用,保证类型的安全性,本质上是个语法糖,不会对JVM运行程序时产生影响。在运行时,泛型信息是擦除了的,List<Integer> 和 List<String> 在运行时类型是一样的,都相当于是List<Object>类型;除了泛型擦除,还有泛型翻译,即List<E>中某些返回值为E(即用泛型接收返回值的方法),在方法调用后会插入checkcast指令,将返回的Object类型对象强制转换成E类型。比如List的方法: E get(int index){...},现在声明一个List<String>对象,则运行时get方法时E被擦除相当于返回Object类型对象,但经过泛型翻译又将Object强转为String。可以说泛型帮助我们完成不少事情:类型安全检查,类型强制转换。

    27. 消除非检查警告

        使用泛型编程时,会看到许多编译器警告:未经检查的强制转换警告,未经检查的方法调用警告,未经检查的参数 化可变⻓度类型警告以及未经检查的转换警告。有警告的代码不一定有异常,但是没警告的代码一定没异常,一定是类型安全的,所以我们应该尽量消除每一个警告。如果有某个非检查警告不好消除但我们确定一定是安全的,则可以用@SuppressWarnings("unchecked")印制哪个警告,并说明原因。

    28. 列表优于数组

        为什么相比数组,List优先使用呢?因为数组是协变的,sub[]数组可能是super[]数组的子类型,而List<String> 不是List<Object>的子类型,它们两者在编译时,编译器把它们看作是不同的类型(运行时经过泛型擦除后二者都是List原始类型)。例如:

    Object[] objects = new Long[1]; 
    objects[0] = "12345";      //协变性,所以编译时是没毛病的。
    Long l = objects[0];      //运行到这里会报类型不一致错误
    ====================================
    List<Object> ol = new ArrayList<Long>(); 
    ol.add("123456");     //这里编译时就会报错 我们宁愿编译时出错

    另外注意,使用参数化类型声明数组是不合法的,原因还是数组是协变的。如果参数化类型数组合法请看如下代码:

    List<String>[] stringLists = new List<String>[1];  //一个List<String>类型的数组
    List<Integer>  intList = List.of(1);            // 一个装整数的List,List<Integer> (jdk9新用法)
    Objects[] objects = stringLists;                // 一个Object型数组,指向List<String>类型的数组
    objects[0] = intList;                           //由于数组的协变性,将List<Integer>赋给List<String>
    String s = stringLists[0].get(0);               //拿元素时,就会出现类型不一致的错误。

    可以看出:经过Object类型的数组的中间运作,原本数组的List<String>类型的元素变为了List<Integer>,失去了泛型可以保持类型安全的作用。

    总之,数组和参数化类型具有非常不同的类型规则,数组是协变和具体化的,参数化类型是不变的。因此,数组不提供编译时类型的安全性。一般来说,数组和参数化类型不能很好地混合工作,如果发现它们混合在一起,得到编译时错误或者警告,应该用列表来替换数组。 

    29. 优先考虑泛型

        也就是说在新建一个类时,优先使用泛型来表示成员属性的类型。而不是直接把所有属性都设为Object,在使用类的时侯再根据具体情况进行类型的强制转换。原因也很清楚了:使用泛型的类在使用时能在编译期间保证不会有类型不一致的错误发生。JDK的许多集合类的实现就是基于泛型的。

    30. 优先使用泛型方法

       当方法参数列表中有原始类型而不使用类型参数时 ,则可能出现一些问题:

    public static Set union(Set s1, Set s2) {
         Set result = new HashSet(s1);
         result.addAll(s2);      //这里如果s1、s2装的数据不一样,就会在后续使用result时出现错误
         return result;  
    } 

    上面合并两个set的代码中,如果集合s1、s2装的数据类型不一样,则合并两个集合新集合里装的数据类型就会不一致,使用时必定出现错误。所以应该使用泛型方法,规定方法只接收哪种类型,避免潜在的类型安全问题。使用泛型保证方法的类型安全性示例如下:

    public static <E> Set<E> union(Set<E> s1, Set<E> s2) { 
        Set<E> result = new HashSet<>(s1); 
        result.addAll(s2); 
        return result; 
    } 

    31. 使用限定通配符增加API的灵活性

        上一条中也说到了:为了防止类型安全,要使用泛型方法。但是使用了泛型方法,形参就只能接收同种类型参数的实参了。也要注意:List<Object>形参并不能接收List<String>实参,因为二者没有父子关系,它们是同一个类只是装的东西类型不同而已。为了增加方法的灵活性,我们可以使用通配符 “?”,List<?>表示可以接收装了任何类型的List。“?” 还可以增加一些限定条件, List<? extends Person> 形参表示可以接收装了 Person类及其子类的List, 而List<? super Person> 形参表示可以接收装了 Person类及其父类的List。

        限定通配符非常有用,一般按PECS来,即:生产者(P),使用 extends(E);消费者(C),使用super(S)。如:形参List<E> list ,E类型的数据都是从list中拿出来的,list是E的生产者,则使用 List<? extends E> list  这个形参比较好;形参Compare<T> c,对象c需要消费T类型对象生成指示顺序的整数,因此用Cpmpare<? super T> c这样的形参比较好。 

        举个更通俗的例子,对栈的push、pop操作:

    //从src拿数据push到栈里
    public void pushAll(Iterable<? extends E> src) {   
       for (E e : src)  {    //src是生产者,数据由src生产出来,因此这个数据要是E或其子类,以便用E来接收从src拿来的数据
          push(e); 
       }
    } 
    ==========================================
    //从栈里拿数据add进dst
    public void popAll(Collection<? super E> dst) {   
        while (!isEmpty()) 
            dst.add(pop());   //dst是消费者,数据由栈里来,类型为E,因此dst可以装的数据要是E或其父类,以便放下E
    }    

    PECS也称为放置原则。虽然通配符+限定编程有点复杂,但是是非常值得的,使用时记住PECS原则(又是消费者又是生产者则需要用一个精确的类型参数),还要记住,所有Comparable和Comparator都是消费者。

    32. 合理的结合泛型和可变参数

        编程时有时我们会使用可变参数:void get(String... s)。可变参数这确实有它的用武之地,但一般不要与泛型结合使用,因为可变参数说到底就是数组的变种,在28条时也说到泛型不能与数组结合使用,否则会有类型安全的隐患。而且虽然泛型可变参数不是类型安全的,但它们是合法的。所有如果选择使用泛型+可变参数编写方法,一定要确保该方法是类型安全的。

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

        平时用的容器类型参数的个数都是规定好了的,如List<E>、Map<K,V>....有个时候,我们需要更灵活的容器,希望不对类型参数做限制即不止能放入声明的两种类型的数据。这听着很简单:我不指定类型参数不就行了,直接使用原始类型就什么类型的数据都可以放了,实际上,这样做有一个缺点就是没有类型安全性。为了可以存多种类型的数据还要兼顾类型安全,可以通过使用Class类来实现:往容器中添数据时,以一个Class对象为key,数据本身为value一起存进去。如:

    //m是某个可以异构容器对象
    m.putM(String.class, "Java"); 
    m.putM(Integer.class, 1024);
    
    String favoriteString = f.getFavorite(String.class); 
    int favoriteInteger = f.getFavorite(Integer.class);  

    其实些都可以由Map<Class<?>,Object>来支持,以Class对象为key也叫做Class令牌。

  • 相关阅读:
    服务端主动关闭 time_wait
    告别Excel!别人家高大上的财务数据分析,这才是老板的最爱
    客户端主动关闭,timed_wait
    两千字揭密 MySQL 8.0.19 三大索引新功能:隐藏索引,降序索引,函数索引
    零基础入门深度学习(3)
    零基础入门深度学习(3)
    零基础入门深度学习(3)
    android 动画学习总结
    android 动画学习总结
    android 动画学习总结
  • 原文地址:https://www.cnblogs.com/shen-qian/p/12068831.html
Copyright © 2011-2022 走看看