Java:Effective java学习笔记之 避免创建不必要的对象

Posted JMW1407

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java:Effective java学习笔记之 避免创建不必要的对象相关的知识,希望对你有一定的参考价值。

避免创建不必要的对象

在Java开发中,程序员要尽可能的避免创建相同的功能的对象,因为这样既消耗内存,又影响程序运行速度。在这种情况下可以考虑重复利用对象。

尽量少的创建对象,如果单个对象能够满足要求,就使用单例模式,反复重用唯一的对象。对一些创建成本低的对象来说,这样做带来的好处也许并不明显。但对于一些创建成本高的对象来说,这样做可以明显地节约系统资源、提升系统性能。有以下几种方法:

  • 1、采用更合适的API或工具类减少对象的创建
  • 2、重用相同功能的对象
  • 3、小心自动装箱(auto boxing)
  • 4、用静态工厂方法而不是构造器

1、采用更合适的API或工具类减少对象的创建

可能导致滥用对象的一个典型例子就是 字符串 。在学习Java基础的过程中,一定会提到String类的对象一旦被创建,它的值就是不能改变的。通过查看JDK中String类的源码,我们可以看到String类是通过一个 byte数组 来存储字符串的,而且这个数组被修饰为final常量。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc /**     
    * The value is used for character storage.    
    *  ...    
    */
    @Stable
    private final byte[] value;
        
    ...

 /*     
    * JDK9之前,String类是用一个char数组来存储字符串的。如果你觉得上面用byte数组来存储字符串不好理解           
    * 的话,也可以简单地理解为String类仍是用一个char数组来存储字符串     
    */
    private final char value[];

如下两种写法看似没有什么区别,但是如果深入jvm底层了解,我们可以利用jvm运行时常量池的特性,避免创建具有相同功能的String对象(尤其是在循环内部创建)可以带来比较可观的性能优化以及节约内存。

// 每次都会创建一个新的String对象,且不会加入常量池
String str = new String("aaa");

因为当我们往构造方法里传入aaa的时候,其实这个aaa就是一个String实例了。我们等于是创建了两个String实例,参数”aaa”本身就是一个String对象,new String()又会产生新的String对象。

正确的做法如下:

String str = "aaa";

根据jdk文档,上述方式实际上等同于:

char data[] = 'a', 'a', 'a';
String str = new String(data);

传入一个字符数组来创建String,避免了创建重复对象。

再举一个常见的例子,我们有时希望遍历一个list,将其中的元素存到一个字符串里,并用逗号分隔。我们可能会用下面这种最low的办法:

public static String listToString(List<String> list) 
    String str = "";
    for (int i = 0; i < list.size(); i++) 
        str += list.get(i);
        if (i < list.size() - 1) 
            str += ",";
        
    
    return str;

这样其实在每次+=的时候都会重新创建String对象,极大地影响了性能。
我们可以修改一下,采用StringBuilder的方式来拼接list:

public static String listToString(List<String> list) 
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = 0; i < list.size(); i++) 
        stringBuilder.append(list.get(i));
        if (i < list.size() - 1) 
            stringBuilder.append(",");
        
    
    return stringBuilder.toString();

除此之外,刚写Java代码的程序员们,也要正确的选择String、StringBuilder、StringBuffer类的使用。

  • String为不可变对象,通常用于定义不变字符串;
  • StringBuilder、StringBuffer用于可变字符串操作场景,如字符串拼接;
    • 其中StringBuffer是线程安全的,它通过Synchronized关键字来实现线程同步。
// StringBuffer中的append()方法
public synchronized StringBuffer append(String str) 
    toStringCache = null;
    super.append(str);
    return this;

 
// StringBuilder中的append()方法
public StringBuilder append(String str) 
    super.append(str);
    return this;

2、重用相同功能的对象

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

修改前

public class Demo1 

    private final Date birthday;

    public Demo1(Date birthday) 
        this.birthday = birthday;
    
    
    //不可取,每一次调动都会新建一个Calendar,一个TimeZone和两个Date
    public Boolean isBabyBoomer()
        //Unnecessary allocation of expensive omitted  被忽略的昂贵的不必要分配
        Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        System.out.println(gmt);
        gmt.set(1946,Calendar.JANUARY,1,0,0,0);
        Date startTime = gmt.getTime();
        gmt.set(1965,Calendar.JANUARY,1,0,0,0);
        Date endTime = gmt.getTime();
        return birthday.compareTo(startTime)>= 0 &&birthday.compareTo(endTime)<0 ;
    

    public static void main(String[] args) 
        Demo1 demo1 = new Demo1(new Date());
        Boolean babyBoomer = demo1.isBabyBoomer();
        Demo1 demo12= new Demo1(new Date());
        Boolean babyBoomer1 = demo12.isBabyBoomer();
    


不可取,每一次调动都会新建一个Calendar,一个TimeZone和两个Date

修改后:

public class Demo2 
    private final Date birthday;

    public Demo2(Date birthday) 
        this.birthday = birthday;
    

    private static final Date startTime;
    private static final Date endTime;

    //静态代码块
    static 
        Calendar gmt = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
        System.out.println(gmt);
        gmt.set(1946,Calendar.JANUARY,1,0,0,0);
        startTime = gmt.getTime();
        gmt.set(1965,Calendar.JANUARY,1,0,0,0);
        endTime = gmt.getTime();
    

    public boolean isBabyBoomer()
        return birthday.compareTo(startTime)>= 0 &&birthday.compareTo(endTime)<0 ;
    

    public static void main(String[] args) 
        Demo2 d1 = new Demo2(new Date());
        boolean b1 = d1.isBabyBoomer();
        Demo2 d2 = new Demo2(new Date());
        boolean b2 = d2.isBabyBoomer();
    


