java的一些原则

来源:互联网 发布:linux cp 文件夹覆盖 编辑:程序博客网 时间:2024/06/18 06:24

1、可变性最小

不可变类指其实例在创建的时候,各个域被赋值,而整个生命周期,域的值不可改变或不会改变。不可变类有很多优点:主要是对象简单,因为每个实例只有一种状态;最主要的优点是不可变类是线程安全的,不需要同步;不可变对象何以被共享,且由他们可以构建更为复杂的不可变对象。但缺点是不可变类对于没一种状态,必须单独维护一个对象,尤其大对象或者很多对象需要创建的场景,不可变类表就有些“重”了。对于不可变类,要求:
1.不提供mutator,即setter方法;
2.类不应该被扩展;
3.域或者为private final域,或者保证域是互斥访问的。即要么域本身不会改变,要么对于可变的域,比如数组,不能讲这个域发布出来,也不提供改变的方法,需要返回的时候,也只提供拷贝,而不是域指向的引用本身。

2、合优于继承,接口优于抽象

复合优于继承
一般,包内继承是安全的,因为一般一个包由一个人维护,他清楚包中的代码,而跨包继承可能出问题。
1、子类的实现依赖超类的实现,虽然继承就是为了复用,但是父类行为的改变也会引起子类的改变。
2、如果父类添加了一个方法,子类正好有这个方法,那么就会冲突。
3、子类会继承父类可以使用的方法,这可能会使子类的方法多于需求。
4、无法实现多继承,但复合是可以复合多个组件的。
复合克服了继承的缺点,同时达到目的。
接口优于抽象
接口优点:
1.现有类很容易被更新为接口。现有类如果实现了接口的方法,那么很容易就实现了接口,如果没有实现,那么实现即可。
2.接口适用于定义mixin类型。mixin表示一个类具有其他特征,比如comparable接口的实现类就表示这个类是可比较的。
3.接口是可以多继承的,所以对于拥有多重身份的类,接口可以合理的实现,抽象类做不到。比如,一个人既是作词人,有事作曲人。
抽象类也有自己的优点,比如抽象类可以实现一些基础方法,然后子类复用这个方法,比较典型的就是模板方法模式。

3、函数对象表示策略

如果一个类仅仅是为了导出一个方法,那么这个类的实例就是函数对象。比如comparator。这些类往往是无状态的,做单利很合适,当做匿名类也很合适。

4、泛型

List: 可以插入任何对象,其实是历史遗留。
List<?>: 只能插入一种对象,这种对象在确定其类型的时候后就不能更改。
List<Object>: 可以插入任何对象,但明确告诉了编译器容器的泛型类型。
泛型是通过擦除实现的,就编译器检查类型,运行时类型信息就被擦除了。所以,并没有List<String>.class,只有List.class。同样,对于instanceof和new T[100]也是徒劳无功的。下面有例子。
import java.util.HashSet;
import java.util.Set;
 
public class GenericBasic {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Set<?> set = new HashSet<String>();
set.add(null);//set只能添加null,因为只有它没有类型
((Set<String>) set).add("i am a string set");// 无法添加
// set.insert("string is my type");// 无法添加
if (set instanceof Set) {
// if (set instanceof Set<String>) {
// 这是错的,因为运行时,String信息被擦除,那么instanceof就没有意义
for (String s : (Set<String>) set) {
System.out.println(s);
}
 
Set<Integer> m = (Set<Integer>) set;// 这么编译通过,但当出现class cast时,出错
System.out.println(m.isEmpty());
for (Integer i : m) {
System.out.println(i);
}
 
}
}
}
既然使用泛型,其实很多时候,应该使用通用类型,比如<? extends People> 或者 <? super Professional>。同时如果引起歧义,可以强制指定类型。如下代码。
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
 
/**
* 引起歧义的时候,强制确定类型。
*/
public class CanStaticMethodGeneric {
 
public static <T> void showType(List<? extends T> o1, List<? extends T> o2) {
System.out.println("I am ok");
}
 
public static void main(String[] args) {
CanStaticMethodGeneric.<Number> showType(new LinkedList<Long>(), new ArrayList<Integer>());
}
}

