android日记(三)

上一篇:android日记(二)

 1.alibaba Java开发规范

2.善用Objects工具类

  • JDK#java.util.Objects类中包含了一些static工具方法,主要用于对Object对象的一些常用处理。Objects没有public构造方法,也不允许反射创建实例,否则报“No java.util.Objects instance for you”。
  • Objects类中的方法,相比Object类,在安全性上升级。以下分别是Object和Objects类中的equals()方法,可以看出,Objects.equasl(a,b)相比a.equals(b),就不用担心a的空指针问题。同样安全性升级还包括Objects.hashCode()、Objects.toString()等方法。
    Object:
    public
    boolean equals(Object obj) { return (this == obj); }
    Objects:
    public
    static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); }
  • Objects类中的方法,相比Object类,在功能上扩展。Objects.deepEquals()深度比较方法,可以根据内容比较两个数组对象。
    public static boolean deepEquals(Object a, Object b) {
            if (a == b)
                return true;
            else if (a == null || b == null)
                return false;
            else
                return Arrays.deepEquals0(a, b);
        }
     static boolean deepEquals0(Object e1, Object e2) {
            assert e1 != null;
            boolean eq;
            if (e1 instanceof Object[] && e2 instanceof Object[])
                eq = deepEquals ((Object[]) e1, (Object[]) e2);
            else if (e1 instanceof byte[] && e2 instanceof byte[])
                eq = equals((byte[]) e1, (byte[]) e2);
            else if (e1 instanceof short[] && e2 instanceof short[])
                eq = equals((short[]) e1, (short[]) e2);
            else if (e1 instanceof int[] && e2 instanceof int[])
                eq = equals((int[]) e1, (int[]) e2);
            else if (e1 instanceof long[] && e2 instanceof long[])
                eq = equals((long[]) e1, (long[]) e2);
            else if (e1 instanceof char[] && e2 instanceof char[])
                eq = equals((char[]) e1, (char[]) e2);
            else if (e1 instanceof float[] && e2 instanceof float[])
                eq = equals((float[]) e1, (float[]) e2);
            else if (e1 instanceof double[] && e2 instanceof double[])
                eq = equals((double[]) e1, (double[]) e2);
            else if (e1 instanceof boolean[] && e2 instanceof boolean[])
                eq = equals((boolean[]) e1, (boolean[]) e2);
            else
                eq = e1.equals(e2);
            return eq;
        }
  • JDK有个习惯,用复数类表示对应类的工具,与Objects类似的还有Arrays、Collections等。

