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

11:养成良好习惯,显式声明UID

在序列化和反序列化的类不一致的情形下,反序列化时会报一个InvalidClassException异常,原因是序列化和反序列化所对应的类版本发生了变化,JVM不能把数据流转换为实例对象。

JVM在反序列化时,会比较数据流中的serialVersionUID与类的serialVersionUID是否相同,如果相同,则认为类没有发生改变,可以把数据流load为实例对象;如果不相同,则会抛出异常InvalidClassException。

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

反序列化的执行过程:

JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,注意是类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name竟然没有赋值,不能引用,于是它很“聪明”地不再初始化,保持原值状态,所以结果就是“混世魔王”了。

注意:在序列化类中,不使用构造函数为final变量赋值。

13:避免为final变量复杂赋值

保存到磁盘上(或网络传输)的对象文件包括两部分:

(1)类描述信息包括包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。要注意的一点是,它并不是class文件的翻版,它不记录方法、构造函数、static变量等的具体实现。之所以类描述会被保存,很简单,是因为能去也能回嘛,这保证反序列化的健壮运行。

(2)非瞬态(transient关键字)和非静态(static关键字)的实例变量值

注意,这里的值如果是一个基本类型,好说,就是一个简单值保存下来;如果是复杂对象,也简单,连该对象和关联类信息一起保存,并且持续递归下去(关联类也必须实现Serializable接口,否则会出现序列化异常),也就是说递归到最后,其实还是基本数据类型的保存。正是因为这两点原因,一个持久化后的对象文件会比一个class类文件大很多,

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

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

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

解决部分属性持久化的方法:

(1)在bonus前加上transient关键字

这是一个方法,但不是一个好方法,加上transient关键字就标志着Salary类失去了分布式部署的功能,它可是HR系统最核心的类了,一旦遭遇性能瓶颈,想再实现分布式部署就不可能了,此方案否定。

(2)新增业务对象增加一个Person4Tax类,完全为计税系统服务,就是说它只有两个属性:姓名和基本工资。符合开闭原则,而且对原系统也没有侵入性,只是增加了工作量而已。这是个方法,但不是最优方法。

(3)请求端过滤在计税系统获得Person对象后,过滤掉Salary的bonus属性,方案可行但不合规矩,因为HR系统中的Salary类安全性竟然让外系统(计税系统)来承担,设计严重失职。

(4)变更传输契约例如改用XML传输,或者重建一个Web Service服务。可以做,但成本太高。

我们在Person类中增加了writeObject和readObject两个方法,并且访问权限都是私有级别,为什么这会改变程序的运行结果呢?

这里使用了序列化独有的机制:序列化回调

Java调用ObjectOutputStream类把一个对象转换成流数据时,会通过反射(Reflection)检查被序列化的类是否有writeObject方法,并且检查其是否符合私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,在从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法,如果有,则会通过该方法读取属性值。此处有几个关键点要说明:

(1)out.defaultWriteObject()告知JVM按照默认的规则写入对象,惯例的写法是写在第一句话里。

(2)in.defaultReadObject()告知JVM按照默认规则读入对象,惯例的写法也是写在第一句话里。

(3)out.writeXX和in.readXX分别是写入和读出相应的值,类似一个队列,先进先出,如果此处有复杂的数据逻辑,建议按封装Collection对象处理。

15:break万万不可忘

16:易变业务使用脚本语言编写

修改Java代码,JVM没有重启,输入参数也没有任何改变,仅仅改变脚本函数即可产生不同的结果。这就是脚本语言对系统设计最有利的地方:可以随时发布而不用重新部署;

17:慎用动态编译

只要是在本地静态编译能够实现的任务,比如编译参数、输入输出、错误监控等,动态编译就都能实现。

Java的动态编译对源提供了多个渠道。比如,可以是字符串(例子中就是字符串),可以是文本文件,也可以是编译过的字节码文件(.class文件),甚至可以是存放在数据库中的明文代码或是字节码。汇总成一句话,只要是符合Java规范的就都可以在运行期动态加载,其实现方式就是实现JavaFileObject接口,重写getCharContent、openInputStream、openOutputStream,或者实现JDK已经提供的两个SimpleJavaFileObject、ForwardingJavaFileObject。

18:避免instanceof非预期结果

'A' instanceof Character

这句话可能有读者会猜错,事实上它编译不通过,为什么呢?因为’A’是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断,不能用于基本类型的判断。

null instanceof String

返回值是false,这是instanceof特有的规则:若左操作数是null,结果就直接返回false,不再运算右操作数是什么类。这对我们的程序非常有利,在使用instanceof操作符时,不用关心被判断的类(也就是左操作数)是否为null,这与我们经常用到的equals、toString方法不同。

(String)null instanceof String

返回值是false,不要看这里有个强制类型转换就认为结果是true,不是的,null是一个万用类型,也可以说它没类型,即使做类型转换还是个null。

1
2
3
4
5
6
7
8
9
//在泛型中判断String对象是否是Data的实例
new GenericClass<String>().isDateInstance("")

class GenericClass<T>{
	//判断是否是Data类型
	public boolean isDateInstance(T t){
		return t instanceof Date;
	}
}

编译通不过?非也,编译通过了,返回值是false。

T是个String类型,与Date之间没有继承或实现关系。

为什么’’t instanceof Date’‘会编译通过呢?那是因为Java的泛型是为编码服务的,在编译成字节码时,T已经是Object类型了,传递的实参是String类型,也就是说T的表面类型是Object,实际类型是String,那’’t instanceof Date’‘这句话就等价于’‘Object instance of Date’‘了,所以返回false就很正常了。

19:断言绝对不是鸡肋

assert虽然是做断言的,但不能将其等价于if…else…这样的条件判断,它在以下两种情况不可使用:

(1)在对外公开的方法中

不能用断言做输入校验,特别是公开方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Client {
	public static void main(String[] args) {
		StringUtils.encode(null);
	}
}
class StringUtils{
	public static String encode(String str){
		assert str!=null:"加密的字符串为null";
		return null;
	}
}

encode方法对输入参数做了不为空的假设,如果为空,则抛出AssertionError错误,但这段程序存在一个严重的问题,encode是一个public方法,这标志着是它对外公开的,任何一个类只要能够传递一个String类型的参数(遵守契约)就可以调用,但是Client类按照规范和契约调用enocde方法,却获得了一个AssertionError错误信息,是谁破坏了契约协定?—是encode方法自己。

