锤炼"单例"

此篇文章不断更新中, 包括根据java发展, 网络资源, 博客评论直接做修改, 以便其他读者不用去扒各地资源, 因为柔和了思想, 纷杂的片段, 无法一个个注明参考处, 请不要惊讶或气愤, 由衷感谢相关博客和评论.

单例是设计模式的一种, 从语义上来说就是一个应用内或者一个进程内或者一个系统内, 某个类有且只有一个实例或对象给外部使用, 比如代表文件系统的对象, 全局配置管理器的对象应该保证只有一个, 有些重量级的可重复利用的大对象如果够用, 可能也会使用单例以减少内存占用. 不过单例可能很简单, 也可能很复杂, 可能无法继承(比如使用枚举实现)或被继承(只能内部类或静态内部类继承, 但已不符合里氏替换原则吧? 做成final类岂不更好?), 从而变得更加面向过程, 所以如果可能, 且不是出于现实模型的语义, 不应优先考虑使用单例. 在现在的测试驱动开发中, 单例模式由于难以被模拟其行为而被视为反模式(anti pattern), 所以如果你是测试驱动开发的开发者, 最好避免使用单例模式, 至少你可能希望单例类实现一个可以充当其类型的接口. 如果存在N多个大对象的单例, 因为几乎无法被垃圾回收, 有可能导致内存问题. 传闻程序员杂志出过一期, 说GoF觉得单例模式导致很多代码”坏味道”, 打算在新书中删除这种模式, 当然权威的归权威, 市场的归市场.

最初的单例实现是从初始化时机和应对多线程这两个方面展开的, 看下面的示例:

public final class Settings {
    public static final Settings SINGLE_INSTANCE = new Settings();
    private Settings() {}
}

这种实现没有考虑延迟初始化, 但获取实例的操作天然能应对多线程. 这是最简单的实现(未必是最差的实现), 如果你确信该类将永远是单例, 使用这种方式是有意义的, 如果你希望保留余地, 比如后续你可能在获取实例的问题上添加权限, 你不想实例赤裸裸的暴露, 亦或者你认为使用静态工厂方法是比较严谨妥当的处理方式, 甚至将来你可能希望根据需求做成非单例, 那么你可能这样写:

public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        //check something here or not
        return singleInstance;
    }
    private Settings() {}
}

这里将该静态方法声明为final, 不是必须的, 可能仅仅出于习惯, 但是你也许不希望内部子类遮蔽隐藏这个方法, 所以声明为final不会令你损失更多, 也没有什么可值得争吵的. 有的人会认为方法声明为final将被内联到所有调用处, 在java的早期版本确实是通过这样的, 但目前来看, 这仅仅是给jvm一个提示而已, 现在的jvm变得更加自主, 多数情况静态工厂方法都会被内联. 或者它本身就不应该被继承:

public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
}

这里注意, 不要依赖类初始化机制, 像静态代码块往往使问题变得更加复杂, 而且绝大多数情况可以使用静态方法(这里是静态工厂方法)代替静态代码块, 所以除非你已深知这里可能的陷阱, 并且某些原因使你不得不这么做, 否则请不要这么做, 尽管它看起来没有你想的那么危险:

public final class Settings {
    private static Settings singleInstance;
    static {
        // maybe do something here
        singleInstance = new Settings();
        // maybe do something here
    }
    public static Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
}

上面这几种实现被称为”饥汉式”, 你可以注意到它由三部分组成, 一个私有构造, 一个私有常量域指向使用私有构造生成的实例, 和一个总是返回这个实例的静态工厂方法. 这样这个实例看起来就是唯一的了. 过去因为平台的限制, 很多用得着用不着的类实例一开始就被加载会拖累启动运行速度, 而且多占着部分内存, 所以这种”饥汉式”方式不被提倡, 目前来看, 随着平台处理速度的飞速已经软件工程的发展, 这种方式简单易用, 避免了很多问题, 不失为是好的选择, 尤其是你看到下面那些需要自己应对多线程的实现时, 不过有个缺点很明显, 就是这种方式无法用在带参构造, 依赖参数的情况.

既然有”饥汉式”就会有”懒汉式”, 就是实例的初始化延迟到第一次真正使用该实例对象时. 重申一次, 在大多数时候, 正常的初始化要优于延迟初始化. “懒汉式”主要用于构造复杂的大对象或带参构造的情况以及对性能有特定要求的情况, 让我们来看看它的演变:

public final class Settings {
    private static Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            singleInstance = new Settings();
        }
        return singleInstance;
    }
    private Settings() {}
}

显然上面的代码在Settings完成构造比较耗时的情况下存在多线程问题, 所以可以加个同步:

public final class Settings {
    private static Settings singleInstance;
    public static final synchronized Settings getSingleInstance() {
        if (singleInstance == null) {
            singleInstance = new Settings();
        }
        return singleInstance;
    }
    private Settings() {}
}

但是这样每次都获取实例都要锁一下, 会产生很多不必要的性能损耗, 尽管你可能说如果jvm确定不会产生多线程访问, 在优化时会做自动锁消除, 但有太多外界代码可以干扰jvm的这种”确定”, 进而你也不应该假设这种不确定的”确定”. 为了解决这个问题, 可以使用争议颇多的”double-check-lock”:

public final class Settings {
    private static Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            synchronized (Settings.class) {
                if (singleInstance == null) {
                    singleInstance = new Settings();
                }
            }
        }
        return singleInstance;
    }
    private Settings() {}
}

因为问题出在当单例实例未构造好的情况下, 如果第二个线程访问, 此时因为赋值语句还没执行, 所以还是null, 导致第二个实例被建. 所以加完锁后再判断一次, 因为加完锁就是串行执行, 而一旦实例已经建好, 也不会走加锁那一条分支.

但至少在java中, 因为构造实例并赋值这个操作不是原子的, 它被分割成多条虚拟机指令, 且java语言规范和虚拟机规范中并没有强制这几条指令的顺序, 即Java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(Java Memory Model)中Cache、寄存器到主内存回写顺序的规定, 所以在乱序中, 可能存在先赋值后构造实例, 具体来说, 首先是分配内存, 正常情况下第二步应该是调用构造器, 然后赋值, 然而也可能是先将变量引用指针指向分配的内存(赋值), 然后在调用构造方法, 怎么执行可能取决于虚拟机实现和优化方案. 不过确定的是理论上在多线程的情况下, 有可能第二个线程使用未完成构造的”半成品”.

JDK1.5之后,官方已经注意到这种问题,因此调整了JMM、具体化了volatile关键字(参见JSR-133内存模型规范或Java语言规范), 可以说保证了在使用volatile时, 所有的写(write)都将先行发生于读(read), 不会打乱单线程内构造赋值这一段的指令执行顺序, 可参考一下这里http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html (如果链接无效, 请见谅). 而JDK1.5前, volatile使变量可见, 立刻看到了赋值, 加剧了问题重现的概率, 尽管我从未试出这种情况. 故在JDK1.5及以上, 可以安全的使用下面的实现, 不过volatile或多或少也会影响到性能:

public final class Settings {
    private static volatile Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            synchronized (Settings.class) {
                if (singleInstance == null) {
                    singleInstance = new Settings();
                }
            }
        }
        return singleInstance;
    }
    private Settings() {}
}

或者这样写也行:

public final class Settings {
    private static volatile Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance != null) {
            return singleInstance;
        }
        synchronized (Settings.class) {
            if (singleInstance == null) {
                singleInstance = new Settings();
            }
            return singleInstance; //return in synchronized block
        }
    }
    private Settings() {}
}

那JDK1.5之前怎么办, 有人推荐使用内联机制避免上述的指令无序的bug. 比如这样解决:

public final class Settings {
    private static volatile Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            synchronized (Settings.class) {
                if (singleInstance == null) {
                    singleInstance = createInstance();
                }
            }
        }
        return singleInstance;
    }
    private static final Settings createInstance() {
        return new Settings(); //maybe inline
    }
    private Settings() {}
}

最新的jvm在优化上更自主, 内联不在依赖任何关键字特征(比如final), 但一般静态工厂方法容易得到内联, 这部分的资料我从哪里见过, 不过理由已经忘了. 也许, 只能是也许, 这在JDK1.5前会是个说得过去的方法, 前提是这些发行版本中内联是确定的, 内联对指令顺序也是确定的.
还有人提出这样的解决方案:

public final class Settings {
    private static Settings singleInstance;
    public static final Settings getSingleInstance() {
        if (singleInstance == null) {
            Settings settings;
            synchronized (Settings.class) {
                settings = singleInstance;
                if (settings == null) {
                    synchronized (Settings.class) {
                        settings = new Settings();
                    } // release inner synchronization lock
                }
                singleInstance = settings;
            }
        }
        return singleInstance;
    }
    private Settings() {}
} 

