当前位置: 首页>后端>正文

数组和集合——读《编写高质量代码:改善Java程序的151个建议》(五)

读书,收获,分享
建议后面的五角星仅代表笔者个人需要注意的程度。
Talk is cheap.Show me the code

建议60:性能考虑,数组是首选★★★☆☆

在Java中由于数组没有List、Set、Map这些集合类用起来方便,于是数组在实际的系统开发中用得越来越少了,我们通常只有在阅读一些开源项目时才会看到它们的身影。但是在基本类型处理方面,数组还是占优势的,而且集合类的底层也都是通过数组实现的。

如例:

    //对数组求和
    public static int sum1(int[] arr) {
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        return sum;
    }
    //对列表求和
    public static int sum2(List<Integer> list) {
        int sum = 0;
        for (int i = 0; i < list.size(); i++) {
            //此处做了自动拆箱操作
            sum += list.get(i);
        }
        return sum;
    }
//所以,效率:sum1() > sum2();
//在实际测试中发现:对基本类型进行求和计算时,数组的效率是集合的10倍。

基本类型是在栈内存中操作的,而对象则是在堆内存中操作的,栈内存的特点是速度快,容量小,堆内存的特点是速度慢,容量大(从性能上来讲,基本类型的处理占优势)。

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

建议61:若有必要,使用变长数组★★☆☆☆

Java中的数组是定长的,经过初始化声明就不可改变长度,这在实际使用中非常不方便。但是可以通过对数组扩容“婉转”地解决该问题,代码如下:

public static <T> T[] expandCapacity(T[] datas, int newLen) {
        newLen = newLen < 0 0 : newLen;
        //生成一个新数组,并拷贝原值
        return Arrays.copyOf(datas, newLen);
}

注意:在实际开发中,如果确实需要变长的数据集,数组也是在考虑范围之内的,不能因固定长度而将其否定。

建议62:警惕数组的浅拷贝★★★☆☆

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

注意:虽然很多时候浅拷贝可以解决业务问题,但更多时候会留下隐患,需要我们提防又提防

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

通过阅读ArrayListadd()方法来看,它是怎么自动管理长度的,代码如下:

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的长度就加1,
         * 而是在达到elementData长度的临界点时,才将elementData扩容1.5倍,
         * 这样实现有什么好处呢?好处是避免了多次调用copyOf方法的性能开销,
         * 否则每加一个元素都要扩容一次,那性能岂不是非常糟糕?!
         * 这里为什么是1.5倍,而不是2.5倍、3.5倍?
         * 原因是一次扩容太大(比如扩容2.5倍),占用的内存也就越大,浪费的内存也就越多
         * (1.5倍扩容,最多浪费33%的数组空间,而2.5倍则最多可能浪费60%的内存);
         * 而一次扩容太小(比如每次扩容1.1倍),则需要多次对数组重新分配内存,性能消耗严重。
         * 经过测试验证,扩容1.5倍即满足了性能要求,也减少了内存消耗。
         */

elementData的初始容量为10,代码如下:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     * 默认初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    // ***省略***
}   

ArrayList的扩容机制可以看出,如果不设置初始容量,系统就按照1.5倍的规则扩容,每次扩容都是一次数组的拷贝,如果数据量很大,这样的拷贝会非常耗费资源,而且效率非常低下。如果我们已经知道一个ArrayList的可能长度,然后对ArrayList设置一个初始容量则可以显著提高系统性能(在大数据量下,是否指定容量会使性能相差5倍以上)。

其他类型集合的扩容处理方式与ArrayList相似,只是数组的长度计算方式不同而已。

注意:非常有必要在集合初始化时声明容量。

建议64:多种最值算法,适时选择★★☆☆☆

对一批数据进行排序,然后找出其中的最大值或最小值,有多种实现方式,示例如下:

//(1)自行实现,快速查找最大值   
public static int max1(int[] data) {
        int max = data[0];
        for (int i : data) {
            max = max > i max : i;
        }
        return max;
}

//(2)先排序,后取值
public static int max2(int[] data) {
        //先排序,clone()是为了不改变数组原有顺序
        Arrays.sort(data.clone());
        return data[data.length - 1];
 }

//增加复杂度,查找仅次于最大值的元素的实现 
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());

 }

首先max1()的效率肯定是高于max2(),但在实际测试中我们发现,如果数组数量少于1万,两者基本上没有差别,在同一个毫秒级别里,此时就可以不用自己写算法了,直接使用数组先排序后取值的方式。

