首页 > 代码库 > 改善java程序的151个建议--数组和集合

改善java程序的151个建议--数组和集合

60、性能考虑,数组是首选,在基本类型处理方面,数组还是占优势的,而且集合类的底层也都是通过数组实现,建议在性能要求较高的场景中使用数组替代集合。

61、如果有必要,使用变长数组:我们可以通过对数组扩容”婉转”地解决数组扩容问题,下面采用的是Arrays数组工具类的copyOf方法,产生了一个newLen长度的新数组,并把原有的值拷贝了进去,之后就可以对超长的元素进行赋值了

技术分享

62、警惕数组的浅拷贝

Person[] p=new Person[5];

Person[] p2=Arrays.copyOf(p,p.length);

如果修改了p2中某个属性的值,例如p2username的值,那么p也会修改,这就是所说的浅拷贝,解决方式是:遍历p,然后重新生成一个Person放入p2中,这就实现了深拷贝,我们在使用集合的时候,例如List,应该避免这样的使用方式:Arrays.copyOf(list.toArray());

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

ArrayList是一个大小可变的数组,但它在底层使用的是数组存储,而且数组是定长的,要实现动态长度必要要进行长度的扩展,底层实现是在元素达到elementData长度的临界点时,将elementData扩容1.5倍,为什么是1.5倍呢,这是因为经过测试验证,扩容1.5倍即满足了性能要求,也减少了内存消耗,源代码如下:

 技术分享

接下来我们看一下ArrayList的构造函数如下:

 技术分享

上面是无参数构造,下面是指定容量的有参函数构造,如果我们不设置容量,系统就按照1.5倍的规则扩容,每次扩容都是一次数组的拷贝,如果数据量很大,这样的拷贝会非常耗费资源,而且效率非常低下。如果我们已经知道了一个ArrayList的可能长度,尽量避免了数组拷贝,然后对其设置一个初始容量则可以显著提高系统性能。其他集合都一样,下面是Vector底层数组的长度计算方式:

技术分享

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

1)自行实现,快速查找最大值

 技术分享

2)先排序,后取值,下面数组使用了clone方法的主要原因是不影响原数组中的排序

 技术分享

3)查找仅次于最大值的元素

 技术分享

最值计算时,使用集合最简单,使用数组性能最优;

65、避开基本类型数组转换列表的陷进

int[] data=http://www.mamicode.com/{1,2,3,4,5};List list=Arrays.asList(data);list.size()得到的结果为1,这是因为asList的方法接收的是一个变长参数,返回一个固定长度的列表,其源代码如下:

 技术分享

数组是一个对象,它是可以泛型化的,也就是说这里把一个int类型的数组作为了T类型,所以转换后在List中就只有一个类型为int数组的元素了,修改方案是将int改为Integer

Integer[] data=http://www.mamicode.com/{1,2,3,4,5};List list=Arrays.asList(data);list.size()得到的结果为5

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

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

看下面一段程序:

	public static void main(String[] args) {		Integer[] datas={1,2,3,4};		List<Integer> list=Arrays.asList(datas);		list.add(6);	}

运行结果抛出:UnsupportedOperationException,原因是此asList底层实现是返回了一个ArrayList,此ArrayList并不是java.util.ArrayList,而是Arrays工具类的一个静态私有内部类,除了Arrays能访问外,其他类都不能访问,这个类中没有提供add方法,其父类AbstractList提供的add方法中抛出该异常,它仅仅实现了5个方法:size()toArray()get()set()contains()asList返回的是一个长度不可变的列表,数组是多长,转换成的列表也就是多长,换句话说,此处的列表只是数组的一个外壳,不再保持列表动态变长的特性,所以非常不推荐如下方式定义和初始化列表:

List<String> list=Arrays.asList(“abc”,”ddd”,”sss”);这个list只能进行读操作

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

我们有这样一个需求,将100万数据放入ArrayListLinkedList中,然后分别通过foreach的方式和遍历下标的方式进行求和并计算平均值运算,得出结果如下:

通过ArrayList下标遍历耗时:2ms,得出平均值为:49

通过ArrayList foreach遍历耗时:6ms,得出平均值为:49

通过LinkedList下标遍历耗时:5515ms,得出平均值为:49

通过LinkedList foreach遍历耗时:3ms,得出平均值为:49

