• 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 + "]";
        }
    
    }
  • 相关阅读:
    OC内存管理
    摘要算法
    加密算法
    编码技术
    Golang遇到的一些问题总结
    SignalR
    uni-app 小程序 vue
    C# 调用 C++ dll的两种方式
    Vue 项目 VSCode 调试
    Navicat 导出 表结构
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/11010509.html
Copyright © 2020-2023  润新知