java 7 collection 详解(一)  详细讲解了Collection、Set、List的相关用法。

    java 7 collection 详解(二) 详细讲解了Queue、Map的相关用法。

    承接着前两篇collection讲解,这一次继续谈一下在collection里的对象排序。在谈对象排序排序之前,先来了解一下,在java里是如何判断两个对象是相等(同)的。

一、基础补充

    在根类Object里提供里两个方法hasCode()和equal()方法,如果两个对象相等,则hasCode的返回值应一致,equal()应该返回true。现在来看一下java API,了解一下这两个方法的定义:

    hasCode():返回该对象的哈希码值。支持此方法是为了提高哈希表的性能。

    hasCode的常规协定是:

  • 在 Java 应用程序执行期间,如果对象进行equals比较时没有被修改任何信息,那么同一对象多次调用 hashCode 方法,必须一致地返回相同的整数。同一程序的两次运行,该整数无需保持一致。
  • 如果根据 equals(Object) 方法判断出两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
  • 如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么对这两个对象中的任一对象上调用 hashCode 方法要求一定生成不同的整数结果。但是,应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

    equals(object obj):指示某个对象obj是否与此对象“相等”。

    equals方法需要遵循一下规则:

  • 自反性:对于任何非空引用值 xx.equals(x) 都应返回 true
  • 对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true
  • 传递性:对于任何非空引用值 xy 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true
  • 一致性:对于任何非空引用值 x 和 y,如果对象进行equals比较时没有被修改任何信息,多次调用 x.equals(y) 始终返回 true 或始终返回 false
  • 对于任何非空引用值 xx.equals(null) 都应返回 false

    简单来说就是,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。 另外,对于hasCode()和equals()方法,如果重载了其中一个,一般来说都需要重载另外一个,即两个方法都要重载。

    对于hasCode()和 equals()方法,初看之下,可能觉得有点难懂。其实,这两个方法的重写很简单,举个例子:重写Student类的hasCode和equals方法(假设Student类只有两个成员变量,班别和姓名),

public class Student {

	private String grade;
	private String name;
	
	public Student(String grade, String name) {
		super();
		this.grade = grade;
		this.name = name;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((grade == null) ? 0 : grade.hashCode());
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof Student))
			return false;
		Student other = (Student) obj;
		if (grade == null) {
			if (other.grade != null)
				return false;
		} else if (!grade.equals(other.grade))
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
	public static void main(String[] args) {
		Student stu1 = new Student("7班", "张三");
		Student stu2 = new Student("7班", "张三");
		Student stu3 = new Student("7班", "李四");
		System.out.println(stu1.equals(stu2));	//true
		System.out.println(stu1.hashCode() == stu2.hashCode());//true
		System.out.println(stu1.equals(stu3));	//false
		System.out.println(stu1 == stu2);		//false
	}
}

    通过Student来看,重写equals方法时,其实就是需要去判断两个对象所携带的信息是否一致。至于hasCode()方法,可能对prime = 31有点迷茫,为什么要用31呢??用别的数字行吗??为此我搜索过相关信息,笼统地说,31是一个魔法数,利于方便地为当前的对象分配内存地址(这个数字不大不小) 减少hash冲突 ,提高查询速度等。

   同样注意到是:System.out.println(stu1 == stu2);在这里引入这个判断输出,只是想指出:equals()方法判断的是对象所包含的的数据是否一致,而==判断是否为同一个对象的应用,详情可以查看这里

  温馨提示:对于eclipse和netbeans都提供了比较完善的关于equals和hasCode的重写方法,以eclipse为例,可以按快捷键ctrl+shift+S,接着选择重写equals()和hasCode()方法。

二、Comparable接口

    Comparable定义了类对象的排序方式,compareTo()方法定义了排序的逻辑,默认是按照类的自然排序逻辑来排序。如下表,总结了一些常用类的默认排序方式:

Class

默认的排序方式

Class

默认的排序方式

Byte

升序(按照数字由小到大)

Float

升序

Character

升序

BigInteger

升序

Long

升序

BigDecimal

升序

Integer

升序

Boolean

Boolean.FALSE < Boolean.TRUE

Short

升序

File

默认使用系统路径的排序方式

Double

升序

String

字典排序

Date

按照日期先后排序

   

   上表所有类都实现了Comparable接口,都可以使用Collections.sort()和Arrays.sort()进行排序,如下所示:

   Collections.sort(list) ;Arrays.sort(array);

    注意的是,所有待排序的对象都应该是可比较(大小)的,如果对象之间不能相互比较(大小),会抛出ClassCastException。同时排序的元素里不能有null,否则会抛出NullPointerException。给一个简答的例子—将string数组排序输出:

