Effective Java学习记录(1)

前阵子翻了翻《Effective Java》这本书,觉得里面的内容不错,觉得有必要留下些笔记以供以后自己翻看。</span>

本书比较老了,大概总结了jdk1.6及以前版本编程的78条原则,大多数到现在也比较适用。


一、创建和销毁对象

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

对于类而言,为了让客户端获得其自身的一个实例,最常用的方法是使用一个公有的构造函数创建一个对象。如下列构造函数:

 new BigInteger(int, int, Random);
该构造函数是用于创建一个随机素数,但显然这个构造器并不能很好的表达这个含义,所以我们最好用下面的方法。

BigInteger.probablePrime(int,Ramdom)
在jdk1.4时已经采用了该方法。

这也是静态方法对于构造函数的第一个优势:静态方法有名称,更方便用户阅读。

让我们再看一下另一个j静态工厂方法例子:

Boolean.valueOf(boolean);
Integer.valueOf(int);
该方法可以有效的避免实例重复创建,可以更好的重用实例。

这也是静态方法对于构造函数的第二个优势:不必在每次调用它们的时候都创建一个新的对象。

java1.5中引入了EnumSet,其中只定义了静态方法,是用于操作枚举。EnumSet很多方法都会调用以下的方法:

   /**
     * Creates an empty enum set with the specified element type.
     *
     * @param <E> The class of the elements in the set
     * @param elementType the class object of the element type for this enum
     *     set
     * @return An empty enum set of the specified type.
     * @throws NullPointerException if <tt>elementType</tt> is null
     */
    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<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
由该方法可以看出,当枚举数量大于64时会返回JumboEnumSet实现,反之是RegularEnumSet。

这是静态方法对于构造函数的第三个优势:可以返回原返回类型的任何子类型的对象。

在使用泛型时,经常会出现以下:

Map<String,List<String>> map=new HashMap<String,List<String>>();

但如果使用静态方法的话,代码可以更简洁。

假如HashMap中定义了以下方法:

public static <K,V> HashMap<K,V> newInstance(){
    return new HashMap<K,V>();
}
那么就可以将new改成如下的方法:

Map<String,List<String>> map=HashMap.newInstance();

这是静态方法对于构造函数的第四个优势:使用泛型创建实例时使用静态共产使代码更简洁

这种类型类型推导在1.6还没有实现,但在1.8中是可以用以下来代替:

Map<String,List<String>> map=new HashMap<>();

静态工厂方法的缺点:

1、如果类中没有提供public或protected构造方法,就不能被子类化,但现在一般提倡组合而不是继承,所以并不是问题。

2、和其他静态方法没有任何区别,一般不会在API文档中被标识出来,最好在类的java doc中有明确阐述类实例时需要调用哪个静态方法。


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

静态工厂和构造函数有个共同的局限性,那就是都不能很好的扩展参数。

如下代码:

构造函数模式:

public class NutritionFact {

    private final int servingSize;

    private final int servings;

    private final int calories;

    private final int fat;

    private final int sodium;

    private final int carbohydrate;

    public NutritionFact(final int servingSize, final int servings, final int calories, final int fat, final int sodium,final int carbohydrate) {
<span style="white-space:pre">	</span>super();
<span style="white-space:pre">	</span>this.servingSize = servingSize;
<span style="white-space:pre">	</span>this.servings = servings;
<span style="white-space:pre">	</span>this.calories = calories;
<span style="white-space:pre">	</span>this.fat = fat;
<span style="white-space:pre">	</span>this.sodium = sodium;
<span style="white-space:pre">	</span>this.carbohydrate = carbohydrate;
    }

    public NutritionFact(final int servingSize, final int servings) {
<span style="white-space:pre">	</span>this(servingSize, servings, 0, 0, 0, 0);
    }

    public NutritionFact(final int servingSize, final int servings, final int calories) {
<span style="white-space:pre">	</span>this(servingSize, servings, calories, 0, 0, 0);
    }

