第二章:创建和销毁对象

本文深入探讨了Java设计的关键原则,包括使用静态工厂方法、构建器模式、Singleton模式的实现方式,以及依赖注入的最佳实践。文章还强调了避免创建不必要的对象、消除过期对象引用、避免使用终结方法的重要性,并推荐了try-with-resources语句来简化资源管理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、用静态工厂方法代替构造器:

  1.1、优势:

          ①、静态工厂有方法名称,而构造器只是和类名一样,很多情况下又要重载,这样不便于阅读。

          ②、静态工厂不必在每次调用的时候都创建新对象,提升性能。

          ③、静态工厂可以返回原返回类型的任何子类型对象,在选择返回对象的类时更灵活。

          ④、静态工厂可以根据参数值来使返回的对象的类随每次调用而发生变化。传入参数不同,得到的对象不同。

          ⑤、静态工厂方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。(本篇难点看例子) 

//四大组成之一:服务接口
public interface LoginService {//这是一个登录服务
    public void login();
}
 
//四大组成之二:服务提供者接口
public interface Provider {//登录服务的提供者。通俗点说就是:通过这个newLoginService()可以获得一个服务。
    public LoginService newLoginService();
}
 
/**
 * 这是一个服务管理器,里面包含了四大组成中的三和四
 * 解释:通过注册将 服务提供者 加入map,然后通过一个静态工厂方法 getService(String name) 返回不同的服务。
 */
public class ServiceManager {
    private static final Map<String, Provider> providers = new HashMap<String, Provider>();//map,保存了注册的服务
 
    private ServiceManager() {
    }
 
    //四大组成之三:提供者注册API  (其实很简单,就是注册一下服务提供者)
    public static void registerProvider(String name, Provider provider) {
        providers.put(name, provider);
    }
 
    //四大组成之四:服务访问API   (客户端只需要传递一个name参数,系统会去匹配服务提供者,然后提供服务)  (静态工厂方法)
    public static LoginService getService(String name) {
        Provider provider = providers.get(name);
        if (provider == null) {
            throw new IllegalArgumentException("No provider registered with name=" + name);
 
        }
        return provider.newLoginService();
    }
}

 

   1.2、缺点

        ①、如果类的构造器是私有的就不能被继承了。建议用复合来实现类的扩展。

        ②、如果命名不规范的话,很难知道它的作用,因为不会像构造器那样在API文档标出来。(一部分命名规则)

1、from:类型转换方法,单个参数,返回一个对应实例。

2、of:聚合方法,多个参数合并起来,返回一个实例。

3、valueOf:比较繁琐的替代方法

4、instance/getInstance:参数决定返回实例

5、create/newInstance:参数决定返回实例,但是每次都保证是一个新的实例

6、getType:参数决定返回实例,在工厂方法处于不同类的时候使用,Type代表工厂方法返回的对象类型。

7、newType:参数决定返回实例,在工厂方法处于不同类的时候使用,保证是一个新的实例。

8、type:getType和newType的简写。

 

2、遇到多个构造器参数时要考虑使用构建器

   2.1、构建器出现原因:

             静态工厂和构造器都不能很好的扩展大量的可选参数,需要根据参数进行多次重载

   2.2、解决方案一:javaBeans模式

             给无参构造器,然后给属性getter/setter。但是会导致类的不确定性,每处setter一次都会使构建出来的类不一样,需要额外付出来管理线程。

   2.3、解决方案二:建造者模式(Builder)

              不直接生成想要的对象,而是让用户利用所有必要的参数调用构造器(或者静态工厂)生成一个builder对象,然后采用流式进行赋值,最后用build方法来生成想要对象。但是为了创建对象,必须先创建它的构造器,需要系统开销。

 

3、用私有构造器或者枚举类型强化Singleton属性

   3.1、实现Singleton的方法一:私有构造器+公有final的静态成员

public class SingletonTest{
    public static final SingletonTest INSTANCE = new SingletonTest();
    private SingletonTest(){....}
    public void leaveTheBuilding(){.........}
}

             优势:API很清楚的表明这个类是Singleton ,并且相对简单(优先考虑)。

             缺点:可以借助AccessibleObject.setAccessible方法通过反射机制调用私有构造器,需要采用如果被要求创建第二个实例的时候抛出异常来确保为Singleton;需要加入readResolve方法来防止每次序列化都创建一个新的实例。

    3.2、实现Singleton的方法二:私有构造器+公有的静态工厂方法