至于getSecond()的要求稍微复杂些,使用集合是最简单的方式,若从性能方面来考虑,数组是最好的选择。

注意:最值计算时使用集合最简单,使用数组性能最优,需适时选择。

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

示例如下:

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

    //asList源代码
    public static <T> List<T> asList(T... a) {
        return new Arrays.ArrayList<>(a);
    }
        /**
         * asList方法输入的是一个泛型变长参数,但基本类型是不能泛型化的
         * 于是在main()中把data(一个int类型的数组)作为了T的类型。
         * 所以,在上述例子中,用基本类型的包装类即可。
         * */

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

建议66:asList方法产生的List对象不可更改★☆☆☆☆

asList()源码:

public static <T> List<T> asList(T... a) {
    //此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内置类
    return new ArrayList<>(a);
}
//内置类
private static class ArrayList<E> extends AbstractList<E> implements 
    RandomAccess, java.io.Serializable {
        //存储列表元素的数组
        private final E[] a;
        //唯一的构造函数
        ArrayList(E[] array) {
           if (array == null)
            throw new NullPointerException();
            a = array;
        }
    //***省略余下源码***
         
        /**
         *  1. ArrayList是一个静态私有内部类,除了Arrays能访问外,其他类都不能访问
         *  2. List.add和List.remove方法它都没有实现, 于是数组是多长,转换成的列表也就是多长(不可变性)
         */  
}   

建议67:不同的列表选择不同的遍历方法★★☆☆☆

ArrayListLinkedList为例:

    //测试得出,使用最优的遍历方式,性能提升了60%以上
    public static int avg(List<Integer> list) {
        int sum = 0;
        if (list instanceof RandomAccess) {//ArrayList数组实现了RandomAccess接口(随机存取接口)
            //可以随机存取,则使用下标遍历
            for (int i = 0; i < list.size(); i++) {
                sum += list.get(i);
            }
        } else {
            //有序存取,使用foreach方式
            for (int i : list) {
                sum += i;
            }
        }
        return sum / list.size();
    }

RandomAccess接口(随机存取接口) : 实现了RandomAccess则表明这个类可以随机存取,对ArrayList来说也就标志着其数据元素之间没有关联,即两个位置相邻的元素之间没有相互依赖和索引关系,可以随机访问和存储。

Java中的foreach语法是iterator(迭代器)的变形用法,也就是说对于ArrayList,需要先创建一个迭代器容器,然后屏蔽内部遍历细节,对外提供hasNext、next等方法。问题是ArrayList实现了RandomAccess接口,已表明元素之间本来没有关系,可是,为了使用迭代器就需要强制建立一种互相“知晓”的关系,比如上一个元素可以判断是否有下一个元素,以及下一个元素是什么等关系,这也就是通过foreach遍历耗时的原因。

LinkedList类不是随机存取的,而是有序存取的,它实现了双向链表,每个数据结点中都有三个数据项:前节点的引用(Previous Node)、本节点元素(Node Element)、后继节点的引用(Next Node),也就是说在LinkedList中的两个元素本来就是有关联的,我知道你的存在,你也知道我的存在,使用foreach也就是迭代器方式效率更高。

注意:列表遍历不是那么简单的,其中很有“学问”,适时选择最优的遍历方式,不要固化为一种

建议68:频繁插入和删除时使用LinkedList★☆☆☆☆

下面以ArrayListLinkedList的元素插入算法为例:

ArrayList的插入(add方法)算法:

    public void add(int index, E element) {
        /*省略校验代码*/
        //若需要扩容,则增大底层数组的长度
        ensureCapactiy(size + 1);
        //给index下标之后的元素(包括当前的元素)的下标加1,空出index位置
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        //赋值index位置元素
        elementData[index] = element;
        //列表长度+1
        size++;
    }
        /**
         *  注意看arraycopy方法,只要是插入一个元素,其后的元素就会向后移动一位,
         *  虽然arraycopy是一个本地方法,效率非常高,但频繁的插入,每次后面的元素都要拷贝一遍,效率就变低了,
         *  特别是在头位置插入元素时。
         * */

LinkedList的插入(add方法)算法:

    public void add(int index, E element) {
        ///调用了私有addBefore方法
        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;
    }
        /**
         *  这是一个典型的双向链表插入算法,把自己插入到链表,
         *  然后再把前节点的next和后节点的previous指向自己。
         *  这样一个插入元素(也就是Entry对象)的过程中,没有任何元素会有拷贝过程,只是引用地址改变了,
         *  那效率当然就高了。
         * */

