Effective Java通俗理解(上)

从BSCAN到BAP:深度解析Tessent MBIST逻辑的协同工作机制与设计考量 本文深入解析了Tessent MBIST逻辑中BSCAN Interface、Logic Group Def、Controller与BAP模块的协同工作机制。文章以一条测试指令的完整路径为例,详细阐述了数据流与控制流如何跨模块、跨时钟域协同工作,并重点探讨了MBIST BAP在协议转换与流量管理中的核心桥梁作用,为芯片内建自测试的集成与调试提供了关键的设计考量和实战技巧。 阅读详情

本文转载自 https://www.cnblogs.com/yulinfeng/p/7282649.html 


这篇博客是Java经典书籍《Effective Java(第二版)》的读书笔记,此书共有78条关于编写高质量Java代码的建议,我会试着逐一对其进行更为通俗易懂地讲解,故此篇博客的更新大约会持续1个月左右。

第1条:考虑用静态工厂方法代替构造器

  通常情况下我们会利用类的构造器对其进行实例化,这似乎毫无疑问。但“静态工厂方法”也需要引起我们的高度注意。
  什么是“静态工厂方法”?这不同于设计模式中的工厂方法,我们可以理解它为“在一个类中用一个静态方法来返回这个类的实例”,例如:

public static People getInstance() {
    return new People();
}

  它是一个“方法”,那么它不同于构造器,它可以随意修改方法名,这就带来第一个优点——有名称。有时一个类的构造器往往不止一个,而它们的名称都是相同的,不同的仅仅是参数,如果不同参数带来不同的含义这样对于调用方来说除了注释很难理解它们有什么不同的含义。例如BigInteger(int, int, Random)返回一个素数,但调用者很难理解API设计者所要想表达的意思,如果此时有BigInteger.probablePrime静态工厂方法,则能一目了然的清楚API设计者所要想表达的含义。举一个JDK的例子:Executors类,在这个类中有newFixedThread、newSingleThreadExecutor、newCachedThreadPool等静态方法,因为它们有“名字”,所有就较为清晰的明白API的含义。

  《Effective Java》中所提到的静态工厂方法第二个优点在于不用重复创建一个对象,实际上也就是勤加载或者称为饿汉式的单例模式。例如:

复制代码
public class Instance() {
    private static Instance instance = new Instance();
    private Instance(){}
    public static Instance getInstance() {
    return instance;
    }
}
复制代码

  静态工厂方法的第三个优点,可以返回原返回类型的任何子类型的。这句话初看不好理解,举个JDK中的例子:Collections类。 

List list = Collections.synchronizedList(new ArrayList()) 

  这个例子就说明了可以返回原返回类型的任何子类型的对象。
  关于静态工厂方法的第四个优点,在创建参数化类型实例的时候,它们使代码变得更加简洁,书中举了一个例子:

Map<String, List<String>> m = new HashMap<String, List<String>>();    //这会显得很繁琐

  给集合类提供静态工厂方法后:

public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}

   但是实际上从JDK7(包括JDK7)之后的集合类可以用以下简洁的代码代替:

Map<String, List<String>> m = new HashMap<>();

  静态工厂方法也有两个缺点:一是公有的静态方法所返回的非公有类不能被实例化,也就是说Collections.synchronizedList返回的SynchronizedList不能被实例化;二是查找API比较麻烦,它们不像普通的类有构造器在API中标识出来,而是和其他普通静态方法一样,鉴于此,书中提到了几种惯用名称:

  valueOf
  of  
  getInstance
  newInstance
  getType
  newType

第2条:遇到多个构造器参数时要考虑用构建器

  你是否写过下面类似的代码:

复制代码
public void Student()  {
    /*必填*/
    private String name;
    private int age;
    /*选填*/
    private String sex;
    private String grade;
    public Student(String name, String sex) {
        this(name, sex, 0);
    }
    public Student(String name, String sex, int age) {
    this(name, sex, age, “”);
    }
    public Student(String name, String sex, int age, String grade) {
    this.name = name;
    this.sex = sex;
    this.age = age;
    this.grade = grade;
    }
}
复制代码

  当我想实例化一个名字叫“kevin”,性别男,但是不写年龄,只有年级“1年级”,这个时候代码就:不得不要为年龄这个参数传递值。如果新增一个只含年级的构造方法,那又将多出许多代码,更严重的是,如果没有一份详尽的文档或者注释,看到如此多的构造方法将无从下手,这就是非常常见的重叠构造器。

Student student = new Student(“Kevin”, “男”, “0”, “1年级”);

  当然还有另外一种方法,只有一个必填项的构造方法,而其他选填项利用setter方法传递。例如:

Student student = new Student(“kevin”, “男”);
student.setGrade(“1年级”);

  这实际上导致了在构造过程中JavaBean可能处于不一致的状态,也就是说实例化对象本该是一气呵成,但现在却分割成了两大步,这会导致它线程不安全,进一步引发不可预知的后果。
  书中提到较为“完美”的解决方案就是利用“Builder模式(建造者模式)”,有关此设计模式可以查看《建造者模式》。这种解决方案属建造者模式的一种形式,其核心就是不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象,再调用类似setter的方法设置相关可选参数。构建器模式如下所示:

复制代码
/**
 * 构建器模式
 * Created by yulinfeng on 2017/8/3.
 */
public class Student {
    /*必填*/
    private String name;
    private int age;
    /*选填*/
    private String sex;
    private String grade;

    public static class Builder {
        private String name;
        private int age;
        private String sex = "";
        private String grade = "";

        public Builder(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public Builder sex(String sex) {
            this.sex = sex;
            return this;
        }
        public Builder grade(String grade) {
            this.grade = grade;
            return this;
        }
        public Student build() {
            return new Student(this);
        }
    }
    private Student(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.sex = builder.sex;
        this.grade = builder.grade;
    }
}
复制代码

  客户端代码:

Student student = new Student.Builder("kevin", 23).grade("1年级").build();

  这样的客户端代码很容易边写,并且便于阅读。对于不了解的可能来说利用构建器模式编写Student类不算易事,甚至代码比重叠构造器的代码更多。所以当可选参数在很多时,谨慎使用重叠构造器,而是使用构建器模式。

2017-08-03

第3条:用私有构造器或者枚举类型强化Singleton属性

  书中的此条目下,我总结出来认为最有价值的是“能被序列化的线程安全的类,被序列化后是否还会是单例”。单例模式几乎人人会写,例如上面提到的勤加载(饿汉式)的单例模式:

复制代码
public class Instance {
    private static final Instance obj = new Instance();
    private Instance() {
    }
    public static Instance getInstance() {
        return obj;
    }
}
复制代码

  毫无疑问这个单例是线程安全的,如果我们希望一个类能被序列化,那么这个类直接实现Serializable就可以了,但是这样能否保证被反序列化过后还是单例呢?

复制代码
 1 import java.io.Serializable;
 2 
 3 /**
 4  * 序列化单例对象
 5  * Created by 余林丰 on 2017/8/4/0004.
 6  */
 7 public class Instance implements Serializable {
 8     private static final Instance obj = new Instance();
 9     private Instance() {
10         
11     }
12     public static Instance getInstance() {
13         return obj;
14     }
15 }
复制代码

