Effective Java(一)

Effective Java(一)

创建和销毁对象

1.考虑使用静态工厂方法代替构造器

静态工厂方法相对于构造器的三大优势:

  1. 静态方法有名称
    构造器的参数很难准确描述正在被返回的对象,并且一个类只能有一个带有制定签名的构造器,如果要避开这一限制就只能将参数列表的类型顺序进行调换,这样会使得对象构造时产生困惑或者错误。
    所以,在一个类需要有多个签名相同的构造器时,就用静态工厂方法代替构造器

  2. 不必在每次调用它们的时候都创建一个新对象
    如果程序经常创建相同的对象,并且创建对象的代价很高,该技术可以极大提高性能
    静态工厂方法可以为重复的调用返回相同的对象。可以帮助实例受控的类严格控制某个时刻哪些实例应该存在。实例受控_使得类确保它是_Singleton_的(仅仅被实例化一次的类,常用来代表那些在本质上唯一的系统组件)或者是_不可实例化_的(只包含静态方法和静态域的类)。还可以使_不可变的类(实例不能被修改,更易于设计,实现和使用)确保不会产生两个相等的实例,确保了这一点,就可以用“==”来代替”equals()“方法,提升性能。

  3. 可以返回原返回类型的任何子类型的对象
    第一种应用:API可以返回对象,同时又不会使对象的类变成公有的。
    接口为静态工厂方法提供了自然的返回类型。因为接口不能有静态方法,所以一般名为Type的接口的静态工厂方法被放在一个名为Types的不可实例化的类中。
    一个接口可以有很多不同的便利实现,这些实现都通过静态工厂方法被放在一个不可实例化类中导出。这样就实现了一种概念意义上的减少,用户可以不用知道返回的是哪一种实例,只用知道自己使用的是哪一种接口即可。
    第二种应用:静态工厂方法返回对象所属的类,在编写该静态工厂方法时可以不用存在。这样就可以构成_服务提供者框架_:

    • 服务接口:提供者实现的,用于实现接口功能。
    • 提供者注册API:系统用来注册实现的,用于用户使用特定的服务。
    • 服务访问API:客户端用来获取服务的实例。
    • 服务提供者接口(可选):提供者负责创建其服务实现的实例。
//服务提供者框架
//
//服务接口,规定服务
public interface Service{
	//规定服务方法
}

//服务提供者接口
public interface Provider{
	Service newService();
	//创建其服务实现的实例
}

//提供服务注册和访问的不可实例化类
public clase Services{
	private Services(){}

	//把服务的名字映射到Services
	private static final Map<String,Provider> providers=
		new ConcurrentHashMap<String,Provider>();
	public static final String DEFAULT_PROVIDER_NAME="<def>"//默认提供者

	//提供者注册API
	public static void registerDefaultProvider(Provider p){
		registerProvider(DEFAULT_PROVIDER_NAME);
	}
	public static void registerProvider(String Name,Provider p){
		providers.put(name,p);
	}

	//服务访问API
	public static Service newInstance(){
		return newInstance(DEFAULT_PROVIDER_NAME);
	}
	public static Service newInstance(String name){
		Provider p=providers.get(name);
		if(p==null){
			throw new IllegalArgumentException("no provider registered with name:"+name);
		}
		return p.newInstance();
	}
}
  1. 在创建参数化类型实例的时候,使代码变得更加简单
    当调用参数化类型构造器的时候,即使参数类型很明显,也必须指明,而且需要提供两次类型参数:

    Map<String,List> m=new HashMap<String,List>();

当使用静态工厂方法的时候,这也被叫做_类型推导_:

public static <K,V> HashMap<K,V>newInstance(){
    return new Hash<K,V>();
}

Map<String,Object> m=HashMap.newInstance();

静态工厂方法的主要缺点:

  1. 类如果不含有公有的或者受保护的构造器,就不能被子类化。但也正因为这样,鼓励程序用_复合_而不是_继承_
  2. 与其他静态方法没有区别,不能在API文档中明确的标示出来。

