Effective Java》读后感

1       创建和销毁对象

1.1    考虑用静态工厂方法代替构造器

静态工厂方法优点:

  • 静态工厂方法与构造器(构造方法)不同的第一大优势在于,它们有名称。见名知意,突出区别。

  • 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。

  • 静态工厂方法与构造器不同的第三大优势在于,它们可以返回原返回类型的任何子类型的对象。

  • 静态工厂方法与构造器不同的第四大优势在于,在创建参数化类型实例的时候,它们使代码变得更加简洁。

例如:Map<String,List<String>> map=newHashMap<String,List<String>>();

假设HashMap提供了静态工厂方法:(目前是没有该静态方法)

public static <K,V> HashMap<K,V> newInstance(){

         return newHashMap<K,V>();

}

上面的代码就可以简写为:Map<String,List<String>> map= HashMap.newInstance();

静态工厂方法缺点:

主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化;

第二个缺点在于,它们与其他的静态方法实际上没有任何区别。

1.2    遇到多个构造器参数时要考虑用构建器

假如我们的一个实体类有很多的属性值,但是这些属性值又是可选的。如果我们遇到这样的是类,如何设计出方便的实体类呢?

 

  • 通常解决办法一:重叠构造器

 

复制代码

 

public class User {

 

   private String id; // id(必填)

 

   private String name; // 用户名(必填)

 

   private String email; // 邮箱(可选)

 

   private int age; // 年龄(可选)

 

   private String phoneNumber; // 电话(可选)

 

   private String address; // 地址(可选)

 

   public User(String id, String name) {

       this(id, name, "qq.com", 0, "120", "广州");

    }

 

   public User(String id, String name, String email) {

       this(id, name, email, 0, "120", "广州");

    }

 

   public User(String id, String name, String email, int age) {

       this(id, name, email, age, "120", "广州");

    }

 

   public User(String id, String name, String email, int age, StringphoneNumber) {

       this(id, name, email, age, phoneNumber, "广州");

    }

 

   public User(String id, String name, String email, int age, StringphoneNumber, String address) {

       this.id = id;

       this.name = name;

       this.email = email;

       this.age = age;

       this.phoneNumber = phoneNumber;

       this.address = address;

    }

 

   public String getId() {

       return id;

    }

 

   public String getName() {

       return name;

    }

 

   public String getEmail() {

       return email;

    }

 

   public int getAge() {

       return age;

    }

 

   public String getPhoneNumber() {

       return phoneNumber;

    }

 

   public String getAddress() {

       return address;

    }

}

 

注:许多你不想设置的参数,但是还不得不为他们传递值

 

  • 通常解决办法二: JavaBean模式

调用一个无参构造器来创造对象,然后调用setter方法来设置每个必须的参数,以及每个相关的可选参数

 

 

复制代码

 

public class User {

 

   private String id; // id(必填)

 

   private String name; // 用户名(必填)

 

   private String email; // 邮箱(可选)

 

   private int age; // 年龄(可选)

 

   private String phoneNumber; // 电话(可选)

 

   private String address; // 地址(可选)publicUser() {

       super();

    }

 

   public void setId(String id) {

       this.id = id;

    }

 

   public void setName(String name) {

       this.name = name;

    }

 

   public void setEmail(String email) {

       this.email = email;

    }

 

   public void setAge(int age) {

       this.age = age;

    }

 

   public void setPhoneNumber(String phoneNumber) {

       this.phoneNumber = phoneNumber;

    }

 

   public void setAddress(String address) {

       this.address = address;

    }

 

   public String getId() {

       return id;

    }

 

   public String getName() {

       return name;

    }

 

   public String getEmail() {

       return email;

    }

 

   public int getAge() {

       return age;

    }

 

   public String getPhoneNumber() {

       return phoneNumber;

    }

 

   public String getAddress() {

       return address;

    }

 

   @Override

   public String toString() {

       return "User [id=" + id + ", name=" + name + ",email=" + email + ", age=" + age + ", phoneNumber="

                + phoneNumber + ",address=" + address + "]";

    }

}

 

注:JavaBeans模式自身有着很严重的缺点。因为构造过程被分到几个调用中,在构造过程中JavaBean可能处于非一致的状态。JavaBeans模式阻止了把类做成不可变的可能,这就需要确保他的线程安全。

 

 

  • 解决办法三:构建器

 

 

复制代码

 

public class User {

 

   private String id;                // id(必填)

 

   private String name;            // 用户名(必填)

 

   private String email;            // 邮箱(可选)

 

   private int age;                // 年龄(可选)

 

   private String phoneNumber;     //电话(可选)

 

   private String address;         //地址(可选)

 

 

   public static class Builder{

       

       private String id;                // id(必填)

 

       private String name;            // 用户名(必填)

 

       private String email;            // 邮箱(可选)

 

