目录
集合的理解和好处
数组
- 长度开始时必须指定,而且一旦指定,不能更改
- 保存的必须为同一类型的元素
- 使用数组进行增加/删除元素的示意代码 - 麻烦
//写出Person数组扩容示意代码
Person[] pers = new Person[1]; //大小是1
pers[0] = new Person();
//增加新的Person对象
Person[] pers2 = new Person[pers.length + 1]; //新创建数组
for() {} //拷贝pers数组的元素到pers2
pers2[pers2.length - 1] = new Person(); //添加新对象
集合
- 可以动态保存任意多个对象,使用比较方便
- 提供了一系列方便的操作对象的方法:add、remove、set、get
- 使用集合添加、删除新元素的示意代码 - 简洁
集合的框架体系
- Java的集合类很多,主要分为两大类【重要,背】
package com.zanedu.collection_;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class Collection_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// Collection
// Map
/*
解读
1. 集合主要是两组(单列集合、双列集合)
2. Collection 接口有两个重要的子接口 List Set,他们的实现子类都是单列集合(单个单个的元素)
3. Map 接口的实现子类 是双列集合(存放的就是K-V)即key和value
*/
//单列集合,放单列数据就是单列集合
ArrayList arrayList = new ArrayList();
arrayList.add("jack");
arrayList.add("tom");
//双列集合,放双列数据就是双列集合
HashMap hashMap = new HashMap();
hashMap.put("NO1", "北京");
hashMap.put("NO2", "上海");
}
}
Collection接口和常用方法
Collection接口实现类的特点
public interface Collection extends Iterable****
- collection实现子类可以存放多个元素,并且每个元素都可以是Object
- 有些Collection的实现类,可以存放重复的元素,有些不可以
- 有些Collection的是实现类,有些是有序的(List),有些不是有序的(Set)
- Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的
- Collection接口常用方法,以实现子类ArrayList来演示
package com.hspedu.collection_;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CollectionMethod_ {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
// add:添加单个元素
list.add("jack");
list.add(10);//list.add(new Integer(10)); //因此他是对象了
list.add(true);
System.out.println(list);
// remove:删除指定元素
// list.remove(0);//删除第一个元素 //返回boolean
list.remove(true);//指定删除某个元素
System.out.println(list);
// contains:查找元素是否存在
System.out.println(list.contains("jack"));//true
// size:获取元素个数
System.out.println(list.size());//2
// isEmpty:判断是否为空
System.out.println(list.isEmpty());//false
// clear:清空
// list.clear();
// System.out.println(list);
// addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println(list);
// containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));//true
// removeAll:删除多个元素
list.add("聊斋");
list.removeAll(list2);
System.out.println(list);
// 说明:以ArrayList实现类来演示.
}
}
Collection接口遍历元素方式1-使用Iterator(迭代器)
基本介绍
- Iterator对象称为迭代器,主要用于遍历 Collection 集合中的元素
- 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
- Iterator仅用于遍历集合,Iterator本身并不存放对象
迭代器的执行原理
Iterator iterator = coll.iterator(); //得到一个集合的迭代器
//hasNext(); 判断是否还有下一个元素
while (iterator.hasNext()) {}
//next()作用:1. 下移 2.将下移以后集合位置上的元素返回
System.out.println(iterator.next());
- 解读:每next一次,就会向下移动一次
- 查看Iterator接口的源码 - 发现如果还有元素就返回true
Iterator接口的方法
- 提示:在调用iterator.next()方法之前****必须要调用iterator.hasNext()来进行检测,看看是否还有元素。若不调用,且之后没有元素,再调用iterator.next()会抛出NoSuchElementException异常
package com.zanedu.collection_;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class CollectionIterator {
@SuppressWarnings({"all"})
public static void main(String[] args) {
//以ArrayList类来演示
Collection col = new ArrayList();
col.add(new Book("三国演义", "罗贯中", 10.1));
col.add(new Book("小李飞刀", "古龙", 5.1));
col.add(new Book("红楼梦", "曹雪芹", 34.1));
// System.out.println(col);
//希望能够遍历col集合
//1. 先得到 col 对于的迭代器
Iterator iterator = col.iterator();
//2. 使用while循环遍历即可
while (iterator.hasNext()) { //判断是否还有数据
//返回下一个元素,类型是Object,什么都可以返回
Object obj = iterator.next();
System.out.println(obj);
}
//快捷键,快速生成 while循环 itit
//显示所有的快捷键的快捷键 ctrl + j
//3. 当退出while循环后,这时iterator迭代器指向了最后的元素
// iterator.next();//抛出异常 NoSuchElementException
//4. 如果希望再次遍历,需要重置我们的迭代器,即将箭头指向最前面的元素
iterator = col.iterator();
System.out.println("===第二次遍历===");
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}
//Book类 - 很简单可以跳
class Book {
private String name;
private String author;
private double price;
public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}
Collection接口遍历对象方式2-for循环增强(即for-each循环)
基本介绍
增强for循环,可以代替iterator迭代器
特点:增强for循环就是简化版的iterator,但底层还是一样的,都是迭代器,只能用于遍历集合或数组
- 基本语法:
for (元素类型 元素名 : 集合名或数组名) {
访问元素
}
案例演示
package com.zanedu.collection_;
import java.util.ArrayList;
import java.util.Collection;
public class CollectionFor {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Collection col = new ArrayList();
col.add(new Book("三国演义", "罗贯中", 10.1));
col.add(new Book("小李飞刀", "古龙", 5.1));
col.add(new Book("红楼梦", "曹雪芹", 34.1));
//解读
//1. 使用增强for循环遍历集合,在Collection集合上使用
//2. 增强for ,底层仍然是迭代器
//3. 增强for可以理解为就是简化版本的迭代器遍历
//4. 快捷方式 I
// for (Object o :) {
//
// }
for (Object obj : col) {
System.out.println(obj);
}
//增强for,也可以直接在数组使用
int[] nums = {1, 8, 10, 90};
for (int i : nums) {
System.out.println(i);
}
}
}
Debug查看源码 - 看底层是否是迭代器
- 在增强for那里打断点,Debug
- 发现直接就进入了iterator迭代器
- **一步步走下去,发现都是迭代器的next和hasNext,因此得出结论:**增强for的底层就是迭代器
练习题
编程题:
- 创建 3 个Dog{name, age}对象,放入到 ArrayList中,赋给 List 引用
- 用迭代器和增强for循环两种方式来遍历
- 重写Dog的toString方法,输出name和age
package com.zanedu.collection_;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CollectionExercise {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("tom", 2));
list.add(new Dog("smith", 1));
list.add(new Dog("jack", 3));
//iterator 迭代器遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
//for循环增强
for (Object o : list) {
System.out.println(o);
}
}
}
/**
* 创建3个Dog {name, age} 对象,放入到ArrayList中,赋给List引用
* 用迭代器和增强for循环两种方式来遍历
* 重写Dog的 toString方法,输出name和age
*/
class Dog {
private String name;
private int age;
public Dog(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 "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
List接口和常用方法
List接口基本介绍
- List接口是Collection接口的子接口
- List集合类中元素有序(即添加顺序和取出顺序一致),且****可重复
- List集合中的每个元素都有其对应的顺序索引,即支持索引
- List容量中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
- JDK API中List接口的是实现类有:
- 常用的有:ArrayList、LinkedList、Vector
package com.zanedu.list_;
import java.util.ArrayList;
import java.util.List;
public class List_ {
public static void main(String[] args) {
//1. List集合类中元素有序(即添加顺序和取出顺序一致),且可重复
List list = new ArrayList();
list.add("jack");
list.add("tom");
list.add("mary");
list.add("zan");
list.add("tom");
System.out.println(list);
//2. List集合中的每个元素都有其对应的顺序索引,即支持索引
//索引从0开始
System.out.println(list.get(3));//zan
}
}
List接口的常用方法
package com.zanedu.list_;
import java.util.ArrayList;
import java.util.List;
public class ListMethod {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add("张三丰");
list.add("贾宝玉");
// void add(int index, Object ele):在index位置插入ele元素
//在 index = 1 的位置插入一个对象
list.add(1, "zan");
System.out.println(list);
// boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
List list2 = new ArrayList();
list2.add("jack");
list2.add("tom");
list.addAll(1, list2);
System.out.println(list);
// Object get(int index):获取指定index位置的元素
// int indexOf(Object obj):返回obj在集合中首次出现的位置
System.out.println(list.indexOf("tom"));//2
// int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
list.add("zan");
System.out.println(list.lastIndexOf("zan"));//5
// Object remove(int index):移除指定index位置的元素,并返回此元素
list.remove(0);
System.out.println(list);
// Object set(int index, Object ele):设置指定index位置的元素为ele , 相当于是替换.
list2.set(1, "玛丽");
// list2.set(10, "玛丽");//越界会抛出异常IndexOutOfBoundsException
System.out.println(list2);
// List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
// 注意返回的子集合 fromIndex <= subList < toIndex == 左闭右开
List reslist = list.subList(0, 2);
System.out.println(reslist);
}
}
List接口课堂练习1
需求:添加10个以上的元素(比如String “hello”),在2号位插入一个元素"zan",获得第五个元素,删除第六个元素,修改第七个元素,再使用迭代器遍历集合,要求:使用List的实现类ArrayList完成
package com.zanedu.list_;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class ListExercise {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
for (int i = 1; i < 12; i++) {
list.add("hello" + i);
}
System.out.println(list);
list.add(2, "zan");
System.out.println(list);
System.out.println("第五个元素=" + list.get(4));
//删除第6个元素
list.remove(5);
System.out.println(list);
//修改第7个元素
list.set(6, "三国演义");
System.out.println(list);
//使用迭代器遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println(obj);
}
}
}
/**
* 添加10个以上的元素(String "hello"),在2号位插入一个元素"zan";
* 获得第5个元素,删除第6个元素,修改第7个元素,在使用迭代器遍历集合
* 要求:使用List的实现类ArrayList完成
*/
List的三种遍历方式【ArrayList、LinkedList、Vector】
- 方式一:使用iterator迭代器
Iterator iter = col.iterator();
while (iter.hasNext()) {
Object o = iter.next();
}
- 方式二:使用增强for循环
for (Object o : col) {
}
- 方式三:使用普通for循环
for (int i = 0; i < list.size(); i++) {
Object obj = list.get(i);
System.out.println(obj);
}
package com.zanedu.list_;
import java.util.*;
public class ListFor {
@SuppressWarnings({"all"})
public static void main(String[] args) {
//List 接口的实现子类 Vector LinkedList ArrayList都可以
// List list = new ArrayList();
// List list = new Vector();
List list = new LinkedList();
list.add("jack");
list.add("tom");
list.add("鱼香肉丝");
list.add("北京烤鸭");
//遍历
//1. 迭代器
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
//2.增强for循环
System.out.println("==增强for==");
for (Object o : list) {
System.out.println(o);
}
//3. 普通for循环
System.out.println("==普通for==");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
实现类的课堂练习2
使用List的实现类添加三本图书,并遍历
需求:
- 按价格排序,从低到高(使用冒泡法)
- 要求使用ArrayList、Linked List、Vector三种集合实现
package com.zanedu.list_;
import java.util.*;
@SuppressWarnings({"all"})
public class ListExercise02 {
public static void main(String[] args) {
// List list = new ArrayList();
// List list = new Vector();
List list = new LinkedList();
list.add(new Book("红楼梦", "曹雪芹", 100));
list.add(new Book("三国演", "罗贯中", 10));
list.add(new Book("西游记", "吴承恩", 120));
//如何对集合进行排序
//遍历
for (Object o : list) {
System.out.println(o);
}
//冒泡排序
sort(list);
System.out.println("===排序后===");
for (Object o : list) {
System.out.println(o);
}
}
//静态方法
public static void sort(List list) {
for (int i = 0; i < list.size(); i++) {
for (int j = 0; j < list.size() - i - 1; j++) {
//取出对象Book
//这里只有向下转型,将其编译类型转成Book,才能调用Book类里面的方法
Book book1 = (Book)list.get(j);
Book book2 = (Book)list.get(j + 1);
if (book1.getPrice() > book2.getPrice()) {
list.set(j, book2);
list.set(j + 1, book1);
}
}
}
}
}
/**
* 使用List的实现类添加三本图书,并遍历
* 要求:按价格排序,从低到高(使用冒泡)
* 要求使用ArrayList、LinkedList、Vector三种集合实现
*/
class Book {
private String name;
private String author;
private double price;
public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "名称:" + name + "\t\t价格:" + price + "\t\t作者:" + author;
}
}
ArrayList底层结构和源码分析
ArrayList的注意事项
- **permits all elements,including null,**ArrayList可以加入null,并且可以加入多个
- ArrayList是由数组来实现数据存储的
- ArrayList基本等同于Vector,除了ArrayList是线程不安全(但是执行效率高),因此****在多线程情况下,不建议使用ArrayList
package com.zanedu.list_;
import java.util.ArrayList;
public class ArrayListDetail {
@SuppressWarnings({"all"})
public static void main(String[] args) {
//ArrayList 是线程不安全的,可以看源码,没有synchronized关键字修饰
/*
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
*/
ArrayList arrayList = new ArrayList();
arrayList.add(null);
arrayList.add("jack");
arrayList.add(null);
arrayList.add(null);
System.out.println(arrayList);
}
}
ArrayList的底层操作机制源码分析(重点、难点)
结论:
- **ArrayList中维护了一个Object类型的数组elementDate.**transient **Object[] elementData; //**transient 表示瞬间,短暂的,表示该属性不会被序列化
- 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第一次添加后,扩容elementData为10,如需要再次扩容,则扩容elementData为原先的1.5倍
- 如果使用的是指定大小的构造器(即有参构造器),则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍
package com.zanedu.list_;
import java.util.ArrayList;
public class ArrayListSource {
@SuppressWarnings({"all"})
public static void main(String[] args) {
//注意:Idea再默认情况下,Debug显示的数据是简化后的,如果希望看到完整的数据
//需要做设置,
//使用无参构造器创建ArrayList对象
// ArrayList list = new ArrayList();
ArrayList list = new ArrayList(8);
//使用for循环给list集合添加1-10数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
//使用for循环给list集合添加 11-15数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}
list.add(100);
list.add(200);
list.add(null);
}
}
- 查看源码 - Debug
- 无参构造器
- 首先就是创建了一个空的elementData数组 = {}
- 执行list.add()
- 先确定是否要扩容
- 然后再执行赋值操作
- 该方法确定minCapacity
第一次扩容为10
- 1. modCount++:记录集合被修改的次数
- 2. 如果elementData的大小不够,就调用grow()去扩容
- 真的扩容
- 使用了扩容机制来确定要扩容到多大
- 第一次newCapacity = 10
- 第二次及其以后,按照1.5倍扩容(因为oldCapacity >> 1 代表除以2,即1 + 0.5 = 1.5倍)
- 扩容使用的是Arrays.copyof()
- 有参构造器
- 与无参构造器的区别在于刚开始创建数组的时候
- 有参构造器是创建了一个指定大小elementData数组
- elementData = new Object[initialCapacity]
- 有参构造器的扩容机制:第一次扩容,就按照elementData的1.5倍扩容,整个的执行流程还是跟前面无参构造器的一样
Vector底层结构和源码剖析
Vector的基本介绍
- Vector类的定义说明
- Vector底层也是一个对象数组:protected Object[] elementData;
- Vector是线程同步的,即线程安全**,Vector类的操作方法带有synchronized**
- 在开发中,需要线程同步安全时,考虑使用Vector
package com.zanedu.list_;
import java.util.Vector;
@SuppressWarnings({"all"})
public class Vector_ {
public static void main(String[] args) {
//无参构造器
//有参数的构造
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i);
}
vector.add(100);
System.out.println("vector=" + vector);
//解读源码
//1. new Vector() 底层
/*
public Vector() {
this(10);
}
补充:如果是 Vector vector = new Vector(8);
走的方法:
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
2. vector.add(i)
2.1 //下面这个方法就添加数据到vector集合
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
2.2 //确定是否需要扩容 条件 : minCapacity - elementData.length>0
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
2.3 //如果 需要的数组大小 不够用,就扩容 , 扩容的算法
//newCapacity = oldCapacity + ((capacityIncrement > 0) ?
// capacityIncrement : oldCapacity);
//就是扩容两倍.
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
*/
}
}
- 源码分析 - Debug
- 添加数据到vector集合
- 确定是否需要扩容 条件 : minCapacity - elementData.length>0
- 如果需要的数组大小不够用,就扩容 , 扩容的算法 - 为扩容2倍
- 结论:
- 无参构造器刚开始开辟10个空间的数组,有参构造器就开辟特定大小的数组
- 如果需要的数组大小不够用,就扩容2倍
Vector和ArrayList的比较
LinkedList底层结构
LinkedList的全面说明
- LinkedList底层实现了双向链表和双端队列特点
- 可以添加任意元素(元素可以重复),包括null
- 线程不安全,没有实现同步
LinkedList的底层操作机制
- LinkedList底层维护了一个双向链表
- LinkedList中维护了两个属性****first和last,分别指向首节点和尾节点
- 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现了双向链表
- 所以LinkedList的元素的添加和删除,不是通过数组来完成的,相对来说效率较高
- 模拟一个简单的双向链表
package com.zanedu.list_;
public class LinkedList01 {
public static void main(String[] args) {
//模拟一个简单的双向链表
Node jack = new Node("jack");
Node tom = new Node("tom");
Node zan = new Node("zan");
//连接三个节点,形成双向链表
//jack -> tom -> zan
jack.next = tom;
tom.next = zan;
//zan -> tom -> jack
zan.pre = tom;
tom.pre = jack;
Node first = jack; //让first引用指向jack,就是双向链表的头节点
Node last = zan;//让last引用指向zan,就是双向链表的尾节点
//演示从头到尾进行遍历
System.out.println("===从头到尾进行遍历===");
while (true) {
if (first == null) {
break;
}
//输出first的信息
System.out.println(first);
first = first.next;
}
//演示从尾到头进行遍历
System.out.println("===从尾到头进行遍历===");
while (true) {
if (last == null) {
break;
}
System.out.println(last);
last = last.pre;
}
//演示链表的添加对象/数据,是多么的方便
//在 tom 和 zan 之间插入一个对象 smith
//1. 先创建一个Node节点,名字为smith
Node smith = new Node("smith");
//下面就把smith加入到了双向链表
smith.next = zan;
smith.pre = tom;
zan.pre = smith;
tom.next = smith;
//重置first,让first再次指向jack
first = jack;
System.out.println("===从头到尾进行遍历===");
while (true) {
if (first == null) {
break;
}
//输出first的信息
System.out.println(first);
first = first.next;
}
last = zan;
//让last指向最后一个节点
//演示从尾到头进行遍历
System.out.println("===从尾到头进行遍历===");
while (true) {
if (last == null) {
break;
}
System.out.println(last);
last = last.pre;
}
}
}
//定义一个Node类,Node对象表示双向链表的一个节点
class Node {
public Object item;//真正存放数据的地方
public Node next;//指向后一个节点
public Node pre;//指向前一个节点
public Node(Object item) {
this.item = item;
}
@Override
public String toString() {
return "Node name=" + item;
}
}
LinkedList的增删改查案例
package com.zanedu.list_;
import java.util.Iterator;
import java.util.LinkedList;
@SuppressWarnings({"all"})
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println(linkedList);
/*
演示一个删除节点的
*/
linkedList.remove();//默认删除第一个节点
//linkedList.remove(2);
System.out.println(linkedList);
//修改某个节点对象
linkedList.set(1, 999);
System.out.println(linkedList);
//得到某个节点对象
//get(1)是得到双向链表的第二个对象
System.out.println(linkedList.get(1));
//因为LinkedList是实现了List接口,所以遍历方式可以迭代器,也可以增强for
System.out.println("===LinkedList遍历迭代器===");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
System.out.println("===LinkedList的在增强for===");
for (Object o : linkedList) {
System.out.println(o);
}
System.out.println("===普通for===");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
/*
linkedList.remove()源码解读
1. 执行removeFirst
public E remove() {
return removeFirst();
}
2. 执行
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
3. 执行unlinkFirst,将 f 指向的双向链表的第一个节点拿掉(删除)
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
*/
/*
linkedList.add源码解读
1. LinkedList linkedList = new LinkedList();
public LinkedList() {}
2. 这时 LinkedList 的属性 first = null,last = null
3. 执行
public boolean add(E e) {
linkLast(e);
return true;
}
4. 将新的节点,加入到双向链表的最后
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
*/
}
}
- linkedList.add源码解读
- (1)LinkedList linkedList = new LinkedList();
- 这时LinkedList的属性 first = null,last = null
- (2)执行
- (3)将新的节点,加入到双向链表的最后
- 第一次add结束后
- linkedList.remove()源码解读
- (1)执行removeFirst
- (2)执行
- (3)执行unlinkFirst,将 f 指向的双向链表的第一个节点拿掉(删除)
LinkedList和ArrayList比较
- 如何选择ArrayList和LinkedList
- 如果我们改查的操作多,选择ArrayList
- 如果我们增删的操作多,选择LinkedList
- 一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
- 在一个项目中,根据业务灵活选择,可能会这样,即一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择
- 注意:两个都是线程不安全的
Set接口和常用方法
Set接口基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 不允许出现重复元素,所以最多包含一个null
- JDK API中Set接口的实现类有:
Set接口的常用方法
- 和List接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
package com.zanedu.set_;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@SuppressWarnings({"all"})
public class SetMethod {
public static void main(String[] args) {
//解读
/*
1. 以Set接口的实现类 HashSet 来讲解Set接口的方法
2. Set 接口的实现类的对象(Set接口对象),不能存放重复的元素,可以添加一个null
3. Set接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)
4. 注意:取出的顺序是固定的,虽然不是添加的顺序(有序),但是是固定的
*/
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john");//重复
set.add("jack");
set.add("zan");
set.add(null);
set.add(null);//再次添加null
System.out.println(set);
set.remove(null);
System.out.println(set);
}
}
Set接口的遍历方式
同Collection的遍历方式一样,因为Set接口是Collection接口的子接口
- 可以使用迭代器
- 增强for循环
- 不能使用索引的方式来获取(即不能使用普通循环)
package com.zanedu.set_;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
@SuppressWarnings({"all"})
public class SetMethod {
public static void main(String[] args) {
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("jack");
set.add("zan");
set.add(null);
//遍历
//方式一:迭代器
System.out.println("===使用迭代器===");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object o = iterator.next();
System.out.println(o);
}
//方式二:增强for
System.out.println("===使用增强for===");
for (Object o : set) {
System.out.println(o);
}
//Set接口对象,不能通过索引来获取
}
}
Set接口实现类-HashSet
HashSet的全面说明
- HashSet实现了Set接口
- HashSet实际上是HashMap**(看源码)**
- 可以存放null值,但是只能有一个null
- HashSet不保证元素是有序的,取决于hash值,再确定索引的结果(即不能保证存放元素的顺序和取出顺序一致)
- 不能有重复元素/对象
package com.zanedu.set_;
import java.util.HashSet;
import java.util.Set;
@SuppressWarnings("all")
public class HashSet_ {
public static void main(String[] args) {
//解读
//1. 构造器走的源码
/*
public HashSet() {
map = new HashMap<>();
}
2. HashSet 可以存放null,但是只能有一个null,即不能重复
*/
Set set = new HashSet();
set.add(null);
set.add(null);
System.out.println(set);
}
}
HashSet案例说明
package com.zanedu.set_;
import java.util.HashSet;
@SuppressWarnings("all")
public class HashSet01 {
public static void main(String[] args) {
//说明
//1. 在执行add方法后,会返回一个boolean
//2. 如果添加成功,返回true,否则返回false
//3. 可以通过remove指定删除哪个对象
HashSet set = new HashSet();
System.out.println(set.add("john"));//true
System.out.println(set.add("lucy"));//true
System.out.println(set.add("john"));//false
System.out.println(set.add("jack"));//true
System.out.println(set.add("Rose"));//true
set.remove("john");
System.out.println(set);
set = new HashSet();
System.out.println(set);
//4. HashSet不能添加相同的元素/数组?
set.add("lucy");//添加成功
set.add("lucy");//加入不了
//都能加进去两只狗,只是名字相同而已
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//OK
System.out.println(set);
//再加深一下,非常经典的面试题
//看源码,做分析
//去看它的源码,即add到底发生了什么,看下面的底层机制分析
set.add(new String("zan"));//ok
set.add(new String("zan"));//添加不进去
System.out.println(set);
}
}
class Dog { //定义了一个Dog类
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
HashSet底层机制说明
-
分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)
-
模拟一个简单的数组+链表结构
package com.zanedu.set_;
@SuppressWarnings("all")
public class HashSetStructure {
public static void main(String[] args) {
//模拟一个 HashSet的底层(HashMap的底层结构)
//1. 创建一个数组,数组的类型是Node[]
//2. 有些人直接把 Node[] 数组称为 表
Node[] table = new Node[16];
System.out.println(table);
//3. 创建节点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack;//将 jack节点挂载到john后边
Node rose = new Node("Rose", null);
jack.next = rose;//将 rose节点挂载到jack后边
Node lucy = new Node("lucy", null);
table[3] = lucy; //把lucy 放到 table表的索引为3的位置
System.out.println(table);
}
}
class Node { //代表节点,存储数据,可以指向下一个节点,从而形成链表
Object item; //存放数据
Node next; //指向下一个节点
public Node(Object item, Node next) { //这里是单列链表
this.item = item;
this.next = next;
}
}
-
以上就是简单的数组+链表结构
-
接下来分析HashSet的添加元素底层是如何实现的(hash() + equals())
结论:
- HashSet底层是HashMap
- 添加一个元素时,会先得到hash值,然后会转成索引值
- 找到存储数据表table,看这个索引位置是否已经存放过元素,即含有元素
- 如果没有,则直接加入
- 如果有,则调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后(equals方法可以进行重写,由程序员决定)
- 在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table表的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(即转换成红黑树)
package com.zanedu.set_;
import java.util.HashMap;
import java.util.HashSet;
@SuppressWarnings("all")
public class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");//到此为止,第一次add分析完毕
hashSet.add("php");//到此为止,第二次add分析完毕
hashSet.add("java");
System.out.println(hashSet);
/*
对HashSet的源码解读
1. 先执行 HashSet()构造器
public HashSet() {
map = new HashMap<>();
}
2. 执行add()
public boolean add(E e) { // e = "java"
return map.put(e, PRESENT)==null; // PRESENT == private static final Object PRESENT = new Object();
}
3. 再执行 put() - 该方法会执行 hash(key)方法,得到 key对应的hash值,不是hashCode,是经过处理的
public V put(K key, V value) {// key = "java",value = PRESENT,共享的
return putVal(hash(key), key, value, false, true);
}
4. 执行 hash(key) - 为了求出hash值 - 算法如下
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5. 执行 putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i; //定义了辅助变量
//table 就是hashMap的一个属性,类型是Node[]
//if 语句表示如果当前table是null或者大小为0
//那么就是第一次扩容,到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据key得到的hash值,去计算该key应该存放到table表的哪个索引位置
//并把这个位置的对象,赋给 p
//(2)判断p是否为null
//(2.1)如果p为null,表示还没有存放元素,那么就创建一个Node(key = "java", value = "PRESENT"`)
// 就放在该位置 tab[i] = newNode(hash, key, value, null);
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//开发技巧提示:在需要的辅助变量(局部变量)时候,在创建
Node<K,V> e; K k;//辅助变量
//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
//并且满足下面两个条件之一:
//(1)准备加入的key 和 p 指向的Node节点的 key是同一个对象
//(2)或者p 指向的Node节点的 key 的equals() 和准备加入的key比较后相同
//说明不能往里面添加了
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断 p 是不是一颗红黑树
//如果是一颗红黑树,就调用 putTreeVal,来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果table对应的索引位置,已经是一个链表了,就使用for循环比较
//(1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
// 注意:在把元素添加到链表后,立即判断该链表是否已经达到8个节点
就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
注意:在转成红黑树时,还进行一个判断:表的大小要 >= 64,如下
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
如果该table数组的大小 < 64,即上面条件成立,则会对table表扩容
只有当上面条件不成立时,才进行转成红黑树
//(2)在依次和该链表的每一个元素比较过程中,如果有相同的情况,就直接break
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//这个TREEIFY_THRESHOLD是 8
treeifyBin(tab, hash);
break; //第一种情况退出
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break; //第二种情况退出
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//size就是我们每加入一个节点Node(key,value,hash,next),size就会++
//不管节点加在table的第一个位置,还是加在链表的某一个位置,都会++
if (++size > threshold)
resize();//扩容
afterNodeInsertion(evict);
return null;
}
*/
}
}
- 源码解读
- (1)先执行 HashSet()构造器
- (2)执行add(),这里的PRESENT => private static final Object PRESENT = new Object();
- (3)再执行 put() - 该方法会执行 hash(key)方法,得到 key对应的hash值,不是hashCode,是经过处理的
- (4)执行 hash(key) - 为了求出hash值 - 算法如下
- (5)执行 putVal()
定义了辅助变量
table就是hashMap的一个属性,类型是Node[]
if语句表示如果当前table是null或者大小为0,那么就是第一次扩容,扩容16个空间
(1)根据key的带的hash值,去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象,赋给p
(2)判断p是否为null
(2.1)如果p为null,表示还没有存放元素,那么就创建一个Node(key = “java”, value = “PRESENT”),就放在该位置tab[i] = newNode(hash, key, value, null);
- 进入上面的else里面
如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样,并且满足以下的两个条件之一
(1)准备加入的key和p指向的Node节点的key是同一个对象
(2)或者p指向的Node节点的key的equals()和准备加入的key比较后相同
那么就说明不能往里面添加
- 上面的if条件不成立
那么就再判断p是不是一颗红黑树,如果是一颗红黑树,就调用putTreeVal方法,来进行添加
- 上面的else if条件不成立
如果table表对应的索引位置,已经是一个链表了,就使用for循环比较
(1)依次和该链表的每一个元素进行比较,如果都不相同,则加入到该链表的最后
注意:在把元素添加到链表后,立即判断该链表是否已经达到8个节点,如果达到,就调用treeifyBin(),对当前这个链表进行树化(转成红黑树),但是要注意的是,在转成红黑树之前,还需要进行一个判断,即表的大小要>=64如下:
如果该table数组的大小<64,即上面条件成立,则会对table表进行扩容2倍,只有当上面的条件不成立时,才进行转成红黑树
(2)在依次和该链表的每一个元素比较过程中,如果有相同的情况,就会直接break
- 最后的size
size就是我们每加入一个节点Node(key, value, hash, next),size就会++
不管节点夹在table的第一个位置,还是加在链表的某一个位置,都会++
- 分析HashSet的扩容和转成红黑树机制
结论:
- HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75 = 12
- 如果table数组使用到了临界值 12,就会扩容到 16*2 = 32,新的临界值就是 32*0.75=24,以此类推
- 在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table表的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),否则仍然采用数组扩容机制
- 底层分析
package com.zanedu.set_;
import java.util.HashSet;
@SuppressWarnings("all")
public class HashSetIncrement {
public static void main(String[] args) {
/*
HashSet底层是HashMap,第一次添加时, table 数组扩容到 16
临界值(threshold) 是 16*加载因子(loadFactor)是0.75 = 12
如果 table数组使用到了临界值 12,就会扩容到 16*2=32,而新的临界值就是32*0.75=24,依次类推
*/
HashSet hashSet = new HashSet();
// for (int i = 0; i < 100; i++) {
// hashSet.add(i);
// }
/*
在Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),满足了这个条件,都会去判断table的大小
并且table的大小 >= MIN_TERRIFY_CAPACITY(默认是64),就会进行树化(红黑树)
否则仍然采用数组扩容机制
*/
//这里为了将一条链表的元素个数达到8,因此这里重写了hashCode方法,只是为了将hash值一样
// for (int i = 0; i <= 12; i++) {
// hashSet.add(new A(i));//
// }
/*
当我们向hashSet增加一个元素时,到底层会封装成 Node -> 加入table表,就算是增加了一个 size++
//不管节点加在table的第一个位置,还是加在链表的某一个位置,都会++
*/
for (int i = 1; i <= 7; i++) { //在table表的某一条链表上添加了7个对象
hashSet.add(new A(i));
}
for (int i = 1; i <= 7; i++) { //在table表的另一条链表上添加了7个对象
hashSet.add(new B(i));
}
}
}
class B {
private int n;
public B(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;
}
}
- 情况分析:
刚开始添加,table表扩容到16
由于临界值是12,因此当table表添加到12的时候,再进行添加,table表会进行扩容
- table表有12个
- 再次添加后 - 进行了扩容
- 情况分析:
- 同一个索引上有8条数据
- 再在这条链表上添加元素,会去查看table的大小是否>=64,若不大于,则会扩容
- 若还在这条链表上添加数组,则仍然会去判断table表是否>=64,若没有64,则会扩容
- 再进行添加,又会去判断,这时table表已经有64个数据了,因此不会再扩容,会将其转换成红黑树
- 我们看到红黑树里就会有left、right等等属性
- 情况分析:
由于下面在定义类的时候,重写了hashCode的方法,因此计算的hash值会一致
我们可以看到,在结束上一个循环的时候,size已经为7,现在我们在另一个表添加对象,看看是否会变化?
我们发现再次添加的时候,它的size变成了8
因此:不管节点加在table的第一个位置,还是加在链表的某一个位置,size都会++
HashSet课堂练习
题一
定义一个Employee类,该类包含:private成员属性name,age
需求:
- 创建3个Employee对象放入HashSet中
- 当name和age的值相同时,认为是相同员工,不能添加到HashSet集合中
package com.zanedu.set_;
import java.util.HashSet;
import java.util.Objects;
@SuppressWarnings("all")
public class HashSetExercise {
public static void main(String[] args) {
/**
定义一个Employee类,该类包含:private成员属性name,age 要求:
创建3个Employee 对象放入 HashSet中
当 name和age的值相同时,认为是相同员工, 不能添加到HashSet集合中
*/
HashSet hashSet = new HashSet();
hashSet.add(new Employee("jack", 20));//ok
hashSet.add(new Employee("zan", 18));//ok
hashSet.add(new Employee("jack", 20));//no
//未重写的时候,加入了3个
//因为hash值不同
System.out.println(hashSet);
}
}
//创建Employee
class Employee {
private String name;
private int age;
public Employee(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 "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//如果name和age的值相同,则返回相同的hash值
//alt+hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
题二
定义一个Employee类,该类包含:private成员属性name,sal,birthday(MyDate类型),其中birthday为MyData类型(属性包括year、month、day)
需求:
- 创建3个Employee放入HashSet中
- 当name和birthday的值相同时,认为是相同员工,不能添加到HashSet集合中
package com.zanedu.set_;
import java.util.HashSet;
import java.util.Objects;
@SuppressWarnings({"all"})
public class HashSetExercise02 {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Employee1("jack", 20, new MyData(10, 10, 10)));
hashSet.add(new Employee1("zan", 20, new MyData(12, 10, 10)));
hashSet.add(new Employee1("jack", 20, new MyData(10, 10, 10)));
System.out.println(hashSet);
}
}
class MyData {
private int year;
private int month;
private int day;
public MyData(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyData myData = (MyData) o;
return year == myData.year && month == myData.month && day == myData.day;
}
@Override
public int hashCode() {
return Objects.hash(year, month, day);
}
}
class Employee1 {
private String name;
private double sal;
private MyData birthday;
public Employee1(String name, double sal, MyData birthday) {
this.name = name;
this.sal = sal;
this.birthday = birthday;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSal() {
return sal;
}
public void setSal(double sal) {
this.sal = sal;
}
public MyData getBirthday() {
return birthday;
}
public void setBirthday(MyData birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "Employee1{" +
"name='" + name + '\'' +
", sal=" + sal +
", birthday=" + birthday +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee1 employee1 = (Employee1) o;
return Objects.equals(name, employee1.name) && Objects.equals(birthday, employee1.birthday);
}
@Override
public int hashCode() {
return Objects.hash(name, birthday);
}
}
Set接口实现类-LinkedHashSet
LinkedHashSet的全面说明
- LinkedHashSet是HashSet的子类
- LinkedHashSet底层是一个LinkedHashMap,底层维护了一个****数组+双向链表
- LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的
- LinkedHashSet不允许添加重复元素
LinkedHashSet底层机制说明
说明:
- 在LinkedHashSet中维护了一个hash表和双向链表(LinkedHashSet有head和tail)
- 每一个节点有before和after属性,这样可以形成双向链表
- 在添加一个元素时,先求hash值,在求索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和HashSet一样】)
- 这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致
package com.zanedu.set_;
import java.util.LinkedHashSet;
import java.util.Set;
@SuppressWarnings("all")
public class LinkedHashSetSource {
public static void main(String[] args) {
//分析一下LinkedHashSet的底层机制
Set set = new LinkedHashSet();
set.add(new String("AA"));
set.add(456);
set.add(456);
set.add(new Customer("刘", 1001));
set.add(128);
set.add("zan");
System.out.println(set);
//解读
//1. LinkedHashSet 加入顺序和取出元素/数据的顺序一致
//2. LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
//3. LinkedHashSet 底层结构(数组table + 双向链表)
//4. 添加第一次时,直接将数组table扩容到 16,并且存放的节点的类型是 LinkedHashMap$Entry
//5. 数组是 HashMap$Node[] ,但是存放的元素/数据是 LinkedHashMap$Entry类型
/*
//继承关系是在内部类完成的
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
*/
}
}
class Customer {
private String name;
private int n;
public Customer(String name, int n) {
this.name = name;
this.n = n;
}
}
- LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
- 添加第一次时,直接将数组table扩容到 16,并且存放的节点的类型是 LinkedHashMap$Entry
- (table表)数组是 HashMap N o d e [ ] ∗ ∗ ∗ ∗ , 但 是 存 放 的 元 素 / 数 据 是 ∗ ∗ ∗ ∗ L i n k e d H a s h M a p Node[]** **,但是存放的元素/数据是** **LinkedHashMap Node[]∗∗∗∗,但是存放的元素/数据是∗∗∗∗LinkedHashMapEntry****类型(向上转型)
- 这就是由于继承关系或者实现关系
- 注意:这个继承关系是在内部类完成的
- 添加第二个元素后,就形成双向链表了
- 还有那个head和tail,指向头和尾
LinkedHashSet练习题
Car类(属性:name、price),如果name和price一样,则认为是相同元素,就不能添加
package com.zanedu.set_;
import java.util.LinkedHashSet;
import java.util.Objects;
@SuppressWarnings("all")
public class LinkedHashSetExercise {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car("奥拓", 1000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//OK
linkedHashSet.add(new Car("法拉利", 10000000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//加入不了
linkedHashSet.add(new Car("保时捷", 70000000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//加入不了
System.out.println(linkedHashSet);
}
}
class Car {
private String name;
private double price;
public Car(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "Car{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Double.compare(car.price, price) == 0 && Objects.equals(name, car.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
Set接口实现类-TreeSet
- 要注意的是TreeSet的add机制,若使用匿名内部类,则当返回的是0就无法添加成功
package com.zanedu.set_;
import java.util.Comparator;
import java.util.TreeSet;
@SuppressWarnings({"all"})
public class TreeSet_ {
public static void main(String[] args) {
/*
解读
1. 当我们使用无参构造器,创建 TreeSet时,是有序的,会按照首字母来排序
2. 需求:添加的元素,按照字符串大小来排序
3. 使用TreeSet提供的构造器,可以传入一个比较器(匿名内部类)
并指定排序规则
4. 看源码
*/
// TreeSet treeSet = new TreeSet();
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//下面调用String的 compareTo方法进行字符串大小比较
//如果要求加入的元素,按照长度大小排序
// return ((String)o2).compareTo((String)o1);
return ((String)o2).length() - ((String)o1).length();
}
});
//添加数据
treeSet.add("jack");
treeSet.add("tom");//3个大小
treeSet.add("za");
treeSet.add("a");
treeSet.add("tom");
treeSet.add("abc");//加入不了
System.out.println(treeSet);
/*
解读
1. 构造器会把传入的比较器对象,给到底层,即赋给了 TreeSet的底层的 TreeMap的属性 this.comparator = comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 第一次添加:把k-v 封装到 Entry对象,放入root
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check,检测是否为空,为空就抛异常
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
3. 在调用 treeSet.add("tom") ,在底层会执行到
if (cpr != null) { //cpr就是我们的匿名内部类(对象)
do {
parent = t;
cmp = cpr.compare(key, t.key); //动态绑定到我们的匿名内部类的compare方法
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果相等,即返回0,这个key就没有加入
return t.setValue(value);
} while (t != null);
}
*/
}
}
Map接口和常用方法
Map接口实现类的特点
- 注意:这里是JDK8的Map接口特点
- Map与Collection并列存在,用于保存具有映射关系的数据 Key-Value
- Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
- Map中的key不允许重复,原因和HashSet一样
- Map中的value可以重复
- Map的key可以为吧null,value也可以是null,注意key为null,只能有一个,但是value为null,可以有多个
- 常用String类作为Map的key
- key 和 value 之间存在单向一对一关系,即通过指定的key总能找到对应的value
package com.zanedu.map_;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings({"all"})
public class Map_ {
public static void main(String[] args) {
//Map接口实现类的特点,使用实现类HashMap
//1. Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value(双列元素)
//2. Map中的key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
//3. Map中的key不允许重复,原因为HashSet一样
//4. Map中的value可以重复
//5. Map 的key可以为null,value也可以为null,但是注意key为null,只能有1个,但value可以有多个
//6. 常用String类作为Map的 key
//7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
Map map = new HashMap();
map.put("no1", "大帅哥");//k-v
map.put("no2", "张无忌");//k-v
map.put("no1", "张三丰");//当有相同的key时,就等价于替换
map.put("no3", "张三丰");//k-v
map.put(null, null);//k-v
map.put(null, "abc");//等价替换
map.put("no4", null);//k-v
map.put("no5", null);//k-v
map.put(1, "赵敏");
map.put(new Object(), "金毛狮王");
//通过get方法,传入key,会返回对应的value
System.out.println(map.get("no2"));
System.out.println(map);
}
}
- Map存放数据的key-value示意图,一对k-v是放在HashMap$Node中的,因为Node实现了Entry接口,有些书上说一对k-v就是一个Entry
真正的Key和Value是存在HashMap$Node中的,而Set和Collection集合只是指向了它,只是建立了一个引用
- Node实现了Entry接口
- 深入讲解
package com.zanedu.map_;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@SuppressWarnings({"all"})
public class MapSource_ {
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1", "大帅哥");//k-v
map.put("no2", "张无忌");//k-v
map.put(new Car(), new Person());
//解读
//1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)
//2. k-v 为了方便程序员的遍历,还会创建 EntrySet集合,该集合存放的元素类型是 Entry
// 而一个 Entry对象就有 k,v EntrySet<Entry<K,V>> 即:public Set<Map.Entry<K,V>> entrySet()
//3. 在entrySet中,定义的类型是 Map.Entry,但是实际上存放的还是 HashMap$Node
//为什么能够放进去,这是因为 HashMap$Node implements Map.Entry
//即底层代码为:static class Node<K,V> implements Map.Entry<K,V>
//4. 这样当把 HashMap$Node 对象存放到 entrySet后,就方便我们的遍历了,因为Map.Entry提供了重要方法
// K getKey(); V getValue();
Set set = map.entrySet();
System.out.println(set.getClass()); // HashMap$EntrySet
for (Object obj : set) {
// System.out.println(obj.getClass()); //HashMap$Node
//为了从HashMap$Node中取出k-v
//1. 先做一个向下转型
Map.Entry entry = (Map.Entry)obj;
System.out.println(entry.getKey() + "-" + entry.getValue());
}
Set set1 = map.keySet();
System.out.println(set1.getClass());
Collection values = map.values();
System.out.println(values.getClass());
}
}
class Car {
}
class Person {
}
- k-v 为了方便程序员的遍历,还会创建 EntrySet集合,该集合存放的元素类型是 Entry**。**
- 而一个 Entry对象就有 k,v。 EntrySet<Entry<K,V>> 即:public Set<Map.Entry<K,V>> entrySet()
- entrySet的运行类型
- 在entrySet中,定义的类型是 Map.Entry,但是实际上存放的还是 HashMap$Node
- 为什么能够放进去,这是因为 HashMap$Node implements Map.Entry
- **即底层代码为:**static class Node<K,V> implements Map.Entry<K,V>
- 这样当把 HashMap$Node 对象存放到 entrySet后**,就方便我们的遍历了,因为Map.Entry提供了重要方法**
- K getKey(); V getValue();
- 向下转型就是因为****HashMap$Node implements Entry
- entrySet是指向table表的
Map真正的结构:table表(数组+链表+红黑树),将每一个Node封装成一个entry,然后再放入entrySet集合
Map接口常用方法
package com.zanedu.map_;
import java.util.HashMap;
import java.util.Map;
@SuppressWarnings({"all"})
public class MapMethod {
public static void main(String[] args) {
//演示map接口常用方法
Map map = new HashMap();
map.put("邓超", new Book("", 100));//OK
map.put("邓超", "孙俪");//替换-> 一会分析源码
map.put("王宝强", "马蓉");//OK
map.put("宋喆", "马蓉");//OK
map.put("刘令博", null);//OK
map.put(null, "刘亦菲");//OK
map.put("鹿晗", "关晓彤");//OK
map.put("zan", "zan的老婆");
System.out.println(map);
//remove:根据键key删除映射
map.remove(null);
System.out.println(map);
//get:根据键获取值
System.out.println(map.get("鹿晗"));
//size:获取元素个数
System.out.println(map.size());
//isEmpty:判断个数是否为0
System.out.println(map.isEmpty());
//clear:清除
// map.clear();
System.out.println(map);
//containsKey:查找键是否存在
System.out.println(map.containsKey("zan"));
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
Map接口遍历方法
- Map遍历的示意图(比List和Set复杂点,但是基本原理一样)
- containsKey:查找键是否存在
- keySet:获取所有的键,key,get(key)
- entrySet:获取所有关系 k-v,getKey(),getValue()
- values:获取所有的值
package com.zanedu.map_;
import java.util.*;
@SuppressWarnings({"all"})
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超", "孙俪");
map.put("王宝强", "马蓉");
map.put("宋喆", "马蓉");
map.put("刘令博", null);
map.put(null, "刘亦菲");
map.put("鹿晗", "关晓彤");
//第一组:先取出所有的Key,再通过Key取出对应的Value
Set keyset = map.keySet();
//(1)增强for
System.out.println("===第一种方式===");
for (Object key : keyset) {
System.out.println(key + "-" + map.get(key));
}
//(2)迭代器
System.out.println("===第二种方式===");
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
//第二组:把所有的 Values取出
Collection values = map.values();
//这里可以使用所有Collection使用的遍历方法
//(1)增强for
System.out.println("===取出所有的value===");
for (Object value : values) {
System.out.println(value);
}
//(2)迭代器
while (iterator.hasNext()) {
Object value = iterator.next();
System.out.println(value);
}
//第三种:通过EntrySet来获取k-v
Set entrySet = map.entrySet();//EntrySet<Entry<K,V>>
//(1)增强for
System.out.println("===使用EntrySet的增强for");
for (Object entry : entrySet) {
//将entry转成 Map.Entry
Map.Entry m = (Map.Entry)entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
//(2)迭代器
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()) {
Object entry = iterator1.next();
// System.out.println(entry.getClass());//HashMap$Node ->实现 Map.Entry
//向下转型 -> Map.Entry
Map.Entry m2 = (Map.Entry)entry;
System.out.println(m2.getKey() + "-" + m2.getValue());
}
}
}
Map接口课堂练习
使用HashMap添加3个员工对象
需求:键:员工id,值:员工对象
遍历显示工资 > 18000的员工
员工类:姓名、工资、员工id
package com.zanedu.map_;
import java.util.*;
@SuppressWarnings({"all"})
public class MapExercise {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put(1, new Employee("jack", 20000, 1));
hashMap.put(2, new Employee("tom", 30000, 2));
hashMap.put(3, new Employee("smith", 10000, 3));
//遍历 - 两种方式
//1. 使用keySet -> 增强for
Set keySet = hashMap.keySet();
for (Object key : keySet) {
//先获取value
Employee employee = (Employee) hashMap.get(key);
if (employee.getSal() > 18000) {
System.out.println(employee);
}
}
//2. 使用 EntrySet -> 迭代器
Set entrySet = hashMap.entrySet();
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
//通过entry取得key和value
Employee employee = (Employee) entry.getValue();
if (employee.getSal() > 18000) {
System.out.println(employee);
}
}
}
}
class Employee {
private String name;
private double sal;
private int id;
public Employee(String name, double sal, int id) {
this.name = name;
this.sal = sal;
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getSal() {
return sal;
}
public void setSal(double sal) {
this.sal = sal;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", sal=" + sal +
", id='" + id + '\'' +
'}';
}
}
Map接口实现类-HashMap
HashMap小结
- Map接口的常用实现类:HashMap、Hashtable、Properties
- HashMap是Map接口使用频率最高的实现类
- HashMap是以 key-value对的方式来存储数据(HashMap$Node类型)
- key不能重复,但是值可以重复,允许使用null键和null值
- 如果添加相同的key,则会覆盖原来的key-val,等同于修改(key不会替换,value会替换)
- 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的(JDK8的HashMap底层:数组+链表+红黑树)
- HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
HashMap底层机制及源码剖析
- 示意图
- (k, v)是一个Node实现了Map.Entry<K, V>
- JDK7.0的HashMap底层实现是【数组+链表】,JDK8.0底层【数组+链表+红黑树】
- 结论:扩容机制(和HashSet相同)
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75
- 当添加key-val时,通过key的hash值得到在table表上的索引,然后判断该索引处是否有元素,如果们没有元素则直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换val;如果不相等则需要判断是树结构还是链表结构,作出相应处理。如果添加时发现容量不够,则需要扩容
- 第一次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)
- 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推
- 在Java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table表的大小 >= MIN_TERRIFY_CAPACITY(默认64),就会进行树化(转成红黑树)
package com.zanedu.map_;
import java.util.HashMap;
@SuppressWarnings({"all"})
public class HashMapSource1 {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put("java", 10);//ok
map.put("php", 10);//ok
map.put("java", 20);//替换 value
System.out.println("map=" + map);
/*
解读
1. 执行构造器 new HashMap()
初始化加载因子 loadfactor = 0.75
HashMap$Node[] table = null
2. 执行put,会调用hash方法,计算key的hash值 h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) { //K = "java", value = 10
return putVal(hash(key), key, value, false, true);
}
3. 执行 putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;//辅助变量
//如果底层的table数组为null,或者length = 0,就扩容到16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//取出hash值对应的table表的索引位置的Node节点,如果为null,就直接把加入的k-v
创建成一个Node,加入到该位置即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;//辅助变量
//如果table表的索引位置的key的hash值相同和新的key的hash值相同,
//并且满足(table表中现有的节点的key和准备添加的key是同一个对象 || equals返回true)
//就认为不能加入新的k-v
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode) //如果当前的table已有的Node是红黑树,就按照红黑树的方式处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果找到的节点,后面是链表,就循环比较
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //如果整个链表没有和它相同的,就加到该链表的最后
p.next = newNode(hash, key, value, null);
//加入后,判断当前链表的个数,是否已经到达8个,到8个后,就调用treeifyBin方法,进行红黑树的树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && //如果在循环比较过程中,发现有相同的,就break,就只是替换下value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; //替换key对应的值
afterNodeAccess(e);
return oldValue;
}
}
++modCount; //每增加一个Node,就size++
if (++size > threshold) //[12-24-48],如果size > 临界值,就扩容
resize();
afterNodeInsertion(evict);
return null;
}
5. 关于树化(转成红黑树)
//如果table表为null,或者大小 < 64,就暂时不树化,而是进行扩容
//否则才会真正的树化 -> 剪枝,即将树转换成链表
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
*/
}
}
- (1) 执行构造器 new HashMap(),初始化加载因子 loadfactor = 0.75,HashMap$Node[] table = null
- (2)执行put,会调用hash方法,计算key的hash值 h = key.hashCode()) ^ (h >>> 16)
- **(3)**执行 putVal
- 补充:关于树化
- 如果table表为null,或者大小 < 64,就暂时不树化,而是进行扩容,否则才会真正的树化 -> 剪枝 = 即将树转换成链表
- 模拟HashMap触发扩容、树化的情况
package com.zanedu.map_;
import java.util.HashMap;
public class HashMapSource2 {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
for (int i = 1; i <= 12; i++) {
hashMap.put(new A(i), "hello");
}
hashMap.put("aaa", "bbb");
System.out.println(hashMap);//12个 k-v
//验证table的扩容机制
//0 -> 16(12) -> 32(24) -> 64(48) -> 128(96)
}
}
class A {
private int num;
public A(int num) {
this.num = num;
}
//所有A对象的hashCode都是100
// @Override
// public int hashCode() {
// return 100;
// }
@Override
public String toString() {
return "A{" +
"num=" + num +
'}';
}
}
- 由于树化需要一条链表的数据为8,因此将其的hashCode重写,保证hash值一样,能在同一条链表上
- 看一下扩容情况:
- 已经在一条链表上加了8条数据,在加入数据看是扩容,还是树化?
- 发现其table表数据没有64个,因此扩容
- 继续添加,继续扩容
- 当扩容到64个时,我们发现再次添加数据,会进行树化
- 发现其多了prev、left、right等等变量
Map接口实现类-Hashtable
Hashtable的基本介绍
- 存放的元素是键值对:即 K-V
- Hashtable的键和值都不能为null,否则会抛出NullPointerException
- Hashtable的使用方法基本和HashMap一样
- Hashtable是线程安全的(synchronized),而HashMap是线程不安全的
package com.zanedu.map_;
import java.util.Hashtable;
@SuppressWarnings("all")
public class HashTableExercise {
public static void main(String[] args) {
Hashtable table = new Hashtable();//ok
table.put("john", 100); //ok
//table.put(null, 100); //异常 NullPointerException
//table.put("john", null);//异常 NullPointerException
table.put("lucy", 100);//ok
table.put("lic", 100);//ok
table.put("lic", 88);//替换
table.put("hello1", 1);
table.put("hello2", 1);
table.put("hello3", 1);
table.put("hello4", 1);
table.put("hello5", 1);
table.put("hello6", 1);
System.out.println(table);
/*
简单说明以下Hashtable的底层
1. 底层有一个数组 Hashtable$Entry[] 初始化大小为11
2. threshold 临界值是 8 = 11 * 0.75
3. 扩容:按照自己的扩容机制来进行即可
4. 执行方法 addEntry(hash, key, value, index); 添加一个K-V,封装到Entry
5. 当 if (count >= threshold) 满足时,就进行扩容
6. 按照 int newCapacity = (oldCapacity << 1) + 1;的大小扩容
*/
}
}
- 看一下底层的扩容机制
- 发现:初始化大小为11,并且之后扩容都是 *2+1
- 扩容就是开辟一个以newCapcacity大小的新Entry数组
Hashtable和HashMap对比
Map接口实现类-Properties
基本介绍
- Properties类继承了Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据
- 他的使用特点和Hashtable类似
- Properties还可以用于从 xxx.properties文件中,加载数据到Properties类对象并进行读取和修改
基本使用
package com.zanedu.map_;
import java.util.Properties;
@SuppressWarnings({"all"})
public class Properties_ {
public static void main(String[] args) {
//解读
/*
1. Properties 继承 Hashtable
2. 可以通过 k-v 存放数据,当然 key 和 value不能为null
*/
//增加
Properties properties = new Properties();
properties.put("john", 100);
// properties.put(null, 100);//抛出空指针异常 NullPointerException
// properties.put("john", null);//抛出空指针异常 NullPointerException
properties.put("lucy", 100);
properties.put("lic", 100);
properties.put("lic", 80);//如果有相同的 key,value会被替换
System.out.println(properties);
//通过k 获取对应的value
System.out.println(properties.get("lic"));//80
System.out.println(properties.getProperty("lic"));//80
//删除
properties.remove("lic");
System.out.println(properties);
//修改
properties.put("john", "雨涵");
System.out.println(properties);
}
}
Map接口实现类-TreeMap
package com.zanedu.map_;
import java.util.Comparator;
import java.util.TreeMap;
@SuppressWarnings({"all"})
public class TreeMap_ {
public static void main(String[] args) {
//使用默认的构造器创建 TreeMap,会默认排序,按照首字母排序
//要求:
/*
按照传入的 k(String) 的大小进行排序
按照传入的 k(String) 的长度来排序
*/
// TreeMap treeMap = new TreeMap();
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
// return ((String)o2).compareTo((String)o1);
return ((String)o1).length() - ((String)o2).length();
}
});
treeMap.put("jack", "杰克");
treeMap.put("tom", "汤姆");
treeMap.put("kristina", "克瑞斯提诺");
treeMap.put("smith", "斯密斯");
treeMap.put("zan", "老张");//加入不了,因为是长度添加,相等为0了,直接返回,但是value值会被替换
System.out.println(treeMap);
/*
解读源码
1. 构造器,把传入的实现类Comparator接口的匿名内部类(对象),传给了TreeMap的comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 调用put方法
2.1第一次添加:把k-v 封装到 Entry对象,放入root
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check,检测是否为空,为空就抛异常
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
2.2 以后添加
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do { //遍历所有的key,给当前key找到适当的位置
parent = t;
cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的compare
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果遍历过程中,发现准备添加Key和当前已有的Key相等,就不添加,直接返回
return t.setValue(value);
} while (t != null);
}
*/
}
}
总结-开发中如何选择集合实现类(记住)
- 在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择
1)先判断存储的类型(一组对象[单列] 或 一组键值对[双列])
2)一组对象[单列]:Collection接口
允许重复:List
增删多:LinkedList(底层维护了一个双向链表)
改查多:ArrayList(底层维护Object类型的可变数组)
不允许重复:Set
无序:HashSet(底层是HashMap,维护了一个哈希表,即数组+链表+红黑树)
排序:TreeSet
插入和取出顺序一致:LinkedHashSet(维护数组+双向链表)
3)一组键值对[双列]:Map
键无序:HashMap(底层是哈希表,jdk7:数组+链表,jdk8:数组+链表+红黑树)
键排序:TreeMap
键插入和取出的顺序一致:LinkedHashMap
读取文件:Properties
Collections工具类
Collections工具类介绍
- Collections是一个操作 Set、List和Map等集合的工具类
- Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
Collections各种方法演示
- reverse(List):反转List中元素的顺序
- shuffle(List):对List集合元素进行随机排序
- sort(List):根据元素的自然顺序对指定List集合元素按升序排序
- sort(List, Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
- swap(List, int, int):将指定List集合中的第 i 处元素和第 j 处元素进行交换
- Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
- Object max(Collection, Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
- Object min(Collection)
- Object min(Collection, Object)
- int frequency(Collection, Object):返回指定集合中指定元素的出现次数
- void copy(List dest, List src):将src中的内容复制到dest中
- boolean replaceAll(List list, Object oldVal, Object newVal):使用新值来替换List对象的所有旧值
package com.zanedu.collections_;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@SuppressWarnings({"all"})
public class Collections_ {
public static void main(String[] args) {
//创建ArrayList集合,用于测试
List list = new ArrayList();
list.add("tom");
list.add("smith");
list.add("king");
list.add("milan");
// reverse(List):反转 List 中元素的顺序
Collections.reverse(list);
System.out.println(list);
// shuffle(List):对 List 集合元素进行随机排序
// for (int i = 0; i < 5; i++) {
// Collections.shuffle(list);
// System.out.println(list);
// }
// sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
Collections.sort(list);
System.out.println("自然排序后=" + list);
// sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
//按照字符串的长度大小来排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//可以加入校验代码
// if (o1 instanceof String) {
//
// }
return ((String)o1).length() - ((String)o2).length();
}
});
System.out.println("按照字符串长度大小排序=" + list);
// swap(List, int,int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Collections.swap(list, 0, 1);
System.out.println("交换后=" + list);
//Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
System.out.println(Collections.max(list));
//Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
Object max = Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length() - ((String)o2).length();
}
});
System.out.println(max);
//Object min(Collection)
//Object min(Collection,Comparator)
//参照上面min即可
//int frequency(Collection,Object):返回指定集合中指定元素的出现次数
System.out.println(Collections.frequency(list, "milan"));
//void copy(List dest,List src):将src中的内容复制到dest中
List dest = new ArrayList();
//为了完成一个完整的拷贝,需要先给dest赋值,大小和list.size()一样
for (int i = 0; i < list.size(); i++) {
dest.add("");
}
//拷贝
Collections.copy(dest, list);
System.out.println(dest);
//boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
//如果list中,有tom就替换成 汤姆
Collections.replaceAll(list, "tom", "汤姆");
System.out.println(list);
}
}