List、Set、Map集合

集合

Java的集合类主要由两个接口派生而出:Collection 和 Map;
在这里插入图片描述
Collection 接口是List、Set和Queue接口的父接口,该接口里定义的方法既可用于操作Set集合,也可用于操作List集合和Queue集合;

List集合

List集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。List集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。List集合默认按元素的添加顺序设置元素的索引;

public class TestList
{
	public static void main(String[] args) 
	{
		List books = new ArrayList();
		//向books集合中添加三个元素
		books.add(new String("轻量级J2EE企业应用实战"));
		books.add(new String("Struts2权威指南"));
		books.add(new String("基于J2EE的Ajax宝典"));
		System.out.println(books);
		
		//将新字符串对象插入在第二个位置
		books.add(1 , new String("ROR敏捷开发最佳实践"));
		for (int i = 0 ; i < books.size() ; i++ )
		{
			System.out.println(books.get(i));
		}
		
		//删除第三个元素
		books.remove(2);
		System.out.println(books);
		
		//判断指定元素在List集合中位置:输出1,表明位于第二位
		System.out.println(books.indexOf(new String("ROR敏捷开发最佳实践")));
		
		//将第二个元素替换成新的字符串对象
		books.set(1, new String("Struts2权威指南"));
		System.out.println(books);
		
		//将books集合的第二个元素(包括)到第三个元素(不包括)截取称子集合
		System.out.println(books.subList(1 , 2));

	}
}

//输出
[轻量级J2EE企业应用实战, Struts2权威指南, 基于J2EE的Ajax宝典]
轻量级J2EE企业应用实战
ROR敏捷开发最佳实践
Struts2权威指南
基于J2EE的Ajax宝典
[轻量级J2EE企业应用实战, ROR敏捷开发最佳实践, 基于J2EE的Ajax宝典]
1
[轻量级J2EE企业应用实战, Struts2权威指南, 基于J2EE的Ajax宝典]
[Struts2权威指南]

List判断两个对象相等只要通过equals()方法比较返回true即可;

class A
{
	public boolean equals(Object obj)
	{
		return true;
	}
}
public class TestList2
{
	public static void main(String[] args) 
	{
		List books = new ArrayList();
		books.add(new String("轻量级J2EE企业应用实战"));
		books.add(new String("Struts2权威指南"));
		books.add(new String("基于J2EE的Ajax宝典"));
		System.out.println(books);
		
		//删除集合中A对象,将导致第一个元素被删除
		books.remove(new A());   //1
		System.out.println(books);
		
		//删除集合中A对象,再次删除集合中第一个元素
		books.remove(new A());    //2
		System.out.println(books);
	}
}

//输出
[轻量级J2EE企业应用实战, Struts2权威指南, 基于J2EE的Ajax宝典]
[Struts2权威指南, 基于J2EE的Ajax宝典]
[基于J2EE的Ajax宝典]

执行1代码时,程序试图删除一个A对象,List将会调用该A对象的equals()方法依次与集合元素进行比较,如果该equals()方法以某个集合元素为参数时返回true,List将会删除该元素-----------A重写了equals()方法,总是返回true;

Java8为List集合增加了sort()和replaceAll()两个常用的默认方法,其中sort()方法需要一个Comparator对象来控制元素排序,程序可使用Lambda表达式来作为参数;而replaceAll()方法则需要一个UnaryOperator来替换所有集合元素,UnaryOperator也是一个函数式接口,因此程序也可使用Lambda表达式作为参数:

public class TestList3
{
	public static void main(String[] args) 
	{
		List books = new ArrayList();
		books.add(new String("轻量级J2EE企业应用实战"));
		books.add(new String("Struts2权威指南"));
		books.add(new String("基于J2EE的Ajax宝典"));
	    books.sort((o1,o2)->((String)o1).length() - ((String)o2).length());
	    System.out.println(books);
	    
	    //该Lambda表达式控制使用每个字符串的长度作为新的集合元素
	    books.replaceAll(ele->((String)ele).length());
	    System.out.println(books);
	}
}
//输出
[Struts2权威指南, 轻量级J2EE企业应用实战, 基于J2EE的Ajax宝典]
[11, 13, 13]

与Set只提供了一个iterator()方法不同,List还额外提供了一个ListIterator()方法,该方法返回一个ListIterator对象,ListIterator接口继承了Iterator接口,提供了专门操作List的方法;

拿ListIterator与普通的Iterator进行对比,不难发现ListIterator增加了向前迭代的功能(Iterator只能向后迭代),而且ListIterator还可通过add()方法向List集合中添加元素(Iterator只能删除元素);

public class TestListIterator
{
	public static void main(String[] args) 
	{
		String[] books = {
			"Struts2权威指南",
			"轻量级J2EE企业应用实战"
		};
		List bookList = new ArrayList();
		for (int i = 0; i < books.length ; i++ )
		{
			bookList.add(books[i]);
		}
		ListIterator lit = bookList.listIterator();
		while (lit.hasNext())
		{
			System.out.println(lit.next());
			lit.add("-------分隔符-------");
		}
		System.out.println("==========下面开始反向迭代===========");
		while(lit.hasPrevious())
		{
			System.out.println(lit.previous());
		}
	}
}

//输出
Struts2权威指南
轻量级J2EE企业应用实战
==========下面开始反向迭代===========
-------分隔符-------
轻量级J2EE企业应用实战
-------分隔符-------
Struts2权威指南

ArrayList和Vector类

ArrayList和Vector作为List类的两个典型实现;

ArrayList和Vector类都是基于数组实现的List类,所以ArrayList和Vector类封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素超出了该数组的长度时,它们的initialCapacity会自动增加;

ArrayList和Vector在用法上几乎完全相同;

ArrayList和Vector的显著区别是:

  • ArrayList是线程不安全的,但Vector是线程安全的;
  • 当元素超过它的初始大小时,Vector会将容量翻倍,ArrayLis只增加50%的大小;

固定长度的List

操作数组的工具类:Arrays,该工具类提供了一个asList(Object… a)方法,该方法可以把一个数组或指定个数的对象转换成一个List集合,这个List集合既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Array的内部类ArrayList的实例;

Array.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里的元素,不可增加、删除该集合里的元素:

public class FixedSizeList
{
	public static void main(String[] args) 
	{
		List fixedList = Arrays.asList("Struts2权威指南" , "ROR敏捷开发最佳实践");
		//获取fixedList的实现类,将输出Arrays$ArrayList
		System.out.println(fixedList.getClass());
		//遍历fixedList的集合元素
		for (int i = 0; i < fixedList.size() ; i++)
		{
			System.out.println(fixedList.get(i));
		}
		//试图增加、删除元素都将引发UnsupportedOperationException异常
		fixedList.add("ROR敏捷开发最佳实践");
		fixedList.remove("Struts2权威指南");
	}
}

LinkedList实现类

LinkedList类是List接口的实现类----这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成栈来使用,也可以当成队列使用:

public class TestLinkedList
{
	public static void main(String[] args) 
	{
		LinkedList books = new LinkedList();
		//将字符串元素加入队列的尾部
		books.offer("Struts2权威指南");
		//将一个字符串元素入栈
		books.push("轻量级J2EE企业应用实战");
		//将字符串元素添加到队列的头部
		books.offerFirst("ROR敏捷开发最佳实践");
		for (int i = 0; i < books.size() ; i++ )
		{
			System.out.println(books.get(i));
		}
		//访问、并不删除队列的第一个元素
		System.out.println(books.peekFirst());
		//访问、并不删除队列的最后一个元素
		System.out.println(books.peekLast());
		//采用出栈的方式将第一个元素pop出队列
		System.out.println(books.pop());
		//下面输出将看到队列中第一个元素被删除
		System.out.println(books);
		//访问、并删除队列的最后一个元素
		System.out.println(books.pollLast());
		//下面输出将看到队列中只剩下中间一个元素:轻量级J2EE企业应用实战
		System.out.println(books);

	}
}
//输出
ROR敏捷开发最佳实践
轻量级J2EE企业应用实战
Struts2权威指南
ROR敏捷开发最佳实践
Struts2权威指南
ROR敏捷开发最佳实践
[轻量级J2EE企业应用实战, Struts2权威指南]
Struts2权威指南
[轻量级J2EE企业应用实战]

LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色(只需改变指针所指的地址即可)。

各种List性能分析

一般来说,由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。但总体来说,ArrayList的性能比LinkedList的性能要好,因此大部分时候都应该考虑ArrayList;

关于使用List集合的建议:

  • 如果需要遍历List集合,对于ArrayList和Vector集合,应该使用随机访问法(get)来遍历集合元素,这样性能更好;对于LinkedList集合,则应该采用迭代器(Iterator)来遍历集合元素;
  • 如果需要经常执行插入、删除操作来改变包含大量数据的List集合的大小,可考虑使用LinkedList集合。使用ArrayList和Vector集合可能需要经常重新分配内存数组的大小,效果可能较差;
  • 如果有多个线程需要同时访问List集合中的元素,可考虑使用Collections将集合包装成线程安全的集合;

Set集合

Set集合不允许包含相同的元素;

HashSet类

HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能;
HashSet具有以下特点:

  • 不能保证元素的排列顺序;
  • HashSet是不同步的,如果多个线程同时访问一个HashSet,假设有两个或者两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步;
  • 集合元素值可以是null;

当向HashSet集合中存入一个元素时,HashSet会调用对象的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法的返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功;

也就是说,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等;

//类A的equals方法总是返回true,但没有重写其hashCode()方法
class A
{
	public boolean equals(Object obj)
	{
		return true;
	}
}
//类B的hashCode()方法总是返回1,但没有重写其equals()方法
class B
{
	public int hashCode()
	{
		return 1;
	}
}
//类C的hashCode()方法总是返回2,并重写其equals()方法
class C
{
	public int hashCode()
	{
		return 2;
	}
	public boolean equals(Object obj)
	{
		return true;
	}
}
public class TestHashSet
{
	public static void main(String[] args) 
	{
		HashSet books = new HashSet();
		//分别向books集合中添加2个A对象,2个B对象,2个C对象
		books.add(new A());
		books.add(new A());
		books.add(new B());
		books.add(new B());
		books.add(new C());
		books.add(new C());
		System.out.println(books.size());
		System.out.println(books);
	}
}
//输出
5
[B@1, B@1, C@2, A@15db9742, A@6d06d69c]

由输出可以看出两个C对象被认为是同一个对象;

注意:当把一个对象放入HashSet中时,如果需要重写该对象对应类的equals()方法,则也应该重写其hashCode()方法。规则是:如果两个对象通过equals()方法比较返回true,这两个对象的hashCode值也应该相同;

hash算法
hash算法的功能是,它能保证快速查找被检索的对象,hash算法的价值在于速度。当需要查询集合中某个元素时,hash算法可以直接根据该元素的hashCode值计算出该元素的存储位置,从而快速定位该元素;

当程序向HashSet集合中添加元素时,HashSet会根据该元素的hashCode值来计算它的存储位置,这样可快速定位该元素;

HashSet中每个能存储元素的 “槽位” 通常称为 “桶” ,如果有多个元素的hashCode值相同,但它们通过equals()方法比较返回false,就需要在一个 “桶” 里放多个元素,这样会导致性能下降;

LinkedHashSet类

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。

也就是说,当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素;

LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时将具有很好的性能,因为它以链表来维护内部顺序;

public class TestLinkedHashSet
{
	public static void main(String[] args) 
	{
		LinkedHashSet books = new LinkedHashSet();
		books.add("Struts2权威指南");
		books.add("轻量级J2EE企业应用实战");
		//删除 Struts2权威指南
		books.remove("Struts2权威指南");
		//重新添加 Struts2权威指南
		books.add("Struts2权威指南");
		System.out.println(books);
	}
}

//输出   [轻量级J2EE企业应用实战, Struts2权威指南]

输出LinkedHashSet集合的元素时,元素的顺序总是与添加顺序一致;

虽然LinkedHashSet使用了链表记录集合元素的添加顺序,但LinkedHashSet依然是HashSet,因此它依然不允许集合元素重复;

TreeSet类

TreeSet是SortedSet接口的实现类。TreeSet可以确保集合元素处于排序状态;

public class TestTreeSetCommon
{
	public static void main(String[] args) 
	{
		TreeSet nums = new TreeSet();
		//向TreeSet中添加四个Integer对象
		nums.add(5);
		nums.add(2);
		nums.add(10);
		nums.add(-9);
		//输出集合元素 [-9, 2, 5, 10],看到集合元素已经处于排序状态
		System.out.println(nums);
		
		//输出集合里的第一个元素-9
		System.out.println(nums.first());
		
		//输出集合里的最后一个元素10
		System.out.println(nums.last());
		
		//返回小于4的子集,不包含4-----[-9, 2]
		System.out.println(nums.headSet(4));
		
		//返回大于5的子集,如果Set中包含5,子集中还包含5-------[5, 10]
		System.out.println(nums.tailSet(5));
		
		//返回大于等于-3,小于4的子集。[2]
		System.out.println(nums.subSet(-3 , 4));
	}
}

TreeSet并不是根据元素的插入顺序进行排序的,而是根据元素实际值的大小来进行排序的;

与HashSet集合采用hash算法来决定元素的存储位置不同,TreeSet采用红黑树的数据结构来存储集合元素。那么TreeSet进行排序的规则是怎样的呢?

TreeSet支持两种排序方法:

  • 自然排序(默认情况下)
  • 定制排序

1:自然排序

TreeSet会调用元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序;

Java提供了一个Comparable接口,该接口里定义了一个compareTo(Object obj)方法,该方法返回一个整数值,实现该接口的类必须实现该方法,实现了该接口的类的对象就可以比较大小。

当一个对象调用该方法与另一个对象进行比较时,例如obj1.compareTo(obj2),如果该方法返回0,则表明这两个对象相等;如果该方法返回一个正整数,则表明obj1大于obj2;如果该方法返回一个负整数,则表明obj1小于obj2。

如果试图把一个对象添加到TreeSet时,则该对象的类必须实现Comparable接口,否则程序将会抛出异常;

class Err
{
}
public class TestTreeSetError
{
	public static void main(String[] args) 
	{
		TreeSet ts = new TreeSet();
		//向TreeSet集合中添加Err对象
		ts.add(new Err());
	}
}

//报错  java.lang.ClassCastException: Err cannot be cast to java.lang.Comparable

另外:如果希望TreeSet能正常运作,TreeSet只能添加同一种类型的对象;

对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0------如果方法比较返回0,TreeSet则会认为它们相等;否则就认为它们不相等;

//Z类,重写了equals方法,总是返回false,
//重写了compareTo(Object obj)方法,总是返回正整数
class Z implements Comparable
{
	int age;
	public Z(int age)
	{
		this.age = age;
	}
	public boolean equals(Object obj)
	{
		return false;
	}
	public int compareTo(Object obj)
	{
		return 1;
	}
}
public class TestTreeSet
{
	public static void main(String[] args) 
	{
		TreeSet set = new TreeSet();
		Z z1 = new Z(6);
		set.add(z1);
		System.out.println(set.add(z1));    //true
		
		//下面输出set集合,将看到有2个元素[Z@15db9742, Z@15db9742]
		System.out.println(set);
		
		//修改set集合的第一个元素的age属性
		((Z)(set.first())).age = 9;
		//输出set集合的最后一个元素的age属性,将看到也变成了9
		System.out.println(((Z)(set.last())).age);
	}
}

在这里插入图片描述

从图8.5可以看到TreeSet对象保存的两个元素(集合里的元素总是引用,但习惯上把被引用的对象称为集合元素),实际上是同一个元素。所以当修改TreeSet集合里第一个元素的age变量后,该TreeSet集合里最后一个元素的age变量也随之改变了;

因此,当需要把一个对象放入TreeSet中,重写该对象对应类的equals()方法时,应保证该方法与compareTo(Object obj)方法方法有一致的结果,其规则是:如果两个对象通过equals()方法比较返回true时,这两个对象通过compareTo(Object obj)方法比较应返回0;

2:定制排序
如果需要实现定制排序,例如以降序排列,则可以通过Comparator接口的帮助。该接口里包含一个int compare(T o1,T o2)方法,该方法用于比较o1和o2的大小:如果该方法返回正整数,则表明o1大于o2;如果该方法返回0,则表明o1等于o2;如果该方法返回负整数,则表明o1小于o2;

如果需要实现定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联,由该Comparator对象负责集合元素的排序逻辑。由于Comparator是一个函数式接口,因此可使用Lambda表达式来代替Comparator对象。
Lambda表达式:

class M
{
	int age;
	public M(int age)
	{
		this.age = age;
	}
	public String toString() {
		return "M[age:"+age+"]";
	}
}
public class TestTreeSet3
{
	public static void main(String[] args) 
	{
		TreeSet ts = new TreeSet((o1,o2)->		
		{
			M m1 = (M)o1;
			M m2 = (M)o2;
			return m1.age > m2.age ? -1 :m1.age < m2.age ?1:0;
		});
		ts.add(new M(5));
		ts.add(new M(-3));
		ts.add(new M(9));
		System.out.println(ts);  //输出[M[age:9], M[age:5], M[age:-3]]
	}
}

匿名内部类:

class M
{
	int age;
	public M(int age)
	{
		this.age = age;
	}
	public String toString() {
		return "M[age:"+age+"]";
	}
}
public class TestTreeSet3
{
	public static void main(String[] args) 
	{
		TreeSet ts = new TreeSet(new Comparator()
		{
			public int compare(Object o1, Object o2)
			{							
				M m1 = (M)o1;
				M m2 = (M)o2;

				if (m1.age > m2.age)
				{
					return -1;
				}
				else if (m1.age == m2.age)
				{
					return 0;
				}
				else
				{
					return 1;
				}
				
			}
		});	
		ts.add(new M(5));
		ts.add(new M(-3));
		ts.add(new M(9));
		System.out.println(ts);  //输出[M[age:9], M[age:5], M[age:-3]]
	}
}

各Set实现类的性能分析

HashSet和TreeSet是Set的两个典型实现,到底该如何选择HashSet和TreeSet呢?

HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保存排序的的Set时,才应该使用TreeSet,否则都应该使用HashSet。

HashSet还有一个子类:LinkedHashSet,对于普通的插入、删除操作,LinkedHashSet比HashSet要略微慢一点,这是由于维护链表所带来的额外开销造成的,但由于有了链表,遍历LinkedHashSet会更快;

HashSet和TreeSet都是线程不安全的,如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性;

Map集合

Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false;
在这里插入图片描述

HashMap和Hashtable类

HashMap和Hashtable都是Map接口的典型实现类,它们之间的关系完全类似于ArrayList和Vector的关系;

Java8改进了HashMap的实现,使用HashMap存在key冲突时依然具有较好的性能;

除此之外,HashMap和Hashtable存在两点典型区别:

  • Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好;
  • Hashtable不允许使用null作为key和value,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap可以使用null作为key或value;

由于HashMap里的key不能重复,所以HashMap里最多只有一个key-value对的key为null,但可以有无数多个key-value对的value为null;

public class NullInHashMap
{
	public static void main(String[] args) 
	{
		HashMap hm = new HashMap();
		//试图将2个key为null的key-value对放入HashMap中
		hm.put(null , null);
		hm.put(null , null);
		//将一个value为null的key-value对放入HashMap中
		hm.put("a" , null);
		//输出Map对象,{null=null, a=null}
		System.out.println(hm);
	}
}

为了成功地在HashMap、Hashtable中存储、获取对象,用作key的对象必须实现hashCode()方法和equals()方法;

原因:HashMap是使用散列的方法来进行快速查找.当进行查找的时候先将你hashmap中的key调用hashcode()方法得到一个散列值,所以这时就需要你对hashcode()方法进行重载,同时这个值对应的是一个LinkedList数组的下标,而这个LinkedList数组中存的就是你的value对象,然后hashmap调用equals方法对LinkedList数组中的value对象值进行比较。而默认的equals()方法是对对象的引用的比较,所以这是你同样要重载equals()方法;(大多数Java类库都实现了equals()方法,以便用来比较对象的内容,而非比较对象的引用)

import java.util.HashMap;
class Key{
	private Integer id;
	
	public Integer get(){
		return id;
	}
	
	public Key(Integer id){
		this.id = id;
	}	
}

public class DD {
	//k1与k2都属于Object子类。如果不重写系统就不得不调用Object类的equals方法,Object的equal则是默认比较对象的引用,故必须
	//重写equal方法。Object的hashCode方法默认返回的是对象的内存地址,故也需要重写hashCode方法
	public static void main(String[] args) throws Exception {
		Key k1 = new Key(1);
        Key k2 = new Key(1);
        HashMap<Key,String> hm = new HashMap<Key,String>();
        hm.put(k1, "Key with id is 1"); 
        System.out.println(hm.get(k2));  
        
		System.out.println(k1.hashCode());  
		System.out.println(k2.hashCode());  
	}
}
//打印:
null
366712642
1829164700

与HashSet集合不能保证元素的顺序一样,HashMap和Hashtable也不能保证其中key-value对的顺序。HashMap、Hashtable判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode()值也相等;

除此之外,HashMap、Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value。那么HashMap、Hashtable如何判断两个value相等呢?HashMap、Hashtable判断两个value相等的标准更简单:只要两个对象通过equals()方法比较返回true即可;

/**
 * A类判断两个A对象相等的标准是count实例变量:只要两个A对象的count变量相等,则通过equals()方法比较
 * 它们返回true,它们的hashCode值也相等;
 * @author yd
 *
 */
class A
{
	int count;
	public A(int count)
	{
		this.count = count;
	}
	public boolean equals(Object obj)
	{
		if (obj == this)
		{
			return true;
		}
		if (obj != null && 
			obj.getClass() == A.class)
		{
			A a = (A)obj;
			if (this.count == a.count)
			{
				return true;
			}
		}
		return false;
	}
	public int hashCode()
	{
		return this.count;
	}
}

/**
 * B对象可以与任何对象相等
 * @author yd
 *
 */
class B
{
	public boolean equals(Object obj)
	{
		return true;
	}
}
public class TestHashtable
{
	public static void main(String[] args) 
	{
		Hashtable ht = new Hashtable();
		ht.put(new A(60000) , "Struts2权威指南");
		ht.put(new A(87563) , "轻量级J2EE企业应用实战");
		ht.put(new A(1232) , new B());
		System.out.println(ht);
		
		//只要两个对象通过equals比较返回true,Hashtable就认为它们是相等的value。
		//因为Hashtable中有一个B对象,它与任何对象通过equals比较都相等,所以下面输出true。
		System.out.println(ht.containsValue("测试字符串"));
		
		//只要两个A对象的count属性相等,它们通过equals比较返回true,且hashCode相等
		//Hashtable即认为它们是相同的key,所以下面输出true。
		System.out.println(ht.containsKey(new A(87563)));
		
		//下面语句可以删除最后一个key-value对
		ht.remove(new A(1232));
		
		for (Object key : ht.keySet())
		{
			System.out.print(key + "---->");
			System.out.print(ht.get(key) + "\n");
		}
	}
}
//输出
{A@ea60=Struts2权威指南, A@1560b=轻量级J2EE企业应用实战, A@4d0=B@15db9742}
true
true
A@ea60---->Struts2权威指南
A@1560b---->轻量级J2EE企业应用实战