这段代码将Settings对象的构造放在了内部里层的synchronized同步块中, 并赋值给临时变量, 期望这个同步锁的释放位置存在内存屏障, 且能阻止构造初始化指令和赋值分配指令间的重排序, 从而对临时变量settings的操作不会影响singleInstance, 直到”singleInstance = settings;”被执行之前, singleInstance还是null. 但是它是不会正确工作的, 尽管逻辑上构造和赋值之间的指令重排序已经不会影响用户使用”半成品”, 但它存在可见性问题, 更重要的是, 同步块的释放保证在此之前–也就是同步块里面–的操作必须完成, 但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行. 就是说编译器完全可以把”singleInstance = settings;”这句移到内部同步块里面执行. 鉴于篇幅就不多说了, 这种方式就不应该使用, 如果还不理解, 至少我是参考的这个: http://www.cs.umd.edu/~pugh/java/memoryModel/BidirectionalMemoryBarrier.html(如果链接无效, 请见谅), 不妨您也看看.

其实也可以不上锁, 不依赖JDK版本解决线程安全和延迟初始化这两个问题, 就是利用静态内部类被主动使用时才初始化加载的机制实现”懒汉式”单例:

public final class Settings {
    private static final class SettingsHolder {
        private static final Settings singleInstance = new Settings();
    }
    public static final Settings getSingleInstance() {
        return SettingsHolder.singleInstance;
    }
    private Settings() {}
}

那怎么办到线程安全的呢? 这种方式本质上并没有解决指令重排序的问题, 而是采用了另一种思路, 就是不让其他线程看到指令重排序的变化来规避了问题. 什么意思呢?
Java语言规范规定, 对于每一个类或接口, 都有一个唯一的初始化锁与之对应. 至于怎么对应由JVM自由实现. 多个线程可能在同一时间尝试去初始化同一个类或接口, 未避免重复初始化, 谁第一个获得初始化锁就成为构造线程, 其他线程只能等待构造完成, 尽管其他线程也会获得一次初始化锁, 以确保这个类已经被初始化过了. 构造线程执行类的静态初始化, 并初始化类中的静态属性, 其实这里也会涉及赋值和调用构造器的顺序无序的问题, 但是因为其他线程还在那等着呢, 看到了这个变化也只能表示无力. 所以是线程安全的, 更细致的过程描述, 可参考http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization (如果链接有效, 请欢呼).
虽然这种方式没有多线程问题, 也没有提前初始化, 弥补了上面几种实现的弊端, 但是它也有自己的弊端:(1)不适于带参构造的情况, 如果非要在初始化中使用参数, 只能使用成员方法, 然后团队做一下约定;(2)不易于单元测试, 这在java web中不是个好消息, 因为无法注入通过静态类方法来获取到的对象的mock;

接下来再看一种《Effective Java》作者, 大师级人物Josh Bloch提倡的方式, 就是使用枚举类型:

public enum Settings {
    SINGLE_INSTANCE;
    //Any code same as in class here
}

