技术-202107-《阿里巴巴灵魂13问》

三目运算符的空指针问题到底是个怎么回事?

最好的做法就是保持三目运算符的第二位和第三位表达式的类型一致,并且如果  要把三目运算符表达式给变量赋值的时候,也尽量保持变量的类型和他们保持一致。  并且,做好单元测试!!!

所以,Java 开发手册中提到要高度注意第二位和第三位表达式的类型对齐过程  中由于自动拆箱发生的 NPE 问题,其实还需要注意使用三目运算符表达式给变量赋  值的时候由于自动拆箱导致的 NPE 问题。

建议初始化 HashMap 的容量大小?

从结果中,我们可以知道,在已知 HashMap 中将要存放的 KV 个数的时候,  设置一个合理的初始化容量可以有效的提高性能。 当然,以上结论也是有理论支撑的。我们在上文介绍过,HashMap 有扩容机  制,就是当达到扩容条件时会进行扩容。HashMap 的扩容条件就是当 HashMap  中的元素个数(size)超过临界值(threshold)时就会自动扩容。在 HashMap 中, threshold = loadFactor * capacity。  所以,如果我们没有设置初始容量大小,随着元素的不断增加,HashMap 会发  生多次扩容,而 HashMap 中的扩容机制决定了每次扩容都需要重建 hash 表,是非  常影响性能的。  从上面的代码示例中,我们还发现,同样是设置初始化容量,设置的数值不同也  会影响性能,那么当我们已知 HashMap 中即将存放的 KV 个数的时候,容量设置成  多少为好呢?

总结

当我们想要在代码中创建一个 HashMap 的时候,如果我们已知这个 Map 中即  将存放的元素个数,给 HashMap 设置初始容量可以在一定程度上提升效率。  但是,JDK 并不会直接拿用户传进来的数字当做默认容量,而是会进行一番运  算,最终得到一个 2 的幂。原因在(全网把 Map 中的 hash() 分析的最透彻的文章,  别无二家。)介绍过,得到这个数字的算法其实是使用了使用无符号右移和按位或运  算来提升效率。

建议创建 HashMap 时设置初始化容量,但是多少合适呢?

JDK 会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是  找到第一个比用户传入的值大的 2 的幂。

但是,这个值看似合理,实际上并不尽然。因为 HashMap 在根据用户传入的  capacity 计算得到的默认容量,并没有考虑到 loadFactor 这个因素,只是简单机  械的计算出第一个大约这个数字的 2 的幂

所以,我们可以认为,当我们明确知道 HashMap 中元素的个数的时候,把默  认容量设置成 expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但  是,同时也会牺牲些内存。

禁止使用 Executors 创建线程池?

可以通过 Executors 静态工厂构建线程池,但一般不建议这样使用。

通过上面的例子,我们知道了Executors创建的线程池存在 OOM 的风险,那  么到底是什么原因导致的呢?我们需要深入Executors的源码来分析一下。  其实,在上面的报错信息中,我们是可以看出蛛丝马迹的,在以上的代码中其实  已经说了,真正的导致 OOM 的其实是LinkedBlockingQueue.offer方法。

这里的问题就出在:不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE。也就是说,如果我们不设置LinkedBlockingQueue的  容量的话,其默认容量将会是Integer.MAX_VALUE

创建线程池的正确姿势

避免使用 Executors 创建线程池,主要是避免使用其中的默认实现,那么我们  可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的  同时,给BlockQueue指定容量就可以了。

这种情况下,一旦提交的线程数超过当前可用线程数时,就会抛出java.util.  concurrent.RejectedExecutionException,这是因为当前线程池使用的队列  是有边界队列,队列已经满了便无法继续处理新的请求。但是异常(Exception)总比  发生错误(Error)要好。

不建议在 for 循环中使用“+”进行字符串拼接?

String 是 Java 中一个不可变的类,所以他一旦被实例化就无法  被修改。

不可变类的实例一旦创建,其成员变量的值就不能被修改。这样设计有很多好  处,比如可以缓存 hashcode、使用更加便利以及更加安全等。

这里要特别说明一点,有人把 Java 中使用+拼接字符串的功能理解为运算符重  载。其实并不是,Java 是不支持运算符重载的。这其实只是 Java 提供的一个语法  糖。后面再详细介绍。 运算符重载:在计算机程序设计中,运算符重载(英语:operator overloading)是多态的一种。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种  功能,以适应不同的数据类型。  语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学  家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的  功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

Concat

StringBuffer