5、枚举:实例域代替序数|EnumSet代替位域|EnumMap代替序数索引

枚举的基本使用如下面的两个例子:
/**
* enum 实质是int。通过共有静态final域为每个枚举常量导出实例的类。因为没有可访问的构造器,客户端既不能创造枚举实例,也不能对枚举类扩展,
* 只能申明枚举常量。枚举类是实例受控的。 enum比自定义常量好,因为枚举是伸缩的,添加的新的枚举常量不会影响原来的枚举实例。
*
* @author MiXian
*
*/
public enum Planet {
MERCURY(3.302e+23, 2.439e6), VENUS(4.869 + 24, 6.052e6), EARTH(5.975e+24, 6.478e6), MARS(6.419E+23, 3.393E6), JUPITER(
1.899E+27, 7.149E7), SATURN(5.685E+26, 6.027E7), URANUS(8.683E+25, 2.556e7), NEPTUNE(1.024e+26, 2.477e7);
 
private final double mass;// kilograms
private final double radius;// meters
private final double surfaceGravity;
 
private static final double G = 6.67E-11;
 
Planet(double m, double r) {//这个构造器只能是私有的
mass = m;
radius = r;
surfaceGravity = G * mass / (radius * radius);
}
 
public double getMass() {
return mass;
}
 
public double getRadius() {
return radius;
}
 
public double getSurfaceGravity() {
return surfaceGravity;
}
 
public double surfaceWeight(double mass) {
return mass * surfaceGravity;
}
public static void main(String[] args) {
double mass = 1;
for(Planet p : Planet.values()) {
System.out.println(p.surfaceWeight(mass));
}
}
}
/**
* 实现策略设计模式。
* 其中,內部的PayType使用了模板方法模式。
*
* @author MiXian
*
*/
public enum PayrollDay {
MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WENSDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(
PayType.WEEKDAY), SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
PayType type;
 
PayrollDay(PayType type) {
this.type = type;
}
 
private static enum PayType {
WEEKDAY {
double overtimePay(double hours) {
return 2 * hours;
}
},
WEEKEND {
double overtimePay(double hours) {
return 4 * hours;
}
};
abstract double overtimePay(double hours);
}
}

实例域代替序数:

使枚举类型会自动创建序数,比如:
public enum HelloEnum {
A, B, C;
public static void main(String[] args) {
for (HelloEnum e : HelloEnum.values()) {
System.out.println(e.ordinal());
}
}
}
将会输出0,1,2。但这意味着A,B,C必须在enum的最前面,且顺序不能变,如果这个枚举有变化,那么原来的程序需要重新修改。所以,应该添加一个实例域,使其代替默认序数的功能。

EnumSet代替位域

什么是位域?
比如表示字体的类:
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
 
public static final String[] styles = new String[] { "STYLE_BOLD", "STYLE_ITALIC", "STYLE_UNDERLINE",
"STYLE_STRIKETHROUGH" };
 
public static void apply(int sty) {
for (int i = 0; i < 4; ++i) {
if ((sty & (1 << i)) != 0) {
System.out.println(styles[i]);
}
}
 
}
public static void main(String[] args) {
Text.apply(Text.STYLE_BOLD | Text.STYLE_ITALIC);//不用EnumSet的话,这么做位域
}
}
用一位表示一个特征。显然为了速度,使用styles冗余表示了类型。用EnumSet实现如下:
public class Text {
public enum Style {
STYLE_BOLD, STYLE_ITALIC, STYLE_UNDERLINE, STYLE_STRIKETHROUGH;
}
 
public void apply(Set<Style> sty) {
for (Style s : sty) {
System.out.println(s);
}
}
public static void main(String[] args) {
new Text().apply(EnumSet.of(Style.STYLE_BOLD, Style.STYLE_STRIKETHROUGH));
}
}

EnumMap代替序數索引