String[] fruits = new String[]{"orange","Apple","pair","watermelon"};
		Arrays.sort(fruits);
		for (String str : fruits) {
			System.out.print(str + " ");
		}


    其输出如下:Apple orange pair watermelon。

    如果需要实现自定义类的排序,则可以选择实现Comparable接口,Comparable接口的代码如下:

public interface Comparable<T> {
    public int compareTo(T o);
}

     在Comparable接口里只定义了compareTo(obj)方法,用来获得此对象与指定对象obj的顺序。如果该对象小于、等于或大于指定对象obj,则分别返回负整数、零或正整数。如果两个对象不能相互比较(大小),此方法会抛出ClassCastException。

    Demo:Student.java演示如何实现Comparable接口,这里按照Id的升序输出,

public class Student implements Comparable<Student> {
	
	private int Id;
	private int score;
	private String name;

	public Student(int id, int score, String name) {
		super();
		if (name == null) {
			throw new NullPointerException();
		}
		Id = id;
		this.score = score;
		this.name = name;
	}

	//按照Id值得大小,升序输出
	@Override
	public int compareTo(Student stu) {
		return Id > stu.Id ? 1 : (Id < stu.Id ? -1 : 0);
	}

	//为了输出的方便和排版,重写toString方法,只是输出Id的值
	@Override
	public String toString() {
		return String.valueOf(Id);
	}
	
	public static void main(String[] args) {
		Student[] stu = {new Student(102, 60, "张三"),
				new Student(101, 70, "李四"),
				new Student(103, 90, "王五")
		};
		List<Student> stuList = Arrays.asList(stu);
		//未排序前的对象顺序
		System.out.println("未排序前的对象顺序:" + stuList);
		
		//以自然逻辑排序的对象顺序,即Comparable定义的排序逻辑(Id值得大小)
		Collections.sort(stuList);
		System.out.println("自然逻辑排序的对象顺序:" + stuList);
	}
}

    其输出如下:image

    在Student.java里实现了Student对象按照Id值大小顺序输出对象,类似得可以定义按照成绩或者名字的自然逻辑顺序输出对象,如下:

    return score > stu.score ? 1:(score < stu.score ? -1:0);

    既然谈到排序,我们再深入一点谈实现Comparable接口需要遵守的规则。在java里,倡导所有实现Comparable接口的类都应该保持与equals()一致的排序顺序(也可以不保持)。什么意思呢??简单得说,对一个类里的任意两个对象obj1和obj2,obj.compareTo(obj2) == 0 与 obj1.equals(obj2)应该具有相同的boolean值。换一种说法就是,在实现Comparable接口的类里应该重写equals()方法(伴随着需要重写hasCode()方法),如上面的Student.java,我们这样重写equals()与hasCode()方法:

@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Id;
		result = prime * result + name.hashCode();
		result = prime * result + score;
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Student))
			return false;
		Student other = (Student) obj;
		return (Id == other.Id) && (score == other.score) && (name.equals(other.name));
	}

     这样Student.java的逻辑就算比较完善了,如果你曾经重写过equals和hasCode方法,会发现这里的重写方法里没有考虑到null异常(因为在构造函数里,确保里Student的对象 不会包含null,所有不需要考虑null异常)。温馨提示一下,对于eclipse和netbeans都提供了比较完善的关于equals和hasCode的重写方法,以eclipse为例,可以按快捷键ctrl+shift+S,接着选择重写equals()和hasCode()方法。

三、Comparator接口

    对于没有实现Comparable接口类,如果需要排序输出对象,同样可以选择提供Comparator的实现,Comparator接口代码如下:

public interface Comparator<T> {
    int compare(T o1, T o2);
}

    在Comparator接口也只是定义里一个compare方法,用来定义对象的排序逻辑,如果对象o1小于、等于或大于对象o2,则分别返回负整数、零或正整数。如果两个对象不能相互比较(大小),此方法会抛出ClassCastException。

   Demo:Student2.java,演示了如何使用Comparator定义输出对象的顺序:

public class Student2 {
	private int Id;
	private int score;
	private String name;

	public Student2(int id, int score, String name) {
		super();
		if (name == null) {
			throw new NullPointerException();
		}
		this.Id = id;
		this.score = score;
		this.name = name;
	}

	//为了输出的方便和排版,重写toString方法,只是输出Id的值
	@Override
	public String toString() {
		return String.valueOf(Id);
	}

	public static class OrderById implements Comparator<Student2>{

		@Override
		public int compare(Student2 stu1, Student2 stu2) {
			return stu1.Id > stu2.Id ? 1:(stu1.Id < stu2.Id ? -1:0);
		}
		
	}
	