3.Java的浅拷贝与深拷贝

  • 不同于赋值(复制了一个引用变量,与原引用指向同一对象),对象拷贝的结果是生成一个新对象,与原对象是两个不同的对象。因此,a'==a 的结果是false。
  • 浅拷贝:拷贝出的新对象,其内部的对象成员,与原对象中的内部成员,具有相同引用(指向同一内存地址)。
  • 深拷贝:拷贝出的新对象,其内部的对象成员,也是新生成的,与原对象中的内部成员,不是同一引用。只不过拷贝完,会给新对象的成员赋上与原对象成员一样的值。
  • Object.clone()方法用于实现拷贝,但是clone方法是protected访问限制的,因此使用时,需要重写clone()方法,并修改访问权限为public。尤其要注意,需要用到clone的类一定要实现Clonable接口。否则,将throw CloneNotSupportException。
    protected Object clone() throws CloneNotSupportedException {
            if (!(this instanceof Cloneable)) {
                throw new CloneNotSupportedException("Class " + getClass().getName() +
                                                     " doesn't implement Cloneable");
            }
    
            return internalClone();
        }
  • Object.clone()实现的是浅拷贝,下面的方法给予了验证。在TestModel中有成员thing为SomeThing对象。在clone()完成后,原对象thing成员的变更,会使拷贝对象发生联动变更。
    public class TestModel implements Cloneable {
        private String name;
        private Integer number;
        private Something thing;
    
        @NonNull
        @Override
        public TestModel clone() throws CloneNotSupportedException {
            return (TestModel) super.clone();//浅拷贝
        }
      ...
    }
    //验证demo:
    private
    void testClone() { Something something = new Something(); something.setThing("shallow copy"); String name = "clone object"; TestModel model = new TestModel(); model.setName(name); model.setNumber(123456); model.setThing(something); try { TestModel cloneModel = model.clone(); boolean b = cloneModel == model; boolean b1 = cloneModel.getName().equals(model.getName()); model.getThing().setThing("after clone");//modify original object will interact cloned object String st1 = model.getThing().getThing();//st1: after clone String st2 = cloneModel.getThing().getThing();//st2: after clone } catch (CloneNotSupportedException e) { e.printStackTrace(); } }
  • 如何实现深拷贝?自己在重写的clone()方法中new新对象,然后对新对象遍历式拷贝赋值。
    public class TestModel implements Cloneable {
        private String name;
        private Integer number;
        private Something thing;
    
        @NonNull
        @Override
        public TestModel clone() throws CloneNotSupportedException {
    //实现深拷贝 TestModel copy
    = new TestModel(); copy.setNumber(this.number); copy.setName(this.name); Something st = new Something(); st.setThing(this.thing.getThing()); copy.setThing(st); return copy; } ... }

4.equals()和==有什么区别

  • equals()方法与“==”有什么区别?比较两个非基本类型的数据,“==”比较引用是否相等,equals()默认情况下也是比较引用。不信的话,看Object.equals()的实现。
    public boolean equals(Object obj) {
            return (this == obj);
        }
  • 那为什么江湖人常说“==”内存地址,equals()是比较内容呢?“==”比较引用(也就是,引用所指向的对象内存地址)是没问题的。但是说equals()是比较内容就不科学了。默认情况下,equals()效果与“==”一样,都是比较引用。那造成equals()比较内容的这种固有映像的原因在哪呢?
  • 原因是有一些类重写了Object.equals()方法,拿String来说,重写后,equals()体内进行逐一比较字符串的字符,也就是再比较字符串的内容。这样干的还有Integer、Double、Long、Boolean等类。
    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = length();
                if (n == anotherString.length()) {
                    int i = 0;
                    while (n-- != 0) {
                        if (charAt(i) != anotherString.charAt(i))
                                return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }

5.为什么重写equals()就一定需要重写hashCode()

  • JAVA API文档关于hashCode有以下几点规定:
    • 在应用某次执行期间,如果equals()方法比较的信息的不变,那么同一对象多次调用hashCode必须得到相同的结果。多次执行应用,不要求结果相同。
    • 如果两个对象通过equals()方法比较是相等的,那么他们的hashCode也一定要相同。
    • 如果两个对象通过equals()方法比较是不相等的,不要求他们的hashCode必须不同。但是程序员应该意识到,不同的对象具有不同的hashCode有助于哈希表的性能。
  • hashCode()是一个native方法,返回的根据对象的内存地址计算出来的一个int值。
  • 一切还要从键值对集合(HashMap,HashSet、HashTable)的存储原理说起,拿HashMap的put方法来看下,put一个对象进table时,是拿(n-1)&hash做的key,而hash实际就是(Key,Value)键值中键的hashCode()。
    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)
                //数组为空,则进行初始化
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)
                //如果tab[i]不存在,就在i位置存储这个对象,其中i=(n-1)&h。
                tab[i] = newNode(hash, key, value, null);
            else {
                //更新oldValue
                ...
                return oldValue;
            }
            return null;
        }
    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
  • 从而,如果键对象Key被重写过equals()方法,改变了相等判断规则,使得两个不同的对象变得相等。然而,当这个对象做为Key键用于HashMap时,就会出现equals()判断相等的两个对象,其hashCode()结果却不等,造成两次put相同的Key时,结果增加了一条数据,而不是更新这个Key对应的Value。看下面这个例子,就是Key对象的equals()方法得到了重写,却忽视了hashCode()方法。
    private void testHashMap() {
            CustomKey customKey1 = new CustomKey(1, "是个写代码的");
            CustomKey customKey2 = new CustomKey(1, "是个写代码的");
            Map<CustomKey, String> map = new HashMap<>();
            String result1 = map.put(customKey1, "value1");
            String result2 = map.put(customKey2, "value2");//同一个key,添加了两条数据,而不是更新
        }
    
        private class CustomKey {
            private int number;
            private String name;
    
            CustomKey(int number, String name) {
                this.number = number;
                this.name = name;
            }
    
            @Override
            public boolean equals(@Nullable Object obj) {
                if (this == obj) {
                    return true;
                }
                if (obj instanceof CustomKey) {
                    CustomKey objKey = (CustomKey) obj;
                    return objKey.number == this.number && objKey.name.equals(this.name);
                }
                return false;
            }
        }

  • 比如String重写equals()后,根据字符串的内容判断对象是否相等,这时候,相应的hashCode()也重写为以内容组装的结果。
    private void testHashMap() {
    String key1 = new String("key");
    String key2 = new String("key"); Map
    <String, String> map = new HashMap<>(); String result1 = map.put(key1, "value1"); String result2 = map.put(key2, "value2");//将key对应的值由value1改成value2 }
    //String重写了equals()方法
    public
    boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = length(); if (n == anotherString.length()) { int i = 0; while (n-- != 0) { if (charAt(i) != anotherString.charAt(i)) return false; i++; } return true; } } return false; }
    //String重写了equals()方法
    public int hashCode() {
    int h = hash;
    final int len = length();
    if (h == 0 && len > 0) {
                for (int i = 0; i < len; i++) {
                    h = 31 * h + charAt(i);
                }
                hash = h;
            }
            return h;
        }
  • 于是乎,可以参考String类的重写方式,对CustomKey类也重写以下hashCode()方法,使得两个对象equasl()相等时,他们的hashCode()值也相等。
    @Override
            public int hashCode() {
                return Integer.valueOf(this.number).hashCode() + this.name.hashCode();//与String类似,Integer类也重写了hashCode()
            }

  • 总结:如果只重写equals()方法,使得两个不同的对象相等时,而不重写hashCode()使他们的hashCode也相等,那么在散列表集合中,他们计算key值将不同,将无法正常存取数据了。

