cuiter  

Java集合

容器基础

使用Lambda表达式遍历集合

Iterable有一个默认方法forEach(Consumer action),Iterable又是Collection的父接口,因此Collection集合可以使用;又因为Consumer是函数式接口,所以可以用Lambda表达式来遍历集合元素。

public class CollectionEach
{
   public static void main(String[] args)
   {
      // 创建一个集合
      var books = new HashSet();
      books.add("轻量级Java EE企业应用实战");
      books.add("疯狂Java讲义");
      books.add("疯狂Android讲义");
      // 调用forEach()方法遍历集合  obj是接收集合元素的参数
      books.forEach(obj -> System.out.println("迭代集合元素:" + obj));
   }
}

使用Iterator遍历集合元素

Collection系列集合,Map系列集合主要用于盛装其他对象,Iterator主要用来遍历Collection集合中的元素,因此又叫做迭代器。

public class IteratorTest
{
   public static void main(String[] args)
   {
      // 创建集合、添加元素的代码与前一个程序相同
      var books = new HashSet();
      books.add("轻量级Java EE企业应用实战");
      books.add("疯狂Java讲义");
      books.add("疯狂Android讲义");
      // 获取books集合对应的迭代器
      var it = books.iterator();
      while (it.hasNext())
      {
         // it.next()方法返回的数据类型是Object类型,因此需要强制类型转换
         var book = (String) it.next();
         System.out.println(book);
         if (book.equals("疯狂Java讲义"))
         {
            // 从集合中删除上一次next方法返回的元素
            it.remove();
            // 使用Iterator迭代过程中,不可修改集合元素,下面代码引发异常  ConcurrentModificationException
            // Iterator使用快速失败(fail-fast)机制,一旦迭代过程中检测到该集合已经被修改(通常是其他线程修改),
            // 程序立即引发ConcurrentModificationException异常,可以避免共享资源而引发的潜在问题
            // books.remove(book);
         }
         // 对book变量赋值,不会改变集合元素本身
         book = "测试字符串";   // 
      }
      System.out.println(books);
   }
}

使用Lambda表达式遍历Iterator

public class IteratorEach
{
   public static void main(String[] args)
   {
      // 创建集合、添加元素的代码与前一个程序相同
      var books = new HashSet();
      books.add("轻量级Java EE企业应用实战");
      books.add("疯狂Java讲义");
      books.add("疯狂Android讲义");
      // 获取books集合对应的迭代器
      var it = books.iterator();
      // 使用Lambda表达式(目标类型是Comsumer)来遍历集合元素
      it.forEachRemaining(obj -> System.out.println("迭代集合元素:" + obj));
   }
}

使用foreach循环遍历集合

public class ForeachTest
{
   public static void main(String[] args)
   {
      // 创建集合、添加元素的代码与前一个程序相同
      var books = new HashSet();
      books.add(new String("轻量级Java EE企业应用实战"));
      books.add(new String("疯狂Java讲义"));
      books.add(new String("疯狂Android讲义"));
      for (var obj : books)
      {
         // 此处的book变量也不是集合元素本身
         var book = (String) obj;
         System.out.println(book);
         if (book.equals("疯狂Android讲义"))
         {
            // 下面代码会引发ConcurrentModificationException异常
            books.remove(book);     // 
         }
      }
      System.out.println(books);
   }
}

使用Predicate操作集合

Predicate叫做谓词,是函数式接口。

public class PredicateTest
{
	public static void main(String[] args)
	{
		// 创建一个集合
		var books = new HashSet();
		books.add("轻量级Java EE企业应用实战");
		books.add("疯狂Java讲义");
		books.add("疯狂iOS讲义");
		books.add("疯狂Ajax讲义");
		books.add("疯狂Android讲义");
		// 使用Lambda表达式(目标类型是Predicate)过滤集合
		books.removeIf(ele -> ((String) ele).length() < 10);
		System.out.println(books);
	}
}

public class PredicateTest2
{
   public static void main(String[] args)
   {
      // 创建books集合、为books集合添加元素的代码与前一个程序相同。
      var books = new HashSet();
      books.add("轻量级Java EE企业应用实战");
      books.add("疯狂Java讲义");
      books.add("疯狂iOS讲义");
      books.add("疯狂Ajax讲义");
      books.add("疯狂Android讲义");
      // 统计书名包含“疯狂”子串的图书数量
      System.out.println(calAll(books, ele->((String) ele).contains("疯狂")));
      // 统计书名包含“Java”子串的图书数量
      System.out.println(calAll(books, ele->((String) ele).contains("Java")));
      // 统计书名字符串长度大于10的图书数量
      System.out.println(calAll(books, ele->((String) ele).length() > 10));
   }
   public static int calAll(Collection books, Predicate p)
   {
      int total = 0;
      for (var obj : books)
      {
         // 使用Predicate的test()方法判断该对象是否满足Predicate指定的条件
         if (p.test(obj))
         {
            total++;
         }
      }
      return total;
   }
}