  测试代码:

复制代码
 1 import java.io.FileInputStream;
 2 import java.io.FileOutputStream;
 3 import java.io.ObjectInputStream;
 4 import java.io.ObjectOutputStream;
 5 
 6 /**
 7  * 序列化与反序列化单例对象
 8  * Created by 余林丰 on 2017/8/4/0004.
 9  */
10 public class Main {
11     public static void main(String[] args) throws Exception{
12         ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\objFile.obj"));
13         Instance instance = Instance.getInstance();
14         out.writeObject(instance);
15         out.close();
16     
17         ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\objFile.obj"));
18         Instance instance1 = (Instance) in.readObject();
19         in = new ObjectInputStream(new FileInputStream("D:\\objFile.obj"));
20         Instance instance2 = (Instance) in.readObject();
21         System.out.println("obj1 hashcode:" + instance1.hashCode());
22         System.out.println("obj2 hashcode:" + instance2.hashCode());
23         in.close();
24     }
25 }
复制代码

  比较两个实例对象的hash值,可以看到执行结果为:

  这就说明被反序列化过后便不再是单例。要保证单例还必须在单例类中实现readResolve的方法:

复制代码
 1 import java.io.Serializable;
 2 
 3 /**
 4  * 序列化单例对象
 5  * Created by 余林丰 on 2017/8/4/0004.
 6  */
 7 public class Instance implements Serializable {
 8     private static final Instance obj = new Instance();
 9     private Instance() {
10         
11     }
12     public static Instance getInstance() {
13         return obj;
14     }
15     private Object readResolve(){
16         return obj;
17     }
18 }
复制代码

  如此一来的执行结果为:

  显然此时被序列化过后还是单例,至于为什么要实现readResolve,这个方法并不是Serializable接口的方法,在此我并没有深究,究其原因估计是在反序列化的时候会调用这个奇怪的方法。

  另外书中还提到了另外高档的一种单例模式,此方法既能保证线程安全,也能保证被反序列化后还是单例。

复制代码
1 /**
2  * 枚举类型单例
3  * Created by 余林丰 on 2017/8/4/0004.
4  */
5 public enum Instance {
6     INSTANCE    //同样可以像普通类一样定义普通的方法变量等
7 }
复制代码

  将测试代码第13行稍作修改:

复制代码
 1 import java.io.FileInputStream;
 2 import java.io.FileOutputStream;
 3 import java.io.ObjectInputStream;
 4 import java.io.ObjectOutputStream;
 5 
 6 /**
 7  * 序列化与反序列化单例对象
 8  * Created by 余林丰 on 2017/8/4/0004.
 9  */
10 public class Main {
11     public static void main(String[] args) throws Exception{
12         ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\objFile.obj"));
13         Instance instance = Instance.INSTANCE;
14         out.writeObject(instance);
15         out.close();
16     
17         ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\objFile.obj"));
18         Instance instance1 = (Instance) in.readObject();
19         in = new ObjectInputStream(new FileInputStream("D:\\objFile.obj"));
20         Instance instance2 = (Instance) in.readObject();
21         System.out.println("obj1 hashcode:" + instance1.hashCode());
22         System.out.println("obj2 hashcode:" + instance2.hashCode());
23         in.close();
24     }
25 }
复制代码

  运行结果为:

  可以看到它自带序列化机制,且防止了多次实例化,非常高档的一个单例实现。

第4条:通过私有构造器强化不可实例的能力

  在此条书中建议有的工具类例如Arrays等,对它们进行实例化并没有意义,所以应该在它们的构造方法上应该使用private修饰。

2017-08-04

第5条:避免创建不必要的对象

  此条目下书中提到“当你应该重用现有对象的时候,请不要创建新的对象”。

  最为突出的例子莫过于字符串常量的创建,众所周知String字符串有两种创建方式

String str = “hello”;
String str = new String(“hello”);

  第一种String字符串的创建是在方法区(JDK7后改到了堆内存)中的常量池中创建一个”hello”常量,将来若再有一个字符串变量为“hello”时将直接指向常量池中的“hello”变量而不用重新创建;第二种则是会在堆变量中创建一个新的String实例,将来若再以此方式创建一个字符串变量为“hello”时还是会重新创建一个String实例。显然第二种方式创建了一个“不必要”的实例,相比较而言第一种方式更加高效。

  另外一个例子则是将”true”变量转换为Boolean类型也有以下两种转换方式:

Boolean b = new Boolean(“true”);
Boolean b = Boolean.valueOf(“true”);

  第一种转换方式也是每次都会在堆内存中创建一个新的Boolean实例;第二种查看其源代码便知不会每次返回一个新的实例,返回的是一个在编译器就已经确定了的static final Boolean型变量:

复制代码
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

public static Boolean valueOf(String s) {
    return toBoolean(s) ? TRUE : FALSE;
}
private static boolean toBoolean(String name) {
    return ((name != null) && name.equalsIgnoreCase("true"));
}
复制代码

  书中举了一个例子是否是在1946年至1964年出生来说明,这个例子在现实当中也很常见:

复制代码
 1 import java.util.Calendar;
 2 import java.util.Date;
 3 import java.util.TimeZone;
 4 
 5 /**
 6  * “是否在1946-1965年出生”
 7  * 这在现实中应该是比较常见的一种写法
 8  * Created by yulinfeng on 8/5/17.
 9  */
10 public class Person {
11     private final Date birthDate;
12 
13     public Person(Date birthDate) {
14         this.birthDate = birthDate;
15     }
16 
17     public boolean isBabyBoomer() {
18         Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));    //新创建Calendar实例
19         gmtCal.set(1949, Calendar.JANUARY, 1, 0, 0, 0);
20         Date boomStart = gmtCal.getTime();  //新创建Date实例
21         gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
22         Date boomEnd = gmtCal.getTime();    //新创建Date实例
23         return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
24     }
25 }
复制代码

  这样的代码我相信人人都写过类似的,书中提到这种代码的写法每次都创建新的实例对象,而实际上是不必要的,而给出了一种比较高效的代码示例:

复制代码
 1 import java.util.Calendar;
 2 import java.util.Date;
 3 import java.util.TimeZone;
 4 
 5 /**
 6  * “是否在1946-1965年出生”
 7  * 这在现实中应该是比较常见的一种写法
 8  * Created by yulinfeng on 8/5/17.
 9  */
10 public class Person {
11     private final Date birthDate;
12     private static final Date BOOM_START;
13     private static final Date BOOM_END;
14     static {
15         Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));    //新创建Calendar实例
16         gmtCal.set(1949, Calendar.JANUARY, 1, 0, 0, 0);
17         BOOM_START = gmtCal.getTime();  //新创建Date实例
18         gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
19         BOOM_END = gmtCal.getTime();    //新创建Date实例
20     }
21     public Person(Date birthDate) {
22         this.birthDate = birthDate;
23     }
24 
25     public boolean isBabyBoomer() {
26         return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
27     }
28 }
复制代码

  利用静态代码块在类Person初始化的时候创建对象,而在之后调用方法判断时不再每次重新创建新的实例对象,这种写法有点“烧脑”,确实有点“不符合”编码习惯,大概是因为这是需要思考才能写出来的原因吧。

第6条:消除过期的对象引用

  此条目较为容易理解,之所以要消除过期的对象引用其目的就在于尽量避免内存泄露的问题,书中举了一个“栈”的例子,其中在弹出一个元素时代码如下:

复制代码
public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    return elements[--size];
}
复制代码

  可以看到弹出元素时仅仅是将元素弹出后在将数组的索引-1,实际上数组维护的那个元素引用还在,也就是说那个被弹出的元素并不会被GC,如此一来最后就很有可能导致内存泄露的问题。解决此类的办法就是,当不再使用这个元素时,将其置为null。

复制代码
public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    elements[size] = null;
    return result;
}
复制代码

  实际上当你写出上面的代码时一定注意这并不是在任意条件下都成立,你应该仔细思考此时的引用是否为过期引用,“清空对象引用应该是一种例外,而不是一种规范行为”。

