《java解惑》读书笔记7——库谜题

1.不可变类:

问题:

下面的程序计算5000+50000+500000值,代码如下:

import java.math.BigInteger;

public class Test{
    public static void main(String[] args){
        BigInteger fiveThousand = new BigInteger("5000");
        BigInteger fiftyThousand = new BigInteger("50000");
        BigInteger fiveHundredThousand = new BigInteger("500000");
        BigInteger total = BigInteger.ZERO;
        total.add(fiveThousand);
        total.add(fiftyThousand);
        total.add(fiveHundredThousand);
        System.out.println(total);
    }
}
对于这个简单的小程序,我们都认为毫无疑问结果应该是555000,但是程序真实运行结果是0.


原因:

BigInteger实例是不可变的,此外String、BigDecimal以及包装类型:Integer、Long、Short、Byte、Character、Boolean、Float和Double都是不可变类,即不能修改它们实例的值,对这些不可变类型的操作将返回新的实例。


结论:

为了在一个包含对不可变对象引用的变量上执行计算,我们需要将计算结果赋值给该变量,代码如下:

import java.math.BigInteger;

public class Test{
    public static void main(String[] args){
        BigInteger fiveThousand = new BigInteger("5000");
        BigInteger fiftyThousand = new BigInteger("50000");
        BigInteger fiveHundredThousand = new BigInteger("500000");
        BigInteger total = BigInteger.ZERO;
        total = total.add(fiveThousand);
        total = total.add(fiftyThousand);
        total = total.add(fiveHundredThousand);
        System.out.println(total);
    }
}
通过上述修改,程序就可以打印出我们期望的555000.

不可变类不存在同步修改的问题,因此在并发多线程情况下不需要对其加锁或同步。


2.equals和hashCode方法:

问题:

下面的程序将一个不可变类添加进集合容器中,然后测试集合中是否包含它,代码如下:

import java.util.HashSet;
import java.util.Set;

public class Test{
    private String firstName, lastName;
    public Test(String firstName, String lastName){
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    public boolean equals(Object o){
        if(!(o instanceof Test)){
            return false;
        }
        Test t = (Test)o;
        return t.firstName.equals(firstName) && t.lastName.equals(lastName);
    }
    
    public static void main(String[] args){
        Set<Test> s = new HashSet<Test>();
        s.add(new Test("Mickey", "Mouse"));
        System.out.println(s.contains(new Test("Mickey", "Mouse")));
    }
}
由于我们重写了Object类的equals比较方法,因此上述程序应该打印输出true,但是真实运行却打印输出false。


原因:

由于上述程序中使用了HashSet集合来存放对象,HashSet集合使用hash算法生成的hashCode来查找对象。

Hash算法约定:相同的对象(equals方法),hashCode一定相同;而hashCode相同,对象不一定相同(Hash碰撞)。

因此在使用基于Hash算法的集合时,覆盖equals方法的同时一定要重写hashCode方法,反之依然(当hash碰撞时,使用equals判定是否是相同对象)。

由于上述程序中Test类没有重写hashCode方法,则默认使用Object类中定义的hashCode方法,即使用对象的标识作为hashCode,当第一个对象被添加进集合中时,使用第一个对象的对象表示作为hashCode,再检查对象是否存在时,使用第二个对象的对象标识作为hashCode,由于这两个hashCode不想等,因此集合没有找到给定的对象,所以打印输出false。


结论:

修改上述程序问题很简单,只需要重写Test类的hashCode方法即可,代码如下:

import java.util.HashSet;
import java.util.Set;

public class Test{
    private String firstName, lastName;
    public Test(String firstName, String lastName){
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    @Override
    public int hashCode() {
        return 37 * firstName.hashCode() + lastName.hashCode();
    }

    @Override
    public boolean equals(Object o){
        if(!(o instanceof Test)){
            return false;
        }
        Test t = (Test)o;
        return t.firstName.equals(firstName) && t.lastName.equals(lastName);
    }
    
    public static void main(String[] args){
        Set<Test> s = new HashSet<Test>();
        s.add(new Test("Mickey", "Mouse"));
        System.out.println(s.contains(new Test("Mickey", "Mouse")));
    }
}

当重写类的equals方法时,一定要记得重写hashCode方法。


3.正确地重写equals方法:

问题:

和前一个谜题一样,上面的程序将对象添加到集合中,并且查询该集合是否包含它,代码如下:

import java.util.HashSet;
import java.util.Set;

public class Test{
    private String firstName, lastName;
    public Test(String firstName, String lastName){
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    @Override
    public int hashCode() {
        return 37 * firstName.hashCode() + lastName.hashCode();
    }

    public boolean equals(Test t){
        return t.firstName.equals(firstName) && t.lastName.equals(lastName);
    }
    
    public static void main(String[] args){
        Set<Test> s = new HashSet<Test>();
        s.add(new Test("Mickey", "Mouse"));
        System.out.println(s.contains(new Test("Mickey", "Mouse")));
    }
}
上述程序中既有equals方法,又有hashCode方法,因此我们觉得应该打印输出true,但是很不幸这次依然打印输出false。


原因:

仔细对比上述程序与第二个谜题中修改后的代码我们发现,hashCode方法没有问题,但是equals方法好像不太一样,Object中声明可以覆盖的equals方法的方法签名为:

public boolean equals(Object o)

但是上述程序中的equals方法的方法签名为:

public boolean equals(Test t)

由于输入参数不是Object类型,因此我们并没有重写Object的equals方法,而是重载了它。

Hash算法使用Object类中的equals(Object)方法来测试对象的相等性,重载的equals方法并没有被Hash算法所调用,因此虽然HashSet通过hashCode定位到了添加的对象,但是通过equals方法比较对象时却返回了false。


结论:

解决上述程序很简单,有两种方法:

方法一:

使用类谜题二中的equals方法,代码如下:

public boolean equals(Object o){
        if(!(o instanceof Test)){
            return false;
        }
        Test t = (Test)o;
        return t.firstName.equals(firstName) && t.lastName.equals(lastName);
    }
方法二:

在Object的equals方法中调用重载的equals方法,代码如下:

public boolean equals(Test t){
        return t.firstName.equals(firstName) && t.lastName.equals(lastName);
    }
    