(2)在执行逻辑代码的情况下

assert的支持是可选的,在开发时可以让它运行,但在生产系统中则不需要其运行了(以便提高性能),因此在assert的布尔表达式中不能执行逻辑代码,否则会因为环境不同而产生不同的逻辑。

1
2
3
public void doSomething(List list,Object element){
		assert list.remove(element):"删除元素" + element + "失败";	
}

在什么情况下能够使用assert呢?

按照正常执行逻辑不可能到达的代码区域可以放置assert。

具体分为三种情况:

(1)在私有方法中放置assert作为输入参数的校验

在私有方法中可以放置assert校验输入参数,因为私有方法的使用者是作者自己,私有方法的调用者和被调用者之间是一种弱契约关系,或者说没有契约关系,其间的约束是依靠作者自己控制的,因此加上assert可以更好地预防自己犯错,或者无意的程序犯错。

(2)流程控制中不可能达到的区域

这类似于JUnit的fail方法,其标志性的意义就是:程序执行到这里就是错误的,

(3)建立程序探针

我们可能会在一段程序中定义两个变量,分别代表两个不同的业务含义,但是两者有固定的关系,例如var1=var2*2,那我们就可以在程序中到处设“桩”,断言这两者的关系,如果不满足即表明程序已经出现了异常,业务也就没有必要运行下去了。

20:不要只替换一个类

对于final修饰的基本类型String类型,编译器会认为它是稳定态(Immutable Status),所以在编译时就直接把值编译到字节码中了,避免了在运行期引用(Run-time Reference),以提高代码的执行效率。

针对我们的例子来说,Client类在编译时,字节码中就写上了“150”这个常量,而不是一个地址引用,因此无论你后续怎么修改常量类,只要不重新编译Client类,输出还是照旧。

而对于final修饰的类(即非基本类型),编译器认为它是不稳定态(Mutable Status),在编译时建立的则是引用关系(该类型也叫做Soft Final),如果Client类引入的常量是一个类或实例,即使不重新编译也会输出最新值。

注意:发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是万全之策。

第2章 基本类型

21:用偶判断,不用奇判断

Java中的取余(%标示符)算法:

1
2
3
public static int remainder(int dividend,int divisor){
		return dividend - dividend / divisor * divisor;
}

判断是否是偶数的错误写法:

(i%2 ==1?"奇数":"偶数")

判断是否是偶数的正确写法:

(i%2 ==0?"偶数":"奇数")

22:用整数类型处理货币

(1)使用BigDecimal

BigDecimal是专门为弥补浮点数无法精确计算的缺憾而设计的类,并且它本身也提供了加减乘除的常用数学算法。特别是与数据库Decimal类型的字段映射时,BigDecimal是最优的解决方案。 (2)使用整型

把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小100倍,这样处理的好处是计算简单、准确,一般在非金融行业(如零售行业)应用较多。此方法还会用于某些零售POS机,它们的输入和输出全部是整数,那运算就更简单。

23:不要让类型默默转换

错误:

long dis2 = LIGHT_SPEED * 60 * 8;

正确:

long dis2 = LIGHT_SPEED * 60L * 8;

long dis2 = 1L*LIGHT_SPEED * 60 * 8;

基本类型转换时,使用主动声明方式减少不必要的Bug。

25:不要让四舍五入亏了一方

修正算法:银行家舍入(Banker’s Round)的近似算法,其规则如下:

  • 舍去位的数值小于5时,直接舍去;
  • 舍去位的数值大于等于6时,进位后舍去;
  • 当舍去位的数值等于5时,分两种情况:5后面还有其他数字(非0),则进位后舍去;若5后面是0(即5是最后一个数字),则根据5前一位数的奇偶性来判断是否需要进位,奇数进位,偶数舍去。

以上规则汇总成一句话:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。

BigDecimal和RoundingMode是一个绝配,想要采用什么舍入模式使用RoundingMode设置即可。目前Java支持以下七种舍入方式:

  • ROUND_UP:远离零方向舍入。

    向远离0的方向舍入,也就是说,向绝对值最大的方向舍入,只要舍弃位非0即进位。

  • ROUND_DOWN:趋向零方向舍入。

    向0方向靠拢,也就是说,向绝对值最小的方向输入,注意:所有的位都舍弃,不存在进位情况。

  • ROUND_CEILING:向正无穷方向舍入。

    向正最大方向靠拢,如果是正数,舍入行为类似于ROUND_UP;如果为负数,则舍入行为类似于ROUND_DOWN。注意:Math.round方法使用的即为此模式。

  • ROUND_FLOOR:向负无穷方向舍入。

    向负无穷方向靠拢,如果是正数,则舍入行为类似于 ROUND_DOWN;如果是负数,则舍入行为类似于ROUND_UP。

  • HALF_UP:最近数字舍入(5进)。

    这就是我们最最经典的四舍五入模式。

  • HALF_DOWN:最近数字舍入(5舍)。

    在四舍五入中,5是进位的,而在HALF_DOWN中却是舍弃不进位。

  • HALF_EVEN:银行家算法。

    在普通的项目中舍入模式不会有太多影响,可以直接使用Math.round方法,但在大量与货币数字交互的项目中,一定要选择好近似的计算模式,尽量减少因算法不同而造成的损失。

26:提防包装类型的null值

包装类型参与运算时,要做null值校验。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
		List<Integer> list = new ArrayList<Integer>();
		list.add(1);
		list.add(2);
		list.add(null);
		System.out.println(f(list));
}
	
public static int f(List<Integer> list){
		int count = 0;
		for(int i:list){
			count += i;
		}
		return count;
}

运行结果:Exception in thread “main” java.lang.NullPointerException

报空指针异常:在程序的for循环中,隐含了一个拆箱过程,在此过程中包装类型转换为了基本类型。我们知道拆箱过程是通过调用包装对象的intValue方法来实现的,由于包装对象是null值,访问其intValue方法报空指针异常也就在所难免了。

解决方法:加入null值检查即可。

1
2
3
4
5
6
7
public static int f(List<Integer> list) {
		int count = 0;
		for (Integer i : list) {
			count += (i!=null)?i:0;
		}
		return count;
}