6.Byte能表示的整数范围到底是[-127,127]还是[-128,127]

  • Byte是8位二进制,最高位用于符号位,正数符号位为0,负数符号数为1。
  • 负数的二进制表示:计算机以补码进行存储和运算,正数的补码=反码=原码,负数的补码等于原码取反后加1。
    +4(Byte)
    原码:0000 0100
    反码:0000 0100
    补码:0000 0100
    
    -5(Byte)
    原码:1000 0101
    反码:1111 1010
    补码:1111 1011
    
    计算4-5,实际是计算4+(-5):
       0000 0100
     +
       1111 1011
    —————————
     =1111 1111  (补) = 1111 1110(反) = 1000 0001(原)= -1(真值)
    
    备注:
    1.计算反码时,符号位不参与取反;
    2.计算补码时,符号位也不参与计算(其实除了-128,[-127,0]的反码最高位之后总会出现0,+1运算时,都不会发生最高位的进位);
    3.加减乘除运算时,符号位也参与计算,计算结果是补码,然后减1取反得到原码。
  • 那是不是就表示,byte最小位1111 1111,最大位01111 111,即[-127,127]呢?其实也差不多,主要是还可以表示-128,也就是一个byte能表示的整数范围是[-128,127]。
  • 首先搞懂为什么要有补码,因为计算器不像人脑,能够很好的把最高位符号位与真值位区分开来,机器更希望把符号位也带入运算。然后人们探索后发现,如果把负数取反加1后,就可以让符合位也参与到计算,得到正确的计算结果。
  • 这时候,1-1这样的运算结果永远是0000 0000,而不会出现1000 0000(所谓的负0)。而恰好-127-1的计算结果是1000 0000,就定义用1000 0000来表示-128。
    -1(Byte)
    原码:1000 0001
    反码:1111 1110
    补码:1111 1111
    
    计算1-1,实际是计算1+(-1):
       0000 0001
     +
       1111 1111
       —————————
     =0000 0000  (补) = 1111 1111(反) = 0000 0000(原)= 0(真值)
    
    计算-127-1,实际是计算-127+(-1):
      1000 0001
    + 1111 1111 
      ————————— 
    = 1000 0000 (补) = 0111 1111(反) = 1000 0000(原)= -128(真值)
  • 总结:本来1000 0000谁也表示不了,相当于浪费了一种表示。那用它来表达谁合适呢,恰好-127-1等于1000 0000,那就用它来表示-128吧。