关于字符串,Java 中除了定义了一个可以用来定义字符串常量的String类以  外,还提供了可以用来定义字符串变量的StringBuffer类,它的对象是可以扩充  和修改的。

StringBuilder

StringUtils.join

总结 本文介绍了什么是字符串拼接,虽然字符串是不可变的,但是还是可以通过新建  字符串的方式来进行字符串的拼接。  常用的字符串拼接方式有五种,分别是使用+、使用concat、使用StringBuilder、使用StringBuffer以及使用StringUtils.join。  由于字符串拼接过程中会创建新的对象,所以如果要在一个循环体中进行字符串  拼接,就要考虑内存问题和效率问题。  因此,经过对比,我们发现,直接使用StringBuilder的方式是效率最高的。  因为StringBuilder天生就是设计来定义可变字符串和字符串的变化操作的。  但是,还要强调的是:  1. 如果不是在循环体中进行字符串拼接的话,直接使用+就好了。  2. 如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替 StringBuilder。

禁止在 foreach 循环里进行元素的 remove/add 操作?

可以看到,它只修改了 modCount,并没有对 expectedModCount 做任何  操作。  简单总结一下,之所以会抛出 ConcurrentModificationException 异常,是因  为我们的代码中使用了增强 for 循环,而在增强 for 循环中,集合遍历是通过 iterator  进行的,但是元素的 add/remove 却是直接使用的集合类自己的方法。这就导致  iterator 在遍历的时候,会发现有一个元素在自己不知不觉的情况下就被删除 / 添加  了,就会抛出一个异常,用来提示用户,可能发生了并发修改!

总结

我们使用的增强 for 循环,其实是 Java 提供的语法糖,其实现原理是借助  Iterator 进行元素的遍历。  但是如果在遍历过程中,不通过 Iterator,而是通过集合类自身的方法对集合进  行添加 / 删除操作。那么在 Iterator 进行下一次的遍历时,经检测发现有一次集合的  修改操作并未通过自身进行,那么可能是发生了并发被其他线程执行的,这时候就会  抛出异常,来提示用户可能发生了并发修改,这就是所谓的 fail-fast 机制

当然还是有很多种方法可以解决这类问题的。比如使用普通 for 循环、使用  Iterator 进行元素删除、使用 Stream 的 filter、使用 fail-safe 的类等。

禁止工程师直接使用日志系统 (Log4j、Logback) 中的 API ?

常用日志框架

j.u.l java.util.logging,Log4j,LogBack,Log4j2,SLF4Jcommons-logging

为什么需要日志门面 前面提到过一个重要的原因,就是为了在应用中屏蔽掉底层日志框架的具体实  现。这样的话,即使有一天要更换代码的日志框架,只需要修改 jar 包,最多再改改  日志输出相关的配置文件就可以了。这就是解除了应用和日志框架之间的耦合。

同理,对于一个设计的全面、完善的日志门面来说,他也应该是天然就兼容了多  种日志框架的。所以,底层框架的更换,日志门面几乎不需要改动。  以上,就是日志门面的一个比较重要的好处——解耦

常用日志门面:SLF4J

总结 在 Java 生态体系中,围绕着日志,有很多成熟的解决方案。关于日志输出,主  要有两类工具。  一类是日志框架,主要用来进行日志的输出的,比如输出到哪个文件,日志格式  如何等。 另外一类是日志门面,主要一套通用的 API,用来屏蔽各个日志框架之间的  差异的。  所以,对于 Java 工程师来说,关于日志工具的使用,最佳实践就是在应用中使  用如 Log4j + SLF4J 这样的组合来进行日志输出。  这样做的最大好处,就是业务层的开发不需要关心底层日志框架的实现及细节,  在编码的时候也不需要考虑日后更换框架所带来的成本。这也是门面模式所带来的  好处。  综上,请不要在你的 Java 代码中出现任何 Log4j 等日志框架的 API 的使用,而  是应该直接使用 SLF4J 这种日志门面

禁止把 SimpleDateFormat 定义成 static 变量?

SimpleDateFormat 中的 format 方法在执行过程中,会使用一个成员变量  calendar 来保存时间。这其实就是问题的关键。  由于我们在声明 SimpleDateFormat 的时候,使用的是 static 定义的。那么  这 个 SimpleDateFormat 就 是 一 个 共 享 变 量, 随 之,SimpleDateFormat 中 的  calendar 也就可以被多个线程访问到。  假设线程 1 刚刚执行完calendar.setTime把时间设置成 2018-11-11,还  没等执行完,线程 2 又执行了calendar.setTime把时间改成了 2018-12-12。  这时候线程 1 继续往下执行,拿到的calendar.getTime得到的时间就是线程 2 改  过之后的。  除了 format 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。  所以,不要把 SimpleDateFormat 作为一个共享变量使用。