27:谨慎包装类型的大小比较

在Java中,“>”和“<”用来判断两个数字类型的大小关系,注意只能是数字型的判断,对于Integer包装类型,是根据其intValue()方法的返回值(也就是其相应的基本类型)进行比较的(其他包装类型是根据相应的value值来比较的,如doubleValue、floatValue等),那很显然,两者不可能有大小关系的。

修改方法:

直接使用Integer实例的compareTo方法即可。但是这类问题的产生更应该说是习惯问题,只要是两个对象之间的比较就应该采用相应的方法,而不是通过Java的默认机制来处理。

28:优先使用整型池

整型池的存在不仅仅提高了系统性能,同时也节约了内存空间,这也是我们使用整型池的原因,也就是在声明包装对象的时候使用valueOf生成,而不是通过构造函数来生成的原因。

在判断对象是否相等的时候,最好是用equals方法,避免用“==”产生非预期结果。

注意:通过包装类的valueOf生成包装实例可以显著提高空间和时间性能。

29:优先选择基本类型

自动装箱有一个重要的原则:基本类型可以先加宽,再转变成宽类型的包装类型,但不能直接转变成宽类型的包装类型。

这句话比较拗口,简单地说就是,int可以加宽转变成long,然后再转变成Long对象,但不能直接转变成包装类型,注意这里指的都是自动转换,不是通过构造函数生成。

注意:基本类型优先考虑。

30:不要随便设置随机种子

在Java中有两种方法可以获得不同的随机数:通过java.util.Random类获得随机数的原理和Math.random方法相同,Math.random()方法也是通过生成一个Random类的实例,然后委托nextDouble()方法的,两者是殊途同归,没有差别。

第3章 类、对象及方法

31:在接口中不要存在实现代码

32:静态变量一定要先声明后赋值

33:不要覆写静态方法

一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。

比如,变量base的表面类型是Base,实际类型是Sub。对于非静态方法,它是根据对象的实际类型来执行的,也就是执行了Sub类中的doAnything方法。而对于静态方法来说就比较特殊了,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法,如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。

通过实例对象访问静态方法或静态属性不是好习惯,它给代码带来了“坏味道”。

34:构造函数尽量简化

35:避免在构造函数中初始化其他类

36:使用构造代码块精炼程序

编译器会把构造代码块插入到每个构造函数的最前端,

构造代码块会在每个构造函数内首先执行(需要注意的是:构造代码块不是在构造函数之前运行的,它依托于构造函数的执行)

构造代码块应用:

(1)初始化实例变量(Instance Variable)

如果每个构造函数都要初始化变量,可以通过构造代码块来实现。当然也可以通过定义一个方法,然后在每个构造函数中调用该方法来实现,没错,可以解决,但是要在每个构造函数中都调用该方法,而这就是其缺点,若采用构造代码块的方式则不用定义和调用,会直接由编译器写入到每个构造函数中,这才是解决此类问题的绝佳方式。

(2)初始化实例环境

一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建对象时创建此场景,例如在JEE开发中,要产生HTTP Request必须首先建立HTTP Session,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在,不存在则创建之。

构造代码块的两个特性:在每个构造函数中都运行和在构造函数中它会首先运行。

37:构造代码块会想你所想

上一个建议是说编译器会把构造代码块插入到每一个构造函数中,但是有一个例外的情况没有说明:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。

这还要从构造代码块的诞生说起,构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码而产生的,因此,Java就很聪明地认为把代码块插入到没有this方法的构造函数中即可,而调用其他构造函数的则不插入,确保每个构造函数只执行一次构造代码块

在构造代码块的处理上,super方法没有任何特殊的地方,编译器只是把构造代码块插入到super方法之后执行而已。

38:使用静态内部类提高封装性

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

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

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

而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置所决定的),其他则不能访问。

(2)静态内部类不依赖外部类

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

(3)普通内部类不能声明static的方法和变量

普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。

39:使用匿名类的构造函数

匿名函数虽然没有名字,但也是可以有构造函数的,它用构造函数块来代替

40:匿名类的构造函数很特殊

一般类(也就是具有显式名字的类)的所有构造函数默认都是调用父类的无参构造的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块。

41:让多重继承成为现实

需要用到多重继承时,可以思考一下内部类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
interface Father {
    public int strong();
}

interface Mother {
    public int kind();
}

class FatherImpl implements Father {
    @Override
    public int strong() {
        return 8;
    }
}

class MotherImpl implements Mother {
    @Override
    public int kind() {
        return 8;
    }
}

成员内部类(也叫做实例内部类,Instance Inner Class):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Son extends FatherImpl implements Mother {
    @Override
    public int strong() {
        return super.strong() + 1;
    }

    @Override
    public int kind() {
        return new MotherSpecial().kind();
    }

    private class MotherSpecial extends MotherImpl {
        @Override
        public int kind() {
            return super.kind() - 1;
        }
    }
}

内部类的一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性。

匿名内部类(Anonymous Inner Class):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Daughter extends MotherImpl implements Father {
    @Override
    public int strong() {
        return new FatherImpl() {
            @Override
            public int strong() {
                return super.strong() - 2;
            }
        }.strong();
    }
}

42:让工具类不可实例化

1
2
3
4
5
6
7
public final class Math {

    /**
     * Don't let anyone instantiate this class.
     */
    private Math() {}
}

43:避免对象的浅拷贝

浅拷贝(Shadow Clone,也叫做影子拷贝)存在对象属性拷贝不彻底的问题。

44:推荐使用序列化实现对象的拷贝

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class CloneUtils {
    @SuppressWarnings("unchecked")
    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;
    }
}

采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便。

45:覆写equals方法时不要识别不出自己

equals的自反性原则:对于任何非空引用x,x.equals(x)应该返回true。

46:equals应该考虑null值情景

equals的对称性原则:对于任何引用x和y的情形,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。

47:在equals中使用getClass进行类型判断

equals的传递性原则:对于实例对象x、y、z来说,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 @Override
public boolean equals(Object obj) {
  if (obj != null && obj.getClass() == this.getClass()) {
    Person p = (Person) obj;
    if (p.getName() == null || name == null) {
      return false;
    } else {
      return name.equalsIgnoreCase(p.getName());
    }
  }
  return false;
}