2.遇到多个构造器的时候考虑构建器Builder

静态工厂方法和构造器都有共同的局限性:都不能很好的扩展到大量的可选参数。
可以的解决办法:

  • 重叠构造器:构造多个构造器,其参数数目递增。缺点:参数多时,客户端代码难以编写,且使用容易出错。
  • JavaBeans模式,调用一个无参构造函数构造对象,使用setter方法设置参数。缺点:构造过程被分到了几个调用中,JavaBean可能处于不一致的状态中,调试起来困难。且阻止了把类做成_不可变_的可能,需要额外努力来保证其线程安全。
  • Builder模式:不直接生成对象,而是让客户端利用所有必要的参数调用构造器/静态工厂,得到一个Builder对象。然后客户端在Builder对象上调用setter方法,设置参数。最后调用builde()方法来生成不可变的对象。

如果类的构造器或者是静态工厂方法中具有多个参数,设计这种类时,Builder模式是一种不错的选择,特别是当大多数参数都是可选的时候,比重叠构造器更易于阅读和编写,比Javabeans更加安全。

3.强化Singleton属性

Singleton为仅仅被实例化一次的类,常用来代表那些在本质上唯一的系统组件。

三种方法:
  1. 公有静态成员为final域:
public class A{
	public static final A INSTANCE = new A();
	private a(){}
	public void doSomethinf(){}
}
  1. 公有成员为静态工厂方法
public class A{
	private static final A INSTANCE=new A();
	private A(){}
	public static A getInstance(){
		return INSTANCE;
	}
	public void doSomething(){}
}

工厂方法的优势在于:它提供了灵活性:在不改变API的前提下,可以改变该类是否应该为Singleton的想法。

  1. 单元素枚举类型(最佳
public enum A{
	INSTANCE;

	public void doSomething(){}
}

我们知道,创建一个对象的方式有:new、克隆、序列化、反射。

由于单例模式提供的是一个私有的构造函数,所以不能外部使用new的方式创建对象。

  • 虽然clone()是Object的方法,也就是说每个对象都拥有一个克隆方法,但是某一个对象直接调用clone方法,会抛出异常,即并不能成功克隆一个对象。调用该方法时,必须实现一个Cloneable 接口。这也就是原型模式的实现方式。还有即如果该类实现了cloneable接口,尽管构造函数是私有的,他也可以创建一个对象。即clone方法是不会调用构造函数的,他是直接从内存中copy内存区域的。所以克隆方式也不需要担心。
  • 一个对象序列化成一个字节流后,若要被反序列化恢复时,会生成一个新的对象,此对象和原来的对象具有一模一样的状态,但归根结底是两个对象。
  • java中提供了反射机制,有句老话“反射可以打破一切封装!”,说明了任何类在反射机制面前都是透明的,通过反射机制可以获得类的各种属性,当然也可以获得类的构造器(就算是私有也没用),从而构造一个新的对象。(但是枚举类除外,下文会提到)。
    通过上述分析,若要实现一个完美的单例模式必须考虑序列化和反射问题。
序列化

把对象转换为字节序列的过程称为对象的序列化
把字节序列恢复为对象的过程称为对象的反序列化
 对象的序列化主要有两种用途:
  1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
  2) 在网络上传送对象的字节序列
在反序列化时,会重新创建一个对象,该对象和原对象内容完全一致,但是终究还是一个新的对象,所以就破坏了Singleton。
为了使第一二种方法实现的Singleton类变成可序列化的,仅仅在声明中加入“implements Serializable”是不够的。为了维护并保证Singleton,必须声明所有实例域都是transient的(不参与序列化),并提供一个readResolve方法。见Effective Java(十一)

