effective java 学习笔记 1.创建和销毁对象

ps:因为是我自己的学习笔记,所以有很多可能只在我自己身上适用,请勿对号入座。
1.考虑用静态工厂方法代替构造器
通常再创建一个新的对象的时候我会new 出来一个对象,我曾经用过spring struts2 都是直接调用接口,而现在用的jfinal框架是提供了一个静态的dao方法供人调用,算是个简单的静态工厂方法,开始只是会用而已,并不能真正理解理解里面的意思。书中列举了四条关于静态工厂方法的好处。
-有名称
-不必每次都创建一个新对象
-屏蔽子类区别每次都使用父类调用
-多参数创建实例,使代码整洁易读

关于名称:
基于我现在的编程经验,再选择构造器的时候,一般都是看参数数量,参数类型,来判断创建新对象
例如:
people(int age int high)
people(int age)
可以判断出一个需要的年龄和身高,一个需要年龄就行,但是并不能晓得具体是用来做什么的,只能通过文档和猜测来判断构造器的用法,而且两个参数的参数类型都是int,不知道会不会发生把高当作age传入作为参数的事情发生(我这种性格。。。),而如果改成
public people getHigh(int age int high){
return new people(age,high);

就可以很直观的看出所需要的参数和构造方法的用处。

关于不必每次都创建一个新对象:
这个就是用在单例模式上的了,对于单例模式来说,一个项目只允许一个对象的实例存在,如果用构造器就会创建多个对象,所以单例模式都会声明一个实例为静态类,只能调用静态返回此实例。

关于:屏蔽子类差异
现在我对子类继承方面没怎么用过,书中引用的JDBC的例子来说明,JDBC使用的是服务提供者框架,JDBC在连接各个种类的数据库时都提供的同样的方法接受数据库的驱动,然后根据驱动程序的不同实例化Connection 但是返回给用户的conn实例都是conn的父类型,方便用户的引用,只需要把驱动放在配置文件中,每次更改数据库不需要更改代码就可以方便的更改数据库,屏蔽了实现的差异。

关于:多参数创建实例,使代码整洁易读
这个多参数指的是泛型多类型,当使用collection接口的对象时都会要提供泛型,而泛型不同却需要从新输入一遍。
书中提供了Map> m = new HashMap>() 和 Map> m = HashMap.newInstance()
截止于博客当前时间 jdk1.8 还未实现 ,不过可以自己封装

书中还列举了两个缺点
-类如果不含共有的或者受保护的构造器,就不能被子类化
-在API显示上与静态方法没有区别

第一条恕本人水平不够并不能很好的参透,里面提到了复合(compsition) 在后续学习中再补充
第二条就是sun公司的问题,但是我们一般都会命名位util工具类的形式,希望以后的java升级中可以加上静态工厂方法的标志。

勉励!

2.遇到多个构造器参数时要考虑用构建器
多个构造器参数构建对象目前对我来说是普遍存在,在用entity类对应表关系的时候参数之多很容易让人抓狂,而在对表进行增改的时候通常都会实例化一个entity类,不管是mybitis还是jfinal都对entity类的增改有很好的支持,所以在进行增改操作的时候,创建entity对象,并对里面的参数赋值就是是头疼的事情
-参数多
-复用率低
-参数用处不明确
-修改不方便

通常最简单的方法就是使用构造函数对每个属性的进行定制,比如
class student{
private String id;
private int age;
private int high;
private String name;
public Student(){}
public Student(String id){}
public Student(String id,int age){}
public Student(String id,int age,int high){}
public Student(String id,int age,int high,String name){}
//可能还会有什么 id 名字 年龄的组合之类的就不一一列举了
}
所以每次调用都是  
Student s = new Student(111,12)//id age
Student s = new Student(112,13,178)//id age high
从代码上看完全不知道参数是做什么用的  而且因为age 和high 都是int类型的,所以搞混淆也是正常的事情。
如果表结构发生变化,修改的代码量也是大的不行,好不如重新生成一次实体类。
这就有了第二种方法的出现,javaBean的方式
先调用无参的构造方法,实例化entity然后用setting方法给entity里面的参数赋值,现在在试用spring 和 jfinal的时候都是用的这种方式
public void setId(String id){this.id = id;}
public String getId(){return this.id;}
public void setAge(int age){this.age=age;}
public int getAge(){return this.age}

这样的话每次实例话entity就是
Student s = new Student();
s.setId(1);s.setAge(12);
这样可以很清楚的看到参数的名字和类型,因为一直使用的是这样方式,我并没有觉得哪里不好的,但是在effective java中作者说这种方式javabean可能会处于不一致的状态,无法检测参数的有效性来保证一致性,并不能把类做成不可变的可能,现在我并不知道在什么时候把类做成不可变的类,在后续学习中会补充这些知识,也许他想说的是线程安全的类?

所以第三个方式就时javabean的增强,builder模式。
在entity类的内部增加一个成员类,成员类做成类似于javabean的方式,再用这个成员类的实例对象作为参数来构造entity对象,
假设在Student中有个Builder的成员类
public static class Builder{
private String id;
private int age;
private int high;
private String name;
public Builder(){}
public Builder id(String id){this.id = id; return this}
public Builder age(int age){this.age = age; return this}
public Builder high(int high){this.high = high; return this}
public Builder name(String name){this.name = name; retuen this}
public Student build(){
return new Student(this);
}
}
private Student(Builder b){
id = b.id;
age = b.age;
high = b.high;
name = b.name;
}
在调用的时候就是 
Student s = new Student.Builder().id(2).age(23).high(178).name("张三").build();
参数很直观,而且可以重复调用修改的话只需要添加方法即可,但是可能因为自身水平不够,确实get不到build模式的G点,我印象深刻的有两个

-反复调用build()方法构造有细微差别的对象
-可以在方法里面写更多验证,和进行计算。

前者可以复用一些同样的参数
后者省却的一些+1 -1的操作

注:书中提到了抽象工厂模式这个在之后的学习中再get。

3.用私有构造器或者枚举类型强化singleton属性
单例模式:保证在项目中只有一个实例存在,不管是多少用户,调用的都是同一个对象,应用于一些只能存在一个的实例。

这里博博学的是两种单例:懒汉模式/饿汉模式
懒汉:
Class Student{
private Student (){
}
private static final Student s = new Student();
public static Student getSingleton(){
return s;
}
}
饿汉:
Class Student{
private Student(){}
private static Student s;
public static Student getSingleton(){
if(s==null){
s = new Student();
}
return s;
}
}
因为构造器是私有的所有只能在方法内调用,保证了对象只有一个;
但是在书中作者说借助AccessibleObject.setAccessible 通过反射机制调用私有方法可以破解封装,所以应该在第二次调用私有方法的时候抛出异常
static int count = 0;
private Student(){
if(count <1){
count++
}else{
throw new RuntimeException();
}
}

书中提到,再对类进行序列话的时候,如果仅仅是实现Serializable接口是不够的,因为每次反序列化一个序列化的实例时,都会创建一个新的实例。为了维护和保证必须声明所有的实例域都是瞬时(transient)的,并提供一个readResolve方法。
这个在第十一章会有具体的说明,我们到时候再来回头看看。

第三种方法:
枚举类:
public enum Srudent{
INSTANCE;
public void leaveTheBuilding(){}
}
枚举再功能上与公有域方法相近,但是它更简洁,无偿的提供了序列化机制,绝对的防止多次实例化,枚举类型是绝对唯一的,即使是在面对复杂的序列化或者反射攻击的时候。
在第六章会有枚举类型的详细介绍,我们再回头看。
4.通过私有构造器强化不可实例化的能力
首先来说说不可实例化的类,一般来说我们会把一些工具类设置成static类型,方便人们的调用,例如Arrays,各种utils,Math,我们不需要对类进行实例化,对于这种类来说构造方法是多余的。
但是我们即时不提供构造方法,在构建类的时候也会提供一个默认的,无参的构造方法。
Class Student{
String name;
int age;
}
就有一个 
public Student(){}
默认的构造方法来进行实例化,但是对于工具类来说我们不需要实例化。
这个默认的构造方法是public 的是可以被创建的,而为了不能被别的类创建,我们可以定义一个无参的构造方法,然后把他的修饰词定义为private 
private Student(){}
这样其他的类就不能引用了;

另外,这样做的话会导致子类也不能调用父类的构造方法,所以一般工具类都是没有子类的。
5.避免创建不必要的对象
在我看来,java中内存资源是有限的,而对象的创建是无限的,虽然java中有自动的垃圾回收机制帮你清理内存但是java虚拟机的运行速度会变慢,所以再编程过程中我们都尽量让对象的创建数量变得合理,其中避免创建不必要的对象就很重要。
在书中第一个例子就是个很荒唐的例子。
String s = new String("test");
String s = "test";
估计很少人知道String也提供了构造方法来创建对象,我们平常都是用""来定义String对象,所以第一个方法其实是定义了两个String对象
一个是"test",一个是s
里面的值都是test,这样的方法就是个垃圾对象制造机。
又比如,Boolean.valueOf(String) 和Boolean(String)
静态方法的调用并没有创建对象,这个时候静态方法要比调用构造方法节省资源的多。

对于"test"来说只要被创建了,之后不管是调用多少次都会是同一个对象,这就是典型的不可变对象,而对于一些可变对象来说也有一些是创建之后不会改变的,书中提到了这个例子。
public class Person{
private final Date birthDate;
//判断birthDate是否是婴儿潮时出生的1946-1964
public boolean isBabyBommer(){
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946)
Date boomStart = gmtCal.getTime();
gmtCal.set(1964)
Date boomEnt = gmtCal.getTime();
return birthDate.compareTo(boomStart)>=0&&
  birthDate.compareTo(boomEnd)<0;
}
}
可以看的出来每次调用isBabyBoommer都会新建一个Calendar、一个TimeZone和两个Date实例,但其实Date都是不会变的,只有birthDate会发生数据的变化,包括Canlendar和TimeZone都是不需要每次都创建的,所以我们可以把上述的代码都写到静态代码块里,
static{
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946)
Date boomStart = gmtCal.getTime();
gmtCal.set(1964)
Date boomEnt = gmtCal.getTime();
}
这样每次只需要创建一次新对象,就可以了。(后文中提到的不会被引用的问题,我觉得如果不会用到,就没必要写进代码里了。)
然后是接口的适配器问题,对于Map的keySet方法,其实keySet是在Map接口中定义的,无论实现类怎么覆盖的方法其实都是返回的同一个父接口的set对象,这样其实不需要每次调用都新建一个set对象了。