经过实际测试得知,LinkedList的插入效率比ArrayList快50倍以上

LinkedList删除和插入效率高,ArrayList修改元素效率高,具体源码,不再粘贴。

得出,如果有大量的写操作(更多的是插入和删除动作),推荐使用LinkedList,不过何为少量,何为大量呢?毕竟80%的性能是消耗在20%的代码上的,所以还是要适时选择,比如一个实时交易的系统,即使写作操再少,使用LinkedList也比ArrayList合适,因为此类系统是争分夺秒的,多N个毫秒可能就会造成交易数据不准确;而对于一个批量系统来说,几十毫秒、几百毫秒,甚至是几千毫秒的差别意义都不大,这时是使用LinkedList还是ArrayList就看个人爱好了,当然,如果系统已经处于性能临界点了那就必须使用LinkedList。

建议69:列表相等只需关心元素数据★★☆☆☆

示例代码:

    public static void main(String[] args) {
        ArrayList<String> strs = new ArrayList<>();
        strs.add("A");

        Vector<String> strs2 = new Vector<>();
        strs2.add("A");

        System.out.println(strs.equals(strs2));
        //运行结果:true
    }

    //AbstractList中equals的源代码:
    public boolean equals(Object o) {
        if (o == this)
            return true;
        //判断是否是列表,注意:只需要实现List接口
        if (!(o instanceof List))
            return false;
        //通过迭代器访问所有元素
        ListIterator<E> e1 = listIterator();
        ListIterator<?> e2 = ((List<?>) o).listIterator();
        //遍历两个list的元素
        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());
    }

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

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

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

建议70:子列表只是原列表的一个视图★★☆☆☆

ListsubList方法示例:

    public static void main(String[] args) {
        //定义一个列表list
        List<String> list = new ArrayList<>();
        list.add("A");
        list.add("B");
        //构造一个包含list的列表list1
        List<String> list1 = new ArrayList<>(list);
        //subList生成与list相同的列表list2
        List<String> list2 = list.subList(0, list.size());
        //list2增加一个元素
        list2.add("C");
        System.out.println(list.equals(list1));
        System.out.println(list.equals(list2));
        //运行结果:
        //false,true
    }

为什么subList生成的新列表,在添加了新元素后,还与原列表相等呢?

subList源码:

    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类的代码
class SubList<E> extends AbstractList<E> {
    //原始列表
    private final AbstractList<E> l;
    //偏移量
    private final int offset;
    private int size;
    //构造函数,list参数就是我们的原始列表  
    SubList(AbstractList<E> list, int fromIndex, int toIndex) {
      /***省略校验代码***/
        //传递原始列表
        l = list;
        offset = fromIndex;
        //子列表的长度
        size = toIndex - fromIndex;
        this.modCount = l.modCount;
    }
    //获取指定位置的元素
    public E get(int index) {
         /***省略校验代码***/
        //从原始字符串中获得指定位置的元素
        return l.get(index+offset);
    }
    //增加或插入
    public void add(int index, E element) {
       /***省略校验代码***/
        //直接增加到原始字符串上
        l.add(index+offset, element);
        this.modCount = l.modCount;
        size++;
    }
     /***其他代码省略***/
}

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

最后,为什么变量listlist1不相等

因为通过ArrayList构造函数创建的List对象list1实际上是新列表,它是通过数组的copyOf动作生成的,所生成的列表list1与原列表list之间没有任何关系(虽然是浅拷贝,但元素类型是String,也就是说元素是深拷贝的),虽然此时equals是相等的,但是随后list又增加了元素,因此equals比较肯定不相等了。

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

建议71:推荐使用subList处理局部列表★★☆☆☆

示例代码:

//需求:一个列表有10个元素,现在要删除索引位置为2~6的元素   
public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i);
        }
    //一行代码就解决问题(subList方法所有的操作都是在原始列表上进行的)
    list.subList(2, 6).clear();
   }

建议72:生成子列表后不要再操作原列表★★☆☆☆

前面说了,subList生成的子列表是原列表的一个视图,那在subList执行完后,如果修改了原列表的内容会怎样呢?

    public static void main(String[] args) {
        List<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长度:" + list.size());
        System.out.println("subList长度:" + subList.size());
    }

运行结果:subListsize方法异常 (并发修改异常)

