编写高质量代码:改善Java程序的151个建议(数组和集合)

集合中的元素必须做到compareTo和equals同步

实现了Comparable接口的元素就可以排序,compareTo方法是Comparable接口要求必须实现的,它与equals方法有关系吗?有关系,在compareTo的返回为0时,它表示的是 进行比较的两个元素时相等的。equals是不是也应该对此作出相应的动作呢?我们看如下代码:

class City implements Comparable<City> {
     private String code;
 
     private String name;
 
     public City(String _code, String _name) {
         code = _code;
         name = _name;
     }
     //code、name的setter和getter方法略
     @Override
     public int compareTo(City o) {
         //按照城市名称排序
         return new CompareToBuilder().append(name, o.name).toComparison();
     }
 
     @Override
     public boolean equals(Object obj) {
         if (null == obj) {
             return false;
         }
         if (this == obj) {
             return true;
         }
         if (obj.getClass() == getClass()) {
             return false;
         }
         City city = (City) obj;
         // 根据code判断是否相等
         return new EqualsBuilder().append(code, city.code).isEquals();
     }
 
 }

我们把多个城市对象放在一个list中,然后使用不同的方法查找同一个城市,看看返回值有神么异常?代码如下:

 public static void main(String[] args) {
         List<City> cities = new ArrayList<City>();
         cities.add(new City("021", "上海"));
         cities.add(new City("021", "沪"));
         // 排序
         Collections.sort(cities);
         // 查找对象
         City city = new City("021", "沪");
         // indexOf方法取得索引值
         int index1 = cities.indexOf(city);
         // binarySearch查找索引值
         int index2 = Collections.binarySearch(cities, city);
         System.out.println(" 索引值(indexOf) :" + index1);
         System.out.println(" 索引值(binarySearch) :" + index2);
     }

输出的index1和index2应该一致吧,都是从一个列表中查找相同的元素,只是使用的算法不同嘛。但是很遗憾,结果不一致:

  

  indexOf返回的是第一个元素,而binarySearch返回的是第二个元素(索引值为1),这是怎么回事呢?

  这是因为indexOf是通过equals方法判断的,equals方法等于true就认为找到符合条件的元素了,而binarySearch查找的依据是compareTo方法的返回值,返回0即认为找到符合条件的元素了。

仔细审查一下代码,我们覆写了compareTo和equals方法,但是两者并不一致。使用indexOf方法查找时 ,遍历每个元素,然后比较equals方法的返回值,因为equals方法是根据code判断的,因此当第一次循环时 ,equals就返回true,indexOf方法结束,查找到指定值。而使用binarySearch二分法查找时,依据的是每个元素的compareTo方法返回值,而compareTo方法又是依赖属性的,name相等就返回0,binarySearch就认为找到元素了。

  问题明白了,修改很easy,将equals方法修改成判断name是否相等即可,虽然可以解决问题,但这是一个很无奈的办法,而且还要依赖我们的系统是否支持此类修改,因为逻辑已经发生了很大的变化,从这个例子,我们可以理解两点:

  • indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找;
  • equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同。

  既然一个决定排序位置,一个是决定相等,那我们就应该保证当排序相同时,其equals也相同,否则就会产生逻辑混乱。

  注意:实现了compareTo方法就应该覆写equals方法,确保两者同步。

建议76:集合运算时使用最优雅方式

  在初中代数中,我们经常会求两个集合的并集、交集、差集等,在Java中也存在着此类运算,那如何实现呢?一提到此类集合操作,大部分的实现者都会说:对两个集合进行遍历,即可求出结果。是的。遍历可以实现并集、交集、差集等运算,但这不是最优雅的处理方式,下面来看看如何进行更优雅、快速、方便的集合操作:

  (1)、并集:也叫作合集,把两个集合加起来即可,这非常简单,代码如下:   

 public static void main(String[] args) {
         List<String> list1 = new ArrayList<String>();
         list1.add("A");
         list1.add("B");
         List<String> list2 = new ArrayList<String>();
         list2.add("C");
         // 并集
         list1.addAll(list2);
     }

 (2)、交集:计算两个集合的共有元素,也就是你有我也有的元素集合,代码如下:

//交集
list1.retainAll(list2);

其中的变量list1和list2是两个列表,仅此一行,list1中就只包含了list1、list2中共有的元素了,注意retailAll方法会删除list1中没有出现在list2中的元素。

  (3)、差集:由所有属于A但不属于B的元素组成的集合,叫做A与B的差集,也就是我有你没有的元素,代码如下:

//差集
list1.removeAll(list2);

 也很简单,从list1中删除出现在list2中的元素,即可得出list1和list2的差集部分。

  (4)、无重复的并集:并集是集合A加集合B,那如果集合A和集合B有交集,就需要确保并集的结果中只有一份交集,此为无重复的并集,此操作也比较简单,代码如下:

        //删除在list1中出现的元素
        list2.removeAll(list1);
        //把剩余的list2元素加到list1中
        list1.addAll(list2);

 可能有人会说,求出两个集合的并集,然后转成hashSet剔除重复元素不就解决了吗?错了,这样解决是不行的,比如集合A有10个元素(其中有两个元素值是相同的),集合B有8个元素,它们的交集有两个元素,我们可以计算出它们的并集是18个元素,而无重复的并集有16个元素,但是如果用hashSet算法,算出来则只有15个元素,因为你把集合A中原本就重复的元素也剔除了。

  之所以介绍并集、交集、差集,那是因为在实际开发中,很少有使用JDK提供的方法实现集合这些操作,基本上都是采用了标准的嵌套for循环:要并集就是加法,要交集就是contains判断是否存在,要差集就使用了!contains(不包含),有时候还要为这类操作提供了一个单独的方法看似很规范,其实应经脱离了优雅的味道。

 集合的这些操作在持久层中使用的非常频繁,从数据库中取出的就是多个数据集合,之后我们就可以使用集合的各种方法构建我们需要的数据,需要两个集合的and结果,那是交集,需要两个集合的or结果,那是并集,需要两个集合的not结果,那是差集。

建议77:使用shuffle打乱列表

  在网站上,我们经常会看到关键字云(word cloud)和标签云(tag cloud),用于表达这个关键字或标签是经常被查阅的,而且还可以看到这些标签的动态运动,每次刷新都会有不一样的关键字或标签,让浏览者觉得这个网站的访问量很大,短短的几分钟就有这么多的搜索量。不过,在Java中该如何实现呢?代码如下:   

 public static void main(String[] args) {
         int tagCloudNum = 10;
         List<String> tagClouds = new ArrayList<String>(tagCloudNum);
         // 初始化标签云,一般是从数据库读入,省略
         Random rand = new Random();
         for (int i = 0; i < tagCloudNum; i++) {
             // 取得随机位置
             int randomPosition = rand.nextInt(tagCloudNum);
             // 当前元素与随机元素交换
             String temp = tagClouds.get(i);
             tagClouds.set(i, tagClouds.get(randomPosition));
             tagClouds.set(randomPosition, temp);
         }
     }

       现从数据库中读取标签,然后使用随机数打乱,每次产生不同的顺序,嗯,确实能让浏览者感觉到我们的标签云顺序在变化---浏览者多嘛!但是,对于乱序处理我们可以有更好的实现方式,先来修改第一版:

public static void main(String[] args) {
         int tagCloudNum = 10;
         List<String> tagClouds = new ArrayList<String>(tagCloudNum);
         // 初始化标签云,一般是从数据库读入,省略
         Random rand = new Random();
         for (int i = 0; i < tagCloudNum; i++) {
             // 取得随机位置
             int randomPosition = rand.nextInt(tagCloudNum);
             // 当前元素与随机元素交换
             Collections.swap(tagClouds, i, randomPosition);
         }
     }

上面使用了Collections的swap方法,该方法会交换两个位置的元素值,不用我们自己写交换代码了。难道乱序到此就优化完了吗?没有,我们可以继续重构,第二版如下:

  public static void main(String[] args) {
         int tagCloudNum = 10;
         List<String> tagClouds = new ArrayList<String>(tagCloudNum);
         // 初始化标签云,一般是从数据库读入,省略
         //打乱顺序
         Collections.shuffle(tagClouds);
     }

 这才是我们想要的结果,就这一行,即可打乱一个列表的顺序,我们不用费尽心思的遍历、替换元素了。我们一般很少用到shuffle这个方法,那它在什么地方用呢?

  • 可用在程序的 "伪装" 上:比如我们例子中的标签云,或者是游侠中的打怪、修行、群殴时宝物的分配策略。
  • 可用在抽奖程序中:比如年会的抽奖程序,先使用shuffle把员工顺序打乱,每个员工的中奖几率相等,然后就可以抽出第一名、第二名。
  • 可以用在安全传输方面:比如发送端发送一组数据,先随机打乱顺序,然后加密发送,接收端解密,然后进行排序,即可实现即使是相同的数据源,也会产生不同密文的效果,加强了数据的安全性。

