代码首先是给人看的,所以写代码要有写诗一样的感觉(哈哈雷军说的),无论是写逻辑、中间件或是写框架都是如此。
想要写好代码首先基础一定要好,所以我最近重新看了Effective Java,虽然这已经不是第一次看了但是还是有很多可以学习的地方。
本系列文章是总结Effective Java文章中我认为最重点的内容,给很多没时间看书的朋友以最短的时间看到这本书的精华。
第二章创建和销毁对象
第1条:考虑用静态方法代替构造器
优势
第一大优势在于,他们有名称。
这个很好理解,他主要解决的是构造方法的重载问题,如下代码:
public class CustomDialog {
//构造一个有标题,有内容,有两个按钮的Dialog
public CustomDialog(String title,
String msg,
String leftButton,
String rightButton,
Object leftOnClickListener,
Object rightOnClickListener) {
}
//构造一个无标题,有内容,有两个按钮的Dialog
public CustomDialog(
String msg,
String leftButton,
String rightButton,
Object leftOnClickListener,
Object rightOnClickListener) {
this(null, msg, leftButton, rightButton, leftOnClickListener, rightOnClickListener);
}
//构造一个有标题,有内容,有一个按钮的Dialog
public CustomDialog(String title,
String msg,
String leftButton,
Object leftOnClickListener) {
this(title, msg, leftButton, null, leftOnClickListener, null);
}
}
如果程序中的公共组件这么写,那么调用这个Dialog组件将是非常麻烦的一件事,每次创建的时候都要看文档,或者点进去看源码非常不直观,如果将代码修改成如下方式:
public class CustomDialog {
//构造一个有标题,有内容,有两个按钮的Dialog
public static CustomDialog createCustomDialog(){
return null;
}
//构造一个无标题,有内容,有两个按钮的Dialog
public static CustomDialog createNoTitleCustomDialog(){
return null;
}
//构造一个有标题,有内容,有一个按钮的Dialog
public static CustomDialog createSingleButtonCustomDialog(){
return null;
}
}
//使用测试代码
public class MainActivity extends Activity {
private CustomDialog customDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//很直观的展示了你创建的Dialog的形式
customDialog = CustomDialog.createCustomDialog();
customDialog = CustomDialog.createNoTitleCustomDialog();
customDialog = CustomDialog.createSingleButtonCustomDialog();
}
这种静态方法因为他有名字,所以创建对象实例的时候非常直观。查看代码的人也很清楚就可以看到你创建的Dialog是什么形式的,是否有标题,是否是单个Button,不用查看文档或者进入源码进行查看。
第二大优势在于,不必在每次调用他们的时候创建一个新的对象。
主要应用于单例模式,如下代码:
public class SingletonClass {
private static volatile SingletonClass instance = null;
public static SingletonClass getInstance() {
if (null == instance)
synchronized (SingletonClass.class) {
if (null == instance) {
instance = new SingletonClass();
}
}
return instance;
}
private SingletonClass() {}
}
第三大优势在于,它们可以返回原返回类型的任何子类型的对象。
面向接口编程,创建对象的方法返回接口,如下代码:
//Mock本地测试数据的类
public class MockDataRespsitoryManager implements IDataRepositoryManager {
......
}
//访问网络的管理类
public class DataRepositoryManager implements IDataRepositoryManager {
......
}
@Singleton
@Module
public class DataRepositoryModule {
......
@Singleton
@Provides
public IDataRepositoryManager providerRepositoryManager(Retrofit retrofit, RxCache rxCache) {
return new DataRepositoryManager(retrofit, rxCache);
}
@Singleton
@Provides
@MockData
public IDataRepositoryManager providerMockRepositoryManager(Application application, @Nullable DataRepositoryModule.MockDataConfig mockDataConfig) {
return new MockDataRespsitoryManager(application,mockDataConfig);
}
......
}
//构造方法传入的是上两个方法返回的对象
public class BaseModel {
protected IDataRepositoryManager repositoryManager;
public BaseModel(IDataRepositoryManager repositoryManager){
this.repositoryManager = repositoryManager;
}
public void onDestory(){
this.repositoryManager = null;
}
}
如上代码片段BaseModel
类的构造方法在正常情况下接收providerRepositoryManager
返回的对象链接网络获取数据,开发前期接收providerMockRepositoryManager
返回的对象从本地读取Mock数据进行调试。
第四大优势在于,在创建参数化实例的时候,他们使代码变得更加简洁。
主要用在创建泛型对象,如下代码:
public class AClass{
public static class BClass{
public static class CClass{
public static class DClass{}
}
}
}
//普通创建Map集合实例,非常冗长。
Map map = new HashMap();
//应用静态工厂方法
public static HashMap newInstance(){
return new HashMap();
}
//应用静态工厂方法创建泛型对象代码简洁了很多。
Map map = newInstance();
缺点
主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化。
还是拿单例模式来举例子:
public class SingletonClass {
private static volatile SingletonClass instance = null;
public static SingletonClass getInstance() {
if (null == instance)
synchronized (SingletonClass.class) {
if (null == instance) {
instance = new SingletonClass();
}
}
return instance;
}
//构造方法是private不是public也不是protected,所以单例不能实现继承
private SingletonClass() {}
}
第二个缺点在于,它们与其他的静态方法实际上没有任何区别。
创建对象静态方法和其他的静态方法没有任何区别,所以如果命名不规范,使用者很难找到创建对象的静态方法。
命名规则例如:
public static Object createXXXX(){
return null;
}
public static Object factoryXXXX(){
return null;
}
public static Object newInstance(){
return null;
}
上面代码片段只是一种假设,每个项目的命名规范不一样。只要遵循同一套规范就会使代码清晰可读。
第2条:遇到多个构造参数时要考虑用建造器
建造者模式:是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
概念性的东西真是不好理解,简单解释为什么要用建造者模式,如果创建一个对象需要很多参数,有必传参数有非必传参数最好用建造者模式。
如下代码片段:
代码源码点击这里
代码涉及到dagger2知识点击这里查看简介
@Singleton
@Module
public class AppDelegateConfig {
private final String baseUrl;
private final File cacheDir;
private final RetrofitConfig retrofitConfig;
private final DataRepositoryModule.OkhttpConfig okhttpConfig;
private final DataRepositoryModule.RxCacheConfig rxCacheConfig;
private final DataRepositoryModule.MockDataConfig mockDataConfig;
private final IHttpErrorHandler iHttpErrorHandler;
private final IHttpResponseHandler iHttpResponseHandler;
private AppDelegateConfig(Builder builder){
baseUrl = builder.baseUrl;
cacheDir = builder.cacheDir;
retrofitConfig = builder.retrofitConfig;
okhttpConfig = builder.okhttpConfig;
rxCacheConfig = builder.rxCacheConfig;
mockDataConfig = builder.mockDataConfig;
iHttpErrorHandler = builder.iHttpErrorHandler;
iHttpResponseHandler = builder.iHttpResponseHandler;
}
@Singleton
@Provides
public String providerBaseUrl() {
return baseUrl;
}
@Singleton
@Provides
public File providerCacheDir() {
return cacheDir;
}
@Singleton
@Provides
@Nullable
public RetrofitConfig providerRetrofitConfig() {
return retrofitConfig;
}
@Singleton
@Provides
@Nullable
public DataRepositoryModule.OkhttpConfig providerOkhttpConfig() {
return okhttpConfig;
}
@Singleton
@Provides
@Nullable
public DataRepositoryModule.RxCacheConfig providerRxCacheConfig() {
return rxCacheConfig;
}
@Singleton
@Provides
@Nullable
public DataRepositoryModule.MockDataConfig providerMockDataConfig(){
if(null == mockDataConfig){
return new DefaultMockDataConfig();
}
return mockDataConfig;
}
@Singleton
@Provides
public IHttpErrorHandler providerIHttpErrorHandler(Application application) {
if(null == iHttpErrorHandler){
return new DefaultHttpErrorHandler(application);
}
return iHttpErrorHandler;
}
@Singleton
@Provides
public IHttpResponseHandler providerIHttpResponseHandler() {
if(null == iHttpResponseHandler){
return new DefaultHttpResponseHandler();
}
return iHttpResponseHandler;
}
public static class Builder{
private String baseUrl;
private File cacheDir;
private RetrofitConfig retrofitConfig;
private DataRepositoryModule.OkhttpConfig okhttpConfig;
private DataRepositoryModule.RxCacheConfig rxCacheConfig;
private DataRepositoryModule.MockDataConfig mockDataConfig;
private IHttpErrorHandler iHttpErrorHandler;
private IHttpResponseHandler iHttpResponseHandler;
public Builder(String baseUrl, File cacheDir){
this.baseUrl = baseUrl;
this.cacheDir = cacheDir;
}
public Builder setRetrofitConfig(RetrofitConfig retrofitConfig) {
this.retrofitConfig = retrofitConfig;
return this;
}
public Builder setOkhttpConfig(DataRepositoryModule.OkhttpConfig okhttpConfig) {
this.okhttpConfig = okhttpConfig;
return this;
}
public Builder setRxCacheConfig(DataRepositoryModule.RxCacheConfig rxCacheConfig) {
this.rxCacheConfig = rxCacheConfig;
return this;
}
public Builder setIHttpErrorHandler(IHttpErrorHandler iHttpErrorHandler){
this.iHttpErrorHandler = iHttpErrorHandler;
return this;
}
public Builder setiHttpResponseHandler(IHttpResponseHandler iHttpResponseHandler) {
this.iHttpResponseHandler = iHttpResponseHandler;
return this;
}
public Builder setMockDataConfig(DataRepositoryModule.MockDataConfig mockDataConfig) {
this.mockDataConfig = mockDataConfig;
return this;
}
public AppDelegateConfig builder(){
return new AppDelegateConfig(this);
}
}
}
AppDelegateConfig appDelegateConfig = new AppDelegateConfig
.Builder("baseUrl", new File("cacheDir"))
.setOkhttpConfig(...)
.setMockDataConfig(...)
.setIHttpErrorHandler(...)
.setiHttpResponseHandler(...)
.setRetrofitConfig(...)
.setRxCacheConfig(...)
.builder();
如上代码片段构建AppDelegateConfig
类实例需要8个参数其中baseUrl
和cacheDir
是必传参数。
如果直接用构造器传递参数的方式来创建实例弊端是:
- 构造器参数冗长而且不直观必须参照着源码或者文档来填写参数,而且容易出错,假如参数之间
String
类型较多容易填错 。 - 在众多参数中,必传参数不明显。
建造者模式很容易的解决了这两个弊端:
- 参数通过单独的方法进行设置,方法的名字描述了该参数的意思非常直观例如
setOkhttpConfig
设置Okhttp
配置。不需要的参数不调用setXXX
方法保证了代码的整洁。由于每个参数都是一个方法来配置,也不容易出错。 - 必传参数通过
Builder
建造者的构造方法传入,保证必须设置。
第3条:用私有构造器或者枚举类型强化Singleton属性
1.单例模式要将构造方法设置成私有的private
,代码在上文中已经多次提到这里就不再赘述了。
2.通过枚举来实现单例模式,如下代码:
public enum Singleton {
//定义一个枚举的元素,它就是 Singleton 的一个实例
INSTANCE;
public void doSomeThing() {
// do something...
}
}
1)线程安全
2)防止序列化
3)防止反射攻击
这篇文章分析的很透彻:请点击
第4条:通过私有构造器强化不可实例的能力
主要说的就是工具类(utility class),例如jdk
自带的java.util.Arrays
、java.util.Collections
等他们类中全都是静态方法和静态常量,实例化和子类化对他们一点意义都没有,所以通过私有化构造方法来控制他们不能被实例化或子类化。
public class Arrays {
// Suppresses default constructor, ensuring non-instantiability.
private Arrays() {}
......
}
public class Collections {
// Suppresses default constructor, ensuring non-instantiability.
private Collections() {
}
......
}
第5条:避免创建不必要的对象
防止创建不必要的对象,浪费内存也减慢了程序的执行速度。
介绍了几个常见的例子:
- 创建字符串
String s = new String("stringette");
//应该优化成
String s = "stringette";
第一行代码每次执行的时候都创建一个新的String
实例,非常没有必要。
第二行代码通过虚拟机进行优化,在同一台虚拟机中运行代码,只要他们包含相同的字符串字面常量,该对象实例就不会重新创建会被重用。
- DateUtils 工具类
public class DateUtils {
private DateUtils(){}
//根据pattern来格式化输入的millis
public static String dateFormat(long millis, String pattern) {
//假如这个工具类用来列表中,这个对象会非常频繁的创建,造内存泄漏,程序运行减慢
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
Date date = new Date(millis);
String dateString = simpleDateFormat.format(date);
return dateString;
}
}
//改进版的时间工具类
public class DateUtils {
//静态对象只在类加载的时候创建一次,有效的解决了上面频繁创建对象造成的内存泄漏问题
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
private DateUtils(){}
//根据pattern来格式化输入的millis
public static String dateFormat(long millis, String pattern) {
simpleDateFormat.applyPattern(pattern);
Date date = new Date(millis);
String dateString = simpleDateFormat.format(date);
return dateString;
}
}
- 自动装箱
public class TestClass {
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
}
这段代码的结果是没有问题的,但是由于Java的自动装箱机制,创造出了大约Integer.MAX_VALUE
个多余的Long
实例,造成内存泄漏,程序运行减慢。
所以要优先使用基本类型进行计算,要当心无意识的自动装箱。
第6条:消除过期的对象引用
只要类是自己管理内存,程序员就应该警惕内存泄漏问题。
Java 内存回收就不再这篇文章进行讨论,简单的理解是一个对象没有任何的强引用这个对象就会在内存回收的时刻被回收掉。
如下代码例子:
public static 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 void push(Object obj) {
ensureCapacity();
elements[size++] = obj;
}
//弹出元素会造成内存泄漏,由于栈中的每个对象都是强引用的,
//虽然在这个地方将元素取出size也进行了收缩,但是弹出对象的强引用还是一直由elements数组进行持有,
//在垃圾回收的时候取法将弹出的对象进行销毁造成了内存泄漏
public Object pop() {
if (0 == size) {
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
//改进版的void pop(); 方法
//在弹出元素的同时,收缩列表,将列表中对这个元素的强引用设置成null,
//这样这个弹出的对象在列表中就没有强引用了,他的声明周期完全取决于外部如何用它,
public Object pop() {
if (0 == size) {
throw new EmptyStackException();
}
Object obj = elements[--size];
elements[size] = null;
return obj;
}
第7条:避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必须要的。根据经验:应该避免使用。
建议:所有需要销毁的对象都必须显示的调用终止方法.
例如:InputStream
的close
方法。
缺点:
- 终结方法不能保证及时执行。
- 可移植性问题。由于终结方法的调用以来jvm垃圾回收算法,所以不同的jvm很可能会有不同的结果。
- 终结方法执行的线程(也就是GC的线程)有可能比你应用程序的任何线程优先级都低。假如利用终结方法来释放内存,程序都已经
OutOfMemoryError
死掉了,终结方法还没有调用。 - 终结方法不会抛出异常。终结者方法中如果有异常则不会打印出任何信息,且终结方法会停止,这样对找bug来说真是难上加难。
- 使用终结方法有非常严重的性能损失。书上举例说:正常创建和销毁一个简单对象时间大约为5.6ns。增加一个终结方法使时间增加到2400ns。慢了大约430倍。
既然java提供了终结方法那么它肯定是有用途的。
用途:
- 安全网。当程序中有对象忘记调用显示终结方法,例如
InputStream
忘记调用close
方法。这个方法可以当做安全网,在这个方法中显示调用close
,虽然不能保证终结方法及时的调用,但是迟一点调用总比永远不释放要好得多。如果终结方法中发现资源还未被终止,则应该在日志中增加一条警告。这代表程序中的bug应该得到修复。还要考虑终结方法会影响到性能上面提到过,是否值得这么做。 - 本地对象。在本书中作者用了本地对等体的名词,其实就是Java编程思想中的本地对象,也就是
java
调用C或C++malloc
出来的内存,这些内存如果不显示的调用free
将无法销毁,java
的GC机制也无法销毁这些对象,所以需要在终结方法中调用free
来释放内存。
其实这些本地对象的销毁一定要在程序中显示调用释放方法。
下面代码是如何正确的编写终结方法:
- 终结方法必须调用父类的终结方法,否则当类继承的情况下父类就永远不可能调用终结方法了。这个代码使用
try{}finally{}
代码块在finally{}
中调用super.finalize();
保证就算发生异常也一定会执行父类的终结方法。 - 为了防止粗心大意忘记调用父类的终结方法,有了第二种写法,终结方法守护。如下第二段代码用了一个匿名
Object
内部类来实现终结方法,在这个内部类的void finalize()
方法中去调用Foo
类需要结束的对象,由于这个匿名Object
类没有父类所以写不写super.finalize();
也无所谓。
@Override
protected void finalize() throws Throwable {
try {
//Finalize subclass state
} finally {
super.finalize();
}
}
public class Foo{
private final Object finalizeGuardian = new Object(){
@Override
protected void finalize() throws Throwable {
//Finalize subclass state
}
};
......
}