list长度:4
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
    at java.util.ArrayList$SubList.size(ArrayList.java:1040)
    at com.jyswm.demo1.t151.Client.main(Client.java:19)
         /**
         * 因为subList取出的列表是原列表的一个视图,原数据集(代码中的list变量)修改了,
         * 但是subList取出的子列表不会重新生成一个新列表(这点与数据库视图是不相同的),
         * 后面在对子列表继续操作时,就会检测到修改计数器与预期的不相同,
         * 于是就抛出了并发修改异常。
         */

SubListsize方法的源码

private class SubList extends AbstractList<E> implements RandomAccess {
 
    public int size() {
            checkForComodification();
            return this.size;
    }
    //checkForComodification方法就是用于检测是否并发修改的
    private void checkForComodification() {
            if (ArrayList.this.modCount != this.modCount)
                throw new ConcurrentModificationException();
    }
}

解决办法:

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //子列表
        List<String> subList = list.subList(0, 2);
        //设置原列表list为只读状态
        list = Collections.unmodifiableList(list);
    }

注意:subList生成子列表后,保持原列表的只读状态。

建议73:使用Comparator进行排序★★☆☆☆

在Java中,要想给数据排序,有两种实现方式,一种是实现Comparable接口,一种是实现Comparator接口,这两者有什么区别呢?示例代码:

/**
 * 员工类
 */
@Data
public class Employee implements Comparable<Employee> {
    /**
     * 工号
     */
    private int id;
    /**
     * 职位
     */
    private Position position;

    public Employee(int id, Position position) {
        this.id = id;
        this.position = position;
    }

    @Override
    public int compareTo(Employee o) {
        return new CompareToBuilder()
                .append(id, o.id).toComparison();
    }

}


public enum Position {
    Boss, Manager, Staff
}

先按id工号来排序

    public static void main(String[] args) {
        List<Employee> list = new ArrayList<>(5);
        list.add(new Employee(1001, Position.Boss));
        list.add(new Employee(1005, Position.Manager));
        list.add(new Employee(1003, Position.Staff));
        list.add(new Employee(1002, Position.Staff));
        list.add(new Employee(1004, Position.Staff));
        //按工号排序
        Collections.sort(list);
        for (Employee employee : list) {
            System.out.println(employee);
        }
        /**
         * 运行结果:
         * Employee(id=1001, position=Boss)
         * Employee(id=1002, position=Staff)
         * Employee(id=1003, position=Staff)
         * Employee(id=1004, position=Staff)
         * Employee(id=1005, position=Manager)
         */
    }

此时,我们希望按照职位来排序,并不重构Employee类,那怎么做呢?

Collections.sort方法,它有一个重载方法Collections. sort(List<T> list, Comparator<?super T>c),可以接受一个Comparator实现类,用法如下:

    class PositionComparator implements Comparator<Employee> {
        @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()))也可以实现倒序排列。

回到原题:在Java中,为什么要有两个排序接口呢?

实现了Comparable接口的类表明自身是可比较的,有了比较才能进行排序;

Comparator接口是一个工具类接口,它的名字(比较器)也已经表明了它的作用:用作比较,它与原有类的逻辑没有关系,只是实现两个类的比较逻辑。

从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法基本不会改变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法。

注意: Comparable接口可以作为实现类的默认排序法,Comparator接口则是一个类的扩展排序工具。

建议74:不推荐使用binarySearch对列表进行检索★★☆☆☆

示例代码:

    public static void main(String[] args) {
        List<String> cities = new ArrayList<>();
        cities.add("上海");
        cities.add("广州");
        cities.add("广州");
        cities.add("北京");
        cities.add("天津");
        //indexOf方法取得索引值
        System.out.println("indexOf索引值:"+cities.indexOf("广州"));
        //binarySearch方法取得索引值
        System.out.println("binarySearch索引值:"+Collections.binarySearch(cities, "广州"));
        /**
         * 运行结果:
         * indexOf索引值:1
         * binarySearch索引值:2
         */
    }

binarySearch方法返回结果错误了,为什么?

JDK上对binarySearch的描述:使用二分搜索法搜索指定列表,以获得指定对象。

但是二分法查询的一个首要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。

简单解决办法:使用Collections.sort排下序,再用binarySearch检索。

需要注意的场景:元素数据是从Web或数据库中传递进来的,原本是一个有规则的业务数据,我们为了查找一个元素对它进行了排序,也就是改变了元素在列表中的位置,那谁来保证业务规则此时的正确性呢?所以说,binarySearch方法在此处受限了。当然,拷贝一个数组,然后再排序,再使用binarySeach查找指定值,也可以解决该问题(除非处于性能的的考虑,否则使用indexOf简单、好用,而且也不会出错)。

