JavaSE基础知识(五)--面向对象代码实现举例(Integer类源码)

Java SE 是什么,包括哪些内容(五)?

本文内容参考自Java8标准

一、面向对象:
  • 1、正式解读"对象"(“如果你明白了"JavaSE基础知识(五)–面向对象思想概述",那么本片篇博文才不会让你感觉吃力”)
    首先,还是需要回到基础类型,在前面的多篇博文中已经详细解读了基础类型的所有相关内容,如果你能结合博文"JavaSE基础知识(五)–面向对象思想概述"一起看,你会发现问题:基础类型需不需要状态?基础类型需不需要行为?基础类型难道仅仅就是一个声明(int a = 5;)就足够用了吗?
    来举例看一看:
    声明一个整数类型(int):

     // 声明一个基础类型(int)
        int a = 5

    从以上这个声明中,我们能得到的信息仅仅就是类型是int标识符是a值是5,再没有其他内容,如果这个时候你突然想用int类型最大值,或者int类型最小值,又或者你突然想知道一个十进制数它的二进制、八进制、十六进制表示分别是什么样的(数制转换)?诸如以上这些内容,我们都需要自己从0开始去写–最大值最小值需要想办法去查(实际上,如果你不特别记忆,你每次要用的时候都需要去查),数制转换肯定需要多行代码,那么,能不能通过类类型的思想,将所有与int类型有关的数值都用一个标识符存储起来(int类型状态),将所有int类型的通用行为(多行代码)收录进一个方法中,同时取一个合适的方法名称标识符?可以的,Java基础类库中(基础类库实际上就是Java语言已经为我们准备好的类类型,直接拿来就可以使用!)有一个类类型Integer,它就是int类型的类类型,它里面有很多状态标识符,以及各种方法。我们需要来看一下它的源码,Java基础类库中的源码都是大神级别的人物写的,如果你有的时候想看看源码,看不懂也是很正常的–因为源码总是考虑到了并且也适用于任何情况!(这里特别提醒一下想完全看懂源码是基本不可能的,如果你能完全看懂,下面的内容就没必要继续看了。)。
    我们首先来看看,有哪些状态标识符,是否有我们预期的int类型最大值最小值
    Integer类类型有int类型的最小值和最大值!
    如果英语稍微入门了的人从标识符MIN_VALUEMAX_VALUE这两个名称就能看出,分别是最小值最大值。但是从0x80000000x7fffffff的数值表现形式上来看,不是专业的计算机从业人员或者是新手,第一时间可能很难理解,实际上,这是数制问题,一般人习惯了十进制,而这里,大神选择了十六进制(因为出现了"f"!)。我们来转换一下:
    0x80000000转换成十进制是-2147483648!
    ![0x7fffffff转换成十进制是2147483647!
    通过以上的转换,我们能确定:类类型Integer中的标识符MIN_VALUEMAX_VALUE其实就是int类型最小值最大值,那么这么做(存储在类类型Integer中)有什么好处呢?
    我们来看下下面的示例:
    直接只用Integer类!
    从代码中可以看出,如果我们需要使用int类型的最大值或者是最小值,那么我们只需要记住有Integer这么一个类类型,然后记住MAX_VALUEMIN_VALUE这两个标识符名称(上面示例代码中的代码形式后期博文会细说,现在需要了解的是"面向对象思想")就行,不管在什么情况下,Integer.MAX_VALUE和Integer.MIN_VALUE始终代表了int类型最大值最小值
    我们继续来看源码,还有什么内容:
    Integer类类型的其他标识符!
    TYPE这里暂时不提,我们来看一下digits为什么这里需要用一个数组来存储0-9以及a-z呢?由此我们可以联想到十六进制十六进制中用到了a,b,c,d,e,f分别表示10,11,12,13,14,15。那么,这里有a-z,所有我们猜测计算机中存在的最大数制是三十六进制(从0数到z刚好是36,但是没有一进制,只有二进制,所以必须从1开始数,数到z,总共是35,我们知道最大的基数进制1,那么最大基数是35进制就是36进制了)!实际上,计算机中最大的进制确实是三十六进制。
    由于以下需要截图的代码长度超出了屏幕范围,所以我直接复制代码了!
    接续看源码,发现没有标识符了,剩下的部分全部都是Integer类型的方法,我们来研究其中几个方法,首先,第一个方法是**toString(int i,int radix)**这个方法的意义在于,能将任何数i,根据你输入的进制基数radix,显示成它应该成为的样子,比如你输入一个数55,同时再输入一个2,就表示你想知道55的2进制是什么样子的:
    我们先来看一下这个方法的实际应用示例:
    十进制55转换成二进制表示!
    实际上,这个方法可以在二,八,十,十六进制之间相互转换,只是你的i需要表示正确,分别用对应进制的数表示方法,比如八进制必须以0开头,十六进制必须以0x开头。
    接下来,我们深入研究这个方法的源码:

     // Integer类型的源码
     public static String toString(int i, int radix) {
     //这里的判断就是上面提到的最小数制为二进制,
     //最大数制为三十六进制
     //如果参数radix为小于2或者是大于36的数值,则直接给它赋值10,
     //也就是默认十进制显示。
          if (radix < Character.MIN_RADIX || 
               radix > Character.MAX_RADIX)
              radix = 10;
    
          /* Use the faster version */
      //如果radix的值刚好好等于10,就调用toString(int i)方法
      //注意这个方法的参数只有1个。
          if (radix == 10) {
              return toString(i);
          }
       //创建一个空间为33的char数组!
          char buf[] = new char[33];
       //这里是通过negative来判断参数i到底是正数还是负数。
       //true和false刚好对应正数和负数。
          boolean negative = (i < 0);
      //定义一个int类型的标识符charPos,初始化为32。
          int charPos = 32;
      //如果i是负数:
          if (!negative) {
      //那么就将负数的绝对值再赋值给i,
      //这个时候i就是正数了。
              i = -i;
          }
      //如果i小于-radix
          while (i <= -radix) {
      //将-(i%radix)的值赋值给char数组的第charPos位置。
      //同时charPos值减1。
              buf[charPos--] = digits[-(i % radix)];
       //将i/radix的值再赋值给i。
              i = i / radix;
        //进行到这里,将回到while(i<-radix);如果i<-radix成立,
        //将继续以上循环步骤
          }
        //如果i<-radix不再成立,则退出while循环体,
        //继续从这里执行下去。
          buf[charPos] = digits[-i];
        //如果i是负数,因为之前赋值了绝对值。
          if (negative) {
         //这里记得需要将负数的符号加回去。
         //所以将"-"放在了charPos数组的最前面。
              buf[--charPos] = '-';
          }
          //将charPos数组作为String对象返回。
          return new String(buf, charPos, (33 - charPos));
      }
    

    其实这个方法的核心思想是,首先判断参数radix是几,那么就是需要转换成几进制,接着判断参数i到底是整数还是负数,然后就用i除以进制radix,这个步骤需要不断的进行,不断的除(所以需要用while循环,while循环后期博文会详解),直到结果比radix小(转换成二进制,最后结果比2小就行,转换成八进制,最后结果比八小就行…所以while循环的终止条件是i<radix,如果i是负数,则就是i<-radix),最后将每一步得到的结果按倒序存储在一个char数组中(这就是为什么最先给char[charPos]赋值,因为charPos是32,int类型就是32位,然后charPos–),然后再将char数组转换成String对象,String对象就是我们最后得到的结果,直接可以将String对象打印输出。
    这段源码的思想值得牢记!
    我们继续来看下一个方法:toUnsignedString(int i, int radix),此方法涉及到Long对象,它的核心思想也是在Long对象的源码中,研究Long源码的时候再说。
    我们继续来看下一个方法:toHexString(int i),这个方法固定将i统统转化成十六进制表示!
    示例:
    十进制转换成十六进制的结果!
    我们继续来看下一个方法:toOctalString(int i),这个方法固定将i统统转化成八进制表示!

    十进制转换成八进制的结果!
    我们继续来看下一个方法: toBinaryString(int i),这个方法固定将i统统转化成二进制表示!
    十进制转换成二进制的结果!
    我们继续来看下一个方法:toUnsignedString0(int val, int shift),这个方法比较特殊,在Integer类以外你是不能直接利用它的,它的存在只是在Integer内部做支撑,支撑其他方法的实现(一般来说在一个类的内部如果会重复用到一段相同的代码,那么可以给这段代码取一个方法名称,并设置为private。):
    private方法仅作为类内部的支撑部分,对外不可见!
    PS:权限控制在后期博文中会专门讲述。
    我们具体来看一下toUnsignedString0(int val, int shift) 具体支撑了哪些方法:
    支撑了其他进制转换成十六进制的方法!
    支撑了其他进制转换成八进制的方法!
    支撑了其他进制转换成二进制的方法!
    我们发现, toUnsignedString0(int val, int shift) 主要支撑的方法都是进制转换的方法,我们来看看它的源码:

    // toUnsignedString0(int val, int shift)方法的源码
    //方法是private,并且它的返回值是String类型。
    private static String toUnsignedString0
                                    (int val, int shift) {
         // assert shift > 0 && 
         //shift <=5 : "Illegal shift value";
         //Integer.SIZE是指int类型是32位,
         //Integer.numberOfLeadingZeros(val)方法得出的结果是
         //二进制的0位有多少个。
         //mag的值是这个数实际表示用了多少位。
         int mag = Integer.SIZE - 
         Integer.numberOfLeadingZeros(val);
         //chars的值是指具体表示这个数需要多少位内存空间
         int chars = 
          Math.max(((mag + (shift - 1)) / shift), 1);
          //开一个chars位的数组对象。
         char[] buf = new char[chars];
         //另一个方法,给chars的每一位赋值。
         formatUnsignedInt(val, shift, buf, 0, chars);
         // Use special constructor which takes over "buf".
         //返回一个String对象,就是return。
         return new String(buf, true);
     }
    

    上面这个方法的源码有一个非常隐秘的技巧,也可以说是数字计算的真理,很值得学习。来捋一下它的思路,我尽量表述清楚:
    首先我们知道它是作用于数制转换的,这个方法最后力求得到的结果是1001011这种形式,而不是0000 0000 0000 0000 0000 0000 0100 1011这种形式,所以,先得到int的位数Integer.SIZE(32)。然后得到前置多少个0(numberOfLeadingZeros(int value)方法能得到),两值相减,得出实际需要的位数(比如1001011实际只需要7位),实际上,到这里,代码的基础都是以二进制为基础的,并未涉及到八进制和十六进制,那么Math.max(((mag + (shift - 1)) / shift), 1); 是什么意思?我说的技巧就在这行代码里面,这行代码的关键就在于,将得到的二进制结果分别转换成八进制和十六进制(转换成八进制,只需要每三位二进制为一组,转换为十六进制,只需要每四位二进制为一组。),再回到源码:
    每四位二进制为一组就转换成了十六进制!
    每三位二进制为一组就转换成了八进制!
    每一位二进制为一组就转换成了二进制!
    所以,我们发现,参数shift就是用来表示二进制位数的。
    那么,我们来具体分析一下:比如一个二进制表示:100101010110,它转换成八进制后,是4位(4526),但是它转换成十六进制,是3位(956),所以:Math.max(((mag + (shift - 1)) / shift), 1); 这行代码不但需要根据shift的值确定进制是多少,还需要计算出所表示进制的位数是多少,还得是通用的,所以是这样:(mag + (shift - 1)) / shift),为什么还需要shift-1这种操作,是因为当出现5/3的时候,值不是1,而是2,因为不管转换成什么进制,只要有1位二进制,都需要目标进制的1位来表示。所以你知道了,要进位就用**(shift - 1)) / shift**这个表达式没错,一般人可能会这么写:

     // 一般人的操作:
      if(shift==1){}
      else if(shift==3){}
      else if(shift==4){}
      else{}
      //或者是switch语句:
       switch(shift){
        case 1:当shift等于1的时候怎么处理
        case 3:当shift等于3的时候怎么处理
        case 4:当shift等于4的时候怎么处理
     }
    

    按照一般人的思维,都会将shift的所有可能值全部列出来分别处理,但是大神不用。一句**(mag + (shift - 1)) / shift)**就能解决所有的问题,所以这个诀窍我们需要学习并且牢记!
    我们来看一下支撑 toUnsignedString0(int val, int shift) 的核心方法:formatUnsignedInt(val, shift, buf, 0, chars),先看它的源码:

    // formatUnsignedInt方法的源码
    static int formatUnsignedInt(int val, int shift, char[] buf, int offset, int len) {
    //定义一个int类型存储传入的len值。
       int charPos = len;
    //定义一个int类型来存储进制,这里有点意思,用的是左移。
    //如果shift是1,那么1左移1位是2,shift是3,1左移3位是8,
    //如果shift是4,1左移4位是16,所以radix根据传入的shift计算得出。
       int radix = 1 << shift;
     //定义一个int类型的mask存储最大的进制基数,因为最大的
     //进制基数都是进制-1,所以是radix-1。
     //不知道看到这里你有没有一点编程的感觉。
       int mask = radix - 1;
     //这里使用的是do..while循环,先执行一次循环体。
       do {
      // buf[offset + --charPos]这个我们能看明白,无论[ ]里面多复杂
      //实际就是用++ --表示了一个数组的下标位置。
      //我们来看下[val & mask],通过"&"我们知道这个是与操作,那么这是什么目的呢?
      //因为是与操作,我们先将mask存储的值转换为二进制表示:我们发现,mask总是
      //对应进制的最大进制基数,二进制它是1(1),八进制它是111(7),十六进制它是1111(15),
      //所以它是用目标值与当前最大进制基数进行与操作。
      //实际上,这个与操作是为了完全保留当前目标值的所有二进制表示。
      //比如1010与1111进行与操作结果还是1010,101与111进行与操作结果还是101。
      //实际上这里是将目标值转换为了二进制,同时根据shift的值将它的二进制表示按
      //每shift的值分成多组,比如shift是4,就每4位为一组。
           buf[offset + --charPos] = Integer.digits[val & mask];
           //每进行一次与运算,就按shift的值进行无符号右移,实际上就是下一组
           //二进制。之前不是说了每4位嘛,这里就等于是与完了这组,继续下一组的意思。
           //你再去了解一下">>>"的功能就明白了。
           val >>>= shift;
           //这里是循环停止的条件
       } while (val != 0 && charPos > 0);
       //最后将最终的charPos值返回。charPos是从第33位开始递减的,返回它的最终值,就知道了
       //目标值实际上有几位。
       //如果实在是看不懂的,就联系我的QQ吧,我可以详细再解释一下。
       return charPos;
    }
    

    通过以上所有的方法,就将一个数完全拆解开来并放进数组中,最后将数组返回,这里涉及到了很多的二进制运算技巧,我们需要牢记!
    由于篇幅有限,源码部分就先暂时介绍这么多,有兴趣的,希望更深入学习源码的,可以关注我后续的博文更新。
    通过以上源码的分析,我们发现,与其每次都从头开始研究,一步一步写代码,不如将所有的基础设置都装在一个整体里面,需要的时候就直接拿来使用,这个整体实际上就是我们说的类类型,有了类类型以后,我们就可以通过类类型创建对象了,比如汽车类型,我可以用汽车类型创建很多辆汽车,简单的入门级理解角度就是,你在创建对象的时候,实际上就是在分配你电脑的内存空间,你的创建的每一个对象都是需要有对应的内存空间来存储的。你只有通过创建某个类类型的对象,才能使用到这个类类型带来的好处。所以为什么说Java是面向对象的编程语言,它不但提供了面向对象的编程机制(就是让你自己可以创建自己需要的对象),它还为你提供了大量的、高效的类类型,以供你在需要的时候去创建这些类类型的对象,比如上面分析的Integer类型,你会发现它的源码可能你很难想到,但是用起来却很好用,很方便!(这些类类型,你可以放心地使用,都是大神写的)。
    那么Java提供的类类型都在哪里呢?不知道你们听过API这个专有名词没有,有专门的API文档,文档里面就包含了所有的Java提供的类类型。
    API文档!
    平时编程中,需要多看看API文档,避免经常自己走弯路,它里面有的很多巧妙的方法,你只要创建一个对象就能直接拿来用就行,实际上,源码是什么你都不用去了解。
    最后在这里,再进行一个核心的对比:
    如果我们需要实现一个方法,它的功能就是依据我们提供的进制数来得出相应的结果,我们需要怎么做:其实就是这些代码:

    // Integer类型的源码
    public static String toString(int i, int radix) {
    //这里的判断就是上面提到的最小数制为二进制,
    //最大数制为三十六进制
    //如果参数radix为小于2或者是大于36的数值,则直接给它赋值10,
    //也就是默认十进制显示。
       if (radix < Character.MIN_RADIX || 
            radix > Character.MAX_RADIX)
           radix = 10;
    
       /* Use the faster version */
    //如果radix的值刚好好等于10,就调用toString(int i)方法
    //注意这个方法的参数只有1个。
       if (radix == 10) {
           return toString(i);
       }
    //创建一个空间为33的char数组!
       char buf[] = new char[33];
    //这里是通过negative来判断参数i到底是正数还是负数。
    //true和false刚好对应正数和负数。
       boolean negative = (i < 0);
    //定义一个int类型的标识符charPos,初始化为32。
       int charPos = 32;
    //如果i是负数:
       if (!negative) {
    //那么就将负数的绝对值再赋值给i,
    //这个时候i就是正数了。
           i = -i;
       }
    //如果i小于-radix
       while (i <= -radix) {
    //将-(i%radix)的值赋值给char数组的第charPos位置。
    //同时charPos值减1。
           buf[charPos--] = digits[-(i % radix)];
    //将i/radix的值再赋值给i。
           i = i / radix;
     //进行到这里,将回到while(i<-radix);如果i<-radix成立,
     //将继续以上循环步骤
       }
     //如果i<-radix不再成立,则退出while循环体,
     //继续从这里执行下去。
       buf[charPos] = digits[-i];
     //如果i是负数,因为之前赋值了绝对值。
       if (negative) {
      //这里记得需要将负数的符号加回去。
      //所以将"-"放在了charPos数组的最前面。
           buf[--charPos] = '-';
       }
       //将charPos数组作为String对象返回。
       return new String(buf, charPos, (33 - charPos));
    }
    ......这里用省略号代表还有很多代码。
    

    如果没有面向对象的机制,我们在任何地方对这个方法有需要,都必须将它重头到尾写一遍。
    那么,有了面向对象机制怎么处理?实际上只需要下面一行代码就行:

    // 直接利用对象调用方法
      Integer.toString(int i, int radix)//这种方式就相当的简洁了,只是在使用之前,你需要告诉编译器你的这个对象存储在哪里,这是之后import关键字的作用。
    

    感受一下面向对象编程的便捷性。编写一次,到处使用。
    下一篇博文将重点关注如何来创建一个对象,它有什么规则。

PS:时间有限,有关Java SE的内容会持续更新!今天就先写这么多,如果有疑问或者有兴趣,可以加QQ:2649160693,并注明CSDN,我会就博文中有疑义的问题做出解答。同时希望博文中不正确的地方各位加以指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值