Effective Java第二版 读后感

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u011109881/article/details/84745065

虽说是读后感,其实我并没有完全读完这本书,中间有些不懂的章节和最后的两章(并发和序列化)是没有看完的,以目前的实力来看,实在看的云里雾里,就决定先放着,看看编程思想后再回头看看。写这篇感想一是为了记录一些东西,方便以后的复习;二是为还没有读过这本书的人提供一个参考,但也仅仅是一个参考而不是一个标准哦。

总的感想

  • 1.注意版本。

买书之前,没有确认版本,结果到的时候才发现第二版的书是09年印刷版,内容是基于jdk5.0 6.0的版本。而最新的第三版是基于较新API的,而且内容已从78条扩充到90条。技术开发,至少作为移动开发者来看,新技术的更新迭代是非常快速的,所以买书的时候看清版本是很重要的,一些过时的经验在新的API中可能是不适用的。第一次在这点上没注意,也算吃一堑长一智吧。

  • 2.适用人群。

确实,如网上多数评论一样,这无疑是一本好书,内容精炼,指点到位。但是正如书中序言所讲的一样,这书不适合初学者。如果你对市面上的一些Java入门书籍没有理解个七七八八,读这本书会很辛苦的。当然你可以备一本参考书,看到不懂的地方参考其他书籍(比如Java编程思想),不过这样会大大降低阅读速度,非常考验人的耐力。

  • 3.书籍特点。

条目式列举,内容精炼,内容交叉。
书籍列举一些编码习惯,使用方法,使用建议,每一个建议作为一个条目,表面上看,条条相分离,但是内里联系千丝万缕。
书中的例子相对入门书籍大幅减少,(否则就无法做到短小精炼了)很多解释都是一句话概括,能不能理解,就得看Java基础和实际经验了,所以如前所述,这并不是适合初学者的书籍。
书中的结构是交叉的,提到第5条可能会涉及第45条和第67条,如果其中有一条不理解,可能就会影响效果。但是这种关联性也便于我们整体把握这本书,让我们了解是否掌握了这本书的内容。

  • 4.关于不理解的部分。

本人对于此书还有很多没有理解的地方,我想原因可能是平时很少使用的部分理解困难(比如并发,序列化),这是因为经验的缺少;另外还有就是基础部分还有待加强,不用的东西忘得自然快,很多东西也许补一下基础知识就能看懂,这也是我为什么建议带本参考书看。
当然我觉得一本书,存在看不懂地方,说明你看这本书是有意义的(暂且假设这是一本值得读的好书)你仍可以从这本书学习到什么,如果读一本书,书中的内容你已完全理解,那么,再继续读下去,其实意义不是很大的,要知道,再读n遍,也许量变产生质变,会产生“顿悟”,但其实是很困难的,而且还可能什么都没有产生。当然,也有其他可能,比如,虽然书中的东西都理解了,但是之前是一团混乱的,但是通过通读,脑子里形成一套体系,这也是有可能的。
一本书,读的时候偶有不理解的地方很正常,不然此书对现在的你来说意义已经不大。但是如果读起来十分不畅,那就可以考虑先放下这本书,它可能并不适合现在的你。这就像武功秘籍一样,入门的还没学会,就贪图深奥的武林绝学,除非是天才,学成的概率可是很小的。
当然,这些只是我的个人想法。
5.关于客户端与服务端
书中提到了很多次客户端,还有导出API等等字样,个人的理解,sun公司是Java SDK的开发者,他说的客户端即是使用Java SDK的终端或者开发者,SDK可以进行打包和导出API,而本书则很大程度是针对SDK开发者的建议。当然,我觉得大多数经验和建议也是适用于客户端开发者的
最后,这本书对于适合的读者来说无疑是本好书。

与实际结合

书中有几个条目,跟我目前的开发经验结合,我觉得其中有这么几条十分受用,列举如下。

  • 1.第二条 遇到多构造器参数时考虑使用构建器

