【编写高质量代码:改善Java程序的151个建议】第5章:数组和集合___建议60~66

如果你浪费了自己的年龄,那是挺可悲的。因为你的青春只能持续一点儿时间——很短的一点儿时间。 —— 王尔德

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

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

package OSChina.Client;

import java.util.ArrayList;
import java.util.List;

public class Client2 {
    public static void main(String[] args) {
        int datas[] = new int[10000000];
        for (int i = 0; i < 10000000; i++) {
            datas[i] = i;
        }
        int sum = 0;
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        System.out.println(sum);
        long end1 = System.currentTimeMillis();
        System.out.println("数组解析耗时:" + (end1 - start1) + "ms");
        sum = 0;
        List<Integer> list = new ArrayList<Integer>();
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list.add(i);
        }
        for (int i = 0; i < list.size(); i++) {
            sum += list.get(i);
        }
        System.out.println(sum);
        long end2 = System.currentTimeMillis();
        System.out.println("list解析耗时:" + (end2 - start2) + "ms");
    }
}

6bdf6092d30ca37794d8655806d349b06f4.jpg

原理:

    //对数组求和
    public static int sum(int datas[]) {
        int sum = 0;
        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        return sum;
    }

对一个int类型 的数组求和,取出所有数组元素并相加,此算法中如果是基本类型则使用数组效率是最高的,使用集合则效率次之。

再看使用List求和: 

    // 对列表求和计算
    public static int sum(List<Integer> datas) {
        int sum = 0;
        for (int i = 0; i < datas.size(); i++) {
            sum += datas.get(i);
        }
        return sum;
    }

注意看sum+=datas.get(i);这行代码,这里其实做了一个拆箱动作,Interger对象通过intValue方法自动转换成一个int基本类型,对于性能濒于临界的系统来说该方案很危险,特别是大数据量的时候,首先,在初始化list数组时都会进行装箱操作,把一个int类型包装成一个interger对象,虽然有整型池在,但不在整型池范围内的都会产生一个新的interger对象,众所周知,基本类型是在栈内存中操作的,而对象是在堆内存中操作的,栈内存的有点:速度快,容量小;堆内存的特点:速度慢,容量大。其次,在进行求和运算时,要做拆箱操作,性能消耗又产生了。对基本类型进行求和运算时,数组的效率是集合的10倍。

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

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

Java中的数组是定长的,一旦经过初始化声明就不可改变长度,这在实际使用中非常不方便。

数组也可以变长:

package OSChina.Client;

import java.util.Arrays;

public class Clinet3 {
    public static <T> T[] expandCapacity(T[] datas,int newLen){
        newLen = newLen< 0?0:newLen;
        return Arrays.copyOf(datas,newLen);
    }

    public static void main(String[] args) {
        Integer[] array = new Integer[60];
        for (int i = 0; i < 65; i++) {
            array[i] = i;
        }
        System.out.println("我是江疏影!");
    }
}

7f1eb9d06bb2e6a445ccbf5539cb5d9376a.jpg

package OSChina.Client;

import java.util.Arrays;

public class Clinet3 {
    public static <T> T[] expandCapacity(T[] datas,int newLen){
        newLen = newLen< 0?0:newLen;
        return Arrays.copyOf(datas,newLen);
    }

    public static void main(String[] args) {
        Integer[] array = new Integer[60];
        array = expandCapacity(array,80);
        for (int i = 0; i < 65; i++) {
            array[i] = i;
        }
        System.out.println("我是江疏影");
    }
}

6a4a69ceb6a761fc4abc19d89d72757da60.jpg

通过这样的处理方式,曲折的解决了数组的变长问题,其实,集合的长度自动维护功能的原理与此类似。在实际开发中,如果确实需要变长的数据集,数组也是在考虑范围之内的,不能因固定长度而将其否定之。

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

import java.util.Arrays;
import org.apache.commons.lang.builder.ToStringBuilder;

public class Client62 {
    public static void main(String[] args) {
        // 气球数量
        int ballonNum = 7;
        // 第一个箱子
        Balloon[] box1 = new Balloon[ballonNum];
        // 初始化第一个箱子中的气球
        for (int i = 0; i < ballonNum; i++) {
            box1[i] = new Balloon(Color.values()[i], i);
        }
        // 第二个箱子的气球是拷贝第一个箱子里的
        Balloon[] box2 = Arrays.copyOf(box1, box1.length);
        // 修改最后一个气球颜色
        box2[6].setColor(Color.Blue);
        // 打印出第一个箱子中的气球颜色
        for (Balloon b : box1) {
            System.out.println(b);
        }

    }
}

