`
chasegalaxy
  • 浏览: 5315 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Effective Java学习笔记(二)

    博客分类:
  • Java
阅读更多
    第二章:创建和销毁对象(条目1-7)
    第1条:考虑用静态工厂方法代替构造器
    静态工厂方法(static factory method)的几大优势:
    1.有名称,能够根据名称知其意,比如:Boolean类的valueOf方法和BigInteger类的probablePrime方法,而使用构造器的话,在含多个构造器时,在不看注释情况下往往不知道该选用哪个。下面是两个例子:
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
    
    public static BigInteger probablePrime(int bitLength, Random rnd) {
        if (bitLength < 2)
            throw new ArithmeticException("bitLength < 2");

        // The cutoff of 95 was chosen empirically for best performance
        return (bitLength < SMALL_PRIME_THRESHOLD ?
                smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) :
                largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
    }

    2.不必在每次调用时都创建一个新对象。
    静态工厂方法能够为重复的调用返回相同对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称为实例受控的类(instance-controlled)。实例受控使得类可以确保它是一个Singleton或者是不可实例化的。
    3.可以返回原返回类型的任何子类型的对象,适用于基于接口的框架(interface-based framework)。下面EnumSet类的noneOf方法就是根据元素个数来返回不同的子类对象的:
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<E>(elementType, universe);
        else
            return new JumboEnumSet<E>(elementType, universe);
    }

    在基于接口的框架中,静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不必存在。这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础。服务提供者框架中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册API(Provider Registration API),这是系统用来注册实现,让客户端访问它们的;服务访问API(Service Access API),是客户端用来获取服务的实例的,第四个组件可选组件服务提供者接口(Service Provider Interface)。典型例子是JDBC API:Connnection是服务接口,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver就是服务提供者接口。
    4.在创建参数化类型实例时,使得代码更简洁。
    HashMap中如果提供如下静态工厂,也就提供了类型推导(type inference),则会更好。
    public static <K, V> HashMap<K, V> newInstance() {
        return new HashMap<K, V>();
    }

    建议在自己的参数化类中(含泛型T),使用以上方法进行实例化。
静态工厂方法的缺点:
    1.类如果不含public/protected的构造器,则不能被子类化。但这样也许会因祸得福,因为鼓励程序用组合(Composition),而不是继承(Inheritance)。
    2.它们实际上和其他静态方法没有任何区别。在API文档中,它们没有像构造器那样在API文档中明确标识出来。所以需要在注释中关注静态工厂方法,添加注释,并遵守标准命名。下面是静态工厂方法的一些惯用名称:
    valueOf:实际上是类型转换方法;
    of:valueOf的简洁形式,在EnumSet中开始流行;
    getInstance:返回的实例根据参数来描述的,对Singleton来说,该方法无参数并返回唯一实例;
    newInstance:像getInstance一样,但newInstance能够确保返回的每个实例与其他实例不同;
    getType:像getInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。
    newType:像newInstance一样,但是在工厂方法处于不同的类中的时候使用。Type表示工厂方法所返回的对象类型。

    第2条:遇到多个构造器参数时要考虑用构造器
    静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。程序员一向习惯用重叠构造器(telescoping constructor)模式。
    重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写(有时会不小心颠倒两个参数顺序,但无编译错误),并且仍然比较难以阅读。
    所以有了第二种:JavaBeans模式,这种模式弥补了重叠构造器的不足。
    但JavaBeans模式自身有着很严重的缺点,因为构造过程被分到了几个调用中,在构造过程中JavaBean可能处于不一致的状态。另外一个缺点就是,JavaBeans模式组织了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
幸运的是,还有第三种模式,就是Builder模式,给出一个示例基本可以理解:
/**
 * 学生类
 * @author chasegalaxy
 * @since 2012-08-01
 */
public class Student {
    private final String code;
    private final String name;
    private final int age;
    private final String email;

    public static class Builder {
        // Required parameters
        private final String code;
        private final String name;

        // Optional parameters - initialized to default values
        private int age = 0;
        private String email = null;

        public Builder(String code, String name) {
            this.code = code;
            this.name = name;
        }
        public Builder age(int val) {
            age = val;
            return this;
        }
        public Builder email(String val) {
            email = val;
            return this;
        }
        public Student build() {
            return new Student(this);
        }
    }
    private Student(Builder builder) {
        code = builder.code;
        name = builder.name;
        age = builder.age;
        email = builder.email;
    }
    // getter goes here...(no setter)
}

    调用方式如下(其中code和name是必须参数,age和email可选,这里只填充了age):
Student obj = new Student.Builder("08109080", "张三").age(11).build();

    设置了参数的builder生成了一个很好地抽象工厂(Abstract Factory),JDK1.5+可使用泛型,代码如下:
// A builder for objects of type T
public interface builder<T> {
    public T build();
}

    带有Builder实例的方法通常利用有限制的通配符类型(bounded wildcard type)来约束构建器的参数类型,比如下面就是构建每个节点的方法,它利用客户端提供的Builder实例来构建树:
Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }

    Builder模式的不足是多了创建构建器的开销,但一般情况下不会非常注重性能。所以给出绝大多数情况都适用的结论:
    如果类的构造器或静态工厂中具有多个参数时,Builder模式就是种不错的选择,特别是当大多数参数都是可选的时候。与传统的重叠构造器模式相比,Builder模式更容易编写和阅读,也比JavaBeans更加安全。

    第3条:用似有构造器或者枚举类型强化Singleton属性
    Singleton指仅仅被实例化一次的类,通常用来表示那些本质上唯一的组件。下面给出用似有构造器强化Singleton属性的例子:
/**
 * 日志管理器
 * @author chasegalaxy
 * @since 2012-08-01
 */
public class LogManager {
    /**
     * 私有构造函数,避免通过构造函数初始化
     */
    private LogManager() {
    }

    /**
     * 日志管理器对象-单例
     */
    private static LogManager logManager = null;

    /**
     * 获取日志管理器实例
     * @return 日志管理器实例
     */
    public static LogManager getInstance() {
        if (null == logManager) {
            logManager = new LogManager();
        }
        return logManager;
    }

    /**
     * 添加日志-略
     */
    public void addLog() {
        // ...
    }
}

    JDK1.5起,实现Singleton还有第三种方法,只需编写一个包含单个元素的枚举类型:
public enum LogManager {
    INSTANCE;

    /**
     * 添加日志-略
     */
    public void addLog() {
        // ...
    }
}

    这种方法更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛应用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

    第4条:通过私有构造器强化不可实例化的能力
    有时候,你可能需要编写只包含静态方法和静态域的类,比如Math,Arrays和Collections。这样的工具类不希望被实例化,实例对它来说没有任何意义。企图通过将类做成抽象类来强制该类不可被实例化,这是行不通的,因为抽象类是专门为了继承而设计的。然而,有一些简单的习惯用法可以确保类不可被实例化:
public class XxxUtil {
    // Suppress default constructor for noninstantiability
    private XxxUtil() {
        throw new AssertionError(); 
    }
    // ...
}

    这种习惯用法使得一个类不能被子类化,所以一般是用于不可子类化的工具类中。

    第5条:避免创建不必要的对象
    一般来说,最好能重用对象而不是在每次需要的时候就创建一个功能相同的新对象。如果对象是不可变的(immutable),它就始终可以被调用。下面这个例子中的代码编写方式是要避免的:
public class Person {
    private final Date birthDate;

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    /**
     * 是否是80后-Don't do this!
     * @return 是否是80后
     */
    public boolean is80s() {
        // Unnecessary allocation of expensive object
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.set(1980, Calendar.JANUARY, 1, 0, 0, 0);
        Date begin = cal.getTime();
        cal.set(1989, Calendar.JANUARY, 1, 0, 0, 0);
        Date end = cal.getTime();

        return birthDate.compareTo(begin) >= 0 && birthDate.compareTo(end) < 0;
    }
}

    使用静态的初始化器(initializer)改进后的代码如下:
public class PersonEx {
    private final Date birthDate;
    
    public PersonEx(Date birthDate) {
        this.birthDate = birthDate;
    }
    
    private static final Date BEGIN;
    private static final Date END;
    
    static {
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        cal.set(1980, Calendar.JANUARY, 1, 0, 0, 0);
        BEGIN = cal.getTime();
        cal.set(1989, Calendar.JANUARY, 1, 0, 0, 0);
        END = cal.getTime();
    }
    
    /**
     * 是否是80后
     * @return 是否是80后
     */
    public boolean is80s() {
        return birthDate.compareTo(BEGIN) >= 0 && birthDate.compareTo(END) < 0;
    }
}

    改进后的Person类只在初始化的时候创建Calendar,TimeZone和Date实例一次,而不是在每次调用is80s的时候都创建这些实例。如果is80s的方法被频繁调用,这种方法将会显著提高性能。
    在JDK1.5以后,有一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。通过下面的一个例子,说明了要优先使用基本类型,而不是装箱基本类型,要当心无意识的自动装箱:
public class AvoidAutoBoxing {
    public static void main(String[] args) {
        Long sum = 0L; // 请使用:long sum = 0L; 性能会提高很多。
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
    }
}

    但是不要错误的认为“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”,如果通过创建附加的对象,能使程序更清晰和简洁,这通常是好事。同理,通过维护对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型示例就是数据库连接池。

    第6条:消除过期的对象引用
    Java中有GC,这很容易给程序员留下这样的印象:认为自己不再需要考虑内存管理的事情了,其实不然,请看下面的简单Stack实现的例子:
// Can you spot the "memory leak"?
public class Stack {
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    private Object[] elements;
    private int size = 0;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

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

    /**
     * Ensure space for at least one more element, roughly
     * doubling the capacity each time the array needs to grow.
     */
    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

    以上程序中隐含了一个内存泄露bug:从Stack中弹出来的对象不会被当做垃圾回收,栈中会一直维护这这些对象的过期引用(obsolete reference),过期引用是永远不会被解除的引用。在极端情况下,会导致磁盘交换(Disk Paging),甚至导致程序失败(OutOfMemoryError)。
    这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。上面的例子修订代码如下:
    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object obj = elements[--size];
        elements[size] = null;
        return obj;
    }

    但也不必这么做:对于每一个对象引用,一旦程序不用到它,就把它清空。其实这样做没必要,这样会让代码很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用的最好办法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然发生。
    那么,上面的Stack类为什么需要清空引用呢?原因在于,Stack类自己管理内存(manage its own memory)。一般而言,只要类是自己管理内存,程序员就应该警惕内存泄露问题。
    内存泄露另一个常见来源是缓存,关于使用缓存框架所要注意的问题不在这里研究。
    内存泄露的第三个常见来源是监听器和其他回调。
    内存泄露通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年,只有仔细检查代码或者使用Heap剖析工具(Heap Profile)才能发现。所以最好能在编写代码时候,就避免该问题。

    第7条:避免使用终结方法
    终结方法(finalizer)通常是不可预测的,也是很危险地,一般情况下不推荐使用。不要把终结方法当做C++的析构器(destructors),在Java中,一般通过try和finally来完成资源释放的工作。
    除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,如果使用了终结方法,就要记住调用super.finalize。(第二章完)

    文章本人原创,转载请注明作者和出处,谢谢。
分享到:
评论

相关推荐

    effective java 读书笔记

    effective java 读书笔记,第二版自己摘要并翻译,以备速查。

    java7hashmap源码-for-java:java学习笔记

    Java学习笔记 Effective Java Topic2:插件销毁对象 2. 多参数情况 使用重叠构造器; 使用Build模式【构建器】: new A.Build().set.set.build(); Build模式也适用于类层次结构 递归类型参数 /* * 递归类型参数: ...

    java-note:Java学习笔记

    Structure /src/main/java ... ├ effective_java Effective Java 中文第二版 ├ jvm 深入理解Java虚拟机:JVM高级特性与最佳实践 ├ lambda JAVA 8实战 ├ netty Netty权威指南 ├ oop ├ recursion

    5本java学习用书

    找了5本学习java的好书和一些例子代码和大家分享,希望能够对您能有所帮助。

    notes:JavaJava后端工程师的学习笔记https

    loveincode's notes 学习工作中的一些记录,收藏。 操作系统 , 编译原理 , 计算机网络 , 互联网协议... 常用数据结构与算法 Java 实现 数据结构 与 排序算法 ...Effective Java , HTTP权威指南 , Java

    leetcode题库-MyNote:`13的学习笔记

    的学习笔记 学习笔记与练习项目源码整理 The Only Easy Day Was Yesterday 编程语言 C 参考书籍 : 《c primer plus》 6th edition 书内习题答案总结 , 优秀源码赏析 快速平方根算法 kilo (1000行的源文本编辑器) ...

    java8源码-esmusssein777.github.io:我的学习记录

    java8 源码 Spring IoC源码 effectiveJava学习笔记 Java8的实战学习笔记

    高级java笔试题-Lookoop:学习笔记

    高级java笔试题 个人博客 c++ c++primer - c++primer顺序容器与关联容器的一些用法 effective c++ - effective c++笔记归纳 Data Structures and Algorithm Analysis 数据结构与一些算法,来自算法导论,数据结构与...

    java软件笔试题-MyNotebook:我大学时期的笔记本

    机器学习 C++笔记 part 4: 数据结构与算法 part 5: 笔试/面试 part 6:专业基础 输入URL到页面加载完成的过程 part 7:读书笔记 thinking in java effective java Android开发艺术探索 Android插件化开发指南

    Android代码-一个集Gank.Io,Rxjava示例,操作符,MD控件使用,各种好玩Ap示例的学习App。

    安卓艺术开发探索读书笔记,EffectiveJava读书笔记. 收集各大神博客以及安卓笔记,安卓面试笔记等方便手机端查看. 更新说明 v2.2.2 1.增加了一些最近在学习比较好的安卓大神的博客. 2.增加了来自GeniusVJR整理的安卓...

    AndroidRank 干货学习客户端

    作者HotBitmapGG,源码StudyProject,一款 Material Design 风格的 AndroidRank 干货学习客户端,需要...安卓艺术开发探索读书笔记,EffectiveJava 读书笔记. 收集各大神博客以及安卓笔记,安卓面试笔记等方便手机端查看.

    asp.net知识库

    VS2005 ASP.NET本地化学习笔记&感受 在自定义Server Control中捆绑JS文件 Step by Step 深度解析Asp.Net2.0中的Callback机制 使用 Web 标准生成 ASP.NET 2.0 Web 站点 ASP.NET 2.0基于SQLSERVER 2005的aspnetdb.mdf...

Global site tag (gtag.js) - Google Analytics