7.Byte无符号右移>>>的坑

  • >>与<<分别是带符号的位右移和左移运算符,>>>是无符号的右移运算,不存在<<<运算。
  • 至于>>与>>>的区别在于,>>右移的时候符号位跟着一起移动,高位补符号位,即正数补0,负数补1;>>>右移的时候,是不带符号位一起移动的,不论正数负数,高位都是补0。
  • 正数没啥好说的,主要看负数。看下面的表达式,明明定义的byte是个负数,但实际结果怎么就成了正数呢?原因在于超出[-128,127]范围后,会向上转型为int。
    byte b = (byte) -129;

    实际结果=127;下面给出计算过程:
    int型-129,
    原码:1000 0000 0000 0000 0000 0000 1000 0001
    反码:1111 1111 1111 1111 1111 1111 0111 1110
    补码:1111 1111 1111 1111 1111 1111 0111 1111
    截断为byte:0111 1111
    转为原码:0111 1111
  • 再来看负数的右移动,带符号的右移动也没什么好说的,主要是无符号右移动>>>,对于byte b = -5,无符号右移时高位补0,那你就以为会移成一个正数吗?下面的结果显示-5>>>2=-2。原因同样是发生了int强转,丢失了精度。
    Byte -5:
    原码:1000 0101
    反码:1111 1010
    补码:1111 1011
    
    >>2 带符号右移两位,高位补1,
    结果:1111 1110
    转为原码:1000 0010 = -2
    >>>2 无符号右移两位,高位补0,
    过程:1 0011 1110 超出[-128,127]的范围,会向上转型为int,即-5的补码为,
    byte强转为int:1111 1111 1111 1111 1111 1111 1111 1011
    再右移2位:0011 1111 1111 1111 1111 1111 1111 1110
    int强转回byte: 1111 1110 转为原码:1000 0101 = -2

8.使用位运算设置View状态

  • 位运算,就是直接对整数在内存中的二进制位进行操作。 因此,位运算最直接的好处就是节省内存空间,提高运算效率。
  • 假设现在有一自定义CustomView,其显示内容或者布局样式根据外部4种状态设定。
    CustomView <-- {FLAH_A、FLAG_B、FLAG_C、FLAG_D}
  • 使用状态变量实现,通常需要定义4种状态对应的boolean值,然后做相应条件下的ui处理。
    class CustomView {
        public boolean FLAG_A = false;
        public boolean FLAG_B = false;
        public boolean FLAG_C = false;
        public boolean FLAG_D = false;
    
        public CustomView(boolean a, boolean b, boolean c, boolean d) {
            this.FLAG_A = a;
            this.FLAG_B = b;
            this.FLAG_C = c;
            this.FLAG_D = d;
            initView();
        }
    
        private void initView() {
            if (FLAG_A) {
                //view handle state for FLAG_A
            }
            if (FLAG_B) {
                //view handle state for FLAG_B
            }
            if (FLAG_C) {
                //view handle state for FLAG_C
            }
            if (FLAG_D) {
                //view handle state for FLAG_D
            }
        }
  • 使用位运算,使用&位运算的结果来判断各种状态。CustomView构造时传入一个int状态值,需要添加的状态就|位运算添加状态。例如,需要添加A、C两种状态,int status = FLAG_A | FLAG_C;
    class CustomView {
        public final int FLAG_A = 1;
        public final int FLAG_B = 1 << 1;
        public final int FLAG_C = 1 << 2;
        public final int FLAG_D = 1 << 3;
        
        public CustomView(int status) {
            initView(status);
        }
    
        private void initView(int status) {
            if ((status & FLAG_A) == FLAG_A) {
                //view handle state for FLAG_A
            }
            if ((status & FLAG_B) == FLAG_B) {
                //view handle state for FLAG_B
            }
            if ((status & FLAG_C) == FLAG_C) {
                //view handle state for FLAG_C
            }
            if ((status & FLAG_D) == FLAG_D) {
                //view handle state for FLAG_D
            }
        }
    }
  • 利用位操作实现状态增删检查。
    private int mState = 0;
    
        //添加状态
        private void addState(int flag) {
            mState |= flag;
        }
    
        //移除状态
        private void removeState(int flag) {
            mState &= ~flag;
        }
    
        //检查状态
        private boolean checkState(int flag) {
            return flag == (mState & flag);
        }
  • 如果你觉得位运算不易阅读,也可以借助java.util.EnumSet实现状态管理,其内部已经帮你完成了位运算,但是枚举带来的内存开销也是不得不考虑的问题。
    private EnumSet<Flags> enumSet = EnumSet.noneOf(Flags.class);
    
        private enum Flags {
            FLAG_A, FLAG_B, FLAG_C, FLAG_D
        }
    
        private void addState(Flags flag) {
            enumSet.add(flag);
        }
    
        private void removeState(Flags flag) {
            enumSet.remove(flag);
        }
    
        private boolean checkState(Flags flag) {
            return enumSet.contains(flag);
        }

9.HashMap中的hash扰动函数

  • HashMap本质是一个链表数组,以键值对Key-Value方式进行存取。计算数组index时,是以Key对象的hashCode()作为基础,然后进行一些扰动计算后得到。
    //JDK1.8中hashMap#put()源码
    public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); ... } static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
  • 首先,tab[i=(n-1)&hash]这句显示,index = (n-1) & h,这句话表示h对n的取模,也就是计算余数,关于这个结论的推导过程在看本文下节中有说明。这里n是当前hashMap数组的长度,那为什么要对n取模呢?原因在于,hashCode()本身是个int值(32位),从-21474836482147483648,前后共40亿的空间,然而内存是放不下一个长度为40亿的数组的,所以是不可能直接用hashCode()值用作index的。而HashMap的初始长度是16,而后随着存储数据的增加到扩容阈值时,进行2倍扩容。因此,用对n的取模结果作为index是非常有必要的。
    对于长度为16的hashMap,现有一个hash值的低16位掩码为
     hash:    10100101 11100101 
            &
      n-1:    0000000   00001111
             ---------------------
    index: 0000000 00000101 //高位全部清零,只保留末4位 index = 5
  • 其次,h = hash(key),hash(key)方法不是直接返回key的hashCode()就完事了,而是当key不为空时,返回(h = key.hashCode()) ^ (h >>> 16),就是key对象的hashCode()值与它右移16位后异或的结果,这个操作过程就叫做对hashCode的扰动。
  • 要弄懂扰动函数的意义,还要从扰动的计算过程入手。hash = h^(h>>>16)相当于让h的高位和低位进行了异或,成为hash的低位,hash的高位与原h的高位一样。
    h=hashCode(): 1100 1001 0011 1010 1110 0110 0111 0101
        __________________________________________________
               h: 1100 1001 0011 1010 1110 0110 0111 0101
          h>>>16: 0000 0000 0000 0000 1100 1001 0011 1010 
      ----------------------------------------------------
      h^(h>>>16): 1100 1001 0011 1010 0010 1111 0100 1111  //高半区不变,低半区混合了原始哈希码的高位和低位。

    这样的好处在于,在随后计算index时,要对hash取模,由于hashMap的长度一般不会超过16位(2^16),从而大概率会丢弃hash的高位,只留下了hash低位。而扰动后的hash低位部分,混合了原始哈希码的高位和低位信息。

  • 试想一下,如果不进行扰动,两个原本不同的hash值,他们高位不相同,但恰好取模留下来的低位部分相同,那么他们将得到相同的index,从而发生碰撞。而当扰动后,低位部分也保留了高位信息,再取模后,相等的概率就大大降低,这对减少hash碰撞是非常有意义的。