	public static void main(String[] args) {
		Student2[] stu = {new Student2(102, 60, "张三"),
				new Student2(101, 70, "李四"),
				new Student2(103, 90, "王五")
		};
		List<Student2> stuList = Arrays.asList(stu);
		//未排序前的对象顺序
		System.out.println("未排序前的对象顺序:" + stuList);
		
		//以Comparator定义的排序输出对象(Id值得大小)
		Collections.sort(stuList,new Student2.OrderById());
		System.out.println("自然逻辑排序的对象顺序:" + stuList);
	}
}

    其输出如下:image

    相对Comparable的实现,Comparator是在类里通过一个静态类来定义输出对象的排序逻辑(按照Id的大小,升序输出;类似的可以定义score和name的排序逻辑),还有一点不同的是sort()函数的使用:

    Collections.sort(stuList,new Student2.OrderById());

    其他的实现基本一致,那是否可以认为Comparator与Comparable是等同的呢??如果只是定义一种排序逻辑,选择Comparator和Comparable都可以,习惯上选择Comparable。可需要多种排序逻辑输出对象的时候,就需要Comparable和Comparator的配合使用。为了更为详细的演示排序,这次同样实现对象的逆序输出,而且以内部类的形式实现Comparator:

Demo:Student.java

public class Student implements Comparable<Student> {

	private int Id;
	private int score;
	private String name;

	public Student(int id, int score, String name) {
		super();
		if (name == null) {
			throw new NullPointerException();
		}
		Id = id;
		this.score = score;
		this.name = name;
	}

	// 为了输出的方便和排版,重写toString方法,只是输出Id的值
	@Override
	public String toString() {
		return String.valueOf(Id);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Id;
		result = prime * result + name.hashCode();
		result = prime * result + score;
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Student))
			return false;
		Student other = (Student) obj;
		return (Id == other.Id) && (score == other.score) && (name.equals(other.name));
	}

	// 按照Id值得大小,升序输出
	@Override
	public int compareTo(Student stu) {
		return Id > stu.Id ? 1 : (Id < stu.Id ? -1 : 0);
		// return score > stu.score ? 1:(score < stu.score ? -1:0);
	}

	// 按照分数的高低排序
	static final Comparator<Student> ORDERBYSCORE_ORDER = new Comparator<Student>() {

		@Override
		public int compare(Student stu1, Student stu2) {
			return stu1.score > stu2.score ? 1 : (stu1.score < stu2.score ? -1 : 0);
		}
	};

	// 按姓名的字典顺序排序
	static final Comparator<Student> ORDERBYNAME_ORDER = new Comparator<Student>() {

		@Override
		public int compare(Student stu1, Student stu2) {
			return stu1.name.compareTo(stu2.name);
		}
	};

	public static void main(String[] args) {
		Student[] stu = { new Student(102, 60, "张三"), 
				new Student(101, 70, "李四"),
				new Student(103, 90, "王五") };
		
		List<Student> stuList = Arrays.asList(stu);
		// 未排序前的对象顺序
		System.out.println("未排序前的对象顺序:" + stuList);

		// 以自然逻辑排序的对象顺序,Comparable定义的逻辑
		Collections.sort(stuList);
		System.out.println("自然逻辑排序的对象顺序:" + stuList);

		// 降序排序输出对象
		Collections.sort(stuList, Collections.reverseOrder());
		System.out.println("降序排序输出对象:" + stuList);

		// 使用Comparator顺序输出对象,OederByScore
		Collections.sort(stuList, ORDERBYSCORE_ORDER);
		System.out.println("OederByScore顺序输出对象" + stuList);

		// 使用另一个Comparator顺序输出对象,OederByName
		Collections.sort(stuList, ORDERBYNAME_ORDER);
		System.out.println("OederByName顺序输出对象" + stuList);
	}
}


    其输出如下:

未排序前的对象顺序:[102, 101, 103] 
自然逻辑排序的对象顺序:[101, 102, 103] 
降序排序输出对象:[103, 102, 101] 
OederByScore顺序输出对象[102, 101, 103] 
OederByName顺序输出对象[102, 101, 103]

    如上代码,可以实现了多种排序逻辑的对象输出,同时注意到逆序输出的方式:

    Collections.sort(stuList, Collections.reverseOrder());

    最后提一下的是,在Demo Student里,都是使用Collections.sort()方法来输出对象的,同样地也可以使用Arrays.sort()方法来输出对象,类似的Arrays.sort()也有两种重载形式:

    Arrays.sort(array);

    Arrays.sort(array,Comparator);

    其用法和Student提到的方法一致。