第7条:避免使用终结方法

  此处所谓的终结方法指的就是finalize()方法,这个方法可能对于从C++转向Java的新手感到混淆,因为在C++中有一个“析构函数”,析构函数所代表的意义就是在这个对象垃圾回收前所做的一些动作例如资源的关闭等。对于Java来说垃圾回收是自动的,或者称之为不可预知或不可控,尽管finalize方法所代表的也是在垃圾回收前所做的一些动作,但对于GC的时间你不能掌握,也就是说不能保证finalize方法会被及时执行,这是很危险的,一般情况下这个方法不会被用到。

  终结方法既然存在那它就并不是毫无用处,第一种用途就是充当一个“安全网”,终结方法“本该”是在GC前做一些清理动作,但GC的时间未知,也就是终结方法执行时间未知,对于FileInputStream类我们都知道应该在try-finally中对其调用close方法,但也许我们会忘记编写此方法,在FileInputStream源代码中就实现了终结方法目的就在于如果忘记了close方法,至少还有终结方法,虽然可能不能得到及时执行,但晚执行总比不执行好吧。第二个用途可能使用的场景就可能比较少,JVM只回收普通对象,对于本地对象(也就是不是Java实现的对象),JVM并不会对它进行回收,此时我们就可以在终结方法中对本地对象进行一些清理操作,但一定记住一定要是“不拥有关键资源的前提”,且在子类中重写了终结方法一定要现实调用super.finalize(),否则父类的终结方法不会被调用。

  综上,对于终结方法,一般代码中并不会使用,如果要使用一定要考虑上面两种用途是否值得去做,万万不应该依赖终结方法来更新重要的持久状态。

2017-08-06

第8条:覆盖equals时请遵守通用约定

  对于equals方法,在编码中最常用的可能就是比较两个字符串是否是值相等的。需要自己重写equals方法的场景可能并不是人人都能有幸碰到,而如果碰到了该怎么办,本条目下书中说明了重写equals方法时需要遵守的一些通用约定,如果不遵守这些约定可能导致无法和其他类配合使用。

  equals方法来自于Object类:

public boolean equals(Object obj) {
    return (this == obj);
}

  可以看到在Object类中equals比较的两个实例是否是引用相等的,换句话说,在不考虑“值相等”的情况下,每个实例都是独一无二的,每个实例都只与它自身相等。何时需要重写equals方法呢?顶级类只提供了引用是否相等,如果你需要自己实现一个逻辑是否相等,此时则需要重写equals方法,例如String类,但当在重写equals方法时,应该遵守以下约定:

  自反性:对于任何非null的引用值x,x.equals(x)必须返回true。也就是说一个类的实例一定是与它本身相等的,不管你怎么实现它的逻辑判断,但它的“本”不能忘。

  对称性:对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。这条比较好理解,x=1,y=1,你不能y=x而x!=y吧。

  传递性:对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)也必须返回true。显而易见。

  一致性:对于任何非null的引用值x和y,只要equals的比较操作在对象中的所用的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致地返回false。这条显然,你不能多调用判断几次它的结果就产生变化了吧。

  对于任何非null的引用值x,x.equals(null)必须返回null。

  一定要反复检查测试自己重写的equals方法是否遵守以上约定,否则程序可能会变得不正常,因为许多类,包括所有的集合类都依赖于是否遵守了equals约定。书中举了详细的例子来说明上述约定,这里不再叙述。

  我们来分析下String类中重写的equals方法:

复制代码
//String.equals
public boolean equals(Object anObject) {
    if (this == anObject) {    //是否等于自身
        return true;
    }
    if (anObject instanceof String) {    //类型是否相等
        String anotherString = (String) anObject;    //转换类型
        int n = value.length;    
        if (n == anotherString.value.length) {    //先判断长度是否相等
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {    //一个一个字符判断值是否相等
                if (v1[i] != v2[i])
                        return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
复制代码

  String中equals方法的实现实际上就是书中给我们的重写equals的一些诀窍:

  1、使用==操作检查“对象是否为这个对象的引用”,这不是必须的,只是作为一种性能优化,例如Integer类中并无此项判断。

  2、使用instanceof操作符检查“参数是否为正确的类型”。

  3、把参数转换成正确的类型。

  4、对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域想匹配。

第9条:覆盖equals时总要覆盖hashCode

  如果这个类仅仅是重写了equals方法而没有重写hashCode,那么这个类和基于散列的集合类一起工作时就会出现问题。

  首先明确一个概念,两个对象使用equals返回true,则它们的hashCode也一定相等;如果两个对象的hashCode相等,则它们的equals则不一定相等。这个概念和散列函数相关,在《哈希》这篇博客里我曾谈到过有关散列(哈希)相关的知识。

  如何实现hashCode,当然你可以使hashCode返回一个固定的数值,任何对象的hashCode都是一个固定的数值,这没有问题。但当它与基于散列的集合类一起工作时,这些元素将具有相同的散列码,进而使得所有对象都被映射到统一散列桶中,使得散列表退化为链表。散列函数应该如何编写在《哈希》 一文中有提到常用的散列算法,这里不再叙述。

第10条:始终要覆盖toString

  这条建议我在实际当中遇到过,因为当时几乎并没有人去重写toString方法,使得我不得不在后来去将几乎所有的POJO类的toString方法都重写了。原因在于在有的场景下会打印一条日志,日志的内容就是POJO类的属性字段值,这个时候toString的意义很明显的就体现出来了,好在eclipse能按照一定的格式自动生成toString方法。有的类是自己已经重新实现了toString方法例如集合类。

第11条:谨慎地覆盖clone

  按照书中的话来讲,能不重写clone就不要去重写,因为它带来的问题太多了。我们暂且不讨论这里面的陷阱有多少,只从对Java基础知识的掌握程度来说明什么是clone,以及什么是“深拷贝”和“浅拷贝”。

  首先观察以下代码,并思考对象在内存中的分配以及引用的变化:

复制代码
 1 /**
 2  * Created by 余林丰 on 2017/8/7.
 3  */
 4 public class Student {
 5     private String name;
 6     private int age;
 7     
 8     public Student(String name, int age) {
 9         this.name = name;
10         this.age = age;
11     }
12     
13     public String getName() {
14         return name;
15     }
16     
17     public void setName(String name) {
18         this.name = name;
19     }
20     
21     public int getAge() {
22         return age;
23     }
24     
25     public void setAge(int age) {
26         this.age = age;
27     }
28 }
复制代码
复制代码
 1 /**
 2  * Created by 余林丰 on 2017/8/7/0004.
 3  */
 4 public class Main {
 5     public static void main(String[] args) throws Exception{
 6         Student stu = new Student("kevin", 23);
 7         Student stu2 = stu;
 8         stu2.setAge(0);
 9         System.out.println(stu.getAge());
10     }
11 }
复制代码

 

  这是一段很简单的代码,Student对象实例stu、stu2在内存中的分配及引用分别如下图所示:

  所以代码中出现修改stu2实例的age字段时,stu中的age字段也被修改了,原因很简单因为它们的引用指向的都是同一个对象实例。

  那如果我们想在实例化一个name=”kevin”,age=23的Student实例怎么办呢?当然可以再写一段Student stu2 = new Student(“kevin”, 23);如果再重新构造一个对象实例很复杂,能不能直接复制呢?显然,使Student实现Cloneable接口并重写clone方法即可,注意此时的重写clone方法在里面仅有一句代码即是即调用父类的clone方法,而不是自定义实现:

复制代码
 1 /**
 2  * Created by 余林丰 on 2017/8/7/0007.
 3  */
 4 public class Student implements Cloneable{
 5     private String name;
 6     private int age;
 7     
 8     public Student(String name, int age) {
 9         this.name = name;
10         this.age = age;
11     }
12     
13     public String getName() {
14         return name;
15     }
16     
17     public void setName(String name) {
18         this.name = name;
19     }
20     
21     public int getAge() {
22         return age;
23     }
24     
25     public void setAge(int age) {
26         this.age = age;
27     }
28     
29     @Override
30     protected Student clone() throws CloneNotSupportedException {
31         return (Student)super.clone();
32     }
33 }
复制代码
复制代码
 1 /**
 2  *
 3  * Created by 余林丰 on 2017/8/7/0004.
 4  */
 5 public class Main {
 6     public static void main(String[] args) throws Exception{
 7         Student stu = new Student("kevin", 23);
 8         Student stu2 = stu.clone();
 9         stu2.setAge(0);
10         System.out.println(stu.getAge());
11     }
12 }
复制代码

  调用clone方法产生的对象实例并不是之前的实例,而是在堆上重新实例化了一个各个参数类型值都相同的实例,所以此时修改stu2的age字段并不会影响到stu,看起来clone就是一个构造器的作用——创建实例。

  上面我们仅仅是说明了什么是clone,接下来我们接着来讲解什么是“深拷贝”和“浅拷贝”。

  在上面的例子Student类中,我们新增一个引用型变量Test类:

复制代码
 1 public class Student implements Cloneable{
 2     private String name;
 3     private int age;
 4     private Test test;
 5     public Student(String name, int age) {
 6         this.name = name;
 7         this.age = age;
 8     }
 9     
10     public String getName() {
11         return name;
12     }
13     
14     public void setName(String name) {
15         this.name = name;
16     }
17     
18     public int getAge() {
19         return age;
20     }
21     
22     public void setAge(int age) {
23         this.age = age;
24     }
25     public String getTest() {
26         return test;
27     }
28     
29     public void setTest(Test test) {
30         this.test= test;
31     }
32     @Override
33     protected Student clone() throws CloneNotSupportedException {
34         return (Student)super.clone();
35     }
36 }
复制代码
复制代码
 1 **
 2  *
 3  * Created by 余林丰 on 2017/8/7/0004.
 4  */
 5 public class Main {
 6     public static void main(String[] args) throws Exception{
 7         Student stu = new Student("kevin", 23);
 8         Student stu2 = stu.clone();
 9         stu2.setAge(0);
10         System.out.println(stu.getAge());
11     }
12 }
复制代码

  实际上测试这段代码可知,clone出来的stu2确实和stu是两个对象实例,但它们的成员变量实际上确是指向的同一个引用(通过比较hashCode可知),这也就是所谓的“浅拷贝”。对应的“深拷贝”则是所有的成员变量都会真正的做一份拷贝。怎么做到“深拷贝”,则是要求将类中的所有引用型变量都要clone。

复制代码
 1 /**
 2  * 深拷贝
 3  * Created by yulinfeng on 8/7/17.
 4  */
 5 public class Student implements Cloneable{
 6     private String name;
 7     private int age;
 8     private Test test;
 9 
10     public Student(String name, int age) {
11         this.name = name;
12         this.age = age;
13     }
14 
15     public String getName() {
16         return name;
17     }
18 
19     public void setName(String name) {
20         this.name = name;
21     }
22 
23     public int getAge() {
24         return age;
25     }
26 
27     public void setAge(int age) {
28         this.age = age;
29     }
30 
31     public Test getTest() {
32         return test;
33     }
34 
35     public void setTest(Test test) {
36         this.test = test;
37     }
38 
39     @Override
40     protected Object clone() throws CloneNotSupportedException {
41         Student stu = (Student)super.clone();
42         stu.test = test.clone();    //Test类也要继承Cloneable
43         return stu;
44     }
45 }
复制代码

  书中是不建议自定义重写clone方法的,如果非要重写书中总结为一句话:clone方法就是一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。

  再说一个与本条目无关的点,查看Cloneable接口实际上可以发现里面什么方法都没有,clone方法却来自Object类,继承了Cloneable接口为什么就能重写clone方法了呢?原因在于clone方法在Object类中的修饰符是protected,而Cloneable接口和Object处于同一个包下,熟悉修饰符的都知道protected的权限限定在同一个包下或者其子类。Cloneable和Object同属于一个包,Cloneable自然能继承clone方法,继承了Cloneable接口的成为了它的子类同样也就继承了clone方法。

2017-08-07

第12条:考虑实现Comparable接口

  关于Comparable接口其中只有一个方法——compareTo。此方法和equals有类似之处,不过它所表达的含义相比equals要更多。equals通常是比较两个值是否相等,相等返回true,不相等返回false。compareTo则约定为第1对象若“大于”第2个对象则返回整数,“等于”则返回0,“小于”则返回负数,compareTo能约定更为复杂的“比较”,例如比较两个字符串进行字典序的比较,str = “abc”, str2 = “abd”,str.equals(str2)返回false,而str.compareTo(str2)则返回正数。compareTo与equals一样同样需要遵守自反性、对称性、传递性。同样有一个强烈的建议就是compareTo应该返回和equals方法相同的结果,但如果不一致,也不是不可以,就是最好能在注释中写明两个方法返回的结果不同。

第13条:使类和成员的可访问性最小化

  如何正确地使用访问权限?首先在定义一个成员变量时类型前会有一个修饰符public 、protected、private(或者没有)。在未学习到“面向对象”时,初学时老师为了讲解方便直接将变量定义为了public,慢慢接触到了面向对象的三大特性:继承、封装、多态,我们也学会了将成员变量的访问权限定义为private。

  此条实际上就是讲解面向对象的三大特性之一——封装。

  private——只有在生命该成员的类才能访问,其他类都不能访问。

  default(默认访问修饰符)——又称为“包级私有”,也就是说只有在同一个包下的类才能访问,就算是它的子类但不是在同一包下也不能访问。

  protected——有两种情况可以访问:1、和default一样同一个包下的类能够访问。2、它的子类也能访问。

  public——任何类都能访问。

  为什么书中提到提到要将可访问性最小化呢?实际上原因在于可维护,一旦你将可访问性置为protected或者public,意味着很大范围的类都能对它就行访问、修改、引用等等,如果你修改了这个变量意味着你要同时修改很多其他的类。但如果可访问性很小private的话,意味着你修改了这个变量,其他类并不知道啊,只对你自己有关系改好你自身就可以了不用担心还有其他哪个地方有用到。

  对于成员变量通常使用private类,如果完全不提供访问的渠道或者途径似乎也是“死”的,这是setter/getter方法,有人在初学时可能会发出疑问,既然提供了能访问修改这个成员变量的方法,那何不直接置为public呢?原因在于方法是可以提供检查的,它能检查传入的值是否符合规范。

  另外还有一条规则限制了降低方法的可访问性的能力,那就是如果方法覆盖了超类中的一个方法,子类中的访问级别就不允许低于超类中的访问级别。就是说如果父类的方法是public,那么子类就不能是private;如果父类的方法是protected,那么子类就只能是protected或者public。

第14条:在公有类中使用访问方法而非公有域

  这一条实际上我们在上一条已经说到对于类的成员变量,我们通常对它的访问控制置为private,并为它提供getter/setter方法。

2017-08-08

第15条:使可变性最小化

  注意第13条指的是“可访问性”主要讲访问修饰符,而这一条指的是“可变性”讲的是final关键字。

  我曾在这篇《“不可变的对象”与“不可变的对象引用”》提到过final关键字,在此我们重新回顾final关键字。对于final关键字,我想很多人对于它的用法基本上是处于以下用法:

public static final String NAME = “hello world”;

  把它和static关键字配合使用使得程序中不会出现硬编码,而是以常量的形式出现,这样也便于修改和阅读,但是final的功效远远不止于此。

  首先来了解下什么是不可变的对象,最为常见的就是String类,这个类大家都清楚是不可变的,至于为什么在赋值过后还能对它进行赋值那是因为在常量池中又定义了一个常量使得引用指向了这个新的常量,而不是在原有值的基础上做修改。

  以下是final的常见用法:

  1、 用final关键字修饰类,使得该类不可以被继承。例如String类就利用final修改使得它不可以被继承。

  2、 用final关键字修饰成员变量,使得该变量变为不可变的对象引用,此时应该给它赋初值,之后它不能被重新赋值,准确来讲是它的引用不可改变。注意是引用不可改变,不代表被引用的对象内部不能改变,如果引用的对象本身是一个引用类型例如:

复制代码
private Student stu = new Student();

stu.setName(“kevin”);

stu.setName(“ylf”); //这是合法的

//stu = new Student(); //这不合法,因为它改变了stu的引用
复制代码

  3、 比较通用的就是开篇提到的和static配合使用定义常量。

  个人认为final关键字的使用比较巧妙,用的好可以省去大量代码来保证线程安全,用的不好则是东施效颦带来严重后果,有关final关键字和线程安全可以参考《Java并发编程实战》一书对于fianl的讲解。

第16条:复合优先于继承

  这条建议也是十分常见,最实际的例子就是《组合模式》的运用。在JDK中一个很明显的例子就是显示锁的实现,例如ReentrantLock继承自Lock接口,但其内部实现则是通过AbstractQueueSynchronizer。

第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

  不要过度设计。

  面向对象编程,从一开始被洗脑难免在上手写代码时都会首先思考有没有公共方法啊,能不能把两个类抽象成一个父类再继承啊等,慎重使用继承,当要使用继承时一定要在文档注释中写明重写这个方法会给其他方法带来什么影响。书中给出建议如果类并不是为了继承而生,那么这个类应该用final修饰禁止子类化。

第18条:接口优于抽象类

  接口和抽象类的异同这是Java基础中的基础。我们不妨来回顾下Java和抽象类的区别:

  接口不能被实例化不能有构造器,抽象类也不能被实例化但可以有构造器;

  接口中不能有实现方法(JDK8在接口中可以有实现方法,称“默认方法”),抽象类中可以有实现方法,也可以没有;

  接口方法的默认修饰符就是public,不可以用其他修饰符,抽象类可以有public、protected、default。

  回到“接口优于抽象类”的问题上来,原因就是Java只支持单继承,但可以实现多个接口。有抽象类的地方基本上都可以看到其中的方法很多是模板方法(有关模板方法模式参考《模板方法模式》)。

第19条:接口只用于定义类型

  这个条目中建议接口不要只用于定义常量使之成为常量接口,如果一个类只有常量应该使用枚举类型或者不可实例化的工具类。JDK中的反例就是java.io.ObjectStreamConstant。

第20条:类层次优于标签类

  标签类是指在类中定义了一个变量,使用该变量的值来控制该做什么动作。

  书中举例:定义一个Figure类,使用Shapre变量,可以传入“长方形”或者“圆形”,根据传入的类型不同调用共同的方法。这个就是一个标签类,如果新增一个“三角形”的话,就得修改这个标签类的代码。

  更好的方法就是利用继承,合理利用继承能更好的体现面向对象的多态性。

第21条:用函数对象表示策略

  什么是函数对象?实际上这是在JDK8之前没有Java不支持lamda表达式,方法参数不能传递一个方法只能通过传递对象的方式“曲线救国”,例如Arrays.sort(T[] a, Comparator<? super T> c)方法,第一个参数传递数组,根据传入第二个自定义的比较类中的比较方法进行排序。如果能传入函数指针、Lambda表达式等,那就自然不用传递一个类。

  从JDK8开始Java已经支持了lambda表达式,不妨简单了解下JDK8的lambda表达式。

  广义上来讲JDK8中lambda表达式有两个部分组成:一是lambda表达式本身,二是函数式接口。函数式接口实际上就是指只包含一个抽象方法的接口,比如Runnable接口只包含run抽象方法。而lambda表达式本身实际上则是对抽象方法的实现。

  首先lambda表达式的语法格式如下所示:

  例如

 (n) -> System.out.println(n)

表示打印n。

  上面提到lambda是对抽象方法的实现,那么实际上这条lambda表达式对应的就是:

public void demo(String n) {
    System.out.println(n);
}

  lambda表达式不过是一个匿名方法实现而已,接下来我们看看到底是如何使用lambda表达式。

复制代码
1 package com.jdk8;
2 
3 /**
4  * 函数式接口,只包含一个抽象方法
5  * Created by 余林丰 on 2017/8/15/0015.
6  */
7 public interface LambdaDemo {
8     void demo(String n);
9 }
复制代码
复制代码
 1 package com.jdk8;
 2 
 3 /**
 4  * lambda例子
 5  */
 6 public class App {
 7     public static void main( String[] args )  {
 8         LambdaDemo lambdaDemo = (n) -> System.out.println(n);   //实例化LambdaDemo类,同时也lambda表达式实现了demo方法
 9         lambdaDemo.demo("hello lambda");
10     }
11 }
复制代码

2017-08-15

第22条:优先考虑静态成员类

  一般情况下我们可能提到最多的是“内部类”这个名词,实际上在类“内部”的类叫做“嵌套类”。嵌套类分为四种:静态成员类、非静态成员类、匿名类和局部类。除了静态成员类,其余三种才被称之为内部类。

  匿名类提及的比较多:

复制代码
public class App {
    public static void main( String[] args )  {
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
                System.out.println("这是一个内部类");
           }
       });  //回顾一下lambda表达式,可表示为Thread thread = new Thread(() -> System.out.println("这是一个内部类"));
       thread.start();
    }
}
复制代码

  静态成员类相比较于非静态成员类就是多了一个static关键字修饰类,另外一个更重要的区别在于非静态成员类的每个实例都包含一个额外的指向外围对象的引用,保存这份引用要耗费时间和空间。

  举个例子,在JDK7中,HashMap内部使用Entry类表示每个键-值对,这个类是static静态的,如果将static去掉仍然可以工作,但每个entry中将会包含一个指向该Map的引用,这样就浪费了空间和时间。

  那么什么时候使用静态什么时候使用非静态呢?

  书中给出了比较明确的原则:如果声明成员类不要求访问外围实例,就要始终把static修饰符放在它的声明中。也就是说如果成员类和外围实例类有交互,那这个类就应该是非静态的,如果没有交互而是作为外围类的一个组件存在在应使用静态的。

  最后一个是局部类,只要是在任何“可以声明局部变量的地方”,都可以声明局部类,用得最少,如果要使用那也必须非常简短。

第23条:请不要新代码中使用原生态类型

  从这条开始涉及泛型相关的点。

  从JDK5开始Java新增了“泛型”新特性,例如:List<String>,在这之前则只有List不会限定类型。

  如今的JDK版本中还是可以写原生类型,但这会带来隐藏的不安全问题。如果在声明一个列表变量时不使用泛型而使用原生类型,如下:

复制代码
/**
 *
 * Created by 余林丰 on 2017/8/17
 */
public class Main {
    public static void main(String[] args) throws Exception{
        List list = new ArrayList();
        list.add("hello");
        list.add(2);
    }
}
复制代码

  可以看到可以插入任意类型的数据。如果使用泛型:  

复制代码
/**
 *
 * Created by 余林丰 on 2017/8/17
 */
public class Main {
    public static void main(String[] args) throws Exception{
        List<String> list = new ArrayList<String>();
        list.add("hello");
        //list.add(2);  //编译期就会出错
    }
}
复制代码

  简而言之,使用泛型相对“安全”,从一开始就能限定数据类型,防止之后不小心插入了错误的类型,而对于原生态类型则不会检查插入的类型,有可能在以后插入了其他类型而只有在运行时才抛出异常,所以鼓励使用泛型。

  另外书中提到了“?”通配符和“<Object>”之间的区别,可参考《Java中的Object,T(泛型),?区别》。

第24条:消除非受检警告

  在使用泛型时,常常会不可避免的出现一些警告,这些警告可能并不会引起开发人员的注意,例如:

  我们应该在代码中尽量消除警告,如果无法消除警告,同时可以证明引起警告的代码是类型安全的,可以使用@SuppreWarnings(“unchecked”)注解,并在注释中加以解释。

第25条:列表优先于数组

  这条建议可引申出列表和数组的区别是什么,列表的内部实现。

  首先数组是协变的,这里的“变”指的是数据类型,而不是说数组的长度,数组的长度当然从一开始就确定不可改变,但对于以下代码确实合法的:

复制代码
/**
 *
 * Created by 余林丰 on 2017/8/17
 */
public class Main {
    public static void main(String[] args) throws Exception{
        Object[] objects = new Long[1];
        objects[0] = "hello world";
        System.out.println(objects[0]);
    }
}
复制代码

  但合法仅存在于编译时期,在运行时会抛出以下错误:

 

  泛型则是不可变的,这个不可变指的也是数据类型例如以下代码:

  这段代码在编译期就会报错。综上,利用数组只有在运行时才会报错,利用列表在编译时就会报错,我们当然希望在错误能在编译时尽早发现。

  大家可能都试图写过“泛型数组”:

List<String>[] lists = new ArrayList<String>[1];

  但是却发现是错误的,编译时抛出Generic array creation错误,并且一时难以想象为什么不能创建泛型数组,书中举了以下例子来说明:

复制代码
List<String>[] lists = new ArrayList<String>[1];    //先假设能创建泛型数组
List<Integer> intList = Arrays.asList(42);  //Integer列表
Object[] objects  = lists;  //根据数组的“协变性”是合法的,例如上面提到的Object[] objects = new Long[1]
objects[0] = intList;   //List<String>和List<Integer>在运行时类型会被擦除为List
String s = lists[0].get(0); //上一步操作过后,实际上取出的是一个initList,即取出是一个Integer
复制代码

  假设第一步不会报错,那么上面的例子在编译时就不会出错,但一到了运行时最后一句话就会抛出ClassCastException异常,也就是说与其在运行时出错,不如将它提前到编译时即不允许创建泛型数组。这就是为什么创建泛型数组是非法的原因:因为它不是类型安全的。要是它合法,编译器在其他正确的程序中发生的转换就会在运行时失败,并出现一个ClassCastExcetion异常。这就违背了泛型系统提供的基本保证。

  不过有一个例外,以下的创建方式却是合法的:

List<?>[] lists = new ArrayList<?>[1]; 

  泛型在运行时它的类型会被擦除,也就是说泛型是不可具体化的,它在运行时所包含的信息比它在编译时所包含的信息更少。唯一可具体化的参数化类型就是无限制的通配符类型,也就是上面提到的例子如List<?>。

此条目几乎一直在说数组和泛型不能很好的配合使用,如果遇到泛型的情况,应该首先考虑列表。

第26条:优先考虑泛型

  引用书中的话“一般来说,将集合声明参数化,以及使用JDK所提供的泛型和泛型方法,这些都不太困难。编写自己的泛型会比较困难一些,但是值得花些时间去学习如何编写”。

  举一个简单例子来帮助我们如何正确编写泛型:

复制代码
/**
 * Created by 余林丰 on 2017/8/17/0017.
 */
public class Test<E> {
    private E[] elements;
    
    public Test() {
        //elements = new E[16];    //编译时出错,不能创建不可具体化的类型的数组
        elements = (E[]) new Object[16];
    }
}
复制代码

  还有另外一种方法:

复制代码
/**
 * Created by 余林丰 on 2017/8/17/0017.
 */
public class Test<E> {
    private Object[] elements;
    
    public Test() {
        //elements = new E[16];    //编译时出错,不能创建不可具体化的类型的数组
        elements = new Object[16];
    }
    
    public E test() {
        return (E) elements[0];
    }
}
复制代码

  当在编写不可避免要使用数组时,可参考以上两种数组和泛型的实现方式。

第27条:优先考虑泛型方法

  泛型方法即在定义方法的返回值前加上<E>,例如Collections.sort方法,至于优点不再多说,一句话能用泛型尽量用泛型。

public static <T> void sort(List<T> list, Comparator<? super T> c)

  这个方法的第二个参数实际上是下条要提到的有限制通配符。

第28条:利用有限制通配符来提升API的灵活性

  之前我们提到了<?>形式的无限制通配符,这里则是有限制通配符。上一条目中已经出现了有限制通配符,它一共有这么2种:

  <? extends E>:表示可接受E类型的子类型;

  <? super E>:表示可接受E类型的父类型。

第29条:优先考虑类型安全的异构容器

  “异构”的英文heterogeneous意为多种多样的,书中所举的例子我认为非常有参考价值,仔细品味。

复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * 异构容器
 * Created by yulinfeng on 8/17/17.
 */
public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null) {
            throw new NullPointerException();
        }
        favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}
