android日记(三)
上一篇:android日记(二)
1.alibaba Java开发规范
- 简洁、有美感,是一个有情怀的程序员应该写出的代码。
- 阿里出品,必属精品。github地址,定期更新,免费下载https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E6%B3%B0%E5%B1%B1%E7%89%88%EF%BC%89.pdf
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位),从-2147483648到2147483648,前后共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%2m =(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...x1x0 = h%n。
- 综合两种情况,都满足h&(n-1) = h%n。所以说,在n是2的幂时,h&(n-1)就是h对n的取模运算。
下一篇:android日记(四)