前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家java】Java中二进制与位运算(“^,&,>>,>>>”),使用移位算法写一个流水号生成器(订单号生成器)

【小家java】Java中二进制与位运算(“^,&,>>,>>>”),使用移位算法写一个流水号生成器(订单号生成器)

作者头像
YourBatman
发布2019-09-03 15:13:37
3.6K0
发布2019-09-03 15:13:37
举报
文章被收录于专栏:BAT的乌托邦

二进制

二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”。

0、1是基本算符。因为它只使用0、1两个数字符号,非常简单方便,易于用电子方式实现

如:

代码语言:javascript
复制
求 1011(2)+11(2) 的和?
结果为:1110(二进制数)

二进制、八进制、十进制与十六进制,它们之间区别在于数运算时是逢几进一位。

进制转换

由于市面上已经有很多解释了,因此本文不再重复造轮子。推荐百度经验的一篇文章:二进制、八进制、十进制、十六进制之间的转换

十进制小数的二进制表示:

整数部分:除以2,取出余数,商继续除以2,直到得到0为止,将取出的余数逆序

小数部分:乘以2,然后取出整数部分,将剩下的小数部分继续乘以2,然后再取整数部分,一直取到小数部分为零为止。**如果永远不为零,则按要求保留足够位数的小数,最后一位做0舍1入。将取出的整数顺序排列。(因此肯定就可能失精度了)

二进制与编码
  • 一般对英文字符而言,一个字节表示一个字符,但是对汉字而言,由于低位的编码已经被使用(早期计算机并不支持中文,因此为了扩展支持,唯一的办法就是采用更多的字节数)只好向高位扩展。
  • 一般字符集编码的范围 utf-8>gbk>iso-8859-1(latin1)>ascll。ascll编码是美国标准信息交换码的英文缩写,包含了常用的字符,如阿拉伯数字,英文字母和一些打印符号,请注意字符和数字的区别,比如’0’字符对应的十进制数字是48。
  • unicode编码包含很多种格式,utf-8是其中最常用的一种,utf-8名称的来自于该编码使用8位一个字节表示一个字符。对于一个汉字而言,它需要3个字节表示一个汉字,但大中华地区人民表示不服,搞一套gbk编码格式,用两个字节表示一个汉字。

因此:计算效率最高

Java中二进制

Java7之前是不支持前置直接表示二进制数的,但现在可以了。

二进制:前置0b/0B

八进制:前置0

十进制:默认的,无需前置

十六禁止:前置0x/0X

代码语言:javascript
复制
    public static void main(String[] args)  {
        //备注 System.out.println()都会自动转为10禁止输出的

        //二进制
        int i = 0B101;
        System.out.println(i); //5
        //八进制
        i = 0101;
        System.out.println(i); //65
        //十进制
        i = 101;
        System.out.println(i); //101
        //十六进制
        i = 0x101;
        System.out.println(i); //257
    }
Java中的进制转换
代码语言:javascript
复制
    public static void main(String[] args) {

        int i = 192;
        System.out.println("---------------------------------");
        System.out.println("十进制转二进制:" + Integer.toBinaryString(i)); //11000000
        System.out.println("十进制转八进制:" + Integer.toOctalString(i)); //300
        System.out.println("十进制转十六进制:" + Integer.toHexString(i)); //c0
        System.out.println("---------------------------------");
        // 统一利用的为Integer的valueOf()方法,parseInt方法也是ok的
        System.out.println("二进制转十进制:" + Integer.valueOf("11000000", 2).toString()); //192
        System.out.println("八进制转十进制:" + Integer.valueOf("300", 8).toString()); //192
        System.out.println("十六进制转十进制:" + Integer.valueOf("c0", 16).toString()); //192
        System.out.println("---------------------------------");
    }
怎么证明Long类型是64位呢?

其实最简单的 我们看看Long类型的最大值,用2进制表示转换成字符串看看长度就行了

代码语言:javascript
复制
    public static void main(String[] args) {

        long l = 100L;
        //如果不是最大值 前面都是0  输出的时候就不会有那么长了
        System.out.println(Long.toBinaryString(l)); //1100100
        System.out.println(Long.toBinaryString(l).length()); //7

        l = Long.MAX_VALUE;
        //正数长度为63为
        System.out.println(Long.toBinaryString(l)); //111111111111111111111111111111111111111111111111111111111111111
        System.out.println(Long.toBinaryString(l).length()); //63

        l = Long.MIN_VALUE;
        //负数长度为64位
        System.out.println(Long.toBinaryString(l)); //1000000000000000000000000000000000000000000000000000000000000000
        System.out.println(Long.toBinaryString(l).length()); //64

    }