// 气球颜色
enum Color {
    Red, Orange, Yellow, Green, Indigo, Blue, Violet
}

// 气球
class Balloon {
    // 编号
    private int id;
    // 颜色
    private Color color;

    public Balloon(Color _color, int _id) {
        color = _color;
        id = _id;
    }

    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;
    }

    @Override
    public String toString() {
        //apache-common-lang包下的ToStringBuilder重写toString方法
        return new ToStringBuilder(this).append("编号", id).append("颜色", color).toString();
    }

}

第二个箱子里最后一个气球的颜色毫无疑问是被修改为蓝色了,不过我们是通过拷贝第一个箱子里的气球然后再修改的方式来实现的,那会对第一个箱子的气球颜色有影响吗?我们看看输出结果:

48898234f6da177b2471d4bb7bf7c40adc5.jpg

最后一个气球颜色竟然也被修改了,我们只是希望修改第二个箱子的气球啊,这是为何?这是典型的浅拷贝(Shallow  Clone)问题,以前第一章序列化时讲过,但是这里与之有一点不同:数组中的元素没有实现Serializable接口。

确实如此,通过copyof方法产生的数组是一个浅拷贝,这与序列化的浅拷贝完全相同:基本类型直接拷贝值,引用类型时拷贝引用地址。

数组的clone同样也是浅拷贝,集合的clone也是浅拷贝。

问题找到了,解决起来也很简单,遍历box1的每个元素,重新生成一个气球对象,并放到box2数组中。

集合list进行业务处理时,需要拷贝集合中的元素,可集合没有提供拷贝方法,自己写很麻烦,干脆使用list.toArray方法转换成数组,然后通过arrays.copyof拷贝,再转回集合,简单边界!但非常遗憾,有时这样会产生浅拷贝的问题。

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

我们经常使用ArrayList、Vector、HashMap等集合,一般都是直接用new跟上类名声明出一个集合来,然后使用add、remove等方法进行操作,而且因为它是自动管理长度的,所以不用我们特别费心超长的问题,这确实是一个非常好的优点,但也有我们必须要注意的事项。

package OSChina.Client;

import java.util.ArrayList;
import java.util.List;

public class Client4 {
    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<Integer>();
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            list1.add(i);
        }
        long end1 = System.currentTimeMillis();
        System.out.println("不设置初始长度耗时:" + (end1 - start1) + "ms");
        long start2 = System.currentTimeMillis();
        List<Integer> list2 = new ArrayList<Integer>(10000000);
        for (int i = 0; i < 10000000; i++) {
            list2.add(i);
        }
        long end2 = System.currentTimeMillis();
        System.out.println("设置初始长度耗时:" + (end2 - start2) + "ms");
    }
}

c37ae24c6cc0bc6fa7fc0702a2553a1f8ad.jpg

如果不设置初始容量,ArrayList的默认初始容量是10,系统会按照1.5倍的规则扩容,每次扩容都是一次数组的拷贝,如果数组量大,这样的拷贝会非常消耗资源,而且效率非常低下。所以,要设置一个ArrayList的可能长度,可以显著提升系统性能。

其它集合也类似,Vector扩容2倍。

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

对一批数据进行排序,然后找出其中的最大值或最小值,这是基本的数据结构知识。在Java中我们可以通过编写算法的方式,也可以通过数组先排序再取值的方式来实现,下面以求最大值为例,解释一下多种算法:

package OSChina.Client;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;

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

    //先排序,后取值
    public static int maxSort(int[] data) {
        Arrays.sort(data);
        return data[data.length - 1];
    }

    public static void main(String[] args) {
        int datas[] = new int[100000000];
        for (int i = 0; i < 100000000; i++) {
            datas[i] = i;
        }
        int sum = 0;

        for (int i = 0; i < datas.length; i++) {
            sum += datas[i];
        }
        long start1 = System.currentTimeMillis();
        System.out.println("快速查找最大值:"+max(datas));
        long end1 = System.currentTimeMillis();
        System.out.println("快速查找最大值耗时:" + (end1 - start1) + "ms");


        long start2 = System.currentTimeMillis();
        System.out.println("先排序,后取值,最大值:"+maxSort(datas));
        long end2 = System.currentTimeMillis();
        System.out.println("先排序,后取值,最大值耗时:" + (end2 - start2) + "ms");
    }
}

