结合实际谈谈个人对代码优化的感想以及java优化

前言

本来想写一篇结合在实际工作中,自己去优化java代码的文章,用于记录便于复习提升自己的;但是在回想起自己在实际工作中诸多因素导致存在的问题(仅针对我个人),个人总结以及去证实了,所悟:合理的设计是必须的,但是在时间不允许的情况下,不要过早的去过分优化你的代码,首先保证它正常运行且无严重bug,其次是保证你的开发任务交付,是否满足实际的需求,再然后考虑代码的可读性,最后整体下来,利用个人时间去优化自己的代码,做到对自己负责和对你写的系统代码负责!

  • 在软件工程领域,有个词叫做「过早优化」。指的是写代码的过程中,还没完成功能,就开始搞优化,例如写到一半,就去想刚才那个函数换个写法是不是效率更高或者兼容性更好,这样一来二去的,会严重你的开发进度,再者会影响你开发的思路,导致本来很简单的东西被你过分设计。
  • 著名的计算机科学家,经典巨著《计算机程序设计的艺术》的作者Donald
    Knuth说过一句话:「过早优化是万恶之源」
    Premature optimization is the root of all evil); 我们说不要过早优化,并不是从不优化,而是让优化伴随整个软件生命周期;在软件领域,又有一个词,叫做 「屎山」 ,指的是代码因为长期缺乏维护和优化,变得难以阅读,难以修改,甚至稍微一改动就会奔溃。
  • 我觉得合理的安排应该是:完成开发任务后,利用个人时间去把自己写的代码不断的优化改进,进而提升自己也是很有必要的,重点是:你可以这个点不断的去思考,不断的去挖掘,从而去学习更多自己所不会的!
    在这里插入图片描述
    为什么说过早优化是万恶之源?

代码优化的目的

我们代码优化的目的是什么?
(下面优先级是个人看法,求同存异,可以互相交流)
在我认为也是存在优先级的:

  • 减少bug的出现;(如果你优化后代码的严重bug反而增多了,那么你的优化一切将毫无意义!);
  • 增加代码的可读性;(相信你也曾接手过一个注释说明都没有,或者精简到只有一行的代码,这些代码的可读性很差,代码优雅不代表它不具有可读性,这二者是不冲突的,后面的人接手后一时半会不能直接下手,严重影响代码的延续);
  • 低耦合高内聚
  • 提高代码的运行效率;(运行效率这个词在软件开发中往往和时间复杂度或者空间复杂度挂钩;实际工作中,我往往会从多个角度考虑问题,比如:jvm、利用计算机语言例如进行计算时可使用移位运算,当然你必须要结合实际业务场景,有需要的话要从多线程或分布式等角度去考虑问题等等);
  • 减少内存空间;(memory leak、out of memory相信你也曾遇到过,不要过分的去创建的对象,合理利用堆栈内存,GC回收,java创建对象的方式有很多种,结合你的业务场景去选择适合的,或者其实很多对象无需创建,只需改变指针变可);
  • 减小代码的体积;(不要去写那些繁琐且冗余的代码,提升代码的可复用性;理清逻辑,不要写那些废话代码,比如:考虑设计成注解、利用可变参数、泛型去设计通用的工具类、设计模式、把一些专门的活交给专门的服务去做例如数据库服务等等);

我认为我的代码写的很烂,所以才不断去学习,不断记录,整个业界有很多规范以及代码优秀方案,我也只是个学习者,再浑沦吞枣的看完了两本优化代码的书籍后多少得到了些许的提升,这篇只写我的优化,后续还会不断去学习更多优秀的优化方案,下面进入主题。

1、优化多if/else

if else作为每种编程语言都不可或缺的条件语句,在编程时会大量的用到。一般建议嵌套不要超过三层,如果一段代码存在过多的if/else嵌套,代码的可读性就会急速下降,后期维护难度也大大提高。

1.1 整理逻辑,多if合并判断

例如:

String sourceTable = xxx;
String dicts = xxx;
if(StringUtils.isNotBlank(sourceTable)){
    
    
...
}else if(StringUtils.isNotBlank(dicts)){
    
    
...
}
if(StringUtils.isBlank(sourceTable) && StringUtils.isNotBlank(dicts)){
    
    
...
}else if(StringUtils.isBlank(dicts) && StringUtils.isNotBlank(sourceTable)){
    
    
...
}
if(StringUtils.isBlank(dicts) && StringUtils.isBlank(sourceTable)){
    
    
...
}
if(StringUtils.isNotBlank(dicts) && StringUtils.isNotBlank(sourceTable)){
    
    
...
}

改成:

if(StringUtils.isBlank(dicts) && StringUtils.isBlank(sourceTable)){
    
    
...
return;
}else if(StringUtils.isNotBlank(sourceTable)){
    
    
...
}
...
return;

1.2 取反判断,提前return

例如

if(StringUtils.isNotBlank(codeTable.getId())){
    
    
...
}else{
    
    
return xxx;
}

改为:

if(StringUtils.isBlank(codeTable.getId())){
    
    
	return xxx;
}
...

1.3 Optional

jdk8新特性

//jdk8之前,从学生中找出年级大于等于18且成绩大于80的
 for (Student student : studentList) {
    
    
        if (student != null) {
    
    
            if (student.getAge() >= 18) {
    
    
                Integer score = student.getScore();
                if (score != null && score > 80) {
    
    
                    System.out.println(student.getName());
                }
            }
        }
 }

改为:

 for (Student student : studentList) {
    
    
        Optional<Student> studentOptional = Optional.of(student);
        Integer score = studentOptional.filter(s -> s.getAge() >= 18).map(Student::getScore).orElse(0);
        if (score > 80) {
    
    
            System.out.println(student.getName());
        }
 }

1.4 switch/case

关于使用switch和if那个效率更快,我这边觉得,if/else适用性更广,而switch/case更局限性,单比速度的话,在java中,多判断的情况下switch/case更快,而且少判断的情况下if/else更快

switch (strTableName) {
    
    
            case "DM_SJZLJC_GZ_001":
                ...
                break;
            case "DM_SJZLJC_GZ_002":
                ...
                break;
            case "DM_SJZLJC_GZ_003":
                ...
                break;
            case "DM_SJZLJC_GZ_004":
                ...
                break;
            case "DM_SJZLJC_GZ_005":
                ...
                break;
            case "DM_SJZLJC_GZ_006":
                ...
                break;
            case "DM_SJZLJC_GZ_007":
                ...
                break;
            case "DM_SJZLJC_GZ_008":
                ...
                break;
            case "DM_SJZLJC_GZ_009":
                ...
                break;
            case "DM_SJZLJC_GZ_010":
                ...
                break;
            case "DM_SJZLJC_GZ_011":
                ...
                break;
            case "DM_SJZLJC_GZ_012":
                ...
                break;
           	default:
                return 0;     

1.5 数组

来自google解释,这是一种编程模式,叫做表驱动法,本质是从表里查询信息来代替逻辑语句,比如有这么一个场景,通过月份来获取当月的天数,仅作为案例演示,数据并不严谨。

例如:

int getDays(int month){
    
    
    if (month == 1)  return 31;
    if (month == 2)  return 29;
    if (month == 3)  return 31;
    if (month == 4)  return 30;
    if (month == 5)  return 31;
    if (month == 6)  return 30;
    if (month == 7)  return 31;
    if (month == 8)  return 31;
    if (month == 9)  return 30;
    if (month == 10)  return 31;
    if (month == 11)  return 30;
    if (month == 12)  return 31;
}

改为:

int monthDays[12] = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
int getDays(int month){
    return monthDays[--month];
}

1.6 策略模式

下面举例是理想中的,实际还要根据业务去拆分,合理设计;
例如:

if (temp.equals("1111")) {
    
    
  ...
} else if (temp.equals("2222")) {
    
    
   ...
} else if (temp.equals("3333")) {
    
    
   ...
} else if (temp.equals("4444")) {
    
    
   ...
}

改为:

//定义接口
public interface Itemp {
    
    
	//模板拼接的动作  
    void splic();  
}  

//定义一个注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TempCode {
    
    
    String tempName();
    String tempValue();
}