    public boolean equals(Object o){
        return o instanceof Test && equals((Test)o);
    }
在覆盖父类方法时要特别小心,很有可能变成重载而引起错误,建议在覆盖的方法上面使用@Override注解。

4.取绝对值的注意事项:

问题:

下面的程序统计整个int类型数值对3取余之后余数分别为0,1,2的个数,代码如下:

public class Test{
    
    public static void main(String[] args){
        final int MODULUS = 3;
        int[] histogram = new int[MODULUS];
        int i = Integer.MIN_VALUE;
        do{
            histogram[Math.abs(i) % MODULUS]++;
        }while(i++ != Integer.MAX_VALUE);
        for(int j = 0; j < MODULUS; j++){
            System.out.println(histogram[j]);
        }
    }
}
整个2的32次方个int数值对3取余运算之后,分别统计余数为0,1和2的个数的总和应该为2的32次方,这三个数应该大致相等,由于2的32次方不能被3整除,因此肯定其中两个数相等,通过运算可以得知2的偶次幂对3取余值为1,2的奇次幂对3取余值为2,因此最大数2的32次方对3取余应该为1,所以程序的最终运行结果应该为1431655765,1431655766和1431655765.

很遗憾,真实程序一运行就报java.lang.ArrayIndexOutOfBoundsException异常。


原因:

从上述数字越界异常,我们知道肯定是数组的索引出了问题,我们知道负数对正数取余时,值肯定为负数,我们上面的程序对int数值取绝对值了,为什么还会出现负数。

问题恰恰出在Math.abs方法上,从Math.abs的文档了解到:该方法几乎总是返回它的参数的绝对值,但是在有一种情况下,它做不到这一点,如果其参数等于Integer.MIN_VALUE,那么产生的结果与该参数相同,它是一个负数。

由于计算机中数值运算采用二进制补码,int类型数据取值范围为负的2的32次方到正的2的32次方减一,0的相反数是其本身,由于负数比正数多一个,因此总有一个负数的相反数没有对应的正数,这个数就是Integer.MIN_VALUE,而它模3取余的值是-1,因此造成数组下标越界。


结论:

为了修改程序的错误,我们需要在取余操作时将结果为负的值转换为正数,代码如下:

public class Test{
    private static int mod(int i, int modulus){
        int result = i % modulus;
        return result < 0 ? result + modulus : result;
    }
    