4a97d5372d0d5e96b9cd4bc658b866f800f.jpg

从效率上将,快速查找法更快一些,只用遍历一次就可以计算出最大值,但在实际测试中发现,如果数组量少于10000,两个基本上没有区别,但在同一个毫秒级别里,此时就可以不用自己写算法了,直接使用数组先排序后取值的方式。

如果数组元素超过10000,就需要依据实际情况来考虑:自己实现,可以提高性能;先排序后取值,简单,通俗易懂。排除性能上的差异,两者都可以选择,甚至后者更方便一些,也更容易想到。

总结一下,数据量不是很大时(10000左右),使用先排序后取值比较好,看着高大上?总比自己写代码好!,数据量过大,出于性能的考虑,可以自己写排序方法!

感觉这条有点吹毛求疵了!

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

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

    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());
    }

注:

① treeSet.lower()方法返回集合中小于指定值的最大值。

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

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

我们在开发中经常会使用Arrays和Collections这两个工具类和列表之间转换,非常方便,但也有时候会出现一些奇怪的问题,来看如下代码:

public class Client65 {
    public static void main(String[] args) {
        int data [] = {1,2,3,4,5};
        List list= Arrays.asList(data);
        System.out.println("列表中的元素数量是:"+list.size());
    }
}

也许你会说,这很简单,list变量的元素数量当然是5了。但是运行后打印出来的列表数量为1。

事实上data确实是一个有5个元素的int类型数组,只是通过asList转换成列表后就只有一个元素了,这是为什么呢?其他4个元素到什么地方去了呢?

我们仔细看一下Arrays.asList的方法说明:输入一个变长参数,返回一个固定长度的列表。注意这里是一个变长参数,看源码:

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

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

解决方法:

Integer data [] = {1,2,3,4,5};

把int替换为Integer即可让输出元素数量为5.需要说明的是,不仅仅是int类型的数组有这个问题,其它7个基本类型的数组也存在相似的问题,这就需要大家注意了,在把基本类型数组转换为列表时,要特别小心asList方法的陷阱,避免出现程序逻辑混乱的情况。

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

上一个建议指出了asList方法在转换基本类型数组时存在的问题,接着我们看一下asList方法返回的列表有何特殊的地方,代码如下: 

package OSChina.Client;

import java.util.Arrays;
import java.util.List;

public class Client5 {
    public static void main(String[] args) {
        // 五天工作制
        Week days[] = { Week.Mon, Week.Tue, Week.Wed, Week.Thu, Week.Fri };
        // 转换为列表
        List<Week> list = Arrays.asList(days);
        // 增加周六为工作日
        list.add(Week.Sat);
        /* do something */
    }
}
enum Week {
    Sun, Mon, Tue, Wed, Thu, Fri, Sat
}

10da38aff70cd1187f82a1cb3f0e84c895e.jpg

UnsupportedOperationException,不支持的操作,居然不支持list的add方法,这是什么原因呢?

此ArrayList非java.util.ArrayList,而是Arrays工具类的一个内部类

我们深入地看看这个ArrayList静态内部类,它仅仅实现了5个方法:

① size:元素数量

② get:获得制定元素

③ set:重置某一元素值

④ contains:是否包含某元素

⑤ toArray:转化为数组,实现了数组的浅拷贝

对于我们经常使用list.add和list.remove方法它都没有实现,也就是说asList返回的是一个长度不可变的列表,数组是多长,转换成的列表也就是多长,换句话说此处的列表只是数组的一个外壳,不再保持列表的动态变长的特性,这才是我们关注的重点。有些开发人员喜欢这样定义个初始化列表: 

List<String> names= Arrays.asList("张三","李四","王五");

一句话完成了列表的定义和初始化,看似很便捷,却隐藏着重大隐患---列表长度无法修改。想想看,如果这样一个List传递到一个允许添加的add操作的方法中,那将会产生何种结果,如果有这种习惯的javaer,请慎之戒之,除非非常自信该List只用于只读操作。

猜你喜欢

转载自blog.csdn.net/guorui_java/article/details/104247804#comments_22062025