在覆写equals时建议使用getClass进行类型判断,而不要使用instanceof。

48:覆写equals方法必须覆写hashCode方法

1
2
3
4
@Override
public int hashCode() {
  return new HashCodeBuilder().append(name).toHashCode();
}

其中HashCodeBuilder是org.apache.commons.lang.builder包下的一个哈希码生成工具,使用起来非常方便,可以直接在项目中集成。

49:推荐覆写toString方法

50:使用package-info类为包服务

51:不要主动进行垃圾回收

不要调用System.gc,即使经常出现内存溢出也不要调用,内存溢出是可分析的,是可以查找出原因的。

第4章 字符串

52:推荐使用String直接量赋值

字符串池(字符串常量池,String Pool或String Constant Pool 或 String Literal Pool),在字符串池中所容纳的都是String字符串对象,它的创建机制是这样的:创建一个字符串时,首先检查池中是否有字面值相等的字符串,如果有,则不再创建,直接返回池中该对象的引用,若没有则创建之,然后放到池中,并返回新建对象的引用。

因为intern会检查当前的对象在对象池中是否有字面值相同的引用对象,如果有则返回池中对象,如果没有则放置到对象池中,并返回当前对象。

String类是一个不可变(Immutable)对象其实有两层意思:

  • 一是String类是final类,不可继承,不可能产生一个String的子类;
  • 二是在String类提供的所有方法中,如果有String返回值,就会新建一个String对象,不对原对象进行修改,这也就保证了原对象是不可改变的。

虽然Java的每个对象都保存在堆内存中,但是字符串池非常特殊,它在编译期已经决定了其存在JVM的常量池(Constant Pool),垃圾回收器是不会对它进行回收的。

53:注意方法中传递的参数要求

replaceAll传递的第一个参数是正则表达式。

54:正确使用String、StringBuffer、StringBuilder

StringBuilder与StringBuffer基本相同,都是可变字符序列,不同点是:StringBuffer是线程安全的,StringBuilder是线程不安全的,翻翻两者的源代码,就会发现在StringBuffer的方法前都有synchronized关键字,这也是StringBuffer在性能上远低于StringBuilder的原因。

在性能方面,由于String类的操作都是产生新的String对象,而StringBuilder和StringBuffer只是一个字符数组的再扩容而已,所以String类的操作要远慢于StringBuffer和StringBuilder。

在不同的场景下使用不同的字符序列:

(1)使用String类的场景在字符串不经常变化的场景中可以使用String类,例如常量的声明、少量的变量运算等。

(2)使用StringBuffer类的场景在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在多线程的环境中,则可以考虑使用StringBuffer,例如XML解析、HTTP参数解析和封装等。

(3)使用StringBuilder类的场景在频繁进行字符串的运算(如拼接、替换、删除等),并且运行在单线程的环境中,则可以考虑使用StringBuilder,如SQL语句的拼装、JSON封装等。

55:注意字符串的位置

Java对加号的处理机制:

在使用加号进行计算的表达式中,只要遇到String字符串,则所有的数据都会转换为String类型进行拼接,如果是原始数据,则直接拼接,如果是对象,则调用toString方法的返回值然后拼接。

56:自由选择字符串拼接方法

对一个字符串进行拼接有三种方法:加号、concat方法及StringBuilder(或StringBuffer)的append方法,其中加号是最常用的,其他两种方式偶尔会出现在一些开源项目中。

“+”加号:

str += "c";

concat:

str = str.concat("c");

append:

1
2
3
4
5
StringBuilder sb = new StringBuilder("a");
for(int i=0;i<MAX_LOOP;i++){
  sb.append("c");
}
String str = sb.toString();

(1)“+”拼接字符串

虽然编译器对字符串的加号做了优化,它会使用StringBuilder的append方法进行追加,按道理来说,其执行时间也应该是0毫秒,不过它最终是通过toString方法转换成String字符串的,例子中“+”拼接的代码与如下代码相同:

str = new StringBuilder(prefix).append("c").toString();

它与纯粹使用StringBuilder的append方法是不同的:一是每次循环都会创建一个StringBuilder对象,二是每次执行完毕都要调用toString方法将其转换为字符串——它的执行时间就是耗费在这里了!

(2)concat拼接字符串

concat源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public String concat(String str) {
  int otherLen = str.length();
  if (otherLen == 0) {
    return this;
  }
  int len = value.length;
  char buf[] = Arrays.copyOf(value, len + otherLen);
  str.getChars(buf, len);
  return new String(buf, true);
}

其整体看上去就是一个数组拷贝,虽然在内存中的处理都是原子性操作,速度非常快,不过,注意看最后的return语句,每次的concat操作都会新创建一个String对象,这就是concat速度慢下来的真正原因,它创建了5万个String对象呀。

(3)append拼接字符串

append源码:

1
2
3
4
5
@Override
public StringBuilder append(String str) {
  super.append(str);
  return this;
}

StringBuilder的append方法直接由父类AbstractStringBuilder实现:

1
2
3
4
5
6
7
8
9
public AbstractStringBuilder append(String str) {
  if (str == null)
    return appendNull();
  int len = str.length();
  ensureCapacityInternal(count + len);
  str.getChars(0, len, value, count);
  count += len;
  return this;
}

ensureCapacityInternal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/**
* For positive values of {@code minimumCapacity}, this method
* behaves like {@code ensureCapacity}, however it is never
* synchronized.
* If {@code minimumCapacity} is non positive due to numeric
* overflow, this method throws {@code OutOfMemoryError}.
*/
private void ensureCapacityInternal(int minimumCapacity) {
  // overflow-conscious code
  if (minimumCapacity - value.length > 0) {
    value = Arrays.copyOf(value,
                          newCapacity(minimumCapacity));
  }
}

整个append方法都在做字符数组处理,加长,然后数组拷贝,这些都是基本的数据处理,没有新建任何对象,所以速度也就最快了!注意:例子中是在最后通过StringBuffer的toString返回了一个字符串,也就是说在5万次循环结束后才生成了一个String对象。

