java:effectivejava学习笔记之避免创建不必要的对象(代码片段)

JMW1407 JMW1407     2023-02-18     718

关键词:

避免创建不必要的对象

在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:effectivejava学习笔记之始终要覆盖tostring方法(代码片段)

Java始终要覆盖toString方法始终要覆盖toString方法1、为什么要覆盖toString方法?2、始终要覆盖toString方法始终要覆盖toString方法1、为什么要覆盖toString方法?publicclassPerson protectedStringname; protectedintage; publicStringgetName() retur... 查看详情

java:effectivejava学习笔记之优先考虑泛型和泛型方法(代码片段)

Java优先考虑泛型和泛型方法1、优先考虑泛型2、优先考虑泛型方法参考1、优先考虑泛型下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。publicclassStackprivateObject[]elem... 查看详情

effectivejava学习笔记之创建和销毁对象

一、考虑用静态工厂方法代替构造器1、此处的静态工厂方法是指返回指为类的对象的静态方法,而不是设计模式中的静态工厂方法。2、静态工厂方法的优势有:a、使用不同的方法名称可显著地表明两个静态工厂方法的不同,而... 查看详情

effectivejava学习笔记之所有对象都通用的方法

一、覆盖equals时请遵守通用约定1、满足下列任何一个条件时,不需要覆盖equals方法a、类的每个实例本质上都是唯一的。此时就是Object中equals方法所表达的含义。b、不关心类是否提供了“逻辑相等”的测试功能c、超类中覆... 查看详情

effectivejava学习笔记之不可实例化的类

在没有显式声明一个类的构造方法时,编译器会生成默认的无参构造方法,在设计工具类时,我们通常将方法设置成静态方法,以类名.方法名的形式调用,此时这个类就没有必要创建实例,我们知道抽象类不可以被实例化,但... 查看详情

java:java学习笔记之java单例模式的简单理解和使用(代码片段)

...单例的实现如下:2、懒汉式终极版本:volatile3、EffectiveJava1——静态内部类4、5.2EffectiveJava2——枚举参考Java单例模式1、饿汉式单例的实现如下://饿汉式实现publicclassSingleBprivatestaticfinalSingleBINSTA 查看详情

java:effectivejava学习笔记之考虑实现comparable接口(代码片段)

Java考虑实现Comparable接口考虑实现Comparable接口1、Comparable接口2、为什么要考虑实现Comparable接口3、compareTo方法的通用约定4、何时以及如何实现Comparable接口4.1、多重比较5、实现Comparable接口所需满足的需求6、总结参考考虑实现Compa... 查看详情

java:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

Java覆盖equals时请遵守通用约定覆盖equals时请遵守通用约定1、为什么要覆盖equals2、需要覆盖equals方法的时机2.1、不需要覆盖equals方法的情况2.2、需要覆盖equals方法的情况2.2.1、自反性2.2.2、对称性2.2.3、传递性2.2.4、一致性2.2.5、... 查看详情

java:effectivejava学习笔记之接口只用于定义类型类层次优于标签类(代码片段)

Java接口只用于定义类型1、接口只用于定义类型1.1、常量接口2、类层次优于标签类1、接口只用于定义类型当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明可以对这个类的实... 查看详情

java:effectivejava学习笔记之列表优先于数组(代码片段)

Java列表优先于数组列表优先于数组1、协变与不可变类型2、运行时检验与编译器检验3、可具体化与不可具体化4、无法很好混用列表和数组5、案例分析参考列表优先于数组1、协变与不可变类型1、数组是协变类型,指继承关... 查看详情

java:effectivejava学习笔记之请不要在新代码中使用原生态类型(代码片段)

Java请不要在新代码中使用原生态类型1、请不要在新代码中使用原生态类型参考1、请不要在新代码中使用原生态类型1、在没有泛型之前,从集合中读取到每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象&#x... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

java:effectivejava学习笔记之接口优于抽象类(代码片段)

Java接口优于抽象类接口优于抽象类1、接口和抽象类2、接口优点3、骨架类3.1、demo参考接口优于抽象类1、接口和抽象类Java中抽象类和接口的区别2、接口优点1、现有的类可以很容易的被更新,以实现新的接口。如果你前期编... 查看详情

java:effectivejava学习笔记之静态工厂方法的简单理解和使用(代码片段)

Java静态工厂方法静态工厂方法一、什么是静态工厂方法?二、静态工厂方法的优势1、静态工厂方法与构造器不同的第一优势在于,它们有名字2、静态工厂方法不用在每次调用的时候都创建一个新的对象3、静态工厂方法... 查看详情

effectivejava第十章并发避免过度同步读书笔记

 避免过度同步为了避免活性失败和安全性失败,再一个被同步的方法或者代码快中,永远不要放弃对客户端的控制。因为外来的,属于不可控的将外来方法调用移出同步的代码快。建立快照使用并发集合,CopyOnWriteArayList。... 查看详情

java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

Java覆盖equals时总要覆盖hashcode覆盖equals时总要覆盖hashcode1.什么是hashcode方法?2.hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)3.为什么要覆盖hashcode3.1、覆盖equals时总要覆盖hashCode3.2、如何在覆盖... 查看详情

java:effectivejava学习笔记之通过私有构造器强化不可实例化的能力(代码片段)

Java通过私有构造器强化不可实例化的能力通过私有构造器强化不可实例化的能力参考通过私有构造器强化不可实例化的能力并非所有的类都是需要实例化的。有时候,我们可能需要编写至包含静态方法和静态域的类。这些类... 查看详情

java:effectivejava学习笔记之复合优先于继承(代码片段)

Java复合优先于继承复合优先于继承1、实现继承和接口继承2、在实际开发中继承的缺点2.1、子类依赖于其超类中特定功能的实现细节3、什么是复合?3.1、书上案例4、复合相比较于继承的优点和缺点5、何时使用继承,何时使用... 查看详情