Effective.Java第1-11条

1. 考虑使用静态工厂方法替代构造方法

  一个类可以提供一个公共静态工厂方法,它只是返回类实例的静态方法。例如JDK的Boolean的valueOf方法:

public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean>
{
    public static final Boolean TRUE = new Boolean(true);

    public static final Boolean FALSE = new Boolean(false);

    public Boolean(boolean value) {
        this.value = value;
    }

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
...
}

  静态工厂方法与设计模式中的工厂方法模式不同。

优点:

(1)静态工厂方法不像构造方法,它们有名字,语义清晰

(2)静态工厂方法不需要每次调用时都创建对象。例如返回静态对象,避免不必要的创建对象。

(3)静态工厂可以返回其返回类型的任何子类型的对象。

(4)其返回类型可以根据参数的不同而不同,比如EnumSet类的下面方法:

    public static <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c) {
        if (c instanceof EnumSet) {
            return ((EnumSet<E>)c).clone();
        } else {
            if (c.isEmpty())
                throw new IllegalArgumentException("Collection is empty");
            Iterator<E> i = c.iterator();
            E first = i.next();
            EnumSet<E> result = EnumSet.of(first);
            while (i.hasNext())
                result.add(i.next());
            return result;
        }
    }

(5)在编写包含该方法的类时,返回的对象的类不需要存在。这种灵活的静态工厂方法构成了服务提供者框架的基础。比如Java数据库连接JDBC。由对应的厂商提供具体的实现类。

缺点:

(1)如果类不含public或protected的构造方法,将不能被继承。

(2)与其它普通静态方法没有区别,没有明确的标识一个静态方法用于实例化类。也就是API不是非常全。所以,一般一个静态工厂方法需要有详细的注释,遵守标准的命名。如下:

from——A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:Date d = Date.from(instant);
of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:Set faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf——from 和 to 更为详细的替代 方式,例如:BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:StackWalker luke = StackWalker.getInstance(options);
create 或 newInstance——与 instance 或 getInstance 类似,除了该方法保证每个调用返回一个新的实例,例如:Object newArray = Array.newInstance(classObject, arrayLen);
getType——与 getInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:FileStore fs = Files.getFileStore(path);
newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:BuweredReader br = Files.newBuweredReader(path);
type—— getType 和 newType 简洁的替代方式,例如:List litany = Collections.list(legacyLitany);

2. 当构造方法参数过多时考虑构造模式

比如一个User,有好多属性,但是只有ID是必须有的,其他属性可有可无。如下:

package cn.qlq.builder;

public class User {

    // 必须字段
    private int id;

    private String name;
    private String sex;
    private String job;
    private String health;
    private String BMI;
    private int height;
    private int weight;

    public User() {
        super();
    }

    public User(int id, String name, String sex, String job, String health, String bMI, int height, int weight) {
        super();
        this.id = id;
        this.name = name;
        this.sex = sex;
        this.job = job;
        this.health = health;
        BMI = bMI;
        this.height = height;
        this.weight = weight;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public String getHealth() {
        return health;
    }

    public void setHealth(String health) {
        this.health = health;
    }

    public String getBMI() {
        return BMI;
    }

    public void setBMI(String bMI) {
        BMI = bMI;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", job=" + job + ", health=" + health + ", BMI="
                + BMI + ", height=" + height + ", weight=" + weight + "]";
    }

}

当我们设置几个属性的时候可以通过构造方法进行创建,但是比如我们只想设置一些属性,其他属性没用,我们可能会写成空值,这样的代码阅读起来很难懂,属性更多的时候更加难以阅读:

User user = new User(1, "张三", "", "", "", "", 0, 0);

也有可能通过setter进行设值,如下:(属性更多的时候需要更多的setter)

        User user = new User();
        user.setId(1);
        user.setName("xxx");
        user.setBMI("XXX");
    ...

解决办法:采用建造模式 + 流式写法

  由于是用Builder模式来创建某个对象,因此就没有必要再定义一个Builder接口,直接提供一个具体的建造者类就可以了。
  对于创建一个复杂的对象,可能会有很多种不同的选择和步骤,干脆去掉“导演者”,把导演者的功能和Client的功能合并起来,也就是说,Client这个时候就相当于导演者,它来指导构建器类去构建需要的复杂对象。

package cn.qlq.builder;

public class UserBuilder {

    private User user = new User();

    /**
     * 构造方法确保ID必有
     * 
     * @param id
     */
    public UserBuilder(int id) {
        user.setId(id);
    }