建议78:减少HashMap中元素的数量 

  本建议是说HahMap中存放数据过多的话会出现内存溢出,代码如下:  

public static void main(String[] args) {
         Map<String, String> map = new HashMap<String, String>();
         List<String> list = new ArrayList<String>();
         final Runtime rt = Runtime.getRuntime();
         // JVM中止前记录信息
         rt.addShutdownHook(new Thread() {
             @Override
             public void run() {
                 StringBuffer sb = new StringBuffer();
                 long heapMaxSize = rt.maxMemory() >> 20;
                 sb.append(" 最大可用内存:" + heapMaxSize + " M\n");
                 long total = rt.totalMemory() >> 20;
                 sb.append(" 堆内存大小:" + total + "M\n");
                 long free = rt.freeMemory() >> 20;
                 sb.append(" 空闲内存:" + free + "M");
                 System.out.println(sb);
             }
         });
         for (int i = 0; i < 40*10000; i++) {
             map.put("key" + i, "value" + i);
 //            list.add("list"+i);
         }
     }  

 这个例子,我经过多次运算,发现在40万的数据并不会内存溢出,如果要复现此问题,需要修改Eclipse的内存配置,才会复现。但现在的机器的内存逐渐的增大,硬件配置的提高,应该可以容纳更多的数据。本人机器是windows64,内存8G配置,Eclipse的配置为   -Xms286M    -Xmx1024M,在单独运行此程序时,数据量加到千万级别才会复现出此问题。但在生产环境中,如果放的是复杂对象,可能同样配置的机器存放的数据量会小一些。

  但如果换成list存放,则同样的配置存放的数据比HashMap要多一些,本人就针对此现象进行分析一下几点:

1.HashMap和ArrayList的长度都是动态增加的,不过两者的扩容机制不同,先说HashMap,它在底层是以数组的方式保存元素的,其中每一个键值对就是一个元素,也就是说HashMap把键值对封装成了一个Entry对象,然后再把Entry对象放到了数组中。也就是说HashMap比ArrayList多了一次封装,多出了一倍的对象。其中HashMap的扩容机制代码如下(resize(2 * table.length)这就是扩容核心代码):

void addEntry(int hash, K key, V value, int bucketIndex) {
         if ((size >= threshold) && (null != table[bucketIndex])) {
             resize(2 * table.length);
             hash = (null != key) ? hash(key) : 0;
             bucketIndex = indexFor(hash, table.length);
         }
 
         createEntry(hash, key, value, bucketIndex);
     }