我们可以知道,通过下标遍历的方式:for(int i=0,size=list.size();i<size;i++)对比,ArrayList的效率远远比LinkedList的效率要高,而通过foreach遍历的方式:for(Integer i:list)对比,LinkedList的效率比ArrayList好,这是因为ArrayList实现了RandomAccess接口(随机存取接口),也就标志着ArrayList是一个可以随机存取的列表,也就说明了其中的数据元素之间是没有关联的,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存储,而使用迭代器就需要强制建立一种相互关系,比如上一个元素可以判断是否有下一个元素,以及下一个元素是什么等等,所以使用ArrayList通过foreach遍历需要为其添加关系,这就是耗时的原因。LinkedList它底层实现了双向链表,其中的两个元素本来就是有关联的,所以使用foreach会效率会比较高,通过底层代码会发现,LinkedListget方法会每次都会进行一次遍历,所以效率非常低。

上面说的就是随机存储列表(例如ArrayList)和有序存取列表(例如LinkedList)的区别,随机存储列表,实现了RandomAccess接口,使用下标遍历的方式效率高,有序存储列表,其元素之间本来就存在关系,使用foreach遍历的方式效率高,我们可以写一个万能遍历方式:

public static void searchQuick(List<Integer> list,String listName){		int sum=0;		String str="";		long startTime=System.currentTimeMillis();		if(list instanceof RandomAccess){			str+=listName+"随机存储,使用下标遍历方式最快";			for(int i=0,size=list.size();i<size;i++){				sum+=list.get(i);			}		}else{			str+=listName+"有序存储,使用foreach遍历方式最快";			for(Integer i:list){				sum+=i;			}		}		long endTime=System.currentTimeMillis();		System.out.println(str+",遍历耗时:"+(endTime-startTime)+"ms,得出平均值为:"+sum/list.size());	}

【知识拓展】RandomAccessCloneableSerializable一样,都是标志性接口,不需要任何实现,只是用来表明其实现类具有某种特质的,实现了Cloneable表明可以被拷贝,实现了Serializable表明可以被序列化,实现了Serializable表明这个类可以进行随机存储(元素间没有关系,可以随机访问和存储)

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

在指定位置插入元素:add(int index,E element)

ArrayList:底层是数组实现,每次插入一个元素,其后的元素就会向后移动一位,效率比较低

LinkedList:底层是一个双向链表,它的插入只是修改相邻元素的nextprevious的引用,效率高

删除元素:remove(int index),和上面一样,LinkedList效率比ArrayList

修改元素:set(int index,E element)ArrayListLinkedList效率高,因为LinkedList是顺序存储的,定位元素是一个折半遍历过程,比较耗时,效率低,ArrayList的修改动作则是数组元素的直接替换,简单高效。

增加元素:add(E elment);ArrayListLinkedList在性能上基本没什么差别

综上所述,适当的场景选择适当的集合操作

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

		ArrayList<String> strs=new ArrayList<String>();		strs.add("abc");		Vector<String> strs2=new Vector<String>();		strs2.add("abc");		LinkedList<String> strs3=new LinkedList<String>();		strs3.add("abc");		System.out.println(strs.equals(strs2));		System.out.println(strs.equals(strs3));		System.out.println(strs2.equals(strs3));

如上程序,程序输出的结果都是true,这是因为这三个列表都实现了List接口,也都继承了AbstractList抽象类,其equals方法是在AbstractList中定义的,源代码如下:

 技术分享

从源代码中可以看出,只要求实现了List接口就成,它不关心List的具体实现类,只要所有的元素相等,并且长度也相等就表明两个List是相等的,与具体的容量类型无关。其他的集合类型,如SetMap也差不多,只关心集合元素,不用考虑集合类型。

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

70、子列表只是原列表的一个视图,subList产生的列表只是原列表的一个视图,所有的修改动作直接作用于原列表

看下面的程序:

	List<String> list1=new ArrayList<String>();		list1.add("A");		list1.add("B");		//构造一个包含list1列表的字符串列表		List<String> list2=new ArrayList<String>(list1);		System.out.println(list1.equals(list2));		//使用subList生成与list1相同的列表		List<String> list3=list1.subList(0, list1.size());		System.out.println(list1.equals(list3));		//list3中增加一个元素		list3.add("C");		System.out.println(list1);//A、B、C		System.out.println(list3);//A、B、C		System.out.println(list1.equals(list2));		System.out.println(list1.equals(list3));