在我所在项目,遇到过类似的代码

    @Override
    public ClassA getClassA(int id, Entity entity, View view, ClassA.ShowType showType) {
        ClassA addressAnnotation = new PorductClassA(ApplicationContextHelper.getInstance().getAppContext(),id, entity, view, showType);
        if (showType == ClassA.ShowType.destination) {
            addressAnnotation.setClickable(false);
        }
        return addressAnnotation;
    }

    @Override
    public ClassA getClassA(int id, Entity entity, EntityFacet facet, View view, ClassA.ShowType showType) {
        ClassA addressAnnotation = new PorductClassA(ApplicationContextHelper.getInstance().getAppContext(),id, entity, facet, view, showType);
        if (showType == ClassA.ShowType.destination) {
            addressAnnotation.setClickable(false);
        }
        return addressAnnotation;
    }

可以看到在上述代码中出现的较多的重复代码,究其原因是创建PorductClassA对象时参数,其构造方法参数不同,那么
如果将第一个重载方法改为

    @Override
    public ClassA getClassA(int id, Entity entity, View view, ClassA.ShowType showType) {
        return this.getClassA(id,entity,null, view, showType)
    }

不就可以重用了么?但是追查到ProductClassA的原代码,看到facet被加上了@NonNull标签,并且它的重载构造器类似这样

    public ClassA(Context context, int id, @NonNull Entity entity, View view, ShowType showType, String priceLabel) {//该方法可以直接调用参数更多的方法
        super(context, id);

        this.entity = entity;

        initGlView(glMapSurfaceView, showType, priceLabel);

        initViews(entity, showType);
    }

    public ClassA(Context context, int id, @NonNull Entity entity, @NonNull EntityFacet facet, View view, ShowType showType, String priceLabel) {
        super(context, id);

        this.entity = entity;

        this.facet = facet;//此处可以判断是否为空 执行调用

        initGlView(glMapSurfaceView, showType, priceLabel);

        initViews(entity, showType);
    }

也就是说在调用方和被调用方,都出现了因参数个数不同,而出现的重复代码。
在EJ中提到了重叠构造器。即

public class Test {
	int number1;
	int number2;
	int number3;
	int number4;
	int number5;
	
	public Test (int number) {
		this(number, 0);
	}

	public Test (int number,int number2) {
		this(number, number2, 0);
	}
	public Test (int number,int number2,int number3) {
		this(number, number2, number3,0);
	}
	public Test (int number,int number2,int number3,int number4) {
		this(number, number2, number3,number4,0);
	}
	public Test (int number,int number2,int number3,int number4,int number5) {
		this.number1 = number;
		this.number2 = number2;
		this.number3 = number3;
		this.number4 = number4;
		this.number5 = number5;
	}
}

形象点的名字可以叫做构造器复用,当参数较多时,使用参数少的构造器调用参数多的构造器。这样,只要处理参数最多的构造器即可。书中的章节提到是使用builder来创建对象,但是,我想到的并不是这个点,我思考到:在因参数个数而重载方法时(包括构造方法和普通方法),如果内容类似,其实可以像上面重叠构造器那样实现代码复用。当然,如果重载的方法因为多加的参数而变得与原来大相径庭,那就还是不要复用了,因为可能会影响代码逻辑的清晰度。像上面我举的项目中的例子,如果去掉@NonNull注解并实现代码复用,虽然无法在编译时实现对象null的检查,但是代码清晰度和重用度却提升了呢。

  • 2.第四十三条 返回零长度的数组或者集合,而不是null

书中列举了一个例子

public Cheese[] getCheeses(){  
if (cheesesInStock.size() == 0) {  
    return null;  
}  

并指出,该方法最好改为:

if (cheesesInStock.isEmpty()) {  
    return Collections.emptyList();  
}

这是因为,如果返回空对象,调用方势必需要多一个有效性检查(非空判断)
比如在项目中有如下类似代码:

    @Override
    public int[] getIntArray(String key, boolean isFromCurrentPage) {
        if (isFromCurrentPage) {
            if (baseFragment.getOwnerPage() != null) {
                return baseFragment.getOwnerPage().getFragmentBundle().getIntArray(key);
            }
        } else {
            Bundle pageData = FlowManager.getInstance().getCurrentFlow().getFlowInfo().getPageData();
            return pageData.getIntArray(key);
        }
        return null;
    }

那么在使用时我们就需要这样做

        int[] categories = getIntArray(OneboxUtil.SearchKeys.categories.name(), true);
        if (categories == null || categories.length <= 0) {
            return;
        }

万一哪次忘记判空,则可能存在隐藏危险。
引申:个人觉得这条建议不仅仅适用于返回集合或数组,也适用于多数普通对象的返回。如果遇到需要返回null对象的情况,如果认为不应该返回null可以直接抛出异常,这样调用者也不需要进行返回值是否为空有效性检查了。
比如

        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        } else if (drawable instanceof VectorDrawable) {
            return getBitmap((VectorDrawable) drawable);
        } else {
            throw new IllegalArgumentException("unsupported drawable type");
        }