相对应的,可以简单看看int的范围:

代码语言:javascript
复制
    public static void main(String[] args) {

        int i = Integer.MAX_VALUE;
        //正数长度为63为
        System.out.println(Integer.toBinaryString(i).length()); //31

        i = Integer.MIN_VALUE;
        //负数长度为64位
        System.out.println(Integer.toBinaryString(i).length()); //32
    }

2进制第一位都是符号位,0表示正数1表示负数

Java中位运算符的使用

在Java中存在着这样一类操作符,是针对二进制进行操作的。它们各自是&、|、^、~、>>、<<、>>>几个位操作符。不管是初始值是依照何种进制,都会换算成二进制进行位操作。

&:按位与

操作的规则是:仅当两个操作数都为1时。输出结果才为1。否则为0

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(2 & 3); // 2
        //10 & 11 = 10 因此结果为2
    }
|:按位或

操作的规则是:仅当两个操作数都为0时,输出的结果才为0。

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(2 | 3); // 3
        //10 | 11 = 11 因此结果为3
    }
^:按位异或

异或运算操作的规则是:仅当两个操作数不同的时候。对应的输出结果才为1,否则为0,示比例如以下:

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(2 ^ 3); // 1
        //10 ^ 11 = 01 因此结果为1
    }
~:按位取反

操作规则为:全部的0置为1,1置为0,示比例如以下:

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(~12); // -13
        //~ 1100 = 10000000 00000000 00000000 00001101 因此结果为-13
    }
<<:左移

左移就是把一个数的全部位数都向左移动若干位,示比例如以下:

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(2 << 3); // 16
        //10 << 3 向左移动三位为10000  转化为十进制为1 * 2的四次方 = 16
    }

左移用得非常多,也非常好理解。x左移多少位,就相当于乘以2的多少次方就行了

>>:右移

右移就是把一个数的全部位数都向右移动若干位

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(2 >> 3); // 0
        //10 >> 3 向右移动三位 位数根本不够 所以直接就为0了
        System.out.println(100 >> 3); //12
        //1100100 >> 3 向右移动三位为1100 转换为十进制为8+4 = 12
        System.out.println(Integer.toBinaryString(100));
    }

右移用得也很多,操作其实就是吧右边的N位直接砍掉即可

>>>:无符号右移(注意:没有无符号左移)

无符号右移,忽略符号位,空位都以0补齐。

10进制转二进制的时候,因为二进制数一般分8位、 16位、32位以及64位 表示一个十进制数,所以在转换过程中,最高位会补零。

在计算机中负数采用二进制的补码表示,10进制转为二进制得到的是源码,将源码按位取反得到的是反码,反码加1得到补码

代码语言:javascript
复制
    public static void main(String[] args) {
        byte byteValue = -1;
        System.out.println(Integer.toBinaryString(byteValue)); //11111111111111111111111111111111
        //byte类型转二进制输出  需要& 0xff   否则是会向上转型为int类型再处理的
        //因为Byte类没有toBinaryString方法
        System.out.println(Integer.toBinaryString(byteValue & 0xff)); //11111111

        //此处也是一样 如果是byte类型 需要 & 0xff再右移
        System.out.println(byteValue & 0xff >>> 4);
    }

二进制的最高位是符号位,0表示正,1表示负。(0一般省略不写)

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(16 >> 2); //4
        System.out.println(16 >>> 2); //4
    }

可见正数做>>>运算的时候和>>是一样的。区别在于负数运算

至于本文中byte为什么要与上0xff,请参考:byte为什么要与上0xff?

>>>>>唯一的不同是它无论原来的最左边是什么数,统统都用0填充。

当我们掌握了Java中的位运算了之后,我们接下来利用位运算的可逆性,来达到隐藏数据的一些效果,并且效率也是非常的高

在JDK的原码中。有很多初始值都是通过位运算计算的,位运算有很多特性,能够在线性增长的数据中起到作用。且对于一些运算,位运算是最直接、最简便的方法。

移位运算还有个很大的作用,就是用在数据库上

其实玩法比较像Linux里的权限控制:权限分为 r 读, w 写, x 执行,其中 它们的权值分别为4,2,1, 所以 如果用户要想拥有这三个权限 就必须 chomd 7 , 即 7=4+2+1 表明 这个用户具有rwx权限,如果只想这个用户具有r,x权限 那么就 chomd 5即可

