袁艺

个人站

欢迎来到我的个人站~


集合

[TOC]

Arraylist

集合是Java中提供的一种容器,可以用来存储多个数据。底层采用的数组结构

 ArrayList al=new ArrayList();//创建了一个长度为0的Object类型数组
 al.add("abc");
//底层会创建一个长度为10的Object数组 Object[] obj=new Object[10]
//obj[0]="abc"
//如果添加的元素的超过10个,底层会开辟一个1.5*10的长度的新数组
//把原数组中的元素拷贝到新数组,再把最后一个元素添加到新数组中

集合和数组都是容器,它们的区别?

  • 数组的长度是固定的,集合的长度是可变的
  • 集合中存储的元素必须是引用类型数据

Arraylist集合继承关系

Arraylist继承了抽象类AbstractList,实现了接口List,List接口继承了Collection接口。

                          Collection 接口     
                               |
 ----------------------------------------------------------------
 |                                                              |
List接口                                                       Set接口
 |                                                              |
  ----------------                                             -------------

 |              |                                             |            |

ArrayList类    LinkedList类                                 HashSet类     LinkedHashSet类

LinkedList

特点

底层采用链表结构,每次查询都要从链头或者链尾找起,查询相对数组较慢,但是删除直接修改元素记录的地址即可不需要移动大量的元素

LinkedList的索引决定从链头开始找还是从链尾开始找,如果该元素小于元素长度的一半则从链头开始找起,如果大于元素长度的一半则从链尾找起

LinkedList特有方法

查询慢,增删快

  • E removeFirst() 移除并返回链表的开头
  • E removeLast() 移除并返回链表的结尾
  • E getFirst() 获取链表的开头
  • E getLast() 获取链表的结尾
  • addFirst(E) 添加到链表的开头
  • addLast(E) 添加到链表的结尾

Vector

Vector集合存储的结构时数组结构,为jdk中最早提供的集合,它是线程同步的,vector中提供了一个独特的取出方式,就是枚举Enumeration(它其实是早期的迭代器)。

Collection集合中的方法

  1. Object[] toArray() 集合中的元素转成一个数组中的元素,即集合转成数组。返回的是一个存储对象的数组,数组的存储对象数据类型是object Java中三种长度表现形式: 数组.length 属性——》返回值 int 字符串.length() 方法——》返回值 int 集合.size() 方法——》返回值 int
  2. boolean contains(Object o) 判断对象是否存在于集合中,对象存在则返回true
  3. void clear() 清空集合中的所有元素,但集合容器本身依然存在
  4. boolean remove(Object o) 移除集合中指定的元素

Collections

Collections与Collection采用了装饰模式,为Collection提供了更多的装饰方法

  • Collections.shuffle方法 对List集合中的元素,进行随机排列
  • Collections.binarySearch方法 对List集合进行二分搜索,方法参数(传递List集合,传递被查找的元素)
  • Collections.sort方法 对于List集合,进行升序排列

迭代器

概述

Java中提供了很多集合,它们在存储元素时,采用的存储方式不同。我们取出这些集合中的元素,可以通过一种通用的方式来完成;

Collection集合元素的通用获取方式:在取元素之前先判断集合中是否有元素,如果有则把元素取出,循环执行,之至把集合中的所有元素全部取出。这种方式的专业术语成为迭代

每种集合的底层的数据结构不同,例如ArrayList是数组、LinkedList是链表,但无论哪种集合都会使用迭代

迭代器的实现原理

接口Iterator有两个抽象方法

  1. next()取出集合的下一个元素

Collection接口中定义了方法 Iterator iterator(),ArrayList重写 Iterator iterator()方法,返回了Iterator接口的实现类对象。 Iterator it =array.iterator(),运行结果就是Iterator接口的实现类的对象,it是Iterator接口的实现类对象,调用方法 hasNext 和 next 集合元素迭代

……  
//调用集合的方法iterator()获取出,Iterator接口的实现类的对象
Iterator<String> it = coll.iterator();
//接口实现类对象,调用方法hasNext()判断集合中是否有元素
//boolean b = it.hasNext();
//System.out.println(b);
//接口的实现类对象,调用方法next()取出集合中的元素
//String s = it.next();
//System.out.println(s);
      