    public static void main(String[] args){
        final int MODULUS = 3;
        int[] histogram = new int[MODULUS];
        int i = Integer.MIN_VALUE;
        do{
            histogram[mod(i, MODULUS)]++;
        }while(i++ != Integer.MAX_VALUE);
        for(int j = 0; j < MODULUS; j++){
            System.out.println(histogram[j]);
        }
    }
}

Math.abs方法不能保证一定会返回非负的结果,如果它的参数是Short,int,long等数据类型的MIN_VALUE时,它的结果就是其本身。


5.比较器的注意事项:

问题:

下面的程序对随机产生的100个伪随机数进行排序,然后打印出排序情况,代码如下:

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;


public class Test{
    
    public static void main(String[] args){
        Random rand = new Random();
        Integer[] array = new Integer[100];
        for(int i = 0; i < array.length; i++){
            array[i] = rand.nextInt();
        }
        Comparator<Integer> comparator = new Comparator<Integer>(){
            public int compare(Integer intOne, Integer intTwo){
                return intTwo - intOne;
            }
        };
        Arrays.sort(array, comparator);
        System.out.println(order(array));
    }
    
    enum Order{ ASCENDING, DESCENDING, CONSTANT, UNORDERED};
    
    static Order order(Integer[] arr){
        boolean ascending = false;
        boolean descending = false;
        for(int i = 1; i < arr.length; i++){
            ascending |= arr[i] > arr[i-1];
            descending |= arr[i] < arr[i-1];
        }
        if(ascending && !descending){
            return Order.ASCENDING;
        }
        if(!ascending && descending){
            return Order.DESCENDING;
        }
        if(!ascending){
            return Order.CONSTANT;
        }
        return Order.UNORDERED;
    }
}
上述程序中比较器的compare方法将传入的第二个参数减去第一个参数的值来确定排序方法返回值,如果为正值表示第二个参数比第一个参数大,如果为0表示两个参数相等,如果为负数表示第二个参数比第一个参数小。该比较器的行为正好与compare方法通常的做法想法,因此,该比较器应该实施的是降序排列。

在对数组排序之后,main方法将该数组传递给了静态方法order用于打印按何种方式进行排序,如果数组中相邻两个元素中后面的元素比前面大时,返回ASCENDING;如果数组中相邻两个元素中后面元素比前面的小,则返回DESCENDING;如果数组中相邻两个元素相等,则返回CONSTANT;否则就返回UNORDERED。

由于上述代码的比较器使用了降序排列,同时由于100个伪随机数相等的概率非常小,因此上述程序应该打印的是DESCENDING,但是程序真实运行结果总是UNORDERED。


原因:

上述程序之所以出现令人疑惑不解的UNORDERED结果,是因为比较器的compare方法写的有问题。

JDK中比较器Comparator所实现的排序关系必须是可传递的,即若(compare(x, y) > 0) && (compare(y, z) > 0),则必定compare(x, z) > 0.

而对于上述compare方法中两个数相减的比较结果,我们试想一下,如果第一个参数很大的正数(接近Integer.MAX_VALUE),第二个参数是个很小的负数(接近Integer.MIN_VALUE),正数减负数运算变成两个很大的正数相加,如计算结果超出Integer.MAX_VALUE,则数据溢出后变为负数。

对照上面的传递性约定,x取很大的正数,y取0,z取很大的负数,则会违反传递性。

因此使用上述比较器的compare方法,有很大可能性返回错误的值,用这样一个错误的排序方法来打印排序方式就得到了令人匪夷所思的UNORDERED。


结论:

解决上述程序问题方法很简单,只需要修改比较器即可,有如下两种方式:

方法一:

使用比较运算符代替相减,代码如下:

Comparator<Integer> comparator = new Comparator<Integer>(){
            public int compare(Integer intOne, Integer intTwo){
                return (intTwo < intOne ? -1 : (intTwo == intOne ? 0 : 1));
            }
        };
方法二:

使用Collection类提供的顺序比较器,代码如下:

Arrays.sort(array, Collections.reverseOrder());
千万不要使用基于减法的比较器,除非能够保证比较的数值间的差永远不会数值溢出。

猜你喜欢

转载自blog.csdn.net/chjttony/article/details/19826029