如何解决

前面介绍过了 SimpleDateFormat 存在的问题以及问题存在的原因,那么有什  么办法解决这种问题呢?  解决方法有很多,这里介绍三个比较常用的方法。 使用局部变量

加同步锁

使用 ThreadLocal

第三种方式,就是使用 ThreadLocal。 ThreadLocal 可以确保每个线程都可以  得到单独的一个 SimpleDateFormat 的对象,那么自然也就不存在竞争问题了

使用 DateTimeFormatter

如果是 Java8 应用,可以使用 DateTimeFormatter 代替 SimpleDateFormat,  这是一个线程安全的格式化工具类。就像官方文档中说的,这个类 simple beautiful  strong immutable thread-safe。

总结

本 文 介 绍 了 SimpleDateFormat 的 用 法,SimpleDateFormat 主 要 可 以 在  String 和 Date 之间做转换,还可以将时间转换成不同时区输出。同时提到在并发场  景中 SimpleDateFormat 是不能保证线程安全的,需要开发者自己来保证其安全性。  主要的几个手段有改为局部变量、使用 synchronized 加锁、使用 Threadlocal  为每一个线程单独创建一个等。  希望通过此文,你可以在使用 SimpleDateFormat 的时候更加得心应手。

禁止开发人员使用 isSuccess 作为变量名?

序列化带来的影响 关于序列化和反序列化请参考Java 对象的序列化与反序列化。我们这里拿比较  常用的 JSON 序列化来举例,看看看常用的 fastJson、jackson 和 Gson 之间有何  区别

我们可以得出结论:fastjson 和 jackson 在把对象序列化成 json 字符串的时候,  是通过反射遍历出该类中的所有 getter 方法,得到 getHollis 和 isSuccess,然后根  据 JavaBeans 规则,他会认为这是两个属性 hollis 和 success 的值。直接序列化  成 json:{“hollis”:”hollischuang”,”success”:true}

所以,在定义 POJO 中的布尔类型的变量时,不要使用 isSuccess 这种形式,  而要直接使用 success !

Boolean 还是 boolean ?

这里建议我们使用包装类型,原因是什么呢?  举一个扣费的例子,我们做一个扣费系统,扣费时需要从外部的定价系统中读取  一个费率的值,我们预期该接口的返回值中会包含一个浮点型的费率字段。当我们取  到这个值得时候就使用公式:金额 * 费率 = 费用 进行计算,计算结果进行划扣。  如果由于计费系统异常,他可能会返回个默认值,如果这个字段是 Double 类型  的话,该默认值为 null,如果该字段是 double 类型的话,该默认值为 0.0。

总结:

本文围绕布尔类型的变量定义的类型和命名展开了介绍,最终我们可以得出结  论,在定义一个布尔类型的变量,尤其是一个给外部提供的接口返回值时,要使用  success 来命名,阿里巴巴 Java 开发手册建议使用封装类来定义 POJO 和 RPC  返回值中的变量。但是这不意味着可以随意的使用 null,我们还是要尽量避免出现对  null 的处理的

禁止开发人员修改 serialVersionUID 字段的值?

序列化是一种对象持久化的手段。普遍应用在网络传输、RMI 等场景中。类通  过实现java.io.Serializable接口以启用其序列化功能。

背景知识 Serializable 和 Externalizable 类通过实现java.io.Serializable接口以启用其序列化功能。未实现此接  口的类将无法进行序列化或反序列化。可序列化类的所有子类型本身都是可序列  化的。  如果读者看过Serializable的源码,就会发现,他只是一个空的接口,里  面什么东西都没有。Serializable 接口没有方法或字段,仅用于标识可序列化的  语义。但是,如果一个类没有实现这个接口,想要被序列化的话,就会抛出java.  io.NotSerializableException异常。

在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果都不是则直接抛出NotSerializableException。

什么是 serialVersionUID 序列化是将对象的状态信息转换为可存储或传输的形式的过程。我们都知道,  Java 对象是保存在 JVM 的堆内存中的,也就是说,如果 JVM 堆不存在了,那么对  象也就跟着消失了。  而序列化提供了一种方案,可以让你在即使 JVM 停机的情况下也能把对象保存  下来的方案。就像我们平时用的 U 盘一样。把 Java 对象序列化成可存储或传输的形  式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中  读取出二进制流,再从二进制流中反序列化出对象。  虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重  要的一点是两个类的序列化 ID 是否一致,这个所谓的序列化 ID,就是我们在代码中  定义的serialVersionUID。

