目录

上一章我们讲了值类型和引用类型两大类型的数据,本章我们就来讲它们的使用。

众所周知,算法的执行效率就两个考量因素:空间时间。而代码是算法的结合体,需要考量的也是这两个因素。看具体需求选择合适的数据类型就成了必备技能。

空间筛选

首先,我们在了解了数据类型大小以及范围的时候,优先考虑空间筛选,因为空间筛选决定的是对错问题,这是最重要的,比如,为了省内存,选了个小的,结果线上出了 bug,这就不是优化时间性能可以改变的了。

优先选择小的

在满足需求的情况下,优先选择更小的数据类型。

比如,我们知道 boolean 单独使用时其实是用 int 表示的,那么我们就用更小的 byte 来表示,0 表示女,1 表示男,也很形象。

而当我们使用 boolean 数组时候,再用 boolean 来表示。

但是,有时候我们也不能光顾着省内存,而忽略了程序本身的正确性。

比如,我现在要创建一个所有用户的历史消息表,那么它的 id 用什么来表示呢?

我想想啊,一个 int 就足够了,因为不可能有十亿多条数据,这是不对的,因为你这是历史数据,历史的积累是很可怕的,比如微信 APP,动不动几个 G 的数据,而且卸载了之后,历史聊天记录也没了,为啥呢?因为历史数据太多了,历史的积累太可怕了。

所以,你应该选择 long 类型的 id,并且做好数据扩充。

优先选择容易改变的

在满足条件的情况下,优先选择容易改变的数据类型。

上一章我们讲到,利用数组可以快速访问每一个元素,但是数组有个缺点就是扩容很费劲,需要连续的内存空间,如果扩容失败,就会造成很严重的影响。

那么,作为一个大工程项目,我们在选择容器类数据类型时,就要优先选择容易改变的,比如后面要讲到的链表。

因为我们的需求不是一成不变的,如果你们产品发了毒誓、立了字据说不会改,那你也不能信!产品的嘴,骗人的鬼。

一定要选易变的,一定要选易变的。

如果是你们自己内定的需求,比如你给服务器传递参数,或者 SDK 层给 API 层传递数据,那你就可以用数组这类不可变长的数据结构。

总之,对于空间这方面的要求就两个:大小易变性,优先选小的,优先选易变的

接下来我们看时间筛选。

时间筛选

选择合适的数据结构能提高程序运行的性能,这是大部分程序员拉开水平的另一个纬度,也是最容易被忽略的维度。

很多程序员觉得:写对就成,能跑就行。你放心,你 35 岁绝对能跑。

优先选数字类型的

首先,我们在选择类型时候,要优先选择数字类型的,比如intlong等,而不是String或对象类型的,为啥呢?

因为数字类型的效率更高,不管是做算术运算、逻辑运算、还是比较运算,数字类型的效率都比引用类型的高,这就等价于提高了程序运行的速度,进而提高了性能。

我们可以看一下 Long 类型和 String 类型的equals()方法,就会发现,Long 类型简单得多,直接比较数值大小就行;而 String 类型呢,先比较长度,再逐个比较每个字符。这一对比,效率就出来了啊。

凡是带有去重功能的集合,比如 Set、Map 等,都是使用 equals 比较两个元素是否相同,相同就删除一个,这样的操作的,就导致 equals 函数被频繁调用;如果集合过大,那么 Long 类型的效率和 String 类型的效率差距就很大,就造成了量变引起质变的效果,所以我们一定要优先使用数字类型的变量。

再比如,我们经常用的 switch 语句,它的实现也很简单,就是将 case 语句排队,然后使用传入的元素,去逐个比较,也是调用了equals()函数,如果相同,那么就执行对应的case分支。

这里有个技巧:使用 switch 语句时,尽量选择连续的值作为casecode。因为编译器对switch有个优化过程,如果 code 是连续的,就会优化为一个类似于升序的数组,而我们又知道,数组的存取效率很高,所以我们要尽量选择连续的数字类型作为switch的 code。

类似的用法我们也可以用在基数排序和桶排序上。

多使用移位操作

前面第三章中,我们提到了位运算,我们知道,移位运算效率很高,所以我们平常的乘除法就可以用移位运算。

比如,现在有个商品,单价是 16,买了 15 份,那简单啊,直接:

int price = 16;
int count = 15;
int sum = price * count; //结果是240

直接就得到总价了,确实不错,写得好,写得妙,写得呱呱叫。但是,我们是可以优化的,因为单价是 16,也就是 2 的 4 次方,我们直接让买的数量左移 4 位不就行了吗?

int price = 16; // 单价是2的4次方
int count = 15;
int sum = count<<4; // 单价也是240

15 是 1111,左移 4 位是 1111 0000,也就是 2^7+2^6+2^5+2^4,也是 240。但是计算效率高了很多。

有人说,你这单价是 16,正好是 2 的 4 次放,我要是单价是 15 呢,你怎么算?

好,假如单价是 15,我买 20 份,那么我就可以这么算:因为 15 是 16-1,也就是 2 的 4 次方 -1,那么我让它左移 4 位,然后再减去自身不就行了吗?也就如下:

int price = 15;
int count = 20;
int sum = (count<<4) - count;

这样一来,不用做乘除法,只需要做移位和加减法,就可以了。