复制代码
复制代码
/**
 * Created by yulinfeng on 8/17/17.
 */
public class Main {

    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", favoriteString, favoriteInteger, favoriteClass.getName());
    }
}
复制代码

  Favorite类使用起来有点Map的感觉,putFavorite方法就类似Map.put,或者说用Map不就能实现吗?例如:

复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * Created by yulinfeng on 8/17/17.
 */
public class Main {

    public static void main(String[] args) {
        Map<Class<?>, Object> map = new HashMap<Class<?>, Object >();
        map.put(String.class, "Java");
        map.put(Integer.class, 122);
        System.out.println(map.get(String.class));
        System.out.println(map.get(Integer.class));
    }
}
复制代码

  能运行和上面结果一致,但问题就在于以下代码:

复制代码
import java.util.HashMap;
import java.util.Map;

/**
 * Created by yulinfeng on 8/17/17.
 */
public class Main {

    public static void main(String[] args) {
        Map<Class<?>, Object> map = new HashMap<Class<?>, Object >();
        map.put(String.class, "Java");
        map.put(Integer.class, 122);

        Object str = map.get(String.class);    //Integer str = (Integer) map.get(String.class);


        Object in = map.get(Integer.class);

    }
}
复制代码

  根据键取出来的值是Object,也就是说这是很危险的一件事情,如果代码写成上面注释那样的话在编译时是无法判断的,只有在运行时才会抛出异常。记住,能在编译时检查就在编译时检查而不要等到真正运行起来才做检查,这也就是上面Favorite所带来的好处,它是类型安全的,同时它也是异构的,这个例子值得细细品味。