public class SingletonTest{
    private static final SingletonTest INSTANCE = new SingletonTest();
    private SingletonTest(){...}
    public static SingletonTest getInstance(){return INSTANCE;}
    public void leaveTheBuilding(){...}
}

             优势:更加灵活,可以改变该类是否为Singleton类;可以应用于泛型Singleton工厂的编写。

             缺点:可以借助AccessibleObject.setAccessible方法通过反射机制调用私有构造器,需要采用如果被要求创建第二个实例的时候抛出异常来确保为Singleton;需要加入readResolve方法来防止每次序列化都创建一个新的实例。

     3.3、声明一个包含单个元素的枚举类型

public enum SingletonTest{
    INSTNACE;
    public void leaveTheBuilding(){......}
}

             优势:更加简洁,并可以避免序列化问题以及反射问题(最佳方法)。

             缺点:当Singleton必须继承一个父类而不是Enum的时候,不适合使用这个方法。

 

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

      4.1、原因和解决方法:

             由于只有当类不包含显式的构造器时,编译器才会生成缺省的构造器,因此只要让这个类包含一个私有构造器,它就不能被实例化。

      4.2、缺陷:

             ①、这种用法会让人觉得构造器就是专门设计成不能被调用一样,需要加入注释。

             ②、它会使一个类不能被子类化,因为所有的子类都需要显式或者隐式的调用父类的构造器,然而它是私有的。

 

5、优先考虑依赖注入来引入资源 

      5.1、将资源依赖注入(以一个拼写检查器类举例,拼写检查器需要依赖词典)

           5.1.1、方法一:将词典作为静态工具类(缺点:只能依赖一个词典,并且该词典为常量值)

/*拼写检查器类 将词典作为静态工具类*/
public class SpellChecker {
	//需要依赖的词典 静态工具类
    private static final Lexicon dictionary = ...;

    private SpellChecker() {
    }
}

          5.1.2、方法二:将词典编写为Singleton类(缺点:只能依赖一个词典,并且该词典为常量值)

/*拼写检查器类 Singleton*/
public class SpellChecker {
	//需要依赖的词典
    private final Lexicon dictionary = ...;
    public static INSTANCE = new SpellChecker(...);

    private SpellChecker(...) {
    }
    
}

         5.1.3、方法三(推荐做法):词典是拼写检查器的一个依赖(dependency),在创建拼写检查器时就将词典注入(injected)其中。这就是依赖注入的一种形式(特点:当创建一个新的实例时,就将该资源传到构造方法中。结合框架去除凌乱的缺点后,提高类的灵活性、可重用性和可测试性)

/*拼写检查器类 依赖注入*/
public class SpellChecker {
	//需要依赖的词典
    private final Lexicon dictionary;

	//在构造方法中注入
    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }
}

      5.2 、将资源工厂依赖注入

             在Java8中增加的接口Supplier<T>,最适合用于表示工厂带有Supplier<T>的方法,应该限制输出工厂的类型参数使用有限制的通配符类型,以便客户端能够传入一个工厂,来创建指定类型的任意子类型。(没用过,比较生疏)

	//生产马赛克方法
	Mosaic create(Supplier<? extends Tile> tileFactory){
		...
	}

 

6、避免创建不必要的对象

     6.1、需要注意的几点:

           6.1.1、最好能重用单个对象,而不是每一次都去new一个新的,这样又快又节省性能。

           6.1.2、对同时提供了静态方法和构造器的不可变类,优先使用静态方法。

           6.1.3、正则表达式应该显式的将正则表达式编译成一个Pattern实例,让他成为类初始化的一部分,并缓存起来,每次调用判断的时候,就是有这个实例。因为这个编译过程成本非常高。

           6.1.4、不要创建多个适配器实例,因为适配器实例本来就是把功能委托给后备对象,它只是提供一个可替代接口而已。

           6.1.5、要优先使用基本类型而不是装箱类型,要当心无意识的自动装箱。(装箱后每一个都成为了一个对象)