       private int age;                // 年龄(可选)

 

       private String phoneNumber;     //电话(可选)

 

       private String address;         //地址(可选)

 

       public Builder(String id, String name) {

           super();

           this.id = id;

           this.name = name;

       }

       

       public Builder email(String email){

           this.email = email;

           return this;

       }

       public Builder age(int age){

           this.age = age;

           return this;

       }

       public Builder phoneNumber(String phoneNumber){

           this.phoneNumber = phoneNumber;

           return this;

       }

       

       public Builder address(String address){

           this.address = address;

           return this;

       }

       public User builder(){

           return new User(this);

       }

    }

   private User(Builder builder){

       this.id = builder.id;

       this.name = builder.name;

       this.email = builder.email;

       this.age = builder.age;

       this.phoneNumber = builder.phoneNumber;

       this.address = builder.address;

    }

   

   @Override

   public String toString() {

       return "User [id=" + id + ", name=" + name + ",email=" + email + ", age=" + age + ", phoneNumber="

                + phoneNumber + ",address=" + address + "]";

    }

 

   public String getId() {

       return id;

    }

 

   public String getName() {

       return name;

    }

 

   public String getEmail() {

       return email;

    }

 

   public int getAge() {

       return age;

    }

 

   public String getPhoneNumber() {

       return phoneNumber;

    }

 

   public String getAddress() {

       return address;

    }

}

 

 注:不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个builder对象。然后客户端在builder对象上调用类似于setter的方法,来设置每个相关的可选参数。最后,客户端调用无参的build方法来生成不可变的对象。

 

测试代码

 

   public static void main( String[] args )

    {

       User user = new User.Builder(UUID.randomUUID().toString(),"parry").address("广州").builder();

       System.out.println(user.toString());

    }

1.3    用私有构造器或者枚举类型强化Singleton属性

1.4    通过私有构造器强化不可实例化的能力

1.5    避免创建不必要的对象

  • 例如:Strings=new String(“abc”); // Don’t do this

该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数(”abc”),本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这个方法在一个循环中,就会创建成千上万的不必要的String实例。

正确的写法:String s=”abc”;

  • Java1.5版本的新特性中,有一种创建多与对象的新方法:自动装箱!

例如:

Long sum=0L;

for(long i=0; i<Integer.MAX_VALUE; i++){

         sum+= i;

}

system.out.println(sum);

执行以上代码,耗时大约43秒;

这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为变量sum使用的Long类型,而不是long,这意味着程序构造了大约231次方个多余的Long实例。如果将Long改为基本类型long。程序运行时长为6.8秒附近。

结论很明显:优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。

1.6    消除过期的对象引用

内存泄漏:如果一个栈先是增长,然后再收缩,那么从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。因为,栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除引用。

出现内存泄漏的三种需要注意的情况:

一:类是自己管理内存;

二:缓存; 把对象放入缓存中,很容易被遗忘。

三:监听器和其他回调。

1.7    避免使用终结方法

2       对于所有对象都通用的方法

尽管Object是一个具体类,但是设计它主要是为了扩展。它的所有非final方法(equalshashCodetoStringclonefinalize)都用明确的通用约定,因为它们被设计成要被覆盖(override)的。

2.1    覆盖equals时请遵守通用约定