binarySearch的二分法查找比indexOf的遍历算法性能上高几十倍。

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

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

示例代码:

    @Data
    static class City implements Comparable<City> {
        private String code;
        private String name;

        public City(String code, String name) {
            this.code = code;
            this.name = name;
        }

        @Override
        public int compareTo(City o) {
            //安装城市名称排序
            return new CompareToBuilder()
                    .append(name, o.name).toComparison();
        }

        @Override
        public boolean equals(Object obj) {
            /***省略其他判断***/
            City city = (City) obj;
            //根据code判断是否相等
            return new EqualsBuilder()
                    .append(code, city.code).isEquals();
        }
    }

    public static void main(String[] args) {
        //把多个城市对象放在一个List中,然后使用不同的方法查找同一个城市,看看返回值有什么异常。
        List<City> cities = new ArrayList<>();
        cities.add(new City("021", "上海"));
        cities.add(new City("021", "沪"));
        //排序
        Collections.sort(cities);
        //查找对象
        City city = new City("021", "沪");
        //indexOf方法取得索引值
        System.out.println("indexOf索引值:" + cities.indexOf(city));
        //binarySearch方法取得索引值
        System.out.println("binarySearch索引值:" + Collections.binarySearch(cities, city));
        /**
         * 运行结果:
         * indexOf索引值:0
         * binarySearch索引值:1
         */
    }

indexOf返回的是第一个元素,而binarySearch返回的是第二个元素,怎么回事呢?

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

从这个例子中,我们可以理解两点:

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

注意:实现了compareTo方法,就应该覆写equals方法,确保两者同步

建议76:集合运算时使用更优雅的方式★☆☆☆☆

    public static void main(String[] args) {
       List<String> list1 = new ArrayList<>();
        list1.add("A");
        List<String> list2 = new ArrayList<>();
        list2.add("B");
    }
  1. 并集

    list1.addAll(list2);
    
  2. 交集

    list1.retainAll(list2);
    
  3. 差集

    list1.removeAll(list2);
    
  4. 无重复并集

    //删除list1中出现的元素
    list2.removeAll(list1);
    //把剩余的list2元素加到list1
    list1.addAll(list2);
    

建议77:使用shuffle打乱列表★☆☆☆☆

示例代码:

    public static void main(String[] args) {
        int num = 10;
        List<String> tags = new ArrayList<>(num);
        for (int i = 0; i < num; i++) {
            tags.add(i+"");
        }
        //打乱顺序
        Collections.shuffle(tags);
    }

建议78:减少HashMap中元素的数量★★★☆☆

问题描述:同一环境中,分别向HashMapList中添加40万个字符串元素,向Map中添加元素的程序报内存溢出,向List中添加元素的程序却正常。

原因一:内部存储结构,代码如下:

    private static class Entry<K,V> implements Map.Entry<K,V> {
        final int hash;
        //键
        final K key;
        //值
        V value;
        //相同哈希码的下一个元素
        Entry<K,V> next;
        /***省略其他代码***/
    }

HashMap底层的数组变量名叫table,它是Entry类型的数组,保存的是一个一个的键值对(在我们的例子中Entry是由两个String类型组成的)。HashMap在底层虽然也是以数组方式保存元素的,其中每一个键值对就是一个元素,但是HashMap把键值对封装成了一个Entry对象,然后再把Entry放到了数组中。HashMapArrayList多了一次封装,把String类型的键值对转换成Entry对象后再放入数组,这就多了40万个对象

原因二:它的扩容机制,代码如下:

//在插入键值对时,会做长度校验,如果大于或等于阀值(threshold变量),则数组长度增大一倍        
if (size++ >= threshold) {
      resize(2 * table.length)
}
//也就是说只要HashMap的size大于数组长度的0.75倍时,就开始扩容
 threshold = (int) (newCapacity * 0.75)

由上得出,Map在扩容的时候预留了太多空间。在内存占用的临界点,由于内存剩余不足,无法提供下次扩容所需要的内存,导致扩容失败。但真正需要添加的数据,可能只是需要再扩容一个单位就可以放下。所以有时会产生,在我们看来,系统所分配的内存完全可以存放下这些数据,但是在用Map存储时却内存溢出了。

注意:尽量让HashMap中的元素少量并简单。

**建议79:集合中的哈希码不要重复★☆☆☆☆