    public NutritionFact(final int servingSize, final int servings, final int calories, final int fat) {
<span style="white-space:pre">	</span>this(servingSize, servings, calories, fat, 0, 0);
    }

    public NutritionFact(final int servingSize, final int servings, final int calories, final int fat,final int sodium) {
<span style="white-space:pre">	</span>this(servingSize, servings, calories, fat, sodium, 0);
    }
}

javabean模式:

public class NutritionFact {

    private int servingSize = -1;

    private int servings = -1;

    private int calories;

    private int fat;

    private int sodium;

    private int carbohydrate;

    public NutritionFact() {
    }

    public void setServingSize(final int servingSize) {
	this.servingSize = servingSize;
    }

    public void setServings(final int servings) {
	this.servings = servings;
    }

    public void setCalories(final int calories) {
	this.calories = calories;
    }

    public void setFat(final int fat) {
	this.fat = fat;
    }

    public void setSodium(final int sodium) {
	this.sodium = sodium;
    }

    public void setCarbohydrate(final int carbohydrate) {
	this.carbohydrate = carbohydrate;
    }

}
构造器模式会使用户设置用户不想要的参数,缺乏灵活性。

而javabean模式,将对象的构建分散到各个set方法中,这样虽然很灵活,但是无法在类的创建时对参数进行校验。

构造器模式:

public class NutritionFact {

    private int servingSize;

    private int servings;

    private int calories;

    private int fat;

    private int sodium;

    private int carbohydrate;

    private NutritionFact() {
	// validate
    }

    static class Builder {
	private int servingSize;

	private int servings;

	private int calories;

	private int fat;

	private int sodium;

	private int carbohydrate;

	public Builder servingSize(final int servingSize) {
	    this.servingSize = servingSize;
	    return this;
	}

	public Builder servings(final int servings) {
	    this.servings = servings;
	    return this;
	}

	public Builder calories(final int calories) {
	    this.calories = calories;
	    return this;
	}

	public Builder fat(final int fat) {
	    this.fat = fat;
	    return this;
	}

	public Builder sodium(final int sodium) {
	    this.sodium = sodium;
	    return this;
	}

	public Builder carbohydrate(final int carbohydrate) {
	    this.carbohydrate = carbohydrate;
	    return this;
	}

	public NutritionFact build() {
	    final NutritionFact fact = new NutritionFact();
	    fact.servings = servings;
	    fact.carbohydrate = carbohydrate;
	    fact.calories = calories;
	    fact.fat = fat;
	    fact.sodium = sodium;
	    fact.carbohydrate = carbohydrate;
	    return fact;
	}

    }

}

这种模式很好的结合了构造器和javabean模式的优点,使用了内部静态类来进行参数的构建,通过build方法来创建对象。

该模式jdk中可以参考StringBuffer、StringBuilder等。

反射可以通过Class.newInstance的方式来创建实例,而builder模式可以很好的防止这一点。


简而言之,如果类的构造器有多个参数,都可以使用这种builder模式来创建。


3、单例模式

第一个单例模式:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
<span style="white-space:pre">	</span>return INSTANCE;
    }

    public void method() {
<span style="white-space:pre">	</span>// do something
    }
}
缺点:AccessibleObject.setAccessible()的方式可以通过反射来调用私有构造函数。

第二个单例模式:

public class Singleton {

    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {
	if (INSTANCE != null) {
	    throw new AssertionError();
	}
    }

    public static Singleton getInstance() {
	return INSTANCE;
    }

    public void method() {
	// do something
    }

}
缺点:构造函数是用于构建对象,抛出异常并不是一个好的习惯。

第三个单例模式:

public enum Singleton {

    INSTANCE;

    public void method() {
	// do something
    }

}
可以说是现在公认的最佳单例模式。


4、消除过期的对象引用

简单的栈实现例子:

import java.util.Arrays;

public class Stack {

    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

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

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

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

