集合
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讲义
学习所做的笔记,特此记录下来