    UserBuilder name(String name) {
        user.setName(name);
        return this;
    }

    UserBuilder sex(String sex) {
        user.setSex(sex);
        return this;
    }

    UserBuilder job(String job) {
        user.setJob(job);
        return this;
    }

    UserBuilder health(String health) {
        user.setHealth(health);
        return this;
    }

    UserBuilder BMI(String BMI) {
        user.setBMI(BMI);
        return this;
    }

    UserBuilder height(int height) {
        user.setHeight(height);
        return this;
    }

    UserBuilder weight(int weight) {
        user.setWeight(weight);
        return this;
    }

    public User build() {
        if (user.getId() == 0) {
            throw new RuntimeException("id必须设置");
        }

        return user;
    }

}

客户端代码:

package cn.qlq.builder;

public class MainClass {

    public static void main(String[] args) {
        UserBuilder userBuilder = new UserBuilder(2);
        User user = userBuilder.name("张三").BMI("xxx").health("健康").build();
        System.out.println(user);
    }

}

  这样的代码读起来也舒服,语义也更好理解。

3.使用私有构造方法或枚举类实现Singleton属性

(1)使用静态工厂方法实现

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
。。。
}

  如果需要防御通过反射创建对象,可以在构造方法中判断,当currentRuntime != null 的时候禁止创建(抛出一个RuntimeException).

(2)枚举类实现

package cn.qlq.thread.fifteen;

/**
 * 枚举实现单例模式
 * 
 * @author Administrator
 *
 */
public enum Singleton_6 {

    instance;

    private Singleton_6() {
        System.out.println("调用构造方法");
    }

    public Singleton_6 getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(Singleton_6.instance.getInstance());
    }
}

4.使用私有构造方法执行非实例化

  我们经常会使用一些只包含静态方法和静态属性的类。这样的类获得了不好的名声,因为有些人滥用这些类而避免以面向对象的方式思考,但是它们确实有着特殊的用途。比如说工具类或者说常量工具类。

  JDK8开始,接口也允许有默认的静态方法和default方法。

  这样的类不允许被实例化:一个实例是没有意义的。然而,在没有显示构造方法的情况下,编译器提供了一个公共的、无参的默认构造方法。因为当类不包含显示构造方法的时候,才会生成一个默认的构造方法,因此可以通过包含一个私有构造方法来实现类的非实例化:

public class DocUtils {

    // 静止实例化
    private DocUtils() {
        throw new RuntimeException();
    }

}

 

 5.  依赖注入优于硬链接资源

  许多类依赖一个或者多个底层资源。如:拼写检查类依赖于一个字典类.

常见一:将此类实现为静态实用工具类

public class SpellChecker {
    private static final Lexicon dictionary = ...;
    private SpellChecker() {} // Noninstantiable
    public static boolean isValid(String word) { ... }
        public static List suggestions(String typo) { ... }
    }

}

常见二:实现为单例

public class SpellChecker {
    private final Lexicon dictionary = ...;

    private SpellChecker(...) {}
    public static INSTANCE = new SpellChecker(...);

    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

  上面两种做法都令人不满意,因为它们假设只有一本字典。

  最高效的做法是使 dictionary  属性设置为非final,并且通过一个方法改变此属性,以实现支持多个字典。

  依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。

// Dependency injection provides flexibility and testability
public class SpellChecker {
    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }

    public boolean isValid(String word) { ... }
    public List<String> suggestions(String typo) { ... }
}

6.  避免创建不必要的对象

  每次需要时重用一个对象而不是创建一个新的功能相同的对象。常用的如静态工厂方法模式,如:

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

例如:

String s = new String("str");

  每次执行都会创建一个String对象,而这些对象并不是必须的,建议改为:

String s = "str";

  优先使用基本类型而不是装箱的基本类型,也要注意无意识地装箱。

7.  消除过期的对象引用

  简单的理解为:一旦对象引用过期,将其设为null。一个好处就是如果之后被错误的引用会抛出NPE。

8.  避免使用Finalizer和Cleaner机制

  Java9中用Cleaner代替了Finalizer,但是Finalizer仍被保留使用。

  注意不能把Java中的Finalizer和Cleaner当成c++的析构函数。在c++中,析构函数是正确的回收对象相关资源方式,是与构造方法对应的。在Java中,当一个对象不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外工作。C++析构函数也被用来回收其他内存资源。Java中,try-finally或者try-with-resources用于此目的。

  Finalizer和Cleaner的一个缺点是不能保证他们能够及时地执行。也就是说当对象引用不可达时,这两个方法的执行时间是不固定的,所以不应该做任何业务相关代码。

  不能相信System.gc() 和 System.runFinalization() 等方法,他们也只是简单的通知进行GC,并不会马上GC。