使用Stream流操作集合

Stream流支持串行和并行聚集操作元素。

使用步骤如下:

  1. 使用Stream或XxxStream(Xxx:int、long......)的builder()类方法创建该stream对应的Builder;
  2. 重复调用Builder的add()方法向该流中添加多个元素;
  3. 调用Builder的build()方法获取对应的Stream;
  4. 调用Stream的聚集方法。
public class IntStreamTest
{
   public static void main(String[] args)
   {
      var is = IntStream.builder()
         .add(20)
         .add(13)
         .add(-2)
         .add(18)
         .build();
      // 下面调用聚集方法的代码每次只能执行一个
//    System.out.println("is所有元素的最大值:" + is.max().getAsInt());
//    System.out.println("is所有元素的最小值:" + is.min().getAsInt());
//    System.out.println("is所有元素的总和:" + is.sum());
//    System.out.println("is所有元素的总数:" + is.count());
//    System.out.println("is所有元素的平均值:" + is.average());
//    System.out.println("is所有元素的平方是否都大于20:"
//       + is.allMatch(ele -> ele * ele > 20));
//    System.out.println("is是否包含任何元素的平方大于20:"
//       + is.anyMatch(ele -> ele * ele > 20));
      // 将is映射成一个新Stream,新Stream的每个元素是原Stream元素的2倍+1
      var newIs = is.map(ele -> ele * 2 + 1);
      // 使用方法引用的方式来遍历集合元素
      newIs.forEach(System.out::println); // 输出41 27 -3 37
   }
}

//******************************       对PredicateTest2.callAll()的改写            **************************************//
public class CollectionStream
{
	public static void main(String[] args)
	{
		// 创建books集合、为books集合添加元素的代码与8.2.5小节的程序相同。
		var books = new HashSet();
		books.add("轻量级Java EE企业应用实战");
		books.add("疯狂Java讲义");
		books.add("疯狂iOS讲义");
		books.add("疯狂Ajax讲义");
		books.add("疯狂Android讲义");
		// 统计书名包含“疯狂”子串的图书数量
		System.out.println(books.stream()
			.filter(ele->((String) ele).contains("疯狂"))
			.count()); // 输出4
		// 统计书名包含“Java”子串的图书数量
		System.out.println(books.stream()
			.filter(ele->((String) ele).contains("Java") )
			.count()); // 输出2
		// 统计书名字符串长度大于10的图书数量
		System.out.println(books.stream()
			.filter(ele->((String) ele).length() > 10)
			.count()); // 输出2
		// 先调用Collection对象的stream()方法将集合转换为Stream,
		// 再调用Stream的mapToInt()方法获取原有的Stream对应的IntStream
		books.stream().mapToInt(ele -> ((String) ele).length())
			// 调用forEach()方法遍历IntStream中每个元素
			.forEach(System.out::println);// 输出8 11 16 7 8
	}
}

Set集合

HashSet类

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

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

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

4、所以存入HashSet的对象,若是equals()被重写了,那么hashCode()也应该重写。

HashSet的特征

  • 不能保证元素的排列顺序,顺序可能与元素的添加顺序不同,元素的顺序可能变化。
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,如果有2条或者2条以上线程同时修改了HashSet集合时,必须通过代码来保证其同步。
  • 集合元素值可以是null。

LinkedHashSet类

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

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

TreeSet类

1、TreeSet是SortedSet接口的唯一实现,正如SortedSet名字所暗示的,TreeSet可以确保集合元素处于排序状态。

2、TreeSet采用红黑树的数据结构对元素进行排序。TreeSet支持两种排序方法:自然排序和定制排序。

  • 自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间大小关系,然后将集合元素按升序排列,这种方式就是自然排列。
  • 定制排序:TreeSet借助于Comparator接口的帮助。该接口里包含一个的int compare(T o1, T o2)方法,该方法用于比较o1和o2的大小。

3、采用自然排序的TreeSet集合的元素没有实现Comparable接口就会引发ClassCastException。

4、TreeSet只能添加同一种类型的元素。

5、TreeSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的compareTo(Object obj)方法返回0。

6、所以存入TreeSet的对象,若是equals()被重写了,那么compareTo(Object obj)也应该重写。

EnumSet类

1、EnumSet是一个专为枚举类设计的集合类,EnumSet中所有元素都必须是指定枚举类型的枚举值,该枚举类型在创建EnumSet时显式或隐式地指定。