10.证明h = (n-1)&h是h对n的取模运算

  • 当n=2k时,证明h%n = h&(n-1)。
  • 设h的二进制表示式为h=xmxm-1xm-2...x1x0,则h=xm*2m+xm-1*2m-1+xm-2*2m-2+...+x1*21+x0*20。当h表示hashCode为int型时,m=30;
  • 从而h%n = h%2=(xm*2m+xm-1*2m-1+xm-2*2m-2+...+x1*21+x0*20)% 2k
  • 由于n=2k,则n-1的二进制表达是000 0111,末尾有k个1,前面全是0,记做(000...111)k
  • 当k>m时,h的每一项都比2k小,从而h%n = h,结果是h本身。
  • 当k>m时,h&(n-1) = (xmxm-1xm-2...x1x0) & (000...111)k  = (xmxm-1xm-2...x1x0)  = h,结果也是h本身。
  • 因此,当k>m时,h&(n-1) = h = h%n。
  • 当k<=m时,余数h%2k = xk*2k+xk-1*2k-1+xk-2*2k-2+...+x1*21+x0*20。余数长度为k,记做xkxk-1xk-2...x1x0
  • 当k<=m时,h&(n-1) = (xmxm-1xm-2...xkxk-1xk-2...x1x0) & (000...111)k = xkxk-1xk-2...x1x0
  • 因此,当k<=m时,h&(n-1) = xkxk-1xk-2...x1x= h%n。
  • 综合两种情况,都满足h&(n-1) = h%n。所以说,在n是2的幂时,h&(n-1)就是h对n的取模运算。

下一篇:android日记(四)

posted @ 2020-06-14 13:35  是个写代码的  阅读(297)  评论(0编辑  收藏  举报