- 1 Java开发中通用的方法和准则
- 建议1:不要在常量和变量中出现易混淆的字母
- 建议2:莫让常量蜕变成变量
- 建议3:三元操作符的类型务必一直
- 建议4:避免带有长参数的方法重载
- 建议5:别让null值和空值威胁到边长方法
- 建议6:覆写变长方法也循规蹈矩
- 建议7:警惕自增的陷阱
- 建议8 不要让旧语法困扰你
- 建议9 少用静态导入
- 建议10 不要在本类中覆盖静态导入的变量和方法
- 建议11 养成良好习惯,显示声明UID
- 建议12 避免用序列化类在构造函数中为不变量赋值
- 建议13 避免为final变量复杂赋值
- 建议14 使用序列化类的私有方法巧妙解决部分属性持久化问题
- 建议15 break万万不可忘
- 建议17 慎用动态编译
- 建议18 避免instanceof非预期结果
- 建议19 断言绝对不是鸡肋
- 建议20 不要只替换一个类
- 2 基本类型
- 3 类、对象及方法
- 4 字符串
- 5 数组和集合
- 建议60 性能考虑,数组是首选
- 建议61 若有必要,使用变长数组
- 建议65 避开基本类型数组转换列表陷阱
- 建议68 频繁插入和删除时使用LinkedList
- 建议70 子列表只是原列表的一个视图
- 建议71 推荐使用subList处理局部列表
- 建议72 生成子列表后不要再操作原列表
- 建议73 使用Comparator进行排序
- 建议74 不推荐使用binarySearch对列表进行检索
- 建议75 集合中的元素必须做到compareTo和equals同步
- 建议76 集合运算时使用更优雅的方式
- 建议77 使用shuffle打乱列表
- 建议78 减少HashMap中的元素的数量
- 建议79:集合中的哈希码不要重复
- 建议80 多线程使用Vector和HashTable
- 建议81 非稳定排序推荐使用List
- 建议82 集合大家族
改善Java程序的151个建议
1 Java开发中通用的方法和准则
建议1:不要在常量和变量中出现易混淆的字母
字母”I”作为长整型标志时务必大写。
建议2:莫让常量蜕变成变量
务必让常量的值在运行期保存不变
建议3:三元操作符的类型务必一直
建议4:避免带有长参数的方法重载
建议5:别让null值和空值威胁到边长方法
public class Client {public void methodA(String str, Integer... is) {}public void methodA(String str, String... strs) {}public static void main(String[] args) {final Client client = new Client();client.methodA("China", 0);client.methodA("China", "people");client.methodA("China");client.methodA("China", null);}}

