Java程序设计|Java——集合


文章目录

  • 一. 集合概述
    • 1. 什么是集合?有什么用?
    • 2. 集合存储什么?
    • 3. 不同集合底层对应不同的数据结构
    • 4. 集合在java JDK中的哪个包下?
    • 5. java中集合分为两大类
      • 5.1 集合继承结构图
  • 二. Collection接口
    • 1. Collection接口中常用方法
      • 1.1 contains方法解析
      • 1.2 remove方法源码分析:
    • 2. 关于集合遍历/迭代专题
  • 三. List接口
    • 1. 测试list接口中常用的方法
    • 2. ArrayList集合
    • 3. LinkedList集合
    • 4. Vector集合
  • 四. Set接口
    • 1. HashSet集合
    • 2. TreeSet集合
  • 五. Map接口
    • 1. Map集合中常用的方法
    • 2. 哈希表数据结构
    • 3. 最主要掌握的是以下两个方法的实现原理:
    • 4. Hashtable类
    • 5. 属性类Properties类
    • 6. TreeSet集合
    • 7. 自定义类型实现Comparable接口
    • 8. 自平衡二叉树数据结构
    • 9. Collections工具类
  • 六. 泛型
    • 1. 泛型的特性
    • 2. 泛型自动类型推断机制
    • 3. 自定义泛型

一. 集合概述 1. 什么是集合?有什么用? 数组其实就是一个集合。集合实际上就是一个容器,可以来容纳其他类型的数据。
集合为什么说在开发中使用较多?
  • 集合是一个容器,是一个载体,可以一次容纳多个对象。在实际开发中,假设连接数据库,数据库当中有10条记录,那么假设把这10条记录查询出来,在java程序中会将10条数据封装成10个java对象,然后将10个java对象放到某一个集合当中,将集合传到前端,然后遍历集合,将一个数据一个数据展现出来。
2. 集合存储什么? 集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址。(或者说集合中存储的是引用)
  • list.add(100);自动装箱Integer
注意
  • 集合在java中本身是一个容器,是一个对象
  • 集合中任何时候存储的都是"引用"
    Java程序设计|Java——集合
    文章图片
3. 不同集合底层对应不同的数据结构 在java中每一个不同的集合,底层会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到不同的数据结构当中。
什么是数据结构?
  • 数据存储的结构就是数据结构。不同数据结构,数据的存储方式不同
  • 例如:数组、二叉树、链表、哈希表…
  • 使用不同的集合等同于使用了不同的数据结构
  • new ArrayList(); 创建一个集合,底层是数组
  • new LinkList(); 创建一个集合对象,底层是链表
  • new TreeSet();创建一个集合对象,底层是二叉树
4. 集合在java JDK中的哪个包下? java.util.*;
  • 所有的集合类和集合结构都在java.util包下
5. java中集合分为两大类 5.1 集合继承结构图
一类是单个方式存储元素:
  • 单个方式存储元素,这一类集合中超级父接口:java.util.Collection
    Java程序设计|Java——集合
    文章图片
一类是以键值对的方式存储元素:
  • 以键值对的方式存储元素,这一类集合中超级父接口:java.util.Map
    Java程序设计|Java——集合
    文章图片
  • 在JDK8之后,如果哈希表单向链表中元素超过8个,单向链表这种数据结构会变成红黑树数据结构。当红黑树上的节点数量小于6时,会重新把红黑树变成单向链表数据结构。这种方式也是为了提高检索效率,二叉树的检索会再次缩小扫描范围,提高效率。初始化容量是16,默认加载因子是0.75
  • Hashtable的key和value都不能为null的;HashMap集合的key和value都是可以为null的
  • Hashtable集合初始化容量11,集合扩容是:原容量*2+1
二. Collection接口 1. Collection接口中常用方法 Collection中能存放什么元素?
  • 没有使用泛型之前,Collection中可以存储Object的所有子类型
  • 使用了泛型之后,Collection中只能存储某个具体的类型
  • 集合中不能直接存储基本数据类型,也不能存储java对象,只是存储java对象的内存地址