这是因为,在进行反序列化时,JVM 会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致  的,可以进行反序列化,否则就会出现序列化版本不一致的异常,即是InvalidCastException。  这也是《阿里巴巴 Java 开发手册》中规定,在兼容性升级中,在修改类的时  候,不要修改serialVersionUID的原因。除非是完全不兼容的两个版本。所以, serialVersionUID其实是验证版本一致性的。

总结

serialVersionUID是用来验证版本一致性的。所以在做兼容性升级的时候,  不要改变类中serialVersionUID的值。  如果一个类实现了 Serializable 接口,一定要记得定义serialVersionUID,否则会发生异常。可以在 IDE 中通过设置,让他帮忙提示,并且可以一键快速生成一  个serialVersionUID。  之所以会发生异常,是因为反序列化过程中做了校验,并且如果没有明确定义的  话,会根据类的属性自动生成一个

建议开发者谨慎使用继承?

从学习 Java 的第一天起,我们就知道 Java 是一种面向对象语言,而学习 Java  的第二天,我们就知道了面向对象的三大基本特性是:封装、继承、多态。  所以,对于很多开发者来说,继承肯定都是不陌生的。但是,继承一定适合所有  的场景吗?毫无忌讳的使用继承来做代码扩展真的好吗?  为什么《阿里巴巴 Java 开发手册》中有一条规定:谨慎使用继承的方式进行扩  展,优先使用组合的方式实现。

面向对象的复用技术 每个人在刚刚学习继承的时候都会或多或少的有这样一个印象:继承可以帮助我  实现类的复用。所以,很多开发人员在需要复用一些代码的时候会很自然的使用类的  继承的方式,因为书上就是这么写的(老师就是这么教的)。但是,其实这样做是不对  的。长期大量的使用继承会给代码带来很高的维护成本。  前面提到复用,这里就简单介绍一下面向对象的复用技术。  复用性是面向对象技术带来的很棒的潜在好处之一。如果运用的好的话可以帮助  我们节省很多开发时间,提升开发效率。但是,如果被滥用那么就可能产生很多难以维护的代码。  作为一门面向对象开发的语言,代码复用是 Java 引人注意的功能之一。Java  代码的复用有继承,组合以及代理三种具体的表现形式。

禁止使用 count( 列名 ) 或 count( 常量 ) 来替代 count(*) ?

数据库查询相信很多人都不陌生,所有经常有人调侃程序员就是 CRUD 专员,  这所谓的 CRUD 指的就是数据库的增删改查。  在数据库的增删改查操作中,使用最频繁的就是查询操作。而在所有查询操作  中,统计数量操作更是经常被用到。  关于数据库中行数统计,无论是 MySQL 还是 Oracle,都有一个函数可以使用,  那就是 COUNT。

简单翻译一下:  1. COUNT(expr) ,返回 SELECT 语句检索的行中 expr 的值不为 NULL 的  数量。结果是一个 BIGINT 值。  2. 如果查询结果没有命中任何记录,则返回 0。

3. 但是,值得注意的是,COUNT(*)的统计结果中,会包含值为 NULL 的行数

除 了COUNT(id)和COUNT(*)以 外, 还 可 以 使 用COUNT(常 量)(如 COUNT(1))来统计行数,那么这三条 SQL 语句有什么区别呢?到底哪种效率更  高呢?为什么《阿里巴巴 Java 开发手册》中强制要求不让使用COUNT(列名)或 COUNT(常量)来替代COUNT(*)呢?

所以,COUNT(常量)和COUNT(*)表示的是直接查询符合条件的数据库表的行  数。而COUNT(列名)表示的是查询符合条件的列的值不为 NULL 的行数。

除 了 查 询 得 到 结 果 集 有 区 别 之 外,COUNT(*)相 比COUNT(常 量)和 COUNT(列名)来讲,COUNT(*)是 SQL92 定义的标准统计行数的语法,因为他是  标准语法,所以 MySQL 数据库对他进行过很多优化

COUNT(*) 的优化 前面提到了COUNT(*)是 SQL92 定义的标准统计行数的语法,所以 MySQL  数据库对他进行过很多优化。

那么,具体都做过哪些事情呢?  这里的介绍要区分不同的执行引擎。MySQL 中比较常用的执行引擎就是  InnoDB 和 MyISAM。 