改进后Demo2 类只会在初始化的时候创建Calendar,TimeZone和Date实例一次,而不是每一次调用isBabyBoomer()方法都创建一次。既提高了性能又使代码的含义更加清晰了。

二、上面我们谈到了一个不可变对象的重用,接下来我们再看看可变对象的重用。

可变对象的重用可以通过视图(views)来实现。比如,Map的keySet()方法就会返回Map对象所有key的Set视图。这个视图是可变的,但是当Map对象不变时,在任何地方返回的任何一个keySet都是一样的,当Map对象改变时,所有的keySet也会相应的发生改变。

package com.czgo.effective;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class TestKeySet 

    public static void main(String[] args) 

        Map<String,Object> map = new HashMap<String,Object>();
        map.put("A", "A");
        map.put("B", "B");
        map.put("C", "C");

        Set<String> set = map.keySet();
        Iterator<String> it = set.iterator();
        while(it.hasNext())
            System.out.println(it.next()+"①");
        

        System.out.println("---------------");

        map.put("D", "D");
        set = map.keySet();
        it = set.iterator();
        while(it.hasNext())
            System.out.println(it.next()+"②");
        

    


3 、小心自动装箱(auto boxing)

public class Main 
    public static void main(String[] args) 
        final long startTime = System.currentTimeMillis();​
        Long sum = 0L; // 将sum声明为Long类型        
        for (long i = 0; i <= Integer.MAX_VALUE; i++) 
            sum += i;
        ​
        final long endTime = System.currentTimeMillis();​
        System.out.println("程序执行了:" + (endTime - startTime) + "ms");
    

程序执行了:7298ms

将sum声明为Long类型时,程序大约会构造 个多余的 Long实例。

如果将sum声明为long类型,程序的执行时间会大大地缩短。

public class Main 
    public static void main(String[] args) 
        final long startTime = System.currentTimeMillis();long sum = 0L; // 将sum声明为long类型        for (long i = 0; i <= Integer.MAX_VALUE; i++) 
            sum += i;
        ​
        final long endTime = System.currentTimeMillis();​
        System.out.println("程序执行了:" + (endTime - startTime) + "ms");
    

程序执行了:1308ms

由此,我们可以得出结论:

  • 优先使用基本数据类型
  • 避免不必要的自动装箱

所以我们在日常开发中,方法内尽量用基本类型,只在入出参的地方用包装类型。多留心,切忌无意识地使用到自动装箱。

4 、用静态工厂方法而不是构造器

对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。

Boolean是常用的类型,在开发中也应该使用Boolean.valueof()而不是new Boolean(),从Boolean的源码可以看出,Boolean类定义了两个final static的属性,而Boolean.valueof()直接返回的是定义的这两个属性,而new Boolean()却会创建新的对象。

public static final Boolean TRUE = new Boolean(true);
 
public static final Boolean FALSE = new Boolean(false);

静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。

package com.czgo.effective;

/**
 * 用valueOf()静态工厂方法代替构造器
 *
 */
public class Test 

    public static void main(String[] args) 
        // 使用带参构造器
        Integer a1 = new Integer("1");
        Integer a2 = new Integer("1");

        //使用valueOf()静态工厂方法
        Integer a3 = Integer.valueOf("1");
        Integer a4 = Integer.valueOf("1");

        //结果为false,因为创建了不同的对象
        System.out.println(a1 == a2);

        //结果为true,因为不会新建对象
        System.out.println(a3 == a4);
    


5 、正则表达式

正则表达式我们经常用于字符串是否合法的校验,Java中正则表达式

public static void main(String[] args) 
 
    String email = "1057301174@qq.com";
    String regex = "^([a-z0-9A-Z]+[-|\\\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\\\.)+[a-zA-Z]2,$";
 
    long start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) 
        email.matches(regex);
    
 
    System.out.println(System.currentTimeMillis() - start);
 

执行这段代码的时间,一共耗时71毫秒,看似好像挺快的!

但是我们做个非常简单的优化,优化后的代码如下所示:

public static void main(String[] args) 
 
    String email = "1057301174@qq.com";
    String regex = "^([a-z0-9A-Z]+[-|\\\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\\\.)+[a-zA-Z]2,$";
    Pattern pattern = Pattern.compile(regex);
 
    long start = System.currentTimeMillis();
    for (int i = 0; i < 10000; i++) 
        //email.matches(regex);
        pattern.matcher(email);
    
 
    System.out.println(System.currentTimeMillis() - start);
 

再次执行代码,一共耗时1毫秒

这是因为String.matches()方法在循环中创建时,每次都需要执行Pattern.compile(regex),而创建Patter实例的成本很高,因为需要将正则表达式编译成一个有限状态机( finite state machine)。

6、补

如果涉及到对象池的应用,除非池中的对象非常重,类似数据库连接,否则最好不要去自己维护一个对象池,因为这样会很复杂。另外,有时考虑到系统的安全性,那么我们需要进行防御性复制,这个在后面会讲到。此时,重复创建对象就是有意义的,因为比起隐含错误和安全漏洞,重复创建对象带来的性能损失是可以接受的。

参考

1、如何在Java中避免创建不必要的对象(备战2022春招或暑期实习,每天进步一点点,打卡100天,Day1)
2、《Effective Java》阅读笔记5 避免创建不必要的对象

以上是关于Java:Effective java学习笔记之 避免创建不必要的对象的主要内容,如果未能解决你的问题,请参考以下文章

Java:Effective java学习笔记之 避免使用终结方法

Java:Effective java学习笔记之 消除过期对象引用

Java:Effective java学习笔记之 列表优先于数组

Java:Effective java学习笔记之 用enum代替int常量

Java:Effective java学习笔记之 复合优先于继承

Java:Effective java学习笔记之 接口优于抽象类