//迭代是反复内容,使用循环实现,循环的条件,集合中没元素, hasNext()返回了false
while(it.hasNext()){
   String s = it.next();
   System.out.println(s);
}
……

迭代器的执行过程

 private class Itr implements Iterator<E> {
        int cursor;       //下一个元素的索引值
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;

        // prevent creating a synthetic constructor
        Itr() {}
		//cursor记录的索引值不等于集合的长度返回true
        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

集合迭代中的转型

  • 在使用集合时需要注意:
    • 集合中存储的其实都是对象的地址
    • 集合中可以存储基本数值了(JDK1.5之后)。因为出席拿了基本类型的包装类,提供了自动装箱操作,这样集合中的元素就是基本数值的包装类对象。
  • 存储时元素自动提升为Object,取出时要使用元素的特有内容,必须要向下转型。注意:如果集合中存放的是多个对象,这时向下转型会发生类型转换异常
  • Iterator接口可以使用<>来控制迭代元素的类型,当使用Iterator控制元素类型后,就不需要强转了。获取到的元素直接就是String类型

迭代器的并发修改异常

java.util.ConcurrentModificationException就是在遍历的过程中,使用了集合方法修改了集合的长度,不允许的

 public class ListDemo1 {  
   public static void main(String[] args) {    
     List<String> list = new ArrayList<String>();
     list.add("abc1");    
     list.add("abc2");    
     list.add("abc3");    
     list.add("abc4");        
     //对集合使用迭代器进行获取,获取时候判断集合中是否存在 "abc3"对象    
     //如果有,添加一个元素 "ABC3"    
     Iterator<String> it = list.iterator();
     while(it.hasNext()){      
       String s = it.next();      
       //对获取出的元素s,进行判断,是不是有"abc3" 
       if(s.equals("abc3")){        
         list.add("ABC3");      
       }      
       System.out.println(s);    
     }  
   } 
 } 

运行上述代码发生了错误 java.util.ConcurrentModificationException这是什么原因呢?  

在迭代过程中,使用了集合的方法对元素进行操作。   导致迭代器并不知道集合中的变化,容易引发数据的不确定性。  

并发修改异常解决办法:   在迭代时,不要使用集合的方法操作元素。   或者通过ListIterator迭代器操作元素是可以的,ListIterator的出现,解决了使用Iterator迭代过程中可能会发生的错误情况。

泛型

定义和使用

JDK1.5 出现新的安全机制,保证程序的安全性,泛型: 指明了集合中存储数据的类型 <数据类型>。

java中的伪泛型

泛型只在编译时存在,编译之后就被擦除了,在编译之前我们可以限制集合的类型,例如:

编译前:ArrayList al=new ArrayList();

编译后:ArrayList al=new ArrayList();

泛型类

  • 定义格式: 修饰符 class 类名<代表泛型的变量> { } ​

      例如API中的ArrayList集合
      class ArrayList<E>{ 
           public boolean add(E e){ }
        public E get(int index){  }
      }
    
  • 使用格式:创建对象时,确定泛型的类型 ​

      例如ArrayList<String> list = new ArrayList<String>();
      此时变量E的值就是String类型
      class ArrayList<String>{ 
        public boolean add(String e){ }
        public String get(int index){  }
      }
    

泛型的方法

  • 定义格式:修饰符 <代表泛型的变量> 返回值类型 方法名(参数){ } ​

    例如API中的ArrayList集合中的方法
      public <T> T[] toArray(T[] a){  } 
      //该方法,用来把集合元素存储到指定数据类型的数组中,返回已存储集合元素的数组
    
  • 使用格式:调用方法时,确定泛型的类型 ​

    例如:
          ArrayList<String> list = new ArrayList<String>();
          String[] arr = new String[100];
          String[] result = list.toArray(arr);
       此时变量T的值就是String类型变量T可以与定义集合的泛型不同
       public <String> String[] toArray(String[] a){  } 
    

泛型的好处

  • 将运行时期的ClassCastException转移到了编译时期,变成了编译失败

  • 避免了类型强转的麻烦

  • 泛型的通配,匹配所有的数据类型 ? <?> ​

        public static void main(String[] args) {
          //创建3个集合对象
          ArrayList<ChuShi> cs = new ArrayList<ChuShi>();
          ArrayList<FuWuYuan> fwy = new ArrayList<FuWuYuan>();
          ArrayList<JingLi> jl = new ArrayList<JingLi>();
                 
          //每个集合存储自己的元素
          cs.add(new ChuShi("张三", "后厨001"));
          cs.add(new ChuShi("李四", "后厨002"));
                 
          fwy.add(new FuWuYuan("翠花", "服务部001"));
          fwy.add(new FuWuYuan("酸菜", "服务部002"));
                 
          jl.add(new JingLi("小名", "董事会001", 123456789.32));
          jl.add(new JingLi("小强", "董事会002", 123456789.33));
    
       //   ArrayList<String> arrayString = new ArrayList<String>();
    
          iterator(jl);
          iterator(fwy);
          iterator(cs);
               
        }
        /*
    * 定义方法,可以同时遍历3集合,遍历三个集合的同时,可以调用工作方法 work
    * ? 通配符,迭代器it.next()方法取出来的是Object类型,怎么调用work方法
    * 强制转换:  it.next()=Object o ==> Employee
    * 方法参数: 控制,可以传递Employee对象,也可以传递Employee的子类的对象
    * 泛型的限定  本案例,父类固定Employee,但是子类可以无限?
    *   ? extends Employee 限制的是父类, 上限限定, 可以传递Employee,传递他的子类对象
    *   ? super   Employee 限制的是子类, 下限限定, 可以传递Employee,传递他的父类对象
    */
    
      public static void iterator(ArrayList<? extends Employee> array){
    

    Iterator<? extends Employee> it = array.iterator(); while(it.hasNext()){ //获取出的next() 数据类型,是什么Employee Employee e = it.next(); e.work(); } }


### List接口

#### 特点

1. 是一个元素存取有序的集合
2. 是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)
3. 集合中可以有重复的元素,通过元素的equals方法,来比较是否为重复元素
4. 常用子接口
   1. ArrayList集合
   2.  LinkedList集合

#### List接口特有的方法

- 增加元素方法
  -  add(Object e):向集合末尾处,添加指定的元素 
  - add(int index, Object e)   向集合指定索引处,添加指定的元素,原有元素依次后移**需要注意索引越界的问题**
- 删除元素方法
  - remove(Object e):将指定元素对象,从集合中删除,返回值为被删除的元素
  - remove(int index):将指定索引处的元素,从集合中删除,返回值为被删除的元素
- 替换元素方法
  - set(int index, Object e):将指定索引处的元素,替换成指定的元素,返回值为替换前的元素
- 查询元素方法
  - get(int index):获取指定索引处的元素,并返回该元素

### Set

#### 特点

- 不包含重复元素的集合,没有索引
- set集合取出元素的方式可以采用迭代器或者增强for循环
- set集合有多个子类,比如:HashSet、LinkedHashSet

### HashSet

- 底层数据结构——哈希表
- 存储、取出速度都比较快
- 线程不安全,运行速度快

### LinkedHashSet

- 基于链表的哈希表实现
- 继承HashSet
- 具有顺序,存储和取出的顺序相同
- 线程不安全的集合,运行速度快

### 哈希表

#### 哈希表的数据结构

![数据结构](https://raw.githubusercontent.com/yuanyi0510/yuanyi0510.github.io/master/images/bolg_images/%E9%9B%86%E5%90%88/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%9A%84%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%84.JPG)

加载因子:表中填入的记录数/哈希表的长度。例如:加载因子是0.75 代表:数组中的16个位置,其中存入了12个元素。

如果在存入第十三个元素,导致存储链子过仓,会降低哈希表的性能,那么此时会扩充哈希表,底层会开辟一个长度为原长度**两倍**的数组,把老元素拷贝到新数组中,在把新元素添加到数组中

**当存入元素数量>哈希表长度*加载因子,就要扩容,因此加载因子决定扩容时机**

#### 字符串对象的哈希值

对象的哈希值,普通的十进制整数,父类Object的方法public int hashCode() 计算结果int整数

string类重写了hashCode方法

​```java
 public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            hash = h = isLatin1() ? StringLatin1.hashCode(value)
                                  : StringUTF16.hashCode(value);
        }
        return h;
    }

哈希表的存储原理

每存入一个新的元素都要走一下三步:

  1. 首先调用本类的hashCod()方法算出哈希值
  2. 在容器中找是否有与新元素哈希值相同的老元素,如果没有直接存入,如果有跳转到第三步
  3. 新元素会与该索引下的老元素利用equals方法一一对比,一旦新元素.equals(老元素)返回true,停止对比,说明重复,不再存入。如果与该索引位置下的老元素对比后返回false,说明没有重复,存入

ArrayList,HashSet判断对象是否重复的原因

  1. ArrayList的contains方法原理:底层依赖于equals方法。ArrayList调用contains方法会调用equals方法,传入的元素的equals方法依次与集合中的旧元素比较,从而根据返回的布尔值判断是否有重复的元素。此时,当ArrayList存放自定义的类型时,由于自定义类型在未重写equals方法前,判断是否重复的依据是地址值,所以如果想根据内容判断是否为重复元素,需要重写元素的equals方法

  2. b:HashSet的add()方法和contains方法()底层都依赖 hashCode()方法与equals方法() Set集合不能存放重复的元素,其添加方法在添加时会判断是否有重复元素,有重复的不添加,没有重复的则添加。 HashSet集合由于时无序的,其判断唯一的依据是元素类型的hashCode与equals方法的返回结果,规则如下:

    • 先判断新元素与集合内已经有的旧元素的HashCode值。
    • 如果不同,说明是不同元素则添加到集合;
    • 如果相同,在判断equals的比较结果,返回true则为相同元素,返回false则为不同元素添加到集合

    所以,使用HashSet存储自定义类型,如果没有重写该类的hashcode与equals方法则判断重复时使用的是地址,如果想通过内容比较元素是否相同,需要重写该元素类的hashcode与equals方法。

hashCode和equals方法的面试题

  1. 两个对象 Person p1 p2 问题: 如果两个对象的哈希值相同 p1.hashCode()==p2.hashCode(),两个对象的equals一定返回true吗 ?p1.equals(p2) 一定是true吗? 正确答案:不一定。
  2. 如果两个对象的equals方法返回true,p1.equals(p2)==true,两个对象的哈希值一定相同吗? 正确答案:一定

解析:在Java应用程序执行期间,如果根据equals方法两个对象是相等的,那么对这两个对象中的每个对象的hash值都必须一样;如果根据equals方法,两个对象不相等,那么对这两个对象中的任意一个对象上调用hashCode方法不要求一定生成不同的整数结果。

两个对象不同(对象属性值不同) equals返回false=====>两个对象调用hashCode()方法哈希值相同

两个对象调用hashCode()方法哈希值不同=====>equals返回true

两个对象不同(对象属性值不同) equals返回false=====>两个对象调用hashCode()方法哈希值不同

两个对象调用hashCode()方法哈希值相同=====>equals返回true

所以说,两个对象的哈希值无论相同还是不同,equals都可能返回true

Map

概述

map接口下的集合与Collection接口下的集合,他们存储数据的形式不同

  1. Collection中的集合,元素是孤立存在的,像集合中存储元素采用一个个元素的方式存储
  2. map中的集合,元素是成对存在的。每个元素由键值两部分组成,通过键可以找到对应的值
  3. Collection中的集合称为单列集合,Map中的集合称为双列集合。
  4. 需要注意的是,Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

常用方法

  • V remove(K) 移除集合中的键值对,返回被移除之前的值

  • V get(K) 通过键对象,获取值对象

  • V put(K,V) K 作为键的对象, V作为值的对象。将键值对存储到集合中

    • 存储的是重复的键,将原有的值,覆盖
    • 返回值一般情况下返回null,
    • 存储重复键的时候,返回被覆盖之前的值
  • 遍历方法keySet方法

    • 获取Map集合中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键
    • 遍历键的Set集合,得到每一个键
    • 根据键利用get(key)去Map找所对应的值
    /*
      *  Map集合的遍历
      *    利用键获取值
      *    Map接口中定义方法keySet
      *    所有的键,存储到Set集合
      */
     public class MapDemo1 {
     	public static void main(String[] args) {
     		/*
     		 *  1. 调用map集合的方法keySet,所有的键存储到Set集合中
     		 *  2. 遍历Set集合,获取出Set集合中的所有元素 (Map中的键)
     		 *  3. 调用map集合方法get,通过键获取到值
     		 */
     		Map<String,Integer> map = new HashMap<String,Integer>();
     		map.put("a", 11);
     		map.put("b", 12);
     		map.put("c", 13);
     		map.put("d", 14);
     		//方法1
     		//1. 调用map集合的方法keySet,所有的键存储到Set集合中
     		Set<String> set = map.keySet();
     		//2. 遍历Set集合,获取出Set集合中的所有元素 (Map中的键)
     		Iterator<String> it = set.iterator();
     		while(it.hasNext()){
     			//it.next返回是Set集合元素,也就是Map中的键
     			//3. 调用map集合方法get,通过键获取到值
     			String key = it.next();
     			Integer value = map.get(key);
     			System.out.println(key+"...."+value);
     		}
     		//方法2
     		System.out.println("=======================");
           		for(String key : map.keySet()){
     			Integer value = map.get(key);
     			System.out.println(key+"...."+value);
     		}
     	}
     }
    
  • Map集合Entry对象

    • 在Map类设计时,提供了一个嵌套接口:Entry。
  • Entry将键值对的对应关系封装成了对象即键值对对象,这样我们在遍历Map集合时,就可以从每一个键值对(Entry)对象中获取对应的键与对应的值。

   interface Map{
   	//Entry是Map的一个内部接口
      //由Map的子类的内部类实现
     interface Entry<K, V> {
         
          K getKey();//获取Entry对象中的键
          V getValue();//获取Entry对象中的值
          V setValue(V value);
          boolean equals(Object o);
          int hashCode();
          public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K, V>> comparingByKey() {
              return (Comparator<Map.Entry<K, V>> & Serializable)
                  (c1, c2) -> c1.getKey().compareTo(c2.getKey());
          }
          public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K, V>> comparingByValue() {
              return (Comparator<Map.Entry<K, V>> & Serializable)
                  (c1, c2) -> c1.getValue().compareTo(c2.getValue());
          }
          public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
              Objects.requireNonNull(cmp);
              return (Comparator<Map.Entry<K, V>> & Serializable)
                  (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
          }
          public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
              Objects.requireNonNull(cmp);
              return (Comparator<Map.Entry<K, V>> & Serializable)
                  (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
          }
      }
   }
  • Map集合遍历方式entrySet方法 Map集合获取方式,entrySet方法,键值对映射关系(结婚证)获取 实现步骤:

    1. 调用map集合方法entrySet()将集合中的映射关系对象,存储到Set集合Set<Entry <K,V> >
    2. 迭代Set集合
    3. 获取出的Set集合的元素,是映射关系对象
    4. 通过映射关系对象方法 getKet, getValue获取键值对

    ​ 创建内部类对象 外部类.内部类 = new

    public static void main(String[] args) {
    		Map<Integer,String> map = new HashMap<Integer, String>();
    		map.put(1, "abc");
    		map.put(2, "bcd");
    		map.put(3, "cde");
    		//1. 调用map集合方法entrySet()将集合中的映射关系对象,存储到Set集合
    		Set<Map.Entry <Integer,String> >  set = map.entrySet();
    		//2. 迭代Set集合
    		Iterator<Map.Entry <Integer,String> > it = set.iterator();
    		while(it.hasNext()){
    			//  3. 获取出的Set集合的元素,是映射关系对象
    			// it.next 获取的是什么对象,也是Map.Entry对象
    			Map.Entry<Integer, String> entry = it.next();
    			//4. 通过映射关系对象方法 getKet, getValue获取键值对
    			Integer key = entry.getKey();
    			String value = entry.getValue();
    			System.out.println(key+"...."+value);
    		}
      	}
    }
    

HashMap

LinkedHashMap

特点:保证迭代顺序

HashTable

特点

  • 底层数据结构为哈希表,特点和hashmap一样
  • Hashtable是线程安全的集合,运行速度慢
  • Hashtable命运和Vector是一样的,从JDK1.2开始,被更先进的HashMap取代
  • Hashtable 不允许存储null值,null键;HashMap 允许存储null值,null键