尴尬现象:通常 我们的数据表中 可能会包含各种状态属性, 例如 blog表中 , 我们需要有字段表示其是否公开,是否有设置密码,是否被管理员封锁,是否被置顶等等。 也会遇到在后期运维中,策划要求增加新的功能而造成你需要增加新的字段。

这样会造成后期的维护困难,数据库增大,索引增大的情况。 这时使用**位运算**就可以巧妙的解决。

  • 公开blog 给status进行或运算 UPDATE blog SET status = status | 1;
  • 加密blog 给status进行或运算 UPDATE blog SET status = status | 2;
  • 封锁blog UPDATE blog SET status = status | 4;
  • 解锁blog UPDATE blog SET status = status ^ 4;
  • 查询所有被置顶的blog SELECT * FROM blog WHERE status & 8;

注意:

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(Long.toBinaryString(10 << 61)); //1000000000000000000000000000000
        System.out.println(Long.toBinaryString(10L << 61)); //100000000000000000000000000000000000000000000000000000000000000
    }

由上面例子警示自己,当我们在移位的时候,一定要注意数据类型。一般我建议显示的表示出来,否则容易出错。我曾经有雪的教训

使用位运算,不借助第三方变量方式交换两个数的值

我们都知道java中两个数a,b互换,需要借助第三方变量作为临时变量来存储数据,再进行互换

在这里我提供两个不需要临时变量的方法

代码语言:javascript
复制
    public static void main(String[] args) {
        int a = 3, b = 5;
        System.out.println(a + "-------" + b);
        a = a + b;
        b = a - b;
        a = a - b;
        System.out.println(a + "-------" + b);
    }

这样能满足绝大部分要求,但是:a+b,可能会超出int型的最大范围,造成进度丢失,有风险

因此采用下面位运算方法更保险:

代码语言:javascript
复制
    public static void main(String[] args) {
        int a = Integer.MAX_VALUE, b = Integer.MAX_VALUE - 10;
        System.out.println(a + "-------" + b);
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;
        System.out.println(a + "-------" + b);
    }

更多方式,请参考我的另外一篇博文:【小家java】交换两个变量数值的方法(四种方法)

流水号生成器(订单号生成器)

生成订单流水号,当然这其实这并不是一个很难的功能,最直接的方式就是日期+主机Id+随机字符串来拼接一个流水号。但是今天有个我认为比较优雅方式来实现。

什么叫优雅:可以参考淘宝、京东的订单号,看似有规律,其实没规律

  1. 不想把相关信息直接暴露出去。
  2. 通过流水号可以快速得到相关业务信息,快速定位问题(这点非常重要)
  3. 使用AtomicInteger可提高并发量,降低了冲突
原理介绍

此流水号构成:日期+Long类型的值 组成的一个一长串数字,形如:2018112019492195304210432,显然前面是日期数据,后面的一长串就蕴含了不少的含义:、当前秒数、商家ID(也可以是你其余的业务数据)、机器ID、一串随机码等等

各部分介绍:

1:第一部分为当前时间的毫秒值。最大999,所以占10位

2:第二部分为:serviceType表示业务类型。比如订单号、操作流水号、消费流水号等等。最大值定为30,足够用了吧。占5位

3:第三部分为:shortParam,表示用户自定义的短参数。可以放置比如订单类型、操作类型等等类别参数。最大值定为30,肯定也是足够用了的。占5位

4:第四部分为:longParam,同上。用户一般可防止id参数,如用户id、商家id等等,最大支持9.9999亿。绝大多数足够用了,占30位

5:第五部分:剩余的位数交给随机数,随机生成一个数,占满剩余位数。一般至少有15位剩余,所以能支持2的15次方的并发,也是足够用了的

6:最后,在上面的long值前面加上日期时间(年月日时分秒)

上源码

Tips:此源码为本人独立编写,自测多种情况,若各位使用中有更好的建议,欢迎留言

代码语言:javascript
复制
/**
 * 通过移位算法 生成流水号
 * <p>
 * --> 通用版本(其实各位可以针对具体场景 给出定制化版本  没关系的)
 * (最直接的方式就是日期+主机Id+随机字符串来拼接一个流水号)
 *
 * @author fangshixiang
 * @description //
 * @date 2018/11/20 14:58
 */
public class SerialNumberUtil {

    //采用long值存储 一共63位
    private static final int BIT_COUNT = 63;
    //各个部分占的最大位数(为了减轻负担,时分秒都放到前面去  不要占用long的位数了  但是毫秒我隐藏起来,方便查问题)
    //毫秒值最大为999(1111100111)占10位
    private static final int SHIFTS_FOR_MILLS = 10;
    //下面是各部分的业务位数(各位根据自己不同的业务需求  自己定制)
    //serviceType占位
    private static final int SHIFTS_FOR_SERVICETYPE = 5;
    //shortParam占位
    private static final int SHIFTS_FOR_SHORTPARAM = 5;
    private static final int SHIFTS_FOR_LONGPARAM = 30;