在插入键值对时会做长度校验,如果大于或者等于阈值,则数组长度会增大一倍。

 void resize(int newCapacity) {
         Entry[] oldTable = table;
         int oldCapacity = oldTable.length;
         if (oldCapacity == MAXIMUM_CAPACITY) {
             threshold = Integer.MAX_VALUE;
             return;
         }
 
         Entry[] newTable = new Entry[newCapacity];
         boolean oldAltHashing = useAltHashing;
         useAltHashing |= sun.misc.VM.isBooted() &&
                 (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
         boolean rehash = oldAltHashing ^ useAltHashing;
         transfer(newTable, rehash);
         table = newTable;
         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
     }

而阈值就是代码中红色标注的部分,新容量*加权因子和MAXIMUM_CAPACITY + 1两个值的最小值。MAXIMUM_CAPACITY的值如下: 

static final int MAXIMUM_CAPACITY = 1 << 30;

而加权因子的值为0.75,代码如下:

static final float DEFAULT_LOAD_FACTOR = 0.75f;

所以hashMap的size大于数组的0.75倍时,就开始扩容,经过计算得知(怎么计算的,以文中例子来说,查找2的N次幂大于40万的最小值即为数组的最大长度,再乘以0.75,也就是最后一次扩容点,计算的结果是N=19),在Map的size为393216时,符合了扩容条件,于是393216个元素开始搬家,要扩容则需要申请一个长度为1048576(当前长度的两倍,2的20次方)的数组,如果此时内存不足以支撑此运算,就会爆出内存溢出。这个就是这个问题的根本原因。

 2、我们思考一下ArrayList的扩容策略,它是在小于数组长度的时候才会扩容1.5倍,经过计算得知,ArrayLsit在超过80万后(一次加两个元素,40万的两倍),最近的一次扩容是在size为1005305时同样的道理,如果此时内存不足以申请扩容1.5倍时的数组,也会出现内存溢出。

 综合来说,HashMap比ArrayList多了一层Entry的底层封装对象,多占用了内存,并且它的扩容策略是2倍长度的递增,同时还会根据阈值判断规则进行判断,因此相对于ArrayList来说,同样的数据,它就会优先内存溢出。

 也许大家会想到,可以在声明时指定HashMap的默认长度和加载因子来减少此问题的发生,可以缓解此问题,可以不再频繁的进行数组扩容,但仍避免不了内存溢出问题,因为键值对的封装对象Entry还是少不了的,内存依然增长比较快,所以尽量让HashMap中的元素少量并简单一点。也可以根据需求以及系统的配置来计算出,自己放入map中的数据会不会造成内存溢出呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/13496.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

某医院网络安全分析案例

背景 我们已将NetInside流量分析系统部署到某市医院的机房内&#xff0c;使用流量分析系统提供实时和历史原始流量。本次分析重点针对网络流量安全进行分析&#xff0c;以供安全取证、网络质量监测以及深层网络分析。 分析时间 报告分析时间范围为&#xff1a;2023-04-12 16…

Cloud Kernel SIG月度动态:发布 Anolis 8.8 镜像、kABI 社区共建流程

Cloud Kernel SIG&#xff08;Special Interest Group&#xff09;&#xff1a;支撑龙蜥内核版本的研发、发布和服务&#xff0c;提供生产可用的高性价比内核产品。 01 SIG 整体进展 Anolis 8.8 镜像发布&#xff0c;默认搭载 ANCK 5.10-013 版本。 Anolis 23 滚动内核更新至…

ai智能文章生成器-ai论文写作

在数字时代&#xff0c;营销推广策略已经向数字化方向发展。今天我们要介绍的是一款名为“智能ai写作免费”的软件&#xff0c;它可以让营销人员轻松地创作新的内容&#xff0c;并且其中不需要过多的技术知识或文学背景。这款软件可以为许多企业和机构带来创造性的帮助。 智能A…

@Async异步线程:Spring 自带的异步解决方案

前言 在项目应用中&#xff0c;使用MQ异步调用来实现系统性能优化&#xff0c;完成服务间数据同步是常用的技术手段。如果是在同一台服务器内部&#xff0c;不涉及到分布式系统&#xff0c;单纯的想实现部分业务的异步执行&#xff0c;这里介绍一个更简单的异步方法调用。 对于…

电脑端(PC)按键精灵——5.找色/找图命令

电脑端(PC)按键精灵——5.找色/找图命令 注&#xff1a;说了键盘、鼠标、其他、控制命令还有安装内容&#xff0c;现在说下颜色/图形命令&#xff0c;这一节相当重要 按键精灵小白入门详细教程&#xff1a; 电脑端(PC)按键精灵—小白入门 详细教程 命令介绍 1.GetPixelCol…

【C++类】

目录 前言一、类的定义二、类的访问限定符及封装2.1访问限定符2.2封装 三、类的大小3.1为什么需要内存对齐3.2为什么成员函数不占用类的内存&#xff1f;3.3为什么空类的大小是1个字节&#xff1f; 四、this指针4.1this指针的引入4.2this指针的特性 五、类的6个默认成员函数5.1…

飞书接入ChatGPT - 将ChatGPT集成到飞书机器人,直接拉满效率 【飞书ChatGPT机器人】

文章目录 前言环境列表视频教程1.飞书设置2.克隆feishu-chatgpt项目3.配置config.yaml文件4.运行feishu-chatgpt项目5.安装cpolar内网穿透6.固定公网地址7.机器人权限配置8.创建版本9.创建测试企业10. 机器人测试 前言 在飞书中创建chatGPT机器人并且对话,在下面操作步骤中,使…

6.4 一阶方程组与高阶方程的数值解法

学习目标&#xff1a; 学习一阶方程组与高阶方程的数值解法的目标可以分为以下几个方面&#xff1a; 掌握一阶方程组和高阶方程的基本概念和求解方法&#xff1b;理解数值解法的概念和原理&#xff0c;了解常见的数值解法&#xff1b;掌握欧拉方法、改进欧拉方法和龙格-库塔方…

深入探讨Linux驱动开发:Linux设备树

文章目录 一、设备树介绍二、设备树框架1.设备树框架2.节点基本格式3.节点部分属性简介 总结 一、设备树介绍 设备树&#xff08;Device Tree&#xff0c;简称 DT&#xff09;是一种在嵌入式系统中描述硬件设备的一种数据结构和编程语言。它用于将硬件设备的配置信息以树形结构…

Springboot 中快速完成文件上传,整合多平台神器

哈喽&#xff0c;大家好~ 又是做好人好事的一天&#xff0c;有个小可爱私下问我有没有好用的springboot文件上传工具&#xff0c;这不巧了嘛&#xff0c;正好我私藏了一个好东西&#xff0c;顺便给小伙伴们也分享一下&#xff0c;demo地址放在文末了。 文件上传在平常不过的一…

最新,有8本SCIE期刊被剔除,4月SCIESSCI期刊目录更新(附最新目录下载)

2023年4月18日&#xff0c;科睿唯安更新了WOS期刊目录&#xff0c;继上次3月WOS期刊目录更新大变动之后&#xff0c;此次4月更新又有8本SCIE期刊发生变动&#xff0c;其中有4本期刊被剔出SCIE数据库&#xff0c;4本期刊更改了名称和ISSN号。更新后的最新SCIE期刊目录共有9505本…

Flask 与 Django 先学哪个呢

本文把 Flask 和 Django 做一个比对&#xff0c;因为我对这两个 Python Web 框架都有实际的开发经验。希望我可以帮助您选择学习哪个框架&#xff0c;因为学习一个框架可能会非常耗时 —— 当然也很有趣&#xff01; 相似之处 让我们从相似之处开始。 No. 1 Flask 和 Djang…

【ctfshow】命令执行->web29-web44

前言 半夜网抑云听歌听emo了 z 刷会儿题不然睡不着了呜呜呜 红中(hong_zh0) CSDN内容合伙人、2023年新星计划web安全方向导师、 华为MindSpore截至目前最年轻的优秀开发者、IK&N战队队长、 吉林师范大学网安大一的一名普通学生、搞网安论文拿了回大挑校二、 阿里云专家博…

数据结构复习题(包含答案)

第一章 概论 一、选择题 1、研究数据结构就是研究&#xff08; D &#xff09;。 A. 数据的逻辑结构 B. 数据的存储结构 C. 数据的逻辑结构和存储结构 D. 数据的逻辑结构、存储结构及其基本操作 2、算法分析的两个主要方面是&#xff08; A …

【小技巧】word文档编辑技巧(一)

文章目录 一、显示显示导航显示所有字符 二、格式格式-三级目录格式-文本格式-图格式-表格式-公式格式-参考文献 三、小技巧交叉引用连续交叉引用表/图目录等自动更新分节符设置页眉/页码word转pdf带导航 一、显示 显示导航 开启导航&#xff1a;视图->显示框->导航窗格…

【Python从入门到进阶】16、文件的打开和关闭

接上篇《15、函数的定义和使用》 上一篇我们学习了Python中函数的定义和使用&#xff0c;包括函数的参数、返回值、局部变量和全景变量等操作。从本篇开始我们来开始学习Python对文件的一些操作&#xff0c;本篇我们主要讲解如何使用Python打开和关闭文件。 一、打开/创建文件…

【SVN】windows SVN安装使用教程(服务器4.3.4版本/客户端1.11.0版本)

介绍 这里是小编成长之路的历程&#xff0c;也是小编的学习之路。希望和各位大佬们一起成长&#xff01; 以下为小编最喜欢的两句话&#xff1a; 要有最朴素的生活和最遥远的梦想&#xff0c;即使明天天寒地冻&#xff0c;山高水远&#xff0c;路远马亡。 一个人为什么要努力&a…

【设计模式】Java 的三种代理模式

文章目录 一、前言二、正文1、静态代理2、动态代理3、Cglib代理Spring中AOP使用代理 三、总结 一、前言 代理(Proxy)模式是一种结构型设计模式&#xff0c;提供了对目标对象另外的访问方式&#xff1b;即通过代理对象访问目标对象。 这样做的好处是&#xff1a;可以在目标对…

activeMQ持久化报错的问题

activeMQ持久化&#xff0c;启动activeMQ报错&#xff0c; INFO | Using Persistence Adapter: JDBCPersistenceAdapter(org.apache.commons.dbcp2.BasicDataSource5148e82a) jvm 1 | WARN | Could not get JDBC connection: Cannot create PoolableConnectionFactory (Commun…

前端学习:HTML头部、布局

目录 HTML头部 一、HTML 元素 二、head标签和header标签的不同 三、HTML 元素 四、HTML 元素 五、HTML 元素 六、 HTML 七、HTML元素 为搜索引擎定义关键词&#xff1a; 为网页定义描述内容&#xff1a; 每60秒刷新当前页面&#xff1a; 八、HTML 九、HTML头部元素…