什么是java容器
-
集合容器主要⽤于保存对象,主要分类有三种List 、Set、Map
- List 有序 可重复的 集合
- 常⻅的List有(线程不安全)ArrayList、LinkedList ;(线程安全) Vector、 CopyOnWriteArrayList等类,
- Set 无序,不可重复 的集合
- 常见的Set的子类有 HashSet,LinkedSet,TreeSet 等,
- Map key-value 键值对
- HashMap,TreeMap,Hashtable,LinkedHashMap 等,
-
它们与Collection的关系
Collection 接口
- 常用方法
方法名 | 说明 |
---|---|
boolean add(E e) | 向集合添加元素e,若指定集合元素改变了则返回true |
boolean addAll(Collection<? extends E> c) | 把集合C中的元素全部添加到集合中,若指定集合元素改变返回true |
void clear() | 清空所有集合元素 |
boolean contains(Object o) | 判断指定集合是否包含对象o |
boolean containsAll(Collection<?> c) | 判断指定集合是否包含集合c的所有元素 |
boolean isEmpty() | 判断指定集合的元素size是否为0 |
boolean remove(Object o) | 删除集合中的元素对象o,若集合有多个o元素,则只会删除第一个元素 |
boolean removeAll(Collection<?> c) | 删除指定集合包含集合c的元素 |
boolean retainAll(Collection<?> c) | 从指定集合中保留包含集合c的元素,其他元素则删除 |
int size() | 集合的元素个数 |
T[] toArray(T[] a) | 将集合转换为T类型的数组 |
Collection 接口方法跟后面 List ,Set 差不多;
List 接口
List 接口 的实现类,可以理解为一个自动扩容的数组,你需要几个元素空间,就动态开辟多少空间;
添加到List集合中的第一个数据下标为0,第二个为1····· ,这个跟数组类似
List实现类介绍
ArrayList
ArrayList底层为数组,所以拥有数组的特性,实现了可变数组的大小
对集合中的元素进行快速的随机访问(查看,修改)
ArrayList 中插入与删除元素的速度相对较慢。
ArrayList 里包含3个构造方法
ArrayList 每次扩容为原来的1.5倍;扩容时机:add元素,并且元素个数超过容量时
ArrayList 容量最大为整数的最大值,Integer.MAX_VALUE
-
/** * 构造一个初始容量为0的空列表 * 当第一次add时,会构造一个容量为10的空列表,也就是数组 */ public ArrayList() { } /** * 构造具有指定初始容量的空列表 */ public ArrayList(int initialCapacity) { } /** * 按照集合迭代器返回的顺序,构造一个包含指定集合元素的列表 * 可以理解为把一个集合的元素赋给构造的集合 */ public ArrayList(Collection<? extends E> c) { }
LinkedList
基于的数据结构是链表,⼀个双向链表,链表数据结构的特点是每个元素分配的空间不
必连续
插⼊和删除元素时速度⾮常快,但访问元素的速度较慢
List 常用方法
public int size() ; | 返回容器元素个数 |
---|---|
public boolean isEmpty(); | 判断是否为空 |
public boolean contains(Object o) ; | 是否包含(o), 如果是引用类型,调用equals( )进行比较 |
public int indexOf(Object o); | 返回列表中指定元素第一次出现的索引,没找到,返回-1 |
public int lastIndexOf(Object o); | 返回列表中指定元素最后一次出现的索引,没找到,返回-1 |
public Object[] toArray(); | 返回一个数组,元素相同,顺序相同 |
public T[] toArray(T[] a); | 参数a为一个数组,若传入的数组类型不匹配报错,数组a长度大于列表的话,将元素索引相同的位置依次赋值给数组 ,数组其它索引位置原有的值不会改变,直接返回该数组的引用地址;若长度不够,就开辟一个新的数组copy并返回 |
public E get(int index) | 获取对应索引下标的元素 |
public E set(int index, E element) | 设置新值,index为下标,element 为新元素 |
public boolean add(E e) | 添加元素 |
public void add(int index, E element) | 将当前位于该位置的元素和后面的元素依次向后移动,再将新元素插入到该位置 |
public E remove(int index) | 删除列表指定位置的元素,将后面元素依次向前移动,并返回删除的元素 |
public boolean remove(Object o) | 删除第一个出现的指定元素,若没有,就保持不变;找到并删除,返回true;否则返回false |
public void clear() | 将列表每个元素置为空,并不会释放列表空间 |
public boolean addAll(Collection<? extends E> c) | 将指定容器的元素追加到列表末尾,成功追加返回true;若在操作过程中修改指定容器 |
-
ArrayList 和 LinkedList 是最常用的, 那么两者的区别是?
-
两个都是List的接⼝,两个都是⾮线程安全的
-
ArrayList是基于动态数组的数据结构,⽽LinkedList是基于链表的数据结构
-
对于随机访问get和set(查询操作),ArrayList要优于LinkedList,因LinkedList要移动指针
-
对于增删操作(add和remove),LinkedList优于ArrayList。
-
Map 接口
HashMap
底层就是⼀个数组结构,数组中的每⼀项⼜是⼀个链表,即数组和链表的结合体
Table是数组,数组的元素是Entry
Entry元素是⼀个key-value键值对,它持有⼀个指向下⼀个 Entry元素的引⽤,table数组的
每个Entry元素同时也作为当前Entry链表的⾸节点,也指向了该链表的下⼀个Entry元素
使⽤put(key, value)存储对象到HashMap中,使⽤get(key)从 HashMap中获取对象。当put()⽅法传递键和值时,会先对键调⽤hashCode()⽅法,计 算并返回的hashCode是⽤于找到Map数组的bucket位置来储存Entry对象的,是⾮线程 安全的,所以HashMap操作速度很快
HashMap 是以key–value对的形式存储的,key值是唯一的(可以为null),一个key只能对应着一个value,但是value是可以重复的。
HashMap 如果再次添加相同的key值,它会覆盖key值所对应的内容,这也是与HashSet不同的一点,Set通过add添加相同的对象,不会再添加到Set中去。
HashMap 提供了get方法,通过key值取对应的value值,但是HashSet只能通过迭代器Iterator来遍历数据,找对象。
jdk1.7 和1.8 中HashMap的主要区别
底层实现由之前的 “数组+链表” 改为 “数组+链表+红⿊树”
当链表节点较少时仍然是以链表存在,当链表节点较多时,默认是⼤于8时会转为红⿊ 树
HashMap 当我们讨论它的容量时,指的是table数组的大小,初始为 16,
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
- 链表新节点插入链表的顺序不同(jdk7是插入头结点,jdk8因为要把链表变为红 黑树所以采用插入尾节点)
谈谈HashMap扩容机制
阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。
扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2 (位运算比数值运算快)
为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?
- 阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(4/3)比较合理,因为这个数和任何2的次幂乘积结果都是整数。
- 负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的
谈谈HashMap存储过程
- 获取到传过来的key,调用hashcode()获取到hash值
- 获取到hash值之后调用indexFor方法,通过获取到的hash值以及数组的长度算出数组的下标 (把哈希值和数组容量转换为二进,再在数组容量范围内与哈希值进行一次与运算,同为1则1,不然则为0,得出数组的下标值,这样可以保证计算出的数组下标不会大于当前数组容量)
- 把传过来的key和value存到该数组下标当中。
- 如该数组下标下以及有值了,则使用链表,jdk7是把新增元素添加到头部节点 jdk8则添加到尾部节点。
TreeMap
- TreeMap 基于平衡二叉树,又叫红黑树;
- 实现了SortedMap, 所以是有序的集合
- 默认是按key 的字典顺序进行排序;
- 也可自定义; Compare接口;
TreeMap , 和 TreeSet 构造方法中都不能指定容量,而且当数据类型 为 类 类型时TreeMap,TreeSet 判断重复元素不用equals 和hashCode方法,而是实现 Compare接口中的比较方法compareTo() 判断相等即重复,以及排序; 或者还可以在构造时,传入 Comparator 比较器作为参数;
Map 常用方法
Map<String,String> map = new HashMap<>();
//往map⾥⾯放key - value;
map.put(“⼩明”,“⼴东⼴州”);
map.put(“⼩东”,“⼴东深圳”);
//根据key获取value
map.get(“⼩东”);
//判断是否包含某个key
map.containsKey(“⼩明”);
//返回map的元素数量
map.size();
//清空容器
map.clear();
//获取所有value集合
map.values();
//返回所有key的集合
map.keySet()
//返回⼀个Set集合,集合的类型为Map.Entry , 是Map声明的⼀个内部接⼝,接⼝为泛型,定
义为Entry<K,V>,
//它表示Map中的⼀个实体(⼀个key-value对),主要有getKey(),getValue⽅法
Set<Map.Entry<String,String>> entrySet = map.entrySet();
//判断map是否为空
map.isEmpty();
Set 接口
- Set 不保存重复的元素,存储⼀组唯⼀,⽆序(存取的顺序会被打乱,跟添加的顺序不一样)的对象
- Set的实现都是对应的Map的⼀ 种封装。⽐如HashSet是对HashMap的封装,TreeSet对应TreeMap
- 所谓封装就是在HashSet 类中,声明了一个私有的HashMap 集合属性
HashSet
- HashSet底层是⼀个HashMap,由于HashMap的put()⽅法是⼀个键值对,当新放⼊HashMap的 Entry中key 与集合中原有Entry的key相同(hashCode()返回值相等) ,并equals⽐较返回 false,才会添加新的key,若为true,则是重复元素,不添加 ,若元素是自定义类 类型,则需要重写hsahCode 和equals 方法
- HashSet集合的值放在了内部封装的HashMap集合的key值中,因为在HashMap中key是不允许重复的
- 允许包含值为null的元素,但最多只能有⼀个null元素,HashMap 也允许null值 和 null键
TreeSet
- TreeSet 具有去重 和排序的特性
- 默认是按key 的字典顺序进行排序;
- 如果是 TreeSet 存的是自定义的类 类型,不需要重写hashCode和equals方法,也能去重;
- 不允许包含值为null的元素
- 如果是 TreeSet 存的是自定义的类 类型,那么需要自定义排序规则,即这个自定义类要实现Comparable接口,并从写compareTo 方法; 否则报错 Collection.Collections.Student cannot be cast to java.lang.Comparable
public class Student implements Comparable{ private String name; private int age; public Student(){} public Student(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Student{" + "name='" + name + '\'' + ", age=" + age + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode() { return Objects.hash(name, age); } // 重写 排序规则 @Override public int compareTo(Object o) { Student student = (Student) o; if(this.getAge()==student.getAge()) //排除两个学生对象年龄一样,而姓名不一样的情况 return 1; return this.getAge()- student.getAge(); } }
这样定义了排序规则后,就可以往TreeSet里添加元素,自动排序了
Comparator 接口 和 Comparable 接口
-
Comparator 接口一般用于自然排序,自己与自己比较,上面的Student类就是这种情况
-
Comparable 接口我们称之为比较器, 具体方法为 int compare (T o1, T o2), 一般是两者的比较,源码上有 @FunctionalInterface 函数式编程注解,那么推荐使用函数式编程来使用,例如Collections.sort( ) , Arrays.sort( ) 方法里作为比较器传入参数
-
例如 :
List<Student> list = new ArrayList<>(); Student s1=new Student("jack",12); Student s2=new Student("tom",6); Student s3=new Student("jk",1); Student s4=new Student("ying",56); list.add(s1); list.add(s2); list.add(s3); list.add(s4); Collections.sort(list, new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o2.getAge()-o1.getAge(); //降序,升序反过来相减 } }); System.out.println(list); //[Student{name='ying', age=56}, Student{name='jack', age=12}, Student{name='tom', age=6}, Student{name='jk', age=1}] // 最大年龄学生 Student student= Collections.max(list, new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o1.getAge()-o2.getAge(); } }); System.out.println(student); //Student{name='ying', age=56}
但当我们Student 类实现了Comparable 接口,上面的 sort(),max()里可以不传Comparator 比较器,也可以; min,max 方法 ,如果不传比较器,Student 类自定义的排序规则为升序的时候,对应最大值,最小值, 若自定义降序,则相反对应,即 此时min = 集合最大值;
当jdk 不知道如何比较时,就需要我们自定义排序,理论上,Comparator 接口 和 Comparable 接口可以通用,互换;使用时还是遵循该有的规范比较好;
集合遍历
遍历List
// 获取index 遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("====================");
//增强for循环 遍历
for (Object str: list //注意泛型与定义时一致,Object通用,若定义时未指定
) { //泛型,这里就不可写 String str :list
System.out.println(str);
}
System.out.println("====================");
// 获取迭代器遍历
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
遍历Set
// 迭代器
Iterator iterator = hashSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println("===================");
//增强for
for (Object obj:hashSet
) {
System.out.println(obj);
}
Set 为啥没有get(Index)的遍历方式呢?
- 无论是HashSet还是TreeSet 都是对 Map结构的封装,map 可以通过 get key 值来获取 value,而Set 是存在 key 值上的,无法通过 key或者value来获取。
遍历Map
// keySet 集合获取到key值, get(key) 获取value
for (Object obj : hashMap.keySet()) {
System.out.println(hashMap.get(obj));
}
System.out.println("================");
// 用 EntrySet,获取 Entry 来遍历
Set<Map.Entry<Integer, String>> entries = hashMap.entrySet();
//Iterator iterator = hashMap.entrySet().iterator(); //也可一步到位
Iterator iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry e = (Map.Entry)iterator.next();
System.out.println(e.getKey()+"-->"+e.getValue());
}
Iterator 迭代器
- Iterator 接口
迭代器类似于指针,指向要遍历的元素,java中提供了Iterator 接口,以及一些方法
boolean hasNext( ) 如果迭代包含更多的元素,则返回true;当前迭代器指向的元素的下一个空间还有元素的话,返回true;
E next( ) 将迭代器下移一位, 返回迭代器指向的元素;
为什么是下一个元素呢?
一开始遍历的时候,迭代器指向的是第一个元素的前一个位置,这时我们判断hasNext(),它就会返回true,第一个元素是存在的;是基于这样的一个设计;
default void remove( ) 移除迭代器指向的元素,每次调用next方法时,只能调用remove一次,多次调用,相当于重复删除了,迭代器没有下移,指向的还是第一次remove的空间;
迭代器在操作过程中,如果通过其他操作修改了元素,则会出错;
例如 :
Iterator iterator = list.iterator(); //获取迭代器 list.remove(0); // 删除第一个元素 String next= (String)iterator.next(); //获取第一个元素 !! 语句报错 System.out.println(next);
- List 集合还有一个 ListIterator迭代器,public interface ListIterator extends Iterator { } 提供了前向遍历的方法 boolean hasPrevious( )判断是否有前一个元素,E previous( ) 返回当前迭代器指向的元素,并向前移动迭代器;
System.out.println(list); // [jack, tom, toney, jeny, 张飞, 李白] ListIterator<String> iterator = list.listIterator(); iterator.next(); iterator.next(); System.out.println(iterator.previous()); // tom System.out.println(iterator.previous()); // jack
提供一种反向遍历的方式:
System.out.println(list); // [jack, tom, toney, jeny, 张飞, 李白] ListIterator<String> iterator = list.listIterator(list.size()); while (iterator.hasPrevious()){ System.out.println(iterator.previous()); }
- 集合的多种删除方式
// 第一种 通过集合自带方法
System.out.println(list);
for (int i = 0; i < list.size(); i++) {
if(list.get(i).equals("张飞"))
list.remove(i);
}
System.out.println(list);
// foreach 报错 ConcurrentModificationException 异常
// System.out.println("====================");
// for (Object str: list
// ) {
// String s = (String) str;
// if(s.equals("李白"))
// list.remove(s);
// }
// System.out.println(list);
System.out.println("====================");
// 第二种,通过迭代器删除
Iterator iterator = list.iterator();
while(iterator.hasNext()){
String next= (String)iterator.next();
if(next.equals("jeny")){
//list.remove("jeny"); // 中途修改元素,报错
iterator.remove();
}
}
System.out.println(list);
结果 :
[jack, tom, toney, jeny, 张飞, 李白]
[jack, tom, toney, jeny, 李白]
====================
[jack, tom, toney, 李白]
- 为什么 foreach 删除不了元素呢?
- java中提供的foreach语法糖其底层实现方式主要有两种:对于集合类或实现迭代器的集合使用迭代器的遍历方式,对于数组集合使用数组的遍历方法。
- 使用foreach进行集合遍历时需要额外注意不能对集合长度进行修改,也就是不能对集合进行增删操作,否则会抛出
ConcurrentModificationException
异常- foreach 遍历集合采用迭代器的方式,如果在遍历过程中使用用集合相关函数对集合进行增删操作操作,就违背了迭代器的使用原则
- ArrayList 源码里有modCount 记录修改次数的值,在迭代器生成的时候,会将modCount 值赋给迭代器中的expectedModCount,若迭代器遍历过程中modCount的值不等于迭代器的expectedModCount ,那么 就会抛出
ConcurrentModificationException
异常
对于集合泛型的理解
- 在我们定义 容器时 ,容器类名后面和应用变量之间的泛型可以不写,注意是不写<> ,而不是写尖括号然后里面不写
- 容器名后面,写尖括号然后里面不写 ,会报错 ( java: 非法的类型开始)
- 这里的泛型不写,容器就会接受Object类型的数据
- new LinkedList<>(); 这里的泛型可写可不写,可只写<>里面啥都不写
Collection co = new LinkedList<>(); co.add("asfas"); co.add(121); co.add(23.23); System.out.println(co); List list1 =new LinkedList<>(); list1.add("asdas"); list1.add(5445); System.out.println(list1); for (Object o : co ) { System.out.println(o); } //以List 为例子 ,有以下正确写法 List list1 = new LinkedList<>(); List list2 = new LinkedList(); List list3 = new LinkedList<String>(); //list3 类型仍然为Object类型 List <String>list4 = new LinkedList (); List <String>list5 = new LinkedList<> (); List <String>list6 = new LinkedList<String> ();
!!!总结 : 容器的类型是由 前面的引用变量指定的,new 后面的泛型不起作用,写不写都可,因为写了也没用; 在前面已经指定数据类型的时候,但如果要写的话就得和前面的泛型保持一致,要不然报错 :(java: 不兼容的类型:)
Collections 类
操作集合的工具类,不能 new 实例,通过静态方法调用
List<String>list =new ArrayList<>();
list.add("aa");
list.add("ll");
list.add("vv");
list.add("kk");
System.out.println("排序前==========");
System.out.println(list);
//默认升序 ,小到大 ;
// Collections.sort(list);
//指定升序
Collections.sort(list, Comparator.naturalOrder());
System.out.println("排序后==========");
System.out.println(list);
//指定降序
Collections.sort(list, Comparator.reverseOrder());
System.out.println("排序后==========");
System.out.println(list);
//随机排序,类似打乱;
Collections.shuffle(list);
System.out.println("打乱后==========");
System.out.println(list);
//将集合改为只读集合
List<String> unmodifiableList = Collections.unmodifiableList(list);
System.out.println(unmodifiableList);
// 反转元素
Collections.reverse(list);
System.out.println(list);
//统计某一个元素在集合中出现的次数
System.out.println(Collections.frequency(list,"kk"));
//拷贝
List copyList = new ArrayList();
Collections.copy(copyList,list);
System.out.println(copyList);
or.reverseOrder());
System.out.println(“排序后==========”);
System.out.println(list);
//随机排序,类似打乱;
Collections.shuffle(list);
System.out.println("打乱后==========");
System.out.println(list);
//将集合改为只读集合
List<String> unmodifiableList = Collections.unmodifiableList(list);
System.out.println(unmodifiableList);
// 反转元素
Collections.reverse(list);
System.out.println(list);
//统计某一个元素在集合中出现的次数
System.out.println(Collections.frequency(list,"kk"));
//拷贝
List copyList = new ArrayList();
Collections.copy(copyList,list);
System.out.println(copyList);
``