三者的实现方法不同,性能也就不同,但并不表示我们一定要使用StringBuilder,这是因为“+”非常符合我们的编码习惯,适合人类阅读,两个字符串拼接,就用加号连一下,这很正常,也很友好,在大多数情况下我们都可以使用加号操作,只有在系统性能临界(如在性能“增之一分则太长”的情况下)的时候才可以考虑使用concat或append方法。而且,很多时候系统80%的性能是消耗在20%的代码上的,我们的精力应该更多的投入到算法和结构上。

适当的场景使用适当的字符串拼接方式。

57:推荐在复杂字符串操作中使用正则表达式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void main(String[] args) {
  Scanner input = new Scanner(System.in);
  while (input.hasNext()) {
    String str = input.nextLine();
    Pattern pattern = Pattern.compile("\\b\\w+\\b");
    Matcher matcher = pattern.matcher(str);
    int wordsCount = 0;
    while (matcher.find()) {
      System.out.println(matcher.group());
      wordsCount++;
    }
    System.out.println(str + " 单词数" + wordsCount);
  }
}

在Java的正则表达式中“\b”表示的是一个单词的边界,它是一个位置界定符,一边为字符或数字,另外一边则非字符或数字,例如“A”这样一个输入就有两个边界,即单词“A”的左右位置,这也就说明了为什么要加上“\w”(它表示的是字符或数字)。

正则表达式在字符串的查找、替换、剪切、复制、删除等方面有着非凡的作用,特别是面对大量的文本字符需要处理(如需要读取大量的LOG日志)时,使用正则表达式可以大幅地提高开发效率和系统性能,但是正则表达式是一个恶魔(Regular Expressions is evil),它会使程序难以读懂。

58:强烈建议使用UTF编码

Java程序涉及的编码包括两部分:

(1)Java文件编码

如果我们使用记事本创建一个.java后缀的文件,则文件的编码格式就是操作系统默认的格式。如果是使用IDE工具创建的,如Eclipse,则依赖于IDE的设置,Eclipse默认是操作系统编码(Windows一般为GBK)。

(2)Class文件编码

通过javac命令生成的后缀名为.class的文件是UTF-8编码的UNICODE文件,这在任何操作系统上都是一样的,只要是class文件就会是UNICODE格式。需要说明的是,UTF是UNICODE的存储和传输格式,它是为了解决UNICODE的高位占用冗余空间而产生的,使用UTF编码就标志着字符集使用的是UNICODE。

59:对字符串排序持一种宽容的心态

Java推荐使用Collator类进行排序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static void main(String[] args) throws Exception {
  String[] strs = {"张三(Z)", "李四(L)", "王五(W)"};
  //定义一个中文排序器
  Comparator c = Collator.getInstance(Locale.CHINA);
  //升序排序
  Arrays.sort(strs, c);
  int i = 0;
  for (String str : strs) {
    System.out.println((++i) + "、" + str);
  }
}

运行结果:

1
2
3
1、李四(L)
2、王五(W)
3、张三(Z)

第5章 数组和集合

60:性能考虑,数组是首选

对一个数据集求和的计算:

 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
38
39
40
public class Client {
    private static final int LOOP_NUM = 100000;

    public static void main(String[] args) {
        int[] score = new int[100];
        List<Integer> list = new ArrayList<>(100);
        for (int i = 0; i < score.length; i++) {
            score[i] = i;
            list.add(i);
        }
        long begin = System.currentTimeMillis();
        for (int i = 0; i < LOOP_NUM; i++) {
            sum(score);
        }
        long end = System.currentTimeMillis();
        System.out.println("Array: " + (end - begin) + " ms");

        for (int i = 0; i < LOOP_NUM; i++) {
            sum(list);
        }
        System.out.println("List: " + (System.currentTimeMillis() - end) + " ms");

    }

    public static int sum(int[] datas) {
        int sum = 0;
        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        return sum;
    }

    public static int sum(List<Integer> datas) {
        int sum = 0;
        for (int i = 0; i < datas.size(); i++) {
            sum += datas.get(i);
        }
        return sum;
    }
}

运行结果:

1
2
Array: 6 ms
List: 10 ms

sum += datas.get(i);

首先,在初始化List数组时要进行装箱动作,把一个int类型包装成一个Integer对象,虽然有整型池在,但不在整型池范围内的都会产生一个新的Integer对象,而且众所周知,基本类型是在栈内存中操作的,而对象则是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大(从性能上来讲,基本类型的处理占优势)。

其次,在进行求和计算(或者其他遍历计算)时要做拆箱动作,Integer对象通过intValue方法自动转换成了一个int基本类型,因此无谓的性能消耗也就产生了。

在实际测试中发现:对基本类型进行求和计算时,数组的效率是集合的10倍。

注意:性能要求较高的场景中使用数组替代集合。

61:若有必要,使用变长数组

62:警惕数组的浅拷贝

通过copyOf方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。数组、集合的clone方法都是浅拷贝。

使用集合(如List)进行业务处理时,比如发觉需要拷贝集合中的元素,可集合没有提供拷贝方法,如果自己写会很麻烦,所以干脆使用List.toArray方法转换成数组,然后通过Arrays.copyOf拷贝,再转换回集合,简单便捷!但是,虽然很多时候浅拷贝可以解决业务问题,但更多时候会留下隐患,需要提防又提防。

63:在明确的场景下,为集合指定初始容量

64:多种最值算法,适时选择

求最大值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static int max(int[] data) {
   int max = data[0];
   for (int i : data) {
      max = max > i ? max : i;
   }
   return max;
}

public static int max2(int[] data) {
   Arrays.sort(data.clone());
   return data[data.length - 1];
}

在代码中为什么要先使用data.clone拷贝再排序呢?

因为数组也是一个对象,不拷贝就改变了原有数组元素的顺序。除非数组元素的顺序无关紧要。

如果要查找仅次于最大值的元素(也就是老二),该如何处理呢?要注意,数组的元素是可以重复的,最大值可能是多个,所以单单一个排序然后取倒数第二个元素是解决不了问题的。

此时,就需要一个特殊的排序算法了,先要剔除重复数据,然后再排序。当然,自己写算法也可以实现,但是集合类已经提供了非常好的方法,要是再使用数组自己写算法就显得有点过时了。数组不能剔除重复数据,但Set集合却是可以的,而且Set的子类TreeSet还能自动排序。