2、EnumSet的集合元素也是有序的,EnumSet以枚举值在Enum类的定义顺序来决定集合元素的顺序。

3、EnumSet在内部以位向量的形式存储,这种存储形式非常紧凑、高效,因此EnumSet对象占用内存很小,而且运行效率很好。尤其是当进行批量操作(如调用containsAll 和 retainAll方法)时,如其参数也是EnumSet集合,则该批量操作的执行速度也非常快。

4、EnumSet集合不允许加入null元素。如果试图插入null元素,EnumSet将抛出 NullPointerException异常。如果仅仅只是试图测试是否出现null元素、或删除null元素都不会抛出异常,只是删除操作将返回false,因为没有任何null元素被删除。

各个Set实现类的性能分析

1、HashSet和TreeSet是Set的两个典型实现,到底如何选择HashSet和TreeSet呢?HashSet的性能总是比TreeSet好,(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet.

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

3、EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素。

4、必须指出的是,Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。如果有多个线程同时访问一个Set集合,并且有超过一个线程修改了该Set集合,则必须手动保证该Set集合的同步性。通常可以通过Collections工具类的synchronizedSortedSet方法来包装该Set集合。此操作最好在创建时进行,以防止对Set集合的意外非同步访问。

List集合

  1. List判断两个对象相等只要通过equals()方法比较返回true即可。
  2. ArrayList和Vector是基于数组实现的List类。
  3. ArrayList是线程不安全的,所以ArrayList性能比Vector更高。
  4. Vector是线程安全的,即使这样也不推荐;推荐用Collections工具类将ArrayList包装成线程安全的。
  5. Stack继承了Vector,因此也非常古老的类了,不推荐使用,对栈的操作可以用ArrayDeque代替。
  6. 操作数组的工具类Arrays提供了asList(Object obj)方法,可以将数组转换为List集合,但这个List集合既不是ArrayList的实例,也不是Vector的实例,而是Arrays中的内部类ArrayList的实例。
  7. Arrays.ArraryList是一个固定长度的List集合,程序只能遍历该集合的元素,不能增加、删除。

Queue集合

PriorityQueue类

  1. PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按照队列元素的大小进行重写排序。
  2. 不允许插入null元素。
  3. 有自然排序和定制排序两种方式。

Deque接口与ArrayDeque类

  1. Deque接口是Queue接口的子接口;代表一个双端队列。
  2. ArrayDeque类是Deque接口基于数组实现的双端队列。
  3. 创建ArrayDeque可指定numElements参数,指定Object[]数组的长度,默认是16。
  4. ArrayDeque可以作为栈和队列使用。

LinkedList类

  1. LinkedList是List接口的实现类,因此可以根据索引来访问集合中的元素;还实现了Deque接口,可以当成双端队列使用。
  2. LinkedList内部以链表的形势来保存集合中的元素。

各线性表的性能分析

  1. LinkedList与ArrayList、ArrayDeque的实现机制完全不同,ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差;但在插入、删除元素时性能比较出色(只需要改变指针所指的地址即可)。需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能(而且实现机制也不好),所以各方面性能都比较差。

  2. 对于所有的内部基于数组的集合实现,例如ArrayList、ArrayDeque等,使用随机访问的性能比使用Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。

  3. Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型的实现;基于数组的线性表和基于链的线性表。Queue代表了队列,Deque代表了双端队列(既可作为队列使用,也可以作为栈使用),接下来对各种实现类的性能进行分析。

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

  5. 关于使用List集合有如下建议。

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

Map集合

  1. Map用于保存具体有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value,key和value都可以是任何引用类型的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。
  2. key和value之间存在单向一对一关系,即通过指定的key,总能找到唯一的,确定的value。从Map中取出数据时,只要给出指定的key,就可以取出对应的数据时。
  3. 如果把Map的两组值拆开来看,Map里的所有key组成了一个set集合(所有的key是没有顺序的,key与key之间不能重复)。
  4. 如果把Map里的所有value放在一起来看,它们类似于一个List,元素与元素之间可以重复,每个元素可以根据索引来查找,只是Map中的索引不再是整数值,而是以另一个对象作为所用。

HashMap和HashTable

  1. Hashtable是一个线程安全的Map实现,但是HashMap是线程不安全的实现,所以HashMap比Hashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。
  2. Hashtable不允许使用null作为key和value,如果视图把null值放进Hashtable中,将会引发NullPointerException异常;但是HashMap可以使用null作为key或value。由于HashMap里的key不能重复,所以HashMap里组多只有一个key-vlaue对的key为null,但可以有无数个key-value对的value为null。
  3. HashMap和Hashtable不能保证其中的key-value对的顺序;
  4. HashMap和Hashtable判断两个key相等的标准:两个key通过equals()方法比较返回true,两个key的hashCode值也相等;
  5. 二者判断两个value相等的标准:只要两个对象通过equals()方法比较返回true即可;
  6. 二者包含一个containsValue()方法,用于判断是否包含指定的value;

使用Properties读取属性文件

Properties类是Hashtable类的子类,Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中“属性名=属性值”加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型。

LinkedHashMap

  1. LinkedHashMap使用双向链表维护key-value对的次序(只考虑key的次序即可),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
  2. LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但它以链表来维护内部顺序,所有在迭代访问Map里的全部元素时将会有较好的性能。

SortedMap接口和TreeMap实现类

  1. TreeMap是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对时,需要根据key进行排序。TreeMap可以保证所有的key-value对处于有序状态。
  2. TreeMap集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的compareTo(Object obj)方法返回0。

WeakHashMap实现类

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

    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);
        //  {英文=中单, java=中等, 数学=及格, 语文=良好}

        //通知系统进行垃圾回收
        System.gc();
        System.runFinalization();
        //在通常情况下,将只看一个key-value对
        System.out.println(whm);
        // {java=中等}
    }
}
  1. 从上面运行结果可以看出,当系统进行垃圾回收时,删除了WeakHashMap对象的前三个key-value对。这是因为添加前三个key-value对(粗体字部分)时,这三个key都是匿名的字符串对象,WeakHashMap只保留了对它们的弱引用,这样垃圾回收时会自动删除这三个key-value对。

  2. WeakHashMap对象第4组的key-value对(①号粗体代码行)的key时一个字符串直接量,(系统将会自动保留对该字符串对象的强引用),所以垃圾回收时不会回收它。

  3. 如果需要使用WeakHashMap的key来保留对象的弱引用,则不要让该key所引用的对象具有任何强引用,否则将失去使用WeakHashMap的意义。

