目录
- 引出
- 通用的方法和准则
- 建议1:不要在常量和变量中出现易混淆的字母
- 建议2:莫让常量蜕变成变量
- 建议3:三元操作符的类型务必一致
- 建议4:避免带有变长参数的方法重载
- 建议5:别让null值和空值威胁到变长方法
- 建议6:覆写变长方法也循规蹈矩
- 建议7:警惕自增的陷阱
- 建议8:不要让旧语法困扰你
- 建议9:少用静态导入
- 建议10:不要在本类中覆盖静态导入的变量和方法
- 建议11:养成良好的习惯,显式声明UID
- 建议12:避免用序列化类在构造函数中为不变量赋值
- 建议13:避免为final变量复杂赋值
- 建议14:使用序列化类的私有方法巧妙解决“部分属性持久化问题”
- 建议15:break万万不可忘
- 建议16:易变业务使用脚本语言编写
- 建议17:慎用动态编译
- 建议18:避免instanceof非预期结果
- 建议19:断言绝对不是鸡肋
- 建议20:不要只替换一个类
- 基本类型使用
- 建议21:用偶判断,不用奇判断
- 建议22:用整数类型处理货币
- 建议23:不要让类型默默转换
- 建议24:边界、边界、还是边界
- 建议25:不要让四舍五入亏了一方
- 建议26:提防包装类型的null值
- 建议27:谨慎包装类型的大小比较
- 建议28:优先使用整型池
- 建议29:优先选择基本类型
- 建议30:不要随便设置随机种子
- 类、对象及方法
- 建议31:在接口中不要存在实现代码
- 建议32:静态变量一定要先声明后赋值
- 建议33:不要覆写静态方法
- 建议34:构造函数尽量简化
- 建议35:避免在构造函数中初始化其他类
- 建议36:使用构造代码块精炼程序
- 建议37:构造代码块会想你所想
- 建议38:使用静态内部类提高封装性
- 建议39:使用匿名类的构造函数
- 建议40:匿名类的构造函数很特殊
- 建议41:让多重继承成为现实
- 建议42:让工具类不可实例化
- 建议43:避免对象的浅拷贝
- 建议44:推荐使用序列化实现对象的拷贝
- 建议45:覆写equals方法时不要识别不出自己
- 建议46:equals应该考虑null值情景
- 建议47:在equals中使用getClass进行类型判断
- 建议48:覆写equals方法必须覆写hashCode方法
- 建议49:推荐覆写toString方法
- 建议50:使用package-info类为包服务
- 建议51:不要主动进行垃圾回收
- 字符串
- 建议52:推荐使用String直接量赋值
- 建议53:注意方法中传递的参数要求
- 建议54:正确使用String、StringBuffer、StringBuilder
- 建议55:注意字符串的位置
- 建议56:自由选择字符串拼接方式
- 建议57:推荐在复杂字符串操作中使用正则表达式
- 建议58:强烈建议使用UTF编码
- 建议59:对字符串排序持一种宽容的心态
- 数组和集合
- 建议60:性能考虑,数组是首选
- 建议61:若有必要,使用变长数组
- 建议62:警惕数组的浅拷贝
- 建议63:在明确的场景下,为集合指定初始容量
- 建议64:多种最值算法,适时选择
- 建议65:避开基本类型数组转换列表陷阱
- 建议66:asList方法产生的List对象不可更改
- 建议67:不同的列表选择不同的遍历方法
- 建议68:频繁插入和删除时使用LinkedList
- 建议69:列表相等只需关心元素数据
- 建议70:子列表只是原列表的一个视图
- 建议71:推荐使用subList处理局部列表
- 建议72:生成子列表后不要再操作原列表
- 建议73:使用Comparator进行排序
- 建议74:不推荐使用binarySearch对列表进行检索
- 建议75:集合中的元素必须做到compareTo和equals同步
- 建议76:集合运算时使用更优雅的方式
- 建议77:使用shuffle打乱列表
- 建议78:减少HashMap中元素的数量
- 建议79:集合中的哈希码不要重复
- 建议80:多线程使用Vector或HashTable
- 建议81:非稳定排序推荐使用List
- 建议82:由点及面,一页知秋—集合大家族
- 枚举和注解
- 建议83:推荐使用枚举定义常量
- 建议84:使用构造函数协助描述枚举项
- 建议85:小心switch带来的空值异常
- 建议86:在switch的default代码块中增加AssertionError错误
- 建议87:使用valueOf前必须进行校验
- 建议88:用枚举实现工厂方法模式更简洁
- 建议89:枚举项的数量控制在64个以内
- 建议90:小心注解继承
- 建议91:枚举和注解结合使用威力更大
- 建议92:注意[@Override](https://github.com/Override)不同版本的区别
- 多线程和并发
- 建议118:不推荐覆写start方法
- 建议119:启动线程前stop方法是不可靠的
- 建议120:不适用stop方法停止线程
- 建议121:线程优先级只使用三个等级
- 建议122:使用线程异常处理器提升系统可靠性
- 建议123:volatile不能保证数据同步
- 建议124:异步运算考虑使用Callable接口
- 建议125:优先选择线程池
- 建议126:适时选择不同的线程池来实现
- 建议127:Lock与synchronized是不一样的
- 建议128:预防线程死锁
- 建议129:适当设置阻塞队列长度
- 建议130:使用CountDownLatch协调子线程
- 建议131:CyclicBarrier让多线程齐步走
- 性能和效率
- 建议132:提升Java性能的基本方法
- 建议133:若非必要,不要克隆对象
- 建议134:推荐使用“望闻问切”的方式诊断性能
- 建议135:必须定义性能衡量标准
- 建议136:枪打出头鸟—解决首要系统性能问题
- 建议137:调整JVM参数以提升性能
- 建议138:性能是个大“咕咚”
- 总结
引出
Java开发建议——通用准则,基本类型,类、对象及方法,字符串,数组和集合,枚举和注解,多线程和并发,性能和效率
通用的方法和准则
建议1:不要在常量和变量中出现易混淆的字母
- i、l、1;o、0等
建议2:莫让常量蜕变成变量
- 代码运行工程中不要改变常量值
建议3:三元操作符的类型务必一致
- 不一致会导致自动类型转换,类型提升int->float->double等
建议4:避免带有变长参数的方法重载
- 变长参数的方法重载之后可能会包含原方法
建议5:别让null值和空值威胁到变长方法
- 两个都包含变长参数的重载方法,当变长参数部分空值,或者为null值时,重载方法不清楚会调用哪一个方法
建议6:覆写变长方法也循规蹈矩
- 变长参数与数组,覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式
建议7:警惕自增的陷阱
- count=count++;操作时JVM首先将count原值拷贝到临时变量区,再执行count加1,之后再将临时变量区的值赋给count,所以count一直为0或者某个初始值。C中count=count;与count++等效,而PHP与Java类似
建议8:不要让旧语法困扰你
- Java中抛弃了C语言中的goto语法,但是还保留了该关键字,只是不进行语义处理,const关键中同样类似
建议9:少用静态导入
- Java5引入的静态导入语法import static,使用静态导入可以减少程序字符输入量,但是会带来很多代码歧义,省略的类约束太少,显得程序晦涩难懂
建议10:不要在本类中覆盖静态导入的变量和方法
- 例如静态导入Math包下的PI常量,类属性中又定义了一个同样名字PI的常量。编译器的“最短路径原则”将会选择使用本类中的PI常量。本类中的属性,方法优先。如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖
建议11:养成良好的习惯,显式声明UID
- 显式声明serialVersionUID可以避免序列化和反序列化中对象不一致,JVM根据serialVersionUID来判断类是否发生改变。隐式声明由编译器在编译的时候根据包名、类名、继承关系等诸多因子计算得出,极其复杂,算出的值基本唯一
建议12:避免用序列化类在构造函数中为不变量赋值
- 在序列化类中,不适用构造函数为final变量赋值)(**序列化规则1:**如果final属性是一个直接量,在反序列化时就会重新计算;**序列化规则2:**反序列化时构造函数不会执行;**反序列化执行过程:**JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name没有赋值(因为反序列化时构造函数不会执行),不能引用,于是它不再初始化,保持原始值状态。整个过程中需要保持serialVersionUID相同
建议13:避免为final变量复杂赋值
- 类序列化保存到磁盘上(或网络传输)的对象文件包括两部分:1、类描述信息:包括包路径、继承关系等。注意,它并不是class文件的翻版,不记录方法、构造函数、static变量等的具体实现。2、非瞬态(transient关键字)和非静态(static关键字)的实例变量值。总结:反序列化时final变量在以下情况下不会被重新赋值:1、通过构造函数为final变量赋值;2、通过方法返回值为final变量赋值;3、final修饰的属性不是基本类型
建议14:使用序列化类的私有方法巧妙解决“部分属性持久化问题”
- 部分属性持久化问题解决方案1:把不需要持久化的属性加上瞬态关键字(transient关键字)即可,但是会使该类失去了分布式部署的功能。方案2:新增业务对象。方案3:请求端过滤。方案4:变更传输契约,即覆写writeObject和readObject私有方法,在两个私有方法体内完成部分属性持久化
建议15:break万万不可忘
- switch语句中,每一个case匹配完都需要使用break关键字跳出,否则会依次执行完所有的case内容。
建议16:易变业务使用脚本语言编写
- 脚本语言:都是在运行期解释执行。脚本语言三大特性:1、灵活:动态类型;2、便捷:解释型语言,不需要编译成二进制,不需要像Java一样生成字节码,依靠解释执行,做到不停止应用变更代码;3、简单:部分简单。Java使用ScriptEngine执行引擎来执行JavaScript脚本代码
建议17:慎用动态编译
- 好处:更加自如地控制编译过程。很少使用,原因:静态编译能够完成大部分工作甚至全部,即使需要使用,也有很好的替代方案,如JRuby、Groovy等无缝的脚本语言。动态编译注意以下4点:
- 1、在框架中谨慎使用:debug困难,成本大;
- 2、不要在要求高性能的项目中使用:需要一个编译过程,比静态编译多了一个执行环节;
- 3、动态编译要考虑安全问题:防止恶意代码;
- 4、记录动态编译过程
建议18:避免instanceof非预期结果
- instanceof用来判断一个对象是否是一个类的实例,只能用于对象的判断,不能用于基本类型的判断(编译不通过),instanceof操作符的左右操作数必须有继承或实现关系,否则编译会失败。例:null instanceof String返回值是false,instanceof特有规则,若左操作数是null,结果就直接返回false,不再运算右操作数是什么类
建议19:断言绝对不是鸡肋
- 防御式编程中经常使用断言(Assertion)对参数和环境做出判断。断言是为调试程序服务的。两个特性:1、默认assert不启用;2、assert抛出的异常AssertionError是继承自Error的
建议20:不要只替换一个类
- 发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是完全之策)(Client类中调用了Constant类中的属性值,如果更改了Constant常量类属性的值,重新编译替换。而不改变或者替换Client类,则Client中调用的Constant常量类的属性值并不会改变。**原因:**对于final修饰的基本类型和String类型,编译器会认为它是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码中了,避免了再运行期引用,以提高代码的执行效率。而对于final修饰的类(即非基本类型),编译器认为它是不稳定态(Mutable Status),在编译时建立的则是引用关系(该类型也叫作Soft Final),如果Client类引入的常量是一个类或实例,即使不重新编译也会输出最新值
基本类型使用
建议21:用偶判断,不用奇判断
- 不要使用奇判断(i%2 == 1 ? “奇数” : “偶数”),使用偶判断(i%2 == 0 ? “偶数” : “奇数”)。原因Java中的取余(%标识符)算法:测试数据输入1 2 0 -1 -2,奇判断的时候,当输入-1时,也会返回偶数。
// 模拟取余计算,dividend被除数,divisor除数
public static int remainder(int dividend, int divisor) {
return dividend - dividend / divisor * divisor;}
建议22:用整数类型处理货币
- 不要使用float或者double计算货币,因为在计算机中浮点数“有可能”是不准确的,它只能无限接近准确值,而不能完全精确。不能使用计算机中的二进制位来表示如0.4等的浮点数。
- 解决方案:
- 1、使用BigDecimal(优先使用)
- 2、使用整型
建议23:不要让类型默默转换
- 基本类型转换时,使用主动声明方式减少不必要的Bug
public static final int LIGHT_SPEED = 30 * 10000 * 1000;long dis2 = LIGHT_SPEED * 60 * 8;
- 以上两句在参与运算时会溢出,因为Java是先运算后再进行类型转换的。因为dis2的三个运算参数都是int类型,三者相乘的结果也是int类型,但是已经超过了int的最大值,所以越界了。解决方法,在运算参数60后加L即可。
建议24:边界、边界、还是边界
- 数字越界是检验条件失效,边界测试;检验条件if(order>0 && order+cur<=LIMIT),输入的数大于0,加上cur的值之后溢出为负值,小于LIMIT,所以满足条件,但不符合要求
建议25:不要让四舍五入亏了一方
- Math.round(10.5)输出结果11;Math.round(-10.5)输出结果-10。这是因为Math.round采用的舍入规则所决定的(采用的是正无穷方向舍入规则),根据不同的场景,慎重选择不同的舍入模式,以提高项目的精准度,减少算法损失
建议26:提防包装类型的null值
- 泛型中不能使用基本类型,只能使用包装类型,null执行自动拆箱操作会抛NullPointerException异常,因为自动拆箱是通过调用包装对象的intValue方法来实现的,而访问null的intValue方法会报空指针异常。谨记一点:包装类参与运算时,要做null值校验,即(i!=null ? i : 0)
建议27:谨慎包装类型的大小比较
- 大于>或者小于<比较时,包装类型会调用intValue方法,执行自动拆箱比较。而==等号用来判断两个操作数是否有相等关系的,如果是基本类型则判断数值是否相等,如果是对象则判断是否是一个对象的两个引用,也就是地址是否相等。通过两次new操作产生的两个包装类型,地址肯定不相等
建议28:优先使用整型池
- 自动装箱是通过调用valueOf方法来实现的,包装类的valueOf生成包装实例可以显著提高空间和时间性能)valueOf方法实现源码:
public static Integer valueOf(int i) { final int offset = 128; if (i >= -128 && i <=127) { return IntegerCache.cache[i + offset]; } return new Integer(i); }class IntegerCache { static final Integer cache[] = new Integer[-(-128) + 127 + 1]; static { for (int i = 0; i < cache.length; i++) cache[i] = new Integer(i - 128); }}
- cache是IntegerCache内部类的一个静态数组,容纳的是**-128到127**之间的Integer对象。通过valueOf产生包装对象时,如果int参数在-128到127之间,则直接从整型池中获得对象,不在该范围的int类型则通过new生成包装对象。在判断对象是否相等的时候,最好是利用equals方法,避免“==”产生非预期结果。
建议29:优先选择基本类型
- int参数先加宽转变成long型,然后自动转换成Long型。Integer.valueOf(i)参数先自动拆箱转变为int类型,与之前类似
建议30:不要随便设置随机种子
- 若非必要,不要设置随机数种子)(Random r = new Random(1000);该代码中1000即为随机种子。在同一台机器上,不管运行多少次,所打印的随机数都是相同的。
在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个规则:1、种子不同,产生不同的随机数;
2、种子相同,即使实例不同也产生相同的随机数。
Random类默认种子(无参构造)是System.nanoTime()的返回值,这个值是距离某一个固定时间点的纳秒数,所以可以产生随机数。java.util.Random类与Math.random方法原理相同
类、对象及方法
建议31:在接口中不要存在实现代码
- 可以通过在接口中声明一个静态常量s,其值是一个匿名内部类的实例对象,可以实现接口中存在实现代码
建议32:静态变量一定要先声明后赋值
- 也可以先使用后声明,因为静态变量是类初始化时首先被加载,JVM会去查找类中所有的静态声明,然后分配空间,分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次,注意这时候只是完成了地址空间的分配还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行,后面的操作都是地址不变,值改变
建议33:不要覆写静态方法
- 一个实例对象有两个类型:表面类型和实际类型,表面类型是声明时的类型,实际类型是对象产生时的类型。对于非静态方法,它是根据对象的实际类型来执行的,即执行了覆写方法。而对于静态方法,首先静态方法不依赖实例对象,通过类名访问;其次,可以通过对象访问静态方法,如果通过对象访问,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行
建议34:构造函数尽量简化
- 通过new关键字生成对象时必然会调用构造函数。子类实例化时,首先会初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。构造函数太复杂有可能造成,对象使用时还没完成初始化
建议35:避免在构造函数中初始化其他类
- 有可能造成不断的new新对象的死循环,直到栈内存被消耗完抛出StackOverflowError异常为止
建议36:使用构造代码块精炼程序
- 四种类型的代码块:
- **1、普通代码块:**在方法后面使用“{}”括起来的代码片段;
- **2、静态代码块:**在类中使用static修饰,并使用“{}”括起来的代码片段;
- **3、同步代码块:**使用synchronized关键字修饰,并使用“{}”括起来的代码片段,表示同一时间只能有一个县城进入到该方法;
- **4、构造代码块:**在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段。编译器会把构造代码块插入到每个构造函数的最前端。
- 构造代码块的两个特性:1、在每个构造函数中都运行;2、在构造函数中它会首先运行
建议37:构造代码块会想你所想
- 编译器会把构造代码块插入到每一个构造函数中,有一个特殊情况:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。如果遇到super关键字,编译器会把构造代码块插入到super方法之后执行
建议38:使用静态内部类提高封装性
- Java嵌套内分为两种:1、静态内部类;2、内部类;静态内部类两个优点:加强了类的封装性和提高了代码的可读性。静态内部类与普通内部类的区别:1、静态内部类不持有外部类的引用,在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性,其他则不能访问。2、静态内部类不依赖外部类,普通内部类与外部类之间是相互依赖的关系,内部类不能脱离外部类实例,同声同死,一起声明,一起被垃圾回收器回收。而静态内部类可以独立存在,即使外部类消亡了;3、普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制
建议39:使用匿名类的构造函数
- List l2 = new ArrayList(){}; //定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已
List l3 = new ArrayList(){{}}; //定义了一个继承于ArrayList的匿名类,并且包含一个初始化块,类似于构造代码块)
建议40:匿名类的构造函数很特殊
- 匿名类初始化时直接调用了父类的同参数构造器,然后再调用自己的构造代码块
建议41:让多重继承成为现实
- Java中一个类可以多种实现,但不能多重继承。使用成员内部类实现多重继承。内部类一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能
建议42:让工具类不可实例化
- 工具类的方法和属性都是静态的,不需要实例即可访问。**实现方式:**将构造函数设置为private,并且在构造函数中抛出Error错误异常
建议43:避免对象的浅拷贝
- 浅拷贝存在对象属性拷贝不彻底的问题。对于只包含基本数据类型的类可以使用浅拷贝;而包含有对象变量的类需要使用序列化与反序列化机制实现深拷贝
建议44:推荐使用序列化实现对象的拷贝
- 通过序列化方式来处理,在内存中通过字节流的拷贝来实现深拷贝。使用此方法进行对象拷贝时需注意两点:1、对象的内部属性都是可序列化的;2、注意方法和属性的特殊修饰符,比如final、static、transient变量的序列化问题都会影响拷贝效果。一个简单办法,使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便
建议45:覆写equals方法时不要识别不出自己
- 需要满足p.equals§返回为真,自反性
建议46:equals应该考虑null值情景
- 覆写equals方法时需要判一下null,否则可能产生NullPointerException异常
建议47:在equals中使用getClass进行类型判断
- 使用getClass方法来代替instanceof进行类型判断
建议48:覆写equals方法必须覆写hashCode方法
- 需要两个相同对象的hashCode方法返回值相同,所以需要覆写hashCode方法,如果不覆写,两个不同对象的hashCode肯定不一样,简单实现hashCode方法,调用org.apache.commons.lang.builder包下的Hash码生成工具HashCodeBuilder
建议49:推荐覆写toString方法
- 原始toString方法显示不人性化
建议50:使用package-info类为包服务
- package-info类是专门为本包服务的,是一个特殊性主要体现在3个方面:1、它不能随便被创建;2、它服务的对象很特殊;3、package-info类不能有实现代码;package-info类的作用:1、声明友好类和包内访问常量;2、为在包上标注注解提供便利;3、提供包的整体注释说明
建议51:不要主动进行垃圾回收
- 主动进行垃圾回收是一个非常危险的动作,因为System.gc要停止所有的响应(Stop 天河world),才能检查内存中是否有可回收的对象,所有的请求都会暂停
字符串
建议52:推荐使用String直接量赋值
- 一般对象都是通过new关键字生成,String还有第二种生成方式,即直接声明方式,如String str = “a”;String中极力推荐使用直接声明的方式,不建议使用new String(“a”)的方式赋值。**原因:直接声明方式:**创建一个字符串对象时,首先检查字符串常量池中是否有字面值相等的字符串,如果有,则不再创建,直接返回池中引用,若没有则创建,然后放到池中,并返回新建对象的引用。**使用new String()方式:**直接声明一个String对象是不检查字符串常量池的,也不会吧对象放到池中。String的intern方法会检查当前的对象在对象池中是否有字面值相同的引用对象,有则返回池中对象,没有则放置到对象池中,并返回当前对象
建议53:注意方法中传递的参数要求
- replaceAll方法传递的第一个参数是正则表达式
建议54:正确使用String、StringBuffer、StringBuilder
- String使用“+”进行字符串连接,之前连接之后会产生一个新的对象,所以会不断的创建新对象,优化之后与StringBuilder和StringBuffer采用同样的append方法进行连接,但是每一次字符串拼接都会调用一次toString方法,所以会很耗时。StringBuffer与StringBuilder基本相同,只是一个字符数组的在扩容而已,都是可变字符序列,不同点是:StringBuffer是线程安全的,StringBuilder是线程不安全的
建议55:注意字符串的位置
- 在“+”表达式中,String字符串具有最高优先级)(Java对加号“+”的处理机制:在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转换为String类型进行拼接,如果是原始数据,则直接拼接,如果是对象,则调用toString方法的返回值然后拼接
建议56:自由选择字符串拼接方式
- 字符串拼接有三种方法:加号、concat方法及StringBuilder(或StringBuffer)的append方法。字符串拼接性能中,StringBuilder的append方法最快,concat方法次之,加号最慢。
原因:
- 1、“+”方法拼接字符串:虽然编译器对字符串的加号做了优化,使用StringBuidler的append方法进行追加,但是与纯粹使用StringBuilder的append方法不同:一是每次循环都会创建一个StringBuilder对象,二是每次执行完都要调用toString方法将其准换为字符串—toString方法最耗时;
- 2、concat方法拼接字符串:就是一个数组拷贝,但是每次的concat操作都会新创建一个String对象,这就是concat速度慢下来的真正原因;
- 3、append方法拼接字符串:StringBuidler的append方法直接由父类AbstractStringBuilder实现,整个append方法都在做字符数组处理,没有新建任何对象,所以速度快
建议57:推荐在复杂字符串操作中使用正则表达式
- 正则表达式是恶魔,威力强大,但难以控制
建议58:强烈建议使用UTF编码
- 一个系统使用统一的编码
建议59:对字符串排序持一种宽容的心态
- 如果排序不是一个关键算法,使用Collator类即可。主要针对于中文
数组和集合
建议60:性能考虑,数组是首选
- 性能要求较高的场景中使用数组替代集合)(基本类型在栈内存中操作,对象在堆内存中操作。数组中使用基本类型是效率最高的,使用集合类会伴随着自动装箱与自动拆箱动作,所以性能相对差一些
建议61:若有必要,使用变长数组
- 使用Arrays.copyOf(datas,newLen)对原数组datas进行扩容处理
建议62:警惕数组的浅拷贝
- 通过Arrays.copyOf(box1,box1.length)方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。数组中的元素没有实现Serializable接口
建议63:在明确的场景下,为集合指定初始容量
- ArrayList集合底层使用数组存储,如果没有初始为ArrayList指定数组大小,默认存储数组大小长度为10,添加的元素达到数组临界值后,使用Arrays.copyOf方法进行1.5倍扩容处理。HashMap是按照倍数扩容的,Stack继承自Vector,所采用扩容规则的也是翻倍
建议64:多种最值算法,适时选择
- 最值计算时使用集合最简单,使用数组性能最优,利用Set集合去重,使用TreeSet集合自动排序
建议65:避开基本类型数组转换列表陷阱
- 原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱)(基本类型是不能泛化的,在java中数组是一个对象,它是可以泛化的。使用Arrays.asList(data)方法传入一个基本类型数组时,会将整个基本类型数组作为一个数组对象存入,所以存入的只会是一个对象。JVM不可能输出Array类型,因为Array是属于java.lang.reflect包的,它是通过反射访问数组元素的工具类。在Java中任何一个数组的类都是“[I”,因为Java并没有定义数组这个类,它是编译器编译的时候生成的,是一个特殊的类
建议66:asList方法产生的List对象不可更改
- 使用add方法向asList方法生成的集合中添加元素时,会抛UnsupportedOperationException异常。原因:asList生成的ArrayList集合并不是java.util.ArrayList集合,而是Arrays工具类的一个内置类,我们经常使用的List.add和List.remove方法它都没有实现,也就是说asList返回的是一个长度不可变的列表。此处的列表只是数组的一个外壳,不再保持列表动态变长的特性
建议67:不同的列表选择不同的遍历方法
- ArrayList数组实现了RandomAccess接口(随机存取接口),ArrayList是一个可以随机存取的列表。集合底层如果是基于数组实现的,实现了RandomAccess接口的集合,使用下标进行遍历访问性能会更高;底层使用双向链表实现的集合,使用foreach的迭代器遍历性能会更高
建议68:频繁插入和删除时使用LinkedList
- ArrayList集合,每次插入或者删除一个元素,其后的所有元素就会向后或者向前移动一位,性能很低。LinkedList集合插入时不需要移动其他元素,性能高;修改元素,LinkedList集合比ArrayList集合要慢很多;添加元素,LinkedList与ArrayList集合性能差不多,LinkedList添加一个ListNode,而ArrayList则在数组后面添加一个Entry
建议69:列表相等只需关心元素数据
- 判断集合是否相等时只须关注元素是否相等即可(ArrayList与Vector都是List,都实现了List接口,也都继承了AbstractList抽象类,其equals方法是在AbstractList中定义的。所以只要求两个集合类实现了List接口就成,不关心List的具体实现类,只要所有的元素相等,并且长度也相等就表明两个List是相等的,与具体的容量类型无关
建议70:子列表只是原列表的一个视图
- 使用==判断相等时,需要满足两个对象地址相等,而使用equals判断两个对象是否相等时,只需要关注表面值是否相等。subList方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,RandomAccessSubList是SubList子类,SubList类中subList方法的实现原理:它返回的SubList类是AbstractList的子类,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,也就是子列表只是原列表的一个视图,所有的修改动作都反映在了原列表上)。
建议71:推荐使用subList处理局部列表
- 需求:要删除一个ArrayList中的20-30范围内的元素;将原列表转换为一个可变列表,然后使用subList获取到原列表20到30范围内的一个视图(View),然后清空该视图内的元素,即可在原列表中删除20到30范围内的元素
建议72:生成子列表后不要再操作原列表
- subList生成子列表后,使用Collections.unmodifiableList(list);保持原列表的只读状态)(利用subList生成子列表后,更改原列表,会造成子列表抛出java.util.ConcurrentModificationException异常。原因:subList取出的列表是原列表的一个视图,原数据集(代码中的list变量)修改了,但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),后面再对子列表操作时,就会检测到修改计数器与预期的不相同,于是就抛出了并发修改异常
建议73:使用Comparator进行排序
- Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具)(两种数据排序实现方式:1、实现Comparable接口,必须要实现compareTo方法,一般由类直接实现,表明自身是可比较的,有了比较才能进行排序;2、实现Comparator接口,必须实现compare方法,Comparator接口是一个工具类接口:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑
建议74:不推荐使用binarySearch对列表进行检索
- indexOf与binarySearch方法功能类似,只是使用了二分法搜索。使用二分查找的首要条件是必须要先排序,不然二分查找的值是不准确的。indexOf方法直接就是遍历搜寻。从性能方面考虑,binarySearch是最好的选择
建议75:集合中的元素必须做到compareTo和equals同步
- 实现了compareTo方法,就应该覆写equals方法,确保两者同步)(在集合中indexOf方法是通过equals方法的返回值判断的,而binarySearch查找的依据是compareTo方法的返回值;equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同
建议76:集合运算时使用更优雅的方式
- 1、并集:list1.addAll(list2);
- 2、交集:list1.retainAll(list2);
- 3、差集:list1.removeAll(list2);
- 4、无重复的并集:list2.removeAll(list1);list1.addAll(list2);
建议77:使用shuffle打乱列表
- 使用Collections.shuffle(tagClouds)打乱列表
建议78:减少HashMap中元素的数量
- 尽量让HashMap中的元素少量并简单)(**现象:**使用HashMap存储数据时,还有空闲内存,却抛出了内存溢出异常;**原因:**HashMap底层的数组变量名叫table,它是Entry类型的数组,保存的是一个一个的键值对。与ArrayList集合相比,HashMap比ArrayList多了一次封装,把String类型的键值对转换成Entry对象后再放入数组,这就多了40万个对象,这是问题产生的第一个原因;HashMap在插入键值对时,会做长度校验,如果大于或等于阈值(threshold变量),则数组长度增大一倍。默认阈值是当前长度与加载因子的乘积,默认的加载因子(loadFactor变量)是0.75,也就是说只要HashMap的size大于数组长度的0.75倍时,就开始扩容。导致到最后,空闲的内存空间不足以增加一次扩容时就会抛出OutOfMemoryError异常
建议79:集合中的哈希码不要重复
- 列表查找不管是遍历查找、链表查找或者是二分查找都不够快。最快的是Hash开头的集合(如HashMap、HashSet等类)查找,原理:根据hashCode定位元素在数组中的位置。HashMap的table数组存储元素特点:1、table数组的长度永远是2的N次幂;2、table数组中的元素是Entry类型;3、table数组中的元素位置是不连续的;每个Entry都有一个next变量,它会指向下一个键值对,用来链表的方式来处理Hash冲突的问题。如果Hash码相同,则添加的元素都使用链表处理,在查找的时候这部分的性能与ArrayList性能差不多
建议80:多线程使用Vector或HashTable
- Vector与ArrayList原理类似,只是是线程安全的,HashTable是HashMap的多线程版本。线程安全:基本所有的集合类都有一个叫快速失败(Fail-Fast)的校验机制,当一个集合在被多个线程修改并访问时,就可能出现ConcurrentModificationException异常,这是为了确保集合方法一致而设置的保护措施;实现原理是modCount修改计数器:如果在读列表时,modCount发生变化(也就是有其他线程修改)则会抛出ConcurrentModificationException异常。**线程同步:**是为了保护集合中的数据不被脏读、脏写而设置的
建议81:非稳定排序推荐使用List
- 非稳定的意思是:经常需要改动;TreeSet集合中元素不可重复,且默认按照升序排序,是根据Comparable接口的compareTo方法的返回值确定排序位置的。SortedSet接口(TreeSet实现了该接口)只是定义了在该集合加入元素时将其进行排序,并不能保证元素修改后的排序结果。因此TreeSet适用于不变量的集合数据排序,但不适合可变量的排序。对于可变量的集合,需要自己手动进行再排序)(SortedSet中的元素被修改后可能会影响其排序位置
建议82:由点及面,一页知秋—集合大家族
- List:实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则;
- Set:Set是不包含重复元素的集合,其主要的实现类有:EnumSet、HashSet、TreeSet,其中EnumSet是枚举类型的专用Set,HashSet是以哈希码决定其元素位置的Set,原理与HashMap相似,提供快速插入与查找方法,TreeSet是一个自动排序的Set,它实现了SortedSet接口;
- Map:可以分为排序Map和非排序Map;排序Map为TreeMap,根据Key值进行自动排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子类,EnumMap则要求其Key必须是某一个枚举类型;
- Queue:分为两类,一类是阻塞式队列,队列满了以后再插入元素会抛异常,主要包括:ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是以数组方式实现的有界阻塞队列;PriorityBlockingQueue是依照优先级组件的队列;LinkedBlockingQueue是通过链表实现的阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可以追加元素,经常使用的是PriorityQueue类。还有一种是双端队列,支持在头、尾两端插入和移除元素,主要实现类是:ArrayDeque、LinkedBlockingDeque、LinkedList;
- 数组:数组能存储基本类型,而集合不行;所有的集合底层存储的都是数组;
- 工具类:数组的工具类是:java.util.Arrays和java.lang.reflect.array;集合的工具类是java.util.Collections;
- 扩展类:可以使用Apache的commons-collections扩展包,也可以使用Google的google-collections扩展包
枚举和注解
建议83:推荐使用枚举定义常量
-
在项目开发中,推荐使用枚举常量替代接口常量和类常量)(常量分为:类常量、接口常量、枚举常量;
-
枚举常量
优点
:
- 1、枚举常量更简单;
- 2、枚举常量属于稳态性(不允许发生越界);
- 3、枚举具有内置方法,values方法可以获取到所有枚举值;
- 4、枚举可以自定义方法
建议84:使用构造函数协助描述枚举项
- 每个枚举项都是该枚举的一个实例。可以通过添加属性,然后通过构造函数给枚举项添加更多描述信息
建议85:小心switch带来的空值异常
- 使用枚举值作为switch(枚举类);语句的条件值时,需要对枚举类进行判断是否为null值。因为Java中的switch语句只能判断byte、short、char、int类型,JDK7可以判断String类型,使用switch语句判断枚举类型时,会根据枚举的排序值匹配。如果传入的只是null的话,获取排序值需要调用如season.ordinal()方法时会抛出NullPointerException异常
建议86:在switch的default代码块中增加AssertionError错误
- switch语句在使用枚举类作为判断条件时,避免出现增加了一个枚举项,而switch语句没做任何修改,编译不会出现问题,但是在运行期会发生非预期的错误。为避免这种情况出现,建议在default后直接抛出一个AssertionError错误。含义是:不要跑到这里来,一跑到这里来马上就会报错
建议87:使用valueOf前必须进行校验
- Enum.valueOf()方法会把一个String类型的名称转变为枚举项,也就是在枚举项中查找出字面值与该参数相等的枚举项。valueOf方法先通过反射从枚举类的常量声明中查找,若找到就直接返回,若找不到就抛出IllegalArgumentException异常
建议88:用枚举实现工厂方法模式更简洁
- 工厂方法模式是“创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其子类”。枚举实现工厂方法模式有两种方法:1、枚举非静态方法实现工厂方法模式;2、通过抽象方法生成产品;优点:1、避免错误调用的发生;2、性能好,使用便捷;3、减低类间耦合性
建议89:枚举项的数量控制在64个以内
- Java提供了两个枚举集合:EnumSet、EnumMap;EnumSet要求其元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举的枚举项。由于枚举类型的实例数量固定并且有限,相对来说EnumSet和EnumMap的效率会比其他Set和Map要高。Java处理EnumSet过程:当枚举项小于等于64时,创建一个RegularEnumSet实例对象,大于64时创一个JumboEnumSet实例对象。RegularEnumSet是把每个枚举项编码映射到一个long类型数字得每一位上,而JumboEnumSet则会先按照64个一组进行拆分,然后每个组再映射到一个long类型的数字得每一位上
建议90:小心注解继承
- 不常用的元注解(Meta-Annotation):@Inherited,它表示一个注解是否可以自动被继承
建议91:枚举和注解结合使用威力更大
- 注解和接口写法类似,都采用了关键字interface,而且都不能有实现代码,常量定义默认都是public static final类型的等,他们的主要不同点:注解要在interface前加上@字符,而且不能继承,不能实现
建议92:注意@Override不同版本的区别
- @Override注解用于方法的覆写上,它在编译期有效,也就是Java编译器在编译时会根据该注解检查方法是否真的是覆写,如果不是就报错,拒绝编译。Java1.5版本中@Override是严格遵守覆写的定义:子类方法与父类方法必须具有相同的方法名、输入参数、输出参数(允许子类缩小)、访问权限(允许子类扩大),父类必须是一个类,不是是接口,否则不能算是覆写。而在Java1.6就开放了很多,实现接口的方法也可以加上@Override注解了。如果是Java1.6版本移植到Java1.5版本中时,需要删除接口实现方法上的@Override注解
多线程和并发
建议118:不推荐覆写start方法
- 继承自Thread类的多线程类不必覆写start方法。原本的start方法中,调用了本地方法start0,它实现了启动线程、申请栈内存、运行run方法、修改线程状态等职责,线程管理和栈内存管理都是由JVM实现的,如果覆盖了start方法,也就是撤销了线程管理和栈内存管理的能力。所以除非必要,不然不要覆写start方法,即使需要覆写start方法,也需要在方法体内加上super.start调用父类中的start方法来启动默认的线程操作
建议119:启动线程前stop方法是不可靠的
- 现象:使用stop方法停止一个线程,而stop方法在此处的目的不是停止一个线程,而是设置线程为不可启用状态。但是运行结果出现奇怪现象:部分线程还是启动了,也就是在某些线程(没有规律)中的start方法正常执行了。在不符合判断规则的情况下,不可启用状态的线程还是启用了,这是线程启动(start方法)一个缺陷。Thread类的stop方法会根据线程状态来判断是终结线程还是设置线程为不可运行状态,对于未启动的线程(线程状态为NEW)来说,会设置其标志位为不可启动,而其他的状态则是直接停止。start方法源码中,start0方法在stop0方法之前,也就是说即使stopBeforeStart为true(不可启动),也会先启动一个线程,然后再stop0结束这个线程,而罪魁祸首就在这里!所以不要使用stop方法进行状态的设置
建议120:不适用stop方法停止线程
- 线程启动完毕后,需要停止,Java只提供了一个stop方法,但是不建议使用,有以下三个问题:1、stop方法是过时的;2、stop方法会导致代码逻辑不完整,stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的,以为stop方法会清除栈内信息,结束该线程,但是可能该线程的一段逻辑非常重,比如子线程的主逻辑、资源回收、情景初始化等,因为stop线程了,这些都不会再执行。子线程执行到何处会被关闭很难定位,这为以后的维护带来了很多麻烦;3、stop方法会破坏原子逻辑,多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是stop方法会丢弃所有的锁,导致原子逻辑受损。Thread提供的interrupt中断线程方法,它不能终止一个正在执行着的线程,它只是修改中断标志唯一。总之,期望终止一个正在运行的线程,不能使用stop方法,需要自行编码实现。如果使用线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭池中的线程
建议121:线程优先级只使用三个等级
- 线程优先级推荐使用MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY三个级别,不建议使用其他7个数字)(线程的优先级(Priority)决定了线程获得CPU运行的机会,优先级越高,运行机会越大。事实:1、并不是严格尊重线程优先级别来执行的,分为10个级别;2、优先级差别越大,运行机会差别越大;对于Java来说,JVM调用操作系统的接口设置优先级,比如Windows是通过调用SetThreadPriority函数来设置的。不同操作系统线程优先级设置是不相同的,Windows有7个优先级,Linux有140个优先级,Freebsd有255个优先级。Java缔造者也发现了该问题,于是在Thread类中设置了三个优先级,建议使用优先级常量,而不是1到10随机的数字
建议122:使用线程异常处理器提升系统可靠性
- 可以使用线程异常处理器来处理相关异常情况的发生,比如当机自动重启,大大提高系统的可靠性。在实际环境中应用注意以下三点:1、共享资源锁定;2、脏数据引起系统逻辑混乱;3、内存溢出,线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批新对象,特别是加入了场景接管,就危险了,有可能发生OutOfMemory内存泄露问题
建议123:volatile不能保证数据同步
- volatile不能保证数据是同步的,只能保证线程能够获得最新值)(volatile关键字比较少用的原因:1、Java1.5之前该关键字在不同的操作系统上有不同的表现,移植性差;2、比较难设计,而且误用较多。在变量钱加上一个volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与主内存交互的,而不是与本地线程的工作内存交互的,保证每个线程都能获得最“新鲜”的变量值。但是volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证多个线程修改的安全性
建议124:异步运算考虑使用Callable接口
- 多线程应用的两种实现方式:一种是实现Runnable接口,另一种是继承Thread类,这两个方式都有缺点:run方法没有返回值,不能抛出异常(归根到底是Runnable接口的缺陷,Thread也是实现了Runnable接口),如果需要知道一个线程的运行结果就需要用户自行设计,线程类本身也不能提供返回值和异常。Java1.5开始引入了新的接口Callable,类似于Runnable接口,实现它就可以实现多线程任务,实现Callable接口的类,只是表明它是一个可调用的任务,并不表示它具有多线程运算能力,还是需要执行器来执行的
建议125:优先选择线程池
- Java1.5以前,实现多线程比较麻烦,需要自己启动线程,并关注同步资源,防止出现线程死锁等问题,Java1.5以后引入了并行计算框架,大大简化了多线程开发。线程有五个状态:新建状态(New)、可运行状态(Runnable,也叫作运行状态)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为运行态后才可能被阻塞或等待,最后终结,不可能产生本末倒置的情况,比如想把结束状态变为新建状态,则会出现异常。线程运行时间分为三个部分:T1为线程启动时间;T2为线程体运行时间;T3为线程销毁时间。每次创建线程都会经过这三个时间会大大增加系统的响应时间。T2是无法避免的,只能通过优化代码来降低运行时间。T1和T3都可以通过线程池(Thread Pool)来缩短时间。线程池的实现涉及一下三个名词:1、工作线程(Worker),线程池中的线程只有两个状态:可运行状态和等待状态;2、任务接口(Task),每个任务必须实现的接口,以供工作线程调度器调度,它主要规定了任务的入口、任务执行完的场景处理、任务的执行状态等。这里的两种类型的任务:具有返回值(或异常)的Callable接口任务和无返回值并兼容旧版本的Runnable接口任务;3、任务队列(Work Queue),也叫作工作队列,用于存放等待处理的任务,一般是BlockingQueue的实现类,用来实现任务的排队处理。线程池的创建过程:创建一个阻塞队列以容纳任务,在第一次执行任务时闯将足够多的线程(不超过许可线程数),并处理任务,之后每个工作线程自行从任务队列中获得任务,直到任务队列中任务数量为0为止,此时,线程将处于等待状态,一旦有任务加入到队列中,即唤醒工作线程进行处理,实现线程的可复用性
建议126:适时选择不同的线程池来实现
- Java的线程池实现从根本上来说只有两个:ThreadPoolExecutor类和ScheduledThreadPoolExecutor类,还是父子关系。为了简化并行计算,Java还提供了一个Executors的静态类,它可以直接生成多种不同的线程池执行器,比如单线程执行器、带缓冲功能的执行器等,归根结底还是以上两个类的封装类
建议127:Lock与synchronized是不一样的
- Lock类(显式锁)和synchronized关键字(内部锁)用在代码块的并发性和内存上时的语义是一样的,都是保持代码块同时只有一个线程具有执行权。显式锁的锁定和释放必须在一个try…finally块中,这是为了确保即使出现运行期异常也能正常释放锁,保证其他线程能够顺利执行。Lock锁为什么不出现互斥情况,所有线程都是同时执行的?原因:这是因为对于同步资源来说,显式锁是对象级别的锁,而内部锁是类级别的锁,也就是说Lock锁是跟随对象的,synchronized锁是跟随类的,更简单地说把Lock定义为多线程类的私有属性是起不到资源互斥作用的,除非是把Lock定义为所有线程共享变量。除了以上不同点之外,还有以下4点不同:1、Lock支持更细粒度的锁控制,假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读写可以并发执行,这一点内部锁很难实现;2、Lock是无阻塞锁,synchronized是阻塞锁,线程A持有锁,线程B也期望获得锁时,如果为Lock,则B线程为等待状态,如果为synchronized,则为阻塞状态;3、Lock可实现公平锁,synchronized只能是非公平锁,什么叫做非公平锁?当一个线程A持有锁,而线程B、C处于阻塞(或等待)状态时,若线程A释放锁。JVM将从线程B、C中随机选择一个线程持有锁并使其获得执行权,这叫做非公平锁(因为它抛弃了先来后到的顺序);若JVM选择了等待时间最长的一个线程持有锁,则为公平锁。需要注意的是,即使是公平锁,JVM也无法准确做到“公平”,在程序中不能以此作为精确计算。显式锁默认是非公平锁,但可以在构造函数中加入参数true来声明出公平锁;4、Lock是代码级的,synchronized是JVM级的,Lock是通过编码实现的,synchronized是在运行期由JVM解释的,相对来说synchronized的优化可能性更高,毕竟是在最核心不为支持的,Lock的优化需要用户自行考虑。相对来说,显式锁使用起来更加便利和强大,在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:灵活、强大则选择Lock,快捷、安全则选择synchronized
建议128:预防线程死锁
- 线程死锁(DeadLock)是多线程编码中最头疼问题,也是最难重现的问题,因为Java是单进程多线程语言。要达到线程死锁需要四个条件:1、互斥条件;2、资源独占条件;3、不剥夺条件;4、循环等待条件;按照以下两种方式来解决:1、避免或减少资源贡献;2、使用自旋锁,如果在获取自旋锁时锁已经有保持者,那么获取锁操作将“自旋”在那里,直到该自旋锁的保持者释放了锁为止
建议129:适当设置阻塞队列长度
- 阻塞队列BlockingQueue扩展了Queue、Collection接口,对元素的插入和提取使用了“阻塞”处理。但是BlockingQueue不能够自行扩容,如果队列已满则会报IllegalStateException:Queue full队列已满异常;这是阻塞队列和非阻塞队列一个重要区别:阻塞队列的容量是固定的,非阻塞队列则是变长的。阻塞队列可以在声明时指定队列的容量,若指定的容量,则元素的数量不可超过该容量,若不指定,队列的容量为Integer的最大值。有此区别的原因是:阻塞队列是为了容纳(或排序)多线程任务而存在的,其服务的对象是多线程应用,而非阻塞队列容纳的则是普通的数据元素。阻塞队列的这种机制对异步计算是非常有帮助的,如果阻塞队列已满,再加入任务则会拒绝加入,而且返回异常,由系统自行处理,避免了异步计算的不可知性。可以使用put方法,它会等队列空出元素,再让自己加入进去,无论等待多长时间都要把该元素插入到队列中,但是此种等待是一个循环,会不停地消耗系统资源,当等待加入的元素数量较多时势必会对系统性能产生影响。offer方法可以优化一下put方法
建议130:使用CountDownLatch协调子线程
- CountDownLatch协调子线程步骤:一个开始计数器,多个结束计数器:1、每一个子线程开始运行,执行代码到begin.await后线程阻塞,等待begin的计数变为0;2、主线程调用begin的countDown方法,使begin的计数器为0;3、每个线程继续运行;4、主线程继续运行下一条语句,end的计数器不为0,主线程等待;5、每个线程运行结束时把end的计数器减1,标志着本线程运行完毕;6、多个线程全部结束,end计数器为0;7、主线程继续执行,打印出结果。类似:领导安排了一个大任务给我,我一个人不可能完成,于是我把该任务分解给10个人做,在10个人全部完成后,我把这10个结果组合起来返回给领导—这就是CountDownLatch的作用
建议131:CyclicBarrier让多线程齐步走
- CyclicBarrier关卡可以让所有线程全部处于等待状态(阻塞),然后在满足条件的情况下继续执行,这就好比是一条起跑线,不管是如何到达起跑线的,只要到达这条起跑线就必须等待其他人员,待人员到齐后再各奔东西,CyclicBarrier关注的是汇合点的信息,而不在乎之前或者之后做何处理。CyclicBarrier可以用在系统的性能测试中,测试并发性
性能和效率
建议132:提升Java性能的基本方法
- 如何让Java程序跑的更快、效率更高、吞吐量更大:1、不要在循环条件中计算,每循环一次就会计算一次,会降低系统效率;2、尽可能把变量、方法声明为final static类型,加上final static修饰后,在类加载后就会生成,每次方法调用则不再重新生成对象了;3、缩小变量的作用范围,目的是加快GC的回收;4、频繁字符串操作使用StringBuilder或StringBuffer;5、使用非线性检索,使用binarySearch查找会比indexOf查找元素快很多,但是使用binarySearch查找时记得先排序;6、覆写Exception的fillInStackTrace方法,fillInStackTrace方法是用来记录异常时的栈信息的,这是非常耗时的动作,如果不需要关注栈信息,则可以覆盖,以提升性能;7、不建立冗余对象
建议133:若非必要,不要克隆对象
- (克隆对象并不比直接生成对象效率高)(通过clone方法生成一个对象时,就会不再执行构造函数了,只是在内存中进行数据块的拷贝,看上去似乎应该比new方法的性能好很多,但事实上,一般情况下new生成的对象比clone生成的性能方面要好很多。JVM对new做了大量的系能优化,而clone方式只是一个冷僻的生成对象的方式,并不是主流,它主要用于构造函数比较复杂,对象属性比较多,通过new关键字创建一个对象比较耗时间的时候
建议134:推荐使用“望闻问切”的方式诊断性能
- 性能诊断遵循“望闻问切”,不可过度急躁
建议135:必须定义性能衡量标准
- 原因:
- 1、性能衡量标准是技术与业务之间的契约;
- 2、性能衡量标志是技术优化的目标
建议136:枪打出头鸟—解决首要系统性能问题
- 解决性能优化要“单线程”小步前进,避免关注点过多而导致精力分散)(解决性能问题时,不要把所有的问题都摆在眼前,这只会“扰乱”你的思维,集中精力,找到那个“出头鸟”,解决它,在大部分情况下,一批性能问题都会迎刃而解
建议137:调整JVM参数以提升性能
- 四个常用的JVM优化手段:
- 1、调整堆内存大小,JVM两种内存:栈内存(Stack)和堆内存(Heap),栈内存的特点是空间小,速度快,用来存放对象的引用及程序中的基本类型;而堆内存的特点是空间比较大,速度慢,一般对象都会在这里生成、使用和消亡。栈空间由线程开辟,线程结束,栈空间由JVM回收,它的大小一般不会对性能有太大影响,但是它会影响系统的稳定性,超过栈内存的容量时,会抛StackOverflowError错误。可以通过**“java -Xss ”设置栈内存大小来解决。堆内存的调整不能太随意,调整得太小,会导致Full GC频繁执行,轻则导致系统性能急速下降,重则导致系统根本无法使用;调整得太大,一则浪费资源(若设置了最小堆内存则可以避免此问题),二则是产生系统不稳定的情况,设置方法“java -Xmx1536 -Xms1024m”**,可以通过将-Xmx和-Xms参数值设置为相同的来固定堆内存大小;
- 2、调整堆内存中各分区的比例,JVM的堆内存包括三部分:新生区(Young Generation Space)、养老区(Tenure Generation Space)、永久存储区(Permanent Space 方法区),其中新生成的对象都在新生区,又分为伊甸区(Eden Space)、幸存0区(Survivor 0 Space)和幸存1区(Survivor 1 Space),当在程序中使用了new关键字时,首先在Eden区生成该对象,如果Eden区满了,则触发minor GC,然后把剩余的对象移到Survivor区(0区或者1区),如果Survivor取也满了,则minor GC会再回收一次,然后再把剩余的对象移到养老区,如果养老区也满了,则会触发Full GC(非常危险的动作,JVM会停止所有的执行,所有系统资源都会让位给垃圾回收器),会对所有的对象过滤一遍,检查是否有可以回收的对象,如果还是没有的话,就抛出OutOfMemoryError错误。一般情况下新生区与养老区的比例为1:3左右,设置命令:“java -XX:NewSize=32m -XX:MaxNewSize=640m -XX:MaxPermSize=1280m -XX:NewRatio=5”,该配置指定新生代初始化为32MB(也就是新生区最小内存为32M),最大不超过640MB,养老区最大不超过1280MB,新生区和养老区的比例为1:5.一般情况下Eden Space : Survivor 0 Space : Survivor 1 Space == 8 : 1 : 1);
- 3、变更GC的垃圾回收策略,设置命令“java -XX:+UseParallelGC -XX:ParallelGCThreads=20”,这里启用了并行垃圾收集机制,并且定义了20个收集线程(默认的收集线程等于CPU的数量),这对多CPU的系统时非常有帮助的,可以大大减少垃圾回收对系统的影响,提高系统性能;
- 4、更换JVM,如果所有的JVM优化都不见效,那就只有更换JVM了,比较流行的三个JVM产品:Java HotSpot VM、Oracle JRockit JVM、IBM JVM。
建议138:性能是个大“咕咚”
- 1、没有慢的系统,只有不满足义务的系统;
- 2、没有慢的系统,只有架构不良的系统;
- 3、没有慢的系统,只有懒惰的技术人员;
- 4、没有慢的系统,只有不愿意投入的系统
总结
Java开发建议——通用准则,基本类型,类、对象及方法,字符串,数组和集合,枚举和注解,多线程和并发,性能和效率