反射
  1. 万事万物皆对象,(当然,基本数据类型,静态成员不是面向对象(属于类的)),所以我们创建的每一个类也都是对象,即类本身是java.lang.Class类的实例对象,但是这些对象都不需要new出来,因为java.lang.Class类的构造方法是私有的

  2. 任何一个类都是Class类的实例对象,这个实例对象有三种表示方式:(我们新建一个Student类)
    ① Class c1 = Student.class;//实际告诉我们任何一个类都有一个隐含的静态成员变量class(知道类名时用)
    ② Class c2 = stu.getClass();//已知该类的对象通过getClass方法(知道对象时用)
    ③Class c3 = Class.forName(“类的全名”);//会有一个ClassNotFoundException异常
    官网解释说:c1,c2表示了Student类的类类型()class type),万事万物皆对象,类也是对象,是Class类的实例对象,这个对象我们成为该类的类类型(有点乱,但是慢慢捋一下还是能理解的)
    这里有一点值得注意,当我们执行System.out.println(c1==c2);语句,结果返回的是true,这是为什么呢?原因是不管c1还是c2都代表了Student类的类类型,一个类可能是Class类的一个实例对象。
    我们完全可以通过类的类类型创建该类的对象实例,即通过c1或c2创建Student的实例。

Student stu = (Student)c1.newInstance();//前提是必须要有无参的构造方法,因为该语句会去调用其无参构造方法。该语句会抛出异常。
  1. Reflection(反射)是Java被视为动态语言的关键,反射机制允许程序在执行期借助于Reflection API取得任何类的內部信息,并能直接操作任意对象的内部属性及方法。
    Java反射机制主要提供了以下功能:
  • 在运行时构造任意一个类的对象
  • 在运行时获取任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法(属性)
  • 生成动态代理
    Class 是一个类; 一个描述类的类.
    封装了描述方法的 Method,
    描述字段的 Filed,
    描述构造器的 Constructor 等属性.
    也就是说,使用反射,即使构造函数,方法,字段是私有的,也能通过反射构造出新的对象。

单元素的枚举类型是实现Singleton的最佳方法。更简洁而且绝对保证防止多次实例化。

4.通过私有构造器强化不可实例化的能力

一些工具类,例如java.lang.Math,java.util.Arrays等只包含静态方法和静态域,它们不希望被实例化,因为实例化对它们没有意义。
然而,在缺省构造器的情况下,系统会默认为其提供一个公有的,无参的缺省构造器。这会导致有些类被无意识的实例化。
但是也不能把类做成抽象类来强制使其不能实例化。所以,我们可以为这样的类提供一个私有的构造函数,所以不能在类的外部调用它,这样就避免了类的无意识实例化。
缺点:因为构造器是私有的,所以类将不能被子类化,因为子类在构造的时候必须显示或者隐式地调用父类的构造函数。

5.避免创建不必要的对象

  1. 重用不可变的对象
  2. 重用已知不会修改的可变对象,放在static{}里面初始化。
  3. 优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。
  4. 小对象的创建优先提升程序的清晰性,间接性和功能性。JVM对于小对象的回收代价是相当廉价的
  5. 提倡使用保护性拷贝的时候,优先创建新的对象,安全性更加重要。

6.消除过期的引用

内存泄露的三种情况:

  1. 类自己管理内存
    比如自己用数组实现栈,数组活动区域中的元素是已分配的,而数组的其余部分则是自由的,但是垃圾回收器不知道这一点;对于垃圾回收器而言,数组中的所有引用都是同等有效的,只有程序员知道数组的非活动部分是不重要的,因此,这时候就应该由程序员把这个情况告诉垃圾回收器:手工清空这些数组元素。

  2. 缓存
    一旦把数据放入缓存中,很容易就会将它遗忘。可以使用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会被自动删除:只要缓存中的项在缓存之外有引用,该项就有意义。
    WeakHashMap 继承于AbstractMap,实现了Map接口。
    HashMap一样,WeakHashMap 也是一个散列表,它存储的内容也是键值对(key-value)映射,而且键和值都可以是null
    不过WeakHashMap的键是“弱键”。在 WeakHashMap 中,当某个键不再正常使用时,会被从WeakHashMap中被自动移除。更精确地说,对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。某个键被终止时,它对应的键值对也就从映射中有效地移除了。
    这个“弱键”的原理呢?大致上就是,通过WeakReference和ReferenceQueue实现的。 WeakHashMap的key是“弱键”,即是WeakReference类型的;ReferenceQueue是一个队列,它会保存被GC回收的“弱键”。实现步骤是:
    (01) 新建WeakHashMap,将“键值对”添加到WeakHashMap中。
    实际上,WeakHashMap是通过数组table保存Entry(键值对);每一个Entry实际上是一个单向链表,即Entry是键值对链表。
    (02) 当某“弱键”不再被其它对象引用,并被GC回收时。在GC回收该“弱键”时,这个“弱键”也同时会被添加到ReferenceQueue(queue)队列中。
    (03) 当下一次我们需要操作WeakHashMap时,会先同步table和queue。table中保存了全部的键值对,而queue中保存被GC回收的键值对;同步它们,就是删除table中被GC回收的键值对
    这就是“弱键”如何被自动从WeakHashMap中删除的步骤了。

    和HashMap一样,WeakHashMap是不同步的。可以使用 Collections.synchronizedMap 方法来构造同步的 WeakHashMap。

  3. 监听和回调
    当你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非采取某些动作,它们就会积聚。
    解决方法:只保存它们的弱引用,例如,只将它们保存成WeakHashMap中的键。

7.避免使用终结方法

终结方法finalize()通常是不可预测的,也是很危险的,一般情况下是不必要的。

  1. Java语言规范并不保证finalize()会被及时执行,即不确定终结方法执行时间,只规定在对象被垃圾回收之前执行
  2. 不应该依赖终结方法来改变重要的持久状态
  3. 终结方法会造成严重的性能损耗

正确使用终结方法

  1. 防止用户使用了创建了对象后,并未使对象提供的显式终止方法(如果有)。终结方法可以充当安全网。

  2. 本地对等体(是一个本地对象,普通对象通过本地方法委托给一个本地对象。)不是一个普通对象,垃圾回收器不会回收它。

一般情况下,终结方法需要能够完成所有必要的工作释放资源,如果需要即时释放资源,那么就需要给本地对等体指定一个显式的终止方法:如InputStream,OutputStream上的close方法。

终结方法链不会自动执行,如果类有终结方法,并且子类覆盖了终结方法,则子类的终结方法必须手工调用超类的终结方法。你应该在try块中终结子类,并在相应的finally块中调用超类的终结方法。

终结方法守卫者

如果子类实现者覆盖了超类的终结方法,但是忘了调用超类的终结方法,那么超类的终结方法永远不会调用。为了防止此种情况出现,可以使用终结方法守卫者。即为每个被终结的对象创建一个附加的对象,该附加对象为一个匿名类,将外围类的终结操作如释放资源放入该匿名类的终结方法中。同时,外围类保存着对该匿名类的唯一引用,即复制给私有变量域。

class A {

    @SuppressWarnings("unused")
    //终结守卫者
    private final Object finalizerGuardian = new Object() {

        @Override
        //终结守卫者的终结方法将被执行
        protected void finalize() {
            System.out.println("A finalize by the finalizerGuardian");
        }
    };


    @Override
    //由于终结方法被子类覆盖,该终结方法并不会被执行
    protected void finalize() {
        System.out.println("A finalize by the finalize method");
    }


    public static void main(String[] args) throws Exception {
        B b = new B();
        b = null;
        System.gc();
        Thread.sleep(500);
    }
}

class B extends A {

    @Override
    public void finalize() {
        System.out.println("B finalize by the finalize method");
    }

}

输出:B finalize by the finalize method
A finalize by the finalizerGuardian

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值