如果需要用enum做索引,那麼不要用其序數,而是用EnumMap。
/**
* 当需要对enum与某些元素做联系,比如映射的时候,就用EnumMap。
*
* @author MiXian
*
*/
public class EnumMapUse {
private enum Person {
TOM, PENNY, RAJ;
}
 
public static final Map<Person, String> personInfor = new EnumMap<Person, String>(Person.class);
 
static {
for (Person p : Person.values()) {
personInfor.put(p, p + "is a good guy");
}
}
}

6、检查参数的有效性

共有方法要明确参数的范围,并在参数出错的情况下,抛出异常。而私有方法则应使用断言,断言在测试的时候开启,发布上线就关闭,这样,本质上就不会产生什么开销了。

7、必要时进行保护性拷贝

import java.util.Date;
 
/**
* 错误的方式,一定不要写这样的类。
*
* @author MiXian
*
*/
public class BadWay {
private final Date born;
 
public BadWay(Date date) {
// date 可能在类外被其他代码控制。
born = date;
}
 
public Date getBorn() {
// 返回的对象可能会被修改。
return born;
}
}

import java.util.Date;
/**
* 这样对这个类的域是由保护的。
*
* @author MiXian
*
*/
public class GoodWay {
private final Date born;
 
public GoodWay(Date date) {
// 这里没有用clone,因为date可能是Date的一个子类,
//我们无法控制这个类。
born = new Date(date.getTime());
}
 
public Date getBorn() {
return new Date(born.getTime());
}
}


但如果使用long表示时间,那么上面的问题就都没有了。所以,尽量使用不可变或者基本类型表示状态是一个很好的选择。

8、慎用可变参数函数

可变参数接受0个至多个参数。但每次调用都会有一次数组分配和初始化操作,这个代价还是比较大的。

9、可以使用int的时候,避免使用float和double

float,double是为了科学计算和工程计算设计的,在计算中,可能出现一些舍入。对于金融领域等要求精确且可以用整型完成计算的领域,应当使用整型计算。而且,可以根据数的范围,分别选择int,long,和BigDecimal。

10、基本类型优于装箱基本类型

基本类型必然表示一个值且只能表示一个值,而两个装箱基本类型的值可能相等,但是他们是两个对象。同时,装箱基本类型可能是null。当然,装箱基本类型更“重”。注意,装箱基本类型的比较必须用equals,不然一般都是错的。其实还可以将装箱基本类型用基本类型开箱后在处理。

11、异常:抛出与抽象相对的异常|使失败保持原子性

抛出与抽象相对的异常

捕获低层异常,抛出高层异常。比如
try {
...
} catch(LowerLevelException e) {
throw new HigherLevelException(e);
}

使失败保持原子性 

方法失败后,应该使对象保持在失败调用之前的状态。就是及时对象失败,我们也不会丢失对象的信息。这就是失败原子性。
最简单的办法当然是构造不可变对象;而对于可变对象,最常见的办法是检查参数的有效性,同时使可能失败的操作在对象改变之前执行,或者保存临时拷贝或者持久化到本地以备失败后恢复。

12、正确理解多线程

这里对多线程只是简单说几句,但是全面理解多线程,篇幅太小,绝对不行。Synchronized不仅保证线程是互斥的,而且保证了护持部分的变量的可见性。volatile只保证可见性。所以下面的例子,后台backLoop进程永远不停止,而backIf进程永远不执行。
/**
* 这是一个错误的例子,不可这么做。
*/
public class NeedSyn {
private static boolean stop = false;
 
public static void main(String[] args) throws InterruptedException {
Thread backLoop = new Thread() {
@Override
public void run() {
int i = 0;
while (!stop) {
++i;
System.out.println(i);
}
}
};
Thread backIf = new Thread() {
@Override
public void run() {
int i = 0;
if (!stop) {
++i;
}
System.out.println(i);
}
};
 
backLoop.start();
backIf.start();
 
Thread.sleep(1000);
stop = true;
}
}
当然,正确同步的方法要确保使用者不会覆盖这些同步的方法,以免被覆盖而造成一系列问题。这里其实也是说避免继承,采用组合就没有问题。
import java.util.ArrayList;
import java.util.List;
 