上面的结果输出为:truetruefalsetrue,前两个我们都知道,那么后面两个为什么会出现这样的结果呢!

这是因为通过subList生成的List是原有List的一个视图,也就是说list3list1的一个视图,对list3的所有的修改动作都会反映在原列表list1上,所以上面中list3增加一个元素C的时候,此时list1也会增加一个C,list1list3保持一致,自然而然就相等了;通过ArrayList构造函数创建的list2实际上它是一个新列表了,它是通过数组的copyOf(浅拷贝)生成的,生成的list2和原列表list1之间没有任何关系了

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

需求:一个列表有100个元素,现在要删除索引位置为2030的元素,我们除了使用循环遍历进行删除外,还可以使用subList进行删除:

list.subList(20,30).clear();因为subList生成的List是原列表的一个视图,所有对subList生成List的操作都会作用到原列表

72、生成子列表后不要再操作原列表,保持原列表的只读状态

下面代码:

		List<String> list1=new ArrayList<String>();		list1.add("A");		list1.add("B");		list1.add("C");		List<String> list2=list1.subList(0, 1);		list2.add("D");		System.out.println(list1.size());		System.out.println(list2.size());

在执行list2.size()的时候会抛出ConcurrentModificationException(并发修改异常),这是因为对原列表list1进行修改后(增加一个元素D)list2取出的列表不会重新生成一个新列表,后面对子列表list2进行操作时,就会检测到修改计数器与预期的不同,于是就抛出了并发修改异常。子列表提供的size方法中检查修改计数器源代码:

 技术分享

subList的其他方法也检测修改计数器,例如setgetadd等方法,若生成子列表后,再修改原列表,这些方法也会抛出ConcurrentModificationException异常

对于子列表操作,因为视图是动态生成的,生成子列表后再操作原列表,必然会导致”视图”的不稳定,最有效的办法就是通过Collections.unmodifiableList方法设置列表为只读状态(防御式编程)

 技术分享

我们的List可以有多个视图,就是说可以有多个子列表,只要生成的子列表多于一个,则任何一个子列表就都不能修改了,否则就会抛出上面的异常!

		List<String> list1=new ArrayList<String>();		list1.add("A");		list1.add("B");		list1.add("C");		//表示list1只读		list1=Collections.unmodifiableList(list1);		//只读后不能对list1进行写入操作,抛出		//ConcurrentModificationException		list1.add("F");		//有多个子列表		List<String> list2=list1.subList(0, 1);		List<String> list3=list1.subList(0, 2);		//多个子列表,就不能对任何子列表进行写入操作了		list3.add("G");		//抛出ConcurrentModificationException		System.out.println(list2.size());

73、使用ComparatorComparable进行排序

我们创建Employee默认让其可以按照id进行排序,实现代码如下:

class Employee implements Comparable<Employee>{		private int id;	private String name;		public Employee(int id,String name){		this.id=id;		this.name=name;	}	public int getId() {		return id;	}	public void setId(int id) {		this.id = id;	}	public String getName() {		return name;	}	public void setName(String name) {		this.name = name;	}		@Override	public int compareTo(Employee e) {		System.out.println("execute compareTo");		Integer eId=Integer.valueOf(this.id);		return eId.compareTo(Integer.valueOf(e.getId()));	}}

测试代码如下:

List<Employee> list=new ArrayList<Employee>();		list.add(new Employee(100,"Mary"));		list.add(new Employee(99,"Jack"));		list.add(new Employee(101,"Lucy"));		list.add(new Employee(103,"John"));		list.add(new Employee(102,"Tom"));		//执行排序,调用Employee默认排序compareTo方法		Collections.sort(list);		for(Employee e:list){			System.out.println("id="+e.getId()+",name="+e.getName());		}

运行后的结果是按照id进行升序排序的,在测试的时候,调用Collections.sort()方法会执行Employee中的compareTo方法

如果我们进行需求改进,需要对Employee中的name进行排序,那么我们该怎么处理呢,当然我们可以修改源代码,但是更好的处理方式是我们定义一个comparator,然后调用Collections.sort(List list,Comparator);重载方法,编写我们的NameComparator

/** * 按照名称进行排序 * @author dell * */class NameComparator implements Comparator<Employee>{	@Override	public int compare(Employee e1, Employee e2) {		System.out.println("execute NameComparator");		return e1.getName().compareTo(e2.getName());	}}

