《编写高质量代码(改善Java程序的151个建议)》读书笔记
时间只是供我垂钓的溪流,在我喝着溪水的时候,我看的到它的沙床,它是那么浅啊,浅浅的洗漱流去了。永恒却留在原地。我愿痛饮,我愿在天上垂钓,在天空的底层,看着石子似的星星。—————《瓦尔登湖》
嗯嗯,昨夜难眠,与Y君讲,要我遇些困苦,是要注定不凡的,我会走完一生的,要给自己些勇气。
2018.11.22
时间只是供我垂钓的溪流,在我喝着溪水的时候,我看的到它的沙床,它是那么浅啊,浅浅的洗漱流去了。永恒却留在原地。我愿痛饮,我愿在天上垂钓,在天空的底层,看着石子似的星星。—————《瓦尔登湖》
建议
第一章,Java开发中通用的方法和准则
1. 不要在常量和变量中出现易混淆的字母,(“1l“,表示一个long型的值,容易看做“11”);
2. 莫让变量脱变成变量(public static final int RAND_CONST = Random().nextInt();)
3. 三元类型操作符的类型必须一致(转化规则,若不可转,返回值Object,为明确类型表达式(变量),正常转化,若为数字与表达式,转换为范围大的,若为字面量数字,类型转化为范围大的)
4. 避免带有变长参数的方法重载(从最简单开始)。
5. 别让null值和空值威胁到重载的变长方法(变长参数N>=0,必须把null定义为具体的类型)。
6. 覆写变长的方法也要循规蹈矩(可访问性一致/公开,参数列表相同(顺序等),返回类型相同/子类,不抛出新异常,或者超过父类的异常类)
7. 警惕自增的陷阱(i =0 ; while(true){i = i++;})
8. 不要让旧语法困扰。
9. 少用静态导入(规则:不使用*通配符,方法名为工具类)。
10. 不要在本类中覆盖静态导入的变量和方法(最短路径原则),一般在原始类重构而不是覆盖。
11. 养成良好的习惯,显示申明UID
1 | public class Person implements Serializable{ |
当序列化和反序列化的版本不一致时,反序列化会报一个InvalidClassException异常,原因是类版本发生变化,JVM不能把数据流转换为实例对象,JVM通过SerialVersionUID(流标识符),即类的版本定义的,可以显示定义可以隐式定义(编译器自动申明),JVM反序列化时,会比较数据流中的SerialVersionUID与类中的SerialVersionUID是否相同,不相同抛出异常。依靠显示申明,改变一端的Person后可以运行。即显示申明SerialVersionUID可以避免对象不一致。但尽量不要以这种方式向JVM”撒谎”。
12. 避免用序列化类在构造函数中为不变量赋值,(反序列化时构造函数不会执行)。
13. 避免为final变量复杂赋值:
(保存在磁盘上的对象文件包括两部分:
- 1,
类文件描述信息
:包括类路径,继承关系,访问权限,变量描述,变量访问权限,方法签名,返回值,以及变量 的关联关系类信息。 - 2,
非瞬态(trtansient)和非静态(static)的的实例变量值
。
反序列化时final变量在一下情况不会被赋值:通过构造函数赋值,通过方法返回值赋值,final修饰的属性不是基本类型
14. 使用序列化类的私有方法巧妙解决部分属性持久化的问题;
1 | private void writeObject(ObjectOutputStream out)throws IOException{ |
在序列化类中增加writeObject和readObject两个方法,使用序列化的独有机制,序列化回调,Java调用ObjectOutputStream类把一个对象转换为流数据时,会通过反射(Reflection)检查被序列化的类是否有writeObject方法,并且检查其是否为私有,无返回值的特性,若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化,在反序列化的时候也会检查是否有私有方法readObject。如果有会通过该方法读取属性。
15. break 万万不可以忘;
16. 易变业务使用脚本语言编写(特性:灵活,便捷,简单),JCP(Java Community Prosess)提出JSR223规范,JavaScript默认支持,
脚本语言可以随时发布而不用重新部署,即脚本语言改变,也能提供不间断的业务服务。
1 | public class main { |
1 | public interface Bindings extends Map<String,Object> |
- 慎用动态编译(注意:在框架中谨慎使用,不要在要求高性能的项目中使用,动态编译要考虑安全问题,记录动态编译过程)
- 避免instanceof非预期的结果。(instanceof操作符的左右必须有继承或实现关系)
1
2
3
4
5
6
7
8"String" instanceof String //返回值为true
new String() instanceof String //返回值为true
new Object() instanceof String //false(可以编译)
'A' instanceof Character //编译不通过,A为基本类型,Character为封装类,前边必须为对象。
null instanceof String //false,特殊规则,如果左操作数是null,结果就直接返回false,不在运运算右操作数,
(String)null instanceof String //false,null是一个万用类型,可以说它没有类型,即使类型转换也是null。
new Date() instanceof String //编译不通过,没有继承实现关系。
T(T为泛型String类变量) instanceof Date; //通过,false,T被编译为Object类,传递String类的值,所以 "Object instanceof Date";
19. 断言绝不是鸡肋:
1 | assert <boolean_expression> |
20. 不要只替换一个类,(发布应用系统时禁止使用类文件替换方式,整体的WAR包发布才是万全之策)
对于final修饰的基本类型和String类型,编译器会认为他是稳定态(Immutable Status),所以编译期间之间把值编译到字节码中,避免运行期引用(Run-time-reference),提高代码执行效率对于final修饰的基本类型和String类型,编译器会认为他是稳定态(Immutable Status),所以编译期间之间把值编译到字节码中,避免运行期引用(Run-time-reference),提高代码执行效率,对于final类来讲编译器认为它是不稳定的,在编译期建立则是引用关系,即到final修饰一个类或实例时,不重新编译也会是最新值。
第二章,基本类型
21. 用于偶判断,不用奇判断,(取模运算,)
1 | i%2 == 1?"奇数":"偶数";//输入-1为偶数 |
22. 用整数类型处理货币(使用BigDecimal类,与数据库映射方便。使用整形(扩大倍数))
23. 不用让类型默默转换,(java 为先运算后进行类型转化的)当运算结果过界就会从头开始(负值),在运算中加入数据范围大的值,基本类型转换时,使用主动声明方式减少不必要的bug.
24. 边界,边界,还是边界(判断数值范围时,要考虑超过类型边界后为负数)。
25. 不要让四舍五入亏了一方(银行家舍入):
26. 堤防包装类型的null值:包装类型参与运算时,要做null值校验。
27. 谨慎包装类型的大小比较:”==”对于基本类型比较的值,对象比较是否为同一个引用。<>不能比较对象大小。
28. 优先使用整形池:装箱生成的对象,装箱动作通过valueOf实现的,chache是IntegerCatch内部类的静态数组,容纳-128—127之间的Integer对象,不在该范围的int类型通过new生成包装对象。即127这个数字的包装每次都是同一个对象,而128不是同一个对象。整形池存在不仅提高了系统性能,节约内存空间。
1 | public static Integer valueOf(int){ |
29. 优先选择基本类型:(自动装箱的重要原则,基本类型可以先加宽,再转变成宽类型的包装类型,但不能直接转换为宽类型的包装类型)
30. 不要随便设置随机种子:
在java中,随机数的产生取决于种子,随机数和种子之间的关系(种子不同,产出的随机数不同,种子相同,即使实例不同也产生相同的随机数)
1 | public static void main(String[] args) { |
获得随机数:Math.random()方法,通过java.util.Random;
第三章,类对象及方法
31. 在接口中不要存在实现代码(接口中声明一个匿名内部类的实例对象的静态常量)。
32. 静态变量一定要先声明后赋值
静态变量是类加载时被分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次,其后的所有赋值操作都是值改变,地址则保持不变。JVM初始化变量是先声明空间,然后在赋值(int i= 12;==>int i ; i =12;)
静态变量在类初始化时首先被加载的,JVM会查找类中所有的静态申明,然后分配空间,只是完成地址空间分配,还没有赋值,之后会根据类中的静态赋值(包括静态类型赋值和静态块赋值)的先后顺序执行。变量先申明后使用。
33. 不要(重写)覆写静态方法,覆写是针对非静态方法(实例方法)的,不针对静态方法(类方法),但是可以隐藏静态方法(属于该类,与父类没有关系)。通过实例对象访问静态方法和属性是不好的习惯。
实例对象有两个类型:表面类型(Apparent Type)
和实际类型(Actual Type)
,表面类型是声明时的类型,实际类型是对象产生时的类型,对于非静态方法,它是根据对象的实际类型来执行的,对于静态方法来说,不依赖实例对象,通过类名访问,通过对象访问静态方法,JVM会通过对象的表面类型查找到静态方法的入口,然后执行,
在子类中构建与父类相同的方法名,输入参数,输出参数,访问权限(权限可以扩大),并且父类子类都是静态方法,此种行为称之为隐藏(Hide),它与覆写有两点不同。
- 1,
表现形式不同
:隐藏用于静态方法,覆写用于非静态,@OVerride可以用于覆写(写上自动检测是否合要求),不能用于隐藏。 - 2,
职责不同
:隐藏的目的是为了抛弃父类静态方法,重现子类方法,覆写是为了将父类的行为增强或减弱。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public class Base {
//父类静态方法
public static void doSomething() {
System.out.println("我是父类的静态方法");
}
//父类非静态方法
public void doAnything() {
System.out.println("我是父类的非静态方法");
}
}
public class Sub extends Base{
//子类同名,同参数的静态方法
public static void doSomething() {
System.out.println("我是子类的静态方法");
}
//覆写父类的非静态方法
public void doAnything() {
System.out.println("我是子类的非静态方法");
}
}
public class Client1 {
public static void main(String[] args) {
Base base1 = new Sub();
Sub base = new Sub();
base.doAnything();
base1.doSomething();
base.doSomething();
}
}
//结果
//我是子类的非静态方法
//我是父类的静态方法
//我是子类的静态方法
34. 构造函数尽量简化。(子类实例化时,会先初始化父类(初始化,不是生成父类对象),就是初始化父类的对象,调用父类的构造函数,然后才会初始化子类的变量,调用子类的构造函数,最后生成一个实例对象)
35. 避免在构造函数中初始化其他类。
36. 使用构造代码精炼程序,
代码块(Code Block):{}包裹的数据体,实现特定算法,一般不能单独运行,要有运行主体,java 中有四种:
- 1,
普通代码块
:方法名后面{ }部分。 - 2,
静态代码块
:在类中使用static修饰的{ },用于静态变量的初始化和对象创建前的环境初始化。类中的静态块会在整个类加载过程中的初始化阶段执行,而不是在类加载过程中的加载阶段执行。初始化阶段是类加载过程中的最后一个阶段,该阶段就是执行类构造器方法的过程, 方法由编译器自动收集类中所有类变量(静态变量)的赋值动作和静态语句块中的语句合并生成 ,一个类一旦进入初始化阶段,必然会执行静态语句块。所以说,静态块一定会在类加载过程中被执行,但不会在加载阶段被执行 - 3,
同步代码块
:使用synchronized修饰的{ },表示同一时间只能有一个线程进入到该方法块,一种多线程保护机制。 - 4,
构造代码块
:在类中没有人任何前缀和后缀的{ },编译器会把构造代码块插入到构造函数的最前端。
在通过new关键字生成一个实例时会先执行构造代码块,然后在执行其他构造函数代码,依托于构造函数运行,不是在构造函数之前运行。应用: - 1,
初始化实例变量(Instance Variable)
:如果每个构造函数都需要初始化变量,可以通过构造代码块实现。 - 2,
初始化实例环境
:当一个对象必须在适的场景才能存在,jee中要产生HTTP Request,必须要建立HTTP session ,可以在创建HTTP Request时通过构造代码块检查HTTP Session是否存在,不存在就创建。
37. 构造代码块会想你所想。(当构造代码块遇到this关键字时(构造函数调用自身其他无参构造函数),则不插入代码块,遇到super时,会放到super方法之后执行)
38. 使用静态内部类提高封装性:
java中的嵌套类(Nesetd Class):分为两种,静态内部类
(也叫静态嵌套类,Static Nested Class)和内部类
(Inner Class),
静态内部类:加强了类的封装性,提高了代码的可读性。
静态内部类不持有外部类的引用(普通内部类可以访问外部类的方法,属性,即使是private类型也可以访问,静态内部类只可以访问外部类的静态方法和静态属性),静态内部类不依赖外部类(普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,同生同死,一起声明,一起被拉圾回收器回收,静态内部类可以独立存在,即使外部类消亡,静态内部类还是可以存在),普通内部不能声明static的方法和变量(常量可以修饰,静态内部类没有限制)
39. 使用匿名类的构造函数
1 | //声明一个ArrayList对象。 |
40. 匿名类的构造函数很特殊:(一般类(具有显示名字的类)的所有构造函数默认都是调用父类的无参构造函数的,而匿名类没有名字,只能有构造代码块代替,它在初始化时直接调用父类的同参构造函数,然后在调用自己的构造代码块)
1 | enum Ops{ADD,SUB } |
41. 让多重继承成为现实(内部类的重要特征,内部类可以继承一个与外部类无关的类,保证了内部类当然独立性)多重继承考虑内部类。
1 | //父亲 |
42. 让工具类不可实例化(一般为静态):在构造函数设置私有,抛出异常。
1 | public class UtilsClass{ |
43. 避免对象的浅拷贝(浅拷贝是java的一种简单的机制,不便于直接使用)
一个类在实现了Cloneable接口就表示它具备了被拷贝的能力,如果在覆写clone()方法就会完全具备拷贝能力。拷贝在内存中进行,所以在性能方面比直接通过new生成对象要快的多,存在缺陷:浅拷贝(Shadow Clobe,也称影子拷贝)存在对象属性拷贝不彻底的问题。拷贝规则:
- 1,
基本类型拷贝其值,。
- 2,
实例对象,拷贝地址引用
,就是说此时新拷贝的对象与原有对象共享该实例变量,不受访问权限的限制。 - 3,
String字符串,拷贝的也是地址
,但是在修改时,会从字符串池(String Pool)中重新生成字符串,原有的字符串保持不变
1 | public class Person implements Cloneable{ |
44. 推存使用序列化实现对象的拷贝,
被拷贝的类只要实现Serializable接口,不需要任何实现,需要加上SerialVersionUID常量,使用需要注意:
- 1,
对象的内部属性都是可序列化的。
- 2,
注意方法和属性的特殊修饰符。
final,static变量的序列化问题会被 引入到拷贝对象中,瞬态变量(trtansient)不能进行序列化。可一采用Apache下的commons工具包中的SerializationUtils类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class CloneUtils {
//拷贝一个对象
//@SuppressWarnings。该批注的作用是给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默。
/*
* 关键字 用途
deprecation 使用了不赞成使用的类或方法时的警告
unchecked 执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型。
fallthrough 当 Switch 程序块直接通往下一种情况而没有 Break 时的警告。
path 在类路径、源文件路径等中有不存在的路径时的警告。
serial 当在可序列化的类上缺少 serialVersionUID 定义时的警告。
finally 任何 finally 子句不能正常完成时的警告。
all 关于以上所有情况的警告。
@SuppressWarnings 批注允许您选择性地取消特定代码段(即,类或方法)中的警告。其中的想法是当您看到警告时,
您将调查它,如果您确定它不是问题,您就可以添加一个 @SuppressWarnings 批注,以使您不会再看到警告。
虽然它听起来似乎会屏蔽潜在的错误,但实际上它将提高代码安全性,因为它将防止您对警告无动于衷 —
您看到的每一个警告都将值得注意。
*/
public static <T extends Serializable> T clone(T obj) {
//拷贝产生的对象
T clonedObj = null;
try {
//读取对象字节数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);//写操作
oos.close();//关闭流
//分配内存空间,写入原始对象,生成新对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
clonedObj = (T)ois.readObject();//读操作
ois.close();
}catch(Exception e) {
e.printStackTrace();
}
return clonedObj;
}
}
45. 覆写equals方法时不要识别不出自己,equals方法的自反原则(对于任何非空引用x,x.equals(x)应该返回true)
46. equals应该考虑null值情景。对称性原则,对于任何引用x和y的情形,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
47. 在equals中使用getClass进行类型判断,注意equals的传递性原则,对于实例对象s,y,z,如果如果s.equals(y)为true,y.equals(z)返回true,那么x.equals(z)也返回true。
48. 覆写equals方法必须覆写hashCode方法。
HashMap的底层处理机制是以数组的方式保存Map条目的,链表保存val,依据传入元素的hashCode方法返回的哈希值决定数组下标,如果该位置已有Map条目了,且与传入的键值相等则不要处理,若不相等则则覆盖,如果数组位置没有条目则插入。并加入到Map条目的链表中。即在检查相等时也是由哈希吗确定位置。
哈希码:由Object方法本地生成,确保每一个对象有一个哈希码(哈希算法,输入任意L,通过一定算法f(L),将其转化为非可逆的输出,一对一,多对一成立),重写hashCode方法:
1 | public int hashCode(){ |
49. 推存覆写toString方法,正常输出:类名+@+hashCode;
50. 使用package-info类为包服务:用于描述和记录包信息的,会被编译,不能声明类,不能继承,没有类关系,用于申明友好类和包内访问常量,为在包上标注一个注解(Annotation)提供方便,即将定义的注解写到这里,提供包的整体注释说明。
51. 不要主动进行垃圾回收。
第四章,字符串
52. 推存使用String直接量赋值,
Java为了避免在一个系统中产生大量的String对象,设计了一个字符串池(也称字符串常量池,String pool 或 String Constant Pool,存在于JVM常量池(Constant Pool)中),在字符串池中容纳的都是String对象,当创建一个字符串时,首先检查池中是否有字面值相等的字符串,有不在创建,直接返回池中该对象的引用,没有则创建,然后放到池中,返回新建对象的引用,所以当用“==”判断相等。当使用new String(“”) 时不会检查池,也不会放入池。不相等,intern方法(会检查池里,有返回)处理相等。String类是一个不可变(Immutable)的类,final不可继承,在String提供的方法中,如果返回字符串一般为新建的String对象,不对原对象进行修改。
53. 注意方法中传递的参数要求,replaceAll传递的是第一个参数为正则表达式。
54. 正确使用String ,StringBuffer , StringBuilder :
CharSequence接口有三个实现类:String , StringBuffer , StringBuilder ,
- String为不可改变量,修改要么创建新的字符串对象,要么返回自己(str.substring(0)),
- StringBuffer是一个可变字符序列,他与String一样,在内存中的都是一个有序的字符序列,不同点是值可以改变,
- StringBuilder 与 StringBuffer在性能上基本相同,都为可变字符序列。不同点为StringBuffer为线程安全的,而StringBuilder为线程不安全。
使用场景
:
- String:常量的声明,少量的变量运算。
- StringBuffer:频繁的字符串运算,多线程(xml,Http解析)。
- StringBuilder:频繁的字符串运算,单线程(SQL语句拼接)。
55. 注意字符串的位置:在+号处理中,String字符串具有最高的优先级。
56. 自由选择字符串拼接方式:
- 1,
“+”号拼接,时间最长
,等价于srt = StringBuilder(srt).append(“c”).toString(); - 2,
concat方法拼接,时间中等
,每次都会新建一个String对象; - 3,
append方法拼接字符串,时间最快,
只生成一个String对象。
57. 推存在复杂字符串操作中使用正则表达式来完成复杂处理。查找单词数,\b\w+\b
58. 强烈建议使用UTF编码。
59. 对字符串排序持一种宽容的心态,如果排序不是关键算法,用Collator类即可。
第五章,数组和集合
60. 性能考虑,数组是首选,集合类的底层都是通过数组实现的,基本类型在栈内存中操作的(速度快,容量小),而对象则是在堆内存中操作的(速度慢,容量大)。性能要求较高的时候用数组代替集合。
61. 若有必要,使用变长数组,
数组扩容:
1 | public class demo { |
集合的长度自动维护功能的原理与此类似。
62. 警惕数组的浅拷贝,数组的copyOf方法产生的数组为一个浅拷贝,与序列化的浅拷贝相同,基本类型拷贝值, 其他拷贝地址,集合的clone方法也为浅拷贝,
- 浅拷贝(浅复制、浅克隆):被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。 换言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
- 深拷贝(深复制、深克隆):被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深拷贝把要复制的对象所引用的对象都复制了一遍。继承自java.lang.Object类的clone()方法是浅复制。
63. 在明确的场景下,为集合指定初始容量,ArrayList中java实现长度的动态管理。如果不设置初始容量,系统就按照1.5倍的规则扩充,每一次扩充都是一次数组拷贝。
1 | public boolean add(E e) { |
vector的处理方式与ArrayList的长度处理相似,不同的地方是提供递增步长(capacityIncrement变量)。不设置容量翻倍。HashMap是按照倍数增加的。
64. 多种最值算法,适时选择,使用集合最简单,使用数组性能最优。
1 | public static int max(int [] data) { |
65. 避开基本类型数组转化为列表陷阱,原始数据类型数组不能做为asList的输入参数,否则会引起程序逻辑混乱。
1 | public static void main(String[] args ){ |
66. asList方法产生的List对象不可更给,
调用add方法会抛不支持的操作的异常,基于Arrays的ArrayList是一个静态私有内部类,除了Arrays能访问以外,其他类都不能访问,且add方法为ArrayList的父类提供,但是没有具体的实现,Arrays的内部类没有覆写add方法。
ArrayList静态内部类实现了5 个方法。s
ize(元素数量),toArray(转化为数组),get(获取指定元素),set(重置某一元素),container(是否包含某个元素),方法 ,没有实现add和remove方法,
即asList返回的为一个长度不可变的列表,数组为多长转换为列表为多长,即不在保持列表动态变长的特性。List
1 | package java_151; |
67.不同的列表选择不同的遍历方法,
对于 ArrayList数组for循环下标方式要比foreach遍历快,
对于LinkedList类讲,foreach方法要比fo循环方法快,
ArrayList数组为随机存取列表,LinkedList为有序存取列表(实现了双向链表,每个数据节点有三个数据项前节点引用(Previons),本节点元素(Node element),后继节点的引用(Next Node ))。ArrayList数组实现了RandomAccess接口(随机存取接口),即为一个随机存取的列表,数据元素之间没有关联,没有依赖和索引关系,
Java中,RandomAccess(随机存取)和Cloneble(可以拷贝)。Serializable(可以序列化)接口一样,为标志性接口,不需要任何实现,只是用来表明实现类具有某种性质,
java中foreach语句为iterator(迭代器)的变形使用
,即迭代器为23种设计模式的一种,提供一种方法访问一个容器中对象的各个元素,同时又无需暴露内部细节,对于ArrayList来讲,创建一个迭代器,然后屏蔽内部遍历细节,提供hasnext,next等方法,
1 | package java_151; |
68.频繁插入和删除时使用LinkedList,
LinkedList的插入效率要比ArrayList快 50倍,删除要比ArrayList快40倍,修改要慢ArrayList许多,ArrayList在写操作时要慢Lnkedlist;
69.列表相等只需关心元素数据,
在java中,列表只是一个容器,只要是同一种类型的容器(List),不会关心容器的细微差(ArrayList和Kinkedlist两者都实现了LIst接口继承了AbstractList抽象类,equals方法在抽象类中定义。),只要确定所有元素数据,个数相等即可,Set,Map与此相同。判断集合是否相等,只需要判断元素是否相等。
1 | public boolean equals(Object o) { |
70.子列表只是原列表的一个视图,
List提供一个subList方法,与String的subString有点类似,subList方法是由AbstractList实现的,它会根据是不是随机存储提供不同的实现方法,SubList返回的类也是AbstractList的子类,其所有的方法(get,add,set,remove等)都是在原始列表上操作的,它自身并没有生成一个数组或是链表,也就是子列表只是原列表的一个视图(View),所有的修改都反映在原列表上。
,
1 | List <String> c = new ArrayList<String>(); |
71.推存使用subList处理局部列表,
1 | List<Integer> initate = Collections.nCopies(100, 0); |
72.生成子列表后不要在操作原列表
,操作抛出java.util.ConcurrentModificationException并发修改异常,因为subLis取出的列表只是原列表的一个视图,原数据集修改了,但是subList取出的子列表不会重新生成一个新列表,后面的对子列表继续操作时,就会检测到修改计数器与预期的不相同,会抛出并发修改异常,subList的其他方法也会检测修改计数器,例如Set,get,add方法,如果生成子列表在操作原列表,必然会导致视图不稳定,有效的办法是通过Collections.unmodifiableList方法设置列表为只读状态当有多个字列表时,任何一个子列表就都不能修改啦。生成子列表后,原列表保持只读状态。防御式编程。
1 | List<String> list = new ArrayList<>(); |
73.使用Comparator进行排序,
在java中,要想给数据进行排序,有两种事项方式,一种为实现Comparable接口,一种是实现Comparator接口,
1 | public interface Comparable<T> |
此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法。实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort)进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。
int compareTo(T o)比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
1 |
|
比较功能,对一些对象的集合施加了一个整体排序 。 可以将比较器传递给排序方法(如Collections.sort或Arrays.sort ),以便对排序顺序进行精确控制。 比较器还可以用来控制某些数据结构(如顺序sorted sets或sorted maps ),或对于不具有对象的集合提供的排序natural ordering ,与Comparable不同,比较器可以可选地允许比较空参数,同时保持对等价关系的要求。
int compare(T o1, T o2) 比较其两个参数的顺序。
1 | package com.liruilong; |
74.不推存使用binarySearch对队列进行检索
。一般使用indexOf方法进行检索,binarySearch使用二分搜索法搜索指定列表,以获得指定的对象,二分法查询,数据要已经实现了升序排序。但是binarySearch在性能上要比indexOf好。
75.集合中的元素必须 做到compareTo和equals同步:实现了Comparable接口的元素就可以排序,compareTo方法是Comparable接口要求必须实现的。当返回0时,表示进行比较的两个元素是相等的,indexOf检索方法是通过equals方法判断的,binarySearch则依赖compare方法查找,不懂?????
76.集合运算时使用更优雅的方式,并集,交集,差集。
1 | List<String> list1 = new ArrayList<>(); |
77.使用shuffle打乱列表,
1 | int tagCloudNum = 10; |
78.减少HashMap中的元素的数量
HashMap在底层也是以数组的方式保存元素的,每一个键值对就是一个元素,HashMap把键值对封装为Entry对象,然后把Entry放到数组中,HashMap的底层数组变量为table,是Entry类型的数组,保存一个一个的键值对,HashMap也可以动态的增加,大于等于阈值,数组增大一倍,阈值为当前长度与加载因子的乘机,默认加载因子为 0.75,即HashMap的size大于等于数组长度的0.75倍,就开始扩容。
79.集合中的哈希码不要重复
随机存取的列表是遍历查找,顺序存储列表是链表查找,或者Collections的二分法查找,HashMap等set集合要快于List集合,HashMap每次增加元素都会先计算器哈希码,然后使用hash方法再次对hashCode进行抽取和统计,同时兼顾哈希码的高位和低位的信息产生唯一值,之后通过indexFor方法与数组长度做一次与运算,计算数组位置,hash的方法和iindexFor方法就是把哈希码转化为数组,
1 |
|
80.多线程使用Vector或HashTable,
Vector是ArrayList的多线程版本,HashTable是HashMap的多线程版本,线程安全和同步修改异常是两个概念,基本上所有集合类都有一个叫做快速失败的校验机制(Ffail-Fast),当一个集合在被多个线程修改并访问时,就可能会出现ConcurrentModificationException 异常,这是为了确保集合方法一致设置对的保护措施,实现原理为modCount修改统计器,当读取列表是发生变化(其他线程也在操作),则会抛出异常,也与 线程同步不同,线程同步是为了保护数据不被脏写,脏读而设置的,Vector的每个方法都加上了synchronized,两个线程进行同样的操作才可以讨论线程同步,一个线程删除一个线程增加,不属于多线程范畴。
1 | package java_151; |
81.非稳定排序推:
存使用List,Set与List的最大区别为Set中的元素不可以重复(基于equals的返回值),Set的实现类有一个比较常用的类TreeSet,该类实现了类默认排序为升序Set集合,插入一个元素,默认按照升序排序,SortedSet(TreeSet实现了该接口)接口只是定义了在给集合加入元素时将其进行排序,不能保证修改后的排序结果。所以TreeSet适合不变量的集合数据排序。Sstring或Integer等
82.由点几面,集合大家族:
`List:实现List的集合主要有ArrayList(动态数组),LinkedList(双向链表),Vector(线程安全的动态数组),Stack(对象栈,先进后出)。
Set: 不包含重复元素的集合,其主要的实现类有:EnumSet(枚举类型专用,所有为枚举类型),HashSet(以哈希码决定元素位置,与HashMap相似,提供快速插入和查找),TreeSet(自动排序Set,实现了SortedSet接口)
Map:分为排序Map和非排序Map,排序Map主要是TreeMap类,根据Key值进行自动排序,非排序Map主要包括:HashMap,HsahTable,Properties,EnumMap等,其中Properties是HashTable的子类,它的主要用途从property文件中加载数据,并提供方便的读写操作:EnumMap则要求Key必须为某一个枚举类型。Map中还有一个weakHashMap类,采用弱键方式实现的Map类,WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,即不用担心内存溢出问题。
Query:队列,分为阻塞式队列和非阻塞式队列,阻塞式队列主要包括:ArrayBlockingQuery(以数组方式实现的有借阻塞数组),PrinonityBoockingQuery(依照优先级组建的队列),LinkedBlockingQuery(通过链表实现的阻塞队列),非阻塞式队列,PrinonityQuery类.
数组:数组与集合的最大区别就是数组能够容纳基本类型,而集合不行,且数组为非动态,集合的底层都是数组。
工具类:数组的工具类时java.util.Arrays和java.lang.reflect.Array,集合的工具类是java.util.Collections.
第六章, 枚举和注解
83.推存使用枚举定义常量,
JLS(Java Language Specification,java语言规范),提倡枚举全部大写,枚举常量简单,属于稳定型,具有内置方法,可以自定义方法,
84.使用构造函数协助描述枚举项
枚举描述,通过枚举的构造函数,声明每个枚举项必须具有的属性的行为。
1 | package java_151; |
85.小心Switch带来的空值异常,
java中Switch可以判断byte,short,int,char即String类型,枚举也可以更在Sewitch后面,原因是switch先计算变量的排序值,然后与枚举常量的每个排序值进行对比,当变量为空时,调用ordinal方法报空指针异常。所以需要判断。switch(s) =s.ordinal;
1 | package java_151; |
86.在switch的default代码块中增加AssertionError错误。当有不存在枚举时需要。
87.使用valueOf前必须进行校验,
枚举类都是java.lang.Enum的子类,可以访问hashCode,name,valueOf等方法,valueOf会在枚举项中查找出字面值与参数相等的枚举项,当不存在时会报错。rllegalArrumentExcep。可以抛出异常
88.用枚举实现工厂方法模式更简洁。
:工厂模式(Factory Method Pattern)即创建对象的接口,让子类决定实例化哪一个类,并使一个类的实例化延迟到其子类,
1 | package java_151; |
使用枚举类的工厂方法模式,可以避免错误调用的发生,性能好,使用便捷,降低类的耦合性最少知识原则(一个对象应该对其他对象有最少的了解,LoD)。
89.枚举项的数量限制在64个以内
java提供个两个枚举项,EnumSet和EnumMap,Enum表示元素必须是某一枚举的枚举项,EnumMap表示Key值必须是某一枚举项,Java的处理机制当枚举项数量小于等于64时,创建一个RegularEnumSet实例对象,大于64小时则创建一个jumboEnumSet 实例对象。枚举项的排序值ordinal是从0.1.2.3……依次递增的,没有重号,没有跳号,RegularEnumSet利用这点吧每个枚举项的ordinal映射到一个long类型的每个位上,long类型为64 位,所以RegularEnumSet类型就只能负责不大于64位。
90.小心注解的继承,
1 | /** |
91.枚举与注解结合使用威力更强大。分析ACM(Access Control List,访问控制列表),ACM要求,资源,权限级别,控制器。
1 | package java_151; |
92.注意@Override不同版本的区别,在1.6版本中可以在实现接口中加上@Override,而以下版本父类必须是一个类,子类方法和父类方法必须具有相同的方法名,输入参数,输出参数(允许缩小),访问权限(允许类扩大)。
第七章,泛型和反射
泛型可减少强制类型的转换,规范集合的元素类型,提高代码的安全性和可读性,优先使用泛型
93.Java的泛型是类型擦除的
Java泛型(Generic)的引入加强了参数类型的安全性,减少了乐行的转化,Java泛型在编译期有效,在运行期被删除,即泛型参数类型在编译后都会被清除掉。
转化规则:
List< String>,List< Integer>,List
List< String>[],类型擦除后是List[];
List<? extends E>,List<? super E>擦除后的类型为List
List<T extends Serializable & Cloneable>擦除后为List< Serializable>
Java中泛型的class对象都是相同的,泛型数组初始化时不能声明泛型类型,instanceof不允许存在泛型参数,
即`List< String >[] listArray = new List< String>[];’编译不成功。
System.out.prinln(list instanceof List< String>);编译不通过
1 | public void arrayMethod(String[] strArray) {} |
94.不能初始化泛型参数和数组,
new T();,new T(5)
都不能通过,new ArrayList< T>()可以通过。ArrayList表面是泛型,其实已经在编译期转型为Object了,数组允许协变(Covariant),即可以容纳所有对象,类的成员变量是在类初始化前初始化的,所有要求在初始化前它必须具有明确的类型,否则则就只能申明,不能初始化。
1 | public class ArrayList<E> extends AbstractList<E> |
95.强制声明泛型的实际类型,强制类型转换,
List< Object >与List< Integer >没有继承关系,不能进行强制转换,强制声明泛型类型,List< Integer > list2 = ArrayUtils.< Integer >asList();即通过声明 强制确定类型。
96.不同的场景使用不同的泛型通配符,
Java泛型支持通配符,可以使用“?”,表示任意类,也可以使用extends 关键字表示某一个类的(接口)的子类型,可以使用super关键字表示某一个类(接口)的父类。
如果一个泛型结构即用作读操作,又用作写操作,使用确切的泛型类型即可。
- 在泛型结构中,只参与“读”操作则限定上界(extends):
1 | public static <E> void read(List<? extends list>) { |
- 泛型结构只参与”写“操作则限定下界(super)。
1 | public static void write(List<? super Number> lsit){ |
JDK的Collections.comp方法实现了把源列表中的所有元素拷贝到目标列表对应的索引上。
1 | public static <T> void copy(List<? super T> dest, List<? extends T> src) { |
97.警惕泛型是不能协变和逆变的。协变,即指窄类型替换宽类型的,逆变即宽类型覆盖窄类型。泛型不支持协变,但可以使用通配符(Wildcard)模拟协变。泛型不支持逆变,即不能把一个父类对象赋值给一个子类类型变量,可以使用super实现。
1 | //子类的doStuff()方法返回值的类型比父类方法要窄,即该方法为协变方法,也称多态。 |
带泛型参数的子类型定义
问 | 答 |
---|---|
Integer 是Number的子类型 | 对 |
ArrayList< Integer>是List< Integer>的子类型 | 对 |
Integer[] 是Number[]的子类型 | 对 |
List< Integer>是List< Number> | 的子类型 |
List< Integer> 是List<? extends Integer>的子类型 | 错 |
List< Integer>是List< ? super Integer>的子类型 | 错 |
98.建议采用的顺序为List< T>,List< ?>,List< Object>,原因为:
- 1,List< T >:表示List集合中的元素都为T类型,具体类型在运行期决定,List< ?>表示为任意类型,List< Object >表示为所以元素为Object类型,因为Object为所有类型的父类,所以List< Objerct>可以容纳所有 的类型。
- 2,List
:可以进行读写操作,add或remove等操作,因为是固定的T类型,在编码期不需要进行类型转化。 - 3,List< ? >:是只读类型,不能进行增加修改操作,因为编译器不知道List中容纳的是什么类型的元素,而且读取的类型为Object类型,需要主动转型,所以经常采用泛型方法的返回值,可以执行删除类型,因为删除动作与泛型类型无关。
- 4,List< Object>也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据时需要向下转型(Docwncast),
99.严格限定泛型类型采用多重界限,
使用“&”设定多重边界(Multi Bounds),在java中的泛型中,可以使用 & 符号关联多个上界并实现多个边界的限定,只有上界才有限定,下界没有多重限定的情况,指定泛型类型T必须为Staff和passenger的共有的子类型,此时变量t就具有了所以限定的属性和方法。
1 | interface staff{ |
100.数组的真实类型必须是泛型类型的子类型,
List接口的toArray()方法可以将一个集合转换为一个数组,返回的是一个Object数组,所以需要自行转变,yoArray(T[] a)虽然返回的事T类型的数组,但是还需要传入一个T类型的数组。
1 | package java_151; |
类型转化异常,不能把一个Object数组转换为一个String数组,数组是一个容器,只有确保容器内的所有元素与期望的类型有父子关系时才能转换,Object数组只能保证数组内的元素是Object类型,却不能保证他们都是String的父类型,所以转换失败。为什么在main方法中抛出异常,因为泛型是类型擦除的,toArray方法经过编译后的代码。
1 | public static Object[] toArray(List<T> list) { |
当一个泛型类(泛型集合)转变为泛型数组时,泛型数组的真实类型不能是泛型类型的父类型,只能是泛型类型的子类型,否则就会出现异常。
101.注意Class类的特殊性:
Java语言把Java源文件编译为后缀为class的字节码文件,然后通过ClassLocale机制把类文件加载到内存中,最后生成实例执行,Java使用元类(MetaClass)来描述加载到内存中的类数据,即Class类,描述类的类对象,
- 无构造函数,不能主动实例化,Class对象在加载时由java虚拟机通过类加载器中的defineClass自动构造。
- 可以描述基本类型
Class as=int.class;
8个基本类型执行JVM中并不是一个对象,一般存在于栈中,通过Class可以描述它们,可以使用int.calss
描述int类型的类对象。 - 对象都是单例模式,一个Class对象描述一个类,只描述一个类,即一个类只有一个Class对象。
Class是java 的反射入口,只有在获得一个类的动态描述时才能动态的加载调用。获得Class对象有三种方法,类属性方法,对象的getClass方法,forName()方法。
102.适时选择getDeclaredXXX和getXXX;
getDeclaredMethod方法获得的是所有public访问级别的方法,包括从父类继承来的方法,而getDeclareMethod获得自身类的所有方法,包括公有的(public),私有(private),方法等,不受访问权限限制。如果需要列出所有继承自父类的方法,可以先获得父类,然后使用getDeclareMethods,之后持续递归。
103.反射访问属性或方法时将Accessible设置为true,
java中通过反射执行方法的步骤,获取一个对象的方法,然后根据isAccessible返回值确定是否能执行,如果返回false,则需要调用`setAccessible(true),在调用invoke执行方法。
Access并不是语法层次理解的访问权限,而是指是否更容易获得,是否进行安全检查。动态的修改一个类或方法或执行方法时都会受到Java安全体系的制约,而安全处理非常消耗资源,所以对于运行期要执行的方法或修改的属性就提供了Accessible可选项,由开发者决定是否要逃避安全体系的检查。
AccessibleObject是field,Method,constructor的父类,决定其是否可以快速访问而不进行访问控制检查,AccessobleObject类中是以override变量保存该值的。
1 | Method method= genericDemo.class.getMethod("toArray"); |
Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,可以提升系统性能,AccessibleObject的其他两个子类field和 constructor也相似,所以要设置Accessible为true。
104.使用forName动态加载类文件,动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,对Java程序来说,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时决定是否要加载一个类,一个类文件只有在被加载到内存中才可能生成实例对象,即加载到内存中,生成Class对象,通过new关键字生成实例对象。
105.动态加载不适合数组,当使用forName加载一个类时,8个基本类型排除,它不是一个具体的类,还要具有可追索的类路径,否则包ClassNotFoundException异常。数组虽然是一个类,但没有定义类路径,可以加载编译后的对象动态动态加载一个对象数组,但是没有意义。在java中数组是定长的,没有长度的数组是不允许存在的。可以使用Array数组反射类来动态加载一个数组。
1 | //动态创建一个数组 |
元素类型 | 编译后的类型 |
---|---|
byte[] | [B |
char[] | [C |
Double[] | [D |
Float[] | [F |
Int[] | [I |
Long[] | [J |
Short[] | [S |
Boolean | [Z |
引用类型(如String) | [L引用类型 |
106.动态可以让代理模式更灵活,java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标对象生成代理,静态代理:通过代理主题角色和具体主题角色共同实现抽象主题角色的逻辑的,只是代理主题角色把相关的执行逻辑委托给了具体主题角色而已。
1 | interface subject{ |
java基于java.lang.reflect.Proxy用于实现动态代理,使SubjectHandler作为主要的逻辑委托对象,invoke是必须要实现的,完成对真实方法的调用。即通过InvocationHandler接口的实现类来实现,所有被代理的方法都是由InvocationHandler接管实际的处理任务。
1 | interface subject{ |
107.使用反射增加装饰模式的普遍性,装饰模式:动态的给一个对象添加一些额外的职责。使用动态代理可以实现装饰模式。
1 | //动物 |
108.反射让模板方法模式更强大,模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可定义该算法的某些特定的步骤,即父类定义抽象模板为骨架,其中包括基本方法(由子类实现的方法,并且在模板方法被调用)和模板方法(实现对基本方法的调用,完成固定的逻辑)
1 | public abstract class AbsPopulator{ |
使用反射后,不需要定义任何抽象方法,只需要定义一个基本的方法鉴别器,即可加载否和规则的基本方法,模板方法根据鉴别器返回执行相应的方法。
109.不需要太多的关注反射效率,
1 | class Utils{ |
第八章 异常
110.提倡异常的封装,可以提供按系统的友好性,提高系统的可维护性,对异常进行分类,解决Java异常机制缺陷。进行异常封装。
1 | class MyException extends Exception{ |
111.采用异常链传递异常,设计模式中有一种叫责任链意识,它的目标是将多个对象连成一条链,并沿着这条链传递该请求,直到有对象处理为止,即先封装,然后传递,把FileNotFoundException封装为MyException,抛到逻辑层,逻辑层根据异常代码确定后续的处理,然后抛出到视图层,在视图层,如果为管理员展现低层次异常,如果为普通用户则展现为封装后异常。
112.受检异常尽可能转换为非受检异常。
113.不要在finally块中处理返回值,
- 会覆盖try块中的返回值(当没有定义变量时,会先返回try中的返回值,然后执行finally会重置返回值),当定义变量时,不会重置try的返回值,异常代码加上try语句就标志着运行时会有一个Throwable线程监视该方法的运行,当出现异常时,交由异常逻辑处理,方法在栈内存中运行的,会按照“先进后出”的原则执行,main方法调用异常方法,main方法在下层,异常方法在上层,当异常方法执行完毕
return a
后,此方法的返回值以确定为固定值(基本类型为值拷贝),此后finally代码块修改已经没有意义(类似值传递),当为引用类型时,因为是地址拷贝,所以会改变。 - 屏蔽异常,无法捕捉到异常,当异常线程在监视到有异常发生时,就会登记当前的异常类型为DateFormatException,但当执行器执行finally语句时,则会重新为doSome方法赋值,告诉调用者没有异常产生,返回值为1。与return语句类似的System.exit(0)或Runtime().exit(0)出现在异常代码块也会产生异常。不要在finally块中出现return语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14public static void doSome(){
try{
throw new RuntimeException();
}finally {
return
}
}
public static void main(String[] args) {
try{
doSome();
}catch (RuntimeException e){
System.out.println("这里永远不会到达!!");
}
}
114.不要在构造函数中抛出异常,java的异常机制有三种,
- Erroe类及其子类表示为错误,它是不需要程序员处理也不能处理的异常,比如VirtualMachineError虚拟机错误,Thread线程僵死等。
- RuntimeException类及其子类表示非受检异常,是系统可能会抛出的异常,编译器不要求强制处理的异常,常见的有NullPointException异常和IndexOutBoundException越界异常,
- Exception类及其子类中除RuntimeException异常,表示受检异常,是程序员必须处理的异常,不处理程序不能通过,IOException和SQLException异常。
一个对象的创建要经过内存分配,静态代码初始化,构造函数执行等过程,一般不在构造函数中抛出异常,是程序员无法处理的,会加重上层代码的编写者的负担,后续代码不会执行,违背了里氏替换原则,父类能出现的地方子类就可以出现,而且将父类替换为子类也不会产生任何异常, java的构造函数允许子类的构造函数抛出更广泛的异常类(正好与类方法的异常机制相反:子类方法的异常类型必须为父类方法的子类型,覆写要求),当替换时,需要增加catch块,构造函数没有覆写的概念,只有构造函数之间的引用调用而已, - 子类构造函数扩展受限。
115.使用Throwsable获得栈信息,AOP编程可以亲松的控制一个方法调用那些类,也能控制那些方法允许别调用,一般来讲切面编程只能控制到方法级别,不能实现代码级别的植入(Weave),即不同类的不同方法参数相同调用相同方法返回不同的值。即要求被调用者具有识别调用者的能力,可以使用Throwable获得栈信息,然后鉴别调用者信息。
JVM在创建一个Throwable类及其子类时会把当前线程的栈信息记录下来,以便在输出异常时准确定位异常原因,在出现异常时,JVM会通过fillInStackTrace(填写执行堆栈跟踪。)方法记录下栈帧的信息,然后生成一个Throwable对象,可以知道类间的调用顺序,方法名称及当前行号。
1 | class Foo{ |
116.异常只为异常服务,即异常块里不添加逻辑,添加逻辑,会造成异常判断降低了系统性能,降低了代码可读性,隐藏了运行期可能产生的异常
117.多使用异常,把性能放一边,异常是主逻辑的例外逻辑,比如 在马路上走路(主逻辑),突然开过一辆车,我要避让(受检异常,必须处理),继续走路,突然一架飞机从我头顶飞过(非受检异常),我可以选择走路(不捕捉),也可以选择职责其噪音污染(捕捉,主逻辑的补充处理),在继续走下去,突然一棵流星砸下类,这是没有选择,属于错误,不能做任何处理。
1 | public void Login(){ |
118.不推存覆写start方法.
119.启动线程前stop方法是不可靠的。
120.不要使用stop方法停止线程。
121.线程优先级只是用三个等级,priority只是表示线程获得CPU运行的机会,不代表强制符号。优先级相同,由操作系统决定,基本上按照FiFO(先入先出)原则,不能完全保证。
122.使用线程异常处理器提升系统可靠性,在java1.5版本后在Thread类中增加了setUncaughhtExceptionHandle方法,实现了异常的捕捉和处理,
1 | class Tcpserver implements Runnable { |
第九章 线程安全
123,volatile不能保证数据同步,
每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory),比如寄存器Register,高速缓存存储器Cache等,线程的计算一般是通过工作内存进行交互的,线程在初始化时从主内存中加载所需要的变量值到工作内存中,然后在线程运行时,如果读取内存,则直接从工作内存中读取,若是写入则先写入到工作内存中,之后在刷新到主内存中,在多线程情况下,可能读到的不是最新的值,可以使用synchronized同步代码块,或使用Lock锁来解决该问题,java可以使用volatile解决,在变量前加volatile关键字,可以保证每个线程对本地变量的访问和修改都是直接与主内存交互的,而不是与本线程的工作内存交互。但是Volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值能够获得最新的值,而不能保证多个线程修改的安全性。
124.异步运算考虑使用Callable接口:
多线程应用有两种实现方式,一种是实现Runnable接口,另一种是继承Thread类,run方法没有返回值。不能抛出异常,使用Callable可以实现多线程任务, Executors 是静态工具类,提供异步执行器的创建能力,一般是异步计算的入口类,Future关注的是线程执行后的 结果。
public class Executors extends Object
工厂和工具方法Executor , ExecutorService , ScheduledExecutorService , ThreadFactory和Callable在此包中定义的类。 该类支持以下几种方法:
创建并返回一个ExecutorService设置的常用的配置设置的方法。
创建并返回一个ScheduledExecutorService的方法, 其中设置了常用的配置设置。
创建并返回“包装”ExecutorService的方法,通过使实现特定的方法无法访问来禁用重新配置。
创建并返回将新创建的线程设置为已知状态的ThreadFactory的方法。
创建并返回一个方法Callable出的其他闭包形式,这样他们就可以在需要的执行方法使用Callable 。
public interface Future
表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由 cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。
boolean cancel(boolean mayInterruptIfRunning) | 试图取消对此任务的执行。 |
V get() | 如有必要,等待计算完成,然后获取其结果。 |
V get(long timeout, TimeUnit unit) | 如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 |
boolean isCancelled() | 如果在任务正常完成前将其取消,则返回 true。 |
boolean isDone() | 如果任务已完成,则返回 true |
1 | class TaxCallabletor implements Callable<Integer>{ |
126,.适时选择不同的线程池来实现,java线程池实现从根本讲,ThreadPoolExecuto类和ScheduleThreadPoolException类。还是父子关系。
线程池的优点:
重用线程池中的线程,减少因对象创建,销毁所带来的性能开销;
能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;
能够多线程进行简单的管理,使线程的使用简单、高效。
线程池框架Executor
java中的线程池是通过Executor框架实现的,Executor 框架包括类:Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable和Future、FutureTask的使用等。
Executor: 所有线程池的接口,只有一个方法。
1 | public interface Executor { |
ExecutorService: 增加Executor的行为,是Executor实现类的最直接接口。
Executors: 提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService 接口。
ThreadPoolExecutor:线程池的具体实现类,一般用的各种线程池都是基于这个类实现的。
构造方法如下:
1 | public ThreadPoolExecutor(int corePoolSize, |
参数介绍:
corePoolSize:线程池的核心线程数,线程池中运行的线程数也永远不会超过 corePoolSize 个,默认情况下可以一直存活。可以通过设置allowCoreThreadTimeOut为True,此时 核心线程数就是0,此时keepAliveTime控制所有线程的超时时间。
maximumPoolSize:线程池允许的最大线程数;
keepAliveTime: 指的是空闲线程结束的超时时间;
unit :是一个枚举,表示 keepAliveTime 的单位;
workQueue:表示存放任务的BlockingQueue<Runnable队列。
BlockingQueue:阻塞队列(BlockingQueue)是java.util.concurrent下的主要用来控制线程同步的工具。如果BlockQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒。同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间才会被唤醒继续操作。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。具体的实现类有LinkedBlockingQueue,ArrayBlockingQueued等。一般其内部的都是通过Lock和Condition(显示锁(Lock)及Condition的学习与使用)来实现阻塞和唤醒。
3.线程池的工作过程
线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
当调用 execute() 方法添加一个任务时,线程池会做如下判断:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。
当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
4.线程池的创建和使用
生成线程池采用了工具类Executors的静态方法,以下是几种常见的线程池。
1)SingleThreadExecutor:单个后台线程 (其缓冲队列是无界的)
1 | public static ExecutorService newSingleThreadExecutor() { |
创建一个单线程的线程池。这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2)FixedThreadPool:只有核心线程的线程池,大小固定 (其缓冲队列是无界的) 。
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3)CachedThreadPool:无界线程池,可以进行自动线程回收。
1 | public static ExecutorService newCachedThreadPool() { |
如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue是一个是缓冲区为1的阻塞队列。
4)ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
1 | public static ExecutorService newScheduledThreadPool(int corePoolSize) { |
5.线程池实现的原理
线程池的实现过程没有用到Synchronized关键字,用的都是volatile,Lock和同步(阻塞)队列,Atomic相关类,FutureTask等等,因为后者的性能更优。理解的过程可以很好的学习源码中并发控制的思想。
在ThreadPoolExecutor主要Worker类来控制线程的复用。看下Worker类简化后的代码,这样方便理解:
1 | private final class Worker implements Runnable { |
Worker是一个Runnable,同时拥有一个thread,这个thread就是要开启的线程,在新建Worker对象时同时新建一个Thread对象,同时将Worker自己作为参数传入TThread,这样当Thread的start()方法调用时,运行的实际上是Worker的run()方法,接着到runWorker()中,有个while循环,一直从getTask()里得到Runnable对象,顺序执行。getTask()又是怎么得到Runnable对象的呢?
1 | private Runnable getTask() { |
这个workQueue就是初始化ThreadPoolExecutor时存放任务的BlockingQueue队列,这个队列里的存放的都是将要执行的Runnable任务。因为BlockingQueue是个阻塞队列,BlockingQueue.take()得到如果是空,则进入等待状态直到BlockingQueue有新的对象被加入时唤醒阻塞的线程。所以一般情况Thread的run()方法就不会结束,而是不断执行从workQueue里的Runnable任务,这就达到了线程复用的原理了。
控制最大并发数:
那Runnable是什么时候放入workQueue?Worker又是什么时候创建,Worker里的Thread的又是什么时候调用start()开启新线程来执行Worker的run()方法的呢?有上面的分析看出Worker里的runWorker()执行任务时是一个接一个,串行进行的,那并发是怎么体现的呢?
很容易想到是在execute(Runnable runnable)时会做上面的一些任务。看下execute里是怎么做的。
1 | public void execute(Runnable command) { |
根据代码再来看上面提到的线程池工作过程中的添加任务的情况:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
线程知识参考知乎:https://zhuanlan.zhihu.com/p/33725107;
127.Lock与Synchronized是不一样的
对于同步资源来讲,显示锁是对象级别的锁,而内部锁是类级别的锁,Lock定义为多线程类的私有属性是起不到资源互斥的作用的,除非包Lock定义为所以线程的共享变量。Lock是无阻塞锁,synchronized是阻塞锁,Lock可实现公平锁,synchronized只能为非公平锁,Lock是代码级的,synchronized是JVM级的。
128.预防线程死锁,
ThreadPoolExecutor提供了四个构造方法:
我们以最后一个构造方法(参数最多的那个),对其参数进行解释:
1 | public ThreadPoolExecutor(int corePoolSize, // 1 |
序号 名称 类型 含义
1 corePoolSize int 核心线程池大小 2 maximumPoolSize int 最大线程池大小
3 keepAliveTime long 线程最大空闲时间 4 unit TimeUnit 时间单位
5 workQueue BlockingQueue
线程等待队列 6 threadFactory ThreadFactory 线程创建工厂
7 handler RejectedExecutionHandler 拒绝策略
如果对这些参数作用有疑惑的请看 ThreadPoolExecutor概述。
知道了各个参数的作用后,我们开始构造符合我们期待的线程池。首先看JDK给我们预定义的几种线程池:
一、预定义线程池
FixedThreadPool
1 | public static ExecutorService newFixedThreadPool(int nThreads) { |
corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
keepAliveTime = 0 该参数默认对核心线程无效,而FixedThreadPool全部为核心线程;
workQueue 为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。是其劣势;
FixedThreadPool的任务执行是无序的;
适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。
CachedThreadPool
1 | public static ExecutorService newCachedThreadPool() { |
corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
keepAliveTime = 60s,线程空闲60s后自动结束。 workQueue 为 SynchronousQueue
同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;
适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求时,可使用CachedThreadPool。
SingleThreadExecutor
1 | public static ExecutorService newSingleThreadExecutor() { |
dome来解释一下:
1 | public static void main(String[] args) { |
对比可以看出,FixedThreadPool可以向下转型为ThreadPoolExecutor,并对其线程池进行配置,而SingleThreadExecutor被包装后,无法成功向下转型。因此,SingleThreadExecutor被定以后,无法修改,做到了真正的Single。
ScheduledThreadPool
1 | public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { |
newScheduledThreadPool调用的是ScheduledThreadPoolExecutor的构造方法,而ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,构造是还是调用了其父类的构造方法。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
二、自定义线程池
以下是自定义线程池,使用了有界队列,自定义ThreadFactory和拒绝策略的demo:
1 |
|
嗯,好累,因为要准备一些考试,这本书就刷到这里啦!哎,小秘密被发现的感觉真不爽。加油生活!!^ _ ^ !!
2019.3.17
《编写高质量代码(改善Java程序的151个建议)》读书笔记
https://liruilongs.github.io/2018/11/22/Java/《编写高质量代码(改善Java程序的151个建议)》读书笔记/