改善Java程序的151个建议

1 Java开发中通用的方法和准则

建议1:不要在常量和变量中出现易混淆的字母

字母”I”作为长整型标志时务必大写。

建议2:莫让常量蜕变成变量

务必让常量的值在运行期保存不变

建议3:三元操作符的类型务必一直

建议4:避免带有长参数的方法重载

建议5:别让null值和空值威胁到边长方法

  1. public class Client {
  2. public void methodA(String str, Integer... is) {
  3. }
  4. public void methodA(String str, String... strs) {
  5. }
  6. public static void main(String[] args) {
  7. final Client client = new Client();
  8. client.methodA("China", 0);
  9. client.methodA("China", "people");
  10. client.methodA("China");
  11. client.methodA("China", null);
  12. }
  13. }

image.png

建议6:覆写变长方法也循规蹈矩

在Java中,子类覆写父类的方法很常见,这样做既可以修正Bug也可以提供扩展的业务功能支持,同时还符合开闭原则。覆写必须满足的条件。

  • 重写的方法不能缩小访问权限
  • 参数列表必须与被重写方法相同
  • 返回类型必须与被重写方法的相同或是其子类
  • 重写方法不能抛出异常,或者超出父类范围的异常,但是可以抛出更少,更有限的异常,或者不抛出异常。
  1. class Base {
  2. void fun(int price, int... discounts) {
  3. System.out.println("Base.fun");
  4. }
  5. }
  6. //子类 覆写父类方法
  7. class Sub extends Base {
  8. @Override
  9. void fun(int price, int[] discounts) {
  10. System.out.println("Sub.fun");
  11. }
  12. }
  13. ///
  14. public static void main(String[] args) {
  15. //向上转型
  16. final Base base = new Sub();
  17. base.fun(100,50);
  18. //不转型
  19. final Sub sub = new Sub();
  20. sub.fun(100,50);
  21. }

base对象把子类的对象Sub向上转型,形参列表是由父类决定的。由于是变长参数,在编译时,“base.fun(100,50)”中的“50”这个实参会被编译器猜测而编译成“{50}”数组,再由子类sub执行。再看直接调用子类的情况,这是编译器并不会把50做类型转换,因为数组本身是一个对象,编译器还没有聪明到两个没有继承关系的类之间做转换,要知道Java是严格要求的类型匹配的,类型不匹配编译器自然就会拒绝,并给于错误提示。

建议7:警惕自增的陷阱

  1. public class Client007 {
  2. public static void main(String[] args) {
  3. int count = 0;
  4. for (int i = 0; i < 10; i++) {
  5. count = count++;
  6. }
  7. System.out.println(count);
  8. }
  9. }

count++是一个表达式,是有返回值的,它的返回值就是count自加前的值。程序第一次循环是的详细处理步骤如下

  • 步骤1 Java把count值拷贝到临时变量区
  • 步骤2 count值加1,这个时候count的值是1
  • 步骤3 返回临时变量区的值,注意这个值是0,没有修改过
  • 步骤4 返回值赋给count,此时count被重置为0

建议8 不要让旧语法困扰你

  1. public class Clint008 {
  2. public static void main(String[] args) {
  3. //数据定义以初始化
  4. int free = 200;
  5. // 其他业务
  6. saveDefault:save(free);
  7. }
  8. static void saveDefault() {
  9. }
  10. static void save(int free) {
  11. }
  12. }

代码没有编译错误,其实就是goto语法。

建议9 少用静态导入

对于静态导入,一般要遵循两个规则

  • 不要使用通配符,除非是导入警惕爱常量类(只包含常量的类或接口)
  • 方法名是具有明确,清晰表象意义的工具类

建议10 不要在本类中覆盖静态导入的变量和方法

建议11 养成良好习惯,显示声明UID

类实现Serializable接口的目的是为了可持久化,比如网络传输或本地存储,为系统分布和易购部署提供先决支持条件。若没有序列化,我们熟悉的远程调用、对象数据库都不可能存在。

通过SerialVersionUID,也叫做流标识符,即类的版本定义,它可以显示声明,也可以隐式声明,显示声明如下:

  1. private static final long serialVersionUID = 1L;

建议12 避免用序列化类在构造函数中为不变量赋值

反序列化的一个规则:反序列化时构造函数不会执行

建议13 避免为final变量复杂赋值

  1. public class Person implements Serializable {
  2. private static final long serialVersionUID = 1L;
  3. //通过方法返回值为final变量赋值
  4. public final String name = initName();
  5. private String initName() {
  6. return "混世魔王";
  7. }
  8. }

反序列化时final变量在一下情况下不会被重新赋值:

  • 通过构造函数为final变量赋值
  • 通过方法返回值为final变量赋值
  • final修饰的属性不是基本类型

建议14 使用序列化类的私有方法巧妙解决部分属性持久化问题