2017-08-17

第30条:用enum代替int常量

  对于枚举类型可能并“不常用”,之所以打引号是因为,“不常用”并不是它不好用,而是因为某项原因例如项目外包或者某个项目不需要自己来维护等等,只是追求快速地实现功能。实际上枚举类型对于强化项目代码的结构和规整很有帮助,是一个必不可少所需要强掌握的技能。

  对于使用过枚举的来说,可能最常见的用的比较多的地方就是错误码了,例如:

复制代码
 1 /**
 2  * 枚举类型错误码
 3  * Created by yulinfeng on 8/20/17.
 4  */
 5 public enum ErrorCode {
 6     FAILURE(0, "操作失败"),
 7     SUCCESS(0, "操作成功");
 8 
 9     private int code;
10     private String msg;
11     ErrorCode(int code, String msg) {
12         this.code = code;
13         this.msg = msg;
14     }
15 
16     public int getCode() {
17         return code;
18     }
19 
20     public void setCode(int code) {
21         this.code = code;
22     }
23 
24     public String getMsg() {
25         return msg;
26     }
27 
28     public void setMsg(String msg) {
29         this.msg = msg;
30     }
31 }
复制代码
复制代码
1 /**
2  * Created by yulinfeng on 8/17/17.
3  */
4 public class Main {
5 
6     public static void main(String[] args) throws InterruptedException {
7         System.out.println(ErrorCode.SUCCESS.getCode() + ErrorCode.SUCCESS.getMsg());
8     }
9 }
复制代码

  这可能是枚举使用比较常见的一种用法。实际上枚举还有其它一些比较“高级”的用法,我们不妨从书中举例来一一说明。首先用枚举来实现加减乘除四种操作:

复制代码
 1 /**
 2  * 加减乘除枚举
 3  * Created by yulinfeng on 8/20/17.
 4  */
 5 public enum Operation {
 6     PLUS, MINUS, TIMES, DIVIDE;
 7 
 8     double apply(double x, double y) {
 9         switch (this) {
10             case PLUS: return x + y;
11             case MINUS: return x - y;
12             case TIMES: return x * y;
13             case DIVIDE: return x / y;
14         }
15         throw new AssertionError("Unknow op:" + this);
16     }
17 }
复制代码
复制代码
 1 /**
 2  * Created by yulinfeng on 8/17/17.
 3  */
 4 public class Main {
 5 
 6     public static void main(String[] args) {
 7         double x = 1.1;
 8         double y = 2.2;
 9         double result = Operation.PLUS.apply(x, y);
10         System.out.println(result);
11     }
12 }
复制代码

  试想如果还需要新增另外一种操作的时候却忘了新增case怎么办?编译时编译器并不会给出任何提示,同样的功能考虑以下实现能很好的避免这种遗忘新增case的情况:

复制代码
 1 /**
 2  * 加减乘除枚举
 3  * Created by yulinfeng on 8/20/17.
 4  */
 5 public enum Operation {
 6     PLUS {
 7         double apply(double x, double y) {
 8             return x + y;
 9         }
10     },
11     MIUS {
12         double apply(double x, double y) {
13             return x - y;
14         }
15     },
16     TIMES {
17         double apply(double x, double y) {
18             return x * y;
19         }
20     },
21     DEVIDE {
22         double apply(double x, double y) {
23             return x / y;
24         }
25     };
26 
27     abstract double apply(double x, double y);
28 }
复制代码

  如果我们想要新增XXX操作,如果不实现apply方法则不会编译通过,这种方法很巧妙的躲过了因为人为失误带来的隐患。

  枚举确实是一种比较神奇的类型,它不需要你new一个实例,并且在枚举天生就是不可变的,因此所有的域应该为final。一定记住像错误码或者一组固定常量的时候就要优先考虑使用枚举,而不是定义int常量甚至直接将字符串硬编码到代码中。



使用 Simulink + Simscape Electrical 搭建一个三相PWM整流器模型 本文详细介绍了使用Simulink搭建三相PWM整流器模型的方法,重点讲解了SVPWM调制策略的实现过程。内容包括系统结构设计、主电路搭建、SVPWM算法实现、双闭环控制器设计以及仿真分析。通过MATLAB Function Block实现SVPWM核心算法,并给出优化建议,如死区补偿、开关频率优化等。该教程为电力电子领域工程师提供了实用的建模与仿真方法,可用于新能源并网、电机驱动等应用场景的研发与优化。 阅读详情

相关推荐

OGG故障指南:OGG-01163 Bad column length (xxx) specified for column

OGG-01163 Bad column length (xxx) specified for column AAA in table OWNER.TABLE, maximum allowable length is yyy

qqqyyy0422的博客 718

返回原对象_java进阶笔记1《Effective java》-管理对象

每天坚持笔记打卡,笔记来源于effective java这本书代码 清晰性和简洁性最为重要:组件的用户永远也不应该被其行为所迷惑 。代码应该被重用,而不是被拷贝用静态工厂方法代替构造器1. 静态工厂方法与构造器不同的第一大优势在于,它们有名称 。​因为一个类只有一个带有指定签名的构造器,开发一般都是提供多个构造器避开这个原则,但是对于是使用者来说,往往不知道调取那个构造器。由于静态工厂方法有名称,...

weixin_39640646的博客 160

unet学习笔记(milesial/Pytorch-UNet)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 unet学习笔记(milesial/Pytorch-UNet)原理讲解unet_parts.py内容predict.py内容 所用到的代码Pytorch-UNet/predict.py at master · milesial/Pytorch-UNet ·GitHub 原理讲解 蓝/白色框表示 feature map; 蓝色箭头表示 3x3 卷积,用于特征提取; 灰色箭头表示skip-connection,用于特征融合; 红色箭头.

weixin_67138138的博客 2553

Effective Java 学习笔记——第二章

Effective Java学习笔记

码农的幸福生活 577

同步辐射材料科学应用指南:从XAFS原位实验到数据处理

材料结构分析是材料研究的核心环节,但传统实验室X射线受限于能量固定、亮度不足,往往难以回答原子局域配位、非晶结构及动态演变等关键问题。同步辐射光源以高通量、连续可调、偏振可控及脉冲时间结构等特性,将材料表征从静态观察推进到原位动态追踪层次。基于X射线吸收精细结构谱(XAFS)、原位实验、X射线衍射与散射、微区成像等技术,研究人员可实现元素选择性分析、近邻原子环境解析及真实工况下的结构演变监测。本文从技术原理出发,覆盖实验方案设计、样品制备、现场对光、数据处理与常见故障排查,并结合典型应用场景,为材料科学领域

weixin_32612263的博客 165

基于COMSOL电弧动态仿真模型优化:高收敛性可修改吹弧方案解析

在多物理场耦合分析中,电弧动态仿真历来是数值稳定性的难点:温度从室温跃升至数万开尔文,电导率随温度呈数量级跳变,流动、传热与电磁场相互牵制,极易引发求解器不收敛或初始值不一致等问题。如何构建高收敛性的数值模型,成为开关设备灭弧性能优化的重要技术基础。通过合理的几何简化、全耦合求解器配置、辅助扫描初始化策略以及参数化建模手段,可以实现对吹弧方案的灵活调整与批量寻优,从而系统性评估气流速度、喷口形状、触头间距等关键因素对熄弧性能的影响。这类仿真方法广泛应用于断路器、接触器等高压电器产品研发,为灭弧室结构设计提供

weixin_29758911的博客 350

effective java读书笔记

其中包含了单个返 void close 方法 Java 类库与第 方类库中的许多类和接口,现在都实现或扩展了AutoCloseable 接口 如果编写了 个类,它代表的是必须被关闭的资源,那么这个类也应该实现 AutoCloseable。静态工厂方法与构造器不同的第三大优势在子,它们可以返回原返回类型的任何子类型的对象, 子类的多态, 方法返回值类型为接口, 实际返回子类。getType像getInstaηce 样,但是在工厂方法处于不同的类中的时-候使用Type表示工厂方法所返回的对象类型。

