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