【Java程序员必知必会的90个细节】1、创建和销毁对象

第一条:用静态工厂方法代替构造器

一、静态工厂方法的优势

1、静态工厂方法有名字。

2、不必在每次调用静态工厂方法的时候都创建一个对象。

3、静态工厂方法可以返回类型的任何子类型的对象。

这样我们在选择返回对象的类时就有了更大的灵活性。

这种灵活的一种应用是,API可以返回对象,同时又不会使对象的类变成共有的。以这种方式隐藏实现类会使API变得非常简洁。这种技术适用于基于接口的框架,因为这种框架中,接口为静态工厂方法提供了自然返回类型。

Java8仍要求接口的所有静态成员都必须是共有的。

在Java9中允许接口有私有的静态工厂方法,但是静态域和静态成员类仍然是共有的。

4、所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。

5、方法返回的独享所属的类,在编写包含该静态工厂方法的类时可以不存在。

这种灵活的静态工厂方法构成了服务提供者框架的基础,例如JDBC的API。

二、静态工厂方法的优缺点

1、类如果不含有公有的或受保护的构造器,就不能被子类化。

2、程序员很难发现它们。

三、静态工厂方法的惯用名称

1、from,类型转换方法

它只有单个参数,返回该类型的一个相对应的实例。

Date date = Date.from(instant);

2、of,聚合方法

带有多个参数,返回该类型的一个实例,把它们合并起来。

Set<Rank> faceCards = EnumSet.of(JACK,QUEEN,KING);

3、valueOf

比from和of更繁琐的一种替代方法。

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

4、instance或者getInstance,返回的实例是通过方法的参数来描述的。

StackWalker luke = StackWalker.getInstance(options);

5、create或者newInstance,和instance和getInstance一样,但create和newInstance能够确保每次调用都返回一个新的实例。

Object newArray = Array.newInstance(classObject,arrayLen);

 6、getType,像getInstance一样,但是在工厂方法处于不同类中的时候使用。Type表示工厂方法所返回的对象类型。

FileStore fs = Files.getFileStore(path);

7、newType,像newInstance一样。

8、type,getType和newType的简版

四、总结

简而言之,静态工厂方法和共有构造器都各有好处,我们需要理解它们各自的长处。

静态工厂方法更加合适,因此切忌第一反应就是提供公有的构造器,而不考虑静态工厂。

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

静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。

1、重叠构造器

重叠构造器可行,但是当许多参数的时候,客户端代码会很难编写并且难以阅读。

2、JavaBeans模式

先调用无参构造器来创建对象,然后调用setter方法来设置每个必要的参数。

JavaBeans模式是我常用的方式,但是JavaBeans也有着很严重的缺点。因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。类无法通过检验构造器参数的有效性来保证一致性。

3、建造者模式(并非设计模式中的建造者模式)

幸运的是,还有第三种替代方法。它既能像重叠构造器模式的安全性,又能保证像JavaBeans模式那样的可读性。这就是建造者模式。

package com.guor.effective.chapter2.worker;

public class Worker {
    private int id;
    private String name;

    private int age;
    private int sex;
    private String school;
    private String address;

    public static class Builder{
        private int id;
        private String name;

        private int age = 18;
        private int sex = 1;
        private String school = "辽宁石油化工大学";
        private String address = "辽宁省大连市高新园区";

        public Builder(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public Builder age(int val){
            age = val;
            return this;
        }

        public Builder sex(int val){
            sex = val;
            return this;
        }

        public Builder school(String val){
            school = val;
            return this;
        }

        public Builder address(String val){
            address = val;
            return this;
        }

        public Worker build() {
            return new Worker(this);
        }
    }

    public Worker(Builder builder){
        int id = builder.id;
        String name = builder.name;

        int age = builder.age;
        int sex = builder.sex;
        String school = builder.school;
        String address = builder.address;
    }