1
2
3
4
5
public static int getSecond(Integer[] data) {
    List<Integer> dataList = Arrays.asList(data);
    TreeSet<Integer> ts = new TreeSet<>(dataList);
    return ts.lower(ts.last());
}

剔除重复元素并升序排列,这都由TreeSet类实现的,然后可再使用lower方法寻找小于最大值的值。

实际应用中求最值,包括最大值、最小值、第二大值、倒数第二小值等,使用集合是最简单的方式,当然若从性能方面来考虑,数组是最好的选择。

65:避开基本类型数组转换列表陷阱

1
2
3
4
5
6
public static void main(String[] args) {
    int[] data = {1, 2, 3, 4, 5};
    List list = Arrays.asList(data);
    System.out.println(list.size());
}
//运行结果:1

Arrays.asList方法:输入一个变长参数,返回一个固定长度的列表。注意这里是一个变长参数。

1
2
3
4
5
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

asList方法输入的是一个泛型变长参数,基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型。

那前面的例子传递了一个int类型的数组,为什么程序没有报编译错呢?在Java中,数组是一个对象,它是可以泛型化的,也就是说例子中是把一个int类型的数组作为了T的类型,所以转换后在List中就只有一个类型为int数组的元素了。

1
2
3
4
5
6
public static void main(String[] args) {
    int[] data = {1, 2, 3, 4, 5};
    List list = Arrays.asList(data);
    System.out.println(list.get(0).getClass());
    System.out.println(data.equals(list.get(0)));
}

运行结果:

class [I true

JVM不可能输出Array类型,因为Array是属于java.lang.reflect包的,它是通过反射访问数组元素的工具类。在Java中任何一个数组的类都是“[I”,究其原因就是Java并没有定义数组这一个类,它是在编译器编译的时候生成的,是一个特殊的类,在JDK的帮助中也没有任何数组类的信息。

修改方案:直接使用包装类即可。

1
2
3
4
5
6
public static void main(String[] args) {
    Integer[] data = {1, 2, 3, 4, 5};
    List list = Arrays.asList(data);
    System.out.println(list.size());
}
//运行结果:5

把int替换为Integer即可让输出元素数量为5。

需要说明的是,不仅仅是int类型的数组有这个问题,其他7个基本类型的数组也存在相似的问题。在把基本类型数组转换成列表时,要特别小心asList方法的陷阱,避免出现程序逻辑混乱的情况。

注意:原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱。

66:asList方法产生的List对象不可更改

1
2
3
4
5
6
7
8
9
public class Client {
    enum Week {Sun, Mon, Tue, Wed, Thu, Fri, Sat}

    public static void main(String[] args) {
        Week[] workDays = {Week.Mon, Week.Tue, Week.Wed, Week.Thu, Week.Fri};
        List<Week> list = Arrays.asList(workDays);
        list.add(Week.Sat);
    }
}

运行结果:

Exception in thread “main” java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108) at com.company.section1.Client.main(Client.java:12)

Arrays.asList方法的源码:

1
2
3
4
5
@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

直接new了一个ArrayList对象返回,难道ArrayList不支持add方法?不可能呀!可能,问题就出在这个ArrayList类上,此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内置类:

 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

    @Override
    public int size() {
        return a.length;
    }

    @Override
    public Object[] toArray() {
        return a.clone();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        int size = size();
        if (a.length < size)
            return Arrays.copyOf(this.a, size,
                                 (Class<? extends T[]>) a.getClass());
        System.arraycopy(this.a, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

    @Override
    public E get(int index) {
        return a[index];
    }

    @Override
    public E set(int index, E element) {
        E oldValue = a[index];
        a[index] = element;
        return oldValue;
    }

    @Override
    public int indexOf(Object o) {
        E[] a = this.a;
        if (o == null) {
            for (int i = 0; i < a.length; i++)
                if (a[i] == null)
                    return i;
        } else {
            for (int i = 0; i < a.length; i++)
                if (o.equals(a[i]))
                    return i;
        }
        return -1;
    }

    @Override
    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(a, Spliterator.ORDERED);
    }

    @Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        for (E e : a) {
            action.accept(e);
        }
    }

    @Override
    public void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        E[] a = this.a;
        for (int i = 0; i < a.length; i++) {
            a[i] = operator.apply(a[i]);
        }
    }

    @Override
    public void sort(Comparator<? super E> c) {
        Arrays.sort(a, c);
    }
}

这里的ArrayList是一个静态私有内部类,除了Arrays能访问外,其他类都不能访问。仔细看这个类,它没有提供add方法,那肯定是父类AbstractList提供了。

AbstractList.add方法:

1
2
3
public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

父类确实提供了,但没有提供具体的实现。

这个ArrayList静态内部类对于经常使用的List.add和List.remove方法它都没有实现,也就是说asList返回的是一个长度不可变的列表,数组是多长,转换成的列表也就是多长,换句话说此处的列表只是数组的一个外壳,不再保持列表动态变长的特性。

所以,以下代码写法需要慎之戒之,除非非常自信该List只用于读操作。

1
List<String> names = Arrays.asList("zhangsan", "lisi", "wangwu");

67:不同的列表选择不同的遍历方法

ArrayList数组实现了RandomAccess接口(随机存取接口),这也就标志着ArrayList是一个可以随机存取的列表。在Java中,RandomAccess和Cloneable、Serializable一样,都是标志性接口,不需要任何实现,只是用来表明其实现类具有某种特质的,实现了Cloneable表明可以被拷贝,实现了Serializable接口表明被序列化了,实现了RandomAccess则表明这个类可以随机存取。

Java为ArrayList类加上了RandomAccess接口,就是在告诉我们,“嘿,ArrayList是随机存取的,采用下标方式遍历列表速度会更快”,接着又有一个问题了:为什么不把RandomAccess加到所有的List实现类上呢?

那是因为有些List实现类不是随机存取的,而是有序存取的,比如LinkedList类,LinkedList也是一个列表,但它实现了双向链表,每个数据结点中都有三个数据项:前节点的引用(Previous Node)、本节点元素(Node Element)、后继节点的引用(Next Node)。

List的get方法:

1
2
3
public interface List<E> extends Collection<E> {
	E get(int index);
}