部分属性持久化问题看似简单。只需要把不需要持久化的属性加上瞬态关键字(transient关键字)即可。这是一种解决方案,但有时行不通。例如一个计税系统和人力资源系统(HR系统)通过RMI对接,计税系统需要从HR系统获得人员的姓名和基本工资,以作为纳税的依据,而HR系统的工资分为两部分:基本工资和绩效工资,但绩效工资却是秘密。不能泄漏。

  1. @Getter
  2. @Setter
  3. public class Salary implements Serializable {
  4. private static final long serialVersionUID = 1L;
  5. //基本工资
  6. private int basePay;
  7. //绩效工资
  8. private int bonus;
  9. public Salary(int basePay, int bonus) {
  10. this.basePay = basePay;
  11. this.bonus = bonus;
  12. }
  13. }
  14. @Setter
  15. @Getter
  16. public class Person implements Serializable {
  17. private static final long serialVersionUID = 1L;
  18. //姓名
  19. private String name;
  20. //薪水
  21. private Salary salary;
  22. public Person(String name, Salary salary) {
  23. this.name = name;
  24. this.salary = salary;
  25. }
  26. }
  27. public class Serialize {
  28. public static void main(String[] args) {
  29. final Salary salary = new Salary(1000, 2500);
  30. final Person person = new Person("张三", salary);
  31. //HR系统持久化,并传递到计税系统
  32. SerializationUtils.writeObject(person);
  33. }
  34. }
  35. public class Deserialize {
  36. public static void main(String[] args) {
  37. //计税系统反序列化 ,并打印信息
  38. final Person p = (Person) SerializationUtils.readObject();
  39. final StringBuffer sb = new StringBuffer();
  40. sb.append("姓名" + p.getName());
  41. sb.append("\t基本工资" + p.getSalary().getBasePay());
  42. sb.append("\t绩效工资" + p.getSalary().getBonus());
  43. System.out.println(sb);
  44. }
  45. }

但是上面代码不符合需求,因为计税系统只能从HR系统中获得人员的姓名和基本工资,不能获得绩效工资。解决方案

  • 在bouns前加上transient关键字:这是一个方法,但不是一个好方法,加上此关键字就标志着Salary类失去了分布式部署的功能,它可是HR系统最核心的类了,一旦遭遇性能瓶颈,想在实现分布式部署就不可能了。
  • 新增业务对象:增加一个Person4Tax类,完全为计税系统服务,就是说他只有两个属性,姓名和基本工资。完全符合开闭原则,而且对原系统也没有侵入性,只是增加了工作量而已。这是个方法但不是最优方法。
  • 请求端过滤:
  • 变更传输锲约:
  1. @Setter
  2. @Getter
  3. public class Person implements Serializable {
  4. private static final long serialVersionUID = 1L;
  5. //姓名
  6. private String name;
  7. //薪水
  8. private transient Salary salary;
  9. //序列化委托方法
  10. private void writeObject(java.io.ObjectOutputStream out) throws IOException {
  11. out.defaultWriteObject();
  12. out.writeObject(salary.getBasePay());
  13. }
  14. //反序列化时委托方法
  15. private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
  16. in.defaultReadObject();
  17. salary = new Salary(in.readInt(), 0);
  18. }
  19. public Person(String name, Salary salary) {
  20. this.name = name;
  21. this.salary = salary;
  22. }
  23. }

建议15 break万万不可忘

case语句后面随手写上break;

建议17 慎用动态编译

建议18 避免instanceof非预期结果

  1. public class Client018 {
  2. public static void main(String[] args) {
  3. //String对象是否是Object的实例
  4. boolean b1 = "String" instanceof Object;
  5. //String对象是否是String的实例
  6. boolean b2 = new String() instanceof String;
  7. boolean b3 = new Object() instanceof String;
  8. boolean b4 = 'A' instanceof Character;
  9. boolean b5 = null instanceof String;
  10. boolean b6 = (String) null instanceof String;
  11. boolean b7 = new Date() instanceof String;
  12. }
  13. }
  • 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 不要只替换一个类

  1. public class Constant {
  2. //定义人类寿命极限
  3. public final static int MAX_AGE=150;
  4. }
  5. public class Client020 {
  6. public static void main(String[] args) {
  7. System.out.println("人类寿命极限是:" + Constant.MAX_AGE);
  8. }
  9. }

对应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值

包装类型参与计算时,要做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 使用静态内部类提高封装性

静态内部类与普通内部类的区别:

  1. 静态内部类不持有外部类的引用

    在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性。

  1. 静态内部类不依赖外部类

    普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生共死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。

  1. 普通内部类不能声明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提供的拷贝,模式是一种浅拷贝,它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝。它的拷贝规则:

  1. 基本类型:如果是基本类型,则拷贝其值,比如int float
  2. 对象:如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例地址,不受访问权限的限制。
  3. 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类,它是专门为本包服务。之所以特殊,主要体现在三个方面

  1. 它不能随便别创建
  2. 它服务的对象很特殊
  3. package-info类不能偶实现代码

package-info类还有几个特殊的地方,比如不可用继承,没有接口,没有类间关系(关联、组合、聚合)等。
它主要表现以下三个方面

  1. 声明友好类和包内访问常量
  2. 为在包上标注注解提供便利
  3. 提供包的整体注释说明

建议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。