java中enum枚举类型其实是一个继承自java.lang.Enum< T >的, 且不可被继承的final class. 里面有一个public static final T INSTANCE常量域, T指枚举名, 这个常量域通过static代码块初始化, 被赋予一个new出来的T类型实例, 且不管你在声明枚举的时候是否定义了构造器(包括带参构造器), 其构造器都会被声明为private, 所以枚举变量其实就是类的静态常量, 此外, 这个类里会替你定义一个private static final Single[] $VALUES, 一个静态公开的values()和valueOf(String)方法, 而且编译器替你把关这个类的实例化和序列化, 哪怕在enum静态内部类里都无法new这个枚举实例. 每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,
另外Java的序列化规范中指出: 在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中, 反序列化的时候则是通过java.lang.Enum的valueOf(Class, String)方法来根据名字查找枚举对象, 所以还是那一个. 同时, 编译器是不允许任何对这种序列化机制的定制的, 因此禁用了writeObject, readObject, readObjectNoData, writeReplace和readResolve等方法(详细可查看http://docs.oracle.com/javase/1.5.0/docs/guide/serialization/spec/serialTOC.html). 且如果已经序列化某个枚举, 对它的任何改变(增删枚举变量)都可能导致反序列化失败, 这恰恰给实现单例提供了方便;
它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象, 写法也简单. 但是它的缺点是无法用于带参构造, 无法继承其他类(可以实现其他接口), 而且enum是在JDK1.5才加入的, 之前版本无法使用, 所以用的人比较少, 对于初级选手不容易一目了然的认出这就是单例;

另外我在网上搜到了一些别出心裁的实现方式, 比如下面这种:

public final class ConcurrentMapSingleton {
    private static final ConcurrentMap<String, ConcurrentMapSingleton > singlePool = new ConcurrentHashMap<>();
    private static volatile ConcurrentMapSingleton singleInstance;
    public static final ConcurrentMapSingleton getSingleInstance() {
        if (singleInstance == null) {
            singleInstance = singlePool.putIfAbsent("SINGLE_INSTANCE", new ConcurrentMapSingleton());
        }
        return singleInstance;
    }
}

有人称它为”登记式”单例, 再比如下面这个:

public final class AtomicBooleanSingleton {
    private static final AtomicBoolean initialized = new AtomicBoolean(false);
    private static volatile AtomicBooleanSingleton singleInstance;
    public static AtomicBooleanSingleton getSingleInstantce() {
        checkInitialized();
        return singleInstance;
    }
    private static final void checkInitialized() {
        if(singleInstance == null && initialized.compareAndSet(false, true)) {
            singleInstance = new AtomicBooleanSingleton();
        }
    }
}

这两个实现是借助并发库辅助类的优势, 但依旧有可能会被重复构造多次, 有可能会产生空指针异常, 有可能导致不完全构造的问题, 可能也不够简洁, 出乎了你心中对单例模式的想象, 不过这种不断要求创新的精神还是很值得鼓励的.

如果你想让某个类在每个需要使用它的线程都有且仅有一个实例, 可以使用ThreadLocal + double check lock 实现, 这里是线程安全的, 且延迟了初始化, 不过被爆有性能问题, 运行不快:

public final class PerThreadSingleton {
    private static final ThreadLocal<PerThreadSingleton> perThreadInstances = new ThreadLocal<>();
    private static PerThreadSingleton threadSingleInstance;
    public static final PerThreadSingleton getThreadSingleInstance() {
        if (perThreadInstances.get() == null) {
            createThreadSingleInstance();
        }
        return threadSingleInstance;
    }
    private static final void createThreadSingleInstance() {
        synchronized (PerThreadSingleton.class) {
            if (threadSingleInstance == null) {
                threadSingleInstance = new PerThreadSingleton();
            }
        }
        perThreadInstances.set(threadSingleInstance);
    }
    private PerThreadSingleton() {}
} 

或者这样(可能重复创建多个实例, 但使用过程中是线程单一实例):

public final class PerThreadSingleton {
    private static final ThreadLocal<PerThreadSingleton> perThreadInstances = new ThreadLocal<>();
    public static final PerThreadSingleton getThreadSingleInstance() {
        if (perThreadInstances.get() == null) {
            perThreadInstances.set(new PerThreadSingleton());
        }
        return perThreadInstances.get();
    }
    private PerThreadSingleton() {}
}

看了这么多单例的实现方式, 到底应该用那种呢? 我们做开发设计工作的时, 应当既要考虑到需求可能出现的扩展与变化, 也应当避免”幻影需求”导致无谓的提升设计, 实现复杂度, 最终反而带来工期, 性能和稳定性的损失. 设计不足与设计过度都是危害, 所以说没有最好的单例模式, 只有最合适上下文的单例模式, 不过理解每种模式出现的原因和解决的问题还是好的.

还没结束, 单例模式是需要团队默契和约束的, 如果真的想破坏, 你会发现单例很脆弱, 容易有意无意的被攻破, 导致多个实例产生, 大致有这么几个问题需要解决:

  • new出多个实例
  • 单例实现cloneable, 拷贝出多个实例
  • 序列化反序列化出多个实例
  • 反射创建实例
  • 使用多个类加载器加载单例类, 导致多个实例并存

我们现在来解决一下这几个问题的解决对策:

  1. 只要将类中的所有构造器声明为private, 即可避免从该类外部实例化该类, 但你无法避免有些人罔顾你做成单例的意图, 肆无忌惮的执意在类内部对其实例化, 这个问题只能做团队约定了, 说句题外话, 个人觉得设计模型, 类层次, 架构是个需要谨慎, 丰富经验, 影响深远的事, 但它往往也是赋予创造性和自由的事, 包括你可以引用其他模块和类库来搭建你的意志. 不同的, 编码看起来是个影响范围很小的事, 但却该看作是一个严如军令, 程式化的, 重复的, 需要奖惩的事;
  2. 对单例做成可clone的, 是个非常不明智的错误, 禁止实现cloneable接口, 不过这个依据是团队约定的问题;
  3. 为了维护和保证是单例, 应该避免序列化, 比如使用枚举方式实现单例, 但如果非要单例类需要实现序列化, 需要把所有的成员属性声明为transient(这样该字段不会被序列化), 并提供一个readResolve方法返回原单例实例(序列化这块可查看http://docs.oracle.com/javase/1.5.0/docs/guide/serialization/spec/serialTOC.html). 这里以简洁的”饿汉式”为例:
public final class Settings implements Serializable {
    private static final long serialVersionUID = 6825273283542226860L;
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
    // use transient like following
    private transient String id;
    // ... other transient property or field
    private Object readResolve() throws ObjectStreamException {
        // NOTICE: The method is defined as follows: 
        // ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
        return getSingleInstance();
    }
}
  1. 抵御反射创建实例, 如果是枚举方式实现, 可忽略这个问题, 如果声明了私有构造器, 可以在构造器中检查, 在它被要求创建第二个实例时抛出异常, 像下面这样:
public final class Settings implements Serializable {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {
        if (singleInstance != null) {
            throw new IllegalStateException("attempt to create second instances");
        }
        // maybe other code here
    }
}

或者如果你的团队有自己的全局安全权限检查策略, 且厌恶了暴力反射, 可以考虑下面这种方式, 下面的只是demo, 必须根据实际情况修改才能使用, 亦或者如果你在开发服务端应用, 应用服务器可能提供相关的后台配置, 那就需要你了解这些配置了, 另外这种方式再Android中不起作用, 因为Android中SecurityManager是个空壳, 没有实现. 再次强调, 以下的仅作为参考思路:

public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {}
    private String id = "main";
    public String getId() {
        return id;
    }
    public static void main(String[] args) throws Exception {
        //if use app server, it may be offer security policy
        SecurityManager securityManager = new SecurityManager();
        System.setSecurityManager(securityManager);
        securityManager.checkPermission(new ReflectPermission("suppressAccessChecks"));

        Class<?> clazz = Class.forName("Settings");
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Settings settings = (Settings) constructor.newInstance();
        System.out.println(settings.getId());
    }
}
  1. 这里不考虑远程, 因为时候情况下, 一旦涉及远程, 意味着你根本无法控制全部风险. 所以仅限在一个jvm内. 对于抵御多个类加载器的攻击, 使用枚举方式实现单例是没有用的, 而且我的认知是, 涉及多个类加载器必然使用反射方式, 只不过这次不是调用私有构造器, 而是调用公开的静态工厂方法获取新实例. 主要的思路有两个: (1)只允许一个类加载器加载; (2)不管多少类加载器加载, 仍能返回同一实例; 显然后者难度较大, 实现复杂, 这里也有两个方向, 一个是预先由一个类加载器加载, 其他类加载器加载时寻找预先加载的类实例, 这样其实与(1)的思路有重叠的地方, 但是怎么让其他类加载器知道被预先加载的事已经超出了单例实现的范畴, 你可能为此需要自定义一个类加载器并查找已经加载的类; 另一个方向是使用动态代理, 并仅暴露代理类, 这样显然会给使用过程带来不便, 而且代码写起来也不简单, 理论上确实是可行的, 不过由于才疏学浅我自己没试出来, 灵感来自http://surguy.net/articles/communication-across-classloaders.xml. 所以这里主要还是考虑(1).
    如果只允许一个类加载器加载, 可以在构造器(提倡)或静态代码块(不提倡)里检查类加载器是否是系统类加载器实例或者是否是你指定的自定义类加载器类型,如果不是则抛出异常. 如果您还要更好的方式, 请不吝赐教. 下面还是使用”饿汉式”为例:
public final class Settings {
    private static final Settings singleInstance = new Settings();
    public static final Settings getSingleInstance() {
        return singleInstance;
    }
    private Settings() {
        if ((Settings.class.getClassLoader() != ClassLoader.getSystemClassLoader())) {
            throw new IllegalStateException("only system class loader can load and init me");
        }
    }
}

现在问题差不多解决完了, 看起来单例真的可以很复杂, 所以开头就建议慎用单例, 对于像管理者角色的类或者定制全局的类使用单例还是有意义, 甚至是必要的, 比起静态类也更为合适, 这里的静态类不是指内部静态类, 也不指作为utils工具角色的, 或者抽象工厂角色的, 里面全是静态方法的类, 而是指直接使用类本身静态域作为状态, 不使用实例对象的类, 当然这里的方法也都是静态方法才能操纵修改静态域. 比起单例, 更应慎用静态类, 它有如下劣势: 更为面向过程, 结构化编程; 无法实现接口, 无法享受多态和继承覆写的好处(可以继承静态类, 可以重载); 无法延迟初始化, 对于复杂初始化要靠静态代码块, 静态代码块容易产生各种坑; 比单例更难于模拟, 不方便单元测试.

最后谈谈我实践单例的心得, 比较有用的单例实现有静态工厂的”饿汉”, double-check-lock的”懒汉”, 静态内部类, 枚举, 并发Map做”登记” 这几种, 少吗? 不少, 不过还是那句话, 没有最好, 只有最适合, 如果全了解每一种实现及其演变, 并知道各自利弊, 还能在应用场景找到最适合, 甚至在此基础上创造出最适合的, 那就更好了! 但大多数人都有偏好, 他们大多习惯并一如既往的只使用一种, 或许是因为他们对这一种的优劣更了然于胸(只应该用自己熟悉的, 以避免不必要的风险). 问题是这种情况下, 你看到一种不熟悉的单例而又没有认出怎么办? 呜呼, 还有很多种实现版本都没有列举, 比如如果你决定全程使用反射实现单例, 谁能拦得了? 显然这种不能被提倡, 所以团队做出约定, 应该用什么样的方式实现, 能否稍稍自由? 刚刚解决上面的问题很多也都依赖团队约定, 其实绝大多数情况下不会有人那么极端, 但是在长期维护和兼容问题的压力下, 无意识的犯错不会存在? 所以我提倡这样一个想法:(带冒号的)”声明式编程”.

声明式编程是新兴语言倡导的, 它包括支持函数式编程的语言和DSL, 比如groovy, scalar, python等, java8也已然为此做出了很多努力. 不过我说的这个范围更广, 也更模糊, 我把凡是在不造成性能可观损失的前提下, 更直观的显示代码要做什么, 而不是怎么做, 要充当什么角色和履行什么职责, 以及直观规避风险, 降低学习成本和维护成本, 这方面的努力都作为”声明式编程”的实践. 比如java为了保证兼容性, 无论是语言本身还是类库, 都有各种坑和陷阱, 这些实践至少希望帮助减少这些项目风险, 不过我的微薄努力以后另起新文再谈吧. 把相关单例的部分说一下, 你可以定义一个注解@Singleton, 标明这个类是单例实现, 团队成员看到这个注解就不应该破坏它原有的意图(当然如果你确信可以改变实现或者废弃这个类). 这样就完了? 不, 你还可以像Lint那样通过这个注解检查代码有哪些问题, 比如继承Cloneable时给出警告, 类内有其他实例创建语句时给出警告, 没有使用volatile做double-check-lock给出警告, 非私有构造给出警告, 等等. 但是我们不使用反射, 而是在源代码级别做分析检查. 那怎么做源码分析呢?
最好是借助IDE的优势提供自定义plugin, 比如如果团队成员是用eclipse开发, 可以基于org.eclipse.jdt.core.jar分析源代码, 如果是intellij IDEA开发, 可以从idea.jar中com.intellij.psi和com.intellij.lang着手, 不过这两个IDE都不是小项目, 缕清思路找到一个合适的下手点需要耗费些时日. 还有就是参考checkstyle或者PMD(注: Findbugs, JDepend是基于类文件而不是源文件), 这种静态源代码分析工具的思路, 他们都是基于AST(Abstract Syntax Tree)语法解析工具, 其中checkstyle是基于ANTLR, PMD是参考JavaCC(注: 其实javaparser项目也是基于JavaCC). 也可以使用我采用的方式, 就是通过javac tool(包括rt.jar和tools.jar, 注: tools.jar和glassfish都集成了codemodel项目去生成源码)完成, 参考网址是http://docs.oracle.com/javase/8/docs/ 选javac看API Specification. 如果采用的注解生成器呢, 可以使用javapoet提高生成java文件的效率.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值