最后一个,一直以来我都觉得使用封装类型会更好,因为封装类型能够提供更多的方法应用,但是其实每次加减都会创建一个新的封装类型对象,而基本类型并不会,所以视情况而定,能用基本类型就用基本类型吧。

这个问题的最终其实时资源分配的问题,一些对象小,代价少,用量少还是每次创建新的对象比较好,而一些重量级的对象,用的多的,最好还是能复用 就复用。
仁者见仁,智者见智。
保护性拷贝(defensive copying):当你应该重用现有对象的时候,请不要创建新的对象。当你应该创建新的对象的时候,请不要重用现有的对象。

6.消除过期的对象引用
java相对于c/c++语言来说有一个优势就是她的垃圾回收机制,当你用完对象之后,它们会被自动回收。所以我们不用考虑内存管理的事情了~才怪~
这里书里举了个栈结构的例子:
//内存泄漏
public class Stack{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Statck(){
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e){
ensureCapacity();
elements[size++] = e;
}

public Object pop(){
if(size== 0 ){
throw new EmptyStackException();
return elements[--size];
}
}

private void ensureCapacity(){
if(elements.length==size){
elements = Arrays.copyOf(elements,2*size+1);
}
}
}

这是个常见的栈结构,其中是没有什么错误的。但是因为一个栈先是增长,再收缩,那么,从栈中弹出的对象将不会被当作垃圾回收,即时不再引用,这时会引起内存泄露(OutOfMemoryError)。
这是因为栈内部维护着对这些对象的过期引用(obsolet reference),永远都不会解除引用。
所以,我们应该做一些事情让垃圾回收机制晓得,这个对象已经没用了。
public Object pop(){
if(size== 0 ){
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;//解除引用
return result;
}
}

之所以回发生这个问题,是因为栈内的内存一旦分配成功,就会形成一个数组储存池(storage pool),其中size范围内的对象在数组活动区域内是已分配的(allocated),而已经清出数组的对象是自由的,但是垃圾回收机制时不知道的。
所以程序员在遇到这种问题的时候应该及时告诉垃圾回收机制,这些对象没用了,赶紧清楚了去。

一般而言,只要是类自己管理内存,程序员就应该警惕内存泄漏问题。

另一个内存泄露的常见来源是缓存,现在我们有了各种电脑安全软件,可以每天清除缓存,以清理电脑垃圾,所以这种问题现在来说是可以解决的。
书中提到用WeakHashMap来代替缓存,当缓存中的项过期之后,它们就自动删除了。

再一个是监听器和其他的回调方法,现在来说,登陆一般都是放在session作用域中,是有时间限制的,我们只需要限制时间就好了,并不需要WeakHashMap类来直接实现。

你可能感兴趣的:(学习笔记)