    ///////////////////////////////////
    //最后的随机数 占满剩余位数
    private static final int SHIFTS_FOR_RANDOMNUM = BIT_COUNT - SHIFTS_FOR_MILLS
            - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM - SHIFTS_FOR_LONGPARAM;


    //掩码 用于辅助萃取出数据  此技巧特别巧妙
    private static final long MASK_FOR_MILLS = (1 << SHIFTS_FOR_MILLS) - 1;
    private static final long MASK_FOR_SERVICETYPE = (1 << SHIFTS_FOR_SERVICETYPE) - 1;
    private static final long MASK_FOR_SHORTPARAM = (1 << SHIFTS_FOR_SHORTPARAM) - 1;
    private static final long MASK_FOR_LONGPARAM = (1 << SHIFTS_FOR_LONGPARAM) - 1;

    //时间模版
    private static final String DATE_PATTERN = "yyyyMMddHHmmss";

    /**
     * 生成流水号  若需要隐藏跟多的参数进来,可以加传参。如订单类型(订单id就没啥必要了)等等
     *
     * @param serviceType 业务类型,比如订单号、消费流水号、操作流水号等等  请保持一个公司内不要重复
     *                    最大值:30(11110) 占5位
     * @param shortParam  短参数 不具体定义什么  一般用于表示类型。如这表示订单流水号,这里可以表示订单类型
     *                    最大值:30(11110) 占5位
     * @param longParam   长参数,一般用于记录id参数什么的,比如是订单的话,这里可以表示商户ID(商户一般不会非常多吧)
     *                    最大值:999999999(101111101011110000011111111) 占30位  表示9.999亿的数据  相信作为id的话,一般都超不过这个数值吧
     * @return 流水号 年月日时分秒+long类型的数字 = string串
     */
    public static String genSerialNum(long serviceType, long shortParam, long longParam) {
        if (serviceType > 30) {
            throw new RuntimeException("the max value of 'serviceType' is 30");
        }
        if (shortParam > 30) {
            throw new RuntimeException("the max value of 'shortParam' is 30");
        }
        if (longParam > 99999999) {
            throw new RuntimeException("the max value of 'longParam' is 99999999");
        }

        //放置毫秒值
        long mills = LocalTime.now().getNano() / 1000000; //备注 此处一定要是long类型 否则会按照int的32位去移位
        long millsShift = mills << (BIT_COUNT - SHIFTS_FOR_MILLS);

        //放置serviceType
        long serviceTypeShift = serviceType << (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE);

        //放置shortParam
        long shortParamShift = shortParam << (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM);

        //放置longParam
        long longParamShift = longParam << (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM - SHIFTS_FOR_LONGPARAM);

        //生成一个指定位数(二进制位数)的随机数  最后一个 不需要左移了 因为长度就是自己
        long randomShift = getBinaryRandom(SHIFTS_FOR_RANDOMNUM);

        //拼接各个部分
        long finalNum = millsShift | serviceTypeShift | shortParamShift | longParamShift | randomShift;

        //最后前面拼接上年月日时分秒 返回出去
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(DATE_PATTERN)) + finalNum;
    }

    /**
     * 拿到指定位数的 首位数字不为0的位数,最终以十进制数返回出来
     *
     * @param count 需要的总位数 总位数不允许超过63
     * @return binary random
     */
    private static long getBinaryRandom(int count) {
        StringBuffer sb = new StringBuffer();
        String str = "01";

        //采用ThreadLocalRandom 生成随机数 避免多线程问题
        ThreadLocalRandom r = ThreadLocalRandom.current();
        for (int i = 0; i < count; i++) {
            int num = r.nextInt(str.length());
            char c = str.charAt(num);
            while (c == '0') { //确保第一个是不为0数字 否则一直循环去获取
                if (i != 0) {
                    break;
                } else {
                    num = r.nextInt(str.length());
                    c = str.charAt(num);
                }
            }
            sb.append(c);
        }
        return Long.valueOf(sb.toString(), 2);
    }

    //===============================提供便捷获取各个部分的工具方法===================================

    /**
     * 从序列号拿到日期 并且格式化为LocalDateTime格式
     *
     * @param serialNumber 流水号
     * @return 日期时间
     */
    public static LocalDateTime getDate(String serialNumber) {
        String dateStr = serialNumber.substring(0, DATE_PATTERN.length());
        return LocalDateTime.parse(dateStr, DateTimeFormatter.ofPattern(DATE_PATTERN));
    }

    /**
     * 拿到毫秒数:是多少毫秒
     *
     * @param serialNumber 流水号
     * @return 毫秒数
     */
    public static long getMills(String serialNumber) {
        return getLongSerialNumber(serialNumber) >> (BIT_COUNT - SHIFTS_FOR_MILLS) & MASK_FOR_MILLS;
    }

    /**
     * 拿到 serviceType
     *
     * @param serialNumber 流水号
     * @return serviceType
     */
    public static long getServiceType(String serialNumber) {
        return getLongSerialNumber(serialNumber) >> (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE) & MASK_FOR_SERVICETYPE;
    }

    /**
     * 拿到 shortParam
     *
     * @param serialNumber 流水号
     * @return shortParam
     */
    public static long getShortParam(String serialNumber) {
        return getLongSerialNumber(serialNumber) >> (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM) & MASK_FOR_SHORTPARAM;
    }

    /**
     * 拿到 longParam
     *
     * @param serialNumber 流水号
     * @return longParam
     */
    public static long getLongParam(String serialNumber) {
        return getLongSerialNumber(serialNumber) >> (BIT_COUNT - SHIFTS_FOR_MILLS - SHIFTS_FOR_SERVICETYPE - SHIFTS_FOR_SHORTPARAM - SHIFTS_FOR_LONGPARAM) & MASK_FOR_LONGPARAM;
    }


    //把日期前缀去掉
    private static long getLongSerialNumber(String serialNumber) {
        return Long.parseLong(serialNumber.substring(DATE_PATTERN.length()));
    }

    //==================================================================

    /**
     * 提供测试的Main方法
     *
     * @param args the input arguments
     */
    public static void main(String[] args) {
        String serialNum = genSerialNum(1, 2, 300);
        System.out.println(serialNum); //20181121173040299068801480344

        //拿long型的值
        System.out.println(getLongSerialNumber(serialNum)); //299068801480344
        System.out.println(Long.toBinaryString(getLongSerialNumber(serialNum)));

        //拿到日期时间
        System.out.println(getDate(serialNum)); //2018-11-21T17:30:40

        //拿毫秒值
        System.out.println((LocalTime.now().getNano() / 1000000));
        System.out.println(getMills(serialNum));

        //拿到serviceType
        System.out.println(getServiceType(serialNum)); //1

        //拿到shortParam
        System.out.println(getShortParam(serialNum)); //2

        //拿到longParam
        System.out.println(getLongParam(serialNum)); //300
    }
}
使用场景