HashMap的查找元素为例:

  1. keyhashCode定位元素的位置,所以查找元素比较快(比List快)。
  2. hashCode冲突时,则遍历对比(和List的遍历对比类似),所以当hashCode冲突时,查找元素的效率会大大降低。

注意:HashMap中的hashCode应避免冲突。

建议80:多线程使用Vector或HashTable★★☆☆☆

VectorArrayList的多线程版本,Vector的每个方法前都加上了synchronized关键字,同时只会允许一个线程进入该方法,确保了程序的可靠性。

HashTableHashMap的多线程版本,它的实现与Vector相同。

建议81:非稳定排序推荐使用List★☆☆☆☆

SetList的最大区别就是Set中的元素不可以重复(这个重复指的equals方法的返回值相等),其他方面则没有太大的区别了,在Set的实现类中有一个比较常用的类:TreeSet,该类实现了类默认排序为升序的Set集合,如果插入一个元素,默认会按照升序排列(是根据Comparable接口的compareTo的返回值确定排序位置了)

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

注意:SortedSet中的元素被修改后可能会影响其排序位置。

建议82:由点及面,一叶知秋—集合大家族★☆☆☆☆

Java中的集合类实在是太丰富了,有常用的ArrayListHashMap,也有不常用的StackQueue,有线程安全的VectorHashTable,也有线程不安全的LinkedListTreeMap,有阻塞式的ArrayBlockingQueue,也有非阻塞式的PriorityQueue等,整个集合家族非常庞大,而且也是错综复杂,可以划分为以下几类:

(1)List

实现List接口的集合主要有:ArrayListLinkedListVectorStack,其中ArrayList是一个动态数组,LinkedList是一个双向链表,Vector是一个线程安全的动态数组,Stack是一个对象栈,遵循先进后出的原则。

(2)Set

Set是不包含重复元素的集合,其主要的实现类有:EnumSetHashSetTreeSet,其中EnumSet是枚举类型的专用Set,所有元素都是枚举类型;HashSet是以哈希码决定其元素位置的Set,其原理与HashMap相似,它提供快速的插入和查找方法;TreeSet是一个自动排序的Set,它实现了SortedSet接口。

(3)Map

Map是一个大家族,它可以分为排序Map和非排序Map,排序Map主要是TreeMap类,它根据Key值进行自动排序;非排序Map主要包括:HashMapHashTablePropertiesEnumMap等,其中PropertiesHashTable的子类,它的主要用途是从Property文件中加载数据,并提供方便的读写操作;EnumMap则是要求其Key必须是某一个枚举类型。

Map中还有一个WeakHashMap类需要说明,它是一个采用弱键方式实现的Map类,它的特点是:WeakHashMap对象的存在并不会阻止垃圾回收器对键值对的回收,也就是说使用WeakHashMap装载数据不用担心内存溢出的问题,GC会自动删除不用的键值对,这是好事。但也存在一个严重问题:GC是静悄悄回收的(何时回收?God knows!),我们的程序无法知晓该动作,存在着重大的隐患。

(4)Queue

队列,它分为两类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括:ArrayBlockingQueuePriorityBlockingQueueLinkedBlockingQueue,其中ArrayBlockingQueue是一个以数组方式实现的有界阻塞队列,PriorityBlockingQueue是依照优先级组建的队列,LinkedBlockingQueue是通过链表实现的阻塞队列;另一类是非阻塞队列,无边界的,只要内存允许,都可以持续追加元素,我们最经常使用的是PriorityQueue类。

还有一种队列,是双端队列,支持在头、尾两端插入和移除元素,它的主要实现类是:ArrayDequeLinkedBlockingDequeLinkedList

(5)数组

数组与集合的最大区别就是数组能够容纳基本类型,而集合就不行,更重要的一点就是所有的集合底层存储的都是数组。

(6)工具类

数组的工具类是java.util.Arraysjava.lang.reflect.Array,集合的工具类是java.util.Collections,有了这两个工具类,操作数组和集合会易如反掌,得心应手。

(7)扩展类

集合类当然可以自行扩展了,想写一个自己的List?没问题,但最好的办法还是“拿来主义”,可以使用Apachecommons-collections扩展包,也可以使用Google的google-collections扩展包,这些足以应对我们的开发需要。

注意:commons-collectionsgoogle-collections是JDK之外的优秀数据集合工具包,使用拿来主义即可。


https://www.xamrdz.com/backend/3q21941456.html

相关文章: