今天看到一道题目,感觉挺简单的,顺便看下作者的答案,如下。
去除重复字符并排序
运行时间限制:无限制
内容限制: 无限制
输入: 字符串
输出: 去除重复字符并排序的字符串
样例输入: aabcdefff
样例输出: abcdef
答案
1 public void deleteAndSort(String str){ 2 int len=str.length(); 3 ArrayList<Character> list=new ArrayList<Character>(); 4 for(int i=0;i<len;i++){ 5 char ele=str.charAt(i); 6 if(!list.contains(ele)){ 7 list.add(ele); 8 } 9 } 10 Character[] arr=(Character[])list.toArray(new Character[0]); 11 int size=arr.length; 12 for(int i=0;i<size;i++){ 13 for(int j=0;j<size-i-1;j++){ 14 if(arr[j]>arr[j+1]){ 15 char tmp=arr[j]; 16 arr[j]=arr[j+1]; 17 arr[j+1]=tmp; 18 } 19 } 20 } 21 for(char c : arr){ 22 System.out.print(c); 23 } 24 }
其中对第六行的一句话list.contains(ele)感到很奇怪,按理说这个List里面都是对象,即使内容相同,但是它们引用(内存地址)和hashCode应该不是一样的,而contains一般都是通过hashCode 来判断是否包含的,这样的话,通过这句话能够知道是否包含吗?于是去查资料。
ArrayList里面contains的源码为:
1 public boolean contains(Object o) { 2 return indexOf(o) >= 0; 3 } 4 5 6 public int indexOf(Object o) { 7 if (o == null) { 8 for (int i = 0; i < size; i++) 9 if (elementData[i]==null) 10 return i; 11 } else { 12 for (int i = 0; i < size; i++) 13 if (o.equals(elementData[i])) 14 return i; 15 } 16 return -1; 17 }
这里面是通过equals来比较是否包含的。
那么equals是通过什么方式进行比较的呢?它与==有什么区别呢?
总的来说有以下两点
1)对于==,如果作用于基本数据类型的变量,则直接比较其存储的 “值”是否相等;
如果作用于引用类型的变量,则比较的是所指向的对象的地址
2)对于equals方法,注意:equals方法不能作用于基本数据类型的变量
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
由于java对八种基本类型byte int short long float double boolean char 都设计了包装类,并且重写了这些包装类的equals方法和hashCode方法,(这里包括Date和String)这样就可以实现上面程序中通过equals来判断是否包含某个字符。
例如String的equals方法的实现:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = count; if (n == anotherString.count) { char v1[] = value; char v2[] = anotherString.value; int i = offset; int j = anotherString.offset; while (n-- != 0) { if (v1[i++] != v2[j++]) return false; } return true; } } return false; }
这里已经不是对地址进行比较了,而是直接对内容中的字符一个一个的比较了。
其次是hashCode().
根据java官方文档的定义,我们可以抽出成以下几个关键点:
1、hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;(hashCode的作用)
2、如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
这句话可以理解成只要equals相同,hashCode必定相同,但是反过来hashCode相同,equals不一定相同。
3、如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
4、两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
例如String的hashCode()
public int hashCode() { int h = hash; if (h == 0) { int off = offset; char val[] = value; int len = count; for (int i = 0; i < len; i++) { h = 31*h + val[off++]; } hash = h; } return h; }
一般情况下进行判断都是使用equals函数,假如一个类没有重写equals函数和hashCode函数那么在进行判断的时候就会使native 的hashCode进行判断。
例如HashSet中不能有重复的对象,它在添加对象的时候先进行hashCode的判断,假如两个对象的hashCode不同,则直接断定这两个对象不同;假如hashCode相同
,则会跳转到判断是否equals。(注意:hashCode相同但是equals未必是true)
String s1=new String("abc"); String s2=new String("abc"); Set hashset=new HashSet(); hashset.add(s1); hashset.add(s2); System.out.println(s1==s2);//false
System.out.println(s1.equals(s2));//true Iterator it=hashset.iterator(); while(it.hasNext()) { System.out.println(it.next()); }
输出:
false
true
abc
假如添加的是自定义的User类,并且这个User类没有重写equals和hashCode方法,结果就是另一个样子
class User
{ String name; int age; public User(String name,int age) { this.name=name; this.age=age; }
public void toString()
{
return name;
}
}
User s1=new User(1,"abc");
User s2=new User(1,"abc");
Set hashset=new HashSet();
hashset.add(s1);
hashset.add(s2);
System.out.println(s1==s2);//false
System.out.println(s1.equals(s2));//false
Iterator it=hashset.iterator();
while(it.hasNext())
{
System.out.println(it.next());
}
输出:
false
false
abc
abc
这里第二个输出的false 因为User类没有重写equals方法,这里equals方法判断的是他们的地址,所以这里是不等的。而abc打印两次,他们的hashCode都不是一样的,肯定打印两次。
说到这里hashCode与内存地址有什么关系呢,假如hashCode是由内存地址产生的,那由对象的一致性原则,hashCode一样,那么它们的内存地址也是一样的,这样他们equals肯定是true,但是事实并不是这样的,事实是equals为true肯定可以推出hashCode相同,但是hashCode相同未必能够推出equals为true。这样就是说hashCode与内存关系不大,参考:hashCode返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。
附:
1、List,Map,Set三者的contains的实现
List的contains的实现如上。
Set的Contains的实现:
public boolean contains(Object o) { return map.containsKey(o); }
其内部使用的是HashTable
而HashMap也是使用同样的办法。
2、标准的基本类型只要值相等,哈希值就相同;
Integer a=10;
Integer b=10;
那么a和b的哈希值就相同。类似的还有Short、Long、Byte、Boolean、String等等
3、同一个对象,与何时运行该程序无关;
哈希值算法中,对象的内存地址不参与运算。因此只要是同一个对象,那么该对象的哈希值就不会改变。
4、关于容器的哈希值
java中常用的容器有List、Map、Set。那么它们的哈希值又有什么特点呢?
假设有如下两个List:
List<String> list1= new ArrayList<String>();
list1.add("item1");
list1.add("item2");
List<String> list2= new ArrayList<String>();
list2.add("item2");
list2.add("item1");
这两个List的哈希值是不一样的。对于List来讲,每一个元素都有它的顺序。如果被添加的顺序不同,最后的哈希值必然不同。
假如有如下两个Map:
Map<String, String> map1= new HashMap<String, String>();
map1.put("a", "1");
map1.put("b", "2");
map1.put("c", "3");
Map<String, String> map2= new HashMap<String, String>();
map2.put("b", "2");
map2.put("a", "1");
map2.put("c", "3");
这两个Map虽然元素添加的顺序不一样,但是每一个元素的Key-Value值一样。Map是一种无序的存储结构,因此它的哈希值与元素添加顺序无关,这两个Map的哈希值相同。
假如有如下两个Set:
Set<String> set1= new HashSet<String>();
set1.add("a");
set1.add("b");
set1.add("c");
Set<String> set2= new HashSet<String>();
set2.add("b");
set2.add("a");
set2.add("c");
类似的,由于Set也是一种无序的存储结构,两个Set虽然添加元素的顺序不一样,但是总体来说元素的个数和内容是一样的。因此这两个Set的哈希值也相同。
其实,Set的实现是基于Map的。我们可以看到,如果将Set中的元素当做Map中的Key,把Map中的value始终设置为null,那么它就变成了一个Set。
Set<String> set1= new HashSet<String>();
set1.add("a");
set1.add("b");
set1.add("c");
Map<String, String> map1= new HashMap<String, String>();
map1.put("a", null);
map1.put("b", null);
map1.put("c", null);
通过实验我最后得到了印证,set1与map1的哈希值相同。