LinkedHashMap实现类

HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类;LinkedHashMap也使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致;

LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能;

public class TestLinkedHashMap
{
	public static void main(String[] args) 
	{
		LinkedHashMap scores = new LinkedHashMap();
		scores.put("语文" , 80);
		scores.put("数学" , 76);
		scores.put("英文" , 76);
		//遍历scores里的所有的key-value对
		for (Object key : scores.keySet())
		{
			System.out.print(key + "------>");
			System.out.print(scores.get(key) + "\n");
		}
	}
}
//输出
语文------>80
数学------>76
英文------>76

LinkedHashMap可以记住key-value对的添加顺序;

使用Properties读写属性文件

Properties类是Hashtable类的子类;Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。

Properties相当于一个key、value都是String类型的Map。

  • String getProperty(String key) :获取Properties中指定属性名对应的属性值,类似于Map的get(Object key)方法;
  • String getProperty(String key, String defaultValue) :与前一个类似,多了一个功能,如果Properties中不存在指定的key时,则该方法指定默认值;
  • Object setProperty(String key, String value) :设置属性值,类似于Hashtable的put()方法;

除此之外,它还提供了两个读写属性文件的方法:

  • void load(InputStream inStream) :从属性文件中加载key-value对,把加载到的key-value对追加到Properties里(Properties是Hashtable的子类,它不保证key-value对之间的次序);
  • void store(OutputStream out, String comments) :将Properties中的key-value对输出到指定的属性文件中;
public class TestProperties
{
	public static void main(String[] args) throws Exception
	{
		Properties props = new Properties();
		//向Properties中增加属性
		props.setProperty("username" , "yeeku");
		props.setProperty("password" , "123456");
		
		//将Properties中的属性保存到a.ini文件中
		props.store(new FileOutputStream("a.ini") , "comment line");
		
		//新建一个Properties对象
		Properties props2 = new Properties();
		
		//向Properties中增加属性
		props2.setProperty("gender" , "male");
		
		//将a.ini文件中的属性名-属性值追加到props2中
		props2.load(new FileInputStream("a.ini") );
		System.out.println(props2);    //输出 {password=123456, gender=male, username=yeeku}
	}
}

在当前文件路径下生成了一个a.ini文件,内容为:

#comment line
#Mon Nov 12 16:06:36 CST 2018
password=123456
username=yeeku

Properties 可以把key-value对以XML文件的形式保存起来,也可以从XML文件中加载key-value对;

SortedMap接口和TreeMap实现类

正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样。Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类;

TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。TreeMap也有两种排序方式:

  • 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException异常;
  • 定制排序:创建TreeMap时,传入一个Comparable对象,该对象负责对TreeMap中的所有key进行排序。采用定制排序时不要求Map的key实现Comparable接口;

TreeMap中判断两个key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认为这两个key是相等的。

//R类,重写了equals方法,如果count属性相等返回true
//重写了compareTo(Object obj)方法,如果count属性相等返回0;
class R implements Comparable
{
	int count;
	public R(int count)
	{
		this.count = count;
	}
	public String toString()
	{
		return "R(count属性:" + count + ")";
	}
	public boolean equals(Object obj)
	{
		if (this == obj)
		{
			return true;
		}
		if (obj != null 
			&& obj.getClass() == R.class)
		{
			R r = (R)obj;
			if (r.count == this.count)
			{
				return true;
			}
		}
		return false;
	}
	public int compareTo(Object obj)
	{
		R r = (R)obj;
		if (this.count > r.count)
		{
			return 1;
		}
		else if (this.count == r.count)
		{
			return 0;
		}
		else
		{
			return -1;
		}
	}
}
public class TestTreeMap
{
	public static void main(String[] args) 
	{
		TreeMap tm = new TreeMap();
		tm.put(new R(3) , "轻量级J2EE企业应用实战");
		tm.put(new R(-5) , "Struts2权威指南");
		tm.put(new R(9) , "ROR敏捷开发最佳实践");
		System.out.println(tm);
		
		//返回该TreeMap的第一个Entry对象
		System.out.println(tm.firstEntry());
		
		//返回该TreeMap的最后一个key值
		System.out.println(tm.lastKey());
		
		//返回该TreeMap的比new R(2)大的最小key值。
		System.out.println(tm.higherKey(new R(2)));
		
		//返回该TreeMap的比new R(2)小的最大的key-value对。
		System.out.println(tm.lowerEntry(new R(2)));
		
		//返回该TreeMap的子TreeMap
		System.out.println(tm.subMap(new R(-1) , new R(4)));

	}
}
//输出
{R(count属性:-5)=Struts2权威指南, R(count属性:3)=轻量级J2EE企业应用实战, R(count属性:9)=ROR敏捷开发最佳实践}
R(count属性:-5)=Struts2权威指南
R(count属性:9)
R(count属性:3)
R(count属性:-5)=Struts2权威指南
{R(count属性:3)=轻量级J2EE企业应用实战}