Collection接口中常用方法
package com.collection; import java.util.ArrayList; import java.util.Collection; /* java.util.Collection接口中常用方法 boolean add(Object e) 向集合中添加元素 int size() 获取集合中元素的个数 void clear() 清空集合 boolean contains(Object o) 判断当前集合中是否包含元素o,包含返回true boolean remove(Object o) 删除集合中的某个元素 boolean isEmpty() 判断该集合元素的个数是否为0 Object[] toArray() 调用这个方法可以把集合转换为数组 */ public class CollectionTest01 { public static void main(String[] args) { //创建一个集合对象 //Collection c = new Collection(); 接口是抽象的,无法实例化 //多态 Collection c = new ArrayList(); //测试Collection接口中的常用方法 c.add(100); //自动装箱,实际上是放进去一个对象的内存地址,Integer x = new Integer(100) c.add(new Object()); c.add(new Student()); c.add(true); //自动装箱//获取集合中元素的个数 System.out.println("集合中元素的个数是:" + c.size()); //4//清空集合 c.clear(); System.out.println("集合中元素的个数是:" + c.size()); //0//再向集合中添加元素 c.add("hello"); //添加"hello对象的内存地址放到集合当中" c.add("world"); c.add("pudding"); //判断集合中是否包含pudding boolean flag = c.contains("pudding"); System.out.println(flag); //true//删除集合中的某个元素 c.remove("world"); System.out.println("集合中元素的个数是:" + c.size()); //2//判断集合是否为空(集合中是否存在元素) System.out.println(c.isEmpty()); //falsec.add("abc"); c.add("def"); c.add(new Student()); //转换为数组 Object[] obj = c.toArray(); for (int i = 0; i < obj.length; i++) { //遍历数组 Object o = obj[i]; System.out.println(o); /* abc def com.collection.Student@154617c */ } } }class Student{}

1.1 contains方法解析
【Java程序设计|Java——集合】Java程序设计|Java——集合
文章图片

package com.collection; import java.util.ArrayList; import java.util.Collection; public class CollectionTest04 { /*boolean contains(Object o) 判断集合中是否包含某个对象o,如果包含返回true 那么它在底层是怎么判断集合中是否包含某个元素的呢? 调用了equals方法进行比对 equals方法返回true,就表示包含这个元素 */ public static void main(String[] args) { //创建集合对象 Collection c = new ArrayList(); String s1 = new String("abc"); //s1 = 0x1111 c.add(s1); String s2 = new String("def"); //s2 = 0x2222 c.add(s2); //集合中元素的个数 System.out.println("集合中元素的个数:" + c.size()); //2String x = new String("abc"); //s3 = 0x5555 //c集合中是否包含x? System.out.println(c.contains(x)); //集合中是否包含"abc"//true } }

测试contains方法:
package com.collection; import java.util.ArrayList; import java.util.Collection; import java.util.Objects; /* 测试contains方法 */ public class CollectionTest05 { public static void main(String[] args) { //创建集合对象 Collection c = new ArrayList(); //创建用户对象 User u1 = new User("jack"); User u2 = new User("jack"); //加入集合 c.add(u1); //判断集合中是否包含u2 /* 没有重写equals之前:这个结果是false 调用Object的equals方法,==比较的是内存地址 */ //System.out.println(c.contains(u2)); //false //重写equals方法之后,比较的时候会比较name System.out.println(c.contains(u2)); //true} }class User{ private String name; public User(){} public User(String name) { this.name = name; }//重写equals方法 //将来调用equals方法的时候,一定是调用这个重写的equals方法 //这个equals方法比较原理是:只要姓名一样就表示同一个用户@Override public boolean equals(Object o) { if (o == null || !(o instanceof User)) return false; if (o == this) return true; User user = (User) o; //如果名字一样表示同一个人(不再比较对象的内存地址了) return user.name.equals(this.name); } }

结论:
  • 存放在一个集合中的类型,一定要重写equals方法,contains一定会调用equals方法
1.2 remove方法源码分析:
public class CollectionTest05 { public static void main(String[] args) { //创建集合对象 Collection cc = new ArrayList(); //创建字符串对象 String s1 = new String("hello"); //加进去 cc.add(s1); //创建了一个新的字符串对象 String s2 = new String("hello"); //删除s2 cc.remove(s2); //集合中的元素个数 System.out.println(cc.size()); //0 } }

结论:
  • Collection接口中的remove方法和contains方法底层都会调用equals
关于集合的remove
  • 重点:当集合的结构发生改变时,迭代器必须重新获取,如果还是用以前老的迭代器,会出现java.util.ConcurrentModificationException异常
  • 在迭代集合元素的过程中,不能调用集合对象的remove方法,删除元素:c.remove(o)
package com.collection; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class CollectionTest06 { public static void main(String[] args) { //创建集合 Collection c = new ArrayList(); //注意:此时获取迭代器,指向的那是集合中没有元素状态下的迭代器 //一定要注意:集合结果只要发生改变,迭代器必须重新获取 //当集合结构发生改变,迭代器没有重写获取时,调用next()方法时:java.util.ConcurrentModificationException //Iterator it = c.iterator(); //添加元素 c.add(1); c.add(2); c.add(3); //获取迭代器 Iterator it = c.iterator(); while (it.hasNext()) { //编写代码时next()方法返回值类型必须是Object //Integer i = it.next Object obj = it.next(); System.out.println(obj); //1 2 3 }Collection c2 = new ArrayList(); c2.add("abc"); c2.add("def"); c2.add("xyz"); Iterator it2 = c2.iterator(); while (it2.hasNext()){ Object o = it2.next(); //删除元素 //删除元素之后,集合结构发生改变,应该重写去获取迭代器 //但是,循环下一次的时候并没有重写获取迭代器,所以出现了异常:java.util.ConcurrentModificationException //出现异常的根本原因:集合中元素删除了,但是没有更新迭代器 //c2.remove(o); 直接通过集合去删除元素,没有通知迭代器。(导致迭代器的快照和原集合状态不同)//使用迭代器可以删除元素 //迭代器去删除时,会自动更新迭代器,并且更新集合(删除集合中元素) it2.remove(); System.out.println(o); } System.out.println(c2.size()); //0 } }

2. 关于集合遍历/迭代专题 Iterator it = c.iterator();
  • 获取的迭代器对象,迭代器用来遍历集合,此时相当于对当前集合的状态拍了一个快照。
  • 迭代器迭代的时候会参照这个快照进行迭代
    Java程序设计|Java——集合
    文章图片
package com.collection; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class CollectionTest02 { public static void main(String[] args) { //注意:以下的遍历方式/迭代方式,是所有Collection通用的一种方式 //Map集合中不能用,在所有的Collection以及子类中使用 //创建集合对象 Collection c = new ArrayList(); //后面的集合无所谓,主要看前面的Collection接口,怎么迭代 //添加元素 c.add("abc"); c.add("def"); c.add(100); c.add(new Object()); //对集合Collection进行遍历 //第一步:获取集合对象的迭代器对象Iterator Iterator it = c.iterator(); //第二步:通过以上获取的迭代器对象开始遍历集合 /* 迭代器对象中的方法: boolean hasNext() 如果仍有元素可以迭代,则返回 true。 Object next() 返回迭代的下一个元素。 */boolean hasNext = it.hasNext(); System.out.println(hasNext); //true if (hasNext) { //不管你当初存进去什么,取出来统一都是Object Object obj = it.next(); System.out.println(obj); //abc }//或者用循环 while (it.hasNext()) { Object object = it.next(); System.out.println(object); } } }

package com.collection; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; public class CollectionTest03 { public static void main(String[] args) { //创建集合对象 Collection c1 = new ArrayList(); //ArrayList集合有序可重复 //添加元素 c1.add(1); c1.add(2); c1.add(3); c1.add(4); //迭代集合 Iterator it = c1.iterator(); while (it.hasNext()){ Object object = it.next(); /*if (object instanceof Integer) { System.out.println("Integer类型"); }*/ //存进去什么类型,取出来还是什么类型,只不过输出的时候转换成字符串 System.out.println(object); }//HashSet集合:无序不可重复 Collection c2 = new HashSet(); c2.add(100); c2.add(200); c2.add(300); c2.add(100); Iterator it2 = c2.iterator(); while (it2.hasNext()){ System.out.println(it2.next()); } } }

三. List接口 1. 测试list接口中常用的方法 List集合中的元素特点:有序可重复
  • 有序:List集合中的元素有下标
  • 从0开始,以1递增
  • 可重复:存储一个1,还可以再存储1
List既然是Collection接口的子接口,那么肯定List接口有自己"特色"方法
package com.collection; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /* 以下只列出List接口特有的常用的方法 void add(int index, Object element) Object get(int index) int indexOf(Object o) int lastIndexOf(Object o) Object remove(int index) Object set(int index, Object element) */ public class ListTest01 { public static void main(String[] args) { //创建List类型集合 List myList = new ArrayList(); //添加元素 myList.add("A"); //默认都是向集合末尾添加元素 myList.add("B"); myList.add("C"); myList.add("D"); //1. 在列表的指定位置插入元素(第一个参数是下标) //这个方法使用不多,因为对于ArrayList集合来说效率比较低 myList.add(1, "king"); //迭代 Iterator it = myList.iterator(); while (it.hasNext()){ Object elt = it.next(); System.out.println(elt); //A B C D }//2. 根据下标获取元素 Object firstObj = myList.get(0); System.out.println(firstObj); //因为有下标,所以List集合有自己比较特殊的遍历方式 //通过下标遍历,(List集合特有的方法) for (int i = 0; i < myList.size(); i++) { Object obj = myList.get(i); System.out.println(obj); }//3. 获取指定对象第一次出现处的索引 System.out.println(myList.indexOf("king")); // 1//4. 获取指定对象最后一次出现处的索引 System.out.println(myList.lastIndexOf("C")); //3//5. 删除指定下标位置的元素 myList.remove(0); System.out.println(myList.size()); System.out.println("====================="); //6. 修改指定位置的元素 myList.set(2,"Soft"); //遍历集合 for (int i = 0; i < myList.size(); i++) { Object obj = myList.get(i); System.out.println(obj); //king B Soft D } } }

2. ArrayList集合
  • ArrayList集合初始化容量是10
  • ArrayList集合底层是Object类型的数组Object[]
  • 构造方法:
    new ArrayList(); new ArrayList(20);

  • ArrayList集合的扩容:增长到原容量的1.5倍
  • 数组优点:检索效率高
  • 数组缺点:随机增删元素效率低,向数组末尾添加元素不受影响
集合ArrayList的构造方法:
package com.collection; import java.util.ArrayList; import java.util.List; public class ArrayListTest01 { public static void main(String[] args) { //默认初始化容量是10 List list1 = new ArrayList(); //集合中的size()方法是获取当前集合中元素的个数,而不是获取集合的容量 System.out.println(list1.size()); //0//指定初始化容量,数组长度为20 List list2 = new ArrayList(20); System.out.println(list2.size()); //0 } }

package com.collection; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; public class ArrayTest02 { public static void main(String[] args) { //默认初始化容量10 List myList1 = new ArrayList(); //指定初始化容量20 List myList2 = new ArrayList(20); Collection c = new HashSet(); c.add(100); c.add(200); c.add(900); c.add(50); //通过这个构造方法就可以将HashSet集合转换为List集合 List myList3 = new ArrayList(c); for (int i = 0; i < myList3.size(); i++) { System.out.println(myList3.get(i)); //50 100 900 200 } } }

3. LinkedList集合 本质是:双向链表
链表优点:
  • 由于链表上的元素空间存储内存地址不连续
  • 所以随机增删元素的时候不会有大量元素位移,因此随机增删效率较高
  • 在以后的开发中,如果遇到随机增删集合中元素的业务比较多,建议使用LinkedList
链表缺点:不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头结点开始遍历,直到找到为止,所以LinkedList集合检索/查找效率较低
ArrayList:把检索效率发挥到了极致(末尾添加元素效率还是很高的)
LinkedList:把随机增删发挥到了极致
  • 由于加元素一般都是往末尾添加,所以ArrayList用的比LinkedList多
package com.collection; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; public class LinkedListTest01 { public static void main(String[] args) { //LinkedList集合底层也是有下标的 //注意:ArrayList之所以检索效率比较高,不是单纯因为下标的原因,是因为底层数组发挥的作用 //LinkedList集合照样有下标,但是检索/查找某个元素的时候效率比较低,因为只能从节点开始一个一个遍历 List list = new LinkedList(); list.add("a"); list.add("b"); list.add("c"); for (int i = 0; i < list.size(); i++) { Object obj = list.get(i); System.out.println(obj); }//LinkList集合有初始化容量吗? //最初这个链表中没有任何元素,first和last引用都是null //不管是LinkedList还是ArrayList,以后写代码时不需要关心具体是哪个集合 //因为我们要面向接口编程,调用的方法都是接口中的方法 //List list2 = new ArrayList(); 这样写底层用了数组 List list2 = new LinkedList(); //这样写底层用来双向链表list2.add("123"); list2.add("456"); list2.add("789"); for (int i = 0; i < list2.size(); i++) { System.out.println(list2.get(i)); } } }

4. Vector集合 底层也是一个数组
初始化容量:10
怎么扩容?
  • 扩容之后是原容量的2倍
Vector在所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的,效率较低,使用较少
怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
  • 使用集合工具类:
  • java.util.Collections; 是集合工具类
  • java.util.Collection; 是集合接口
package com.collection; import java.util.*; public class VectorTest { public static void main(String[] args) { //创建一个Vector集合 //Vector vector = new Vector(); List vector = new Vector(); //添加元素 //默认容量10个 vector.add(1); vector.add(2); vector.add(3); vector.add(4); vector.add(5); vector.add(5); vector.add(6); vector.add(7); vector.add(8); vector.add(9); vector.add(10); //满了之后扩容(扩容是) vector.add(11); Iterator it = vector.iterator(); while (it.hasNext()) { Object obj = it.next(); System.out.println(obj); }//这个可能以后要使用 List mylist = new ArrayList(); //非线程安全//变成线程安全 Collections.synchronizedList(mylist); //mylist集合就是线程安全的 mylist.add("111"); mylist.add("222"); mylist.add("333"); } }

四. Set接口 1. HashSet集合 存储元素特点:
  • 存储时顺序和取出的顺序不同
  • 不可重复
  • 放到HashSet集合中的元素实际上是放到HashMap集合的key部分了
package com.collection; import java.util.HashSet; import java.util.Set; public class HashSetTest01 { public static void main(String[] args) { //演示一下HashSet集合特点 Set stringSet = new HashSet<>(); //添加元素 stringSet.add("hello1"); stringSet.add("hello4"); stringSet.add("hello3"); stringSet.add("hello2"); stringSet.add("hello3"); stringSet.add("hello4"); //遍历 /* 结果: hello1 hello4 hello2 hello3 */ for(String s:stringSet){ System.out.println(s); } } }

2. TreeSet集合 TreeSet集合存储元素特点:
  • 无序不可重复的,但是存储的元素可以自动按照大小顺序排序
  • 称为可排序集合
  • 无序指的是:存进去的顺序和取出来的顺序不同。并且没有下标
package com.collection; import java.util.Set; import java.util.TreeSet; public class TreeSetTest01 { public static void main(String[] args) { //创建集合对象 Set strs = new TreeSet<>(); //添加元素 strs.add("A"); strs.add("B"); strs.add("Z"); strs.add("Y"); strs.add("Z"); strs.add("M"); //遍历 /* 结果: A B M Y Z */ for (String s:strs) { System.out.println(s); } } }

五. Map接口 java.util.Map接口中常用的方法:
  • Map和Collection没有继承关系
  • Map集合以key和value的方式存储数据:键值对
  • key和value都是引用数据类型
  • key和value都是存储对象的内存地址
  • key起到主导地位,value是key的一个附属品
  • HashMap允许key部分为null
1. Map集合中常用的方法 Java程序设计|Java——集合
文章图片

package com.collection; import java.util.Collection; import java.util.HashMap; import java.util.Map; /* Map集合中常用的方法 V put(K key, V value)向Map集合中添加键值对 V get(Object key)通过key获取value void clear() 清空Map集合 boolean containsKey(Object key) 判断Map中是否包含某个key boolean containsValue(Object value) 判断Map中是否包含某个Value boolean isEmpty() 判断Map集合中元素个数是否为0 V remove(Object key)通过key删除键值对 int size() 获取Map集合中键值对的个数 Collection values() 获取Map集合中所有的value,返回一个CollectionSet keySet() 获取Map集合所有的key(所有的键是一个Set集合)Set> entrySet() 将Map集合转换为Set集合 假设现在有一个Map集合,如下所示: map1集合对象 keyvalue ------------------- 1zhangsan 2lisi 3wangwu 4zhaoliuSet set = map1.entrySet(); set集合对象 1=zhangsan 2=lisi 3=wangwu 4=zhaoliu 【注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是Map.Entry】 Map.Entry和String一样,都是一种类型的名字,只不过:Map.Entry是静态内部类,是Map中的静态内部类 */ public class MapTest01 { public static void main(String[] args) { //创建Map集合对象 Map map = new HashMap<>(); //向Map集合中添加键值对 map.put(1,"zhangsan"); //1在这里进行了自动装箱 map.put(2,"lisi"); map.put(3,"wangwu"); map.put(4,"zhaoliu"); //通过key获取value String value = https://www.it610.com/article/map.get(2); System.out.println(value); //获取键值对的数量 System.out.println("键值对的数量:" + map.size()); //4 //通过key删除key-value map.remove(2); System.out.println("键值对的数量:" + map.size()); //3//判断是否包含某个key //contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法 System.out.println(map.containsKey(4)); //true //判断是否包含某个value System.out.println(map.containsValue("wangwu")); //true//获取所有的value Collection values = map.values(); for (String s:values){ System.out.println(s); }//清空map集合 map.clear(); System.out.println("键值对的数量:" + map.size()); //0//判断是否为空 System.out.println(map.isEmpty()); //true } }

package com.collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class MapTest02 { public static void main(String[] args) { //第一种方式:获取所有的key,通过遍历key,来遍历value Map map = new HashMap<>(); map.put(1,"zhangsan"); map.put(2,"lisi"); map.put(3,"wangwu"); map.put(4,"zhaoliu"); //遍历Map集合 //获取所有的key,所有的key是一个Set集合 Set keys = map.keySet(); //遍历key,通过key获取value //迭代器可以 Iterator it = keys.iterator(); while (it.hasNext()){ Integer key = it.next(); String value = https://www.it610.com/article/map.get(key); System.out.println(key +"=" + value); /* 1=zhangsan 2=lisi 3=wangwu 4=zhaoliu */ }//foreach也可以 for (Integer key:keys){ System.out.println(key + "=" + map.get(key)); }//第二种方式:Set> entrySet() //将Map集合直接全部转换为Set集合 //Set集合中元素的类型是:Map.Entry Set> set = map.entrySet(); //遍历Set集合,每一次取出一个Node //迭代器 Iterator> it2 = set.iterator(); while (it2.hasNext()){ Map.Entry node = it2.next(); Integer key = node.getKey(); String value = https://www.it610.com/article/node.getValue(); System.out.println(key +"=" + value); }//foreach方式 //这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值 //这种方式适合大数据量 for (Map.Entry node : set){ System.out.println(node.getKey() + "=" + node.getValue()); } } }

Java程序设计|Java——集合
文章图片

2. 哈希表数据结构 HashMap集合:
  • HashMap集合底层是哈希表/散列表的数据结构
哈希表是一个怎样的数据结构?
  • 哈希表是一个数组和单向链表的结合体
  • 数组:在查询方面效率很高,随机增删方面效率很低
  • 单向链表:在随机增删方法效率较高,在查询方法效率较低
  • 哈希表将以上两种数据结构融合在一起
HashMap集合底层的源代码
public class HashMap{ //HashMap底层实际上就是一个数组(一维数组) Node table; //静态内部类HashMap.Node static class Node { final int hash; //哈希值(哈希值是key的hashCode()方法执行结果。hash值通过哈希函数/算法,可以转化存储成数组下标) final K key; //存储到Map集合中的那个key V value; //存储到Map集合中的那个value Node next; //下一个节点的内存地址 }

Java程序设计|Java——集合
文章图片

3. 最主要掌握的是以下两个方法的实现原理:
  • map.put(k,v)
  • v = map.get(k)
map.put(k,v)实现原理:
  • 第一步:先将k,v封装到Node对象当中
  • 第二步:底层会调用k的hashCode()方法得出hash值,然后通过哈希函数/哈希算法,将hash值转换成数组的下标,下标位置上如果没有任何元素,就把Node添加到这个位置上了。如果说下标对应的位置上有链表,此时会拿着k和链表上的每一个节点中的k进行equals,如果所有的equals方法返回都是false,那么这个新节点将会被添加到链表的末尾。如果其中有一个equals返回来true,那么这个结点的value将会被覆盖
v = map.get(k)实现原理:
  • 先调用k的hashCode()方法得出哈希值,通过哈希算法转换成数组下标,通过数组下标快速定位到某个位置上,如果这个位置上什么也没有,那么会拿着参数k和单链表上的每一个节点中的k进行equals,如果所有equals方法返回false,那么get方法返回null,只要其中有一个节点的k和参数k的equals时候返回true,那么此时这个节点的value就是我们要找的value,get方法最终返回这个要找的value
为什么哈希表的随机增删,以及查询效率都很高?
  • 增删是在链表上完成。查询也不需要都扫描,只需要部分扫描。
  • 重点:通过讲解可以得出HashMap集合的key,会先后调用两个方法,一个方法是hashCode(),另一个方法是equals(),那么这两个方法都需要重写
放在HashMap集合key部分的元素其实就是放到HashSet集合中了,所有HashSet集合中的元素也需要同时重写HashCode()+equals()方法
注意:同一个单向链表上所有节点的hash相同,因为他们的数组下标是一样的。但同一个链表上k和k的equals方法肯定返回的是false,都不相等
哈希表HashMap使用不当无法发挥性能!
  • 假设所有将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了纯单向链表。这种情况我们称为:散列分布不均匀
HashMap集合的默认初始化容量是16,默认加载因子是0.75,达到75%开始扩容
  • 记住:HashMap集合初始化容量必须是2的倍数,因为达到散列均匀,为了提高HashMap集合的存储效率所必须的
package com.collection; import java.util.HashMap; import java.util.Map; import java.util.Set; public class HashMapTest01 { public static void main(String[] args) { //测试HashMap集合key部分的元素特点 //Integer是key,它的hashCode和equals都重写了 Map map = new HashMap<>(); map.put(1111,"zhangsan"); map.put(6666,"lisi"); map.put(7777,"wangwu"); map.put(2222,"zhaoliu"); map.put(2222,"king"); //key重复的时候value会自动覆盖System.out.println(map.size()); //4//遍历Map集合 Set> set = map.entrySet(); for (Map.Entry entry : set){ //验证结果:HashMap集合key部分元素:无序不可重复 System.out.println(entry.getKey() + "=" + entry.getValue()); } } }

package com.bean; import java.util.HashSet; import java.util.Set; /* 1. 向Map集合中存,以及从Map集合中取,都是县调用key的hashCode方法,然后在调用equals方法 equals方法有可能调用,也有可能不会调用 2. 注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写, 并且equals方法返回如果是true,hashCode()方法返回的值必须一样 equals方法返回true表示两个对象相同,在同一个单向链表上比较 那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的 所有hashCode()方法的返回值也应该是相同的 3. 放在hashCode集合key部分,以及放在hashSet集合中的元素,需要同时重写hashCode方法和equals方法 */ public class HashMapTest02 { public static void main(String[] args) { Student s1 = new Student("zhangsan"); Student s2 = new Student("zhangsan"); //重写equals方法之前是false System.out.println(s1.equals(s2)); //false//重写equals方法之后是true System.out.println(s1.equals(s2)); System.out.println("s1的hashCode="+s1.hashCode()); //22307196 System.out.println("s2的hashCode="+s2.hashCode()); //10568834//s1.equals(s2)已经是true了,表示s1和s2是一样的,那么王hashCode集合中方法的话, //按说只能放进去一个(HashSet集合特点无序不可重复) Set students = new HashSet<>(); students.add(s1); students.add(s2); System.out.println(students.size()); //这个结果按说应该是1,但是结果为2 //重写equals和hashCode之后hashCode都为-1432604525 } }package com.bean; import java.util.Objects; public class Student { private String name; public Student() { }public Student(String name) { this.name = name; }public String getName() { return name; }public void setName(String name) { this.name = name; }//重写equals (如果名字一样,表示同一个学生) /*public boolean equals(Object object){ if (object == null || !(object instanceof Student)) return false; if (object == this) return true; Student student = (Student)object; if (this.name.equals(student.name)) return true; return false; }*/@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Objects.equals(name, student.name); }@Override public int hashCode() { return Objects.hash(name); } }

4. Hashtable类
package com.bean; import java.util.Hashtable; import java.util.Map; /* Hashtable的key和value都不能为null的 HashMap集合的key和value都是可以为null的Hashtable方法都带有synchronized:线程安全的 线程安全有其他的方案,这个Hashtable对线程的处理导致效率较低,使用少Hashtable和HashMap一样,底层都是哈希表数据结构 */ public class HashTableTest01 { public static void main(String[] args) { Map map = new Hashtable(); map.put(null,"123"); //出现异常} }

5. 属性类Properties类
package com.collection; import java.util.Properties; /* 掌握Properties属性类对象的相关方法 Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型 Properties被称为属性类对象 Properties是线程安全的 */ public class PropertiesTest01 { public static void main(String[] args) { //创建一个Properties对象 Properties properties = new Properties(); //需要掌握Properties的两个方法,一个存,一个取 properties.setProperty("url","jdbc:mysql//localhost:3306"); properties.setProperty("username","root"); properties.setProperty("password","123"); // 通过key获取value String url = properties.getProperty("url"); String username = properties.getProperty("username"); String password = properties.getProperty("password"); System.out.println(url); System.out.println(username); System.out.println(password); } }

6. TreeSet集合
  • TreeSet集合底层实际上是一个TreeMap
  • TreeMap集合底层是一个二叉树
  • 放到TreeSet集合中的元素,等同于放到TreeMap集合key部分
  • TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。称为可排序集合
package com.collection; import java.util.TreeSet; /* */ public class TreeSetTest02 { public static void main(String[] args) { //创建一个TreeSet集合 TreeSet ts = new TreeSet<>(); //添加String ts.add("zhangsan"); ts.add("lisi"); ts.add("wangwu"); //遍历 for (String s:ts){ //安装字典顺序,升序 System.out.println(s); }TreeSet ts2 = new TreeSet<>(); ts2.add(100); ts2.add(200); ts2.add(900); ts2.add(10); ts2.add(150); for (Integer elt:ts2){ System.out.println(elt); } } }lisi wangwu zhangsan 10 100 150 200 900

7. 自定义类型实现Comparable接口
package com.collection; import java.util.TreeSet; public class TreeSetTest03 { public static void main(String[] args) { Customer c1 = new Customer(32); Customer c2 = new Customer(20); Customer c3 = new Customer(30); Customer c4 = new Customer(25); //创建TreeSet集合 TreeSet customers = new TreeSet<>(); //添加元素 customers.add(c1); customers.add(c2); customers.add(c3); customers.add(c4); //遍历 for (Customer c : customers){ System.out.println(c); } } }/* 放在TreeSet集合中的元素需要实现java.lang.Comparable接口 并且实现compareTo方法,equals可以不写 */ class Customer implements Comparable{ int age; public Customer(int age) { this.age = age; }//需要在这个方法中编写比较的逻辑:按照什么进行比较 //k.compareTo(t.key) //拿着参数k和集合中每一个k进行比较,返回值可能是><=0 @Override public int compareTo(Customer c) { //c1.compareTo(c2); //this是c1 //c是c2 //c1和c2比较的时候,就是this和c比较 /*int age1 = this.age; int age2 = c.age; if (age1 == age2){ return 0; } else if (age1>age2){ return -1; } else { return 1; }*/return this.age - c.age; //=0 >0 <0 }public String toString(){ return "Customer[age="+age+"]"; } }Customer[age=20] Customer[age=25] Customer[age=30] Customer[age=32]

比较规则怎么写?
package com.collection; import java.util.TreeSet; /* 先按照年龄升序,如果年龄一样再按照姓名升序 */ public class TreeSetTest04 { public static void main(String[] args) { TreeSet vips = new TreeSet<>(); vips.add(new Vip("zhangsan",20)); vips.add(new Vip("zhangsi",20)); vips.add(new Vip("lisi",20)); vips.add(new Vip("king",18)); for (Vip vip : vips){ System.out.println(vip); } } }class Vip implements Comparable{ String name; int age; public Vip(String name, int age) { this.name = name; this.age = age; }@Override public String toString() { return "Vip{" + "name='" + name + '\'' + ", age=" + age + '}'; }@Override public int compareTo(Vip v) { if (this.age == v.age) { //年龄相同按照名字排序 //姓名是String类型,可以直接比较,调用compareTo来完成比较 return this.name.compareTo(v.name); } else { return this.age - v.age; } } }Vip{name='king', age=18} Vip{name='lisi', age=20} Vip{name='zhangsan', age=20} Vip{name='zhangsi', age=20}

CompareTo方法的返回值很重要:
  • 返回0表示相同,value会覆盖
  • 返回>0,会继续在右子树上找
  • 返回<0,会继续在左子树上找
8. 自平衡二叉树数据结构
  • TreeSet/TreeMap是自平衡二叉树,遵循左小右大原则存放,所以这个存放的时候要进行比较
  • TreeSet集合/TreeMap集合采用的是中序遍历方式
  • Iterator迭代器采用的是中序遍历方式
TreeSet集合中元素可排序的第二种方式:使用比较器的方式
放到TreeSet或者TreeMap集合key部分的元素想做到排序,包括两种方式:
  • 第一种:放到集合中的元素实现java.lang.Comparable接口
  • 第二种:在构造TreeSet或者TreeMap集合的时候给他传一个比较器对象
Comparable和Comparator怎么选择?
  • 当比较规则不会发生改变的时候,或者说当比较规则只有一个的时候,建议实现Comparable接口。
  • 如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口
  • Comparator接口的设计符合OCP原则。
package com.collection; import java.util.Comparator; import java.util.TreeSet; public class TreeSetTest05 { public static void main(String[] args) { //创建TreeSet集合的时候,需要使用这个比较器 //TreeSet wuGuis = new TreeSet<>(); 这样不行,没有通过构造方法传递一个比较器进去//给构造方法传递一个比较器 //方式一:TreeSet wuGuis = new TreeSet<>(new WuGuiComparator()); //方式二:大家可以使用匿名内部类的方式(这个类没有名字。直接new接口) TreeSet wuGuis = new TreeSet<>(new Comparator() { @Override public int compare(WuGui o1, WuGui o2) { return o1.age - o2.age; } }); wuGuis.add(new WuGui(1000)); wuGuis.add(new WuGui(800)); wuGuis.add(new WuGui(100)); for (WuGui wuGui : wuGuis){ System.out.println(wuGui); } } }class WuGui { int age; public WuGui(int age) { this.age = age; }@Override public String toString() { return "WuGui{" + "age=" + age + '}'; } }//单独在这里编写一个比较器 //比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的) /* 方式一: class WuGuiComparator implements Comparator{@Override public int compare(WuGui o1, WuGui o2) { return o1.age - o2.age; } }*/WuGui{age=100} WuGui{age=800} WuGui{age=1000}

9. Collections工具类
  • java.util.Collection 集合接口
  • java.util.Collections 集合工具类,方便集合的操作
package com.collection; import java.util.*; public class CollectionsTest { public static void main(String[] args) { //ArrayList集合不是线程安全的 List list = new ArrayList<>(); //变成线程安全 Collections.synchronizedList(list); //排序 list.add("abc"); list.add("abx"); list.add("sbc"); list.add("abe"); Collections.sort(list); for (String s: list){ System.out.println(s); }List dogs = new ArrayList<>(); dogs.add(new Dog(1000)); dogs.add(new Dog(800)); dogs.add(new Dog(100)); //注意:对list集合中元素排序,需要保证list集合中元素实现了:Comparable接口 Collections.sort(dogs); for (Dog dog : dogs){ System.out.println(dog); }//对Set集合怎么排序? Set set = new HashSet<>(); set.add("zhangsan"); set.add("zhangs"); set.add("lisi"); //将Set集合转换成List集合 List myList = new ArrayList<>(set); } }class Dog implements Comparable{ int age; public Dog(int age) { this.age = age; }@Override public int compareTo(Dog o) { return this.age - o.age; }@Override public String toString() { return "Dog{" + "age=" + age + '}'; } }

六. 泛型 1. 泛型的特性 JDK5.0之后推出的新特性:泛型
  • 泛型这种语法机制,只是程序编译阶段起作用,只是给编译器参考的(运行阶段泛型没有)
使用泛型的好处:
  • 第一:集合中存储的元素类型统一
  • 第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的向下转型
使用泛型的缺点:
  • 导致集合中存储的元素缺乏多样性
  • 大多数业务中,集合中元素的类型还是统一的,所有这种泛型特性还是被大家所认可
package com.collection; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenericTest01 { public static void main(String[] args) { /* //不使用泛型机制,分析程序缺点 List myList = new ArrayList(); //准备对象 Cat c = new Cat(); Bird b = new Bird(); //将对象添加到集合当中 myList.add(c); myList.add(b); //遍历集合,取出每个Animal,让他move Iterator it = myList.iterator(); while (it.hasNext()) { //没有这个语法,通过迭代器取出的就是Object //Animal a = it.next(); Object obj = it.next(); //obj中没有move方法,无法调用,需要向下转型 if (obj instanceof Animal) { Animal a = (Animal) obj; a.move(); } } *///使用JDK5.0之后的泛型机制 //使用泛型List之后,表示List集合中只允许存储Animal类型的数据 //用泛型来指定集合中存储的数据类型 List myList = new ArrayList(); //指定List集合中只能存储Animal,那么存储String就编译报错了 //这样用了泛型之后,集合中元素的数据类型更加统一了 //myList.add("abc"); Cat c = new Cat(); Bird b = new Bird(); myList.add(c); myList.add(b); //获取迭代器 //这个表示迭代器迭代的是Animal类型 Iterator it = myList.iterator(); while (it.hasNext()) { //使用泛型之后,每一次迭代返回的数据都是Animal类型 Animal a = it.next(); //这里不需要进行强制类型转换了。直接调用 a.move(); //调用子类型特有的方法还是需要向下转换的 Animal a1 = it.next(); if (a1 instanceof Cat) { Cat c1 = (Cat)a; c1.catchMouse(); } if (a1 instanceof Bird) { Bird b1 = (Bird) a1; b1.fly(); }} } }class Animal{ //父类自带方法 public void move(){ System.out.println("动物在移动"); } }class Cat extends Animal{ //子类特有方法 public void catchMouse(){ System.out.println("猫抓老鼠"); } }class Bird extends Animal{ //子类特有方法 public void fly(){ System.out.println("鸟儿飞翔"); } }

2. 泛型自动类型推断机制 JDK8之后引入:自动类型推断机制(又称钻石表达式)
package com.collection; import sun.corba.Bridge; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenericTest02 { public static void main(String[] args) { //ArrayList<这里的类型会自动推断>(),前提是JDK8之后才允许 //自动类型推断,钻石表达式 List myList = new ArrayList<>(); myList.add(new Animal()); myList.add(new Cat()); myList.add(new Bird()); //遍历 Iterator it = myList.iterator(); while (it.hasNext()) { Animal a = it.next(); a.move(); }List stringList = new ArrayList<>(); //类型不匹配 //stringList.add(new Cat()); //类型不匹配 //stringList.add(123); } }

3. 自定义泛型 自定义泛型的时候<>,尖括号中的是一个标识符,随便写。
java源代码中经常出现的是:< E>和< T>
  • E是element元素单词首字母
  • T是type单词首字母
package com.collection; public class GenericTest03<标识符随便写> {public void doSome(标识符随便写 o){ System.out.println(o); }public static void main(String[] args){ //new对象的时候指定了泛型是:String类型 GenericTest03 gt = new GenericTest03<>(); //类型不匹配 //gt.doSome(100); gt.doSome("abc"); GenericTest03 gt2 = new GenericTest03<>(); gt2.doSome(100); MyIterator mi = new MyIterator<>(); String s1 = mi.get(); MyIterator mi2 = new MyIterator<>(); Animal a = mi2.get(); //不用泛型就是Object类型 GenericTest03 gt3 = new GenericTest03(); gt3.doSome(new Object()); } }class MyIterator{ public T get(){ return null; } }

    推荐阅读