当我们认为某些情况不和常理时,可以直接抛出异常,而不是return null.

  • 3.第四十一条 慎用重载

先看个例子

public class CollectionClassifier {
    public static String classify(Set<?> s) {
        return "Set";
    }
 
    public static String classify(List<?> lst) {
        return "List";
    }
 
    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }
 
    public static void main(String[] args) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };
 
        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

他的输出如下

Unknown Collection
Unknown Collection
Unknown Collection

怎么样,出乎意料?
原因是重载方法的执行判定在编译时,比如ClassA a = new ClassB();执行方法时,重载认为他的类型是ClassA而不是ClassB。是不是感觉和想象的不同?那是因为我们被另外一个概念混淆了,那就是覆盖。覆盖方法的调用取决于运行时,也就是它的实际类型,比如上面的ClassA a = new ClassB();中,如果执行方法,它的类型判定其实是ClassB。
之所以说慎用重载就是因为像上面的重载方法,实际调用时我们无法判断实际调用的方法是哪个。原因是他们的参数个数相同,并且又存在父子类关系,导致最终调用与想象的不一致。EJ的建议是,最保守的策略是:永远不要重载相同参数个数的方法,或者你可以取其他名字,而不是使用重载机制。
总之这条总结的建议的最终结论就是:要么不要重载,重载时一旦出现调用就能确定具体调用的是哪个方法。另:可以参照Java编程思想第四版5.2.1附近的章节,其讲述内容本质类似。

  • 4.第四十四条 为所有导出的API元素编写文档注释。

事实上,我认为这里说的“为所有导出的API元素”编写文档注释,其实也可以用在客户端开发上,当客户端逻辑复杂时,如果没有注释,则可能出现自己看不懂自己写的代码或者无法快速回忆自己写的代码的情况,在熟悉其他人的代码时也会耗费较多的时间。
EJ中指出,通常我们可以对这些内容做出注释:类,接口,构造器,方法和域,其中,方法的注释是重点,它需要注释的地方有方法的前置条件,后置条件,作用,原理,副作用(比如线程是否安全,速度,开销等等),参数,返回值,异常等等。
书中并未涉及为什么要写注释,但是对如何写注释给出了相对详细的说明。此处只是列举了一些我认为比较重要的部分。

  • 5.第五十二条 通过接口引用对象

此处,我觉得该条目可以改为,通过接口或基类引用对象。通常我们声明时会这样声明ObjectA a = new ObjectB()。ObjectA通常应该是ObjectB的接口或基类,这样的好处是灵活。
比如你有这样一个声明
List users = new Vector<>();
后期需要变动实现,只要修改声明的地方即可
List users = new ArrayList<>();
而如果你是这样声明的:
Vector users = new Vector<>();
则无法这样灵活地修改
因为可能存在实际调用的Vector类方法在ArrayList中并不存在。
不可以这样做的情况:
当我们调用方法时,该方法在父类或者接口中不存在,则不适用此规则。

6.其他
书中存在很多很常规的建议,默认大多数人都会遵守,比如变量的范围尽可能小,字符串拼接消耗性能,命名规范等等,但是也有一些我们实际用的比较少但关键时刻会比较管用的知识点。
比如,实际开发时对于泛型的掌握我以前只知道使用List users之类最简单的泛型,但是EJ中提及如果需要定义泛型类时,可能会用到

	    public static <E> Set<E> union(Set<E> s1,Set<E> s2){
	    	return null;
	    }

的类似的“奇怪的声明”又或者使用到<? extends Xxx>的泛型
所以这本书除了能学到一些前辈总结的经验,更大的意义是让我们拓宽了视野,只是沉浸在日常的开发中,有时会误认为开发也就那样,而实际上我们使用的SDK,或者框架可以有很多看上去千奇百怪实际上却是专门设计好的用法。所以,多读书吧,少年!

猜你喜欢

转载自blog.csdn.net/u011109881/article/details/84745065