equals方法实现了等价关系:

  • 自反性(reflexive:对于任何非null的引用值xx.equals(x)必须返回true

  • 对称性(symmetric:对于任何非null的引用值xy,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true

  • 传递性(transitive:对于任何非null的引用值xyz,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)必须返回true

  • 一致性(consistent:对于任何非null的引用值xy,只要equals的比较操作再对象中所有的信息没有被修改,多次调用x.equals(y)就会一致地返回true,或者一致的返回false

  • 非空性(Non-nullity):对于任何非null的引用值xx.equals(null)必须返回false

  •     覆盖equals时总要覆盖hashCode

引用java API,关于ObjecthashCode方法的一段话:

返回该对象的哈希码值。支持此方法是为了提高哈希表(例如 java.util.Hashtable 提供的哈希表)的性能。

hashCode 的常规协定是:

 

  • Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。

  • 如果根据equals(Object) 方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。

  • 如果根据equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

  •     始终要覆盖toString

  •     谨慎覆盖clone

  •     考虑实现Comparable接口

3       类和接口

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

对于成员(域、方法、嵌套类和嵌套接口)有四种可访问级别:下面根据可访问性递增顺序说明:

  • 私有的(private:只有在声明该成员的顶层类内部才可以访问这个成员;

  • 包级私有(package-private):声明该成员的包内部的任何类都可以访问这个成员。从技术上讲,它被称为“缺省访问级别”;

  • 受保护的(protected):声明该成员的类的子类可以访问这个成员(但是有一些限制[JLS6.6.2])。

  • 公有的(public:在任何地方都可以访问。

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

  •     使可变性最小化

为了使类称为不可变,要遵循下面五条规则:

1,  不要提供任何会修改对象状态的方法;

2,  保证类不会被扩展;

3,  使所有的域都是final的;

4,  使所有的域都称为私有的;

5,  确保对于任何可变组件的互斥访问

3.4    复合优先于继承

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

3.6    接口优于抽象类

现有的类可以很容易被更新,以实现新的接口;

接口是定义mixin(混合类型)的理想选择;

接口允许我们构造非层次结构的类型框架。

3.7    接口只用于定义类型

3.8    类层次优于标签类

3.9    用函数对象表示策略

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

3.10        优先考虑静态成员类

4       泛型

4.1    请不要在新代码中使用原生态类型

4.2    消除非受检警告

4.3    列表优先于数组

4.4    优先考虑泛型和泛型方法

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

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

5       枚举和注解

5.1    enum代替int常量

5.2    用实例域代替序数

5.3    EnumSet代替位域

5.4    EnumMap代替序数索引

5.5    注解优先于命名模式

5.6    用标记接口定义类型

6       方法

6.1    检查参数的有效性

6.2    必要时进行保护性拷贝

6.3    谨慎设计方法签名

谨慎地选择方法的名称;见名知意

不要过于追求提供便利的方法;

避免过长的参数列表;

6.4    慎用重载

6.5    慎用可变参数

6.6    返回零长度的数组或者集合,而不是null

6.7    为所有导出的API元素编写文档注释

7       通用程序设计

7.1    将局部变量的作用域最小化

7.2    for-each循环优先于for循环

无法使用for-each循环的三种情况:

1,过滤:如果需要遍历集合,并删除选定的元素,就需要使用显式迭代器,以便可以调用它的remove方法;

2,转换:如果需要遍历列表或者数组,并取代它部分或者全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值

3,平行迭代:如果需要并行的遍历多个集合,需要显式的控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移

以上情况,需要使用普通for循环。

7.3    了解和使用类库

7.4    精确的答案,避免使用floatdouble

精确计算,推荐使用BigDecimal(性能会相对慢一些)

7.5    基本类型优先于装箱基本类型

7.6    如果使用其他类型更合适,避免使用string类型

字符串不适合代替其他的值类型;

字符串不适合代替枚举类型;

字符串不适合代替聚集类型;

字符串不适合代替能力表;

7.7    当心字符串连接的性能

为了获得可以接收的性能,请使用StringBuilderStringBuffer代替String进行字符串的拼接。

7.8    通过接口引用对象

7.9    接口优先于反射机制

使用反射机制的缺点:

1,丧失了编译时类型检查的好处,包括异常检查;

2,执行反射访问所需要的代码非常笨拙和冗长。

2,性能损失。反射方法调用比普通方法调用慢了很多。

7.10        谨慎的调用本地方法

7.11        谨慎的进行优化

不要去计较效率上的一些小小的得失,在97%的情况下,不成熟的优化才是一切问题的根源。

 

在优化方面,我们应该遵守两条规则:

1,  不要进行优化;

2,  还是不要进行优化;也就是说在你没有绝对清晰的优化方案之前,请不要进行优化。

 

总结:不要费力编写快速的程序,应该努力编写好的程序,速度自然随之而来。

7.12        遵守普遍接受的命名惯例

8       异常

8.1    只针对异常的情况才使用异常

8.2    对可恢复的情况使用受检异常,对编程错误使用运行时异常

8.3    避免不必要的使用受检异常

8.4    优先使用标准的异常

8.5    抛出与抽象相对应的异常

8.6    每个方法抛出的异常都要有文档或注释

8.7    在细节消息中包含能捕获失败的信息

8.8    努力让失败保持原子性

8.9    不要忽略异常

9       并发

9.1    同步访问共享的可变数据

9.2    避免过度同步

9.3    executortask优先于线程

9.4    并发工具优先于waitnotify

9.5    线程安全性的文档化

9.6    慎用延迟初始化

9.7    不要依赖线程调度器

9.8    避免使用线程组

10  序列化

10.1        谨慎实现Serializable接口

缺点是:

1,  类实现该接口之后,一旦一个类被发布,就大大降低了“改变该类的实现”的灵活性。

2,  它增加了出现bug和安全漏铜的可能性;

3,随着类发行新的版本,相关的测试负担也增加了。

10.2        考虑使用自定义的序列化形式

10.3        保护性地编写readObject方法

Mutableperiod***。

编写更加健壮的readObject方法:

  • 对于对象引用域必须保持为私有的类,要保护性的拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别;

  • 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。

  • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口

  • 无论是直接还是间接方式,都不用调用类中任何可被覆盖的方法

  •         对于实例控制,枚举类型优先于readResolve

如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为tansient的。否则,会被Mutableperiod***。

10.5        考虑用序列化代理代替序列化实例