测试:测试结果为按照名称的升序进行排序

	//按照名称自己定制排序,实现Comparator接口,默认排序不会被执行		Collections.sort(list,new NameComparator());		for(Employee e:list){			System.out.println("id="+e.getId()+",name="+e.getName());		}

ComparableComparator:实现了Comparable接口的类表明自身是可以进行比较的,有了比较才能进行排序,而Comparator接口是一个工具类接口,它的名字也已经表明其作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑;Comparable接口可以作为实现类的默认排序法,一个类只能有一个固定的、由compareTo方法提供的默认排序算法;Comparator接口则是一个类的扩展排序工具,一个类可以有很多的比较器

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

情景,我们需要在List中查找出一个元素的索引,我们可以通过indexOf的方式进行查询,也可以通过binarySearch的方式进行检索,只是通过binarySearch进行检索的时候存在List中的元素必须是有序的,否则会出错,这就可能会打乱了我们实际从数据库或者web等查询出来的集合数据,但是使用binarySearch检索性能高、效率快,所以我们有类似需求的时候我们得根据实际场景来决定使用的方法。

75、集合中的元素必须做到compareToequals同步

我们在进行indexOfbinarySearch方法进行索引查找的时候,如果compareTo中的比较与equals方法中的比较不一致,例如compareTo中比较的是id属性,而equals中比较的是name属性,那么就会造成结果不一样,这是因为:

indexOf是通过equals方法判断的,equals等于true就认为找到符合条件的元素,而binarySearch查找的依据是compareTo方法的返回值,返回0即认为找到符合条件的元素,如果我们覆写这两个方法的时候不一致,就会出现问题;equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同,所以我们就应该保证当排序位置相同的时候,其equals也相同,实现了compareTo方法,就应该覆写equals方法,确保两者同步,否则就会产生逻辑混乱

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

集合的交集、并集、差集的操作在持久层中使用得非常频繁,从数据库中取出的就是多个数据集合,之后我们可以使用集合中的各种方法构建我们需要的数据,需要两个集合的and结构,就是交集,需要两个集合的or结果,那就是并集,需要两个集合的not结果,那就是差集

集合求并集:list1.addAll(list2);

集合求交集:list1.retainAll(list2);

集合求差集:属于list1但不属于list2list1.removeAll(list2);

无重复的并集:1、删除list1中出现的元素:list2.remove(list1);2、把剩余的list2元素添加到list1中:list1.addAll(list2);

77、使用shuffle打乱列表

当我们要对List集合中的元素进行顺序打乱的时候,我们可能会想到通过一个随机数,然后遍历List集合,将元素与随机元素交换顺序:

 技术分享

其实我们还可以使用一个方法很方便的来实现上面的乱序操作:

Collections.shuffle(List list);我们可以根据项目适当的场景来使用此功能

78、减少HashMap中元素的数量

HashMap在底层比ArrayList多了一层Entry的对象封装,多占用了内存,并且它的扩容策略是2倍长度的递增,同时还会根据阀值判断规则进行判断,因此相对于ArrayList来说,大量添加数据会先出现内存溢出,所以尽量让HashMap中的元素少量并简单

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

一个列表查找某值是非常耗资源的,随机存储的列表是遍历查找,顺序存储列表是链表查找,或者是Collections的二分法查找,但是这都不怎么快,最快的还是要数Hash开头的集合,例如HashMapHashSet等等的查找,例如我们同样在ArrayListHashMap添加100000个元素,当我们使用contains方法进行查找最后一个值的时候,会发现HashMap的效率比ArrayList高出很多

HashMap的查找原理:其中使用hashCode定位元素,如有哈希冲突,则遍历对比,在没有哈希冲突的情况下,HashMap的查找则是依赖hashCode定位的,因为是直接定位,效率当然就高;如果哈希码相同,它的查找效率就与ArrayList没什么两样了,所以在一些项目中,虽然我们重写了hashCode方法但是返回值确是固定的,此时如果把这些对象插入到HashMap中,查找就会比较耗时了,在HashMap中的hashCode应避免冲突。

80、多线程环境下考虑使用VectorHashTable

我们这里需要理解两个概念:线程安全和同步修改的问题

