【JavaSE】集合

Java集合


2019-07-05  12:39:09  by冲冲

1. 集合的由来

通常情况下,程序直到运行时,才知道需要创建多少个对象。但在开发阶段,我们根本不知道到底需要多少个数量的对象,甚至不知道它的准确类型。为了满足这些常规的编程需要,我们要求能在任何时候,任何地点创建任意数量的对象,而这些对象用什么来容纳呢?我们首先想到了数组,但是数组只能放统一类型的数据,而且其长度是固定的,那该怎么办呢?集合便应运而生了!Java集合框架为程序员提供了预先包装的数据结构和算法来操纵他们。

2. 集合的概念

Java集合类,存放于 java.util 包中,是一个用来存放对象的容器。

集合只能存放对象。比如你存一个 int 型数据 1 放入集合中,其实它是自动转换成 Integer 类后存入的,Java中每一种基本类型都有对应的引用类型。

② 集合存放的是对象的引用,对象本身还是放在堆内存中。

③ 集合可以存放不同类型、不限数量的对象。

④ 任何对象加入集合类后,自动转变为Object类型,所以在取出的时候,需要进行强制类型转换。(解决方案:使用泛型

3. 集合的框架

阅读Java集合框架图,不难发现:

① 所有的集合类(除Map系列)都实现了 Iterator 接口。 Iterator 接口定义了遍历集合元素的方法,主要是 hasNext(),next(),remove()三种方法。它的子接口 ListIterator 在它的基础上又添加三种方法,分别是 add(),previous(),hasPrevious()。也就是说如果实现 Iterator 接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会再被遍历到,通常无序集合实现的都是这个接口,比如HashSet;而那些元素有序的集合,实现的一般都是 ListIterator接口,实现这个接口的集合可以双向遍历,既可以通过next()访问下一个元素,又可以通过previous()访问前一个元素,比如ArrayList。

② 在Java 1.2之前,Java提供特设类,有 Dictionary, Vector, Stack, Properties 这些类用来存储和操作对象组。虽然这些类都非常有用,但是它们缺少一个核心的统一主题。由于这个原因,使用 Vector 类的方式和使用 Properties 类的方式有着很大不同。对此,当前整个Java集合框架都围绕一组标准接口来设计。你可以直接使用这些接口的标准实现(ArrayList、HashSet等等)。当然,除此之外你也可以通过这些接口来实现满足自己需求的集合。

③ 抽象类的意义。如果要自己实现一个集合类,去实现那些抽象的接口会非常麻烦,工作量很大。这个时候就可以使用抽象类,这些抽象类中给我们提供了许多现成的实现,我们只需要根据自己的需求重写一些方法或者添加一些方法就可以实现自己需要的集合类,工作量大大降低。

④ 用于学习背诵的集合框架图

4. 集合的详解

(1)Iterator接口

Iterator 迭代器,它是Java集合的顶层接口(不包括 map 系列的集合,Map接口是 map 系列集合的顶层接口)。因此,除了 map 系列的集合,我们都能通过迭代器来对集合中的元素进行遍历。

Iterator 接口定义的三个方法有:
① Object next():返回迭代器刚越过的元素的引用,返回值是 Object,需要强制转换成自己需要的类型
② boolean hasNext():判断容器内是否还有可供访问的元素
③ void remove():删除迭代器刚越过的元素

 追溯到Collection接口,发现其继承的是Iterable接口。

Iterable接口与Iterator接口的关系,发现Iterable接口封装了Iterator接口。

 1 public class Collection {     //测试Iterable接口的遍历
 2     @SuppressWarnings({ "rawtypes", "unchecked" })
 3     public static void main(String[] args) {
 4         // 产生一个 List 集合,典型实现为 ArrayList。
 5         List list = new ArrayList();
 6         // 添加三个元素
 7         list.add("黑奴");
 8         list.add("肥猪");
 9         list.add("渣狗");
10         list.add("憨熊");
11         // 构造 List 的迭代器
12         Iterator it = list.iterator();
13         // 通过迭代器遍历元素
14         while (it.hasNext()) {
15             Object obj = it.next();
16             System.out.println(obj);
17         }
18     }
19 }

 输出结果:

1 黑奴
2 肥猪
3 渣狗
4 憨熊

(2)Collection接口

源码: public interface Collection<E> extends Iterable<E> {...}

 1 public class Collection {
 2     public static void main(String[] args) {
 3         // 将 ArrayList集合作为 Collection的实现类
 4         Collection collection = new ArrayList();
 5 
 6         // 添加元素
 7         collection.add("A");
 8         collection.add("B");
 9 
10         // 删除指定元素
11         collection.remove("A");
12         // 删除所有元素
13 
14         // 检测是否存在某个元素
15         collection.removeAll(c);
16         collection.add("C");
17         collection.contains("C");
18 
19         // 判断是否为空
20         collection.isEmpty();
21 
22         // 利用增强for循环遍历集合
23         for (Object obj : collection) {
24             System.out.println(obj);
25         }
26         
27         // 利用迭代器 Iterator遍历集合
28         Iterator iterator = collection.iterator();
29         while (iterator.hasNext()) {
30             Object obj = iterator.next();
31             System.out.println(obj);
32         }
33     }
34 }

(3)List接口:有序、元素可以重复

源码:public interface List<E> extends Collection<E> {...}

由于 List 接口是继承于 Collection 接口,所以基本的方法如上所示。
void add(Object obj) ② void remove(Object obj) ③ void removeAll() ④ boolean contains(Object obj) ⑤ boolean isEmpty()
List 接口的三个典型实现:
① List list1 = new ArrayList();  //底层数据结构是数组,查询快,增删慢;线程不安全,效率高。动态增长为原来的1.5倍。
② List list2 = new Vector();     //底层数据结构是数组,查询快,增删慢;线程安全,效率低,几乎已经淘汰了这个集合。动态增长为原来2倍。
③ List list3 = new LinkedList(); //底层数据结构是链表,查询慢,增删快;线程不安全,效率高。允许元素为null。

理解:
① 数组就像一排已经编号站好的人,要找出第10个人很容易,根据编号就能找到。但插入、删除慢,往某个位置插入或删除一个人时,后面的人身上的编号都要变。当然,加入或删除的人是在末尾的话就快。
② 链表就像手牵着手站成一圈的人,要找第10个人不容易,必须从第一个人一个个数过去。但插入、删除快。插入时只要解开两个人的手,并重新牵上新加进来的人的手就可以。删除一样的道理。
public class Collection {  
//List接口其他方法:指定位置插入元素,替换元素,遍历还可以使用普通for循环。
public static void main(String[] args) { // 产生一个 List 集合,典型实现为 ArrayList List list = new ArrayList(); // 在指定地方添加元素 list.add(2,"A"); // 在指定地方替换元素 list.set(2, "a"); // 获得指定对象的索引 int i = list.indexOf("a"); System.out.println("索引为:" + i); // 遍历:普通for循环 for (int j = 0; j < list.size(); j++) { System.out.println(list.get(j)); } } }
 1 // ArrayList的三种遍历
 2 
 3 public class Collection {
 4     public static void main(String[] args) {
 5         List<String> list = new ArrayList<String>();
 6         list.add("AA");
 7         list.add("BB");
 8         list.add("CC");
 9         
10         // 第一种遍历:使用for-each
11         for (String str : list) { // 等价于for(int i=0;i<list.size();i++)
12             System.out.println(str);
13         }
14 
15         // 第二种遍历:把线性表表变为数组
16         String[] strArray = new String[list.size()];
17         list.toArray(strArray);
18         for (int i = 0; i < strArray.length; i++) //等价于 for(String str:strArray)
19         {
20             System.out.println(strArray[i]);
21         }
22 
23         // 第三种遍历:使用迭代器
24         Iterator<String> it = list.iterator();
25         while (it.hasNext())  // 判断下一个元素之后有值
26         {
27             System.out.println(it.next()); //输出当前越过的元素
28         }
29     }
30

(4)Set接口:无序、元素不能重复

Set 接口的三个典型实现:
① Set hashSet = new HashSet();  //无序,元素不重复。底层是哈希表。元素可以是null,但是只能一个null。
② Set linkedHashSet = new LinkedHashSet();  //有序,元素不重复。底层是哈希表和链表,哈希表保证元素唯一性,链表保证元素有序插入。
③ Set treeSet = new TreeSet();  //有序,元素不重复。底层是红黑树。元素不能是null。默认进行排序(升序),所以元素必须相同类型。
(4.1)HashSet
HashSet的底层是哈希表,本质是一个数组,存在的意义是加快查询速度。 原因:通常元素在数组中的索引位置是随机的,即元素的取值和元素的位置之间不存在确定的关系。 因此,在数组中查找特定值时,需要把查找值和其他元素进行比较,此时的查询效率取决于查找过程中比较的次数 n 。 而 HashSet 集合底层数组的索引和值存在一个确定的关系:index
=hash(value)。我们只需要调用这个公式,就能快速的找到元素或者索引。 对于 HashSet,如果两个对象通过 equals()方法返回 true,这两个对象的 hashCode 值也应该相同。 原因:当向HashSet集合中存入一个元素时,HashSet会先调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据hashCode值决定该对象在HashSet中的存储位置。 如果 hashCode 值不同,直接把该元素存储到 hashCode()指定的位置。 如果 hashCode 值相同,那么会继续判断该元素和集合对象的 equals()作比较。 当 hashCode 相同,equals()为 true,则视为两者同一个对象,所以不保存在 hashSet 中。 当 hashCode 相同,equals()为 false,则存储在之前对象同槽位的链表上,但是这会非常麻烦,我们应该约束这种情况,
即保证:如果两个对象通过 equals()方法返回
true,这两个对象的 hashCode 值也应该相同。 因此,每一个存储到哈希表中的对象,都得提供 hashCode()和 equals()方法的实现,用来判断是否是同一个对象。 对于 HashSet 集合,我们要保证如果两个对象通过 equals()方法返回 true,这两个对象的 hashCode 值也应该相同。
(4.2)TreeSet
① 必须放入同类的对象。因为默认会进行排序,否则可能会发生类型转换异常.我们可以使用泛型来进行限制。
Set treeSet = new TreeSet(); treeSet.add(1); //添加一个 Integer 类型的数据 treeSet.add("a"); //添加一个 String 类型的数据 System.out.println(treeSet); //会报类型转换异常的错误
 如果使用 TreeSet() 无参数的构造器创建一个 TreeSet 对象, 则要求放入其中的元素的类必须实现 Comparable 接口,所以在其中不能放入null元素。
TreeSet的排序:
① 自动排序:添加自定义对象的时候,必须要实现 Comparable 接口,并要覆盖 compareTo(Object obj) 方法来自定义比较规则。
如果 this > obj,返回正数 1;
如果 this < obj,返回负数 -1;
如果 this = obj,返回 0 ,则认为这两个对象相等。
其中,数值类型比较数值大小,char和string类型比较unicode值的数值大小。
 1 ② 定制排序: 当需要把一个对象放入 TreeSet 中,需要重写该对象对应的 equals() 方法,并保证该方法与 compareTo(Object obj) 方法有一致的结果。
 2 
 3 public class TreeSetTest {
 4     public static void main(String[] args) {
 5         Person p1 = new Person(1);
 6         Person p2 = new Person(2);
 7         Person p3 = new Person(3);
 8          
 9         Set<Person> set = new TreeSet<>(new Person());
10         set.add(p1);
11         set.add(p2);
12         set.add(p3);
13         System.out.println(set);  //结果为[1, 2, 3]
14     }
15 }
16  
17 class Person implements Comparator<Person>{
18     public int age;
19     public Person(){}
20     public Person(int age){
21         this.age = age;
22     }
23     @Override
24     /***
25      * 根据年龄大小进行排序
26      */
27     public int compare(Person o1, Person o2) {
28         // TODO Auto-generated method stub
29         if(o1.age > o2.age){
30             return 1;
31         }else if(o1.age < o2.age){
32             return -1;
33         }else{
34             return 0;
35         }
36     }
37      
38     @Override
39     public String toString() {
40         // TODO Auto-generated method stub
41         return ""+this.age;
42     }
43 }
三个 Set 接口的实现类比较:
共同点: ① 都不允许元素重复。 ② 都不是线程安全的。解决办法:Set set = Collections.synchronizedSet(set 对象); 不同点: ① HashSet:不保证元素的添加顺序,底层采用哈希表算法,查询效率高。判断两个元素是否相等,equals()方法返回true且hashCode()值相等。因此,存入HashSet中的元素要覆盖equals()方法和hashCode()方法。 ② LinkedHashSet:HashSet 的子类,底层采用哈希表算法以及链表算法,既保证了元素的添加顺序,也保证了查询效率。但是整体性能要低于 HashSet。  ③ TreeSet:不保证元素的添加顺序,但是会对集合中的元素进行排序。底层采用 红-黑树算法(树结构比较适合范围查询)。
(5)Map接口:key-value 类型的键值对,key不允许重复,value可以重复

① 严格来说 Map 并不是一个集合,而是两个集合之间的映射关系。

② 两个集合之间通过构成映射关系的两个数据,可以看作是map的一条数据。即 Entry( key,value ) 。Map 可以看成是由多个 Entry 组成。

③ 因为 Map 集合即没有实现于 Collection 接口,也没有实现 Iterable 接口,所以不能对 Map 集合进行 for-each 遍历。

 1 public class Collection {
 2     public static void main(String[] args) {
 3         Map<String, Object> hashMap = new HashMap<>();
 4         // 添加元素到Map中
 5         hashMap.put("key1", "value1");
 6         hashMap.put("key2", "value2");
 7         hashMap.put("key3", "value3");
 8         hashMap.put("key4", "value4");
 9         hashMap.put("key5", "value5");
10 
11         // 删除Map中的元素,通过key的值
12         hashMap.remove("key1");
13         // 通过get(key)得到Map中的value
14         Object str1 = hashMap.get("key2");
15 
16         // 可以通过添加方法来修改Map中的元素
17         hashMap.put("key2", "value22");
18 
19         // 第一种遍历:通过map.values()得到value集合。缺点是不能遍历key。
20         java.util.Collection<Object> value = hashMap.values();
21         for (Object obj : value) {
22             System.out.println(obj);
23         }
24 
25         // 第二种遍历:通过map.keySet()得到key集合,然后通过get(key)得到value。
26         Set<String> set = hashMap.keySet();
27         for (String str : set) {
28             System.out.println(str + "=" + hashMap.get(str));
29         }
30 
31         // 第三种遍历:通过Map.entrySet()得到 Map的Entry集合,然后遍历。(推荐)
32         Set<Map.Entry<String, Object>> entrys = hashMap.entrySet();
33         for (Map.Entry<String, Object> entry : entrys) {
34             String key = entry.getKey();
35             Object value2 = entry.getValue();
36             System.out.println(key + "=" + value2);
37         }
38 
39         // 第四种遍历:通过Map.entrySet使用iterator遍历key和value。
40         Iterator<Entry<String, Object>> it = hashMap.entrySet().iterator();
41         while (it.hasNext()) {
42             Entry<String, Object> entry = it.next();
43             System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
44         }
45 
46         System.out.println(hashMap);
47     }
48 }

输出结果:

value22
value5
value3
value4
----------------
key2=value22
key5=value5
key3=value3
key4=value4
----------------
key2=value22
key5=value5
key3=value3
key4=value4
----------------
key= key2 and value= value22
key= key5 and value= value5
key= key3 and value= value3
key= key4 and value= value4
----------------
{key2=value22, key5=value5, key3=value3, key4=value4}
Map接口的常用实现类:
① HashMap:底层是哈希表。特点是key是无序插入,且key不能重复。key判断重复标准:key1和key2是否equals为true且hashCode相等。非线程安全。
② TreeMap:底层是红黑树。特点是key是无序插入,但是会自动升序排序,且key不能重复。key判断重复标准是:compareTo/compare的返回值是0。非线程安全。
③ LinkedHashMap:底层是链表和哈希表。特点是按添加的先后顺序进行有序插入,且key不能重复。key判断重复标准同HashMap。非线程安全。
④ HashTable:底层是哈希表,是HashMap的前身(类似于Vector是ArrayList的前身)。打死都不要用。线程安全。
⑤ Properties:HashTable的子类,要求键、值都为String类型。用来加载资源文件(.properties类型文件)。

注意:
① 定义Map时,key通常使用不可变类string,把key作为value的唯一值。
② HashMap、TreeMap、LinkedHashMap都是非线程安全的,但是性能高。解决方案:Map map = Collections.synchronizedMap( Map对象 );
③ HashTable线程安全,但是性能低。
Map 和 Set 集合的关系:
① 都有几个类型的集合。HashMap和HashSet的底层都是哈希表,TreeMap和TreeSet的底层都是红-黑树,LinkedHashMap和LinkedHashSet的底层都是哈希表和红-黑树。
② 分析 Set 的底层源码,我们可以看到,Set 集合就是由Map集合的Key组成。



参考:

https://www.cnblogs.com/ysocean/p/6555373.html

https://www.runoob.com/java/java-collections.html

原文地址:https://www.cnblogs.com/yadiel-cc/p/11135543.html