private static long sum(){
    Long sum = 0L;
    for(long i = 0; i<=Integer.MAX_VALUE; i++){
        sum += i;
    }
    return sum;
}

注:变量sum被声明为Long后比声明为long多出五六秒的运行时间。

     6.1.6、除非对象池中是非常重量级的对象,尽量不要通过维护自己的对象池来避免创建对象。因为维护对象池会把代码弄的凌乱,同时增加内存占用,还损害性能。

 

7、消除过期的对象引用

         注意:虽然java有JVM垃圾回收机制,但是还是需要关注内存管理的事情

public 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 e) {  
         ensureCapacity();  
         elements[size++] = e;  
     }  
  
     public Object pop() {  
         if (size == 0)  
             throw new EmptyStackException();  
         return elements[--size];  
     }  
  
     /** 
     * Ensure space for at least one more element, roughly 
     * doubling the capacity each time the array needs to grow. 
     */  

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

          当我们调用pop方法是,该方法将返回当前栈顶的elements,同时将该栈的活动区间(size)减一,然而此时被弹出的Object仍然保持至少两处引用,一个是返回的对象,另一个则是该返回对象在elements数组中原有栈顶位置的引用。这样即便外部对象在使用之后不再引用该Object,那么它仍然不会被垃圾收集器释放,久而久之导致了更多类似对象的内存泄露。

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

       7.1、 需要手动处理内存的情况:

                  7.1.1、类是自己管理内存,如例子中的Stack类

                  7.1.2、使用对象缓存机制时,需要考虑从缓存中换出的对象,或者是长期不被访问到的对象

                  7.1.3、时间监听器和相关回调。用户会在需要时显式的注册,然而却忘记再不用的时候注销这些回调接口实现类。

 

8、避免使用终结方法和消除方法 

      8.1、表述意思:

              尽量不要在类中覆盖finalize方法,在里面写一些释放类中资源的语句。

      8.2、原因:  

              8.2.1、finalize方法不能保证它能被及时执行。

              8.2.2、finalize方法甚至不会执行。

              8.2.3、System.gc和System.runFinalization这两个方法只是能增加finalize方法被调用的几率。

              8.2.4、唯一能保证finalize方法被执行的方法有两个,System.runFinalizersOnExit和Runtime.runFinalizersOnExit但是这两个方法已经被弃用。

              8.2.5、覆盖并使用终结方法会造成严重的性能损失。

      8.3、释放资源正确做法:

               只需要提供一个Public修饰的终止方法,用来释放资源

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }

    public static void main(String... args) {
    MyObject object = new MyObject();
    try{
        //在这里面使用object;
        ...
    }  finally {
        //在这里面关闭object;
        object.close();
        }
    }
}

      8.4、使用终结/清除方法的情形:

               8.4.1、用终结/清除方法充当安全网,当我们提供的public修饰的终结方法被在外部忘记调用的时候提供一种安全保障。

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }

    //安全网
    @Overried
    protected void finalize() throws Throwable {
        try{
            close();
        }  finally  {
            super.finalize();
        }
    }
}

             8.4.2、回收不了被本地代码(C或者C++)托管的对象的时候

 

9、try-with-resources优先于try-finally

       9.1、原因:

             9.1.1、当有多个资源嵌套的时候,try-finally语句会一团糟。

             9.1.2、当底层出现异常时,close中的异常会覆盖掉try中的异常,导致没有打印出有价值的错误堆栈信息。

       9.2、采用try-with-resources解决方案:

             9.2.1、try-with-resources方法想要关闭资源的类必须实现AutoCloseable接口,包含无返回值的close方法。将要关闭的资源放在了try后面的括号里面,自己就会在用完的时候释放资源,而且如果发生异常也是会打印出第一个异常,close里面的异常就会被禁止,我们就可以找到对我们有用的异常。

    public static void copy(String src, String dst) throws IOException {

        try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dst);) {
            byte[] buf = new byte[1024];
            int n;
            while ((n = in.read(buf)) >= 0) {
                out.write(buf, 0, n);
            }
        }
    }

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值