Java坤坤的博客 136

光伏逆变器在环测试解决方案

光伏行业是利用太阳能光辐射将其转化为可用电能的行业。近年来,为应对气候变化和能源安全等挑战,全球对清洁能源的需求不断增长。太阳能作为最具潜力和可再生的能源之一,被视为替代传统化石能源的重要选择,光伏行业因此得到了迅猛发展,并在全球范围内得到了广泛应用。许多国家和地区都将光伏技术作为关键的能源发展方向。尤其是在新兴和发展中国家,光伏发电具有巨大的增长潜力,将成为未来市场竞争的重点。随着规模化生产的推进,光伏组件的制造成本不断降低。

shiwei0813的博客 960

Effective Java笔记

文章目录第二章:创建和销毁对象第1条: 静态工厂方法代替构造器优势1:它们有名称优势2:不必在每次调用它们的时候都创建一个新对象。优势3:它们可以返回原返回类型的任何子类对象。优势4:所返回的对象的类可以随着每次调用而发生变化优势5:方法返回的对象所属的类,在编写包含该静态方法的类时可以不存在。缺陷1:类如果不含公有或保护的构造器。就不能被子类化(继承)。#### 缺陷2:没有统一的标准。相对构造函数较难查明。 第二章:创建和销毁对象 第1条: 静态工厂方法代替构造器 优势1:它们有名称 更为清楚。 优势2

weixin_44677487的博客 535

逆变器、转换器、DCDC、继电器等器件的原理和作用

交流电变成直流电的过程:整流==》滤波==》稳压。

m0_61973119的博客 8030

YOLOv11改进 - C3k2融合 | C3k2融合融合WDBB宽度多样分支模块(Width Diverse Branch Block)通过多尺度分支增强特征表征能力

本文着眼于灰度图像目标检测所面临的挑战,提出了增强型目标检测网络 YOLO - MIF,该网络整合了多种多信息融合策略以对 YOLOv8 网络进行改进。文章开篇介绍了一种可创建伪多通道灰度图像的技术,此技术能增加网络的通道信息,同时缓解潜在的图像噪声与虚焦模糊问题。随后运用网络结构重新参数化技术,在不增加推断时间的情况下提升网络的检测性能。此外,引入了一种新颖的解耦式检测头,增强了模型处理灰度图像时的表现能力。

魔改工程师的博客 770

Effective Java通俗理解(下)

Effective Java通俗理解(上) 第31条:用实例域代替序数   枚举类型有一个ordinal方法,它范围该常量的序数从0开始,不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用实例域,例如: 1 /** 2 * 枚举类型错误码 3 * Created by yulinfeng on 8/20/17. 4 */ 5 public enum

airfish20000的博客 407

Smart PLC高速脉冲输入计米轮模块(编码器测速/计米详细讲解)

线缆行业单绞机PLC控制算法详细解读可以参看下面的文章链接:线缆行业单绞机控制算法(详细图解+代码)_RXXW_Dor的博客-优快云博客在了解单绞机之前需要大家对收放卷以及排线控制有一定的了解,不清楚的可以参看下面几篇博客,这里不再赘述,受水平和能力所限,文中难免出现错误和不足之处,诚恳的欢迎大家批评和指正。PLC张力控制(开环闭环算法分析)_RXXW_Dor的博客-优快云博客_plc闭环控制实例。SMART PLC时间间隔定时器应用(高速脉冲测频/测速)_RXXW_Dor的博客-优快云博客。

RXXW_Dor的博客 2819

《Thinking In Java》 VS 《Effective Java》 VS 《深入理解Java虚拟机》

今天刚考完试,在5天以前的前4个月时间里,我很庆幸自己选了一个看书的学习途径。从上个学期的暑假到现学期的11周里面,读了《Thinking In Java》、《深入理解Java虚拟机》和《Effective Java》,感悟颇多。首先先说明一下,我以前是看视频学习的,就做过两三个学校的网站,都已经上线在使用。没做过什么特别高大上的东西,所以我是处在新手的角度去读书的,我也知道自己的理解很浅,不

小浩子的博客 2万+

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

一. 初识Effective Java

这几天看了Effective Java,感觉这本书中讲解的Java中的一些方面帮助我们更深的理解Java,学习Java,所以决定把里面我觉得重要的点 记再这里。 第一章,简单通俗地讲到本书中大多数条目是通过程序示例进行说明。 许多的代码示例说明了 许多设计模式 和 习惯用法,还会有设计模式和习惯用法交叉引用。 1.代码规则:清晰性和简洁性最为重要; 模块的用户不应该被模块的行为...

Jason_LH1024的博客 290

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Effective Java阅读心得

最近再次拜读了Effective Java一书,确实很有感悟,第一次读的时候我的java基础还很不牢,代码积累也不多,读起来总是云里雾里,而这一次读的时候,确实有很多共鸣,感悟,在书上自己写了一些心得体会,俗话说好记性不如烂笔头,也记录到博客里,有需要的时候可以查阅一下第一章  引言引言部分就没啥说的了,就是介绍一下本书第二章  创建和销毁对象第一条:考虑用静态工厂方法代替构造器其实这里指的静态工...

Qunter的博客 2397

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

5天带你读完《Effective Java》(一)

Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说:“如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

长三月的游戏开发 2929

Java高并发effective

用户线程(User Thread)和守护线程(Daemon Thread) Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。 Daemo...

weixin_42765975的博客 215

快速带你看完《Effective Java》—— 创建和销毁对象篇

文章目录1 静态工厂代替构造器2 构造函数有多个参数时要考虑使用构建器3 使用private构造器或枚举类型强化Singleton属性4 使用privete的构造函数强化不可实例化的能力5 引用资源时应优先考虑依赖注入6 避免创建不必要的对象7 消除过期的对象引用8 避免使用终结方法和清除方法9 try-with-resources优先于try-finally 豆瓣评分9.8的图书《Effective Java》,是当今世界顶尖高手Josh Bloch的著作,在我之前的文章里我也提到过,编程就像练武,既需要

如切如磋,如琢如磨 879

【effective Java读书笔记】注解(一)

为什么许多面试Java都喜欢用这本书上的知识? 原因很简单,读懂这本书,其实考java深入一些就是考源码分析,这本书把jdk优秀的源码许多地方都有所涉及,并形成了自己的总结。我们只需要吸收这部分优秀的代码理念。 阅读本文之前,如果对注解了解较少,可以阅读这篇文章了解基本的概念。 什么情况下我们会自定义注解?书中提及的是工具铁匠,也就是给开发人员编写开发工具的开发人员会用到。但是,这并不妨碍我们学会理解使用它。

haibo_bear的博客 430

5天带你读完《Effective Java》(四)

Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说: “如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

AlbenXie的博客 96

Effective java / SQL / C# / Debugging / python

《Effective Java中文版(原书第3版)》 Java之父James Gosling鼎力推荐 Jolt获奖作品全新升级 大量完整的示例代码和透彻的技术分析 Java程序员的经典参考书 针对Java 7、8、9充分展示 新的Java编程语言及其类库特性,包括在其面向对象根部添加的函数编程构造 PC版:http://product.china-pub.com/8054944 移动版:http:...

互动出版网 215
上一篇: EXTJS系列笔记————gridpanel中的记录根据不同条件显示不同值。
下一篇: Effective Java通俗理解(下)
阿尔卑斯0283
博客等级 码龄14年 51粉丝 103原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值