改善java程序之数组和集合1

来源:互联网 发布:淘宝上便宜衣服能买吗 编辑:程序博客网 时间:2024/05/16 08:05
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<Integer> datas){
        int sum = 0;
        for (int i = 0; i < datas.size(); i++) {
            sum += datas.get(i);
        }
        return sum;
    }
}
基本类型是在栈内存中操作的,而对象则是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大。

61 若有必要,使用变长数组
    public static <T> 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<Integer> dataList = Arrays.asList(data);
        //转换为TreeSet,删除重复元素并升序排列
        TreeSet<Integer> ts = new TreeSet<Integer>(dataList);
        //取出比最大值小的最大值,第二大
        return ts.lower(ts.last());
    }
注意:最值计算时使用集合最简单,使用数组性能最优

65 避开基本类型数组转换列表陷阱
Arrays.asList()的方法说明:输入一个变长参数,返回一个固定长度的列表
    public static <T> List<T> asList(T... a){
        return new ArrayList<T>(a);
    }
asList方法输入的是一个泛型变长参数,基本类型是不能泛型化的,也就是说8个基本类型不能作为泛型参数,要想作为泛型参数就必须使用其所对应的包装类型。
注意:原始类型数组不能作为asList的输入参数,否则会引起程序逻辑混乱。

66 asList方法产生的List对象不可更改
因为asList方法直接new了一个ArrayList对象返回,此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内置类,其构造函数如下:
    //这是一个静态私有内部类
    private static class ArrayList<E> extends AbstractList<E> 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<String> 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<E> entry(int index){
        /* 检查下标是否越界 */
        Entry<E> 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<Integer> 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<E> addBefore(E e,Entry<E> entry){
        //组装一个新节点,previous指向原节点的前节点,next指向原节点
        Entry<E> newEntry = new Entry<E>(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){
        //取得原始值
        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> e = entry(index);
        E oldVal = e.element;
        //节点的元素替换
        e.element = element;
        return oldVal;
    }
这里使用了entry方法定位元素,LinkedList这种顺序存取列表的元素定位方式会折半遍历,这是一个极耗时的操作;而ArrayList的修改动作则是数组元素的直接替换,简单高效。

69 列表相等只需要关心元素数据
    public static void main(String[] args) {
        ArrayList<String> strs = new ArrayList<String>();
        strs.add("A");
        Vector<String> strs2 = new Vector<String>();
        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<E> e1 = (ListIterator<E>) ((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<String> c= new ArrayList<String>();
        c.add("A");
        c.add("B");
        //构造一个包含c列表的字符串列表
        List<String> c1 = new ArrayList<String>(c);
        //subList生成与c相同的列表
        List<String> 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<E> subList(int fromIndex, int toIndex) {
        return (this instanceof RandomAccess ?
                new RandomAccessSubList<E>(this, fromIndex, toIndex) : 
                    new SubList<E>(this, fromIndex, toIndex));
    }
subList方法是由AbstractList实现的,它会根据是不是可以随机存取来提供不同的SubList实现方式,RandomAccessSubList也是SubList子类,所以所有的操作都是由SubList类实现的(除了自身的SubList方法外),SubList类的代码如下:
class SubList<E> extends AbstractList<E> {
    //原始列表
    private AbstractList<E> l;
    //偏移量
    private int offset;
    //构造函数,注意list参数就是我们的原始列表
    SubList(AbstractList<E> 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<Integer> initData = Collections.nCopies(100, 0);
        //转换为可变列表
        List<Integer> list = new ArrayList<Integer>(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<Integer> initData = Collections.nCopies(100, 0);
        //转换为可变列表
        ArrayList<Integer> list = new ArrayList<Integer>(initData);
        //删除指定范围的元素
        list.subList(20, 30).clear();
    }
用subList先取出一个子列表,然后清空;因为subList返回的List是原始列表的一个视图,删除这个视图中的所有元素,最终就会反映到原始字符串上。

72 生成子列表后不要再操作原列表
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        list.add("A");
        list.add("B");
        list.add("C");
        List<String> 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<String> list = new ArrayList<String>();
        List<String> subList = list.subList(0, 2);
        //设置列表为只读状态
        list = Collections.unmodifiableList(list);
        //对list进行只读操作
        doReadSomething(list);
        //对subList进行读写操作
        doReadAndWriteSomething(subList);
    }
注意:subList生成子列表后,保持原列表的只读状态。
0 0