那人又说了,你这属于取巧,15 距离 16 就差一,我要是单价是 13 呢?

13 等于 8+4+1,也就是 2^3+2^2+1,那就是:

int price = 13;
int count = 20;
int sum = (count<<3) + (count<<2)+count;

直接移位相加就可以了,连减法都不用了。

其实,电脑做乘除法用的就是加法的原则,比如:25×1125\times1125×11,电脑就是将 25 累加 11 次得出的结果,你要是没有背乘法表的情况下,让你来加,你能急炸了。但是人家是电脑,电脑计算的速度非常快,很快就得出结果了。

但是我还是比较善良的,完全站在电脑的角度替它着想了,所以就优化成移位运算了,这样也可以避免量变引起质变的后果。

大数据的使用

上一章我们提到浮点类型的时候说到,二进制存在精度问题,那么在我们对数据的精度要求高的情况下,要怎么办呢?

我们可以采用 API 中提供的大数据类型,直接在编辑器中敲 Big 关键词应该都有的,比如 Java 中的BigDecimal这个类。它就可以保证我们的精度问题,其实这类 API 内部采用的都是字符串,然后将数据分段表示。

比如,Long 最大能表示 64 位数,如果我要表示 100 位呢?

嗯,我用BigInteger,它内部其实是个字符串,然后按照正常的加减运算执行,然后将结果用字符串表示出来,没办法,因为 Long 表示不下啊。这就是字符串的强大之处,正如前面我们说的,如果所有的数据类型只能保留一个,那么这个类型肯定是字符串

我们来模拟一个加法,来了解下大数据的实现原理。

题目:将两个字符串相加,并将结果以字符串方式放回。比如:

num1 = "11";
num2 = "123";
num1+num2 = "134";

这里我就不秀代码了,直接贴官方答案:

public String addStrings(String num1, String num2) {
    // add表示进位 
    int i = num1.length() - 1, j = num2.length() - 1, add = 0;
    StringBuffer ans = new StringBuffer();
    while (i >= 0 || j >= 0 || add != 0) {
        // 如果还有数据,就取这个数据,否则就是0
        int x = i >= 0 ? num1.charAt(i) - '0' : 0;
        int y = j >= 0 ? num2.charAt(j) - '0' : 0;
        // 本次相加的结果,需要加上上一次的进位
        int result = x + y + add;
        // 缀上相结果的个位数,比如相加得到15,就缀上5
        ans.append(result % 10);
        // 结果大于10就是有进位
        add = result / 10;
        i--;
        j--;
    }
    // 计算完以后的答案需要翻转过来
    ans.reverse();
    return ans.toString();
}

核心思路说白了就是,将每一位相加,相加后的进位算到下一次里面去,结果用字符串保存下来,这样不管数据有多大,都不会越界,都能得到正确的结果。

自定义数据类型

有人说,这些数据类型都不满足我啊,怎么办呢?这时候你就可以使用自定义数据类型了,也就是

比如,现在我有辆车,车又不是 int 类型的,也不是 String 类型的,嗯,那我就定义一个类型,这个类型就叫做“车”。

class Car {}

那么,怎么使用呢?跟一般的类型一样,比如你用 int 的时候就是:

int a;

那么你用自定义的“车”的时候,就是:

Car car;

没啥区别,其实对于电脑来说就是:一个类型,一个变量,劳资不管你是啥,你只要是这个原则,我照单全收。

等等,等等,刚刚我们说到,数组的缺点是扩容难,那么既然我们都可以自定义数据类型了,我们能不能定义一个扩容简单的呢?

当然可以!

我们这就可以定义一个链表,我们知道,数组扩容难,是因为它需要元素连续,我们只要让元素不连续不就行了吗? 那不连续怎么找下一个元素呢?我们可以让上一个元素持有下一个元素的地址啊。

class Node {
    int value;
    Node next;
}

嗯,好了。

啥?就这么点代码就是链表了?

对啊,就这么简单。

比如,现在有元素 1、2、3,我要存放进去,那么就如下:

Node node3 = new Node();
node3.value = 3;
node3.next = null;

Node node2 = new Node();
node2.value = 2;
node2.next = node3;

Node node1 = new Node();
node1.value = 1;
node1.next = node2;

就可以了,我们只需要知道链表的第一个元素,也就是 node1 就行了,我们要获取到 node3 的话,就先用 node1 获取到 node2,然后再用 node2 获取到 node3 ;也就是:

node2 = node1.next;
node3 = node2.next;

那这也太费劲了,得挨个去问一遍,但是这样不需要占用连续的内存啊,就算 node1 在地球,node2 在月球,node3 在木叶忍者村,只要根据 next 就能获取到啊。

那这确实挺不错的,有时候还挺有用的。

总结

本章我们讲了怎么选择合适的数据类型,以及使用大数据类型和自定义数据类型。

  • 优先选择小的,因为小的更省内存。
  • 优先选择容易扩容的,项目是多变的,要面向改变编程。
  • 优先选择数字类型的,数字类型的效率更高。
  • 多使用移位操作,移位操作可以提高程序的运行速度。
  • 遇到越界或精度问题可以使用大数据,大数据的核心是使用字符串实现的。
  • 自定义数据类型,当现有数据类型不能满足时,就用自定义数据类型。

那么下一章,我们就来看这些数据是怎么被读入内存并执行的。