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

若是你浪费了本身的年龄,那是挺可悲的。由于你的青春只能持续一点儿时间——很短的一点儿时间。 —— 王尔德java

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

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

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

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

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

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

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

建议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");
    }
}

原理:

//对数组求和
    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("我是江疏影!");
    }
}

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("我是江疏影");
    }
}

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

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

}

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

最后一个气球颜色居然也被修改了,咱们只是但愿修改第二个箱子的气球啊,这是为什么?这是典型的浅拷贝(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");
    }
}

若是不设置初始容量,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");
    }
}

从效率上将,快速查找法更快一些,只用遍历一次就能够计算出最大值,但在实际测试中发现,若是数组量少于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只用于只读操做。

 

编写高质量代码:改善Java程序的151个建议@目录

相关文章
相关标签/搜索