• 和我一起学Effective Java之创建和销毁对象


    前言

    主要学习创建和销毁对象:

    • 1.何时以及如何创建对象
    • 2.何时以及如何避免创建对象
    • 3.如何确保它们能够适时地销毁
    • 4.如何管理对象销毁之前必须进行的清理动作

    正文

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

    获取类的实例的常用方法有:

    • 1.公有的构造器
    • 2.公有的静态工厂方法

    下面通过Boolean类(基本类型boolean的包装类)的简单示例来学习:

    //公有的构造器
      public Boolean(String s) {
            this(parseBoolean(s));
        }
    //公有的静态工厂方法
    public static Boolean valueOf(boolean b) {
            return (b ? TRUE : FALSE);
        }
    

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

    1.有名称

    具有适当名称的静态工厂方法更易使用,产生的代码更易阅读。可用名称来突出它们之间的区别。

    如构造器BigInteger(int,int,Random)返回的BigInteger可能为素数,若用名为BigInteger.probablePrime的静态工厂方法来表示,显得更加清楚。

    2.不必每次调用它们的时候都创建一个新对象

    不可变类可以使用预先构建好的实例,或者是将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。

    例如之前的Boolean的静态工厂方法就说明了这项技术,这种方法类似于FlyWeight模式。如果程序经常请求创建相同的对象,并且创建对象的代价很高,这项技术极大地提升了性能。

    //缓存起来的Boolean实例
     public static final Boolean TRUE = new Boolean(true);
     public static final Boolean FALSE = new Boolean(false);
    
     public static Boolean valueOf(boolean b) {
            return (b ? TRUE : FALSE);
        }
    

    静态工厂方法为重复的调用返回相同的对象。

    3.可返回原返回类型的任何子类型的对象

    4.创建参数化类型实例时,可使代码变得更加简洁

    由于《Effective Java》这本书在编写的时候,JDK1.7还没有推出,因此在调用参数化类的构造器时,类型参数都必须要指明。而静态工厂方法能替我们实现类型推导的功能。

      //JDK7之前
            Map<String,List<String>> map = new HashMap<String,List<String>>();
      //JDK7
            Map<String,List<String>> m = new HashMap<>();
       //使用静态工厂方法
       public class MyHashMap extends HashMap {
        public static <K,V> HashMap<K,V> newInstance(){
            return new HashMap<K,V>();
        }
       
       //静态工厂方法实现类型推导 
       Map<String,List<String>> m = MyHashMap.newInstance();
    }
    
    

    静态工厂方法的缺点:

    • 1.类如果不含公有的或受保护的构造器,就不能被子类化。
    • 2.静态工厂方法和其他的静态方法实际上没有任何区别。
    /*
     *静态工厂方法的惯用名称 
     */
    //valueOf:返回的实例与它的参数具有相同的值
    //String.valueOf(int)方法
     public static String valueOf(int value) {
            return Integer.toString(value);
        }
     
     //of:valueOf的简洁版
     //EnumSet.of(E)方法
     public static <E extends Enum<E>> EnumSet<E> of(E e) {
            EnumSet<E> set = EnumSet.noneOf(e.getDeclaringClass());
            set.add(e);
            return set;
        }   
        
      //getInstance:返回的实例是通过方法的参数来描述的。
      //newInstance:返回的每个实例与其他的所有实例不同。
      //getType:Type表示工厂方法返回的对象类型
      //newType:与newInstance类似
       
    

    二、多个构造器参数考虑用构建器

    静态工厂和构造器的局限性:不能很好地扩展到大量的可选参数

    对于有大量的参数的类的编写:

    /**
     *重叠构造器模式
     *提供一个只有必要参数的构造器,第二个有一个可选参数,第二个有两个,依此类推。
     */
     
     public class NutritionFacts {
        /**
         * 必选元素
         */
        private final int servingSize;
        private final int servings;
        /**
         * 可选元素
         */
        private final int calories;
        private final int fat;
        private final int sodium;
        private final int carbohydrate;
    
        /**
         * 只包含必选元素的构造方法
         */
        public NutritionFacts(int servingSize,int servings){
            this(servingSize,servings,0);
        }
        /**
         * 有一个可选元素的构造方法,以下依此类推
         */
        public NutritionFacts(int servingSize,int servings,int calories){
            this(servingSize,servings,calories,0);
        }
        
        public NutritionFacts(int servingSize,int servings,int calories,int fat){
            this(servingSize,servings,calories,fat,0);
        }
        
        public NutritionFacts(int servingSize,int servings,int calories,int fat,int sodium){
            this(servingSize,servings,calories,fat,sodium,0);
        }
        
        public NutritionFacts(int servingSize,int servings,int calories,int fat,int sodium,int carbohydrate){
            this.servingSize = servingSize;
            this.servings = servings;
            this.calories = calories;
            this.fat = fat;
            this.sodium = sodium;
            this.carbohydrate = carbohydrate;
        }
    }
    
     
    

    此方法虽可行,但如果有许多参数时,客户端代码很难编写,并且难以阅读

    /**
     *JavaBeans模式
     *无参构造方法创建对象,使用setter方法设置必选或可选参数。
     */
     public class NutritionFactsWithJavaBeans {
        /**
         * 必选元素
         */
        private int servingSize;
        private int servings;
        /**
         * 可选元素
         */
        private int calories;
        private int fat;
        private int sodium;
        private int carbohydrate;
    
        /**
         * 无参构造方法
         */
        public NutritionFactsWithJavaBeans(){
    
        }
        /**
         * Setter方法
         */
        public void setServingSize(int servingSize) {
            this.servingSize = servingSize;
        }
    
        public void setServings(int servings) {
            this.servings = servings;
        }
    
        public void setCalories(int calories) {
            this.calories = calories;
        }
    
        public void setFat(int fat) {
            this.fat = fat;
        }
    
        public void setSodium(int sodium) {
            this.sodium = sodium;
        }
    
        public void setCarbohydrate(int carbohydrate) {
            this.carbohydrate = carbohydrate;
        }
    
        public static void main(String[] args) {
            NutritionFactsWithJavaBeans cocaCola = 
                    new NutritionFactsWithJavaBeans();
            
            cocaCola.setServingSize(240);
            cocaCola.setServings(8);
            cocaCola.setCalories(100);
            cocaCola.setSodium(35);
            cocaCola.setCarbohydrate(27);
        }
    }
    
    

    JavaBeans模式创建实例容易,代码易读。但有很严重的缺点

    • 1.构造过程中可能出现不一致的状态,调试困难。
    • 2.因为有set方法,使得在JavaBeans模式中,不能将类变为不可变的,需要额外的努力来确保它的线程安全。

    重叠构造器的安全性+JavaBeans模式的可读性====>Builder模式

    易写易读,模拟了具名的可选参数。build方法可检验约束条件。

    public class NutritionFactsWithBuilder {
        private final int servingSize;
        private final int servings;
    
        private final int calories;
        private final int fat;
        private final int sodium;
        private final int carbohydrate;
    
        public static class Builder {
            //必选元素
            private final int servingSize;
            private final int servings;
    
            //可选元素,设置默认值初始化
            private int calories = 0;
            private int fat = 0;
            private int sodium = 0;
            private int carbohydrate = 0;
    
            public Builder(int servingSize, int servings) {
                this.servingSize = servingSize;
                this.servings = servings;
            }
    
            public Builder calories(int calories) {
                this.calories = calories;
                return this;
            }
    
            public Builder fat(int fat){
                this.fat = fat;
                return this;
            }
    
            public Builder sodium(int sodium){
                this.sodium = sodium;
                return this;
            }
    
            public Builder carbohydrate(int carbohydrate){
                this.carbohydrate = carbohydrate;
                return this;
            }
            public NutritionFactsWithBuilder build(){
                return new NutritionFactsWithBuilder(this);
            }
        }
    
        private NutritionFactsWithBuilder(Builder builder){
            servingSize = builder.servingSize;
            servings = builder.servings;
            calories = builder.calories;
            fat = builder.fat;
            sodium = builder.sodium;
            carbohydrate = builder.carbohydrate;
        }
    
        public static void main(String[] args) {
            NutritionFactsWithBuilder nutritionFactsWithBuilder
                    = new NutritionFactsWithBuilder.Builder(20,30).calories(3).build();
        }
    
    }
    
    

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

    Singleton表示仅仅被实例化一次的类。

    实现Singleton的方法:

    • 1.public static final域实现
    public class SingletonClazz{
       //public属性将本类唯一实例暴露出去
       public static final SingletonClazz INSTANCE = new SingletonClazz();
       //构造方法私有 保证全局唯一性
       private SingletonClazz(){
       }
       public void test(){
          System.out.println("test method");
       }
    }
    

    缺点:利用反射机制可调用到私有构造方法

     try {
                Constructor<SingletonClazz> constructor = SingletonClazz.class.getDeclaredConstructor();
                constructor.setAccessible(true);
    
                SingletonClazz clazz =  constructor.newInstance();
                clazz.test();
            } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
                e.printStackTrace();
            }
    
    

    如何防止调用私有构造方法,修改构造方法,在创建第二个实例的时候抛出异常即可。

     private SingletonClazz(){
            if(INSTANCE!=null)
                throw new RuntimeException("cannot create more than one instance of SingletonClazz");
        }
    
    • 2.静态工厂方法实现
    public class SingletonClazz {
    
        private static final SingletonClazz INSTANCE = new SingletonClazz();
        private SingletonClazz(){
    
        }
        public static SingletonClazz getInstance(){
            return INSTANCE;
        }
    
        public void test(){
            System.out.println("test method");
        }
    }
    

    这个方法相比于之前的方法的优势是,灵活性,不需要更改API的前提下,可以改变该类是否应该为Singleton。

    使Singleton类变成为可序列化的:

    • 实现Serializable接口
    • 需要声明所有实例为瞬时的(transient)
    • 提供一个readResolve()方法。

    3.编写一个包含单个元素的枚举类型来实现

    public enum SingletonClazz {
        INSTANCE;
        
        public void test(){
            System.out.println("test method");
        }
    }
    

    优点:简洁,并提供了序列化机制,而且能保证不会被多次实例化(即使是面对复杂的序列化或是发射攻击的时候)----->实现Singleton的最佳方法

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

    在缺少显式构造器的情况下,编译器会自动提供一个公有的,无参的缺省构造器

    public class DefaultConstructor {
        //没有显示构造器
        public static void main(String[] args) {
            //使用编译器提供的公有的,无参的缺省构造器
            DefaultConstructor defaultConstructor = new DefaultConstructor();
        }
    }
    

    通过将类做成抽象类来实现不可实例化的目的是不可取的。继承抽象类,子类也可以被实例化。同时也会误导用户,以为是为了继承而设计的。

    public abstract class AbstractClazz {
       //通过抽象类来实现不可实例化是不可取的
       public static void main(String[] args) {
           //仍旧可以通过继承抽象类,来达到实例化子类的目的
           SubClazz subClazz = new SubClazz();
       }
    }
    
    class SubClazz extends AbstractClazz{
        
    }
    

    正确做法是:将构造器显式地声明为私有的。

    public class UtilityClazz {
        //防止类被实例化
        private UtilityClazz(){
            throw new AssertionError();
        }
    
    }
    
    

    避免创建不必要的对象

    最好能重用对象,而不是在每次需要的时候创建一个相同功能的新对象。

    重用不可变对象。

    public class NoNeedObject {
    
        public static void main(String[] args) {
            String s1 = new String("12345");//错误做法
            String s2 = "12345";//正确做法
            String s3 = "12345";
            String s4 = new String("12345");
    
            check(s1,s2);//不同
            check(s2,s3);//相同 重用了对象
            check(s1,s4);//不同
    
        }
        public static void check(String one,String two){
            //==比较的是两个引用是否指向同一个对象
            if(one == two)
                System.out.println("same address");
            else
                System.out.println("different address");
        }
    }
    
    

    重用那些已知不会被修改的可变对象。

    class Person{
        private final Date birthDate;
    
        public Person(Date birthDate){
            this.birthDate = birthDate;
        }
    
        public boolean isBabyBoomer(){
            //没有必要的对象创建
            //每次判断都会生成Calendar,Date,TimeZone实例
            Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
            Date boomStart = gmtCal.getTime();
            gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
            Date boomEnd = gmtCal.getTime();
            return birthDate.compareTo(boomStart)>=0&&birthDate.compareTo(boomEnd)<=0;
        }
    }
    

    用一个静态的初始化器来避免上面那种效率低下的情况。

    class Person{
        private final Date birthDate;
    
        public Person(Date birthDate){
            this.birthDate = birthDate;
        }
    
        private static final Date BOOM_START;
        private static final Date BOOM_END;
    
        static {
            System.out.println("创建Calendar等对象");
            Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
            BOOM_START = gmtCal.getTime();
            gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
            BOOM_END = gmtCal.getTime();
        }
    
        public boolean isBabyBoomer(){
            return birthDate.compareTo(BOOM_START)>=0&&birthDate.compareTo(BOOM_END)<=0;
        }
       }
    

    自动装箱(JDK5引入):Java编译器能在基本类型和包装类之间自动转换。如intInteger,doubleDouble等等。

    相关学习链接:
    1.https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
    2.https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html

     public static void main(String[] args) {
            //使用包装类Long
            long beforetime = System.currentTimeMillis();
            Long sum = 0L;
            for(long i = 0;i<Integer.MAX_VALUE;i++){
                sum+=i;
            }
            long aftertime = System.currentTimeMillis();
    
            System.out.println("Long--->sum="+sum);
            System.out.println("time="+(aftertime-beforetime));
    
            //使用基本类型long
            beforetime = System.currentTimeMillis();
            long sum2 = 0L;
            for(long i = 0;i<Integer.MAX_VALUE;i++){
                sum2+=i;
            }
            aftertime = System.currentTimeMillis();
            System.out.println("long--->sum="+sum2);
            System.out.println("time="+(aftertime-beforetime));
    
     }
    
    

    运行结果:

    Long--->sum=2305843005992468481
    time=9642
    long--->sum=2305843005992468481
    time=777
    

    结论:优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱。

    注意: 很多规定只是建议,不要矫枉过正,犯了教条主义的错误,一定要与实际的开发情况结合,实事求是。

    不要错误地认为“应该尽可能地避免创建对象”,应该是“避免创建不必要的对象”,注意是不必要的!

    通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象非常重要。

    消除过期的对象引用

    手工管理内存的语言:C或C++

    具有垃圾回收功能的语言:Java,简化程序员的工作

    public class Stack {
        private Object[] elements;
        private int size = 0;
        private static final int DEFAULT_INITIAL_SIZE = 16;
    
        public Stack(){
            elements = new Object[DEFAULT_INITIAL_SIZE];
        }
    
        public void push(Object e){
            ensureCapacity();
            elements[size++] = e;
        }
    
        /**
         * stack: push(1) push(2) push(3) push(4) push(5) pop() pop()
         *      5
         *      4
         *      3  ---->栈顶
         *      2
         *      1  
         *      
         *      栈内部维护着过期的引用,也就是永远不会再被解除的引用,如4和5
         *    fixed:
         *        elements[size]=null;
         *        
         */
        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);
            }
        }
    }
    
    

    上面这段代码存在内存泄露的问题,当栈先是增长,然后弹出,从栈中弹出的对象都不会被当做垃圾回收。

    栈内部维护着这些对象的过期引用,也就是永远不会被解除的引用。栈数组中下标大于或等于size的元素的引用都是过期的引用。

    在支持垃圾回收的语言中, 内存泄露(也就是无意识的对象保持)很隐蔽。

    修复办法:一旦对象引用过期,就清空这些引用。

    注意:清空对象引用应该是一种例外,而不是一种规范行为。

    容易导致内存泄露的几种情形:

    • 类自己管理内存
    • 缓存
    • 监听器和其他回调

    避免使用终结方法

    终结方法(finalizer):不可预测,危险,一般情况下是不必要的。

    终结方法的缺点:

    • 不能保证会被及时地执行,而且根本不会保证它们会被执行。(所以不应该依赖终结方法来更新重要的持久状态
    • 严重的性能损失

    终止类的对象中封装的资源(文件或线程),只需提供一个显式的终止方法。不需要编写终结方法。

    例子:

    • InputStream,OutputStream,java.sql.Connection的close方法
    • java.util.Timer的cancel方法

    一般与try-catch结合起来使用,以确保及时终止

  • 相关阅读:
    iOS,Layer
    iOS 手势冲突
    ECharts
    手动安装Package Control
    webstorm配置svn详解
    js 对象 类型转换
    google 跨域解决办法
    关于内层DIV设置margin-top不起作用的解决方案
    图片与文字在div里实现垂直水平都居中
    css3之3D翻牌效果
  • 原文地址:https://www.cnblogs.com/JohnTsai/p/5281805.html
Copyright © 2020-2023  润新知