@TempCode(tempName= "1111",value = "1111")
@Component("temp1")
public class temp1 implements Itemp {
    
    
    @Override
    public void splic() {
    
    
        ...
    }
}
 
@TempCode(tempName= "2222",value = "2222")
@Component("temp2")
public class temp2 implements Itemp {
    
    
    @Override
    public void splic() {
    
    
        ...
    }
}
 
@TempCode(tempName= "3333",value = "3333")
@Component("temp3")
public class temp3 implements Itemp {
    
    
    @Override
    public void splic() {
    
    
        ...
    }
}

@TempCode(tempName= "4444",value = "4444")
@Component("temp4")
public class temp4 implements Itemp {
    
    
    @Override
    public void splic() {
    
    
        ...
    }
}


@Service
@Slf4j
public class TempService implements ApplicationListener<ContextRefreshedEvent> {
    
    
    private static Map<String, Itemp> iTempMap = null;
 
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
    
    
        //在初始化或刷新ApplicationContext时发布
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        //获取所有拥有特定TempCode注解的Bean(temp1、temp2、temp3、temp4)
        Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(TempCode.class);
 
        if (beansWithAnnotation != null) {
    
    
            payMap = new HashMap<>();
            beansWithAnnotation.forEach((key, value) -> {
    
    
                String bizType = value.getClass().getAnnotation(TempCode.class).value();
                iTempMap.put(bizType, (ITemp) value);
            });
        }
    }
 
    public void splic(String code) {
    
    
        iTempMap.get(code).splic();
    }
}
 
 
    @GetMapping("/splic")
    @ApiOperation("模板拼接")
    public void splic(String code){
    
    
        tempService.splic(code);
    }

2、循环

  • 优化多for循环
    应减少循环体内容,合理利用break和continue
    例如:
for (int i = 0, length = list.size(); i < length; ++i){
    
    
	if(StringUtils.isBank(list.get(i)){
    
    
                continue;
            }
}

for (int i = 0, length = list.size(); i < length; ++i){
    
    
	if(StringUtils.isBank(list.get(i)){
    
    
                break;
            }
}

使用Map和for代替多层for循环,
例如:

for (ProjectApplyVO projectVo : lstProjectVo) {
    
    
            lstTableVo.forEach(tableVo ->{
    
    
                if (projectVo.getApplyId().equals(tableVo.getId())){
    
    
                    String tableName = "";
                    String directoryName = "";
                    int tableNum = 0;
                    if (Objects.nonNull(tableVo.getTableInfos().get(0))){
    
    
                        tableNum = tableVo.getTableInfos().size();
                        if (StringUtils.isNotBlank(tableVo.getTableInfos().get(0).getTableName())){
    
    
                            tableName = tableVo.getTableInfos().get(0).getTableName();
                        }
                        if (StringUtils.isNotBlank(tableVo.getTableInfos().get(0).getDictionaryPath())){
    
    
                            directoryName = tableVo.getTableInfos().get(0).getDictionaryPath();
                        }
                    }
                    //上報工作流
                    projectVo.setApplyContent(workflowVO.getApplyContentStr(ApplyFlowConstants.APPLY_ZCTEMP_STR,tableVo.getProjectName(),directoryName,tableName,tableNum));
                }
            });
        }

改为:

//jdk8中stream()list转map,空指针会报错
Map<String, List<TableInfoVO>> lstTableMap =
            lstTableVo.stream().collect(Collectors.toMap(DataApplyVO::getId, DataApplyVO -> DataApplyVO.getTableInfos() == null ? new ArrayList<>()  : DataApplyVO.getTableInfos(),(v1, v2) -> v1));


for (int i = 0, length = lstProjectVo.size(); i < length; ++i) {
    
    
ProjectApplyVO projectVo = lstProjectVo.get(i);
            List<TableInfoVO> tableInfoVOS = lstTableMap.get(projectVo.getApplyId());
            if (CollectionUtils.isEmpty(tableInfoVOS)){
    
    
                break;
            }
            TableInfoVO tableVo = tableInfoVOS.get(0);
            //上報工作流
            projectVo.setApplyContent(workflowVO.getApplyContentStr(ApplyFlowConstants.APPLY_ZCTEMP_STR,projectVo.getProjectName(),tableVo.getDictionaryPath(),tableVo.getTableName(),tableInfoVOS.size()));
        }
  • 使用++i,代替i++
    例如:
for (int i = 0; i < list.size(); i++){
    
    ...}

建议改为:

for (int i = 0, length = list.size(); i < length; ++i){
    
    ...}
//或者,提前把length = list.size();定义出来,不要每次循环都要调用一次size();
int length = list.size();
for (int i = 0, i < length; ++i){
    
    ...}
  • 在list.size()很大的时候,就减少了很多的消耗;
  • for循环中 i++ 和 ++i 的使用效果是一样的,但是for循环中 i++的耗时比++i的要长一些,for循环中 i++ 在处理时,i++实际为i = i+1,执行时先创建临时变量保存 i 值,然后再+1,而++i不需要的,没有这个过程,所以++i的性能高于i++;
  • for循环中 i++由于要创建临时变量并保存i的值,所以需要占用内存,使用完后释放内存,一个是造成资源占用,一个是数据量大时,造成程序性能低; 数据量大些时候就比较明显了;
  • for、foreach、stream选择
  • 如果数据在1万以内的话,for循环效率高于foreach和stream;如果数据量在10万的时候,stream效率最高,其次是foreach,最后是for。另外需要注意的是如果数据达到100万的话,parallelStream异步并行处理效率最高,高于foreach和for。
  • foreach循环的底层实现原理就是迭代器Iterator,如果是顺序访问的,则使用Iterator会效率更高”的意思就是顺序访问的那些类实例,使用foreach循环去遍历。
  • 应该使用Collection.isEmpty()检测是否为空

使用Collection.size() 来检测空逻辑上没有问题,但是使用 Collection.isEmpty() 使得代码更易读,并且可以获得更好的性能。任何 Collection.isEmpty() 实现的时间复杂度都是 O(1) ,但是某些Collection.size() 实现的时间复杂度可能是O(n)。

  • 频繁调用 Collection.contains 方法请使用 Set

在 java 集合类库中,List 的 contains 方法普遍时间复杂度是 O(n) ,如果在代码中需要频繁调用 contains
方法查找数据,可以先将 list 转换成 HashSet 实现,将 O(n) 的时间复杂度降为 O(1) 。

  • 嵌套循环
    嵌套循环应该遵循“外小内大”的原则虽然循环次数没变,但是耗时却长了很大。

  • 防止死循环

  • 基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList

  • 应当使用最普通的for循环而不是foreach循环来遍历
    例如:

if (list instanceof RandomAccess)
{
    
    
    for (int i = 0; i < list.size(); i++){
    
    }
}
else
{
    
    
    Iterator<?> iterator = list.iterable();
    while (iterator.hasNext()){
    
    iterator.next()}
}
  • 准确的来说,应该是:实现RandomAccess接口的集合比如ArrayList,应当使用最普通的for循环而不是foreach循环来遍历;
  • JDK API,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。
  • foreach循环的底层实现原理就是迭代器Iterator,参见Java语法糖1:可变长度参数以及foreach循环原理。所以后半句”反过来,如果是顺序访问的,则使用Iterator会效率更高”的意思就是顺序访问的那些类实例,使用foreach循环去遍历。
  • 循环内不要不断创建对象引用
    例如:
for (int i = 1; i <= count; i++)
{
    
    
    Object obj = new Object();    
}

改为:

Object obj = null;
for (int i = 0; i <= count; i++)
{
    
    
    obj = new Object();
}

第一种做法,会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了
第二种做法,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。

  • 循环体内不要使用”+”进行字符串拼接,而直接使用StringBuilder不断append
    例如:
public String appendStr(String oriStr, String... appendStrs) {
    
    
    if (appendStrs == null || appendStrs.length == 0) {
    
    
        return oriStr;
      }
    for (String appendStr : appendStrs) {
    
    
        oriStr += appendStr;
    }
    return oriStr;
}

改为:

public String appendStr(String oriStr, String... appendStrs) {
    
    
    if (appendStrs == null || appendStrs.length == 0) {
    
    
        return oriStr;
      }
      StringBuilder sb=new StringBuilder(appendStrs);
    for (StringBuilder sb: sb) {
    
    
        sb.append(sb);
    }
    return sb.toString();
}

在jvm中,每次虚拟机碰到”+”这个操作符对字符串进行拼接的时候,会new出一个StringBuilder,然后调用append方法,最后调用toString()方法转换字符串赋值给oriStr对象,即循环多少次,就会new出多少个StringBuilder()来,这对于内存是一种浪费。

  • 不要对数组使用toString()方法
    例如:
public static void main(String[] args)
{
    
    
    int[] is = new int[]{
    
    1, 2, 3};
    System.out.println(is.toString());
}
结果:[I@18a992f

有可能因为数组引用is为空而导致空指针异常。不过虽然对数组toString()没有意义,但是对集合toString()是可以打印出集合里面的内容的,因为集合的父类AbstractCollections重写了Object的toString()方法。

3、关于集合和字符串

  • String类尽量使用StringBuild、StringBuffer

一般字符串在编译期间java会进行优化,但是在循环中字符拼接,java编辑期无法做到优化,所以需要使用StringBuild 、 StringBuffer进行替换

  • 字符串变量和字符串常量equals的时候将字符串常量写在前面
    这么做主要是可以避免空指针异常
    例如:
String str = "123";
if (str.equals("123"))
{
    
    
    ...
}

改为:

String str = "123";
if ("123".equals(str))
{
    
    
    ...
}
  • 顺序插入和随机访问比较多的场景使用ArrayList,元素删除和中间插入比较多的场景使用LinkedList
  • 尽量给集合、字符串初始化长度
  • 如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度;
  • ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等;
  • 可以通过类(这里指的不仅仅是下面的StringBuilder)的构造函数来设定它的初始化容量,这样可以明显地提升性能。
    例如:
    • StringBuilder()      // 默认分配16个字符的空间
    • StringBuilder(int size)  // 默认分配size个字符的空间
    • StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间
  • length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2的N次幂,那么:在4096的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间,把原来的4096个字符拷贝到新的的字符数组中去;
  • 这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0初始大小建议设置为2的N次幂
  • 如果你看过ArrayList源码,你就会发现它的默认大小是10,如果添加元素超过了一定的阀值,会按1.5倍的大小扩容。
  • 当复制大量数据时,使用System.arraycopy()命令
  • 尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销
  • 如果要使用String拼接模版,或者替换模版关键字等,建议使用String.format(tempStr, parameter);
    例如:
	/**
     * 拼接模板
     * @param tempStr   模板
     * @param parameter 多个参数
     * @return 模板
     */
    public String getApplyContentStr(final String tempStr,final Object... parameter) {
    
    
        return  String.format(tempStr, parameter);
    }

//申请項目模板
    public static final String APPLY_XMTEMP_STR  =  "项目名称【 %s 】所属部门【 %s 】项目经理【 %s 】开发人员【 %s 】等 %d 人";

//模版拼接
	workflowVO.getApplyContentStr(ApplyFlowConstants.APPLY_XMTEMP_STR,vo.getProjectName(),vo.getDept(),vo.getManager(),arrDeveloper[0],arrDeveloper.length));
  • 不要将数组声明为public static final

因为这毫无意义,这样只是定义了引用为static
final,数组的内容还是可以随意改变的,将数组声明为public更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

  • 使用最有效率的方式去遍历Map
    遍历Map的方式有很多,通常场景下我们需要的是遍历Map中的Key和Value,那么推荐使用的、效率最高的方式是:
public static void main(String[] args)
{
    
    
    HashMap<String, String> hm = new HashMap<String, String>();
    hm.put("111", "222");
    Set<Map.Entry<String, String>> entrySet = hm.entrySet();
    Iterator<Map.Entry<String, String>> iter = entrySet.iterator();
    while (iter.hasNext())
    {
    
    
        Map.Entry<String, String> entry = iter.next();
        System.out.println(entry.getKey() + " " + entry.getValue());
    }
}

如果你只是想遍历一下这个Map的key值,那用Set<String> keySet = hm.keySet();会比较合适一些;

  • 公用的集合类中不使用的数据一定要及时remove掉

如果一个集合类是公用的(也就是说不是方法里面的属性),那么这个集合里面的元素是不会自动释放的,因为始终有引用指向它们。所以,如果公用集合里面的某些数据不使用而不去remove掉它们,那么将会造成这个公用集合不断增大,使得系统有内存泄露的隐患。

  • 把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据“+” 最慢
    遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。
  • String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断;
  • Integer.toString()方法就不说了,直接调用了;
  • i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串 三者对比下来,明显是2最快、1次之、3最慢

4、关于异常

  • 不捕获Java类库中定义的继承自RuntimeException的运行时异常类

异常处理效率低,RuntimeException的运行时异常类,其中绝大多数完全可以由程序员来规避,比如:

  • ArithmeticException可以通过判断除数是否为空来规避 NullPointerException可以通过判断对象是否为空来规避;
  • IndexOutOfBoundsException可以通过判断数组/字符串长度来规避;
  • ClassCastException可以通过instanceof关键字来规避;
  • ConcurrentModificationException可以使用迭代器来规避;
  • 慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

  • try/catch里的内容应尽可能的少

一般而言,只要try块范围越小,对java的优化机制的影响是就越小。所以保证try块范围尽量只覆盖抛出异常的地方,就可以使得异常对java优化的机制的影响最小化。

try for还是for try更影响性能?看看别人大佬写的

5、关于变量的使用

  • 变量的分类分为:局部变量、成员变量、静态变量;变量有默认的初始值,每种变量存在jvm中的位置不同;所以我们合理的利用变量,不仅可以节约内存,提升运行效率;
    成员变量&&全局变量&&局部变量的区别
  • 尽可能使用局部变量;调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中,速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外
    的垃圾回收。
  • 尽量减少对变量的重复计算;明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现
    场、调用方法完毕时恢复现场等。
    例如:
for (int i = 0; i < list.size(); i++){
    
    ...}

建议改为:

for (int i = 0, length = list.size(); i < length; ++i){
    
    ...}
//或者,提前把length = list.size();定义出来,不要每次循环都要调用一次size();
int length = list.size();
for (int i = 0, i < length; ++i){
    
    ...}
  • 在list.size()很大的时候,就减少了很多的消耗;
  • for循环中 i++ 和 ++i 的使用效果是一样的,但是for循环中 i++的耗时比++i的要长一些,for循环中 i++ 在处理时,i++实际为i = i+1,执行时先创建临时变量保存 i 值,然后再+1,而++i不需要的,没有这个过程,所以++i的性能高于i++;
  • for循环中 i++由于要创建临时变量并保存i的值,所以需要占用内存,使用完后释放内存,一个是造成资源占用,一个是数据量大时,造成程序性能低; 数据量大些时候就比较明显了;
  • 尽量采用懒加载的策略,即在需要的时候才创建
    例如:
//conten只有if里局部用到时
String conten = "内容";
if (true)
{
    
    
  list.add(conten);
}

可以改为:

//变量懒加载的策略需结合变量作用域去灵活使用
if (true)
{
    
    
  String conten = "内容";
  list.add(conten );
}  
  • 尽量避免随意使用静态变量
    当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的;
    例如:
public class A
{
    
    
    private static B b = new B();  
}

此时静态变量b的生命周期与A类相同,在GC中,如果A类不被回收,那么引用b指向的B对象会常驻内存,直到程序终止;

  • 慎重使用static静态

使用静态后,编译时会直接创建,而且直到程序结束,一般只会用在常量,公共方法上,因为需要保证随时随地使用,基于这一需求,它不太使用于对象的创建上,会浪费内存;

  • 将常量声明为static final,并以大写命名
	/**
     * 反射解析设置值异常
     */
    public static final String ZSL_REFLEX_STR  = "反射解析设置值异常";

关于jvm常量池

  • 这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值,我们知道,常量是存在常量池中的,不需要再次创建,;
  • 将常量的名字以大写命名也可以方便区分出常量与变量,加上static final修饰,保证使用的速度和不被外界力量所改变;
  • 不要使用魔法值

切记以常量定义的方式替代魔鬼数字,魔鬼数字的存在将极大地降低代码可读性,字符串常量是否使用常量定义可以视情况而定,-1、0 和 1
不被视为魔法值。

  • 比较时把常量写前面

6、关于对象、方法、类的使用

  • 尽量指定类、方法的final修饰符
    例如:
//一些常量类里可以使用
public final class ApplyFlowConstants {
    
    ...}

带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大。

  • 不想传入参被修改,建议使用final修饰符修饰入参
    例如:
    /**
     * 拼接模板
     * @param tempStr   模板
     * @param parameter 多个参数
     * @return 模板
     */
    public String getApplyContentStr(final String tempStr,final Object... parameter) {
    
    
        return  String.format(tempStr, parameter);
    }
  • 在给方法传参时,不想在该方法内对参数进行修改,那么方法的设计者可以使用final修饰符修饰入参;
  • 方法参数前面加final关键字是为了防止传入数据在方法体中被修改;
  • 在参数进行final修饰时,如果是基本类型,那么在方法体内更改数值会报错; 如果是引用类型,修改内容不会报错,但是修改地址会报错;
  • 其中String比较特别,修改String内容时也会更改其地址,所以修改String内容时也会报错;
  • 尽量重用对象
  • 由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。
  • 当某一时刻,你的生成了大量的对象,占用了大量的堆内存且GC无法及时对这些大量的对象进回收,导致内存泄漏memory leak堆积,就会产生了内存溢出out of memory;
  • 要合理生成堆内存,尽量重用对象;
  • 不要创建一些不使用的对象,不要导入一些不使用的类

毫无意义,如果代码中出现The value of the local variable i is not usedThe import java.util is never used,那么请删除这些无用的内容,不仅没用到,还增大的代码的体积;

  • 不要让public方法中有太多的形参
    例如:
   /**
     * 上報工作流
     * @param applyVo 申请表
     * @param strId 申请表主键
     * @param processId 申请表工作流ID
     * @param objUserTask IUserTaskServiceClient 工作流接口
     * @throws AbstractBpmsException BPMS异常
     */
    public void reportWorkFlow(T applyVo, String strId, String processId, IUserTaskServiceClient objUserTask){
    
    ...};
	//调用
	workflowVO.reportWorkFlow(projectApplyVO, projectApplyVO.getProjectId(),projectApplyAppService.getProcessId(),objUserTask);

可以改为:

	public void reportWorkFlow(Map<T> tMap){
    
    ...};
	//调用
	String processId = projectApplyAppService.getProcessId()Map<T> tMap = new HashMap<>();
	tMap.put("tVo",applyVo);
	tMap.put("processId",processId);
	tMap.put("task",objUserTask);
	workflowVO.reportWorkFlow(tMap);
	
	//或者
	public void reportWorkFlow(T applyVo){
    
    ...};
	//调用
	applyVo.setProcessId(processId);
	applyVo.setObjUserTask(objUserTask);
	workflowVO.reportWorkFlow(applyVo);

public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:

  • 违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合;
  • 参数太多,可能会由于传错参数,导致方法调用的出错概率增加;
  • 静态类、单例类、工厂类将它们的构造函数置为private

这是因为静态类、单例类、工厂类这种类本来我们就不需要外部将它们new出来,将构造函数置为private之后,保证了这些类不会产生实例对象。

  • 尽量在合适的场合使用单例
  • 使用单例可以减轻加载的负担、缩短加载的时间、提高加载的效率;
  • 并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:
    • 控制资源的使用,通过线程同步来控制资源的并发访问;
    • 控制实例的产生,以达到节约资源的目的;
    • 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信
  • 采用单例模式时,注意线程安全
  • 程序运行过程中避免使用反射
  • 不建议在程序运行过程中使用尤其是频繁使用反射机制,特别是Method的invoke方法;
  • 如果确实有必要,一种建议性的做法是:将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存; 因为用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。
  • 所有重写的方法必须保留@Override注解

这么做有三个原因:

  • 清楚地可以知道这个方法由父类继承而来
  • 加了@Override注解可以马上判断是否重写成功,对加了@Override注解无论是实现类还是它对应的抽象类,错误的命名马上报出编译错误
  • 对象比较时,使用Objects工具类来进行对象的equals比较,直接a.equals(b),有空指针异常
    的风险

    例如:
		String s1 = null;
		String s2 = "abc";
		boolean b = s1.equals(s2);
		System.out.println(b);
		//java.lang.NullPointerException null是不能调用方法的,会抛出异常

改为:

		String s1 = null;
		String s2 = "abc";
		boolean b2 = Objects.equals(s1, s2);
		System.out.println(b2);//false
  • long或者Long初始赋值时,使用大写的L而不是小写的l,因为字母l极易与数字1混淆,这个点非常细节,值得注意
  • 乘法和除法使用移位操作
    例如:
for (val = 0; val < 100000; val += 5)
{
    
    
  a = val * 8;
  b = val / 2;
}

改为:

for (val = 0; val < 100000; val += 5)
{
    
    
  a = val << 3;
  b = val >> 1;
}

用移位操作可以极大地提高性能,因为在计算机底层,对位的操作是最方便、最快的,移位操作虽然快,但是可能会使代码不太好理解,因此最好加上相应的注释。

  • 不要对超出范围的基本数据类型做向下强制转型
    例如:
public static void main(String[] args)
{
    
    
    long l = 12345678901234L;
    int i = (int)l;
    System.out.println(i);
}
//我们可能期望得到其中的某几位,但是结果却是:1942892530

Java中long是8个字节64位的,所以12345678901234在计算机中的表示应该是:
0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010
一个int型数据是4个字节32位的,从低位取出上面这串二进制数据的前32位是:
0111 0011 1100 1110 0010 1111 1111 0010
这串二进制表示为十进制1942892530,所以就是我们上面的控制台上输出的内容。从这个例子上还能顺便得到两个结论:

  • 整型默认的数据类型是int,long l = 12345678901234L,这个数字已经超出了int的范围了,所以最后有一个L,表示这是一个long型数。顺便,浮点型的默认类型是double,所以定义float的时候要写成float
    f =3.5f
  • 接下来再写一句int ii = l + i;会报错,因为long + int是一个long,不能赋值给int;

7、关于多线程和流

  • 及时清除不再需要的会话

为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多的会话时,如果内存不足,那么操作系统会把部分数据转移到磁盘,应用服务器也可能根据MRU(最近最频繁使用)算法把部分不活跃的会话转储到磁盘,甚至可能抛出内存不足的异常。如果会话要被转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很昂贵的。因此,当会话不再需要时,应当及时调用HttpSession的invalidate()方法清除会话。

  • 使用同步代码块替代同步方法
  • 使用池
    例如:使用数据库连接池和线程池,这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程
  • 使用带缓冲的输入输出流进行IO操作
    带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、
    BufferedOutputStream,这可以极大地提升IO效率
  • 关闭流
    在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。使用try/catch/finally最终无论如何都关闭流。
    例如:
try{
    
    
   ...
}catch (Exception e){
    
    
    ...
}finally{
    
    
 XXX.close();
}
  • 避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降,JDK7之后,可以使用ThreadLocalRandom来获取随机数

  • 对于ThreadLocal使用前或者使用后一定要先remove
    当前基本所有的项目都使用了线程池技术,这非常好,可以动态配置线程数、可以重用线程。
    然而,如果你在项目中使用到了ThreadLocal,一定要记得使用前或者使用后remove一下。这是因为上面提到了线程池技术做的是一个线程重用,这意味着代码运行过程中,一条线程使用完毕,并不会被销毁而是等待下一次的使用。我们看一下Thread类中,持有ThreadLocal.ThreadLocalMap的引用:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

线程不销毁意味着上条线程set的ThreadLocal.ThreadLocalMap中的数据依然存在,那么在下一条线程重用这个Thread的时候,很可能get到的是上条线程set的数据而不是自己想要的内容。
这个问题非常隐晦,一旦出现这个原因导致的错误,没有相关经验或者没有扎实的基础非常难发现这个问题,因此在写代码的时候就要注意这一点,这将给你后续减少很多的工作量。

  • SimpleDateFormat线程不安全
    使用java8的DateTimeFormatter类。
  • 少用Executors创建线程池

猜你喜欢

转载自blog.csdn.net/qq_37432174/article/details/130027153