    private void ensureCapacity() {
	if (elements.length <= size) {
	    elements = Arrays.copyOf(elements, 2 * size + 1);
	}
    }

}

初看这个例子并没有很明显的错误,但是当程序先push了很多对象然后再pop的话,就有可能会因为内存不足抛出OutOfMemoryError了。

解决方法也不难:

    public Object pop() {
	if (size == 0) {
	    throw new AssertionError();
	}
	final Object result = elements[--size];
	elements[size] = null;
	return result;
    }

内存泄漏的另一个常见来源是缓存,一旦将对象引用放在缓存里面,就很容易被遗忘,从而使之长期放在缓存中。对于这种情况,有几种可能的解决方案。

如果恰好需要实现如下的需求,只要缓存之外存在对某个项的键的引用,该项就有意义。可以使用WeakHashMap,这个map保存的key都是弱引用。如果key被get过,那么下次取值时内存紧张,虚拟机就会回收map中一部分被标记的对象从而使虚拟机保持平衡。

public static void main(String[] args) {
	WeakHashMap<Integer, String> map=new WeakHashMap<Integer, String>();
	for(int i=0;i<10000000;i++){
		map.put(i, "hello");
		map.get(i);
	}
	System.out.println(map.size());
}
输出:982317(根据机器性能和jvm内存参数)

public static void main(String[] args) {
	HashMap<Integer, String> map=new HashMap<Integer, String>();
//	WeakHashMap<Integer, String> map=new WeakHashMap<Integer, String>();
	for(int i=0;i<10000000;i++){
		map.put(i, "hello");
		map.get(i);
	}
	System.out.println(map.size());
}
输出:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space(机器性能足够好,不会抛一场,会输出10000000)

可以简单的看出WeakHashMap可以有效的减少不必要的缓存内容。


二、Object中的方法

1、始终要覆盖toString

toString()这个方法很特别,定义在Object中,所有的类都会集成Object,也就是说所有的类都有toString方法。

如果不覆盖toString方法的话,那么调用它时会出现诸如:“PhoneNumber@163b91”之类的返回结果。

这种结果虽然很简短、易阅读(toString的通用约定),但很难懂,如果进行覆盖,返回这个类的值或许更方便阅读。

因为很多在程序运行和调试时,有时候会经常把对象当做字符串打印出来,所有覆盖toString方法是很有必要的。

toString重写不像equals和hashcode这种规则那么重要,但好的toString实现可以使类更舒适。

toString的覆盖也带来了一些麻烦,在类与类继承关系较多时,有可能会因为一个父类覆盖了方法,而子类没覆盖导致print子类时直接返回了父类定义的方法,这会给程序带来困扰。所以不建议多层继承关系,因为继承关系使得代码更复杂,可以使用组合模式进行代替,这也是现在比较推荐的一种方法。


2、Comparable接口

public interface Comparable<T> {
    
    public int compareTo(T o);
}
该方法的约定和equals类似:对称性、传递性、内容不被修改多次调用的一致性。

下面是BigDecimal的Comparable的实现

    public int compareTo(BigDecimal val) {
        // Quick path for equal scale and non-inflated case.
        if (scale == val.scale) {
            long xs = intCompact;
            long ys = val.intCompact;
            if (xs != INFLATED && ys != INFLATED)
                return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
        }
        int xsign = this.signum();
        int ysign = val.signum();
        if (xsign != ysign)
            return (xsign > ysign) ? 1 : -1;
        if (xsign == 0)
            return 0;
        int cmp = compareMagnitude(val);
        return (xsign > 0) ? cmp : -cmp;
    }

Comparable接口定义的compare方法返回的是int,但BigDecimal却返回了1和-1,这似乎很不合理。

对于像这种比较数字,比较值的实现,返回的结果最好还是1和-1,因为有负数的存在,可能会超过有符号32位的int最大值,返回就会变成一个负值。这种情况可能在实际应用不会经常发生,但api设计时是需要确保杜绝这种情况。

然而当可以确保比较的双方都是同符号的话,则应该可以直接返回诸如a-b的值。

这一章的内容有些乏味和纯理论,讲的也都是大多数java开发人员知晓和遵守的规则,有时间再进行整理。


三、类和接口

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

区别模块的设计的好坏,最重要的因素在于,这个模块对于外部模块而言,是否很好的隐藏了其实现的细节。设计良好的模块会隐藏掉所有的实现,只暴露出一系列的接口的。

模块和模块的通信,应该只通过API之间的调用,一个模块是不应该使用和了解其他模块的内部实现细节的。

对于顶层的(非嵌套的)类和接口只有2种访问级别:package-private和public

这样可以将实现类放入package-private,而接口可以放在public的包中。但是有一点还是要吐槽下的,这2种级别似乎并不够用,因为经常会出现需要提供工厂类来给其他模块进行调用,但是遗憾的是因为包级别私有使得工厂类为了调用实现类智能定义在实现类的包中,使得代码结构看着并不怎么优雅。

当然之前用的eclipse plugin可以很好的解决这个问题,因为可以定义包的的导出可见性,而maven似乎没有这个功能(也可能是我并没有了解到)。

当然也可以使用osgi的调用将之封成接口给其他模块调用,但似乎为了一点优雅需要花的代价又比较大。

现在的很多框架都是为了降低软件的耦合度而提供一些诸如spring的ioc、aop等解决方案,也许以后会有更好。


2、复合优先于继承

继承是实现代码重用的有效手段,但它并非是完成这项工作的最佳工具。使用不当会导致软件变得十分脆弱。在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个开发者的控制下。对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是安全的。然而,对于普通的具体实现类进行跨包的继承,则是很危险的。

与方法调用不同的是,继承打破了封装性。比如:子类依赖超类的一些方法,而超类可能会在未来被修改,这时子类很可能就会被破坏,即使他的代码完全没有变。因为子类必须要跟着父类的更新而演变,除非父类是专门为了扩展而设计的,并且有良好的说明文档。

来看下面一个例子:

public class InstrumentedHashSet<E> extends HashSet<E> implements Set<E> {

    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    @Override
    public boolean add(final E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(final Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }

}
功能:当set每添加一个对象时,计数会+1。

 <span style="white-space:pre">	</span>final InstrumentedHashSet set = new InstrumentedHashSet<>();
        set.addAll(Arrays.asList("hello", "world", "asdssd"));
        System.out.println(set.getAddCount());
预期的输出结果为3,但实际运行的结果为6。父类的abstractCollection中的allAll()会调用add(),这样使用addAll()增加的每个元素都会被计算2次。

那么是不是只要不修改allAll方法就可以保证功能的正确呢?

其实不然,虽然这可以保证当前的计算结果不正确,但无法保证父类在未来的版本中实现不会被修改。

要解决这种这个问题,只能在我们的实现类中重写addAll方法,而不复用父类的方法实现,这样明显并不合理。

然后用组合却可以很好的解决这一问题。看下下面的代码:

public class ForwardHashSet<E> implements Set<E> {

    private final HashSet<E> set = new HashSet<>();

    private int addCount = 0;

    @Override
    public <T> T[] toArray(final T[] a) {
        return set.toArray(a);
    }

    @Override
    public boolean add(final E e) {
        addCount++;
        return set.add(e);
    }

    @Override
    public boolean addAll(final Collection<? extends E> c) {
        addCount += c.size();
        return set.addAll(c);
    }

    @Override
    public boolean remove(final Object o) {
        return set.remove(o);
    }

//其他实现略
}
这样实现的话,无论父类实现怎么变化,只要接口不变,功能都能保证正确。


3、接口优于抽象类

java提供了2种设计机制用于定义允许多个实现的类型:接口和抽象类。
















































你可能感兴趣的:(Effective Java学习记录(1))