关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
本文基于Effective Java中创建和销毁对象的章节汇总出8个相关的好习惯(文末附案例地址)
思维导图如下:
1.静态工厂代替构造器
1.可以自定义名称,见名知意
见名知意的方法更容易理解,比如 getInstanceByCode 根据Code获取实例
2.获取对象时,可以使用单例、享元等思想进行复用
Boolean value = Boolean.valueOf(true); public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }
3.可以返回原类型的子类
Collections.singletonList(null); public static <T> List<T> singletonList(T o) { return new SingletonList<>(o); }
4.返回对象可以随着入参发生改变
根据入参key的不同可以获取不同的策略
public static <T extends XXStrategy> T getStrategyByKey(String key) { return (T) strategyFactory.get(key); }
5.返回对象的类可以在编写静态工厂时不存在
在编写静态工厂中获取对象时,对象可以不存在(未编译),通过反射的手段在运行时加载进来
典型的例子就是JDBC,在使用前通过反射加载驱动
Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection("url", "username", "password");
小结
业务中使用场景挺多的,比如:spring的getBean、实现业务的策略工厂(基本上能覆盖1-5特点)
策略工厂可以使用Map维护code以及对应的策略,这样就已经实现1、2、4
实现3时还需要使用泛型进行转换(上面案例给出),使用反射实现5
2.多个构造器参数考虑建造者模式
构造器的入参太多时容易弄混,还好现在的IDE开发工具可以提示参数名称,如果参数名称取的一团糟也会容易混淆
在这种场景下可以选择建造者模式 Builder
//构造器 Student student = new Student("张三", 18, "男", "北京", "13812345678", "", "12345678"); //Builder Student.builder() .name("菜菜的后端私房菜") .age(18) .sex("男") .address("北京") .phone("13812345678") .email("") .qq("12345678") .build();
提高阅读性、多建一个类的开销、可以防止对象在构造期间逃逸
3.私有构造或枚举强化单例
私有构造防止调用构造创建
可以通过字段直接获取单例,也可以通过静态工厂方法获取单例 (饿汉式)
//通过字段 SingletonField singletonField = SingletonField.INSTANCE; //通过静态工厂方法 SingletonStaticFactory singletonStaticFactory = SingletonStaticFactory.getInstance(); //通过枚举 SingletonEnum singletonEnum = SingletonEnum.INSTANCE;
通过反射、JDK序列化依旧可以调用构造,破坏单例
使用枚举避免反射调用单例
反射在实例化前会判断是否枚举,枚举则抛出异常
if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
4.考虑依赖注入
构造器注入,能够灵活使用多种依赖
static class DependencyInjectionNotUse { private Util util = new Util(); public DependencyInjectionNotUse() { } } static class DependencyInjectionUse { private Util util; public DependencyInjectionUse(Util util) { this.util = util; } }
依赖太多在大项目中会很乱,可以使用DI框架如 spring的IOC
5.避免创建不必要的对象
//创建了不必要对象 String a = new String("菜菜的后端私房菜"); String b = "菜菜的后端私房菜";
自动拆装箱,也会创建不必要对象(先转成long相加,再转Long)
Long sum = 0L; for (long i = 0; i <= Integer.MAX_VALUE; i++) { sum += i; // Long.valueOf(sum = sum.longValue() + i); }
多使用单例、享元模式,复用资源,避免创建不必要对象:字符串、自动拆装箱等
6.消除过期的对象引用
Java采用引用计数法,如果不再使用对象时需要置空,否则会认为还要使用,从而导致内存泄漏
public Object pop() { if (size == 0) throw new EmptyStackException(); //出栈时需要将元素清除,不再引用 //elements[size-1] = null return elements[--size]; }
缓存、监听器也可能导致内存泄漏,注意使用弱引用或注意关闭
7.不使用finalize方法
finalize 由守护线程执行,无法预估执行的时机
finalize 为不可达对象才会执行的方法,如果要清楚资源使用finally关闭资源
感兴趣的同学可以查看这篇文章:如何判断对象“已死”
8.try-with-resources优于try-finally
twr会自动关闭资源,优先使用
//try-finally BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { br.close(); } //twr try-with-resources try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); }
总结
考虑使用静态工厂方法代替构造器,静态工厂方法能够见名知意、可以使用单例或享元模式返回对象、搭配泛型返回对象可以是原类型子类、返回的对象可以随着入参改变、返回的对象所在的类可以在编译期不存在
在参数较多时可以考虑使用建造者模式,可以代码可读性更高、防止构造对象期间发生逃逸
可以通过私有构造强化单例,但能够被反射、序列号破坏单例;使用枚举单元素强化单例则可以避免破坏(在反射实例化前判断为枚举则抛出异常)
对象依赖的“工具”不是固定的时,可以采用依赖注入DI的方式进行改变,而不是直接写死;频繁使用DI在大项目中会比较混乱,使用DI框架可以解决,比如 Spring 的IOC
避免创建不必要的对象,如String的字符串常量、基本类型与包装类型的自动拆装箱
消除过期的引用对象:不再使用对象时,需要消除引用关系,否则基于引用计数法的Java则无法给对象进行回收,从而导致内存泄漏
不使用finalize方法:finalize方法是只有对象成为不可达对象才会调用,而且由守护线程执行,无法预估执行时机,不要使用其做清理工作
try-with-resources优于try-finally:twr自动关闭资源,避免忘记,在多层嵌套时阅读性也较好
最后(不要白嫖,一键三连求求拉~)
本篇文章被收入专栏 Effective Java,感兴趣的同学可以持续关注喔
本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava 感兴趣的同学可以stat下持续关注喔~
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