WeakHashMap实现类

WeakHashMap与HashMap的用法基本相似。与HashMap的区别在于,HashMap的key保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap的所有key所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些key所对应的key-value对;但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,WeakHashMap也可能自动删除这些key所对应的key-value对;

WeakHashMap中的每个key对象只持有对实际对象的弱引用,因此,当垃圾回收了该key所对应的实际对象之后,WeakHashMap会自动删除该key对应的key-value对;

public class TestWeakHashMap
{
	public static void main(String[] args) 
	{
		WeakHashMap whm = new WeakHashMap();
		//将WeakHashMap中添加三个key-value对,
		//三个key都是匿名字符串对象(没有其他引用)
		whm.put(new String("语文") , new String("良好"));
		whm.put(new String("数学") , new String("及格"));
		whm.put(new String("英文") , new String("中等"));
		//将WeakHashMap中添加一个key-value对,
		//该key是一个系统缓存的字符串对象。
		whm.put("java" , new String("中等"));
		//输出whm对象,将看到4个key-value对。
		System.out.println(whm);
		//通知系统立即进行垃圾回收
		System.gc();
		System.runFinalization();
		//通常情况下,将只看到一个key-value对。
		System.out.println(whm);
	}
}
//输出
{英文=中等, java=中等, 数学=及格, 语文=良好}
{java=中等}

IdentityHashMap实现类

这个Map实现类的实现机制与HashMap基本相似,但它在处理两个key相等时比较独特:在IdentityHashMap中,当且仅当两个key严格相等(key1==key2)时,IdentityHashMap才认为两个key相等;对于普通的HashMap而言,只要key1和key2通过equals()方法比较返回true,且它们的hashCode值相等即可;

IdentityHashMap提供了与HashMap基本相似的方法,也允许使用null作为key和value。与HashMap相似:IdentityHashMap也不保证key-value对之间的顺序,更不能保证它们的顺序随时间的推移保持不变;

public class TestIdentityHashMap
{
	public static void main(String[] args) 
	{
		IdentityHashMap ihm = new IdentityHashMap();
		//下面两行代码将会向IdentityHashMap对象中添加2个key-value对
		ihm.put(new String("语文") , 89);
		ihm.put(new String("语文") , 78);

        //都是字符串常量,且字符序列完全相同,Java使用常量池来管理字符串常量,所以通过==比较返回true
		ihm.put("java" , 93);
		ihm.put("java" , 98);
		System.out.println(ihm);
	}
}
//输出
{语文=89, java=98, 语文=78}

各Map实现类的性能分析

虽然HashMap和Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashMap通常比Hashtable块;

TreeMap通常比HashMap、Hashtable要慢(尤其在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对);

使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象;

对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap;

LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中的key-value时的添加顺序。

IdentityHashMap性能没有什么特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用==而不是equals()方法来判断元素相等;

书籍:疯狂Java讲义
学习所做的笔记,特此记录下来

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值