60 性能考虑,数组是首选
package qz.test.equals;
import java.util.List;
public class ArrayTest {
public static void main(String[] args) {
}
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 datas){
int sum = 0;
for (int i = 0; i < datas.size(); i++) {
sum += datas.get(i);
}
return sum;
}
}
基本类型是在栈内存中操作的,而对象则是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大。
61 若有必要,使用变长数组
public static T[] expandCapacity(T[] datas,int newLen){
//不能是负值
newLen = newLen < 0 ? 0 : newLen;
//生成一个新数组,并拷贝原值
return Arrays.copyOf(datas,newLen);
}
62 警惕数组的浅拷贝
package qz.test.equals;
import java.util.Arrays;
public class Client {
public static void main(String[] args) {
//气球数量
int ballonNum = 7;
//第一个箱子
Ballon[] box1 = new Ballon[ballonNum];
//初始化第一个箱子中的气球
for (int i = 0; i < box1.length; i++) {
box1[i] = new Ballon(Color.values()[i],i);
}
//第二个箱子的气球是拷贝的第一个箱子里的
Ballon[] box2 = Arrays.copyOf(box1, box1.length);
//修改最后一个气球颜色
box2[6].setColor(Color.Blue);
//打印出第一个箱子中的气球颜色
for (Ballon ballon : box1) {
System.out.println(ballon);
}
}
}
//气球颜色
enum Color {
Red,Orange,Yellow,Green,Indigo,Blue,Violet;
}
//气球
class Ballon{
//编号
private int id;
//颜色
private Color color;
public Ballon(Color _color,int _id){
id = _id;
color = _color;
}
//apache-common包下的ToStringBuilder重写toString方法
@Override
public String toString(){
return new ToStringBuilder(this).append("编号",id).append("颜色",color).toString();
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Color getColor() {
return color;
}
public void setColor(Color color) {
this.color = color;
}
}
编号:0,颜色:Red
编号:1,颜色:Orange
编号:2,颜色:Yellow
编号:3,颜色:Green
编号:4,颜色:Indigo
编号:5,颜色:Blue
编号:6,颜色:Blue
通过copyOf方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型是直接拷贝值,其他都是拷贝引用地址。数组的clone方法也是与此相同,同样是浅拷贝而且集合的clone方法也都是浅拷贝。
63 在明确的场景下,为集合指定初始容量
ArrayList的add实现
public boolean add(E e){
//扩展长度
ensureCapacity(size + 1);
//追加元素
elementData[size++] = e;//数组存储
return true;
}
public void ensureCapacity(int minCapacity){
//修改计数器
modCount++;
//上次(原始)定义的数组长度
int oldCapacity = elementData.length;
//当前需要的长度超过了数组长度
if(minCapacity > oldCapacity){
Object oldData[] = elementData;
//计算新数组长度
int newCapacity = (OldCapacity * 3) / 2 + 1;
if(newCapacity < minCapacity)
newCapacity = minCapacity;
//数组拷贝,生成新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
elementData的默认长度是10,ArrayList的无参构造:
public ArrayList(){
//默认是长度为10的数组
this(10);
}
//指定数组长度的有参构造
public ArrayList(int initialCapacity){
super();
if(initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity:"+initialCapacity);
//声明指定长度的数组,容纳element
this.elementData = new Object[initialCapacity];
}
Vector的处理方式与ArrayList像素,只是数组的长度计算方式不同而已
private void ensureCapacityHelper(int minCapacity){
int oldCapacity = elementData.length;
if(minCapacity > oldCapacity){
Object[] oldData = elementData;
//若有递增步长,则按照步长增长;否则,扩容两倍
int newCapacity = (capacityIncrement > 0) ? (olcCapacity + capacityIncrement) : (oldCapacity * 2);
//越界检查,否则超过int最大值
if(newCapacity < minCapacity)
newCapacity = minCapacity;
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
64 多种最值算法,适时选择
//(1)自行实现,快速查找最大值 速度最快的算法
public static int max(int[] data){
int max = data[0];
for (int i : data) {
max = max > i ?max : i;
}
return max;
}
//(2)先排序,后取值
public static int max(int[] data){
//先排序
Arrays.sort(data.clone());//数组也是一个对象,不拷贝就改变了原有数组元素的顺序
//然后取值
return data[data.length - 1];
}
//(3)先剔除重复数据,然后再排序
public static int getSecond(Integer[] data){
//转换为列表
List dataList = Arrays.asList(data);
//转换为TreeSet,删除重复元素并升序排列
TreeSet ts = new TreeSet(dataList);
//取出比最大值小的最大值,第二大
return ts.lower(ts.last());
}
注意:最值计算时使用集合最简单,使用数组性能最优
65 避开基本类型数组转换列表陷阱
Arrays.asList()的方法说明:输入一个变长参数,返回一个固定长度的列表
public static List asList(T... a){
return new ArrayList(a);
}
asList方法输入的是一个泛型变长参数,基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型。
注意:原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱。
66 asList方法产生的List对象不可更改
因为asList方法直接new了一个ArrayList对象返回,此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内置类,其构造函数如下:
//这是一个静态私有内部类
private static class ArrayList extends AbstractList implements RandomAccess,java.io.Serializable{
//存储列表元素的数组
private final E[] a;
//唯一的构造函数
ArrayList(E[] array){
if(null == array)
throw new NullPointerException();
a = array;
}
@Override
public E get(int index) {
return null;
}
@Override
public int size() {
return 0;
}
}
这里的ArrayList是一个静态私有内部类,除了Arrays能访问外,其他类都不能访问。这个类没有提供add方法,父类AbstractList提供了(但没有提供具体的实现):
public boolean add(E e){
throw new UnsupportedOperationException();
}
ArrayList静态内部类,仅仅实现了5个方法:
size:元素数量
toArray:转化为数组,实现了数组的浅拷贝
get:获取指定元素
set:重置某一个元素值
contains:是否包含某元素
对于我们经常使用的List.add和List.remove方法它都没有实现,也就是说asList返回的是一个长度不可变的列表,数组是多长,转换成的列表也就是多长,换句话说此处的列表只是数组的一个外壳,不再保持列表动态变长的特性。
通过如下方式定义和初始化列表是不可取的:
List names = Arrays.adList("张三","李四","王五");
因为列表的长度无法修改。
67 不同的列表选择不同的遍历方法
ArrayList数组实现了RandomAccess接口(随机存取接口),标志着ArrayList是一个可以随机存取的列表。在Java中,RandomAccess和Cloneable、Serializable一样,都是标志性接口,不需要任何实现,只是用来表明其实现类具有某种特质的,实现了Cloneable表明可以被拷贝,实现了Serializable接口表明被序列化了,实现了RandomAccess则表明这个了可以随机存取;ArrayList数据元素之间没有关联,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存储;因此ArrayList采用下标方式遍历列表速度会更快。
LinkedList采用下标方式(get方法访问元素)遍历元素源码:
public E get(int index{
return entry(index).element;
}
private Entry entry(int index){
/* 检查下标是否越界 */
Entry e = header;
if(index < (size >> 1)){
//如果下标小于中间值。则从头节点开始搜索
for (int i = 0; i <= index; i++)
e = e.next;
}else{
//如果下标大于等于中间值,则从尾节点反向遍历
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
重构后的average方法代码如下:
public static int average(List list){
int sum = 0;
//可以随机存取,则使用下标遍历
if(list instanceof RandomAccess){
for(int i = 0,size = list.size();i < size;i++)
sum += list.get(i);
}else{
//有序存取,使用foreach方式
for (int i : list) {
sum += i;
}
}
//除以人数,计算平均值
return sum / list.size();
}
68 频繁插入和删除时使用LinkedList
(1)插入元素
public void add(int index,E element){
/* 检查下标是否越界,代码不再拷贝 */
//若需要扩容,则增大底层数组的长度
ensureCapacity(size + 1);
//给index下标之后的元素(包括当前元素)的下标加1,空出index位置
System.arraycopy(elementData, index, elementData, index + 1, size - index);
//赋值index位置原色
elementData[index] = element;
//
列表
长度+1
size++;
}
arraycopy方法只要是插入一个元素,其后的元素就会向后移动一位,频繁的插入,每次后面的元素都要拷贝一遍,效率就会变低,特别是在头位置插入元素时;可使用LinkedList类,LinkedList是一个双向链表,它的插入只是修改相邻元素的next和previous引用,其插入算法如下:
public void add(int index,E element){
addBefore(element,(index == size ? header : entry(index)));
}
private Entry addBefore(E e,Entry entry){
//组装一个新节点,previous指向原节点的前节点,next指向原节点
Entry newEntry = new Entry(e,entry,entry.previous);
//前节点的next指向自己
newEntry.previous.next = newEntry;
//后节点的previous指向自己
newEntry.next.previous = newEntry;
//长度+1
size++;
//修改计数器+1
modCount++;
return newEntry;
}
(2)删除元素
ArrayList提供了删除指定位置上额元素、删除指定值元素、删除一个下表范围内的元素集等删除动作,三者的实现原理基本相似,都是找到索引位置,然后删除,以remove方法为例,源码如下:
public E remove(int index){
//下标校验
RangeCheck(index);
//修改计数器+1
modCount++;
//记录要删除的元素值
E oldValue = (E) elementData(index);
//有多少个元素向前移动
int numMoved = size - index - 1;
if(numMoved > 0)
//index后的元素向前移动一位
System.arraycopy(elementData, index + 1, elementData, index, numMoved);
//列表长度减1,并且最后一位设为null
elementData[--size] = null;
//返回删除的值
return oldValue;
}
index位置后的 元素都向前移动了一位,最后一位空出来了,这又是一次数组拷贝,和插入一样,ArrayList其他的两个删除方法与此相似。
LinkedList提供了非常多的删除操作,比如删除指定位置元素、删除头元素等,与之相关的poll方法也会执行删除动作,删除指定位置元素的方法remove,源代码如下:
private E remove(Entry e){
//取得原始值
E result = e.element;
//前节点next指向当前节点的next
e.previous.next = e.next;
//后节点的previous指向当前节点的previous
e.next.previous = e.previous;
//置空当前节点的next和previous
e.next = e.previous = null;
//当前元素置空
e.element = null;
//列表长度减1
size--;
//修改计数器+1
modCount++;
return result;
}
这也是双向链表的标准删除算法,没有任何耗时的操作,全部是引用指针的变更,效率高。
(3)修改元素
修改元素值这一点LinkedList输给了ArrayList,这是因为LinkedList是顺序存取的,因此定位元素必然是一个遍历过程,效率大打折扣,set方法的源码如下:
public E set(int index,E element){
//定位节点
Entry e = entry(index);
E oldVal = e.element;
//节点的元素替换
e.element = element;
return oldVal;
}
这里使用了entry方法定位元素,LinkedList这种顺序存取列表的元素定位方式会折半遍历,这是一个极耗时的操作;而ArrayList的修改动作则是数组元素的直接替换,简单高效。
69 列表相等只需要关心元素数据
public static void main(String[] args) {
ArrayList strs = new ArrayList();
strs.add("A");
Vector strs2 = new Vector();
strs2.add("A");
System.out.println(strs.equals(strs2));
}
两个类不同,结果相同:两者都是列表,都实现了List接口,也都继承了AbstractList抽象类,其equals方法是在AbstractList中定义的,源码如下:
public boolean equals(Object o){
if(this == o)
return true;
//是否是List列表,注意这里:只要实现List接口即可
if(!(o instanceof List))
return false;
//通过迭代器访问list的所有元素
ListIterator e1 = (ListIterator) ((List) this).listIterator();
ListIterator e2 = (ListIterator) ((List) o).listIterator();
//遍历两个list元素
while(e1.hasNext() && e2.hasNext()){
E o1 = null;
try {
o1 = e1.next();
} catch (SAXException e) {
e.printStackTrace();
} catch (JAXBException e) {
e.printStackTrace();
}
Object o2 = null;
try {
o2 = e2.next();
} catch (SAXException e) {
e.printStackTrace();
} catch (JAXBException e) {
e.printStackTrace();
}
//只要存在着不相等就退出
if(!(null == o1 ? null == o2 : o1.equals(o2)))
return false;
}
//长度是否也相等
return !(e1.hasNext() || e2.hasNext());
}
只要所有的元素相等,并且长度也相等就表明两个List是相等的,与具体的容量类型无关。
其他的集合类型,如Set、Map等与此相同,也是只关心集合元素,不用考虑集合类型。
70 子列表只是原列表的一个视图
List接口提供了subList方法,其作用是返回一个列表的子列表,这与String类的subString有点类似,
public static void main(String[] args) {
//定义一个包含两个字符串的列表
List c= new ArrayList();
c.add("A");
c.add("B");
//构造一个包含c列表的字符串列表
List c1 = new ArrayList(c);
//subList生成与c相同的列表
List c2 = c.subList(0, c.size());
//c2增加一个元素
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
String类的subString方法
public static void main(String[] args) {
String str = "AB";
String str1 = new String(str);
String str2 = str.substring(0) + "c";
System.out.println("str == str1 ? " + str1.equals(str1));
System.out.println("str == str2 ? " + str1.equals(str2));
}
str与str1是相等的(虽然不是同一个对象,但用equals方法判断是相等的),但它们与str2不相等,因为str2在对象池中重新生成了一个新的对象,其表面值是ABC,那当然与str和str1不相等。
str == str1 ? true
str == str2 ? false
subList源码如下:
public List 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类的代码如下:
class SubList extends AbstractList {
//原始列表
private AbstractList l;
//偏移量
private int offset;
//构造函数,注意list参数就是我们的原始列表
SubList(AbstractList list, int fromIndex, int toIndex){
/* 下标校验,略 */
//传递原始列表
l = list;
offset = fromIndex;
//子列表的长度
size = toIndex - fromIndex;
}
//获得指定位置的元素
public E get(int index){
/* 校验部分,略 */
//从原始字符串中获得指定位置的元素
return l.get(index + offset);
}
//增加或插入
public void add(int index,E element){
/* 校验部分,略 */
//直接增加到原始字符串上
l.add(index + offset, element);
/* 处理长度和修改计数器 */
}
@Override
public int size() {
return 0;
}
}
subList方法的实现原理:它返回的SubList类也是AbstractList的子类,其所有的方法如get、set、add、remove等都是在原始列表上的操作它自身并没有生成一个数组或是链表,也就是子列表只是原列表的一个视图(View),所有的修改动作都反映在了原列表上。
c与c1不相等:因为通过ArrayList构造函数创建的List对象c1实际上是新列表,它是通过数组的copyOf动作生成的,所生成的列表c1与原列表c之间没有任何关系(虽然是浅拷贝,但元素类型是String,也就是说元素是深拷贝的)。
71 推荐使用subList处理局部列表
public static void main(String[] args) {
//初始化一个固定长度,不可变列表
List initData = Collections.nCopies(100, 0);
//转换为可变列表
List list = new ArrayList(initData);
//遍历,删除符合条件的元素
for (int i = 0, size = list.size(); i < size; i++) {
if(i >= 20 && i < 30)
list.remove(i);
}
for (int i = 20; i < 30; i++) {
if(i < list.size())
list.remove(i);
}
}
使用subList方法实现:
public static void main(String[] args) {
//初始化一个固定长度,不可变列表
List initData = Collections.nCopies(100, 0);
//转换为可变列表
ArrayList list = new ArrayList(initData);
//删除指定范围的元素
list.subList(20, 30).clear();
}
用subList先取出一个子列表,然后清空;因为subList返回的List是原始列表的一个视图,删除这个视图中的所有元素,最终就会反映到原始字符串上。
72 生成子列表后不要再操作原列表
public static void main(String[] args) {
List list = new ArrayList();
list.add("A");
list.add("B");
list.add("C");
List subList = list.subList(0, 2);
//原字符串增加一个元素
list.add("D");
System.out.println("原列表长度:" + list.size());
System.out.println("子列表长度:" + subList.size());
}
结果为:
原列表长度:4
Exception in thread "main" java.util.ConcurrentModificationException
出现这个问题的最终原因还是在子列表提供的size方法的检查上(修改计数器),size的源代码:
public int size(){
checkForComodification();
return size;
}
private void checkForComodification(){
//判断当前修改计数器是否与子列表生成时一致
if(l.modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount是在SubList子列表的构造函数中赋值的,其值等于生成子列表时的修改次数(modCount变量)。因此在生成子列表后在修改原始列表,l.modCount的值就必然比expectedModCount大1,不再保持相等了,于是就抛出了异常。
对于子列表操作,因为视图是动态生成的,生成子列表后再操作原列表,必然会导致“视图”的不稳定,最有效的办法就是通过Collections.unmodifiableList方法设置列表为只读状态,代码如下:
public static void main(String[] args) {
List list = new ArrayList();
List subList = list.subList(0, 2);
//设置列表为只读状态
list = Collections.unmodifiableList(list);
//对list进行只读操作
doReadSomething(list);
//对subList进行读写操作
doReadAndWriteSomething(subList);
}
注意:subList生成子列表后,保持原列表的只读状态。
73 使用Comparator进行排序
在Java中,要想给数据排序,有两种实现方式,一种是实现Comparable接口,一种是Comparator接口。
public class SubListAndCollectionsDemo {
public static void main(String[] args) {
List list = new ArrayList(5);
//一个老板
list.add(new Employee(1001,"张三",Position.Boss));
//两个经理
list.add(new Employee(1003,"王五",Position.Manager));
list.add(new Employee(1006,"刘柳",Position.Manager));
//两个职员
list.add(new Employee(1002,"李四",Position.Staff));
list.add(new Employee(1004,"赵七",Position.Staff));
//按照id排序,也就是按照资历深浅排序
Collections.sort(list);
//按照职位降序排序
Collections.sort(list, new PositionComparator());
for (Employee employee : list) {
System.out.println(employee);
}
}
}
class Employee implements Comparable {
private int id;
private String name;
private Position position;
public Employee(int _id,String _name,Position _position) {
id =_id;
name = _name;
position = _position;
}
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;
}
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
@Override
public int compareTo(Employee o) {
return new CompareToBuilder().append(id,o.id).toComparison();
}
@Override
public String toString(){
return ToStringBuilder.reflectionToString(this);
}
}
enum Position {
Boss,Manager,Staff
}
class PositionComparator implements Comparator {
@Override
public int compare(Employee o1, Employee o2) {
//按照职位降序排序
return o1.getPosition().compareTo(o2.getPosition());
}
}
若按职位临时倒序排列,有两个解决办法:
(1)直接使用Collections.reverse(List> list)方法实现倒序排列
(2)通过Collections.sort(list,Collections.reverseOrder(new PositionComparator))也可以实现倒序排列
先按照职位排序,职位相同再按照工号排序
public int compareTo(Employee o){
return new CompareToBuilder()
.append(position, o.position)//职位排序
.append(id, o.id).toComparison();//工号排序
}
实现了Comparable接口的类表明自身是可比较的,有了比较才能排序;而Comparator接口是一个工具类接口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑,从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法基本不会改变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法。
注意:Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具。
74 不推荐使用binarySearch对列表进行检索
对一个列表进行检索时,使用得最多的是indexOf方法,它简单、好用,而且也不会出错,虽然它只能检索到第一个符合条件的值,但是我们可以生成子列表后再检索,这样也就可以查找出所有符合条件的值了。
Collections工具类也提供了一个检索方法:binarySearch,但是在使用这个方法时有一些注意事项:
public static void main(String[] args) {
List cities = new ArrayList();
cities.add("南京");
cities.add("上海");
cities.add("上海");
cities.add("武汉");
cities.add("重庆");
//indexOf方法取得索引值
int index1 = cities.indexOf("上海");
//binarySearch查找到索引值
int index2 = Collections.binarySearch(cities, "上海");
System.out.println("索引值(indexOf):" + index1);
System.out.println("索引值(binarySearch):" + index2);
}
结果为1,2
binarySearch使用二分搜索法搜索指定列表,以获得指定对象:
public static int binarySearch(List extends Comparable super T>> list,T key){
if(list instanceof RandomAccess || list.size() < 5000)
//随机存取列表或者元素数量少于5000的顺序存取列表
return Collections.indexedBinarySearch(list, key);
else
//元素数量大于5000的顺序存取列表
return Collections.iteratorBinarySearch(list, key);
}
private static 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;
}
//没有找到,返回负值
return -(low + 1);
}
注意看红色字体部分,首先是获得中间索引值,本例中索引值是2.
indexOf的实现源码:
public int indexOf(Object o){
if(null == o){
//null元素查找
for (int i = 0; i < size; i++) {
if(null == elementData[i])
return i;
}
}else{
//非null元素查找
for (int i = 0; i < size; i++) {
//两个元素是否相等,这一这里是equals方法
if(o.equals(elementData[i]))
return i;
}
}
//没找到,返回-1
return -1;
}
indexOf方法就是一个遍历,找到第一个元素值相等则返回。
二分查找的一个首要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。使用Collections.sort排下序即可。
binarySearch的二分法查找比indexOf的遍历算法性能上高很多特别是在大数据集而且目标值又接近尾部时,因此在从性能的角度考虑时可以选择binarySearch。
75 集合中的元素必须做到compareTo和equals同步
实现了Comparable接口的元素就可以排序,compareTo方法是Comparable接口要求必须实现的,它与equals方法有关,在compareTo的返回为0时,它表示的是进行比较的两个元素是相等的。
class City implements Comparable{
//城市编码
private String code;
//城市名称
private String name;
public City(String _code,String _name){
code = _code;
name = _name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(City o) {
//按照城市名称排序
return new CompareToBuilder().append(name,o.name).toComparison();
}
@Override
public boolean equals(Object obj){
if(null == obj)
return false;
if(this == null)
return true;
if(obj.getClass() != getClass())
return false;
City city = (City)obj;
//根据code判断是否相等
return new EqualsBuilder().append(code,city.code).isEquals();
}
}
public static void main(String[] args) {
List cities = new ArrayList();
cities.add(new City("021","上海"));
cities.add(new City("021","沪"));
//排序
Collections.sort(cities);
//查找对象
City city = new City("021","沪");
//indexOf方法取得索引值
int index1 = cities.indexOf(city);
//binarySearch查找索引值
int index2 = Collections.binarySearch(cities, city);
System.out.println("索引值(indexOf:)" + index1);
System.out.println("索引值(binarySearch:)" + index2);
}
结果为:
索引值(indexOf):0
索引值(bianrySearch):1
这是因为indexOf是通过equals方法判断的,equals等于true就认为找到符合条件的元素了,而binarySearch查找的依据是compareTo方法的返回值,返回0即认为找到符合条件的元素。使用indexOf方法查找时,遍历每个元素,然后比较equals方法的返回值,因为equals方法是根据code判断的,;而使用binarySearch二分法查找时,依据的是每个元素的compareTo方法返回值,而compareTo方法又是依赖name属性的,name相等就返回0,binarySearch就认为找到元素了。修改也很容易,将equals方法修改成判断name是否相等即可。
总结两点:
(1)indexOf依赖equals方法查找,binarySearch则依赖compareTo方法查找;
(2)equals是判断元素是否相等,compareTo是判断元素在排序中的位置是否相同
注意:实现了compareTo方法,就应该覆写equals方法,确保两者同步。
76 集合运算时使用更优雅的方式
(1)并集
也叫作合集,把两个集合加起来即可:listA.addAll(listB);
(2)交集
计算两个集合的共有元素:listA.retainAll(listB);
(3)差集
由所有属于A但不属于B的元素组成的集合,叫做A与B的差集:listA.removeAll(listB);
(4)无重复的并集
//删除在listA中出现的元素
listB.removeAll(listA);
//把剩余的listB元素加到listA中
listA.addAll(listB);
77 使用shuffle打乱列表
public static void main(String[] args) {
int tagCloudNum = 10;
List tagClouds = new ArrayList(tagCloudNum);
//初始化标签云,一般是从数据库读入
Random rand = new Random();
for (int i = 0; i < tagCloudNum; i++) {
//取得随机位置
int randomPosition = rand.nextInt(tagCloudNum);
//当前元素与随机元素交换
String temp = tagClouds.get(i);
tagClouds.set(i, tagClouds.get(randomPosition));
tagClouds.set(randomPosition, temp);
}
}
更好的实现方式:
public static void main(String[] args) {
int tagCloudNum = 10;
List tagClouds = new ArrayList(tagCloudNum);
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;
List tagClouds = new ArrayList(tagCloudNum);
//打乱顺序
Collections.shuffle(tagClouds);
}
shuffle可以用在以下地方:
(1)可以用在程序的“伪装”上
比如上例中的标签云,或者是游戏中的打怪、修行、群殴时宝物的分配策略。
(2)可以用在抽奖程序中
(3)可以用在安全传输方面
比如发送端发送一组数据,先随机打乱顺序,然后加密发送,接收端解密,然后自行排序,即可实现即使是相同的数据源,也会产生不同密文的效果,加强了数据的安全性。
78 减少HashMap中元素的数量
在系统开发中,我们经常会使用HashMap作为数据集容器,或者是用缓冲池来处理,一般很稳定,但偶尔也会出现内存溢出的问题,而且这经常是与HashMap有关的,比如我们使用缓冲池操作数据时,大批量的增删改查操作就可能会让内存溢出。
public static void main(String[] args) {
Map map = new HashMap();
final Runtime rt = Runtime.getRuntime();
//JVM终止前记录内存信息
rt.addShutdownHook(new Thread(){
@Override
public void run(){
StringBuffer sb = new StringBuffer();
long heapMaxSize = rt.maxMemory() >> 20;
sb.append("最大可用内存:" + heapMaxSize + "M\n");
long total = rt.totalMemory() >> 20;
sb.append("堆内存大小:" + total + "M\n");
long free = rt.freeMemory() >> 20;
sb.append("空闲内存:" + free + "M");
System.out.println(sb);
}
});
//放入键值对
for (int i = 0; i < 39321700; i++)
map.put("key" + i, "value" + i);
}
输出结果为:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.HashMap.resize(HashMap.java:462)
at java.util.HashMap.addEntry(HashMap.java:755)
at java.util.HashMap.put(HashMap.java:385)
at qz.test.equals.HashMapDemo.main(HashMapDemo.java:24)
最大可用内存:247M
堆内存大小:247M
空闲内存:8M
换成ArrayList存储:
public static void main(String[] args) {
List list = new ArrayList();
final Runtime rt = Runtime.getRuntime();
//JVM终止前记录内存信息
rt.addShutdownHook(new Thread(){
@Override
public void run(){
StringBuffer sb = new StringBuffer();
long heapMaxSize = rt.maxMemory() >> 20;
sb.append("最大可用内存:" + heapMaxSize + "M\n");
long total = rt.totalMemory() >> 20;
sb.append("堆内存大小:" + total + "M\n");
long free = rt.freeMemory() >> 20;
sb.append("空闲内存:" + free + "M");
System.out.println(sb);
}
});
for(int i = 0;i < 400000;i++){
list.add("key" + i);
list.add("value" + i);
}
}
同样的情况下,ArrayList没有溢出,但HashMap却溢出了。
HashMap在底层也是以数组方式保存元素的,其中每一个键值对就是一个元素,也就是说HashMap把键值对封装成了一个Entry对象,然后再把Entry放到了数组中。
static class Entry implements Map.Entry{
//键
final K key;
//值
V value;
//相同哈希码的下一个元素
Entry next;
final int hash;
@Override
public K getKey() {
return null;
}
@Override
public V getValue() {
return null;
}
@Override
public V setValue(V value) {
return null;
}
}
HashMap底层的数组变量名叫table,它是Entry类型的数组,保存的是一个一个的键值对。HashMap的长度是可以动态增加的,它的扩容机制与ArrayList稍有不同:在插入键值对时,会做长度校验,如果大于或等于阈值,则数组长度增大一倍;默认的阈值是当前长度与加载因子的乘积,默认的加载因子是0.75,也就是说只要HashMap的size大于数组长度的0.75倍时,就开始扩容。
内存溢出的根本原因就是因为HashMap在扩容时出现了内存溢出错误;ArrayList也会出现这种情况。
综合来说,HashMap比ArrayList多了一个层Entry的底层对象封装,多占用了内存,并且它的扩容策略是2倍长度的递增,同时还会依据阈值判断规则进行判断,因此相对于ArrayList来说,它就会先出现内存溢出。
79 集合中的哈希码不要重复
在列表中查找某值是非常耗费资源的,随机存取的列表是遍历查找,顺序存储列表是链表查找,Collenctions的二分法查找,都不够快,最快的要数以Hash开头的集合(HashMap/HashSet等)查找。
HashMap的containsKey方法代码如下:
public boolean containsKey(Object key){
//判断getEntry是否为空
return getEntry(key) != null;
}
final Entry getEntry(Object key) {
//计算key的哈希码
int hash = (null == key) ? 0 : hash(key.hashCode());
//定位Entry,indexFor方法是根据hash定位数组的位置的
for (
Entry e = table(indexFor(hash,table.length));e != null;e = e.next) {
Object k;
//哈希码相同,并且键也相等才符合条件
if(e.hash == hash && ((k = key) || (null != key && key.equals(k))))
return e;
}
return null;
}
通过indexFor方法定位Entry在数组table中的位置,这是HashMap实现的一个关键点,怎么能根据hashCode定位它在数组中的位置,需要从HashMap的table数组是如何存储元素的说起,首先要说明以下三点:
(1)table数组的长度永远是2的N次幂;
(2)table数组中的元素是Entry类型;
(3)table数组中的元素位置是不连续的。
HashMap每次增加元素时都会先计算其哈希码,然后使用hash方法再次对hashCode进行抽取和统计,同时兼顾哈希码的高位和低位信息产生一个唯一值,也就是说hashCode不同,hash方法返回的值也不同,之后再通过indexFor方法与数组长度做一次与运算,即可计算出其在数组中的位置,简单地说,hash方法和indexFor方法就是把哈希码转变成数组的下标,源代码如下:
static int hash(int h){
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h,int length){
return h & (length - 1);
}
哈希运算存在着哈希冲突问题,即对于一个固定的哈希算法f(k),允许出现f(k1)=f(k2),但
k1≠k2的情况,也就是说两个不同的Entry,可能产生相同的哈希码,HashMap是通过链表处理这种冲突问题的,每个键值对都是一个Entry,其中每个Entry都有一个next变量,也就是说它会指向下一个键值对——这是一个单向链表,该链表是由addEntry方法完成的,其代码如下:
void addEntry(int hash,K key,V value,int bucketIndex){
//取得当前位置元素
Entry e = table[bucketIndex];
//生成新的键值对,并进行替换,建立链表
table[bucketIndex] = new Entry(hash,key,value,e);
//判断是否需要扩容
if(size++ >= threshold)
resize(2 * table.length);
}
这段程序涵盖了两个业务逻辑:如果新加入的键值对的hashCode是唯一的,那直接插入的数组中,它Entry的next值则为null;如果新加入的键值对的hashCode与其他元素冲突,则替换掉数组中的当前值,并把新加入的Entry的next变量指向被替换掉的元素。
HashMap的存储主线还是数组,遇到哈希冲突的时候则使用链表解决。
如果哈希码相同,它的查找效率就与ArrayList没什么两样了,遍历对比,性能大打折扣。
注意 HashMap中的hashCode应避免冲突。
80 多线程使用Vector或HashTable
Vector是ArrayList的多线程版本,HashTable是HashMap的多线程版本。
public static void main(String[] args) {
//火车票列表
final List tickets = new ArrayList();
//初始化票据池
for (int i = 0; i < 100000; i++) {
tickets.add("火车票" + i);
}
//退票
Thread returnTktThread = new Thread(){
public void run(){
while(true){
tickets.add("火车票" + new Random().nextInt());
}
}
};
//售票
Thread saleTktThread = new Thread(){
public void run(){
for (String ticket : tickets) {
tickets.remove(ticket);
}
}
};
//启动退票线程
returnTktThread.start();
//启动售票线程
saleTktThread.start();
}
for (int i = 0; i < 10; i++) {
new Thread(){
public void run(){
while(true){
System.out.println(Thread.currentThread().getId()+"--"+tickets.remove(0));
}
}
}.start();
}
多线程环境下考虑使用Vector或HashTable。
81 非稳定排序推荐使用List
Set与List的最大区别就是Set中的元素不可以重复(这个重复指的是equals方法的返回值相等);TreeSet该类实现了类默认排序为升序的Set集合,如果插入一个元素,默认会按照升序排列(根据Comparable接口的compareTo的返回值确定排序)
SortedSet接口只是定义了在给集合加入元素时将其进行排序,并不能保证元素修改后的排序结果,因此TreeSet适用于不变量的集合数据排序,比如String、Integer等类型,但不适用于可变量的排序,特别是不确定何时元素会发生变化的数据集合。
new TreeSet(new ArrayList(set));
使用List,Collections.sort()方法对List排序。
List不能保证集合中的元素唯一,它是可以重复的,而Set能保证元素唯一,不重复。List自行解决重复问题(转变为HashSet,剔除后再转回来)。
SortedSet中的元素被修改后可能会影响其排序位置。
82 集合大家族
常用:ArrayList、HashMap
不常用:Stack、Queue
线程安全:Vector、HashTable
线程不安全:LinkedList、TreeMap
阻塞式:ArrayBlockingQueue
非阻塞式:PriorityQueue
(1)List
实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack,其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则。
(2)Set
不包含重复元素的集合,主要的实现类有:EnumSet、HashSet、TreeSet,EnumSet是枚举类型的专用Set,所有元素都是枚举类型;HashSet是以哈希码决定其元素位置的Set,其原理与HashMap相似,它提供快速的插入和查找方法;TreeSet是一个自动排序的Set,它实现了SortedSet接口。
(3)Map
分为排序Map和非排序Map,排序Map主要是TreeMap类,它根据key值进行自动排序;非排序Map主要包括:HashMap、HashTable、Properties、EnumMap等,其中Properties是HashTable的子类,它的主要用途是从Property文件中加载数据,并提供方便的读写操作;EnumMap则是要求其key必须是某一个枚举类型。WeakHashMap是一个采用弱键方式实现的Map类,特点是:WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,使用WeakHashMap装载数据不用担心内存溢出的问题,GC会自动删除不用的键值对,但存在一个严重的问题:GC是静悄悄回收的,我们的程序无法知晓该动作,存在着重大的隐患。
(4)Queue
它分为两类,一类是阻塞式队列,队列满了以后再插入后抛出异常,主要包括:ArrayBlockingQueue、PriorityBlockingQueue、LinkedBlockingQueue,其中ArrayBlockingQueue是一个以数组方式实现的有界阻塞队列,PriorityBlockingQueue是依照优先级组建的队列,LinkedBlockingQueue是通过链表实现的阻塞队列:另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加元素,最常用的是PriorityQueue类。
还有一种队列,是双端队列,支持在头、尾两端插入和移除元素,它的主要实现类是:ArrayDeque、LinkedBlockingDeque、LinkedList。
(5)数组
数组与集合的最大区别就是数组能够容纳基本类型,而集合不行,更重要的一点就是所有的集合底层存储的都是数组。
(6)工具类
数组的工具类是java.util.Arrays和java.lang.reflect.Array,集合的工具类是java.util.Collections
(7)扩展类
Apache的commons-collections扩展包,Google的google-collections扩展包。