    @Override
    public String toString() {
        return "Worker{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                ", school='" + school + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}
package com.guor.effective.chapter2.worker;

public class Test {
    public static void main(String[] args) {
        Worker worker = new Worker
                .Builder(1,"郭晓彤").age(1).sex(1).school("中铁诺德幼儿园")
                .address("中铁诺德滨海花园").build();
        System.out.println(worker.toString());
    }
}

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

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

package com.guor.effective.chapter2.test;

public class Test2 {

    private Test2(){
        System.out.println("我是一个私有构造器");
    }

    public static void constructor() {
        System.out.println("我是一个工具类");
    }
}

【Java程序员必知必会的90个细节】1、创建和销毁对象

第5条:优先考虑依赖注入来引用资源

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

1、自动装箱

创建多余对象的方法,有一种叫自动装箱,它允许程序员将基本类型和封装类型混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除,它们在语义上还有着微妙的差别,在性能上也有着明显的差别。

举例说明,计算所有int正整数值的总和。

(1)基本类型性能测试

package com.guor.effective.chapter2.test;

public class getIntSum {
    public static void main(String[] args) {
        long a= System.currentTimeMillis();//获取当前系统时间(毫秒)
        long sum = 0;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println("int正整数之和:"+sum);
        System.out.println("程序执行时间为:"+(System.currentTimeMillis()-a)+"毫秒");
    }
}

【Java程序员必知必会的90个细节】1、创建和销毁对象

(2)封装类型性能测试 

【Java程序员必知必会的90个细节】1、创建和销毁对象

Long比long时间差的还是挺悬殊的。要优先使用基本类型,而不是封装类型,要当心无意识的自动装箱。

2、创建有意义的小对象

不要错误的认为此条所介绍内容暗示着“创建对象的代价非常昂贵,我们应该尽可能的创建对象”。相反,由于小对象的构造器只做少量的显示工作,所以小对象的创建和回收工作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象提升程序的清晰性、简洁性、功能性,这通常是件好事。反之,通过维护自己的对象池来避免创建对象,并不是一种好的做法,除非池中的对象是非常重量级的,正确使用对象池的典型示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。一般而言,维护自己的对象池,一般会将代码变得很乱,同时增加内存占用,而且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。

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

1、自己管理内存

只要类时自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放,该元素中包含的任何对象引用都应该被清空。

2、内存泄漏的另一种常见来源是缓存。

一旦把对象放到缓存中,就有可能被遗忘,长时间留在缓存中。对于这个问题,可以使用WeakHashMap代表缓存,当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

3、内存泄漏的第三种来源是监听器和其它回调

如果你实现一个API,客户端在这个API中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则它们就会不断地堆积起来。确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用,例如,只讲它们保存成WeakHashMap中的键。

由于内存泄漏不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,如果能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。

第8条:避免使用终结方法和消除方法

1、终结方法和消除方法

终结方法通常是不可预测的,也是很危险的,一般情况下是不必要的。

消除方法没有终结方法那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

2、永远不应该依赖终结方法或消除方法来更新重要的持久状态。

3、终结方法finalize()

finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。

虽然以上以对象救赎举例,但finalize()的作用往往被认为是用来做最后的资源回收。
基于在自我救赎中的表现来看,此方法有很大的不确定性(不保证方法中的任务执行完)而且运行代价较高。所以用来回收资源也不会有什么好的表现。

4、终结方法攻击

终结方法攻击:

如果从构造器或者它的序列化对等体抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途而废的对象上运行。这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松地在这个对象上调用任何原本永远不允许在这里出现的方法,这种方法的调用就是终结方法攻击。final类不会受到终结方法攻击,因为没有人能够写出final类的恶意子类。为了防止非final类受到终结方法攻击,要编写一个空的final的finalize方法。

5、try-with-resources

既然存在终结方法攻击,一般可以让类实现AutoCloseable,并要求器客户端在每个实例不再需要的时候调用close方法,一般利用try-with-resources确保终止。

6、那么终结方法和消除方法有什么好处呢?

(1)当close忘记被调用时,终结方法或消除方法可以充当“安全网”。

(2)回收本地对等体。

本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象。因为本地对等体不是一个普通对象,因此不能被垃圾回收,此时可以用终结方法或消除方法进行回收。

第9条:try-with-resources优先于try-fanally

try-with-resources语句是一种声明了一种或多种资源的try语句。资源是指在程序用完了之后必须要关闭的对象。try-with-resources语句保证了每个声明了的资源在语句结束的时候都会被关闭。任何实现了java.lang.AutoCloseable接口的对象,和实现了java.io.Closeable接口的对象,都可以当做资源使用。

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

在这个例子中,try-with-resources语句种声明的是BufferedReader资源。声明语句紧跟着在try关键词的圆括号里面。BufferedReader从Java SE7开始就实现了java.lang.AutoCloseable接口。因为BufferedReader声明在了try-with-resources里面,所以无论try语句是正常结束还是异常结束(比方说BufferedReader.readLine方法抛出一个IOException异常),它都会被关闭。

下面的例子用try-with-resources语句自动关闭一个java.sql.Statement对象:

public static void viewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");
            System.out.println(coffeeName + ", " + supplierID + ", " +
                              price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

注意:try-with-resources语句也可以像普通的try语句一样,有catch和finally代码块。在try-with-resources语句中,任何的catch和finally代码块都在所有被声明的资源被关闭后执行。

相关文章

暂无评论

暂无评论...