MyISAM 和 InnoDB 有很多区别,其中有一个关键的区别和我们接下来要介绍  的COUNT(*)有关,那就是MyISAM 不支持事务,MyISAM 中的锁是表级锁;而  InnoDB 支持事务,并且支持行级锁。  因为 MyISAM 的锁是表级锁,所以同一张表上面的操作需要串行进行,所以, MyISAM 做了一个简单的优化,那就是它可以把表的总行数单独记录下来,如果从  一张表中使用 COUNT(*) 进行查询的时候,可以直接返回这个记录下来的数值就可  以了,当然,前提是不能有 where 条件.

MyISAM 之所以可以把表中的总行数记录下来供 COUNT(*) 查询使用,那是因  为 MyISAM 数据库是表级锁,不会有并发的数据库行数修改,所以查询得到的行数是准确的。  但是,对于 InnoDB 来说,就不能做这种缓存操作了,因为 InnoDB 支持事务,  其中大部分操作都是行级锁,所以可能表的行数可能会被并发修改,那么缓存记录下  来的总行数就不准确了。  但是,InnoDB 还是针对 COUNT(*) 语句做了些优化的。  在 InnoDB 中,使用 COUNT(*) 查询行数的时候,不可避免的要进行扫表了,  那么,就可以在扫表过程中下功夫来优化效率了。  从 MySQL 8.0.13 开始,针对InnoDB的SELECT COUNT(*) FROM tbl_  name语句,确实在扫表的过程中做了一些优化。前提是查询语句中不包含 WHERE  或 GROUP BY 等条件。 我们知道,COUNT(*) 的目的只是为了统计总行数,所以,他根本不关心自己  查到的具体值,所以,他如果能够在扫表的过程中,选择一个成本较低的索引进行的  话,那就可以大大节省时间。  我们知道,InnoDB 中索引分为聚簇索引(主键索引)和非聚簇索引(非主键索  引),聚簇索引的叶子节点中保存的是整行记录,而非聚簇索引的叶子节点中保存的  是该行记录的主键的值。  所以,相比之下,非聚簇索引要比聚簇索引小很多,所以MySQL 会优先选择  最小的非聚簇索引来扫表。所以,当我们建表的时候,除了主键索引以外,创建一个  非主键索引还是有必要的。  至此,我们介绍完了 MySQL 数据库对于 COUNT(*) 的优化,这些优化的前提  都是查询语句中不包含 WHERE 以及 GROUP BY 条件。

COUNT(*) 和 COUNT(1)

介绍完了COUNT(*),接下来看看COUNT(1),对于,这二者到底有没有区别,  网上的说法众说纷纭。  有的说COUNT(*)执行时会转换成COUNT(1),所以 COUNT(1) 少了转换步  骤,所以更快。  还有的说,因为 MySQL 针对COUNT(*)做了特殊优化,所以COUNT(*)更快。  那么,到底哪种说法是对的呢?看下 MySQL 官方文档是怎么说的:  InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.  画重点:same way,no performance difference。所以,对于 COUNT(1)  和 COUNT(*),MySQL 的优化是完全一样的,根本不存在谁比谁快!  那既然COUNT(*)和COUNT(1)一样,建议用哪个呢?  建议使用COUNT(*)!因为这个是 SQL92 定义的标准统计行数的语法,而且本  文只是基于 MySQL 做了分析,关于 Oracle 中的这个问题,也是众说纷纭的呢。

COUNT( 字段 )

最后,就是我们一直还没提到的 COUNT( 字段 ),他的查询就比较简单粗暴了,  就是进行全表扫描,然后判断指定字段的值是不是为 NULL,不为 NULL 则累加。  相比COUNT(*),COUNT(字段)多了一个步骤就是判断所查询的字段是否为  NULL,所以他的性能要比COUNT(*)慢。

总结 本文介绍了 COUNT 函数的用法,主要用于统计表行数。主要用法有COUNT(*)、COUNT(字段)和COUNT(1)。  因为COUNT(*)是 SQL92 定义的标准统计行数的语法,所以 MySQL 对他进  行了很多优化,MyISAM 中会直接把表的总行数单独记录下来供COUNT(*)查询,  而 InnoDB 则会在扫表的时候选择最小的索引来降低成本。当然,这些优化的前提都  是没有进行 where 和 group 的条件查询。  在 InnoDB 中COUNT(*)和COUNT(1)实现上没有区别,而且效率一样,但是 COUNT(字段)需要进行字段的非 NULL 判断,所以效率会低一些。  因为COUNT(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直  接使用COUNT(*)查询表的行数!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HELLO XF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值