/**
* 这个类展示如何在单线程下抛出ConcurrentModificationException。不應該寫這種方法。
*/
public class ForEachAndRemove {
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 10; ++i) {
list.add(i);
}
for (Integer i : list) {
System.out.print(i + " ");
}
System.out.println();
for (Integer i : list) {
System.out.println(i);
list.remove(i);
}
}
}

解决上面的异常的问题,可以使用CopyOnWriteArrayList。即
List<Integer> list = new CopyOnWriteArrayList<Integer>();

事实上,java自身的多线程访问的同步工具就已经十分出色,大部分的需求都可以实现,对于十分简单的多线程操作,可以直接使用Thread,对于较复杂的,可以使用Executor。同时,由于自己用Object的wait和notify的开发难度很大,应该多使用同步工具。
同步工具指java.util.concurrent包下的工具,主要分为:Executor Framework, Concurrent Collection 和 Synchronizer。
Concurrent Collection是实现同步的集合类型,比如ConcurrentHashMap,BlockingQueue等。
Synchronizer是实现同步功能的开关,比如CountDownLatch, Semaphore, CyclicBarrier和Exchanger。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
 
/**
* 使用Countdown制作任务管理器。
*/
public class TimingConcurrentExecutor {
/**
* 同时执行concurrentThreads个任务,同时返回执行时间。
* executor必须可以创建足够的线程执行,否则executor会由于无法创建足够的线程,将发生饥饿死锁。
* 所以executor不可以是single executor等无法提供足够线程的executor。
*
* @param executor
* @param concurrentThreads
* @param action
* @return
* @throws InterruptedException
*/
public static long time(Executor executor, int concurrentThreads, final Runnable action)
throws InterruptedException {
final CountDownLatch ready = new CountDownLatch(concurrentThreads);
final CountDownLatch start = new CountDownLatch(1);
final CountDownLatch done = new CountDownLatch(concurrentThreads);
 
for (int i = 0; i < concurrentThreads; ++i) {
executor.execute(new Runnable() {
@Override
public void run() {
ready.countDown(); // 这个线程已经准备好了,那么latch减少一个。
try {
start.await(); // 等待start latch放过
action.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
done.countDown(); // 结束后,减少一个latch
}
}
 
});
}
 
ready.await(); // 等待全部准备好后,就开始执行
long startNanos = System.currentTimeMillis();
start.countDown(); // 放行,所有在start.await()后面的代码开始可以执行
done.await(); // 等待done都减少到0,即所有的线程都完毕后放行
long doneNanos = System.currentTimeMillis();
 
return doneNanos - startNanos;
}
}
注意几点:1、线程的数目的问题,有些书建议线程数目不要明显多于处理器的数量;也有些书认为,最佳数目是处理器数目+1。2、线程优先级应该尽量少用,因为优先级依赖于具体平台,使用优先级会使可移植性降低;所以依赖于线程优先级处理任务是不合理的。

13、慎用延迟初始化

下面的例子中有讲解。
/**
* 这里介绍一种实际应该被使用的延迟初始化方法。 双重锁定的方法在Concurrent java in practice被否定了。
* 使用Holder的方法可以很好的替代。
*
* 在单例模式使用Holder实现单例,而对于域的访问采用延迟初始化可能会降低性能,所以effective java不建议使用对域的延迟初始化。
*
* @author MiXian
*
*/
public class LazyInit {
private LazyInit() {}
 
private static class Holder {
private static LazyInit inst = new LazyInit();
}
 
public static LazyInit getInstance() {
return Holder.inst;
}
}

14、序列化


使用默认的序列化机制,就一意味着这个类的域被导出了,那么通过序列化,外部其实就获得了这个类的域;那么一旦你改变这个类,就可能使旧版本的类反序列化。
序列化版本UID,即serialVersionUID表示这个类的序列化版本,自动生成这个静态域会受到类名,域名,类型等多种因素的影响,如果改变这个类,可能会造成类序列化兼容性的问题,在这个id不一致的情况下,反序列化会抛出InvalidCastException。
需要注意的是,反序列化其实也是构造对象的一种方法,如果实际对象构造需要满足一些初始化条件,并且这些条件写在了有参数的构造函数中,那么反序列化构造过程可能会破坏对象约束关系。
为继承而实现的类,应该尽可能少实现serializable接口。
当实例域被初始化为默认值的情形违背了这个类的逻辑限定,那么这个类就应当添加下面的方法。
private void readObjectNoData() throws InvalidObjectException {
throw new InvalidObjectException("data required");
}

