包装类的注意点 前言 :
今天在刷leetcode76 时候遇到一个奇怪的bug,发现是一个常见的笔试考题
补充 :
2021.05.03 :原理其实就是一个享元模式的应用,关于享元模式在并发编程笔记 中有详细解释
1. bug 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class Solution { public String minWindow (String s, String t) { Map<Character, Integer> window = new HashMap<>(); Map<Character, Integer> need = new HashMap<>(); for (int i = 0 ; i < t.length(); i++) need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0 ) + 1 ); int left = 0 , right = 0 ; int valid = 0 ; int start = 0 , length = Integer.MAX_VALUE; while (right < s.length()){ char c = s.charAt(right); if (need.containsKey(c)){ window.put(c, window.getOrDefault(c, 0 ) + 1 ); if (need.get(c) == window.get(c)){ valid++; } } right++; while (valid == need.size()){ if (right - left < length){ start = left; length = right - left; } c = s.charAt(left); if (need.containsKey(c)){ if (window.get(c) == need.get(c)){ valid--; } window.put(c, window.get(c) - 1 ); } left++; } } return length == Integer.MAX_VALUE ? "" : s.substring(start, start + length); } }
本题是滑动窗口的典型题目,但是我发现,valid
在字符串较大的实例总是不能自增。
也就是在14行和30行的window.get(c) == need.get(c)
判断始终不成立
2. Integer源码分析 问题出在Integer包装类上,因为我们在hashmap中只能存放包装类,会把我们put的int类型自动装箱
我们可以看一下包装类的代码。
自动装箱调用valueOf方法
1 2 3 4 5 public static Integer valueOf (int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
IntegerCache 的源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private static class IntegerCache { static final int low = -128 ; static final int high; static final Integer cache[]; static { int h = 127 ; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high" ); if (integerCacheHighPropValue != null ) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127 ); h = Math.min(i, Integer .MAX_VALUE - (-low) -1 ); } catch ( NumberFormatException nfe) { } } high = h; cache = new Integer [(high - low) + 1 ]; int j = low; for (int k = 0 ; k < cache.length; k++) cache[k] = new Integer (j++); assert IntegerCache.high >= 127 ; } private IntegerCache() {} }
观察源码,我们可以发现,在[IntegerCache.low, IntegerCache.high]之间的int类型在装箱后放在IntegerCache.cache数组里面的,所以在此范围之间的Integer的引用是相同的,所以在[-127, 128]范围==
的判断能够成立,而超过之后==
判断失效。
补充:Double 和 Float 没有缓存机制,都是直接返回新的对象;Integer、Short、Byte、Character 有缓存机制
3. debug代码 那么我们只要拆箱比较就好了,Integer转int使用Integer类的intValue方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Solution { public String minWindow (String s, String t) { Map<Character, Integer> window = new HashMap<>(); Map<Character, Integer> need = new HashMap<>(); for (int i = 0 ; i < t.length(); i++) need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0 ) + 1 ); int left = 0 , right = 0 ; int valid = 0 ; int start = 0 , length = Integer.MAX_VALUE; while (right < s.length()){ char c = s.charAt(right); if (need.containsKey(c)){ window.put(c, window.getOrDefault(c, 0 ) + 1 ); if (need.get(c).intValue() == window.get(c).intValue()){ valid++; } } right++; while (valid == need.size()){ if (right - left < length){ start = left; length = right - left; } c = s.charAt(left); if (need.containsKey(c)){ if (window.get(c).intValue() == need.get(c).intValue()){ valid--; } window.put(c, window.get(c) - 1 ); } left++; } } return length == Integer.MAX_VALUE ? "" : s.substring(start, start + length); } }
4. 相关经典笔试题 这里参考了帖子
第一题 1 2 3 4 5 6 7 8 9 10 11 12 13 Integer a = 10 ; Integer b = 20 ; Integer c = 30 ; Integer d = 30 ; Integer e = 300 ; Integer f = 300 ; Integer g = a + b; c == d; e == f; c == g; c == (a + b); c.equals(a + b);
结果:true、false、true、true、true
第三个 :由于运用了算术运算符(+),因此右边的算式在进行计算之前会先调用 intVal() 方法进行拆箱,在进行相加,然后得到的结果30之后,由于前面是 Integer g,因此还会再调用 valueOf() 方法进行装箱。由于此时 cache() 数组中已经有了 30 了,因此直接指向缓存池中的 30 这个 Integer 对象。此时 c == g 比较的还是对象的地址是否相同
第四个 :由于右边是 a+b,包含算术运算,因此会调用 intVal() 方法,将左边的 c 进行拆箱,之后又分别对 a 和 b 进行拆箱,即一共调用了三次拆箱过程,最后比较的是数值大小,而不是地址
第五个 :通过 equals 的源码看到,传入的参数需要是一个对象,这就意味着,和 equals 比较的时候,一定是装完箱之后的结果a + b 的时候,a 和 b 各自执行 intVal() 方法进行拆箱,完成相加的计算之后,再对计算出的结果执行 valueOf() 方法进行装箱,此时再传入 equals 方法进行比较。equals 比较两个对象的时候,直接比较两个值拆箱之后的结果,即比较的是数值,本例中两个数值相同,所以输出为 true
1 2 3 4 5 6 public boolean equals (Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false ; }
第二题 1 2 3 4 5 6 7 8 Integer a = 10 ; Integer b = 20 ; Long g = 30L ; Long h = 20L ; g == (a + b); g.equals(a + b); g.equals(a + h);
结果:true、false、true
和上面一样,同样是使用拆箱,第一题比较的拆完箱之后的值,但是需要注意的是,Integer 类型调用 Integer.valueOf 进行拆箱,而 Long 类型调用 Long.valueOf 进行拆箱
最后一个结果是 true,因为 a + h 会使小的精度转为大的精度,最终的 30 是 Long 类型的,因此结果是 true
第三题 1 2 3 4 5 6 7 Double i1 = 100.0 ; Double i2 = 100.0 ; Double i3 = 200.0 ; Double i4 = 200.0 ; i1 == i2 i3 == i4
结果:false、false
Double没有缓存机制
第四题 1 2 3 4 5 6 7 8 Boolean b1 = false ; Boolean b2 = false ; Boolean b3 = true ; Boolean b4 = true ; b1 == b2 b3 == b4 b1 == b3
结果:true、true、false
为什么会这样呢?看一下 Boolean 的 valueOf() 的源代码
1 2 3 public static Boolean valueOf (boolean b) { return (b ? TRUE : FALSE); }
其中的 TRUE 和 FALSE,代表两个静态成员属性
1 2 3 public static final Boolean TRUE = new Boolean(true );public static final Boolean FALSE = new Boolean(false );
因此可以知道了,每次传入的 true 或 false,都是指向同一个 Boolean 对象,因此他们的引用肯定相同了
参考资料