我们可以使用它来生成订单号、操作流水号、消费记录等等

二进制中的原码, 反码, 补码

在探求为何机器要使用补码之前, 让我们先了解原码, 反码和补码的概念.对于一个数, 计算机要使用一定的编码方式进行存储. 原码, 反码, 补码是机器存储一个具体数字的编码方式.

原码

原码就是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 比如如果是8位二进制

所以8位二进制数的取值范围就是:

1111 1111 , 0111 1111

-127 , 127

原码是人脑最容易理解和计算的表示方式.

反码

正数的反码是其本身,负数的反码是在其原码的基础上, 符号位不变,其余各个位取反.

正数:00000001原 = 00000001反

负数:10000001原 = 11111110反

如果一个反码表示的是负数, 人脑无法直观的看出来它的数值. 通常要将其转换成原码再计算.

补码

正数的补码就是其本身

负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, **最后+1. (即在反码的基础上+1)**

正数:00000001原 = 00000001反 = 00000001补

负数:10000001原 = 11111110反 = 11111111补

补码表示方式也是人脑无法直观看出其数值的. 通常也需要转换成原码在计算其数值

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018年11月20日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二进制
    • 进制转换
      • 十进制小数的二进制表示:
        • 二进制与编码
        • Java中二进制
          • Java中的进制转换
          • 怎么证明Long类型是64位呢?
          • Java中位运算符的使用
            • &:按位与
              • |:按位或
                • ^:按位异或
                  • ~:按位取反
                    • <<:左移
                      • >>:右移
                        • >>>:无符号右移(注意:没有无符号左移)
                          • 移位运算还有个很大的作用,就是用在数据库上
                          • 使用位运算,不借助第三方变量方式交换两个数的值
                          • 流水号生成器(订单号生成器)
                            • 原理介绍
                            • 上源码
                              • 使用场景
                              • 二进制中的原码, 反码, 补码
                                • 原码
                                  • 反码
                                    • 补码
                                    领券
                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档