下面例子學習一下:
import java.util.concurrent.atomic.AtomicReference;
 
/**
* 父类不Serializable,子类序列化方法如下。
* 具體見<tt>Foo</tt>
* 所有共有或者protected方法都必须先调用checkInit。
*/
public class AbstractFoo {
private int x, y;
 
private enum State {
NEW, INITIALIZING, INITIALIZED;
}
 
// 保证原子性
private final AtomicReference<State> init = new AtomicReference<State>();
 
public AbstractFoo(int x, int y) {
initialize(x, y);
}
 
// 允许子类反序列化
protected AbstractFoo() {
}
 
// 子类调用这个方法,完成初始化后的约束关系
protected void initialize(int x, int y) {
if (!init.compareAndSet(State.NEW, State.INITIALIZING)) {
throw new IllegalStateException("Already initialized");
}
this.x = x;
this.y = y;
init.set(State.INITIALIZED);
}
 
// 下面的方法是在子类调用writeObject的时候
protected final int getX() {
checkInit();
return x;
}
 
protected final int getY() {
checkInit();
return y;
}
 
private void checkInit() {
if (init.get() != State.INITIALIZED) {
throw new IllegalStateException("Uninitialized");
}
}
}
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
 
public class Foo extends AbstractFoo implements Serializable {
 
private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
s.defaultReadObject();
int x = s.readInt();
int y = s.readInt();
initialize(x, y);
}
 
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(getX());
s.writeInt(getY());
}
 
public Foo(int x, int y) {
super(x, y);
}
 
private static final long serialVersionUID = 3064836850956086768L;
}

什么时候采用默认的序列化形式呢?effective java认为:当对象的物理表示等同于它的逻辑表示的时候,可以采用默认的序列化方法。简单的说,就是如果你期望将对象序列化成一个对象的图的形式,那么默认的序列化就是可以接受的 。如果物理表示和逻辑表示相差很大,那么采用默认的序列化方法会产生很严重的问题。这时候应该自定义序列化方法。
如果采用了默认的序列化,那么最好提供一个readObject确保约束性和安全性。多线程环境下,还要保证同步要求。同时,因为序列化是对象可以在网络传播,所以要像构造不可变对象那样,注意保护对象,必要的时候,进行对象拷贝,以隔离域与外界对象的沟通。
对于单例,反序列化是一个挑战,它会破坏单例。解决办法是给单例类增加一个readResolve方法。如下:
public class Elvis {
private static final Elvis inst = new Elvis();
private Elvis() {}
public Elvis getInstance() {
return inst;
}
// here
private Object readResolve() {
return inst;
}
}
反序列化的时候,readResolve被调用,这个结果返回给实例,从而保证实例inst仍然指向原来被序列化的实例,这个方法保证了单例。
effective java建议用enum代替readresolve,因为jvm提供对枚举实例的控制。
最后,effective java还建议采用序列化代理代替序列化实例。这种方法是采用静态内部类和重写writeReplace(外围),readResolve(内部)方法实现的。具体如下:
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
 
public class ProxyWay {
private int v;
 
public ProxyWay(int x) {
v = x;
}
 
private Object writeReplace() {
// 序列化时,将外围实例转变为内部代理
return new SerializationProxy(this);
}
 
private Object readObject(ObjectInputStream s) throws InvalidObjectException {
// 反序列化的时候,返回
throw new InvalidObjectException("Proxy required");
}
 
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 5877315670630821416L;
private int v;
 
SerializationProxy(ProxyWay o) {
v = o.v;
}
 
private Object readResolve() {
// 反序列化的时候,返回
return new ProxyWay(v);
}
 
}
}
以上内容主要来自《effective java》,理解不到位,请指出!  谢谢!

0 0