ArrayList的get方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  public E get(int index) {
      rangeCheck(index);

      return elementData(index);
  }

  @SuppressWarnings("unchecked")
  E elementData(int index) {
    return (E) elementData[index];
  }
}

LinkedList的get方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
	public E get(int index) {
      checkElementIndex(index);
      return node(index).item;
  }
  Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
      Node<E> x = first;
      for (int i = 0; i < index; i++)
        x = x.next;
      return x;
    } else {
      Node<E> x = last;
      for (int i = size - 1; i > index; i--)
        x = x.prev;
      return x;
    }
  } 
}

不同的列表采用不同的遍历方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static int average(List<Integer> list) {
    int sum = 0;
    if (list instanceof RandomAccess) {
        for (int i = 0, size = list.size(); i < size; i++) {
            sum += list.get(i);
        }
    } else {
        for (int i : list) {
            sum += i;
        }
    }
    return sum / list.size();
}

68:频繁插入和删除时使用LinkedList

69:列表相等只需关心元素数据

1
2
3
4
5
6
7
8
ArrayList<String> strs = new ArrayList<>();
strs.add("A");

Vector<String> strs2 = new Vector<>();
strs2.add("A");
System.out.println(strs.equals(strs2));

//true

一个是ArrayList,一个是Vector,两者都是列表(List),都实现了List接口,也都继承了AbastractList抽象类,其equals方法是在AbstractList中定义的。

AbstractList的equals方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof List))
        return false;

    ListIterator<E> e1 = listIterator();
    ListIterator<?> e2 = ((List<?>) o).listIterator();
    while (e1.hasNext() && e2.hasNext()) {
        E o1 = e1.next();
        Object o2 = e2.next();
        if (!(o1==null ? o2==null : o1.equals(o2)))
            return false;
    }
    return !(e1.hasNext() || e2.hasNext());
}

Compares the specified object with this list for equality. Returns true if and only if the specified object is also a list, both lists have the same size, and all corresponding pairs of elements in the two lists are equal. (Two elements e1 and e2 are equal if (e1==null ? e2==null : e1.equals(e2)).) In other words, two lists are defined to be equal if they contain the same elements in the same order.

This implementation first checks if the specified object is this list. If so, it returns true; if not, it checks if the specified object is a list. If not, it returns false; if so, it iterates over both lists, comparing corresponding pairs of elements. If any comparison returns false, this method returns false. If either iterator runs out of elements before the other it returns false (as the lists are of unequal length); otherwise it returns true when the iterations complete.

列表只是一个容器,只要是同一种类型的容器(如List),不用关心容器的细节差别(如ArrayList与LinkedList),只要确定所有的元素数据相等,那这两个列表就是相等的。

其他的集合类型,如Set、Map等与此相同,也是只关心集合元素,不用考虑集合类型。

判断集合是否相等时只须关注元素是否相等即可。

70:子列表只是原列表的一个视图

1
2
3
4
5
6
7
8
List<String> c = new ArrayList<>();
c.add("A");
c.add("B");
List<String> c1 = new ArrayList<>(c);
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));

运行结果:

c == c1? false c == c2? true

List的subList方法:

1
List<E> subList(int fromIndex, int toIndex);

Returns a view of the portion of this list between the specified fromIndex, inclusive, and toIndex, exclusive. (If fromIndex and toIndex are equal, the returned list is empty.) The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa. The returned list supports all of the optional list operations supported by this list.

This method eliminates the need for explicit range operations (of the sort that commonly exist for arrays). Any operation that expects a list can be used as a range operation by passing a subList view instead of a whole list. For example, the following idiom removes a range of elements from a list:

1
      list.subList(from, to).clear();

Similar idioms may be constructed for indexOf and lastIndexOf, and all of the algorithms in theCollections class can be applied to a subList.

The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)

AbstractList的subList方法:

1
2
3
4
5
public List<E> subList(int fromIndex, int toIndex) {
    return (this instanceof RandomAccess ?
            new RandomAccessSubList<>(this, fromIndex, toIndex) :
            new SubList<>(this, fromIndex, toIndex));
}

subList方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,不过,随机存储的使用频率比较高,而且RandomAccessSubList也是SubList子类,所以所有的操作都是由SubList类实现的(除了自身的SubList方法外)。

SubList类的代码:

 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
class SubList<E> extends AbstractList<E> {
    private final AbstractList<E> l;
    private final int offset;
    private int size;

    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
        if (fromIndex < 0)
            throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
        if (toIndex > list.size())
            throw new IndexOutOfBoundsException("toIndex = " + toIndex);
        if (fromIndex > toIndex)
            throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                               ") > toIndex(" + toIndex + ")");
        l = list;
        offset = fromIndex;
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }

    public E set(int index, E element) {
        rangeCheck(index);
        checkForComodification();
        return l.set(index+offset, element);
    }
  ...
}

subList方法的实现原理:它返回的SubList类也是AbstractList的子类,其所有的方法如get、set、add、remove等都是在原始列表上的操作,它自身并没有生成一个数组或是链表,也就是子列表只是原列表的一个视图(View),所有的修改动作都反映在了原列表上。

我们例子中的c2增加了一个元素C,不过增加的元素C到了c列表上,两个变量的元素仍保持完全一致,相等也就很自然了。

解释完相等的问题,再回过头来看看为什么变量c与c1不相等。很简单,因为通过ArrayList构造函数创建的List对象c1实际上是新列表,它是通过数组的copyOf动作生成的,所生成的列表c1与原列表c之间没有任何关系(虽然是浅拷贝,但元素类型是String,也就是说元素是深拷贝的),然后c又增加了元素,因为c1与c之间已经没有一毛钱的关系了,那自然是不相等了。

注意:subList产生的列表只是一个视图,所有的修改动作直接作用于原列表。

71:推荐使用subList处理局部列表

72:生成子列表后不要再操作原列表

1
2
3
4
5
6
7
8
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> subList = list.subList(0, 2);
list.add("D");
System.out.println("list size: " + list.size());
System.out.println("subList size: " + subList.size());

运行结果:

list size: 4 Exception in thread “main” java.util.ConcurrentModificationException at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241) at java.util.ArrayList$SubList.size(ArrayList.java:1050) at com.company.section2.Client.main(Client.java:15)

subList的size方法出现了并发修改异常:

因为subList取出的列表是原列表的一个视图,原数据集(代码中的list变量)修改了,但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),后面在对子列表继续操作时,就会检测到修改计数器与预期的不相同,于是就抛出了并发修改异常。

SubList的size方法:

1
2
3
4
public int size() {
    checkForComodification();
    return this.size;
}

checkForComodification方法:

1
2
3
4
private void checkForComodification() {
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}

SubList的其他方法也会检测修改计数器modCount,例如set、get、add等方法,若生成子列表后,再修改原列表,这些方法也会抛出ConcurrentModificationException异常。对于子列表操作,因为视图是动态生成的,生成子列表后再操作原列表,必然会导致“视图”的不稳定,最有效的办法就是通过Collections. unmodifiableList方法设置列表为只读状态。

1
2
3
4
5
6
7
8
9
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> subList = list.subList(0, 2);
list = Collections.unmodifiableList(list);
subList.add("D");
System.out.println("list size: " + list.size());
System.out.println("subList size: " + subList.size());

73:使用Comparator进行排序

74:不推荐使用binarySearch对列表进行检索

ArrayList的indexOf方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

Collections的binarySearch方法:

1
2
3
4
5
6
7
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

indexedBinarySearch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private static <T>
int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
    int low = 0;
    int high = list.size()-1;

    while (low <= high) {
        int mid = (low + high) >>> 1;
        Comparable<? super T> midVal = list.get(mid);
        int cmp = midVal.compareTo(key);

        if (cmp < 0)
            low = mid + 1;
        else if (cmp > 0)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found
}

iteratorBinarySearch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private static <T>
int iteratorBinarySearch(List<? extends Comparable<? super T>> list, T key)
{
    int low = 0;
    int high = list.size()-1;
    ListIterator<? extends Comparable<? super T>> i = list.listIterator();

    while (low <= high) {
        int mid = (low + high) >>> 1;
        Comparable<? super T> midVal = get(i, mid);
        int cmp = midVal.compareTo(key);

        if (cmp < 0)
            low = mid + 1;
        else if (cmp > 0)
            high = mid - 1;
        else
            return mid; // key found
    }
    return -(low + 1);  // key not found
}

注意这里:

1
int cmp = midVal.compareTo(key);

indexOf是通过equals方法判断的,equals等于true就认为找到符合条件的元素了,而binarySearch查找的依据是compareTo方法的返回值,返回0即认为找到符合条件的元素。

仔细审查一下代码,我们覆写了compareTo和equals方法,但是两者并不一致。使用indexOf方法查找时,遍历每个元素,然后比较equals方法的返回值,因为equals方法是根据code判断的,因此当第一次循环时,equals就返回了true,indexOf方法结束,查找到指定值。而使用binarySearch二分法查找时,依据的是每个元素的compareTo方法返回值,而compareTo方法又是依赖name属性的,name相等就返回0,binarySearch就认为找到元素了。

注意:从性能方面考虑,binarySearch是最好的选择。

75:集合中的元素必须做到compareTo和equals同步

  • indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找。
  • equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同。

76:集合运算时使用更优雅的方式

并集:

1
list1.addAll(list2);

交集:

1
list1.retainAll(list2);

差集:

1
list1.removeAll(list2);

无重复的并集:

1
2
list2.removeAll(list1);
list1.addAll(list2);

很少有程序员使用JDK提供的方法来实现这些集合操作,基本上都是采用的标准的嵌套for循环:要并集就是加法,要交集了就使用contains判断是否存在,要差集了就使用!contains(不包含),有时候还要为这类操作提供一个单独方法,看似很规范,但已经脱离了优雅的味道。

77:使用shuffle打乱列表

1
Collections.shuffle(tagClouds);

78:减少HashMap中元素的数量

 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
38
39
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

HashMap比ArrayList多了一个层Node的底层对象封装,多占用了内存,并且它的扩容策略是2倍长度的递增,同时还会依据阀值判断规则进行判断,因此相对于ArrayList来说,它就会先出现内存溢出。

79:集合中的哈希码不要重复

HashMap比ArryList快了40多倍!两者的contains方法都是判断是否包含指定值,为何差距如此巨大呢?而且如果数据量增大,差距也会非线性地增大。

HashMap的containsKey方法:

1
2
3
public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

80:多线程使用Vector或HashTable

Vector是ArrayList的多线程版本,HashTable是HashMap的多线程版本。

 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
public static void main(String[] args) {
    final List<String> tickets = new ArrayList<>();
    for (int i = 0; i < 100000; i++) {
        tickets.add("ticket" + i);
    }

    Thread returnThread = new Thread() {
        public void run() {
            while (true) {
                tickets.add("ticket" + new Random().nextInt());
            }
        }

        ;
    };

    Thread saleThread = new Thread() {
        public void run() {
            for (String ticket : tickets) {
                tickets.remove(ticket);
            }
        }

        ;
    };

    returnThread.start();
    saleThread.start();

}

运行结果:

Exception in thread “Thread-1” java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911) at java.util.ArrayList$Itr.next(ArrayList.java:861) at com.company.Client$2.run(Client.java:28)

final List<String> tickets = new ArrayList<>();的ArrayList换成Vector,依然抛出相同的异常。

这是因为混淆了线程安全和同步修改异常,基本上所有的集合类都有一个叫做快速失败(Fail-Fast)的校验机制,当一个集合在被多个线程修改并访问时,就可能会出现ConcurrentModificationException异常,这是为了确保集合方法一致而设置的保护措施,它的实现原理就是我们经常提到的modCount修改计数器:如果在读列表时,modCount发生变化(也就是有其他线程修改)则会抛出ConcurrentModificationException异常。这与线程同步是两码事,线程同步是为了保护集合中的数据不被脏读、脏写而设置的。

虽然在系统开发中一再说明,除非必要,否则不要使用synchronized,这是从性能的角度考虑的,但是一旦涉及多线程时(注意这里说的是真正的多线程,不是并发修改的问题,比如一个线程增加,一个线程删除,这不属于多线程的范畴),Vector会是最佳选择,当然自己在程序中加synchronized也是可行的方法。

81:非稳定排序推荐使用List

SortedSet接口(TreeSet实现了该接口)只是定义了在给集合加入元素时将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,比如String、Integer等类型,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合。