同步修改:多个线程对同一对象不同方法进行操作,例如addremove,并发修改:一个线程增加,一个线程删除,这不属于多线程

	/**	 * 这属于同步修改问题,不是线程同步,多个线程访问同一对象的不同的方法	 * ArrayList和Vector都会抛出ConcurrentModificationException异常	 * @param datas	 */	public static void synchroUpdate(final List<String> datas){		Thread addThread=new Thread(){			@Override			public void run() {				while(true){					datas.add("新增:"+new Random().nextInt());				}			}		};		//减少		Thread removeThread=new Thread(){			@Override			public void run() {				for(String str:datas){					datas.remove(str);				}			}		};		addThread.start();		removeThread.start();	}

线程安全:多个线程对同一对象的同一个方法进行操作,例如多个线程访问remove方法

	/**	 * 多线程问题,多个线程访问同一对象的同一个方法	 * 如果使用ArrayList,则datas.remove(0)可能会取出同样的数据,这是线程不安全所造成的,我们可以使用synchronized解决	 * 如果使用Vector,因为它是线程安全的,所以datas.remove(0)不会出现同样的数据,它的内部实现已经使用了synchronized	 * @param datas	 */	public static void mulThread(final List<String> datas){		for(int i=0;i<10;i++){			new Thread(){				public void run() {					while(true){						System.out.println(Thread.currentThread().getId()+"--"+datas.remove(0));					}				};			}.start();		}	}

系统开发过程中,除非必要,否则不要使用synchronized关键字,这是从性能的角度考虑的,但是一旦涉及多线程的时候(这里说的是真正的多线程,而不是并发修改的问题,比如一个线程增加,一个线程删除,这不是多线程)VectorHashTable会是最佳的选择,使用ArrayListHashMap结合synchronized也是可行的方法。

81、SortedSot中的元素被修改后可能会影响其排序位置,非稳定排序推荐使用List

非稳定:不确定集合中的元素何时会发生变化

TreeSet:实现了类默认排序为升序的Set集合,其中的元素不能重复,TreeSet实现了SortedSet接口,它只是定义了在给集合加入元素时将其进行排序,不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,例如StringInteger等类型,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合,修改后的TreeSet再排序解决方案:

New TreeSet<Person>(new ArrayList<Person>(TreeSet set));

List:实现自动排序,即使修改也能进行自动排序,我们可以使用Collections.sort方法对List进行排序,但是List中的元素是可以重复的

相应排序选择相应的解决方案!

82、集合大家族

List:集合中元素可重复

    ArrayList:一个动态数组

    LinkedList:一个双向链表

    Vector:一个线程安全的动态数组

    Stack:一个对象栈,遵循先进后出的原则

Set:集合中元素不能重复

    EnumSet:枚举类型的专用Set,其中所有元素都是枚举类型

    HashSet:是以哈希码决定其元素位置的Set,原理与HashMap相似,提供快速的插入和查找方法

    TreeSet:是一个自动排序的Set,它实现了SortedSet接口

Map:键值集合

    排序MapTreeMap,它根据Key值进行自动排序

    非排序MapHashMapHashTablePropertieEnumMap,其中PropertiesHashTable的子类,它的用途是从Property文件中加载数据,并提供方便的读写操作;EnumMap是要求其Key必须是某一个枚举类型

    WeakHashMap:是一个采用弱键方式实现的Map类,它的特点是:WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,使用其装载数据不用担心内存溢出问题,GC会自动删除不用的键值对,但是GC是悄悄回收的,程序不知该动作,所以存在重大的隐患

Queue

    阻塞式队列:队列满了以后再插入元素则会抛出异常,其中主要有:

    ArrayBlockingQueue:一个以数组方式实现的有界阻塞队列

    PriorityBlockingQueue:依照优先级组建的队列

     LinkedBlockingQueue:通过链表实现的阻塞队列

    非阻塞队列:无边界的,只要内存允许,都可以持续追加元素,最常使用的是PriorityQueue

    双端队列,支持在头、尾两端插入和移出元素:ArrayDequeLinkedBlockingDequeLinkedList

数组:数组和集合最大的区别就是数组能够容纳基本类型,而集合就不行,它容纳的基本类型数据也是通过该java装箱转换为对象类型放进去的,所有的集合底层存储的都是数组

工具类:

    数组工具类:java.util.Arraysjava.lang.reflect.Array

    集合工具类:java.util.Collections

扩展类:集合类我们可以自行扩展,最好的办法是拿来主义,可以使用Apache中的commons-collections扩展包,也可以使用Google中的google-collections这些优秀的数据集合扩展包

改善java程序的151个建议--数组和集合