关于开发注意事项的经验之谈

很多项目的祖传代码总会有一些比较调皮的问题,比如上百行的if-else或者switch-case,比如成百上千的无用对象的创建,比如一看就头疼的代码,反正就是这样那样的问题,造成无法维护和扩展,可读性差,甚至想打一套军体拳。如果你不想被后人打死,那么在开发和维护的时候,有些地方需要注意一下。

1. 类、方法、变量的命名

如果有疑问,建议改行;

2. 遍历Map类集合KV

使用entrySet,不要用keySet,因为keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从hashMap中取出key对应的value,entrySet是遍历一次就把key和value放入entry中,效率更高;

java8推荐使用forEach:

  Map<String, String> map = new HashMap<>();
  map.put("1", "sb");
  map.forEach((k, v) -> {
      System.out.println(k);
      System.out.println(v);
  });

3. 包装类创建用valueOf

如果需要创建比如Integer之类的包装对象,用valueOf方法,不要用new,Integer会在[-128,127]之间对值进行缓存,避免对象分配,区间之外才会new;

 public static Integer valueOf(int i) {
     if (i >= IntegerCache.low && i <= IntegerCache.high)
         return IntegerCache.cache[i + (-IntegerCache.low)];
     return new Integer(i);
 }

4. 本地变量赋值从不使用

就是下面这样的骚操作,我还是真见过。

List<String> list = new ArrayList<>();
// 其他代码...
list = getList();

我也不知道为什么要new,例中list引用指向其他对象后,原来创建的对象成了无谓的开销;

5. 不要在循环遍历集合的时候remove/add元素

  1. 通过变量i索引遍历,删除或增加元素后List的容量改变,原索引指向其他元素,后续无法通过索引正确操作;
  2. 通过foreach遍历,删除或增加时会抛出ConcurrentModificationException,因为foreach会生成iterator,保存一个expectedModCount,遍历时增删导致ModCount变化,两者不相等,抛出异常;

所以只能通过iterator方式迭代操作;

6. IO流关闭

关闭的方法有很多,一定记得关掉就对了;

7. 拼串用StringBuffer/StringBuilder,而非String

用String做字符串拼接会造成内存浪费,反编译出的字节码文件显示每次循环都会new一个StringBuilder,然后进行append操作,最后toString返回String对象,所以不要在循环体内 str += “sb”;
在这里插入图片描述

8. 慎用clone

List<String> list = new ArrayList<>();
//...bla_bla_bla
List<String> list1 = list.clone();

想像上面这样拿到对象副本分开操作,这是在骗自己,你会发现副本对象add/remove操作的时候,原对象也会跟着add/remove,也就是改变是相互影响的,并没有达到创建副本的目的;
要实现深克隆要么实现Cloneable接口重写clone方法,但是如果对象里面引用对象,层数过多就得每个引用的对象都操作一次;要么将对象序列化成流,再把流反序列化成对象;

9. 单例模式

懒汉单例和饿汉单例都不科学,懒汉有线程安全问题,加锁又影响性能;饿汉在类装载时就初始化,没有达到懒加载的效果;
推荐使用登记式单例或者枚举,其中枚举能解决序列化和反射创建单例对象的问题,即其他方式通过反射创建的两个对象仍然不同,有被攻击的风险

登记式单例:

public class Singleton {
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton(){}
    
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

枚举单例:

public enum SingletonEnum {
    
    INSTANCE;
    
    public SingletonEnum getInstance() {
        return INSTANCE;
    }
}

10. 创建集合尽量指定初始大小

即在创建ArrayList或者HashMap的时候,如果已知元素数量,就传入构造方法,避免由于频繁扩容带来的开销;
jdk1.7中,HashMap在多线程环境下线程不安全的原因就是扩容可能造成链表死循环;

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

11. 方法内代码不宜过多

经常能见别人的代码一个方法内几百行代码,可读性很差,如果你的方法一个屏幕都装不下,那要考虑把代码抽取出来;

12. 异常处理

不要一言不合就全捕获Exception,具体异常单独捕获,而且方便问题的定位排查,业务要处理好异常捕获后的工作,到底是抛出到上一级还是做其他处理,都会影响代码质量,但是千万不要试图用异常去控制业务流程走向
有两点关于异常的操作提醒:

  1. 日志打印异常log.error(e)或者log.error(e.getMessage())都不能获取异常的追溯,即无法知道是哪一行抛出的异常,要使用log.error(“msg”, e)才能完整打印异常;
  2. 事务的回滚必须抛出未捕获的运行时异常,即非受检异常:
    如果发生运行时异常,你捕获了异常并且直接处理了,那么事务是不会回滚的,可以在catch里将RuntimeException抛出,或者不去try/catch,等全局异常aop去处理;
    如果发生受检异常,无论在方法签名上抛出,还是直接try/catch处理,事务都不会回滚,catch块抛出非受检异常也不合适,可以使用 @Transactional(rollbackFor = Exception.class),抛出受检异常和非受检异常时都能回滚;

13. 禁用Executors创建线程池

这个是阿里开发手册的规定,要通过ThreadPoolExecutor的方式,简单的说,就是必须指定线程池构造参数策略,避免引起OOM异常;还有,记得shutdown;

14. ThreadLocal使用完及时remove避免内存泄漏

下面这篇文章讲过ThreadLocal与内存泄漏问题:
https://blog.csdn.net/unclecoco/article/details/103176609

15. String判断

对于String类型值的判断,是否不为null或者不为空串,建议使用StringUtils的isBlank或者isEmpty(两者区别是对换行符、回车符和空格的判断,isEmpty判断空格是false);判断String和某个字符串是否相等,养成常量写前面的习惯:“str”.equals(str),避免空指针异常;

16. 不要使用魔鬼数字

foo(1),return 1,写盲人代码呢?看看HashMap,容量,装载因子这些都用了常量去定义;在增删改的时候,建议定义一个枚举去解释特定数字比如-1, 0 ,1等,然后返回枚举值,这样代码容易看懂。

public enum InsertStatus {

    INSERT_FAILED(0),
    INSERT_SUCCESS(1),
    INSERT_DUPLICATEKEY(-2),
    INSERT_UNKNOWNERROR(-1);

    private int insertCode;

    InsertStatus(int insertCode) {
        this.insertCode = insertCode;
    }

    public void setInsertCode(int insertCode) {
        this.insertCode = insertCode;
    }

    public int getInsertCode() {
        return insertCode;
    }
}

17. 尽可能手动释放不再使用的大对象

大对象如果没有及时被垃圾回收,就有可能造成OOM,如果当前线程由于某些原因没有结束,甚至会影响其他线程申请内存;

引申一道面试题:如果一条线程OOM,其他线程还能否继续工作?
答: 如果不做异常捕获,当前异常线程OOM后,线程结束,GC会释放资源,其他线程能正常工作;如果做了异常捕获,catch后不结束,线程未异常退出,引用对象不释放,其他线程则无法工作。

18. 过多if-else或者switch-case

这在祖传代码里面最常见了,多一个场景就多一个if或case,谁也不愿意重构,大家一起开心。太多if-else其实也并不慢,只是难看而已。
解决方案:

  1. 反射;把所有场景放入一个map,key代表场景,value代表场景对应要执行的方法名称,可以写在静态代码块或监听器,反正在项目启动时执行,然后传入场景,去map里匹配,匹配到哪个场景,就反射执行哪个方法,这样不需要if-else或switch-case了,增加场景的时候在map里put进去;
String methodName = map.get(scene);
Class clz = Class.forName("com.xxx.xxx.Bean");
Method method = claz.getMethod(methodName);
method.invoke(clz.newInstance());

结论:遇到这种人大家一起孤立他,就反射那执行效率(不绝对),过程中创建对象既浪费时间又浪费了空间,在业务场景中不适合用反射来写业务代码,为什么提出来,单纯是因为我优化时这样干过而已;

  1. 工厂模式 + 策略模式
    设计一个接口,抽象出所有场景;
public interface SceneService {

    public void doSomething();
}

设计所有场景类,每个场景就是一个策略;除了实现SceneService接口,还有InitializingBean接口,在bean完成注册后会执行重写的afterPropertiesSet方法,在这个方法里,我们把场景bean注册到工厂里面,这个工厂就可以根据传入的类型取出相应的场景对象;

@Service
public class Scene1 implements SceneService, InitializingBean  {
 
    @Override
    public void doSomething() {
    	// doSomething when scene1...
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        StrategyFactory .register("scene1",this);
    }
}
@Service
public class Scene2 implements SceneService, InitializingBean  {
 
    @Override
    public void doSomething() {
    	// doSomething when scene2...
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        StrategyFactory.register("scene2",this);
    }
}

创建工厂类,上面说了工厂在项目启动时就存储了场景实例;工厂主要维护一个map,提供注册和获取的方法;

public class StrategyFactory {
 
    private static Map<String, SceneService> map = new ConcurrentHashMap<>();
 
    public static SceneService getScene(String type){
        return map.get(type);
    }
 
    public static void register(String type, SceneService sceneService){
        map.put(userType,sceneService);
    }
}

应用:

SceneService sceneService = StrategyFactory.getScene(type);
sceneService.doSomething();

19. 六原则一法则

单一职责原则,里氏替换原则,组合/聚合复用原则,依赖倒转原则,开闭原则,接口隔离原则,迪米特法则(高内聚,低耦合);

你可能感兴趣的:(Java)