建议6:覆写变长方法也循规蹈矩
在Java中,子类覆写父类的方法很常见,这样做既可以修正Bug也可以提供扩展的业务功能支持,同时还符合开闭原则。覆写必须满足的条件。
- 重写的方法不能缩小访问权限
- 参数列表必须与被重写方法相同
- 返回类型必须与被重写方法的相同或是其子类
- 重写方法不能抛出异常,或者超出父类范围的异常,但是可以抛出更少,更有限的异常,或者不抛出异常。
class Base {void fun(int price, int... discounts) {System.out.println("Base.fun");}}//子类 覆写父类方法class Sub extends Base {@Overridevoid fun(int price, int[] discounts) {System.out.println("Sub.fun");}}///public static void main(String[] args) {//向上转型final Base base = new Sub();base.fun(100,50);//不转型final Sub sub = new Sub();sub.fun(100,50);}
base对象把子类的对象Sub向上转型,形参列表是由父类决定的。由于是变长参数,在编译时,“base.fun(100,50)”中的“50”这个实参会被编译器猜测而编译成“{50}”数组,再由子类sub执行。再看直接调用子类的情况,这是编译器并不会把50做类型转换,因为数组本身是一个对象,编译器还没有聪明到两个没有继承关系的类之间做转换,要知道Java是严格要求的类型匹配的,类型不匹配编译器自然就会拒绝,并给于错误提示。
建议7:警惕自增的陷阱
public class Client007 {public static void main(String[] args) {int count = 0;for (int i = 0; i < 10; i++) {count = count++;}System.out.println(count);}}
count++是一个表达式,是有返回值的,它的返回值就是count自加前的值。程序第一次循环是的详细处理步骤如下
- 步骤1 Java把count值拷贝到临时变量区
- 步骤2 count值加1,这个时候count的值是1
- 步骤3 返回临时变量区的值,注意这个值是0,没有修改过
- 步骤4 返回值赋给count,此时count被重置为0
建议8 不要让旧语法困扰你
public class Clint008 {public static void main(String[] args) {//数据定义以初始化int free = 200;// 其他业务saveDefault:save(free);}static void saveDefault() {}static void save(int free) {}}
代码没有编译错误,其实就是goto语法。
建议9 少用静态导入
对于静态导入,一般要遵循两个规则
- 不要使用通配符,除非是导入警惕爱常量类(只包含常量的类或接口)
- 方法名是具有明确,清晰表象意义的工具类
建议10 不要在本类中覆盖静态导入的变量和方法
建议11 养成良好习惯,显示声明UID
类实现Serializable接口的目的是为了可持久化,比如网络传输或本地存储,为系统分布和易购部署提供先决支持条件。若没有序列化,我们熟悉的远程调用、对象数据库都不可能存在。
通过SerialVersionUID,也叫做流标识符,即类的版本定义,它可以显示声明,也可以隐式声明,显示声明如下:
private static final long serialVersionUID = 1L;
建议12 避免用序列化类在构造函数中为不变量赋值
反序列化的一个规则:反序列化时构造函数不会执行
建议13 避免为final变量复杂赋值
public class Person implements Serializable {private static final long serialVersionUID = 1L;//通过方法返回值为final变量赋值public final String name = initName();private String initName() {return "混世魔王";}}
反序列化时final变量在一下情况下不会被重新赋值:
- 通过构造函数为final变量赋值
- 通过方法返回值为final变量赋值
- final修饰的属性不是基本类型
建议14 使用序列化类的私有方法巧妙解决部分属性持久化问题
部分属性持久化问题看似简单。只需要把不需要持久化的属性加上瞬态关键字(transient关键字)即可。这是一种解决方案,但有时行不通。例如一个计税系统和人力资源系统(HR系统)通过RMI对接,计税系统需要从HR系统获得人员的姓名和基本工资,以作为纳税的依据,而HR系统的工资分为两部分:基本工资和绩效工资,但绩效工资却是秘密。不能泄漏。
@Getter@Setterpublic class Salary implements Serializable {private static final long serialVersionUID = 1L;//基本工资private int basePay;//绩效工资private int bonus;public Salary(int basePay, int bonus) {this.basePay = basePay;this.bonus = bonus;}}@Setter@Getterpublic class Person implements Serializable {private static final long serialVersionUID = 1L;//姓名private String name;//薪水private Salary salary;public Person(String name, Salary salary) {this.name = name;this.salary = salary;}}public class Serialize {public static void main(String[] args) {final Salary salary = new Salary(1000, 2500);final Person person = new Person("张三", salary);//HR系统持久化,并传递到计税系统SerializationUtils.writeObject(person);}}public class Deserialize {public static void main(String[] args) {//计税系统反序列化 ,并打印信息final Person p = (Person) SerializationUtils.readObject();final StringBuffer sb = new StringBuffer();sb.append("姓名" + p.getName());sb.append("\t基本工资" + p.getSalary().getBasePay());sb.append("\t绩效工资" + p.getSalary().getBonus());System.out.println(sb);}}
但是上面代码不符合需求,因为计税系统只能从HR系统中获得人员的姓名和基本工资,不能获得绩效工资。解决方案
- 在bouns前加上transient关键字:这是一个方法,但不是一个好方法,加上此关键字就标志着Salary类失去了分布式部署的功能,它可是HR系统最核心的类了,一旦遭遇性能瓶颈,想在实现分布式部署就不可能了。
- 新增业务对象:增加一个Person4Tax类,完全为计税系统服务,就是说他只有两个属性,姓名和基本工资。完全符合开闭原则,而且对原系统也没有侵入性,只是增加了工作量而已。这是个方法但不是最优方法。
- 请求端过滤:
- 变更传输锲约:
@Setter@Getterpublic class Person implements Serializable {private static final long serialVersionUID = 1L;//姓名private String name;//薪水private transient Salary salary;//序列化委托方法private void writeObject(java.io.ObjectOutputStream out) throws IOException {out.defaultWriteObject();out.writeObject(salary.getBasePay());}//反序列化时委托方法private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {in.defaultReadObject();salary = new Salary(in.readInt(), 0);}public Person(String name, Salary salary) {this.name = name;this.salary = salary;}}
建议15 break万万不可忘
case语句后面随手写上break;
建议17 慎用动态编译
建议18 避免instanceof非预期结果
public class Client018 {public static void main(String[] args) {//String对象是否是Object的实例boolean b1 = "String" instanceof Object;//String对象是否是String的实例boolean b2 = new String() instanceof String;boolean b3 = new Object() instanceof String;boolean b4 = 'A' instanceof Character;boolean b5 = null instanceof String;boolean b6 = (String) null instanceof String;boolean b7 = new Date() instanceof String;}}
new Object() instanceof String;
返回时false,但是可以编译通过,只要instanceof关键字的左右两个操作数有继承或者实现关系,就可以编译通过
'A' instanceof Character;
编译通不过,因为’A’是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于判断对象,不能用于基本类型的判断
null instanceof String;
这是instance特有的规则:若左操作数是null,结果就直接返回false,不再运行右操作时是什么类
(String) null instanceof String;
虽然有强制类型转换,null是一个万用类型,也可以说没有类型,即使做类型转换还是null
new Date() instanceof String;Date和String没有继承或者实现关系。所以编译时就会直接报错。
建议19 断言绝对不是鸡肋
建议20 不要只替换一个类
public class Constant {//定义人类寿命极限public final static int MAX_AGE=150;}public class Client020 {public static void main(String[] args) {System.out.println("人类寿命极限是:" + Constant.MAX_AGE);}}
对应final修饰的基本类型和String类型,编译器会认为它们是稳定态,所以在编译时就直接把值编译到字节码中了,避免了在运行期引用,以提高代码的执行效率,上诉例子中,Client类在编译时,字节码总就写上了150这个常量,而不是一个地址引用,因此无论后续怎么修改常量类,只要不重新编译Client类,输出依旧。
而对于final修饰的类。编译器认为它是不稳定太,在编译时建立的则是引用关系。如果client类引入的常量是一个类或者实例,即使不重新编译也会输出最新值。
2 基本类型
建议21 用偶判断,不用奇判断
建议22 用整数型类型处理货币
建议23 不要让类型默认转型
建议24 边界,边界,还是边界
某商家生成的电子产品非常畅销,需要提前30天才能抢到手,同时它还规定了一个会员可拥有的最多产品数量,目的是防止囤积呀货肆意加价。会员的预定过程是这样的:先登录官网网站,选择产品类型,然后设置需要预定的数量,提交,符合规则则提示下单成功,不符合规则提示下单失败,后台模拟处理如下:
public class Client022 {
//一个会员拥有产品的最多数量
private final static int LIMIT=2000;
public static void main(String[] args) {
//会员当前拥有的产品数量
int cur=1000;
final Scanner input = new Scanner(System.in);
System.out.println("输入需要预定的数量:");
while (input.hasNextInt()){
final int order = input.nextInt();
if (order>0&order+cur<=LIMIT){
System.out.println("你已经成功预定的"+order+"个产品");
}else {
System.out.println("超过限额,预定失败");
}
}
}
}
当输入2147483647的时候,竟然预定成功了。
这是int类型的最大值。其加上1000,结果超出范围已经是负数了:数字越界使校验条件失效。
建议25 不要让四舍五入亏了一方
public class Clinet025 {
public static void main(String[] args) {
//11
System.out.println("10.5近视值" + Math.round(10.5));
//-10
System.out.println("-10.5近视值" + Math.round(-10.5));
}
}
绝对值相同的两个数字,近似值为什么就不同呢?这是由Math.round采用的舍入规则锁决定的(采用的是正无穷向舍入规则)。四舍五入是由误差的,其误差是舍入位的一半。
银行家舍入的近似算法,其规则如下:
- 舍去位的数值小于5时,直接舍去
- 舍去位的数值大于等于6时,进位舍去
- 当舍去位的数值等于5时,分两种情况:5后面还有其他数字(非零),则进位后舍去,若5后面是0,则根据5前一个数的奇偶性来判断是否需要进位,技术进位,偶数舍去。
以上规则汇总一句话:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
rount(10.5551)=10.56
rount(10.555)=10.56
rount(10.545)=10.54
public static void main(String[] args) {
//存款
BigDecimal d = new BigDecimal("888888");
//月利率 乘3计算季利率
BigDecimal r = new BigDecimal(0.001875 * 3);
//银行家舍入法
final BigDecimal i = d.multiply(r).setScale(2, RoundingMode.HALF_EVEN);
}
目前Java支持一下七种舍入方式
- BigDecimal.ROUND_UP
- ROUND_DOWN
- ROUND_CEILING
- ROUND_FLOOR
- ROUND_HALF_UP
建议26 堤防包装类型的null值
建议27 谨慎包装类型的大小比较
**
建议28 优先使用整型池
通过包装类的valueOf生成包装实例可以显著提高空间和时间性能。
**
建议29 优先选择基本类型
建议30 不要随便设置随机种子
3 类、对象及方法
建议31 在接口中不要存在实现代码
建议32 静态变量一定要先声明后赋值
public class Clinet32 {
//位置1
public static int i = 1;
static {
i = 100;
}
// 位置2
// public static int i=1;
public static void main(String[] args) {
System.out.println(i);
}
}
建议33 不要覆写静态方法
建议34 构造函数尽量简化
建议35 避免在构造函数中初始化其他类
建议38 使用静态内部类提高封装性
静态内部类与普通内部类的区别:
- 静态内部类不持有外部类的引用
在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性。
- 静态内部类不依赖外部类
普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生共死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
- 普通内部类不能声明static方法和变量
普通内部类不能声明static的方法和属性,这里说的是常量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。
建议43 避免对象的浅拷贝
public class Client043 {
public static void main(String[] args) {
final Person f = new Person("父亲");
final Person s1 = new Person("大儿子", f);
//小儿子的信息是通过大儿子拷贝过来的
final Person s2 = s1.clone();
s2.setName("小儿子");
System.out.println(s1.getName() + " 的父亲是 " + s1.getFather().getName());
System.out.println(s2.getName() + " 的父亲是 " + s2.getFather().getName());
}
}
@Setter
@Getter
public class Person implements Cloneable {
private String name;
private Person father;
public Person(String name) {
this.name = name;
}
public Person(String name, Person father) {
this.name = name;
this.father = father;
}
public Person clone() {
Person p = null;
try {
p = (Person) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return p;
}
}
public static void main(String[] args) {
final Person f = new Person("父亲");
final Person s1 = new Person("大儿子", f);
//小儿子的信息是通过大儿子拷贝过来的
final Person s2 = s1.clone();
s2.setName("小儿子");
s1.getFather().setName("干爹");
System.out.println(s1.getName() + " 的父亲是 " + s1.getFather().getName());
System.out.println(s2.getName() + " 的父亲是 " + s2.getFather().getName());
}
Object提供的拷贝,模式是一种浅拷贝,它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝。它的拷贝规则:
- 基本类型:如果是基本类型,则拷贝其值,比如int float
- 对象:如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例地址,不受访问权限的限制。
- String字符串:这是比较特殊拷贝,拷贝的也是一个地址,是个引用。但是在修改是,它会从字符串池中重新生成新的字符串,原有的字符串对象保持不变。
建议45 覆写equals方法时不要识别不出自己
public class Client45 {
@Setter
@Getter
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o instanceof Person) {
final Person p = (Person) o;
return name.equalsIgnoreCase(p.getName().trim());
}
return false;
}
}
public static void main(String[] args) {
final Person p1 = new Person("张三");
//这个名字后有个空格
final Person p2 = new Person("张三 ");
List<Person> l = new ArrayList<>(2);
l.add(p1);
l.add(p2);
//true
System.out.println("是否包含张三" + l.contains(p1));
//false
System.out.println("是否包含张三" + l.contains(p2));
}
}
因为有空格导致,所以次问题中equals去掉trim
建议46 equals应该考虑null值情景
上述例子中稍微修改。如下
public static void main(String[] args) {
final Person p1 = new Person("张三");
//这个名字后有个空格
final Person p2 = new Person(null);
List<Person> l = new ArrayList<>(2);
l.add(p1);
l.add(p2);
//true
System.out.println("是否包含张三" + l.contains(p1));
//false
System.out.println("是否包含张三" + l.contains(p2));
}
调整
@Override
public boolean equals(Object o) {
if (o instanceof Person) {
final Person p = (Person) o;
if (p.getName() == null || name == null) {
return false;
}
return name.equalsIgnoreCase(p.getName());
}
return false;
}
建议47 在equals中使用getClass进行类型判读
public class Client047 {
@Setter
@Getter
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o instanceof Person) {
final Person p = (Person) o;
if (p.getName() == null || name == null) {
return false;
}
return name.equalsIgnoreCase(p.getName());
}
return false;
}
}
@Setter
@Getter
private static class Employee extends Person {
private int id;
public Employee(String name, int id) {
super(name);
this.id = id;
}
@Override
public boolean equals(Object o) {
if (o instanceof Employee) {
final Employee e = (Employee) o;
return super.equals(o) && e.getId() == id;
}
return false;
}
}
public static void main(String[] args) {
final Employee e1 = new Employee("张三", 100);
final Employee e2 = new Employee("张三", 1001);
final Person p1 = new Person("张三");
System.out.println(p1.equals(e1));//true
System.out.println(p1.equals(e2));//true
System.out.println(e1.equals(e2));//false
}
}
p1竟然等于e1
因为p1.equals(e1)调用的是父类Person的equals方法进行判断的,它使用了instanceof关键字检查e1是否是Person的实例,由于两者存在继承关系,那结果当然是true。
使用getClass进行类型判断
建议48 覆写equals方法必须覆写hashCode方法
public class Client048 {
@Setter
@Getter
private static class Person {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return new EqualsBuilder().append(name, person.name).isEquals();
}
}
public static void main(String[] args) {
Map<Person, Object> map = new HashMap<Person, Object>() {
{
put(new Person("张三"), new Object());
}
};
final ArrayList<Person> list = new ArrayList<>() {
{
add(new Person("张三"));
}
};
//列表是否包含
final boolean b1 = list.contains(new Person("张三"));
final boolean b2 = map.containsKey(new Person("张三"));
}
}
b1,Person类的equals覆写了,不再判断两个地址是否相等,而是根据人员的姓名来判断两个对象是否相等。
b2,把张三这个对象作为Map的键(Key),放入去,得到的结果却不同。
原因是HashMap底层处理机制是以数组的方式保存Map条目(Map Entry)的,这其中的关键是这个数组下标的处理机制;依据传入元素的hashcode方法的返回值决定其数组下标。
对象的hashcode方法的返回值?是一个对象的哈希码,是由Object类的本地方法生成的,确保每个对象有一个哈希码。
建议50 使用package-info类为包服务
Java中有个特殊的类:package-info类,它是专门为本包服务。之所以特殊,主要体现在三个方面
- 它不能随便别创建
- 它服务的对象很特殊
- package-info类不能偶实现代码
package-info类还有几个特殊的地方,比如不可用继承,没有接口,没有类间关系(关联、组合、聚合)等。
它主要表现以下三个方面
- 声明友好类和包内访问常量
- 为在包上标注注解提供便利
- 提供包的整体注释说明
建议51 不要主动进行垃圾回收
System.gc要停止所有的响应,才能检查内存中是否有可回收的对象。
不要调用System.gc,即使经常出现内存溢出也不要调用,内存溢出时可分析的,是可以查找出原因的,GC不是一个好招数。
4 字符串
建议52 推荐使用String直接赋值
public class Client052 {
public static void main(String[] args) {
String s1 = "中国";
String s2 = "中国";
String s3 = new String("中国");
String s4 = s3.intern();
boolean b1 = (s1 == s2);//true
boolean b2 = (s1 == s3);//false
boolean b3 = (s1 == s4);//true
System.out.println(b1);
System.out.println(b2);
System.out.println(b3);
}
}
Java为了避免在一个系统中大量产生String对象,于是就设计了一个字符串池。在字符串池中所容纳的都是String字符串对象,他的创建机制是这样的:创建一个字符串时,首先检查池中是佛有面值相等的字符串,如果有,则不再创建,直接返回池中该对象的引用,若没有则创建,然后放到池中,并返回新建对象的引用。
使用new String()。直接声明一个string对象是不检查字符串池的,也不会把对象放到池中。
使用intern方法处理,此方法会检查当前的对象在对象池中是否有字面值相同的引用对象,如果有则返回池中对象,如果没有则放置到对象池中,并返回当前对象。
建议53 注意方法中传递的参数要求
5 数组和集合
建议60 性能考虑,数组是首选
public static int sum(int[] datas){
int sum=0;
for (int data : datas) {
sum+=data;
}
return sum;
}
public static int sum(List<Integer> datas){
int sum=0;
for (int data : datas) {
sum+=data;
}
return sum;
}
对于集合,Integer对象通过intValue方法自动转换成一个int基本类型,对于性能濒于临界的系统来说该方案是危险的,特别是大数量的时候,首先,在初始化List数组是要进行装箱动作,把一个int类型包装成一个Integer对象,虽然有整型池在,但不在整型池范围内的都会产生一个新的Integer对象,而众所周知,基本类型是在栈内存中操作的,而对象则是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大。其次,在进行求和计算的时候要做拆箱动作,因此无所谓的性能消耗也就产生了。
建议61 若有必要,使用变长数组
Java中的数组是定长的,一旦经过初始化声明就不可以改变长度,这在实际使用者非常不方便。可以通过数组扩容来解决该问题
public static <T> T[] expandCapacity(T[] data, int newLen) {
//不能是负值
newLen = newLen<0?0:newLen;
return Arrays.copyOf(data,newLen);
}
建议65 避开基本类型数组转换列表陷阱
public static void main(String[] args) {
int[] data = {1, 2, 3, 4, 5};
final List<int[]> lis = Arrays.asList(data);
System.out.println("列表中的元素:" + lis.size());//1
}
asList方法输入的是一个泛型变成参数。基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数。想要作为泛型参数就必须使用其所对应的包装类型。
建议68 频繁插入和删除时使用LinkedList
建议70 子列表只是原列表的一个视图
public static void main(String[] args) {
//定义一个包含量给字符串的列表
final List<String> c = new ArrayList<>();
c.add("A");
c.add("B");
//构造一个包含c列表的字符串列表
final List<String> c1 = new ArrayList<>(c);
final List<String> c2 = c.subList(0, c.size());
c2.add("C");
System.out.println("c==c1" + c.equals(c1));
System.out.println("c==c2" + c.equals(c2));//true
}
subList产生的列表只是一个视图,所有的修改动作直接作用于原列表
建议71 推荐使用subList处理局部列表
删除索引20~30的元素。
public static void main(String[] args) {
//初始化一个固定长度,不可变列表
final List<Integer> initData = Collections.nCopies(100, 0);
//转换为可变列表
final ArrayList<Integer> list = new ArrayList<>(initData);
//删除指定范围元素
list.subList(20, 30).clear();
}
建议72 生成子列表后不要再操作原列表
public static void main(String[] args) {
final List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
final List<String> subList = list.subList(0, 2);
list.add("D");
System.out.println("原列表长度:" + list.size());
//ConcurrentModificationException
System.out.println("子列表长度:" + subList.size());
}
subList的size方法出现了异常,而且还是并发修改异常。因为subList取出来的原列表是一个视图,原数据集(代码中的list变量)修改了,但是subList取出的子列表不会重新生成一个新列表,后面在对子列表继续操作时,就会检测到修改计数器与预期的不同,于是抛出了并发修改异常。
subList的其他方法也会检测修改计数器,例如set、get、add,若生成子列表后,在修改原列表,这些方法也会抛出ConcurrentModificationException异常。
对于子列表操作,因为视图是动态生成的,生成子列表后操作做原列表,必然导致视图不稳定,最有效的办法就是通过Collections.unmodifiableList方法设置列表为制度状态。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
final List<String> subList = list.subList(0, 2);
list = Collections.unmodifiableList(list);
//对list进行只读操作
doReadSomething(list);
//对subList进行读写操作
doReadAndWriteSomething(subList);
}
建议73 使用Comparator进行排序
Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具。
建议74 不推荐使用binarySearch对列表进行检索
**
public static void main(String[] args) {
final List<String> citys = new ArrayList<>();
citys.add("上海");
citys.add("广州");
citys.add("广州");
citys.add("北京");
citys.add("天津");
final int index1 = citys.indexOf("广州");
final int index2 = Collections.binarySearch(citys, "广州");
System.out.println("索引值(indexof):" + index1);//索引值(indexof):1
System.out.println("索引值(binarySearch):" + index2);//索引值(binarySearch):2
}
二分法查询的一个首要前提是:数据已经实现升序,否则二分法查找是不准确的。
使用binarySearch的二分法查找比indexOf的遍历算法性能要高很多,特别是大数据集而且目标值有接近尾部是,binarySearch方法比indexOf相比,性能上会提升几十倍,因此从性能的角度考虑时可以选择binarySearch.
建议75 集合中的元素必须做到compareTo和equals同步
实现了Comparable接口的元素就可以排序,compareTo方法是Comparable接口要求必须实现的,它与equals方法有关系吗?有关系,在compareTo的返回值为0时,它表示的是进行比较的两个元素是相等的,equals是不是也应该对此做出相应的动作呢?
@Getter
@Setter
public class City implements Comparable<City> {
private String code;
private String name;
public City(String code, String name) {
this.code = code;
this.name = name;
}
@Override
public int compareTo(City o) {
return new CompareToBuilder().append(name, o.name).toComparison();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
City city = (City) o;
return new EqualsBuilder().append(code, city.code).isEquals();
}
public static void main(String[] args) {
final List<City> cities = new ArrayList<>();
cities.add(new City("021", "上海"));
cities.add(new City("021", "沪"));
//排序
Collections.sort(cities);
final City city = new City("021", "沪");
final int indexOf = cities.indexOf(city);
final int binarySearch = Collections.binarySearch(cities, city);
System.out.println("索引值(indexOf):" + indexOf);
System.out.println("索引值(binarySearch):" + binarySearch);
}
}
indexOf 是通过equals判断的,equals等于true就认为找到了符合条件的元素了。而binarySearch查找的依据是compareTo返回的值,返回0即认为找到符合条件的元素。
建议76 集合运算时使用更优雅的方式
public static void main(String[] args) {
//集合 交集 并集 差集
final ArrayList<String> list1 = new ArrayList<>();
list1.add("A");
list1.add("B");
final ArrayList<String> list2 = new ArrayList<>();
list2.add("C");
list2.add("B");
//并集
list1.addAll(list2);
System.out.println(Arrays.toString(list1.toArray()));
//交集
list1.retainAll(list2);
//差集
list1.remove(list2);
//无重复的并集
list2.remove(list1);
list1.addAll(list2);
}
建议77 使用shuffle打乱列表
在网站上我们经常会看到关键字云和标签云,用于表明这个关键字或标签是经常被查阅的,而且看到这些标签的动态运动,每次刷新都会有不一样的关键字或标签,让浏览者感觉这个完整访问量非常大,短短的几分钟就有这么多搜索量。
public static void main(String[] args) {
int tagCloudNum=10;
final List<String> tagClouds = new ArrayList<>(tagCloudNum);
//初始化标签云,一般从数据库读入,
final Random rand = new Random();
for (int i = 0; i < tagCloudNum; i++) {
//取得随机位置
int randomPosition=rand.nextInt(tagCloudNum);
//当前元素与随机元素交换
final String temp = tagClouds.get(i);
tagClouds.set(i,tagClouds.get(randomPosition));
tagClouds.set(randomPosition,temp);
}
}
使用Collections的swap交换
public static void main(String[] args) {
int tagCloudNum = 10;
final List<String> tagClouds = new ArrayList<>(tagCloudNum);
//初始化标签云,一般从数据库读入,
final Random rand = new Random();
for (int i = 0; i < tagCloudNum; i++) {
//取得随机位置
int randomPosition = rand.nextInt(tagCloudNum);
//当前元素与随机元素交换
Collections.swap(tagClouds, i, randomPosition);
}
}
public static void main(String[] args) {
int tagCloudNum = 10;
final List<String> tagClouds = new ArrayList<>(tagCloudNum);
//初始化标签云,一般从数据库读入,
Collections.shuffle(tagClouds);
}
shuffle可以用在程序的“伪装”上。例如标签云,游戏的打怪、修行、抽奖程序中。
建议78 减少HashMap中的元素的数量
在系统开发中,我们会经常使用HashMap作为数据集容器,或者用缓存池来处理,一般很稳定,但偶尔也会出现内存溢出问题。而且这经常与hashMap有关,比如我们使用缓冲池操作数据时,大批量的增删改查操作就可能让内存溢出。
public static void main1(String[] args) {
Map<String, String> map = new HashMap<>();
final Runtime rt = Runtime.getRuntime();
rt.addShutdownHook(new Thread(() -> {
final StringBuffer sb = new StringBuffer();
final long heapMaxSize = rt.maxMemory() >> 20;
sb.append("最大可用内存:" + heapMaxSize + " M\n");
final long total = rt.totalMemory() >> 20;
sb.append("堆内存大小:" + total + " M\n");
final long free = rt.freeMemory() >> 20;
sb.append("空闲内存:" + free + " M\n");
System.out.println(sb);
}));
//放入近40万键值对
for (int i = 0; i < 393217; i++) {
map.put("key" + i, "value" + i);
}
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
final Runtime rt = Runtime.getRuntime();
rt.addShutdownHook(new Thread(() -> {
final StringBuffer sb = new StringBuffer();
final long heapMaxSize = rt.maxMemory() >> 20;
sb.append("最大可用内存:" + heapMaxSize + " M\n");
final long total = rt.totalMemory() >> 20;
sb.append("堆内存大小:" + total + " M\n");
final long free = rt.freeMemory() >> 20;
sb.append("空闲内存:" + free + " M\n");
System.out.println(sb);
}));
//放入近40万键值对
for (int i = 0; i < 400000; i++) {
list.add("key" + i);
list.add("value" + i);
}
}
HashMap比ArrayLsit多了一层Entry的底层对象封装,多占用了内存,并且它的扩容策略是2被长度的递增,同时还会依据阀值判断规则进行判断,因此相对于ArrayLsit来说,它就会先出现内存溢出。
建议79:集合中的哈希码不要重复
在一个列表中查找某值是非常消耗资源的。随机存取的列表是遍历查找,顺序存储列表是链表查找,或者是Collections的二分法查找,但这都不够快,毕竟都是遍历,最快的还要数以Hash开头的集合。
建议80 多线程使用Vector和HashTable
public static void main(String[] args) {
//.ConcurrentModificationException
// final List<String> tickets = new ArrayList<>();
//ConcurrentModificationException
final List<String> tickets = new Vector<>();
//初始化票据池
for (int i = 0; i < 100_000; i++) {
tickets.add("火车票" + i);
}
//退票
final Thread returnThread = new Thread(() -> {
while (true) {
tickets.add("车票" + new Random().nextInt());
}
});
//售票
final Thread saleThread = new Thread(() -> {
for (String ticket : tickets) {
tickets.remove(ticket);
}
});
//启动退票线程
returnThread.start();
//启动售票线程
saleThread.start();
}
Arraylist会抛出ConcurrentModificationException异常,换成Vector这个线程安全的集合,也会抛出ConcurrentModificationException异常。
这是因为混淆了线程安全和同步修改异常。基本上所有的集合类都有一个叫做快速失败(Fail-Fast)的校验机制,当一个集合在被多个线程修改并访问时,就可能会出现ConcurrentModificationException长,这是为了确保却和方法一致而设置的保护措施,它的时间原理就是我们经常提到的modCount修改计数器:如果在读列表是,modCount发生变化则会抛出ConcurrentModificationException异常。这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读,脏写而设置的,,以下是线程安全的使用。
public static void main(String[] args) {
final List<String> tickets = new Vector<>();
//初始化票据池
for (int i = 0; i < 100_000; i++) {
tickets.add("火车票" + i);
}
//10个售票窗口
for (int i = 0; i < 10; i++) {
new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName() + "_" + tickets.remove(0));
}
}).start();
}
}
建议81 非稳定排序推荐使用List
Set与List的最大区别就是et中的元素不可重复(这个重复指的equals方法返回值相等),其他方面则没有什么太大区别了,在Set实现类中有一个比较常用的类需要了解一下:TreeSet,该类实现了默认排序为升序的Set集合,如果插入一个元素,默认会按照升序排列(当然是根据Comparable接口的CompareTo的返回值确定排序位置了)。不过这样的排序不适合经常变化的场景中。
public class Client081 {
@Getter
@Setter
static class Person implements Comparable<Person> {
private int height;
public Person(int height) {
this.height = height;
}
@Override
public int compareTo(Person o) {
return new CompareToBuilder().append(height, o.height)
.toComparison();
}
}
public static void main(String[] args) {
final SortedSet<Person> set = new TreeSet<>();
set.add(new Person(180));
set.add(new Person(175));
//身高最矮的人长高了
set.first().setHeight(185);
for (Person p : set) {
System.out.println("身高" + p.getHeight());
}
}
}
SortedSet接口(TreeSet实现了该接口)只是定义了在给集合加入元素是将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,比如String Integer等类型,但不适用于可变量的排序,特别是不确定何时元素发生变化的数据集合。
建议82 集合大家族
1 list
实现ist集合的主要有:ArrayList、LinkedList、Vector、Stack其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则。
2set
Set是不包含重复元素的集合,其主要的实现类:EnumSet、HashSet、TreeSet,HashSet是以哈希码决定其元素位置的Set, 其原理与HashMap相似,它提供快速的插入和查找方法,TreeSet是一个自动排序的Set,他实现了SortedSet接口。
3 Map
Map是一个大家族,它可以分为排序Map和非排序Map,排序Map主要是TreeMap,它根据Key值进行自动排序,非排序Map主要包括:HashMap,HashTable,Properties,EnumMap等。
Map中还有一个WwakHashMap类需要说明,它是有个一个采用弱键方式实现的Map,它的特点是:WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,也就是说明使用WeakHashMap转载数据不用担心内存溢出的问题,GC会自动删除不用的键值对,这是好事。但也存在一个严重问题:GC是悄悄回收的,我们的程序无法知晓该动作,存在着重大隐患。
4 Queue
队列,它分为两种,一类是阻塞式队列,队列满了以后在插入元素则抛出异常,主要包括ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQeque,其中ArrayBlockingQueue是以一个数组放回寺实现的有界阻塞队列,PriorityBlockingQueue是依照优先级组件的队列,LinkedBlockingQeque是通过链表实现的阻塞队列;
另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加到元素。我们最经常使用的是PriorityQueue类。
还有一种是双端队列,支持在头,尾两端插入和移除元素,它的主要实现类是ArrayDeque、LinkedBlockingDeque、LinkedList
5 数组
6工具类
数据的工具类java.util.Arrays和java.lang.reflect.Array。集合的工具类是java.util.Collections。有了这两个工具类,操作数组和集合会易如反掌,得心应手。
7扩展类
Apache的commons-collections
和Google的google-collections。