IdentityHashMap实现类

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

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

public class HashMapTest {

    public static void main(String[] args) {
        IdentityHashMap ihm = new IdentityHashMap();

        //下面两行代码将会向IdentityHashMap对象中添加两个key-value对
        ihm.put(new String("语文"),89);
        ihm.put(new String("语文"), 78);

        //下面两行代码只会向IdentityHashMap对象中添加也一个key-value对
        ihm.put("java", 93);
        ihm.put("java", 98);
        System.out.println(ihm);
        // {语文=89, java=98, 语文=78}
    }

}

上面试图向IdentityHashMap对象中添加4个key-value对,前两个key-value对中的key是新创建的字符串对象,它们通过 == 比较不相等,所以IdentityHashMap会把它们当成2个key来处理;后2个key-value对中的key都是字符串常量,而且它们的字符序列完全相同,Java使用常量池来管理字符串常量,所以它们通过 == 比较返回true,IdentityHashMap会认为它们是同一个key,因此只有一次可以添加成功。

EnumMap实现类

EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值,创建枚举类必须显式或隐式地指定它对应的枚举类。EnumMap有如下特征:

  1. EnumMap在内部以数组形式保存,所以这种形式非常紧凑、高效。
  2. EnumMap根据key的自然顺序(即枚举值在枚举类中定义顺序)来维护key-value对顺序。当程序通过keySet()、entrySet()、values()等方法遍历EnumMap时可以看到这种顺序。
  3. EnumMap不允许使用null作为key,但允许使用null作为value。如果试图使用null作为key时将抛出NullPointerException一次。
  4. 与创建普通Map有所区别的是,创建EnumMap必须指定一个枚举类,从而将该EnumMap和指定枚举类关联起来。
enum Season
{
    SPRING,
    SUMMER,
    FALL,
    WINTER
}

public class EnumMapTest {
    public static void main(String[] args) {
        //创建EnumMap对象,该EnumMap对象的所有key都是Season枚举类的枚举值
        EnumMap enumMap = new EnumMap(Season.class);

        enumMap.put(Season.SUMMER, "夏日炎炎");
        enumMap.put(Season.SPRING, "春暖花开");
        System.out.println(enumMap);
    }
}

各Map实现类的性能分析

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

  2. TreeMap通常比HashMap、Hashtable要慢(尤其是在插入、删除key-value对时更慢),因为TreeMap底层用红黑树来管理key-value对。

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

  4. 对于一般的场景,程序多考虑使用HashMap,因为HashMap正是为快速查找设计的。但程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。

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

  6. EnumMap性能最好,但它只能使用同一个枚举类的枚举值作为key。

posted on 2021-04-02 16:02  jiaotong  阅读(61)  评论(0)    收藏  举报