9.  使用 try-with-resources语句替代 try-finally 语句

  关闭流的方式有很多种方式,一般都是手动关闭,比如 InputStream、OutputStream和Java.sql.Connection。

package cn.qlq;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainClass {

    public static void main(String[] args) {
        File file = new File("/usr/test.txt");
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(file);
            outputStream = new FileOutputStream(file);
            // 使用流
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

}

再次升级变为如下方式,有点类似于IOUtils的方法:

package cn.qlq;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainClass {

    public static void main(String[] args) {
        File file = new File("/usr/test.txt");
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = new FileInputStream(file);
            outputStream = new FileOutputStream(file);
            // 使用流
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            closeQuietly(outputStream);
            closeQuietly(inputStream);
        }

    }

    private static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                // ignored
            }
        }
    }

}

  上面代码比较冗余,而且不易阅读,Java7引入了 try-with-resources。代码阅读比较简单,而且提供了更好的诊断。如下:(建议这种写法,当然catch代码块可以不写,一般用于记录一些信息)

    public static String readValue(String filePath, String defaultValue) {
        File file = new File("/usr/test.txt");
        try (InputStream inputStream = new FileInputStream(file);
                OutputStream outputStream = new FileOutputStream(file)) {

            // 读取文件返回值
            return "";
        } catch (Throwable e) {
            // 记录日志
            return defaultValue;
        }
    }

10.  重写equals()方法时遵守通用约定

按照约定,equals方法要满足以下规则。

自反性:  对于任何非空引用x,x.equals(x) 一定是true

对称性: 对于非空引用x和y, x.equals(y)  和  y.equals(x)结果一致

传递性:  a 和 b equals , b 和 c  equals,那么 a 和 c也一定equals。

一致性: 对于任何非空引用x和y,如果在equals比较中使用的信息没有被修改,则x.equals(y) 的多次调用必须始终返回true或者false。

非空性: 对于任何非空引用x,  x.equals(null) 一定是false

同时,也有一些提醒,比如:

(1)重写了euqals方法的对象必须同时重写hashCode()方法。

(2)在equals方法中,不要将参数的Object类型换成其他类型。

编写高质量equals()方法的建议:

(1)使用==运算符检查参数是否为该对象的引用。如果是,返回ttrue。这只是一种性能优化。

(2)使用 instanceof 来检查参数是否是正确的类型,如果不是返回false。

(3)参数转换为正确的类型。

(4)对于每个类的重要属性,在equals中进行比较。

  对于类型为非float或double的基本类型,使用==运算符比较;对于对象引用属性,递归地调用equals方法;对于float基本类型的属性,使用Float.compare(float, float)方法;对于double基本类型的属性,使用Double.compare(double, double)。

  在很多情况下,不要重写equals方法,从Object继承的完全是想要的。如果确实重写了equals()方法,那么一定要比较这个类的所有属性,并且遵守上面五条规则。

  在比较两个对象是否是同一个对象的时候用equals,不用==。

11.  重写了euqals方法的对象必须同时重写hashCode()方法。

  这个方法返回对象的散列码,返回值是int类型的散列码。对象的散列码是为了更好的支持基于哈希机制的Java集合类,例如 Hashtable, HashMap, HashSet 等。

  等价的(调用equals返回true)对象必须产生相同的散列码。不等价的对象,不要求产生的散列码不相同。

  假设只重写equals而不重写hashcode,那么类的hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的,所以会导致equals相等的两个对象的hasCode()不一定相等,违反了上面的约定。

  

 例如:使用commons-lang包自带的工具类编写equals和hasCode方法:

package cn.qlq;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

public class User implements Cloneable {

    private int age;
    private String name, sex;

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (obj == this) {
            return true;
        }

        if (!(obj instanceof User)) {
            return false;
        }

        final User tmpUser = (User) obj;
        return new EqualsBuilder().appendSuper(super.equals(obj)).append(name, tmpUser.getName()).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(name).toHashCode();
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "User [age=" + age + ", name=" + name + ", sex=" + sex + "]";
    }

}
原文地址:https://www.cnblogs.com/qlqwjy/p/11010509.html